From 5869f285ead8d808833b3357fd8853ef7c5a1d14 Mon Sep 17 00:00:00 2001 From: Farhan Syah Date: Wed, 21 Oct 2026 10:53:38 +0800 Subject: [PATCH 01/12] docs: add contributor assignment agreement and update copyright holder Add CLA.md and point CONTRIBUTING.md and the pull request template at it. Change the BUSL-1.1 licensor and copyright line to Biz Impulse Solutions Sdn. Bhd. Add a NOTICE file to each Apache-2.0 library crate. --- .github/PULL_REQUEST_TEMPLATE.md | 1 + CLA.md | 25 +++++++++++++++++++++++++ CONTRIBUTING.md | 10 +++++++--- LICENSE | 4 ++-- nodedb-array/NOTICE | 2 ++ nodedb-client/NOTICE | 2 ++ nodedb-codec/NOTICE | 2 ++ nodedb-columnar/NOTICE | 2 ++ nodedb-crdt/NOTICE | 2 ++ nodedb-fts/NOTICE | 2 ++ nodedb-graph/NOTICE | 2 ++ nodedb-mem/NOTICE | 2 ++ nodedb-physical/NOTICE | 2 ++ nodedb-query/NOTICE | 2 ++ nodedb-spatial/NOTICE | 2 ++ nodedb-sql/NOTICE | 2 ++ nodedb-strict/NOTICE | 2 ++ nodedb-types/NOTICE | 2 ++ nodedb-vector-gpu/NOTICE | 2 ++ nodedb-vector/NOTICE | 2 ++ nodedb-wal/NOTICE | 2 ++ 21 files changed, 69 insertions(+), 5 deletions(-) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 CLA.md create mode 100644 nodedb-array/NOTICE create mode 100644 nodedb-client/NOTICE create mode 100644 nodedb-codec/NOTICE create mode 100644 nodedb-columnar/NOTICE create mode 100644 nodedb-crdt/NOTICE create mode 100644 nodedb-fts/NOTICE create mode 100644 nodedb-graph/NOTICE create mode 100644 nodedb-mem/NOTICE create mode 100644 nodedb-physical/NOTICE create mode 100644 nodedb-query/NOTICE create mode 100644 nodedb-spatial/NOTICE create mode 100644 nodedb-sql/NOTICE create mode 100644 nodedb-strict/NOTICE create mode 100644 nodedb-types/NOTICE create mode 100644 nodedb-vector-gpu/NOTICE create mode 100644 nodedb-vector/NOTICE create mode 100644 nodedb-wal/NOTICE diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..426d166ab --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1 @@ +- [ ] I have signed the Contributor Assignment Agreement. diff --git a/CLA.md b/CLA.md new file mode 100644 index 000000000..02c610323 --- /dev/null +++ b/CLA.md @@ -0,0 +1,25 @@ +# CONTRIBUTOR ASSIGNMENT AGREEMENT + +**NodeDB** + +Date: the date on which the Contributor signs through the CLA assistant. + +**PARTIES.** (1) **the person who signs through the CLA assistant, identified by their GitHub account** (the **"Contributor"**); and (2) **BIZ IMPULSE SOLUTIONS SDN. BHD.** (Company Registration No. 202201016965 (1462662-D)) (the **"Company"**). + +**1. Contributions.** The Contributor has contributed or will contribute code, documentation and other material to the repositories at github.com/NodeDB-Lab/nodedb and github.com/NodeDB-Lab/nodedb-studio under the GitHub account through which the Contributor signs, from the first contribution to the date above, and will contribute more. Those contributions, and every later contribution that the Contributor submits to a repository of the Company, are the **"Contributions"**. + +**2. Assignment.** In consideration of RM1.00, which the Contributor acknowledges receiving, and of the Company's promises in clauses 3 and 4, the Contributor sells and assigns to the Company absolutely all copyright and other intellectual property rights in the Contributions, including the right to sue for any past infringement. The parties intend this to be a sale for that consideration and not a gift. The assignment of each Contribution takes effect when it is made. This document is in writing, as section 27(3) of the Copyright Act 1987 requires, and section 27(6) of that Act allows the assignment of future works. + +**3. Licence back.** The Company grants the Contributor a perpetual, irrevocable, worldwide, royalty-free, non-exclusive licence to use, copy, modify and distribute the Contributions for any purpose. The Contributions stay available to the public under their published licence. + +**4. Credit and moral rights.** The Company will keep the Contributor's name in the project's commit history and credits. For the purposes of section 25 of the Copyright Act 1987, the Contributor consents to the modification and adaptation of the Contributions. + +**5. Confirmation.** The Contributor confirms that: + +(a) the Contributions are the Contributor's own original work, or the Contributor has the right to assign them; + +(b) no employer or other person owns or claims the Contributions, or that person has consented in writing to the assignment in clause 2 and has given the Company a copy of its consent; and + +(c) the Contributor knows of no third party right that the Contributions infringe. + +**6. General.** No other payment is due. The Company will pay the stamp duty on this document and the cost of preparing it, and will stamp it within thirty (30) days after it is signed. The Contributor is not obliged to contribute again. The Contributor will, at the Company's request and cost, sign the documents that the Company reasonably requires to perfect its title. This document is governed by the laws of Malaysia, and the courts of Malaysia have exclusive jurisdiction. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 652651b20..1eeac9d65 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -31,7 +31,7 @@ These planes communicate only through bounded lock-free bridges: **Mixing planes is a correctness bug, not a style issue.** Spawning a Tokio task from Data Plane code, or calling io_uring from Control Plane code, will be rejected in review regardless of how small the change is. -Before writing any Data Plane or engine code, read `nodedb/CLAUDE.md` in full. It covers the plane rules, engine routing via `EngineRules`, serialization requirements, error handling conventions, and file size limits — all of which are enforced in review. +Before writing any Data Plane or engine code, follow these rules. Review enforces them: plane separation, engine routing through `EngineRules`, MessagePack for internal transport, typed errors, and the 500-line file limit. --- @@ -106,7 +106,7 @@ cargo nextest run --all-features # tests cargo deny check # dependency audit ``` -Key rules (full list in `nodedb/CLAUDE.md`): +Key rules: - No `.unwrap()` in library code — use `thiserror`, propagate errors - No `Result` — always typed error enums @@ -234,7 +234,11 @@ NodeDB uses a mixed-license model for its runtime and library crates: | `nodedb-types`, `nodedb-client`, `nodedb-query`, `nodedb-codec`, `nodedb-spatial`, `nodedb-graph`, `nodedb-vector`, `nodedb-vector-gpu`, `nodedb-fts`, `nodedb-strict`, `nodedb-columnar`, `nodedb-array`, `nodedb-sql`, `nodedb-physical`, `nodedb-wal`, `nodedb-mem`, `nodedb-crdt` | [Apache 2.0](LICENSE.APACHE-2.0) | | `nodedb` (server), `nodedb-bridge`, `nodedb-raft`, `nodedb-cluster` | [BUSL-1.1](LICENSE) | -**What this means for contributors:** contributions to Apache-2.0 crates are under Apache 2.0; contributions to server crates are under BUSL-1.1. No CLA is required for either. +**What this means for contributors:** you sign the Contributor Assignment Agreement (the CLA) once, before your first pull request merges. It assigns the copyright in your contributions to Biz Impulse Solutions Sdn. Bhd. and gives you a licence back to use your own code for any purpose. The code stays public under the licence of its crate: Apache 2.0 for the library crates, BUSL-1.1 for the server crates. + +## Contributor Assignment Agreement + +Our CLA assistant comments on your first pull request with a link to sign. You sign once for all later pull requests. The agreement is [CLA.md](CLA.md) in this repository. If you write code for an employer, your employer must consent in the form the agreement provides. **What this means for SDK/tool builders:** you can depend on the Apache-2.0 crates freely in your own open-source or commercial projects. The BUSL restriction only applies to the server crates — you are not affected unless you are redistributing the NodeDB server itself as a hosted service. diff --git a/LICENSE b/LICENSE index 6a32b2084..f26e8554a 100644 --- a/LICENSE +++ b/LICENSE @@ -2,9 +2,9 @@ Business Source License 1.1 Parameters -Licensor: Farhan Syah +Licensor: Biz Impulse Solutions Sdn. Bhd. Licensed Work: NodeDB - The Licensed Work is (c) 2026 Farhan Syah + The Licensed Work is (c) 2026 Biz Impulse Solutions Sdn. Bhd. Change Date: 2030-05-01 Change License: Apache License, Version 2.0 diff --git a/nodedb-array/NOTICE b/nodedb-array/NOTICE new file mode 100644 index 000000000..4bf4bfc89 --- /dev/null +++ b/nodedb-array/NOTICE @@ -0,0 +1,2 @@ +Copyright 2026 Biz Impulse Solutions Sdn. Bhd. and the NodeDB contributors. +This product is licensed under the Apache License, Version 2.0. diff --git a/nodedb-client/NOTICE b/nodedb-client/NOTICE new file mode 100644 index 000000000..4bf4bfc89 --- /dev/null +++ b/nodedb-client/NOTICE @@ -0,0 +1,2 @@ +Copyright 2026 Biz Impulse Solutions Sdn. Bhd. and the NodeDB contributors. +This product is licensed under the Apache License, Version 2.0. diff --git a/nodedb-codec/NOTICE b/nodedb-codec/NOTICE new file mode 100644 index 000000000..4bf4bfc89 --- /dev/null +++ b/nodedb-codec/NOTICE @@ -0,0 +1,2 @@ +Copyright 2026 Biz Impulse Solutions Sdn. Bhd. and the NodeDB contributors. +This product is licensed under the Apache License, Version 2.0. diff --git a/nodedb-columnar/NOTICE b/nodedb-columnar/NOTICE new file mode 100644 index 000000000..4bf4bfc89 --- /dev/null +++ b/nodedb-columnar/NOTICE @@ -0,0 +1,2 @@ +Copyright 2026 Biz Impulse Solutions Sdn. Bhd. and the NodeDB contributors. +This product is licensed under the Apache License, Version 2.0. diff --git a/nodedb-crdt/NOTICE b/nodedb-crdt/NOTICE new file mode 100644 index 000000000..4bf4bfc89 --- /dev/null +++ b/nodedb-crdt/NOTICE @@ -0,0 +1,2 @@ +Copyright 2026 Biz Impulse Solutions Sdn. Bhd. and the NodeDB contributors. +This product is licensed under the Apache License, Version 2.0. diff --git a/nodedb-fts/NOTICE b/nodedb-fts/NOTICE new file mode 100644 index 000000000..4bf4bfc89 --- /dev/null +++ b/nodedb-fts/NOTICE @@ -0,0 +1,2 @@ +Copyright 2026 Biz Impulse Solutions Sdn. Bhd. and the NodeDB contributors. +This product is licensed under the Apache License, Version 2.0. diff --git a/nodedb-graph/NOTICE b/nodedb-graph/NOTICE new file mode 100644 index 000000000..4bf4bfc89 --- /dev/null +++ b/nodedb-graph/NOTICE @@ -0,0 +1,2 @@ +Copyright 2026 Biz Impulse Solutions Sdn. Bhd. and the NodeDB contributors. +This product is licensed under the Apache License, Version 2.0. diff --git a/nodedb-mem/NOTICE b/nodedb-mem/NOTICE new file mode 100644 index 000000000..4bf4bfc89 --- /dev/null +++ b/nodedb-mem/NOTICE @@ -0,0 +1,2 @@ +Copyright 2026 Biz Impulse Solutions Sdn. Bhd. and the NodeDB contributors. +This product is licensed under the Apache License, Version 2.0. diff --git a/nodedb-physical/NOTICE b/nodedb-physical/NOTICE new file mode 100644 index 000000000..4bf4bfc89 --- /dev/null +++ b/nodedb-physical/NOTICE @@ -0,0 +1,2 @@ +Copyright 2026 Biz Impulse Solutions Sdn. Bhd. and the NodeDB contributors. +This product is licensed under the Apache License, Version 2.0. diff --git a/nodedb-query/NOTICE b/nodedb-query/NOTICE new file mode 100644 index 000000000..4bf4bfc89 --- /dev/null +++ b/nodedb-query/NOTICE @@ -0,0 +1,2 @@ +Copyright 2026 Biz Impulse Solutions Sdn. Bhd. and the NodeDB contributors. +This product is licensed under the Apache License, Version 2.0. diff --git a/nodedb-spatial/NOTICE b/nodedb-spatial/NOTICE new file mode 100644 index 000000000..4bf4bfc89 --- /dev/null +++ b/nodedb-spatial/NOTICE @@ -0,0 +1,2 @@ +Copyright 2026 Biz Impulse Solutions Sdn. Bhd. and the NodeDB contributors. +This product is licensed under the Apache License, Version 2.0. diff --git a/nodedb-sql/NOTICE b/nodedb-sql/NOTICE new file mode 100644 index 000000000..4bf4bfc89 --- /dev/null +++ b/nodedb-sql/NOTICE @@ -0,0 +1,2 @@ +Copyright 2026 Biz Impulse Solutions Sdn. Bhd. and the NodeDB contributors. +This product is licensed under the Apache License, Version 2.0. diff --git a/nodedb-strict/NOTICE b/nodedb-strict/NOTICE new file mode 100644 index 000000000..4bf4bfc89 --- /dev/null +++ b/nodedb-strict/NOTICE @@ -0,0 +1,2 @@ +Copyright 2026 Biz Impulse Solutions Sdn. Bhd. and the NodeDB contributors. +This product is licensed under the Apache License, Version 2.0. diff --git a/nodedb-types/NOTICE b/nodedb-types/NOTICE new file mode 100644 index 000000000..4bf4bfc89 --- /dev/null +++ b/nodedb-types/NOTICE @@ -0,0 +1,2 @@ +Copyright 2026 Biz Impulse Solutions Sdn. Bhd. and the NodeDB contributors. +This product is licensed under the Apache License, Version 2.0. diff --git a/nodedb-vector-gpu/NOTICE b/nodedb-vector-gpu/NOTICE new file mode 100644 index 000000000..4bf4bfc89 --- /dev/null +++ b/nodedb-vector-gpu/NOTICE @@ -0,0 +1,2 @@ +Copyright 2026 Biz Impulse Solutions Sdn. Bhd. and the NodeDB contributors. +This product is licensed under the Apache License, Version 2.0. diff --git a/nodedb-vector/NOTICE b/nodedb-vector/NOTICE new file mode 100644 index 000000000..4bf4bfc89 --- /dev/null +++ b/nodedb-vector/NOTICE @@ -0,0 +1,2 @@ +Copyright 2026 Biz Impulse Solutions Sdn. Bhd. and the NodeDB contributors. +This product is licensed under the Apache License, Version 2.0. diff --git a/nodedb-wal/NOTICE b/nodedb-wal/NOTICE new file mode 100644 index 000000000..4bf4bfc89 --- /dev/null +++ b/nodedb-wal/NOTICE @@ -0,0 +1,2 @@ +Copyright 2026 Biz Impulse Solutions Sdn. Bhd. and the NodeDB contributors. +This product is licensed under the Apache License, Version 2.0. From b622a1a8901e27ebdbb586d8db4a26447220a894 Mon Sep 17 00:00:00 2001 From: Farhan Syah Date: Wed, 21 Oct 2026 10:55:12 +0800 Subject: [PATCH 02/12] chore: set crate authors to Biz Impulse Solutions and the NodeDB contributors --- Cargo.toml | 1 + nodedb-array/Cargo.toml | 1 + nodedb-bridge/Cargo.toml | 1 + nodedb-client-tests/Cargo.toml | 1 + nodedb-client/Cargo.toml | 1 + nodedb-cluster-tests/Cargo.toml | 1 + nodedb-cluster/Cargo.toml | 1 + nodedb-codec/Cargo.toml | 1 + nodedb-columnar/Cargo.toml | 1 + nodedb-crdt/Cargo.toml | 1 + nodedb-fts/Cargo.toml | 1 + nodedb-graph/Cargo.toml | 1 + nodedb-mem/Cargo.toml | 1 + nodedb-physical/Cargo.toml | 1 + nodedb-query/Cargo.toml | 1 + nodedb-raft/Cargo.toml | 1 + nodedb-raster/Cargo.toml | 1 + nodedb-spatial/Cargo.toml | 1 + nodedb-sql/Cargo.toml | 1 + nodedb-strict/Cargo.toml | 1 + nodedb-test-support/Cargo.toml | 1 + nodedb-types/Cargo.toml | 1 + nodedb-vector-gpu/Cargo.toml | 1 + nodedb-vector/Cargo.toml | 1 + nodedb-wal/Cargo.toml | 1 + nodedb/Cargo.toml | 1 + 26 files changed, 26 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 0cec15a63..8613149b0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,7 @@ rust-version = "1.96" license = "BUSL-1.1" readme = "README.md" repository = "https://github.com/NodeDB-Lab/nodedb" +authors = ["Biz Impulse Solutions Sdn. Bhd. and the NodeDB contributors"] homepage = "https://nodedb.dev" documentation = "https://docs.rs/nodedb" diff --git a/nodedb-array/Cargo.toml b/nodedb-array/Cargo.toml index 315332b3f..431c6365b 100644 --- a/nodedb-array/Cargo.toml +++ b/nodedb-array/Cargo.toml @@ -5,6 +5,7 @@ version.workspace = true edition.workspace = true rust-version.workspace = true license = "Apache-2.0" +authors = ["Biz Impulse Solutions Sdn. Bhd. and the NodeDB contributors"] readme = "README.md" repository.workspace = true homepage.workspace = true diff --git a/nodedb-bridge/Cargo.toml b/nodedb-bridge/Cargo.toml index 56d2cc32d..9d58291ec 100644 --- a/nodedb-bridge/Cargo.toml +++ b/nodedb-bridge/Cargo.toml @@ -5,6 +5,7 @@ version.workspace = true edition.workspace = true rust-version.workspace = true license.workspace = true +authors.workspace = true readme = "README.md" repository.workspace = true homepage.workspace = true diff --git a/nodedb-client-tests/Cargo.toml b/nodedb-client-tests/Cargo.toml index d022de19f..850be729b 100644 --- a/nodedb-client-tests/Cargo.toml +++ b/nodedb-client-tests/Cargo.toml @@ -4,6 +4,7 @@ version.workspace = true edition.workspace = true rust-version.workspace = true license.workspace = true +authors.workspace = true publish = false description = "End-to-end integration tests for nodedb-client. Spins up a real server via nodedb-test-support and exercises NodeDbRemote / NativeClient round-trips." diff --git a/nodedb-client/Cargo.toml b/nodedb-client/Cargo.toml index 90dfb3f9a..ba0ba399b 100644 --- a/nodedb-client/Cargo.toml +++ b/nodedb-client/Cargo.toml @@ -5,6 +5,7 @@ version.workspace = true edition.workspace = true rust-version.workspace = true license = "Apache-2.0" +authors = ["Biz Impulse Solutions Sdn. Bhd. and the NodeDB contributors"] readme = "README.md" repository.workspace = true homepage.workspace = true diff --git a/nodedb-cluster-tests/Cargo.toml b/nodedb-cluster-tests/Cargo.toml index 2ddb736c0..db1073ec4 100644 --- a/nodedb-cluster-tests/Cargo.toml +++ b/nodedb-cluster-tests/Cargo.toml @@ -4,6 +4,7 @@ version.workspace = true edition.workspace = true rust-version.workspace = true license.workspace = true +authors.workspace = true publish = false description = "3-node integration test suite for NodeDB. Heavy; runs in its own CI stage." diff --git a/nodedb-cluster/Cargo.toml b/nodedb-cluster/Cargo.toml index c8c04a79b..b00d9a661 100644 --- a/nodedb-cluster/Cargo.toml +++ b/nodedb-cluster/Cargo.toml @@ -5,6 +5,7 @@ version.workspace = true edition.workspace = true rust-version.workspace = true license.workspace = true +authors.workspace = true readme = "README.md" repository.workspace = true homepage.workspace = true diff --git a/nodedb-codec/Cargo.toml b/nodedb-codec/Cargo.toml index 712a4a6a2..dba0c82a5 100644 --- a/nodedb-codec/Cargo.toml +++ b/nodedb-codec/Cargo.toml @@ -5,6 +5,7 @@ version.workspace = true edition.workspace = true rust-version.workspace = true license = "Apache-2.0" +authors = ["Biz Impulse Solutions Sdn. Bhd. and the NodeDB contributors"] readme = "README.md" repository.workspace = true homepage.workspace = true diff --git a/nodedb-columnar/Cargo.toml b/nodedb-columnar/Cargo.toml index 69d2727ca..bb7f09ebc 100644 --- a/nodedb-columnar/Cargo.toml +++ b/nodedb-columnar/Cargo.toml @@ -5,6 +5,7 @@ version.workspace = true edition.workspace = true rust-version.workspace = true license = "Apache-2.0" +authors = ["Biz Impulse Solutions Sdn. Bhd. and the NodeDB contributors"] readme = "README.md" repository.workspace = true homepage.workspace = true diff --git a/nodedb-crdt/Cargo.toml b/nodedb-crdt/Cargo.toml index e5e100bf7..5cba16cb5 100644 --- a/nodedb-crdt/Cargo.toml +++ b/nodedb-crdt/Cargo.toml @@ -5,6 +5,7 @@ version.workspace = true edition.workspace = true rust-version.workspace = true license = "Apache-2.0" +authors = ["Biz Impulse Solutions Sdn. Bhd. and the NodeDB contributors"] readme = "README.md" repository.workspace = true homepage.workspace = true diff --git a/nodedb-fts/Cargo.toml b/nodedb-fts/Cargo.toml index 78fd19768..565314f0f 100644 --- a/nodedb-fts/Cargo.toml +++ b/nodedb-fts/Cargo.toml @@ -5,6 +5,7 @@ version.workspace = true edition.workspace = true rust-version.workspace = true license = "Apache-2.0" +authors = ["Biz Impulse Solutions Sdn. Bhd. and the NodeDB contributors"] readme = "README.md" repository.workspace = true homepage.workspace = true diff --git a/nodedb-graph/Cargo.toml b/nodedb-graph/Cargo.toml index 6b2555ca0..50b09aaa1 100644 --- a/nodedb-graph/Cargo.toml +++ b/nodedb-graph/Cargo.toml @@ -5,6 +5,7 @@ version.workspace = true edition.workspace = true rust-version.workspace = true license = "Apache-2.0" +authors = ["Biz Impulse Solutions Sdn. Bhd. and the NodeDB contributors"] readme = "README.md" repository.workspace = true homepage.workspace = true diff --git a/nodedb-mem/Cargo.toml b/nodedb-mem/Cargo.toml index 19d1d79dd..d50deff43 100644 --- a/nodedb-mem/Cargo.toml +++ b/nodedb-mem/Cargo.toml @@ -5,6 +5,7 @@ version.workspace = true edition.workspace = true rust-version.workspace = true license = "Apache-2.0" +authors = ["Biz Impulse Solutions Sdn. Bhd. and the NodeDB contributors"] readme = "README.md" repository.workspace = true homepage.workspace = true diff --git a/nodedb-physical/Cargo.toml b/nodedb-physical/Cargo.toml index 2dca5186c..d8c360e7f 100644 --- a/nodedb-physical/Cargo.toml +++ b/nodedb-physical/Cargo.toml @@ -4,6 +4,7 @@ version.workspace = true edition.workspace = true rust-version.workspace = true license = "Apache-2.0" +authors = ["Biz Impulse Solutions Sdn. Bhd. and the NodeDB contributors"] readme = "README.md" repository.workspace = true homepage.workspace = true diff --git a/nodedb-query/Cargo.toml b/nodedb-query/Cargo.toml index 666178a06..144dec761 100644 --- a/nodedb-query/Cargo.toml +++ b/nodedb-query/Cargo.toml @@ -5,6 +5,7 @@ version.workspace = true edition.workspace = true rust-version.workspace = true license = "Apache-2.0" +authors = ["Biz Impulse Solutions Sdn. Bhd. and the NodeDB contributors"] readme = "README.md" repository.workspace = true homepage.workspace = true diff --git a/nodedb-raft/Cargo.toml b/nodedb-raft/Cargo.toml index 50ac35058..4c99eed5d 100644 --- a/nodedb-raft/Cargo.toml +++ b/nodedb-raft/Cargo.toml @@ -5,6 +5,7 @@ version.workspace = true edition.workspace = true rust-version.workspace = true license.workspace = true +authors.workspace = true readme = "README.md" repository.workspace = true homepage.workspace = true diff --git a/nodedb-raster/Cargo.toml b/nodedb-raster/Cargo.toml index b2c2a63c3..f4d530380 100644 --- a/nodedb-raster/Cargo.toml +++ b/nodedb-raster/Cargo.toml @@ -5,6 +5,7 @@ version.workspace = true edition.workspace = true rust-version.workspace = true license = "Apache-2.0" +authors = ["Biz Impulse Solutions Sdn. Bhd. and the NodeDB contributors"] repository.workspace = true homepage.workspace = true documentation = "https://docs.rs/nodedb-raster" diff --git a/nodedb-spatial/Cargo.toml b/nodedb-spatial/Cargo.toml index 41cd9614a..3336ad7a7 100644 --- a/nodedb-spatial/Cargo.toml +++ b/nodedb-spatial/Cargo.toml @@ -5,6 +5,7 @@ version.workspace = true edition.workspace = true rust-version.workspace = true license = "Apache-2.0" +authors = ["Biz Impulse Solutions Sdn. Bhd. and the NodeDB contributors"] readme = "README.md" repository.workspace = true homepage.workspace = true diff --git a/nodedb-sql/Cargo.toml b/nodedb-sql/Cargo.toml index f06dc6ab9..f6daf7936 100644 --- a/nodedb-sql/Cargo.toml +++ b/nodedb-sql/Cargo.toml @@ -4,6 +4,7 @@ version.workspace = true edition.workspace = true rust-version.workspace = true license = "Apache-2.0" +authors = ["Biz Impulse Solutions Sdn. Bhd. and the NodeDB contributors"] readme = "README.md" repository.workspace = true homepage.workspace = true diff --git a/nodedb-strict/Cargo.toml b/nodedb-strict/Cargo.toml index 469707742..7bfa0cbc9 100644 --- a/nodedb-strict/Cargo.toml +++ b/nodedb-strict/Cargo.toml @@ -5,6 +5,7 @@ version.workspace = true edition.workspace = true rust-version.workspace = true license = "Apache-2.0" +authors = ["Biz Impulse Solutions Sdn. Bhd. and the NodeDB contributors"] readme = "README.md" repository.workspace = true homepage.workspace = true diff --git a/nodedb-test-support/Cargo.toml b/nodedb-test-support/Cargo.toml index ba22e37a3..9db699c65 100644 --- a/nodedb-test-support/Cargo.toml +++ b/nodedb-test-support/Cargo.toml @@ -4,6 +4,7 @@ version.workspace = true edition.workspace = true rust-version.workspace = true license.workspace = true +authors.workspace = true publish = false description = "Shared test harness for NodeDB integration tests (not for end-user consumption)." diff --git a/nodedb-types/Cargo.toml b/nodedb-types/Cargo.toml index 646fcb604..db64dda1a 100644 --- a/nodedb-types/Cargo.toml +++ b/nodedb-types/Cargo.toml @@ -5,6 +5,7 @@ version.workspace = true edition.workspace = true rust-version.workspace = true license = "Apache-2.0" +authors = ["Biz Impulse Solutions Sdn. Bhd. and the NodeDB contributors"] readme = "README.md" repository.workspace = true homepage.workspace = true diff --git a/nodedb-vector-gpu/Cargo.toml b/nodedb-vector-gpu/Cargo.toml index 035e2eaf5..c5e123e85 100644 --- a/nodedb-vector-gpu/Cargo.toml +++ b/nodedb-vector-gpu/Cargo.toml @@ -5,6 +5,7 @@ edition.workspace = true rust-version.workspace = true publish = false license = "Apache-2.0" +authors = ["Biz Impulse Solutions Sdn. Bhd. and the NodeDB contributors"] readme = "README.md" repository.workspace = true homepage.workspace = true diff --git a/nodedb-vector/Cargo.toml b/nodedb-vector/Cargo.toml index 0291ef1ef..7e509960b 100644 --- a/nodedb-vector/Cargo.toml +++ b/nodedb-vector/Cargo.toml @@ -4,6 +4,7 @@ version.workspace = true edition.workspace = true rust-version.workspace = true license = "Apache-2.0" +authors = ["Biz Impulse Solutions Sdn. Bhd. and the NodeDB contributors"] readme = "README.md" repository.workspace = true homepage.workspace = true diff --git a/nodedb-wal/Cargo.toml b/nodedb-wal/Cargo.toml index db6938905..37a135bfa 100644 --- a/nodedb-wal/Cargo.toml +++ b/nodedb-wal/Cargo.toml @@ -5,6 +5,7 @@ version.workspace = true edition.workspace = true rust-version.workspace = true license = "Apache-2.0" +authors = ["Biz Impulse Solutions Sdn. Bhd. and the NodeDB contributors"] readme = "README.md" repository.workspace = true homepage.workspace = true diff --git a/nodedb/Cargo.toml b/nodedb/Cargo.toml index 633e841e6..06109907b 100644 --- a/nodedb/Cargo.toml +++ b/nodedb/Cargo.toml @@ -4,6 +4,7 @@ version.workspace = true edition.workspace = true rust-version.workspace = true license.workspace = true +authors.workspace = true readme = "README.md" repository.workspace = true homepage.workspace = true From 991151f4e02346f01070548c6496761ffe0cfa51 Mon Sep 17 00:00:00 2001 From: Farhan Syah Date: Wed, 21 Oct 2026 10:55:31 +0800 Subject: [PATCH 03/12] docs(raster): add NOTICE file Add a NOTICE file to nodedb-raster, matching the other Apache-2.0 library crates. --- nodedb-raster/NOTICE | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 nodedb-raster/NOTICE diff --git a/nodedb-raster/NOTICE b/nodedb-raster/NOTICE new file mode 100644 index 000000000..4bf4bfc89 --- /dev/null +++ b/nodedb-raster/NOTICE @@ -0,0 +1,2 @@ +Copyright 2026 Biz Impulse Solutions Sdn. Bhd. and the NodeDB contributors. +This product is licensed under the Apache License, Version 2.0. From 2c7f1ecdf8adcec735b8f5e4d79c257a35154bd3 Mon Sep 17 00:00:00 2001 From: Farhan Syah Date: Wed, 21 Oct 2026 21:38:57 +0800 Subject: [PATCH 04/12] docs: document spatial reference systems, array attributes, GeoTIFF load, and bitemporal graph reads --- CONTRIBUTING.md | 2 +- docs/array.md | 224 ++++++++++++++++++++++++++++++++-- docs/bitemporal.md | 40 ++++++ docs/getting-started.md | 13 +- docs/graph.md | 47 +++++-- docs/real-time.md | 2 + docs/security/auth.md | 9 ++ docs/security/encryption.md | 57 +++++++++ docs/spatial.md | 236 ++++++++++++++++++++++++++++++------ 9 files changed, 571 insertions(+), 59 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1eeac9d65..3211483bd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -231,7 +231,7 @@ NodeDB uses a mixed-license model for its runtime and library crates: | Crates | License | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------- | -| `nodedb-types`, `nodedb-client`, `nodedb-query`, `nodedb-codec`, `nodedb-spatial`, `nodedb-graph`, `nodedb-vector`, `nodedb-vector-gpu`, `nodedb-fts`, `nodedb-strict`, `nodedb-columnar`, `nodedb-array`, `nodedb-sql`, `nodedb-physical`, `nodedb-wal`, `nodedb-mem`, `nodedb-crdt` | [Apache 2.0](LICENSE.APACHE-2.0) | +| `nodedb-types`, `nodedb-srid`, `nodedb-crs`, `nodedb-client`, `nodedb-query`, `nodedb-codec`, `nodedb-spatial`, `nodedb-graph`, `nodedb-vector`, `nodedb-vector-gpu`, `nodedb-fts`, `nodedb-strict`, `nodedb-columnar`, `nodedb-array`, `nodedb-raster`, `nodedb-raster-load`, `nodedb-sql`, `nodedb-physical`, `nodedb-wal`, `nodedb-mem`, `nodedb-crdt` | [Apache 2.0](LICENSE.APACHE-2.0) | | `nodedb` (server), `nodedb-bridge`, `nodedb-raft`, `nodedb-cluster` | [BUSL-1.1](LICENSE) | **What this means for contributors:** you sign the Contributor Assignment Agreement (the CLA) once, before your first pull request merges. It assigns the copyright in your contributions to Biz Impulse Solutions Sdn. Bhd. and gives you a licence back to use your own code for any purpose. The code stays public under the licence of its crate: Apache 2.0 for the library crates, BUSL-1.1 for the server crates. diff --git a/docs/array.md b/docs/array.md index dd5777d26..d051a167b 100644 --- a/docs/array.md +++ b/docs/array.md @@ -64,6 +64,7 @@ An attribute stores each cell at its declared width. A value outside the width i | Type | Aliases | Range | Column type (OID) | | --------- | -------------------------- | ---------------------------------- | ------------------------- | +| `INT8` | `TINYINT` | -128 to 127 | `smallint` (21) | | `UINT8` | | 0 to 255 | `smallint` (21) | | `INT16` | `SMALLINT`, `INT2` | -32768 to 32767 | `smallint` (21) | | `UINT16` | | 0 to 65535 | `integer` (23) | @@ -76,12 +77,57 @@ An attribute stores each cell at its declared width. A value outside the width i | `BYTES` | `BLOB`, `BYTEA` | raw bytes | `bytea` (17) | - An unsigned type reads as the next wider signed column type, so every stored value fits. +- The canonical names give the width in bits, so `INT8` is the 1-byte signed type, like `UINT8`. This differs from PostgreSQL, where `int8` is an 8-byte integer. For an 8-byte attribute, write `INT64` or `BIGINT`. Collections keep the PostgreSQL meaning of `INT8`. +- The PostgreSQL aliases give the width in bytes: `INT2` is `INT16` and `INT4` is `INT32`. - `INT` is the 4-byte `INT32`, as in PostgreSQL. A bare `FLOAT` is the 8-byte `FLOAT64`. +- A GeoTIFF band of signed 8-bit samples loads into `INT8` or any wider signed or float type. A band of signed 32-bit samples loads into `INT32`, `INT64`, or `FLOAT64`. - A float written to a `FLOAT32` attribute rounds to the nearest `f32`. Only a finite value past the `f32` range is refused. `0.1` stored as `FLOAT32` reads back as `0.1`. - An integer literal is accepted by every integer and float type. A float literal is accepted by `FLOAT32` and `FLOAT64` only. - An integer column compresses by its value range, so a `UINT16` column takes the same bytes on disk as an `INT64` column of the same values. A `FLOAT32` column stores at most 4 bytes per value, half the raw width of `FLOAT64`. - A `NULL` stays `NULL` through a flush and a compaction, in every type. +### Attribute Metadata + +An attribute can carry reading metadata after its type: `NODATA`, `SCALE`, `OFFSET`, and `UNITS`, in any order and mixed with `NOT NULL`. + +```sql +CREATE ARRAY scene + DIMS (row INT64 [0..10979], col INT64 [0..10979]) + ATTRS ( + red UINT16 NODATA 0 SCALE 0.0000275 OFFSET -0.2 UNITS 'reflectance', + qa UINT8 NOT NULL + ) + TILE_EXTENTS (512, 512); +``` + +| Clause | Applies to | Meaning | +| -------- | ----------------- | ------- | +| `NODATA` | numeric attributes | The raw value that marks a cell as holding no data. A number, `NAN`, `INF`, or `'-inf'` (quoted). | +| `SCALE` | numeric attributes | The factor a physical read multiplies the raw value by. Finite and non-zero. | +| `OFFSET` | numeric attributes | The term a physical read adds after the scale. Finite. | +| `UNITS` | every attribute | The unit of the physical value, as a quoted string. Not empty. | + +- A cell stores the raw value. Metadata never changes a stored cell. +- `NODATA` must be a value of the attribute type: a whole number in range for an integer type, and a value inside the `f32` range (or `NAN`, `INF`, `'-inf'`) for `FLOAT32`. A value the type cannot hold is refused with SQLSTATE `22000`. +- A `FLOAT32` cell matches the nearest `f32` of `NODATA`, as GDAL compares. `NAN` matches `NAN`. +- A physical read gives `raw * scale + offset` as `FLOAT64`. A missing `SCALE` reads as 1 and a missing `OFFSET` as 0. A nodata cell reads as `NULL`. See `physical => true` on [`ARRAY_SLICE`](#array_slice--range-query) and [`ARRAY_AGG`](#array_agg--aggregate-an-attribute). +- Nodata is not data: `ARRAY_AGG` skips a nodata cell for every reducer, `COUNT` included, with or without `physical`. +- `DESCRIBE` shows the metadata after the type, in the DDL form that recreates it: `UINT16 NODATA 0.0 SCALE 2.75e-5 OFFSET -0.2 UNITS 'reflectance'`. + +Change the metadata with `ALTER ARRAY`: + +```sql +-- Set or replace fields. A field the clause omits keeps its value. +ALTER ARRAY scene ALTER ATTR red SET (NODATA 0, SCALE 0.0000275); + +-- Remove fields. Each named field must be set (SQLSTATE 55000 otherwise). +ALTER ARRAY scene ALTER ATTR red DROP OFFSET, UNITS; +``` + +Metadata is not part of the schema hash. Setting or dropping it keeps every stored segment readable and replicates through the array catalog. An unknown attribute is refused with SQLSTATE `42703`. + +`COPY FROM '.tif' WITH (FORMAT geotiff)` picks a pixel's nodata in this order: the COPY `NODATA` option, then the file's `GDAL_NODATA`, then each attribute's `NODATA`. The first two apply to every band. Attribute metadata gives each band the nodata of the attribute it loads into. A pixel is skipped when every loaded band holds its own nodata. A band that holds nodata in a kept pixel writes `NULL`. + ## Examples ### Basic Creation and Insertion @@ -166,6 +212,14 @@ SELECT * FROM ARRAY_SLICE( | `bounds` | Yes | STRING | Quoted object literal `'{ dim: [lo, hi] }'`. Each range is closed: both ends are included. Omitted dims = full range | | `attrs` | No | ARRAY[STRING] | Attributes to project, such as `['height']`. `'*'` = all attributes. Omit it to read all attributes | | `limit` | No | INT | Max cells returned, as a non-negative integer literal. Omit it for no limit | +| `physical => true` | No | BOOL | Named argument. Read each attribute physically: `raw * scale + offset` as `double precision` for an attribute with a `SCALE` or `OFFSET`, and `NULL` for a nodata cell. Default `false` | + +`ARRAY_SLICE` takes one named argument, `physical`. `ARRAY_PROJECT` and `ARRAY_ELEMENTWISE` take none. An unknown named argument, or `=` in place of `=>`, is refused: + +```sql +SELECT x, y, red +FROM ARRAY_SLICE('scene', '{row: [0, 511], col: [0, 511]}', ['red'], physical => true); +``` #### Result Relation @@ -173,7 +227,7 @@ SELECT * FROM ARRAY_SLICE( | Declared type | Column type | | ------------- | ------------------ | -| `UINT8`, `INT16` (attribute only) | `smallint` | +| `INT8`, `UINT8`, `INT16` (attribute only) | `smallint` | | `UINT16`, `INT32` (attribute only) | `integer` | | `UINT32` (attribute only) | `bigint` | | `INT64` | `bigint` | @@ -238,20 +292,48 @@ SELECT * FROM ARRAY_AGG('spatial_grid', 'temperature', 'SUM'); SELECT * FROM ARRAY_AGG('spatial_grid', 'temperature', 'SUM', 'x'); ``` -The result has a `result` column. A grouped call also has a `group` column that holds the dimension value. The `group` column takes the group dimension's declared type: `TIMESTAMP_MS` gives `timestamp`, `FLOAT64` gives `float8`, `INT64` gives `int8`, and `STRING` gives `text`. A temporal call (`AS OF`) appends one summary row with the `bool` column `truncated_before_horizon`. Reducers are case-insensitive: `'SUM'`, `'COUNT'`, `'MIN'`, `'MAX'`, and `'MEAN'` (or `'AVG'`). +The result has a `result` column. A grouped call also has a `group` column that holds the dimension value. The `group` column takes the group dimension's declared type: `TIMESTAMP_MS` gives `timestamp`, `FLOAT64` gives `float8`, `INT64` gives `int8`, and `STRING` gives `text`. A temporal call (`AS OF`) appends one summary row with the `bool` column `truncated_before_horizon`. Reducer names are case-insensitive. -The `result` type follows the reducer and the attribute type: +| Reducer | Parameters | Integer attribute | Float attribute | Definition | +| ------- | ---------- | ----------------- | --------------- | ---------- | +| `'count'` | — | `bigint` | `bigint` | The non-null, non-nodata cells. | +| `'sum'` | — | `bigint`, exact | `double precision` | | +| `'min'`, `'max'` | — | the attribute's own column type, exact | the attribute's own column type | `NaN` sorts above every other float. | +| `'mean'` (`'avg'`) | — | `double precision` | `double precision` | Sum over count, merged exactly. | +| `'median'` | — | `double precision` | `double precision` | `'percentile'` with `p => 0.5`. | +| `'percentile'` | `p => 0..1` | `double precision` | `double precision` | Exact. With `N` values sorted ascending and numbered from 0, the value at rank `h = p * (N - 1)`: `v[floor(h)] + (h - floor(h)) * (v[ceil(h)] - v[floor(h)])`. This is PostgreSQL's `percentile_cont`. | +| `'mode'` | — | the attribute's own column type | the attribute's own column type | The value the most cells hold. Among ties, the smallest value. | +| `'var_pop'`, `'var_samp'` (`'variance'`) | — | `double precision` | `double precision` | Population variance `m2 / N`, sample variance `m2 / (N - 1)`. `'variance'` is the sample form, as in PostgreSQL. | +| `'stddev_pop'`, `'stddev_samp'` (`'stddev'`) | — | `double precision` | `double precision` | Square roots of the variances. `'stddev'` is the sample form. | +| `'histogram'` | `bins => 1..65536`, `lo => n`, `hi => n` | see below | see below | Fixed-width bin counts over `[lo, hi]`. | -| Reducer | Integer attribute | Float attribute | -| ------------- | -------------------------------------- | ------------------ | -| `COUNT` | `bigint` | `bigint` | -| `SUM` | `bigint`, exact | `double precision` | -| `MIN` / `MAX` | the attribute's own column type, exact | the attribute's own column type | -| `MEAN` | `double precision` | `double precision` | +`ARRAY_AGG` takes these named arguments after its positional ones: `p`, `bins`, `lo`, `hi`, and `physical`. A reducer refuses a parameter it does not take, and refuses to run without one it needs. An unknown named argument, or `=` in place of `=>`, is refused. -`SUM`, `MEAN`, `MIN`, and `MAX` accept integer and float attributes only. `COUNT` accepts every attribute type. Another reducer over a `STRING` or `BYTES` attribute is refused at plan time with a type mismatch error. +```sql +-- 90th percentile of reflectance per scene date, physical values +SELECT * FROM ARRAY_AGG('scene', 'red', 'percentile', 'date', p => 0.9, physical => true); + +-- Population standard deviation of the whole array +SELECT * FROM ARRAY_AGG('scene', 'red', 'stddev_pop'); +``` + +A histogram splits `[lo, hi]` into `bins` bins of width `(hi - lo) / bins`. Bin `i` holds the values in `[lo + i*w, lo + (i+1)*w)`, and the last bin also holds `hi`. A value below `lo`, above `hi`, or `NaN` falls in no bin and is not counted. A histogram returns one row per bin, zero-count bins included, with `bin_lo` and `bin_hi` (`double precision`) and `count` (`bigint`) in place of `result`. A grouped histogram returns one row per bin per group, after `group`: + +```sql +SELECT * FROM ARRAY_AGG('scene', 'ndvi', 'histogram', bins => 4, lo => -1, hi => 1); +-- bin_lo | bin_hi | count +-- --------+--------+------- +-- -1 | -0.5 | 12 +-- -0.5 | 0 | 40 +-- 0 | 0.5 | 311 +-- 0.5 | 1 | 145 +``` -An integer `SUM` adds exactly, so `2^53 + 1` stays `9007199254740993`. A `SUM` past the `bigint` range is refused with SQLSTATE `22003`. `MIN` and `MAX` sort `NaN` above every other float, as PostgreSQL does. +Every reducer accepts integer and float attributes. `COUNT` also accepts `STRING` and `BYTES`. Another reducer over a `STRING` or `BYTES` attribute is refused at plan time with a type mismatch error. Every reducer groups by a dimension. + +Every reducer is exact across tiles, cores, and nodes: each core and shard sends a partial that merges without loss. An integer `SUM` adds exactly, so `2^53 + 1` stays `9007199254740993`. A `SUM` past the `bigint` range is refused with SQLSTATE `22003`. `'median'`, `'percentile'`, and `'mode'` carry every distinct value with its count. They hold at most 4,194,304 distinct values per group, about 64 MiB. Past that budget the query is refused with SQLSTATE `54000`, never approximated. An integer attribute of 16 bits or less never reaches the budget. To stay under it on a wide attribute, narrow the box or zone, or group by a dimension. The variances use Welford's method, merged by Chan's formula. + +With `physical => true`, every reducer reads `raw * scale + offset` for an attribute with a `SCALE` or `OFFSET`, so the result type is that of a `FLOAT64` attribute. A nodata cell never contributes, with or without `physical`. An attribute whose every cell in the region is nodata returns `NULL` for every scalar reducer, `COUNT` included. Three more optional arguments bound the cells the reducer reads: @@ -317,6 +399,126 @@ SELECT ARRAY_COMPACT('spatial_grid'); Returns one object `{"compacted": n}`. `n` counts the cores that merged segments. It is `0` when the compaction picker found nothing to merge. A failure raises an error. Compaction is background-automatic, but can be triggered manually. +## Bulk Load: COPY + +`COPY FROM STDIN` and `COPY FROM '.csv'` load CSV rows: the dims in schema order, then the attributes. `COPY FROM ... WITH (FORMAT geotiff)` loads a raster. Every `COPY ... FROM` needs a superuser. + +### Raster Sources + +```sql +-- One file: band i loads into attribute i. +COPY scene FROM '/data/LC09_SR.tif' WITH (FORMAT geotiff, TIME '2024-03-15T03:12:00Z'); + +-- One file per band group, read in lockstep: each cell gets every attribute. +COPY scene FROM ( + 's3://usgs-landsat/collection02/level-2/.../LC09_..._SR_B4.TIF', + 's3://usgs-landsat/collection02/level-2/.../LC09_..._SR_B5.TIF', + 's3://usgs-landsat/collection02/level-2/.../LC09_..._QA_PIXEL.TIF' +) WITH (FORMAT geotiff, BANDS (red, nir, qa), CREDENTIAL landsat, REQUESTER_PAYS true, + TIME '2024-03-15T03:12:00Z'); + +-- An HTTPS source behind Earthdata Login. +COPY scene FROM 'https://data.lpdaac.earthdatacloud.nasa.gov/.../HLS.S30....B04.tif' + WITH (FORMAT geotiff, CREDENTIAL earthdata); +``` + +A source is one of: + +| Source | Read with | +| ---------------------- | --------- | +| `'/abs/path.tif'` | Positional reads of a server file. The path must be absolute and hold no `..`. | +| `'https://host/a.tif'` | HTTP range requests through the egress policy. | +| `'s3://bucket/key'` | Ranged S3 `GET`s through the egress policy. | + +- A list `('', '', ...)` loads one raster per band group. The bands of the load are every source's bands in source order. `BANDS` maps them in that order. +- Every source of one COPY shares one image size, geotransform, CRS and block layout. A mismatch is refused with SQLSTATE `22023` naming the source and what differs. +- The load reads the same block index from every source, decodes them, and writes one cell per pixel with every attribute. A cell write carries the full attribute vector, so loading band files one COPY at a time overwrites earlier attributes. Use one COPY with a source list instead. +- A remote answer must be `206 Partial Content` with a `Content-Range` that names the same total length every time. The first strong `ETag` is pinned and sent as `If-Match` on every later request. A source that changes mid-load fails with `55000`. A server that ignores `Range` (`200`) is refused with `0A000`. +- An S3 object pins its size and ETag the same way. Without a credential the bucket is read unsigned, in the region AWS reports for it. +- Text formats read one server file. An HTTP or S3 source and a source list need `FORMAT geotiff` or a `.tif` / `.tiff` path. + +### Raster Options + +| Option | Meaning | +| ---------------------------- | ------- | +| `BANDS (a, SKIP, b)` | Where each band of the load goes. Without it, band `i` loads into attribute `i`. | +| `NODATA ` | The nodata of every band, in place of the file's. | +| `TIME ''` | The instant the one `TIMESTAMP` dim takes for every cell. | +| `CREDENTIAL ` | The credential remote sources authenticate with. | +| `REQUESTER_PAYS true` | An S3 read bills the credential's account. | +| `OVERVIEW ` | Read image `n`: 0 is full resolution, 1 the first overview. The geotransform scales with the overview, so the array's GEOREF must be on the overview grid. | +| `WINDOW (x0, y0, x1, y1)` | Load the pixel columns `x0..x1` and rows `y0..y1` only (half-open). The load fetches only the blocks the window touches. The window's footprint alone must fit the dim domains. | +| `ADOPT_METADATA true` | An attribute with no metadata takes the band's nodata, scale, offset and units from the file. Default `false`: a load never changes the schema unasked. | + +- A band loads only into an attribute that holds every value of its sample type exactly (`42804` otherwise). +- A pixel the file masks out (an internal mask, or the band's own LERC mask) holds no data in that band. A pixel where no loaded band holds data is skipped. A band with no data in a kept pixel writes `NULL`, or is refused with `23502` when the attribute is `NOT NULL`. +- `ADOPT_METADATA` changes the schema, so it runs outside a transaction block only (`25001`). An attribute with any metadata keeps all of it. + +### Credentials + +```sql +CREATE CREDENTIAL landsat (TYPE 's3', ACCESS_KEY_ID 'AKIA...', SECRET_ACCESS_KEY '...', + REGION 'us-west-2', SESSION_TOKEN '...'); +CREATE CREDENTIAL minio (TYPE 's3', ACCESS_KEY_ID '...', SECRET_ACCESS_KEY '...', + REGION 'us-east-1', ENDPOINT 'https://minio.internal:9000'); +CREATE CREDENTIAL earthdata (TYPE 'bearer', TOKEN 'eyJ...'); +CREATE CREDENTIAL blob (TYPE 'sas', TOKEN 'sv=2022-11-02&sig=...'); +SHOW CREDENTIALS; +DROP CREDENTIAL [IF EXISTS] earthdata; +``` + +- Every credential statement needs a superuser (`42501`). A credential belongs to the tenant that created it. +- `TYPE 'bearer'` sends `Authorization: Bearer `. `TYPE 'sas'` appends the token to the URL query. `TYPE 's3'` signs S3 requests. +- The secret is sealed with AES-256-GCM before the statement proposes it, so the metadata log, the catalog and every snapshot hold ciphertext only. The key is the `[credential_encryption]` key file. Every node of a cluster needs the same file. +- `SHOW CREDENTIALS` lists the name, type, owner and creation time. No statement, error message or audit record shows a secret. +- Without `[credential_encryption]`, `CREATE CREDENTIAL` and `COPY ... CREDENTIAL` are refused with `55000`. + +### Egress Policy + +One `[egress]` policy governs every outbound request: remote sources, JWKS fetches, change-stream and alert webhooks, SIEM export, OTLP trace and metrics export, and the Vault key provider. A request reaches only what the policy allows: + +- `https://` only. `http://` needs the host on the allow-list. +- No IP-literal host, at any hop. +- Every address a host resolves to must be global unicast. An IPv6 address that embeds an IPv4 address (IPv4-mapped, IPv4-compatible, NAT64 `64:ff9b::/96`, 6to4 `2002::/16`) passes only when that IPv4 address does. Teredo, site-local, discard-only and NAT64 local-use addresses never pass. A host on the allow-list may also resolve into `allow_cidrs`. +- Every redirect hop is checked, and every resolved address is checked at connect time, so a rebinding DNS answer never reaches a private address. +- The `ENDPOINT` of an S3 credential passes the policy at `CREATE CREDENTIAL` and again at every `COPY`. + +```toml +[egress] +allow_http = false +allow_hosts = ["mirror.internal"] +allow_cidrs = ["10.42.0.0/16"] + +[credential_encryption] +# 32 bytes, mode 0600, owned by the server's user: +# head -c 32 /dev/urandom > /etc/nodedb/keys/credential.key +key_path = "/etc/nodedb/keys/credential.key" +``` + +A refused URL or address is `42501`. + +### Memory and Planes + +At most 8 range fetches run at once, and close ranges merge into one request. Decoded block sets queue two deep between the reader and the load. Network reads run on the Control Plane, decoding on its blocking pool. The Data Plane receives only the array writes. + +### Raster Load Errors + +| SQLSTATE | Cause | +| -------- | ----- | +| `22023` | A CRS, grid, block layout or window mismatch, or a nodata value the sample type cannot hold. | +| `22003` | A footprint or `TIME` outside a dim domain. | +| `22P04` | A malformed or corrupt file, or a malformed HTTP answer. | +| `0A000` | A file feature the reader lacks, a missing overview, or a server that ignores `Range`. | +| `23502` | No data for a `NOT NULL` attribute. | +| `28000` | The remote source refused the credentials (HTTP 401 or 403). | +| `42501` | The egress policy refused a URL or address. | +| `42704` | An unknown credential. | +| `42804` | A band type the attribute cannot hold exactly. | +| `54000` | A block or header over the decode cap. | +| `55000` | No GEOREF, no credential key, or a source that changed mid-load. | +| `58030` | A source that cannot be read or does not exist. | +| `08006` | A connection to a remote source failed. | + ## Bitemporal Support Arrays support dual timestamping: diff --git a/docs/bitemporal.md b/docs/bitemporal.md index 8023a3526..cbae47f5d 100644 --- a/docs/bitemporal.md +++ b/docs/bitemporal.md @@ -133,6 +133,31 @@ SELECT lon, lat, temp_c FROM ARRAY_SLICE( AS OF SYSTEM TIME '2026-06-06T00:00:00Z'; ``` +### Relationship Timelines (Graph) + +Every edge write is a new version. A graph read takes the same `AS OF` clauses, and a write names an edge's valid time with `VALID_TIME ... UNTIL ...`: + +```sql +-- Alice reports to Bob for the first half of 2025. +GRAPH INSERT EDGE IN 'org' FROM 'alice' TO 'bob' TYPE 'reports_to' + VALID_TIME '2025-01-01T00:00:00Z' UNTIL '2025-07-01T00:00:00Z'; + +-- Then to Carol. +GRAPH INSERT EDGE IN 'org' FROM 'alice' TO 'carol' TYPE 'reports_to' + VALID_TIME '2025-07-01T00:00:00Z'; + +-- Who Alice reported to on 2025-03-01. +GRAPH NEIGHBORS IN 'org' OF 'alice' AS OF VALID TIME '2025-03-01T00:00:00Z'; + +-- The reporting chain as the database held it at a past moment. +GRAPH TRAVERSE FROM 'alice' IN 'org' DEPTH 5 AS OF SYSTEM TIME 1735689600000; + +-- Influence as of that moment. +GRAPH ALGO PAGERANK ON org AS OF SYSTEM TIME 1735689600000; +``` + +`GRAPH TRAVERSE`, `GRAPH NEIGHBORS`, `GRAPH PATH`, `MATCH`, `GRAPH ALGO` and `GRAPH RAG FUSION` read one cut per statement, on one node or across a cluster. `AS OF SYSTEM TIME NULL` and a valid-time range are refused on graph reads. See [Graph](graph.md#bitemporal-support). + ### Lineage and Compliance Track row lineage across corrections: @@ -164,6 +189,21 @@ ORDER BY _ts_system ASC; `AS OF SYSTEM TIME NULL` returns every system-time version of each matching row, ordered ascending. Each version row carries the user columns plus three synthetic temporal columns: `_ts_system`, `_ts_valid_from`, and `_ts_valid_until`. Unbounded valid-time ends surface as the raw `i64::MIN` / `i64::MAX` sentinels (matching the Columnar and Timeseries engines). It is supported on the Document (strict and schemaless), Columnar, and Timeseries engines, and on `ARRAY_SLICE`. An `ARRAY_SLICE` version row carries the dimension and attribute columns plus `_ts_system` only. It is not supported on the Graph engine or on `ARRAY_AGG`, nor when reading through a database clone — those return a typed error rather than collapsing to a single version. +**Deletes in the audit log.** A `DELETE` and a `TRUNCATE` close a row with a delete version. On Columnar, an `UPDATE` that changes the primary key closes the old key the same way. The audit log shows each delete version as a row, on the Document (strict and schemaless) and Columnar engines alike: + +- `_ts_system` is the system time of the delete. +- `_ts_valid_from` and `_ts_valid_until` are both `i64::MIN`. The empty valid interval marks the row as a delete. +- The row repeats the cells of the version it closes, the row's key among them. +- A delete version leaves the audit log with the version it closes. Once retention or a GDPR erasure removes that version, the delete version no longer shows. + +To list only delete versions, filter on `_ts_valid_from = _ts_valid_until`. To hide them, filter on `_ts_valid_from < _ts_valid_until`. A read at a cut (`AS OF SYSTEM TIME `) never returns a delete version: a row deleted at or before the cut is absent. An `AS OF VALID TIME` filter never matches a delete version, since no instant falls inside an empty interval. + +Document versions and graph edge versions take their system time from one clock. A `MATCH` reads its node documents at the same cut as its edges, so a node's properties and its edges answer the same moment. `_ts_system` shows the version's millisecond. Versions written within one millisecond keep their write order. `AS OF SYSTEM TIME ` covers every version written up to the end of that millisecond. The versions one transaction writes on one shard share one system time. A cross-shard transaction that commits through the Calvin sequencer shares one system time on every shard. + +One statement reads one cut. Every `AS OF SYSTEM TIME` read of a statement, both sides of a join included, reads at the same instant, on every core and every node. A time past the moment the statement is planned reads the state at that moment. A write that commits later never appears in that statement, and the next statement takes a fresh cut. + +In a multi-node cluster, a write that commits outside the Calvin sequencer takes its system time from the clock of the node that writes it. A statement's cut includes it only when that clock is not ahead of the planning node's clock, after the planning node has applied every write it holds. Keep node clocks synchronized (NTP or PTP): a write committed just before a statement can fall outside the statement's cut by up to the clock skew between the two nodes. Writes through the Calvin sequencer and writes on a single node have no such bound. + `AS OF` reads run at parity with non-temporal reads: `WHERE` predicates (including on strict Binary-Tuple collections), `ORDER BY`, computed columns, and window functions all apply to temporal queries. **Reserved columns never leak.** Bitemporal collections store their temporal envelope in reserved columns (`__system_from_ms`, `__valid_from_ms`, `__valid_until_ms`). These are hidden from user projections: `SELECT *` on a bitemporal collection returns exactly the declared user columns. Temporal data is only exposed through the synthetic `_ts_*` columns of audit queries. diff --git a/docs/getting-started.md b/docs/getting-started.md index f41858104..e79076af3 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -335,7 +335,9 @@ key_path = "/etc/nodedb/keys/wal.key" With `pitr.enabled = true`, startup stops unless `[cold_storage]` is configured and opens, and unless `[encryption]` is configured. The WAL archive is the only copy of a segment once a checkpoint deletes it locally. Base -snapshots are encrypted with the WAL key. With `pitr.enabled = false` and no +snapshots are encrypted with the WAL key, which `[encryption]` reads from +`key_path` or from Vault or AWS KMS (see `security/encryption.md`). With +`pitr.enabled = false` and no `[cold_storage]`, checkpoints delete WAL segments without archiving them. Archived segments are stored under `{prefix}wal/{node_id}/{incarnation}/`. @@ -661,8 +663,13 @@ Each value must be between `1` and `86400000` (one day). Both keys belong under | `observability.otlp.export.metrics_interval_secs` | `NODEDB_OTLP_EXPORT_INTERVAL` | `15` | | `observability.debug_endpoints_enabled` | `NODEDB_DEBUG_ENDPOINTS_ENABLED` | `false` | -`NODEDB_OTLP_EXPORT_ENDPOINT` takes an `http://` or `https://` URL. The debug -endpoints expose raft internals, so they stay off until you enable them. +`NODEDB_OTLP_EXPORT_ENDPOINT` takes an `http://` or `https://` URL with a DNS +host. With export enabled, the server pushes its metrics to +`/v1/metrics` every `metrics_interval_secs` and its trace spans to +`/v1/traces`. Both go through the `[egress]` policy: a collector on a private +network, or one reached over `http://`, needs its host on `allow_hosts` and its +addresses in `allow_cidrs`. The debug endpoints expose raft internals, so they +stay off until you enable them. ### Startup validation diff --git a/docs/graph.md b/docs/graph.md index 244bf0fae..689443628 100644 --- a/docs/graph.md +++ b/docs/graph.md @@ -379,6 +379,7 @@ Variable-length `[*min..max]` MATCH expansion is bounded by operational knobs in | `varlen_max_frontier`| 100000 | Cap on the live per-hop frontier; overflow pages via resume. | | `max_depth` | 10 | Maximum traversal depth. | | `max_visited` | 100000 | Memory budget on visited nodes. | +| `max_historical_csr_cache_entries` | 8 | Historical CSRs one core keeps for `AS OF` reads. `0` keeps none. | Resume cursors work both locally and across shards, so a capped expansion continues from where it stopped instead of failing. @@ -386,25 +387,51 @@ Resume cursors work both locally and across shards, so a capped expansion contin ## Bitemporal Support -Graph edges support both system time and valid time, enabling relationship timelines and historical traversal. +Every edge write is versioned. Each version carries the system time of its write and a valid time `[from, until)`. Graph reads take `AS OF SYSTEM TIME` and `AS OF VALID TIME`. ```sql --- Insert an edge that becomes valid in the future (scheduled relationship) +-- An edge valid from a set date. Without VALID_TIME the write's own time +-- starts the valid time. Without UNTIL it stays open. GRAPH INSERT EDGE IN 'edges' FROM 'user:alice' TO 'project:rocket' TYPE 'assigned_to' PROPERTIES { assigned_date: '2026-05-01' } - VALID_TIME '2026-05-01T00:00:00Z'; + VALID_TIME '2026-05-01T00:00:00Z' UNTIL '2026-09-01T00:00:00Z'; --- Query the graph as it existed at a past moment -GRAPH TRAVERSE FROM 'user:alice' DEPTH 2 -AS OF SYSTEM TIME 1700000000000; +-- The graph as it stood at a past moment. +GRAPH TRAVERSE FROM 'user:alice' IN 'edges' DEPTH 2 + AS OF SYSTEM TIME 1700000000000; --- Query relationships that were valid at a specific date -MATCH (u:User)-[:assigned_to]->(p:Project) -WHERE u.id = 'alice' -AS OF VALID TIME 1746086400000; -- May 1, 2026 +GRAPH NEIGHBORS IN 'edges' OF 'user:alice' AS OF SYSTEM TIME '2025-01-15T00:00:00Z'; + +GRAPH PATH IN 'edges' FROM 'user:alice' TO 'project:rocket' MAX_DEPTH 4 + AS OF SYSTEM TIME 1700000000000; + +-- The relationships valid on a date. +MATCH (u)-[:assigned_to]->(p) IN 'edges' + AS OF VALID TIME 1746086400000 + RETURN u, p; + +-- Algorithms and GraphRAG run over the graph at the cut. +GRAPH ALGO PAGERANK ON edges AS OF SYSTEM TIME 1700000000000; +GRAPH RAG FUSION ON edges QUERY ARRAY[0.1, 0.2, 0.3] + AS OF SYSTEM TIME 1700000000000; ``` +Rules: + +- **Clauses** — `AS OF SYSTEM TIME` and `AS OF VALID TIME` go anywhere after the command words. A time is an integer of ms since the Unix epoch, `NOW()`, or an ISO-8601 string. +- **One cut per statement** — a statement pins one system time. Every hop of a walk, every superstep of a BSP algorithm, and every node of a cluster reads that cut. A time past the statement's start reads the graph as of the start. +- **Complete at the cut** — before a node serves any read of a historical statement, it waits until it holds every edge version at or below the cut. A statement whose deadline ends during that wait fails with `DEADLINE_EXCEEDED`. It never returns a partial graph. +- **Valid time** — an edge is read when its valid time contains the `AS OF VALID TIME` instant. A version that does not contain it is passed over for the next older version. +- **Collection** — a historical read names its collection (`IN`, `ON`). Edge versions are stored per collection. +- **Writes** — `GRAPH INSERT EDGE`, `GRAPH DELETE EDGE`, `GRAPH LABEL` and `GRAPH UNLABEL` refuse `AS OF`. `UNTIL` needs `VALID_TIME`, and `UNTIL` must be after `VALID_TIME`. +- **Not supported** — `AS OF SYSTEM TIME NULL` (every version) and a `FOR VALID_TIME FROM ... TO ...` range are refused on graph reads. +- **Node properties** — a MATCH `WHERE` on node properties reads a `bitemporal = true` node collection at the same cut. A plain node collection has no history and reads its current documents. +- **Node identity and labels** — a historical read takes each node's identity from the edge version the cut resolves to, and each node's labels as they stood at the cut. A node deleted after the cut keeps its identity and labels there. +- **Transactions** — a historical read inside a transaction reads committed versions only. Staged writes appear in live reads. + +Each Data Plane core keeps the most recently used historical CSRs, up to `max_historical_csr_cache_entries` in `[tuning.graph]` (default 8). A write at or below a cached cut, a TRUNCATE, a purge, a restore, or a node label change drops the CSRs it affects. + This is useful for organizational hierarchies, historical relationship analysis, and compliance audits. See [Bitemporal](bitemporal.md) for detailed examples. --- diff --git a/docs/real-time.md b/docs/real-time.md index c0336bdd0..26ba5ce83 100644 --- a/docs/real-time.md +++ b/docs/real-time.md @@ -228,6 +228,8 @@ WITH ( Each POST includes `X-Idempotency-Key`, `X-Fencing-Token`, `X-CDC-Offset`, `X-Event-Sequence`, `X-Partition`, and `X-LSN` headers. 4xx client errors (except 429) are not retried. +The URL goes through the `[egress]` policy: `https://` to a DNS name that resolves to global unicast addresses, or a host on the allow-list. No redirect is followed. A URL the policy refuses stops the stream's delivery. Alert `NOTIFY WEBHOOK` URLs pass the same policy when the alert is created (`42501` on refusal) and before every send. + In a cluster, one node delivers a stream at a time: the node that holds the leader lease of the stream's owning Raft group. It reads every partition, including partitions whose rows live on other nodes. It checks its lease right before each POST and right before each offset commit. When that node fails, the next lease holder resumes from the committed offsets. Delivery is at-least-once: a POST can succeed after the old owner lost its lease but before it committed the offset, and the new owner then sends that event again. Two headers let an endpoint apply each event once: | Header | Value | What the endpoint does with it | diff --git a/docs/security/auth.md b/docs/security/auth.md index 34c3ae466..b2dc5fc86 100644 --- a/docs/security/auth.md +++ b/docs/security/auth.md @@ -127,6 +127,15 @@ JWT claims map to `$auth.*` session variables: Supported algorithms: ES256, ES384, RS256. Built-in JWKS cache with disk fallback and circuit breaker for provider outages. +JWKS fetches go through the `[egress]` policy. The default admits `https://` to a DNS name that resolves to global unicast addresses only. A provider on a private network needs its host on the allow-list: + +```toml +[egress] +allow_http = true # only for the hosts below +allow_hosts = ["keycloak.internal"] +allow_cidrs = ["10.42.0.0/16"] # addresses those hosts may resolve into +``` + **Note:** The OIDC provider mechanism (above) is the recommended approach for modern SSO; this `nodedb.toml` method is supported for backward compatibility. Every static provider requires an explicit `tenant_id`; startup fails closed when it is omitted. Providers may share an issuer only when each has a distinct, non-empty audience. JWTs may assert ordinary built-in and custom roles, but they cannot assert NodeDB superuser authority. NodeDB ignores both the `is_superuser` claim and the `"superuser"` role string on every external JWT path. Superuser must be granted through NodeDB credential administration. diff --git a/docs/security/encryption.md b/docs/security/encryption.md index 70dd02ba5..8e2593a91 100644 --- a/docs/security/encryption.md +++ b/docs/security/encryption.md @@ -26,6 +26,45 @@ head -c 32 /dev/urandom > /etc/nodedb/keys/wal.key chmod 600 /etc/nodedb/keys/wal.key ``` +**Key sources:** `key_path` is the default source. A key-management +service supplies the key instead through an `[encryption.key_derivation]` +table. Set exactly one of `key_path` and `key_derivation`; startup refuses +both or neither. The same key also derives the segment and checkpoint keys, +and `nodedb restore` reads it from the same source. + +| `type` | Source | +| --------- | ---------------------------------------------------------------------- | +| `file` | A 32-byte key file (`key_path`), as `[encryption] key_path`. | +| `vault` | HashiCorp Vault transit engine: unwraps a wrapped data key on disk. | +| `aws_kms` | AWS KMS `Decrypt` of a ciphertext blob on disk. | + +```toml +[encryption.key_derivation] +type = "vault" +addr = "https://vault.internal:8200" +token_path = "/etc/nodedb/keys/vault.token" # mode 0600 +key_name = "nodedb-wal" +mount = "transit" +ciphertext_blob_path = "/var/lib/nodedb/.wal-key.wrapped" +``` + +Create the wrapped key once with `vault write -f transit/datakey/wrapped/nodedb-wal` +and store its `ciphertext` in `ciphertext_blob_path`. The Vault address goes +through the `[egress]` policy: a private Vault needs its host on +`allow_hosts` and its addresses in `allow_cidrs`, and an IP-literal address +is refused. + +```toml +[encryption.key_derivation] +type = "aws_kms" +key_id = "arn:aws:kms:us-east-1:123456789012:key/..." +region = "us-east-1" +ciphertext_blob_path = "/var/lib/nodedb/.wal-key.kms" +``` + +The KMS client uses the AWS SDK credential chain and its own HTTP stack, not +the `[egress]` policy. + When `[encryption]` is absent, WAL records are written in plaintext. This is acceptable only for local development or when the data directory is on an encrypted volume (LUKS, dm-crypt, FileVault, BitLocker). @@ -159,6 +198,23 @@ When `[backup_encryption]` is absent, a warning is emitted at the first backup operation. Unencrypted backups should not leave the data center or be stored in shared object storage without additional encryption. +### Credential Encryption + +`CREATE CREDENTIAL` secrets (S3 keys, bearer tokens, SAS signatures for +remote `COPY` sources) are sealed with AES-256-GCM under a key derived +from the credential key, before the statement proposes them. The metadata +log, the catalog and every snapshot hold ciphertext only. The key file must +be 32 bytes, mode `0400` or `0600`, owned by the server's user, and differ +from the WAL and backup keys. Every node of a cluster needs the same file. + +```toml +[credential_encryption] +key_path = "/etc/nodedb/keys/credential.key" +``` + +When `[credential_encryption]` is absent, `CREATE CREDENTIAL` is refused +with SQLSTATE `55000`. + ## Encryption in Transit See [protocols.md — TLS](protocols.md#tls) for wire encryption @@ -177,3 +233,4 @@ and is appropriate only for local development. | Vamana DiskANN segments | Filesystem | OS volume encryption | Plaintext on disk | | L2 cold tier (S3) | SSE (object store) | `[cold_storage].sse_mode`| No SSE header sent | | Backups | AES-256-GCM (app) | `[backup_encryption].key_path` | Plaintext backup | +| COPY credentials | AES-256-GCM (app) | `[credential_encryption].key_path` | `CREATE CREDENTIAL` refused | diff --git a/docs/spatial.md b/docs/spatial.md index f4aaad6ba..c51bee450 100644 --- a/docs/spatial.md +++ b/docs/spatial.md @@ -17,11 +17,12 @@ Spatial is a **columnar profile**. Collections with a `SPATIAL_INDEX` column mod - **R\*-tree index** — Bulk loading, nearest neighbor, range queries - **Geohash** — Encode/decode, neighbor cells, area covering - **H3 hexagonal index** — Uber's H3 via h3o for uniform-area spatial binning -- **OGC predicates** — `ST_Contains`, `ST_Intersects`, `ST_Within`, `ST_Disjoint`, `ST_DWithin`, `ST_IsValid` -- **Geometry constructors** — `ST_Point`, `ST_MakePoint`, `ST_GeomFromText` (WKT), `ST_GeomFromWKB`, `ST_GeomFromGeoJSON`, `ST_MakeLine`, `ST_MakePolygon`, `ST_MakeEnvelope` -- **Accessors and measures** — `ST_AsText`, `ST_AsGeoJSON`, `ST_X`, `ST_Y`, `ST_GeometryType`, `ST_NPoints`, `ST_SRID`, `ST_SetSRID`, `ST_Transform`, `ST_Distance`, `ST_Length`, `ST_Perimeter`, `ST_Area` -- **Geometry operations** — `ST_Buffer`, `ST_Envelope`, `ST_Centroid`, `ST_Union`, `ST_Intersection` -- **Format support** — WKB, WKT, GeoJSON interchange. GeoParquet v1.1.0 + GeoArrow metadata. +- **OGC predicates** — `ST_Contains`, `ST_Intersects`, `ST_Within`, `ST_Disjoint`, `ST_DWithin`, `ST_Touches`, `ST_Crosses`, `ST_Overlaps`, `ST_Equals`, `ST_Covers`, `ST_CoveredBy`, `ST_Relate`, `ST_3DDWithin`, `ST_3DIntersects` +- **Z and M ordinates** — every geometry is XY, XYZ, XYM or XYZM. Every storage form and encoding keeps Z and M. +- **Geodesic measures** — distance, length, perimeter and area on the CRS's own ellipsoid (Karney's algorithms) +- **Overlay and shape operations** — `ST_Buffer`, `ST_Union`, `ST_Intersection`, `ST_Difference`, `ST_SymDifference`, `ST_ConvexHull`, `ST_ConcaveHull`, `ST_Simplify`, `ST_MakeValid`, `ST_Split`, `ST_Subdivide` +- **Linear referencing** — `ST_LineInterpolatePoint`, `ST_LocateAlong`, `ST_LocateBetween`, `ST_InterpolatePoint` +- **Format support** — WKT, EWKT, ISO WKB, EWKB and GeoJSON interchange. GeoParquet v1.1.0 and GeoArrow metadata builders in the `nodedb-spatial` crate. - **Hybrid spatial-vector** — Spatial R\*-tree narrows candidates by location, then HNSW ranks by semantic similarity in one query - **Spatial join** — R\*-tree probe join between two collections - **Distributed** — Scatter-gather across shards, shard routing by geometry, geofencing @@ -100,57 +101,149 @@ A string literal in geometry position is read as either WKT or GeoJSON, so | Function | Result | | --- | --- | | `ST_Point(lng, lat)` | Point | -| `ST_MakePoint(x, y [, z])` | Point | -| `ST_GeomFromText(wkt [, srid])` | Geometry parsed from WKT | +| `ST_MakePoint(x, y [, z [, m]])` | Point: XY, XYZ or XYZM | +| `ST_MakePointM(x, y, m)` | XYM Point | +| `ST_GeomFromText(text [, srid])` / `ST_GeomFromEWKT` | Geometry parsed from WKT or EWKT | | `ST_GeomFromGeoJSON(json)` | Geometry parsed from GeoJSON | -| `ST_GeomFromWKB(bytes [, srid])` | Geometry parsed from WKB; accepts an `X'...'` literal or its hex string | -| `ST_MakeLine(point, point, ...)` | LineString | +| `ST_GeomFromWKB(bytes [, srid])` / `ST_GeomFromEWKB` | Geometry parsed from ISO WKB or EWKB; accepts an `X'...'` literal or its hex string | +| `ST_MakeLine(point, point, ...)` | LineString in the points' SRID and dimension | | `ST_MakePolygon(ring, ...)` | Polygon, each ring an array of `[lng, lat]` pairs | | `ST_MakeEnvelope(min_lng, min_lat, max_lng, max_lat)` | Rectangular Polygon | +| `ST_TileEnvelope(z, x, y)` | Web Mercator (`3857`) Polygon of one XYZ map tile | + +A parse error fails the statement with SQLSTATE `22000`. The error names the +function and the byte offset of the error. A malformed geometry is never read +as NULL. ### Accessors | Function | Result | | --- | --- | -| `ST_AsText(geom)` | WKT rendering | -| `ST_AsGeoJSON(geom)` | GeoJSON rendering | -| `ST_X(geom)` / `ST_Y(geom)` | Ordinate of a Point; NULL for any other geometry | +| `ST_AsText(geom)` | WKT rendering, Z and M included | +| `ST_AsEWKT(geom)` | `SRID=;` then WKT | +| `ST_AsGeoJSON(geom)` | GeoJSON rendering, Z included, M dropped | +| `ST_AsBinary(geom [, 'NDR'\|'XDR'])` | ISO WKB bytes, little-endian unless `'XDR'` | +| `ST_AsEWKB(geom [, 'NDR'\|'XDR'])` | EWKB bytes with the SRID | +| `ST_X(geom)` / `ST_Y(geom)` / `ST_Z(geom)` / `ST_M(geom)` | Ordinate of a Point; NULL for any other geometry, an EMPTY point, or a missing Z or M | +| `ST_ZMin(geom)` / `ST_ZMax(geom)` | Least / greatest Z; `0` for a geometry without Z | +| `ST_CoordDim(geom)` / `ST_NDims(geom)` | Ordinates per position: 2, 3 or 4 | +| `ST_ZMFlag(geom)` | `0` XY, `1` XYM, `2` XYZ, `3` XYZM | +| `ST_HasZ(geom)` / `ST_HasM(geom)` | Whether the geometry carries Z / M | | `ST_GeometryType(geom)` | Type name, e.g. `Point` | -| `ST_NPoints(geom)` | Total vertex count | +| `ST_NPoints(geom)` | Total vertex count; `0` for EMPTY | +| `ST_IsEmpty(geom)` | Whether the geometry is EMPTY | | `ST_SRID(geom)` | SRID of the geometry; `4326` unless the geometry carries another SRID. See [Spatial reference systems](#spatial-reference-systems) | -| `ST_IsValid(geom)` | Whether the geometry is well-formed | +| `ST_IsValid(geom)` | Whether the geometry is valid. See [Validity](#validity) | +| `ST_IsValidReason(geom)` | `Valid Geometry`, or every reason the geometry is invalid | + +### Z and M ordinates + +A geometry is XY, XYZ, XYM or XYZM. Every position in one geometry has the +same dimension. Z is a height and M is a measure. Two-dimensional predicates +and measures read X and Y only. + +| Input | Z and M | +| --- | --- | +| WKT | `POINT Z (1 2 3)`, `POINT M (1 2 4)`, `POINT ZM (1 2 3 4)`. Untagged text with three numbers is XYZ, four is XYZM. | +| WKB / EWKB | ISO type codes 1000 (Z), 2000 (M), 3000 (ZM), and the EWKB Z, M and SRID flags | +| GeoJSON | A third position member is Z. GeoJSON has no M. | + +| Function | Result | +| --- | --- | +| `ST_Force2D(geom)` | Geometry without Z and M | +| `ST_Force3D(geom [, z])` / `ST_Force3DZ` | XYZ geometry; a missing Z becomes `z`, default `0`; M is dropped | +| `ST_Force3DM(geom [, m])` | XYM geometry; a missing M becomes `m`, default `0`; Z is dropped | +| `ST_Force4D(geom [, z [, m]])` | XYZM geometry; missing ordinates default to `0` | + +### EMPTY geometries + +Every geometry kind has an EMPTY form: `POINT EMPTY`, `LINESTRING EMPTY`, +`POLYGON EMPTY`, the three multi kinds, and `GEOMETRYCOLLECTION EMPTY`. WKB +writes an EMPTY point with NaN ordinates, as PostGIS does. GeoJSON writes an +empty `coordinates` array. + +- An EMPTY geometry is stored, and never enters the R\*-tree. +- An EMPTY operand matches no predicate. +- `ST_Distance` with an EMPTY operand is infinite. +- `ST_Area`, `ST_Length` and `ST_Perimeter` of EMPTY are `0`. + +### Storage + +| Engine | Stored form | +| --- | --- | +| Document (schemaless) | Native geometry value: SRID, shape, Z and M | +| Document (strict), Columnar, Timeseries, Spatial | EWKB | + +A geometry read back renders the same WKT it was written with. ### Spatial reference systems Every geometry carries an SRID. The default is `4326` (WGS 84, longitude and latitude in degrees). GeoJSON reads and writes a `crs` member for any SRID other than `4326`. +NodeDB defines every geographic 2D and projected CRS of the EPSG dataset v12.029: 611 geographic and 5,305 projected CRSs. The SRID is the EPSG code. NodeDB adds two CRSs of its own: + | SRID | System | Unit | | --- | --- | --- | -| `4326` | WGS 84 geographic | degrees | -| `32647` | UTM zone 47N on WGS 84 | metres | -| `32648` | UTM zone 48N on WGS 84 | metres | -| `3375` | GDM2000 Peninsula RSO | metres | -| `3376` | GDM2000 East Malaysia (Borneo) RSO | metres | +| `990001` | MODIS sinusoidal (`SR-ORG:6842`) on a sphere of radius 6,371,007.181 m | metres | +| `990002` | Himawari geostationary view, sub-satellite longitude 140.7°E, on WGS 84 | metres | -The set is closed. Any other SRID is refused. +The EPSG CRSs of these methods are not defined. Their SRIDs are refused: + +| Method | CRSs | Example SRIDs | +| --- | --- | --- | +| Colombia Urban | 32 | `6244`, `6245` | +| Bonne (South Orientated) | 2 | `2963`, `5017` | +| Transverse Mercator Zoned Grid System | 2 | `32600`, `32700` | +| Lambert Azimuthal Equal Area (Spherical) | 1 | `9311` | +| Transverse Mercator 3D | 1 | `9895` | +| Local Orthographic | 1 | `10622` | +| Tunisia Mining Grid | 1 | `22300` | +| Lambert Conic Near-Conformal | 1 | `22700` | + +NodeDB also defines EPSG CRSs published after v12.029: + +| SRID | System | Source | +| --- | --- | --- | +| `12287` | GDM2020, Geocentric Datum of Malaysia 2020 (EPSG datum `1471`), GRS 1980, aligned with ITRF2014 at epoch 2020.0 | EPSG change 2026.081 | +| `25314` to `25322` | GDM2020 state Cassini-Soldner grids: Johor, Sembilan and Melaka, Pahang, Selangor, Terengganu, Pinang, Kedah and Perlis, Perak, Kelantan | EPSG change 2026.081, JUPEM circular KPUP 3/2021 Table 7 | + +GDM2020 relates to WGS 84 through EPSG transformation `12291`: a null shift with an accuracy of 1 m. These GDM2020 definitions are not available: + +- GDM2000 to GDM2020. JUPEM defines it through velocity and post-seismic deformation grids that it does not publish openly. +- GDM2020 Peninsular RSO and East Malaysia BRSO. EPSG has no codes for them, and JUPEM does not publish their parameters openly. + +A transform between two datums runs through WGS 84 with the datum shift EPSG records for each CRS. Two CRSs on one datum skip the shift. A GeoJSON `crs` name takes the forms `EPSG:n`, `urn:ogc:def:crs:EPSG::n`, `http://www.opengis.net/def/crs/EPSG/0/n`, the OGC CRS84 URN or URL, and `SR-ORG:6842`. | Function | Result | | --- | --- | | `ST_SRID(geom)` | SRID of the geometry | | `ST_SetSRID(geom, srid)` | Geometry relabeled with `srid`. Coordinates stay as they are. | -| `ST_Transform(geom, srid)` | Geometry with every coordinate reprojected into `srid` | +| `ST_Transform(geom, srid [, epoch])` | Geometry with every coordinate reprojected into `srid`. `epoch` is the coordinate epoch as a decimal year. | -`ST_GeomFromText` and `ST_GeomFromWKB` take an optional `srid` argument. `ST_SetSRID` and `ST_Transform` return NULL when `srid` is NULL. +`ST_GeomFromText` and `ST_GeomFromWKB` take an optional `srid` argument. `ST_SetSRID` and `ST_Transform` return NULL when `srid` is NULL. `ST_Transform` returns NULL when `epoch` is NULL. ```sql SELECT ST_Transform(ST_Point(101.7117, 3.1579), 32647); +-- 910001: a user-defined CRS with a time-dependent shift to WGS 84. +SELECT ST_Transform(ST_SetSRID(ST_Point(128.0, -16.0), 910001), 4326, 2013.9); ``` +#### Time-dependent datum shifts + +A datum shift is a 3-, 7- or 15-parameter Helmert transformation. The 15-parameter form adds a yearly rate for each of the seven parameters and a parameter reference epoch t0 (IOGP Guidance Note 7-2 §4.2.5, EPSG methods 1053 to 1058). At coordinate epoch t each parameter p becomes p + rate × (t − t0). + +- `epoch` must lie in 1900..=2200. Any other value raises a data exception with SQLSTATE `22000`. +- Without `epoch`, each shift runs at its own reference epoch, where only the seven static parameters act. +- A static shift gives the same result at every epoch. + +A user-defined CRS (`CREATE SPATIAL_REF_SYS`) carries a time-dependent shift in WKT2 or PROJJSON: a `BOUNDCRS` / `BoundCRS` to WGS 84 whose transformation uses a time-dependent Helmert method with the rate parameters (EPSG 1040 to 1046) and the parameter reference epoch (EPSG 1047). A PROJ string cannot carry rates, so `nodedb.spatial_ref_sys` shows `proj4text` as NULL for such a CRS, as PostGIS does for a CRS PROJ strings cannot express. Its `srtext` holds the full WKT2. + Geometry functions read the SRID of each operand. They never transform implicitly. These cases raise a data exception with SQLSTATE `22000`: - Two operands in different SRIDs. The error reads `operands have different SRIDs and ; use ST_Transform`. -- An SRID outside the supported set. The error lists the supported codes. +- An SRID that names no defined CRS. The error reads `SRID is not defined`, or `SRID is outside 1..=998999` for a code out of range. - A coordinate outside the region the target projection is defined on. +- An `ST_Transform` epoch outside 1900..=2200, or not a finite number. Call `ST_Transform` on one operand to bring both into the same SRID. @@ -158,14 +251,21 @@ A spatial index fixes its SRID at its first entry. It refuses geometries in any ### Measures -Every measure is in meters, and square meters for area. A WGS 84 (`4326`) geometry is measured geodesically. A geometry in a projected SRID is measured in planar grid meters. `ST_Distance` needs both operands in the same SRID. +Every measure is in meters, and square meters for area. A geometry in a geographic CRS, such as `4326`, is measured on geodesics of that CRS's ellipsoid, with Karney's algorithms (GeographicLib). A geometry in a projected SRID is measured in planar grid meters, the CRS unit converted to meters. Two-geometry measures need both operands in the same SRID. | Function | Result | | --- | --- | -| `ST_Distance(a, b)` | Distance between two geometries | +| `ST_Distance(a, b)` | Minimum distance between two geometries | | `ST_Length(geom)` | Total length of linear components; `0` for points and polygons | | `ST_Perimeter(geom)` | Boundary length of areal components, holes included | | `ST_Area(geom)` | Area of areal components, less holes; `0` for points and lines | +| `ST_3DDistance(a, b)` | Minimum 3D distance | +| `ST_3DLength(geom)` | Total 3D length of linear components | + +3D measures read Z as meters, and a missing Z as `0`. A projected CRS +measures Euclidean distance in grid meters. A geographic CRS maps every +position to Earth-centred (ECEF) coordinates on its ellipsoid, with Z as +ellipsoidal height, and measures the straight chord. ### Predicates and operations @@ -174,15 +274,83 @@ Every measure is in meters, and square meters for area. A WGS 84 (`4326`) geomet | `ST_Contains(a, b)` | Whether `a` contains `b` | | `ST_Within(a, b)` | Whether `a` lies within `b` — the converse of `ST_Contains` | | `ST_Intersects(a, b)` / `ST_Disjoint(a, b)` | Whether the two geometries share any point | -| `ST_DWithin(a, b, meters)` | Whether the two geometries lie within a distance: geodesic for `4326`, planar for a projected SRID | -| `ST_Buffer(geom, meters [, segments])` | Geometry grown by a distance | -| `ST_Envelope(geom)` | Bounding rectangle | -| `ST_Centroid(geom)` | Centroid, weighted by the highest dimension present | -| `ST_Union(a, b)` / `ST_Intersection(a, b)` | Combined / shared geometry | - -`ST_Contains`, `ST_Within`, `ST_Intersects` and `ST_DWithin` are answered from -the R\*-tree when the first argument is an indexed geometry column; the rest -evaluate per row. +| `ST_Touches(a, b)` | Whether the boundaries meet and the interiors do not | +| `ST_Crosses(a, b)` | Whether the interiors meet in a lower dimension | +| `ST_Overlaps(a, b)` | Whether two geometries of one dimension share part of their interiors | +| `ST_Equals(a, b)` | Whether the two geometries cover the same points | +| `ST_Covers(a, b)` / `ST_CoveredBy(a, b)` | Whether no point of `b` lies outside `a` / the converse | +| `ST_Relate(a, b)` | DE-9IM intersection matrix, e.g. `212101212` | +| `ST_Relate(a, b, pattern)` | Whether the matrix matches a DE-9IM pattern of `T`, `F`, `*`, `0`, `1`, `2` | +| `ST_DWithin(a, b, meters)` | Whether the two geometries lie within a distance: geodesic in a geographic CRS, planar in a projected one | +| `ST_3DDWithin(a, b, meters)` | Whether the two geometries lie within a 3D distance | +| `ST_3DIntersects(a, b)` | Whether the two geometries meet in 3D | +| `ST_Buffer(geom, meters [, segments])` | Geometry grown by a distance. See [Buffer](#buffer) | +| `ST_Envelope(geom)` | Bounding rectangle; a point or a line for a degenerate box | +| `ST_Centroid(geom)` | Centroid, weighted by the highest dimension present: geodesic area or length in a geographic CRS | +| `ST_Union(a, b)` | Every point of either geometry, overlaps dissolved | +| `ST_Intersection(a, b)` | Points shared by both geometries | +| `ST_Difference(a, b)` | Points of `a` outside `b` | +| `ST_SymDifference(a, b)` | Points in exactly one geometry | +| `ST_ConvexHull(geom)` | Smallest convex polygon holding every vertex | +| `ST_ConcaveHull(geom, ratio)` | Concave polygon holding every vertex; `ratio` 1 gives the convex hull | +| `ST_Simplify(geom, tolerance)` | Ramer–Douglas–Peucker simplification | +| `ST_SimplifyPreserveTopology(geom, tolerance)` | Visvalingam–Whyatt simplification that creates no self-intersection | +| `ST_MakeValid(geom)` | Valid geometry covering the same area | +| `ST_Subdivide(geom [, max_vertices])` | GEOMETRYCOLLECTION of pieces with at most `max_vertices` vertices each, default 256 | +| `ST_Segmentize(geom, max_length)` | Geometry with extra vertices so no edge exceeds `max_length` meters | +| `ST_ClosestPoint(a, b)` | Point of `a` closest to `b` | +| `ST_Split(input, blade)` | GEOMETRYCOLLECTION of the pieces `blade` cuts `input` into | +| `ST_ShiftLongitude(geom)` | Longitudes moved between the -180..180 and 0..360 ranges | + +The overlay operations (`ST_Union`, `ST_Intersection`, `ST_Difference`, +`ST_SymDifference`) accept every geometry kind. A mixed result is a +GEOMETRYCOLLECTION. The overlay, simplification, hulls, `ST_Split` and +`ST_Subdivide` work in the coordinates of the geometry's CRS. `tolerance` +is in CRS units. + +`ST_Contains`, `ST_Within`, `ST_Intersects`, `ST_DWithin` and `ST_3DDWithin` +are answered from the R\*-tree when the first argument is an indexed geometry +column; the rest evaluate per row. The distance predicates widen the search +box to cover every point within the distance, on the smallest radius of +curvature of the ellipsoid. + +### Buffer + +`ST_Buffer` takes a distance in meters in every CRS. A negative distance +shrinks polygons and erases points and lines. Curves have `segments` sides +per full circle, default 32. + +- A projected geometry buffers in its grid. +- A geographic geometry within 12° of longitude and away from the poles buffers in a Transverse Mercator projection centred on it, on its own ellipsoid. The scale error stays under 0.6 % at its edge. +- A geographic geometry past 80° latitude buffers in a polar Lambert azimuthal equal-area projection. +- Any other geographic geometry buffers each component in its own local projection, and unions the pieces. + +### Validity + +`ST_IsValid` follows the OGC simple-features rules: + +- Every coordinate is finite. +- A LineString has at least 2 points. +- A ring has at least 4 points and is closed. +- A ring does not cross itself. +- A hole lies inside its shell, and rings do not overlap or share a line. +- The polygons of a MultiPolygon do not overlap or share a line. + +Ring winding order is not a validity rule. `ST_IsValidReason` names the +component and ring of each error, e.g. `polygon 1 shell: ring crosses +itself`. + +### Linear referencing + +| Function | Result | +| --- | --- | +| `ST_LineInterpolatePoint(line, fraction)` | Point at `fraction` (0 to 1) of the line's length | +| `ST_LocateAlong(geom, m [, offset])` | MultiPoint of every position with measure `m`, moved `offset` CRS units to the left | +| `ST_LocateBetween(geom, from_m, to_m [, offset])` | Parts of the geometry whose measure lies between `from_m` and `to_m`, moved `offset` CRS units to the left | +| `ST_InterpolatePoint(line, point)` | M of the line at the position closest to `point` | + +`ST_LocateAlong`, `ST_LocateBetween` and `ST_InterpolatePoint` need a +geometry with M. ```sql -- Read stored geometry back in either interchange format From c726e41df7e5a270b1bb7c800ce57678c14c1316 Mon Sep 17 00:00:00 2001 From: Farhan Syah Date: Wed, 21 Oct 2026 21:39:12 +0800 Subject: [PATCH 05/12] feat(spatial): add CRS registry, GeoTIFF array load, array attr metadata, and bitemporal graph reads Spatial: - Add nodedb-crs and nodedb-srid crates for coordinate reference system definitions and SRID resolution, with a catalog-backed spatial_ref_sys table and DDL. - Rework nodedb-spatial around typed errors, geodesic and 3D measures, WKT/WKB readers with Z/M ordinates, and overlay ops. - Extend the raster crate with TIFF/COG decoding, codecs, and sources, and add nodedb-raster-load for bulk array load. Array: - Add attribute metadata, reducers, and a physical query layer. Bitemporal and graph: - Add statement-level system/valid-time cuts, columnar system stamps, persisted node labels, and as-of graph reads. Cluster and security: - Purge state on vShard departure, add credential DDL, a JWKS registry, egress policy, and a WAL key vault. --- .github/workflows/fuzz.yml | 4 +- Cargo.lock | 357 +- Cargo.toml | 20 +- fuzz/Cargo.toml | 1 + fuzz/corpus/geotiff_decoder/cog-mask.tif | Bin 0 -> 3324 bytes fuzz/corpus/geotiff_decoder/gdal-metadata.tif | Bin 0 -> 1369 bytes fuzz/corpus/geotiff_decoder/j-gray-strip.tif | Bin 0 -> 689 bytes fuzz/corpus/geotiff_decoder/j-ycbcr-tile.tif | Bin 0 -> 1736 bytes .../geotiff_decoder/l-lerc-pixel-tile.tif | Bin 0 -> 1955 bytes .../l-lerc_deflate-band-strip.tif | Bin 0 -> 1305 bytes .../geotiff_decoder/l-lerc_zstd-mask.tif | Bin 0 -> 1440 bytes .../m-deflate-p3-tile-classic-contig-be.tif | Bin 0 -> 1217 bytes .../m-lzw-p2-tile-big-separate-le.tif | Bin 0 -> 1140 bytes .../m-none-p1-strip-classic-contig-be.tif | Bin 0 -> 1696 bytes ...m-packbits-p1-tile-classic-separate-le.tif | Bin 0 -> 1979 bytes .../m-zstd-p2-strip-big-contig-be.tif | Bin 0 -> 601 bytes fuzz/src/targets/geotiff_decoder.rs | 42 +- nodedb-array/Cargo.toml | 1 + nodedb-array/src/codec/attr_col/column.rs | 30 + nodedb-array/src/codec/attr_col/varlen.rs | 1 + nodedb-array/src/error.rs | 20 + nodedb-array/src/lib.rs | 2 +- nodedb-array/src/query/aggregate.rs | 412 - nodedb-array/src/query/aggregate/fold.rs | 580 + nodedb-array/src/query/aggregate/mod.rs | 18 + nodedb-array/src/query/aggregate/reducer.rs | 35 + nodedb-array/src/query/aggregate/result.rs | 298 + nodedb-array/src/query/mod.rs | 8 +- nodedb-array/src/query/physical.rs | 97 + nodedb-array/src/query/region.rs | 19 +- nodedb-array/src/query/zone_frame.rs | 164 +- nodedb-array/src/schema/array_schema.rs | 127 +- nodedb-array/src/schema/attr_meta.rs | 291 + nodedb-array/src/schema/attr_spec.rs | 21 + nodedb-array/src/schema/builder.rs | 2 +- nodedb-array/src/schema/georef.rs | 153 +- nodedb-array/src/schema/mod.rs | 2 + nodedb-array/src/schema/validation/attrs.rs | 22 + nodedb-array/src/schema/validation/georef.rs | 2 +- nodedb-array/src/tile/mbr.rs | 1 + nodedb-array/src/types/cell_value/narrow.rs | 10 + nodedb-array/src/types/cell_value/value.rs | 6 + nodedb-cluster-tests/Cargo.toml | 2 + .../cases/bitemporal_array_cluster_cut.rs | 143 + .../cases/bitemporal_replica_stamps.rs | 419 + .../cases/bitemporal_txn_columnar.rs | 224 + .../calvin_multi_shard_bitemporal_restart.rs | 58 + .../cases/cluster_array_slice_zone.rs | 10 +- .../cases/cluster_array_ws_rpc.rs | 6 +- .../cases/cluster_restore_columnar_stamps.rs | 135 + .../cluster_restore_documents_restart.rs | 42 +- .../cases/cluster_restore_labels_only_mark.rs | 108 + .../cases/cluster_spatial_ref_sys.rs | 103 + .../cases/columnar_bitemporal_cut_cluster.rs | 140 + .../cases/graph_live_cut_calvin_stamps.rs | 158 + .../tests/common_suite/cases/mod.rs | 12 + .../cases/vshard_departure_crdt.rs | 85 + .../cases/vshard_departure_fixture.rs | 470 + .../cases/vshard_departure_purge.rs | 77 + .../cases/vshard_departure_purge_restart.rs | 101 + .../cases/webhook_sink_partial_replication.rs | 7 +- .../cases/webhook_sink_single_owner.rs | 7 +- .../cases/sql_cluster_cross_node_dml.rs | 6 + .../graph_algo_pagerank_as_of_cross_node.rs | 137 + .../graph_match_label_as_of_cross_node.rs | 153 + .../graph_traverse_as_of_cross_node.rs | 205 + .../coordinator/elementwise.rs | 2 +- .../src/distributed_array/coordinator/read.rs | 7 +- .../src/distributed_array/handler.rs | 15 +- nodedb-cluster/src/distributed_array/merge.rs | 63 +- nodedb-cluster/src/distributed_array/wire.rs | 22 +- nodedb-cluster/src/lib.rs | 6 +- nodedb-cluster/src/raft_loop/mod.rs | 2 +- nodedb-cluster/src/raft_loop/propose_gate.rs | 30 +- .../src/rpc_codec/data_plane_error.rs | 4 + nodedb-columnar/Cargo.toml | 1 + nodedb-columnar/src/error.rs | 7 + nodedb-columnar/src/geometry_cell.rs | 78 + nodedb-columnar/src/lib.rs | 1 + .../src/memtable/column_data/access.rs | 6 +- .../src/memtable/column_data/mutation.rs | 327 - .../memtable/column_data/mutation_ingest.rs | 195 - .../src/memtable/column_data/push.rs | 21 +- nodedb-columnar/src/mutation/engine.rs | 24 +- nodedb-columnar/src/mutation/flush.rs | 11 +- nodedb-columnar/src/mutation/snapshot.rs | 18 + nodedb-columnar/src/mutation/truncate.rs | 4 + nodedb-columnar/src/mutation/write.rs | 278 +- nodedb-columnar/src/reader/cell.rs | 17 +- nodedb-columnar/src/writer/segment_writer.rs | 7 +- nodedb-crdt/Cargo.toml | 1 + nodedb-crdt/src/constraint_checks.rs | 6 +- nodedb-crdt/src/pre_validate.rs | 17 +- nodedb-crdt/src/state/core.rs | 10 + nodedb-crdt/src/validator/incoming.rs | 14 + nodedb-crdt/src/validator/mod.rs | 2 + nodedb-crdt/src/validator/policy_dispatch.rs | 66 +- nodedb-crdt/src/validator/validate.rs | 53 +- .../constraint_resolution/cascade_defer.rs | 35 +- .../tests/constraint_resolution/fk_chain.rs | 35 +- .../constraint_resolution/unique_rename.rs | 35 +- nodedb-crdt/tests/crdt_sql_paradox.rs | 23 +- nodedb-crs/Cargo.toml | 23 + nodedb-crs/NOTICE | 2 + nodedb-crs/README.md | 82 + nodedb-crs/data/epsg.tsv | 5918 ++++ nodedb-crs/data/epsg_supplement.tsv | 12 + nodedb-crs/src/datum/ecef.rs | 96 + nodedb-crs/src/datum/helmert.rs | 261 + nodedb-crs/src/datum/mod.rs | 8 + nodedb-crs/src/def/area.rs | 89 + nodedb-crs/src/def/axis.rs | 226 + nodedb-crs/src/def/crs_def.rs | 129 + nodedb-crs/src/def/datum.rs | 420 + nodedb-crs/src/def/ellipsoid.rs | 349 + nodedb-crs/src/def/epoch.rs | 65 + nodedb-crs/src/def/helmert_rates.rs | 77 + nodedb-crs/src/def/mod.rs | 24 + nodedb-crs/src/def/unit.rs | 131 + nodedb-crs/src/error.rs | 133 + nodedb-crs/src/geo_transform.rs | 174 + nodedb-crs/src/lib.rs | 50 + nodedb-crs/src/method/aea.rs | 358 + nodedb-crs/src/method/aeqd.rs | 576 + nodedb-crs/src/method/angle.rs | 54 + nodedb-crs/src/method/authalic.rs | 227 + nodedb-crs/src/method/cass.rs | 592 + nodedb-crs/src/method/cea.rs | 259 + nodedb-crs/src/method/conformal.rs | 111 + nodedb-crs/src/method/eqc.rs | 245 + nodedb-crs/src/method/eqearth.rs | 255 + nodedb-crs/src/method/geodesic/inverse.rs | 506 + nodedb-crs/src/method/geodesic/line.rs | 405 + nodedb-crs/src/method/geodesic/math.rs | 211 + nodedb-crs/src/method/geodesic/mod.rs | 19 + nodedb-crs/src/method/geodesic/model.rs | 219 + nodedb-crs/src/method/geodesic/polygon.rs | 177 + nodedb-crs/src/method/geodesic/series.rs | 255 + nodedb-crs/src/method/geodesic/start.rs | 344 + nodedb-crs/src/method/geos.rs | 376 + nodedb-crs/src/method/krovak.rs | 534 + nodedb-crs/src/method/labrd.rs | 355 + nodedb-crs/src/method/laea.rs | 573 + nodedb-crs/src/method/lcc.rs | 401 + nodedb-crs/src/method/merc.rs | 268 + nodedb-crs/src/method/meridian.rs | 189 + nodedb-crs/src/method/mod.rs | 58 + nodedb-crs/src/method/nzmg.rs | 293 + .../hom.rs => nodedb-crs/src/method/omerc.rs | 255 +- nodedb-crs/src/method/poly.rs | 353 + nodedb-crs/src/method/projection.rs | 197 + nodedb-crs/src/method/projector.rs | 162 + nodedb-crs/src/method/sinu.rs | 238 + nodedb-crs/src/method/somerc.rs | 331 + nodedb-crs/src/method/stere.rs | 642 + nodedb-crs/src/method/sterea.rs | 320 + .../tm.rs => nodedb-crs/src/method/tmerc.rs | 243 +- nodedb-crs/src/method/webmerc.rs | 194 + nodedb-crs/src/parse/helmert_params.rs | 512 + nodedb-crs/src/parse/json.rs | 347 + nodedb-crs/src/parse/methods/azimuthal.rs | 93 + nodedb-crs/src/parse/methods/conic.rs | 103 + nodedb-crs/src/parse/methods/cylindrical.rs | 53 + nodedb-crs/src/parse/methods/lookup.rs | 169 + nodedb-crs/src/parse/methods/mercator.rs | 109 + nodedb-crs/src/parse/methods/mod.rs | 21 + nodedb-crs/src/parse/methods/params.rs | 82 + nodedb-crs/src/parse/methods/spec.rs | 137 + nodedb-crs/src/parse/mod.rs | 26 + nodedb-crs/src/parse/model.rs | 374 + nodedb-crs/src/parse/proj_string.rs | 513 + nodedb-crs/src/parse/proj_writer.rs | 194 + nodedb-crs/src/parse/projjson.rs | 442 + nodedb-crs/src/parse/projjson_writer.rs | 328 + nodedb-crs/src/parse/round_trip_cases.rs | 51 + nodedb-crs/src/parse/tokens.rs | 203 + nodedb-crs/src/parse/unit_names.rs | 149 + nodedb-crs/src/parse/wkt2.rs | 433 + nodedb-crs/src/parse/wkt2_tree.rs | 285 + nodedb-crs/src/parse/wkt2_writer.rs | 256 + nodedb-crs/src/registry/builtin.rs | 406 + nodedb-crs/src/registry/context.rs | 331 + nodedb-crs/src/registry/custom.rs | 231 + nodedb-crs/src/registry/mod.rs | 16 + nodedb-crs/src/transform.rs | 529 + nodedb-crs/tests/fixtures/proj_reference.tsv | 28755 ++++++++++++++++ .../fixtures/proj_reference_malaysia.tsv | 483 + nodedb-crs/tests/gdm2020.rs | 95 + nodedb-crs/tests/proj_reference.rs | 154 + nodedb-graph/src/csr/index/mutation.rs | 14 + nodedb-graph/src/csr/index/restore.rs | 6 + nodedb-physical/Cargo.toml | 1 + nodedb-physical/src/physical_plan/array.rs | 93 +- .../src/physical_plan/cluster_array.rs | 14 +- nodedb-physical/src/physical_plan/columnar.rs | 70 +- .../src/physical_plan/document/op.rs | 30 +- .../src/physical_plan/document/row_source.rs | 17 +- .../src/physical_plan/graph/algo_stage.rs | 7 +- .../src/physical_plan/graph/batch_edge.rs | 6 + .../src/physical_plan/graph/bsp.rs | 8 +- .../src/physical_plan/graph/mod.rs | 2 + nodedb-physical/src/physical_plan/graph/op.rs | 101 +- .../src/physical_plan/graph/read_scope.rs | 152 + .../src/physical_plan/graph/wcc.rs | 6 +- nodedb-physical/src/physical_plan/meta.rs | 27 + .../src/physical_plan/meta_restore.rs | 37 +- .../src/physical_plan/meta_snapshot.rs | 24 +- nodedb-physical/src/physical_plan/mod.rs | 11 +- nodedb-physical/src/physical_plan/routing.rs | 2 - nodedb-physical/src/physical_plan/spatial.rs | 2 + .../src/physical_plan/truncate_target.rs | 3 + nodedb-query/Cargo.toml | 1 + nodedb-query/src/expr/eval.rs | 143 +- nodedb-query/src/expr_parse/parser.rs | 32 +- nodedb-query/src/expr_parse/predicate.rs | 3 +- nodedb-query/src/functions/eval.rs | 34 +- nodedb-query/src/geo_functions/accessors.rs | 256 +- .../src/geo_functions/catalog/spec.rs | 82 +- .../src/geo_functions/catalog/table.rs | 71 +- .../src/geo_functions/constructors.rs | 707 +- nodedb-query/src/geo_functions/crs.rs | 281 +- nodedb-query/src/geo_functions/dispatch.rs | 65 +- nodedb-query/src/geo_functions/encode.rs | 153 + nodedb-query/src/geo_functions/helpers.rs | 299 +- nodedb-query/src/geo_functions/linear.rs | 180 + nodedb-query/src/geo_functions/measures.rs | 275 +- nodedb-query/src/geo_functions/mod.rs | 7 + nodedb-query/src/geo_functions/overlay.rs | 437 + nodedb-query/src/geo_functions/predicates.rs | 365 +- nodedb-query/src/geo_functions/srid_refs.rs | 168 + nodedb-query/src/json_expr.rs | 3 +- .../src/msgpack_scan/aggregate_helpers.rs | 26 +- nodedb-query/src/msgpack_scan/filter.rs | 152 +- nodedb-query/src/msgpack_scan/group_key.rs | 68 +- nodedb-query/src/scan_filter/types.rs | 15 +- nodedb-query/src/window/aggregate.rs | 93 +- nodedb-query/src/window/arg.rs | 4 +- nodedb-query/src/window/eval.rs | 78 +- nodedb-query/src/window/helpers.rs | 11 +- nodedb-query/src/window/offset.rs | 10 +- nodedb-query/src/window/ranking.rs | 19 +- nodedb-query/src/window/running.rs | 4 +- nodedb-query/src/window/value_agg.rs | 33 +- nodedb-query/src/window/value_eval.rs | 157 +- nodedb-query/src/window/value_partition.rs | 9 +- nodedb-raster-load/Cargo.toml | 17 + nodedb-raster-load/NOTICE | 2 + nodedb-raster-load/src/bands.rs | 276 + nodedb-raster-load/src/coords.rs | 139 + nodedb-raster-load/src/error.rs | 216 + nodedb-raster-load/src/grid.rs | 361 + nodedb-raster-load/src/lib.rs | 43 + nodedb-raster-load/src/metadata.rs | 75 + nodedb-raster-load/src/nodata.rs | 106 + nodedb-raster-load/src/options.rs | 30 + nodedb-raster-load/src/plan.rs | 360 + nodedb-raster-load/src/rows.rs | 430 + nodedb-raster-load/src/source.rs | 215 + nodedb-raster-load/src/view/mod.rs | 13 + nodedb-raster-load/src/view/row.rs | 36 + nodedb-raster-load/src/view/schema.rs | 123 + nodedb-raster-load/src/window.rs | 121 + nodedb-raster/Cargo.toml | 13 +- nodedb-raster/src/block.rs | 248 +- nodedb-raster/src/codec/compression.rs | 172 + nodedb-raster/src/codec/deflate.rs | 128 + nodedb-raster/src/codec/error.rs | 79 + nodedb-raster/src/codec/jpeg.rs | 123 + nodedb-raster/src/codec/lerc.rs | 259 + nodedb-raster/src/codec/lerc_blob.rs | 135 + nodedb-raster/src/codec/lzw.rs | 113 + nodedb-raster/src/codec/mod.rs | 18 + nodedb-raster/src/codec/packbits.rs | 131 + nodedb-raster/src/codec/predictor.rs | 193 + nodedb-raster/src/codec/zstd.rs | 110 + nodedb-raster/src/crs.rs | 119 - nodedb-raster/src/crs/mod.rs | 14 + nodedb-raster/src/crs/projection_keys.rs | 312 + nodedb-raster/src/crs/registry_match.rs | 200 + nodedb-raster/src/crs/srid.rs | 403 + nodedb-raster/src/crs/units.rs | 94 + nodedb-raster/src/crs/user_defined.rs | 224 + nodedb-raster/src/error.rs | 196 +- nodedb-raster/src/header.rs | 100 + nodedb-raster/src/layout.rs | 92 +- nodedb-raster/src/lib.rs | 40 +- nodedb-raster/src/limits.rs | 89 +- nodedb-raster/src/preflight.rs | 288 - nodedb-raster/src/reader.rs | 267 - nodedb-raster/src/reader/driver.rs | 149 + nodedb-raster/src/reader/mod.rs | 9 + nodedb-raster/src/reader/traits.rs | 60 + nodedb-raster/src/sample.rs | 82 +- nodedb-raster/src/source/cache.rs | 171 + nodedb-raster/src/source/mod.rs | 9 + nodedb-raster/src/source/range.rs | 89 + nodedb-raster/src/tiff/byte_order.rs | 84 + nodedb-raster/src/tiff/chain.rs | 99 + nodedb-raster/src/tiff/decode.rs | 103 + nodedb-raster/src/tiff/error.rs | 102 + nodedb-raster/src/tiff/fetch.rs | 126 + nodedb-raster/src/tiff/file_header.rs | 126 + nodedb-raster/src/tiff/gdal_metadata.rs | 222 + nodedb-raster/src/tiff/geo.rs | 94 + nodedb-raster/src/{ => tiff}/geokeys.rs | 74 + nodedb-raster/src/tiff/ifd.rs | 243 + nodedb-raster/src/tiff/image.rs | 299 + nodedb-raster/src/tiff/mask.rs | 225 + nodedb-raster/src/tiff/mod.rs | 27 + nodedb-raster/src/tiff/opener.rs | 207 + nodedb-raster/src/tiff/reader.rs | 234 + nodedb-raster/src/tiff/samples.rs | 171 + nodedb-raster/src/tiff/tags.rs | 158 + nodedb-raster/src/tiff/tile_index.rs | 128 + nodedb-raster/src/{ => tiff}/transform.rs | 112 +- nodedb-raster/src/tiff/values.rs | 248 + .../tests/fixtures/cog-mask-0.mask.z | Bin 0 -> 35 bytes .../tests/fixtures/cog-mask-1.mask.z | Bin 0 -> 21 bytes nodedb-raster/tests/fixtures/cog-mask-1.raw.z | Bin 0 -> 1194 bytes .../tests/fixtures/cog-mask-2.mask.z | Bin 0 -> 18 bytes nodedb-raster/tests/fixtures/cog-mask-2.raw.z | 3 + nodedb-raster/tests/fixtures/cog-mask.tif | Bin 0 -> 3324 bytes .../tests/fixtures/gdal-metadata.tif | Bin 0 -> 1369 bytes nodedb-raster/tests/fixtures/gen_fixtures.py | 287 + .../tests/fixtures/j-gray-strip.raw.z | Bin 0 -> 869 bytes nodedb-raster/tests/fixtures/j-gray-strip.tif | Bin 0 -> 689 bytes nodedb-raster/tests/fixtures/j-rgb-tile.raw.z | Bin 0 -> 2266 bytes nodedb-raster/tests/fixtures/j-rgb-tile.tif | Bin 0 -> 1656 bytes .../tests/fixtures/j-ycbcr-tile.raw.z | Bin 0 -> 2597 bytes nodedb-raster/tests/fixtures/j-ycbcr-tile.tif | Bin 0 -> 1736 bytes .../tests/fixtures/l-lerc-band-strip.tif | Bin 0 -> 1769 bytes .../tests/fixtures/l-lerc-band-tile.tif | Bin 0 -> 2263 bytes .../tests/fixtures/l-lerc-bigtiff.tif | Bin 0 -> 1388 bytes .../tests/fixtures/l-lerc-mask.mask.z | Bin 0 -> 19 bytes .../tests/fixtures/l-lerc-mask.raw.z | Bin 0 -> 662 bytes nodedb-raster/tests/fixtures/l-lerc-mask.tif | Bin 0 -> 2097 bytes .../tests/fixtures/l-lerc-pixel-strip.tif | Bin 0 -> 1461 bytes .../tests/fixtures/l-lerc-pixel-tile.tif | Bin 0 -> 1955 bytes .../fixtures/l-lerc_deflate-band-strip.tif | Bin 0 -> 1305 bytes .../fixtures/l-lerc_deflate-band-tile.tif | Bin 0 -> 1401 bytes .../tests/fixtures/l-lerc_deflate-mask.mask.z | Bin 0 -> 19 bytes .../tests/fixtures/l-lerc_deflate-mask.raw.z | Bin 0 -> 662 bytes .../tests/fixtures/l-lerc_deflate-mask.tif | Bin 0 -> 1300 bytes .../fixtures/l-lerc_deflate-pixel-strip.tif | Bin 0 -> 866 bytes .../fixtures/l-lerc_deflate-pixel-tile.tif | Bin 0 -> 1080 bytes .../tests/fixtures/l-lerc_zstd-band-strip.tif | Bin 0 -> 1461 bytes .../tests/fixtures/l-lerc_zstd-band-tile.tif | Bin 0 -> 1494 bytes .../tests/fixtures/l-lerc_zstd-mask.mask.z | Bin 0 -> 19 bytes .../tests/fixtures/l-lerc_zstd-mask.raw.z | Bin 0 -> 662 bytes .../tests/fixtures/l-lerc_zstd-mask.tif | Bin 0 -> 1440 bytes .../fixtures/l-lerc_zstd-pixel-strip.tif | Bin 0 -> 934 bytes .../tests/fixtures/l-lerc_zstd-pixel-tile.tif | Bin 0 -> 1104 bytes .../tests/fixtures/landsat-c2-1.raw.z | Bin 0 -> 1730 bytes nodedb-raster/tests/fixtures/landsat-c2.raw.z | Bin 0 -> 4121 bytes nodedb-raster/tests/fixtures/landsat-c2.tif | Bin 0 -> 5491 bytes .../m-deflate-p1-strip-big-contig-be.tif | Bin 0 -> 1575 bytes .../m-deflate-p1-strip-big-separate-le.tif | Bin 0 -> 1791 bytes .../m-deflate-p1-strip-classic-contig-be.tif | Bin 0 -> 1447 bytes ...m-deflate-p1-strip-classic-separate-le.tif | Bin 0 -> 1631 bytes .../m-deflate-p1-tile-big-contig-be.tif | Bin 0 -> 1659 bytes .../m-deflate-p1-tile-big-separate-le.tif | Bin 0 -> 1809 bytes .../m-deflate-p1-tile-classic-contig-be.tif | Bin 0 -> 1531 bytes .../m-deflate-p1-tile-classic-separate-le.tif | Bin 0 -> 1649 bytes .../m-deflate-p2-strip-big-contig-be.tif | Bin 0 -> 571 bytes .../m-deflate-p2-strip-big-separate-le.tif | Bin 0 -> 653 bytes .../m-deflate-p2-strip-classic-contig-be.tif | Bin 0 -> 443 bytes ...m-deflate-p2-strip-classic-separate-le.tif | Bin 0 -> 493 bytes .../m-deflate-p2-tile-big-contig-be.tif | Bin 0 -> 780 bytes .../m-deflate-p2-tile-big-separate-le.tif | Bin 0 -> 916 bytes .../m-deflate-p2-tile-classic-contig-be.tif | Bin 0 -> 636 bytes .../m-deflate-p2-tile-classic-separate-le.tif | Bin 0 -> 756 bytes .../m-deflate-p3-strip-big-contig-be.tif | Bin 0 -> 1056 bytes .../m-deflate-p3-strip-big-separate-le.tif | Bin 0 -> 1206 bytes .../m-deflate-p3-strip-classic-contig-be.tif | Bin 0 -> 912 bytes ...m-deflate-p3-strip-classic-separate-le.tif | Bin 0 -> 1046 bytes .../m-deflate-p3-tile-big-contig-be.tif | Bin 0 -> 1377 bytes .../m-deflate-p3-tile-big-separate-le.tif | Bin 0 -> 1642 bytes .../m-deflate-p3-tile-classic-contig-be.tif | Bin 0 -> 1217 bytes .../m-deflate-p3-tile-classic-separate-le.tif | Bin 0 -> 1466 bytes .../fixtures/m-lzw-p1-strip-big-contig-be.tif | Bin 0 -> 1871 bytes .../m-lzw-p1-strip-big-separate-le.tif | Bin 0 -> 2075 bytes .../m-lzw-p1-strip-classic-contig-be.tif | Bin 0 -> 1743 bytes .../m-lzw-p1-strip-classic-separate-le.tif | Bin 0 -> 1915 bytes .../fixtures/m-lzw-p1-tile-big-contig-be.tif | Bin 0 -> 2070 bytes .../m-lzw-p1-tile-big-separate-le.tif | Bin 0 -> 2360 bytes .../m-lzw-p1-tile-classic-contig-be.tif | Bin 0 -> 1942 bytes .../m-lzw-p1-tile-classic-separate-le.tif | Bin 0 -> 2200 bytes .../fixtures/m-lzw-p2-strip-big-contig-be.tif | Bin 0 -> 655 bytes .../m-lzw-p2-strip-big-separate-le.tif | Bin 0 -> 781 bytes .../m-lzw-p2-strip-classic-contig-be.tif | Bin 0 -> 527 bytes .../m-lzw-p2-strip-classic-separate-le.tif | Bin 0 -> 621 bytes .../fixtures/m-lzw-p2-tile-big-contig-be.tif | Bin 0 -> 942 bytes .../m-lzw-p2-tile-big-separate-le.tif | Bin 0 -> 1140 bytes .../m-lzw-p2-tile-classic-contig-be.tif | Bin 0 -> 798 bytes .../m-lzw-p2-tile-classic-separate-le.tif | Bin 0 -> 980 bytes .../fixtures/m-lzw-p3-strip-big-contig-be.tif | Bin 0 -> 1206 bytes .../m-lzw-p3-strip-big-separate-le.tif | Bin 0 -> 1316 bytes .../m-lzw-p3-strip-classic-contig-be.tif | Bin 0 -> 1062 bytes .../m-lzw-p3-strip-classic-separate-le.tif | Bin 0 -> 1156 bytes .../fixtures/m-lzw-p3-tile-big-contig-be.tif | Bin 0 -> 1627 bytes .../m-lzw-p3-tile-big-separate-le.tif | Bin 0 -> 1798 bytes .../m-lzw-p3-tile-classic-contig-be.tif | Bin 0 -> 1467 bytes .../m-lzw-p3-tile-classic-separate-le.tif | Bin 0 -> 1622 bytes .../m-none-p1-strip-big-contig-be.tif | Bin 0 -> 1824 bytes .../m-none-p1-strip-big-separate-le.tif | Bin 0 -> 1872 bytes .../m-none-p1-strip-classic-contig-be.tif | Bin 0 -> 1696 bytes .../m-none-p1-strip-classic-separate-le.tif | Bin 0 -> 1712 bytes .../fixtures/m-none-p1-tile-big-contig-be.tif | Bin 0 -> 4496 bytes .../m-none-p1-tile-big-separate-le.tif | Bin 0 -> 4544 bytes .../m-none-p1-tile-classic-contig-be.tif | Bin 0 -> 4368 bytes .../m-none-p1-tile-classic-separate-le.tif | Bin 0 -> 4384 bytes .../m-packbits-p1-strip-big-contig-be.tif | Bin 0 -> 1842 bytes .../m-packbits-p1-strip-big-separate-le.tif | Bin 0 -> 1908 bytes .../m-packbits-p1-strip-classic-contig-be.tif | Bin 0 -> 1714 bytes ...-packbits-p1-strip-classic-separate-le.tif | Bin 0 -> 1748 bytes .../m-packbits-p1-tile-big-contig-be.tif | Bin 0 -> 1968 bytes .../m-packbits-p1-tile-big-separate-le.tif | Bin 0 -> 2139 bytes .../m-packbits-p1-tile-classic-contig-be.tif | Bin 0 -> 1840 bytes ...m-packbits-p1-tile-classic-separate-le.tif | Bin 0 -> 1979 bytes .../m-zstd-p1-strip-big-contig-be.tif | Bin 0 -> 1582 bytes .../m-zstd-p1-strip-big-separate-le.tif | Bin 0 -> 1728 bytes .../m-zstd-p1-strip-classic-contig-be.tif | Bin 0 -> 1454 bytes .../m-zstd-p1-strip-classic-separate-le.tif | Bin 0 -> 1568 bytes .../fixtures/m-zstd-p1-tile-big-contig-be.tif | Bin 0 -> 1641 bytes .../m-zstd-p1-tile-big-separate-le.tif | Bin 0 -> 1847 bytes .../m-zstd-p1-tile-classic-contig-be.tif | Bin 0 -> 1513 bytes .../m-zstd-p1-tile-classic-separate-le.tif | Bin 0 -> 1687 bytes .../m-zstd-p2-strip-big-contig-be.tif | Bin 0 -> 601 bytes .../m-zstd-p2-strip-big-separate-le.tif | Bin 0 -> 727 bytes .../m-zstd-p2-strip-classic-contig-be.tif | Bin 0 -> 473 bytes .../m-zstd-p2-strip-classic-separate-le.tif | Bin 0 -> 567 bytes .../fixtures/m-zstd-p2-tile-big-contig-be.tif | Bin 0 -> 853 bytes .../m-zstd-p2-tile-big-separate-le.tif | Bin 0 -> 1103 bytes .../m-zstd-p2-tile-classic-contig-be.tif | Bin 0 -> 709 bytes .../m-zstd-p2-tile-classic-separate-le.tif | Bin 0 -> 943 bytes .../m-zstd-p3-strip-big-contig-be.tif | Bin 0 -> 1134 bytes .../m-zstd-p3-strip-big-separate-le.tif | Bin 0 -> 1397 bytes .../m-zstd-p3-strip-classic-contig-be.tif | Bin 0 -> 990 bytes .../m-zstd-p3-strip-classic-separate-le.tif | Bin 0 -> 1237 bytes .../fixtures/m-zstd-p3-tile-big-contig-be.tif | Bin 0 -> 1427 bytes .../m-zstd-p3-tile-big-separate-le.tif | Bin 0 -> 1679 bytes .../m-zstd-p3-tile-classic-contig-be.tif | Bin 0 -> 1267 bytes .../m-zstd-p3-tile-classic-separate-le.tif | Bin 0 -> 1503 bytes nodedb-raster/tests/fixtures/manifest.tsv | 120 + nodedb-raster/tests/fixtures/t-float64.tif | Bin 0 -> 1183 bytes nodedb-raster/tests/fixtures/t-int16.tif | Bin 0 -> 933 bytes nodedb-raster/tests/fixtures/t-int32.tif | Bin 0 -> 1049 bytes nodedb-raster/tests/fixtures/t-int8.tif | Bin 0 -> 665 bytes nodedb-raster/tests/fixtures/t-uint32.tif | Bin 0 -> 999 bytes nodedb-raster/tests/fixtures/t-uint8.tif | Bin 0 -> 648 bytes nodedb-raster/tests/geotiff/cog.rs | 141 + nodedb-raster/tests/geotiff/files.rs | 222 + nodedb-raster/tests/geotiff/fixtures.rs | 47 +- nodedb-raster/tests/geotiff/georef.rs | 44 +- nodedb-raster/tests/geotiff/image.rs | 120 + nodedb-raster/tests/geotiff/main.rs | 7 +- nodedb-raster/tests/geotiff/malformed.rs | 195 + nodedb-raster/tests/geotiff/robustness.rs | 21 +- nodedb-raster/tests/geotiff/strips.rs | 51 +- nodedb-raster/tests/geotiff/tiles.rs | 32 +- nodedb-spatial/Cargo.toml | 2 + nodedb-spatial/src/crs/ellipsoid.rs | 69 - nodedb-spatial/src/crs/error.rs | 130 - nodedb-spatial/src/crs/mod.rs | 16 +- nodedb-spatial/src/crs/params.rs | 103 - nodedb-spatial/src/crs/transform.rs | 274 +- nodedb-spatial/src/distance3d.rs | 363 + nodedb-spatial/src/error.rs | 49 + nodedb-spatial/src/geo_meta.rs | 217 +- nodedb-spatial/src/geodesic/measure.rs | 268 + nodedb-spatial/src/geodesic/mod.rs | 8 + nodedb-spatial/src/geodesic/segment.rs | 91 + nodedb-spatial/src/hybrid.rs | 60 +- nodedb-spatial/src/lib.rs | 49 +- nodedb-spatial/src/measures.rs | 261 +- nodedb-spatial/src/metric.rs | 320 + nodedb-spatial/src/operations.rs | 503 +- nodedb-spatial/src/ops/boolean.rs | 436 + nodedb-spatial/src/ops/buffer.rs | 365 + nodedb-spatial/src/ops/closest.rs | 101 + nodedb-spatial/src/ops/convert.rs | 146 + nodedb-spatial/src/ops/hull.rs | 116 + nodedb-spatial/src/ops/linear.rs | 496 + nodedb-spatial/src/ops/mod.rs | 37 + nodedb-spatial/src/ops/noding.rs | 137 + nodedb-spatial/src/ops/pieces.rs | 158 + nodedb-spatial/src/ops/relate.rs | 115 + nodedb-spatial/src/ops/repair.rs | 114 + nodedb-spatial/src/ops/simplify.rs | 97 + nodedb-spatial/src/ops/split.rs | 451 + nodedb-spatial/src/ops/subdivide.rs | 157 + nodedb-spatial/src/ops/tile.rs | 153 + nodedb-spatial/src/orient.rs | 26 +- nodedb-spatial/src/parts.rs | 92 + nodedb-spatial/src/persist.rs | 63 +- nodedb-spatial/src/planar/area.rs | 17 +- nodedb-spatial/src/planar/buffer.rs | 37 - nodedb-spatial/src/planar/distance.rs | 107 +- nodedb-spatial/src/planar/mod.rs | 5 +- nodedb-spatial/src/predicates/contains.rs | 6 +- nodedb-spatial/src/predicates/distance.rs | 291 +- nodedb-spatial/src/predicates/edge.rs | 7 +- nodedb-spatial/src/predicates/intersection.rs | 336 - nodedb-spatial/src/predicates/intersects.rs | 9 +- nodedb-spatial/src/predicates/mod.rs | 2 - nodedb-spatial/src/rtree/bulk_load.rs | 22 +- nodedb-spatial/src/rtree/mod.rs | 2 + nodedb-spatial/src/rtree/tree.rs | 67 +- nodedb-spatial/src/rtree/tree_srid.rs | 64 + nodedb-spatial/src/spatial_join.rs | 54 +- nodedb-spatial/src/test_support.rs | 13 + nodedb-spatial/src/text.rs | 140 + nodedb-spatial/src/validate.rs | 542 +- nodedb-spatial/src/value.rs | 120 + nodedb-spatial/src/wkb/error.rs | 44 + nodedb-spatial/src/wkb/ewkb.rs | 58 +- nodedb-spatial/src/wkb/format.rs | 123 +- nodedb-spatial/src/wkb/mod.rs | 23 +- nodedb-spatial/src/wkb/read.rs | 833 +- nodedb-spatial/src/wkb/write.rs | 405 +- nodedb-spatial/src/wkt.rs | 451 - nodedb-spatial/src/wkt/error.rs | 53 + nodedb-spatial/src/wkt/mod.rs | 17 + nodedb-spatial/src/wkt/read.rs | 724 + nodedb-spatial/src/wkt/write.rs | 152 + nodedb-sql/Cargo.toml | 1 + nodedb-sql/src/catalog.rs | 9 + nodedb-sql/src/ddl_ast/graph_parse/cursor.rs | 20 + nodedb-sql/src/ddl_ast/graph_parse/entry.rs | 218 +- .../src/ddl_ast/graph_parse/variants.rs | 54 +- nodedb-sql/src/ddl_ast/mod.rs | 4 +- nodedb-sql/src/ddl_ast/parse/audit_text.rs | 137 + nodedb-sql/src/ddl_ast/parse/copy_from.rs | 121 +- nodedb-sql/src/ddl_ast/parse/copy_options.rs | 84 +- nodedb-sql/src/ddl_ast/parse/credential.rs | 273 + nodedb-sql/src/ddl_ast/parse/dispatch.rs | 10 +- nodedb-sql/src/ddl_ast/parse/mod.rs | 4 + .../src/ddl_ast/parse/spatial_ref_sys.rs | 154 + .../src/ddl_ast/statement/copy_source.rs | 136 + .../src/ddl_ast/statement/credential.rs | 72 + .../src/ddl_ast/statement/maintenance.rs | 35 +- nodedb-sql/src/ddl_ast/statement/mod.rs | 4 + .../src/ddl_ast/statement/types/graph.rs | 26 +- .../src/ddl_ast/statement/types/misc.rs | 50 +- .../builtins/scalars/spatial/arg_shape.rs | 59 +- .../builtins/scalars/spatial/triggers.rs | 11 +- nodedb-sql/src/functions/registry.rs | 1 + nodedb-sql/src/lib.rs | 2 +- nodedb-sql/src/parser/array_stmt/ast.rs | 10 +- .../src/parser/array_stmt/parse_impl/alter.rs | 14 +- .../parser/array_stmt/parse_impl/attr_meta.rs | 221 + .../parser/array_stmt/parse_impl/create.rs | 36 +- .../parser/array_stmt/parse_impl/georef.rs | 34 +- .../src/parser/array_stmt/parse_impl/mod.rs | 1 + nodedb-sql/src/parser/normalize.rs | 16 +- nodedb-sql/src/parser/preprocess/temporal.rs | 82 +- nodedb-sql/src/planner/array_ddl.rs | 119 +- nodedb-sql/src/planner/array_dml.rs | 1 + nodedb-sql/src/planner/array_fn/agg_args.rs | 217 + nodedb-sql/src/planner/array_fn/helpers.rs | 134 +- nodedb-sql/src/planner/array_fn/maint_fn.rs | 25 +- nodedb-sql/src/planner/array_fn/mod.rs | 1 + nodedb-sql/src/planner/array_fn/pushdown.rs | 2 + nodedb-sql/src/planner/array_fn/table_fn.rs | 71 +- nodedb-sql/src/planner/array_fn/zone.rs | 41 +- nodedb-sql/src/planner/array_georef.rs | 56 +- .../src/planner/bitmap_emit/predicate.rs | 31 +- nodedb-sql/src/planner/catalog_expr_fold.rs | 11 +- nodedb-sql/src/planner/const_fold.rs | 104 +- .../src/planner/declared_type_coerce.rs | 3 +- .../src/planner/declared_vector_coerce.rs | 1 + nodedb-sql/src/planner/defaults/compiled.rs | 1 + nodedb-sql/src/planner/defaults/convert.rs | 2 + nodedb-sql/src/planner/dml/target.rs | 2 +- nodedb-sql/src/planner/dml/update_delete.rs | 4 +- .../src/planner/dml_helpers/ast_extract.rs | 25 +- .../src/planner/dml_helpers/kv_insert.rs | 3 +- .../src/planner/dml_helpers/value_convert.rs | 22 +- .../src/planner/geometry_expr/resolve.rs | 114 +- nodedb-sql/src/planner/search_scope/lookup.rs | 1 + .../src/planner/select/array_cell_geom.rs | 1 + nodedb-sql/src/planner/select/array_from.rs | 1 + nodedb-sql/src/planner/select/cte_catalog.rs | 4 + .../src/planner/select/entry/fixtures.rs | 1 + nodedb-sql/src/planner/select/select_stmt.rs | 9 +- nodedb-sql/src/planner/select/where_search.rs | 55 +- nodedb-sql/src/resolver/array_tvf.rs | 160 +- nodedb-sql/src/temporal.rs | 5 +- nodedb-sql/src/types/collection.rs | 2 +- nodedb-sql/src/types/plan/variants/array.rs | 8 + nodedb-sql/src/types/query.rs | 1 + nodedb-sql/src/types_array.rs | 273 - nodedb-sql/src/types_array/attr.rs | 120 + nodedb-sql/src/types_array/mod.rs | 26 + nodedb-sql/src/types_array/ops.rs | 117 + nodedb-sql/src/types_array/query.rs | 89 + nodedb-sql/src/types_array/schema.rs | 120 + nodedb-sql/src/types_expr.rs | 3 + nodedb-sql/src/visitor/plan_visitor/args.rs | 10 +- .../src/visitor/plan_visitor/dispatch_rest.rs | 6 + nodedb-srid/Cargo.toml | 21 + nodedb-srid/NOTICE | 2 + nodedb-srid/src/error.rs | 96 + nodedb-srid/src/kind.rs | 57 + nodedb-srid/src/lib.rs | 22 + nodedb-srid/src/name.rs | 154 + nodedb-srid/src/resolver.rs | 76 + nodedb-srid/src/srid.rs | 255 + nodedb-strict/Cargo.toml | 1 + nodedb-strict/src/decode.rs | 4 +- nodedb-strict/src/decode/value.rs | 51 +- nodedb-strict/src/encode.rs | 10 +- nodedb-strict/src/encode/value.rs | 34 +- nodedb-strict/src/error.rs | 7 + nodedb-test-support/Cargo.toml | 1 + nodedb-test-support/src/booted_state.rs | 1 + .../node/inspect/core_placement.rs | 22 +- .../node/lifecycle/spawn_full.rs | 11 + .../src/cluster_harness/pitr.rs | 4 +- .../src/cluster_harness/retriable.rs | 5 +- nodedb-test-support/src/core_loop_runner.rs | 6 + nodedb-test-support/src/jwks_fixture.rs | 29 +- .../src/pgwire_harness/multicore.rs | 1 + .../src/pgwire_harness/restart.rs | 1 + .../src/pgwire_harness/start.rs | 1 + nodedb-test-support/src/sync_client.rs | 42 +- nodedb-test-support/src/tx_batch_helpers.rs | 4 + nodedb-types/Cargo.toml | 2 + nodedb-types/src/array_agg/dist.rs | 436 + nodedb-types/src/array_agg/dist_acc.rs | 246 + nodedb-types/src/array_agg/histogram.rs | 185 + nodedb-types/src/array_agg/mod.rs | 19 + nodedb-types/src/array_agg/moments.rs | 112 + nodedb-types/src/bbox.rs | 76 +- nodedb-types/src/columnar/dml_wal_record.rs | 12 + nodedb-types/src/columnar/image_wal_record.rs | 10 + .../src/columnar/resolved_dml_wal_record.rs | 11 + nodedb-types/src/columnar/schema.rs | 44 +- .../src/columnar/truncate_wal_record.rs | 20 +- nodedb-types/src/columnar/wal_record.rs | 10 + nodedb-types/src/config/tuning/engines.rs | 15 + nodedb-types/src/geometry/math.rs | 6 +- nodedb-types/src/geometry/mod.rs | 17 +- nodedb-types/src/geometry/ordinates.rs | 323 + nodedb-types/src/geometry/shape.rs | 241 +- nodedb-types/src/geometry/srid.rs | 169 +- nodedb-types/src/geometry/tagged.rs | 368 - .../src/geometry/tagged/crs_member.rs | 215 + .../src/geometry/tagged/geojson_read.rs | 388 + .../src/geometry/tagged/geojson_write.rs | 164 + nodedb-types/src/geometry/tagged/mod.rs | 15 + nodedb-types/src/geometry/tagged/msgpack.rs | 123 + .../src/geometry/tagged/srid_geometry.rs | 408 + nodedb-types/src/hlc.rs | 20 +- nodedb-types/src/json_msgpack/geometry_ext.rs | 49 + nodedb-types/src/json_msgpack/mod.rs | 2 + nodedb-types/src/json_msgpack/reader/json.rs | 29 +- .../src/json_msgpack/reader/native.rs | 10 +- nodedb-types/src/json_msgpack/transcoder.rs | 35 +- nodedb-types/src/json_msgpack/writer.rs | 11 +- nodedb-types/src/lib.rs | 3 + nodedb-types/src/sync/wire/delta.rs | 7 + nodedb-types/src/sync/wire/mod.rs | 3 +- nodedb-types/src/sync/wire/session.rs | 104 +- nodedb-types/src/sync/wire/spatial.rs | 2 +- nodedb-types/src/temporal/ordinal.rs | 62 +- nodedb-types/src/temporal/system_time.rs | 112 +- nodedb-types/src/value/msgpack.rs | 36 +- nodedb-types/src/value/sql_literal.rs | 7 +- nodedb-types/src/wall_clock.rs | 50 + nodedb/Cargo.toml | 2 + nodedb/src/bootstrap/catalog_open.rs | 7 + nodedb/src/bootstrap/data_plane.rs | 36 + nodedb/src/bootstrap/jwks_registry.rs | 50 + nodedb/src/bootstrap/mod.rs | 2 + nodedb/src/bootstrap/remote_sources.rs | 48 + nodedb/src/bootstrap/state_wiring.rs | 42 +- nodedb/src/bootstrap/wal_init.rs | 11 +- nodedb/src/bridge/envelope/applied_entry.rs | 36 + nodedb/src/bridge/envelope/error_code.rs | 4 + nodedb/src/bridge/envelope/error_code_from.rs | 9 +- nodedb/src/bridge/envelope/mod.rs | 2 + nodedb/src/bridge/envelope/request.rs | 12 +- nodedb/src/bridge/envelope/response.rs | 23 +- nodedb/src/config/auth/jwt.rs | 69 +- nodedb/src/config/server/config.rs | 17 +- nodedb/src/config/server/domain.rs | 1 + nodedb/src/config/server/egress.rs | 101 + nodedb/src/config/server/mod.rs | 2 + nodedb/src/config/server/tls.rs | 143 +- nodedb/src/control/array_catalog/attr_meta.rs | 173 + nodedb/src/control/array_catalog/ddl.rs | 7 +- nodedb/src/control/array_catalog/georef.rs | 125 +- nodedb/src/control/array_catalog/mod.rs | 2 + .../control/array_catalog/schema_recode.rs | 75 + .../control/array_sync/raft_apply/common.rs | 2 + .../backup/restore/columnar_reissue.rs | 43 +- .../backup/restore/orchestrate/database.rs | 2 + .../backup/restore/orchestrate/reissue.rs | 4 + .../backup/restore/redo_reissue/commit.rs | 48 +- .../backup/restore/redo_reissue/documents.rs | 12 +- .../backup/restore/redo_reissue/edges.rs | 11 +- .../backup/restore/redo_reissue/labels.rs | 132 + .../backup/restore/redo_reissue/mod.rs | 5 +- .../backup/restore/redo_reissue/prepared.rs | 2 +- .../backup/restore/redo_reissue/reissue.rs | 15 +- .../backup/restore/redo_reissue/sub_record.rs | 18 +- nodedb/src/control/backup/restore/sections.rs | 5 + nodedb/src/control/backup/snapshot_keys.rs | 8 + .../control/catalog_entry/apply/credential.rs | 30 + .../control/catalog_entry/apply/dispatch.rs | 16 +- nodedb/src/control/catalog_entry/apply/mod.rs | 2 + .../catalog_entry/apply/spatial_ref_sys.rs | 84 + .../control/catalog_entry/authorization.rs | 6 +- .../control/catalog_entry/descriptor_stamp.rs | 6 +- nodedb/src/control/catalog_entry/entry.rs | 23 + nodedb/src/control/catalog_entry/kind.rs | 4 + .../post_apply/async_dispatch/dispatcher.rs | 13 + .../post_apply/async_dispatch/mod.rs | 1 + .../async_dispatch/spatial_ref_sys.rs | 47 + .../post_apply/gateway_invalidation.rs | 14 + .../control/catalog_entry/post_apply/mod.rs | 2 + .../catalog_entry/post_apply/oidc_provider.rs | 33 + .../control/catalog_entry/post_apply/sync.rs | 18 +- nodedb/src/control/clone/resolver/resolve.rs | 24 +- nodedb/src/control/clone/resolver/rewrite.rs | 52 +- .../control/clone/resolver/rewrite_engine.rs | 4 +- .../cluster/array_cluster_exec/elementwise.rs | 8 +- .../cluster/array_cluster_exec/executor.rs | 26 +- .../control/cluster/array_cluster_helpers.rs | 270 +- .../cluster/array_executor/elementwise.rs | 5 +- .../cluster/array_executor/executor.rs | 18 +- .../control/cluster/array_executor/read.rs | 4 +- .../control/cluster/array_executor/refusal.rs | 1 + .../calvin/scheduler/driver/core/routing.rs | 9 +- .../control/cluster/data_plane_error_wire.rs | 4 + .../control/cluster/execution_error_wire.rs | 1 + .../metadata_applier/audit_describe.rs | 10 + .../cluster/metadata_applier/pending_ddl.rs | 2 +- .../control/cluster/metadata_applier/wedge.rs | 1 + .../cluster/metadata_image/reconcile.rs | 8 +- .../control/cluster/metadata_image/reload.rs | 1 + nodedb/src/control/cluster/mod.rs | 1 + .../src/control/cluster/snapshot_applier.rs | 12 +- .../src/control/cluster/snapshot_builder.rs | 10 +- .../cluster/snapshot_builder_filter.rs | 31 +- .../control/cluster/snapshot_install/split.rs | 38 +- .../src/control/cluster/start_raft_helpers.rs | 8 + .../control/cluster/vshard_departure/fence.rs | 253 + .../cluster/vshard_departure/ledger.rs | 98 + .../control/cluster/vshard_departure/mod.rs | 16 + .../control/cluster/vshard_departure/pass.rs | 297 + .../control/cluster/vshard_departure/purge.rs | 206 + .../cluster/vshard_departure/resident.rs | 125 + .../control/cluster/vshard_departure/task.rs | 45 + nodedb/src/control/crdt_post_image_policy.rs | 94 +- nodedb/src/control/crs_context.rs | 28 + .../distributed_applier/apply_loop/start.rs | 3 + .../apply_loop/write_dispatch.rs | 9 +- nodedb/src/control/exec_receiver/executor.rs | 25 +- nodedb/src/control/gateway/dispatcher.rs | 10 + .../error_map/class_parity/code_samples.rs | 4 +- .../error_map/class_parity/error_index.rs | 3 +- .../error_map/class_parity/error_samples.rs | 3 + nodedb/src/control/gateway/error_map/resp.rs | 1 + nodedb/src/control/gateway/invalidation.rs | 10 + nodedb/src/control/gateway/plan_cache.rs | 8 + .../control/gateway/version_set/plan_keys.rs | 1 - nodedb/src/control/lease/drain_propose.rs | 12 +- nodedb/src/control/lease/mod.rs | 2 - nodedb/src/control/lease/renewal.rs | 2 +- nodedb/src/control/lease/wall_time.rs | 15 - .../maintenance/clone_materializer/chained.rs | 2 +- .../clone_materializer/columnar.rs | 1 + .../clone_materializer/document.rs | 19 +- .../clone_materializer/document_copy.rs | 2 +- .../merge_orchestrator/expand_staged_merge.rs | 1 + .../merge_orchestrator/orchestrator.rs | 1 + .../control/metadata_proposer/ddl_prepare.rs | 14 +- nodedb/src/control/mod.rs | 2 + nodedb/src/control/orchestrated_write.rs | 2 + nodedb/src/control/otel/exporter.rs | 152 +- .../calvin/dependent_recon_node_edges.rs | 7 +- nodedb/src/control/planner/calvin/dispatch.rs | 2 + .../control/planner/calvin/dispatch_multi.rs | 2 + .../control/planner/calvin/edge_truncate.rs | 2 +- ...edge_sequencing.rs => graph_sequencing.rs} | 113 +- nodedb/src/control/planner/calvin/mod.rs | 9 +- .../planner/calvin/submit/edge_slices.rs | 2 + .../control/planner/calvin/tx_class/shared.rs | 2 + .../calvin/tx_class/write_keys/graph.rs | 1 - .../calvin/tx_class/write_keys/plan.rs | 5 + .../calvin/tx_class/write_keys/restore.rs | 15 +- .../src/control/planner/calvin/write_class.rs | 5 +- .../catalog_adapter/sql_catalog_impl.rs | 6 + .../control/planner/context/query/context.rs | 7 + .../src/control/planner/context/query/mod.rs | 1 + .../control/planner/context/query/planning.rs | 11 + .../planner/context/query/planning_built.rs | 7 + .../planner/context/query/statement_cut.rs | 40 + .../control/planner/implicit_edges/insert.rs | 3 + .../control/planner/implicit_edges/routed.rs | 3 + .../planner/materialized_sum/cross_shard.rs | 3 +- .../planner/materialized_sum/predicate.rs | 19 +- .../planner/materialized_sum/resolve.rs | 2 + .../planner/materialized_sum/settle.rs | 14 +- .../planner/materialized_sum/stored.rs | 49 +- nodedb/src/control/planner/mod.rs | 1 + .../planner/period_lock/point_update.rs | 6 +- .../control/planner/period_lock/predicate.rs | 6 +- .../control/planner/plan_children/document.rs | 32 + .../src/control/planner/plan_children/mod.rs | 2 + .../control/planner/plan_children/query.rs | 47 + .../control/planner/plan_children/vector.rs | 38 + .../control/planner/plan_children/walk_mut.rs | 29 + .../planner/redaction_refusal/graph.rs | 1 - .../control/planner/redaction_refusal/plan.rs | 4 + .../control/planner/rls_injection/columnar.rs | 7 + .../control/planner/rls_injection/context.rs | 10 + .../control/planner/rls_injection/document.rs | 4 +- .../control/planner/rls_injection/graph.rs | 13 +- .../src/control/planner/rls_injection/meta.rs | 2 + .../rls_injection/permission_tree/graph.rs | 11 +- .../rls_injection/permission_tree/meta.rs | 2 + .../src/control/planner/rls_injection/plan.rs | 1 + .../sql_plan_convert/array_alter_convert.rs | 23 +- .../array_convert/attr_type.rs | 3 + .../sql_plan_convert/array_convert/ddl.rs | 18 +- .../array_fn_convert/aggregate.rs | 16 +- .../array_fn_convert/elementwise.rs | 10 +- .../array_fn_convert/helpers.rs | 69 +- .../array_fn_convert/region.rs | 62 +- .../array_fn_convert/slice.rs | 16 +- .../planner/sql_plan_convert/convert.rs | 12 + .../sql_plan_convert/dml/insert/convert.rs | 1 + .../planner/sql_plan_convert/dml/kv_insert.rs | 2 +- .../planner/sql_plan_convert/dml/merge.rs | 10 +- .../dml/update_delete/delete.rs | 1 + .../dml/update_delete/update.rs | 11 +- .../planner/sql_plan_convert/dml/upsert.rs | 1 + .../sql_plan_convert/expr/bridge_expr.rs | 10 +- .../sql_plan_convert/kv_counter_shape.rs | 2 +- .../control/planner/sql_plan_convert/mod.rs | 2 + .../output_schema/build/constant.rs | 3 +- .../output_schema/cte_types.rs | 8 + .../planner/sql_plan_convert/row_source.rs | 13 +- .../planner/sql_plan_convert/scan/core.rs | 19 +- .../planner/sql_plan_convert/scan/helpers.rs | 16 - .../planner/sql_plan_convert/scan/join.rs | 2 +- .../planner/sql_plan_convert/scan/mod.rs | 1 - .../planner/sql_plan_convert/scan/spatial.rs | 1 + .../sql_plan_convert/scan/timeseries.rs | 4 +- .../planner/sql_plan_convert/set_ops.rs | 6 +- .../planner/sql_plan_convert/valid_time.rs | 43 + .../sql_plan_convert/value/assignments.rs | 4 +- .../planner/sql_plan_convert/value/convert.rs | 12 +- .../sql_plan_convert/value/msgpack_write.rs | 35 +- nodedb/src/control/planner/system_cut/mod.rs | 10 + nodedb/src/control/planner/system_cut/pin.rs | 182 + .../src/control/planner/system_cut/sites.rs | 186 + nodedb/src/control/read_cut/mod.rs | 13 + .../run_cut.rs => read_cut/run_marker.rs} | 8 +- nodedb/src/control/read_cut/statement_cap.rs | 353 + nodedb/src/control/read_cut/wait.rs | 184 + .../src/control/security/audit/export_sink.rs | 4 +- nodedb/src/control/security/blacklist/ip.rs | 121 +- .../src/control/security/blacklist/store.rs | 17 +- .../security/catalog/audit_dml_index.rs | 6 +- .../security/catalog/bootstrap_tables.rs | 3 + .../security/catalog/calvin_barrier_logs.rs | 2 + .../control/security/catalog/collection.rs | 2 +- .../control/security/catalog/collections.rs | 1 + .../control/security/catalog/credentials.rs | 156 + .../src/control/security/catalog/functions.rs | 1 + nodedb/src/control/security/catalog/mod.rs | 8 + .../src/control/security/catalog/read_only.rs | 7 + .../security/catalog/redo_snapshot_owed.rs | 1 + .../security/catalog/replicated_image.rs | 4 + .../security/catalog/spatial_ref_sys.rs | 191 + .../control/security/catalog/srid_refs/mod.rs | 8 + .../security/catalog/srid_refs/scan.rs | 287 + .../security/catalog/srid_refs/text.rs | 151 + .../control/security/catalog/surrogate_hwm.rs | 2 + .../security/catalog/system_catalog.rs | 188 +- nodedb/src/control/security/catalog/tables.rs | 5 + .../control/security/catalog/test_faults.rs | 77 + nodedb/src/control/security/catalog/users.rs | 1 + .../security/catalog/vshard_residency.rs | 165 + .../src/control/security/catalog/wasm_raw.rs | 70 + .../src/control/security/conditional/parse.rs | 4 +- nodedb/src/control/security/egress/client.rs | 195 + .../control/security/egress/global_unicast.rs | 169 + nodedb/src/control/security/egress/mod.rs | 16 + nodedb/src/control/security/egress/policy.rs | 211 + nodedb/src/control/security/egress/remote.rs | 46 + .../src/control/security/egress/resolver.rs | 112 + .../security/identity/plan_permission.rs | 5 +- nodedb/src/control/security/jwks/cache.rs | 11 + nodedb/src/control/security/jwks/fetch.rs | 264 +- nodedb/src/control/security/jwks/mod.rs | 1 - .../control/security/jwks/registry/catalog.rs | 98 + .../control/security/jwks/registry/init.rs | 57 +- .../src/control/security/jwks/registry/mod.rs | 4 + .../control/security/jwks/registry/state.rs | 8 +- .../control/security/jwks/registry/targets.rs | 93 + .../security/jwks/registry/validate.rs | 51 +- nodedb/src/control/security/jwks/url.rs | 376 - .../control/security/keystore/derivation.rs | 21 +- nodedb/src/control/security/keystore/mod.rs | 2 + nodedb/src/control/security/keystore/vault.rs | 94 +- .../src/control/security/keystore/wal_key.rs | 70 + nodedb/src/control/security/mod.rs | 3 + nodedb/src/control/security/net/cidr.rs | 144 + nodedb/src/control/security/net/mod.rs | 5 + .../src/control/security/predicate_typing.rs | 3 +- nodedb/src/control/security/rls/eval.rs | 120 +- nodedb/src/control/security/siem/delivery.rs | 22 +- nodedb/src/control/security/siem/exporter.rs | 53 +- nodedb/src/control/security/siem/metrics.rs | 7 +- .../control/security/source_credential/kek.rs | 60 + .../control/security/source_credential/mod.rs | 11 + .../security/source_credential/seal.rs | 162 + .../security/source_credential/secret.rs | 62 + .../security/source_credential/stored.rs | 58 + .../dispatch_utils/change_events/extract.rs | 8 +- .../control/server/dispatch_utils/dispatch.rs | 4 + .../server/dispatch_utils/durable_write.rs | 6 +- .../submit_write/funnel/dispatch.rs | 7 +- .../submit_write/funnel/driver.rs | 8 +- .../submit_write/funnel/pending.rs | 2 + .../submit_write/funnel/wal_append.rs | 21 +- .../dispatch_utils/submit_write/params.rs | 6 + .../server/dispatch_utils/write_abort.rs | 3 + .../server/exchange/all_cores/dispatch.rs | 15 +- .../server/exchange/all_cores/fanout.rs | 1 - .../control/server/exchange/all_cores/mod.rs | 2 +- .../server/exchange/all_cores/read_cut.rs | 60 +- .../server/exchange/all_cores/snapshot.rs | 13 + .../control/server/exchange/cluster_leaf.rs | 6 +- .../resolve/exchange/post_process_arm.rs | 7 +- .../src/control/server/graph_dispatch/bfs.rs | 8 + .../graph_dispatch/bsp_pagerank/coord.rs | 12 +- .../graph_dispatch/bsp_pagerank/scatter.rs | 4 + .../server/graph_dispatch/bsp_wcc/coord.rs | 10 +- .../server/graph_dispatch/bsp_wcc/scatter.rs | 10 +- .../server/graph_dispatch/cluster_resolve.rs | 9 + .../src/control/server/graph_dispatch/hop.rs | 17 +- .../server/graph_dispatch/keyed_read.rs | 6 + .../server/graph_dispatch/match_broadcast.rs | 10 + .../match_scatter/round_loop.rs | 2 + .../match_scatter/round_zero.rs | 3 + .../src/control/server/graph_dispatch/mod.rs | 3 +- .../control/server/graph_dispatch/presence.rs | 19 +- .../server/graph_dispatch/rag_fusion/coord.rs | 9 + .../server/graph_dispatch/read_groups.rs | 2 + .../server/graph_dispatch/read_scope.rs | 77 + .../server/graph_dispatch/shortest_path.rs | 7 + .../graph_dispatch/traverse_subgraph.rs | 13 +- .../server/graph_dispatch/walk_reads.rs | 152 +- .../server/graph_dispatch/whole_graph.rs | 44 +- nodedb/src/control/server/http/routes/crdt.rs | 10 +- .../server/http/routes/query_stream.rs | 1 + .../src/control/server/native/dispatch/ctx.rs | 1 - .../server/native/dispatch/graph_match.rs | 3 +- .../server/native/dispatch/graph_owner.rs | 78 +- .../native/dispatch/plan_builder/columnar.rs | 1 + .../native/dispatch/plan_builder/dispatch.rs | 2 +- .../dispatch/plan_builder/document_bulk.rs | 2 +- .../native/dispatch/plan_builder/graph.rs | 49 +- .../native/dispatch/plan_builder/spatial.rs | 6 +- .../server/native/dispatch/raw_dispatch.rs | 10 +- .../server/native/session/session_stream.rs | 4 +- .../src/control/server/pgwire/catalog/mod.rs | 1 + .../control/server/pgwire/catalog/provider.rs | 3 +- .../control/server/pgwire/catalog/schema.rs | 16 +- .../server/pgwire/catalog/spatial_ref_sys.rs | 73 + .../pgwire/handler/core/simple_query.rs | 15 +- .../server/pgwire/handler/dispatch/local.rs | 1 + .../server/pgwire/handler/prepared/execute.rs | 20 +- .../handler/routing/dispatch_loop/finish.rs | 1 + .../handler/routing/execute_dml_hooks.rs | 1 + .../server/pgwire/handler/routing/set_ops.rs | 2 + .../pgwire/handler/routing/streaming.rs | 1 + .../control/server/pgwire/handler/sql_exec.rs | 29 +- .../server/pgwire/handler/stream_response.rs | 2 + .../control/server/pgwire/types/error_map.rs | 1 + .../response_shape/compose/array_slice.rs | 3 +- .../server/response_shape/compose/kernel.rs | 92 +- .../response_shape/compose/materialized.rs | 43 +- .../server/response_shape/returning.rs | 129 +- .../server/response_shape/stamp/evaluate.rs | 34 +- .../server/response_shape/stamp/substitute.rs | 108 +- .../response_shape/types/plan_kind/graph.rs | 1 - .../src/control/server/response_shape/walk.rs | 12 +- .../server/response_translate/dispatch.rs | 1 - .../server/response_translate/vector.rs | 30 +- .../shared/authorization/requirements.rs | 2 +- .../server/shared/check_constraint/enforce.rs | 6 +- .../server/shared/check_constraint/simple.rs | 12 +- .../control/server/shared/clone_read/entry.rs | 15 +- .../server/shared/cluster_array_dispatch.rs | 1 + .../server/shared/ddl/neutral/alert/create.rs | 12 + .../collection/copy_from/array_load/decode.rs | 4 + .../copy_from/array_load/raster/adopt.rs | 64 + .../copy_from/array_load/raster/bands.rs | 280 - .../array_load/raster/driver/array_view.rs | 199 + .../array_load/raster/driver/blocks.rs | 236 + .../array_load/raster/driver/drive.rs | 180 + .../array_load/raster/driver/error.rs | 20 + .../array_load/raster/driver/load_options.rs | 71 + .../copy_from/array_load/raster/driver/mod.rs | 11 + .../array_load/raster/driver/open.rs | 81 + .../array_load/raster/fetch/error.rs | 96 + .../copy_from/array_load/raster/fetch/http.rs | 363 + .../array_load/raster/fetch/local.rs | 91 + .../array_load/raster/fetch/memory.rs | 57 + .../copy_from/array_load/raster/fetch/mod.rs | 21 + .../copy_from/array_load/raster/fetch/pin.rs | 94 + .../array_load/raster/fetch/sources.rs | 55 + .../array_load/raster/fetch/store.rs | 315 + .../array_load/raster/fetch/test_server.rs | 131 + .../array_load/raster/fetch/traits.rs | 41 + .../copy_from/array_load/raster/grid.rs | 375 - .../copy_from/array_load/raster/mod.rs | 7 +- .../copy_from/array_load/raster/open.rs | 429 - .../copy_from/array_load/raster/pixels.rs | 282 - .../copy_from/array_load/raster/refusal.rs | 92 +- .../copy_from/array_load/raster/task.rs | 315 +- .../ddl/neutral/collection/copy_from/entry.rs | 48 +- .../ddl/neutral/collection/describe_array.rs | 40 +- .../neutral/collection/dml/parse/statement.rs | 46 +- .../shared/ddl/neutral/collection/register.rs | 2 + .../server/shared/ddl/neutral/crdt_ops.rs | 10 +- .../server/shared/ddl/neutral/credential.rs | 267 + .../shared/ddl/neutral/dsl/crdt_merge.rs | 10 +- .../shared/ddl/neutral/dsl/search_fusion.rs | 12 +- .../shared/ddl/neutral/graph_ops/algo.rs | 9 +- .../shared/ddl/neutral/graph_ops/dispatch.rs | 31 +- .../shared/ddl/neutral/graph_ops/edge.rs | 24 +- .../ddl/neutral/graph_ops/rag_fusion.rs | 26 +- .../shared/ddl/neutral/graph_ops/traverse.rs | 36 +- .../server/shared/ddl/neutral/match_ops.rs | 17 +- .../control/server/shared/ddl/neutral/mod.rs | 2 + .../control/server/shared/ddl/neutral/oidc.rs | 39 + .../neutral/query_functions/balance_as_of.rs | 19 +- .../neutral/query_functions/verify_balance.rs | 19 +- .../shared/ddl/neutral/router/dispatch.rs | 18 +- .../shared/ddl/neutral/router/typed_misc.rs | 39 +- .../shared/ddl/neutral/spatial_ref_sys.rs | 284 + .../shared/ddl/neutral/tree_ops/children.rs | 2 + .../ddl/neutral/tree_ops/create_index.rs | 2 + .../server/shared/ddl/neutral/tree_ops/sum.rs | 2 + .../shared/ddl/neutral/typeguard/validate.rs | 2 + .../ddl/neutral/version_history/restore.rs | 18 +- .../control/server/shared/ddl/sql_parse.rs | 26 +- .../src/control/server/shared/ddl/sqlstate.rs | 8 + .../server/shared/plan_util/collection.rs | 2 + .../shared/plan_util/collection_meta.rs | 4 + .../shared/plan_util/collection_rows.rs | 21 +- .../server/shared/plan_util/read_key.rs | 3 +- nodedb/src/control/server/shared/retry.rs | 1 + .../control/server/shared/returning/inject.rs | 4 +- .../server/shared/session/audit_context.rs | 95 +- .../session/commit/buffered_dispatch.rs | 172 + .../server/shared/session/commit/mod.rs | 1 + .../server/shared/session/commit/run.rs | 139 +- .../control/server/shared/session/read_set.rs | 2 +- .../server/shared/sql/staging_predicates.rs | 3 + .../server/shared/txn_route/row_triggers.rs | 16 +- .../shared/write_admission/admission_keys.rs | 2 + .../shared/write_admission/leader_gate.rs | 87 +- .../predicate/txn_buffering/classify.rs | 53 +- .../write_admission/write_order_fence.rs | 2 + .../server/sync/async_dispatch/delta/apply.rs | 72 +- .../sync/async_dispatch/delta/authorize.rs | 1 + .../sync/async_dispatch/delta/compensation.rs | 4 +- .../sync/async_dispatch/delta/outcome.rs | 1 + .../sync/async_dispatch/delta/signature.rs | 1 + .../control/server/sync/columnar_handler.rs | 1 + .../server/sync/raft_dispatch/propose.rs | 6 +- nodedb/src/control/server/sync/refusal.rs | 8 +- nodedb/src/control/server/sync/security.rs | 1 + .../src/control/server/sync/session/delta.rs | 10 + .../control/server/sync/session/dispatch.rs | 1 + .../control/server/sync/session/handshake.rs | 75 +- .../server/sync/session_handler/announce.rs | 7 +- nodedb/src/control/server/sync/wire.rs | 10 +- .../control/server/wal_dispatch/columnar.rs | 35 +- .../src/control/server/wal_dispatch/crdt.rs | 2 +- .../control/server/wal_dispatch/document.rs | 14 +- .../src/control/server/wal_dispatch/graph.rs | 20 +- .../server/wal_dispatch/graph_labels.rs | 23 - nodedb/src/control/server/wal_dispatch/mod.rs | 4 +- .../server/wal_dispatch/system_stamp.rs | 269 + .../control/server/wal_dispatch/timeseries.rs | 33 +- .../server/wal_dispatch/write_set_redo.rs | 4 +- nodedb/src/control/source_read/pages.rs | 15 +- nodedb/src/control/source_read/query.rs | 8 +- nodedb/src/control/source_read/shape.rs | 35 +- nodedb/src/control/state/fields.rs | 8 +- nodedb/src/control/state/init.rs | 16 +- .../src/control/state/init_prod/auth_parts.rs | 22 +- nodedb/src/control/state/init_prod/mod.rs | 2 + nodedb/src/control/state/init_prod/open.rs | 20 +- .../control/state/init_prod/open_config.rs | 28 + nodedb/src/control/state/init_variants.rs | 38 + nodedb/src/control/state/methods.rs | 10 +- nodedb/src/control/state/mod.rs | 2 +- .../surrogate/assign/bind_plan/binder.rs | 2 + .../surrogate/assign/bind_plan/graph.rs | 1 - nodedb/src/control/trace_export.rs | 64 +- .../src/control/trigger/batch/when_filter.rs | 34 +- nodedb/src/control/trigger/dml_hook.rs | 3 +- nodedb/src/control/trigger/when_parse.rs | 2 +- .../expand_staged_update_from_join.rs | 1 + .../orchestrator.rs | 2 + .../wal_replication/decode/columnar.rs | 18 +- .../control/wal_replication/decode/entry.rs | 18 +- .../decode/entry_columnar_family.rs | 16 +- .../wal_replication/decode/entry_graph.rs | 15 +- .../control/wal_replication/decode/graph.rs | 43 +- .../src/control/wal_replication/decode/mod.rs | 2 +- .../wal_replication/decode_sync_engines.rs | 9 +- .../wal_replication/encode/columnar.rs | 7 +- .../encode/entry_columnar_family.rs | 25 +- .../wal_replication/encode/entry_graph.rs | 15 +- .../control/wal_replication/encode/graph.rs | 41 +- nodedb/src/control/wal_replication/mod.rs | 1 + nodedb/src/control/wal_replication/propose.rs | 11 +- .../wal_replication/replicable_write.rs | 1 + .../wal_replication/transaction_redo/apply.rs | 3 + .../src/control/wal_replication/types/mod.rs | 3 + .../wal_replication/types/replicated_entry.rs | 14 + .../wal_replication/types/replicated_write.rs | 20 +- .../wal_replication/types/system_span.rs | 193 + .../wal_replication/types/wire_shapes.rs | 7 + nodedb/src/control/write_resolve/columnar.rs | 4 + nodedb/src/control/write_resolve/graph.rs | 1 - nodedb/src/ctl/restore/env.rs | 25 +- nodedb/src/ctl/restore/run.rs | 4 +- nodedb/src/data/executor/array_checkpoint.rs | 1 + .../data/executor/cascade_journal_tests.rs | 2 + .../columnar_checkpoint/geometry_restore.rs | 39 +- .../data/executor/core_loop/apply_scope.rs | 15 +- .../executor/core_loop/bitemporal_time.rs | 517 +- .../data/executor/core_loop/crs_context.rs | 31 + .../data/executor/core_loop/event_image.rs | 4 +- .../data/executor/core_loop/filter_match.rs | 12 +- .../src/data/executor/core_loop/graph_view.rs | 102 + .../executor/core_loop/hash_chain_state.rs | 36 + .../data/executor/core_loop/maintenance.rs | 11 +- nodedb/src/data/executor/core_loop/mod.rs | 6 +- nodedb/src/data/executor/core_loop/open.rs | 9 +- .../src/data/executor/core_loop/redo_image.rs | 26 +- .../src/data/executor/core_loop/response.rs | 7 + nodedb/src/data/executor/core_loop/state.rs | 57 +- .../executor/core_loop/ts_replay_state.rs | 24 + .../executor/core_loop/write_index/record.rs | 4 +- .../executor/core_loop/write_index/tests.rs | 2 +- .../core_loop/write_index/validate.rs | 3 +- .../src/data/executor/crdt_checkpoint/load.rs | 3 +- .../data/executor/dispatch/array/aggregate.rs | 112 +- .../dispatch/array/aggregate_helpers.rs | 115 +- .../data/executor/dispatch/array/convert.rs | 1 + .../executor/dispatch/array/elementwise.rs | 13 +- .../src/data/executor/dispatch/array/entry.rs | 93 +- .../src/data/executor/dispatch/array/read.rs | 48 +- nodedb/src/data/executor/dispatch/columnar.rs | 37 +- nodedb/src/data/executor/dispatch/document.rs | 86 +- .../data/executor/dispatch/document_admit.rs | 70 +- .../executor/dispatch/document_register.rs | 2 + nodedb/src/data/executor/dispatch/execute.rs | 18 +- .../src/data/executor/dispatch/graph/mod.rs | 2 + .../executor/dispatch/graph/node_labels.rs | 54 +- .../src/data/executor/dispatch/graph/rag.rs | 131 + .../src/data/executor/dispatch/graph/route.rs | 455 +- nodedb/src/data/executor/dispatch/meta.rs | 12 + .../dispatch/meta_retention/columnar_plain.rs | 304 +- .../dispatch/meta_retention/handlers.rs | 7 + .../src/data/executor/dispatch/timeseries.rs | 4 +- .../data/executor/enforcement/chain_guard.rs | 42 +- .../data/executor/enforcement/chain_link.rs | 2 +- .../enforcement/materialized_sum/apply.rs | 3 +- .../enforcement/materialized_sum/delta.rs | 30 +- .../materialized_sum/divergence.rs | 4 +- .../executor/enforcement/transition_check.rs | 105 +- .../executor/enforcement/typeguard/check.rs | 160 +- .../executor/enforcement/typeguard/hook.rs | 70 +- .../executor/enforcement/typeguard/inject.rs | 3 +- .../src/data/executor/enforcement/unique.rs | 19 +- .../enforcement/update_transitions.rs | 11 +- .../data/executor/enforcement/write_hook.rs | 2 +- nodedb/src/data/executor/graph_label_apply.rs | 190 + .../executor/graph_label_checkpoint/load.rs | 7 +- .../executor/graph_label_checkpoint/mod.rs | 13 +- .../src/data/executor/handlers/accum/feed.rs | 44 +- .../data/executor/handlers/accum/finalize.rs | 62 +- .../src/data/executor/handlers/accum/merge.rs | 211 +- .../src/data/executor/handlers/accum/new.rs | 4 +- .../src/data/executor/handlers/accum/state.rs | 9 +- .../data/executor/handlers/aggregate/rows.rs | 9 +- .../handlers/aggregate/shuffle_merge.rs | 34 +- .../aggregate/streaming/accumulate.rs | 22 +- .../handlers/aggregate/streaming/finalize.rs | 5 +- .../data/executor/handlers/bulk_dml/delete.rs | 33 +- .../handlers/bulk_dml/delete_cascade.rs | 6 +- .../data/executor/handlers/bulk_dml/scan.rs | 32 +- .../executor/handlers/bulk_dml/txn_match.rs | 3 +- .../data/executor/handlers/bulk_dml/update.rs | 28 +- .../handlers/bulk_dml/update_persist.rs | 10 +- .../handlers/bulk_dml/update_project.rs | 27 +- .../executor/handlers/columnar_assignments.rs | 12 +- .../executor/handlers/columnar_close_apply.rs | 362 + .../executor/handlers/columnar_mutation.rs | 51 +- .../handlers/columnar_mutation_apply.rs | 110 +- .../handlers/columnar_read/bitemporal.rs | 52 +- .../handlers/columnar_read/convert.rs | 76 +- .../executor/handlers/columnar_read/filter.rs | 27 +- .../columnar_read/materialize_scan.rs | 24 +- .../executor/handlers/columnar_read/mod.rs | 2 + .../handlers/columnar_read/scan/execute.rs | 161 +- .../handlers/columnar_read/scan/order.rs | 3 +- .../handlers/columnar_read/scan_flushed.rs | 20 +- .../executor/handlers/columnar_read/sort.rs | 3 +- .../handlers/columnar_read/system_fold.rs | 64 + .../handlers/columnar_read/versions.rs | 396 + .../executor/handlers/columnar_resolve.rs | 15 +- .../handlers/columnar_resolved_mutation.rs | 47 +- .../executor/handlers/columnar_truncate.rs | 229 +- .../handlers/columnar_write/geometry_index.rs | 41 +- .../columnar_write/geometry_remove.rs | 2 +- .../handlers/columnar_write/insert.rs | 5 + .../executor/handlers/columnar_write/mod.rs | 5 +- .../handlers/columnar_write/read_prior.rs | 44 +- .../handlers/columnar_write/row_ingest.rs | 104 +- .../handlers/columnar_write/schema.rs | 94 +- .../handlers/columnar_write/system_stamp.rs | 529 + .../handlers/control/calvin/active_passive.rs | 7 +- .../handlers/control/calvin/static_stage.rs | 6 +- .../handlers/control/calvin_reply/images.rs | 10 +- .../handlers/control/calvin_reply/payload.rs | 17 +- .../handlers/control/calvin_reply/stage.rs | 22 +- .../handlers/control/calvin_resolve.rs | 17 +- .../control/checkpoint_durable_lsn.rs | 2 +- .../handlers/control/checkpoint_install.rs | 8 +- .../data/executor/handlers/control/crdt.rs | 2 + .../handlers/control/crdt_apply/gated.rs | 2 + .../handlers/control/crdt_apply/local.rs | 2 + .../executor/handlers/control/crdt_doc.rs | 18 +- .../executor/handlers/control/crdt_preview.rs | 8 +- .../handlers/control/range_scan_versioned.rs | 7 +- .../executor/handlers/control/snapshot.rs | 2 + .../executor/handlers/convert_bitemporal.rs | 14 +- .../document/index_backfill_versions.rs | 27 +- .../executor/handlers/document/index_fetch.rs | 27 +- .../handlers/document/index_maintenance.rs | 3 +- .../handlers/document/read/audit_body.rs | 29 +- .../executor/handlers/document/read/fetch.rs | 28 +- .../handlers/document/read/fetch_types.rs | 5 +- .../document/read/materialize_scan.rs | 44 +- .../handlers/document/read/projection.rs | 10 +- .../executor/handlers/document/read/scan.rs | 13 +- .../handlers/document/resolve/apply.rs | 23 +- .../handlers/document/resolve/apply_row.rs | 4 +- .../handlers/document/resolve/bulk.rs | 36 +- .../handlers/document/resolve/context.rs | 2 + .../handlers/document/resolve/point.rs | 38 +- .../handlers/document/resolve/upsert.rs | 16 +- .../handlers/document/resolve/upsert_batch.rs | 28 +- .../handlers/document/sort/compare.rs | 3 +- .../handlers/document/sort/external.rs | 5 +- .../handlers/document/sort/in_memory.rs | 63 +- .../handlers/document/write/batch_insert.rs | 2 +- .../handlers/document/write/register.rs | 20 + .../src/data/executor/handlers/generated.rs | 3 +- nodedb/src/data/executor/handlers/graph.rs | 117 +- .../src/data/executor/handlers/graph_algo.rs | 86 +- .../data/executor/handlers/graph_algo_csr.rs | 81 + .../executor/handlers/graph_algo_edges.rs | 61 +- .../src/data/executor/handlers/graph_bsp.rs | 43 +- .../executor/handlers/graph_edge_predicate.rs | 42 +- .../executor/handlers/graph_edge_resolve.rs | 1 + .../handlers/graph_edge_write/delete.rs | 13 + .../handlers/graph_edge_write/delete_batch.rs | 6 + .../executor/handlers/graph_edge_write/put.rs | 41 +- .../handlers/graph_edge_write/put_batch.rs | 33 +- .../handlers/graph_edge_write/shared.rs | 6 + .../data/executor/handlers/graph_expansion.rs | 15 +- .../src/data/executor/handlers/graph_match.rs | 219 +- .../executor/handlers/graph_match_envelope.rs | 100 + .../executor/handlers/graph_match_view.rs | 87 + .../executor/handlers/graph_node_guard.rs | 2 + .../src/data/executor/handlers/graph_rag.rs | 55 +- .../executor/handlers/graph_rag_stages.rs | 13 +- .../executor/handlers/graph_rag_triple.rs | 37 +- .../data/executor/handlers/graph_temporal.rs | 96 +- .../data/executor/handlers/graph_traversal.rs | 83 +- .../src/data/executor/handlers/graph_wcc.rs | 35 +- .../executor/handlers/grouping_sets_exec.rs | 21 +- .../executor/handlers/join/grace_drive.rs | 2 +- .../handlers/join/grace_partitioner.rs | 6 +- .../executor/handlers/join/grace_probe.rs | 4 +- .../executor/handlers/join/grace_spill.rs | 7 +- .../src/data/executor/handlers/join/hash.rs | 36 +- .../executor/handlers/join/hash_handlers.rs | 10 +- .../executor/handlers/join/nested_loop.rs | 2 + .../src/data/executor/handlers/join/params.rs | 23 +- .../data/executor/handlers/join/row_source.rs | 5 +- .../data/executor/handlers/join/support.rs | 8 +- .../src/data/executor/handlers/kv/atomic.rs | 44 +- .../data/executor/handlers/kv/batch_plan.rs | 2 + .../data/executor/handlers/kv/batch_put.rs | 11 +- .../executor/handlers/kv/conflict_merge.rs | 48 +- .../data/executor/handlers/kv/crud/delete.rs | 10 +- .../src/data/executor/handlers/kv/crud/get.rs | 2 + .../executor/handlers/kv/crud/write_upsert.rs | 12 +- .../executor/handlers/kv/declared_body.rs | 2 + nodedb/src/data/executor/handlers/kv/field.rs | 11 +- .../executor/handlers/kv/predicate/matches.rs | 3 +- .../executor/handlers/kv/resolve/apply.rs | 10 +- .../handlers/kv/resolve/atomic_ops.rs | 36 +- .../executor/handlers/kv/resolve/batch_ops.rs | 14 +- .../handlers/kv/resolve/predicate_ops.rs | 22 +- .../handlers/kv/resolve/transfer_ops.rs | 4 + .../executor/handlers/kv/resolve/write_ops.rs | 55 +- nodedb/src/data/executor/handlers/kv/rls.rs | 12 +- nodedb/src/data/executor/handlers/kv/scan.rs | 7 +- .../src/data/executor/handlers/kv/transfer.rs | 24 +- nodedb/src/data/executor/handlers/kv/ttl.rs | 9 +- .../data/executor/handlers/merge_helpers.rs | 12 +- .../merge_orchestrated/apply/insert_rows.rs | 2 +- .../merge_orchestrated/apply/orchestrate.rs | 8 +- .../merge_orchestrated/apply/update_rows.rs | 2 +- .../merge_orchestrated/apply_support.rs | 23 +- .../merge_orchestrated/delete_arms.rs | 2 +- .../handlers/merge_orchestrated/plan.rs | 6 +- nodedb/src/data/executor/handlers/mod.rs | 4 + .../executor/handlers/point/apply_delete.rs | 22 +- .../executor/handlers/point/apply_put/core.rs | 31 +- .../handlers/point/apply_put/enforce.rs | 11 +- .../handlers/point/apply_put/index.rs | 51 +- .../handlers/point/apply_put/stored_body.rs | 13 +- .../handlers/point/apply_put/types.rs | 7 +- .../data/executor/handlers/point/delete.rs | 22 +- .../executor/handlers/point/delete_version.rs | 22 +- .../src/data/executor/handlers/point/get.rs | 17 +- .../data/executor/handlers/point/insert.rs | 4 +- .../src/data/executor/handlers/point/put.rs | 4 +- .../executor/handlers/point/update/exec.rs | 34 +- .../executor/handlers/point/update/persist.rs | 6 +- .../handlers/point/update/post_image.rs | 13 +- .../executor/handlers/point/update_reindex.rs | 37 +- .../data/executor/handlers/provider_scan.rs | 8 +- .../handlers/provider_scan_compute.rs | 6 +- nodedb/src/data/executor/handlers/purge.rs | 15 +- .../src/data/executor/handlers/recursive.rs | 2 +- .../executor/handlers/recursive_iterate.rs | 49 +- .../data/executor/handlers/returning_rows.rs | 116 +- nodedb/src/data/executor/handlers/rls_eval.rs | 88 +- .../data/executor/handlers/rls_write_gate.rs | 129 +- .../handlers/snapshot/capture_labels.rs | 42 + .../data/executor/handlers/snapshot/create.rs | 20 + .../executor/handlers/snapshot/departure.rs | 228 + .../data/executor/handlers/snapshot/mod.rs | 2 + .../handlers/snapshot/restore/engines.rs | 19 +- .../executor/handlers/snapshot/restore/mod.rs | 1 + .../handlers/snapshot/restore/node_labels.rs | 323 + .../snapshot/restore/tenant_snapshot.rs | 42 +- .../handlers/snapshot/restore/text.rs | 2 +- .../handlers/snapshot/restore_segments.rs | 5 +- .../src/data/executor/handlers/sort_utils.rs | 6 +- .../executor/handlers/spatial/full_scan.rs | 19 +- .../executor/handlers/spatial/rtree_scan.rs | 41 +- .../data/executor/handlers/spatial_refine.rs | 164 +- .../data/executor/handlers/spatial_sync.rs | 119 +- .../data/executor/handlers/spill/groupby.rs | 106 +- .../src/data/executor/handlers/text_rows.rs | 50 +- .../src/data/executor/handlers/text_search.rs | 2 +- .../executor/handlers/text_search_hybrid.rs | 2 +- .../executor/handlers/text_search_scan.rs | 4 +- .../executor/handlers/text_search_triple.rs | 11 +- .../executor/handlers/timeseries/aggregate.rs | 2 + .../executor/handlers/timeseries/encode.rs | 8 +- .../executor/handlers/timeseries/flush.rs | 2 +- .../handlers/timeseries/flush_rollforward.rs | 4 +- .../handlers/timeseries/group_flush.rs | 2 +- .../executor/handlers/timeseries/ingest.rs | 1 + .../handlers/timeseries/ingest_dispatch.rs | 90 +- .../handlers/timeseries/ingest_resolved.rs | 3 + .../data/executor/handlers/timeseries/mod.rs | 2 +- .../handlers/timeseries/raw_scan/gather.rs | 22 +- .../timeseries/raw_scan/partition_scan.rs | 123 +- .../handlers/timeseries/raw_scan/row_emit.rs | 11 +- .../handlers/timeseries/raw_scan/scan.rs | 7 +- .../handlers/timeseries/resolve_ingest.rs | 3 +- .../executor/handlers/timeseries/rls_gate.rs | 87 +- .../data/executor/handlers/timeseries/sort.rs | 56 +- .../executor/handlers/timeseries/truncate.rs | 8 +- .../data/executor/handlers/timeseries_wal.rs | 95 +- .../handlers/timeseries_wal_decode.rs | 2 + .../handlers/timeseries_wal_payload.rs | 3 + .../transaction/overlay/array_staged/types.rs | 2 +- .../transaction/overlay/columnar_merge.rs | 14 +- .../transaction/overlay/graph_staged/edges.rs | 41 +- .../transaction/overlay/graph_staged/mod.rs | 2 +- .../overlay/graph_staged/txn_overlay.rs | 19 +- .../transaction/overlay/graph_staged/types.rs | 13 +- .../handlers/transaction/overlay/merge.rs | 2 + .../handlers/transaction/overlay/mod.rs | 2 +- .../transaction/overlay/spatial_merge.rs | 19 +- .../transaction/overlay/staged_sidecar.rs | 5 +- .../transaction/overlay/timeseries_merge.rs | 5 +- .../handlers/transaction/redo_apply/entry.rs | 7 +- .../transaction/redo_apply/sub_ops.rs | 7 +- .../transaction/redo_apply/validate.rs | 1 + .../transaction/resolve/columnar_image.rs | 35 +- .../transaction/resolve/columnar_ordinal.rs | 211 + .../transaction/resolve/doc_ordinal.rs | 232 + .../handlers/transaction/resolve/document.rs | 40 +- .../handlers/transaction/resolve/entry.rs | 118 +- .../handlers/transaction/resolve/graph.rs | 99 +- .../handlers/transaction/resolve/mod.rs | 2 + .../handlers/transaction/resolve/restore.rs | 127 +- .../transaction/resolve/timeseries.rs | 5 +- .../handlers/transaction/stage_write/body.rs | 31 +- .../transaction/stage_write/constraint.rs | 1 + .../transaction/stage_write/dispatch.rs | 12 +- .../transaction/stage_write/stage_columnar.rs | 10 +- .../stage_write/stage_columnar_dml.rs | 28 +- .../stage_write/stage_columnar_family.rs | 8 + .../stage_columnar_resolved_dml.rs | 22 + .../transaction/stage_write/stage_graph.rs | 118 +- .../stage_write/stage_kv_atomic.rs | 1 + .../stage_write/stage_kv_conflict.rs | 1 + .../stage_write/stage_kv_predicate.rs | 5 +- .../stage_write/stage_point_document.rs | 2 +- .../stage_write/stage_returning.rs | 1 + .../transaction/stage_write/stage_rls.rs | 30 +- .../stage_write/stage_timeseries.rs | 30 +- .../stage_write/stage_timeseries_ilp.rs | 3 +- .../transaction/stage_write/stage_upsert.rs | 14 +- .../transaction/stage_write/stage_vector.rs | 9 +- .../stage_write/stage_vector_targets.rs | 19 +- .../handlers/transaction/undo/apply.rs | 4 +- .../transaction/undo/columnar_insert.rs | 4 +- .../handlers/transaction/undo/document.rs | 50 +- .../transaction/undo/document_outcome.rs | 6 +- .../handlers/transaction/undo/edge_cut.rs | 6 + .../handlers/transaction/undo/edge_write.rs | 3 + .../handlers/transaction/undo/entry.rs | 20 +- .../handlers/transaction/undo/graph_node.rs | 38 +- .../handlers/transaction/undo/rollback.rs | 14 +- .../handlers/transaction/undo/spatial.rs | 9 +- .../handlers/transaction/undo/spatial_row.rs | 4 +- .../handlers/transaction/undo/timeseries.rs | 2 +- .../handlers/transaction/write_version.rs | 1 - .../transaction/write_version_parity_tests.rs | 3 + nodedb/src/data/executor/handlers/truncate.rs | 30 +- .../data/executor/handlers/truncate_row.rs | 6 +- .../handlers/unregister_collection.rs | 7 +- .../handlers/unregister_materialized_view.rs | 3 +- .../executor/handlers/update_from_join.rs | 53 +- .../handlers/update_from_join_collect.rs | 20 +- .../handlers/update_from_join_write.rs | 26 +- .../executor/handlers/upsert/exec/batch.rs | 2 +- .../handlers/upsert/exec/batch_plan.rs | 19 +- .../executor/handlers/upsert/exec/insert.rs | 13 +- .../handlers/upsert/exec/merge_body.rs | 3 +- .../handlers/upsert/exec/overwrite.rs | 19 +- .../data/executor/handlers/upsert/merge.rs | 48 +- .../executor/handlers/vector_direct_delete.rs | 10 +- .../handlers/vector_direct_resolve/apply.rs | 29 +- .../vector_direct_resolve/resolve_batch.rs | 4 +- .../vector_direct_resolve/resolve_delete.rs | 3 +- .../vector_direct_resolve/resolve_update.rs | 2 +- .../vector_direct_resolve/resolve_upsert.rs | 1 + .../handlers/vector_direct_targets.rs | 7 +- .../executor/handlers/vector_direct_update.rs | 24 +- .../executor/handlers/vector_search_window.rs | 15 +- .../data/executor/handlers/vector_sparse.rs | 1 + .../executor/handlers/vector_sparse_window.rs | 22 +- .../data/executor/handlers/vector_upsert.rs | 6 +- .../executor/handlers/verify_hash_chain.rs | 3 +- .../src/data/executor/handlers/write_body.rs | 5 +- nodedb/src/data/executor/mod.rs | 1 + nodedb/src/data/executor/replay_floors.rs | 12 +- nodedb/src/data/executor/row_shape.rs | 6 +- nodedb/src/data/executor/scan_normalize.rs | 30 +- nodedb/src/data/executor/scan_versioned.rs | 2 +- nodedb/src/data/executor/snapshot/capture.rs | 17 +- .../data/executor/spatial_checkpoint/load.rs | 2 +- .../data/executor/spatial_checkpoint/write.rs | 4 +- .../src/data/executor/strict_format/coerce.rs | 15 +- .../src/data/executor/strict_format/encode.rs | 11 +- .../src/data/executor/strict_format/render.rs | 48 +- .../executor/timeseries_checkpoint/flush.rs | 4 +- .../executor/timeseries_checkpoint/load.rs | 4 +- .../executor/timeseries_checkpoint/stamp.rs | 15 +- nodedb/src/data/executor/wal_replay/crdt.rs | 3 + nodedb/src/data/executor/wal_replay_all.rs | 1 + .../data/executor/wal_replay_columnar_dml.rs | 34 +- .../executor/wal_replay_columnar_image.rs | 263 +- .../executor/wal_replay_columnar_truncate.rs | 75 +- .../data/executor/wal_replay_graph_labels.rs | 427 +- .../executor/wal_replay_kv_insert_conflict.rs | 3 + .../data/executor/wal_replay_redo_document.rs | 52 +- .../wal_replay_redo_document_apply.rs | 3 +- .../data/executor/wal_replay_redo_graph.rs | 65 +- .../executor/wal_replay_redo_graph_cut.rs | 10 + .../executor/wal_replay_row_image_tests.rs | 2 + nodedb/src/data/runtime/boot_seed.rs | 10 +- nodedb/src/data/runtime/params.rs | 3 + nodedb/src/data/runtime/spawn.rs | 3 + nodedb/src/diag/context/crs.rs | 68 + nodedb/src/diag/context/mod.rs | 4 + nodedb/src/diag/context/vshard_departure.rs | 41 + nodedb/src/diag/mod.rs | 13 +- nodedb/src/diag/recording/crs.rs | 28 + nodedb/src/diag/recording/mod.rs | 4 + nodedb/src/diag/recording/vshard_departure.rs | 26 + nodedb/src/engine/array/engine.rs | 7 +- nodedb/src/engine/crdt/tenant_state/apply.rs | 20 +- .../crdt/tenant_state/apply_validated.rs | 63 +- .../engine/crdt/tenant_state/constraints.rs | 6 +- nodedb/src/engine/crdt/tenant_state/core.rs | 72 +- nodedb/src/engine/crdt/tenant_state/rows.rs | 129 +- .../engine/crdt/tenant_state/snapshot_io.rs | 19 +- .../crdt/tenant_state/snapshot_restore.rs | 7 +- .../src/engine/crdt/tenant_state/validate.rs | 3 +- nodedb/src/engine/document/crdt_store.rs | 3 + nodedb/src/engine/document/predicate.rs | 8 +- nodedb/src/engine/document/store/config.rs | 6 +- .../src/engine/document/store/engine/batch.rs | 22 +- .../engine/document/store/engine/delete.rs | 6 +- .../src/engine/document/store/engine/put.rs | 14 +- .../src/engine/graph/csr/historical_cache.rs | 450 + nodedb/src/engine/graph/csr/mod.rs | 3 + nodedb/src/engine/graph/csr/rebuild.rs | 15 +- nodedb/src/engine/graph/edge_store/cascade.rs | 2 +- nodedb/src/engine/graph/edge_store/mod.rs | 12 +- .../graph/edge_store/node_label_snapshot.rs | 49 + .../engine/graph/edge_store/node_labels.rs | 481 + nodedb/src/engine/graph/edge_store/purge.rs | 168 +- nodedb/src/engine/graph/edge_store/scan.rs | 4 +- nodedb/src/engine/graph/edge_store/store.rs | 14 +- .../edge_store/temporal/collection_scan.rs | 304 + .../engine/graph/edge_store/temporal/mod.rs | 6 +- .../graph/edge_store/temporal/payload.rs | 47 +- .../engine/graph/edge_store/temporal/query.rs | 27 +- .../graph/edge_store/temporal/visibility.rs | 16 + .../engine/graph/edge_store/temporal/write.rs | 33 +- nodedb/src/engine/graph/olap/parallel.rs | 2 +- nodedb/src/engine/graph/olap/snapshot.rs | 26 +- .../graph/pattern/executor/core/triple.rs | 75 + .../src/engine/graph/pattern/executor/mod.rs | 2 +- .../graph/pattern/executor/predicates.rs | 45 +- .../src/engine/sparse/btree_versioned/doc.rs | 152 +- .../engine/sparse/btree_versioned/index.rs | 10 +- .../src/engine/sparse/btree_versioned/key.rs | 14 +- .../src/engine/sparse/btree_versioned/mod.rs | 5 +- .../engine/sparse/btree_versioned/purge.rs | 69 +- .../src/engine/sparse/btree_versioned/scan.rs | 74 +- .../engine/sparse/btree_versioned/snapshot.rs | 4 +- .../engine/sparse/btree_versioned/value.rs | 29 +- nodedb/src/engine/spatial/mod.rs | 13 +- nodedb/src/error/types.rs | 8 + nodedb/src/error_classify/public.rs | 4 +- nodedb/src/error_classify/unclassified.rs | 1 + nodedb/src/error_from.rs | 18 +- nodedb/src/error_from_columnar.rs | 5 + nodedb/src/error_from_data_plane.rs | 1 + nodedb/src/error_from_eval.rs | 19 +- nodedb/src/error_from_spatial.rs | 43 + nodedb/src/event/alert/notify.rs | 101 +- nodedb/src/event/test_utils.rs | 2 +- nodedb/src/event/trigger/dispatcher/batch.rs | 1 + nodedb/src/event/wal_replay.rs | 12 +- nodedb/src/event/wal_replay_doc_shapes.rs | 33 +- nodedb/src/event/wal_replay_parse.rs | 6 +- nodedb/src/event/webhook/delivery.rs | 93 +- nodedb/src/lib.rs | 1 + nodedb/src/main_boot/data_plane.rs | 23 +- nodedb/src/main_boot/shared_state.rs | 5 +- nodedb/src/query/materialized_sum_delta.rs | 28 +- nodedb/src/query/materialized_sum_images.rs | 150 +- nodedb/src/query/materialized_sum_keys.rs | 45 +- nodedb/src/storage/cold_filter.rs | 47 +- nodedb/src/types/mod.rs | 4 +- nodedb/src/types/record_home.rs | 25 + nodedb/src/types/snapshot.rs | 19 + nodedb/src/util/rmpv_value.rs | 39 +- nodedb/src/wal/manager/append_index.rs | 8 +- nodedb/src/wal/manager/appender.rs | 10 + nodedb/src/wal/manager/core.rs | 10 + nodedb/src/wal/mod.rs | 2 +- nodedb/src/wal/redo/capture.rs | 18 +- nodedb/src/wal/redo/mod.rs | 2 + nodedb/src/wal/redo/node_label.rs | 89 + nodedb/src/wal/redo/record.rs | 9 + nodedb/src/wal/timeseries_batch_payload.rs | 5 + nodedb/tests/inproc/cases/auth_escalation.rs | 2 +- .../inproc/cases/auth_jwks_fetch_policy.rs | 41 +- .../tests/inproc/cases/auth_jwks_security.rs | 40 +- .../inproc/cases/auth_jwt_external_roles.rs | 27 +- .../inproc/cases/bitemporal_crdt_sync.rs | 9 + .../tests/inproc/cases/bitemporal_recovery.rs | 6 +- .../inproc/cases/bitemporal_temporal_purge.rs | 8 +- .../cases/calvin_determinism_contract.rs | 27 +- .../inproc/cases/catalog_integrity_verify.rs | 4 + .../inproc/cases/catalog_recovery_check.rs | 2 +- nodedb/tests/inproc/cases/core_loop.rs | 6 + .../cases/cross_engine_bitmap_currency.rs | 1 + .../inproc/cases/document_bitemporal_dml.rs | 18 +- .../inproc/cases/document_bitemporal_store.rs | 12 +- .../test_aggregate_chunk_limit.rs | 1 + .../cases/executor_tests/test_array_ops.rs | 18 +- .../executor_tests/test_columnar_aggregate.rs | 5 + .../test_conflict_policy_register.rs | 1 + .../test_cross_engine_validation.rs | 336 +- .../test_document_index_recovery.rs | 209 + .../executor_tests/test_generated_columns.rs | 1 + .../inproc/cases/executor_tests/test_graph.rs | 22 + .../cases/executor_tests/test_graph_bounds.rs | 9 + .../test_graph_edge_predicate.rs | 4 + .../test_graph_savepoint_overlay.rs | 5 + .../test_graph_txn_overlay_no_partition.rs | 3 + .../test_graph_txn_overlay_reads.rs | 6 + .../test_range_scan_bitemporal.rs | 2 + .../test_replay_and_engine_isolation.rs | 266 + .../cases/executor_tests/test_rrf_fusion.rs | 132 + .../test_security_and_isolation.rs | 283 +- .../test_tenant_isolation_graph.rs | 4 + .../test_tenant_isolation_graph_negative.rs | 9 + .../test_tenant_isolation_rls.rs | 23 +- .../test_tenant_isolation_rls_negative.rs | 54 +- .../cases/executor_tests/test_tenant_purge.rs | 3 + .../test_transaction_cross_engine.rs | 9 + .../test_transaction_matrix_helpers.rs | 3 + .../test_transaction_matrix_kv.rs | 3 + .../inproc/cases/graph_cross_core_bfs.rs | 2 + .../inproc/cases/graph_temporal_dispatch.rs | 43 +- .../inproc/cases/http_bearer_jwt_auth.rs | 11 +- .../tests/inproc/cases/http_client_sharing.rs | 25 +- .../tests/inproc/cases/oidc_authentication.rs | 41 +- .../tests/inproc/cases/oidc_bearer_audit.rs | 2 +- .../tests/inproc/cases/oidc_external_roles.rs | 46 +- .../tests/inproc/cases/oidc_native_claims.rs | 20 +- .../tests/inproc/cases/oidc_provider_ddl.rs | 98 + .../tests/inproc/cases/pgwire_auth_audit.rs | 6 +- nodedb/tests/inproc/cases/rls_fuzz.rs | 2 +- .../inproc/cases/role_assignment_rules.rs | 2 +- nodedb/tests/inproc/cases/siem_export.rs | 32 +- .../cases/surrogate_round_trip/inserts.rs | 3 + .../surrogate_round_trip/intersection.rs | 6 + nodedb/tests/inproc/cases/sync_compat.rs | 7 +- .../inproc/cases/sync_handshake_jwt_auth.rs | 6 +- .../cases/sync_shape_read_authorization.rs | 13 +- .../cases/sync_state_transition_by_role.rs | 109 +- .../inproc/cases/tls_policy_enforcement.rs | 2 +- nodedb/tests/inproc/cases/trigger_batching.rs | 36 +- .../cases/write_admission_leader_gate.rs | 57 + .../tests/wire/cases/alert_webhook_egress.rs | 48 + nodedb/tests/wire/cases/array_attr_meta.rs | 256 + .../wire/cases/array_attr_meta_geotiff.rs | 116 + nodedb/tests/wire/cases/array_georef.rs | 2 +- nodedb/tests/wire/cases/array_georef_zone.rs | 29 +- .../tests/wire/cases/array_geotiff/files.rs | 40 + .../tests/wire/cases/array_geotiff/fixture.rs | 19 +- .../array_geotiff/fixtures/adopt-metadata.tif | Bin 0 -> 2644 bytes .../cases/array_geotiff/fixtures/band-b4.tif | Bin 0 -> 4686 bytes .../fixtures/band-b5-shifted.tif | Bin 0 -> 4681 bytes .../fixtures/band-b5-tiles128.tif | Bin 0 -> 3252 bytes .../cases/array_geotiff/fixtures/band-b5.tif | Bin 0 -> 4681 bytes .../cases/array_geotiff/fixtures/band-qa.tif | Bin 0 -> 3042 bytes .../cases/array_geotiff/fixtures/expected.tsv | 8 + .../array_geotiff/fixtures/gen_fixtures.py | 130 + .../array_geotiff/fixtures/scene-overview.tif | Bin 0 -> 6701 bytes .../wire/cases/array_geotiff/http_server.rs | 97 + nodedb/tests/wire/cases/array_geotiff/mod.rs | 2 + .../tests/wire/cases/array_geotiff/support.rs | 13 +- .../wire/cases/array_geotiff_multi_source.rs | 171 + .../tests/wire/cases/array_geotiff_window.rs | 170 + nodedb/tests/wire/cases/array_reducers.rs | 226 + .../cases/bitemporal_array_statement_cut.rs | 146 + .../cases/bitemporal_document_txn_restart.rs | 2 +- .../wire/cases/bitemporal_statement_cut.rs | 201 + .../wire/cases/bitemporal_valid_time_range.rs | 31 + nodedb/tests/wire/cases/credential_ddl.rs | 242 + .../document_bitemporal_audit_deletes.rs | 106 + .../wire/cases/engine_surface_spatial.rs | 11 +- .../cases/graph_bitemporal_document_cut.rs | 190 + .../cases/graph_bitemporal_node_identity.rs | 134 + .../wire/cases/graph_bitemporal_reads.rs | 251 + .../wire/cases/graph_bitemporal_valid_time.rs | 240 + nodedb/tests/wire/cases/mod.rs | 26 + .../wire/cases/oidc_catalog_provider_boot.rs | 93 + .../tests/wire/cases/otlp_metrics_export.rs | 89 + nodedb/tests/wire/cases/secret_audit_text.rs | 70 + nodedb/tests/wire/cases/spatial_crs.rs | 12 +- nodedb/tests/wire/cases/spatial_crs_open.rs | 273 + nodedb/tests/wire/cases/spatial_crs_units.rs | 76 + nodedb/tests/wire/cases/spatial_ops.rs | 268 + nodedb/tests/wire/cases/spatial_ref_sys.rs | 344 + .../wire/cases/spatial_ref_sys_raster.rs | 256 + nodedb/tests/wire/cases/spatial_zm.rs | 334 + .../cases/sql_backup_restore_node_labels.rs | 104 + nodedb/tests/wire/cases/wal_key_vault.rs | 113 + nodedb/tests/wire/harness/config_toml.rs | 83 + nodedb/tests/wire/harness/lifecycle.rs | 30 + scripts/ci/check_robust_parsing.py | 1 + scripts/gen_epsg.py | 1095 + 1711 files changed, 125376 insertions(+), 16830 deletions(-) create mode 100644 fuzz/corpus/geotiff_decoder/cog-mask.tif create mode 100644 fuzz/corpus/geotiff_decoder/gdal-metadata.tif create mode 100644 fuzz/corpus/geotiff_decoder/j-gray-strip.tif create mode 100644 fuzz/corpus/geotiff_decoder/j-ycbcr-tile.tif create mode 100644 fuzz/corpus/geotiff_decoder/l-lerc-pixel-tile.tif create mode 100644 fuzz/corpus/geotiff_decoder/l-lerc_deflate-band-strip.tif create mode 100644 fuzz/corpus/geotiff_decoder/l-lerc_zstd-mask.tif create mode 100644 fuzz/corpus/geotiff_decoder/m-deflate-p3-tile-classic-contig-be.tif create mode 100644 fuzz/corpus/geotiff_decoder/m-lzw-p2-tile-big-separate-le.tif create mode 100644 fuzz/corpus/geotiff_decoder/m-none-p1-strip-classic-contig-be.tif create mode 100644 fuzz/corpus/geotiff_decoder/m-packbits-p1-tile-classic-separate-le.tif create mode 100644 fuzz/corpus/geotiff_decoder/m-zstd-p2-strip-big-contig-be.tif delete mode 100644 nodedb-array/src/query/aggregate.rs create mode 100644 nodedb-array/src/query/aggregate/fold.rs create mode 100644 nodedb-array/src/query/aggregate/mod.rs create mode 100644 nodedb-array/src/query/aggregate/reducer.rs create mode 100644 nodedb-array/src/query/aggregate/result.rs create mode 100644 nodedb-array/src/query/physical.rs create mode 100644 nodedb-array/src/schema/attr_meta.rs create mode 100644 nodedb-cluster-tests/tests/common_suite/cases/bitemporal_array_cluster_cut.rs create mode 100644 nodedb-cluster-tests/tests/common_suite/cases/bitemporal_replica_stamps.rs create mode 100644 nodedb-cluster-tests/tests/common_suite/cases/bitemporal_txn_columnar.rs create mode 100644 nodedb-cluster-tests/tests/common_suite/cases/cluster_restore_columnar_stamps.rs create mode 100644 nodedb-cluster-tests/tests/common_suite/cases/cluster_restore_labels_only_mark.rs create mode 100644 nodedb-cluster-tests/tests/common_suite/cases/cluster_spatial_ref_sys.rs create mode 100644 nodedb-cluster-tests/tests/common_suite/cases/columnar_bitemporal_cut_cluster.rs create mode 100644 nodedb-cluster-tests/tests/common_suite/cases/graph_live_cut_calvin_stamps.rs create mode 100644 nodedb-cluster-tests/tests/common_suite/cases/vshard_departure_crdt.rs create mode 100644 nodedb-cluster-tests/tests/common_suite/cases/vshard_departure_fixture.rs create mode 100644 nodedb-cluster-tests/tests/common_suite/cases/vshard_departure_purge.rs create mode 100644 nodedb-cluster-tests/tests/common_suite/cases/vshard_departure_purge_restart.rs create mode 100644 nodedb-cluster-tests/tests/sql_cluster_cross_node_dml_tests/graph_algo_pagerank_as_of_cross_node.rs create mode 100644 nodedb-cluster-tests/tests/sql_cluster_cross_node_dml_tests/graph_match_label_as_of_cross_node.rs create mode 100644 nodedb-cluster-tests/tests/sql_cluster_cross_node_dml_tests/graph_traverse_as_of_cross_node.rs create mode 100644 nodedb-columnar/src/geometry_cell.rs delete mode 100644 nodedb-columnar/src/memtable/column_data/mutation.rs delete mode 100644 nodedb-columnar/src/memtable/column_data/mutation_ingest.rs create mode 100644 nodedb-crdt/src/validator/incoming.rs create mode 100644 nodedb-crs/Cargo.toml create mode 100644 nodedb-crs/NOTICE create mode 100644 nodedb-crs/README.md create mode 100644 nodedb-crs/data/epsg.tsv create mode 100644 nodedb-crs/data/epsg_supplement.tsv create mode 100644 nodedb-crs/src/datum/ecef.rs create mode 100644 nodedb-crs/src/datum/helmert.rs create mode 100644 nodedb-crs/src/datum/mod.rs create mode 100644 nodedb-crs/src/def/area.rs create mode 100644 nodedb-crs/src/def/axis.rs create mode 100644 nodedb-crs/src/def/crs_def.rs create mode 100644 nodedb-crs/src/def/datum.rs create mode 100644 nodedb-crs/src/def/ellipsoid.rs create mode 100644 nodedb-crs/src/def/epoch.rs create mode 100644 nodedb-crs/src/def/helmert_rates.rs create mode 100644 nodedb-crs/src/def/mod.rs create mode 100644 nodedb-crs/src/def/unit.rs create mode 100644 nodedb-crs/src/error.rs create mode 100644 nodedb-crs/src/geo_transform.rs create mode 100644 nodedb-crs/src/lib.rs create mode 100644 nodedb-crs/src/method/aea.rs create mode 100644 nodedb-crs/src/method/aeqd.rs create mode 100644 nodedb-crs/src/method/angle.rs create mode 100644 nodedb-crs/src/method/authalic.rs create mode 100644 nodedb-crs/src/method/cass.rs create mode 100644 nodedb-crs/src/method/cea.rs create mode 100644 nodedb-crs/src/method/conformal.rs create mode 100644 nodedb-crs/src/method/eqc.rs create mode 100644 nodedb-crs/src/method/eqearth.rs create mode 100644 nodedb-crs/src/method/geodesic/inverse.rs create mode 100644 nodedb-crs/src/method/geodesic/line.rs create mode 100644 nodedb-crs/src/method/geodesic/math.rs create mode 100644 nodedb-crs/src/method/geodesic/mod.rs create mode 100644 nodedb-crs/src/method/geodesic/model.rs create mode 100644 nodedb-crs/src/method/geodesic/polygon.rs create mode 100644 nodedb-crs/src/method/geodesic/series.rs create mode 100644 nodedb-crs/src/method/geodesic/start.rs create mode 100644 nodedb-crs/src/method/geos.rs create mode 100644 nodedb-crs/src/method/krovak.rs create mode 100644 nodedb-crs/src/method/labrd.rs create mode 100644 nodedb-crs/src/method/laea.rs create mode 100644 nodedb-crs/src/method/lcc.rs create mode 100644 nodedb-crs/src/method/merc.rs create mode 100644 nodedb-crs/src/method/meridian.rs create mode 100644 nodedb-crs/src/method/mod.rs create mode 100644 nodedb-crs/src/method/nzmg.rs rename nodedb-spatial/src/crs/hom.rs => nodedb-crs/src/method/omerc.rs (50%) create mode 100644 nodedb-crs/src/method/poly.rs create mode 100644 nodedb-crs/src/method/projection.rs create mode 100644 nodedb-crs/src/method/projector.rs create mode 100644 nodedb-crs/src/method/sinu.rs create mode 100644 nodedb-crs/src/method/somerc.rs create mode 100644 nodedb-crs/src/method/stere.rs create mode 100644 nodedb-crs/src/method/sterea.rs rename nodedb-spatial/src/crs/tm.rs => nodedb-crs/src/method/tmerc.rs (52%) create mode 100644 nodedb-crs/src/method/webmerc.rs create mode 100644 nodedb-crs/src/parse/helmert_params.rs create mode 100644 nodedb-crs/src/parse/json.rs create mode 100644 nodedb-crs/src/parse/methods/azimuthal.rs create mode 100644 nodedb-crs/src/parse/methods/conic.rs create mode 100644 nodedb-crs/src/parse/methods/cylindrical.rs create mode 100644 nodedb-crs/src/parse/methods/lookup.rs create mode 100644 nodedb-crs/src/parse/methods/mercator.rs create mode 100644 nodedb-crs/src/parse/methods/mod.rs create mode 100644 nodedb-crs/src/parse/methods/params.rs create mode 100644 nodedb-crs/src/parse/methods/spec.rs create mode 100644 nodedb-crs/src/parse/mod.rs create mode 100644 nodedb-crs/src/parse/model.rs create mode 100644 nodedb-crs/src/parse/proj_string.rs create mode 100644 nodedb-crs/src/parse/proj_writer.rs create mode 100644 nodedb-crs/src/parse/projjson.rs create mode 100644 nodedb-crs/src/parse/projjson_writer.rs create mode 100644 nodedb-crs/src/parse/round_trip_cases.rs create mode 100644 nodedb-crs/src/parse/tokens.rs create mode 100644 nodedb-crs/src/parse/unit_names.rs create mode 100644 nodedb-crs/src/parse/wkt2.rs create mode 100644 nodedb-crs/src/parse/wkt2_tree.rs create mode 100644 nodedb-crs/src/parse/wkt2_writer.rs create mode 100644 nodedb-crs/src/registry/builtin.rs create mode 100644 nodedb-crs/src/registry/context.rs create mode 100644 nodedb-crs/src/registry/custom.rs create mode 100644 nodedb-crs/src/registry/mod.rs create mode 100644 nodedb-crs/src/transform.rs create mode 100644 nodedb-crs/tests/fixtures/proj_reference.tsv create mode 100644 nodedb-crs/tests/fixtures/proj_reference_malaysia.tsv create mode 100644 nodedb-crs/tests/gdm2020.rs create mode 100644 nodedb-crs/tests/proj_reference.rs create mode 100644 nodedb-physical/src/physical_plan/graph/read_scope.rs create mode 100644 nodedb-query/src/geo_functions/encode.rs create mode 100644 nodedb-query/src/geo_functions/linear.rs create mode 100644 nodedb-query/src/geo_functions/overlay.rs create mode 100644 nodedb-query/src/geo_functions/srid_refs.rs create mode 100644 nodedb-raster-load/Cargo.toml create mode 100644 nodedb-raster-load/NOTICE create mode 100644 nodedb-raster-load/src/bands.rs create mode 100644 nodedb-raster-load/src/coords.rs create mode 100644 nodedb-raster-load/src/error.rs create mode 100644 nodedb-raster-load/src/grid.rs create mode 100644 nodedb-raster-load/src/lib.rs create mode 100644 nodedb-raster-load/src/metadata.rs create mode 100644 nodedb-raster-load/src/nodata.rs create mode 100644 nodedb-raster-load/src/options.rs create mode 100644 nodedb-raster-load/src/plan.rs create mode 100644 nodedb-raster-load/src/rows.rs create mode 100644 nodedb-raster-load/src/source.rs create mode 100644 nodedb-raster-load/src/view/mod.rs create mode 100644 nodedb-raster-load/src/view/row.rs create mode 100644 nodedb-raster-load/src/view/schema.rs create mode 100644 nodedb-raster-load/src/window.rs create mode 100644 nodedb-raster/src/codec/compression.rs create mode 100644 nodedb-raster/src/codec/deflate.rs create mode 100644 nodedb-raster/src/codec/error.rs create mode 100644 nodedb-raster/src/codec/jpeg.rs create mode 100644 nodedb-raster/src/codec/lerc.rs create mode 100644 nodedb-raster/src/codec/lerc_blob.rs create mode 100644 nodedb-raster/src/codec/lzw.rs create mode 100644 nodedb-raster/src/codec/mod.rs create mode 100644 nodedb-raster/src/codec/packbits.rs create mode 100644 nodedb-raster/src/codec/predictor.rs create mode 100644 nodedb-raster/src/codec/zstd.rs delete mode 100644 nodedb-raster/src/crs.rs create mode 100644 nodedb-raster/src/crs/mod.rs create mode 100644 nodedb-raster/src/crs/projection_keys.rs create mode 100644 nodedb-raster/src/crs/registry_match.rs create mode 100644 nodedb-raster/src/crs/srid.rs create mode 100644 nodedb-raster/src/crs/units.rs create mode 100644 nodedb-raster/src/crs/user_defined.rs create mode 100644 nodedb-raster/src/header.rs delete mode 100644 nodedb-raster/src/preflight.rs delete mode 100644 nodedb-raster/src/reader.rs create mode 100644 nodedb-raster/src/reader/driver.rs create mode 100644 nodedb-raster/src/reader/mod.rs create mode 100644 nodedb-raster/src/reader/traits.rs create mode 100644 nodedb-raster/src/source/cache.rs create mode 100644 nodedb-raster/src/source/mod.rs create mode 100644 nodedb-raster/src/source/range.rs create mode 100644 nodedb-raster/src/tiff/byte_order.rs create mode 100644 nodedb-raster/src/tiff/chain.rs create mode 100644 nodedb-raster/src/tiff/decode.rs create mode 100644 nodedb-raster/src/tiff/error.rs create mode 100644 nodedb-raster/src/tiff/fetch.rs create mode 100644 nodedb-raster/src/tiff/file_header.rs create mode 100644 nodedb-raster/src/tiff/gdal_metadata.rs create mode 100644 nodedb-raster/src/tiff/geo.rs rename nodedb-raster/src/{ => tiff}/geokeys.rs (78%) create mode 100644 nodedb-raster/src/tiff/ifd.rs create mode 100644 nodedb-raster/src/tiff/image.rs create mode 100644 nodedb-raster/src/tiff/mask.rs create mode 100644 nodedb-raster/src/tiff/mod.rs create mode 100644 nodedb-raster/src/tiff/opener.rs create mode 100644 nodedb-raster/src/tiff/reader.rs create mode 100644 nodedb-raster/src/tiff/samples.rs create mode 100644 nodedb-raster/src/tiff/tags.rs create mode 100644 nodedb-raster/src/tiff/tile_index.rs rename nodedb-raster/src/{ => tiff}/transform.rs (69%) create mode 100644 nodedb-raster/src/tiff/values.rs create mode 100644 nodedb-raster/tests/fixtures/cog-mask-0.mask.z create mode 100644 nodedb-raster/tests/fixtures/cog-mask-1.mask.z create mode 100644 nodedb-raster/tests/fixtures/cog-mask-1.raw.z create mode 100644 nodedb-raster/tests/fixtures/cog-mask-2.mask.z create mode 100644 nodedb-raster/tests/fixtures/cog-mask-2.raw.z create mode 100644 nodedb-raster/tests/fixtures/cog-mask.tif create mode 100644 nodedb-raster/tests/fixtures/gdal-metadata.tif create mode 100644 nodedb-raster/tests/fixtures/gen_fixtures.py create mode 100644 nodedb-raster/tests/fixtures/j-gray-strip.raw.z create mode 100644 nodedb-raster/tests/fixtures/j-gray-strip.tif create mode 100644 nodedb-raster/tests/fixtures/j-rgb-tile.raw.z create mode 100644 nodedb-raster/tests/fixtures/j-rgb-tile.tif create mode 100644 nodedb-raster/tests/fixtures/j-ycbcr-tile.raw.z create mode 100644 nodedb-raster/tests/fixtures/j-ycbcr-tile.tif create mode 100644 nodedb-raster/tests/fixtures/l-lerc-band-strip.tif create mode 100644 nodedb-raster/tests/fixtures/l-lerc-band-tile.tif create mode 100644 nodedb-raster/tests/fixtures/l-lerc-bigtiff.tif create mode 100644 nodedb-raster/tests/fixtures/l-lerc-mask.mask.z create mode 100644 nodedb-raster/tests/fixtures/l-lerc-mask.raw.z create mode 100644 nodedb-raster/tests/fixtures/l-lerc-mask.tif create mode 100644 nodedb-raster/tests/fixtures/l-lerc-pixel-strip.tif create mode 100644 nodedb-raster/tests/fixtures/l-lerc-pixel-tile.tif create mode 100644 nodedb-raster/tests/fixtures/l-lerc_deflate-band-strip.tif create mode 100644 nodedb-raster/tests/fixtures/l-lerc_deflate-band-tile.tif create mode 100644 nodedb-raster/tests/fixtures/l-lerc_deflate-mask.mask.z create mode 100644 nodedb-raster/tests/fixtures/l-lerc_deflate-mask.raw.z create mode 100644 nodedb-raster/tests/fixtures/l-lerc_deflate-mask.tif create mode 100644 nodedb-raster/tests/fixtures/l-lerc_deflate-pixel-strip.tif create mode 100644 nodedb-raster/tests/fixtures/l-lerc_deflate-pixel-tile.tif create mode 100644 nodedb-raster/tests/fixtures/l-lerc_zstd-band-strip.tif create mode 100644 nodedb-raster/tests/fixtures/l-lerc_zstd-band-tile.tif create mode 100644 nodedb-raster/tests/fixtures/l-lerc_zstd-mask.mask.z create mode 100644 nodedb-raster/tests/fixtures/l-lerc_zstd-mask.raw.z create mode 100644 nodedb-raster/tests/fixtures/l-lerc_zstd-mask.tif create mode 100644 nodedb-raster/tests/fixtures/l-lerc_zstd-pixel-strip.tif create mode 100644 nodedb-raster/tests/fixtures/l-lerc_zstd-pixel-tile.tif create mode 100644 nodedb-raster/tests/fixtures/landsat-c2-1.raw.z create mode 100644 nodedb-raster/tests/fixtures/landsat-c2.raw.z create mode 100644 nodedb-raster/tests/fixtures/landsat-c2.tif create mode 100644 nodedb-raster/tests/fixtures/m-deflate-p1-strip-big-contig-be.tif create mode 100644 nodedb-raster/tests/fixtures/m-deflate-p1-strip-big-separate-le.tif create mode 100644 nodedb-raster/tests/fixtures/m-deflate-p1-strip-classic-contig-be.tif create mode 100644 nodedb-raster/tests/fixtures/m-deflate-p1-strip-classic-separate-le.tif create mode 100644 nodedb-raster/tests/fixtures/m-deflate-p1-tile-big-contig-be.tif create mode 100644 nodedb-raster/tests/fixtures/m-deflate-p1-tile-big-separate-le.tif create mode 100644 nodedb-raster/tests/fixtures/m-deflate-p1-tile-classic-contig-be.tif create mode 100644 nodedb-raster/tests/fixtures/m-deflate-p1-tile-classic-separate-le.tif create mode 100644 nodedb-raster/tests/fixtures/m-deflate-p2-strip-big-contig-be.tif create mode 100644 nodedb-raster/tests/fixtures/m-deflate-p2-strip-big-separate-le.tif create mode 100644 nodedb-raster/tests/fixtures/m-deflate-p2-strip-classic-contig-be.tif create mode 100644 nodedb-raster/tests/fixtures/m-deflate-p2-strip-classic-separate-le.tif create mode 100644 nodedb-raster/tests/fixtures/m-deflate-p2-tile-big-contig-be.tif create mode 100644 nodedb-raster/tests/fixtures/m-deflate-p2-tile-big-separate-le.tif create mode 100644 nodedb-raster/tests/fixtures/m-deflate-p2-tile-classic-contig-be.tif create mode 100644 nodedb-raster/tests/fixtures/m-deflate-p2-tile-classic-separate-le.tif create mode 100644 nodedb-raster/tests/fixtures/m-deflate-p3-strip-big-contig-be.tif create mode 100644 nodedb-raster/tests/fixtures/m-deflate-p3-strip-big-separate-le.tif create mode 100644 nodedb-raster/tests/fixtures/m-deflate-p3-strip-classic-contig-be.tif create mode 100644 nodedb-raster/tests/fixtures/m-deflate-p3-strip-classic-separate-le.tif create mode 100644 nodedb-raster/tests/fixtures/m-deflate-p3-tile-big-contig-be.tif create mode 100644 nodedb-raster/tests/fixtures/m-deflate-p3-tile-big-separate-le.tif create mode 100644 nodedb-raster/tests/fixtures/m-deflate-p3-tile-classic-contig-be.tif create mode 100644 nodedb-raster/tests/fixtures/m-deflate-p3-tile-classic-separate-le.tif create mode 100644 nodedb-raster/tests/fixtures/m-lzw-p1-strip-big-contig-be.tif create mode 100644 nodedb-raster/tests/fixtures/m-lzw-p1-strip-big-separate-le.tif create mode 100644 nodedb-raster/tests/fixtures/m-lzw-p1-strip-classic-contig-be.tif create mode 100644 nodedb-raster/tests/fixtures/m-lzw-p1-strip-classic-separate-le.tif create mode 100644 nodedb-raster/tests/fixtures/m-lzw-p1-tile-big-contig-be.tif create mode 100644 nodedb-raster/tests/fixtures/m-lzw-p1-tile-big-separate-le.tif create mode 100644 nodedb-raster/tests/fixtures/m-lzw-p1-tile-classic-contig-be.tif create mode 100644 nodedb-raster/tests/fixtures/m-lzw-p1-tile-classic-separate-le.tif create mode 100644 nodedb-raster/tests/fixtures/m-lzw-p2-strip-big-contig-be.tif create mode 100644 nodedb-raster/tests/fixtures/m-lzw-p2-strip-big-separate-le.tif create mode 100644 nodedb-raster/tests/fixtures/m-lzw-p2-strip-classic-contig-be.tif create mode 100644 nodedb-raster/tests/fixtures/m-lzw-p2-strip-classic-separate-le.tif create mode 100644 nodedb-raster/tests/fixtures/m-lzw-p2-tile-big-contig-be.tif create mode 100644 nodedb-raster/tests/fixtures/m-lzw-p2-tile-big-separate-le.tif create mode 100644 nodedb-raster/tests/fixtures/m-lzw-p2-tile-classic-contig-be.tif create mode 100644 nodedb-raster/tests/fixtures/m-lzw-p2-tile-classic-separate-le.tif create mode 100644 nodedb-raster/tests/fixtures/m-lzw-p3-strip-big-contig-be.tif create mode 100644 nodedb-raster/tests/fixtures/m-lzw-p3-strip-big-separate-le.tif create mode 100644 nodedb-raster/tests/fixtures/m-lzw-p3-strip-classic-contig-be.tif create mode 100644 nodedb-raster/tests/fixtures/m-lzw-p3-strip-classic-separate-le.tif create mode 100644 nodedb-raster/tests/fixtures/m-lzw-p3-tile-big-contig-be.tif create mode 100644 nodedb-raster/tests/fixtures/m-lzw-p3-tile-big-separate-le.tif create mode 100644 nodedb-raster/tests/fixtures/m-lzw-p3-tile-classic-contig-be.tif create mode 100644 nodedb-raster/tests/fixtures/m-lzw-p3-tile-classic-separate-le.tif create mode 100644 nodedb-raster/tests/fixtures/m-none-p1-strip-big-contig-be.tif create mode 100644 nodedb-raster/tests/fixtures/m-none-p1-strip-big-separate-le.tif create mode 100644 nodedb-raster/tests/fixtures/m-none-p1-strip-classic-contig-be.tif create mode 100644 nodedb-raster/tests/fixtures/m-none-p1-strip-classic-separate-le.tif create mode 100644 nodedb-raster/tests/fixtures/m-none-p1-tile-big-contig-be.tif create mode 100644 nodedb-raster/tests/fixtures/m-none-p1-tile-big-separate-le.tif create mode 100644 nodedb-raster/tests/fixtures/m-none-p1-tile-classic-contig-be.tif create mode 100644 nodedb-raster/tests/fixtures/m-none-p1-tile-classic-separate-le.tif create mode 100644 nodedb-raster/tests/fixtures/m-packbits-p1-strip-big-contig-be.tif create mode 100644 nodedb-raster/tests/fixtures/m-packbits-p1-strip-big-separate-le.tif create mode 100644 nodedb-raster/tests/fixtures/m-packbits-p1-strip-classic-contig-be.tif create mode 100644 nodedb-raster/tests/fixtures/m-packbits-p1-strip-classic-separate-le.tif create mode 100644 nodedb-raster/tests/fixtures/m-packbits-p1-tile-big-contig-be.tif create mode 100644 nodedb-raster/tests/fixtures/m-packbits-p1-tile-big-separate-le.tif create mode 100644 nodedb-raster/tests/fixtures/m-packbits-p1-tile-classic-contig-be.tif create mode 100644 nodedb-raster/tests/fixtures/m-packbits-p1-tile-classic-separate-le.tif create mode 100644 nodedb-raster/tests/fixtures/m-zstd-p1-strip-big-contig-be.tif create mode 100644 nodedb-raster/tests/fixtures/m-zstd-p1-strip-big-separate-le.tif create mode 100644 nodedb-raster/tests/fixtures/m-zstd-p1-strip-classic-contig-be.tif create mode 100644 nodedb-raster/tests/fixtures/m-zstd-p1-strip-classic-separate-le.tif create mode 100644 nodedb-raster/tests/fixtures/m-zstd-p1-tile-big-contig-be.tif create mode 100644 nodedb-raster/tests/fixtures/m-zstd-p1-tile-big-separate-le.tif create mode 100644 nodedb-raster/tests/fixtures/m-zstd-p1-tile-classic-contig-be.tif create mode 100644 nodedb-raster/tests/fixtures/m-zstd-p1-tile-classic-separate-le.tif create mode 100644 nodedb-raster/tests/fixtures/m-zstd-p2-strip-big-contig-be.tif create mode 100644 nodedb-raster/tests/fixtures/m-zstd-p2-strip-big-separate-le.tif create mode 100644 nodedb-raster/tests/fixtures/m-zstd-p2-strip-classic-contig-be.tif create mode 100644 nodedb-raster/tests/fixtures/m-zstd-p2-strip-classic-separate-le.tif create mode 100644 nodedb-raster/tests/fixtures/m-zstd-p2-tile-big-contig-be.tif create mode 100644 nodedb-raster/tests/fixtures/m-zstd-p2-tile-big-separate-le.tif create mode 100644 nodedb-raster/tests/fixtures/m-zstd-p2-tile-classic-contig-be.tif create mode 100644 nodedb-raster/tests/fixtures/m-zstd-p2-tile-classic-separate-le.tif create mode 100644 nodedb-raster/tests/fixtures/m-zstd-p3-strip-big-contig-be.tif create mode 100644 nodedb-raster/tests/fixtures/m-zstd-p3-strip-big-separate-le.tif create mode 100644 nodedb-raster/tests/fixtures/m-zstd-p3-strip-classic-contig-be.tif create mode 100644 nodedb-raster/tests/fixtures/m-zstd-p3-strip-classic-separate-le.tif create mode 100644 nodedb-raster/tests/fixtures/m-zstd-p3-tile-big-contig-be.tif create mode 100644 nodedb-raster/tests/fixtures/m-zstd-p3-tile-big-separate-le.tif create mode 100644 nodedb-raster/tests/fixtures/m-zstd-p3-tile-classic-contig-be.tif create mode 100644 nodedb-raster/tests/fixtures/m-zstd-p3-tile-classic-separate-le.tif create mode 100644 nodedb-raster/tests/fixtures/manifest.tsv create mode 100644 nodedb-raster/tests/fixtures/t-float64.tif create mode 100644 nodedb-raster/tests/fixtures/t-int16.tif create mode 100644 nodedb-raster/tests/fixtures/t-int32.tif create mode 100644 nodedb-raster/tests/fixtures/t-int8.tif create mode 100644 nodedb-raster/tests/fixtures/t-uint32.tif create mode 100644 nodedb-raster/tests/fixtures/t-uint8.tif create mode 100644 nodedb-raster/tests/geotiff/cog.rs create mode 100644 nodedb-raster/tests/geotiff/files.rs create mode 100644 nodedb-raster/tests/geotiff/image.rs create mode 100644 nodedb-raster/tests/geotiff/malformed.rs delete mode 100644 nodedb-spatial/src/crs/ellipsoid.rs delete mode 100644 nodedb-spatial/src/crs/error.rs delete mode 100644 nodedb-spatial/src/crs/params.rs create mode 100644 nodedb-spatial/src/distance3d.rs create mode 100644 nodedb-spatial/src/error.rs create mode 100644 nodedb-spatial/src/geodesic/measure.rs create mode 100644 nodedb-spatial/src/geodesic/mod.rs create mode 100644 nodedb-spatial/src/geodesic/segment.rs create mode 100644 nodedb-spatial/src/metric.rs create mode 100644 nodedb-spatial/src/ops/boolean.rs create mode 100644 nodedb-spatial/src/ops/buffer.rs create mode 100644 nodedb-spatial/src/ops/closest.rs create mode 100644 nodedb-spatial/src/ops/convert.rs create mode 100644 nodedb-spatial/src/ops/hull.rs create mode 100644 nodedb-spatial/src/ops/linear.rs create mode 100644 nodedb-spatial/src/ops/mod.rs create mode 100644 nodedb-spatial/src/ops/noding.rs create mode 100644 nodedb-spatial/src/ops/pieces.rs create mode 100644 nodedb-spatial/src/ops/relate.rs create mode 100644 nodedb-spatial/src/ops/repair.rs create mode 100644 nodedb-spatial/src/ops/simplify.rs create mode 100644 nodedb-spatial/src/ops/split.rs create mode 100644 nodedb-spatial/src/ops/subdivide.rs create mode 100644 nodedb-spatial/src/ops/tile.rs create mode 100644 nodedb-spatial/src/parts.rs delete mode 100644 nodedb-spatial/src/planar/buffer.rs delete mode 100644 nodedb-spatial/src/predicates/intersection.rs create mode 100644 nodedb-spatial/src/rtree/tree_srid.rs create mode 100644 nodedb-spatial/src/text.rs create mode 100644 nodedb-spatial/src/value.rs create mode 100644 nodedb-spatial/src/wkb/error.rs delete mode 100644 nodedb-spatial/src/wkt.rs create mode 100644 nodedb-spatial/src/wkt/error.rs create mode 100644 nodedb-spatial/src/wkt/mod.rs create mode 100644 nodedb-spatial/src/wkt/read.rs create mode 100644 nodedb-spatial/src/wkt/write.rs create mode 100644 nodedb-sql/src/ddl_ast/parse/audit_text.rs create mode 100644 nodedb-sql/src/ddl_ast/parse/credential.rs create mode 100644 nodedb-sql/src/ddl_ast/parse/spatial_ref_sys.rs create mode 100644 nodedb-sql/src/ddl_ast/statement/copy_source.rs create mode 100644 nodedb-sql/src/ddl_ast/statement/credential.rs create mode 100644 nodedb-sql/src/parser/array_stmt/parse_impl/attr_meta.rs create mode 100644 nodedb-sql/src/planner/array_fn/agg_args.rs delete mode 100644 nodedb-sql/src/types_array.rs create mode 100644 nodedb-sql/src/types_array/attr.rs create mode 100644 nodedb-sql/src/types_array/mod.rs create mode 100644 nodedb-sql/src/types_array/ops.rs create mode 100644 nodedb-sql/src/types_array/query.rs create mode 100644 nodedb-sql/src/types_array/schema.rs create mode 100644 nodedb-srid/Cargo.toml create mode 100644 nodedb-srid/NOTICE create mode 100644 nodedb-srid/src/error.rs create mode 100644 nodedb-srid/src/kind.rs create mode 100644 nodedb-srid/src/lib.rs create mode 100644 nodedb-srid/src/name.rs create mode 100644 nodedb-srid/src/resolver.rs create mode 100644 nodedb-srid/src/srid.rs create mode 100644 nodedb-types/src/array_agg/dist.rs create mode 100644 nodedb-types/src/array_agg/dist_acc.rs create mode 100644 nodedb-types/src/array_agg/histogram.rs create mode 100644 nodedb-types/src/array_agg/mod.rs create mode 100644 nodedb-types/src/array_agg/moments.rs create mode 100644 nodedb-types/src/geometry/ordinates.rs delete mode 100644 nodedb-types/src/geometry/tagged.rs create mode 100644 nodedb-types/src/geometry/tagged/crs_member.rs create mode 100644 nodedb-types/src/geometry/tagged/geojson_read.rs create mode 100644 nodedb-types/src/geometry/tagged/geojson_write.rs create mode 100644 nodedb-types/src/geometry/tagged/mod.rs create mode 100644 nodedb-types/src/geometry/tagged/msgpack.rs create mode 100644 nodedb-types/src/geometry/tagged/srid_geometry.rs create mode 100644 nodedb-types/src/json_msgpack/geometry_ext.rs create mode 100644 nodedb-types/src/wall_clock.rs create mode 100644 nodedb/src/bootstrap/jwks_registry.rs create mode 100644 nodedb/src/bootstrap/remote_sources.rs create mode 100644 nodedb/src/bridge/envelope/applied_entry.rs create mode 100644 nodedb/src/config/server/egress.rs create mode 100644 nodedb/src/control/array_catalog/attr_meta.rs create mode 100644 nodedb/src/control/array_catalog/schema_recode.rs create mode 100644 nodedb/src/control/backup/restore/redo_reissue/labels.rs create mode 100644 nodedb/src/control/catalog_entry/apply/credential.rs create mode 100644 nodedb/src/control/catalog_entry/apply/spatial_ref_sys.rs create mode 100644 nodedb/src/control/catalog_entry/post_apply/async_dispatch/spatial_ref_sys.rs create mode 100644 nodedb/src/control/catalog_entry/post_apply/oidc_provider.rs create mode 100644 nodedb/src/control/cluster/vshard_departure/fence.rs create mode 100644 nodedb/src/control/cluster/vshard_departure/ledger.rs create mode 100644 nodedb/src/control/cluster/vshard_departure/mod.rs create mode 100644 nodedb/src/control/cluster/vshard_departure/pass.rs create mode 100644 nodedb/src/control/cluster/vshard_departure/purge.rs create mode 100644 nodedb/src/control/cluster/vshard_departure/resident.rs create mode 100644 nodedb/src/control/cluster/vshard_departure/task.rs create mode 100644 nodedb/src/control/crs_context.rs delete mode 100644 nodedb/src/control/lease/wall_time.rs rename nodedb/src/control/planner/calvin/{edge_sequencing.rs => graph_sequencing.rs} (60%) create mode 100644 nodedb/src/control/planner/context/query/statement_cut.rs create mode 100644 nodedb/src/control/planner/plan_children/walk_mut.rs delete mode 100644 nodedb/src/control/planner/sql_plan_convert/scan/helpers.rs create mode 100644 nodedb/src/control/planner/sql_plan_convert/valid_time.rs create mode 100644 nodedb/src/control/planner/system_cut/mod.rs create mode 100644 nodedb/src/control/planner/system_cut/pin.rs create mode 100644 nodedb/src/control/planner/system_cut/sites.rs create mode 100644 nodedb/src/control/read_cut/mod.rs rename nodedb/src/control/{server/graph_dispatch/run_cut.rs => read_cut/run_marker.rs} (90%) create mode 100644 nodedb/src/control/read_cut/statement_cap.rs create mode 100644 nodedb/src/control/read_cut/wait.rs create mode 100644 nodedb/src/control/security/catalog/credentials.rs create mode 100644 nodedb/src/control/security/catalog/spatial_ref_sys.rs create mode 100644 nodedb/src/control/security/catalog/srid_refs/mod.rs create mode 100644 nodedb/src/control/security/catalog/srid_refs/scan.rs create mode 100644 nodedb/src/control/security/catalog/srid_refs/text.rs create mode 100644 nodedb/src/control/security/catalog/test_faults.rs create mode 100644 nodedb/src/control/security/catalog/vshard_residency.rs create mode 100644 nodedb/src/control/security/catalog/wasm_raw.rs create mode 100644 nodedb/src/control/security/egress/client.rs create mode 100644 nodedb/src/control/security/egress/global_unicast.rs create mode 100644 nodedb/src/control/security/egress/mod.rs create mode 100644 nodedb/src/control/security/egress/policy.rs create mode 100644 nodedb/src/control/security/egress/remote.rs create mode 100644 nodedb/src/control/security/egress/resolver.rs create mode 100644 nodedb/src/control/security/jwks/registry/catalog.rs create mode 100644 nodedb/src/control/security/jwks/registry/targets.rs delete mode 100644 nodedb/src/control/security/jwks/url.rs create mode 100644 nodedb/src/control/security/keystore/wal_key.rs create mode 100644 nodedb/src/control/security/net/cidr.rs create mode 100644 nodedb/src/control/security/net/mod.rs create mode 100644 nodedb/src/control/security/source_credential/kek.rs create mode 100644 nodedb/src/control/security/source_credential/mod.rs create mode 100644 nodedb/src/control/security/source_credential/seal.rs create mode 100644 nodedb/src/control/security/source_credential/secret.rs create mode 100644 nodedb/src/control/security/source_credential/stored.rs create mode 100644 nodedb/src/control/server/graph_dispatch/read_scope.rs create mode 100644 nodedb/src/control/server/pgwire/catalog/spatial_ref_sys.rs create mode 100644 nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/adopt.rs delete mode 100644 nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/bands.rs create mode 100644 nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/driver/array_view.rs create mode 100644 nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/driver/blocks.rs create mode 100644 nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/driver/drive.rs create mode 100644 nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/driver/error.rs create mode 100644 nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/driver/load_options.rs create mode 100644 nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/driver/mod.rs create mode 100644 nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/driver/open.rs create mode 100644 nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/fetch/error.rs create mode 100644 nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/fetch/http.rs create mode 100644 nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/fetch/local.rs create mode 100644 nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/fetch/memory.rs create mode 100644 nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/fetch/mod.rs create mode 100644 nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/fetch/pin.rs create mode 100644 nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/fetch/sources.rs create mode 100644 nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/fetch/store.rs create mode 100644 nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/fetch/test_server.rs create mode 100644 nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/fetch/traits.rs delete mode 100644 nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/grid.rs delete mode 100644 nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/open.rs delete mode 100644 nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/pixels.rs create mode 100644 nodedb/src/control/server/shared/ddl/neutral/credential.rs create mode 100644 nodedb/src/control/server/shared/ddl/neutral/spatial_ref_sys.rs create mode 100644 nodedb/src/control/server/shared/session/commit/buffered_dispatch.rs delete mode 100644 nodedb/src/control/server/wal_dispatch/graph_labels.rs create mode 100644 nodedb/src/control/server/wal_dispatch/system_stamp.rs create mode 100644 nodedb/src/control/state/init_prod/open_config.rs create mode 100644 nodedb/src/control/wal_replication/types/system_span.rs create mode 100644 nodedb/src/data/executor/core_loop/crs_context.rs create mode 100644 nodedb/src/data/executor/core_loop/graph_view.rs create mode 100644 nodedb/src/data/executor/core_loop/hash_chain_state.rs create mode 100644 nodedb/src/data/executor/core_loop/ts_replay_state.rs create mode 100644 nodedb/src/data/executor/dispatch/graph/rag.rs create mode 100644 nodedb/src/data/executor/graph_label_apply.rs create mode 100644 nodedb/src/data/executor/handlers/columnar_close_apply.rs create mode 100644 nodedb/src/data/executor/handlers/columnar_read/system_fold.rs create mode 100644 nodedb/src/data/executor/handlers/columnar_read/versions.rs create mode 100644 nodedb/src/data/executor/handlers/columnar_write/system_stamp.rs create mode 100644 nodedb/src/data/executor/handlers/graph_algo_csr.rs create mode 100644 nodedb/src/data/executor/handlers/graph_match_envelope.rs create mode 100644 nodedb/src/data/executor/handlers/graph_match_view.rs create mode 100644 nodedb/src/data/executor/handlers/snapshot/capture_labels.rs create mode 100644 nodedb/src/data/executor/handlers/snapshot/departure.rs create mode 100644 nodedb/src/data/executor/handlers/snapshot/restore/node_labels.rs create mode 100644 nodedb/src/data/executor/handlers/transaction/resolve/columnar_ordinal.rs create mode 100644 nodedb/src/data/executor/handlers/transaction/resolve/doc_ordinal.rs create mode 100644 nodedb/src/diag/context/crs.rs create mode 100644 nodedb/src/diag/context/vshard_departure.rs create mode 100644 nodedb/src/diag/recording/crs.rs create mode 100644 nodedb/src/diag/recording/vshard_departure.rs create mode 100644 nodedb/src/engine/graph/csr/historical_cache.rs create mode 100644 nodedb/src/engine/graph/edge_store/node_label_snapshot.rs create mode 100644 nodedb/src/engine/graph/edge_store/node_labels.rs create mode 100644 nodedb/src/engine/graph/edge_store/temporal/collection_scan.rs create mode 100644 nodedb/src/error_from_spatial.rs create mode 100644 nodedb/src/wal/redo/node_label.rs create mode 100644 nodedb/tests/inproc/cases/executor_tests/test_document_index_recovery.rs create mode 100644 nodedb/tests/inproc/cases/executor_tests/test_replay_and_engine_isolation.rs create mode 100644 nodedb/tests/inproc/cases/executor_tests/test_rrf_fusion.rs create mode 100644 nodedb/tests/wire/cases/alert_webhook_egress.rs create mode 100644 nodedb/tests/wire/cases/array_attr_meta.rs create mode 100644 nodedb/tests/wire/cases/array_attr_meta_geotiff.rs create mode 100644 nodedb/tests/wire/cases/array_geotiff/files.rs create mode 100644 nodedb/tests/wire/cases/array_geotiff/fixtures/adopt-metadata.tif create mode 100644 nodedb/tests/wire/cases/array_geotiff/fixtures/band-b4.tif create mode 100644 nodedb/tests/wire/cases/array_geotiff/fixtures/band-b5-shifted.tif create mode 100644 nodedb/tests/wire/cases/array_geotiff/fixtures/band-b5-tiles128.tif create mode 100644 nodedb/tests/wire/cases/array_geotiff/fixtures/band-b5.tif create mode 100644 nodedb/tests/wire/cases/array_geotiff/fixtures/band-qa.tif create mode 100644 nodedb/tests/wire/cases/array_geotiff/fixtures/expected.tsv create mode 100644 nodedb/tests/wire/cases/array_geotiff/fixtures/gen_fixtures.py create mode 100644 nodedb/tests/wire/cases/array_geotiff/fixtures/scene-overview.tif create mode 100644 nodedb/tests/wire/cases/array_geotiff/http_server.rs create mode 100644 nodedb/tests/wire/cases/array_geotiff_multi_source.rs create mode 100644 nodedb/tests/wire/cases/array_geotiff_window.rs create mode 100644 nodedb/tests/wire/cases/array_reducers.rs create mode 100644 nodedb/tests/wire/cases/bitemporal_array_statement_cut.rs create mode 100644 nodedb/tests/wire/cases/bitemporal_statement_cut.rs create mode 100644 nodedb/tests/wire/cases/bitemporal_valid_time_range.rs create mode 100644 nodedb/tests/wire/cases/credential_ddl.rs create mode 100644 nodedb/tests/wire/cases/document_bitemporal_audit_deletes.rs create mode 100644 nodedb/tests/wire/cases/graph_bitemporal_document_cut.rs create mode 100644 nodedb/tests/wire/cases/graph_bitemporal_node_identity.rs create mode 100644 nodedb/tests/wire/cases/graph_bitemporal_reads.rs create mode 100644 nodedb/tests/wire/cases/graph_bitemporal_valid_time.rs create mode 100644 nodedb/tests/wire/cases/oidc_catalog_provider_boot.rs create mode 100644 nodedb/tests/wire/cases/otlp_metrics_export.rs create mode 100644 nodedb/tests/wire/cases/secret_audit_text.rs create mode 100644 nodedb/tests/wire/cases/spatial_crs_open.rs create mode 100644 nodedb/tests/wire/cases/spatial_crs_units.rs create mode 100644 nodedb/tests/wire/cases/spatial_ops.rs create mode 100644 nodedb/tests/wire/cases/spatial_ref_sys.rs create mode 100644 nodedb/tests/wire/cases/spatial_ref_sys_raster.rs create mode 100644 nodedb/tests/wire/cases/spatial_zm.rs create mode 100644 nodedb/tests/wire/cases/sql_backup_restore_node_labels.rs create mode 100644 nodedb/tests/wire/cases/wal_key_vault.rs create mode 100644 scripts/gen_epsg.py diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml index d95fa3a06..457923770 100644 --- a/.github/workflows/fuzz.yml +++ b/.github/workflows/fuzz.yml @@ -12,6 +12,7 @@ on: - fuzz/** - nodedb-codec/** - nodedb-raster/** + - nodedb-raster-load/** - nodedb-columnar/** - nodedb-strict/** - nodedb-wal/** @@ -110,4 +111,5 @@ jobs: - name: Check decoders build for wasm32 run: | cargo check --target wasm32-unknown-unknown \ - -p nodedb-codec -p nodedb-columnar -p nodedb-strict + -p nodedb-codec -p nodedb-columnar -p nodedb-strict \ + -p nodedb-raster -p nodedb-raster-load diff --git a/Cargo.lock b/Cargo.lock index ed330df75..679625c4c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1049,6 +1049,18 @@ dependencies = [ "typenum", ] +[[package]] +name = "bitvec" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddcec3d12c579d40898fe0a9a358a803c23e9c52ca3c425707f81c9436211837" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "blake2" version = "0.11.0" @@ -1447,27 +1459,27 @@ dependencies = [ [[package]] name = "cranelift-assembler-x64" -version = "0.136.3" +version = "0.136.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6d504c8c7ba3f3accaf8e98b7096beb98e1a271404357744c7e9e730c6865ff" +checksum = "c9c587da2696fbaa58e7a2fa2e5bfdd365d0928219f36992304ac1f15e5bc254" dependencies = [ "cranelift-assembler-x64-meta", ] [[package]] name = "cranelift-assembler-x64-meta" -version = "0.136.3" +version = "0.136.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c065757905596bc06e82969ebd7b92d691b7fd710a7e9e8181ae0c30dab588d0" +checksum = "f28a60027b7556b680b051651291053ec381edf4ffbc01784997ae51a64269bb" dependencies = [ "cranelift-srcgen", ] [[package]] name = "cranelift-bforest" -version = "0.136.3" +version = "0.136.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca7162cd4d4fc5f9f2f88d1459d939882203d93a8c5a19ef2c405b2e82ef13f" +checksum = "62bfdb846acebfd591d601994ea283c98d525ac0bc9a84ca3f2c36bcf5b69ff1" dependencies = [ "cranelift-entity", "wasmtime-internal-core", @@ -1475,9 +1487,9 @@ dependencies = [ [[package]] name = "cranelift-bitset" -version = "0.136.3" +version = "0.136.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ec2ba2d4f3d959ec4023a10266c3dcfb28381d1938d507a9a5e1abb78bbcc88" +checksum = "aa9f8cc1080739d5f72b4ee7e00aff7d589f1284beaf0a19472c07878be7f299" dependencies = [ "serde", "serde_derive", @@ -1486,9 +1498,9 @@ dependencies = [ [[package]] name = "cranelift-codegen" -version = "0.136.3" +version = "0.136.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51a64cb821d5fed56bdd6053acd36fa928cd82b70bf6b19b8b14b8b27d047b9a" +checksum = "d85417d493ecec833b5134546b4a252c78cc0bd5be11729a35d25dcff9e4e70b" dependencies = [ "bumpalo", "cranelift-assembler-x64", @@ -1517,9 +1529,9 @@ dependencies = [ [[package]] name = "cranelift-codegen-meta" -version = "0.136.3" +version = "0.136.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "142be1e267a7497ba3c2b5a829eaf5387b92b9bcdba6e0a6020f1ffccae7f2b8" +checksum = "d01c8ed0098360bae69ad6b82952d4247516799ce8050d45ad168d6689f4eae6" dependencies = [ "cranelift-assembler-x64-meta", "cranelift-codegen-shared", @@ -1530,24 +1542,24 @@ dependencies = [ [[package]] name = "cranelift-codegen-shared" -version = "0.136.3" +version = "0.136.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e41f75410da6cce8601330137d73718f679db81cb790a33de3871b5c90a0de3" +checksum = "04f9b86d809cec9c4e88b201f1b3ed3b0347674c9ccfbb8c355228669e34be81" [[package]] name = "cranelift-control" -version = "0.136.3" +version = "0.136.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c2616c6d01c14921ac9289ef65eb7702d9c760dfdc4537a8ee5bf38ea2832c6" +checksum = "ce182038fb10b719841e65fdf5deeee258c684f63439f5be680c2be059274b58" dependencies = [ "arbitrary", ] [[package]] name = "cranelift-entity" -version = "0.136.3" +version = "0.136.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a03e8b44c4877cf2fb21e5788d514af6fd291a066ed9ae37adf114a25fecb4aa" +checksum = "7bca812b03a7b942796391f25bc02aa70a1a007fee3ce2756490c788429e9c39" dependencies = [ "cranelift-bitset", "serde", @@ -1557,9 +1569,9 @@ dependencies = [ [[package]] name = "cranelift-frontend" -version = "0.136.3" +version = "0.136.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "366ff7cb014bd08752210eba55a650969f8db9b52670d69d2ea0b23e1e9e3edf" +checksum = "a0c30563d2aef95e8d1a00a920199c99117b350339599f0e9487a7a1cf5d59de" dependencies = [ "cranelift-codegen", "hashbrown 0.17.1", @@ -1570,15 +1582,15 @@ dependencies = [ [[package]] name = "cranelift-isle" -version = "0.136.3" +version = "0.136.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ea6526ebf9064fff8ce3b1e26afad7027f9e6bb0e45d2b9200e4cbab8343345" +checksum = "9c54184333a275698edb1d3bdff226131a25d47133d792aa716e47e32b8b7f05" [[package]] name = "cranelift-native" -version = "0.136.3" +version = "0.136.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b44d323f3e5108a8a5bc61f3acd227285ece066e19afe9aece211d66b34a64ea" +checksum = "eb67a4c8a3180b5f1d77dffcd6317c05386ced42e5a31c7004523a9d99dc85c1" dependencies = [ "cranelift-codegen", "libc", @@ -1587,9 +1599,9 @@ dependencies = [ [[package]] name = "cranelift-srcgen" -version = "0.136.3" +version = "0.136.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc173168f291ff3b95b5d0769d6a05b1eaef5e6d74c2600affab8396b4e7b420" +checksum = "b533db1679b4b728568bd490da6533cf50974c48a4d7534e78f1e0e02bc39769" [[package]] name = "crash-context" @@ -2182,6 +2194,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28a80e3145d8ad11ba0995949bbcf48b9df2be62772b3d351ef017dff6ecb853" +[[package]] +name = "float_next_after" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37007738a80ea34f969af54a3390dd72cacdef654974cfd449c9f6f72dbaac10" + [[package]] name = "fluxbench" version = "0.2.0" @@ -2346,6 +2364,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futures" version = "0.3.34" @@ -2474,6 +2498,49 @@ dependencies = [ "rustc-hash", ] +[[package]] +name = "geo" +version = "0.33.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30eb1fdc57c1e5cfd11826fe0caec4b9dc7901f3758263bb506228d88c8d9e9a" +dependencies = [ + "float_next_after", + "geo-types", + "geographiclib-rs", + "i_overlay", + "log", + "num-traits", + "rand 0.10.3", + "rand_pcg", + "robust", + "rstar", + "sif-itree", + "spade", +] + +[[package]] +name = "geo-types" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "777d18aa0f12f8b285331cd867133ee14422b3f023f6d388034c47d43e28786a" +dependencies = [ + "approx", + "num-traits", + "rstar", + "serde", + "spade", + "thiserror 2.0.21", +] + +[[package]] +name = "geographiclib-rs" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5a7f08910fd98737a6eda7568e7c5e645093e073328eeef49758cfe8b0489c7" +dependencies = [ + "libm", +] + [[package]] name = "getrandom" version = "0.2.17" @@ -2675,6 +2742,11 @@ name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] [[package]] name = "hashbrown" @@ -2907,6 +2979,48 @@ dependencies = [ "tracing", ] +[[package]] +name = "i_float" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "813145bb0ad5b60f55cbbf3c74cdceda1c0a9d253b35c4cc36ae0df7887cb78f" +dependencies = [ + "libm", +] + +[[package]] +name = "i_key_sort" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d73d122b937fca067feb0ad74f62388920272b27c356d4df2d0cfdd59e044cf0" + +[[package]] +name = "i_overlay" +version = "4.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8dd314b4668e2b3a12508f2e125558c82a6c0a8636fa5107a900f79ce414e450" +dependencies = [ + "i_float", + "i_key_sort", + "i_shape", + "i_tree", +] + +[[package]] +name = "i_shape" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa9eac533d7509a8ab87672b60ac610c17240f9ea4851d26227689fdfe349c8" +dependencies = [ + "i_float", +] + +[[package]] +name = "i_tree" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4804bdc1dc124eb7e1aa9e144ecc04096bcf787a10a15fa44af682b51f0f6cce" + [[package]] name = "iana-time-zone" version = "0.1.65" @@ -3324,6 +3438,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" +[[package]] +name = "lerc-rs" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af29646ba82a905e2b503fabadb693be59364006155ac6e4c39db3c6863f1c56" +dependencies = [ + "bitvec", + "libm", + "thiserror 2.0.21", +] + [[package]] name = "lexical-core" version = "1.0.6" @@ -3968,6 +4093,7 @@ dependencies = [ "nodedb-codec", "nodedb-columnar", "nodedb-crdt", + "nodedb-crs", "nodedb-fts", "nodedb-graph", "nodedb-mem", @@ -3975,6 +4101,7 @@ dependencies = [ "nodedb-query", "nodedb-raft", "nodedb-raster", + "nodedb-raster-load", "nodedb-spatial", "nodedb-sql", "nodedb-strict", @@ -4044,6 +4171,7 @@ dependencies = [ "crc32c", "loro", "nodedb-codec", + "nodedb-crs", "nodedb-spatial", "nodedb-types", "nodedb-wal", @@ -4158,7 +4286,9 @@ dependencies = [ "nodedb-bridge", "nodedb-client", "nodedb-cluster", + "nodedb-columnar", "nodedb-crdt", + "nodedb-crs", "nodedb-physical", "nodedb-query", "nodedb-raft", @@ -4207,6 +4337,7 @@ dependencies = [ "faultbox", "nodedb-codec", "nodedb-mem", + "nodedb-spatial", "nodedb-types", "nodedb-wal", "roaring", @@ -4228,6 +4359,7 @@ dependencies = [ "hkdf", "hmac 0.12.1", "loro", + "nodedb-crs", "nodedb-query", "nodedb-types", "serde", @@ -4240,6 +4372,16 @@ dependencies = [ "zerompk", ] +[[package]] +name = "nodedb-crs" +version = "0.7.0-dev" +dependencies = [ + "nodedb-srid", + "serde", + "thiserror 2.0.21", + "zerompk", +] + [[package]] name = "nodedb-fts" version = "0.7.0-dev" @@ -4297,6 +4439,7 @@ name = "nodedb-physical" version = "0.7.0-dev" dependencies = [ "nodedb-array", + "nodedb-crs", "nodedb-graph", "nodedb-query", "nodedb-sql", @@ -4312,6 +4455,7 @@ name = "nodedb-query" version = "0.7.0-dev" dependencies = [ "hex", + "nodedb-crs", "nodedb-fts", "nodedb-spatial", "nodedb-types", @@ -4347,16 +4491,32 @@ dependencies = [ name = "nodedb-raster" version = "0.7.0-dev" dependencies = [ + "flate2", + "lerc-rs", + "nodedb-crs", "nodedb-types", + "ruzstd", "thiserror 2.0.21", "tiff", + "weezl", + "zune-jpeg", +] + +[[package]] +name = "nodedb-raster-load" +version = "0.7.0-dev" +dependencies = [ + "nodedb-raster", + "thiserror 2.0.21", ] [[package]] name = "nodedb-spatial" version = "0.7.0-dev" dependencies = [ + "geo", "h3o", + "nodedb-crs", "nodedb-mem", "nodedb-types", "nodedb-wal", @@ -4375,6 +4535,7 @@ version = "0.7.0-dev" dependencies = [ "chrono", "hex", + "nodedb-crs", "nodedb-query", "nodedb-spatial", "nodedb-types", @@ -4384,11 +4545,21 @@ dependencies = [ "thiserror 2.0.21", ] +[[package]] +name = "nodedb-srid" +version = "0.7.0-dev" +dependencies = [ + "serde", + "thiserror 2.0.21", + "zerompk", +] + [[package]] name = "nodedb-strict" version = "0.7.0-dev" dependencies = [ "arrow", + "nodedb-spatial", "nodedb-types", "rust_decimal", "serde_json", @@ -4414,6 +4585,7 @@ dependencies = [ "nodedb-client", "nodedb-cluster", "nodedb-crdt", + "nodedb-crs", "nodedb-mem", "nodedb-physical", "nodedb-types", @@ -4444,6 +4616,8 @@ dependencies = [ "hex", "nanoid", "nodedb-codec", + "nodedb-crs", + "nodedb-srid", "rand 0.10.3", "rkyv", "roaring", @@ -5334,7 +5508,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" dependencies = [ "anyhow", - "itertools 0.14.0", + "itertools 0.11.0", "proc-macro2", "quote", "syn 2.0.119", @@ -5371,9 +5545,9 @@ dependencies = [ [[package]] name = "pulley-interpreter" -version = "49.0.3" +version = "49.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "def59f28ccaf2cb4f8dfaac83197fd7d56a4470a70e4f2589454c731370101c5" +checksum = "5cd3de43f4f4cff536de8902097edbdf749321b97dda548f36a37918eb85bba3" dependencies = [ "cranelift-bitset", "log", @@ -5383,9 +5557,9 @@ dependencies = [ [[package]] name = "pulley-macros" -version = "49.0.3" +version = "49.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a1834d3b3ef994f266a0c9ed29a8214f2c2faaa9b1c44babfa57566bb05af83" +checksum = "8319a8fa5269b4895957f8795861c42822bd26218e495f70060a963323d94847" dependencies = [ "proc-macro2", "quote", @@ -5506,6 +5680,12 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rancor" version = "0.1.3" @@ -5939,6 +6119,12 @@ dependencies = [ "byteorder", ] +[[package]] +name = "robust" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e27ee8bb91ca0adcf0ecb116293afa12d393f9c2b9b9cd54d33e8078fe19839" + [[package]] name = "rsa" version = "0.9.10" @@ -5959,6 +6145,17 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rstar" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "421400d13ccfd26dfa5858199c30a5d76f9c54e0dba7575273025b43c5175dbb" +dependencies = [ + "heapless 0.8.0", + "num-traits", + "smallvec", +] + [[package]] name = "rust-stemmers" version = "1.2.0" @@ -6106,9 +6303,9 @@ checksum = "eec689c0bc40ff2458a5977b6619cb718087084a18e02a131c599b62d05e1a5f" [[package]] name = "rustls-webpki" -version = "0.103.15" +version = "0.103.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" +checksum = "4443b1b6d39d7c663139942cc4558262562d2c84123bb6282ef177009ca8b281" dependencies = [ "aws-lc-rs", "ring", @@ -6429,6 +6626,12 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" +[[package]] +name = "sif-itree" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7f45b8998ced5134fb1d75732c77842a3e888f19c1ff98481822e8fbfbf930b" + [[package]] name = "signal-hook-registry" version = "1.4.8" @@ -6590,6 +6793,18 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "spade" +version = "2.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9699399fd9349b00b184f5635b074f9ec93afffef30c853f8c875b32c0f8c7fa" +dependencies = [ + "hashbrown 0.16.1", + "num-traits", + "robust", + "smallvec", +] + [[package]] name = "spin" version = "0.9.9" @@ -6764,6 +6979,12 @@ dependencies = [ "syn 3.0.9", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "target-lexicon" version = "0.13.5" @@ -6777,7 +6998,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.3", + "getrandom 0.3.4", "once_cell", "rustix", "windows-sys 0.61.2", @@ -7632,9 +7853,9 @@ dependencies = [ [[package]] name = "wasmtime" -version = "49.0.3" +version = "49.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fbdd39f5c66f63ba6b5b31391b324b7ade7237d29c866cdb561330118ef436e" +checksum = "36abb25de5518707e5ba96b586366b09db168d2d4976cb9dac57265a317497ea" dependencies = [ "addr2line", "async-trait", @@ -7669,9 +7890,9 @@ dependencies = [ [[package]] name = "wasmtime-environ" -version = "49.0.3" +version = "49.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e915c27826de4f7b4411b61fd6db32ca448657a5136b8c8e8874a15e90cecbf4" +checksum = "92f16a4d09fb1fbf2dcfeddb9818e1517a258e13b505b3bfdadcf47a19bf31ca" dependencies = [ "anyhow", "cpp_demangle", @@ -7700,15 +7921,15 @@ dependencies = [ [[package]] name = "wasmtime-internal-component-util" -version = "49.0.3" +version = "49.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e754a3ff5690c34eb4713bd7c65f60dd607ead4e39042cbf52ac8011de14455b" +checksum = "4006009fc3598af4e895ab18c6c33d2c6b7c657b1bd0a755ca7ee1abef6f0c16" [[package]] name = "wasmtime-internal-core" -version = "49.0.3" +version = "49.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba00c5db77adb0749ee059068e5a417bfac3d8969f5a3479d9bd1ca8c1ba248d" +checksum = "947a732ad659f42c10ec639706fc8cb1f068dd9a55eebde8a1f24dfd06dfa525" dependencies = [ "hashbrown 0.17.1", "libm", @@ -7717,9 +7938,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-cranelift" -version = "49.0.3" +version = "49.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97ceeb4325712a253f463e50a69b5a547fbbecd52e45612360c543eaa25a03ff" +checksum = "fbfb8b455d524939e03df42e3e749f8e26e26ba2acd8b08fa00b3bda55686750" dependencies = [ "cranelift-codegen", "cranelift-control", @@ -7743,9 +7964,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-fiber" -version = "49.0.3" +version = "49.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eb3f21bc32b5382ea95f83ce9a5f2f62b68f1b25a37d0802a41a65236eae5ae" +checksum = "bd8aa8711f68b2cdd94c2b85ba5c8317edaa1ac4ee0a0534b957123680239307" dependencies = [ "cc", "libc", @@ -7757,9 +7978,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-jit-debug" -version = "49.0.3" +version = "49.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e786f9925126e0d2e466c5308e572e8b6226f4047596228a3d63d99528a78949" +checksum = "e873e4504faf50aa8ce255d6ea7210d87b39d5f379c2486782e2c5f179c70ca8" dependencies = [ "cc", "wasmtime-internal-versioned-export-macros", @@ -7767,9 +7988,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-jit-icache-coherence" -version = "49.0.3" +version = "49.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92cb1dd5da78f5b21d65e91063bdd8c2eb343c1f077a1dcbc29003d4a794abb0" +checksum = "197ab1fda5da958a5c0b8a97eb241beafc48ac568b82ef4f12f38d6866c74173" dependencies = [ "libc", "wasmtime-internal-core", @@ -7778,9 +7999,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-unwinder" -version = "49.0.3" +version = "49.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0243d244f6dd07ccd114e6bb099e8dd1197b47de5c054576adc9483f80f65d8" +checksum = "5577f3a5a979a7c3b1b23c1e9f13e70a8583acc8828471ec56f282117fc4952b" dependencies = [ "cranelift-codegen", "log", @@ -7790,9 +8011,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-versioned-export-macros" -version = "49.0.3" +version = "49.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20fa5616cd875ec78f5b92020fa47ced7c8bb923079185c8475751098b8c4a69" +checksum = "f6399c9c4050ed431cf66e21c05fadd6f9731db44476f452f8c7142b4597b883" dependencies = [ "proc-macro2", "quote", @@ -7821,9 +8042,9 @@ dependencies = [ [[package]] name = "webpki-root-certs" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" +checksum = "58ec80e73fd8baf57bf0d9615554c75348bda990d6d5bbeab964dd068319fecb" dependencies = [ "rustls-pki-types", ] @@ -8037,6 +8258,15 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "x509-certificate" version = "0.25.0" @@ -8280,3 +8510,18 @@ dependencies = [ "cc", "pkg-config", ] + +[[package]] +name = "zune-core" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56377fd46368984a170bc5aac5567e52ca5da874caa60bea39fcbca78fb658b" + +[[package]] +name = "zune-jpeg" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" +dependencies = [ + "zune-core", +] diff --git a/Cargo.toml b/Cargo.toml index 8613149b0..c81798e63 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,8 @@ members = [ "nodedb-raft", "nodedb-cluster", "nodedb-types", + "nodedb-srid", + "nodedb-crs", "nodedb-client", "nodedb-query", "nodedb-codec", @@ -20,6 +22,7 @@ members = [ "nodedb-columnar", "nodedb-array", "nodedb-raster", + "nodedb-raster-load", "nodedb-sql", "nodedb-physical", "nodedb-test-support", @@ -33,9 +36,9 @@ version = "0.7.0-dev" edition = "2024" rust-version = "1.96" license = "BUSL-1.1" +authors = ["Biz Impulse Solutions Sdn. Bhd. and the NodeDB contributors"] readme = "README.md" repository = "https://github.com/NodeDB-Lab/nodedb" -authors = ["Biz Impulse Solutions Sdn. Bhd. and the NodeDB contributors"] homepage = "https://nodedb.dev" documentation = "https://docs.rs/nodedb" @@ -43,6 +46,8 @@ documentation = "https://docs.rs/nodedb" # Foundation crates (internal) nodedb = { path = "nodedb", version = "0.7.0-dev" } nodedb-types = { path = "nodedb-types", version = "0.7.0-dev" } +nodedb-srid = { path = "nodedb-srid", version = "0.7.0-dev" } +nodedb-crs = { path = "nodedb-crs", version = "0.7.0-dev" } nodedb-client = { path = "nodedb-client", version = "0.7.0-dev" } nodedb-bridge = { path = "nodedb-bridge", version = "0.7.0-dev" } nodedb-wal = { path = "nodedb-wal", version = "0.7.0-dev" } @@ -61,6 +66,7 @@ nodedb-strict = { path = "nodedb-strict", version = "0.7.0-dev" } nodedb-columnar = { path = "nodedb-columnar", version = "0.7.0-dev" } nodedb-array = { path = "nodedb-array", version = "0.7.0-dev" } nodedb-raster = { path = "nodedb-raster", version = "0.7.0-dev" } +nodedb-raster-load = { path = "nodedb-raster-load", version = "0.7.0-dev" } nodedb-sql = { path = "nodedb-sql", version = "0.7.0-dev" } nodedb-physical = { path = "nodedb-physical", version = "0.7.0-dev" } nodedb-test-support = { path = "nodedb-test-support" } @@ -97,10 +103,14 @@ libc = "0.2" # Checksums crc32c = "0.6" -# GeoTIFF decoding with the deflate and LZW codecs only. -# The raster reader refuses JPEG, fax and WebP compression. +# Writes GeoTIFF test fixtures only. nodedb-raster decodes TIFF itself. tiff = { version = "0.11.3", default-features = false, features = ["deflate", "lzw"] } +# Pure-Rust raster codecs of nodedb-raster (wasm32-safe, no C). +weezl = "0.1.12" +zune-jpeg = "0.5.15" +lerc-rs = { version = "0.7.0", default-features = false, features = ["std"] } + # Crypto # Pinned to the digest 0.10 generation: `rsa` (JWT RS256) has no release on # digest 0.11, so sha2 0.11 / hmac 0.13 / aes-gcm 0.11 / p256 0.14 cannot be @@ -264,6 +274,10 @@ nexar = "0.1" # H3 hexagonal spatial index h3o = "0.11" +# Planar geometry algorithms (buffer, boolean ops, DE-9IM, hulls, repair). +# No `multithreading` (rayon) or `earcut`: the spatial crate builds for wasm32. +geo = { version = "0.33", default-features = false, features = ["spade"] } + # Linear algebra (SVD, matrix ops for OPQ Procrustes rotation) nalgebra = "0.35" diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index bd6e25a09..bcc8c5f6d 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -17,6 +17,7 @@ tempfile = "3" nodedb-codec = { path = "../nodedb-codec" } nodedb-columnar = { path = "../nodedb-columnar" } nodedb-mem = { path = "../nodedb-mem" } +nodedb-crs = { path = "../nodedb-crs" } nodedb-raster = { path = "../nodedb-raster" } nodedb-strict = { path = "../nodedb-strict" } nodedb-types = { path = "../nodedb-types" } diff --git a/fuzz/corpus/geotiff_decoder/cog-mask.tif b/fuzz/corpus/geotiff_decoder/cog-mask.tif new file mode 100644 index 0000000000000000000000000000000000000000..3214fe3c0e18aeaf6c819ef3beec143598eb28e7 GIT binary patch literal 3324 zcmcgteNYo;9)33j2m!)J>eorl8bHpXk}5qoCCM&iOIHGsBuJ^FD+to!0dtjVsT|n> z6}@&!IxWrFYny)2R=pXuJ)-)G;CXPin)hvc1|EW)x}m@Nts}?TR6MXZmSl`*|j!%bk@qTWSWQx ztV5{cE1db!C8wR`t*~Zg9Zn%%D6VmG4#Cc?<5(wWQI}QNsw)J(qQq9dj&<_IR!-n7 zypy+87%OaQx}0^C!K#z9!xPqX7NMGVJ|ghtY$<22G1hPnHA+W_ia`{-BLkqK+yGn= z<*C49qFe<$F3QQ7In#K?G;WM?1?(w~a-vf*jaw)sN`kqrC%L<4UFeym4<76Jb)aPb`l+6H_G4*v=6i%8+h8`^5?Yujo~ zYML(M+ZvkbmfGe9qu#cjv#;m5YKYUpmant&6{UK*zO}jbi58*KCRkZ0@vc~LO7#<0y!qtdJNEzV}Q+Q6HJk~RptgDXo z#G|<=4vImc=QBxg6x!!fhR3Cu)uj%)FrUt&tDNQZGp#KRbbet)M`X4GX;27+As!+R z2fw@aWZz7+Cui?fdvf+(wHK$={^(t`7oqUC-4eAaw=|(q=ktMo-<>Lg=2WK=PCAu5 z{{R$)i}J7GetM^_fJkO`OZtAwE$Q+9m)o=9Y$MKJxy$YOJKV;?>bDm4g4BpOmy4%^Z{h{5qOzVWqyWGMpIu386NkFIxa0-yAMyL}o1}I2G z=q11;U@bI6Kfs7g@HKzy`ybG8!ug|sWH{Rd_&q=mXFCC7fSY$$p@6LH}CkTj_m8;{{ziTU{Ugbq0(X=J5@eT)&oG4=uxH;e~Vk z0iP}8o8O0V^E*2Z$+{Qu@Tp6f54cDivwhwW;>XgLj}wp-pMLcw_ENEO&gYFeL)a#S zemp<+?alW+*HvjyKN64RJ#&HtdvY}xCP!PTz zzhd8`Lj+5ssbK7i%ev|X<-N}w3LLyL$ez;OiYAWyqw9EdF@Z6BU`zex(}{Cb(wNrW zn@J00Os9ql{$1&BO><+O+0>Qh8~7;|G$;c7O80W?y$7CJL@ z@e17pfhjzfON4V$-GYq?Wl+HrN`L$Of1TN;bzj%hUY;537Ud@ufzv9NKlKvp3s0|aI(HwdboO{HM6e>8P^xM+hE}nV4dx39YHPv3A2pm-U zH>jk%CA4cFh<7d1*a&9IJ0e|HUkAPys-)o@+Se<}8x;YUN-A6RwdaJM)+LDO213%_SAiv*UB@dF0lhz@Cl{=M7rBo%!F1^?TgpT0rXyD z$`*o%@egrp%HEF7w_0WUa!m`t?13lN*Ur=^HPGa< zG|Jvcqwph|rwC@3UL&0Z*Cv|AUU48Jx3hhNBJiNne>g!J*3-ThMLAg!7*CL%JvQMf z$fkA0%;2v?ISXp(`zmD@!R*qj>Knjyj7qw!bx&r}zL!OLfg*4uLD~*tqgnJM%a~ph eI~Gzw_`&z4x?NW0_5JtziqOY}nr?!br2hwj1U_Z} literal 0 HcmV?d00001 diff --git a/fuzz/corpus/geotiff_decoder/gdal-metadata.tif b/fuzz/corpus/geotiff_decoder/gdal-metadata.tif new file mode 100644 index 0000000000000000000000000000000000000000..586dcef0f7db06d22551975f7e24aec65fc822d6 GIT binary patch literal 1369 zcmaiyZ%9*77{=dSV`NY)qy!1MUJxXj+g)W1w&8Gd3~kyrAwjUqcGZQObGxe~=pPjn zeGvK(^dSfeB7&koC_F{9$?iRz+K2peYc8Lc>rZ)nE_{6HtJ;TKw9UsW;Y^H>9CZ-BD_&bE>Zx3CU3?qCzWxCep-5f0+)9^z~-adv>R_yiZ7!B@EHJieosuHq*`bQ8Z3 zAw2R!Wp)%ArFny~F0wQ~u=_ll|4Y^r3it<)>4uin z49&MgmOWuZPsUotiI-2l{Hc`8O6swIjiS1qma|ZwIV|Sf81X!qbfc-|7-DXAz3XqZwq<0piQTYIdPrS z>|Js^r)4JfD%!>RbwG7GonlXYX09@}oS9wyhqErEim?*XVKMRVj4IY@MrpBE*r|$r TOQ)vv)Z~;=F5_`*J-GA-R41r- literal 0 HcmV?d00001 diff --git a/fuzz/corpus/geotiff_decoder/j-gray-strip.tif b/fuzz/corpus/geotiff_decoder/j-gray-strip.tif new file mode 100644 index 0000000000000000000000000000000000000000..63234b5e28e4bf8cb0a3cc86b7897056fec7276b GIT binary patch literal 689 zcmebD)MDUZU|`^3U|?isU<9%>fS3`=mH@JuplpyTW+5lq0rb2EBcmY0|04|IApe1_g%M0F9PB^|X#oaC7Le27fQ^BX ziCIL+@c%6a4o09JdxkSkXEctrojr1~uvKEy%^k~=mAQ=d_{_% zyXWN8o}Bi!*UYKY%gQ8jvu4bv>5rF6T4-H+98^$l!n1elA*H{&7cCG_^1CI<858g= z-ikQSFhT-RN`Qfh85CAq@(Y#`Mgc z-0#kvIrE+Kof*IX3D5z+JODz0AXGEFreVF`t(j$J|$D z99s})O~cuQ0bh3+dk&6s9L8RNnESYey@<&0F}_{sNEIjuhy`q#Ajan3!EbK5SZjty z$#*aYe?cQO;)+t7z8hV67{DVUKniX_VvVoi+@kUKp8xrd4n?m*7EnRp4;sTq)QKWv zVv;ONI%AU2Xh<>`OlH+$GAm|-!D6>4R?TL!8RsRZ*tHZ@vuWH3p}2;LdQsGCCWA?v zvqguIO$ZbnSinMw3j~Ewg+zY@J8H(GnlhP6IKB{5FVTcw0+n2D!ZeQ;|J!CR0}yd| zfdUxplruz(idM}Xy#hu`a9^rG2K;;=_wBqx#?Rz|PxZ=`6OL~#d3LoG2AdasUm#u2 zdDc{#F+1aAUQP)+h?15|==H~*o;DJQ;20V0SL75~qO zfXo%u4U*I9@&?{bjNqr`qdyc6wimv+>B!iGexSB$@{e63%g>I!b>4qz9ehxI!?Lus z|BF&r%gBrR$lj3A$_<62e-}$phJ-A~?`nG1+ zCl{V+yKAZtUpqG!%Kyr_gj1%>aGx-qJ}M|GD(aLqwe$3vMjUcwHnMZ%_) z15W`^#VNT}Zy=HWh4Yb3<+=S$TKjL6XCoU=uDZ8zknQ;L*yCzATz;{yzO};~?hkf$ zjxF1>-XH8v+j3-JJmZs2==AlN&EARY*T#>j)$gw@D=(gVpjfwr5;>vtD3d^x71im| z*)y{P7>$;27I{(_jye^*oG6_pP^{)bajMq*l9@%@^ZU8Wqc``Zt?15bd3|N!i3Mr{ zTU}7MeQ9HJ(KcUi-DdwUtGk}x-<1TLx30aGnp?ZG>h@P@&s6WYqHF zbV8g54%Jh>jt#d?&{{akC BT-g8s literal 0 HcmV?d00001 diff --git a/fuzz/corpus/geotiff_decoder/l-lerc-pixel-tile.tif b/fuzz/corpus/geotiff_decoder/l-lerc-pixel-tile.tif new file mode 100644 index 0000000000000000000000000000000000000000..90be602f48ad1b6d2de00c80109548650f9c3307 GIT binary patch literal 1955 zcmebD)MDUZU|`^9U|?isU<9&6fS3`=76P)FfNUlpEdYefP;vJT1_m}L8>B}R$_A;E zK@xX@vIT(R&PZ$*MivGZpx7xOb_I%q*oS~P2&x9iWC#YbLFO?6VZ#z8h9V&Q5fFbm z$^sU@1!RN3CZ@@Z;f$LZLVylo0~$1wm4V>{8w1!(ki3n%i=&TkYDr>BVo9PMmx6+g zXGv+p1$RIl|8+}w87<^KTl8qEVe)^%_KLzA>5C-`L6u_X+ z-UQ?@gZNfY1+G96)?Vh9m}Sra4Ruj*OiS415+V4kWg8Nyz(bS+LRBLrBA} z%V6drRi0Vqu_x9{oXmbf(Y=OEv#aIhn?QCQ;8US4%3IHH&&?IsT-cS&UIs4188YY8p0)iiROl?16hSE3l>5$-tD0nPMU7 zmf=7vIK_f^;Y>S#DVCwrq42{WLr@lAn4njw1o_F4w(l_r^kc13>~&1j6Mq)ddrM@ddxgX4OGxK~p4a`d2WVw7EnAM`EmAMSeTD(e1dmWg?#)gQJX*I{2 zu|-KZz9_lS<^VPbNK`W20!7Ix2f>dIzt3;vb@F)FDqW?K)TwiBMe+nko*=&{HqC2y znqlg((_jOF(m0>Z%sUpTKCkAV<%)&9LAzG2Al3j^kP9>qRV?mzRXX`(a&Xkj)afbf zG}GsiWI&f&*URP0mfg}?v*q5d)$5NMS+CtjtN~0+$ie!1n)6g_!MYjff1D|OL%Rbk zc&0Lag9IyQ;-a6+<~GQ^%2=xC%)pQeiF24T6dDwWoQKr7-)?=l==Zw4gp{+fft(Bg D0%G+6 literal 0 HcmV?d00001 diff --git a/fuzz/corpus/geotiff_decoder/l-lerc_deflate-band-strip.tif b/fuzz/corpus/geotiff_decoder/l-lerc_deflate-band-strip.tif new file mode 100644 index 0000000000000000000000000000000000000000..db0d3b9d3dae151b3253d1df0eb8f4bc2241952f GIT binary patch literal 1305 zcmebD)MDUZU|`^5U|?isU<9&6fS3`=76P)FfNUlpEdYefP;vJT1_m}L8>B~&kp*nZ zF(6wMDh|>k24%AX+2TMp$fj*TEQ6#b2+9TuGXw+KAbpHL*sz3&p$N!+2gIL_vVg^( z0ofo>#Zbdg%aFxT4W=^~5`d1l1;k-M&6k1LmYIQJ2N0XEFfeQcVju$h&Boou(Z@Hn zBrzqiB+-sbLBYneBsEtdFEKaOR>|4lHz3G0IM~zQFFwdM%rz+3)5+7vGbB<;AtgUI zF*DCr$~m&>Ux<4B_t*! zB-~xGwN2Eco54_7U^#;!yTE(4q%<~(iUj_J_6`}XOzZ+jPaHUM;=zd<3lAPRr>?bl zO{&KFW!Xt@pGYP=eJZeF+-e)*1R;jurd*|EBv;jbfaC^7|c2eAh+ zPg^ovHwX^Y~Q1WgU=g*IvCuvW9(!6^+=W5+M%R}~bm4B=a(^Hc%n&ap5*2ZC` ze_OQiH%-xEMh2!Csuz%LnsM-u>ItqTt_?E;JAxZ#Fn5$Q8yYk7fNbIwof)kP^I89KR&;*Ki2KDp$naFl;SSvaune~#A!x@evnhs|ekNj?!F_V!^?nd*< zHZOLA+C{8B=M(0fJaWLId82c4<6Qp`x5(20alY4&+&XD|;M`Sp3oS!sdo$JDD>g0M zyB-*ak9vNdzP2e&!O>8iJ-D62!_Y&~L(s#~xA?jXgVaXdFciz<6GTt(K*Bs16z0Dh zB~)XkF;@Za#DgxTGsT8jJO()qSQ$>1>emQx3>PC(iQ$uZ{PV@ cz20rbhQZeS0na8syw+3ebu@+{J4lri081OtSO5S3 literal 0 HcmV?d00001 diff --git a/fuzz/corpus/geotiff_decoder/l-lerc_zstd-mask.tif b/fuzz/corpus/geotiff_decoder/l-lerc_zstd-mask.tif new file mode 100644 index 0000000000000000000000000000000000000000..70fa08f5ad675f52e308083f74ae88cd8527f0a6 GIT binary patch literal 1440 zcmY+Cdo6SZE==uIy+j0LFh6z zO|F&77^*|)H;zk4ing{~nn_`*9jd{`XwRPZr{jqbap23GMIVjn?-R%t zrU+sMDFVotOs26?go)%NL86dO@e5{$a2Q-JD>x{c!w6$=xGY~*AS-k?g&dolC`d@6 zQ&?;tCL@{~%HjEi@;D3%6d26q1|mH)x5Yt_G0pA&8vwPz?ccqtF&c9a_qlM(qtZ3C zDAR3KvWa4=UA)M!ZR5t7FFjt@*LE_1`O8jNFN8Yy*ms4@=t`ya9Y3V0Q!{!t}SRjL|Q4^$H>PL(!~ld|(rRLixd>81_M zp6WK$kZMY$S7Frzwe`hca(}Wp9Md9fE^2OXu1NYdDKjZMDL)A3<@wU`IM)R2wf&%R z$J7&U@gg(CsvSVEVE?P(*=<_W$IGd?EYjQ+3^j<5ANU z`e%k?<2s?{~ArJDpw~(5=-Qi&%9`H^%5OhgL$_$$<{T zS1~#Vy@3eD&*x|JGx@*rufEArLZfzbe~Z$G?#wrF zO=$mv(9K~ZsKIvWEzFl+b_6gC!Db83E$+#uJq)t(>!f#N*PW-j)$=aF&e|Tsr(^3c z5^qaC#F7iX&pIq!^Wk9iyF;pTW+@Oe!}NWFr895@6cDcsl&tRkV=68pM4XjnOM|sSTx!uczkQMmD38Rhu7}n@3K1aMx>fVst z3WA^1#meqme2tC@<5SJ;GFuD>+M{%qFs81}Mu&#)SHXpbGo^#m=;wbXypVX4h+W%u z#g8#kDOz8cm}+O2b-Ti~voj>Vf-vjqX#lR8W@t*9-@6QoC-?RZqZVIG9?8do$GK!j z@qmZHrjM>+E2QF8ZzFnI7G$v~a#_i0CI}sF+Y>E5h{F#Ap|o%9yBtj_9<#m-ru;ke!|2*%SEOl_-EN$Qc&dZ1( z!?49ww3ioR6bTVz>$Es-VO#lLQ!Kc3FQVCYPgGPt;mc*|@6dpQvv4R}mtf*TDw@1s z^cjii^NbjFXN{xO=-tQ2(G5`J2q#zT{()0ms^Mv=7Z|ChBjX(tv?AbXZaV%=n!oNf b(dUMjZHT)UndpxLHy3v(I*Ub(vQ_^9W_U%3 literal 0 HcmV?d00001 diff --git a/fuzz/corpus/geotiff_decoder/m-deflate-p3-tile-classic-contig-be.tif b/fuzz/corpus/geotiff_decoder/m-deflate-p3-tile-classic-contig-be.tif new file mode 100644 index 0000000000000000000000000000000000000000..7308fd24534b2205b8fb8f9b3bf0fd443ca82c19 GIT binary patch literal 1217 zcmebEWzb?^VBla7Vq{=o0kVJ;g9sxdoGrx2#J~&`V`5NXP+(*Rvl)Tv85r51Y@iNC zQ7D@U$d+PYg_`q>Q5w#E$0&oOUIU432sE34fd`1cG1@}Kfi^Na!QCLh=#0d60keT_ zWn^M>1*-?Kg&2dNdO?;4gV`XLgWL`RAb-JeNoHDFT4qkFUO^>DSOMt0cR=q;191`K zIYwQEa0Y*%1Q1lrSvqmQ_Y((^sfSV? z7~h{>#jA8EKxYGA{Gzoo2hM!RR(SQZ>F1hLT4$1{*vqEb|Ka8OJ$-@eUGAga99v!r zhwWs2AXB*d{I;1#dqd5R=cL^iZ3?^l=gY=D98n*BC`ZOwcJvxwdr*1$Ox+uPrT4Du z`$N|(Xcu`XpAlHR#GGa4vE~P-Up(Bax~A7ozV?nAw`|^gXX&@^S%vmAh?#~jE1m54 zWYYWRM^EqRKW?+KmG_@caejSyW5+91?duDU&eXnKci~vinyNdyFZgtarMy*^2>-~k zrQnWFt_$BbzW?7(Z237QJjA^6HlyyHpx<%7oa)!rZ;zaKq~z|pMzPHLYYJNWomg8t(coce9$Wdere1i>8!( zvhPig-0gGOAZqWMRLkIoS$_ZIFK2GvqGoIVVP)hC)6e{R0-v*Io4!1`S10=yQ5aS8ORjLKQPGJk(_6?Z)dCR zz4xu+aX-$k_nF9WK%$UeWo>h!Nzc8ne(XA5gtN^xZ0aW8eKE1_%5vTf0{*|77fiqW z{hPuy-@Q96ez9|VzS?&FeoSY`l51D4nn$eobU!~kb@^8=u{rh*?>^ScM*iL;W#7=c z<<0k>)AgpNCMFngH{UGR5B;|`TXkx{ri|ueQ-UXJ{eL)jtM+FPf2@0*TISs)ks@6F|}^VeK1fA{aX{Pq3&xul-` zdvyJ)xk~N)-Sy|+-ub-0&Y{If~sbJwVYjZI@{uC12b(8MmI<8#JGr)Sapji=2`Ok^@FE)+Bz rY;4qd;Bw#}KNCpmj8D#b$qmfx9e;Uwc@`&#z)VySVqh?16Ji7ajF8Ly literal 0 HcmV?d00001 diff --git a/fuzz/corpus/geotiff_decoder/m-lzw-p2-tile-big-separate-le.tif b/fuzz/corpus/geotiff_decoder/m-lzw-p2-tile-big-separate-le.tif new file mode 100644 index 0000000000000000000000000000000000000000..1b80ef0d4a4ede563f346674aa64d8d7f568d3da GIT binary patch literal 1140 zcmebD)MnsdU|}b9qN$vQ2Hg5 zR$zwctA^6dED*jeln!U;V6b8^XNZQ<4Gazp6*6L+?9EPz0vi~bMEToX5|w|ngsJlA zyDknaVEkq--=?l2WTEuVQ#sf3@r3p>tL!8gPCY$b*|W>j8MvY_f}=qOWUR=S{|ruv0XtGm7$!6wRA}AdVx`L)ek5V>i8XOH4C!tQ8gIO* z(|J3QLH_(plL`sOPRHm^Cvzgg4SEgKCP!py6&ePddp=>a&p)oFg#iyWx=aL)doI;{ zxqwO4sqNUx&aBJFb$xiFXEibeODNC%?bXKdrf`|-yo+3lQO~qym+oO?WN5H)P$*y# z;$>}dd>Akz!GV>#!&%9ofF(>_JWLZN_JW{RtOQKd@FR-Gwl5|+cR4tCHo|LW;$|zj@+UdZpk2^KKTN(R)gZg^RXrjqG~xDjBXG5%-;%0sjoOGF*Rz7 zPH$xI%Iv8(Jf|IGb&hz`;cX*uK5N~Iw+onD53L2dL!L6@r{N87o)htO6=P zuJbY3y}0OtxZwHz+3#mj`2W14Pm!E)61n_*KD*B*u25pM*>i2Dcuu^B5*LhF;et3l z=9ltCJwTiP^KtWE$sWcdgP$TL&S7NsrK6F|evni9^vKN2?9sDZTCaYQ&MD~b`1&47 zQZE(J<0`2^Dy!#INiV62UQ;c-t@@g%TlBtKXtr+EN9wFkbdNsM{rXamXp^4McX~=c zYN&oyik?<^J+CyqqUw4>bu>;5HAT%dL#;JW9kp0@XoWKMxgOM4>Z5P;q`ucP`bjV7 zfJW+fy{W(TuKv+`vBvc@L5(#{8JeYbTA)r^8se29UK`?#A>J0^T_OG@#D_!tXNXU0 zV(i=dT&&p?&_}WMpKzr<<7#!)a&^;c_0)Rx))w{Gju7t&@%|7W4e^N(pVJf)HG_Pb z$7Na^`?Z3r^f?vu71!z;uGjb6sGqo51GQ5_v{%D*NTYO2V{}Rr;`~e_k7kji1#t$J zQc^1^tF=_pMyhBVwX}=+`h{C`m=^kzTXmYwn0qu=Skxgs`CbQ7)L{<(2NJ? zlb?qv%wv>bAY~XrD#J-<6g3$`JtokYX=E^qb}R_-(h#o<@!Ak?4Dq%Q@1iLED8*pP zF^tNLpc=1Jo3S)tGEJFIE9TOHMIl}u;?*HuAL1<`-Vx$GA>JS2qai*K;&asCE$T9! zMogtSGil5G5HAUFR*2VxcteP{hInU)_lEdTh>wN%6cY^7oQGLXVu6dW)Fo-pLb|Yw zds)Q;tm9EOhj@F4cZYaih>wK$c!yrhrWEv z0DjkraCt>ouBzG%o3L%%Vk*OR5m!Bt**&V*JH06bI2JSb30DC z6QlWycR0f&=i&oj!W zJj|6n!qxn`YkRC4c(R*%x?6d!J9rVlyA6N)HvaLQzR36aQa|KE?(5G z^Q*4mw_Mlb-N;kj+%w(Q^FzEO#91L;6XFda-s%E=#FzUCU+JfOwTJo|zwD|W?Hl}# nZ}KEJ@ds|{Id1QTAzl{ZRUuv%;>{u69^&00-WTE{AwK>uE+crw literal 0 HcmV?d00001 diff --git a/fuzz/corpus/geotiff_decoder/m-packbits-p1-tile-classic-separate-le.tif b/fuzz/corpus/geotiff_decoder/m-packbits-p1-tile-classic-separate-le.tif new file mode 100644 index 0000000000000000000000000000000000000000..8bdd181e851147be12ffa716c3ee8330199dd38d GIT binary patch literal 1979 zcmaKtd6bT27{;&bnlUpWCfT=V${LvvSwk2a2ieIovdm;MvhVx8jL4d0WQ~wDLWGDS zWXm2#$tZ=Ctt`(oeeXH*$IJJf`@FyRd+ztWpYzH`Ar&9I~7zz$Yx{O_NhJ&9kK_WVGw&ZB!4`wYx!$ zG+fiN+vRUIqU^Jd)7!$00`IZ6sUNqOw9zX*E-o%UAvUsGe?-`Yl0m>Z8x@0r1RH}w zfa0ORcpKsR3^6EeuWM4T|G&}D7R!w6$cKU`ZkH>d8fv3HnxZv2;60cK6hS1)qB3fr z4jQ5b+9D2J&CK2 ziFsIpl~|80*okjp24fTwF%`410L!o%8?g<$u^&fp3K#GzOcEAj1=e9Rc3>|K;yBLY z60YI~?%*NP(QL&o?89N4w8wlI*KiB>ZIi>8nc10-1!>M887a7q+jxLrW?&ZPVg!pY zl4V($HE8bHlMQ7iW@BC!U@?|rc~)gD)?*X4VtbmLG%U;#EW=8y&S*AZbH*^13GB%M z98Ob?RT#y(Y|NHy$4>0ZJ{-(ZOypF~<^q~#Y{QQ1%pUB|VI0dzoWUe6<_fOkX6~Tr z%|RT=37o=NoX@3P#SPrbUEIgRJjwGk(>a%mxSVUbiQBn{2Y8HUn9LMj=WRZq`J7+! zYktQcc!EFi7yiz_c#lx1GAWz#${gisUgQ=2$$$7CgOpy+DW{AItAxs^l0v$pABJK~ z3PL`_NB9_@SXzTG@D;wXbO=A<9DYtg=pXnS|JvcXiqMRFp1Jvg!{RK>3asX^4jZxs z+d7PAH}>TahvPVzGda)UGOp%EZgaSo2YH-l9bV=&-r{|S!OEa4$|Z{hR7|B*-eGk` ztAUzZG&gydj~Jq7l~sB4qFz!-l~WZ(sjeEUrA)YTD8CBnWxcA3dP8sOEj3dcbyR2d zP=6&`E1vWXZKqD^sy?pfFpbqD&9L~SZ1=nI_djrY_#VdJ|G=Tz={NrVduRO&jlch1 z2a3}8`|tIoHjTgkUbo_C{QdWOIGV=af3LGkX#D;6`n{XR-+#}7OEmuedu~w1-+#}T z2pNC>J&&r&`1|kK6(i&ClxM|Y8GoldSCSNR6sK_!SM0Rh#9chHe<{|baOPls7IOFs zU*qen>99VVvNbz6?806g$Po@FavJAwp~ID2&n?{Pa6gam6fZcu${W1HhYrJ(S=p7( z=~_gQDyzy4YpI@^sFlN5C8(zcI2@%!P1S6Ni?u@QwArF*q__2s-qri+tw9>837Vo= zny;l=r48CD(_Q^ERAcmkrfaSiX}Q*FleTM*4(OQ9Jbj#YX`c@3q^p^%6kXSCi%%P; E$FG*scmMzZ literal 0 HcmV?d00001 diff --git a/fuzz/corpus/geotiff_decoder/m-zstd-p2-strip-big-contig-be.tif b/fuzz/corpus/geotiff_decoder/m-zstd-p2-strip-big-contig-be.tif new file mode 100644 index 0000000000000000000000000000000000000000..5b61fb5d93460664d94b663e343226b58bb02b01 GIT binary patch literal 601 zcmZvXy-Pw-7{;IH_;G|pZekhb;7}w4g?$ha5)F-l2t@>i1PMk%MGg+aZS4ajA{s*c z1Kc7C8WQRch=zuSXl|;(!9nQU&lG$w=e`fW^E~IgC!NLw0&q&Rg+{<@Yb6meG}*<% zCKK=&U6p{QK2`L%jL#W^DCYlXFitdBzvWlbzKb~ybohj60|%XbGwCz4QP=0iEatF) zaSp@WGmdr^M|!ZBo25HTq6bT=H(3?58g-^Re9nB?4rT|s8!LAH#qKSui|M06KA$h_ z?M&{MOq95rYUe6gB_7mh+mLY_FQF!}g(rzJKBXUl%(=dMCPMW_RSyM=D|Tj+Xy68I z$VbLLxG2%$hU5@SP$AhnR3^SS&ud7o@t#G#D|N}H`rnF(8GJp|uTDj{UaRUCeb#O9 epC+L5P0T3YugD5b96)*z^^LACr#Ke u8 { AttrType::Int32 => 8, AttrType::UInt32 => 9, AttrType::Float32 => 10, + AttrType::Int8 => 11, } } @@ -51,6 +52,7 @@ fn type_of(tag: u8) -> ArrayResult> { 8 => AttrType::Int32, 9 => AttrType::UInt32, 10 => AttrType::Float32, + 11 => AttrType::Int8, other => { return Err(ArrayError::SegmentCorruption { detail: format!("attr col: unknown tag {other:#04x}"), @@ -93,6 +95,7 @@ pub fn encode_attr_col(values: &[CellValue]) -> ArrayResult> { let present: Vec<&CellValue> = values.iter().filter(|v| !v.is_null()).collect(); let payload = match dtype { AttrType::Int64 + | AttrType::Int8 | AttrType::UInt8 | AttrType::Int16 | AttrType::UInt16 @@ -145,6 +148,7 @@ pub fn decode_attr_col(data: &[u8], rows: usize) -> ArrayResult> let body = &data[pos..]; let values = match dtype { AttrType::Int64 + | AttrType::Int8 | AttrType::UInt8 | AttrType::Int16 | AttrType::UInt16 @@ -198,6 +202,7 @@ mod tests { .map(|i| CellValue::Bytes(vec![i as u8; 3])) .collect(), (0..40).map(|i| CellValue::UInt8(i as u8 * 6)).collect(), + (0..40).map(|i| CellValue::Int8(i as i8 * -3)).collect(), (0..40).map(|i| CellValue::Int16(i as i16 * -800)).collect(), (0..40) .map(|i| CellValue::UInt16(i as u16 * 1600)) @@ -298,6 +303,31 @@ mod tests { )); } + /// INT8 keeps its full signed range through the codec, and a stored + /// value past it is corruption. + #[test] + fn int8_round_trips_its_full_range() { + let col = vec![ + CellValue::Int8(i8::MIN), + CellValue::Null, + CellValue::Int8(-1), + CellValue::Int8(0), + CellValue::Int8(i8::MAX), + ]; + let encoded = encode_attr_col(&col).unwrap(); + assert_eq!(encoded[0], tag_of(AttrType::Int8)); + assert_eq!(round_trip(&col), col); + + let mut data = vec![tag_of(AttrType::Int8)]; + data.extend_from_slice(&1u32.to_le_bytes()); + data.push(0); + data.extend_from_slice(&nodedb_codec::fastlanes::encode(&[-129]).unwrap()); + assert!(matches!( + decode_attr_col(&data, 1), + Err(ArrayError::SegmentCorruption { .. }) + )); + } + #[test] fn an_unknown_tag_is_corruption() { assert!(decode_attr_col(&[0xEE, 0, 0, 0, 0], 0).is_err()); diff --git a/nodedb-array/src/codec/attr_col/varlen.rs b/nodedb-array/src/codec/attr_col/varlen.rs index 879f20cf5..19c82f1fd 100644 --- a/nodedb-array/src/codec/attr_col/varlen.rs +++ b/nodedb-array/src/codec/attr_col/varlen.rs @@ -65,6 +65,7 @@ pub(super) fn decode_varlen( AttrType::Bytes => CellValue::Bytes(bytes.to_vec()), AttrType::Int64 | AttrType::Float64 + | AttrType::Int8 | AttrType::UInt8 | AttrType::Int16 | AttrType::UInt16 diff --git a/nodedb-array/src/error.rs b/nodedb-array/src/error.rs index de157476b..190029d60 100644 --- a/nodedb-array/src/error.rs +++ b/nodedb-array/src/error.rs @@ -41,6 +41,23 @@ pub enum ArrayError { #[error("tile-extent vector rejected on '{array}': {detail}")] InvalidTileExtents { array: String, detail: String }, + /// An attr's nodata, scale, offset or units does not fit its type. + #[error("attribute '{attr}' metadata rejected on '{array}': {detail}")] + InvalidAttrMeta { + array: String, + attr: String, + detail: String, + }, + + /// The schema's content fields failed to encode. + #[error("schema of '{array}' failed to encode: {detail}")] + SchemaEncode { array: String, detail: String }, + + /// An exact aggregate holds more distinct values than its memory budget + /// allows. SQLSTATE `54000`. + #[error("aggregate exceeds its memory budget: {detail}")] + AggregateBudgetExceeded { detail: String }, + /// A georef names a missing or ill-typed dim, or carries a degenerate /// transform. #[error("georef rejected on '{array}': {detail}")] @@ -159,12 +176,15 @@ impl ArrayError { | ArrayError::InvalidAttr { array, .. } | ArrayError::InvalidTileExtents { array, .. } | ArrayError::InvalidGeoref { array, .. } + | ArrayError::InvalidAttrMeta { array, .. } + | ArrayError::SchemaEncode { array, .. } | ArrayError::CoordArityMismatch { array, .. } | ArrayError::CoordOutOfDomain { array, .. } | ArrayError::CellTypeMismatch { array, .. } | ArrayError::Narrow { array, .. } => array, ArrayError::ZoneFrame(e) => e.array(), ArrayError::NumericOverflow { .. } + | ArrayError::AggregateBudgetExceeded { .. } | ArrayError::SegmentCorruption { .. } | ArrayError::UnsupportedFormat { .. } | ArrayError::UnsupportedSegmentFormat { .. } diff --git a/nodedb-array/src/lib.rs b/nodedb-array/src/lib.rs index fe86d2825..f87d8dcd4 100644 --- a/nodedb-array/src/lib.rs +++ b/nodedb-array/src/lib.rs @@ -24,7 +24,7 @@ pub use coord::{ }; pub use error::{ArrayError, ArrayResult}; pub use schema::{ - ArrayGeoref, ArraySchema, ArraySchemaBuilder, AttrSpec, CellOrder, DimSpec, DimType, + ArrayGeoref, ArraySchema, ArraySchemaBuilder, AttrMeta, AttrSpec, CellOrder, DimSpec, DimType, GeoTransform, TileOrder, }; pub use segment::{ diff --git a/nodedb-array/src/query/aggregate.rs b/nodedb-array/src/query/aggregate.rs deleted file mode 100644 index e9be5ae76..000000000 --- a/nodedb-array/src/query/aggregate.rs +++ /dev/null @@ -1,412 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -//! Reductions over a sparse tile. -//! -//! Five reducers (Sum / Count / Min / Max / Mean) operate on a single -//! attribute column. The shape is partial-friendly: each tile produces -//! an [`AggregateResult`] which the executor merges across tiles via -//! [`AggregateResult::merge`]. Mean carries (sum, count) so merges are -//! exact rather than averaging averages. -//! -//! Group-by ([`group_by_dim`]) buckets cells by one dim's values and -//! returns a `Vec<(CoordValue, AggregateResult)>` ordered by first -//! appearance. Sort/order is the caller's job — keeping insertion -//! order avoids forcing `Ord` on `CoordValue`. - -use std::collections::HashMap; - -use crate::error::{ArrayError, ArrayResult}; -use crate::query::agg_number::{AggNumber, AggScalar}; -use crate::tile::sparse_tile::{RowKind, SparseTile}; -use crate::types::cell_value::value::CellValue; -use crate::types::coord::value::CoordValue; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Reducer { - Sum, - Count, - Min, - Max, - Mean, -} - -/// Partial result for one reducer over one (group of) cell(s). -/// `Mean` retains `(sum, count)` so partials merge exactly. An integer attr -/// accumulates exactly; see [`AggNumber`]. -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum AggregateResult { - Sum { - value: AggNumber, - count: u64, - }, - Count { - count: u64, - }, - Min { - value: AggNumber, - count: u64, - }, - Max { - value: AggNumber, - count: u64, - }, - Mean { - sum: AggNumber, - count: u64, - }, - /// No non-null cells observed yet for this reducer. - Empty(Reducer), -} - -impl AggregateResult { - /// Merge two partials of the same reducer kind. Mismatched - /// reducers produce a left-biased result (the executor only ever - /// merges same-kind partials, so this is a programming-error path). - pub fn merge(self, other: AggregateResult) -> AggregateResult { - use AggregateResult::*; - match (self, other) { - (Empty(_), x) | (x, Empty(_)) => x, - ( - Sum { - value: a, - count: ca, - }, - Sum { - value: b, - count: cb, - }, - ) => Sum { - value: a.sum_with(b), - count: ca + cb, - }, - (Count { count: a }, Count { count: b }) => Count { count: a + b }, - ( - Min { - value: a, - count: ca, - }, - Min { - value: b, - count: cb, - }, - ) => Min { - value: a.min_with(b), - count: ca + cb, - }, - ( - Max { - value: a, - count: ca, - }, - Max { - value: b, - count: cb, - }, - ) => Max { - value: a.max_with(b), - count: ca + cb, - }, - (Mean { sum: a, count: ca }, Mean { sum: b, count: cb }) => Mean { - sum: a.sum_with(b), - count: ca + cb, - }, - (lhs, _) => lhs, - } - } - - /// Final value, or `None` when no cell contributed. `SUM` of an integer - /// attr is an exact integer and refuses past `i64` (SQLSTATE `22003`). - /// `MIN` / `MAX` keep the attr's kind. `COUNT` is an integer. `MEAN` is - /// always a float. - pub fn finalize(self) -> ArrayResult> { - match self { - AggregateResult::Sum { value, .. } - | AggregateResult::Min { value, .. } - | AggregateResult::Max { value, .. } => value.finish().map(Some), - AggregateResult::Count { count } => Ok(Some(AggScalar::Int( - i64::try_from(count).map_err(|_| ArrayError::NumericOverflow { - detail: format!("count {count} is out of range for bigint"), - })?, - ))), - AggregateResult::Mean { sum, count } if count > 0 => { - Ok(Some(AggScalar::Float(sum.to_f64() / count as f64))) - } - AggregateResult::Mean { .. } | AggregateResult::Empty(_) => Ok(None), - } - } -} - -fn empty(r: Reducer) -> AggregateResult { - AggregateResult::Empty(r) -} - -/// Reduce one attr column over all rows of a tile. `attr_idx` must be -/// in range; non-numeric / null cells are skipped (Count includes -/// non-null cells regardless of dtype, since "count of cells with this -/// attribute populated" is a sensible fold across all attr types). -pub fn aggregate_attr(tile: &SparseTile, attr_idx: usize, reducer: Reducer) -> AggregateResult { - let Some(col) = tile.attr_cols.get(attr_idx) else { - return empty(reducer); - }; - let mut acc = empty(reducer); - for v in col { - let one = single_cell(v, reducer); - acc = acc.merge(one); - } - acc -} - -fn single_cell(v: &CellValue, reducer: Reducer) -> AggregateResult { - use AggregateResult::*; - let n = AggNumber::of_cell(v); - match (reducer, n) { - (Reducer::Count, _) if !v.is_null() => Count { count: 1 }, - (Reducer::Count, _) => Empty(Reducer::Count), - (Reducer::Sum, Some(x)) => Sum { value: x, count: 1 }, - (Reducer::Min, Some(x)) => Min { value: x, count: 1 }, - (Reducer::Max, Some(x)) => Max { value: x, count: 1 }, - (Reducer::Mean, Some(x)) => Mean { sum: x, count: 1 }, - (r, None) => Empty(r), - } -} - -/// One bucket of a group-by aggregate. The key is the dim value the -/// rows share; `result` is the reducer's running partial over that -/// bucket. -#[derive(Debug, Clone)] -pub struct GroupAggregate { - pub key: CoordValue, - pub result: AggregateResult, -} - -/// Group rows by one dim's values and reduce `attr_idx` per group. -/// Returns groups in first-seen order. -pub fn group_by_dim( - tile: &SparseTile, - dim_idx: usize, - attr_idx: usize, - reducer: Reducer, -) -> Vec { - let Some(dict) = tile.dim_dicts.get(dim_idx) else { - return Vec::new(); - }; - let Some(col) = tile.attr_cols.get(attr_idx) else { - return Vec::new(); - }; - let mut order: Vec = Vec::new(); - let mut by_key: HashMap = HashMap::new(); - // Iterate physical rows; track live_idx separately because attr_cols only - // has entries for Live rows — sentinel rows carry no attr payload. - let mut live_idx = 0usize; - for row in 0..tile.row_count() { - let kind = match tile.row_kind(row) { - Ok(k) => k, - Err(_) => break, - }; - if kind != RowKind::Live { - continue; - } - let cell = match col.get(live_idx) { - Some(c) => c, - None => break, - }; - live_idx += 1; - let key = dict.values[dict.indices[row] as usize].clone(); - let one = single_cell(cell, reducer); - match by_key.get_mut(&key) { - Some(slot) => *slot = slot.merge(one), - None => { - order.push(key.clone()); - by_key.insert(key, empty(reducer).merge(one)); - } - } - } - order - .into_iter() - .map(|k| GroupAggregate { - result: by_key.remove(&k).unwrap_or(empty(reducer)), - key: k, - }) - .collect() -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::schema::ArraySchema; - use crate::schema::ArraySchemaBuilder; - use crate::schema::attr_spec::{AttrSpec, AttrType}; - use crate::schema::dim_spec::{DimSpec, DimType}; - use crate::tile::sparse_tile::SparseTileBuilder; - use crate::types::domain::{Domain, DomainBound}; - - fn schema() -> ArraySchema { - ArraySchemaBuilder::new("g") - .dim(DimSpec::new( - "k", - DimType::Int64, - Domain::new(DomainBound::Int64(0), DomainBound::Int64(15)), - )) - .attr(AttrSpec::new("v", AttrType::Float64, true)) - .tile_extents(vec![16]) - .build() - .unwrap() - } - - fn tile(rows: &[(i64, Option)]) -> SparseTile { - let s = schema(); - let mut b = SparseTileBuilder::new(&s); - for (k, v) in rows { - let cv = v.map(CellValue::Float64).unwrap_or(CellValue::Null); - b.push(&[CoordValue::Int64(*k)], &[cv]).unwrap(); - } - b.build() - } - - fn fin(r: AggregateResult) -> Option { - r.finalize().expect("finalize") - } - - fn float(v: f64) -> Option { - Some(AggScalar::Float(v)) - } - - fn int_tile(dtype: AttrType, values: &[CellValue]) -> SparseTile { - let s = ArraySchemaBuilder::new("n") - .dim(DimSpec::new( - "k", - DimType::Int64, - Domain::new(DomainBound::Int64(0), DomainBound::Int64(15)), - )) - .attr(AttrSpec::new("v", dtype, true)) - .tile_extents(vec![16]) - .build() - .unwrap(); - let mut b = SparseTileBuilder::new(&s); - for (k, v) in values.iter().enumerate() { - b.push(&[CoordValue::Int64(k as i64)], std::slice::from_ref(v)) - .unwrap(); - } - b.build() - } - - #[test] - fn sum_skips_nulls() { - let t = tile(&[(0, Some(1.0)), (1, None), (2, Some(3.0))]); - let r = aggregate_attr(&t, 0, Reducer::Sum); - assert_eq!(fin(r), float(4.0)); - } - - #[test] - fn count_includes_nonnull_only() { - let t = tile(&[(0, Some(1.0)), (1, None), (2, Some(3.0))]); - let r = aggregate_attr(&t, 0, Reducer::Count); - assert_eq!(fin(r), Some(AggScalar::Int(2))); - } - - #[test] - fn min_max_mean() { - let t = tile(&[(0, Some(1.0)), (1, Some(5.0)), (2, Some(3.0))]); - assert_eq!(fin(aggregate_attr(&t, 0, Reducer::Min)), float(1.0)); - assert_eq!(fin(aggregate_attr(&t, 0, Reducer::Max)), float(5.0)); - assert_eq!(fin(aggregate_attr(&t, 0, Reducer::Mean)), float(3.0)); - } - - #[test] - fn merge_combines_partials_exactly() { - let a = AggregateResult::Mean { - sum: AggNumber::Float(10.0), - count: 4, - }; - let b = AggregateResult::Mean { - sum: AggNumber::Float(6.0), - count: 2, - }; - // (10 + 6) / (4 + 2) = 16/6 - assert_eq!(fin(a.merge(b)), float(16.0 / 6.0)); - } - - #[test] - fn empty_reducer_finalizes_to_none() { - let t = tile(&[(0, None)]); - let r = aggregate_attr(&t, 0, Reducer::Sum); - assert_eq!(fin(r), None); - } - - /// An `INT64` SUM keeps every unit past 2^53, and MIN / MAX stay exact - /// integers. MEAN is a float. - #[test] - fn integer_reducers_are_exact() { - let big = (1i64 << 53) + 1; - let t = int_tile( - AttrType::Int64, - &[CellValue::Int64(big), CellValue::Null, CellValue::Int64(0)], - ); - assert_eq!( - fin(aggregate_attr(&t, 0, Reducer::Sum)), - Some(AggScalar::Int(big)) - ); - assert_eq!( - fin(aggregate_attr(&t, 0, Reducer::Max)), - Some(AggScalar::Int(big)) - ); - assert_eq!( - fin(aggregate_attr(&t, 0, Reducer::Min)), - Some(AggScalar::Int(0)) - ); - assert!(matches!( - fin(aggregate_attr(&t, 0, Reducer::Mean)), - Some(AggScalar::Float(_)) - )); - } - - #[test] - fn integer_sum_past_bigint_is_refused() { - let t = int_tile( - AttrType::Int64, - &[CellValue::Int64(i64::MAX), CellValue::Int64(1)], - ); - assert!(matches!( - aggregate_attr(&t, 0, Reducer::Sum).finalize(), - Err(ArrayError::NumericOverflow { .. }) - )); - } - - #[test] - fn narrow_attrs_reduce() { - let t = int_tile( - AttrType::UInt16, - &[CellValue::UInt16(65535), CellValue::UInt16(65535)], - ); - assert_eq!( - fin(aggregate_attr(&t, 0, Reducer::Sum)), - Some(AggScalar::Int(131_070)) - ); - let f = int_tile( - AttrType::Float32, - &[CellValue::Float32(0.5), CellValue::Float32(f32::NAN)], - ); - assert!(matches!( - fin(aggregate_attr(&f, 0, Reducer::Max)), - Some(AggScalar::Float(v)) if v.is_nan() - )); - assert_eq!(fin(aggregate_attr(&f, 0, Reducer::Min)), float(0.5)); - } - - #[test] - fn group_by_buckets_by_dim_value() { - let t = tile(&[ - (0, Some(1.0)), - (1, Some(2.0)), - (0, Some(3.0)), - (1, Some(4.0)), - ]); - let g = group_by_dim(&t, 0, 0, Reducer::Sum); - assert_eq!(g.len(), 2); - // first-seen order: 0, then 1 - assert_eq!(g[0].key, CoordValue::Int64(0)); - assert_eq!(fin(g[0].result), float(4.0)); - assert_eq!(g[1].key, CoordValue::Int64(1)); - assert_eq!(fin(g[1].result), float(6.0)); - } -} diff --git a/nodedb-array/src/query/aggregate/fold.rs b/nodedb-array/src/query/aggregate/fold.rs new file mode 100644 index 000000000..77b9aa616 --- /dev/null +++ b/nodedb-array/src/query/aggregate/fold.rs @@ -0,0 +1,580 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Folding a tile's cells into reducer partials. +//! +//! A NULL cell and a cell equal to the attr's nodata contribute to no +//! reducer, `COUNT` included: nodata is not data. With `physical` set, a +//! numeric cell of an attr with a scale or offset contributes +//! `raw * scale + offset` as a float. + +use std::collections::HashMap; + +use nodedb_types::array_agg::{DistAcc, DistValue, Moments}; + +use super::reducer::Reducer; +use super::result::AggregateResult; +use crate::query::agg_number::AggNumber; +use crate::schema::attr_meta::AttrMeta; +use crate::tile::sparse_tile::{RowKind, SparseTile}; +use crate::types::cell_value::value::CellValue; +use crate::types::coord::value::CoordValue; + +/// How a reducer reads one attr's cells. +#[derive(Debug, Clone, Copy)] +pub struct AggRead<'a> { + pub reducer: Reducer, + /// The attr's metadata: its nodata, scale and offset. + pub meta: &'a AttrMeta, + /// Whether numeric cells read as `raw * scale + offset`. + pub physical: bool, +} + +impl AggRead<'_> { + /// The number `v` contributes, or `None` for a NULL, nodata or + /// non-numeric cell. + fn value(&self, v: &CellValue) -> Option { + if v.is_null() || self.meta.is_nodata(v) { + return None; + } + if self.physical + && let (Some((scale, offset)), Some(raw)) = (self.meta.transform(), v.as_f64()) + { + return Some(DistValue::Float(raw * scale + offset)); + } + match v.as_i64() { + Some(i) => Some(DistValue::Int(i)), + None => v.as_float().map(DistValue::Float), + } + } + + /// Fold cell `v` into `acc`. + fn fold(&self, acc: &mut AggregateResult, v: &CellValue) { + if matches!(self.reducer, Reducer::Count) { + if !v.is_null() && !self.meta.is_nodata(v) { + acc.absorb(AggregateResult::Count { count: 1 }); + } + return; + } + let Some(value) = self.value(v) else { + return; + }; + let number = match value { + DistValue::Int(i) => AggNumber::Int(i128::from(i)), + DistValue::Float(f) => AggNumber::Float(f), + }; + let as_f64 = number.to_f64(); + match self.reducer { + Reducer::Count => {} + Reducer::Sum => acc.absorb(AggregateResult::Sum { + value: number, + count: 1, + }), + Reducer::Min => acc.absorb(AggregateResult::Min { + value: number, + count: 1, + }), + Reducer::Max => acc.absorb(AggregateResult::Max { + value: number, + count: 1, + }), + Reducer::Mean => acc.absorb(AggregateResult::Mean { + sum: number, + count: 1, + }), + Reducer::Median | Reducer::Percentile(_) | Reducer::Mode => match acc { + AggregateResult::Dist { acc: dist, .. } => dist.insert(value, 1), + other => { + let mut dist = DistAcc::default(); + dist.insert(value, 1); + other.absorb(AggregateResult::Dist { + reducer: self.reducer, + acc: dist, + }); + } + }, + Reducer::StdDevPop | Reducer::StdDevSamp | Reducer::VarPop | Reducer::VarSamp => acc + .absorb(AggregateResult::Moments { + reducer: self.reducer, + moments: Moments::one(as_f64), + }), + Reducer::Histogram(spec) => { + if let AggregateResult::Empty(_) = acc { + *acc = AggregateResult::Histogram { + spec, + counts: spec.empty_counts(), + }; + } + if let (AggregateResult::Histogram { counts, .. }, Some(bin)) = + (acc, spec.bin_of(as_f64)) + && let Some(slot) = counts.get_mut(bin) + { + *slot += 1; + } + } + } + } +} + +/// Reduce attr `attr_idx` over every live row of `tile`. +pub fn aggregate_attr(tile: &SparseTile, attr_idx: usize, read: &AggRead<'_>) -> AggregateResult { + let mut acc = AggregateResult::empty(read.reducer); + if let Some(col) = tile.attr_cols.get(attr_idx) { + for v in col { + read.fold(&mut acc, v); + } + } + acc +} + +/// One bucket of a group-by aggregate: the dim value the rows share and +/// the reducer's partial over them. +#[derive(Debug, Clone)] +pub struct GroupAggregate { + pub key: CoordValue, + pub result: AggregateResult, +} + +/// Group rows by dim `dim_idx` and reduce `attr_idx` per group. A group +/// exists once any live row holds its key, even when every cell of it is +/// NULL or nodata. Returns groups in first-seen order. +pub fn group_by_dim( + tile: &SparseTile, + dim_idx: usize, + attr_idx: usize, + read: &AggRead<'_>, +) -> Vec { + let Some(dict) = tile.dim_dicts.get(dim_idx) else { + return Vec::new(); + }; + let Some(col) = tile.attr_cols.get(attr_idx) else { + return Vec::new(); + }; + let mut order: Vec = Vec::new(); + let mut by_key: HashMap = HashMap::new(); + // attr_cols holds entries for Live rows only, so the live index runs + // apart from the physical row. + let mut live_idx = 0usize; + for row in 0..tile.row_count() { + let Ok(kind) = tile.row_kind(row) else { + break; + }; + if kind != RowKind::Live { + continue; + } + let Some(cell) = col.get(live_idx) else { + break; + }; + live_idx += 1; + let Some(key) = dict + .indices + .get(row) + .and_then(|&i| dict.values.get(i as usize)) + else { + break; + }; + if !by_key.contains_key(key) { + order.push(key.clone()); + by_key.insert(key.clone(), AggregateResult::empty(read.reducer)); + } + if let Some(slot) = by_key.get_mut(key) { + read.fold(slot, cell); + } + } + order + .into_iter() + .map(|k| GroupAggregate { + result: by_key + .remove(&k) + .unwrap_or(AggregateResult::empty(read.reducer)), + key: k, + }) + .collect() +} + +#[cfg(test)] +mod tests { + use nodedb_types::array_agg::HistogramSpec; + + use super::*; + use crate::error::ArrayError; + use crate::query::agg_number::AggScalar; + use crate::query::aggregate::result::AggOutput; + use crate::schema::ArraySchemaBuilder; + use crate::schema::attr_spec::{AttrSpec, AttrType}; + use crate::schema::dim_spec::{DimSpec, DimType}; + use crate::tile::sparse_tile::SparseTileBuilder; + use crate::types::domain::{Domain, DomainBound}; + + static NO_META: AttrMeta = AttrMeta { + nodata: None, + scale: None, + offset: None, + units: None, + }; + + fn read(reducer: Reducer) -> AggRead<'static> { + AggRead { + reducer, + meta: &NO_META, + physical: false, + } + } + + /// A tile of `values` in attr `v` of type `dtype`, cell `k` at dim `k` + /// modulo `groups`. + fn tile_of(dtype: AttrType, values: &[CellValue], groups: i64) -> SparseTile { + let s = ArraySchemaBuilder::new("n") + .dim(DimSpec::new( + "k", + DimType::Int64, + Domain::new(DomainBound::Int64(0), DomainBound::Int64(1023)), + )) + .dim(DimSpec::new( + "i", + DimType::Int64, + Domain::new(DomainBound::Int64(0), DomainBound::Int64(1023)), + )) + .attr(AttrSpec::new("v", dtype, true)) + .tile_extents(vec![1024, 1024]) + .build() + .unwrap(); + let mut b = SparseTileBuilder::new(&s); + for (i, v) in values.iter().enumerate() { + let i = i as i64; + b.push( + &[CoordValue::Int64(i % groups), CoordValue::Int64(i)], + std::slice::from_ref(v), + ) + .unwrap(); + } + b.build() + } + + fn floats(values: &[Option]) -> SparseTile { + let cells: Vec = values + .iter() + .map(|v| v.map(CellValue::Float64).unwrap_or(CellValue::Null)) + .collect(); + tile_of(AttrType::Float64, &cells, 1) + } + + fn scalar(r: AggregateResult) -> Option { + match r.finalize().expect("finalize") { + AggOutput::Scalar(v) => v, + AggOutput::Bins(b) => panic!("scalar reducer answered bins: {b:?}"), + } + } + + fn float(v: f64) -> Option { + Some(AggScalar::Float(v)) + } + + fn close(got: Option, want: f64) { + match got { + Some(AggScalar::Float(v)) => assert!((v - want).abs() < 1e-9, "{v} != {want}"), + other => panic!("expected float {want}, got {other:?}"), + } + } + + #[test] + fn sum_count_min_max_mean_skip_nulls() { + let t = floats(&[Some(1.0), None, Some(5.0), Some(3.0)]); + assert_eq!( + scalar(aggregate_attr(&t, 0, &read(Reducer::Sum))), + float(9.0) + ); + assert_eq!( + scalar(aggregate_attr(&t, 0, &read(Reducer::Count))), + Some(AggScalar::Int(3)) + ); + assert_eq!( + scalar(aggregate_attr(&t, 0, &read(Reducer::Min))), + float(1.0) + ); + assert_eq!( + scalar(aggregate_attr(&t, 0, &read(Reducer::Max))), + float(5.0) + ); + assert_eq!( + scalar(aggregate_attr(&t, 0, &read(Reducer::Mean))), + float(3.0) + ); + } + + #[test] + fn integer_reducers_are_exact() { + let big = (1i64 << 53) + 1; + let t = tile_of( + AttrType::Int64, + &[CellValue::Int64(big), CellValue::Null, CellValue::Int64(0)], + 1, + ); + assert_eq!( + scalar(aggregate_attr(&t, 0, &read(Reducer::Sum))), + Some(AggScalar::Int(big)) + ); + assert_eq!( + scalar(aggregate_attr(&t, 0, &read(Reducer::Max))), + Some(AggScalar::Int(big)) + ); + let over = tile_of( + AttrType::Int64, + &[CellValue::Int64(i64::MAX), CellValue::Int64(1)], + 1, + ); + assert!(matches!( + aggregate_attr(&over, 0, &read(Reducer::Sum)).finalize(), + Err(ArrayError::NumericOverflow { .. }) + )); + } + + #[test] + fn median_percentile_and_mode() { + let t = floats(&[Some(4.0), Some(1.0), Some(3.0), Some(2.0), None]); + assert_eq!( + scalar(aggregate_attr(&t, 0, &read(Reducer::Median))), + float(2.5) + ); + close( + scalar(aggregate_attr(&t, 0, &read(Reducer::Percentile(0.9)))), + 3.7, + ); + let ints = tile_of( + AttrType::Int8, + &[ + CellValue::Int8(7), + CellValue::Int8(-2), + CellValue::Int8(7), + CellValue::Int8(-2), + CellValue::Int8(5), + ], + 1, + ); + // -2 and 7 tie at two cells each; the smaller wins. + assert_eq!( + scalar(aggregate_attr(&ints, 0, &read(Reducer::Mode))), + Some(AggScalar::Int(-2)) + ); + assert_eq!( + scalar(aggregate_attr(&ints, 0, &read(Reducer::Median))), + float(5.0) + ); + } + + #[test] + fn variance_and_standard_deviation() { + let t = floats(&[ + Some(2.0), + Some(4.0), + Some(4.0), + Some(4.0), + Some(5.0), + Some(5.0), + Some(7.0), + Some(9.0), + ]); + close(scalar(aggregate_attr(&t, 0, &read(Reducer::VarPop))), 4.0); + close( + scalar(aggregate_attr(&t, 0, &read(Reducer::StdDevPop))), + 2.0, + ); + close( + scalar(aggregate_attr(&t, 0, &read(Reducer::VarSamp))), + 32.0 / 7.0, + ); + close( + scalar(aggregate_attr(&t, 0, &read(Reducer::StdDevSamp))), + (32.0f64 / 7.0).sqrt(), + ); + let one = floats(&[Some(3.0)]); + assert_eq!( + scalar(aggregate_attr(&one, 0, &read(Reducer::VarSamp))), + None + ); + } + + #[test] + fn histogram_counts_in_range_values() { + let spec = HistogramSpec::new(2, 0.0, 10.0).expect("valid"); + let t = floats(&[ + Some(0.0), + Some(4.9), + Some(5.0), + Some(10.0), + Some(11.0), + None, + ]); + let AggOutput::Bins(bins) = aggregate_attr(&t, 0, &read(Reducer::Histogram(spec))) + .finalize() + .expect("finalize") + else { + panic!("histogram answers bins"); + }; + let counts: Vec = bins.iter().map(|b| b.count).collect(); + assert_eq!(counts, vec![2, 2]); + assert_eq!((bins[1].lo, bins[1].hi), (5.0, 10.0)); + } + + /// Every reducer: two partials of a split column merge to the result of + /// one pass over the whole column. + #[test] + fn merged_partials_equal_a_single_pass() { + let values: Vec> = [3.0, -1.5, 8.0, 3.0, 0.25, 9.5, 3.0, -4.0, 2.0] + .iter() + .map(|v| Some(*v)) + .collect(); + let spec = HistogramSpec::new(4, -4.0, 10.0).expect("valid"); + let whole = floats(&values); + let (left, right) = values.split_at(4); + let (a, b) = (floats(left), floats(right)); + for reducer in [ + Reducer::Sum, + Reducer::Count, + Reducer::Min, + Reducer::Max, + Reducer::Mean, + Reducer::Median, + Reducer::Percentile(0.3), + Reducer::Mode, + Reducer::VarPop, + Reducer::VarSamp, + Reducer::StdDevPop, + Reducer::StdDevSamp, + Reducer::Histogram(spec), + ] { + let r = read(reducer); + let merged = aggregate_attr(&a, 0, &r) + .merge(aggregate_attr(&b, 0, &r)) + .finalize() + .expect("merged"); + let single = aggregate_attr(&whole, 0, &r).finalize().expect("single"); + match (merged, single) { + ( + AggOutput::Scalar(Some(AggScalar::Float(x))), + AggOutput::Scalar(Some(AggScalar::Float(y))), + ) => { + assert!((x - y).abs() < 1e-9, "{reducer:?}: {x} != {y}") + } + (x, y) => assert_eq!(x, y, "{reducer:?}"), + } + } + } + + /// An all-NULL or all-nodata column answers NULL for every scalar + /// reducer, `COUNT` included. + #[test] + fn all_nodata_answers_null() { + let meta = AttrMeta { + nodata: Some(0.0), + ..AttrMeta::default() + }; + let t = tile_of( + AttrType::UInt16, + &[CellValue::UInt16(0), CellValue::Null, CellValue::UInt16(0)], + 1, + ); + for reducer in [ + Reducer::Sum, + Reducer::Count, + Reducer::Min, + Reducer::Mean, + Reducer::Median, + Reducer::Mode, + Reducer::VarPop, + ] { + let r = AggRead { + reducer, + meta: &meta, + physical: false, + }; + assert_eq!(scalar(aggregate_attr(&t, 0, &r)), None, "{reducer:?}"); + } + } + + #[test] + fn nodata_is_excluded_and_physical_applies_scale_offset() { + let meta = AttrMeta { + nodata: Some(0.0), + scale: Some(0.5), + offset: Some(-1.0), + units: None, + }; + let t = tile_of( + AttrType::UInt16, + &[ + CellValue::UInt16(0), + CellValue::UInt16(4), + CellValue::UInt16(8), + ], + 1, + ); + let raw = AggRead { + reducer: Reducer::Sum, + meta: &meta, + physical: false, + }; + assert_eq!( + scalar(aggregate_attr(&t, 0, &raw)), + Some(AggScalar::Int(12)) + ); + let count = AggRead { + reducer: Reducer::Count, + ..raw + }; + assert_eq!( + scalar(aggregate_attr(&t, 0, &count)), + Some(AggScalar::Int(2)) + ); + let physical = AggRead { + physical: true, + ..raw + }; + // (4 * 0.5 - 1) + (8 * 0.5 - 1) = 4. + assert_eq!(scalar(aggregate_attr(&t, 0, &physical)), float(4.0)); + let mode = AggRead { + reducer: Reducer::Max, + ..physical + }; + assert_eq!(scalar(aggregate_attr(&t, 0, &mode)), float(3.0)); + } + + #[test] + fn group_by_works_with_every_reducer_kind() { + // Cell k sits in group k % 2: group 0 holds 1, 3, 5; group 1 holds 2, 4. + let cells: Vec = (1..=5).map(CellValue::Int64).collect(); + let t = tile_of(AttrType::Int64, &cells, 2); + let spec = HistogramSpec::new(1, 0.0, 10.0).expect("valid"); + let by = |reducer| group_by_dim(&t, 0, 0, &read(reducer)); + let g = by(Reducer::Median); + assert_eq!(g.len(), 2); + assert_eq!(g[0].key, CoordValue::Int64(0)); + assert_eq!(scalar(g[0].result.clone()), float(3.0)); + assert_eq!(scalar(g[1].result.clone()), float(3.0)); + let g = by(Reducer::Sum); + assert_eq!(scalar(g[0].result.clone()), Some(AggScalar::Int(9))); + let g = by(Reducer::VarPop); + close(scalar(g[1].result.clone()), 1.0); + let g = by(Reducer::Histogram(spec)); + let AggOutput::Bins(bins) = g[0].result.clone().finalize().expect("bins") else { + panic!("histogram answers bins"); + }; + assert_eq!(bins[0].count, 3); + } + + #[test] + fn narrow_float_max_keeps_nan_high() { + let f = tile_of( + AttrType::Float32, + &[CellValue::Float32(0.5), CellValue::Float32(f32::NAN)], + 1, + ); + assert!(matches!( + scalar(aggregate_attr(&f, 0, &read(Reducer::Max))), + Some(AggScalar::Float(v)) if v.is_nan() + )); + assert_eq!( + scalar(aggregate_attr(&f, 0, &read(Reducer::Min))), + float(0.5) + ); + } +} diff --git a/nodedb-array/src/query/aggregate/mod.rs b/nodedb-array/src/query/aggregate/mod.rs new file mode 100644 index 000000000..40a28310c --- /dev/null +++ b/nodedb-array/src/query/aggregate/mod.rs @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Reductions over a sparse tile. +//! +//! Each reducer folds one attribute column into an [`AggregateResult`] +//! partial. Partials merge exactly across tiles, cores and shards, so a +//! merged result equals a single pass over every cell. +//! +//! Group-by ([`group_by_dim`]) buckets cells by one dim's values and +//! returns the groups in first-seen order. + +pub mod fold; +pub mod reducer; +pub mod result; + +pub use fold::{AggRead, GroupAggregate, aggregate_attr, group_by_dim}; +pub use reducer::Reducer; +pub use result::{AggOutput, AggregateResult}; diff --git a/nodedb-array/src/query/aggregate/reducer.rs b/nodedb-array/src/query/aggregate/reducer.rs new file mode 100644 index 000000000..034b8efc2 --- /dev/null +++ b/nodedb-array/src/query/aggregate/reducer.rs @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! The reducers an array aggregate runs. + +use nodedb_types::array_agg::HistogramSpec; + +/// One array reducer. +/// +/// - `Sum`, `Min`, `Max`: exact for an integer attr. `Min` / `Max` sort NaN +/// above every other float. +/// - `Count`: the non-null, non-nodata cells of any attr type. +/// - `Mean`: carries `(sum, count)`, so merges are exact. +/// - `Median`, `Percentile(p)`: exact, by linear interpolation between the +/// closest ranks (PostgreSQL `percentile_cont`). `Median` is `p = 0.5`. +/// - `Mode`: the value the most cells hold, the smallest among ties. +/// - `StdDevPop`, `StdDevSamp`, `VarPop`, `VarSamp`: population and sample +/// forms, as PostgreSQL names them. +/// - `Histogram`: fixed-width bin counts over `[lo, hi]`. +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Reducer { + Sum, + Count, + Min, + Max, + Mean, + Median, + /// `0 <= p <= 1`. + Percentile(f64), + StdDevPop, + StdDevSamp, + VarPop, + VarSamp, + Mode, + Histogram(HistogramSpec), +} diff --git a/nodedb-array/src/query/aggregate/result.rs b/nodedb-array/src/query/aggregate/result.rs new file mode 100644 index 000000000..9d52a6442 --- /dev/null +++ b/nodedb-array/src/query/aggregate/result.rs @@ -0,0 +1,298 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Partial results of the array reducers, their exact merge, and their +//! final value. + +use nodedb_types::array_agg::{ + DistAcc, DistBudgetExceeded, DistValue, HistogramBin, HistogramSpec, Moments, +}; + +use super::reducer::Reducer; +use crate::error::{ArrayError, ArrayResult}; +use crate::query::agg_number::{AggNumber, AggScalar}; + +/// Partial result for one reducer over one (group of) cell(s). +#[derive(Debug, Clone, PartialEq)] +pub enum AggregateResult { + Sum { + value: AggNumber, + count: u64, + }, + Count { + count: u64, + }, + Min { + value: AggNumber, + count: u64, + }, + Max { + value: AggNumber, + count: u64, + }, + /// `(sum, count)`, so partials merge exactly. + Mean { + sum: AggNumber, + count: u64, + }, + /// `Median`, `Percentile` and `Mode`: every distinct value with its count. + Dist { + reducer: Reducer, + acc: DistAcc, + }, + /// The variance and standard-deviation reducers. + Moments { + reducer: Reducer, + moments: Moments, + }, + /// Histogram bin counts, one per bin of `spec`. + Histogram { + spec: HistogramSpec, + counts: Vec, + }, + /// No cell contributed yet. + Empty(Reducer), +} + +/// The final value of a reducer. +#[derive(Debug, Clone, PartialEq)] +pub enum AggOutput { + /// One value. `None` when no cell contributed. + Scalar(Option), + /// One entry per histogram bin, zero-count bins included. + Bins(Vec), +} + +impl AggregateResult { + /// The partial no cell has contributed to. + pub fn empty(reducer: Reducer) -> Self { + AggregateResult::Empty(reducer) + } + + /// Merge two partials of the same reducer. The executor merges only + /// same-reducer partials. A mismatched pair keeps the left side. + pub fn merge(self, other: AggregateResult) -> AggregateResult { + use AggregateResult::*; + match (self, other) { + (Empty(_), x) | (x, Empty(_)) => x, + ( + Sum { + value: a, + count: ca, + }, + Sum { + value: b, + count: cb, + }, + ) => Sum { + value: a.sum_with(b), + count: ca + cb, + }, + (Count { count: a }, Count { count: b }) => Count { count: a + b }, + ( + Min { + value: a, + count: ca, + }, + Min { + value: b, + count: cb, + }, + ) => Min { + value: a.min_with(b), + count: ca + cb, + }, + ( + Max { + value: a, + count: ca, + }, + Max { + value: b, + count: cb, + }, + ) => Max { + value: a.max_with(b), + count: ca + cb, + }, + (Mean { sum: a, count: ca }, Mean { sum: b, count: cb }) => Mean { + sum: a.sum_with(b), + count: ca + cb, + }, + (Dist { reducer, mut acc }, Dist { acc: other, .. }) => { + acc.merge(other); + Dist { reducer, acc } + } + (Moments { reducer, moments }, Moments { moments: other, .. }) => Moments { + reducer, + moments: moments.merge(other), + }, + (Histogram { spec, mut counts }, Histogram { counts: other, .. }) => { + spec.merge_counts(&mut counts, &other); + Histogram { spec, counts } + } + (lhs, _) => lhs, + } + } + + /// Merge `other` into this partial in place. + pub fn absorb(&mut self, other: AggregateResult) { + let current = std::mem::replace(self, AggregateResult::Empty(Reducer::Count)); + *self = current.merge(other); + } + + /// The final value. + /// + /// - `SUM` of an integer attr is an exact integer and refuses past `i64` + /// (SQLSTATE `22003`). `MIN`, `MAX` and `MODE` keep the attr's kind. + /// `COUNT` is an integer. + /// - `MEAN`, `MEDIAN`, `PERCENTILE`, the variances and the standard + /// deviations are floats. + /// - A median, percentile or mode past the distinct-value budget refuses + /// (SQLSTATE `54000`). + /// - A histogram answers every bin, a zero count included. + pub fn finalize(self) -> ArrayResult { + let scalar = |v: Option| -> ArrayResult { Ok(AggOutput::Scalar(v)) }; + match self { + AggregateResult::Sum { value, .. } + | AggregateResult::Min { value, .. } + | AggregateResult::Max { value, .. } => scalar(Some(value.finish()?)), + AggregateResult::Count { count } => scalar(Some(AggScalar::Int( + i64::try_from(count).map_err(|_| ArrayError::NumericOverflow { + detail: format!("count {count} is out of range for bigint"), + })?, + ))), + AggregateResult::Mean { sum, count } if count > 0 => { + scalar(Some(AggScalar::Float(sum.to_f64() / count as f64))) + } + AggregateResult::Mean { .. } => scalar(None), + AggregateResult::Dist { reducer, acc } => scalar(finish_dist(reducer, acc)?), + AggregateResult::Moments { reducer, moments } => { + scalar(finish_moments(reducer, &moments).map(AggScalar::Float)) + } + AggregateResult::Histogram { spec, counts } => { + Ok(AggOutput::Bins(spec.finish(&counts))) + } + AggregateResult::Empty(Reducer::Histogram(spec)) => { + Ok(AggOutput::Bins(spec.finish(&[]))) + } + AggregateResult::Empty(_) => scalar(None), + } + } +} + +/// The final value of a median, percentile or mode. +fn finish_dist(reducer: Reducer, acc: DistAcc) -> ArrayResult> { + let dist = acc.into_dist(); + let refused = |e: DistBudgetExceeded| ArrayError::AggregateBudgetExceeded { + detail: e.to_string(), + }; + Ok(match reducer { + Reducer::Median => dist.percentile(0.5).map_err(refused)?.map(AggScalar::Float), + Reducer::Percentile(p) => dist.percentile(p).map_err(refused)?.map(AggScalar::Float), + Reducer::Mode => dist.mode().map_err(refused)?.map(|v| match v { + DistValue::Int(i) => AggScalar::Int(i), + DistValue::Float(f) => AggScalar::Float(f), + }), + Reducer::Sum + | Reducer::Count + | Reducer::Min + | Reducer::Max + | Reducer::Mean + | Reducer::StdDevPop + | Reducer::StdDevSamp + | Reducer::VarPop + | Reducer::VarSamp + | Reducer::Histogram(_) => None, + }) +} + +/// The final value of a variance or standard deviation. +pub fn finish_moments(reducer: Reducer, moments: &Moments) -> Option { + match reducer { + Reducer::VarPop => moments.var_pop(), + Reducer::VarSamp => moments.var_samp(), + Reducer::StdDevPop => moments.var_pop().map(f64::sqrt), + Reducer::StdDevSamp => moments.var_samp().map(f64::sqrt), + Reducer::Sum + | Reducer::Count + | Reducer::Min + | Reducer::Max + | Reducer::Mean + | Reducer::Median + | Reducer::Percentile(_) + | Reducer::Mode + | Reducer::Histogram(_) => None, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn fin(r: AggregateResult) -> Option { + match r.finalize().expect("finalize") { + AggOutput::Scalar(v) => v, + AggOutput::Bins(b) => panic!("scalar reducer answered bins: {b:?}"), + } + } + + #[test] + fn mean_merges_exactly() { + let a = AggregateResult::Mean { + sum: AggNumber::Float(10.0), + count: 4, + }; + let b = AggregateResult::Mean { + sum: AggNumber::Float(6.0), + count: 2, + }; + assert_eq!(fin(a.merge(b)), Some(AggScalar::Float(16.0 / 6.0))); + } + + #[test] + fn empty_scalar_reducers_finalize_to_none() { + for r in [ + Reducer::Sum, + Reducer::Count, + Reducer::Median, + Reducer::Percentile(0.9), + Reducer::Mode, + Reducer::VarSamp, + Reducer::StdDevPop, + ] { + assert_eq!(fin(AggregateResult::Empty(r)), None, "{r:?}"); + } + } + + #[test] + fn an_empty_histogram_answers_every_bin_at_zero() { + let spec = HistogramSpec::new(3, 0.0, 3.0).expect("valid"); + let AggOutput::Bins(bins) = AggregateResult::Empty(Reducer::Histogram(spec)) + .finalize() + .expect("finalize") + else { + panic!("histogram answers bins"); + }; + assert_eq!(bins.len(), 3); + assert!(bins.iter().all(|b| b.count == 0)); + } + + #[test] + fn an_over_budget_distribution_is_refused() { + let r = AggregateResult::Dist { + reducer: Reducer::Median, + acc: DistAcc::OverBudget { distinct: 7 }, + }; + assert!(matches!( + r.finalize(), + Err(ArrayError::AggregateBudgetExceeded { .. }) + )); + } + + #[test] + fn absorb_equals_merge() { + let mut a = AggregateResult::Count { count: 2 }; + a.absorb(AggregateResult::Count { count: 3 }); + assert_eq!(a, AggregateResult::Count { count: 5 }); + } +} diff --git a/nodedb-array/src/query/mod.rs b/nodedb-array/src/query/mod.rs index cc898231a..fa1cebb7c 100644 --- a/nodedb-array/src/query/mod.rs +++ b/nodedb-array/src/query/mod.rs @@ -4,7 +4,7 @@ //! //! Pure, tile-local primitives: slice (coord-range filter), project //! (attr subset), region (coordinate box plus polygon zone), aggregate -//! (sum/count/min/max/mean with optional group-by + cross-tile merge), +//! (exact reducers with optional group-by + cross-tile merge), //! elementwise (binary ops between two aligned tiles) and rechunk //! (re-bucket cells into a new tile-extent layout). No engine state, no plane crossings — the executor wires these //! into the physical plan. @@ -13,6 +13,7 @@ pub mod agg_number; pub mod aggregate; pub mod ceiling; pub mod elementwise; +pub mod physical; pub mod project; pub mod rechunk; pub mod region; @@ -21,9 +22,12 @@ pub mod slice; pub mod zone_frame; pub use agg_number::{AggNumber, AggScalar}; -pub use aggregate::{AggregateResult, GroupAggregate, Reducer, aggregate_attr, group_by_dim}; +pub use aggregate::{ + AggOutput, AggRead, AggregateResult, GroupAggregate, Reducer, aggregate_attr, group_by_dim, +}; pub use ceiling::{CeilingParams, CeilingResult, ceiling_resolve_cell}; pub use elementwise::{BinaryOp, elementwise}; +pub use physical::{physical_cells, physical_sparse}; pub use project::{Projection, project_sparse}; pub use rechunk::rechunk_sparse; pub use region::{ diff --git a/nodedb-array/src/query/physical.rs b/nodedb-array/src/query/physical.rs new file mode 100644 index 000000000..36e5544f0 --- /dev/null +++ b/nodedb-array/src/query/physical.rs @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Physical reads: every attr cell through its attr's metadata. +//! +//! A nodata cell reads as NULL. A numeric cell of an attr with a scale or +//! offset reads as `raw * scale + offset`, a `FLOAT64`. Every other cell +//! reads unchanged. See [`crate::schema::AttrMeta::physical_cell`]. + +use crate::schema::ArraySchema; +use crate::tile::sparse_tile::SparseTile; +use crate::types::cell_value::value::CellValue; + +/// `tile` with every attr column in schema order read physically. +pub fn physical_sparse(schema: &ArraySchema, mut tile: SparseTile) -> SparseTile { + for (col, attr) in tile.attr_cols.iter_mut().zip(&schema.attrs) { + if attr.meta.nodata.is_none() && attr.meta.transform().is_none() { + continue; + } + for cell in col.iter_mut() { + *cell = attr + .meta + .physical_cell(std::mem::replace(cell, CellValue::Null)); + } + } + tile +} + +/// One cell's attrs, in schema order, read physically. +pub fn physical_cells(schema: &ArraySchema, attrs: Vec) -> Vec { + attrs + .into_iter() + .zip(&schema.attrs) + .map(|(cell, attr)| attr.meta.physical_cell(cell)) + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::schema::ArraySchemaBuilder; + use crate::schema::attr_meta::AttrMeta; + use crate::schema::attr_spec::{AttrSpec, AttrType}; + use crate::schema::dim_spec::{DimSpec, DimType}; + use crate::tile::sparse_tile::SparseTileBuilder; + use crate::types::coord::value::CoordValue; + use crate::types::domain::{Domain, DomainBound}; + + fn schema() -> ArraySchema { + ArraySchemaBuilder::new("scene") + .dim(DimSpec::new( + "x", + DimType::Int64, + Domain::new(DomainBound::Int64(0), DomainBound::Int64(15)), + )) + .attr( + AttrSpec::new("red", AttrType::UInt16, true).with_meta(AttrMeta { + nodata: Some(0.0), + scale: Some(0.5), + offset: Some(1.0), + units: Some("reflectance".into()), + }), + ) + .attr(AttrSpec::new("qa", AttrType::UInt8, true)) + .tile_extents(vec![16]) + .build() + .unwrap() + } + + #[test] + fn nodata_reads_null_and_scaled_attrs_read_floats() { + let s = schema(); + let mut b = SparseTileBuilder::new(&s); + b.push( + &[CoordValue::Int64(0)], + &[CellValue::UInt16(0), CellValue::UInt8(3)], + ) + .unwrap(); + b.push( + &[CoordValue::Int64(1)], + &[CellValue::UInt16(4), CellValue::UInt8(0)], + ) + .unwrap(); + let t = physical_sparse(&s, b.build()); + assert_eq!( + t.attr_cols[0], + vec![CellValue::Null, CellValue::Float64(3.0)] + ); + assert_eq!( + t.attr_cols[1], + vec![CellValue::UInt8(3), CellValue::UInt8(0)] + ); + assert_eq!( + physical_cells(&s, vec![CellValue::UInt16(2), CellValue::UInt8(9)]), + vec![CellValue::Float64(2.0), CellValue::UInt8(9)] + ); + } +} diff --git a/nodedb-array/src/query/region.rs b/nodedb-array/src/query/region.rs index b41c2864b..d756e385d 100644 --- a/nodedb-array/src/query/region.rs +++ b/nodedb-array/src/query/region.rs @@ -18,7 +18,7 @@ //! * [`filter_region_owned`] — per-cell filter of one tile, through a //! [`PreparedRegion`] built once per scan. -use nodedb_spatial::crs::Transformer; +use nodedb_crs::Transformer; use nodedb_types::geometry::point_in_polygon; use super::slice::{Slice, cell_in_slice, filter_sparse, slice_sparse_owned, tile_overlaps_slice}; @@ -315,6 +315,7 @@ mod tests { /// A zone drawn in `zone_srid` over a world-mode array in `array_srid`. fn framed(array_srid: Srid, zone_srid: Srid, polygons: Vec) -> ArrayRegion { + let ctx = nodedb_crs::CrsContext::builtin_only(); ArrayRegion::new( Slice::default(), Some(PolygonZone { @@ -323,8 +324,8 @@ mod tests { polygons, frame: Some(ZoneFrame { transform: None, - array_srid, - zone_srid, + array_crs: (*ctx.resolve(array_srid).unwrap()).clone(), + zone_crs: (*ctx.resolve(zone_srid).unwrap()).clone(), dim_bbox: [-180.0, -90.0, 180.0, 90.0], }), }), @@ -334,8 +335,8 @@ mod tests { #[test] fn a_framed_zone_keeps_the_edge_and_hole_rules() { let r = framed( - Srid::Wgs84, - Srid::Wgs84, + Srid::WGS84, + Srid::WGS84, vec![ZonePolygon { exterior: square(0.0, 10.0), holes: vec![square(4.0, 6.0)], @@ -360,8 +361,8 @@ mod tests { #[test] fn a_cell_the_projection_cannot_map_is_outside() { let r = framed( - Srid::Wgs84, - Srid::Utm47N, + Srid::WGS84, + Srid::new(32647).unwrap(), vec![ZonePolygon { exterior: square(-1.0e7, 1.0e7), holes: Vec::new(), @@ -378,8 +379,8 @@ mod tests { #[test] fn a_framed_zone_prunes_by_its_dim_box() { let mut r = framed( - Srid::Wgs84, - Srid::Wgs84, + Srid::WGS84, + Srid::WGS84, vec![ZonePolygon { exterior: square(0.0, 50.0), holes: Vec::new(), diff --git a/nodedb-array/src/query/zone_frame.rs b/nodedb-array/src/query/zone_frame.rs index b94948cf2..1f731ae1c 100644 --- a/nodedb-array/src/query/zone_frame.rs +++ b/nodedb-array/src/query/zone_frame.rs @@ -8,8 +8,14 @@ //! casting runs there. A zone edge is straight in the zone SRID, so its image //! in dim space is curved. The dim box densifies each exterior edge by //! recursive bisection before it takes the bounds. +//! +//! The Control Plane resolves both CRS definitions when it builds the frame +//! and the frame carries them, so the Data Plane maps cells without a CRS +//! registry. + +use std::sync::Arc; -use nodedb_spatial::crs::Transformer; +use nodedb_crs::{CrsContext, CrsDef, Transformer, UnknownSrid}; use nodedb_types::geometry::Srid; use serde::{Deserialize, Serialize}; @@ -32,7 +38,7 @@ const WORLD_TOLERANCE_DIVISOR: f64 = 64.0; /// Offset from a pixel corner to its centre, in pixels. const PIXEL_CENTRE: f64 = 0.5; -/// The SRIDs and affine that carry a cell into the zone's SRID. +/// The CRS definitions and affine that carry a cell into the zone's SRID. #[derive( Debug, Clone, @@ -45,8 +51,10 @@ const PIXEL_CENTRE: f64 = 0.5; pub struct ZoneFrame { /// The georef affine. `None` means the dims hold world coordinates. pub transform: Option, - pub array_srid: Srid, - pub zone_srid: Srid, + /// The array's GEOREF CRS, resolved. + pub array_crs: CrsDef, + /// The zone's CRS, resolved. + pub zone_crs: CrsDef, /// `[min_x, min_y, max_x, max_y]` in dim coordinates. /// /// Every cell the zone holds lies inside it. Pruning reads it. Membership @@ -57,7 +65,15 @@ pub struct ZoneFrame { impl ZoneFrame { /// The cell-to-zone transformer. Build one per scan, never one per cell. pub fn cell_transformer(&self) -> Transformer { - Transformer::new(self.array_srid, self.zone_srid) + Transformer::new( + Arc::new(self.array_crs.clone()), + Arc::new(self.zone_crs.clone()), + ) + } + + /// The SRID of the zone's coordinates. + pub fn zone_srid(&self) -> Srid { + self.zone_crs.srid } /// Position in the zone SRID of the centre of the cell at dim @@ -81,7 +97,10 @@ impl ZoneFrame { }, None => [coord_to_f64(x)?, coord_to_f64(y)?], }; - transformer.apply(world).ok() + transformer + .apply([world[0], world[1], 0.0]) + .ok() + .map(|[zx, zy, _]| [zx, zy]) } } @@ -116,6 +135,11 @@ pub enum ZoneFrameError { /// The zone has no exterior position. SQLSTATE `22000`. #[error("zone on array '{array}' has no exterior position")] EmptyZone { array: String }, + + /// The zone SRID or the array's GEOREF SRID names no defined CRS. + /// SQLSTATE `22000`. + #[error("zone on array '{array}': {source}")] + Crs { array: String, source: UnknownSrid }, } impl ZoneFrameError { @@ -125,12 +149,14 @@ impl ZoneFrameError { ZoneFrameError::NoGeoref { array } | ZoneFrameError::VertexBudget { array, .. } | ZoneFrameError::OutOfDomain { array, .. } - | ZoneFrameError::EmptyZone { array } => array, + | ZoneFrameError::EmptyZone { array } + | ZoneFrameError::Crs { array, .. } => array, } } } /// Build the frame of `polygons`, drawn in `zone_srid`, over `schema`. +/// Both SRIDs resolve through `ctx`. /// /// The dim box is padded by twice the chord tolerance. An `INT64` dim rounds /// it outward to whole coordinates. @@ -138,14 +164,16 @@ pub fn build_zone_frame( schema: &ArraySchema, zone_srid: Srid, polygons: &[ZonePolygon], + ctx: &CrsContext<'_>, ) -> Result { - build_with_budget(schema, zone_srid, polygons, MAX_FRAME_VERTICES) + build_with_budget(schema, zone_srid, polygons, ctx, MAX_FRAME_VERTICES) } fn build_with_budget( schema: &ArraySchema, zone_srid: Srid, polygons: &[ZonePolygon], + ctx: &CrsContext<'_>, budget: usize, ) -> Result { let georef = schema @@ -154,13 +182,21 @@ fn build_with_budget( .ok_or_else(|| ZoneFrameError::NoGeoref { array: schema.name.clone(), })?; + let resolve = |srid: Srid| { + ctx.resolve(srid).map_err(|source| ZoneFrameError::Crs { + array: schema.name.clone(), + source, + }) + }; + let zone_crs = resolve(zone_srid)?; + let array_crs = resolve(georef.srid)?; let tolerance = match georef.transform { Some(_) => PIXEL_TOLERANCE, None => world_tolerance(schema, georef.x_dim, georef.y_dim), }; let mut walk = EdgeWalk { array: &schema.name, - transformer: Transformer::new(zone_srid, georef.srid), + transformer: Transformer::new(Arc::clone(&zone_crs), Arc::clone(&array_crs)), transform: georef.transform, zone_srid, tolerance, @@ -186,8 +222,8 @@ fn build_with_budget( } Ok(ZoneFrame { transform: georef.transform, - array_srid: georef.srid, - zone_srid, + array_crs: (*array_crs).clone(), + zone_crs: (*zone_crs).clone(), dim_bbox, }) } @@ -277,10 +313,11 @@ impl EdgeWalk<'_> { }); } self.mapped += 1; - let world = self + let [wx, wy, _] = self .transformer - .apply(p) + .apply([p[0], p[1], 0.0]) .map_err(|e| self.out_of_domain(p, e.to_string()))?; + let world = [wx, wy]; let dim = match &self.transform { None => world, Some(gt) => { @@ -326,10 +363,25 @@ mod tests { /// Pixel size of the UTM grids, metres. const PIXEL_M: f64 = 30.0; + fn ctx() -> CrsContext<'static> { + CrsContext::builtin_only() + } + + fn utm47n() -> Srid { + Srid::new(32647).unwrap() + } + + fn transformer(from: Srid, to: Srid) -> Transformer { + ctx().transformer(from, to).unwrap() + } + + fn apply2(t: &Transformer, [x, y]: [f64; 2]) -> [f64; 2] { + let [tx, ty, _] = t.apply([x, y, 0.0]).unwrap(); + [tx, ty] + } + fn utm(lon: f64, lat: f64) -> [f64; 2] { - Transformer::new(Srid::Wgs84, Srid::Utm47N) - .apply([lon, lat]) - .unwrap() + apply2(&transformer(Srid::WGS84, utm47n()), [lon, lat]) } fn pixel_schema(gt: [f64; 6]) -> ArraySchema { @@ -348,7 +400,7 @@ mod tests { .build() .unwrap() .with_georef(Some(ArrayGeoref { - srid: Srid::Utm47N, + srid: utm47n(), x_dim: 1, y_dim: 0, transform: Some(GeoTransform(gt)), @@ -404,7 +456,7 @@ mod tests { fn a_curved_edge_bounds_every_cell_inside_it() { let (schema, cols, rows) = grid_at_six_north(); let polygons = vec![rect(99.0, 6.0, 100.0, 6.5)]; - let frame = build_zone_frame(&schema, Srid::Wgs84, &polygons).unwrap(); + let frame = build_zone_frame(&schema, Srid::WGS84, &polygons, &ctx()).unwrap(); let zone = PolygonZone { x_dim: 1, y_dim: 0, @@ -437,7 +489,7 @@ mod tests { let (schema, cols, rows) = grid_at_six_north(); let gt = schema.georef.as_ref().unwrap().transform.unwrap(); let polygons = vec![rect(99.0, 6.0, 100.0, 6.5)]; - let frame = build_zone_frame(&schema, Srid::Wgs84, &polygons).unwrap(); + let frame = build_zone_frame(&schema, Srid::WGS84, &polygons, &ctx()).unwrap(); let zone = PolygonZone { x_dim: 1, y_dim: 0, @@ -450,13 +502,13 @@ mod tests { .iter() .map(|p| utm(p[0], p[1])) .collect(); - let to_lon_lat = Transformer::new(Srid::Utm47N, Srid::Wgs84); + let to_lon_lat = transformer(utm47n(), Srid::WGS84); let (mut differs, mut inside) = (0usize, 0usize); for row in 0..rows { for col in 0..cols { let exact = prepared.contains(&[CoordValue::Int64(row), CoordValue::Int64(col)]); let (x, y) = gt.cell_centre(col, row); - let [lon, lat] = to_lon_lat.apply([x, y]).unwrap(); + let [lon, lat] = apply2(&to_lon_lat, [x, y]); let truth = (99.0..100.0).contains(&lon) && (6.0..6.5).contains(&lat); assert_eq!(exact, truth, "cell ({row}, {col}) at ({lon}, {lat})"); let vertex_only = nodedb_types::geometry::point_in_polygon(x, y, &straight); @@ -476,8 +528,13 @@ mod tests { let (schema, _, _) = grid_at_six_north(); // The south edge straddles the central meridian at 99°E. In UTM the // parallel dips south at the meridian, between the two vertices. - let frame = - build_zone_frame(&schema, Srid::Wgs84, &[rect(98.5, 6.0, 99.5, 6.001)]).unwrap(); + let frame = build_zone_frame( + &schema, + Srid::WGS84, + &[rect(98.5, 6.0, 99.5, 6.001)], + &ctx(), + ) + .unwrap(); let gt = schema.georef.as_ref().unwrap().transform.unwrap(); let row_of = |lon: f64| { let p = utm(lon, 6.0); @@ -498,9 +555,14 @@ mod tests { #[test] fn a_world_frame_pads_by_the_tile_tolerance() { - let schema = world_schema(Srid::Wgs84, -180.0, 180.0, 64); - let frame = - build_zone_frame(&schema, Srid::Wgs84, &[rect(100.0, 2.0, 101.0, 3.0)]).unwrap(); + let schema = world_schema(Srid::WGS84, -180.0, 180.0, 64); + let frame = build_zone_frame( + &schema, + Srid::WGS84, + &[rect(100.0, 2.0, 101.0, 3.0)], + &ctx(), + ) + .unwrap(); // Same SRID: the edges map straight, and the pad is 2 · 64 / 64. assert_eq!(frame.dim_bbox, [98.0, 0.0, 103.0, 5.0]); assert_eq!(frame.transform, None); @@ -508,9 +570,9 @@ mod tests { #[test] fn a_zone_without_georef_is_refused() { - let mut schema = world_schema(Srid::Wgs84, 0.0, 10.0, 1); + let mut schema = world_schema(Srid::WGS84, 0.0, 10.0, 1); schema.georef = None; - let err = build_zone_frame(&schema, Srid::Wgs84, &[rect(1.0, 1.0, 2.0, 2.0)]) + let err = build_zone_frame(&schema, Srid::WGS84, &[rect(1.0, 1.0, 2.0, 2.0)], &ctx()) .expect_err("refused"); assert!(matches!(err, ZoneFrameError::NoGeoref { .. }), "{err:?}"); } @@ -518,8 +580,13 @@ mod tests { #[test] fn a_vertex_outside_the_projection_is_refused() { let (schema, _, _) = grid_at_six_north(); - let err = build_zone_frame(&schema, Srid::Wgs84, &[rect(150.0, 6.0, 151.0, 7.0)]) - .expect_err("refused"); + let err = build_zone_frame( + &schema, + Srid::WGS84, + &[rect(150.0, 6.0, 151.0, 7.0)], + &ctx(), + ) + .expect_err("refused"); assert!(matches!(err, ZoneFrameError::OutOfDomain { .. }), "{err:?}"); } @@ -527,8 +594,9 @@ mod tests { fn the_vertex_budget_is_enforced() { let (schema, _, _) = grid_at_six_north(); let polygons = [rect(99.0, 6.0, 100.0, 6.5)]; - assert!(build_with_budget(&schema, Srid::Wgs84, &polygons, 1 << 16).is_ok()); - let err = build_with_budget(&schema, Srid::Wgs84, &polygons, 8).expect_err("over budget"); + assert!(build_with_budget(&schema, Srid::WGS84, &polygons, &ctx(), 1 << 16).is_ok()); + let err = + build_with_budget(&schema, Srid::WGS84, &polygons, &ctx(), 8).expect_err("over budget"); assert_eq!( err, ZoneFrameError::VertexBudget { @@ -556,7 +624,7 @@ mod tests { ], holes: Vec::new(), }; - let err = build_zone_frame(&schema, Srid::Wgs84, &[ring]).expect_err("over budget"); + let err = build_zone_frame(&schema, Srid::WGS84, &[ring], &ctx()).expect_err("over budget"); assert!( matches!(err, ZoneFrameError::VertexBudget { .. }), "{err:?}" @@ -580,13 +648,13 @@ mod tests { fn every_inside_cell_lies_in_the_dim_bbox_for_random_zones() { let (schema, cols, rows) = grid_at_six_north(); let gt = schema.georef.as_ref().unwrap().transform.unwrap(); - let to_lon_lat = Transformer::new(Srid::Utm47N, Srid::Wgs84); + let to_lon_lat = transformer(utm47n(), Srid::WGS84); let mut rng = Lcg(0x5eed); for case in 0..24 { // A random quadrilateral whose corners fall inside the grid. let corner = |rng: &mut Lcg| { let (x, y) = gt.pixel_to_world(rng.next() * cols as f64, rng.next() * rows as f64); - to_lon_lat.apply([x, y]).unwrap() + apply2(&to_lon_lat, [x, y]) }; let a = corner(&mut rng); let b = corner(&mut rng); @@ -596,7 +664,7 @@ mod tests { exterior: vec![a, b, c, d, a], holes: Vec::new(), }]; - let frame = build_zone_frame(&schema, Srid::Wgs84, &polygons).unwrap(); + let frame = build_zone_frame(&schema, Srid::WGS84, &polygons, &ctx()).unwrap(); let [x0, y0, x1, y1] = frame.dim_bbox; let zone = PolygonZone { x_dim: 1, @@ -621,14 +689,34 @@ mod tests { #[test] fn a_frame_round_trips_through_msgpack() { + let ctx = ctx(); let frame = ZoneFrame { transform: Some(GeoTransform([1.0, 2.0, 0.0, 3.0, 0.0, -2.0])), - array_srid: Srid::Utm47N, - zone_srid: Srid::Wgs84, + array_crs: (*ctx.resolve(utm47n()).unwrap()).clone(), + zone_crs: (*ctx.resolve(Srid::WGS84).unwrap()).clone(), dim_bbox: [0.0, 1.0, 2.0, 3.0], }; let bytes = zerompk::to_msgpack_vec(&frame).unwrap(); let back: ZoneFrame = zerompk::from_msgpack(&bytes).unwrap(); assert_eq!(back, frame); } + + #[test] + fn an_undefined_zone_srid_is_refused() { + let (schema, _, _) = grid_at_six_north(); + let err = build_zone_frame( + &schema, + Srid::new(999).unwrap(), + &[rect(99.0, 6.0, 100.0, 6.5)], + &ctx(), + ) + .expect_err("refused"); + assert_eq!( + err, + ZoneFrameError::Crs { + array: "scene".into(), + source: UnknownSrid::Undefined { code: 999 } + } + ); + } } diff --git a/nodedb-array/src/schema/array_schema.rs b/nodedb-array/src/schema/array_schema.rs index abd394cf1..3550f3f5c 100644 --- a/nodedb-array/src/schema/array_schema.rs +++ b/nodedb-array/src/schema/array_schema.rs @@ -11,12 +11,13 @@ use serde::{Deserialize, Serialize}; -use super::attr_spec::AttrSpec; +use super::attr_meta::AttrMeta; +use super::attr_spec::{AttrSpec, AttrType}; use super::cell_order::{CellOrder, TileOrder}; use super::dim_spec::DimSpec; use super::georef::ArrayGeoref; use super::validation; -use crate::error::ArrayResult; +use crate::error::{ArrayError, ArrayResult}; /// Full array schema. #[derive( @@ -63,36 +64,77 @@ impl ArraySchema { self.attrs.iter().find(|a| a.name == name) } + /// This schema with `meta` in place of attr `attr`'s metadata, checked + /// against the attr type. + pub fn with_attr_meta(mut self, attr: &str, meta: AttrMeta) -> ArrayResult { + let array = self.name.clone(); + let spec = self + .attrs + .iter_mut() + .find(|a| a.name == attr) + .ok_or_else(|| ArrayError::InvalidAttr { + array: array.clone(), + attr: attr.to_string(), + detail: "no such attribute".to_string(), + })?; + meta.check(spec.dtype) + .map_err(|e| ArrayError::InvalidAttrMeta { + array, + attr: attr.to_string(), + detail: e.to_string(), + })?; + spec.meta = meta; + Ok(self) + } + /// MessagePack encoding of the schema's *content* fields — the parts /// that determine compatibility for cross-array element-wise ops. - /// Excludes the array `name` so two structurally-identical schemas - /// with different names produce identical bytes (and therefore - /// identical `schema_hash`). - pub fn content_msgpack(&self) -> Vec { - // Encode tuple of the structural fields. zerompk derives all - // cover the inner types. + /// Excludes the array `name`, the georef, and every attr's metadata, so + /// two structurally identical schemas produce identical bytes (and + /// therefore identical `schema_hash`). + pub fn content_msgpack(&self) -> ArrayResult> { + let attrs: Vec> = self + .attrs + .iter() + .map(|a| AttrContent { + name: &a.name, + dtype: a.dtype, + nullable: a.nullable, + }) + .collect(); let payload = SchemaContent { dims: &self.dims, - attrs: &self.attrs, + attrs, tile_extents: &self.tile_extents, cell_order: self.cell_order, tile_order: self.tile_order, }; - zerompk::to_msgpack_vec(&payload).unwrap_or_default() + zerompk::to_msgpack_vec(&payload).map_err(|e| ArrayError::SchemaEncode { + array: self.name.clone(), + detail: e.to_string(), + }) } } /// Borrowed view of the `ArraySchema` fields that participate in -/// content-only hashing (everything except `name`). +/// content-only hashing. #[derive(zerompk::ToMessagePack)] struct SchemaContent<'a> { dims: &'a Vec, - attrs: &'a Vec, + attrs: Vec>, tile_extents: &'a Vec, cell_order: CellOrder, tile_order: TileOrder, } +/// The content of one attr: everything but its metadata. +#[derive(zerompk::ToMessagePack)] +struct AttrContent<'a> { + name: &'a String, + dtype: AttrType, + nullable: bool, +} + #[cfg(test)] mod tests { use super::super::attr_spec::AttrType; @@ -121,7 +163,7 @@ mod tests { let a = mk("alpha"); let b = mk("beta"); assert_ne!(a.name, b.name); - assert_eq!(a.content_msgpack(), b.content_msgpack()); + assert_eq!(a.content_msgpack().unwrap(), b.content_msgpack().unwrap()); } #[test] @@ -173,7 +215,7 @@ mod tests { fn pixel_georef() -> ArrayGeoref { ArrayGeoref { - srid: nodedb_types::geometry::Srid::Utm47N, + srid: nodedb_types::geometry::Srid::new(32647).unwrap(), x_dim: 1, y_dim: 0, transform: Some(super::super::georef::GeoTransform([ @@ -187,7 +229,62 @@ mod tests { let plain = raster(); let geo = raster().with_georef(Some(pixel_georef())).unwrap(); assert_ne!(plain, geo); - assert_eq!(plain.content_msgpack(), geo.content_msgpack()); + assert_eq!( + plain.content_msgpack().unwrap(), + geo.content_msgpack().unwrap() + ); + } + + fn landsat_meta() -> AttrMeta { + AttrMeta { + nodata: Some(0.0), + scale: Some(0.0000275), + offset: Some(-0.2), + units: Some("reflectance".into()), + } + } + + #[test] + fn content_msgpack_ignores_attr_meta() { + let plain = raster(); + let with_meta = raster().with_attr_meta("b1", landsat_meta()).unwrap(); + assert_ne!(plain, with_meta); + assert_eq!( + plain.content_msgpack().unwrap(), + with_meta.content_msgpack().unwrap() + ); + let dropped = with_meta.with_attr_meta("b1", AttrMeta::default()).unwrap(); + assert_eq!(dropped, plain); + } + + #[test] + fn attr_meta_round_trips_and_is_checked() { + let with_meta = raster().with_attr_meta("b1", landsat_meta()).unwrap(); + let bytes = zerompk::to_msgpack_vec(&with_meta).unwrap(); + let back: ArraySchema = zerompk::from_msgpack(&bytes).unwrap(); + assert_eq!(back.attrs[0].meta, landsat_meta()); + let bad = AttrMeta { + nodata: Some(-1.0), + ..AttrMeta::default() + }; + assert!(matches!( + raster().with_attr_meta("b1", bad), + Err(ArrayError::InvalidAttrMeta { .. }) + )); + assert!(matches!( + raster().with_attr_meta("b9", landsat_meta()), + Err(ArrayError::InvalidAttr { .. }) + )); + } + + #[test] + fn content_change_changes_the_bytes() { + let mut narrowed = raster(); + narrowed.attrs[0] = AttrSpec::new("b1", AttrType::Int8, true); + assert_ne!( + raster().content_msgpack().unwrap(), + narrowed.content_msgpack().unwrap() + ); } #[test] diff --git a/nodedb-array/src/schema/attr_meta.rs b/nodedb-array/src/schema/attr_meta.rs new file mode 100644 index 000000000..00e521cf9 --- /dev/null +++ b/nodedb-array/src/schema/attr_meta.rs @@ -0,0 +1,291 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Reading metadata of one attribute: nodata, scale, offset, units. +//! +//! A stored cell holds the raw value. The physical value of a numeric cell +//! is `raw * scale + offset`, with a missing `scale` read as 1 and a +//! missing `offset` read as 0. A cell equal to `nodata` holds no data. +//! +//! The metadata stays out of [`super::ArraySchema::content_msgpack`], so +//! setting or dropping it never changes `schema_hash`. + +use serde::{Deserialize, Serialize}; + +use super::attr_spec::AttrType; +use crate::types::cell_value::value::CellValue; + +/// Optional reading metadata of an attribute. Every field `None` means +/// the attribute carries none. +#[derive( + Debug, Clone, Default, Serialize, Deserialize, zerompk::ToMessagePack, zerompk::FromMessagePack, +)] +pub struct AttrMeta { + /// The raw value that marks a cell as holding no data. + pub nodata: Option, + /// The factor a reader multiplies a raw value by. + pub scale: Option, + /// The term a reader adds after the scale. + pub offset: Option, + /// The unit of the physical value, such as `reflectance`. + pub units: Option, +} + +/// Bit equality, so a NaN nodata equals itself. +impl PartialEq for AttrMeta { + fn eq(&self, other: &Self) -> bool { + let bits = |v: Option| v.map(f64::to_bits); + bits(self.nodata) == bits(other.nodata) + && bits(self.scale) == bits(other.scale) + && bits(self.offset) == bits(other.offset) + && self.units == other.units + } +} + +impl Eq for AttrMeta {} + +impl AttrMeta { + /// Whether no field is set. + pub fn is_empty(&self) -> bool { + self.nodata.is_none() + && self.scale.is_none() + && self.offset.is_none() + && self.units.is_none() + } + + /// The `(scale, offset)` a reader applies, or `None` when neither is set. + pub fn transform(&self) -> Option<(f64, f64)> { + if self.scale.is_none() && self.offset.is_none() { + return None; + } + Some((self.scale.unwrap_or(1.0), self.offset.unwrap_or(0.0))) + } + + /// Whether `v` equals the nodata value. + /// + /// An integer cell matches a whole nodata exactly. A `FLOAT32` cell + /// matches the nearest `f32` of the nodata, as GDAL compares. NaN + /// matches NaN. NULL, `STRING` and `BYTES` cells never match. + pub fn is_nodata(&self, v: &CellValue) -> bool { + let Some(nodata) = self.nodata else { + return false; + }; + match v { + CellValue::Float64(f) => *f == nodata || (f.is_nan() && nodata.is_nan()), + CellValue::Float32(f) => { + let n = nodata as f32; + *f == n || (f.is_nan() && n.is_nan()) + } + CellValue::Int64(_) + | CellValue::Int8(_) + | CellValue::UInt8(_) + | CellValue::Int16(_) + | CellValue::UInt16(_) + | CellValue::Int32(_) + | CellValue::UInt32(_) => match (v.as_i64(), whole_i64(nodata)) { + (Some(cell), Some(n)) => cell == n, + _ => false, + }, + CellValue::String(_) | CellValue::Bytes(_) | CellValue::Null => false, + } + } + + /// The physical value of `v`: NULL for a nodata cell, `raw * scale + + /// offset` as `FLOAT64` when a transform is set, and `v` unchanged + /// otherwise. + pub fn physical_cell(&self, v: CellValue) -> CellValue { + if self.is_nodata(&v) { + return CellValue::Null; + } + match (self.transform(), v.as_f64()) { + (Some((scale, offset)), Some(raw)) => CellValue::Float64(raw * scale + offset), + _ => v, + } + } + + /// Check every set field against attribute type `dtype`. + /// + /// - `NODATA`, `SCALE` and `OFFSET` apply to numeric attributes only. + /// - `SCALE` is finite and non-zero. `OFFSET` is finite. + /// - `NODATA` is a value of `dtype`: a whole number in range for an + /// integer type, and a value inside the `f32` range (or NaN or an + /// infinity) for `FLOAT32`. + /// - `UNITS` is non-empty. + pub fn check(&self, dtype: AttrType) -> Result<(), AttrMetaError> { + let numeric = dtype.int_range().is_some() || dtype.is_float(); + if !numeric && (self.nodata.is_some() || self.scale.is_some() || self.offset.is_some()) { + return Err(AttrMetaError::NotNumeric { + dtype: dtype.sql_name(), + }); + } + if let Some(scale) = self.scale + && (!scale.is_finite() || scale == 0.0) + { + return Err(AttrMetaError::Scale { scale }); + } + if let Some(offset) = self.offset + && !offset.is_finite() + { + return Err(AttrMetaError::Offset { offset }); + } + if let Some(nodata) = self.nodata + && !nodata_fits(nodata, dtype) + { + return Err(AttrMetaError::Nodata { + nodata, + dtype: dtype.sql_name(), + }); + } + if self.units.as_deref().is_some_and(str::is_empty) { + return Err(AttrMetaError::EmptyUnits); + } + Ok(()) + } +} + +/// Why attr metadata does not fit its attribute. +#[derive(Debug, Clone, PartialEq, thiserror::Error)] +pub enum AttrMetaError { + #[error("NODATA, SCALE and OFFSET apply to numeric attributes, not {dtype}")] + NotNumeric { dtype: &'static str }, + #[error("SCALE {scale} must be finite and non-zero")] + Scale { scale: f64 }, + #[error("OFFSET {offset} must be finite")] + Offset { offset: f64 }, + #[error("NODATA {nodata} is not a {dtype} value, so no cell can match it")] + Nodata { nodata: f64, dtype: &'static str }, + #[error("UNITS must not be empty; drop it with DROP UNITS")] + EmptyUnits, +} + +/// `v` as an `i64` when it is a whole number inside the `i64` range. +fn whole_i64(v: f64) -> Option { + // 2^63 is exact in f64. Every whole f64 in [-2^63, 2^63) is an i64. + const TWO_POW_63: f64 = 9_223_372_036_854_775_808.0; + (v.fract() == 0.0 && (-TWO_POW_63..TWO_POW_63).contains(&v)).then_some(v as i64) +} + +/// Whether `nodata` is a value of `dtype`. +fn nodata_fits(nodata: f64, dtype: AttrType) -> bool { + match dtype { + AttrType::Float64 => true, + AttrType::Float32 => !nodata.is_finite() || nodata.abs() <= f64::from(f32::MAX), + AttrType::Int64 + | AttrType::Int8 + | AttrType::UInt8 + | AttrType::Int16 + | AttrType::UInt16 + | AttrType::Int32 + | AttrType::UInt32 => match (whole_i64(nodata), dtype.int_range()) { + (Some(n), Some((lo, hi))) => lo <= n && n <= hi, + _ => false, + }, + AttrType::String | AttrType::Bytes => false, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn meta(nodata: Option, scale: Option, offset: Option) -> AttrMeta { + AttrMeta { + nodata, + scale, + offset, + units: None, + } + } + + #[test] + fn nodata_must_be_a_value_of_the_type() { + assert!(meta(Some(0.0), None, None).check(AttrType::UInt16).is_ok()); + assert!(meta(Some(-128.0), None, None).check(AttrType::Int8).is_ok()); + assert!( + meta(Some(f64::NAN), None, None) + .check(AttrType::Float32) + .is_ok() + ); + for (nodata, dtype) in [ + (-1.0, AttrType::UInt16), + (65_536.0, AttrType::UInt16), + (0.5, AttrType::UInt8), + (128.0, AttrType::Int8), + (f64::NAN, AttrType::Int32), + (1e39, AttrType::Float32), + (9_223_372_036_854_775_808.0, AttrType::Int64), + ] { + assert!( + meta(Some(nodata), None, None).check(dtype).is_err(), + "{nodata} as {dtype:?}" + ); + } + } + + #[test] + fn scale_offset_and_units_are_checked() { + assert!(meta(None, Some(0.0), None).check(AttrType::Int16).is_err()); + assert!( + meta(None, Some(f64::INFINITY), None) + .check(AttrType::Int16) + .is_err() + ); + assert!( + meta(None, None, Some(f64::NAN)) + .check(AttrType::Int16) + .is_err() + ); + assert!(meta(Some(0.0), None, None).check(AttrType::String).is_err()); + let empty_units = AttrMeta { + units: Some(String::new()), + ..AttrMeta::default() + }; + assert!(empty_units.check(AttrType::Float64).is_err()); + let text_units = AttrMeta { + units: Some("label".into()), + ..AttrMeta::default() + }; + assert!(text_units.check(AttrType::String).is_ok()); + } + + #[test] + fn nodata_matches_by_type() { + let ints = meta(Some(0.0), None, None); + assert!(ints.is_nodata(&CellValue::UInt16(0))); + assert!(!ints.is_nodata(&CellValue::UInt16(1))); + assert!(!ints.is_nodata(&CellValue::Null)); + let neg = meta(Some(-9999.0), None, None); + assert!(neg.is_nodata(&CellValue::Int16(-9999))); + assert!(neg.is_nodata(&CellValue::Float32(-9999.0))); + let tenth = meta(Some(0.1), None, None); + assert!(tenth.is_nodata(&CellValue::Float32(0.1))); + assert!(!tenth.is_nodata(&CellValue::Int64(0))); + let nan = meta(Some(f64::NAN), None, None); + assert!(nan.is_nodata(&CellValue::Float64(f64::NAN))); + assert!(nan.is_nodata(&CellValue::Float32(f32::NAN))); + assert!(!nan.is_nodata(&CellValue::Float64(1.0))); + } + + #[test] + fn physical_cells_apply_scale_and_offset() { + let landsat = meta(Some(0.0), Some(0.0000275), Some(-0.2)); + assert_eq!(landsat.physical_cell(CellValue::UInt16(0)), CellValue::Null); + let CellValue::Float64(v) = landsat.physical_cell(CellValue::UInt16(10_000)) else { + panic!("a scaled cell is FLOAT64"); + }; + assert!((v - 0.075).abs() < 1e-12, "{v}"); + let plain = meta(Some(0.0), None, None); + assert_eq!( + plain.physical_cell(CellValue::UInt16(7)), + CellValue::UInt16(7) + ); + assert_eq!(plain.physical_cell(CellValue::Null), CellValue::Null); + } + + #[test] + fn nan_nodata_equals_itself() { + let a = meta(Some(f64::NAN), None, None); + assert_eq!(a, a.clone()); + assert_ne!(a, meta(None, None, None)); + assert!(AttrMeta::default().is_empty()); + } +} diff --git a/nodedb-array/src/schema/attr_spec.rs b/nodedb-array/src/schema/attr_spec.rs index fdb9add65..fe0ae0543 100644 --- a/nodedb-array/src/schema/attr_spec.rs +++ b/nodedb-array/src/schema/attr_spec.rs @@ -4,6 +4,8 @@ use serde::{Deserialize, Serialize}; +use super::attr_meta::AttrMeta; + /// Attribute type tag. One-to-one with /// [`crate::types::cell_value::value::CellValue`] (excluding `Null`, /// which is per-cell, not per-attribute). @@ -38,6 +40,10 @@ pub enum AttrType { #[serde(rename = "uint32")] UInt32, Float32, + /// A 1-byte signed integer. The array DDL names widths in bits, so + /// `INT8` is this type, not PostgreSQL's 8-byte `int8`. + #[serde(rename = "int8")] + Int8, } impl AttrType { @@ -54,6 +60,7 @@ impl AttrType { AttrType::Int32 => "INT32", AttrType::UInt32 => "UINT32", AttrType::Float32 => "FLOAT32", + AttrType::Int8 => "INT8", } } @@ -62,6 +69,7 @@ impl AttrType { pub fn int_range(self) -> Option<(i64, i64)> { match self { AttrType::Int64 => Some((i64::MIN, i64::MAX)), + AttrType::Int8 => Some((i64::from(i8::MIN), i64::from(i8::MAX))), AttrType::UInt8 => Some((0, i64::from(u8::MAX))), AttrType::Int16 => Some((i64::from(i16::MIN), i64::from(i16::MAX))), AttrType::UInt16 => Some((0, i64::from(u16::MAX))), @@ -76,6 +84,7 @@ impl AttrType { match self { AttrType::Float64 | AttrType::Float32 => true, AttrType::Int64 + | AttrType::Int8 | AttrType::UInt8 | AttrType::Int16 | AttrType::UInt16 @@ -103,6 +112,10 @@ pub struct AttrSpec { pub dtype: AttrType, /// Whether the attribute may carry [`crate::types::CellValue::Null`]. pub nullable: bool, + /// Nodata, scale, offset and units. Not part of + /// [`super::ArraySchema::content_msgpack`], so it never changes + /// `schema_hash`. + pub meta: AttrMeta, } impl AttrSpec { @@ -111,8 +124,14 @@ impl AttrSpec { name: name.into(), dtype, nullable, + meta: AttrMeta::default(), } } + + /// This attribute with `meta` in place of its current metadata. + pub fn with_meta(self, meta: AttrMeta) -> Self { + Self { meta, ..self } + } } #[cfg(test)] @@ -129,6 +148,8 @@ mod tests { #[test] fn integer_ranges_match_their_widths() { assert_eq!(AttrType::UInt8.int_range(), Some((0, 255))); + assert_eq!(AttrType::Int8.int_range(), Some((-128, 127))); + assert_eq!(AttrType::Int8.sql_name(), "INT8"); assert_eq!(AttrType::Int16.int_range(), Some((-32768, 32767))); assert_eq!(AttrType::UInt16.int_range(), Some((0, 65535))); assert_eq!(AttrType::UInt32.int_range(), Some((0, 4_294_967_295))); diff --git a/nodedb-array/src/schema/builder.rs b/nodedb-array/src/schema/builder.rs index 2d2c84792..42cd1db15 100644 --- a/nodedb-array/src/schema/builder.rs +++ b/nodedb-array/src/schema/builder.rs @@ -151,7 +151,7 @@ mod tests { fn georef(transform: Option<[f64; 6]>) -> ArrayGeoref { ArrayGeoref { - srid: nodedb_types::geometry::Srid::Wgs84, + srid: nodedb_types::geometry::Srid::WGS84, x_dim: 1, y_dim: 0, transform: transform.map(super::super::georef::GeoTransform), diff --git a/nodedb-array/src/schema/georef.rs b/nodedb-array/src/schema/georef.rs index a890c135f..1dbd9517d 100644 --- a/nodedb-array/src/schema/georef.rs +++ b/nodedb-array/src/schema/georef.rs @@ -6,89 +6,10 @@ //! The georef stays out of [`super::ArraySchema::content_msgpack`]. Adding, //! changing, or removing it leaves `schema_hash` and every segment as they are. +pub use nodedb_crs::GeoTransform; use nodedb_types::geometry::Srid; use serde::{Deserialize, Serialize}; -/// Affine pixel-to-world transform in GDAL coefficient order. -/// -/// For a continuous pixel position `(u, v)`, `u` along the column axis and -/// `v` along the row axis: -/// -/// - `X = GT0 + u·GT1 + v·GT2` -/// - `Y = GT3 + u·GT4 + v·GT5` -/// -/// Pixel `(i, j)` covers `[i, i+1) × [j, j+1)`. Its centre is `(i+0.5, j+0.5)`. -#[derive( - Debug, Clone, Copy, Serialize, Deserialize, zerompk::ToMessagePack, zerompk::FromMessagePack, -)] -pub struct GeoTransform(pub [f64; 6]); - -impl PartialEq for GeoTransform { - /// Bitwise equality. Validation admits finite coefficients only, so - /// this is equality of the stored values. - fn eq(&self, other: &Self) -> bool { - self.0 - .iter() - .zip(other.0.iter()) - .all(|(a, b)| a.to_bits() == b.to_bits()) - } -} - -impl Eq for GeoTransform {} - -impl GeoTransform { - /// `GT1·GT5 − GT2·GT4`. Zero means the transform has no inverse. - pub fn determinant(&self) -> f64 { - let [_, gt1, gt2, _, gt4, gt5] = self.0; - gt1 * gt5 - gt2 * gt4 - } - - /// World position of the continuous pixel position `(u, v)`. - pub fn pixel_to_world(&self, u: f64, v: f64) -> (f64, f64) { - let [gt0, gt1, gt2, gt3, gt4, gt5] = self.0; - (gt0 + u * gt1 + v * gt2, gt3 + u * gt4 + v * gt5) - } - - /// World position of the centre of pixel `(i, j)`. - pub fn cell_centre(&self, i: i64, j: i64) -> (f64, f64) { - self.pixel_to_world(i as f64 + 0.5, j as f64 + 0.5) - } - - /// Continuous pixel position of the world position `(x, y)`. - /// - /// Returns `None` when the determinant is zero or not finite. - pub fn world_to_pixel(&self, x: f64, y: f64) -> Option<(f64, f64)> { - let det = self.determinant(); - if det == 0.0 || !det.is_finite() { - return None; - } - let [gt0, gt1, gt2, gt3, gt4, gt5] = self.0; - let dx = x - gt0; - let dy = y - gt3; - Some(((gt5 * dx - gt2 * dy) / det, (gt1 * dy - gt4 * dx) / det)) - } - - /// Index of the pixel that holds the world position `(x, y)`. - /// - /// Returns `None` when the transform has no inverse or the index falls - /// outside `i64`. - pub fn world_to_cell(&self, x: f64, y: f64) -> Option<(i64, i64)> { - let (u, v) = self.world_to_pixel(x, y)?; - Some((floor_to_i64(u)?, floor_to_i64(v)?)) - } -} - -/// `floor(value)` as `i64`. `None` for a non-finite or out-of-range value. -fn floor_to_i64(value: f64) -> Option { - let floored = value.floor(); - // `i64::MAX as f64` rounds up to 2^63, which is outside `i64`. - if floored.is_finite() && floored >= i64::MIN as f64 && floored < i64::MAX as f64 { - Some(floored as i64) - } else { - None - } -} - /// Georeference of an array. /// /// `x_dim` and `y_dim` index into [`super::ArraySchema::dims`]. @@ -118,78 +39,10 @@ pub struct ArrayGeoref { mod tests { use super::*; - const EPS: f64 = 1e-9; - - fn assert_close(a: (f64, f64), b: (f64, f64), eps: f64) { - assert!( - (a.0 - b.0).abs() <= eps && (a.1 - b.1).abs() <= eps, - "{a:?} != {b:?}" - ); - } - - #[test] - fn north_up_centre_is_index_plus_half() { - let gt = GeoTransform([100_000.0, 10.0, 0.0, 200_000.0, 0.0, -10.0]); - assert_close(gt.cell_centre(0, 0), (100_005.0, 199_995.0), EPS); - assert_close(gt.cell_centre(3, 2), (100_035.0, 199_975.0), EPS); - } - - #[test] - fn centre_inverse_round_trip_north_up() { - let gt = GeoTransform([100_000.0, 10.0, 0.0, 200_000.0, 0.0, -10.0]); - for (i, j) in [(0, 0), (7, 3), (255, 191), (-4, 9)] { - let (x, y) = gt.cell_centre(i, j); - let (u, v) = gt.world_to_pixel(x, y).unwrap(); - assert_close((u, v), (i as f64 + 0.5, j as f64 + 0.5), EPS); - assert_eq!(gt.world_to_cell(x, y), Some((i, j))); - } - } - - #[test] - fn centre_inverse_round_trip_rotated() { - let gt = GeoTransform([500_000.0, 8.660254, 5.0, 4_000_000.0, 5.0, -8.660254]); - assert!(gt.determinant().abs() > 0.0); - for (i, j) in [(0, 0), (12, 40), (1023, 511)] { - let (x, y) = gt.cell_centre(i, j); - let (u, v) = gt.world_to_pixel(x, y).unwrap(); - assert_close((u, v), (i as f64 + 0.5, j as f64 + 0.5), 1e-6); - assert_eq!(gt.world_to_cell(x, y), Some((i, j))); - } - } - - #[test] - fn centre_inverse_round_trip_geographic_exponent() { - let res = 8.983152841195214e-05; - let gt = GeoTransform([100.5, res, 0.0, 6.5, 0.0, -res]); - let (x, y) = gt.cell_centre(0, 0); - assert_close((x, y), (100.5 + res / 2.0, 6.5 - res / 2.0), 1e-15); - for (i, j) in [(0, 0), (100, 200), (11_131, 5_000)] { - let (x, y) = gt.cell_centre(i, j); - let (u, v) = gt.world_to_pixel(x, y).unwrap(); - assert_close((u, v), (i as f64 + 0.5, j as f64 + 0.5), 1e-6); - assert_eq!(gt.world_to_cell(x, y), Some((i, j))); - } - } - - #[test] - fn singular_transform_has_no_inverse() { - let gt = GeoTransform([0.0, 1.0, 2.0, 0.0, 2.0, 4.0]); - assert_eq!(gt.determinant(), 0.0); - assert!(gt.world_to_pixel(1.0, 1.0).is_none()); - assert!(gt.world_to_cell(1.0, 1.0).is_none()); - } - - #[test] - fn out_of_range_index_is_none() { - let gt = GeoTransform([0.0, 1e-10, 0.0, 0.0, 0.0, -1e-10]); - assert!(gt.world_to_pixel(1e10, -1.0).is_some()); - assert!(gt.world_to_cell(1e10, -1.0).is_none()); - } - #[test] fn georef_msgpack_round_trip() { let g = ArrayGeoref { - srid: Srid::Utm47N, + srid: Srid::new(32647).unwrap(), x_dim: 1, y_dim: 0, transform: Some(GeoTransform([600_000.0, 10.0, 0.0, 700_000.0, 0.0, -10.0])), @@ -199,7 +52,7 @@ mod tests { assert_eq!(back, g); let world = ArrayGeoref { - srid: Srid::Wgs84, + srid: Srid::WGS84, x_dim: 0, y_dim: 1, transform: None, diff --git a/nodedb-array/src/schema/mod.rs b/nodedb-array/src/schema/mod.rs index c2096d48c..276c45238 100644 --- a/nodedb-array/src/schema/mod.rs +++ b/nodedb-array/src/schema/mod.rs @@ -1,6 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 pub mod array_schema; +pub mod attr_meta; pub mod attr_spec; pub mod builder; pub mod cell_check; @@ -10,6 +11,7 @@ pub mod georef; pub mod validation; pub use array_schema::ArraySchema; +pub use attr_meta::{AttrMeta, AttrMetaError}; pub use attr_spec::{AttrSpec, AttrType}; pub use builder::ArraySchemaBuilder; pub use cell_order::{CellOrder, TileOrder}; diff --git a/nodedb-array/src/schema/validation/attrs.rs b/nodedb-array/src/schema/validation/attrs.rs index 82b8bbdc4..13d02f3f3 100644 --- a/nodedb-array/src/schema/validation/attrs.rs +++ b/nodedb-array/src/schema/validation/attrs.rs @@ -30,6 +30,13 @@ pub fn check(array: &str, attrs: &[AttrSpec]) -> ArrayResult<()> { detail: "duplicate attribute name".to_string(), }); } + a.meta + .check(a.dtype) + .map_err(|e| ArrayError::InvalidAttrMeta { + array: array.to_string(), + attr: a.name.clone(), + detail: e.to_string(), + })?; } Ok(()) } @@ -53,6 +60,21 @@ mod tests { assert!(check("a", &attrs).is_err()); } + #[test] + fn rejects_unrepresentable_nodata() { + let attrs = + vec![ + AttrSpec::new("b1", AttrType::UInt8, true).with_meta(crate::schema::AttrMeta { + nodata: Some(256.0), + ..crate::schema::AttrMeta::default() + }), + ]; + assert!(matches!( + check("a", &attrs), + Err(ArrayError::InvalidAttrMeta { .. }) + )); + } + #[test] fn accepts_well_formed_attrs() { let attrs = vec![ diff --git a/nodedb-array/src/schema/validation/georef.rs b/nodedb-array/src/schema/validation/georef.rs index 401446645..203251c87 100644 --- a/nodedb-array/src/schema/validation/georef.rs +++ b/nodedb-array/src/schema/validation/georef.rs @@ -109,7 +109,7 @@ mod tests { fn georef(x: u32, y: u32, transform: Option<[f64; 6]>) -> ArrayGeoref { ArrayGeoref { - srid: Srid::Utm47N, + srid: Srid::new(32647).unwrap(), x_dim: x, y_dim: y, transform: transform.map(GeoTransform), diff --git a/nodedb-array/src/tile/mbr.rs b/nodedb-array/src/tile/mbr.rs index 81e050e03..8fa64bc65 100644 --- a/nodedb-array/src/tile/mbr.rs +++ b/nodedb-array/src/tile/mbr.rs @@ -133,6 +133,7 @@ impl MbrBuilder { CellValue::Null => self.attr_nulls[i] += 1, CellValue::Int64(_) | CellValue::Float64(_) + | CellValue::Int8(_) | CellValue::UInt8(_) | CellValue::Int16(_) | CellValue::UInt16(_) diff --git a/nodedb-array/src/types/cell_value/narrow.rs b/nodedb-array/src/types/cell_value/narrow.rs index 2744b24c0..0ffe02794 100644 --- a/nodedb-array/src/types/cell_value/narrow.rs +++ b/nodedb-array/src/types/cell_value/narrow.rs @@ -54,6 +54,9 @@ impl CellValue { }; match dtype { AttrType::Int64 => Ok(CellValue::Int64(v)), + AttrType::Int8 => i8::try_from(v) + .map(CellValue::Int8) + .map_err(|_| out_of_range()), AttrType::UInt8 => u8::try_from(v) .map(CellValue::UInt8) .map_err(|_| out_of_range()), @@ -93,6 +96,7 @@ impl CellValue { Ok(CellValue::Float32(narrowed)) } AttrType::Int64 + | AttrType::Int8 | AttrType::UInt8 | AttrType::Int16 | AttrType::UInt16 @@ -129,6 +133,10 @@ mod tests { CellValue::from_int_literal(4_294_967_295, AttrType::UInt32), Ok(CellValue::UInt32(u32::MAX)) ); + assert_eq!( + CellValue::from_int_literal(-128, AttrType::Int8), + Ok(CellValue::Int8(-128)) + ); assert_eq!( CellValue::from_int_literal(3, AttrType::Float32), Ok(CellValue::Float32(3.0)) @@ -147,6 +155,8 @@ mod tests { (2_147_483_648, AttrType::Int32), (4_294_967_296, AttrType::UInt32), (-1, AttrType::UInt32), + (128, AttrType::Int8), + (-129, AttrType::Int8), ]; for (v, dtype) in cases { assert!( diff --git a/nodedb-array/src/types/cell_value/value.rs b/nodedb-array/src/types/cell_value/value.rs index 659fc1295..52dedab84 100644 --- a/nodedb-array/src/types/cell_value/value.rs +++ b/nodedb-array/src/types/cell_value/value.rs @@ -40,6 +40,7 @@ pub enum CellValue { Int32(i32), UInt32(u32), Float32(f32), + Int8(i8), } impl CellValue { @@ -61,6 +62,7 @@ impl CellValue { CellValue::Int32(_) => Some(AttrType::Int32), CellValue::UInt32(_) => Some(AttrType::UInt32), CellValue::Float32(_) => Some(AttrType::Float32), + CellValue::Int8(_) => Some(AttrType::Int8), } } @@ -69,6 +71,7 @@ impl CellValue { pub fn as_i64(&self) -> Option { match self { CellValue::Int64(v) => Some(*v), + CellValue::Int8(v) => Some(i64::from(*v)), CellValue::UInt8(v) => Some(i64::from(*v)), CellValue::Int16(v) => Some(i64::from(*v)), CellValue::UInt16(v) => Some(i64::from(*v)), @@ -90,6 +93,7 @@ impl CellValue { CellValue::Float64(v) => Some(*v), CellValue::Float32(v) => Some(widen_f32(*v)), CellValue::Int64(_) + | CellValue::Int8(_) | CellValue::UInt8(_) | CellValue::Int16(_) | CellValue::UInt16(_) @@ -127,6 +131,8 @@ mod tests { assert_eq!(CellValue::Float32(0.5).as_float(), Some(0.5)); assert_eq!(CellValue::Float32(0.5).as_i64(), None); assert_eq!(CellValue::UInt8(1).attr_type(), Some(AttrType::UInt8)); + assert_eq!(CellValue::Int8(-1).attr_type(), Some(AttrType::Int8)); + assert_eq!(CellValue::Int8(-128).as_i64(), Some(-128)); assert_eq!(CellValue::Null.attr_type(), None); } } diff --git a/nodedb-cluster-tests/Cargo.toml b/nodedb-cluster-tests/Cargo.toml index db1073ec4..4968254e7 100644 --- a/nodedb-cluster-tests/Cargo.toml +++ b/nodedb-cluster-tests/Cargo.toml @@ -27,7 +27,9 @@ nodedb-array = { workspace = true } nodedb-bridge = { workspace = true } nodedb-client = { workspace = true, features = ["native"] } nodedb-cluster = { workspace = true } +nodedb-columnar = { workspace = true } nodedb-crdt = { workspace = true } +nodedb-crs = { workspace = true } nodedb-physical = { workspace = true } nodedb-query = { workspace = true } nodedb-raft = { workspace = true } diff --git a/nodedb-cluster-tests/tests/common_suite/cases/bitemporal_array_cluster_cut.rs b/nodedb-cluster-tests/tests/common_suite/cases/bitemporal_array_cluster_cut.rs new file mode 100644 index 000000000..40579299e --- /dev/null +++ b/nodedb-cluster-tests/tests/common_suite/cases/bitemporal_array_cluster_cut.rs @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! A cluster array aggregate reads one pinned system-time cut. +//! +//! `ARRAY_AGG ... AS OF SYSTEM TIME` pins to one ordinal under the +//! statement's read cut. The pinned scope, with its cut marker, rides each +//! shard request, and every serving node waits for the marker before it +//! reads. A grouped aggregate run on any node while a writer appends cells +//! across every shard reads a prefix of the writer's history, and a past +//! cut reads the same groups on every node and every run. + +use std::collections::BTreeSet; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::time::Duration; + +use crate::common::cluster_harness::{TestCluster, TestClusterNode}; + +/// Cells the writer appends, one per `x`. Tiles of extent 4 spread them over +/// 16 tiles and many vShards. +const CELLS: i64 = 64; +/// Cells written before the past cut. +const BEFORE_CUT: i64 = 16; + +fn now_ms() -> i64 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("clock after the epoch") + .as_millis() as i64 +} + +/// A cut one day past the wall clock. +fn future_ms() -> i64 { + now_ms() + 86_400_000 +} + +async fn write_cell(node: &TestClusterNode, x: i64) { + let sql = format!("INSERT INTO ARRAY bac_cut COORDS ({x}) VALUES ({x})"); + node.exec(&sql) + .await + .unwrap_or_else(|e| panic!("{sql}: {e}")); +} + +/// The `x` groups a grouped count reads through `node` at `cut`. Every +/// group counts one cell. +async fn groups_at(node: &TestClusterNode, cut: i64) -> BTreeSet { + let sql = + format!("SELECT * FROM ARRAY_AGG('bac_cut', 'v', 'count', 'x') AS OF SYSTEM TIME {cut}"); + let messages = node + .client + .simple_query(&sql) + .await + .unwrap_or_else(|e| panic!("{sql}: {e}")); + messages + .iter() + .filter_map(|message| match message { + tokio_postgres::SimpleQueryMessage::Row(row) => { + let group = row.try_get("group").ok().flatten()?; + let count: f64 = row + .get("result") + .and_then(|r| r.parse().ok()) + .unwrap_or_else(|| panic!("{sql}: group {group} has no count")); + assert_eq!(count, 1.0, "{sql}: group {group} counts one cell"); + Some(group.parse().expect("group")) + } + _ => None, + }) + .collect() +} + +/// Check that `groups` is a prefix `0..n` of the writer's history. +fn assert_prefix(groups: &BTreeSet, stage: &str) { + let expected: BTreeSet = (0..groups.len() as i64).collect(); + assert_eq!( + groups, &expected, + "{stage}: the aggregate read a hole in the writer's history, so its shards read \ + different cuts" + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 6)] +async fn a_cluster_array_aggregate_reads_one_pinned_cut() { + let cluster = TestCluster::spawn_three() + .await + .expect("3-node cluster spawn"); + cluster + .exec_ddl_on_any_leader(&format!( + "CREATE ARRAY bac_cut DIMS (x INT64 [0..{}]) ATTRS (v INT64) TILE_EXTENTS (4)", + CELLS - 1 + )) + .await + .expect("CREATE ARRAY bac_cut"); + + for x in 0..BEFORE_CUT { + write_cell(&cluster.nodes[0], x).await; + } + tokio::time::sleep(Duration::from_millis(50)).await; + let past_cut = now_ms(); + tokio::time::sleep(Duration::from_millis(50)).await; + + let done = AtomicBool::new(false); + let writer = async { + for x in BEFORE_CUT..CELLS { + write_cell(&cluster.nodes[0], x).await; + } + done.store(true, Ordering::SeqCst); + }; + let reader = async { + let mut reads = 0usize; + while !done.load(Ordering::SeqCst) { + let node = &cluster.nodes[reads % cluster.nodes.len()]; + let groups = groups_at(node, future_ms()).await; + assert_prefix( + &groups, + &format!("future cut through node {}", node.node_id), + ); + reads += 1; + } + reads + }; + let ((), reads) = tokio::join!(writer, reader); + assert!(reads > 0, "the aggregate ran while the writer wrote"); + + let before: BTreeSet = (0..BEFORE_CUT).collect(); + let all: BTreeSet = (0..CELLS).collect(); + for node in &cluster.nodes { + assert_eq!( + groups_at(node, future_ms()).await, + all, + "node {}: every cell is under a future cut after the writer ends", + node.node_id + ); + for run in 0..2 { + assert_eq!( + groups_at(node, past_cut).await, + before, + "node {}, run {run}: a past cut reads the cells written before it", + node.node_id + ); + } + } + + cluster.shutdown().await; +} diff --git a/nodedb-cluster-tests/tests/common_suite/cases/bitemporal_replica_stamps.rs b/nodedb-cluster-tests/tests/common_suite/cases/bitemporal_replica_stamps.rs new file mode 100644 index 000000000..31b1be210 --- /dev/null +++ b/nodedb-cluster-tests/tests/common_suite/cases/bitemporal_replica_stamps.rs @@ -0,0 +1,419 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! A bitemporal version written outside Calvin has one system time on every +//! replica. +//! +//! A single-key autocommit write replicates through its data group's Raft +//! log. The group's leader stamps the entry with an ordinal of its clock as +//! it admits it, and every replica installs that ordinal. Each node's own +//! stored document version keys and columnar `_ts_system` cells then agree +//! ordinal for ordinal. After the leader stops, the new leader answers an +//! `AS OF` at each version's system time with the rows the old leader +//! answered. + +use std::collections::BTreeSet; +use std::time::Duration; + +use nodedb::types::TenantDataSnapshot; +use nodedb_types::{DatabaseId, TenantId}; + +use crate::common::cluster_harness::{TestCluster, TestClusterNode, read_once_a_leader_exists}; + +const DOCS: &str = "brs_docs"; +const COLS: &str = "brs_cols"; +const UPDATED: &str = "brs_updated"; +const DELETED: &str = "brs_deleted"; +const TENANT: u64 = 1; + +/// Every cell of every row `sql` returns through `node`, one string per row, +/// sorted. +pub(super) async fn rows(node: &TestClusterNode, sql: &str) -> Vec { + let messages = read_once_a_leader_exists( + sql, + Duration::from_secs(30), + Duration::from_millis(100), + || node.client.simple_query(sql), + ) + .await; + let mut out: Vec = messages + .iter() + .filter_map(|message| match message { + tokio_postgres::SimpleQueryMessage::Row(row) => Some( + (0..row.len()) + .map(|i| row.get(i).unwrap_or("NULL")) + .collect::>() + .join("|"), + ), + _ => None, + }) + .collect(); + out.sort(); + out +} + +/// Every version key of `DOCS` the cores of `node` store. The key ends in +/// the version's raw system-time ordinal. +fn doc_version_keys(snapshots: &[TenantDataSnapshot]) -> BTreeSet { + let marker = format!(":{DOCS}:"); + snapshots + .iter() + .flat_map(|snap| snap.documents_versioned.iter().map(|(key, _)| key)) + .filter(|key| key.contains(&marker)) + .cloned() + .collect() +} + +/// Every row of columnar collection `coll` the cores of `node` hold in +/// their memtables, `_ts_system` ordinal included. +fn columnar_rows(snapshots: &[TenantDataSnapshot], coll: &str) -> BTreeSet { + let suffix = format!(":{coll}"); + let mut out = BTreeSet::new(); + for (key, bytes) in snapshots + .iter() + .flat_map(|snap| snap.columnar_engines.iter()) + .filter(|(key, _)| key.ends_with(&suffix)) + { + let snap: nodedb_columnar::ColumnarEngineSnapshot = + zerompk::from_msgpack(bytes).unwrap_or_else(|e| panic!("{key}: decode: {e}")); + let (engine, _segments, _surrogates) = nodedb_columnar::MutationEngine::from_snapshot(snap) + .unwrap_or_else(|e| panic!("{key}: rebuild: {e}")); + let ts_idx = engine + .schema() + .ts_system_idx() + .expect("a bitemporal schema has _ts_system"); + for scanned in engine.scan_memtable_rows_with_surrogates() { + let (_, values) = scanned.unwrap_or_else(|e| panic!("{key}: row: {e}")); + let ts = match values.get(ts_idx) { + Some(nodedb_types::Value::Integer(ts)) => *ts, + other => panic!("{key}: no _ts_system ordinal: {other:?}"), + }; + out.insert(format!("{values:?} ts={ts}")); + } + } + out +} + +/// The audit log of `coll` through `node`, and for each distinct system +/// time in it the rows an `AS OF` read at it returns. The audit log of a +/// bitemporal columnar collection holds its tombstone versions too. +pub(super) async fn cut_view( + node: &TestClusterNode, + coll: &str, + cols: &str, +) -> Vec<(String, Vec)> { + let stamps: BTreeSet = rows( + node, + &format!("SELECT _ts_system FROM {coll} AS OF SYSTEM TIME NULL"), + ) + .await + .into_iter() + .collect(); + let mut view = Vec::with_capacity(stamps.len()); + for stamp in stamps { + let at = rows( + node, + &format!("SELECT {cols} FROM {coll} AS OF SYSTEM TIME {stamp}"), + ) + .await; + view.push((stamp, at)); + } + view +} + +/// The node id of the leader of `coll`'s data group, as `node` sees it. +fn group_leader(node: &TestClusterNode, coll: &str) -> u64 { + let vshard = nodedb_cluster::routing::vshard_for_collection( + nodedb_types::CollectionKey::from_bare(DatabaseId::DEFAULT, coll), + ); + let routing = node + .shared + .cluster_routing + .as_ref() + .expect("cluster routing") + .read() + .unwrap_or_else(|p| p.into_inner()); + let group = routing.group_for_vshard(vshard).expect("vShard group"); + routing + .group_info(group) + .map(|info| info.leader) + .unwrap_or(0) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 6)] +async fn a_version_written_outside_calvin_has_one_system_time_on_every_replica() { + let mut cluster = TestCluster::spawn_three().await.expect("3-node cluster"); + for ddl in [ + format!( + "CREATE COLLECTION {DOCS} (id STRING PRIMARY KEY, value STRING) \ + WITH (engine='document_schemaless', bitemporal=true)" + ), + format!( + "CREATE COLLECTION {COLS} (id BIGINT PRIMARY KEY, v BIGINT) \ + WITH (engine='columnar', bitemporal=true)" + ), + ] { + cluster + .exec_ddl_on_any_leader(&ddl) + .await + .unwrap_or_else(|e| panic!("{ddl}: {e}")); + } + + // Single-key autocommit writes from a node that need not lead either + // group: each replicates as one data-group entry. + let writer = &cluster.nodes[1]; + for sql in [ + format!("INSERT INTO {DOCS} (id, value) VALUES ('k1', 'draft')"), + format!("INSERT INTO {COLS} (id, v) VALUES (1, 10)"), + format!("UPDATE {DOCS} SET value = 'final' WHERE id = 'k1'"), + // A declared primary key refuses a second INSERT of a current key, + // bitemporal or not: the second version comes from an UPDATE. + format!("UPDATE {COLS} SET v = 20 WHERE id = 1"), + format!("INSERT INTO {DOCS} (id, value) VALUES ('k2', 'other')"), + ] { + writer + .exec(&sql) + .await + .unwrap_or_else(|e| panic!("{sql}: {e}")); + tokio::time::sleep(Duration::from_millis(5)).await; + } + cluster + .wait_for_full_apply_convergence(Duration::from_secs(30)) + .await; + + // Each replica's own stored versions carry the same raw ordinals. + let mut doc_keys = Vec::new(); + let mut col_rows = Vec::new(); + for node in &cluster.nodes { + let snapshots = node.stored_tenant_snapshots(TenantId::new(TENANT)).await; + doc_keys.push((node.node_id, doc_version_keys(&snapshots))); + col_rows.push((node.node_id, columnar_rows(&snapshots, COLS))); + } + let (first_node, first_docs) = &doc_keys[0]; + assert_eq!( + first_docs.len(), + 3, + "node {first_node}: three document versions" + ); + for (node_id, keys) in &doc_keys[1..] { + assert_eq!( + keys, first_docs, + "node {node_id}: every document version keys the system time node {first_node} keys" + ); + } + let (first_node, first_cols) = &col_rows[0]; + assert_eq!( + first_cols.len(), + 2, + "node {first_node}: two columnar versions" + ); + for (node_id, cells) in &col_rows[1..] { + assert_eq!( + cells, first_cols, + "node {node_id}: every columnar version stamps the _ts_system node {first_node} stamps" + ); + } + + // Every node reads its own replica, and answers each cut alike. + for node in &cluster.nodes { + node.client + .simple_query("SET default_read_consistency = 'eventual'") + .await + .unwrap_or_else(|e| panic!("node {}: eventual reads: {e}", node.node_id)); + } + let docs_before = cut_view(&cluster.nodes[0], DOCS, "id, value").await; + let cols_before = cut_view(&cluster.nodes[0], COLS, "id, v").await; + assert!(!docs_before.is_empty() && !cols_before.is_empty()); + for node in &cluster.nodes[1..] { + assert_eq!(cut_view(node, DOCS, "id, value").await, docs_before); + assert_eq!(cut_view(node, COLS, "id, v").await, cols_before); + } + + // Stop the documents group's leader. The new leader serves each cut from + // the ordinals it installed as a follower. + let leader = group_leader(&cluster.nodes[0], DOCS); + assert!(leader != 0, "the documents group has a leader"); + let index = cluster + .nodes + .iter() + .position(|node| node.node_id == leader) + .expect("leader node present"); + let _stopped = cluster.stop_member(index).await.expect("stop the leader"); + for node in &cluster.nodes { + node.client + .simple_query("SET default_read_consistency = 'strong'") + .await + .unwrap_or_else(|e| panic!("node {}: strong reads: {e}", node.node_id)); + assert_eq!( + cut_view(node, DOCS, "id, value").await, + docs_before, + "node {} after the leader stopped: the document cuts answer as before", + node.node_id + ); + assert_eq!( + cut_view(node, COLS, "id, v").await, + cols_before, + "node {} after the leader stopped: the columnar cuts answer as before", + node.node_id + ); + } + + cluster.shutdown().await; +} + +/// Every stored `_ts_system` ordinal of columnar collection `coll` on +/// `node`, tombstone versions included, ascending. +pub(super) async fn stored_stamps(node: &TestClusterNode, coll: &str) -> Vec { + let snapshots = node.stored_tenant_snapshots(TenantId::new(TENANT)).await; + let mut stamps: Vec = columnar_rows(&snapshots, coll) + .into_iter() + .map(|row| { + row.rsplit("ts=") + .next() + .and_then(|ts| ts.parse().ok()) + .unwrap_or_else(|| panic!("row without an ordinal: {row}")) + }) + .collect(); + stamps.sort_unstable(); + stamps +} + +/// Two UPDATEs of one bitemporal columnar key land two new versions. With +/// the insert, the key holds three versions under three distinct ascending +/// system times, the same raw ordinals on every replica, and an `AS OF` at +/// each version's `_ts_system` returns that version. +#[tokio::test(flavor = "multi_thread", worker_threads = 6)] +async fn each_columnar_update_lands_a_version_with_its_own_system_time() { + let cluster = TestCluster::spawn_three().await.expect("3-node cluster"); + cluster + .exec_ddl_on_any_leader(&format!( + "CREATE COLLECTION {UPDATED} (id BIGINT PRIMARY KEY, v BIGINT) \ + WITH (engine='columnar', bitemporal=true)" + )) + .await + .expect("CREATE COLLECTION"); + let writer = &cluster.nodes[2]; + for sql in [ + format!("INSERT INTO {UPDATED} (id, v) VALUES (1, 10)"), + format!("UPDATE {UPDATED} SET v = 20 WHERE id = 1"), + format!("UPDATE {UPDATED} SET v = 30 WHERE id = 1"), + ] { + writer + .exec(&sql) + .await + .unwrap_or_else(|e| panic!("{sql}: {e}")); + // Distinct milliseconds, so each version has its own cut. + tokio::time::sleep(Duration::from_millis(5)).await; + } + cluster + .wait_for_full_apply_convergence(Duration::from_secs(30)) + .await; + + let first = stored_stamps(&cluster.nodes[0], UPDATED).await; + assert_eq!(first.len(), 3, "three versions: {first:?}"); + assert!( + first.windows(2).all(|pair| pair[0] < pair[1]), + "distinct ascending system times: {first:?}" + ); + for node in &cluster.nodes[1..] { + assert_eq!( + stored_stamps(node, UPDATED).await, + first, + "node {}: every version stamps the ordinal node {} stamps", + node.node_id, + cluster.nodes[0].node_id + ); + } + + for node in &cluster.nodes { + node.client + .simple_query("SET default_read_consistency = 'eventual'") + .await + .unwrap_or_else(|e| panic!("node {}: eventual reads: {e}", node.node_id)); + let view = cut_view(node, UPDATED, "id, v").await; + let values: Vec> = view.into_iter().map(|(_, at)| at).collect(); + assert_eq!( + values, + vec![ + vec!["1|10".to_string()], + vec!["1|20".to_string()], + vec!["1|30".to_string()], + ], + "node {}: each cut returns its version", + node.node_id + ); + } + + cluster.shutdown().await; +} + +/// A DELETE of a bitemporal columnar key closes it with a tombstone version. +/// The key holds the inserted version and the tombstone under two ascending +/// system times, the same raw ordinals on every replica. A current read +/// returns no row. The audit log holds both versions: an `AS OF` at the +/// insert's system time returns the inserted row, and one at the +/// tombstone's returns none. +#[tokio::test(flavor = "multi_thread", worker_threads = 6)] +async fn a_columnar_delete_closes_its_key_with_its_own_system_time() { + let cluster = TestCluster::spawn_three().await.expect("3-node cluster"); + cluster + .exec_ddl_on_any_leader(&format!( + "CREATE COLLECTION {DELETED} (id BIGINT PRIMARY KEY, v BIGINT) \ + WITH (engine='columnar', bitemporal=true)" + )) + .await + .expect("CREATE COLLECTION"); + let writer = &cluster.nodes[2]; + for sql in [ + format!("INSERT INTO {DELETED} (id, v) VALUES (1, 10)"), + format!("DELETE FROM {DELETED} WHERE id = 1"), + ] { + writer + .exec(&sql) + .await + .unwrap_or_else(|e| panic!("{sql}: {e}")); + // Distinct milliseconds, so the insert has a cut before the close. + tokio::time::sleep(Duration::from_millis(5)).await; + } + cluster + .wait_for_full_apply_convergence(Duration::from_secs(30)) + .await; + + let first = stored_stamps(&cluster.nodes[0], DELETED).await; + assert_eq!(first.len(), 2, "the version and its tombstone: {first:?}"); + assert!( + first[0] < first[1], + "the tombstone stamps after the version: {first:?}" + ); + for node in &cluster.nodes[1..] { + assert_eq!( + stored_stamps(node, DELETED).await, + first, + "node {}: every version stamps the ordinal node {} stamps", + node.node_id, + cluster.nodes[0].node_id + ); + } + + for node in &cluster.nodes { + node.client + .simple_query("SET default_read_consistency = 'eventual'") + .await + .unwrap_or_else(|e| panic!("node {}: eventual reads: {e}", node.node_id)); + assert_eq!( + rows(node, &format!("SELECT id, v FROM {DELETED}")).await, + Vec::::new(), + "node {}: a current read excludes the closed key", + node.node_id + ); + let view = cut_view(node, DELETED, "id, v").await; + assert_eq!( + view.into_iter().map(|(_, at)| at).collect::>(), + vec![vec!["1|10".to_string()], Vec::new()], + "node {}: a cut before the close returns the closed version, and a cut at the \ + close returns none", + node.node_id + ); + } + + cluster.shutdown().await; +} diff --git a/nodedb-cluster-tests/tests/common_suite/cases/bitemporal_txn_columnar.rs b/nodedb-cluster-tests/tests/common_suite/cases/bitemporal_txn_columnar.rs new file mode 100644 index 000000000..01f2d237c --- /dev/null +++ b/nodedb-cluster-tests/tests/common_suite/cases/bitemporal_txn_columnar.rs @@ -0,0 +1,224 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! A transaction's UPDATE and DELETE on a bitemporal columnar collection keep +//! every version. +//! +//! The resolve stamps the transaction's rows with its one system time, raised +//! above each key's newest version, and the redo carries the stamps. Every +//! replica installs the new version of an updated key and the tombstone of a +//! deleted key at that stamp, and keeps the base rows as history. A rolled +//! back transaction installs nothing. A TRUNCATE, autocommit or inside a +//! transaction, closes every current key the same way. + +use std::time::Duration; + +use super::bitemporal_replica_stamps::{cut_view, rows, stored_stamps}; +use crate::common::cluster_harness::{TestCluster, TestClusterNode}; + +const COLL: &str = "btx_cols"; +const TRUNC: &str = "btx_trunc"; + +/// Run `statements` through one session of `node`, panicking on an error. +async fn session_run(node: &TestClusterNode, statements: &[String]) { + for sql in statements { + node.client + .simple_query(sql) + .await + .unwrap_or_else(|e| panic!("node {}: {sql}: {e}", node.node_id)); + } +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 6)] +async fn a_transaction_keeps_every_columnar_version_it_supersedes() { + let cluster = TestCluster::spawn_three().await.expect("3-node cluster"); + cluster + .exec_ddl_on_any_leader(&format!( + "CREATE COLLECTION {COLL} (id BIGINT PRIMARY KEY, v BIGINT) \ + WITH (engine='columnar', bitemporal=true)" + )) + .await + .expect("CREATE COLLECTION"); + let writer = &cluster.nodes[2]; + for sql in [ + format!("INSERT INTO {COLL} (id, v) VALUES (1, 10)"), + format!("INSERT INTO {COLL} (id, v) VALUES (2, 20)"), + ] { + writer + .exec(&sql) + .await + .unwrap_or_else(|e| panic!("{sql}: {e}")); + // Distinct milliseconds, so each version has its own cut. + tokio::time::sleep(Duration::from_millis(5)).await; + } + cluster + .wait_for_full_apply_convergence(Duration::from_secs(30)) + .await; + let seeded = stored_stamps(&cluster.nodes[0], COLL).await; + assert_eq!(seeded.len(), 2, "two inserted versions: {seeded:?}"); + + let txn = |end: &str| { + vec![ + "BEGIN".to_string(), + format!("UPDATE {COLL} SET v = 11 WHERE id = 1"), + format!("DELETE FROM {COLL} WHERE id = 2"), + end.to_string(), + ] + }; + + // A rolled back transaction leaves the history as it was. + session_run(writer, &txn("ROLLBACK")).await; + cluster + .wait_for_full_apply_convergence(Duration::from_secs(30)) + .await; + for node in &cluster.nodes { + assert_eq!( + stored_stamps(node, COLL).await, + seeded, + "node {}: a rollback installs no version", + node.node_id + ); + } + + tokio::time::sleep(Duration::from_millis(5)).await; + session_run(writer, &txn("COMMIT")).await; + cluster + .wait_for_full_apply_convergence(Duration::from_secs(30)) + .await; + + let first = stored_stamps(&cluster.nodes[0], COLL).await; + assert_eq!( + first.len(), + 4, + "both inserted versions, id 1's new version and id 2's tombstone: {first:?}" + ); + assert_eq!(first[..2], seeded[..], "the base versions stay"); + assert_eq!( + first[2], first[3], + "the transaction's versions share its stamp" + ); + assert!( + first[2] > first[1], + "the transaction stamps after the base: {first:?}" + ); + for node in &cluster.nodes[1..] { + assert_eq!( + stored_stamps(node, COLL).await, + first, + "node {}: every version stamps the ordinal node {} stamps", + node.node_id, + cluster.nodes[0].node_id + ); + } + + for node in &cluster.nodes { + node.client + .simple_query("SET default_read_consistency = 'eventual'") + .await + .unwrap_or_else(|e| panic!("node {}: eventual reads: {e}", node.node_id)); + assert_eq!( + rows(node, &format!("SELECT id, v FROM {COLL}")).await, + vec!["1|11".to_string()], + "node {}: the current state after the commit", + node.node_id + ); + let view = cut_view(node, COLL, "id, v").await; + assert_eq!( + view.into_iter().map(|(_, at)| at).collect::>(), + vec![ + vec!["1|10".to_string()], + vec!["1|10".to_string(), "2|20".to_string()], + vec!["1|11".to_string()], + ], + "node {}: each cut returns the state at its stamp", + node.node_id + ); + } + + cluster.shutdown().await; +} + +/// A TRUNCATE of a bitemporal columnar collection closes every current key +/// with a tombstone at one ordinal and keeps every version, autocommit and +/// inside a transaction alike. Every replica stores the same ordinals, and a +/// cut before each truncate returns the rows it closed. +#[tokio::test(flavor = "multi_thread", worker_threads = 6)] +async fn a_truncate_closes_every_columnar_key_and_keeps_history() { + let cluster = TestCluster::spawn_three().await.expect("3-node cluster"); + cluster + .exec_ddl_on_any_leader(&format!( + "CREATE COLLECTION {TRUNC} (id BIGINT PRIMARY KEY, v BIGINT) \ + WITH (engine='columnar', bitemporal=true)" + )) + .await + .expect("CREATE COLLECTION"); + let writer = &cluster.nodes[2]; + let pause = || tokio::time::sleep(Duration::from_millis(5)); + for sql in [ + format!("INSERT INTO {TRUNC} (id, v) VALUES (1, 10)"), + format!("INSERT INTO {TRUNC} (id, v) VALUES (2, 20)"), + format!("TRUNCATE {TRUNC}"), + format!("INSERT INTO {TRUNC} (id, v) VALUES (3, 30)"), + ] { + writer + .exec(&sql) + .await + .unwrap_or_else(|e| panic!("{sql}: {e}")); + pause().await; + } + session_run( + writer, + &[ + "BEGIN".to_string(), + format!("TRUNCATE {TRUNC}"), + "COMMIT".to_string(), + ], + ) + .await; + cluster + .wait_for_full_apply_convergence(Duration::from_secs(30)) + .await; + + let first = stored_stamps(&cluster.nodes[0], TRUNC).await; + assert_eq!( + first.len(), + 6, + "three versions and three tombstones: {first:?}" + ); + for node in &cluster.nodes[1..] { + assert_eq!( + stored_stamps(node, TRUNC).await, + first, + "node {}: every version stamps the ordinal node {} stamps", + node.node_id, + cluster.nodes[0].node_id + ); + } + + for node in &cluster.nodes { + node.client + .simple_query("SET default_read_consistency = 'eventual'") + .await + .unwrap_or_else(|e| panic!("node {}: eventual reads: {e}", node.node_id)); + assert_eq!( + rows(node, &format!("SELECT id, v FROM {TRUNC}")).await, + Vec::::new(), + "node {}: no current row after the truncates", + node.node_id + ); + let view = cut_view(node, TRUNC, "id, v").await; + assert_eq!( + view.into_iter().map(|(_, at)| at).collect::>(), + vec![ + vec!["1|10".to_string()], + vec!["1|10".to_string(), "2|20".to_string()], + Vec::new(), + vec!["3|30".to_string()], + Vec::new(), + ], + "node {}: a cut before each truncate returns the rows it closed", + node.node_id + ); + } + + cluster.shutdown().await; +} diff --git a/nodedb-cluster-tests/tests/common_suite/cases/calvin_multi_shard_bitemporal_restart.rs b/nodedb-cluster-tests/tests/common_suite/cases/calvin_multi_shard_bitemporal_restart.rs index 3214c4a7a..f96013d19 100644 --- a/nodedb-cluster-tests/tests/common_suite/cases/calvin_multi_shard_bitemporal_restart.rs +++ b/nodedb-cluster-tests/tests/common_suite/cases/calvin_multi_shard_bitemporal_restart.rs @@ -77,6 +77,25 @@ async fn current_value( })) } +/// `value` for `id` in a bitemporal document collection `AS OF SYSTEM TIME +/// as_of_ms`, or `None` if not visible at that cut. +async fn value_as_of( + client: &tokio_postgres::Client, + coll: &str, + id: &str, + as_of_ms: &str, +) -> Result, tokio_postgres::Error> { + let msgs = client + .simple_query(&format!( + "SELECT id, value FROM {coll} AS OF SYSTEM TIME {as_of_ms}" + )) + .await?; + Ok(msgs.iter().find_map(|m| match m { + SimpleQueryMessage::Row(r) if r.get("id") == Some(id) => r.get("value").map(str::to_owned), + _ => None, + })) +} + /// Sorted `_ts_system` stamps of every audit-log version (`AS OF SYSTEM TIME /// NULL`) belonging to `id` in `coll`. Filters by id in Rust rather than in /// SQL — `AS OF SYSTEM TIME NULL` combined with `WHERE` is not exercised @@ -244,6 +263,28 @@ async fn calvin_multi_shard_bitemporal_commit_survives_wal_only_restart() { tokio::time::sleep(Duration::from_millis(25)).await; } + // One Calvin transaction stamps one system-time ordinal on every shard it + // writes, the ordinal its edges would take, so one cut splits it whole. + assert_eq!( + stamp_a, stamp_b, + "both shards' versions carry the transaction's one system time" + ); + // A read at the stamp sees both rows of the transaction. + for (coll, id, value) in [(&coll_a, "a1", "va"), (&coll_b, "b1", "vb")] { + let at_stamp = read_once_a_leader_exists( + "row as of the committed stamp", + Duration::from_secs(10), + Duration::from_millis(25), + || value_as_of(&node.client, coll, id, &stamp_a), + ) + .await; + assert_eq!( + at_stamp.as_deref(), + Some(value), + "{id} is visible AS OF its committed stamp" + ); + } + // WAL-only restart: shut down cleanly (flushing the WAL, releasing every // redb handle) and reopen against the SAME directory — no checkpoint, so // both rows must be reconstructed purely from the committed cross-shard @@ -312,5 +353,22 @@ async fn calvin_multi_shard_bitemporal_commit_survives_wal_only_restart() { tokio::time::sleep(Duration::from_millis(25)).await; } + // After the restart the same cut answers the same rows: replay installed + // the committed ordinal, not one from the replay-time clock. + for (coll, id, value) in [(&coll_a, "a1", "va"), (&coll_b, "b1", "vb")] { + let at_stamp = read_once_a_leader_exists( + "row as of the committed stamp after restart", + Duration::from_secs(10), + Duration::from_millis(25), + || value_as_of(&node.client, coll, id, &stamp_a), + ) + .await; + assert_eq!( + at_stamp.as_deref(), + Some(value), + "{id} is visible AS OF its committed stamp after a WAL-only restart" + ); + } + node.shutdown().await; } diff --git a/nodedb-cluster-tests/tests/common_suite/cases/cluster_array_slice_zone.rs b/nodedb-cluster-tests/tests/common_suite/cases/cluster_array_slice_zone.rs index 45223c396..8c0b290dc 100644 --- a/nodedb-cluster-tests/tests/common_suite/cases/cluster_array_slice_zone.rs +++ b/nodedb-cluster-tests/tests/common_suite/cases/cluster_array_slice_zone.rs @@ -59,7 +59,7 @@ fn expected_cells() -> BTreeSet<(i64, i64)> { .build() .expect("schema") .with_georef(Some(ArrayGeoref { - srid: Srid::Utm47N, + srid: Srid::new(32647).unwrap(), x_dim: 1, y_dim: 0, transform: Some(GeoTransform(GT)), @@ -69,7 +69,13 @@ fn expected_cells() -> BTreeSet<(i64, i64)> { exterior: RING.to_vec(), holes: Vec::new(), }]; - let frame = build_zone_frame(&schema, Srid::Wgs84, &polygons).expect("frame"); + let frame = build_zone_frame( + &schema, + Srid::WGS84, + &polygons, + &nodedb_crs::CrsContext::builtin_only(), + ) + .expect("frame"); let zone = PolygonZone { x_dim: 1, y_dim: 0, diff --git a/nodedb-cluster-tests/tests/common_suite/cases/cluster_array_ws_rpc.rs b/nodedb-cluster-tests/tests/common_suite/cases/cluster_array_ws_rpc.rs index 3deabf598..83ea82aef 100644 --- a/nodedb-cluster-tests/tests/common_suite/cases/cluster_array_ws_rpc.rs +++ b/nodedb-cluster-tests/tests/common_suite/cases/cluster_array_ws_rpc.rs @@ -44,7 +44,9 @@ async fn ws_rpc_sql(node: &TestClusterNode, sql: &str) -> serde_json::Value { .to_identity(HARNESS_SUPERUSER, AuthMethod::Trust) .expect("the harness superuser exists on every node"); let query_ctx = QueryContext::for_state_with_lease(&node.shared); - execute_sql( + // Boxed: the statement future is deep enough to exceed the layout query + // depth of this test crate when held inline. + Box::pin(execute_sql( &node.shared, &query_ctx, &identity, @@ -52,7 +54,7 @@ async fn ws_rpc_sql(node: &TestClusterNode, sql: &str) -> serde_json::Value { sql, TraceId::generate(), "127.0.0.1:40000", - ) + )) .await .unwrap_or_else(|e| panic!("ws_rpc {sql} on node {}: {e:?}", node.node_id)) } diff --git a/nodedb-cluster-tests/tests/common_suite/cases/cluster_restore_columnar_stamps.rs b/nodedb-cluster-tests/tests/common_suite/cases/cluster_restore_columnar_stamps.rs new file mode 100644 index 000000000..c8e3eeb8c --- /dev/null +++ b/nodedb-cluster-tests/tests/common_suite/cases/cluster_restore_columnar_stamps.rs @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! A cluster RESTORE keeps the system time of every bitemporal columnar +//! version. +//! +//! One cluster writes two versions of one key into a bitemporal columnar +//! collection and records, for each version's `_ts_system`, the rows an +//! `AS OF` read at it returns. A fresh cluster restores the backup. Every +//! node then reads its own replica and answers each original stamp with the +//! same rows, before and after every node restarts. + +use std::collections::BTreeSet; +use std::time::Duration; + +use super::cluster_restore_documents_restart::{drain_backup, push_restore}; +use crate::common::cluster_harness::{TestCluster, TestClusterNode, read_once_a_leader_exists}; + +const COLL: &str = "crc_metrics"; + +/// Every row `sql` returns through `node`, its cells joined, sorted. +async fn rows(node: &TestClusterNode, sql: &str) -> Vec { + let messages = read_once_a_leader_exists( + sql, + Duration::from_secs(30), + Duration::from_millis(100), + || node.client.simple_query(sql), + ) + .await; + let mut out: Vec = messages + .iter() + .filter_map(|message| match message { + tokio_postgres::SimpleQueryMessage::Row(row) => Some( + (0..row.len()) + .map(|i| row.get(i).unwrap_or("NULL")) + .collect::>() + .join("|"), + ), + _ => None, + }) + .collect(); + out.sort(); + out +} + +/// Each `_ts_system` of the collection's versions on `node`, with the rows a +/// read `AS OF SYSTEM TIME` that stamp returns there. +async fn cut_view(node: &TestClusterNode) -> Vec<(String, Vec)> { + let stamps: BTreeSet = rows( + node, + &format!("SELECT _ts_system FROM {COLL} AS OF SYSTEM TIME NULL"), + ) + .await + .into_iter() + .collect(); + let mut view = Vec::with_capacity(stamps.len()); + for stamp in stamps { + let at = rows( + node, + &format!("SELECT id, v FROM {COLL} AS OF SYSTEM TIME {stamp}"), + ) + .await; + view.push((stamp, at)); + } + view +} + +/// Every node of `cluster` reads its own replica and answers each original +/// stamp as the source did. +async fn assert_stamps_kept(cluster: &TestCluster, before: &[(String, Vec)], stage: &str) { + cluster + .wait_for_full_apply_convergence(Duration::from_secs(30)) + .await; + for node in &cluster.nodes { + node.client + .simple_query("SET default_read_consistency = 'eventual'") + .await + .unwrap_or_else(|e| panic!("node {}: eventual reads: {e}", node.node_id)); + assert_eq!( + cut_view(node).await, + before, + "{stage}, node {}: each restored version keeps its system time", + node.node_id + ); + } +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn a_restore_keeps_every_columnar_versions_system_time() { + let source = TestCluster::spawn_three().await.expect("source cluster"); + source + .exec_ddl_on_any_leader(&format!( + "CREATE COLLECTION {COLL} (id BIGINT PRIMARY KEY, v BIGINT) \ + WITH (engine='columnar', bitemporal=true)" + )) + .await + .expect("CREATE COLLECTION"); + for sql in [ + format!("INSERT INTO {COLL} (id, v) VALUES (1, 10)"), + format!("INSERT INTO {COLL} (id, v) VALUES (2, 5)"), + // A declared primary key refuses a second INSERT of a current key, + // bitemporal or not: the second version comes from an UPDATE. + format!("UPDATE {COLL} SET v = 20 WHERE id = 1"), + ] { + source.nodes[0] + .exec(&sql) + .await + .unwrap_or_else(|e| panic!("{sql}: {e}")); + // Distinct milliseconds, so each version has its own cut. + tokio::time::sleep(Duration::from_millis(5)).await; + } + source + .wait_for_full_apply_convergence(Duration::from_secs(30)) + .await; + let before = cut_view(&source.nodes[0]).await; + assert_eq!(before.len(), 3, "three versions, three system times"); + assert_eq!( + before.first().map(|(_, at)| at.clone()), + Some(vec!["1|10".to_string()]), + "the first cut reads the first version alone" + ); + let backup = drain_backup(&source.nodes[0].client).await; + source.shutdown().await; + + let target = TestCluster::spawn_three().await.expect("target cluster"); + push_restore(&target.nodes[1].client, backup).await; + assert_stamps_kept(&target, &before, "after the restore").await; + + let target = target + .restart_all() + .await + .unwrap_or_else(|e| panic!("restart every node: {e}")); + assert_stamps_kept(&target, &before, "after every node restarted").await; + + target.shutdown().await; +} diff --git a/nodedb-cluster-tests/tests/common_suite/cases/cluster_restore_documents_restart.rs b/nodedb-cluster-tests/tests/common_suite/cases/cluster_restore_documents_restart.rs index 57bab4ecc..777946895 100644 --- a/nodedb-cluster-tests/tests/common_suite/cases/cluster_restore_documents_restart.rs +++ b/nodedb-cluster-tests/tests/common_suite/cases/cluster_restore_documents_restart.rs @@ -50,7 +50,7 @@ fn db_detail(e: &tokio_postgres::Error) -> String { } } -async fn drain_backup(client: &tokio_postgres::Client) -> Vec { +pub(super) async fn drain_backup(client: &tokio_postgres::Client) -> Vec { let stream = client .copy_out(&format!("COPY (BACKUP TENANT {TENANT}) TO STDOUT")) .await @@ -63,7 +63,7 @@ async fn drain_backup(client: &tokio_postgres::Client) -> Vec { bytes } -async fn push_restore(client: &tokio_postgres::Client, envelope: Vec) { +pub(super) async fn push_restore(client: &tokio_postgres::Client, envelope: Vec) { let sink = client .copy_in::<_, Bytes>(&format!("COPY tenant_restore({TENANT}) FROM STDIN")) .await @@ -179,6 +179,26 @@ async fn assert_restored_on(node: &TestClusterNode, stage: &str) { ); } +/// Each system time of the bitemporal row's versions on `node`, with the +/// values a read `AS OF SYSTEM TIME` that time returns there. +async fn cut_view(node: &TestClusterNode) -> Vec<(String, Vec)> { + let stamps = column( + node, + "SELECT _ts_system FROM crd_ledger AS OF SYSTEM TIME NULL", + ) + .await; + let mut view = Vec::with_capacity(stamps.len()); + for stamp in stamps { + let values = column( + node, + &format!("SELECT value FROM crd_ledger AS OF SYSTEM TIME {stamp}"), + ) + .await; + view.push((stamp, values)); + } + view +} + async fn assert_restored(cluster: &TestCluster, stage: &str) { cluster .wait_for_full_apply_convergence(Duration::from_secs(30)) @@ -186,6 +206,24 @@ async fn assert_restored(cluster: &TestCluster, stage: &str) { for node in &cluster.nodes { assert_restored_on(node, stage).await; } + // Every replica keys the restored versions at the ordinals the backup + // carried, so a read at each version's system time answers the same rows + // on every node. + let first = cut_view(&cluster.nodes[0]).await; + assert_eq!(first.len(), 2, "{stage}: both versions carry a system time"); + assert_eq!( + first.last().map(|(_, values)| values.clone()), + Some(vec!["final".to_string()]), + "{stage}: a read at the newest version's system time sees it" + ); + for node in &cluster.nodes[1..] { + assert_eq!( + cut_view(node).await, + first, + "{stage}, node {}: every replica answers each cut alike", + node.node_id + ); + } let duplicate = cluster.nodes[0] .exec("INSERT INTO crd_people (id, city, email) VALUES ('dave', 'oslo', 'a@x')") .await; diff --git a/nodedb-cluster-tests/tests/common_suite/cases/cluster_restore_labels_only_mark.rs b/nodedb-cluster-tests/tests/common_suite/cases/cluster_restore_labels_only_mark.rs new file mode 100644 index 000000000..ab6df070f --- /dev/null +++ b/nodedb-cluster-tests/tests/common_suite/cases/cluster_restore_labels_only_mark.rs @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! A RESTORE whose only data is a node label raises the restore mark. +//! +//! Node labels belong to no collection: a node's labels home on +//! `VShardId::from_key(node)`. The backup here holds an empty collection and +//! one labeled node, so the restore re-issues exactly one batch: the label. +//! Every replica of the node's home group raises the tenant's restore mark +//! for that batch, so a later RESTORE's staleness guard sees the write. After +//! every node restarts, each replica still holds the mark and the label. + +use std::time::Duration; + +use nodedb::control::state::tenant_marks::MarkSite; +use nodedb_types::TenantId; + +use crate::common::cluster_harness::shared_steps::{drain_backup, group_of_key, push_restore}; +use crate::common::cluster_harness::{TestCluster, TestClusterNode, wait_for}; + +const TENANT: u64 = 1; +const NODE: &str = "rlm_carol"; +const LABEL: &str = "Admin"; + +/// Whether `node` holds a restore mark of `TENANT` in `group`. +fn holds_restore_mark(node: &TestClusterNode, group: u64) -> bool { + node.shared + .tenant_marks + .get_all(group, TENANT) + .iter() + .any(|mark| mark.site == MarkSite::Restore && mark.restore_id != 0) +} + +/// Whether `node`'s own cores hold `NODE` labeled `LABEL`. +async fn holds_label(node: &TestClusterNode) -> bool { + node.stored_tenant_snapshots(TenantId::new(TENANT)) + .await + .iter() + .any(|snap| { + snap.node_labels + .iter() + .any(|(_, _, labeled, labels)| labeled == NODE && labels.iter().any(|l| l == LABEL)) + }) +} + +async fn assert_restored(cluster: &TestCluster, group: u64, stage: &str) { + cluster + .wait_for_full_apply_convergence(Duration::from_secs(30)) + .await; + for node in &cluster.nodes { + assert!( + holds_restore_mark(node, group), + "{stage}, node {}: the label batch raised the restore mark in group {group}", + node.node_id + ); + assert!( + holds_label(node).await, + "{stage}, node {}: the restored label is stored", + node.node_id + ); + } +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 6)] +async fn a_labels_only_restore_raises_the_restore_mark() { + let source = TestCluster::spawn_three().await.expect("source cluster"); + source + .exec_ddl_on_any_leader("CREATE COLLECTION rlm_people WITH (engine='document_schemaless')") + .await + .expect("CREATE COLLECTION"); + source.nodes[0] + .exec(&format!("GRAPH LABEL '{NODE}' AS '{LABEL}'")) + .await + .expect("GRAPH LABEL"); + source + .wait_for_full_apply_convergence(Duration::from_secs(30)) + .await; + let backup = drain_backup(&source.nodes[0].client, TENANT).await; + source.shutdown().await; + + let target = TestCluster::spawn_three().await.expect("target cluster"); + let group = group_of_key(&target.nodes[0], NODE); + for node in &target.nodes { + assert!( + !holds_restore_mark(node, group), + "node {}: a fresh cluster holds no restore mark", + node.node_id + ); + } + push_restore(&target.nodes[0].client, TENANT, backup).await; + wait_for( + "every replica raises the restore mark", + Duration::from_secs(30), + Duration::from_millis(100), + || { + target + .nodes + .iter() + .all(|node| holds_restore_mark(node, group)) + }, + ) + .await; + assert_restored(&target, group, "after the restore").await; + + let target = target.restart_all().await.expect("restart every node"); + assert_restored(&target, group, "after a restart").await; + + target.shutdown().await; +} diff --git a/nodedb-cluster-tests/tests/common_suite/cases/cluster_spatial_ref_sys.rs b/nodedb-cluster-tests/tests/common_suite/cases/cluster_spatial_ref_sys.rs new file mode 100644 index 000000000..fe844f02f --- /dev/null +++ b/nodedb-cluster-tests/tests/common_suite/cases/cluster_spatial_ref_sys.rs @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: BUSL-1.1 +//! 3-node cluster test for `CREATE SPATIAL_REF_SYS` and `DROP +//! SPATIAL_REF_SYS`. +//! +//! The test matches `binary(/cluster/)` in nextest.toml, so it runs in the +//! serial `cluster` test group. A user-defined CRS defined through one node +//! replicates through the metadata group: every node resolves it, transforms +//! to it alike and lists it, and every node refuses it once it is dropped. + +use std::time::Duration; + +use nodedb_crs::{CrsContext, CustomCrsSet, Srid, parse_proj_string}; + +use crate::common; + +use common::cluster_harness::TestCluster; + +const CODE: u32 = 910_201; + +const TEST_TM: &str = + "+proj=tmerc +lat_0=0 +lon_0=101.25 +k=0.99991 +x_0=123456 +y_0=0 +ellps=GRS80 +units=m"; + +/// `(x, y)` of `(103.8, 1.45)` in [`TEST_TM`], computed in the test. +fn expected() -> (f64, f64) { + let srid = Srid::new(CODE).expect("srid"); + let mut set = CustomCrsSet::new(); + set.define(parse_proj_string(srid, "c", TEST_TM).expect("parses")) + .expect("defines"); + let t = CrsContext::new(&set) + .transformer(Srid::WGS84, srid) + .expect("defined"); + let [x, y, _] = t.apply([103.8, 1.45, 0.0]).expect("inside"); + (x, y) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn a_user_defined_crs_resolves_on_every_node() { + let cluster = TestCluster::spawn_three() + .await + .expect("3-node cluster spawn"); + cluster + .exec_ddl_on_any_leader(&format!( + "CREATE SPATIAL_REF_SYS {CODE} (NAME 'cluster tm', PROJ '{TEST_TM}')" + )) + .await + .expect("CREATE SPATIAL_REF_SYS"); + cluster + .wait_for_full_apply_convergence(Duration::from_secs(10)) + .await; + + let (want_x, want_y) = expected(); + let sql = format!( + "SELECT ST_X(ST_Transform(ST_Point(103.8, 1.45), {CODE})) AS x, \ + ST_Y(ST_Transform(ST_Point(103.8, 1.45), {CODE})) AS y" + ); + for (idx, node) in cluster.nodes.iter().enumerate() { + let rows = node + .query_named_rows(&sql) + .await + .unwrap_or_else(|e| panic!("node {idx}: {sql}: {e}")); + assert_eq!(rows.len(), 1, "node {idx}: {rows:?}"); + let num = |c: &str| -> f64 { + rows[0][c] + .parse() + .unwrap_or_else(|e| panic!("node {idx}: {c} in {rows:?}: {e}")) + }; + assert!( + (num("x") - want_x).abs() < 1e-3 && (num("y") - want_y).abs() < 1e-3, + "node {idx}: got ({}, {}), want ({want_x}, {want_y})", + num("x"), + num("y") + ); + let listed = node + .query_named_rows(&format!( + "SELECT name FROM nodedb.spatial_ref_sys WHERE srid = {CODE}" + )) + .await + .unwrap_or_else(|e| panic!("node {idx}: view: {e}")); + assert_eq!(listed.len(), 1, "node {idx}: {listed:?}"); + assert_eq!(listed[0]["name"], "cluster tm", "node {idx}"); + } + + cluster + .exec_ddl_on_any_leader(&format!("DROP SPATIAL_REF_SYS {CODE}")) + .await + .expect("DROP SPATIAL_REF_SYS"); + cluster + .wait_for_full_apply_convergence(Duration::from_secs(10)) + .await; + for (idx, node) in cluster.nodes.iter().enumerate() { + let err = node + .exec(&format!( + "SELECT ST_Transform(ST_Point(103.8, 1.45), {CODE})" + )) + .await + .expect_err("a dropped code is refused"); + assert!( + err.contains(&format!("SRID {CODE} was dropped")), + "node {idx}: {err}" + ); + } + cluster.shutdown().await; +} diff --git a/nodedb-cluster-tests/tests/common_suite/cases/columnar_bitemporal_cut_cluster.rs b/nodedb-cluster-tests/tests/common_suite/cases/columnar_bitemporal_cut_cluster.rs new file mode 100644 index 000000000..17865b34d --- /dev/null +++ b/nodedb-cluster-tests/tests/common_suite/cases/columnar_bitemporal_cut_cluster.rs @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! A bitemporal columnar `AS OF` reads one answer on every node. +//! +//! The data group's leader stamps each insert's `_ts_system` run as it admits +//! the entry, every replica installs that run, and its WAL record carries it. +//! A cut between two versions of a key reads the first on every node: after +//! replication catch-up, after the data group's leader stops (the new leader +//! serves the cut from the stamps it installed), and after the former leader +//! restarts and replays its WAL. + +use std::time::Duration; + +use nodedb_types::DatabaseId; + +use crate::common::cluster_harness::{TestCluster, TestClusterNode, read_once_a_leader_exists}; + +const COLL: &str = "btc_cut"; + +fn now_ms() -> i64 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("clock after the epoch") + .as_millis() as i64 +} + +/// The `v` of every row `sql` returns through `node`, sorted. +async fn values(node: &TestClusterNode, sql: &str) -> Vec { + let messages = read_once_a_leader_exists( + sql, + Duration::from_secs(30), + Duration::from_millis(100), + || node.client.simple_query(sql), + ) + .await; + let mut out: Vec = messages + .iter() + .filter_map(|message| match message { + tokio_postgres::SimpleQueryMessage::Row(row) => row.get(0).map(str::to_owned), + _ => None, + }) + .collect(); + out.sort(); + out +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 6)] +async fn a_columnar_as_of_reads_one_answer_on_every_node() { + let mut cluster = TestCluster::spawn_three().await.expect("3-node cluster"); + cluster + .exec_ddl_on_any_leader(&format!( + "CREATE COLLECTION {COLL} (id BIGINT PRIMARY KEY, v BIGINT) \ + WITH (engine='columnar', bitemporal=true)" + )) + .await + .expect("CREATE COLLECTION"); + + cluster.nodes[0] + .exec(&format!("INSERT INTO {COLL} (id, v) VALUES (1, 10)")) + .await + .expect("insert v1"); + tokio::time::sleep(Duration::from_millis(50)).await; + let cut = now_ms(); + tokio::time::sleep(Duration::from_millis(50)).await; + cluster.nodes[0] + // A declared primary key refuses a second INSERT of a current key, + // bitemporal or not: the second version comes from an UPDATE. + .exec(&format!("UPDATE {COLL} SET v = 20 WHERE id = 1")) + .await + .expect("insert v2"); + cluster + .wait_for_full_apply_convergence(Duration::from_secs(30)) + .await; + + let as_of = format!("SELECT v FROM {COLL} AS OF SYSTEM TIME {cut}"); + for node in &cluster.nodes { + assert_eq!( + values(node, &as_of).await, + vec!["10".to_string()], + "node {}: the cut reads the first version", + node.node_id + ); + } + + // Stop the data group's leader: a surviving replica serves the cut from + // the ordinals it installed. + let vshard = nodedb_cluster::routing::vshard_for_collection( + nodedb_types::CollectionKey::from_bare(DatabaseId::DEFAULT, COLL), + ); + let leader = { + let routing = cluster.nodes[0] + .shared + .cluster_routing + .as_ref() + .expect("cluster routing") + .read() + .unwrap_or_else(|p| p.into_inner()); + let group = routing.group_for_vshard(vshard).expect("vShard group"); + routing + .group_info(group) + .map(|info| info.leader) + .unwrap_or(0) + }; + assert!(leader != 0, "the collection's group has a leader"); + let index = cluster + .nodes + .iter() + .position(|node| node.node_id == leader) + .expect("leader node present"); + let stopped = cluster.stop_member(index).await.expect("stop the leader"); + + for node in &cluster.nodes { + assert_eq!( + values(node, &as_of).await, + vec!["10".to_string()], + "node {} after the leader stopped: the cut reads the first version", + node.node_id + ); + } + + // The former leader restarts from its own WAL and checkpoints, and + // replays each record at the ordinals its live apply stamped. + cluster + .restart_member(stopped) + .await + .expect("restart the former leader"); + cluster + .wait_for_full_apply_convergence(Duration::from_secs(30)) + .await; + for node in &cluster.nodes { + assert_eq!( + values(node, &as_of).await, + vec!["10".to_string()], + "node {} after the restart: the cut reads the first version", + node.node_id + ); + } + + cluster.shutdown().await; +} diff --git a/nodedb-cluster-tests/tests/common_suite/cases/graph_live_cut_calvin_stamps.rs b/nodedb-cluster-tests/tests/common_suite/cases/graph_live_cut_calvin_stamps.rs new file mode 100644 index 000000000..546c78896 --- /dev/null +++ b/nodedb-cluster-tests/tests/common_suite/cases/graph_live_cut_calvin_stamps.rs @@ -0,0 +1,158 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! Every edge write takes a Calvin ordinal, so a live WCC run reads the same +//! graph on every node. +//! +//! A live distributed graph run reads at its Calvin cut marker's cut, which +//! covers only Calvin-stamped edge versions. An edge write stamped from a +//! node's own clock would sit above that cut on some replica and below it on +//! another. Edges are written here through every SQL path: autocommit put, +//! autocommit delete, and a session transaction's COMMIT. Right after the +//! last acknowledgement, each node runs a live WCC and must see every +//! acknowledged edge, grouped the same way. Each replica then stores every +//! edge version under the same system time: the replicas applied one +//! replicated ordinal, not three local clocks. + +use std::collections::{BTreeMap, BTreeSet}; +use std::time::Duration; + +use nodedb_types::TenantId; + +use crate::common::cluster_harness::{TestCluster, TestClusterNode, wait_for}; + +const COLL: &str = "glcut"; +const TENANT: u64 = 1; + +/// `node_id -> component_id` of a live WCC run through `node`. +async fn components(node: &TestClusterNode) -> BTreeMap { + let sql = format!("GRAPH ALGO WCC ON '{COLL}'"); + let messages = node + .client + .simple_query(&sql) + .await + .unwrap_or_else(|e| panic!("node {}: {sql}: {e}", node.node_id)); + messages + .iter() + .filter_map(|message| match message { + tokio_postgres::SimpleQueryMessage::Row(row) => Some(( + row.get("node_id").unwrap_or("").to_string(), + row.get("component_id").unwrap_or("").to_string(), + )), + _ => None, + }) + .collect() +} + +/// The components of `assignment` as sets of graph nodes, ignoring ids. +fn partition(assignment: &BTreeMap) -> BTreeSet> { + let mut by_component: BTreeMap<&str, BTreeSet> = BTreeMap::new(); + for (node, component) in assignment { + by_component + .entry(component.as_str()) + .or_default() + .insert(node.clone()); + } + by_component.into_values().collect() +} + +/// Every edge version key of `COLL` the cores of `node` store. +async fn stored_edge_keys(node: &TestClusterNode) -> BTreeSet { + let prefix = format!("{COLL}\x00"); + node.stored_tenant_snapshots(TenantId::new(TENANT)) + .await + .into_iter() + .flat_map(|snap| snap.edges.into_iter().map(|(key, _)| key)) + .filter(|key| key.starts_with(&prefix)) + .collect() +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 6)] +async fn a_live_wcc_reads_every_edge_write_path_alike_on_every_node() { + let cluster = TestCluster::spawn_three().await.expect("3-node cluster"); + cluster + .exec_ddl_on_any_leader(&format!("CREATE COLLECTION {COLL}")) + .await + .expect("CREATE COLLECTION"); + wait_for( + "every node sees the collection", + Duration::from_secs(10), + Duration::from_millis(50), + || { + cluster + .nodes + .iter() + .all(|n| n.cached_collection_count() >= 1) + }, + ) + .await; + + let writer = &cluster.nodes[1]; + // Autocommit puts, and a put the same session deletes again. + for (src, dst) in [("a0", "a1"), ("a1", "a2"), ("a2", "a3")] { + writer + .exec(&format!( + "GRAPH INSERT EDGE IN '{COLL}' FROM '{src}' TO '{dst}' TYPE 'K'" + )) + .await + .unwrap_or_else(|e| panic!("insert {src} -> {dst}: {e}")); + } + writer + .exec(&format!( + "GRAPH DELETE EDGE IN '{COLL}' FROM 'a2' TO 'a3' TYPE 'K'" + )) + .await + .expect("delete a2 -> a3"); + // A session transaction's COMMIT. + writer + .client + .simple_query(&format!( + "BEGIN; \ + GRAPH INSERT EDGE IN '{COLL}' FROM 'b0' TO 'b1' TYPE 'K'; \ + GRAPH INSERT EDGE IN '{COLL}' FROM 'b1' TO 'b2' TYPE 'K'; \ + COMMIT" + )) + .await + .expect("COMMIT the edge transaction"); + + // No wait: the run's cut marker is placed after every acknowledgement. + let first = components(&cluster.nodes[0]).await; + let chain_a: BTreeSet = ["a0", "a1", "a2"].iter().map(|s| s.to_string()).collect(); + let chain_b: BTreeSet = ["b0", "b1", "b2"].iter().map(|s| s.to_string()).collect(); + let groups = partition(&first); + assert!( + groups.contains(&chain_a), + "the autocommit chain is one component: {groups:?}" + ); + assert!( + groups.contains(&chain_b), + "the committed chain is one component: {groups:?}" + ); + for node in &cluster.nodes[1..] { + assert_eq!( + partition(&components(node).await), + groups, + "node {} reads the same graph", + node.node_id + ); + } + + cluster + .wait_for_full_apply_convergence(Duration::from_secs(30)) + .await; + let stored = stored_edge_keys(&cluster.nodes[0]).await; + assert!( + stored.len() >= 5, + "every put leaves a version: {}", + stored.len() + ); + for node in &cluster.nodes[1..] { + assert_eq!( + stored_edge_keys(node).await, + stored, + "node {} stores every edge version under the same system time", + node.node_id + ); + } + + cluster.shutdown().await; +} diff --git a/nodedb-cluster-tests/tests/common_suite/cases/mod.rs b/nodedb-cluster-tests/tests/common_suite/cases/mod.rs index d75f1fda7..9fe255655 100644 --- a/nodedb-cluster-tests/tests/common_suite/cases/mod.rs +++ b/nodedb-cluster-tests/tests/common_suite/cases/mod.rs @@ -11,6 +11,9 @@ mod array_vshard_migration; mod assign_surrogate_cross_node; mod backup_schedule_failover; mod bitemporal_array_cluster; +mod bitemporal_array_cluster_cut; +mod bitemporal_replica_stamps; +mod bitemporal_txn_columnar; mod calvin_backup_cut_order; mod calvin_backup_cut_snapshot_install; mod calvin_cdc_net_kinds; @@ -69,13 +72,17 @@ mod cluster_execute_request; mod cluster_partition_strategy_replication; mod cluster_pitr_restore_point; mod cluster_post_apply_follower_dispatch; +mod cluster_restore_columnar_stamps; mod cluster_restore_documents_restart; +mod cluster_restore_labels_only_mark; mod cluster_restore_refuses_on_non_replica; mod cluster_restore_retry; mod cluster_restore_surrogate_conflict; mod cluster_restore_surrogate_floor; +mod cluster_spatial_ref_sys; mod cluster_surrogate_replication; mod column_stats_cross_node; +mod columnar_bitemporal_cut_cluster; mod committed_publish_failover; mod committed_publish_metadata_lag; mod constraint_delivery; @@ -97,6 +104,7 @@ mod gateway_execute; mod gateway_hung_node_budget; mod gather_join_in_txn_occ; mod graph_index_edges_replicate_to_followers; +mod graph_live_cut_calvin_stamps; mod graph_match_ryow_cross_node; mod graph_wcc_core_error_cross_node; mod hilo_surrogate_uniqueness; @@ -183,6 +191,10 @@ mod trigger_shipped_block_atomic_cross_node; mod ts_native_ingest; mod vector_alter_params_cross_node; mod vector_index_dispatch_cross_node; +mod vshard_departure_crdt; +mod vshard_departure_fixture; +mod vshard_departure_purge; +mod vshard_departure_purge_restart; mod vshard_names; mod webhook_sink_partial_replication; mod webhook_sink_single_owner; diff --git a/nodedb-cluster-tests/tests/common_suite/cases/vshard_departure_crdt.rs b/nodedb-cluster-tests/tests/common_suite/cases/vshard_departure_crdt.rs new file mode 100644 index 000000000..3055d4865 --- /dev/null +++ b/nodedb-cluster-tests/tests/common_suite/cases/vshard_departure_crdt.rs @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! CRDT rows of the vShard departure purge tests. +//! +//! The fixture writes its CRDT row through the native protocol, into a +//! collection homed on a departed vShard. A movable-list insert +//! (`OpCode::CrdtListInsert`) needs its document to exist, so the fixture +//! first creates the document with a `crdt_apply` delta over the same native +//! connection, as the other cluster CRDT tests create theirs. The gateway +//! routes each op to the collection's vShard and proposes it through the data +//! group's Raft log. + +use std::collections::HashMap; + +use nodedb::types::TenantDataSnapshot; +use nodedb_client::NodeDb; +use nodedb_types::value::Value; + +use super::vshard_departure_fixture::first_column; +use crate::common::cluster_harness::TestClusterNode; + +/// The CRDT document the fixture writes. +pub const CRDT_DOC: &str = "r1x"; +const CRDT_LIST: &str = "items"; + +/// A Loro delta that creates document `CRDT_DOC` of `collection`: the +/// collection's root map holds the row as a map container with one field. +/// Hex-encoded, the form `crdt_apply` takes. +fn create_doc_delta_hex(collection: &str) -> String { + let doc = loro::LoroDoc::new(); + let row = doc + .get_map(collection) + .insert_container(CRDT_DOC, loro::LoroMap::new()) + .expect("row container"); + row.insert("name", "departed").expect("field"); + doc.commit(); + hex::encode( + doc.export(loro::ExportMode::Snapshot) + .expect("export loro snapshot"), + ) +} + +/// Write one CRDT row into `collection` through `node`'s native listener: +/// create the document, then insert a block into its movable list. +pub async fn write_crdt_row(node: &TestClusterNode, collection: &str) { + let client = node.native_client(); + let create = format!( + "SELECT crdt_apply('{collection}', '{CRDT_DOC}', '{}')", + create_doc_delta_hex(collection) + ); + client + .execute_sql(&create, &[]) + .await + .unwrap_or_else(|e| panic!("native crdt_apply into {collection}: {e}")); + let mut block = HashMap::new(); + block.insert("title".to_string(), Value::String("departed".to_string())); + client + .list_insert(collection, CRDT_DOC, CRDT_LIST, 0, &Value::Object(block)) + .await + .unwrap_or_else(|e| panic!("native CRDT list insert into {collection}: {e}")); +} + +/// Whether `snap` holds CRDT state of `collection`. +/// +/// Any exported document of the collection counts, empty rows included: a +/// Loro export carries the full op history, so a document whose rows were +/// deleted still holds every value the departed row ever had. +pub fn holds_crdt_state(snap: &TenantDataSnapshot, collection: &str) -> bool { + snap.crdt_state + .iter() + .any(|(_, _, key, _)| key.contains(collection)) +} + +/// Assert `node` serves the CRDT row of `collection`. +pub async fn assert_crdt_served(node: &TestClusterNode, collection: &str, stage: &str) { + let state = first_column( + node, + &format!("SELECT crdt_state('{collection}', '{CRDT_DOC}')"), + ) + .await; + assert!( + state.len() == 1 && !state[0].is_empty(), + "{stage}: the CRDT row of {collection} serves: {state:?}" + ); +} diff --git a/nodedb-cluster-tests/tests/common_suite/cases/vshard_departure_fixture.rs b/nodedb-cluster-tests/tests/common_suite/cases/vshard_departure_fixture.rs new file mode 100644 index 000000000..594436675 --- /dev/null +++ b/nodedb-cluster-tests/tests/common_suite/cases/vshard_departure_fixture.rs @@ -0,0 +1,470 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! Shared setup of the vShard departure purge tests. +//! +//! A 3-node cluster at RF 3 gains a fourth node. Placement then picks three +//! of the four nodes for each data group by rendezvous hashing, so each +//! group drops one node. The fixture computes that placement up front, picks +//! a group one of the founding nodes leaves, and writes rows of every engine +//! homed on that group's vShards: schemaless documents with a search index, +//! strict documents, KV, columnar, timeseries, vector, spatial, graph edges, +//! a node label, array cells, and a CRDT row written over the native +//! protocol. +//! +//! Each probe reads one node's own cores (`stored_tenant_snapshots`), never +//! a pgwire query: the gateway forwards reads to the vShard owners. + +use std::collections::BTreeSet; +use std::time::Duration; + +use nodedb::types::{DatabaseId, TenantDataSnapshot, TenantId, VShardId}; + +use super::vshard_departure_crdt::{assert_crdt_served, holds_crdt_state, write_crdt_row}; +use crate::common::cluster_harness::{ + TestCluster, TestClusterNode, read_once_a_leader_exists, wait_for, +}; + +pub const TENANT: u64 = 1; +const ARRAY_CELLS: u64 = 256; +const MAX_TRIES: u32 = 4096; + +/// The names every engine's rows go under. Each ends in `x`, so no name is a +/// substring of another. +pub struct Names { + pub doc: String, + pub strict: String, + pub kv: String, + pub columnar: String, + pub timeseries: String, + pub vector: String, + pub spatial: String, + pub graph: String, + pub array: String, + /// The collection the native CRDT row goes under. + pub crdt: String, + /// Edge endpoints and the labeled node, all homed on departed vShards. + pub src: String, + pub dst: String, + /// An endpoint homed on a vShard the leaving node keeps. + pub kept_dst: String, +} + +impl Names { + /// The collections whose rows a stored snapshot keys by name. + pub fn collections(&self) -> [&str; 7] { + [ + &self.doc, + &self.strict, + &self.kv, + &self.columnar, + &self.timeseries, + &self.vector, + &self.spatial, + ] + } +} + +pub struct DepartureFixture { + pub cluster: TestCluster, + /// The founding node placement drops from `group`. + pub leaving: u64, + pub group: u64, + pub departed: BTreeSet, + pub names: Names, +} + +fn data_groups(routing: &nodedb_cluster::RoutingTable) -> Vec { + let mut groups: Vec = routing + .group_ids() + .into_iter() + .filter(|g| { + *g != nodedb_cluster::METADATA_GROUP_ID + && *g != nodedb_cluster::calvin::SEQUENCER_GROUP_ID + }) + .collect(); + groups.sort_unstable(); + groups +} + +fn collection_on(prefix: &str, vshards: &BTreeSet, wanted: bool) -> String { + (0..MAX_TRIES) + .map(|i| format!("{prefix}{i}x")) + .find(|name| { + let vshard = nodedb_types::CollectionKey::from_bare(DatabaseId::DEFAULT, name) + .vshard() + .as_u32(); + vshards.contains(&vshard) == wanted + }) + .unwrap_or_else(|| panic!("no collection name under {prefix} in {MAX_TRIES} tries")) +} + +fn key_on(prefix: &str, vshards: &BTreeSet, wanted: bool) -> String { + (0..MAX_TRIES) + .map(|i| format!("{prefix}{i}x")) + .find(|key| vshards.contains(&VShardId::from_key(key.as_bytes()).as_u32()) == wanted) + .unwrap_or_else(|| panic!("no node key under {prefix} in {MAX_TRIES} tries")) +} + +impl DepartureFixture { + /// Spawn the cluster, pick the leaving node and group, and write every + /// engine's rows on the group's vShards. + pub async fn seed() -> Self { + let cluster = TestCluster::spawn_three().await.expect("3-node cluster"); + let (group, leaving, departed, kept_out) = { + let routing = cluster.nodes[0] + .shared + .cluster_routing + .as_ref() + .expect("cluster routing") + .read() + .unwrap_or_else(|p| p.into_inner()); + let groups = data_groups(&routing); + let placement = + nodedb_cluster::rebalancer::placement::compute_placement(&[1, 2, 3, 4], &groups, 3); + let (group, leaving) = placement + .iter() + .find_map(|(group, placed)| { + (1..=3u64) + .find(|node| !placed.contains(node)) + .map(|node| (*group, node)) + }) + .expect("placement drops a founding node from some data group"); + let departed: BTreeSet = routing.vshards_for_group(group).into_iter().collect(); + // Every vShard of every group the leaving node leaves: a kept + // endpoint homes outside all of them. + let kept_out: BTreeSet = placement + .iter() + .filter(|(_, placed)| !placed.contains(&leaving)) + .flat_map(|(group, _)| routing.vshards_for_group(*group)) + .collect(); + (group, leaving, departed, kept_out) + }; + let names = Names { + doc: collection_on("vdpdoc", &departed, true), + strict: collection_on("vdpstrict", &departed, true), + kv: collection_on("vdpkv", &departed, true), + columnar: collection_on("vdpcol", &departed, true), + timeseries: collection_on("vdpts", &departed, true), + vector: collection_on("vdpvec", &departed, true), + spatial: collection_on("vdpgeo", &departed, true), + graph: collection_on("vdpgraph", &departed, true), + array: "vdparrx".to_string(), + crdt: collection_on("vdpcrdt", &departed, true), + src: key_on("vdpsrc", &departed, true), + dst: key_on("vdpdst", &departed, true), + kept_dst: key_on("vdpkept", &kept_out, false), + }; + let fixture = Self { + cluster, + leaving, + group, + departed, + names, + }; + fixture.write_rows().await; + fixture + .cluster + .wait_for_full_apply_convergence(Duration::from_secs(30)) + .await; + fixture + } + + async fn write_rows(&self) { + let n = &self.names; + let ddl = [ + format!( + "CREATE COLLECTION {} (id STRING PRIMARY KEY, body STRING) \ + WITH (engine='document_schemaless')", + n.doc + ), + format!( + "CREATE SEARCH INDEX idx_{0}_fts ON {0} FIELDS body ANALYZER 'standard'", + n.doc + ), + format!( + "CREATE COLLECTION {} (id STRING PRIMARY KEY, n INT) WITH (engine='document_strict')", + n.strict + ), + format!( + "CREATE COLLECTION {} (key STRING PRIMARY KEY, value STRING) WITH (engine='kv')", + n.kv + ), + format!( + "CREATE COLLECTION {} COLUMNS (id TEXT, region TEXT, ts BIGINT) \ + WITH (engine='columnar')", + n.columnar + ), + format!( + "CREATE COLLECTION {} COLUMNS (id TEXT, ts BIGINT TIME_KEY, metric TEXT, \ + value FLOAT) WITH (engine='timeseries')", + n.timeseries + ), + format!("CREATE COLLECTION {} TYPE document", n.vector), + format!( + "CREATE VECTOR INDEX idx_{0} ON {0} (embedding) METRIC cosine DIM 3", + n.vector + ), + format!( + "CREATE COLLECTION {} COLUMNS (id TEXT, location GEOMETRY, name TEXT) \ + WITH (engine='spatial')", + n.spatial + ), + format!("CREATE COLLECTION {}", n.graph), + format!("CREATE COLLECTION {}", n.crdt), + format!( + "CREATE ARRAY {} DIMS (x INT64 [0..63], y INT64 [0..63]) ATTRS (v INT64) \ + TILE_EXTENTS (4, 4)", + n.array + ), + ]; + for sql in &ddl { + self.cluster + .exec_ddl_on_any_leader(sql) + .await + .unwrap_or_else(|e| panic!("{sql}: {e}")); + } + let mut writes = vec![ + format!( + "INSERT INTO {} (id, body) VALUES ('d1', 'alpha beta')", + n.doc + ), + format!("INSERT INTO {} (id, n) VALUES ('s1', 7)", n.strict), + format!("INSERT INTO {} (key, value) VALUES ('k1', 'v1')", n.kv), + format!( + "INSERT INTO {} (id, region, ts) VALUES ('c1', 'eu', 1)", + n.columnar + ), + format!( + "INSERT INTO {} (id, ts, metric, value) VALUES ('t1', 1000, 'cpu', 1.0)", + n.timeseries + ), + format!( + "INSERT INTO {} (id, body, embedding) VALUES ('v1', 'x', ARRAY[0.1,0.2,0.3])", + n.vector + ), + format!( + "INSERT INTO {} (id, location, name) VALUES \ + ('p1', '{{\"type\":\"Point\",\"coordinates\":[-122.4,37.8]}}', 'SF')", + n.spatial + ), + format!( + "GRAPH INSERT EDGE IN '{}' FROM '{}' TO '{}' TYPE 'L'", + n.graph, n.src, n.dst + ), + format!( + "GRAPH INSERT EDGE IN '{}' FROM '{}' TO '{}' TYPE 'L'", + n.graph, n.src, n.kept_dst + ), + format!("GRAPH LABEL '{}' AS 'Departed'", n.src), + ]; + // One cell per 4x4 tile, so the cells spread over many vShards. + for row in 0..16u64 { + let cells: Vec = (0..16u64) + .map(|col| { + format!( + "COORDS ({}, {}) VALUES ({})", + row * 4, + col * 4, + row * 16 + col + ) + }) + .collect(); + writes.push(format!( + "INSERT INTO ARRAY {} {}", + n.array, + cells.join(", ") + )); + } + for sql in &writes { + self.cluster.nodes[0] + .exec(sql) + .await + .unwrap_or_else(|e| panic!("{sql}: {e}")); + } + write_crdt_row(&self.cluster.nodes[0], &n.crdt).await; + } + + /// The node placement drops from the group. + pub fn leaving_node(&self) -> &TestClusterNode { + self.cluster + .nodes + .iter() + .find(|node| node.node_id == self.leaving) + .expect("leaving node present") + } + + /// Add the fourth node and wait until every node's routing view drops + /// the leaving node from the group's voters. + pub async fn depart(&mut self) { + self.cluster + .add_learner_node() + .await + .expect("add the fourth node"); + let (group, leaving) = (self.group, self.leaving); + wait_for( + "every node sees the leaving node out of the group", + Duration::from_secs(90), + Duration::from_millis(200), + || { + self.cluster.nodes.iter().all(|node| { + let routing = node + .shared + .cluster_routing + .as_ref() + .expect("cluster routing") + .read() + .unwrap_or_else(|p| p.into_inner()); + routing.group_info(group).is_some_and(|info| { + !info.members.contains(&leaving) + && !info.learners.contains(&leaving) + && info.members.contains(&4) + }) + }) + }, + ) + .await; + } + + /// What `node`'s own cores hold of the departed vShards' data, one line + /// per kind. Empty once a purge finished there. + pub async fn stale_on(&self, node: &TestClusterNode) -> Vec { + let snapshots = node.stored_tenant_snapshots(TenantId::new(TENANT)).await; + stale_in(&snapshots, &self.names, &self.departed) + } + + /// Whether `node` still holds the edge with an endpoint on a vShard it + /// keeps. + pub async fn holds_kept_edge(&self, node: &TestClusterNode) -> bool { + let prefix = format!( + "{}\x00{}\x00L\x00{}\x00", + self.names.graph, self.names.src, self.names.kept_dst + ); + node.stored_tenant_snapshots(TenantId::new(TENANT)) + .await + .iter() + .any(|snap| snap.edges.iter().any(|(key, _)| key.starts_with(&prefix))) + } + + /// Every engine's rows read back through `node`, served by the vShards' + /// owners. + pub async fn assert_served(&self, node: &TestClusterNode, stage: &str) { + let n = &self.names; + for collection in n.collections() { + let rows = first_column(node, &format!("SELECT COUNT(*) FROM {collection}")).await; + assert_eq!( + rows, + vec!["1".to_string()], + "{stage}: {collection} serves its row" + ); + } + let edges = first_column( + node, + &format!("MATCH (a)-[:L]->(b) IN '{}' RETURN b", n.graph), + ) + .await; + let mut want = vec![n.dst.clone(), n.kept_dst.clone()]; + want.sort(); + assert_eq!(edges, want, "{stage}: both edges serve"); + let labeled = first_column( + node, + &format!("MATCH (a:Departed)-[:L]->(b) IN '{}' RETURN a", n.graph), + ) + .await; + assert!( + labeled.contains(&n.src), + "{stage}: the label serves: {labeled:?}" + ); + let cells = first_column( + node, + &format!( + "SELECT v FROM ARRAY_SLICE('{}', '{{x: [0, 63], y: [0, 63]}}', ['v'], 1000)", + n.array + ), + ) + .await; + assert_eq!( + cells.len() as u64, + ARRAY_CELLS, + "{stage}: every array cell serves" + ); + assert_crdt_served(node, &n.crdt, stage).await; + } +} + +/// The departed data `snapshots` hold, one line per kind. +fn stale_in( + snapshots: &[TenantDataSnapshot], + names: &Names, + departed: &BTreeSet, +) -> Vec { + let mut stale = Vec::new(); + for snap in snapshots { + for collection in names.collections() { + let keyed = snap + .documents + .iter() + .chain(&snap.kv_tables) + .chain(&snap.columnar_engines) + .chain(&snap.timeseries) + .chain(&snap.vectors) + .any(|(key, _)| key.contains(collection)) + || snap + .flushed_ts_segments + .iter() + .any(|blob| blob.collection_key.contains(collection)); + if keyed { + stale.push(format!("rows of {collection}")); + } + } + let departed_edge = format!("{}\x00{}\x00L\x00{}\x00", names.graph, names.src, names.dst); + if snap + .edges + .iter() + .any(|(key, _)| key.starts_with(&departed_edge)) + { + stale.push("the edge both of whose endpoints left".into()); + } + if snap + .node_labels + .iter() + .any(|(_, _, node, _)| node == &names.src) + || snap + .node_label_versions + .iter() + .any(|(_, _, node, _, _)| node == &names.src) + { + stale.push("the label of a node that left".into()); + } + if snap + .arrays + .iter() + .any(|blob| departed.contains(&blob.vshard)) + { + stale.push("array cells of a departed vShard".into()); + } + if holds_crdt_state(snap, &names.crdt) { + stale.push("the CRDT row of a departed vShard".into()); + } + } + stale +} + +/// The first column of every row `sql` returns through `node`, sorted. +pub async fn first_column(node: &TestClusterNode, sql: &str) -> Vec { + let messages = read_once_a_leader_exists( + sql, + Duration::from_secs(30), + Duration::from_millis(100), + || node.client.simple_query(sql), + ) + .await; + let mut values: Vec = messages + .iter() + .filter_map(|message| match message { + tokio_postgres::SimpleQueryMessage::Row(row) => row.get(0).map(str::to_owned), + _ => None, + }) + .collect(); + values.sort(); + values +} diff --git a/nodedb-cluster-tests/tests/common_suite/cases/vshard_departure_purge.rs b/nodedb-cluster-tests/tests/common_suite/cases/vshard_departure_purge.rs new file mode 100644 index 000000000..a09bcc9a7 --- /dev/null +++ b/nodedb-cluster-tests/tests/common_suite/cases/vshard_departure_purge.rs @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! A node that leaves a vShard's data group purges its copy of the vShard's +//! data once the new owners serve it. +//! +//! Before the move the leaving node's own cores hold every engine's rows of +//! the departed vShards. After placement drops it from the group, every +//! engine's rows, CRDT included, still read back through the cluster, and +//! the leaving node's cores hold none of them. An edge with one endpoint on a vShard the node +//! keeps stays there. + +use std::time::Duration; + +use super::vshard_departure_fixture::DepartureFixture; +use crate::common::cluster_harness::wait_for_async; + +#[tokio::test(flavor = "multi_thread", worker_threads = 6)] +async fn a_departed_vshard_is_purged_from_its_old_owner() { + let mut fx = DepartureFixture::seed().await; + + let before = fx.stale_on(fx.leaving_node()).await; + for kind in [ + "the edge both of whose endpoints left", + "the label of a node that left", + "array cells of a departed vShard", + "the CRDT row of a departed vShard", + ] { + assert!( + before.iter().any(|line| line == kind), + "the leaving node holds {kind} before the move: {before:?}" + ); + } + for collection in fx.names.collections() { + let line = format!("rows of {collection}"); + assert!( + before.contains(&line), + "the leaving node holds the rows of {collection} before the move: {before:?}" + ); + } + + fx.depart().await; + + let probe = &fx; + wait_for_async( + "the leaving node purges every departed row", + Duration::from_secs(60), + Duration::from_millis(250), + || async move { probe.stale_on(probe.leaving_node()).await.is_empty() }, + ) + .await; + assert!( + fx.holds_kept_edge(fx.leaving_node()).await, + "an edge with an endpoint on a vShard the node keeps stays" + ); + + let rows = fx + .leaving_node() + .shared + .credentials + .catalog() + .load_vshard_residency() + .expect("residency rows"); + for vshard in &fx.departed { + assert!( + rows.iter() + .all(|(v, row)| v != vshard || (row.departed && row.purged)), + "vShard {vshard} is departed and purged, or settled: {rows:?}" + ); + } + + for node in &fx.cluster.nodes { + fx.assert_served(node, &format!("through node {}", node.node_id)) + .await; + } + + fx.cluster.shutdown().await; +} diff --git a/nodedb-cluster-tests/tests/common_suite/cases/vshard_departure_purge_restart.rs b/nodedb-cluster-tests/tests/common_suite/cases/vshard_departure_purge_restart.rs new file mode 100644 index 000000000..1e4c3ce5a --- /dev/null +++ b/nodedb-cluster-tests/tests/common_suite/cases/vshard_departure_purge_restart.rs @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! A departure purge that a restart interrupts runs again at the next boot. +//! +//! The fail point `vshard_departure::purge` fails every purge on the leaving +//! node. The node records the departure, writes its barrier, and keeps the +//! stale rows. It then stops with the purge owed, and the fail point clears. +//! At the next boot the owed purge runs, and the node's cores hold none of +//! the departed rows, while every engine's rows still read back through the +//! cluster. +//! +//! Requires `--features failpoints`. + +#![cfg(feature = "failpoints")] + +use std::time::Duration; + +use nodedb_test_support::fail_point::{FailAction, FailGuard}; + +use super::vshard_departure_fixture::DepartureFixture; +use crate::common::cluster_harness::{wait_for, wait_for_async}; + +const PURGE_FAULT: &str = "vshard_departure::purge"; + +#[tokio::test(flavor = "multi_thread", worker_threads = 6)] +async fn an_interrupted_departure_purge_completes_after_a_restart() { + let mut fx = DepartureFixture::seed().await; + let fault = FailGuard::for_node( + fx.leaving, + PURGE_FAULT, + FailAction::Fail("injected departure purge fault".into()), + ); + + fx.depart().await; + + // The departure is recorded and owed; the purge itself never ran. + let owed = |fx: &DepartureFixture| { + let rows = fx + .leaving_node() + .shared + .credentials + .catalog() + .load_vshard_residency() + .expect("residency rows"); + fx.departed.iter().all(|vshard| { + rows.iter() + .any(|(v, row)| v == vshard && row.departed && !row.purged && row.barrier_lsn > 0) + }) + }; + wait_for( + "the leaving node records the departure and its barrier", + Duration::from_secs(60), + Duration::from_millis(250), + || owed(&fx), + ) + .await; + let stale = fx.stale_on(fx.leaving_node()).await; + assert!( + !stale.is_empty(), + "an interrupted purge leaves the stale rows in place" + ); + + let index = fx + .cluster + .nodes + .iter() + .position(|node| node.node_id == fx.leaving) + .expect("leaving node index"); + let stopped = fx + .cluster + .stop_member(index) + .await + .expect("stop the leaving node"); + drop(fault); + fx.cluster + .restart_member(stopped) + .await + .expect("restart the leaving node"); + + let probe = &fx; + wait_for_async( + "the restarted node runs the owed purge", + Duration::from_secs(60), + Duration::from_millis(250), + || async move { probe.stale_on(probe.leaving_node()).await.is_empty() }, + ) + .await; + assert!( + fx.holds_kept_edge(fx.leaving_node()).await, + "an edge with an endpoint on a vShard the node keeps stays" + ); + for node in &fx.cluster.nodes { + fx.assert_served( + node, + &format!("after the restart, through node {}", node.node_id), + ) + .await; + } + + fx.cluster.shutdown().await; +} diff --git a/nodedb-cluster-tests/tests/common_suite/cases/webhook_sink_partial_replication.rs b/nodedb-cluster-tests/tests/common_suite/cases/webhook_sink_partial_replication.rs index 56209bb4e..0ac97a174 100644 --- a/nodedb-cluster-tests/tests/common_suite/cases/webhook_sink_partial_replication.rs +++ b/nodedb-cluster-tests/tests/common_suite/cases/webhook_sink_partial_replication.rs @@ -93,7 +93,12 @@ async fn insert(cluster: &TestCluster, collection: &str, row: usize) { #[tokio::test(flavor = "multi_thread", worker_threads = 4)] async fn a_webhook_reads_events_from_the_node_that_holds_them() { let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind"); - let url = format!("http://{}/hook", listener.local_addr().expect("addr")); + // The egress policy refuses an IP-literal host, so the URL names the + // loopback listener by DNS name. + let url = format!( + "http://localhost:{}/hook", + listener.local_addr().expect("addr").port() + ); let deliveries: Deliveries = Arc::default(); let tokens: FencingTokens = Arc::default(); let server = tokio::spawn(serve( diff --git a/nodedb-cluster-tests/tests/common_suite/cases/webhook_sink_single_owner.rs b/nodedb-cluster-tests/tests/common_suite/cases/webhook_sink_single_owner.rs index 61832b72a..25a51ec62 100644 --- a/nodedb-cluster-tests/tests/common_suite/cases/webhook_sink_single_owner.rs +++ b/nodedb-cluster-tests/tests/common_suite/cases/webhook_sink_single_owner.rs @@ -139,7 +139,12 @@ async fn insert(cluster: &TestCluster, node: usize, row: usize) { #[tokio::test(flavor = "multi_thread", worker_threads = 4)] async fn a_webhook_delivers_every_event_once_across_a_failover() { let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind"); - let url = format!("http://{}/hook", listener.local_addr().expect("addr")); + // The egress policy refuses an IP-literal host, so the URL names the + // loopback listener by DNS name. + let url = format!( + "http://localhost:{}/hook", + listener.local_addr().expect("addr").port() + ); let deliveries: Deliveries = Arc::default(); let tokens: FencingTokens = Arc::default(); let server = tokio::spawn(serve( diff --git a/nodedb-cluster-tests/tests/dml_suite/cases/sql_cluster_cross_node_dml.rs b/nodedb-cluster-tests/tests/dml_suite/cases/sql_cluster_cross_node_dml.rs index e6cad4bb9..6bedbcd9e 100644 --- a/nodedb-cluster-tests/tests/dml_suite/cases/sql_cluster_cross_node_dml.rs +++ b/nodedb-cluster-tests/tests/dml_suite/cases/sql_cluster_cross_node_dml.rs @@ -19,6 +19,8 @@ mod cluster_boot; mod ddl_objects; #[path = "../../sql_cluster_cross_node_dml_tests/gather_cross_node.rs"] mod gather_cross_node; +#[path = "../../sql_cluster_cross_node_dml_tests/graph_algo_pagerank_as_of_cross_node.rs"] +mod graph_algo_pagerank_as_of_cross_node; #[path = "../../sql_cluster_cross_node_dml_tests/graph_algo_pagerank_cross_node.rs"] mod graph_algo_pagerank_cross_node; #[path = "../../sql_cluster_cross_node_dml_tests/graph_algo_pagerank_dangling_cross_node.rs"] @@ -39,6 +41,8 @@ mod graph_homed_read_per_core; mod graph_implicit_reverse_cross_node; #[path = "../../sql_cluster_cross_node_dml_tests/graph_match_cross_node.rs"] mod graph_match_cross_node; +#[path = "../../sql_cluster_cross_node_dml_tests/graph_match_label_as_of_cross_node.rs"] +mod graph_match_label_as_of_cross_node; #[path = "../../sql_cluster_cross_node_dml_tests/graph_match_read_set_cross_node.rs"] mod graph_match_read_set_cross_node; #[path = "../../sql_cluster_cross_node_dml_tests/graph_match_ryow_cross_node.rs"] @@ -59,6 +63,8 @@ mod graph_owner_reads_cross_node; mod graph_rag_fusion_cross_node; #[path = "../../sql_cluster_cross_node_dml_tests/graph_rag_fusion_naming_cross_node.rs"] mod graph_rag_fusion_naming_cross_node; +#[path = "../../sql_cluster_cross_node_dml_tests/graph_traverse_as_of_cross_node.rs"] +mod graph_traverse_as_of_cross_node; #[path = "../../sql_cluster_cross_node_dml_tests/graph_traverse_cross_node.rs"] mod graph_traverse_cross_node; #[path = "../../sql_cluster_cross_node_dml_tests/graph_traverse_reverse_cross_node.rs"] diff --git a/nodedb-cluster-tests/tests/sql_cluster_cross_node_dml_tests/graph_algo_pagerank_as_of_cross_node.rs b/nodedb-cluster-tests/tests/sql_cluster_cross_node_dml_tests/graph_algo_pagerank_as_of_cross_node.rs new file mode 100644 index 000000000..d9f8266a2 --- /dev/null +++ b/nodedb-cluster-tests/tests/sql_cluster_cross_node_dml_tests/graph_algo_pagerank_as_of_cross_node.rs @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! Historical distributed PageRank: `GRAPH ALGO PAGERANK ... AS OF SYSTEM TIME` +//! runs the BSP supersteps across every shard at one pinned cut. +//! +//! A ring spread across all three nodes is written, a timestamp taken, then +//! one ring edge is deleted and an edge to a new node added. A run at the +//! timestamp answers the ring from every coordinator: exactly the ring's +//! nodes, uniform ranks summing to 1.0. A current run answers the changed +//! graph. + +use std::collections::HashSet; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +use crate::common::cluster_harness::{TestCluster, wait_for}; + +const N: usize = 24; + +async fn pagerank_rows(client: &tokio_postgres::Client, sql: &str) -> Vec<(String, f64)> { + let msgs = client + .simple_query(sql) + .await + .expect("simple_query PAGERANK"); + let mut out = Vec::new(); + for m in &msgs { + if let tokio_postgres::SimpleQueryMessage::Row(r) = m { + let node = r.get("node_id").unwrap_or("").to_string(); + let rank: f64 = r.get("rank").unwrap_or("0").parse().unwrap_or(0.0); + out.push((node, rank)); + } + } + out +} + +/// Wait until `sql` run from every node names exactly `expected`. +async fn wait_every_node_sees(cluster: &TestCluster, sql: &str, expected: &HashSet) { + for idx in 0..cluster.nodes.len() { + wait_for( + &format!("node {idx} sees {} pagerank nodes", expected.len()), + Duration::from_secs(30), + Duration::from_millis(200), + || { + tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(async { + let rows = pagerank_rows(&cluster.nodes[idx].client, sql).await; + let names: HashSet = rows.into_iter().map(|(n, _)| n).collect(); + &names == expected + }) + }) + }, + ) + .await; + } +} + +fn now_ms() -> i64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("clock after the epoch") + .as_millis() as i64 +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 6)] +async fn historical_pagerank_runs_bsp_at_one_cut_cross_node() { + let cluster = TestCluster::spawn_three().await.expect("3-node cluster"); + cluster + .exec_ddl_on_any_leader("CREATE COLLECTION gpr_asof") + .await + .expect("CREATE COLLECTION gpr_asof"); + wait_for( + "all 3 nodes see gpr_asof", + Duration::from_secs(10), + Duration::from_millis(50), + || { + cluster + .nodes + .iter() + .all(|n| n.cached_collection_count() >= 1) + }, + ) + .await; + + let client = &cluster.nodes[0].client; + for i in 0..N { + let src = format!("n_{i}"); + let dst = format!("n_{}", (i + 1) % N); + client + .simple_query(&format!( + "GRAPH INSERT EDGE IN 'gpr_asof' FROM '{src}' TO '{dst}' TYPE 'K'" + )) + .await + .unwrap_or_else(|e| panic!("insert {src} -> {dst}: {e}")); + } + let ring: HashSet = (0..N).map(|i| format!("n_{i}")).collect(); + wait_every_node_sees(&cluster, "GRAPH ALGO PAGERANK ON 'gpr_asof'", &ring).await; + + tokio::time::sleep(Duration::from_millis(100)).await; + let cut = now_ms(); + tokio::time::sleep(Duration::from_millis(100)).await; + + client + .simple_query("GRAPH DELETE EDGE IN 'gpr_asof' FROM 'n_0' TO 'n_1' TYPE 'K'") + .await + .expect("delete ring edge"); + client + .simple_query("GRAPH INSERT EDGE IN 'gpr_asof' FROM 'n_0' TO 'extra' TYPE 'K'") + .await + .expect("insert edge to a new node"); + let mut changed = ring.clone(); + changed.insert("extra".to_string()); + wait_every_node_sees(&cluster, "GRAPH ALGO PAGERANK ON 'gpr_asof'", &changed).await; + + let historical = format!("GRAPH ALGO PAGERANK ON 'gpr_asof' AS OF SYSTEM TIME {cut}"); + for idx in 0..cluster.nodes.len() { + let rows = pagerank_rows(&cluster.nodes[idx].client, &historical).await; + let names: HashSet = rows.iter().map(|(n, _)| n.clone()).collect(); + assert_eq!( + names, ring, + "node {idx}: a run at the cut covers exactly the ring; got {rows:?}" + ); + assert_eq!(rows.len(), N, "node {idx}: one row per node; got {rows:?}"); + let sum: f64 = rows.iter().map(|(_, r)| *r).sum(); + assert!( + (sum - 1.0).abs() < 1e-3, + "node {idx}: historical ranks sum to ~1.0; got {sum}" + ); + let uniform = 1.0 / N as f64; + for (node, rank) in &rows { + assert!( + (rank - uniform).abs() < 1e-3, + "node {idx}: the ring is uniform at the cut; {node} has {rank}" + ); + } + } + + cluster.shutdown().await; +} diff --git a/nodedb-cluster-tests/tests/sql_cluster_cross_node_dml_tests/graph_match_label_as_of_cross_node.rs b/nodedb-cluster-tests/tests/sql_cluster_cross_node_dml_tests/graph_match_label_as_of_cross_node.rs new file mode 100644 index 000000000..9db0d8ac0 --- /dev/null +++ b/nodedb-cluster-tests/tests/sql_cluster_cross_node_dml_tests/graph_match_label_as_of_cross_node.rs @@ -0,0 +1,153 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! Historical label-filtered `MATCH ... AS OF SYSTEM TIME` across nodes. +//! +//! A label write runs as a Calvin transaction. Its redo record carries the +//! transaction's ordinal, so every replica writes the node's label version at +//! that one ordinal. A historical read pins one cut, and every owner waits +//! until it installed every version at or below it. So a label-filtered MATCH +//! at the cut answers the labels at the cut, the same from every node, after +//! later label changes too. + +use std::collections::HashSet; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +use crate::common::cluster_harness::{TestCluster, wait_for}; + +const COLL: &str = "glabel_asof"; +const SOURCES: usize = 8; + +type Pairs = HashSet<(String, String)>; + +/// The `(a, b)` rows of a label-filtered `MATCH (a:L)-[:K]->(b)`. +async fn pairs(client: &tokio_postgres::Client, sql: &str) -> Vec<(String, String)> { + let msgs = client + .simple_query(sql) + .await + .unwrap_or_else(|e| panic!("{sql}: {e}")); + msgs.iter() + .filter_map(|m| match m { + tokio_postgres::SimpleQueryMessage::Row(r) => Some(( + r.get("a").unwrap_or("").to_string(), + r.get("b").unwrap_or("").to_string(), + )), + _ => None, + }) + .collect() +} + +fn match_sql(label: &str, as_of: Option) -> String { + let cut = as_of.map_or(String::new(), |ms| format!(" AS OF SYSTEM TIME {ms}")); + format!("MATCH (a:{label})-[:K]->(b) IN '{COLL}'{cut} RETURN a, b") +} + +async fn exec(client: &tokio_postgres::Client, sql: &str) { + client + .simple_query(sql) + .await + .unwrap_or_else(|e| panic!("{sql}: {e}")); +} + +fn now_ms() -> i64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("clock after the epoch") + .as_millis() as i64 +} + +fn source_pairs(sources: impl IntoIterator) -> Pairs { + sources + .into_iter() + .map(|i| (format!("src_{i}"), format!("dst_{i}"))) + .collect() +} + +/// Wait until `sql` from every node returns exactly `expected`. +async fn wait_every_node_reads(cluster: &TestCluster, sql: &str, expected: &Pairs) { + for idx in 0..cluster.nodes.len() { + wait_for( + &format!("node {idx} reads {} rows of {sql}", expected.len()), + Duration::from_secs(30), + Duration::from_millis(100), + || { + tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(async { + let rows = pairs(&cluster.nodes[idx].client, sql).await; + &rows.into_iter().collect::() == expected + }) + }) + }, + ) + .await; + } +} + +/// Assert `sql` from every node returns exactly `expected`, each row once. +async fn assert_every_node_reads(cluster: &TestCluster, sql: &str, expected: &Pairs) { + for idx in 0..cluster.nodes.len() { + let rows = pairs(&cluster.nodes[idx].client, sql).await; + let set: Pairs = rows.iter().cloned().collect(); + assert_eq!( + rows.len(), + set.len(), + "node {idx}: duplicate rows in {sql}: {rows:?}" + ); + assert_eq!(&set, expected, "node {idx}: {sql} returned {rows:?}"); + } +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 6)] +async fn historical_label_match_reads_the_cut_from_every_node() { + let cluster = TestCluster::spawn_three().await.expect("3-node cluster"); + cluster + .exec_ddl_on_any_leader(&format!("CREATE COLLECTION {COLL}")) + .await + .expect("CREATE COLLECTION"); + wait_for( + "all 3 nodes see the collection", + Duration::from_secs(10), + Duration::from_millis(50), + || { + cluster + .nodes + .iter() + .all(|n| n.cached_collection_count() >= 1) + }, + ) + .await; + + // Distinct source names spread across vShards on all three nodes, so the + // label versions live on every node. + let client = &cluster.nodes[0].client; + for i in 0..SOURCES { + exec( + client, + &format!("GRAPH INSERT EDGE IN '{COLL}' FROM 'src_{i}' TO 'dst_{i}' TYPE 'K'"), + ) + .await; + exec(client, &format!("GRAPH LABEL 'src_{i}' AS 'Hot'")).await; + } + let all = source_pairs(0..SOURCES); + wait_every_node_reads(&cluster, &match_sql("Hot", None), &all).await; + + tokio::time::sleep(Duration::from_millis(50)).await; + let cut = now_ms(); + tokio::time::sleep(Duration::from_millis(50)).await; + + // Move every even source from `Hot` to `Cold`. + let even: Vec = (0..SOURCES).filter(|i| i % 2 == 0).collect(); + for i in &even { + exec(client, &format!("GRAPH UNLABEL 'src_{i}' AS 'Hot'")).await; + exec(client, &format!("GRAPH LABEL 'src_{i}' AS 'Cold'")).await; + } + let odd = source_pairs((0..SOURCES).filter(|i| i % 2 == 1)); + let cold = source_pairs(even.iter().copied()); + wait_every_node_reads(&cluster, &match_sql("Hot", None), &odd).await; + wait_every_node_reads(&cluster, &match_sql("Cold", None), &cold).await; + + // At the cut every source was `Hot` and none was `Cold`, on every node. + assert_every_node_reads(&cluster, &match_sql("Hot", Some(cut)), &all).await; + assert_every_node_reads(&cluster, &match_sql("Cold", Some(cut)), &Pairs::new()).await; + + cluster.shutdown().await; +} diff --git a/nodedb-cluster-tests/tests/sql_cluster_cross_node_dml_tests/graph_traverse_as_of_cross_node.rs b/nodedb-cluster-tests/tests/sql_cluster_cross_node_dml_tests/graph_traverse_as_of_cross_node.rs new file mode 100644 index 000000000..827a0207f --- /dev/null +++ b/nodedb-cluster-tests/tests/sql_cluster_cross_node_dml_tests/graph_traverse_as_of_cross_node.rs @@ -0,0 +1,205 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! Historical `GRAPH TRAVERSE ... AS OF SYSTEM TIME` across nodes. +//! +//! Edges are keyed by source node, so the hops of a 2-hop walk expand on the +//! nodes that own each frontier node's vShard. A historical walk pins one +//! cut on the coordinator. Every owner waits until it installed every edge +//! version at or below that cut before it expands a hop, so the walk answers +//! the full graph at the cut from any coordinator: +//! +//! - right after the writes, before any wait for replication, and +//! - after later deletes and inserts changed the current graph. + +use std::collections::HashSet; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +use crate::common::cluster_harness::{TestCluster, wait_for}; + +const COLL: &str = "gtrav_asof"; +const MIDS: usize = 8; + +/// The node ids a `GRAPH TRAVERSE` result row names. +async fn traverse_ids(client: &tokio_postgres::Client, sql: &str) -> Vec { + let msgs = client + .simple_query(sql) + .await + .unwrap_or_else(|e| panic!("{sql}: {e}")); + let row = msgs + .iter() + .find_map(|m| match m { + tokio_postgres::SimpleQueryMessage::Row(r) => Some(r), + _ => None, + }) + .unwrap_or_else(|| panic!("{sql}: no result row")); + let raw = row.get("result").expect("result column present"); + let v: serde_json::Value = serde_json::from_str(raw).expect("result column is valid JSON"); + v.get("nodes") + .and_then(|n| n.as_array()) + .expect("traverse result has a nodes array") + .iter() + .map(|n| { + n.get("id") + .and_then(|id| id.as_str()) + .expect("node has string id") + .to_string() + }) + .collect() +} + +/// Wait until a traverse from every node reaches exactly `expected`. +async fn wait_every_node_reaches(cluster: &TestCluster, sql: &str, expected: &HashSet) { + for idx in 0..cluster.nodes.len() { + wait_for( + &format!("node {idx} reaches {} nodes", expected.len()), + Duration::from_secs(30), + Duration::from_millis(100), + || { + tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(async { + let ids = traverse_ids(&cluster.nodes[idx].client, sql).await; + &ids.into_iter().collect::>() == expected + }) + }) + }, + ) + .await; + } +} + +/// Assert a traverse from every node returns exactly `expected`, each id once. +async fn assert_every_node_reaches(cluster: &TestCluster, sql: &str, expected: &HashSet) { + for idx in 0..cluster.nodes.len() { + let ids = traverse_ids(&cluster.nodes[idx].client, sql).await; + let set: HashSet = ids.iter().cloned().collect(); + assert_eq!( + ids.len(), + set.len(), + "node {idx}: duplicate node ids in {sql}: {ids:?}" + ); + assert_eq!(&set, expected, "node {idx}: {sql} returned {ids:?}"); + } +} + +async fn exec(client: &tokio_postgres::Client, sql: &str) { + client + .simple_query(sql) + .await + .unwrap_or_else(|e| panic!("{sql}: {e}")); +} + +fn now_ms() -> i64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("clock after the epoch") + .as_millis() as i64 +} + +fn ids(names: impl IntoIterator) -> HashSet { + names.into_iter().collect() +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 6)] +async fn historical_traverse_reads_the_full_cut_from_any_node() { + let cluster = TestCluster::spawn_three().await.expect("3-node cluster"); + cluster + .exec_ddl_on_any_leader(&format!("CREATE COLLECTION {COLL}")) + .await + .expect("CREATE COLLECTION"); + wait_for( + "all 3 nodes see the collection", + Duration::from_secs(10), + Duration::from_millis(50), + || { + cluster + .nodes + .iter() + .all(|n| n.cached_collection_count() >= 1) + }, + ) + .await; + + let client = &cluster.nodes[0].client; + + // Warm-up: every node's routing view converges, so a coordinator sends + // each frontier node to its owner. + for i in 0..10 { + exec( + client, + &format!("GRAPH INSERT EDGE IN '{COLL}' FROM 'warm' TO 'w_{i}' TYPE 'l'"), + ) + .await; + } + let warm = ids(std::iter::once("warm".to_string()).chain((0..10).map(|i| format!("w_{i}")))); + wait_every_node_reaches( + &cluster, + &format!("GRAPH TRAVERSE IN '{COLL}' FROM 'warm' DEPTH 1 LABEL 'l' DIRECTION out"), + &warm, + ) + .await; + + // root -> m_i -> l_i_0, l_i_1. Each m_i expands on its own owner. + for i in 0..MIDS { + exec( + client, + &format!("GRAPH INSERT EDGE IN '{COLL}' FROM 'root' TO 'm_{i}' TYPE 'l'"), + ) + .await; + for j in 0..2 { + exec( + client, + &format!("GRAPH INSERT EDGE IN '{COLL}' FROM 'm_{i}' TO 'l_{i}_{j}' TYPE 'l'"), + ) + .await; + } + } + let at_cut = ids(std::iter::once("root".to_string()) + .chain((0..MIDS).map(|i| format!("m_{i}"))) + .chain((0..MIDS).flat_map(|i| (0..2).map(move |j| format!("l_{i}_{j}"))))); + + // The cut follows every acknowledged write. No wait for replication: a + // remote owner that has not installed the writes yet waits for the cut + // before it expands its hop. + tokio::time::sleep(Duration::from_millis(50)).await; + let cut = now_ms(); + tokio::time::sleep(Duration::from_millis(50)).await; + let historical = format!( + "GRAPH TRAVERSE IN '{COLL}' FROM 'root' DEPTH 2 LABEL 'l' DIRECTION out \ + AS OF SYSTEM TIME {cut}" + ); + assert_every_node_reaches(&cluster, &historical, &at_cut).await; + + // Change the current graph: cut one hop-1 edge and one hop-2 edge, add a + // new hop-1 node. + exec( + client, + &format!("GRAPH DELETE EDGE IN '{COLL}' FROM 'root' TO 'm_0' TYPE 'l'"), + ) + .await; + exec( + client, + &format!("GRAPH DELETE EDGE IN '{COLL}' FROM 'm_1' TO 'l_1_0' TYPE 'l'"), + ) + .await; + exec( + client, + &format!("GRAPH INSERT EDGE IN '{COLL}' FROM 'root' TO 'late' TYPE 'l'"), + ) + .await; + let mut current = at_cut.clone(); + for gone in ["m_0", "l_0_0", "l_0_1", "l_1_0"] { + current.remove(gone); + } + current.insert("late".to_string()); + wait_every_node_reaches( + &cluster, + &format!("GRAPH TRAVERSE IN '{COLL}' FROM 'root' DEPTH 2 LABEL 'l' DIRECTION out"), + ¤t, + ) + .await; + + // The walk at the cut still reads the graph before the changes. + assert_every_node_reaches(&cluster, &historical, &at_cut).await; + + cluster.shutdown().await; +} diff --git a/nodedb-cluster/src/distributed_array/coordinator/elementwise.rs b/nodedb-cluster/src/distributed_array/coordinator/elementwise.rs index 0bfbbca74..14946db1b 100644 --- a/nodedb-cluster/src/distributed_array/coordinator/elementwise.rs +++ b/nodedb-cluster/src/distributed_array/coordinator/elementwise.rs @@ -119,7 +119,7 @@ mod tests { op_msgpack: vec![0x00], attr_idx: 0, shard_bucket: None, - system_as_of: None, + system_time: nodedb_types::SystemTimeScope::Current, valid_at_ms: None, txn_id: None, } diff --git a/nodedb-cluster/src/distributed_array/coordinator/read.rs b/nodedb-cluster/src/distributed_array/coordinator/read.rs index 6a98607a3..711fce833 100644 --- a/nodedb-cluster/src/distributed_array/coordinator/read.rs +++ b/nodedb-cluster/src/distributed_array/coordinator/read.rs @@ -435,6 +435,7 @@ mod tests { valid_at_ms: None, txn_id: None, zone_msgpack: vec![], + physical: false, }; // 3 shards × 2 rows each = 6 merged rows. @@ -477,6 +478,7 @@ mod tests { valid_at_ms: None, txn_id: None, zone_msgpack: vec![], + physical: false, }; let result = coord @@ -495,10 +497,11 @@ mod tests { group_by_dim: -1, cell_filter_msgpack: vec![], shard_bucket: None, - system_as_of: None, + system_time: nodedb_types::SystemTimeScope::Current, valid_at_ms: None, txn_id: None, region_msgpack: vec![], + physical: false, } } @@ -663,6 +666,7 @@ mod tests { valid_at_ms: None, txn_id: None, zone_msgpack: vec![], + physical: false, }; // coordinator_limit = 0 → no cutoff → 20 rows. @@ -738,6 +742,7 @@ mod tests { valid_at_ms: None, txn_id: None, zone_msgpack: vec![], + physical: false, }; let result = coord .coord_slice(req, 0, nodedb_types::SystemTimeScope::Current) diff --git a/nodedb-cluster/src/distributed_array/handler.rs b/nodedb-cluster/src/distributed_array/handler.rs index 049043037..13d55ce19 100644 --- a/nodedb-cluster/src/distributed_array/handler.rs +++ b/nodedb-cluster/src/distributed_array/handler.rs @@ -417,16 +417,15 @@ mod tests { valid_at_ms: None, txn_id: None, zone_msgpack: vec![], + physical: false, }; zerompk::to_msgpack_vec(&req).unwrap() } fn make_agg_req_bytes() -> Vec { - // Encode a minimal Sum reducer (c_enum byte). - // We use rmp_serde directly since we don't have ArrayReducer here, - // but any single-byte msgpack value works as the reducer payload for - // StubExecutor (it never decodes it). - let reducer_bytes = vec![0x00u8]; // Sum = 0 as c_enum + // StubExecutor never decodes the reducer, so any msgpack value + // serves as its payload. + let reducer_bytes = vec![0x00u8]; let req = super::super::wire::ArrayShardAggReq { array_id_msgpack: vec![], attr_idx: 0, @@ -434,10 +433,11 @@ mod tests { group_by_dim: -1, cell_filter_msgpack: vec![], shard_bucket: None, - system_as_of: None, + system_time: nodedb_types::SystemTimeScope::Current, valid_at_ms: None, txn_id: None, region_msgpack: vec![], + physical: false, }; zerompk::to_msgpack_vec(&req).unwrap() } @@ -466,7 +466,7 @@ mod tests { op_msgpack: vec![0x00], attr_idx: 0, shard_bucket: None, - system_as_of: None, + system_time: nodedb_types::SystemTimeScope::Current, valid_at_ms: None, txn_id: None, }; @@ -781,6 +781,7 @@ mod tests { valid_at_ms: None, txn_id: None, zone_msgpack: vec![], + physical: false, } } diff --git a/nodedb-cluster/src/distributed_array/merge.rs b/nodedb-cluster/src/distributed_array/merge.rs index d29c039a1..e119f6a60 100644 --- a/nodedb-cluster/src/distributed_array/merge.rs +++ b/nodedb-cluster/src/distributed_array/merge.rs @@ -20,6 +20,8 @@ use serde::{Deserialize, Serialize}; +use nodedb_types::array_agg::ValueDist; + use super::agg_number::ArrayAggNumber; use super::group_key::ArrayAggGroupKey; use super::wire::{ArrayShardAggResp, ArrayShardSliceResp}; @@ -27,8 +29,10 @@ use crate::error::{ClusterError, Result}; /// Partial aggregate contributed by a single shard for one group-by bucket. /// -/// Carries enough state for all supported reducers (SUM, COUNT, MIN, MAX, -/// MEAN). Welford fields enable variance/stddev if a future reducer needs it. +/// Carries enough state for every reducer: SUM, COUNT, MIN, MAX and MEAN +/// from the running numbers, the variances and standard deviations from the +/// Welford fields, MEDIAN / PERCENTILE / MODE from the exact `dist`, and +/// HISTOGRAM from `bins`. Every field merges exactly. #[derive( Debug, Clone, Serialize, Deserialize, zerompk::ToMessagePack, zerompk::FromMessagePack, )] @@ -43,6 +47,11 @@ pub struct ArrayAggPartial { /// Welford mean — enables MEAN without a second pass. pub welford_mean: f64, pub welford_m2: f64, + /// Every distinct value with its count. `Empty` for a reducer that + /// needs no distribution. + pub dist: ValueDist, + /// Histogram bin counts. Empty for any other reducer. + pub bins: Vec, } impl ArrayAggPartial { @@ -65,6 +74,8 @@ impl ArrayAggPartial { max: val, welford_mean: val.as_f64(), welford_m2: 0.0, + dist: ValueDist::Empty, + bins: Vec::new(), } } @@ -78,18 +89,32 @@ impl ArrayAggPartial { max: ArrayAggNumber::Float(f64::NEG_INFINITY), welford_mean: 0.0, welford_m2: 0.0, + dist: ValueDist::Empty, + bins: Vec::new(), } } /// Merge another partial into this one using parallel Welford. + /// + /// Histogram bins add even when a side counted no value in range, so a + /// group keeps its bins. pub fn merge(&mut self, other: &ArrayAggPartial) { + if self.bins.len() < other.bins.len() { + self.bins.resize(other.bins.len(), 0); + } + for (slot, n) in self.bins.iter_mut().zip(&other.bins) { + *slot = slot.saturating_add(*n); + } if other.count == 0 { return; } if self.count == 0 { + let bins = std::mem::take(&mut self.bins); *self = other.clone(); + self.bins = bins; return; } + self.dist = std::mem::replace(&mut self.dist, ValueDist::Empty).merge(other.dist.clone()); self.sum = self.sum.sum_with(other.sum); self.min = self.min.min_with(other.min); self.max = self.max.max_with(other.max); @@ -107,7 +132,7 @@ impl ArrayAggPartial { } } -/// Returns `true` if any shard reported that `system_as_of` fell below its +/// Returns `true` if any shard reported that the system-time cutoff fell below its /// oldest tile version and it produced zero rows as a result. /// /// Callers combine this flag via logical OR across shards to propagate the @@ -116,7 +141,7 @@ pub fn any_truncated_before_horizon_slice(shard_resps: &[ArrayShardSliceResp]) - shard_resps.iter().any(|r| r.truncated_before_horizon) } -/// Returns `true` if any shard reported that `system_as_of` fell below its +/// Returns `true` if any shard reported that the system-time cutoff fell below its /// oldest tile version, causing the shard to contribute zero partials. pub fn any_truncated_before_horizon_agg(shard_resps: &[ArrayShardAggResp]) -> bool { shard_resps.iter().any(|r| r.truncated_before_horizon) @@ -330,6 +355,36 @@ mod tests { assert!((a.sum.as_f64() - 42.0).abs() < f64::EPSILON); } + /// Distributions and histogram bins merge exactly across shards, and a + /// shard with no in-range value still keeps its bins. + #[test] + fn dist_and_bins_merge_across_shards() { + use nodedb_types::array_agg::IntRun; + let with = |runs: Vec<(i64, u64)>, bins: Vec| ArrayAggPartial { + count: runs.iter().map(|r| r.1).sum(), + dist: ValueDist::Ints( + runs.into_iter() + .map(|(value, count)| IntRun { value, count }) + .collect(), + ), + bins, + ..ArrayAggPartial::empty(g(0)) + }; + let mut a = with(vec![(1, 1), (5, 2)], vec![1, 2]); + a.merge(&with(vec![(5, 1), (9, 1)], vec![0, 2])); + assert_eq!(a.count, 5); + assert_eq!(a.dist.percentile(0.5), Ok(Some(5.0))); + assert_eq!(a.bins, vec![1, 4]); + + let mut empty = ArrayAggPartial { + bins: vec![0, 0], + ..ArrayAggPartial::empty(g(0)) + }; + empty.merge(&with(vec![(3, 1)], vec![1, 0])); + assert_eq!(empty.bins, vec![1, 0]); + assert_eq!(empty.count, 1); + } + #[test] fn merge_slice_rows_concatenates() { let r0 = ArrayShardSliceResp { diff --git a/nodedb-cluster/src/distributed_array/wire.rs b/nodedb-cluster/src/distributed_array/wire.rs index bc6209fe1..507f9157f 100644 --- a/nodedb-cluster/src/distributed_array/wire.rs +++ b/nodedb-cluster/src/distributed_array/wire.rs @@ -57,6 +57,8 @@ pub struct ArrayShardSliceReq { /// Zerompk encoding of `nodedb_array::query::PolygonZone`, forwarded from /// `ArrayOp::Slice::zone_msgpack`. Empty = no zone. pub zone_msgpack: Vec, + /// Read attrs physically, forwarded from `ArrayOp::Slice::physical`. + pub physical: bool, } /// Gather response: shard returns matching rows as opaque msgpack row bytes. @@ -67,7 +69,7 @@ pub struct ArrayShardSliceResp { pub rows_msgpack: Vec>, /// True when the shard hit the `limit` and may have more rows. pub truncated: bool, - /// True when `system_as_of` is below the oldest tile version on this shard + /// True when the system-time cutoff is below the oldest tile version on this shard /// and the shard produced zero rows as a result of that horizon. pub truncated_before_horizon: bool, /// The write versions the shard read observed, one per vShard it covers, @@ -83,7 +85,7 @@ pub struct ArrayShardSliceResp { pub struct ArrayShardAggReq { pub array_id_msgpack: Vec, pub attr_idx: u32, - /// Zerompk encoding of `ArrayReducer` (c_enum, 1 byte on wire). + /// Zerompk encoding of `ArrayReducer`. pub reducer_msgpack: Vec, /// Negative means no group-by; non-negative is the dimension index. pub group_by_dim: i32, @@ -92,9 +94,10 @@ pub struct ArrayShardAggReq { /// The bucket this shard owns. The shard counts only cells whose tile /// key falls in it. `None` counts every cell. pub shard_bucket: Option, - /// Bitemporal system-time cutoff forwarded from `ArrayOp::Aggregate::system_as_of`. - /// `None` = live read. - pub system_as_of: Option, + /// Bitemporal system-time scope forwarded from + /// `ArrayOp::Aggregate::system_time`. A `Pinned` scope names the cut + /// marker the shard waits for before it reads. + pub system_time: nodedb_types::SystemTimeScope, /// Bitemporal valid-time point forwarded from `ArrayOp::Aggregate::valid_at_ms`. /// `None` = no valid-time filter. pub valid_at_ms: Option, @@ -111,6 +114,8 @@ pub struct ArrayShardAggReq { /// Zerompk encoding of `nodedb_array::query::ArrayRegion`, forwarded from /// `ArrayOp::Aggregate::region_msgpack`. Empty = the whole array. pub region_msgpack: Vec, + /// Reduce physical values, forwarded from `ArrayOp::Aggregate::physical`. + pub physical: bool, } /// Gather response: shard returns partial aggregate(s) for merge. @@ -119,7 +124,7 @@ pub struct ArrayShardAggResp { pub shard_id: u32, /// One partial per group-by bucket (or one entry when group_by_dim < 0). pub partials: Vec, - /// True when `system_as_of` is below the oldest tile version on this shard + /// True when the system-time cutoff is below the oldest tile version on this shard /// and the shard produced zero rows as a result of that horizon. pub truncated_before_horizon: bool, /// The write versions the shard read observed (see @@ -147,8 +152,9 @@ pub struct ArrayShardElementwiseReq { /// falls in it, so vShards that share one Data Plane emit no row twice. /// `None` pairs every cell. pub shard_bucket: Option, - /// Bitemporal system-time cutoff for both operands. `None` = live read. - pub system_as_of: Option, + /// Bitemporal system-time scope for both operands (see + /// [`ArrayShardAggReq::system_time`]). + pub system_time: nodedb_types::SystemTimeScope, /// Bitemporal valid-time point for both operands. `None` = no /// valid-time filter. pub valid_at_ms: Option, diff --git a/nodedb-cluster/src/lib.rs b/nodedb-cluster/src/lib.rs index 5ed99f51c..6f5e90aed 100644 --- a/nodedb-cluster/src/lib.rs +++ b/nodedb-cluster/src/lib.rs @@ -117,9 +117,9 @@ pub use migration_executor::{ }; pub use multi_raft::{GroupStatus, MultiRaft}; pub use raft_loop::{ - ApplyPermit, AssignRemoteSurrogate, AuthLeaseService, BuiltGroupSnapshot, CalvinSubmit, - CalvinSubmitInbox, CommitApplier, DataProposeGate, GroupApplyGates, InstallPermit, - MetadataSnapshotCapture, ProposeHold, RaftLoop, ReleaseReservation, ReserveRead, + Admitted, ApplyPermit, AssignRemoteSurrogate, AuthLeaseService, BuiltGroupSnapshot, + CalvinSubmit, CalvinSubmitInbox, CommitApplier, DataProposeGate, GroupApplyGates, + InstallPermit, MetadataSnapshotCapture, ProposeHold, RaftLoop, ReleaseReservation, ReserveRead, ShuffleAggregator, ShuffleConsumer, ShuffleProducer, ShuffleReceiver, SnapshotApplier, SnapshotBuilder, SnapshotQuarantineHook, VShardEnvelopeHandler, }; diff --git a/nodedb-cluster/src/raft_loop/mod.rs b/nodedb-cluster/src/raft_loop/mod.rs index 039f16c38..7a9a8efa2 100644 --- a/nodedb-cluster/src/raft_loop/mod.rs +++ b/nodedb-cluster/src/raft_loop/mod.rs @@ -49,4 +49,4 @@ pub use hooks::{ }; pub use in_flight_snapshots::{InFlightSnapshotGuard, InFlightSnapshots}; pub use loop_core::{CommitApplier, RaftLoop, VShardEnvelopeHandler}; -pub use propose_gate::{DataProposeGate, ProposeHold}; +pub use propose_gate::{Admitted, DataProposeGate, ProposeHold}; diff --git a/nodedb-cluster/src/raft_loop/propose_gate.rs b/nodedb-cluster/src/raft_loop/propose_gate.rs index 198a854d5..6e7a3c170 100644 --- a/nodedb-cluster/src/raft_loop/propose_gate.rs +++ b/nodedb-cluster/src/raft_loop/propose_gate.rs @@ -12,6 +12,10 @@ //! leader through [`RaftLoop::propose_admitted`]. `nodedb-cluster` cannot //! depend on `nodedb`, so the gate lives in the host crate behind //! [`DataProposeGate`]. +//! +//! The gate also stamps the entry with what only the leader decides: the +//! host returns the stamped bytes, and the leader proposes those in place of +//! the bytes the proposer sent. use crate::error::Result; use crate::forward::PlanExecutor; @@ -25,6 +29,17 @@ pub trait ProposeHold: Send { fn landed(self: Box, group_id: u64, log_index: u64); } +/// What the write gate admitted for one entry. +#[derive(Default)] +pub struct Admitted { + /// The lock keys the entry holds. `None` when it takes none. + pub hold: Option>, + /// The bytes the leader proposes in place of the bytes the proposer + /// sent: the entry with the leader's stamps. `None` proposes the bytes + /// as sent. + pub entry: Option>, +} + /// The host's write gate for data-group proposals on their leader. #[async_trait::async_trait] pub trait DataProposeGate: Send + Sync + 'static { @@ -32,9 +47,9 @@ pub trait DataProposeGate: Send + Sync + 'static { /// `vshard_id`, on this leader. A contended entry waits for its keys /// until `deadline`, the proposer's own deadline. /// - /// - `Ok(None)`: the entry takes no lock key. - /// - `Ok(Some(hold))`: the entry holds its keys until `hold` releases - /// them. + /// - `Ok(admitted)` with no `hold`: the entry takes no lock key. + /// - `Ok(admitted)` with a `hold`: the entry holds its keys until the + /// hold releases them. /// - `Err(ClusterError::Calvin(CalvinError::RouteToSequencer))`: a held /// key blocks the write, and the proposer submits it to the Calvin /// sequencer. @@ -45,7 +60,7 @@ pub trait DataProposeGate: Send + Sync + 'static { vshard_id: u32, entry: &[u8], deadline: tokio::time::Instant, - ) -> Result>>; + ) -> Result; } impl RaftLoop { @@ -63,13 +78,14 @@ impl RaftLoop { data: &[u8], deadline: tokio::time::Instant, ) -> Result<(u64, u64)> { - let hold = match &self.data_propose_gate { + let Admitted { hold, entry } = match &self.data_propose_gate { Some(gate) if self.leads_group_of(vshard_id) => { gate.admit(vshard_id, data, deadline).await? } - Some(_) | None => None, + Some(_) | None => Admitted::default(), }; - let (group_id, log_index) = self.propose(vshard_id, data.to_vec())?; + let entry = entry.unwrap_or_else(|| data.to_vec()); + let (group_id, log_index) = self.propose(vshard_id, entry)?; if let Some(hold) = hold { hold.landed(group_id, log_index); } diff --git a/nodedb-cluster/src/rpc_codec/data_plane_error.rs b/nodedb-cluster/src/rpc_codec/data_plane_error.rs index e1157d806..2fb766a97 100644 --- a/nodedb-cluster/src/rpc_codec/data_plane_error.rs +++ b/nodedb-cluster/src/rpc_codec/data_plane_error.rs @@ -212,6 +212,10 @@ pub enum DataPlaneErrorCode { label: String, limit: u64, }, + /// The statement passed a fixed program limit (SQLSTATE `54000`). + ProgramLimitExceeded { + detail: String, + }, /// Text does not parse as the column's type (SQLSTATE `22P02`). InvalidTextRepresentation { detail: String, diff --git a/nodedb-columnar/Cargo.toml b/nodedb-columnar/Cargo.toml index bb7f09ebc..950f16527 100644 --- a/nodedb-columnar/Cargo.toml +++ b/nodedb-columnar/Cargo.toml @@ -22,6 +22,7 @@ diagnostics = ["dep:faultbox"] [dependencies] nodedb-types = { workspace = true } +nodedb-spatial = { workspace = true } nodedb-codec = { workspace = true } nodedb-mem = { workspace = true } thiserror = { workspace = true } diff --git a/nodedb-columnar/src/error.rs b/nodedb-columnar/src/error.rs index 56f4e926c..a1d254b72 100644 --- a/nodedb-columnar/src/error.rs +++ b/nodedb-columnar/src/error.rs @@ -63,6 +63,13 @@ pub enum ColumnarError { source: nodedb_types::MsgpackError, }, + /// A value for a `Geometry` column that does not parse or encode. + #[error("geometry for column '{column}': {source}")] + Geometry { + column: String, + source: nodedb_spatial::SpatialError, + }, + #[error("null violation: column '{0}' is NOT NULL")] NullViolation(String), diff --git a/nodedb-columnar/src/geometry_cell.rs b/nodedb-columnar/src/geometry_cell.rs new file mode 100644 index 000000000..a7be58908 --- /dev/null +++ b/nodedb-columnar/src/geometry_cell.rs @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! The stored form of a `Geometry` column cell: little-endian EWKB, which +//! keeps the SRID, Z and M. +//! +//! A write accepts every shape a geometry takes on its way in: the native +//! geometry, its text (GeoJSON, EWKT, WKT, hex EWKB), WKB / EWKB bytes, or +//! a GeoJSON object. Stored SRIDs are checked for syntax only. Whether an +//! SRID is defined is a question for the planner that admitted the row. + +use nodedb_spatial::{SpatialError, SridCheck}; +use nodedb_types::value::Value; + +use crate::error::ColumnarError; + +/// The EWKB bytes of a value written to geometry column `column`. +pub(crate) fn encode_geometry_cell(value: &Value, column: &str) -> Result, ColumnarError> { + nodedb_spatial::ewkb_of_value(value, SridCheck::Syntax).map_err(|e| match e { + SpatialError::NotGeometryValue { .. } => ColumnarError::TypeMismatch { + column: column.to_string(), + expected: "Geometry".into(), + }, + source => ColumnarError::Geometry { + column: column.to_string(), + source, + }, + }) +} + +/// The reason a corruption error reports for a cell that is not EWKB. +pub(crate) fn not_ewkb(error: SpatialError) -> String { + format!("geometry cell is not EWKB: {error}") +} + +#[cfg(test)] +mod tests { + use super::*; + use nodedb_types::geometry::{Geometry, Ordinates, Srid, SridGeometry}; + + #[test] + fn every_input_form_stores_as_ewkb_and_decodes() { + let zm = SridGeometry::with_ordinates( + Srid::new(32648).unwrap(), + Geometry::point(1.0, 2.0), + Ordinates::new(Some(vec![3.0]), Some(vec![4.0])), + ) + .unwrap(); + let native = encode_geometry_cell(&Value::Geometry(zm.clone()), "g").unwrap(); + assert_eq!( + nodedb_spatial::value_of_ewkb(&native), + Ok(Value::Geometry(zm)) + ); + let text = encode_geometry_cell(&Value::String("POINT M (1 2 9)".into()), "g").unwrap(); + let Ok(Value::Geometry(g)) = nodedb_spatial::value_of_ewkb(&text) else { + panic!(); + }; + assert_eq!(g.ordinates().m(), Some(&[9.0][..])); + } + + #[test] + fn malformed_input_is_refused() { + assert!(matches!( + encode_geometry_cell(&Value::String("POINT(".into()), "g"), + Err(ColumnarError::Geometry { .. }) + )); + assert!(matches!( + encode_geometry_cell(&Value::Integer(1), "g"), + Err(ColumnarError::TypeMismatch { .. }) + )); + assert!(nodedb_spatial::value_of_ewkb(b"{\"type\":\"Point\"}").is_err()); + let mut object = std::collections::HashMap::new(); + object.insert("type".to_string(), Value::String("Point".into())); + assert!(matches!( + encode_geometry_cell(&Value::Object(object), "g"), + Err(ColumnarError::Geometry { .. }) + )); + } +} diff --git a/nodedb-columnar/src/lib.rs b/nodedb-columnar/src/lib.rs index e7c5171de..762dd33f8 100644 --- a/nodedb-columnar/src/lib.rs +++ b/nodedb-columnar/src/lib.rs @@ -22,6 +22,7 @@ pub mod delete_bitmap; pub mod error; pub mod filter; pub mod format; +pub(crate) mod geometry_cell; pub mod materialize_rows; pub mod memtable; pub mod mutation; diff --git a/nodedb-columnar/src/memtable/column_data/access.rs b/nodedb-columnar/src/memtable/column_data/access.rs index bf0b632a8..3aebe1672 100644 --- a/nodedb-columnar/src/memtable/column_data/access.rs +++ b/nodedb-columnar/src/memtable/column_data/access.rs @@ -123,10 +123,8 @@ impl ColumnData { Self::Geometry { data, offsets, .. } => { let start = offsets[row] as usize; let end = offsets[row + 1] as usize; - Value::String( - utf8_cell(&data[start..end]) - .map_err(|e| corrupt(format!("text cell is not UTF-8: {e}")))?, - ) + nodedb_spatial::value_of_ewkb(&data[start..end]) + .map_err(|e| corrupt(crate::geometry_cell::not_ewkb(e)))? } Self::Vector { data, dim, .. } => { let d = *dim as usize; diff --git a/nodedb-columnar/src/memtable/column_data/mutation.rs b/nodedb-columnar/src/memtable/column_data/mutation.rs deleted file mode 100644 index 8ba0dbae9..000000000 --- a/nodedb-columnar/src/memtable/column_data/mutation.rs +++ /dev/null @@ -1,327 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -//! Mutation methods on `ColumnData`: truncate, push, push_valid. -//! -//! `push_ref` and `backfill_nulls` live in `mutation_ingest.rs`. - -use nodedb_types::value::Value; - -use crate::error::ColumnarError; - -use super::types::ColumnData; - -#[path = "mutation_ingest.rs"] -mod mutation_ingest; - -impl ColumnData { - /// Truncate this column to `n` rows, discarding any rows beyond that point. - /// - /// Used by transaction rollback to restore the column to its pre-write state. - /// Panics in debug builds if `n > self.len()`. - pub(crate) fn truncate(&mut self, n: usize) { - debug_assert!( - n <= self.len(), - "truncate({n}) exceeds column length {}", - self.len() - ); - match self { - Self::Int64 { values, valid } => { - values.truncate(n); - if let Some(v) = valid { - v.truncate(n); - } - } - Self::Float64 { values, valid } => { - values.truncate(n); - if let Some(v) = valid { - v.truncate(n); - } - } - Self::Bool { values, valid } => { - values.truncate(n); - if let Some(v) = valid { - v.truncate(n); - } - } - Self::Timestamp { values, valid } => { - values.truncate(n); - if let Some(v) = valid { - v.truncate(n); - } - } - Self::Decimal { values, valid } => { - values.truncate(n); - if let Some(v) = valid { - v.truncate(n); - } - } - Self::Uuid { values, valid } => { - values.truncate(n); - if let Some(v) = valid { - v.truncate(n); - } - } - Self::String { - data, - offsets, - valid, - } => { - // offsets has length n_rows + 1; truncate to n+1. - if n < offsets.len().saturating_sub(1) { - let byte_end = offsets[n] as usize; - data.truncate(byte_end); - offsets.truncate(n + 1); - } - if let Some(v) = valid { - v.truncate(n); - } - } - Self::Bytes { - data, - offsets, - valid, - } => { - if n < offsets.len().saturating_sub(1) { - let byte_end = offsets[n] as usize; - data.truncate(byte_end); - offsets.truncate(n + 1); - } - if let Some(v) = valid { - v.truncate(n); - } - } - Self::Geometry { - data, - offsets, - valid, - } => { - if n < offsets.len().saturating_sub(1) { - let byte_end = offsets[n] as usize; - data.truncate(byte_end); - offsets.truncate(n + 1); - } - if let Some(v) = valid { - v.truncate(n); - } - } - Self::Vector { data, dim, valid } => { - let d = *dim as usize; - if d > 0 { - data.truncate(n * d); - } - if let Some(v) = valid { - v.truncate(n); - } - } - Self::DictEncoded { ids, valid, .. } => { - ids.truncate(n); - if let Some(v) = valid { - v.truncate(n); - } - } - } - } - - /// Push a validity bit (if the column is nullable). - #[inline(always)] - pub(crate) fn push_valid(valid: &mut Option>, is_valid: bool) { - if let Some(v) = valid { - v.push(is_valid); - } - } - - /// Append a value. Returns error if type doesn't match. - pub(crate) fn push(&mut self, value: &Value, col_name: &str) -> Result<(), ColumnarError> { - match (self, value) { - (Self::Int64 { values, valid }, Value::Null) => { - values.push(0); - Self::push_valid(valid, false); - } - (Self::Float64 { values, valid }, Value::Null) => { - values.push(0.0); - Self::push_valid(valid, false); - } - (Self::Bool { values, valid }, Value::Null) => { - values.push(false); - Self::push_valid(valid, false); - } - (Self::Timestamp { values, valid }, Value::Null) => { - values.push(0); - Self::push_valid(valid, false); - } - (Self::Decimal { values, valid }, Value::Null) => { - values.push([0u8; 16]); - Self::push_valid(valid, false); - } - (Self::Uuid { values, valid }, Value::Null) => { - values.push([0u8; 16]); - Self::push_valid(valid, false); - } - (Self::String { offsets, valid, .. }, Value::Null) => { - offsets.push(*offsets.last().unwrap_or(&0)); - Self::push_valid(valid, false); - } - (Self::Bytes { offsets, valid, .. }, Value::Null) => { - offsets.push(*offsets.last().unwrap_or(&0)); - Self::push_valid(valid, false); - } - (Self::Geometry { offsets, valid, .. }, Value::Null) => { - offsets.push(*offsets.last().unwrap_or(&0)); - Self::push_valid(valid, false); - } - (Self::Vector { data, dim, valid }, Value::Null) => { - data.extend(std::iter::repeat_n(0.0f32, *dim as usize)); - Self::push_valid(valid, false); - } - (Self::Int64 { values, valid }, Value::Integer(v)) => { - values.push(*v); - Self::push_valid(valid, true); - } - (Self::Float64 { values, valid }, Value::Float(v)) => { - values.push(*v); - Self::push_valid(valid, true); - } - (Self::Float64 { values, valid }, Value::Integer(v)) => { - values.push(*v as f64); - Self::push_valid(valid, true); - } - (Self::Bool { values, valid }, Value::Bool(v)) => { - values.push(*v); - Self::push_valid(valid, true); - } - (Self::Timestamp { values, valid }, Value::DateTime(dt)) - | (Self::Timestamp { values, valid }, Value::NaiveDateTime(dt)) => { - values.push(dt.micros); - Self::push_valid(valid, true); - } - (Self::Timestamp { values, valid }, Value::Integer(micros)) => { - values.push(*micros); - Self::push_valid(valid, true); - } - (Self::Decimal { values, valid }, Value::Decimal(d)) => { - values.push(d.serialize()); - Self::push_valid(valid, true); - } - (Self::Uuid { values, valid }, Value::Uuid(s)) => { - let bytes = uuid::Uuid::parse_str(s) - .map(|u| *u.as_bytes()) - .unwrap_or([0u8; 16]); - values.push(bytes); - Self::push_valid(valid, true); - } - ( - Self::String { - data, - offsets, - valid, - }, - Value::String(s), - ) => { - data.extend_from_slice(s.as_bytes()); - offsets.push(data.len() as u32); - Self::push_valid(valid, true); - } - ( - Self::Bytes { - data, - offsets, - valid, - }, - Value::Bytes(b), - ) => { - data.extend_from_slice(b); - offsets.push(data.len() as u32); - Self::push_valid(valid, true); - } - ( - Self::Geometry { - data, - offsets, - valid, - }, - Value::Geometry(g), - ) => { - if let Ok(json) = sonic_rs::to_vec(g) { - data.extend_from_slice(&json); - } - offsets.push(data.len() as u32); - Self::push_valid(valid, true); - } - ( - Self::Geometry { - data, - offsets, - valid, - }, - Value::String(s), - ) => { - data.extend_from_slice(s.as_bytes()); - offsets.push(data.len() as u32); - Self::push_valid(valid, true); - } - (Self::Vector { data, dim, valid }, Value::Array(arr)) => { - let d = *dim as usize; - for (i, v) in arr.iter().take(d).enumerate() { - let f = match v { - Value::Float(f) => *f as f32, - Value::Integer(n) => *n as f32, - _ => 0.0, - }; - if i < d { - data.push(f); - } - } - for _ in arr.len()..d { - data.push(0.0); - } - Self::push_valid(valid, true); - } - (Self::DictEncoded { ids, valid, .. }, Value::Null) => { - ids.push(0); - Self::push_valid(valid, false); - } - ( - Self::DictEncoded { - ids, - dictionary, - reverse, - valid, - }, - Value::String(s), - ) => { - let id = if let Some(&existing) = reverse.get(s.as_str()) { - existing - } else { - let new_id = dictionary.len() as u32; - dictionary.push(s.clone()); - reverse.insert(s.clone(), new_id); - new_id - }; - ids.push(id); - Self::push_valid(valid, true); - } - (other, val) => { - let type_name = match other { - Self::Int64 { .. } => "Int64", - Self::Float64 { .. } => "Float64", - Self::Bool { .. } => "Bool", - Self::Timestamp { .. } => "Timestamp", - Self::Decimal { .. } => "Decimal", - Self::Uuid { .. } => "Uuid", - Self::String { .. } => "String", - Self::Bytes { .. } => "Bytes", - Self::Geometry { .. } => "Geometry", - Self::Vector { .. } => "Vector", - Self::DictEncoded { .. } => "DictEncoded", - }; - let _ = val; - return Err(ColumnarError::TypeMismatch { - column: col_name.to_string(), - expected: type_name.to_string(), - }); - } - } - Ok(()) - } - -} diff --git a/nodedb-columnar/src/memtable/column_data/mutation_ingest.rs b/nodedb-columnar/src/memtable/column_data/mutation_ingest.rs deleted file mode 100644 index cf17ed88a..000000000 --- a/nodedb-columnar/src/memtable/column_data/mutation_ingest.rs +++ /dev/null @@ -1,195 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -//! `push_ref` and `backfill_nulls` methods on `ColumnData`. - -use crate::error::ColumnarError; -use crate::memtable::IngestValue; - -use super::types::ColumnData; - -impl ColumnData { - /// Append a borrowed value (zero-copy for strings). Used by `ingest_row_refs`. - pub(crate) fn push_ref( - &mut self, - value: &IngestValue<'_>, - col_name: &str, - ) -> Result<(), ColumnarError> { - match (self, value) { - (Self::Int64 { values, valid }, IngestValue::Null) => { - values.push(0); - Self::push_valid(valid, false); - } - (Self::Float64 { values, valid }, IngestValue::Null) => { - values.push(0.0); - Self::push_valid(valid, false); - } - (Self::Bool { values, valid }, IngestValue::Null) => { - values.push(false); - Self::push_valid(valid, false); - } - (Self::Timestamp { values, valid }, IngestValue::Null) => { - values.push(0); - Self::push_valid(valid, false); - } - (Self::String { offsets, valid, .. }, IngestValue::Null) => { - offsets.push(*offsets.last().unwrap_or(&0)); - Self::push_valid(valid, false); - } - (Self::DictEncoded { ids, valid, .. }, IngestValue::Null) => { - ids.push(0); - Self::push_valid(valid, false); - } - (Self::Int64 { values, valid }, IngestValue::Int64(v)) => { - values.push(*v); - Self::push_valid(valid, true); - } - (Self::Float64 { values, valid }, IngestValue::Float64(v)) => { - values.push(*v); - Self::push_valid(valid, true); - } - (Self::Float64 { values, valid }, IngestValue::Int64(v)) => { - values.push(*v as f64); - Self::push_valid(valid, true); - } - (Self::Bool { values, valid }, IngestValue::Bool(v)) => { - values.push(*v); - Self::push_valid(valid, true); - } - (Self::Timestamp { values, valid }, IngestValue::Timestamp(v)) => { - values.push(*v); - Self::push_valid(valid, true); - } - (Self::Timestamp { values, valid }, IngestValue::Int64(v)) => { - values.push(*v); - Self::push_valid(valid, true); - } - ( - Self::String { - data, - offsets, - valid, - }, - IngestValue::Str(s), - ) => { - data.extend_from_slice(s.as_bytes()); - offsets.push(data.len() as u32); - Self::push_valid(valid, true); - } - ( - Self::DictEncoded { - ids, - dictionary, - reverse, - valid, - }, - IngestValue::Str(s), - ) => { - let id = if let Some(&existing) = reverse.get(*s) { - existing - } else { - let new_id = dictionary.len() as u32; - dictionary.push((*s).to_string()); - reverse.insert((*s).to_string(), new_id); - new_id - }; - ids.push(id); - Self::push_valid(valid, true); - } - (other, _) => { - let type_name = match other { - Self::Int64 { .. } => "Int64", - Self::Float64 { .. } => "Float64", - Self::Bool { .. } => "Bool", - Self::Timestamp { .. } => "Timestamp", - Self::Decimal { .. } => "Decimal", - Self::Uuid { .. } => "Uuid", - Self::String { .. } => "String", - Self::Bytes { .. } => "Bytes", - Self::Geometry { .. } => "Geometry", - Self::Vector { .. } => "Vector", - Self::DictEncoded { .. } => "DictEncoded", - }; - return Err(ColumnarError::TypeMismatch { - column: col_name.to_string(), - expected: type_name.to_string(), - }); - } - } - Ok(()) - } - - /// Backfill a column with null/default values for existing rows. - pub(crate) fn backfill_nulls(&mut self, count: usize) { - match self { - Self::Int64 { values, valid } => { - values.extend(std::iter::repeat_n(0i64, count)); - if let Some(v) = valid { - v.extend(std::iter::repeat_n(false, count)); - } - } - Self::Float64 { values, valid } => { - values.extend(std::iter::repeat_n(f64::NAN, count)); - if let Some(v) = valid { - v.extend(std::iter::repeat_n(false, count)); - } - } - Self::Bool { values, valid } => { - values.extend(std::iter::repeat_n(false, count)); - if let Some(v) = valid { - v.extend(std::iter::repeat_n(false, count)); - } - } - Self::Timestamp { values, valid } => { - values.extend(std::iter::repeat_n(0i64, count)); - if let Some(v) = valid { - v.extend(std::iter::repeat_n(false, count)); - } - } - Self::Decimal { values, valid } => { - values.extend(std::iter::repeat_n([0u8; 16], count)); - if let Some(v) = valid { - v.extend(std::iter::repeat_n(false, count)); - } - } - Self::Uuid { values, valid } => { - values.extend(std::iter::repeat_n([0u8; 16], count)); - if let Some(v) = valid { - v.extend(std::iter::repeat_n(false, count)); - } - } - Self::String { offsets, valid, .. } => { - let last = *offsets.last().unwrap_or(&0); - offsets.extend(std::iter::repeat_n(last, count)); - if let Some(v) = valid { - v.extend(std::iter::repeat_n(false, count)); - } - } - Self::Bytes { offsets, valid, .. } => { - let last = *offsets.last().unwrap_or(&0); - offsets.extend(std::iter::repeat_n(last, count)); - if let Some(v) = valid { - v.extend(std::iter::repeat_n(false, count)); - } - } - Self::Geometry { offsets, valid, .. } => { - let last = *offsets.last().unwrap_or(&0); - offsets.extend(std::iter::repeat_n(last, count)); - if let Some(v) = valid { - v.extend(std::iter::repeat_n(false, count)); - } - } - Self::Vector { data, dim, valid } => { - data.extend(std::iter::repeat_n(0.0f32, *dim as usize * count)); - if let Some(v) = valid { - v.extend(std::iter::repeat_n(false, count)); - } - } - Self::DictEncoded { ids, valid, .. } => { - ids.extend(std::iter::repeat_n(0u32, count)); - if let Some(v) = valid { - v.extend(std::iter::repeat_n(false, count)); - } - } - } - } -} diff --git a/nodedb-columnar/src/memtable/column_data/push.rs b/nodedb-columnar/src/memtable/column_data/push.rs index 0d962a474..72cb82f77 100644 --- a/nodedb-columnar/src/memtable/column_data/push.rs +++ b/nodedb-columnar/src/memtable/column_data/push.rs @@ -291,29 +291,18 @@ impl ColumnData { offsets.push(data.len() as u32); Self::push_valid(valid, true); } + // EWKB, so the cell keeps its SRID, Z and M. A value that does + // not parse or encode refuses the row. ( Self::Geometry { data, offsets, valid, }, - Value::Geometry(g), + value, ) => { - if let Ok(json) = sonic_rs::to_vec(g) { - data.extend_from_slice(&json); - } - offsets.push(data.len() as u32); - Self::push_valid(valid, true); - } - ( - Self::Geometry { - data, - offsets, - valid, - }, - Value::String(s), - ) => { - data.extend_from_slice(s.as_bytes()); + let ewkb = crate::geometry_cell::encode_geometry_cell(value, col_name)?; + data.extend_from_slice(&ewkb); offsets.push(data.len() as u32); Self::push_valid(valid, true); } diff --git a/nodedb-columnar/src/mutation/engine.rs b/nodedb-columnar/src/mutation/engine.rs index 3882fb40e..0e07560f3 100644 --- a/nodedb-columnar/src/mutation/engine.rs +++ b/nodedb-columnar/src/mutation/engine.rs @@ -11,7 +11,7 @@ use nodedb_types::value::Value; use crate::delete_bitmap::DeleteBitmap; use crate::error::ColumnarError; use crate::memtable::ColumnarMemtable; -use crate::pk_index::{PkIndex, encode_pk}; +use crate::pk_index::{PkIndex, RowLocation, encode_pk}; use crate::wal_record::ColumnarWalRecord; /// Coordinates all columnar mutations for a single collection. @@ -23,6 +23,10 @@ pub struct MutationEngine { pub(super) schema: ColumnarSchema, pub(super) memtable: ColumnarMemtable, pub(super) pk_index: PkIndex, + /// Primary key to the tombstone version the latest bitemporal close of + /// that key wrote. A closed key is unbound in `pk_index`. A later insert + /// of the key leaves the entry: the row it binds is the newer version. + pub(super) closed_index: PkIndex, /// Per-segment delete bitmaps. Key = segment_id. pub(super) delete_bitmaps: HashMap, /// PK column indices in the schema. @@ -82,6 +86,7 @@ impl MutationEngine { schema, memtable, pk_index: PkIndex::new(), + closed_index: PkIndex::new(), delete_bitmaps: HashMap::new(), pk_col_indices, next_segment_id: 1, @@ -113,6 +118,12 @@ impl MutationEngine { &mut self.pk_index } + /// Where the tombstone the latest bitemporal close of `pk_bytes` wrote + /// lives. `None` for a key never closed. + pub fn closed_location(&self, pk_bytes: &[u8]) -> Option { + self.closed_index.get(pk_bytes).copied() + } + /// Access a segment's delete bitmap. pub fn delete_bitmap(&self, segment_id: u64) -> Option<&DeleteBitmap> { self.delete_bitmaps.get(&segment_id) @@ -274,11 +285,20 @@ impl MutationEngine { /// /// A cut row can carry a tombstone: a later row of the same write /// replaced it. Those tombstones are cleared too, so a row appended - /// later at that index starts live. + /// later at that index starts live. A close whose tombstone row is cut + /// loses its `closed_index` entry. pub(super) fn cut_memtable_to(&mut self, row_count: usize) { if let Some(bm) = self.delete_bitmaps.get_mut(&self.memtable_segment_id) { bm.unmark_from(row_count as u32); } + let memtable_segment_id = self.memtable_segment_id; + self.closed_index + .remap_segment(memtable_segment_id, |row_index| { + ((row_index as usize) < row_count).then_some(RowLocation { + segment_id: memtable_segment_id, + row_index, + }) + }); self.memtable.truncate_to(row_count); self.memtable_surrogates.truncate(row_count); self.memtable_row_counter = row_count as u32; diff --git a/nodedb-columnar/src/mutation/flush.rs b/nodedb-columnar/src/mutation/flush.rs index 3e44c81a3..c799f3ae0 100644 --- a/nodedb-columnar/src/mutation/flush.rs +++ b/nodedb-columnar/src/mutation/flush.rs @@ -11,8 +11,8 @@ use super::engine::{MutationEngine, MutationResult}; impl MutationEngine { /// Notify the engine that the memtable was flushed to a new segment. /// - /// Remaps the PK index entries of the memtable's virtual segment to - /// `new_segment_id` and moves the memtable's delete bitmap with them, so + /// Remaps the PK index and closed-key entries of the memtable's virtual + /// segment to `new_segment_id` and moves the memtable's delete bitmap with them, so /// a row tombstoned while in the memtable stays tombstoned in the /// segment. The virtual segment id itself never changes: it names the /// memtable, not a flushed segment, so no flushed segment can ever share @@ -34,6 +34,13 @@ impl MutationEngine { row_index: old_row, }) }); + self.closed_index + .remap_segment(self.memtable_segment_id, |old_row| { + Some(RowLocation { + segment_id: new_segment_id, + row_index: old_row, + }) + }); if let Some(bitmap) = self.delete_bitmaps.remove(&self.memtable_segment_id) { self.delete_bitmaps.insert(new_segment_id, bitmap); } diff --git a/nodedb-columnar/src/mutation/snapshot.rs b/nodedb-columnar/src/mutation/snapshot.rs index c95d6f744..d7af2deff 100644 --- a/nodedb-columnar/src/mutation/snapshot.rs +++ b/nodedb-columnar/src/mutation/snapshot.rs @@ -119,6 +119,12 @@ pub struct ColumnarEngineSnapshot { pub memtable_surrogates: Vec>, /// Serialised `PkIndex` bytes (via `PkIndex::to_bytes()`). pub pk_index_bytes: Vec, + /// Serialised closed-key index bytes (via `PkIndex::to_bytes()`): each + /// key a bitemporal close unbound, to its tombstone row. Empty when no + /// key was ever closed. + #[msgpack(default)] + #[serde(default)] + pub closed_index_bytes: Vec, /// Serialised delete bitmaps for FLUSHED segments only. /// /// Excludes the entry keyed by `memtable_segment_id`; that is @@ -172,6 +178,11 @@ impl MutationEngine { // Serialise the PK index. let pk_index_bytes = self.pk_index.to_bytes()?; + let closed_index_bytes = if self.closed_index.is_empty() { + Vec::new() + } else { + self.closed_index.to_bytes()? + }; // Partition delete bitmaps: separate the memtable's virtual segment // from flushed-segment bitmaps. @@ -193,6 +204,7 @@ impl MutationEngine { memtable_columns, memtable_surrogates: self.memtable_surrogates.clone(), pk_index_bytes, + closed_index_bytes, delete_bitmaps, memtable_delete_bitmap_bytes, flushed_segments: flushed_segments.to_vec(), @@ -233,6 +245,11 @@ impl MutationEngine { // Rebuild PK index. let pk_index = PkIndex::from_bytes(&snap.pk_index_bytes)?; + let closed_index = if snap.closed_index_bytes.is_empty() { + PkIndex::new() + } else { + PkIndex::from_bytes(&snap.closed_index_bytes)? + }; // Rebuild flushed-segment delete bitmaps. let mut delete_bitmaps: HashMap = HashMap::new(); @@ -276,6 +293,7 @@ impl MutationEngine { schema: snap.schema, memtable, pk_index, + closed_index, delete_bitmaps, pk_col_indices, next_segment_id: snap.next_segment_id, diff --git a/nodedb-columnar/src/mutation/truncate.rs b/nodedb-columnar/src/mutation/truncate.rs index 6849416d8..6f418f8ad 100644 --- a/nodedb-columnar/src/mutation/truncate.rs +++ b/nodedb-columnar/src/mutation/truncate.rs @@ -20,6 +20,7 @@ use super::engine::MutationEngine; pub struct TruncatedRows { memtable: ColumnarMemtable, pk_index: PkIndex, + closed_index: PkIndex, delete_bitmaps: HashMap, memtable_row_counter: u32, memtable_surrogates: Vec>, @@ -46,6 +47,7 @@ impl MutationEngine { TruncatedRows { memtable, pk_index: std::mem::take(&mut self.pk_index), + closed_index: std::mem::take(&mut self.closed_index), delete_bitmaps: std::mem::take(&mut self.delete_bitmaps), memtable_row_counter: std::mem::replace(&mut self.memtable_row_counter, 0), memtable_surrogates: std::mem::take(&mut self.memtable_surrogates), @@ -58,12 +60,14 @@ impl MutationEngine { let TruncatedRows { memtable, pk_index, + closed_index, delete_bitmaps, memtable_row_counter, memtable_surrogates, } = rows; self.memtable = memtable; self.pk_index = pk_index; + self.closed_index = closed_index; self.delete_bitmaps = delete_bitmaps; self.memtable_row_counter = memtable_row_counter; self.memtable_surrogates = memtable_surrogates; diff --git a/nodedb-columnar/src/mutation/write.rs b/nodedb-columnar/src/mutation/write.rs index 915a257ef..1a1905daf 100644 --- a/nodedb-columnar/src/mutation/write.rs +++ b/nodedb-columnar/src/mutation/write.rs @@ -1,6 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 -//! Write-path mutations: insert, insert_if_absent, delete, update. +//! Write-path mutations: insert, insert_if_absent, delete, close, update. use nodedb_types::surrogate::Surrogate; use nodedb_types::value::Value; @@ -174,24 +174,73 @@ impl MutationEngine { .get(&pk_bytes) .copied() .ok_or(ColumnarError::PrimaryKeyNotFound)?; + Ok(self.delete_bound(pk_bytes, location)) + } - // Generate WAL record BEFORE applying. + /// Mark the row `pk_bytes` binds at `location` deleted and unbind it. + fn delete_bound(&mut self, pk_bytes: Vec, location: RowLocation) -> MutationResult { let wal = ColumnarWalRecord::DeleteRows { collection: self.collection.clone(), segment_id: location.segment_id, row_indices: vec![location.row_index], }; - - // Mark in delete bitmap. - let bitmap = self.delete_bitmaps.entry(location.segment_id).or_default(); - bitmap.mark_deleted(location.row_index); - - // Remove from PK index. + self.delete_bitmaps + .entry(location.segment_id) + .or_default() + .mark_deleted(location.row_index); self.pk_index.remove(&pk_bytes); - - Ok(MutationResult { + MutationResult { wal_records: vec![wal], - }) + } + } + + /// Close a key of a bitemporal collection: append `tombstone`, the + /// version that ends the key, and unbind the key. The version the close + /// supersedes stays as history for `AS OF` reads. The tombstone is bound + /// in the closed-key index, so a later write of the key can read its + /// system time. + /// + /// The tombstone keeps the cross-engine surrogate of the row it closes, + /// read as [`Self::update`] reads it. A collection that is not + /// bitemporal keeps no history: the row is deleted as [`Self::delete`] + /// deletes it, and `tombstone` is not written. + /// + /// `PrimaryKeyNotFound` when the tombstone's key is unbound. An invalid + /// tombstone returns an error and leaves the key bound. + pub fn close( + &mut self, + tombstone: &[Value], + flushed_surrogate: Option, + ) -> Result { + let pk_bytes = self.extract_pk_bytes(tombstone)?; + let location = self + .pk_index + .get(&pk_bytes) + .copied() + .ok_or(ColumnarError::PrimaryKeyNotFound)?; + if !self.schema.is_bitemporal() { + return Ok(self.delete_bound(pk_bytes, location)); + } + let surrogate = if location.segment_id == self.memtable_segment_id { + self.memtable_surrogates + .get(location.row_index as usize) + .copied() + .flatten() + } else { + flushed_surrogate + }; + let mut wal_records = Vec::with_capacity(1); + self.commit_row( + tombstone, + pk_bytes.clone(), + surrogate, + &[], + &mut wal_records, + )?; + if let Some(tombstone_location) = self.pk_index.remove(&pk_bytes) { + self.closed_index.upsert(pk_bytes, tombstone_location); + } + Ok(MutationResult { wal_records }) } /// Update a row by PK: DELETE old + INSERT new. @@ -201,7 +250,10 @@ impl MutationEngine { /// /// Returns WAL records for both the delete and the insert. The old row /// is tombstoned only after the new row is appended, so an invalid new - /// row returns an error and leaves the old row live. + /// row returns an error and leaves the old row live. A bitemporal + /// collection tombstones nothing: the old row stays as the version the + /// update superseded, visible to `AS OF` reads, and the PK index binds + /// the new row. /// /// NOTE: The caller must provide the full old row values for the re-insert. /// This method takes the complete new row (already merged with old values). @@ -216,6 +268,36 @@ impl MutationEngine { old_pk: &Value, new_values: &[Value], flushed_surrogate: Option, + ) -> Result { + self.update_row(old_pk, new_values, None, flushed_surrogate) + } + + /// [`Self::update`] for a key-changing update of a bitemporal + /// collection: the old key closes with `tombstone` as [`Self::close`] + /// closes a key, so a current read finds no row under it. The tombstone + /// is appended before the new row. When the new row is refused, the + /// tombstone is cut and the old key is bound again, so an error leaves + /// the engine unchanged. + /// + /// An update that keeps its key, or a collection that is not + /// bitemporal, writes no tombstone and runs as [`Self::update`]. + pub fn update_closing( + &mut self, + old_pk: &Value, + new_values: &[Value], + tombstone: &[Value], + flushed_surrogate: Option, + ) -> Result { + self.update_row(old_pk, new_values, Some(tombstone), flushed_surrogate) + } + + /// Shared body of [`Self::update`] and [`Self::update_closing`]. + fn update_row( + &mut self, + old_pk: &Value, + new_values: &[Value], + tombstone: Option<&[Value]>, + flushed_surrogate: Option, ) -> Result { let old_pk_bytes = encode_pk(old_pk); let old_location = self @@ -235,11 +317,14 @@ impl MutationEngine { let new_pk_bytes = self.extract_pk_bytes(new_values)?; let pk_changed = new_pk_bytes != old_pk_bytes; - // The old row is always tombstoned. When the PK changes, a + // A non-bitemporal old row is tombstoned. When the PK changes, a // non-bitemporal row already bound to the new PK is replaced too, - // as an upsert replaces it. + // as an upsert replaces it. A bitemporal collection keeps both as + // history. let mut replaced = Vec::with_capacity(2); - replaced.push(old_location); + if !self.schema.is_bitemporal() { + replaced.push(old_location); + } if pk_changed && !self.schema.is_bitemporal() && let Some(prior) = self.pk_index.get(&new_pk_bytes).copied() @@ -247,7 +332,36 @@ impl MutationEngine { replaced.push(prior); } - let mut wal_records = Vec::with_capacity(replaced.len() + 1); + let mut wal_records = Vec::with_capacity(replaced.len() + 2); + let closing = tombstone.filter(|_| pk_changed && self.schema.is_bitemporal()); + if let Some(tombstone) = closing { + let row_count_before = self.memtable.row_count(); + self.commit_row( + tombstone, + old_pk_bytes.clone(), + surrogate, + &[], + &mut wal_records, + )?; + if let Err(error) = self.commit_row( + new_values, + new_pk_bytes, + surrogate, + &replaced, + &mut wal_records, + ) { + self.rollback_memtable_inserts( + row_count_before, + &[], + &[(old_pk_bytes, old_location)], + ); + return Err(error); + } + if let Some(tombstone_location) = self.pk_index.remove(&old_pk_bytes) { + self.closed_index.upsert(old_pk_bytes, tombstone_location); + } + return Ok(MutationResult { wal_records }); + } self.commit_row( new_values, new_pk_bytes, @@ -388,4 +502,136 @@ mod tests { assert!(!engine.pk_index().contains(&encode_pk(&Value::Integer(1)))); assert!(engine.pk_index().contains(&encode_pk(&Value::Integer(2)))); } + + /// A bitemporal update keeps the version it supersedes: no tombstone, no + /// delete record, and the PK index binds the new version. + #[test] + fn a_bitemporal_update_keeps_the_superseded_version() { + let schema = ColumnarSchema::new(vec![ + ColumnDef::required("_ts_system", ColumnType::Int64), + ColumnDef::required("id", ColumnType::Int64).with_primary_key(), + ColumnDef::required("v", ColumnType::Int64), + ]) + .expect("valid"); + let mut engine = MutationEngine::new("t".into(), schema); + let version = + |ts: i64, v: i64| vec![Value::Integer(ts), Value::Integer(1), Value::Integer(v)]; + engine.insert(&version(100, 10)).expect("insert"); + + let result = engine + .update(&Value::Integer(1), &version(200, 20), None) + .expect("update"); + assert_eq!( + result.wal_records.len(), + 1, + "an insert record, no delete record" + ); + + assert_eq!(live_rows(&engine), vec![version(100, 10), version(200, 20)]); + let bound = engine + .pk_index() + .get(&encode_pk(&Value::Integer(1))) + .copied() + .expect("bound"); + assert_eq!(bound.row_index, 1, "the PK binds the new version"); + } + + /// A bitemporal close appends the tombstone, keeps the closed version, + /// unbinds the key and binds the tombstone in the closed-key index. A + /// flush carries the closed-key entry to the new segment. + #[test] + fn a_bitemporal_close_keeps_the_closed_version() { + let schema = ColumnarSchema::new(vec![ + ColumnDef::required("_ts_system", ColumnType::Int64), + ColumnDef::required("id", ColumnType::Int64).with_primary_key(), + ColumnDef::required("v", ColumnType::Int64), + ]) + .expect("valid"); + let mut engine = MutationEngine::new("t".into(), schema); + let version = + |ts: i64, v: i64| vec![Value::Integer(ts), Value::Integer(1), Value::Integer(v)]; + engine.insert(&version(100, 10)).expect("insert"); + + let result = engine.close(&version(200, 10), None).expect("close"); + assert_eq!( + result.wal_records.len(), + 1, + "an insert record, no delete record" + ); + + assert_eq!(live_rows(&engine), vec![version(100, 10), version(200, 10)]); + let pk = encode_pk(&Value::Integer(1)); + assert!( + !engine.pk_index().contains(&pk), + "the closed key is unbound" + ); + assert_eq!(engine.closed_location(&pk).map(|l| l.row_index), Some(1)); + assert!(matches!( + engine.close(&version(300, 10), None), + Err(ColumnarError::PrimaryKeyNotFound) + )); + + let segment_id = engine.next_segment_id(); + engine.on_memtable_flushed(segment_id).expect("flush"); + assert_eq!( + engine.closed_location(&pk), + Some(RowLocation { + segment_id, + row_index: 1 + }) + ); + } + + /// A key-changing bitemporal update closes the old key with its + /// tombstone, keeps the old version, and binds the new key. A refused + /// new row leaves the engine unchanged. + #[test] + fn a_key_changing_bitemporal_update_closes_the_old_key() { + let schema = ColumnarSchema::new(vec![ + ColumnDef::required("_ts_system", ColumnType::Int64), + ColumnDef::required("id", ColumnType::Int64).with_primary_key(), + ColumnDef::required("v", ColumnType::Int64), + ]) + .expect("valid"); + let mut engine = MutationEngine::new("t".into(), schema); + let row = |ts: i64, id: i64, v: i64| { + vec![Value::Integer(ts), Value::Integer(id), Value::Integer(v)] + }; + engine.insert(&row(100, 1, 10)).expect("insert"); + + let refused = engine.update_closing( + &Value::Integer(1), + &[Value::Integer(200), Value::Integer(2), Value::Null], + &row(200, 1, 10), + None, + ); + assert!(refused.is_err()); + assert_eq!(live_rows(&engine), vec![row(100, 1, 10)]); + let old = encode_pk(&Value::Integer(1)); + assert_eq!(engine.pk_index().get(&old).map(|l| l.row_index), Some(0)); + assert_eq!(engine.closed_location(&old), None); + + engine + .update_closing(&Value::Integer(1), &row(200, 2, 10), &row(200, 1, 10), None) + .expect("update"); + assert_eq!( + live_rows(&engine), + vec![row(100, 1, 10), row(200, 1, 10), row(200, 2, 10)] + ); + assert!(!engine.pk_index().contains(&old)); + assert_eq!(engine.closed_location(&old).map(|l| l.row_index), Some(1)); + let new = encode_pk(&Value::Integer(2)); + assert_eq!(engine.pk_index().get(&new).map(|l| l.row_index), Some(2)); + } + + /// A close on a collection that is not bitemporal deletes the row. + #[test] + fn a_plain_close_deletes_the_row() { + let mut engine = MutationEngine::new("t".into(), schema()); + engine.insert(&row(1, "a", 0.5)).expect("insert"); + engine.close(&row(1, "a", 0.5), None).expect("close"); + assert!(live_rows(&engine).is_empty()); + assert!(engine.pk_index().is_empty()); + assert_eq!(engine.closed_location(&encode_pk(&Value::Integer(1))), None); + } } diff --git a/nodedb-columnar/src/reader/cell.rs b/nodedb-columnar/src/reader/cell.rs index fab8d5f6e..0b785aba1 100644 --- a/nodedb-columnar/src/reader/cell.rs +++ b/nodedb-columnar/src/reader/cell.rs @@ -99,10 +99,11 @@ fn cell_bytes<'a>(data: &'a [u8], offsets: &[u32], row: usize) -> Result<&'a [u8 /// A binary cell as the `Value` the memtable reads for `declared`. fn binary_cell_value(bytes: &[u8], declared: &ColumnType) -> Result { Ok(match declared { - ColumnType::String - | ColumnType::Regex - | ColumnType::SparseVector - | ColumnType::Geometry => Value::String(utf8(bytes, declared)?.to_owned()), + ColumnType::String | ColumnType::Regex | ColumnType::SparseVector => { + Value::String(utf8(bytes, declared)?.to_owned()) + } + ColumnType::Geometry => nodedb_spatial::value_of_ewkb(bytes) + .map_err(|e| corruption(crate::geometry_cell::not_ewkb(e)))?, ColumnType::Bytes | ColumnType::Array | ColumnType::Set @@ -308,7 +309,12 @@ mod tests { let first = &expected[1]; assert_eq!(first[10], Value::Uuid(uuid::Uuid::from_u128(2).to_string())); assert_eq!(first[11], Value::Ulid(ulid_text(1))); - assert_eq!(first[12], Value::String("POINT(1 1)".into())); + assert_eq!( + first[12], + Value::Geometry(nodedb_types::geometry::SridGeometry::wgs84( + nodedb_types::geometry::Geometry::point(1.0, 1.0) + )) + ); assert_eq!(first[15], Value::Integer(3)); assert_eq!(first[16], Value::String("^r1$".into())); assert_eq!(expected[4][10], Value::Null); @@ -328,6 +334,7 @@ mod tests { ColumnType::Ulid, ColumnType::Decimal(None), ColumnType::Vector(2), + ColumnType::Geometry, ] { assert!( decoded_cell_value(&col, 0, &ty).is_err(), diff --git a/nodedb-columnar/src/writer/segment_writer.rs b/nodedb-columnar/src/writer/segment_writer.rs index beab51176..dd3d13c36 100644 --- a/nodedb-columnar/src/writer/segment_writer.rs +++ b/nodedb-columnar/src/writer/segment_writer.rs @@ -171,12 +171,11 @@ fn select_codec(col_type: &ColumnType) -> ResolvedColumnCodec { ColumnType::Timestamp | ColumnType::Timestamptz | ColumnType::SystemTimestamp => { ColumnTypeHint::Timestamp } - ColumnType::String - | ColumnType::Geometry - | ColumnType::Regex - | ColumnType::SparseVector => ColumnTypeHint::String, + ColumnType::String | ColumnType::Regex | ColumnType::SparseVector => ColumnTypeHint::String, + // Geometry cells are EWKB bytes. ColumnType::Bool | ColumnType::Bytes + | ColumnType::Geometry | ColumnType::Decimal { .. } | ColumnType::Uuid | ColumnType::Ulid => { diff --git a/nodedb-crdt/Cargo.toml b/nodedb-crdt/Cargo.toml index 5cba16cb5..b1279cbca 100644 --- a/nodedb-crdt/Cargo.toml +++ b/nodedb-crdt/Cargo.toml @@ -21,6 +21,7 @@ serde = { workspace = true } zerompk = { workspace = true } nodedb-types = { workspace = true } nodedb-query = { workspace = true } +nodedb-crs = { workspace = true } sha2 = { workspace = true } hmac = { workspace = true } hkdf = { workspace = true } diff --git a/nodedb-crdt/src/constraint_checks.rs b/nodedb-crdt/src/constraint_checks.rs index 100cad89e..578b7b69c 100644 --- a/nodedb-crdt/src/constraint_checks.rs +++ b/nodedb-crdt/src/constraint_checks.rs @@ -39,6 +39,7 @@ impl Validator { state: &impl RowLookup, change: &ProposedChange, constraint: &Constraint, + crs: &nodedb_crs::CrsContext<'_>, ) -> Result, nodedb_query::EvalError> { match &constraint.kind { ConstraintKind::Unique => Ok(self.check_unique(state, change, constraint)), @@ -51,7 +52,7 @@ impl Validator { ref_key, } => Ok(self.check_foreign_key(state, change, constraint, ref_collection, ref_key)), ConstraintKind::NotNull => Ok(self.check_not_null(change, constraint)), - ConstraintKind::Check { expr, .. } => self.check_expr(change, constraint, expr), + ConstraintKind::Check { expr, .. } => self.check_expr(change, constraint, expr, crs), } } @@ -66,6 +67,7 @@ impl Validator { change: &ProposedChange, constraint: &Constraint, expr: &str, + crs: &nodedb_crs::CrsContext<'_>, ) -> Result, nodedb_query::EvalError> { // Build a row Value::Object from the proposed change's fields so the // expression evaluator can resolve column references by name. @@ -100,7 +102,7 @@ impl Validator { // `ValidationOutcome::EvalError`. That keeps it out of the conflict- // policy machinery: an unevaluable predicate is a hard SQLSTATE-22012 // failure, never something LastWriterWins et al. may "resolve". - match parsed.eval(&row_value) { + match parsed.eval(&row_value, crs) { Ok(nodedb_types::Value::Bool(true)) => Ok(None), Ok(nodedb_types::Value::Null) => Ok(None), Ok(_) => Ok(Some(Violation { diff --git a/nodedb-crdt/src/pre_validate.rs b/nodedb-crdt/src/pre_validate.rs index 07272e71c..1e2d6fb80 100644 --- a/nodedb-crdt/src/pre_validate.rs +++ b/nodedb-crdt/src/pre_validate.rs @@ -35,8 +35,9 @@ pub fn pre_validate( validator: &Validator, state: &impl RowLookup, change: &ProposedChange, + crs: &nodedb_crs::CrsContext<'_>, ) -> PreValidationResult { - match validator.validate(state, change) { + match validator.validate(state, change, crs) { ValidationOutcome::Accepted => PreValidationResult::Proceed, ValidationOutcome::Rejected(violations) => { let v = &violations[0]; @@ -79,7 +80,12 @@ mod tests { fields: vec![("email".into(), LoroValue::String("a@b.com".into()))], }; - match pre_validate(&validator, &state, &change) { + match pre_validate( + &validator, + &state, + &change, + &nodedb_crs::CrsContext::builtin_only(), + ) { PreValidationResult::FastReject { constraint, .. } => { assert_eq!(constraint, "name_nn"); } @@ -102,7 +108,12 @@ mod tests { }; assert!(matches!( - pre_validate(&validator, &state, &change), + pre_validate( + &validator, + &state, + &change, + &nodedb_crs::CrsContext::builtin_only() + ), PreValidationResult::Proceed )); } diff --git a/nodedb-crdt/src/state/core.rs b/nodedb-crdt/src/state/core.rs index 93ef1a2ba..13f99e45d 100644 --- a/nodedb-crdt/src/state/core.rs +++ b/nodedb-crdt/src/state/core.rs @@ -450,6 +450,16 @@ impl CrdtState { pub fn peer_id(&self) -> u64 { self.peer_id } + + /// Mint every later operation under `peer_id`. Operations already in the + /// document keep the ids they were minted with. + pub fn set_peer_id(&mut self, peer_id: u64) -> Result<()> { + self.doc + .set_peer_id(peer_id) + .map_err(|e| CrdtError::Loro(format!("failed to set peer_id {peer_id}: {e}")))?; + self.peer_id = peer_id; + Ok(()) + } } impl RowLookup for CrdtState { diff --git a/nodedb-crdt/src/validator/incoming.rs b/nodedb-crdt/src/validator/incoming.rs new file mode 100644 index 000000000..5ade5c290 --- /dev/null +++ b/nodedb-crdt/src/validator/incoming.rs @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Incoming delta parameters for policy-aware validation. + +use crate::CrdtAuthContext; + +/// The delta a peer sends, with who sent it and when. +pub struct IncomingDelta { + pub peer_id: u64, + pub auth: CrdtAuthContext, + pub delta_bytes: Vec, + /// Hybrid Logical Clock timestamp of the incoming write. + pub hlc_timestamp: u64, +} diff --git a/nodedb-crdt/src/validator/mod.rs b/nodedb-crdt/src/validator/mod.rs index f5836d9e0..8bdeef48f 100644 --- a/nodedb-crdt/src/validator/mod.rs +++ b/nodedb-crdt/src/validator/mod.rs @@ -9,9 +9,11 @@ pub mod bitemporal; mod core; +pub mod incoming; mod policy_dispatch; mod types; mod validate; pub use core::Validator; +pub use incoming::IncomingDelta; pub use types::{ProposedChange, ValidationOutcome, Violation}; diff --git a/nodedb-crdt/src/validator/policy_dispatch.rs b/nodedb-crdt/src/validator/policy_dispatch.rs index cfbde7a0a..9f57874af 100644 --- a/nodedb-crdt/src/validator/policy_dispatch.rs +++ b/nodedb-crdt/src/validator/policy_dispatch.rs @@ -2,7 +2,6 @@ //! Policy resolution dispatch for validation violations. -use crate::CrdtAuthContext; use crate::constraint::{Constraint, ConstraintKind}; use crate::dead_letter::CompensationHint; use crate::error::Result; @@ -10,6 +9,7 @@ use crate::policy::{ConflictPolicy, PolicyResolution, ResolvedAction}; use crate::row_lookup::RowLookup; use super::core::Validator; +use super::incoming::IncomingDelta; use super::types::{ProposedChange, ValidationOutcome, Violation}; impl Validator { @@ -21,10 +21,9 @@ impl Validator { /// # Arguments /// /// * `state` — current CRDT state - /// * `peer_id` — source peer ID /// * `change` — proposed change - /// * `delta_bytes` — raw delta bytes - /// * `hlc_timestamp` — Hybrid Logical Clock timestamp of the incoming write + /// * `incoming` — source peer ID, auth context, raw delta bytes, and HLC timestamp + /// * `crs` — coordinate reference system context /// /// Returns: /// - `Ok(PolicyResolution::AutoResolved(_))` if the policy auto-fixed the violation @@ -35,13 +34,17 @@ impl Validator { pub fn validate_with_policy( &mut self, state: &impl RowLookup, - peer_id: u64, - auth: CrdtAuthContext, change: &ProposedChange, - delta_bytes: Vec, - hlc_timestamp: u64, + incoming: IncomingDelta, + crs: &nodedb_crs::CrsContext<'_>, ) -> Result { - match self.validate(state, change) { + let IncomingDelta { + peer_id, + auth, + delta_bytes, + hlc_timestamp, + } = incoming; + match self.validate(state, change, crs) { ValidationOutcome::Accepted => { // No violation; return synthetic "auto-resolved" to maintain API consistency Ok(PolicyResolution::AutoResolved( @@ -255,6 +258,7 @@ mod tests { use loro::LoroValue; use super::*; + use crate::CrdtAuthContext; use crate::constraint::ConstraintSet; use crate::state::CrdtState; @@ -294,11 +298,14 @@ mod tests { let resolution = validator .validate_with_policy( &state, - 42, - CrdtAuthContext::default(), &change, - b"delta".to_vec(), - now, + IncomingDelta { + peer_id: 42, + auth: CrdtAuthContext::default(), + delta_bytes: b"delta".to_vec(), + hlc_timestamp: now, + }, + &nodedb_crs::CrsContext::builtin_only(), ) .unwrap(); @@ -350,11 +357,14 @@ mod tests { let resolution = validator .validate_with_policy( &state, - 42, - CrdtAuthContext::default(), &change, - b"delta".to_vec(), - now, + IncomingDelta { + peer_id: 42, + auth: CrdtAuthContext::default(), + delta_bytes: b"delta".to_vec(), + hlc_timestamp: now, + }, + &nodedb_crs::CrsContext::builtin_only(), ) .unwrap(); @@ -395,11 +405,14 @@ mod tests { let resolution = validator .validate_with_policy( &state, - 42, - CrdtAuthContext::default(), &change, - b"delta".to_vec(), - now, + IncomingDelta { + peer_id: 42, + auth: CrdtAuthContext::default(), + delta_bytes: b"delta".to_vec(), + hlc_timestamp: now, + }, + &nodedb_crs::CrsContext::builtin_only(), ) .unwrap(); @@ -443,11 +456,14 @@ mod tests { let resolution = validator .validate_with_policy( &state, - 42, - CrdtAuthContext::default(), &change, - b"delta".to_vec(), - now, + IncomingDelta { + peer_id: 42, + auth: CrdtAuthContext::default(), + delta_bytes: b"delta".to_vec(), + hlc_timestamp: now, + }, + &nodedb_crs::CrsContext::builtin_only(), ) .unwrap(); diff --git a/nodedb-crdt/src/validator/validate.rs b/nodedb-crdt/src/validator/validate.rs index 1870bad1f..5f98bfe97 100644 --- a/nodedb-crdt/src/validator/validate.rs +++ b/nodedb-crdt/src/validator/validate.rs @@ -8,6 +8,7 @@ use crate::policy::PolicyResolution; use crate::row_lookup::RowLookup; use super::core::Validator; +use super::incoming::IncomingDelta; use super::types::{ProposedChange, ValidationOutcome, Violation}; /// Convert the first violation into a [`CrdtError::ConstraintViolation`], appending @@ -77,13 +78,19 @@ impl Validator { /// Validate a proposed change against all applicable constraints. /// /// Returns `Accepted` if all constraints pass, or `Rejected` with - /// detailed violation information. - pub fn validate(&self, state: &impl RowLookup, change: &ProposedChange) -> ValidationOutcome { + /// detailed violation information. CHECK predicates resolve SRIDs + /// through `crs`. + pub fn validate( + &self, + state: &impl RowLookup, + change: &ProposedChange, + crs: &nodedb_crs::CrsContext<'_>, + ) -> ValidationOutcome { let constraints = self.constraints.for_collection(&change.collection); let mut violations = Vec::new(); for constraint in constraints { - match self.check_constraint(state, change, constraint) { + match self.check_constraint(state, change, constraint, crs) { Ok(Some(violation)) => violations.push(violation), Ok(None) => {} // A predicate that could not be evaluated (division/modulo by @@ -129,6 +136,7 @@ impl Validator { auth: CrdtAuthContext, change: &ProposedChange, delta_bytes: Vec, + crs: &nodedb_crs::CrsContext<'_>, ) -> Result<()> { // Check auth expiry: agents that accumulated deltas offline must // re-authenticate before syncing. @@ -152,7 +160,17 @@ impl Validator { .unwrap_or_default() .as_millis() as u64; - match self.validate_with_policy(state, peer_id, auth, change, delta_bytes, hlc_timestamp)? { + match self.validate_with_policy( + state, + change, + IncomingDelta { + peer_id, + auth, + delta_bytes, + hlc_timestamp, + }, + crs, + )? { PolicyResolution::AutoResolved(_) => Ok(()), PolicyResolution::Deferred { violations, .. } => { // Violation was deferred for retry; return error to signal this. @@ -208,7 +226,7 @@ mod tests { }; assert!(matches!( - validator.validate(&state, &change), + validator.validate(&state, &change, &nodedb_crs::CrsContext::builtin_only()), ValidationOutcome::Accepted )); } @@ -229,7 +247,7 @@ mod tests { )], }; - match validator.validate(&state, &change) { + match validator.validate(&state, &change, &nodedb_crs::CrsContext::builtin_only()) { ValidationOutcome::Rejected(v) => { assert_eq!(v.len(), 1); assert_eq!(v[0].constraint_name, "users_name_nn"); @@ -254,7 +272,7 @@ mod tests { fields: vec![("author_id".into(), LoroValue::String("u1".into()))], }; - match validator.validate(&state, &change) { + match validator.validate(&state, &change, &nodedb_crs::CrsContext::builtin_only()) { ValidationOutcome::Rejected(v) => { assert_eq!(v[0].constraint_name, "posts_author_fk"); assert!(matches!( @@ -290,7 +308,7 @@ mod tests { }; assert!(matches!( - validator.validate(&state, &change), + validator.validate(&state, &change, &nodedb_crs::CrsContext::builtin_only()), ValidationOutcome::Accepted )); } @@ -318,6 +336,7 @@ mod tests { CrdtAuthContext::default(), &change, b"delta-bytes".to_vec(), + &nodedb_crs::CrsContext::builtin_only(), ) .unwrap_err(); @@ -344,7 +363,7 @@ mod tests { }; assert!(matches!( - validator.validate(&state, &change), + validator.validate(&state, &change, &nodedb_crs::CrsContext::builtin_only()), ValidationOutcome::Accepted )); } @@ -363,7 +382,7 @@ mod tests { fields: vec![("age".into(), LoroValue::I64(-1))], }; - match validator.validate(&state, &change) { + match validator.validate(&state, &change, &nodedb_crs::CrsContext::builtin_only()) { ValidationOutcome::Rejected(v) => { assert_eq!(v.len(), 1); assert_eq!(v[0].constraint_name, "people_age_check"); @@ -390,7 +409,7 @@ mod tests { fields: vec![("age".into(), LoroValue::I64(5))], }; - match validator.validate(&state, &change) { + match validator.validate(&state, &change, &nodedb_crs::CrsContext::builtin_only()) { ValidationOutcome::Rejected(v) => { assert_eq!(v.len(), 1); assert_eq!(v[0].constraint_name, "people_age_check"); @@ -425,7 +444,7 @@ mod tests { fields: vec![("age".into(), LoroValue::I64(0))], }; - match validator.validate(&state, &change) { + match validator.validate(&state, &change, &nodedb_crs::CrsContext::builtin_only()) { ValidationOutcome::EvalError { constraint_name, error, @@ -468,6 +487,7 @@ mod signing_tests { CrdtAuthContext::default(), &change(), b"delta".to_vec(), + &nodedb_crs::CrsContext::builtin_only(), ) .unwrap_err(); assert!(matches!(error, CrdtError::InvalidSignature { .. })); @@ -483,7 +503,14 @@ mod signing_tests { }; let error = validator - .validate_or_reject(&state, 2, auth, &change(), b"delta".to_vec()) + .validate_or_reject( + &state, + 2, + auth, + &change(), + b"delta".to_vec(), + &nodedb_crs::CrsContext::builtin_only(), + ) .unwrap_err(); assert!(matches!(error, CrdtError::InvalidSignature { .. })); } diff --git a/nodedb-crdt/tests/constraint_resolution/cascade_defer.rs b/nodedb-crdt/tests/constraint_resolution/cascade_defer.rs index 965c2a56a..e84944022 100644 --- a/nodedb-crdt/tests/constraint_resolution/cascade_defer.rs +++ b/nodedb-crdt/tests/constraint_resolution/cascade_defer.rs @@ -5,7 +5,7 @@ use loro::LoroValue; use nodedb_crdt::policy::{PolicyResolution, ResolvedAction}; use nodedb_crdt::state::CrdtState; -use nodedb_crdt::validator::Validator; +use nodedb_crdt::validator::{IncomingDelta, Validator}; use crate::common::{AuthCtx, cascade_defer_policy, post_change, schema, wall_now_ms}; @@ -27,11 +27,14 @@ fn cascade_defer_succeeds_when_parent_arrives_before_ttl() { let r = validator .validate_with_policy( &leader, - 42, - AuthCtx::default(), &post, - b"post-delta".to_vec(), - started_ms, + IncomingDelta { + peer_id: 42, + auth: AuthCtx::default(), + delta_bytes: b"post-delta".to_vec(), + hlc_timestamp: started_ms, + }, + &nodedb_crs::CrsContext::builtin_only(), ) .unwrap(); match r { @@ -65,11 +68,14 @@ fn cascade_defer_succeeds_when_parent_arrives_before_ttl() { let r2 = validator .validate_with_policy( &leader, - 42, - AuthCtx::default(), &post, - b"post-delta".to_vec(), - retry_now, + IncomingDelta { + peer_id: 42, + auth: AuthCtx::default(), + delta_bytes: b"post-delta".to_vec(), + hlc_timestamp: retry_now, + }, + &nodedb_crs::CrsContext::builtin_only(), ) .unwrap(); assert!( @@ -101,11 +107,14 @@ fn cascade_defer_expires_to_dlq_when_parent_never_arrives() { let r = validator .validate_with_policy( &leader, - 42, - AuthCtx::default(), &post_change("post-orphan", "ghost-user"), - b"orphan-delta".to_vec(), - started_ms, + IncomingDelta { + peer_id: 42, + auth: AuthCtx::default(), + delta_bytes: b"orphan-delta".to_vec(), + hlc_timestamp: started_ms, + }, + &nodedb_crs::CrsContext::builtin_only(), ) .unwrap(); assert!(matches!(r, PolicyResolution::Deferred { .. })); diff --git a/nodedb-crdt/tests/constraint_resolution/fk_chain.rs b/nodedb-crdt/tests/constraint_resolution/fk_chain.rs index ea44f1221..f857f66de 100644 --- a/nodedb-crdt/tests/constraint_resolution/fk_chain.rs +++ b/nodedb-crdt/tests/constraint_resolution/fk_chain.rs @@ -7,7 +7,7 @@ use nodedb_crdt::policy::{ CollectionPolicy, ConflictPolicy, PolicyRegistry, PolicyResolution, ResolvedAction, }; use nodedb_crdt::state::CrdtState; -use nodedb_crdt::validator::Validator; +use nodedb_crdt::validator::{IncomingDelta, Validator}; use crate::common::{AuthCtx, post_change, schema, user_change, wall_now_ms}; @@ -36,11 +36,14 @@ fn fk_chain_child_arrives_before_parent_then_resolves() { let r_post = validator .validate_with_policy( &leader, - 1, - AuthCtx::default(), &post, - b"post-delta".to_vec(), - started_ms, + IncomingDelta { + peer_id: 1, + auth: AuthCtx::default(), + delta_bytes: b"post-delta".to_vec(), + hlc_timestamp: started_ms, + }, + &nodedb_crs::CrsContext::builtin_only(), ) .unwrap(); assert!(matches!(r_post, PolicyResolution::Deferred { .. })); @@ -50,11 +53,14 @@ fn fk_chain_child_arrives_before_parent_then_resolves() { let r_user = validator .validate_with_policy( &leader, - 2, - AuthCtx::default(), &user, - b"user-delta".to_vec(), - started_ms + 50, + IncomingDelta { + peer_id: 2, + auth: AuthCtx::default(), + delta_bytes: b"user-delta".to_vec(), + hlc_timestamp: started_ms + 50, + }, + &nodedb_crs::CrsContext::builtin_only(), ) .unwrap(); assert!( @@ -85,11 +91,14 @@ fn fk_chain_child_arrives_before_parent_then_resolves() { let r_retry = validator .validate_with_policy( &leader, - 1, - AuthCtx::default(), &post, - b"post-delta".to_vec(), - retry_now, + IncomingDelta { + peer_id: 1, + auth: AuthCtx::default(), + delta_bytes: b"post-delta".to_vec(), + hlc_timestamp: retry_now, + }, + &nodedb_crs::CrsContext::builtin_only(), ) .unwrap(); assert!( diff --git a/nodedb-crdt/tests/constraint_resolution/unique_rename.rs b/nodedb-crdt/tests/constraint_resolution/unique_rename.rs index 4f71c846d..302225794 100644 --- a/nodedb-crdt/tests/constraint_resolution/unique_rename.rs +++ b/nodedb-crdt/tests/constraint_resolution/unique_rename.rs @@ -7,7 +7,7 @@ use loro::LoroValue; use nodedb_crdt::policy::{PolicyResolution, ResolvedAction}; use nodedb_crdt::state::CrdtState; -use nodedb_crdt::validator::Validator; +use nodedb_crdt::validator::{IncomingDelta, Validator}; use crate::common::{AuthCtx, rename_suffix_policy, schema, user_change}; @@ -27,11 +27,14 @@ fn multi_agent_unique_rename_advances_suffix_monotonically() { let r_a = validator .validate_with_policy( &leader, - 1, - AuthCtx::default(), &user_change("user-a", "Alice", "alice@example.com"), - b"delta-a".to_vec(), - now_ms, + IncomingDelta { + peer_id: 1, + auth: AuthCtx::default(), + delta_bytes: b"delta-a".to_vec(), + hlc_timestamp: now_ms, + }, + &nodedb_crs::CrsContext::builtin_only(), ) .unwrap(); assert!( @@ -58,11 +61,14 @@ fn multi_agent_unique_rename_advances_suffix_monotonically() { let r_b = validator .validate_with_policy( &leader, - 2, - AuthCtx::default(), &user_change("user-b", "Bob", "alice@example.com"), - b"delta-b".to_vec(), - now_ms + 100, + IncomingDelta { + peer_id: 2, + auth: AuthCtx::default(), + delta_bytes: b"delta-b".to_vec(), + hlc_timestamp: now_ms + 100, + }, + &nodedb_crs::CrsContext::builtin_only(), ) .unwrap(); let suffix_b = match r_b { @@ -101,11 +107,14 @@ fn multi_agent_unique_rename_advances_suffix_monotonically() { let r_c = validator .validate_with_policy( &leader, - 3, - AuthCtx::default(), &user_change("user-c", "Carol", "alice@example.com"), - b"delta-c".to_vec(), - now_ms + 200, + IncomingDelta { + peer_id: 3, + auth: AuthCtx::default(), + delta_bytes: b"delta-c".to_vec(), + hlc_timestamp: now_ms + 200, + }, + &nodedb_crs::CrsContext::builtin_only(), ) .unwrap(); let suffix_c = match r_c { diff --git a/nodedb-crdt/tests/crdt_sql_paradox.rs b/nodedb-crdt/tests/crdt_sql_paradox.rs index a35d5b1ae..76fc3cc10 100644 --- a/nodedb-crdt/tests/crdt_sql_paradox.rs +++ b/nodedb-crdt/tests/crdt_sql_paradox.rs @@ -57,7 +57,11 @@ fn split_brain_unique_violation_with_compensation() { }; assert!(matches!( - validator.validate(&leader_state, &agent_b_change), + validator.validate( + &leader_state, + &agent_b_change, + &nodedb_crs::CrsContext::builtin_only() + ), ValidationOutcome::Accepted )); @@ -94,6 +98,7 @@ fn split_brain_unique_violation_with_compensation() { nodedb_crdt::CrdtAuthContext::default(), &agent_a_change, b"agent-a-delta".to_vec(), + &nodedb_crs::CrsContext::builtin_only(), ); assert!(result.is_err()); @@ -129,7 +134,11 @@ fn split_brain_unique_violation_with_compensation() { // The retry succeeds. assert!(matches!( - validator.validate(&leader_state, &retry_change), + validator.validate( + &leader_state, + &retry_change, + &nodedb_crs::CrsContext::builtin_only() + ), ValidationOutcome::Accepted )); assert!(validator.dlq().is_empty()); @@ -161,6 +170,7 @@ fn offline_agent_references_deleted_user() { nodedb_crdt::CrdtAuthContext::default(), &change, b"post-delta".to_vec(), + &nodedb_crs::CrsContext::builtin_only(), ); assert!(result.is_err()); @@ -214,7 +224,12 @@ fn pre_validation_saves_raft_roundtrip() { ], }; - match pre_validate::pre_validate(&validator, &leader_state, &change) { + match pre_validate::pre_validate( + &validator, + &leader_state, + &change, + &nodedb_crs::CrsContext::builtin_only(), + ) { PreValidationResult::FastReject { constraint, .. } => { assert_eq!(constraint, "users_email_unique"); } @@ -275,7 +290,7 @@ fn not_null_violation_hints_provide_field() { ], }; - match validator.validate(&state, &change) { + match validator.validate(&state, &change, &nodedb_crs::CrsContext::builtin_only()) { ValidationOutcome::Rejected(violations) => { assert_eq!(violations.len(), 1); match &violations[0].hint { diff --git a/nodedb-crs/Cargo.toml b/nodedb-crs/Cargo.toml new file mode 100644 index 000000000..9d2be1fcf --- /dev/null +++ b/nodedb-crs/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "nodedb-crs" +description = "Coordinate reference systems for NodeDB: EPSG registry, PROJ-string parsing, projections and datum shifts" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license = "Apache-2.0" +authors = ["Biz Impulse Solutions Sdn. Bhd. and the NodeDB contributors"] +readme = "README.md" +repository.workspace = true +homepage.workspace = true +documentation = "https://docs.rs/nodedb-crs" +keywords = ["crs", "epsg", "projection", "geodesy", "proj"] +categories = ["science::geo"] + +[features] +default = [] + +[dependencies] +nodedb-srid = { workspace = true } +thiserror = { workspace = true } +serde = { workspace = true } +zerompk = { workspace = true } diff --git a/nodedb-crs/NOTICE b/nodedb-crs/NOTICE new file mode 100644 index 000000000..4bf4bfc89 --- /dev/null +++ b/nodedb-crs/NOTICE @@ -0,0 +1,2 @@ +Copyright 2026 Biz Impulse Solutions Sdn. Bhd. and the NodeDB contributors. +This product is licensed under the Apache License, Version 2.0. diff --git a/nodedb-crs/README.md b/nodedb-crs/README.md new file mode 100644 index 000000000..f9ec4240f --- /dev/null +++ b/nodedb-crs/README.md @@ -0,0 +1,82 @@ +# `nodedb-crs` + +> Coordinate reference systems for NodeDB: an EPSG registry, user-defined CRSs, PROJ-string, WKT2 and PROJJSON text, map projections and datum shifts. + +Pure Rust with no I/O and no native dependency, so it builds for `wasm32-unknown-unknown`. NodeDB Origin and Lite resolve and transform every SRID through it. + +## What it does + +- `CrsContext` resolves an SRID to a `CrsDef`: user-defined CRSs first, then the built-in table. +- `Transformer` maps coordinates between two definitions through ECEF. `apply` runs each time-dependent datum shift at its parameter reference epoch. `apply_at` takes a `CoordinateEpoch`, a decimal year in 1900..=2200. +- `parse_proj_string`, `parse_wkt2` and `parse_projjson` read a definition. `CrsDef::to_proj_string`, `to_wkt2` and `to_projjson` write one back, and a round trip reads back the same definition. +- `CustomCrsSet` holds user-defined CRSs (`CREATE SPATIAL_REF_SYS`). A dropped code stays a tombstone and is never reused. + +The SRID type and the `SridResolver` trait live in `nodedb-srid`, which carries no table. A crate that only stores SRIDs depends on `nodedb-srid` and never links the EPSG table. + +## Supported CRSs + +| Range | Authority | Contents | +|---|---|---| +| 1 to 899 999 | EPSG | 5 916 geographic and projected CRSs from EPSG v12.029 | +| 12287, 25314 to 25322 | EPSG | GDM2020 and its nine state Cassini grids, from EPSG change 2026.081 (`data/epsg_supplement.tsv`) | +| 910 000 to 989 999 | user | `CREATE SPATIAL_REF_SYS` definitions | +| 990 001 | NodeDB | MODIS sinusoidal grid (SR-ORG:6842) | +| 990 002 | NodeDB | Himawari geostationary view, 140.7° E | + +Projection methods: Transverse Mercator and UTM, Hotine Oblique Mercator (variants A and B), Cassini-Soldner (and hyperbolic), Mercator (variants A and B), Pseudo-Mercator, Lambert Cylindrical Equal Area, Sinusoidal, Geostationary Satellite, Lambert Conic Conformal (1SP and 2SP), Polar and oblique Stereographic, UPS, Albers Equal Area, Lambert Azimuthal Equal Area, Krovak (and modified), Swiss Oblique Mercator, American Polyconic, Equidistant Cylindrical, Equal Earth, Azimuthal Equidistant (and Guam), New Zealand Map Grid, and Laborde Oblique Mercator. + +Datum shifts: 3-, 7- and 15-parameter (time-dependent) Helmert, position-vector and coordinate-frame conventions, IOGP Guidance Note 7-2 §4.2.3 and §4.2.5 (EPSG methods 9603, 9606, 9607, 1031, 1032, 1033, 1035, 1037, 1038, and 1053 to 1058). WKT2 and PROJJSON read and write the rates and the parameter reference epoch. A PROJ string has no rates: `to_proj_string` refuses a time-dependent shift with `ProjExportError::TimeDependentShift`, as PROJ does. Linear units: every PROJ `+units` name and any `+to_meter` factor. + +The table holds every non-deprecated EPSG geographic 2D and projected CRS, except the few the generator skips with a stated reason: methods with no implementation here, such as Bonne (South Orientated), Colombia Urban and the Tunisia Mining Grid. A datum EPSG relates to WGS 84 by a grid carries its best Helmert transformation, or no shift. + +## GDM2020 + +GDM2020 (Geocentric Datum of Malaysia 2020, EPSG datum 1471) is in the EPSG registry from change 2026.081, revision 2026-10-05. No released PROJ or pyproj bundles it: PROJ 9.9.0 carries EPSG v13.102. `data/epsg_supplement.tsv` holds its rows with their EPSG codes: + +| Code | CRS | +|---|---| +| 12287 | GDM2020, geographic 2D, GRS 1980 | +| 25314 | GDM2020 / Johor Grid | +| 25315 | GDM2020 / Sembilan and Melaka Grid | +| 25316 | GDM2020 / Pahang Grid | +| 25317 | GDM2020 / Selangor Grid | +| 25318 | GDM2020 / Terengganu Grid | +| 25319 | GDM2020 / Pinang Grid | +| 25320 | GDM2020 / Kedah and Perlis Grid | +| 25321 | GDM2020 / Perak Grid | +| 25322 | GDM2020 / Kelantan Grid | + +Sources: + +- EPSG Geodetic Parameter Registry API, `https://apps.epsg.org/api/v1/`: datum 1471, CRS 12287, conversions 12314 to 12322 (Cassini-Soldner, origins in sexagesimal DMS, false easting and northing 0 m), projected CRSs 25314 to 25322, transformation 12291 (GDM2020 to WGS 84, null geocentric translations, accuracy 1 m). +- JUPEM, Technical Guide to the Coordinate Conversion, Datum Transformation and Map Projection, circular KPUP 3/2021: Table 7 gives the same GDM2020 state origins. Test example 4.6 (i) checks EPSG:25320. Its printed easting is 1.000 m off the Cassini-Soldner formula, and its northing matches to the millimetre. + +GDM2000 to GDM2020 needs JUPEM's velocity and post-seismic deformation grids, which JUPEM does not publish openly, so the crate has no shift between them. GDM2020 Peninsular RSO and East Malaysia BRSO have no EPSG code, and JUPEM withholds their parameters (KPUP 3/2021 Figure 14), so the crate does not define them. + +## Accuracy against PROJ + +`tests/proj_reference.rs` transforms the centre and two inset corners of each CRS's area of use, forward and inverse, and compares with PROJ 9.8.1. A point agrees within 1 mm projected and 1e-9 degrees geographic, unless its fixture row carries its own tolerance in metres. + +The crate applies no grid shift (NADCON, NTv2, geoid models). Where PROJ picks a grid transformation between two datums, the result here differs by that grid's correction, up to several metres. The view `nodedb.spatial_ref_sys` shows each built-in's published Helmert accuracy in `datum_shift_accuracy_m`. + +## Regenerating the EPSG table + +`data/epsg.tsv` is generated from a pinned pyproj (3.8.0, bundling PROJ 9.8.1 and EPSG v12.029). The script refuses any other version and exits non-zero on a method the Rust parser does not implement. It also writes `data/epsg_supplement.tsv` from its `SUPPLEMENT` definitions, and stops when the pinned EPSG dataset already holds one of those codes. + +Create the virtual environment under the workspace build directory, never under `/tmp`: + +```bash +python3 -m venv target-cluster/epsg-venv +target-cluster/epsg-venv/bin/pip install pyproj==3.8.0 +target-cluster/epsg-venv/bin/python scripts/gen_epsg.py # write data/epsg.tsv +target-cluster/epsg-venv/bin/python scripts/gen_epsg.py --fixtures # also write tests/fixtures/*.tsv +target-cluster/epsg-venv/bin/python scripts/gen_epsg.py --check # compare with the committed files +``` + +## Status + +Pre-1.0. APIs can change between minor versions until 1.0. See the workspace [`README.md`](../README.md) for the full project overview and the GitHub [release notes](https://github.com/NodeDB-Lab/nodedb/releases) for per-version changes. + +## License + +Licensed under the Apache License, Version 2.0 ([`LICENSE-APACHE`](../LICENSE.APACHE-2.0)). diff --git a/nodedb-crs/data/epsg.tsv b/nodedb-crs/data/epsg.tsv new file mode 100644 index 000000000..de26b87a8 --- /dev/null +++ b/nodedb-crs/data/epsg.tsv @@ -0,0 +1,5918 @@ +# EPSG v12.029 PROJ 9.8.1 +code kind name area accuracy_m datum proj +2000 projected Anguilla 1957 / British West Indies Grid -63.22,18.11,-62.92,18.33 6600 +proj=tmerc +lat_0=0 +lon_0=-62 +k=0.9995 +x_0=400000 +y_0=0 +a=6378249.145 +rf=293.465 +units=m +no_defs +2001 projected Antigua 1943 / British West Indies Grid -61.95,16.94,-61.61,17.22 10 6601 +proj=tmerc +lat_0=0 +lon_0=-62 +k=0.9995 +x_0=400000 +y_0=0 +a=6378249.145 +rf=293.465 +towgs84=-255,-15,71 +units=m +no_defs +2002 projected Dominica 1945 / British West Indies Grid -61.55,15.14,-61.2,15.69 10 6602 +proj=tmerc +lat_0=0 +lon_0=-62 +k=0.9995 +x_0=400000 +y_0=0 +a=6378249.145 +rf=293.465 +towgs84=725,685,536 +units=m +no_defs +2003 projected Grenada 1953 / British West Indies Grid -61.84,11.94,-61.35,12.57 10 6603 +proj=tmerc +lat_0=0 +lon_0=-62 +k=0.9995 +x_0=400000 +y_0=0 +a=6378249.145 +rf=293.465 +towgs84=72,213.7,93 +units=m +no_defs +2004 projected Montserrat 1958 / British West Indies Grid -62.29,16.62,-62.08,16.87 44 6604 +proj=tmerc +lat_0=0 +lon_0=-62 +k=0.9995 +x_0=400000 +y_0=0 +a=6378249.145 +rf=293.465 +towgs84=174,359,365 +units=m +no_defs +2005 projected St. Kitts 1955 / British West Indies Grid -62.92,17.06,-62.5,17.46 10 6605 +proj=tmerc +lat_0=0 +lon_0=-62 +k=0.9995 +x_0=400000 +y_0=0 +a=6378249.145 +rf=293.465 +towgs84=9,183,236 +units=m +no_defs +2006 projected St. Lucia 1955 / British West Indies Grid -61.13,13.66,-60.82,14.16 2 6606 +proj=tmerc +lat_0=0 +lon_0=-62 +k=0.9995 +x_0=400000 +y_0=0 +a=6378249.145 +rf=293.465 +towgs84=-153,153,307 +units=m +no_defs +2007 projected St. Vincent 45 / British West Indies Grid -61.52,12.54,-61.07,13.44 1 6607 +proj=tmerc +lat_0=0 +lon_0=-62 +k=0.9995 +x_0=400000 +y_0=0 +a=6378249.145 +rf=293.465 +towgs84=195.671,332.517,274.607 +units=m +no_defs +2009 projected NAD27(CGQ77) / SCoPQ zone 3 -60,50.2,-57.1,52.01 6609 +proj=tmerc +lat_0=0 +lon_0=-58.5 +k=0.9999 +x_0=304800 +y_0=0 +ellps=clrk66 +units=m +no_defs +2010 projected NAD27(CGQ77) / SCoPQ zone 4 -63,47.16,-60,52.01 6609 +proj=tmerc +lat_0=0 +lon_0=-61.5 +k=0.9999 +x_0=304800 +y_0=0 +ellps=clrk66 +units=m +no_defs +2011 projected NAD27(CGQ77) / SCoPQ zone 5 -66,47.95,-63,60.42 6609 +proj=tmerc +lat_0=0 +lon_0=-64.5 +k=0.9999 +x_0=304800 +y_0=0 +ellps=clrk66 +units=m +no_defs +2012 projected NAD27(CGQ77) / SCoPQ zone 6 -69,47.31,-66,59 6609 +proj=tmerc +lat_0=0 +lon_0=-67.5 +k=0.9999 +x_0=304800 +y_0=0 +ellps=clrk66 +units=m +no_defs +2013 projected NAD27(CGQ77) / SCoPQ zone 7 -72,45.01,-69,61.8 6609 +proj=tmerc +lat_0=0 +lon_0=-70.5 +k=0.9999 +x_0=304800 +y_0=0 +ellps=clrk66 +units=m +no_defs +2014 projected NAD27(CGQ77) / SCoPQ zone 8 -75,44.99,-72,62.53 6609 +proj=tmerc +lat_0=0 +lon_0=-73.5 +k=0.9999 +x_0=304800 +y_0=0 +ellps=clrk66 +units=m +no_defs +2015 projected NAD27(CGQ77) / SCoPQ zone 9 -78,45.37,-75,62.62 6609 +proj=tmerc +lat_0=0 +lon_0=-76.5 +k=0.9999 +x_0=304800 +y_0=0 +ellps=clrk66 +units=m +no_defs +2016 projected NAD27(CGQ77) / SCoPQ zone 10 -79.85,46.23,-78,62.45 6609 +proj=tmerc +lat_0=0 +lon_0=-79.5 +k=0.9999 +x_0=304800 +y_0=0 +ellps=clrk66 +units=m +no_defs +2017 projected NAD27(76) / MTM zone 8 -75,44.98,-74.35,45.65 6608 +proj=tmerc +lat_0=0 +lon_0=-73.5 +k=0.9999 +x_0=304800 +y_0=0 +ellps=clrk66 +units=m +no_defs +2018 projected NAD27(76) / MTM zone 9 -78,43.63,-75,46.25 6608 +proj=tmerc +lat_0=0 +lon_0=-76.5 +k=0.9999 +x_0=304800 +y_0=0 +ellps=clrk66 +units=m +no_defs +2019 projected NAD27(76) / MTM zone 10 -81,42.26,-77.99,47.33 6608 +proj=tmerc +lat_0=0 +lon_0=-79.5 +k=0.9999 +x_0=304800 +y_0=0 +ellps=clrk66 +units=m +no_defs +2020 projected NAD27(76) / MTM zone 11 -83.6,41.67,-81,46 6608 +proj=tmerc +lat_0=0 +lon_0=-82.5 +k=0.9999 +x_0=304800 +y_0=0 +ellps=clrk66 +units=m +no_defs +2021 projected NAD27(76) / MTM zone 12 -82.5,46,-79.5,55.21 6608 +proj=tmerc +lat_0=0 +lon_0=-81 +k=0.9999 +x_0=304800 +y_0=0 +ellps=clrk66 +units=m +no_defs +2022 projected NAD27(76) / MTM zone 13 -85.5,46,-82.5,55.59 6608 +proj=tmerc +lat_0=0 +lon_0=-84 +k=0.9999 +x_0=304800 +y_0=0 +ellps=clrk66 +units=m +no_defs +2023 projected NAD27(76) / MTM zone 14 -88.5,47.17,-85.5,56.7 6608 +proj=tmerc +lat_0=0 +lon_0=-87 +k=0.9999 +x_0=304800 +y_0=0 +ellps=clrk66 +units=m +no_defs +2024 projected NAD27(76) / MTM zone 15 -91.5,47.97,-88.5,56.9 6608 +proj=tmerc +lat_0=0 +lon_0=-90 +k=0.9999 +x_0=304800 +y_0=0 +ellps=clrk66 +units=m +no_defs +2025 projected NAD27(76) / MTM zone 16 -94.5,48.06,-91.5,55.2 6608 +proj=tmerc +lat_0=0 +lon_0=-93 +k=0.9999 +x_0=304800 +y_0=0 +ellps=clrk66 +units=m +no_defs +2026 projected NAD27(76) / MTM zone 17 -95.16,48.69,-94.5,53.24 6608 +proj=tmerc +lat_0=0 +lon_0=-96 +k=0.9999 +x_0=304800 +y_0=0 +ellps=clrk66 +units=m +no_defs +2027 projected NAD27(76) / UTM zone 15N -95.16,48.03,-90,56.2 6608 +proj=utm +zone=15 +ellps=clrk66 +units=m +no_defs +2028 projected NAD27(76) / UTM zone 16N -90,46.11,-84,56.9 6608 +proj=utm +zone=16 +ellps=clrk66 +units=m +no_defs +2029 projected NAD27(76) / UTM zone 17N -84,41.67,-78,55.37 6608 +proj=utm +zone=17 +ellps=clrk66 +units=m +no_defs +2030 projected NAD27(76) / UTM zone 18N -78,43.63,-74.35,46.25 6608 +proj=utm +zone=18 +ellps=clrk66 +units=m +no_defs +2031 projected NAD27(CGQ77) / UTM zone 17N -79.85,46.23,-78,62.45 6609 +proj=utm +zone=17 +ellps=clrk66 +units=m +no_defs +2032 projected NAD27(CGQ77) / UTM zone 18N -78,44.99,-72,62.62 6609 +proj=utm +zone=18 +ellps=clrk66 +units=m +no_defs +2033 projected NAD27(CGQ77) / UTM zone 19N -72,45.01,-66,61.8 6609 +proj=utm +zone=19 +ellps=clrk66 +units=m +no_defs +2034 projected NAD27(CGQ77) / UTM zone 20N -66,47.16,-60,60.42 6609 +proj=utm +zone=20 +ellps=clrk66 +units=m +no_defs +2035 projected NAD27(CGQ77) / UTM zone 21N -60,50.2,-57.1,52.01 6609 +proj=utm +zone=21 +ellps=clrk66 +units=m +no_defs +2039 projected Israel 1993 / Israeli TM Grid 34.17,29.45,35.69,33.28 0.5 6141 +proj=tmerc +lat_0=31.7343936111111 +lon_0=35.2045169444444 +k=1.0000067 +x_0=219529.584 +y_0=626907.39 +ellps=GRS80 +towgs84=23.772,17.49,17.859,-0.3132,-1.85274,1.67299,-5.4262 +units=m +no_defs +2040 projected Locodjo 1965 / UTM zone 30N -6,4.92,-2.48,10.46 15 6142 +proj=utm +zone=30 +a=6378249.145 +rf=293.465 +towgs84=-125,53,467 +units=m +no_defs +2041 projected Abidjan 1987 / UTM zone 30N -6,4.92,-2.48,10.46 2 6143 +proj=utm +zone=30 +a=6378249.145 +rf=293.465 +towgs84=-124.76,53,466.79 +units=m +no_defs +2042 projected Locodjo 1965 / UTM zone 29N -8.61,4.29,-6,10.74 6142 +proj=utm +zone=29 +a=6378249.145 +rf=293.465 +units=m +no_defs +2043 projected Abidjan 1987 / UTM zone 29N -8.61,4.29,-6,10.74 2 6143 +proj=utm +zone=29 +a=6378249.145 +rf=293.465 +towgs84=-124.76,53,466.79 +units=m +no_defs +2044 projected Hanoi 1972 / Gauss-Kruger zone 18 102.14,8.33,108,23.4 5 6147 +proj=tmerc +lat_0=0 +lon_0=105 +k=1 +x_0=18500000 +y_0=0 +ellps=krass +towgs84=-17.51,-108.32,-62.39 +units=m +no_defs +2045 projected Hanoi 1972 / Gauss-Kruger zone 19 108,10.43,109.53,21.56 6147 +proj=tmerc +lat_0=0 +lon_0=111 +k=1 +x_0=19500000 +y_0=0 +ellps=krass +units=m +no_defs +2046 projected Hartebeesthoek94 / Lo15 14.35,-23.15,14.6,-22.68 1 6148 +proj=tmerc +axis=wsu +lat_0=0 +lon_0=15 +k=1 +x_0=0 +y_0=0 +ellps=WGS84 +towgs84=0,0,0 +units=m +no_defs +2047 projected Hartebeesthoek94 / Lo17 16.45,-33.1,18,-28.03 1 6148 +proj=tmerc +axis=wsu +lat_0=0 +lon_0=17 +k=1 +x_0=0 +y_0=0 +ellps=WGS84 +towgs84=0,0,0 +units=m +no_defs +2048 projected Hartebeesthoek94 / Lo19 17.99,-34.88,20,-28.38 1 6148 +proj=tmerc +axis=wsu +lat_0=0 +lon_0=19 +k=1 +x_0=0 +y_0=0 +ellps=WGS84 +towgs84=0,0,0 +units=m +no_defs +2049 projected Hartebeesthoek94 / Lo21 19.99,-34.88,22.01,-24.76 1 6148 +proj=tmerc +axis=wsu +lat_0=0 +lon_0=21 +k=1 +x_0=0 +y_0=0 +ellps=WGS84 +towgs84=0,0,0 +units=m +no_defs +2050 projected Hartebeesthoek94 / Lo23 22,-34.26,24.01,-25.26 1 6148 +proj=tmerc +axis=wsu +lat_0=0 +lon_0=23 +k=1 +x_0=0 +y_0=0 +ellps=WGS84 +towgs84=0,0,0 +units=m +no_defs +2051 projected Hartebeesthoek94 / Lo25 24,-34.26,26.01,-24.71 1 6148 +proj=tmerc +axis=wsu +lat_0=0 +lon_0=25 +k=1 +x_0=0 +y_0=0 +ellps=WGS84 +towgs84=0,0,0 +units=m +no_defs +2052 projected Hartebeesthoek94 / Lo27 26,-33.83,28,-22.92 1 6148 +proj=tmerc +axis=wsu +lat_0=0 +lon_0=27 +k=1 +x_0=0 +y_0=0 +ellps=WGS84 +towgs84=0,0,0 +units=m +no_defs +2053 projected Hartebeesthoek94 / Lo29 27.99,-33.03,30.01,-22.13 1 6148 +proj=tmerc +axis=wsu +lat_0=0 +lon_0=29 +k=1 +x_0=0 +y_0=0 +ellps=WGS84 +towgs84=0,0,0 +units=m +no_defs +2054 projected Hartebeesthoek94 / Lo31 29.99,-31.38,32.02,-22.22 1 6148 +proj=tmerc +axis=wsu +lat_0=0 +lon_0=31 +k=1 +x_0=0 +y_0=0 +ellps=WGS84 +towgs84=0,0,0 +units=m +no_defs +2055 projected Hartebeesthoek94 / Lo33 31.95,-28.94,32.95,-26.8 1 6148 +proj=tmerc +axis=wsu +lat_0=0 +lon_0=33 +k=1 +x_0=0 +y_0=0 +ellps=WGS84 +towgs84=0,0,0 +units=m +no_defs +2056 projected CH1903+ / LV95 5.95,45.81,10.5,47.81 1 6150 +proj=somerc +lat_0=46.9524055555556 +lon_0=7.43958333333333 +k_0=1 +x_0=2600000 +y_0=1200000 +ellps=bessel +towgs84=674.374,15.056,405.346 +units=m +no_defs +2057 projected Rassadiran / Nakhl e Taqi 52.5,27.39,52.71,27.61 0.5 6153 +proj=omerc +lat_0=27.5188288055556 +lonc=52.6035391666667 +alpha=0.571661194444444 +gamma=0.571661194444444 +k=0.999895934 +x_0=658377.437 +y_0=3044969.194 +ellps=intl +towgs84=-133.63,-157.5,-158.62 +units=m +no_defs +2058 projected ED50(ED77) / UTM zone 38N 44.03,30.99,48,39.78 1 6154 +proj=utm +zone=38 +ellps=intl +towgs84=-110.33,-97.73,-119.85,0.3423,1.1634,0.2715,0.063 +units=m +no_defs +2059 projected ED50(ED77) / UTM zone 39N 48,25.47,54,39.71 1 6154 +proj=utm +zone=39 +ellps=intl +towgs84=-110.33,-97.73,-119.85,0.3423,1.1634,0.2715,0.063 +units=m +no_defs +2060 projected ED50(ED77) / UTM zone 40N 54,25.32,60,38.29 1 6154 +proj=utm +zone=40 +ellps=intl +towgs84=-110.33,-97.73,-119.85,0.3423,1.1634,0.2715,0.063 +units=m +no_defs +2061 projected ED50(ED77) / UTM zone 41N 60,25.02,63.34,37.06 1 6154 +proj=utm +zone=41 +ellps=intl +towgs84=-110.33,-97.73,-119.85,0.3423,1.1634,0.2715,0.063 +units=m +no_defs +2062 projected Madrid 1870 (Madrid) / Spain LCC -9.37,35.95,3.39,43.82 6903 +proj=lcc +lat_1=40 +lat_0=40 +lon_0=0 +k_0=0.9988085293 +x_0=600000 +y_0=600000 +a=6378298.3 +rf=294.73 +pm=-3.687375 +units=m +no_defs +2065 projected S-JTSK (Ferro) / Krovak 12.09,47.73,22.56,51.06 6818 +proj=krovak +axis=swu +lat_0=49.5 +lon_0=42.5 +alpha=30.2881397527778 +k=0.9999 +x_0=0 +y_0=0 +ellps=bessel +pm=-17.6666666666667 +units=m +no_defs +2066 projected Mount Dillon / Tobago Grid -60.9,11.08,-60.44,11.41 6157 +proj=cass +lat_0=11.2521786111111 +lon_0=-60.6860088888889 +x_0=37718.66159325 +y_0=36209.91512952 +a=6378293.64520876 +b=6356617.98767984 +to_meter=0.201166195164 +no_defs +2067 projected Naparima 1955 / UTM zone 20N -61.98,9.99,-60.85,10.9 1 6158 +proj=utm +zone=20 +ellps=intl +towgs84=-0.465,372.095,171.736 +units=m +no_defs +2068 projected ELD79 / Libya zone 5 9.31,25.37,10.01,30.49 5 6159 +proj=tmerc +lat_0=0 +lon_0=9 +k=0.9999 +x_0=200000 +y_0=0 +ellps=intl +towgs84=-115.8543,-99.0583,-152.4616 +units=m +no_defs +2069 projected ELD79 / Libya zone 6 10,23.51,12,33.23 5 6159 +proj=tmerc +lat_0=0 +lon_0=11 +k=0.9999 +x_0=200000 +y_0=0 +ellps=intl +towgs84=-115.8543,-99.0583,-152.4616 +units=m +no_defs +2070 projected ELD79 / Libya zone 7 12,22.8,14,33.06 5 6159 +proj=tmerc +lat_0=0 +lon_0=13 +k=0.9999 +x_0=200000 +y_0=0 +ellps=intl +towgs84=-115.8543,-99.0583,-152.4616 +units=m +no_defs +2071 projected ELD79 / Libya zone 8 14,22.61,16,32.79 5 6159 +proj=tmerc +lat_0=0 +lon_0=15 +k=0.9999 +x_0=200000 +y_0=0 +ellps=intl +towgs84=-115.8543,-99.0583,-152.4616 +units=m +no_defs +2072 projected ELD79 / Libya zone 9 16,22.51,18.01,31.34 5 6159 +proj=tmerc +lat_0=0 +lon_0=17 +k=0.9999 +x_0=200000 +y_0=0 +ellps=intl +towgs84=-115.8543,-99.0583,-152.4616 +units=m +no_defs +2073 projected ELD79 / Libya zone 10 18,21.54,20,32.17 5 6159 +proj=tmerc +lat_0=0 +lon_0=19 +k=0.9999 +x_0=200000 +y_0=0 +ellps=intl +towgs84=-115.8543,-99.0583,-152.4616 +units=m +no_defs +2074 projected ELD79 / Libya zone 11 20,20.54,22,33 5 6159 +proj=tmerc +lat_0=0 +lon_0=21 +k=0.9999 +x_0=200000 +y_0=0 +ellps=intl +towgs84=-115.8543,-99.0583,-152.4616 +units=m +no_defs +2075 projected ELD79 / Libya zone 12 22,19.5,24,32.97 5 6159 +proj=tmerc +lat_0=0 +lon_0=23 +k=0.9999 +x_0=200000 +y_0=0 +ellps=intl +towgs84=-115.8543,-99.0583,-152.4616 +units=m +no_defs +2076 projected ELD79 / Libya zone 13 24,19.99,25.21,32.15 5 6159 +proj=tmerc +lat_0=0 +lon_0=25 +k=0.9999 +x_0=200000 +y_0=0 +ellps=intl +towgs84=-115.8543,-99.0583,-152.4616 +units=m +no_defs +2077 projected ELD79 / UTM zone 32N 9.31,23.51,12,33.23 5 6159 +proj=utm +zone=32 +ellps=intl +towgs84=-115.8543,-99.0583,-152.4616 +units=m +no_defs +2078 projected ELD79 / UTM zone 33N 12,22.51,18.01,33.06 5 6159 +proj=utm +zone=33 +ellps=intl +towgs84=-115.8543,-99.0583,-152.4616 +units=m +no_defs +2079 projected ELD79 / UTM zone 34N 17.99,19.5,24,33 5 6159 +proj=utm +zone=34 +ellps=intl +towgs84=-115.8543,-99.0583,-152.4616 +units=m +no_defs +2080 projected ELD79 / UTM zone 35N 24,19.99,25.21,32.15 5 6159 +proj=utm +zone=35 +ellps=intl +towgs84=-115.8543,-99.0583,-152.4616 +units=m +no_defs +2081 projected Chos Malal 1914 / Argentina 2 -70.51,-43.41,-67.49,-31.91 2.5 6160 +proj=tmerc +lat_0=-90 +lon_0=-69 +k=1 +x_0=2500000 +y_0=0 +ellps=intl +towgs84=15.75,164.93,126.18 +units=m +no_defs +2082 projected Pampa del Castillo / Argentina 2 -70.5,-50.34,-67.49,-42.49 2.5 6161 +proj=tmerc +lat_0=-90 +lon_0=-69 +k=1 +x_0=2500000 +y_0=0 +ellps=intl +towgs84=-233.43,6.65,173.64 +units=m +no_defs +2083 projected Hito XVIII 1963 / Argentina 2 -68.64,-54.9,-67.5,-52.59 0.5 6254 +proj=tmerc +lat_0=-90 +lon_0=-69 +k=1 +x_0=2500000 +y_0=0 +ellps=intl +towgs84=18.38,192.45,96.82,0.056,-0.142,-0.2,-0.0013 +units=m +no_defs +2084 projected Hito XVIII 1963 / UTM zone 19S -68.62,-54.61,-66,-51.65 44 6254 +proj=utm +zone=19 +south +ellps=intl +towgs84=16,196,93 +units=m +no_defs +2087 projected ELD79 / TM 12 NE 9.31,22.61,15,33.23 5 6159 +proj=tmerc +lat_0=0 +lon_0=12 +k=0.9996 +x_0=500000 +y_0=0 +ellps=intl +towgs84=-115.8543,-99.0583,-152.4616 +units=m +no_defs +2088 projected Carthage / TM 11 NE 7.81,33.22,13.67,38.41 1 6223 +proj=tmerc +lat_0=0 +lon_0=11 +k=0.9996 +x_0=500000 +y_0=0 +ellps=clrk80ign +towgs84=-260.1,5.5,432.2 +units=m +no_defs +2089 projected Yemen NGN96 / UTM zone 38N 42,11.57,48.01,17.95 1 6163 +proj=utm +zone=38 +ellps=WGS84 +towgs84=0,0,0 +units=m +no_defs +2090 projected Yemen NGN96 / UTM zone 39N 48,9.45,54.01,19 1 6163 +proj=utm +zone=39 +ellps=WGS84 +towgs84=0,0,0 +units=m +no_defs +2093 projected Hanoi 1972 / GK 106 NE 105.49,9.03,107.58,11.04 5 6147 +proj=tmerc +lat_0=0 +lon_0=106 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=-17.51,-108.32,-62.39 +units=m +no_defs +2094 projected WGS 72BE / TM 106 NE 106.54,7.99,110,11.15 2 6324 +proj=tmerc +lat_0=0 +lon_0=106 +k=0.9996 +x_0=500000 +y_0=0 +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +2095 projected Bissau / UTM zone 28N -16.77,10.87,-13.64,12.69 25 6165 +proj=utm +zone=28 +ellps=intl +towgs84=-173,253,27 +units=m +no_defs +2096 projected Korean 1985 / East Belt 128,34.49,129.65,38.64 6162 +proj=tmerc +lat_0=38 +lon_0=129 +k=1 +x_0=200000 +y_0=500000 +ellps=bessel +units=m +no_defs +2097 projected Korean 1985 / Central Belt 126,33.96,128,38.33 6162 +proj=tmerc +lat_0=38 +lon_0=127 +k=1 +x_0=200000 +y_0=500000 +ellps=bessel +units=m +no_defs +2098 projected Korean 1985 / West Belt 124.53,33.99,126,38.04 6162 +proj=tmerc +lat_0=38 +lon_0=125 +k=1 +x_0=200000 +y_0=500000 +ellps=bessel +units=m +no_defs +2099 projected Qatar 1948 / Qatar Grid 50.69,24.55,51.68,26.2 6286 +proj=cass +lat_0=25.3823611111111 +lon_0=50.7613888888889 +x_0=100000 +y_0=100000 +ellps=helmert +units=m +no_defs +2100 projected GGRS87 / Greek Grid 19.57,34.88,28.3,41.75 1 6121 +proj=tmerc +lat_0=0 +lon_0=24 +k=0.9996 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=-199.87,74.79,246.62 +units=m +no_defs +2101 projected Lake / Maracaibo Grid M1 -72.25,10,-71.5,11 6249 +proj=lcc +lat_1=10.1666666666667 +lat_0=10.1666666666667 +lon_0=-71.6056177777778 +k_0=1 +x_0=0 +y_0=-52684.972 +ellps=intl +units=m +no_defs +2102 projected Lake / Maracaibo Grid -72.25,10,-71.5,11 6249 +proj=lcc +lat_1=10.1666666666667 +lat_0=10.1666666666667 +lon_0=-71.6056177777778 +k_0=1 +x_0=200000 +y_0=147315.028 +ellps=intl +units=m +no_defs +2103 projected Lake / Maracaibo Grid M3 -72.25,10,-71.5,11 6249 +proj=lcc +lat_1=10.1666666666667 +lat_0=10.1666666666667 +lon_0=-71.6056177777778 +k_0=1 +x_0=500000 +y_0=447315.028 +ellps=intl +units=m +no_defs +2104 projected Lake / Maracaibo La Rosa Grid -71.5,10,-71.17,10.51 6249 +proj=lcc +lat_1=10.1666666666667 +lat_0=10.1666666666667 +lon_0=-71.6056177777778 +k_0=1 +x_0=-17044 +y_0=-23139.97 +ellps=intl +units=m +no_defs +2105 projected NZGD2000 / Mount Eden 2000 171.99,-39.01,176.12,-34.1 1 6167 +proj=tmerc +lat_0=-36.8797222222222 +lon_0=174.764166666667 +k=0.9999 +x_0=400000 +y_0=800000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +2106 projected NZGD2000 / Bay of Plenty 2000 175.75,-39.13,177.23,-37.22 1 6167 +proj=tmerc +lat_0=-37.7611111111111 +lon_0=176.466111111111 +k=1 +x_0=400000 +y_0=800000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +2107 projected NZGD2000 / Poverty Bay 2000 176.73,-39.04,178.63,-37.49 1 6167 +proj=tmerc +lat_0=-38.6244444444444 +lon_0=177.885555555556 +k=1 +x_0=400000 +y_0=800000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +2108 projected NZGD2000 / Hawkes Bay 2000 175.8,-40.57,178.07,-38.87 1 6167 +proj=tmerc +lat_0=-39.6508333333333 +lon_0=176.673611111111 +k=1 +x_0=400000 +y_0=800000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +2109 projected NZGD2000 / Taranaki 2000 173.68,-39.78,175.44,-38.4 1 6167 +proj=tmerc +lat_0=-39.1355555555556 +lon_0=174.227777777778 +k=1 +x_0=400000 +y_0=800000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +2110 projected NZGD2000 / Tuhirangi 2000 174.88,-39.55,176.33,-38.87 1 6167 +proj=tmerc +lat_0=-39.5122222222222 +lon_0=175.64 +k=1 +x_0=400000 +y_0=800000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +2111 projected NZGD2000 / Wanganui 2000 174.4,-40.97,176.27,-39.46 1 6167 +proj=tmerc +lat_0=-40.2419444444444 +lon_0=175.488055555556 +k=1 +x_0=400000 +y_0=800000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +2112 projected NZGD2000 / Wairarapa 2000 175.01,-41.67,176.55,-40.29 1 6167 +proj=tmerc +lat_0=-40.9252777777778 +lon_0=175.647222222222 +k=1 +x_0=400000 +y_0=800000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +2113 projected NZGD2000 / Wellington 2000 174.52,-41.5,175.36,-40.91 1 6167 +proj=tmerc +lat_0=-41.3011111111111 +lon_0=174.776388888889 +k=1 +x_0=400000 +y_0=800000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +2114 projected NZGD2000 / Collingwood 2000 172.16,-41.22,173.13,-40.44 1 6167 +proj=tmerc +lat_0=-40.7147222222222 +lon_0=172.671944444444 +k=1 +x_0=400000 +y_0=800000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +2115 projected NZGD2000 / Nelson 2000 172.4,-42.18,174.08,-40.66 1 6167 +proj=tmerc +lat_0=-41.2744444444444 +lon_0=173.299166666667 +k=1 +x_0=400000 +y_0=800000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +2116 projected NZGD2000 / Karamea 2000 171.96,-41.49,172.7,-40.75 1 6167 +proj=tmerc +lat_0=-41.2897222222222 +lon_0=172.108888888889 +k=1 +x_0=400000 +y_0=800000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +2117 projected NZGD2000 / Buller 2000 171.27,-42.19,172.41,-41.42 1 6167 +proj=tmerc +lat_0=-41.8105555555556 +lon_0=171.581111111111 +k=1 +x_0=400000 +y_0=800000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +2118 projected NZGD2000 / Grey 2000 171.15,-42.74,172.75,-41.5 1 6167 +proj=tmerc +lat_0=-42.3336111111111 +lon_0=171.549722222222 +k=1 +x_0=400000 +y_0=800000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +2119 projected NZGD2000 / Amuri 2000 171.88,-42.95,173.55,-42.09 1 6167 +proj=tmerc +lat_0=-42.6888888888889 +lon_0=173.01 +k=1 +x_0=400000 +y_0=800000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +2120 projected NZGD2000 / Marlborough 2000 172.95,-42.65,174.46,-40.85 1 6167 +proj=tmerc +lat_0=-41.5444444444444 +lon_0=173.801944444444 +k=1 +x_0=400000 +y_0=800000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +2121 projected NZGD2000 / Hokitika 2000 170.39,-43.23,171.89,-42.41 1 6167 +proj=tmerc +lat_0=-42.8861111111111 +lon_0=170.979722222222 +k=1 +x_0=400000 +y_0=800000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +2122 projected NZGD2000 / Okarito 2000 169.21,-43.85,170.89,-43 1 6167 +proj=tmerc +lat_0=-43.11 +lon_0=170.260833333333 +k=1 +x_0=400000 +y_0=800000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +2123 projected NZGD2000 / Jacksons Bay 2000 168.02,-44.4,170.01,-43.67 1 6167 +proj=tmerc +lat_0=-43.9777777777778 +lon_0=168.606111111111 +k=1 +x_0=400000 +y_0=800000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +2124 projected NZGD2000 / Mount Pleasant 2000 171.11,-43.96,173.38,-42.69 1 6167 +proj=tmerc +lat_0=-43.5905555555556 +lon_0=172.726944444444 +k=1 +x_0=400000 +y_0=800000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +2125 projected NZGD2000 / Gawler 2000 170.68,-44.25,172.26,-43.13 1 6167 +proj=tmerc +lat_0=-43.7486111111111 +lon_0=171.360555555556 +k=1 +x_0=400000 +y_0=800000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +2126 projected NZGD2000 / Timaru 2000 169.82,-44.98,171.55,-43.35 1 6167 +proj=tmerc +lat_0=-44.4019444444444 +lon_0=171.057222222222 +k=1 +x_0=400000 +y_0=800000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +2127 projected NZGD2000 / Lindis Peak 2000 168.62,-45.4,170.24,-43.71 1 6167 +proj=tmerc +lat_0=-44.735 +lon_0=169.4675 +k=1 +x_0=400000 +y_0=800000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +2128 projected NZGD2000 / Mount Nicholas 2000 167.72,-45.58,169.11,-44.29 1 6167 +proj=tmerc +lat_0=-45.1327777777778 +lon_0=168.398611111111 +k=1 +x_0=400000 +y_0=800000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +2129 projected NZGD2000 / Mount York 2000 166.37,-46.33,168.21,-44.53 1 6167 +proj=tmerc +lat_0=-45.5636111111111 +lon_0=167.738611111111 +k=1 +x_0=400000 +y_0=800000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +2130 projected NZGD2000 / Observation Point 2000 169.77,-45.82,171.24,-44.61 1 6167 +proj=tmerc +lat_0=-45.8161111111111 +lon_0=170.628333333333 +k=1 +x_0=400000 +y_0=800000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +2131 projected NZGD2000 / North Taieri 2000 168.64,-46.73,170.87,-45.23 1 6167 +proj=tmerc +lat_0=-45.8613888888889 +lon_0=170.2825 +k=0.99996 +x_0=400000 +y_0=800000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +2132 projected NZGD2000 / Bluff 2000 167.29,-47.33,168.97,-45.33 1 6167 +proj=tmerc +lat_0=-46.6 +lon_0=168.342777777778 +k=1 +x_0=400000 +y_0=800000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +2133 projected NZGD2000 / UTM zone 58S 162,-55.89,168,-39.68 1 6167 +proj=utm +zone=58 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +2134 projected NZGD2000 / UTM zone 59S 168,-55.95,174,-30.78 1 6167 +proj=utm +zone=59 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +2135 projected NZGD2000 / UTM zone 60S 174,-54.32,180,-26.42 1 6167 +proj=utm +zone=60 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +2136 projected Accra / Ghana National Grid -3.25,4.67,1.23,11.16 6 6168 +proj=tmerc +lat_0=4.66666666666667 +lon_0=-1 +k=0.99975 +x_0=274319.739163358 +y_0=0 +a=6378300 +rf=296 +towgs84=-170,33,326 +to_meter=0.304799710181509 +no_defs +2137 projected Accra / TM 1 NW -3.79,1.4,2.1,6.06 25 6168 +proj=tmerc +lat_0=0 +lon_0=-1 +k=0.9996 +x_0=500000 +y_0=0 +a=6378300 +rf=296 +towgs84=-171.16,17.29,325.21,0,0,0.814,-0.38 +units=m +no_defs +2138 projected NAD27(CGQ77) / Quebec Lambert -79.85,44.99,-57.1,62.62 6609 +proj=lcc +lat_0=44 +lon_0=-68.5 +lat_1=60 +lat_2=46 +x_0=0 +y_0=0 +ellps=clrk66 +units=m +no_defs +2154 projected RGF93 v1 / Lambert-93 -9.86,41.15,10.38,51.56 1 6171 +proj=lcc +lat_0=46.5 +lon_0=3 +lat_1=49 +lat_2=44 +x_0=700000 +y_0=6600000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +2157 projected IRENET95 / Irish Transverse Mercator -10.56,51.39,-5.34,55.43 1 6173 +proj=tmerc +lat_0=53.5 +lon_0=-8 +k=0.99982 +x_0=600000 +y_0=750000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +2158 projected IRENET95 / UTM zone 29N -10.56,51.39,-5.34,55.43 1 6173 +proj=utm +zone=29 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +2159 projected Sierra Leone 1924 / New Colony Grid -13.34,8.32,-13.13,8.55 6174 +proj=tmerc +lat_0=6.66666666666667 +lon_0=-12 +k=1 +x_0=152399.855090754 +y_0=0 +a=6378300 +rf=296 +to_meter=0.304799710181509 +no_defs +2160 projected Sierra Leone 1924 / New War Office Grid -13.34,8.32,-13.13,8.55 6174 +proj=tmerc +lat_0=6.66666666666667 +lon_0=-12 +k=1 +x_0=243839.768145207 +y_0=182879.826108905 +a=6378300 +rf=296 +to_meter=0.304799710181509 +no_defs +2161 projected Sierra Leone 1968 / UTM zone 28N -13.35,7.15,-12,9.94 26 6175 +proj=utm +zone=28 +a=6378249.145 +rf=293.465 +towgs84=-88,4,101 +units=m +no_defs +2162 projected Sierra Leone 1968 / UTM zone 29N -12,6.88,-10.26,10 26 6175 +proj=utm +zone=29 +a=6378249.145 +rf=293.465 +towgs84=-88,4,101 +units=m +no_defs +2164 projected Locodjo 1965 / TM 5 NW -7.55,1.02,-3.11,5.19 15 6142 +proj=tmerc +lat_0=0 +lon_0=-5 +k=0.9996 +x_0=500000 +y_0=0 +a=6378249.145 +rf=293.465 +towgs84=-125,53,467 +units=m +no_defs +2165 projected Abidjan 1987 / TM 5 NW -7.55,1.02,-3.11,5.19 2 6143 +proj=tmerc +lat_0=0 +lon_0=-5 +k=0.9996 +x_0=500000 +y_0=0 +a=6378249.145 +rf=293.465 +towgs84=-123.1,53.2,465.4 +units=m +no_defs +2169 projected LUREF / Luxembourg TM 5.73,49.44,6.53,50.19 1 6181 +proj=tmerc +lat_0=49.8333333333333 +lon_0=6.16666666666667 +k=1 +x_0=80000 +y_0=100000 +ellps=intl +towgs84=-189.6806,18.3463,-42.7695,-0.33746,-3.09264,2.53861,0.4598 +units=m +no_defs +2172 projected Pulkovo 1942(58) / Poland zone II 19,51.33,23.95,54.51 1 6179 +proj=sterea +lat_0=53.0019444444444 +lon_0=21.5027777777778 +k=0.9998 +x_0=4603000 +y_0=5806000 +ellps=krass +towgs84=33.4,-146.6,-76.3,-0.359,-0.053,0.844,-0.84 +units=m +no_defs +2173 projected Pulkovo 1942(58) / Poland zone III 14.14,52.16,20,54.89 1 6179 +proj=sterea +lat_0=53.5833333333333 +lon_0=17.0083333333333 +k=0.9998 +x_0=3501000 +y_0=5999000 +ellps=krass +towgs84=33.4,-146.6,-76.3,-0.359,-0.053,0.844,-0.84 +units=m +no_defs +2174 projected Pulkovo 1942(58) / Poland zone IV 14.14,49.39,19.09,53.34 1 6179 +proj=sterea +lat_0=51.6708333333333 +lon_0=16.6722222222222 +k=0.9998 +x_0=3703000 +y_0=5627000 +ellps=krass +towgs84=33.4,-146.6,-76.3,-0.359,-0.053,0.844,-0.84 +units=m +no_defs +2175 projected Pulkovo 1942(58) / Poland zone V 18.33,49.39,19.67,51.34 1 6179 +proj=tmerc +lat_0=0 +lon_0=18.9583333333333 +k=0.999983 +x_0=237000 +y_0=-4700000 +ellps=krass +towgs84=33.4,-146.6,-76.3,-0.359,-0.053,0.844,-0.84 +units=m +no_defs +2176 projected ETRF2000-PL / CS2000/15 14.14,50.26,16.5,55.35 1 1305 +proj=tmerc +lat_0=0 +lon_0=15 +k=0.999923 +x_0=5500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +2177 projected ETRF2000-PL / CS2000/18 16.5,49.39,19.5,55.93 1 1305 +proj=tmerc +lat_0=0 +lon_0=18 +k=0.999923 +x_0=6500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +2178 projected ETRF2000-PL / CS2000/21 19.5,49.09,22.5,54.55 1 1305 +proj=tmerc +lat_0=0 +lon_0=21 +k=0.999923 +x_0=7500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +2179 projected ETRF2000-PL / CS2000/24 22.5,49,24.15,54.41 1 1305 +proj=tmerc +lat_0=0 +lon_0=24 +k=0.999923 +x_0=8500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +2180 projected ETRF2000-PL / CS92 14.14,49,24.15,55.93 1 1305 +proj=tmerc +lat_0=0 +lon_0=19 +k=0.9993 +x_0=500000 +y_0=-5300000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +2188 projected Azores Occidental 1939 / UTM zone 25N -31.34,39.3,-31.02,39.77 5 6182 +proj=utm +zone=25 +ellps=intl +towgs84=-422.651,-172.995,84.02 +units=m +no_defs +2189 projected Azores Central 1948 / UTM zone 26N -28.9,38.32,-26.97,39.14 6 6183 +proj=utm +zone=26 +ellps=intl +towgs84=-104,167,-38 +units=m +no_defs +2190 projected Azores Oriental 1940 / UTM zone 26N -25.92,36.87,-24.72,37.96 44 6184 +proj=utm +zone=26 +ellps=intl +towgs84=-203,141,53 +units=m +no_defs +2193 projected NZGD2000 / New Zealand Transverse Mercator 2000 166.37,-47.33,178.63,-34.1 1 6167 +proj=tmerc +lat_0=0 +lon_0=173 +k=0.9996 +x_0=1600000 +y_0=10000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +2195 projected NAD83(HARN) / UTM zone 2S -170.88,-14.59,-168.09,-14.11 2 6152 +proj=utm +zone=2 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +2196 projected ETRS89 / Kp2000 Jutland 8,54.67,11.29,57.8 1 6258 +proj=tmerc +lat_0=0 +lon_0=9.5 +k=0.99995 +x_0=200000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +2197 projected ETRS89 / Kp2000 Zealand 10.79,54.51,12.87,56.79 1 6258 +proj=tmerc +lat_0=0 +lon_0=12 +k=0.99995 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +2198 projected ETRS89 / Kp2000 Bornholm 14.59,54.94,15.25,55.38 1 6258 +proj=tmerc +lat_0=0 +lon_0=15 +k=1 +x_0=900000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +2200 projected ATS77 / New Brunswick Stereographic (ATS77) -69.05,44.56,-63.7,48.07 6122 +proj=sterea +lat_0=46.5 +lon_0=-66.5 +k=0.999912 +x_0=300000 +y_0=800000 +a=6378135 +rf=298.257 +units=m +no_defs +2201 projected REGVEN / UTM zone 18N -73.38,7.02,-71.99,11.62 1 6189 +proj=utm +zone=18 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +2202 projected REGVEN / UTM zone 19N -72,0.73,-66,15.64 1 6189 +proj=utm +zone=19 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +2203 projected REGVEN / UTM zone 20N -66,0.64,-58.95,16.75 1 6189 +proj=utm +zone=20 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +2204 projected NAD27 / Tennessee -90.31,34.98,-81.65,36.68 10 6267 +proj=lcc +lat_0=34.6666666666667 +lon_0=-86 +lat_1=35.25 +lat_2=36.4166666666667 +x_0=609601.219202438 +y_0=30480.0609601219 +ellps=clrk66 +towgs84=-8,160,176 +to_meter=0.304800609601219 +no_defs +2205 projected NAD83 / Kentucky North -85.96,37.71,-82.47,39.15 4 6269 +proj=lcc +lat_0=37.5 +lon_0=-84.25 +lat_1=37.9666666666667 +lat_2=38.9666666666667 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +2206 projected ED50 / 3-degree Gauss-Kruger zone 9 25.62,36.5,28.5,42.11 2 6230 +proj=tmerc +lat_0=0 +lon_0=27 +k=1 +x_0=9500000 +y_0=0 +ellps=intl +towgs84=-84.1,-101.8,-129.7,0,0,0.468,1.05 +units=m +no_defs +2207 projected ED50 / 3-degree Gauss-Kruger zone 10 28.5,36.06,31.5,41.46 2 6230 +proj=tmerc +lat_0=0 +lon_0=30 +k=1 +x_0=10500000 +y_0=0 +ellps=intl +towgs84=-84.1,-101.8,-129.7,0,0,0.468,1.05 +units=m +no_defs +2208 projected ED50 / 3-degree Gauss-Kruger zone 11 31.5,35.97,34.5,42.07 2 6230 +proj=tmerc +lat_0=0 +lon_0=33 +k=1 +x_0=11500000 +y_0=0 +ellps=intl +towgs84=-84.1,-101.8,-129.7,0,0,0.468,1.05 +units=m +no_defs +2209 projected ED50 / 3-degree Gauss-Kruger zone 12 34.5,35.81,37.5,42.15 2 6230 +proj=tmerc +lat_0=0 +lon_0=36 +k=1 +x_0=12500000 +y_0=0 +ellps=intl +towgs84=-84.1,-101.8,-129.7,0,0,0.468,1.05 +units=m +no_defs +2210 projected ED50 / 3-degree Gauss-Kruger zone 13 37.5,36.66,40.5,41.19 2 6230 +proj=tmerc +lat_0=0 +lon_0=39 +k=1 +x_0=13500000 +y_0=0 +ellps=intl +towgs84=-84.1,-101.8,-129.7,0,0,0.468,1.05 +units=m +no_defs +2211 projected ED50 / 3-degree Gauss-Kruger zone 14 40.5,37.02,43.5,41.6 2 6230 +proj=tmerc +lat_0=0 +lon_0=42 +k=1 +x_0=14500000 +y_0=0 +ellps=intl +towgs84=-84.1,-101.8,-129.7,0,0,0.468,1.05 +units=m +no_defs +2212 projected ED50 / 3-degree Gauss-Kruger zone 15 43.5,36.97,44.83,41.02 2 6230 +proj=tmerc +lat_0=0 +lon_0=45 +k=1 +x_0=15500000 +y_0=0 +ellps=intl +towgs84=-84.1,-101.8,-129.7,0,0,0.468,1.05 +units=m +no_defs +2213 projected ETRS89 / TM 30 NE 28.64,43.44,31.41,45.2 1 6258 +proj=tmerc +lat_0=0 +lon_0=30 +k=0.9996 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +2215 projected Manoca 1962 / UTM zone 32N 8.45,2.16,10.4,4.99 0.5 6193 +proj=utm +zone=32 +ellps=clrk80ign +towgs84=-70.9,-151.8,-41.4 +units=m +no_defs +2216 projected Qoornoq 1927 / UTM zone 22N -54,60.63,-48,73.05 1 6194 +proj=utm +zone=22 +ellps=intl +towgs84=163.511,127.533,-159.789,0,0,0.814,-0.6 +units=m +no_defs +2217 projected Qoornoq 1927 / UTM zone 23N -48,59.74,-42.52,62.05 1 6194 +proj=utm +zone=23 +ellps=intl +towgs84=163.511,127.533,-159.789,0,0,0.814,-0.6 +units=m +no_defs +2218 projected Scoresbysund 1952 / Greenland zone 5 east -29.69,69,-21.32,72 1 6195 +proj=lcc +axis=wnu +lat_1=70.5 +lat_0=70.5 +lon_0=-24 +k_0=1 +x_0=0 +y_0=0 +ellps=intl +towgs84=105,326,-102.5,0,0,0.814,-0.6 +units=m +no_defs +2219 projected ATS77 / UTM zone 19N -69,43.64,-66,48.07 6122 +proj=utm +zone=19 +a=6378135 +rf=298.257 +units=m +no_defs +2220 projected ATS77 / UTM zone 20N -66,43.41,-59.73,47.98 6122 +proj=utm +zone=20 +a=6378135 +rf=298.257 +units=m +no_defs +2221 projected Scoresbysund 1952 / Greenland zone 6 east -26.99,68.66,-25.14,69 1 6195 +proj=lcc +axis=wnu +lat_1=67.5 +lat_0=67.5 +lon_0=-32 +k_0=1 +x_0=0 +y_0=0 +ellps=intl +towgs84=105,326,-102.5,0,0,0.814,-0.6 +units=m +no_defs +2222 projected NAD83 / Arizona East (ft) -111.71,31.33,-109.04,37.01 4 6269 +proj=tmerc +lat_0=31 +lon_0=-110.166666666667 +k=0.9999 +x_0=213360 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +2223 projected NAD83 / Arizona Central (ft) -113.35,31.33,-110.44,37.01 4 6269 +proj=tmerc +lat_0=31 +lon_0=-111.916666666667 +k=0.9999 +x_0=213360 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +2224 projected NAD83 / Arizona West (ft) -114.81,32.05,-112.52,37 4 6269 +proj=tmerc +lat_0=31 +lon_0=-113.75 +k=0.999933333 +x_0=213360 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +2225 projected NAD83 / California zone 1 (ftUS) -124.45,39.59,-119.99,42.01 4 6269 +proj=lcc +lat_0=39.3333333333333 +lon_0=-122 +lat_1=41.6666666666667 +lat_2=40 +x_0=2000000.0001016 +y_0=500000.0001016 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +2226 projected NAD83 / California zone 2 (ftUS) -124.06,38.02,-119.54,40.16 4 6269 +proj=lcc +lat_0=37.6666666666667 +lon_0=-122 +lat_1=39.8333333333333 +lat_2=38.3333333333333 +x_0=2000000.0001016 +y_0=500000.0001016 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +2227 projected NAD83 / California zone 3 (ftUS) -123.02,36.73,-117.83,38.71 4 6269 +proj=lcc +lat_0=36.5 +lon_0=-120.5 +lat_1=38.4333333333333 +lat_2=37.0666666666667 +x_0=2000000.0001016 +y_0=500000.0001016 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +2228 projected NAD83 / California zone 4 (ftUS) -122.01,35.78,-115.62,37.58 4 6269 +proj=lcc +lat_0=35.3333333333333 +lon_0=-119 +lat_1=37.25 +lat_2=36 +x_0=2000000.0001016 +y_0=500000.0001016 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +2229 projected NAD83 / California zone 5 (ftUS) -121.42,32.76,-114.12,35.81 4 6269 +proj=lcc +lat_0=33.5 +lon_0=-118 +lat_1=35.4666666666667 +lat_2=34.0333333333333 +x_0=2000000.0001016 +y_0=500000.0001016 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +2230 projected NAD83 / California zone 6 (ftUS) -118.15,32.53,-114.42,34.08 4 6269 +proj=lcc +lat_0=32.1666666666667 +lon_0=-116.25 +lat_1=33.8833333333333 +lat_2=32.7833333333333 +x_0=2000000.0001016 +y_0=500000.0001016 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +2231 projected NAD83 / Colorado North (ftUS) -109.06,39.56,-102.04,41.01 4 6269 +proj=lcc +lat_0=39.3333333333333 +lon_0=-105.5 +lat_1=40.7833333333333 +lat_2=39.7166666666667 +x_0=914401.828803658 +y_0=304800.609601219 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +2232 projected NAD83 / Colorado Central (ftUS) -109.06,38.14,-102.04,40.09 4 6269 +proj=lcc +lat_0=37.8333333333333 +lon_0=-105.5 +lat_1=39.75 +lat_2=38.45 +x_0=914401.828803658 +y_0=304800.609601219 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +2233 projected NAD83 / Colorado South (ftUS) -109.06,36.98,-102.04,38.68 4 6269 +proj=lcc +lat_0=36.6666666666667 +lon_0=-105.5 +lat_1=38.4333333333333 +lat_2=37.2333333333333 +x_0=914401.828803658 +y_0=304800.609601219 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +2234 projected NAD83 / Connecticut (ftUS) -73.73,40.98,-71.78,42.05 4 6269 +proj=lcc +lat_0=40.8333333333333 +lon_0=-72.75 +lat_1=41.8666666666667 +lat_2=41.2 +x_0=304800.609601219 +y_0=152400.30480061 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +2235 projected NAD83 / Delaware (ftUS) -75.8,38.44,-74.97,39.85 4 6269 +proj=tmerc +lat_0=38 +lon_0=-75.4166666666667 +k=0.999995 +x_0=200000.0001016 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +2236 projected NAD83 / Florida East (ftUS) -82.33,24.41,-79.97,30.83 4 6269 +proj=tmerc +lat_0=24.3333333333333 +lon_0=-81 +k=0.999941177 +x_0=200000.0001016 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +2237 projected NAD83 / Florida West (ftUS) -83.34,26.27,-81.13,29.6 4 6269 +proj=tmerc +lat_0=24.3333333333333 +lon_0=-82 +k=0.999941177 +x_0=200000.0001016 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +2238 projected NAD83 / Florida North (ftUS) -87.63,29.21,-82.04,31.01 4 6269 +proj=lcc +lat_0=29 +lon_0=-84.5 +lat_1=30.75 +lat_2=29.5833333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +2239 projected NAD83 / Georgia East (ftUS) -83.47,30.36,-80.77,34.68 4 6269 +proj=tmerc +lat_0=30 +lon_0=-82.1666666666667 +k=0.9999 +x_0=200000.0001016 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +2240 projected NAD83 / Georgia West (ftUS) -85.61,30.62,-82.99,35.01 4 6269 +proj=tmerc +lat_0=30 +lon_0=-84.1666666666667 +k=0.9999 +x_0=699999.9998984 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +2241 projected NAD83 / Idaho East (ftUS) -113.24,41.99,-111.04,44.75 4 6269 +proj=tmerc +lat_0=41.6666666666667 +lon_0=-112.166666666667 +k=0.999947368 +x_0=200000.0001016 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +2242 projected NAD83 / Idaho Central (ftUS) -115.3,41.99,-112.67,45.7 4 6269 +proj=tmerc +lat_0=41.6666666666667 +lon_0=-114 +k=0.999947368 +x_0=500000.0001016 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +2243 projected NAD83 / Idaho West (ftUS) -117.24,41.99,-114.32,49.01 4 6269 +proj=tmerc +lat_0=41.6666666666667 +lon_0=-115.75 +k=0.999933333 +x_0=800000.0001016 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +2246 projected NAD83 / Kentucky North (ftUS) -85.96,37.71,-82.47,39.15 4 6269 +proj=lcc +lat_0=37.5 +lon_0=-84.25 +lat_1=37.9666666666667 +lat_2=38.9666666666667 +x_0=500000.0001016 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +2247 projected NAD83 / Kentucky South (ftUS) -89.57,36.49,-81.95,38.17 4 6269 +proj=lcc +lat_0=36.3333333333333 +lon_0=-85.75 +lat_1=37.9333333333333 +lat_2=36.7333333333333 +x_0=500000.0001016 +y_0=500000.0001016 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +2248 projected NAD83 / Maryland (ftUS) -79.49,37.97,-74.97,39.73 4 6269 +proj=lcc +lat_0=37.6666666666667 +lon_0=-77 +lat_1=39.45 +lat_2=38.3 +x_0=399999.9998984 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +2249 projected NAD83 / Massachusetts Mainland (ftUS) -73.5,41.46,-69.86,42.89 4 6269 +proj=lcc +lat_0=41 +lon_0=-71.5 +lat_1=42.6833333333333 +lat_2=41.7166666666667 +x_0=200000.0001016 +y_0=750000 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +2250 projected NAD83 / Massachusetts Island (ftUS) -70.91,41.19,-69.89,41.51 4 6269 +proj=lcc +lat_0=41 +lon_0=-70.5 +lat_1=41.4833333333333 +lat_2=41.2833333333333 +x_0=500000.0001016 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +2251 projected NAD83 / Michigan North (ft) -90.42,45.08,-83.44,48.32 4 6269 +proj=lcc +lat_0=44.7833333333333 +lon_0=-87 +lat_1=47.0833333333333 +lat_2=45.4833333333333 +x_0=7999999.999968 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +2252 projected NAD83 / Michigan Central (ft) -87.06,43.8,-82.27,45.92 4 6269 +proj=lcc +lat_0=43.3166666666667 +lon_0=-84.3666666666667 +lat_1=45.7 +lat_2=44.1833333333333 +x_0=5999999.999976 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +2253 projected NAD83 / Michigan South (ft) -87.2,41.69,-82.13,44.22 4 6269 +proj=lcc +lat_0=41.5 +lon_0=-84.3666666666667 +lat_1=43.6666666666667 +lat_2=42.1 +x_0=3999999.999984 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +2254 projected NAD83 / Mississippi East (ftUS) -89.97,30.01,-88.09,35.01 4 6269 +proj=tmerc +lat_0=29.5 +lon_0=-88.8333333333333 +k=0.99995 +x_0=300000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +2255 projected NAD83 / Mississippi West (ftUS) -91.65,31,-89.37,35.01 4 6269 +proj=tmerc +lat_0=29.5 +lon_0=-90.3333333333333 +k=0.99995 +x_0=699999.9998984 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +2256 projected NAD83 / Montana (ft) -116.07,44.35,-104.04,49.01 4 6269 +proj=lcc +lat_0=44.25 +lon_0=-109.5 +lat_1=49 +lat_2=45 +x_0=599999.9999976 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +2257 projected NAD83 / New Mexico East (ftUS) -105.72,32,-102.99,37 4 6269 +proj=tmerc +lat_0=31 +lon_0=-104.333333333333 +k=0.999909091 +x_0=165000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +2258 projected NAD83 / New Mexico Central (ftUS) -107.73,31.78,-104.84,37 4 6269 +proj=tmerc +lat_0=31 +lon_0=-106.25 +k=0.9999 +x_0=500000.0001016 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +2259 projected NAD83 / New Mexico West (ftUS) -109.06,31.33,-106.32,37 4 6269 +proj=tmerc +lat_0=31 +lon_0=-107.833333333333 +k=0.999916667 +x_0=830000.0001016 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +2260 projected NAD83 / New York East (ftUS) -75.87,40.88,-73.23,45.02 4 6269 +proj=tmerc +lat_0=38.8333333333333 +lon_0=-74.5 +k=0.9999 +x_0=150000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +2261 projected NAD83 / New York Central (ftUS) -77.75,41.99,-75.06,44.41 4 6269 +proj=tmerc +lat_0=40 +lon_0=-76.5833333333333 +k=0.9999375 +x_0=249999.9998984 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +2262 projected NAD83 / New York West (ftUS) -79.77,41.99,-77.36,43.64 4 6269 +proj=tmerc +lat_0=40 +lon_0=-78.5833333333333 +k=0.9999375 +x_0=350000.0001016 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +2263 projected NAD83 / New York Long Island (ftUS) -74.26,40.47,-71.8,41.3 4 6269 +proj=lcc +lat_0=40.1666666666667 +lon_0=-74 +lat_1=41.0333333333333 +lat_2=40.6666666666667 +x_0=300000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +2264 projected NAD83 / North Carolina (ftUS) -84.33,33.83,-75.38,36.59 4 6269 +proj=lcc +lat_0=33.75 +lon_0=-79 +lat_1=36.1666666666667 +lat_2=34.3333333333333 +x_0=609601.219202438 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +2265 projected NAD83 / North Dakota North (ft) -104.07,47.15,-96.83,49.01 4 6269 +proj=lcc +lat_0=47 +lon_0=-100.5 +lat_1=48.7333333333333 +lat_2=47.4333333333333 +x_0=599999.9999976 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +2266 projected NAD83 / North Dakota South (ft) -104.05,45.93,-96.55,47.83 4 6269 +proj=lcc +lat_0=45.6666666666667 +lon_0=-100.5 +lat_1=47.4833333333333 +lat_2=46.1833333333333 +x_0=599999.9999976 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +2267 projected NAD83 / Oklahoma North (ftUS) -103,35.27,-94.42,37.01 4 6269 +proj=lcc +lat_0=35 +lon_0=-98 +lat_1=36.7666666666667 +lat_2=35.5666666666667 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +2268 projected NAD83 / Oklahoma South (ftUS) -100,33.62,-94.42,35.57 4 6269 +proj=lcc +lat_0=33.3333333333333 +lon_0=-98 +lat_1=35.2333333333333 +lat_2=33.9333333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +2269 projected NAD83 / Oregon North (ft) -124.17,43.95,-116.47,46.26 4 6269 +proj=lcc +lat_0=43.6666666666667 +lon_0=-120.5 +lat_1=46 +lat_2=44.3333333333333 +x_0=2500000.0001424 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +2270 projected NAD83 / Oregon South (ft) -124.6,41.98,-116.9,44.56 4 6269 +proj=lcc +lat_0=41.6666666666667 +lon_0=-120.5 +lat_1=44 +lat_2=42.3333333333333 +x_0=1500000.0001464 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +2271 projected NAD83 / Pennsylvania North (ftUS) -80.53,40.6,-74.7,42.53 4 6269 +proj=lcc +lat_0=40.1666666666667 +lon_0=-77.75 +lat_1=41.95 +lat_2=40.8833333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +2272 projected NAD83 / Pennsylvania South (ftUS) -80.53,39.71,-74.72,41.18 4 6269 +proj=lcc +lat_0=39.3333333333333 +lon_0=-77.75 +lat_1=40.9666666666667 +lat_2=39.9333333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +2273 projected NAD83 / South Carolina (ft) -83.36,32.05,-78.52,35.21 4 6269 +proj=lcc +lat_0=31.8333333333333 +lon_0=-81 +lat_1=34.8333333333333 +lat_2=32.5 +x_0=609600 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +2274 projected NAD83 / Tennessee (ftUS) -90.31,34.98,-81.65,36.68 4 6269 +proj=lcc +lat_0=34.3333333333333 +lon_0=-86 +lat_1=36.4166666666667 +lat_2=35.25 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +2275 projected NAD83 / Texas North (ftUS) -103.03,34.3,-99.99,36.5 4 6269 +proj=lcc +lat_0=34 +lon_0=-101.5 +lat_1=36.1833333333333 +lat_2=34.65 +x_0=200000.0001016 +y_0=999999.9998984 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +2276 projected NAD83 / Texas North Central (ftUS) -103.07,31.72,-94,34.58 4 6269 +proj=lcc +lat_0=31.6666666666667 +lon_0=-98.5 +lat_1=33.9666666666667 +lat_2=32.1333333333333 +x_0=600000 +y_0=2000000.0001016 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +2277 projected NAD83 / Texas Central (ftUS) -106.66,29.78,-93.5,32.27 4 6269 +proj=lcc +lat_0=29.6666666666667 +lon_0=-100.333333333333 +lat_1=31.8833333333333 +lat_2=30.1166666666667 +x_0=699999.9998984 +y_0=3000000 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +2278 projected NAD83 / Texas South Central (ftUS) -105,27.78,-93.76,30.67 4 6269 +proj=lcc +lat_0=27.8333333333333 +lon_0=-99 +lat_1=30.2833333333333 +lat_2=28.3833333333333 +x_0=600000 +y_0=3999999.9998984 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +2279 projected NAD83 / Texas South (ftUS) -100.2,25.83,-96.85,28.21 4 6269 +proj=lcc +lat_0=25.6666666666667 +lon_0=-98.5 +lat_1=27.8333333333333 +lat_2=26.1666666666667 +x_0=300000 +y_0=5000000.0001016 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +2280 projected NAD83 / Utah North (ft) -114.04,40.55,-109.04,42.01 4 6269 +proj=lcc +lat_0=40.3333333333333 +lon_0=-111.5 +lat_1=41.7833333333333 +lat_2=40.7166666666667 +x_0=500000.0001504 +y_0=999999.999996 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +2281 projected NAD83 / Utah Central (ft) -114.05,38.49,-109.04,41.08 4 6269 +proj=lcc +lat_0=38.3333333333333 +lon_0=-111.5 +lat_1=40.65 +lat_2=39.0166666666667 +x_0=500000.0001504 +y_0=1999999.999992 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +2282 projected NAD83 / Utah South (ft) -114.05,36.99,-109.04,38.58 4 6269 +proj=lcc +lat_0=36.6666666666667 +lon_0=-111.5 +lat_1=38.35 +lat_2=37.2166666666667 +x_0=500000.0001504 +y_0=2999999.999988 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +2283 projected NAD83 / Virginia North (ftUS) -80.06,37.77,-76.51,39.46 4 6269 +proj=lcc +lat_0=37.6666666666667 +lon_0=-78.5 +lat_1=39.2 +lat_2=38.0333333333333 +x_0=3500000.0001016 +y_0=2000000.0001016 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +2284 projected NAD83 / Virginia South (ftUS) -83.68,36.54,-75.31,38.28 4 6269 +proj=lcc +lat_0=36.3333333333333 +lon_0=-78.5 +lat_1=37.9666666666667 +lat_2=36.7666666666667 +x_0=3500000.0001016 +y_0=999999.9998984 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +2285 projected NAD83 / Washington North (ftUS) -124.79,47.08,-117.02,49.05 4 6269 +proj=lcc +lat_0=47 +lon_0=-120.833333333333 +lat_1=48.7333333333333 +lat_2=47.5 +x_0=500000.0001016 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +2286 projected NAD83 / Washington South (ftUS) -124.4,45.54,-116.91,47.61 4 6269 +proj=lcc +lat_0=45.3333333333333 +lon_0=-120.5 +lat_1=47.3333333333333 +lat_2=45.8333333333333 +x_0=500000.0001016 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +2287 projected NAD83 / Wisconsin North (ftUS) -92.89,45.37,-88.05,47.31 4 6269 +proj=lcc +lat_0=45.1666666666667 +lon_0=-90 +lat_1=46.7666666666667 +lat_2=45.5666666666667 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +2288 projected NAD83 / Wisconsin Central (ftUS) -92.89,43.98,-86.25,45.8 4 6269 +proj=lcc +lat_0=43.8333333333333 +lon_0=-90 +lat_1=45.5 +lat_2=44.25 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +2289 projected NAD83 / Wisconsin South (ftUS) -91.43,42.48,-86.95,44.33 4 6269 +proj=lcc +lat_0=42 +lon_0=-90 +lat_1=44.0666666666667 +lat_2=42.7333333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +2290 projected ATS77 / Prince Edward Isl. Stereographic (ATS77) -64.49,45.9,-61.9,47.09 6122 +proj=sterea +lat_0=47.25 +lon_0=-63 +k=0.999912 +x_0=700000 +y_0=400000 +a=6378135 +rf=298.257 +units=m +no_defs +2294 projected ATS77 / MTM Nova Scotia zone 4 -63,44.64,-59.73,47.08 6122 +proj=tmerc +lat_0=0 +lon_0=-61.5 +k=0.9999 +x_0=4500000 +y_0=0 +a=6378135 +rf=298.257 +units=m +no_defs +2295 projected ATS77 / MTM Nova Scotia zone 5 -66.28,43.41,-63,46.02 6122 +proj=tmerc +lat_0=0 +lon_0=-64.5 +k=0.9999 +x_0=5500000 +y_0=0 +a=6378135 +rf=298.257 +units=m +no_defs +2296 projected Ammassalik 1958 / Greenland zone 7 east -38.86,65.52,-36.81,65.91 1 6196 +proj=lcc +axis=wnu +lat_1=64.5 +lat_0=64.5 +lon_0=-40 +k_0=1 +x_0=0 +y_0=0 +ellps=intl +towgs84=-45,417,-3.5,0,0,0.814,-0.6 +units=m +no_defs +2299 projected Qoornoq 1927 / Greenland zone 2 west -73.29,78,-65.24,79 1 6194 +proj=lcc +axis=wnu +lat_1=79.5 +lat_0=79.5 +lon_0=-64 +k_0=1 +x_0=0 +y_0=0 +ellps=intl +towgs84=163.511,127.533,-159.789,0,0,0.814,-0.6 +units=m +no_defs +2301 projected Qoornoq 1927 / Greenland zone 3 west -72.79,75,-56.31,78 1 6194 +proj=lcc +axis=wnu +lat_1=76.5 +lat_0=76.5 +lon_0=-64 +k_0=1 +x_0=0 +y_0=0 +ellps=intl +towgs84=163.511,127.533,-159.789,0,0,0.814,-0.6 +units=m +no_defs +2303 projected Qoornoq 1927 / Greenland zone 4 west -58.21,72,-52.02,75 1 6194 +proj=lcc +axis=wnu +lat_1=73.5 +lat_0=73.5 +lon_0=-52 +k_0=1 +x_0=0 +y_0=0 +ellps=intl +towgs84=163.511,127.533,-159.789,0,0,0.814,-0.6 +units=m +no_defs +2304 projected Qoornoq 1927 / Greenland zone 5 west -56.06,69,-49.11,72 1 6194 +proj=lcc +axis=wnu +lat_1=70.5 +lat_0=70.5 +lon_0=-52 +k_0=1 +x_0=0 +y_0=0 +ellps=intl +towgs84=163.511,127.533,-159.789,0,0,0.814,-0.6 +units=m +no_defs +2305 projected Qoornoq 1927 / Greenland zone 6 west -54.09,66,-49.4,69 1 6194 +proj=lcc +axis=wnu +lat_1=67.5 +lat_0=67.5 +lon_0=-52 +k_0=1 +x_0=0 +y_0=0 +ellps=intl +towgs84=163.511,127.533,-159.789,0,0,0.814,-0.6 +units=m +no_defs +2306 projected Qoornoq 1927 / Greenland zone 7 west -53.7,63,-49.17,66 1 6194 +proj=lcc +axis=wnu +lat_1=64.5 +lat_0=64.5 +lon_0=-52 +k_0=1 +x_0=0 +y_0=0 +ellps=intl +towgs84=163.511,127.533,-159.789,0,0,0.814,-0.6 +units=m +no_defs +2307 projected Qoornoq 1927 / Greenland zone 8 -50.72,59.74,-42.52,63 1 6194 +proj=lcc +axis=wnu +lat_1=61.5 +lat_0=61.5 +lon_0=-48 +k_0=1 +x_0=0 +y_0=0 +ellps=intl +towgs84=163.511,127.533,-159.789,0,0,0.814,-0.6 +units=m +no_defs +2308 projected Batavia / TM 109 SE 105.77,-6.89,110.01,-4.07 5 6211 +proj=tmerc +lat_0=0 +lon_0=109 +k=0.9996 +x_0=500000 +y_0=10000000 +ellps=bessel +towgs84=-378.873,676.002,-46.255 +units=m +no_defs +2309 projected WGS 84 / TM 116 SE 112.8,-8.46,117.01,-6.8 6326 +proj=tmerc +lat_0=0 +lon_0=116 +k=0.9996 +x_0=500000 +y_0=10000000 +datum=WGS84 +units=m +no_defs +2310 projected WGS 84 / TM 132 SE 131.89,-2.94,133.82,-1.97 6326 +proj=tmerc +lat_0=0 +lon_0=132 +k=0.9996 +x_0=500000 +y_0=10000000 +datum=WGS84 +units=m +no_defs +2311 projected WGS 84 / TM 6 NE 2.66,1.92,8.49,6.38 6326 +proj=tmerc +lat_0=0 +lon_0=6 +k=0.9996 +x_0=500000 +y_0=0 +datum=WGS84 +units=m +no_defs +2312 projected Garoua / UTM zone 33N 12.9,8.92,14.19,9.87 6197 +proj=utm +zone=33 +a=6378249.145 +rf=293.465 +units=m +no_defs +2313 projected Kousseri / UTM zone 33N 14.17,11.7,15.09,12.77 6198 +proj=utm +zone=33 +a=6378249.145 +rf=293.465 +units=m +no_defs +2314 projected Trinidad 1903 / Trinidad Grid (ftCla) -62.09,9.83,-60,11.51 2 6302 +proj=cass +lat_0=10.4416666666667 +lon_0=-61.3333333333333 +x_0=86501.46392052 +y_0=65379.0134283 +a=6378293.64520876 +b=6356617.98767984 +towgs84=-61.702,284.488,472.052 +to_meter=0.3047972654 +no_defs +2315 projected Campo Inchauspe / UTM zone 19S -68.62,-54.61,-66,-51.65 9 6221 +proj=utm +zone=19 +south +ellps=intl +towgs84=-148,136,90 +units=m +no_defs +2316 projected Campo Inchauspe / UTM zone 20S -66,-54.93,-61.49,-51.36 9 6221 +proj=utm +zone=20 +south +ellps=intl +towgs84=-148,136,90 +units=m +no_defs +2317 projected PSAD56 / ICN Regional -73.38,0.64,-59.8,12.25 23 6248 +proj=lcc +lat_0=6 +lon_0=-66 +lat_1=9 +lat_2=3 +x_0=1000000 +y_0=1000000 +ellps=intl +towgs84=-295,173,-371 +units=m +no_defs +2318 projected Ain el Abd / Aramco Lambert 34.51,16.37,55.67,32.16 18 6204 +proj=lcc +lat_0=25.08951 +lon_0=48 +lat_1=17 +lat_2=33 +x_0=0 +y_0=0 +ellps=intl +towgs84=-143,-236,7 +units=m +no_defs +2319 projected ED50 / TM27 25.62,36.5,28.5,42.11 2 6230 +proj=tmerc +lat_0=0 +lon_0=27 +k=1 +x_0=500000 +y_0=0 +ellps=intl +towgs84=-84.1,-101.8,-129.7,0,0,0.468,1.05 +units=m +no_defs +2320 projected ED50 / TM30 28.5,36.06,31.5,41.46 2 6230 +proj=tmerc +lat_0=0 +lon_0=30 +k=1 +x_0=500000 +y_0=0 +ellps=intl +towgs84=-84.1,-101.8,-129.7,0,0,0.468,1.05 +units=m +no_defs +2321 projected ED50 / TM33 31.5,35.97,34.5,42.07 2 6230 +proj=tmerc +lat_0=0 +lon_0=33 +k=1 +x_0=500000 +y_0=0 +ellps=intl +towgs84=-84.1,-101.8,-129.7,0,0,0.468,1.05 +units=m +no_defs +2322 projected ED50 / TM36 34.5,35.81,37.5,42.15 2 6230 +proj=tmerc +lat_0=0 +lon_0=36 +k=1 +x_0=500000 +y_0=0 +ellps=intl +towgs84=-84.1,-101.8,-129.7,0,0,0.468,1.05 +units=m +no_defs +2323 projected ED50 / TM39 37.5,36.66,40.5,41.19 2 6230 +proj=tmerc +lat_0=0 +lon_0=39 +k=1 +x_0=500000 +y_0=0 +ellps=intl +towgs84=-84.1,-101.8,-129.7,0,0,0.468,1.05 +units=m +no_defs +2324 projected ED50 / TM42 40.5,37.02,43.5,41.6 2 6230 +proj=tmerc +lat_0=0 +lon_0=42 +k=1 +x_0=500000 +y_0=0 +ellps=intl +towgs84=-84.1,-101.8,-129.7,0,0,0.468,1.05 +units=m +no_defs +2325 projected ED50 / TM45 43.5,36.97,44.83,41.02 2 6230 +proj=tmerc +lat_0=0 +lon_0=45 +k=1 +x_0=500000 +y_0=0 +ellps=intl +towgs84=-84.1,-101.8,-129.7,0,0,0.468,1.05 +units=m +no_defs +2326 projected Hong Kong 1980 Grid System 113.76,22.13,114.51,22.58 1 6611 +proj=tmerc +lat_0=22.3121333333333 +lon_0=114.178555555556 +k=1 +x_0=836694.05 +y_0=819069.8 +ellps=intl +towgs84=-162.619,-276.959,-161.764,0.067753,-2.243648,-1.158828,-1.094246 +units=m +no_defs +2327 projected Xian 1980 / Gauss-Kruger zone 13 73.62,35.42,78.01,41.07 6610 +proj=tmerc +lat_0=0 +lon_0=75 +k=1 +x_0=13500000 +y_0=0 +ellps=IAU76 +units=m +no_defs +2328 projected Xian 1980 / Gauss-Kruger zone 14 77.98,29.16,84,47.23 6610 +proj=tmerc +lat_0=0 +lon_0=81 +k=1 +x_0=14500000 +y_0=0 +ellps=IAU76 +units=m +no_defs +2329 projected Xian 1980 / Gauss-Kruger zone 15 84,27.32,90,49.18 6610 +proj=tmerc +lat_0=0 +lon_0=87 +k=1 +x_0=15500000 +y_0=0 +ellps=IAU76 +units=m +no_defs +2330 projected Xian 1980 / Gauss-Kruger zone 16 90,27.71,96.01,47.9 6610 +proj=tmerc +lat_0=0 +lon_0=93 +k=1 +x_0=16500000 +y_0=0 +ellps=IAU76 +units=m +no_defs +2331 projected Xian 1980 / Gauss-Kruger zone 17 96,21.13,102.01,43.18 6610 +proj=tmerc +lat_0=0 +lon_0=99 +k=1 +x_0=17500000 +y_0=0 +ellps=IAU76 +units=m +no_defs +2332 projected Xian 1980 / Gauss-Kruger zone 18 102,21.53,108,42.47 6610 +proj=tmerc +lat_0=0 +lon_0=105 +k=1 +x_0=18500000 +y_0=0 +ellps=IAU76 +units=m +no_defs +2333 projected Xian 1980 / Gauss-Kruger zone 19 108,18.11,114,45.11 6610 +proj=tmerc +lat_0=0 +lon_0=111 +k=1 +x_0=19500000 +y_0=0 +ellps=IAU76 +units=m +no_defs +2334 projected Xian 1980 / Gauss-Kruger zone 20 114,22.14,120,51.52 6610 +proj=tmerc +lat_0=0 +lon_0=117 +k=1 +x_0=20500000 +y_0=0 +ellps=IAU76 +units=m +no_defs +2335 projected Xian 1980 / Gauss-Kruger zone 21 120,26.34,126,53.56 6610 +proj=tmerc +lat_0=0 +lon_0=123 +k=1 +x_0=21500000 +y_0=0 +ellps=IAU76 +units=m +no_defs +2336 projected Xian 1980 / Gauss-Kruger zone 22 126,40.89,132,52.79 6610 +proj=tmerc +lat_0=0 +lon_0=129 +k=1 +x_0=22500000 +y_0=0 +ellps=IAU76 +units=m +no_defs +2337 projected Xian 1980 / Gauss-Kruger zone 23 132,45.02,134.77,48.4 6610 +proj=tmerc +lat_0=0 +lon_0=135 +k=1 +x_0=23500000 +y_0=0 +ellps=IAU76 +units=m +no_defs +2338 projected Xian 1980 / Gauss-Kruger CM 75E 73.62,35.42,78.01,41.07 6610 +proj=tmerc +lat_0=0 +lon_0=75 +k=1 +x_0=500000 +y_0=0 +ellps=IAU76 +units=m +no_defs +2339 projected Xian 1980 / Gauss-Kruger CM 81E 77.98,29.16,84,47.23 6610 +proj=tmerc +lat_0=0 +lon_0=81 +k=1 +x_0=500000 +y_0=0 +ellps=IAU76 +units=m +no_defs +2340 projected Xian 1980 / Gauss-Kruger CM 87E 84,27.32,90,49.18 6610 +proj=tmerc +lat_0=0 +lon_0=87 +k=1 +x_0=500000 +y_0=0 +ellps=IAU76 +units=m +no_defs +2341 projected Xian 1980 / Gauss-Kruger CM 93E 90,27.71,96.01,47.9 6610 +proj=tmerc +lat_0=0 +lon_0=93 +k=1 +x_0=500000 +y_0=0 +ellps=IAU76 +units=m +no_defs +2342 projected Xian 1980 / Gauss-Kruger CM 99E 96,21.13,102.01,43.18 6610 +proj=tmerc +lat_0=0 +lon_0=99 +k=1 +x_0=500000 +y_0=0 +ellps=IAU76 +units=m +no_defs +2343 projected Xian 1980 / Gauss-Kruger CM 105E 102,21.53,108,42.47 6610 +proj=tmerc +lat_0=0 +lon_0=105 +k=1 +x_0=500000 +y_0=0 +ellps=IAU76 +units=m +no_defs +2344 projected Xian 1980 / Gauss-Kruger CM 111E 108,18.11,114,45.11 6610 +proj=tmerc +lat_0=0 +lon_0=111 +k=1 +x_0=500000 +y_0=0 +ellps=IAU76 +units=m +no_defs +2345 projected Xian 1980 / Gauss-Kruger CM 117E 114,22.14,120,51.52 6610 +proj=tmerc +lat_0=0 +lon_0=117 +k=1 +x_0=500000 +y_0=0 +ellps=IAU76 +units=m +no_defs +2346 projected Xian 1980 / Gauss-Kruger CM 123E 120,26.34,126,53.56 6610 +proj=tmerc +lat_0=0 +lon_0=123 +k=1 +x_0=500000 +y_0=0 +ellps=IAU76 +units=m +no_defs +2347 projected Xian 1980 / Gauss-Kruger CM 129E 126,40.89,132,52.79 6610 +proj=tmerc +lat_0=0 +lon_0=129 +k=1 +x_0=500000 +y_0=0 +ellps=IAU76 +units=m +no_defs +2348 projected Xian 1980 / Gauss-Kruger CM 135E 132,45.02,134.77,48.4 6610 +proj=tmerc +lat_0=0 +lon_0=135 +k=1 +x_0=500000 +y_0=0 +ellps=IAU76 +units=m +no_defs +2349 projected Xian 1980 / 3-degree Gauss-Kruger zone 25 73.62,35.81,76.5,40.65 6610 +proj=tmerc +lat_0=0 +lon_0=75 +k=1 +x_0=25500000 +y_0=0 +ellps=IAU76 +units=m +no_defs +2350 projected Xian 1980 / 3-degree Gauss-Kruger zone 26 76.5,31.03,79.5,41.83 6610 +proj=tmerc +lat_0=0 +lon_0=78 +k=1 +x_0=26500000 +y_0=0 +ellps=IAU76 +units=m +no_defs +2351 projected Xian 1980 / 3-degree Gauss-Kruger zone 27 79.5,29.95,82.51,45.88 6610 +proj=tmerc +lat_0=0 +lon_0=81 +k=1 +x_0=27500000 +y_0=0 +ellps=IAU76 +units=m +no_defs +2352 projected Xian 1980 / 3-degree Gauss-Kruger zone 28 82.5,28.26,85.5,47.23 6610 +proj=tmerc +lat_0=0 +lon_0=84 +k=1 +x_0=28500000 +y_0=0 +ellps=IAU76 +units=m +no_defs +2353 projected Xian 1980 / 3-degree Gauss-Kruger zone 29 85.5,27.8,88.5,49.18 6610 +proj=tmerc +lat_0=0 +lon_0=87 +k=1 +x_0=29500000 +y_0=0 +ellps=IAU76 +units=m +no_defs +2354 projected Xian 1980 / 3-degree Gauss-Kruger zone 30 88.49,27.32,91.51,48.42 6610 +proj=tmerc +lat_0=0 +lon_0=90 +k=1 +x_0=30500000 +y_0=0 +ellps=IAU76 +units=m +no_defs +2355 projected Xian 1980 / 3-degree Gauss-Kruger zone 31 91.5,27.71,94.5,45.13 6610 +proj=tmerc +lat_0=0 +lon_0=93 +k=1 +x_0=31500000 +y_0=0 +ellps=IAU76 +units=m +no_defs +2356 projected Xian 1980 / 3-degree Gauss-Kruger zone 32 94.5,28.23,97.51,44.5 6610 +proj=tmerc +lat_0=0 +lon_0=96 +k=1 +x_0=32500000 +y_0=0 +ellps=IAU76 +units=m +no_defs +2357 projected Xian 1980 / 3-degree Gauss-Kruger zone 33 97.5,21.43,100.5,42.76 6610 +proj=tmerc +lat_0=0 +lon_0=99 +k=1 +x_0=33500000 +y_0=0 +ellps=IAU76 +units=m +no_defs +2358 projected Xian 1980 / 3-degree Gauss-Kruger zone 34 100.5,21.13,103.5,42.69 6610 +proj=tmerc +lat_0=0 +lon_0=102 +k=1 +x_0=34500000 +y_0=0 +ellps=IAU76 +units=m +no_defs +2359 projected Xian 1980 / 3-degree Gauss-Kruger zone 35 103.5,22.5,106.51,42.21 6610 +proj=tmerc +lat_0=0 +lon_0=105 +k=1 +x_0=35500000 +y_0=0 +ellps=IAU76 +units=m +no_defs +2360 projected Xian 1980 / 3-degree Gauss-Kruger zone 36 106.5,18.19,109.51,42.47 6610 +proj=tmerc +lat_0=0 +lon_0=108 +k=1 +x_0=36500000 +y_0=0 +ellps=IAU76 +units=m +no_defs +2361 projected Xian 1980 / 3-degree Gauss-Kruger zone 37 109.5,18.11,112.5,45.11 6610 +proj=tmerc +lat_0=0 +lon_0=111 +k=1 +x_0=37500000 +y_0=0 +ellps=IAU76 +units=m +no_defs +2362 projected Xian 1980 / 3-degree Gauss-Kruger zone 38 112.5,21.52,115.5,45.45 6610 +proj=tmerc +lat_0=0 +lon_0=114 +k=1 +x_0=38500000 +y_0=0 +ellps=IAU76 +units=m +no_defs +2363 projected Xian 1980 / 3-degree Gauss-Kruger zone 39 115.5,22.6,118.5,49.88 6610 +proj=tmerc +lat_0=0 +lon_0=117 +k=1 +x_0=39500000 +y_0=0 +ellps=IAU76 +units=m +no_defs +2364 projected Xian 1980 / 3-degree Gauss-Kruger zone 40 118.5,24.43,121.5,53.33 6610 +proj=tmerc +lat_0=0 +lon_0=120 +k=1 +x_0=40500000 +y_0=0 +ellps=IAU76 +units=m +no_defs +2365 projected Xian 1980 / 3-degree Gauss-Kruger zone 41 121.5,28.22,124.5,53.56 6610 +proj=tmerc +lat_0=0 +lon_0=123 +k=1 +x_0=41500000 +y_0=0 +ellps=IAU76 +units=m +no_defs +2366 projected Xian 1980 / 3-degree Gauss-Kruger zone 42 124.5,40.19,127.5,53.2 6610 +proj=tmerc +lat_0=0 +lon_0=126 +k=1 +x_0=42500000 +y_0=0 +ellps=IAU76 +units=m +no_defs +2367 projected Xian 1980 / 3-degree Gauss-Kruger zone 43 127.5,41.37,130.5,50.25 6610 +proj=tmerc +lat_0=0 +lon_0=129 +k=1 +x_0=43500000 +y_0=0 +ellps=IAU76 +units=m +no_defs +2368 projected Xian 1980 / 3-degree Gauss-Kruger zone 44 130.5,42.42,133.5,48.88 6610 +proj=tmerc +lat_0=0 +lon_0=132 +k=1 +x_0=44500000 +y_0=0 +ellps=IAU76 +units=m +no_defs +2369 projected Xian 1980 / 3-degree Gauss-Kruger zone 45 133.5,45.85,134.77,48.4 6610 +proj=tmerc +lat_0=0 +lon_0=135 +k=1 +x_0=45500000 +y_0=0 +ellps=IAU76 +units=m +no_defs +2370 projected Xian 1980 / 3-degree Gauss-Kruger CM 75E 73.62,35.81,76.5,40.65 6610 +proj=tmerc +lat_0=0 +lon_0=75 +k=1 +x_0=500000 +y_0=0 +ellps=IAU76 +units=m +no_defs +2371 projected Xian 1980 / 3-degree Gauss-Kruger CM 78E 76.5,31.03,79.5,41.83 6610 +proj=tmerc +lat_0=0 +lon_0=78 +k=1 +x_0=500000 +y_0=0 +ellps=IAU76 +units=m +no_defs +2372 projected Xian 1980 / 3-degree Gauss-Kruger CM 81E 79.5,29.95,82.51,45.88 6610 +proj=tmerc +lat_0=0 +lon_0=81 +k=1 +x_0=500000 +y_0=0 +ellps=IAU76 +units=m +no_defs +2373 projected Xian 1980 / 3-degree Gauss-Kruger CM 84E 82.5,28.26,85.5,47.23 6610 +proj=tmerc +lat_0=0 +lon_0=84 +k=1 +x_0=500000 +y_0=0 +ellps=IAU76 +units=m +no_defs +2374 projected Xian 1980 / 3-degree Gauss-Kruger CM 87E 85.5,27.8,88.5,49.18 6610 +proj=tmerc +lat_0=0 +lon_0=87 +k=1 +x_0=500000 +y_0=0 +ellps=IAU76 +units=m +no_defs +2375 projected Xian 1980 / 3-degree Gauss-Kruger CM 90E 88.49,27.32,91.51,48.42 6610 +proj=tmerc +lat_0=0 +lon_0=90 +k=1 +x_0=500000 +y_0=0 +ellps=IAU76 +units=m +no_defs +2376 projected Xian 1980 / 3-degree Gauss-Kruger CM 93E 91.5,27.71,94.5,45.13 6610 +proj=tmerc +lat_0=0 +lon_0=93 +k=1 +x_0=500000 +y_0=0 +ellps=IAU76 +units=m +no_defs +2377 projected Xian 1980 / 3-degree Gauss-Kruger CM 96E 94.5,28.23,97.51,44.5 6610 +proj=tmerc +lat_0=0 +lon_0=96 +k=1 +x_0=500000 +y_0=0 +ellps=IAU76 +units=m +no_defs +2378 projected Xian 1980 / 3-degree Gauss-Kruger CM 99E 97.5,21.43,100.5,42.76 6610 +proj=tmerc +lat_0=0 +lon_0=99 +k=1 +x_0=500000 +y_0=0 +ellps=IAU76 +units=m +no_defs +2379 projected Xian 1980 / 3-degree Gauss-Kruger CM 102E 100.5,21.13,103.5,42.69 6610 +proj=tmerc +lat_0=0 +lon_0=102 +k=1 +x_0=500000 +y_0=0 +ellps=IAU76 +units=m +no_defs +2380 projected Xian 1980 / 3-degree Gauss-Kruger CM 105E 103.5,22.5,106.51,42.21 6610 +proj=tmerc +lat_0=0 +lon_0=105 +k=1 +x_0=500000 +y_0=0 +ellps=IAU76 +units=m +no_defs +2381 projected Xian 1980 / 3-degree Gauss-Kruger CM 108E 106.5,18.19,109.51,42.47 6610 +proj=tmerc +lat_0=0 +lon_0=108 +k=1 +x_0=500000 +y_0=0 +ellps=IAU76 +units=m +no_defs +2382 projected Xian 1980 / 3-degree Gauss-Kruger CM 111E 109.5,18.11,112.5,45.11 6610 +proj=tmerc +lat_0=0 +lon_0=111 +k=1 +x_0=500000 +y_0=0 +ellps=IAU76 +units=m +no_defs +2383 projected Xian 1980 / 3-degree Gauss-Kruger CM 114E 112.5,21.52,115.5,45.45 6610 +proj=tmerc +lat_0=0 +lon_0=114 +k=1 +x_0=500000 +y_0=0 +ellps=IAU76 +units=m +no_defs +2384 projected Xian 1980 / 3-degree Gauss-Kruger CM 117E 115.5,22.6,118.5,49.88 6610 +proj=tmerc +lat_0=0 +lon_0=117 +k=1 +x_0=500000 +y_0=0 +ellps=IAU76 +units=m +no_defs +2385 projected Xian 1980 / 3-degree Gauss-Kruger CM 120E 118.5,24.43,121.5,53.33 6610 +proj=tmerc +lat_0=0 +lon_0=120 +k=1 +x_0=500000 +y_0=0 +ellps=IAU76 +units=m +no_defs +2386 projected Xian 1980 / 3-degree Gauss-Kruger CM 123E 121.5,28.22,124.5,53.56 6610 +proj=tmerc +lat_0=0 +lon_0=123 +k=1 +x_0=500000 +y_0=0 +ellps=IAU76 +units=m +no_defs +2387 projected Xian 1980 / 3-degree Gauss-Kruger CM 126E 124.5,40.19,127.5,53.2 6610 +proj=tmerc +lat_0=0 +lon_0=126 +k=1 +x_0=500000 +y_0=0 +ellps=IAU76 +units=m +no_defs +2388 projected Xian 1980 / 3-degree Gauss-Kruger CM 129E 127.5,41.37,130.5,50.25 6610 +proj=tmerc +lat_0=0 +lon_0=129 +k=1 +x_0=500000 +y_0=0 +ellps=IAU76 +units=m +no_defs +2389 projected Xian 1980 / 3-degree Gauss-Kruger CM 132E 130.5,42.42,133.5,48.88 6610 +proj=tmerc +lat_0=0 +lon_0=132 +k=1 +x_0=500000 +y_0=0 +ellps=IAU76 +units=m +no_defs +2390 projected Xian 1980 / 3-degree Gauss-Kruger CM 135E 133.5,45.85,134.77,48.4 6610 +proj=tmerc +lat_0=0 +lon_0=135 +k=1 +x_0=500000 +y_0=0 +ellps=IAU76 +units=m +no_defs +2391 projected KKJ / Finland zone 1 19.5,59.76,22.5,69.33 1 6123 +proj=tmerc +lat_0=0 +lon_0=21 +k=1 +x_0=1500000 +y_0=0 +ellps=intl +towgs84=-96.062,-82.428,-121.753,4.801,0.345,-1.376,1.496 +units=m +no_defs +2392 projected KKJ / Finland zone 2 22.5,59.75,25.5,68.9 1 6123 +proj=tmerc +lat_0=0 +lon_0=24 +k=1 +x_0=2500000 +y_0=0 +ellps=intl +towgs84=-96.062,-82.428,-121.753,4.801,0.345,-1.376,1.496 +units=m +no_defs +2393 projected KKJ / Finland Uniform Coordinate System 25.5,60.18,28.51,70.09 1 6123 +proj=tmerc +lat_0=0 +lon_0=27 +k=1 +x_0=3500000 +y_0=0 +ellps=intl +towgs84=-96.062,-82.428,-121.753,4.801,0.345,-1.376,1.496 +units=m +no_defs +2394 projected KKJ / Finland zone 4 28.5,60.94,31.5,69.81 1 6123 +proj=tmerc +lat_0=0 +lon_0=30 +k=1 +x_0=4500000 +y_0=0 +ellps=intl +towgs84=-96.062,-82.428,-121.753,4.801,0.345,-1.376,1.496 +units=m +no_defs +2395 projected South Yemen / Gauss-Kruger zone 8 43.37,12.54,48.01,17.95 5 6164 +proj=tmerc +lat_0=0 +lon_0=45 +k=1 +x_0=8500000 +y_0=0 +ellps=krass +towgs84=-76,-138,67 +units=m +no_defs +2396 projected South Yemen / Gauss-Kruger zone 9 48,13.94,53.14,19 5 6164 +proj=tmerc +lat_0=0 +lon_0=51 +k=1 +x_0=9500000 +y_0=0 +ellps=krass +towgs84=-76,-138,67 +units=m +no_defs +2397 projected Pulkovo 1942(83) / 3-degree Gauss-Kruger zone 3 9.92,50.35,10.5,51.56 2 6178 +proj=tmerc +lat_0=0 +lon_0=9 +k=1 +x_0=3500000 +y_0=0 +ellps=krass +towgs84=24,-123,-94,0.02,-0.25,-0.13,1.1 +units=m +no_defs +2398 projected Pulkovo 1942(83) / 3-degree Gauss-Kruger zone 4 10.5,48.97,13.5,54.74 2 6178 +proj=tmerc +lat_0=0 +lon_0=12 +k=1 +x_0=4500000 +y_0=0 +ellps=krass +towgs84=24,-123,-94,0.02,-0.25,-0.13,1.1 +units=m +no_defs +2399 projected Pulkovo 1942(83) / 3-degree Gauss-Kruger zone 5 13.5,46.54,16.5,54.72 5 6178 +proj=tmerc +lat_0=0 +lon_0=15 +k=1 +x_0=5500000 +y_0=0 +ellps=krass +towgs84=26,-121,-78 +units=m +no_defs +2401 projected Beijing 1954 / 3-degree Gauss-Kruger zone 25 73.62,35.81,76.5,40.65 6214 +proj=tmerc +lat_0=0 +lon_0=75 +k=1 +x_0=25500000 +y_0=0 +ellps=krass +units=m +no_defs +2402 projected Beijing 1954 / 3-degree Gauss-Kruger zone 26 76.5,31.03,79.5,41.83 1 6214 +proj=tmerc +lat_0=0 +lon_0=78 +k=1 +x_0=26500000 +y_0=0 +ellps=krass +towgs84=15.8,-154.4,-82.3 +units=m +no_defs +2403 projected Beijing 1954 / 3-degree Gauss-Kruger zone 27 79.5,29.95,82.51,45.88 1 6214 +proj=tmerc +lat_0=0 +lon_0=81 +k=1 +x_0=27500000 +y_0=0 +ellps=krass +towgs84=15.8,-154.4,-82.3 +units=m +no_defs +2404 projected Beijing 1954 / 3-degree Gauss-Kruger zone 28 82.5,28.26,85.5,47.23 1 6214 +proj=tmerc +lat_0=0 +lon_0=84 +k=1 +x_0=28500000 +y_0=0 +ellps=krass +towgs84=15.8,-154.4,-82.3 +units=m +no_defs +2405 projected Beijing 1954 / 3-degree Gauss-Kruger zone 29 85.5,27.8,88.5,49.18 1 6214 +proj=tmerc +lat_0=0 +lon_0=87 +k=1 +x_0=29500000 +y_0=0 +ellps=krass +towgs84=15.8,-154.4,-82.3 +units=m +no_defs +2406 projected Beijing 1954 / 3-degree Gauss-Kruger zone 30 88.49,27.32,91.51,48.42 6214 +proj=tmerc +lat_0=0 +lon_0=90 +k=1 +x_0=30500000 +y_0=0 +ellps=krass +units=m +no_defs +2407 projected Beijing 1954 / 3-degree Gauss-Kruger zone 31 91.5,27.71,94.5,45.13 6214 +proj=tmerc +lat_0=0 +lon_0=93 +k=1 +x_0=31500000 +y_0=0 +ellps=krass +units=m +no_defs +2408 projected Beijing 1954 / 3-degree Gauss-Kruger zone 32 94.5,28.23,97.51,44.5 6214 +proj=tmerc +lat_0=0 +lon_0=96 +k=1 +x_0=32500000 +y_0=0 +ellps=krass +units=m +no_defs +2409 projected Beijing 1954 / 3-degree Gauss-Kruger zone 33 97.5,21.43,100.5,42.76 6214 +proj=tmerc +lat_0=0 +lon_0=99 +k=1 +x_0=33500000 +y_0=0 +ellps=krass +units=m +no_defs +2410 projected Beijing 1954 / 3-degree Gauss-Kruger zone 34 100.5,21.13,103.5,42.69 6214 +proj=tmerc +lat_0=0 +lon_0=102 +k=1 +x_0=34500000 +y_0=0 +ellps=krass +units=m +no_defs +2411 projected Beijing 1954 / 3-degree Gauss-Kruger zone 35 103.5,22.5,106.51,42.21 6214 +proj=tmerc +lat_0=0 +lon_0=105 +k=1 +x_0=35500000 +y_0=0 +ellps=krass +units=m +no_defs +2412 projected Beijing 1954 / 3-degree Gauss-Kruger zone 36 106.5,18.19,109.51,42.47 1 6214 +proj=tmerc +lat_0=0 +lon_0=108 +k=1 +x_0=36500000 +y_0=0 +ellps=krass +towgs84=11.911,-154.833,-80.079 +units=m +no_defs +2413 projected Beijing 1954 / 3-degree Gauss-Kruger zone 37 109.5,18.11,112.5,45.11 15 6214 +proj=tmerc +lat_0=0 +lon_0=111 +k=1 +x_0=37500000 +y_0=0 +ellps=krass +towgs84=31.4,-144.3,-74.8,0,0,0.814,-0.38 +units=m +no_defs +2414 projected Beijing 1954 / 3-degree Gauss-Kruger zone 38 112.5,21.52,115.5,45.45 15 6214 +proj=tmerc +lat_0=0 +lon_0=114 +k=1 +x_0=38500000 +y_0=0 +ellps=krass +towgs84=31.4,-144.3,-74.8,0,0,0.814,-0.38 +units=m +no_defs +2415 projected Beijing 1954 / 3-degree Gauss-Kruger zone 39 115.5,22.6,118.5,49.88 15 6214 +proj=tmerc +lat_0=0 +lon_0=117 +k=1 +x_0=39500000 +y_0=0 +ellps=krass +towgs84=31.4,-144.3,-74.8,0,0,0.814,-0.38 +units=m +no_defs +2416 projected Beijing 1954 / 3-degree Gauss-Kruger zone 40 118.5,24.43,121.5,53.33 15 6214 +proj=tmerc +lat_0=0 +lon_0=120 +k=1 +x_0=40500000 +y_0=0 +ellps=krass +towgs84=15.53,-113.82,-41.38,0,0,0.814,-0.38 +units=m +no_defs +2417 projected Beijing 1954 / 3-degree Gauss-Kruger zone 41 121.5,28.22,124.5,53.56 15 6214 +proj=tmerc +lat_0=0 +lon_0=123 +k=1 +x_0=41500000 +y_0=0 +ellps=krass +towgs84=15.53,-113.82,-41.38,0,0,0.814,-0.38 +units=m +no_defs +2418 projected Beijing 1954 / 3-degree Gauss-Kruger zone 42 124.5,40.19,127.5,53.2 6214 +proj=tmerc +lat_0=0 +lon_0=126 +k=1 +x_0=42500000 +y_0=0 +ellps=krass +units=m +no_defs +2419 projected Beijing 1954 / 3-degree Gauss-Kruger zone 43 127.5,41.37,130.5,50.25 6214 +proj=tmerc +lat_0=0 +lon_0=129 +k=1 +x_0=43500000 +y_0=0 +ellps=krass +units=m +no_defs +2420 projected Beijing 1954 / 3-degree Gauss-Kruger zone 44 130.5,42.42,133.5,48.88 6214 +proj=tmerc +lat_0=0 +lon_0=132 +k=1 +x_0=44500000 +y_0=0 +ellps=krass +units=m +no_defs +2421 projected Beijing 1954 / 3-degree Gauss-Kruger zone 45 133.5,45.85,134.77,48.4 6214 +proj=tmerc +lat_0=0 +lon_0=135 +k=1 +x_0=45500000 +y_0=0 +ellps=krass +units=m +no_defs +2422 projected Beijing 1954 / 3-degree Gauss-Kruger CM 75E 73.62,35.81,76.5,40.65 6214 +proj=tmerc +lat_0=0 +lon_0=75 +k=1 +x_0=500000 +y_0=0 +ellps=krass +units=m +no_defs +2423 projected Beijing 1954 / 3-degree Gauss-Kruger CM 78E 76.5,31.03,79.5,41.83 1 6214 +proj=tmerc +lat_0=0 +lon_0=78 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=15.8,-154.4,-82.3 +units=m +no_defs +2424 projected Beijing 1954 / 3-degree Gauss-Kruger CM 81E 79.5,29.95,82.51,45.88 1 6214 +proj=tmerc +lat_0=0 +lon_0=81 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=15.8,-154.4,-82.3 +units=m +no_defs +2425 projected Beijing 1954 / 3-degree Gauss-Kruger CM 84E 82.5,28.26,85.5,47.23 1 6214 +proj=tmerc +lat_0=0 +lon_0=84 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=15.8,-154.4,-82.3 +units=m +no_defs +2426 projected Beijing 1954 / 3-degree Gauss-Kruger CM 87E 85.5,27.8,88.5,49.18 1 6214 +proj=tmerc +lat_0=0 +lon_0=87 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=15.8,-154.4,-82.3 +units=m +no_defs +2427 projected Beijing 1954 / 3-degree Gauss-Kruger CM 90E 88.49,27.32,91.51,48.42 6214 +proj=tmerc +lat_0=0 +lon_0=90 +k=1 +x_0=500000 +y_0=0 +ellps=krass +units=m +no_defs +2428 projected Beijing 1954 / 3-degree Gauss-Kruger CM 93E 91.5,27.71,94.5,45.13 6214 +proj=tmerc +lat_0=0 +lon_0=93 +k=1 +x_0=500000 +y_0=0 +ellps=krass +units=m +no_defs +2429 projected Beijing 1954 / 3-degree Gauss-Kruger CM 96E 94.5,28.23,97.51,44.5 6214 +proj=tmerc +lat_0=0 +lon_0=96 +k=1 +x_0=500000 +y_0=0 +ellps=krass +units=m +no_defs +2430 projected Beijing 1954 / 3-degree Gauss-Kruger CM 99E 97.5,21.43,100.5,42.76 6214 +proj=tmerc +lat_0=0 +lon_0=99 +k=1 +x_0=500000 +y_0=0 +ellps=krass +units=m +no_defs +2431 projected Beijing 1954 / 3-degree Gauss-Kruger CM 102E 100.5,21.13,103.5,42.69 6214 +proj=tmerc +lat_0=0 +lon_0=102 +k=1 +x_0=500000 +y_0=0 +ellps=krass +units=m +no_defs +2432 projected Beijing 1954 / 3-degree Gauss-Kruger CM 105E 103.5,22.5,106.51,42.21 6214 +proj=tmerc +lat_0=0 +lon_0=105 +k=1 +x_0=500000 +y_0=0 +ellps=krass +units=m +no_defs +2433 projected Beijing 1954 / 3-degree Gauss-Kruger CM 108E 106.5,18.19,109.51,42.47 1 6214 +proj=tmerc +lat_0=0 +lon_0=108 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=11.911,-154.833,-80.079 +units=m +no_defs +2434 projected Beijing 1954 / 3-degree Gauss-Kruger CM 111E 109.5,18.11,112.5,45.11 15 6214 +proj=tmerc +lat_0=0 +lon_0=111 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=31.4,-144.3,-74.8,0,0,0.814,-0.38 +units=m +no_defs +2435 projected Beijing 1954 / 3-degree Gauss-Kruger CM 114E 112.5,21.52,115.5,45.45 15 6214 +proj=tmerc +lat_0=0 +lon_0=114 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=31.4,-144.3,-74.8,0,0,0.814,-0.38 +units=m +no_defs +2436 projected Beijing 1954 / 3-degree Gauss-Kruger CM 117E 115.5,22.6,118.5,49.88 15 6214 +proj=tmerc +lat_0=0 +lon_0=117 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=31.4,-144.3,-74.8,0,0,0.814,-0.38 +units=m +no_defs +2437 projected Beijing 1954 / 3-degree Gauss-Kruger CM 120E 118.5,24.43,121.5,53.33 15 6214 +proj=tmerc +lat_0=0 +lon_0=120 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=15.53,-113.82,-41.38,0,0,0.814,-0.38 +units=m +no_defs +2438 projected Beijing 1954 / 3-degree Gauss-Kruger CM 123E 121.5,28.22,124.5,53.56 15 6214 +proj=tmerc +lat_0=0 +lon_0=123 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=15.53,-113.82,-41.38,0,0,0.814,-0.38 +units=m +no_defs +2439 projected Beijing 1954 / 3-degree Gauss-Kruger CM 126E 124.5,40.19,127.5,53.2 6214 +proj=tmerc +lat_0=0 +lon_0=126 +k=1 +x_0=500000 +y_0=0 +ellps=krass +units=m +no_defs +2440 projected Beijing 1954 / 3-degree Gauss-Kruger CM 129E 127.5,41.37,130.5,50.25 6214 +proj=tmerc +lat_0=0 +lon_0=129 +k=1 +x_0=500000 +y_0=0 +ellps=krass +units=m +no_defs +2441 projected Beijing 1954 / 3-degree Gauss-Kruger CM 132E 130.5,42.42,133.5,48.88 6214 +proj=tmerc +lat_0=0 +lon_0=132 +k=1 +x_0=500000 +y_0=0 +ellps=krass +units=m +no_defs +2442 projected Beijing 1954 / 3-degree Gauss-Kruger CM 135E 133.5,45.85,134.77,48.4 6214 +proj=tmerc +lat_0=0 +lon_0=135 +k=1 +x_0=500000 +y_0=0 +ellps=krass +units=m +no_defs +2443 projected JGD2000 / Japan Plane Rectangular CS I 128.17,26.96,130.46,34.74 1 6612 +proj=tmerc +lat_0=33 +lon_0=129.5 +k=0.9999 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +2444 projected JGD2000 / Japan Plane Rectangular CS II 129.76,30.18,132.05,33.99 1 6612 +proj=tmerc +lat_0=33 +lon_0=131 +k=0.9999 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +2445 projected JGD2000 / Japan Plane Rectangular CS III 130.81,33.72,133.49,36.38 1 6612 +proj=tmerc +lat_0=36 +lon_0=132.166666666667 +k=0.9999 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +2446 projected JGD2000 / Japan Plane Rectangular CS IV 131.95,32.69,134.81,34.45 1 6612 +proj=tmerc +lat_0=33 +lon_0=133.5 +k=0.9999 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +2447 projected JGD2000 / Japan Plane Rectangular CS V 133.13,34.13,135.47,35.71 1 6612 +proj=tmerc +lat_0=36 +lon_0=134.333333333333 +k=0.9999 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +2448 projected JGD2000 / Japan Plane Rectangular CS VI 134.86,33.4,136.99,36.33 1 6612 +proj=tmerc +lat_0=36 +lon_0=136 +k=0.9999 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +2449 projected JGD2000 / Japan Plane Rectangular CS VII 136.22,34.51,137.84,37.58 1 6612 +proj=tmerc +lat_0=36 +lon_0=137.166666666667 +k=0.9999 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +2450 projected JGD2000 / Japan Plane Rectangular CS VIII 137.32,34.54,139.91,38.58 1 6612 +proj=tmerc +lat_0=36 +lon_0=138.5 +k=0.9999 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +2451 projected JGD2000 / Japan Plane Rectangular CS IX 138.4,29.31,141.11,37.98 1 6612 +proj=tmerc +lat_0=36 +lon_0=139.833333333333 +k=0.9999 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +2452 projected JGD2000 / Japan Plane Rectangular CS X 139.49,37.73,142.14,41.58 1 6612 +proj=tmerc +lat_0=40 +lon_0=140.833333333333 +k=0.9999 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +2453 projected JGD2000 / Japan Plane Rectangular CS XI 139.34,41.34,141.46,43.42 1 6612 +proj=tmerc +lat_0=44 +lon_0=140.25 +k=0.9999 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +2454 projected JGD2000 / Japan Plane Rectangular CS XII 140.89,42.15,143.61,45.54 1 6612 +proj=tmerc +lat_0=44 +lon_0=142.25 +k=0.9999 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +2455 projected JGD2000 / Japan Plane Rectangular CS XIII 142.61,41.87,145.87,44.4 1 6612 +proj=tmerc +lat_0=44 +lon_0=144.25 +k=0.9999 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +2456 projected JGD2000 / Japan Plane Rectangular CS XIV 141.2,24.67,142.33,27.8 1 6612 +proj=tmerc +lat_0=26 +lon_0=142 +k=0.9999 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +2457 projected JGD2000 / Japan Plane Rectangular CS XV 126.63,26.02,128.4,26.91 1 6612 +proj=tmerc +lat_0=26 +lon_0=127.5 +k=0.9999 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +2458 projected JGD2000 / Japan Plane Rectangular CS XVI 122.83,23.98,125.51,24.94 1 6612 +proj=tmerc +lat_0=26 +lon_0=124 +k=0.9999 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +2459 projected JGD2000 / Japan Plane Rectangular CS XVII 131.12,24.4,131.38,26.01 1 6612 +proj=tmerc +lat_0=26 +lon_0=131 +k=0.9999 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +2460 projected JGD2000 / Japan Plane Rectangular CS XVIII 136.02,20.37,136.16,20.48 1 6612 +proj=tmerc +lat_0=20 +lon_0=136 +k=0.9999 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +2461 projected JGD2000 / Japan Plane Rectangular CS XIX 153.91,24.22,154.05,24.35 1 6612 +proj=tmerc +lat_0=26 +lon_0=154 +k=0.9999 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +2462 projected Albanian 1987 / Gauss-Kruger zone 4 19.22,39.64,21.06,42.67 1 6191 +proj=tmerc +lat_0=0 +lon_0=21 +k=1 +x_0=4500000 +y_0=0 +ellps=krass +towgs84=-44.183,-0.58,-38.489,2.3867,2.7072,-3.5196,-8.2703 +units=m +no_defs +2463 projected Pulkovo 1995 / Gauss-Kruger CM 21E 19.57,54.32,22.87,55.32 1 6200 +proj=tmerc +lat_0=0 +lon_0=21 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2464 projected Pulkovo 1995 / Gauss-Kruger CM 27E 26.61,55.69,30,69.47 1 6200 +proj=tmerc +lat_0=0 +lon_0=27 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2465 projected Pulkovo 1995 / Gauss-Kruger CM 33E 30,50.34,36,70.02 1 6200 +proj=tmerc +lat_0=0 +lon_0=33 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2466 projected Pulkovo 1995 / Gauss-Kruger CM 39E 36,43.18,42.01,69.23 1 6200 +proj=tmerc +lat_0=0 +lon_0=39 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2467 projected Pulkovo 1995 / Gauss-Kruger CM 45E 42,41.19,48,80.91 1 6200 +proj=tmerc +lat_0=0 +lon_0=45 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2468 projected Pulkovo 1995 / Gauss-Kruger CM 51E 48,41.39,54,81.4 1 6200 +proj=tmerc +lat_0=0 +lon_0=51 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2469 projected Pulkovo 1995 / Gauss-Kruger CM 57E 54,50.47,60,81.91 1 6200 +proj=tmerc +lat_0=0 +lon_0=57 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2470 projected Pulkovo 1995 / Gauss-Kruger CM 63E 60,50.66,66,81.77 1 6200 +proj=tmerc +lat_0=0 +lon_0=63 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2471 projected Pulkovo 1995 / Gauss-Kruger CM 69E 66,54.1,72,77.07 1 6200 +proj=tmerc +lat_0=0 +lon_0=69 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2472 projected Pulkovo 1995 / Gauss-Kruger CM 75E 72,53.17,78,79.71 1 6200 +proj=tmerc +lat_0=0 +lon_0=75 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2473 projected Pulkovo 1995 / Gauss-Kruger CM 81E 78,50.69,84,81.03 1 6200 +proj=tmerc +lat_0=0 +lon_0=81 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2474 projected Pulkovo 1995 / Gauss-Kruger CM 87E 84,49.07,90,81.27 1 6200 +proj=tmerc +lat_0=0 +lon_0=87 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2475 projected Pulkovo 1995 / Gauss-Kruger CM 93E 90,49.89,96,81.35 1 6200 +proj=tmerc +lat_0=0 +lon_0=93 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2476 projected Pulkovo 1995 / Gauss-Kruger CM 99E 96,49.73,102,81.32 1 6200 +proj=tmerc +lat_0=0 +lon_0=99 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2477 projected Pulkovo 1995 / Gauss-Kruger CM 105E 102,49.64,108,79.48 1 6200 +proj=tmerc +lat_0=0 +lon_0=105 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2478 projected Pulkovo 1995 / Gauss-Kruger CM 111E 108,49.14,114,76.81 1 6200 +proj=tmerc +lat_0=0 +lon_0=111 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2479 projected Pulkovo 1995 / Gauss-Kruger CM 117E 114,49.51,120,75.96 1 6200 +proj=tmerc +lat_0=0 +lon_0=117 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2480 projected Pulkovo 1995 / Gauss-Kruger CM 123E 120,51.51,126,74 1 6200 +proj=tmerc +lat_0=0 +lon_0=123 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2481 projected Pulkovo 1995 / Gauss-Kruger CM 129E 126,42.25,132,73.61 1 6200 +proj=tmerc +lat_0=0 +lon_0=129 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2482 projected Pulkovo 1995 / Gauss-Kruger CM 135E 132,42.63,138,76.15 1 6200 +proj=tmerc +lat_0=0 +lon_0=135 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2483 projected Pulkovo 1995 / Gauss-Kruger CM 141E 138,45.84,144,76.27 1 6200 +proj=tmerc +lat_0=0 +lon_0=141 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2484 projected Pulkovo 1995 / Gauss-Kruger CM 147E 144,43.6,150,76.82 1 6200 +proj=tmerc +lat_0=0 +lon_0=147 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2485 projected Pulkovo 1995 / Gauss-Kruger CM 153E 150,45.77,156,76.26 1 6200 +proj=tmerc +lat_0=0 +lon_0=153 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2486 projected Pulkovo 1995 / Gauss-Kruger CM 159E 156,50.27,162,77.2 1 6200 +proj=tmerc +lat_0=0 +lon_0=159 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2487 projected Pulkovo 1995 / Gauss-Kruger CM 165E 162,54.47,168,70.03 1 6200 +proj=tmerc +lat_0=0 +lon_0=165 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2488 projected Pulkovo 1995 / Gauss-Kruger CM 171E 168,54.45,174,70.19 1 6200 +proj=tmerc +lat_0=0 +lon_0=171 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2489 projected Pulkovo 1995 / Gauss-Kruger CM 177E 174,61.65,180,71.59 1 6200 +proj=tmerc +lat_0=0 +lon_0=177 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2490 projected Pulkovo 1995 / Gauss-Kruger CM 177W -180,64.35,-174,71.65 1 6200 +proj=tmerc +lat_0=0 +lon_0=-177 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2491 projected Pulkovo 1995 / Gauss-Kruger CM 171W -174,64.2,-168.97,67.18 1 6200 +proj=tmerc +lat_0=0 +lon_0=-171 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2494 projected Pulkovo 1942 / Gauss-Kruger CM 21E 19.57,47.95,24,59.44 4.5 6284 +proj=tmerc +lat_0=0 +lon_0=21 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=25,-141,-78.5,0,0.35,0.736,0 +units=m +no_defs +2495 projected Pulkovo 1942 / Gauss-Kruger CM 27E 24,45.18,30.01,69.47 3 6284 +proj=tmerc +lat_0=0 +lon_0=27 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2496 projected Pulkovo 1942 / Gauss-Kruger CM 33E 30,44.32,36,70.02 3 6284 +proj=tmerc +lat_0=0 +lon_0=33 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2497 projected Pulkovo 1942 / Gauss-Kruger CM 39E 36,41.43,42,69.23 3 6284 +proj=tmerc +lat_0=0 +lon_0=39 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2498 projected Pulkovo 1942 / Gauss-Kruger CM 45E 42,38.84,48.01,80.91 4.5 6284 +proj=tmerc +lat_0=0 +lon_0=45 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=25,-141,-78.5,0,0.35,0.736,0 +units=m +no_defs +2499 projected Pulkovo 1942 / Gauss-Kruger CM 51E 48,37.34,54,81.4 4.5 6284 +proj=tmerc +lat_0=0 +lon_0=51 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=25,-141,-78.5,0,0.35,0.736,0 +units=m +no_defs +2500 projected Pulkovo 1942 / Gauss-Kruger CM 57E 54,37.05,60,81.91 4.5 6284 +proj=tmerc +lat_0=0 +lon_0=57 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=25,-141,-78.5,0,0.35,0.736,0 +units=m +no_defs +2501 projected Pulkovo 1942 / Gauss-Kruger CM 63E 60,35.14,66,81.77 4.5 6284 +proj=tmerc +lat_0=0 +lon_0=63 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=25,-141,-78.5,0,0.35,0.736,0 +units=m +no_defs +2502 projected Pulkovo 1942 / Gauss-Kruger CM 69E 66,36.67,72,77.07 4.5 6284 +proj=tmerc +lat_0=0 +lon_0=69 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=25,-141,-78.5,0,0.35,0.736,0 +units=m +no_defs +2503 projected Pulkovo 1942 / Gauss-Kruger CM 75E 72,36.79,78,79.71 4.5 6284 +proj=tmerc +lat_0=0 +lon_0=75 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=25,-141,-78.5,0,0.35,0.736,0 +units=m +no_defs +2504 projected Pulkovo 1942 / Gauss-Kruger CM 81E 78,41.04,84.01,81.03 4.5 6284 +proj=tmerc +lat_0=0 +lon_0=81 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=25,-141,-78.5,0,0.35,0.736,0 +units=m +no_defs +2505 projected Pulkovo 1942 / Gauss-Kruger CM 87E 84,46.82,90,81.27 3 6284 +proj=tmerc +lat_0=0 +lon_0=87 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2506 projected Pulkovo 1942 / Gauss-Kruger CM 93E 90,49.89,96,81.35 3 6284 +proj=tmerc +lat_0=0 +lon_0=93 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2507 projected Pulkovo 1942 / Gauss-Kruger CM 99E 96,49.73,102,81.32 3 6284 +proj=tmerc +lat_0=0 +lon_0=99 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2508 projected Pulkovo 1942 / Gauss-Kruger CM 105E 102,49.64,108,79.48 3 6284 +proj=tmerc +lat_0=0 +lon_0=105 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2509 projected Pulkovo 1942 / Gauss-Kruger CM 111E 108,49.14,114,76.81 3 6284 +proj=tmerc +lat_0=0 +lon_0=111 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2510 projected Pulkovo 1942 / Gauss-Kruger CM 117E 114,49.51,120,75.96 3 6284 +proj=tmerc +lat_0=0 +lon_0=117 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2511 projected Pulkovo 1942 / Gauss-Kruger CM 123E 120,51.51,126,74 3 6284 +proj=tmerc +lat_0=0 +lon_0=123 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2512 projected Pulkovo 1942 / Gauss-Kruger CM 129E 126,42.25,132,73.61 3 6284 +proj=tmerc +lat_0=0 +lon_0=129 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2513 projected Pulkovo 1942 / Gauss-Kruger CM 135E 132,42.63,138,76.15 3 6284 +proj=tmerc +lat_0=0 +lon_0=135 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2514 projected Pulkovo 1942 / Gauss-Kruger CM 141E 138,45.84,144,76.27 3 6284 +proj=tmerc +lat_0=0 +lon_0=141 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2515 projected Pulkovo 1942 / Gauss-Kruger CM 147E 144,43.6,150,76.82 3 6284 +proj=tmerc +lat_0=0 +lon_0=147 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2516 projected Pulkovo 1942 / Gauss-Kruger CM 153E 150,45.77,156,76.26 3 6284 +proj=tmerc +lat_0=0 +lon_0=153 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2517 projected Pulkovo 1942 / Gauss-Kruger CM 159E 156,50.27,162,77.2 3 6284 +proj=tmerc +lat_0=0 +lon_0=159 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2518 projected Pulkovo 1942 / Gauss-Kruger CM 165E 162,54.47,168,70.03 3 6284 +proj=tmerc +lat_0=0 +lon_0=165 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2519 projected Pulkovo 1942 / Gauss-Kruger CM 171E 168,54.45,174,70.19 3 6284 +proj=tmerc +lat_0=0 +lon_0=171 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2520 projected Pulkovo 1942 / Gauss-Kruger CM 177E 174,61.65,180,71.59 3 6284 +proj=tmerc +lat_0=0 +lon_0=177 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2521 projected Pulkovo 1942 / Gauss-Kruger CM 177W -180,64.35,-174,71.65 3 6284 +proj=tmerc +lat_0=0 +lon_0=-177 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2522 projected Pulkovo 1942 / Gauss-Kruger CM 171W -174,64.2,-168.97,67.18 3 6284 +proj=tmerc +lat_0=0 +lon_0=-171 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2523 projected Pulkovo 1942 / 3-degree Gauss-Kruger zone 7 19.57,48.24,22.5,59.1 4.5 6284 +proj=tmerc +lat_0=0 +lon_0=21 +k=1 +x_0=7500000 +y_0=0 +ellps=krass +towgs84=25,-141,-78.5,0,0.35,0.736,0 +units=m +no_defs +2524 projected Pulkovo 1942 / 3-degree Gauss-Kruger zone 8 22.5,47.71,25.5,59.75 3 6284 +proj=tmerc +lat_0=0 +lon_0=24 +k=1 +x_0=8500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2525 projected Pulkovo 1942 / 3-degree Gauss-Kruger zone 9 25.49,45.26,28.51,68.93 3 6284 +proj=tmerc +lat_0=0 +lon_0=27 +k=1 +x_0=9500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2526 projected Pulkovo 1942 / 3-degree Gauss-Kruger zone 10 28.5,45.18,31.5,69.85 3 6284 +proj=tmerc +lat_0=0 +lon_0=30 +k=1 +x_0=10500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2527 projected Pulkovo 1942 / 3-degree Gauss-Kruger zone 11 31.5,44.32,34.5,70.02 3 6284 +proj=tmerc +lat_0=0 +lon_0=33 +k=1 +x_0=11500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2528 projected Pulkovo 1942 / 3-degree Gauss-Kruger zone 12 34.5,44.61,37.5,69.39 3 6284 +proj=tmerc +lat_0=0 +lon_0=36 +k=1 +x_0=12500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2529 projected Pulkovo 1942 / 3-degree Gauss-Kruger zone 13 37.5,43.07,40.5,68.8 3 6284 +proj=tmerc +lat_0=0 +lon_0=39 +k=1 +x_0=13500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2530 projected Pulkovo 1942 / 3-degree Gauss-Kruger zone 14 40.5,41.01,43.51,68.74 4.5 6284 +proj=tmerc +lat_0=0 +lon_0=42 +k=1 +x_0=14500000 +y_0=0 +ellps=krass +towgs84=25,-141,-78.5,0,0.35,0.736,0 +units=m +no_defs +2531 projected Pulkovo 1942 / 3-degree Gauss-Kruger zone 15 43.5,38.84,46.5,80.8 4.5 6284 +proj=tmerc +lat_0=0 +lon_0=45 +k=1 +x_0=15500000 +y_0=0 +ellps=krass +towgs84=25,-141,-78.5,0,0.35,0.736,0 +units=m +no_defs +2532 projected Pulkovo 1942 / 3-degree Gauss-Kruger zone 16 46.5,38.31,49.5,80.91 4.5 6284 +proj=tmerc +lat_0=0 +lon_0=48 +k=1 +x_0=16500000 +y_0=0 +ellps=krass +towgs84=25,-141,-78.5,0,0.35,0.736,0 +units=m +no_defs +2533 projected Pulkovo 1942 / 3-degree Gauss-Kruger zone 17 49.5,37.66,52.5,81.22 4.5 6284 +proj=tmerc +lat_0=0 +lon_0=51 +k=1 +x_0=17500000 +y_0=0 +ellps=krass +towgs84=25,-141,-78.5,0,0.35,0.736,0 +units=m +no_defs +2534 projected Pulkovo 1942 / 3-degree Gauss-Kruger zone 18 52.5,37.33,55.5,81.41 4.5 6284 +proj=tmerc +lat_0=0 +lon_0=54 +k=1 +x_0=18500000 +y_0=0 +ellps=krass +towgs84=25,-141,-78.5,0,0.35,0.736,0 +units=m +no_defs +2535 projected Pulkovo 1942 / 3-degree Gauss-Kruger zone 19 55.5,37.64,58.5,81.89 4.5 6284 +proj=tmerc +lat_0=0 +lon_0=57 +k=1 +x_0=19500000 +y_0=0 +ellps=krass +towgs84=25,-141,-78.5,0,0.35,0.736,0 +units=m +no_defs +2536 projected Pulkovo 1942 / 3-degree Gauss-Kruger zone 20 58.5,35.51,61.5,81.91 4.5 6284 +proj=tmerc +lat_0=0 +lon_0=60 +k=1 +x_0=20500000 +y_0=0 +ellps=krass +towgs84=25,-141,-78.5,0,0.35,0.736,0 +units=m +no_defs +2537 projected Pulkovo 1942 / 3-degree Gauss-Kruger zone 21 61.5,35.14,64.5,81.77 4.5 6284 +proj=tmerc +lat_0=0 +lon_0=63 +k=1 +x_0=21500000 +y_0=0 +ellps=krass +towgs84=25,-141,-78.5,0,0.35,0.736,0 +units=m +no_defs +2538 projected Pulkovo 1942 / 3-degree Gauss-Kruger zone 22 64.5,36.27,67.5,81.25 4.5 6284 +proj=tmerc +lat_0=0 +lon_0=66 +k=1 +x_0=22500000 +y_0=0 +ellps=krass +towgs84=25,-141,-78.5,0,0.35,0.736,0 +units=m +no_defs +2539 projected Pulkovo 1942 / 3-degree Gauss-Kruger zone 23 67.5,36.93,70.5,77.07 4.5 6284 +proj=tmerc +lat_0=0 +lon_0=69 +k=1 +x_0=23500000 +y_0=0 +ellps=krass +towgs84=25,-141,-78.5,0,0.35,0.736,0 +units=m +no_defs +2540 projected Pulkovo 1942 / 3-degree Gauss-Kruger zone 24 70.5,36.67,73.5,73.57 4.5 6284 +proj=tmerc +lat_0=0 +lon_0=72 +k=1 +x_0=24500000 +y_0=0 +ellps=krass +towgs84=25,-141,-78.5,0,0.35,0.736,0 +units=m +no_defs +2541 projected Pulkovo 1942 / 3-degree Gauss-Kruger zone 25 73.5,37.22,76.5,79.71 4.5 6284 +proj=tmerc +lat_0=0 +lon_0=75 +k=1 +x_0=25500000 +y_0=0 +ellps=krass +towgs84=25,-141,-78.5,0,0.35,0.736,0 +units=m +no_defs +2542 projected Pulkovo 1942 / 3-degree Gauss-Kruger zone 26 76.5,40.44,79.5,81.03 4.5 6284 +proj=tmerc +lat_0=0 +lon_0=78 +k=1 +x_0=26500000 +y_0=0 +ellps=krass +towgs84=25,-141,-78.5,0,0.35,0.736,0 +units=m +no_defs +2543 projected Pulkovo 1942 / 3-degree Gauss-Kruger zone 27 79.5,41.82,82.5,81.03 3 6284 +proj=tmerc +lat_0=0 +lon_0=81 +k=1 +x_0=27500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2544 projected Pulkovo 1942 / 3-degree Gauss-Kruger zone 28 82.5,45.11,85.5,77.56 3 6284 +proj=tmerc +lat_0=0 +lon_0=84 +k=1 +x_0=28500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2545 projected Pulkovo 1942 / 3-degree Gauss-Kruger zone 29 85.5,47.05,88.5,77.16 3 6284 +proj=tmerc +lat_0=0 +lon_0=87 +k=1 +x_0=29500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2546 projected Pulkovo 1942 / 3-degree Gauss-Kruger zone 30 88.5,49.44,91.5,81.28 3 6284 +proj=tmerc +lat_0=0 +lon_0=90 +k=1 +x_0=30500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2547 projected Pulkovo 1942 / 3-degree Gauss-Kruger zone 31 91.5,50.16,94.5,81.26 3 6284 +proj=tmerc +lat_0=0 +lon_0=93 +k=1 +x_0=31500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2548 projected Pulkovo 1942 / 3-degree Gauss-Kruger zone 32 94.5,49.73,97.5,81.35 3 6284 +proj=tmerc +lat_0=0 +lon_0=96 +k=1 +x_0=32500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2549 projected Pulkovo 1942 / 3-degree Gauss-Kruger zone 33 97.5,49.79,100.5,80.9 3 6284 +proj=tmerc +lat_0=0 +lon_0=99 +k=1 +x_0=33500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2551 projected Pulkovo 1942 / 3-degree Gauss-Kruger zone 34 100.5,50.17,103.5,79.71 3 6284 +proj=tmerc +lat_0=0 +lon_0=102 +k=1 +x_0=34500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2552 projected Pulkovo 1942 / 3-degree Gauss-Kruger zone 35 103.5,50.13,106.5,79.21 3 6284 +proj=tmerc +lat_0=0 +lon_0=105 +k=1 +x_0=35500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2553 projected Pulkovo 1942 / 3-degree Gauss-Kruger zone 36 106.5,49.25,109.5,78.4 3 6284 +proj=tmerc +lat_0=0 +lon_0=108 +k=1 +x_0=36500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2554 projected Pulkovo 1942 / 3-degree Gauss-Kruger zone 37 109.5,49.14,112.5,76.81 3 6284 +proj=tmerc +lat_0=0 +lon_0=111 +k=1 +x_0=37500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2555 projected Pulkovo 1942 / 3-degree Gauss-Kruger zone 38 112.5,49.49,115.5,76.7 3 6284 +proj=tmerc +lat_0=0 +lon_0=114 +k=1 +x_0=38500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2556 projected Pulkovo 1942 / 3-degree Gauss-Kruger zone 39 115.5,49.51,118.5,74.43 3 6284 +proj=tmerc +lat_0=0 +lon_0=117 +k=1 +x_0=39500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2557 projected Pulkovo 1942 / 3-degree Gauss-Kruger zone 40 118.5,49.87,121.5,73.63 3 6284 +proj=tmerc +lat_0=0 +lon_0=120 +k=1 +x_0=40500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2558 projected Pulkovo 1942 / 3-degree Gauss-Kruger zone 41 121.5,53.18,124.5,74 3 6284 +proj=tmerc +lat_0=0 +lon_0=123 +k=1 +x_0=41500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2559 projected Pulkovo 1942 / 3-degree Gauss-Kruger zone 42 124.5,49.88,127.5,74 3 6284 +proj=tmerc +lat_0=0 +lon_0=126 +k=1 +x_0=42500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2560 projected Pulkovo 1942 / 3-degree Gauss-Kruger zone 43 127.5,42.67,130.5,73.59 3 6284 +proj=tmerc +lat_0=0 +lon_0=129 +k=1 +x_0=43500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2561 projected Pulkovo 1942 / 3-degree Gauss-Kruger zone 44 130.5,42.25,133.5,71.99 3 6284 +proj=tmerc +lat_0=0 +lon_0=132 +k=1 +x_0=44500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2562 projected Pulkovo 1942 / 3-degree Gauss-Kruger zone 45 133.5,42.74,136.5,75.9 3 6284 +proj=tmerc +lat_0=0 +lon_0=135 +k=1 +x_0=45500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2563 projected Pulkovo 1942 / 3-degree Gauss-Kruger zone 46 136.5,44.76,139.5,76.27 3 6284 +proj=tmerc +lat_0=0 +lon_0=138 +k=1 +x_0=46500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2564 projected Pulkovo 1942 / 3-degree Gauss-Kruger zone 47 139.5,45.84,142.5,76.23 3 6284 +proj=tmerc +lat_0=0 +lon_0=141 +k=1 +x_0=47500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2565 projected Pulkovo 1942 / 3-degree Gauss-Kruger zone 48 142.5,43.61,145.5,75.98 3 6284 +proj=tmerc +lat_0=0 +lon_0=144 +k=1 +x_0=48500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2566 projected Pulkovo 1942 / 3-degree Gauss-Kruger zone 49 145.5,43.6,148.5,76.76 3 6284 +proj=tmerc +lat_0=0 +lon_0=147 +k=1 +x_0=49500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2567 projected Pulkovo 1942 / 3-degree Gauss-Kruger zone 50 148.5,45.21,151.5,76.82 3 6284 +proj=tmerc +lat_0=0 +lon_0=150 +k=1 +x_0=50500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2568 projected Pulkovo 1942 / 3-degree Gauss-Kruger zone 51 151.5,46.72,154.5,76.26 3 6284 +proj=tmerc +lat_0=0 +lon_0=153 +k=1 +x_0=51500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2569 projected Pulkovo 1942 / 3-degree Gauss-Kruger zone 52 154.5,49.02,157.5,77.2 3 6284 +proj=tmerc +lat_0=0 +lon_0=156 +k=1 +x_0=52500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2570 projected Pulkovo 1942 / 3-degree Gauss-Kruger zone 53 157.5,51.36,160.5,71.12 3 6284 +proj=tmerc +lat_0=0 +lon_0=159 +k=1 +x_0=53500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2571 projected Pulkovo 1942 / 3-degree Gauss-Kruger zone 54 160.5,54.34,163.5,70.98 3 6284 +proj=tmerc +lat_0=0 +lon_0=162 +k=1 +x_0=54500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2572 projected Pulkovo 1942 / 3-degree Gauss-Kruger zone 55 163.5,54.69,166.5,69.82 3 6284 +proj=tmerc +lat_0=0 +lon_0=165 +k=1 +x_0=55500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2573 projected Pulkovo 1942 / 3-degree Gauss-Kruger zone 56 166.5,54.45,169.5,70.07 3 6284 +proj=tmerc +lat_0=0 +lon_0=168 +k=1 +x_0=56500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2574 projected Pulkovo 1942 / 3-degree Gauss-Kruger zone 57 169.5,59.86,172.5,70.19 3 6284 +proj=tmerc +lat_0=0 +lon_0=171 +k=1 +x_0=57500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2575 projected Pulkovo 1942 / 3-degree Gauss-Kruger zone 58 172.5,60.99,175.5,70.02 3 6284 +proj=tmerc +lat_0=0 +lon_0=174 +k=1 +x_0=58500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2576 projected Pulkovo 1942 / 3-degree Gauss-Kruger zone 59 175.5,62.09,178.5,71.1 3 6284 +proj=tmerc +lat_0=0 +lon_0=177 +k=1 +x_0=59500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2578 projected Pulkovo 1942 / 3-degree Gauss-Kruger zone 61 -178.5,64.74,-175.5,71.61 3 6284 +proj=tmerc +lat_0=0 +lon_0=-177 +k=1 +x_0=61500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2579 projected Pulkovo 1942 / 3-degree Gauss-Kruger zone 62 -175.5,64.2,-172.5,67.78 3 6284 +proj=tmerc +lat_0=0 +lon_0=-174 +k=1 +x_0=62500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2580 projected Pulkovo 1942 / 3-degree Gauss-Kruger zone 63 -172.5,64.35,-169.57,67.06 3 6284 +proj=tmerc +lat_0=0 +lon_0=-171 +k=1 +x_0=63500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2581 projected Pulkovo 1942 / 3-degree Gauss-Kruger zone 64 -169.22,65.7,-168.97,65.86 3 6284 +proj=tmerc +lat_0=0 +lon_0=-168 +k=1 +x_0=64500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2582 projected Pulkovo 1942 / 3-degree Gauss-Kruger CM 21E 19.57,48.24,22.5,59.1 4.5 6284 +proj=tmerc +lat_0=0 +lon_0=21 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=25,-141,-78.5,0,0.35,0.736,0 +units=m +no_defs +2583 projected Pulkovo 1942 / 3-degree Gauss-Kruger CM 24E 22.5,47.71,25.5,59.75 3 6284 +proj=tmerc +lat_0=0 +lon_0=24 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2584 projected Pulkovo 1942 / 3-degree Gauss-Kruger CM 27E 25.49,45.26,28.51,68.93 3 6284 +proj=tmerc +lat_0=0 +lon_0=27 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2585 projected Pulkovo 1942 / 3-degree Gauss-Kruger CM 30E 28.5,45.18,31.5,69.85 3 6284 +proj=tmerc +lat_0=0 +lon_0=30 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2586 projected Pulkovo 1942 / 3-degree Gauss-Kruger CM 33E 31.5,44.32,34.5,70.02 3 6284 +proj=tmerc +lat_0=0 +lon_0=33 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2587 projected Pulkovo 1942 / 3-degree Gauss-Kruger CM 36E 34.5,44.61,37.5,69.39 3 6284 +proj=tmerc +lat_0=0 +lon_0=36 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2588 projected Pulkovo 1942 / 3-degree Gauss-Kruger CM 39E 37.5,43.07,40.5,68.8 3 6284 +proj=tmerc +lat_0=0 +lon_0=39 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2589 projected Pulkovo 1942 / 3-degree Gauss-Kruger CM 42E 40.5,41.01,43.51,68.74 4.5 6284 +proj=tmerc +lat_0=0 +lon_0=42 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=25,-141,-78.5,0,0.35,0.736,0 +units=m +no_defs +2590 projected Pulkovo 1942 / 3-degree Gauss-Kruger CM 45E 43.5,38.84,46.5,80.8 4.5 6284 +proj=tmerc +lat_0=0 +lon_0=45 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=25,-141,-78.5,0,0.35,0.736,0 +units=m +no_defs +2591 projected Pulkovo 1942 / 3-degree Gauss-Kruger CM 48E 46.5,38.31,49.5,80.91 4.5 6284 +proj=tmerc +lat_0=0 +lon_0=48 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=25,-141,-78.5,0,0.35,0.736,0 +units=m +no_defs +2592 projected Pulkovo 1942 / 3-degree Gauss-Kruger CM 51E 49.5,37.66,52.5,81.22 4.5 6284 +proj=tmerc +lat_0=0 +lon_0=51 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=25,-141,-78.5,0,0.35,0.736,0 +units=m +no_defs +2593 projected Pulkovo 1942 / 3-degree Gauss-Kruger CM 54E 52.5,37.33,55.5,81.41 4.5 6284 +proj=tmerc +lat_0=0 +lon_0=54 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=25,-141,-78.5,0,0.35,0.736,0 +units=m +no_defs +2594 projected Pulkovo 1942 / 3-degree Gauss-Kruger CM 57E 55.5,37.64,58.5,81.89 4.5 6284 +proj=tmerc +lat_0=0 +lon_0=57 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=25,-141,-78.5,0,0.35,0.736,0 +units=m +no_defs +2595 projected Pulkovo 1942 / 3-degree Gauss-Kruger CM 60E 58.5,35.51,61.5,81.91 4.5 6284 +proj=tmerc +lat_0=0 +lon_0=60 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=25,-141,-78.5,0,0.35,0.736,0 +units=m +no_defs +2596 projected Pulkovo 1942 / 3-degree Gauss-Kruger CM 63E 61.5,35.14,64.5,81.77 4.5 6284 +proj=tmerc +lat_0=0 +lon_0=63 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=25,-141,-78.5,0,0.35,0.736,0 +units=m +no_defs +2597 projected Pulkovo 1942 / 3-degree Gauss-Kruger CM 66E 64.5,36.27,67.5,81.25 4.5 6284 +proj=tmerc +lat_0=0 +lon_0=66 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=25,-141,-78.5,0,0.35,0.736,0 +units=m +no_defs +2598 projected Pulkovo 1942 / 3-degree Gauss-Kruger CM 69E 67.5,36.93,70.5,77.07 4.5 6284 +proj=tmerc +lat_0=0 +lon_0=69 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=25,-141,-78.5,0,0.35,0.736,0 +units=m +no_defs +2599 projected Pulkovo 1942 / 3-degree Gauss-Kruger CM 72E 70.5,36.67,73.5,73.57 4.5 6284 +proj=tmerc +lat_0=0 +lon_0=72 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=25,-141,-78.5,0,0.35,0.736,0 +units=m +no_defs +2601 projected Pulkovo 1942 / 3-degree Gauss-Kruger CM 75E 73.5,37.22,76.5,79.71 4.5 6284 +proj=tmerc +lat_0=0 +lon_0=75 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=25,-141,-78.5,0,0.35,0.736,0 +units=m +no_defs +2602 projected Pulkovo 1942 / 3-degree Gauss-Kruger CM 78E 76.5,40.44,79.5,81.03 4.5 6284 +proj=tmerc +lat_0=0 +lon_0=78 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=25,-141,-78.5,0,0.35,0.736,0 +units=m +no_defs +2603 projected Pulkovo 1942 / 3-degree Gauss-Kruger CM 81E 79.5,41.82,82.5,81.03 3 6284 +proj=tmerc +lat_0=0 +lon_0=81 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2604 projected Pulkovo 1942 / 3-degree Gauss-Kruger CM 84E 82.5,45.11,85.5,77.56 3 6284 +proj=tmerc +lat_0=0 +lon_0=84 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2605 projected Pulkovo 1942 / 3-degree Gauss-Kruger CM 87E 85.5,47.05,88.5,77.16 3 6284 +proj=tmerc +lat_0=0 +lon_0=87 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2606 projected Pulkovo 1942 / 3-degree Gauss-Kruger CM 90E 88.5,49.44,91.5,81.28 3 6284 +proj=tmerc +lat_0=0 +lon_0=90 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2607 projected Pulkovo 1942 / 3-degree Gauss-Kruger CM 93E 91.5,50.16,94.5,81.26 3 6284 +proj=tmerc +lat_0=0 +lon_0=93 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2608 projected Pulkovo 1942 / 3-degree Gauss-Kruger CM 96E 94.5,49.73,97.5,81.35 3 6284 +proj=tmerc +lat_0=0 +lon_0=96 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2609 projected Pulkovo 1942 / 3-degree Gauss-Kruger CM 99E 97.5,49.79,100.5,80.9 3 6284 +proj=tmerc +lat_0=0 +lon_0=99 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2610 projected Pulkovo 1942 / 3-degree Gauss-Kruger CM 102E 100.5,50.17,103.5,79.71 3 6284 +proj=tmerc +lat_0=0 +lon_0=102 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2611 projected Pulkovo 1942 / 3-degree Gauss-Kruger CM 105E 103.5,50.13,106.5,79.21 3 6284 +proj=tmerc +lat_0=0 +lon_0=105 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2612 projected Pulkovo 1942 / 3-degree Gauss-Kruger CM 108E 106.5,49.25,109.5,78.4 3 6284 +proj=tmerc +lat_0=0 +lon_0=108 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2613 projected Pulkovo 1942 / 3-degree Gauss-Kruger CM 111E 109.5,49.14,112.5,76.81 3 6284 +proj=tmerc +lat_0=0 +lon_0=111 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2614 projected Pulkovo 1942 / 3-degree Gauss-Kruger CM 114E 112.5,49.49,115.5,76.7 3 6284 +proj=tmerc +lat_0=0 +lon_0=114 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2615 projected Pulkovo 1942 / 3-degree Gauss-Kruger CM 117E 115.5,49.51,118.5,74.43 3 6284 +proj=tmerc +lat_0=0 +lon_0=117 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2616 projected Pulkovo 1942 / 3-degree Gauss-Kruger CM 120E 118.5,49.87,121.5,73.63 3 6284 +proj=tmerc +lat_0=0 +lon_0=120 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2617 projected Pulkovo 1942 / 3-degree Gauss-Kruger CM 123E 121.5,53.18,124.5,74 3 6284 +proj=tmerc +lat_0=0 +lon_0=123 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2618 projected Pulkovo 1942 / 3-degree Gauss-Kruger CM 126E 124.5,49.88,127.5,74 3 6284 +proj=tmerc +lat_0=0 +lon_0=126 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2619 projected Pulkovo 1942 / 3-degree Gauss-Kruger CM 129E 127.5,42.67,130.5,73.59 3 6284 +proj=tmerc +lat_0=0 +lon_0=129 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2620 projected Pulkovo 1942 / 3-degree Gauss-Kruger CM 132E 130.5,42.25,133.5,71.99 3 6284 +proj=tmerc +lat_0=0 +lon_0=132 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2621 projected Pulkovo 1942 / 3-degree Gauss-Kruger CM 135E 133.5,42.74,136.5,75.9 3 6284 +proj=tmerc +lat_0=0 +lon_0=135 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2622 projected Pulkovo 1942 / 3-degree Gauss-Kruger CM 138E 136.5,44.76,139.5,76.27 3 6284 +proj=tmerc +lat_0=0 +lon_0=138 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2623 projected Pulkovo 1942 / 3-degree Gauss-Kruger CM 141E 139.5,45.84,142.5,76.23 3 6284 +proj=tmerc +lat_0=0 +lon_0=141 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2624 projected Pulkovo 1942 / 3-degree Gauss-Kruger CM 144E 142.5,43.61,145.5,75.98 3 6284 +proj=tmerc +lat_0=0 +lon_0=144 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2625 projected Pulkovo 1942 / 3-degree Gauss-Kruger CM 147E 145.5,43.6,148.5,76.76 3 6284 +proj=tmerc +lat_0=0 +lon_0=147 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2626 projected Pulkovo 1942 / 3-degree Gauss-Kruger CM 150E 148.5,45.21,151.5,76.82 3 6284 +proj=tmerc +lat_0=0 +lon_0=150 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2627 projected Pulkovo 1942 / 3-degree Gauss-Kruger CM 153E 151.5,46.72,154.5,76.26 3 6284 +proj=tmerc +lat_0=0 +lon_0=153 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2628 projected Pulkovo 1942 / 3-degree Gauss-Kruger CM 156E 154.5,49.02,157.5,77.2 3 6284 +proj=tmerc +lat_0=0 +lon_0=156 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2629 projected Pulkovo 1942 / 3-degree Gauss-Kruger CM 159E 157.5,51.36,160.5,71.12 3 6284 +proj=tmerc +lat_0=0 +lon_0=159 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2630 projected Pulkovo 1942 / 3-degree Gauss-Kruger CM 162E 160.5,54.34,163.5,70.98 3 6284 +proj=tmerc +lat_0=0 +lon_0=162 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2631 projected Pulkovo 1942 / 3-degree Gauss-Kruger CM 165E 163.5,54.69,166.5,69.82 3 6284 +proj=tmerc +lat_0=0 +lon_0=165 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2632 projected Pulkovo 1942 / 3-degree Gauss-Kruger CM 168E 166.5,54.45,169.5,70.07 3 6284 +proj=tmerc +lat_0=0 +lon_0=168 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2633 projected Pulkovo 1942 / 3-degree Gauss-Kruger CM 171E 169.5,59.86,172.5,70.19 3 6284 +proj=tmerc +lat_0=0 +lon_0=171 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2634 projected Pulkovo 1942 / 3-degree Gauss-Kruger CM 174E 172.5,60.99,175.5,70.02 3 6284 +proj=tmerc +lat_0=0 +lon_0=174 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2635 projected Pulkovo 1942 / 3-degree Gauss-Kruger CM 177E 175.5,62.09,178.5,71.1 3 6284 +proj=tmerc +lat_0=0 +lon_0=177 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2636 projected Pulkovo 1942 / 3-degree Gauss-Kruger CM 180E 178.5,62.24,-178.5,71.65 3 6284 +proj=tmerc +lat_0=0 +lon_0=180 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2637 projected Pulkovo 1942 / 3-degree Gauss-Kruger CM 177W -178.5,64.74,-175.5,71.61 3 6284 +proj=tmerc +lat_0=0 +lon_0=-177 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2638 projected Pulkovo 1942 / 3-degree Gauss-Kruger CM 174W -175.5,64.2,-172.5,67.78 3 6284 +proj=tmerc +lat_0=0 +lon_0=-174 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2639 projected Pulkovo 1942 / 3-degree Gauss-Kruger CM 171W -172.5,64.35,-169.57,67.06 3 6284 +proj=tmerc +lat_0=0 +lon_0=-171 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2640 projected Pulkovo 1942 / 3-degree Gauss-Kruger CM 168W -169.22,65.7,-168.97,65.86 3 6284 +proj=tmerc +lat_0=0 +lon_0=-168 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2641 projected Pulkovo 1995 / 3-degree Gauss-Kruger zone 7 19.57,54.32,22.5,55.32 1 6200 +proj=tmerc +lat_0=0 +lon_0=21 +k=1 +x_0=7500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2642 projected Pulkovo 1995 / 3-degree Gauss-Kruger zone 8 22.5,54.34,22.87,55.07 1 6200 +proj=tmerc +lat_0=0 +lon_0=24 +k=1 +x_0=8500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2643 projected Pulkovo 1995 / 3-degree Gauss-Kruger zone 9 26.61,56.05,28.51,68.93 1 6200 +proj=tmerc +lat_0=0 +lon_0=27 +k=1 +x_0=9500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2644 projected Pulkovo 1995 / 3-degree Gauss-Kruger zone 10 28.5,52.85,31.5,69.85 1 6200 +proj=tmerc +lat_0=0 +lon_0=30 +k=1 +x_0=10500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2645 projected Pulkovo 1995 / 3-degree Gauss-Kruger zone 11 31.5,51.24,34.5,70.02 1 6200 +proj=tmerc +lat_0=0 +lon_0=33 +k=1 +x_0=11500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2646 projected Pulkovo 1995 / 3-degree Gauss-Kruger zone 12 34.5,44.61,37.51,69.39 1 6200 +proj=tmerc +lat_0=0 +lon_0=36 +k=1 +x_0=12500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2647 projected Pulkovo 1995 / 3-degree Gauss-Kruger zone 13 37.5,43.33,40.5,68.8 1 6200 +proj=tmerc +lat_0=0 +lon_0=39 +k=1 +x_0=13500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2648 projected Pulkovo 1995 / 3-degree Gauss-Kruger zone 14 40.5,42.87,43.5,68.74 1 6200 +proj=tmerc +lat_0=0 +lon_0=42 +k=1 +x_0=14500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2649 projected Pulkovo 1995 / 3-degree Gauss-Kruger zone 15 43.5,41.89,46.5,80.8 1 6200 +proj=tmerc +lat_0=0 +lon_0=45 +k=1 +x_0=15500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2650 projected Pulkovo 1995 / 3-degree Gauss-Kruger zone 16 46.5,41.19,49.5,80.91 1 6200 +proj=tmerc +lat_0=0 +lon_0=48 +k=1 +x_0=16500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2651 projected Pulkovo 1995 / 3-degree Gauss-Kruger zone 17 49.5,42.36,52.5,81.22 1 6200 +proj=tmerc +lat_0=0 +lon_0=51 +k=1 +x_0=17500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2652 projected Pulkovo 1995 / 3-degree Gauss-Kruger zone 18 52.5,50.52,55.5,81.41 1 6200 +proj=tmerc +lat_0=0 +lon_0=54 +k=1 +x_0=18500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2653 projected Pulkovo 1995 / 3-degree Gauss-Kruger zone 19 55.5,50.53,58.5,81.89 1 6200 +proj=tmerc +lat_0=0 +lon_0=57 +k=1 +x_0=19500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2654 projected Pulkovo 1995 / 3-degree Gauss-Kruger zone 20 58.5,50.47,61.5,81.91 1 6200 +proj=tmerc +lat_0=0 +lon_0=60 +k=1 +x_0=20500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2655 projected Pulkovo 1995 / 3-degree Gauss-Kruger zone 21 61.5,51.03,64.5,81.77 1 6200 +proj=tmerc +lat_0=0 +lon_0=63 +k=1 +x_0=21500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2656 projected Pulkovo 1995 / 3-degree Gauss-Kruger zone 22 64.5,54.31,67.5,81.25 1 6200 +proj=tmerc +lat_0=0 +lon_0=66 +k=1 +x_0=22500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2657 projected Pulkovo 1995 / 3-degree Gauss-Kruger zone 23 67.5,54.85,70.5,77.07 1 6200 +proj=tmerc +lat_0=0 +lon_0=69 +k=1 +x_0=23500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2658 projected Pulkovo 1995 / 3-degree Gauss-Kruger zone 24 70.5,53.43,73.5,73.57 1 6200 +proj=tmerc +lat_0=0 +lon_0=72 +k=1 +x_0=24500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2659 projected Pulkovo 1995 / 3-degree Gauss-Kruger zone 25 73.5,53.47,76.5,79.71 1 6200 +proj=tmerc +lat_0=0 +lon_0=75 +k=1 +x_0=25500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2660 projected Pulkovo 1995 / 3-degree Gauss-Kruger zone 26 76.5,51.49,79.5,81.03 1 6200 +proj=tmerc +lat_0=0 +lon_0=78 +k=1 +x_0=26500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2661 projected Pulkovo 1995 / 3-degree Gauss-Kruger zone 27 79.5,50.7,82.5,81.03 1 6200 +proj=tmerc +lat_0=0 +lon_0=81 +k=1 +x_0=27500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2662 projected Pulkovo 1995 / 3-degree Gauss-Kruger zone 28 82.5,49.58,85.5,77.56 1 6200 +proj=tmerc +lat_0=0 +lon_0=84 +k=1 +x_0=28500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2663 projected Pulkovo 1995 / 3-degree Gauss-Kruger zone 29 85.5,49.07,88.5,77.16 1 6200 +proj=tmerc +lat_0=0 +lon_0=87 +k=1 +x_0=29500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2664 projected Pulkovo 1995 / 3-degree Gauss-Kruger zone 30 88.5,49.44,91.5,81.28 1 6200 +proj=tmerc +lat_0=0 +lon_0=90 +k=1 +x_0=30500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2665 projected Pulkovo 1995 / 3-degree Gauss-Kruger zone 31 91.5,50.16,94.5,81.26 1 6200 +proj=tmerc +lat_0=0 +lon_0=93 +k=1 +x_0=31500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2666 projected Pulkovo 1995 / 3-degree Gauss-Kruger zone 32 94.5,49.73,97.5,81.35 1 6200 +proj=tmerc +lat_0=0 +lon_0=96 +k=1 +x_0=32500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2667 projected Pulkovo 1995 / 3-degree Gauss-Kruger zone 33 97.5,49.79,100.5,80.9 1 6200 +proj=tmerc +lat_0=0 +lon_0=99 +k=1 +x_0=33500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2668 projected Pulkovo 1995 / 3-degree Gauss-Kruger zone 34 100.5,50.17,103.5,79.71 1 6200 +proj=tmerc +lat_0=0 +lon_0=102 +k=1 +x_0=34500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2669 projected Pulkovo 1995 / 3-degree Gauss-Kruger zone 35 103.5,50.13,106.5,79.21 1 6200 +proj=tmerc +lat_0=0 +lon_0=105 +k=1 +x_0=35500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2670 projected Pulkovo 1995 / 3-degree Gauss-Kruger zone 36 106.5,49.25,109.5,78.4 1 6200 +proj=tmerc +lat_0=0 +lon_0=108 +k=1 +x_0=36500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2671 projected Pulkovo 1995 / 3-degree Gauss-Kruger zone 37 109.5,49.14,112.5,76.81 1 6200 +proj=tmerc +lat_0=0 +lon_0=111 +k=1 +x_0=37500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2672 projected Pulkovo 1995 / 3-degree Gauss-Kruger zone 38 112.5,49.49,115.5,76.7 1 6200 +proj=tmerc +lat_0=0 +lon_0=114 +k=1 +x_0=38500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2673 projected Pulkovo 1995 / 3-degree Gauss-Kruger zone 39 115.5,49.51,118.5,74.43 1 6200 +proj=tmerc +lat_0=0 +lon_0=117 +k=1 +x_0=39500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2674 projected Pulkovo 1995 / 3-degree Gauss-Kruger zone 40 118.5,49.87,121.5,73.63 1 6200 +proj=tmerc +lat_0=0 +lon_0=120 +k=1 +x_0=40500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2675 projected Pulkovo 1995 / 3-degree Gauss-Kruger zone 41 121.5,53.18,124.5,74 1 6200 +proj=tmerc +lat_0=0 +lon_0=123 +k=1 +x_0=41500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2676 projected Pulkovo 1995 / 3-degree Gauss-Kruger zone 42 124.5,49.88,127.5,74 1 6200 +proj=tmerc +lat_0=0 +lon_0=126 +k=1 +x_0=42500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2677 projected Pulkovo 1995 / 3-degree Gauss-Kruger zone 43 127.5,42.67,130.5,73.59 1 6200 +proj=tmerc +lat_0=0 +lon_0=129 +k=1 +x_0=43500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2678 projected Pulkovo 1995 / 3-degree Gauss-Kruger zone 44 130.5,42.25,133.5,71.99 1 6200 +proj=tmerc +lat_0=0 +lon_0=132 +k=1 +x_0=44500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2679 projected Pulkovo 1995 / 3-degree Gauss-Kruger zone 45 133.5,42.74,136.5,75.9 1 6200 +proj=tmerc +lat_0=0 +lon_0=135 +k=1 +x_0=45500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2680 projected Pulkovo 1995 / 3-degree Gauss-Kruger zone 46 136.5,44.76,139.5,76.27 1 6200 +proj=tmerc +lat_0=0 +lon_0=138 +k=1 +x_0=46500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2681 projected Pulkovo 1995 / 3-degree Gauss-Kruger zone 47 139.5,45.84,142.5,76.23 1 6200 +proj=tmerc +lat_0=0 +lon_0=141 +k=1 +x_0=47500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2682 projected Pulkovo 1995 / 3-degree Gauss-Kruger zone 48 142.5,43.61,145.5,75.98 1 6200 +proj=tmerc +lat_0=0 +lon_0=144 +k=1 +x_0=48500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2683 projected Pulkovo 1995 / 3-degree Gauss-Kruger zone 49 145.5,43.6,148.5,76.76 1 6200 +proj=tmerc +lat_0=0 +lon_0=147 +k=1 +x_0=49500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2684 projected Pulkovo 1995 / 3-degree Gauss-Kruger zone 50 148.5,45.21,151.5,76.82 1 6200 +proj=tmerc +lat_0=0 +lon_0=150 +k=1 +x_0=50500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2685 projected Pulkovo 1995 / 3-degree Gauss-Kruger zone 51 151.5,46.72,154.5,76.26 1 6200 +proj=tmerc +lat_0=0 +lon_0=153 +k=1 +x_0=51500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2686 projected Pulkovo 1995 / 3-degree Gauss-Kruger zone 52 154.5,49.02,157.5,77.2 1 6200 +proj=tmerc +lat_0=0 +lon_0=156 +k=1 +x_0=52500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2687 projected Pulkovo 1995 / 3-degree Gauss-Kruger zone 53 157.5,51.36,160.5,71.12 1 6200 +proj=tmerc +lat_0=0 +lon_0=159 +k=1 +x_0=53500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2688 projected Pulkovo 1995 / 3-degree Gauss-Kruger zone 54 160.5,54.34,163.5,70.98 1 6200 +proj=tmerc +lat_0=0 +lon_0=162 +k=1 +x_0=54500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2689 projected Pulkovo 1995 / 3-degree Gauss-Kruger zone 55 163.5,54.69,166.5,69.82 1 6200 +proj=tmerc +lat_0=0 +lon_0=165 +k=1 +x_0=55500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2690 projected Pulkovo 1995 / 3-degree Gauss-Kruger zone 56 166.5,54.45,169.5,70.07 1 6200 +proj=tmerc +lat_0=0 +lon_0=168 +k=1 +x_0=56500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2691 projected Pulkovo 1995 / 3-degree Gauss-Kruger zone 57 169.5,59.86,172.5,70.19 1 6200 +proj=tmerc +lat_0=0 +lon_0=171 +k=1 +x_0=57500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2692 projected Pulkovo 1995 / 3-degree Gauss-Kruger zone 58 172.5,60.99,175.5,70.02 1 6200 +proj=tmerc +lat_0=0 +lon_0=174 +k=1 +x_0=58500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2693 projected Pulkovo 1995 / 3-degree Gauss-Kruger zone 59 175.5,62.09,178.5,71.1 1 6200 +proj=tmerc +lat_0=0 +lon_0=177 +k=1 +x_0=59500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2695 projected Pulkovo 1995 / 3-degree Gauss-Kruger zone 61 -178.5,64.74,-175.5,71.61 1 6200 +proj=tmerc +lat_0=0 +lon_0=-177 +k=1 +x_0=61500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2696 projected Pulkovo 1995 / 3-degree Gauss-Kruger zone 62 -175.5,64.2,-172.5,67.78 1 6200 +proj=tmerc +lat_0=0 +lon_0=-174 +k=1 +x_0=62500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2697 projected Pulkovo 1995 / 3-degree Gauss-Kruger zone 63 -172.5,64.35,-169.57,67.06 1 6200 +proj=tmerc +lat_0=0 +lon_0=-171 +k=1 +x_0=63500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2698 projected Pulkovo 1995 / 3-degree Gauss-Kruger zone 64 -169.22,65.7,-168.97,65.86 1 6200 +proj=tmerc +lat_0=0 +lon_0=-168 +k=1 +x_0=64500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2699 projected Pulkovo 1995 / 3-degree Gauss-Kruger CM 21E 19.57,54.32,22.5,55.32 1 6200 +proj=tmerc +lat_0=0 +lon_0=21 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2700 projected Pulkovo 1995 / 3-degree Gauss-Kruger CM 24E 22.5,54.34,22.87,55.07 1 6200 +proj=tmerc +lat_0=0 +lon_0=24 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2701 projected Pulkovo 1995 / 3-degree Gauss-Kruger CM 27E 26.61,56.05,28.51,68.93 1 6200 +proj=tmerc +lat_0=0 +lon_0=27 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2702 projected Pulkovo 1995 / 3-degree Gauss-Kruger CM 30E 28.5,52.85,31.5,69.85 1 6200 +proj=tmerc +lat_0=0 +lon_0=30 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2703 projected Pulkovo 1995 / 3-degree Gauss-Kruger CM 33E 31.5,51.24,34.5,70.02 1 6200 +proj=tmerc +lat_0=0 +lon_0=33 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2704 projected Pulkovo 1995 / 3-degree Gauss-Kruger CM 36E 34.5,44.61,37.51,69.39 1 6200 +proj=tmerc +lat_0=0 +lon_0=36 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2705 projected Pulkovo 1995 / 3-degree Gauss-Kruger CM 39E 37.5,43.33,40.5,68.8 1 6200 +proj=tmerc +lat_0=0 +lon_0=39 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2706 projected Pulkovo 1995 / 3-degree Gauss-Kruger CM 42E 40.5,42.87,43.5,68.74 1 6200 +proj=tmerc +lat_0=0 +lon_0=42 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2707 projected Pulkovo 1995 / 3-degree Gauss-Kruger CM 45E 43.5,41.89,46.5,80.8 1 6200 +proj=tmerc +lat_0=0 +lon_0=45 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2708 projected Pulkovo 1995 / 3-degree Gauss-Kruger CM 48E 46.5,41.19,49.5,80.91 1 6200 +proj=tmerc +lat_0=0 +lon_0=48 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2709 projected Pulkovo 1995 / 3-degree Gauss-Kruger CM 51E 49.5,42.36,52.5,81.22 1 6200 +proj=tmerc +lat_0=0 +lon_0=51 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2710 projected Pulkovo 1995 / 3-degree Gauss-Kruger CM 54E 52.5,50.52,55.5,81.41 1 6200 +proj=tmerc +lat_0=0 +lon_0=54 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2711 projected Pulkovo 1995 / 3-degree Gauss-Kruger CM 57E 55.5,50.53,58.5,81.89 1 6200 +proj=tmerc +lat_0=0 +lon_0=57 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2712 projected Pulkovo 1995 / 3-degree Gauss-Kruger CM 60E 58.5,50.47,61.5,81.91 1 6200 +proj=tmerc +lat_0=0 +lon_0=60 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2713 projected Pulkovo 1995 / 3-degree Gauss-Kruger CM 63E 61.5,51.03,64.5,81.77 1 6200 +proj=tmerc +lat_0=0 +lon_0=63 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2714 projected Pulkovo 1995 / 3-degree Gauss-Kruger CM 66E 64.5,54.31,67.5,81.25 1 6200 +proj=tmerc +lat_0=0 +lon_0=66 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2715 projected Pulkovo 1995 / 3-degree Gauss-Kruger CM 69E 67.5,54.85,70.5,77.07 1 6200 +proj=tmerc +lat_0=0 +lon_0=69 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2716 projected Pulkovo 1995 / 3-degree Gauss-Kruger CM 72E 70.5,53.43,73.5,73.57 1 6200 +proj=tmerc +lat_0=0 +lon_0=72 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2717 projected Pulkovo 1995 / 3-degree Gauss-Kruger CM 75E 73.5,53.47,76.5,79.71 1 6200 +proj=tmerc +lat_0=0 +lon_0=75 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2718 projected Pulkovo 1995 / 3-degree Gauss-Kruger CM 78E 76.5,51.49,79.5,81.03 1 6200 +proj=tmerc +lat_0=0 +lon_0=78 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2719 projected Pulkovo 1995 / 3-degree Gauss-Kruger CM 81E 79.5,50.7,82.5,81.03 1 6200 +proj=tmerc +lat_0=0 +lon_0=81 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2720 projected Pulkovo 1995 / 3-degree Gauss-Kruger CM 84E 82.5,49.58,85.5,77.56 1 6200 +proj=tmerc +lat_0=0 +lon_0=84 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2721 projected Pulkovo 1995 / 3-degree Gauss-Kruger CM 87E 85.5,49.07,88.5,77.16 1 6200 +proj=tmerc +lat_0=0 +lon_0=87 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2722 projected Pulkovo 1995 / 3-degree Gauss-Kruger CM 90E 88.5,49.44,91.5,81.28 1 6200 +proj=tmerc +lat_0=0 +lon_0=90 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2723 projected Pulkovo 1995 / 3-degree Gauss-Kruger CM 93E 91.5,50.16,94.5,81.26 1 6200 +proj=tmerc +lat_0=0 +lon_0=93 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2724 projected Pulkovo 1995 / 3-degree Gauss-Kruger CM 96E 94.5,49.73,97.5,81.35 1 6200 +proj=tmerc +lat_0=0 +lon_0=96 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2725 projected Pulkovo 1995 / 3-degree Gauss-Kruger CM 99E 97.5,49.79,100.5,80.9 1 6200 +proj=tmerc +lat_0=0 +lon_0=99 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2726 projected Pulkovo 1995 / 3-degree Gauss-Kruger CM 102E 100.5,50.17,103.5,79.71 1 6200 +proj=tmerc +lat_0=0 +lon_0=102 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2727 projected Pulkovo 1995 / 3-degree Gauss-Kruger CM 105E 103.5,50.13,106.5,79.21 1 6200 +proj=tmerc +lat_0=0 +lon_0=105 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2728 projected Pulkovo 1995 / 3-degree Gauss-Kruger CM 108E 106.5,49.25,109.5,78.4 1 6200 +proj=tmerc +lat_0=0 +lon_0=108 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2729 projected Pulkovo 1995 / 3-degree Gauss-Kruger CM 111E 109.5,49.14,112.5,76.81 1 6200 +proj=tmerc +lat_0=0 +lon_0=111 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2730 projected Pulkovo 1995 / 3-degree Gauss-Kruger CM 114E 112.5,49.49,115.5,76.7 1 6200 +proj=tmerc +lat_0=0 +lon_0=114 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2731 projected Pulkovo 1995 / 3-degree Gauss-Kruger CM 117E 115.5,49.51,118.5,74.43 1 6200 +proj=tmerc +lat_0=0 +lon_0=117 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2732 projected Pulkovo 1995 / 3-degree Gauss-Kruger CM 120E 118.5,49.87,121.5,73.63 1 6200 +proj=tmerc +lat_0=0 +lon_0=120 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2733 projected Pulkovo 1995 / 3-degree Gauss-Kruger CM 123E 121.5,53.18,124.5,74 1 6200 +proj=tmerc +lat_0=0 +lon_0=123 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2734 projected Pulkovo 1995 / 3-degree Gauss-Kruger CM 126E 124.5,49.88,127.5,74 1 6200 +proj=tmerc +lat_0=0 +lon_0=126 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2735 projected Pulkovo 1995 / 3-degree Gauss-Kruger CM 129E 127.5,42.67,130.5,73.59 1 6200 +proj=tmerc +lat_0=0 +lon_0=129 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2736 projected Tete / UTM zone 36S 30.21,-26.87,36,-11.41 17 6127 +proj=utm +zone=36 +south +ellps=clrk66 +towgs84=-80,-100,-228 +units=m +no_defs +2737 projected Tete / UTM zone 37S 35.99,-18.98,40.9,-10.42 17 6127 +proj=utm +zone=37 +south +ellps=clrk66 +towgs84=-80,-100,-228 +units=m +no_defs +2738 projected Pulkovo 1995 / 3-degree Gauss-Kruger CM 132E 130.5,42.25,133.5,71.99 1 6200 +proj=tmerc +lat_0=0 +lon_0=132 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2739 projected Pulkovo 1995 / 3-degree Gauss-Kruger CM 135E 133.5,42.74,136.5,75.9 1 6200 +proj=tmerc +lat_0=0 +lon_0=135 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2740 projected Pulkovo 1995 / 3-degree Gauss-Kruger CM 138E 136.5,44.76,139.5,76.27 1 6200 +proj=tmerc +lat_0=0 +lon_0=138 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2741 projected Pulkovo 1995 / 3-degree Gauss-Kruger CM 141E 139.5,45.84,142.5,76.23 1 6200 +proj=tmerc +lat_0=0 +lon_0=141 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2742 projected Pulkovo 1995 / 3-degree Gauss-Kruger CM 144E 142.5,43.61,145.5,75.98 1 6200 +proj=tmerc +lat_0=0 +lon_0=144 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2743 projected Pulkovo 1995 / 3-degree Gauss-Kruger CM 147E 145.5,43.6,148.5,76.76 1 6200 +proj=tmerc +lat_0=0 +lon_0=147 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2744 projected Pulkovo 1995 / 3-degree Gauss-Kruger CM 150E 148.5,45.21,151.5,76.82 1 6200 +proj=tmerc +lat_0=0 +lon_0=150 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2745 projected Pulkovo 1995 / 3-degree Gauss-Kruger CM 153E 151.5,46.72,154.5,76.26 1 6200 +proj=tmerc +lat_0=0 +lon_0=153 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2746 projected Pulkovo 1995 / 3-degree Gauss-Kruger CM 156E 154.5,49.02,157.5,77.2 1 6200 +proj=tmerc +lat_0=0 +lon_0=156 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2747 projected Pulkovo 1995 / 3-degree Gauss-Kruger CM 159E 157.5,51.36,160.5,71.12 1 6200 +proj=tmerc +lat_0=0 +lon_0=159 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2748 projected Pulkovo 1995 / 3-degree Gauss-Kruger CM 162E 160.5,54.34,163.5,70.98 1 6200 +proj=tmerc +lat_0=0 +lon_0=162 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2749 projected Pulkovo 1995 / 3-degree Gauss-Kruger CM 165E 163.5,54.69,166.5,69.82 1 6200 +proj=tmerc +lat_0=0 +lon_0=165 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2750 projected Pulkovo 1995 / 3-degree Gauss-Kruger CM 168E 166.5,54.45,169.5,70.07 1 6200 +proj=tmerc +lat_0=0 +lon_0=168 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2751 projected Pulkovo 1995 / 3-degree Gauss-Kruger CM 171E 169.5,59.86,172.5,70.19 1 6200 +proj=tmerc +lat_0=0 +lon_0=171 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2752 projected Pulkovo 1995 / 3-degree Gauss-Kruger CM 174E 172.5,60.99,175.5,70.02 1 6200 +proj=tmerc +lat_0=0 +lon_0=174 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2753 projected Pulkovo 1995 / 3-degree Gauss-Kruger CM 177E 175.5,62.09,178.5,71.1 1 6200 +proj=tmerc +lat_0=0 +lon_0=177 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2754 projected Pulkovo 1995 / 3-degree Gauss-Kruger CM 180E 178.5,62.24,-178.5,71.65 1 6200 +proj=tmerc +lat_0=0 +lon_0=180 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2755 projected Pulkovo 1995 / 3-degree Gauss-Kruger CM 177W -178.5,64.74,-175.5,71.61 1 6200 +proj=tmerc +lat_0=0 +lon_0=-177 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2756 projected Pulkovo 1995 / 3-degree Gauss-Kruger CM 174W -175.5,64.2,-172.5,67.78 1 6200 +proj=tmerc +lat_0=0 +lon_0=-174 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2757 projected Pulkovo 1995 / 3-degree Gauss-Kruger CM 171W -172.5,64.35,-169.57,67.06 1 6200 +proj=tmerc +lat_0=0 +lon_0=-171 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2758 projected Pulkovo 1995 / 3-degree Gauss-Kruger CM 168W -169.22,65.7,-168.97,65.86 1 6200 +proj=tmerc +lat_0=0 +lon_0=-168 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +2759 projected NAD83(HARN) / Alabama East -86.79,30.99,-84.89,35 1 6152 +proj=tmerc +lat_0=30.5 +lon_0=-85.8333333333333 +k=0.99996 +x_0=200000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2760 projected NAD83(HARN) / Alabama West -88.48,30.14,-86.3,35.02 1 6152 +proj=tmerc +lat_0=30 +lon_0=-87.5 +k=0.999933333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2761 projected NAD83(HARN) / Arizona East -111.71,31.33,-109.04,37.01 1 6152 +proj=tmerc +lat_0=31 +lon_0=-110.166666666667 +k=0.9999 +x_0=213360 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2762 projected NAD83(HARN) / Arizona Central -113.35,31.33,-110.44,37.01 1 6152 +proj=tmerc +lat_0=31 +lon_0=-111.916666666667 +k=0.9999 +x_0=213360 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2763 projected NAD83(HARN) / Arizona West -114.81,32.05,-112.52,37 1 6152 +proj=tmerc +lat_0=31 +lon_0=-113.75 +k=0.999933333 +x_0=213360 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2764 projected NAD83(HARN) / Arkansas North -94.62,34.67,-89.64,36.5 1 6152 +proj=lcc +lat_0=34.3333333333333 +lon_0=-92 +lat_1=36.2333333333333 +lat_2=34.9333333333333 +x_0=400000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2765 projected NAD83(HARN) / Arkansas South -94.48,33.01,-90.4,35.1 1 6152 +proj=lcc +lat_0=32.6666666666667 +lon_0=-92 +lat_1=34.7666666666667 +lat_2=33.3 +x_0=400000 +y_0=400000 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2766 projected NAD83(HARN) / California zone 1 -124.45,39.59,-119.99,42.01 1 6152 +proj=lcc +lat_0=39.3333333333333 +lon_0=-122 +lat_1=41.6666666666667 +lat_2=40 +x_0=2000000 +y_0=500000 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2767 projected NAD83(HARN) / California zone 2 -124.06,38.02,-119.54,40.16 1 6152 +proj=lcc +lat_0=37.6666666666667 +lon_0=-122 +lat_1=39.8333333333333 +lat_2=38.3333333333333 +x_0=2000000 +y_0=500000 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2768 projected NAD83(HARN) / California zone 3 -123.02,36.73,-117.83,38.71 1 6152 +proj=lcc +lat_0=36.5 +lon_0=-120.5 +lat_1=38.4333333333333 +lat_2=37.0666666666667 +x_0=2000000 +y_0=500000 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2769 projected NAD83(HARN) / California zone 4 -122.01,35.78,-115.62,37.58 1 6152 +proj=lcc +lat_0=35.3333333333333 +lon_0=-119 +lat_1=37.25 +lat_2=36 +x_0=2000000 +y_0=500000 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2770 projected NAD83(HARN) / California zone 5 -121.42,32.76,-114.12,35.81 1 6152 +proj=lcc +lat_0=33.5 +lon_0=-118 +lat_1=35.4666666666667 +lat_2=34.0333333333333 +x_0=2000000 +y_0=500000 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2771 projected NAD83(HARN) / California zone 6 -118.15,32.53,-114.42,34.08 1 6152 +proj=lcc +lat_0=32.1666666666667 +lon_0=-116.25 +lat_1=33.8833333333333 +lat_2=32.7833333333333 +x_0=2000000 +y_0=500000 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2772 projected NAD83(HARN) / Colorado North -109.06,39.56,-102.04,41.01 1 6152 +proj=lcc +lat_0=39.3333333333333 +lon_0=-105.5 +lat_1=40.7833333333333 +lat_2=39.7166666666667 +x_0=914401.8289 +y_0=304800.6096 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2773 projected NAD83(HARN) / Colorado Central -109.06,38.14,-102.04,40.09 1 6152 +proj=lcc +lat_0=37.8333333333333 +lon_0=-105.5 +lat_1=39.75 +lat_2=38.45 +x_0=914401.8289 +y_0=304800.6096 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2774 projected NAD83(HARN) / Colorado South -109.06,36.98,-102.04,38.68 1 6152 +proj=lcc +lat_0=36.6666666666667 +lon_0=-105.5 +lat_1=38.4333333333333 +lat_2=37.2333333333333 +x_0=914401.8289 +y_0=304800.6096 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2775 projected NAD83(HARN) / Connecticut -73.73,40.98,-71.78,42.05 1 6152 +proj=lcc +lat_0=40.8333333333333 +lon_0=-72.75 +lat_1=41.8666666666667 +lat_2=41.2 +x_0=304800.6096 +y_0=152400.3048 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2776 projected NAD83(HARN) / Delaware -75.8,38.44,-74.97,39.85 1 6152 +proj=tmerc +lat_0=38 +lon_0=-75.4166666666667 +k=0.999995 +x_0=200000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2777 projected NAD83(HARN) / Florida East -82.33,24.41,-79.97,30.83 1 6152 +proj=tmerc +lat_0=24.3333333333333 +lon_0=-81 +k=0.999941177 +x_0=200000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2778 projected NAD83(HARN) / Florida West -83.34,26.27,-81.13,29.6 1 6152 +proj=tmerc +lat_0=24.3333333333333 +lon_0=-82 +k=0.999941177 +x_0=200000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2779 projected NAD83(HARN) / Florida North -87.63,29.21,-82.04,31.01 1 6152 +proj=lcc +lat_0=29 +lon_0=-84.5 +lat_1=30.75 +lat_2=29.5833333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2780 projected NAD83(HARN) / Georgia East -83.47,30.36,-80.77,34.68 1 6152 +proj=tmerc +lat_0=30 +lon_0=-82.1666666666667 +k=0.9999 +x_0=200000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2781 projected NAD83(HARN) / Georgia West -85.61,30.62,-82.99,35.01 1 6152 +proj=tmerc +lat_0=30 +lon_0=-84.1666666666667 +k=0.9999 +x_0=700000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2782 projected NAD83(HARN) / Hawaii zone 1 -156.1,18.87,-154.74,20.33 2 6152 +proj=tmerc +lat_0=18.8333333333333 +lon_0=-155.5 +k=0.999966667 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +2783 projected NAD83(HARN) / Hawaii zone 2 -157.36,20.45,-155.93,21.26 2 6152 +proj=tmerc +lat_0=20.3333333333333 +lon_0=-156.666666666667 +k=0.999966667 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +2784 projected NAD83(HARN) / Hawaii zone 3 -158.33,21.2,-157.61,21.75 2 6152 +proj=tmerc +lat_0=21.1666666666667 +lon_0=-158 +k=0.99999 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +2785 projected NAD83(HARN) / Hawaii zone 4 -159.85,21.81,-159.23,22.29 2 6152 +proj=tmerc +lat_0=21.8333333333333 +lon_0=-159.5 +k=0.99999 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +2786 projected NAD83(HARN) / Hawaii zone 5 -160.3,21.73,-159.99,22.07 2 6152 +proj=tmerc +lat_0=21.6666666666667 +lon_0=-160.166666666667 +k=1 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +2787 projected NAD83(HARN) / Idaho East -113.24,41.99,-111.04,44.75 1 6152 +proj=tmerc +lat_0=41.6666666666667 +lon_0=-112.166666666667 +k=0.999947368 +x_0=200000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2788 projected NAD83(HARN) / Idaho Central -115.3,41.99,-112.67,45.7 1 6152 +proj=tmerc +lat_0=41.6666666666667 +lon_0=-114 +k=0.999947368 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2789 projected NAD83(HARN) / Idaho West -117.24,41.99,-114.32,49.01 1 6152 +proj=tmerc +lat_0=41.6666666666667 +lon_0=-115.75 +k=0.999933333 +x_0=800000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2790 projected NAD83(HARN) / Illinois East -89.27,37.06,-87.02,42.5 1 6152 +proj=tmerc +lat_0=36.6666666666667 +lon_0=-88.3333333333333 +k=0.999975 +x_0=300000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2791 projected NAD83(HARN) / Illinois West -91.52,36.97,-88.92,42.51 1 6152 +proj=tmerc +lat_0=36.6666666666667 +lon_0=-90.1666666666667 +k=0.999941177 +x_0=700000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2792 projected NAD83(HARN) / Indiana East -86.59,37.95,-84.78,41.77 1 6152 +proj=tmerc +lat_0=37.5 +lon_0=-85.6666666666667 +k=0.999966667 +x_0=100000 +y_0=250000 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2793 projected NAD83(HARN) / Indiana West -88.1,37.77,-86.24,41.77 1 6152 +proj=tmerc +lat_0=37.5 +lon_0=-87.0833333333333 +k=0.999966667 +x_0=900000 +y_0=250000 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2794 projected NAD83(HARN) / Iowa North -96.65,41.85,-90.15,43.51 1 6152 +proj=lcc +lat_0=41.5 +lon_0=-93.5 +lat_1=43.2666666666667 +lat_2=42.0666666666667 +x_0=1500000 +y_0=1000000 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2795 projected NAD83(HARN) / Iowa South -96.14,40.36,-90.14,42.04 1 6152 +proj=lcc +lat_0=40 +lon_0=-93.5 +lat_1=41.7833333333333 +lat_2=40.6166666666667 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2796 projected NAD83(HARN) / Kansas North -102.06,38.52,-94.58,40.01 1 6152 +proj=lcc +lat_0=38.3333333333333 +lon_0=-98 +lat_1=39.7833333333333 +lat_2=38.7166666666667 +x_0=400000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2797 projected NAD83(HARN) / Kansas South -102.05,36.99,-94.6,38.88 1 6152 +proj=lcc +lat_0=36.6666666666667 +lon_0=-98.5 +lat_1=38.5666666666667 +lat_2=37.2666666666667 +x_0=400000 +y_0=400000 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2798 projected NAD83(HARN) / Kentucky North -85.96,37.71,-82.47,39.15 1 6152 +proj=lcc +lat_0=37.5 +lon_0=-84.25 +lat_1=37.9666666666667 +lat_2=38.9666666666667 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2799 projected NAD83(HARN) / Kentucky South -89.57,36.49,-81.95,38.17 1 6152 +proj=lcc +lat_0=36.3333333333333 +lon_0=-85.75 +lat_1=37.9333333333333 +lat_2=36.7333333333333 +x_0=500000 +y_0=500000 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2800 projected NAD83(HARN) / Louisiana North -94.05,30.85,-90.86,33.03 1 6152 +proj=lcc +lat_0=30.5 +lon_0=-92.5 +lat_1=32.6666666666667 +lat_2=31.1666666666667 +x_0=1000000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2801 projected NAD83(HARN) / Louisiana South -93.94,28.85,-88.75,31.07 1 6152 +proj=lcc +lat_0=28.5 +lon_0=-91.3333333333333 +lat_1=30.7 +lat_2=29.3 +x_0=1000000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2802 projected NAD83(HARN) / Maine East -70.03,43.88,-66.91,47.47 1 6152 +proj=tmerc +lat_0=43.6666666666667 +lon_0=-68.5 +k=0.9999 +x_0=300000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2803 projected NAD83(HARN) / Maine West -71.09,43.04,-69.26,46.58 1 6152 +proj=tmerc +lat_0=42.8333333333333 +lon_0=-70.1666666666667 +k=0.999966667 +x_0=900000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2804 projected NAD83(HARN) / Maryland -79.49,37.97,-74.97,39.73 1 6152 +proj=lcc +lat_0=37.6666666666667 +lon_0=-77 +lat_1=39.45 +lat_2=38.3 +x_0=400000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2805 projected NAD83(HARN) / Massachusetts Mainland -73.5,41.46,-69.86,42.89 1 6152 +proj=lcc +lat_0=41 +lon_0=-71.5 +lat_1=42.6833333333333 +lat_2=41.7166666666667 +x_0=200000 +y_0=750000 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2806 projected NAD83(HARN) / Massachusetts Island -70.91,41.19,-69.89,41.51 1 6152 +proj=lcc +lat_0=41 +lon_0=-70.5 +lat_1=41.4833333333333 +lat_2=41.2833333333333 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2807 projected NAD83(HARN) / Michigan North -90.42,45.08,-83.44,48.32 1 6152 +proj=lcc +lat_0=44.7833333333333 +lon_0=-87 +lat_1=47.0833333333333 +lat_2=45.4833333333333 +x_0=8000000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2808 projected NAD83(HARN) / Michigan Central -87.06,43.8,-82.27,45.92 1 6152 +proj=lcc +lat_0=43.3166666666667 +lon_0=-84.3666666666667 +lat_1=45.7 +lat_2=44.1833333333333 +x_0=6000000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2809 projected NAD83(HARN) / Michigan South -87.2,41.69,-82.13,44.22 1 6152 +proj=lcc +lat_0=41.5 +lon_0=-84.3666666666667 +lat_1=43.6666666666667 +lat_2=42.1 +x_0=4000000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2810 projected NAD83(HARN) / Minnesota North -97.22,46.64,-89.49,49.38 1 6152 +proj=lcc +lat_0=46.5 +lon_0=-93.1 +lat_1=48.6333333333333 +lat_2=47.0333333333333 +x_0=800000 +y_0=100000 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2811 projected NAD83(HARN) / Minnesota Central -96.86,45.28,-92.29,47.48 1 6152 +proj=lcc +lat_0=45 +lon_0=-94.25 +lat_1=47.05 +lat_2=45.6166666666667 +x_0=800000 +y_0=100000 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2812 projected NAD83(HARN) / Minnesota South -96.85,43.49,-91.21,45.59 1 6152 +proj=lcc +lat_0=43 +lon_0=-94 +lat_1=45.2166666666667 +lat_2=43.7833333333333 +x_0=800000 +y_0=100000 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2813 projected NAD83(HARN) / Mississippi East -89.97,30.01,-88.09,35.01 1 6152 +proj=tmerc +lat_0=29.5 +lon_0=-88.8333333333333 +k=0.99995 +x_0=300000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2814 projected NAD83(HARN) / Mississippi West -91.65,31,-89.37,35.01 1 6152 +proj=tmerc +lat_0=29.5 +lon_0=-90.3333333333333 +k=0.99995 +x_0=700000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2815 projected NAD83(HARN) / Missouri East -91.97,35.98,-89.1,40.61 1 6152 +proj=tmerc +lat_0=35.8333333333333 +lon_0=-90.5 +k=0.999933333 +x_0=250000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2816 projected NAD83(HARN) / Missouri Central -93.79,36.48,-91.41,40.61 1 6152 +proj=tmerc +lat_0=35.8333333333333 +lon_0=-92.5 +k=0.999933333 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2817 projected NAD83(HARN) / Missouri West -95.77,36.48,-93.48,40.59 1 6152 +proj=tmerc +lat_0=36.1666666666667 +lon_0=-94.5 +k=0.999941177 +x_0=850000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2818 projected NAD83(HARN) / Montana -116.07,44.35,-104.04,49.01 1 6152 +proj=lcc +lat_0=44.25 +lon_0=-109.5 +lat_1=49 +lat_2=45 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2819 projected NAD83(HARN) / Nebraska -104.06,39.99,-95.3,43.01 1 6152 +proj=lcc +lat_0=39.8333333333333 +lon_0=-100 +lat_1=43 +lat_2=40 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2820 projected NAD83(HARN) / Nevada East -117.01,34.99,-114.03,42 1 6152 +proj=tmerc +lat_0=34.75 +lon_0=-115.583333333333 +k=0.9999 +x_0=200000 +y_0=8000000 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2821 projected NAD83(HARN) / Nevada Central -118.19,36,-114.99,41 1 6152 +proj=tmerc +lat_0=34.75 +lon_0=-116.666666666667 +k=0.9999 +x_0=500000 +y_0=6000000 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2822 projected NAD83(HARN) / Nevada West -120,36.95,-116.99,42 1 6152 +proj=tmerc +lat_0=34.75 +lon_0=-118.583333333333 +k=0.9999 +x_0=800000 +y_0=4000000 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2823 projected NAD83(HARN) / New Hampshire -72.56,42.69,-70.63,45.31 1 6152 +proj=tmerc +lat_0=42.5 +lon_0=-71.6666666666667 +k=0.999966667 +x_0=300000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2824 projected NAD83(HARN) / New Jersey -75.6,38.87,-73.88,41.36 1 6152 +proj=tmerc +lat_0=38.8333333333333 +lon_0=-74.5 +k=0.9999 +x_0=150000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2825 projected NAD83(HARN) / New Mexico East -105.72,32,-102.99,37 1 6152 +proj=tmerc +lat_0=31 +lon_0=-104.333333333333 +k=0.999909091 +x_0=165000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2826 projected NAD83(HARN) / New Mexico Central -107.73,31.78,-104.84,37 1 6152 +proj=tmerc +lat_0=31 +lon_0=-106.25 +k=0.9999 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2827 projected NAD83(HARN) / New Mexico West -109.06,31.33,-106.32,37 1 6152 +proj=tmerc +lat_0=31 +lon_0=-107.833333333333 +k=0.999916667 +x_0=830000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2828 projected NAD83(HARN) / New York East -75.87,40.88,-73.23,45.02 1 6152 +proj=tmerc +lat_0=38.8333333333333 +lon_0=-74.5 +k=0.9999 +x_0=150000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2829 projected NAD83(HARN) / New York Central -77.75,41.99,-75.06,44.41 1 6152 +proj=tmerc +lat_0=40 +lon_0=-76.5833333333333 +k=0.9999375 +x_0=250000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2830 projected NAD83(HARN) / New York West -79.77,41.99,-77.36,43.64 1 6152 +proj=tmerc +lat_0=40 +lon_0=-78.5833333333333 +k=0.9999375 +x_0=350000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2831 projected NAD83(HARN) / New York Long Island -74.26,40.47,-71.8,41.3 1 6152 +proj=lcc +lat_0=40.1666666666667 +lon_0=-74 +lat_1=41.0333333333333 +lat_2=40.6666666666667 +x_0=300000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2832 projected NAD83(HARN) / North Dakota North -104.07,47.15,-96.83,49.01 1 6152 +proj=lcc +lat_0=47 +lon_0=-100.5 +lat_1=48.7333333333333 +lat_2=47.4333333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2833 projected NAD83(HARN) / North Dakota South -104.05,45.93,-96.55,47.83 1 6152 +proj=lcc +lat_0=45.6666666666667 +lon_0=-100.5 +lat_1=47.4833333333333 +lat_2=46.1833333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2834 projected NAD83(HARN) / Ohio North -84.81,40.1,-80.51,42.33 1 6152 +proj=lcc +lat_0=39.6666666666667 +lon_0=-82.5 +lat_1=41.7 +lat_2=40.4333333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2835 projected NAD83(HARN) / Ohio South -84.83,38.4,-80.7,40.36 1 6152 +proj=lcc +lat_0=38 +lon_0=-82.5 +lat_1=40.0333333333333 +lat_2=38.7333333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2836 projected NAD83(HARN) / Oklahoma North -103,35.27,-94.42,37.01 1 6152 +proj=lcc +lat_0=35 +lon_0=-98 +lat_1=36.7666666666667 +lat_2=35.5666666666667 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2837 projected NAD83(HARN) / Oklahoma South -100,33.62,-94.42,35.57 1 6152 +proj=lcc +lat_0=33.3333333333333 +lon_0=-98 +lat_1=35.2333333333333 +lat_2=33.9333333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2838 projected NAD83(HARN) / Oregon North -124.17,43.95,-116.47,46.26 1 6152 +proj=lcc +lat_0=43.6666666666667 +lon_0=-120.5 +lat_1=46 +lat_2=44.3333333333333 +x_0=2500000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2839 projected NAD83(HARN) / Oregon South -124.6,41.98,-116.9,44.56 1 6152 +proj=lcc +lat_0=41.6666666666667 +lon_0=-120.5 +lat_1=44 +lat_2=42.3333333333333 +x_0=1500000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2840 projected NAD83(HARN) / Rhode Island -71.85,41.13,-71.08,42.02 1 6152 +proj=tmerc +lat_0=41.0833333333333 +lon_0=-71.5 +k=0.99999375 +x_0=100000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2841 projected NAD83(HARN) / South Dakota North -104.07,44.14,-96.45,45.95 1 6152 +proj=lcc +lat_0=43.8333333333333 +lon_0=-100 +lat_1=45.6833333333333 +lat_2=44.4166666666667 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2842 projected NAD83(HARN) / South Dakota South -104.06,42.48,-96.43,44.79 1 6152 +proj=lcc +lat_0=42.3333333333333 +lon_0=-100.333333333333 +lat_1=44.4 +lat_2=42.8333333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2843 projected NAD83(HARN) / Tennessee -90.31,34.98,-81.65,36.68 1 6152 +proj=lcc +lat_0=34.3333333333333 +lon_0=-86 +lat_1=36.4166666666667 +lat_2=35.25 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2844 projected NAD83(HARN) / Texas North -103.03,34.3,-99.99,36.5 1 6152 +proj=lcc +lat_0=34 +lon_0=-101.5 +lat_1=36.1833333333333 +lat_2=34.65 +x_0=200000 +y_0=1000000 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2845 projected NAD83(HARN) / Texas North Central -103.07,31.72,-94,34.58 1 6152 +proj=lcc +lat_0=31.6666666666667 +lon_0=-98.5 +lat_1=33.9666666666667 +lat_2=32.1333333333333 +x_0=600000 +y_0=2000000 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2846 projected NAD83(HARN) / Texas Central -106.66,29.78,-93.5,32.27 1 6152 +proj=lcc +lat_0=29.6666666666667 +lon_0=-100.333333333333 +lat_1=31.8833333333333 +lat_2=30.1166666666667 +x_0=700000 +y_0=3000000 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2847 projected NAD83(HARN) / Texas South Central -105,27.78,-93.76,30.67 1 6152 +proj=lcc +lat_0=27.8333333333333 +lon_0=-99 +lat_1=30.2833333333333 +lat_2=28.3833333333333 +x_0=600000 +y_0=4000000 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2848 projected NAD83(HARN) / Texas South -100.2,25.83,-96.85,28.21 1 6152 +proj=lcc +lat_0=25.6666666666667 +lon_0=-98.5 +lat_1=27.8333333333333 +lat_2=26.1666666666667 +x_0=300000 +y_0=5000000 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2849 projected NAD83(HARN) / Utah North -114.04,40.55,-109.04,42.01 1 6152 +proj=lcc +lat_0=40.3333333333333 +lon_0=-111.5 +lat_1=41.7833333333333 +lat_2=40.7166666666667 +x_0=500000 +y_0=1000000 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2850 projected NAD83(HARN) / Utah Central -114.05,38.49,-109.04,41.08 1 6152 +proj=lcc +lat_0=38.3333333333333 +lon_0=-111.5 +lat_1=40.65 +lat_2=39.0166666666667 +x_0=500000 +y_0=2000000 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2851 projected NAD83(HARN) / Utah South -114.05,36.99,-109.04,38.58 1 6152 +proj=lcc +lat_0=36.6666666666667 +lon_0=-111.5 +lat_1=38.35 +lat_2=37.2166666666667 +x_0=500000 +y_0=3000000 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2852 projected NAD83(HARN) / Vermont -73.44,42.72,-71.5,45.03 1 6152 +proj=tmerc +lat_0=42.5 +lon_0=-72.5 +k=0.999964286 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2853 projected NAD83(HARN) / Virginia North -80.06,37.77,-76.51,39.46 1 6152 +proj=lcc +lat_0=37.6666666666667 +lon_0=-78.5 +lat_1=39.2 +lat_2=38.0333333333333 +x_0=3500000 +y_0=2000000 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2854 projected NAD83(HARN) / Virginia South -83.68,36.54,-75.31,38.28 1 6152 +proj=lcc +lat_0=36.3333333333333 +lon_0=-78.5 +lat_1=37.9666666666667 +lat_2=36.7666666666667 +x_0=3500000 +y_0=1000000 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2855 projected NAD83(HARN) / Washington North -124.79,47.08,-117.02,49.05 1 6152 +proj=lcc +lat_0=47 +lon_0=-120.833333333333 +lat_1=48.7333333333333 +lat_2=47.5 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2856 projected NAD83(HARN) / Washington South -124.4,45.54,-116.91,47.61 1 6152 +proj=lcc +lat_0=45.3333333333333 +lon_0=-120.5 +lat_1=47.3333333333333 +lat_2=45.8333333333333 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2857 projected NAD83(HARN) / West Virginia North -81.76,38.76,-77.72,40.64 1 6152 +proj=lcc +lat_0=38.5 +lon_0=-79.5 +lat_1=40.25 +lat_2=39 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2858 projected NAD83(HARN) / West Virginia South -82.65,37.2,-79.05,39.17 1 6152 +proj=lcc +lat_0=37 +lon_0=-81 +lat_1=38.8833333333333 +lat_2=37.4833333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2859 projected NAD83(HARN) / Wisconsin North -92.89,45.37,-88.05,47.31 1 6152 +proj=lcc +lat_0=45.1666666666667 +lon_0=-90 +lat_1=46.7666666666667 +lat_2=45.5666666666667 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2860 projected NAD83(HARN) / Wisconsin Central -92.89,43.98,-86.25,45.8 1 6152 +proj=lcc +lat_0=43.8333333333333 +lon_0=-90 +lat_1=45.5 +lat_2=44.25 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2861 projected NAD83(HARN) / Wisconsin South -91.43,42.48,-86.95,44.33 1 6152 +proj=lcc +lat_0=42 +lon_0=-90 +lat_1=44.0666666666667 +lat_2=42.7333333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2862 projected NAD83(HARN) / Wyoming East -106.33,40.99,-104.05,45.01 1 6152 +proj=tmerc +lat_0=40.5 +lon_0=-105.166666666667 +k=0.9999375 +x_0=200000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2863 projected NAD83(HARN) / Wyoming East Central -108.63,40.99,-106,45.01 1 6152 +proj=tmerc +lat_0=40.5 +lon_0=-107.333333333333 +k=0.9999375 +x_0=400000 +y_0=100000 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2864 projected NAD83(HARN) / Wyoming West Central -111.06,40.99,-107.5,45.01 1 6152 +proj=tmerc +lat_0=40.5 +lon_0=-108.75 +k=0.9999375 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2865 projected NAD83(HARN) / Wyoming West -111.06,40.99,-109.04,44.67 1 6152 +proj=tmerc +lat_0=40.5 +lon_0=-110.083333333333 +k=0.9999375 +x_0=800000 +y_0=100000 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2866 projected NAD83(HARN) / Puerto Rico and Virgin Is. -67.97,17.62,-64.51,18.57 2 6152 +proj=lcc +lat_0=17.8333333333333 +lon_0=-66.4333333333333 +lat_1=18.4333333333333 +lat_2=18.0333333333333 +x_0=200000 +y_0=200000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +2867 projected NAD83(HARN) / Arizona East (ft) -111.71,31.33,-109.04,37.01 1 6152 +proj=tmerc +lat_0=31 +lon_0=-110.166666666667 +k=0.9999 +x_0=213360 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.3048 +no_defs +2868 projected NAD83(HARN) / Arizona Central (ft) -113.35,31.33,-110.44,37.01 1 6152 +proj=tmerc +lat_0=31 +lon_0=-111.916666666667 +k=0.9999 +x_0=213360 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.3048 +no_defs +2869 projected NAD83(HARN) / Arizona West (ft) -114.81,32.05,-112.52,37 1 6152 +proj=tmerc +lat_0=31 +lon_0=-113.75 +k=0.999933333 +x_0=213360 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.3048 +no_defs +2870 projected NAD83(HARN) / California zone 1 (ftUS) -124.45,39.59,-119.99,42.01 1 6152 +proj=lcc +lat_0=39.3333333333333 +lon_0=-122 +lat_1=41.6666666666667 +lat_2=40 +x_0=2000000.0001016 +y_0=500000.0001016 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +2871 projected NAD83(HARN) / California zone 2 (ftUS) -124.06,38.02,-119.54,40.16 1 6152 +proj=lcc +lat_0=37.6666666666667 +lon_0=-122 +lat_1=39.8333333333333 +lat_2=38.3333333333333 +x_0=2000000.0001016 +y_0=500000.0001016 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +2872 projected NAD83(HARN) / California zone 3 (ftUS) -123.02,36.73,-117.83,38.71 1 6152 +proj=lcc +lat_0=36.5 +lon_0=-120.5 +lat_1=38.4333333333333 +lat_2=37.0666666666667 +x_0=2000000.0001016 +y_0=500000.0001016 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +2873 projected NAD83(HARN) / California zone 4 (ftUS) -122.01,35.78,-115.62,37.58 1 6152 +proj=lcc +lat_0=35.3333333333333 +lon_0=-119 +lat_1=37.25 +lat_2=36 +x_0=2000000.0001016 +y_0=500000.0001016 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +2874 projected NAD83(HARN) / California zone 5 (ftUS) -121.42,32.76,-114.12,35.81 1 6152 +proj=lcc +lat_0=33.5 +lon_0=-118 +lat_1=35.4666666666667 +lat_2=34.0333333333333 +x_0=2000000.0001016 +y_0=500000.0001016 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +2875 projected NAD83(HARN) / California zone 6 (ftUS) -118.15,32.53,-114.42,34.08 1 6152 +proj=lcc +lat_0=32.1666666666667 +lon_0=-116.25 +lat_1=33.8833333333333 +lat_2=32.7833333333333 +x_0=2000000.0001016 +y_0=500000.0001016 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +2876 projected NAD83(HARN) / Colorado North (ftUS) -109.06,39.56,-102.04,41.01 1 6152 +proj=lcc +lat_0=39.3333333333333 +lon_0=-105.5 +lat_1=40.7833333333333 +lat_2=39.7166666666667 +x_0=914401.828803658 +y_0=304800.609601219 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +2877 projected NAD83(HARN) / Colorado Central (ftUS) -109.06,38.14,-102.04,40.09 1 6152 +proj=lcc +lat_0=37.8333333333333 +lon_0=-105.5 +lat_1=39.75 +lat_2=38.45 +x_0=914401.828803658 +y_0=304800.609601219 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +2878 projected NAD83(HARN) / Colorado South (ftUS) -109.06,36.98,-102.04,38.68 1 6152 +proj=lcc +lat_0=36.6666666666667 +lon_0=-105.5 +lat_1=38.4333333333333 +lat_2=37.2333333333333 +x_0=914401.828803658 +y_0=304800.609601219 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +2879 projected NAD83(HARN) / Connecticut (ftUS) -73.73,40.98,-71.78,42.05 1 6152 +proj=lcc +lat_0=40.8333333333333 +lon_0=-72.75 +lat_1=41.8666666666667 +lat_2=41.2 +x_0=304800.609601219 +y_0=152400.30480061 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +2880 projected NAD83(HARN) / Delaware (ftUS) -75.8,38.44,-74.97,39.85 1 6152 +proj=tmerc +lat_0=38 +lon_0=-75.4166666666667 +k=0.999995 +x_0=200000.0001016 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +2881 projected NAD83(HARN) / Florida East (ftUS) -82.33,24.41,-79.97,30.83 1 6152 +proj=tmerc +lat_0=24.3333333333333 +lon_0=-81 +k=0.999941177 +x_0=200000.0001016 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +2882 projected NAD83(HARN) / Florida West (ftUS) -83.34,26.27,-81.13,29.6 1 6152 +proj=tmerc +lat_0=24.3333333333333 +lon_0=-82 +k=0.999941177 +x_0=200000.0001016 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +2883 projected NAD83(HARN) / Florida North (ftUS) -87.63,29.21,-82.04,31.01 1 6152 +proj=lcc +lat_0=29 +lon_0=-84.5 +lat_1=30.75 +lat_2=29.5833333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +2884 projected NAD83(HARN) / Georgia East (ftUS) -83.47,30.36,-80.77,34.68 1 6152 +proj=tmerc +lat_0=30 +lon_0=-82.1666666666667 +k=0.9999 +x_0=200000.0001016 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +2885 projected NAD83(HARN) / Georgia West (ftUS) -85.61,30.62,-82.99,35.01 1 6152 +proj=tmerc +lat_0=30 +lon_0=-84.1666666666667 +k=0.9999 +x_0=699999.9998984 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +2886 projected NAD83(HARN) / Idaho East (ftUS) -113.24,41.99,-111.04,44.75 1 6152 +proj=tmerc +lat_0=41.6666666666667 +lon_0=-112.166666666667 +k=0.999947368 +x_0=200000.0001016 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +2887 projected NAD83(HARN) / Idaho Central (ftUS) -115.3,41.99,-112.67,45.7 1 6152 +proj=tmerc +lat_0=41.6666666666667 +lon_0=-114 +k=0.999947368 +x_0=500000.0001016 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +2888 projected NAD83(HARN) / Idaho West (ftUS) -117.24,41.99,-114.32,49.01 1 6152 +proj=tmerc +lat_0=41.6666666666667 +lon_0=-115.75 +k=0.999933333 +x_0=800000.0001016 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +2891 projected NAD83(HARN) / Kentucky North (ftUS) -85.96,37.71,-82.47,39.15 1 6152 +proj=lcc +lat_0=37.5 +lon_0=-84.25 +lat_1=37.9666666666667 +lat_2=38.9666666666667 +x_0=500000.0001016 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +2892 projected NAD83(HARN) / Kentucky South (ftUS) -89.57,36.49,-81.95,38.17 1 6152 +proj=lcc +lat_0=36.3333333333333 +lon_0=-85.75 +lat_1=37.9333333333333 +lat_2=36.7333333333333 +x_0=500000.0001016 +y_0=500000.0001016 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +2893 projected NAD83(HARN) / Maryland (ftUS) -79.49,37.97,-74.97,39.73 1 6152 +proj=lcc +lat_0=37.6666666666667 +lon_0=-77 +lat_1=39.45 +lat_2=38.3 +x_0=399999.9998984 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +2894 projected NAD83(HARN) / Massachusetts Mainland (ftUS) -73.5,41.46,-69.86,42.89 1 6152 +proj=lcc +lat_0=41 +lon_0=-71.5 +lat_1=42.6833333333333 +lat_2=41.7166666666667 +x_0=200000.0001016 +y_0=750000 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +2895 projected NAD83(HARN) / Massachusetts Island (ftUS) -70.91,41.19,-69.89,41.51 1 6152 +proj=lcc +lat_0=41 +lon_0=-70.5 +lat_1=41.4833333333333 +lat_2=41.2833333333333 +x_0=500000.0001016 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +2896 projected NAD83(HARN) / Michigan North (ft) -90.42,45.08,-83.44,48.32 1 6152 +proj=lcc +lat_0=44.7833333333333 +lon_0=-87 +lat_1=47.0833333333333 +lat_2=45.4833333333333 +x_0=7999999.999968 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.3048 +no_defs +2897 projected NAD83(HARN) / Michigan Central (ft) -87.06,43.8,-82.27,45.92 1 6152 +proj=lcc +lat_0=43.3166666666667 +lon_0=-84.3666666666667 +lat_1=45.7 +lat_2=44.1833333333333 +x_0=5999999.999976 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.3048 +no_defs +2898 projected NAD83(HARN) / Michigan South (ft) -87.2,41.69,-82.13,44.22 1 6152 +proj=lcc +lat_0=41.5 +lon_0=-84.3666666666667 +lat_1=43.6666666666667 +lat_2=42.1 +x_0=3999999.999984 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.3048 +no_defs +2899 projected NAD83(HARN) / Mississippi East (ftUS) -89.97,30.01,-88.09,35.01 1 6152 +proj=tmerc +lat_0=29.5 +lon_0=-88.8333333333333 +k=0.99995 +x_0=300000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +2900 projected NAD83(HARN) / Mississippi West (ftUS) -91.65,31,-89.37,35.01 1 6152 +proj=tmerc +lat_0=29.5 +lon_0=-90.3333333333333 +k=0.99995 +x_0=699999.9998984 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +2901 projected NAD83(HARN) / Montana (ft) -116.07,44.35,-104.04,49.01 1 6152 +proj=lcc +lat_0=44.25 +lon_0=-109.5 +lat_1=49 +lat_2=45 +x_0=599999.9999976 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.3048 +no_defs +2902 projected NAD83(HARN) / New Mexico East (ftUS) -105.72,32,-102.99,37 1 6152 +proj=tmerc +lat_0=31 +lon_0=-104.333333333333 +k=0.999909091 +x_0=165000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +2903 projected NAD83(HARN) / New Mexico Central (ftUS) -107.73,31.78,-104.84,37 1 6152 +proj=tmerc +lat_0=31 +lon_0=-106.25 +k=0.9999 +x_0=500000.0001016 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +2904 projected NAD83(HARN) / New Mexico West (ftUS) -109.06,31.33,-106.32,37 1 6152 +proj=tmerc +lat_0=31 +lon_0=-107.833333333333 +k=0.999916667 +x_0=830000.0001016 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +2905 projected NAD83(HARN) / New York East (ftUS) -75.87,40.88,-73.23,45.02 1 6152 +proj=tmerc +lat_0=38.8333333333333 +lon_0=-74.5 +k=0.9999 +x_0=150000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +2906 projected NAD83(HARN) / New York Central (ftUS) -77.75,41.99,-75.06,44.41 1 6152 +proj=tmerc +lat_0=40 +lon_0=-76.5833333333333 +k=0.9999375 +x_0=249999.9998984 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +2907 projected NAD83(HARN) / New York West (ftUS) -79.77,41.99,-77.36,43.64 1 6152 +proj=tmerc +lat_0=40 +lon_0=-78.5833333333333 +k=0.9999375 +x_0=350000.0001016 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +2908 projected NAD83(HARN) / New York Long Island (ftUS) -74.26,40.47,-71.8,41.3 1 6152 +proj=lcc +lat_0=40.1666666666667 +lon_0=-74 +lat_1=41.0333333333333 +lat_2=40.6666666666667 +x_0=300000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +2909 projected NAD83(HARN) / North Dakota North (ft) -104.07,47.15,-96.83,49.01 1 6152 +proj=lcc +lat_0=47 +lon_0=-100.5 +lat_1=48.7333333333333 +lat_2=47.4333333333333 +x_0=599999.9999976 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.3048 +no_defs +2910 projected NAD83(HARN) / North Dakota South (ft) -104.05,45.93,-96.55,47.83 1 6152 +proj=lcc +lat_0=45.6666666666667 +lon_0=-100.5 +lat_1=47.4833333333333 +lat_2=46.1833333333333 +x_0=599999.9999976 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.3048 +no_defs +2911 projected NAD83(HARN) / Oklahoma North (ftUS) -103,35.27,-94.42,37.01 1 6152 +proj=lcc +lat_0=35 +lon_0=-98 +lat_1=36.7666666666667 +lat_2=35.5666666666667 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +2912 projected NAD83(HARN) / Oklahoma South (ftUS) -100,33.62,-94.42,35.57 1 6152 +proj=lcc +lat_0=33.3333333333333 +lon_0=-98 +lat_1=35.2333333333333 +lat_2=33.9333333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +2913 projected NAD83(HARN) / Oregon North (ft) -124.17,43.95,-116.47,46.26 1 6152 +proj=lcc +lat_0=43.6666666666667 +lon_0=-120.5 +lat_1=46 +lat_2=44.3333333333333 +x_0=2500000.0001424 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.3048 +no_defs +2914 projected NAD83(HARN) / Oregon South (ft) -124.6,41.98,-116.9,44.56 1 6152 +proj=lcc +lat_0=41.6666666666667 +lon_0=-120.5 +lat_1=44 +lat_2=42.3333333333333 +x_0=1500000.0001464 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.3048 +no_defs +2915 projected NAD83(HARN) / Tennessee (ftUS) -90.31,34.98,-81.65,36.68 1 6152 +proj=lcc +lat_0=34.3333333333333 +lon_0=-86 +lat_1=36.4166666666667 +lat_2=35.25 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +2916 projected NAD83(HARN) / Texas North (ftUS) -103.03,34.3,-99.99,36.5 1 6152 +proj=lcc +lat_0=34 +lon_0=-101.5 +lat_1=36.1833333333333 +lat_2=34.65 +x_0=200000.0001016 +y_0=999999.9998984 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +2917 projected NAD83(HARN) / Texas North Central (ftUS) -103.07,31.72,-94,34.58 1 6152 +proj=lcc +lat_0=31.6666666666667 +lon_0=-98.5 +lat_1=33.9666666666667 +lat_2=32.1333333333333 +x_0=600000 +y_0=2000000.0001016 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +2918 projected NAD83(HARN) / Texas Central (ftUS) -106.66,29.78,-93.5,32.27 1 6152 +proj=lcc +lat_0=29.6666666666667 +lon_0=-100.333333333333 +lat_1=31.8833333333333 +lat_2=30.1166666666667 +x_0=699999.9998984 +y_0=3000000 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +2919 projected NAD83(HARN) / Texas South Central (ftUS) -105,27.78,-93.76,30.67 1 6152 +proj=lcc +lat_0=27.8333333333333 +lon_0=-99 +lat_1=30.2833333333333 +lat_2=28.3833333333333 +x_0=600000 +y_0=3999999.9998984 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +2920 projected NAD83(HARN) / Texas South (ftUS) -100.2,25.83,-96.85,28.21 1 6152 +proj=lcc +lat_0=25.6666666666667 +lon_0=-98.5 +lat_1=27.8333333333333 +lat_2=26.1666666666667 +x_0=300000 +y_0=5000000.0001016 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +2921 projected NAD83(HARN) / Utah North (ft) -114.04,40.55,-109.04,42.01 1 6152 +proj=lcc +lat_0=40.3333333333333 +lon_0=-111.5 +lat_1=41.7833333333333 +lat_2=40.7166666666667 +x_0=500000.0001504 +y_0=999999.999996 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.3048 +no_defs +2922 projected NAD83(HARN) / Utah Central (ft) -114.05,38.49,-109.04,41.08 1 6152 +proj=lcc +lat_0=38.3333333333333 +lon_0=-111.5 +lat_1=40.65 +lat_2=39.0166666666667 +x_0=500000.0001504 +y_0=1999999.999992 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.3048 +no_defs +2923 projected NAD83(HARN) / Utah South (ft) -114.05,36.99,-109.04,38.58 1 6152 +proj=lcc +lat_0=36.6666666666667 +lon_0=-111.5 +lat_1=38.35 +lat_2=37.2166666666667 +x_0=500000.0001504 +y_0=2999999.999988 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.3048 +no_defs +2924 projected NAD83(HARN) / Virginia North (ftUS) -80.06,37.77,-76.51,39.46 1 6152 +proj=lcc +lat_0=37.6666666666667 +lon_0=-78.5 +lat_1=39.2 +lat_2=38.0333333333333 +x_0=3500000.0001016 +y_0=2000000.0001016 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +2925 projected NAD83(HARN) / Virginia South (ftUS) -83.68,36.54,-75.31,38.28 1 6152 +proj=lcc +lat_0=36.3333333333333 +lon_0=-78.5 +lat_1=37.9666666666667 +lat_2=36.7666666666667 +x_0=3500000.0001016 +y_0=999999.9998984 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +2926 projected NAD83(HARN) / Washington North (ftUS) -124.79,47.08,-117.02,49.05 1 6152 +proj=lcc +lat_0=47 +lon_0=-120.833333333333 +lat_1=48.7333333333333 +lat_2=47.5 +x_0=500000.0001016 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +2927 projected NAD83(HARN) / Washington South (ftUS) -124.4,45.54,-116.91,47.61 1 6152 +proj=lcc +lat_0=45.3333333333333 +lon_0=-120.5 +lat_1=47.3333333333333 +lat_2=45.8333333333333 +x_0=500000.0001016 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +2928 projected NAD83(HARN) / Wisconsin North (ftUS) -92.89,45.37,-88.05,47.31 1 6152 +proj=lcc +lat_0=45.1666666666667 +lon_0=-90 +lat_1=46.7666666666667 +lat_2=45.5666666666667 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +2929 projected NAD83(HARN) / Wisconsin Central (ftUS) -92.89,43.98,-86.25,45.8 1 6152 +proj=lcc +lat_0=43.8333333333333 +lon_0=-90 +lat_1=45.5 +lat_2=44.25 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +2930 projected NAD83(HARN) / Wisconsin South (ftUS) -91.43,42.48,-86.95,44.33 1 6152 +proj=lcc +lat_0=42 +lon_0=-90 +lat_1=44.0666666666667 +lat_2=42.7333333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +2931 projected Beduaram / TM 13 NE 7.81,12.8,14.9,16.7 15 6213 +proj=tmerc +lat_0=0 +lon_0=13 +k=0.9996 +x_0=500000 +y_0=0 +ellps=clrk80ign +towgs84=-106,-87,188 +units=m +no_defs +2932 projected QND95 / Qatar National Grid 50.69,24.55,51.68,26.2 0 6614 +proj=tmerc +lat_0=24.45 +lon_0=51.2166666666667 +k=0.99999 +x_0=200000 +y_0=300000 +ellps=intl +towgs84=-119.4248,-303.65872,-11.00061,1.164298,0.174458,1.096259,3.657065 +units=m +no_defs +2933 projected Segara / UTM zone 50S 116.72,-1.24,117.99,0 1 6613 +proj=utm +zone=50 +south +ellps=bessel +towgs84=-404.78,685.68,45.47 +units=m +no_defs +2935 projected Pulkovo 1942 / CS63 zone A1 39.99,41.37,43.04,43.59 3 6284 +proj=tmerc +lat_0=0.116666666666667 +lon_0=41.5333333333333 +k=1 +x_0=1300000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2936 projected Pulkovo 1942 / CS63 zone A2 43.03,38.87,46.04,43.05 4.5 6284 +proj=tmerc +lat_0=0.116666666666667 +lon_0=44.5333333333333 +k=1 +x_0=2300000 +y_0=0 +ellps=krass +towgs84=25,-141,-78.5,0,0.35,0.736,0 +units=m +no_defs +2937 projected Pulkovo 1942 / CS63 zone A3 46.03,38.38,49.04,42.1 4.5 6284 +proj=tmerc +lat_0=0.116666666666667 +lon_0=47.5333333333333 +k=1 +x_0=3300000 +y_0=0 +ellps=krass +towgs84=25,-141,-78.5,0,0.35,0.736,0 +units=m +no_defs +2938 projected Pulkovo 1942 / CS63 zone A4 49.03,37.89,51.73,42.59 4.5 6284 +proj=tmerc +lat_0=0.116666666666667 +lon_0=50.5333333333333 +k=1 +x_0=4300000 +y_0=0 +ellps=krass +towgs84=25,-141,-78.5,0,0.35,0.736,0 +units=m +no_defs +2939 projected Pulkovo 1942 / CS63 zone K2 49.26,41.15,52.27,51.77 4.5 6284 +proj=tmerc +lat_0=0.133333333333333 +lon_0=50.7666666666667 +k=1 +x_0=2300000 +y_0=0 +ellps=krass +towgs84=25,-141,-78.5,0,0.35,0.736,0 +units=m +no_defs +2940 projected Pulkovo 1942 / CS63 zone K3 52.26,41.46,55.27,51.79 3 6284 +proj=tmerc +lat_0=0.133333333333333 +lon_0=53.7666666666667 +k=1 +x_0=3300000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2941 projected Pulkovo 1942 / CS63 zone K4 55.26,41.26,58.27,51.14 3 6284 +proj=tmerc +lat_0=0.133333333333333 +lon_0=56.7666666666667 +k=1 +x_0=4300000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +2942 projected Porto Santo / UTM zone 28N -17.31,32.35,-16.23,33.15 44 6615 +proj=utm +zone=28 +ellps=intl +towgs84=-499,-249,314 +units=m +no_defs +2943 projected Selvagem Grande / UTM zone 28N -16.11,29.98,-15.79,30.21 44 6616 +proj=utm +zone=28 +ellps=intl +towgs84=-289,-124,60 +units=m +no_defs +2945 projected NAD83(CSRS) / MTM zone 3 -60,47.5,-57.1,55.38 1 6140 +proj=tmerc +lat_0=0 +lon_0=-58.5 +k=0.9999 +x_0=304800 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2946 projected NAD83(CSRS) / MTM zone 4 -63,47.16,-60,58.92 1 6140 +proj=tmerc +lat_0=0 +lon_0=-61.5 +k=0.9999 +x_0=304800 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2947 projected NAD83(CSRS) / MTM zone 5 -66,47.95,-63,60.52 1 6140 +proj=tmerc +lat_0=0 +lon_0=-64.5 +k=0.9999 +x_0=304800 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2948 projected NAD83(CSRS) / MTM zone 6 -69,47.31,-66,59 1 6140 +proj=tmerc +lat_0=0 +lon_0=-67.5 +k=0.9999 +x_0=304800 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2949 projected NAD83(CSRS) / MTM zone 7 -72,45.01,-69,61.8 1 6140 +proj=tmerc +lat_0=0 +lon_0=-70.5 +k=0.9999 +x_0=304800 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2950 projected NAD83(CSRS) / MTM zone 8 -75,44.98,-72,62.53 1 6140 +proj=tmerc +lat_0=0 +lon_0=-73.5 +k=0.9999 +x_0=304800 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2951 projected NAD83(CSRS) / MTM zone 9 -78,43.63,-75,62.65 1 6140 +proj=tmerc +lat_0=0 +lon_0=-76.5 +k=0.9999 +x_0=304800 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2952 projected NAD83(CSRS) / MTM zone 10 -81,42.26,-78,62.45 1 6140 +proj=tmerc +lat_0=0 +lon_0=-79.5 +k=0.9999 +x_0=304800 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2953 projected NAD83(CSRS) / New Brunswick Stereographic -69.05,44.56,-63.7,48.07 1 6140 +proj=sterea +lat_0=46.5 +lon_0=-66.5 +k=0.999912 +x_0=2500000 +y_0=7500000 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2954 projected NAD83(CSRS) / Prince Edward Isl. Stereographic (NAD83) -64.49,45.9,-61.9,47.09 1 6140 +proj=sterea +lat_0=47.25 +lon_0=-63 +k=0.999912 +x_0=400000 +y_0=800000 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2955 projected NAD83(CSRS) / UTM zone 11N -120,48.99,-114,83.5 1 6140 +proj=utm +zone=11 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2956 projected NAD83(CSRS) / UTM zone 12N -114,48.99,-108,84 1 6140 +proj=utm +zone=12 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2957 projected NAD83(CSRS) / UTM zone 13N -108,48.99,-102,84 1 6140 +proj=utm +zone=13 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2958 projected NAD83(CSRS) / UTM zone 17N -84,41.67,-78,84 1 6140 +proj=utm +zone=17 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2959 projected NAD83(CSRS) / UTM zone 18N -78,43.63,-72,84 1 6140 +proj=utm +zone=18 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2960 projected NAD83(CSRS) / UTM zone 19N -72,40.8,-66,84 1 6140 +proj=utm +zone=19 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2961 projected NAD83(CSRS) / UTM zone 20N -66,38.21,-59.99,84 1 6140 +proj=utm +zone=20 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2962 projected NAD83(CSRS) / UTM zone 21N -60,38.56,-54,84 1 6140 +proj=utm +zone=21 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2964 projected NAD27 / Alaska Albers 172.42,51.3,-129.99,71.4 12 6267 +proj=aea +lat_0=50 +lon_0=-154 +lat_1=55 +lat_2=65 +x_0=0 +y_0=0 +ellps=clrk66 +towgs84=-5,135,172 +to_meter=0.304800609601219 +no_defs +2965 projected NAD83 / Indiana East (ftUS) -86.59,37.95,-84.78,41.77 4 6269 +proj=tmerc +lat_0=37.5 +lon_0=-85.6666666666667 +k=0.999966667 +x_0=99999.9998983998 +y_0=249999.9998984 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +2966 projected NAD83 / Indiana West (ftUS) -88.1,37.77,-86.24,41.77 4 6269 +proj=tmerc +lat_0=37.5 +lon_0=-87.0833333333333 +k=0.999966667 +x_0=900000 +y_0=249999.9998984 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +2967 projected NAD83(HARN) / Indiana East (ftUS) -86.59,37.95,-84.78,41.77 1 6152 +proj=tmerc +lat_0=37.5 +lon_0=-85.6666666666667 +k=0.999966667 +x_0=99999.9998983998 +y_0=249999.9998984 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +2968 projected NAD83(HARN) / Indiana West (ftUS) -88.1,37.77,-86.24,41.77 1 6152 +proj=tmerc +lat_0=37.5 +lon_0=-87.0833333333333 +k=0.999966667 +x_0=900000 +y_0=249999.9998984 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +2969 projected Fort Marigot / UTM zone 20N -63.21,17.82,-62.73,18.17 10 6621 +proj=utm +zone=20 +ellps=intl +towgs84=137,248,-430 +units=m +no_defs +2970 projected Guadeloupe 1948 / UTM zone 20N -61.85,15.8,-60.97,16.55 0.1 6622 +proj=utm +zone=20 +ellps=intl +towgs84=-472.29,-5.63,-304.12,0.4362,-0.8374,0.2563,1.8984 +units=m +no_defs +2971 projected CSG67 / UTM zone 22N -54,3.43,-51.61,5.81 10 6623 +proj=utm +zone=22 +ellps=intl +towgs84=-186,230,110 +units=m +no_defs +2972 projected RGFG95 / UTM zone 22N -54,2.17,-49.45,8.88 1 6624 +proj=utm +zone=22 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +2973 projected Martinique 1938 / UTM zone 20N -61.29,14.35,-60.76,14.93 0.1 6625 +proj=utm +zone=20 +ellps=intl +towgs84=126.93,547.94,130.41,-2.7867,5.1612,-0.8584,13.8227 +units=m +no_defs +2975 projected RGR92 / UTM zone 40S 54,-24.72,58.24,-18.28 1 6627 +proj=utm +zone=40 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +2976 projected Tahiti 52 / UTM zone 6S -150,-17.93,-149.09,-17.41 10 6628 +proj=utm +zone=6 +south +ellps=intl +towgs84=162,117,154 +units=m +no_defs +2977 projected Tahaa 54 / UTM zone 5S -151.91,-16.96,-150.89,-16.17 1 6629 +proj=utm +zone=5 +south +ellps=intl +towgs84=72.51,345.411,79.241,-1.5862,-0.8826,-0.5495,1.3653 +units=m +no_defs +2978 projected IGN72 Nuku Hiva / UTM zone 7S -140.31,-9.57,-139.44,-8.72 10 6630 +proj=utm +zone=7 +south +ellps=intl +towgs84=84,274,65 +units=m +no_defs +2980 projected Combani 1950 / UTM zone 38S 44.98,-13.05,45.35,-12.61 10 6632 +proj=utm +zone=38 +south +ellps=intl +towgs84=-382,-59,-262 +units=m +no_defs +2981 projected IGN56 Lifou / UTM zone 58S 166.98,-21.24,167.52,-20.62 1 6633 +proj=utm +zone=58 +south +ellps=intl +towgs84=335.47,222.58,-230.94 +units=m +no_defs +2985 projected Petrels 1972 / Terre Adelie Polar Stereographic 139.44,-66.78,141.5,-66.1 10 6636 +proj=stere +lat_0=-90 +lat_ts=-67 +lon_0=140 +x_0=300000 +y_0=-2299363.4878305923 +ellps=intl +towgs84=365,194,166 +units=m +no_defs +2986 projected Perroud 1950 / Terre Adelie Polar Stereographic 136,-67.13,142,-65.61 10 6637 +proj=stere +lat_0=-90 +lat_ts=-67 +lon_0=140 +x_0=300000 +y_0=-2299363.4878305923 +ellps=intl +towgs84=325,154,172 +units=m +no_defs +2987 projected Saint Pierre et Miquelon 1950 / UTM zone 21N -56.48,46.69,-56.07,47.19 1 6638 +proj=utm +zone=21 +ellps=clrk66 +towgs84=11.363,424.148,373.13 +units=m +no_defs +2988 projected MOP78 / UTM zone 1S -176.25,-13.41,-176.07,-13.16 10 6639 +proj=utm +zone=1 +south +ellps=intl +towgs84=253,-132,-127 +units=m +no_defs +2991 projected NAD83 / Oregon LCC (m) -124.6,41.98,-116.47,46.26 4 6269 +proj=lcc +lat_0=41.75 +lon_0=-120.5 +lat_1=43 +lat_2=45.5 +x_0=400000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +2992 projected NAD83 / Oregon GIC Lambert (ft) -124.6,41.98,-116.47,46.26 4 6269 +proj=lcc +lat_0=41.75 +lon_0=-120.5 +lat_1=43 +lat_2=45.5 +x_0=399999.9999984 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +2993 projected NAD83(HARN) / Oregon LCC (m) -124.6,41.98,-116.47,46.26 1 6152 +proj=lcc +lat_0=41.75 +lon_0=-120.5 +lat_1=43 +lat_2=45.5 +x_0=400000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +2994 projected NAD83(HARN) / Oregon GIC Lambert (ft) -124.6,41.98,-116.47,46.26 1 6152 +proj=lcc +lat_0=41.75 +lon_0=-120.5 +lat_1=43 +lat_2=45.5 +x_0=399999.9999984 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.3048 +no_defs +2995 projected IGN53 Mare / UTM zone 58S 167.75,-21.71,168,-21.32 2 6641 +proj=utm +zone=58 +south +ellps=intl +towgs84=287.58,177.78,-135.41 +units=m +no_defs +2996 projected ST84 Ile des Pins / UTM zone 58S 167.36,-22.73,167.61,-22.49 10 6642 +proj=utm +zone=58 +south +ellps=intl +towgs84=-13,-348,292 +units=m +no_defs +2997 projected ST71 Belep / UTM zone 58S 163.54,-19.85,163.75,-19.5 1 6643 +proj=utm +zone=58 +south +ellps=intl +towgs84=-480.26,-438.32,-643.429,16.3119,20.1721,-4.0349,-111.7002 +units=m +no_defs +2998 projected NEA74 Noumea / UTM zone 58S 166.35,-22.37,166.54,-22.19 1 6644 +proj=utm +zone=58 +south +ellps=intl +towgs84=-10.18,-350.43,291.37 +units=m +no_defs +2999 projected Grand Comoros / UTM zone 38S 43.16,-11.99,43.55,-11.31 6646 +proj=utm +zone=38 +south +ellps=intl +towgs84=-963,510,-359 +units=m +no_defs +3000 projected Segara / NEIEZ 114.55,-4.24,119.06,4.29 5 6613 +proj=merc +lon_0=110 +k=0.997 +x_0=3900000 +y_0=900000 +ellps=bessel +towgs84=-387.06,636.53,46.29 +units=m +no_defs +3001 projected Batavia / NEIEZ 95.16,-8.91,115.77,5.97 6 6211 +proj=merc +lon_0=110 +k=0.997 +x_0=3900000 +y_0=900000 +ellps=bessel +towgs84=-377,681,-50 +units=m +no_defs +3002 projected Makassar / NEIEZ 118.71,-6.54,120.78,-1.88 6257 +proj=merc +lon_0=110 +k=0.997 +x_0=3900000 +y_0=900000 +ellps=bessel +towgs84=-587.8,519.75,145.76 +units=m +no_defs +3003 projected Monte Mario / Italy zone 1 5.93,36.53,12,47.04 4 6265 +proj=tmerc +lat_0=0 +lon_0=9 +k=0.9996 +x_0=1500000 +y_0=0 +ellps=intl +towgs84=-104.1,-49.1,-9.9,0.971,-2.917,0.714,-11.68 +units=m +no_defs +3004 projected Monte Mario / Italy zone 2 12,34.76,18.99,47.1 4 6265 +proj=tmerc +lat_0=0 +lon_0=15 +k=0.9996 +x_0=2520000 +y_0=0 +ellps=intl +towgs84=-104.1,-49.1,-9.9,0.971,-2.917,0.714,-11.68 +units=m +no_defs +3005 projected NAD83 / BC Albers -139.04,48.25,-114.08,60.01 4 6269 +proj=aea +lat_0=45 +lon_0=-126 +lat_1=50 +lat_2=58.5 +x_0=1000000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3006 projected SWEREF99 TM 10.03,54.96,24.17,69.07 1 6619 +proj=utm +zone=33 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3007 projected SWEREF99 12 00 10.93,56.74,13.11,60.13 1 6619 +proj=tmerc +lat_0=0 +lon_0=12 +k=1 +x_0=150000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3008 projected SWEREF99 13 30 12.12,55.28,14.79,62.28 1 6619 +proj=tmerc +lat_0=0 +lon_0=13.5 +k=1 +x_0=150000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3009 projected SWEREF99 15 00 13.54,55.95,16.15,61.62 1 6619 +proj=tmerc +lat_0=0 +lon_0=15 +k=1 +x_0=150000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3010 projected SWEREF99 16 30 15.41,56.15,17.63,62.26 1 6619 +proj=tmerc +lat_0=0 +lon_0=16.5 +k=1 +x_0=150000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3011 projected SWEREF99 18 00 17.08,58.66,19.61,60.7 1 6619 +proj=tmerc +lat_0=0 +lon_0=18 +k=1 +x_0=150000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3012 projected SWEREF99 14 15 11.93,61.55,15.55,64.39 1 6619 +proj=tmerc +lat_0=0 +lon_0=14.25 +k=1 +x_0=150000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3013 projected SWEREF99 15 45 13.66,60.44,17.01,65.13 1 6619 +proj=tmerc +lat_0=0 +lon_0=15.75 +k=1 +x_0=150000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3014 projected SWEREF99 17 15 14.31,62.12,19.04,67.19 1 6619 +proj=tmerc +lat_0=0 +lon_0=17.25 +k=1 +x_0=150000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3015 projected SWEREF99 18 45 17.18,56.86,20.22,66.17 1 6619 +proj=tmerc +lat_0=0 +lon_0=18.75 +k=1 +x_0=150000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3016 projected SWEREF99 20 15 16.08,63.45,23.28,69.07 1 6619 +proj=tmerc +lat_0=0 +lon_0=20.25 +k=1 +x_0=150000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3017 projected SWEREF99 21 45 19.63,65.01,22.91,66.43 1 6619 +proj=tmerc +lat_0=0 +lon_0=21.75 +k=1 +x_0=150000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3018 projected SWEREF99 23 15 21.85,65.49,24.17,68.14 1 6619 +proj=tmerc +lat_0=0 +lon_0=23.25 +k=1 +x_0=150000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3019 projected RT90 7.5 gon V 10.93,57.29,12.91,59.73 1 6124 +proj=tmerc +lat_0=0 +lon_0=11.3082777777778 +k=1 +x_0=1500000 +y_0=0 +ellps=bessel +towgs84=414.1,41.3,603.1,-0.855,2.141,-7.023,0 +units=m +no_defs +3020 projected RT90 5 gon V 11.81,55.28,15.44,64.39 1 6124 +proj=tmerc +lat_0=0 +lon_0=13.5582777777778 +k=1 +x_0=1500000 +y_0=0 +ellps=bessel +towgs84=414.1,41.3,603.1,-0.855,2.141,-7.023,0 +units=m +no_defs +3021 projected RT90 2.5 gon V 13.66,55.95,17.73,67.18 1 6124 +proj=tmerc +lat_0=0 +lon_0=15.8082777777778 +k=1 +x_0=1500000 +y_0=0 +ellps=bessel +towgs84=414.1,41.3,603.1,-0.855,2.141,-7.023,0 +units=m +no_defs +3022 projected RT90 0 gon 16.08,56.86,20.22,68.54 1 6124 +proj=tmerc +lat_0=0 +lon_0=18.0582777777778 +k=1 +x_0=1500000 +y_0=0 +ellps=bessel +towgs84=414.1,41.3,603.1,-0.855,2.141,-7.023,0 +units=m +no_defs +3023 projected RT90 2.5 gon O 18.4,63.37,22.2,69.07 1 6124 +proj=tmerc +lat_0=0 +lon_0=20.3082777777778 +k=1 +x_0=1500000 +y_0=0 +ellps=bessel +towgs84=414.1,41.3,603.1,-0.855,2.141,-7.023,0 +units=m +no_defs +3024 projected RT90 5 gon O 21.34,65.24,24.17,68.58 1 6124 +proj=tmerc +lat_0=0 +lon_0=22.5582777777778 +k=1 +x_0=1500000 +y_0=0 +ellps=bessel +towgs84=414.1,41.3,603.1,-0.855,2.141,-7.023,0 +units=m +no_defs +3025 projected RT38 7.5 gon V 10.93,57.29,12.91,59.73 6308 +proj=tmerc +lat_0=0 +lon_0=11.3082777777778 +k=1 +x_0=1500000 +y_0=0 +ellps=bessel +units=m +no_defs +3026 projected RT38 5 gon V 11.81,55.28,15.44,64.39 6308 +proj=tmerc +lat_0=0 +lon_0=13.5582777777778 +k=1 +x_0=1500000 +y_0=0 +ellps=bessel +units=m +no_defs +3027 projected RT38 2.5 gon V 13.66,55.95,17.73,67.18 6308 +proj=tmerc +lat_0=0 +lon_0=15.8082777777778 +k=1 +x_0=1500000 +y_0=0 +ellps=bessel +units=m +no_defs +3028 projected RT38 0 gon 16.08,56.86,20.22,68.54 6308 +proj=tmerc +lat_0=0 +lon_0=18.0582777777778 +k=1 +x_0=1500000 +y_0=0 +ellps=bessel +units=m +no_defs +3029 projected RT38 2.5 gon O 18.4,63.37,22.2,69.07 6308 +proj=tmerc +lat_0=0 +lon_0=20.3082777777778 +k=1 +x_0=1500000 +y_0=0 +ellps=bessel +units=m +no_defs +3030 projected RT38 5 gon O 21.34,65.24,24.17,68.58 6308 +proj=tmerc +lat_0=0 +lon_0=22.5582777777778 +k=1 +x_0=1500000 +y_0=0 +ellps=bessel +units=m +no_defs +3031 projected WGS 84 / Antarctic Polar Stereographic -180,-90,180,-60 6326 +proj=stere +lat_0=-90 +lat_ts=-71 +lon_0=0 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3032 projected WGS 84 / Australian Antarctic Polar Stereographic 45,-90,160,-60 6326 +proj=stere +lat_0=-90 +lat_ts=-71 +lon_0=70 +x_0=6000000 +y_0=6000000 +datum=WGS84 +units=m +no_defs +3033 projected WGS 84 / Australian Antarctic Lambert 45,-80,160,-60 6326 +proj=lcc +lat_0=-50 +lon_0=70 +lat_1=-68.5 +lat_2=-74.5 +x_0=6000000 +y_0=6000000 +datum=WGS84 +units=m +no_defs +3034 projected ETRS89-extended / LCC Europe -35.58,24.6,44.83,84.73 1 6258 +proj=lcc +lat_0=52 +lon_0=10 +lat_1=35 +lat_2=65 +x_0=4000000 +y_0=2800000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3035 projected ETRS89-extended / LAEA Europe -35.58,24.6,44.83,84.73 1 6258 +proj=laea +lat_0=52 +lon_0=10 +x_0=4321000 +y_0=3210000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3036 projected Moznet / UTM zone 36S 30.21,-27.58,36,-11.41 1 6130 +proj=utm +zone=36 +south +ellps=WGS84 +towgs84=0,0,0 +units=m +no_defs +3037 projected Moznet / UTM zone 37S 36,-27.71,42,-10.09 1 6130 +proj=utm +zone=37 +south +ellps=WGS84 +towgs84=0,0,0 +units=m +no_defs +3040 projected ETRS89 / UTM zone 28N (N-E) -16.1,34.93,-11.99,72.44 1 6258 +proj=utm +zone=28 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3041 projected ETRS89 / UTM zone 29N (N-E) -12.01,34.91,-6,74.13 1 6258 +proj=utm +zone=29 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3042 projected ETRS89 / UTM zone 30N (N-E) -6.01,35.26,0,80.49 1 6258 +proj=utm +zone=30 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3043 projected ETRS89 / UTM zone 31N (N-E) 0,37,6.01,82.45 1 6258 +proj=utm +zone=31 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3044 projected ETRS89 / UTM zone 32N (N-E) 6,36.53,12.01,84.01 1 6258 +proj=utm +zone=32 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3045 projected ETRS89 / UTM zone 33N (N-E) 12,34.49,18.01,84.01 1 6258 +proj=utm +zone=33 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3046 projected ETRS89 / UTM zone 34N (N-E) 18,33.59,24.01,84.01 1 6258 +proj=utm +zone=34 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3047 projected ETRS89 / UTM zone 35N (N-E) 24,33.26,30.01,84.01 1 6258 +proj=utm +zone=35 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3048 projected ETRS89 / UTM zone 36N (N-E) 30,33.91,36.01,84.01 1 6258 +proj=utm +zone=36 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3049 projected ETRS89 / UTM zone 37N (N-E) 36,72.99,38.01,79.07 1 6258 +proj=utm +zone=37 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3052 projected Reykjavik 1900 / Lambert 1900 -24.66,63.34,-13.38,66.59 10 6657 +proj=lcc +axis=wnu +lat_1=65 +lat_0=65 +lon_0=-19.022125 +k_0=1 +x_0=0 +y_0=0 +a=6377019.27 +rf=300 +towgs84=-28,199,5 +units=m +no_defs +3053 projected Hjorsey 1955 / Lambert 1955 -24.66,63.34,-13.38,66.59 7 6658 +proj=lcc +axis=wnu +lat_1=65 +lat_0=65 +lon_0=-18 +k_0=1 +x_0=-500000 +y_0=500000 +ellps=intl +towgs84=-73,47,-83 +units=m +no_defs +3054 projected Hjorsey 1955 / UTM zone 26N -24.66,64.71,-24,65.85 7 6658 +proj=utm +zone=26 +ellps=intl +towgs84=-73,47,-83 +units=m +no_defs +3055 projected Hjorsey 1955 / UTM zone 27N -24,63.34,-18,66.52 7 6658 +proj=utm +zone=27 +ellps=intl +towgs84=-73,47,-83 +units=m +no_defs +3056 projected Hjorsey 1955 / UTM zone 28N -18,63.45,-13.38,66.58 7 6658 +proj=utm +zone=28 +ellps=intl +towgs84=-73,47,-83 +units=m +no_defs +3057 projected ISN93 / Lambert 1993 -30.87,59.96,-5.55,69.59 1 6659 +proj=lcc +lat_0=65 +lon_0=-19 +lat_1=64.25 +lat_2=65.75 +x_0=500000 +y_0=500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3058 projected Helle 1954 / Jan Mayen Grid -9.17,70.75,-7.87,71.24 1 6660 +proj=tmerc +lat_0=0 +lon_0=-8.5 +k=1 +x_0=50000 +y_0=-7800000 +ellps=intl +towgs84=982.6087,552.753,-540.873,6.68162662527694,-31.6114924086422,-19.8481610048168,16.805 +units=m +no_defs +3059 projected LKS-92 / Latvia TM 19.06,55.67,28.24,58.09 1 6661 +proj=tmerc +lat_0=0 +lon_0=24 +k=0.9996 +x_0=500000 +y_0=-6000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3060 projected IGN72 Grande Terre / UTM zone 58S 163.92,-22.45,167.09,-20.03 2 6634 +proj=utm +zone=58 +south +ellps=intl +towgs84=-11.64,-348.6,291.98 +units=m +no_defs +3061 projected Porto Santo 1995 / UTM zone 28N -17.31,32.35,-16.23,33.15 1 6663 +proj=utm +zone=28 +ellps=intl +towgs84=-210.502,-66.902,-48.476,2.094,-15.067,-5.817,0.485 +units=m +no_defs +3062 projected Azores Oriental 1995 / UTM zone 26N -25.92,36.87,-24.72,37.96 1 6664 +proj=utm +zone=26 +ellps=intl +towgs84=-208.719,129.685,52.092,-0.195,-0.014,0.327,0.198 +units=m +no_defs +3063 projected Azores Central 1995 / UTM zone 26N -28.9,38.32,-26.97,39.14 1 6665 +proj=utm +zone=26 +ellps=intl +towgs84=-103.088,162.481,-28.276,0.167,0.082,0.168,-1.504 +units=m +no_defs +3064 projected IGM95 / UTM zone 32N 5.93,36.53,12,47.04 1 6670 +proj=utm +zone=32 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3065 projected IGM95 / UTM zone 33N 12,34.79,18,47.1 1 6670 +proj=utm +zone=33 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3066 projected ED50 / Jordan TM 34.88,29.18,39.31,33.38 2.5 6230 +proj=tmerc +lat_0=0 +lon_0=37 +k=0.9998 +x_0=500000 +y_0=-3000000 +ellps=intl +towgs84=-112,-110.3,-140.2 +units=m +no_defs +3067 projected EUREF-FIN / TM35FIN(E,N) 19.08,58.84,31.59,70.09 1 1391 +proj=utm +zone=35 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3068 projected DHDN / Soldner Berlin 13.09,52.33,13.76,52.65 2 6314 +proj=cass +lat_0=52.4186482777778 +lon_0=13.6272036666667 +x_0=40000 +y_0=10000 +ellps=bessel +towgs84=612.4,77,440.2,-0.054,0.057,-2.797,2.55 +units=m +no_defs +3069 projected NAD27 / Wisconsin Transverse Mercator -92.89,42.48,-86.25,47.31 10 6267 +proj=tmerc +lat_0=0 +lon_0=-90 +k=0.9996 +x_0=500000 +y_0=-4500000 +ellps=clrk66 +towgs84=-8,160,176 +units=m +no_defs +3070 projected NAD83 / Wisconsin Transverse Mercator -92.89,42.48,-86.25,47.31 4 6269 +proj=tmerc +lat_0=0 +lon_0=-90 +k=0.9996 +x_0=520000 +y_0=-4480000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3071 projected NAD83(HARN) / Wisconsin Transverse Mercator -92.89,42.48,-86.25,47.31 1 6152 +proj=tmerc +lat_0=0 +lon_0=-90 +k=0.9996 +x_0=520000 +y_0=-4480000 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +3072 projected NAD83 / Maine CS2000 East -68.58,44.18,-66.91,47.37 4 6269 +proj=tmerc +lat_0=43.8333333333333 +lon_0=-67.875 +k=0.99998 +x_0=700000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3074 projected NAD83 / Maine CS2000 West -71.09,43.07,-69.6,46.58 4 6269 +proj=tmerc +lat_0=42.8333333333333 +lon_0=-70.375 +k=0.99998 +x_0=300000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3075 projected NAD83(HARN) / Maine CS2000 East -68.58,44.18,-66.91,47.37 1 6152 +proj=tmerc +lat_0=43.8333333333333 +lon_0=-67.875 +k=0.99998 +x_0=700000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +3077 projected NAD83(HARN) / Maine CS2000 West -71.09,43.07,-69.6,46.58 1 6152 +proj=tmerc +lat_0=42.8333333333333 +lon_0=-70.375 +k=0.99998 +x_0=300000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +3078 projected NAD83 / Michigan Oblique Mercator -90.42,41.69,-82.13,48.32 4 6269 +proj=omerc +no_uoff +lat_0=45.3091666666667 +lonc=-86 +alpha=337.25556 +gamma=337.25556 +k=0.9996 +x_0=2546731.496 +y_0=-4354009.816 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3079 projected NAD83(HARN) / Michigan Oblique Mercator -90.42,41.69,-82.13,48.32 1 6152 +proj=omerc +no_uoff +lat_0=45.3091666666667 +lonc=-86 +alpha=337.25556 +gamma=337.25556 +k=0.9996 +x_0=2546731.496 +y_0=-4354009.816 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +3080 projected NAD27 / Shackleford -106.66,25.83,-93.5,36.5 7 6267 +proj=lcc +lat_0=31.1666666666667 +lon_0=-100 +lat_1=27.4166666666667 +lat_2=34.9166666666667 +x_0=914400 +y_0=914400 +ellps=clrk66 +towgs84=-8,159,175 +to_meter=0.3048 +no_defs +3081 projected NAD83 / Texas State Mapping System -106.66,25.83,-93.5,36.5 4 6269 +proj=lcc +lat_0=31.1666666666667 +lon_0=-100 +lat_1=27.4166666666667 +lat_2=34.9166666666667 +x_0=1000000 +y_0=1000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3082 projected NAD83 / Texas Centric Lambert Conformal -106.66,25.83,-93.5,36.5 4 6269 +proj=lcc +lat_0=18 +lon_0=-100 +lat_1=27.5 +lat_2=35 +x_0=1500000 +y_0=5000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3083 projected NAD83 / Texas Centric Albers Equal Area -106.66,25.83,-93.5,36.5 4 6269 +proj=aea +lat_0=18 +lon_0=-100 +lat_1=27.5 +lat_2=35 +x_0=1500000 +y_0=6000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3084 projected NAD83(HARN) / Texas Centric Lambert Conformal -106.66,25.83,-93.5,36.5 1 6152 +proj=lcc +lat_0=18 +lon_0=-100 +lat_1=27.5 +lat_2=35 +x_0=1500000 +y_0=5000000 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +3085 projected NAD83(HARN) / Texas Centric Albers Equal Area -106.66,25.83,-93.5,36.5 1 6152 +proj=aea +lat_0=18 +lon_0=-100 +lat_1=27.5 +lat_2=35 +x_0=1500000 +y_0=6000000 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +3086 projected NAD83 / Florida GDL Albers -87.63,24.41,-79.97,31.01 4 6269 +proj=aea +lat_0=24 +lon_0=-84 +lat_1=24 +lat_2=31.5 +x_0=400000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3087 projected NAD83(HARN) / Florida GDL Albers -87.63,24.41,-79.97,31.01 1 6152 +proj=aea +lat_0=24 +lon_0=-84 +lat_1=24 +lat_2=31.5 +x_0=400000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +3088 projected NAD83 / Kentucky Single Zone -89.57,36.49,-81.95,39.15 4 6269 +proj=lcc +lat_0=36.3333333333333 +lon_0=-85.75 +lat_1=37.0833333333333 +lat_2=38.6666666666667 +x_0=1500000 +y_0=1000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3089 projected NAD83 / Kentucky Single Zone (ftUS) -89.57,36.49,-81.95,39.15 4 6269 +proj=lcc +lat_0=36.3333333333333 +lon_0=-85.75 +lat_1=37.0833333333333 +lat_2=38.6666666666667 +x_0=1500000 +y_0=999999.9998984 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3090 projected NAD83(HARN) / Kentucky Single Zone -89.57,36.49,-81.95,39.15 1 6152 +proj=lcc +lat_0=36.3333333333333 +lon_0=-85.75 +lat_1=37.0833333333333 +lat_2=38.6666666666667 +x_0=1500000 +y_0=1000000 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +3091 projected NAD83(HARN) / Kentucky Single Zone (ftUS) -89.57,36.49,-81.95,39.15 1 6152 +proj=lcc +lat_0=36.3333333333333 +lon_0=-85.75 +lat_1=37.0833333333333 +lat_2=38.6666666666667 +x_0=1500000 +y_0=999999.9998984 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +3092 projected Tokyo / UTM zone 51N 123.62,23.98,125.51,24.94 9 6301 +proj=utm +zone=51 +ellps=bessel +towgs84=-146.414,507.337,680.507 +units=m +no_defs +3093 projected Tokyo / UTM zone 52N 126.63,24.4,132,34.9 9 6301 +proj=utm +zone=52 +ellps=bessel +towgs84=-146.414,507.337,680.507 +units=m +no_defs +3094 projected Tokyo / UTM zone 53N 132,20.37,138,37.58 9 6301 +proj=utm +zone=53 +ellps=bessel +towgs84=-146.414,507.337,680.507 +units=m +no_defs +3095 projected Tokyo / UTM zone 54N 138,24.67,144,45.54 9 6301 +proj=utm +zone=54 +ellps=bessel +towgs84=-146.414,507.337,680.507 +units=m +no_defs +3096 projected Tokyo / UTM zone 55N 144,42.84,145.87,44.4 9 6301 +proj=utm +zone=55 +ellps=bessel +towgs84=-146.414,507.337,680.507 +units=m +no_defs +3097 projected JGD2000 / UTM zone 51N 122.38,21.1,126,29.71 1 6612 +proj=utm +zone=51 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3098 projected JGD2000 / UTM zone 52N 126,21.12,132,38.63 1 6612 +proj=utm +zone=52 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3099 projected JGD2000 / UTM zone 53N 132,17.09,138,43.55 1 6612 +proj=utm +zone=53 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3100 projected JGD2000 / UTM zone 54N 138,17.63,144,46.05 1 6612 +proj=utm +zone=54 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3101 projected JGD2000 / UTM zone 55N 144,23.03,147.86,45.65 1 6612 +proj=utm +zone=55 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3102 projected American Samoa 1962 / American Samoa Lambert -170.88,-14.43,-169.38,-14.11 44 6169 +proj=lcc +lat_1=-14.2666666666667 +lat_0=-14.2666666666667 +lon_0=-170 +k_0=1 +x_0=152400.30480061 +y_0=95169.3116586233 +ellps=clrk66 +towgs84=-115,118,426 +to_meter=0.304800609601219 +no_defs +3106 projected Gulshan 303 / TM 90 NE 88.01,20.52,92.67,26.64 1 6682 +proj=tmerc +lat_0=0 +lon_0=90 +k=0.9996 +x_0=500000 +y_0=0 +ellps=evrst30 +towgs84=283.729,735.942,261.143 +units=m +no_defs +3107 projected GDA94 / SA Lambert 128.99,-38.13,141.01,-25.99 3 6283 +proj=lcc +lat_0=-32 +lon_0=135 +lat_1=-28 +lat_2=-36 +x_0=1000000 +y_0=2000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3108 projected ETRS89 / Guernsey Grid -3.06,49.21,-2.03,49.94 1 6258 +proj=tmerc +lat_0=49.5 +lon_0=-2.41666666666667 +k=0.999997 +x_0=47000 +y_0=50000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3109 projected ETRS89 / Jersey Transverse Mercator -2.56,48.87,-1.81,49.44 1 6258 +proj=tmerc +lat_0=49.225 +lon_0=-2.135 +k=0.9999999 +x_0=40000 +y_0=70000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3110 projected AGD66 / Vicgrid66 140.96,-39.2,150.04,-33.98 3 6202 +proj=lcc +lat_0=-37 +lon_0=145 +lat_1=-36 +lat_2=-38 +x_0=2500000 +y_0=4500000 +ellps=aust_SA +towgs84=-117.808,-51.536,137.784,0.303,0.446,0.234,-0.29 +units=m +no_defs +3111 projected GDA94 / Vicgrid 140.96,-39.2,150.04,-33.98 3 6283 +proj=lcc +lat_0=-37 +lon_0=145 +lat_1=-36 +lat_2=-38 +x_0=2500000 +y_0=2500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3112 projected GDA94 / Geoscience Australia Lambert 112.85,-43.7,153.69,-9.86 3 6283 +proj=lcc +lat_0=0 +lon_0=134 +lat_1=-18 +lat_2=-36 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3113 projected GDA94 / BCSG02 152.37,-29.36,153.69,-24.64 3 6283 +proj=tmerc +lat_0=-28 +lon_0=153 +k=0.99999 +x_0=50000 +y_0=100000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3114 projected MAGNA-SIRGAS / Colombia Far West zone -79.1,1.23,-78.58,2.48 1 6686 +proj=tmerc +lat_0=4.59620041666667 +lon_0=-80.0775079166667 +k=1 +x_0=1000000 +y_0=1000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3115 projected MAGNA-SIRGAS / Colombia West zone -78.59,0.03,-75.58,10.21 1 6686 +proj=tmerc +lat_0=4.59620041666667 +lon_0=-77.0775079166667 +k=1 +x_0=1000000 +y_0=1000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3116 projected MAGNA-SIRGAS / Colombia Bogota zone -75.59,-2.51,-72.58,11.82 1 6686 +proj=tmerc +lat_0=4.59620041666667 +lon_0=-74.0775079166667 +k=1 +x_0=1000000 +y_0=1000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3117 projected MAGNA-SIRGAS / Colombia East Central zone -72.59,-4.23,-69.58,12.52 1 6686 +proj=tmerc +lat_0=4.59620041666667 +lon_0=-71.0775079166667 +k=1 +x_0=1000000 +y_0=1000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3118 projected MAGNA-SIRGAS / Colombia East zone -69.59,-2.25,-66.87,6.31 1 6686 +proj=tmerc +lat_0=4.59620041666667 +lon_0=-68.0775079166667 +k=1 +x_0=1000000 +y_0=1000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3119 projected Douala 1948 / AEF west 8.45,2.16,10.4,4.99 10 6192 +proj=tmerc +lat_0=0 +lon_0=10.5 +k=0.999 +x_0=1000000 +y_0=1000000 +ellps=intl +towgs84=-206.1,-174.7,-87.7 +units=m +no_defs +3120 projected Pulkovo 1942(58) / Poland zone I 18,49,24.15,52.34 1 6179 +proj=sterea +lat_0=50.625 +lon_0=21.0833333333333 +k=0.9998 +x_0=4637000 +y_0=5467000 +ellps=krass +towgs84=33.4,-146.6,-76.3,-0.359,-0.053,0.844,-0.84 +units=m +no_defs +3121 projected PRS92 / Philippines zone 1 116.04,6.21,118,18.64 0.05 6683 +proj=tmerc +lat_0=0 +lon_0=117 +k=0.99995 +x_0=500000 +y_0=0 +ellps=clrk66 +towgs84=-127.62,-67.24,-47.04,-3.068,4.903,1.578,-1.06 +units=m +no_defs +3122 projected PRS92 / Philippines zone 2 118,3.02,120.07,20.42 0.05 6683 +proj=tmerc +lat_0=0 +lon_0=119 +k=0.99995 +x_0=500000 +y_0=0 +ellps=clrk66 +towgs84=-127.62,-67.24,-47.04,-3.068,4.903,1.578,-1.06 +units=m +no_defs +3123 projected PRS92 / Philippines zone 3 119.7,3,122.21,21.62 0.05 6683 +proj=tmerc +lat_0=0 +lon_0=121 +k=0.99995 +x_0=500000 +y_0=0 +ellps=clrk66 +towgs84=-127.62,-67.24,-47.04,-3.068,4.903,1.578,-1.06 +units=m +no_defs +3124 projected PRS92 / Philippines zone 4 121.74,3.44,124.29,22.18 0.05 6683 +proj=tmerc +lat_0=0 +lon_0=123 +k=0.99995 +x_0=500000 +y_0=0 +ellps=clrk66 +towgs84=-127.62,-67.24,-47.04,-3.068,4.903,1.578,-1.06 +units=m +no_defs +3125 projected PRS92 / Philippines zone 5 123.73,4.76,126.65,21.97 0.05 6683 +proj=tmerc +lat_0=0 +lon_0=125 +k=0.99995 +x_0=500000 +y_0=0 +ellps=clrk66 +towgs84=-127.62,-67.24,-47.04,-3.068,4.903,1.578,-1.06 +units=m +no_defs +3126 projected EUREF-FIN / ETRS-GK19FIN 19.24,60.08,19.5,60.34 1 1391 +proj=tmerc +lat_0=0 +lon_0=19 +k=1 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3127 projected EUREF-FIN / ETRS-GK20FIN 19.5,59.92,20.5,60.48 1 1391 +proj=tmerc +lat_0=0 +lon_0=20 +k=1 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3128 projected EUREF-FIN / ETRS-GK21FIN 20.5,59.84,21.5,69.33 1 1391 +proj=tmerc +lat_0=0 +lon_0=21 +k=1 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3129 projected EUREF-FIN / ETRS-GK22FIN 21.5,59.76,22.5,69.31 1 1391 +proj=tmerc +lat_0=0 +lon_0=22 +k=1 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3130 projected EUREF-FIN / ETRS-GK23FIN 22.5,59.75,23.5,68.74 1 1391 +proj=tmerc +lat_0=0 +lon_0=23 +k=1 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3131 projected EUREF-FIN / ETRS-GK24FIN 23.5,59.86,24.5,68.84 1 1391 +proj=tmerc +lat_0=0 +lon_0=24 +k=1 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3132 projected EUREF-FIN / ETRS-GK25FIN 24.5,59.94,25.5,68.9 1 1391 +proj=tmerc +lat_0=0 +lon_0=25 +k=1 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3133 projected EUREF-FIN / ETRS-GK26FIN 25.5,60.18,26.5,69.94 1 1391 +proj=tmerc +lat_0=0 +lon_0=26 +k=1 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3134 projected EUREF-FIN / ETRS-GK27FIN 26.5,60.36,27.5,70.05 1 1391 +proj=tmerc +lat_0=0 +lon_0=27 +k=1 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3135 projected EUREF-FIN / ETRS-GK28FIN 27.5,60.42,28.5,70.09 1 1391 +proj=tmerc +lat_0=0 +lon_0=28 +k=1 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3136 projected EUREF-FIN / ETRS-GK29FIN 28.5,60.94,29.5,69.81 1 1391 +proj=tmerc +lat_0=0 +lon_0=29 +k=1 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3137 projected EUREF-FIN / ETRS-GK30FIN 29.5,61.43,30.5,67.98 1 1391 +proj=tmerc +lat_0=0 +lon_0=30 +k=1 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3138 projected EUREF-FIN / ETRS-GK31FIN 30.5,62.08,31.59,64.27 1 1391 +proj=tmerc +lat_0=0 +lon_0=31 +k=1 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3139 projected Vanua Levu 1915 / Vanua Levu Grid 178.42,-17.07,-179.77,-16.1 50 6748 +proj=cass +hyperbolic +lat_0=-16.25 +lon_0=179.333333333333 +x_0=251727.9155424 +y_0=334519.953768 +a=6378306.3696 +b=6356571.996 +towgs84=51,391,-36 +to_meter=0.201168 +no_defs +3140 projected Viti Levu 1912 / Viti Levu Grid 177.19,-18.32,178.75,-17.25 5 6752 +proj=cass +lat_0=-18 +lon_0=178 +x_0=109435.392 +y_0=141622.272 +a=6378306.3696 +b=6356571.996 +towgs84=98,390,-22 +to_meter=0.201168 +no_defs +3141 projected Fiji 1956 / UTM zone 60S 176.81,-19.22,180,-16.1 7 6721 +proj=utm +zone=60 +south +ellps=intl +towgs84=265.025,384.929,-194.046 +units=m +no_defs +3142 projected Fiji 1956 / UTM zone 1S -180,-17.04,-179.77,-16.1 7 6721 +proj=utm +zone=1 +south +ellps=intl +towgs84=265.025,384.929,-194.046 +units=m +no_defs +3144 projected FD54 / Faroe Lambert -7.49,61.33,-6.33,62.41 10 6741 +proj=lcc +axis=wnu +lat_1=62 +lat_0=62 +lon_0=-9 +k_0=1 +x_0=-500000 +y_0=500000 +ellps=intl +towgs84=-240.33,-267.41,-34.61 +units=m +no_defs +3145 projected ETRS89 / Faroe Lambert -7.49,61.33,-6.33,62.41 1 6258 +proj=lcc +axis=wnu +lat_1=62 +lat_0=62 +lon_0=-9 +k_0=1 +x_0=-500000 +y_0=500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3148 projected Indian 1960 / UTM zone 48N 102.14,8.33,108,23.4 44 6131 +proj=utm +zone=48 +ellps=evrst30 +towgs84=198,881,317 +units=m +no_defs +3149 projected Indian 1960 / UTM zone 49N 108,10.43,109.53,21.56 44 6131 +proj=utm +zone=49 +ellps=evrst30 +towgs84=198,881,317 +units=m +no_defs +3152 projected ST74 17.75,59.22,18.2,59.43 1 6619 +proj=tmerc +lat_0=0 +lon_0=18.05779 +k=0.99999425 +x_0=100178.1808 +y_0=-6500614.7836 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3153 projected NAD83(CSRS) / BC Albers -139.04,48.25,-114.08,60.01 1 6140 +proj=aea +lat_0=45 +lon_0=-126 +lat_1=50 +lat_2=58.5 +x_0=1000000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +3154 projected NAD83(CSRS) / UTM zone 7N -141.01,52.05,-138,72.53 1 6140 +proj=utm +zone=7 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +3155 projected NAD83(CSRS) / UTM zone 8N -138,48.06,-132,79.42 1 6140 +proj=utm +zone=8 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +3156 projected NAD83(CSRS) / UTM zone 9N -132,46.52,-126,80.93 1 6140 +proj=utm +zone=9 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +3157 projected NAD83(CSRS) / UTM zone 10N -126,48.13,-120,81.8 1 6140 +proj=utm +zone=10 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +3158 projected NAD83(CSRS) / UTM zone 14N -102,48.99,-96,84 1 6140 +proj=utm +zone=14 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +3159 projected NAD83(CSRS) / UTM zone 15N -96,48.03,-90,84 1 6140 +proj=utm +zone=15 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +3160 projected NAD83(CSRS) / UTM zone 16N -90,46.11,-84,84 1 6140 +proj=utm +zone=16 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +3161 projected NAD83 / Ontario MNR Lambert -95.16,41.67,-74.35,56.9 4 6269 +proj=lcc +lat_0=0 +lon_0=-85 +lat_1=44.5 +lat_2=53.5 +x_0=930000 +y_0=6430000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3162 projected NAD83(CSRS) / Ontario MNR Lambert -95.16,41.67,-74.35,56.9 1 6140 +proj=lcc +lat_0=0 +lon_0=-85 +lat_1=44.5 +lat_2=53.5 +x_0=930000 +y_0=6430000 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +3163 projected RGNC91-93 / Lambert New Caledonia 163.54,-22.73,168.19,-19.5 1 6749 +proj=lcc +lat_0=-21.5 +lon_0=166 +lat_1=-20.6666666666667 +lat_2=-22.3333333333333 +x_0=400000 +y_0=300000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3164 projected ST87 Ouvea / UTM zone 58S 166.44,-20.77,166.71,-20.34 1 6750 +proj=utm +zone=58 +south +ellps=WGS84 +towgs84=-56.263,16.136,-22.856 +units=m +no_defs +3165 projected NEA74 Noumea / Noumea Lambert 166.35,-22.37,166.54,-22.19 1 6644 +proj=lcc +lat_0=-22.26969175 +lon_0=166.44242575 +lat_1=-22.24469175 +lat_2=-22.29469175 +x_0=0.66 +y_0=1.02 +ellps=intl +towgs84=-10.18,-350.43,291.37 +units=m +no_defs +3166 projected NEA74 Noumea / Noumea Lambert 2 166.35,-22.37,166.54,-22.19 1 6644 +proj=lcc +lat_0=-22.2697222222222 +lon_0=166.4425 +lat_1=-22.2447222222222 +lat_2=-22.2947222222222 +x_0=8.313 +y_0=-2.354 +ellps=intl +towgs84=-10.18,-350.43,291.37 +units=m +no_defs +3167 projected Kertau (RSO) / RSO Malaya (ch) 99.59,1.21,104.6,6.72 15 6751 +proj=omerc +no_uoff +lat_0=4 +lonc=102.25 +alpha=323.0257905 +gamma=323.130102361111 +k=0.99984 +x_0=804670.24 +y_0=0 +ellps=evrst69 +towgs84=-11,851,5 +to_meter=20.116756 +no_defs +3168 projected Kertau (RSO) / RSO Malaya (m) 99.59,1.21,104.6,6.72 15 6751 +proj=omerc +no_uoff +lat_0=4 +lonc=102.25 +alpha=323.0257905 +gamma=323.130102361111 +k=0.99984 +x_0=804670.24 +y_0=0 +ellps=evrst69 +towgs84=-11,851,5 +units=m +no_defs +3169 projected RGNC91-93 / UTM zone 57S 156.25,-26.03,162.01,-15.34 1 6749 +proj=utm +zone=57 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3170 projected RGNC91-93 / UTM zone 58S 162,-26.45,168,-14.83 1 6749 +proj=utm +zone=58 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3171 projected RGNC91-93 / UTM zone 59S 168,-25.95,174.28,-19.75 1 6749 +proj=utm +zone=59 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3172 projected IGN53 Mare / UTM zone 59S 168,-21.71,168.19,-21.35 2 6641 +proj=utm +zone=59 +south +ellps=intl +towgs84=287.58,177.78,-135.41 +units=m +no_defs +3173 projected fk89 / Faroe Lambert FK89 -7.49,61.33,-6.33,62.41 6753 +proj=lcc +axis=wnu +lat_1=62 +lat_0=62 +lon_0=-9 +k_0=1 +x_0=-700000 +y_0=700000 +ellps=intl +units=m +no_defs +3174 projected NAD83 / Great Lakes Albers -93.17,40.99,-74.47,50.74 4 6269 +proj=aea +lat_0=45.568977 +lon_0=-84.455955 +lat_1=42.122774 +lat_2=49.01518 +x_0=1000000 +y_0=1000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3175 projected NAD83 / Great Lakes and St Lawrence Albers -93.17,40.99,-54.75,52.22 4 6269 +proj=aea +lat_0=45.568977 +lon_0=-83.248627 +lat_1=42.122774 +lat_2=49.01518 +x_0=1000000 +y_0=1000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3176 projected Indian 1960 / TM 106 NE 106.54,7.99,110,11.15 44 6131 +proj=tmerc +lat_0=0 +lon_0=106 +k=0.9996 +x_0=500000 +y_0=0 +ellps=evrst30 +towgs84=182,915,344 +units=m +no_defs +3177 projected LGD2006 / Libya TM 9.31,19.5,26.21,35.23 0.1 6754 +proj=tmerc +lat_0=0 +lon_0=17 +k=0.9965 +x_0=1000000 +y_0=0 +ellps=intl +towgs84=-208.4058,-109.8777,-2.5764 +units=m +no_defs +3178 projected GR96 / UTM zone 18N -75,74.52,-72,79.04 1 1421 +proj=utm +zone=18 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3179 projected GR96 / UTM zone 19N -72,73.24,-66,80.9 1 1421 +proj=utm +zone=19 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3180 projected GR96 / UTM zone 20N -66,68.92,-60,82.22 1 1421 +proj=utm +zone=20 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3181 projected GR96 / UTM zone 21N -60,58.91,-54,84 1 1421 +proj=utm +zone=21 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3182 projected GR96 / UTM zone 22N -54,56.9,-48,84 1 1421 +proj=utm +zone=22 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3183 projected GR96 / UTM zone 23N -48,56.38,-42,84 1 1421 +proj=utm +zone=23 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3184 projected GR96 / UTM zone 24N -42,56.56,-36,84 1 1421 +proj=utm +zone=24 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3185 projected GR96 / UTM zone 25N -36,60.16,-30,84 1 1421 +proj=utm +zone=25 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3186 projected GR96 / UTM zone 26N -30,64.96,-24,84 1 1421 +proj=utm +zone=26 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3187 projected GR96 / UTM zone 27N -24,67.7,-18,84 1 1421 +proj=utm +zone=27 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3188 projected GR96 / UTM zone 28N -18,68.67,-12,84 1 1421 +proj=utm +zone=28 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3189 projected GR96 / UTM zone 29N -12,72.43,-6,84 1 1421 +proj=utm +zone=29 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3190 projected LGD2006 / Libya TM zone 5 9.31,25.37,10.01,30.49 0.1 6754 +proj=tmerc +lat_0=0 +lon_0=9 +k=0.99995 +x_0=200000 +y_0=0 +ellps=intl +towgs84=-208.4058,-109.8777,-2.5764 +units=m +no_defs +3191 projected LGD2006 / Libya TM zone 6 10,23.51,12,33.23 0.1 6754 +proj=tmerc +lat_0=0 +lon_0=11 +k=0.99995 +x_0=200000 +y_0=0 +ellps=intl +towgs84=-208.4058,-109.8777,-2.5764 +units=m +no_defs +3192 projected LGD2006 / Libya TM zone 7 12,22.8,14,33.06 0.1 6754 +proj=tmerc +lat_0=0 +lon_0=13 +k=0.99995 +x_0=200000 +y_0=0 +ellps=intl +towgs84=-208.4058,-109.8777,-2.5764 +units=m +no_defs +3193 projected LGD2006 / Libya TM zone 8 14,22.61,16,32.79 0.1 6754 +proj=tmerc +lat_0=0 +lon_0=15 +k=0.99995 +x_0=200000 +y_0=0 +ellps=intl +towgs84=-208.4058,-109.8777,-2.5764 +units=m +no_defs +3194 projected LGD2006 / Libya TM zone 9 16,22.51,18.01,31.34 0.1 6754 +proj=tmerc +lat_0=0 +lon_0=17 +k=0.99995 +x_0=200000 +y_0=0 +ellps=intl +towgs84=-208.4058,-109.8777,-2.5764 +units=m +no_defs +3195 projected LGD2006 / Libya TM zone 10 18,21.54,20,32.17 0.1 6754 +proj=tmerc +lat_0=0 +lon_0=19 +k=0.99995 +x_0=200000 +y_0=0 +ellps=intl +towgs84=-208.4058,-109.8777,-2.5764 +units=m +no_defs +3196 projected LGD2006 / Libya TM zone 11 20,20.54,22,33 0.1 6754 +proj=tmerc +lat_0=0 +lon_0=21 +k=0.99995 +x_0=200000 +y_0=0 +ellps=intl +towgs84=-208.4058,-109.8777,-2.5764 +units=m +no_defs +3197 projected LGD2006 / Libya TM zone 12 22,19.5,24,32.97 0.1 6754 +proj=tmerc +lat_0=0 +lon_0=23 +k=0.99995 +x_0=200000 +y_0=0 +ellps=intl +towgs84=-208.4058,-109.8777,-2.5764 +units=m +no_defs +3198 projected LGD2006 / Libya TM zone 13 24,19.99,25.21,32.15 0.1 6754 +proj=tmerc +lat_0=0 +lon_0=25 +k=0.99995 +x_0=200000 +y_0=0 +ellps=intl +towgs84=-208.4058,-109.8777,-2.5764 +units=m +no_defs +3199 projected LGD2006 / UTM zone 32N 9.31,23.51,12,33.92 0.1 6754 +proj=utm +zone=32 +ellps=intl +towgs84=-208.4058,-109.8777,-2.5764 +units=m +no_defs +3200 projected FD58 / Iraq zone 47.13,26.21,53.61,33.22 0.5 6132 +proj=lcc +lat_1=32.5 +lat_0=32.5 +lon_0=45 +k_0=0.9987864078 +x_0=1500000 +y_0=1166200 +a=6378249.145 +rf=293.465 +towgs84=-241.54,-163.64,396.06 +units=m +no_defs +3201 projected LGD2006 / UTM zone 33N 12,22.51,18,35.23 0.1 6754 +proj=utm +zone=33 +ellps=intl +towgs84=-208.4058,-109.8777,-2.5764 +units=m +no_defs +3202 projected LGD2006 / UTM zone 34N 17.99,19.5,24.01,35.03 0.1 6754 +proj=utm +zone=34 +ellps=intl +towgs84=-208.4058,-109.8777,-2.5764 +units=m +no_defs +3203 projected LGD2006 / UTM zone 35N 24,19.99,26.21,33.6 0.1 6754 +proj=utm +zone=35 +ellps=intl +towgs84=-208.4058,-109.8777,-2.5764 +units=m +no_defs +3204 projected WGS 84 / SCAR IMW SP19-20 -72,-64,-60,-60 6326 +proj=lcc +lat_0=-90 +lon_0=-66 +lat_1=-60.6666666666667 +lat_2=-63.3333333333333 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3205 projected WGS 84 / SCAR IMW SP21-22 -60,-64,-48,-60 6326 +proj=lcc +lat_0=-90 +lon_0=-54 +lat_1=-60.6666666666667 +lat_2=-63.3333333333333 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3206 projected WGS 84 / SCAR IMW SP23-24 -48,-64,-36,-60 6326 +proj=lcc +lat_0=-90 +lon_0=-42 +lat_1=-60.6666666666667 +lat_2=-63.3333333333333 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3207 projected WGS 84 / SCAR IMW SQ01-02 -180,-68,-168,-64 6326 +proj=lcc +lat_0=-90 +lon_0=-174 +lat_1=-64.6666666666667 +lat_2=-67.3333333333333 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3208 projected WGS 84 / SCAR IMW SQ19-20 -72,-68,-60,-64 6326 +proj=lcc +lat_0=-90 +lon_0=-66 +lat_1=-64.6666666666667 +lat_2=-67.3333333333333 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3209 projected WGS 84 / SCAR IMW SQ21-22 -60,-68,-48,-64 6326 +proj=lcc +lat_0=-90 +lon_0=-54 +lat_1=-64.6666666666667 +lat_2=-67.3333333333333 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3210 projected WGS 84 / SCAR IMW SQ37-38 36,-68,48,-64 6326 +proj=lcc +lat_0=-90 +lon_0=42 +lat_1=-64.6666666666667 +lat_2=-67.3333333333333 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3211 projected WGS 84 / SCAR IMW SQ39-40 48,-68,60,-64 6326 +proj=lcc +lat_0=-90 +lon_0=54 +lat_1=-64.6666666666667 +lat_2=-67.3333333333333 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3212 projected WGS 84 / SCAR IMW SQ41-42 60,-68,72,-64 6326 +proj=lcc +lat_0=-90 +lon_0=66 +lat_1=-64.6666666666667 +lat_2=-67.3333333333333 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3213 projected WGS 84 / SCAR IMW SQ43-44 72,-68,84,-64 6326 +proj=lcc +lat_0=-90 +lon_0=78 +lat_1=-64.6666666666667 +lat_2=-67.3333333333333 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3214 projected WGS 84 / SCAR IMW SQ45-46 84,-68,96,-64 6326 +proj=lcc +lat_0=-90 +lon_0=90 +lat_1=-64.6666666666667 +lat_2=-67.3333333333333 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3215 projected WGS 84 / SCAR IMW SQ47-48 96,-68,108,-64 6326 +proj=lcc +lat_0=-90 +lon_0=102 +lat_1=-64.6666666666667 +lat_2=-67.3333333333333 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3216 projected WGS 84 / SCAR IMW SQ49-50 108,-68,120,-64 6326 +proj=lcc +lat_0=-90 +lon_0=114 +lat_1=-64.6666666666667 +lat_2=-67.3333333333333 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3217 projected WGS 84 / SCAR IMW SQ51-52 120,-68,132,-64 6326 +proj=lcc +lat_0=-90 +lon_0=126 +lat_1=-64.6666666666667 +lat_2=-67.3333333333333 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3218 projected WGS 84 / SCAR IMW SQ53-54 132,-68,144,-64 6326 +proj=lcc +lat_0=-90 +lon_0=138 +lat_1=-64.6666666666667 +lat_2=-67.3333333333333 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3219 projected WGS 84 / SCAR IMW SQ55-56 144,-68,156,-64 6326 +proj=lcc +lat_0=-90 +lon_0=150 +lat_1=-64.6666666666667 +lat_2=-67.3333333333333 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3220 projected WGS 84 / SCAR IMW SQ57-58 156,-68,168,-64 6326 +proj=lcc +lat_0=-90 +lon_0=162 +lat_1=-64.6666666666667 +lat_2=-67.3333333333333 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3221 projected WGS 84 / SCAR IMW SR13-14 -108,-72,-96,-68 6326 +proj=lcc +lat_0=-90 +lon_0=-102 +lat_1=-68.6666666666667 +lat_2=-71.3333333333333 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3222 projected WGS 84 / SCAR IMW SR15-16 -96,-72,-84,-68 6326 +proj=lcc +lat_0=-90 +lon_0=-90 +lat_1=-68.6666666666667 +lat_2=-71.3333333333333 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3223 projected WGS 84 / SCAR IMW SR17-18 -84,-72,-72,-68 6326 +proj=lcc +lat_0=-90 +lon_0=-78 +lat_1=-68.6666666666667 +lat_2=-71.3333333333333 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3224 projected WGS 84 / SCAR IMW SR19-20 -72,-72,-60,-68 6326 +proj=lcc +lat_0=-90 +lon_0=-66 +lat_1=-68.6666666666667 +lat_2=-71.3333333333333 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3225 projected WGS 84 / SCAR IMW SR27-28 -24,-72,-12,-68 6326 +proj=lcc +lat_0=-90 +lon_0=-18 +lat_1=-68.6666666666667 +lat_2=-71.3333333333333 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3226 projected WGS 84 / SCAR IMW SR29-30 -12,-72,0,-68 6326 +proj=lcc +lat_0=-90 +lon_0=-6 +lat_1=-68.6666666666667 +lat_2=-71.3333333333333 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3227 projected WGS 84 / SCAR IMW SR31-32 0,-72,12,-68 6326 +proj=lcc +lat_0=-90 +lon_0=6 +lat_1=-68.6666666666667 +lat_2=-71.3333333333333 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3228 projected WGS 84 / SCAR IMW SR33-34 12,-72,24,-68 6326 +proj=lcc +lat_0=-90 +lon_0=18 +lat_1=-68.6666666666667 +lat_2=-71.3333333333333 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3229 projected WGS 84 / SCAR IMW SR35-36 24,-72,36,-68 6326 +proj=lcc +lat_0=-90 +lon_0=30 +lat_1=-68.6666666666667 +lat_2=-71.3333333333333 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3230 projected WGS 84 / SCAR IMW SR37-38 36,-72,48,-68 6326 +proj=lcc +lat_0=-90 +lon_0=42 +lat_1=-68.6666666666667 +lat_2=-71.3333333333333 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3231 projected WGS 84 / SCAR IMW SR39-40 48,-72,60,-68 6326 +proj=lcc +lat_0=-90 +lon_0=54 +lat_1=-68.6666666666667 +lat_2=-71.3333333333333 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3232 projected WGS 84 / SCAR IMW SR41-42 60,-72,72,-68 6326 +proj=lcc +lat_0=-90 +lon_0=66 +lat_1=-68.6666666666667 +lat_2=-71.3333333333333 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3233 projected WGS 84 / SCAR IMW SR43-44 72,-72,84,-68 6326 +proj=lcc +lat_0=-90 +lon_0=78 +lat_1=-68.6666666666667 +lat_2=-71.3333333333333 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3234 projected WGS 84 / SCAR IMW SR45-46 84,-72,96,-68 6326 +proj=lcc +lat_0=-90 +lon_0=90 +lat_1=-68.6666666666667 +lat_2=-71.3333333333333 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3235 projected WGS 84 / SCAR IMW SR47-48 96,-72,108,-68 6326 +proj=lcc +lat_0=-90 +lon_0=102 +lat_1=-68.6666666666667 +lat_2=-71.3333333333333 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3236 projected WGS 84 / SCAR IMW SR49-50 108,-72,120,-68 6326 +proj=lcc +lat_0=-90 +lon_0=114 +lat_1=-68.6666666666667 +lat_2=-71.3333333333333 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3237 projected WGS 84 / SCAR IMW SR51-52 120,-72,132,-68 6326 +proj=lcc +lat_0=-90 +lon_0=126 +lat_1=-68.6666666666667 +lat_2=-71.3333333333333 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3238 projected WGS 84 / SCAR IMW SR53-54 132,-72,144,-68 6326 +proj=lcc +lat_0=-90 +lon_0=138 +lat_1=-68.6666666666667 +lat_2=-71.3333333333333 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3239 projected WGS 84 / SCAR IMW SR55-56 144,-72,156,-68 6326 +proj=lcc +lat_0=-90 +lon_0=150 +lat_1=-68.6666666666667 +lat_2=-71.3333333333333 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3240 projected WGS 84 / SCAR IMW SR57-58 156,-72,168,-68 6326 +proj=lcc +lat_0=-90 +lon_0=162 +lat_1=-68.6666666666667 +lat_2=-71.3333333333333 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3241 projected WGS 84 / SCAR IMW SR59-60 168,-72,180,-68 6326 +proj=lcc +lat_0=-90 +lon_0=174 +lat_1=-68.6666666666667 +lat_2=-71.3333333333333 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3242 projected WGS 84 / SCAR IMW SS04-06 -162,-76,-144,-72 6326 +proj=lcc +lat_0=-90 +lon_0=-153 +lat_1=-72.6666666666667 +lat_2=-75.3333333333333 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3243 projected WGS 84 / SCAR IMW SS07-09 -144,-76,-126,-72 6326 +proj=lcc +lat_0=-90 +lon_0=-135 +lat_1=-72.6666666666667 +lat_2=-75.3333333333333 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3244 projected WGS 84 / SCAR IMW SS10-12 -126,-76,-108,-72 6326 +proj=lcc +lat_0=-90 +lon_0=-117 +lat_1=-72.6666666666667 +lat_2=-75.3333333333333 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3245 projected WGS 84 / SCAR IMW SS13-15 -108,-76,-90,-72 6326 +proj=lcc +lat_0=-90 +lon_0=-99 +lat_1=-72.6666666666667 +lat_2=-75.3333333333333 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3246 projected WGS 84 / SCAR IMW SS16-18 -90,-76,-72,-72 6326 +proj=lcc +lat_0=-90 +lon_0=-81 +lat_1=-72.6666666666667 +lat_2=-75.3333333333333 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3247 projected WGS 84 / SCAR IMW SS19-21 -72,-76,-54,-72 6326 +proj=lcc +lat_0=-90 +lon_0=-63 +lat_1=-72.6666666666667 +lat_2=-75.3333333333333 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3248 projected WGS 84 / SCAR IMW SS25-27 -36,-76,-18,-72 6326 +proj=lcc +lat_0=-90 +lon_0=-27 +lat_1=-72.6666666666667 +lat_2=-75.3333333333333 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3249 projected WGS 84 / SCAR IMW SS28-30 -18,-76,0,-72 6326 +proj=lcc +lat_0=-90 +lon_0=-9 +lat_1=-72.6666666666667 +lat_2=-75.3333333333333 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3250 projected WGS 84 / SCAR IMW SS31-33 0,-76,18,-72 6326 +proj=lcc +lat_0=-90 +lon_0=9 +lat_1=-72.6666666666667 +lat_2=-75.3333333333333 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3251 projected WGS 84 / SCAR IMW SS34-36 18,-76,36,-72 6326 +proj=lcc +lat_0=-90 +lon_0=27 +lat_1=-72.6666666666667 +lat_2=-75.3333333333333 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3252 projected WGS 84 / SCAR IMW SS37-39 36,-76,54,-72 6326 +proj=lcc +lat_0=-90 +lon_0=45 +lat_1=-72.6666666666667 +lat_2=-75.3333333333333 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3253 projected WGS 84 / SCAR IMW SS40-42 54,-76,72,-72 6326 +proj=lcc +lat_0=-90 +lon_0=63 +lat_1=-72.6666666666667 +lat_2=-75.3333333333333 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3254 projected WGS 84 / SCAR IMW SS43-45 72,-76,90,-72 6326 +proj=lcc +lat_0=-90 +lon_0=81 +lat_1=-72.6666666666667 +lat_2=-75.3333333333333 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3255 projected WGS 84 / SCAR IMW SS46-48 90,-76,108,-72 6326 +proj=lcc +lat_0=-90 +lon_0=99 +lat_1=-72.6666666666667 +lat_2=-75.3333333333333 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3256 projected WGS 84 / SCAR IMW SS49-51 108,-76,126,-72 6326 +proj=lcc +lat_0=-90 +lon_0=117 +lat_1=-72.6666666666667 +lat_2=-75.3333333333333 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3257 projected WGS 84 / SCAR IMW SS52-54 126,-76,144,-72 6326 +proj=lcc +lat_0=-90 +lon_0=135 +lat_1=-72.6666666666667 +lat_2=-75.3333333333333 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3258 projected WGS 84 / SCAR IMW SS55-57 144,-76,162,-72 6326 +proj=lcc +lat_0=-90 +lon_0=153 +lat_1=-72.6666666666667 +lat_2=-75.3333333333333 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3259 projected WGS 84 / SCAR IMW SS58-60 162,-76,180,-72 6326 +proj=lcc +lat_0=-90 +lon_0=171 +lat_1=-72.6666666666667 +lat_2=-75.3333333333333 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3260 projected WGS 84 / SCAR IMW ST01-04 -180,-80,-156,-76 6326 +proj=lcc +lat_0=-90 +lon_0=-168 +lat_1=-76.6666666666667 +lat_2=-79.3333333333333 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3261 projected WGS 84 / SCAR IMW ST05-08 -156,-80,-132,-76 6326 +proj=lcc +lat_0=-90 +lon_0=-144 +lat_1=-76.6666666666667 +lat_2=-79.3333333333333 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3262 projected WGS 84 / SCAR IMW ST09-12 -132,-80,-108,-76 6326 +proj=lcc +lat_0=-90 +lon_0=-120 +lat_1=-76.6666666666667 +lat_2=-79.3333333333333 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3263 projected WGS 84 / SCAR IMW ST13-16 -108,-80,-84,-76 6326 +proj=lcc +lat_0=-90 +lon_0=-96 +lat_1=-76.6666666666667 +lat_2=-79.3333333333333 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3264 projected WGS 84 / SCAR IMW ST17-20 -84,-80,-60,-76 6326 +proj=lcc +lat_0=-90 +lon_0=-72 +lat_1=-76.6666666666667 +lat_2=-79.3333333333333 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3265 projected WGS 84 / SCAR IMW ST21-24 -60,-80,-36,-76 6326 +proj=lcc +lat_0=-90 +lon_0=-48 +lat_1=-76.6666666666667 +lat_2=-79.3333333333333 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3266 projected WGS 84 / SCAR IMW ST25-28 -36,-80,-12,-76 6326 +proj=lcc +lat_0=-90 +lon_0=-24 +lat_1=-76.6666666666667 +lat_2=-79.3333333333333 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3267 projected WGS 84 / SCAR IMW ST29-32 -12,-80,12,-76 6326 +proj=lcc +lat_0=-90 +lon_0=0 +lat_1=-76.6666666666667 +lat_2=-79.3333333333333 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3268 projected WGS 84 / SCAR IMW ST33-36 12,-80,36,-76 6326 +proj=lcc +lat_0=-90 +lon_0=24 +lat_1=-76.6666666666667 +lat_2=-79.3333333333333 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3269 projected WGS 84 / SCAR IMW ST37-40 36,-80,60,-76 6326 +proj=lcc +lat_0=-90 +lon_0=48 +lat_1=-76.6666666666667 +lat_2=-79.3333333333333 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3270 projected WGS 84 / SCAR IMW ST41-44 60,-80,84,-76 6326 +proj=lcc +lat_0=-90 +lon_0=72 +lat_1=-76.6666666666667 +lat_2=-79.3333333333333 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3271 projected WGS 84 / SCAR IMW ST45-48 84,-80,108,-76 6326 +proj=lcc +lat_0=-90 +lon_0=96 +lat_1=-76.6666666666667 +lat_2=-79.3333333333333 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3272 projected WGS 84 / SCAR IMW ST49-52 108,-80,132,-76 6326 +proj=lcc +lat_0=-90 +lon_0=120 +lat_1=-76.6666666666667 +lat_2=-79.3333333333333 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3273 projected WGS 84 / SCAR IMW ST53-56 132,-80,156,-76 6326 +proj=lcc +lat_0=-90 +lon_0=144 +lat_1=-76.6666666666667 +lat_2=-79.3333333333333 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3274 projected WGS 84 / SCAR IMW ST57-60 156,-80,180,-76 6326 +proj=lcc +lat_0=-90 +lon_0=168 +lat_1=-76.6666666666667 +lat_2=-79.3333333333333 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3275 projected WGS 84 / SCAR IMW SU01-05 -180,-84,-150,-80 6326 +proj=stere +lat_0=-90 +lat_ts=-80.2386111111111 +lon_0=-165 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3276 projected WGS 84 / SCAR IMW SU06-10 -150,-84,-120,-80 6326 +proj=stere +lat_0=-90 +lat_ts=-80.2386111111111 +lon_0=-135 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3277 projected WGS 84 / SCAR IMW SU11-15 -120,-84,-90,-80 6326 +proj=stere +lat_0=-90 +lat_ts=-80.2386111111111 +lon_0=-105 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3278 projected WGS 84 / SCAR IMW SU16-20 -90,-84,-60,-80 6326 +proj=stere +lat_0=-90 +lat_ts=-80.2386111111111 +lon_0=-75 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3279 projected WGS 84 / SCAR IMW SU21-25 -60,-84,-30,-80 6326 +proj=stere +lat_0=-90 +lat_ts=-80.2386111111111 +lon_0=-45 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3280 projected WGS 84 / SCAR IMW SU26-30 -30,-84,0,-80 6326 +proj=stere +lat_0=-90 +lat_ts=-80.2386111111111 +lon_0=-15 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3281 projected WGS 84 / SCAR IMW SU31-35 0,-84,30,-80 6326 +proj=stere +lat_0=-90 +lat_ts=-80.2386111111111 +lon_0=15 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3282 projected WGS 84 / SCAR IMW SU36-40 30,-84,60,-80 6326 +proj=stere +lat_0=-90 +lat_ts=-80.2386111111111 +lon_0=45 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3283 projected WGS 84 / SCAR IMW SU41-45 60,-84,90,-80 6326 +proj=stere +lat_0=-90 +lat_ts=-80.2386111111111 +lon_0=75 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3284 projected WGS 84 / SCAR IMW SU46-50 90,-84,120,-80 6326 +proj=stere +lat_0=-90 +lat_ts=-80.2386111111111 +lon_0=105 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3285 projected WGS 84 / SCAR IMW SU51-55 120,-84,150,-80 6326 +proj=stere +lat_0=-90 +lat_ts=-80.2386111111111 +lon_0=135 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3286 projected WGS 84 / SCAR IMW SU56-60 150,-84,180,-80 6326 +proj=stere +lat_0=-90 +lat_ts=-80.2386111111111 +lon_0=165 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3287 projected WGS 84 / SCAR IMW SV01-10 -180,-88,-120,-84 6326 +proj=stere +lat_0=-90 +lat_ts=-80.2386111111111 +lon_0=-150 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3288 projected WGS 84 / SCAR IMW SV11-20 -120,-88,-60,-84 6326 +proj=stere +lat_0=-90 +lat_ts=-80.2386111111111 +lon_0=-90 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3289 projected WGS 84 / SCAR IMW SV21-30 -60,-88,0,-84 6326 +proj=stere +lat_0=-90 +lat_ts=-80.2386111111111 +lon_0=-30 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3290 projected WGS 84 / SCAR IMW SV31-40 0,-88,60,-84 6326 +proj=stere +lat_0=-90 +lat_ts=-80.2386111111111 +lon_0=30 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3291 projected WGS 84 / SCAR IMW SV41-50 60,-88,120,-84 6326 +proj=stere +lat_0=-90 +lat_ts=-80.2386111111111 +lon_0=90 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3292 projected WGS 84 / SCAR IMW SV51-60 120,-88,180,-84 6326 +proj=stere +lat_0=-90 +lat_ts=-80.2386111111111 +lon_0=150 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3293 projected WGS 84 / SCAR IMW SW01-60 -180,-90,180,-88 6326 +proj=stere +lat_0=-90 +lat_ts=-80.2386111111111 +lon_0=0 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3294 projected WGS 84 / USGS Transantarctic Mountains 149.83,-80,174.01,-68.6 6326 +proj=lcc +lat_0=-78 +lon_0=162 +lat_1=-76.6666666666667 +lat_2=-79.3333333333333 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3295 projected Guam 1963 / Yap Islands 137.99,9.39,138.27,9.69 6675 +proj=aeqd +lat_0=9.54670833333333 +lon_0=138.168744444444 +x_0=40000 +y_0=60000 +ellps=clrk66 +units=m +no_defs +3296 projected RGPF / UTM zone 5S -158.13,-26.7,-150,-12.5 0.5 6687 +proj=utm +zone=5 +south +ellps=GRS80 +towgs84=0.072,-0.507,-0.245,0.0183,-0.0003,0.007,-0.0093 +units=m +no_defs +3297 projected RGPF / UTM zone 6S -150,-31.2,-144,-7.29 0.5 6687 +proj=utm +zone=6 +south +ellps=GRS80 +towgs84=0.072,-0.507,-0.245,0.0183,-0.0003,0.007,-0.0093 +units=m +no_defs +3298 projected RGPF / UTM zone 7S -144,-31.24,-138,-4.52 0.5 6687 +proj=utm +zone=7 +south +ellps=GRS80 +towgs84=0.072,-0.507,-0.245,0.0183,-0.0003,0.007,-0.0093 +units=m +no_defs +3299 projected RGPF / UTM zone 8S -138,-26.58,-131.97,-5.52 0.5 6687 +proj=utm +zone=8 +south +ellps=GRS80 +towgs84=0.072,-0.507,-0.245,0.0183,-0.0003,0.007,-0.0093 +units=m +no_defs +3300 projected Estonian Coordinate System of 1992 21.74,57.52,28.2,59.75 0.5 6133 +proj=lcc +lat_0=57.5175539305556 +lon_0=24 +lat_1=59.3333333333333 +lat_2=58 +x_0=500000 +y_0=6375000 +ellps=GRS80 +towgs84=0.055,-0.541,-0.185,0.0183,-0.0003,-0.007,-0.014 +units=m +no_defs +3301 projected Estonian Coordinate System of 1997 20.37,57.52,28.2,60 1 6180 +proj=lcc +lat_0=57.5175539305556 +lon_0=24 +lat_1=59.3333333333333 +lat_2=58 +x_0=500000 +y_0=6375000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3302 projected IGN63 Hiva Oa / UTM zone 7S -139.23,-10.08,-138.75,-9.64 2 6689 +proj=utm +zone=7 +south +ellps=intl +towgs84=410.793,54.542,80.501,-2.5596,-2.3517,-0.6594,17.3218 +units=m +no_defs +3303 projected Fatu Iva 72 / UTM zone 7S -138.75,-10.6,-138.54,-10.36 2 6688 +proj=utm +zone=7 +south +ellps=intl +towgs84=347.175,1077.618,2623.677,33.9058,-70.6776,9.4013,186.0647 +units=m +no_defs +3304 projected Tahiti 79 / UTM zone 6S -149.7,-17.93,-149.09,-17.44 1 6690 +proj=utm +zone=6 +south +ellps=intl +towgs84=221.597,152.441,176.523,2.403,1.3893,0.884,11.4648 +units=m +no_defs +3305 projected Moorea 87 / UTM zone 6S -150,-17.63,-149.73,-17.41 1 6691 +proj=utm +zone=6 +south +ellps=intl +towgs84=218.769,150.75,176.75,3.5231,2.0037,1.288,10.9817 +units=m +no_defs +3306 projected Maupiti 83 / UTM zone 5S -152.39,-16.57,-152.14,-16.34 1 6692 +proj=utm +zone=5 +south +ellps=intl +towgs84=217.109,86.452,23.711,0.0183,-0.0003,0.007,-0.0093 +units=m +no_defs +3307 projected Nakhl-e Ghanem / UTM zone 39N 51.8,27.3,53.01,28.2 0.2 6693 +proj=utm +zone=39 +ellps=WGS84 +towgs84=0,-0.15,0.68 +units=m +no_defs +3308 projected GDA94 / NSW Lambert 140.99,-37.53,153.69,-28.15 3 6283 +proj=lcc +lat_0=-33.25 +lon_0=147 +lat_1=-30.75 +lat_2=-35.75 +x_0=9300000 +y_0=4500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3309 projected NAD27 / California Albers -124.45,32.53,-114.12,42.01 7 6267 +proj=aea +lat_0=0 +lon_0=-120 +lat_1=34 +lat_2=40.5 +x_0=0 +y_0=-4000000 +ellps=clrk66 +towgs84=-8,159,175 +units=m +no_defs +3310 projected NAD83 / California Albers -124.45,32.53,-114.12,42.01 4 6269 +proj=aea +lat_0=0 +lon_0=-120 +lat_1=34 +lat_2=40.5 +x_0=0 +y_0=-4000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3311 projected NAD83(HARN) / California Albers -124.45,32.53,-114.12,42.01 1 6152 +proj=aea +lat_0=0 +lon_0=-120 +lat_1=34 +lat_2=40.5 +x_0=0 +y_0=-4000000 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +3312 projected CSG67 / UTM zone 21N -54.45,4.84,-54,5.69 10 6623 +proj=utm +zone=21 +ellps=intl +towgs84=-186,230,110 +units=m +no_defs +3313 projected RGFG95 / UTM zone 21N -54.61,2.11,-54,5.69 1 6624 +proj=utm +zone=21 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3316 projected Kasai 1953 / Congo TM zone 22 21.5,-7.31,23.01,-5.31 6696 +proj=tmerc +lat_0=0 +lon_0=22 +k=0.9999 +x_0=500000 +y_0=10000000 +a=6378249.145 +rf=293.465 +units=m +no_defs +3317 projected Kasai 1953 / Congo TM zone 24 23,-6.99,25,-5.01 6696 +proj=tmerc +lat_0=0 +lon_0=24 +k=0.9999 +x_0=500000 +y_0=10000000 +a=6378249.145 +rf=293.465 +units=m +no_defs +3318 projected IGC 1962 / Congo TM zone 12 12.17,-6.04,13.01,-4.67 6697 +proj=tmerc +lat_0=0 +lon_0=12 +k=0.9999 +x_0=500000 +y_0=10000000 +a=6378249.145 +rf=293.465 +units=m +no_defs +3319 projected IGC 1962 / Congo TM zone 14 13,-5.91,15.01,-4.28 6697 +proj=tmerc +lat_0=0 +lon_0=14 +k=0.9999 +x_0=500000 +y_0=10000000 +a=6378249.145 +rf=293.465 +units=m +no_defs +3320 projected IGC 1962 / Congo TM zone 16 15,-5.87,17,-3.29 6697 +proj=tmerc +lat_0=0 +lon_0=16 +k=0.9999 +x_0=500000 +y_0=10000000 +a=6378249.145 +rf=293.465 +units=m +no_defs +3321 projected IGC 1962 / Congo TM zone 18 17,-5.38,19,-3.4 6697 +proj=tmerc +lat_0=0 +lon_0=18 +k=0.9999 +x_0=500000 +y_0=10000000 +a=6378249.145 +rf=293.465 +units=m +no_defs +3322 projected IGC 1962 / Congo TM zone 20 19,-7.29,21,-4.01 6697 +proj=tmerc +lat_0=0 +lon_0=20 +k=0.9999 +x_0=500000 +y_0=10000000 +a=6378249.145 +rf=293.465 +units=m +no_defs +3323 projected IGC 1962 / Congo TM zone 22 21,-7.31,23,-5.31 6697 +proj=tmerc +lat_0=0 +lon_0=22 +k=0.9999 +x_0=500000 +y_0=10000000 +a=6378249.145 +rf=293.465 +units=m +no_defs +3324 projected IGC 1962 / Congo TM zone 24 23,-6.99,25,-5.01 6697 +proj=tmerc +lat_0=0 +lon_0=24 +k=0.9999 +x_0=500000 +y_0=10000000 +a=6378249.145 +rf=293.465 +units=m +no_defs +3325 projected IGC 1962 / Congo TM zone 26 25,-7.26,27,-4.23 6697 +proj=tmerc +lat_0=0 +lon_0=26 +k=0.9999 +x_0=500000 +y_0=10000000 +a=6378249.145 +rf=293.465 +units=m +no_defs +3326 projected IGC 1962 / Congo TM zone 28 27,-7.36,29,-4.24 6697 +proj=tmerc +lat_0=0 +lon_0=28 +k=0.9999 +x_0=500000 +y_0=10000000 +a=6378249.145 +rf=293.465 +units=m +no_defs +3327 projected IGC 1962 / Congo TM zone 30 29,-6.04,29.64,-4.34 6697 +proj=tmerc +lat_0=0 +lon_0=30 +k=0.9999 +x_0=500000 +y_0=10000000 +a=6378249.145 +rf=293.465 +units=m +no_defs +3328 projected Pulkovo 1942(58) / GUGiK-80 14.14,49,24.15,54.89 1 6179 +proj=sterea +lat_0=52.1666666666667 +lon_0=19.1666666666667 +k=0.999714 +x_0=500000 +y_0=500000 +ellps=krass +towgs84=33.4,-146.6,-76.3,-0.359,-0.053,0.844,-0.84 +units=m +no_defs +3329 projected Pulkovo 1942(58) / 3-degree Gauss-Kruger zone 5 13.5,46.54,16.5,54.72 1 6179 +proj=tmerc +lat_0=0 +lon_0=15 +k=1 +x_0=5500000 +y_0=0 +ellps=krass +towgs84=33.4,-146.6,-76.3,-0.359,-0.053,0.844,-0.84 +units=m +no_defs +3330 projected Pulkovo 1942(58) / 3-degree Gauss-Kruger zone 6 16.5,40.14,19.5,54.89 1 6179 +proj=tmerc +lat_0=0 +lon_0=18 +k=1 +x_0=6500000 +y_0=0 +ellps=krass +towgs84=33.4,-146.6,-76.3,-0.359,-0.053,0.844,-0.84 +units=m +no_defs +3331 projected Pulkovo 1942(58) / 3-degree Gauss-Kruger zone 7 19.5,39.64,22.5,54.51 1 6179 +proj=tmerc +lat_0=0 +lon_0=21 +k=1 +x_0=7500000 +y_0=0 +ellps=krass +towgs84=33.4,-146.6,-76.3,-0.359,-0.053,0.844,-0.84 +units=m +no_defs +3332 projected Pulkovo 1942(58) / 3-degree Gauss-Kruger zone 8 22.5,41.24,25.5,54.41 3 6179 +proj=tmerc +lat_0=0 +lon_0=24 +k=1 +x_0=8500000 +y_0=0 +ellps=krass +towgs84=2.329,-147.042,-92.08,-0.309,0.325,0.497,5.69 +units=m +no_defs +3333 projected Pulkovo 1942(58) / Gauss-Kruger zone 3 12,45.78,18,54.89 1 6179 +proj=tmerc +lat_0=0 +lon_0=15 +k=1 +x_0=3500000 +y_0=0 +ellps=krass +towgs84=33.4,-146.6,-76.3,-0.359,-0.053,0.844,-0.84 +units=m +no_defs +3334 projected Pulkovo 1942(58) / Gauss-Kruger zone 4 18,39.64,24,54.89 1 6179 +proj=tmerc +lat_0=0 +lon_0=21 +k=1 +x_0=4500000 +y_0=0 +ellps=krass +towgs84=33.4,-146.6,-76.3,-0.359,-0.053,0.844,-0.84 +units=m +no_defs +3335 projected Pulkovo 1942(58) / Gauss-Kruger zone 5 24,41.24,29.74,50.93 3 6179 +proj=tmerc +lat_0=0 +lon_0=27 +k=1 +x_0=5500000 +y_0=0 +ellps=krass +towgs84=2.329,-147.042,-92.08,-0.309,0.325,0.497,5.69 +units=m +no_defs +3336 projected IGN 1962 Kerguelen / UTM zone 42S 68.69,-49.78,70.62,-48.6 10 6698 +proj=utm +zone=42 +south +ellps=intl +towgs84=145,-187,103 +units=m +no_defs +3337 projected Le Pouce 1934 / Mauritius Grid 57.25,-20.57,57.85,-19.94 2 6699 +proj=lcc +lat_1=-20.1950694444444 +lat_0=-20.1950694444444 +lon_0=57.5218277777778 +k_0=1 +x_0=1000000 +y_0=1000000 +a=6378249.145 +rf=293.465 +towgs84=-770.1,158.4,-498.2 +units=m +no_defs +3338 projected NAD83 / Alaska Albers 172.42,51.3,-129.99,71.4 4 6269 +proj=aea +lat_0=50 +lon_0=-154 +lat_1=55 +lat_2=65 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3339 projected IGCB 1955 / Congo TM zone 12 12.17,-6.04,13.01,-4.67 5 6701 +proj=tmerc +lat_0=0 +lon_0=12 +k=0.9999 +x_0=500000 +y_0=10000000 +a=6378249.145 +rf=293.465 +towgs84=-79.9,-158,-168.9 +units=m +no_defs +3340 projected IGCB 1955 / Congo TM zone 14 13,-5.91,15.01,-4.28 5 6701 +proj=tmerc +lat_0=0 +lon_0=14 +k=0.9999 +x_0=500000 +y_0=10000000 +a=6378249.145 +rf=293.465 +towgs84=-79.9,-158,-168.9 +units=m +no_defs +3341 projected IGCB 1955 / Congo TM zone 16 14.99,-5.87,16.28,-4.42 5 6701 +proj=tmerc +lat_0=0 +lon_0=16 +k=0.9999 +x_0=500000 +y_0=10000000 +a=6378249.145 +rf=293.465 +towgs84=-79.9,-158,-168.9 +units=m +no_defs +3342 projected IGCB 1955 / UTM zone 33S 12.17,-6.04,16.28,-4.28 5 6701 +proj=utm +zone=33 +south +a=6378249.145 +rf=293.465 +towgs84=-79.9,-158,-168.9 +units=m +no_defs +3343 projected Mauritania 1999 / UTM zone 28N -18,14.72,-12,23.46 1 6702 +proj=utm +zone=28 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3344 projected Mauritania 1999 / UTM zone 29N -12,14.75,-6,27.3 1 6702 +proj=utm +zone=29 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3345 projected Mauritania 1999 / UTM zone 30N -6,15.49,-4.8,25.74 1 6702 +proj=utm +zone=30 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3346 projected LKS94 / Lithuania TM 19.02,53.89,26.82,56.45 1 6126 +proj=tmerc +lat_0=0 +lon_0=24 +k=0.9998 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3347 projected NAD83 / Statistics Canada Lambert -141.01,38.21,-40.73,86.46 4 6269 +proj=lcc +lat_0=63.390675 +lon_0=-91.8666666666667 +lat_1=49 +lat_2=77 +x_0=6200000 +y_0=3000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3348 projected NAD83(CSRS) / Statistics Canada Lambert -141.01,38.21,-40.73,86.46 1 6140 +proj=lcc +lat_0=63.390675 +lon_0=-91.8666666666667 +lat_1=49 +lat_2=77 +x_0=6200000 +y_0=3000000 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +3350 projected Pulkovo 1942 / CS63 zone C0 19.57,54.17,23.45,59.27 4.5 6284 +proj=tmerc +lat_0=0.1 +lon_0=21.95 +k=1 +x_0=250000 +y_0=0 +ellps=krass +towgs84=25,-141,-78.5,0,0.35,0.736,0 +units=m +no_defs +3351 projected Pulkovo 1942 / CS63 zone C1 23.45,53.89,26.45,59.72 3 6284 +proj=tmerc +lat_0=0.1 +lon_0=24.95 +k=1 +x_0=1250000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +3352 projected Pulkovo 1942 / CS63 zone C2 26.45,55.15,28.24,59.61 3 6284 +proj=tmerc +lat_0=0.1 +lon_0=27.95 +k=1 +x_0=2250000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +3353 projected Mhast (onshore) / UTM zone 32S 10.53,-6.04,13.1,-4.38 6704 +proj=utm +zone=32 +south +ellps=intl +units=m +no_defs +3354 projected Mhast (offshore) / UTM zone 32S 10.53,-6.04,12.37,-5.05 6705 +proj=utm +zone=32 +south +ellps=intl +units=m +no_defs +3355 projected Egypt Gulf of Suez S-650 TL / Red Belt 32.34,27.19,34.27,30.01 5 6706 +proj=tmerc +lat_0=30 +lon_0=31 +k=1 +x_0=615000 +y_0=810000 +ellps=helmert +towgs84=-146.21,112.63,4.05 +units=m +no_defs +3358 projected NAD83(HARN) / North Carolina -84.33,33.83,-75.38,36.59 1 6152 +proj=lcc +lat_0=33.75 +lon_0=-79 +lat_1=36.1666666666667 +lat_2=34.3333333333333 +x_0=609601.22 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +3360 projected NAD83(HARN) / South Carolina -83.36,32.05,-78.52,35.21 1 6152 +proj=lcc +lat_0=31.8333333333333 +lon_0=-81 +lat_1=34.8333333333333 +lat_2=32.5 +x_0=609600 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +3361 projected NAD83(HARN) / South Carolina (ft) -83.36,32.05,-78.52,35.21 1 6152 +proj=lcc +lat_0=31.8333333333333 +lon_0=-81 +lat_1=34.8333333333333 +lat_2=32.5 +x_0=609600 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.3048 +no_defs +3362 projected NAD83(HARN) / Pennsylvania North -80.53,40.6,-74.7,42.53 1 6152 +proj=lcc +lat_0=40.1666666666667 +lon_0=-77.75 +lat_1=41.95 +lat_2=40.8833333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +3363 projected NAD83(HARN) / Pennsylvania North (ftUS) -80.53,40.6,-74.7,42.53 1 6152 +proj=lcc +lat_0=40.1666666666667 +lon_0=-77.75 +lat_1=41.95 +lat_2=40.8833333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +3364 projected NAD83(HARN) / Pennsylvania South -80.53,39.71,-74.72,41.18 1 6152 +proj=lcc +lat_0=39.3333333333333 +lon_0=-77.75 +lat_1=40.9666666666667 +lat_2=39.9333333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +3365 projected NAD83(HARN) / Pennsylvania South (ftUS) -80.53,39.71,-74.72,41.18 1 6152 +proj=lcc +lat_0=39.3333333333333 +lon_0=-77.75 +lat_1=40.9666666666667 +lat_2=39.9333333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +3367 projected IGN Astro 1960 / UTM zone 28N -17.08,14.72,-12,23.46 6700 +proj=utm +zone=28 +a=6378249.145 +rf=293.465 +units=m +no_defs +3368 projected IGN Astro 1960 / UTM zone 29N -12,14.75,-6,27.3 6700 +proj=utm +zone=29 +a=6378249.145 +rf=293.465 +units=m +no_defs +3369 projected IGN Astro 1960 / UTM zone 30N -6,15.49,-4.8,25.74 6700 +proj=utm +zone=30 +a=6378249.145 +rf=293.465 +units=m +no_defs +3370 projected NAD27 / UTM zone 59N 167.65,49.01,174.01,56.28 18 6267 +proj=utm +zone=59 +ellps=clrk66 +towgs84=2,204,105 +units=m +no_defs +3371 projected NAD27 / UTM zone 60N 174,47.92,180,56.67 18 6267 +proj=utm +zone=60 +ellps=clrk66 +towgs84=2,204,105 +units=m +no_defs +3372 projected NAD83 / UTM zone 59N 167.65,49.01,174.01,56.28 8 6269 +proj=utm +zone=59 +ellps=GRS80 +towgs84=-2,0,4 +units=m +no_defs +3373 projected NAD83 / UTM zone 60N 174,47.92,180,56.67 8 6269 +proj=utm +zone=60 +ellps=GRS80 +towgs84=-2,0,4 +units=m +no_defs +3374 projected FD54 / UTM zone 29N -7.49,61.33,-6.33,62.41 10 6741 +proj=utm +zone=29 +ellps=intl +towgs84=-240.33,-267.41,-34.61 +units=m +no_defs +3375 projected GDM2000 / Peninsula RSO 98.02,1.13,105.82,7.81 1 6742 +proj=omerc +no_uoff +lat_0=4 +lonc=102.25 +alpha=323.025796466667 +gamma=323.130102361111 +k=0.99984 +x_0=804671 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3376 projected GDM2000 / East Malaysia BRSO 109.31,0.85,119.61,7.67 1 6742 +proj=omerc +no_uoff +lat_0=4 +lonc=115 +alpha=53.31580995 +gamma=53.1301023611111 +k=0.99984 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3377 projected GDM2000 / Johor Grid 102.44,1.21,104.6,2.95 1 6742 +proj=cass +lat_0=2.12167974444444 +lon_0=103.427936236111 +x_0=-14810.562 +y_0=8758.32 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3378 projected GDM2000 / Sembilan and Melaka Grid 101.7,2.03,102.71,3.28 1 6742 +proj=cass +lat_0=2.68234763611111 +lon_0=101.974905041667 +x_0=3673.785 +y_0=-4240.573 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3379 projected GDM2000 / Pahang Grid 101.33,2.45,103.67,4.78 1 6742 +proj=cass +lat_0=3.76938808888889 +lon_0=102.368298983333 +x_0=-7368.228 +y_0=6485.858 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3380 projected GDM2000 / Selangor Grid 100.76,2.54,101.97,3.87 1 6742 +proj=cass +lat_0=3.68464905 +lon_0=101.389107913889 +x_0=-34836.161 +y_0=56464.049 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3381 projected GDM2000 / Terengganu Grid 102.38,3.89,103.72,5.9 1 6742 +proj=cass +lat_0=4.9762852 +lon_0=103.070275625 +x_0=19594.245 +y_0=3371.895 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3382 projected GDM2000 / Pinang Grid 100.12,5.12,100.56,5.59 1 6742 +proj=cass +lat_0=5.42151754166667 +lon_0=100.344376963889 +x_0=-23.414 +y_0=62.283 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3383 projected GDM2000 / Kedah and Perlis Grid 99.59,5.08,101.12,6.72 1 6742 +proj=cass +lat_0=5.96467271388889 +lon_0=100.636371111111 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3384 projected GDM2000 / Perak Grid 100.07,3.66,102,5.92 1 6742 +proj=cass +lat_0=4.85906302222222 +lon_0=100.815410586111 +x_0=-1.769 +y_0=133454.779 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3385 projected GDM2000 / Kelantan Grid 101.33,4.54,102.67,6.29 1 6742 +proj=cass +lat_0=5.97254365833333 +lon_0=102.295241669444 +x_0=13227.851 +y_0=8739.894 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3386 projected KKJ / Finland zone 0 19.24,60.08,19.5,60.34 1 6123 +proj=tmerc +lat_0=0 +lon_0=18 +k=1 +x_0=500000 +y_0=0 +ellps=intl +towgs84=-96.062,-82.428,-121.753,4.801,0.345,-1.376,1.496 +units=m +no_defs +3387 projected KKJ / Finland zone 5 31.5,62.83,31.59,63 1 6123 +proj=tmerc +lat_0=0 +lon_0=33 +k=1 +x_0=5500000 +y_0=0 +ellps=intl +towgs84=-96.062,-82.428,-121.753,4.801,0.345,-1.376,1.496 +units=m +no_defs +3388 projected Pulkovo 1942 / Caspian Sea Mercator 46.95,37.35,53.93,46.97 4.5 6284 +proj=merc +lat_ts=42 +lon_0=51 +x_0=0 +y_0=0 +ellps=krass +towgs84=25,-141,-78.5,0,0.35,0.736,0 +units=m +no_defs +3389 projected Pulkovo 1942 / 3-degree Gauss-Kruger zone 60 178.5,62.24,-178.5,71.65 3 6284 +proj=tmerc +lat_0=0 +lon_0=180 +k=1 +x_0=60500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +3390 projected Pulkovo 1995 / 3-degree Gauss-Kruger zone 60 178.5,62.24,-178.5,71.65 1 6200 +proj=tmerc +lat_0=0 +lon_0=180 +k=1 +x_0=60500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +3391 projected Karbala 1979 / UTM zone 37N 38.79,31.14,42,36.75 1 6743 +proj=utm +zone=37 +a=6378249.145 +rf=293.465 +towgs84=70.995,-335.916,262.898 +units=m +no_defs +3392 projected Karbala 1979 / UTM zone 38N 42,29.06,48,37.39 1 6743 +proj=utm +zone=38 +a=6378249.145 +rf=293.465 +towgs84=70.995,-335.916,262.898 +units=m +no_defs +3393 projected Karbala 1979 / UTM zone 39N 48,29.87,48.61,31 1 6743 +proj=utm +zone=39 +a=6378249.145 +rf=293.465 +towgs84=70.995,-335.916,262.898 +units=m +no_defs +3394 projected Nahrwan 1934 / Iraq zone 38.79,29.06,51.06,37.39 30 6744 +proj=lcc +lat_1=32.5 +lat_0=32.5 +lon_0=45 +k_0=0.9987864078 +x_0=1500000 +y_0=1166200 +a=6378249.145 +rf=293.465 +towgs84=-242.2,-144.9,370.3 +units=m +no_defs +3395 projected WGS 84 / World Mercator -180,-80,180,84 6326 +proj=merc +lon_0=0 +k=1 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3396 projected PD/83 / 3-degree Gauss-Kruger zone 3 9.92,50.35,10.5,51.56 6746 +proj=tmerc +lat_0=0 +lon_0=9 +k=1 +x_0=3500000 +y_0=0 +ellps=bessel +units=m +no_defs +3397 projected PD/83 / 3-degree Gauss-Kruger zone 4 10.5,50.2,12.56,51.64 6746 +proj=tmerc +lat_0=0 +lon_0=12 +k=1 +x_0=4500000 +y_0=0 +ellps=bessel +units=m +no_defs +3398 projected RD/83 / 3-degree Gauss-Kruger zone 4 11.89,50.2,13.51,51.66 6745 +proj=tmerc +lat_0=0 +lon_0=12 +k=1 +x_0=4500000 +y_0=0 +ellps=bessel +units=m +no_defs +3399 projected RD/83 / 3-degree Gauss-Kruger zone 5 13.5,50.62,15.04,51.58 6745 +proj=tmerc +lat_0=0 +lon_0=15 +k=1 +x_0=5500000 +y_0=0 +ellps=bessel +units=m +no_defs +3400 projected NAD83 / Alberta 10-TM (Forest) -120,48.99,-109.98,60 4 6269 +proj=tmerc +lat_0=0 +lon_0=-115 +k=0.9992 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3401 projected NAD83 / Alberta 10-TM (Resource) -120,48.99,-109.98,60 4 6269 +proj=tmerc +lat_0=0 +lon_0=-115 +k=0.9992 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3402 projected NAD83(CSRS) / Alberta 10-TM (Forest) -120,48.99,-109.98,60 1 6140 +proj=tmerc +lat_0=0 +lon_0=-115 +k=0.9992 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +3403 projected NAD83(CSRS) / Alberta 10-TM (Resource) -120,48.99,-109.98,60 1 6140 +proj=tmerc +lat_0=0 +lon_0=-115 +k=0.9992 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +3404 projected NAD83(HARN) / North Carolina (ftUS) -84.33,33.83,-75.38,36.59 1 6152 +proj=lcc +lat_0=33.75 +lon_0=-79 +lat_1=36.1666666666667 +lat_2=34.3333333333333 +x_0=609601.219202438 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +3405 projected VN-2000 / UTM zone 48N 102.14,8.33,108,23.4 1 6756 +proj=utm +zone=48 +ellps=WGS84 +towgs84=-191.90441429,-39.30318279,-111.45032835,0.00928836,-0.01975479,0.00427372,0.252906278 +units=m +no_defs +3406 projected VN-2000 / UTM zone 49N 108,10.43,109.53,21.56 1 6756 +proj=utm +zone=49 +ellps=WGS84 +towgs84=-191.90441429,-39.30318279,-111.45032835,0.00928836,-0.01975479,0.00427372,0.252906278 +units=m +no_defs +3407 projected Hong Kong 1963 Grid System 113.76,22.13,114.51,22.58 6738 +proj=cass +lat_0=22.3121333333333 +lon_0=114.178555555556 +x_0=40243.5777560424 +y_0=19069.9335151258 +a=6378293.64520876 +b=6356617.98767984 +to_meter=0.3047972654 +no_defs +3408 projected NSIDC EASE-Grid North -180,0,180,90 1360 +proj=laea +lat_0=90 +lon_0=0 +x_0=0 +y_0=0 +R=6371228 +units=m +no_defs +3409 projected NSIDC EASE-Grid South -180,-90,180,0 1360 +proj=laea +lat_0=-90 +lon_0=0 +x_0=0 +y_0=0 +R=6371228 +units=m +no_defs +3410 projected NSIDC EASE-Grid Global -180,-86,180,86 1360 +proj=cea +lat_ts=30 +lon_0=0 +x_0=0 +y_0=0 +R=6371228 +units=m +no_defs +3411 projected NSIDC Sea Ice Polar Stereographic North -180,60,180,90 1359 +proj=stere +lat_0=90 +lat_ts=70 +lon_0=-45 +x_0=0 +y_0=0 +a=6378273 +b=6356889.449 +units=m +no_defs +3412 projected NSIDC Sea Ice Polar Stereographic South -180,-90,180,-60 1359 +proj=stere +lat_0=-90 +lat_ts=-70 +lon_0=0 +x_0=0 +y_0=0 +a=6378273 +b=6356889.449 +units=m +no_defs +3413 projected WGS 84 / NSIDC Sea Ice Polar Stereographic North -180,60,180,90 6326 +proj=stere +lat_0=90 +lat_ts=70 +lon_0=-45 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3414 projected SVY21 / Singapore TM 103.59,1.13,104.07,1.47 1 6757 +proj=tmerc +lat_0=1.36666666666667 +lon_0=103.833333333333 +k=1 +x_0=28001.642 +y_0=38744.572 +ellps=WGS84 +towgs84=0,0,0 +units=m +no_defs +3415 projected WGS 72BE / South China Sea Lambert 110.13,18.31,116.76,22.89 2 6324 +proj=lcc +lat_0=21 +lon_0=114 +lat_1=18 +lat_2=24 +x_0=500000 +y_0=500000 +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +3416 projected ETRS89 / Austria Lambert 9.53,46.4,17.17,49.02 1 6258 +proj=lcc +lat_0=47.5 +lon_0=13.3333333333333 +lat_1=49 +lat_2=46 +x_0=400000 +y_0=400000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3417 projected NAD83 / Iowa North (ftUS) -96.65,41.85,-90.15,43.51 4 6269 +proj=lcc +lat_0=41.5 +lon_0=-93.5 +lat_1=43.2666666666667 +lat_2=42.0666666666667 +x_0=1500000 +y_0=999999.99998984 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3418 projected NAD83 / Iowa South (ftUS) -96.14,40.36,-90.14,42.04 4 6269 +proj=lcc +lat_0=40 +lon_0=-93.5 +lat_1=41.7833333333333 +lat_2=40.6166666666667 +x_0=500000.00001016 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3419 projected NAD83 / Kansas North (ftUS) -102.06,38.52,-94.58,40.01 4 6269 +proj=lcc +lat_0=38.3333333333333 +lon_0=-98 +lat_1=39.7833333333333 +lat_2=38.7166666666667 +x_0=399999.99998984 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3420 projected NAD83 / Kansas South (ftUS) -102.05,36.99,-94.6,38.88 4 6269 +proj=lcc +lat_0=36.6666666666667 +lon_0=-98.5 +lat_1=38.5666666666667 +lat_2=37.2666666666667 +x_0=399999.99998984 +y_0=399999.99998984 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3421 projected NAD83 / Nevada East (ftUS) -117.01,34.99,-114.03,42 4 6269 +proj=tmerc +lat_0=34.75 +lon_0=-115.583333333333 +k=0.9999 +x_0=200000.00001016 +y_0=8000000.00001016 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3422 projected NAD83 / Nevada Central (ftUS) -118.19,36,-114.99,41 4 6269 +proj=tmerc +lat_0=34.75 +lon_0=-116.666666666667 +k=0.9999 +x_0=500000.00001016 +y_0=6000000 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3423 projected NAD83 / Nevada West (ftUS) -120,36.95,-116.99,42 4 6269 +proj=tmerc +lat_0=34.75 +lon_0=-118.583333333333 +k=0.9999 +x_0=800000.00001016 +y_0=3999999.99998984 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3424 projected NAD83 / New Jersey (ftUS) -75.6,38.87,-73.88,41.36 4 6269 +proj=tmerc +lat_0=38.8333333333333 +lon_0=-74.5 +k=0.9999 +x_0=150000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3425 projected NAD83(HARN) / Iowa North (ftUS) -96.65,41.85,-90.15,43.51 1 6152 +proj=lcc +lat_0=41.5 +lon_0=-93.5 +lat_1=43.2666666666667 +lat_2=42.0666666666667 +x_0=1500000 +y_0=999999.99998984 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +3426 projected NAD83(HARN) / Iowa South (ftUS) -96.14,40.36,-90.14,42.04 1 6152 +proj=lcc +lat_0=40 +lon_0=-93.5 +lat_1=41.7833333333333 +lat_2=40.6166666666667 +x_0=500000.00001016 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +3427 projected NAD83(HARN) / Kansas North (ftUS) -102.06,38.52,-94.58,40.01 1 6152 +proj=lcc +lat_0=38.3333333333333 +lon_0=-98 +lat_1=39.7833333333333 +lat_2=38.7166666666667 +x_0=399999.99998984 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +3428 projected NAD83(HARN) / Kansas South (ftUS) -102.05,36.99,-94.6,38.88 1 6152 +proj=lcc +lat_0=36.6666666666667 +lon_0=-98.5 +lat_1=38.5666666666667 +lat_2=37.2666666666667 +x_0=399999.99998984 +y_0=399999.99998984 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +3429 projected NAD83(HARN) / Nevada East (ftUS) -117.01,34.99,-114.03,42 1 6152 +proj=tmerc +lat_0=34.75 +lon_0=-115.583333333333 +k=0.9999 +x_0=200000.00001016 +y_0=8000000.00001016 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +3430 projected NAD83(HARN) / Nevada Central (ftUS) -118.19,36,-114.99,41 1 6152 +proj=tmerc +lat_0=34.75 +lon_0=-116.666666666667 +k=0.9999 +x_0=500000.00001016 +y_0=6000000 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +3431 projected NAD83(HARN) / Nevada West (ftUS) -120,36.95,-116.99,42 1 6152 +proj=tmerc +lat_0=34.75 +lon_0=-118.583333333333 +k=0.9999 +x_0=800000.00001016 +y_0=3999999.99998984 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +3432 projected NAD83(HARN) / New Jersey (ftUS) -75.6,38.87,-73.88,41.36 1 6152 +proj=tmerc +lat_0=38.8333333333333 +lon_0=-74.5 +k=0.9999 +x_0=150000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +3433 projected NAD83 / Arkansas North (ftUS) -94.62,34.67,-89.64,36.5 4 6269 +proj=lcc +lat_0=34.3333333333333 +lon_0=-92 +lat_1=36.2333333333333 +lat_2=34.9333333333333 +x_0=399999.99998984 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3434 projected NAD83 / Arkansas South (ftUS) -94.48,33.01,-90.4,35.1 4 6269 +proj=lcc +lat_0=32.6666666666667 +lon_0=-92 +lat_1=34.7666666666667 +lat_2=33.3 +x_0=399999.99998984 +y_0=399999.99998984 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3435 projected NAD83 / Illinois East (ftUS) -89.27,37.06,-87.02,42.5 4 6269 +proj=tmerc +lat_0=36.6666666666667 +lon_0=-88.3333333333333 +k=0.999975 +x_0=300000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3436 projected NAD83 / Illinois West (ftUS) -91.52,36.97,-88.92,42.51 4 6269 +proj=tmerc +lat_0=36.6666666666667 +lon_0=-90.1666666666667 +k=0.999941177 +x_0=699999.99998984 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3437 projected NAD83 / New Hampshire (ftUS) -72.56,42.69,-70.63,45.31 4 6269 +proj=tmerc +lat_0=42.5 +lon_0=-71.6666666666667 +k=0.999966667 +x_0=300000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3438 projected NAD83 / Rhode Island (ftUS) -71.85,41.13,-71.08,42.02 4 6269 +proj=tmerc +lat_0=41.0833333333333 +lon_0=-71.5 +k=0.99999375 +x_0=99999.99998984 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3439 projected PSD93 / UTM zone 39N 51.99,16.59,54,19.67 0.5 6134 +proj=utm +zone=39 +a=6378249.145 +rf=293.465 +towgs84=-180.624,-225.516,173.919,-0.81,-1.898,8.336,16.71006 +units=m +no_defs +3440 projected PSD93 / UTM zone 40N 54,16.89,59.91,26.58 0.5 6134 +proj=utm +zone=40 +a=6378249.145 +rf=293.465 +towgs84=-180.624,-225.516,173.919,-0.81,-1.898,8.336,16.71006 +units=m +no_defs +3441 projected NAD83(HARN) / Arkansas North (ftUS) -94.62,34.67,-89.64,36.5 1 6152 +proj=lcc +lat_0=34.3333333333333 +lon_0=-92 +lat_1=36.2333333333333 +lat_2=34.9333333333333 +x_0=399999.99998984 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +3442 projected NAD83(HARN) / Arkansas South (ftUS) -94.48,33.01,-90.4,35.1 1 6152 +proj=lcc +lat_0=32.6666666666667 +lon_0=-92 +lat_1=34.7666666666667 +lat_2=33.3 +x_0=399999.99998984 +y_0=399999.99998984 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +3443 projected NAD83(HARN) / Illinois East (ftUS) -89.27,37.06,-87.02,42.5 1 6152 +proj=tmerc +lat_0=36.6666666666667 +lon_0=-88.3333333333333 +k=0.999975 +x_0=300000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +3444 projected NAD83(HARN) / Illinois West (ftUS) -91.52,36.97,-88.92,42.51 1 6152 +proj=tmerc +lat_0=36.6666666666667 +lon_0=-90.1666666666667 +k=0.999941177 +x_0=699999.99998984 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +3445 projected NAD83(HARN) / New Hampshire (ftUS) -72.56,42.69,-70.63,45.31 1 6152 +proj=tmerc +lat_0=42.5 +lon_0=-71.6666666666667 +k=0.999966667 +x_0=300000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +3446 projected NAD83(HARN) / Rhode Island (ftUS) -71.85,41.13,-71.08,42.02 1 6152 +proj=tmerc +lat_0=41.0833333333333 +lon_0=-71.5 +k=0.99999375 +x_0=99999.99998984 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +3447 projected ETRS89 / Belgian Lambert 2005 2.5,49.5,6.4,51.51 1 6258 +proj=lcc +lat_0=50.797815 +lon_0=4.35921583333333 +lat_1=49.8333333333333 +lat_2=51.1666666666667 +x_0=150328 +y_0=166262 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3448 projected JAD2001 / Jamaica Metric Grid -78.43,17.64,-76.17,18.58 0 6758 +proj=lcc +lat_1=18 +lat_0=18 +lon_0=-77 +k_0=1 +x_0=750000 +y_0=650000 +ellps=WGS84 +towgs84=0,0,0 +units=m +no_defs +3449 projected JAD2001 / UTM zone 17N -80.6,14.16,-77.99,19.36 0 6758 +proj=utm +zone=17 +ellps=WGS84 +towgs84=0,0,0 +units=m +no_defs +3450 projected JAD2001 / UTM zone 18N -78,14.08,-74.51,19.2 0 6758 +proj=utm +zone=18 +ellps=WGS84 +towgs84=0,0,0 +units=m +no_defs +3451 projected NAD83 / Louisiana North (ftUS) -94.05,30.85,-90.86,33.03 4 6269 +proj=lcc +lat_0=30.5 +lon_0=-92.5 +lat_1=32.6666666666667 +lat_2=31.1666666666667 +x_0=999999.99998984 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3452 projected NAD83 / Louisiana South (ftUS) -93.94,28.85,-88.75,31.07 4 6269 +proj=lcc +lat_0=28.5 +lon_0=-91.3333333333333 +lat_1=30.7 +lat_2=29.3 +x_0=999999.99998984 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3453 projected NAD83 / Louisiana Offshore (ftUS) -93.38,25.58,-86.53,29.35 4 6269 +proj=lcc +lat_0=25.5 +lon_0=-91.3333333333333 +lat_1=27.8333333333333 +lat_2=26.1666666666667 +x_0=999999.99998984 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3455 projected NAD83 / South Dakota South (ftUS) -104.06,42.48,-96.43,44.79 4 6269 +proj=lcc +lat_0=42.3333333333333 +lon_0=-100.333333333333 +lat_1=44.4 +lat_2=42.8333333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3456 projected NAD83(HARN) / Louisiana North (ftUS) -94.05,30.85,-90.86,33.03 1 6152 +proj=lcc +lat_0=30.5 +lon_0=-92.5 +lat_1=32.6666666666667 +lat_2=31.1666666666667 +x_0=999999.99998984 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +3457 projected NAD83(HARN) / Louisiana South (ftUS) -93.94,28.85,-88.75,31.07 1 6152 +proj=lcc +lat_0=28.5 +lon_0=-91.3333333333333 +lat_1=30.7 +lat_2=29.3 +x_0=999999.99998984 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +3458 projected NAD83(HARN) / South Dakota North (ftUS) -104.07,44.14,-96.45,45.95 1 6152 +proj=lcc +lat_0=43.8333333333333 +lon_0=-100 +lat_1=45.6833333333333 +lat_2=44.4166666666667 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +3459 projected NAD83(HARN) / South Dakota South (ftUS) -104.06,42.48,-96.43,44.79 1 6152 +proj=lcc +lat_0=42.3333333333333 +lon_0=-100.333333333333 +lat_1=44.4 +lat_2=42.8333333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +3460 projected Fiji 1986 / Fiji Map Grid 176.81,-20.81,-178.15,-12.42 2 6720 +proj=tmerc +lat_0=-17 +lon_0=178.75 +k=0.99985 +x_0=2000000 +y_0=4000000 +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.2263 +units=m +no_defs +3461 projected Dabola 1981 / UTM zone 28N -15.13,9.01,-12,12.68 25 6155 +proj=utm +zone=28 +ellps=clrk80ign +towgs84=-83,37,124 +units=m +no_defs +3462 projected Dabola 1981 / UTM zone 29N -12,7.19,-7.65,12.51 25 6155 +proj=utm +zone=29 +ellps=clrk80ign +towgs84=-83,37,124 +units=m +no_defs +3463 projected NAD83 / Maine CS2000 Central -70.03,43.75,-68.33,47.47 4 6269 +proj=tmerc +lat_0=43.5 +lon_0=-69.125 +k=0.99998 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3464 projected NAD83(HARN) / Maine CS2000 Central -70.03,43.75,-68.33,47.47 1 6152 +proj=tmerc +lat_0=43.5 +lon_0=-69.125 +k=0.99998 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +3465 projected NAD83(NSRS2007) / Alabama East -86.79,30.99,-84.89,35 2 6759 +proj=tmerc +lat_0=30.5 +lon_0=-85.8333333333333 +k=0.99996 +x_0=200000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3466 projected NAD83(NSRS2007) / Alabama West -88.48,30.14,-86.3,35.02 2 6759 +proj=tmerc +lat_0=30 +lon_0=-87.5 +k=0.999933333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3467 projected NAD83(NSRS2007) / Alaska Albers 172.42,51.3,-129.99,71.4 2 6759 +proj=aea +lat_0=50 +lon_0=-154 +lat_1=55 +lat_2=65 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3468 projected NAD83(NSRS2007) / Alaska zone 1 -141,54.61,-129.99,60.35 2 6759 +proj=omerc +no_uoff +lat_0=57 +lonc=-133.666666666667 +alpha=323.130102361111 +gamma=323.130102361111 +k=0.9999 +x_0=5000000 +y_0=-5000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3469 projected NAD83(NSRS2007) / Alaska zone 2 -144.01,59.72,-140.98,70.16 2 6759 +proj=tmerc +lat_0=54 +lon_0=-142 +k=0.9999 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3470 projected NAD83(NSRS2007) / Alaska zone 3 -148,59.72,-144,70.38 2 6759 +proj=tmerc +lat_0=54 +lon_0=-146 +k=0.9999 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3471 projected NAD83(NSRS2007) / Alaska zone 4 -152.01,59.11,-147.99,70.63 2 6759 +proj=tmerc +lat_0=54 +lon_0=-150 +k=0.9999 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3472 projected NAD83(NSRS2007) / Alaska zone 5 -156,55.72,-151.86,71.28 2 6759 +proj=tmerc +lat_0=54 +lon_0=-154 +k=0.9999 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3473 projected NAD83(NSRS2007) / Alaska zone 6 -160,54.89,-155.99,71.4 2 6759 +proj=tmerc +lat_0=54 +lon_0=-158 +k=0.9999 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3474 projected NAD83(NSRS2007) / Alaska zone 7 -164.01,54.32,-160,70.74 2 6759 +proj=tmerc +lat_0=54 +lon_0=-162 +k=0.9999 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3475 projected NAD83(NSRS2007) / Alaska zone 8 -168.26,54.34,-164,69.05 2 6759 +proj=tmerc +lat_0=54 +lon_0=-166 +k=0.9999 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3476 projected NAD83(NSRS2007) / Alaska zone 9 -173.16,56.49,-168,65.82 2 6759 +proj=tmerc +lat_0=54 +lon_0=-170 +k=0.9999 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3477 projected NAD83(NSRS2007) / Alaska zone 10 172.42,51.3,-164.84,54.34 2 6759 +proj=lcc +lat_0=51 +lon_0=-176 +lat_1=53.8333333333333 +lat_2=51.8333333333333 +x_0=1000000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3478 projected NAD83(NSRS2007) / Arizona Central -113.35,31.33,-110.44,37.01 2 6759 +proj=tmerc +lat_0=31 +lon_0=-111.916666666667 +k=0.9999 +x_0=213360 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3479 projected NAD83(NSRS2007) / Arizona Central (ft) -113.35,31.33,-110.44,37.01 2 6759 +proj=tmerc +lat_0=31 +lon_0=-111.916666666667 +k=0.9999 +x_0=213360 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +3480 projected NAD83(NSRS2007) / Arizona East -111.71,31.33,-109.04,37.01 2 6759 +proj=tmerc +lat_0=31 +lon_0=-110.166666666667 +k=0.9999 +x_0=213360 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3481 projected NAD83(NSRS2007) / Arizona East (ft) -111.71,31.33,-109.04,37.01 2 6759 +proj=tmerc +lat_0=31 +lon_0=-110.166666666667 +k=0.9999 +x_0=213360 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +3482 projected NAD83(NSRS2007) / Arizona West -114.81,32.05,-112.52,37 2 6759 +proj=tmerc +lat_0=31 +lon_0=-113.75 +k=0.999933333 +x_0=213360 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3483 projected NAD83(NSRS2007) / Arizona West (ft) -114.81,32.05,-112.52,37 2 6759 +proj=tmerc +lat_0=31 +lon_0=-113.75 +k=0.999933333 +x_0=213360 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +3484 projected NAD83(NSRS2007) / Arkansas North -94.62,34.67,-89.64,36.5 2 6759 +proj=lcc +lat_0=34.3333333333333 +lon_0=-92 +lat_1=36.2333333333333 +lat_2=34.9333333333333 +x_0=400000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3485 projected NAD83(NSRS2007) / Arkansas North (ftUS) -94.62,34.67,-89.64,36.5 2 6759 +proj=lcc +lat_0=34.3333333333333 +lon_0=-92 +lat_1=36.2333333333333 +lat_2=34.9333333333333 +x_0=399999.99998984 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3486 projected NAD83(NSRS2007) / Arkansas South -94.48,33.01,-90.4,35.1 2 6759 +proj=lcc +lat_0=32.6666666666667 +lon_0=-92 +lat_1=34.7666666666667 +lat_2=33.3 +x_0=400000 +y_0=400000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3487 projected NAD83(NSRS2007) / Arkansas South (ftUS) -94.48,33.01,-90.4,35.1 2 6759 +proj=lcc +lat_0=32.6666666666667 +lon_0=-92 +lat_1=34.7666666666667 +lat_2=33.3 +x_0=399999.99998984 +y_0=399999.99998984 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3488 projected NAD83(NSRS2007) / California Albers -124.45,32.53,-114.12,42.01 2 6759 +proj=aea +lat_0=0 +lon_0=-120 +lat_1=34 +lat_2=40.5 +x_0=0 +y_0=-4000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3489 projected NAD83(NSRS2007) / California zone 1 -124.45,39.59,-119.99,42.01 2 6759 +proj=lcc +lat_0=39.3333333333333 +lon_0=-122 +lat_1=41.6666666666667 +lat_2=40 +x_0=2000000 +y_0=500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3490 projected NAD83(NSRS2007) / California zone 1 (ftUS) -124.45,39.59,-119.99,42.01 2 6759 +proj=lcc +lat_0=39.3333333333333 +lon_0=-122 +lat_1=41.6666666666667 +lat_2=40 +x_0=2000000.0001016 +y_0=500000.0001016 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3491 projected NAD83(NSRS2007) / California zone 2 -124.06,38.02,-119.54,40.16 2 6759 +proj=lcc +lat_0=37.6666666666667 +lon_0=-122 +lat_1=39.8333333333333 +lat_2=38.3333333333333 +x_0=2000000 +y_0=500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3492 projected NAD83(NSRS2007) / California zone 2 (ftUS) -124.06,38.02,-119.54,40.16 2 6759 +proj=lcc +lat_0=37.6666666666667 +lon_0=-122 +lat_1=39.8333333333333 +lat_2=38.3333333333333 +x_0=2000000.0001016 +y_0=500000.0001016 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3493 projected NAD83(NSRS2007) / California zone 3 -123.02,36.73,-117.83,38.71 2 6759 +proj=lcc +lat_0=36.5 +lon_0=-120.5 +lat_1=38.4333333333333 +lat_2=37.0666666666667 +x_0=2000000 +y_0=500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3494 projected NAD83(NSRS2007) / California zone 3 (ftUS) -123.02,36.73,-117.83,38.71 2 6759 +proj=lcc +lat_0=36.5 +lon_0=-120.5 +lat_1=38.4333333333333 +lat_2=37.0666666666667 +x_0=2000000.0001016 +y_0=500000.0001016 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3495 projected NAD83(NSRS2007) / California zone 4 -122.01,35.78,-115.62,37.58 2 6759 +proj=lcc +lat_0=35.3333333333333 +lon_0=-119 +lat_1=37.25 +lat_2=36 +x_0=2000000 +y_0=500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3496 projected NAD83(NSRS2007) / California zone 4 (ftUS) -122.01,35.78,-115.62,37.58 2 6759 +proj=lcc +lat_0=35.3333333333333 +lon_0=-119 +lat_1=37.25 +lat_2=36 +x_0=2000000.0001016 +y_0=500000.0001016 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3497 projected NAD83(NSRS2007) / California zone 5 -121.42,32.76,-114.12,35.81 2 6759 +proj=lcc +lat_0=33.5 +lon_0=-118 +lat_1=35.4666666666667 +lat_2=34.0333333333333 +x_0=2000000 +y_0=500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3498 projected NAD83(NSRS2007) / California zone 5 (ftUS) -121.42,32.76,-114.12,35.81 2 6759 +proj=lcc +lat_0=33.5 +lon_0=-118 +lat_1=35.4666666666667 +lat_2=34.0333333333333 +x_0=2000000.0001016 +y_0=500000.0001016 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3499 projected NAD83(NSRS2007) / California zone 6 -118.15,32.53,-114.42,34.08 2 6759 +proj=lcc +lat_0=32.1666666666667 +lon_0=-116.25 +lat_1=33.8833333333333 +lat_2=32.7833333333333 +x_0=2000000 +y_0=500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3500 projected NAD83(NSRS2007) / California zone 6 (ftUS) -118.15,32.53,-114.42,34.08 2 6759 +proj=lcc +lat_0=32.1666666666667 +lon_0=-116.25 +lat_1=33.8833333333333 +lat_2=32.7833333333333 +x_0=2000000.0001016 +y_0=500000.0001016 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3501 projected NAD83(NSRS2007) / Colorado Central -109.06,38.14,-102.04,40.09 2 6759 +proj=lcc +lat_0=37.8333333333333 +lon_0=-105.5 +lat_1=39.75 +lat_2=38.45 +x_0=914401.8289 +y_0=304800.6096 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3502 projected NAD83(NSRS2007) / Colorado Central (ftUS) -109.06,38.14,-102.04,40.09 2 6759 +proj=lcc +lat_0=37.8333333333333 +lon_0=-105.5 +lat_1=39.75 +lat_2=38.45 +x_0=914401.828803658 +y_0=304800.609601219 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3503 projected NAD83(NSRS2007) / Colorado North -109.06,39.56,-102.04,41.01 2 6759 +proj=lcc +lat_0=39.3333333333333 +lon_0=-105.5 +lat_1=40.7833333333333 +lat_2=39.7166666666667 +x_0=914401.8289 +y_0=304800.6096 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3504 projected NAD83(NSRS2007) / Colorado North (ftUS) -109.06,39.56,-102.04,41.01 2 6759 +proj=lcc +lat_0=39.3333333333333 +lon_0=-105.5 +lat_1=40.7833333333333 +lat_2=39.7166666666667 +x_0=914401.828803658 +y_0=304800.609601219 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3505 projected NAD83(NSRS2007) / Colorado South -109.06,36.98,-102.04,38.68 2 6759 +proj=lcc +lat_0=36.6666666666667 +lon_0=-105.5 +lat_1=38.4333333333333 +lat_2=37.2333333333333 +x_0=914401.8289 +y_0=304800.6096 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3506 projected NAD83(NSRS2007) / Colorado South (ftUS) -109.06,36.98,-102.04,38.68 2 6759 +proj=lcc +lat_0=36.6666666666667 +lon_0=-105.5 +lat_1=38.4333333333333 +lat_2=37.2333333333333 +x_0=914401.828803658 +y_0=304800.609601219 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3507 projected NAD83(NSRS2007) / Connecticut -73.73,40.98,-71.78,42.05 2 6759 +proj=lcc +lat_0=40.8333333333333 +lon_0=-72.75 +lat_1=41.8666666666667 +lat_2=41.2 +x_0=304800.6096 +y_0=152400.3048 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3508 projected NAD83(NSRS2007) / Connecticut (ftUS) -73.73,40.98,-71.78,42.05 2 6759 +proj=lcc +lat_0=40.8333333333333 +lon_0=-72.75 +lat_1=41.8666666666667 +lat_2=41.2 +x_0=304800.609601219 +y_0=152400.30480061 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3509 projected NAD83(NSRS2007) / Delaware -75.8,38.44,-74.97,39.85 2 6759 +proj=tmerc +lat_0=38 +lon_0=-75.4166666666667 +k=0.999995 +x_0=200000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3510 projected NAD83(NSRS2007) / Delaware (ftUS) -75.8,38.44,-74.97,39.85 2 6759 +proj=tmerc +lat_0=38 +lon_0=-75.4166666666667 +k=0.999995 +x_0=200000.0001016 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3511 projected NAD83(NSRS2007) / Florida East -82.33,24.41,-79.97,30.83 2 6759 +proj=tmerc +lat_0=24.3333333333333 +lon_0=-81 +k=0.999941177 +x_0=200000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3512 projected NAD83(NSRS2007) / Florida East (ftUS) -82.33,24.41,-79.97,30.83 2 6759 +proj=tmerc +lat_0=24.3333333333333 +lon_0=-81 +k=0.999941177 +x_0=200000.0001016 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3513 projected NAD83(NSRS2007) / Florida GDL Albers -87.63,24.41,-79.97,31.01 2 6759 +proj=aea +lat_0=24 +lon_0=-84 +lat_1=24 +lat_2=31.5 +x_0=400000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3514 projected NAD83(NSRS2007) / Florida North -87.63,29.21,-82.04,31.01 2 6759 +proj=lcc +lat_0=29 +lon_0=-84.5 +lat_1=30.75 +lat_2=29.5833333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3515 projected NAD83(NSRS2007) / Florida North (ftUS) -87.63,29.21,-82.04,31.01 2 6759 +proj=lcc +lat_0=29 +lon_0=-84.5 +lat_1=30.75 +lat_2=29.5833333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3516 projected NAD83(NSRS2007) / Florida West -83.34,26.27,-81.13,29.6 2 6759 +proj=tmerc +lat_0=24.3333333333333 +lon_0=-82 +k=0.999941177 +x_0=200000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3517 projected NAD83(NSRS2007) / Florida West (ftUS) -83.34,26.27,-81.13,29.6 2 6759 +proj=tmerc +lat_0=24.3333333333333 +lon_0=-82 +k=0.999941177 +x_0=200000.0001016 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3518 projected NAD83(NSRS2007) / Georgia East -83.47,30.36,-80.77,34.68 2 6759 +proj=tmerc +lat_0=30 +lon_0=-82.1666666666667 +k=0.9999 +x_0=200000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3519 projected NAD83(NSRS2007) / Georgia East (ftUS) -83.47,30.36,-80.77,34.68 2 6759 +proj=tmerc +lat_0=30 +lon_0=-82.1666666666667 +k=0.9999 +x_0=200000.0001016 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3520 projected NAD83(NSRS2007) / Georgia West -85.61,30.62,-82.99,35.01 2 6759 +proj=tmerc +lat_0=30 +lon_0=-84.1666666666667 +k=0.9999 +x_0=700000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3521 projected NAD83(NSRS2007) / Georgia West (ftUS) -85.61,30.62,-82.99,35.01 2 6759 +proj=tmerc +lat_0=30 +lon_0=-84.1666666666667 +k=0.9999 +x_0=699999.9998984 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3522 projected NAD83(NSRS2007) / Idaho Central -115.3,41.99,-112.67,45.7 2 6759 +proj=tmerc +lat_0=41.6666666666667 +lon_0=-114 +k=0.999947368 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3523 projected NAD83(NSRS2007) / Idaho Central (ftUS) -115.3,41.99,-112.67,45.7 2 6759 +proj=tmerc +lat_0=41.6666666666667 +lon_0=-114 +k=0.999947368 +x_0=500000.0001016 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3524 projected NAD83(NSRS2007) / Idaho East -113.24,41.99,-111.04,44.75 2 6759 +proj=tmerc +lat_0=41.6666666666667 +lon_0=-112.166666666667 +k=0.999947368 +x_0=200000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3525 projected NAD83(NSRS2007) / Idaho East (ftUS) -113.24,41.99,-111.04,44.75 2 6759 +proj=tmerc +lat_0=41.6666666666667 +lon_0=-112.166666666667 +k=0.999947368 +x_0=200000.0001016 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3526 projected NAD83(NSRS2007) / Idaho West -117.24,41.99,-114.32,49.01 2 6759 +proj=tmerc +lat_0=41.6666666666667 +lon_0=-115.75 +k=0.999933333 +x_0=800000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3527 projected NAD83(NSRS2007) / Idaho West (ftUS) -117.24,41.99,-114.32,49.01 2 6759 +proj=tmerc +lat_0=41.6666666666667 +lon_0=-115.75 +k=0.999933333 +x_0=800000.0001016 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3528 projected NAD83(NSRS2007) / Illinois East -89.27,37.06,-87.02,42.5 2 6759 +proj=tmerc +lat_0=36.6666666666667 +lon_0=-88.3333333333333 +k=0.999975 +x_0=300000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3529 projected NAD83(NSRS2007) / Illinois East (ftUS) -89.27,37.06,-87.02,42.5 2 6759 +proj=tmerc +lat_0=36.6666666666667 +lon_0=-88.3333333333333 +k=0.999975 +x_0=300000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3530 projected NAD83(NSRS2007) / Illinois West -91.52,36.97,-88.92,42.51 2 6759 +proj=tmerc +lat_0=36.6666666666667 +lon_0=-90.1666666666667 +k=0.999941177 +x_0=700000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3531 projected NAD83(NSRS2007) / Illinois West (ftUS) -91.52,36.97,-88.92,42.51 2 6759 +proj=tmerc +lat_0=36.6666666666667 +lon_0=-90.1666666666667 +k=0.999941177 +x_0=699999.99998984 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3532 projected NAD83(NSRS2007) / Indiana East -86.59,37.95,-84.78,41.77 2 6759 +proj=tmerc +lat_0=37.5 +lon_0=-85.6666666666667 +k=0.999966667 +x_0=100000 +y_0=250000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3533 projected NAD83(NSRS2007) / Indiana East (ftUS) -86.59,37.95,-84.78,41.77 2 6759 +proj=tmerc +lat_0=37.5 +lon_0=-85.6666666666667 +k=0.999966667 +x_0=99999.9998983998 +y_0=249999.9998984 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3534 projected NAD83(NSRS2007) / Indiana West -88.1,37.77,-86.24,41.77 2 6759 +proj=tmerc +lat_0=37.5 +lon_0=-87.0833333333333 +k=0.999966667 +x_0=900000 +y_0=250000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3535 projected NAD83(NSRS2007) / Indiana West (ftUS) -88.1,37.77,-86.24,41.77 2 6759 +proj=tmerc +lat_0=37.5 +lon_0=-87.0833333333333 +k=0.999966667 +x_0=900000 +y_0=249999.9998984 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3536 projected NAD83(NSRS2007) / Iowa North -96.65,41.85,-90.15,43.51 2 6759 +proj=lcc +lat_0=41.5 +lon_0=-93.5 +lat_1=43.2666666666667 +lat_2=42.0666666666667 +x_0=1500000 +y_0=1000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3537 projected NAD83(NSRS2007) / Iowa North (ftUS) -96.65,41.85,-90.15,43.51 2 6759 +proj=lcc +lat_0=41.5 +lon_0=-93.5 +lat_1=43.2666666666667 +lat_2=42.0666666666667 +x_0=1500000 +y_0=999999.99998984 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3538 projected NAD83(NSRS2007) / Iowa South -96.14,40.36,-90.14,42.04 2 6759 +proj=lcc +lat_0=40 +lon_0=-93.5 +lat_1=41.7833333333333 +lat_2=40.6166666666667 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3539 projected NAD83(NSRS2007) / Iowa South (ftUS) -96.14,40.36,-90.14,42.04 2 6759 +proj=lcc +lat_0=40 +lon_0=-93.5 +lat_1=41.7833333333333 +lat_2=40.6166666666667 +x_0=500000.00001016 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3540 projected NAD83(NSRS2007) / Kansas North -102.06,38.52,-94.58,40.01 2 6759 +proj=lcc +lat_0=38.3333333333333 +lon_0=-98 +lat_1=39.7833333333333 +lat_2=38.7166666666667 +x_0=400000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3541 projected NAD83(NSRS2007) / Kansas North (ftUS) -102.06,38.52,-94.58,40.01 2 6759 +proj=lcc +lat_0=38.3333333333333 +lon_0=-98 +lat_1=39.7833333333333 +lat_2=38.7166666666667 +x_0=399999.99998984 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3542 projected NAD83(NSRS2007) / Kansas South -102.05,36.99,-94.6,38.88 2 6759 +proj=lcc +lat_0=36.6666666666667 +lon_0=-98.5 +lat_1=38.5666666666667 +lat_2=37.2666666666667 +x_0=400000 +y_0=400000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3543 projected NAD83(NSRS2007) / Kansas South (ftUS) -102.05,36.99,-94.6,38.88 2 6759 +proj=lcc +lat_0=36.6666666666667 +lon_0=-98.5 +lat_1=38.5666666666667 +lat_2=37.2666666666667 +x_0=399999.99998984 +y_0=399999.99998984 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3544 projected NAD83(NSRS2007) / Kentucky North -85.96,37.71,-82.47,39.15 2 6759 +proj=lcc +lat_0=37.5 +lon_0=-84.25 +lat_1=37.9666666666667 +lat_2=38.9666666666667 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3545 projected NAD83(NSRS2007) / Kentucky North (ftUS) -85.96,37.71,-82.47,39.15 2 6759 +proj=lcc +lat_0=37.5 +lon_0=-84.25 +lat_1=37.9666666666667 +lat_2=38.9666666666667 +x_0=500000.0001016 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3546 projected NAD83(NSRS2007) / Kentucky Single Zone -89.57,36.49,-81.95,39.15 2 6759 +proj=lcc +lat_0=36.3333333333333 +lon_0=-85.75 +lat_1=37.0833333333333 +lat_2=38.6666666666667 +x_0=1500000 +y_0=1000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3547 projected NAD83(NSRS2007) / Kentucky Single Zone (ftUS) -89.57,36.49,-81.95,39.15 2 6759 +proj=lcc +lat_0=36.3333333333333 +lon_0=-85.75 +lat_1=37.0833333333333 +lat_2=38.6666666666667 +x_0=1500000 +y_0=999999.9998984 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3548 projected NAD83(NSRS2007) / Kentucky South -89.57,36.49,-81.95,38.17 2 6759 +proj=lcc +lat_0=36.3333333333333 +lon_0=-85.75 +lat_1=37.9333333333333 +lat_2=36.7333333333333 +x_0=500000 +y_0=500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3549 projected NAD83(NSRS2007) / Kentucky South (ftUS) -89.57,36.49,-81.95,38.17 2 6759 +proj=lcc +lat_0=36.3333333333333 +lon_0=-85.75 +lat_1=37.9333333333333 +lat_2=36.7333333333333 +x_0=500000.0001016 +y_0=500000.0001016 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3550 projected NAD83(NSRS2007) / Louisiana North -94.05,30.85,-90.86,33.03 2 6759 +proj=lcc +lat_0=30.5 +lon_0=-92.5 +lat_1=32.6666666666667 +lat_2=31.1666666666667 +x_0=1000000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3551 projected NAD83(NSRS2007) / Louisiana North (ftUS) -94.05,30.85,-90.86,33.03 2 6759 +proj=lcc +lat_0=30.5 +lon_0=-92.5 +lat_1=32.6666666666667 +lat_2=31.1666666666667 +x_0=999999.99998984 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3552 projected NAD83(NSRS2007) / Louisiana South -93.94,28.85,-88.75,31.07 2 6759 +proj=lcc +lat_0=28.5 +lon_0=-91.3333333333333 +lat_1=30.7 +lat_2=29.3 +x_0=1000000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3553 projected NAD83(NSRS2007) / Louisiana South (ftUS) -93.94,28.85,-88.75,31.07 2 6759 +proj=lcc +lat_0=28.5 +lon_0=-91.3333333333333 +lat_1=30.7 +lat_2=29.3 +x_0=999999.99998984 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3554 projected NAD83(NSRS2007) / Maine CS2000 Central -70.03,43.75,-68.33,47.47 2 6759 +proj=tmerc +lat_0=43.5 +lon_0=-69.125 +k=0.99998 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3555 projected NAD83(NSRS2007) / Maine CS2000 East -68.58,44.18,-66.91,47.37 2 6759 +proj=tmerc +lat_0=43.8333333333333 +lon_0=-67.875 +k=0.99998 +x_0=700000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3556 projected NAD83(NSRS2007) / Maine CS2000 West -71.09,43.07,-69.6,46.58 2 6759 +proj=tmerc +lat_0=42.8333333333333 +lon_0=-70.375 +k=0.99998 +x_0=300000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3557 projected NAD83(NSRS2007) / Maine East -70.03,43.88,-66.91,47.47 2 6759 +proj=tmerc +lat_0=43.6666666666667 +lon_0=-68.5 +k=0.9999 +x_0=300000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3558 projected NAD83(NSRS2007) / Maine West -71.09,43.04,-69.26,46.58 2 6759 +proj=tmerc +lat_0=42.8333333333333 +lon_0=-70.1666666666667 +k=0.999966667 +x_0=900000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3559 projected NAD83(NSRS2007) / Maryland -79.49,37.97,-74.97,39.73 2 6759 +proj=lcc +lat_0=37.6666666666667 +lon_0=-77 +lat_1=39.45 +lat_2=38.3 +x_0=400000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3560 projected NAD83 / Utah North (ftUS) -114.04,40.55,-109.04,42.01 4 6269 +proj=lcc +lat_0=40.3333333333333 +lon_0=-111.5 +lat_1=41.7833333333333 +lat_2=40.7166666666667 +x_0=500000.00001016 +y_0=999999.99998984 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3561 projected Old Hawaiian / Hawaii zone 1 -156.1,18.87,-154.74,20.33 38 6135 +proj=tmerc +lat_0=18.8333333333333 +lon_0=-155.5 +k=0.999966667 +x_0=152400.30480061 +y_0=0 +ellps=clrk66 +towgs84=61,-285,-181 +to_meter=0.304800609601219 +no_defs +3562 projected Old Hawaiian / Hawaii zone 2 -157.36,20.45,-155.93,21.26 38 6135 +proj=tmerc +lat_0=20.3333333333333 +lon_0=-156.666666666667 +k=0.999966667 +x_0=152400.30480061 +y_0=0 +ellps=clrk66 +towgs84=61,-285,-181 +to_meter=0.304800609601219 +no_defs +3563 projected Old Hawaiian / Hawaii zone 3 -158.33,21.2,-157.61,21.75 14 6135 +proj=tmerc +lat_0=21.1666666666667 +lon_0=-158 +k=0.99999 +x_0=152400.30480061 +y_0=0 +ellps=clrk66 +towgs84=58,-283,-182 +to_meter=0.304800609601219 +no_defs +3564 projected Old Hawaiian / Hawaii zone 4 -159.85,21.81,-159.23,22.29 35 6135 +proj=tmerc +lat_0=21.8333333333333 +lon_0=-159.5 +k=0.99999 +x_0=152400.30480061 +y_0=0 +ellps=clrk66 +towgs84=45,-290,-172 +to_meter=0.304800609601219 +no_defs +3565 projected Old Hawaiian / Hawaii zone 5 -160.3,21.73,-159.99,22.07 38 6135 +proj=tmerc +lat_0=21.6666666666667 +lon_0=-160.166666666667 +k=1 +x_0=152400.30480061 +y_0=0 +ellps=clrk66 +towgs84=61,-285,-181 +to_meter=0.304800609601219 +no_defs +3566 projected NAD83 / Utah Central (ftUS) -114.05,38.49,-109.04,41.08 4 6269 +proj=lcc +lat_0=38.3333333333333 +lon_0=-111.5 +lat_1=40.65 +lat_2=39.0166666666667 +x_0=500000.00001016 +y_0=2000000.00001016 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3567 projected NAD83 / Utah South (ftUS) -114.05,36.99,-109.04,38.58 4 6269 +proj=lcc +lat_0=36.6666666666667 +lon_0=-111.5 +lat_1=38.35 +lat_2=37.2166666666667 +x_0=500000.00001016 +y_0=3000000 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3568 projected NAD83(HARN) / Utah North (ftUS) -114.04,40.55,-109.04,42.01 1 6152 +proj=lcc +lat_0=40.3333333333333 +lon_0=-111.5 +lat_1=41.7833333333333 +lat_2=40.7166666666667 +x_0=500000.00001016 +y_0=999999.99998984 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +3569 projected NAD83(HARN) / Utah Central (ftUS) -114.05,38.49,-109.04,41.08 1 6152 +proj=lcc +lat_0=38.3333333333333 +lon_0=-111.5 +lat_1=40.65 +lat_2=39.0166666666667 +x_0=500000.00001016 +y_0=2000000.00001016 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +3570 projected NAD83(HARN) / Utah South (ftUS) -114.05,36.99,-109.04,38.58 1 6152 +proj=lcc +lat_0=36.6666666666667 +lon_0=-111.5 +lat_1=38.35 +lat_2=37.2166666666667 +x_0=500000.00001016 +y_0=3000000 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +3571 projected WGS 84 / North Pole LAEA Bering Sea -180,45,180,90 6326 +proj=laea +lat_0=90 +lon_0=180 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3572 projected WGS 84 / North Pole LAEA Alaska -180,45,180,90 6326 +proj=laea +lat_0=90 +lon_0=-150 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3573 projected WGS 84 / North Pole LAEA Canada -180,45,180,90 6326 +proj=laea +lat_0=90 +lon_0=-100 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3574 projected WGS 84 / North Pole LAEA Atlantic -180,45,180,90 6326 +proj=laea +lat_0=90 +lon_0=-40 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3575 projected WGS 84 / North Pole LAEA Europe -180,45,180,90 6326 +proj=laea +lat_0=90 +lon_0=10 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3576 projected WGS 84 / North Pole LAEA Russia -180,45,180,90 6326 +proj=laea +lat_0=90 +lon_0=90 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3577 projected GDA94 / Australian Albers 112.85,-43.7,153.69,-9.86 3 6283 +proj=aea +lat_0=0 +lon_0=132 +lat_1=-18 +lat_2=-36 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3578 projected NAD83 / Yukon Albers -141.01,59.99,-123.91,69.7 4 6269 +proj=aea +lat_0=59 +lon_0=-132.5 +lat_1=61.6666666666667 +lat_2=68 +x_0=500000 +y_0=500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3579 projected NAD83(CSRS) / Yukon Albers -141.01,59.99,-123.91,69.7 1 6140 +proj=aea +lat_0=59 +lon_0=-132.5 +lat_1=61.6666666666667 +lat_2=68 +x_0=500000 +y_0=500000 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +3580 projected NAD83 / NWT Lambert -136.46,59.98,-102,78.81 4 6269 +proj=lcc +lat_0=0 +lon_0=-112 +lat_1=62 +lat_2=70 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3581 projected NAD83(CSRS) / NWT Lambert -136.46,59.98,-102,78.81 1 6140 +proj=lcc +lat_0=0 +lon_0=-112 +lat_1=62 +lat_2=70 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +3582 projected NAD83(NSRS2007) / Maryland (ftUS) -79.49,37.97,-74.97,39.73 2 6759 +proj=lcc +lat_0=37.6666666666667 +lon_0=-77 +lat_1=39.45 +lat_2=38.3 +x_0=399999.9998984 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3583 projected NAD83(NSRS2007) / Massachusetts Island -70.91,41.19,-69.89,41.51 2 6759 +proj=lcc +lat_0=41 +lon_0=-70.5 +lat_1=41.4833333333333 +lat_2=41.2833333333333 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3584 projected NAD83(NSRS2007) / Massachusetts Island (ftUS) -70.91,41.19,-69.89,41.51 2 6759 +proj=lcc +lat_0=41 +lon_0=-70.5 +lat_1=41.4833333333333 +lat_2=41.2833333333333 +x_0=500000.0001016 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3585 projected NAD83(NSRS2007) / Massachusetts Mainland -73.5,41.46,-69.86,42.89 2 6759 +proj=lcc +lat_0=41 +lon_0=-71.5 +lat_1=42.6833333333333 +lat_2=41.7166666666667 +x_0=200000 +y_0=750000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3586 projected NAD83(NSRS2007) / Massachusetts Mainland (ftUS) -73.5,41.46,-69.86,42.89 2 6759 +proj=lcc +lat_0=41 +lon_0=-71.5 +lat_1=42.6833333333333 +lat_2=41.7166666666667 +x_0=200000.0001016 +y_0=750000 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3587 projected NAD83(NSRS2007) / Michigan Central -87.06,43.8,-82.27,45.92 2 6759 +proj=lcc +lat_0=43.3166666666667 +lon_0=-84.3666666666667 +lat_1=45.7 +lat_2=44.1833333333333 +x_0=6000000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3588 projected NAD83(NSRS2007) / Michigan Central (ft) -87.06,43.8,-82.27,45.92 2 6759 +proj=lcc +lat_0=43.3166666666667 +lon_0=-84.3666666666667 +lat_1=45.7 +lat_2=44.1833333333333 +x_0=5999999.999976 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +3589 projected NAD83(NSRS2007) / Michigan North -90.42,45.08,-83.44,48.32 2 6759 +proj=lcc +lat_0=44.7833333333333 +lon_0=-87 +lat_1=47.0833333333333 +lat_2=45.4833333333333 +x_0=8000000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3590 projected NAD83(NSRS2007) / Michigan North (ft) -90.42,45.08,-83.44,48.32 2 6759 +proj=lcc +lat_0=44.7833333333333 +lon_0=-87 +lat_1=47.0833333333333 +lat_2=45.4833333333333 +x_0=7999999.999968 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +3591 projected NAD83(NSRS2007) / Michigan Oblique Mercator -90.42,41.69,-82.13,48.32 2 6759 +proj=omerc +no_uoff +lat_0=45.3091666666667 +lonc=-86 +alpha=337.25556 +gamma=337.25556 +k=0.9996 +x_0=2546731.496 +y_0=-4354009.816 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3592 projected NAD83(NSRS2007) / Michigan South -87.2,41.69,-82.13,44.22 2 6759 +proj=lcc +lat_0=41.5 +lon_0=-84.3666666666667 +lat_1=43.6666666666667 +lat_2=42.1 +x_0=4000000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3593 projected NAD83(NSRS2007) / Michigan South (ft) -87.2,41.69,-82.13,44.22 2 6759 +proj=lcc +lat_0=41.5 +lon_0=-84.3666666666667 +lat_1=43.6666666666667 +lat_2=42.1 +x_0=3999999.999984 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +3594 projected NAD83(NSRS2007) / Minnesota Central -96.86,45.28,-92.29,47.48 2 6759 +proj=lcc +lat_0=45 +lon_0=-94.25 +lat_1=47.05 +lat_2=45.6166666666667 +x_0=800000 +y_0=100000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3595 projected NAD83(NSRS2007) / Minnesota North -97.22,46.64,-89.49,49.38 2 6759 +proj=lcc +lat_0=46.5 +lon_0=-93.1 +lat_1=48.6333333333333 +lat_2=47.0333333333333 +x_0=800000 +y_0=100000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3596 projected NAD83(NSRS2007) / Minnesota South -96.85,43.49,-91.21,45.59 2 6759 +proj=lcc +lat_0=43 +lon_0=-94 +lat_1=45.2166666666667 +lat_2=43.7833333333333 +x_0=800000 +y_0=100000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3597 projected NAD83(NSRS2007) / Mississippi East -89.97,30.01,-88.09,35.01 2 6759 +proj=tmerc +lat_0=29.5 +lon_0=-88.8333333333333 +k=0.99995 +x_0=300000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3598 projected NAD83(NSRS2007) / Mississippi East (ftUS) -89.97,30.01,-88.09,35.01 2 6759 +proj=tmerc +lat_0=29.5 +lon_0=-88.8333333333333 +k=0.99995 +x_0=300000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3599 projected NAD83(NSRS2007) / Mississippi West -91.65,31,-89.37,35.01 2 6759 +proj=tmerc +lat_0=29.5 +lon_0=-90.3333333333333 +k=0.99995 +x_0=700000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3600 projected NAD83(NSRS2007) / Mississippi West (ftUS) -91.65,31,-89.37,35.01 2 6759 +proj=tmerc +lat_0=29.5 +lon_0=-90.3333333333333 +k=0.99995 +x_0=699999.9998984 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3601 projected NAD83(NSRS2007) / Missouri Central -93.79,36.48,-91.41,40.61 2 6759 +proj=tmerc +lat_0=35.8333333333333 +lon_0=-92.5 +k=0.999933333 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3602 projected NAD83(NSRS2007) / Missouri East -91.97,35.98,-89.1,40.61 2 6759 +proj=tmerc +lat_0=35.8333333333333 +lon_0=-90.5 +k=0.999933333 +x_0=250000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3603 projected NAD83(NSRS2007) / Missouri West -95.77,36.48,-93.48,40.59 2 6759 +proj=tmerc +lat_0=36.1666666666667 +lon_0=-94.5 +k=0.999941177 +x_0=850000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3604 projected NAD83(NSRS2007) / Montana -116.07,44.35,-104.04,49.01 2 6759 +proj=lcc +lat_0=44.25 +lon_0=-109.5 +lat_1=49 +lat_2=45 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3605 projected NAD83(NSRS2007) / Montana (ft) -116.07,44.35,-104.04,49.01 2 6759 +proj=lcc +lat_0=44.25 +lon_0=-109.5 +lat_1=49 +lat_2=45 +x_0=599999.9999976 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +3606 projected NAD83(NSRS2007) / Nebraska -104.06,39.99,-95.3,43.01 2 6759 +proj=lcc +lat_0=39.8333333333333 +lon_0=-100 +lat_1=43 +lat_2=40 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3607 projected NAD83(NSRS2007) / Nevada Central -118.19,36,-114.99,41 2 6759 +proj=tmerc +lat_0=34.75 +lon_0=-116.666666666667 +k=0.9999 +x_0=500000 +y_0=6000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3608 projected NAD83(NSRS2007) / Nevada Central (ftUS) -118.19,36,-114.99,41 2 6759 +proj=tmerc +lat_0=34.75 +lon_0=-116.666666666667 +k=0.9999 +x_0=500000.00001016 +y_0=6000000 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3609 projected NAD83(NSRS2007) / Nevada East -117.01,34.99,-114.03,42 2 6759 +proj=tmerc +lat_0=34.75 +lon_0=-115.583333333333 +k=0.9999 +x_0=200000 +y_0=8000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3610 projected NAD83(NSRS2007) / Nevada East (ftUS) -117.01,34.99,-114.03,42 2 6759 +proj=tmerc +lat_0=34.75 +lon_0=-115.583333333333 +k=0.9999 +x_0=200000.00001016 +y_0=8000000.00001016 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3611 projected NAD83(NSRS2007) / Nevada West -120,36.95,-116.99,42 2 6759 +proj=tmerc +lat_0=34.75 +lon_0=-118.583333333333 +k=0.9999 +x_0=800000 +y_0=4000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3612 projected NAD83(NSRS2007) / Nevada West (ftUS) -120,36.95,-116.99,42 2 6759 +proj=tmerc +lat_0=34.75 +lon_0=-118.583333333333 +k=0.9999 +x_0=800000.00001016 +y_0=3999999.99998984 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3613 projected NAD83(NSRS2007) / New Hampshire -72.56,42.69,-70.63,45.31 2 6759 +proj=tmerc +lat_0=42.5 +lon_0=-71.6666666666667 +k=0.999966667 +x_0=300000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3614 projected NAD83(NSRS2007) / New Hampshire (ftUS) -72.56,42.69,-70.63,45.31 2 6759 +proj=tmerc +lat_0=42.5 +lon_0=-71.6666666666667 +k=0.999966667 +x_0=300000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3615 projected NAD83(NSRS2007) / New Jersey -75.6,38.87,-73.88,41.36 2 6759 +proj=tmerc +lat_0=38.8333333333333 +lon_0=-74.5 +k=0.9999 +x_0=150000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3616 projected NAD83(NSRS2007) / New Jersey (ftUS) -75.6,38.87,-73.88,41.36 2 6759 +proj=tmerc +lat_0=38.8333333333333 +lon_0=-74.5 +k=0.9999 +x_0=150000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3617 projected NAD83(NSRS2007) / New Mexico Central -107.73,31.78,-104.84,37 2 6759 +proj=tmerc +lat_0=31 +lon_0=-106.25 +k=0.9999 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3618 projected NAD83(NSRS2007) / New Mexico Central (ftUS) -107.73,31.78,-104.84,37 2 6759 +proj=tmerc +lat_0=31 +lon_0=-106.25 +k=0.9999 +x_0=500000.0001016 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3619 projected NAD83(NSRS2007) / New Mexico East -105.72,32,-102.99,37 2 6759 +proj=tmerc +lat_0=31 +lon_0=-104.333333333333 +k=0.999909091 +x_0=165000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3620 projected NAD83(NSRS2007) / New Mexico East (ftUS) -105.72,32,-102.99,37 2 6759 +proj=tmerc +lat_0=31 +lon_0=-104.333333333333 +k=0.999909091 +x_0=165000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3621 projected NAD83(NSRS2007) / New Mexico West -109.06,31.33,-106.32,37 2 6759 +proj=tmerc +lat_0=31 +lon_0=-107.833333333333 +k=0.999916667 +x_0=830000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3622 projected NAD83(NSRS2007) / New Mexico West (ftUS) -109.06,31.33,-106.32,37 2 6759 +proj=tmerc +lat_0=31 +lon_0=-107.833333333333 +k=0.999916667 +x_0=830000.0001016 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3623 projected NAD83(NSRS2007) / New York Central -77.75,41.99,-75.06,44.41 2 6759 +proj=tmerc +lat_0=40 +lon_0=-76.5833333333333 +k=0.9999375 +x_0=250000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3624 projected NAD83(NSRS2007) / New York Central (ftUS) -77.75,41.99,-75.06,44.41 2 6759 +proj=tmerc +lat_0=40 +lon_0=-76.5833333333333 +k=0.9999375 +x_0=249999.9998984 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3625 projected NAD83(NSRS2007) / New York East -75.87,40.88,-73.23,45.02 2 6759 +proj=tmerc +lat_0=38.8333333333333 +lon_0=-74.5 +k=0.9999 +x_0=150000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3626 projected NAD83(NSRS2007) / New York East (ftUS) -75.87,40.88,-73.23,45.02 2 6759 +proj=tmerc +lat_0=38.8333333333333 +lon_0=-74.5 +k=0.9999 +x_0=150000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3627 projected NAD83(NSRS2007) / New York Long Island -74.26,40.47,-71.8,41.3 2 6759 +proj=lcc +lat_0=40.1666666666667 +lon_0=-74 +lat_1=41.0333333333333 +lat_2=40.6666666666667 +x_0=300000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3628 projected NAD83(NSRS2007) / New York Long Island (ftUS) -74.26,40.47,-71.8,41.3 2 6759 +proj=lcc +lat_0=40.1666666666667 +lon_0=-74 +lat_1=41.0333333333333 +lat_2=40.6666666666667 +x_0=300000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3629 projected NAD83(NSRS2007) / New York West -79.77,41.99,-77.36,43.64 2 6759 +proj=tmerc +lat_0=40 +lon_0=-78.5833333333333 +k=0.9999375 +x_0=350000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3630 projected NAD83(NSRS2007) / New York West (ftUS) -79.77,41.99,-77.36,43.64 2 6759 +proj=tmerc +lat_0=40 +lon_0=-78.5833333333333 +k=0.9999375 +x_0=350000.0001016 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3631 projected NAD83(NSRS2007) / North Carolina -84.33,33.83,-75.38,36.59 2 6759 +proj=lcc +lat_0=33.75 +lon_0=-79 +lat_1=36.1666666666667 +lat_2=34.3333333333333 +x_0=609601.22 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3632 projected NAD83(NSRS2007) / North Carolina (ftUS) -84.33,33.83,-75.38,36.59 2 6759 +proj=lcc +lat_0=33.75 +lon_0=-79 +lat_1=36.1666666666667 +lat_2=34.3333333333333 +x_0=609601.219202438 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3633 projected NAD83(NSRS2007) / North Dakota North -104.07,47.15,-96.83,49.01 2 6759 +proj=lcc +lat_0=47 +lon_0=-100.5 +lat_1=48.7333333333333 +lat_2=47.4333333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3634 projected NAD83(NSRS2007) / North Dakota North (ft) -104.07,47.15,-96.83,49.01 2 6759 +proj=lcc +lat_0=47 +lon_0=-100.5 +lat_1=48.7333333333333 +lat_2=47.4333333333333 +x_0=599999.9999976 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +3635 projected NAD83(NSRS2007) / North Dakota South -104.05,45.93,-96.55,47.83 2 6759 +proj=lcc +lat_0=45.6666666666667 +lon_0=-100.5 +lat_1=47.4833333333333 +lat_2=46.1833333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3636 projected NAD83(NSRS2007) / North Dakota South (ft) -104.05,45.93,-96.55,47.83 2 6759 +proj=lcc +lat_0=45.6666666666667 +lon_0=-100.5 +lat_1=47.4833333333333 +lat_2=46.1833333333333 +x_0=599999.9999976 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +3637 projected NAD83(NSRS2007) / Ohio North -84.81,40.1,-80.51,42.33 2 6759 +proj=lcc +lat_0=39.6666666666667 +lon_0=-82.5 +lat_1=41.7 +lat_2=40.4333333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3638 projected NAD83(NSRS2007) / Ohio South -84.83,38.4,-80.7,40.36 2 6759 +proj=lcc +lat_0=38 +lon_0=-82.5 +lat_1=40.0333333333333 +lat_2=38.7333333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3639 projected NAD83(NSRS2007) / Oklahoma North -103,35.27,-94.42,37.01 2 6759 +proj=lcc +lat_0=35 +lon_0=-98 +lat_1=36.7666666666667 +lat_2=35.5666666666667 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3640 projected NAD83(NSRS2007) / Oklahoma North (ftUS) -103,35.27,-94.42,37.01 2 6759 +proj=lcc +lat_0=35 +lon_0=-98 +lat_1=36.7666666666667 +lat_2=35.5666666666667 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3641 projected NAD83(NSRS2007) / Oklahoma South -100,33.62,-94.42,35.57 2 6759 +proj=lcc +lat_0=33.3333333333333 +lon_0=-98 +lat_1=35.2333333333333 +lat_2=33.9333333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3642 projected NAD83(NSRS2007) / Oklahoma South (ftUS) -100,33.62,-94.42,35.57 2 6759 +proj=lcc +lat_0=33.3333333333333 +lon_0=-98 +lat_1=35.2333333333333 +lat_2=33.9333333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3643 projected NAD83(NSRS2007) / Oregon LCC (m) -124.6,41.98,-116.47,46.26 2 6759 +proj=lcc +lat_0=41.75 +lon_0=-120.5 +lat_1=43 +lat_2=45.5 +x_0=400000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3644 projected NAD83(NSRS2007) / Oregon GIC Lambert (ft) -124.6,41.98,-116.47,46.26 2 6759 +proj=lcc +lat_0=41.75 +lon_0=-120.5 +lat_1=43 +lat_2=45.5 +x_0=399999.9999984 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +3645 projected NAD83(NSRS2007) / Oregon North -124.17,43.95,-116.47,46.26 2 6759 +proj=lcc +lat_0=43.6666666666667 +lon_0=-120.5 +lat_1=46 +lat_2=44.3333333333333 +x_0=2500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3646 projected NAD83(NSRS2007) / Oregon North (ft) -124.17,43.95,-116.47,46.26 2 6759 +proj=lcc +lat_0=43.6666666666667 +lon_0=-120.5 +lat_1=46 +lat_2=44.3333333333333 +x_0=2500000.0001424 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +3647 projected NAD83(NSRS2007) / Oregon South -124.6,41.98,-116.9,44.56 2 6759 +proj=lcc +lat_0=41.6666666666667 +lon_0=-120.5 +lat_1=44 +lat_2=42.3333333333333 +x_0=1500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3648 projected NAD83(NSRS2007) / Oregon South (ft) -124.6,41.98,-116.9,44.56 2 6759 +proj=lcc +lat_0=41.6666666666667 +lon_0=-120.5 +lat_1=44 +lat_2=42.3333333333333 +x_0=1500000.0001464 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +3649 projected NAD83(NSRS2007) / Pennsylvania North -80.53,40.6,-74.7,42.53 2 6759 +proj=lcc +lat_0=40.1666666666667 +lon_0=-77.75 +lat_1=41.95 +lat_2=40.8833333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3650 projected NAD83(NSRS2007) / Pennsylvania North (ftUS) -80.53,40.6,-74.7,42.53 2 6759 +proj=lcc +lat_0=40.1666666666667 +lon_0=-77.75 +lat_1=41.95 +lat_2=40.8833333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3651 projected NAD83(NSRS2007) / Pennsylvania South -80.53,39.71,-74.72,41.18 2 6759 +proj=lcc +lat_0=39.3333333333333 +lon_0=-77.75 +lat_1=40.9666666666667 +lat_2=39.9333333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3652 projected NAD83(NSRS2007) / Pennsylvania South (ftUS) -80.53,39.71,-74.72,41.18 2 6759 +proj=lcc +lat_0=39.3333333333333 +lon_0=-77.75 +lat_1=40.9666666666667 +lat_2=39.9333333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3653 projected NAD83(NSRS2007) / Rhode Island -71.85,41.13,-71.08,42.02 2 6759 +proj=tmerc +lat_0=41.0833333333333 +lon_0=-71.5 +k=0.99999375 +x_0=100000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3654 projected NAD83(NSRS2007) / Rhode Island (ftUS) -71.85,41.13,-71.08,42.02 2 6759 +proj=tmerc +lat_0=41.0833333333333 +lon_0=-71.5 +k=0.99999375 +x_0=99999.99998984 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3655 projected NAD83(NSRS2007) / South Carolina -83.36,32.05,-78.52,35.21 2 6759 +proj=lcc +lat_0=31.8333333333333 +lon_0=-81 +lat_1=34.8333333333333 +lat_2=32.5 +x_0=609600 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3656 projected NAD83(NSRS2007) / South Carolina (ft) -83.36,32.05,-78.52,35.21 2 6759 +proj=lcc +lat_0=31.8333333333333 +lon_0=-81 +lat_1=34.8333333333333 +lat_2=32.5 +x_0=609600 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +3657 projected NAD83(NSRS2007) / South Dakota North -104.07,44.14,-96.45,45.95 2 6759 +proj=lcc +lat_0=43.8333333333333 +lon_0=-100 +lat_1=45.6833333333333 +lat_2=44.4166666666667 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3658 projected NAD83(NSRS2007) / South Dakota North (ftUS) -104.07,44.14,-96.45,45.95 2 6759 +proj=lcc +lat_0=43.8333333333333 +lon_0=-100 +lat_1=45.6833333333333 +lat_2=44.4166666666667 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3659 projected NAD83(NSRS2007) / South Dakota South -104.06,42.48,-96.43,44.79 2 6759 +proj=lcc +lat_0=42.3333333333333 +lon_0=-100.333333333333 +lat_1=44.4 +lat_2=42.8333333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3660 projected NAD83(NSRS2007) / South Dakota South (ftUS) -104.06,42.48,-96.43,44.79 2 6759 +proj=lcc +lat_0=42.3333333333333 +lon_0=-100.333333333333 +lat_1=44.4 +lat_2=42.8333333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3661 projected NAD83(NSRS2007) / Tennessee -90.31,34.98,-81.65,36.68 2 6759 +proj=lcc +lat_0=34.3333333333333 +lon_0=-86 +lat_1=36.4166666666667 +lat_2=35.25 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3662 projected NAD83(NSRS2007) / Tennessee (ftUS) -90.31,34.98,-81.65,36.68 2 6759 +proj=lcc +lat_0=34.3333333333333 +lon_0=-86 +lat_1=36.4166666666667 +lat_2=35.25 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3663 projected NAD83(NSRS2007) / Texas Central -106.66,29.78,-93.5,32.27 2 6759 +proj=lcc +lat_0=29.6666666666667 +lon_0=-100.333333333333 +lat_1=31.8833333333333 +lat_2=30.1166666666667 +x_0=700000 +y_0=3000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3664 projected NAD83(NSRS2007) / Texas Central (ftUS) -106.66,29.78,-93.5,32.27 2 6759 +proj=lcc +lat_0=29.6666666666667 +lon_0=-100.333333333333 +lat_1=31.8833333333333 +lat_2=30.1166666666667 +x_0=699999.9998984 +y_0=3000000 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3665 projected NAD83(NSRS2007) / Texas Centric Albers Equal Area -106.66,25.83,-93.5,36.5 2 6759 +proj=aea +lat_0=18 +lon_0=-100 +lat_1=27.5 +lat_2=35 +x_0=1500000 +y_0=6000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3666 projected NAD83(NSRS2007) / Texas Centric Lambert Conformal -106.66,25.83,-93.5,36.5 2 6759 +proj=lcc +lat_0=18 +lon_0=-100 +lat_1=27.5 +lat_2=35 +x_0=1500000 +y_0=5000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3667 projected NAD83(NSRS2007) / Texas North -103.03,34.3,-99.99,36.5 2 6759 +proj=lcc +lat_0=34 +lon_0=-101.5 +lat_1=36.1833333333333 +lat_2=34.65 +x_0=200000 +y_0=1000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3668 projected NAD83(NSRS2007) / Texas North (ftUS) -103.03,34.3,-99.99,36.5 2 6759 +proj=lcc +lat_0=34 +lon_0=-101.5 +lat_1=36.1833333333333 +lat_2=34.65 +x_0=200000.0001016 +y_0=999999.9998984 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3669 projected NAD83(NSRS2007) / Texas North Central -103.07,31.72,-94,34.58 2 6759 +proj=lcc +lat_0=31.6666666666667 +lon_0=-98.5 +lat_1=33.9666666666667 +lat_2=32.1333333333333 +x_0=600000 +y_0=2000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3670 projected NAD83(NSRS2007) / Texas North Central (ftUS) -103.07,31.72,-94,34.58 2 6759 +proj=lcc +lat_0=31.6666666666667 +lon_0=-98.5 +lat_1=33.9666666666667 +lat_2=32.1333333333333 +x_0=600000 +y_0=2000000.0001016 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3671 projected NAD83(NSRS2007) / Texas South -100.2,25.83,-96.85,28.21 2 6759 +proj=lcc +lat_0=25.6666666666667 +lon_0=-98.5 +lat_1=27.8333333333333 +lat_2=26.1666666666667 +x_0=300000 +y_0=5000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3672 projected NAD83(NSRS2007) / Texas South (ftUS) -100.2,25.83,-96.85,28.21 2 6759 +proj=lcc +lat_0=25.6666666666667 +lon_0=-98.5 +lat_1=27.8333333333333 +lat_2=26.1666666666667 +x_0=300000 +y_0=5000000.0001016 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3673 projected NAD83(NSRS2007) / Texas South Central -105,27.78,-93.76,30.67 2 6759 +proj=lcc +lat_0=27.8333333333333 +lon_0=-99 +lat_1=30.2833333333333 +lat_2=28.3833333333333 +x_0=600000 +y_0=4000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3674 projected NAD83(NSRS2007) / Texas South Central (ftUS) -105,27.78,-93.76,30.67 2 6759 +proj=lcc +lat_0=27.8333333333333 +lon_0=-99 +lat_1=30.2833333333333 +lat_2=28.3833333333333 +x_0=600000 +y_0=3999999.9998984 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3675 projected NAD83(NSRS2007) / Utah Central -114.05,38.49,-109.04,41.08 2 6759 +proj=lcc +lat_0=38.3333333333333 +lon_0=-111.5 +lat_1=40.65 +lat_2=39.0166666666667 +x_0=500000 +y_0=2000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3676 projected NAD83(NSRS2007) / Utah Central (ft) -114.05,38.49,-109.04,41.08 2 6759 +proj=lcc +lat_0=38.3333333333333 +lon_0=-111.5 +lat_1=40.65 +lat_2=39.0166666666667 +x_0=500000.0001504 +y_0=1999999.999992 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +3677 projected NAD83(NSRS2007) / Utah Central (ftUS) -114.05,38.49,-109.04,41.08 2 6759 +proj=lcc +lat_0=38.3333333333333 +lon_0=-111.5 +lat_1=40.65 +lat_2=39.0166666666667 +x_0=500000.00001016 +y_0=2000000.00001016 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3678 projected NAD83(NSRS2007) / Utah North -114.04,40.55,-109.04,42.01 2 6759 +proj=lcc +lat_0=40.3333333333333 +lon_0=-111.5 +lat_1=41.7833333333333 +lat_2=40.7166666666667 +x_0=500000 +y_0=1000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3679 projected NAD83(NSRS2007) / Utah North (ft) -114.04,40.55,-109.04,42.01 2 6759 +proj=lcc +lat_0=40.3333333333333 +lon_0=-111.5 +lat_1=41.7833333333333 +lat_2=40.7166666666667 +x_0=500000.0001504 +y_0=999999.999996 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +3680 projected NAD83(NSRS2007) / Utah North (ftUS) -114.04,40.55,-109.04,42.01 2 6759 +proj=lcc +lat_0=40.3333333333333 +lon_0=-111.5 +lat_1=41.7833333333333 +lat_2=40.7166666666667 +x_0=500000.00001016 +y_0=999999.99998984 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3681 projected NAD83(NSRS2007) / Utah South -114.05,36.99,-109.04,38.58 2 6759 +proj=lcc +lat_0=36.6666666666667 +lon_0=-111.5 +lat_1=38.35 +lat_2=37.2166666666667 +x_0=500000 +y_0=3000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3682 projected NAD83(NSRS2007) / Utah South (ft) -114.05,36.99,-109.04,38.58 2 6759 +proj=lcc +lat_0=36.6666666666667 +lon_0=-111.5 +lat_1=38.35 +lat_2=37.2166666666667 +x_0=500000.0001504 +y_0=2999999.999988 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +3683 projected NAD83(NSRS2007) / Utah South (ftUS) -114.05,36.99,-109.04,38.58 2 6759 +proj=lcc +lat_0=36.6666666666667 +lon_0=-111.5 +lat_1=38.35 +lat_2=37.2166666666667 +x_0=500000.00001016 +y_0=3000000 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3684 projected NAD83(NSRS2007) / Vermont -73.44,42.72,-71.5,45.03 2 6759 +proj=tmerc +lat_0=42.5 +lon_0=-72.5 +k=0.999964286 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3685 projected NAD83(NSRS2007) / Virginia North -80.06,37.77,-76.51,39.46 2 6759 +proj=lcc +lat_0=37.6666666666667 +lon_0=-78.5 +lat_1=39.2 +lat_2=38.0333333333333 +x_0=3500000 +y_0=2000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3686 projected NAD83(NSRS2007) / Virginia North (ftUS) -80.06,37.77,-76.51,39.46 2 6759 +proj=lcc +lat_0=37.6666666666667 +lon_0=-78.5 +lat_1=39.2 +lat_2=38.0333333333333 +x_0=3500000.0001016 +y_0=2000000.0001016 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3687 projected NAD83(NSRS2007) / Virginia South -83.68,36.54,-75.31,38.28 2 6759 +proj=lcc +lat_0=36.3333333333333 +lon_0=-78.5 +lat_1=37.9666666666667 +lat_2=36.7666666666667 +x_0=3500000 +y_0=1000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3688 projected NAD83(NSRS2007) / Virginia South (ftUS) -83.68,36.54,-75.31,38.28 2 6759 +proj=lcc +lat_0=36.3333333333333 +lon_0=-78.5 +lat_1=37.9666666666667 +lat_2=36.7666666666667 +x_0=3500000.0001016 +y_0=999999.9998984 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3689 projected NAD83(NSRS2007) / Washington North -124.79,47.08,-117.02,49.05 2 6759 +proj=lcc +lat_0=47 +lon_0=-120.833333333333 +lat_1=48.7333333333333 +lat_2=47.5 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3690 projected NAD83(NSRS2007) / Washington North (ftUS) -124.79,47.08,-117.02,49.05 2 6759 +proj=lcc +lat_0=47 +lon_0=-120.833333333333 +lat_1=48.7333333333333 +lat_2=47.5 +x_0=500000.0001016 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3691 projected NAD83(NSRS2007) / Washington South -124.4,45.54,-116.91,47.61 2 6759 +proj=lcc +lat_0=45.3333333333333 +lon_0=-120.5 +lat_1=47.3333333333333 +lat_2=45.8333333333333 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3692 projected NAD83(NSRS2007) / Washington South (ftUS) -124.4,45.54,-116.91,47.61 2 6759 +proj=lcc +lat_0=45.3333333333333 +lon_0=-120.5 +lat_1=47.3333333333333 +lat_2=45.8333333333333 +x_0=500000.0001016 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3693 projected NAD83(NSRS2007) / West Virginia North -81.76,38.76,-77.72,40.64 2 6759 +proj=lcc +lat_0=38.5 +lon_0=-79.5 +lat_1=40.25 +lat_2=39 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3694 projected NAD83(NSRS2007) / West Virginia South -82.65,37.2,-79.05,39.17 2 6759 +proj=lcc +lat_0=37 +lon_0=-81 +lat_1=38.8833333333333 +lat_2=37.4833333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3695 projected NAD83(NSRS2007) / Wisconsin Central -92.89,43.98,-86.25,45.8 2 6759 +proj=lcc +lat_0=43.8333333333333 +lon_0=-90 +lat_1=45.5 +lat_2=44.25 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3696 projected NAD83(NSRS2007) / Wisconsin Central (ftUS) -92.89,43.98,-86.25,45.8 2 6759 +proj=lcc +lat_0=43.8333333333333 +lon_0=-90 +lat_1=45.5 +lat_2=44.25 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3697 projected NAD83(NSRS2007) / Wisconsin North -92.89,45.37,-88.05,47.31 2 6759 +proj=lcc +lat_0=45.1666666666667 +lon_0=-90 +lat_1=46.7666666666667 +lat_2=45.5666666666667 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3698 projected NAD83(NSRS2007) / Wisconsin North (ftUS) -92.89,45.37,-88.05,47.31 2 6759 +proj=lcc +lat_0=45.1666666666667 +lon_0=-90 +lat_1=46.7666666666667 +lat_2=45.5666666666667 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3699 projected NAD83(NSRS2007) / Wisconsin South -91.43,42.48,-86.95,44.33 2 6759 +proj=lcc +lat_0=42 +lon_0=-90 +lat_1=44.0666666666667 +lat_2=42.7333333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3700 projected NAD83(NSRS2007) / Wisconsin South (ftUS) -91.43,42.48,-86.95,44.33 2 6759 +proj=lcc +lat_0=42 +lon_0=-90 +lat_1=44.0666666666667 +lat_2=42.7333333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3701 projected NAD83(NSRS2007) / Wisconsin Transverse Mercator -92.89,42.48,-86.25,47.31 2 6759 +proj=tmerc +lat_0=0 +lon_0=-90 +k=0.9996 +x_0=520000 +y_0=-4480000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3702 projected NAD83(NSRS2007) / Wyoming East -106.33,40.99,-104.05,45.01 2 6759 +proj=tmerc +lat_0=40.5 +lon_0=-105.166666666667 +k=0.9999375 +x_0=200000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3703 projected NAD83(NSRS2007) / Wyoming East Central -108.63,40.99,-106,45.01 2 6759 +proj=tmerc +lat_0=40.5 +lon_0=-107.333333333333 +k=0.9999375 +x_0=400000 +y_0=100000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3704 projected NAD83(NSRS2007) / Wyoming West Central -111.06,40.99,-107.5,45.01 2 6759 +proj=tmerc +lat_0=40.5 +lon_0=-108.75 +k=0.9999375 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3705 projected NAD83(NSRS2007) / Wyoming West -111.06,40.99,-109.04,44.67 2 6759 +proj=tmerc +lat_0=40.5 +lon_0=-110.083333333333 +k=0.9999375 +x_0=800000 +y_0=100000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3706 projected NAD83(NSRS2007) / UTM zone 59N 167.65,49.01,174.01,56.28 2 6759 +proj=utm +zone=59 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3707 projected NAD83(NSRS2007) / UTM zone 60N 174,47.92,180,56.67 2 6759 +proj=utm +zone=60 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3708 projected NAD83(NSRS2007) / UTM zone 1N -180,47.88,-173.99,63.21 2 6759 +proj=utm +zone=1 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3709 projected NAD83(NSRS2007) / UTM zone 2N -174,48.66,-167.99,73.05 2 6759 +proj=utm +zone=2 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3710 projected NAD83(NSRS2007) / UTM zone 3N -168,49.52,-161.99,74.29 2 6759 +proj=utm +zone=3 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3711 projected NAD83(NSRS2007) / UTM zone 4N -162,50.98,-155.99,74.71 2 6759 +proj=utm +zone=4 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3712 projected NAD83(NSRS2007) / UTM zone 5N -156,52.15,-149.99,74.71 2 6759 +proj=utm +zone=5 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3713 projected NAD83(NSRS2007) / UTM zone 6N -150,54.05,-143.99,74.13 2 6759 +proj=utm +zone=6 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3714 projected NAD83(NSRS2007) / UTM zone 7N -144,53.47,-137.99,73.59 2 6759 +proj=utm +zone=7 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3715 projected NAD83(NSRS2007) / UTM zone 8N -138,53.6,-131.99,73.04 2 6759 +proj=utm +zone=8 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3716 projected NAD83(NSRS2007) / UTM zone 9N -132,35.38,-126,56.84 2 6759 +proj=utm +zone=9 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3717 projected NAD83(NSRS2007) / UTM zone 10N -126,30.54,-119.99,49.09 2 6759 +proj=utm +zone=10 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3718 projected NAD83(NSRS2007) / UTM zone 11N -120,30.88,-114,49.01 2 6759 +proj=utm +zone=11 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3719 projected NAD83(NSRS2007) / UTM zone 12N -114,31.33,-108,49.01 2 6759 +proj=utm +zone=12 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3720 projected NAD83(NSRS2007) / UTM zone 13N -108,28.98,-102,49.01 2 6759 +proj=utm +zone=13 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3721 projected NAD83(NSRS2007) / UTM zone 14N -102,25.83,-96,49.01 2 6759 +proj=utm +zone=14 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3722 projected NAD83(NSRS2007) / UTM zone 15N -96.01,25.61,-90,49.38 2 6759 +proj=utm +zone=15 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3723 projected NAD83(NSRS2007) / UTM zone 16N -90,23.97,-84,48.32 2 6759 +proj=utm +zone=16 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3724 projected NAD83(NSRS2007) / UTM zone 17N -84,23.81,-78,46.13 2 6759 +proj=utm +zone=17 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3725 projected NAD83(NSRS2007) / UTM zone 18N -78,28.28,-72,45.03 2 6759 +proj=utm +zone=18 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3726 projected NAD83(NSRS2007) / UTM zone 19N -72,33.61,-65.99,47.47 2 6759 +proj=utm +zone=19 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3727 projected Reunion 1947 / TM Reunion 55.16,-21.42,55.91,-20.81 44 6626 +proj=tmerc +lat_0=-21.1166666666667 +lon_0=55.5333333333333 +k=1 +x_0=160000 +y_0=50000 +ellps=intl +towgs84=94,-948,-1262 +units=m +no_defs +3728 projected NAD83(NSRS2007) / Ohio North (ftUS) -84.81,40.1,-80.51,42.33 2 6759 +proj=lcc +lat_0=39.6666666666667 +lon_0=-82.5 +lat_1=41.7 +lat_2=40.4333333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3729 projected NAD83(NSRS2007) / Ohio South (ftUS) -84.83,38.4,-80.7,40.36 2 6759 +proj=lcc +lat_0=38 +lon_0=-82.5 +lat_1=40.0333333333333 +lat_2=38.7333333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3730 projected NAD83(NSRS2007) / Wyoming East (ftUS) -106.33,40.99,-104.05,45.01 2 6759 +proj=tmerc +lat_0=40.5 +lon_0=-105.166666666667 +k=0.9999375 +x_0=200000.00001016 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3731 projected NAD83(NSRS2007) / Wyoming East Central (ftUS) -108.63,40.99,-106,45.01 2 6759 +proj=tmerc +lat_0=40.5 +lon_0=-107.333333333333 +k=0.9999375 +x_0=399999.99998984 +y_0=99999.99998984 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3732 projected NAD83(NSRS2007) / Wyoming West Central (ftUS) -111.06,40.99,-107.5,45.01 2 6759 +proj=tmerc +lat_0=40.5 +lon_0=-108.75 +k=0.9999375 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3733 projected NAD83(NSRS2007) / Wyoming West (ftUS) -111.06,40.99,-109.04,44.67 2 6759 +proj=tmerc +lat_0=40.5 +lon_0=-110.083333333333 +k=0.9999375 +x_0=800000.00001016 +y_0=99999.99998984 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3734 projected NAD83 / Ohio North (ftUS) -84.81,40.1,-80.51,42.33 4 6269 +proj=lcc +lat_0=39.6666666666667 +lon_0=-82.5 +lat_1=41.7 +lat_2=40.4333333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3735 projected NAD83 / Ohio South (ftUS) -84.83,38.4,-80.7,40.36 4 6269 +proj=lcc +lat_0=38 +lon_0=-82.5 +lat_1=40.0333333333333 +lat_2=38.7333333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3736 projected NAD83 / Wyoming East (ftUS) -106.33,40.99,-104.05,45.01 4 6269 +proj=tmerc +lat_0=40.5 +lon_0=-105.166666666667 +k=0.9999375 +x_0=200000.00001016 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3737 projected NAD83 / Wyoming East Central (ftUS) -108.63,40.99,-106,45.01 4 6269 +proj=tmerc +lat_0=40.5 +lon_0=-107.333333333333 +k=0.9999375 +x_0=399999.99998984 +y_0=99999.99998984 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3738 projected NAD83 / Wyoming West Central (ftUS) -111.06,40.99,-107.5,45.01 4 6269 +proj=tmerc +lat_0=40.5 +lon_0=-108.75 +k=0.9999375 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3739 projected NAD83 / Wyoming West (ftUS) -111.06,40.99,-109.04,44.67 4 6269 +proj=tmerc +lat_0=40.5 +lon_0=-110.083333333333 +k=0.9999375 +x_0=800000.00001016 +y_0=99999.99998984 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3740 projected NAD83(HARN) / UTM zone 10N -124.79,33.85,-119.99,49.05 1 6152 +proj=utm +zone=10 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +3741 projected NAD83(HARN) / UTM zone 11N -120,32.26,-114,49.01 1 6152 +proj=utm +zone=11 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +3742 projected NAD83(HARN) / UTM zone 12N -114,31.33,-108,49.01 1 6152 +proj=utm +zone=12 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +3743 projected NAD83(HARN) / UTM zone 13N -108,28.98,-102,49.01 1 6152 +proj=utm +zone=13 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +3744 projected NAD83(HARN) / UTM zone 14N -102,25.83,-95.99,49.01 1 6152 +proj=utm +zone=14 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +3745 projected NAD83(HARN) / UTM zone 15N -96,28.42,-89.99,49.38 1 6152 +proj=utm +zone=15 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +3746 projected NAD83(HARN) / UTM zone 16N -90,28.85,-83.99,48.32 1 6152 +proj=utm +zone=16 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +3747 projected NAD83(HARN) / UTM zone 17N -84.01,24.41,-78,46.13 1 6152 +proj=utm +zone=17 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +3748 projected NAD83(HARN) / UTM zone 18N -78,33.84,-72,45.03 1 6152 +proj=utm +zone=18 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +3749 projected NAD83(HARN) / UTM zone 19N -72,40.96,-66.91,47.47 1 6152 +proj=utm +zone=19 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +3750 projected NAD83(HARN) / UTM zone 4N -160.3,19.51,-155.99,22.29 2 6152 +proj=utm +zone=4 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3751 projected NAD83(HARN) / UTM zone 5N -156,18.87,-154.74,20.86 2 6152 +proj=utm +zone=5 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3753 projected NAD83(HARN) / Ohio North (ftUS) -84.81,40.1,-80.51,42.33 1 6152 +proj=lcc +lat_0=39.6666666666667 +lon_0=-82.5 +lat_1=41.7 +lat_2=40.4333333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +3754 projected NAD83(HARN) / Ohio South (ftUS) -84.83,38.4,-80.7,40.36 1 6152 +proj=lcc +lat_0=38 +lon_0=-82.5 +lat_1=40.0333333333333 +lat_2=38.7333333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +3755 projected NAD83(HARN) / Wyoming East (ftUS) -106.33,40.99,-104.05,45.01 1 6152 +proj=tmerc +lat_0=40.5 +lon_0=-105.166666666667 +k=0.9999375 +x_0=200000.00001016 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +3756 projected NAD83(HARN) / Wyoming East Central (ftUS) -108.63,40.99,-106,45.01 1 6152 +proj=tmerc +lat_0=40.5 +lon_0=-107.333333333333 +k=0.9999375 +x_0=399999.99998984 +y_0=99999.99998984 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +3757 projected NAD83(HARN) / Wyoming West Central (ftUS) -111.06,40.99,-107.5,45.01 1 6152 +proj=tmerc +lat_0=40.5 +lon_0=-108.75 +k=0.9999375 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +3758 projected NAD83(HARN) / Wyoming West (ftUS) -111.06,40.99,-109.04,44.67 1 6152 +proj=tmerc +lat_0=40.5 +lon_0=-110.083333333333 +k=0.9999375 +x_0=800000.00001016 +y_0=99999.99998984 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +3759 projected NAD83 / Hawaii zone 3 (ftUS) -158.33,21.2,-157.61,21.75 4 6269 +proj=tmerc +lat_0=21.1666666666667 +lon_0=-158 +k=0.99999 +x_0=500000.00001016 +y_0=0 +ellps=GRS80 +towgs84=1,1,-1 +to_meter=0.304800609601219 +no_defs +3760 projected NAD83(HARN) / Hawaii zone 3 (ftUS) -158.33,21.2,-157.61,21.75 2 6152 +proj=tmerc +lat_0=21.1666666666667 +lon_0=-158 +k=0.99999 +x_0=500000.00001016 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +3761 projected NAD83(CSRS) / UTM zone 22N -54,39.5,-47.99,57.65 1 6140 +proj=utm +zone=22 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +3762 projected WGS 84 / South Georgia Lambert -38.08,-54.95,-35.74,-53.93 6326 +proj=lcc +lat_0=-55 +lon_0=-37 +lat_1=-54 +lat_2=-54.75 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3763 projected ETRS89 / Portugal TM06 -9.56,36.95,-6.19,42.16 1 6258 +proj=tmerc +lat_0=39.6682583333333 +lon_0=-8.13310833333333 +k=1 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3764 projected NZGD2000 / Chatham Island Circuit 2000 -177.25,-44.64,-175.54,-43.3 1 6167 +proj=tmerc +lat_0=-44 +lon_0=-176.5 +k=1 +x_0=400000 +y_0=800000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3765 projected HTRS96 / Croatia TM 13.43,42.34,19.43,46.54 1 6761 +proj=tmerc +lat_0=0 +lon_0=16.5 +k=0.9999 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3766 projected HTRS96 / Croatia LCC 13,41.62,19.43,46.54 1 6761 +proj=lcc +lat_0=0 +lon_0=16.5 +lat_1=45.9166666666667 +lat_2=43.0833333333333 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3767 projected HTRS96 / UTM zone 33N 13,41.63,18,46.54 1 6761 +proj=utm +zone=33 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3768 projected HTRS96 / UTM zone 34N 18,41.62,19.43,45.92 1 6761 +proj=utm +zone=34 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3769 projected Bermuda 1957 / UTM zone 20N -64.89,32.21,-64.61,32.43 1 6216 +proj=utm +zone=20 +ellps=clrk66 +towgs84=-292.295,248.758,429.447,4.9971,2.99,6.6906,1.0289 +units=m +no_defs +3770 projected BDA2000 / Bermuda 2000 National Grid -68.83,28.91,-60.7,35.73 1 6762 +proj=tmerc +lat_0=32 +lon_0=-64.75 +k=1 +x_0=550000 +y_0=100000 +ellps=WGS84 +towgs84=0,0,0 +units=m +no_defs +3771 projected NAD27 / Alberta 3TM ref merid 111 W -112.5,48.99,-109.98,60 13 6267 +proj=tmerc +lat_0=0 +lon_0=-111 +k=0.9999 +x_0=0 +y_0=0 +ellps=clrk66 +towgs84=-7,162,188 +units=m +no_defs +3772 projected NAD27 / Alberta 3TM ref merid 114 W -115.5,48.99,-112.5,60 13 6267 +proj=tmerc +lat_0=0 +lon_0=-114 +k=0.9999 +x_0=0 +y_0=0 +ellps=clrk66 +towgs84=-7,162,188 +units=m +no_defs +3773 projected NAD27 / Alberta 3TM ref merid 117 W -118.5,50.77,-115.5,60 8 6267 +proj=tmerc +lat_0=0 +lon_0=-117 +k=0.9999 +x_0=0 +y_0=0 +ellps=clrk66 +towgs84=4,159,188 +units=m +no_defs +3775 projected NAD83 / Alberta 3TM ref merid 111 W -112.5,48.99,-109.98,60 4 6269 +proj=tmerc +lat_0=0 +lon_0=-111 +k=0.9999 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3776 projected NAD83 / Alberta 3TM ref merid 114 W -115.5,48.99,-112.5,60 4 6269 +proj=tmerc +lat_0=0 +lon_0=-114 +k=0.9999 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3777 projected NAD83 / Alberta 3TM ref merid 117 W -118.5,50.77,-115.5,60 4 6269 +proj=tmerc +lat_0=0 +lon_0=-117 +k=0.9999 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3779 projected NAD83(CSRS) / Alberta 3TM ref merid 111 W -112.5,48.99,-109.98,60 1 6140 +proj=tmerc +lat_0=0 +lon_0=-111 +k=0.9999 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +3780 projected NAD83(CSRS) / Alberta 3TM ref merid 114 W -115.5,48.99,-112.5,60 1 6140 +proj=tmerc +lat_0=0 +lon_0=-114 +k=0.9999 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +3781 projected NAD83(CSRS) / Alberta 3TM ref merid 117 W -118.5,50.77,-115.5,60 1 6140 +proj=tmerc +lat_0=0 +lon_0=-117 +k=0.9999 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +3783 projected Pitcairn 2006 / Pitcairn TM 2006 -130.16,-25.14,-130.01,-25 1 6763 +proj=tmerc +lat_0=-25.0685526111111 +lon_0=-130.112967111111 +k=1 +x_0=14200 +y_0=15500 +ellps=WGS84 +towgs84=0,0,0 +units=m +no_defs +3784 projected Pitcairn 1967 / UTM zone 9S -130.16,-25.14,-130.01,-25 44 6729 +proj=utm +zone=9 +south +ellps=intl +towgs84=185,165,42 +units=m +no_defs +3788 projected NZGD2000 / Auckland Islands TM 2000 165.55,-51.13,166.93,-47.8 1 6167 +proj=tmerc +lat_0=0 +lon_0=166 +k=1 +x_0=3500000 +y_0=10000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3789 projected NZGD2000 / Campbell Island TM 2000 168.65,-52.83,169.6,-52.26 1 6167 +proj=tmerc +lat_0=0 +lon_0=169 +k=1 +x_0=3500000 +y_0=10000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3790 projected NZGD2000 / Antipodes Islands TM 2000 178.4,-49.92,179.37,-47.54 1 6167 +proj=tmerc +lat_0=0 +lon_0=179 +k=1 +x_0=3500000 +y_0=10000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3791 projected NZGD2000 / Raoul Island TM 2000 -179.07,-31.56,-177.62,-29.03 1 6167 +proj=tmerc +lat_0=0 +lon_0=-178 +k=1 +x_0=3500000 +y_0=10000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3793 projected NZGD2000 / Chatham Islands TM 2000 -177.25,-44.64,-175.54,-43.3 1 6167 +proj=tmerc +lat_0=0 +lon_0=-176.5 +k=1 +x_0=3500000 +y_0=10000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3794 projected Slovenia 1996 / Slovene National Grid 13.38,45.42,16.61,46.88 1 6765 +proj=tmerc +lat_0=0 +lon_0=15 +k=0.9999 +x_0=500000 +y_0=-5000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3795 projected NAD27 / Cuba Norte -85.01,21.38,-76.91,23.25 1 6267 +proj=lcc +lat_0=22.35 +lon_0=-81 +lat_1=23 +lat_2=21.7 +x_0=500000 +y_0=280296.016 +ellps=clrk66 +towgs84=2.478,149.752,197.726,0.526,0.498,-0.501,0.685 +units=m +no_defs +3796 projected NAD27 / Cuba Sur -78.69,19.77,-74.07,21.5 1 6267 +proj=lcc +lat_0=20.7166666666667 +lon_0=-76.8333333333333 +lat_1=21.3 +lat_2=20.1333333333333 +x_0=500000 +y_0=229126.939 +ellps=clrk66 +towgs84=2.478,149.752,197.726,0.526,0.498,-0.501,0.685 +units=m +no_defs +3797 projected NAD27 / MTQ Lambert -79.85,44.99,-57.1,62.62 9 6267 +proj=lcc +lat_0=44 +lon_0=-70 +lat_1=50 +lat_2=46 +x_0=800000 +y_0=0 +ellps=clrk66 +towgs84=-22,160,190 +units=m +no_defs +3798 projected NAD83 / MTQ Lambert -79.85,44.99,-57.1,62.62 4 6269 +proj=lcc +lat_0=44 +lon_0=-70 +lat_1=50 +lat_2=46 +x_0=800000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3799 projected NAD83(CSRS) / MTQ Lambert -79.85,44.99,-57.1,62.62 1 6140 +proj=lcc +lat_0=44 +lon_0=-70 +lat_1=50 +lat_2=46 +x_0=800000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +3800 projected NAD27 / Alberta 3TM ref merid 120 W -120,52.88,-118.5,60 8 6267 +proj=tmerc +lat_0=0 +lon_0=-120 +k=0.9999 +x_0=0 +y_0=0 +ellps=clrk66 +towgs84=4,159,188 +units=m +no_defs +3801 projected NAD83 / Alberta 3TM ref merid 120 W -120,52.88,-118.5,60 4 6269 +proj=tmerc +lat_0=0 +lon_0=-120 +k=0.9999 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3802 projected NAD83(CSRS) / Alberta 3TM ref merid 120 W -120,52.88,-118.5,60 1 6140 +proj=tmerc +lat_0=0 +lon_0=-120 +k=0.9999 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +3812 projected ETRS89 / Belgian Lambert 2008 2.5,49.5,6.4,51.51 1 6258 +proj=lcc +lat_0=50.797815 +lon_0=4.35921583333333 +lat_1=49.8333333333333 +lat_2=51.1666666666667 +x_0=649328 +y_0=665262 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3814 projected NAD83 / Mississippi TM -91.65,30.01,-88.09,35.01 4 6269 +proj=tmerc +lat_0=32.5 +lon_0=-89.75 +k=0.9998335 +x_0=500000 +y_0=1300000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3815 projected NAD83(HARN) / Mississippi TM -91.65,30.01,-88.09,35.01 1 6152 +proj=tmerc +lat_0=32.5 +lon_0=-89.75 +k=0.9998335 +x_0=500000 +y_0=1300000 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +3816 projected NAD83(NSRS2007) / Mississippi TM -91.65,30.01,-88.09,35.01 2 6759 +proj=tmerc +lat_0=32.5 +lon_0=-89.75 +k=0.9998335 +x_0=500000 +y_0=1300000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3819 geographic HD1909 16.11,45.74,22.9,48.58 3 1024 +proj=longlat +ellps=bessel +towgs84=595.48,121.69,515.35,4.115,-2.9383,0.853,-3.408 +no_defs +3821 geographic TWD67 119.25,21.87,122.06,25.34 1025 +proj=longlat +ellps=aust_SA +no_defs +3824 geographic TWD97 114.32,17.36,123.61,26.96 1 1026 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +3825 projected TWD97 / TM2 zone 119 118,18.63,120,24.65 1 1026 +proj=tmerc +lat_0=0 +lon_0=119 +k=0.9999 +x_0=250000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3826 projected TWD97 / TM2 zone 121 119.99,20.41,122.06,26.72 1 1026 +proj=tmerc +lat_0=0 +lon_0=121 +k=0.9999 +x_0=250000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3827 projected TWD67 / TM2 zone 119 119.25,23.13,119.78,23.82 1025 +proj=tmerc +lat_0=0 +lon_0=119 +k=0.9999 +x_0=250000 +y_0=0 +ellps=aust_SA +units=m +no_defs +3828 projected TWD67 / TM2 zone 121 119.99,21.87,122.06,25.34 1025 +proj=tmerc +lat_0=0 +lon_0=121 +k=0.9999 +x_0=250000 +y_0=0 +ellps=aust_SA +units=m +no_defs +3829 projected Hu Tzu Shan 1950 / UTM zone 51N 119.25,21.87,122.06,25.34 26 6236 +proj=utm +zone=51 +ellps=intl +towgs84=-637,-549,-203 +units=m +no_defs +3832 projected WGS 84 / PDC Mercator 98.69,-60,-68,66.67 6326 +proj=merc +lon_0=150 +k=1 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3833 projected Pulkovo 1942(58) / Gauss-Kruger zone 2 9.92,50.2,12,54.23 6179 +proj=tmerc +lat_0=0 +lon_0=9 +k=1 +x_0=2500000 +y_0=0 +ellps=krass +units=m +no_defs +3834 projected Pulkovo 1942(83) / Gauss-Kruger zone 2 9.92,50.2,12,54.23 2 6178 +proj=tmerc +lat_0=0 +lon_0=9 +k=1 +x_0=2500000 +y_0=0 +ellps=krass +towgs84=24,-123,-94,0.02,-0.25,-0.13,1.1 +units=m +no_defs +3835 projected Pulkovo 1942(83) / Gauss-Kruger zone 3 12,45.78,18.01,54.74 5 6178 +proj=tmerc +lat_0=0 +lon_0=15 +k=1 +x_0=3500000 +y_0=0 +ellps=krass +towgs84=26,-121,-78 +units=m +no_defs +3836 projected Pulkovo 1942(83) / Gauss-Kruger zone 4 18,45.74,22.9,50.06 4 6178 +proj=tmerc +lat_0=0 +lon_0=21 +k=1 +x_0=4500000 +y_0=0 +ellps=krass +towgs84=28,-121,-77 +units=m +no_defs +3837 projected Pulkovo 1942(58) / 3-degree Gauss-Kruger zone 3 9.92,50.35,10.5,51.56 6179 +proj=tmerc +lat_0=0 +lon_0=9 +k=1 +x_0=3500000 +y_0=0 +ellps=krass +units=m +no_defs +3838 projected Pulkovo 1942(58) / 3-degree Gauss-Kruger zone 4 10.5,48.97,13.5,54.74 6179 +proj=tmerc +lat_0=0 +lon_0=12 +k=1 +x_0=4500000 +y_0=0 +ellps=krass +units=m +no_defs +3839 projected Pulkovo 1942(58) / 3-degree Gauss-Kruger zone 9 25.5,41.28,28.5,48.27 3 6179 +proj=tmerc +lat_0=0 +lon_0=27 +k=1 +x_0=9500000 +y_0=0 +ellps=krass +towgs84=2.329,-147.042,-92.08,-0.309,0.325,0.497,5.69 +units=m +no_defs +3840 projected Pulkovo 1942(58) / 3-degree Gauss-Kruger zone 10 28.5,43.34,29.74,45.44 3 6179 +proj=tmerc +lat_0=0 +lon_0=30 +k=1 +x_0=10500000 +y_0=0 +ellps=krass +towgs84=2.329,-147.042,-92.08,-0.309,0.325,0.497,5.69 +units=m +no_defs +3841 projected Pulkovo 1942(83) / 3-degree Gauss-Kruger zone 6 16.5,45.74,19.5,50.45 4 6178 +proj=tmerc +lat_0=0 +lon_0=18 +k=1 +x_0=6500000 +y_0=0 +ellps=krass +towgs84=28,-121,-77 +units=m +no_defs +3844 projected Pulkovo 1942(58) / Stereo70 20.26,43.44,31.41,48.27 3 6179 +proj=sterea +lat_0=46 +lon_0=25 +k=0.99975 +x_0=500000 +y_0=500000 +ellps=krass +towgs84=2.329,-147.042,-92.08,-0.309,0.325,0.497,5.69 +units=m +no_defs +3845 projected SWEREF99 / RT90 7.5 gon V emulation 10.93,57.29,12.91,59.73 1 6619 +proj=tmerc +lat_0=0 +lon_0=11.30625 +k=1.000006 +x_0=1500025.141 +y_0=-667.282 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3846 projected SWEREF99 / RT90 5 gon V emulation 11.81,55.28,15.44,64.39 1 6619 +proj=tmerc +lat_0=0 +lon_0=13.5562666666667 +k=1.0000058 +x_0=1500044.695 +y_0=-667.13 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3847 projected SWEREF99 / RT90 2.5 gon V emulation 13.66,55.95,17.73,67.18 1 6619 +proj=tmerc +lat_0=0 +lon_0=15.8062845294444 +k=1.00000561024 +x_0=1500064.274 +y_0=-667.711 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3848 projected SWEREF99 / RT90 0 gon emulation 16.08,56.86,20.22,68.54 1 6619 +proj=tmerc +lat_0=0 +lon_0=18.0563 +k=1.0000054 +x_0=1500083.521 +y_0=-668.844 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3849 projected SWEREF99 / RT90 2.5 gon O emulation 18.4,63.37,22.2,69.07 1 6619 +proj=tmerc +lat_0=0 +lon_0=20.3063166666667 +k=1.0000052 +x_0=1500102.765 +y_0=-670.706 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3850 projected SWEREF99 / RT90 5 gon O emulation 21.34,65.24,24.17,68.58 1 6619 +proj=tmerc +lat_0=0 +lon_0=22.5563333333333 +k=1.0000049 +x_0=1500121.846 +y_0=-672.557 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3851 projected NZGD2000 / NZCS2000 160.6,-55.95,-171.2,-25.88 1 6167 +proj=lcc +lat_0=-41 +lon_0=173 +lat_1=-37.5 +lat_2=-44.5 +x_0=3000000 +y_0=7000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3852 projected RSRGD2000 / DGLC2000 145,-81,169,-76 1 6764 +proj=lcc +lat_0=-90 +lon_0=157 +lat_1=-76.6666666666667 +lat_2=-79.3333333333333 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3854 projected County ST74 17.25,58.69,19.61,60.27 1 6619 +proj=tmerc +lat_0=0 +lon_0=18.05787 +k=0.99999506 +x_0=100182.7406 +y_0=-6500620.1207 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3857 projected WGS 84 / Pseudo-Mercator -180,-85.06,180,85.06 6326 +proj=webmerc +datum=WGS84 +units=m +no_defs +3873 projected EUREF-FIN / GK19FIN 19.24,60.08,19.5,60.34 1 1391 +proj=tmerc +lat_0=0 +lon_0=19 +k=1 +x_0=19500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3874 projected EUREF-FIN / GK20FIN 19.5,59.92,20.5,60.48 1 1391 +proj=tmerc +lat_0=0 +lon_0=20 +k=1 +x_0=20500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3875 projected EUREF-FIN / GK21FIN 20.5,59.84,21.5,69.33 1 1391 +proj=tmerc +lat_0=0 +lon_0=21 +k=1 +x_0=21500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3876 projected EUREF-FIN / GK22FIN 21.5,59.76,22.5,69.31 1 1391 +proj=tmerc +lat_0=0 +lon_0=22 +k=1 +x_0=22500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3877 projected EUREF-FIN / GK23FIN 22.5,59.75,23.5,68.74 1 1391 +proj=tmerc +lat_0=0 +lon_0=23 +k=1 +x_0=23500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3878 projected EUREF-FIN / GK24FIN 23.5,59.86,24.5,68.84 1 1391 +proj=tmerc +lat_0=0 +lon_0=24 +k=1 +x_0=24500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3879 projected EUREF-FIN / GK25FIN 24.5,59.94,25.5,68.9 1 1391 +proj=tmerc +lat_0=0 +lon_0=25 +k=1 +x_0=25500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3880 projected EUREF-FIN / GK26FIN 25.5,60.18,26.5,69.94 1 1391 +proj=tmerc +lat_0=0 +lon_0=26 +k=1 +x_0=26500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3881 projected EUREF-FIN / GK27FIN 26.5,60.36,27.5,70.05 1 1391 +proj=tmerc +lat_0=0 +lon_0=27 +k=1 +x_0=27500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3882 projected EUREF-FIN / GK28FIN 27.5,60.42,28.5,70.09 1 1391 +proj=tmerc +lat_0=0 +lon_0=28 +k=1 +x_0=28500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3883 projected EUREF-FIN / GK29FIN 28.5,60.94,29.5,69.81 1 1391 +proj=tmerc +lat_0=0 +lon_0=29 +k=1 +x_0=29500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3884 projected EUREF-FIN / GK30FIN 29.5,61.43,30.5,67.98 1 1391 +proj=tmerc +lat_0=0 +lon_0=30 +k=1 +x_0=30500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3885 projected EUREF-FIN / GK31FIN 30.5,62.08,31.59,64.27 1 1391 +proj=tmerc +lat_0=0 +lon_0=31 +k=1 +x_0=31500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3889 geographic IGRS 38.79,29.06,48.75,37.39 1 1029 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +3890 projected IGRS / UTM zone 37N 38.79,31.14,42,36.75 1 1029 +proj=utm +zone=37 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3891 projected IGRS / UTM zone 38N 42,29.06,48,37.39 1 1029 +proj=utm +zone=38 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3892 projected IGRS / UTM zone 39N 48,29.6,48.75,31 1 1029 +proj=utm +zone=39 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3893 projected ED50 / Iraq National Grid 38.79,29.06,48.61,37.39 2 6230 +proj=tmerc +lat_0=29.0262683333333 +lon_0=46.5 +k=0.9994 +x_0=800000 +y_0=0 +ellps=intl +towgs84=-84.1,-101.8,-129.7,0,0,0.468,1.05 +units=m +no_defs +3906 geographic MGI 1901 13.38,40.85,23.04,46.88 5 1031 +proj=longlat +ellps=bessel +towgs84=682,-203,480 +no_defs +3912 projected MGI 1901 / Slovene National Grid 13.38,45.42,16.61,46.88 1 1031 +proj=tmerc +lat_0=0 +lon_0=15 +k=0.9999 +x_0=500000 +y_0=-5000000 +ellps=bessel +towgs84=476.08,125.947,417.81,4.610862,2.388137,-11.942335,9.896638 +units=m +no_defs +3920 projected Puerto Rico / UTM zone 20N -64.88,18.28,-64.25,18.78 6 6139 +proj=utm +zone=20 +ellps=clrk66 +towgs84=11,72,-101 +units=m +no_defs +3942 projected RGF93 v1 / CC42 -1.06,41.31,9.63,43.07 1 6171 +proj=lcc +lat_0=42 +lon_0=3 +lat_1=41.25 +lat_2=42.75 +x_0=1700000 +y_0=1200000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3943 projected RGF93 v1 / CC43 -1.79,42.33,7.65,44.01 1 6171 +proj=lcc +lat_0=43 +lon_0=3 +lat_1=42.25 +lat_2=43.75 +x_0=1700000 +y_0=2200000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3944 projected RGF93 v1 / CC44 -1.79,42.92,7.71,45 1 6171 +proj=lcc +lat_0=44 +lon_0=3 +lat_1=43.25 +lat_2=44.75 +x_0=1700000 +y_0=3200000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3945 projected RGF93 v1 / CC45 -1.46,44,7.71,46 1 6171 +proj=lcc +lat_0=45 +lon_0=3 +lat_1=44.25 +lat_2=45.75 +x_0=1700000 +y_0=4200000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3946 projected RGF93 v1 / CC46 -2.21,45,7.16,47 1 6171 +proj=lcc +lat_0=46 +lon_0=3 +lat_1=45.25 +lat_2=46.75 +x_0=1700000 +y_0=5200000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3947 projected RGF93 v1 / CC47 -4.77,46,7.63,48 1 6171 +proj=lcc +lat_0=47 +lon_0=3 +lat_1=46.25 +lat_2=47.75 +x_0=1700000 +y_0=6200000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3948 projected RGF93 v1 / CC48 -4.87,47,8.23,49 1 6171 +proj=lcc +lat_0=48 +lon_0=3 +lat_1=47.25 +lat_2=48.75 +x_0=1700000 +y_0=7200000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3949 projected RGF93 v1 / CC49 -4.87,48,8.23,50 1 6171 +proj=lcc +lat_0=49 +lon_0=3 +lat_1=48.25 +lat_2=49.75 +x_0=1700000 +y_0=8200000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3950 projected RGF93 v1 / CC50 -2.03,49,8.08,51.14 1 6171 +proj=lcc +lat_0=50 +lon_0=3 +lat_1=49.25 +lat_2=50.75 +x_0=1700000 +y_0=9200000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3968 projected NAD83 / Virginia Lambert -83.68,36.54,-75.31,39.46 4 6269 +proj=lcc +lat_0=36 +lon_0=-79.5 +lat_1=37 +lat_2=39.5 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3969 projected NAD83(HARN) / Virginia Lambert -83.68,36.54,-75.31,39.46 1 6152 +proj=lcc +lat_0=36 +lon_0=-79.5 +lat_1=37 +lat_2=39.5 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +3970 projected NAD83(NSRS2007) / Virginia Lambert -83.68,36.54,-75.31,39.46 2 6759 +proj=lcc +lat_0=36 +lon_0=-79.5 +lat_1=37 +lat_2=39.5 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3976 projected WGS 84 / NSIDC Sea Ice Polar Stereographic South -180,-90,180,-60 6326 +proj=stere +lat_0=-90 +lat_ts=-70 +lon_0=0 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3978 projected NAD83 / Canada Atlas Lambert -141.01,38.21,-40.73,86.46 4 6269 +proj=lcc +lat_0=49 +lon_0=-95 +lat_1=49 +lat_2=77 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +3979 projected NAD83(CSRS) / Canada Atlas Lambert -141.01,38.21,-40.73,86.46 1 6140 +proj=lcc +lat_0=49 +lon_0=-95 +lat_1=49 +lat_2=77 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +3986 projected Katanga 1955 / Katanga Gauss zone A 28.5,-13.46,30.78,-4.99 6695 +proj=tmerc +lat_0=-9 +lon_0=30 +k=1 +x_0=200000 +y_0=500000 +ellps=clrk66 +units=m +no_defs +3987 projected Katanga 1955 / Katanga Gauss zone B 26.5,-13.44,29.5,-4.99 1.5 6695 +proj=tmerc +lat_0=-9 +lon_0=28 +k=1 +x_0=200000 +y_0=500000 +ellps=clrk66 +towgs84=-103.746,-9.614,-255.95 +units=m +no_defs +3988 projected Katanga 1955 / Katanga Gauss zone C 24.5,-12.08,27.5,-4.99 1.5 6695 +proj=tmerc +lat_0=-9 +lon_0=26 +k=1 +x_0=200000 +y_0=500000 +ellps=clrk66 +towgs84=-103.746,-9.614,-255.95 +units=m +no_defs +3989 projected Katanga 1955 / Katanga Gauss zone D 21.74,-11.72,25.51,-6.32 6695 +proj=tmerc +lat_0=-9 +lon_0=24 +k=1 +x_0=200000 +y_0=500000 +ellps=clrk66 +units=m +no_defs +3991 projected Puerto Rico State Plane CS of 1927 -67.97,17.87,-65.19,18.57 6 6139 +proj=lcc +lat_0=17.8333333333333 +lon_0=-66.4333333333333 +lat_1=18.4333333333333 +lat_2=18.0333333333333 +x_0=152400.30480061 +y_0=0 +ellps=clrk66 +towgs84=11,72,-101 +to_meter=0.304800609601219 +no_defs +3992 projected Puerto Rico / St. Croix -65.09,17.62,-64.51,18.44 6 6139 +proj=lcc +lat_0=17.8333333333333 +lon_0=-66.4333333333333 +lat_1=18.4333333333333 +lat_2=18.0333333333333 +x_0=152400.30480061 +y_0=30480.0609601219 +ellps=clrk66 +towgs84=11,72,-101 +to_meter=0.304800609601219 +no_defs +3993 projected Guam 1963 / Guam SPCS 144.58,13.18,145.01,13.7 6 6675 +proj=aeqd +guam +lat_0=13.4724663527778 +lon_0=144.748750705556 +x_0=50000 +y_0=50000 +ellps=clrk66 +towgs84=-100,-248,259 +units=m +no_defs +3994 projected WGS 84 / Mercator 41 155,-60,-169.99,-25 6326 +proj=merc +lat_ts=-41 +lon_0=100 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3995 projected WGS 84 / Arctic Polar Stereographic -180,60,180,90 6326 +proj=stere +lat_0=90 +lat_ts=71 +lon_0=0 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3996 projected WGS 84 / IBCAO Polar Stereographic -180,60,180,90 6326 +proj=stere +lat_0=90 +lat_ts=75 +lon_0=0 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +3997 projected WGS 84 / Dubai Local TM 54.84,24.85,55.55,25.34 6326 +proj=tmerc +lat_0=0 +lon_0=55.3333333333333 +k=1 +x_0=500000 +y_0=0 +datum=WGS84 +units=m +no_defs +4023 geographic MOLDREF99 26.63,45.44,30.13,48.47 1 1032 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +4026 projected MOLDREF99 / Moldova TM 26.63,45.44,30.13,48.47 1 1032 +proj=tmerc +lat_0=0 +lon_0=28.4 +k=0.99994 +x_0=200000 +y_0=-5000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +4037 projected WGS 84 / TMzn35N 26.63,45.44,30,48.47 6326 +proj=utm +zone=35 +datum=WGS84 +units=m +no_defs +4038 projected WGS 84 / TMzn36N 30,46.37,30.13,46.47 6326 +proj=utm +zone=36 +datum=WGS84 +units=m +no_defs +4046 geographic RGRDC 2005 11.79,-13.46,29.81,-3.41 1 1033 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +4048 projected RGRDC 2005 / Congo TM zone 12 11.79,-6.04,13,-4.67 1 1033 +proj=tmerc +lat_0=0 +lon_0=12 +k=0.9999 +x_0=500000 +y_0=10000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +4049 projected RGRDC 2005 / Congo TM zone 14 13,-5.91,15.01,-4.28 1 1033 +proj=tmerc +lat_0=0 +lon_0=14 +k=0.9999 +x_0=500000 +y_0=10000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +4050 projected RGRDC 2005 / Congo TM zone 16 15,-7.31,17,-3.41 1 1033 +proj=tmerc +lat_0=0 +lon_0=16 +k=0.9999 +x_0=500000 +y_0=10000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +4051 projected RGRDC 2005 / Congo TM zone 18 17,-8.11,19,-3.43 1 1033 +proj=tmerc +lat_0=0 +lon_0=18 +k=0.9999 +x_0=500000 +y_0=10000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +4056 projected RGRDC 2005 / Congo TM zone 20 19,-8,21,-3.77 1 1033 +proj=tmerc +lat_0=0 +lon_0=20 +k=0.9999 +x_0=500000 +y_0=10000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +4057 projected RGRDC 2005 / Congo TM zone 22 21,-11.24,23.01,-4.18 1 1033 +proj=tmerc +lat_0=0 +lon_0=22 +k=0.9999 +x_0=500000 +y_0=10000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +4058 projected RGRDC 2005 / Congo TM zone 24 23,-11.47,25,-4.58 1 1033 +proj=tmerc +lat_0=0 +lon_0=24 +k=0.9999 +x_0=500000 +y_0=10000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +4059 projected RGRDC 2005 / Congo TM zone 26 25,-11.99,27,-4.99 1 1033 +proj=tmerc +lat_0=0 +lon_0=26 +k=0.9999 +x_0=500000 +y_0=10000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +4060 projected RGRDC 2005 / Congo TM zone 28 27,-13.39,29,-6.43 1 1033 +proj=tmerc +lat_0=0 +lon_0=28 +k=0.9999 +x_0=500000 +y_0=10000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +4061 projected RGRDC 2005 / UTM zone 33S 12,-8.11,18.01,-3.41 1 1033 +proj=utm +zone=33 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +4062 projected RGRDC 2005 / UTM zone 34S 18,-11.24,24,-3.57 1 1033 +proj=utm +zone=34 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +4063 projected RGRDC 2005 / UTM zone 35S 24,-13.46,29.81,-4.79 1 1033 +proj=utm +zone=35 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +4071 projected Chua / UTM zone 23S -48.1,-15.94,-47.1,-15.37 5 6224 +proj=utm +zone=23 +south +ellps=intl +towgs84=-144.35,242.88,-33.2 +units=m +no_defs +4075 geographic SREF98 18.81,42.23,23.01,46.19 1 1034 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +4081 geographic REGCAN95 -21.93,24.6,-11.75,32.76 1 1035 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +4082 projected REGCAN95 / UTM zone 27N -21.93,24.6,-18,31.19 1 1035 +proj=utm +zone=27 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +4083 projected REGCAN95 / UTM zone 28N -18,25.25,-11.75,32.76 1 1035 +proj=utm +zone=28 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +4087 projected WGS 84 / World Equidistant Cylindrical -180,-90,180,90 6326 +proj=eqc +lat_ts=0 +lat_0=0 +lon_0=0 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +4093 projected ETRS89 / DKTM1 8,54.8,10,57.64 1 6258 +proj=tmerc +lat_0=0 +lon_0=9 +k=0.99998 +x_0=200000 +y_0=-5000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +4094 projected ETRS89 / DKTM2 9,54.67,11.29,57.8 1 6258 +proj=tmerc +lat_0=0 +lon_0=10 +k=0.99998 +x_0=400000 +y_0=-5000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +4095 projected ETRS89 / DKTM3 10.79,54.51,12.87,56.79 1 6258 +proj=tmerc +lat_0=0 +lon_0=11.75 +k=0.99998 +x_0=600000 +y_0=-5000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +4096 projected ETRS89 / DKTM4 14.59,54.94,15.25,55.38 1 6258 +proj=tmerc +lat_0=0 +lon_0=15 +k=1 +x_0=800000 +y_0=-5000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +4120 geographic Greek 19.57,34.88,28.3,41.75 6120 +proj=longlat +ellps=bessel +no_defs +4121 geographic GGRS87 19.57,34.88,28.3,41.75 1 6121 +proj=longlat +ellps=GRS80 +towgs84=-199.87,74.79,246.62 +no_defs +4122 geographic ATS77 -69.05,43.41,-59.73,48.07 6122 +proj=longlat +a=6378135 +rf=298.257 +no_defs +4123 geographic KKJ 19.24,59.75,31.59,70.09 1 6123 +proj=longlat +ellps=intl +towgs84=-96.062,-82.428,-121.753,4.801,0.345,-1.376,1.496 +no_defs +4124 geographic RT90 10.03,54.96,24.17,69.07 1 6124 +proj=longlat +ellps=bessel +towgs84=414.1,41.3,603.1,-0.855,2.141,-7.023,0 +no_defs +4127 geographic Tete 30.21,-26.87,40.9,-10.42 17 6127 +proj=longlat +ellps=clrk66 +towgs84=-80,-100,-228 +no_defs +4128 geographic Madzansua 30.21,-17.76,35.37,-14.01 6128 +proj=longlat +ellps=clrk66 +no_defs +4129 geographic Observatario 31.29,-26.87,35.65,-19.84 17 6129 +proj=longlat +ellps=clrk66 +towgs84=-132,-110,-335 +no_defs +4130 geographic Moznet 30.21,-27.71,43.03,-10.09 1 6130 +proj=longlat +ellps=WGS84 +towgs84=0,0,0 +no_defs +4131 geographic Indian 1960 102.14,7.99,110,23.4 44 6131 +proj=longlat +ellps=evrst30 +towgs84=198,881,317 +no_defs +4132 geographic FD58 47.13,26.21,53.61,33.22 0.5 6132 +proj=longlat +a=6378249.145 +rf=293.465 +towgs84=-241.54,-163.64,396.06 +no_defs +4133 geographic EST92 21.74,57.52,28.2,59.75 0.5 6133 +proj=longlat +ellps=GRS80 +towgs84=0.055,-0.541,-0.185,0.0183,-0.0003,-0.007,-0.014 +no_defs +4134 geographic PSD93 51.99,16.59,59.91,26.58 0.5 6134 +proj=longlat +a=6378249.145 +rf=293.465 +towgs84=-180.624,-225.516,173.919,-0.81,-1.898,8.336,16.71006 +no_defs +4135 geographic Old Hawaiian -160.3,18.87,-154.74,22.29 38 6135 +proj=longlat +ellps=clrk66 +towgs84=61,-285,-181 +no_defs +4136 geographic St. Lawrence Island -171.97,62.89,-168.59,63.84 6136 +proj=longlat +ellps=clrk66 +no_defs +4137 geographic St. Paul Island -170.51,57.06,-170.04,57.28 6137 +proj=longlat +ellps=clrk66 +no_defs +4138 geographic St. George Island -169.88,56.49,-169.38,56.67 6138 +proj=longlat +ellps=clrk66 +no_defs +4139 geographic Puerto Rico -67.97,17.62,-64.25,18.78 6 6139 +proj=longlat +ellps=clrk66 +towgs84=11,72,-101 +no_defs +4141 geographic Israel 1993 34.17,29.45,35.69,33.28 0.5 6141 +proj=longlat +ellps=GRS80 +towgs84=23.772,17.49,17.859,-0.3132,-1.85274,1.67299,-5.4262 +no_defs +4142 geographic Locodjo 1965 -8.61,1.02,-2.48,10.74 15 6142 +proj=longlat +a=6378249.145 +rf=293.465 +towgs84=-125,53,467 +no_defs +4143 geographic Abidjan 1987 -8.61,1.02,-2.48,10.74 2 6143 +proj=longlat +a=6378249.145 +rf=293.465 +towgs84=-124.76,53,466.79 +no_defs +4144 geographic Kalianpur 1937 60.86,8.02,101.17,37.07 5 6144 +proj=longlat +ellps=evrst30 +towgs84=214,804,268 +no_defs +4145 geographic Kalianpur 1962 60.86,21.05,77.83,37.07 1 6145 +proj=longlat +a=6377301.243 +rf=300.8017255 +towgs84=275.57,676.78,229.6 +no_defs +4146 geographic Kalianpur 1975 68.13,8.02,97.42,35.51 22 6146 +proj=longlat +a=6377299.151 +rf=300.8017255 +towgs84=295,736,257 +no_defs +4147 geographic Hanoi 1972 102.14,8.33,109.53,23.4 5 6147 +proj=longlat +ellps=krass +towgs84=-17.51,-108.32,-62.39 +no_defs +4148 geographic Hartebeesthoek94 13.33,-50.32,42.85,-22.13 1 6148 +proj=longlat +ellps=WGS84 +towgs84=0,0,0 +no_defs +4149 geographic CH1903 5.95,45.81,10.5,47.81 1.5 6149 +proj=longlat +ellps=bessel +towgs84=674.374,15.056,405.346 +no_defs +4150 geographic CH1903+ 5.95,45.81,10.5,47.81 1 6150 +proj=longlat +ellps=bessel +towgs84=674.374,15.056,405.346 +no_defs +4151 geographic CHTRS95 5.95,45.81,10.5,47.81 1 6151 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +4152 geographic NAD83(HARN) 144.58,-14.59,-64.51,71.4 2 6152 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +4153 geographic Rassadiran 52.5,27.39,52.71,27.61 0.5 6153 +proj=longlat +ellps=intl +towgs84=-133.63,-157.5,-158.62 +no_defs +4154 geographic ED50(ED77) 44.03,23.34,63.34,39.78 1 6154 +proj=longlat +ellps=intl +towgs84=-110.33,-97.73,-119.85,0.3423,1.1634,0.2715,0.063 +no_defs +4155 geographic Dabola 1981 -15.13,7.19,-7.65,12.68 25 6155 +proj=longlat +ellps=clrk80ign +towgs84=-83,37,124 +no_defs +4156 geographic S-JTSK 12.09,47.73,22.56,51.06 6 6156 +proj=longlat +ellps=bessel +towgs84=589,76,480 +no_defs +4157 geographic Mount Dillon -60.9,11.08,-60.44,11.41 6157 +proj=longlat +a=6378293.64520876 +b=6356617.98767984 +no_defs +4158 geographic Naparima 1955 -61.98,9.99,-60.85,10.9 1 6158 +proj=longlat +ellps=intl +towgs84=-0.465,372.095,171.736 +no_defs +4159 geographic ELD79 9.31,19.5,26.21,35.23 5 6159 +proj=longlat +ellps=intl +towgs84=-115.8543,-99.0583,-152.4616 +no_defs +4160 geographic Chos Malal 1914 -72.14,-43.41,-65.86,-31.91 2.5 6160 +proj=longlat +ellps=intl +towgs84=15.75,164.93,126.18 +no_defs +4161 geographic Pampa del Castillo -73.59,-50.34,-65.47,-42.49 2.5 6161 +proj=longlat +ellps=intl +towgs84=-233.43,6.65,173.64 +no_defs +4162 geographic Korean 1985 124.53,33.14,131.01,38.64 6162 +proj=longlat +ellps=bessel +no_defs +4163 geographic Yemen NGN96 41.08,8.95,57.96,19 1 6163 +proj=longlat +ellps=WGS84 +towgs84=0,0,0 +no_defs +4164 geographic South Yemen 43.37,12.54,53.14,19 5 6164 +proj=longlat +ellps=krass +towgs84=-76,-138,67 +no_defs +4165 geographic Bissau -16.77,10.87,-13.64,12.69 25 6165 +proj=longlat +ellps=intl +towgs84=-173,253,27 +no_defs +4166 geographic Korean 1995 124.53,33.14,131.01,38.64 2 6166 +proj=longlat +ellps=WGS84 +towgs84=0,0,0 +no_defs +4167 geographic NZGD2000 160.6,-55.95,-171.2,-25.88 1 6167 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +4168 geographic Accra -3.79,1.4,2.1,11.16 25 6168 +proj=longlat +a=6378300 +rf=296 +towgs84=-199,32,322 +no_defs +4169 geographic American Samoa 1962 -170.88,-14.43,-169.38,-14.11 44 6169 +proj=longlat +ellps=clrk66 +towgs84=-115,118,426 +no_defs +4170 geographic SIRGAS 1995 -113.21,-59.87,-26,16.75 2 6170 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +4171 geographic RGF93 v1 -9.86,41.15,10.38,51.56 1 6171 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +4173 geographic IRENET95 -10.56,51.39,-5.34,55.43 1 6173 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +4174 geographic Sierra Leone 1924 -13.34,8.32,-13.13,8.55 6174 +proj=longlat +a=6378300 +rf=296 +no_defs +4175 geographic Sierra Leone 1968 -13.35,6.88,-10.26,10 26 6175 +proj=longlat +a=6378249.145 +rf=293.465 +towgs84=-88,4,101 +no_defs +4176 geographic Australian Antarctic 45,-90,160,-60 1 6176 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +4178 geographic Pulkovo 1942(83) 9.92,41.24,28.68,54.74 5 6178 +proj=longlat +ellps=krass +towgs84=26,-121,-78 +no_defs +4179 geographic Pulkovo 1942(58) 9.92,39.63,31.41,54.89 3 6179 +proj=longlat +ellps=krass +towgs84=2.329,-147.042,-92.08,-0.309,0.325,0.497,5.69 +no_defs +4180 geographic EST97 20.37,57.52,28.2,60 1 6180 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +4181 geographic LUREF 5.73,49.44,6.53,50.19 1 6181 +proj=longlat +ellps=intl +towgs84=-189.6806,18.3463,-42.7695,-0.33746,-3.09264,2.53861,0.4598 +no_defs +4182 geographic Azores Occidental 1939 -31.34,39.3,-31.02,39.77 5 6182 +proj=longlat +ellps=intl +towgs84=-422.651,-172.995,84.02 +no_defs +4183 geographic Azores Central 1948 -28.9,38.32,-26.97,39.14 6 6183 +proj=longlat +ellps=intl +towgs84=-104,167,-38 +no_defs +4184 geographic Azores Oriental 1940 -25.92,36.87,-24.72,37.96 44 6184 +proj=longlat +ellps=intl +towgs84=-203,141,53 +no_defs +4188 geographic OSNI 1952 -8.18,53.96,-5.34,55.36 1 6188 +proj=longlat +ellps=airy +towgs84=482.5,-130.6,564.6,-1.042,-0.214,-0.631,8.15 +no_defs +4189 geographic REGVEN -73.38,0.64,-58.95,16.75 1 6189 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +4190 geographic POSGAR 98 -73.59,-58.41,-52.63,-21.78 1 6190 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +4191 geographic Albanian 1987 19.22,39.64,21.06,42.67 1 6191 +proj=longlat +ellps=krass +towgs84=-44.183,-0.58,-38.489,2.3867,2.7072,-3.5196,-8.2703 +no_defs +4192 geographic Douala 1948 8.45,2.16,10.4,4.99 10 6192 +proj=longlat +ellps=intl +towgs84=-206.1,-174.7,-87.7 +no_defs +4193 geographic Manoca 1962 8.45,2.16,10.4,4.99 0.5 6193 +proj=longlat +ellps=clrk80ign +towgs84=-70.9,-151.8,-41.4 +no_defs +4194 geographic Qoornoq 1927 -73.29,59.74,-42.52,79 1 6194 +proj=longlat +ellps=intl +towgs84=163.511,127.533,-159.789,0,0,0.814,-0.6 +no_defs +4195 geographic Scoresbysund 1952 -29.69,68.66,-19.89,74.58 1 6195 +proj=longlat +ellps=intl +towgs84=105,326,-102.5,0,0,0.814,-0.6 +no_defs +4196 geographic Ammassalik 1958 -38.86,65.52,-36.81,65.91 1 6196 +proj=longlat +ellps=intl +towgs84=-45,417,-3.5,0,0,0.814,-0.6 +no_defs +4197 geographic Garoua 12.9,8.92,14.19,9.87 6197 +proj=longlat +a=6378249.145 +rf=293.465 +no_defs +4198 geographic Kousseri 14.17,11.7,15.09,12.77 6198 +proj=longlat +a=6378249.145 +rf=293.465 +no_defs +4199 geographic Egypt 1930 24.7,21.97,36.95,31.68 6199 +proj=longlat +ellps=intl +no_defs +4200 geographic Pulkovo 1995 18.92,39.87,-168.97,85.19 1 6200 +proj=longlat +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +no_defs +4201 geographic Adindan 21.82,3.4,47.99,22.24 9 6201 +proj=longlat +a=6378249.145 +rf=293.465 +towgs84=-166,-15,204 +no_defs +4202 geographic AGD66 109.23,-47.2,163.2,-1.3 3 6202 +proj=longlat +ellps=aust_SA +towgs84=-117.808,-51.536,137.784,0.303,0.446,0.234,-0.29 +no_defs +4203 geographic AGD84 109.23,-38.53,153.61,-9.37 3 6203 +proj=longlat +ellps=aust_SA +towgs84=-117.763,-51.51,139.061,0.292,0.443,0.277,-0.191 +no_defs +4204 geographic Ain el Abd 34.51,16.37,55.67,32.16 18 6204 +proj=longlat +ellps=intl +towgs84=-143,-236,7 +no_defs +4205 geographic Afgooye 40.98,-1.71,51.47,12.03 44 6205 +proj=longlat +ellps=krass +towgs84=-43,-163,45 +no_defs +4206 geographic Agadez 0.16,11.69,16,23.53 6206 +proj=longlat +ellps=clrk80ign +no_defs +4207 geographic Lisbon -9.56,36.95,-6.19,42.16 2 6207 +proj=longlat +ellps=intl +towgs84=-288.885,-91.744,126.244,-1.691,0.41,-0.211,-4.598 +no_defs +4208 geographic Aratu -53.38,-35.71,-26,4.26 1 6208 +proj=longlat +ellps=intl +towgs84=-151.99,287.04,-147.45 +no_defs +4209 geographic Arc 1950 19.99,-26.88,35.93,-8.19 44 6209 +proj=longlat +a=6378249.145 +rf=293.4663077 +towgs84=-143,-90,-294 +no_defs +4210 geographic Arc 1960 28.85,-11.75,41.91,4.63 35 6210 +proj=longlat +a=6378249.145 +rf=293.465 +towgs84=-160,-6,-302 +no_defs +4211 geographic Batavia 95.16,-8.91,117.01,5.97 6 6211 +proj=longlat +ellps=bessel +towgs84=-377,681,-50 +no_defs +4212 geographic Barbados 1938 -59.71,13,-59.37,13.39 3 6212 +proj=longlat +a=6378249.145 +rf=293.465 +towgs84=31.95,300.99,419.19 +no_defs +4213 geographic Beduaram 7.81,12.8,14.9,16.7 15 6213 +proj=longlat +ellps=clrk80ign +towgs84=-106,-87,188 +no_defs +4214 geographic Beijing 1954 73.62,16.7,134.77,53.56 1 6214 +proj=longlat +ellps=krass +towgs84=15.8,-154.4,-82.3 +no_defs +4215 geographic BD50 2.5,49.5,6.4,51.51 6215 +proj=longlat +ellps=intl +no_defs +4216 geographic Bermuda 1957 -64.89,32.21,-64.61,32.43 1 6216 +proj=longlat +ellps=clrk66 +towgs84=-292.295,248.758,429.447,4.9971,2.99,6.6906,1.0289 +no_defs +4217 projected NAD83 / BLM 59N (ftUS) 167.65,49.01,174.01,56.28 8 6269 +proj=tmerc +lat_0=0 +lon_0=171 +k=0.9996 +x_0=500000.001016002 +y_0=0 +ellps=GRS80 +towgs84=-2,0,4 +to_meter=0.304800609601219 +no_defs +4218 geographic Bogota 1975 -79.1,-4.23,-66.87,13.68 10 6218 +proj=longlat +ellps=intl +towgs84=307,304,-318 +no_defs +4219 geographic Bukit Rimpah 105.07,-3.3,108.35,-1.44 6219 +proj=longlat +ellps=bessel +towgs84=-384,664,-48 +no_defs +4220 geographic Camacupa 1948 8.2,-18.02,24.09,-5.82 5 6220 +proj=longlat +a=6378249.145 +rf=293.465 +towgs84=-50.9,-347.6,-231 +no_defs +4221 geographic Campo Inchauspe -73.59,-54.93,-53.65,-21.78 9 6221 +proj=longlat +ellps=intl +towgs84=-148,136,90 +no_defs +4222 geographic Cape 16.45,-34.88,32.95,-17.78 9 6222 +proj=longlat +a=6378249.145 +rf=293.4663077 +towgs84=-136,-108,-292 +no_defs +4223 geographic Carthage 7.49,30.23,13.67,38.41 14 6223 +proj=longlat +ellps=clrk80ign +towgs84=-263,6,431 +no_defs +4224 geographic Chua -62.57,-31.91,-47.1,-15.37 12 6224 +proj=longlat +ellps=intl +towgs84=-134,229,-29 +no_defs +4225 geographic Corrego Alegre 1970-72 -58.16,-33.78,-34.74,-2.68 5 6225 +proj=longlat +ellps=intl +towgs84=-206.05,168.28,-3.82 +no_defs +4227 geographic Deir ez Zor 35.04,32.31,42.38,37.3 5 6227 +proj=longlat +ellps=clrk80ign +towgs84=-190.421,8.532,238.69 +no_defs +4229 geographic Egypt 1907 24.7,21.89,37.91,33.82 11 6229 +proj=longlat +ellps=helmert +towgs84=-130,110,-13 +no_defs +4230 geographic ED50 -16.1,25.71,48.61,84.73 10 6230 +proj=longlat +ellps=intl +towgs84=-87,-98,-121 +no_defs +4231 geographic ED87 -10.56,34.88,38.01,84.73 1 6231 +proj=longlat +ellps=intl +towgs84=-83.11,-97.38,-117.22,0.00569290865241986,-0.0446975835137458,0.0442850539012516,0.1218 +no_defs +4232 geographic Fahud 51.99,16.59,59.91,26.42 7 6232 +proj=longlat +a=6378249.145 +rf=293.465 +towgs84=-345,3,223 +no_defs +4236 geographic Hu Tzu Shan 1950 119.25,21.87,122.06,25.34 26 6236 +proj=longlat +ellps=intl +towgs84=-637,-549,-203 +no_defs +4237 geographic HD72 16.11,45.74,22.9,48.58 1 6237 +proj=longlat +ellps=GRS67 +towgs84=52.17,-71.82,-14.9 +no_defs +4238 geographic ID74 95.16,-10.98,141.01,5.97 3 6238 +proj=longlat +a=6378160 +rf=298.247 +towgs84=-1.977,-13.06,-9.993,0.364,0.254,0.689,-1.037 +no_defs +4239 geographic Indian 1954 92.2,5.63,105.64,28.55 21 6239 +proj=longlat +ellps=evrst30 +towgs84=217,823,299 +no_defs +4240 geographic Indian 1975 97.34,5.63,105.64,20.46 3 6240 +proj=longlat +ellps=evrst30 +towgs84=293,836,318,0.5,1.6,-2.8,2.1 +no_defs +4241 geographic Jamaica 1875 -78.43,17.64,-76.17,18.58 6241 +proj=longlat +a=6378249.14480801 +b=6356514.96620413 +no_defs +4242 geographic JAD69 -78.43,17.64,-76.17,18.58 1 6242 +proj=longlat +ellps=clrk66 +towgs84=-33.722,153.789,94.959,-8.581,-4.478,4.54,8.95 +no_defs +4243 geographic Kalianpur 1880 60.86,8.02,101.17,37.07 6243 +proj=longlat +a=6377299.36559538 +b=6356098.35900516 +no_defs +4244 geographic Kandawala 79.64,5.86,81.95,9.88 35 6244 +proj=longlat +ellps=evrst30 +towgs84=-97,787,86 +no_defs +4245 geographic Kertau 1968 99.59,1.13,105.82,7.81 15 6245 +proj=longlat +ellps=evrst48 +towgs84=-11,851,5 +no_defs +4246 geographic KOC 46.54,28.53,48.48,30.09 1 6246 +proj=longlat +a=6378249.145 +rf=293.465 +towgs84=-294.7,-200.1,525.5 +no_defs +4247 geographic La Canoa -73.38,0.64,-59.8,12.25 2.5 6247 +proj=longlat +ellps=intl +towgs84=-273.5,110.6,-357.9 +no_defs +4248 geographic PSAD56 -81.41,-43.5,-47.99,12.68 42 6248 +proj=longlat +ellps=intl +towgs84=-288,175,-376 +no_defs +4249 geographic Lake -72.4,8.72,-70.78,11.04 6249 +proj=longlat +ellps=intl +no_defs +4250 geographic Leigon -3.79,1.4,2.1,11.16 5 6250 +proj=longlat +a=6378249.145 +rf=293.465 +towgs84=-130,29,364 +no_defs +4251 geographic Liberia 1964 -11.52,4.29,-7.36,8.52 26 6251 +proj=longlat +a=6378249.145 +rf=293.465 +towgs84=-90,40,88 +no_defs +4252 geographic Lome -0.15,2.91,2.42,11.14 6252 +proj=longlat +ellps=clrk80ign +no_defs +4253 geographic Luzon 1911 116.89,4.99,126.65,19.45 17 6253 +proj=longlat +ellps=clrk66 +towgs84=-133,-77,-51 +no_defs +4254 geographic Hito XVIII 1963 -74.83,-55.96,-63.73,-51.65 44 6254 +proj=longlat +ellps=intl +towgs84=16,196,93 +no_defs +4255 geographic Herat North 60.5,29.4,74.92,38.48 6255 +proj=longlat +ellps=intl +towgs84=-333,-222,114 +no_defs +4256 geographic Mahe 1971 55.3,-4.86,55.59,-4.5 44 6256 +proj=longlat +a=6378249.145 +rf=293.465 +towgs84=41,-220,-134 +no_defs +4257 geographic Makassar 118.71,-6.54,120.78,-1.88 6257 +proj=longlat +ellps=bessel +towgs84=-587.8,519.75,145.76 +no_defs +4258 geographic ETRS89 -16.1,33.26,38.01,84.73 1 6258 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +4259 geographic Malongo 1987 10.53,-6.04,12.37,-5.05 5 6259 +proj=longlat +ellps=intl +towgs84=-254.1,-5.36,-100.29 +no_defs +4261 geographic Merchich -17.16,20.71,-1.01,35.97 7 6261 +proj=longlat +ellps=clrk80ign +towgs84=31,146,47 +no_defs +4262 geographic Massawa 36.44,12.36,43.31,18.1 44 6262 +proj=longlat +ellps=bessel +towgs84=639,405,60 +no_defs +4263 geographic Minna 2.66,1.92,14.65,13.9 15 6263 +proj=longlat +a=6378249.145 +rf=293.465 +towgs84=-92,-93,122 +no_defs +4265 geographic Monte Mario 5.93,34.76,18.99,47.1 4 6265 +proj=longlat +ellps=intl +towgs84=-104.1,-49.1,-9.9,0.971,-2.917,0.714,-11.68 +no_defs +4266 geographic M'poraloko 7.03,-6.37,14.52,2.32 0.5 6266 +proj=longlat +ellps=clrk80ign +towgs84=-80.7,-132.5,41.1 +no_defs +4267 geographic NAD27 167.65,7.15,-47.74,83.17 20 6267 +proj=longlat +ellps=clrk66 +towgs84=-10,158,187 +no_defs +4269 geographic NAD83 167.65,14.92,-40.73,86.45 4 6269 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +4270 geographic Nahrwan 1967 50.55,22.63,57.13,27.05 44 6270 +proj=longlat +a=6378249.145 +rf=293.465 +towgs84=-249,-156,381 +no_defs +4271 geographic Naparima 1972 -60.9,11.08,-60.44,11.41 26 6271 +proj=longlat +ellps=intl +towgs84=-2,374,172 +no_defs +4272 geographic NZGD49 165.87,-47.65,179.27,-33.89 4 6272 +proj=longlat +ellps=intl +towgs84=59.47,-5.04,187.44,0.47,-0.1,1.024,-4.5993 +no_defs +4273 geographic NGO 1948 4.39,57.9,31.32,71.24 3 6273 +proj=longlat +a=6377492.018 +rf=299.1528128 +towgs84=278.3,93,474.5,7.889,0.05,-6.61,6.21 +no_defs +4274 geographic Datum 73 -9.56,36.95,-6.19,42.16 1 6274 +proj=longlat +ellps=intl +towgs84=-239.749,88.181,30.488,0.263,0.082,1.211,2.229 +no_defs +4275 geographic NTF -4.87,41.31,9.63,51.14 2 6275 +proj=longlat +ellps=clrk80ign +towgs84=-168,-60,320 +no_defs +4276 geographic NSWC 9Z-2 -180,-90,180,90 6276 +proj=longlat +ellps=NWL9D +no_defs +4277 geographic OSGB36 -9.01,49.75,2.01,61.01 2 6277 +proj=longlat +ellps=airy +towgs84=446.448,-125.157,542.06,0.15,0.247,0.842,-20.489 +no_defs +4278 geographic OSGB70 -8.82,49.79,1.92,60.94 6278 +proj=longlat +ellps=airy +no_defs +4279 geographic OS(SN)80 -10.56,49.81,1.84,60.9 6279 +proj=longlat +ellps=airy +no_defs +4281 geographic Palestine 1923 34.17,29.18,39.31,33.38 2 6281 +proj=longlat +a=6378300.789 +b=6356566.435 +towgs84=-275.7224,94.7824,340.8944,-8.001,-4.42,-11.821,1 +no_defs +4282 geographic Pointe Noire 8.84,-6.91,18.65,3.72 44 6282 +proj=longlat +ellps=clrk80ign +towgs84=-148,51,-291 +no_defs +4283 geographic GDA94 93.41,-60.55,173.34,-8.47 3 6283 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +4284 geographic Pulkovo 1942 19.57,35.14,-168.97,81.91 4.5 6284 +proj=longlat +ellps=krass +towgs84=25,-141,-78.5,0,0.35,0.736,0 +no_defs +4285 geographic Qatar 1974 50.55,24.55,53.04,27.05 1 6285 +proj=longlat +ellps=intl +towgs84=-128.16,-282.42,21.93 +no_defs +4286 geographic Qatar 1948 50.69,24.55,51.68,26.2 6286 +proj=longlat +ellps=helmert +no_defs +4288 geographic Loma Quintana -73.38,7.75,-59.8,12.25 6288 +proj=longlat +ellps=intl +no_defs +4289 geographic Amersfoort 3.2,50.75,7.22,53.7 1 6289 +proj=longlat +ellps=bessel +towgs84=565.4171,50.3319,465.5524,-0.398957388243134,0.343987817378283,-1.87740163998045,4.0725 +no_defs +4292 geographic Sapper Hill 1943 -61.55,-52.51,-57.6,-50.96 2 6292 +proj=longlat +ellps=intl +towgs84=-355,21,72 +no_defs +4293 geographic Schwarzeck 8.24,-30.64,25.27,-16.95 35 6293 +proj=longlat +ellps=bess_nam +towgs84=616,97,-251 +no_defs +4295 geographic Serindung 108.79,0.06,109.78,2.13 6295 +proj=longlat +ellps=bessel +no_defs +4297 geographic Tananarive 42.53,-26.59,51.03,-11.69 3 6297 +proj=longlat +ellps=intl +towgs84=-198.383,-240.517,-107.909 +no_defs +4298 geographic Timbalai 1948 109.31,0.85,119.61,7.67 19 6298 +proj=longlat +ellps=evrstSS +towgs84=-679,669,-48 +no_defs +4299 geographic TM65 -10.56,51.39,-5.34,55.43 1 6299 +proj=longlat +a=6377340.189 +rf=299.3249646 +towgs84=482.5,-130.6,564.6,-1.042,-0.214,-0.631,8.15 +no_defs +4300 geographic TM75 -10.56,51.39,-5.34,55.43 1 6300 +proj=longlat +a=6377340.189 +rf=299.3249646 +towgs84=482.5,-130.6,564.6,-1.042,-0.214,-0.631,8.15 +no_defs +4301 geographic Tokyo 122.83,20.37,154.05,45.54 9 6301 +proj=longlat +ellps=bessel +towgs84=-146.414,507.337,680.507 +no_defs +4302 geographic Trinidad 1903 -62.09,9.83,-60,11.51 2 6302 +proj=longlat +a=6378293.64520876 +b=6356617.98767984 +towgs84=-61.702,284.488,472.052 +no_defs +4303 geographic TC(1948) 51.56,22.63,56.03,25.34 6303 +proj=longlat +ellps=helmert +no_defs +4304 geographic Voirol 1875 -2.95,31.99,9.09,37.14 6304 +proj=longlat +ellps=clrk80ign +towgs84=-73,-247,227 +no_defs +4306 geographic Bern 1938 5.95,45.81,10.5,47.81 6306 +proj=longlat +ellps=bessel +no_defs +4307 geographic Nord Sahara 1959 -8.67,18.97,11.99,38.8 1 6307 +proj=longlat +a=6378249.145 +rf=293.465 +towgs84=-267.407,-47.068,446.357,-0.179423,5.577661,-1.27762,1.204866 +no_defs +4308 geographic RT38 10.93,55.28,24.17,69.07 6308 +proj=longlat +ellps=bessel +no_defs +4309 geographic Yacare -58.49,-35,-53.09,-30.09 1.5 6309 +proj=longlat +ellps=intl +towgs84=-124.45,183.74,44.64,-0.4384,0.5446,-0.9706,-2.1365 +no_defs +4310 geographic Yoff -20.22,10.64,-11.36,16.7 5 6310 +proj=longlat +ellps=clrk80ign +towgs84=-30,190,89 +no_defs +4311 geographic Zanderij -58.08,1.83,-52.66,9.35 11 6311 +proj=longlat +ellps=intl +towgs84=-265,120,-358 +no_defs +4312 geographic MGI 9.53,46.4,17.17,49.02 1.5 6312 +proj=longlat +ellps=bessel +towgs84=577.326,90.129,463.919,5.137,1.474,5.297,2.4232 +no_defs +4313 geographic BD72 2.5,49.5,6.4,51.51 1 6313 +proj=longlat +ellps=intl +towgs84=-106.8686,52.2978,-103.7239,0.3366,-0.457,1.8422,-1.2747 +no_defs +4314 geographic DHDN 5.86,47.27,13.84,55.09 3 6314 +proj=longlat +ellps=bessel +towgs84=598.1,73.7,418.2,0.202,0.045,-2.455,6.7 +no_defs +4315 geographic Conakry 1905 -15.13,7.19,-7.65,12.68 30 6315 +proj=longlat +ellps=clrk80ign +towgs84=-23,259,-9 +no_defs +4316 geographic Dealul Piscului 1930 20.26,43.62,29.74,48.27 10 6316 +proj=longlat +ellps=intl +towgs84=103.25,-100.4,-307.19 +no_defs +4318 geographic NGN 46.54,28.53,48.48,30.09 1 6318 +proj=longlat +ellps=WGS84 +towgs84=-3.2,-5.7,2.8 +no_defs +4319 geographic KUDAMS 47.78,29.17,48.16,29.45 1 6319 +proj=longlat +ellps=GRS80 +towgs84=226.702,-193.337,-35.371,-2.229,-4.391,9.238,0.9798 +no_defs +4322 geographic WGS 72 -180,-90,180,90 2 6322 +proj=longlat +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +no_defs +4324 geographic WGS 72BE -180,-90,180,90 2 6324 +proj=longlat +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +no_defs +4326 geographic WGS 84 -180,-90,180,90 6326 +proj=longlat +datum=WGS84 +no_defs +4390 projected Kertau 1968 / Johor Grid 102.44,1.21,104.6,2.95 15 6245 +proj=cass +lat_0=2.04258333333333 +lon_0=103.562758333333 +x_0=0 +y_0=0 +ellps=evrst48 +towgs84=-11,851,5 +units=m +no_defs +4391 projected Kertau 1968 / Sembilan and Melaka Grid 101.7,2.03,102.71,3.28 15 6245 +proj=cass +lat_0=2.71228333333333 +lon_0=101.941166666667 +x_0=-242.005 +y_0=-948.547 +ellps=evrst48 +towgs84=-11,851,5 +units=m +no_defs +4392 projected Kertau 1968 / Pahang Grid 101.33,2.45,103.67,4.78 15 6245 +proj=cass +lat_0=3.71097222222222 +lon_0=102.436177777778 +x_0=0 +y_0=0 +ellps=evrst48 +towgs84=-11,851,5 +units=m +no_defs +4393 projected Kertau 1968 / Selangor Grid 100.76,2.54,101.97,3.87 15 6245 +proj=cass +lat_0=3.68034444444444 +lon_0=101.508244444444 +x_0=-21759.438 +y_0=55960.906 +ellps=evrst48 +towgs84=-11,851,5 +units=m +no_defs +4394 projected Kertau 1968 / Terengganu Grid 102.38,3.89,103.72,5.9 15 6245 +proj=cass +lat_0=4.94614166666667 +lon_0=102.895208333333 +x_0=0 +y_0=0 +ellps=evrst48 +towgs84=-11,851,5 +units=m +no_defs +4395 projected Kertau 1968 / Pinang Grid 100.12,5.12,100.56,5.59 15 6245 +proj=cass +lat_0=5.421325 +lon_0=100.345869444444 +x_0=0 +y_0=0 +ellps=evrst48 +towgs84=-11,851,5 +units=m +no_defs +4396 projected Kertau 1968 / Kedah and Perlis Grid 99.59,5.08,101.12,6.72 15 6245 +proj=cass +lat_0=5.96514722222222 +lon_0=100.637594444444 +x_0=0 +y_0=0 +ellps=evrst48 +towgs84=-11,851,5 +units=m +no_defs +4397 projected Kertau 1968 / Perak Revised Grid 100.07,3.66,102,5.92 15 6245 +proj=cass +lat_0=4.85938055555556 +lon_0=100.816766666667 +x_0=0 +y_0=133453.669 +ellps=evrst48 +towgs84=-11,851,5 +units=m +no_defs +4398 projected Kertau 1968 / Kelantan Grid 101.33,4.54,102.67,6.29 15 6245 +proj=cass +lat_0=5.89392222222222 +lon_0=102.177291666667 +x_0=0 +y_0=0 +ellps=evrst48 +towgs84=-11,851,5 +units=m +no_defs +4399 projected NAD27 / BLM 59N (ftUS) 167.65,49.01,174.01,56.28 18 6267 +proj=tmerc +lat_0=0 +lon_0=171 +k=0.9996 +x_0=500000.001016002 +y_0=0 +ellps=clrk66 +towgs84=2,204,105 +to_meter=0.304800609601219 +no_defs +4400 projected NAD27 / BLM 60N (ftUS) 174,47.92,180,56.67 18 6267 +proj=tmerc +lat_0=0 +lon_0=177 +k=0.9996 +x_0=500000.001016002 +y_0=0 +ellps=clrk66 +towgs84=2,204,105 +to_meter=0.304800609601219 +no_defs +4401 projected NAD27 / BLM 1N (ftUS) -180,47.88,-173.99,63.21 15 6267 +proj=tmerc +lat_0=0 +lon_0=-177 +k=0.9996 +x_0=500000.001016002 +y_0=0 +ellps=clrk66 +towgs84=-2,152,149 +to_meter=0.304800609601219 +no_defs +4402 projected NAD27 / BLM 2N (ftUS) -174,48.66,-167.99,73.05 15 6267 +proj=tmerc +lat_0=0 +lon_0=-171 +k=0.9996 +x_0=500000.001016002 +y_0=0 +ellps=clrk66 +towgs84=-2,152,149 +to_meter=0.304800609601219 +no_defs +4403 projected NAD27 / BLM 3N (ftUS) -168,49.52,-161.99,74.29 12 6267 +proj=tmerc +lat_0=0 +lon_0=-165 +k=0.9996 +x_0=500000.001016002 +y_0=0 +ellps=clrk66 +towgs84=-5,135,172 +to_meter=0.304800609601219 +no_defs +4404 projected NAD27 / BLM 4N (ftUS) -162,50.98,-155.99,74.71 12 6267 +proj=tmerc +lat_0=0 +lon_0=-159 +k=0.9996 +x_0=500000.001016002 +y_0=0 +ellps=clrk66 +towgs84=-5,135,172 +to_meter=0.304800609601219 +no_defs +4405 projected NAD27 / BLM 5N (ftUS) -156,52.15,-149.99,74.71 12 6267 +proj=tmerc +lat_0=0 +lon_0=-153 +k=0.9996 +x_0=500000.001016002 +y_0=0 +ellps=clrk66 +towgs84=-5,135,172 +to_meter=0.304800609601219 +no_defs +4406 projected NAD27 / BLM 6N (ftUS) -150,54.05,-143.99,74.13 12 6267 +proj=tmerc +lat_0=0 +lon_0=-147 +k=0.9996 +x_0=500000.001016002 +y_0=0 +ellps=clrk66 +towgs84=-5,135,172 +to_meter=0.304800609601219 +no_defs +4407 projected NAD27 / BLM 7N (ftUS) -144,53.47,-137.99,73.59 12 6267 +proj=tmerc +lat_0=0 +lon_0=-141 +k=0.9996 +x_0=500000.001016002 +y_0=0 +ellps=clrk66 +towgs84=-5,135,172 +to_meter=0.304800609601219 +no_defs +4408 projected NAD27 / BLM 8N (ftUS) -138,53.6,-131.99,73.04 20 6267 +proj=tmerc +lat_0=0 +lon_0=-135 +k=0.9996 +x_0=500000.001016002 +y_0=0 +ellps=clrk66 +towgs84=-10,158,187 +to_meter=0.304800609601219 +no_defs +4409 projected NAD27 / BLM 9N (ftUS) -132,35.38,-126,56.84 20 6267 +proj=tmerc +lat_0=0 +lon_0=-129 +k=0.9996 +x_0=500000.001016002 +y_0=0 +ellps=clrk66 +towgs84=-10,158,187 +to_meter=0.304800609601219 +no_defs +4410 projected NAD27 / BLM 10N (ftUS) -126,30.54,-119.99,49.09 10 6267 +proj=tmerc +lat_0=0 +lon_0=-123 +k=0.9996 +x_0=500000.001016002 +y_0=0 +ellps=clrk66 +towgs84=-8,160,176 +to_meter=0.304800609601219 +no_defs +4411 projected NAD27 / BLM 11N (ftUS) -120,30.88,-114,49.01 7 6267 +proj=tmerc +lat_0=0 +lon_0=-117 +k=0.9996 +x_0=500000.001016002 +y_0=0 +ellps=clrk66 +towgs84=-8,159,175 +to_meter=0.304800609601219 +no_defs +4412 projected NAD27 / BLM 12N (ftUS) -114,31.33,-108,49.01 7 6267 +proj=tmerc +lat_0=0 +lon_0=-111 +k=0.9996 +x_0=500000.001016002 +y_0=0 +ellps=clrk66 +towgs84=-8,159,175 +to_meter=0.304800609601219 +no_defs +4413 projected NAD27 / BLM 13N (ftUS) -108,28.98,-102,49.01 7 6267 +proj=tmerc +lat_0=0 +lon_0=-105 +k=0.9996 +x_0=500000.001016002 +y_0=0 +ellps=clrk66 +towgs84=-8,159,175 +to_meter=0.304800609601219 +no_defs +4414 projected NAD83(HARN) / Guam Map Grid 144.58,13.18,145.01,13.7 2 6152 +proj=tmerc +lat_0=13.5 +lon_0=144.75 +k=1 +x_0=100000 +y_0=200000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +4415 projected Katanga 1955 / Katanga Lambert 21.74,-13.46,30.78,-4.99 1.5 6695 +proj=lcc +lat_0=-9 +lon_0=26 +lat_1=-6.5 +lat_2=-11.5 +x_0=500000 +y_0=500000 +ellps=clrk66 +towgs84=-103.746,-9.614,-255.95 +units=m +no_defs +4417 projected Pulkovo 1942(83) / 3-degree Gauss-Kruger zone 7 19.5,46.1,22.5,49.59 4 6178 +proj=tmerc +lat_0=0 +lon_0=21 +k=1 +x_0=7500000 +y_0=0 +ellps=krass +towgs84=28,-121,-77 +units=m +no_defs +4418 projected NAD27 / BLM 18N (ftUS) -78,28.28,-72,45.03 10 6267 +proj=tmerc +lat_0=0 +lon_0=-75 +k=0.9996 +x_0=500000.001016002 +y_0=0 +ellps=clrk66 +towgs84=-8,160,176 +to_meter=0.304800609601219 +no_defs +4419 projected NAD27 / BLM 19N (ftUS) -72,33.61,-65.99,47.47 10 6267 +proj=tmerc +lat_0=0 +lon_0=-69 +k=0.9996 +x_0=500000.001016002 +y_0=0 +ellps=clrk66 +towgs84=-8,160,176 +to_meter=0.304800609601219 +no_defs +4420 projected NAD83 / BLM 60N (ftUS) 174,47.92,180,56.67 8 6269 +proj=tmerc +lat_0=0 +lon_0=177 +k=0.9996 +x_0=500000.001016002 +y_0=0 +ellps=GRS80 +towgs84=-2,0,4 +to_meter=0.304800609601219 +no_defs +4421 projected NAD83 / BLM 1N (ftUS) -180,47.88,-173.99,63.21 8 6269 +proj=tmerc +lat_0=0 +lon_0=-177 +k=0.9996 +x_0=500000.001016002 +y_0=0 +ellps=GRS80 +towgs84=-2,0,4 +to_meter=0.304800609601219 +no_defs +4422 projected NAD83 / BLM 2N (ftUS) -174,48.66,-167.99,73.05 4 6269 +proj=tmerc +lat_0=0 +lon_0=-171 +k=0.9996 +x_0=500000.001016002 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +4423 projected NAD83 / BLM 3N (ftUS) -168,49.52,-161.99,74.29 4 6269 +proj=tmerc +lat_0=0 +lon_0=-165 +k=0.9996 +x_0=500000.001016002 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +4424 projected NAD83 / BLM 4N (ftUS) -162,50.98,-155.99,74.71 4 6269 +proj=tmerc +lat_0=0 +lon_0=-159 +k=0.9996 +x_0=500000.001016002 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +4425 projected NAD83 / BLM 5N (ftUS) -156,52.15,-149.99,74.71 4 6269 +proj=tmerc +lat_0=0 +lon_0=-153 +k=0.9996 +x_0=500000.001016002 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +4426 projected NAD83 / BLM 6N (ftUS) -150,54.05,-143.99,74.13 4 6269 +proj=tmerc +lat_0=0 +lon_0=-147 +k=0.9996 +x_0=500000.001016002 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +4427 projected NAD83 / BLM 7N (ftUS) -144,53.47,-137.99,73.59 4 6269 +proj=tmerc +lat_0=0 +lon_0=-141 +k=0.9996 +x_0=500000.001016002 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +4428 projected NAD83 / BLM 8N (ftUS) -138,53.6,-131.99,73.04 4 6269 +proj=tmerc +lat_0=0 +lon_0=-135 +k=0.9996 +x_0=500000.001016002 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +4429 projected NAD83 / BLM 9N (ftUS) -132,35.38,-126,56.84 4 6269 +proj=tmerc +lat_0=0 +lon_0=-129 +k=0.9996 +x_0=500000.001016002 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +4430 projected NAD83 / BLM 10N (ftUS) -126,30.54,-119.99,49.09 4 6269 +proj=tmerc +lat_0=0 +lon_0=-123 +k=0.9996 +x_0=500000.001016002 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +4431 projected NAD83 / BLM 11N (ftUS) -120,30.88,-114,49.01 4 6269 +proj=tmerc +lat_0=0 +lon_0=-117 +k=0.9996 +x_0=500000.001016002 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +4432 projected NAD83 / BLM 12N (ftUS) -114,31.33,-108,49.01 4 6269 +proj=tmerc +lat_0=0 +lon_0=-111 +k=0.9996 +x_0=500000.001016002 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +4433 projected NAD83 / BLM 13N (ftUS) -108,28.98,-102,49.01 4 6269 +proj=tmerc +lat_0=0 +lon_0=-105 +k=0.9996 +x_0=500000.001016002 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +4434 projected Pulkovo 1942(83) / 3-degree Gauss-Kruger zone 8 22.5,47.76,22.9,49.1 4 6178 +proj=tmerc +lat_0=0 +lon_0=24 +k=1 +x_0=8500000 +y_0=0 +ellps=krass +towgs84=28,-121,-77 +units=m +no_defs +4437 projected NAD83(NSRS2007) / Puerto Rico and Virgin Is. -67.97,17.62,-64.51,18.57 2 6759 +proj=lcc +lat_0=17.8333333333333 +lon_0=-66.4333333333333 +lat_1=18.4333333333333 +lat_2=18.0333333333333 +x_0=200000 +y_0=200000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +4438 projected NAD83 / BLM 18N (ftUS) -78,28.28,-72,45.03 4 6269 +proj=tmerc +lat_0=0 +lon_0=-75 +k=0.9996 +x_0=500000.001016002 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +4439 projected NAD83 / BLM 19N (ftUS) -72,33.61,-65.99,47.47 4 6269 +proj=tmerc +lat_0=0 +lon_0=-69 +k=0.9996 +x_0=500000.001016002 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +4455 projected NAD27 / Pennsylvania South -80.53,39.71,-74.72,41.18 10 6267 +proj=lcc +lat_0=39.3333333333333 +lon_0=-77.75 +lat_1=40.9666666666667 +lat_2=39.9333333333333 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +towgs84=-8,160,176 +to_meter=0.304800609601219 +no_defs +4456 projected NAD27 / New York Long Island -74.26,40.47,-71.8,41.3 10 6267 +proj=lcc +lat_0=40.5 +lon_0=-74 +lat_1=41.0333333333333 +lat_2=40.6666666666667 +x_0=609601.219202438 +y_0=30480.0609601219 +ellps=clrk66 +towgs84=-8,160,176 +to_meter=0.304800609601219 +no_defs +4457 projected NAD83 / South Dakota North (ftUS) -104.07,44.14,-96.45,45.95 4 6269 +proj=lcc +lat_0=43.8333333333333 +lon_0=-100 +lat_1=45.6833333333333 +lat_2=44.4166666666667 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +4462 projected WGS 84 / Australian Centre for Remote Sensing Lambert 112.85,-43.7,153.69,-9.86 6326 +proj=lcc +lat_0=-27 +lon_0=132 +lat_1=-18 +lat_2=-36 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +4463 geographic RGSPM06 -57.1,43.41,-55.9,47.37 1 1038 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +4467 projected RGSPM06 / UTM zone 21N -57.1,43.41,-55.9,47.37 1 1038 +proj=utm +zone=21 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +4470 geographic RGM04 43.68,-14.49,46.7,-11.33 1 1036 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +4471 projected RGM04 / UTM zone 38S 43.68,-14.49,46.7,-11.33 1 1036 +proj=utm +zone=38 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +4475 geographic Cadastre 1997 44.98,-13.05,45.35,-12.61 1 1037 +proj=longlat +ellps=intl +towgs84=-381.788,-57.501,-256.673 +no_defs +4483 geographic Mexico ITRF92 -122.19,12.1,-84.64,32.72 1 1042 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +4484 projected Mexico ITRF92 / UTM zone 11N -122.19,15.01,-114,32.72 1 1042 +proj=utm +zone=11 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +4485 projected Mexico ITRF92 / UTM zone 12N -114,15.09,-108,32.27 1 1042 +proj=utm +zone=12 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +4486 projected Mexico ITRF92 / UTM zone 13N -108,14.05,-102,31.79 1 1042 +proj=utm +zone=13 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +4487 projected Mexico ITRF92 / UTM zone 14N -102,12.3,-96,29.81 1 1042 +proj=utm +zone=14 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +4488 projected Mexico ITRF92 / UTM zone 15N -96,12.1,-90,26 1 1042 +proj=utm +zone=15 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +4489 projected Mexico ITRF92 / UTM zone 16N -90,17.81,-84.64,25.77 1 1042 +proj=utm +zone=16 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +4490 geographic China Geodetic Coordinate System 2000 73.62,16.7,134.77,53.56 1043 +proj=longlat +ellps=GRS80 +no_defs +4491 projected CGCS2000 / Gauss-Kruger zone 13 73.62,35.42,78.01,41.07 1043 +proj=tmerc +lat_0=0 +lon_0=75 +k=1 +x_0=13500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +4492 projected CGCS2000 / Gauss-Kruger zone 14 77.98,29.16,84,47.23 1043 +proj=tmerc +lat_0=0 +lon_0=81 +k=1 +x_0=14500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +4493 projected CGCS2000 / Gauss-Kruger zone 15 84,27.32,90,49.18 1043 +proj=tmerc +lat_0=0 +lon_0=87 +k=1 +x_0=15500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +4494 projected CGCS2000 / Gauss-Kruger zone 16 90,27.71,96.01,47.9 1043 +proj=tmerc +lat_0=0 +lon_0=93 +k=1 +x_0=16500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +4495 projected CGCS2000 / Gauss-Kruger zone 17 96,21.13,102.01,43.18 1043 +proj=tmerc +lat_0=0 +lon_0=99 +k=1 +x_0=17500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +4496 projected CGCS2000 / Gauss-Kruger zone 18 102,17.75,108,42.47 1043 +proj=tmerc +lat_0=0 +lon_0=105 +k=1 +x_0=18500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +4497 projected CGCS2000 / Gauss-Kruger zone 19 108,16.7,114,45.11 1043 +proj=tmerc +lat_0=0 +lon_0=111 +k=1 +x_0=19500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +4498 projected CGCS2000 / Gauss-Kruger zone 20 114,19.02,120,51.52 1043 +proj=tmerc +lat_0=0 +lon_0=117 +k=1 +x_0=20500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +4499 projected CGCS2000 / Gauss-Kruger zone 21 120,24.64,126,53.56 1043 +proj=tmerc +lat_0=0 +lon_0=123 +k=1 +x_0=21500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +4500 projected CGCS2000 / Gauss-Kruger zone 22 126,29.7,132,52.79 1043 +proj=tmerc +lat_0=0 +lon_0=129 +k=1 +x_0=22500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +4501 projected CGCS2000 / Gauss-Kruger zone 23 132,45.02,134.77,48.4 1043 +proj=tmerc +lat_0=0 +lon_0=135 +k=1 +x_0=23500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +4502 projected CGCS2000 / Gauss-Kruger CM 75E 73.62,35.42,78.01,41.07 1043 +proj=tmerc +lat_0=0 +lon_0=75 +k=1 +x_0=500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +4503 projected CGCS2000 / Gauss-Kruger CM 81E 77.98,29.16,84,47.23 1043 +proj=tmerc +lat_0=0 +lon_0=81 +k=1 +x_0=500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +4504 projected CGCS2000 / Gauss-Kruger CM 87E 84,27.32,90,49.18 1043 +proj=tmerc +lat_0=0 +lon_0=87 +k=1 +x_0=500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +4505 projected CGCS2000 / Gauss-Kruger CM 93E 90,27.71,96.01,47.9 1043 +proj=tmerc +lat_0=0 +lon_0=93 +k=1 +x_0=500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +4506 projected CGCS2000 / Gauss-Kruger CM 99E 96,21.13,102.01,43.18 1043 +proj=tmerc +lat_0=0 +lon_0=99 +k=1 +x_0=500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +4507 projected CGCS2000 / Gauss-Kruger CM 105E 102,17.75,108,42.47 1043 +proj=tmerc +lat_0=0 +lon_0=105 +k=1 +x_0=500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +4508 projected CGCS2000 / Gauss-Kruger CM 111E 108,16.7,114,45.11 1043 +proj=tmerc +lat_0=0 +lon_0=111 +k=1 +x_0=500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +4509 projected CGCS2000 / Gauss-Kruger CM 117E 114,19.02,120,51.52 1043 +proj=tmerc +lat_0=0 +lon_0=117 +k=1 +x_0=500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +4510 projected CGCS2000 / Gauss-Kruger CM 123E 120,24.64,126,53.56 1043 +proj=tmerc +lat_0=0 +lon_0=123 +k=1 +x_0=500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +4511 projected CGCS2000 / Gauss-Kruger CM 129E 126,29.7,132,52.79 1043 +proj=tmerc +lat_0=0 +lon_0=129 +k=1 +x_0=500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +4512 projected CGCS2000 / Gauss-Kruger CM 135E 132,45.02,134.77,48.4 1043 +proj=tmerc +lat_0=0 +lon_0=135 +k=1 +x_0=500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +4513 projected CGCS2000 / 3-degree Gauss-Kruger zone 25 73.62,35.81,76.5,40.65 1043 +proj=tmerc +lat_0=0 +lon_0=75 +k=1 +x_0=25500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +4514 projected CGCS2000 / 3-degree Gauss-Kruger zone 26 76.5,31.03,79.5,41.83 1043 +proj=tmerc +lat_0=0 +lon_0=78 +k=1 +x_0=26500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +4515 projected CGCS2000 / 3-degree Gauss-Kruger zone 27 79.5,29.95,82.51,45.88 1043 +proj=tmerc +lat_0=0 +lon_0=81 +k=1 +x_0=27500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +4516 projected CGCS2000 / 3-degree Gauss-Kruger zone 28 82.5,28.26,85.5,47.23 1043 +proj=tmerc +lat_0=0 +lon_0=84 +k=1 +x_0=28500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +4517 projected CGCS2000 / 3-degree Gauss-Kruger zone 29 85.5,27.8,88.5,49.18 1043 +proj=tmerc +lat_0=0 +lon_0=87 +k=1 +x_0=29500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +4518 projected CGCS2000 / 3-degree Gauss-Kruger zone 30 88.49,27.32,91.51,48.42 1043 +proj=tmerc +lat_0=0 +lon_0=90 +k=1 +x_0=30500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +4519 projected CGCS2000 / 3-degree Gauss-Kruger zone 31 91.5,27.71,94.5,45.13 1043 +proj=tmerc +lat_0=0 +lon_0=93 +k=1 +x_0=31500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +4520 projected CGCS2000 / 3-degree Gauss-Kruger zone 32 94.5,28.23,97.51,44.5 1043 +proj=tmerc +lat_0=0 +lon_0=96 +k=1 +x_0=32500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +4521 projected CGCS2000 / 3-degree Gauss-Kruger zone 33 97.5,21.43,100.5,42.76 1043 +proj=tmerc +lat_0=0 +lon_0=99 +k=1 +x_0=33500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +4522 projected CGCS2000 / 3-degree Gauss-Kruger zone 34 100.5,21.13,103.5,42.69 1043 +proj=tmerc +lat_0=0 +lon_0=102 +k=1 +x_0=34500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +4523 projected CGCS2000 / 3-degree Gauss-Kruger zone 35 103.5,22.5,106.51,42.21 1043 +proj=tmerc +lat_0=0 +lon_0=105 +k=1 +x_0=35500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +4524 projected CGCS2000 / 3-degree Gauss-Kruger zone 36 106.5,18.19,109.51,42.47 1043 +proj=tmerc +lat_0=0 +lon_0=108 +k=1 +x_0=36500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +4525 projected CGCS2000 / 3-degree Gauss-Kruger zone 37 109.5,18.11,112.5,45.11 1043 +proj=tmerc +lat_0=0 +lon_0=111 +k=1 +x_0=37500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +4526 projected CGCS2000 / 3-degree Gauss-Kruger zone 38 112.5,21.52,115.5,45.45 1043 +proj=tmerc +lat_0=0 +lon_0=114 +k=1 +x_0=38500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +4527 projected CGCS2000 / 3-degree Gauss-Kruger zone 39 115.5,22.6,118.5,49.88 1043 +proj=tmerc +lat_0=0 +lon_0=117 +k=1 +x_0=39500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +4528 projected CGCS2000 / 3-degree Gauss-Kruger zone 40 118.5,24.43,121.5,53.33 1043 +proj=tmerc +lat_0=0 +lon_0=120 +k=1 +x_0=40500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +4529 projected CGCS2000 / 3-degree Gauss-Kruger zone 41 121.5,28.22,124.5,53.56 1043 +proj=tmerc +lat_0=0 +lon_0=123 +k=1 +x_0=41500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +4530 projected CGCS2000 / 3-degree Gauss-Kruger zone 42 124.5,40.19,127.5,53.2 1043 +proj=tmerc +lat_0=0 +lon_0=126 +k=1 +x_0=42500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +4531 projected CGCS2000 / 3-degree Gauss-Kruger zone 43 127.5,41.37,130.5,50.25 1043 +proj=tmerc +lat_0=0 +lon_0=129 +k=1 +x_0=43500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +4532 projected CGCS2000 / 3-degree Gauss-Kruger zone 44 130.5,42.42,133.5,48.88 1043 +proj=tmerc +lat_0=0 +lon_0=132 +k=1 +x_0=44500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +4533 projected CGCS2000 / 3-degree Gauss-Kruger zone 45 133.5,45.85,134.77,48.4 1043 +proj=tmerc +lat_0=0 +lon_0=135 +k=1 +x_0=45500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +4534 projected CGCS2000 / 3-degree Gauss-Kruger CM 75E 73.62,35.81,76.5,40.65 1043 +proj=tmerc +lat_0=0 +lon_0=75 +k=1 +x_0=500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +4535 projected CGCS2000 / 3-degree Gauss-Kruger CM 78E 76.5,31.03,79.5,41.83 1043 +proj=tmerc +lat_0=0 +lon_0=78 +k=1 +x_0=500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +4536 projected CGCS2000 / 3-degree Gauss-Kruger CM 81E 79.5,29.95,82.51,45.88 1043 +proj=tmerc +lat_0=0 +lon_0=81 +k=1 +x_0=500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +4537 projected CGCS2000 / 3-degree Gauss-Kruger CM 84E 82.5,28.26,85.5,47.23 1043 +proj=tmerc +lat_0=0 +lon_0=84 +k=1 +x_0=500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +4538 projected CGCS2000 / 3-degree Gauss-Kruger CM 87E 85.5,27.8,88.5,49.18 1043 +proj=tmerc +lat_0=0 +lon_0=87 +k=1 +x_0=500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +4539 projected CGCS2000 / 3-degree Gauss-Kruger CM 90E 88.49,27.32,91.51,48.42 1043 +proj=tmerc +lat_0=0 +lon_0=90 +k=1 +x_0=500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +4540 projected CGCS2000 / 3-degree Gauss-Kruger CM 93E 91.5,27.71,94.5,45.13 1043 +proj=tmerc +lat_0=0 +lon_0=93 +k=1 +x_0=500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +4541 projected CGCS2000 / 3-degree Gauss-Kruger CM 96E 94.5,28.23,97.51,44.5 1043 +proj=tmerc +lat_0=0 +lon_0=96 +k=1 +x_0=500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +4542 projected CGCS2000 / 3-degree Gauss-Kruger CM 99E 97.5,21.43,100.5,42.76 1043 +proj=tmerc +lat_0=0 +lon_0=99 +k=1 +x_0=500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +4543 projected CGCS2000 / 3-degree Gauss-Kruger CM 102E 100.5,21.13,103.5,42.69 1043 +proj=tmerc +lat_0=0 +lon_0=102 +k=1 +x_0=500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +4544 projected CGCS2000 / 3-degree Gauss-Kruger CM 105E 103.5,22.5,106.51,42.21 1043 +proj=tmerc +lat_0=0 +lon_0=105 +k=1 +x_0=500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +4545 projected CGCS2000 / 3-degree Gauss-Kruger CM 108E 106.5,18.19,109.51,42.47 1043 +proj=tmerc +lat_0=0 +lon_0=108 +k=1 +x_0=500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +4546 projected CGCS2000 / 3-degree Gauss-Kruger CM 111E 109.5,18.11,112.5,45.11 1043 +proj=tmerc +lat_0=0 +lon_0=111 +k=1 +x_0=500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +4547 projected CGCS2000 / 3-degree Gauss-Kruger CM 114E 112.5,21.52,115.5,45.45 1043 +proj=tmerc +lat_0=0 +lon_0=114 +k=1 +x_0=500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +4548 projected CGCS2000 / 3-degree Gauss-Kruger CM 117E 115.5,22.6,118.5,49.88 1043 +proj=tmerc +lat_0=0 +lon_0=117 +k=1 +x_0=500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +4549 projected CGCS2000 / 3-degree Gauss-Kruger CM 120E 118.5,24.43,121.5,53.33 1043 +proj=tmerc +lat_0=0 +lon_0=120 +k=1 +x_0=500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +4550 projected CGCS2000 / 3-degree Gauss-Kruger CM 123E 121.5,28.22,124.5,53.56 1043 +proj=tmerc +lat_0=0 +lon_0=123 +k=1 +x_0=500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +4551 projected CGCS2000 / 3-degree Gauss-Kruger CM 126E 124.5,40.19,127.5,53.2 1043 +proj=tmerc +lat_0=0 +lon_0=126 +k=1 +x_0=500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +4552 projected CGCS2000 / 3-degree Gauss-Kruger CM 129E 127.5,41.37,130.5,50.25 1043 +proj=tmerc +lat_0=0 +lon_0=129 +k=1 +x_0=500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +4553 projected CGCS2000 / 3-degree Gauss-Kruger CM 132E 130.5,42.42,133.5,48.88 1043 +proj=tmerc +lat_0=0 +lon_0=132 +k=1 +x_0=500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +4554 projected CGCS2000 / 3-degree Gauss-Kruger CM 135E 133.5,45.85,134.77,48.4 1043 +proj=tmerc +lat_0=0 +lon_0=135 +k=1 +x_0=500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +4555 geographic New Beijing 73.62,18.11,134.77,53.56 1045 +proj=longlat +ellps=krass +no_defs +4558 geographic RRAF 1991 -63.66,14.08,-57.52,18.54 1 1047 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +4559 projected RRAF 1991 / UTM zone 20N -63.66,14.08,-60,18.32 1 1047 +proj=utm +zone=20 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +4568 projected New Beijing / Gauss-Kruger zone 13 73.62,35.42,78.01,41.07 1045 +proj=tmerc +lat_0=0 +lon_0=75 +k=1 +x_0=13500000 +y_0=0 +ellps=krass +units=m +no_defs +4569 projected New Beijing / Gauss-Kruger zone 14 77.98,29.16,84,47.23 1045 +proj=tmerc +lat_0=0 +lon_0=81 +k=1 +x_0=14500000 +y_0=0 +ellps=krass +units=m +no_defs +4570 projected New Beijing / Gauss-Kruger zone 15 84,27.32,90,49.18 1045 +proj=tmerc +lat_0=0 +lon_0=87 +k=1 +x_0=15500000 +y_0=0 +ellps=krass +units=m +no_defs +4571 projected New Beijing / Gauss-Kruger zone 16 90,27.71,96.01,47.9 1045 +proj=tmerc +lat_0=0 +lon_0=93 +k=1 +x_0=16500000 +y_0=0 +ellps=krass +units=m +no_defs +4572 projected New Beijing / Gauss-Kruger zone 17 96,21.13,102.01,43.18 1045 +proj=tmerc +lat_0=0 +lon_0=99 +k=1 +x_0=17500000 +y_0=0 +ellps=krass +units=m +no_defs +4573 projected New Beijing / Gauss-Kruger zone 18 102,21.53,108,42.47 1045 +proj=tmerc +lat_0=0 +lon_0=105 +k=1 +x_0=18500000 +y_0=0 +ellps=krass +units=m +no_defs +4574 projected New Beijing / Gauss-Kruger zone 19 108,18.11,114,45.11 1045 +proj=tmerc +lat_0=0 +lon_0=111 +k=1 +x_0=19500000 +y_0=0 +ellps=krass +units=m +no_defs +4575 projected New Beijing / Gauss-Kruger zone 20 114,22.14,120,51.52 1045 +proj=tmerc +lat_0=0 +lon_0=117 +k=1 +x_0=20500000 +y_0=0 +ellps=krass +units=m +no_defs +4576 projected New Beijing / Gauss-Kruger zone 21 120,26.34,126,53.56 1045 +proj=tmerc +lat_0=0 +lon_0=123 +k=1 +x_0=21500000 +y_0=0 +ellps=krass +units=m +no_defs +4577 projected New Beijing / Gauss-Kruger zone 22 126,40.89,132,52.79 1045 +proj=tmerc +lat_0=0 +lon_0=129 +k=1 +x_0=22500000 +y_0=0 +ellps=krass +units=m +no_defs +4578 projected New Beijing / Gauss-Kruger zone 23 132,45.02,134.77,48.4 1045 +proj=tmerc +lat_0=0 +lon_0=135 +k=1 +x_0=23500000 +y_0=0 +ellps=krass +units=m +no_defs +4579 projected New Beijing / Gauss-Kruger CM 75E 73.62,35.42,78.01,41.07 1045 +proj=tmerc +lat_0=0 +lon_0=75 +k=1 +x_0=500000 +y_0=0 +ellps=krass +units=m +no_defs +4580 projected New Beijing / Gauss-Kruger CM 81E 77.98,29.16,84,47.23 1045 +proj=tmerc +lat_0=0 +lon_0=81 +k=1 +x_0=500000 +y_0=0 +ellps=krass +units=m +no_defs +4581 projected New Beijing / Gauss-Kruger CM 87E 84,27.32,90,49.18 1045 +proj=tmerc +lat_0=0 +lon_0=87 +k=1 +x_0=500000 +y_0=0 +ellps=krass +units=m +no_defs +4582 projected New Beijing / Gauss-Kruger CM 93E 90,27.71,96.01,47.9 1045 +proj=tmerc +lat_0=0 +lon_0=93 +k=1 +x_0=500000 +y_0=0 +ellps=krass +units=m +no_defs +4583 projected New Beijing / Gauss-Kruger CM 99E 96,21.13,102.01,43.18 1045 +proj=tmerc +lat_0=0 +lon_0=99 +k=1 +x_0=500000 +y_0=0 +ellps=krass +units=m +no_defs +4584 projected New Beijing / Gauss-Kruger CM 105E 102,21.53,108,42.47 1045 +proj=tmerc +lat_0=0 +lon_0=105 +k=1 +x_0=500000 +y_0=0 +ellps=krass +units=m +no_defs +4585 projected New Beijing / Gauss-Kruger CM 111E 108,18.11,114,45.11 1045 +proj=tmerc +lat_0=0 +lon_0=111 +k=1 +x_0=500000 +y_0=0 +ellps=krass +units=m +no_defs +4586 projected New Beijing / Gauss-Kruger CM 117E 114,22.14,120,51.52 1045 +proj=tmerc +lat_0=0 +lon_0=117 +k=1 +x_0=500000 +y_0=0 +ellps=krass +units=m +no_defs +4587 projected New Beijing / Gauss-Kruger CM 123E 120,26.34,126,53.56 1045 +proj=tmerc +lat_0=0 +lon_0=123 +k=1 +x_0=500000 +y_0=0 +ellps=krass +units=m +no_defs +4588 projected New Beijing / Gauss-Kruger CM 129E 126,40.89,132,52.79 1045 +proj=tmerc +lat_0=0 +lon_0=129 +k=1 +x_0=500000 +y_0=0 +ellps=krass +units=m +no_defs +4589 projected New Beijing / Gauss-Kruger CM 135E 132,45.02,134.77,48.4 1045 +proj=tmerc +lat_0=0 +lon_0=135 +k=1 +x_0=500000 +y_0=0 +ellps=krass +units=m +no_defs +4600 geographic Anguilla 1957 -63.22,18.11,-62.92,18.33 6600 +proj=longlat +a=6378249.145 +rf=293.465 +no_defs +4601 geographic Antigua 1943 -61.95,16.94,-61.61,17.22 10 6601 +proj=longlat +a=6378249.145 +rf=293.465 +towgs84=-255,-15,71 +no_defs +4602 geographic Dominica 1945 -61.55,15.14,-61.2,15.69 10 6602 +proj=longlat +a=6378249.145 +rf=293.465 +towgs84=725,685,536 +no_defs +4603 geographic Grenada 1953 -61.84,11.94,-61.35,12.57 10 6603 +proj=longlat +a=6378249.145 +rf=293.465 +towgs84=72,213.7,93 +no_defs +4604 geographic Montserrat 1958 -62.29,16.62,-62.08,16.87 44 6604 +proj=longlat +a=6378249.145 +rf=293.465 +towgs84=174,359,365 +no_defs +4605 geographic St. Kitts 1955 -62.92,17.06,-62.5,17.46 10 6605 +proj=longlat +a=6378249.145 +rf=293.465 +towgs84=9,183,236 +no_defs +4606 geographic St. Lucia 1955 -61.13,13.66,-60.82,14.16 2 6606 +proj=longlat +a=6378249.145 +rf=293.465 +towgs84=-153,153,307 +no_defs +4607 geographic St. Vincent 1945 -61.52,12.54,-61.07,13.44 1 6607 +proj=longlat +a=6378249.145 +rf=293.465 +towgs84=195.671,332.517,274.607 +no_defs +4608 geographic NAD27(76) -95.16,41.67,-74.35,56.9 6608 +proj=longlat +ellps=clrk66 +no_defs +4609 geographic NAD27(CGQ77) -79.85,44.99,-57.1,62.62 6609 +proj=longlat +ellps=clrk66 +no_defs +4610 geographic Xian 1980 73.62,18.11,134.77,53.56 6610 +proj=longlat +ellps=IAU76 +no_defs +4611 geographic Hong Kong 1980 113.76,22.13,114.51,22.58 1 6611 +proj=longlat +ellps=intl +towgs84=-162.619,-276.959,-161.764,0.067753,-2.243648,-1.158828,-1.094246 +no_defs +4612 geographic JGD2000 122.38,17.09,157.65,46.05 1 6612 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +4613 geographic Segara 114.55,-4.24,119.06,4.29 5 6613 +proj=longlat +ellps=bessel +towgs84=-387.06,636.53,46.29 +no_defs +4614 geographic QND95 50.69,24.55,51.68,26.2 0 6614 +proj=longlat +ellps=intl +towgs84=-119.4248,-303.65872,-11.00061,1.164298,0.174458,1.096259,3.657065 +no_defs +4615 geographic Porto Santo -17.31,32.35,-16.23,33.15 44 6615 +proj=longlat +ellps=intl +towgs84=-499,-249,314 +no_defs +4616 geographic Selvagem Grande -16.11,29.98,-15.79,30.21 44 6616 +proj=longlat +ellps=intl +towgs84=-289,-124,60 +no_defs +4617 geographic NAD83(CSRS) -141.01,38.21,-40.73,86.46 1 6140 +proj=longlat +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +no_defs +4618 geographic SAD69 -91.72,-55.96,-25.28,12.52 19 6618 +proj=longlat +ellps=aust_SA +towgs84=-57,1,-41 +no_defs +4619 geographic SWEREF99 10.03,54.96,24.17,69.07 1 6619 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +4620 geographic Point 58 -17.19,10.26,30.42,15.7 44 6620 +proj=longlat +a=6378249.145 +rf=293.465 +towgs84=-106,-129,165 +no_defs +4621 geographic Fort Marigot -63.21,17.82,-62.73,18.17 10 6621 +proj=longlat +ellps=intl +towgs84=137,248,-430 +no_defs +4622 geographic Guadeloupe 1948 -61.85,15.8,-60.97,16.55 0.1 6622 +proj=longlat +ellps=intl +towgs84=-472.29,-5.63,-304.12,0.4362,-0.8374,0.2563,1.8984 +no_defs +4623 geographic CSG67 -54.45,3.43,-51.61,5.81 10 6623 +proj=longlat +ellps=intl +towgs84=-186,230,110 +no_defs +4624 geographic RGFG95 -54.61,2.11,-49.45,8.88 1 6624 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +4625 geographic Martinique 1938 -61.29,14.35,-60.76,14.93 0.1 6625 +proj=longlat +ellps=intl +towgs84=126.93,547.94,130.41,-2.7867,5.1612,-0.8584,13.8227 +no_defs +4626 geographic Reunion 1947 55.16,-21.42,55.91,-20.81 44 6626 +proj=longlat +ellps=intl +towgs84=94,-948,-1262 +no_defs +4627 geographic RGR92 51.83,-24.72,58.24,-18.28 1 6627 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +4628 geographic Tahiti 52 -150,-17.93,-149.09,-17.41 10 6628 +proj=longlat +ellps=intl +towgs84=162,117,154 +no_defs +4629 geographic Tahaa 54 -151.91,-16.96,-150.89,-16.17 1 6629 +proj=longlat +ellps=intl +towgs84=72.51,345.411,79.241,-1.5862,-0.8826,-0.5495,1.3653 +no_defs +4630 geographic IGN72 Nuku Hiva -140.31,-9.57,-139.44,-8.72 10 6630 +proj=longlat +ellps=intl +towgs84=84,274,65 +no_defs +4632 geographic Combani 1950 44.98,-13.05,45.35,-12.61 10 6632 +proj=longlat +ellps=intl +towgs84=-382,-59,-262 +no_defs +4633 geographic IGN56 Lifou 166.98,-21.24,167.52,-20.62 1 6633 +proj=longlat +ellps=intl +towgs84=335.47,222.58,-230.94 +no_defs +4636 geographic Petrels 1972 139.44,-66.78,141.5,-66.1 10 6636 +proj=longlat +ellps=intl +towgs84=365,194,166 +no_defs +4637 geographic Perroud 1950 136,-67.13,142,-65.61 10 6637 +proj=longlat +ellps=intl +towgs84=325,154,172 +no_defs +4638 geographic Saint Pierre et Miquelon 1950 -56.48,46.69,-56.07,47.19 1 6638 +proj=longlat +ellps=clrk66 +towgs84=11.363,424.148,373.13 +no_defs +4639 geographic MOP78 -176.25,-13.41,-176.07,-13.16 10 6639 +proj=longlat +ellps=intl +towgs84=253,-132,-127 +no_defs +4641 geographic IGN53 Mare 167.75,-21.71,168.19,-21.32 2 6641 +proj=longlat +ellps=intl +towgs84=287.58,177.78,-135.41 +no_defs +4642 geographic ST84 Ile des Pins 167.36,-22.73,167.61,-22.49 10 6642 +proj=longlat +ellps=intl +towgs84=-13,-348,292 +no_defs +4643 geographic ST71 Belep 163.54,-19.85,163.75,-19.5 1 6643 +proj=longlat +ellps=intl +towgs84=-480.26,-438.32,-643.429,16.3119,20.1721,-4.0349,-111.7002 +no_defs +4644 geographic NEA74 Noumea 166.35,-22.37,166.54,-22.19 1 6644 +proj=longlat +ellps=intl +towgs84=-10.18,-350.43,291.37 +no_defs +4646 geographic Grand Comoros 43.16,-11.99,43.55,-11.31 6646 +proj=longlat +ellps=intl +towgs84=-963,510,-359 +no_defs +4647 projected ETRS89 / UTM zone 32N (zE-N) 6,47.27,12.01,55.47 1 6258 +proj=tmerc +lat_0=0 +lon_0=9 +k=0.9996 +x_0=32500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +4652 projected New Beijing / 3-degree Gauss-Kruger zone 25 73.62,35.81,76.5,40.65 1045 +proj=tmerc +lat_0=0 +lon_0=75 +k=1 +x_0=25500000 +y_0=0 +ellps=krass +units=m +no_defs +4653 projected New Beijing / 3-degree Gauss-Kruger zone 26 76.5,31.03,79.5,41.83 1045 +proj=tmerc +lat_0=0 +lon_0=78 +k=1 +x_0=26500000 +y_0=0 +ellps=krass +units=m +no_defs +4654 projected New Beijing / 3-degree Gauss-Kruger zone 27 79.5,29.95,82.51,45.88 1045 +proj=tmerc +lat_0=0 +lon_0=81 +k=1 +x_0=27500000 +y_0=0 +ellps=krass +units=m +no_defs +4655 projected New Beijing / 3-degree Gauss-Kruger zone 28 82.5,28.26,85.5,47.23 1045 +proj=tmerc +lat_0=0 +lon_0=84 +k=1 +x_0=28500000 +y_0=0 +ellps=krass +units=m +no_defs +4656 projected New Beijing / 3-degree Gauss-Kruger zone 29 85.5,27.8,88.5,49.18 1045 +proj=tmerc +lat_0=0 +lon_0=87 +k=1 +x_0=29500000 +y_0=0 +ellps=krass +units=m +no_defs +4657 geographic Reykjavik 1900 -24.66,63.34,-13.38,66.59 10 6657 +proj=longlat +a=6377019.27 +rf=300 +towgs84=-28,199,5 +no_defs +4658 geographic Hjorsey 1955 -24.66,63.34,-13.38,66.59 7 6658 +proj=longlat +ellps=intl +towgs84=-73,47,-83 +no_defs +4659 geographic ISN93 -30.87,59.96,-5.55,69.59 1 6659 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +4660 geographic Helle 1954 -9.17,70.75,-7.87,71.24 1 6660 +proj=longlat +ellps=intl +towgs84=982.6087,552.753,-540.873,6.68162662527694,-31.6114924086422,-19.8481610048168,16.805 +no_defs +4661 geographic LKS-92 19.06,55.67,28.24,58.09 1 6661 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +4662 geographic IGN72 Grande Terre 163.92,-22.45,167.09,-20.03 2 6634 +proj=longlat +ellps=intl +towgs84=-11.64,-348.6,291.98 +no_defs +4663 geographic Porto Santo 1995 -17.31,32.35,-16.23,33.15 1 6663 +proj=longlat +ellps=intl +towgs84=-210.502,-66.902,-48.476,2.094,-15.067,-5.817,0.485 +no_defs +4664 geographic Azores Oriental 1995 -25.92,36.87,-24.72,37.96 1 6664 +proj=longlat +ellps=intl +towgs84=-208.719,129.685,52.092,-0.195,-0.014,0.327,0.198 +no_defs +4665 geographic Azores Central 1995 -28.9,38.32,-26.97,39.14 1 6665 +proj=longlat +ellps=intl +towgs84=-103.088,162.481,-28.276,0.167,0.082,0.168,-1.504 +no_defs +4666 geographic Lisbon 1890 -9.56,36.95,-6.19,42.16 1 6666 +proj=longlat +ellps=bessel +towgs84=631.392,-66.551,481.442,1.09,-4.445,-4.487,-4.43 +no_defs +4667 geographic IKBD-92 46.36,29.06,48.61,30.32 1 6667 +proj=longlat +ellps=WGS84 +towgs84=0,0,0 +no_defs +4668 geographic ED79 -10.56,34.88,38.01,84.73 6 6668 +proj=longlat +ellps=intl +towgs84=-86,-98,-119 +no_defs +4669 geographic LKS94 19.02,53.89,26.82,56.45 1 6126 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +4670 geographic IGM95 5.93,34.76,18.99,47.1 1 6670 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +4671 geographic Voirol 1879 -2.95,31.99,9.09,37.14 6671 +proj=longlat +ellps=clrk80ign +no_defs +4672 geographic Chatham Islands 1971 -177.25,-44.64,-175.54,-43.3 26 6672 +proj=longlat +ellps=intl +towgs84=175,-38,113 +no_defs +4673 geographic Chatham Islands 1979 -177.25,-44.64,-175.54,-43.3 2 6673 +proj=longlat +ellps=intl +towgs84=174.05,-25.49,112.57,0,0,0.554,0.2263 +no_defs +4674 geographic SIRGAS 2000 -122.19,-59.87,-25.28,32.72 1 6674 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +4675 geographic Guam 1963 144.58,13.18,146.12,20.61 6 6675 +proj=longlat +ellps=clrk66 +towgs84=-100,-248,259 +no_defs +4676 geographic Vientiane 1982 100.09,13.92,107.64,22.5 6 6676 +proj=longlat +ellps=krass +towgs84=42.358,-124.688,-37.366 +no_defs +4677 geographic Lao 1993 100.09,13.92,107.64,22.5 6 6677 +proj=longlat +ellps=krass +towgs84=43.933,-129.593,-39.331 +no_defs +4678 geographic Lao 1997 100.09,13.92,107.64,22.5 5 6678 +proj=longlat +ellps=krass +towgs84=44.585,-131.212,-39.544 +no_defs +4679 geographic Jouik 1961 -17.08,19.37,-15.88,21.34 1 6679 +proj=longlat +a=6378249.145 +rf=293.465 +towgs84=-80.01,253.26,291.19 +no_defs +4680 geographic Nouakchott 1965 -16.57,16.81,-15.59,19.41 5 6680 +proj=longlat +a=6378249.145 +rf=293.465 +towgs84=124.5,-63.5,-281 +no_defs +4682 geographic Gulshan 303 88.01,18.56,92.67,26.64 1 6682 +proj=longlat +ellps=evrst30 +towgs84=283.729,735.942,261.143 +no_defs +4683 geographic PRS92 116.04,3,129.95,22.18 0.05 6683 +proj=longlat +ellps=clrk66 +towgs84=-127.62,-67.24,-47.04,-3.068,4.903,1.578,-1.06 +no_defs +4684 geographic Gan 1970 72.81,-0.69,73.69,7.08 44 6684 +proj=longlat +ellps=intl +towgs84=-133,-321,50 +no_defs +4686 geographic MAGNA-SIRGAS -84.77,-4.23,-66.87,15.51 1 6686 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +4687 geographic RGPF -158.13,-31.24,-131.97,-4.52 0.5 6687 +proj=longlat +ellps=GRS80 +towgs84=0.072,-0.507,-0.245,0.0183,-0.0003,0.007,-0.0093 +no_defs +4688 geographic Fatu Iva 72 -138.75,-10.6,-138.54,-10.36 2 6688 +proj=longlat +ellps=intl +towgs84=347.175,1077.618,2623.677,33.9058,-70.6776,9.4013,186.0647 +no_defs +4689 geographic IGN63 Hiva Oa -139.23,-10.08,-138.75,-9.64 2 6689 +proj=longlat +ellps=intl +towgs84=410.793,54.542,80.501,-2.5596,-2.3517,-0.6594,17.3218 +no_defs +4690 geographic Tahiti 79 -149.7,-17.93,-149.09,-17.44 1 6690 +proj=longlat +ellps=intl +towgs84=221.597,152.441,176.523,2.403,1.3893,0.884,11.4648 +no_defs +4691 geographic Moorea 87 -150,-17.63,-149.73,-17.41 1 6691 +proj=longlat +ellps=intl +towgs84=218.769,150.75,176.75,3.5231,2.0037,1.288,10.9817 +no_defs +4692 geographic Maupiti 83 -152.39,-16.57,-152.14,-16.34 1 6692 +proj=longlat +ellps=intl +towgs84=217.109,86.452,23.711,0.0183,-0.0003,0.007,-0.0093 +no_defs +4693 geographic Nakhl-e Ghanem 51.8,27.3,53.01,28.2 0.2 6693 +proj=longlat +ellps=WGS84 +towgs84=0,-0.15,0.68 +no_defs +4694 geographic POSGAR 94 -73.59,-58.41,-52.63,-21.78 1 6694 +proj=longlat +ellps=WGS84 +towgs84=0,0,0 +no_defs +4695 geographic Katanga 1955 21.74,-13.46,30.78,-4.99 1.5 6695 +proj=longlat +ellps=clrk66 +towgs84=-103.746,-9.614,-255.95 +no_defs +4696 geographic Kasai 1953 21.5,-7.31,26.26,-5.01 6696 +proj=longlat +a=6378249.145 +rf=293.465 +no_defs +4697 geographic IGC 1962 6th Parallel South 12.17,-7.36,29.64,-3.29 6697 +proj=longlat +a=6378249.145 +rf=293.465 +no_defs +4698 geographic IGN 1962 Kerguelen 68.69,-49.78,70.62,-48.6 10 6698 +proj=longlat +ellps=intl +towgs84=145,-187,103 +no_defs +4699 geographic Le Pouce 1934 57.25,-20.57,57.85,-19.94 2 6699 +proj=longlat +a=6378249.145 +rf=293.465 +towgs84=-770.1,158.4,-498.2 +no_defs +4700 geographic IGN Astro 1960 -17.08,14.72,-4.8,27.3 6700 +proj=longlat +a=6378249.145 +rf=293.465 +no_defs +4701 geographic IGCB 1955 12.17,-6.04,16.28,-4.28 5 6701 +proj=longlat +a=6378249.145 +rf=293.465 +towgs84=-79.9,-158,-168.9 +no_defs +4702 geographic Mauritania 1999 -20.04,14.72,-4.8,27.3 1 6702 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +4703 geographic Mhast 1951 10.53,-6.04,13.1,-4.38 6703 +proj=longlat +a=6378249.145 +rf=293.465 +no_defs +4704 geographic Mhast (onshore) 10.53,-6.04,13.1,-4.38 6704 +proj=longlat +ellps=intl +no_defs +4705 geographic Mhast (offshore) 10.53,-6.04,12.37,-5.05 6705 +proj=longlat +ellps=intl +no_defs +4706 geographic Egypt Gulf of Suez S-650 TL 32.34,27.19,34.27,30.01 5 6706 +proj=longlat +ellps=helmert +towgs84=-146.21,112.63,4.05 +no_defs +4707 geographic Tern Island 1961 -166.36,23.69,-166.03,23.93 44 6707 +proj=longlat +ellps=intl +towgs84=114,-116,-333 +no_defs +4708 geographic Cocos Islands 1965 96.76,-12.27,96.99,-11.76 44 6708 +proj=longlat +ellps=aust_SA +towgs84=-491,-22,435 +no_defs +4709 geographic Iwo Jima 1945 141.2,24.67,141.42,24.89 44 6709 +proj=longlat +ellps=intl +towgs84=145,75,-272 +no_defs +4710 geographic Astro DOS 71 -5.85,-16.08,-5.59,-15.85 1 6710 +proj=longlat +ellps=intl +towgs84=-323.65,551.39,-491.22 +no_defs +4711 geographic Marcus Island 1952 153.91,24.22,154.05,24.35 44 6711 +proj=longlat +ellps=intl +towgs84=124,-234,-25 +no_defs +4712 geographic Ascension Island 1958 -14.46,-8.03,-14.24,-7.83 44 6712 +proj=longlat +ellps=intl +towgs84=-205,107,53 +no_defs +4713 geographic Ayabelle Lighthouse 41.75,10.94,44.15,12.72 17 6713 +proj=longlat +a=6378249.145 +rf=293.465 +towgs84=-77,-128,142 +no_defs +4714 geographic Bellevue 168.09,-20.31,169.95,-17.37 35 6714 +proj=longlat +ellps=intl +towgs84=-127,-769,472 +no_defs +4715 geographic Camp Area Astro 165.73,-77.94,167.43,-77.17 6715 +proj=longlat +ellps=intl +towgs84=-104,-129,239 +no_defs +4716 geographic Phoenix Islands 1966 -174.6,-4.76,-170.66,-2.68 26 6716 +proj=longlat +ellps=intl +towgs84=298,-304,-375 +no_defs +4717 geographic Cape Canaveral -82.33,20.86,-72.68,30.83 6 6717 +proj=longlat +ellps=clrk66 +towgs84=-2,151,181 +no_defs +4718 geographic Solomon 1968 155.62,-10.9,162.44,-6.55 44 6718 +proj=longlat +ellps=intl +towgs84=230,-199,-752 +no_defs +4719 geographic Easter Island 1967 -109.51,-27.25,-109.16,-27.01 44 6719 +proj=longlat +ellps=intl +towgs84=211,147,111 +no_defs +4720 geographic Fiji 1986 176.81,-20.81,-178.15,-12.42 2 6720 +proj=longlat +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.2263 +no_defs +4721 geographic Fiji 1956 176.81,-19.22,-179.77,-16.1 7 6721 +proj=longlat +ellps=intl +towgs84=265.025,384.929,-194.046 +no_defs +4722 geographic South Georgia 1968 -38.08,-54.95,-35.74,-53.93 44 6722 +proj=longlat +ellps=intl +towgs84=-794,119,-298 +no_defs +4723 geographic GCGD59 -81.46,19.21,-81.04,19.41 1 6723 +proj=longlat +ellps=clrk66 +towgs84=-179.483,-69.379,-27.584,-7.862,8.163,6.042,-13.925 +no_defs +4724 geographic Diego Garcia 1969 72.3,-7.49,72.55,-7.18 44 6724 +proj=longlat +ellps=intl +towgs84=208,-435,-229 +no_defs +4725 geographic Johnston Island 1961 -169.59,16.67,-169.47,16.79 44 6725 +proj=longlat +ellps=intl +towgs84=189,-79,-202 +no_defs +4726 geographic SIGD61 -80.14,19.63,-79.69,19.78 1 6726 +proj=longlat +ellps=clrk66 +towgs84=8.853,-52.644,180.304,-0.393,-2.323,2.96,-24.081 +no_defs +4727 geographic Midway 1961 -177.45,28.13,-177.31,28.28 44 6727 +proj=longlat +ellps=intl +towgs84=403,-81,277 +no_defs +4728 geographic PN84 -18.22,27.58,-16.08,28.9 44 6728 +proj=longlat +ellps=intl +towgs84=-307,-92,127 +no_defs +4729 geographic Pitcairn 1967 -130.16,-25.14,-130.01,-25 44 6729 +proj=longlat +ellps=intl +towgs84=185,165,42 +no_defs +4730 geographic Santo 1965 166.47,-17.32,168.71,-14.57 44 6730 +proj=longlat +ellps=intl +towgs84=170,42,84 +no_defs +4732 geographic Marshall Islands 1960 162.27,8.66,167.82,19.38 6 6732 +proj=longlat +ellps=hough +towgs84=102,52,-38 +no_defs +4733 geographic Wake Island 1952 166.55,19.22,166.72,19.38 44 6733 +proj=longlat +ellps=intl +towgs84=276,-57,149 +no_defs +4734 geographic Tristan 1968 -12.76,-40.42,-9.8,-37 44 6734 +proj=longlat +ellps=intl +towgs84=-632,438,-609 +no_defs +4735 geographic Kusaie 1951 162.85,5.21,163.1,5.43 44 6735 +proj=longlat +ellps=intl +towgs84=647,1777,-1124 +no_defs +4736 geographic Deception Island -60.89,-63.08,-60.35,-62.82 35 6736 +proj=longlat +a=6378249.145 +rf=293.465 +towgs84=260,12,-147 +no_defs +4737 geographic KGD2002 122.71,28.6,134.28,40.27 1 6737 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +4738 geographic Hong Kong 1963 113.76,22.13,114.51,22.58 6738 +proj=longlat +a=6378293.64520876 +b=6356617.98767984 +no_defs +4739 geographic Hong Kong 1963(67) 113.76,22.13,114.51,22.58 1 6739 +proj=longlat +ellps=intl +towgs84=-156,-271,-189 +no_defs +4740 geographic PZ-90 -180,-90,180,90 1.5 6740 +proj=longlat +a=6378136 +rf=298.257839303 +towgs84=0,0,1.5,0,0,0.076,0 +no_defs +4741 geographic FD54 -7.49,61.33,-6.33,62.41 10 6741 +proj=longlat +ellps=intl +towgs84=-240.33,-267.41,-34.61 +no_defs +4742 geographic GDM2000 98.02,0.85,119.61,7.81 1 6742 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +4743 geographic Karbala 1979 38.79,29.06,48.61,37.39 1 6743 +proj=longlat +a=6378249.145 +rf=293.465 +towgs84=70.995,-335.916,262.898 +no_defs +4744 geographic Nahrwan 1934 38.79,29.06,51.06,37.39 30 6744 +proj=longlat +a=6378249.145 +rf=293.465 +towgs84=-242.2,-144.9,370.3 +no_defs +4745 geographic RD/83 11.89,50.2,15.04,51.66 6745 +proj=longlat +ellps=bessel +no_defs +4746 geographic PD/83 9.87,50.2,12.66,51.65 6746 +proj=longlat +ellps=bessel +no_defs +4747 geographic GR96 -75,56.38,7.99,87.02 1 1421 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +4748 geographic Vanua Levu 1915 178.42,-17.07,-179.77,-16.1 50 6748 +proj=longlat +a=6378306.3696 +b=6356571.996 +towgs84=51,391,-36 +no_defs +4749 geographic RGNC91-93 156.25,-26.45,174.28,-14.83 1 6749 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +4750 geographic ST87 Ouvea 166.44,-20.77,166.71,-20.34 1 6750 +proj=longlat +ellps=WGS84 +towgs84=-56.263,16.136,-22.856 +no_defs +4751 geographic Kertau (RSO) 99.59,1.13,104.6,6.72 15 6751 +proj=longlat +ellps=evrst69 +towgs84=-11,851,5 +no_defs +4752 geographic Viti Levu 1912 177.19,-18.32,178.75,-17.25 5 6752 +proj=longlat +a=6378306.3696 +b=6356571.996 +towgs84=98,390,-22 +no_defs +4753 geographic fk89 -7.49,61.33,-6.33,62.41 6753 +proj=longlat +ellps=intl +no_defs +4754 geographic LGD2006 9.31,19.5,26.21,35.23 0.1 6754 +proj=longlat +ellps=intl +towgs84=-208.4058,-109.8777,-2.5764 +no_defs +4755 geographic DGN95 92.01,-13.95,141.46,7.79 1 6755 +proj=longlat +ellps=WGS84 +towgs84=0,0,0 +no_defs +4756 geographic VN-2000 102.14,8.33,109.53,23.4 1 6756 +proj=longlat +ellps=WGS84 +towgs84=-191.90441429,-39.30318279,-111.45032835,0.00928836,-0.01975479,0.00427372,0.252906278 +no_defs +4757 geographic SVY21 103.59,1.13,104.07,1.47 1 6757 +proj=longlat +ellps=WGS84 +towgs84=0,0,0 +no_defs +4758 geographic JAD2001 -80.6,14.08,-74.51,19.36 0 6758 +proj=longlat +ellps=WGS84 +towgs84=0,0,0 +no_defs +4759 geographic NAD83(NSRS2007) 167.65,14.92,-63.88,74.71 2 6759 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +4760 geographic WGS 66 -180,-90,180,90 6760 +proj=longlat +ellps=NWL9D +no_defs +4761 geographic HTRS96 13,41.62,19.43,46.54 1 6761 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +4762 geographic BDA2000 -68.83,28.91,-60.7,35.73 1 6762 +proj=longlat +ellps=WGS84 +towgs84=0,0,0 +no_defs +4763 geographic Pitcairn 2006 -130.16,-25.14,-130.01,-25 1 6763 +proj=longlat +ellps=WGS84 +towgs84=0,0,0 +no_defs +4764 geographic RSRGD2000 144.99,-90,-144.99,-59.99 1 6764 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +4765 geographic Slovenia 1996 13.38,45.42,16.61,46.88 1 6765 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +4766 projected New Beijing / 3-degree Gauss-Kruger zone 30 88.49,27.32,91.51,48.42 1045 +proj=tmerc +lat_0=0 +lon_0=90 +k=1 +x_0=30500000 +y_0=0 +ellps=krass +units=m +no_defs +4767 projected New Beijing / 3-degree Gauss-Kruger zone 31 91.5,27.71,94.5,45.13 1045 +proj=tmerc +lat_0=0 +lon_0=93 +k=1 +x_0=31500000 +y_0=0 +ellps=krass +units=m +no_defs +4768 projected New Beijing / 3-degree Gauss-Kruger zone 32 94.5,28.23,97.51,44.5 1045 +proj=tmerc +lat_0=0 +lon_0=96 +k=1 +x_0=32500000 +y_0=0 +ellps=krass +units=m +no_defs +4769 projected New Beijing / 3-degree Gauss-Kruger zone 33 97.5,21.43,100.5,42.76 1045 +proj=tmerc +lat_0=0 +lon_0=99 +k=1 +x_0=33500000 +y_0=0 +ellps=krass +units=m +no_defs +4770 projected New Beijing / 3-degree Gauss-Kruger zone 34 100.5,21.13,103.5,42.69 1045 +proj=tmerc +lat_0=0 +lon_0=102 +k=1 +x_0=34500000 +y_0=0 +ellps=krass +units=m +no_defs +4771 projected New Beijing / 3-degree Gauss-Kruger zone 35 103.5,22.5,106.51,42.21 1045 +proj=tmerc +lat_0=0 +lon_0=105 +k=1 +x_0=35500000 +y_0=0 +ellps=krass +units=m +no_defs +4772 projected New Beijing / 3-degree Gauss-Kruger zone 36 106.5,18.19,109.51,42.47 1045 +proj=tmerc +lat_0=0 +lon_0=108 +k=1 +x_0=36500000 +y_0=0 +ellps=krass +units=m +no_defs +4773 projected New Beijing / 3-degree Gauss-Kruger zone 37 109.5,18.11,112.5,45.11 1045 +proj=tmerc +lat_0=0 +lon_0=111 +k=1 +x_0=37500000 +y_0=0 +ellps=krass +units=m +no_defs +4774 projected New Beijing / 3-degree Gauss-Kruger zone 38 112.5,21.52,115.5,45.45 1045 +proj=tmerc +lat_0=0 +lon_0=114 +k=1 +x_0=38500000 +y_0=0 +ellps=krass +units=m +no_defs +4775 projected New Beijing / 3-degree Gauss-Kruger zone 39 115.5,22.6,118.5,49.88 1045 +proj=tmerc +lat_0=0 +lon_0=117 +k=1 +x_0=39500000 +y_0=0 +ellps=krass +units=m +no_defs +4776 projected New Beijing / 3-degree Gauss-Kruger zone 40 118.5,24.43,121.5,53.33 1045 +proj=tmerc +lat_0=0 +lon_0=120 +k=1 +x_0=40500000 +y_0=0 +ellps=krass +units=m +no_defs +4777 projected New Beijing / 3-degree Gauss-Kruger zone 41 121.5,28.22,124.5,53.56 1045 +proj=tmerc +lat_0=0 +lon_0=123 +k=1 +x_0=41500000 +y_0=0 +ellps=krass +units=m +no_defs +4778 projected New Beijing / 3-degree Gauss-Kruger zone 42 124.5,40.19,127.5,53.2 1045 +proj=tmerc +lat_0=0 +lon_0=126 +k=1 +x_0=42500000 +y_0=0 +ellps=krass +units=m +no_defs +4779 projected New Beijing / 3-degree Gauss-Kruger zone 43 127.5,41.37,130.5,50.25 1045 +proj=tmerc +lat_0=0 +lon_0=129 +k=1 +x_0=43500000 +y_0=0 +ellps=krass +units=m +no_defs +4780 projected New Beijing / 3-degree Gauss-Kruger zone 44 130.5,42.42,133.5,48.88 1045 +proj=tmerc +lat_0=0 +lon_0=132 +k=1 +x_0=44500000 +y_0=0 +ellps=krass +units=m +no_defs +4781 projected New Beijing / 3-degree Gauss-Kruger zone 45 133.5,45.85,134.77,48.4 1045 +proj=tmerc +lat_0=0 +lon_0=135 +k=1 +x_0=45500000 +y_0=0 +ellps=krass +units=m +no_defs +4782 projected New Beijing / 3-degree Gauss-Kruger CM 75E 73.62,35.81,76.5,40.65 1045 +proj=tmerc +lat_0=0 +lon_0=75 +k=1 +x_0=500000 +y_0=0 +ellps=krass +units=m +no_defs +4783 projected New Beijing / 3-degree Gauss-Kruger CM 78E 76.5,31.03,79.5,41.83 1045 +proj=tmerc +lat_0=0 +lon_0=78 +k=1 +x_0=500000 +y_0=0 +ellps=krass +units=m +no_defs +4784 projected New Beijing / 3-degree Gauss-Kruger CM 81E 79.5,29.95,82.51,45.88 1045 +proj=tmerc +lat_0=0 +lon_0=81 +k=1 +x_0=500000 +y_0=0 +ellps=krass +units=m +no_defs +4785 projected New Beijing / 3-degree Gauss-Kruger CM 84E 82.5,28.26,85.5,47.23 1045 +proj=tmerc +lat_0=0 +lon_0=84 +k=1 +x_0=500000 +y_0=0 +ellps=krass +units=m +no_defs +4786 projected New Beijing / 3-degree Gauss-Kruger CM 87E 85.5,27.8,88.5,49.18 1045 +proj=tmerc +lat_0=0 +lon_0=87 +k=1 +x_0=500000 +y_0=0 +ellps=krass +units=m +no_defs +4787 projected New Beijing / 3-degree Gauss-Kruger CM 90E 88.49,27.32,91.51,48.42 1045 +proj=tmerc +lat_0=0 +lon_0=90 +k=1 +x_0=500000 +y_0=0 +ellps=krass +units=m +no_defs +4788 projected New Beijing / 3-degree Gauss-Kruger CM 93E 91.5,27.71,94.5,45.13 1045 +proj=tmerc +lat_0=0 +lon_0=93 +k=1 +x_0=500000 +y_0=0 +ellps=krass +units=m +no_defs +4789 projected New Beijing / 3-degree Gauss-Kruger CM 96E 94.5,28.23,97.51,44.5 1045 +proj=tmerc +lat_0=0 +lon_0=96 +k=1 +x_0=500000 +y_0=0 +ellps=krass +units=m +no_defs +4790 projected New Beijing / 3-degree Gauss-Kruger CM 99E 97.5,21.43,100.5,42.76 1045 +proj=tmerc +lat_0=0 +lon_0=99 +k=1 +x_0=500000 +y_0=0 +ellps=krass +units=m +no_defs +4791 projected New Beijing / 3-degree Gauss-Kruger CM 102E 100.5,21.13,103.5,42.69 1045 +proj=tmerc +lat_0=0 +lon_0=102 +k=1 +x_0=500000 +y_0=0 +ellps=krass +units=m +no_defs +4792 projected New Beijing / 3-degree Gauss-Kruger CM 105E 103.5,22.5,106.51,42.21 1045 +proj=tmerc +lat_0=0 +lon_0=105 +k=1 +x_0=500000 +y_0=0 +ellps=krass +units=m +no_defs +4793 projected New Beijing / 3-degree Gauss-Kruger CM 108E 106.5,18.19,109.51,42.47 1045 +proj=tmerc +lat_0=0 +lon_0=108 +k=1 +x_0=500000 +y_0=0 +ellps=krass +units=m +no_defs +4794 projected New Beijing / 3-degree Gauss-Kruger CM 111E 109.5,18.11,112.5,45.11 1045 +proj=tmerc +lat_0=0 +lon_0=111 +k=1 +x_0=500000 +y_0=0 +ellps=krass +units=m +no_defs +4795 projected New Beijing / 3-degree Gauss-Kruger CM 114E 112.5,21.52,115.5,45.45 1045 +proj=tmerc +lat_0=0 +lon_0=114 +k=1 +x_0=500000 +y_0=0 +ellps=krass +units=m +no_defs +4796 projected New Beijing / 3-degree Gauss-Kruger CM 117E 115.5,22.6,118.5,49.88 1045 +proj=tmerc +lat_0=0 +lon_0=117 +k=1 +x_0=500000 +y_0=0 +ellps=krass +units=m +no_defs +4797 projected New Beijing / 3-degree Gauss-Kruger CM 120E 118.5,24.43,121.5,53.33 1045 +proj=tmerc +lat_0=0 +lon_0=120 +k=1 +x_0=500000 +y_0=0 +ellps=krass +units=m +no_defs +4798 projected New Beijing / 3-degree Gauss-Kruger CM 123E 121.5,28.22,124.5,53.56 1045 +proj=tmerc +lat_0=0 +lon_0=123 +k=1 +x_0=500000 +y_0=0 +ellps=krass +units=m +no_defs +4799 projected New Beijing / 3-degree Gauss-Kruger CM 126E 124.5,40.19,127.5,53.2 1045 +proj=tmerc +lat_0=0 +lon_0=126 +k=1 +x_0=500000 +y_0=0 +ellps=krass +units=m +no_defs +4800 projected New Beijing / 3-degree Gauss-Kruger CM 129E 127.5,41.37,130.5,50.25 1045 +proj=tmerc +lat_0=0 +lon_0=129 +k=1 +x_0=500000 +y_0=0 +ellps=krass +units=m +no_defs +4801 geographic CH1903 (Bern) 5.95,45.81,10.5,47.81 6801 +proj=longlat +ellps=bessel +pm=7.43958333333333 +no_defs +4802 geographic Bogota 1975 (Bogota) -79.1,-4.23,-66.87,12.52 6802 +proj=longlat +ellps=intl +pm=-74.0809166666667 +no_defs +4803 geographic Lisbon (Lisbon) -9.56,36.95,-6.19,42.16 6803 +proj=longlat +ellps=intl +pm=-9.13190611111111 +no_defs +4804 geographic Makassar (Jakarta) 118.71,-6.54,120.78,-1.88 6804 +proj=longlat +ellps=bessel +pm=106.807719444444 +no_defs +4805 geographic MGI (Ferro) 9.53,40.85,23.04,49.02 6805 +proj=longlat +ellps=bessel +pm=-17.6666666666667 +no_defs +4806 geographic Monte Mario (Rome) 5.93,34.76,18.99,47.1 6806 +proj=longlat +ellps=intl +pm=12.4523333333333 +no_defs +4807 geographic NTF (Paris) -4.87,41.31,9.63,51.14 6807 +proj=longlat +ellps=clrk80ign +pm=2.33722917 +no_defs +4809 geographic BD50 (Brussels) 2.5,49.5,6.4,51.51 6809 +proj=longlat +ellps=intl +pm=4.367975 +no_defs +4810 geographic Tananarive (Paris) 43.18,-25.64,50.56,-11.89 6810 +proj=longlat +ellps=intl +pm=2.33722917 +no_defs +4811 geographic Voirol 1875 (Paris) -2.95,31.99,9.09,37.14 6811 +proj=longlat +ellps=clrk80ign +pm=2.33722917 +no_defs +4812 projected New Beijing / 3-degree Gauss-Kruger CM 132E 130.5,42.42,133.5,48.88 1045 +proj=tmerc +lat_0=0 +lon_0=132 +k=1 +x_0=500000 +y_0=0 +ellps=krass +units=m +no_defs +4813 geographic Batavia (Jakarta) 95.16,-8.91,115.77,5.97 6813 +proj=longlat +ellps=bessel +pm=106.807719444444 +no_defs +4814 geographic RT38 (Stockholm) 10.93,55.28,24.17,69.07 6814 +proj=longlat +ellps=bessel +pm=18.0582777777778 +no_defs +4815 geographic Greek (Athens) 19.57,34.88,28.3,41.75 6815 +proj=longlat +ellps=bessel +pm=23.7163375 +no_defs +4816 geographic Carthage (Paris) 7.49,30.23,11.59,37.4 6816 +proj=longlat +ellps=clrk80ign +pm=2.33722917 +no_defs +4817 geographic NGO 1948 (Oslo) 4.39,57.9,31.32,71.24 6817 +proj=longlat +a=6377492.018 +rf=299.1528128 +pm=10.7229166666667 +no_defs +4818 geographic S-JTSK (Ferro) 12.09,47.73,22.56,51.06 6818 +proj=longlat +ellps=bessel +pm=-17.6666666666667 +no_defs +4820 geographic Segara (Jakarta) 114.55,-4.24,119.06,4.29 6820 +proj=longlat +ellps=bessel +pm=106.807719444444 +no_defs +4821 geographic Voirol 1879 (Paris) -2.95,31.99,9.09,37.14 6821 +proj=longlat +ellps=clrk80ign +pm=2.33722917 +no_defs +4822 projected New Beijing / 3-degree Gauss-Kruger CM 135E 133.5,45.85,134.77,48.4 1045 +proj=tmerc +lat_0=0 +lon_0=135 +k=1 +x_0=500000 +y_0=0 +ellps=krass +units=m +no_defs +4823 geographic Sao Tome 6.41,-0.04,6.82,0.46 1044 +proj=longlat +ellps=intl +no_defs +4824 geographic Principe 7.27,1.48,7.52,1.76 1046 +proj=longlat +ellps=intl +no_defs +4826 projected WGS 84 / Cape Verde National -28.85,11.47,-19.53,20.54 6326 +proj=lcc +lat_0=15.8333333333333 +lon_0=-24 +lat_1=15 +lat_2=16.6666666666667 +x_0=161587.83 +y_0=128511.202 +datum=WGS84 +units=m +no_defs +4839 projected ETRS89 / LCC Germany (N-E) 5.86,47.27,15.04,55.09 1 6258 +proj=lcc +lat_0=51 +lon_0=10.5 +lat_1=48.6666666666667 +lat_2=53.6666666666667 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +4901 geographic ATF (Paris) -4.87,42.33,8.23,51.14 6901 +proj=longlat +a=6376523 +rf=308.64 +pm=2.33720833333333 +no_defs +4903 geographic Madrid 1870 (Madrid) -9.37,35.95,3.39,43.82 6903 +proj=longlat +a=6378298.3 +rf=294.73 +pm=-3.687375 +no_defs +4904 geographic Lisbon 1890 (Lisbon) -9.56,36.95,-6.19,42.16 6904 +proj=longlat +ellps=bessel +pm=-9.13190611111111 +no_defs +5013 geographic PTRA08 -35.58,29.24,-12.48,43.07 1 1041 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +5014 projected PTRA08 / UTM zone 25N -35.58,35.25,-30,43.07 1 1041 +proj=utm +zone=25 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5015 projected PTRA08 / UTM zone 26N -30,33.52,-24,42.96 1 1041 +proj=utm +zone=26 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5016 projected PTRA08 / UTM zone 28N -18,29.24,-12.48,36.46 1 1041 +proj=utm +zone=28 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5018 projected Lisbon / Portuguese Grid New -9.56,36.95,-6.19,42.16 2 6207 +proj=tmerc +lat_0=39.6666666666667 +lon_0=-8.13190611111111 +k=1 +x_0=0 +y_0=0 +ellps=intl +towgs84=-288.885,-91.744,126.244,-1.691,0.41,-0.211,-4.598 +units=m +no_defs +5041 projected WGS 84 / UPS North (E,N) -180,60,180,90 6326 +proj=stere +lat_0=90 +lon_0=0 +k=0.994 +x_0=2000000 +y_0=2000000 +datum=WGS84 +units=m +no_defs +5042 projected WGS 84 / UPS South (E,N) -180,-90,180,-60 6326 +proj=stere +lat_0=-90 +lon_0=0 +k=0.994 +x_0=2000000 +y_0=2000000 +datum=WGS84 +units=m +no_defs +5048 projected EUREF-FIN / TM35FIN(N,E) 19.08,58.84,31.59,70.09 1 1391 +proj=utm +zone=35 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5069 projected NAD27 / Conus Albers -124.79,24.41,-66.91,49.38 10 6267 +proj=aea +lat_0=23 +lon_0=-96 +lat_1=29.5 +lat_2=45.5 +x_0=0 +y_0=0 +ellps=clrk66 +towgs84=-8,160,176 +units=m +no_defs +5070 projected NAD83 / Conus Albers -124.79,24.41,-66.91,49.38 4 6269 +proj=aea +lat_0=23 +lon_0=-96 +lat_1=29.5 +lat_2=45.5 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5071 projected NAD83(HARN) / Conus Albers -124.79,24.41,-66.91,49.38 1 6152 +proj=aea +lat_0=23 +lon_0=-96 +lat_1=29.5 +lat_2=45.5 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +5072 projected NAD83(NSRS2007) / Conus Albers -124.79,24.41,-66.91,49.38 2 6759 +proj=aea +lat_0=23 +lon_0=-96 +lat_1=29.5 +lat_2=45.5 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5105 projected ETRS89-NOR [EUREF89] / NTM zone 5 4.39,58.32,6.01,62.64 1 1407 +proj=tmerc +lat_0=58 +lon_0=5.5 +k=1 +x_0=100000 +y_0=1000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5106 projected ETRS89-NOR [EUREF89] / NTM zone 6 6,57.93,7.01,63.03 1 1407 +proj=tmerc +lat_0=58 +lon_0=6.5 +k=1 +x_0=100000 +y_0=1000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5107 projected ETRS89-NOR [EUREF89] / NTM zone 7 6.99,57.9,8.01,63.59 1 1407 +proj=tmerc +lat_0=58 +lon_0=7.5 +k=1 +x_0=100000 +y_0=1000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5108 projected ETRS89-NOR [EUREF89] / NTM zone 8 8,58,9.01,64.1 1 1407 +proj=tmerc +lat_0=58 +lon_0=8.5 +k=1 +x_0=100000 +y_0=1000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5109 projected ETRS89-NOR [EUREF89] / NTM zone 9 9,58.44,10.01,64.25 1 1407 +proj=tmerc +lat_0=58 +lon_0=9.5 +k=1 +x_0=100000 +y_0=1000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5110 projected ETRS89-NOR [EUREF89] / NTM zone 10 10,58.9,11.01,65.27 1 1407 +proj=tmerc +lat_0=58 +lon_0=10.5 +k=1 +x_0=100000 +y_0=1000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5111 projected ETRS89-NOR [EUREF89] / NTM zone 11 11,58.88,12.01,67.58 1 1407 +proj=tmerc +lat_0=58 +lon_0=11.5 +k=1 +x_0=100000 +y_0=1000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5112 projected ETRS89-NOR [EUREF89] / NTM zone 12 12,59.88,13.01,68.16 1 1407 +proj=tmerc +lat_0=58 +lon_0=12.5 +k=1 +x_0=100000 +y_0=1000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5113 projected ETRS89-NOR [EUREF89] / NTM zone 13 13,64.01,14.01,68.4 1 1407 +proj=tmerc +lat_0=58 +lon_0=13.5 +k=1 +x_0=100000 +y_0=1000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5114 projected ETRS89-NOR [EUREF89] / NTM zone 14 14,64.03,15.01,69.07 1 1407 +proj=tmerc +lat_0=58 +lon_0=14.5 +k=1 +x_0=100000 +y_0=1000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5115 projected ETRS89-NOR [EUREF89] / NTM zone 15 15,66.14,16.01,69.38 1 1407 +proj=tmerc +lat_0=58 +lon_0=15.5 +k=1 +x_0=100000 +y_0=1000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5116 projected ETRS89-NOR [EUREF89] / NTM zone 16 15.99,66.88,17.01,69.49 1 1407 +proj=tmerc +lat_0=58 +lon_0=16.5 +k=1 +x_0=100000 +y_0=1000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5117 projected ETRS89-NOR [EUREF89] / NTM zone 17 17,67.94,18.01,69.96 1 1407 +proj=tmerc +lat_0=58 +lon_0=17.5 +k=1 +x_0=100000 +y_0=1000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5118 projected ETRS89-NOR [EUREF89] / NTM zone 18 18,68.04,19.01,70.29 1 1407 +proj=tmerc +lat_0=58 +lon_0=18.5 +k=1 +x_0=100000 +y_0=1000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5119 projected ETRS89-NOR [EUREF89] / NTM zone 19 19,68.33,20.01,70.35 1 1407 +proj=tmerc +lat_0=58 +lon_0=19.5 +k=1 +x_0=100000 +y_0=1000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5120 projected ETRS89-NOR [EUREF89] / NTM zone 20 20,68.37,21.01,70.37 1 1407 +proj=tmerc +lat_0=58 +lon_0=20.5 +k=1 +x_0=100000 +y_0=1000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5121 projected ETRS89-NOR [EUREF89] / NTM zone 21 21,69.03,22.01,70.73 1 1407 +proj=tmerc +lat_0=58 +lon_0=21.5 +k=1 +x_0=100000 +y_0=1000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5122 projected ETRS89-NOR [EUREF89] / NTM zone 22 22,68.69,23.01,70.93 1 1407 +proj=tmerc +lat_0=58 +lon_0=22.5 +k=1 +x_0=100000 +y_0=1000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5123 projected ETRS89-NOR [EUREF89] / NTM zone 23 23,68.62,24.01,71.15 1 1407 +proj=tmerc +lat_0=58 +lon_0=23.5 +k=1 +x_0=100000 +y_0=1000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5124 projected ETRS89-NOR [EUREF89] / NTM zone 24 24,68.58,25.01,71.17 1 1407 +proj=tmerc +lat_0=58 +lon_0=24.5 +k=1 +x_0=100000 +y_0=1000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5125 projected ETRS89-NOR [EUREF89] / NTM zone 25 25,68.59,26.01,71.24 1 1407 +proj=tmerc +lat_0=58 +lon_0=25.5 +k=1 +x_0=100000 +y_0=1000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5126 projected ETRS89-NOR [EUREF89] / NTM zone 26 26,69.71,27.01,71.2 1 1407 +proj=tmerc +lat_0=58 +lon_0=26.5 +k=1 +x_0=100000 +y_0=1000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5127 projected ETRS89-NOR [EUREF89] / NTM zone 27 27,69.9,28.01,71.19 1 1407 +proj=tmerc +lat_0=58 +lon_0=27.5 +k=1 +x_0=100000 +y_0=1000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5128 projected ETRS89-NOR [EUREF89] / NTM zone 28 28,69.03,29.01,71.15 1 1407 +proj=tmerc +lat_0=58 +lon_0=28.5 +k=1 +x_0=100000 +y_0=1000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5129 projected ETRS89-NOR [EUREF89] / NTM zone 29 29,69.02,30.01,70.94 1 1407 +proj=tmerc +lat_0=58 +lon_0=29.5 +k=1 +x_0=100000 +y_0=1000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5130 projected ETRS89-NOR [EUREF89] / NTM zone 30 30,69.46,31.32,70.77 1 1407 +proj=tmerc +lat_0=58 +lon_0=30.5 +k=1 +x_0=100000 +y_0=1000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5132 geographic Tokyo 1892 122.83,20.37,154.05,45.54 1048 +proj=longlat +ellps=bessel +no_defs +5167 projected Korean 1985 / East Sea Belt 130.71,37.39,131.01,37.62 6162 +proj=tmerc +lat_0=38 +lon_0=131 +k=1 +x_0=200000 +y_0=500000 +ellps=bessel +units=m +no_defs +5168 projected Korean 1985 / Central Belt Jeju 126.09,33.14,127.01,33.61 6162 +proj=tmerc +lat_0=38 +lon_0=127 +k=1 +x_0=200000 +y_0=550000 +ellps=bessel +units=m +no_defs +5169 projected Tokyo 1892 / Korea West Belt 124.27,33.99,126,40.9 1048 +proj=tmerc +lat_0=38 +lon_0=125 +k=1 +x_0=200000 +y_0=500000 +ellps=bessel +units=m +no_defs +5170 projected Tokyo 1892 / Korea Central Belt 126,33.14,128,41.8 1048 +proj=tmerc +lat_0=38 +lon_0=127 +k=1 +x_0=200000 +y_0=500000 +ellps=bessel +units=m +no_defs +5171 projected Tokyo 1892 / Korea East Belt 128,34.49,130,43.01 1048 +proj=tmerc +lat_0=38 +lon_0=129 +k=1 +x_0=200000 +y_0=500000 +ellps=bessel +units=m +no_defs +5172 projected Tokyo 1892 / Korea East Sea Belt 130,37.39,131.01,42.98 1048 +proj=tmerc +lat_0=38 +lon_0=131 +k=1 +x_0=200000 +y_0=500000 +ellps=bessel +units=m +no_defs +5173 projected Korean 1985 / Modified West Belt 124.53,33.99,126,38.04 6162 +proj=tmerc +lat_0=38 +lon_0=125.002890277778 +k=1 +x_0=200000 +y_0=500000 +ellps=bessel +units=m +no_defs +5174 projected Korean 1985 / Modified Central Belt 126,33.96,128,38.33 6162 +proj=tmerc +lat_0=38 +lon_0=127.002890277778 +k=1 +x_0=200000 +y_0=500000 +ellps=bessel +units=m +no_defs +5175 projected Korean 1985 / Modified Central Belt Jeju 126.09,33.14,127.01,33.61 6162 +proj=tmerc +lat_0=38 +lon_0=127.002890277778 +k=1 +x_0=200000 +y_0=550000 +ellps=bessel +units=m +no_defs +5176 projected Korean 1985 / Modified East Belt 128,34.49,129.65,38.64 6162 +proj=tmerc +lat_0=38 +lon_0=129.002890277778 +k=1 +x_0=200000 +y_0=500000 +ellps=bessel +units=m +no_defs +5177 projected Korean 1985 / Modified East Sea Belt 130.71,37.39,131.01,37.62 6162 +proj=tmerc +lat_0=38 +lon_0=131.002890277778 +k=1 +x_0=200000 +y_0=500000 +ellps=bessel +units=m +no_defs +5178 projected Korean 1985 / Unified CS 124.53,33.14,131.01,38.64 6162 +proj=tmerc +lat_0=38 +lon_0=127.5 +k=0.9996 +x_0=1000000 +y_0=2000000 +ellps=bessel +units=m +no_defs +5179 projected KGD2002 / Unified CS 122.71,28.6,134.28,40.27 1 6737 +proj=tmerc +lat_0=38 +lon_0=127.5 +k=0.9996 +x_0=1000000 +y_0=2000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5180 projected KGD2002 / West Belt 124.53,33.99,126,38.04 1 6737 +proj=tmerc +lat_0=38 +lon_0=125 +k=1 +x_0=200000 +y_0=500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5181 projected KGD2002 / Central Belt 126,33.96,128,38.33 1 6737 +proj=tmerc +lat_0=38 +lon_0=127 +k=1 +x_0=200000 +y_0=500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5182 projected KGD2002 / Central Belt Jeju 126.09,33.14,127.01,33.61 1 6737 +proj=tmerc +lat_0=38 +lon_0=127 +k=1 +x_0=200000 +y_0=550000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5183 projected KGD2002 / East Belt 128,34.49,129.65,38.64 1 6737 +proj=tmerc +lat_0=38 +lon_0=129 +k=1 +x_0=200000 +y_0=500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5184 projected KGD2002 / East Sea Belt 130.71,37.39,131.01,37.62 1 6737 +proj=tmerc +lat_0=38 +lon_0=131 +k=1 +x_0=200000 +y_0=500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5185 projected KGD2002 / West Belt 2010 124.53,33.99,126,38.04 1 6737 +proj=tmerc +lat_0=38 +lon_0=125 +k=1 +x_0=200000 +y_0=600000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5186 projected KGD2002 / Central Belt 2010 126,33.14,128,38.33 1 6737 +proj=tmerc +lat_0=38 +lon_0=127 +k=1 +x_0=200000 +y_0=600000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5187 projected KGD2002 / East Belt 2010 128,34.49,129.65,38.64 1 6737 +proj=tmerc +lat_0=38 +lon_0=129 +k=1 +x_0=200000 +y_0=600000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5188 projected KGD2002 / East Sea Belt 2010 130.71,37.39,131.01,37.62 1 6737 +proj=tmerc +lat_0=38 +lon_0=131 +k=1 +x_0=200000 +y_0=600000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5221 projected S-JTSK (Ferro) / Krovak East North 12.09,47.73,22.56,51.06 6818 +proj=krovak +lat_0=49.5 +lon_0=42.5 +alpha=30.2881397527778 +k=0.9999 +x_0=0 +y_0=0 +ellps=bessel +pm=-17.6666666666667 +units=m +no_defs +5223 projected WGS 84 / Gabon TM 8.65,-3.98,14.52,2.32 6326 +proj=tmerc +lat_0=0 +lon_0=12 +k=0.9996 +x_0=500000 +y_0=500000 +datum=WGS84 +units=m +no_defs +5224 projected S-JTSK/05 (Ferro) / Modified Krovak 12.09,48.58,18.86,51.06 1055 +proj=mod_krovak +axis=swu +lat_0=49.5 +lon_0=42.5 +alpha=30.2881397222222 +k=0.9999 +x_0=5000000 +y_0=5000000 +ellps=bessel +pm=-17.6666666666667 +units=m +no_defs +5225 projected S-JTSK/05 (Ferro) / Modified Krovak East North 12.09,48.58,18.86,51.06 1055 +proj=mod_krovak +lat_0=49.5 +lon_0=42.5 +alpha=30.2881397222222 +k=0.9999 +x_0=5000000 +y_0=5000000 +ellps=bessel +pm=-17.6666666666667 +units=m +no_defs +5228 geographic S-JTSK/05 12.09,48.58,18.86,51.06 1 1052 +proj=longlat +ellps=bessel +towgs84=572.213,85.334,461.94,4.9732,1.529,5.2484,3.5378 +no_defs +5229 geographic S-JTSK/05 (Ferro) 12.09,48.58,18.86,51.06 1055 +proj=longlat +ellps=bessel +pm=-17.6666666666667 +no_defs +5233 geographic SLD99 79.64,5.86,81.95,9.88 14 1053 +proj=longlat +ellps=evrst30 +towgs84=-0.293,766.95,87.713,0.195704,1.695068,3.473016,-0.039338 +no_defs +5234 projected Kandawala / Sri Lanka Grid 79.64,5.86,81.95,9.88 35 6244 +proj=tmerc +lat_0=7.00048027777778 +lon_0=80.7717111111111 +k=0.9999238418 +x_0=200000 +y_0=200000 +ellps=evrst30 +towgs84=-97,787,86 +units=m +no_defs +5235 projected SLD99 / Sri Lanka Grid 1999 79.64,5.86,81.95,9.88 14 1053 +proj=tmerc +lat_0=7.00047152777778 +lon_0=80.7717130833333 +k=0.9999238418 +x_0=500000 +y_0=500000 +ellps=evrst30 +towgs84=-0.293,766.95,87.713,0.195704,1.695068,3.473016,-0.039338 +units=m +no_defs +5243 projected ETRS89 / LCC Germany (E-N) 5.86,47.27,15.04,55.09 1 6258 +proj=lcc +lat_0=51 +lon_0=10.5 +lat_1=48.6666666666667 +lat_2=53.6666666666667 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5246 geographic GDBD2009 112.37,4.01,115.37,6.31 1 1056 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +5247 projected GDBD2009 / Brunei BRSO 112.37,4.01,115.37,6.31 1 1056 +proj=omerc +no_uoff +lat_0=4 +lonc=115 +alpha=53.31580995 +gamma=53.1301023611111 +k=0.99984 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5252 geographic TUREF 25.62,34.42,44.83,43.45 1 1057 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +5253 projected TUREF / TM27 25.62,36.5,28.5,42.11 1 1057 +proj=tmerc +lat_0=0 +lon_0=27 +k=1 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5254 projected TUREF / TM30 28.5,36.06,31.5,41.46 1 1057 +proj=tmerc +lat_0=0 +lon_0=30 +k=1 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5255 projected TUREF / TM33 31.5,35.97,34.5,42.07 1 1057 +proj=tmerc +lat_0=0 +lon_0=33 +k=1 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5256 projected TUREF / TM36 34.5,35.81,37.5,42.15 1 1057 +proj=tmerc +lat_0=0 +lon_0=36 +k=1 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5257 projected TUREF / TM39 37.5,36.66,40.5,41.19 1 1057 +proj=tmerc +lat_0=0 +lon_0=39 +k=1 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5258 projected TUREF / TM42 40.5,37.02,43.5,41.6 1 1057 +proj=tmerc +lat_0=0 +lon_0=42 +k=1 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5259 projected TUREF / TM45 43.5,36.97,44.83,41.02 1 1057 +proj=tmerc +lat_0=0 +lon_0=45 +k=1 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5264 geographic DRUKREF 03 88.74,26.7,92.13,28.33 1 1058 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +5266 projected DRUKREF 03 / Bhutan National Grid 88.74,26.7,92.13,28.33 1 1058 +proj=tmerc +lat_0=0 +lon_0=90 +k=1 +x_0=250000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5269 projected TUREF / 3-degree Gauss-Kruger zone 9 25.62,36.5,28.5,42.11 1 1057 +proj=tmerc +lat_0=0 +lon_0=27 +k=1 +x_0=9500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5270 projected TUREF / 3-degree Gauss-Kruger zone 10 28.5,36.06,31.5,41.46 1 1057 +proj=tmerc +lat_0=0 +lon_0=30 +k=1 +x_0=10500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5271 projected TUREF / 3-degree Gauss-Kruger zone 11 31.5,35.97,34.5,42.07 1 1057 +proj=tmerc +lat_0=0 +lon_0=33 +k=1 +x_0=11500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5272 projected TUREF / 3-degree Gauss-Kruger zone 12 34.5,35.81,37.5,42.15 1 1057 +proj=tmerc +lat_0=0 +lon_0=36 +k=1 +x_0=12500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5273 projected TUREF / 3-degree Gauss-Kruger zone 13 37.5,36.66,40.5,41.19 1 1057 +proj=tmerc +lat_0=0 +lon_0=39 +k=1 +x_0=13500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5274 projected TUREF / 3-degree Gauss-Kruger zone 14 40.5,37.02,43.5,41.6 1 1057 +proj=tmerc +lat_0=0 +lon_0=42 +k=1 +x_0=14500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5275 projected TUREF / 3-degree Gauss-Kruger zone 15 43.5,36.97,44.83,41.02 1 1057 +proj=tmerc +lat_0=0 +lon_0=45 +k=1 +x_0=15500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5292 projected DRUKREF 03 / Bumthang TM 90.46,27.33,91.02,28.09 1 1058 +proj=tmerc +lat_0=0 +lon_0=90.7333333333333 +k=1 +x_0=250000 +y_0=-2500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5293 projected DRUKREF 03 / Chhukha TM 89.26,26.71,89.83,27.32 1 1058 +proj=tmerc +lat_0=0 +lon_0=89.55 +k=1 +x_0=250000 +y_0=-2500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5294 projected DRUKREF 03 / Dagana TM 89.63,26.7,90.08,27.29 1 1058 +proj=tmerc +lat_0=0 +lon_0=89.85 +k=1 +x_0=250000 +y_0=-2500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5295 projected DRUKREF 03 / Gasa TM 89.44,27.72,90.47,28.33 1 1058 +proj=tmerc +lat_0=0 +lon_0=90.0333333333333 +k=1 +x_0=250000 +y_0=-2500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5296 projected DRUKREF 03 / Ha TM 88.9,27.02,89.39,27.62 1 1058 +proj=tmerc +lat_0=0 +lon_0=90.15 +k=1 +x_0=250000 +y_0=-2500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5297 projected DRUKREF 03 / Lhuentse TM 90.77,27.39,91.49,28.09 1 1058 +proj=tmerc +lat_0=0 +lon_0=91.1333333333333 +k=1 +x_0=250000 +y_0=-2500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5298 projected DRUKREF 03 / Mongar TM 90.95,26.93,91.5,27.61 1 1058 +proj=tmerc +lat_0=0 +lon_0=91.2333333333333 +k=1 +x_0=250000 +y_0=-2500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5299 projected DRUKREF 03 / Paro TM 89.12,27.18,89.56,27.79 1 1058 +proj=tmerc +lat_0=0 +lon_0=89.35 +k=1 +x_0=250000 +y_0=-2500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5300 projected DRUKREF 03 / Pemagatshel TM 91,26.78,91.56,27.18 1 1058 +proj=tmerc +lat_0=0 +lon_0=91.35 +k=1 +x_0=250000 +y_0=-2500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5301 projected DRUKREF 03 / Punakha TM 89.63,27.46,90.08,27.87 1 1058 +proj=tmerc +lat_0=0 +lon_0=89.85 +k=1 +x_0=250000 +y_0=-2500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5302 projected DRUKREF 03 / Samdrup Jongkhar TM 91.39,26.79,92.13,27.25 1 1058 +proj=tmerc +lat_0=0 +lon_0=91.5666666666667 +k=1 +x_0=250000 +y_0=-2500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5303 projected DRUKREF 03 / Samtse TM 88.74,26.8,89.38,27.28 1 1058 +proj=tmerc +lat_0=0 +lon_0=89.0666666666667 +k=1 +x_0=250000 +y_0=-2500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5304 projected DRUKREF 03 / Sarpang TM 90.01,26.73,90.78,27.23 1 1058 +proj=tmerc +lat_0=0 +lon_0=90.2666666666667 +k=1 +x_0=250000 +y_0=-2500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5305 projected DRUKREF 03 / Thimphu TM 89.22,27.14,89.77,28.01 1 1058 +proj=tmerc +lat_0=0 +lon_0=89.55 +k=1 +x_0=250000 +y_0=-2500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5306 projected DRUKREF 03 / Trashigang TM 91.37,27.01,92.13,27.49 1 1058 +proj=tmerc +lat_0=0 +lon_0=91.75 +k=1 +x_0=250000 +y_0=-2500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5307 projected DRUKREF 03 / Trongsa TM 90.26,27.13,90.76,27.79 1 1058 +proj=tmerc +lat_0=0 +lon_0=90.5 +k=1 +x_0=250000 +y_0=-2500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5308 projected DRUKREF 03 / Tsirang TM 90,26.81,90.35,27.2 1 1058 +proj=tmerc +lat_0=0 +lon_0=90.1666666666667 +k=1 +x_0=250000 +y_0=-2500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5309 projected DRUKREF 03 / Wangdue Phodrang TM 89.71,27.11,90.54,28.08 1 1058 +proj=tmerc +lat_0=0 +lon_0=90.1166666666667 +k=1 +x_0=250000 +y_0=-2500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5310 projected DRUKREF 03 / Yangtse TM 91.34,27.37,91.77,28 1 1058 +proj=tmerc +lat_0=0 +lon_0=91.5666666666667 +k=1 +x_0=250000 +y_0=-2500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5311 projected DRUKREF 03 / Zhemgang TM 90.53,26.77,91.19,27.39 1 1058 +proj=tmerc +lat_0=0 +lon_0=90.8666666666667 +k=1 +x_0=250000 +y_0=-2500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5316 projected ETRS89 / Faroe TM -13.91,59.94,-0.48,65.7 1 6258 +proj=tmerc +lat_0=0 +lon_0=-7 +k=0.999997 +x_0=200000 +y_0=-6000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5320 projected NAD83 / Teranet Ontario Lambert -95.16,41.67,-74.35,56.9 4 6269 +proj=lcc +lat_0=0 +lon_0=-84 +lat_1=44.5 +lat_2=54.5 +x_0=1000000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5321 projected NAD83(CSRS) / Teranet Ontario Lambert -95.16,41.67,-74.35,56.9 1 6140 +proj=lcc +lat_0=0 +lon_0=-84 +lat_1=44.5 +lat_2=54.5 +x_0=1000000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +5324 geographic ISN2004 -30.87,59.96,-5.55,69.59 1 1060 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +5325 projected ISN2004 / Lambert 2004 -30.87,59.96,-5.55,69.59 1 1060 +proj=lcc +lat_0=65 +lon_0=-19 +lat_1=64.25 +lat_2=65.75 +x_0=1700000 +y_0=300000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5329 projected Segara (Jakarta) / NEIEZ 114.55,-4.24,119.06,4.29 6820 +proj=merc +lon_0=3.19228055555556 +k=0.997 +x_0=3900000 +y_0=900000 +ellps=bessel +pm=106.807719444444 +units=m +no_defs +5330 projected Batavia (Jakarta) / NEIEZ 95.16,-8.91,115.77,5.97 6813 +proj=merc +lon_0=3.19228055555556 +k=0.997 +x_0=3900000 +y_0=900000 +ellps=bessel +pm=106.807719444444 +units=m +no_defs +5331 projected Makassar (Jakarta) / NEIEZ 118.71,-6.54,120.78,-1.88 6804 +proj=merc +lon_0=3.19228055555556 +k=0.997 +x_0=3900000 +y_0=900000 +ellps=bessel +pm=106.807719444444 +units=m +no_defs +5337 projected Aratu / UTM zone 25S -36,-20.11,-30,0 1 6208 +proj=utm +zone=25 +south +ellps=intl +towgs84=-151.99,287.04,-147.45 +units=m +no_defs +5340 geographic POSGAR 2007 -73.59,-58.41,-52.63,-21.78 0.5 1062 +proj=longlat +ellps=WGS84 +towgs84=-0.41,0.46,-0.35 +no_defs +5343 projected POSGAR 2007 / Argentina 1 -73.59,-52,-70.5,-36.16 0.5 1062 +proj=tmerc +lat_0=-90 +lon_0=-72 +k=1 +x_0=1500000 +y_0=0 +ellps=WGS84 +towgs84=-0.41,0.46,-0.35 +units=m +no_defs +5344 projected POSGAR 2007 / Argentina 2 -70.5,-54.9,-67.49,-24.08 0.5 1062 +proj=tmerc +lat_0=-90 +lon_0=-69 +k=1 +x_0=2500000 +y_0=0 +ellps=WGS84 +towgs84=-0.41,0.46,-0.35 +units=m +no_defs +5345 projected POSGAR 2007 / Argentina 3 -67.5,-55.11,-64.49,-21.78 0.5 1062 +proj=tmerc +lat_0=-90 +lon_0=-66 +k=1 +x_0=3500000 +y_0=0 +ellps=WGS84 +towgs84=-0.41,0.46,-0.35 +units=m +no_defs +5346 projected POSGAR 2007 / Argentina 4 -64.5,-54.91,-61.5,-21.99 0.5 1062 +proj=tmerc +lat_0=-90 +lon_0=-63 +k=1 +x_0=4500000 +y_0=0 +ellps=WGS84 +towgs84=-0.41,0.46,-0.35 +units=m +no_defs +5347 projected POSGAR 2007 / Argentina 5 -61.51,-39.06,-58.5,-23.37 0.5 1062 +proj=tmerc +lat_0=-90 +lon_0=-60 +k=1 +x_0=5500000 +y_0=0 +ellps=WGS84 +towgs84=-0.41,0.46,-0.35 +units=m +no_defs +5348 projected POSGAR 2007 / Argentina 6 -58.5,-38.59,-55.49,-24.84 0.5 1062 +proj=tmerc +lat_0=-90 +lon_0=-57 +k=1 +x_0=6500000 +y_0=0 +ellps=WGS84 +towgs84=-0.41,0.46,-0.35 +units=m +no_defs +5349 projected POSGAR 2007 / Argentina 7 -55.5,-28.11,-53.65,-25.49 0.5 1062 +proj=tmerc +lat_0=-90 +lon_0=-54 +k=1 +x_0=7500000 +y_0=0 +ellps=WGS84 +towgs84=-0.41,0.46,-0.35 +units=m +no_defs +5354 geographic MARGEN -69.66,-22.91,-57.52,-9.67 1 1063 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +5355 projected MARGEN / UTM zone 20S -66,-22.87,-60,-9.67 1 1063 +proj=utm +zone=20 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5356 projected MARGEN / UTM zone 19S -69.66,-22.91,-66,-9.77 1 1063 +proj=utm +zone=19 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5357 projected MARGEN / UTM zone 21S -60,-20.17,-57.52,-16.27 1 1063 +proj=utm +zone=21 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5360 geographic SIRGAS-Chile 2002 -113.21,-59.87,-65.72,-17.5 9 1064 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +5361 projected SIRGAS-Chile 2002 / UTM zone 19S -72,-59.87,-66,-17.5 9 1064 +proj=utm +zone=19 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5362 projected SIRGAS-Chile 2002 / UTM zone 18S -78,-59.36,-71.99,-18.35 12 1064 +proj=utm +zone=18 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5365 geographic CR05 -90.45,2.15,-81.43,11.77 1 1065 +proj=longlat +ellps=WGS84 +towgs84=-0.16959,0.35312,0.51846,0.03385,-0.16325,0.03446,0.03693 +no_defs +5367 projected CR05 / CRTM05 -86.5,2.21,-81.43,11.77 1 1065 +proj=tmerc +lat_0=0 +lon_0=-84 +k=0.9999 +x_0=500000 +y_0=0 +ellps=WGS84 +towgs84=-0.16959,0.35312,0.51846,0.03385,-0.16325,0.03446,0.03693 +units=m +no_defs +5371 geographic MACARIO SOLIS -84.32,5,-77.04,12.51 1 1066 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +5373 geographic Peru96 -84.68,-21.05,-68.67,-0.03 1 1067 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +5381 geographic SIRGAS-ROU98 -58.49,-37.77,-50.01,-30.09 1 1068 +proj=longlat +ellps=WGS84 +towgs84=0,0,0 +no_defs +5382 projected SIRGAS-ROU98 / UTM zone 21S -58.49,-36.63,-54,-30.09 1 1068 +proj=utm +zone=21 +south +ellps=WGS84 +towgs84=0,0,0 +units=m +no_defs +5383 projected SIRGAS-ROU98 / UTM zone 22S -54,-37.77,-50.01,-31.9 1 1068 +proj=utm +zone=22 +south +ellps=WGS84 +towgs84=0,0,0 +units=m +no_defs +5387 projected Peru96 / UTM zone 18S -78,-21.05,-72,-0.03 1 1067 +proj=utm +zone=18 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5389 projected Peru96 / UTM zone 19S -72,-20.44,-68.67,-2.14 1 1067 +proj=utm +zone=19 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5393 geographic SIRGAS_ES2007.8 -91.43,9.97,-87.65,14.44 1 1069 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +5396 projected SIRGAS 2000 / UTM zone 26S -30,-23.86,-25.28,4.26 1 6674 +proj=utm +zone=26 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5451 geographic Ocotepeque 1935 -92.29,7.98,-82.53,17.83 14 1070 +proj=longlat +ellps=clrk66 +towgs84=205,96,-98 +no_defs +5456 projected Ocotepeque 1935 / Costa Rica Norte -85.97,9.53,-82.53,11.22 8 1070 +proj=lcc +lat_1=10.4666666666667 +lat_0=10.4666666666667 +lon_0=-84.3333333333333 +k_0=0.99995696 +x_0=500000 +y_0=271820.522 +ellps=clrk66 +towgs84=213.11,9.37,-74.95 +units=m +no_defs +5457 projected Ocotepeque 1935 / Costa Rica Sur -85.74,7.98,-82.53,9.94 8 1070 +proj=lcc +lat_1=9 +lat_0=9 +lon_0=-83.6666666666667 +k_0=0.99995696 +x_0=500000 +y_0=327987.436 +ellps=clrk66 +towgs84=213.11,9.37,-74.95 +units=m +no_defs +5459 projected Ocotepeque 1935 / Guatemala Sur -92.29,13.69,-88.19,15.86 14 1070 +proj=lcc +lat_1=14.9 +lat_0=14.9 +lon_0=-90.3333333333333 +k_0=0.99989906 +x_0=500000 +y_0=325992.681 +ellps=clrk66 +towgs84=205,96,-98 +units=m +no_defs +5460 projected Ocotepeque 1935 / El Salvador Lambert -90.11,13.1,-87.69,14.44 14 1070 +proj=lcc +lat_1=13.7833333333333 +lat_0=13.7833333333333 +lon_0=-89 +k_0=0.99996704 +x_0=500000 +y_0=295809.184 +ellps=clrk66 +towgs84=205,96,-98 +units=m +no_defs +5461 projected Ocotepeque 1935 / Nicaragua Norte -87.74,12.8,-83.08,15.03 14 1070 +proj=lcc +lat_1=13.8666666666667 +lat_0=13.8666666666667 +lon_0=-85.5 +k_0=0.99990314 +x_0=500000 +y_0=359891.816 +ellps=clrk66 +towgs84=205,96,-98 +units=m +no_defs +5462 projected Ocotepeque 1935 / Nicaragua Sur -87.63,10.7,-83.42,12.8 14 1070 +proj=lcc +lat_1=11.7333333333333 +lat_0=11.7333333333333 +lon_0=-85.5 +k_0=0.99992228 +x_0=500000 +y_0=288876.327 +ellps=clrk66 +towgs84=205,96,-98 +units=m +no_defs +5463 projected SAD69 / UTM zone 17N -80.18,0,-78,2.7 19 6618 +proj=utm +zone=17 +ellps=aust_SA +towgs84=-57,1,-41 +units=m +no_defs +5464 geographic Sibun Gorge 1922 -89.22,15.88,-87.72,18.49 1071 +proj=longlat +a=6378293.64520876 +b=6356617.98767984 +no_defs +5467 geographic Panama-Colon 1911 -83.04,7.15,-77.19,9.68 1072 +proj=longlat +ellps=clrk66 +no_defs +5469 projected Panama-Colon 1911 / Panama Lambert -83.04,7.15,-77.19,9.68 1072 +proj=lcc +lat_1=8.41666666666667 +lat_0=8.41666666666667 +lon_0=-80 +k_0=0.99989909 +x_0=500000 +y_0=294865.303 +ellps=clrk66 +units=m +no_defs +5472 projected Panama-Colon 1911 / Panama Polyconic -83.04,7.15,-77.19,9.68 1072 +proj=poly +lat_0=8.25 +lon_0=-81 +x_0=914391.7962 +y_0=999404.721715486 +ellps=clrk66 +to_meter=0.9143917962 +no_defs +5479 projected RSRGD2000 / MSLC2000 153,-81,173,-76 1 6764 +proj=lcc +lat_0=-78 +lon_0=163 +lat_1=-76.6666666666667 +lat_2=-79.3333333333333 +x_0=7000000 +y_0=5000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5480 projected RSRGD2000 / BCLC2000 157,-76,173,-73 1 6764 +proj=lcc +lat_0=-74.5 +lon_0=165 +lat_1=-73.6666666666667 +lat_2=-75.3333333333333 +x_0=5000000 +y_0=3000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5481 projected RSRGD2000 / PCLC2000 160,-73,172,-69.5 1 6764 +proj=lcc +lat_0=-71.5 +lon_0=166 +lat_1=-70.6666666666667 +lat_2=-72.3333333333333 +x_0=3000000 +y_0=1000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5482 projected RSRGD2000 / RSPS2000 150,-90,-150,-76 1 6764 +proj=stere +lat_0=-90 +lon_0=180 +k=0.994 +x_0=5000000 +y_0=1000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5489 geographic RGAF09 -63.66,14.08,-57.52,18.54 1 1073 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +5490 projected RGAF09 / UTM zone 20N -63.66,14.08,-60,18.32 1 1073 +proj=utm +zone=20 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5513 projected S-JTSK / Krovak 12.09,47.73,22.56,51.06 6 6156 +proj=krovak +axis=swu +lat_0=49.5 +lon_0=24.8333333333333 +alpha=30.2881397527778 +k=0.9999 +x_0=0 +y_0=0 +ellps=bessel +towgs84=589,76,480 +units=m +no_defs +5514 projected S-JTSK / Krovak East North 12.09,47.73,22.56,51.06 6 6156 +proj=krovak +lat_0=49.5 +lon_0=24.8333333333333 +alpha=30.2881397527778 +k=0.9999 +x_0=0 +y_0=0 +ellps=bessel +towgs84=589,76,480 +units=m +no_defs +5515 projected S-JTSK/05 / Modified Krovak 12.09,48.58,18.86,51.06 1 1052 +proj=mod_krovak +axis=swu +lat_0=49.5 +lon_0=24.8333333333333 +alpha=30.2881397222222 +k=0.9999 +x_0=5000000 +y_0=5000000 +ellps=bessel +towgs84=572.213,85.334,461.94,4.9732,1.529,5.2484,3.5378 +units=m +no_defs +5516 projected S-JTSK/05 / Modified Krovak East North 12.09,48.58,18.86,51.06 1 1052 +proj=mod_krovak +lat_0=49.5 +lon_0=24.8333333333333 +alpha=30.2881397222222 +k=0.9999 +x_0=5000000 +y_0=5000000 +ellps=bessel +towgs84=572.213,85.334,461.94,4.9732,1.529,5.2484,3.5378 +units=m +no_defs +5518 projected CI1971 / Chatham Islands Map Grid -177.25,-44.64,-175.54,-43.3 26 6672 +proj=tmerc +lat_0=-44 +lon_0=-176.5 +k=1 +x_0=350000 +y_0=650000 +ellps=intl +towgs84=175,-38,113 +units=m +no_defs +5519 projected CI1979 / Chatham Islands Map Grid -177.25,-44.64,-175.54,-43.3 2 6673 +proj=tmerc +lat_0=-44 +lon_0=-176.5 +k=1 +x_0=350000 +y_0=650000 +ellps=intl +towgs84=174.05,-25.49,112.57,0,0,0.554,0.2263 +units=m +no_defs +5520 projected DHDN / 3-degree Gauss-Kruger zone 1 3.34,55.24,4.51,55.92 6314 +proj=tmerc +lat_0=0 +lon_0=3 +k=1 +x_0=1500000 +y_0=0 +ellps=bessel +units=m +no_defs +5523 projected WGS 84 / Gabon TM 2011 7.03,-6.37,14.52,2.32 6326 +proj=tmerc +lat_0=0 +lon_0=11.5 +k=0.9996 +x_0=1500000 +y_0=5500000 +datum=WGS84 +units=m +no_defs +5524 geographic Corrego Alegre 1961 -58.16,-27.5,-38.82,-14.99 1074 +proj=longlat +ellps=intl +no_defs +5527 geographic SAD69(96) -74.01,-35.71,-25.28,7.04 5 1075 +proj=longlat +ellps=aust_SA +towgs84=-67.35,3.88,-38.22 +no_defs +5530 projected SAD69(96) / Brazil Polyconic -74.01,-35.71,-25.28,7.04 5 1075 +proj=poly +lat_0=0 +lon_0=-54 +x_0=5000000 +y_0=10000000 +ellps=aust_SA +towgs84=-67.35,3.88,-38.22 +units=m +no_defs +5531 projected SAD69(96) / UTM zone 21S -60,-31.91,-53.99,4.51 5 1075 +proj=utm +zone=21 +south +ellps=aust_SA +towgs84=-67.35,3.88,-38.22 +units=m +no_defs +5533 projected SAD69(96) / UTM zone 23S -48,-33.5,-42,5.13 5 1075 +proj=utm +zone=23 +south +ellps=aust_SA +towgs84=-67.35,3.88,-38.22 +units=m +no_defs +5534 projected SAD69(96) / UTM zone 24S -42,-26.35,-36,0.74 5 1075 +proj=utm +zone=24 +south +ellps=aust_SA +towgs84=-67.35,3.88,-38.22 +units=m +no_defs +5535 projected SAD69(96) / UTM zone 25S -36,-23.8,-29.99,4.19 5 1075 +proj=utm +zone=25 +south +ellps=aust_SA +towgs84=-67.35,3.88,-38.22 +units=m +no_defs +5536 projected Corrego Alegre 1961 / UTM zone 21S -58.16,-27.5,-54,-17.99 1074 +proj=utm +zone=21 +south +ellps=intl +units=m +no_defs +5537 projected Corrego Alegre 1961 / UTM zone 22S -54,-27.5,-47.99,-14.99 1074 +proj=utm +zone=22 +south +ellps=intl +units=m +no_defs +5538 projected Corrego Alegre 1961 / UTM zone 23S -48,-25.29,-42,-15 1074 +proj=utm +zone=23 +south +ellps=intl +units=m +no_defs +5539 projected Corrego Alegre 1961 / UTM zone 24S -42,-22.96,-38.82,-14.99 1074 +proj=utm +zone=24 +south +ellps=intl +units=m +no_defs +5546 geographic PNG94 139.2,-14.75,162.81,2.58 2 1076 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +5550 projected PNG94 / PNGMG94 zone 54 139.2,-11.15,144,2.31 2 1076 +proj=utm +zone=54 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5551 projected PNG94 / PNGMG94 zone 55 144,-13.88,150.01,2.58 2 1076 +proj=utm +zone=55 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5552 projected PNG94 / PNGMG94 zone 56 150,-14.75,156,1.98 2 1076 +proj=utm +zone=56 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5559 projected Ocotepeque 1935 / Guatemala Norte -91.86,15.85,-88.34,17.83 14 1070 +proj=lcc +lat_1=16.8166666666667 +lat_0=16.8166666666667 +lon_0=-90.3333333333333 +k_0=0.99992226 +x_0=500000 +y_0=292209.579 +ellps=clrk66 +towgs84=205,96,-98 +units=m +no_defs +5561 geographic UCS-2000 22.15,43.18,40.18,52.38 1 1077 +proj=longlat +ellps=krass +towgs84=24,-121,-76 +no_defs +5562 projected UCS-2000 / Gauss-Kruger zone 4 22.15,47.95,24,51.66 1 1077 +proj=tmerc +lat_0=0 +lon_0=21 +k=1 +x_0=4500000 +y_0=0 +ellps=krass +towgs84=24,-121,-76 +units=m +no_defs +5563 projected UCS-2000 / Gauss-Kruger zone 5 24,45.1,30,51.96 1 1077 +proj=tmerc +lat_0=0 +lon_0=27 +k=1 +x_0=5500000 +y_0=0 +ellps=krass +towgs84=24,-121,-76 +units=m +no_defs +5564 projected UCS-2000 / Gauss-Kruger zone 6 30,43.18,36,52.38 1 1077 +proj=tmerc +lat_0=0 +lon_0=33 +k=1 +x_0=6500000 +y_0=0 +ellps=krass +towgs84=24,-121,-76 +units=m +no_defs +5565 projected UCS-2000 / Gauss-Kruger zone 7 36,43.43,40.18,50.44 1 1077 +proj=tmerc +lat_0=0 +lon_0=39 +k=1 +x_0=7500000 +y_0=0 +ellps=krass +towgs84=24,-121,-76 +units=m +no_defs +5566 projected UCS-2000 / Gauss-Kruger CM 21E 22.15,47.95,24,51.66 1 1077 +proj=tmerc +lat_0=0 +lon_0=21 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24,-121,-76 +units=m +no_defs +5567 projected UCS-2000 / Gauss-Kruger CM 27E 24,45.1,30,51.96 1 1077 +proj=tmerc +lat_0=0 +lon_0=27 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24,-121,-76 +units=m +no_defs +5568 projected UCS-2000 / Gauss-Kruger CM 33E 30,43.18,36,52.38 1 1077 +proj=tmerc +lat_0=0 +lon_0=33 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24,-121,-76 +units=m +no_defs +5569 projected UCS-2000 / Gauss-Kruger CM 39E 36,43.43,40.18,50.44 1 1077 +proj=tmerc +lat_0=0 +lon_0=39 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=24,-121,-76 +units=m +no_defs +5588 projected NAD27 / New Brunswick Stereographic (NAD27) -69.05,44.56,-63.7,48.07 9 6267 +proj=sterea +lat_0=46.5 +lon_0=-66.5 +k=0.999912 +x_0=304800 +y_0=304800 +ellps=clrk66 +towgs84=-22,160,190 +to_meter=0.3048 +no_defs +5589 projected Sibun Gorge 1922 / Colony Grid -89.22,15.88,-87.72,18.49 1071 +proj=tmerc +lat_0=17.0612419444444 +lon_0=-88.6318575 +k=1 +x_0=66220.0283308276 +y_0=135779.50998853 +a=6378293.64520876 +b=6356617.98767984 +to_meter=0.3047972654 +no_defs +5593 geographic FEH2010 10.66,54.33,12.01,54.83 1 1078 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +5596 projected FEH2010 / Fehmarnbelt TM 10.66,54.33,12.01,54.83 1 1078 +proj=tmerc +lat_0=0 +lon_0=11.3333333333333 +k=1 +x_0=1000000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5623 projected NAD27 / Michigan East -84.87,41.69,-82.13,46.04 10 6267 +proj=tmerc +lat_0=41.5 +lon_0=-83.6666666666667 +k=0.999942857 +x_0=152400.30480061 +y_0=0 +ellps=clrk66 +towgs84=-8,160,176 +to_meter=0.304800609601219 +no_defs +5624 projected NAD27 / Michigan Old Central -87.61,41.75,-84.6,46.11 10 6267 +proj=tmerc +lat_0=41.5 +lon_0=-85.75 +k=0.999909091 +x_0=152400.30480061 +y_0=0 +ellps=clrk66 +towgs84=-8,160,176 +to_meter=0.304800609601219 +no_defs +5625 projected NAD27 / Michigan West -90.42,45.09,-83.44,48.32 10 6267 +proj=tmerc +lat_0=41.5 +lon_0=-88.75 +k=0.999909091 +x_0=152400.30480061 +y_0=0 +ellps=clrk66 +towgs84=-8,160,176 +to_meter=0.304800609601219 +no_defs +5627 projected ED50 / TM 6 NE 3.04,41.15,10.38,43.74 2 6230 +proj=tmerc +lat_0=0 +lon_0=6 +k=0.9996 +x_0=500000 +y_0=0 +ellps=intl +towgs84=-84,-97,-117 +units=m +no_defs +5629 projected Moznet / UTM zone 38S 35.99,-18.98,40.9,-10.42 1 6130 +proj=utm +zone=38 +south +ellps=WGS84 +towgs84=0,0,0 +units=m +no_defs +5631 projected Pulkovo 1942(58) / Gauss-Kruger zone 2 (E-N) 9.92,50.2,12,54.23 6179 +proj=tmerc +lat_0=0 +lon_0=9 +k=1 +x_0=2500000 +y_0=0 +ellps=krass +units=m +no_defs +5632 projected PTRA08 / LCC Europe -35.58,29.24,-12.48,43.07 1 1041 +proj=lcc +lat_0=52 +lon_0=10 +lat_1=35 +lat_2=65 +x_0=4000000 +y_0=2800000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5633 projected PTRA08 / LAEA Europe -35.58,29.24,-12.48,43.07 1 1041 +proj=laea +lat_0=52 +lon_0=10 +x_0=4321000 +y_0=3210000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5634 projected REGCAN95 / LCC Europe -21.93,24.6,-11.75,32.76 1 1035 +proj=lcc +lat_0=52 +lon_0=10 +lat_1=35 +lat_2=65 +x_0=4000000 +y_0=2800000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5635 projected REGCAN95 / LAEA Europe -21.93,24.6,-11.75,32.76 1 1035 +proj=laea +lat_0=52 +lon_0=10 +x_0=4321000 +y_0=3210000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5636 projected TUREF / LAEA Europe 25.62,34.42,44.83,43.45 1 1057 +proj=laea +lat_0=52 +lon_0=10 +x_0=4321000 +y_0=3210000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5637 projected TUREF / LCC Europe 25.62,34.42,44.83,43.45 1 1057 +proj=lcc +lat_0=52 +lon_0=10 +lat_1=35 +lat_2=65 +x_0=4000000 +y_0=2800000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5638 projected ISN2004 / LAEA Europe -30.87,59.96,-5.55,69.59 1 1060 +proj=laea +lat_0=52 +lon_0=10 +x_0=4321000 +y_0=3210000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5639 projected ISN2004 / LCC Europe -30.87,59.96,-5.55,69.59 1 1060 +proj=lcc +lat_0=52 +lon_0=10 +lat_1=35 +lat_2=65 +x_0=4000000 +y_0=2800000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5641 projected SIRGAS 2000 / Brazil Mercator -51.64,-5.74,-32.43,7.04 1 6674 +proj=merc +lat_ts=-2 +lon_0=-43 +x_0=5000000 +y_0=10000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5643 projected ED50 / SPBA LCC -1.67,50.5,22,56 10 6230 +proj=lcc +lat_0=48 +lon_0=10 +lat_1=52.6666666666667 +lat_2=54.3333333333333 +x_0=815000 +y_0=0 +ellps=intl +towgs84=-87,-98,-121 +units=m +no_defs +5644 projected RGR92 / UTM zone 39S 51.83,-24.37,54,-18.52 1 6627 +proj=utm +zone=39 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5646 projected NAD83 / Vermont (ftUS) -73.44,42.72,-71.5,45.03 4 6269 +proj=tmerc +lat_0=42.5 +lon_0=-72.5 +k=0.999964286 +x_0=500000.00001016 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +5649 projected ETRS89 / UTM zone 31N (zE-N) 3.34,50.97,6.01,55.92 1 6258 +proj=tmerc +lat_0=0 +lon_0=3 +k=0.9996 +x_0=31500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5650 projected ETRS89 / UTM zone 33N (zE-N) 11.57,47.46,15.04,55.03 1 6258 +proj=tmerc +lat_0=0 +lon_0=15 +k=0.9996 +x_0=33500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5651 projected ETRS89 / UTM zone 31N (N-zE) 3.34,50.97,6.01,55.92 1 6258 +proj=tmerc +lat_0=0 +lon_0=3 +k=0.9996 +x_0=31500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5652 projected ETRS89 / UTM zone 32N (N-zE) 6,47.27,12.01,55.47 1 6258 +proj=tmerc +lat_0=0 +lon_0=9 +k=0.9996 +x_0=32500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5653 projected ETRS89 / UTM zone 33N (N-zE) 11.57,47.46,15.04,55.03 1 6258 +proj=tmerc +lat_0=0 +lon_0=15 +k=0.9996 +x_0=33500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5654 projected NAD83(HARN) / Vermont (ftUS) -73.44,42.72,-71.5,45.03 1 6152 +proj=tmerc +lat_0=42.5 +lon_0=-72.5 +k=0.999964286 +x_0=500000.00001016 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +5655 projected NAD83(NSRS2007) / Vermont (ftUS) -73.44,42.72,-71.5,45.03 2 6759 +proj=tmerc +lat_0=42.5 +lon_0=-72.5 +k=0.999964286 +x_0=500000.00001016 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +5659 projected Monte Mario / TM Emilia-Romagna 9.19,43.73,12.76,45.14 4 6265 +proj=tmerc +lat_0=0 +lon_0=9 +k=0.9996 +x_0=500053 +y_0=-3999820 +ellps=intl +towgs84=-104.1,-49.1,-9.9,0.971,-2.917,0.714,-11.68 +units=m +no_defs +5663 projected Pulkovo 1942(58) / Gauss-Kruger zone 3 (E-N) 12,45.78,18,54.89 1 6179 +proj=tmerc +lat_0=0 +lon_0=15 +k=1 +x_0=3500000 +y_0=0 +ellps=krass +towgs84=33.4,-146.6,-76.3,-0.359,-0.053,0.844,-0.84 +units=m +no_defs +5664 projected Pulkovo 1942(83) / Gauss-Kruger zone 2 (E-N) 9.92,50.2,12,54.23 2 6178 +proj=tmerc +lat_0=0 +lon_0=9 +k=1 +x_0=2500000 +y_0=0 +ellps=krass +towgs84=24,-123,-94,0.02,-0.25,-0.13,1.1 +units=m +no_defs +5665 projected Pulkovo 1942(83) / Gauss-Kruger zone 3 (E-N) 12,45.78,18.01,54.74 5 6178 +proj=tmerc +lat_0=0 +lon_0=15 +k=1 +x_0=3500000 +y_0=0 +ellps=krass +towgs84=26,-121,-78 +units=m +no_defs +5666 projected PD/83 / 3-degree Gauss-Kruger zone 3 (E-N) 9.92,50.35,10.5,51.56 6746 +proj=tmerc +lat_0=0 +lon_0=9 +k=1 +x_0=3500000 +y_0=0 +ellps=bessel +units=m +no_defs +5667 projected PD/83 / 3-degree Gauss-Kruger zone 4 (E-N) 10.5,50.2,12.56,51.64 6746 +proj=tmerc +lat_0=0 +lon_0=12 +k=1 +x_0=4500000 +y_0=0 +ellps=bessel +units=m +no_defs +5668 projected RD/83 / 3-degree Gauss-Kruger zone 4 (E-N) 11.89,50.2,13.51,51.66 6745 +proj=tmerc +lat_0=0 +lon_0=12 +k=1 +x_0=4500000 +y_0=0 +ellps=bessel +units=m +no_defs +5669 projected RD/83 / 3-degree Gauss-Kruger zone 5 (E-N) 13.5,50.62,15.04,51.58 6745 +proj=tmerc +lat_0=0 +lon_0=15 +k=1 +x_0=5500000 +y_0=0 +ellps=bessel +units=m +no_defs +5670 projected Pulkovo 1942(58) / 3-degree Gauss-Kruger zone 3 (E-N) 9.92,50.35,10.5,51.56 6179 +proj=tmerc +lat_0=0 +lon_0=9 +k=1 +x_0=3500000 +y_0=0 +ellps=krass +units=m +no_defs +5671 projected Pulkovo 1942(58) / 3-degree Gauss-Kruger zone 4 (E-N) 10.5,48.97,13.5,54.74 6179 +proj=tmerc +lat_0=0 +lon_0=12 +k=1 +x_0=4500000 +y_0=0 +ellps=krass +units=m +no_defs +5672 projected Pulkovo 1942(58) / 3-degree Gauss-Kruger zone 5 (E-N) 13.5,46.54,16.5,54.72 1 6179 +proj=tmerc +lat_0=0 +lon_0=15 +k=1 +x_0=5500000 +y_0=0 +ellps=krass +towgs84=33.4,-146.6,-76.3,-0.359,-0.053,0.844,-0.84 +units=m +no_defs +5673 projected Pulkovo 1942(83) / 3-degree Gauss-Kruger zone 3 (E-N) 9.92,50.35,10.5,51.56 2 6178 +proj=tmerc +lat_0=0 +lon_0=9 +k=1 +x_0=3500000 +y_0=0 +ellps=krass +towgs84=24,-123,-94,0.02,-0.25,-0.13,1.1 +units=m +no_defs +5674 projected Pulkovo 1942(83) / 3-degree Gauss-Kruger zone 4 (E-N) 10.5,48.97,13.5,54.74 2 6178 +proj=tmerc +lat_0=0 +lon_0=12 +k=1 +x_0=4500000 +y_0=0 +ellps=krass +towgs84=24,-123,-94,0.02,-0.25,-0.13,1.1 +units=m +no_defs +5675 projected Pulkovo 1942(83) / 3-degree Gauss-Kruger zone 5 (E-N) 13.5,46.54,16.5,54.72 5 6178 +proj=tmerc +lat_0=0 +lon_0=15 +k=1 +x_0=5500000 +y_0=0 +ellps=krass +towgs84=26,-121,-78 +units=m +no_defs +5676 projected DHDN / 3-degree Gauss-Kruger zone 2 (E-N) 5.86,49.11,7.51,53.81 3 6314 +proj=tmerc +lat_0=0 +lon_0=6 +k=1 +x_0=2500000 +y_0=0 +ellps=bessel +towgs84=598.1,73.7,418.2,0.202,0.045,-2.455,6.7 +units=m +no_defs +5677 projected DHDN / 3-degree Gauss-Kruger zone 3 (E-N) 7.5,47.27,10.51,55.09 3 6314 +proj=tmerc +lat_0=0 +lon_0=9 +k=1 +x_0=3500000 +y_0=0 +ellps=bessel +towgs84=598.1,73.7,418.2,0.202,0.045,-2.455,6.7 +units=m +no_defs +5678 projected DHDN / 3-degree Gauss-Kruger zone 4 (E-N) 10.5,47.39,13.51,54.59 3 6314 +proj=tmerc +lat_0=0 +lon_0=12 +k=1 +x_0=4500000 +y_0=0 +ellps=bessel +towgs84=598.1,73.7,418.2,0.202,0.045,-2.455,6.7 +units=m +no_defs +5679 projected DHDN / 3-degree Gauss-Kruger zone 5 (E-N) 13.5,48.51,13.84,48.98 3 6314 +proj=tmerc +lat_0=0 +lon_0=15 +k=1 +x_0=5500000 +y_0=0 +ellps=bessel +towgs84=598.1,73.7,418.2,0.202,0.045,-2.455,6.7 +units=m +no_defs +5680 projected DHDN / 3-degree Gauss-Kruger zone 1 (E-N) 3.34,55.24,4.51,55.92 6314 +proj=tmerc +lat_0=0 +lon_0=3 +k=1 +x_0=1500000 +y_0=0 +ellps=bessel +units=m +no_defs +5681 geographic DB_REF 5.86,47.27,15.04,55.09 1081 +proj=longlat +ellps=bessel +no_defs +5682 projected DB_REF / 3-degree Gauss-Kruger zone 2 (E-N) 5.86,49.11,7.51,53.81 1081 +proj=tmerc +lat_0=0 +lon_0=6 +k=1 +x_0=2500000 +y_0=0 +ellps=bessel +units=m +no_defs +5683 projected DB_REF / 3-degree Gauss-Kruger zone 3 (E-N) 7.5,47.27,10.51,55.09 1081 +proj=tmerc +lat_0=0 +lon_0=9 +k=1 +x_0=3500000 +y_0=0 +ellps=bessel +units=m +no_defs +5684 projected DB_REF / 3-degree Gauss-Kruger zone 4 (E-N) 10.5,47.39,13.51,54.74 1081 +proj=tmerc +lat_0=0 +lon_0=12 +k=1 +x_0=4500000 +y_0=0 +ellps=bessel +units=m +no_defs +5685 projected DB_REF / 3-degree Gauss-Kruger zone 5 (E-N) 13.5,48.51,15.04,54.72 1081 +proj=tmerc +lat_0=0 +lon_0=15 +k=1 +x_0=5500000 +y_0=0 +ellps=bessel +units=m +no_defs +5700 projected NZGD2000 / UTM zone 1S -180,-52.97,-174,-25.88 1 6167 +proj=utm +zone=1 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5825 projected AGD66 / ACT Standard Grid 148.76,-35.93,149.4,-35.12 3 6202 +proj=tmerc +lat_0=-35.3177362777778 +lon_0=149.009294830556 +k=1.000086 +x_0=200000 +y_0=600000 +ellps=aust_SA +towgs84=-117.808,-51.536,137.784,0.303,0.446,0.234,-0.29 +units=m +no_defs +5836 projected Yemen NGN96 / UTM zone 37N 41.08,14.73,42,16.36 1 6163 +proj=utm +zone=37 +ellps=WGS84 +towgs84=0,0,0 +units=m +no_defs +5837 projected Yemen NGN96 / UTM zone 40N 54,8.95,57.96,14.95 1 6163 +proj=utm +zone=40 +ellps=WGS84 +towgs84=0,0,0 +units=m +no_defs +5839 projected Peru96 / UTM zone 17S -84,-17.33,-78,-3.11 1 1067 +proj=utm +zone=17 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5842 projected WGS 84 / TM 12 SE 10.41,-8.01,12.84,-5.05 6326 +proj=tmerc +lat_0=0 +lon_0=12 +k=0.9996 +x_0=500000 +y_0=10000000 +datum=WGS84 +units=m +no_defs +5844 projected RGRDC 2005 / Congo TM zone 30 29,-13.46,29.81,-12.15 1 1033 +proj=tmerc +lat_0=0 +lon_0=30 +k=0.9999 +x_0=500000 +y_0=10000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5858 projected SAD69(96) / UTM zone 22S -54.01,-35.71,-47.99,7.04 5 1075 +proj=utm +zone=22 +south +ellps=aust_SA +towgs84=-67.35,3.88,-38.22 +units=m +no_defs +5875 projected SAD69(96) / UTM zone 18S -74.01,-10.01,-71.99,-4.59 5 1075 +proj=utm +zone=18 +south +ellps=aust_SA +towgs84=-67.35,3.88,-38.22 +units=m +no_defs +5876 projected SAD69(96) / UTM zone 19S -72,-11.14,-65.99,2.15 5 1075 +proj=utm +zone=19 +south +ellps=aust_SA +towgs84=-67.35,3.88,-38.22 +units=m +no_defs +5877 projected SAD69(96) / UTM zone 20S -66,-16.28,-59.99,5.28 5 1075 +proj=utm +zone=20 +south +ellps=aust_SA +towgs84=-67.35,3.88,-38.22 +units=m +no_defs +5879 projected Cadastre 1997 / UTM zone 38S 44.98,-13.05,45.35,-12.61 1 1037 +proj=utm +zone=38 +south +ellps=intl +towgs84=-381.788,-57.501,-256.673 +units=m +no_defs +5880 projected SIRGAS 2000 / Brazil Polyconic -74.01,-35.71,-25.28,7.04 1 6674 +proj=poly +lat_0=0 +lon_0=-54 +x_0=5000000 +y_0=10000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5886 geographic TGD2005 -179.08,-25.68,-171.28,-14.14 1 1095 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +5887 projected TGD2005 / Tonga Map Grid -179.08,-25.68,-171.28,-14.14 1 1095 +proj=tmerc +lat_0=0 +lon_0=-177 +k=0.9996 +x_0=1500000 +y_0=5000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +5896 projected VN-2000 / TM-3 zone 481 102.14,9.2,103.51,22.82 1 6756 +proj=tmerc +lat_0=0 +lon_0=102 +k=0.9999 +x_0=500000 +y_0=0 +ellps=WGS84 +towgs84=-191.90441429,-39.30318279,-111.45032835,0.00928836,-0.01975479,0.00427372,0.252906278 +units=m +no_defs +5897 projected VN-2000 / TM-3 zone 482 103.5,8.33,106.51,23.4 1 6756 +proj=tmerc +lat_0=0 +lon_0=105 +k=0.9999 +x_0=500000 +y_0=0 +ellps=WGS84 +towgs84=-191.90441429,-39.30318279,-111.45032835,0.00928836,-0.01975479,0.00427372,0.252906278 +units=m +no_defs +5898 projected VN-2000 / TM-3 zone 491 106.5,8.57,109.53,22.95 1 6756 +proj=tmerc +lat_0=0 +lon_0=108 +k=0.9999 +x_0=500000 +y_0=0 +ellps=WGS84 +towgs84=-191.90441429,-39.30318279,-111.45032835,0.00928836,-0.01975479,0.00427372,0.252906278 +units=m +no_defs +5899 projected VN-2000 / TM-3 107-45 106.43,8.57,108.76,21.67 1 6756 +proj=tmerc +lat_0=0 +lon_0=107.75 +k=0.9999 +x_0=500000 +y_0=0 +ellps=WGS84 +towgs84=-191.90441429,-39.30318279,-111.45032835,0.00928836,-0.01975479,0.00427372,0.252906278 +units=m +no_defs +5921 projected WGS 84 / EPSG Arctic Regional zone A1 -156,75,-66,87.01 6326 +proj=lcc +lat_0=81.317226 +lon_0=-111 +lat_1=85 +lat_2=77 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +5922 projected WGS 84 / EPSG Arctic Regional zone A2 -84,75,6.01,87.01 6326 +proj=lcc +lat_0=81.317226 +lon_0=-39 +lat_1=85 +lat_2=77 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +5923 projected WGS 84 / EPSG Arctic Regional zone A3 -12,75,78.01,87.01 6326 +proj=lcc +lat_0=81.317226 +lon_0=33 +lat_1=85 +lat_2=77 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +5924 projected WGS 84 / EPSG Arctic Regional zone A4 60,75,150.01,87.01 6326 +proj=lcc +lat_0=81.317226 +lon_0=105 +lat_1=85 +lat_2=77 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +5925 projected WGS 84 / EPSG Arctic Regional zone A5 132,75,-138,87.01 6326 +proj=lcc +lat_0=81.317226 +lon_0=177 +lat_1=85 +lat_2=77 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +5926 projected WGS 84 / EPSG Arctic Regional zone B1 -156,67,-66,79.01 6326 +proj=lcc +lat_0=73.1557408611111 +lon_0=-111 +lat_1=77 +lat_2=69 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +5927 projected WGS 84 / EPSG Arctic Regional zone B2 -84,67,6.01,79.01 6326 +proj=lcc +lat_0=73.1557408611111 +lon_0=-39 +lat_1=77 +lat_2=69 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +5928 projected WGS 84 / EPSG Arctic Regional zone B3 -12,67,78.01,79.01 6326 +proj=lcc +lat_0=73.1557408611111 +lon_0=33 +lat_1=77 +lat_2=69 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +5929 projected WGS 84 / EPSG Arctic Regional zone B4 60,67,150.01,79.01 6326 +proj=lcc +lat_0=73.1557408611111 +lon_0=105 +lat_1=77 +lat_2=69 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +5930 projected WGS 84 / EPSG Arctic Regional zone B5 132,67,-138,79.01 6326 +proj=lcc +lat_0=73.1557408611111 +lon_0=177 +lat_1=77 +lat_2=69 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +5931 projected WGS 84 / EPSG Arctic Regional zone C1 -156,59,-66,71 6326 +proj=lcc +lat_0=65.1012708888889 +lon_0=-111 +lat_1=69 +lat_2=61 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +5932 projected WGS 84 / EPSG Arctic Regional zone C2 -84,59,6,71 6326 +proj=lcc +lat_0=65.1012708888889 +lon_0=-39 +lat_1=69 +lat_2=61 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +5933 projected WGS 84 / EPSG Arctic Regional zone C3 -12,59,78.01,71 6326 +proj=lcc +lat_0=65.1012708888889 +lon_0=33 +lat_1=69 +lat_2=61 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +5934 projected WGS 84 / EPSG Arctic Regional zone C4 60,59,150.01,71 6326 +proj=lcc +lat_0=65.1012708888889 +lon_0=105 +lat_1=69 +lat_2=61 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +5935 projected WGS 84 / EPSG Arctic Regional zone C5 132,59,-138,71 6326 +proj=lcc +lat_0=65.1012708888889 +lon_0=177 +lat_1=69 +lat_2=61 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +5936 projected WGS 84 / EPSG Alaska Polar Stereographic -180,60,180,90 6326 +proj=stere +lat_0=90 +lon_0=-150 +k=0.994 +x_0=2000000 +y_0=2000000 +datum=WGS84 +units=m +no_defs +5937 projected WGS 84 / EPSG Canada Polar Stereographic -180,60,180,90 6326 +proj=stere +lat_0=90 +lon_0=-100 +k=0.994 +x_0=2000000 +y_0=2000000 +datum=WGS84 +units=m +no_defs +5938 projected WGS 84 / EPSG Greenland Polar Stereographic -180,60,180,90 6326 +proj=stere +lat_0=90 +lon_0=-33 +k=0.994 +x_0=2000000 +y_0=2000000 +datum=WGS84 +units=m +no_defs +5939 projected WGS 84 / EPSG Norway Polar Stereographic -180,60,180,90 6326 +proj=stere +lat_0=90 +lon_0=18 +k=0.994 +x_0=2000000 +y_0=2000000 +datum=WGS84 +units=m +no_defs +5940 projected WGS 84 / EPSG Russia Polar Stereographic -180,60,180,90 6326 +proj=stere +lat_0=90 +lon_0=105 +k=0.994 +x_0=2000000 +y_0=2000000 +datum=WGS84 +units=m +no_defs +6050 projected GR96 / EPSG Arctic zone 1-25 -60,82.83,0,87.84 1 1421 +proj=lcc +lat_0=85.4371183333333 +lon_0=-30 +lat_1=87 +lat_2=83.6666666666667 +x_0=25500000 +y_0=1500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6051 projected GR96 / EPSG Arctic zone 2-18 -72,79.5,-32,84.51 1 1421 +proj=lcc +lat_0=82.0584248888889 +lon_0=-52 +lat_1=83.6666666666667 +lat_2=80.3333333333333 +x_0=18500000 +y_0=2500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6052 projected GR96 / EPSG Arctic zone 2-20 -32,79.5,8.01,84.51 1 1421 +proj=lcc +lat_0=82.0584248888889 +lon_0=-12 +lat_1=83.6666666666667 +lat_2=80.3333333333333 +x_0=20500000 +y_0=2500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6053 projected GR96 / EPSG Arctic zone 3-29 -75,76.16,-54,81.17 1 1421 +proj=lcc +lat_0=78.7073375277778 +lon_0=-69 +lat_1=80.3333333333333 +lat_2=77 +x_0=29500000 +y_0=3500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6054 projected GR96 / EPSG Arctic zone 3-31 -54,76.16,-24,81.17 1 1421 +proj=lcc +lat_0=78.7073375277778 +lon_0=-39 +lat_1=80.3333333333333 +lat_2=77 +x_0=31500000 +y_0=3500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6055 projected GR96 / EPSG Arctic zone 3-33 -24,76.16,1.89,81.17 1 1421 +proj=lcc +lat_0=78.7073375277778 +lon_0=-10 +lat_1=80.3333333333333 +lat_2=77 +x_0=33500000 +y_0=3500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6056 projected GR96 / EPSG Arctic zone 4-20 -76,72.83,-51,77.84 1 1421 +proj=lcc +lat_0=75.3644033055556 +lon_0=-64 +lat_1=77 +lat_2=73.6666666666667 +x_0=20500000 +y_0=4500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6057 projected GR96 / EPSG Arctic zone 4-22 -51,72.83,-26,77.84 1 1421 +proj=lcc +lat_0=75.3644033055556 +lon_0=-39 +lat_1=77 +lat_2=73.6666666666667 +x_0=22500000 +y_0=4500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6058 projected GR96 / EPSG Arctic zone 4-24 -26,72.83,-2,77.84 1 1421 +proj=lcc +lat_0=75.3644033055556 +lon_0=-14 +lat_1=77 +lat_2=73.6666666666667 +x_0=24500000 +y_0=4500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6059 projected GR96 / EPSG Arctic zone 5-41 -71.89,69.48,-51.99,74.51 1 1421 +proj=lcc +lat_0=72.0250091944445 +lon_0=-62 +lat_1=73.6666666666667 +lat_2=70.3333333333333 +x_0=41500000 +y_0=5500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6060 projected GR96 / EPSG Arctic zone 5-43 -52,69.5,-32,74.51 1 1421 +proj=lcc +lat_0=72.0250091944445 +lon_0=-42 +lat_1=73.6666666666667 +lat_2=70.3333333333333 +x_0=43500000 +y_0=5500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6061 projected GR96 / EPSG Arctic zone 5-45 -32,69.5,-12,74.51 1 1421 +proj=lcc +lat_0=72.0250091944445 +lon_0=-22 +lat_1=73.6666666666667 +lat_2=70.3333333333333 +x_0=45500000 +y_0=5500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6062 projected GR96 / EPSG Arctic zone 6-26 -65,66.16,-47,71.17 1 1421 +proj=lcc +lat_0=68.6874755555556 +lon_0=-56 +lat_1=70.3333333333333 +lat_2=67 +x_0=26500000 +y_0=6500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6063 projected GR96 / EPSG Arctic zone 6-28 -47,66.16,-29,71.17 1 1421 +proj=lcc +lat_0=68.6874755555556 +lon_0=-38 +lat_1=70.3333333333333 +lat_2=67 +x_0=28500000 +y_0=6500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6064 projected GR96 / EPSG Arctic zone 6-30 -29,66.16,-11,71.17 1 1421 +proj=lcc +lat_0=68.6874755555556 +lon_0=-20 +lat_1=70.3333333333333 +lat_2=67 +x_0=30500000 +y_0=6500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6065 projected GR96 / EPSG Arctic zone 7-11 -59,62.83,-42,67.84 1 1421 +proj=lcc +lat_0=65.3510393055555 +lon_0=-51 +lat_1=67 +lat_2=63.6666666666667 +x_0=11500000 +y_0=7500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6066 projected GR96 / EPSG Arctic zone 7-13 -42,62.83,-25,67.84 1 1421 +proj=lcc +lat_0=65.3510393055555 +lon_0=-34 +lat_1=67 +lat_2=63.6666666666667 +x_0=13500000 +y_0=7500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6067 projected GR96 / EPSG Arctic zone 8-20 -59,59.5,-44,64.51 1 1421 +proj=lcc +lat_0=62.0153068888889 +lon_0=-52 +lat_1=63.6666666666667 +lat_2=60.3333333333333 +x_0=20500000 +y_0=8500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6068 projected GR96 / EPSG Arctic zone 8-22 -44,59.5,-29,64.51 1 1421 +proj=lcc +lat_0=62.0153068888889 +lon_0=-37 +lat_1=63.6666666666667 +lat_2=60.3333333333333 +x_0=22500000 +y_0=8500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6069 projected ETRS89 / EPSG Arctic zone 2-22 -4,79.5,36.01,84.51 1 6258 +proj=lcc +lat_0=82.0584248888889 +lon_0=16 +lat_1=83.6666666666667 +lat_2=80.3333333333333 +x_0=22500000 +y_0=2500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6070 projected ETRS89 / EPSG Arctic zone 3-11 -3.35,76.16,38.01,81.17 1 6258 +proj=lcc +lat_0=78.7073375277778 +lon_0=21 +lat_1=80.3333333333333 +lat_2=77 +x_0=11500000 +y_0=3500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6071 projected ETRS89 / EPSG Arctic zone 4-26 -2,72.83,22.01,77.84 1 6258 +proj=lcc +lat_0=75.3644033055556 +lon_0=10 +lat_1=77 +lat_2=73.6666666666667 +x_0=26500000 +y_0=4500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6072 projected ETRS89 / EPSG Arctic zone 4-28 22,72.83,46.01,77.84 1 6258 +proj=lcc +lat_0=75.3644033055556 +lon_0=34 +lat_1=77 +lat_2=73.6666666666667 +x_0=28500000 +y_0=4500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6073 projected ETRS89 / EPSG Arctic zone 5-11 4,69.5,24.01,74.51 1 6258 +proj=lcc +lat_0=72.0250091944445 +lon_0=14 +lat_1=73.6666666666667 +lat_2=70.3333333333333 +x_0=11500000 +y_0=5500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6074 projected ETRS89 / EPSG Arctic zone 5-13 24,69.5,44.01,74.51 1 6258 +proj=lcc +lat_0=72.0250091944445 +lon_0=34 +lat_1=73.6666666666667 +lat_2=70.3333333333333 +x_0=13500000 +y_0=5500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6075 projected WGS 84 / EPSG Arctic zone 2-24 33,79.5,73.01,84.51 6326 +proj=lcc +lat_0=82.0584248888889 +lon_0=53 +lat_1=83.6666666666667 +lat_2=80.3333333333333 +x_0=24500000 +y_0=2500000 +datum=WGS84 +units=m +no_defs +6076 projected WGS 84 / EPSG Arctic zone 2-26 73,79.5,113.01,84.51 6326 +proj=lcc +lat_0=82.0584248888889 +lon_0=93 +lat_1=83.6666666666667 +lat_2=80.3333333333333 +x_0=26500000 +y_0=2500000 +datum=WGS84 +units=m +no_defs +6077 projected WGS 84 / EPSG Arctic zone 3-13 34.99,76.16,67.01,81.17 6326 +proj=lcc +lat_0=78.7073375277778 +lon_0=52 +lat_1=80.3333333333333 +lat_2=77 +x_0=13500000 +y_0=3500000 +datum=WGS84 +units=m +no_defs +6078 projected WGS 84 / EPSG Arctic zone 3-15 67,76.16,98.01,81.17 6326 +proj=lcc +lat_0=78.7073375277778 +lon_0=83 +lat_1=80.3333333333333 +lat_2=77 +x_0=15500000 +y_0=3500000 +datum=WGS84 +units=m +no_defs +6079 projected WGS 84 / EPSG Arctic zone 3-17 98,76.16,129.01,81.17 6326 +proj=lcc +lat_0=78.7073375277778 +lon_0=114 +lat_1=80.3333333333333 +lat_2=77 +x_0=17500000 +y_0=3500000 +datum=WGS84 +units=m +no_defs +6080 projected WGS 84 / EPSG Arctic zone 3-19 129,76.16,160.01,81.17 6326 +proj=lcc +lat_0=78.7073375277778 +lon_0=145 +lat_1=80.3333333333333 +lat_2=77 +x_0=19500000 +y_0=3500000 +datum=WGS84 +units=m +no_defs +6081 projected WGS 84 / EPSG Arctic zone 4-30 46,72.83,70.01,77.84 6326 +proj=lcc +lat_0=75.3644033055556 +lon_0=58 +lat_1=77 +lat_2=73.6666666666667 +x_0=30500000 +y_0=4500000 +datum=WGS84 +units=m +no_defs +6082 projected WGS 84 / EPSG Arctic zone 4-32 70,72.83,94.01,77.84 6326 +proj=lcc +lat_0=75.3644033055556 +lon_0=82 +lat_1=77 +lat_2=73.6666666666667 +x_0=32500000 +y_0=4500000 +datum=WGS84 +units=m +no_defs +6083 projected WGS 84 / EPSG Arctic zone 4-34 94,72.83,118.01,77.84 6326 +proj=lcc +lat_0=75.3644033055556 +lon_0=106 +lat_1=77 +lat_2=73.6666666666667 +x_0=34500000 +y_0=4500000 +datum=WGS84 +units=m +no_defs +6084 projected WGS 84 / EPSG Arctic zone 4-36 118,72.83,142.01,77.84 6326 +proj=lcc +lat_0=75.3644033055556 +lon_0=130 +lat_1=77 +lat_2=73.6666666666667 +x_0=36500000 +y_0=4500000 +datum=WGS84 +units=m +no_defs +6085 projected WGS 84 / EPSG Arctic zone 4-38 142,72.83,166.01,77.84 6326 +proj=lcc +lat_0=75.3644033055556 +lon_0=154 +lat_1=77 +lat_2=73.6666666666667 +x_0=38500000 +y_0=4500000 +datum=WGS84 +units=m +no_defs +6086 projected WGS 84 / EPSG Arctic zone 4-40 166,72.83,-168.99,77.84 6326 +proj=lcc +lat_0=75.3644033055556 +lon_0=179 +lat_1=77 +lat_2=73.6666666666667 +x_0=40500000 +y_0=4500000 +datum=WGS84 +units=m +no_defs +6087 projected WGS 84 / EPSG Arctic zone 5-15 44,69.5,64.01,74.51 6326 +proj=lcc +lat_0=72.0250091944445 +lon_0=54 +lat_1=73.6666666666667 +lat_2=70.3333333333333 +x_0=15500000 +y_0=5500000 +datum=WGS84 +units=m +no_defs +6088 projected WGS 84 / EPSG Arctic zone 5-17 64,69.5,85.01,74.51 6326 +proj=lcc +lat_0=72.0250091944445 +lon_0=74 +lat_1=73.6666666666667 +lat_2=70.3333333333333 +x_0=17500000 +y_0=5500000 +datum=WGS84 +units=m +no_defs +6089 projected WGS 84 / EPSG Arctic zone 5-19 85,69.5,106.01,74.51 6326 +proj=lcc +lat_0=72.0250091944445 +lon_0=95 +lat_1=73.6666666666667 +lat_2=70.3333333333333 +x_0=19500000 +y_0=5500000 +datum=WGS84 +units=m +no_defs +6090 projected WGS 84 / EPSG Arctic zone 5-21 106,69.5,127.01,74.51 6326 +proj=lcc +lat_0=72.0250091944445 +lon_0=116 +lat_1=73.6666666666667 +lat_2=70.3333333333333 +x_0=21500000 +y_0=5500000 +datum=WGS84 +units=m +no_defs +6091 projected WGS 84 / EPSG Arctic zone 5-23 127,69.5,148,74.51 6326 +proj=lcc +lat_0=72.0250091944445 +lon_0=137 +lat_1=73.6666666666667 +lat_2=70.3333333333333 +x_0=23500000 +y_0=5500000 +datum=WGS84 +units=m +no_defs +6092 projected WGS 84 / EPSG Arctic zone 5-25 148,69.5,169.01,74.51 6326 +proj=lcc +lat_0=72.0250091944445 +lon_0=158 +lat_1=73.6666666666667 +lat_2=70.3333333333333 +x_0=25500000 +y_0=5500000 +datum=WGS84 +units=m +no_defs +6093 projected WGS 84 / EPSG Arctic zone 5-27 169,69.5,-169,74.51 6326 +proj=lcc +lat_0=72.0250091944445 +lon_0=179 +lat_1=73.6666666666667 +lat_2=70.3333333333333 +x_0=27500000 +y_0=5500000 +datum=WGS84 +units=m +no_defs +6094 projected NAD83(NSRS2007) / EPSG Arctic zone 5-29 -173,69.5,-153,74.51 2 6759 +proj=lcc +lat_0=72.0250091944445 +lon_0=-163 +lat_1=73.6666666666667 +lat_2=70.3333333333333 +x_0=29500000 +y_0=5500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6095 projected NAD83(NSRS2007) / EPSG Arctic zone 5-31 -157,69.5,-137,74.51 2 6759 +proj=lcc +lat_0=72.0250091944445 +lon_0=-147 +lat_1=73.6666666666667 +lat_2=70.3333333333333 +x_0=31500000 +y_0=5500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6096 projected NAD83(NSRS2007) / EPSG Arctic zone 6-14 -174,66.16,-156,71.17 2 6759 +proj=lcc +lat_0=68.6874755555556 +lon_0=-165 +lat_1=70.3333333333333 +lat_2=67 +x_0=14500000 +y_0=6500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6097 projected NAD83(NSRS2007) / EPSG Arctic zone 6-16 -156,66.16,-138,71.17 2 6759 +proj=lcc +lat_0=68.6874755555556 +lon_0=-147 +lat_1=70.3333333333333 +lat_2=67 +x_0=16500000 +y_0=6500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6098 projected NAD83(CSRS) / EPSG Arctic zone 1-23 -120,82.83,-60,87.84 1 6140 +proj=lcc +lat_0=85.4371183333333 +lon_0=-90 +lat_1=87 +lat_2=83.6666666666667 +x_0=23500000 +y_0=1500000 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +6099 projected NAD83(CSRS) / EPSG Arctic zone 2-14 -135,79.5,-95,84.51 1 6140 +proj=lcc +lat_0=82.0584248888889 +lon_0=-115 +lat_1=83.6666666666667 +lat_2=80.3333333333333 +x_0=14500000 +y_0=2500000 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +6100 projected NAD83(CSRS) / EPSG Arctic zone 2-16 -95,79.5,-55,84.51 1 6140 +proj=lcc +lat_0=82.0584248888889 +lon_0=-75 +lat_1=83.6666666666667 +lat_2=80.3333333333333 +x_0=16500000 +y_0=2500000 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +6101 projected NAD83(CSRS) / EPSG Arctic zone 3-25 -144,76.16,-114,81.17 1 6140 +proj=lcc +lat_0=78.7073375277778 +lon_0=-129 +lat_1=80.3333333333333 +lat_2=77 +x_0=25500000 +y_0=3500000 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +6102 projected NAD83(CSRS) / EPSG Arctic zone 3-27 -114,76.16,-84,81.17 1 6140 +proj=lcc +lat_0=78.7073375277778 +lon_0=-99 +lat_1=80.3333333333333 +lat_2=77 +x_0=27500000 +y_0=3500000 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +6103 projected NAD83(CSRS) / EPSG Arctic zone 3-29 -84,76.16,-64.78,81.17 1 6140 +proj=lcc +lat_0=78.7073375277778 +lon_0=-69 +lat_1=80.3333333333333 +lat_2=77 +x_0=29500000 +y_0=3500000 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +6104 projected NAD83(CSRS) / EPSG Arctic zone 4-14 -141,72.83,-116,77.84 1 6140 +proj=lcc +lat_0=75.3644033055556 +lon_0=-129 +lat_1=77 +lat_2=73.6666666666667 +x_0=14500000 +y_0=4500000 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +6105 projected NAD83(CSRS) / EPSG Arctic zone 4-16 -116,72.83,-91,77.84 1 6140 +proj=lcc +lat_0=75.3644033055556 +lon_0=-104 +lat_1=77 +lat_2=73.6666666666667 +x_0=16500000 +y_0=4500000 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +6106 projected NAD83(CSRS) / EPSG Arctic zone 4-18 -91,72.83,-67,77.84 1 6140 +proj=lcc +lat_0=75.3644033055556 +lon_0=-79 +lat_1=77 +lat_2=73.6666666666667 +x_0=18500000 +y_0=4500000 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +6107 projected NAD83(CSRS) / EPSG Arctic zone 5-33 -141,69.5,-121,74.51 1 6140 +proj=lcc +lat_0=72.0250091944445 +lon_0=-131 +lat_1=73.6666666666667 +lat_2=70.3333333333333 +x_0=33500000 +y_0=5500000 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +6108 projected NAD83(CSRS) / EPSG Arctic zone 5-35 -121,69.5,-101,74.51 1 6140 +proj=lcc +lat_0=72.0250091944445 +lon_0=-111 +lat_1=73.6666666666667 +lat_2=70.3333333333333 +x_0=35500000 +y_0=5500000 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +6109 projected NAD83(CSRS) / EPSG Arctic zone 5-37 -101,69.5,-81,74.51 1 6140 +proj=lcc +lat_0=72.0250091944445 +lon_0=-91 +lat_1=73.6666666666667 +lat_2=70.3333333333333 +x_0=37500000 +y_0=5500000 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +6110 projected NAD83(CSRS) / EPSG Arctic zone 5-39 -81,69.5,-61,74.51 1 6140 +proj=lcc +lat_0=72.0250091944445 +lon_0=-71 +lat_1=73.6666666666667 +lat_2=70.3333333333333 +x_0=39500000 +y_0=5500000 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +6111 projected NAD83(CSRS) / EPSG Arctic zone 6-18 -141,66.16,-122,71.17 1 6140 +proj=lcc +lat_0=68.6874755555556 +lon_0=-132 +lat_1=70.3333333333333 +lat_2=67 +x_0=18500000 +y_0=6500000 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +6112 projected NAD83(CSRS) / EPSG Arctic zone 6-20 -122,66.16,-103,71.17 1 6140 +proj=lcc +lat_0=68.6874755555556 +lon_0=-113 +lat_1=70.3333333333333 +lat_2=67 +x_0=20500000 +y_0=6500000 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +6113 projected NAD83(CSRS) / EPSG Arctic zone 6-22 -103,66.16,-84,71.17 1 6140 +proj=lcc +lat_0=68.6874755555556 +lon_0=-94 +lat_1=70.3333333333333 +lat_2=67 +x_0=22500000 +y_0=6500000 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +6114 projected NAD83(CSRS) / EPSG Arctic zone 6-24 -84,66.16,-65,71.17 1 6140 +proj=lcc +lat_0=68.6874755555556 +lon_0=-75 +lat_1=70.3333333333333 +lat_2=67 +x_0=24500000 +y_0=6500000 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +6115 projected WGS 84 / EPSG Arctic zone 1-27 0,82.83,60.01,87.84 6326 +proj=lcc +lat_0=85.4371183333333 +lon_0=30 +lat_1=87 +lat_2=83.6666666666667 +x_0=27500000 +y_0=1500000 +datum=WGS84 +units=m +no_defs +6116 projected WGS 84 / EPSG Arctic zone 1-29 60,82.83,120.01,87.84 6326 +proj=lcc +lat_0=85.4371183333333 +lon_0=90 +lat_1=87 +lat_2=83.6666666666667 +x_0=29500000 +y_0=1500000 +datum=WGS84 +units=m +no_defs +6117 projected WGS 84 / EPSG Arctic zone 1-31 120,82.83,180,87.83 6326 +proj=lcc +lat_0=85.4371183333333 +lon_0=150 +lat_1=87 +lat_2=83.6666666666667 +x_0=31500000 +y_0=1500000 +datum=WGS84 +units=m +no_defs +6118 projected WGS 84 / EPSG Arctic zone 1-21 -180,82.83,-120,87.84 6326 +proj=lcc +lat_0=85.4371183333333 +lon_0=-150 +lat_1=87 +lat_2=83.6666666666667 +x_0=21500000 +y_0=1500000 +datum=WGS84 +units=m +no_defs +6119 projected WGS 84 / EPSG Arctic zone 2-28 113,79.5,153.01,84.51 6326 +proj=lcc +lat_0=82.0584248888889 +lon_0=133 +lat_1=83.6666666666667 +lat_2=80.3333333333333 +x_0=28500000 +y_0=2500000 +datum=WGS84 +units=m +no_defs +6120 projected WGS 84 / EPSG Arctic zone 2-10 146,79.5,-173.99,84.51 6326 +proj=lcc +lat_0=82.0584248888889 +lon_0=166 +lat_1=83.6666666666667 +lat_2=80.3333333333333 +x_0=10500000 +y_0=2500000 +datum=WGS84 +units=m +no_defs +6121 projected WGS 84 / EPSG Arctic zone 2-12 -174,79.5,-134.99,84.51 6326 +proj=lcc +lat_0=82.0584248888889 +lon_0=-154 +lat_1=83.6666666666667 +lat_2=80.3333333333333 +x_0=12500000 +y_0=2500000 +datum=WGS84 +units=m +no_defs +6122 projected WGS 84 / EPSG Arctic zone 3-21 160,76.16,-168.99,81.17 6326 +proj=lcc +lat_0=78.7073375277778 +lon_0=176 +lat_1=80.3333333333333 +lat_2=77 +x_0=21500000 +y_0=3500000 +datum=WGS84 +units=m +no_defs +6123 projected WGS 84 / EPSG Arctic zone 3-23 -169,76.16,-138,81.17 6326 +proj=lcc +lat_0=78.7073375277778 +lon_0=-153 +lat_1=80.3333333333333 +lat_2=77 +x_0=23500000 +y_0=3500000 +datum=WGS84 +units=m +no_defs +6124 projected WGS 84 / EPSG Arctic zone 4-12 -169,72.83,-141,77.84 6326 +proj=lcc +lat_0=75.3644033055556 +lon_0=-155 +lat_1=77 +lat_2=73.6666666666667 +x_0=12500000 +y_0=4500000 +datum=WGS84 +units=m +no_defs +6125 projected ETRS89 / EPSG Arctic zone 5-47 -15,69.5,5.01,74.51 1 6258 +proj=lcc +lat_0=72.0250091944445 +lon_0=-5 +lat_1=73.6666666666667 +lat_2=70.3333333333333 +x_0=47500000 +y_0=5500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6128 projected Grand Cayman National Grid 1959 -81.46,19.21,-81.04,19.41 1 6723 +proj=tmerc +lat_0=0 +lon_0=-81 +k=0.9996 +x_0=499999.999998 +y_0=0 +ellps=clrk66 +towgs84=-179.483,-69.379,-27.584,-7.862,8.163,6.042,-13.925 +to_meter=0.3048 +no_defs +6129 projected Sister Islands National Grid 1961 -80.14,19.63,-79.69,19.78 1 6726 +proj=tmerc +lat_0=0 +lon_0=-81 +k=0.9996 +x_0=499999.999998 +y_0=0 +ellps=clrk66 +towgs84=8.853,-52.644,180.304,-0.393,-2.323,2.96,-24.081 +to_meter=0.3048 +no_defs +6135 geographic CIGD11 -83.6,17.58,-78.72,20.68 1 1100 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +6201 projected NAD27 / Michigan Central -87.06,43.8,-82.27,45.92 10 6267 +proj=lcc +lat_0=43.3166666666667 +lon_0=-84.3333333333333 +lat_1=44.1833333333333 +lat_2=45.7 +x_0=609601.219202438 +y_0=0 +k_0=1.0000382 +ellps=clrk66 +towgs84=-8,160,176 +to_meter=0.304800609601219 +no_defs +6202 projected NAD27 / Michigan South -87.2,41.69,-82.13,44.22 10 6267 +proj=lcc +lat_0=41.5 +lon_0=-84.3333333333333 +lat_1=42.1 +lat_2=43.6666666666667 +x_0=609601.219202438 +y_0=0 +k_0=1.0000382 +ellps=clrk66 +towgs84=-8,160,176 +to_meter=0.304800609601219 +no_defs +6204 projected Macedonia State Coordinate System 20.45,40.85,23.04,42.36 2 1031 +proj=tmerc +lat_0=0 +lon_0=21 +k=0.9999 +x_0=500000 +y_0=0 +ellps=bessel +towgs84=521.748,229.489,590.921,4.029,4.488,-15.521,-9.78 +units=m +no_defs +6207 geographic Nepal 1981 80.06,26.34,88.21,30.43 0.3 1111 +proj=longlat +ellps=evrst30 +towgs84=293.17,726.18,245.36 +no_defs +6210 projected SIRGAS 2000 / UTM zone 23N -48,0,-41.99,5.13 1 6674 +proj=utm +zone=23 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6211 projected SIRGAS 2000 / UTM zone 24N -42,0,-38.22,0.74 1 6674 +proj=utm +zone=24 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6307 projected NAD83(CORS96) / Puerto Rico and Virgin Is. -67.97,17.62,-64.51,18.57 1133 +proj=lcc +lat_0=17.8333333333333 +lon_0=-66.4333333333333 +lat_1=18.4333333333333 +lat_2=18.0333333333333 +x_0=200000 +y_0=200000 +ellps=GRS80 +units=m +no_defs +6311 geographic CGRS93 32.2,34.59,34.65,35.74 1 1112 +proj=longlat +ellps=WGS84 +towgs84=8.846,-4.394,-1.122,-0.00237,-0.146528,0.130428,0.783926 +no_defs +6312 projected CGRS93 / Cyprus Local Transverse Mercator 32.2,34.59,34.65,35.74 1 1112 +proj=tmerc +lat_0=0 +lon_0=33 +k=0.99995 +x_0=200000 +y_0=-3500000 +ellps=WGS84 +towgs84=8.846,-4.394,-1.122,-0.00237,-0.146528,0.130428,0.783926 +units=m +no_defs +6316 projected MGI 1901 / Balkans zone 7 19.5,41.85,22.51,46.19 5 1031 +proj=tmerc +lat_0=0 +lon_0=21 +k=0.9999 +x_0=7500000 +y_0=0 +ellps=bessel +towgs84=682,-203,480 +units=m +no_defs +6318 geographic NAD83(2011) 167.65,14.92,-63.88,74.71 2 1116 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +6322 geographic NAD83(PA11) 157.47,-17.56,-151.27,31.8 1117 +proj=longlat +ellps=GRS80 +no_defs +6325 geographic NAD83(MA11) 129.48,1.64,149.55,23.9 1118 +proj=longlat +ellps=GRS80 +no_defs +6328 projected NAD83(2011) / UTM zone 59N 167.65,49.01,174.01,56.28 2 1116 +proj=utm +zone=59 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6329 projected NAD83(2011) / UTM zone 60N 174,47.92,180,56.67 2 1116 +proj=utm +zone=60 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6330 projected NAD83(2011) / UTM zone 1N -180,47.88,-173.99,63.21 2 1116 +proj=utm +zone=1 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6331 projected NAD83(2011) / UTM zone 2N -174,48.66,-167.99,73.05 2 1116 +proj=utm +zone=2 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6332 projected NAD83(2011) / UTM zone 3N -168,49.52,-161.99,74.29 2 1116 +proj=utm +zone=3 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6333 projected NAD83(2011) / UTM zone 4N -162,50.98,-155.99,74.71 2 1116 +proj=utm +zone=4 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6334 projected NAD83(2011) / UTM zone 5N -156,52.15,-149.99,74.71 2 1116 +proj=utm +zone=5 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6335 projected NAD83(2011) / UTM zone 6N -150,54.05,-143.99,74.13 2 1116 +proj=utm +zone=6 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6336 projected NAD83(2011) / UTM zone 7N -144,53.47,-137.99,73.59 2 1116 +proj=utm +zone=7 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6337 projected NAD83(2011) / UTM zone 8N -138,53.6,-131.99,73.04 2 1116 +proj=utm +zone=8 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6338 projected NAD83(2011) / UTM zone 9N -132,35.38,-126,56.84 2 1116 +proj=utm +zone=9 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6339 projected NAD83(2011) / UTM zone 10N -126,30.54,-119.99,49.09 2 1116 +proj=utm +zone=10 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6340 projected NAD83(2011) / UTM zone 11N -120,30.88,-114,49.01 2 1116 +proj=utm +zone=11 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6341 projected NAD83(2011) / UTM zone 12N -114,31.33,-108,49.01 2 1116 +proj=utm +zone=12 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6342 projected NAD83(2011) / UTM zone 13N -108,28.98,-102,49.01 2 1116 +proj=utm +zone=13 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6343 projected NAD83(2011) / UTM zone 14N -102,25.83,-96,49.01 2 1116 +proj=utm +zone=14 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6344 projected NAD83(2011) / UTM zone 15N -96.01,25.61,-90,49.38 2 1116 +proj=utm +zone=15 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6345 projected NAD83(2011) / UTM zone 16N -90,23.97,-84,48.32 2 1116 +proj=utm +zone=16 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6346 projected NAD83(2011) / UTM zone 17N -84,23.81,-78,46.13 2 1116 +proj=utm +zone=17 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6347 projected NAD83(2011) / UTM zone 18N -78,28.28,-72,45.03 2 1116 +proj=utm +zone=18 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6348 projected NAD83(2011) / UTM zone 19N -72,33.61,-65.99,47.47 2 1116 +proj=utm +zone=19 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6350 projected NAD83(2011) / Conus Albers -124.79,24.41,-66.91,49.38 2 1116 +proj=aea +lat_0=23 +lon_0=-96 +lat_1=29.5 +lat_2=45.5 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6351 projected NAD83(2011) / EPSG Arctic zone 5-29 -173,69.5,-153,74.51 2 1116 +proj=lcc +lat_0=72.0250091944445 +lon_0=-163 +lat_1=73.6666666666667 +lat_2=70.3333333333333 +x_0=29500000 +y_0=5500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6352 projected NAD83(2011) / EPSG Arctic zone 5-31 -157,69.5,-137,74.51 2 1116 +proj=lcc +lat_0=72.0250091944445 +lon_0=-147 +lat_1=73.6666666666667 +lat_2=70.3333333333333 +x_0=31500000 +y_0=5500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6353 projected NAD83(2011) / EPSG Arctic zone 6-14 -174,66.16,-156,71.17 2 1116 +proj=lcc +lat_0=68.6874755555556 +lon_0=-165 +lat_1=70.3333333333333 +lat_2=67 +x_0=14500000 +y_0=6500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6354 projected NAD83(2011) / EPSG Arctic zone 6-16 -156,66.16,-138,71.17 2 1116 +proj=lcc +lat_0=68.6874755555556 +lon_0=-147 +lat_1=70.3333333333333 +lat_2=67 +x_0=16500000 +y_0=6500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6355 projected NAD83(2011) / Alabama East -86.79,30.99,-84.89,35 2 1116 +proj=tmerc +lat_0=30.5 +lon_0=-85.8333333333333 +k=0.99996 +x_0=200000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6356 projected NAD83(2011) / Alabama West -88.48,30.14,-86.3,35.02 2 1116 +proj=tmerc +lat_0=30 +lon_0=-87.5 +k=0.999933333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6362 projected Mexico ITRF92 / LCC -122.19,12.1,-84.64,32.72 1 1042 +proj=lcc +lat_0=12 +lon_0=-102 +lat_1=17.5 +lat_2=29.5 +x_0=2500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6365 geographic Mexico ITRF2008 -122.19,12.1,-84.64,32.72 1 1120 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +6366 projected Mexico ITRF2008 / UTM zone 11N -122.19,15.01,-114,32.72 1 1120 +proj=utm +zone=11 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6367 projected Mexico ITRF2008 / UTM zone 12N -114,15.09,-108,32.27 1 1120 +proj=utm +zone=12 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6368 projected Mexico ITRF2008 / UTM zone 13N -108,14.05,-102,31.79 1 1120 +proj=utm +zone=13 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6369 projected Mexico ITRF2008 / UTM zone 14N -102,12.3,-96,29.81 1 1120 +proj=utm +zone=14 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6370 projected Mexico ITRF2008 / UTM zone 15N -96,12.1,-90,26 1 1120 +proj=utm +zone=15 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6371 projected Mexico ITRF2008 / UTM zone 16N -90,17.81,-84.64,25.77 1 1120 +proj=utm +zone=16 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6372 projected Mexico ITRF2008 / LCC -122.19,12.1,-84.64,32.72 1 1120 +proj=lcc +lat_0=12 +lon_0=-102 +lat_1=17.5 +lat_2=29.5 +x_0=2500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6381 projected UCS-2000 / Ukraine TM zone 7 22.15,48.24,22.5,48.98 1 1077 +proj=tmerc +lat_0=0 +lon_0=21 +k=1 +x_0=300000 +y_0=0 +ellps=krass +towgs84=24,-121,-76 +units=m +no_defs +6382 projected UCS-2000 / Ukraine TM zone 8 22.5,47.71,25.5,51.96 1 1077 +proj=tmerc +lat_0=0 +lon_0=24 +k=1 +x_0=300000 +y_0=0 +ellps=krass +towgs84=24,-121,-76 +units=m +no_defs +6383 projected UCS-2000 / Ukraine TM zone 9 25.5,45.26,28.5,51.94 1 1077 +proj=tmerc +lat_0=0 +lon_0=27 +k=1 +x_0=300000 +y_0=0 +ellps=krass +towgs84=24,-121,-76 +units=m +no_defs +6384 projected UCS-2000 / Ukraine TM zone 10 28.5,43.42,31.5,52.12 1 1077 +proj=tmerc +lat_0=0 +lon_0=30 +k=1 +x_0=300000 +y_0=0 +ellps=krass +towgs84=24,-121,-76 +units=m +no_defs +6385 projected UCS-2000 / Ukraine TM zone 11 31.5,43.18,34.5,52.38 1 1077 +proj=tmerc +lat_0=0 +lon_0=33 +k=1 +x_0=300000 +y_0=0 +ellps=krass +towgs84=24,-121,-76 +units=m +no_defs +6386 projected UCS-2000 / Ukraine TM zone 12 34.5,43.24,37.5,51.25 1 1077 +proj=tmerc +lat_0=0 +lon_0=36 +k=1 +x_0=300000 +y_0=0 +ellps=krass +towgs84=24,-121,-76 +units=m +no_defs +6387 projected UCS-2000 / Ukraine TM zone 13 37.5,46.77,40.18,50.39 1 1077 +proj=tmerc +lat_0=0 +lon_0=39 +k=1 +x_0=300000 +y_0=0 +ellps=krass +towgs84=24,-121,-76 +units=m +no_defs +6391 projected Cayman Islands National Grid 2011 -83.6,17.58,-78.72,20.68 1 1100 +proj=lcc +lat_0=19.3333333333333 +lon_0=-80.5666666666667 +lat_1=19.3333333333333 +lat_2=19.7 +x_0=899160 +y_0=579120 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +6393 projected NAD83(2011) / Alaska Albers 172.42,51.3,-129.99,71.4 2 1116 +proj=aea +lat_0=50 +lon_0=-154 +lat_1=55 +lat_2=65 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6394 projected NAD83(2011) / Alaska zone 1 -141,54.61,-129.99,60.35 2 1116 +proj=omerc +no_uoff +lat_0=57 +lonc=-133.666666666667 +alpha=323.130102361111 +gamma=323.130102361111 +k=0.9999 +x_0=5000000 +y_0=-5000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6395 projected NAD83(2011) / Alaska zone 2 -144.01,59.72,-140.98,70.16 2 1116 +proj=tmerc +lat_0=54 +lon_0=-142 +k=0.9999 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6396 projected NAD83(2011) / Alaska zone 3 -148,59.72,-144,70.38 2 1116 +proj=tmerc +lat_0=54 +lon_0=-146 +k=0.9999 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6397 projected NAD83(2011) / Alaska zone 4 -152.01,59.11,-147.99,70.63 2 1116 +proj=tmerc +lat_0=54 +lon_0=-150 +k=0.9999 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6398 projected NAD83(2011) / Alaska zone 5 -156,55.72,-151.86,71.28 2 1116 +proj=tmerc +lat_0=54 +lon_0=-154 +k=0.9999 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6399 projected NAD83(2011) / Alaska zone 6 -160,54.89,-155.99,71.4 2 1116 +proj=tmerc +lat_0=54 +lon_0=-158 +k=0.9999 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6400 projected NAD83(2011) / Alaska zone 7 -164.01,54.32,-160,70.74 2 1116 +proj=tmerc +lat_0=54 +lon_0=-162 +k=0.9999 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6401 projected NAD83(2011) / Alaska zone 8 -168.26,54.34,-164,69.05 2 1116 +proj=tmerc +lat_0=54 +lon_0=-166 +k=0.9999 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6402 projected NAD83(2011) / Alaska zone 9 -173.16,56.49,-168,65.82 2 1116 +proj=tmerc +lat_0=54 +lon_0=-170 +k=0.9999 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6403 projected NAD83(2011) / Alaska zone 10 172.42,51.3,-164.84,54.34 2 1116 +proj=lcc +lat_0=51 +lon_0=-176 +lat_1=53.8333333333333 +lat_2=51.8333333333333 +x_0=1000000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6404 projected NAD83(2011) / Arizona Central -113.35,31.33,-110.44,37.01 2 1116 +proj=tmerc +lat_0=31 +lon_0=-111.916666666667 +k=0.9999 +x_0=213360 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6405 projected NAD83(2011) / Arizona Central (ft) -113.35,31.33,-110.44,37.01 2 1116 +proj=tmerc +lat_0=31 +lon_0=-111.916666666667 +k=0.9999 +x_0=213360 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +6406 projected NAD83(2011) / Arizona East -111.71,31.33,-109.04,37.01 2 1116 +proj=tmerc +lat_0=31 +lon_0=-110.166666666667 +k=0.9999 +x_0=213360 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6407 projected NAD83(2011) / Arizona East (ft) -111.71,31.33,-109.04,37.01 2 1116 +proj=tmerc +lat_0=31 +lon_0=-110.166666666667 +k=0.9999 +x_0=213360 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +6408 projected NAD83(2011) / Arizona West -114.81,32.05,-112.52,37 2 1116 +proj=tmerc +lat_0=31 +lon_0=-113.75 +k=0.999933333 +x_0=213360 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6409 projected NAD83(2011) / Arizona West (ft) -114.81,32.05,-112.52,37 2 1116 +proj=tmerc +lat_0=31 +lon_0=-113.75 +k=0.999933333 +x_0=213360 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +6410 projected NAD83(2011) / Arkansas North -94.62,34.67,-89.64,36.5 2 1116 +proj=lcc +lat_0=34.3333333333333 +lon_0=-92 +lat_1=36.2333333333333 +lat_2=34.9333333333333 +x_0=400000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6411 projected NAD83(2011) / Arkansas North (ftUS) -94.62,34.67,-89.64,36.5 2 1116 +proj=lcc +lat_0=34.3333333333333 +lon_0=-92 +lat_1=36.2333333333333 +lat_2=34.9333333333333 +x_0=399999.99998984 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6412 projected NAD83(2011) / Arkansas South -94.48,33.01,-90.4,35.1 2 1116 +proj=lcc +lat_0=32.6666666666667 +lon_0=-92 +lat_1=34.7666666666667 +lat_2=33.3 +x_0=400000 +y_0=400000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6413 projected NAD83(2011) / Arkansas South (ftUS) -94.48,33.01,-90.4,35.1 2 1116 +proj=lcc +lat_0=32.6666666666667 +lon_0=-92 +lat_1=34.7666666666667 +lat_2=33.3 +x_0=399999.99998984 +y_0=399999.99998984 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6414 projected NAD83(2011) / California Albers -124.45,32.53,-114.12,42.01 2 1116 +proj=aea +lat_0=0 +lon_0=-120 +lat_1=34 +lat_2=40.5 +x_0=0 +y_0=-4000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6415 projected NAD83(2011) / California zone 1 -124.45,39.59,-119.99,42.01 2 1116 +proj=lcc +lat_0=39.3333333333333 +lon_0=-122 +lat_1=41.6666666666667 +lat_2=40 +x_0=2000000 +y_0=500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6416 projected NAD83(2011) / California zone 1 (ftUS) -124.45,39.59,-119.99,42.01 2 1116 +proj=lcc +lat_0=39.3333333333333 +lon_0=-122 +lat_1=41.6666666666667 +lat_2=40 +x_0=2000000.0001016 +y_0=500000.0001016 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6417 projected NAD83(2011) / California zone 2 -124.06,38.02,-119.54,40.16 2 1116 +proj=lcc +lat_0=37.6666666666667 +lon_0=-122 +lat_1=39.8333333333333 +lat_2=38.3333333333333 +x_0=2000000 +y_0=500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6418 projected NAD83(2011) / California zone 2 (ftUS) -124.06,38.02,-119.54,40.16 2 1116 +proj=lcc +lat_0=37.6666666666667 +lon_0=-122 +lat_1=39.8333333333333 +lat_2=38.3333333333333 +x_0=2000000.0001016 +y_0=500000.0001016 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6419 projected NAD83(2011) / California zone 3 -123.02,36.73,-117.83,38.71 2 1116 +proj=lcc +lat_0=36.5 +lon_0=-120.5 +lat_1=38.4333333333333 +lat_2=37.0666666666667 +x_0=2000000 +y_0=500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6420 projected NAD83(2011) / California zone 3 (ftUS) -123.02,36.73,-117.83,38.71 2 1116 +proj=lcc +lat_0=36.5 +lon_0=-120.5 +lat_1=38.4333333333333 +lat_2=37.0666666666667 +x_0=2000000.0001016 +y_0=500000.0001016 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6421 projected NAD83(2011) / California zone 4 -122.01,35.78,-115.62,37.58 2 1116 +proj=lcc +lat_0=35.3333333333333 +lon_0=-119 +lat_1=37.25 +lat_2=36 +x_0=2000000 +y_0=500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6422 projected NAD83(2011) / California zone 4 (ftUS) -122.01,35.78,-115.62,37.58 2 1116 +proj=lcc +lat_0=35.3333333333333 +lon_0=-119 +lat_1=37.25 +lat_2=36 +x_0=2000000.0001016 +y_0=500000.0001016 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6423 projected NAD83(2011) / California zone 5 -121.42,32.76,-114.12,35.81 2 1116 +proj=lcc +lat_0=33.5 +lon_0=-118 +lat_1=35.4666666666667 +lat_2=34.0333333333333 +x_0=2000000 +y_0=500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6424 projected NAD83(2011) / California zone 5 (ftUS) -121.42,32.76,-114.12,35.81 2 1116 +proj=lcc +lat_0=33.5 +lon_0=-118 +lat_1=35.4666666666667 +lat_2=34.0333333333333 +x_0=2000000.0001016 +y_0=500000.0001016 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6425 projected NAD83(2011) / California zone 6 -118.15,32.53,-114.42,34.08 2 1116 +proj=lcc +lat_0=32.1666666666667 +lon_0=-116.25 +lat_1=33.8833333333333 +lat_2=32.7833333333333 +x_0=2000000 +y_0=500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6426 projected NAD83(2011) / California zone 6 (ftUS) -118.15,32.53,-114.42,34.08 2 1116 +proj=lcc +lat_0=32.1666666666667 +lon_0=-116.25 +lat_1=33.8833333333333 +lat_2=32.7833333333333 +x_0=2000000.0001016 +y_0=500000.0001016 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6427 projected NAD83(2011) / Colorado Central -109.06,38.14,-102.04,40.09 2 1116 +proj=lcc +lat_0=37.8333333333333 +lon_0=-105.5 +lat_1=39.75 +lat_2=38.45 +x_0=914401.8289 +y_0=304800.6096 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6428 projected NAD83(2011) / Colorado Central (ftUS) -109.06,38.14,-102.04,40.09 2 1116 +proj=lcc +lat_0=37.8333333333333 +lon_0=-105.5 +lat_1=39.75 +lat_2=38.45 +x_0=914401.828803658 +y_0=304800.609601219 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6429 projected NAD83(2011) / Colorado North -109.06,39.56,-102.04,41.01 2 1116 +proj=lcc +lat_0=39.3333333333333 +lon_0=-105.5 +lat_1=40.7833333333333 +lat_2=39.7166666666667 +x_0=914401.8289 +y_0=304800.6096 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6430 projected NAD83(2011) / Colorado North (ftUS) -109.06,39.56,-102.04,41.01 2 1116 +proj=lcc +lat_0=39.3333333333333 +lon_0=-105.5 +lat_1=40.7833333333333 +lat_2=39.7166666666667 +x_0=914401.828803658 +y_0=304800.609601219 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6431 projected NAD83(2011) / Colorado South -109.06,36.98,-102.04,38.68 2 1116 +proj=lcc +lat_0=36.6666666666667 +lon_0=-105.5 +lat_1=38.4333333333333 +lat_2=37.2333333333333 +x_0=914401.8289 +y_0=304800.6096 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6432 projected NAD83(2011) / Colorado South (ftUS) -109.06,36.98,-102.04,38.68 2 1116 +proj=lcc +lat_0=36.6666666666667 +lon_0=-105.5 +lat_1=38.4333333333333 +lat_2=37.2333333333333 +x_0=914401.828803658 +y_0=304800.609601219 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6433 projected NAD83(2011) / Connecticut -73.73,40.98,-71.78,42.05 2 1116 +proj=lcc +lat_0=40.8333333333333 +lon_0=-72.75 +lat_1=41.8666666666667 +lat_2=41.2 +x_0=304800.6096 +y_0=152400.3048 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6434 projected NAD83(2011) / Connecticut (ftUS) -73.73,40.98,-71.78,42.05 2 1116 +proj=lcc +lat_0=40.8333333333333 +lon_0=-72.75 +lat_1=41.8666666666667 +lat_2=41.2 +x_0=304800.609601219 +y_0=152400.30480061 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6435 projected NAD83(2011) / Delaware -75.8,38.44,-74.97,39.85 2 1116 +proj=tmerc +lat_0=38 +lon_0=-75.4166666666667 +k=0.999995 +x_0=200000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6436 projected NAD83(2011) / Delaware (ftUS) -75.8,38.44,-74.97,39.85 2 1116 +proj=tmerc +lat_0=38 +lon_0=-75.4166666666667 +k=0.999995 +x_0=200000.0001016 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6437 projected NAD83(2011) / Florida East -82.33,24.41,-79.97,30.83 2 1116 +proj=tmerc +lat_0=24.3333333333333 +lon_0=-81 +k=0.999941177 +x_0=200000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6438 projected NAD83(2011) / Florida East (ftUS) -82.33,24.41,-79.97,30.83 2 1116 +proj=tmerc +lat_0=24.3333333333333 +lon_0=-81 +k=0.999941177 +x_0=200000.0001016 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6439 projected NAD83(2011) / Florida GDL Albers -87.63,24.41,-79.97,31.01 2 1116 +proj=aea +lat_0=24 +lon_0=-84 +lat_1=24 +lat_2=31.5 +x_0=400000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6440 projected NAD83(2011) / Florida North -87.63,29.21,-82.04,31.01 2 1116 +proj=lcc +lat_0=29 +lon_0=-84.5 +lat_1=30.75 +lat_2=29.5833333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6441 projected NAD83(2011) / Florida North (ftUS) -87.63,29.21,-82.04,31.01 2 1116 +proj=lcc +lat_0=29 +lon_0=-84.5 +lat_1=30.75 +lat_2=29.5833333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6442 projected NAD83(2011) / Florida West -83.34,26.27,-81.13,29.6 2 1116 +proj=tmerc +lat_0=24.3333333333333 +lon_0=-82 +k=0.999941177 +x_0=200000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6443 projected NAD83(2011) / Florida West (ftUS) -83.34,26.27,-81.13,29.6 2 1116 +proj=tmerc +lat_0=24.3333333333333 +lon_0=-82 +k=0.999941177 +x_0=200000.0001016 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6444 projected NAD83(2011) / Georgia East -83.47,30.36,-80.77,34.68 2 1116 +proj=tmerc +lat_0=30 +lon_0=-82.1666666666667 +k=0.9999 +x_0=200000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6445 projected NAD83(2011) / Georgia East (ftUS) -83.47,30.36,-80.77,34.68 2 1116 +proj=tmerc +lat_0=30 +lon_0=-82.1666666666667 +k=0.9999 +x_0=200000.0001016 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6446 projected NAD83(2011) / Georgia West -85.61,30.62,-82.99,35.01 2 1116 +proj=tmerc +lat_0=30 +lon_0=-84.1666666666667 +k=0.9999 +x_0=700000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6447 projected NAD83(2011) / Georgia West (ftUS) -85.61,30.62,-82.99,35.01 2 1116 +proj=tmerc +lat_0=30 +lon_0=-84.1666666666667 +k=0.9999 +x_0=699999.9998984 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6448 projected NAD83(2011) / Idaho Central -115.3,41.99,-112.67,45.7 2 1116 +proj=tmerc +lat_0=41.6666666666667 +lon_0=-114 +k=0.999947368 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6449 projected NAD83(2011) / Idaho Central (ftUS) -115.3,41.99,-112.67,45.7 2 1116 +proj=tmerc +lat_0=41.6666666666667 +lon_0=-114 +k=0.999947368 +x_0=500000.0001016 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6450 projected NAD83(2011) / Idaho East -113.24,41.99,-111.04,44.75 2 1116 +proj=tmerc +lat_0=41.6666666666667 +lon_0=-112.166666666667 +k=0.999947368 +x_0=200000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6451 projected NAD83(2011) / Idaho East (ftUS) -113.24,41.99,-111.04,44.75 2 1116 +proj=tmerc +lat_0=41.6666666666667 +lon_0=-112.166666666667 +k=0.999947368 +x_0=200000.0001016 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6452 projected NAD83(2011) / Idaho West -117.24,41.99,-114.32,49.01 2 1116 +proj=tmerc +lat_0=41.6666666666667 +lon_0=-115.75 +k=0.999933333 +x_0=800000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6453 projected NAD83(2011) / Idaho West (ftUS) -117.24,41.99,-114.32,49.01 2 1116 +proj=tmerc +lat_0=41.6666666666667 +lon_0=-115.75 +k=0.999933333 +x_0=800000.0001016 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6454 projected NAD83(2011) / Illinois East -89.27,37.06,-87.02,42.5 2 1116 +proj=tmerc +lat_0=36.6666666666667 +lon_0=-88.3333333333333 +k=0.999975 +x_0=300000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6455 projected NAD83(2011) / Illinois East (ftUS) -89.27,37.06,-87.02,42.5 2 1116 +proj=tmerc +lat_0=36.6666666666667 +lon_0=-88.3333333333333 +k=0.999975 +x_0=300000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6456 projected NAD83(2011) / Illinois West -91.52,36.97,-88.92,42.51 2 1116 +proj=tmerc +lat_0=36.6666666666667 +lon_0=-90.1666666666667 +k=0.999941177 +x_0=700000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6457 projected NAD83(2011) / Illinois West (ftUS) -91.52,36.97,-88.92,42.51 2 1116 +proj=tmerc +lat_0=36.6666666666667 +lon_0=-90.1666666666667 +k=0.999941177 +x_0=699999.99998984 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6458 projected NAD83(2011) / Indiana East -86.59,37.95,-84.78,41.77 2 1116 +proj=tmerc +lat_0=37.5 +lon_0=-85.6666666666667 +k=0.999966667 +x_0=100000 +y_0=250000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6459 projected NAD83(2011) / Indiana East (ftUS) -86.59,37.95,-84.78,41.77 2 1116 +proj=tmerc +lat_0=37.5 +lon_0=-85.6666666666667 +k=0.999966667 +x_0=99999.9998983998 +y_0=249999.9998984 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6460 projected NAD83(2011) / Indiana West -88.1,37.77,-86.24,41.77 2 1116 +proj=tmerc +lat_0=37.5 +lon_0=-87.0833333333333 +k=0.999966667 +x_0=900000 +y_0=250000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6461 projected NAD83(2011) / Indiana West (ftUS) -88.1,37.77,-86.24,41.77 2 1116 +proj=tmerc +lat_0=37.5 +lon_0=-87.0833333333333 +k=0.999966667 +x_0=900000 +y_0=249999.9998984 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6462 projected NAD83(2011) / Iowa North -96.65,41.85,-90.15,43.51 2 1116 +proj=lcc +lat_0=41.5 +lon_0=-93.5 +lat_1=43.2666666666667 +lat_2=42.0666666666667 +x_0=1500000 +y_0=1000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6463 projected NAD83(2011) / Iowa North (ftUS) -96.65,41.85,-90.15,43.51 2 1116 +proj=lcc +lat_0=41.5 +lon_0=-93.5 +lat_1=43.2666666666667 +lat_2=42.0666666666667 +x_0=1500000 +y_0=999999.99998984 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6464 projected NAD83(2011) / Iowa South -96.14,40.36,-90.14,42.04 2 1116 +proj=lcc +lat_0=40 +lon_0=-93.5 +lat_1=41.7833333333333 +lat_2=40.6166666666667 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6465 projected NAD83(2011) / Iowa South (ftUS) -96.14,40.36,-90.14,42.04 2 1116 +proj=lcc +lat_0=40 +lon_0=-93.5 +lat_1=41.7833333333333 +lat_2=40.6166666666667 +x_0=500000.00001016 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6466 projected NAD83(2011) / Kansas North -102.06,38.52,-94.58,40.01 2 1116 +proj=lcc +lat_0=38.3333333333333 +lon_0=-98 +lat_1=39.7833333333333 +lat_2=38.7166666666667 +x_0=400000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6467 projected NAD83(2011) / Kansas North (ftUS) -102.06,38.52,-94.58,40.01 2 1116 +proj=lcc +lat_0=38.3333333333333 +lon_0=-98 +lat_1=39.7833333333333 +lat_2=38.7166666666667 +x_0=399999.99998984 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6468 projected NAD83(2011) / Kansas South -102.05,36.99,-94.6,38.88 2 1116 +proj=lcc +lat_0=36.6666666666667 +lon_0=-98.5 +lat_1=38.5666666666667 +lat_2=37.2666666666667 +x_0=400000 +y_0=400000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6469 projected NAD83(2011) / Kansas South (ftUS) -102.05,36.99,-94.6,38.88 2 1116 +proj=lcc +lat_0=36.6666666666667 +lon_0=-98.5 +lat_1=38.5666666666667 +lat_2=37.2666666666667 +x_0=399999.99998984 +y_0=399999.99998984 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6470 projected NAD83(2011) / Kentucky North -85.96,37.71,-82.47,39.15 2 1116 +proj=lcc +lat_0=37.5 +lon_0=-84.25 +lat_1=37.9666666666667 +lat_2=38.9666666666667 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6471 projected NAD83(2011) / Kentucky North (ftUS) -85.96,37.71,-82.47,39.15 2 1116 +proj=lcc +lat_0=37.5 +lon_0=-84.25 +lat_1=37.9666666666667 +lat_2=38.9666666666667 +x_0=500000.0001016 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6472 projected NAD83(2011) / Kentucky Single Zone -89.57,36.49,-81.95,39.15 2 1116 +proj=lcc +lat_0=36.3333333333333 +lon_0=-85.75 +lat_1=37.0833333333333 +lat_2=38.6666666666667 +x_0=1500000 +y_0=1000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6473 projected NAD83(2011) / Kentucky Single Zone (ftUS) -89.57,36.49,-81.95,39.15 2 1116 +proj=lcc +lat_0=36.3333333333333 +lon_0=-85.75 +lat_1=37.0833333333333 +lat_2=38.6666666666667 +x_0=1500000 +y_0=999999.9998984 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6474 projected NAD83(2011) / Kentucky South -89.57,36.49,-81.95,38.17 2 1116 +proj=lcc +lat_0=36.3333333333333 +lon_0=-85.75 +lat_1=37.9333333333333 +lat_2=36.7333333333333 +x_0=500000 +y_0=500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6475 projected NAD83(2011) / Kentucky South (ftUS) -89.57,36.49,-81.95,38.17 2 1116 +proj=lcc +lat_0=36.3333333333333 +lon_0=-85.75 +lat_1=37.9333333333333 +lat_2=36.7333333333333 +x_0=500000.0001016 +y_0=500000.0001016 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6476 projected NAD83(2011) / Louisiana North -94.05,30.85,-90.86,33.03 2 1116 +proj=lcc +lat_0=30.5 +lon_0=-92.5 +lat_1=32.6666666666667 +lat_2=31.1666666666667 +x_0=1000000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6477 projected NAD83(2011) / Louisiana North (ftUS) -94.05,30.85,-90.86,33.03 2 1116 +proj=lcc +lat_0=30.5 +lon_0=-92.5 +lat_1=32.6666666666667 +lat_2=31.1666666666667 +x_0=999999.99998984 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6478 projected NAD83(2011) / Louisiana South -93.94,28.85,-88.75,31.07 2 1116 +proj=lcc +lat_0=28.5 +lon_0=-91.3333333333333 +lat_1=30.7 +lat_2=29.3 +x_0=1000000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6479 projected NAD83(2011) / Louisiana South (ftUS) -93.94,28.85,-88.75,31.07 2 1116 +proj=lcc +lat_0=28.5 +lon_0=-91.3333333333333 +lat_1=30.7 +lat_2=29.3 +x_0=999999.99998984 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6480 projected NAD83(2011) / Maine CS2000 Central -70.03,43.75,-68.33,47.47 2 1116 +proj=tmerc +lat_0=43.5 +lon_0=-69.125 +k=0.99998 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6481 projected NAD83(2011) / Maine CS2000 East -68.58,44.18,-66.91,47.37 2 1116 +proj=tmerc +lat_0=43.8333333333333 +lon_0=-67.875 +k=0.99998 +x_0=700000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6482 projected NAD83(2011) / Maine CS2000 West -71.09,43.07,-69.6,46.58 2 1116 +proj=tmerc +lat_0=42.8333333333333 +lon_0=-70.375 +k=0.99998 +x_0=300000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6483 projected NAD83(2011) / Maine East -70.03,43.88,-66.91,47.47 2 1116 +proj=tmerc +lat_0=43.6666666666667 +lon_0=-68.5 +k=0.9999 +x_0=300000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6484 projected NAD83(2011) / Maine East (ftUS) -70.03,43.88,-66.91,47.47 2 1116 +proj=tmerc +lat_0=43.6666666666667 +lon_0=-68.5 +k=0.9999 +x_0=300000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6485 projected NAD83(2011) / Maine West -71.09,43.04,-69.26,46.58 2 1116 +proj=tmerc +lat_0=42.8333333333333 +lon_0=-70.1666666666667 +k=0.999966667 +x_0=900000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6486 projected NAD83(2011) / Maine West (ftUS) -71.09,43.04,-69.26,46.58 2 1116 +proj=tmerc +lat_0=42.8333333333333 +lon_0=-70.1666666666667 +k=0.999966667 +x_0=900000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6487 projected NAD83(2011) / Maryland -79.49,37.97,-74.97,39.73 2 1116 +proj=lcc +lat_0=37.6666666666667 +lon_0=-77 +lat_1=39.45 +lat_2=38.3 +x_0=400000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6488 projected NAD83(2011) / Maryland (ftUS) -79.49,37.97,-74.97,39.73 2 1116 +proj=lcc +lat_0=37.6666666666667 +lon_0=-77 +lat_1=39.45 +lat_2=38.3 +x_0=399999.9998984 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6489 projected NAD83(2011) / Massachusetts Island -70.91,41.19,-69.89,41.51 2 1116 +proj=lcc +lat_0=41 +lon_0=-70.5 +lat_1=41.4833333333333 +lat_2=41.2833333333333 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6490 projected NAD83(2011) / Massachusetts Island (ftUS) -70.91,41.19,-69.89,41.51 2 1116 +proj=lcc +lat_0=41 +lon_0=-70.5 +lat_1=41.4833333333333 +lat_2=41.2833333333333 +x_0=500000.0001016 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6491 projected NAD83(2011) / Massachusetts Mainland -73.5,41.46,-69.86,42.89 2 1116 +proj=lcc +lat_0=41 +lon_0=-71.5 +lat_1=42.6833333333333 +lat_2=41.7166666666667 +x_0=200000 +y_0=750000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6492 projected NAD83(2011) / Massachusetts Mainland (ftUS) -73.5,41.46,-69.86,42.89 2 1116 +proj=lcc +lat_0=41 +lon_0=-71.5 +lat_1=42.6833333333333 +lat_2=41.7166666666667 +x_0=200000.0001016 +y_0=750000 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6493 projected NAD83(2011) / Michigan Central -87.06,43.8,-82.27,45.92 2 1116 +proj=lcc +lat_0=43.3166666666667 +lon_0=-84.3666666666667 +lat_1=45.7 +lat_2=44.1833333333333 +x_0=6000000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6494 projected NAD83(2011) / Michigan Central (ft) -87.06,43.8,-82.27,45.92 2 1116 +proj=lcc +lat_0=43.3166666666667 +lon_0=-84.3666666666667 +lat_1=45.7 +lat_2=44.1833333333333 +x_0=5999999.999976 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +6495 projected NAD83(2011) / Michigan North -90.42,45.08,-83.44,48.32 2 1116 +proj=lcc +lat_0=44.7833333333333 +lon_0=-87 +lat_1=47.0833333333333 +lat_2=45.4833333333333 +x_0=8000000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6496 projected NAD83(2011) / Michigan North (ft) -90.42,45.08,-83.44,48.32 2 1116 +proj=lcc +lat_0=44.7833333333333 +lon_0=-87 +lat_1=47.0833333333333 +lat_2=45.4833333333333 +x_0=7999999.999968 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +6497 projected NAD83(2011) / Michigan Oblique Mercator -90.42,41.69,-82.13,48.32 2 1116 +proj=omerc +no_uoff +lat_0=45.3091666666667 +lonc=-86 +alpha=337.25556 +gamma=337.25556 +k=0.9996 +x_0=2546731.496 +y_0=-4354009.816 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6498 projected NAD83(2011) / Michigan South -87.2,41.69,-82.13,44.22 2 1116 +proj=lcc +lat_0=41.5 +lon_0=-84.3666666666667 +lat_1=43.6666666666667 +lat_2=42.1 +x_0=4000000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6499 projected NAD83(2011) / Michigan South (ft) -87.2,41.69,-82.13,44.22 2 1116 +proj=lcc +lat_0=41.5 +lon_0=-84.3666666666667 +lat_1=43.6666666666667 +lat_2=42.1 +x_0=3999999.999984 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +6500 projected NAD83(2011) / Minnesota Central -96.86,45.28,-92.29,47.48 2 1116 +proj=lcc +lat_0=45 +lon_0=-94.25 +lat_1=47.05 +lat_2=45.6166666666667 +x_0=800000 +y_0=100000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6501 projected NAD83(2011) / Minnesota Central (ftUS) -96.86,45.28,-92.29,47.48 2 1116 +proj=lcc +lat_0=45 +lon_0=-94.25 +lat_1=47.05 +lat_2=45.6166666666667 +x_0=800000.00001016 +y_0=99999.99998984 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6502 projected NAD83(2011) / Minnesota North -97.22,46.64,-89.49,49.38 2 1116 +proj=lcc +lat_0=46.5 +lon_0=-93.1 +lat_1=48.6333333333333 +lat_2=47.0333333333333 +x_0=800000 +y_0=100000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6503 projected NAD83(2011) / Minnesota North (ftUS) -97.22,46.64,-89.49,49.38 2 1116 +proj=lcc +lat_0=46.5 +lon_0=-93.1 +lat_1=48.6333333333333 +lat_2=47.0333333333333 +x_0=800000.00001016 +y_0=99999.99998984 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6504 projected NAD83(2011) / Minnesota South -96.85,43.49,-91.21,45.59 2 1116 +proj=lcc +lat_0=43 +lon_0=-94 +lat_1=45.2166666666667 +lat_2=43.7833333333333 +x_0=800000 +y_0=100000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6505 projected NAD83(2011) / Minnesota South (ftUS) -96.85,43.49,-91.21,45.59 2 1116 +proj=lcc +lat_0=43 +lon_0=-94 +lat_1=45.2166666666667 +lat_2=43.7833333333333 +x_0=800000.00001016 +y_0=99999.99998984 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6506 projected NAD83(2011) / Mississippi East -89.97,30.01,-88.09,35.01 2 1116 +proj=tmerc +lat_0=29.5 +lon_0=-88.8333333333333 +k=0.99995 +x_0=300000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6507 projected NAD83(2011) / Mississippi East (ftUS) -89.97,30.01,-88.09,35.01 2 1116 +proj=tmerc +lat_0=29.5 +lon_0=-88.8333333333333 +k=0.99995 +x_0=300000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6508 projected NAD83(2011) / Mississippi TM -91.65,30.01,-88.09,35.01 2 1116 +proj=tmerc +lat_0=32.5 +lon_0=-89.75 +k=0.9998335 +x_0=500000 +y_0=1300000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6509 projected NAD83(2011) / Mississippi West -91.65,31,-89.37,35.01 2 1116 +proj=tmerc +lat_0=29.5 +lon_0=-90.3333333333333 +k=0.99995 +x_0=700000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6510 projected NAD83(2011) / Mississippi West (ftUS) -91.65,31,-89.37,35.01 2 1116 +proj=tmerc +lat_0=29.5 +lon_0=-90.3333333333333 +k=0.99995 +x_0=699999.9998984 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6511 projected NAD83(2011) / Missouri Central -93.79,36.48,-91.41,40.61 2 1116 +proj=tmerc +lat_0=35.8333333333333 +lon_0=-92.5 +k=0.999933333 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6512 projected NAD83(2011) / Missouri East -91.97,35.98,-89.1,40.61 2 1116 +proj=tmerc +lat_0=35.8333333333333 +lon_0=-90.5 +k=0.999933333 +x_0=250000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6513 projected NAD83(2011) / Missouri West -95.77,36.48,-93.48,40.59 2 1116 +proj=tmerc +lat_0=36.1666666666667 +lon_0=-94.5 +k=0.999941177 +x_0=850000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6514 projected NAD83(2011) / Montana -116.07,44.35,-104.04,49.01 2 1116 +proj=lcc +lat_0=44.25 +lon_0=-109.5 +lat_1=49 +lat_2=45 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6515 projected NAD83(2011) / Montana (ft) -116.07,44.35,-104.04,49.01 2 1116 +proj=lcc +lat_0=44.25 +lon_0=-109.5 +lat_1=49 +lat_2=45 +x_0=599999.9999976 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +6516 projected NAD83(2011) / Nebraska -104.06,39.99,-95.3,43.01 2 1116 +proj=lcc +lat_0=39.8333333333333 +lon_0=-100 +lat_1=43 +lat_2=40 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6518 projected NAD83(2011) / Nevada Central -118.19,36,-114.99,41 2 1116 +proj=tmerc +lat_0=34.75 +lon_0=-116.666666666667 +k=0.9999 +x_0=500000 +y_0=6000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6519 projected NAD83(2011) / Nevada Central (ftUS) -118.19,36,-114.99,41 2 1116 +proj=tmerc +lat_0=34.75 +lon_0=-116.666666666667 +k=0.9999 +x_0=500000.00001016 +y_0=6000000 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6520 projected NAD83(2011) / Nevada East -117.01,34.99,-114.03,42 2 1116 +proj=tmerc +lat_0=34.75 +lon_0=-115.583333333333 +k=0.9999 +x_0=200000 +y_0=8000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6521 projected NAD83(2011) / Nevada East (ftUS) -117.01,34.99,-114.03,42 2 1116 +proj=tmerc +lat_0=34.75 +lon_0=-115.583333333333 +k=0.9999 +x_0=200000.00001016 +y_0=8000000.00001016 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6522 projected NAD83(2011) / Nevada West -120,36.95,-116.99,42 2 1116 +proj=tmerc +lat_0=34.75 +lon_0=-118.583333333333 +k=0.9999 +x_0=800000 +y_0=4000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6523 projected NAD83(2011) / Nevada West (ftUS) -120,36.95,-116.99,42 2 1116 +proj=tmerc +lat_0=34.75 +lon_0=-118.583333333333 +k=0.9999 +x_0=800000.00001016 +y_0=3999999.99998984 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6524 projected NAD83(2011) / New Hampshire -72.56,42.69,-70.63,45.31 2 1116 +proj=tmerc +lat_0=42.5 +lon_0=-71.6666666666667 +k=0.999966667 +x_0=300000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6525 projected NAD83(2011) / New Hampshire (ftUS) -72.56,42.69,-70.63,45.31 2 1116 +proj=tmerc +lat_0=42.5 +lon_0=-71.6666666666667 +k=0.999966667 +x_0=300000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6526 projected NAD83(2011) / New Jersey -75.6,38.87,-73.88,41.36 2 1116 +proj=tmerc +lat_0=38.8333333333333 +lon_0=-74.5 +k=0.9999 +x_0=150000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6527 projected NAD83(2011) / New Jersey (ftUS) -75.6,38.87,-73.88,41.36 2 1116 +proj=tmerc +lat_0=38.8333333333333 +lon_0=-74.5 +k=0.9999 +x_0=150000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6528 projected NAD83(2011) / New Mexico Central -107.73,31.78,-104.84,37 2 1116 +proj=tmerc +lat_0=31 +lon_0=-106.25 +k=0.9999 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6529 projected NAD83(2011) / New Mexico Central (ftUS) -107.73,31.78,-104.84,37 2 1116 +proj=tmerc +lat_0=31 +lon_0=-106.25 +k=0.9999 +x_0=500000.0001016 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6530 projected NAD83(2011) / New Mexico East -105.72,32,-102.99,37 2 1116 +proj=tmerc +lat_0=31 +lon_0=-104.333333333333 +k=0.999909091 +x_0=165000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6531 projected NAD83(2011) / New Mexico East (ftUS) -105.72,32,-102.99,37 2 1116 +proj=tmerc +lat_0=31 +lon_0=-104.333333333333 +k=0.999909091 +x_0=165000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6532 projected NAD83(2011) / New Mexico West -109.06,31.33,-106.32,37 2 1116 +proj=tmerc +lat_0=31 +lon_0=-107.833333333333 +k=0.999916667 +x_0=830000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6533 projected NAD83(2011) / New Mexico West (ftUS) -109.06,31.33,-106.32,37 2 1116 +proj=tmerc +lat_0=31 +lon_0=-107.833333333333 +k=0.999916667 +x_0=830000.0001016 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6534 projected NAD83(2011) / New York Central -77.75,41.99,-75.06,44.41 2 1116 +proj=tmerc +lat_0=40 +lon_0=-76.5833333333333 +k=0.9999375 +x_0=250000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6535 projected NAD83(2011) / New York Central (ftUS) -77.75,41.99,-75.06,44.41 2 1116 +proj=tmerc +lat_0=40 +lon_0=-76.5833333333333 +k=0.9999375 +x_0=249999.9998984 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6536 projected NAD83(2011) / New York East -75.87,40.88,-73.23,45.02 2 1116 +proj=tmerc +lat_0=38.8333333333333 +lon_0=-74.5 +k=0.9999 +x_0=150000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6537 projected NAD83(2011) / New York East (ftUS) -75.87,40.88,-73.23,45.02 2 1116 +proj=tmerc +lat_0=38.8333333333333 +lon_0=-74.5 +k=0.9999 +x_0=150000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6538 projected NAD83(2011) / New York Long Island -74.26,40.47,-71.8,41.3 2 1116 +proj=lcc +lat_0=40.1666666666667 +lon_0=-74 +lat_1=41.0333333333333 +lat_2=40.6666666666667 +x_0=300000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6539 projected NAD83(2011) / New York Long Island (ftUS) -74.26,40.47,-71.8,41.3 2 1116 +proj=lcc +lat_0=40.1666666666667 +lon_0=-74 +lat_1=41.0333333333333 +lat_2=40.6666666666667 +x_0=300000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6540 projected NAD83(2011) / New York West -79.77,41.99,-77.36,43.64 2 1116 +proj=tmerc +lat_0=40 +lon_0=-78.5833333333333 +k=0.9999375 +x_0=350000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6541 projected NAD83(2011) / New York West (ftUS) -79.77,41.99,-77.36,43.64 2 1116 +proj=tmerc +lat_0=40 +lon_0=-78.5833333333333 +k=0.9999375 +x_0=350000.0001016 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6542 projected NAD83(2011) / North Carolina -84.33,33.83,-75.38,36.59 2 1116 +proj=lcc +lat_0=33.75 +lon_0=-79 +lat_1=36.1666666666667 +lat_2=34.3333333333333 +x_0=609601.22 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6543 projected NAD83(2011) / North Carolina (ftUS) -84.33,33.83,-75.38,36.59 2 1116 +proj=lcc +lat_0=33.75 +lon_0=-79 +lat_1=36.1666666666667 +lat_2=34.3333333333333 +x_0=609601.219202438 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6544 projected NAD83(2011) / North Dakota North -104.07,47.15,-96.83,49.01 2 1116 +proj=lcc +lat_0=47 +lon_0=-100.5 +lat_1=48.7333333333333 +lat_2=47.4333333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6545 projected NAD83(2011) / North Dakota North (ft) -104.07,47.15,-96.83,49.01 2 1116 +proj=lcc +lat_0=47 +lon_0=-100.5 +lat_1=48.7333333333333 +lat_2=47.4333333333333 +x_0=599999.9999976 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +6546 projected NAD83(2011) / North Dakota South -104.05,45.93,-96.55,47.83 2 1116 +proj=lcc +lat_0=45.6666666666667 +lon_0=-100.5 +lat_1=47.4833333333333 +lat_2=46.1833333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6547 projected NAD83(2011) / North Dakota South (ft) -104.05,45.93,-96.55,47.83 2 1116 +proj=lcc +lat_0=45.6666666666667 +lon_0=-100.5 +lat_1=47.4833333333333 +lat_2=46.1833333333333 +x_0=599999.9999976 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +6548 projected NAD83(2011) / Ohio North -84.81,40.1,-80.51,42.33 2 1116 +proj=lcc +lat_0=39.6666666666667 +lon_0=-82.5 +lat_1=41.7 +lat_2=40.4333333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6549 projected NAD83(2011) / Ohio North (ftUS) -84.81,40.1,-80.51,42.33 2 1116 +proj=lcc +lat_0=39.6666666666667 +lon_0=-82.5 +lat_1=41.7 +lat_2=40.4333333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6550 projected NAD83(2011) / Ohio South -84.83,38.4,-80.7,40.36 2 1116 +proj=lcc +lat_0=38 +lon_0=-82.5 +lat_1=40.0333333333333 +lat_2=38.7333333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6551 projected NAD83(2011) / Ohio South (ftUS) -84.83,38.4,-80.7,40.36 2 1116 +proj=lcc +lat_0=38 +lon_0=-82.5 +lat_1=40.0333333333333 +lat_2=38.7333333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6552 projected NAD83(2011) / Oklahoma North -103,35.27,-94.42,37.01 2 1116 +proj=lcc +lat_0=35 +lon_0=-98 +lat_1=36.7666666666667 +lat_2=35.5666666666667 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6553 projected NAD83(2011) / Oklahoma North (ftUS) -103,35.27,-94.42,37.01 2 1116 +proj=lcc +lat_0=35 +lon_0=-98 +lat_1=36.7666666666667 +lat_2=35.5666666666667 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6554 projected NAD83(2011) / Oklahoma South -100,33.62,-94.42,35.57 2 1116 +proj=lcc +lat_0=33.3333333333333 +lon_0=-98 +lat_1=35.2333333333333 +lat_2=33.9333333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6555 projected NAD83(2011) / Oklahoma South (ftUS) -100,33.62,-94.42,35.57 2 1116 +proj=lcc +lat_0=33.3333333333333 +lon_0=-98 +lat_1=35.2333333333333 +lat_2=33.9333333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6556 projected NAD83(2011) / Oregon LCC (m) -124.6,41.98,-116.47,46.26 2 1116 +proj=lcc +lat_0=41.75 +lon_0=-120.5 +lat_1=43 +lat_2=45.5 +x_0=400000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6557 projected NAD83(2011) / Oregon GIC Lambert (ft) -124.6,41.98,-116.47,46.26 2 1116 +proj=lcc +lat_0=41.75 +lon_0=-120.5 +lat_1=43 +lat_2=45.5 +x_0=399999.9999984 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +6558 projected NAD83(2011) / Oregon North -124.17,43.95,-116.47,46.26 2 1116 +proj=lcc +lat_0=43.6666666666667 +lon_0=-120.5 +lat_1=46 +lat_2=44.3333333333333 +x_0=2500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6559 projected NAD83(2011) / Oregon North (ft) -124.17,43.95,-116.47,46.26 2 1116 +proj=lcc +lat_0=43.6666666666667 +lon_0=-120.5 +lat_1=46 +lat_2=44.3333333333333 +x_0=2500000.0001424 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +6560 projected NAD83(2011) / Oregon South -124.6,41.98,-116.9,44.56 2 1116 +proj=lcc +lat_0=41.6666666666667 +lon_0=-120.5 +lat_1=44 +lat_2=42.3333333333333 +x_0=1500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6561 projected NAD83(2011) / Oregon South (ft) -124.6,41.98,-116.9,44.56 2 1116 +proj=lcc +lat_0=41.6666666666667 +lon_0=-120.5 +lat_1=44 +lat_2=42.3333333333333 +x_0=1500000.0001464 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +6562 projected NAD83(2011) / Pennsylvania North -80.53,40.6,-74.7,42.53 2 1116 +proj=lcc +lat_0=40.1666666666667 +lon_0=-77.75 +lat_1=41.95 +lat_2=40.8833333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6563 projected NAD83(2011) / Pennsylvania North (ftUS) -80.53,40.6,-74.7,42.53 2 1116 +proj=lcc +lat_0=40.1666666666667 +lon_0=-77.75 +lat_1=41.95 +lat_2=40.8833333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6564 projected NAD83(2011) / Pennsylvania South -80.53,39.71,-74.72,41.18 2 1116 +proj=lcc +lat_0=39.3333333333333 +lon_0=-77.75 +lat_1=40.9666666666667 +lat_2=39.9333333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6565 projected NAD83(2011) / Pennsylvania South (ftUS) -80.53,39.71,-74.72,41.18 2 1116 +proj=lcc +lat_0=39.3333333333333 +lon_0=-77.75 +lat_1=40.9666666666667 +lat_2=39.9333333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6566 projected NAD83(2011) / Puerto Rico and Virgin Is. -67.97,17.62,-64.51,18.57 2 1116 +proj=lcc +lat_0=17.8333333333333 +lon_0=-66.4333333333333 +lat_1=18.4333333333333 +lat_2=18.0333333333333 +x_0=200000 +y_0=200000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6567 projected NAD83(2011) / Rhode Island -71.85,41.13,-71.08,42.02 2 1116 +proj=tmerc +lat_0=41.0833333333333 +lon_0=-71.5 +k=0.99999375 +x_0=100000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6568 projected NAD83(2011) / Rhode Island (ftUS) -71.85,41.13,-71.08,42.02 2 1116 +proj=tmerc +lat_0=41.0833333333333 +lon_0=-71.5 +k=0.99999375 +x_0=99999.99998984 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6569 projected NAD83(2011) / South Carolina -83.36,32.05,-78.52,35.21 2 1116 +proj=lcc +lat_0=31.8333333333333 +lon_0=-81 +lat_1=34.8333333333333 +lat_2=32.5 +x_0=609600 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6570 projected NAD83(2011) / South Carolina (ft) -83.36,32.05,-78.52,35.21 2 1116 +proj=lcc +lat_0=31.8333333333333 +lon_0=-81 +lat_1=34.8333333333333 +lat_2=32.5 +x_0=609600 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +6571 projected NAD83(2011) / South Dakota North -104.07,44.14,-96.45,45.95 2 1116 +proj=lcc +lat_0=43.8333333333333 +lon_0=-100 +lat_1=45.6833333333333 +lat_2=44.4166666666667 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6572 projected NAD83(2011) / South Dakota North (ftUS) -104.07,44.14,-96.45,45.95 2 1116 +proj=lcc +lat_0=43.8333333333333 +lon_0=-100 +lat_1=45.6833333333333 +lat_2=44.4166666666667 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6573 projected NAD83(2011) / South Dakota South -104.06,42.48,-96.43,44.79 2 1116 +proj=lcc +lat_0=42.3333333333333 +lon_0=-100.333333333333 +lat_1=44.4 +lat_2=42.8333333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6574 projected NAD83(2011) / South Dakota South (ftUS) -104.06,42.48,-96.43,44.79 2 1116 +proj=lcc +lat_0=42.3333333333333 +lon_0=-100.333333333333 +lat_1=44.4 +lat_2=42.8333333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6575 projected NAD83(2011) / Tennessee -90.31,34.98,-81.65,36.68 2 1116 +proj=lcc +lat_0=34.3333333333333 +lon_0=-86 +lat_1=36.4166666666667 +lat_2=35.25 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6576 projected NAD83(2011) / Tennessee (ftUS) -90.31,34.98,-81.65,36.68 2 1116 +proj=lcc +lat_0=34.3333333333333 +lon_0=-86 +lat_1=36.4166666666667 +lat_2=35.25 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6577 projected NAD83(2011) / Texas Central -106.66,29.78,-93.5,32.27 2 1116 +proj=lcc +lat_0=29.6666666666667 +lon_0=-100.333333333333 +lat_1=31.8833333333333 +lat_2=30.1166666666667 +x_0=700000 +y_0=3000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6578 projected NAD83(2011) / Texas Central (ftUS) -106.66,29.78,-93.5,32.27 2 1116 +proj=lcc +lat_0=29.6666666666667 +lon_0=-100.333333333333 +lat_1=31.8833333333333 +lat_2=30.1166666666667 +x_0=699999.9998984 +y_0=3000000 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6579 projected NAD83(2011) / Texas Centric Albers Equal Area -106.66,25.83,-93.5,36.5 2 1116 +proj=aea +lat_0=18 +lon_0=-100 +lat_1=27.5 +lat_2=35 +x_0=1500000 +y_0=6000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6580 projected NAD83(2011) / Texas Centric Lambert Conformal -106.66,25.83,-93.5,36.5 2 1116 +proj=lcc +lat_0=18 +lon_0=-100 +lat_1=27.5 +lat_2=35 +x_0=1500000 +y_0=5000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6581 projected NAD83(2011) / Texas North -103.03,34.3,-99.99,36.5 2 1116 +proj=lcc +lat_0=34 +lon_0=-101.5 +lat_1=36.1833333333333 +lat_2=34.65 +x_0=200000 +y_0=1000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6582 projected NAD83(2011) / Texas North (ftUS) -103.03,34.3,-99.99,36.5 2 1116 +proj=lcc +lat_0=34 +lon_0=-101.5 +lat_1=36.1833333333333 +lat_2=34.65 +x_0=200000.0001016 +y_0=999999.9998984 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6583 projected NAD83(2011) / Texas North Central -103.07,31.72,-94,34.58 2 1116 +proj=lcc +lat_0=31.6666666666667 +lon_0=-98.5 +lat_1=33.9666666666667 +lat_2=32.1333333333333 +x_0=600000 +y_0=2000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6584 projected NAD83(2011) / Texas North Central (ftUS) -103.07,31.72,-94,34.58 2 1116 +proj=lcc +lat_0=31.6666666666667 +lon_0=-98.5 +lat_1=33.9666666666667 +lat_2=32.1333333333333 +x_0=600000 +y_0=2000000.0001016 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6585 projected NAD83(2011) / Texas South -100.2,25.83,-96.85,28.21 2 1116 +proj=lcc +lat_0=25.6666666666667 +lon_0=-98.5 +lat_1=27.8333333333333 +lat_2=26.1666666666667 +x_0=300000 +y_0=5000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6586 projected NAD83(2011) / Texas South (ftUS) -100.2,25.83,-96.85,28.21 2 1116 +proj=lcc +lat_0=25.6666666666667 +lon_0=-98.5 +lat_1=27.8333333333333 +lat_2=26.1666666666667 +x_0=300000 +y_0=5000000.0001016 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6587 projected NAD83(2011) / Texas South Central -105,27.78,-93.76,30.67 2 1116 +proj=lcc +lat_0=27.8333333333333 +lon_0=-99 +lat_1=30.2833333333333 +lat_2=28.3833333333333 +x_0=600000 +y_0=4000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6588 projected NAD83(2011) / Texas South Central (ftUS) -105,27.78,-93.76,30.67 2 1116 +proj=lcc +lat_0=27.8333333333333 +lon_0=-99 +lat_1=30.2833333333333 +lat_2=28.3833333333333 +x_0=600000 +y_0=3999999.9998984 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6589 projected NAD83(2011) / Vermont -73.44,42.72,-71.5,45.03 2 1116 +proj=tmerc +lat_0=42.5 +lon_0=-72.5 +k=0.999964286 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6590 projected NAD83(2011) / Vermont (ftUS) -73.44,42.72,-71.5,45.03 2 1116 +proj=tmerc +lat_0=42.5 +lon_0=-72.5 +k=0.999964286 +x_0=500000.00001016 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6591 projected NAD83(2011) / Virginia Lambert -83.68,36.54,-75.31,39.46 2 1116 +proj=lcc +lat_0=36 +lon_0=-79.5 +lat_1=37 +lat_2=39.5 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6592 projected NAD83(2011) / Virginia North -80.06,37.77,-76.51,39.46 2 1116 +proj=lcc +lat_0=37.6666666666667 +lon_0=-78.5 +lat_1=39.2 +lat_2=38.0333333333333 +x_0=3500000 +y_0=2000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6593 projected NAD83(2011) / Virginia North (ftUS) -80.06,37.77,-76.51,39.46 2 1116 +proj=lcc +lat_0=37.6666666666667 +lon_0=-78.5 +lat_1=39.2 +lat_2=38.0333333333333 +x_0=3500000.0001016 +y_0=2000000.0001016 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6594 projected NAD83(2011) / Virginia South -83.68,36.54,-75.31,38.28 2 1116 +proj=lcc +lat_0=36.3333333333333 +lon_0=-78.5 +lat_1=37.9666666666667 +lat_2=36.7666666666667 +x_0=3500000 +y_0=1000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6595 projected NAD83(2011) / Virginia South (ftUS) -83.68,36.54,-75.31,38.28 2 1116 +proj=lcc +lat_0=36.3333333333333 +lon_0=-78.5 +lat_1=37.9666666666667 +lat_2=36.7666666666667 +x_0=3500000.0001016 +y_0=999999.9998984 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6596 projected NAD83(2011) / Washington North -124.79,47.08,-117.02,49.05 2 1116 +proj=lcc +lat_0=47 +lon_0=-120.833333333333 +lat_1=48.7333333333333 +lat_2=47.5 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6597 projected NAD83(2011) / Washington North (ftUS) -124.79,47.08,-117.02,49.05 2 1116 +proj=lcc +lat_0=47 +lon_0=-120.833333333333 +lat_1=48.7333333333333 +lat_2=47.5 +x_0=500000.0001016 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6598 projected NAD83(2011) / Washington South -124.4,45.54,-116.91,47.61 2 1116 +proj=lcc +lat_0=45.3333333333333 +lon_0=-120.5 +lat_1=47.3333333333333 +lat_2=45.8333333333333 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6599 projected NAD83(2011) / Washington South (ftUS) -124.4,45.54,-116.91,47.61 2 1116 +proj=lcc +lat_0=45.3333333333333 +lon_0=-120.5 +lat_1=47.3333333333333 +lat_2=45.8333333333333 +x_0=500000.0001016 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6600 projected NAD83(2011) / West Virginia North -81.76,38.76,-77.72,40.64 2 1116 +proj=lcc +lat_0=38.5 +lon_0=-79.5 +lat_1=40.25 +lat_2=39 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6601 projected NAD83(2011) / West Virginia North (ftUS) -81.76,38.76,-77.72,40.64 2 1116 +proj=lcc +lat_0=38.5 +lon_0=-79.5 +lat_1=40.25 +lat_2=39 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6602 projected NAD83(2011) / West Virginia South -82.65,37.2,-79.05,39.17 2 1116 +proj=lcc +lat_0=37 +lon_0=-81 +lat_1=38.8833333333333 +lat_2=37.4833333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6603 projected NAD83(2011) / West Virginia South (ftUS) -82.65,37.2,-79.05,39.17 2 1116 +proj=lcc +lat_0=37 +lon_0=-81 +lat_1=38.8833333333333 +lat_2=37.4833333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6605 projected NAD83(2011) / Wisconsin Central (ftUS) -92.89,43.98,-86.25,45.8 2 1116 +proj=lcc +lat_0=43.8333333333333 +lon_0=-90 +lat_1=45.5 +lat_2=44.25 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6606 projected NAD83(2011) / Wisconsin North -92.89,45.37,-88.05,47.31 2 1116 +proj=lcc +lat_0=45.1666666666667 +lon_0=-90 +lat_1=46.7666666666667 +lat_2=45.5666666666667 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6607 projected NAD83(2011) / Wisconsin North (ftUS) -92.89,45.37,-88.05,47.31 2 1116 +proj=lcc +lat_0=45.1666666666667 +lon_0=-90 +lat_1=46.7666666666667 +lat_2=45.5666666666667 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6608 projected NAD83(2011) / Wisconsin South -91.43,42.48,-86.95,44.33 2 1116 +proj=lcc +lat_0=42 +lon_0=-90 +lat_1=44.0666666666667 +lat_2=42.7333333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6609 projected NAD83(2011) / Wisconsin South (ftUS) -91.43,42.48,-86.95,44.33 2 1116 +proj=lcc +lat_0=42 +lon_0=-90 +lat_1=44.0666666666667 +lat_2=42.7333333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6610 projected NAD83(2011) / Wisconsin Transverse Mercator -92.89,42.48,-86.25,47.31 2 1116 +proj=tmerc +lat_0=0 +lon_0=-90 +k=0.9996 +x_0=520000 +y_0=-4480000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6611 projected NAD83(2011) / Wyoming East -106.33,40.99,-104.05,45.01 2 1116 +proj=tmerc +lat_0=40.5 +lon_0=-105.166666666667 +k=0.9999375 +x_0=200000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6612 projected NAD83(2011) / Wyoming East (ftUS) -106.33,40.99,-104.05,45.01 2 1116 +proj=tmerc +lat_0=40.5 +lon_0=-105.166666666667 +k=0.9999375 +x_0=200000.00001016 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6613 projected NAD83(2011) / Wyoming East Central -108.63,40.99,-106,45.01 2 1116 +proj=tmerc +lat_0=40.5 +lon_0=-107.333333333333 +k=0.9999375 +x_0=400000 +y_0=100000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6614 projected NAD83(2011) / Wyoming East Central (ftUS) -108.63,40.99,-106,45.01 2 1116 +proj=tmerc +lat_0=40.5 +lon_0=-107.333333333333 +k=0.9999375 +x_0=399999.99998984 +y_0=99999.99998984 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6615 projected NAD83(2011) / Wyoming West -111.06,40.99,-109.04,44.67 2 1116 +proj=tmerc +lat_0=40.5 +lon_0=-110.083333333333 +k=0.9999375 +x_0=800000 +y_0=100000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6616 projected NAD83(2011) / Wyoming West (ftUS) -111.06,40.99,-109.04,44.67 2 1116 +proj=tmerc +lat_0=40.5 +lon_0=-110.083333333333 +k=0.9999375 +x_0=800000.00001016 +y_0=99999.99998984 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6617 projected NAD83(2011) / Wyoming West Central -111.06,40.99,-107.5,45.01 2 1116 +proj=tmerc +lat_0=40.5 +lon_0=-108.75 +k=0.9999375 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6618 projected NAD83(2011) / Wyoming West Central (ftUS) -111.06,40.99,-107.5,45.01 2 1116 +proj=tmerc +lat_0=40.5 +lon_0=-108.75 +k=0.9999375 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6619 projected NAD83(2011) / Utah Central -114.05,38.49,-109.04,41.08 2 1116 +proj=lcc +lat_0=38.3333333333333 +lon_0=-111.5 +lat_1=40.65 +lat_2=39.0166666666667 +x_0=500000 +y_0=2000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6620 projected NAD83(2011) / Utah North -114.04,40.55,-109.04,42.01 2 1116 +proj=lcc +lat_0=40.3333333333333 +lon_0=-111.5 +lat_1=41.7833333333333 +lat_2=40.7166666666667 +x_0=500000 +y_0=1000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6621 projected NAD83(2011) / Utah South -114.05,36.99,-109.04,38.58 2 1116 +proj=lcc +lat_0=36.6666666666667 +lon_0=-111.5 +lat_1=38.35 +lat_2=37.2166666666667 +x_0=500000 +y_0=3000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6622 projected NAD83(CSRS)v2 / Quebec Lambert -79.85,44.99,-57.1,62.62 1193 +proj=lcc +lat_0=44 +lon_0=-68.5 +lat_1=60 +lat_2=46 +x_0=0 +y_0=0 +ellps=GRS80 +units=m +no_defs +6623 projected NAD83 / Quebec Albers -79.85,44.99,-57.1,62.62 4 6269 +proj=aea +lat_0=44 +lon_0=-68.5 +lat_1=60 +lat_2=46 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6624 projected NAD83(CSRS)v2 / Quebec Albers -79.85,44.99,-57.1,62.62 1193 +proj=aea +lat_0=44 +lon_0=-68.5 +lat_1=60 +lat_2=46 +x_0=0 +y_0=0 +ellps=GRS80 +units=m +no_defs +6625 projected NAD83(2011) / Utah Central (ftUS) -114.05,38.49,-109.04,41.08 2 1116 +proj=lcc +lat_0=38.3333333333333 +lon_0=-111.5 +lat_1=40.65 +lat_2=39.0166666666667 +x_0=500000.00001016 +y_0=2000000.00001016 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6626 projected NAD83(2011) / Utah North (ftUS) -114.04,40.55,-109.04,42.01 2 1116 +proj=lcc +lat_0=40.3333333333333 +lon_0=-111.5 +lat_1=41.7833333333333 +lat_2=40.7166666666667 +x_0=500000.00001016 +y_0=999999.99998984 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6627 projected NAD83(2011) / Utah South (ftUS) -114.05,36.99,-109.04,38.58 2 1116 +proj=lcc +lat_0=36.6666666666667 +lon_0=-111.5 +lat_1=38.35 +lat_2=37.2166666666667 +x_0=500000.00001016 +y_0=3000000 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6628 projected NAD83(PA11) / Hawaii zone 1 -156.1,18.87,-154.74,20.33 1117 +proj=tmerc +lat_0=18.8333333333333 +lon_0=-155.5 +k=0.999966667 +x_0=500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +6629 projected NAD83(PA11) / Hawaii zone 2 -157.36,20.45,-155.93,21.26 1117 +proj=tmerc +lat_0=20.3333333333333 +lon_0=-156.666666666667 +k=0.999966667 +x_0=500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +6630 projected NAD83(PA11) / Hawaii zone 3 -158.33,21.2,-157.61,21.75 1117 +proj=tmerc +lat_0=21.1666666666667 +lon_0=-158 +k=0.99999 +x_0=500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +6631 projected NAD83(PA11) / Hawaii zone 4 -159.85,21.81,-159.23,22.29 1117 +proj=tmerc +lat_0=21.8333333333333 +lon_0=-159.5 +k=0.99999 +x_0=500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +6632 projected NAD83(PA11) / Hawaii zone 5 -160.3,21.73,-159.99,22.07 1117 +proj=tmerc +lat_0=21.6666666666667 +lon_0=-160.166666666667 +k=1 +x_0=500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +6633 projected NAD83(PA11) / Hawaii zone 3 (ftUS) -158.33,21.2,-157.61,21.75 1117 +proj=tmerc +lat_0=21.1666666666667 +lon_0=-158 +k=0.99999 +x_0=500000.00001016 +y_0=0 +ellps=GRS80 +to_meter=0.304800609601219 +no_defs +6634 projected NAD83(PA11) / UTM zone 4N -160.3,19.51,-155.99,22.29 1117 +proj=utm +zone=4 +ellps=GRS80 +units=m +no_defs +6635 projected NAD83(PA11) / UTM zone 5N -156,18.87,-154.74,20.86 1117 +proj=utm +zone=5 +ellps=GRS80 +units=m +no_defs +6636 projected NAD83(PA11) / UTM zone 2S -170.88,-14.59,-168.09,-14.11 1117 +proj=utm +zone=2 +south +ellps=GRS80 +units=m +no_defs +6637 projected NAD83(MA11) / Guam Map Grid 144.58,13.18,145.01,13.7 1118 +proj=tmerc +lat_0=13.5 +lon_0=144.75 +k=1 +x_0=100000 +y_0=200000 +ellps=GRS80 +units=m +no_defs +6646 projected Karbala 1979 / Iraq National Grid 38.79,29.06,48.61,37.39 1 6743 +proj=tmerc +lat_0=29.0262683333333 +lon_0=46.5 +k=0.9994 +x_0=800000 +y_0=0 +a=6378249.145 +rf=293.465 +towgs84=70.995,-335.916,262.898 +units=m +no_defs +6668 geographic JGD2011 122.38,17.09,157.65,46.05 1 1128 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +6669 projected JGD2011 / Japan Plane Rectangular CS I 128.17,26.96,130.46,34.74 1 1128 +proj=tmerc +lat_0=33 +lon_0=129.5 +k=0.9999 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6670 projected JGD2011 / Japan Plane Rectangular CS II 129.76,30.18,132.05,33.99 1 1128 +proj=tmerc +lat_0=33 +lon_0=131 +k=0.9999 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6671 projected JGD2011 / Japan Plane Rectangular CS III 130.81,33.72,133.49,36.38 1 1128 +proj=tmerc +lat_0=36 +lon_0=132.166666666667 +k=0.9999 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6672 projected JGD2011 / Japan Plane Rectangular CS IV 131.95,32.69,134.81,34.45 1 1128 +proj=tmerc +lat_0=33 +lon_0=133.5 +k=0.9999 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6673 projected JGD2011 / Japan Plane Rectangular CS V 133.13,34.13,135.47,35.71 1 1128 +proj=tmerc +lat_0=36 +lon_0=134.333333333333 +k=0.9999 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6674 projected JGD2011 / Japan Plane Rectangular CS VI 134.86,33.4,136.99,36.33 1 1128 +proj=tmerc +lat_0=36 +lon_0=136 +k=0.9999 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6675 projected JGD2011 / Japan Plane Rectangular CS VII 136.22,34.51,137.84,37.58 1 1128 +proj=tmerc +lat_0=36 +lon_0=137.166666666667 +k=0.9999 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6676 projected JGD2011 / Japan Plane Rectangular CS VIII 137.32,34.54,139.91,38.58 1 1128 +proj=tmerc +lat_0=36 +lon_0=138.5 +k=0.9999 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6677 projected JGD2011 / Japan Plane Rectangular CS IX 138.4,29.31,141.11,37.98 1 1128 +proj=tmerc +lat_0=36 +lon_0=139.833333333333 +k=0.9999 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6678 projected JGD2011 / Japan Plane Rectangular CS X 139.49,37.73,142.14,41.58 1 1128 +proj=tmerc +lat_0=40 +lon_0=140.833333333333 +k=0.9999 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6679 projected JGD2011 / Japan Plane Rectangular CS XI 139.34,41.34,141.46,43.42 1 1128 +proj=tmerc +lat_0=44 +lon_0=140.25 +k=0.9999 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6680 projected JGD2011 / Japan Plane Rectangular CS XII 140.89,42.15,143.61,45.54 1 1128 +proj=tmerc +lat_0=44 +lon_0=142.25 +k=0.9999 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6681 projected JGD2011 / Japan Plane Rectangular CS XIII 142.61,41.87,145.87,44.4 1 1128 +proj=tmerc +lat_0=44 +lon_0=144.25 +k=0.9999 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6682 projected JGD2011 / Japan Plane Rectangular CS XIV 141.2,24.67,142.33,27.8 1 1128 +proj=tmerc +lat_0=26 +lon_0=142 +k=0.9999 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6683 projected JGD2011 / Japan Plane Rectangular CS XV 126.63,26.02,128.4,26.91 1 1128 +proj=tmerc +lat_0=26 +lon_0=127.5 +k=0.9999 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6684 projected JGD2011 / Japan Plane Rectangular CS XVI 122.83,23.98,125.51,24.94 1 1128 +proj=tmerc +lat_0=26 +lon_0=124 +k=0.9999 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6685 projected JGD2011 / Japan Plane Rectangular CS XVII 131.12,24.4,131.38,26.01 1 1128 +proj=tmerc +lat_0=26 +lon_0=131 +k=0.9999 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6686 projected JGD2011 / Japan Plane Rectangular CS XVIII 136.02,20.37,136.16,20.48 1 1128 +proj=tmerc +lat_0=20 +lon_0=136 +k=0.9999 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6687 projected JGD2011 / Japan Plane Rectangular CS XIX 153.91,24.22,154.05,24.35 1 1128 +proj=tmerc +lat_0=26 +lon_0=154 +k=0.9999 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6688 projected JGD2011 / UTM zone 51N 122.38,21.1,126,29.71 1 1128 +proj=utm +zone=51 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6689 projected JGD2011 / UTM zone 52N 126,21.12,132,38.63 1 1128 +proj=utm +zone=52 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6690 projected JGD2011 / UTM zone 53N 132,17.09,138,43.55 1 1128 +proj=utm +zone=53 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6691 projected JGD2011 / UTM zone 54N 138,17.63,144,46.05 1 1128 +proj=utm +zone=54 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6692 projected JGD2011 / UTM zone 55N 144,23.03,147.86,45.65 1 1128 +proj=utm +zone=55 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6703 projected WGS 84 / TM 60 SW -63.01,-56.25,-56.99,-47.68 6326 +proj=tmerc +lat_0=0 +lon_0=-60 +k=0.9996 +x_0=500000 +y_0=10000000 +datum=WGS84 +units=m +no_defs +6706 geographic RDN2008 5.93,34.76,18.99,47.1 1 1132 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +6707 projected RDN2008 / UTM zone 32N (N-E) 5.93,36.53,12,47.04 1 1132 +proj=utm +zone=32 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6708 projected RDN2008 / UTM zone 33N (N-E) 12,34.79,18,47.1 1 1132 +proj=utm +zone=33 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6709 projected RDN2008 / UTM zone 34N (N-E) 17.99,34.76,18.99,41.64 1 1132 +proj=utm +zone=34 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6720 projected WGS 84 / CIG92 105.48,-10.63,105.77,-10.36 6326 +proj=tmerc +lat_0=0 +lon_0=105.625 +k=1.000024 +x_0=50000 +y_0=1300000 +datum=WGS84 +units=m +no_defs +6721 projected GDA94 / CIG94 105.48,-10.63,105.77,-10.36 3 6283 +proj=tmerc +lat_0=0 +lon_0=105.625 +k=1.00002514 +x_0=50000 +y_0=1300000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6722 projected WGS 84 / CKIG92 96.76,-12.27,96.99,-11.76 6326 +proj=tmerc +lat_0=0 +lon_0=96.875 +k=1 +x_0=50000 +y_0=1400000 +datum=WGS84 +units=m +no_defs +6723 projected GDA94 / CKIG94 96.76,-12.27,96.99,-11.76 3 6283 +proj=tmerc +lat_0=0 +lon_0=96.875 +k=0.99999387 +x_0=50000 +y_0=1500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6736 projected GDA94 / MGA zone 46 93.41,-15.46,96.01,-8.57 3 6283 +proj=utm +zone=46 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6737 projected GDA94 / MGA zone 47 96,-15.56,100.34,-8.47 3 6283 +proj=utm +zone=47 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6738 projected GDA94 / MGA zone 59 168,-32.48,173.35,-25.61 3 6283 +proj=utm +zone=59 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6783 geographic NAD83(CORS96) 167.65,14.92,-63.88,74.71 1133 +proj=longlat +ellps=GRS80 +no_defs +6784 projected NAD83(CORS96) / Oregon Baker zone (m) -118.15,44.6,-117.37,45.19 1133 +proj=tmerc +lat_0=44.5 +lon_0=-117.833333333333 +k=1.00016 +x_0=40000 +y_0=0 +ellps=GRS80 +units=m +no_defs +6785 projected NAD83(CORS96) / Oregon Baker zone (ft) -118.15,44.6,-117.37,45.19 1133 +proj=tmerc +lat_0=44.5 +lon_0=-117.833333333333 +k=1.00016 +x_0=39999.99999984 +y_0=0 +ellps=GRS80 +to_meter=0.3048 +no_defs +6786 projected NAD83(2011) / Oregon Baker zone (m) -118.15,44.6,-117.37,45.19 2 1116 +proj=tmerc +lat_0=44.5 +lon_0=-117.833333333333 +k=1.00016 +x_0=40000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6787 projected NAD83(2011) / Oregon Baker zone (ft) -118.15,44.6,-117.37,45.19 2 1116 +proj=tmerc +lat_0=44.5 +lon_0=-117.833333333333 +k=1.00016 +x_0=39999.99999984 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +6788 projected NAD83(CORS96) / Oregon Bend-Klamath Falls zone (m) -122.45,41.88,-120.77,43.89 1133 +proj=tmerc +lat_0=41.75 +lon_0=-121.75 +k=1.0002 +x_0=80000 +y_0=0 +ellps=GRS80 +units=m +no_defs +6789 projected NAD83(CORS96) / Oregon Bend-Klamath Falls zone (ft) -122.45,41.88,-120.77,43.89 1133 +proj=tmerc +lat_0=41.75 +lon_0=-121.75 +k=1.0002 +x_0=79999.99999968 +y_0=0 +ellps=GRS80 +to_meter=0.3048 +no_defs +6790 projected NAD83(2011) / Oregon Bend-Klamath Falls zone (m) -122.45,41.88,-120.77,43.89 2 1116 +proj=tmerc +lat_0=41.75 +lon_0=-121.75 +k=1.0002 +x_0=80000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6791 projected NAD83(2011) / Oregon Bend-Klamath Falls zone (ft) -122.45,41.88,-120.77,43.89 2 1116 +proj=tmerc +lat_0=41.75 +lon_0=-121.75 +k=1.0002 +x_0=79999.99999968 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +6792 projected NAD83(CORS96) / Oregon Bend-Redmond-Prineville zone (m) -121.88,43.76,-119.79,44.98 1133 +proj=lcc +lat_1=44.6666666666667 +lat_0=44.6666666666667 +lon_0=-121.25 +k_0=1.00012 +x_0=80000 +y_0=130000 +ellps=GRS80 +units=m +no_defs +6793 projected NAD83(CORS96) / Oregon Bend-Redmond-Prineville zone (ft) -121.88,43.76,-119.79,44.98 1133 +proj=lcc +lat_1=44.6666666666667 +lat_0=44.6666666666667 +lon_0=-121.25 +k_0=1.00012 +x_0=79999.99999968 +y_0=130000.00001472 +ellps=GRS80 +to_meter=0.3048 +no_defs +6794 projected NAD83(2011) / Oregon Bend-Redmond-Prineville zone (m) -121.88,43.76,-119.79,44.98 2 1116 +proj=lcc +lat_1=44.6666666666667 +lat_0=44.6666666666667 +lon_0=-121.25 +k_0=1.00012 +x_0=80000 +y_0=130000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6795 projected NAD83(2011) / Oregon Bend-Redmond-Prineville zone (ft) -121.88,43.76,-119.79,44.98 2 1116 +proj=lcc +lat_1=44.6666666666667 +lat_0=44.6666666666667 +lon_0=-121.25 +k_0=1.00012 +x_0=79999.99999968 +y_0=130000.00001472 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +6796 projected NAD83(CORS96) / Oregon Bend-Burns zone (m) -120.95,43.34,-118.8,44.28 1133 +proj=lcc +lat_1=43.6666666666667 +lat_0=43.6666666666667 +lon_0=-119.75 +k_0=1.0002 +x_0=120000 +y_0=60000 +ellps=GRS80 +units=m +no_defs +6797 projected NAD83(CORS96) / Oregon Bend-Burns zone (ft) -120.95,43.34,-118.8,44.28 1133 +proj=lcc +lat_1=43.6666666666667 +lat_0=43.6666666666667 +lon_0=-119.75 +k_0=1.0002 +x_0=119999.99999952 +y_0=59999.99999976 +ellps=GRS80 +to_meter=0.3048 +no_defs +6798 projected NAD83(2011) / Oregon Bend-Burns zone (m) -120.95,43.34,-118.8,44.28 2 1116 +proj=lcc +lat_1=43.6666666666667 +lat_0=43.6666666666667 +lon_0=-119.75 +k_0=1.0002 +x_0=120000 +y_0=60000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6799 projected NAD83(2011) / Oregon Bend-Burns zone (ft) -120.95,43.34,-118.8,44.28 2 1116 +proj=lcc +lat_1=43.6666666666667 +lat_0=43.6666666666667 +lon_0=-119.75 +k_0=1.0002 +x_0=119999.99999952 +y_0=59999.99999976 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +6800 projected NAD83(CORS96) / Oregon Canyonville-Grants Pass zone (m) -123.83,42.49,-122.43,43.24 1133 +proj=tmerc +lat_0=42.5 +lon_0=-123.333333333333 +k=1.00007 +x_0=40000 +y_0=0 +ellps=GRS80 +units=m +no_defs +6801 projected NAD83(CORS96) / Oregon Canyonville-Grants Pass zone (ft) -123.83,42.49,-122.43,43.24 1133 +proj=tmerc +lat_0=42.5 +lon_0=-123.333333333333 +k=1.00007 +x_0=39999.99999984 +y_0=0 +ellps=GRS80 +to_meter=0.3048 +no_defs +6802 projected NAD83(2011) / Oregon Canyonville-Grants Pass zone (m) -123.83,42.49,-122.43,43.24 2 1116 +proj=tmerc +lat_0=42.5 +lon_0=-123.333333333333 +k=1.00007 +x_0=40000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6803 projected NAD83(2011) / Oregon Canyonville-Grants Pass zone (ft) -123.83,42.49,-122.43,43.24 2 1116 +proj=tmerc +lat_0=42.5 +lon_0=-123.333333333333 +k=1.00007 +x_0=39999.99999984 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +6804 projected NAD83(CORS96) / Oregon Columbia River East zone (m) -122.05,45.49,-118.89,46.08 1133 +proj=lcc +lat_1=45.6666666666667 +lat_0=45.6666666666667 +lon_0=-120.5 +k_0=1.000008 +x_0=150000 +y_0=30000 +ellps=GRS80 +units=m +no_defs +6805 projected NAD83(CORS96) / Oregon Columbia River East zone (ft) -122.05,45.49,-118.89,46.08 1133 +proj=lcc +lat_1=45.6666666666667 +lat_0=45.6666666666667 +lon_0=-120.5 +k_0=1.000008 +x_0=150000.00001464 +y_0=30000.00001512 +ellps=GRS80 +to_meter=0.3048 +no_defs +6806 projected NAD83(2011) / Oregon Columbia River East zone (m) -122.05,45.49,-118.89,46.08 2 1116 +proj=lcc +lat_1=45.6666666666667 +lat_0=45.6666666666667 +lon_0=-120.5 +k_0=1.000008 +x_0=150000 +y_0=30000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6807 projected NAD83(2011) / Oregon Columbia River East zone (ft) -122.05,45.49,-118.89,46.08 2 1116 +proj=lcc +lat_1=45.6666666666667 +lat_0=45.6666666666667 +lon_0=-120.5 +k_0=1.000008 +x_0=150000.00001464 +y_0=30000.00001512 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +6808 projected NAD83(CORS96) / Oregon Columbia River West zone (m) -124.33,45.17,-121.47,46.56 1133 +proj=omerc +no_uoff +lat_0=45.9166666666667 +lonc=-123 +alpha=295 +gamma=295 +k=1 +x_0=7000000 +y_0=-3000000 +ellps=GRS80 +units=m +no_defs +6809 projected NAD83(CORS96) / Oregon Columbia River West zone (ft) -124.33,45.17,-121.47,46.56 1133 +proj=omerc +no_uoff +lat_0=45.9166666666667 +lonc=-123 +alpha=295 +gamma=295 +k=1 +x_0=7000000.00000248 +y_0=-2999999.999988 +ellps=GRS80 +to_meter=0.3048 +no_defs +6810 projected NAD83(2011) / Oregon Columbia River West zone (m) -124.33,45.17,-121.47,46.56 2 1116 +proj=omerc +no_uoff +lat_0=45.9166666666667 +lonc=-123 +alpha=295 +gamma=295 +k=1 +x_0=7000000 +y_0=-3000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6811 projected NAD83(2011) / Oregon Columbia River West zone (ft) -124.33,45.17,-121.47,46.56 2 1116 +proj=omerc +no_uoff +lat_0=45.9166666666667 +lonc=-123 +alpha=295 +gamma=295 +k=1 +x_0=7000000.00000248 +y_0=-2999999.999988 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +6812 projected NAD83(CORS96) / Oregon Cottage Grove-Canyonville zone (m) -123.96,42.82,-122.4,43.88 1133 +proj=tmerc +lat_0=42.8333333333333 +lon_0=-123.333333333333 +k=1.000023 +x_0=50000 +y_0=0 +ellps=GRS80 +units=m +no_defs +6813 projected NAD83(CORS96) / Oregon Cottage Grove-Canyonville zone (ft) -123.96,42.82,-122.4,43.88 1133 +proj=tmerc +lat_0=42.8333333333333 +lon_0=-123.333333333333 +k=1.000023 +x_0=50000.00001504 +y_0=0 +ellps=GRS80 +to_meter=0.3048 +no_defs +6814 projected NAD83(2011) / Oregon Cottage Grove-Canyonville zone (m) -123.96,42.82,-122.4,43.88 2 1116 +proj=tmerc +lat_0=42.8333333333333 +lon_0=-123.333333333333 +k=1.000023 +x_0=50000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6815 projected NAD83(2011) / Oregon Cottage Grove-Canyonville zone (ft) -123.96,42.82,-122.4,43.88 2 1116 +proj=tmerc +lat_0=42.8333333333333 +lon_0=-123.333333333333 +k=1.000023 +x_0=50000.00001504 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +6816 projected NAD83(CORS96) / Oregon Dufur-Madras zone (m) -121.95,44.63,-120.46,45.55 1133 +proj=tmerc +lat_0=44.5 +lon_0=-121 +k=1.00011 +x_0=80000 +y_0=0 +ellps=GRS80 +units=m +no_defs +6817 projected NAD83(CORS96) / Oregon Dufur-Madras zone (ft) -121.95,44.63,-120.46,45.55 1133 +proj=tmerc +lat_0=44.5 +lon_0=-121 +k=1.00011 +x_0=79999.99999968 +y_0=0 +ellps=GRS80 +to_meter=0.3048 +no_defs +6818 projected NAD83(2011) / Oregon Dufur-Madras zone (m) -121.95,44.63,-120.46,45.55 2 1116 +proj=tmerc +lat_0=44.5 +lon_0=-121 +k=1.00011 +x_0=80000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6819 projected NAD83(2011) / Oregon Dufur-Madras zone (ft) -121.95,44.63,-120.46,45.55 2 1116 +proj=tmerc +lat_0=44.5 +lon_0=-121 +k=1.00011 +x_0=79999.99999968 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +6820 projected NAD83(CORS96) / Oregon Eugene zone (m) -123.8,43.74,-122.18,44.71 1133 +proj=tmerc +lat_0=43.75 +lon_0=-123.166666666667 +k=1.000015 +x_0=50000 +y_0=0 +ellps=GRS80 +units=m +no_defs +6821 projected NAD83(CORS96) / Oregon Eugene zone (ft) -123.8,43.74,-122.18,44.71 1133 +proj=tmerc +lat_0=43.75 +lon_0=-123.166666666667 +k=1.000015 +x_0=50000.00001504 +y_0=0 +ellps=GRS80 +to_meter=0.3048 +no_defs +6822 projected NAD83(2011) / Oregon Eugene zone (m) -123.8,43.74,-122.18,44.71 2 1116 +proj=tmerc +lat_0=43.75 +lon_0=-123.166666666667 +k=1.000015 +x_0=50000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6823 projected NAD83(2011) / Oregon Eugene zone (ft) -123.8,43.74,-122.18,44.71 2 1116 +proj=tmerc +lat_0=43.75 +lon_0=-123.166666666667 +k=1.000015 +x_0=50000.00001504 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +6824 projected NAD83(CORS96) / Oregon Grants Pass-Ashland zone (m) -123.95,41.88,-122.37,42.85 1133 +proj=tmerc +lat_0=41.75 +lon_0=-123.333333333333 +k=1.000043 +x_0=50000 +y_0=0 +ellps=GRS80 +units=m +no_defs +6825 projected NAD83(CORS96) / Oregon Grants Pass-Ashland zone (ft) -123.95,41.88,-122.37,42.85 1133 +proj=tmerc +lat_0=41.75 +lon_0=-123.333333333333 +k=1.000043 +x_0=50000.00001504 +y_0=0 +ellps=GRS80 +to_meter=0.3048 +no_defs +6826 projected NAD83(2011) / Oregon Grants Pass-Ashland zone (m) -123.95,41.88,-122.37,42.85 2 1116 +proj=tmerc +lat_0=41.75 +lon_0=-123.333333333333 +k=1.000043 +x_0=50000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6827 projected NAD83(2011) / Oregon Grants Pass-Ashland zone (ft) -123.95,41.88,-122.37,42.85 2 1116 +proj=tmerc +lat_0=41.75 +lon_0=-123.333333333333 +k=1.000043 +x_0=50000.00001504 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +6828 projected NAD83(CORS96) / Oregon Gresham-Warm Springs zone (m) -122.43,45.02,-121.68,45.55 1133 +proj=tmerc +lat_0=45 +lon_0=-122.333333333333 +k=1.00005 +x_0=10000 +y_0=0 +ellps=GRS80 +units=m +no_defs +6829 projected NAD83(CORS96) / Oregon Gresham-Warm Springs zone (ft) -122.43,45.02,-121.68,45.55 1133 +proj=tmerc +lat_0=45 +lon_0=-122.333333333333 +k=1.00005 +x_0=10000.0000152 +y_0=0 +ellps=GRS80 +to_meter=0.3048 +no_defs +6830 projected NAD83(2011) / Oregon Gresham-Warm Springs zone (m) -122.43,45.02,-121.68,45.55 2 1116 +proj=tmerc +lat_0=45 +lon_0=-122.333333333333 +k=1.00005 +x_0=10000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6831 projected NAD83(2011) / Oregon Gresham-Warm Springs zone (ft) -122.43,45.02,-121.68,45.55 2 1116 +proj=tmerc +lat_0=45 +lon_0=-122.333333333333 +k=1.00005 +x_0=10000.0000152 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +6832 projected NAD83(CORS96) / Oregon La Grande zone (m) -118.17,45.13,-117.14,45.8 1133 +proj=tmerc +lat_0=45 +lon_0=-118 +k=1.00013 +x_0=40000 +y_0=0 +ellps=GRS80 +units=m +no_defs +6833 projected NAD83(CORS96) / Oregon La Grande zone (ft) -118.17,45.13,-117.14,45.8 1133 +proj=tmerc +lat_0=45 +lon_0=-118 +k=1.00013 +x_0=39999.99999984 +y_0=0 +ellps=GRS80 +to_meter=0.3048 +no_defs +6834 projected NAD83(2011) / Oregon La Grande zone (m) -118.17,45.13,-117.14,45.8 2 1116 +proj=tmerc +lat_0=45 +lon_0=-118 +k=1.00013 +x_0=40000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6835 projected NAD83(2011) / Oregon La Grande zone (ft) -118.17,45.13,-117.14,45.8 2 1116 +proj=tmerc +lat_0=45 +lon_0=-118 +k=1.00013 +x_0=39999.99999984 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +6836 projected NAD83(CORS96) / Oregon Ontario zone (m) -117.9,43.41,-116.7,44.65 1133 +proj=tmerc +lat_0=43.25 +lon_0=-117 +k=1.0001 +x_0=80000 +y_0=0 +ellps=GRS80 +units=m +no_defs +6837 projected NAD83(CORS96) / Oregon Ontario zone (ft) -117.9,43.41,-116.7,44.65 1133 +proj=tmerc +lat_0=43.25 +lon_0=-117 +k=1.0001 +x_0=79999.99999968 +y_0=0 +ellps=GRS80 +to_meter=0.3048 +no_defs +6838 projected NAD83(2011) / Oregon Ontario zone (m) -117.9,43.41,-116.7,44.65 2 1116 +proj=tmerc +lat_0=43.25 +lon_0=-117 +k=1.0001 +x_0=80000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6839 projected NAD83(2011) / Oregon Ontario zone (ft) -117.9,43.41,-116.7,44.65 2 1116 +proj=tmerc +lat_0=43.25 +lon_0=-117 +k=1.0001 +x_0=79999.99999968 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +6840 projected NAD83(CORS96) / Oregon Coast zone (m) -124.84,41.89,-123.35,46.4 1133 +proj=omerc +no_uoff +lat_0=44.75 +lonc=-124.05 +alpha=5 +gamma=5 +k=1 +x_0=-300000 +y_0=-4600000 +ellps=GRS80 +units=m +no_defs +6841 projected NAD83(CORS96) / Oregon Coast zone (ft) -124.84,41.89,-123.35,46.4 1133 +proj=omerc +no_uoff +lat_0=44.75 +lonc=-124.05 +alpha=5 +gamma=5 +k=1 +x_0=-299999.9999988 +y_0=-4600000.00001208 +ellps=GRS80 +to_meter=0.3048 +no_defs +6842 projected NAD83(2011) / Oregon Coast zone (m) -124.84,41.89,-123.35,46.4 2 1116 +proj=omerc +no_uoff +lat_0=44.75 +lonc=-124.05 +alpha=5 +gamma=5 +k=1 +x_0=-300000 +y_0=-4600000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6843 projected NAD83(2011) / Oregon Coast zone (ft) -124.84,41.89,-123.35,46.4 2 1116 +proj=omerc +no_uoff +lat_0=44.75 +lonc=-124.05 +alpha=5 +gamma=5 +k=1 +x_0=-299999.9999988 +y_0=-4600000.00001208 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +6844 projected NAD83(CORS96) / Oregon Pendleton zone (m) -119.36,45.46,-118.17,46.02 1133 +proj=tmerc +lat_0=45.25 +lon_0=-119.166666666667 +k=1.000045 +x_0=60000 +y_0=0 +ellps=GRS80 +units=m +no_defs +6845 projected NAD83(CORS96) / Oregon Pendleton zone (ft) -119.36,45.46,-118.17,46.02 1133 +proj=tmerc +lat_0=45.25 +lon_0=-119.166666666667 +k=1.000045 +x_0=59999.99999976 +y_0=0 +ellps=GRS80 +to_meter=0.3048 +no_defs +6846 projected NAD83(2011) / Oregon Pendleton zone (m) -119.36,45.46,-118.17,46.02 2 1116 +proj=tmerc +lat_0=45.25 +lon_0=-119.166666666667 +k=1.000045 +x_0=60000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6847 projected NAD83(2011) / Oregon Pendleton zone (ft) -119.36,45.46,-118.17,46.02 2 1116 +proj=tmerc +lat_0=45.25 +lon_0=-119.166666666667 +k=1.000045 +x_0=59999.99999976 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +6848 projected NAD83(CORS96) / Oregon Pendleton-La Grande zone (m) -118.64,45.13,-118.09,45.64 1133 +proj=tmerc +lat_0=45.0833333333333 +lon_0=-118.333333333333 +k=1.000175 +x_0=30000 +y_0=0 +ellps=GRS80 +units=m +no_defs +6849 projected NAD83(CORS96) / Oregon Pendleton-La Grande zone (ft) -118.64,45.13,-118.09,45.64 1133 +proj=tmerc +lat_0=45.0833333333333 +lon_0=-118.333333333333 +k=1.000175 +x_0=30000.00001512 +y_0=0 +ellps=GRS80 +to_meter=0.3048 +no_defs +6850 projected NAD83(2011) / Oregon Pendleton-La Grande zone (m) -118.64,45.13,-118.09,45.64 2 1116 +proj=tmerc +lat_0=45.0833333333333 +lon_0=-118.333333333333 +k=1.000175 +x_0=30000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6851 projected NAD83(2011) / Oregon Pendleton-La Grande zone (ft) -118.64,45.13,-118.09,45.64 2 1116 +proj=tmerc +lat_0=45.0833333333333 +lon_0=-118.333333333333 +k=1.000175 +x_0=30000.00001512 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +6852 projected NAD83(CORS96) / Oregon Portland zone (m) -123.53,45.23,-122.11,46.01 1133 +proj=lcc +lat_1=45.5 +lat_0=45.5 +lon_0=-122.75 +k_0=1.000002 +x_0=100000 +y_0=50000 +ellps=GRS80 +units=m +no_defs +6853 projected NAD83(CORS96) / Oregon Portland zone (ft) -123.53,45.23,-122.11,46.01 1133 +proj=lcc +lat_1=45.5 +lat_0=45.5 +lon_0=-122.75 +k_0=1.000002 +x_0=99999.9999996 +y_0=50000.00001504 +ellps=GRS80 +to_meter=0.3048 +no_defs +6854 projected NAD83(2011) / Oregon Portland zone (m) -123.53,45.23,-122.11,46.01 2 1116 +proj=lcc +lat_1=45.5 +lat_0=45.5 +lon_0=-122.75 +k_0=1.000002 +x_0=100000 +y_0=50000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6855 projected NAD83(2011) / Oregon Portland zone (ft) -123.53,45.23,-122.11,46.01 2 1116 +proj=lcc +lat_1=45.5 +lat_0=45.5 +lon_0=-122.75 +k_0=1.000002 +x_0=99999.9999996 +y_0=50000.00001504 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +6856 projected NAD83(CORS96) / Oregon Salem zone (m) -123.73,44.32,-121.89,45.3 1133 +proj=tmerc +lat_0=44.3333333333333 +lon_0=-123.083333333333 +k=1.00001 +x_0=50000 +y_0=0 +ellps=GRS80 +units=m +no_defs +6857 projected NAD83(CORS96) / Oregon Salem zone (ft) -123.73,44.32,-121.89,45.3 1133 +proj=tmerc +lat_0=44.3333333333333 +lon_0=-123.083333333333 +k=1.00001 +x_0=50000.00001504 +y_0=0 +ellps=GRS80 +to_meter=0.3048 +no_defs +6858 projected NAD83(2011) / Oregon Salem zone (m) -123.73,44.32,-121.89,45.3 2 1116 +proj=tmerc +lat_0=44.3333333333333 +lon_0=-123.083333333333 +k=1.00001 +x_0=50000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6859 projected NAD83(2011) / Oregon Salem zone (ft) -123.73,44.32,-121.89,45.3 2 1116 +proj=tmerc +lat_0=44.3333333333333 +lon_0=-123.083333333333 +k=1.00001 +x_0=50000.00001504 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +6860 projected NAD83(CORS96) / Oregon Santiam Pass zone (m) -122.51,44.1,-121.69,44.66 1133 +proj=tmerc +lat_0=44.0833333333333 +lon_0=-122.5 +k=1.000155 +x_0=0 +y_0=0 +ellps=GRS80 +units=m +no_defs +6861 projected NAD83(CORS96) / Oregon Santiam Pass zone (ft) -122.51,44.1,-121.69,44.66 1133 +proj=tmerc +lat_0=44.0833333333333 +lon_0=-122.5 +k=1.000155 +x_0=0 +y_0=0 +ellps=GRS80 +to_meter=0.3048 +no_defs +6862 projected NAD83(2011) / Oregon Santiam Pass zone (m) -122.51,44.1,-121.69,44.66 2 1116 +proj=tmerc +lat_0=44.0833333333333 +lon_0=-122.5 +k=1.000155 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6863 projected NAD83(2011) / Oregon Santiam Pass zone (ft) -122.51,44.1,-121.69,44.66 2 1116 +proj=tmerc +lat_0=44.0833333333333 +lon_0=-122.5 +k=1.000155 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +6867 projected NAD83(CORS96) / Oregon LCC (m) -124.6,41.98,-116.47,46.26 1133 +proj=lcc +lat_0=41.75 +lon_0=-120.5 +lat_1=43 +lat_2=45.5 +x_0=400000 +y_0=0 +ellps=GRS80 +units=m +no_defs +6868 projected NAD83(CORS96) / Oregon GIC Lambert (ft) -124.6,41.98,-116.47,46.26 1133 +proj=lcc +lat_0=41.75 +lon_0=-120.5 +lat_1=43 +lat_2=45.5 +x_0=399999.9999984 +y_0=0 +ellps=GRS80 +to_meter=0.3048 +no_defs +6870 projected ETRS89 / Albania TM 2010 19.22,39.64,21.06,42.67 1 6258 +proj=tmerc +lat_0=0 +lon_0=20 +k=1 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6875 projected RDN2008 / Italy zone (N-E) 5.93,34.76,18.99,47.1 1 1132 +proj=tmerc +lat_0=0 +lon_0=12 +k=0.9985 +x_0=7000000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6876 projected RDN2008 / Zone 12 (N-E) 5.93,34.76,18.99,47.1 1 1132 +proj=tmerc +lat_0=0 +lon_0=12 +k=1 +x_0=3000000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6879 projected NAD83(2011) / Wisconsin Central -92.89,43.98,-86.25,45.8 2 1116 +proj=lcc +lat_0=43.8333333333333 +lon_0=-90 +lat_1=45.5 +lat_2=44.25 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6880 projected NAD83(2011) / Nebraska (ftUS) -104.06,39.99,-95.3,43.01 2 1116 +proj=lcc +lat_0=39.8333333333333 +lon_0=-100 +lat_1=43 +lat_2=40 +x_0=500000.00001016 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6881 geographic Aden 1925 43.37,12.54,53.14,19 1135 +proj=longlat +a=6378249.145 +rf=293.465 +towgs84=-24,-203,268 +no_defs +6882 geographic Bekaa Valley 1920 35.04,33.06,36.63,34.65 1137 +proj=longlat +a=6378249.145 +rf=293.465 +towgs84=-183,-15,273 +no_defs +6883 geographic Bioko 8.37,3.14,9.02,3.82 42 1136 +proj=longlat +ellps=intl +towgs84=-235,-110,393 +no_defs +6884 projected NAD83(CORS96) / Oregon North -124.17,43.95,-116.47,46.26 1133 +proj=lcc +lat_0=43.6666666666667 +lon_0=-120.5 +lat_1=46 +lat_2=44.3333333333333 +x_0=2500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +6885 projected NAD83(CORS96) / Oregon North (ft) -124.17,43.95,-116.47,46.26 1133 +proj=lcc +lat_0=43.6666666666667 +lon_0=-120.5 +lat_1=46 +lat_2=44.3333333333333 +x_0=2500000.0001424 +y_0=0 +ellps=GRS80 +to_meter=0.3048 +no_defs +6886 projected NAD83(CORS96) / Oregon South -124.6,41.98,-116.9,44.56 1133 +proj=lcc +lat_0=41.6666666666667 +lon_0=-120.5 +lat_1=44 +lat_2=42.3333333333333 +x_0=1500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +6887 projected NAD83(CORS96) / Oregon South (ft) -124.6,41.98,-116.9,44.56 1133 +proj=lcc +lat_0=41.6666666666667 +lon_0=-120.5 +lat_1=44 +lat_2=42.3333333333333 +x_0=1500000.0001464 +y_0=0 +ellps=GRS80 +to_meter=0.3048 +no_defs +6892 geographic South East Island 1943 55.15,-4.86,56.01,-3.66 1 1138 +proj=longlat +a=6378249.145 +rf=293.465 +towgs84=-76.269,-16.683,68.562,-6.275,10.536,-4.286,-13.686 +no_defs +6894 geographic Gambia -16.88,13.05,-13.79,13.83 43 1139 +proj=longlat +a=6378249.145 +rf=293.465 +towgs84=-63,176,185 +no_defs +6915 projected South East Island 1943 / UTM zone 40N 55.15,-4.86,56.01,-3.66 1 1138 +proj=utm +zone=40 +a=6378249.145 +rf=293.465 +towgs84=-76.269,-16.683,68.562,-6.275,10.536,-4.286,-13.686 +units=m +no_defs +6922 projected NAD83 / Kansas LCC -102.06,36.99,-94.58,40.01 4 6269 +proj=lcc +lat_0=36 +lon_0=-98.25 +lat_1=39.5 +lat_2=37.5 +x_0=400000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6923 projected NAD83 / Kansas LCC (ftUS) -102.06,36.99,-94.58,40.01 4 6269 +proj=lcc +lat_0=36 +lon_0=-98.25 +lat_1=39.5 +lat_2=37.5 +x_0=399999.99998984 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6924 projected NAD83(2011) / Kansas LCC -102.06,36.99,-94.58,40.01 2 1116 +proj=lcc +lat_0=36 +lon_0=-98.25 +lat_1=39.5 +lat_2=37.5 +x_0=400000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6925 projected NAD83(2011) / Kansas LCC (ftUS) -102.06,36.99,-94.58,40.01 2 1116 +proj=lcc +lat_0=36 +lon_0=-98.25 +lat_1=39.5 +lat_2=37.5 +x_0=399999.99998984 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +6931 projected WGS 84 / NSIDC EASE-Grid 2.0 North -180,0,180,90 6326 +proj=laea +lat_0=90 +lon_0=0 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +6932 projected WGS 84 / NSIDC EASE-Grid 2.0 South -180,-90,180,0 6326 +proj=laea +lat_0=-90 +lon_0=0 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +6933 projected WGS 84 / NSIDC EASE-Grid 2.0 Global -180,-86,180,86 6326 +proj=cea +lat_ts=30 +lon_0=0 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +6962 projected ETRS89 / Albania LCC 2010 19.22,39.64,21.06,42.67 1 6258 +proj=lcc +lat_0=41 +lon_0=20 +lat_1=39 +lat_2=43 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +6966 projected NAD27 / Michigan North -90.42,45.08,-83.44,48.32 10 6267 +proj=lcc +lat_0=44.7833333333333 +lon_0=-87 +lat_1=45.4833333333333 +lat_2=47.0833333333333 +x_0=609601.219202438 +y_0=0 +k_0=1.0000382 +ellps=clrk66 +towgs84=-8,160,176 +to_meter=0.304800609601219 +no_defs +6983 geographic IG05 Intermediate CRS 34.17,29.45,35.69,33.28 1142 +proj=longlat +ellps=GRS80 +no_defs +6984 projected Israeli Grid 05 34.17,29.45,35.69,33.28 1142 +proj=tmerc +lat_0=31.7343936111111 +lon_0=35.2045169444444 +k=1.0000067 +x_0=219529.584 +y_0=626907.39 +ellps=GRS80 +units=m +no_defs +6990 geographic IG05/12 Intermediate CRS 34.17,29.45,35.69,33.28 0.1 1144 +proj=longlat +ellps=GRS80 +towgs84=23.772,17.49,17.859,-0.3132,-1.85274,1.67299,-5.4262 +no_defs +6991 projected Israeli Grid 05/12 34.17,29.45,35.69,33.28 0.1 1144 +proj=tmerc +lat_0=31.7343936111111 +lon_0=35.2045169444444 +k=1.0000067 +x_0=219529.584 +y_0=626907.39 +ellps=GRS80 +towgs84=23.772,17.49,17.859,-0.3132,-1.85274,1.67299,-5.4262 +units=m +no_defs +7005 projected Nahrwan 1934 / UTM zone 37N 38.79,31.14,42,36.75 30 6744 +proj=utm +zone=37 +a=6378249.145 +rf=293.465 +towgs84=-242.2,-144.9,370.3 +units=m +no_defs +7006 projected Nahrwan 1934 / UTM zone 38N 42,29.06,48,37.39 30 6744 +proj=utm +zone=38 +a=6378249.145 +rf=293.465 +towgs84=-242.2,-144.9,370.3 +units=m +no_defs +7007 projected Nahrwan 1934 / UTM zone 39N 48,29.6,48.75,31 30 6744 +proj=utm +zone=39 +a=6378249.145 +rf=293.465 +towgs84=-242.2,-144.9,370.3 +units=m +no_defs +7035 geographic RGSPM06 (lon-lat) -57.1,43.41,-55.9,47.37 1 1038 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +7037 geographic RGR92 (lon-lat) 51.83,-24.72,58.24,-18.28 1 6627 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +7039 geographic RGM04 (lon-lat) 43.68,-14.49,46.7,-11.33 1 1036 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +7041 geographic RGFG95 (lon-lat) -54.61,2.11,-49.45,8.88 1 6624 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +7057 projected NAD83(2011) / IaRCS zone 1 -96.6,42.9,-93.97,43.51 2 1116 +proj=lcc +lat_1=43.2 +lat_0=43.2 +lon_0=-95.25 +k_0=1.000052 +x_0=3505207.01041402 +y_0=2926085.8521717 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7058 projected NAD83(2011) / IaRCS zone 2 -93.98,42.9,-91.6,43.51 2 1116 +proj=lcc +lat_1=43.1666666666667 +lat_0=43.1666666666667 +lon_0=-92.75 +k_0=1.000043 +x_0=3810007.62001524 +y_0=2987045.97409195 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7059 projected NAD83(2011) / IaRCS zone 3 -91.62,42.29,-90.89,43.51 2 1116 +proj=tmerc +lat_0=40.25 +lon_0=-91.2 +k=1.000035 +x_0=4114808.22961646 +y_0=2529845.05969012 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7060 projected NAD83(2011) / IaRCS zone 4 -96.65,42.2,-93,42.92 2 1116 +proj=lcc +lat_1=42.5333333333333 +lat_0=42.5333333333333 +lon_0=-94.8333333333333 +k_0=1.000045 +x_0=4419608.83921768 +y_0=2621285.24257049 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7061 projected NAD83(2011) / IaRCS zone 5 -93.03,42.2,-91.59,43.09 2 1116 +proj=lcc +lat_1=42.65 +lat_0=42.65 +lon_0=-92.25 +k_0=1.000032 +x_0=4724409.4488189 +y_0=2712725.42545085 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7062 projected NAD83(2011) / IaRCS zone 6 -96.37,40.58,-95.04,42.22 2 1116 +proj=tmerc +lat_0=40.25 +lon_0=-95.7333333333333 +k=1.000039 +x_0=5029210.05842012 +y_0=2011684.02336805 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7063 projected NAD83(2011) / IaRCS zone 7 -95.16,41.15,-94.16,42.22 2 1116 +proj=tmerc +lat_0=40.25 +lon_0=-94.6333333333333 +k=1.000045 +x_0=5334010.66802134 +y_0=2072644.14528829 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7064 projected NAD83(2011) / IaRCS zone 8 -94.29,41.15,-93.23,42.22 2 1116 +proj=tmerc +lat_0=40.25 +lon_0=-93.7166666666667 +k=1.000033 +x_0=5638811.27762256 +y_0=2133604.26720853 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7065 projected NAD83(2011) / IaRCS zone 9 -93.35,41.16,-92.29,42.3 2 1116 +proj=tmerc +lat_0=40.25 +lon_0=-92.8166666666667 +k=1.000027 +x_0=5943611.88722378 +y_0=2194564.38912878 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7066 projected NAD83(2011) / IaRCS zone 10 -92.31,41.42,-90.89,42.3 2 1116 +proj=lcc +lat_1=41.8333333333333 +lat_0=41.8333333333333 +lon_0=-91.6666666666667 +k_0=1.00002 +x_0=6248412.49682499 +y_0=2438404.87680975 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7067 projected NAD83(2011) / IaRCS zone 11 -91.14,41.44,-90.14,42.68 2 1116 +proj=tmerc +lat_0=40.25 +lon_0=-90.5333333333333 +k=1.000027 +x_0=6553213.10642621 +y_0=2316484.63296927 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7068 projected NAD83(2011) / IaRCS zone 12 -95.39,40.57,-92.17,41.17 2 1116 +proj=lcc +lat_1=40.9166666666667 +lat_0=40.9166666666667 +lon_0=-93.75 +k_0=1.000037 +x_0=6858013.71602743 +y_0=1889763.77952756 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7069 projected NAD83(2011) / IaRCS zone 13 -92.42,40.59,-91.48,41.52 2 1116 +proj=tmerc +lat_0=40.25 +lon_0=-91.9166666666667 +k=1.00002 +x_0=7162814.32562865 +y_0=1950723.9014478 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7070 projected NAD83(2011) / IaRCS zone 14 -91.72,40.36,-90.78,41.6 2 1116 +proj=tmerc +lat_0=40.25 +lon_0=-91.25 +k=1.000018 +x_0=7467614.93522987 +y_0=1889763.77952756 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7073 geographic RGTAAF07 37.98,-67.13,142,-20.91 1 1113 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +7074 projected RGTAAF07 / UTM zone 37S 37.98,-25.7,41.82,-20.91 1 1113 +proj=utm +zone=37 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7075 projected RGTAAF07 / UTM zone 38S 45.37,-49.38,48.01,-43.12 1 1113 +proj=utm +zone=38 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7076 projected RGTAAF07 / UTM zone 39S 48,-49.82,54.01,-42.61 1 1113 +proj=utm +zone=39 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7077 projected RGTAAF07 / UTM zone 40S 54,-49.61,57.16,-43.3 1 1113 +proj=utm +zone=40 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7078 projected RGTAAF07 / UTM zone 41S 62.96,-53.03,66,-45.73 1 1113 +proj=utm +zone=41 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7079 projected RGTAAF07 / UTM zone 42S 66,-53.24,72.01,-45.11 1 1113 +proj=utm +zone=42 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7080 projected RGTAAF07 / UTM zone 43S 72,-51.19,78.01,-34.47 1 1113 +proj=utm +zone=43 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7081 projected RGTAAF07 / UTM zone 44S 78,-42.04,81.83,-34.5 1 1113 +proj=utm +zone=44 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7084 geographic RGF93 v1 (lon-lat) -9.86,41.15,10.38,51.56 1 6171 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +7086 geographic RGAF09 (lon-lat) -63.66,14.08,-57.52,18.54 1 1073 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +7109 projected NAD83(2011) / RMTCRS St Mary (m) -113.97,48.55,-113,49.01 2 1116 +proj=tmerc +lat_0=48.5 +lon_0=-112.5 +k=1.00016 +x_0=150000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7110 projected NAD83(2011) / RMTCRS Blackfeet (m) -113.84,48,-112,49.01 2 1116 +proj=tmerc +lat_0=48 +lon_0=-112.5 +k=1.00019 +x_0=100000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7111 projected NAD83(2011) / RMTCRS Milk River (m) -112.5,47.78,-108.74,49.01 2 1116 +proj=lcc +lat_1=48.5 +lat_0=48.5 +lon_0=-111 +k_0=1.000145 +x_0=150000 +y_0=200000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7112 projected NAD83(2011) / RMTCRS Fort Belknap (m) -110.84,47.78,-106.99,49.01 2 1116 +proj=lcc +lat_1=48.5 +lat_0=48.5 +lon_0=-108.5 +k_0=1.00012 +x_0=200000 +y_0=150000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7113 projected NAD83(2011) / RMTCRS Fort Peck Assiniboine (m) -107.57,48.01,-104.78,48.88 2 1116 +proj=lcc +lat_1=48.3333333333333 +lat_0=48.3333333333333 +lon_0=-105.5 +k_0=1.00012 +x_0=200000 +y_0=100000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7114 projected NAD83(2011) / RMTCRS Fort Peck Sioux (m) -107.76,47.75,-104.04,49.01 2 1116 +proj=lcc +lat_1=48.3333333333333 +lat_0=48.3333333333333 +lon_0=-105.5 +k_0=1.00009 +x_0=100000 +y_0=50000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7115 projected NAD83(2011) / RMTCRS Crow (m) -108.84,44.99,-106.66,46.09 2 1116 +proj=tmerc +lat_0=44.75 +lon_0=-107.75 +k=1.000148 +x_0=200000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7116 projected NAD83(2011) / RMTCRS Bobcat (m) -112.34,45.36,-110.75,46.59 2 1116 +proj=lcc +lat_1=46.25 +lat_0=46.25 +lon_0=-111.25 +k_0=1.000185 +x_0=100000 +y_0=100000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7117 projected NAD83(2011) / RMTCRS Billings (m) -109.42,44.99,-107.99,47.41 2 1116 +proj=lcc +lat_1=45.7833333333333 +lat_0=45.7833333333333 +lon_0=-108.416666666667 +k_0=1.0001515 +x_0=200000 +y_0=50000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7118 projected NAD83(2011) / RMTCRS Wind River (m) -109.5,42.69,-107.94,43.86 2 1116 +proj=tmerc +lat_0=42.6666666666667 +lon_0=-108.333333333333 +k=1.00024 +x_0=100000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7119 projected NAD83(2011) / RMTCRS St Mary (ft) -113.97,48.55,-113,49.01 2 1116 +proj=tmerc +lat_0=48.5 +lon_0=-112.5 +k=1.00016 +x_0=150000.00001464 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +7120 projected NAD83(2011) / RMTCRS Blackfeet (ft) -113.84,48,-112,49.01 2 1116 +proj=tmerc +lat_0=48 +lon_0=-112.5 +k=1.00019 +x_0=99999.9999996 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +7121 projected NAD83(2011) / RMTCRS Milk River (ft) -112.5,47.78,-108.74,49.01 2 1116 +proj=lcc +lat_1=48.5 +lat_0=48.5 +lon_0=-111 +k_0=1.000145 +x_0=150000.00001464 +y_0=199999.9999992 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +7122 projected NAD83(2011) / RMTCRS Fort Belknap (ft) -110.84,47.78,-106.99,49.01 2 1116 +proj=lcc +lat_1=48.5 +lat_0=48.5 +lon_0=-108.5 +k_0=1.00012 +x_0=199999.9999992 +y_0=150000.00001464 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +7123 projected NAD83(2011) / RMTCRS Fort Peck Assiniboine (ft) -107.57,48.01,-104.78,48.88 2 1116 +proj=lcc +lat_1=48.3333333333333 +lat_0=48.3333333333333 +lon_0=-105.5 +k_0=1.00012 +x_0=199999.9999992 +y_0=99999.9999996 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +7124 projected NAD83(2011) / RMTCRS Fort Peck Sioux (ft) -107.76,47.75,-104.04,49.01 2 1116 +proj=lcc +lat_1=48.3333333333333 +lat_0=48.3333333333333 +lon_0=-105.5 +k_0=1.00009 +x_0=99999.9999996 +y_0=49999.99971024 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +7125 projected NAD83(2011) / RMTCRS Crow (ft) -108.84,44.99,-106.66,46.09 2 1116 +proj=tmerc +lat_0=44.75 +lon_0=-107.75 +k=1.000148 +x_0=199999.9999992 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +7126 projected NAD83(2011) / RMTCRS Bobcat (ft) -112.34,45.36,-110.75,46.59 2 1116 +proj=lcc +lat_1=46.25 +lat_0=46.25 +lon_0=-111.25 +k_0=1.000185 +x_0=99999.9999996 +y_0=99999.9999996 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +7127 projected NAD83(2011) / RMTCRS Billings (ft) -109.42,44.99,-107.99,47.41 2 1116 +proj=lcc +lat_1=45.7833333333333 +lat_0=45.7833333333333 +lon_0=-108.416666666667 +k_0=1.0001515 +x_0=199999.9999992 +y_0=50000.00001504 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +7128 projected NAD83(2011) / RMTCRS Wind River (ftUS) -109.5,42.69,-107.94,43.86 2 1116 +proj=tmerc +lat_0=42.6666666666667 +lon_0=-108.333333333333 +k=1.00024 +x_0=99999.99998984 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7131 projected NAD83(2011) / San Francisco CS13 -123.56,36.85,-121.2,38.87 2 1116 +proj=tmerc +lat_0=37.75 +lon_0=-122.45 +k=1.000007 +x_0=48000 +y_0=24000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7132 projected NAD83(2011) / San Francisco CS13 (ftUS) -123.56,36.85,-121.2,38.87 2 1116 +proj=tmerc +lat_0=37.75 +lon_0=-122.45 +k=1.000007 +x_0=48000 +y_0=24000 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7133 geographic RGTAAF07 (lon-lat) 37.98,-67.13,142,-20.91 1 1113 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +7136 geographic IGD05 32.99,29.45,35.69,33.53 1114 +proj=longlat +ellps=WGS84 +no_defs +7139 geographic IGD05/12 32.99,29.45,35.69,33.53 1115 +proj=longlat +ellps=WGS84 +no_defs +7142 projected Palestine 1923 / Palestine Grid modified 34.17,29.18,39.31,33.38 2 6281 +proj=tmerc +lat_0=31.7340969444444 +lon_0=35.2120805555556 +k=1 +x_0=170251.555 +y_0=126867.909 +a=6378300.789 +b=6356566.435 +towgs84=-275.7224,94.7824,340.8944,-8.001,-4.42,-11.821,1 +units=m +no_defs +7257 projected NAD83(2011) / InGCS Adams (m) -85.08,40.56,-84.8,40.93 2 1116 +proj=tmerc +lat_0=40.55 +lon_0=-84.95 +k=1.000034 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7258 projected NAD83(2011) / InGCS Adams (ftUS) -85.08,40.56,-84.8,40.93 2 1116 +proj=tmerc +lat_0=40.55 +lon_0=-84.95 +k=1.000034 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7259 projected NAD83(2011) / InGCS Allen (m) -85.34,40.91,-84.8,41.28 2 1116 +proj=tmerc +lat_0=40.9 +lon_0=-85.05 +k=1.000031 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7260 projected NAD83(2011) / InGCS Allen (ftUS) -85.34,40.91,-84.8,41.28 2 1116 +proj=tmerc +lat_0=40.9 +lon_0=-85.05 +k=1.000031 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7261 projected NAD83(2011) / InGCS Bartholomew (m) -86.09,39.03,-85.68,39.36 2 1116 +proj=tmerc +lat_0=39 +lon_0=-85.85 +k=1.000026 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7262 projected NAD83(2011) / InGCS Bartholomew (ftUS) -86.09,39.03,-85.68,39.36 2 1116 +proj=tmerc +lat_0=39 +lon_0=-85.85 +k=1.000026 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7263 projected NAD83(2011) / InGCS Benton (m) -87.53,40.47,-87.09,40.74 2 1116 +proj=tmerc +lat_0=40.45 +lon_0=-87.3 +k=1.000029 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7264 projected NAD83(2011) / InGCS Benton (ftUS) -87.53,40.47,-87.09,40.74 2 1116 +proj=tmerc +lat_0=40.45 +lon_0=-87.3 +k=1.000029 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7265 projected NAD83(2011) / InGCS Blackford-Delaware (m) -85.58,40.07,-85.2,40.57 2 1116 +proj=tmerc +lat_0=40.05 +lon_0=-85.4 +k=1.000038 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7266 projected NAD83(2011) / InGCS Blackford-Delaware (ftUS) -85.58,40.07,-85.2,40.57 2 1116 +proj=tmerc +lat_0=40.05 +lon_0=-85.4 +k=1.000038 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7267 projected NAD83(2011) / InGCS Boone-Hendricks (m) -86.7,39.6,-86.24,40.19 2 1116 +proj=tmerc +lat_0=39.6 +lon_0=-86.5 +k=1.000036 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7268 projected NAD83(2011) / InGCS Boone-Hendricks (ftUS) -86.7,39.6,-86.24,40.19 2 1116 +proj=tmerc +lat_0=39.6 +lon_0=-86.5 +k=1.000036 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7269 projected NAD83(2011) / InGCS Brown (m) -86.39,39.04,-86.07,39.35 2 1116 +proj=tmerc +lat_0=39 +lon_0=-86.3 +k=1.00003 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7270 projected NAD83(2011) / InGCS Brown (ftUS) -86.39,39.04,-86.07,39.35 2 1116 +proj=tmerc +lat_0=39 +lon_0=-86.3 +k=1.00003 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7271 projected NAD83(2011) / InGCS Carroll (m) -86.78,40.43,-86.37,40.74 2 1116 +proj=tmerc +lat_0=40.4 +lon_0=-86.65 +k=1.000026 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7272 projected NAD83(2011) / InGCS Carroll (ftUS) -86.78,40.43,-86.37,40.74 2 1116 +proj=tmerc +lat_0=40.4 +lon_0=-86.65 +k=1.000026 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7273 projected NAD83(2011) / InGCS Cass (m) -86.59,40.56,-86.16,40.92 2 1116 +proj=tmerc +lat_0=40.55 +lon_0=-86.4 +k=1.000028 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7274 projected NAD83(2011) / InGCS Cass (ftUS) -86.59,40.56,-86.16,40.92 2 1116 +proj=tmerc +lat_0=40.55 +lon_0=-86.4 +k=1.000028 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7275 projected NAD83(2011) / InGCS Clark-Floyd-Scott (m) -86.04,38.17,-85.41,38.84 2 1116 +proj=tmerc +lat_0=38.15 +lon_0=-85.6 +k=1.000021 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7276 projected NAD83(2011) / InGCS Clark-Floyd-Scott (ftUS) -86.04,38.17,-85.41,38.84 2 1116 +proj=tmerc +lat_0=38.15 +lon_0=-85.6 +k=1.000021 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7277 projected NAD83(2011) / InGCS Clay (m) -87.25,39.16,-86.93,39.61 2 1116 +proj=tmerc +lat_0=39.15 +lon_0=-87.15 +k=1.000024 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7278 projected NAD83(2011) / InGCS Clay (ftUS) -87.25,39.16,-86.93,39.61 2 1116 +proj=tmerc +lat_0=39.15 +lon_0=-87.15 +k=1.000024 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7279 projected NAD83(2011) / InGCS Clinton (m) -86.7,40.17,-86.24,40.44 2 1116 +proj=tmerc +lat_0=40.15 +lon_0=-86.6 +k=1.000032 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7280 projected NAD83(2011) / InGCS Clinton (ftUS) -86.7,40.17,-86.24,40.44 2 1116 +proj=tmerc +lat_0=40.15 +lon_0=-86.6 +k=1.000032 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7281 projected NAD83(2011) / InGCS Crawford-Lawrence-Orange (m) -86.69,38.1,-86.24,39 2 1116 +proj=tmerc +lat_0=38.1 +lon_0=-86.5 +k=1.000025 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7282 projected NAD83(2011) / InGCS Crawford-Lawrence-Orange (ftUS) -86.69,38.1,-86.24,39 2 1116 +proj=tmerc +lat_0=38.1 +lon_0=-86.5 +k=1.000025 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7283 projected NAD83(2011) / InGCS Daviess-Greene (m) -87.28,38.49,-86.68,39.18 2 1116 +proj=tmerc +lat_0=38.45 +lon_0=-87.1 +k=1.000018 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7284 projected NAD83(2011) / InGCS Daviess-Greene (ftUS) -87.28,38.49,-86.68,39.18 2 1116 +proj=tmerc +lat_0=38.45 +lon_0=-87.1 +k=1.000018 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7285 projected NAD83(2011) / InGCS Dearborn-Ohio-Switzerland (m) -85.21,38.68,-84.78,39.31 2 1116 +proj=tmerc +lat_0=38.65 +lon_0=-84.9 +k=1.000029 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7286 projected NAD83(2011) / InGCS Dearborn-Ohio-Switzerland (ftUS) -85.21,38.68,-84.78,39.31 2 1116 +proj=tmerc +lat_0=38.65 +lon_0=-84.9 +k=1.000029 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7287 projected NAD83(2011) / InGCS Decatur-Rush (m) -85.69,39.13,-85.29,39.79 2 1116 +proj=tmerc +lat_0=39.1 +lon_0=-85.65 +k=1.000036 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7288 projected NAD83(2011) / InGCS Decatur-Rush (ftUS) -85.69,39.13,-85.29,39.79 2 1116 +proj=tmerc +lat_0=39.1 +lon_0=-85.65 +k=1.000036 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7289 projected NAD83(2011) / InGCS DeKalb (m) -85.2,41.26,-84.8,41.54 2 1116 +proj=tmerc +lat_0=41.25 +lon_0=-84.95 +k=1.000036 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7290 projected NAD83(2011) / InGCS DeKalb (ftUS) -85.2,41.26,-84.8,41.54 2 1116 +proj=tmerc +lat_0=41.25 +lon_0=-84.95 +k=1.000036 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7291 projected NAD83(2011) / InGCS Dubois-Martin (m) -87.08,38.2,-86.67,38.91 2 1116 +proj=tmerc +lat_0=38.2 +lon_0=-86.95 +k=1.00002 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7292 projected NAD83(2011) / InGCS Dubois-Martin (ftUS) -87.08,38.2,-86.67,38.91 2 1116 +proj=tmerc +lat_0=38.2 +lon_0=-86.95 +k=1.00002 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7293 projected NAD83(2011) / InGCS Elkhart-Kosciusko-Wabash (m) -86.08,40.65,-85.63,41.77 2 1116 +proj=tmerc +lat_0=40.65 +lon_0=-85.85 +k=1.000033 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7294 projected NAD83(2011) / InGCS Elkhart-Kosciusko-Wabash (ftUS) -86.08,40.65,-85.63,41.77 2 1116 +proj=tmerc +lat_0=40.65 +lon_0=-85.85 +k=1.000033 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7295 projected NAD83(2011) / InGCS Fayette-Franklin-Union (m) -85.31,39.26,-84.81,39.79 2 1116 +proj=tmerc +lat_0=39.25 +lon_0=-85.05 +k=1.000038 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7296 projected NAD83(2011) / InGCS Fayette-Franklin-Union (ftUS) -85.31,39.26,-84.81,39.79 2 1116 +proj=tmerc +lat_0=39.25 +lon_0=-85.05 +k=1.000038 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7297 projected NAD83(2011) / InGCS Fountain-Warren (m) -87.54,39.95,-87.09,40.48 2 1116 +proj=tmerc +lat_0=39.95 +lon_0=-87.3 +k=1.000025 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7298 projected NAD83(2011) / InGCS Fountain-Warren (ftUS) -87.54,39.95,-87.09,40.48 2 1116 +proj=tmerc +lat_0=39.95 +lon_0=-87.3 +k=1.000025 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7299 projected NAD83(2011) / InGCS Fulton-Marshall-St. Joseph (m) -86.53,40.9,-85.94,41.77 2 1116 +proj=tmerc +lat_0=40.9 +lon_0=-86.3 +k=1.000031 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7300 projected NAD83(2011) / InGCS Fulton-Marshall-St. Joseph (ftUS) -86.53,40.9,-85.94,41.77 2 1116 +proj=tmerc +lat_0=40.9 +lon_0=-86.3 +k=1.000031 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7301 projected NAD83(2011) / InGCS Gibson (m) -87.99,38.16,-87.31,38.54 2 1116 +proj=tmerc +lat_0=38.15 +lon_0=-87.65 +k=1.000013 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7302 projected NAD83(2011) / InGCS Gibson (ftUS) -87.99,38.16,-87.31,38.54 2 1116 +proj=tmerc +lat_0=38.15 +lon_0=-87.65 +k=1.000013 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7303 projected NAD83(2011) / InGCS Grant (m) -85.87,40.37,-85.44,40.66 2 1116 +proj=tmerc +lat_0=40.35 +lon_0=-85.7 +k=1.000034 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7304 projected NAD83(2011) / InGCS Grant (ftUS) -85.87,40.37,-85.44,40.66 2 1116 +proj=tmerc +lat_0=40.35 +lon_0=-85.7 +k=1.000034 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7305 projected NAD83(2011) / InGCS Hamilton-Tipton (m) -86.25,39.92,-85.86,40.41 2 1116 +proj=tmerc +lat_0=39.9 +lon_0=-86 +k=1.000034 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7306 projected NAD83(2011) / InGCS Hamilton-Tipton (ftUS) -86.25,39.92,-85.86,40.41 2 1116 +proj=tmerc +lat_0=39.9 +lon_0=-86 +k=1.000034 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7307 projected NAD83(2011) / InGCS Hancock-Madison (m) -85.96,39.69,-85.57,40.38 2 1116 +proj=tmerc +lat_0=39.65 +lon_0=-85.8 +k=1.000036 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7308 projected NAD83(2011) / InGCS Hancock-Madison (ftUS) -85.96,39.69,-85.57,40.38 2 1116 +proj=tmerc +lat_0=39.65 +lon_0=-85.8 +k=1.000036 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7309 projected NAD83(2011) / InGCS Harrison-Washington (m) -86.33,37.95,-85.84,38.79 2 1116 +proj=tmerc +lat_0=37.95 +lon_0=-86.15 +k=1.000027 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7310 projected NAD83(2011) / InGCS Harrison-Washington (ftUS) -86.33,37.95,-85.84,38.79 2 1116 +proj=tmerc +lat_0=37.95 +lon_0=-86.15 +k=1.000027 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7311 projected NAD83(2011) / InGCS Henry (m) -85.6,39.78,-85.2,40.08 2 1116 +proj=tmerc +lat_0=39.75 +lon_0=-85.45 +k=1.000043 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7312 projected NAD83(2011) / InGCS Henry (ftUS) -85.6,39.78,-85.2,40.08 2 1116 +proj=tmerc +lat_0=39.75 +lon_0=-85.45 +k=1.000043 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7313 projected NAD83(2011) / InGCS Howard-Miami (m) -86.38,40.37,-85.86,41 2 1116 +proj=tmerc +lat_0=40.35 +lon_0=-86.15 +k=1.000031 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7314 projected NAD83(2011) / InGCS Howard-Miami (ftUS) -86.38,40.37,-85.86,41 2 1116 +proj=tmerc +lat_0=40.35 +lon_0=-86.15 +k=1.000031 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7315 projected NAD83(2011) / InGCS Huntington-Whitley (m) -85.69,40.65,-85.3,41.3 2 1116 +proj=tmerc +lat_0=40.65 +lon_0=-85.5 +k=1.000034 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7316 projected NAD83(2011) / InGCS Huntington-Whitley (ftUS) -85.69,40.65,-85.3,41.3 2 1116 +proj=tmerc +lat_0=40.65 +lon_0=-85.5 +k=1.000034 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7317 projected NAD83(2011) / InGCS Jackson (m) -86.32,38.72,-85.79,39.08 2 1116 +proj=tmerc +lat_0=38.7 +lon_0=-85.95 +k=1.000022 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7318 projected NAD83(2011) / InGCS Jackson (ftUS) -86.32,38.72,-85.79,39.08 2 1116 +proj=tmerc +lat_0=38.7 +lon_0=-85.95 +k=1.000022 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7319 projected NAD83(2011) / InGCS Jasper-Porter (m) -87.28,40.73,-86.92,41.77 2 1116 +proj=tmerc +lat_0=40.7 +lon_0=-87.1 +k=1.000027 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7320 projected NAD83(2011) / InGCS Jasper-Porter (ftUS) -87.28,40.73,-86.92,41.77 2 1116 +proj=tmerc +lat_0=40.7 +lon_0=-87.1 +k=1.000027 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7321 projected NAD83(2011) / InGCS Jay (m) -85.22,40.3,-84.8,40.58 2 1116 +proj=tmerc +lat_0=40.3 +lon_0=-85 +k=1.000038 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7322 projected NAD83(2011) / InGCS Jay (ftUS) -85.22,40.3,-84.8,40.58 2 1116 +proj=tmerc +lat_0=40.3 +lon_0=-85 +k=1.000038 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7323 projected NAD83(2011) / InGCS Jefferson (m) -85.69,38.58,-85.2,38.92 2 1116 +proj=tmerc +lat_0=38.55 +lon_0=-85.35 +k=1.000028 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7324 projected NAD83(2011) / InGCS Jefferson (ftUS) -85.69,38.58,-85.2,38.92 2 1116 +proj=tmerc +lat_0=38.55 +lon_0=-85.35 +k=1.000028 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7325 projected NAD83(2011) / InGCS Jennings (m) -85.8,38.8,-85.43,39.2 2 1116 +proj=tmerc +lat_0=38.8 +lon_0=-85.8 +k=1.000025 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7326 projected NAD83(2011) / InGCS Jennings (ftUS) -85.8,38.8,-85.43,39.2 2 1116 +proj=tmerc +lat_0=38.8 +lon_0=-85.8 +k=1.000025 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7327 projected NAD83(2011) / InGCS Johnson-Marion (m) -86.33,39.34,-85.93,39.93 2 1116 +proj=tmerc +lat_0=39.3 +lon_0=-86.15 +k=1.000031 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7328 projected NAD83(2011) / InGCS Johnson-Marion (ftUS) -86.33,39.34,-85.93,39.93 2 1116 +proj=tmerc +lat_0=39.3 +lon_0=-86.15 +k=1.000031 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7329 projected NAD83(2011) / InGCS Knox (m) -87.76,38.41,-87.09,38.91 2 1116 +proj=tmerc +lat_0=38.4 +lon_0=-87.45 +k=1.000015 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7330 projected NAD83(2011) / InGCS Knox (ftUS) -87.76,38.41,-87.09,38.91 2 1116 +proj=tmerc +lat_0=38.4 +lon_0=-87.45 +k=1.000015 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7331 projected NAD83(2011) / InGCS LaGrange-Noble (m) -85.66,41.26,-85.19,41.77 2 1116 +proj=tmerc +lat_0=41.25 +lon_0=-85.45 +k=1.000037 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7332 projected NAD83(2011) / InGCS LaGrange-Noble (ftUS) -85.66,41.26,-85.19,41.77 2 1116 +proj=tmerc +lat_0=41.25 +lon_0=-85.45 +k=1.000037 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7333 projected NAD83(2011) / InGCS Lake-Newton (m) -87.53,40.73,-87.21,41.77 2 1116 +proj=tmerc +lat_0=40.7 +lon_0=-87.4 +k=1.000026 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7334 projected NAD83(2011) / InGCS Lake-Newton (ftUS) -87.53,40.73,-87.21,41.77 2 1116 +proj=tmerc +lat_0=40.7 +lon_0=-87.4 +k=1.000026 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7335 projected NAD83(2011) / InGCS LaPorte-Pulaski-Starke (m) -86.94,40.9,-86.46,41.77 2 1116 +proj=tmerc +lat_0=40.9 +lon_0=-86.75 +k=1.000027 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7336 projected NAD83(2011) / InGCS LaPorte-Pulaski-Starke (ftUS) -86.94,40.9,-86.46,41.77 2 1116 +proj=tmerc +lat_0=40.9 +lon_0=-86.75 +k=1.000027 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7337 projected NAD83(2011) / InGCS Monroe-Morgan (m) -86.69,38.99,-86.24,39.64 2 1116 +proj=tmerc +lat_0=38.95 +lon_0=-86.5 +k=1.000028 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7338 projected NAD83(2011) / InGCS Monroe-Morgan (ftUS) -86.69,38.99,-86.24,39.64 2 1116 +proj=tmerc +lat_0=38.95 +lon_0=-86.5 +k=1.000028 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7339 projected NAD83(2011) / InGCS Montgomery-Putnam (m) -87.1,39.47,-86.64,40.22 2 1116 +proj=tmerc +lat_0=39.45 +lon_0=-86.95 +k=1.000031 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7340 projected NAD83(2011) / InGCS Montgomery-Putnam (ftUS) -87.1,39.47,-86.64,40.22 2 1116 +proj=tmerc +lat_0=39.45 +lon_0=-86.95 +k=1.000031 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7341 projected NAD83(2011) / InGCS Owen (m) -87.06,39.16,-86.63,39.48 2 1116 +proj=tmerc +lat_0=39.15 +lon_0=-86.9 +k=1.000026 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7342 projected NAD83(2011) / InGCS Owen (ftUS) -87.06,39.16,-86.63,39.48 2 1116 +proj=tmerc +lat_0=39.15 +lon_0=-86.9 +k=1.000026 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7343 projected NAD83(2011) / InGCS Parke-Vermillion (m) -87.54,39.6,-87,40.15 2 1116 +proj=tmerc +lat_0=39.6 +lon_0=-87.35 +k=1.000022 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7344 projected NAD83(2011) / InGCS Parke-Vermillion (ftUS) -87.54,39.6,-87,40.15 2 1116 +proj=tmerc +lat_0=39.6 +lon_0=-87.35 +k=1.000022 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7345 projected NAD83(2011) / InGCS Perry (m) -86.82,37.84,-86.43,38.27 2 1116 +proj=tmerc +lat_0=37.8 +lon_0=-86.7 +k=1.00002 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7346 projected NAD83(2011) / InGCS Perry (ftUS) -86.82,37.84,-86.43,38.27 2 1116 +proj=tmerc +lat_0=37.8 +lon_0=-86.7 +k=1.00002 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7347 projected NAD83(2011) / InGCS Pike-Warrick (m) -87.48,37.87,-87.01,38.56 2 1116 +proj=tmerc +lat_0=37.85 +lon_0=-87.3 +k=1.000015 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7348 projected NAD83(2011) / InGCS Pike-Warrick (ftUS) -87.48,37.87,-87.01,38.56 2 1116 +proj=tmerc +lat_0=37.85 +lon_0=-87.3 +k=1.000015 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7349 projected NAD83(2011) / InGCS Posey (m) -88.1,37.77,-87.68,38.24 2 1116 +proj=tmerc +lat_0=37.75 +lon_0=-87.95 +k=1.000013 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7350 projected NAD83(2011) / InGCS Posey (ftUS) -88.1,37.77,-87.68,38.24 2 1116 +proj=tmerc +lat_0=37.75 +lon_0=-87.95 +k=1.000013 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7351 projected NAD83(2011) / InGCS Randolph-Wayne (m) -85.23,39.71,-84.8,40.32 2 1116 +proj=tmerc +lat_0=39.7 +lon_0=-85.05 +k=1.000044 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7352 projected NAD83(2011) / InGCS Randolph-Wayne (ftUS) -85.23,39.71,-84.8,40.32 2 1116 +proj=tmerc +lat_0=39.7 +lon_0=-85.05 +k=1.000044 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7353 projected NAD83(2011) / InGCS Ripley (m) -85.45,38.91,-85.06,39.32 2 1116 +proj=tmerc +lat_0=38.9 +lon_0=-85.3 +k=1.000038 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7354 projected NAD83(2011) / InGCS Ripley (ftUS) -85.45,38.91,-85.06,39.32 2 1116 +proj=tmerc +lat_0=38.9 +lon_0=-85.3 +k=1.000038 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7355 projected NAD83(2011) / InGCS Shelby (m) -85.96,39.34,-85.62,39.7 2 1116 +proj=tmerc +lat_0=39.3 +lon_0=-85.9 +k=1.00003 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7356 projected NAD83(2011) / InGCS Shelby (ftUS) -85.96,39.34,-85.62,39.7 2 1116 +proj=tmerc +lat_0=39.3 +lon_0=-85.9 +k=1.00003 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7357 projected NAD83(2011) / InGCS Spencer (m) -87.27,37.78,-86.76,38.21 2 1116 +proj=tmerc +lat_0=37.75 +lon_0=-87.05 +k=1.000014 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7358 projected NAD83(2011) / InGCS Spencer (ftUS) -87.27,37.78,-86.76,38.21 2 1116 +proj=tmerc +lat_0=37.75 +lon_0=-87.05 +k=1.000014 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7359 projected NAD83(2011) / InGCS Steuben (m) -85.2,41.52,-84.8,41.77 2 1116 +proj=tmerc +lat_0=41.5 +lon_0=-85 +k=1.000041 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7360 projected NAD83(2011) / InGCS Steuben (ftUS) -85.2,41.52,-84.8,41.77 2 1116 +proj=tmerc +lat_0=41.5 +lon_0=-85 +k=1.000041 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7361 projected NAD83(2011) / InGCS Sullivan (m) -87.66,38.9,-87.24,39.26 2 1116 +proj=tmerc +lat_0=38.9 +lon_0=-87.5 +k=1.000017 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7362 projected NAD83(2011) / InGCS Sullivan (ftUS) -87.66,38.9,-87.24,39.26 2 1116 +proj=tmerc +lat_0=38.9 +lon_0=-87.5 +k=1.000017 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7363 projected NAD83(2011) / InGCS Tippecanoe-White (m) -87.1,40.21,-86.58,40.92 2 1116 +proj=tmerc +lat_0=40.2 +lon_0=-86.9 +k=1.000026 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7364 projected NAD83(2011) / InGCS Tippecanoe-White (ftUS) -87.1,40.21,-86.58,40.92 2 1116 +proj=tmerc +lat_0=40.2 +lon_0=-86.9 +k=1.000026 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7365 projected NAD83(2011) / InGCS Vanderburgh (m) -87.71,37.82,-87.44,38.17 2 1116 +proj=tmerc +lat_0=37.8 +lon_0=-87.55 +k=1.000015 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7366 projected NAD83(2011) / InGCS Vanderburgh (ftUS) -87.71,37.82,-87.44,38.17 2 1116 +proj=tmerc +lat_0=37.8 +lon_0=-87.55 +k=1.000015 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7367 projected NAD83(2011) / InGCS Vigo (m) -87.63,39.25,-87.19,39.61 2 1116 +proj=tmerc +lat_0=39.25 +lon_0=-87.45 +k=1.00002 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7368 projected NAD83(2011) / InGCS Vigo (ftUS) -87.63,39.25,-87.19,39.61 2 1116 +proj=tmerc +lat_0=39.25 +lon_0=-87.45 +k=1.00002 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7369 projected NAD83(2011) / InGCS Wells (m) -85.45,40.56,-85.06,40.92 2 1116 +proj=tmerc +lat_0=40.55 +lon_0=-85.25 +k=1.000034 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7370 projected NAD83(2011) / InGCS Wells (ftUS) -85.45,40.56,-85.06,40.92 2 1116 +proj=tmerc +lat_0=40.55 +lon_0=-85.25 +k=1.000034 +x_0=240000 +y_0=36000 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7373 geographic ONGD14 51.99,14.33,63.38,26.74 2 1147 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +7374 projected ONGD14 / UTM zone 39N 51.99,14.94,54.01,19.67 2 1147 +proj=utm +zone=39 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7375 projected ONGD14 / UTM zone 40N 54,14.33,60.01,26.74 2 1147 +proj=utm +zone=40 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7376 projected ONGD14 / UTM zone 41N 60,16.37,63.38,24.09 2 1147 +proj=utm +zone=41 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7528 projected NAD83(2011) / WISCRS Adams and Juneau (m) -90.32,43.64,-89.59,44.25 2 1116 +proj=tmerc +lat_0=43.3666666666667 +lon_0=-90 +k=1.0000365285 +x_0=147218.6942 +y_0=0.0037 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7529 projected NAD83(2011) / WISCRS Ashland (m) -90.93,45.98,-90.3,47.09 2 1116 +proj=tmerc +lat_0=45.7061111111111 +lon_0=-90.6222222222222 +k=1.0000495683 +x_0=172821.9461 +y_0=0.0017 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7530 projected NAD83(2011) / WISCRS Barron (m) -92.16,45.2,-91.53,45.65 2 1116 +proj=tmerc +lat_0=45.1333333333333 +lon_0=-91.85 +k=1.0000486665 +x_0=93150 +y_0=0.0029 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7531 projected NAD83(2011) / WISCRS Bayfield (m) -91.56,46.15,-90.75,47.01 2 1116 +proj=lcc +lat_1=46.6696483772222 +lat_0=46.6696483772222 +lon_0=-91.1527777777778 +k_0=1.0000331195 +x_0=228600.4575 +y_0=148551.4837 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7532 projected NAD83(2011) / WISCRS Brown (m) -88.26,44.24,-87.76,44.68 2 1116 +proj=tmerc +lat_0=43 +lon_0=-88 +k=1.00002 +x_0=31600 +y_0=4600 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7533 projected NAD83(2011) / WISCRS Buffalo (m) -92.09,44.02,-91.52,44.6 2 1116 +proj=tmerc +lat_0=43.4813888888889 +lon_0=-91.7972222222222 +k=1.0000382778 +x_0=175260.3502 +y_0=0.0048 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7534 projected NAD83(2011) / WISCRS Burnett (m) -92.89,45.63,-92.03,46.16 2 1116 +proj=lcc +lat_1=45.8987148658333 +lat_0=45.8987148658333 +lon_0=-92.4577777777778 +k_0=1.0000383841 +x_0=64008.1276 +y_0=59445.9043 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7535 projected NAD83(2011) / WISCRS Calumet, Fond du Lac, Outagamie and Winnebago (m) -88.89,43.54,-88.04,44.6 2 1116 +proj=tmerc +lat_0=42.7194444444444 +lon_0=-88.5 +k=1.0000286569 +x_0=244754.8893 +y_0=0.0049 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7536 projected NAD83(2011) / WISCRS Chippewa (m) -91.67,44.85,-90.92,45.3 2 1116 +proj=lcc +lat_1=44.9778568986111 +lat_0=44.9778568986111 +lon_0=-91.2944444444444 +k_0=1.0000391127 +x_0=60045.72 +y_0=44091.4346 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7537 projected NAD83(2011) / WISCRS Clark (m) -90.93,44.42,-90.31,45.04 2 1116 +proj=tmerc +lat_0=43.6 +lon_0=-90.7083333333333 +k=1.0000463003 +x_0=199949.1989 +y_0=0.0086 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7538 projected NAD83(2011) / WISCRS Columbia (m) -89.79,43.28,-89,43.65 2 1116 +proj=lcc +lat_1=43.4625466458333 +lat_0=43.4625466458333 +lon_0=-89.3944444444444 +k_0=1.00003498 +x_0=169164.3381 +y_0=111569.6134 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7539 projected NAD83(2011) / WISCRS Crawford (m) -91.22,42.98,-90.66,43.43 2 1116 +proj=lcc +lat_1=43.200055605 +lat_0=43.200055605 +lon_0=-90.9388888888889 +k_0=1.0000349151 +x_0=113690.6274 +y_0=53703.1201 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7540 projected NAD83(2011) / WISCRS Dane (m) -89.84,42.84,-89,43.3 2 1116 +proj=lcc +lat_1=43.0695160375 +lat_0=43.0695160375 +lon_0=-89.4222222222222 +k_0=1.0000384786 +x_0=247193.2944 +y_0=146591.9896 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7541 projected NAD83(2011) / WISCRS Dodge and Jefferson (m) -89.02,42.84,-88.4,43.64 2 1116 +proj=tmerc +lat_0=41.4722222222222 +lon_0=-88.775 +k=1.0000346418 +x_0=263347.7263 +y_0=0.0076 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7542 projected NAD83(2011) / WISCRS Door (m) -87.74,44.67,-86.8,45.43 2 1116 +proj=tmerc +lat_0=44.4 +lon_0=-87.2722222222222 +k=1.0000187521 +x_0=158801.1176 +y_0=0.0023 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7543 projected NAD83(2011) / WISCRS Douglas (m) -92.3,46.15,-91.55,46.76 2 1116 +proj=tmerc +lat_0=45.8833333333333 +lon_0=-91.9166666666667 +k=1.0000385418 +x_0=59131.3183 +y_0=0.0041 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7544 projected NAD83(2011) / WISCRS Dunn (m) -92.16,44.68,-91.64,45.21 2 1116 +proj=tmerc +lat_0=44.4083333333333 +lon_0=-91.8944444444444 +k=1.0000410324 +x_0=51816.104 +y_0=0.003 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7545 projected NAD83(2011) / WISCRS Eau Claire (m) -91.66,44.59,-90.92,44.86 2 1116 +proj=lcc +lat_1=44.8722811263889 +lat_0=44.8722811263889 +lon_0=-91.2888888888889 +k_0=1.000035079 +x_0=120091.4402 +y_0=91687.9239 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7546 projected NAD83(2011) / WISCRS Florence (m) -88.69,45.71,-88.05,46.03 2 1116 +proj=tmerc +lat_0=45.4388888888889 +lon_0=-88.1416666666667 +k=1.0000552095 +x_0=133502.6683 +y_0=0.0063 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7547 projected NAD83(2011) / WISCRS Forest (m) -89.05,45.37,-88.42,46.08 2 1116 +proj=tmerc +lat_0=44.0055555555556 +lon_0=-88.6333333333333 +k=1.0000673004 +x_0=275844.5533 +y_0=0.0157 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7548 projected NAD83(2011) / WISCRS Grant (m) -91.16,42.5,-90.42,43.22 2 1116 +proj=tmerc +lat_0=41.4111111111111 +lon_0=-90.8 +k=1.0000349452 +x_0=242316.4841 +y_0=0.01 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7549 projected NAD83(2011) / WISCRS Green and Lafayette (m) -90.43,42.5,-89.36,42.86 2 1116 +proj=lcc +lat_1=42.6375622769444 +lat_0=42.6375622769444 +lon_0=-89.8388888888889 +k_0=1.0000390487 +x_0=170078.7403 +y_0=45830.2947 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7550 projected NAD83(2011) / WISCRS Green Lake and Marquette (m) -89.6,43.63,-88.88,43.99 2 1116 +proj=lcc +lat_1=43.8070001177778 +lat_0=43.8070001177778 +lon_0=-89.2416666666667 +k_0=1.0000344057 +x_0=150876.3018 +y_0=79170.7795 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7551 projected NAD83(2011) / WISCRS Iowa (m) -90.43,42.81,-89.83,43.21 2 1116 +proj=tmerc +lat_0=42.5388888888889 +lon_0=-90.1611111111111 +k=1.0000394961 +x_0=113081.0261 +y_0=0.0045 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7552 projected NAD83(2011) / WISCRS Iron (m) -90.56,45.98,-89.92,46.6 2 1116 +proj=tmerc +lat_0=45.4333333333333 +lon_0=-90.2555555555556 +k=1.0000677153 +x_0=220980.4419 +y_0=0.0085 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7553 projected NAD83(2011) / WISCRS Jackson (m) -91.17,44.07,-90.31,44.6 2 1116 +proj=tmerc +lat_0=44.2533351277778 +lon_0=-90.8442965194444 +k=1.0000353 +x_0=27000 +y_0=25000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7554 projected NAD83(2011) / WISCRS Kenosha, Milwaukee, Ozaukee and Racine (m) -88.31,42.49,-87.75,43.55 2 1116 +proj=tmerc +lat_0=42.2166666666667 +lon_0=-87.8944444444444 +k=1.0000260649 +x_0=185928.3728 +y_0=0.0009 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7555 projected NAD83(2011) / WISCRS Kewaunee, Manitowoc and Sheboygan (m) -88.17,43.54,-87.37,44.68 2 1116 +proj=tmerc +lat_0=43.2666666666667 +lon_0=-87.55 +k=1.0000233704 +x_0=79857.7614 +y_0=0.0012 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7556 projected NAD83(2011) / WISCRS La Crosse (m) -91.43,43.72,-90.91,44.1 2 1116 +proj=tmerc +lat_0=43.4511111111111 +lon_0=-91.3166666666667 +k=1.0000319985 +x_0=130454.6598 +y_0=0.0033 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7557 projected NAD83(2011) / WISCRS Langlade (m) -89.43,45.02,-88.63,45.48 2 1116 +proj=lcc +lat_1=45.1542371052778 +lat_0=45.1542371052778 +lon_0=-89.0333333333333 +k_0=1.0000627024 +x_0=198425.197 +y_0=105279.7829 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7558 projected NAD83(2011) / WISCRS Lincoln (m) -90.05,45.11,-89.42,45.56 2 1116 +proj=tmerc +lat_0=44.8444444444444 +lon_0=-89.7333333333333 +k=1.0000599003 +x_0=116129.0323 +y_0=0.0058 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7559 projected NAD83(2011) / WISCRS Marathon (m) -90.32,44.68,-89.22,45.13 2 1116 +proj=lcc +lat_1=44.9009044236111 +lat_0=44.9009044236111 +lon_0=-89.77 +k_0=1.000053289 +x_0=74676.1493 +y_0=55049.2669 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7560 projected NAD83(2011) / WISCRS Marinette (m) -88.43,44.96,-87.48,45.8 2 1116 +proj=tmerc +lat_0=44.6916666666667 +lon_0=-87.7111111111111 +k=1.0000234982 +x_0=238658.8794 +y_0=0.0032 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7561 projected NAD83(2011) / WISCRS Menominee (m) -88.99,44.85,-88.48,45.12 2 1116 +proj=tmerc +lat_0=44.7166666666667 +lon_0=-88.4166666666667 +k=1.0000362499 +x_0=105461.0121 +y_0=0.0029 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7562 projected NAD83(2011) / WISCRS Monroe (m) -90.98,43.72,-90.31,44.17 2 1116 +proj=lcc +lat_1=44.0000739286111 +lat_0=44.0000739286111 +lon_0=-90.6416666666667 +k_0=1.0000434122 +x_0=204521.209 +y_0=121923.9861 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7563 projected NAD83(2011) / WISCRS Oconto (m) -88.69,44.67,-87.76,45.38 2 1116 +proj=tmerc +lat_0=44.3972222222222 +lon_0=-87.9083333333333 +k=1.0000236869 +x_0=182880.3676 +y_0=0.0033 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7564 projected NAD83(2011) / WISCRS Oneida (m) -90.05,45.46,-89.04,45.91 2 1116 +proj=lcc +lat_1=45.7042237702778 +lat_0=45.7042237702778 +lon_0=-89.5444444444444 +k_0=1.0000686968 +x_0=70104.1401 +y_0=57588.0346 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7565 projected NAD83(2011) / WISCRS Pepin and Pierce (m) -92.81,44.4,-91.65,44.87 2 1116 +proj=lcc +lat_1=44.6361488719444 +lat_0=44.6361488719444 +lon_0=-92.2277777777778 +k_0=1.0000362977 +x_0=167640.3354 +y_0=86033.0876 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7566 projected NAD83(2011) / WISCRS Polk (m) -92.89,45.2,-92.15,45.73 2 1116 +proj=tmerc +lat_0=44.6611111111111 +lon_0=-92.6333333333333 +k=1.0000433849 +x_0=141732.2823 +y_0=0.0059 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7567 projected NAD83(2011) / WISCRS Portage (m) -89.85,44.24,-89.22,44.69 2 1116 +proj=lcc +lat_1=44.4168239752778 +lat_0=44.4168239752778 +lon_0=-89.5 +k_0=1.000039936 +x_0=56388.1128 +y_0=50022.1874 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7568 projected NAD83(2011) / WISCRS Price (m) -90.68,45.37,-90.04,45.99 2 1116 +proj=tmerc +lat_0=44.5555555555556 +lon_0=-90.4888888888889 +k=1.0000649554 +x_0=227990.8546 +y_0=0.0109 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7569 projected NAD83(2011) / WISCRS Richland (m) -90.68,43.16,-90.19,43.56 2 1116 +proj=lcc +lat_1=43.3223129275 +lat_0=43.3223129275 +lon_0=-90.4305555555556 +k_0=1.0000375653 +x_0=202387.6048 +y_0=134255.4253 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7570 projected NAD83(2011) / WISCRS Rock (m) -89.37,42.49,-88.77,42.85 2 1116 +proj=tmerc +lat_0=41.9444444444444 +lon_0=-89.0722222222222 +k=1.0000337311 +x_0=146304.2926 +y_0=0.0068 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7571 projected NAD83(2011) / WISCRS Rusk (m) -91.55,45.29,-90.67,45.64 2 1116 +proj=tmerc +lat_0=43.9194444444444 +lon_0=-91.0666666666667 +k=1.0000495976 +x_0=250546.1013 +y_0=0.0234 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7572 projected NAD83(2011) / WISCRS Sauk (m) -90.32,43.14,-89.59,43.65 2 1116 +proj=tmerc +lat_0=42.8194444444444 +lon_0=-89.9 +k=1.0000373868 +x_0=185623.5716 +y_0=0.0051 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7573 projected NAD83(2011) / WISCRS Sawyer (m) -91.56,45.63,-90.67,46.16 2 1116 +proj=lcc +lat_1=45.9000991313889 +lat_0=45.9000991313889 +lon_0=-91.1166666666667 +k_0=1.0000573461 +x_0=216713.2336 +y_0=120734.1631 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7574 projected NAD83(2011) / WISCRS Shawano (m) -89.23,44.58,-88.24,45.03 2 1116 +proj=tmerc +lat_0=44.0361111111111 +lon_0=-88.6055555555556 +k=1.000032144 +x_0=262433.3253 +y_0=0.0096 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7575 projected NAD83(2011) / WISCRS St. Croix (m) -92.81,44.85,-92.13,45.22 2 1116 +proj=tmerc +lat_0=44.0361111111111 +lon_0=-92.6333333333333 +k=1.0000381803 +x_0=165506.7302 +y_0=0.0103 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7576 projected NAD83(2011) / WISCRS Taylor (m) -90.93,45.03,-90.04,45.39 2 1116 +proj=lcc +lat_1=45.1778220858333 +lat_0=45.1778220858333 +lon_0=-90.4833333333333 +k_0=1.0000597566 +x_0=187147.5744 +y_0=107746.7522 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7577 projected NAD83(2011) / WISCRS Trempealeau (m) -91.62,43.98,-91.15,44.6 2 1116 +proj=tmerc +lat_0=43.1611111111111 +lon_0=-91.3666666666667 +k=1.0000361538 +x_0=256946.9138 +y_0=0.0041 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7578 projected NAD83(2011) / WISCRS Vernon (m) -91.28,43.42,-90.31,43.74 2 1116 +proj=lcc +lat_1=43.5750329397222 +lat_0=43.5750329397222 +lon_0=-90.7833333333333 +k_0=1.0000408158 +x_0=222504.4451 +y_0=47532.0602 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7579 projected NAD83(2011) / WISCRS Vilas (m) -90.05,45.85,-88.93,46.3 2 1116 +proj=lcc +lat_1=46.0778440905556 +lat_0=46.0778440905556 +lon_0=-89.4888888888889 +k_0=1.0000730142 +x_0=134417.0689 +y_0=50337.1092 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7580 projected NAD83(2011) / WISCRS Walworth (m) -88.78,42.49,-88.3,42.85 2 1116 +proj=lcc +lat_1=42.6694620969444 +lat_0=42.6694620969444 +lon_0=-88.5416666666667 +k_0=1.0000367192 +x_0=232562.8651 +y_0=111088.2224 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7581 projected NAD83(2011) / WISCRS Washburn (m) -92.06,45.63,-91.54,46.16 2 1116 +proj=lcc +lat_1=45.9612198333333 +lat_0=45.9612198333333 +lon_0=-91.7833333333333 +k_0=1.0000475376 +x_0=234086.8682 +y_0=188358.6058 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7582 projected NAD83(2011) / WISCRS Washington (m) -88.42,43.19,-88.03,43.55 2 1116 +proj=tmerc +lat_0=42.9180555555556 +lon_0=-88.0638888888889 +k=1.00003738 +x_0=120091.4415 +y_0=0.003 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7583 projected NAD83(2011) / WISCRS Waukesha (m) -88.55,42.84,-88.06,43.2 2 1116 +proj=tmerc +lat_0=42.5694444444444 +lon_0=-88.225 +k=1.0000346179 +x_0=208788.418 +y_0=0.0034 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7584 projected NAD83(2011) / WISCRS Waupaca (m) -89.23,44.24,-88.6,44.69 2 1116 +proj=tmerc +lat_0=43.4202777777778 +lon_0=-88.8166666666667 +k=1.0000333645 +x_0=185013.9709 +y_0=0.007 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7585 projected NAD83(2011) / WISCRS Waushara (m) -89.6,43.98,-88.88,44.25 2 1116 +proj=lcc +lat_1=44.1139440458333 +lat_0=44.1139440458333 +lon_0=-89.2416666666667 +k_0=1.0000392096 +x_0=120091.4402 +y_0=45069.7587 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7586 projected NAD83(2011) / WISCRS Wood (m) -90.32,44.24,-89.72,44.69 2 1116 +proj=lcc +lat_1=44.3625954694444 +lat_0=44.3625954694444 +lon_0=-90 +k_0=1.0000421209 +x_0=208483.6173 +y_0=134589.754 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7587 projected NAD83(2011) / WISCRS Adams and Juneau (ftUS) -90.32,43.64,-89.59,44.25 2 1116 +proj=tmerc +lat_0=43.3666666666667 +lon_0=-90 +k=1.0000365285 +x_0=147218.694132588 +y_0=0.00365760731521463 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7588 projected NAD83(2011) / WISCRS Ashland (ftUS) -90.93,45.98,-90.3,47.09 2 1116 +proj=tmerc +lat_0=45.7061111111111 +lon_0=-90.6222222222222 +k=1.0000495683 +x_0=172821.945948692 +y_0=0.00182880365760732 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7589 projected NAD83(2011) / WISCRS Barron (ftUS) -92.16,45.2,-91.53,45.65 2 1116 +proj=tmerc +lat_0=45.1333333333333 +lon_0=-91.85 +k=1.0000486665 +x_0=93150 +y_0=0.00304800609601219 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7590 projected NAD83(2011) / WISCRS Bayfield (ftUS) -91.56,46.15,-90.75,47.01 2 1116 +proj=lcc +lat_1=46.6696483772222 +lat_0=46.6696483772222 +lon_0=-91.1527777777778 +k_0=1.0000331195 +x_0=228600.457505715 +y_0=148551.483566167 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7591 projected NAD83(2011) / WISCRS Brown (ftUS) -88.26,44.24,-87.76,44.68 2 1116 +proj=tmerc +lat_0=43 +lon_0=-88 +k=1.00002 +x_0=31599.9998983998 +y_0=4599.9998983998 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7592 projected NAD83(2011) / WISCRS Buffalo (ftUS) -92.09,44.02,-91.52,44.6 2 1116 +proj=tmerc +lat_0=43.4813888888889 +lon_0=-91.7972222222222 +k=1.0000382778 +x_0=175260.3502159 +y_0=0.00487680975361951 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7593 projected NAD83(2011) / WISCRS Burnett (ftUS) -92.89,45.63,-92.03,46.16 2 1116 +proj=lcc +lat_1=45.8987148658333 +lat_0=45.8987148658333 +lon_0=-92.4577777777778 +k_0=1.0000383841 +x_0=64008.1277114554 +y_0=59445.9041910084 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7594 projected NAD83(2011) / WISCRS Calumet, Fond du Lac, Outagamie and Winnebago (ftUS) -88.89,43.54,-88.04,44.6 2 1116 +proj=tmerc +lat_0=42.7194444444444 +lon_0=-88.5 +k=1.0000286569 +x_0=244754.889204978 +y_0=0.00487680975361951 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7595 projected NAD83(2011) / WISCRS Chippewa (ftUS) -91.67,44.85,-90.92,45.3 2 1116 +proj=lcc +lat_1=44.9778568986111 +lat_0=44.9778568986111 +lon_0=-91.2944444444444 +k_0=1.0000391127 +x_0=60045.7200914402 +y_0=44091.434493269 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7596 projected NAD83(2011) / WISCRS Clark (ftUS) -90.93,44.42,-90.31,45.04 2 1116 +proj=tmerc +lat_0=43.6 +lon_0=-90.7083333333333 +k=1.0000463003 +x_0=199949.198983998 +y_0=0.00853441706883414 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7597 projected NAD83(2011) / WISCRS Columbia (ftUS) -89.79,43.28,-89,43.65 2 1116 +proj=lcc +lat_1=43.4625466458333 +lat_0=43.4625466458333 +lon_0=-89.3944444444444 +k_0=1.00003498 +x_0=169164.338023876 +y_0=111569.613512827 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7598 projected NAD83(2011) / WISCRS Crawford (ftUS) -91.22,42.98,-90.66,43.43 2 1116 +proj=lcc +lat_1=43.200055605 +lat_0=43.200055605 +lon_0=-90.9388888888889 +k_0=1.0000349151 +x_0=113690.627381255 +y_0=53703.1202438405 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7599 projected NAD83(2011) / WISCRS Dane (ftUS) -89.84,42.84,-89,43.3 2 1116 +proj=lcc +lat_1=43.0695160375 +lat_0=43.0695160375 +lon_0=-89.4222222222222 +k_0=1.0000384786 +x_0=247193.294386589 +y_0=146591.989636779 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7600 projected NAD83(2011) / WISCRS Dodge and Jefferson (ftUS) -89.02,42.84,-88.4,43.64 2 1116 +proj=tmerc +lat_0=41.4722222222222 +lon_0=-88.775 +k=1.0000346418 +x_0=263347.726390653 +y_0=0.00762001524003048 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7601 projected NAD83(2011) / WISCRS Door (ftUS) -87.74,44.67,-86.8,45.43 2 1116 +proj=tmerc +lat_0=44.4 +lon_0=-87.2722222222222 +k=1.0000187521 +x_0=158801.117602235 +y_0=0.00243840487680975 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7602 projected NAD83(2011) / WISCRS Douglas (ftUS) -92.3,46.15,-91.55,46.76 2 1116 +proj=tmerc +lat_0=45.8833333333333 +lon_0=-91.9166666666667 +k=1.0000385418 +x_0=59131.3182626365 +y_0=0.00396240792481585 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7603 projected NAD83(2011) / WISCRS Dunn (ftUS) -92.16,44.68,-91.64,45.21 2 1116 +proj=tmerc +lat_0=44.4083333333333 +lon_0=-91.8944444444444 +k=1.0000410324 +x_0=51816.1039370079 +y_0=0.00304800609601219 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7604 projected NAD83(2011) / WISCRS Eau Claire (ftUS) -91.66,44.59,-90.92,44.86 2 1116 +proj=lcc +lat_1=44.8722811263889 +lat_0=44.8722811263889 +lon_0=-91.2888888888889 +k_0=1.000035079 +x_0=120091.44018288 +y_0=91687.9239014478 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7605 projected NAD83(2011) / WISCRS Florence (ftUS) -88.69,45.71,-88.05,46.03 2 1116 +proj=tmerc +lat_0=45.4388888888889 +lon_0=-88.1416666666667 +k=1.0000552095 +x_0=133502.668224536 +y_0=0.0064008128016256 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7606 projected NAD83(2011) / WISCRS Forest (ftUS) -89.05,45.37,-88.42,46.08 2 1116 +proj=tmerc +lat_0=44.0055555555556 +lon_0=-88.6333333333333 +k=1.0000673004 +x_0=275844.553213106 +y_0=0.0158496316992634 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7607 projected NAD83(2011) / WISCRS Grant (ftUS) -91.16,42.5,-90.42,43.22 2 1116 +proj=tmerc +lat_0=41.4111111111111 +lon_0=-90.8 +k=1.0000349452 +x_0=242316.484023368 +y_0=0.0100584201168402 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7608 projected NAD83(2011) / WISCRS Green and Lafayette (ftUS) -90.43,42.5,-89.36,42.86 2 1116 +proj=lcc +lat_1=42.6375622769444 +lat_0=42.6375622769444 +lon_0=-89.8388888888889 +k_0=1.0000390487 +x_0=170078.74015748 +y_0=45830.2948437897 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7609 projected NAD83(2011) / WISCRS Green Lake and Marquette (ftUS) -89.6,43.63,-88.88,43.99 2 1116 +proj=lcc +lat_1=43.8070001177778 +lat_0=43.8070001177778 +lon_0=-89.2416666666667 +k_0=1.0000344057 +x_0=150876.301752604 +y_0=79170.7793751588 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7610 projected NAD83(2011) / WISCRS Iowa (ftUS) -90.43,42.81,-89.83,43.21 2 1116 +proj=tmerc +lat_0=42.5388888888889 +lon_0=-90.1611111111111 +k=1.0000394961 +x_0=113081.026162052 +y_0=0.00457200914401829 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7611 projected NAD83(2011) / WISCRS Iron (ftUS) -90.56,45.98,-89.92,46.6 2 1116 +proj=tmerc +lat_0=45.4333333333333 +lon_0=-90.2555555555556 +k=1.0000677153 +x_0=220980.441960884 +y_0=0.00853441706883414 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7612 projected NAD83(2011) / WISCRS Jackson (ftUS) -91.17,44.07,-90.31,44.6 2 1116 +proj=tmerc +lat_0=44.2533351277778 +lon_0=-90.8442965194444 +k=1.0000353 +x_0=27000 +y_0=24999.9998983998 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7613 projected NAD83(2011) / WISCRS Kenosha, Milwaukee, Ozaukee and Racine (ftUS) -88.31,42.49,-87.75,43.55 2 1116 +proj=tmerc +lat_0=42.2166666666667 +lon_0=-87.8944444444444 +k=1.0000260649 +x_0=185928.372771146 +y_0=0.000914401828803658 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7614 projected NAD83(2011) / WISCRS Kewaunee, Manitowoc and Sheboygan (ftUS) -88.17,43.54,-87.37,44.68 2 1116 +proj=tmerc +lat_0=43.2666666666667 +lon_0=-87.55 +k=1.0000233704 +x_0=79857.7615443231 +y_0=0.00121920243840488 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7615 projected NAD83(2011) / WISCRS La Crosse (ftUS) -91.43,43.72,-90.91,44.1 2 1116 +proj=tmerc +lat_0=43.4511111111111 +lon_0=-91.3166666666667 +k=1.0000319985 +x_0=130454.659690119 +y_0=0.00335280670561341 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7616 projected NAD83(2011) / WISCRS Langlade (ftUS) -89.43,45.02,-88.63,45.48 2 1116 +proj=lcc +lat_1=45.1542371052778 +lat_0=45.1542371052778 +lon_0=-89.0333333333333 +k_0=1.0000627024 +x_0=198425.196850394 +y_0=105279.782880366 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7617 projected NAD83(2011) / WISCRS Lincoln (ftUS) -90.05,45.11,-89.42,45.56 2 1116 +proj=tmerc +lat_0=44.8444444444444 +lon_0=-89.7333333333333 +k=1.0000599003 +x_0=116129.032258065 +y_0=0.00579121158242317 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7618 projected NAD83(2011) / WISCRS Marathon (ftUS) -90.32,44.68,-89.22,45.13 2 1116 +proj=lcc +lat_1=44.9009044236111 +lat_0=44.9009044236111 +lon_0=-89.77 +k_0=1.000053289 +x_0=74676.1493522987 +y_0=55049.2669545339 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7619 projected NAD83(2011) / WISCRS Marinette (ftUS) -88.43,44.96,-87.48,45.8 2 1116 +proj=tmerc +lat_0=44.6916666666667 +lon_0=-87.7111111111111 +k=1.0000234982 +x_0=238658.879451359 +y_0=0.00304800609601219 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7620 projected NAD83(2011) / WISCRS Menominee (ftUS) -88.99,44.85,-88.48,45.12 2 1116 +proj=tmerc +lat_0=44.7166666666667 +lon_0=-88.4166666666667 +k=1.0000362499 +x_0=105461.012141224 +y_0=0.00304800609601219 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7621 projected NAD83(2011) / WISCRS Monroe (ftUS) -90.98,43.72,-90.31,44.17 2 1116 +proj=lcc +lat_1=44.0000739286111 +lat_0=44.0000739286111 +lon_0=-90.6416666666667 +k_0=1.0000434122 +x_0=204521.209042418 +y_0=121923.986182372 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7622 projected NAD83(2011) / WISCRS Oconto (ftUS) -88.69,44.67,-87.76,45.38 2 1116 +proj=tmerc +lat_0=44.3972222222222 +lon_0=-87.9083333333333 +k=1.0000236869 +x_0=182880.367589535 +y_0=0.00335280670561341 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7623 projected NAD83(2011) / WISCRS Oneida (ftUS) -90.05,45.46,-89.04,45.91 2 1116 +proj=lcc +lat_1=45.7042237702778 +lat_0=45.7042237702778 +lon_0=-89.5444444444444 +k_0=1.0000686968 +x_0=70104.1402082804 +y_0=57588.0347472695 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7624 projected NAD83(2011) / WISCRS Pepin and Pierce (ftUS) -92.81,44.4,-91.65,44.87 2 1116 +proj=lcc +lat_1=44.6361488719444 +lat_0=44.6361488719444 +lon_0=-92.2277777777778 +k_0=1.0000362977 +x_0=167640.335280671 +y_0=86033.0877317755 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7625 projected NAD83(2011) / WISCRS Polk (ftUS) -92.89,45.2,-92.15,45.73 2 1116 +proj=tmerc +lat_0=44.6611111111111 +lon_0=-92.6333333333333 +k=1.0000433849 +x_0=141732.282245365 +y_0=0.00579121158242317 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7626 projected NAD83(2011) / WISCRS Portage (ftUS) -89.85,44.24,-89.22,44.69 2 1116 +proj=lcc +lat_1=44.4168239752778 +lat_0=44.4168239752778 +lon_0=-89.5 +k_0=1.000039936 +x_0=56388.1127762256 +y_0=50022.1874523749 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7627 projected NAD83(2011) / WISCRS Price (ftUS) -90.68,45.37,-90.04,45.99 2 1116 +proj=tmerc +lat_0=44.5555555555556 +lon_0=-90.4888888888889 +k=1.0000649554 +x_0=227990.854457709 +y_0=0.0109728219456439 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7628 projected NAD83(2011) / WISCRS Richland (ftUS) -90.68,43.16,-90.19,43.56 2 1116 +proj=lcc +lat_1=43.3223129275 +lat_0=43.3223129275 +lon_0=-90.4305555555556 +k_0=1.0000375653 +x_0=202387.60477521 +y_0=134255.425450851 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7629 projected NAD83(2011) / WISCRS Rock (ftUS) -89.37,42.49,-88.77,42.85 2 1116 +proj=tmerc +lat_0=41.9444444444444 +lon_0=-89.0722222222222 +k=1.0000337311 +x_0=146304.292608585 +y_0=0.00670561341122682 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7630 projected NAD83(2011) / WISCRS Rusk (ftUS) -91.55,45.29,-90.67,45.64 2 1116 +proj=tmerc +lat_0=43.9194444444444 +lon_0=-91.0666666666667 +k=1.0000495976 +x_0=250546.101397003 +y_0=0.0234696469392939 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7631 projected NAD83(2011) / WISCRS Sauk (ftUS) -90.32,43.14,-89.59,43.65 2 1116 +proj=tmerc +lat_0=42.8194444444444 +lon_0=-89.9 +k=1.0000373868 +x_0=185623.571551943 +y_0=0.00518161036322073 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7632 projected NAD83(2011) / WISCRS Sawyer (ftUS) -91.56,45.63,-90.67,46.16 2 1116 +proj=lcc +lat_1=45.9000991313889 +lat_0=45.9000991313889 +lon_0=-91.1166666666667 +k_0=1.0000573461 +x_0=216713.233731268 +y_0=120734.163169926 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7633 projected NAD83(2011) / WISCRS Shawano (ftUS) -89.23,44.58,-88.24,45.03 2 1116 +proj=tmerc +lat_0=44.0361111111111 +lon_0=-88.6055555555556 +k=1.000032144 +x_0=262433.32517145 +y_0=0.0094488188976378 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7634 projected NAD83(2011) / WISCRS St. Croix (ftUS) -92.81,44.85,-92.13,45.22 2 1116 +proj=tmerc +lat_0=44.0361111111111 +lon_0=-92.6333333333333 +k=1.0000381803 +x_0=165506.73009906 +y_0=0.0103632207264415 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7635 projected NAD83(2011) / WISCRS Taylor (ftUS) -90.93,45.03,-90.04,45.39 2 1116 +proj=lcc +lat_1=45.1778220858333 +lat_0=45.1778220858333 +lon_0=-90.4833333333333 +k_0=1.0000597566 +x_0=187147.574295149 +y_0=107746.752146304 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7636 projected NAD83(2011) / WISCRS Trempealeau (ftUS) -91.62,43.98,-91.15,44.6 2 1116 +proj=tmerc +lat_0=43.1611111111111 +lon_0=-91.3666666666667 +k=1.0000361538 +x_0=256946.913893828 +y_0=0.00396240792481585 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7637 projected NAD83(2011) / WISCRS Vernon (ftUS) -91.28,43.42,-90.31,43.74 2 1116 +proj=lcc +lat_1=43.5750329397222 +lat_0=43.5750329397222 +lon_0=-90.7833333333333 +k_0=1.0000408158 +x_0=222504.44500889 +y_0=47532.0603505207 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7638 projected NAD83(2011) / WISCRS Vilas (ftUS) -90.05,45.85,-88.93,46.3 2 1116 +proj=lcc +lat_1=46.0778440905556 +lat_0=46.0778440905556 +lon_0=-89.4888888888889 +k_0=1.0000730142 +x_0=134417.068834138 +y_0=50337.1092710185 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7639 projected NAD83(2011) / WISCRS Walworth (ftUS) -88.78,42.49,-88.3,42.85 2 1116 +proj=lcc +lat_1=42.6694620969444 +lat_0=42.6694620969444 +lon_0=-88.5416666666667 +k_0=1.0000367192 +x_0=232562.86512573 +y_0=111088.222402845 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7640 projected NAD83(2011) / WISCRS Washburn (ftUS) -92.06,45.63,-91.54,46.16 2 1116 +proj=lcc +lat_1=45.9612198333333 +lat_0=45.9612198333333 +lon_0=-91.7833333333333 +k_0=1.0000475376 +x_0=234086.868173736 +y_0=188358.605943612 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7641 projected NAD83(2011) / WISCRS Washington (ftUS) -88.42,43.19,-88.03,43.55 2 1116 +proj=tmerc +lat_0=42.9180555555556 +lon_0=-88.0638888888889 +k=1.00003738 +x_0=120091.441402083 +y_0=0.00304800609601219 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7642 projected NAD83(2011) / WISCRS Waukesha (ftUS) -88.55,42.84,-88.06,43.2 2 1116 +proj=tmerc +lat_0=42.5694444444444 +lon_0=-88.225 +k=1.0000346179 +x_0=208788.417881636 +y_0=0.00335280670561341 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7643 projected NAD83(2011) / WISCRS Waupaca (ftUS) -89.23,44.24,-88.6,44.69 2 1116 +proj=tmerc +lat_0=43.4202777777778 +lon_0=-88.8166666666667 +k=1.0000333645 +x_0=185013.970942342 +y_0=0.00701041402082804 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7644 projected NAD83(2011) / WISCRS Waushara (ftUS) -89.6,43.98,-88.88,44.25 2 1116 +proj=lcc +lat_1=44.1139440458333 +lat_0=44.1139440458333 +lon_0=-89.2416666666667 +k_0=1.0000392096 +x_0=120091.44018288 +y_0=45069.7588011176 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7645 projected NAD83(2011) / WISCRS Wood (ftUS) -90.32,44.24,-89.72,44.69 2 1116 +proj=lcc +lat_1=44.3625954694444 +lat_0=44.3625954694444 +lon_0=-90 +k_0=1.0000421209 +x_0=208483.617272035 +y_0=134589.753924308 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +7683 geographic GSK-2011 18.92,39.87,-168.97,85.19 1 1159 +proj=longlat +ellps=GSK2011 +towgs84=0,0,0 +no_defs +7686 geographic Kyrg-06 69.24,39.19,80.29,43.22 1 1160 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +7692 projected Kyrg-06 / zone 1 69.24,39.51,70.02,40.22 1 1160 +proj=tmerc +lat_0=0 +lon_0=68.5166666666667 +k=1 +x_0=1300000 +y_0=14743.5 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7693 projected Kyrg-06 / zone 2 70.01,39.19,73.02,42.83 1 1160 +proj=tmerc +lat_0=0 +lon_0=71.5166666666667 +k=1 +x_0=2300000 +y_0=14743.5 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7694 projected Kyrg-06 / zone 3 73.01,39.35,76.02,43.22 1 1160 +proj=tmerc +lat_0=0 +lon_0=74.5166666666667 +k=1 +x_0=3300000 +y_0=14743.5 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7695 projected Kyrg-06 / zone 4 76.01,40.35,79.02,43 1 1160 +proj=tmerc +lat_0=0 +lon_0=77.5166666666667 +k=1 +x_0=4300000 +y_0=14743.5 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7696 projected Kyrg-06 / zone 5 79.01,41.66,80.29,42.8 1 1160 +proj=tmerc +lat_0=0 +lon_0=80.5166666666667 +k=1 +x_0=5300000 +y_0=14743.5 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7755 projected WGS 84 / India NSF LCC 65.6,3.87,97.42,35.51 6326 +proj=lcc +lat_0=24 +lon_0=80 +lat_1=12.472955 +lat_2=35.1728044444444 +x_0=4000000 +y_0=4000000 +datum=WGS84 +units=m +no_defs +7756 projected WGS 84 / Andhra Pradesh 76.75,12.61,84.81,19.92 6326 +proj=lcc +lat_0=16.25543298 +lon_0=80.875 +lat_1=13.75 +lat_2=18.75 +x_0=1000000 +y_0=1000000 +datum=WGS84 +units=m +no_defs +7757 projected WGS 84 / Arunachal Pradesh 91.55,26.65,97.42,29.47 6326 +proj=lcc +lat_0=28.00157897 +lon_0=94.5 +lat_1=27 +lat_2=29 +x_0=1000000 +y_0=1000000 +datum=WGS84 +units=m +no_defs +7758 projected WGS 84 / Assam 89.69,24.13,96.03,27.98 6326 +proj=lcc +lat_0=26.00257703 +lon_0=92.75 +lat_1=24.6666666666667 +lat_2=27.3333333333333 +x_0=1000000 +y_0=1000000 +datum=WGS84 +units=m +no_defs +7759 projected WGS 84 / Bihar 83.31,24.28,88.3,27.86 6326 +proj=lcc +lat_0=25.87725247 +lon_0=85.875 +lat_1=24.625 +lat_2=27.125 +x_0=1000000 +y_0=1000000 +datum=WGS84 +units=m +no_defs +7760 projected WGS 84 / Delhi 76.83,28.4,77.34,28.89 6326 +proj=lcc +lat_0=28.62510126 +lon_0=77 +lat_1=28.375 +lat_2=28.875 +x_0=1000000 +y_0=1000000 +datum=WGS84 +units=m +no_defs +7761 projected WGS 84 / Gujarat 68.13,20.05,74.48,24.71 6326 +proj=lcc +lat_0=22.37807121 +lon_0=71.375 +lat_1=20.7916666666667 +lat_2=23.9583333333333 +x_0=1000000 +y_0=1000000 +datum=WGS84 +units=m +no_defs +7762 projected WGS 84 / Haryana 74.46,27.65,77.6,30.94 6326 +proj=lcc +lat_0=29.25226266 +lon_0=76 +lat_1=28.0833333333333 +lat_2=30.4166666666667 +x_0=1000000 +y_0=1000000 +datum=WGS84 +units=m +no_defs +7763 projected WGS 84 / Himachal Pradesh 75.57,30.38,79,33.26 6326 +proj=lcc +lat_0=31.75183497 +lon_0=77.375 +lat_1=30.75 +lat_2=32.75 +x_0=1000000 +y_0=1000000 +datum=WGS84 +units=m +no_defs +7764 projected WGS 84 / Jammu and Kashmir 73.76,32.27,79.57,35.51 6326 +proj=lcc +lat_0=34.75570874 +lon_0=76.5 +lat_1=33.0833333333333 +lat_2=36.4166666666667 +x_0=1000000 +y_0=1000000 +datum=WGS84 +units=m +no_defs +7765 projected WGS 84 / Jharkhand 83.32,21.96,87.98,25.35 6326 +proj=lcc +lat_0=23.62652682 +lon_0=85.625 +lat_1=22.5416666666667 +lat_2=24.7083333333333 +x_0=1000000 +y_0=1000000 +datum=WGS84 +units=m +no_defs +7766 projected WGS 84 / Madhya Pradesh 74.03,21.07,82.81,26.88 6326 +proj=lcc +lat_0=24.00529821 +lon_0=78.375 +lat_1=22 +lat_2=26 +x_0=1000000 +y_0=1000000 +datum=WGS84 +units=m +no_defs +7767 projected WGS 84 / Maharashtra 72.6,15.6,80.9,22.04 6326 +proj=lcc +lat_0=18.88015774 +lon_0=76.75 +lat_1=16.625 +lat_2=21.125 +x_0=1000000 +y_0=1000000 +datum=WGS84 +units=m +no_defs +7768 projected WGS 84 / Manipur 92.97,23.84,94.76,25.7 6326 +proj=lcc +lat_0=24.75060911 +lon_0=94 +lat_1=24.0833333333333 +lat_2=25.4166666666667 +x_0=1000000 +y_0=1000000 +datum=WGS84 +units=m +no_defs +7769 projected WGS 84 / Meghalaya 89.82,25.03,92.81,26.12 6326 +proj=lcc +lat_0=25.62524747 +lon_0=91.375 +lat_1=25.2083333333333 +lat_2=26.0416666666667 +x_0=1000000 +y_0=1000000 +datum=WGS84 +units=m +no_defs +7770 projected WGS 84 / Nagaland 93.33,25.2,95.25,27.05 6326 +proj=lcc +lat_0=26.12581974 +lon_0=94.375 +lat_1=25.375 +lat_2=26.875 +x_0=1000000 +y_0=1000000 +datum=WGS84 +units=m +no_defs +7771 projected WGS 84 / India Northeast 89.69,21.94,97.42,29.47 6326 +proj=lcc +lat_0=25.63452135 +lon_0=93.5 +lat_1=23.0416666666667 +lat_2=28.2083333333333 +x_0=1000000 +y_0=1000000 +datum=WGS84 +units=m +no_defs +7772 projected WGS 84 / Orissa 81.38,17.8,87.5,22.57 6326 +proj=lcc +lat_0=20.25305174 +lon_0=84.375 +lat_1=18.5833333333333 +lat_2=21.9166666666667 +x_0=1000000 +y_0=1000000 +datum=WGS84 +units=m +no_defs +7773 projected WGS 84 / Punjab 73.87,29.54,76.94,32.58 6326 +proj=lcc +lat_0=31.00178226 +lon_0=75.375 +lat_1=30 +lat_2=32 +x_0=1000000 +y_0=1000000 +datum=WGS84 +units=m +no_defs +7774 projected WGS 84 / Rajasthan 69.48,23.06,78.27,30.2 6326 +proj=lcc +lat_0=26.88505546 +lon_0=73.875 +lat_1=24.2916666666667 +lat_2=29.4583333333333 +x_0=1000000 +y_0=1000000 +datum=WGS84 +units=m +no_defs +7775 projected WGS 84 / Uttar Pradesh 77.08,23.87,84.64,30.42 6326 +proj=lcc +lat_0=27.13270823 +lon_0=80.875 +lat_1=24.875 +lat_2=29.375 +x_0=1000000 +y_0=1000000 +datum=WGS84 +units=m +no_defs +7776 projected WGS 84 / Uttaranchal 77.56,28.71,81.02,31.48 6326 +proj=lcc +lat_0=30.0017132 +lon_0=79.375 +lat_1=29 +lat_2=31 +x_0=1000000 +y_0=1000000 +datum=WGS84 +units=m +no_defs +7777 projected WGS 84 / Andaman and Nicobar 92.15,6.7,94.33,13.73 6326 +proj=tmerc +lat_0=10.25 +lon_0=93.25 +k=0.9999428 +x_0=1000000 +y_0=1000000 +datum=WGS84 +units=m +no_defs +7778 projected WGS 84 / Chhattisgarh 80.23,17.78,84.39,24.11 6326 +proj=tmerc +lat_0=21 +lon_0=82.25 +k=0.9998332 +x_0=1000000 +y_0=1000000 +datum=WGS84 +units=m +no_defs +7779 projected WGS 84 / Goa 73.61,14.86,74.35,15.8 6326 +proj=tmerc +lat_0=15.375 +lon_0=74 +k=0.9999913 +x_0=1000000 +y_0=1000000 +datum=WGS84 +units=m +no_defs +7780 projected WGS 84 / Karnataka 74,11.57,78.58,18.46 6326 +proj=tmerc +lat_0=15.125 +lon_0=76.375 +k=0.9998012 +x_0=1000000 +y_0=1000000 +datum=WGS84 +units=m +no_defs +7781 projected WGS 84 / Kerala 74.81,8.25,77.4,12.8 6326 +proj=tmerc +lat_0=10.5 +lon_0=76 +k=0.9999177 +x_0=1000000 +y_0=1000000 +datum=WGS84 +units=m +no_defs +7782 projected WGS 84 / Lakshadweep 72.04,8.21,73.76,11.76 6326 +proj=tmerc +lat_0=10 +lon_0=73.125 +k=0.9999536 +x_0=1000000 +y_0=1000000 +datum=WGS84 +units=m +no_defs +7783 projected WGS 84 / Mizoram 92.25,21.94,93.45,24.53 6326 +proj=tmerc +lat_0=23.125 +lon_0=92.75 +k=0.9999821 +x_0=1000000 +y_0=1000000 +datum=WGS84 +units=m +no_defs +7784 projected WGS 84 / Sikkim 88.01,27.08,88.92,28.14 6326 +proj=tmerc +lat_0=27.625 +lon_0=88.5 +k=0.9999926 +x_0=1000000 +y_0=1000000 +datum=WGS84 +units=m +no_defs +7785 projected WGS 84 / Tamil Nadu 76.22,8.02,80.4,13.59 6326 +proj=tmerc +lat_0=10.875 +lon_0=78.375 +k=0.9997942 +x_0=1000000 +y_0=1000000 +datum=WGS84 +units=m +no_defs +7786 projected WGS 84 / Tripura 91.15,22.94,92.34,24.54 6326 +proj=tmerc +lat_0=23.75 +lon_0=91.75 +k=0.9999822 +x_0=1000000 +y_0=1000000 +datum=WGS84 +units=m +no_defs +7787 projected WGS 84 / West Bengal 85.82,21.49,89.88,27.23 6326 +proj=tmerc +lat_0=24.375 +lon_0=87.875 +k=0.9998584 +x_0=1000000 +y_0=1000000 +datum=WGS84 +units=m +no_defs +7791 projected RDN2008 / UTM zone 32N 5.93,36.53,12,47.04 1 1132 +proj=utm +zone=32 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7792 projected RDN2008 / UTM zone 33N 12,34.79,18,47.1 1 1132 +proj=utm +zone=33 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7793 projected RDN2008 / UTM zone 34N 17.99,34.76,18.99,41.64 1 1132 +proj=utm +zone=34 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7794 projected RDN2008 / Italy zone (E-N) 5.93,34.76,18.99,47.1 1 1132 +proj=tmerc +lat_0=0 +lon_0=12 +k=0.9985 +x_0=7000000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7795 projected RDN2008 / Zone 12 (E-N) 5.93,34.76,18.99,47.1 1 1132 +proj=tmerc +lat_0=0 +lon_0=12 +k=1 +x_0=3000000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7798 geographic BGS2005 22.36,41.24,31.35,44.23 1 1167 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +7799 projected BGS2005 / UTM zone 34N (N-E) 22.36,41.32,24,44.23 1 1167 +proj=utm +zone=34 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7800 projected BGS2005 / UTM zone 35N (N-E) 24,41.24,30.01,44.15 1 1167 +proj=utm +zone=35 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7801 projected BGS2005 / CCS2005 22.36,41.24,28.68,44.23 1 1167 +proj=lcc +lat_0=42.6678756833333 +lon_0=25.5 +lat_1=42 +lat_2=43.3333333333333 +x_0=500000 +y_0=4725824.3591 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7803 projected BGS2005 / UTM zone 34N 22.36,41.32,24,44.23 1 1167 +proj=utm +zone=34 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7805 projected BGS2005 / UTM zone 36N 30,42.56,31.35,43.67 1 1167 +proj=utm +zone=36 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7825 projected Pulkovo 1942 / CS63 zone X1 22.15,47.71,25.01,51.92 3 6284 +proj=tmerc +lat_0=0.0833333333333333 +lon_0=23.5 +k=1 +x_0=1300000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +7826 projected Pulkovo 1942 / CS63 zone X2 25,47.72,28.01,51.96 3 6284 +proj=tmerc +lat_0=0.0833333333333333 +lon_0=26.5 +k=1 +x_0=2300000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +7827 projected Pulkovo 1942 / CS63 zone X3 28,45.18,31.01,52.09 3 6284 +proj=tmerc +lat_0=0.0833333333333333 +lon_0=29.5 +k=1 +x_0=3300000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +7828 projected Pulkovo 1942 / CS63 zone X4 31,44.32,34.01,52.38 3 6284 +proj=tmerc +lat_0=0.0833333333333333 +lon_0=32.5 +k=1 +x_0=4300000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +7829 projected Pulkovo 1942 / CS63 zone X5 34,44.33,37.01,52.25 3 6284 +proj=tmerc +lat_0=0.0833333333333333 +lon_0=35.5 +k=1 +x_0=5300000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +7830 projected Pulkovo 1942 / CS63 zone X6 37,46.8,40.01,50.44 3 6284 +proj=tmerc +lat_0=0.0833333333333333 +lon_0=38.5 +k=1 +x_0=6300000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +7831 projected Pulkovo 1942 / CS63 zone X7 40,48.8,40.18,49.62 3 6284 +proj=tmerc +lat_0=0.0833333333333333 +lon_0=41.5 +k=1 +x_0=7300000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +7844 geographic GDA2020 93.41,-60.55,173.34,-8.47 3 1168 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +7845 projected GDA2020 / GA LCC 112.85,-43.7,153.69,-9.86 3 1168 +proj=lcc +lat_0=0 +lon_0=134 +lat_1=-18 +lat_2=-36 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7846 projected GDA2020 / MGA zone 46 93.41,-15.46,96.01,-8.57 3 1168 +proj=utm +zone=46 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7847 projected GDA2020 / MGA zone 47 96,-15.56,100.34,-8.47 3 1168 +proj=utm +zone=47 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7848 projected GDA2020 / MGA zone 48 102.14,-34.68,108.01,-8.87 3 1168 +proj=utm +zone=48 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7849 projected GDA2020 / MGA zone 49 108,-37.84,114.01,-10.72 3 1168 +proj=utm +zone=49 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7850 projected GDA2020 / MGA zone 50 114,-38.53,120.01,-12.06 3 1168 +proj=utm +zone=50 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7851 projected GDA2020 / MGA zone 51 120,-38.07,126.01,-10.46 3 1168 +proj=utm +zone=51 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7852 projected GDA2020 / MGA zone 52 125.99,-37.38,132,-9.1 3 1168 +proj=utm +zone=52 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7853 projected GDA2020 / MGA zone 53 132,-40.71,138.01,-8.88 3 1168 +proj=utm +zone=53 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7854 projected GDA2020 / MGA zone 54 138,-48.19,144.01,-9.08 3 1168 +proj=utm +zone=54 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7855 projected GDA2020 / MGA zone 55 144,-50.89,150.01,-9.23 3 1168 +proj=utm +zone=55 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7856 projected GDA2020 / MGA zone 56 150,-58.96,156,-13.87 3 1168 +proj=utm +zone=56 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7857 projected GDA2020 / MGA zone 57 156,-60.56,162.01,-14.08 3 1168 +proj=utm +zone=57 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7858 projected GDA2020 / MGA zone 58 162,-59.39,168,-25.94 3 1168 +proj=utm +zone=58 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7859 projected GDA2020 / MGA zone 59 168,-32.48,173.35,-25.61 3 1168 +proj=utm +zone=59 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7877 projected Astro DOS 71 / SHLG71 -5.85,-16.08,-5.59,-15.85 1 6710 +proj=tmerc +lat_0=-15.9666666666667 +lon_0=-5.71666666666667 +k=1 +x_0=300000 +y_0=2000000 +ellps=intl +towgs84=-323.65,551.39,-491.22 +units=m +no_defs +7878 projected Astro DOS 71 / UTM zone 30S -5.85,-16.08,-5.59,-15.85 1 6710 +proj=utm +zone=30 +south +ellps=intl +towgs84=-323.65,551.39,-491.22 +units=m +no_defs +7881 geographic St. Helena Tritan -5.85,-16.08,-5.59,-15.85 1 1173 +proj=longlat +ellps=WGS84 +towgs84=-0.077,0.079,0.086 +no_defs +7882 projected St. Helena Tritan / SHLG(Tritan) -5.85,-16.08,-5.59,-15.85 1 1173 +proj=tmerc +lat_0=-15.9666666666667 +lon_0=-5.71666666666667 +k=1 +x_0=299483.737 +y_0=2000527.879 +ellps=WGS84 +towgs84=-0.077,0.079,0.086 +units=m +no_defs +7883 projected St. Helena Tritan / UTM zone 30S -5.85,-16.08,-5.59,-15.85 1 1173 +proj=utm +zone=30 +south +ellps=WGS84 +towgs84=-0.077,0.079,0.086 +units=m +no_defs +7886 geographic SHGD2015 -5.85,-16.08,-5.59,-15.85 1 1174 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +7887 projected SHMG2015 -5.85,-16.08,-5.59,-15.85 1 1174 +proj=utm +zone=30 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7899 projected GDA2020 / Vicgrid 140.96,-39.2,150.04,-33.98 3 1168 +proj=lcc +lat_0=-37 +lon_0=145 +lat_1=-36 +lat_2=-38 +x_0=2500000 +y_0=2500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +7991 projected NAD27 / MTM zone 10 -81,42.26,-77.99,47.33 10 6267 +proj=tmerc +lat_0=0 +lon_0=-79.5 +k=0.9999 +x_0=304800 +y_0=0 +ellps=clrk66 +towgs84=-8,160,176 +units=m +no_defs +7992 projected Malongo 1987 / UTM zone 33S 11.79,-6.04,12.37,-5.79 5 6259 +proj=utm +zone=33 +south +ellps=intl +towgs84=-254.1,-5.36,-100.29 +units=m +no_defs +8013 projected GDA2020 / ALB2020 117.55,-35.21,118.22,-34.75 3 1168 +proj=tmerc +lat_0=0 +lon_0=117.883333333333 +k=1.0000044 +x_0=50000 +y_0=4100000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +8014 projected GDA2020 / BIO2020 114.9,-22.2,115.59,-20.21 3 1168 +proj=tmerc +lat_0=0 +lon_0=115.25 +k=1.0000022 +x_0=60000 +y_0=2700000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +8015 projected GDA2020 / BRO2020 122.08,-18.09,122.62,-16.75 3 1168 +proj=tmerc +lat_0=0 +lon_0=122.333333333333 +k=1.00000298 +x_0=50000 +y_0=2300000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +8016 projected GDA2020 / BCG2020 115.18,-33.75,115.87,-33.4 3 1168 +proj=tmerc +lat_0=0 +lon_0=115.433333333333 +k=0.99999592 +x_0=50000 +y_0=4000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +8017 projected GDA2020 / CARN2020 113.33,-25.5,114,-23 3 1168 +proj=tmerc +lat_0=0 +lon_0=113.666666666667 +k=0.99999796 +x_0=50000 +y_0=3050000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +8018 projected GDA2020 / CIG2020 105.48,-10.63,105.77,-10.36 3 1168 +proj=tmerc +lat_0=0 +lon_0=105.625 +k=1.00002514 +x_0=50000 +y_0=1400000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +8019 projected GDA2020 / CKIG2020 96.76,-12.27,96.99,-11.76 3 1168 +proj=tmerc +lat_0=0 +lon_0=96.875 +k=0.99999387 +x_0=50000 +y_0=1600000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +8020 projected GDA2020 / COL2020 115.73,-34.67,116.4,-33.25 3 1168 +proj=tmerc +lat_0=0 +lon_0=115.933333333333 +k=1.000019 +x_0=40000 +y_0=4100000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +8021 projected GDA2020 / ESP2020 121.56,-34.5,122.2,-33.33 3 1168 +proj=tmerc +lat_0=0 +lon_0=121.883333333333 +k=1.0000055 +x_0=50000 +y_0=4050000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +8022 projected GDA2020 / EXM2020 113.75,-22.42,114.25,-21.75 3 1168 +proj=tmerc +lat_0=0 +lon_0=114.066666666667 +k=1.00000236 +x_0=50000 +y_0=2750000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +8023 projected GDA2020 / GCG2020 114.51,-29.1,115,-28.48 3 1168 +proj=tmerc +lat_0=0 +lon_0=114.583333333333 +k=1.00000628 +x_0=50000 +y_0=3450000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +8024 projected GDA2020 / GOLD2020 121,-32.25,121.84,-28.75 3 1168 +proj=tmerc +lat_0=0 +lon_0=121.5 +k=1.00004949 +x_0=60000 +y_0=3800000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +8025 projected GDA2020 / JCG2020 114.83,-30.74,115.34,-29.08 3 1168 +proj=tmerc +lat_0=0 +lon_0=114.983333333333 +k=1.00000314 +x_0=50000 +y_0=3650000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +8026 projected GDA2020 / KALB2020 113.9,-28.5,114.75,-27.16 3 1168 +proj=tmerc +lat_0=0 +lon_0=114.315277777778 +k=1.000014 +x_0=55000 +y_0=3700000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +8027 projected GDA2020 / KAR2020 116.58,-20.92,117.25,-20.25 3 1168 +proj=tmerc +lat_0=0 +lon_0=116.933333333333 +k=0.9999989 +x_0=50000 +y_0=2550000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +8028 projected GDA2020 / KUN2020 128.5,-16.75,129,-14.75 3 1168 +proj=tmerc +lat_0=0 +lon_0=128.75 +k=1.0000165 +x_0=50000 +y_0=2100000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +8029 projected GDA2020 / LCG2020 115.15,-31.49,115.62,-30.71 3 1168 +proj=tmerc +lat_0=0 +lon_0=115.366666666667 +k=1.00000157 +x_0=50000 +y_0=3750000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +8030 projected GDA2020 / MRCG2020 114.96,-34.42,115.24,-33.51 3 1168 +proj=tmerc +lat_0=0 +lon_0=115.166666666667 +k=1.0000055 +x_0=50000 +y_0=4050000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +8031 projected GDA2020 / PCG2020 115.44,-33.42,116.09,-31.33 3 1168 +proj=tmerc +lat_0=0 +lon_0=115.816666666667 +k=0.99999906 +x_0=50000 +y_0=3900000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +8032 projected GDA2020 / PHG2020 118.25,-20.79,118.97,-20.1 3 1168 +proj=tmerc +lat_0=0 +lon_0=118.6 +k=1.00000135 +x_0=50000 +y_0=2500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +8035 projected WGS 84 / TM Zone 20N (ftUS) -62.09,9.83,-59.99,12.34 6326 +proj=tmerc +lat_0=0 +lon_0=-63 +k=0.9996 +x_0=500000.0001016 +y_0=0 +datum=WGS84 +to_meter=0.304800609601219 +no_defs +8036 projected WGS 84 / TM Zone 21N (ftUS) -60,9.95,-57.28,12.19 6326 +proj=tmerc +lat_0=0 +lon_0=-57 +k=0.9996 +x_0=500000.0001016 +y_0=0 +datum=WGS84 +to_meter=0.304800609601219 +no_defs +8042 geographic Gusterberg (Ferro) 12.07,46.93,16.83,51.06 1188 +proj=longlat +a=6376045 +rf=310 +pm=-17.6666666666667 +no_defs +8043 geographic St. Stephen (Ferro) 14.41,47.42,18.86,50.45 1189 +proj=longlat +a=6376045 +rf=310 +pm=-17.6666666666667 +no_defs +8044 projected Gusterberg Grid (Ferro) 12.07,46.93,16.83,51.06 1188 +proj=cass +axis=swu +lat_0=48.0384638888889 +lon_0=31.8041805555556 +x_0=0 +y_0=0 +a=6376045 +rf=310 +pm=-17.6666666666667 +units=m +no_defs +8045 projected St. Stephen Grid (Ferro) 14.41,47.42,18.86,50.45 1189 +proj=cass +axis=swu +lat_0=48.2087611111111 +lon_0=34.0409222222222 +x_0=0 +y_0=0 +a=6376045 +rf=310 +pm=-17.6666666666667 +units=m +no_defs +8058 projected GDA2020 / NSW Lambert 140.99,-37.53,153.69,-28.15 3 1168 +proj=lcc +lat_0=-33.25 +lon_0=147 +lat_1=-30.75 +lat_2=-35.75 +x_0=9300000 +y_0=4500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +8059 projected GDA2020 / SA Lambert 128.99,-38.13,141.01,-25.99 3 1168 +proj=lcc +lat_0=-32 +lon_0=135 +lat_1=-28 +lat_2=-36 +x_0=1000000 +y_0=2000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +8065 projected NAD83(2011) / PCCS zone 1 (ft) -111.57,31.43,-110.44,32.52 2 1116 +proj=omerc +lat_0=32.25 +lonc=-111.4 +alpha=45 +gamma=45 +k=1.00011 +x_0=48768 +y_0=243840 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +8066 projected NAD83(2011) / PCCS zone 2 (ft) -112.52,31.5,-111.56,32.51 2 1116 +proj=tmerc +lat_0=31.25 +lon_0=-112.166666666667 +k=1.00009 +x_0=548640 +y_0=304800 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +8067 projected NAD83(2011) / PCCS zone 3 (ft) -113.33,31.8,-112.51,32.51 2 1116 +proj=tmerc +lat_0=31.5 +lon_0=-113.166666666667 +k=1.000055 +x_0=182880 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +8068 projected NAD83(2011) / PCCS zone 4 (ft) -110.87,32.33,-110.61,32.49 2 1116 +proj=lcc +lat_1=30.5 +lat_0=30.5 +lon_0=-110.75 +k_0=0.9998 +x_0=9144 +y_0=-188976 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +8082 projected NAD83(CSRS)v6 / MTM NS 2010 zone 4 -63,44.64,-59.73,47.08 1197 +proj=tmerc +lat_0=0 +lon_0=-61.5 +k=0.9999 +x_0=24500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +8083 projected NAD83(CSRS)v6 / MTM NS 2010 zone 5 -66.28,43.41,-63,46.02 1197 +proj=tmerc +lat_0=0 +lon_0=-64.5 +k=0.9999 +x_0=25500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +8086 geographic ISN2016 -30.87,59.96,-5.55,69.59 1 1187 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +8088 projected ISN2016 / Lambert 2016 -30.87,59.96,-5.55,69.59 1 1187 +proj=lcc +lat_0=65 +lon_0=-19 +lat_1=64.25 +lat_2=65.75 +x_0=2700000 +y_0=300000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +8090 projected NAD83(HARN) / WISCRS Florence (m) -88.69,45.71,-88.05,46.03 1 6152 +proj=tmerc +lat_0=45.4388888888889 +lon_0=-88.1416666666667 +k=1.0000552095 +x_0=133502.6683 +y_0=0.0063 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +8091 projected NAD83(HARN) / WISCRS Florence (ftUS) -88.69,45.71,-88.05,46.03 1 6152 +proj=tmerc +lat_0=45.4388888888889 +lon_0=-88.1416666666667 +k=1.0000552095 +x_0=133502.668224536 +y_0=0.0064008128016256 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +8092 projected NAD83(HARN) / WISCRS Eau Claire (m) -91.66,44.59,-90.92,44.86 1 6152 +proj=lcc +lat_1=44.8722811263889 +lat_0=44.8722811263889 +lon_0=-91.2888888888889 +k_0=1.000035079 +x_0=120091.4402 +y_0=91687.9239 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +8093 projected NAD83(HARN) / WISCRS Eau Claire (ftUS) -91.66,44.59,-90.92,44.86 1 6152 +proj=lcc +lat_1=44.8722811263889 +lat_0=44.8722811263889 +lon_0=-91.2888888888889 +k_0=1.000035079 +x_0=120091.44018288 +y_0=91687.9239014478 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +8095 projected NAD83(HARN) / WISCRS Wood (m) -90.32,44.24,-89.72,44.69 1 6152 +proj=lcc +lat_1=44.3625954694444 +lat_0=44.3625954694444 +lon_0=-90 +k_0=1.0000421209 +x_0=208483.6173 +y_0=134589.754 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +8096 projected NAD83(HARN) / WISCRS Wood (ftUS) -90.32,44.24,-89.72,44.69 1 6152 +proj=lcc +lat_1=44.3625954694444 +lat_0=44.3625954694444 +lon_0=-90 +k_0=1.0000421209 +x_0=208483.617272035 +y_0=134589.753924308 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +8097 projected NAD83(HARN) / WISCRS Waushara (m) -89.6,43.98,-88.88,44.25 1 6152 +proj=lcc +lat_1=44.1139440458333 +lat_0=44.1139440458333 +lon_0=-89.2416666666667 +k_0=1.0000392096 +x_0=120091.4402 +y_0=45069.7587 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +8098 projected NAD83(HARN) / WISCRS Waushara (ftUS) -89.6,43.98,-88.88,44.25 1 6152 +proj=lcc +lat_1=44.1139440458333 +lat_0=44.1139440458333 +lon_0=-89.2416666666667 +k_0=1.0000392096 +x_0=120091.44018288 +y_0=45069.7588011176 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +8099 projected NAD83(HARN) / WISCRS Waupaca (m) -89.23,44.24,-88.6,44.69 1 6152 +proj=tmerc +lat_0=43.4202777777778 +lon_0=-88.8166666666667 +k=1.0000333645 +x_0=185013.9709 +y_0=0.007 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +8100 projected NAD83(HARN) / WISCRS Waupaca (ftUS) -89.23,44.24,-88.6,44.69 1 6152 +proj=tmerc +lat_0=43.4202777777778 +lon_0=-88.8166666666667 +k=1.0000333645 +x_0=185013.970942342 +y_0=0.00701041402082804 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +8101 projected NAD83(HARN) / WISCRS Waukesha (m) -88.55,42.84,-88.06,43.2 1 6152 +proj=tmerc +lat_0=42.5694444444444 +lon_0=-88.225 +k=1.0000346179 +x_0=208788.418 +y_0=0.0034 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +8102 projected NAD83(HARN) / WISCRS Waukesha (ftUS) -88.55,42.84,-88.06,43.2 1 6152 +proj=tmerc +lat_0=42.5694444444444 +lon_0=-88.225 +k=1.0000346179 +x_0=208788.417881636 +y_0=0.00335280670561341 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +8103 projected NAD83(HARN) / WISCRS Washington (m) -88.42,43.19,-88.03,43.55 1 6152 +proj=tmerc +lat_0=42.9180555555556 +lon_0=-88.0638888888889 +k=1.00003738 +x_0=120091.4415 +y_0=0.003 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +8104 projected NAD83(HARN) / WISCRS Washington (ftUS) -88.42,43.19,-88.03,43.55 1 6152 +proj=tmerc +lat_0=42.9180555555556 +lon_0=-88.0638888888889 +k=1.00003738 +x_0=120091.441402083 +y_0=0.00304800609601219 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +8105 projected NAD83(HARN) / WISCRS Washburn (m) -92.06,45.63,-91.54,46.16 1 6152 +proj=lcc +lat_1=45.9612198333333 +lat_0=45.9612198333333 +lon_0=-91.7833333333333 +k_0=1.0000475376 +x_0=234086.8682 +y_0=188358.6058 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +8106 projected NAD83(HARN) / WISCRS Washburn (ftUS) -92.06,45.63,-91.54,46.16 1 6152 +proj=lcc +lat_1=45.9612198333333 +lat_0=45.9612198333333 +lon_0=-91.7833333333333 +k_0=1.0000475376 +x_0=234086.868173736 +y_0=188358.605943612 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +8107 projected NAD83(HARN) / WISCRS Walworth (m) -88.78,42.49,-88.3,42.85 1 6152 +proj=lcc +lat_1=42.6694620969444 +lat_0=42.6694620969444 +lon_0=-88.5416666666667 +k_0=1.0000367192 +x_0=232562.8651 +y_0=111088.2224 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +8108 projected NAD83(HARN) / WISCRS Walworth (ftUS) -88.78,42.49,-88.3,42.85 1 6152 +proj=lcc +lat_1=42.6694620969444 +lat_0=42.6694620969444 +lon_0=-88.5416666666667 +k_0=1.0000367192 +x_0=232562.86512573 +y_0=111088.222402845 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +8109 projected NAD83(HARN) / WISCRS Vilas (m) -90.05,45.85,-88.93,46.3 1 6152 +proj=lcc +lat_1=46.0778440905556 +lat_0=46.0778440905556 +lon_0=-89.4888888888889 +k_0=1.0000730142 +x_0=134417.0689 +y_0=50337.1092 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +8110 projected NAD83(HARN) / WISCRS Vilas (ftUS) -90.05,45.85,-88.93,46.3 1 6152 +proj=lcc +lat_1=46.0778440905556 +lat_0=46.0778440905556 +lon_0=-89.4888888888889 +k_0=1.0000730142 +x_0=134417.068834138 +y_0=50337.1092710185 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +8111 projected NAD83(HARN) / WISCRS Vernon (m) -91.28,43.42,-90.31,43.74 1 6152 +proj=lcc +lat_1=43.5750329397222 +lat_0=43.5750329397222 +lon_0=-90.7833333333333 +k_0=1.0000408158 +x_0=222504.4451 +y_0=47532.0602 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +8112 projected NAD83(HARN) / WISCRS Vernon (ftUS) -91.28,43.42,-90.31,43.74 1 6152 +proj=lcc +lat_1=43.5750329397222 +lat_0=43.5750329397222 +lon_0=-90.7833333333333 +k_0=1.0000408158 +x_0=222504.44500889 +y_0=47532.0603505207 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +8113 projected NAD83(HARN) / WISCRS Trempealeau (m) -91.62,43.98,-91.15,44.6 1 6152 +proj=tmerc +lat_0=43.1611111111111 +lon_0=-91.3666666666667 +k=1.0000361538 +x_0=256946.9138 +y_0=0.0041 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +8114 projected NAD83(HARN) / WISCRS Trempealeau (ftUS) -91.62,43.98,-91.15,44.6 1 6152 +proj=tmerc +lat_0=43.1611111111111 +lon_0=-91.3666666666667 +k=1.0000361538 +x_0=256946.913893828 +y_0=0.00396240792481585 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +8115 projected NAD83(HARN) / WISCRS Taylor (m) -90.93,45.03,-90.04,45.39 1 6152 +proj=lcc +lat_1=45.1778220858333 +lat_0=45.1778220858333 +lon_0=-90.4833333333333 +k_0=1.0000597566 +x_0=187147.5744 +y_0=107746.7522 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +8116 projected NAD83(HARN) / WISCRS Taylor (ftUS) -90.93,45.03,-90.04,45.39 1 6152 +proj=lcc +lat_1=45.1778220858333 +lat_0=45.1778220858333 +lon_0=-90.4833333333333 +k_0=1.0000597566 +x_0=187147.574295149 +y_0=107746.752146304 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +8117 projected NAD83(HARN) / WISCRS St. Croix (m) -92.81,44.85,-92.13,45.22 1 6152 +proj=tmerc +lat_0=44.0361111111111 +lon_0=-92.6333333333333 +k=1.0000381803 +x_0=165506.7302 +y_0=0.0103 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +8118 projected NAD83(HARN) / WISCRS St. Croix (ftUS) -92.81,44.85,-92.13,45.22 1 6152 +proj=tmerc +lat_0=44.0361111111111 +lon_0=-92.6333333333333 +k=1.0000381803 +x_0=165506.73009906 +y_0=0.0103632207264415 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +8119 projected NAD83(HARN) / WISCRS Shawano (m) -89.23,44.58,-88.24,45.03 1 6152 +proj=tmerc +lat_0=44.0361111111111 +lon_0=-88.6055555555556 +k=1.000032144 +x_0=262433.3253 +y_0=0.0096 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +8120 projected NAD83(HARN) / WISCRS Shawano (ftUS) -89.23,44.58,-88.24,45.03 1 6152 +proj=tmerc +lat_0=44.0361111111111 +lon_0=-88.6055555555556 +k=1.000032144 +x_0=262433.32517145 +y_0=0.0094488188976378 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +8121 projected NAD83(HARN) / WISCRS Sawyer (m) -91.56,45.63,-90.67,46.16 1 6152 +proj=lcc +lat_1=45.9000991313889 +lat_0=45.9000991313889 +lon_0=-91.1166666666667 +k_0=1.0000573461 +x_0=216713.2336 +y_0=120734.1631 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +8122 projected NAD83(HARN) / WISCRS Sawyer (ftUS) -91.56,45.63,-90.67,46.16 1 6152 +proj=lcc +lat_1=45.9000991313889 +lat_0=45.9000991313889 +lon_0=-91.1166666666667 +k_0=1.0000573461 +x_0=216713.233731268 +y_0=120734.163169926 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +8123 projected NAD83(HARN) / WISCRS Sauk (m) -90.32,43.14,-89.59,43.65 1 6152 +proj=tmerc +lat_0=42.8194444444444 +lon_0=-89.9 +k=1.0000373868 +x_0=185623.5716 +y_0=0.0051 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +8124 projected NAD83(HARN) / WISCRS Sauk (ftUS) -90.32,43.14,-89.59,43.65 1 6152 +proj=tmerc +lat_0=42.8194444444444 +lon_0=-89.9 +k=1.0000373868 +x_0=185623.571551943 +y_0=0.00518161036322073 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +8125 projected NAD83(HARN) / WISCRS Rusk (m) -91.55,45.29,-90.67,45.64 1 6152 +proj=tmerc +lat_0=43.9194444444444 +lon_0=-91.0666666666667 +k=1.0000495976 +x_0=250546.1013 +y_0=0.0234 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +8126 projected NAD83(HARN) / WISCRS Rusk (ftUS) -91.55,45.29,-90.67,45.64 1 6152 +proj=tmerc +lat_0=43.9194444444444 +lon_0=-91.0666666666667 +k=1.0000495976 +x_0=250546.101397003 +y_0=0.0234696469392939 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +8127 projected NAD83(HARN) / WISCRS Rock (m) -89.37,42.49,-88.77,42.85 1 6152 +proj=tmerc +lat_0=41.9444444444444 +lon_0=-89.0722222222222 +k=1.0000337311 +x_0=146304.2926 +y_0=0.0068 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +8128 projected NAD83(HARN) / WISCRS Rock (ftUS) -89.37,42.49,-88.77,42.85 1 6152 +proj=tmerc +lat_0=41.9444444444444 +lon_0=-89.0722222222222 +k=1.0000337311 +x_0=146304.292608585 +y_0=0.00670561341122682 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +8129 projected NAD83(HARN) / WISCRS Richland (m) -90.68,43.16,-90.19,43.56 1 6152 +proj=lcc +lat_1=43.3223129275 +lat_0=43.3223129275 +lon_0=-90.4305555555556 +k_0=1.0000375653 +x_0=202387.6048 +y_0=134255.4253 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +8130 projected NAD83(HARN) / WISCRS Richland (ftUS) -90.68,43.16,-90.19,43.56 1 6152 +proj=lcc +lat_1=43.3223129275 +lat_0=43.3223129275 +lon_0=-90.4305555555556 +k_0=1.0000375653 +x_0=202387.60477521 +y_0=134255.425450851 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +8131 projected NAD83(HARN) / WISCRS Price (m) -90.68,45.37,-90.04,45.99 1 6152 +proj=tmerc +lat_0=44.5555555555556 +lon_0=-90.4888888888889 +k=1.0000649554 +x_0=227990.8546 +y_0=0.0109 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +8132 projected NAD83(HARN) / WISCRS Price (ftUS) -90.68,45.37,-90.04,45.99 1 6152 +proj=tmerc +lat_0=44.5555555555556 +lon_0=-90.4888888888889 +k=1.0000649554 +x_0=227990.854457709 +y_0=0.0109728219456439 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +8133 projected NAD83(HARN) / WISCRS Portage (m) -89.85,44.24,-89.22,44.69 1 6152 +proj=lcc +lat_1=44.4168239752778 +lat_0=44.4168239752778 +lon_0=-89.5 +k_0=1.000039936 +x_0=56388.1128 +y_0=50022.1874 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +8134 projected NAD83(HARN) / WISCRS Portage (ftUS) -89.85,44.24,-89.22,44.69 1 6152 +proj=lcc +lat_1=44.4168239752778 +lat_0=44.4168239752778 +lon_0=-89.5 +k_0=1.000039936 +x_0=56388.1127762256 +y_0=50022.1874523749 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +8135 projected NAD83(HARN) / WISCRS Polk (m) -92.89,45.2,-92.15,45.73 1 6152 +proj=tmerc +lat_0=44.6611111111111 +lon_0=-92.6333333333333 +k=1.0000433849 +x_0=141732.2823 +y_0=0.0059 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +8136 projected NAD83(HARN) / WISCRS Polk (ftUS) -92.89,45.2,-92.15,45.73 1 6152 +proj=tmerc +lat_0=44.6611111111111 +lon_0=-92.6333333333333 +k=1.0000433849 +x_0=141732.282245365 +y_0=0.00579121158242317 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +8137 projected NAD83(HARN) / WISCRS Pepin and Pierce (m) -92.81,44.4,-91.65,44.87 1 6152 +proj=lcc +lat_1=44.6361488719444 +lat_0=44.6361488719444 +lon_0=-92.2277777777778 +k_0=1.0000362977 +x_0=167640.3354 +y_0=86033.0876 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +8138 projected NAD83(HARN) / WISCRS Pepin and Pierce (ftUS) -92.81,44.4,-91.65,44.87 1 6152 +proj=lcc +lat_1=44.6361488719444 +lat_0=44.6361488719444 +lon_0=-92.2277777777778 +k_0=1.0000362977 +x_0=167640.335280671 +y_0=86033.0877317755 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +8139 projected NAD83(HARN) / WISCRS Oneida (m) -90.05,45.46,-89.04,45.91 1 6152 +proj=lcc +lat_1=45.7042237702778 +lat_0=45.7042237702778 +lon_0=-89.5444444444444 +k_0=1.0000686968 +x_0=70104.1401 +y_0=57588.0346 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +8140 projected NAD83(HARN) / WISCRS Oneida (ftUS) -90.05,45.46,-89.04,45.91 1 6152 +proj=lcc +lat_1=45.7042237702778 +lat_0=45.7042237702778 +lon_0=-89.5444444444444 +k_0=1.0000686968 +x_0=70104.1402082804 +y_0=57588.0347472695 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +8141 projected NAD83(HARN) / WISCRS Oconto (m) -88.69,44.67,-87.76,45.38 1 6152 +proj=tmerc +lat_0=44.3972222222222 +lon_0=-87.9083333333333 +k=1.0000236869 +x_0=182880.3676 +y_0=0.0033 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +8142 projected NAD83(HARN) / WISCRS Oconto (ftUS) -88.69,44.67,-87.76,45.38 1 6152 +proj=tmerc +lat_0=44.3972222222222 +lon_0=-87.9083333333333 +k=1.0000236869 +x_0=182880.367589535 +y_0=0.00335280670561341 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +8143 projected NAD83(HARN) / WISCRS Monroe (m) -90.98,43.72,-90.31,44.17 1 6152 +proj=lcc +lat_1=44.0000739286111 +lat_0=44.0000739286111 +lon_0=-90.6416666666667 +k_0=1.0000434122 +x_0=204521.209 +y_0=121923.9861 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +8144 projected NAD83(HARN) / WISCRS Monroe (ftUS) -90.98,43.72,-90.31,44.17 1 6152 +proj=lcc +lat_1=44.0000739286111 +lat_0=44.0000739286111 +lon_0=-90.6416666666667 +k_0=1.0000434122 +x_0=204521.209042418 +y_0=121923.986182372 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +8145 projected NAD83(HARN) / WISCRS Menominee (m) -88.99,44.85,-88.48,45.12 1 6152 +proj=tmerc +lat_0=44.7166666666667 +lon_0=-88.4166666666667 +k=1.0000362499 +x_0=105461.0121 +y_0=0.0029 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +8146 projected NAD83(HARN) / WISCRS Menominee (ftUS) -88.99,44.85,-88.48,45.12 1 6152 +proj=tmerc +lat_0=44.7166666666667 +lon_0=-88.4166666666667 +k=1.0000362499 +x_0=105461.012141224 +y_0=0.00304800609601219 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +8147 projected NAD83(HARN) / WISCRS Marinette (m) -88.43,44.96,-87.48,45.8 1 6152 +proj=tmerc +lat_0=44.6916666666667 +lon_0=-87.7111111111111 +k=1.0000234982 +x_0=238658.8794 +y_0=0.0032 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +8148 projected NAD83(HARN) / WISCRS Marinette (ftUS) -88.43,44.96,-87.48,45.8 1 6152 +proj=tmerc +lat_0=44.6916666666667 +lon_0=-87.7111111111111 +k=1.0000234982 +x_0=238658.879451359 +y_0=0.00304800609601219 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +8149 projected NAD83(HARN) / WISCRS Marathon (m) -90.32,44.68,-89.22,45.13 1 6152 +proj=lcc +lat_1=44.9009044236111 +lat_0=44.9009044236111 +lon_0=-89.77 +k_0=1.000053289 +x_0=74676.1493 +y_0=55049.2669 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +8150 projected NAD83(HARN) / WISCRS Marathon (ftUS) -90.32,44.68,-89.22,45.13 1 6152 +proj=lcc +lat_1=44.9009044236111 +lat_0=44.9009044236111 +lon_0=-89.77 +k_0=1.000053289 +x_0=74676.1493522987 +y_0=55049.2669545339 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +8151 projected NAD83(HARN) / WISCRS Lincoln (m) -90.05,45.11,-89.42,45.56 1 6152 +proj=tmerc +lat_0=44.8444444444444 +lon_0=-89.7333333333333 +k=1.0000599003 +x_0=116129.0323 +y_0=0.0058 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +8152 projected NAD83(HARN) / WISCRS Lincoln (ftUS) -90.05,45.11,-89.42,45.56 1 6152 +proj=tmerc +lat_0=44.8444444444444 +lon_0=-89.7333333333333 +k=1.0000599003 +x_0=116129.032258065 +y_0=0.00579121158242317 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +8153 projected NAD83(HARN) / WISCRS Langlade (m) -89.43,45.02,-88.63,45.48 1 6152 +proj=lcc +lat_1=45.1542371052778 +lat_0=45.1542371052778 +lon_0=-89.0333333333333 +k_0=1.0000627024 +x_0=198425.197 +y_0=105279.7829 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +8154 projected NAD83(HARN) / WISCRS Langlade (ftUS) -89.43,45.02,-88.63,45.48 1 6152 +proj=lcc +lat_1=45.1542371052778 +lat_0=45.1542371052778 +lon_0=-89.0333333333333 +k_0=1.0000627024 +x_0=198425.196850394 +y_0=105279.782880366 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +8155 projected NAD83(HARN) / WISCRS La Crosse (m) -91.43,43.72,-90.91,44.1 1 6152 +proj=tmerc +lat_0=43.4511111111111 +lon_0=-91.3166666666667 +k=1.0000319985 +x_0=130454.6598 +y_0=0.0033 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +8156 projected NAD83(HARN) / WISCRS La Crosse (ftUS) -91.43,43.72,-90.91,44.1 1 6152 +proj=tmerc +lat_0=43.4511111111111 +lon_0=-91.3166666666667 +k=1.0000319985 +x_0=130454.659690119 +y_0=0.00335280670561341 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +8157 projected NAD83(HARN) / WISCRS Kewaunee, Manitowoc and Sheboygan (m) -88.17,43.54,-87.37,44.68 1 6152 +proj=tmerc +lat_0=43.2666666666667 +lon_0=-87.55 +k=1.0000233704 +x_0=79857.7614 +y_0=0.0012 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +8158 projected NAD83(HARN) / WISCRS Kewaunee, Manitowoc and Sheboygan (ftUS) -88.17,43.54,-87.37,44.68 1 6152 +proj=tmerc +lat_0=43.2666666666667 +lon_0=-87.55 +k=1.0000233704 +x_0=79857.7615443231 +y_0=0.00121920243840488 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +8159 projected NAD83(HARN) / WISCRS Kenosha, Milwaukee, Ozaukee and Racine (m) -88.31,42.49,-87.75,43.55 1 6152 +proj=tmerc +lat_0=42.2166666666667 +lon_0=-87.8944444444444 +k=1.0000260649 +x_0=185928.3728 +y_0=0.0009 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +8160 projected NAD83(HARN) / WISCRS Kenosha, Milwaukee, Ozaukee and Racine (ftUS) -88.31,42.49,-87.75,43.55 1 6152 +proj=tmerc +lat_0=42.2166666666667 +lon_0=-87.8944444444444 +k=1.0000260649 +x_0=185928.372771146 +y_0=0.000914401828803658 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +8161 projected NAD83(HARN) / WISCRS Jackson (m) -91.17,44.07,-90.31,44.6 1 6152 +proj=tmerc +lat_0=44.2533351277778 +lon_0=-90.8442965194444 +k=1.0000353 +x_0=27000 +y_0=25000 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +8162 projected NAD83(HARN) / WISCRS Jackson (ftUS) -91.17,44.07,-90.31,44.6 1 6152 +proj=tmerc +lat_0=44.2533351277778 +lon_0=-90.8442965194444 +k=1.0000353 +x_0=27000 +y_0=24999.9998983998 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +8163 projected NAD83(HARN) / WISCRS Iron (m) -90.56,45.98,-89.92,46.6 1 6152 +proj=tmerc +lat_0=45.4333333333333 +lon_0=-90.2555555555556 +k=1.0000677153 +x_0=220980.4419 +y_0=0.0085 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +8164 projected NAD83(HARN) / WISCRS Iron (ftUS) -90.56,45.98,-89.92,46.6 1 6152 +proj=tmerc +lat_0=45.4333333333333 +lon_0=-90.2555555555556 +k=1.0000677153 +x_0=220980.441960884 +y_0=0.00853441706883414 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +8165 projected NAD83(HARN) / WISCRS Iowa (m) -90.43,42.81,-89.83,43.21 1 6152 +proj=tmerc +lat_0=42.5388888888889 +lon_0=-90.1611111111111 +k=1.0000394961 +x_0=113081.0261 +y_0=0.0045 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +8166 projected NAD83(HARN) / WISCRS Iowa (ftUS) -90.43,42.81,-89.83,43.21 1 6152 +proj=tmerc +lat_0=42.5388888888889 +lon_0=-90.1611111111111 +k=1.0000394961 +x_0=113081.026162052 +y_0=0.00457200914401829 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +8167 projected NAD83(HARN) / WISCRS Green Lake and Marquette (m) -89.6,43.63,-88.88,43.99 1 6152 +proj=lcc +lat_1=43.8070001177778 +lat_0=43.8070001177778 +lon_0=-89.2416666666667 +k_0=1.0000344057 +x_0=150876.3018 +y_0=79170.7795 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +8168 projected NAD83(HARN) / WISCRS Green Lake and Marquette (ftUS) -89.6,43.63,-88.88,43.99 1 6152 +proj=lcc +lat_1=43.8070001177778 +lat_0=43.8070001177778 +lon_0=-89.2416666666667 +k_0=1.0000344057 +x_0=150876.301752604 +y_0=79170.7793751588 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +8169 projected NAD83(HARN) / WISCRS Green and Lafayette (m) -90.43,42.5,-89.36,42.86 1 6152 +proj=lcc +lat_1=42.6375622769444 +lat_0=42.6375622769444 +lon_0=-89.8388888888889 +k_0=1.0000390487 +x_0=170078.7403 +y_0=45830.2947 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +8170 projected NAD83(HARN) / WISCRS Green and Lafayette (ftUS) -90.43,42.5,-89.36,42.86 1 6152 +proj=lcc +lat_1=42.6375622769444 +lat_0=42.6375622769444 +lon_0=-89.8388888888889 +k_0=1.0000390487 +x_0=170078.74015748 +y_0=45830.2948437897 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +8171 projected NAD83(HARN) / WISCRS Grant (m) -91.16,42.5,-90.42,43.22 1 6152 +proj=tmerc +lat_0=41.4111111111111 +lon_0=-90.8 +k=1.0000349452 +x_0=242316.4841 +y_0=0.01 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +8172 projected NAD83(HARN) / WISCRS Grant (ftUS) -91.16,42.5,-90.42,43.22 1 6152 +proj=tmerc +lat_0=41.4111111111111 +lon_0=-90.8 +k=1.0000349452 +x_0=242316.484023368 +y_0=0.0100584201168402 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +8173 projected NAD83(HARN) / WISCRS Forest (m) -89.05,45.37,-88.42,46.08 1 6152 +proj=tmerc +lat_0=44.0055555555556 +lon_0=-88.6333333333333 +k=1.0000673004 +x_0=275844.5533 +y_0=0.0157 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +8177 projected NAD83(HARN) / WISCRS Forest (ftUS) -89.05,45.37,-88.42,46.08 1 6152 +proj=tmerc +lat_0=44.0055555555556 +lon_0=-88.6333333333333 +k=1.0000673004 +x_0=275844.553213106 +y_0=0.0158496316992634 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +8179 projected NAD83(HARN) / WISCRS Dunn (m) -92.16,44.68,-91.64,45.21 1 6152 +proj=tmerc +lat_0=44.4083333333333 +lon_0=-91.8944444444444 +k=1.0000410324 +x_0=51816.104 +y_0=0.003 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +8180 projected NAD83(HARN) / WISCRS Dunn (ftUS) -92.16,44.68,-91.64,45.21 1 6152 +proj=tmerc +lat_0=44.4083333333333 +lon_0=-91.8944444444444 +k=1.0000410324 +x_0=51816.1039370079 +y_0=0.00304800609601219 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +8181 projected NAD83(HARN) / WISCRS Douglas (m) -92.3,46.15,-91.55,46.76 1 6152 +proj=tmerc +lat_0=45.8833333333333 +lon_0=-91.9166666666667 +k=1.0000385418 +x_0=59131.3183 +y_0=0.0041 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +8182 projected NAD83(HARN) / WISCRS Douglas (ftUS) -92.3,46.15,-91.55,46.76 1 6152 +proj=tmerc +lat_0=45.8833333333333 +lon_0=-91.9166666666667 +k=1.0000385418 +x_0=59131.3182626365 +y_0=0.00396240792481585 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +8184 projected NAD83(HARN) / WISCRS Door (m) -87.74,44.67,-86.8,45.43 1 6152 +proj=tmerc +lat_0=44.4 +lon_0=-87.2722222222222 +k=1.0000187521 +x_0=158801.1176 +y_0=0.0023 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +8185 projected NAD83(HARN) / WISCRS Door (ftUS) -87.74,44.67,-86.8,45.43 1 6152 +proj=tmerc +lat_0=44.4 +lon_0=-87.2722222222222 +k=1.0000187521 +x_0=158801.117602235 +y_0=0.00243840487680975 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +8187 projected NAD83(HARN) / WISCRS Dodge and Jefferson (m) -89.02,42.84,-88.4,43.64 1 6152 +proj=tmerc +lat_0=41.4722222222222 +lon_0=-88.775 +k=1.0000346418 +x_0=263347.7263 +y_0=0.0076 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +8189 projected NAD83(HARN) / WISCRS Dodge and Jefferson (ftUS) -89.02,42.84,-88.4,43.64 1 6152 +proj=tmerc +lat_0=41.4722222222222 +lon_0=-88.775 +k=1.0000346418 +x_0=263347.726390653 +y_0=0.00762001524003048 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +8191 projected NAD83(HARN) / WISCRS Dane (m) -89.84,42.84,-89,43.3 1 6152 +proj=lcc +lat_1=43.0695160375 +lat_0=43.0695160375 +lon_0=-89.4222222222222 +k_0=1.0000384786 +x_0=247193.2944 +y_0=146591.9896 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +8193 projected NAD83(HARN) / WISCRS Dane (ftUS) -89.84,42.84,-89,43.3 1 6152 +proj=lcc +lat_1=43.0695160375 +lat_0=43.0695160375 +lon_0=-89.4222222222222 +k_0=1.0000384786 +x_0=247193.294386589 +y_0=146591.989636779 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +8196 projected NAD83(HARN) / WISCRS Crawford (m) -91.22,42.98,-90.66,43.43 1 6152 +proj=lcc +lat_1=43.200055605 +lat_0=43.200055605 +lon_0=-90.9388888888889 +k_0=1.0000349151 +x_0=113690.6274 +y_0=53703.1201 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +8197 projected NAD83(HARN) / WISCRS Crawford (ftUS) -91.22,42.98,-90.66,43.43 1 6152 +proj=lcc +lat_1=43.200055605 +lat_0=43.200055605 +lon_0=-90.9388888888889 +k_0=1.0000349151 +x_0=113690.627381255 +y_0=53703.1202438405 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +8198 projected NAD83(HARN) / WISCRS Columbia (m) -89.79,43.28,-89,43.65 1 6152 +proj=lcc +lat_1=43.4625466458333 +lat_0=43.4625466458333 +lon_0=-89.3944444444444 +k_0=1.00003498 +x_0=169164.3381 +y_0=111569.6134 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +8200 projected NAD83(HARN) / WISCRS Columbia (ftUS) -89.79,43.28,-89,43.65 1 6152 +proj=lcc +lat_1=43.4625466458333 +lat_0=43.4625466458333 +lon_0=-89.3944444444444 +k_0=1.00003498 +x_0=169164.338023876 +y_0=111569.613512827 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +8201 projected NAD83(HARN) / WISCRS Clark (m) -90.93,44.42,-90.31,45.04 1 6152 +proj=tmerc +lat_0=43.6 +lon_0=-90.7083333333333 +k=1.0000463003 +x_0=199949.1989 +y_0=0.0086 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +8202 projected NAD83(HARN) / WISCRS Clark (ftUS) -90.93,44.42,-90.31,45.04 1 6152 +proj=tmerc +lat_0=43.6 +lon_0=-90.7083333333333 +k=1.0000463003 +x_0=199949.198983998 +y_0=0.00853441706883414 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +8203 projected NAD83(HARN) / WISCRS Chippewa (m) -91.67,44.85,-90.92,45.3 1 6152 +proj=lcc +lat_1=44.9778568986111 +lat_0=44.9778568986111 +lon_0=-91.2944444444444 +k_0=1.0000391127 +x_0=60045.72 +y_0=44091.4346 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +8204 projected NAD83(HARN) / WISCRS Chippewa (ftUS) -91.67,44.85,-90.92,45.3 1 6152 +proj=lcc +lat_1=44.9778568986111 +lat_0=44.9778568986111 +lon_0=-91.2944444444444 +k_0=1.0000391127 +x_0=60045.7200914402 +y_0=44091.434493269 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +8205 projected NAD83(HARN) / WISCRS Calumet, Fond du Lac, Outagamie and Winnebago (m) -88.89,43.54,-88.04,44.6 1 6152 +proj=tmerc +lat_0=42.7194444444444 +lon_0=-88.5 +k=1.0000286569 +x_0=244754.8893 +y_0=0.0049 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +8206 projected NAD83(HARN) / WISCRS Calumet, Fond du Lac, Outagamie and Winnebago (ftUS) -88.89,43.54,-88.04,44.6 1 6152 +proj=tmerc +lat_0=42.7194444444444 +lon_0=-88.5 +k=1.0000286569 +x_0=244754.889204978 +y_0=0.00487680975361951 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +8207 projected NAD83(HARN) / WISCRS Burnett (m) -92.89,45.63,-92.03,46.16 1 6152 +proj=lcc +lat_1=45.8987148658333 +lat_0=45.8987148658333 +lon_0=-92.4577777777778 +k_0=1.0000383841 +x_0=64008.1276 +y_0=59445.9043 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +8208 projected NAD83(HARN) / WISCRS Burnett (ftUS) -92.89,45.63,-92.03,46.16 1 6152 +proj=lcc +lat_1=45.8987148658333 +lat_0=45.8987148658333 +lon_0=-92.4577777777778 +k_0=1.0000383841 +x_0=64008.1277114554 +y_0=59445.9041910084 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +8209 projected NAD83(HARN) / WISCRS Buffalo (m) -92.09,44.02,-91.52,44.6 1 6152 +proj=tmerc +lat_0=43.4813888888889 +lon_0=-91.7972222222222 +k=1.0000382778 +x_0=175260.3502 +y_0=0.0048 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +8210 projected NAD83(HARN) / WISCRS Buffalo (ftUS) -92.09,44.02,-91.52,44.6 1 6152 +proj=tmerc +lat_0=43.4813888888889 +lon_0=-91.7972222222222 +k=1.0000382778 +x_0=175260.3502159 +y_0=0.00487680975361951 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +8212 projected NAD83(HARN) / WISCRS Brown (m) -88.26,44.24,-87.76,44.68 1 6152 +proj=tmerc +lat_0=43 +lon_0=-88 +k=1.00002 +x_0=31600 +y_0=4600 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +8213 projected NAD83(HARN) / WISCRS Brown (ftUS) -88.26,44.24,-87.76,44.68 1 6152 +proj=tmerc +lat_0=43 +lon_0=-88 +k=1.00002 +x_0=31599.9998983998 +y_0=4599.9998983998 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +8214 projected NAD83(HARN) / WISCRS Bayfield (m) -91.56,46.15,-90.75,47.01 1 6152 +proj=lcc +lat_1=46.6696483772222 +lat_0=46.6696483772222 +lon_0=-91.1527777777778 +k_0=1.0000331195 +x_0=228600.4575 +y_0=148551.4837 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +8216 projected NAD83(HARN) / WISCRS Bayfield (ftUS) -91.56,46.15,-90.75,47.01 1 6152 +proj=lcc +lat_1=46.6696483772222 +lat_0=46.6696483772222 +lon_0=-91.1527777777778 +k_0=1.0000331195 +x_0=228600.457505715 +y_0=148551.483566167 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +8218 projected NAD83(HARN) / WISCRS Barron (m) -92.16,45.2,-91.53,45.65 1 6152 +proj=tmerc +lat_0=45.1333333333333 +lon_0=-91.85 +k=1.0000486665 +x_0=93150 +y_0=0.0029 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +8220 projected NAD83(HARN) / WISCRS Barron (ftUS) -92.16,45.2,-91.53,45.65 1 6152 +proj=tmerc +lat_0=45.1333333333333 +lon_0=-91.85 +k=1.0000486665 +x_0=93150 +y_0=0.00304800609601219 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +8222 projected NAD83(HARN) / WISCRS Ashland (m) -90.93,45.98,-90.3,47.09 1 6152 +proj=tmerc +lat_0=45.7061111111111 +lon_0=-90.6222222222222 +k=1.0000495683 +x_0=172821.9461 +y_0=0.0017 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +8224 projected NAD83(HARN) / WISCRS Ashland (ftUS) -90.93,45.98,-90.3,47.09 1 6152 +proj=tmerc +lat_0=45.7061111111111 +lon_0=-90.6222222222222 +k=1.0000495683 +x_0=172821.945948692 +y_0=0.00182880365760732 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +8225 projected NAD83(HARN) / WISCRS Adams and Juneau (m) -90.32,43.64,-89.59,44.25 1 6152 +proj=tmerc +lat_0=43.3666666666667 +lon_0=-90 +k=1.0000365285 +x_0=147218.6942 +y_0=0.0037 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +8226 projected NAD83(HARN) / WISCRS Adams and Juneau (ftUS) -90.32,43.64,-89.59,44.25 1 6152 +proj=tmerc +lat_0=43.3666666666667 +lon_0=-90 +k=1.0000365285 +x_0=147218.694132588 +y_0=0.00365760731521463 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +8232 geographic NAD83(CSRS96) -141.01,38.21,-40.73,86.46 1192 +proj=longlat +ellps=GRS80 +no_defs +8237 geographic NAD83(CSRS)v2 -141.01,38.21,-40.73,86.46 1193 +proj=longlat +ellps=GRS80 +no_defs +8240 geographic NAD83(CSRS)v3 -141.01,38.21,-40.73,86.46 1194 +proj=longlat +ellps=GRS80 +no_defs +8246 geographic NAD83(CSRS)v4 -141.01,38.21,-40.73,86.46 1195 +proj=longlat +ellps=GRS80 +no_defs +8249 geographic NAD83(CSRS)v5 -141.01,38.21,-40.73,86.46 1196 +proj=longlat +ellps=GRS80 +no_defs +8252 geographic NAD83(CSRS)v6 -141.01,38.21,-40.73,86.46 1197 +proj=longlat +ellps=GRS80 +no_defs +8255 geographic NAD83(CSRS)v7 -141.01,38.21,-40.73,86.46 1198 +proj=longlat +ellps=GRS80 +no_defs +8311 projected NAD83(2011) / Oregon Burns-Harper zone (m) -118.61,43.49,-117.49,44.19 2 1116 +proj=tmerc +lat_0=43.5 +lon_0=-117.666666666667 +k=1.00014 +x_0=90000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +8312 projected NAD83(2011) / Oregon Burns-Harper zone (ft) -118.61,43.49,-117.49,44.19 2 1116 +proj=tmerc +lat_0=43.5 +lon_0=-117.666666666667 +k=1.00014 +x_0=90000.00001488 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +8313 projected NAD83(2011) / Oregon Canyon City-Burns zone (m) -119.22,43.52,-118.52,44.36 2 1116 +proj=tmerc +lat_0=43.5 +lon_0=-119 +k=1.00022 +x_0=20000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +8314 projected NAD83(2011) / Oregon Canyon City-Burns zone (ft) -119.22,43.52,-118.52,44.36 2 1116 +proj=tmerc +lat_0=43.5 +lon_0=-119 +k=1.00022 +x_0=19999.99999992 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +8315 projected NAD83(2011) / Oregon Coast Range North zone (m) -123.81,45.4,-123.01,45.98 2 1116 +proj=lcc +lat_1=45.5833333333333 +lat_0=45.5833333333333 +lon_0=-123.416666666667 +k_0=1.000045 +x_0=30000 +y_0=20000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +8316 projected NAD83(2011) / Oregon Coast Range North zone (ft) -123.81,45.4,-123.01,45.98 2 1116 +proj=lcc +lat_1=45.5833333333333 +lat_0=45.5833333333333 +lon_0=-123.416666666667 +k_0=1.000045 +x_0=30000.00001512 +y_0=19999.99999992 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +8317 projected NAD83(2011) / Oregon Dayville-Prairie City zone (m) -119.89,44.24,-118.61,44.94 2 1116 +proj=tmerc +lat_0=44.25 +lon_0=-119.633333333333 +k=1.00012 +x_0=20000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +8318 projected NAD83(2011) / Oregon Dayville-Prairie City zone (ft) -119.89,44.24,-118.61,44.94 2 1116 +proj=tmerc +lat_0=44.25 +lon_0=-119.633333333333 +k=1.00012 +x_0=19999.99999992 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +8319 projected NAD83(2011) / Oregon Denio-Burns zone (m) -119.41,41.88,-117.67,43.54 2 1116 +proj=tmerc +lat_0=41.75 +lon_0=-118.416666666667 +k=1.00019 +x_0=80000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +8320 projected NAD83(2011) / Oregon Denio-Burns zone (ft) -119.41,41.88,-117.67,43.54 2 1116 +proj=tmerc +lat_0=41.75 +lon_0=-118.416666666667 +k=1.00019 +x_0=79999.99999968 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +8321 projected NAD83(2011) / Oregon Halfway zone (m) -117.61,44.61,-116.63,45.28 2 1116 +proj=lcc +lat_1=45.25 +lat_0=45.25 +lon_0=-117.25 +k_0=1.000085 +x_0=40000 +y_0=70000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +8322 projected NAD83(2011) / Oregon Halfway zone (ft) -117.61,44.61,-116.63,45.28 2 1116 +proj=lcc +lat_1=45.25 +lat_0=45.25 +lon_0=-117.25 +k_0=1.000085 +x_0=39999.99999984 +y_0=70000.00001496 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +8323 projected NAD83(2011) / Oregon Medford-Diamond Lake zone (m) -122.73,42.53,-121.71,43.34 2 1116 +proj=lcc +lat_1=42 +lat_0=42 +lon_0=-122.25 +k_0=1.00004 +x_0=60000 +y_0=-60000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +8324 projected NAD83(2011) / Oregon Medford-Diamond Lake zone (ft) -122.73,42.53,-121.71,43.34 2 1116 +proj=lcc +lat_1=42 +lat_0=42 +lon_0=-122.25 +k_0=1.00004 +x_0=59999.99999976 +y_0=-59999.99999976 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +8325 projected NAD83(2011) / Oregon Mitchell zone (m) -120.56,44.38,-119.82,44.78 2 1116 +proj=lcc +lat_1=47 +lat_0=47 +lon_0=-120.25 +k_0=0.99927 +x_0=30000 +y_0=290000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +8326 projected NAD83(2011) / Oregon Mitchell zone (ft) -120.56,44.38,-119.82,44.78 2 1116 +proj=lcc +lat_1=47 +lat_0=47 +lon_0=-120.25 +k_0=0.99927 +x_0=30000.00001512 +y_0=290000.00001408 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +8327 projected NAD83(2011) / Oregon North Central zone (m) -121.79,44.89,-119.03,45.95 2 1116 +proj=lcc +lat_1=46.1666666666667 +lat_0=46.1666666666667 +lon_0=-120.5 +k_0=1 +x_0=100000 +y_0=140000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +8328 projected NAD83(2011) / Oregon North Central zone (ft) -121.79,44.89,-119.03,45.95 2 1116 +proj=lcc +lat_1=46.1666666666667 +lat_0=46.1666666666667 +lon_0=-120.5 +k_0=1 +x_0=99999.9999996 +y_0=139999.99999944 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +8329 projected NAD83(2011) / Oregon Ochoco Summit zone (m) -121.01,44.21,-120.31,44.61 2 1116 +proj=lcc +lat_1=43.5 +lat_0=43.5 +lon_0=-120.5 +k_0=1.00006 +x_0=40000 +y_0=-80000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +8330 projected NAD83(2011) / Oregon Ochoco Summit zone (ft) -121.01,44.21,-120.31,44.61 2 1116 +proj=lcc +lat_1=43.5 +lat_0=43.5 +lon_0=-120.5 +k_0=1.00006 +x_0=39999.99999984 +y_0=-79999.99999968 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +8331 projected NAD83(2011) / Oregon Owyhee zone (m) -118.14,41.88,-116.85,43.54 2 1116 +proj=tmerc +lat_0=41.75 +lon_0=-117.583333333333 +k=1.00018 +x_0=70000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +8332 projected NAD83(2011) / Oregon Owyhee zone (ft) -118.14,41.88,-116.85,43.54 2 1116 +proj=tmerc +lat_0=41.75 +lon_0=-117.583333333333 +k=1.00018 +x_0=70000.00001496 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +8333 projected NAD83(2011) / Oregon Pilot Rock-Ukiah zone (m) -119.65,44.99,-118.11,46.04 2 1116 +proj=lcc +lat_1=46.1666666666667 +lat_0=46.1666666666667 +lon_0=-119 +k_0=1.000025 +x_0=50000 +y_0=130000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +8334 projected NAD83(2011) / Oregon Pilot Rock-Ukiah zone (ft) -119.65,44.99,-118.11,46.04 2 1116 +proj=lcc +lat_1=46.1666666666667 +lat_0=46.1666666666667 +lon_0=-119 +k_0=1.000025 +x_0=50000.00001504 +y_0=130000.00001472 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +8335 projected NAD83(2011) / Oregon Prairie City-Brogan zone (m) -118.77,44.16,-117.48,45.02 2 1116 +proj=lcc +lat_1=44 +lat_0=44 +lon_0=-118 +k_0=1.00017 +x_0=60000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +8336 projected NAD83(2011) / Oregon Prairie City-Brogan zone (ft) -118.77,44.16,-117.48,45.02 2 1116 +proj=lcc +lat_1=44 +lat_0=44 +lon_0=-118 +k_0=1.00017 +x_0=59999.99999976 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +8337 projected NAD83(2011) / Oregon Riley-Lakeview zone (m) -120.97,41.88,-119.15,43.45 2 1116 +proj=tmerc +lat_0=41.75 +lon_0=-120.333333333333 +k=1.000215 +x_0=70000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +8338 projected NAD83(2011) / Oregon Riley-Lakeview zone (ft) -120.97,41.88,-119.15,43.45 2 1116 +proj=tmerc +lat_0=41.75 +lon_0=-120.333333333333 +k=1.000215 +x_0=70000.00001496 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +8339 projected NAD83(2011) / Oregon Siskiyou Pass zone (m) -122.71,41.95,-121.96,42.46 2 1116 +proj=lcc +lat_1=42.5 +lat_0=42.5 +lon_0=-122.583333333333 +k_0=1.00015 +x_0=10000 +y_0=60000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +8340 projected NAD83(2011) / Oregon Siskiyou Pass zone (ft) -122.71,41.95,-121.96,42.46 2 1116 +proj=lcc +lat_1=42.5 +lat_0=42.5 +lon_0=-122.583333333333 +k_0=1.00015 +x_0=10000.0000152 +y_0=59999.99999976 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +8341 projected NAD83(2011) / Oregon Ukiah-Fox zone (m) -119.35,44.52,-118.64,45.21 2 1116 +proj=lcc +lat_1=45.25 +lat_0=45.25 +lon_0=-119 +k_0=1.00014 +x_0=30000 +y_0=90000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +8342 projected NAD83(2011) / Oregon Ukiah-Fox zone (ft) -119.35,44.52,-118.64,45.21 2 1116 +proj=lcc +lat_1=45.25 +lat_0=45.25 +lon_0=-119 +k_0=1.00014 +x_0=30000.00001512 +y_0=90000.00001488 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +8343 projected NAD83(2011) / Oregon Wallowa zone (m) -118.16,45.27,-116.42,46.05 2 1116 +proj=tmerc +lat_0=45.25 +lon_0=-117.5 +k=1.000195 +x_0=60000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +8344 projected NAD83(2011) / Oregon Wallowa zone (ft) -118.16,45.27,-116.42,46.05 2 1116 +proj=tmerc +lat_0=45.25 +lon_0=-117.5 +k=1.000195 +x_0=59999.99999976 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +8345 projected NAD83(2011) / Oregon Warner Highway zone (m) -120.42,41.95,-119.3,42.43 2 1116 +proj=lcc +lat_1=42.5 +lat_0=42.5 +lon_0=-120 +k_0=1.000245 +x_0=40000 +y_0=60000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +8346 projected NAD83(2011) / Oregon Warner Highway zone (ft) -120.42,41.95,-119.3,42.43 2 1116 +proj=lcc +lat_1=42.5 +lat_0=42.5 +lon_0=-120 +k_0=1.000245 +x_0=39999.99999984 +y_0=59999.99999976 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +8347 projected NAD83(2011) / Oregon Willamette Pass zone (m) -122.26,42.99,-121.48,44.18 2 1116 +proj=tmerc +lat_0=43 +lon_0=-122 +k=1.000223 +x_0=20000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +8348 projected NAD83(2011) / Oregon Willamette Pass zone (ft) -122.26,42.99,-121.48,44.18 2 1116 +proj=tmerc +lat_0=43 +lon_0=-122 +k=1.000223 +x_0=19999.99999992 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +8351 geographic S-JTSK [JTSK03] 16.84,47.73,22.56,49.61 1 1201 +proj=longlat +ellps=bessel +towgs84=485.021,169.465,483.839,7.786342,4.397554,4.102655,0 +no_defs +8352 projected S-JTSK [JTSK03] / Krovak 16.84,47.73,22.56,49.61 1 1201 +proj=krovak +axis=swu +lat_0=49.5 +lon_0=24.8333333333333 +alpha=30.2881397527778 +k=0.9999 +x_0=0 +y_0=0 +ellps=bessel +towgs84=485.021,169.465,483.839,7.786342,4.397554,4.102655,0 +units=m +no_defs +8353 projected S-JTSK [JTSK03] / Krovak East North 16.84,47.73,22.56,49.61 1 1201 +proj=krovak +lat_0=49.5 +lon_0=24.8333333333333 +alpha=30.2881397527778 +k=0.9999 +x_0=0 +y_0=0 +ellps=bessel +towgs84=485.021,169.465,483.839,7.786342,4.397554,4.102655,0 +units=m +no_defs +8379 projected NAD83 / NCRS Las Vegas (m) -115.5,35.88,-114.71,36.43 4 6269 +proj=tmerc +lat_0=36.25 +lon_0=-114.966666666667 +k=1.0001 +x_0=100000 +y_0=200000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +8380 projected NAD83 / NCRS Las Vegas (ftUS) -115.5,35.88,-114.71,36.43 4 6269 +proj=tmerc +lat_0=36.25 +lon_0=-114.966666666667 +k=1.0001 +x_0=99999.99998984 +y_0=200000.00001016 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +8381 projected NAD83 / NCRS Las Vegas high (m) -115.5,35.88,-114.71,36.43 4 6269 +proj=tmerc +lat_0=36.25 +lon_0=-114.966666666667 +k=1.000135 +x_0=300000 +y_0=400000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +8382 projected NAD83 / NCRS Las Vegas high (ftUS) -115.5,35.88,-114.71,36.43 4 6269 +proj=tmerc +lat_0=36.25 +lon_0=-114.966666666667 +k=1.000135 +x_0=300000 +y_0=399999.99998984 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +8383 projected NAD83(2011) / NCRS Las Vegas (m) -115.5,35.88,-114.71,36.43 2 1116 +proj=tmerc +lat_0=36.25 +lon_0=-114.966666666667 +k=1.0001 +x_0=100000 +y_0=200000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +8384 projected NAD83(2011) / NCRS Las Vegas (ftUS) -115.5,35.88,-114.71,36.43 2 1116 +proj=tmerc +lat_0=36.25 +lon_0=-114.966666666667 +k=1.0001 +x_0=99999.99998984 +y_0=200000.00001016 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +8385 projected NAD83(2011) / NCRS Las Vegas high (m) -115.5,35.88,-114.71,36.43 2 1116 +proj=tmerc +lat_0=36.25 +lon_0=-114.966666666667 +k=1.000135 +x_0=300000 +y_0=400000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +8387 projected NAD83(2011) / NCRS Las Vegas high (ftUS) -115.5,35.88,-114.71,36.43 2 1116 +proj=tmerc +lat_0=36.25 +lon_0=-114.966666666667 +k=1.000135 +x_0=300000 +y_0=399999.99998984 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +8391 projected GDA94 / WEIPA94 141.5,-13.5,142.51,-11.49 3 6283 +proj=tmerc +lat_0=0 +lon_0=141 +k=0.999929 +x_0=300000 +y_0=2000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +8395 projected ETRS89 / Gauss-Kruger CM 9E 8.33,53.36,10.41,53.99 1 6258 +proj=tmerc +lat_0=0 +lon_0=9 +k=1 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +8427 geographic Hong Kong Geodetic CS 113.76,22.13,114.51,22.58 1 1209 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +8428 geographic Macao 1920 113.52,22.06,113.68,22.23 1207 +proj=longlat +ellps=intl +no_defs +8431 geographic Macao 2008 113.52,22.06,113.68,22.23 1 1208 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +8433 projected Macao 1920 / Macao Grid 113.52,22.06,113.68,22.23 1207 +proj=tmerc +lat_0=22.2123972222222 +lon_0=113.536469444444 +k=1 +x_0=20000 +y_0=20000 +ellps=intl +units=m +no_defs +8441 projected Tananarive / Laborde Grid 43.18,-25.64,50.56,-11.89 3 6297 +proj=labrd +lat_0=-18.9 +lon_0=46.4372291666667 +azi=18.9 +k=0.9995 +x_0=400000 +y_0=800000 +ellps=intl +towgs84=-198.383,-240.517,-107.909 +units=m +no_defs +8455 projected RGTAAF07 / UTM zone 53S 136,-66.73,138.01,-65.61 1 1113 +proj=utm +zone=53 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +8456 projected RGTAAF07 / UTM zone 54S 138,-67.13,142,-66.1 1 1113 +proj=utm +zone=54 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +8518 projected NAD83(2011) / KS RCS zone 1 -102.06,38.69,-101.38,40.01 2 1116 +proj=tmerc +lat_0=37.5 +lon_0=-101.6 +k=1.000156 +x_0=457200.914401829 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +8519 projected NAD83(2011) / KS RCS zone 2 -101.49,38.69,-100.71,40.01 2 1116 +proj=tmerc +lat_0=37.5 +lon_0=-100.95 +k=1.000134 +x_0=762001.524003048 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +8520 projected NAD83(2011) / KS RCS zone 3 -100.82,38.69,-100.14,40.01 2 1116 +proj=tmerc +lat_0=37.5 +lon_0=-100.35 +k=1.000116 +x_0=1066802.13360427 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +8521 projected NAD83(2011) / KS RCS zone 4 -100.19,38.69,-99.03,40.01 2 1116 +proj=tmerc +lat_0=37.5 +lon_0=-99.45 +k=1.000082 +x_0=1371602.74320549 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +8522 projected NAD83(2011) / KS RCS zone 5 -99.07,38.26,-98.47,40.01 2 1116 +proj=tmerc +lat_0=37.5 +lon_0=-98.6666666666667 +k=1.000078 +x_0=1676403.35280671 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +8523 projected NAD83(2011) / KS RCS zone 6 -98.51,38.15,-97.92,40.01 2 1116 +proj=tmerc +lat_0=37.5 +lon_0=-98.15 +k=1.000068 +x_0=1981203.96240793 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +8524 projected NAD83(2011) / KS RCS zone 7 -97.94,38.08,-96.8,40.01 2 1116 +proj=tmerc +lat_0=37.5 +lon_0=-97.3333333333333 +k=1.000049 +x_0=2286004.57200914 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +8525 projected NAD83(2011) / KS RCS zone 8 -96.97,38.73,-95.94,39.57 2 1116 +proj=lcc +lat_1=39.1666666666667 +lat_0=39.1666666666667 +lon_0=-96.5 +k_0=1.000044 +x_0=2590805.18161036 +y_0=182880.365760732 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +8526 projected NAD83(2011) / KS RCS zone 9 -96.94,38.08,-95.94,38.88 2 1116 +proj=lcc +lat_1=38.5 +lat_0=38.5 +lon_0=-96.5 +k_0=1.00005 +x_0=2895605.79121158 +y_0=91440.1828803658 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +8527 projected NAD83(2011) / KS RCS zone 10 -96.81,39.21,-94.85,40.01 2 1116 +proj=lcc +lat_1=39.6333333333333 +lat_0=39.6333333333333 +lon_0=-95.75 +k_0=1.00004 +x_0=3200406.4008128 +y_0=213360.426720853 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +8528 projected NAD83(2011) / KS RCS zone 11 -96.04,38.73,-94.58,39.43 2 1116 +proj=lcc +lat_1=39.1 +lat_0=39.1 +lon_0=-95.25 +k_0=1.000033 +x_0=3505207.01041402 +y_0=182880.365760732 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +8529 projected NAD83(2011) / KS RCS zone 12 -102.05,36.99,-101.06,38.71 2 1116 +proj=tmerc +lat_0=36.75 +lon_0=-101.416666666667 +k=1.00014 +x_0=3810007.62001524 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +8531 projected NAD83(2011) / KS RCS zone 13 -101.13,36.99,-100.08,38.71 2 1116 +proj=tmerc +lat_0=36.75 +lon_0=-100.4 +k=1.000109 +x_0=4114808.22961646 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +8533 projected NAD83(2011) / KS RCS zone 14 -100.25,36.99,-99.54,38.7 2 1116 +proj=tmerc +lat_0=36.75 +lon_0=-99.6666666666667 +k=1.000097 +x_0=4419608.83921768 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +8534 projected NAD83(2011) / KS RCS zone 15 -99.59,36.99,-98.91,38.7 2 1116 +proj=tmerc +lat_0=36.75 +lon_0=-99.2 +k=1.000087 +x_0=4724409.4488189 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +8535 projected NAD83(2011) / KS RCS zone 16 -99.03,36.99,-98.34,38.27 2 1116 +proj=tmerc +lat_0=36.75 +lon_0=-98.55 +k=1.000069 +x_0=5029210.05842012 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +8536 projected NAD83(2011) / KS RCS zone 17 -98.48,37.38,-96.52,38.18 2 1116 +proj=lcc +lat_1=37.7666666666667 +lat_0=37.7666666666667 +lon_0=-97.5 +k_0=1.000059 +x_0=5334010.66802134 +y_0=121920.243840488 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +8538 projected NAD83(2011) / KS RCS zone 18 -98.35,36.99,-96.52,37.48 2 1116 +proj=lcc +lat_1=37.1833333333333 +lat_0=37.1833333333333 +lon_0=-97.5 +k_0=1.000055 +x_0=5638811.27762256 +y_0=60960.1219202439 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +8539 projected NAD83(2011) / KS RCS zone 19 -96.53,36.99,-95.5,38.88 2 1116 +proj=tmerc +lat_0=36.75 +lon_0=-95.9666666666667 +k=1.000034 +x_0=5943611.88722378 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +8540 projected NAD83(2011) / KS RCS zone 20 -95.53,36.99,-94.6,38.74 2 1116 +proj=tmerc +lat_0=36.75 +lon_0=-95.0833333333333 +k=1.000031 +x_0=6248412.49682499 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +8545 geographic NAD83(HARN Corrected) -67.97,17.62,-64.51,18.57 1212 +proj=longlat +ellps=GRS80 +no_defs +8677 projected MGI 1901 / Balkans zone 5 13.38,42.95,16.5,46.88 5 1031 +proj=tmerc +lat_0=0 +lon_0=15 +k=0.9999 +x_0=5500000 +y_0=0 +ellps=bessel +towgs84=682,-203,480 +units=m +no_defs +8678 projected MGI 1901 / Balkans zone 6 16.5,41.79,19.51,46.55 5 1031 +proj=tmerc +lat_0=0 +lon_0=18 +k=0.9999 +x_0=6500000 +y_0=0 +ellps=bessel +towgs84=682,-203,480 +units=m +no_defs +8679 projected MGI 1901 / Balkans zone 8 22.5,41.11,23.04,44.7 5 1031 +proj=tmerc +lat_0=0 +lon_0=24 +k=0.9999 +x_0=8500000 +y_0=0 +ellps=bessel +towgs84=682,-203,480 +units=m +no_defs +8682 projected SRB_ETRS89 / UTM zone 34N 18.81,42.23,23.01,46.19 1 1214 +proj=utm +zone=34 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +8685 geographic SRB_ETRS89 18.81,42.23,23.01,46.19 1 1214 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +8686 projected MGI 1901 / Slovenia Grid 13.38,45.42,16.61,46.88 1 1031 +proj=tmerc +lat_0=0 +lon_0=15 +k=0.9999 +x_0=500000 +y_0=0 +ellps=bessel +towgs84=476.08,125.947,417.81,4.610862,2.388137,-11.942335,9.896638 +units=m +no_defs +8687 projected Slovenia 1996 / UTM zone 33N 13.38,45.42,16.61,46.88 1 6765 +proj=utm +zone=33 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +8692 projected NAD83(MA11) / UTM zone 54N 141.19,10.95,144.01,23.9 1118 +proj=utm +zone=54 +ellps=GRS80 +units=m +no_defs +8693 projected NAD83(MA11) / UTM zone 55N 144,11.05,149.55,23.9 1118 +proj=utm +zone=55 +ellps=GRS80 +units=m +no_defs +8694 geographic Camacupa 2015 8.2,-18.02,24.09,-4.38 3 1217 +proj=longlat +a=6378249.145 +rf=293.465 +towgs84=-93.799,-132.737,-219.073,-1.844,0.648,-6.37,-0.169 +no_defs +8699 geographic RSAO13 8.2,-18.02,24.09,-4.38 1 1220 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +8818 geographic MTRF-2000 34.44,16.29,55.67,32.16 1 1218 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +8826 projected NAD83 / Idaho Transverse Mercator -117.24,41.99,-111.04,49.01 4 6269 +proj=tmerc +lat_0=42 +lon_0=-114 +k=0.9996 +x_0=2500000 +y_0=1200000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +8836 projected MTRF-2000 / UTM zone 36N 34.44,24.92,36.01,29.38 1 1218 +proj=utm +zone=36 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +8837 projected MTRF-2000 / UTM zone 37N 36,16.29,42,32.16 1 1218 +proj=utm +zone=37 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +8838 projected MTRF-2000 / UTM zone 38N 41.99,16.35,48,31.15 1 1218 +proj=utm +zone=38 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +8839 projected MTRF-2000 / UTM zone 39N 47.99,17.94,54.01,28.94 1 1218 +proj=utm +zone=39 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +8840 projected MTRF-2000 / UTM zone 40N 54,19.66,55.67,22.77 1 1218 +proj=utm +zone=40 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +8857 projected WGS 84 / Equal Earth Greenwich -180,-90,180,90 6326 +proj=eqearth +lon_0=0 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +8858 projected WGS 84 / Equal Earth Americas 90.01,-90,89.99,90 6326 +proj=eqearth +lon_0=-90 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +8859 projected WGS 84 / Equal Earth Asia-Pacific -29.99,-90,-30.01,90 6326 +proj=eqearth +lon_0=150 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +8860 geographic NAD83(FBN) 144.58,-14.59,-64.51,49.38 1211 +proj=longlat +ellps=GRS80 +no_defs +8888 geographic WGS 84 (Transit) -180,-90,180,90 2 1166 +proj=longlat +ellps=WGS84 +towgs84=0,0,0 +no_defs +8900 geographic RGWF96 179.49,-15.94,-174.27,-9.84 1 1223 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +8902 geographic RGWF96 (lon-lat) 179.49,-15.94,-174.27,-9.84 1 1223 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +8903 projected RGWF96 / UTM zone 1S 179.49,-15.94,-174.27,-9.84 1 1223 +proj=utm +zone=1 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +8907 geographic CR-SIRGAS epoch 2014.59 -90.45,2.15,-81.43,11.77 1225 +proj=longlat +ellps=GRS80 +no_defs +8908 projected CR-SIRGAS epoch 2014.59 / CRTM05 -85.97,7.98,-82.53,11.22 1225 +proj=tmerc +lat_0=0 +lon_0=-84 +k=0.9999 +x_0=500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +8909 projected CR-SIRGAS epoch 2014.59 / UTM zone 16N -90.45,2.15,-82.92,11.11 1225 +proj=utm +zone=16 +ellps=GRS80 +units=m +no_defs +8910 projected CR-SIRGAS epoch 2014.59 / UTM zone 17N -83.6,9.6,-81.43,11.77 1225 +proj=utm +zone=17 +ellps=GRS80 +units=m +no_defs +8949 geographic SIRGAS-Chile 2010 -113.21,-59.87,-65.72,-17.5 22 1243 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +8950 projected SIRGAS-Chile 2010 / UTM zone 18S -78,-59.36,-71.99,-18.35 22 1243 +proj=utm +zone=18 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +8951 projected SIRGAS-Chile 2010 / UTM zone 19S -72,-59.87,-66,-17.5 22 1243 +proj=utm +zone=19 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +8972 geographic SIRGAS-CON DGF00P01 -122.19,-59.87,-25.28,32.72 1227 +proj=longlat +ellps=GRS80 +no_defs +8973 geographic SIRGAS-CON DGF01P01 -122.19,-59.87,-25.28,32.72 1228 +proj=longlat +ellps=GRS80 +no_defs +8974 geographic SIRGAS-CON DGF01P02 -122.19,-59.87,-25.28,32.72 1229 +proj=longlat +ellps=GRS80 +no_defs +8975 geographic SIRGAS-CON DGF02P01 -122.19,-59.87,-25.28,32.72 1230 +proj=longlat +ellps=GRS80 +no_defs +8976 geographic SIRGAS-CON DGF04P01 -122.19,-59.87,-25.28,32.72 1231 +proj=longlat +ellps=GRS80 +no_defs +8977 geographic SIRGAS-CON DGF05P01 -122.19,-59.87,-25.28,32.72 1232 +proj=longlat +ellps=GRS80 +no_defs +8978 geographic SIRGAS-CON DGF06P01 -122.19,-59.87,-25.28,32.72 1233 +proj=longlat +ellps=GRS80 +no_defs +8979 geographic SIRGAS-CON DGF07P01 -122.19,-59.87,-25.28,32.72 1234 +proj=longlat +ellps=GRS80 +no_defs +8980 geographic SIRGAS-CON DGF08P01 -122.19,-59.87,-25.28,32.72 1235 +proj=longlat +ellps=GRS80 +no_defs +8981 geographic SIRGAS-CON SIR09P01 -122.19,-59.87,-25.28,32.72 1236 +proj=longlat +ellps=GRS80 +no_defs +8982 geographic SIRGAS-CON SIR10P01 -122.19,-59.87,-25.28,32.72 1237 +proj=longlat +ellps=GRS80 +no_defs +8983 geographic SIRGAS-CON SIR11P01 -122.19,-59.87,-25.28,32.72 1238 +proj=longlat +ellps=GRS80 +no_defs +8984 geographic SIRGAS-CON SIR13P01 -122.19,-59.87,-25.28,32.72 1239 +proj=longlat +ellps=GRS80 +no_defs +8985 geographic SIRGAS-CON SIR14P01 -122.19,-59.87,-25.28,32.72 1240 +proj=longlat +ellps=GRS80 +no_defs +8986 geographic SIRGAS-CON SIR15P01 -122.19,-59.87,-25.28,32.72 1241 +proj=longlat +ellps=GRS80 +no_defs +8987 geographic SIRGAS-CON SIR17P01 -122.19,-59.87,-25.28,32.72 1242 +proj=longlat +ellps=GRS80 +no_defs +8988 geographic ITRF88 -180,-90,180,90 6647 +proj=longlat +ellps=GRS80 +no_defs +8989 geographic ITRF89 -180,-90,180,90 6648 +proj=longlat +ellps=GRS80 +no_defs +8990 geographic ITRF90 -180,-90,180,90 6649 +proj=longlat +ellps=GRS80 +no_defs +8991 geographic ITRF91 -180,-90,180,90 6650 +proj=longlat +ellps=GRS80 +no_defs +8992 geographic ITRF92 -180,-90,180,90 6651 +proj=longlat +ellps=GRS80 +no_defs +8993 geographic ITRF93 -180,-90,180,90 6652 +proj=longlat +ellps=GRS80 +no_defs +8994 geographic ITRF94 -180,-90,180,90 6653 +proj=longlat +ellps=GRS80 +no_defs +8995 geographic ITRF96 -180,-90,180,90 6654 +proj=longlat +ellps=GRS80 +no_defs +8996 geographic ITRF97 -180,-90,180,90 6655 +proj=longlat +ellps=GRS80 +no_defs +8997 geographic ITRF2000 -180,-90,180,90 1 6656 +proj=longlat +ellps=GRS80 +towgs84=-0.321999999999999,0.372,-0.153000000000006 +no_defs +8998 geographic ITRF2005 -180,-90,180,90 6896 +proj=longlat +ellps=GRS80 +no_defs +8999 geographic ITRF2008 -180,-90,180,90 1061 +proj=longlat +ellps=GRS80 +no_defs +9000 geographic ITRF2014 -180,-90,180,90 1165 +proj=longlat +ellps=GRS80 +no_defs +9003 geographic IGS97 -180,-90,180,90 1244 +proj=longlat +ellps=GRS80 +no_defs +9006 geographic IGS00 -180,-90,180,90 1245 +proj=longlat +ellps=GRS80 +no_defs +9009 geographic IGb00 -180,-90,180,90 1246 +proj=longlat +ellps=GRS80 +no_defs +9012 geographic IGS05 -180,-90,180,90 1247 +proj=longlat +ellps=GRS80 +no_defs +9014 geographic IGS08 -180,-90,180,90 1141 +proj=longlat +ellps=GRS80 +no_defs +9017 geographic IGb08 -180,-90,180,90 1248 +proj=longlat +ellps=GRS80 +no_defs +9019 geographic IGS14 -180,-90,180,90 1191 +proj=longlat +ellps=GRS80 +no_defs +9039 projected ISN2016 / LAEA Europe -30.87,59.96,-5.55,69.59 1 1187 +proj=laea +lat_0=52 +lon_0=10 +x_0=4321000 +y_0=3210000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +9040 projected ISN2016 / LCC Europe -30.87,59.96,-5.55,69.59 1 1187 +proj=lcc +lat_0=52 +lon_0=10 +lat_1=35 +lat_2=65 +x_0=4000000 +y_0=2800000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +9053 geographic WGS 84 (G730) -180,-90,180,90 2 1152 +proj=longlat +ellps=WGS84 +towgs84=0,0,0 +no_defs +9054 geographic WGS 84 (G873) -180,-90,180,90 2 1153 +proj=longlat +ellps=WGS84 +towgs84=0,0,0 +no_defs +9055 geographic WGS 84 (G1150) -180,-90,180,90 2 1154 +proj=longlat +ellps=WGS84 +towgs84=0,0,0 +no_defs +9056 geographic WGS 84 (G1674) -180,-90,180,90 2 1155 +proj=longlat +ellps=WGS84 +towgs84=0,0,0 +no_defs +9057 geographic WGS 84 (G1762) -180,-90,180,90 2 1156 +proj=longlat +ellps=WGS84 +towgs84=0,0,0 +no_defs +9059 geographic ETRF89 -16.1,33.26,38.01,84.73 1.1 1178 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +9060 geographic ETRF90 -16.1,33.26,38.01,84.73 1.1 1179 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +9061 geographic ETRF91 -16.1,33.26,38.01,84.73 1.1 1180 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +9062 geographic ETRF92 -16.1,33.26,38.01,84.73 1.1 1181 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +9063 geographic ETRF93 -16.1,33.26,38.01,84.73 1.1 1182 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +9064 geographic ETRF94 -16.1,33.26,38.01,84.73 1.1 1183 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +9065 geographic ETRF96 -16.1,33.26,38.01,84.73 1.1 1184 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +9066 geographic ETRF97 -16.1,33.26,38.01,84.73 1.1 1185 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +9067 geographic ETRF2000 -16.1,33.26,38.01,84.73 1.1 1186 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +9068 geographic ETRF2005 -16.1,33.26,38.01,84.73 1.1 1204 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +9069 geographic ETRF2014 -16.1,33.26,38.01,84.73 1.1 1206 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +9072 geographic NAD83(MARP00) 129.48,1.64,149.55,23.9 1221 +proj=longlat +ellps=GRS80 +no_defs +9075 geographic NAD83(PACP00) 157.47,-17.56,-151.27,31.8 1249 +proj=longlat +ellps=GRS80 +no_defs +9140 geographic KOSOVAREF01 19.97,41.85,21.8,43.25 1 1251 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +9141 projected KOSOVAREF01 / Balkans zone 7 19.97,41.85,21.8,43.25 1 1251 +proj=tmerc +lat_0=0 +lon_0=21 +k=0.9999 +x_0=7500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +9148 geographic SIRGAS-Chile 2013 -113.21,-59.87,-65.72,-17.5 22 1252 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +9149 projected SIRGAS-Chile 2013 / UTM zone 18S -78,-59.36,-71.99,-18.35 22 1252 +proj=utm +zone=18 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +9150 projected SIRGAS-Chile 2013 / UTM zone 19S -72,-59.87,-66,-17.5 22 1252 +proj=utm +zone=19 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +9153 geographic SIRGAS-Chile 2016 -113.21,-59.87,-65.72,-17.5 22 1253 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +9154 projected SIRGAS-Chile 2016 / UTM zone 18S -78,-59.36,-71.99,-18.35 22 1253 +proj=utm +zone=18 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +9155 projected SIRGAS-Chile 2016 / UTM zone 19S -72,-59.87,-66,-17.5 22 1253 +proj=utm +zone=19 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +9156 projected RSAO13 / UTM zone 32S 8.2,-17.28,12.01,-5.03 1 1220 +proj=utm +zone=32 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +9157 projected RSAO13 / UTM zone 33S 12,-17.44,18,-4.38 1 1220 +proj=utm +zone=33 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +9158 projected RSAO13 / UTM zone 34S 17.99,-18.02,24.09,-6.91 1 1220 +proj=utm +zone=34 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +9159 projected RSAO13 / TM 12 SE 8.2,-17.26,13.86,-6.01 1 1220 +proj=tmerc +lat_0=0 +lon_0=12 +k=0.9996 +x_0=500000 +y_0=10000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +9191 projected WGS 84 / NIWA Albers 155,-60,-169.99,-25 6326 +proj=aea +lat_0=-40 +lon_0=175 +lat_1=-30 +lat_2=-50 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +9205 projected VN-2000 / TM-3 103-00 102.14,20.89,103.99,22.82 1 6756 +proj=tmerc +lat_0=0 +lon_0=103 +k=0.9999 +x_0=500000 +y_0=0 +ellps=WGS84 +towgs84=-191.90441429,-39.30318279,-111.45032835,0.00928836,-0.01975479,0.00427372,0.252906278 +units=m +no_defs +9206 projected VN-2000 / TM-3 104-00 103.21,20.57,105.03,22.04 1 6756 +proj=tmerc +lat_0=0 +lon_0=104 +k=0.9999 +x_0=500000 +y_0=0 +ellps=WGS84 +towgs84=-191.90441429,-39.30318279,-111.45032835,0.00928836,-0.01975479,0.00427372,0.252906278 +units=m +no_defs +9207 projected VN-2000 / TM-3 104-30 103.4,8.33,105.54,10.55 1 6756 +proj=tmerc +lat_0=0 +lon_0=104.5 +k=0.9999 +x_0=500000 +y_0=0 +ellps=WGS84 +towgs84=-191.90441429,-39.30318279,-111.45032835,0.00928836,-0.01975479,0.00427372,0.252906278 +units=m +no_defs +9208 projected VN-2000 / TM-3 104-45 103.53,10.18,105.86,22.85 1 6756 +proj=tmerc +lat_0=0 +lon_0=104.75 +k=0.9999 +x_0=500000 +y_0=0 +ellps=WGS84 +towgs84=-191.90441429,-39.30318279,-111.45032835,0.00928836,-0.01975479,0.00427372,0.252906278 +units=m +no_defs +9209 projected VN-2000 / TM-3 105-30 104.33,9.19,106.69,23.4 1 6756 +proj=tmerc +lat_0=0 +lon_0=105.5 +k=0.9999 +x_0=500000 +y_0=0 +ellps=WGS84 +towgs84=-191.90441429,-39.30318279,-111.45032835,0.00928836,-0.01975479,0.00427372,0.252906278 +units=m +no_defs +9210 projected VN-2000 / TM-3 105-45 105.26,9.75,107.8,23.12 1 6756 +proj=tmerc +lat_0=0 +lon_0=105.75 +k=0.9999 +x_0=500000 +y_0=0 +ellps=WGS84 +towgs84=-191.90441429,-39.30318279,-111.45032835,0.00928836,-0.01975479,0.00427372,0.252906278 +units=m +no_defs +9211 projected VN-2000 / TM-3 106-00 104.83,16.92,107.03,22.7 1 6756 +proj=tmerc +lat_0=0 +lon_0=106 +k=0.9999 +x_0=500000 +y_0=0 +ellps=WGS84 +towgs84=-191.90441429,-39.30318279,-111.45032835,0.00928836,-0.01975479,0.00427372,0.252906278 +units=m +no_defs +9212 projected VN-2000 / TM-3 106-15 106.41,11.3,107.43,17.22 1 6756 +proj=tmerc +lat_0=0 +lon_0=106.25 +k=0.9999 +x_0=500000 +y_0=0 +ellps=WGS84 +towgs84=-191.90441429,-39.30318279,-111.45032835,0.00928836,-0.01975479,0.00427372,0.252906278 +units=m +no_defs +9213 projected VN-2000 / TM-3 106-30 105.43,21.32,106.25,22.75 1 6756 +proj=tmerc +lat_0=0 +lon_0=106.5 +k=0.9999 +x_0=500000 +y_0=0 +ellps=WGS84 +towgs84=-191.90441429,-39.30318279,-111.45032835,0.00928836,-0.01975479,0.00427372,0.252906278 +units=m +no_defs +9214 projected VN-2000 / TM-3 107-00 105.88,15.99,108.24,21.63 1 6756 +proj=tmerc +lat_0=0 +lon_0=107 +k=0.9999 +x_0=500000 +y_0=0 +ellps=WGS84 +towgs84=-191.90441429,-39.30318279,-111.45032835,0.00928836,-0.01975479,0.00427372,0.252906278 +units=m +no_defs +9215 projected VN-2000 / TM-3 107-15 106.09,21.32,107.37,22.47 1 6756 +proj=tmerc +lat_0=0 +lon_0=107.25 +k=0.9999 +x_0=500000 +y_0=0 +ellps=WGS84 +towgs84=-191.90441429,-39.30318279,-111.45032835,0.00928836,-0.01975479,0.00427372,0.252906278 +units=m +no_defs +9216 projected VN-2000 / TM-3 107-30 107.33,13.92,108.55,15.42 1 6756 +proj=tmerc +lat_0=0 +lon_0=107.5 +k=0.9999 +x_0=500000 +y_0=0 +ellps=WGS84 +towgs84=-191.90441429,-39.30318279,-111.45032835,0.00928836,-0.01975479,0.00427372,0.252906278 +units=m +no_defs +9217 projected VN-2000 / TM-3 108-15 108.55,11.25,109.53,14.71 1 6756 +proj=tmerc +lat_0=0 +lon_0=108.25 +k=0.9999 +x_0=500000 +y_0=0 +ellps=WGS84 +towgs84=-191.90441429,-39.30318279,-111.45032835,0.00928836,-0.01975479,0.00427372,0.252906278 +units=m +no_defs +9218 projected VN-2000 / TM-3 108-30 107.2,10.43,109.52,14.61 1 6756 +proj=tmerc +lat_0=0 +lon_0=108.5 +k=0.9999 +x_0=500000 +y_0=0 +ellps=WGS84 +towgs84=-191.90441429,-39.30318279,-111.45032835,0.00928836,-0.01975479,0.00427372,0.252906278 +units=m +no_defs +9221 projected Hartebeesthoek94 / ZAF BSU Albers 25E 13.33,-38.17,36.54,-22.13 1 6148 +proj=aea +lat_0=-30 +lon_0=25 +lat_1=-22 +lat_2=-38 +x_0=1400000 +y_0=1300000 +ellps=WGS84 +towgs84=0,0,0 +units=m +no_defs +9222 projected Hartebeesthoek94 / ZAF BSU Albers 44E 32.71,-50.32,42.85,-43.26 1 6148 +proj=aea +lat_0=-42 +lon_0=44 +lat_1=-34 +lat_2=-50 +x_0=1200000 +y_0=1300000 +ellps=WGS84 +towgs84=0,0,0 +units=m +no_defs +9248 geographic Tapi Aike -73.28,-52.43,-68.3,-50.33 5 1257 +proj=longlat +ellps=intl +towgs84=-192.26,65.72,132.08 +no_defs +9249 projected Tapi Aike / Argentina 1 -73.28,-52,-70.5,-50.33 5 1257 +proj=tmerc +lat_0=-90 +lon_0=-72 +k=1 +x_0=1500000 +y_0=0 +ellps=intl +towgs84=-192.26,65.72,132.08 +units=m +no_defs +9250 projected Tapi Aike / Argentina 2 -70.5,-52.43,-68.3,-50.33 5 1257 +proj=tmerc +lat_0=-90 +lon_0=-69 +k=1 +x_0=2500000 +y_0=0 +ellps=intl +towgs84=-192.26,65.72,132.08 +units=m +no_defs +9251 geographic MMN -68.64,-55.11,-63.73,-52.59 2.5 1258 +proj=longlat +ellps=intl +towgs84=-9.5,122.9,138.2 +no_defs +9252 projected MMN / Argentina 2 -68.64,-55.11,-63.73,-52.59 2.5 1258 +proj=tmerc +lat_0=-90 +lon_0=-69 +k=1 +x_0=2500000 +y_0=0 +ellps=intl +towgs84=-9.5,122.9,138.2 +units=m +no_defs +9253 geographic MMS -68.64,-55.11,-63.73,-52.59 2.5 1259 +proj=longlat +ellps=intl +towgs84=-78.1,101.6,133.3 +no_defs +9254 projected MMS / Argentina 2 -68.64,-55.11,-63.73,-52.59 2.5 1259 +proj=tmerc +lat_0=-90 +lon_0=-69 +k=1 +x_0=2500000 +y_0=0 +ellps=intl +towgs84=-78.1,101.6,133.3 +units=m +no_defs +9265 projected POSGAR 2007 / UTM zone 19S -68.62,-54.61,-66,-51.65 0.5 1062 +proj=utm +zone=19 +south +ellps=WGS84 +towgs84=-0.41,0.46,-0.35 +units=m +no_defs +9271 projected MGI / Austria West 9.53,46.77,11.84,47.61 1.5 6312 +proj=tmerc +lat_0=0 +lon_0=10.3333333333333 +k=1 +x_0=0 +y_0=0 +ellps=bessel +towgs84=577.326,90.129,463.919,5.137,1.474,5.297,2.4232 +units=m +no_defs +9272 projected MGI / Austria Central 11.83,46.4,14.84,48.79 1.5 6312 +proj=tmerc +lat_0=0 +lon_0=13.3333333333333 +k=1 +x_0=0 +y_0=0 +ellps=bessel +towgs84=577.326,90.129,463.919,5.137,1.474,5.297,2.4232 +units=m +no_defs +9273 projected MGI / Austria East 14.83,46.56,17.17,49.02 1.5 6312 +proj=tmerc +lat_0=0 +lon_0=16.3333333333333 +k=1 +x_0=0 +y_0=0 +ellps=bessel +towgs84=577.326,90.129,463.919,5.137,1.474,5.297,2.4232 +units=m +no_defs +9284 projected Pampa del Castillo / Argentina 1 -73.59,-50.34,-70.5,-44.94 2.5 6161 +proj=tmerc +lat_0=-90 +lon_0=-72 +k=1 +x_0=1500000 +y_0=0 +ellps=intl +towgs84=-233.43,6.65,173.64 +units=m +no_defs +9285 projected Pampa del Castillo / Argentina 3 -67.5,-49.05,-65.47,-43.58 2.5 6161 +proj=tmerc +lat_0=-90 +lon_0=-66 +k=1 +x_0=3500000 +y_0=0 +ellps=intl +towgs84=-233.43,6.65,173.64 +units=m +no_defs +9294 geographic ONGD17 51.99,14.33,63.38,26.74 0.1 1263 +proj=longlat +ellps=GRS80 +towgs84=1.16835,-1.42001,-2.24431,-0.00822,-0.05508,0.01818,0.23388 +no_defs +9295 projected ONGD17 / UTM zone 39N 51.99,14.94,54.01,19.67 0.1 1263 +proj=utm +zone=39 +ellps=GRS80 +towgs84=1.16835,-1.42001,-2.24431,-0.00822,-0.05508,0.01818,0.23388 +units=m +no_defs +9296 projected ONGD17 / UTM zone 40N 54,14.33,60.01,26.74 0.1 1263 +proj=utm +zone=40 +ellps=GRS80 +towgs84=1.16835,-1.42001,-2.24431,-0.00822,-0.05508,0.01818,0.23388 +units=m +no_defs +9297 projected ONGD17 / UTM zone 41N 60,16.37,63.38,24.09 0.1 1263 +proj=utm +zone=41 +ellps=GRS80 +towgs84=1.16835,-1.42001,-2.24431,-0.00822,-0.05508,0.01818,0.23388 +units=m +no_defs +9299 geographic HS2-IRF -2.75,51.45,0,53.3 1264 +proj=longlat +ellps=GRS80 +no_defs +9300 projected HS2 Survey Grid -2.75,51.45,0,53.3 1264 +proj=tmerc +lat_0=52.3 +lon_0=-1.5 +k=1 +x_0=198873.0046 +y_0=375064.3871 +ellps=GRS80 +units=m +no_defs +9309 geographic ATRF2014 93.41,-60.55,173.34,-8.47 1291 +proj=longlat +ellps=GRS80 +no_defs +9333 geographic KSA-GRF17 34.44,16.29,55.67,32.16 1 1268 +proj=longlat +ellps=GRS80 +towgs84=0,0,0,-0.008393,0.000749,-0.010276,0 +no_defs +9354 projected WGS 84 / IBCSO Polar Stereographic -180,-90,180,-50 6326 +proj=stere +lat_0=-90 +lat_ts=-65 +lon_0=0 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +9356 projected KSA-GRF17 / UTM zone 36N 34.44,24.92,36.01,29.38 1 1268 +proj=utm +zone=36 +ellps=GRS80 +towgs84=0,0,0,-0.008393,0.000749,-0.010276,0 +units=m +no_defs +9357 projected KSA-GRF17 / UTM zone 37N 36,16.29,42,32.16 1 1268 +proj=utm +zone=37 +ellps=GRS80 +towgs84=0,0,0,-0.008393,0.000749,-0.010276,0 +units=m +no_defs +9358 projected KSA-GRF17 / UTM zone 38N 41.99,16.35,48,31.15 1 1268 +proj=utm +zone=38 +ellps=GRS80 +towgs84=0,0,0,-0.008393,0.000749,-0.010276,0 +units=m +no_defs +9359 projected KSA-GRF17 / UTM zone 39N 47.99,17.94,54.01,28.94 1 1268 +proj=utm +zone=39 +ellps=GRS80 +towgs84=0,0,0,-0.008393,0.000749,-0.010276,0 +units=m +no_defs +9360 projected KSA-GRF17 / UTM zone 40N 54,19.66,55.67,22.77 1 1268 +proj=utm +zone=40 +ellps=GRS80 +towgs84=0,0,0,-0.008393,0.000749,-0.010276,0 +units=m +no_defs +9364 geographic TPEN11-IRF -3.14,53.32,-1.34,53.9 1266 +proj=longlat +ellps=GRS80 +no_defs +9367 projected TPEN11 Grid -3.14,53.32,-1.34,53.9 1266 +proj=tmerc +lat_0=53.5 +lon_0=-2.25 +k=1 +x_0=203252.175 +y_0=407512.765 +ellps=GRS80 +units=m +no_defs +9372 geographic MML07-IRF -1.89,51.46,0.16,53.42 1271 +proj=longlat +ellps=GRS80 +no_defs +9373 projected MML07 Grid -1.89,51.46,0.16,53.42 1271 +proj=tmerc +lat_0=52.45 +lon_0=-0.85 +k=1 +x_0=49350.157 +y_0=108398.212 +ellps=GRS80 +units=m +no_defs +9377 projected MAGNA-SIRGAS 2018 / Origen-Nacional -84.77,-4.23,-66.87,15.51 1 1329 +proj=tmerc +lat_0=4 +lon_0=-73 +k=0.9992 +x_0=5000000 +y_0=2000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +9380 geographic IGb14 -180,-90,180,90 1272 +proj=longlat +ellps=GRS80 +no_defs +9384 geographic AbInvA96_2020-IRF -4.31,57.1,-2.1,57.71 1273 +proj=longlat +ellps=GRS80 +no_defs +9387 projected AbInvA96_2020 Grid -4.31,57.1,-2.1,57.71 1273 +proj=tmerc +lat_0=57.4 +lon_0=-3.2 +k=1 +x_0=155828.702 +y_0=115225.707 +ellps=GRS80 +units=m +no_defs +9391 projected BGS2005 / UTM zone 35N 24,41.24,30.01,44.15 1 1167 +proj=utm +zone=35 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +9403 geographic PN68 -18.22,27.58,-13.37,29.47 44 1286 +proj=longlat +ellps=intl +towgs84=-307,-92,127 +no_defs +9404 projected PN68 / UTM zone 27N -18.22,27.59,-18,27.88 44 1286 +proj=utm +zone=27 +ellps=intl +towgs84=-307,-92,127 +units=m +no_defs +9405 projected PN68 / UTM zone 28N -18.06,27.58,-13.37,29.47 44 1286 +proj=utm +zone=28 +ellps=intl +towgs84=-307,-92,127 +units=m +no_defs +9406 projected PN84 / UTM zone 27N -18.22,27.59,-18,27.88 44 6728 +proj=utm +zone=27 +ellps=intl +towgs84=-307,-92,127 +units=m +no_defs +9407 projected PN84 / UTM zone 28N -18.06,27.58,-16.08,28.9 44 6728 +proj=utm +zone=28 +ellps=intl +towgs84=-307,-92,127 +units=m +no_defs +9453 geographic GBK19-IRF -4.65,55.55,-4.05,55.95 1289 +proj=longlat +ellps=GRS80 +no_defs +9456 projected GBK19 Grid -4.65,55.55,-4.05,55.95 1289 +proj=tmerc +lat_0=55.75 +lon_0=-4.35 +k=1 +x_0=93720.394 +y_0=113870.493 +ellps=GRS80 +units=m +no_defs +9470 geographic SRGI2013 92.01,-13.95,141.46,7.79 1293 +proj=longlat +ellps=WGS84 +no_defs +9473 projected GDA2020 / Australian Albers 112.85,-43.7,153.69,-9.86 3 1168 +proj=aea +lat_0=0 +lon_0=132 +lat_1=-18 +lat_2=-36 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +9474 geographic PZ-90.02 -180,-90,180,90 1157 +proj=longlat +a=6378136 +rf=298.257839303 +no_defs +9475 geographic PZ-90.11 -180,-90,180,90 1158 +proj=longlat +a=6378136 +rf=298.257839303 +no_defs +9476 projected SRGI2013 / UTM zone 46N 92.01,0,96,7.79 1293 +proj=utm +zone=46 +ellps=WGS84 +units=m +no_defs +9477 projected SRGI2013 / UTM zone 47N 96,0,102,7.49 1293 +proj=utm +zone=47 +ellps=WGS84 +units=m +no_defs +9478 projected SRGI2013 / UTM zone 48N 102,0,108,6.94 1293 +proj=utm +zone=48 +ellps=WGS84 +units=m +no_defs +9479 projected SRGI2013 / UTM zone 49N 108,0,114,7.37 1293 +proj=utm +zone=49 +ellps=WGS84 +units=m +no_defs +9480 projected SRGI2013 / UTM zone 50N 114,0,120,4.37 1293 +proj=utm +zone=50 +ellps=WGS84 +units=m +no_defs +9481 projected SRGI2013 / UTM zone 51N 120,0,126,5.48 1293 +proj=utm +zone=51 +ellps=WGS84 +units=m +no_defs +9482 projected SRGI2013 / UTM zone 52N 126,0,132,6.68 1293 +proj=utm +zone=52 +ellps=WGS84 +units=m +no_defs +9487 projected SRGI2013 / UTM zone 47S 96,-8.86,102,0 1293 +proj=utm +zone=47 +south +ellps=WGS84 +units=m +no_defs +9488 projected SRGI2013 / UTM zone 48S 102,-10.73,108.01,0 1293 +proj=utm +zone=48 +south +ellps=WGS84 +units=m +no_defs +9489 projected SRGI2013 / UTM zone 49S 108,-12.07,114,0 1293 +proj=utm +zone=49 +south +ellps=WGS84 +units=m +no_defs +9490 projected SRGI2013 / UTM zone 50S 114,-13.06,120.01,0 1293 +proj=utm +zone=50 +south +ellps=WGS84 +units=m +no_defs +9491 projected SRGI2013 / UTM zone 51S 120,-13.95,126.01,0.01 1293 +proj=utm +zone=51 +south +ellps=WGS84 +units=m +no_defs +9492 projected SRGI2013 / UTM zone 52S 126,-9.45,132,0 1293 +proj=utm +zone=52 +south +ellps=WGS84 +units=m +no_defs +9493 projected SRGI2013 / UTM zone 53S 132,-10.06,138.01,0 1293 +proj=utm +zone=53 +south +ellps=WGS84 +units=m +no_defs +9494 projected SRGI2013 / UTM zone 54S 138,-10.84,141.46,0 1293 +proj=utm +zone=54 +south +ellps=WGS84 +units=m +no_defs +9498 projected POSGAR 2007 / CABA 2019 -58.54,-34.71,-58.29,-34.5 0.5 1062 +proj=tmerc +lat_0=-34.6292666666667 +lon_0=-58.4633083333333 +k=1 +x_0=20000 +y_0=70000 +ellps=WGS84 +towgs84=-0.41,0.46,-0.35 +units=m +no_defs +9547 geographic LTF2004(G) 4.65,44.87,7.88,45.89 1295 +proj=longlat +ellps=GRS80 +no_defs +9549 projected LTF2004(C) 4.65,44.87,7.88,45.89 1295 +proj=lcc +lat_1=44.3791666666667 +k_0=1 +lat_0=45.1833333333333 +lon_0=6.81666666666667 +x_0=150000 +y_0=50000 +ellps=GRS80 +units=m +no_defs +9674 projected NAD83 / USFS R6 Albers -124.79,41.98,-116.47,49.05 4 6269 +proj=aea +lat_0=34 +lon_0=-120 +lat_1=43 +lat_2=48 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +9678 projected Gulshan 303 / Bangladesh Transverse Mercator 88.01,20.52,92.67,26.64 1 6682 +proj=tmerc +lat_0=0 +lon_0=90 +k=0.9996 +x_0=500000 +y_0=-2000000 +ellps=evrst30 +towgs84=283.729,735.942,261.143 +units=m +no_defs +9680 projected WGS 84 / TM 90 NE 88.01,18.56,92.67,26.64 6326 +proj=tmerc +lat_0=0 +lon_0=90 +k=0.9996 +x_0=500000 +y_0=0 +datum=WGS84 +units=m +no_defs +9696 geographic REDGEOMIN -113.21,-59.87,-65.72,-17.5 1304 +proj=longlat +ellps=GRS80 +no_defs +9697 projected REDGEOMIN / UTM zone 12S -109.51,-27.25,-109.16,-27.01 1304 +proj=utm +zone=12 +south +ellps=GRS80 +units=m +no_defs +9698 projected REDGEOMIN / UTM zone 18S -78,-59.36,-71.99,-18.35 1304 +proj=utm +zone=18 +south +ellps=GRS80 +units=m +no_defs +9699 projected REDGEOMIN / UTM zone 19S -72,-59.87,-66,-17.5 1304 +proj=utm +zone=19 +south +ellps=GRS80 +units=m +no_defs +9702 geographic ETRF2000-PL 14.14,49,24.15,55.93 1 1305 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +9709 projected NAD83(CSRS) / UTM zone 23N -48,39.85,-42,54.47 1 6140 +proj=utm +zone=23 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +9712 projected NAD83 / UTM zone 24N -42,45.53,-40.73,49.53 6269 +proj=utm +zone=24 +ellps=GRS80 +units=m +no_defs +9713 projected NAD83(CSRS) / UTM zone 24N -42,45.53,-40.73,49.53 1 6140 +proj=utm +zone=24 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +9716 projected IGM95 / UTM zone 34N 17.99,34.76,18.99,41.64 1 6670 +proj=utm +zone=34 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +9739 geographic EOS21-IRF -3.56,55.55,-1.94,57.21 1308 +proj=longlat +ellps=GRS80 +no_defs +9741 projected EOS21 Grid -3.56,55.55,-1.94,57.21 1308 +proj=tmerc +lat_0=56.35 +lon_0=-2.75 +k=1 +x_0=74996.927 +y_0=133508.35 +ellps=GRS80 +units=m +no_defs +9748 projected NAD83(2011) / Alabama East (ftUS) -86.79,30.99,-84.89,35 2 1116 +proj=tmerc +lat_0=30.5 +lon_0=-85.8333333333333 +k=0.99996 +x_0=200000.0001016 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +9749 projected NAD83(2011) / Alabama West (ftUS) -88.48,30.14,-86.3,35.02 2 1116 +proj=tmerc +lat_0=30 +lon_0=-87.5 +k=0.999933333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +9755 geographic WGS 84 (G2139) -180,-90,180,90 2 1309 +proj=longlat +ellps=WGS84 +towgs84=0,0,0 +no_defs +9758 geographic ECML14_NB-IRF -1.9,54.85,-1.3,55.3 1310 +proj=longlat +ellps=GRS80 +no_defs +9761 projected ECML14_NB Grid -1.9,54.85,-1.3,55.3 1310 +proj=tmerc +lat_0=55.05 +lon_0=-1.55 +k=1 +x_0=112242.8512 +y_0=402313.7432 +ellps=GRS80 +units=m +no_defs +9763 geographic EWR2-IRF -1.43,51.7,-0.36,52.24 1311 +proj=longlat +ellps=GRS80 +no_defs +9766 projected EWR2 Grid -1.43,51.7,-0.36,52.24 1311 +proj=tmerc +lat_0=51.95 +lon_0=-0.9 +k=1 +x_0=192519.9715 +y_0=146942.6806 +ellps=GRS80 +units=m +no_defs +9777 geographic RGF93 v2 -9.86,41.15,10.38,51.56 1 1312 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +9779 geographic RGF93 v2 (lon-lat) -9.86,41.15,10.38,51.56 1 1312 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +9782 geographic RGF93 v2b -9.86,41.15,10.38,51.56 1 1313 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +9784 geographic RGF93 v2b (lon-lat) -9.86,41.15,10.38,51.56 1 1313 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +9793 projected RGF93 v2 / Lambert-93 -9.86,41.15,10.38,51.56 1 1312 +proj=lcc +lat_0=46.5 +lon_0=3 +lat_1=49 +lat_2=44 +x_0=700000 +y_0=6600000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +9794 projected RGF93 v2b / Lambert-93 -9.86,41.15,10.38,51.56 1 1313 +proj=lcc +lat_0=46.5 +lon_0=3 +lat_1=49 +lat_2=44 +x_0=700000 +y_0=6600000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +9821 projected UCS-2000 / LCS-32 Kyiv region 29.26,49.17,32.16,51.55 1 1077 +proj=tmerc +lat_0=0 +lon_0=30.5 +k=1 +x_0=300000 +y_0=0 +ellps=krass +towgs84=24,-121,-76 +units=m +no_defs +9822 projected RGF93 v2 / CC42 -1.06,41.31,9.63,43.07 1 1312 +proj=lcc +lat_0=42 +lon_0=3 +lat_1=41.25 +lat_2=42.75 +x_0=1700000 +y_0=1200000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +9823 projected RGF93 v2 / CC43 -1.79,42.33,7.65,44.01 1 1312 +proj=lcc +lat_0=43 +lon_0=3 +lat_1=42.25 +lat_2=43.75 +x_0=1700000 +y_0=2200000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +9824 projected RGF93 v2 / CC44 -1.79,42.92,7.71,45 1 1312 +proj=lcc +lat_0=44 +lon_0=3 +lat_1=43.25 +lat_2=44.75 +x_0=1700000 +y_0=3200000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +9825 projected RGF93 v2 / CC45 -1.46,44,7.71,46 1 1312 +proj=lcc +lat_0=45 +lon_0=3 +lat_1=44.25 +lat_2=45.75 +x_0=1700000 +y_0=4200000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +9826 projected RGF93 v2 / CC46 -2.21,45,7.16,47 1 1312 +proj=lcc +lat_0=46 +lon_0=3 +lat_1=45.25 +lat_2=46.75 +x_0=1700000 +y_0=5200000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +9827 projected RGF93 v2 / CC47 -4.77,46,7.63,48 1 1312 +proj=lcc +lat_0=47 +lon_0=3 +lat_1=46.25 +lat_2=47.75 +x_0=1700000 +y_0=6200000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +9828 projected RGF93 v2 / CC48 -4.87,47,8.23,49 1 1312 +proj=lcc +lat_0=48 +lon_0=3 +lat_1=47.25 +lat_2=48.75 +x_0=1700000 +y_0=7200000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +9829 projected RGF93 v2 / CC49 -4.87,48,8.23,50 1 1312 +proj=lcc +lat_0=49 +lon_0=3 +lat_1=48.25 +lat_2=49.75 +x_0=1700000 +y_0=8200000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +9830 projected RGF93 v2 / CC50 -2.03,49,8.08,51.14 1 1312 +proj=lcc +lat_0=50 +lon_0=3 +lat_1=49.25 +lat_2=50.75 +x_0=1700000 +y_0=9200000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +9831 projected UCS-2000 / LCS-01 Crimea 32.47,44.38,36.65,46.24 1 1077 +proj=tmerc +lat_0=0 +lon_0=34.5 +k=1 +x_0=300000 +y_0=0 +ellps=krass +towgs84=24,-121,-76 +units=m +no_defs +9832 projected UCS-2000 / LCS-05 Vinnytsia 27.37,48.06,30.02,49.89 1 1077 +proj=tmerc +lat_0=0 +lon_0=28.6666666666667 +k=1 +x_0=300000 +y_0=0 +ellps=krass +towgs84=24,-121,-76 +units=m +no_defs +9833 projected UCS-2000 / LCS-07 Volyn 23.6,50.28,26.11,51.97 1 1077 +proj=tmerc +lat_0=0 +lon_0=24.8333333333333 +k=1 +x_0=300000 +y_0=0 +ellps=krass +towgs84=24,-121,-76 +units=m +no_defs +9834 projected UCS-2000 / LCS-12 Dnipropetrovsk 32.95,47.45,36.94,49.19 1 1077 +proj=tmerc +lat_0=0 +lon_0=35 +k=1 +x_0=300000 +y_0=0 +ellps=krass +towgs84=24,-121,-76 +units=m +no_defs +9835 projected UCS-2000 / LCS-14 Donetsk 36.54,46.86,39.1,49.24 1 1077 +proj=tmerc +lat_0=0 +lon_0=37.5 +k=1 +x_0=300000 +y_0=0 +ellps=krass +towgs84=24,-121,-76 +units=m +no_defs +9836 projected UCS-2000 / LCS-18 Zhytomyr 27.19,49.58,29.74,51.68 1 1077 +proj=tmerc +lat_0=0 +lon_0=28.5 +k=1 +x_0=300000 +y_0=0 +ellps=krass +towgs84=24,-121,-76 +units=m +no_defs +9837 projected UCS-2000 / LCS-21 Zakarpattia 22.13,47.89,24.63,49.1 1 1077 +proj=tmerc +lat_0=0 +lon_0=23.5 +k=1 +x_0=300000 +y_0=0 +ellps=krass +towgs84=24,-121,-76 +units=m +no_defs +9838 projected UCS-2000 / LCS-23 Zaporizhzhia 34.17,46.07,37.25,48.15 1 1077 +proj=tmerc +lat_0=0 +lon_0=36 +k=1 +x_0=300000 +y_0=0 +ellps=krass +towgs84=24,-121,-76 +units=m +no_defs +9839 projected UCS-2000 / LCS-26 Ivano-Frankivsk 23.53,47.72,25.66,49.56 1 1077 +proj=tmerc +lat_0=0 +lon_0=24.75 +k=1 +x_0=300000 +y_0=0 +ellps=krass +towgs84=24,-121,-76 +units=m +no_defs +9840 projected UCS-2000 / LCS-35 Kirovohrad 29.74,47.74,33.9,49.25 1 1077 +proj=tmerc +lat_0=0 +lon_0=32 +k=1 +x_0=300000 +y_0=0 +ellps=krass +towgs84=24,-121,-76 +units=m +no_defs +9841 projected UCS-2000 / LCS-44 Luhansk 37.83,47.82,40.23,50.09 1 1077 +proj=tmerc +lat_0=0 +lon_0=39 +k=1 +x_0=300000 +y_0=0 +ellps=krass +towgs84=24,-121,-76 +units=m +no_defs +9842 projected RGF93 v2b / CC42 -1.06,41.31,9.63,43.07 1 1313 +proj=lcc +lat_0=42 +lon_0=3 +lat_1=41.25 +lat_2=42.75 +x_0=1700000 +y_0=1200000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +9843 projected RGF93 v2b / CC43 -1.79,42.33,7.65,44.01 1 1313 +proj=lcc +lat_0=43 +lon_0=3 +lat_1=42.25 +lat_2=43.75 +x_0=1700000 +y_0=2200000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +9844 projected RGF93 v2b / CC44 -1.79,42.92,7.71,45 1 1313 +proj=lcc +lat_0=44 +lon_0=3 +lat_1=43.25 +lat_2=44.75 +x_0=1700000 +y_0=3200000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +9845 projected RGF93 v2b / CC45 -1.46,44,7.71,46 1 1313 +proj=lcc +lat_0=45 +lon_0=3 +lat_1=44.25 +lat_2=45.75 +x_0=1700000 +y_0=4200000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +9846 projected RGF93 v2b / CC46 -2.21,45,7.16,47 1 1313 +proj=lcc +lat_0=46 +lon_0=3 +lat_1=45.25 +lat_2=46.75 +x_0=1700000 +y_0=5200000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +9847 projected RGF93 v2b / CC47 -4.77,46,7.63,48 1 1313 +proj=lcc +lat_0=47 +lon_0=3 +lat_1=46.25 +lat_2=47.75 +x_0=1700000 +y_0=6200000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +9848 projected RGF93 v2b / CC48 -4.87,47,8.23,49 1 1313 +proj=lcc +lat_0=48 +lon_0=3 +lat_1=47.25 +lat_2=48.75 +x_0=1700000 +y_0=7200000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +9849 projected RGF93 v2b / CC49 -4.87,48,8.23,50 1 1313 +proj=lcc +lat_0=49 +lon_0=3 +lat_1=48.25 +lat_2=49.75 +x_0=1700000 +y_0=8200000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +9850 projected RGF93 v2b / CC50 -2.03,49,8.08,51.14 1 1313 +proj=lcc +lat_0=50 +lon_0=3 +lat_1=49.25 +lat_2=50.75 +x_0=1700000 +y_0=9200000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +9851 projected UCS-2000 / LCS-46 Lviv 22.64,48.71,25.43,50.65 1 1077 +proj=tmerc +lat_0=0 +lon_0=24 +k=1 +x_0=300000 +y_0=0 +ellps=krass +towgs84=24,-121,-76 +units=m +no_defs +9852 projected UCS-2000 / LCS-48 Mykolaiv 30.2,46.36,33.19,48.23 1 1077 +proj=tmerc +lat_0=0 +lon_0=31.8333333333333 +k=1 +x_0=300000 +y_0=0 +ellps=krass +towgs84=24,-121,-76 +units=m +no_defs +9853 projected UCS-2000 / LCS-51 Odessa 28.21,45.2,31.3,48.24 1 1077 +proj=tmerc +lat_0=0 +lon_0=30 +k=1 +x_0=300000 +y_0=0 +ellps=krass +towgs84=24,-121,-76 +units=m +no_defs +9854 projected UCS-2000 / LCS-53 Poltava 32.08,48.74,35.49,50.55 1 1077 +proj=tmerc +lat_0=0 +lon_0=33.8333333333333 +k=1 +x_0=300000 +y_0=0 +ellps=krass +towgs84=24,-121,-76 +units=m +no_defs +9855 projected UCS-2000 / LCS-56 Rivne 25.08,50,27.74,51.95 1 1077 +proj=tmerc +lat_0=0 +lon_0=27 +k=1 +x_0=300000 +y_0=0 +ellps=krass +towgs84=24,-121,-76 +units=m +no_defs +9856 projected UCS-2000 / LCS-59 Sumy 32.93,50.1,35.67,52.37 1 1077 +proj=tmerc +lat_0=0 +lon_0=34.5 +k=1 +x_0=300000 +y_0=0 +ellps=krass +towgs84=24,-121,-76 +units=m +no_defs +9857 projected UCS-2000 / LCS-61 Ternopil 24.71,48.5,26.45,50.27 1 1077 +proj=tmerc +lat_0=0 +lon_0=25.5 +k=1 +x_0=300000 +y_0=0 +ellps=krass +towgs84=24,-121,-76 +units=m +no_defs +9858 projected UCS-2000 / LCS-63 Kharkiv 34.84,48.52,38.1,50.46 1 1077 +proj=tmerc +lat_0=0 +lon_0=36.5 +k=1 +x_0=300000 +y_0=0 +ellps=krass +towgs84=24,-121,-76 +units=m +no_defs +9859 projected UCS-2000 / LCS-65 Kherson 31.51,45.75,35.11,47.6 1 1077 +proj=tmerc +lat_0=0 +lon_0=33.5 +k=1 +x_0=300000 +y_0=0 +ellps=krass +towgs84=24,-121,-76 +units=m +no_defs +9860 projected UCS-2000 / LCS-68 Khmelnytsky 26.13,48.45,27.9,50.59 1 1077 +proj=tmerc +lat_0=0 +lon_0=27 +k=1 +x_0=300000 +y_0=0 +ellps=krass +towgs84=24,-121,-76 +units=m +no_defs +9861 projected UCS-2000 / LCS-71 Cherkasy 29.6,48.45,32.89,50.23 1 1077 +proj=tmerc +lat_0=0 +lon_0=31.5 +k=1 +x_0=300000 +y_0=0 +ellps=krass +towgs84=24,-121,-76 +units=m +no_defs +9862 projected UCS-2000 / LCS-73 Chernivtsi 24.9,47.72,27.54,48.68 1 1077 +proj=tmerc +lat_0=0 +lon_0=26 +k=1 +x_0=300000 +y_0=0 +ellps=krass +towgs84=24,-121,-76 +units=m +no_defs +9863 projected UCS-2000 / LCS-74 Chernihiv 30.48,50.34,33.51,52.38 1 1077 +proj=tmerc +lat_0=0 +lon_0=32 +k=1 +x_0=300000 +y_0=0 +ellps=krass +towgs84=24,-121,-76 +units=m +no_defs +9864 projected UCS-2000 / LCS-80 Kyiv city 30.23,50.21,30.83,50.59 1 1077 +proj=tmerc +lat_0=0 +lon_0=30.5 +k=1 +x_0=300000 +y_0=0 +ellps=krass +towgs84=24,-121,-76 +units=m +no_defs +9865 projected UCS-2000 / LCS-85 Sevastopol 33.37,44.38,33.93,44.85 1 1077 +proj=tmerc +lat_0=0 +lon_0=33 +k=1 +x_0=300000 +y_0=0 +ellps=krass +towgs84=24,-121,-76 +units=m +no_defs +9866 geographic MRH21-IRF -3.27,51.35,-0.36,53.26 1314 +proj=longlat +ellps=GRS80 +no_defs +9869 projected MRH21 Grid -3.27,51.35,-0.36,53.26 1314 +proj=tmerc +lat_0=52.3 +lon_0=-1.8 +k=1 +x_0=227286.9881 +y_0=265751.2874 +ellps=GRS80 +units=m +no_defs +9871 geographic MOLDOR11-IRF -2.4,53.25,-1.39,53.55 1315 +proj=longlat +ellps=GRS80 +no_defs +9874 projected PNG94 / PNGMG94 zone 57 156,-14.26,162.01,-1.11 2 1076 +proj=utm +zone=57 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +9875 projected PNG94 / PNGMG94 zone 58 162,-4.36,162.81,-2.34 2 1076 +proj=utm +zone=58 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +9880 projected MOLDOR11 Grid -2.4,53.25,-1.39,53.55 1315 +proj=tmerc +lat_0=53.35 +lon_0=-1.9 +k=1 +x_0=226574.2032 +y_0=390894.838 +ellps=GRS80 +units=m +no_defs +9939 geographic EBBWV14-IRF -3.3,51.5,-2.89,51.85 1319 +proj=longlat +ellps=GRS80 +no_defs +9943 projected EBBWV14 Grid -3.3,51.5,-2.89,51.85 1319 +proj=tmerc +lat_0=51.75 +lon_0=-3.1 +k=1 +x_0=106702.326 +y_0=119968.1395 +ellps=GRS80 +units=m +no_defs +9945 projected Macedonia State Coordinate System truncated 20.45,40.85,23.04,42.36 2 1031 +proj=tmerc +lat_0=0 +lon_0=21 +k=0.9999 +x_0=500000 +y_0=-4000000 +ellps=bessel +towgs84=521.748,229.489,590.921,4.029,4.488,-15.521,-9.78 +units=m +no_defs +9947 projected ISN2004 / LAEA Iceland -30.87,59.96,-5.55,69.59 1 1060 +proj=laea +lat_0=65 +lon_0=-19 +x_0=1700000 +y_0=1300000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +9964 geographic HULLEE13-IRF -1.7,53.6,-0.27,53.9 1317 +proj=longlat +ellps=GRS80 +no_defs +9967 projected HULLEE13 Grid -1.7,53.6,-0.27,53.9 1317 +proj=tmerc +lat_0=53.75 +lon_0=-0.95 +k=1 +x_0=140859.7394 +y_0=247512.2812 +ellps=GRS80 +units=m +no_defs +9969 geographic SCM22-IRF -4.4,55.7,-3.3,57.55 1320 +proj=longlat +ellps=GRS80 +no_defs +9972 projected SCM22 Grid -4.4,55.7,-3.3,57.55 1320 +proj=tmerc +lat_0=56.6 +lon_0=-3.85 +k=1 +x_0=108600.972 +y_0=239087.349 +ellps=GRS80 +units=m +no_defs +9974 geographic FNL22-IRF -4.6,57.4,-3,58.64 1321 +proj=longlat +ellps=GRS80 +no_defs +9977 projected FNL22 Grid -4.6,57.4,-3,58.64 1321 +proj=tmerc +lat_0=58 +lon_0=-3.8 +k=1 +x_0=139618.9493 +y_0=183110.794 +ellps=GRS80 +units=m +no_defs +9990 geographic ITRF2020 -180,-90,180,90 1322 +proj=longlat +ellps=GRS80 +no_defs +10158 geographic S34J-IRF 8,54.67,11.29,57.8 1332 +proj=longlat +ellps=intl +no_defs +10160 projected S34J reconstruction east-orientated 8,54.67,11.29,57.8 1332 +proj=tmerc +lat_0=0 +lon_0=10.37 +k=1 +x_0=-210327 +y_0=-6034310 +ellps=intl +units=m +no_defs +10175 geographic DoPw22-IRF -4.51,52.45,-3.8,53.01 1334 +proj=longlat +ellps=GRS80 +no_defs +10178 geographic IGS20 -180,-90,180,90 1333 +proj=longlat +ellps=GRS80 +no_defs +10183 projected DoPw22 Grid -4.51,52.45,-3.8,53.01 1334 +proj=tmerc +lat_0=52.7 +lon_0=-4.15 +k=1 +x_0=64859.6557 +y_0=122266.5277 +ellps=GRS80 +units=m +no_defs +10185 geographic ShAb07-IRF -4.16,52.37,-2.6,52.77 1335 +proj=longlat +ellps=GRS80 +no_defs +10188 projected ShAb07 Grid -4.16,52.37,-2.6,52.77 1335 +proj=tmerc +lat_0=52.6 +lon_0=-3.35 +k=1 +x_0=56023.5377 +y_0=24567.6764 +ellps=GRS80 +units=m +no_defs +10191 geographic CNH22-IRF -4.71,53.02,-2.28,53.46 1336 +proj=longlat +ellps=GRS80 +no_defs +10194 projected CNH22 Grid -4.71,53.02,-2.28,53.46 1336 +proj=lcc +lat_0=53.25 +lon_0=-3.5 +lat_1=53.1 +lat_2=53.4 +x_0=212548.8756 +y_0=495230.9254 +ellps=GRS80 +units=m +no_defs +10196 geographic CWS13-IRF -3.16,52.5,-2.65,53.26 1338 +proj=longlat +ellps=GRS80 +no_defs +10199 projected CWS13 Grid -3.16,52.5,-2.65,53.26 1338 +proj=tmerc +lat_0=52.4 +lon_0=-2.9 +k=1 +x_0=199668.0926 +y_0=89354.3229 +ellps=GRS80 +units=m +no_defs +10204 geographic DIBA15-IRF -1.46,51.57,-1.15,52.11 1339 +proj=longlat +ellps=GRS80 +no_defs +10207 projected DIBA15 Grid -1.46,51.57,-1.15,52.11 1339 +proj=tmerc +lat_0=51.85 +lon_0=-1.3 +k=1 +x_0=511622.854 +y_0=324737.4633 +ellps=GRS80 +units=m +no_defs +10209 geographic GWPBS22-IRF -4.26,51.25,-0.1,52.06 1340 +proj=longlat +ellps=GRS80 +no_defs +10212 projected GWPBS22 Grid -4.26,51.25,-0.1,52.06 1340 +proj=lcc +lat_0=51.65 +lon_0=-2.65 +lat_1=51.4 +lat_2=51.9 +x_0=168854.016 +y_0=193447.117 +ellps=GRS80 +units=m +no_defs +10214 geographic GWWAB22-IRF -3.6,51.35,-3.12,51.81 1341 +proj=longlat +ellps=GRS80 +no_defs +10217 projected GWWAB22 Grid -3.6,51.35,-3.12,51.81 1341 +proj=lcc +lat_0=51.65 +lon_0=-2.65 +lat_1=51.4 +lat_2=51.9 +x_0=168854.016 +y_0=193447.117 +ellps=GRS80 +units=m +no_defs +10219 geographic GWWWA22-IRF -5.16,51.55,-3.6,52.06 1342 +proj=longlat +ellps=GRS80 +no_defs +10222 projected GWWWA22 Grid -5.16,51.55,-3.6,52.06 1342 +proj=lcc +lat_0=51.65 +lon_0=-2.65 +lat_1=51.4 +lat_2=51.9 +x_0=168854.016 +y_0=193447.117 +ellps=GRS80 +units=m +no_defs +10224 geographic MALS09-IRF -2.26,51.45,-0.05,52.91 1343 +proj=longlat +ellps=GRS80 +no_defs +10227 projected MALS09 Grid -2.26,51.45,-0.05,52.91 1343 +proj=tmerc +lat_0=52.2 +lon_0=-1.15 +k=1 +x_0=175262.1809 +y_0=174688.2508 +ellps=GRS80 +units=m +no_defs +10229 geographic OxWo08-IRF -2.31,51.65,-1.15,52.26 1344 +proj=longlat +ellps=GRS80 +no_defs +10235 projected OxWo08 Grid -2.31,51.65,-1.15,52.26 1344 +proj=tmerc +lat_0=51.95 +lon_0=-1.7 +k=1 +x_0=134791.6965 +y_0=121872.5056 +ellps=GRS80 +units=m +no_defs +10237 geographic SYC20-IRF -2.91,52.65,-2.3,53.16 1345 +proj=longlat +ellps=GRS80 +no_defs +10240 projected SYC20 Grid -2.91,52.65,-2.3,53.16 1345 +proj=tmerc +lat_0=52.9 +lon_0=-2.6 +k=1 +x_0=110094.4312 +y_0=120623.8396 +ellps=GRS80 +units=m +no_defs +10249 geographic S34S-IRF 10.79,54.51,12.87,56.79 1337 +proj=longlat +ellps=intl +no_defs +10250 projected S34S reconstruction east-orientated 10.79,54.51,12.87,56.79 1337 +proj=tmerc +lat_0=0 +lon_0=10.37 +k=1 +x_0=-210327 +y_0=-6034310 +ellps=intl +units=m +no_defs +10252 geographic S45B-IRF 14.59,54.94,15.25,55.38 1346 +proj=longlat +ellps=intl +no_defs +10254 projected S45B reconstruction east-orientated 14.59,54.94,15.25,55.38 1346 +proj=tmerc +lat_0=55.11171 +lon_0=14.88927 +k=1 +x_0=-50000 +y_0=50000 +ellps=intl +units=m +no_defs +10256 geographic GS-IRF 8,54.51,12.87,57.8 1347 +proj=longlat +a=6377019.27 +rf=300 +no_defs +10258 projected GS reconstruction east-orientated 8,54.51,12.87,57.8 1347 +proj=lcc +lat_1=56 +k_0=1 +lat_0=55 +lon_0=10.37775 +x_0=0 +y_0=0 +a=6377019.27 +rf=300 +units=m +no_defs +10260 geographic GSB-IRF 14.59,54.94,15.25,55.38 1348 +proj=longlat +a=6377019.27 +rf=300 +no_defs +10262 projected GSB reconstruction east-orientated 14.59,54.94,15.25,55.38 1348 +proj=lcc +lat_1=56 +k_0=1 +lat_0=55 +lon_0=14.92775 +x_0=-18831.46 +y_0=5614.621 +a=6377019.27 +rf=300 +units=m +no_defs +10265 geographic KK-IRF 12.23,55.51,12.73,55.82 1349 +proj=longlat +a=6377019.27 +rf=300 +no_defs +10266 projected KK reconstruction east-orientated 12.23,55.51,12.73,55.82 1349 +proj=lcc +lat_1=56 +k_0=1 +lat_0=55 +lon_0=10.37775 +x_0=0 +y_0=0 +a=6377019.27 +rf=300 +units=m +no_defs +10268 geographic Ostenfeld-IRF 8.37,54.8,10.16,55.47 1350 +proj=longlat +ellps=bessel +no_defs +10270 projected Ostenfeld reconstruction 8.37,54.8,10.16,55.47 1350 +proj=tmerc +lat_0=54.4685 +lon_0=9.233 +k=1 +x_0=0 +y_0=0 +ellps=bessel +units=m +no_defs +10272 geographic SMITB20-IRF -4.11,50.65,-3.6,50.86 1351 +proj=longlat +ellps=GRS80 +no_defs +10275 projected SMITB20 Grid -4.11,50.65,-3.6,50.86 1351 +proj=tmerc +lat_0=50.75 +lon_0=-3.85 +k=1 +x_0=110693.666 +y_0=102089.2943 +ellps=GRS80 +units=m +no_defs +10277 geographic RBEPP12-IRF -5.63,50.05,-0.85,51.71 1352 +proj=longlat +ellps=GRS80 +no_defs +10280 projected RBEPP12 Grid -5.63,50.05,-0.85,51.71 1352 +proj=lcc +lat_0=50.85 +lon_0=-3.25 +lat_1=50.3 +lat_2=51.45 +x_0=372382.8292 +y_0=217764.7796 +ellps=GRS80 +units=m +no_defs +10284 geographic ETRS89/DREF91/2016 3.34,47.27,15.04,55.92 1353 +proj=longlat +ellps=GRS80 +no_defs +10285 projected ETRS89/DREF91/2016 / 3-degree Gauss-Kruger zone 3 8.33,53.36,10.41,53.99 1353 +proj=tmerc +lat_0=0 +lon_0=9 +k=1 +x_0=3500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +10286 projected ETRS89/DREF91/2016 / UTM zone 31N (N-zE) 3.34,50.97,6.01,55.92 1353 +proj=tmerc +lat_0=0 +lon_0=3 +k=0.9996 +x_0=31500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +10287 projected ETRS89/DREF91/2016 / UTM zone 31N (zE-N) 3.34,50.97,6.01,55.92 1353 +proj=tmerc +lat_0=0 +lon_0=3 +k=0.9996 +x_0=31500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +10288 projected ETRS89/DREF91/2016 / UTM zone 32N (N-zE) 6,47.27,12.01,55.47 1353 +proj=tmerc +lat_0=0 +lon_0=9 +k=0.9996 +x_0=32500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +10289 projected ETRS89/DREF91/2016 / UTM zone 32N (zE-N) 6,47.27,12.01,55.47 1353 +proj=tmerc +lat_0=0 +lon_0=9 +k=0.9996 +x_0=32500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +10290 projected ETRS89/DREF91/2016 / UTM zone 33N (N-zE) 11.57,47.46,15.04,55.03 1353 +proj=tmerc +lat_0=0 +lon_0=15 +k=0.9996 +x_0=33500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +10291 projected ETRS89/DREF91/2016 / UTM zone 33N (zE-N) 11.57,47.46,15.04,55.03 1353 +proj=tmerc +lat_0=0 +lon_0=15 +k=0.9996 +x_0=33500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +10299 geographic RGSH2020 -8.67,18.97,11.99,38.8 1 1355 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +10305 geographic LKS-2020 19.06,55.67,28.24,58.09 1 1356 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +10306 projected LKS-2020 / Latvia TM 19.06,55.67,28.24,58.09 1 1356 +proj=tmerc +lat_0=0 +lon_0=24 +k=0.9996 +x_0=500000 +y_0=-6000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +10307 geographic RGNC91-93 (lon-lat) 156.25,-26.45,174.28,-14.83 1 6749 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +10310 geographic RGNC15 156.25,-26.45,174.28,-14.83 1 1357 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +10312 geographic RGNC15 (lon-lat) 156.25,-26.45,174.28,-14.83 1 1357 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +10314 projected RGNC15 / Lambert New Caledonia 2015 163.54,-22.73,168.19,-19.5 1 1357 +proj=lcc +lat_0=-21.5 +lon_0=166 +lat_1=-20.6666666666667 +lat_2=-22.3333333333333 +x_0=2400000 +y_0=2300000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +10315 projected RGNC15 / UTM zone 57S 156.25,-26.03,162.01,-15.34 1 1357 +proj=utm +zone=57 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +10316 projected RGNC15 / UTM zone 58S 162,-26.45,168,-14.83 1 1357 +proj=utm +zone=58 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +10317 projected RGNC15 / UTM zone 59S 168,-25.95,174.28,-19.75 1 1357 +proj=utm +zone=59 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +10328 geographic BH_ETRS89 15.74,42.56,19.62,45.27 1 1358 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +10329 projected BH_ETRS89 / TM 15.74,42.56,19.62,45.27 1 1358 +proj=tmerc +lat_0=0 +lon_0=18 +k=0.9999 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +10345 geographic Hughes 1980 -180,-90,180,90 1359 +proj=longlat +a=6378273 +b=6356889.449 +no_defs +10346 geographic NSIDC Authalic Sphere -180,-90,180,90 1360 +proj=longlat +R=6371228 +no_defs +10414 geographic NAD83(CSRS)v8 -141.01,38.21,-40.73,86.46 1365 +proj=longlat +ellps=GRS80 +no_defs +10448 projected GDA94 / ALB94 117.55,-35.21,118.22,-34.75 3 6283 +proj=tmerc +lat_0=0 +lon_0=117.883333333333 +k=1.0000044 +x_0=50000 +y_0=4000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +10449 projected GDA94 / BIO94 114.9,-22.2,115.59,-20.21 3 6283 +proj=tmerc +lat_0=0 +lon_0=115.25 +k=1.0000022 +x_0=60000 +y_0=2600000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +10450 projected GDA94 / BRO94 122.08,-18.09,122.62,-16.75 3 6283 +proj=tmerc +lat_0=0 +lon_0=122.333333333333 +k=1.00000298 +x_0=50000 +y_0=2200000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +10451 projected GDA94 / BCG94 115.18,-33.75,115.87,-33.4 3 6283 +proj=tmerc +lat_0=0 +lon_0=115.433333333333 +k=0.99999592 +x_0=50000 +y_0=3900000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +10452 projected GDA94 / CARN94 113.33,-25.5,114,-23 3 6283 +proj=tmerc +lat_0=0 +lon_0=113.666666666667 +k=0.99999796 +x_0=50000 +y_0=2950000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +10453 projected GDA94 / COL94 115.73,-34.67,116.4,-33.25 3 6283 +proj=tmerc +lat_0=0 +lon_0=115.933333333333 +k=1.000019 +x_0=40000 +y_0=4000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +10454 projected GDA94 / ESP94 121.56,-34.5,122.2,-33.33 3 6283 +proj=tmerc +lat_0=0 +lon_0=121.883333333333 +k=1.0000055 +x_0=50000 +y_0=3950000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +10455 projected GDA94 / EXM94 113.75,-22.42,114.25,-21.75 3 6283 +proj=tmerc +lat_0=0 +lon_0=114.066666666667 +k=1.00000236 +x_0=50000 +y_0=2650000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +10456 projected GDA94 / GCG94 114.51,-29.1,115,-28.48 3 6283 +proj=tmerc +lat_0=0 +lon_0=114.583333333333 +k=1.00000628 +x_0=50000 +y_0=3350000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +10457 projected GDA94 / GOLD94 121,-32.25,121.84,-28.75 3 6283 +proj=tmerc +lat_0=0 +lon_0=121.5 +k=1.00004949 +x_0=60000 +y_0=3700000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +10458 projected GDA94 / JCG94 114.83,-30.74,115.34,-29.08 3 6283 +proj=tmerc +lat_0=0 +lon_0=114.983333333333 +k=1.00000314 +x_0=50000 +y_0=3550000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +10459 projected GDA94 / KALB94 113.9,-28.5,114.75,-27.16 3 6283 +proj=tmerc +lat_0=0 +lon_0=114.315277777778 +k=1.000014 +x_0=55000 +y_0=3600000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +10460 projected GDA94 / KAR94 116.58,-20.92,117.25,-20.25 3 6283 +proj=tmerc +lat_0=0 +lon_0=116.933333333333 +k=0.9999989 +x_0=50000 +y_0=2450000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +10461 projected GDA94 / KUN94 128.5,-16.75,129,-14.75 3 6283 +proj=tmerc +lat_0=0 +lon_0=128.75 +k=1.0000165 +x_0=50000 +y_0=2000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +10462 projected GDA94 / LCG94 115.15,-31.49,115.62,-30.71 3 6283 +proj=tmerc +lat_0=0 +lon_0=115.366666666667 +k=1.00000157 +x_0=50000 +y_0=3650000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +10463 projected GDA94 / MRCG94 114.96,-34.42,115.24,-33.51 3 6283 +proj=tmerc +lat_0=0 +lon_0=115.166666666667 +k=1.0000055 +x_0=50000 +y_0=3950000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +10464 projected GDA94 / PCG94 115.44,-33.42,116.09,-31.33 3 6283 +proj=tmerc +lat_0=0 +lon_0=115.816666666667 +k=0.99999906 +x_0=50000 +y_0=3800000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +10465 projected GDA94 / PHG94 118.25,-20.79,118.97,-20.1 3 6283 +proj=tmerc +lat_0=0 +lon_0=118.6 +k=1.00000135 +x_0=50000 +y_0=2400000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +10468 geographic COV23-IRF -1.85,52.3,-1.3,52.5 1366 +proj=longlat +ellps=GRS80 +no_defs +10471 projected COV23 Grid -1.85,52.3,-1.3,52.5 1366 +proj=tmerc +lat_0=52.4 +lon_0=-1.55 +k=1 +x_0=116887.9989 +y_0=102194.9369 +ellps=GRS80 +units=m +no_defs +10475 geographic BBT2000 11.04,46.45,11.91,47.33 1 1367 +proj=longlat +ellps=WGS84 +towgs84=0,0,0 +no_defs +10477 projected BBT2000 / BBT-TM 11.04,46.45,11.91,47.33 1 1367 +proj=tmerc +lat_0=46.9807763055556 +lon_0=11.52849375 +k=1.000121 +x_0=20000 +y_0=100000 +ellps=WGS84 +towgs84=0,0,0 +units=m +no_defs +10481 projected NAD83 / TWDB GM -106.66,25.83,-93.5,36.5 4 6269 +proj=aea +lat_0=31.25 +lon_0=-100 +lat_1=27.5 +lat_2=35 +x_0=1500000 +y_0=6000000 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +10516 projected NAD83(2011) / Adjusted Jackson (ftUS) -91.17,44.07,-90.31,44.6 2 1116 +proj=tmerc +lat_0=44.2533353222222 +lon_0=-90.8442965138889 +k=1.0000353 +x_0=27000 +y_0=24999.9998983998 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +10571 geographic ETRF2020 -16.1,33.26,38.01,84.73 1.1 1382 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +10592 projected WGS 84 / GLANCE Africa -31.68,-43.89,79.3,38.8 6326 +proj=laea +lat_0=5 +lon_0=20 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +10594 projected WGS 84 / GLANCE Asia 31.45,-10.8,-168.43,83.67 6326 +proj=laea +lat_0=45 +lon_0=100 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +10596 projected WGS 84 / GLANCE Europe -42.52,29.24,51.73,83.67 6326 +proj=laea +lat_0=55 +lon_0=20 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +10598 projected WGS 84 / GLANCE North America -180,7.98,180,90 6326 +proj=laea +lat_0=50 +lon_0=-100 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +10601 projected WGS 84 / GLANCE Oceania 72.37,-60.56,-121.05,21.57 6326 +proj=laea +lat_0=-15 +lon_0=135 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +10603 projected WGS 84 / GLANCE South America -124.82,-59.87,-14.58,30.31 6326 +proj=laea +lat_0=-15 +lon_0=-60 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +10606 geographic WGS 84 (G2296) -180,-90,180,90 2 1383 +proj=longlat +ellps=WGS84 +towgs84=0,0,0 +no_defs +10623 geographic ECML14-IRF -3.45,51.45,0.05,56.1 1385 +proj=longlat +ellps=GRS80 +no_defs +10626 projected ECML14 Grid -3.45,51.45,0.05,56.1 1385 +proj=tmerc +lat_0=53.8 +lon_0=-1.6 +k=1 +x_0=108021.121 +y_0=263196.8721 +ellps=GRS80 +units=m +no_defs +10628 geographic WC05-IRF -4.33,51.4,-0.04,55.92 1386 +proj=longlat +ellps=GRS80 +no_defs +10632 projected WC05 Grid -4.33,51.4,-0.04,55.92 1386 +proj=tmerc +lat_0=53.45 +lon_0=-2.15 +k=1 +x_0=209900.2337 +y_0=401958.2494 +ellps=GRS80 +units=m +no_defs +10636 geographic Saba -63.31,17.56,-63.16,17.71 1379 +proj=longlat +ellps=intl +no_defs +10639 geographic BES2020 Saba -63.31,17.56,-63.16,17.71 0.5 1380 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +10641 projected Saba DPnet -63.31,17.56,-63.16,17.71 1379 +proj=tmerc +lat_0=0 +lon_0=-63 +k=0.9996 +x_0=29973.97 +y_0=-1947925.94 +ellps=intl +units=m +no_defs +10665 projected SIRGAS 2000 / Porto Alegre TM -51.3,-30.27,-51.01,-29.93 1 6674 +proj=tmerc +lat_0=0 +lon_0=-51 +k=0.999995 +x_0=300000 +y_0=5000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +10671 geographic RGM23 43.68,-14.49,46.7,-11.33 1 1389 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +10673 geographic RGM23 (lon-lat) 43.68,-14.49,46.7,-11.33 1 1389 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +10674 projected RGM23 / UTM zone 38S 43.68,-14.49,46.7,-11.33 1 1389 +proj=utm +zone=38 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +10690 geographic EUREF-FIN 19.08,58.84,31.59,70.09 1 1391 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +10699 projected EUREF-FIN / UTM zone 34N 19.08,58.84,24,69.33 1 1391 +proj=utm +zone=34 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +10702 projected EUREF-FIN / UTM zone 36N 30,61.73,31.59,67.71 1 1391 +proj=utm +zone=36 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +10725 geographic UZGD2024 55.99,37.18,73.17,45.58 1 1392 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +10726 projected UZGD2024 / UzREF24 zone 40 55.99,41.25,60,45.58 1 1392 +proj=tmerc +lat_0=40 +lon_0=57 +k=0.9999984965 +x_0=350000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +10727 projected UZGD2024 / UzREF24 zone 41 60,38.22,66,44.82 1 1392 +proj=tmerc +lat_0=37 +lon_0=63 +k=0.99999730738 +x_0=350000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +10728 projected UZGD2024 / UzREF24 zone 42 66,37.18,72,43 1 1392 +proj=tmerc +lat_0=36 +lon_0=69 +k=0.9999962402 +x_0=350000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +10729 projected UZGD2024 / UzREF24 zone 43 72,40.26,73.17,41.21 1 1392 +proj=tmerc +lat_0=39 +lon_0=75 +k=0.999995546 +x_0=350000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +10731 projected ETRS89/DREF91/2016 / UTM zone 31N 3.34,50.97,6.01,55.92 1353 +proj=utm +zone=31 +ellps=GRS80 +units=m +no_defs +10732 projected ETRS89/DREF91/2016 / UTM zone 32N 6,47.27,12.01,55.47 1353 +proj=utm +zone=32 +ellps=GRS80 +units=m +no_defs +10733 projected ETRS89/DREF91/2016 / UTM zone 33N 11.57,47.46,15.04,55.03 1353 +proj=utm +zone=33 +ellps=GRS80 +units=m +no_defs +10736 geographic Sint Eustatius -63.05,17.41,-62.88,17.58 1393 +proj=longlat +ellps=intl +no_defs +10739 geographic BES2020 Sint Eustatius -63.05,17.41,-62.88,17.58 0.5 1394 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +10744 projected Sint Eustatius DPnet short -63.05,17.41,-62.88,17.58 1393 +proj=tmerc +lat_0=0 +lon_0=-63 +k=0.9996 +x_0=578.55 +y_0=-1930396.26 +ellps=intl +units=m +no_defs +10745 projected Sint Eustatius DPnet long -63.05,17.41,-62.88,17.58 1393 +proj=utm +zone=20 +ellps=intl +units=m +no_defs +10758 geographic Bonaire -68.47,11.97,-68.14,12.36 1396 +proj=longlat +ellps=intl +no_defs +10759 projected Bonaire DPnet -68.47,11.97,-68.14,12.36 1396 +proj=tmerc +lat_0=12.180658675 +lon_0=-68.2518022805556 +k=1 +x_0=23209.56 +y_0=21423.99 +ellps=intl +units=m +no_defs +10762 geographic Bonaire 2004 -68.47,11.97,-68.14,12.36 1 1397 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +10773 projected SIRGAS 2000 / Ribeirao Preto Local TM -47.99,-21.37,-47.64,-21.06 1 6674 +proj=tmerc +lat_0=-21.2 +lon_0=-47.7833333333333 +k=1.000092 +x_0=15000 +y_0=25000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +10781 geographic ITRF2020-u2023 -180,-90,180,90 1399 +proj=longlat +ellps=GRS80 +no_defs +10785 geographic IGb20 -180,-90,180,90 1400 +proj=longlat +ellps=GRS80 +no_defs +10791 geographic UGRF 29.57,-1.48,35.01,4.23 1 1401 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +10792 projected UGRF / UTM zone 35N 29.71,0,30,0.86 1 1401 +proj=utm +zone=35 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +10793 projected UGRF / UTM zone 36N 30,0,35.01,4.23 1 1401 +proj=utm +zone=36 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +10794 projected UGRF / UTM zone 36S 30,-1.45,33.98,0 1 1401 +proj=utm +zone=36 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +10795 projected UGRF / UTM zone 35S 29.57,-1.48,30,0 1 1401 +proj=utm +zone=35 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +10800 geographic LibRef21 -13.59,1.02,-7.36,8.52 1 1402 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +10801 projected LibRef21 / UTM zone 28N -13.59,3.12,-12,6.4 1 1402 +proj=utm +zone=28 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +10802 projected LibRef21 / UTM zone 29N -12,1.02,-7.36,8.52 1 1402 +proj=utm +zone=29 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +10807 geographic NKG_ETRF14 3.24,53.89,31.77,71.39 1403 +proj=longlat +ellps=GRS80 +no_defs +10820 projected WGS 84 / Agriculture Canada Albers -139.04,41.67,-52.54,60.01 6326 +proj=aea +lat_0=40 +lon_0=-96 +lat_1=44.75 +lat_2=55.75 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs +10831 geographic Georgia Geodetic Datum 38.97,41.04,46.72,43.59 0.03 1404 +proj=longlat +ellps=GRS80 +towgs84=-2.0796,-0.3484,1.7009,-0.05465,0.06718,-0.06143,0.0181 +no_defs +10833 projected Georgia Geodetic Datum / Lambert 39.99,41.04,46.72,43.59 0.03 1404 +proj=lcc +lat_0=42.5 +lon_0=43.5 +lat_1=41.6666666666667 +lat_2=42.6666666666667 +x_0=1300000 +y_0=400000 +ellps=GRS80 +towgs84=-2.0796,-0.3484,1.7009,-0.05465,0.06718,-0.06143,0.0181 +units=m +no_defs +10836 projected Georgia Geodetic Datum / UTM zone 37N (N-E) 38.97,41.43,42,43.59 0.03 1404 +proj=utm +zone=37 +ellps=GRS80 +towgs84=-2.0796,-0.3484,1.7009,-0.05465,0.06718,-0.06143,0.0181 +units=m +no_defs +10837 projected Georgia Geodetic Datum / UTM zone 38N (N-E) 41.99,41.04,46.72,43.24 0.03 1404 +proj=utm +zone=38 +ellps=GRS80 +towgs84=-2.0796,-0.3484,1.7009,-0.05465,0.06718,-0.06143,0.0181 +units=m +no_defs +10849 geographic EWR3-IRF -1.43,51.7,0.27,52.33 1405 +proj=longlat +ellps=GRS80 +no_defs +10851 projected EWR3 Grid -1.43,51.7,0.27,52.33 1405 +proj=tmerc +lat_0=51.95 +lon_0=-0.9 +k=1 +x_0=192519.9715 +y_0=146942.6806 +ellps=GRS80 +units=m +no_defs +10857 projected SIRGAS 2000 / Brazil Albers -74.01,-33.78,-34.74,5.28 1 6674 +proj=aea +lat_0=-12 +lon_0=-54 +lat_1=-2 +lat_2=-22 +x_0=5000000 +y_0=10000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +10860 geographic WSPG-IRF -1.25,54.35,-0.45,54.7 1406 +proj=longlat +ellps=GRS80 +no_defs +10863 projected WSPG Grid -1.25,54.35,-0.45,54.7 1406 +proj=tmerc +lat_0=54.5 +lon_0=-0.85 +k=1 +x_0=39630.9213 +y_0=36016.293 +ellps=GRS80 +units=m +no_defs +10875 geographic ETRS89-NOR [EUREF89] -3.35,56.08,38.01,84.73 1 1407 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +10892 geographic ETRS89-DNK 3.24,54.36,16.51,58.27 1412 +proj=longlat +ellps=GRS80 +no_defs +10898 geographic Asse 2025 10.6,52.11,10.7,52.16 1413 +proj=longlat +ellps=bessel +no_defs +10899 projected Asse 2025 / 3-degree Gauss-Kruger zone 4 (E-N) 10.6,52.11,10.7,52.16 1413 +proj=tmerc +lat_0=0 +lon_0=12 +k=1 +x_0=4500000 +y_0=0 +ellps=bessel +units=m +no_defs +10910 geographic CSRN epoch 2025.0 (NAD83 2011) -124.45,32.53,-114.12,42.01 2 1414 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +10911 projected CSRN2025 (NAD83 2011) / California zone 1 (ftUS) -124.45,39.59,-119.99,42.01 2 1414 +proj=lcc +lat_0=39.3333333333333 +lon_0=-122 +lat_1=41.6666666666667 +lat_2=40 +x_0=2000000.0001016 +y_0=500000.0001016 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +10912 projected CSRN2025 (NAD83 2011) / California zone 2 (ftUS) -124.06,38.02,-119.54,40.16 2 1414 +proj=lcc +lat_0=37.6666666666667 +lon_0=-122 +lat_1=39.8333333333333 +lat_2=38.3333333333333 +x_0=2000000.0001016 +y_0=500000.0001016 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +10913 projected CSRN2025 (NAD83 2011) / California zone 3 (ftUS) -123.02,36.73,-117.83,38.71 2 1414 +proj=lcc +lat_0=36.5 +lon_0=-120.5 +lat_1=38.4333333333333 +lat_2=37.0666666666667 +x_0=2000000.0001016 +y_0=500000.0001016 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +10914 projected CSRN2025 (NAD83 2011) / California zone 4 (ftUS) -122.01,35.78,-115.62,37.58 2 1414 +proj=lcc +lat_0=35.3333333333333 +lon_0=-119 +lat_1=37.25 +lat_2=36 +x_0=2000000.0001016 +y_0=500000.0001016 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +10915 projected CSRN2025 (NAD83 2011) / California zone 5 (ftUS) -121.42,32.76,-114.12,35.81 2 1414 +proj=lcc +lat_0=33.5 +lon_0=-118 +lat_1=35.4666666666667 +lat_2=34.0333333333333 +x_0=2000000.0001016 +y_0=500000.0001016 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +10916 projected CSRN2025 (NAD83 2011) / California zone 6 (ftUS) -118.15,32.53,-114.42,34.08 2 1414 +proj=lcc +lat_0=32.1666666666667 +lon_0=-116.25 +lat_1=33.8833333333333 +lat_2=32.7833333333333 +x_0=2000000.0001016 +y_0=500000.0001016 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +10917 projected CSRN2025 (NAD83 2011) / California Albers -124.45,32.53,-114.12,42.01 2 1414 +proj=aea +lat_0=0 +lon_0=-120 +lat_1=34 +lat_2=40.5 +x_0=0 +y_0=-4000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +10921 projected CSRN2025 (NAD83 2011) / California zone 1 -124.45,39.59,-119.99,42.01 2 1414 +proj=lcc +lat_0=39.3333333333333 +lon_0=-122 +lat_1=41.6666666666667 +lat_2=40 +x_0=2000000 +y_0=500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +10922 projected CSRN2025 (NAD83 2011) / California zone 2 -124.06,38.02,-119.54,40.16 2 1414 +proj=lcc +lat_0=37.6666666666667 +lon_0=-122 +lat_1=39.8333333333333 +lat_2=38.3333333333333 +x_0=2000000 +y_0=500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +10923 projected CSRN2025 (NAD83 2011) / California zone 3 -123.02,36.73,-117.83,38.71 2 1414 +proj=lcc +lat_0=36.5 +lon_0=-120.5 +lat_1=38.4333333333333 +lat_2=37.0666666666667 +x_0=2000000 +y_0=500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +10924 projected CSRN2025 (NAD83 2011) / California zone 4 -122.01,35.78,-115.62,37.58 2 1414 +proj=lcc +lat_0=35.3333333333333 +lon_0=-119 +lat_1=37.25 +lat_2=36 +x_0=2000000 +y_0=500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +10925 projected CSRN2025 (NAD83 2011) / California zone 5 -121.42,32.76,-114.12,35.81 2 1414 +proj=lcc +lat_0=33.5 +lon_0=-118 +lat_1=35.4666666666667 +lat_2=34.0333333333333 +x_0=2000000 +y_0=500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +10926 projected CSRN2025 (NAD83 2011) / California zone 6 -118.15,32.53,-114.42,34.08 2 1414 +proj=lcc +lat_0=32.1666666666667 +lon_0=-116.25 +lat_1=33.8833333333333 +lat_2=32.7833333333333 +x_0=2000000 +y_0=500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +10941 geographic QazTRF-23 46.49,40.59,87.35,55.45 1 1417 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +10942 projected QazTRF-23 / Gauss-Kruger zone 8 46.49,47.69,48,50.47 1 1417 +proj=tmerc +lat_0=0 +lon_0=45 +k=1 +x_0=8500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +10943 projected QazTRF-23 / Gauss-Kruger zone 9 48,41.15,54,51.79 1 1417 +proj=tmerc +lat_0=0 +lon_0=51 +k=1 +x_0=9500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +10944 projected QazTRF-23 / Gauss-Kruger zone 10 54,41.26,60,51.18 1 1417 +proj=tmerc +lat_0=0 +lon_0=57 +k=1 +x_0=10500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +10945 projected QazTRF-23 / Gauss-Kruger zone 11 60,42.87,66,54.71 1 1417 +proj=tmerc +lat_0=0 +lon_0=63 +k=1 +x_0=11500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +10946 projected QazTRF-23 / Gauss-Kruger zone 12 66,40.59,72,55.45 1 1417 +proj=tmerc +lat_0=0 +lon_0=69 +k=1 +x_0=12500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +10947 projected QazTRF-23 / Gauss-Kruger zone 13 72,42.4,78,54.45 1 1417 +proj=tmerc +lat_0=0 +lon_0=75 +k=1 +x_0=13500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +10948 projected QazTRF-23 / Gauss-Kruger zone 14 78,42.19,84,53.18 1 1417 +proj=tmerc +lat_0=0 +lon_0=81 +k=1 +x_0=14500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +10949 projected QazTRF-23 / Gauss-Kruger zone 15 84,46.82,87.35,50.7 1 1417 +proj=tmerc +lat_0=0 +lon_0=87 +k=1 +x_0=15500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +10952 geographic CSRN epoch 2025.0 (ITRF2020) -124.45,32.53,-114.12,42.01 1418 +proj=longlat +ellps=GRS80 +no_defs +10956 geographic GR96(1996) -75,56.38,7.99,87.02 1.25 1420 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +10959 geographic GR96(2021) -75,56.38,7.99,87.02 1.25 1419 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +10968 geographic NATRF2022 167.65,23.81,-40.73,86.46 1 1422 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +10979 projected NATRF2022 / Gulf -97.22,23.82,-81.17,30.25 1 1422 +proj=lcc +lat_1=28 +lat_0=28 +lon_0=-90 +k_0=0.9996 +x_0=1600200 +y_0=533400 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +10980 projected NATRF2022 / Gulf (ft) -97.22,23.82,-81.17,30.25 1 1422 +proj=lcc +lat_1=28 +lat_0=28 +lon_0=-90 +k_0=0.9996 +x_0=1600200 +y_0=533400 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +10981 projected NATRF2022 / Gulf Louisiana Shelf (ft) -93.82,27.82,-87.76,30.02 1 1422 +proj=lcc +lat_1=30 +lat_0=30 +lon_0=-91.3333333333333 +k_0=0.9999165 +x_0=609610.287 +y_0=147761.325 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +10982 projected NATRF2022 / Gulf Texas Shelf North (ft) -96.86,27.79,-93.41,29.54 1 1422 +proj=lcc +lat_1=29.3325 +lat_0=29.3325 +lon_0=-99 +k_0=0.9998499 +x_0=609633.909 +y_0=166114.095 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +10983 projected NATRF2022 / Gulf Texas Shelf South (ft) -97.22,25.97,-95.36,27.84 1 1422 +proj=lcc +lat_1=27 +lat_0=27 +lon_0=-98.5 +k_0=0.9998844 +x_0=609630.48 +y_0=147679.41 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +10984 projected NATRF2022 / Gulf West (ft) -96.52,26,-95.87,27.61 1 1422 +proj=tmerc +lat_0=0 +lon_0=-99 +k=0.999584 +x_0=500032.02 +y_0=-161.163 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +10985 projected NATRF2022 / Gulf West Central (ft) -95.92,25.61,-89.86,28.69 1 1422 +proj=tmerc +lat_0=0 +lon_0=-93 +k=0.9995867 +x_0=500012.589 +y_0=-167.259 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +10986 projected NATRF2022 / Gulf East Central (ft) -89.95,23.95,-83.91,30.25 1 1422 +proj=tmerc +lat_0=0 +lon_0=-87 +k=0.9995904 +x_0=499995.063 +y_0=-178.308 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +10987 projected NATRF2022 / Gulf East (ft) -84.09,23.82,-81.17,29.94 1 1422 +proj=tmerc +lat_0=0 +lon_0=-81 +k=0.9995987 +x_0=499980.204 +y_0=-206.121 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +11012 projected ETRS89-NOR [EUREF89] / UTM zone 30N (N-E) -3.35,63.47,0,80.49 1 1407 +proj=utm +zone=30 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +11013 projected ETRS89-NOR [EUREF89] / UTM zone 31N (N-E) 0,56.08,6,82.45 1 1407 +proj=utm +zone=31 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +11014 projected ETRS89-NOR [EUREF89] / UTM zone 32N (N-E) 5.99,56.88,12,84 1 1407 +proj=utm +zone=32 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +11015 projected ETRS89-NOR [EUREF89] / UTM zone 33N (N-E) 12,59.88,18,84 1 1407 +proj=utm +zone=33 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +11016 projected ETRS89-NOR [EUREF89] / UTM zone 34N (N-E) 18,68.04,24,84 1 1407 +proj=utm +zone=34 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +11017 projected ETRS89-NOR [EUREF89] / UTM zone 35N (N-E) 23.99,68.58,30,84 1 1407 +proj=utm +zone=35 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +11018 projected ETRS89-NOR [EUREF89] / UTM zone 36N (N-E) 30,69.46,36,84 1 1407 +proj=utm +zone=36 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +11019 projected ETRS89-NOR [EUREF89] / UTM zone 37N (N-E) 36,72.99,38,79.07 1 1407 +proj=utm +zone=37 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +11020 projected ETRS89-NOR [EUREF89] / UTM zone 30N -3.35,63.47,0,80.49 1 1407 +proj=utm +zone=30 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +11021 projected ETRS89-NOR [EUREF89] / UTM zone 31N 0,56.08,6,82.45 1 1407 +proj=utm +zone=31 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +11022 projected ETRS89-NOR [EUREF89] / UTM zone 32N 5.99,56.88,12,84 1 1407 +proj=utm +zone=32 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +11023 projected ETRS89-NOR [EUREF89] / UTM zone 33N 12,59.88,18,84 1 1407 +proj=utm +zone=33 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +11024 projected ETRS89-NOR [EUREF89] / UTM zone 34N 18,68.04,24,84 1 1407 +proj=utm +zone=34 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +11025 projected ETRS89-NOR [EUREF89] / UTM zone 35N 23.99,68.58,30,84 1 1407 +proj=utm +zone=35 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +11026 projected ETRS89-NOR [EUREF89] / UTM zone 36N 30,69.46,36,84 1 1407 +proj=utm +zone=36 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +11027 projected ETRS89-NOR [EUREF89] / UTM zone 37N 36,72.99,38,79.07 1 1407 +proj=utm +zone=37 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +11043 geographic CR-SIRGAS epoch 2019.24 -90.45,2.15,-81.43,11.77 1428 +proj=longlat +ellps=GRS80 +no_defs +11114 projected MAGNA-SIRGAS 2018 / Colombia Far West zone -79.1,1.23,-78.58,2.48 1 1329 +proj=tmerc +lat_0=4.59620322222222 +lon_0=-80.0775077694444 +k=1 +x_0=1000000 +y_0=1000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +11115 projected MAGNA-SIRGAS 2018 / Colombia West zone -78.59,0.03,-75.58,10.21 1 1329 +proj=tmerc +lat_0=4.59620322222222 +lon_0=-77.0775077694444 +k=1 +x_0=1000000 +y_0=1000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +11116 projected MAGNA-SIRGAS 2018 / Colombia Bogota zone -75.59,-2.51,-72.58,11.82 1 1329 +proj=tmerc +lat_0=4.59620322222222 +lon_0=-74.0775077694444 +k=1 +x_0=1000000 +y_0=1000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +11117 projected MAGNA-SIRGAS 2018 / Colombia East Central zone -72.59,-4.23,-69.58,12.52 1 1329 +proj=tmerc +lat_0=4.59620322222222 +lon_0=-71.0775077694444 +k=1 +x_0=1000000 +y_0=1000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +11118 projected MAGNA-SIRGAS 2018 / Colombia East zone -69.59,-2.25,-66.87,6.31 1 1329 +proj=tmerc +lat_0=4.59620322222222 +lon_0=-68.0775077694444 +k=1 +x_0=1000000 +y_0=1000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +20002 projected MWC18 Grid -3.15,53.09,-2.1,53.65 1324 +proj=tmerc +lat_0=53.35 +lon_0=-2.55 +k=1 +x_0=171975.9382 +y_0=116744.6938 +ellps=GRS80 +units=m +no_defs +20004 projected Pulkovo 1995 / Gauss-Kruger zone 4 19.57,54.32,22.87,55.32 1 6200 +proj=tmerc +lat_0=0 +lon_0=21 +k=1 +x_0=4500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +20005 projected Pulkovo 1995 / Gauss-Kruger zone 5 26.61,55.69,30,69.47 1 6200 +proj=tmerc +lat_0=0 +lon_0=27 +k=1 +x_0=5500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +20006 projected Pulkovo 1995 / Gauss-Kruger zone 6 30,50.34,36,70.02 1 6200 +proj=tmerc +lat_0=0 +lon_0=33 +k=1 +x_0=6500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +20007 projected Pulkovo 1995 / Gauss-Kruger zone 7 36,43.18,42.01,69.23 1 6200 +proj=tmerc +lat_0=0 +lon_0=39 +k=1 +x_0=7500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +20008 projected Pulkovo 1995 / Gauss-Kruger zone 8 42,41.19,48,80.91 1 6200 +proj=tmerc +lat_0=0 +lon_0=45 +k=1 +x_0=8500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +20009 projected Pulkovo 1995 / Gauss-Kruger zone 9 48,41.39,54,81.4 1 6200 +proj=tmerc +lat_0=0 +lon_0=51 +k=1 +x_0=9500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +20010 projected Pulkovo 1995 / Gauss-Kruger zone 10 54,50.47,60,81.91 1 6200 +proj=tmerc +lat_0=0 +lon_0=57 +k=1 +x_0=10500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +20011 projected Pulkovo 1995 / Gauss-Kruger zone 11 60,50.66,66,81.77 1 6200 +proj=tmerc +lat_0=0 +lon_0=63 +k=1 +x_0=11500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +20012 projected Pulkovo 1995 / Gauss-Kruger zone 12 66,54.1,72,77.07 1 6200 +proj=tmerc +lat_0=0 +lon_0=69 +k=1 +x_0=12500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +20013 projected Pulkovo 1995 / Gauss-Kruger zone 13 72,53.17,78,79.71 1 6200 +proj=tmerc +lat_0=0 +lon_0=75 +k=1 +x_0=13500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +20014 projected Pulkovo 1995 / Gauss-Kruger zone 14 78,50.69,84,81.03 1 6200 +proj=tmerc +lat_0=0 +lon_0=81 +k=1 +x_0=14500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +20015 projected Pulkovo 1995 / Gauss-Kruger zone 15 84,49.07,90,81.27 1 6200 +proj=tmerc +lat_0=0 +lon_0=87 +k=1 +x_0=15500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +20016 projected Pulkovo 1995 / Gauss-Kruger zone 16 90,49.89,96,81.35 1 6200 +proj=tmerc +lat_0=0 +lon_0=93 +k=1 +x_0=16500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +20017 projected Pulkovo 1995 / Gauss-Kruger zone 17 96,49.73,102,81.32 1 6200 +proj=tmerc +lat_0=0 +lon_0=99 +k=1 +x_0=17500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +20018 projected Pulkovo 1995 / Gauss-Kruger zone 18 102,49.64,108,79.48 1 6200 +proj=tmerc +lat_0=0 +lon_0=105 +k=1 +x_0=18500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +20019 projected Pulkovo 1995 / Gauss-Kruger zone 19 108,49.14,114,76.81 1 6200 +proj=tmerc +lat_0=0 +lon_0=111 +k=1 +x_0=19500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +20020 projected Pulkovo 1995 / Gauss-Kruger zone 20 114,49.51,120,75.96 1 6200 +proj=tmerc +lat_0=0 +lon_0=117 +k=1 +x_0=20500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +20021 projected Pulkovo 1995 / Gauss-Kruger zone 21 120,51.51,126,74 1 6200 +proj=tmerc +lat_0=0 +lon_0=123 +k=1 +x_0=21500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +20022 projected Pulkovo 1995 / Gauss-Kruger zone 22 126,42.25,132,73.61 1 6200 +proj=tmerc +lat_0=0 +lon_0=129 +k=1 +x_0=22500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +20023 projected Pulkovo 1995 / Gauss-Kruger zone 23 132,42.63,138,76.15 1 6200 +proj=tmerc +lat_0=0 +lon_0=135 +k=1 +x_0=23500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +20024 projected Pulkovo 1995 / Gauss-Kruger zone 24 138,45.84,144,76.27 1 6200 +proj=tmerc +lat_0=0 +lon_0=141 +k=1 +x_0=24500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +20025 projected Pulkovo 1995 / Gauss-Kruger zone 25 144,43.6,150,76.82 1 6200 +proj=tmerc +lat_0=0 +lon_0=147 +k=1 +x_0=25500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +20026 projected Pulkovo 1995 / Gauss-Kruger zone 26 150,45.77,156,76.26 1 6200 +proj=tmerc +lat_0=0 +lon_0=153 +k=1 +x_0=26500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +20027 projected Pulkovo 1995 / Gauss-Kruger zone 27 156,50.27,162,77.2 1 6200 +proj=tmerc +lat_0=0 +lon_0=159 +k=1 +x_0=27500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +20028 projected Pulkovo 1995 / Gauss-Kruger zone 28 162,54.47,168,70.03 1 6200 +proj=tmerc +lat_0=0 +lon_0=165 +k=1 +x_0=28500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +20029 projected Pulkovo 1995 / Gauss-Kruger zone 29 168,54.45,174,70.19 1 6200 +proj=tmerc +lat_0=0 +lon_0=171 +k=1 +x_0=29500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +20030 projected Pulkovo 1995 / Gauss-Kruger zone 30 174,61.65,180,71.59 1 6200 +proj=tmerc +lat_0=0 +lon_0=177 +k=1 +x_0=30500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +20031 projected Pulkovo 1995 / Gauss-Kruger zone 31 -180,64.35,-174,71.65 1 6200 +proj=tmerc +lat_0=0 +lon_0=-177 +k=1 +x_0=31500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +20032 projected Pulkovo 1995 / Gauss-Kruger zone 32 -174,64.2,-168.97,67.18 1 6200 +proj=tmerc +lat_0=0 +lon_0=-171 +k=1 +x_0=32500000 +y_0=0 +ellps=krass +towgs84=24.47,-130.89,-81.56,0,0,0.13,-0.22 +units=m +no_defs +20033 geographic MWC18-IRF -3.15,53.09,-2.1,53.65 1324 +proj=longlat +ellps=GRS80 +no_defs +20041 geographic SIRGAS-Chile 2021 -113.21,-59.87,-65.72,-17.5 22 1327 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +20042 projected SIRGAS-Chile 2021 / UTM zone 12S -113.21,-30.54,-108,-23.72 1327 +proj=utm +zone=12 +south +ellps=GRS80 +units=m +no_defs +20046 geographic MAGNA-SIRGAS 2018 -84.77,-4.23,-66.87,15.51 1 1329 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +20047 projected GDA2020 / BCSG2020 152.37,-29.36,153.69,-24.64 3 1168 +proj=tmerc +lat_0=-28 +lon_0=153 +k=0.99999 +x_0=150000 +y_0=200000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +20048 projected SIRGAS-Chile 2021 / UTM zone 18S -78,-59.36,-71.99,-18.35 22 1327 +proj=utm +zone=18 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +20049 projected SIRGAS-Chile 2021 / UTM zone 19S -72,-59.87,-66,-17.5 22 1327 +proj=utm +zone=19 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +20050 projected NAD83(2011) / Amtrak NECCS21 (ft) -78.51,38.64,-70.48,42.74 2 1116 +proj=omerc +lat_0=40.8333333333333 +lonc=-74 +alpha=58 +gamma=58 +k=0.99999 +x_0=457200 +y_0=457200 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.3048 +no_defs +20135 projected Adindan / UTM zone 35N 23.99,4.21,30.01,22.01 7 6201 +proj=utm +zone=35 +a=6378249.145 +rf=293.465 +towgs84=-161,-14,205 +units=m +no_defs +20136 projected Adindan / UTM zone 36N 29.99,3.49,36,22.24 7 6201 +proj=utm +zone=36 +a=6378249.145 +rf=293.465 +towgs84=-161,-14,205 +units=m +no_defs +20137 projected Adindan / UTM zone 37N 36,3.4,42,22.01 9 6201 +proj=utm +zone=37 +a=6378249.145 +rf=293.465 +towgs84=-166,-15,204 +units=m +no_defs +20138 projected Adindan / UTM zone 38N 42,4.11,47.99,12.85 6 6201 +proj=utm +zone=38 +a=6378249.145 +rf=293.465 +towgs84=-165,-11,206 +units=m +no_defs +20249 projected AGD66 / AMG zone 49 109.23,-37.84,114,-17.19 3 6202 +proj=utm +zone=49 +south +ellps=aust_SA +towgs84=-117.808,-51.536,137.784,0.303,0.446,0.234,-0.29 +units=m +no_defs +20250 projected AGD66 / AMG zone 50 114,-38.53,120,-12.61 3 6202 +proj=utm +zone=50 +south +ellps=aust_SA +towgs84=-117.808,-51.536,137.784,0.303,0.446,0.234,-0.29 +units=m +no_defs +20251 projected AGD66 / AMG zone 51 120,-38.07,126.01,-10.46 3 6202 +proj=utm +zone=51 +south +ellps=aust_SA +towgs84=-117.808,-51.536,137.784,0.303,0.446,0.234,-0.29 +units=m +no_defs +20252 projected AGD66 / AMG zone 52 125.99,-37.38,132,-9.1 3 6202 +proj=utm +zone=52 +south +ellps=aust_SA +towgs84=-117.808,-51.536,137.784,0.303,0.446,0.234,-0.29 +units=m +no_defs +20253 projected AGD66 / AMG zone 53 132,-40.71,138.01,-8.88 3 6202 +proj=utm +zone=53 +south +ellps=aust_SA +towgs84=-117.808,-51.536,137.784,0.303,0.446,0.234,-0.29 +units=m +no_defs +20254 projected AGD66 / AMG zone 54 138,-46.63,144.01,-2.53 3 6202 +proj=utm +zone=54 +south +ellps=aust_SA +towgs84=-117.808,-51.536,137.784,0.303,0.446,0.234,-0.29 +units=m +no_defs +20255 projected AGD66 / AMG zone 55 144,-47.2,150.01,-1.3 3 6202 +proj=utm +zone=55 +south +ellps=aust_SA +towgs84=-117.808,-51.536,137.784,0.303,0.446,0.234,-0.29 +units=m +no_defs +20256 projected AGD66 / AMG zone 56 150,-46.44,156,-2.32 3 6202 +proj=utm +zone=56 +south +ellps=aust_SA +towgs84=-117.808,-51.536,137.784,0.303,0.446,0.234,-0.29 +units=m +no_defs +20257 projected AGD66 / AMG zone 57 156,-35.13,162.01,-14.08 3 6202 +proj=utm +zone=57 +south +ellps=aust_SA +towgs84=-117.808,-51.536,137.784,0.303,0.446,0.234,-0.29 +units=m +no_defs +20258 projected AGD66 / AMG zone 58 162,-34.22,163.2,-27.25 3 6202 +proj=utm +zone=58 +south +ellps=aust_SA +towgs84=-117.808,-51.536,137.784,0.303,0.446,0.234,-0.29 +units=m +no_defs +20349 projected AGD84 / AMG zone 49 109.23,-37.84,114,-17.19 3 6203 +proj=utm +zone=49 +south +ellps=aust_SA +towgs84=-117.763,-51.51,139.061,0.292,0.443,0.277,-0.191 +units=m +no_defs +20350 projected AGD84 / AMG zone 50 114,-38.53,120,-12.61 3 6203 +proj=utm +zone=50 +south +ellps=aust_SA +towgs84=-117.763,-51.51,139.061,0.292,0.443,0.277,-0.191 +units=m +no_defs +20351 projected AGD84 / AMG zone 51 120,-38.07,126.01,-10.46 3 6203 +proj=utm +zone=51 +south +ellps=aust_SA +towgs84=-117.763,-51.51,139.061,0.292,0.443,0.277,-0.191 +units=m +no_defs +20352 projected AGD84 / AMG zone 52 125.99,-37.05,132.01,-9.37 3 6203 +proj=utm +zone=52 +south +ellps=aust_SA +towgs84=-117.763,-51.51,139.061,0.292,0.443,0.277,-0.191 +units=m +no_defs +20353 projected AGD84 / AMG zone 53 132,-36.14,138,-25.99 3 6203 +proj=utm +zone=53 +south +ellps=aust_SA +towgs84=-117.763,-51.51,139.061,0.292,0.443,0.277,-0.191 +units=m +no_defs +20354 projected AGD84 / AMG zone 54 138,-38.13,144.01,-9.86 3 6203 +proj=utm +zone=54 +south +ellps=aust_SA +towgs84=-117.763,-51.51,139.061,0.292,0.443,0.277,-0.191 +units=m +no_defs +20355 projected AGD84 / AMG zone 55 144,-29.01,150,-14.01 3 6203 +proj=utm +zone=55 +south +ellps=aust_SA +towgs84=-117.763,-51.51,139.061,0.292,0.443,0.277,-0.191 +units=m +no_defs +20356 projected AGD84 / AMG zone 56 150,-29.19,153.61,-22 3 6203 +proj=utm +zone=56 +south +ellps=aust_SA +towgs84=-117.763,-51.51,139.061,0.292,0.443,0.277,-0.191 +units=m +no_defs +20436 projected Ain el Abd / UTM zone 36N 34.51,26.82,36,29.38 18 6204 +proj=utm +zone=36 +ellps=intl +towgs84=-143,-236,7 +units=m +no_defs +20437 projected Ain el Abd / UTM zone 37N 36,16.59,42,32.16 18 6204 +proj=utm +zone=37 +ellps=intl +towgs84=-143,-236,7 +units=m +no_defs +20438 projected Ain el Abd / UTM zone 38N 42,16.37,48.01,31.15 18 6204 +proj=utm +zone=38 +ellps=intl +towgs84=-143,-236,7 +units=m +no_defs +20439 projected Ain el Abd / UTM zone 39N 47.99,17.94,54.01,30.04 18 6204 +proj=utm +zone=39 +ellps=intl +towgs84=-143,-236,7 +units=m +no_defs +20440 projected Ain el Abd / UTM zone 40N 54,19.66,55.67,22.77 18 6204 +proj=utm +zone=40 +ellps=intl +towgs84=-143,-236,7 +units=m +no_defs +20499 projected Ain el Abd / Bahrain Grid 50.39,25.53,50.85,26.34 18 6204 +proj=utm +zone=39 +ellps=intl +towgs84=-143,-236,7 +units=m +no_defs +20538 projected Afgooye / UTM zone 38N 42,0,48,11.52 44 6205 +proj=utm +zone=38 +ellps=krass +towgs84=-43,-163,45 +units=m +no_defs +20539 projected Afgooye / UTM zone 39N 48,4.44,51.47,12.03 44 6205 +proj=utm +zone=39 +ellps=krass +towgs84=-43,-163,45 +units=m +no_defs +20790 projected Lisbon (Lisbon) / Portuguese National Grid -9.56,36.95,-6.19,42.16 6803 +proj=tmerc +lat_0=39.6666666666667 +lon_0=1 +k=1 +x_0=200000 +y_0=300000 +ellps=intl +pm=-9.13190611111111 +units=m +no_defs +20791 projected Lisbon (Lisbon) / Portuguese Grid -9.56,36.95,-6.19,42.16 6803 +proj=tmerc +lat_0=39.6666666666667 +lon_0=1 +k=1 +x_0=0 +y_0=0 +ellps=intl +pm=-9.13190611111111 +units=m +no_defs +20822 projected Aratu / UTM zone 22S -53.38,-35.71,-47.99,-25.01 1 6208 +proj=utm +zone=22 +south +ellps=intl +towgs84=-157.84,308.54,-146.6 +units=m +no_defs +20823 projected Aratu / UTM zone 23S -48.01,-33.5,-41.99,0 1 6208 +proj=utm +zone=23 +south +ellps=intl +towgs84=-157.84,308.54,-146.6 +units=m +no_defs +20824 projected Aratu / UTM zone 24S -42.01,-26.35,-36,0.01 1 6208 +proj=utm +zone=24 +south +ellps=intl +towgs84=-151.99,287.04,-147.45 +units=m +no_defs +20904 projected GSK-2011 / Gauss-Kruger zone 4 19.57,54.32,22.87,55.32 1 1159 +proj=tmerc +lat_0=0 +lon_0=21 +k=1 +x_0=4500000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +20905 projected GSK-2011 / Gauss-Kruger zone 5 26.61,55.69,30,69.47 1 1159 +proj=tmerc +lat_0=0 +lon_0=27 +k=1 +x_0=5500000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +20906 projected GSK-2011 / Gauss-Kruger zone 6 30,50.34,36,70.02 1 1159 +proj=tmerc +lat_0=0 +lon_0=33 +k=1 +x_0=6500000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +20907 projected GSK-2011 / Gauss-Kruger zone 7 36,43.18,42.01,69.23 1 1159 +proj=tmerc +lat_0=0 +lon_0=39 +k=1 +x_0=7500000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +20908 projected GSK-2011 / Gauss-Kruger zone 8 42,41.19,48,80.91 1 1159 +proj=tmerc +lat_0=0 +lon_0=45 +k=1 +x_0=8500000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +20909 projected GSK-2011 / Gauss-Kruger zone 9 48,41.39,54,81.4 1 1159 +proj=tmerc +lat_0=0 +lon_0=51 +k=1 +x_0=9500000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +20910 projected GSK-2011 / Gauss-Kruger zone 10 54,50.47,60,81.91 1 1159 +proj=tmerc +lat_0=0 +lon_0=57 +k=1 +x_0=10500000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +20911 projected GSK-2011 / Gauss-Kruger zone 11 60,50.66,66,81.77 1 1159 +proj=tmerc +lat_0=0 +lon_0=63 +k=1 +x_0=11500000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +20912 projected GSK-2011 / Gauss-Kruger zone 12 66,54.1,72,77.07 1 1159 +proj=tmerc +lat_0=0 +lon_0=69 +k=1 +x_0=12500000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +20913 projected GSK-2011 / Gauss-Kruger zone 13 72,53.17,78,79.71 1 1159 +proj=tmerc +lat_0=0 +lon_0=75 +k=1 +x_0=13500000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +20914 projected GSK-2011 / Gauss-Kruger zone 14 78,50.69,84,81.03 1 1159 +proj=tmerc +lat_0=0 +lon_0=81 +k=1 +x_0=14500000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +20915 projected GSK-2011 / Gauss-Kruger zone 15 84,49.07,90,81.27 1 1159 +proj=tmerc +lat_0=0 +lon_0=87 +k=1 +x_0=15500000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +20916 projected GSK-2011 / Gauss-Kruger zone 16 90,49.89,96,81.35 1 1159 +proj=tmerc +lat_0=0 +lon_0=93 +k=1 +x_0=16500000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +20917 projected GSK-2011 / Gauss-Kruger zone 17 96,49.73,102,81.32 1 1159 +proj=tmerc +lat_0=0 +lon_0=99 +k=1 +x_0=17500000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +20918 projected GSK-2011 / Gauss-Kruger zone 18 102,49.64,108,79.48 1 1159 +proj=tmerc +lat_0=0 +lon_0=105 +k=1 +x_0=18500000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +20919 projected GSK-2011 / Gauss-Kruger zone 19 108,49.14,114,76.81 1 1159 +proj=tmerc +lat_0=0 +lon_0=111 +k=1 +x_0=19500000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +20920 projected GSK-2011 / Gauss-Kruger zone 20 114,49.51,120,75.96 1 1159 +proj=tmerc +lat_0=0 +lon_0=117 +k=1 +x_0=20500000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +20921 projected GSK-2011 / Gauss-Kruger zone 21 120,51.51,126,74 1 1159 +proj=tmerc +lat_0=0 +lon_0=123 +k=1 +x_0=21500000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +20922 projected GSK-2011 / Gauss-Kruger zone 22 126,42.25,132,73.61 1 1159 +proj=tmerc +lat_0=0 +lon_0=129 +k=1 +x_0=22500000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +20923 projected GSK-2011 / Gauss-Kruger zone 23 132,42.63,138,76.15 1 1159 +proj=tmerc +lat_0=0 +lon_0=135 +k=1 +x_0=23500000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +20924 projected GSK-2011 / Gauss-Kruger zone 24 138,45.84,144,76.27 1 1159 +proj=tmerc +lat_0=0 +lon_0=141 +k=1 +x_0=24500000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +20925 projected GSK-2011 / Gauss-Kruger zone 25 144,43.6,150,76.82 1 1159 +proj=tmerc +lat_0=0 +lon_0=147 +k=1 +x_0=25500000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +20926 projected GSK-2011 / Gauss-Kruger zone 26 150,45.77,156,76.26 1 1159 +proj=tmerc +lat_0=0 +lon_0=153 +k=1 +x_0=26500000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +20927 projected GSK-2011 / Gauss-Kruger zone 27 156,50.27,162,77.2 1 1159 +proj=tmerc +lat_0=0 +lon_0=159 +k=1 +x_0=27500000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +20928 projected GSK-2011 / Gauss-Kruger zone 28 162,54.47,168,70.03 1 1159 +proj=tmerc +lat_0=0 +lon_0=165 +k=1 +x_0=28500000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +20929 projected GSK-2011 / Gauss-Kruger zone 29 168,54.45,174,70.19 1 1159 +proj=tmerc +lat_0=0 +lon_0=171 +k=1 +x_0=29500000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +20930 projected GSK-2011 / Gauss-Kruger zone 30 174,61.65,180,71.59 1 1159 +proj=tmerc +lat_0=0 +lon_0=177 +k=1 +x_0=30500000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +20931 projected GSK-2011 / Gauss-Kruger zone 31 -180,64.35,-174,71.65 1 1159 +proj=tmerc +lat_0=0 +lon_0=-177 +k=1 +x_0=31500000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +20932 projected GSK-2011 / Gauss-Kruger zone 32 -174,64.2,-168.97,67.18 1 1159 +proj=tmerc +lat_0=0 +lon_0=-171 +k=1 +x_0=32500000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +20934 projected Arc 1950 / UTM zone 34S 19.99,-26.88,24,-10.86 44 6209 +proj=utm +zone=34 +south +a=6378249.145 +rf=293.4663077 +towgs84=-143,-90,-294 +units=m +no_defs +20935 projected Arc 1950 / UTM zone 35S 24,-25.84,30,-8.31 44 6209 +proj=utm +zone=35 +south +a=6378249.145 +rf=293.4663077 +towgs84=-143,-90,-294 +units=m +no_defs +20936 projected Arc 1950 / UTM zone 36S 30,-22.42,35.93,-8.19 44 6209 +proj=utm +zone=36 +south +a=6378249.145 +rf=293.4663077 +towgs84=-143,-90,-294 +units=m +no_defs +21004 projected GSK-2011 / Gauss-Kruger CM 21E 19.57,54.32,22.87,55.32 1 1159 +proj=tmerc +lat_0=0 +lon_0=21 +k=1 +x_0=500000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21005 projected GSK-2011 / Gauss-Kruger CM 27E 26.61,55.69,30,69.47 1 1159 +proj=tmerc +lat_0=0 +lon_0=27 +k=1 +x_0=500000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21006 projected GSK-2011 / Gauss-Kruger CM 33E 30,50.34,36,70.02 1 1159 +proj=tmerc +lat_0=0 +lon_0=33 +k=1 +x_0=500000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21007 projected GSK-2011 / Gauss-Kruger CM 39E 36,43.18,42.01,69.23 1 1159 +proj=tmerc +lat_0=0 +lon_0=39 +k=1 +x_0=500000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21008 projected GSK-2011 / Gauss-Kruger CM 45E 42,41.19,48,80.91 1 1159 +proj=tmerc +lat_0=0 +lon_0=45 +k=1 +x_0=500000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21009 projected GSK-2011 / Gauss-Kruger CM 51E 48,41.39,54,81.4 1 1159 +proj=tmerc +lat_0=0 +lon_0=51 +k=1 +x_0=500000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21010 projected GSK-2011 / Gauss-Kruger CM 57E 54,50.47,60,81.91 1 1159 +proj=tmerc +lat_0=0 +lon_0=57 +k=1 +x_0=500000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21011 projected GSK-2011 / Gauss-Kruger CM 63E 60,50.66,66,81.77 1 1159 +proj=tmerc +lat_0=0 +lon_0=63 +k=1 +x_0=500000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21012 projected GSK-2011 / Gauss-Kruger CM 69E 66,54.1,72,77.07 1 1159 +proj=tmerc +lat_0=0 +lon_0=69 +k=1 +x_0=500000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21013 projected GSK-2011 / Gauss-Kruger CM 75E 72,53.17,78,79.71 1 1159 +proj=tmerc +lat_0=0 +lon_0=75 +k=1 +x_0=500000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21014 projected GSK-2011 / Gauss-Kruger CM 81E 78,50.69,84,81.03 1 1159 +proj=tmerc +lat_0=0 +lon_0=81 +k=1 +x_0=500000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21015 projected GSK-2011 / Gauss-Kruger CM 87E 84,49.07,90,81.27 1 1159 +proj=tmerc +lat_0=0 +lon_0=87 +k=1 +x_0=500000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21016 projected GSK-2011 / Gauss-Kruger CM 93E 90,49.89,96,81.35 1 1159 +proj=tmerc +lat_0=0 +lon_0=93 +k=1 +x_0=500000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21017 projected GSK-2011 / Gauss-Kruger CM 99E 96,49.73,102,81.32 1 1159 +proj=tmerc +lat_0=0 +lon_0=99 +k=1 +x_0=500000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21018 projected GSK-2011 / Gauss-Kruger CM 105E 102,49.64,108,79.48 1 1159 +proj=tmerc +lat_0=0 +lon_0=105 +k=1 +x_0=500000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21019 projected GSK-2011 / Gauss-Kruger CM 111E 108,49.14,114,76.81 1 1159 +proj=tmerc +lat_0=0 +lon_0=111 +k=1 +x_0=500000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21020 projected GSK-2011 / Gauss-Kruger CM 117E 114,49.51,120,75.96 1 1159 +proj=tmerc +lat_0=0 +lon_0=117 +k=1 +x_0=500000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21021 projected GSK-2011 / Gauss-Kruger CM 123E 120,51.51,126,74 1 1159 +proj=tmerc +lat_0=0 +lon_0=123 +k=1 +x_0=500000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21022 projected GSK-2011 / Gauss-Kruger CM 129E 126,42.25,132,73.61 1 1159 +proj=tmerc +lat_0=0 +lon_0=129 +k=1 +x_0=500000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21023 projected GSK-2011 / Gauss-Kruger CM 135E 132,42.63,138,76.15 1 1159 +proj=tmerc +lat_0=0 +lon_0=135 +k=1 +x_0=500000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21024 projected GSK-2011 / Gauss-Kruger CM 141E 138,45.84,144,76.27 1 1159 +proj=tmerc +lat_0=0 +lon_0=141 +k=1 +x_0=500000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21025 projected GSK-2011 / Gauss-Kruger CM 147E 144,43.6,150,76.82 1 1159 +proj=tmerc +lat_0=0 +lon_0=147 +k=1 +x_0=500000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21026 projected GSK-2011 / Gauss-Kruger CM 153E 150,45.77,156,76.26 1 1159 +proj=tmerc +lat_0=0 +lon_0=153 +k=1 +x_0=500000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21027 projected GSK-2011 / Gauss-Kruger CM 159E 156,50.27,162,77.2 1 1159 +proj=tmerc +lat_0=0 +lon_0=159 +k=1 +x_0=500000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21028 projected GSK-2011 / Gauss-Kruger CM 165E 162,54.47,168,70.03 1 1159 +proj=tmerc +lat_0=0 +lon_0=165 +k=1 +x_0=500000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21029 projected GSK-2011 / Gauss-Kruger CM 171E 168,54.45,174,70.19 1 1159 +proj=tmerc +lat_0=0 +lon_0=171 +k=1 +x_0=500000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21030 projected GSK-2011 / Gauss-Kruger CM 177E 174,61.65,180,71.59 1 1159 +proj=tmerc +lat_0=0 +lon_0=177 +k=1 +x_0=500000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21031 projected GSK-2011 / Gauss-Kruger CM 177W -180,64.35,-174,71.65 1 1159 +proj=tmerc +lat_0=0 +lon_0=-177 +k=1 +x_0=500000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21032 projected GSK-2011 / Gauss-Kruger CM 171W -174,64.2,-168.97,67.18 1 1159 +proj=tmerc +lat_0=0 +lon_0=-171 +k=1 +x_0=500000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21035 projected Arc 1960 / UTM zone 35S 29.34,-6.81,30,0 35 6210 +proj=utm +zone=35 +south +a=6378249.145 +rf=293.465 +towgs84=-160,-6,-302 +units=m +no_defs +21036 projected Arc 1960 / UTM zone 36S 29.99,-11.61,36,0.01 35 6210 +proj=utm +zone=36 +south +a=6378249.145 +rf=293.465 +towgs84=-160,-6,-302 +units=m +no_defs +21037 projected Arc 1960 / UTM zone 37S 36,-11.75,41.6,0 35 6210 +proj=utm +zone=37 +south +a=6378249.145 +rf=293.465 +towgs84=-160,-6,-302 +units=m +no_defs +21095 projected Arc 1960 / UTM zone 35N 29.71,0,30,0.86 35 6210 +proj=utm +zone=35 +a=6378249.145 +rf=293.465 +towgs84=-160,-6,-302 +units=m +no_defs +21096 projected Arc 1960 / UTM zone 36N 29.99,0,36,4.63 35 6210 +proj=utm +zone=36 +a=6378249.145 +rf=293.465 +towgs84=-160,-6,-302 +units=m +no_defs +21097 projected Arc 1960 / UTM zone 37N 36,0,41.91,4.49 6 6210 +proj=utm +zone=37 +a=6378249.145 +rf=293.465 +towgs84=-157,-2,-299 +units=m +no_defs +21148 projected Batavia / UTM zone 48S 105.06,-7.79,108,-4.07 6 6211 +proj=utm +zone=48 +south +ellps=bessel +towgs84=-377,681,-50 +units=m +no_defs +21149 projected Batavia / UTM zone 49S 108,-8.67,114,-4.27 6 6211 +proj=utm +zone=49 +south +ellps=bessel +towgs84=-377,681,-50 +units=m +no_defs +21150 projected Batavia / UTM zone 50S 114,-8.91,117.01,-5.33 6 6211 +proj=utm +zone=50 +south +ellps=bessel +towgs84=-377,681,-50 +units=m +no_defs +21207 projected GSK-2011 / GSK 3GK zone 7 19.57,54.32,22.5,55.32 1 1159 +proj=tmerc +lat_0=0 +lon_0=21 +k=1 +x_0=7250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21208 projected GSK-2011 / GSK 3GK zone 8 22.5,54.34,22.87,55.07 1 1159 +proj=tmerc +lat_0=0 +lon_0=24 +k=1 +x_0=8250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21209 projected GSK-2011 / GSK 3GK zone 9 26.61,56.05,28.51,68.93 1 1159 +proj=tmerc +lat_0=0 +lon_0=27 +k=1 +x_0=9250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21210 projected GSK-2011 / GSK 3GK zone 10 28.5,52.85,31.5,69.85 1 1159 +proj=tmerc +lat_0=0 +lon_0=30 +k=1 +x_0=10250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21211 projected GSK-2011 / GSK 3GK zone 11 31.5,51.24,34.5,70.02 1 1159 +proj=tmerc +lat_0=0 +lon_0=33 +k=1 +x_0=11250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21212 projected GSK-2011 / GSK 3GK zone 12 34.5,44.61,37.51,69.39 1 1159 +proj=tmerc +lat_0=0 +lon_0=36 +k=1 +x_0=12250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21213 projected GSK-2011 / GSK 3GK zone 13 37.5,43.33,40.5,68.8 1 1159 +proj=tmerc +lat_0=0 +lon_0=39 +k=1 +x_0=13250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21214 projected GSK-2011 / GSK 3GK zone 14 40.5,42.87,43.5,68.74 1 1159 +proj=tmerc +lat_0=0 +lon_0=42 +k=1 +x_0=14250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21215 projected GSK-2011 / GSK 3GK zone 15 43.5,41.89,46.5,80.8 1 1159 +proj=tmerc +lat_0=0 +lon_0=45 +k=1 +x_0=15250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21216 projected GSK-2011 / GSK 3GK zone 16 46.5,41.19,49.5,80.91 1 1159 +proj=tmerc +lat_0=0 +lon_0=48 +k=1 +x_0=16250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21217 projected GSK-2011 / GSK 3GK zone 17 49.5,42.36,52.5,81.22 1 1159 +proj=tmerc +lat_0=0 +lon_0=51 +k=1 +x_0=17250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21218 projected GSK-2011 / GSK 3GK zone 18 52.5,50.52,55.5,81.41 1 1159 +proj=tmerc +lat_0=0 +lon_0=54 +k=1 +x_0=18250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21219 projected GSK-2011 / GSK 3GK zone 19 55.5,50.53,58.5,81.89 1 1159 +proj=tmerc +lat_0=0 +lon_0=57 +k=1 +x_0=19250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21220 projected GSK-2011 / GSK 3GK zone 20 58.5,50.47,61.5,81.91 1 1159 +proj=tmerc +lat_0=0 +lon_0=60 +k=1 +x_0=20250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21221 projected GSK-2011 / GSK 3GK zone 21 61.5,51.03,64.5,81.77 1 1159 +proj=tmerc +lat_0=0 +lon_0=63 +k=1 +x_0=21250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21222 projected GSK-2011 / GSK 3GK zone 22 64.5,54.31,67.5,81.25 1 1159 +proj=tmerc +lat_0=0 +lon_0=66 +k=1 +x_0=22250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21223 projected GSK-2011 / GSK 3GK zone 23 67.5,54.85,70.5,77.07 1 1159 +proj=tmerc +lat_0=0 +lon_0=69 +k=1 +x_0=23250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21224 projected GSK-2011 / GSK 3GK zone 24 70.5,53.43,73.5,73.57 1 1159 +proj=tmerc +lat_0=0 +lon_0=72 +k=1 +x_0=24250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21225 projected GSK-2011 / GSK 3GK zone 25 73.5,53.47,76.5,79.71 1 1159 +proj=tmerc +lat_0=0 +lon_0=75 +k=1 +x_0=25250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21226 projected GSK-2011 / GSK 3GK zone 26 76.5,51.49,79.5,81.03 1 1159 +proj=tmerc +lat_0=0 +lon_0=78 +k=1 +x_0=26250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21227 projected GSK-2011 / GSK 3GK zone 27 79.5,50.7,82.5,81.03 1 1159 +proj=tmerc +lat_0=0 +lon_0=81 +k=1 +x_0=27250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21228 projected GSK-2011 / GSK 3GK zone 28 82.5,49.58,85.5,77.56 1 1159 +proj=tmerc +lat_0=0 +lon_0=84 +k=1 +x_0=28250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21229 projected GSK-2011 / GSK 3GK zone 29 85.5,49.07,88.5,77.16 1 1159 +proj=tmerc +lat_0=0 +lon_0=87 +k=1 +x_0=29250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21230 projected GSK-2011 / GSK 3GK zone 30 88.5,49.44,91.5,81.28 1 1159 +proj=tmerc +lat_0=0 +lon_0=90 +k=1 +x_0=30250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21231 projected GSK-2011 / GSK 3GK zone 31 91.5,50.16,94.5,81.26 1 1159 +proj=tmerc +lat_0=0 +lon_0=93 +k=1 +x_0=31250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21232 projected GSK-2011 / GSK 3GK zone 32 94.5,49.73,97.5,81.35 1 1159 +proj=tmerc +lat_0=0 +lon_0=96 +k=1 +x_0=32250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21233 projected GSK-2011 / GSK 3GK zone 33 97.5,49.79,100.5,80.9 1 1159 +proj=tmerc +lat_0=0 +lon_0=99 +k=1 +x_0=33250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21234 projected GSK-2011 / GSK 3GK zone 34 100.5,50.17,103.5,79.71 1 1159 +proj=tmerc +lat_0=0 +lon_0=102 +k=1 +x_0=34250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21235 projected GSK-2011 / GSK 3GK zone 35 103.5,50.13,106.5,79.21 1 1159 +proj=tmerc +lat_0=0 +lon_0=105 +k=1 +x_0=35250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21236 projected GSK-2011 / GSK 3GK zone 36 106.5,49.25,109.5,78.4 1 1159 +proj=tmerc +lat_0=0 +lon_0=108 +k=1 +x_0=36250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21237 projected GSK-2011 / GSK 3GK zone 37 109.5,49.14,112.5,76.81 1 1159 +proj=tmerc +lat_0=0 +lon_0=111 +k=1 +x_0=37250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21238 projected GSK-2011 / GSK 3GK zone 38 112.5,49.49,115.5,76.7 1 1159 +proj=tmerc +lat_0=0 +lon_0=114 +k=1 +x_0=38250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21239 projected GSK-2011 / GSK 3GK zone 39 115.5,49.51,118.5,74.43 1 1159 +proj=tmerc +lat_0=0 +lon_0=117 +k=1 +x_0=39250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21240 projected GSK-2011 / GSK 3GK zone 40 118.5,49.87,121.5,73.63 1 1159 +proj=tmerc +lat_0=0 +lon_0=120 +k=1 +x_0=40250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21241 projected GSK-2011 / GSK 3GK zone 41 121.5,53.18,124.5,74 1 1159 +proj=tmerc +lat_0=0 +lon_0=123 +k=1 +x_0=41250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21242 projected GSK-2011 / GSK 3GK zone 42 124.5,49.88,127.5,74 1 1159 +proj=tmerc +lat_0=0 +lon_0=126 +k=1 +x_0=42250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21243 projected GSK-2011 / GSK 3GK zone 43 127.5,42.67,130.5,73.59 1 1159 +proj=tmerc +lat_0=0 +lon_0=129 +k=1 +x_0=43250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21244 projected GSK-2011 / GSK 3GK zone 44 130.5,42.25,133.5,71.99 1 1159 +proj=tmerc +lat_0=0 +lon_0=132 +k=1 +x_0=44250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21245 projected GSK-2011 / GSK 3GK zone 45 133.5,42.74,136.5,75.9 1 1159 +proj=tmerc +lat_0=0 +lon_0=135 +k=1 +x_0=45250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21246 projected GSK-2011 / GSK 3GK zone 46 136.5,44.76,139.5,76.27 1 1159 +proj=tmerc +lat_0=0 +lon_0=138 +k=1 +x_0=46250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21247 projected GSK-2011 / GSK 3GK zone 47 139.5,45.84,142.5,76.23 1 1159 +proj=tmerc +lat_0=0 +lon_0=141 +k=1 +x_0=47250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21248 projected GSK-2011 / GSK 3GK zone 48 142.5,43.61,145.5,75.98 1 1159 +proj=tmerc +lat_0=0 +lon_0=144 +k=1 +x_0=48250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21249 projected GSK-2011 / GSK 3GK zone 49 145.5,43.6,148.5,76.76 1 1159 +proj=tmerc +lat_0=0 +lon_0=147 +k=1 +x_0=49250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21250 projected GSK-2011 / GSK 3GK zone 50 148.5,45.21,151.5,76.82 1 1159 +proj=tmerc +lat_0=0 +lon_0=150 +k=1 +x_0=50250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21251 projected GSK-2011 / GSK 3GK zone 51 151.5,46.72,154.5,76.26 1 1159 +proj=tmerc +lat_0=0 +lon_0=153 +k=1 +x_0=51250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21252 projected GSK-2011 / GSK 3GK zone 52 154.5,49.02,157.5,77.2 1 1159 +proj=tmerc +lat_0=0 +lon_0=156 +k=1 +x_0=52250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21253 projected GSK-2011 / GSK 3GK zone 53 157.5,51.36,160.5,71.12 1 1159 +proj=tmerc +lat_0=0 +lon_0=159 +k=1 +x_0=53250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21254 projected GSK-2011 / GSK 3GK zone 54 160.5,54.34,163.5,70.98 1 1159 +proj=tmerc +lat_0=0 +lon_0=162 +k=1 +x_0=54250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21255 projected GSK-2011 / GSK 3GK zone 55 163.5,54.69,166.5,69.82 1 1159 +proj=tmerc +lat_0=0 +lon_0=165 +k=1 +x_0=55250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21256 projected GSK-2011 / GSK 3GK zone 56 166.5,54.45,169.5,70.07 1 1159 +proj=tmerc +lat_0=0 +lon_0=168 +k=1 +x_0=56250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21257 projected GSK-2011 / GSK 3GK zone 57 169.5,59.86,172.5,70.19 1 1159 +proj=tmerc +lat_0=0 +lon_0=171 +k=1 +x_0=57250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21258 projected GSK-2011 / GSK 3GK zone 58 172.5,60.99,175.5,70.02 1 1159 +proj=tmerc +lat_0=0 +lon_0=174 +k=1 +x_0=58250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21259 projected GSK-2011 / GSK 3GK zone 59 175.5,62.09,178.5,71.1 1 1159 +proj=tmerc +lat_0=0 +lon_0=177 +k=1 +x_0=59250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21260 projected GSK-2011 / GSK 3GK zone 60 178.5,62.24,-178.5,71.65 1 1159 +proj=tmerc +lat_0=0 +lon_0=180 +k=1 +x_0=60250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21261 projected GSK-2011 / GSK 3GK zone 61 -178.5,64.74,-175.5,71.61 1 1159 +proj=tmerc +lat_0=0 +lon_0=-177 +k=1 +x_0=61250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21262 projected GSK-2011 / GSK 3GK zone 62 -175.5,64.2,-172.5,67.78 1 1159 +proj=tmerc +lat_0=0 +lon_0=-174 +k=1 +x_0=62250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21263 projected GSK-2011 / GSK 3GK zone 63 -172.5,64.35,-169.57,67.06 1 1159 +proj=tmerc +lat_0=0 +lon_0=-171 +k=1 +x_0=63250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21264 projected GSK-2011 / GSK 3GK zone 64 -169.22,65.7,-168.97,65.86 1 1159 +proj=tmerc +lat_0=0 +lon_0=-168 +k=1 +x_0=64250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21291 projected Barbados 1938 / British West Indies Grid -59.71,13,-59.37,13.39 3 6212 +proj=tmerc +lat_0=0 +lon_0=-62 +k=0.9995 +x_0=400000 +y_0=0 +a=6378249.145 +rf=293.465 +towgs84=31.95,300.99,419.19 +units=m +no_defs +21292 projected Barbados 1938 / Barbados National Grid -59.71,13,-59.37,13.39 3 6212 +proj=tmerc +lat_0=13.1763888888889 +lon_0=-59.5597222222222 +k=0.9999986 +x_0=30000 +y_0=75000 +a=6378249.145 +rf=293.465 +towgs84=31.95,300.99,419.19 +units=m +no_defs +21307 projected GSK-2011 / GSK 3GK CM 21E 19.57,54.32,22.5,55.32 1 1159 +proj=tmerc +lat_0=0 +lon_0=21 +k=1 +x_0=250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21308 projected GSK-2011 / GSK 3GK CM 24E 22.5,54.34,22.87,55.07 1 1159 +proj=tmerc +lat_0=0 +lon_0=24 +k=1 +x_0=250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21309 projected GSK-2011 / GSK 3GK CM 27E 26.61,56.05,28.51,68.93 1 1159 +proj=tmerc +lat_0=0 +lon_0=27 +k=1 +x_0=250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21310 projected GSK-2011 / GSK 3GK CM 30E 28.5,52.85,31.5,69.85 1 1159 +proj=tmerc +lat_0=0 +lon_0=30 +k=1 +x_0=250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21311 projected GSK-2011 / GSK 3GK CM 33E 31.5,51.24,34.5,70.02 1 1159 +proj=tmerc +lat_0=0 +lon_0=33 +k=1 +x_0=250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21312 projected GSK-2011 / GSK 3GK CM 36E 34.5,44.61,37.51,69.39 1 1159 +proj=tmerc +lat_0=0 +lon_0=36 +k=1 +x_0=250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21313 projected GSK-2011 / GSK 3GK CM 39E 37.5,43.33,40.5,68.8 1 1159 +proj=tmerc +lat_0=0 +lon_0=39 +k=1 +x_0=250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21314 projected GSK-2011 / GSK 3GK CM 42E 40.5,42.87,43.5,68.74 1 1159 +proj=tmerc +lat_0=0 +lon_0=42 +k=1 +x_0=250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21315 projected GSK-2011 / GSK 3GK CM 45E 43.5,41.89,46.5,80.8 1 1159 +proj=tmerc +lat_0=0 +lon_0=45 +k=1 +x_0=250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21316 projected GSK-2011 / GSK 3GK CM 48E 46.5,41.19,49.5,80.91 1 1159 +proj=tmerc +lat_0=0 +lon_0=48 +k=1 +x_0=250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21317 projected GSK-2011 / GSK 3GK CM 51E 49.5,42.36,52.5,81.22 1 1159 +proj=tmerc +lat_0=0 +lon_0=51 +k=1 +x_0=250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21318 projected GSK-2011 / GSK 3GK CM 54E 52.5,50.52,55.5,81.41 1 1159 +proj=tmerc +lat_0=0 +lon_0=54 +k=1 +x_0=250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21319 projected GSK-2011 / GSK 3GK CM 57E 55.5,50.53,58.5,81.89 1 1159 +proj=tmerc +lat_0=0 +lon_0=57 +k=1 +x_0=250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21320 projected GSK-2011 / GSK 3GK CM 60E 58.5,50.47,61.5,81.91 1 1159 +proj=tmerc +lat_0=0 +lon_0=60 +k=1 +x_0=250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21321 projected GSK-2011 / GSK 3GK CM 63E 61.5,51.03,64.5,81.77 1 1159 +proj=tmerc +lat_0=0 +lon_0=63 +k=1 +x_0=250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21322 projected GSK-2011 / GSK 3GK CM 66E 64.5,54.31,67.5,81.25 1 1159 +proj=tmerc +lat_0=0 +lon_0=66 +k=1 +x_0=250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21323 projected GSK-2011 / GSK 3GK CM 69E 67.5,54.85,70.5,77.07 1 1159 +proj=tmerc +lat_0=0 +lon_0=69 +k=1 +x_0=250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21324 projected GSK-2011 / GSK 3GK CM 72E 70.5,53.43,73.5,73.57 1 1159 +proj=tmerc +lat_0=0 +lon_0=72 +k=1 +x_0=250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21325 projected GSK-2011 / GSK 3GK CM 75E 73.5,53.47,76.5,79.71 1 1159 +proj=tmerc +lat_0=0 +lon_0=75 +k=1 +x_0=250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21326 projected GSK-2011 / GSK 3GK CM 78E 76.5,51.49,79.5,81.03 1 1159 +proj=tmerc +lat_0=0 +lon_0=78 +k=1 +x_0=250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21327 projected GSK-2011 / GSK 3GK CM 81E 79.5,50.7,82.5,81.03 1 1159 +proj=tmerc +lat_0=0 +lon_0=81 +k=1 +x_0=250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21328 projected GSK-2011 / GSK 3GK CM 84E 82.5,49.58,85.5,77.56 1 1159 +proj=tmerc +lat_0=0 +lon_0=84 +k=1 +x_0=250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21329 projected GSK-2011 / GSK 3GK CM 87E 85.5,49.07,88.5,77.16 1 1159 +proj=tmerc +lat_0=0 +lon_0=87 +k=1 +x_0=250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21330 projected GSK-2011 / GSK 3GK CM 90E 88.5,49.44,91.5,81.28 1 1159 +proj=tmerc +lat_0=0 +lon_0=90 +k=1 +x_0=250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21331 projected GSK-2011 / GSK 3GK CM 93E 91.5,50.16,94.5,81.26 1 1159 +proj=tmerc +lat_0=0 +lon_0=93 +k=1 +x_0=250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21332 projected GSK-2011 / GSK 3GK CM 96E 94.5,49.73,97.5,81.35 1 1159 +proj=tmerc +lat_0=0 +lon_0=96 +k=1 +x_0=250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21333 projected GSK-2011 / GSK 3GK CM 99E 97.5,49.79,100.5,80.9 1 1159 +proj=tmerc +lat_0=0 +lon_0=99 +k=1 +x_0=250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21334 projected GSK-2011 / GSK 3GK CM 102E 100.5,50.17,103.5,79.71 1 1159 +proj=tmerc +lat_0=0 +lon_0=102 +k=1 +x_0=250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21335 projected GSK-2011 / GSK 3GK CM 105E 103.5,50.13,106.5,79.21 1 1159 +proj=tmerc +lat_0=0 +lon_0=105 +k=1 +x_0=250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21336 projected GSK-2011 / GSK 3GK CM 108E 106.5,49.25,109.5,78.4 1 1159 +proj=tmerc +lat_0=0 +lon_0=108 +k=1 +x_0=250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21337 projected GSK-2011 / GSK 3GK CM 111E 109.5,49.14,112.5,76.81 1 1159 +proj=tmerc +lat_0=0 +lon_0=111 +k=1 +x_0=250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21338 projected GSK-2011 / GSK 3GK CM 114E 112.5,49.49,115.5,76.7 1 1159 +proj=tmerc +lat_0=0 +lon_0=114 +k=1 +x_0=250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21339 projected GSK-2011 / GSK 3GK CM 117E 115.5,49.51,118.5,74.43 1 1159 +proj=tmerc +lat_0=0 +lon_0=117 +k=1 +x_0=250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21340 projected GSK-2011 / GSK 3GK CM 120E 118.5,49.87,121.5,73.63 1 1159 +proj=tmerc +lat_0=0 +lon_0=120 +k=1 +x_0=250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21341 projected GSK-2011 / GSK 3GK CM 123E 121.5,53.18,124.5,74 1 1159 +proj=tmerc +lat_0=0 +lon_0=123 +k=1 +x_0=250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21342 projected GSK-2011 / GSK 3GK CM 126E 124.5,49.88,127.5,74 1 1159 +proj=tmerc +lat_0=0 +lon_0=126 +k=1 +x_0=250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21343 projected GSK-2011 / GSK 3GK CM 129E 127.5,42.67,130.5,73.59 1 1159 +proj=tmerc +lat_0=0 +lon_0=129 +k=1 +x_0=250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21344 projected GSK-2011 / GSK 3GK CM 132E 130.5,42.25,133.5,71.99 1 1159 +proj=tmerc +lat_0=0 +lon_0=132 +k=1 +x_0=250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21345 projected GSK-2011 / GSK 3GK CM 135E 133.5,42.74,136.5,75.9 1 1159 +proj=tmerc +lat_0=0 +lon_0=135 +k=1 +x_0=250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21346 projected GSK-2011 / GSK 3GK CM 138E 136.5,44.76,139.5,76.27 1 1159 +proj=tmerc +lat_0=0 +lon_0=138 +k=1 +x_0=250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21347 projected GSK-2011 / GSK 3GK CM 141E 139.5,45.84,142.5,76.23 1 1159 +proj=tmerc +lat_0=0 +lon_0=141 +k=1 +x_0=250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21348 projected GSK-2011 / GSK 3GK CM 144E 142.5,43.61,145.5,75.98 1 1159 +proj=tmerc +lat_0=0 +lon_0=144 +k=1 +x_0=250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21349 projected GSK-2011 / GSK 3GK CM 147E 145.5,43.6,148.5,76.76 1 1159 +proj=tmerc +lat_0=0 +lon_0=147 +k=1 +x_0=250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21350 projected GSK-2011 / GSK 3GK CM 150E 148.5,45.21,151.5,76.82 1 1159 +proj=tmerc +lat_0=0 +lon_0=150 +k=1 +x_0=250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21351 projected GSK-2011 / GSK 3GK CM 153E 151.5,46.72,154.5,76.26 1 1159 +proj=tmerc +lat_0=0 +lon_0=153 +k=1 +x_0=250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21352 projected GSK-2011 / GSK 3GK CM 156E 154.5,49.02,157.5,77.2 1 1159 +proj=tmerc +lat_0=0 +lon_0=156 +k=1 +x_0=250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21353 projected GSK-2011 / GSK 3GK CM 159E 157.5,51.36,160.5,71.12 1 1159 +proj=tmerc +lat_0=0 +lon_0=159 +k=1 +x_0=250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21354 projected GSK-2011 / GSK 3GK CM 162E 160.5,54.34,163.5,70.98 1 1159 +proj=tmerc +lat_0=0 +lon_0=162 +k=1 +x_0=250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21355 projected GSK-2011 / GSK 3GK CM 165E 163.5,54.69,166.5,69.82 1 1159 +proj=tmerc +lat_0=0 +lon_0=165 +k=1 +x_0=250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21356 projected GSK-2011 / GSK 3GK CM 168E 166.5,54.45,169.5,70.07 1 1159 +proj=tmerc +lat_0=0 +lon_0=168 +k=1 +x_0=250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21357 projected GSK-2011 / GSK 3GK CM 171E 169.5,59.86,172.5,70.19 1 1159 +proj=tmerc +lat_0=0 +lon_0=171 +k=1 +x_0=250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21358 projected GSK-2011 / GSK 3GK CM 174E 172.5,60.99,175.5,70.02 1 1159 +proj=tmerc +lat_0=0 +lon_0=174 +k=1 +x_0=250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21359 projected GSK-2011 / GSK 3GK CM 177E 175.5,62.09,178.5,71.1 1 1159 +proj=tmerc +lat_0=0 +lon_0=177 +k=1 +x_0=250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21360 projected GSK-2011 / GSK 3GK CM 180E 178.5,62.24,-178.5,71.65 1 1159 +proj=tmerc +lat_0=0 +lon_0=180 +k=1 +x_0=250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21361 projected GSK-2011 / GSK 3GK CM 177W -178.5,64.74,-175.5,71.61 1 1159 +proj=tmerc +lat_0=0 +lon_0=-177 +k=1 +x_0=250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21362 projected GSK-2011 / GSK 3GK CM 174W -175.5,64.2,-172.5,67.78 1 1159 +proj=tmerc +lat_0=0 +lon_0=-174 +k=1 +x_0=250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21363 projected GSK-2011 / GSK 3GK CM 171W -172.5,64.35,-169.57,67.06 1 1159 +proj=tmerc +lat_0=0 +lon_0=-171 +k=1 +x_0=250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21364 projected GSK-2011 / GSK 3GK CM 168W -169.22,65.7,-168.97,65.86 1 1159 +proj=tmerc +lat_0=0 +lon_0=-168 +k=1 +x_0=250000 +y_0=0 +ellps=GSK2011 +towgs84=0,0,0 +units=m +no_defs +21413 projected Beijing 1954 / Gauss-Kruger zone 13 73.62,35.42,78.01,41.07 1 6214 +proj=tmerc +lat_0=0 +lon_0=75 +k=1 +x_0=13500000 +y_0=0 +ellps=krass +towgs84=15.8,-154.4,-82.3 +units=m +no_defs +21414 projected Beijing 1954 / Gauss-Kruger zone 14 77.98,29.16,84,47.23 1 6214 +proj=tmerc +lat_0=0 +lon_0=81 +k=1 +x_0=14500000 +y_0=0 +ellps=krass +towgs84=15.8,-154.4,-82.3 +units=m +no_defs +21415 projected Beijing 1954 / Gauss-Kruger zone 15 84,27.32,90,49.18 1 6214 +proj=tmerc +lat_0=0 +lon_0=87 +k=1 +x_0=15500000 +y_0=0 +ellps=krass +towgs84=15.8,-154.4,-82.3 +units=m +no_defs +21416 projected Beijing 1954 / Gauss-Kruger zone 16 90,27.71,96.01,47.9 6214 +proj=tmerc +lat_0=0 +lon_0=93 +k=1 +x_0=16500000 +y_0=0 +ellps=krass +units=m +no_defs +21417 projected Beijing 1954 / Gauss-Kruger zone 17 96,21.13,102.01,43.18 6214 +proj=tmerc +lat_0=0 +lon_0=99 +k=1 +x_0=17500000 +y_0=0 +ellps=krass +units=m +no_defs +21418 projected Beijing 1954 / Gauss-Kruger zone 18 102,21.53,108,42.47 1 6214 +proj=tmerc +lat_0=0 +lon_0=105 +k=1 +x_0=18500000 +y_0=0 +ellps=krass +towgs84=11.911,-154.833,-80.079 +units=m +no_defs +21419 projected Beijing 1954 / Gauss-Kruger zone 19 108,18.11,114,45.11 15 6214 +proj=tmerc +lat_0=0 +lon_0=111 +k=1 +x_0=19500000 +y_0=0 +ellps=krass +towgs84=31.4,-144.3,-74.8,0,0,0.814,-0.38 +units=m +no_defs +21420 projected Beijing 1954 / Gauss-Kruger zone 20 114,22.14,120,51.52 15 6214 +proj=tmerc +lat_0=0 +lon_0=117 +k=1 +x_0=20500000 +y_0=0 +ellps=krass +towgs84=15.53,-113.82,-41.38,0,0,0.814,-0.38 +units=m +no_defs +21421 projected Beijing 1954 / Gauss-Kruger zone 21 120,26.34,126,53.56 15 6214 +proj=tmerc +lat_0=0 +lon_0=123 +k=1 +x_0=21500000 +y_0=0 +ellps=krass +towgs84=15.53,-113.82,-41.38,0,0,0.814,-0.38 +units=m +no_defs +21422 projected Beijing 1954 / Gauss-Kruger zone 22 126,40.89,132,52.79 6214 +proj=tmerc +lat_0=0 +lon_0=129 +k=1 +x_0=22500000 +y_0=0 +ellps=krass +units=m +no_defs +21423 projected Beijing 1954 / Gauss-Kruger zone 23 132,45.02,134.77,48.4 6214 +proj=tmerc +lat_0=0 +lon_0=135 +k=1 +x_0=23500000 +y_0=0 +ellps=krass +units=m +no_defs +21453 projected Beijing 1954 / Gauss-Kruger CM 75E 73.62,35.42,78.01,41.07 1 6214 +proj=tmerc +lat_0=0 +lon_0=75 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=15.8,-154.4,-82.3 +units=m +no_defs +21454 projected Beijing 1954 / Gauss-Kruger CM 81E 77.98,29.16,84,47.23 1 6214 +proj=tmerc +lat_0=0 +lon_0=81 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=15.8,-154.4,-82.3 +units=m +no_defs +21455 projected Beijing 1954 / Gauss-Kruger CM 87E 84,27.32,90,49.18 1 6214 +proj=tmerc +lat_0=0 +lon_0=87 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=15.8,-154.4,-82.3 +units=m +no_defs +21456 projected Beijing 1954 / Gauss-Kruger CM 93E 90,27.71,96.01,47.9 6214 +proj=tmerc +lat_0=0 +lon_0=93 +k=1 +x_0=500000 +y_0=0 +ellps=krass +units=m +no_defs +21457 projected Beijing 1954 / Gauss-Kruger CM 99E 96,21.13,102.01,43.18 6214 +proj=tmerc +lat_0=0 +lon_0=99 +k=1 +x_0=500000 +y_0=0 +ellps=krass +units=m +no_defs +21458 projected Beijing 1954 / Gauss-Kruger CM 105E 102,21.53,108,42.47 1 6214 +proj=tmerc +lat_0=0 +lon_0=105 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=11.911,-154.833,-80.079 +units=m +no_defs +21459 projected Beijing 1954 / Gauss-Kruger CM 111E 108,18.11,114,45.11 15 6214 +proj=tmerc +lat_0=0 +lon_0=111 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=31.4,-144.3,-74.8,0,0,0.814,-0.38 +units=m +no_defs +21460 projected Beijing 1954 / Gauss-Kruger CM 117E 114,22.14,120,51.52 15 6214 +proj=tmerc +lat_0=0 +lon_0=117 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=15.53,-113.82,-41.38,0,0,0.814,-0.38 +units=m +no_defs +21461 projected Beijing 1954 / Gauss-Kruger CM 123E 120,26.34,126,53.56 15 6214 +proj=tmerc +lat_0=0 +lon_0=123 +k=1 +x_0=500000 +y_0=0 +ellps=krass +towgs84=15.53,-113.82,-41.38,0,0,0.814,-0.38 +units=m +no_defs +21462 projected Beijing 1954 / Gauss-Kruger CM 129E 126,40.89,132,52.79 6214 +proj=tmerc +lat_0=0 +lon_0=129 +k=1 +x_0=500000 +y_0=0 +ellps=krass +units=m +no_defs +21463 projected Beijing 1954 / Gauss-Kruger CM 135E 132,45.02,134.77,48.4 6214 +proj=tmerc +lat_0=0 +lon_0=135 +k=1 +x_0=500000 +y_0=0 +ellps=krass +units=m +no_defs +21500 projected BD50 (Brussels) / Belge Lambert 50 2.5,49.5,6.4,51.51 6809 +proj=lcc +lat_0=90 +lon_0=0 +lat_1=49.8333333333333 +lat_2=51.1666666666667 +x_0=150000 +y_0=5400000 +ellps=intl +pm=4.367975 +units=m +no_defs +21780 projected CH1903 (Bern) / LV03C 5.95,45.81,10.5,47.81 6801 +proj=somerc +lat_0=46.9524055555556 +lon_0=0 +k_0=1 +x_0=0 +y_0=0 +ellps=bessel +pm=7.43958333333333 +units=m +no_defs +21781 projected CH1903 / LV03 5.95,45.81,10.5,47.81 1.5 6149 +proj=somerc +lat_0=46.9524055555556 +lon_0=7.43958333333333 +k_0=1 +x_0=600000 +y_0=200000 +ellps=bessel +towgs84=674.374,15.056,405.346 +units=m +no_defs +21782 projected CH1903 / LV03C-G 9.47,47.05,9.64,47.28 1.5 6149 +proj=somerc +lat_0=46.9524055555556 +lon_0=7.43958333333333 +k_0=1 +x_0=0 +y_0=0 +ellps=bessel +towgs84=674.374,15.056,405.346 +units=m +no_defs +21818 projected Bogota 1975 / UTM zone 18N -77.37,7.9,-72,13.68 10 6218 +proj=utm +zone=18 +ellps=intl +towgs84=307,304,-318 +units=m +no_defs +21896 projected Bogota 1975 / Colombia West zone -79.1,0.03,-75.58,10.21 10 6218 +proj=tmerc +lat_0=4.59904722222222 +lon_0=-77.0809166666667 +k=1 +x_0=1000000 +y_0=1000000 +ellps=intl +towgs84=307,304,-318 +units=m +no_defs +21897 projected Bogota 1975 / Colombia Bogota zone -75.59,-2.51,-72.58,11.82 10 6218 +proj=tmerc +lat_0=4.59904722222222 +lon_0=-74.0809166666667 +k=1 +x_0=1000000 +y_0=1000000 +ellps=intl +towgs84=307,304,-318 +units=m +no_defs +21898 projected Bogota 1975 / Colombia East Central zone -72.59,-4.23,-69.58,12.52 10 6218 +proj=tmerc +lat_0=4.59904722222222 +lon_0=-71.0809166666667 +k=1 +x_0=1000000 +y_0=1000000 +ellps=intl +towgs84=307,304,-318 +units=m +no_defs +21899 projected Bogota 1975 / Colombia East zone -69.59,-2.25,-66.87,6.31 1 6218 +proj=tmerc +lat_0=4.59904722222222 +lon_0=-68.0809166666667 +k=1 +x_0=1000000 +y_0=1000000 +ellps=intl +towgs84=221.899,274.136,-397.554,-2.80844591036278,0.44850858891268,2.81017234679107,-2.199943 +units=m +no_defs +22032 projected Camacupa 1948 / UTM zone 32S 8.2,-17.26,12,-6.03 5 6220 +proj=utm +zone=32 +south +a=6378249.145 +rf=293.465 +towgs84=-50.9,-347.6,-231 +units=m +no_defs +22033 projected Camacupa 1948 / UTM zone 33S 12,-17.44,18,-5.82 5 6220 +proj=utm +zone=33 +south +a=6378249.145 +rf=293.465 +towgs84=-50.9,-347.6,-231 +units=m +no_defs +22091 projected Camacupa 1948 / TM 11.30 SE 10.83,-6.59,11.67,-6.03 5 6220 +proj=tmerc +lat_0=0 +lon_0=11.5 +k=0.9996 +x_0=500000 +y_0=10000000 +a=6378249.145 +rf=293.465 +towgs84=-50.9,-347.6,-231 +units=m +no_defs +22092 projected Camacupa 1948 / TM 12 SE 8.2,-17.26,13.86,-6.01 5 6220 +proj=tmerc +lat_0=0 +lon_0=12 +k=0.9996 +x_0=500000 +y_0=10000000 +a=6378249.145 +rf=293.465 +towgs84=-50.9,-347.6,-231 +units=m +no_defs +22171 projected POSGAR 98 / Argentina 1 -73.59,-52,-70.5,-36.16 1 6190 +proj=tmerc +lat_0=-90 +lon_0=-72 +k=1 +x_0=1500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +22172 projected POSGAR 98 / Argentina 2 -70.5,-54.9,-67.49,-24.08 1 6190 +proj=tmerc +lat_0=-90 +lon_0=-69 +k=1 +x_0=2500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +22173 projected POSGAR 98 / Argentina 3 -67.5,-55.11,-64.49,-21.78 1 6190 +proj=tmerc +lat_0=-90 +lon_0=-66 +k=1 +x_0=3500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +22174 projected POSGAR 98 / Argentina 4 -64.5,-54.91,-61.5,-21.99 1 6190 +proj=tmerc +lat_0=-90 +lon_0=-63 +k=1 +x_0=4500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +22175 projected POSGAR 98 / Argentina 5 -61.51,-39.06,-58.5,-23.37 1 6190 +proj=tmerc +lat_0=-90 +lon_0=-60 +k=1 +x_0=5500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +22176 projected POSGAR 98 / Argentina 6 -58.5,-38.59,-55.49,-24.84 1 6190 +proj=tmerc +lat_0=-90 +lon_0=-57 +k=1 +x_0=6500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +22177 projected POSGAR 98 / Argentina 7 -55.5,-28.11,-53.65,-25.49 1 6190 +proj=tmerc +lat_0=-90 +lon_0=-54 +k=1 +x_0=7500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +22181 projected POSGAR 94 / Argentina 1 -73.59,-52,-70.5,-36.16 1 6694 +proj=tmerc +lat_0=-90 +lon_0=-72 +k=1 +x_0=1500000 +y_0=0 +ellps=WGS84 +towgs84=0,0,0 +units=m +no_defs +22182 projected POSGAR 94 / Argentina 2 -70.5,-54.9,-67.49,-24.08 1 6694 +proj=tmerc +lat_0=-90 +lon_0=-69 +k=1 +x_0=2500000 +y_0=0 +ellps=WGS84 +towgs84=0,0,0 +units=m +no_defs +22183 projected POSGAR 94 / Argentina 3 -67.5,-55.11,-64.49,-21.78 1 6694 +proj=tmerc +lat_0=-90 +lon_0=-66 +k=1 +x_0=3500000 +y_0=0 +ellps=WGS84 +towgs84=0,0,0 +units=m +no_defs +22184 projected POSGAR 94 / Argentina 4 -64.5,-54.91,-61.5,-21.99 1 6694 +proj=tmerc +lat_0=-90 +lon_0=-63 +k=1 +x_0=4500000 +y_0=0 +ellps=WGS84 +towgs84=0,0,0 +units=m +no_defs +22185 projected POSGAR 94 / Argentina 5 -61.51,-39.06,-58.5,-23.37 1 6694 +proj=tmerc +lat_0=-90 +lon_0=-60 +k=1 +x_0=5500000 +y_0=0 +ellps=WGS84 +towgs84=0,0,0 +units=m +no_defs +22186 projected POSGAR 94 / Argentina 6 -58.5,-38.59,-55.49,-24.84 1 6694 +proj=tmerc +lat_0=-90 +lon_0=-57 +k=1 +x_0=6500000 +y_0=0 +ellps=WGS84 +towgs84=0,0,0 +units=m +no_defs +22187 projected POSGAR 94 / Argentina 7 -55.5,-28.11,-53.65,-25.49 1 6694 +proj=tmerc +lat_0=-90 +lon_0=-54 +k=1 +x_0=7500000 +y_0=0 +ellps=WGS84 +towgs84=0,0,0 +units=m +no_defs +22191 projected Campo Inchauspe / Argentina 1 -73.59,-52,-70.5,-36.16 9 6221 +proj=tmerc +lat_0=-90 +lon_0=-72 +k=1 +x_0=1500000 +y_0=0 +ellps=intl +towgs84=-148,136,90 +units=m +no_defs +22192 projected Campo Inchauspe / Argentina 2 -70.5,-52.43,-67.49,-24.08 9 6221 +proj=tmerc +lat_0=-90 +lon_0=-69 +k=1 +x_0=2500000 +y_0=0 +ellps=intl +towgs84=-148,136,90 +units=m +no_defs +22193 projected Campo Inchauspe / Argentina 3 -67.5,-49.05,-64.49,-21.78 9 6221 +proj=tmerc +lat_0=-90 +lon_0=-66 +k=1 +x_0=3500000 +y_0=0 +ellps=intl +towgs84=-148,136,90 +units=m +no_defs +22194 projected Campo Inchauspe / Argentina 4 -64.5,-43.14,-61.49,-21.99 9 6221 +proj=tmerc +lat_0=-90 +lon_0=-63 +k=1 +x_0=4500000 +y_0=0 +ellps=intl +towgs84=-148,136,90 +units=m +no_defs +22195 projected Campo Inchauspe / Argentina 5 -61.51,-39.06,-58.5,-23.37 9 6221 +proj=tmerc +lat_0=-90 +lon_0=-60 +k=1 +x_0=5500000 +y_0=0 +ellps=intl +towgs84=-148,136,90 +units=m +no_defs +22196 projected Campo Inchauspe / Argentina 6 -58.5,-38.59,-55.49,-24.84 9 6221 +proj=tmerc +lat_0=-90 +lon_0=-57 +k=1 +x_0=6500000 +y_0=0 +ellps=intl +towgs84=-148,136,90 +units=m +no_defs +22197 projected Campo Inchauspe / Argentina 7 -55.5,-28.11,-53.65,-25.49 9 6221 +proj=tmerc +lat_0=-90 +lon_0=-54 +k=1 +x_0=7500000 +y_0=0 +ellps=intl +towgs84=-148,136,90 +units=m +no_defs +22207 projected NAD83(CSRS)v2 / UTM zone 7N -141.01,52.05,-138,72.53 1193 +proj=utm +zone=7 +ellps=GRS80 +units=m +no_defs +22208 projected NAD83(CSRS)v2 / UTM zone 8N -138,48.06,-132,79.42 1193 +proj=utm +zone=8 +ellps=GRS80 +units=m +no_defs +22209 projected NAD83(CSRS)v2 / UTM zone 9N -132,46.52,-126,80.93 1193 +proj=utm +zone=9 +ellps=GRS80 +units=m +no_defs +22210 projected NAD83(CSRS)v2 / UTM zone 10N -126,48.13,-120,81.8 1193 +proj=utm +zone=10 +ellps=GRS80 +units=m +no_defs +22211 projected NAD83(CSRS)v2 / UTM zone 11N -120,48.99,-114,83.5 1193 +proj=utm +zone=11 +ellps=GRS80 +units=m +no_defs +22212 projected NAD83(CSRS)v2 / UTM zone 12N -114,48.99,-108,84 1193 +proj=utm +zone=12 +ellps=GRS80 +units=m +no_defs +22213 projected NAD83(CSRS)v2 / UTM zone 13N -108,48.99,-102,84 1193 +proj=utm +zone=13 +ellps=GRS80 +units=m +no_defs +22214 projected NAD83(CSRS)v2 / UTM zone 14N -102,48.99,-96,84 1193 +proj=utm +zone=14 +ellps=GRS80 +units=m +no_defs +22215 projected NAD83(CSRS)v2 / UTM zone 15N -96,48.03,-90,84 1193 +proj=utm +zone=15 +ellps=GRS80 +units=m +no_defs +22216 projected NAD83(CSRS)v2 / UTM zone 16N -90,46.11,-84,84 1193 +proj=utm +zone=16 +ellps=GRS80 +units=m +no_defs +22217 projected NAD83(CSRS)v2 / UTM zone 17N -84,41.67,-78,84 1193 +proj=utm +zone=17 +ellps=GRS80 +units=m +no_defs +22218 projected NAD83(CSRS)v2 / UTM zone 18N -78,43.63,-72,84 1193 +proj=utm +zone=18 +ellps=GRS80 +units=m +no_defs +22219 projected NAD83(CSRS)v2 / UTM zone 19N -72,40.8,-66,84 1193 +proj=utm +zone=19 +ellps=GRS80 +units=m +no_defs +22220 projected NAD83(CSRS)v2 / UTM zone 20N -66,38.21,-59.99,84 1193 +proj=utm +zone=20 +ellps=GRS80 +units=m +no_defs +22221 projected NAD83(CSRS)v2 / UTM zone 21N -60,38.56,-54,84 1193 +proj=utm +zone=21 +ellps=GRS80 +units=m +no_defs +22222 projected NAD83(CSRS)v2 / UTM zone 22N -54,39.5,-47.99,57.65 1193 +proj=utm +zone=22 +ellps=GRS80 +units=m +no_defs +22229 projected RGSH2020 / UTM zone 29N -8.67,25.73,-6,29.85 1 1355 +proj=utm +zone=29 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +22230 projected RGSH2020 / UTM zone 30N -6,21.82,0,37.01 1 1355 +proj=utm +zone=30 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +22231 projected RGSH2020 / UTM zone 31N 0,18.97,6.01,38.77 1 1355 +proj=utm +zone=31 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +22232 projected RGSH2020 / UTM zone 32N 6,19.6,11.99,38.8 1 1355 +proj=utm +zone=32 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +22234 projected Cape / UTM zone 34S 19.99,-26.88,24,-17.99 9 6222 +proj=utm +zone=34 +south +a=6378249.145 +rf=293.4663077 +towgs84=-136,-108,-292 +units=m +no_defs +22235 projected Cape / UTM zone 35S 24,-25.84,29.38,-17.78 9 6222 +proj=utm +zone=35 +south +a=6378249.145 +rf=293.4663077 +towgs84=-136,-108,-292 +units=m +no_defs +22239 projected NAD83(CSRS)v2 / PEI Stereographic -64.49,45.9,-61.9,47.09 1193 +proj=sterea +lat_0=47.25 +lon_0=-63 +k=0.999912 +x_0=400000 +y_0=800000 +ellps=GRS80 +units=m +no_defs +22240 projected NAD83(CSRS)v2 / NB Stereographic -69.05,44.56,-63.7,48.07 1193 +proj=sterea +lat_0=46.5 +lon_0=-66.5 +k=0.999912 +x_0=2500000 +y_0=7500000 +ellps=GRS80 +units=m +no_defs +22243 projected NAD83(CSRS)v2 / SCoPQ zone 3 -60,50.2,-57.1,52.01 1193 +proj=tmerc +lat_0=0 +lon_0=-58.5 +k=0.9999 +x_0=304800 +y_0=0 +ellps=GRS80 +units=m +no_defs +22244 projected NAD83(CSRS)v2 / SCoPQ zone 4 -63,47.16,-60,52.01 1193 +proj=tmerc +lat_0=0 +lon_0=-61.5 +k=0.9999 +x_0=304800 +y_0=0 +ellps=GRS80 +units=m +no_defs +22245 projected NAD83(CSRS)v2 / SCoPQ zone 5 -66,47.95,-63,60.42 1193 +proj=tmerc +lat_0=0 +lon_0=-64.5 +k=0.9999 +x_0=304800 +y_0=0 +ellps=GRS80 +units=m +no_defs +22246 projected NAD83(CSRS)v2 / SCoPQ zone 6 -69,47.31,-66,59 1193 +proj=tmerc +lat_0=0 +lon_0=-67.5 +k=0.9999 +x_0=304800 +y_0=0 +ellps=GRS80 +units=m +no_defs +22247 projected NAD83(CSRS)v2 / SCoPQ zone 7 -72,45.01,-69,61.8 1193 +proj=tmerc +lat_0=0 +lon_0=-70.5 +k=0.9999 +x_0=304800 +y_0=0 +ellps=GRS80 +units=m +no_defs +22248 projected NAD83(CSRS)v2 / SCoPQ zone 8 -75,44.99,-72,62.53 1193 +proj=tmerc +lat_0=0 +lon_0=-73.5 +k=0.9999 +x_0=304800 +y_0=0 +ellps=GRS80 +units=m +no_defs +22249 projected NAD83(CSRS)v2 / SCoPQ zone 9 -78,45.37,-75,62.62 1193 +proj=tmerc +lat_0=0 +lon_0=-76.5 +k=0.9999 +x_0=304800 +y_0=0 +ellps=GRS80 +units=m +no_defs +22250 projected NAD83(CSRS)v2 / SCoPQ zone 10 -79.85,46.23,-78,62.45 1193 +proj=tmerc +lat_0=0 +lon_0=-79.5 +k=0.9999 +x_0=304800 +y_0=0 +ellps=GRS80 +units=m +no_defs +22262 projected NAD83(CSRS)v2 / Alberta 3TM ref merid 111 W -112.5,48.99,-109.98,60 1193 +proj=tmerc +lat_0=0 +lon_0=-111 +k=0.9999 +x_0=0 +y_0=0 +ellps=GRS80 +units=m +no_defs +22263 projected NAD83(CSRS)v2 / Alberta 3TM ref merid 114 W -115.5,48.99,-112.5,60 1193 +proj=tmerc +lat_0=0 +lon_0=-114 +k=0.9999 +x_0=0 +y_0=0 +ellps=GRS80 +units=m +no_defs +22264 projected NAD83(CSRS)v2 / Alberta 3TM ref merid 117 W -118.5,50.77,-115.5,60 1193 +proj=tmerc +lat_0=0 +lon_0=-117 +k=0.9999 +x_0=0 +y_0=0 +ellps=GRS80 +units=m +no_defs +22265 projected NAD83(CSRS)v2 / Alberta 3TM ref merid 120 W -120,52.88,-118.5,60 1193 +proj=tmerc +lat_0=0 +lon_0=-120 +k=0.9999 +x_0=0 +y_0=0 +ellps=GRS80 +units=m +no_defs +22275 projected Cape / Lo15 14.35,-23.15,14.6,-22.68 6222 +proj=tmerc +axis=wsu +lat_0=0 +lon_0=15 +k=1 +x_0=0 +y_0=0 +a=6378249.145 +rf=293.4663077 +units=m +no_defs +22277 projected Cape / Lo17 16.45,-33.1,18,-28.03 9 6222 +proj=tmerc +axis=wsu +lat_0=0 +lon_0=17 +k=1 +x_0=0 +y_0=0 +a=6378249.145 +rf=293.4663077 +towgs84=-136,-108,-292 +units=m +no_defs +22279 projected Cape / Lo19 17.99,-34.88,20,-28.38 9 6222 +proj=tmerc +axis=wsu +lat_0=0 +lon_0=19 +k=1 +x_0=0 +y_0=0 +a=6378249.145 +rf=293.4663077 +towgs84=-136,-108,-292 +units=m +no_defs +22281 projected Cape / Lo21 19.99,-34.88,22.01,-24.76 9 6222 +proj=tmerc +axis=wsu +lat_0=0 +lon_0=21 +k=1 +x_0=0 +y_0=0 +a=6378249.145 +rf=293.4663077 +towgs84=-136,-108,-292 +units=m +no_defs +22283 projected Cape / Lo23 22,-34.26,24.01,-25.26 9 6222 +proj=tmerc +axis=wsu +lat_0=0 +lon_0=23 +k=1 +x_0=0 +y_0=0 +a=6378249.145 +rf=293.4663077 +towgs84=-136,-108,-292 +units=m +no_defs +22285 projected Cape / Lo25 24,-34.26,26.01,-24.71 9 6222 +proj=tmerc +axis=wsu +lat_0=0 +lon_0=25 +k=1 +x_0=0 +y_0=0 +a=6378249.145 +rf=293.4663077 +towgs84=-136,-108,-292 +units=m +no_defs +22287 projected Cape / Lo27 26,-33.83,28,-22.92 9 6222 +proj=tmerc +axis=wsu +lat_0=0 +lon_0=27 +k=1 +x_0=0 +y_0=0 +a=6378249.145 +rf=293.4663077 +towgs84=-136,-108,-292 +units=m +no_defs +22289 projected Cape / Lo29 27.99,-33.03,30.01,-22.13 9 6222 +proj=tmerc +axis=wsu +lat_0=0 +lon_0=29 +k=1 +x_0=0 +y_0=0 +a=6378249.145 +rf=293.4663077 +towgs84=-136,-108,-292 +units=m +no_defs +22291 projected Cape / Lo31 29.99,-31.38,32.02,-22.22 9 6222 +proj=tmerc +axis=wsu +lat_0=0 +lon_0=31 +k=1 +x_0=0 +y_0=0 +a=6378249.145 +rf=293.4663077 +towgs84=-136,-108,-292 +units=m +no_defs +22293 projected Cape / Lo33 31.95,-28.94,32.95,-26.8 9 6222 +proj=tmerc +axis=wsu +lat_0=0 +lon_0=33 +k=1 +x_0=0 +y_0=0 +a=6378249.145 +rf=293.4663077 +towgs84=-136,-108,-292 +units=m +no_defs +22307 projected NAD83(CSRS)v3 / UTM zone 7N -141.01,52.05,-138,72.53 1194 +proj=utm +zone=7 +ellps=GRS80 +units=m +no_defs +22308 projected NAD83(CSRS)v3 / UTM zone 8N -138,48.06,-132,79.42 1194 +proj=utm +zone=8 +ellps=GRS80 +units=m +no_defs +22309 projected NAD83(CSRS)v3 / UTM zone 9N -132,46.52,-126,80.93 1194 +proj=utm +zone=9 +ellps=GRS80 +units=m +no_defs +22310 projected NAD83(CSRS)v3 / UTM zone 10N -126,48.13,-120,81.8 1194 +proj=utm +zone=10 +ellps=GRS80 +units=m +no_defs +22311 projected NAD83(CSRS)v3 / UTM zone 11N -120,48.99,-114,83.5 1194 +proj=utm +zone=11 +ellps=GRS80 +units=m +no_defs +22312 projected NAD83(CSRS)v3 / UTM zone 12N -114,48.99,-108,84 1194 +proj=utm +zone=12 +ellps=GRS80 +units=m +no_defs +22313 projected NAD83(CSRS)v3 / UTM zone 13N -108,48.99,-102,84 1194 +proj=utm +zone=13 +ellps=GRS80 +units=m +no_defs +22314 projected NAD83(CSRS)v3 / UTM zone 14N -102,48.99,-96,84 1194 +proj=utm +zone=14 +ellps=GRS80 +units=m +no_defs +22315 projected NAD83(CSRS)v3 / UTM zone 15N -96,48.03,-90,84 1194 +proj=utm +zone=15 +ellps=GRS80 +units=m +no_defs +22316 projected NAD83(CSRS)v3 / UTM zone 16N -90,46.11,-84,84 1194 +proj=utm +zone=16 +ellps=GRS80 +units=m +no_defs +22317 projected NAD83(CSRS)v3 / UTM zone 17N -84,41.67,-78,84 1194 +proj=utm +zone=17 +ellps=GRS80 +units=m +no_defs +22318 projected NAD83(CSRS)v3 / UTM zone 18N -78,43.63,-72,84 1194 +proj=utm +zone=18 +ellps=GRS80 +units=m +no_defs +22319 projected NAD83(CSRS)v3 / UTM zone 19N -72,40.8,-66,84 1194 +proj=utm +zone=19 +ellps=GRS80 +units=m +no_defs +22320 projected NAD83(CSRS)v3 / UTM zone 20N -66,38.21,-59.99,84 1194 +proj=utm +zone=20 +ellps=GRS80 +units=m +no_defs +22321 projected NAD83(CSRS)v3 / UTM zone 21N -60,38.56,-54,84 1194 +proj=utm +zone=21 +ellps=GRS80 +units=m +no_defs +22322 projected NAD83(CSRS)v3 / UTM zone 22N -54,39.5,-47.99,57.65 1194 +proj=utm +zone=22 +ellps=GRS80 +units=m +no_defs +22332 projected Carthage / UTM zone 32N 7.81,33.22,13.67,38.41 1 6223 +proj=utm +zone=32 +ellps=clrk80ign +towgs84=-260.1,5.5,432.2 +units=m +no_defs +22337 projected NAD83(CSRS)v3 / MTM NS 1997 zone 5 -66.28,43.41,-63,46.02 1194 +proj=tmerc +lat_0=0 +lon_0=-64.5 +k=0.9999 +x_0=15500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +22338 projected NAD83(CSRS)v3 / MTM NS 1997 zone 4 -63,44.64,-59.73,47.08 1194 +proj=tmerc +lat_0=0 +lon_0=-61.5 +k=0.9999 +x_0=14500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +22348 projected NAD83(CSRS)v3 / MTM zone 8 -75,44.98,-74.35,45.65 1194 +proj=tmerc +lat_0=0 +lon_0=-73.5 +k=0.9999 +x_0=304800 +y_0=0 +ellps=GRS80 +units=m +no_defs +22349 projected NAD83(CSRS)v3 / MTM zone 9 -78,43.63,-75,46.25 1194 +proj=tmerc +lat_0=0 +lon_0=-76.5 +k=0.9999 +x_0=304800 +y_0=0 +ellps=GRS80 +units=m +no_defs +22350 projected NAD83(CSRS)v3 / MTM zone 10 -81,42.26,-77.99,47.33 1194 +proj=tmerc +lat_0=0 +lon_0=-79.5 +k=0.9999 +x_0=304800 +y_0=0 +ellps=GRS80 +units=m +no_defs +22351 projected NAD83(CSRS)v3 / MTM zone 11 -83.6,41.67,-81,46 1194 +proj=tmerc +lat_0=0 +lon_0=-82.5 +k=0.9999 +x_0=304800 +y_0=0 +ellps=GRS80 +units=m +no_defs +22352 projected NAD83(CSRS)v3 / MTM zone 12 -82.5,46,-79.5,55.21 1194 +proj=tmerc +lat_0=0 +lon_0=-81 +k=0.9999 +x_0=304800 +y_0=0 +ellps=GRS80 +units=m +no_defs +22353 projected NAD83(CSRS)v3 / MTM zone 13 -85.5,46,-82.5,55.59 1194 +proj=tmerc +lat_0=0 +lon_0=-84 +k=0.9999 +x_0=304800 +y_0=0 +ellps=GRS80 +units=m +no_defs +22354 projected NAD83(CSRS)v3 / MTM zone 14 -88.5,47.17,-85.5,56.7 1194 +proj=tmerc +lat_0=0 +lon_0=-87 +k=0.9999 +x_0=304800 +y_0=0 +ellps=GRS80 +units=m +no_defs +22355 projected NAD83(CSRS)v3 / MTM zone 15 -91.5,47.97,-88.5,56.9 1194 +proj=tmerc +lat_0=0 +lon_0=-90 +k=0.9999 +x_0=304800 +y_0=0 +ellps=GRS80 +units=m +no_defs +22356 projected NAD83(CSRS)v3 / MTM zone 16 -94.5,48.06,-91.5,55.2 1194 +proj=tmerc +lat_0=0 +lon_0=-93 +k=0.9999 +x_0=304800 +y_0=0 +ellps=GRS80 +units=m +no_defs +22357 projected NAD83(CSRS)v3 / MTM zone 17 -95.16,48.69,-94.5,53.24 1194 +proj=tmerc +lat_0=0 +lon_0=-96 +k=0.9999 +x_0=304800 +y_0=0 +ellps=GRS80 +units=m +no_defs +22391 projected Carthage / Nord Tunisie 8.18,34.65,11.37,37.4 1 6223 +proj=lcc +lat_1=36 +lat_0=36 +lon_0=9.9 +k_0=0.999625544 +x_0=500000 +y_0=300000 +ellps=clrk80ign +towgs84=-260.1,5.5,432.2 +units=m +no_defs +22392 projected Carthage / Sud Tunisie 7.49,30.23,11.59,34.66 14 6223 +proj=lcc +lat_1=33.3 +lat_0=33.3 +lon_0=9.9 +k_0=0.999625769 +x_0=500000 +y_0=300000 +ellps=clrk80ign +towgs84=-263,6,431 +units=m +no_defs +22407 projected NAD83(CSRS)v4 / UTM zone 7N -141.01,52.05,-138,72.53 1195 +proj=utm +zone=7 +ellps=GRS80 +units=m +no_defs +22408 projected NAD83(CSRS)v4 / UTM zone 8N -138,48.06,-132,79.42 1195 +proj=utm +zone=8 +ellps=GRS80 +units=m +no_defs +22409 projected NAD83(CSRS)v4 / UTM zone 9N -132,46.52,-126,80.93 1195 +proj=utm +zone=9 +ellps=GRS80 +units=m +no_defs +22410 projected NAD83(CSRS)v4 / UTM zone 10N -126,48.13,-120,81.8 1195 +proj=utm +zone=10 +ellps=GRS80 +units=m +no_defs +22411 projected NAD83(CSRS)v4 / UTM zone 11N -120,48.99,-114,83.5 1195 +proj=utm +zone=11 +ellps=GRS80 +units=m +no_defs +22412 projected NAD83(CSRS)v4 / UTM zone 12N -114,48.99,-108,84 1195 +proj=utm +zone=12 +ellps=GRS80 +units=m +no_defs +22413 projected NAD83(CSRS)v4 / UTM zone 13N -108,48.99,-102,84 1195 +proj=utm +zone=13 +ellps=GRS80 +units=m +no_defs +22414 projected NAD83(CSRS)v4 / UTM zone 14N -102,48.99,-96,84 1195 +proj=utm +zone=14 +ellps=GRS80 +units=m +no_defs +22415 projected NAD83(CSRS)v4 / UTM zone 15N -96,48.03,-90,84 1195 +proj=utm +zone=15 +ellps=GRS80 +units=m +no_defs +22416 projected NAD83(CSRS)v4 / UTM zone 16N -90,46.11,-84,84 1195 +proj=utm +zone=16 +ellps=GRS80 +units=m +no_defs +22417 projected NAD83(CSRS)v4 / UTM zone 17N -84,41.67,-78,84 1195 +proj=utm +zone=17 +ellps=GRS80 +units=m +no_defs +22418 projected NAD83(CSRS)v4 / UTM zone 18N -78,43.63,-72,84 1195 +proj=utm +zone=18 +ellps=GRS80 +units=m +no_defs +22419 projected NAD83(CSRS)v4 / UTM zone 19N -72,40.8,-66,84 1195 +proj=utm +zone=19 +ellps=GRS80 +units=m +no_defs +22420 projected NAD83(CSRS)v4 / UTM zone 20N -66,38.21,-59.99,84 1195 +proj=utm +zone=20 +ellps=GRS80 +units=m +no_defs +22421 projected NAD83(CSRS)v4 / UTM zone 21N -60,38.56,-54,84 1195 +proj=utm +zone=21 +ellps=GRS80 +units=m +no_defs +22422 projected NAD83(CSRS)v4 / UTM zone 22N -54,39.5,-47.99,57.65 1195 +proj=utm +zone=22 +ellps=GRS80 +units=m +no_defs +22462 projected NAD83(CSRS)v4 / Alberta 3TM ref merid 111 W -112.5,48.99,-109.98,60 1195 +proj=tmerc +lat_0=0 +lon_0=-111 +k=0.9999 +x_0=0 +y_0=0 +ellps=GRS80 +units=m +no_defs +22463 projected NAD83(CSRS)v4 / Alberta 3TM ref merid 114 W -115.5,48.99,-112.5,60 1195 +proj=tmerc +lat_0=0 +lon_0=-114 +k=0.9999 +x_0=0 +y_0=0 +ellps=GRS80 +units=m +no_defs +22464 projected NAD83(CSRS)v4 / Alberta 3TM ref merid 117 W -118.5,50.77,-115.5,60 1195 +proj=tmerc +lat_0=0 +lon_0=-117 +k=0.9999 +x_0=0 +y_0=0 +ellps=GRS80 +units=m +no_defs +22465 projected NAD83(CSRS)v4 / Alberta 3TM ref merid 120 W -120,52.88,-118.5,60 1195 +proj=tmerc +lat_0=0 +lon_0=-120 +k=0.9999 +x_0=0 +y_0=0 +ellps=GRS80 +units=m +no_defs +22521 projected Corrego Alegre 1970-72 / UTM zone 21S -58.16,-31.91,-54,-17.99 5 6225 +proj=utm +zone=21 +south +ellps=intl +towgs84=-206.05,168.28,-3.82 +units=m +no_defs +22522 projected Corrego Alegre 1970-72 / UTM zone 22S -54,-33.78,-48,-15 5 6225 +proj=utm +zone=22 +south +ellps=intl +towgs84=-206.05,168.28,-3.82 +units=m +no_defs +22523 projected Corrego Alegre 1970-72 / UTM zone 23S -48,-25.29,-42,-15 5 6225 +proj=utm +zone=23 +south +ellps=intl +towgs84=-206.05,168.28,-3.82 +units=m +no_defs +22524 projected Corrego Alegre 1970-72 / UTM zone 24S -42,-22.96,-36,-2.68 5 6225 +proj=utm +zone=24 +south +ellps=intl +towgs84=-206.05,168.28,-3.82 +units=m +no_defs +22525 projected Corrego Alegre 1970-72 / UTM zone 25S -36,-10.1,-34.74,-4.99 5 6225 +proj=utm +zone=25 +south +ellps=intl +towgs84=-206.05,168.28,-3.82 +units=m +no_defs +22607 projected NAD83(CSRS)v6 / UTM zone 7N -141.01,52.05,-138,72.53 1197 +proj=utm +zone=7 +ellps=GRS80 +units=m +no_defs +22608 projected NAD83(CSRS)v6 / UTM zone 8N -138,48.06,-132,79.42 1197 +proj=utm +zone=8 +ellps=GRS80 +units=m +no_defs +22609 projected NAD83(CSRS)v6 / UTM zone 9N -132,46.52,-126,80.93 1197 +proj=utm +zone=9 +ellps=GRS80 +units=m +no_defs +22610 projected NAD83(CSRS)v6 / UTM zone 10N -126,48.13,-120,81.8 1197 +proj=utm +zone=10 +ellps=GRS80 +units=m +no_defs +22611 projected NAD83(CSRS)v6 / UTM zone 11N -120,48.99,-114,83.5 1197 +proj=utm +zone=11 +ellps=GRS80 +units=m +no_defs +22612 projected NAD83(CSRS)v6 / UTM zone 12N -114,48.99,-108,84 1197 +proj=utm +zone=12 +ellps=GRS80 +units=m +no_defs +22613 projected NAD83(CSRS)v6 / UTM zone 13N -108,48.99,-102,84 1197 +proj=utm +zone=13 +ellps=GRS80 +units=m +no_defs +22614 projected NAD83(CSRS)v6 / UTM zone 14N -102,48.99,-96,84 1197 +proj=utm +zone=14 +ellps=GRS80 +units=m +no_defs +22615 projected NAD83(CSRS)v6 / UTM zone 15N -96,48.03,-90,84 1197 +proj=utm +zone=15 +ellps=GRS80 +units=m +no_defs +22616 projected NAD83(CSRS)v6 / UTM zone 16N -90,46.11,-84,84 1197 +proj=utm +zone=16 +ellps=GRS80 +units=m +no_defs +22617 projected NAD83(CSRS)v6 / UTM zone 17N -84,41.67,-78,84 1197 +proj=utm +zone=17 +ellps=GRS80 +units=m +no_defs +22618 projected NAD83(CSRS)v6 / UTM zone 18N -78,43.63,-72,84 1197 +proj=utm +zone=18 +ellps=GRS80 +units=m +no_defs +22619 projected NAD83(CSRS)v6 / UTM zone 19N -72,40.8,-66,84 1197 +proj=utm +zone=19 +ellps=GRS80 +units=m +no_defs +22620 projected NAD83(CSRS)v6 / UTM zone 20N -66,38.21,-59.99,84 1197 +proj=utm +zone=20 +ellps=GRS80 +units=m +no_defs +22621 projected NAD83(CSRS)v6 / UTM zone 21N -60,38.56,-54,84 1197 +proj=utm +zone=21 +ellps=GRS80 +units=m +no_defs +22622 projected NAD83(CSRS)v6 / UTM zone 22N -54,39.5,-47.99,57.65 1197 +proj=utm +zone=22 +ellps=GRS80 +units=m +no_defs +22639 projected NAD83(CSRS)v6 / PEI Stereographic -64.49,45.9,-61.9,47.09 1197 +proj=sterea +lat_0=47.25 +lon_0=-63 +k=0.999912 +x_0=400000 +y_0=800000 +ellps=GRS80 +units=m +no_defs +22641 projected NAD83(CSRS)v6 / MTM zone 1 -54.5,46.56,-52.54,49.89 1197 +proj=tmerc +lat_0=0 +lon_0=-53 +k=0.9999 +x_0=304800 +y_0=0 +ellps=GRS80 +units=m +no_defs +22642 projected NAD83(CSRS)v6 / MTM zone 2 -57.5,46.81,-54.49,54.71 1197 +proj=tmerc +lat_0=0 +lon_0=-56 +k=0.9999 +x_0=304800 +y_0=0 +ellps=GRS80 +units=m +no_defs +22643 projected NAD83(CSRS)v6 / MTM zone 3 -59.48,47.5,-57.5,50.54 1197 +proj=tmerc +lat_0=0 +lon_0=-58.5 +k=0.9999 +x_0=304800 +y_0=0 +ellps=GRS80 +units=m +no_defs +22644 projected NAD83(CSRS)v6 / MTM zone 4 -63,52,-60,58.92 1197 +proj=tmerc +lat_0=0 +lon_0=-61.5 +k=0.9999 +x_0=304800 +y_0=0 +ellps=GRS80 +units=m +no_defs +22645 projected NAD83(CSRS)v6 / MTM zone 5 -66,51.58,-63,60.52 1197 +proj=tmerc +lat_0=0 +lon_0=-64.5 +k=0.9999 +x_0=304800 +y_0=0 +ellps=GRS80 +units=m +no_defs +22646 projected NAD83(CSRS)v6 / MTM zone 6 -67.81,52.05,-66,55.34 1197 +proj=tmerc +lat_0=0 +lon_0=-67.5 +k=0.9999 +x_0=304800 +y_0=0 +ellps=GRS80 +units=m +no_defs +22648 projected NAD83(CSRS)v6 / MTM zone 8 -75,44.98,-74.35,45.65 1197 +proj=tmerc +lat_0=0 +lon_0=-73.5 +k=0.9999 +x_0=304800 +y_0=0 +ellps=GRS80 +units=m +no_defs +22649 projected NAD83(CSRS)v6 / MTM zone 9 -78,43.63,-75,46.25 1197 +proj=tmerc +lat_0=0 +lon_0=-76.5 +k=0.9999 +x_0=304800 +y_0=0 +ellps=GRS80 +units=m +no_defs +22650 projected NAD83(CSRS)v6 / MTM zone 10 -81,42.26,-77.99,47.33 1197 +proj=tmerc +lat_0=0 +lon_0=-79.5 +k=0.9999 +x_0=304800 +y_0=0 +ellps=GRS80 +units=m +no_defs +22651 projected NAD83(CSRS)v6 / MTM zone 11 -83.6,41.67,-81,46 1197 +proj=tmerc +lat_0=0 +lon_0=-82.5 +k=0.9999 +x_0=304800 +y_0=0 +ellps=GRS80 +units=m +no_defs +22652 projected NAD83(CSRS)v6 / MTM zone 12 -82.5,46,-79.5,55.21 1197 +proj=tmerc +lat_0=0 +lon_0=-81 +k=0.9999 +x_0=304800 +y_0=0 +ellps=GRS80 +units=m +no_defs +22653 projected NAD83(CSRS)v6 / MTM zone 13 -85.5,46,-82.5,55.59 1197 +proj=tmerc +lat_0=0 +lon_0=-84 +k=0.9999 +x_0=304800 +y_0=0 +ellps=GRS80 +units=m +no_defs +22654 projected NAD83(CSRS)v6 / MTM zone 14 -88.5,47.17,-85.5,56.7 1197 +proj=tmerc +lat_0=0 +lon_0=-87 +k=0.9999 +x_0=304800 +y_0=0 +ellps=GRS80 +units=m +no_defs +22655 projected NAD83(CSRS)v6 / MTM zone 15 -91.5,47.97,-88.5,56.9 1197 +proj=tmerc +lat_0=0 +lon_0=-90 +k=0.9999 +x_0=304800 +y_0=0 +ellps=GRS80 +units=m +no_defs +22656 projected NAD83(CSRS)v6 / MTM zone 16 -94.5,48.06,-91.5,55.2 1197 +proj=tmerc +lat_0=0 +lon_0=-93 +k=0.9999 +x_0=304800 +y_0=0 +ellps=GRS80 +units=m +no_defs +22657 projected NAD83(CSRS)v6 / MTM zone 17 -95.16,48.69,-94.5,53.24 1197 +proj=tmerc +lat_0=0 +lon_0=-96 +k=0.9999 +x_0=304800 +y_0=0 +ellps=GRS80 +units=m +no_defs +22707 projected NAD83(CSRS)v7 / UTM zone 7N -141.01,52.05,-138,72.53 1198 +proj=utm +zone=7 +ellps=GRS80 +units=m +no_defs +22708 projected NAD83(CSRS)v7 / UTM zone 8N -138,48.06,-132,79.42 1198 +proj=utm +zone=8 +ellps=GRS80 +units=m +no_defs +22709 projected NAD83(CSRS)v7 / UTM zone 9N -132,46.52,-126,80.93 1198 +proj=utm +zone=9 +ellps=GRS80 +units=m +no_defs +22710 projected NAD83(CSRS)v7 / UTM zone 10N -126,48.13,-120,81.8 1198 +proj=utm +zone=10 +ellps=GRS80 +units=m +no_defs +22711 projected NAD83(CSRS)v7 / UTM zone 11N -120,48.99,-114,83.5 1198 +proj=utm +zone=11 +ellps=GRS80 +units=m +no_defs +22712 projected NAD83(CSRS)v7 / UTM zone 12N -114,48.99,-108,84 1198 +proj=utm +zone=12 +ellps=GRS80 +units=m +no_defs +22713 projected NAD83(CSRS)v7 / UTM zone 13N -108,48.99,-102,84 1198 +proj=utm +zone=13 +ellps=GRS80 +units=m +no_defs +22714 projected NAD83(CSRS)v7 / UTM zone 14N -102,48.99,-96,84 1198 +proj=utm +zone=14 +ellps=GRS80 +units=m +no_defs +22715 projected NAD83(CSRS)v7 / UTM zone 15N -96,48.03,-90,84 1198 +proj=utm +zone=15 +ellps=GRS80 +units=m +no_defs +22716 projected NAD83(CSRS)v7 / UTM zone 16N -90,46.11,-84,84 1198 +proj=utm +zone=16 +ellps=GRS80 +units=m +no_defs +22717 projected NAD83(CSRS)v7 / UTM zone 17N -84,41.67,-78,84 1198 +proj=utm +zone=17 +ellps=GRS80 +units=m +no_defs +22718 projected NAD83(CSRS)v7 / UTM zone 18N -78,43.63,-72,84 1198 +proj=utm +zone=18 +ellps=GRS80 +units=m +no_defs +22719 projected NAD83(CSRS)v7 / UTM zone 19N -72,40.8,-66,84 1198 +proj=utm +zone=19 +ellps=GRS80 +units=m +no_defs +22720 projected NAD83(CSRS)v7 / UTM zone 20N -66,38.21,-59.99,84 1198 +proj=utm +zone=20 +ellps=GRS80 +units=m +no_defs +22721 projected NAD83(CSRS)v7 / UTM zone 21N -60,38.56,-54,84 1198 +proj=utm +zone=21 +ellps=GRS80 +units=m +no_defs +22722 projected NAD83(CSRS)v7 / UTM zone 22N -54,39.5,-47.99,57.65 1198 +proj=utm +zone=22 +ellps=GRS80 +units=m +no_defs +22739 projected NAD83(CSRS)v7 / PEI Stereographic -64.49,45.9,-61.9,47.09 1198 +proj=sterea +lat_0=47.25 +lon_0=-63 +k=0.999912 +x_0=400000 +y_0=800000 +ellps=GRS80 +units=m +no_defs +22762 projected NAD83(CSRS)v7 / Alberta 3TM ref merid 111 W -112.5,48.99,-109.98,60 1198 +proj=tmerc +lat_0=0 +lon_0=-111 +k=0.9999 +x_0=0 +y_0=0 +ellps=GRS80 +units=m +no_defs +22763 projected NAD83(CSRS)v7 / Alberta 3TM ref merid 114 W -115.5,48.99,-112.5,60 1198 +proj=tmerc +lat_0=0 +lon_0=-114 +k=0.9999 +x_0=0 +y_0=0 +ellps=GRS80 +units=m +no_defs +22764 projected NAD83(CSRS)v7 / Alberta 3TM ref merid 117 W -118.5,50.77,-115.5,60 1198 +proj=tmerc +lat_0=0 +lon_0=-117 +k=0.9999 +x_0=0 +y_0=0 +ellps=GRS80 +units=m +no_defs +22765 projected NAD83(CSRS)v7 / Alberta 3TM ref merid 120 W -120,52.88,-118.5,60 1198 +proj=tmerc +lat_0=0 +lon_0=-120 +k=0.9999 +x_0=0 +y_0=0 +ellps=GRS80 +units=m +no_defs +22770 projected Deir ez Zor / Syria Lambert 35.04,32.31,42.38,37.3 5 6227 +proj=lcc +lat_1=34.65 +lat_0=34.65 +lon_0=37.35 +k_0=0.9996256 +x_0=300000 +y_0=300000 +ellps=clrk80ign +towgs84=-190.421,8.532,238.69 +units=m +no_defs +22780 projected Deir ez Zor / Levant Stereographic 35.04,32.31,42.38,37.3 5 6227 +proj=sterea +lat_0=34.2 +lon_0=39.15 +k=0.9995341 +x_0=0 +y_0=0 +ellps=clrk80ign +towgs84=-190.421,8.532,238.69 +units=m +no_defs +22807 projected NAD83(CSRS)v8 / UTM zone 7N -141.01,52.05,-138,72.53 1365 +proj=utm +zone=7 +ellps=GRS80 +units=m +no_defs +22808 projected NAD83(CSRS)v8 / UTM zone 8N -138,48.06,-132,79.42 1365 +proj=utm +zone=8 +ellps=GRS80 +units=m +no_defs +22809 projected NAD83(CSRS)v8 / UTM zone 9N -132,46.52,-126,80.93 1365 +proj=utm +zone=9 +ellps=GRS80 +units=m +no_defs +22810 projected NAD83(CSRS)v8 / UTM zone 10N -126,48.13,-120,81.8 1365 +proj=utm +zone=10 +ellps=GRS80 +units=m +no_defs +22811 projected NAD83(CSRS)v8 / UTM zone 11N -120,48.99,-114,83.5 1365 +proj=utm +zone=11 +ellps=GRS80 +units=m +no_defs +22812 projected NAD83(CSRS)v8 / UTM zone 12N -114,48.99,-108,84 1365 +proj=utm +zone=12 +ellps=GRS80 +units=m +no_defs +22813 projected NAD83(CSRS)v8 / UTM zone 13N -108,48.99,-102,84 1365 +proj=utm +zone=13 +ellps=GRS80 +units=m +no_defs +22814 projected NAD83(CSRS)v8 / UTM zone 14N -102,48.99,-96,84 1365 +proj=utm +zone=14 +ellps=GRS80 +units=m +no_defs +22815 projected NAD83(CSRS)v8 / UTM zone 15N -96,48.03,-90,84 1365 +proj=utm +zone=15 +ellps=GRS80 +units=m +no_defs +22816 projected NAD83(CSRS)v8 / UTM zone 16N -90,46.11,-84,84 1365 +proj=utm +zone=16 +ellps=GRS80 +units=m +no_defs +22817 projected NAD83(CSRS)v8 / UTM zone 17N -84,41.67,-78,84 1365 +proj=utm +zone=17 +ellps=GRS80 +units=m +no_defs +22818 projected NAD83(CSRS)v8 / UTM zone 18N -78,43.63,-72,84 1365 +proj=utm +zone=18 +ellps=GRS80 +units=m +no_defs +22819 projected NAD83(CSRS)v8 / UTM zone 19N -72,40.8,-66,84 1365 +proj=utm +zone=19 +ellps=GRS80 +units=m +no_defs +22820 projected NAD83(CSRS)v8 / UTM zone 20N -66,38.21,-59.99,84 1365 +proj=utm +zone=20 +ellps=GRS80 +units=m +no_defs +22821 projected NAD83(CSRS)v8 / UTM zone 21N -60,38.56,-54,84 1365 +proj=utm +zone=21 +ellps=GRS80 +units=m +no_defs +22822 projected NAD83(CSRS)v8 / UTM zone 22N -54,39.5,-47.99,57.65 1365 +proj=utm +zone=22 +ellps=GRS80 +units=m +no_defs +22991 projected Egypt 1907 / Blue Belt 33,21.97,36.95,31.36 11 6229 +proj=tmerc +lat_0=30 +lon_0=35 +k=1 +x_0=300000 +y_0=1100000 +ellps=helmert +towgs84=-130,110,-13 +units=m +no_defs +22992 projected Egypt 1907 / Red Belt 29,21.99,34.27,33.82 11 6229 +proj=tmerc +lat_0=30 +lon_0=31 +k=1 +x_0=615000 +y_0=810000 +ellps=helmert +towgs84=-130,110,-13 +units=m +no_defs +22993 projected Egypt 1907 / Purple Belt 24.7,28.18,29,31.68 11 6229 +proj=tmerc +lat_0=30 +lon_0=27 +k=1 +x_0=700000 +y_0=200000 +ellps=helmert +towgs84=-130,110,-13 +units=m +no_defs +22994 projected Egypt 1907 / Extended Purple Belt 24.99,21.99,29.01,28.19 11 6229 +proj=tmerc +lat_0=30 +lon_0=27 +k=1 +x_0=700000 +y_0=1200000 +ellps=helmert +towgs84=-130,110,-13 +units=m +no_defs +23028 projected ED50 / UTM zone 28N -16.1,48.43,-12,56.57 1 6230 +proj=utm +zone=28 +ellps=intl +towgs84=-89.5,-93.8,-123.1,0,0,-0.156,1.2 +units=m +no_defs +23029 projected ED50 / UTM zone 29N -12,36.13,-6,62.41 10 6230 +proj=utm +zone=29 +ellps=intl +towgs84=-87,-98,-121 +units=m +no_defs +23030 projected ED50 / UTM zone 30N -6,35.26,0.01,80.49 10 6230 +proj=utm +zone=30 +ellps=intl +towgs84=-87,-98,-121 +units=m +no_defs +23031 projected ED50 / UTM zone 31N 0,38.56,6,82.45 10 6230 +proj=utm +zone=31 +ellps=intl +towgs84=-87,-98,-121 +units=m +no_defs +23032 projected ED50 / UTM zone 32N 5.99,36.53,12,84.33 10 6230 +proj=utm +zone=32 +ellps=intl +towgs84=-87,-98,-121 +units=m +no_defs +23033 projected ED50 / UTM zone 33N 12,34.49,18.01,84.42 10 6230 +proj=utm +zone=33 +ellps=intl +towgs84=-87,-98,-121 +units=m +no_defs +23034 projected ED50 / UTM zone 34N 17.99,33.59,24.01,84.54 10 6230 +proj=utm +zone=34 +ellps=intl +towgs84=-87,-98,-121 +units=m +no_defs +23035 projected ED50 / UTM zone 35N 24,25.71,30.01,84.73 10 6230 +proj=utm +zone=35 +ellps=intl +towgs84=-87,-98,-121 +units=m +no_defs +23036 projected ED50 / UTM zone 36N 30,29.19,36,84.7 10 6230 +proj=utm +zone=36 +ellps=intl +towgs84=-89.05,-87.03,-124.56 +units=m +no_defs +23037 projected ED50 / UTM zone 37N 36,29.18,42,79.07 10 6230 +proj=utm +zone=37 +ellps=intl +towgs84=-89.05,-87.03,-124.56 +units=m +no_defs +23038 projected ED50 / UTM zone 38N 42,36.97,44.83,41.6 2 6230 +proj=utm +zone=38 +ellps=intl +towgs84=-84.1,-101.8,-129.7,0,0,0.468,1.05 +units=m +no_defs +23090 projected ED50 / TM 0 N -5.05,51.03,3.4,62.03 1 6230 +proj=tmerc +lat_0=0 +lon_0=0 +k=0.9996 +x_0=500000 +y_0=0 +ellps=intl +towgs84=-89.5,-93.8,-123.1,0,0,-0.156,1.2 +units=m +no_defs +23095 projected ED50 / TM 5 NE 2.53,51.44,6.37,55.77 1 6230 +proj=tmerc +lat_0=0 +lon_0=5 +k=0.9996 +x_0=500000 +y_0=0 +ellps=intl +towgs84=-89.5,-93.8,-123.1,0,0,-0.156,1.2 +units=m +no_defs +23239 projected Fahud / UTM zone 39N 51.99,16.59,54,19.67 7 6232 +proj=utm +zone=39 +a=6378249.145 +rf=293.465 +towgs84=-345,3,223 +units=m +no_defs +23240 projected Fahud / UTM zone 40N 54,16.89,59.91,26.42 7 6232 +proj=utm +zone=40 +a=6378249.145 +rf=293.465 +towgs84=-345,3,223 +units=m +no_defs +23301 projected NAD83(2011) / ICS83-Freeport (ftUS) -90.66,41.92,-89.39,42.51 2 1116 +proj=lcc +lat_1=42.2 +lat_0=42.2 +lon_0=-89.95 +k_0=1.000029 +x_0=549860.2997206 +y_0=230124.460248921 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +23302 projected NAD83(2011) / ICS83-Rockford (ftUS) -89.69,41.58,-88.93,42.51 2 1116 +proj=tmerc +lat_0=41.25 +lon_0=-89.25 +k=1.000029 +x_0=860147.320294641 +y_0=149961.8999238 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +23303 projected NAD83(2011) / ICS83-Aurora (ftUS) -88.95,41.45,-88.19,42.5 2 1116 +proj=tmerc +lat_0=41.25 +lon_0=-88.5 +k=1.00003 +x_0=1150012.7000254 +y_0=149961.8999238 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +23304 projected NAD83(2011) / ICS83-Chicago (ftUS) -88.27,41.46,-87.52,42.5 2 1116 +proj=tmerc +lat_0=41.25 +lon_0=-87.8 +k=1.000023 +x_0=1449936.499873 +y_0=149961.8999238 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +23305 projected NAD83(2011) / ICS83-Moline (ftUS) -91.08,41.32,-90.15,41.79 2 1116 +proj=lcc +lat_1=41.55 +lat_0=41.55 +lon_0=-90.6 +k_0=1.000024 +x_0=1749860.2997206 +y_0=230124.460248921 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +23306 projected NAD83(2011) / ICS83-Sterling (ftUS) -90.44,41.14,-89.62,41.94 2 1116 +proj=lcc +lat_1=41.55 +lat_0=41.55 +lon_0=-90.05 +k_0=1.00002 +x_0=2050088.9001778 +y_0=230124.460248921 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +23307 projected NAD83(2011) / ICS83-Ottawa (ftUS) -89.87,40.92,-88.24,41.64 2 1116 +proj=lcc +lat_1=41.3 +lat_0=41.3 +lon_0=-89.05 +k_0=1.000023 +x_0=2360071.12014224 +y_0=230124.460248921 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +23308 projected NAD83(2011) / ICS83-Joliet (ftUS) -88.27,40.99,-87.52,41.73 2 1116 +proj=tmerc +lat_0=40.55 +lon_0=-88 +k=1.000022 +x_0=2649936.499873 +y_0=149961.8999238 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +23309 projected NAD83(2011) / ICS83-Monmouth (ftUS) -91.19,40.62,-90.43,41.34 2 1116 +proj=tmerc +lat_0=40.2 +lon_0=-90.85 +k=1.000024 +x_0=2949860.2997206 +y_0=149961.8999238 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +23310 projected NAD83(2011) / ICS83-Galesburg (ftUS) -90.46,40.18,-89.63,41.24 2 1116 +proj=tmerc +lat_0=37.25 +lon_0=-90.1 +k=1.000023 +x_0=70104.1402082804 +y_0=149961.8999238 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +23311 projected NAD83(2011) / ICS83-Peoria (ftUS) -89.99,40.31,-89.26,40.98 2 1116 +proj=tmerc +lat_0=37.4 +lon_0=-89.65 +k=1.000023 +x_0=420015.24003048 +y_0=189585.979171958 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +23312 projected NAD83(2011) / ICS83-Eureka (ftUS) -89.64,40.59,-88.92,41.15 2 1116 +proj=tmerc +lat_0=37.45 +lon_0=-89.3 +k=1.000025 +x_0=840030.48006096 +y_0=130149.860299721 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +23313 projected NAD83(2011) / ICS83-Bloomington (ftUS) -89.27,40.28,-88.45,40.76 2 1116 +proj=lcc +lat_1=40.5 +lat_0=40.5 +lon_0=-88.85 +k_0=1.000031 +x_0=1150012.7000254 +y_0=530048.26009652 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +23314 projected NAD83(2011) / ICS83-Pontiac (ftUS) -88.94,40.61,-88.23,41.12 2 1116 +proj=lcc +lat_1=40.9 +lat_0=40.9 +lon_0=-88.55 +k_0=1.000025 +x_0=1449936.499873 +y_0=530048.26009652 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +23315 projected NAD83(2011) / ICS83-Watseka (ftUS) -88.46,40.39,-87.52,41.02 2 1116 +proj=lcc +lat_1=40.75 +lat_0=40.75 +lon_0=-87.95 +k_0=1.000024 +x_0=1749860.2997206 +y_0=530048.26009652 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +23316 projected NAD83(2011) / ICS83-Quincy (ftUS) -91.52,39.75,-90.9,40.64 2 1116 +proj=tmerc +lat_0=36.75 +lon_0=-91.25 +k=1.000023 +x_0=2050088.9001778 +y_0=149961.8999238 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +23317 projected NAD83(2011) / ICS83-Macomb (ftUS) -90.92,39.83,-90.19,40.64 2 1116 +proj=tmerc +lat_0=36.8 +lon_0=-90.6 +k=1.000024 +x_0=2350012.7000254 +y_0=149961.8999238 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +23318 projected NAD83(2011) / ICS83-Lincoln (ftUS) -90.59,39.87,-89.14,40.44 2 1116 +proj=lcc +lat_1=40.15 +lat_0=40.15 +lon_0=-89.8 +k_0=1.000018 +x_0=2670053.34010668 +y_0=530048.26009652 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +23319 projected NAD83(2011) / ICS83-Decatur (ftUS) -89.22,39.21,-88.46,40.29 2 1116 +proj=tmerc +lat_0=36.3 +lon_0=-88.8 +k=1.000024 +x_0=2949860.2997206 +y_0=149961.8999238 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +23320 projected NAD83(2011) / ICS83-Champaign (ftUS) -88.47,39.86,-87.52,40.5 2 1116 +proj=lcc +lat_1=40.15 +lat_0=40.15 +lon_0=-88 +k_0=1.000026 +x_0=99974.5999491999 +y_0=860147.320294641 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +23321 projected NAD83(2011) / ICS83-Jacksonville (ftUS) -91.38,39.39,-89.92,39.88 2 1116 +proj=lcc +lat_1=39.65 +lat_0=39.65 +lon_0=-90.6 +k_0=1.000023 +x_0=380086.36017272 +y_0=860147.320294641 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +23322 projected NAD83(2011) / ICS83-Springfield (ftUS) -90,39.52,-89.21,39.98 2 1116 +proj=lcc +lat_1=39.75 +lat_0=39.75 +lon_0=-89.65 +k_0=1.000022 +x_0=709880.61976124 +y_0=879959.35991872 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +23323 projected NAD83(2011) / ICS83-Charleston (ftUS) -88.48,39.37,-87.53,39.89 2 1116 +proj=lcc +lat_1=39.65 +lat_0=39.65 +lon_0=-88 +k_0=1.000024 +x_0=1150012.7000254 +y_0=840030.48006096 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +23324 projected NAD83(2011) / ICS83-Jerseyville (ftUS) -90.94,38.86,-90.14,39.41 2 1116 +proj=tmerc +lat_0=32.85 +lon_0=-90.5 +k=1.000019 +x_0=1449936.499873 +y_0=149961.8999238 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +23325 projected NAD83(2011) / ICS83-Carlinville (ftUS) -90.63,38.99,-89.69,39.53 2 1116 +proj=lcc +lat_1=39.3 +lat_0=39.3 +lon_0=-90.15 +k_0=1.00002 +x_0=1749860.2997206 +y_0=840030.48006096 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +23326 projected NAD83(2011) / ICS83-Taylorville (ftUS) -89.71,38.99,-89.02,39.83 2 1116 +proj=tmerc +lat_0=33.15 +lon_0=-89.4 +k=1.000023 +x_0=2050088.9001778 +y_0=149961.8999238 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +23327 projected NAD83(2011) / ICS83-Effingham (ftUS) -89.64,38.73,-88.36,39.22 2 1116 +proj=lcc +lat_1=38.95 +lat_0=38.95 +lon_0=-89 +k_0=1.000019 +x_0=2350012.7000254 +y_0=840030.48006096 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +23328 projected NAD83(2011) / ICS83-Robinson (ftUS) -88.48,38.84,-87.51,39.49 2 1116 +proj=lcc +lat_1=39.1 +lat_0=39.1 +lon_0=-88 +k_0=1.000017 +x_0=2649936.499873 +y_0=840030.48006096 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +23329 projected NAD83(2011) / ICS83-Belleville (ftUS) -90.38,38.08,-89.59,39 2 1116 +proj=tmerc +lat_0=32.35 +lon_0=-90 +k=1.000016 +x_0=2949860.2997206 +y_0=149961.8999238 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +23330 projected NAD83(2011) / ICS83-Mount Vernon (ftUS) -89.71,38.12,-88.69,38.83 2 1116 +proj=lcc +lat_1=38.45 +lat_0=38.45 +lon_0=-89.15 +k_0=1.000015 +x_0=99974.5999491999 +y_0=1150012.7000254 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +23331 projected NAD83(2011) / ICS83-Olney (ftUS) -88.71,38.23,-87.49,38.92 2 1116 +proj=lcc +lat_1=38.55 +lat_0=38.55 +lon_0=-88.15 +k_0=1.000013 +x_0=380086.36017272 +y_0=1150012.7000254 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +23332 projected NAD83(2011) / ICS83-Carbondale (ftUS) -90.21,37.56,-87.91,38.26 2 1116 +proj=lcc +lat_1=37.9 +lat_0=37.9 +lon_0=-88.95 +k_0=1.000012 +x_0=729997.45999492 +y_0=1150012.7000254 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +23333 projected NAD83(2011) / ICS83-Metropolis (ftUS) -89.53,36.97,-88.06,37.61 2 1116 +proj=lcc +lat_1=37.2 +lat_0=37.2 +lon_0=-88.9 +k_0=1.00001 +x_0=1110083.82016764 +y_0=1170129.54025908 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +23700 projected HD72 / EOV 16.11,45.74,22.9,48.58 1 6237 +proj=somerc +lat_0=47.1443937222222 +lon_0=19.0485717777778 +k_0=0.99993 +x_0=650000 +y_0=200000 +ellps=GRS67 +towgs84=52.17,-71.82,-14.9 +units=m +no_defs +23830 projected DGN95 / Indonesia TM-3 zone 46.2 95.16,2.55,96,5.97 1 6755 +proj=tmerc +lat_0=0 +lon_0=94.5 +k=0.9999 +x_0=200000 +y_0=1500000 +ellps=WGS84 +towgs84=0,0,0 +units=m +no_defs +23831 projected DGN95 / Indonesia TM-3 zone 47.1 96,-1.81,99,5.42 1 6755 +proj=tmerc +lat_0=0 +lon_0=97.5 +k=0.9999 +x_0=200000 +y_0=1500000 +ellps=WGS84 +towgs84=0,0,0 +units=m +no_defs +23832 projected DGN95 / Indonesia TM-3 zone 47.2 99,-3.57,102,3.71 1 6755 +proj=tmerc +lat_0=0 +lon_0=100.5 +k=0.9999 +x_0=200000 +y_0=1500000 +ellps=WGS84 +towgs84=0,0,0 +units=m +no_defs +23833 projected DGN95 / Indonesia TM-3 zone 48.1 102,-5.99,105,1.68 1 6755 +proj=tmerc +lat_0=0 +lon_0=103.5 +k=0.9999 +x_0=200000 +y_0=1500000 +ellps=WGS84 +towgs84=0,0,0 +units=m +no_defs +23834 projected DGN95 / Indonesia TM-3 zone 48.2 105,-7.79,108,4.11 1 6755 +proj=tmerc +lat_0=0 +lon_0=106.5 +k=0.9999 +x_0=200000 +y_0=1500000 +ellps=WGS84 +towgs84=0,0,0 +units=m +no_defs +23835 projected DGN95 / Indonesia TM-3 zone 49.1 108,-8.31,111,4.25 1 6755 +proj=tmerc +lat_0=0 +lon_0=109.5 +k=0.9999 +x_0=200000 +y_0=1500000 +ellps=WGS84 +towgs84=0,0,0 +units=m +no_defs +23836 projected DGN95 / Indonesia TM-3 zone 49.2 111,-8.67,114,1.59 1 6755 +proj=tmerc +lat_0=0 +lon_0=112.5 +k=0.9999 +x_0=200000 +y_0=1500000 +ellps=WGS84 +towgs84=0,0,0 +units=m +no_defs +23837 projected DGN95 / Indonesia TM-3 zone 50.1 114,-9.15,117.01,4.37 1 6755 +proj=tmerc +lat_0=0 +lon_0=115.5 +k=0.9999 +x_0=200000 +y_0=1500000 +ellps=WGS84 +towgs84=0,0,0 +units=m +no_defs +23838 projected DGN95 / Indonesia TM-3 zone 50.2 117,-10.15,120,4.36 1 6755 +proj=tmerc +lat_0=0 +lon_0=118.5 +k=0.9999 +x_0=200000 +y_0=1500000 +ellps=WGS84 +towgs84=0,0,0 +units=m +no_defs +23839 projected DGN95 / Indonesia TM-3 zone 51.1 120,-10.98,123,1.4 1 6755 +proj=tmerc +lat_0=0 +lon_0=121.5 +k=0.9999 +x_0=200000 +y_0=1500000 +ellps=WGS84 +towgs84=0,0,0 +units=m +no_defs +23840 projected DGN95 / Indonesia TM-3 zone 51.2 123,-10.92,126,3.84 1 6755 +proj=tmerc +lat_0=0 +lon_0=124.5 +k=0.9999 +x_0=200000 +y_0=1500000 +ellps=WGS84 +towgs84=0,0,0 +units=m +no_defs +23841 projected DGN95 / Indonesia TM-3 zone 52.1 126,-8.32,129,4.59 1 6755 +proj=tmerc +lat_0=0 +lon_0=127.5 +k=0.9999 +x_0=200000 +y_0=1500000 +ellps=WGS84 +towgs84=0,0,0 +units=m +no_defs +23842 projected DGN95 / Indonesia TM-3 zone 52.2 129,-8.41,132,0.1 1 6755 +proj=tmerc +lat_0=0 +lon_0=130.5 +k=0.9999 +x_0=200000 +y_0=1500000 +ellps=WGS84 +towgs84=0,0,0 +units=m +no_defs +23843 projected DGN95 / Indonesia TM-3 zone 53.1 132,-7.3,135,-0.29 1 6755 +proj=tmerc +lat_0=0 +lon_0=133.5 +k=0.9999 +x_0=200000 +y_0=1500000 +ellps=WGS84 +towgs84=0,0,0 +units=m +no_defs +23844 projected DGN95 / Indonesia TM-3 zone 53.2 135,-8.49,138,-0.58 1 6755 +proj=tmerc +lat_0=0 +lon_0=136.5 +k=0.9999 +x_0=200000 +y_0=1500000 +ellps=WGS84 +towgs84=0,0,0 +units=m +no_defs +23845 projected DGN95 / Indonesia TM-3 zone 54.1 138,-9.19,141.01,-1.49 1 6755 +proj=tmerc +lat_0=0 +lon_0=139.5 +k=0.9999 +x_0=200000 +y_0=1500000 +ellps=WGS84 +towgs84=0,0,0 +units=m +no_defs +23846 projected ID74 / UTM zone 46N 95.16,2.55,96,5.97 3 6238 +proj=utm +zone=46 +a=6378160 +rf=298.247 +towgs84=-1.977,-13.06,-9.993,0.364,0.254,0.689,-1.037 +units=m +no_defs +23847 projected ID74 / UTM zone 47N 96,0,102,5.42 3 6238 +proj=utm +zone=47 +a=6378160 +rf=298.247 +towgs84=-1.977,-13.06,-9.993,0.364,0.254,0.689,-1.037 +units=m +no_defs +23848 projected ID74 / UTM zone 48N 102,0,108,4.11 3 6238 +proj=utm +zone=48 +a=6378160 +rf=298.247 +towgs84=-1.977,-13.06,-9.993,0.364,0.254,0.689,-1.037 +units=m +no_defs +23849 projected ID74 / UTM zone 49N 108,0,114,4.25 3 6238 +proj=utm +zone=49 +a=6378160 +rf=298.247 +towgs84=-1.977,-13.06,-9.993,0.364,0.254,0.689,-1.037 +units=m +no_defs +23850 projected ID74 / UTM zone 50N 114,0,120,4.37 3 6238 +proj=utm +zone=50 +a=6378160 +rf=298.247 +towgs84=-1.977,-13.06,-9.993,0.364,0.254,0.689,-1.037 +units=m +no_defs +23851 projected ID74 / UTM zone 51N 120,0,125.71,3.84 3 6238 +proj=utm +zone=51 +a=6378160 +rf=298.247 +towgs84=-1.977,-13.06,-9.993,0.364,0.254,0.689,-1.037 +units=m +no_defs +23852 projected ID74 / UTM zone 52N 126.55,0,131,4.59 3 6238 +proj=utm +zone=52 +a=6378160 +rf=298.247 +towgs84=-1.977,-13.06,-9.993,0.364,0.254,0.689,-1.037 +units=m +no_defs +23866 projected DGN95 / UTM zone 46N 92.01,0,96,7.79 1 6755 +proj=utm +zone=46 +ellps=WGS84 +towgs84=0,0,0 +units=m +no_defs +23867 projected DGN95 / UTM zone 47N 96,0,102,7.49 1 6755 +proj=utm +zone=47 +ellps=WGS84 +towgs84=0,0,0 +units=m +no_defs +23868 projected DGN95 / UTM zone 48N 102,0,108,6.94 1 6755 +proj=utm +zone=48 +ellps=WGS84 +towgs84=0,0,0 +units=m +no_defs +23869 projected DGN95 / UTM zone 49N 108,0,114,7.37 1 6755 +proj=utm +zone=49 +ellps=WGS84 +towgs84=0,0,0 +units=m +no_defs +23870 projected DGN95 / UTM zone 50N 114,0,120,4.37 1 6755 +proj=utm +zone=50 +ellps=WGS84 +towgs84=0,0,0 +units=m +no_defs +23871 projected DGN95 / UTM zone 51N 120,0,126,5.48 1 6755 +proj=utm +zone=51 +ellps=WGS84 +towgs84=0,0,0 +units=m +no_defs +23872 projected DGN95 / UTM zone 52N 126,0,132,6.68 1 6755 +proj=utm +zone=52 +ellps=WGS84 +towgs84=0,0,0 +units=m +no_defs +23877 projected DGN95 / UTM zone 47S 96,-8.86,102,0 1 6755 +proj=utm +zone=47 +south +ellps=WGS84 +towgs84=0,0,0 +units=m +no_defs +23878 projected DGN95 / UTM zone 48S 102,-10.73,108.01,0 1 6755 +proj=utm +zone=48 +south +ellps=WGS84 +towgs84=0,0,0 +units=m +no_defs +23879 projected DGN95 / UTM zone 49S 108,-12.07,114,0 1 6755 +proj=utm +zone=49 +south +ellps=WGS84 +towgs84=0,0,0 +units=m +no_defs +23880 projected DGN95 / UTM zone 50S 114,-13.06,120.01,0 1 6755 +proj=utm +zone=50 +south +ellps=WGS84 +towgs84=0,0,0 +units=m +no_defs +23881 projected DGN95 / UTM zone 51S 120,-13.95,126.01,0.01 1 6755 +proj=utm +zone=51 +south +ellps=WGS84 +towgs84=0,0,0 +units=m +no_defs +23882 projected DGN95 / UTM zone 52S 126,-9.45,132,0 1 6755 +proj=utm +zone=52 +south +ellps=WGS84 +towgs84=0,0,0 +units=m +no_defs +23883 projected DGN95 / UTM zone 53S 132,-10.06,138.01,0 1 6755 +proj=utm +zone=53 +south +ellps=WGS84 +towgs84=0,0,0 +units=m +no_defs +23884 projected DGN95 / UTM zone 54S 138,-10.84,141.46,0 1 6755 +proj=utm +zone=54 +south +ellps=WGS84 +towgs84=0,0,0 +units=m +no_defs +23887 projected ID74 / UTM zone 47S 98.24,-3.57,102,0 3 6238 +proj=utm +zone=47 +south +a=6378160 +rf=298.247 +towgs84=-1.977,-13.06,-9.993,0.364,0.254,0.689,-1.037 +units=m +no_defs +23888 projected ID74 / UTM zone 48S 102,-7.79,108,0 3 6238 +proj=utm +zone=48 +south +a=6378160 +rf=298.247 +towgs84=-1.977,-13.06,-9.993,0.364,0.254,0.689,-1.037 +units=m +no_defs +23889 projected ID74 / UTM zone 49S 108,-8.67,114,0 3 6238 +proj=utm +zone=49 +south +a=6378160 +rf=298.247 +towgs84=-1.977,-13.06,-9.993,0.364,0.254,0.689,-1.037 +units=m +no_defs +23890 projected ID74 / UTM zone 50S 114,-10.15,120,0 3 6238 +proj=utm +zone=50 +south +a=6378160 +rf=298.247 +towgs84=-1.977,-13.06,-9.993,0.364,0.254,0.689,-1.037 +units=m +no_defs +23891 projected ID74 / UTM zone 51S 120,-10.98,126,0 3 6238 +proj=utm +zone=51 +south +a=6378160 +rf=298.247 +towgs84=-1.977,-13.06,-9.993,0.364,0.254,0.689,-1.037 +units=m +no_defs +23892 projected ID74 / UTM zone 52S 126,-8.41,132,0 3 6238 +proj=utm +zone=52 +south +a=6378160 +rf=298.247 +towgs84=-1.977,-13.06,-9.993,0.364,0.254,0.689,-1.037 +units=m +no_defs +23893 projected ID74 / UTM zone 53S 132,-8.49,138,-0.29 3 6238 +proj=utm +zone=53 +south +a=6378160 +rf=298.247 +towgs84=-1.977,-13.06,-9.993,0.364,0.254,0.689,-1.037 +units=m +no_defs +23894 projected ID74 / UTM zone 54S 138,-9.19,141.01,-1.49 3 6238 +proj=utm +zone=54 +south +a=6378160 +rf=298.247 +towgs84=-1.977,-13.06,-9.993,0.364,0.254,0.689,-1.037 +units=m +no_defs +23946 projected Indian 1954 / UTM zone 46N 92.2,15.66,96.01,27.14 6239 +proj=utm +zone=46 +ellps=evrst30 +units=m +no_defs +23947 projected Indian 1954 / UTM zone 47N 95.99,5.63,102.01,28.55 21 6239 +proj=utm +zone=47 +ellps=evrst30 +towgs84=217,823,299 +units=m +no_defs +23948 projected Indian 1954 / UTM zone 48N 102,6.02,105.64,18.44 21 6239 +proj=utm +zone=48 +ellps=evrst30 +towgs84=217,823,299 +units=m +no_defs +24047 projected Indian 1975 / UTM zone 47N 97.34,5.63,102.01,20.46 3 6240 +proj=utm +zone=47 +ellps=evrst30 +towgs84=293,836,318,0.5,1.6,-2.8,2.1 +units=m +no_defs +24048 projected Indian 1975 / UTM zone 48N 102,6.02,105.64,18.44 3 6240 +proj=utm +zone=48 +ellps=evrst30 +towgs84=293,836,318,0.5,1.6,-2.8,2.1 +units=m +no_defs +24100 projected Jamaica 1875 / Jamaica (Old Grid) -78.43,17.64,-76.17,18.58 6241 +proj=lcc +lat_1=18 +lat_0=18 +lon_0=-77 +k_0=1 +x_0=167638.49597 +y_0=121918.90616 +a=6378249.14480801 +b=6356514.96620413 +to_meter=0.3047972654 +no_defs +24200 projected JAD69 / Jamaica National Grid -78.43,17.64,-76.17,18.58 1 6242 +proj=lcc +lat_1=18 +lat_0=18 +lon_0=-77 +k_0=1 +x_0=250000 +y_0=150000 +ellps=clrk66 +towgs84=-33.722,153.789,94.959,-8.581,-4.478,4.54,8.95 +units=m +no_defs +24305 projected Kalianpur 1937 / UTM zone 45N 88.01,21.59,90,26.64 18 6144 +proj=utm +zone=45 +ellps=evrst30 +towgs84=282,726,254 +units=m +no_defs +24306 projected Kalianpur 1937 / UTM zone 46N 90,20.52,92.67,25.29 18 6144 +proj=utm +zone=46 +ellps=evrst30 +towgs84=282,726,254 +units=m +no_defs +24311 projected Kalianpur 1962 / UTM zone 41N 60.86,24.98,66.01,29.87 1 6145 +proj=utm +zone=41 +a=6377301.243 +rf=300.8017255 +towgs84=275.57,676.78,229.6 +units=m +no_defs +24312 projected Kalianpur 1962 / UTM zone 42N 66,23.64,72.01,36.56 3 6145 +proj=utm +zone=42 +a=6377301.243 +rf=300.8017255 +towgs84=230.25,632.76,161.03,1.114,-1.115,-1.212,12.584 +units=m +no_defs +24313 projected Kalianpur 1962 / UTM zone 43N 72,28.21,77.83,37.07 6145 +proj=utm +zone=43 +a=6377301.243 +rf=300.8017255 +towgs84=283,682,231 +units=m +no_defs +24342 projected Kalianpur 1975 / UTM zone 42N 68.13,20.64,72.01,28.22 22 6146 +proj=utm +zone=42 +a=6377299.151 +rf=300.8017255 +towgs84=295,736,257 +units=m +no_defs +24343 projected Kalianpur 1975 / UTM zone 43N 72,8.02,78.01,35.51 22 6146 +proj=utm +zone=43 +a=6377299.151 +rf=300.8017255 +towgs84=295,736,257 +units=m +no_defs +24344 projected Kalianpur 1975 / UTM zone 44N 78,8.29,84.01,35.5 22 6146 +proj=utm +zone=44 +a=6377299.151 +rf=300.8017255 +towgs84=295,736,257 +units=m +no_defs +24345 projected Kalianpur 1975 / UTM zone 45N 84,18.18,90.01,28.14 22 6146 +proj=utm +zone=45 +a=6377299.151 +rf=300.8017255 +towgs84=295,736,257 +units=m +no_defs +24346 projected Kalianpur 1975 / UTM zone 46N 90,21.94,96.01,29.42 22 6146 +proj=utm +zone=46 +a=6377299.151 +rf=300.8017255 +towgs84=295,736,257 +units=m +no_defs +24347 projected Kalianpur 1975 / UTM zone 47N 96,27.1,97.42,29.47 22 6146 +proj=utm +zone=47 +a=6377299.151 +rf=300.8017255 +towgs84=295,736,257 +units=m +no_defs +24370 projected Kalianpur 1880 / India zone 0 71.18,35.58,77.01,37.07 6243 +proj=lcc +lat_1=39.5 +lat_0=39.5 +lon_0=68 +k_0=0.99846154 +x_0=2153865.73916853 +y_0=2368292.1946281 +a=6377299.36559538 +b=6356098.35900516 +to_meter=0.914398530744441 +no_defs +24371 projected Kalianpur 1880 / India zone I 60.86,28,81.64,35.59 6243 +proj=lcc +lat_1=32.5 +lat_0=32.5 +lon_0=68 +k_0=0.99878641 +x_0=2743195.59223332 +y_0=914398.530744441 +a=6377299.36559538 +b=6356098.35900516 +to_meter=0.914398530744441 +no_defs +24372 projected Kalianpur 1880 / India zone IIa 61.59,21,82.01,28.01 6243 +proj=lcc +lat_1=26 +lat_0=26 +lon_0=74 +k_0=0.99878641 +x_0=2743195.59223332 +y_0=914398.530744441 +a=6377299.36559538 +b=6356098.35900516 +to_meter=0.914398530744441 +no_defs +24373 projected Kalianpur 1880 / India zone IIIa 70.14,15,87.15,21.01 6243 +proj=lcc +lat_1=19 +lat_0=19 +lon_0=80 +k_0=0.99878641 +x_0=2743195.59223332 +y_0=914398.530744441 +a=6377299.36559538 +b=6356098.35900516 +to_meter=0.914398530744441 +no_defs +24374 projected Kalianpur 1880 / India zone IVa 73.94,8.02,80.4,15.01 6243 +proj=lcc +lat_1=12 +lat_0=12 +lon_0=80 +k_0=0.99878641 +x_0=2743195.59223332 +y_0=914398.530744441 +a=6377299.36559538 +b=6356098.35900516 +to_meter=0.914398530744441 +no_defs +24375 projected Kalianpur 1937 / India zone IIb 88.01,20.52,92.67,26.64 18 6144 +proj=lcc +lat_1=26 +lat_0=26 +lon_0=90 +k_0=0.99878641 +x_0=2743185.69 +y_0=914395.23 +ellps=evrst30 +towgs84=282,726,254 +units=m +no_defs +24376 projected Kalianpur 1962 / India zone I 60.86,28,77.83,35.59 3 6145 +proj=lcc +lat_1=32.5 +lat_0=32.5 +lon_0=68 +k_0=0.99878641 +x_0=2743196.4 +y_0=914398.8 +a=6377301.243 +rf=300.8017255 +towgs84=230.25,632.76,161.03,1.114,-1.115,-1.212,12.584 +units=m +no_defs +24377 projected Kalianpur 1962 / India zone IIa 61.59,23.64,71.91,28.01 3 6145 +proj=lcc +lat_1=26 +lat_0=26 +lon_0=74 +k_0=0.99878641 +x_0=2743196.4 +y_0=914398.8 +a=6377301.243 +rf=300.8017255 +towgs84=230.25,632.76,161.03,1.114,-1.115,-1.212,12.584 +units=m +no_defs +24378 projected Kalianpur 1975 / India zone I 70.35,28,81.64,35.51 22 6146 +proj=lcc +lat_1=32.5 +lat_0=32.5 +lon_0=68 +k_0=0.99878641 +x_0=2743195.5 +y_0=914398.5 +a=6377299.151 +rf=300.8017255 +towgs84=295,736,257 +units=m +no_defs +24379 projected Kalianpur 1975 / India zone IIa 68.13,21,82.01,28.01 22 6146 +proj=lcc +lat_1=26 +lat_0=26 +lon_0=74 +k_0=0.99878641 +x_0=2743195.5 +y_0=914398.5 +a=6377299.151 +rf=300.8017255 +towgs84=295,736,257 +units=m +no_defs +24380 projected Kalianpur 1975 / India zone IIb 82,21,97.42,29.47 22 6146 +proj=lcc +lat_1=26 +lat_0=26 +lon_0=90 +k_0=0.99878641 +x_0=2743195.5 +y_0=914398.5 +a=6377299.151 +rf=300.8017255 +towgs84=295,736,257 +units=m +no_defs +24381 projected Kalianpur 1975 / India zone IIIa 70.14,15,87.15,21.01 22 6146 +proj=lcc +lat_1=19 +lat_0=19 +lon_0=80 +k_0=0.99878641 +x_0=2743195.5 +y_0=914398.5 +a=6377299.151 +rf=300.8017255 +towgs84=295,736,257 +units=m +no_defs +24382 projected Kalianpur 1880 / India zone IIb 82,21,101.17,29.47 6243 +proj=lcc +lat_1=26 +lat_0=26 +lon_0=90 +k_0=0.99878641 +x_0=2743195.59223332 +y_0=914398.530744441 +a=6377299.36559538 +b=6356098.35900516 +to_meter=0.914398530744441 +no_defs +24383 projected Kalianpur 1975 / India zone IVa 73.94,8.02,80.4,15.01 22 6146 +proj=lcc +lat_1=12 +lat_0=12 +lon_0=80 +k_0=0.99878641 +x_0=2743195.5 +y_0=914398.5 +a=6377299.151 +rf=300.8017255 +towgs84=295,736,257 +units=m +no_defs +24500 projected Kertau 1968 / Singapore Grid 103.59,1.13,104.07,1.47 15 6245 +proj=cass +lat_0=1.28764666666667 +lon_0=103.853002222222 +x_0=30000 +y_0=30000 +ellps=evrst48 +towgs84=-11,851,5 +units=m +no_defs +24547 projected Kertau 1968 / UTM zone 47N 99.59,2.29,102.01,6.72 15 6245 +proj=utm +zone=47 +ellps=evrst48 +towgs84=-11,851,5 +units=m +no_defs +24548 projected Kertau 1968 / UTM zone 48N 102,1.21,105.82,7.81 15 6245 +proj=utm +zone=48 +ellps=evrst48 +towgs84=-11,851,5 +units=m +no_defs +24600 projected KOC Lambert 46.54,28.53,48.48,30.09 1 6246 +proj=lcc +lat_1=32.5 +lat_0=32.5 +lon_0=45 +k_0=0.9987864078 +x_0=1500000 +y_0=1166200 +a=6378249.145 +rf=293.465 +towgs84=-294.7,-200.1,525.5 +units=m +no_defs +24718 projected La Canoa / UTM zone 18N -73.38,7.02,-71.99,11.62 6247 +proj=utm +zone=18 +ellps=intl +units=m +no_defs +24719 projected La Canoa / UTM zone 19N -72,0.73,-66,12.25 2.5 6247 +proj=utm +zone=19 +ellps=intl +towgs84=-273.5,110.6,-357.9 +units=m +no_defs +24720 projected La Canoa / UTM zone 20N -66,0.64,-59.8,11.23 2.5 6247 +proj=utm +zone=20 +ellps=intl +towgs84=-273.5,110.6,-357.9 +units=m +no_defs +24817 projected PSAD56 / UTM zone 17N -80.18,0,-78,1.45 5 6248 +proj=utm +zone=17 +ellps=intl +towgs84=-60.31,245.935,31.008,12.324,3.755,-7.37,0.447 +units=m +no_defs +24818 projected PSAD56 / UTM zone 18N -78,0,-71.99,11.62 26 6248 +proj=utm +zone=18 +ellps=intl +towgs84=-282,169,-371 +units=m +no_defs +24819 projected PSAD56 / UTM zone 19N -72,0.73,-66,12.68 23 6248 +proj=utm +zone=19 +ellps=intl +towgs84=-295,173,-371 +units=m +no_defs +24820 projected PSAD56 / UTM zone 20N -66,0.64,-59.99,11.23 23 6248 +proj=utm +zone=20 +ellps=intl +towgs84=-295,173,-371 +units=m +no_defs +24821 projected PSAD56 / UTM zone 21N -60,1.18,-56.47,8.58 17 6248 +proj=utm +zone=21 +ellps=intl +towgs84=-298,159,-369 +units=m +no_defs +24877 projected PSAD56 / UTM zone 17S -81.41,-10.53,-78,0 42 6248 +proj=utm +zone=17 +south +ellps=intl +towgs84=-288,175,-376 +units=m +no_defs +24878 projected PSAD56 / UTM zone 18S -78,-43.5,-71.99,0 42 6248 +proj=utm +zone=18 +south +ellps=intl +towgs84=-288,175,-376 +units=m +no_defs +24879 projected PSAD56 / UTM zone 19S -72,-43.5,-66,-2.14 42 6248 +proj=utm +zone=19 +south +ellps=intl +towgs84=-288,175,-376 +units=m +no_defs +24880 projected PSAD56 / UTM zone 20S -66,-22.87,-60,-9.67 19 6248 +proj=utm +zone=20 +south +ellps=intl +towgs84=-270,188,-388 +units=m +no_defs +24881 projected PSAD56 / UTM zone 21S -60,-20.17,-57.52,-16.27 19 6248 +proj=utm +zone=21 +south +ellps=intl +towgs84=-270,188,-388 +units=m +no_defs +24882 projected PSAD56 / UTM zone 22S -51.64,-1.05,-48,5.6 10 6248 +proj=utm +zone=22 +south +ellps=intl +towgs84=-291.87,106.37,-364.52 +units=m +no_defs +24891 projected PSAD56 / Peru west zone -81.41,-8.32,-79,-3.38 16 6248 +proj=tmerc +lat_0=-6 +lon_0=-80.5 +k=0.99983008 +x_0=222000 +y_0=1426834.743 +ellps=intl +towgs84=-279,175,-379 +units=m +no_defs +24892 projected PSAD56 / Peru central zone -79,-16.57,-73,-0.03 16 6248 +proj=tmerc +lat_0=-9.5 +lon_0=-76 +k=0.99932994 +x_0=720000 +y_0=1039979.159 +ellps=intl +towgs84=-279,175,-379 +units=m +no_defs +24893 projected PSAD56 / Peru east zone -73,-18.39,-68.67,-2.14 42 6248 +proj=tmerc +lat_0=-9.5 +lon_0=-70.5 +k=0.99952992 +x_0=1324000 +y_0=1040084.558 +ellps=intl +towgs84=-288,175,-376 +units=m +no_defs +25000 projected Leigon / Ghana Metre Grid -3.79,1.4,2.1,11.16 5 6250 +proj=tmerc +lat_0=4.66666666666667 +lon_0=-1 +k=0.99975 +x_0=274319.51 +y_0=0 +a=6378249.145 +rf=293.465 +towgs84=-130,29,364 +units=m +no_defs +25231 projected Lome / UTM zone 31N -0.15,2.91,2.42,11.14 6252 +proj=utm +zone=31 +ellps=clrk80ign +units=m +no_defs +25391 projected Luzon 1911 / Philippines zone I 116.89,7.75,118,9.32 17 6253 +proj=tmerc +lat_0=0 +lon_0=117 +k=0.99995 +x_0=500000 +y_0=0 +ellps=clrk66 +towgs84=-133,-77,-51 +units=m +no_defs +25392 projected Luzon 1911 / Philippines zone II 118,8.82,120.07,11.58 17 6253 +proj=tmerc +lat_0=0 +lon_0=119 +k=0.99995 +x_0=500000 +y_0=0 +ellps=clrk66 +towgs84=-133,-77,-51 +units=m +no_defs +25393 projected Luzon 1911 / Philippines zone III 119.7,4.99,122.21,19.45 17 6253 +proj=tmerc +lat_0=0 +lon_0=121 +k=0.99995 +x_0=500000 +y_0=0 +ellps=clrk66 +towgs84=-133,-77,-51 +units=m +no_defs +25394 projected Luzon 1911 / Philippines zone IV 121.74,6.35,124.29,18.58 17 6253 +proj=tmerc +lat_0=0 +lon_0=123 +k=0.99995 +x_0=500000 +y_0=0 +ellps=clrk66 +towgs84=-133,-77,-51 +units=m +no_defs +25395 projected Luzon 1911 / Philippines zone V 123.73,5.5,126.65,14.15 44 6253 +proj=tmerc +lat_0=0 +lon_0=125 +k=0.99995 +x_0=500000 +y_0=0 +ellps=clrk66 +towgs84=-133,-79,-72 +units=m +no_defs +25828 projected ETRS89 / UTM zone 28N -16.1,34.93,-11.99,72.44 1 6258 +proj=utm +zone=28 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +25829 projected ETRS89 / UTM zone 29N -12,34.91,-6,74.13 1 6258 +proj=utm +zone=29 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +25830 projected ETRS89 / UTM zone 30N -6,35.26,0.01,80.49 1 6258 +proj=utm +zone=30 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +25831 projected ETRS89 / UTM zone 31N 0,37,6.01,82.45 1 6258 +proj=utm +zone=31 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +25832 projected ETRS89 / UTM zone 32N 6,36.53,12.01,84.01 1 6258 +proj=utm +zone=32 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +25833 projected ETRS89 / UTM zone 33N 12,34.79,18.01,84.01 1 6258 +proj=utm +zone=33 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +25834 projected ETRS89 / UTM zone 34N 18,34.76,24.01,84.01 1 6258 +proj=utm +zone=34 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +25835 projected ETRS89 / UTM zone 35N 24,41.24,30.01,84.01 1 6258 +proj=utm +zone=35 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +25836 projected ETRS89 / UTM zone 36N 30,42.56,36.01,84.01 1 6258 +proj=utm +zone=36 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +25837 projected ETRS89 / UTM zone 37N 36,72.99,38.01,79.07 1 6258 +proj=utm +zone=37 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +25884 projected ETRS89 / TM Baltic93 19.02,53.89,28.24,60 1 6258 +proj=tmerc +lat_0=0 +lon_0=24 +k=0.9996 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +25932 projected Malongo 1987 / UTM zone 32S 10.53,-6.04,12.37,-5.05 5 6259 +proj=utm +zone=32 +south +ellps=intl +towgs84=-254.1,-5.36,-100.29 +units=m +no_defs +26191 projected Merchich / Nord Maroc -9.85,31.49,-1.01,35.97 7 6261 +proj=lcc +lat_1=33.3 +lat_0=33.3 +lon_0=-5.4 +k_0=0.999625769 +x_0=500000 +y_0=300000 +ellps=clrk80ign +towgs84=31,146,47 +units=m +no_defs +26192 projected Merchich / Sud Maroc -13.24,27.66,-3.59,31.51 7 6261 +proj=lcc +lat_1=29.7 +lat_0=29.7 +lon_0=-5.4 +k_0=0.999615596 +x_0=500000 +y_0=300000 +ellps=clrk80ign +towgs84=31,146,47 +units=m +no_defs +26194 projected Merchich / Sahara Nord -15.42,24.29,-8.66,27.67 7 6261 +proj=lcc +lat_1=26.1 +lat_0=26.1 +lon_0=-5.4 +k_0=0.999616304 +x_0=1200000 +y_0=400000 +ellps=clrk80ign +towgs84=31,146,47 +units=m +no_defs +26195 projected Merchich / Sahara Sud -17.16,20.71,-12,24.31 6261 +proj=lcc +lat_1=22.5 +lat_0=22.5 +lon_0=-5.4 +k_0=0.999616437 +x_0=1500000 +y_0=400000 +ellps=clrk80ign +units=m +no_defs +26237 projected Massawa / UTM zone 37N 36.44,12.36,43.31,18.1 44 6262 +proj=utm +zone=37 +ellps=bessel +towgs84=639,405,60 +units=m +no_defs +26331 projected Minna / UTM zone 31N 2.66,1.92,6,6.14 7 6263 +proj=utm +zone=31 +a=6378249.145 +rf=293.465 +towgs84=-93.6,-83.7,113.8 +units=m +no_defs +26332 projected Minna / UTM zone 32N 6,2.61,7.82,3.68 7 6263 +proj=utm +zone=32 +a=6378249.145 +rf=293.465 +towgs84=-93.6,-83.7,113.8 +units=m +no_defs +26391 projected Minna / Nigeria West Belt 2.69,3.57,6.5,13.9 15 6263 +proj=tmerc +lat_0=4 +lon_0=4.5 +k=0.99975 +x_0=230738.26 +y_0=0 +a=6378249.145 +rf=293.465 +towgs84=-92,-93,122 +units=m +no_defs +26392 projected Minna / Nigeria Mid Belt 6.5,3.57,10.51,13.53 15 6263 +proj=tmerc +lat_0=4 +lon_0=8.5 +k=0.99975 +x_0=670553.98 +y_0=0 +a=6378249.145 +rf=293.465 +towgs84=-92,-93,122 +units=m +no_defs +26393 projected Minna / Nigeria East Belt 10.49,6.43,14.65,13.72 15 6263 +proj=tmerc +lat_0=4 +lon_0=12.5 +k=0.99975 +x_0=1110369.7 +y_0=0 +a=6378249.145 +rf=293.465 +towgs84=-92,-93,122 +units=m +no_defs +26632 projected M'poraloko / UTM zone 32N 9.25,0,12,2.32 0.5 6266 +proj=utm +zone=32 +ellps=clrk80ign +towgs84=-80.7,-132.5,41.1 +units=m +no_defs +26692 projected M'poraloko / UTM zone 32S 7.03,-6.37,12.01,2.32 0.5 6266 +proj=utm +zone=32 +south +ellps=clrk80ign +towgs84=-80.7,-132.5,41.1 +units=m +no_defs +26701 projected NAD27 / UTM zone 1N -180,47.88,-173.99,63.21 15 6267 +proj=utm +zone=1 +ellps=clrk66 +towgs84=-2,152,149 +units=m +no_defs +26702 projected NAD27 / UTM zone 2N -174,48.66,-167.99,73.05 15 6267 +proj=utm +zone=2 +ellps=clrk66 +towgs84=-2,152,149 +units=m +no_defs +26703 projected NAD27 / UTM zone 3N -168,49.52,-161.99,74.29 12 6267 +proj=utm +zone=3 +ellps=clrk66 +towgs84=-5,135,172 +units=m +no_defs +26704 projected NAD27 / UTM zone 4N -162,50.98,-155.99,74.71 12 6267 +proj=utm +zone=4 +ellps=clrk66 +towgs84=-5,135,172 +units=m +no_defs +26705 projected NAD27 / UTM zone 5N -156,52.15,-149.99,74.71 12 6267 +proj=utm +zone=5 +ellps=clrk66 +towgs84=-5,135,172 +units=m +no_defs +26706 projected NAD27 / UTM zone 6N -150,54.05,-143.99,74.13 12 6267 +proj=utm +zone=6 +ellps=clrk66 +towgs84=-5,135,172 +units=m +no_defs +26707 projected NAD27 / UTM zone 7N -144,53.47,-138,73.59 12 6267 +proj=utm +zone=7 +ellps=clrk66 +towgs84=-5,135,172 +units=m +no_defs +26708 projected NAD27 / UTM zone 8N -138,52.58,-132,73.04 20 6267 +proj=utm +zone=8 +ellps=clrk66 +towgs84=-10,158,187 +units=m +no_defs +26709 projected NAD27 / UTM zone 9N -132,49.18,-126,72.03 8 6267 +proj=utm +zone=9 +ellps=clrk66 +towgs84=4,159,188 +units=m +no_defs +26710 projected NAD27 / UTM zone 10N -126,34.4,-119.99,77.13 20 6267 +proj=utm +zone=10 +ellps=clrk66 +towgs84=-10,158,187 +units=m +no_defs +26711 projected NAD27 / UTM zone 11N -120,26.93,-114,78.13 20 6267 +proj=utm +zone=11 +ellps=clrk66 +towgs84=-10,158,187 +units=m +no_defs +26712 projected NAD27 / UTM zone 12N -114,18.66,-108,78.81 10 6267 +proj=utm +zone=12 +ellps=clrk66 +towgs84=-8,160,176 +units=m +no_defs +26713 projected NAD27 / UTM zone 13N -108,17.86,-102,79.42 10 6267 +proj=utm +zone=13 +ellps=clrk66 +towgs84=-8,160,176 +units=m +no_defs +26714 projected NAD27 / UTM zone 14N -102,15.59,-96,80.74 10 6267 +proj=utm +zone=14 +ellps=clrk66 +towgs84=-8,160,176 +units=m +no_defs +26715 projected NAD27 / UTM zone 15N -96,13.63,-90,81.96 20 6267 +proj=utm +zone=15 +ellps=clrk66 +towgs84=-10,158,187 +units=m +no_defs +26716 projected NAD27 / UTM zone 16N -90.01,9.27,-84,82.54 20 6267 +proj=utm +zone=16 +ellps=clrk66 +towgs84=-10,158,187 +units=m +no_defs +26717 projected NAD27 / UTM zone 17N -84,7.98,-78,83.03 20 6267 +proj=utm +zone=17 +ellps=clrk66 +towgs84=-10,158,187 +units=m +no_defs +26718 projected NAD27 / UTM zone 18N -78,18.83,-72,83.16 20 6267 +proj=utm +zone=18 +ellps=clrk66 +towgs84=-10,158,187 +units=m +no_defs +26719 projected NAD27 / UTM zone 19N -72,33.61,-66,83.17 20 6267 +proj=utm +zone=19 +ellps=clrk66 +towgs84=-10,158,187 +units=m +no_defs +26720 projected NAD27 / UTM zone 20N -66,38.21,-59.99,82.97 20 6267 +proj=utm +zone=20 +ellps=clrk66 +towgs84=-10,158,187 +units=m +no_defs +26721 projected NAD27 / UTM zone 21N -60,40.57,-54,68.93 20 6267 +proj=utm +zone=21 +ellps=clrk66 +towgs84=-10,158,187 +units=m +no_defs +26722 projected NAD27 / UTM zone 22N -54,39.5,-47.99,57.65 20 6267 +proj=utm +zone=22 +ellps=clrk66 +towgs84=-10,158,187 +units=m +no_defs +26729 projected NAD27 / Alabama East -86.79,30.99,-84.89,35 10 6267 +proj=tmerc +lat_0=30.5 +lon_0=-85.8333333333333 +k=0.99996 +x_0=152400.30480061 +y_0=0 +ellps=clrk66 +towgs84=-8,160,176 +to_meter=0.304800609601219 +no_defs +26730 projected NAD27 / Alabama West -88.48,30.14,-86.3,35.02 10 6267 +proj=tmerc +lat_0=30 +lon_0=-87.5 +k=0.999933333 +x_0=152400.30480061 +y_0=0 +ellps=clrk66 +towgs84=-8,160,176 +to_meter=0.304800609601219 +no_defs +26731 projected NAD27 / Alaska zone 1 -141,54.61,-129.99,60.35 12 6267 +proj=omerc +no_uoff +lat_0=57 +lonc=-133.666666666667 +alpha=323.130102361111 +gamma=323.130102361111 +k=0.9999 +x_0=5000000.001016 +y_0=-5000000.001016 +ellps=clrk66 +towgs84=-5,135,172 +to_meter=0.304800609601219 +no_defs +26732 projected NAD27 / Alaska zone 2 -144.01,59.72,-140.98,70.16 12 6267 +proj=tmerc +lat_0=54 +lon_0=-142 +k=0.9999 +x_0=152400.30480061 +y_0=0 +ellps=clrk66 +towgs84=-5,135,172 +to_meter=0.304800609601219 +no_defs +26733 projected NAD27 / Alaska zone 3 -148,59.72,-144,70.38 12 6267 +proj=tmerc +lat_0=54 +lon_0=-146 +k=0.9999 +x_0=152400.30480061 +y_0=0 +ellps=clrk66 +towgs84=-5,135,172 +to_meter=0.304800609601219 +no_defs +26734 projected NAD27 / Alaska zone 4 -152.01,59.11,-147.99,70.63 12 6267 +proj=tmerc +lat_0=54 +lon_0=-150 +k=0.9999 +x_0=152400.30480061 +y_0=0 +ellps=clrk66 +towgs84=-5,135,172 +to_meter=0.304800609601219 +no_defs +26735 projected NAD27 / Alaska zone 5 -156,55.72,-151.86,71.28 12 6267 +proj=tmerc +lat_0=54 +lon_0=-154 +k=0.9999 +x_0=152400.30480061 +y_0=0 +ellps=clrk66 +towgs84=-5,135,172 +to_meter=0.304800609601219 +no_defs +26736 projected NAD27 / Alaska zone 6 -160,54.89,-155.99,71.4 12 6267 +proj=tmerc +lat_0=54 +lon_0=-158 +k=0.9999 +x_0=152400.30480061 +y_0=0 +ellps=clrk66 +towgs84=-5,135,172 +to_meter=0.304800609601219 +no_defs +26737 projected NAD27 / Alaska zone 7 -164.01,54.32,-160,70.74 12 6267 +proj=tmerc +lat_0=54 +lon_0=-162 +k=0.9999 +x_0=213360.426720853 +y_0=0 +ellps=clrk66 +towgs84=-5,135,172 +to_meter=0.304800609601219 +no_defs +26738 projected NAD27 / Alaska zone 8 -168.26,54.34,-164,69.05 12 6267 +proj=tmerc +lat_0=54 +lon_0=-166 +k=0.9999 +x_0=152400.30480061 +y_0=0 +ellps=clrk66 +towgs84=-5,135,172 +to_meter=0.304800609601219 +no_defs +26739 projected NAD27 / Alaska zone 9 -173.16,56.49,-168,65.82 12 6267 +proj=tmerc +lat_0=54 +lon_0=-170 +k=0.9999 +x_0=182880.365760732 +y_0=0 +ellps=clrk66 +towgs84=-5,135,172 +to_meter=0.304800609601219 +no_defs +26740 projected NAD27 / Alaska zone 10 172.42,51.3,-164.84,54.34 15 6267 +proj=lcc +lat_0=51 +lon_0=-176 +lat_1=53.8333333333333 +lat_2=51.8333333333333 +x_0=914401.828803658 +y_0=0 +ellps=clrk66 +towgs84=-2,152,149 +to_meter=0.304800609601219 +no_defs +26741 projected NAD27 / California zone I -124.45,39.59,-119.99,42.01 7 6267 +proj=lcc +lat_0=39.3333333333333 +lon_0=-122 +lat_1=41.6666666666667 +lat_2=40 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +towgs84=-8,159,175 +to_meter=0.304800609601219 +no_defs +26742 projected NAD27 / California zone II -124.06,38.02,-119.54,40.16 7 6267 +proj=lcc +lat_0=37.6666666666667 +lon_0=-122 +lat_1=39.8333333333333 +lat_2=38.3333333333333 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +towgs84=-8,159,175 +to_meter=0.304800609601219 +no_defs +26743 projected NAD27 / California zone III -123.02,36.73,-117.83,38.71 7 6267 +proj=lcc +lat_0=36.5 +lon_0=-120.5 +lat_1=38.4333333333333 +lat_2=37.0666666666667 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +towgs84=-8,159,175 +to_meter=0.304800609601219 +no_defs +26744 projected NAD27 / California zone IV -122.01,35.78,-115.62,37.58 7 6267 +proj=lcc +lat_0=35.3333333333333 +lon_0=-119 +lat_1=37.25 +lat_2=36 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +towgs84=-8,159,175 +to_meter=0.304800609601219 +no_defs +26745 projected NAD27 / California zone V -121.43,32.76,-114.12,35.81 7 6267 +proj=lcc +lat_0=33.5 +lon_0=-118 +lat_1=35.4666666666667 +lat_2=34.0333333333333 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +towgs84=-8,159,175 +to_meter=0.304800609601219 +no_defs +26746 projected NAD27 / California zone VI -118.15,32.53,-114.42,34.08 7 6267 +proj=lcc +lat_0=32.1666666666667 +lon_0=-116.25 +lat_1=33.8833333333333 +lat_2=32.7833333333333 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +towgs84=-8,159,175 +to_meter=0.304800609601219 +no_defs +26748 projected NAD27 / Arizona East -111.71,31.33,-109.04,37.01 7 6267 +proj=tmerc +lat_0=31 +lon_0=-110.166666666667 +k=0.9999 +x_0=152400.30480061 +y_0=0 +ellps=clrk66 +towgs84=-8,159,175 +to_meter=0.304800609601219 +no_defs +26749 projected NAD27 / Arizona Central -113.35,31.33,-110.44,37.01 7 6267 +proj=tmerc +lat_0=31 +lon_0=-111.916666666667 +k=0.9999 +x_0=152400.30480061 +y_0=0 +ellps=clrk66 +towgs84=-8,159,175 +to_meter=0.304800609601219 +no_defs +26750 projected NAD27 / Arizona West -114.81,32.05,-112.52,37 7 6267 +proj=tmerc +lat_0=31 +lon_0=-113.75 +k=0.999933333 +x_0=152400.30480061 +y_0=0 +ellps=clrk66 +towgs84=-8,159,175 +to_meter=0.304800609601219 +no_defs +26751 projected NAD27 / Arkansas North -94.62,34.67,-89.64,36.5 7 6267 +proj=lcc +lat_0=34.3333333333333 +lon_0=-92 +lat_1=36.2333333333333 +lat_2=34.9333333333333 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +towgs84=-8,159,175 +to_meter=0.304800609601219 +no_defs +26752 projected NAD27 / Arkansas South -94.48,33.01,-90.4,35.1 7 6267 +proj=lcc +lat_0=32.6666666666667 +lon_0=-92 +lat_1=34.7666666666667 +lat_2=33.3 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +towgs84=-8,159,175 +to_meter=0.304800609601219 +no_defs +26753 projected NAD27 / Colorado North -109.06,39.56,-102.04,41.01 7 6267 +proj=lcc +lat_0=39.3333333333333 +lon_0=-105.5 +lat_1=39.7166666666667 +lat_2=40.7833333333333 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +towgs84=-8,159,175 +to_meter=0.304800609601219 +no_defs +26754 projected NAD27 / Colorado Central -109.06,38.14,-102.04,40.09 7 6267 +proj=lcc +lat_0=37.8333333333333 +lon_0=-105.5 +lat_1=39.75 +lat_2=38.45 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +towgs84=-8,159,175 +to_meter=0.304800609601219 +no_defs +26755 projected NAD27 / Colorado South -109.06,36.98,-102.04,38.68 7 6267 +proj=lcc +lat_0=36.6666666666667 +lon_0=-105.5 +lat_1=38.4333333333333 +lat_2=37.2333333333333 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +towgs84=-8,159,175 +to_meter=0.304800609601219 +no_defs +26756 projected NAD27 / Connecticut -73.73,40.98,-71.78,42.05 10 6267 +proj=lcc +lat_0=40.8333333333333 +lon_0=-72.75 +lat_1=41.8666666666667 +lat_2=41.2 +x_0=182880.365760732 +y_0=0 +ellps=clrk66 +towgs84=-8,160,176 +to_meter=0.304800609601219 +no_defs +26757 projected NAD27 / Delaware -75.8,38.44,-74.97,39.85 10 6267 +proj=tmerc +lat_0=38 +lon_0=-75.4166666666667 +k=0.999995 +x_0=152400.30480061 +y_0=0 +ellps=clrk66 +towgs84=-8,160,176 +to_meter=0.304800609601219 +no_defs +26758 projected NAD27 / Florida East -82.33,24.41,-79.97,30.83 10 6267 +proj=tmerc +lat_0=24.3333333333333 +lon_0=-81 +k=0.999941177 +x_0=152400.30480061 +y_0=0 +ellps=clrk66 +towgs84=-8,160,176 +to_meter=0.304800609601219 +no_defs +26759 projected NAD27 / Florida West -83.34,26.27,-81.13,29.6 10 6267 +proj=tmerc +lat_0=24.3333333333333 +lon_0=-82 +k=0.999941177 +x_0=152400.30480061 +y_0=0 +ellps=clrk66 +towgs84=-8,160,176 +to_meter=0.304800609601219 +no_defs +26760 projected NAD27 / Florida North -87.63,29.21,-82.04,31.01 10 6267 +proj=lcc +lat_0=29 +lon_0=-84.5 +lat_1=30.75 +lat_2=29.5833333333333 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +towgs84=-8,160,176 +to_meter=0.304800609601219 +no_defs +26766 projected NAD27 / Georgia East -83.47,30.36,-80.77,34.68 10 6267 +proj=tmerc +lat_0=30 +lon_0=-82.1666666666667 +k=0.9999 +x_0=152400.30480061 +y_0=0 +ellps=clrk66 +towgs84=-8,160,176 +to_meter=0.304800609601219 +no_defs +26767 projected NAD27 / Georgia West -85.61,30.62,-82.99,35.01 10 6267 +proj=tmerc +lat_0=30 +lon_0=-84.1666666666667 +k=0.9999 +x_0=152400.30480061 +y_0=0 +ellps=clrk66 +towgs84=-8,160,176 +to_meter=0.304800609601219 +no_defs +26768 projected NAD27 / Idaho East -113.24,41.99,-111.04,44.75 7 6267 +proj=tmerc +lat_0=41.6666666666667 +lon_0=-112.166666666667 +k=0.999947368 +x_0=152400.30480061 +y_0=0 +ellps=clrk66 +towgs84=-8,159,175 +to_meter=0.304800609601219 +no_defs +26769 projected NAD27 / Idaho Central -115.3,41.99,-112.67,45.7 7 6267 +proj=tmerc +lat_0=41.6666666666667 +lon_0=-114 +k=0.999947368 +x_0=152400.30480061 +y_0=0 +ellps=clrk66 +towgs84=-8,159,175 +to_meter=0.304800609601219 +no_defs +26770 projected NAD27 / Idaho West -117.24,41.99,-114.32,49.01 7 6267 +proj=tmerc +lat_0=41.6666666666667 +lon_0=-115.75 +k=0.999933333 +x_0=152400.30480061 +y_0=0 +ellps=clrk66 +towgs84=-8,159,175 +to_meter=0.304800609601219 +no_defs +26771 projected NAD27 / Illinois East -89.27,37.06,-87.02,42.5 10 6267 +proj=tmerc +lat_0=36.6666666666667 +lon_0=-88.3333333333333 +k=0.999975 +x_0=152400.30480061 +y_0=0 +ellps=clrk66 +towgs84=-8,160,176 +to_meter=0.304800609601219 +no_defs +26772 projected NAD27 / Illinois West -91.52,36.97,-88.92,42.51 10 6267 +proj=tmerc +lat_0=36.6666666666667 +lon_0=-90.1666666666667 +k=0.999941177 +x_0=152400.30480061 +y_0=0 +ellps=clrk66 +towgs84=-8,160,176 +to_meter=0.304800609601219 +no_defs +26773 projected NAD27 / Indiana East -86.59,37.95,-84.78,41.77 10 6267 +proj=tmerc +lat_0=37.5 +lon_0=-85.6666666666667 +k=0.999966667 +x_0=152400.30480061 +y_0=0 +ellps=clrk66 +towgs84=-8,160,176 +to_meter=0.304800609601219 +no_defs +26774 projected NAD27 / Indiana West -88.1,37.77,-86.24,41.77 10 6267 +proj=tmerc +lat_0=37.5 +lon_0=-87.0833333333333 +k=0.999966667 +x_0=152400.30480061 +y_0=0 +ellps=clrk66 +towgs84=-8,160,176 +to_meter=0.304800609601219 +no_defs +26775 projected NAD27 / Iowa North -96.65,41.85,-90.15,43.51 7 6267 +proj=lcc +lat_0=41.5 +lon_0=-93.5 +lat_1=43.2666666666667 +lat_2=42.0666666666667 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +towgs84=-8,159,175 +to_meter=0.304800609601219 +no_defs +26776 projected NAD27 / Iowa South -96.14,40.36,-90.14,42.04 7 6267 +proj=lcc +lat_0=40 +lon_0=-93.5 +lat_1=41.7833333333333 +lat_2=40.6166666666667 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +towgs84=-8,159,175 +to_meter=0.304800609601219 +no_defs +26777 projected NAD27 / Kansas North -102.06,38.52,-94.58,40.01 7 6267 +proj=lcc +lat_0=38.3333333333333 +lon_0=-98 +lat_1=39.7833333333333 +lat_2=38.7166666666667 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +towgs84=-8,159,175 +to_meter=0.304800609601219 +no_defs +26778 projected NAD27 / Kansas South -102.05,36.99,-94.6,38.88 7 6267 +proj=lcc +lat_0=36.6666666666667 +lon_0=-98.5 +lat_1=38.5666666666667 +lat_2=37.2666666666667 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +towgs84=-8,159,175 +to_meter=0.304800609601219 +no_defs +26779 projected NAD27 / Kentucky North -85.96,37.71,-82.47,39.15 10 6267 +proj=lcc +lat_0=37.5 +lon_0=-84.25 +lat_1=37.9666666666667 +lat_2=38.9666666666667 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +towgs84=-8,160,176 +to_meter=0.304800609601219 +no_defs +26780 projected NAD27 / Kentucky South -89.57,36.49,-81.95,38.17 10 6267 +proj=lcc +lat_0=36.3333333333333 +lon_0=-85.75 +lat_1=36.7333333333333 +lat_2=37.9333333333333 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +towgs84=-8,160,176 +to_meter=0.304800609601219 +no_defs +26781 projected NAD27 / Louisiana North -94.05,30.85,-90.86,33.03 7 6267 +proj=lcc +lat_0=30.6666666666667 +lon_0=-92.5 +lat_1=31.1666666666667 +lat_2=32.6666666666667 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +towgs84=-8,159,175 +to_meter=0.304800609601219 +no_defs +26782 projected NAD27 / Louisiana South -93.94,27.82,-87.76,31.07 10 6267 +proj=lcc +lat_0=28.6666666666667 +lon_0=-91.3333333333333 +lat_1=29.3 +lat_2=30.7 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +towgs84=-8,160,176 +to_meter=0.304800609601219 +no_defs +26783 projected NAD27 / Maine East -70.03,43.88,-66.91,47.47 9 6267 +proj=tmerc +lat_0=43.8333333333333 +lon_0=-68.5 +k=0.9999 +x_0=152400.30480061 +y_0=0 +ellps=clrk66 +towgs84=-22,160,190 +to_meter=0.304800609601219 +no_defs +26784 projected NAD27 / Maine West -71.09,43.04,-69.26,46.58 10 6267 +proj=tmerc +lat_0=42.8333333333333 +lon_0=-70.1666666666667 +k=0.999966667 +x_0=152400.30480061 +y_0=0 +ellps=clrk66 +towgs84=-8,160,176 +to_meter=0.304800609601219 +no_defs +26785 projected NAD27 / Maryland -79.49,37.97,-74.97,39.73 10 6267 +proj=lcc +lat_0=37.8333333333333 +lon_0=-77 +lat_1=38.3 +lat_2=39.45 +x_0=243840.487680975 +y_0=0 +ellps=clrk66 +towgs84=-8,160,176 +to_meter=0.304800609601219 +no_defs +26786 projected NAD27 / Massachusetts Mainland -73.5,41.46,-69.86,42.89 10 6267 +proj=lcc +lat_0=41 +lon_0=-71.5 +lat_1=41.7166666666667 +lat_2=42.6833333333333 +x_0=182880.365760732 +y_0=0 +ellps=clrk66 +towgs84=-8,160,176 +to_meter=0.304800609601219 +no_defs +26787 projected NAD27 / Massachusetts Island -70.91,41.19,-69.89,41.51 10 6267 +proj=lcc +lat_0=41 +lon_0=-70.5 +lat_1=41.2833333333333 +lat_2=41.4833333333333 +x_0=60960.1219202439 +y_0=0 +ellps=clrk66 +towgs84=-8,160,176 +to_meter=0.304800609601219 +no_defs +26791 projected NAD27 / Minnesota North -97.22,46.64,-89.49,49.38 10 6267 +proj=lcc +lat_0=46.5 +lon_0=-93.1 +lat_1=47.0333333333333 +lat_2=48.6333333333333 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +towgs84=-8,160,176 +to_meter=0.304800609601219 +no_defs +26792 projected NAD27 / Minnesota Central -96.86,45.28,-92.29,47.48 7 6267 +proj=lcc +lat_0=45 +lon_0=-94.25 +lat_1=45.6166666666667 +lat_2=47.05 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +towgs84=-8,159,175 +to_meter=0.304800609601219 +no_defs +26793 projected NAD27 / Minnesota South -96.85,43.49,-91.21,45.59 7 6267 +proj=lcc +lat_0=43 +lon_0=-94 +lat_1=43.7833333333333 +lat_2=45.2166666666667 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +towgs84=-8,159,175 +to_meter=0.304800609601219 +no_defs +26794 projected NAD27 / Mississippi East -89.97,30.01,-88.09,35.01 10 6267 +proj=tmerc +lat_0=29.6666666666667 +lon_0=-88.8333333333333 +k=0.99996 +x_0=152400.30480061 +y_0=0 +ellps=clrk66 +towgs84=-8,160,176 +to_meter=0.304800609601219 +no_defs +26795 projected NAD27 / Mississippi West -91.65,31,-89.37,35.01 10 6267 +proj=tmerc +lat_0=30.5 +lon_0=-90.3333333333333 +k=0.999941177 +x_0=152400.30480061 +y_0=0 +ellps=clrk66 +towgs84=-8,160,176 +to_meter=0.304800609601219 +no_defs +26796 projected NAD27 / Missouri East -91.97,35.98,-89.1,40.61 10 6267 +proj=tmerc +lat_0=35.8333333333333 +lon_0=-90.5 +k=0.999933333 +x_0=152400.30480061 +y_0=0 +ellps=clrk66 +towgs84=-8,160,176 +to_meter=0.304800609601219 +no_defs +26797 projected NAD27 / Missouri Central -93.79,36.48,-91.41,40.61 7 6267 +proj=tmerc +lat_0=35.8333333333333 +lon_0=-92.5 +k=0.999933333 +x_0=152400.30480061 +y_0=0 +ellps=clrk66 +towgs84=-8,159,175 +to_meter=0.304800609601219 +no_defs +26798 projected NAD27 / Missouri West -95.77,36.48,-93.48,40.59 7 6267 +proj=tmerc +lat_0=36.1666666666667 +lon_0=-94.5 +k=0.999941177 +x_0=152400.30480061 +y_0=0 +ellps=clrk66 +towgs84=-8,159,175 +to_meter=0.304800609601219 +no_defs +26799 projected NAD27 / California zone VII -118.96,33.66,-117.63,34.83 7 6267 +proj=lcc +lat_0=34.1333333333333 +lon_0=-118.333333333333 +lat_1=34.4166666666667 +lat_2=33.8666666666667 +x_0=1276106.4505969 +y_0=1268253.00685801 +ellps=clrk66 +towgs84=-8,159,175 +to_meter=0.304800609601219 +no_defs +26847 projected NAD83 / Maine East (ftUS) -70.03,43.88,-66.91,47.47 4 6269 +proj=tmerc +lat_0=43.6666666666667 +lon_0=-68.5 +k=0.9999 +x_0=300000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +26848 projected NAD83 / Maine West (ftUS) -71.09,43.04,-69.26,46.58 4 6269 +proj=tmerc +lat_0=42.8333333333333 +lon_0=-70.1666666666667 +k=0.999966667 +x_0=900000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +26849 projected NAD83 / Minnesota North (ftUS) -97.22,46.64,-89.49,49.38 4 6269 +proj=lcc +lat_0=46.5 +lon_0=-93.1 +lat_1=48.6333333333333 +lat_2=47.0333333333333 +x_0=800000.00001016 +y_0=99999.99998984 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +26850 projected NAD83 / Minnesota Central (ftUS) -96.86,45.28,-92.29,47.48 4 6269 +proj=lcc +lat_0=45 +lon_0=-94.25 +lat_1=47.05 +lat_2=45.6166666666667 +x_0=800000.00001016 +y_0=99999.99998984 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +26851 projected NAD83 / Minnesota South (ftUS) -96.85,43.49,-91.21,45.59 4 6269 +proj=lcc +lat_0=43 +lon_0=-94 +lat_1=45.2166666666667 +lat_2=43.7833333333333 +x_0=800000.00001016 +y_0=99999.99998984 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +26852 projected NAD83 / Nebraska (ftUS) -104.06,39.99,-95.3,43.01 4 6269 +proj=lcc +lat_0=39.8333333333333 +lon_0=-100 +lat_1=43 +lat_2=40 +x_0=500000.00001016 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +26853 projected NAD83 / West Virginia North (ftUS) -81.76,38.76,-77.72,40.64 4 6269 +proj=lcc +lat_0=38.5 +lon_0=-79.5 +lat_1=40.25 +lat_2=39 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +26854 projected NAD83 / West Virginia South (ftUS) -82.65,37.2,-79.05,39.17 4 6269 +proj=lcc +lat_0=37 +lon_0=-81 +lat_1=38.8833333333333 +lat_2=37.4833333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +26855 projected NAD83(HARN) / Maine East (ftUS) -70.03,43.88,-66.91,47.47 1 6152 +proj=tmerc +lat_0=43.6666666666667 +lon_0=-68.5 +k=0.9999 +x_0=300000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +26856 projected NAD83(HARN) / Maine West (ftUS) -71.09,43.04,-69.26,46.58 1 6152 +proj=tmerc +lat_0=42.8333333333333 +lon_0=-70.1666666666667 +k=0.999966667 +x_0=900000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +26857 projected NAD83(HARN) / Minnesota North (ftUS) -97.22,46.64,-89.49,49.38 1 6152 +proj=lcc +lat_0=46.5 +lon_0=-93.1 +lat_1=48.6333333333333 +lat_2=47.0333333333333 +x_0=800000.00001016 +y_0=99999.99998984 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +26858 projected NAD83(HARN) / Minnesota Central (ftUS) -96.86,45.28,-92.29,47.48 1 6152 +proj=lcc +lat_0=45 +lon_0=-94.25 +lat_1=47.05 +lat_2=45.6166666666667 +x_0=800000.00001016 +y_0=99999.99998984 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +26859 projected NAD83(HARN) / Minnesota South (ftUS) -96.85,43.49,-91.21,45.59 1 6152 +proj=lcc +lat_0=43 +lon_0=-94 +lat_1=45.2166666666667 +lat_2=43.7833333333333 +x_0=800000.00001016 +y_0=99999.99998984 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +26860 projected NAD83(HARN) / Nebraska (ftUS) -104.06,39.99,-95.3,43.01 1 6152 +proj=lcc +lat_0=39.8333333333333 +lon_0=-100 +lat_1=43 +lat_2=40 +x_0=500000.00001016 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +26861 projected NAD83(HARN) / West Virginia North (ftUS) -81.76,38.76,-77.72,40.64 1 6152 +proj=lcc +lat_0=38.5 +lon_0=-79.5 +lat_1=40.25 +lat_2=39 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +26862 projected NAD83(HARN) / West Virginia South (ftUS) -82.65,37.2,-79.05,39.17 1 6152 +proj=lcc +lat_0=37 +lon_0=-81 +lat_1=38.8833333333333 +lat_2=37.4833333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +to_meter=0.304800609601219 +no_defs +26863 projected NAD83(NSRS2007) / Maine East (ftUS) -70.03,43.88,-66.91,47.47 2 6759 +proj=tmerc +lat_0=43.6666666666667 +lon_0=-68.5 +k=0.9999 +x_0=300000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +26864 projected NAD83(NSRS2007) / Maine West (ftUS) -71.09,43.04,-69.26,46.58 2 6759 +proj=tmerc +lat_0=42.8333333333333 +lon_0=-70.1666666666667 +k=0.999966667 +x_0=900000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +26865 projected NAD83(NSRS2007) / Minnesota North (ftUS) -97.22,46.64,-89.49,49.38 2 6759 +proj=lcc +lat_0=46.5 +lon_0=-93.1 +lat_1=48.6333333333333 +lat_2=47.0333333333333 +x_0=800000.00001016 +y_0=99999.99998984 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +26866 projected NAD83(NSRS2007) / Minnesota Central (ftUS) -96.86,45.28,-92.29,47.48 2 6759 +proj=lcc +lat_0=45 +lon_0=-94.25 +lat_1=47.05 +lat_2=45.6166666666667 +x_0=800000.00001016 +y_0=99999.99998984 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +26867 projected NAD83(NSRS2007) / Minnesota South (ftUS) -96.85,43.49,-91.21,45.59 2 6759 +proj=lcc +lat_0=43 +lon_0=-94 +lat_1=45.2166666666667 +lat_2=43.7833333333333 +x_0=800000.00001016 +y_0=99999.99998984 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +26868 projected NAD83(NSRS2007) / Nebraska (ftUS) -104.06,39.99,-95.3,43.01 2 6759 +proj=lcc +lat_0=39.8333333333333 +lon_0=-100 +lat_1=43 +lat_2=40 +x_0=500000.00001016 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +26869 projected NAD83(NSRS2007) / West Virginia North (ftUS) -81.76,38.76,-77.72,40.64 2 6759 +proj=lcc +lat_0=38.5 +lon_0=-79.5 +lat_1=40.25 +lat_2=39 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +26870 projected NAD83(NSRS2007) / West Virginia South (ftUS) -82.65,37.2,-79.05,39.17 2 6759 +proj=lcc +lat_0=37 +lon_0=-81 +lat_1=38.8833333333333 +lat_2=37.4833333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +26891 projected NAD83(CSRS) / MTM zone 11 -83.6,41.67,-81,46 1 6140 +proj=tmerc +lat_0=0 +lon_0=-82.5 +k=0.9999 +x_0=304800 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +26892 projected NAD83(CSRS) / MTM zone 12 -82.5,46,-79.5,55.21 1 6140 +proj=tmerc +lat_0=0 +lon_0=-81 +k=0.9999 +x_0=304800 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +26893 projected NAD83(CSRS) / MTM zone 13 -85.5,46,-82.5,55.59 1 6140 +proj=tmerc +lat_0=0 +lon_0=-84 +k=0.9999 +x_0=304800 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +26894 projected NAD83(CSRS) / MTM zone 14 -88.5,47.17,-85.5,56.7 1 6140 +proj=tmerc +lat_0=0 +lon_0=-87 +k=0.9999 +x_0=304800 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +26895 projected NAD83(CSRS) / MTM zone 15 -91.5,47.97,-88.5,56.9 1 6140 +proj=tmerc +lat_0=0 +lon_0=-90 +k=0.9999 +x_0=304800 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +26896 projected NAD83(CSRS) / MTM zone 16 -94.5,48.06,-91.5,55.2 1 6140 +proj=tmerc +lat_0=0 +lon_0=-93 +k=0.9999 +x_0=304800 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +26897 projected NAD83(CSRS) / MTM zone 17 -95.16,48.69,-94.5,53.24 1 6140 +proj=tmerc +lat_0=0 +lon_0=-96 +k=0.9999 +x_0=304800 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +26898 projected NAD83(CSRS) / MTM zone 1 -54.5,46.56,-52.54,49.89 1 6140 +proj=tmerc +lat_0=0 +lon_0=-53 +k=0.9999 +x_0=304800 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +26899 projected NAD83(CSRS) / MTM zone 2 -57.5,46.81,-54.49,54.71 1 6140 +proj=tmerc +lat_0=0 +lon_0=-56 +k=0.9999 +x_0=304800 +y_0=0 +ellps=GRS80 +towgs84=-0.991,1.9072,0.5129,0.0257899075194932,0.0096500989602704,0.0116599432323421,0 +units=m +no_defs +26901 projected NAD83 / UTM zone 1N -180,47.88,-173.99,63.21 8 6269 +proj=utm +zone=1 +ellps=GRS80 +towgs84=-2,0,4 +units=m +no_defs +26902 projected NAD83 / UTM zone 2N -174,48.66,-167.99,73.05 4 6269 +proj=utm +zone=2 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26903 projected NAD83 / UTM zone 3N -168,49.52,-161.99,74.29 4 6269 +proj=utm +zone=3 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26904 projected NAD83 / UTM zone 4N -162,15.57,-155.99,74.71 4 6269 +proj=utm +zone=4 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26905 projected NAD83 / UTM zone 5N -156,15.56,-149.99,74.71 4 6269 +proj=utm +zone=5 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26906 projected NAD83 / UTM zone 6N -150,54.05,-143.99,74.13 4 6269 +proj=utm +zone=6 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26907 projected NAD83 / UTM zone 7N -144,52.05,-137.99,73.59 4 6269 +proj=utm +zone=7 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26908 projected NAD83 / UTM zone 8N -138,48.06,-132,79.42 4 6269 +proj=utm +zone=8 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26909 projected NAD83 / UTM zone 9N -132,35.38,-126,80.93 4 6269 +proj=utm +zone=9 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26910 projected NAD83 / UTM zone 10N -126,30.54,-119.99,81.8 4 6269 +proj=utm +zone=10 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26911 projected NAD83 / UTM zone 11N -120,30.88,-114,83.5 4 6269 +proj=utm +zone=11 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26912 projected NAD83 / UTM zone 12N -114,31.33,-108,84 4 6269 +proj=utm +zone=12 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26913 projected NAD83 / UTM zone 13N -108,28.98,-102,84 4 6269 +proj=utm +zone=13 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26914 projected NAD83 / UTM zone 14N -102,25.83,-96,84 4 6269 +proj=utm +zone=14 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26915 projected NAD83 / UTM zone 15N -96,25.61,-90,84 4 6269 +proj=utm +zone=15 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26916 projected NAD83 / UTM zone 16N -90,23.97,-84,84 4 6269 +proj=utm +zone=16 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26917 projected NAD83 / UTM zone 17N -84,23.81,-78,84 4 6269 +proj=utm +zone=17 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26918 projected NAD83 / UTM zone 18N -78,28.28,-72,84 4 6269 +proj=utm +zone=18 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26919 projected NAD83 / UTM zone 19N -72,14.92,-66,84 4 6269 +proj=utm +zone=19 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26920 projected NAD83 / UTM zone 20N -66,15.63,-60,84 4 6269 +proj=utm +zone=20 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26921 projected NAD83 / UTM zone 21N -60,38.56,-54,84 4 6269 +proj=utm +zone=21 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26922 projected NAD83 / UTM zone 22N -54,39.5,-47.99,57.65 4 6269 +proj=utm +zone=22 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26923 projected NAD83 / UTM zone 23N -48,39.85,-42,54.47 4 6269 +proj=utm +zone=23 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26929 projected NAD83 / Alabama East -86.79,30.99,-84.89,35 4 6269 +proj=tmerc +lat_0=30.5 +lon_0=-85.8333333333333 +k=0.99996 +x_0=200000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26930 projected NAD83 / Alabama West -88.48,30.14,-86.3,35.02 4 6269 +proj=tmerc +lat_0=30 +lon_0=-87.5 +k=0.999933333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26931 projected NAD83 / Alaska zone 1 -141,54.61,-129.99,60.35 4 6269 +proj=omerc +no_uoff +lat_0=57 +lonc=-133.666666666667 +alpha=323.130102361111 +gamma=323.130102361111 +k=0.9999 +x_0=5000000 +y_0=-5000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26932 projected NAD83 / Alaska zone 2 -144.01,59.72,-140.98,70.16 4 6269 +proj=tmerc +lat_0=54 +lon_0=-142 +k=0.9999 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26933 projected NAD83 / Alaska zone 3 -148,59.72,-144,70.38 4 6269 +proj=tmerc +lat_0=54 +lon_0=-146 +k=0.9999 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26934 projected NAD83 / Alaska zone 4 -152.01,59.11,-147.99,70.63 4 6269 +proj=tmerc +lat_0=54 +lon_0=-150 +k=0.9999 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26935 projected NAD83 / Alaska zone 5 -156,55.72,-151.86,71.28 4 6269 +proj=tmerc +lat_0=54 +lon_0=-154 +k=0.9999 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26936 projected NAD83 / Alaska zone 6 -160,54.89,-155.99,71.4 4 6269 +proj=tmerc +lat_0=54 +lon_0=-158 +k=0.9999 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26937 projected NAD83 / Alaska zone 7 -164.01,54.32,-160,70.74 4 6269 +proj=tmerc +lat_0=54 +lon_0=-162 +k=0.9999 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26938 projected NAD83 / Alaska zone 8 -168.26,54.34,-164,69.05 4 6269 +proj=tmerc +lat_0=54 +lon_0=-166 +k=0.9999 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26939 projected NAD83 / Alaska zone 9 -173.16,56.49,-168,65.82 4 6269 +proj=tmerc +lat_0=54 +lon_0=-170 +k=0.9999 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26940 projected NAD83 / Alaska zone 10 172.42,51.3,-164.84,54.34 8 6269 +proj=lcc +lat_0=51 +lon_0=-176 +lat_1=53.8333333333333 +lat_2=51.8333333333333 +x_0=1000000 +y_0=0 +ellps=GRS80 +towgs84=-2,0,4 +units=m +no_defs +26941 projected NAD83 / California zone 1 -124.45,39.59,-119.99,42.01 4 6269 +proj=lcc +lat_0=39.3333333333333 +lon_0=-122 +lat_1=41.6666666666667 +lat_2=40 +x_0=2000000 +y_0=500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26942 projected NAD83 / California zone 2 -124.06,38.02,-119.54,40.16 4 6269 +proj=lcc +lat_0=37.6666666666667 +lon_0=-122 +lat_1=39.8333333333333 +lat_2=38.3333333333333 +x_0=2000000 +y_0=500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26943 projected NAD83 / California zone 3 -123.02,36.73,-117.83,38.71 4 6269 +proj=lcc +lat_0=36.5 +lon_0=-120.5 +lat_1=38.4333333333333 +lat_2=37.0666666666667 +x_0=2000000 +y_0=500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26944 projected NAD83 / California zone 4 -122.01,35.78,-115.62,37.58 4 6269 +proj=lcc +lat_0=35.3333333333333 +lon_0=-119 +lat_1=37.25 +lat_2=36 +x_0=2000000 +y_0=500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26945 projected NAD83 / California zone 5 -121.42,32.76,-114.12,35.81 4 6269 +proj=lcc +lat_0=33.5 +lon_0=-118 +lat_1=35.4666666666667 +lat_2=34.0333333333333 +x_0=2000000 +y_0=500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26946 projected NAD83 / California zone 6 -118.15,32.53,-114.42,34.08 4 6269 +proj=lcc +lat_0=32.1666666666667 +lon_0=-116.25 +lat_1=33.8833333333333 +lat_2=32.7833333333333 +x_0=2000000 +y_0=500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26948 projected NAD83 / Arizona East -111.71,31.33,-109.04,37.01 4 6269 +proj=tmerc +lat_0=31 +lon_0=-110.166666666667 +k=0.9999 +x_0=213360 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26949 projected NAD83 / Arizona Central -113.35,31.33,-110.44,37.01 4 6269 +proj=tmerc +lat_0=31 +lon_0=-111.916666666667 +k=0.9999 +x_0=213360 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26950 projected NAD83 / Arizona West -114.81,32.05,-112.52,37 4 6269 +proj=tmerc +lat_0=31 +lon_0=-113.75 +k=0.999933333 +x_0=213360 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26951 projected NAD83 / Arkansas North -94.62,34.67,-89.64,36.5 4 6269 +proj=lcc +lat_0=34.3333333333333 +lon_0=-92 +lat_1=36.2333333333333 +lat_2=34.9333333333333 +x_0=400000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26952 projected NAD83 / Arkansas South -94.48,33.01,-90.4,35.1 4 6269 +proj=lcc +lat_0=32.6666666666667 +lon_0=-92 +lat_1=34.7666666666667 +lat_2=33.3 +x_0=400000 +y_0=400000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26953 projected NAD83 / Colorado North -109.06,39.56,-102.04,41.01 4 6269 +proj=lcc +lat_0=39.3333333333333 +lon_0=-105.5 +lat_1=40.7833333333333 +lat_2=39.7166666666667 +x_0=914401.8289 +y_0=304800.6096 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26954 projected NAD83 / Colorado Central -109.06,38.14,-102.04,40.09 4 6269 +proj=lcc +lat_0=37.8333333333333 +lon_0=-105.5 +lat_1=39.75 +lat_2=38.45 +x_0=914401.8289 +y_0=304800.6096 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26955 projected NAD83 / Colorado South -109.06,36.98,-102.04,38.68 4 6269 +proj=lcc +lat_0=36.6666666666667 +lon_0=-105.5 +lat_1=38.4333333333333 +lat_2=37.2333333333333 +x_0=914401.8289 +y_0=304800.6096 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26956 projected NAD83 / Connecticut -73.73,40.98,-71.78,42.05 4 6269 +proj=lcc +lat_0=40.8333333333333 +lon_0=-72.75 +lat_1=41.8666666666667 +lat_2=41.2 +x_0=304800.6096 +y_0=152400.3048 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26957 projected NAD83 / Delaware -75.8,38.44,-74.97,39.85 4 6269 +proj=tmerc +lat_0=38 +lon_0=-75.4166666666667 +k=0.999995 +x_0=200000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26958 projected NAD83 / Florida East -82.33,24.41,-79.97,30.83 4 6269 +proj=tmerc +lat_0=24.3333333333333 +lon_0=-81 +k=0.999941177 +x_0=200000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26959 projected NAD83 / Florida West -83.34,26.27,-81.13,29.6 4 6269 +proj=tmerc +lat_0=24.3333333333333 +lon_0=-82 +k=0.999941177 +x_0=200000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26960 projected NAD83 / Florida North -87.63,29.21,-82.04,31.01 4 6269 +proj=lcc +lat_0=29 +lon_0=-84.5 +lat_1=30.75 +lat_2=29.5833333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26961 projected NAD83 / Hawaii zone 1 -156.1,18.87,-154.74,20.33 4 6269 +proj=tmerc +lat_0=18.8333333333333 +lon_0=-155.5 +k=0.999966667 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=1,1,-1 +units=m +no_defs +26962 projected NAD83 / Hawaii zone 2 -157.36,20.45,-155.93,21.26 4 6269 +proj=tmerc +lat_0=20.3333333333333 +lon_0=-156.666666666667 +k=0.999966667 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=1,1,-1 +units=m +no_defs +26963 projected NAD83 / Hawaii zone 3 -158.33,21.2,-157.61,21.75 4 6269 +proj=tmerc +lat_0=21.1666666666667 +lon_0=-158 +k=0.99999 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=1,1,-1 +units=m +no_defs +26964 projected NAD83 / Hawaii zone 4 -159.85,21.81,-159.23,22.29 4 6269 +proj=tmerc +lat_0=21.8333333333333 +lon_0=-159.5 +k=0.99999 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=1,1,-1 +units=m +no_defs +26965 projected NAD83 / Hawaii zone 5 -160.3,21.73,-159.99,22.07 4 6269 +proj=tmerc +lat_0=21.6666666666667 +lon_0=-160.166666666667 +k=1 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=1,1,-1 +units=m +no_defs +26966 projected NAD83 / Georgia East -83.47,30.36,-80.77,34.68 4 6269 +proj=tmerc +lat_0=30 +lon_0=-82.1666666666667 +k=0.9999 +x_0=200000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26967 projected NAD83 / Georgia West -85.61,30.62,-82.99,35.01 4 6269 +proj=tmerc +lat_0=30 +lon_0=-84.1666666666667 +k=0.9999 +x_0=700000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26968 projected NAD83 / Idaho East -113.24,41.99,-111.04,44.75 4 6269 +proj=tmerc +lat_0=41.6666666666667 +lon_0=-112.166666666667 +k=0.999947368 +x_0=200000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26969 projected NAD83 / Idaho Central -115.3,41.99,-112.67,45.7 4 6269 +proj=tmerc +lat_0=41.6666666666667 +lon_0=-114 +k=0.999947368 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26970 projected NAD83 / Idaho West -117.24,41.99,-114.32,49.01 4 6269 +proj=tmerc +lat_0=41.6666666666667 +lon_0=-115.75 +k=0.999933333 +x_0=800000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26971 projected NAD83 / Illinois East -89.27,37.06,-87.02,42.5 4 6269 +proj=tmerc +lat_0=36.6666666666667 +lon_0=-88.3333333333333 +k=0.999975 +x_0=300000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26972 projected NAD83 / Illinois West -91.52,36.97,-88.92,42.51 4 6269 +proj=tmerc +lat_0=36.6666666666667 +lon_0=-90.1666666666667 +k=0.999941177 +x_0=700000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26973 projected NAD83 / Indiana East -86.59,37.95,-84.78,41.77 4 6269 +proj=tmerc +lat_0=37.5 +lon_0=-85.6666666666667 +k=0.999966667 +x_0=100000 +y_0=250000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26974 projected NAD83 / Indiana West -88.1,37.77,-86.24,41.77 4 6269 +proj=tmerc +lat_0=37.5 +lon_0=-87.0833333333333 +k=0.999966667 +x_0=900000 +y_0=250000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26975 projected NAD83 / Iowa North -96.65,41.85,-90.15,43.51 4 6269 +proj=lcc +lat_0=41.5 +lon_0=-93.5 +lat_1=43.2666666666667 +lat_2=42.0666666666667 +x_0=1500000 +y_0=1000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26976 projected NAD83 / Iowa South -96.14,40.36,-90.14,42.04 4 6269 +proj=lcc +lat_0=40 +lon_0=-93.5 +lat_1=41.7833333333333 +lat_2=40.6166666666667 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26977 projected NAD83 / Kansas North -102.06,38.52,-94.58,40.01 4 6269 +proj=lcc +lat_0=38.3333333333333 +lon_0=-98 +lat_1=39.7833333333333 +lat_2=38.7166666666667 +x_0=400000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26978 projected NAD83 / Kansas South -102.05,36.99,-94.6,38.88 4 6269 +proj=lcc +lat_0=36.6666666666667 +lon_0=-98.5 +lat_1=38.5666666666667 +lat_2=37.2666666666667 +x_0=400000 +y_0=400000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26980 projected NAD83 / Kentucky South -89.57,36.49,-81.95,38.17 4 6269 +proj=lcc +lat_0=36.3333333333333 +lon_0=-85.75 +lat_1=37.9333333333333 +lat_2=36.7333333333333 +x_0=500000 +y_0=500000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26981 projected NAD83 / Louisiana North -94.05,30.85,-90.86,33.03 4 6269 +proj=lcc +lat_0=30.5 +lon_0=-92.5 +lat_1=32.6666666666667 +lat_2=31.1666666666667 +x_0=1000000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26982 projected NAD83 / Louisiana South -93.94,28.85,-88.75,31.07 4 6269 +proj=lcc +lat_0=28.5 +lon_0=-91.3333333333333 +lat_1=30.7 +lat_2=29.3 +x_0=1000000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26983 projected NAD83 / Maine East -70.03,43.88,-66.91,47.47 4 6269 +proj=tmerc +lat_0=43.6666666666667 +lon_0=-68.5 +k=0.9999 +x_0=300000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26984 projected NAD83 / Maine West -71.09,43.04,-69.26,46.58 4 6269 +proj=tmerc +lat_0=42.8333333333333 +lon_0=-70.1666666666667 +k=0.999966667 +x_0=900000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26985 projected NAD83 / Maryland -79.49,37.97,-74.97,39.73 4 6269 +proj=lcc +lat_0=37.6666666666667 +lon_0=-77 +lat_1=39.45 +lat_2=38.3 +x_0=400000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26986 projected NAD83 / Massachusetts Mainland -73.5,41.46,-69.86,42.89 4 6269 +proj=lcc +lat_0=41 +lon_0=-71.5 +lat_1=42.6833333333333 +lat_2=41.7166666666667 +x_0=200000 +y_0=750000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26987 projected NAD83 / Massachusetts Island -70.91,41.19,-69.89,41.51 4 6269 +proj=lcc +lat_0=41 +lon_0=-70.5 +lat_1=41.4833333333333 +lat_2=41.2833333333333 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26988 projected NAD83 / Michigan North -90.42,45.08,-83.44,48.32 4 6269 +proj=lcc +lat_0=44.7833333333333 +lon_0=-87 +lat_1=47.0833333333333 +lat_2=45.4833333333333 +x_0=8000000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26989 projected NAD83 / Michigan Central -87.06,43.8,-82.27,45.92 4 6269 +proj=lcc +lat_0=43.3166666666667 +lon_0=-84.3666666666667 +lat_1=45.7 +lat_2=44.1833333333333 +x_0=6000000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26990 projected NAD83 / Michigan South -87.2,41.69,-82.13,44.22 4 6269 +proj=lcc +lat_0=41.5 +lon_0=-84.3666666666667 +lat_1=43.6666666666667 +lat_2=42.1 +x_0=4000000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26991 projected NAD83 / Minnesota North -97.22,46.64,-89.49,49.38 4 6269 +proj=lcc +lat_0=46.5 +lon_0=-93.1 +lat_1=48.6333333333333 +lat_2=47.0333333333333 +x_0=800000 +y_0=100000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26992 projected NAD83 / Minnesota Central -96.86,45.28,-92.29,47.48 4 6269 +proj=lcc +lat_0=45 +lon_0=-94.25 +lat_1=47.05 +lat_2=45.6166666666667 +x_0=800000 +y_0=100000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26993 projected NAD83 / Minnesota South -96.85,43.49,-91.21,45.59 4 6269 +proj=lcc +lat_0=43 +lon_0=-94 +lat_1=45.2166666666667 +lat_2=43.7833333333333 +x_0=800000 +y_0=100000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26994 projected NAD83 / Mississippi East -89.97,30.01,-88.09,35.01 4 6269 +proj=tmerc +lat_0=29.5 +lon_0=-88.8333333333333 +k=0.99995 +x_0=300000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26995 projected NAD83 / Mississippi West -91.65,31,-89.37,35.01 4 6269 +proj=tmerc +lat_0=29.5 +lon_0=-90.3333333333333 +k=0.99995 +x_0=700000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26996 projected NAD83 / Missouri East -91.97,35.98,-89.1,40.61 4 6269 +proj=tmerc +lat_0=35.8333333333333 +lon_0=-90.5 +k=0.999933333 +x_0=250000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26997 projected NAD83 / Missouri Central -93.79,36.48,-91.41,40.61 4 6269 +proj=tmerc +lat_0=35.8333333333333 +lon_0=-92.5 +k=0.999933333 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +26998 projected NAD83 / Missouri West -95.77,36.48,-93.48,40.59 4 6269 +proj=tmerc +lat_0=36.1666666666667 +lon_0=-94.5 +k=0.999941177 +x_0=850000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +27039 projected Nahrwan 1967 / UTM zone 39N 50.55,22.76,54.01,27.05 44 6270 +proj=utm +zone=39 +a=6378249.145 +rf=293.465 +towgs84=-249,-156,381 +units=m +no_defs +27040 projected Nahrwan 1967 / UTM zone 40N 54,22.63,57.13,26.27 44 6270 +proj=utm +zone=40 +a=6378249.145 +rf=293.465 +towgs84=-249,-156,381 +units=m +no_defs +27120 projected Naparima 1972 / UTM zone 20N -60.9,11.08,-60.44,11.41 26 6271 +proj=utm +zone=20 +ellps=intl +towgs84=-2,374,172 +units=m +no_defs +27200 projected NZGD49 / New Zealand Map Grid 166.37,-47.33,178.63,-34.1 4 6272 +proj=nzmg +lat_0=-41 +lon_0=173 +x_0=2510000 +y_0=6023150 +ellps=intl +towgs84=59.47,-5.04,187.44,0.47,-0.1,1.024,-4.5993 +units=m +no_defs +27205 projected NZGD49 / Mount Eden Circuit 171.99,-39.01,176.12,-34.1 4 6272 +proj=tmerc +lat_0=-36.8798652777778 +lon_0=174.764339361111 +k=0.9999 +x_0=300000 +y_0=700000 +ellps=intl +towgs84=59.47,-5.04,187.44,0.47,-0.1,1.024,-4.5993 +units=m +no_defs +27206 projected NZGD49 / Bay of Plenty Circuit 175.75,-39.13,177.23,-37.22 4 6272 +proj=tmerc +lat_0=-37.7612498055556 +lon_0=176.46619725 +k=1 +x_0=300000 +y_0=700000 +ellps=intl +towgs84=59.47,-5.04,187.44,0.47,-0.1,1.024,-4.5993 +units=m +no_defs +27207 projected NZGD49 / Poverty Bay Circuit 176.73,-39.04,178.63,-37.49 4 6272 +proj=tmerc +lat_0=-38.6247027777778 +lon_0=177.885636277778 +k=1 +x_0=300000 +y_0=700000 +ellps=intl +towgs84=59.47,-5.04,187.44,0.47,-0.1,1.024,-4.5993 +units=m +no_defs +27208 projected NZGD49 / Hawkes Bay Circuit 175.8,-40.57,178.07,-38.87 4 6272 +proj=tmerc +lat_0=-39.6509293055556 +lon_0=176.673680527778 +k=1 +x_0=300000 +y_0=700000 +ellps=intl +towgs84=59.47,-5.04,187.44,0.47,-0.1,1.024,-4.5993 +units=m +no_defs +27209 projected NZGD49 / Taranaki Circuit 173.68,-39.78,175.44,-38.4 4 6272 +proj=tmerc +lat_0=-39.1357583055556 +lon_0=174.22801175 +k=1 +x_0=300000 +y_0=700000 +ellps=intl +towgs84=59.47,-5.04,187.44,0.47,-0.1,1.024,-4.5993 +units=m +no_defs +27210 projected NZGD49 / Tuhirangi Circuit 174.88,-39.55,176.33,-38.87 4 6272 +proj=tmerc +lat_0=-39.5124703888889 +lon_0=175.640036805556 +k=1 +x_0=300000 +y_0=700000 +ellps=intl +towgs84=59.47,-5.04,187.44,0.47,-0.1,1.024,-4.5993 +units=m +no_defs +27211 projected NZGD49 / Wanganui Circuit 174.4,-40.97,176.27,-39.46 4 6272 +proj=tmerc +lat_0=-40.2419471388889 +lon_0=175.488099611111 +k=1 +x_0=300000 +y_0=700000 +ellps=intl +towgs84=59.47,-5.04,187.44,0.47,-0.1,1.024,-4.5993 +units=m +no_defs +27212 projected NZGD49 / Wairarapa Circuit 175.01,-41.67,176.55,-40.29 4 6272 +proj=tmerc +lat_0=-40.9255326388889 +lon_0=175.647349666667 +k=1 +x_0=300000 +y_0=700000 +ellps=intl +towgs84=59.47,-5.04,187.44,0.47,-0.1,1.024,-4.5993 +units=m +no_defs +27213 projected NZGD49 / Wellington Circuit 174.52,-41.5,175.36,-40.91 4 6272 +proj=tmerc +lat_0=-41.3013196388889 +lon_0=174.776623111111 +k=1 +x_0=300000 +y_0=700000 +ellps=intl +towgs84=59.47,-5.04,187.44,0.47,-0.1,1.024,-4.5993 +units=m +no_defs +27214 projected NZGD49 / Collingwood Circuit 172.16,-41.22,173.13,-40.44 4 6272 +proj=tmerc +lat_0=-40.7147590555556 +lon_0=172.6720465 +k=1 +x_0=300000 +y_0=700000 +ellps=intl +towgs84=59.47,-5.04,187.44,0.47,-0.1,1.024,-4.5993 +units=m +no_defs +27215 projected NZGD49 / Nelson Circuit 172.4,-42.18,174.08,-40.66 4 6272 +proj=tmerc +lat_0=-41.2745447222222 +lon_0=173.299316805556 +k=1 +x_0=300000 +y_0=700000 +ellps=intl +towgs84=59.47,-5.04,187.44,0.47,-0.1,1.024,-4.5993 +units=m +no_defs +27216 projected NZGD49 / Karamea Circuit 171.96,-41.49,172.7,-40.75 4 6272 +proj=tmerc +lat_0=-41.2899115277778 +lon_0=172.109028194444 +k=1 +x_0=300000 +y_0=700000 +ellps=intl +towgs84=59.47,-5.04,187.44,0.47,-0.1,1.024,-4.5993 +units=m +no_defs +27217 projected NZGD49 / Buller Circuit 171.27,-42.19,172.41,-41.42 4 6272 +proj=tmerc +lat_0=-41.8108028611111 +lon_0=171.581260055556 +k=1 +x_0=300000 +y_0=700000 +ellps=intl +towgs84=59.47,-5.04,187.44,0.47,-0.1,1.024,-4.5993 +units=m +no_defs +27218 projected NZGD49 / Grey Circuit 171.15,-42.74,172.75,-41.5 4 6272 +proj=tmerc +lat_0=-42.3336942777778 +lon_0=171.549771305556 +k=1 +x_0=300000 +y_0=700000 +ellps=intl +towgs84=59.47,-5.04,187.44,0.47,-0.1,1.024,-4.5993 +units=m +no_defs +27219 projected NZGD49 / Amuri Circuit 171.88,-42.95,173.55,-42.09 4 6272 +proj=tmerc +lat_0=-42.6891165833333 +lon_0=173.010133388889 +k=1 +x_0=300000 +y_0=700000 +ellps=intl +towgs84=59.47,-5.04,187.44,0.47,-0.1,1.024,-4.5993 +units=m +no_defs +27220 projected NZGD49 / Marlborough Circuit 172.95,-42.65,174.46,-40.85 4 6272 +proj=tmerc +lat_0=-41.5444866666667 +lon_0=173.802074111111 +k=1 +x_0=300000 +y_0=700000 +ellps=intl +towgs84=59.47,-5.04,187.44,0.47,-0.1,1.024,-4.5993 +units=m +no_defs +27221 projected NZGD49 / Hokitika Circuit 170.39,-43.23,171.89,-42.41 4 6272 +proj=tmerc +lat_0=-42.8863223611111 +lon_0=170.9799935 +k=1 +x_0=300000 +y_0=700000 +ellps=intl +towgs84=59.47,-5.04,187.44,0.47,-0.1,1.024,-4.5993 +units=m +no_defs +27222 projected NZGD49 / Okarito Circuit 169.21,-43.85,170.89,-43 4 6272 +proj=tmerc +lat_0=-43.1101281388889 +lon_0=170.260925833333 +k=1 +x_0=300000 +y_0=700000 +ellps=intl +towgs84=59.47,-5.04,187.44,0.47,-0.1,1.024,-4.5993 +units=m +no_defs +27223 projected NZGD49 / Jacksons Bay Circuit 168.02,-44.4,170.01,-43.67 4 6272 +proj=tmerc +lat_0=-43.9778028888889 +lon_0=168.606267 +k=1 +x_0=300000 +y_0=700000 +ellps=intl +towgs84=59.47,-5.04,187.44,0.47,-0.1,1.024,-4.5993 +units=m +no_defs +27224 projected NZGD49 / Mount Pleasant Circuit 171.11,-43.96,173.38,-42.69 4 6272 +proj=tmerc +lat_0=-43.5906375833333 +lon_0=172.727193583333 +k=1 +x_0=300000 +y_0=700000 +ellps=intl +towgs84=59.47,-5.04,187.44,0.47,-0.1,1.024,-4.5993 +units=m +no_defs +27225 projected NZGD49 / Gawler Circuit 170.68,-44.25,172.26,-43.13 4 6272 +proj=tmerc +lat_0=-43.7487115555556 +lon_0=171.360748472222 +k=1 +x_0=300000 +y_0=700000 +ellps=intl +towgs84=59.47,-5.04,187.44,0.47,-0.1,1.024,-4.5993 +units=m +no_defs +27226 projected NZGD49 / Timaru Circuit 169.82,-44.98,171.55,-43.35 4 6272 +proj=tmerc +lat_0=-44.4022203611111 +lon_0=171.057250833333 +k=1 +x_0=300000 +y_0=700000 +ellps=intl +towgs84=59.47,-5.04,187.44,0.47,-0.1,1.024,-4.5993 +units=m +no_defs +27227 projected NZGD49 / Lindis Peak Circuit 168.62,-45.4,170.24,-43.71 4 6272 +proj=tmerc +lat_0=-44.7352679722222 +lon_0=169.467755083333 +k=1 +x_0=300000 +y_0=700000 +ellps=intl +towgs84=59.47,-5.04,187.44,0.47,-0.1,1.024,-4.5993 +units=m +no_defs +27228 projected NZGD49 / Mount Nicholas Circuit 167.72,-45.58,169.11,-44.29 4 6272 +proj=tmerc +lat_0=-45.1329025833333 +lon_0=168.398641194444 +k=1 +x_0=300000 +y_0=700000 +ellps=intl +towgs84=59.47,-5.04,187.44,0.47,-0.1,1.024,-4.5993 +units=m +no_defs +27229 projected NZGD49 / Mount York Circuit 166.37,-46.33,168.21,-44.53 4 6272 +proj=tmerc +lat_0=-45.5637261666667 +lon_0=167.738861777778 +k=1 +x_0=300000 +y_0=700000 +ellps=intl +towgs84=59.47,-5.04,187.44,0.47,-0.1,1.024,-4.5993 +units=m +no_defs +27230 projected NZGD49 / Observation Point Circuit 169.77,-45.82,171.24,-44.61 4 6272 +proj=tmerc +lat_0=-45.8161966111111 +lon_0=170.628595166667 +k=1 +x_0=300000 +y_0=700000 +ellps=intl +towgs84=59.47,-5.04,187.44,0.47,-0.1,1.024,-4.5993 +units=m +no_defs +27231 projected NZGD49 / North Taieri Circuit 168.64,-46.73,170.87,-45.23 4 6272 +proj=tmerc +lat_0=-45.8615133611111 +lon_0=170.282589111111 +k=0.99996 +x_0=300000 +y_0=700000 +ellps=intl +towgs84=59.47,-5.04,187.44,0.47,-0.1,1.024,-4.5993 +units=m +no_defs +27232 projected NZGD49 / Bluff Circuit 167.29,-47.33,168.97,-45.33 4 6272 +proj=tmerc +lat_0=-46.6000096111111 +lon_0=168.342872 +k=1 +x_0=300002.66 +y_0=699999.58 +ellps=intl +towgs84=59.47,-5.04,187.44,0.47,-0.1,1.024,-4.5993 +units=m +no_defs +27258 projected NZGD49 / UTM zone 58S 165.87,-47.65,168,-42.59 4 6272 +proj=utm +zone=58 +south +ellps=intl +towgs84=59.47,-5.04,187.44,0.47,-0.1,1.024,-4.5993 +units=m +no_defs +27259 projected NZGD49 / UTM zone 59S 168,-47.64,174,-33.89 4 6272 +proj=utm +zone=59 +south +ellps=intl +towgs84=59.47,-5.04,187.44,0.47,-0.1,1.024,-4.5993 +units=m +no_defs +27260 projected NZGD49 / UTM zone 60S 174,-44.13,179.27,-34.24 4 6272 +proj=utm +zone=60 +south +ellps=intl +towgs84=59.47,-5.04,187.44,0.47,-0.1,1.024,-4.5993 +units=m +no_defs +27291 projected NZGD49 / North Island Grid 171.99,-41.67,178.63,-34.1 4 6272 +proj=tmerc +lat_0=-39 +lon_0=175.5 +k=1 +x_0=274319.524384809 +y_0=365759.365846411 +ellps=intl +towgs84=59.47,-5.04,187.44,0.47,-0.1,1.024,-4.5993 +to_meter=0.914398414616029 +no_defs +27292 projected NZGD49 / South Island Grid 166.37,-47.33,174.46,-40.44 4 6272 +proj=tmerc +lat_0=-44 +lon_0=171.5 +k=1 +x_0=457199.207308014 +y_0=457199.207308014 +ellps=intl +towgs84=59.47,-5.04,187.44,0.47,-0.1,1.024,-4.5993 +to_meter=0.914398414616029 +no_defs +27391 projected NGO 1948 (Oslo) / NGO zone I 4.39,57.92,7.23,63.17 6817 +proj=tmerc +lat_0=58 +lon_0=-4.66666666666667 +k=1 +x_0=0 +y_0=0 +a=6377492.018 +rf=299.1528128 +pm=10.7229166666667 +units=m +no_defs +27392 projected NGO 1948 (Oslo) / NGO zone II 7.22,57.9,9.56,64.23 6817 +proj=tmerc +lat_0=58 +lon_0=-2.33333333333333 +k=1 +x_0=0 +y_0=0 +a=6377492.018 +rf=299.1528128 +pm=10.7229166666667 +units=m +no_defs +27393 projected NGO 1948 (Oslo) / NGO zone III 9.55,58.78,11.98,67.58 6817 +proj=tmerc +lat_0=58 +lon_0=0 +k=1 +x_0=0 +y_0=0 +a=6377492.018 +rf=299.1528128 +pm=10.7229166666667 +units=m +no_defs +27394 projected NGO 1948 (Oslo) / NGO zone IV 11.97,59.88,15.06,69.1 6817 +proj=tmerc +lat_0=58 +lon_0=2.5 +k=1 +x_0=0 +y_0=0 +a=6377492.018 +rf=299.1528128 +pm=10.7229166666667 +units=m +no_defs +27395 projected NGO 1948 (Oslo) / NGO zone V 15.05,66.15,18.89,70.27 6817 +proj=tmerc +lat_0=58 +lon_0=6.16666666666667 +k=1 +x_0=0 +y_0=0 +a=6377492.018 +rf=299.1528128 +pm=10.7229166666667 +units=m +no_defs +27396 projected NGO 1948 (Oslo) / NGO zone VI 18.88,68.33,22.89,70.93 6817 +proj=tmerc +lat_0=58 +lon_0=10.1666666666667 +k=1 +x_0=0 +y_0=0 +a=6377492.018 +rf=299.1528128 +pm=10.7229166666667 +units=m +no_defs +27397 projected NGO 1948 (Oslo) / NGO zone VII 22.88,68.58,26.98,71.24 6817 +proj=tmerc +lat_0=58 +lon_0=14.1666666666667 +k=1 +x_0=0 +y_0=0 +a=6377492.018 +rf=299.1528128 +pm=10.7229166666667 +units=m +no_defs +27398 projected NGO 1948 (Oslo) / NGO zone VIII 26.97,69.02,31.32,71.19 6817 +proj=tmerc +lat_0=58 +lon_0=18.3333333333333 +k=1 +x_0=0 +y_0=0 +a=6377492.018 +rf=299.1528128 +pm=10.7229166666667 +units=m +no_defs +27429 projected Datum 73 / UTM zone 29N -9.56,36.95,-6.19,42.16 1 6274 +proj=utm +zone=29 +ellps=intl +towgs84=-239.749,88.181,30.488,0.263,0.082,1.211,2.229 +units=m +no_defs +27493 projected Datum 73 / Modified Portuguese Grid -9.56,36.95,-6.19,42.16 1 6274 +proj=tmerc +lat_0=39.6666666666667 +lon_0=-8.13190611111111 +k=1 +x_0=180.598 +y_0=-86.99 +ellps=intl +towgs84=-239.749,88.181,30.488,0.263,0.082,1.211,2.229 +units=m +no_defs +27500 projected ATF (Paris) / Nord de Guerre 6.84,47.42,8.23,49.07 6901 +proj=lcc +lat_1=49.5 +lat_0=49.5 +lon_0=5.4 +k_0=0.99950908 +x_0=500000 +y_0=300000 +a=6376523 +rf=308.64 +pm=2.33720833333333 +units=m +no_defs +27561 projected NTF (Paris) / Lambert Nord France -4.87,48.14,8.23,51.14 6807 +proj=lcc +lat_1=49.5 +lat_0=49.5 +lon_0=0 +k_0=0.999877341 +x_0=600000 +y_0=200000 +ellps=clrk80ign +pm=2.33722916999999 +units=m +no_defs +27562 projected NTF (Paris) / Lambert Centre France -4.8,45.44,7.63,48.15 6807 +proj=lcc +lat_1=46.8 +lat_0=46.8 +lon_0=0 +k_0=0.99987742 +x_0=600000 +y_0=200000 +ellps=clrk80ign +pm=2.33722916999999 +units=m +no_defs +27563 projected NTF (Paris) / Lambert Sud France -1.79,42.33,7.71,45.46 6807 +proj=lcc +lat_1=44.1 +lat_0=44.1 +lon_0=0 +k_0=0.999877499 +x_0=600000 +y_0=200000 +ellps=clrk80ign +pm=2.33722916999999 +units=m +no_defs +27564 projected NTF (Paris) / Lambert Corse 8.5,41.31,9.63,43.07 6807 +proj=lcc +lat_1=42.165 +lat_0=42.165 +lon_0=0 +k_0=0.99994471 +x_0=234.358 +y_0=185861.369 +ellps=clrk80ign +pm=2.33722916999999 +units=m +no_defs +27571 projected NTF (Paris) / Lambert zone I -4.87,48.14,8.23,51.14 6807 +proj=lcc +lat_1=49.5 +lat_0=49.5 +lon_0=0 +k_0=0.999877341 +x_0=600000 +y_0=1200000 +ellps=clrk80ign +pm=2.33722916999999 +units=m +no_defs +27572 projected NTF (Paris) / Lambert zone II -4.87,42.33,8.23,51.14 6807 +proj=lcc +lat_1=46.8 +lat_0=46.8 +lon_0=0 +k_0=0.99987742 +x_0=600000 +y_0=2200000 +ellps=clrk80ign +pm=2.33722916999999 +units=m +no_defs +27573 projected NTF (Paris) / Lambert zone III -1.79,42.33,7.71,45.46 6807 +proj=lcc +lat_1=44.1 +lat_0=44.1 +lon_0=0 +k_0=0.999877499 +x_0=600000 +y_0=3200000 +ellps=clrk80ign +pm=2.33722916999999 +units=m +no_defs +27574 projected NTF (Paris) / Lambert zone IV 8.5,41.31,9.63,43.07 6807 +proj=lcc +lat_1=42.165 +lat_0=42.165 +lon_0=0 +k_0=0.99994471 +x_0=234.358 +y_0=4185861.369 +ellps=clrk80ign +pm=2.33722916999999 +units=m +no_defs +27700 projected OSGB36 / British National Grid -9.01,49.75,2.01,61.01 2 6277 +proj=tmerc +lat_0=49 +lon_0=-2 +k=0.9996012717 +x_0=400000 +y_0=-100000 +ellps=airy +towgs84=446.448,-125.157,542.06,0.15,0.247,0.842,-20.489 +units=m +no_defs +27701 projected WGS 84 / Equi7 Africa -31.68,-43.89,79.3,38.8 6326 +proj=aeqd +lat_0=8.5 +lon_0=21.5 +x_0=5621452.02 +y_0=5990638.423 +datum=WGS84 +units=m +no_defs +27702 projected WGS 84 / Equi7 Antarctica -180,-90,180,-34.47 6326 +proj=aeqd +lat_0=-90 +lon_0=0 +x_0=3714266.977 +y_0=3402016.506 +datum=WGS84 +units=m +no_defs +27703 projected WGS 84 / Equi7 Asia 31.45,-10.8,-168.43,83.67 6326 +proj=aeqd +lat_0=47 +lon_0=94 +x_0=4340913.848 +y_0=4812712.923 +datum=WGS84 +units=m +no_defs +27704 projected WGS 84 / Equi7 Europe -42.52,29.24,51.73,83.67 6326 +proj=aeqd +lat_0=53 +lon_0=24 +x_0=5837287.82 +y_0=2121415.696 +datum=WGS84 +units=m +no_defs +27705 projected WGS 84 / Equi7 North America -180,7.98,180,90 6326 +proj=aeqd +lat_0=52 +lon_0=-97.5 +x_0=8264722.177 +y_0=4867518.353 +datum=WGS84 +units=m +no_defs +27706 projected WGS 84 / Equi7 Oceania 72.37,-60.56,-121.05,21.57 6326 +proj=aeqd +lat_0=-19.5 +lon_0=131.5 +x_0=6988408.536 +y_0=7654884.537 +datum=WGS84 +units=m +no_defs +27707 projected WGS 84 / Equi7 South America -124.82,-59.87,-14.58,30.31 6326 +proj=aeqd +lat_0=-14 +lon_0=-60.5 +x_0=7257179.236 +y_0=5592024.446 +datum=WGS84 +units=m +no_defs +28191 projected Palestine 1923 / Palestine Grid 34.17,29.18,39.31,33.38 2 6281 +proj=cass +lat_0=31.7340969444444 +lon_0=35.2120805555556 +x_0=170251.555 +y_0=126867.909 +a=6378300.789 +b=6356566.435 +towgs84=-275.7224,94.7824,340.8944,-8.001,-4.42,-11.821,1 +units=m +no_defs +28192 projected Palestine 1923 / Palestine Belt 34.17,29.18,39.31,33.38 2 6281 +proj=tmerc +lat_0=31.7340969444444 +lon_0=35.2120805555556 +k=1 +x_0=170251.555 +y_0=1126867.909 +a=6378300.789 +b=6356566.435 +towgs84=-275.7224,94.7824,340.8944,-8.001,-4.42,-11.821,1 +units=m +no_defs +28193 projected Palestine 1923 / Israeli CS Grid 34.17,29.45,35.69,33.28 2 6281 +proj=cass +lat_0=31.7340969444444 +lon_0=35.2120805555556 +x_0=170251.555 +y_0=1126867.909 +a=6378300.789 +b=6356566.435 +towgs84=-275.7224,94.7824,340.8944,-8.001,-4.42,-11.821,1 +units=m +no_defs +28232 projected Pointe Noire / UTM zone 32S 8.84,-6.91,18.65,3.72 44 6282 +proj=utm +zone=32 +south +ellps=clrk80ign +towgs84=-148,51,-291 +units=m +no_defs +28348 projected GDA94 / MGA zone 48 102.14,-34.68,108.01,-8.87 3 6283 +proj=utm +zone=48 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +28349 projected GDA94 / MGA zone 49 108,-37.84,114.01,-10.72 3 6283 +proj=utm +zone=49 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +28350 projected GDA94 / MGA zone 50 114,-38.53,120.01,-12.06 3 6283 +proj=utm +zone=50 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +28351 projected GDA94 / MGA zone 51 120,-38.07,126.01,-10.46 3 6283 +proj=utm +zone=51 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +28352 projected GDA94 / MGA zone 52 125.99,-37.38,132,-9.1 3 6283 +proj=utm +zone=52 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +28353 projected GDA94 / MGA zone 53 132,-40.71,138.01,-8.88 3 6283 +proj=utm +zone=53 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +28354 projected GDA94 / MGA zone 54 138,-48.19,144.01,-9.08 3 6283 +proj=utm +zone=54 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +28355 projected GDA94 / MGA zone 55 144,-50.89,150.01,-9.23 3 6283 +proj=utm +zone=55 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +28356 projected GDA94 / MGA zone 56 150,-58.96,156,-13.87 3 6283 +proj=utm +zone=56 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +28357 projected GDA94 / MGA zone 57 156,-60.56,162.01,-14.08 3 6283 +proj=utm +zone=57 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +28358 projected GDA94 / MGA zone 58 162,-59.39,168,-25.94 3 6283 +proj=utm +zone=58 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +28404 projected Pulkovo 1942 / Gauss-Kruger zone 4 19.57,47.95,24,59.44 4.5 6284 +proj=tmerc +lat_0=0 +lon_0=21 +k=1 +x_0=4500000 +y_0=0 +ellps=krass +towgs84=25,-141,-78.5,0,0.35,0.736,0 +units=m +no_defs +28405 projected Pulkovo 1942 / Gauss-Kruger zone 5 24,45.18,30.01,69.47 3 6284 +proj=tmerc +lat_0=0 +lon_0=27 +k=1 +x_0=5500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +28406 projected Pulkovo 1942 / Gauss-Kruger zone 6 30,44.32,36,70.02 3 6284 +proj=tmerc +lat_0=0 +lon_0=33 +k=1 +x_0=6500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +28407 projected Pulkovo 1942 / Gauss-Kruger zone 7 36,41.43,42,69.23 3 6284 +proj=tmerc +lat_0=0 +lon_0=39 +k=1 +x_0=7500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +28408 projected Pulkovo 1942 / Gauss-Kruger zone 8 42,38.84,48.01,80.91 4.5 6284 +proj=tmerc +lat_0=0 +lon_0=45 +k=1 +x_0=8500000 +y_0=0 +ellps=krass +towgs84=25,-141,-78.5,0,0.35,0.736,0 +units=m +no_defs +28409 projected Pulkovo 1942 / Gauss-Kruger zone 9 48,37.34,54,81.4 4.5 6284 +proj=tmerc +lat_0=0 +lon_0=51 +k=1 +x_0=9500000 +y_0=0 +ellps=krass +towgs84=25,-141,-78.5,0,0.35,0.736,0 +units=m +no_defs +28410 projected Pulkovo 1942 / Gauss-Kruger zone 10 54,37.05,60,81.91 4.5 6284 +proj=tmerc +lat_0=0 +lon_0=57 +k=1 +x_0=10500000 +y_0=0 +ellps=krass +towgs84=25,-141,-78.5,0,0.35,0.736,0 +units=m +no_defs +28411 projected Pulkovo 1942 / Gauss-Kruger zone 11 60,35.14,66,81.77 4.5 6284 +proj=tmerc +lat_0=0 +lon_0=63 +k=1 +x_0=11500000 +y_0=0 +ellps=krass +towgs84=25,-141,-78.5,0,0.35,0.736,0 +units=m +no_defs +28412 projected Pulkovo 1942 / Gauss-Kruger zone 12 66,36.67,72,77.07 4.5 6284 +proj=tmerc +lat_0=0 +lon_0=69 +k=1 +x_0=12500000 +y_0=0 +ellps=krass +towgs84=25,-141,-78.5,0,0.35,0.736,0 +units=m +no_defs +28413 projected Pulkovo 1942 / Gauss-Kruger zone 13 72,36.79,78,79.71 4.5 6284 +proj=tmerc +lat_0=0 +lon_0=75 +k=1 +x_0=13500000 +y_0=0 +ellps=krass +towgs84=25,-141,-78.5,0,0.35,0.736,0 +units=m +no_defs +28414 projected Pulkovo 1942 / Gauss-Kruger zone 14 78,41.04,84.01,81.03 4.5 6284 +proj=tmerc +lat_0=0 +lon_0=81 +k=1 +x_0=14500000 +y_0=0 +ellps=krass +towgs84=25,-141,-78.5,0,0.35,0.736,0 +units=m +no_defs +28415 projected Pulkovo 1942 / Gauss-Kruger zone 15 84,46.82,90,81.27 3 6284 +proj=tmerc +lat_0=0 +lon_0=87 +k=1 +x_0=15500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +28416 projected Pulkovo 1942 / Gauss-Kruger zone 16 90,49.89,96,81.35 3 6284 +proj=tmerc +lat_0=0 +lon_0=93 +k=1 +x_0=16500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +28417 projected Pulkovo 1942 / Gauss-Kruger zone 17 96,49.73,102,81.32 3 6284 +proj=tmerc +lat_0=0 +lon_0=99 +k=1 +x_0=17500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +28418 projected Pulkovo 1942 / Gauss-Kruger zone 18 102,49.64,108,79.48 3 6284 +proj=tmerc +lat_0=0 +lon_0=105 +k=1 +x_0=18500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +28419 projected Pulkovo 1942 / Gauss-Kruger zone 19 108,49.14,114,76.81 3 6284 +proj=tmerc +lat_0=0 +lon_0=111 +k=1 +x_0=19500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +28420 projected Pulkovo 1942 / Gauss-Kruger zone 20 114,49.51,120,75.96 3 6284 +proj=tmerc +lat_0=0 +lon_0=117 +k=1 +x_0=20500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +28421 projected Pulkovo 1942 / Gauss-Kruger zone 21 120,51.51,126,74 3 6284 +proj=tmerc +lat_0=0 +lon_0=123 +k=1 +x_0=21500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +28422 projected Pulkovo 1942 / Gauss-Kruger zone 22 126,42.25,132,73.61 3 6284 +proj=tmerc +lat_0=0 +lon_0=129 +k=1 +x_0=22500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +28423 projected Pulkovo 1942 / Gauss-Kruger zone 23 132,42.63,138,76.15 3 6284 +proj=tmerc +lat_0=0 +lon_0=135 +k=1 +x_0=23500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +28424 projected Pulkovo 1942 / Gauss-Kruger zone 24 138,45.84,144,76.27 3 6284 +proj=tmerc +lat_0=0 +lon_0=141 +k=1 +x_0=24500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +28425 projected Pulkovo 1942 / Gauss-Kruger zone 25 144,43.6,150,76.82 3 6284 +proj=tmerc +lat_0=0 +lon_0=147 +k=1 +x_0=25500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +28426 projected Pulkovo 1942 / Gauss-Kruger zone 26 150,45.77,156,76.26 3 6284 +proj=tmerc +lat_0=0 +lon_0=153 +k=1 +x_0=26500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +28427 projected Pulkovo 1942 / Gauss-Kruger zone 27 156,50.27,162,77.2 3 6284 +proj=tmerc +lat_0=0 +lon_0=159 +k=1 +x_0=27500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +28428 projected Pulkovo 1942 / Gauss-Kruger zone 28 162,54.47,168,70.03 3 6284 +proj=tmerc +lat_0=0 +lon_0=165 +k=1 +x_0=28500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +28429 projected Pulkovo 1942 / Gauss-Kruger zone 29 168,54.45,174,70.19 3 6284 +proj=tmerc +lat_0=0 +lon_0=171 +k=1 +x_0=29500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +28430 projected Pulkovo 1942 / Gauss-Kruger zone 30 174,61.65,180,71.59 3 6284 +proj=tmerc +lat_0=0 +lon_0=177 +k=1 +x_0=30500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +28431 projected Pulkovo 1942 / Gauss-Kruger zone 31 -180,64.35,-174,71.65 3 6284 +proj=tmerc +lat_0=0 +lon_0=-177 +k=1 +x_0=31500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +28432 projected Pulkovo 1942 / Gauss-Kruger zone 32 -174,64.2,-168.97,67.18 3 6284 +proj=tmerc +lat_0=0 +lon_0=-171 +k=1 +x_0=32500000 +y_0=0 +ellps=krass +towgs84=23.57,-140.95,-79.8,0,0.35,0.79,-0.22 +units=m +no_defs +28600 projected Qatar 1974 / Qatar National Grid 50.69,24.55,51.68,26.2 1 6285 +proj=tmerc +lat_0=24.45 +lon_0=51.2166666666667 +k=0.99999 +x_0=200000 +y_0=300000 +ellps=intl +towgs84=-128.033,-283.697,21.052 +units=m +no_defs +28991 projected Amersfoort / RD Old 3.2,50.75,7.22,53.7 1 6289 +proj=sterea +lat_0=52.1561605555556 +lon_0=5.38763888888889 +k=0.9999079 +x_0=0 +y_0=0 +ellps=bessel +towgs84=565.4171,50.3319,465.5524,-0.398957388243134,0.343987817378283,-1.87740163998045,4.0725 +units=m +no_defs +28992 projected Amersfoort / RD New 3.2,50.75,7.22,53.7 1 6289 +proj=sterea +lat_0=52.1561605555556 +lon_0=5.38763888888889 +k=0.9999079 +x_0=155000 +y_0=463000 +ellps=bessel +towgs84=565.4171,50.3319,465.5524,-0.398957388243134,0.343987817378283,-1.87740163998045,4.0725 +units=m +no_defs +29101 projected SAD69 / Brazil Polyconic -74.01,-35.71,-25.28,7.04 5 6618 +proj=poly +lat_0=0 +lon_0=-54 +x_0=5000000 +y_0=10000000 +ellps=aust_SA +towgs84=-67.35,3.88,-38.22 +units=m +no_defs +29168 projected SAD69 / UTM zone 18N -78,0,-72,12.31 10 6618 +proj=utm +zone=18 +ellps=aust_SA +towgs84=-44,6,-36 +units=m +no_defs +29169 projected SAD69 / UTM zone 19N -72,0,-66,12.52 19 6618 +proj=utm +zone=19 +ellps=aust_SA +towgs84=-57,1,-41 +units=m +no_defs +29170 projected SAD69 / UTM zone 20N -66,0.64,-59.99,11.23 8 6618 +proj=utm +zone=20 +ellps=aust_SA +towgs84=-45,8,-33 +units=m +no_defs +29171 projected SAD69 / UTM zone 21N -60,1.18,-54,8.6 19 6618 +proj=utm +zone=21 +ellps=aust_SA +towgs84=-57,1,-41 +units=m +no_defs +29172 projected SAD69 / UTM zone 22N -54,1.68,-46.65,5.81 5 6618 +proj=utm +zone=22 +ellps=aust_SA +towgs84=-67.35,3.88,-38.22 +units=m +no_defs +29187 projected SAD69 / UTM zone 17S -81.41,-10.53,-78,0 19 6618 +proj=utm +zone=17 +south +ellps=aust_SA +towgs84=-57,1,-41 +units=m +no_defs +29188 projected SAD69 / UTM zone 18S -78,-45,-71.99,0 19 6618 +proj=utm +zone=18 +south +ellps=aust_SA +towgs84=-57,1,-41 +units=m +no_defs +29189 projected SAD69 / UTM zone 19S -72,-45,-65.99,2.15 19 6618 +proj=utm +zone=19 +south +ellps=aust_SA +towgs84=-57,1,-41 +units=m +no_defs +29190 projected SAD69 / UTM zone 20S -66,-45,-59.99,5.28 19 6618 +proj=utm +zone=20 +south +ellps=aust_SA +towgs84=-57,1,-41 +units=m +no_defs +29191 projected SAD69 / UTM zone 21S -60,-38.91,-53.99,4.51 19 6618 +proj=utm +zone=21 +south +ellps=aust_SA +towgs84=-57,1,-41 +units=m +no_defs +29192 projected SAD69 / UTM zone 22S -54,-35.71,-47.99,7.04 5 6618 +proj=utm +zone=22 +south +ellps=aust_SA +towgs84=-67.35,3.88,-38.22 +units=m +no_defs +29193 projected SAD69 / UTM zone 23S -48,-33.5,-42,5.13 5 6618 +proj=utm +zone=23 +south +ellps=aust_SA +towgs84=-67.35,3.88,-38.22 +units=m +no_defs +29194 projected SAD69 / UTM zone 24S -42,-26.35,-36,0.74 5 6618 +proj=utm +zone=24 +south +ellps=aust_SA +towgs84=-67.35,3.88,-38.22 +units=m +no_defs +29195 projected SAD69 / UTM zone 25S -36,-23.8,-29.99,4.19 5 6618 +proj=utm +zone=25 +south +ellps=aust_SA +towgs84=-67.35,3.88,-38.22 +units=m +no_defs +29220 projected Sapper Hill 1943 / UTM zone 20S -61.55,-52.33,-59.99,-50.96 6292 +proj=utm +zone=20 +south +ellps=intl +units=m +no_defs +29221 projected Sapper Hill 1943 / UTM zone 21S -60,-52.51,-57.6,-51.13 2 6292 +proj=utm +zone=21 +south +ellps=intl +towgs84=-355,21,72 +units=m +no_defs +29333 projected Schwarzeck / UTM zone 33S 8.24,-30.64,16.46,-17.24 35 6293 +proj=utm +zone=33 +south +ellps=bess_nam +towgs84=616,97,-251 +units=m +no_defs +29371 projected Schwarzeck / Lo22/11 11.66,-18.53,12,-17.15 35 6293 +proj=tmerc +axis=wsu +lat_0=-22 +lon_0=11 +k=1 +x_0=0 +y_0=0 +ellps=bess_nam +towgs84=616,97,-251 +to_meter=1.0000135965 +no_defs +29373 projected Schwarzeck / Lo22/13 12,-21.9,14.01,-16.95 35 6293 +proj=tmerc +axis=wsu +lat_0=-22 +lon_0=13 +k=1 +x_0=0 +y_0=0 +ellps=bess_nam +towgs84=616,97,-251 +to_meter=1.0000135965 +no_defs +29375 projected Schwarzeck / Lo22/15 14,-28.3,16,-17.38 35 6293 +proj=tmerc +axis=wsu +lat_0=-22 +lon_0=15 +k=1 +x_0=0 +y_0=0 +ellps=bess_nam +towgs84=616,97,-251 +to_meter=1.0000135965 +no_defs +29377 projected Schwarzeck / Lo22/17 15.99,-28.83,18.01,-17.38 35 6293 +proj=tmerc +axis=wsu +lat_0=-22 +lon_0=17 +k=1 +x_0=0 +y_0=0 +ellps=bess_nam +towgs84=616,97,-251 +to_meter=1.0000135965 +no_defs +29379 projected Schwarzeck / Lo22/19 18,-28.97,20,-17.38 35 6293 +proj=tmerc +axis=wsu +lat_0=-22 +lon_0=19 +k=1 +x_0=0 +y_0=0 +ellps=bess_nam +towgs84=616,97,-251 +to_meter=1.0000135965 +no_defs +29381 projected Schwarzeck / Lo22/21 19.99,-22.01,22,-17.85 35 6293 +proj=tmerc +axis=wsu +lat_0=-22 +lon_0=21 +k=1 +x_0=0 +y_0=0 +ellps=bess_nam +towgs84=616,97,-251 +to_meter=1.0000135965 +no_defs +29383 projected Schwarzeck / Lo22/23 21.99,-18.49,24,-17.52 35 6293 +proj=tmerc +axis=wsu +lat_0=-22 +lon_0=23 +k=1 +x_0=0 +y_0=0 +ellps=bess_nam +towgs84=616,97,-251 +to_meter=1.0000135965 +no_defs +29385 projected Schwarzeck / Lo22/25 24,-18.18,25.27,-17.47 35 6293 +proj=tmerc +axis=wsu +lat_0=-22 +lon_0=25 +k=1 +x_0=0 +y_0=0 +ellps=bess_nam +towgs84=616,97,-251 +to_meter=1.0000135965 +no_defs +29701 projected Tananarive (Paris) / Laborde Grid 43.18,-25.64,50.56,-11.89 6810 +proj=labrd +lat_0=-18.9 +lon_0=44.1 +azi=18.9 +k=0.9995 +x_0=400000 +y_0=800000 +ellps=intl +pm=2.33722916999999 +units=m +no_defs +29702 projected Tananarive (Paris) / Laborde Grid approximation 43.18,-25.64,50.56,-11.89 6810 +proj=omerc +lat_0=-18.9 +lonc=44.1 +alpha=18.9 +gamma=18.9 +k=0.9995 +x_0=400000 +y_0=800000 +ellps=intl +pm=2.33722916999999 +units=m +no_defs +29738 projected Tananarive / UTM zone 38S 42.53,-26.59,48,-13 3 6297 +proj=utm +zone=38 +south +ellps=intl +towgs84=-198.383,-240.517,-107.909 +units=m +no_defs +29739 projected Tananarive / UTM zone 39S 48,-24.21,51.03,-11.69 3 6297 +proj=utm +zone=39 +south +ellps=intl +towgs84=-198.383,-240.517,-107.909 +units=m +no_defs +29849 projected Timbalai 1948 / UTM zone 49N 109.31,0.85,114,7.37 19 6298 +proj=utm +zone=49 +ellps=evrstSS +towgs84=-679,669,-48 +units=m +no_defs +29850 projected Timbalai 1948 / UTM zone 50N 114,1.43,119.61,7.67 19 6298 +proj=utm +zone=50 +ellps=evrstSS +towgs84=-679,669,-48 +units=m +no_defs +29871 projected Timbalai 1948 / RSO Borneo (ch) 109.31,0.85,119.61,7.67 19 6298 +proj=omerc +lat_0=4 +lonc=115 +alpha=53.3158204722222 +gamma=53.1301023611111 +k=0.99984 +x_0=590476.87146304 +y_0=442857.653094361 +ellps=evrstSS +towgs84=-679,669,-48 +to_meter=20.1167651215526 +no_defs +29872 projected Timbalai 1948 / RSO Borneo (ftSe) 109.31,0.85,119.61,7.67 19 6298 +proj=omerc +lat_0=4 +lonc=115 +alpha=53.3158204722222 +gamma=53.1301023611111 +k=0.99984 +x_0=590476.872743198 +y_0=442857.654557398 +ellps=evrstSS +towgs84=-679,669,-48 +to_meter=0.304799471538676 +no_defs +29873 projected Timbalai 1948 / RSO Borneo (m) 109.31,0.85,119.61,7.67 19 6298 +proj=omerc +lat_0=4 +lonc=115 +alpha=53.3158204722222 +gamma=53.1301023611111 +k=0.99984 +x_0=590476.87 +y_0=442857.65 +ellps=evrstSS +towgs84=-679,669,-48 +units=m +no_defs +29874 projected Timbalai 1948 / RSO Sarawak LSD (m) 109.54,0.85,115.69,5.03 19 6298 +proj=omerc +no_uoff +lat_0=4 +lonc=115 +alpha=53.3158204722222 +gamma=53.1301023611111 +k=0.99984 +x_0=2000000 +y_0=5000000 +ellps=evrstSS +towgs84=-679,669,-48 +units=m +no_defs +29901 projected OSNI 1952 / Irish National Grid -8.18,53.96,-5.34,55.36 1 6188 +proj=tmerc +lat_0=53.5 +lon_0=-8 +k=1 +x_0=200000 +y_0=250000 +ellps=airy +towgs84=482.5,-130.6,564.6,-1.042,-0.214,-0.631,8.15 +units=m +no_defs +29902 projected TM65 / Irish Grid -10.56,51.39,-5.93,55.43 1 6299 +proj=tmerc +lat_0=53.5 +lon_0=-8 +k=1.000035 +x_0=200000 +y_0=250000 +a=6377340.189 +rf=299.3249646 +towgs84=482.5,-130.6,564.6,-1.042,-0.214,-0.631,8.15 +units=m +no_defs +29903 projected TM75 / Irish Grid -10.56,51.39,-5.34,55.43 1 6300 +proj=tmerc +lat_0=53.5 +lon_0=-8 +k=1.000035 +x_0=200000 +y_0=250000 +a=6377340.189 +rf=299.3249646 +towgs84=482.5,-130.6,564.6,-1.042,-0.214,-0.631,8.15 +units=m +no_defs +30161 projected Tokyo / Japan Plane Rectangular CS I 128.17,26.96,130.46,34.74 9 6301 +proj=tmerc +lat_0=33 +lon_0=129.5 +k=0.9999 +x_0=0 +y_0=0 +ellps=bessel +towgs84=-146.414,507.337,680.507 +units=m +no_defs +30162 projected Tokyo / Japan Plane Rectangular CS II 129.76,30.18,132.05,33.99 9 6301 +proj=tmerc +lat_0=33 +lon_0=131 +k=0.9999 +x_0=0 +y_0=0 +ellps=bessel +towgs84=-146.414,507.337,680.507 +units=m +no_defs +30163 projected Tokyo / Japan Plane Rectangular CS III 130.81,33.72,133.49,36.38 9 6301 +proj=tmerc +lat_0=36 +lon_0=132.166666666667 +k=0.9999 +x_0=0 +y_0=0 +ellps=bessel +towgs84=-146.414,507.337,680.507 +units=m +no_defs +30164 projected Tokyo / Japan Plane Rectangular CS IV 131.95,32.69,134.81,34.45 9 6301 +proj=tmerc +lat_0=33 +lon_0=133.5 +k=0.9999 +x_0=0 +y_0=0 +ellps=bessel +towgs84=-146.414,507.337,680.507 +units=m +no_defs +30165 projected Tokyo / Japan Plane Rectangular CS V 133.13,34.13,135.47,35.71 9 6301 +proj=tmerc +lat_0=36 +lon_0=134.333333333333 +k=0.9999 +x_0=0 +y_0=0 +ellps=bessel +towgs84=-146.414,507.337,680.507 +units=m +no_defs +30166 projected Tokyo / Japan Plane Rectangular CS VI 134.86,33.4,136.99,36.33 9 6301 +proj=tmerc +lat_0=36 +lon_0=136 +k=0.9999 +x_0=0 +y_0=0 +ellps=bessel +towgs84=-146.414,507.337,680.507 +units=m +no_defs +30167 projected Tokyo / Japan Plane Rectangular CS VII 136.22,34.51,137.84,37.58 9 6301 +proj=tmerc +lat_0=36 +lon_0=137.166666666667 +k=0.9999 +x_0=0 +y_0=0 +ellps=bessel +towgs84=-146.414,507.337,680.507 +units=m +no_defs +30168 projected Tokyo / Japan Plane Rectangular CS VIII 137.32,34.54,139.91,38.58 9 6301 +proj=tmerc +lat_0=36 +lon_0=138.5 +k=0.9999 +x_0=0 +y_0=0 +ellps=bessel +towgs84=-146.414,507.337,680.507 +units=m +no_defs +30169 projected Tokyo / Japan Plane Rectangular CS IX 138.4,29.31,141.11,37.98 9 6301 +proj=tmerc +lat_0=36 +lon_0=139.833333333333 +k=0.9999 +x_0=0 +y_0=0 +ellps=bessel +towgs84=-146.414,507.337,680.507 +units=m +no_defs +30170 projected Tokyo / Japan Plane Rectangular CS X 139.49,37.73,142.14,41.58 9 6301 +proj=tmerc +lat_0=40 +lon_0=140.833333333333 +k=0.9999 +x_0=0 +y_0=0 +ellps=bessel +towgs84=-146.414,507.337,680.507 +units=m +no_defs +30171 projected Tokyo / Japan Plane Rectangular CS XI 139.34,41.34,141.46,43.42 9 6301 +proj=tmerc +lat_0=44 +lon_0=140.25 +k=0.9999 +x_0=0 +y_0=0 +ellps=bessel +towgs84=-146.414,507.337,680.507 +units=m +no_defs +30172 projected Tokyo / Japan Plane Rectangular CS XII 140.89,42.15,143.61,45.54 9 6301 +proj=tmerc +lat_0=44 +lon_0=142.25 +k=0.9999 +x_0=0 +y_0=0 +ellps=bessel +towgs84=-146.414,507.337,680.507 +units=m +no_defs +30173 projected Tokyo / Japan Plane Rectangular CS XIII 142.61,41.87,145.87,44.4 9 6301 +proj=tmerc +lat_0=44 +lon_0=144.25 +k=0.9999 +x_0=0 +y_0=0 +ellps=bessel +towgs84=-146.414,507.337,680.507 +units=m +no_defs +30174 projected Tokyo / Japan Plane Rectangular CS XIV 141.2,24.67,142.33,27.8 9 6301 +proj=tmerc +lat_0=26 +lon_0=142 +k=0.9999 +x_0=0 +y_0=0 +ellps=bessel +towgs84=-146.414,507.337,680.507 +units=m +no_defs +30175 projected Tokyo / Japan Plane Rectangular CS XV 126.63,26.02,128.4,26.91 9 6301 +proj=tmerc +lat_0=26 +lon_0=127.5 +k=0.9999 +x_0=0 +y_0=0 +ellps=bessel +towgs84=-146.414,507.337,680.507 +units=m +no_defs +30176 projected Tokyo / Japan Plane Rectangular CS XVI 122.83,23.98,125.51,24.94 9 6301 +proj=tmerc +lat_0=26 +lon_0=124 +k=0.9999 +x_0=0 +y_0=0 +ellps=bessel +towgs84=-146.414,507.337,680.507 +units=m +no_defs +30177 projected Tokyo / Japan Plane Rectangular CS XVII 131.12,24.4,131.38,26.01 9 6301 +proj=tmerc +lat_0=26 +lon_0=131 +k=0.9999 +x_0=0 +y_0=0 +ellps=bessel +towgs84=-146.414,507.337,680.507 +units=m +no_defs +30178 projected Tokyo / Japan Plane Rectangular CS XVIII 136.02,20.37,136.16,20.48 9 6301 +proj=tmerc +lat_0=20 +lon_0=136 +k=0.9999 +x_0=0 +y_0=0 +ellps=bessel +towgs84=-146.414,507.337,680.507 +units=m +no_defs +30179 projected Tokyo / Japan Plane Rectangular CS XIX 153.91,24.22,154.05,24.35 9 6301 +proj=tmerc +lat_0=26 +lon_0=154 +k=0.9999 +x_0=0 +y_0=0 +ellps=bessel +towgs84=-146.414,507.337,680.507 +units=m +no_defs +30200 projected Trinidad 1903 / Trinidad Grid -62.09,9.83,-60,11.51 2 6302 +proj=cass +lat_0=10.4416666666667 +lon_0=-61.3333333333333 +x_0=86501.46392052 +y_0=65379.0134283 +a=6378293.64520876 +b=6356617.98767984 +towgs84=-61.702,284.488,472.052 +to_meter=0.201166195164 +no_defs +30339 projected TC(1948) / UTM zone 39N 51.56,22.76,54.01,24.32 6303 +proj=utm +zone=39 +ellps=helmert +units=m +no_defs +30340 projected TC(1948) / UTM zone 40N 53.99,22.63,56.03,25.34 6303 +proj=utm +zone=40 +ellps=helmert +units=m +no_defs +30491 projected Voirol 1875 / Nord Algerie (ancienne) -2.22,34.64,8.64,37.14 6304 +proj=lcc +lat_1=36 +lat_0=36 +lon_0=2.7 +k_0=0.999625544 +x_0=500000 +y_0=300000 +ellps=clrk80ign +towgs84=-73,-247,227 +units=m +no_defs +30492 projected Voirol 1875 / Sud Algerie (ancienne) -2.95,31.99,9.09,34.66 6304 +proj=lcc +lat_1=33.3 +lat_0=33.3 +lon_0=2.7 +k_0=0.999625769 +x_0=500000 +y_0=300000 +ellps=clrk80ign +towgs84=-73,-247,227 +units=m +no_defs +30493 projected Voirol 1879 / Nord Algerie (ancienne) -2.22,34.64,8.64,37.14 6671 +proj=lcc +lat_1=36 +lat_0=36 +lon_0=2.7 +k_0=0.999625544 +x_0=500000 +y_0=300000 +ellps=clrk80ign +units=m +no_defs +30494 projected Voirol 1879 / Sud Algerie (ancienne) -2.95,31.99,9.09,34.66 6671 +proj=lcc +lat_1=33.3 +lat_0=33.3 +lon_0=2.7 +k_0=0.999625769 +x_0=500000 +y_0=300000 +ellps=clrk80ign +units=m +no_defs +30729 projected Nord Sahara 1959 / UTM zone 29N -8.67,25.73,-6,29.85 1 6307 +proj=utm +zone=29 +a=6378249.145 +rf=293.465 +towgs84=-267.407,-47.068,446.357,-0.179423,5.577661,-1.27762,1.204866 +units=m +no_defs +30730 projected Nord Sahara 1959 / UTM zone 30N -6,21.82,0,37.01 1 6307 +proj=utm +zone=30 +a=6378249.145 +rf=293.465 +towgs84=-267.407,-47.068,446.357,-0.179423,5.577661,-1.27762,1.204866 +units=m +no_defs +30731 projected Nord Sahara 1959 / UTM zone 31N 0,18.97,6.01,38.77 1 6307 +proj=utm +zone=31 +a=6378249.145 +rf=293.465 +towgs84=-267.407,-47.068,446.357,-0.179423,5.577661,-1.27762,1.204866 +units=m +no_defs +30732 projected Nord Sahara 1959 / UTM zone 32N 6,19.6,11.99,38.8 1 6307 +proj=utm +zone=32 +a=6378249.145 +rf=293.465 +towgs84=-267.407,-47.068,446.357,-0.179423,5.577661,-1.27762,1.204866 +units=m +no_defs +30791 projected Nord Sahara 1959 / Nord Algerie -2.22,34.64,8.64,37.14 1 6307 +proj=lcc +lat_1=36 +lat_0=36 +lon_0=2.7 +k_0=0.999625544 +x_0=500135 +y_0=300090 +a=6378249.145 +rf=293.465 +towgs84=-267.407,-47.068,446.357,-0.179423,5.577661,-1.27762,1.204866 +units=m +no_defs +30792 projected Nord Sahara 1959 / Sud Algerie -3.85,31.49,9.22,34.66 1 6307 +proj=lcc +lat_1=33.3 +lat_0=33.3 +lon_0=2.7 +k_0=0.999625769 +x_0=500135 +y_0=300090 +a=6378249.145 +rf=293.465 +towgs84=-267.407,-47.068,446.357,-0.179423,5.577661,-1.27762,1.204866 +units=m +no_defs +31028 projected Yoff / UTM zone 28N -20.22,10.64,-11.36,16.7 5 6310 +proj=utm +zone=28 +ellps=clrk80ign +towgs84=-30,190,89 +units=m +no_defs +31121 projected Zanderij / UTM zone 21N -58.08,1.83,-52.66,9.35 11 6311 +proj=utm +zone=21 +ellps=intl +towgs84=-265,120,-358 +units=m +no_defs +31154 projected Zanderij / TM 54 NW -57.25,5.34,-52.66,9.35 11 6311 +proj=tmerc +lat_0=0 +lon_0=-54 +k=0.9996 +x_0=500000 +y_0=0 +ellps=intl +towgs84=-265,120,-358 +units=m +no_defs +31170 projected Zanderij / Suriname Old TM -58.08,1.83,-53.95,6.06 11 6311 +proj=tmerc +lat_0=0 +lon_0=-55.6833333333333 +k=0.9996 +x_0=500000 +y_0=0 +ellps=intl +towgs84=-265,120,-358 +units=m +no_defs +31171 projected Zanderij / Suriname TM -58.08,1.83,-53.95,6.06 11 6311 +proj=tmerc +lat_0=0 +lon_0=-55.6833333333333 +k=0.9999 +x_0=500000 +y_0=0 +ellps=intl +towgs84=-265,120,-358 +units=m +no_defs +31251 projected MGI (Ferro) / Austria GK West Zone 9.53,46.77,11.84,47.61 6805 +proj=tmerc +lat_0=0 +lon_0=28 +k=1 +x_0=0 +y_0=-5000000 +ellps=bessel +pm=-17.6666666666667 +units=m +no_defs +31252 projected MGI (Ferro) / Austria GK Central Zone 11.83,46.4,14.84,48.79 6805 +proj=tmerc +lat_0=0 +lon_0=31 +k=1 +x_0=0 +y_0=-5000000 +ellps=bessel +pm=-17.6666666666667 +units=m +no_defs +31253 projected MGI (Ferro) / Austria GK East Zone 14.83,46.56,17.17,49.02 6805 +proj=tmerc +lat_0=0 +lon_0=34 +k=1 +x_0=0 +y_0=-5000000 +ellps=bessel +pm=-17.6666666666667 +units=m +no_defs +31254 projected MGI / Austria GK West 9.53,46.77,11.84,47.61 1.5 6312 +proj=tmerc +lat_0=0 +lon_0=10.3333333333333 +k=1 +x_0=0 +y_0=-5000000 +ellps=bessel +towgs84=577.326,90.129,463.919,5.137,1.474,5.297,2.4232 +units=m +no_defs +31255 projected MGI / Austria GK Central 11.83,46.4,14.84,48.79 1.5 6312 +proj=tmerc +lat_0=0 +lon_0=13.3333333333333 +k=1 +x_0=0 +y_0=-5000000 +ellps=bessel +towgs84=577.326,90.129,463.919,5.137,1.474,5.297,2.4232 +units=m +no_defs +31256 projected MGI / Austria GK East 14.83,46.56,17.17,49.02 1.5 6312 +proj=tmerc +lat_0=0 +lon_0=16.3333333333333 +k=1 +x_0=0 +y_0=-5000000 +ellps=bessel +towgs84=577.326,90.129,463.919,5.137,1.474,5.297,2.4232 +units=m +no_defs +31257 projected MGI / Austria GK M28 9.53,46.77,11.84,47.61 1.5 6312 +proj=tmerc +lat_0=0 +lon_0=10.3333333333333 +k=1 +x_0=150000 +y_0=-5000000 +ellps=bessel +towgs84=577.326,90.129,463.919,5.137,1.474,5.297,2.4232 +units=m +no_defs +31258 projected MGI / Austria GK M31 11.83,46.4,14.84,48.79 1.5 6312 +proj=tmerc +lat_0=0 +lon_0=13.3333333333333 +k=1 +x_0=450000 +y_0=-5000000 +ellps=bessel +towgs84=577.326,90.129,463.919,5.137,1.474,5.297,2.4232 +units=m +no_defs +31259 projected MGI / Austria GK M34 14.83,46.56,17.17,49.02 1.5 6312 +proj=tmerc +lat_0=0 +lon_0=16.3333333333333 +k=1 +x_0=750000 +y_0=-5000000 +ellps=bessel +towgs84=577.326,90.129,463.919,5.137,1.474,5.297,2.4232 +units=m +no_defs +31281 projected MGI (Ferro) / Austria West Zone 9.53,46.77,11.84,47.61 6805 +proj=tmerc +lat_0=0 +lon_0=28 +k=1 +x_0=0 +y_0=0 +ellps=bessel +pm=-17.6666666666667 +units=m +no_defs +31282 projected MGI (Ferro) / Austria Central Zone 11.83,46.4,14.84,48.79 6805 +proj=tmerc +lat_0=0 +lon_0=31 +k=1 +x_0=0 +y_0=0 +ellps=bessel +pm=-17.6666666666667 +units=m +no_defs +31283 projected MGI (Ferro) / Austria East Zone 14.83,46.56,17.17,49.02 6805 +proj=tmerc +lat_0=0 +lon_0=34 +k=1 +x_0=0 +y_0=0 +ellps=bessel +pm=-17.6666666666667 +units=m +no_defs +31284 projected MGI / Austria M28 9.53,46.77,11.84,47.61 1.5 6312 +proj=tmerc +lat_0=0 +lon_0=10.3333333333333 +k=1 +x_0=150000 +y_0=0 +ellps=bessel +towgs84=577.326,90.129,463.919,5.137,1.474,5.297,2.4232 +units=m +no_defs +31285 projected MGI / Austria M31 11.83,46.4,14.84,48.79 1.5 6312 +proj=tmerc +lat_0=0 +lon_0=13.3333333333333 +k=1 +x_0=450000 +y_0=0 +ellps=bessel +towgs84=577.326,90.129,463.919,5.137,1.474,5.297,2.4232 +units=m +no_defs +31286 projected MGI / Austria M34 14.83,46.56,17.17,49.02 1.5 6312 +proj=tmerc +lat_0=0 +lon_0=16.3333333333333 +k=1 +x_0=750000 +y_0=0 +ellps=bessel +towgs84=577.326,90.129,463.919,5.137,1.474,5.297,2.4232 +units=m +no_defs +31287 projected MGI / Austria Lambert 9.53,46.4,17.17,49.02 1.5 6312 +proj=lcc +lat_0=47.5 +lon_0=13.3333333333333 +lat_1=49 +lat_2=46 +x_0=400000 +y_0=400000 +ellps=bessel +towgs84=577.326,90.129,463.919,5.137,1.474,5.297,2.4232 +units=m +no_defs +31288 projected MGI (Ferro) / Austria zone M28 9.53,46.77,11.84,47.61 6805 +proj=tmerc +lat_0=0 +lon_0=28 +k=1 +x_0=150000 +y_0=0 +ellps=bessel +pm=-17.6666666666667 +units=m +no_defs +31289 projected MGI (Ferro) / Austria zone M31 11.83,46.4,14.84,48.79 6805 +proj=tmerc +lat_0=0 +lon_0=31 +k=1 +x_0=450000 +y_0=0 +ellps=bessel +pm=-17.6666666666667 +units=m +no_defs +31290 projected MGI (Ferro) / Austria zone M34 14.83,46.56,17.17,49.02 6805 +proj=tmerc +lat_0=0 +lon_0=34 +k=1 +x_0=750000 +y_0=0 +ellps=bessel +pm=-17.6666666666667 +units=m +no_defs +31300 projected BD72 / Belge Lambert 72 2.5,49.5,6.4,51.51 1 6313 +proj=lcc +lat_0=90 +lon_0=4.35693972222222 +lat_1=49.8333333333333 +lat_2=51.1666666666667 +x_0=150000.01256 +y_0=5400088.4378 +ellps=intl +towgs84=-106.8686,52.2978,-103.7239,0.3366,-0.457,1.8422,-1.2747 +units=m +no_defs +31370 projected BD72 / Belgian Lambert 72 2.5,49.5,6.4,51.51 1 6313 +proj=lcc +lat_0=90 +lon_0=4.36748666666667 +lat_1=51.1666672333333 +lat_2=49.8333339 +x_0=150000.013 +y_0=5400088.438 +ellps=intl +towgs84=-106.8686,52.2978,-103.7239,0.3366,-0.457,1.8422,-1.2747 +units=m +no_defs +31466 projected DHDN / 3-degree Gauss-Kruger zone 2 5.86,49.11,7.51,53.81 3 6314 +proj=tmerc +lat_0=0 +lon_0=6 +k=1 +x_0=2500000 +y_0=0 +ellps=bessel +towgs84=598.1,73.7,418.2,0.202,0.045,-2.455,6.7 +units=m +no_defs +31467 projected DHDN / 3-degree Gauss-Kruger zone 3 7.5,47.27,10.51,55.09 3 6314 +proj=tmerc +lat_0=0 +lon_0=9 +k=1 +x_0=3500000 +y_0=0 +ellps=bessel +towgs84=598.1,73.7,418.2,0.202,0.045,-2.455,6.7 +units=m +no_defs +31468 projected DHDN / 3-degree Gauss-Kruger zone 4 10.5,47.39,13.51,54.59 3 6314 +proj=tmerc +lat_0=0 +lon_0=12 +k=1 +x_0=4500000 +y_0=0 +ellps=bessel +towgs84=598.1,73.7,418.2,0.202,0.045,-2.455,6.7 +units=m +no_defs +31469 projected DHDN / 3-degree Gauss-Kruger zone 5 13.5,48.51,13.84,48.98 3 6314 +proj=tmerc +lat_0=0 +lon_0=15 +k=1 +x_0=5500000 +y_0=0 +ellps=bessel +towgs84=598.1,73.7,418.2,0.202,0.045,-2.455,6.7 +units=m +no_defs +31528 projected Conakry 1905 / UTM zone 28N -15.13,9.01,-12,12.68 30 6315 +proj=utm +zone=28 +ellps=clrk80ign +towgs84=-23,259,-9 +units=m +no_defs +31529 projected Conakry 1905 / UTM zone 29N -12,7.19,-7.65,12.51 30 6315 +proj=utm +zone=29 +ellps=clrk80ign +towgs84=-23,259,-9 +units=m +no_defs +31600 projected Dealul Piscului 1930 / Stereo 33 20.26,43.62,29.74,48.27 10 6316 +proj=sterea +lat_0=45.9 +lon_0=25.3924658888889 +k=0.9996667 +x_0=500000 +y_0=500000 +ellps=intl +towgs84=103.25,-100.4,-307.19 +units=m +no_defs +31838 projected NGN / UTM zone 38N 46.54,28.53,48,30.09 1 6318 +proj=utm +zone=38 +ellps=WGS84 +towgs84=-3.2,-5.7,2.8 +units=m +no_defs +31839 projected NGN / UTM zone 39N 48,28.54,48.48,30.04 1 6318 +proj=utm +zone=39 +ellps=WGS84 +towgs84=-3.2,-5.7,2.8 +units=m +no_defs +31901 projected KUDAMS / KTM 47.78,29.17,48.16,29.45 1 6319 +proj=tmerc +lat_0=0 +lon_0=48 +k=1 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=226.702,-193.337,-35.371,-2.229,-4.391,9.238,0.9798 +units=m +no_defs +31965 projected SIRGAS 2000 / UTM zone 11N -120,15.01,-114,32.72 1 6674 +proj=utm +zone=11 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +31966 projected SIRGAS 2000 / UTM zone 12N -114,15.09,-108,32.27 1 6674 +proj=utm +zone=12 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +31967 projected SIRGAS 2000 / UTM zone 13N -108,14.05,-102,31.79 1 6674 +proj=utm +zone=13 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +31968 projected SIRGAS 2000 / UTM zone 14N -102.01,12.3,-96,29.81 1 6674 +proj=utm +zone=14 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +31969 projected SIRGAS 2000 / UTM zone 15N -96,0,-90,26 1 6674 +proj=utm +zone=15 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +31970 projected SIRGAS 2000 / UTM zone 16N -90,0,-83.99,25.77 1 6674 +proj=utm +zone=16 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +31971 projected SIRGAS 2000 / UTM zone 17N -84,0,-78,19.54 1 6674 +proj=utm +zone=17 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +31972 projected SIRGAS 2000 / UTM zone 18N -78,0,-72,15.04 1 6674 +proj=utm +zone=18 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +31973 projected SIRGAS 2000 / UTM zone 19N -72,0,-66,15.64 1 6674 +proj=utm +zone=19 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +31974 projected SIRGAS 2000 / UTM zone 20N -66,0.64,-59.99,16.75 1 6674 +proj=utm +zone=20 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +31975 projected SIRGAS 2000 / UTM zone 21N -60,1.18,-54,12.19 1 6674 +proj=utm +zone=21 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +31976 projected SIRGAS 2000 / UTM zone 22N -54,0,-47.99,9.24 1 6674 +proj=utm +zone=22 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +31977 projected SIRGAS 2000 / UTM zone 17S -84,-56.45,-78,0 1 6674 +proj=utm +zone=17 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +31978 projected SIRGAS 2000 / UTM zone 18S -78,-59.36,-71.99,0 1 6674 +proj=utm +zone=18 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +31979 projected SIRGAS 2000 / UTM zone 19S -72,-59.87,-66,2.15 1 6674 +proj=utm +zone=19 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +31980 projected SIRGAS 2000 / UTM zone 20S -66,-58.39,-60,5.28 1 6674 +proj=utm +zone=20 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +31981 projected SIRGAS 2000 / UTM zone 21S -60,-44.82,-54,4.51 1 6674 +proj=utm +zone=21 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +31982 projected SIRGAS 2000 / UTM zone 22S -54,-54.18,-47.99,7.04 1 6674 +proj=utm +zone=22 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +31983 projected SIRGAS 2000 / UTM zone 23S -48,-33.5,-42,5.13 1 6674 +proj=utm +zone=23 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +31984 projected SIRGAS 2000 / UTM zone 24S -42,-26.35,-36,0.74 1 6674 +proj=utm +zone=24 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +31985 projected SIRGAS 2000 / UTM zone 25S -36,-23.8,-29.99,4.19 1 6674 +proj=utm +zone=25 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +31986 projected SIRGAS 1995 / UTM zone 17N -84,0.9,-78,15.51 2 6170 +proj=utm +zone=17 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +31987 projected SIRGAS 1995 / UTM zone 18N -78,0,-72,15.04 2 6170 +proj=utm +zone=18 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +31988 projected SIRGAS 1995 / UTM zone 19N -72,0,-66,15.64 2 6170 +proj=utm +zone=19 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +31989 projected SIRGAS 1995 / UTM zone 20N -66,0,-60,16.75 2 6170 +proj=utm +zone=20 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +31990 projected SIRGAS 1995 / UTM zone 21N -60,0,-54,10.7 2 6170 +proj=utm +zone=21 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +31991 projected SIRGAS 1995 / UTM zone 22N -54,0,-48,9.24 2 6170 +proj=utm +zone=22 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +31992 projected SIRGAS 1995 / UTM zone 17S -84,-56.45,-75.21,1.45 2 6170 +proj=utm +zone=17 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +31993 projected SIRGAS 1995 / UTM zone 18S -78,-59.36,-71.99,0 2 6170 +proj=utm +zone=18 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +31994 projected SIRGAS 1995 / UTM zone 19S -72,-59.87,-66,0 2 6170 +proj=utm +zone=19 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +31995 projected SIRGAS 1995 / UTM zone 20S -66,-58.39,-60,0 2 6170 +proj=utm +zone=20 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +31996 projected SIRGAS 1995 / UTM zone 21S -60,-44.82,-54,0 2 6170 +proj=utm +zone=21 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +31997 projected SIRGAS 1995 / UTM zone 22S -54,-39.95,-48,0 2 6170 +proj=utm +zone=22 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +31998 projected SIRGAS 1995 / UTM zone 23S -48,-33.5,-42,0 2 6170 +proj=utm +zone=23 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +31999 projected SIRGAS 1995 / UTM zone 24S -42,-26.35,-36,0 2 6170 +proj=utm +zone=24 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +32000 projected SIRGAS 1995 / UTM zone 25S -36,-20.11,-30,0 2 6170 +proj=utm +zone=25 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +32001 projected NAD27 / Montana North -116.07,47.41,-104.04,49.01 7 6267 +proj=lcc +lat_0=47 +lon_0=-109.5 +lat_1=48.7166666666667 +lat_2=47.85 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +towgs84=-8,159,175 +to_meter=0.304800609601219 +no_defs +32002 projected NAD27 / Montana Central -116.06,46.17,-104.04,48.26 7 6267 +proj=lcc +lat_0=45.8333333333333 +lon_0=-109.5 +lat_1=47.8833333333333 +lat_2=46.45 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +towgs84=-8,159,175 +to_meter=0.304800609601219 +no_defs +32003 projected NAD27 / Montana South -114.57,44.35,-104.04,46.87 7 6267 +proj=lcc +lat_0=44 +lon_0=-109.5 +lat_1=46.4 +lat_2=44.8666666666667 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +towgs84=-8,159,175 +to_meter=0.304800609601219 +no_defs +32005 projected NAD27 / Nebraska North -104.06,41.68,-96.07,43.01 7 6267 +proj=lcc +lat_0=41.3333333333333 +lon_0=-100 +lat_1=41.85 +lat_2=42.8166666666667 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +towgs84=-8,159,175 +to_meter=0.304800609601219 +no_defs +32006 projected NAD27 / Nebraska South -104.06,39.99,-95.3,42.01 7 6267 +proj=lcc +lat_0=39.6666666666667 +lon_0=-99.5 +lat_1=40.2833333333333 +lat_2=41.7166666666667 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +towgs84=-8,159,175 +to_meter=0.304800609601219 +no_defs +32007 projected NAD27 / Nevada East -117.01,34.99,-114.03,42 7 6267 +proj=tmerc +lat_0=34.75 +lon_0=-115.583333333333 +k=0.9999 +x_0=152400.30480061 +y_0=0 +ellps=clrk66 +towgs84=-8,159,175 +to_meter=0.304800609601219 +no_defs +32008 projected NAD27 / Nevada Central -118.19,36,-114.99,41 7 6267 +proj=tmerc +lat_0=34.75 +lon_0=-116.666666666667 +k=0.9999 +x_0=152400.30480061 +y_0=0 +ellps=clrk66 +towgs84=-8,159,175 +to_meter=0.304800609601219 +no_defs +32009 projected NAD27 / Nevada West -120,36.95,-116.99,42 7 6267 +proj=tmerc +lat_0=34.75 +lon_0=-118.583333333333 +k=0.9999 +x_0=152400.30480061 +y_0=0 +ellps=clrk66 +towgs84=-8,159,175 +to_meter=0.304800609601219 +no_defs +32010 projected NAD27 / New Hampshire -72.56,42.69,-70.63,45.31 10 6267 +proj=tmerc +lat_0=42.5 +lon_0=-71.6666666666667 +k=0.999966667 +x_0=152400.30480061 +y_0=0 +ellps=clrk66 +towgs84=-8,160,176 +to_meter=0.304800609601219 +no_defs +32011 projected NAD27 / New Jersey -75.6,38.87,-73.88,41.36 10 6267 +proj=tmerc +lat_0=38.8333333333333 +lon_0=-74.6666666666667 +k=0.999975 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +towgs84=-8,160,176 +to_meter=0.304800609601219 +no_defs +32012 projected NAD27 / New Mexico East -105.72,32,-102.99,37 7 6267 +proj=tmerc +lat_0=31 +lon_0=-104.333333333333 +k=0.999909091 +x_0=152400.30480061 +y_0=0 +ellps=clrk66 +towgs84=-8,159,175 +to_meter=0.304800609601219 +no_defs +32013 projected NAD27 / New Mexico Central -107.73,31.78,-104.83,37 7 6267 +proj=tmerc +lat_0=31 +lon_0=-106.25 +k=0.9999 +x_0=152400.30480061 +y_0=0 +ellps=clrk66 +towgs84=-8,159,175 +to_meter=0.304800609601219 +no_defs +32014 projected NAD27 / New Mexico West -109.06,31.33,-106.32,37 7 6267 +proj=tmerc +lat_0=31 +lon_0=-107.833333333333 +k=0.999916667 +x_0=152400.30480061 +y_0=0 +ellps=clrk66 +towgs84=-8,159,175 +to_meter=0.304800609601219 +no_defs +32015 projected NAD27 / New York East -75.87,40.88,-73.23,45.02 10 6267 +proj=tmerc +lat_0=40 +lon_0=-74.3333333333333 +k=0.999966667 +x_0=152400.30480061 +y_0=0 +ellps=clrk66 +towgs84=-8,160,176 +to_meter=0.304800609601219 +no_defs +32016 projected NAD27 / New York Central -77.75,41.99,-75.06,44.41 10 6267 +proj=tmerc +lat_0=40 +lon_0=-76.5833333333333 +k=0.9999375 +x_0=152400.30480061 +y_0=0 +ellps=clrk66 +towgs84=-8,160,176 +to_meter=0.304800609601219 +no_defs +32017 projected NAD27 / New York West -79.77,41.99,-77.36,43.64 10 6267 +proj=tmerc +lat_0=40 +lon_0=-78.5833333333333 +k=0.9999375 +x_0=152400.30480061 +y_0=0 +ellps=clrk66 +towgs84=-8,160,176 +to_meter=0.304800609601219 +no_defs +32019 projected NAD27 / North Carolina -84.33,33.83,-75.38,36.59 10 6267 +proj=lcc +lat_0=33.75 +lon_0=-79 +lat_1=34.3333333333333 +lat_2=36.1666666666667 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +towgs84=-8,160,176 +to_meter=0.304800609601219 +no_defs +32020 projected NAD27 / North Dakota North -104.07,47.15,-96.83,49.01 7 6267 +proj=lcc +lat_0=47 +lon_0=-100.5 +lat_1=47.4333333333333 +lat_2=48.7333333333333 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +towgs84=-8,159,175 +to_meter=0.304800609601219 +no_defs +32021 projected NAD27 / North Dakota South -104.05,45.93,-96.55,47.83 7 6267 +proj=lcc +lat_0=45.6666666666667 +lon_0=-100.5 +lat_1=46.1833333333333 +lat_2=47.4833333333333 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +towgs84=-8,159,175 +to_meter=0.304800609601219 +no_defs +32022 projected NAD27 / Ohio North -84.81,40.1,-80.51,42.33 10 6267 +proj=lcc +lat_0=39.6666666666667 +lon_0=-82.5 +lat_1=40.4333333333333 +lat_2=41.7 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +towgs84=-8,160,176 +to_meter=0.304800609601219 +no_defs +32023 projected NAD27 / Ohio South -84.83,38.4,-80.7,40.36 10 6267 +proj=lcc +lat_0=38 +lon_0=-82.5 +lat_1=38.7333333333333 +lat_2=40.0333333333333 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +towgs84=-8,160,176 +to_meter=0.304800609601219 +no_defs +32024 projected NAD27 / Oklahoma North -103,35.27,-94.42,37.01 7 6267 +proj=lcc +lat_0=35 +lon_0=-98 +lat_1=35.5666666666667 +lat_2=36.7666666666667 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +towgs84=-8,159,175 +to_meter=0.304800609601219 +no_defs +32025 projected NAD27 / Oklahoma South -100,33.62,-94.42,35.57 7 6267 +proj=lcc +lat_0=33.3333333333333 +lon_0=-98 +lat_1=33.9333333333333 +lat_2=35.2333333333333 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +towgs84=-8,159,175 +to_meter=0.304800609601219 +no_defs +32026 projected NAD27 / Oregon North -124.17,43.95,-116.47,46.26 7 6267 +proj=lcc +lat_0=43.6666666666667 +lon_0=-120.5 +lat_1=44.3333333333333 +lat_2=46 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +towgs84=-8,159,175 +to_meter=0.304800609601219 +no_defs +32027 projected NAD27 / Oregon South -124.6,41.98,-116.9,44.56 7 6267 +proj=lcc +lat_0=41.6666666666667 +lon_0=-120.5 +lat_1=42.3333333333333 +lat_2=44 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +towgs84=-8,159,175 +to_meter=0.304800609601219 +no_defs +32028 projected NAD27 / Pennsylvania North -80.53,40.6,-74.7,42.53 10 6267 +proj=lcc +lat_0=40.1666666666667 +lon_0=-77.75 +lat_1=40.8833333333333 +lat_2=41.95 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +towgs84=-8,160,176 +to_meter=0.304800609601219 +no_defs +32030 projected NAD27 / Rhode Island -71.85,41.13,-71.08,42.02 10 6267 +proj=tmerc +lat_0=41.0833333333333 +lon_0=-71.5 +k=0.9999938 +x_0=152400.30480061 +y_0=0 +ellps=clrk66 +towgs84=-8,160,176 +to_meter=0.304800609601219 +no_defs +32031 projected NAD27 / South Carolina North -83.36,33.46,-78.52,35.21 10 6267 +proj=lcc +lat_0=33 +lon_0=-81 +lat_1=33.7666666666667 +lat_2=34.9666666666667 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +towgs84=-8,160,176 +to_meter=0.304800609601219 +no_defs +32033 projected NAD27 / South Carolina South -82.03,32.05,-78.95,33.95 10 6267 +proj=lcc +lat_0=31.8333333333333 +lon_0=-81 +lat_1=32.3333333333333 +lat_2=33.6666666666667 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +towgs84=-8,160,176 +to_meter=0.304800609601219 +no_defs +32034 projected NAD27 / South Dakota North -104.07,44.14,-96.45,45.95 7 6267 +proj=lcc +lat_0=43.8333333333333 +lon_0=-100 +lat_1=44.4166666666667 +lat_2=45.6833333333333 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +towgs84=-8,159,175 +to_meter=0.304800609601219 +no_defs +32035 projected NAD27 / South Dakota South -104.06,42.48,-96.43,44.79 7 6267 +proj=lcc +lat_0=42.3333333333333 +lon_0=-100.333333333333 +lat_1=42.8333333333333 +lat_2=44.4 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +towgs84=-8,159,175 +to_meter=0.304800609601219 +no_defs +32037 projected NAD27 / Texas North -103.03,34.3,-99.99,36.5 7 6267 +proj=lcc +lat_0=34 +lon_0=-101.5 +lat_1=34.65 +lat_2=36.1833333333333 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +towgs84=-8,159,175 +to_meter=0.304800609601219 +no_defs +32038 projected NAD27 / Texas North Central -103.07,31.72,-94,34.58 7 6267 +proj=lcc +lat_0=31.6666666666667 +lon_0=-97.5 +lat_1=32.1333333333333 +lat_2=33.9666666666667 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +towgs84=-8,159,175 +to_meter=0.304800609601219 +no_defs +32039 projected NAD27 / Texas Central -106.66,29.78,-93.5,32.27 7 6267 +proj=lcc +lat_0=29.6666666666667 +lon_0=-100.333333333333 +lat_1=30.1166666666667 +lat_2=31.8833333333333 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +towgs84=-8,159,175 +to_meter=0.304800609601219 +no_defs +32040 projected NAD27 / Texas South Central -105,27.78,-93.41,30.67 7 6267 +proj=lcc +lat_0=27.8333333333333 +lon_0=-99 +lat_1=28.3833333333333 +lat_2=30.2833333333333 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +towgs84=-8,159,175 +to_meter=0.304800609601219 +no_defs +32041 projected NAD27 / Texas South -100.2,25.83,-95.36,28.21 7 6267 +proj=lcc +lat_0=25.6666666666667 +lon_0=-98.5 +lat_1=26.1666666666667 +lat_2=27.8333333333333 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +towgs84=-8,159,175 +to_meter=0.304800609601219 +no_defs +32042 projected NAD27 / Utah North -114.04,40.55,-109.04,42.01 7 6267 +proj=lcc +lat_0=40.3333333333333 +lon_0=-111.5 +lat_1=40.7166666666667 +lat_2=41.7833333333333 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +towgs84=-8,159,175 +to_meter=0.304800609601219 +no_defs +32043 projected NAD27 / Utah Central -114.05,38.49,-109.04,41.08 7 6267 +proj=lcc +lat_0=38.3333333333333 +lon_0=-111.5 +lat_1=39.0166666666667 +lat_2=40.65 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +towgs84=-8,159,175 +to_meter=0.304800609601219 +no_defs +32044 projected NAD27 / Utah South -114.05,36.99,-109.04,38.58 7 6267 +proj=lcc +lat_0=36.6666666666667 +lon_0=-111.5 +lat_1=37.2166666666667 +lat_2=38.35 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +towgs84=-8,159,175 +to_meter=0.304800609601219 +no_defs +32045 projected NAD27 / Vermont -73.44,42.72,-71.5,45.03 10 6267 +proj=tmerc +lat_0=42.5 +lon_0=-72.5 +k=0.999964286 +x_0=152400.30480061 +y_0=0 +ellps=clrk66 +towgs84=-8,160,176 +to_meter=0.304800609601219 +no_defs +32046 projected NAD27 / Virginia North -80.06,37.77,-76.51,39.46 10 6267 +proj=lcc +lat_0=37.6666666666667 +lon_0=-78.5 +lat_1=38.0333333333333 +lat_2=39.2 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +towgs84=-8,160,176 +to_meter=0.304800609601219 +no_defs +32047 projected NAD27 / Virginia South -83.68,36.54,-75.31,38.28 10 6267 +proj=lcc +lat_0=36.3333333333333 +lon_0=-78.5 +lat_1=36.7666666666667 +lat_2=37.9666666666667 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +towgs84=-8,160,176 +to_meter=0.304800609601219 +no_defs +32048 projected NAD27 / Washington North -124.79,47.08,-117.02,49.05 7 6267 +proj=lcc +lat_0=47 +lon_0=-120.833333333333 +lat_1=47.5 +lat_2=48.7333333333333 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +towgs84=-8,159,175 +to_meter=0.304800609601219 +no_defs +32049 projected NAD27 / Washington South -124.4,45.54,-116.91,47.96 7 6267 +proj=lcc +lat_0=45.3333333333333 +lon_0=-120.5 +lat_1=45.8333333333333 +lat_2=47.3333333333333 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +towgs84=-8,159,175 +to_meter=0.304800609601219 +no_defs +32050 projected NAD27 / West Virginia North -81.76,38.76,-77.72,40.64 10 6267 +proj=lcc +lat_0=38.5 +lon_0=-79.5 +lat_1=39 +lat_2=40.25 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +towgs84=-8,160,176 +to_meter=0.304800609601219 +no_defs +32051 projected NAD27 / West Virginia South -82.65,37.2,-79.05,39.17 10 6267 +proj=lcc +lat_0=37 +lon_0=-81 +lat_1=37.4833333333333 +lat_2=38.8833333333333 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +towgs84=-8,160,176 +to_meter=0.304800609601219 +no_defs +32052 projected NAD27 / Wisconsin North -92.89,45.37,-88.05,47.31 10 6267 +proj=lcc +lat_0=45.1666666666667 +lon_0=-90 +lat_1=45.5666666666667 +lat_2=46.7666666666667 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +towgs84=-8,160,176 +to_meter=0.304800609601219 +no_defs +32053 projected NAD27 / Wisconsin Central -92.89,43.98,-86.25,45.8 10 6267 +proj=lcc +lat_0=43.8333333333333 +lon_0=-90 +lat_1=44.25 +lat_2=45.5 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +towgs84=-8,160,176 +to_meter=0.304800609601219 +no_defs +32054 projected NAD27 / Wisconsin South -91.43,42.48,-86.95,44.33 10 6267 +proj=lcc +lat_0=42 +lon_0=-90 +lat_1=42.7333333333333 +lat_2=44.0666666666667 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +towgs84=-8,160,176 +to_meter=0.304800609601219 +no_defs +32055 projected NAD27 / Wyoming East -106.33,40.99,-104.05,45.01 7 6267 +proj=tmerc +lat_0=40.6666666666667 +lon_0=-105.166666666667 +k=0.999941177 +x_0=152400.30480061 +y_0=0 +ellps=clrk66 +towgs84=-8,159,175 +to_meter=0.304800609601219 +no_defs +32056 projected NAD27 / Wyoming East Central -108.63,40.99,-106,45.01 7 6267 +proj=tmerc +lat_0=40.6666666666667 +lon_0=-107.333333333333 +k=0.999941177 +x_0=152400.30480061 +y_0=0 +ellps=clrk66 +towgs84=-8,159,175 +to_meter=0.304800609601219 +no_defs +32057 projected NAD27 / Wyoming West Central -111.06,40.99,-107.5,45.01 7 6267 +proj=tmerc +lat_0=40.6666666666667 +lon_0=-108.75 +k=0.999941177 +x_0=152400.30480061 +y_0=0 +ellps=clrk66 +towgs84=-8,159,175 +to_meter=0.304800609601219 +no_defs +32058 projected NAD27 / Wyoming West -111.06,40.99,-109.04,44.67 7 6267 +proj=tmerc +lat_0=40.6666666666667 +lon_0=-110.083333333333 +k=0.999941177 +x_0=152400.30480061 +y_0=0 +ellps=clrk66 +towgs84=-8,159,175 +to_meter=0.304800609601219 +no_defs +32064 projected NAD27 / BLM 14N (ftUS) -102,25.83,-95.87,49.01 7 6267 +proj=tmerc +lat_0=0 +lon_0=-99 +k=0.9996 +x_0=500000.001016002 +y_0=0 +ellps=clrk66 +towgs84=-8,159,175 +to_meter=0.304800609601219 +no_defs +32065 projected NAD27 / BLM 15N (ftUS) -96.01,25.61,-89.86,49.38 10 6267 +proj=tmerc +lat_0=0 +lon_0=-93 +k=0.9996 +x_0=500000.001016002 +y_0=0 +ellps=clrk66 +towgs84=-8,160,176 +to_meter=0.304800609601219 +no_defs +32066 projected NAD27 / BLM 16N (ftUS) -90.01,23.95,-83.91,48.32 10 6267 +proj=tmerc +lat_0=0 +lon_0=-87 +k=0.9996 +x_0=500000.001016002 +y_0=0 +ellps=clrk66 +towgs84=-8,160,176 +to_meter=0.304800609601219 +no_defs +32067 projected NAD27 / BLM 17N (ftUS) -84.09,23.81,-77.99,46.13 10 6267 +proj=tmerc +lat_0=0 +lon_0=-81 +k=0.9996 +x_0=500000.001016002 +y_0=0 +ellps=clrk66 +towgs84=-8,160,176 +to_meter=0.304800609601219 +no_defs +32081 projected NAD27 / MTM zone 1 -54.5,46.56,-52.54,49.89 9 6267 +proj=tmerc +lat_0=0 +lon_0=-53 +k=0.9999 +x_0=304800 +y_0=0 +ellps=clrk66 +towgs84=-22,160,190 +units=m +no_defs +32082 projected NAD27 / MTM zone 2 -57.5,46.81,-54.49,54.71 9 6267 +proj=tmerc +lat_0=0 +lon_0=-56 +k=0.9999 +x_0=304800 +y_0=0 +ellps=clrk66 +towgs84=-22,160,190 +units=m +no_defs +32083 projected NAD27 / MTM zone 3 -59.48,47.5,-57.5,50.54 9 6267 +proj=tmerc +lat_0=0 +lon_0=-58.5 +k=0.9999 +x_0=304800 +y_0=0 +ellps=clrk66 +towgs84=-22,160,190 +units=m +no_defs +32084 projected NAD27 / MTM zone 4 -63,52,-60,58.92 9 6267 +proj=tmerc +lat_0=0 +lon_0=-61.5 +k=0.9999 +x_0=304800 +y_0=0 +ellps=clrk66 +towgs84=-22,160,190 +units=m +no_defs +32085 projected NAD27 / MTM zone 5 -66,51.58,-63,60.52 8 6267 +proj=tmerc +lat_0=0 +lon_0=-64.5 +k=0.9999 +x_0=304800 +y_0=0 +ellps=clrk66 +towgs84=4,159,188 +units=m +no_defs +32086 projected NAD27 / MTM zone 6 -67.81,52.05,-66,55.34 8 6267 +proj=tmerc +lat_0=0 +lon_0=-67.5 +k=0.9999 +x_0=304800 +y_0=0 +ellps=clrk66 +towgs84=4,159,188 +units=m +no_defs +32098 projected NAD27 / Quebec Lambert -79.85,44.99,-57.1,62.62 9 6267 +proj=lcc +lat_0=44 +lon_0=-68.5 +lat_1=60 +lat_2=46 +x_0=0 +y_0=0 +ellps=clrk66 +towgs84=-22,160,190 +units=m +no_defs +32099 projected NAD27 / Louisiana Offshore -93.38,25.58,-86.53,29.35 8 6267 +proj=lcc +lat_0=25.6666666666667 +lon_0=-91.3333333333333 +lat_1=27.8333333333333 +lat_2=26.1666666666667 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +towgs84=-7,158,172 +to_meter=0.304800609601219 +no_defs +32100 projected NAD83 / Montana -116.07,44.35,-104.04,49.01 4 6269 +proj=lcc +lat_0=44.25 +lon_0=-109.5 +lat_1=49 +lat_2=45 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +32104 projected NAD83 / Nebraska -104.06,39.99,-95.3,43.01 4 6269 +proj=lcc +lat_0=39.8333333333333 +lon_0=-100 +lat_1=43 +lat_2=40 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +32107 projected NAD83 / Nevada East -117.01,34.99,-114.03,42 4 6269 +proj=tmerc +lat_0=34.75 +lon_0=-115.583333333333 +k=0.9999 +x_0=200000 +y_0=8000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +32108 projected NAD83 / Nevada Central -118.19,36,-114.99,41 4 6269 +proj=tmerc +lat_0=34.75 +lon_0=-116.666666666667 +k=0.9999 +x_0=500000 +y_0=6000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +32109 projected NAD83 / Nevada West -120,36.95,-116.99,42 4 6269 +proj=tmerc +lat_0=34.75 +lon_0=-118.583333333333 +k=0.9999 +x_0=800000 +y_0=4000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +32110 projected NAD83 / New Hampshire -72.56,42.69,-70.63,45.31 4 6269 +proj=tmerc +lat_0=42.5 +lon_0=-71.6666666666667 +k=0.999966667 +x_0=300000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +32111 projected NAD83 / New Jersey -75.6,38.87,-73.88,41.36 4 6269 +proj=tmerc +lat_0=38.8333333333333 +lon_0=-74.5 +k=0.9999 +x_0=150000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +32112 projected NAD83 / New Mexico East -105.72,32,-102.99,37 4 6269 +proj=tmerc +lat_0=31 +lon_0=-104.333333333333 +k=0.999909091 +x_0=165000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +32113 projected NAD83 / New Mexico Central -107.73,31.78,-104.84,37 4 6269 +proj=tmerc +lat_0=31 +lon_0=-106.25 +k=0.9999 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +32114 projected NAD83 / New Mexico West -109.06,31.33,-106.32,37 4 6269 +proj=tmerc +lat_0=31 +lon_0=-107.833333333333 +k=0.999916667 +x_0=830000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +32115 projected NAD83 / New York East -75.87,40.88,-73.23,45.02 4 6269 +proj=tmerc +lat_0=38.8333333333333 +lon_0=-74.5 +k=0.9999 +x_0=150000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +32116 projected NAD83 / New York Central -77.75,41.99,-75.06,44.41 4 6269 +proj=tmerc +lat_0=40 +lon_0=-76.5833333333333 +k=0.9999375 +x_0=250000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +32117 projected NAD83 / New York West -79.77,41.99,-77.36,43.64 4 6269 +proj=tmerc +lat_0=40 +lon_0=-78.5833333333333 +k=0.9999375 +x_0=350000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +32118 projected NAD83 / New York Long Island -74.26,40.47,-71.8,41.3 4 6269 +proj=lcc +lat_0=40.1666666666667 +lon_0=-74 +lat_1=41.0333333333333 +lat_2=40.6666666666667 +x_0=300000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +32119 projected NAD83 / North Carolina -84.33,33.83,-75.38,36.59 4 6269 +proj=lcc +lat_0=33.75 +lon_0=-79 +lat_1=36.1666666666667 +lat_2=34.3333333333333 +x_0=609601.22 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +32120 projected NAD83 / North Dakota North -104.07,47.15,-96.83,49.01 4 6269 +proj=lcc +lat_0=47 +lon_0=-100.5 +lat_1=48.7333333333333 +lat_2=47.4333333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +32121 projected NAD83 / North Dakota South -104.05,45.93,-96.55,47.83 4 6269 +proj=lcc +lat_0=45.6666666666667 +lon_0=-100.5 +lat_1=47.4833333333333 +lat_2=46.1833333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +32122 projected NAD83 / Ohio North -84.81,40.1,-80.51,42.33 4 6269 +proj=lcc +lat_0=39.6666666666667 +lon_0=-82.5 +lat_1=41.7 +lat_2=40.4333333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +32123 projected NAD83 / Ohio South -84.83,38.4,-80.7,40.36 4 6269 +proj=lcc +lat_0=38 +lon_0=-82.5 +lat_1=40.0333333333333 +lat_2=38.7333333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +32124 projected NAD83 / Oklahoma North -103,35.27,-94.42,37.01 4 6269 +proj=lcc +lat_0=35 +lon_0=-98 +lat_1=36.7666666666667 +lat_2=35.5666666666667 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +32125 projected NAD83 / Oklahoma South -100,33.62,-94.42,35.57 4 6269 +proj=lcc +lat_0=33.3333333333333 +lon_0=-98 +lat_1=35.2333333333333 +lat_2=33.9333333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +32126 projected NAD83 / Oregon North -124.17,43.95,-116.47,46.26 4 6269 +proj=lcc +lat_0=43.6666666666667 +lon_0=-120.5 +lat_1=46 +lat_2=44.3333333333333 +x_0=2500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +32127 projected NAD83 / Oregon South -124.6,41.98,-116.9,44.56 4 6269 +proj=lcc +lat_0=41.6666666666667 +lon_0=-120.5 +lat_1=44 +lat_2=42.3333333333333 +x_0=1500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +32128 projected NAD83 / Pennsylvania North -80.53,40.6,-74.7,42.53 4 6269 +proj=lcc +lat_0=40.1666666666667 +lon_0=-77.75 +lat_1=41.95 +lat_2=40.8833333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +32129 projected NAD83 / Pennsylvania South -80.53,39.71,-74.72,41.18 4 6269 +proj=lcc +lat_0=39.3333333333333 +lon_0=-77.75 +lat_1=40.9666666666667 +lat_2=39.9333333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +32130 projected NAD83 / Rhode Island -71.85,41.13,-71.08,42.02 4 6269 +proj=tmerc +lat_0=41.0833333333333 +lon_0=-71.5 +k=0.99999375 +x_0=100000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +32133 projected NAD83 / South Carolina -83.36,32.05,-78.52,35.21 4 6269 +proj=lcc +lat_0=31.8333333333333 +lon_0=-81 +lat_1=34.8333333333333 +lat_2=32.5 +x_0=609600 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +32134 projected NAD83 / South Dakota North -104.07,44.14,-96.45,45.95 4 6269 +proj=lcc +lat_0=43.8333333333333 +lon_0=-100 +lat_1=45.6833333333333 +lat_2=44.4166666666667 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +32135 projected NAD83 / South Dakota South -104.06,42.48,-96.43,44.79 4 6269 +proj=lcc +lat_0=42.3333333333333 +lon_0=-100.333333333333 +lat_1=44.4 +lat_2=42.8333333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +32136 projected NAD83 / Tennessee -90.31,34.98,-81.65,36.68 4 6269 +proj=lcc +lat_0=34.3333333333333 +lon_0=-86 +lat_1=36.4166666666667 +lat_2=35.25 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +32137 projected NAD83 / Texas North -103.03,34.3,-99.99,36.5 4 6269 +proj=lcc +lat_0=34 +lon_0=-101.5 +lat_1=36.1833333333333 +lat_2=34.65 +x_0=200000 +y_0=1000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +32138 projected NAD83 / Texas North Central -103.07,31.72,-94,34.58 4 6269 +proj=lcc +lat_0=31.6666666666667 +lon_0=-98.5 +lat_1=33.9666666666667 +lat_2=32.1333333333333 +x_0=600000 +y_0=2000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +32139 projected NAD83 / Texas Central -106.66,29.78,-93.5,32.27 4 6269 +proj=lcc +lat_0=29.6666666666667 +lon_0=-100.333333333333 +lat_1=31.8833333333333 +lat_2=30.1166666666667 +x_0=700000 +y_0=3000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +32140 projected NAD83 / Texas South Central -105,27.78,-93.76,30.67 4 6269 +proj=lcc +lat_0=27.8333333333333 +lon_0=-99 +lat_1=30.2833333333333 +lat_2=28.3833333333333 +x_0=600000 +y_0=4000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +32141 projected NAD83 / Texas South -100.2,25.83,-96.85,28.21 4 6269 +proj=lcc +lat_0=25.6666666666667 +lon_0=-98.5 +lat_1=27.8333333333333 +lat_2=26.1666666666667 +x_0=300000 +y_0=5000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +32142 projected NAD83 / Utah North -114.04,40.55,-109.04,42.01 4 6269 +proj=lcc +lat_0=40.3333333333333 +lon_0=-111.5 +lat_1=41.7833333333333 +lat_2=40.7166666666667 +x_0=500000 +y_0=1000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +32143 projected NAD83 / Utah Central -114.05,38.49,-109.04,41.08 4 6269 +proj=lcc +lat_0=38.3333333333333 +lon_0=-111.5 +lat_1=40.65 +lat_2=39.0166666666667 +x_0=500000 +y_0=2000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +32144 projected NAD83 / Utah South -114.05,36.99,-109.04,38.58 4 6269 +proj=lcc +lat_0=36.6666666666667 +lon_0=-111.5 +lat_1=38.35 +lat_2=37.2166666666667 +x_0=500000 +y_0=3000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +32145 projected NAD83 / Vermont -73.44,42.72,-71.5,45.03 4 6269 +proj=tmerc +lat_0=42.5 +lon_0=-72.5 +k=0.999964286 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +32146 projected NAD83 / Virginia North -80.06,37.77,-76.51,39.46 4 6269 +proj=lcc +lat_0=37.6666666666667 +lon_0=-78.5 +lat_1=39.2 +lat_2=38.0333333333333 +x_0=3500000 +y_0=2000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +32147 projected NAD83 / Virginia South -83.68,36.54,-75.31,38.28 4 6269 +proj=lcc +lat_0=36.3333333333333 +lon_0=-78.5 +lat_1=37.9666666666667 +lat_2=36.7666666666667 +x_0=3500000 +y_0=1000000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +32148 projected NAD83 / Washington North -124.79,47.08,-117.02,49.05 4 6269 +proj=lcc +lat_0=47 +lon_0=-120.833333333333 +lat_1=48.7333333333333 +lat_2=47.5 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +32149 projected NAD83 / Washington South -124.4,45.54,-116.91,47.61 4 6269 +proj=lcc +lat_0=45.3333333333333 +lon_0=-120.5 +lat_1=47.3333333333333 +lat_2=45.8333333333333 +x_0=500000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +32150 projected NAD83 / West Virginia North -81.76,38.76,-77.72,40.64 4 6269 +proj=lcc +lat_0=38.5 +lon_0=-79.5 +lat_1=40.25 +lat_2=39 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +32151 projected NAD83 / West Virginia South -82.65,37.2,-79.05,39.17 4 6269 +proj=lcc +lat_0=37 +lon_0=-81 +lat_1=38.8833333333333 +lat_2=37.4833333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +32152 projected NAD83 / Wisconsin North -92.89,45.37,-88.05,47.31 4 6269 +proj=lcc +lat_0=45.1666666666667 +lon_0=-90 +lat_1=46.7666666666667 +lat_2=45.5666666666667 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +32153 projected NAD83 / Wisconsin Central -92.89,43.98,-86.25,45.8 4 6269 +proj=lcc +lat_0=43.8333333333333 +lon_0=-90 +lat_1=45.5 +lat_2=44.25 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +32154 projected NAD83 / Wisconsin South -91.43,42.48,-86.95,44.33 4 6269 +proj=lcc +lat_0=42 +lon_0=-90 +lat_1=44.0666666666667 +lat_2=42.7333333333333 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +32155 projected NAD83 / Wyoming East -106.33,40.99,-104.05,45.01 4 6269 +proj=tmerc +lat_0=40.5 +lon_0=-105.166666666667 +k=0.9999375 +x_0=200000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +32156 projected NAD83 / Wyoming East Central -108.63,40.99,-106,45.01 4 6269 +proj=tmerc +lat_0=40.5 +lon_0=-107.333333333333 +k=0.9999375 +x_0=400000 +y_0=100000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +32157 projected NAD83 / Wyoming West Central -111.06,40.99,-107.5,45.01 4 6269 +proj=tmerc +lat_0=40.5 +lon_0=-108.75 +k=0.9999375 +x_0=600000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +32158 projected NAD83 / Wyoming West -111.06,40.99,-109.04,44.67 4 6269 +proj=tmerc +lat_0=40.5 +lon_0=-110.083333333333 +k=0.9999375 +x_0=800000 +y_0=100000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +32159 projected NAD83 / Wyoming Lambert -111.06,40.99,-104.05,45.01 4 6269 +proj=lcc +lat_0=41 +lon_0=-107.5 +lat_1=41 +lat_2=45 +x_0=500000 +y_0=200000 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +32161 projected NAD83 / Puerto Rico & Virgin Is. -68.49,14.92,-63.88,21.86 6269 +proj=lcc +lat_0=17.8333333333333 +lon_0=-66.4333333333333 +lat_1=18.4333333333333 +lat_2=18.0333333333333 +x_0=200000 +y_0=200000 +ellps=GRS80 +units=m +no_defs +32164 projected NAD83 / BLM 14N (ftUS) -102,25.83,-95.87,49.01 4 6269 +proj=tmerc +lat_0=0 +lon_0=-99 +k=0.9996 +x_0=500000.001016002 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +32165 projected NAD83 / BLM 15N (ftUS) -96.01,25.61,-89.86,49.38 4 6269 +proj=tmerc +lat_0=0 +lon_0=-93 +k=0.9996 +x_0=500000.001016002 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +32166 projected NAD83 / BLM 16N (ftUS) -90.01,23.95,-83.91,48.32 4 6269 +proj=tmerc +lat_0=0 +lon_0=-87 +k=0.9996 +x_0=500000.001016002 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +32167 projected NAD83 / BLM 17N (ftUS) -84.09,23.81,-77.99,46.13 4 6269 +proj=tmerc +lat_0=0 +lon_0=-81 +k=0.9996 +x_0=500000.001016002 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +to_meter=0.304800609601219 +no_defs +32181 projected NAD83 / MTM zone 1 -54.5,46.56,-52.54,49.89 4 6269 +proj=tmerc +lat_0=0 +lon_0=-53 +k=0.9999 +x_0=304800 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +32182 projected NAD83 / MTM zone 2 -57.5,46.81,-54.49,54.71 4 6269 +proj=tmerc +lat_0=0 +lon_0=-56 +k=0.9999 +x_0=304800 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +32183 projected NAD83 / MTM zone 3 -60,47.5,-57.1,55.38 4 6269 +proj=tmerc +lat_0=0 +lon_0=-58.5 +k=0.9999 +x_0=304800 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +32184 projected NAD83 / MTM zone 4 -63,47.16,-60,58.92 4 6269 +proj=tmerc +lat_0=0 +lon_0=-61.5 +k=0.9999 +x_0=304800 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +32185 projected NAD83 / MTM zone 5 -66,47.95,-63,60.52 4 6269 +proj=tmerc +lat_0=0 +lon_0=-64.5 +k=0.9999 +x_0=304800 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +32186 projected NAD83 / MTM zone 6 -69,47.31,-66,59 4 6269 +proj=tmerc +lat_0=0 +lon_0=-67.5 +k=0.9999 +x_0=304800 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +32187 projected NAD83 / MTM zone 7 -72,45.01,-69,61.8 4 6269 +proj=tmerc +lat_0=0 +lon_0=-70.5 +k=0.9999 +x_0=304800 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +32188 projected NAD83 / MTM zone 8 -75,44.98,-72,62.53 4 6269 +proj=tmerc +lat_0=0 +lon_0=-73.5 +k=0.9999 +x_0=304800 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +32189 projected NAD83 / MTM zone 9 -78,43.63,-75,62.65 4 6269 +proj=tmerc +lat_0=0 +lon_0=-76.5 +k=0.9999 +x_0=304800 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +32190 projected NAD83 / MTM zone 10 -81,42.26,-78,62.45 4 6269 +proj=tmerc +lat_0=0 +lon_0=-79.5 +k=0.9999 +x_0=304800 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +32191 projected NAD83 / MTM zone 11 -83.6,41.67,-81,46 4 6269 +proj=tmerc +lat_0=0 +lon_0=-82.5 +k=0.9999 +x_0=304800 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +32192 projected NAD83 / MTM zone 12 -82.5,46,-79.5,55.21 4 6269 +proj=tmerc +lat_0=0 +lon_0=-81 +k=0.9999 +x_0=304800 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +32193 projected NAD83 / MTM zone 13 -85.5,46,-82.5,55.59 4 6269 +proj=tmerc +lat_0=0 +lon_0=-84 +k=0.9999 +x_0=304800 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +32194 projected NAD83 / MTM zone 14 -88.5,47.17,-85.5,56.7 4 6269 +proj=tmerc +lat_0=0 +lon_0=-87 +k=0.9999 +x_0=304800 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +32195 projected NAD83 / MTM zone 15 -91.5,47.97,-88.5,56.9 4 6269 +proj=tmerc +lat_0=0 +lon_0=-90 +k=0.9999 +x_0=304800 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +32196 projected NAD83 / MTM zone 16 -94.5,48.06,-91.5,55.2 4 6269 +proj=tmerc +lat_0=0 +lon_0=-93 +k=0.9999 +x_0=304800 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +32197 projected NAD83 / MTM zone 17 -95.16,48.69,-94.5,53.24 4 6269 +proj=tmerc +lat_0=0 +lon_0=-96 +k=0.9999 +x_0=304800 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +32198 projected NAD83 / Quebec Lambert -79.85,44.99,-57.1,62.62 4 6269 +proj=lcc +lat_0=44 +lon_0=-68.5 +lat_1=60 +lat_2=46 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +32199 projected NAD83 / Louisiana Offshore -93.38,25.58,-86.53,29.35 4 6269 +proj=lcc +lat_0=25.5 +lon_0=-91.3333333333333 +lat_1=27.8333333333333 +lat_2=26.1666666666667 +x_0=1000000 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +32201 projected WGS 72 / UTM zone 1N -180,0,-174,84 2 6322 +proj=utm +zone=1 +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32202 projected WGS 72 / UTM zone 2N -174,0,-168,84 2 6322 +proj=utm +zone=2 +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32203 projected WGS 72 / UTM zone 3N -168,0,-162,84 2 6322 +proj=utm +zone=3 +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32204 projected WGS 72 / UTM zone 4N -162,0,-156,84 2 6322 +proj=utm +zone=4 +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32205 projected WGS 72 / UTM zone 5N -156,0,-150,84 2 6322 +proj=utm +zone=5 +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32206 projected WGS 72 / UTM zone 6N -150,0,-144,84 2 6322 +proj=utm +zone=6 +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32207 projected WGS 72 / UTM zone 7N -144,0,-138,84 2 6322 +proj=utm +zone=7 +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32208 projected WGS 72 / UTM zone 8N -138,0,-132,84 2 6322 +proj=utm +zone=8 +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32209 projected WGS 72 / UTM zone 9N -132,0,-126,84 2 6322 +proj=utm +zone=9 +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32210 projected WGS 72 / UTM zone 10N -126,0,-120,84 2 6322 +proj=utm +zone=10 +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32211 projected WGS 72 / UTM zone 11N -120,0,-114,84 2 6322 +proj=utm +zone=11 +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32212 projected WGS 72 / UTM zone 12N -114,0,-108,84 2 6322 +proj=utm +zone=12 +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32213 projected WGS 72 / UTM zone 13N -108,0,-102,84 2 6322 +proj=utm +zone=13 +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32214 projected WGS 72 / UTM zone 14N -102,0,-96,84 2 6322 +proj=utm +zone=14 +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32215 projected WGS 72 / UTM zone 15N -96,0,-90,84 2 6322 +proj=utm +zone=15 +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32216 projected WGS 72 / UTM zone 16N -90,0,-84,84 2 6322 +proj=utm +zone=16 +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32217 projected WGS 72 / UTM zone 17N -84,0,-78,84 2 6322 +proj=utm +zone=17 +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32218 projected WGS 72 / UTM zone 18N -78,0,-72,84 2 6322 +proj=utm +zone=18 +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32219 projected WGS 72 / UTM zone 19N -72,0,-66,84 2 6322 +proj=utm +zone=19 +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32220 projected WGS 72 / UTM zone 20N -66,0,-60,84 2 6322 +proj=utm +zone=20 +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32221 projected WGS 72 / UTM zone 21N -60,0,-54,84 2 6322 +proj=utm +zone=21 +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32222 projected WGS 72 / UTM zone 22N -54,0,-48,84 2 6322 +proj=utm +zone=22 +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32223 projected WGS 72 / UTM zone 23N -48,0,-42,84 2 6322 +proj=utm +zone=23 +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32224 projected WGS 72 / UTM zone 24N -42,0,-36,84 2 6322 +proj=utm +zone=24 +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32225 projected WGS 72 / UTM zone 25N -36,0,-30,84 2 6322 +proj=utm +zone=25 +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32226 projected WGS 72 / UTM zone 26N -30,0,-24,84 2 6322 +proj=utm +zone=26 +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32227 projected WGS 72 / UTM zone 27N -24,0,-18,84 2 6322 +proj=utm +zone=27 +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32228 projected WGS 72 / UTM zone 28N -18,0,-12,84 2 6322 +proj=utm +zone=28 +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32229 projected WGS 72 / UTM zone 29N -12,0,-6,84 2 6322 +proj=utm +zone=29 +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32230 projected WGS 72 / UTM zone 30N -6,0,0,84 2 6322 +proj=utm +zone=30 +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32231 projected WGS 72 / UTM zone 31N 0,0,6,84 2 6322 +proj=utm +zone=31 +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32232 projected WGS 72 / UTM zone 32N 6,0,12,84 2 6322 +proj=utm +zone=32 +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32233 projected WGS 72 / UTM zone 33N 12,0,18,84 2 6322 +proj=utm +zone=33 +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32234 projected WGS 72 / UTM zone 34N 18,0,24,84 2 6322 +proj=utm +zone=34 +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32235 projected WGS 72 / UTM zone 35N 24,0,30,84 2 6322 +proj=utm +zone=35 +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32236 projected WGS 72 / UTM zone 36N 30,0,36,84 2 6322 +proj=utm +zone=36 +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32237 projected WGS 72 / UTM zone 37N 36,0,42,84 2 6322 +proj=utm +zone=37 +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32238 projected WGS 72 / UTM zone 38N 42,0,48,84 2 6322 +proj=utm +zone=38 +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32239 projected WGS 72 / UTM zone 39N 48,0,54,84 2 6322 +proj=utm +zone=39 +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32240 projected WGS 72 / UTM zone 40N 54,0,60,84 2 6322 +proj=utm +zone=40 +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32241 projected WGS 72 / UTM zone 41N 60,0,66,84 2 6322 +proj=utm +zone=41 +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32242 projected WGS 72 / UTM zone 42N 66,0,72,84 2 6322 +proj=utm +zone=42 +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32243 projected WGS 72 / UTM zone 43N 72,0,78,84 2 6322 +proj=utm +zone=43 +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32244 projected WGS 72 / UTM zone 44N 78,0,84,84 2 6322 +proj=utm +zone=44 +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32245 projected WGS 72 / UTM zone 45N 84,0,90,84 2 6322 +proj=utm +zone=45 +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32246 projected WGS 72 / UTM zone 46N 90,0,96,84 2 6322 +proj=utm +zone=46 +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32247 projected WGS 72 / UTM zone 47N 96,0,102,84 2 6322 +proj=utm +zone=47 +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32248 projected WGS 72 / UTM zone 48N 102,0,108,84 2 6322 +proj=utm +zone=48 +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32249 projected WGS 72 / UTM zone 49N 108,0,114,84 2 6322 +proj=utm +zone=49 +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32250 projected WGS 72 / UTM zone 50N 114,0,120,84 2 6322 +proj=utm +zone=50 +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32251 projected WGS 72 / UTM zone 51N 120,0,126,84 2 6322 +proj=utm +zone=51 +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32252 projected WGS 72 / UTM zone 52N 126,0,132,84 2 6322 +proj=utm +zone=52 +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32253 projected WGS 72 / UTM zone 53N 132,0,138,84 2 6322 +proj=utm +zone=53 +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32254 projected WGS 72 / UTM zone 54N 138,0,144,84 2 6322 +proj=utm +zone=54 +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32255 projected WGS 72 / UTM zone 55N 144,0,150,84 2 6322 +proj=utm +zone=55 +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32256 projected WGS 72 / UTM zone 56N 150,0,156,84 2 6322 +proj=utm +zone=56 +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32257 projected WGS 72 / UTM zone 57N 156,0,162,84 2 6322 +proj=utm +zone=57 +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32258 projected WGS 72 / UTM zone 58N 162,0,168,84 2 6322 +proj=utm +zone=58 +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32259 projected WGS 72 / UTM zone 59N 168,0,174,84 2 6322 +proj=utm +zone=59 +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32260 projected WGS 72 / UTM zone 60N 174,0,180,84 2 6322 +proj=utm +zone=60 +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32301 projected WGS 72 / UTM zone 1S -180,-80,-174,0 2 6322 +proj=utm +zone=1 +south +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32302 projected WGS 72 / UTM zone 2S -174,-80,-168,0 2 6322 +proj=utm +zone=2 +south +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32303 projected WGS 72 / UTM zone 3S -168,-80,-162,0 2 6322 +proj=utm +zone=3 +south +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32304 projected WGS 72 / UTM zone 4S -162,-80,-156,0 2 6322 +proj=utm +zone=4 +south +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32305 projected WGS 72 / UTM zone 5S -156,-80,-150,0 2 6322 +proj=utm +zone=5 +south +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32306 projected WGS 72 / UTM zone 6S -150,-80,-144,0 2 6322 +proj=utm +zone=6 +south +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32307 projected WGS 72 / UTM zone 7S -144,-80,-138,0 2 6322 +proj=utm +zone=7 +south +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32308 projected WGS 72 / UTM zone 8S -138,-80,-132,0 2 6322 +proj=utm +zone=8 +south +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32309 projected WGS 72 / UTM zone 9S -132,-80,-126,0 2 6322 +proj=utm +zone=9 +south +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32310 projected WGS 72 / UTM zone 10S -126,-80,-120,0 2 6322 +proj=utm +zone=10 +south +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32311 projected WGS 72 / UTM zone 11S -120,-80,-114,0 2 6322 +proj=utm +zone=11 +south +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32312 projected WGS 72 / UTM zone 12S -114,-80,-108,0 2 6322 +proj=utm +zone=12 +south +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32313 projected WGS 72 / UTM zone 13S -108,-80,-102,0 2 6322 +proj=utm +zone=13 +south +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32314 projected WGS 72 / UTM zone 14S -102,-80,-96,0 2 6322 +proj=utm +zone=14 +south +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32315 projected WGS 72 / UTM zone 15S -96,-80,-90,0 2 6322 +proj=utm +zone=15 +south +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32316 projected WGS 72 / UTM zone 16S -90,-80,-84,0 2 6322 +proj=utm +zone=16 +south +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32317 projected WGS 72 / UTM zone 17S -84,-80,-78,0 2 6322 +proj=utm +zone=17 +south +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32318 projected WGS 72 / UTM zone 18S -78,-80,-72,0 2 6322 +proj=utm +zone=18 +south +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32319 projected WGS 72 / UTM zone 19S -72,-80,-66,0 2 6322 +proj=utm +zone=19 +south +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32320 projected WGS 72 / UTM zone 20S -66,-80,-60,0 2 6322 +proj=utm +zone=20 +south +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32321 projected WGS 72 / UTM zone 21S -60,-80,-54,0 2 6322 +proj=utm +zone=21 +south +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32322 projected WGS 72 / UTM zone 22S -54,-80,-48,0 2 6322 +proj=utm +zone=22 +south +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32323 projected WGS 72 / UTM zone 23S -48,-80,-42,0 2 6322 +proj=utm +zone=23 +south +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32324 projected WGS 72 / UTM zone 24S -42,-80,-36,0 2 6322 +proj=utm +zone=24 +south +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32325 projected WGS 72 / UTM zone 25S -36,-80,-30,0 2 6322 +proj=utm +zone=25 +south +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32326 projected WGS 72 / UTM zone 26S -30,-80,-24,0 2 6322 +proj=utm +zone=26 +south +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32327 projected WGS 72 / UTM zone 27S -24,-80,-18,0 2 6322 +proj=utm +zone=27 +south +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32328 projected WGS 72 / UTM zone 28S -18,-80,-12,0 2 6322 +proj=utm +zone=28 +south +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32329 projected WGS 72 / UTM zone 29S -12,-80,-6,0 2 6322 +proj=utm +zone=29 +south +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32330 projected WGS 72 / UTM zone 30S -6,-80,0,0 2 6322 +proj=utm +zone=30 +south +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32331 projected WGS 72 / UTM zone 31S 0,-80,6,0 2 6322 +proj=utm +zone=31 +south +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32332 projected WGS 72 / UTM zone 32S 6,-80,12,0 2 6322 +proj=utm +zone=32 +south +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32333 projected WGS 72 / UTM zone 33S 12,-80,18,0 2 6322 +proj=utm +zone=33 +south +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32334 projected WGS 72 / UTM zone 34S 18,-80,24,0 2 6322 +proj=utm +zone=34 +south +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32335 projected WGS 72 / UTM zone 35S 24,-80,30,0 2 6322 +proj=utm +zone=35 +south +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32336 projected WGS 72 / UTM zone 36S 30,-80,36,0 2 6322 +proj=utm +zone=36 +south +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32337 projected WGS 72 / UTM zone 37S 36,-80,42,0 2 6322 +proj=utm +zone=37 +south +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32338 projected WGS 72 / UTM zone 38S 42,-80,48,0 2 6322 +proj=utm +zone=38 +south +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32339 projected WGS 72 / UTM zone 39S 48,-80,54,0 2 6322 +proj=utm +zone=39 +south +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32340 projected WGS 72 / UTM zone 40S 54,-80,60,0 2 6322 +proj=utm +zone=40 +south +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32341 projected WGS 72 / UTM zone 41S 60,-80,66,0 2 6322 +proj=utm +zone=41 +south +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32342 projected WGS 72 / UTM zone 42S 66,-80,72,0 2 6322 +proj=utm +zone=42 +south +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32343 projected WGS 72 / UTM zone 43S 72,-80,78,0 2 6322 +proj=utm +zone=43 +south +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32344 projected WGS 72 / UTM zone 44S 78,-80,84,0 2 6322 +proj=utm +zone=44 +south +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32345 projected WGS 72 / UTM zone 45S 84,-80,90,0 2 6322 +proj=utm +zone=45 +south +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32346 projected WGS 72 / UTM zone 46S 90,-80,96,0 2 6322 +proj=utm +zone=46 +south +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32347 projected WGS 72 / UTM zone 47S 96,-80,102,0 2 6322 +proj=utm +zone=47 +south +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32348 projected WGS 72 / UTM zone 48S 102,-80,108,0 2 6322 +proj=utm +zone=48 +south +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32349 projected WGS 72 / UTM zone 49S 108,-80,114,0 2 6322 +proj=utm +zone=49 +south +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32350 projected WGS 72 / UTM zone 50S 114,-80,120,0 2 6322 +proj=utm +zone=50 +south +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32351 projected WGS 72 / UTM zone 51S 120,-80,126,0 2 6322 +proj=utm +zone=51 +south +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32352 projected WGS 72 / UTM zone 52S 126,-80,132,0 2 6322 +proj=utm +zone=52 +south +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32353 projected WGS 72 / UTM zone 53S 132,-80,138,0 2 6322 +proj=utm +zone=53 +south +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32354 projected WGS 72 / UTM zone 54S 138,-80,144,0 2 6322 +proj=utm +zone=54 +south +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32355 projected WGS 72 / UTM zone 55S 144,-80,150,0 2 6322 +proj=utm +zone=55 +south +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32356 projected WGS 72 / UTM zone 56S 150,-80,156,0 2 6322 +proj=utm +zone=56 +south +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32357 projected WGS 72 / UTM zone 57S 156,-80,162,0 2 6322 +proj=utm +zone=57 +south +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32358 projected WGS 72 / UTM zone 58S 162,-80,168,0 2 6322 +proj=utm +zone=58 +south +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32359 projected WGS 72 / UTM zone 59S 168,-80,174,0 2 6322 +proj=utm +zone=59 +south +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32360 projected WGS 72 / UTM zone 60S 174,-80,180,0 2 6322 +proj=utm +zone=60 +south +ellps=WGS72 +towgs84=0,0,4.5,0,0,0.554,0.219 +units=m +no_defs +32401 projected WGS 72BE / UTM zone 1N -180,0,-174,84 2 6324 +proj=utm +zone=1 +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32402 projected WGS 72BE / UTM zone 2N -174,0,-168,84 2 6324 +proj=utm +zone=2 +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32403 projected WGS 72BE / UTM zone 3N -168,0,-162,84 2 6324 +proj=utm +zone=3 +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32404 projected WGS 72BE / UTM zone 4N -162,0,-156,84 2 6324 +proj=utm +zone=4 +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32405 projected WGS 72BE / UTM zone 5N -156,0,-150,84 2 6324 +proj=utm +zone=5 +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32406 projected WGS 72BE / UTM zone 6N -150,0,-144,84 2 6324 +proj=utm +zone=6 +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32407 projected WGS 72BE / UTM zone 7N -144,0,-138,84 2 6324 +proj=utm +zone=7 +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32408 projected WGS 72BE / UTM zone 8N -138,0,-132,84 2 6324 +proj=utm +zone=8 +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32409 projected WGS 72BE / UTM zone 9N -132,0,-126,84 2 6324 +proj=utm +zone=9 +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32410 projected WGS 72BE / UTM zone 10N -126,0,-120,84 2 6324 +proj=utm +zone=10 +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32411 projected WGS 72BE / UTM zone 11N -120,0,-114,84 2 6324 +proj=utm +zone=11 +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32412 projected WGS 72BE / UTM zone 12N -114,0,-108,84 2 6324 +proj=utm +zone=12 +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32413 projected WGS 72BE / UTM zone 13N -108,0,-102,84 2 6324 +proj=utm +zone=13 +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32414 projected WGS 72BE / UTM zone 14N -102,0,-96,84 2 6324 +proj=utm +zone=14 +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32415 projected WGS 72BE / UTM zone 15N -96,0,-90,84 2 6324 +proj=utm +zone=15 +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32416 projected WGS 72BE / UTM zone 16N -90,0,-84,84 2 6324 +proj=utm +zone=16 +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32417 projected WGS 72BE / UTM zone 17N -84,0,-78,84 2 6324 +proj=utm +zone=17 +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32418 projected WGS 72BE / UTM zone 18N -78,0,-72,84 2 6324 +proj=utm +zone=18 +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32419 projected WGS 72BE / UTM zone 19N -72,0,-66,84 2 6324 +proj=utm +zone=19 +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32420 projected WGS 72BE / UTM zone 20N -66,0,-60,84 2 6324 +proj=utm +zone=20 +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32421 projected WGS 72BE / UTM zone 21N -60,0,-54,84 2 6324 +proj=utm +zone=21 +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32422 projected WGS 72BE / UTM zone 22N -54,0,-48,84 2 6324 +proj=utm +zone=22 +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32423 projected WGS 72BE / UTM zone 23N -48,0,-42,84 2 6324 +proj=utm +zone=23 +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32424 projected WGS 72BE / UTM zone 24N -42,0,-36,84 2 6324 +proj=utm +zone=24 +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32425 projected WGS 72BE / UTM zone 25N -36,0,-30,84 2 6324 +proj=utm +zone=25 +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32426 projected WGS 72BE / UTM zone 26N -30,0,-24,84 2 6324 +proj=utm +zone=26 +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32427 projected WGS 72BE / UTM zone 27N -24,0,-18,84 2 6324 +proj=utm +zone=27 +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32428 projected WGS 72BE / UTM zone 28N -18,0,-12,84 2 6324 +proj=utm +zone=28 +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32429 projected WGS 72BE / UTM zone 29N -12,0,-6,84 2 6324 +proj=utm +zone=29 +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32430 projected WGS 72BE / UTM zone 30N -6,0,0,84 2 6324 +proj=utm +zone=30 +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32431 projected WGS 72BE / UTM zone 31N 0,0,6,84 2 6324 +proj=utm +zone=31 +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32432 projected WGS 72BE / UTM zone 32N 6,0,12,84 2 6324 +proj=utm +zone=32 +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32433 projected WGS 72BE / UTM zone 33N 12,0,18,84 2 6324 +proj=utm +zone=33 +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32434 projected WGS 72BE / UTM zone 34N 18,0,24,84 2 6324 +proj=utm +zone=34 +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32435 projected WGS 72BE / UTM zone 35N 24,0,30,84 2 6324 +proj=utm +zone=35 +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32436 projected WGS 72BE / UTM zone 36N 30,0,36,84 2 6324 +proj=utm +zone=36 +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32437 projected WGS 72BE / UTM zone 37N 36,0,42,84 2 6324 +proj=utm +zone=37 +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32438 projected WGS 72BE / UTM zone 38N 42,0,48,84 2 6324 +proj=utm +zone=38 +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32439 projected WGS 72BE / UTM zone 39N 48,0,54,84 2 6324 +proj=utm +zone=39 +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32440 projected WGS 72BE / UTM zone 40N 54,0,60,84 2 6324 +proj=utm +zone=40 +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32441 projected WGS 72BE / UTM zone 41N 60,0,66,84 2 6324 +proj=utm +zone=41 +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32442 projected WGS 72BE / UTM zone 42N 66,0,72,84 2 6324 +proj=utm +zone=42 +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32443 projected WGS 72BE / UTM zone 43N 72,0,78,84 2 6324 +proj=utm +zone=43 +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32444 projected WGS 72BE / UTM zone 44N 78,0,84,84 2 6324 +proj=utm +zone=44 +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32445 projected WGS 72BE / UTM zone 45N 84,0,90,84 2 6324 +proj=utm +zone=45 +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32446 projected WGS 72BE / UTM zone 46N 90,0,96,84 2 6324 +proj=utm +zone=46 +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32447 projected WGS 72BE / UTM zone 47N 96,0,102,84 2 6324 +proj=utm +zone=47 +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32448 projected WGS 72BE / UTM zone 48N 102,0,108,84 2 6324 +proj=utm +zone=48 +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32449 projected WGS 72BE / UTM zone 49N 108,0,114,84 2 6324 +proj=utm +zone=49 +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32450 projected WGS 72BE / UTM zone 50N 114,0,120,84 2 6324 +proj=utm +zone=50 +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32451 projected WGS 72BE / UTM zone 51N 120,0,126,84 2 6324 +proj=utm +zone=51 +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32452 projected WGS 72BE / UTM zone 52N 126,0,132,84 2 6324 +proj=utm +zone=52 +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32453 projected WGS 72BE / UTM zone 53N 132,0,138,84 2 6324 +proj=utm +zone=53 +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32454 projected WGS 72BE / UTM zone 54N 138,0,144,84 2 6324 +proj=utm +zone=54 +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32455 projected WGS 72BE / UTM zone 55N 144,0,150,84 2 6324 +proj=utm +zone=55 +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32456 projected WGS 72BE / UTM zone 56N 150,0,156,84 2 6324 +proj=utm +zone=56 +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32457 projected WGS 72BE / UTM zone 57N 156,0,162,84 2 6324 +proj=utm +zone=57 +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32458 projected WGS 72BE / UTM zone 58N 162,0,168,84 2 6324 +proj=utm +zone=58 +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32459 projected WGS 72BE / UTM zone 59N 168,0,174,84 2 6324 +proj=utm +zone=59 +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32460 projected WGS 72BE / UTM zone 60N 174,0,180,84 2 6324 +proj=utm +zone=60 +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32501 projected WGS 72BE / UTM zone 1S -180,-80,-174,0 2 6324 +proj=utm +zone=1 +south +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32502 projected WGS 72BE / UTM zone 2S -174,-80,-168,0 2 6324 +proj=utm +zone=2 +south +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32503 projected WGS 72BE / UTM zone 3S -168,-80,-162,0 2 6324 +proj=utm +zone=3 +south +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32504 projected WGS 72BE / UTM zone 4S -162,-80,-156,0 2 6324 +proj=utm +zone=4 +south +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32505 projected WGS 72BE / UTM zone 5S -156,-80,-150,0 2 6324 +proj=utm +zone=5 +south +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32506 projected WGS 72BE / UTM zone 6S -150,-80,-144,0 2 6324 +proj=utm +zone=6 +south +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32507 projected WGS 72BE / UTM zone 7S -144,-80,-138,0 2 6324 +proj=utm +zone=7 +south +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32508 projected WGS 72BE / UTM zone 8S -138,-80,-132,0 2 6324 +proj=utm +zone=8 +south +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32509 projected WGS 72BE / UTM zone 9S -132,-80,-126,0 2 6324 +proj=utm +zone=9 +south +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32510 projected WGS 72BE / UTM zone 10S -126,-80,-120,0 2 6324 +proj=utm +zone=10 +south +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32511 projected WGS 72BE / UTM zone 11S -120,-80,-114,0 2 6324 +proj=utm +zone=11 +south +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32512 projected WGS 72BE / UTM zone 12S -114,-80,-108,0 2 6324 +proj=utm +zone=12 +south +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32513 projected WGS 72BE / UTM zone 13S -108,-80,-102,0 2 6324 +proj=utm +zone=13 +south +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32514 projected WGS 72BE / UTM zone 14S -102,-80,-96,0 2 6324 +proj=utm +zone=14 +south +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32515 projected WGS 72BE / UTM zone 15S -96,-80,-90,0 2 6324 +proj=utm +zone=15 +south +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32516 projected WGS 72BE / UTM zone 16S -90,-80,-84,0 2 6324 +proj=utm +zone=16 +south +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32517 projected WGS 72BE / UTM zone 17S -84,-80,-78,0 2 6324 +proj=utm +zone=17 +south +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32518 projected WGS 72BE / UTM zone 18S -78,-80,-72,0 2 6324 +proj=utm +zone=18 +south +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32519 projected WGS 72BE / UTM zone 19S -72,-80,-66,0 2 6324 +proj=utm +zone=19 +south +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32520 projected WGS 72BE / UTM zone 20S -66,-80,-60,0 2 6324 +proj=utm +zone=20 +south +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32521 projected WGS 72BE / UTM zone 21S -60,-80,-54,0 2 6324 +proj=utm +zone=21 +south +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32522 projected WGS 72BE / UTM zone 22S -54,-80,-48,0 2 6324 +proj=utm +zone=22 +south +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32523 projected WGS 72BE / UTM zone 23S -48,-80,-42,0 2 6324 +proj=utm +zone=23 +south +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32524 projected WGS 72BE / UTM zone 24S -42,-80,-36,0 2 6324 +proj=utm +zone=24 +south +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32525 projected WGS 72BE / UTM zone 25S -36,-80,-30,0 2 6324 +proj=utm +zone=25 +south +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32526 projected WGS 72BE / UTM zone 26S -30,-80,-24,0 2 6324 +proj=utm +zone=26 +south +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32527 projected WGS 72BE / UTM zone 27S -24,-80,-18,0 2 6324 +proj=utm +zone=27 +south +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32528 projected WGS 72BE / UTM zone 28S -18,-80,-12,0 2 6324 +proj=utm +zone=28 +south +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32529 projected WGS 72BE / UTM zone 29S -12,-80,-6,0 2 6324 +proj=utm +zone=29 +south +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32530 projected WGS 72BE / UTM zone 30S -6,-80,0,0 2 6324 +proj=utm +zone=30 +south +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32531 projected WGS 72BE / UTM zone 31S 0,-80,6,0 2 6324 +proj=utm +zone=31 +south +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32532 projected WGS 72BE / UTM zone 32S 6,-80,12,0 2 6324 +proj=utm +zone=32 +south +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32533 projected WGS 72BE / UTM zone 33S 12,-80,18,0 2 6324 +proj=utm +zone=33 +south +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32534 projected WGS 72BE / UTM zone 34S 18,-80,24,0 2 6324 +proj=utm +zone=34 +south +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32535 projected WGS 72BE / UTM zone 35S 24,-80,30,0 2 6324 +proj=utm +zone=35 +south +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32536 projected WGS 72BE / UTM zone 36S 30,-80,36,0 2 6324 +proj=utm +zone=36 +south +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32537 projected WGS 72BE / UTM zone 37S 36,-80,42,0 2 6324 +proj=utm +zone=37 +south +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32538 projected WGS 72BE / UTM zone 38S 42,-80,48,0 2 6324 +proj=utm +zone=38 +south +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32539 projected WGS 72BE / UTM zone 39S 48,-80,54,0 2 6324 +proj=utm +zone=39 +south +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32540 projected WGS 72BE / UTM zone 40S 54,-80,60,0 2 6324 +proj=utm +zone=40 +south +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32541 projected WGS 72BE / UTM zone 41S 60,-80,66,0 2 6324 +proj=utm +zone=41 +south +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32542 projected WGS 72BE / UTM zone 42S 66,-80,72,0 2 6324 +proj=utm +zone=42 +south +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32543 projected WGS 72BE / UTM zone 43S 72,-80,78,0 2 6324 +proj=utm +zone=43 +south +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32544 projected WGS 72BE / UTM zone 44S 78,-80,84,0 2 6324 +proj=utm +zone=44 +south +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32545 projected WGS 72BE / UTM zone 45S 84,-80,90,0 2 6324 +proj=utm +zone=45 +south +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32546 projected WGS 72BE / UTM zone 46S 90,-80,96,0 2 6324 +proj=utm +zone=46 +south +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32547 projected WGS 72BE / UTM zone 47S 96,-80,102,0 2 6324 +proj=utm +zone=47 +south +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32548 projected WGS 72BE / UTM zone 48S 102,-80,108,0 2 6324 +proj=utm +zone=48 +south +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32549 projected WGS 72BE / UTM zone 49S 108,-80,114,0 2 6324 +proj=utm +zone=49 +south +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32550 projected WGS 72BE / UTM zone 50S 114,-80,120,0 2 6324 +proj=utm +zone=50 +south +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32551 projected WGS 72BE / UTM zone 51S 120,-80,126,0 2 6324 +proj=utm +zone=51 +south +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32552 projected WGS 72BE / UTM zone 52S 126,-80,132,0 2 6324 +proj=utm +zone=52 +south +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32553 projected WGS 72BE / UTM zone 53S 132,-80,138,0 2 6324 +proj=utm +zone=53 +south +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32554 projected WGS 72BE / UTM zone 54S 138,-80,144,0 2 6324 +proj=utm +zone=54 +south +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32555 projected WGS 72BE / UTM zone 55S 144,-80,150,0 2 6324 +proj=utm +zone=55 +south +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32556 projected WGS 72BE / UTM zone 56S 150,-80,156,0 2 6324 +proj=utm +zone=56 +south +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32557 projected WGS 72BE / UTM zone 57S 156,-80,162,0 2 6324 +proj=utm +zone=57 +south +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32558 projected WGS 72BE / UTM zone 58S 162,-80,168,0 2 6324 +proj=utm +zone=58 +south +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32559 projected WGS 72BE / UTM zone 59S 168,-80,174,0 2 6324 +proj=utm +zone=59 +south +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32560 projected WGS 72BE / UTM zone 60S 174,-80,180,0 2 6324 +proj=utm +zone=60 +south +ellps=WGS72 +towgs84=0,0,1.9,0,0,0.814,-0.38 +units=m +no_defs +32601 projected WGS 84 / UTM zone 1N -180,0,-174,84 6326 +proj=utm +zone=1 +datum=WGS84 +units=m +no_defs +32602 projected WGS 84 / UTM zone 2N -174,0,-168,84 6326 +proj=utm +zone=2 +datum=WGS84 +units=m +no_defs +32603 projected WGS 84 / UTM zone 3N -168,0,-162,84 6326 +proj=utm +zone=3 +datum=WGS84 +units=m +no_defs +32604 projected WGS 84 / UTM zone 4N -162,0,-156,84 6326 +proj=utm +zone=4 +datum=WGS84 +units=m +no_defs +32605 projected WGS 84 / UTM zone 5N -156,0,-150,84 6326 +proj=utm +zone=5 +datum=WGS84 +units=m +no_defs +32606 projected WGS 84 / UTM zone 6N -150,0,-144,84 6326 +proj=utm +zone=6 +datum=WGS84 +units=m +no_defs +32607 projected WGS 84 / UTM zone 7N -144,0,-138,84 6326 +proj=utm +zone=7 +datum=WGS84 +units=m +no_defs +32608 projected WGS 84 / UTM zone 8N -138,0,-132,84 6326 +proj=utm +zone=8 +datum=WGS84 +units=m +no_defs +32609 projected WGS 84 / UTM zone 9N -132,0,-126,84 6326 +proj=utm +zone=9 +datum=WGS84 +units=m +no_defs +32610 projected WGS 84 / UTM zone 10N -126,0,-120,84 6326 +proj=utm +zone=10 +datum=WGS84 +units=m +no_defs +32611 projected WGS 84 / UTM zone 11N -120,0,-114,84 6326 +proj=utm +zone=11 +datum=WGS84 +units=m +no_defs +32612 projected WGS 84 / UTM zone 12N -114,0,-108,84 6326 +proj=utm +zone=12 +datum=WGS84 +units=m +no_defs +32613 projected WGS 84 / UTM zone 13N -108,0,-102,84 6326 +proj=utm +zone=13 +datum=WGS84 +units=m +no_defs +32614 projected WGS 84 / UTM zone 14N -102,0,-96,84 6326 +proj=utm +zone=14 +datum=WGS84 +units=m +no_defs +32615 projected WGS 84 / UTM zone 15N -96,0,-90,84 6326 +proj=utm +zone=15 +datum=WGS84 +units=m +no_defs +32616 projected WGS 84 / UTM zone 16N -90,0,-84,84 6326 +proj=utm +zone=16 +datum=WGS84 +units=m +no_defs +32617 projected WGS 84 / UTM zone 17N -84,0,-78,84 6326 +proj=utm +zone=17 +datum=WGS84 +units=m +no_defs +32618 projected WGS 84 / UTM zone 18N -78,0,-72,84 6326 +proj=utm +zone=18 +datum=WGS84 +units=m +no_defs +32619 projected WGS 84 / UTM zone 19N -72,0,-66,84 6326 +proj=utm +zone=19 +datum=WGS84 +units=m +no_defs +32620 projected WGS 84 / UTM zone 20N -66,0,-60,84 6326 +proj=utm +zone=20 +datum=WGS84 +units=m +no_defs +32621 projected WGS 84 / UTM zone 21N -60,0,-54,84 6326 +proj=utm +zone=21 +datum=WGS84 +units=m +no_defs +32622 projected WGS 84 / UTM zone 22N -54,0,-48,84 6326 +proj=utm +zone=22 +datum=WGS84 +units=m +no_defs +32623 projected WGS 84 / UTM zone 23N -48,0,-42,84 6326 +proj=utm +zone=23 +datum=WGS84 +units=m +no_defs +32624 projected WGS 84 / UTM zone 24N -42,0,-36,84 6326 +proj=utm +zone=24 +datum=WGS84 +units=m +no_defs +32625 projected WGS 84 / UTM zone 25N -36,0,-30,84 6326 +proj=utm +zone=25 +datum=WGS84 +units=m +no_defs +32626 projected WGS 84 / UTM zone 26N -30,0,-24,84 6326 +proj=utm +zone=26 +datum=WGS84 +units=m +no_defs +32627 projected WGS 84 / UTM zone 27N -24,0,-18,84 6326 +proj=utm +zone=27 +datum=WGS84 +units=m +no_defs +32628 projected WGS 84 / UTM zone 28N -18,0,-12,84 6326 +proj=utm +zone=28 +datum=WGS84 +units=m +no_defs +32629 projected WGS 84 / UTM zone 29N -12.01,0,-6,84.01 6326 +proj=utm +zone=29 +datum=WGS84 +units=m +no_defs +32630 projected WGS 84 / UTM zone 30N -6,0,0,84 6326 +proj=utm +zone=30 +datum=WGS84 +units=m +no_defs +32631 projected WGS 84 / UTM zone 31N 0,0,6,84 6326 +proj=utm +zone=31 +datum=WGS84 +units=m +no_defs +32632 projected WGS 84 / UTM zone 32N 6,0,12,84 6326 +proj=utm +zone=32 +datum=WGS84 +units=m +no_defs +32633 projected WGS 84 / UTM zone 33N 12,0,18,84 6326 +proj=utm +zone=33 +datum=WGS84 +units=m +no_defs +32634 projected WGS 84 / UTM zone 34N 18,0,24,84 6326 +proj=utm +zone=34 +datum=WGS84 +units=m +no_defs +32635 projected WGS 84 / UTM zone 35N 24,0,30,84 6326 +proj=utm +zone=35 +datum=WGS84 +units=m +no_defs +32636 projected WGS 84 / UTM zone 36N 30,0,36,84 6326 +proj=utm +zone=36 +datum=WGS84 +units=m +no_defs +32637 projected WGS 84 / UTM zone 37N 36,0,42,84 6326 +proj=utm +zone=37 +datum=WGS84 +units=m +no_defs +32638 projected WGS 84 / UTM zone 38N 42,0,48,84 6326 +proj=utm +zone=38 +datum=WGS84 +units=m +no_defs +32639 projected WGS 84 / UTM zone 39N 48,0,54,84 6326 +proj=utm +zone=39 +datum=WGS84 +units=m +no_defs +32640 projected WGS 84 / UTM zone 40N 54,0,60,84 6326 +proj=utm +zone=40 +datum=WGS84 +units=m +no_defs +32641 projected WGS 84 / UTM zone 41N 60,0,66,84 6326 +proj=utm +zone=41 +datum=WGS84 +units=m +no_defs +32642 projected WGS 84 / UTM zone 42N 66,0,72,84 6326 +proj=utm +zone=42 +datum=WGS84 +units=m +no_defs +32643 projected WGS 84 / UTM zone 43N 72,0,78,84 6326 +proj=utm +zone=43 +datum=WGS84 +units=m +no_defs +32644 projected WGS 84 / UTM zone 44N 78,0,84,84 6326 +proj=utm +zone=44 +datum=WGS84 +units=m +no_defs +32645 projected WGS 84 / UTM zone 45N 84,0,90,84 6326 +proj=utm +zone=45 +datum=WGS84 +units=m +no_defs +32646 projected WGS 84 / UTM zone 46N 90,0,96,84 6326 +proj=utm +zone=46 +datum=WGS84 +units=m +no_defs +32647 projected WGS 84 / UTM zone 47N 96,0,102,84 6326 +proj=utm +zone=47 +datum=WGS84 +units=m +no_defs +32648 projected WGS 84 / UTM zone 48N 102,0,108,84 6326 +proj=utm +zone=48 +datum=WGS84 +units=m +no_defs +32649 projected WGS 84 / UTM zone 49N 108,0,114,84 6326 +proj=utm +zone=49 +datum=WGS84 +units=m +no_defs +32650 projected WGS 84 / UTM zone 50N 114,0,120,84 6326 +proj=utm +zone=50 +datum=WGS84 +units=m +no_defs +32651 projected WGS 84 / UTM zone 51N 120,0,126,84 6326 +proj=utm +zone=51 +datum=WGS84 +units=m +no_defs +32652 projected WGS 84 / UTM zone 52N 126,0,132,84 6326 +proj=utm +zone=52 +datum=WGS84 +units=m +no_defs +32653 projected WGS 84 / UTM zone 53N 132,0,138,84 6326 +proj=utm +zone=53 +datum=WGS84 +units=m +no_defs +32654 projected WGS 84 / UTM zone 54N 138,0,144,84 6326 +proj=utm +zone=54 +datum=WGS84 +units=m +no_defs +32655 projected WGS 84 / UTM zone 55N 144,0,150,84 6326 +proj=utm +zone=55 +datum=WGS84 +units=m +no_defs +32656 projected WGS 84 / UTM zone 56N 150,0,156,84 6326 +proj=utm +zone=56 +datum=WGS84 +units=m +no_defs +32657 projected WGS 84 / UTM zone 57N 156,0,162,84 6326 +proj=utm +zone=57 +datum=WGS84 +units=m +no_defs +32658 projected WGS 84 / UTM zone 58N 162,0,168,84 6326 +proj=utm +zone=58 +datum=WGS84 +units=m +no_defs +32659 projected WGS 84 / UTM zone 59N 168,0,174,84 6326 +proj=utm +zone=59 +datum=WGS84 +units=m +no_defs +32660 projected WGS 84 / UTM zone 60N 174,0,180,84 6326 +proj=utm +zone=60 +datum=WGS84 +units=m +no_defs +32661 projected WGS 84 / UPS North (N,E) -180,60,180,90 6326 +proj=stere +lat_0=90 +lon_0=0 +k=0.994 +x_0=2000000 +y_0=2000000 +datum=WGS84 +units=m +no_defs +32664 projected WGS 84 / BLM 14N (ftUS) -97.22,25.97,-95.87,28.43 6326 +proj=tmerc +lat_0=0 +lon_0=-99 +k=0.9996 +x_0=500000.001016002 +y_0=0 +datum=WGS84 +to_meter=0.304800609601219 +no_defs +32665 projected WGS 84 / BLM 15N (ftUS) -96,25.61,-89.86,29.73 6326 +proj=tmerc +lat_0=0 +lon_0=-93 +k=0.9996 +x_0=500000.001016002 +y_0=0 +datum=WGS84 +to_meter=0.304800609601219 +no_defs +32666 projected WGS 84 / BLM 16N (ftUS) -90.01,23.95,-83.91,30.25 6326 +proj=tmerc +lat_0=0 +lon_0=-87 +k=0.9996 +x_0=500000.001016002 +y_0=0 +datum=WGS84 +to_meter=0.304800609601219 +no_defs +32667 projected WGS 84 / BLM 17N (ftUS) -84.09,23.82,-81.17,29.94 6326 +proj=tmerc +lat_0=0 +lon_0=-81 +k=0.9996 +x_0=500000.001016002 +y_0=0 +datum=WGS84 +to_meter=0.304800609601219 +no_defs +32701 projected WGS 84 / UTM zone 1S -180,-80,-174,0 6326 +proj=utm +zone=1 +south +datum=WGS84 +units=m +no_defs +32702 projected WGS 84 / UTM zone 2S -174,-80,-168,0 6326 +proj=utm +zone=2 +south +datum=WGS84 +units=m +no_defs +32703 projected WGS 84 / UTM zone 3S -168,-80,-162,0 6326 +proj=utm +zone=3 +south +datum=WGS84 +units=m +no_defs +32704 projected WGS 84 / UTM zone 4S -162,-80,-156,0 6326 +proj=utm +zone=4 +south +datum=WGS84 +units=m +no_defs +32705 projected WGS 84 / UTM zone 5S -156,-80,-150,0 6326 +proj=utm +zone=5 +south +datum=WGS84 +units=m +no_defs +32706 projected WGS 84 / UTM zone 6S -150,-80,-144,0 6326 +proj=utm +zone=6 +south +datum=WGS84 +units=m +no_defs +32707 projected WGS 84 / UTM zone 7S -144,-80,-138,0 6326 +proj=utm +zone=7 +south +datum=WGS84 +units=m +no_defs +32708 projected WGS 84 / UTM zone 8S -138,-80,-132,0 6326 +proj=utm +zone=8 +south +datum=WGS84 +units=m +no_defs +32709 projected WGS 84 / UTM zone 9S -132,-80,-126,0 6326 +proj=utm +zone=9 +south +datum=WGS84 +units=m +no_defs +32710 projected WGS 84 / UTM zone 10S -126,-80,-120,0 6326 +proj=utm +zone=10 +south +datum=WGS84 +units=m +no_defs +32711 projected WGS 84 / UTM zone 11S -120,-80,-114,0 6326 +proj=utm +zone=11 +south +datum=WGS84 +units=m +no_defs +32712 projected WGS 84 / UTM zone 12S -114,-80,-108,0 6326 +proj=utm +zone=12 +south +datum=WGS84 +units=m +no_defs +32713 projected WGS 84 / UTM zone 13S -108,-80,-102,0 6326 +proj=utm +zone=13 +south +datum=WGS84 +units=m +no_defs +32714 projected WGS 84 / UTM zone 14S -102,-80,-96,0 6326 +proj=utm +zone=14 +south +datum=WGS84 +units=m +no_defs +32715 projected WGS 84 / UTM zone 15S -96,-80,-90,0 6326 +proj=utm +zone=15 +south +datum=WGS84 +units=m +no_defs +32716 projected WGS 84 / UTM zone 16S -90,-80,-84,0 6326 +proj=utm +zone=16 +south +datum=WGS84 +units=m +no_defs +32717 projected WGS 84 / UTM zone 17S -84,-80,-78,0 6326 +proj=utm +zone=17 +south +datum=WGS84 +units=m +no_defs +32718 projected WGS 84 / UTM zone 18S -78,-80,-72,0 6326 +proj=utm +zone=18 +south +datum=WGS84 +units=m +no_defs +32719 projected WGS 84 / UTM zone 19S -72,-80,-66,0 6326 +proj=utm +zone=19 +south +datum=WGS84 +units=m +no_defs +32720 projected WGS 84 / UTM zone 20S -66,-80,-60,0 6326 +proj=utm +zone=20 +south +datum=WGS84 +units=m +no_defs +32721 projected WGS 84 / UTM zone 21S -60,-80,-54,0 6326 +proj=utm +zone=21 +south +datum=WGS84 +units=m +no_defs +32722 projected WGS 84 / UTM zone 22S -54,-80,-48,0 6326 +proj=utm +zone=22 +south +datum=WGS84 +units=m +no_defs +32723 projected WGS 84 / UTM zone 23S -48,-80,-42,0 6326 +proj=utm +zone=23 +south +datum=WGS84 +units=m +no_defs +32724 projected WGS 84 / UTM zone 24S -42,-80,-36,0 6326 +proj=utm +zone=24 +south +datum=WGS84 +units=m +no_defs +32725 projected WGS 84 / UTM zone 25S -36,-80,-30,0 6326 +proj=utm +zone=25 +south +datum=WGS84 +units=m +no_defs +32726 projected WGS 84 / UTM zone 26S -30,-80,-24,0 6326 +proj=utm +zone=26 +south +datum=WGS84 +units=m +no_defs +32727 projected WGS 84 / UTM zone 27S -24,-80,-18,0 6326 +proj=utm +zone=27 +south +datum=WGS84 +units=m +no_defs +32728 projected WGS 84 / UTM zone 28S -18,-80,-12,0 6326 +proj=utm +zone=28 +south +datum=WGS84 +units=m +no_defs +32729 projected WGS 84 / UTM zone 29S -12,-80,-6,0 6326 +proj=utm +zone=29 +south +datum=WGS84 +units=m +no_defs +32730 projected WGS 84 / UTM zone 30S -6,-80,0,0 6326 +proj=utm +zone=30 +south +datum=WGS84 +units=m +no_defs +32731 projected WGS 84 / UTM zone 31S 0,-80,6,0 6326 +proj=utm +zone=31 +south +datum=WGS84 +units=m +no_defs +32732 projected WGS 84 / UTM zone 32S 6,-80,12,0 6326 +proj=utm +zone=32 +south +datum=WGS84 +units=m +no_defs +32733 projected WGS 84 / UTM zone 33S 12,-80,18,0 6326 +proj=utm +zone=33 +south +datum=WGS84 +units=m +no_defs +32734 projected WGS 84 / UTM zone 34S 18,-80,24,0 6326 +proj=utm +zone=34 +south +datum=WGS84 +units=m +no_defs +32735 projected WGS 84 / UTM zone 35S 24,-80,30,0 6326 +proj=utm +zone=35 +south +datum=WGS84 +units=m +no_defs +32736 projected WGS 84 / UTM zone 36S 30,-80,36,0 6326 +proj=utm +zone=36 +south +datum=WGS84 +units=m +no_defs +32737 projected WGS 84 / UTM zone 37S 36,-80,42,0 6326 +proj=utm +zone=37 +south +datum=WGS84 +units=m +no_defs +32738 projected WGS 84 / UTM zone 38S 42,-80,48,0 6326 +proj=utm +zone=38 +south +datum=WGS84 +units=m +no_defs +32739 projected WGS 84 / UTM zone 39S 48,-80,54,0 6326 +proj=utm +zone=39 +south +datum=WGS84 +units=m +no_defs +32740 projected WGS 84 / UTM zone 40S 54,-80,60,0 6326 +proj=utm +zone=40 +south +datum=WGS84 +units=m +no_defs +32741 projected WGS 84 / UTM zone 41S 60,-80,66,0 6326 +proj=utm +zone=41 +south +datum=WGS84 +units=m +no_defs +32742 projected WGS 84 / UTM zone 42S 66,-80,72,0 6326 +proj=utm +zone=42 +south +datum=WGS84 +units=m +no_defs +32743 projected WGS 84 / UTM zone 43S 72,-80,78,0 6326 +proj=utm +zone=43 +south +datum=WGS84 +units=m +no_defs +32744 projected WGS 84 / UTM zone 44S 78,-80,84,0 6326 +proj=utm +zone=44 +south +datum=WGS84 +units=m +no_defs +32745 projected WGS 84 / UTM zone 45S 84,-80,90,0 6326 +proj=utm +zone=45 +south +datum=WGS84 +units=m +no_defs +32746 projected WGS 84 / UTM zone 46S 90,-80,96,0 6326 +proj=utm +zone=46 +south +datum=WGS84 +units=m +no_defs +32747 projected WGS 84 / UTM zone 47S 96,-80,102,0 6326 +proj=utm +zone=47 +south +datum=WGS84 +units=m +no_defs +32748 projected WGS 84 / UTM zone 48S 102,-80,108,0 6326 +proj=utm +zone=48 +south +datum=WGS84 +units=m +no_defs +32749 projected WGS 84 / UTM zone 49S 108,-80,114,0 6326 +proj=utm +zone=49 +south +datum=WGS84 +units=m +no_defs +32750 projected WGS 84 / UTM zone 50S 114,-80,120,0 6326 +proj=utm +zone=50 +south +datum=WGS84 +units=m +no_defs +32751 projected WGS 84 / UTM zone 51S 120,-80,126,0 6326 +proj=utm +zone=51 +south +datum=WGS84 +units=m +no_defs +32752 projected WGS 84 / UTM zone 52S 126,-80,132,0 6326 +proj=utm +zone=52 +south +datum=WGS84 +units=m +no_defs +32753 projected WGS 84 / UTM zone 53S 132,-80,138,0 6326 +proj=utm +zone=53 +south +datum=WGS84 +units=m +no_defs +32754 projected WGS 84 / UTM zone 54S 138,-80,144,0 6326 +proj=utm +zone=54 +south +datum=WGS84 +units=m +no_defs +32755 projected WGS 84 / UTM zone 55S 144,-80,150,0 6326 +proj=utm +zone=55 +south +datum=WGS84 +units=m +no_defs +32756 projected WGS 84 / UTM zone 56S 150,-80,156,0 6326 +proj=utm +zone=56 +south +datum=WGS84 +units=m +no_defs +32757 projected WGS 84 / UTM zone 57S 156,-80,162,0 6326 +proj=utm +zone=57 +south +datum=WGS84 +units=m +no_defs +32758 projected WGS 84 / UTM zone 58S 162,-80,168,0 6326 +proj=utm +zone=58 +south +datum=WGS84 +units=m +no_defs +32759 projected WGS 84 / UTM zone 59S 168,-80,174,0 6326 +proj=utm +zone=59 +south +datum=WGS84 +units=m +no_defs +32760 projected WGS 84 / UTM zone 60S 174,-80,180,0 6326 +proj=utm +zone=60 +south +datum=WGS84 +units=m +no_defs +32761 projected WGS 84 / UPS South (N,E) -180,-90,180,-60 6326 +proj=stere +lat_0=-90 +lon_0=0 +k=0.994 +x_0=2000000 +y_0=2000000 +datum=WGS84 +units=m +no_defs +32766 projected WGS 84 / TM 36 SE 32.64,-27.71,43.03,-10.09 6326 +proj=tmerc +lat_0=0 +lon_0=36 +k=0.9996 +x_0=500000 +y_0=10000000 +datum=WGS84 +units=m +no_defs diff --git a/nodedb-crs/data/epsg_supplement.tsv b/nodedb-crs/data/epsg_supplement.tsv new file mode 100644 index 000000000..2b1886eb2 --- /dev/null +++ b/nodedb-crs/data/epsg_supplement.tsv @@ -0,0 +1,12 @@ +# EPSG supplement: EPSG change 2026.081, absent from EPSG v12.029. PROJ 9.8.1 +code kind name area accuracy_m datum proj +12287 geographic GDM2020 98.02,0.85,119.61,7.81 1 1471 +proj=longlat +ellps=GRS80 +towgs84=0,0,0 +no_defs +25314 projected GDM2020 / Johor Grid 102.44,1.21,104.6,2.95 1 1471 +proj=cass +lat_0=2.04255439166667 +lon_0=103.561069988889 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +25315 projected GDM2020 / Sembilan and Melaka Grid 101.7,2.03,102.71,3.28 1 1471 +proj=cass +lat_0=2.71211928888889 +lon_0=101.939706794444 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +25316 projected GDM2020 / Pahang Grid 101.33,2.45,103.67,4.78 1 1471 +proj=cass +lat_0=3.710746625 +lon_0=102.434617275 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +25317 projected GDM2020 / Selangor Grid 100.76,2.54,101.97,3.87 1 1471 +proj=cass +lat_0=3.68010363888889 +lon_0=101.506805472222 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +25318 projected GDM2020 / Terengganu Grid 102.38,3.89,103.72,5.9 1 1471 +proj=cass +lat_0=4.94582420833333 +lon_0=102.893616386111 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +25319 projected GDM2020 / Pinang Grid 100.12,5.12,100.56,5.59 1 1471 +proj=cass +lat_0=5.420888725 +lon_0=100.344658966667 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +25320 projected GDM2020 / Kedah and Perlis Grid 99.59,5.08,101.12,6.72 1 1471 +proj=cass +lat_0=5.96467151666667 +lon_0=100.636374988889 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +25321 projected GDM2020 / Perak Grid 100.07,3.66,102,5.92 1 1471 +proj=cass +lat_0=4.859066725 +lon_0=100.815412119444 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs +25322 projected GDM2020 / Kelantan Grid 101.33,4.54,102.67,6.29 1 1471 +proj=cass +lat_0=5.893631975 +lon_0=102.175627286111 +x_0=0 +y_0=0 +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs diff --git a/nodedb-crs/src/datum/ecef.rs b/nodedb-crs/src/datum/ecef.rs new file mode 100644 index 000000000..6cceff0ec --- /dev/null +++ b/nodedb-crs/src/datum/ecef.rs @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Geodetic ↔ Earth-centred, Earth-fixed (ECEF) Cartesian coordinates. + +use crate::def::Ellipsoid; + +/// Iterations allowed for Bowring's latitude refinement. Each iteration +/// roughly triples the correct digits, so three reach double precision on +/// Earth-sized ellipsoids. The cap only bounds bad input. +const BOWRING_MAX_ITERS: usize = 8; + +/// Latitude change in radians below which Bowring's iteration stops, about +/// 6 nm on the ground. +const LATITUDE_TOLERANCE_RAD: f64 = 1e-15; + +/// λ, φ radians and h metres above `ellipsoid` to ECEF metres. +pub fn geodetic_to_ecef(lam: f64, phi: f64, h: f64, ellipsoid: &Ellipsoid) -> [f64; 3] { + let e2 = ellipsoid.e2(); + let (sin_phi, cos_phi) = phi.sin_cos(); + let (sin_lam, cos_lam) = lam.sin_cos(); + let n = ellipsoid.a / (1.0 - e2 * sin_phi * sin_phi).sqrt(); + [ + (n + h) * cos_phi * cos_lam, + (n + h) * cos_phi * sin_lam, + (n * (1.0 - e2) + h) * sin_phi, + ] +} + +/// ECEF metres to λ, φ radians and h metres above `ellipsoid`, by Bowring's +/// iteration on the parametric latitude. +pub fn ecef_to_geodetic(xyz: [f64; 3], ellipsoid: &Ellipsoid) -> (f64, f64, f64) { + let [x, y, z] = xyz; + let a = ellipsoid.a; + let b = ellipsoid.b(); + let e2 = ellipsoid.e2(); + let ep2 = e2 / (1.0 - e2); + let p = x.hypot(y); + let lam = y.atan2(x); + + let mut beta = (a * z).atan2(b * p); + let mut phi = 0.0; + for _ in 0..BOWRING_MAX_ITERS { + let (sin_b, cos_b) = beta.sin_cos(); + let next = (z + ep2 * b * sin_b.powi(3)).atan2(p - e2 * a * cos_b.powi(3)); + let done = (next - phi).abs() < LATITUDE_TOLERANCE_RAD; + phi = next; + if done { + break; + } + let (sin_p, cos_p) = phi.sin_cos(); + beta = (b * sin_p).atan2(a * cos_p); + } + let (sin_phi, cos_phi) = phi.sin_cos(); + let h = p * cos_phi + z * sin_phi - a * (1.0 - e2 * sin_phi * sin_phi).sqrt(); + (lam, phi, h) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn round_trip_below_a_micrometre() { + for ell in [ + Ellipsoid::WGS84, + Ellipsoid::EVEREST_1830_MODIFIED, + Ellipsoid::sphere(6_371_007.181), + ] { + for (lon, lat, h) in [ + (101.7117, 3.1579, 45.0), + (-75.0, -89.9, 1000.0), + (179.9, 60.0, -100.0), + (0.0, 0.0, 0.0), + (12.0, 90.0, 10.0), + ] { + let xyz = geodetic_to_ecef(f64::to_radians(lon), f64::to_radians(lat), h, &ell); + let (lam, phi, h2) = ecef_to_geodetic(xyz, &ell); + let back = geodetic_to_ecef(lam, phi, h2, &ell); + for (got, want) in back.iter().zip(xyz) { + assert!( + (got - want).abs() < 1e-6, + "{lon} {lat} {h}: {back:?} vs {xyz:?}" + ); + } + assert!((h2 - h).abs() < 1e-6, "{h2} vs {h}"); + } + } + } + + /// The equator on the prime meridian lies at x = a. + #[test] + fn equator_point() { + let xyz = geodetic_to_ecef(0.0, 0.0, 0.0, &Ellipsoid::WGS84); + assert_eq!(xyz, [6_378_137.0, 0.0, 0.0]); + } +} diff --git a/nodedb-crs/src/datum/helmert.rs b/nodedb-crs/src/datum/helmert.rs new file mode 100644 index 000000000..54f2b1d87 --- /dev/null +++ b/nodedb-crs/src/datum/helmert.rs @@ -0,0 +1,261 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! The 7-parameter Helmert transformation in its small-angle form, IOGP +//! Guidance Note 7-2 §4.2.3. This is the form PROJ `+towgs84` applies. +//! +//! [`Helmert::forward`] and [`Helmert::inverse`] apply the static +//! parameters. A time-dependent transformation, GN 7-2 §4.2.5, first takes +//! its parameters at the coordinate epoch with [`Helmert::at_epoch`]. + +use crate::def::Helmert; + +/// Radians per arc-second, π / 648 000. +const RAD_PER_ARCSEC: f64 = core::f64::consts::PI / 648_000.0; + +/// Scale per part per million. +const PER_PPM: f64 = 1e-6; + +/// Cross product a × b. +fn cross(a: [f64; 3], b: [f64; 3]) -> [f64; 3] { + [ + a[1] * b[2] - a[2] * b[1], + a[2] * b[0] - a[0] * b[2], + a[0] * b[1] - a[1] * b[0], + ] +} + +/// Dot product a · b. +fn dot(a: [f64; 3], b: [f64; 3]) -> f64 { + a[0] * b[0] + a[1] * b[1] + a[2] * b[2] +} + +impl Helmert { + /// Position-vector rotations in radians. + fn rotation_rad(&self) -> [f64; 3] { + self.position_vector_rotations().map(|r| r * RAD_PER_ARCSEC) + } + + /// Translation vector in metres. + fn translation_m(&self) -> [f64; 3] { + [self.tx_m, self.ty_m, self.tz_m] + } + + /// The static transformation this one equals at coordinate epoch + /// `epoch`, decimal year: each parameter p becomes p + ṗ (t − t0), + /// IOGP GN 7-2 §4.2.5. A static transformation returns itself. + pub fn at_epoch(&self, epoch: f64) -> Helmert { + let Some(rates) = self.rates else { + return *self; + }; + let dt = epoch - rates.reference_epoch; + let r = rates.values(); + Helmert { + tx_m: self.tx_m + r[0] * dt, + ty_m: self.ty_m + r[1] * dt, + tz_m: self.tz_m + r[2] * dt, + rx_arcsec: self.rx_arcsec + r[3] * dt, + ry_arcsec: self.ry_arcsec + r[4] * dt, + rz_arcsec: self.rz_arcsec + r[5] * dt, + scale_ppm: self.scale_ppm + r[6] * dt, + convention: self.convention, + accuracy_m: self.accuracy_m, + rates: None, + } + } + + /// Map source ECEF metres to WGS 84 ECEF metres: + /// V_T = (1 + dS) (I + K) V_S + T, where K V = r × V in the + /// position-vector convention. + pub fn forward(&self, v: [f64; 3]) -> [f64; 3] { + let m = 1.0 + self.scale_ppm * PER_PPM; + let rv = cross(self.rotation_rad(), v); + let t = self.translation_m(); + [ + m * (v[0] + rv[0]) + t[0], + m * (v[1] + rv[1]) + t[1], + m * (v[2] + rv[2]) + t[2], + ] + } + + /// The exact inverse of [`Helmert::forward`]: + /// V_S = (I + K)⁻¹ (V_T − T) / (1 + dS), with + /// (I + K)⁻¹ = (I − K + r rᵀ) / (1 + |r|²). + pub fn inverse(&self, v: [f64; 3]) -> [f64; 3] { + let m = 1.0 + self.scale_ppm * PER_PPM; + let t = self.translation_m(); + let u = [(v[0] - t[0]) / m, (v[1] - t[1]) / m, (v[2] - t[2]) / m]; + let r = self.rotation_rad(); + let ru = cross(r, u); + let r_dot_u = dot(r, u); + let det = 1.0 + dot(r, r); + [ + (u[0] - ru[0] + r[0] * r_dot_u) / det, + (u[1] - ru[1] + r[1] * r_dot_u) / det, + (u[2] - ru[2] + r[2] * r_dot_u) / det, + ] + } +} + +#[cfg(test)] +mod tests { + use crate::def::{Helmert, HelmertRates, RotationConvention}; + + /// IOGP GN 7-2 §4.2.3 worked example: WGS 72 → WGS 84, EPSG + /// transformation 1238, position-vector convention. + fn wgs72_to_wgs84() -> Helmert { + Helmert::position_vector([0.0, 0.0, 4.5, 0.0, 0.0, 0.554, 0.219]) + } + + /// Input of the GN 7-2 example, WGS 72 geocentric metres. + const GN_INPUT: [f64; 3] = [3_657_660.66, 255_768.55, 5_201_382.11]; + + /// Half a centimetre: GN 7-2 states its results to 0.01 m. + const GN_HALF_CM: f64 = 0.005; + + #[test] + fn iogp_position_vector_example() { + let out = wgs72_to_wgs84().forward(GN_INPUT); + // GN 7-2 prints YT = 255 778.43 m and ZT = 5 201 387.75 m. + assert!((out[1] - 255_778.43).abs() < GN_HALF_CM, "{out:?}"); + assert!((out[2] - 5_201_387.75).abs() < GN_HALF_CM, "{out:?}"); + // GN 7-2 prints XT = 3 657 660.78 m. Its own matrix formula gives + // M (XS − rZ YS) = 3 657 660.774 m, which rounds to .77, so the + // check uses the formula value to a millimetre. + assert!((out[0] - 3_657_660.774_1).abs() < 1e-3, "{out:?}"); + } + + /// GN 7-2: the coordinate-frame parameters for the same example are the + /// position-vector ones with the rotations negated. + #[test] + fn coordinate_frame_gives_the_same_result() { + let pv = wgs72_to_wgs84(); + let mut cf = Helmert::position_vector([0.0, 0.0, 4.5, 0.0, 0.0, -0.554, 0.219]); + cf.convention = RotationConvention::CoordinateFrame; + assert_eq!(pv.forward(GN_INPUT), cf.forward(GN_INPUT)); + } + + /// IOGP GN 7-2 (September 2019) §4.2.5 worked example: EPSG + /// transformation 6276, ITRF2008 to GDA94, Time-dependent Coordinate + /// Frame rotation (method 1056), coordinate epoch 2013.90. Translations + /// in metres, rotations in arc-seconds, scale in ppm, per year for the + /// rates. + fn itrf2008_to_gda94() -> Helmert { + let mut h = Helmert::position_vector([ + -0.084_68, + -0.019_42, + 0.032_01, + -0.000_425_4, + 0.002_257_8, + 0.002_401_5, + 0.009_71, + ]); + h.convention = RotationConvention::CoordinateFrame; + h.rates = Some(HelmertRates::new( + [ + 0.001_42, + 0.001_34, + 0.000_90, + 0.001_546_1, + 0.001_182_0, + 0.001_155_1, + 0.000_109, + ], + GN_TD_REFERENCE_EPOCH, + )); + h + } + + /// Parameter reference epoch t0 of EPSG transformation 6276. + const GN_TD_REFERENCE_EPOCH: f64 = 1994.00; + + /// Coordinate epoch t of the GN 7-2 §4.2.5 example. + const GN_TD_EPOCH: f64 = 2013.90; + + /// ITRF2008 input of the GN 7-2 §4.2.5 example, metres. + const GN_TD_INPUT: [f64; 3] = [-3_789_470.710, 4_841_770.404, -1_690_893.952]; + + /// GDA94 output GN 7-2 §4.2.5 prints, metres. + const GN_TD_OUTPUT: [f64; 3] = [-3_789_470.004, 4_841_770.686, -1_690_895.108]; + + /// PROJ 9.8.1 `+proj=helmert` with the EPSG 6276 parameters, + /// `+t_epoch=1994 +convention=coordinate_frame`, on GN_TD_INPUT at + /// 2013.90. + const PROJ_TD_OUTPUT: [f64; 3] = [ + -3_789_470.004_157_804, + 4_841_770.686_452_708, + -1_690_895.108_020_519_6, + ]; + + /// Agreement with PROJ, which applies the same linear form. + const PROJ_TD_TOLERANCE_M: f64 = 1e-6; + + #[test] + fn iogp_time_dependent_coordinate_frame_example() { + let out = itrf2008_to_gda94() + .at_epoch(GN_TD_EPOCH) + .forward(GN_TD_INPUT); + for i in 0..3 { + assert!((out[i] - GN_TD_OUTPUT[i]).abs() < GN_HALF_CM, "{out:?}"); + assert!( + (out[i] - PROJ_TD_OUTPUT[i]).abs() < PROJ_TD_TOLERANCE_M, + "{out:?}" + ); + } + // The reverse direction returns the GN 7-2 input. + let back = itrf2008_to_gda94() + .at_epoch(GN_TD_EPOCH) + .inverse(GN_TD_OUTPUT); + for i in 0..3 { + assert!((back[i] - GN_TD_INPUT[i]).abs() < GN_HALF_CM, "{back:?}"); + } + } + + /// Method 1053 is method 1056 with every rotation and rotation rate + /// negated, GN 7-2 §4.2.5. + #[test] + fn time_dependent_position_vector_matches_coordinate_frame() { + let cf = itrf2008_to_gda94(); + let mut pv = cf; + pv.convention = RotationConvention::PositionVector; + pv.rx_arcsec = -cf.rx_arcsec; + pv.ry_arcsec = -cf.ry_arcsec; + pv.rz_arcsec = -cf.rz_arcsec; + let mut rates = cf.rates.unwrap(); + rates.drx_arcsec_per_yr = -rates.drx_arcsec_per_yr; + rates.dry_arcsec_per_yr = -rates.dry_arcsec_per_yr; + rates.drz_arcsec_per_yr = -rates.drz_arcsec_per_yr; + pv.rates = Some(rates); + let a = cf.at_epoch(GN_TD_EPOCH).forward(GN_TD_INPUT); + let b = pv.at_epoch(GN_TD_EPOCH).forward(GN_TD_INPUT); + for i in 0..3 { + assert!((a[i] - b[i]).abs() < 1e-9, "{a:?} {b:?}"); + } + } + + /// At the reference epoch the rates contribute nothing. + #[test] + fn reference_epoch_applies_the_static_parameters() { + let h = itrf2008_to_gda94(); + let mut fixed = h; + fixed.rates = None; + assert_eq!( + h.at_epoch(GN_TD_REFERENCE_EPOCH).forward(GN_TD_INPUT), + fixed.forward(GN_TD_INPUT) + ); + } + + #[test] + fn inverse_round_trip_below_a_micrometre() { + let h = Helmert::position_vector([-11.0, 851.0, 5.0, 2.5, -1.75, 3.2, 12.0]); + for v in [ + GN_INPUT, + [-2_000_000.0, 5_500_000.0, -2_500_000.0], + [6_378_137.0, 0.0, 0.0], + ] { + let back = h.inverse(h.forward(v)); + for (got, want) in back.iter().zip(v) { + assert!((got - want).abs() < 1e-6, "{v:?} -> {back:?}"); + } + } + } +} diff --git a/nodedb-crs/src/datum/mod.rs b/nodedb-crs/src/datum/mod.rs new file mode 100644 index 000000000..ef8e00734 --- /dev/null +++ b/nodedb-crs/src/datum/mod.rs @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Datum shift math: geodetic ↔ ECEF and the Helmert transformation. + +pub mod ecef; +pub mod helmert; + +pub use ecef::{ecef_to_geodetic, geodetic_to_ecef}; diff --git a/nodedb-crs/src/def/area.rs b/nodedb-crs/src/def/area.rs new file mode 100644 index 000000000..578edd40f --- /dev/null +++ b/nodedb-crs/src/def/area.rs @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Area of use of a CRS. + +use serde::{Deserialize, Serialize}; + +use crate::error::CrsParseError; + +/// Geographic bounds where a CRS is valid, WGS 84 degrees. `west > east` +/// marks an area crossing the anti-meridian. +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Serialize, + Deserialize, + zerompk::ToMessagePack, + zerompk::FromMessagePack, +)] +pub struct AreaOfUse { + pub west: f64, + pub south: f64, + pub east: f64, + pub north: f64, +} + +impl AreaOfUse { + /// Parse `west,south,east,north`. An empty string is no area. + pub fn parse(text: &str) -> Result, CrsParseError> { + if text.is_empty() { + return Ok(None); + } + let bad = || CrsParseError::invalid("area", text); + let mut parts = text.split(',').map(|p| p.trim().parse::()); + let mut next = || parts.next().ok_or_else(bad)?.map_err(|_| bad()); + let area = AreaOfUse { + west: next()?, + south: next()?, + east: next()?, + north: next()?, + }; + if parts.next().is_some() { + return Err(bad()); + } + Ok(Some(area)) + } + + /// Whether `[lon, lat]` WGS 84 degrees lies inside the area. + pub fn contains(&self, lon: f64, lat: f64) -> bool { + let in_lat = (self.south..=self.north).contains(&lat); + let in_lon = if self.west <= self.east { + (self.west..=self.east).contains(&lon) + } else { + lon >= self.west || lon <= self.east + }; + in_lat && in_lon + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_four_numbers() { + let area = AreaOfUse::parse("102,0,108,84").unwrap().unwrap(); + assert_eq!(area.west, 102.0); + assert_eq!(area.north, 84.0); + assert!(area.contains(105.0, 3.0)); + assert!(!area.contains(101.0, 3.0)); + } + + #[test] + fn empty_is_none_and_bad_is_error() { + assert_eq!(AreaOfUse::parse(""), Ok(None)); + assert!(AreaOfUse::parse("1,2,3").is_err()); + assert!(AreaOfUse::parse("1,2,3,4,5").is_err()); + assert!(AreaOfUse::parse("1,x,3,4").is_err()); + } + + #[test] + fn anti_meridian_area() { + let area = AreaOfUse::parse("170,-10,-170,10").unwrap().unwrap(); + assert!(area.contains(175.0, 0.0)); + assert!(area.contains(-175.0, 0.0)); + assert!(!area.contains(0.0, 0.0)); + } +} diff --git a/nodedb-crs/src/def/axis.rs b/nodedb-crs/src/def/axis.rs new file mode 100644 index 000000000..1f2bea49d --- /dev/null +++ b/nodedb-crs/src/def/axis.rs @@ -0,0 +1,226 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Axis order and direction of a CRS, as PROJ `+axis` spells it. +//! +//! The native order is east, north, up: longitude, latitude, height for a +//! geographic CRS, and easting, northing, height for a projected one. +//! `+axis=xyz` names the direction of each CRS ordinate with one letter +//! from `e w n s u d`. PROJ's `axisswap` step defines the mapping: +//! CRS ordinate `i` is `sign(i) · native[axis(i)]`. + +use core::fmt; + +use serde::{Deserialize, Serialize}; + +use crate::error::CrsParseError; + +/// The direction of one CRS ordinate. +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Eq, + Hash, + Serialize, + Deserialize, + zerompk::ToMessagePack, + zerompk::FromMessagePack, +)] +pub enum AxisDirection { + East, + West, + North, + South, + Up, + Down, +} + +impl AxisDirection { + /// The direction PROJ writes as `letter`. + pub fn from_letter(letter: char) -> Option { + match letter { + 'e' => Some(AxisDirection::East), + 'w' => Some(AxisDirection::West), + 'n' => Some(AxisDirection::North), + 's' => Some(AxisDirection::South), + 'u' => Some(AxisDirection::Up), + 'd' => Some(AxisDirection::Down), + _ => None, + } + } + + /// The PROJ letter of this direction. + pub fn letter(self) -> char { + match self { + AxisDirection::East => 'e', + AxisDirection::West => 'w', + AxisDirection::North => 'n', + AxisDirection::South => 's', + AxisDirection::Up => 'u', + AxisDirection::Down => 'd', + } + } + + /// Index of the native ordinate this direction reads: 0 east, 1 north, + /// 2 up. + pub fn native_index(self) -> usize { + match self { + AxisDirection::East | AxisDirection::West => 0, + AxisDirection::North | AxisDirection::South => 1, + AxisDirection::Up | AxisDirection::Down => 2, + } + } + + /// +1 along the native direction, −1 against it. + pub fn sign(self) -> f64 { + match self { + AxisDirection::East | AxisDirection::North | AxisDirection::Up => 1.0, + AxisDirection::West | AxisDirection::South | AxisDirection::Down => -1.0, + } + } +} + +/// The direction of each of the three CRS ordinates. +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Eq, + Hash, + Serialize, + Deserialize, + zerompk::ToMessagePack, + zerompk::FromMessagePack, +)] +pub struct AxisOrder { + pub x: AxisDirection, + pub y: AxisDirection, + pub z: AxisDirection, +} + +impl AxisOrder { + /// East, north, up: the native order, PROJ `+axis=enu`. + pub const ENU: AxisOrder = AxisOrder { + x: AxisDirection::East, + y: AxisDirection::North, + z: AxisDirection::Up, + }; + + /// Parse a PROJ `+axis` value: three letters from `e w n s u d`, one + /// per native axis. A repeated axis, such as `ewu`, is refused, as PROJ + /// refuses it. + pub fn parse(text: &str) -> Result { + let bad = || CrsParseError::invalid("axis", text); + let mut letters = text.chars().map(AxisDirection::from_letter); + let mut next = || letters.next().flatten().ok_or_else(bad); + let order = AxisOrder { + x: next()?, + y: next()?, + z: next()?, + }; + if text.chars().count() != 3 { + return Err(bad()); + } + let [a, b, c] = order.directions().map(AxisDirection::native_index); + if a == b || a == c || b == c { + return Err(bad()); + } + Ok(order) + } + + /// Whether this is the native order. + pub fn is_enu(&self) -> bool { + *self == AxisOrder::ENU + } + + /// The directions of x, y and z. + pub fn directions(&self) -> [AxisDirection; 3] { + [self.x, self.y, self.z] + } + + /// Native east, north, up ordinates to CRS ordinates. + pub fn from_native(&self, native: [f64; 3]) -> [f64; 3] { + self.directions() + .map(|d| d.sign() * native[d.native_index()]) + } + + /// CRS ordinates to native east, north, up ordinates. + pub fn to_native(&self, ordinates: [f64; 3]) -> [f64; 3] { + let mut native = [0.0; 3]; + for (d, v) in self.directions().into_iter().zip(ordinates) { + native[d.native_index()] = d.sign() * v; + } + native + } +} + +impl Default for AxisOrder { + fn default() -> Self { + AxisOrder::ENU + } +} + +impl fmt::Display for AxisOrder { + /// The PROJ `+axis` value, such as `enu`. + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for d in self.directions() { + fmt::Write::write_char(f, d.letter())?; + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_every_permutation_and_sign() { + let mut accepted = 0; + for a in "ewnsud".chars() { + for b in "ewnsud".chars() { + for c in "ewnsud".chars() { + let text: String = [a, b, c].iter().collect(); + if let Ok(order) = AxisOrder::parse(&text) { + assert_eq!(order.to_string(), text); + accepted += 1; + } + } + } + } + // 3! axis orders times 2³ signs. + assert_eq!(accepted, 48); + } + + #[test] + fn refuses_repeats_length_and_letters() { + for bad in ["", "en", "enuu", "ewu", "nsu", "enn", "xyz", "eNu", "enü"] { + assert!( + matches!( + AxisOrder::parse(bad), + Err(CrsParseError::InvalidValue { .. }) + ), + "{bad}" + ); + } + } + + #[test] + fn swaps_and_negates_like_axisswap() { + let wsu = AxisOrder::parse("wsu").unwrap(); + assert_eq!(wsu.from_native([1.0, 2.0, 3.0]), [-1.0, -2.0, 3.0]); + let swu = AxisOrder::parse("swu").unwrap(); + assert_eq!(swu.from_native([1.0, 2.0, 3.0]), [-2.0, -1.0, 3.0]); + let neu = AxisOrder::parse("neu").unwrap(); + assert_eq!(neu.from_native([1.0, 2.0, 3.0]), [2.0, 1.0, 3.0]); + let und = AxisOrder::parse("dne").unwrap(); + assert_eq!(und.from_native([1.0, 2.0, 3.0]), [-3.0, 2.0, 1.0]); + for order in [wsu, swu, neu, und, AxisOrder::ENU] { + let v = [7.5, -3.25, 11.0]; + assert_eq!(order.to_native(order.from_native(v)), v, "{order}"); + } + assert!(AxisOrder::default().is_enu()); + } +} diff --git a/nodedb-crs/src/def/crs_def.rs b/nodedb-crs/src/def/crs_def.rs new file mode 100644 index 000000000..73468583b --- /dev/null +++ b/nodedb-crs/src/def/crs_def.rs @@ -0,0 +1,129 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! A complete coordinate reference system definition. + +use serde::{Deserialize, Serialize}; + +use super::area::AreaOfUse; +use super::axis::AxisOrder; +use super::datum::Datum; +use super::unit::LinearUnit; +use crate::method::ProjectionDef; +use crate::srid::Srid; + +pub use nodedb_srid::CrsKind; + +/// One CRS: datum, optional projection, unit, axis order and area of use. +/// +/// Serialized field by field, so a decode reproduces the value exactly. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct CrsDef { + pub srid: Srid, + pub name: Box, + pub kind: CrsKind, + pub datum: Datum, + /// The map projection. `None` for a geographic CRS. + pub projection: Option, + /// Unit of projected coordinates. Geographic coordinates are always + /// degrees and ignore it. + pub unit: LinearUnit, + /// Order and direction of the coordinate axes, PROJ `+axis`. + pub axis: AxisOrder, + pub area_of_use: Option, +} + +/// Number of fields in the MessagePack array form of [`CrsDef`]. +const CRS_DEF_FIELDS: usize = 8; + +impl CrsDef { + /// Whether coordinates are longitude / latitude degrees. + pub fn is_geographic(&self) -> bool { + match self.kind { + CrsKind::Geographic => true, + CrsKind::Projected => false, + } + } +} + +impl zerompk::ToMessagePack for CrsDef { + fn write(&self, writer: &mut W) -> zerompk::Result<()> { + writer.write_array_len(CRS_DEF_FIELDS)?; + self.srid.write(writer)?; + writer.write_string(&self.name)?; + self.kind.write(writer)?; + self.datum.write(writer)?; + self.projection.write(writer)?; + self.unit.write(writer)?; + self.axis.write(writer)?; + self.area_of_use.write(writer) + } +} + +impl<'a> zerompk::FromMessagePack<'a> for CrsDef { + fn read>(reader: &mut R) -> zerompk::Result { + reader.check_array_len(CRS_DEF_FIELDS)?; + Ok(CrsDef { + srid: Srid::read(reader)?, + name: String::read(reader)?.into_boxed_str(), + kind: CrsKind::read(reader)?, + datum: Datum::read(reader)?, + projection: Option::::read(reader)?, + unit: LinearUnit::read(reader)?, + axis: AxisOrder::read(reader)?, + area_of_use: Option::::read(reader)?, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::def::{DatumShift, Ellipsoid, Helmert, RotationConvention}; + use crate::method::TmParams; + + fn sample() -> CrsDef { + let mut helmert = Helmert::position_vector([1.0, -2.5, 3.25, 0.1, 0.2, -0.3, 1.5]); + helmert.convention = RotationConvention::CoordinateFrame; + helmert.accuracy_m = Some(2.0); + helmert.rates = Some(crate::def::HelmertRates::new( + [0.001, -0.002, 0.0, 0.0001, 0.0, -0.0003, 0.01], + 2020.0, + )); + CrsDef { + srid: Srid::new(910_001).unwrap(), + name: "Test TM".into(), + kind: CrsKind::Projected, + datum: Datum { + ellipsoid: Ellipsoid::EVEREST_1830_MODIFIED, + prime_meridian_deg: 2.5, + to_wgs84: DatumShift::Helmert(helmert), + epsg_code: Some(6245), + }, + projection: Some(ProjectionDef::TransverseMercator(TmParams { + lat0_deg: 1.0, + lon0_deg: 102.0, + k0: 0.9999, + false_easting: 1.0e5, + false_northing: -2.0e5, + })), + unit: LinearUnit { + metres_per_unit: 0.3048, + }, + axis: AxisOrder::parse("swu").unwrap(), + area_of_use: AreaOfUse::parse("99,1,105,7").unwrap(), + } + } + + #[test] + fn msgpack_round_trip_is_exact() { + let def = sample(); + let bytes = zerompk::to_msgpack_vec(&def).unwrap(); + let back: CrsDef = zerompk::from_msgpack(&bytes).unwrap(); + assert_eq!(back, def); + } + + #[test] + fn kind_helper() { + assert!(!sample().is_geographic()); + } +} diff --git a/nodedb-crs/src/def/datum.rs b/nodedb-crs/src/def/datum.rs new file mode 100644 index 000000000..4c9ff28cc --- /dev/null +++ b/nodedb-crs/src/def/datum.rs @@ -0,0 +1,420 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Geodetic datums, their shift to WGS 84, and the PROJ `+datum` and `+pm` +//! names. + +use serde::{Deserialize, Serialize}; + +use super::ellipsoid::Ellipsoid; +use super::helmert_rates::HelmertRates; + +/// A geodetic datum: an ellipsoid, a prime meridian, and the shift that +/// takes its geocentric coordinates to WGS 84. +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Serialize, + Deserialize, + zerompk::ToMessagePack, + zerompk::FromMessagePack, +)] +pub struct Datum { + pub ellipsoid: Ellipsoid, + /// Prime meridian, degrees east of Greenwich. + pub prime_meridian_deg: f64, + pub to_wgs84: DatumShift, + /// EPSG code of the datum, when the CRS comes from the EPSG table. + /// PROJ strings do not carry it. + pub epsg_code: Option, +} + +/// How a datum's geocentric coordinates map to WGS 84. +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Serialize, + Deserialize, + zerompk::ToMessagePack, + zerompk::FromMessagePack, +)] +pub enum DatumShift { + /// No shift is known. On the WGS 84 ellipsoid this is WGS 84 itself. + /// On any other ellipsoid the datum is unknown, and a transform passes + /// its geodetic coordinates through unchanged, as PROJ does. + Identity, + /// A 3-, 7- or 15-parameter Helmert transformation to WGS 84. + Helmert(Helmert), +} + +/// A Helmert transformation in the small-angle form PROJ `+towgs84` uses, +/// with optional yearly rates for the time-dependent form. +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Serialize, + Deserialize, + zerompk::ToMessagePack, + zerompk::FromMessagePack, +)] +pub struct Helmert { + pub tx_m: f64, + pub ty_m: f64, + pub tz_m: f64, + pub rx_arcsec: f64, + pub ry_arcsec: f64, + pub rz_arcsec: f64, + pub scale_ppm: f64, + pub convention: RotationConvention, + /// Published accuracy of the transformation, metres. + pub accuracy_m: Option, + /// Yearly rates and their reference epoch. `None` for a static + /// transformation. The static parameters above hold at the reference + /// epoch. + pub rates: Option, +} + +/// Sign convention of the Helmert rotations. +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Eq, + Serialize, + Deserialize, + zerompk::ToMessagePack, + zerompk::FromMessagePack, +)] +pub enum RotationConvention { + /// EPSG method 1033 / 9606. PROJ `+towgs84` uses this convention. + PositionVector, + /// EPSG method 1032 / 9607. Rotations are the negated position-vector + /// rotations. + CoordinateFrame, +} + +impl Helmert { + /// A translation-only transformation. + pub const fn translation(tx_m: f64, ty_m: f64, tz_m: f64) -> Helmert { + Helmert::position_vector([tx_m, ty_m, tz_m, 0.0, 0.0, 0.0, 0.0]) + } + + /// A position-vector transformation from the seven `+towgs84` values: + /// tx, ty, tz metres, rx, ry, rz arc-seconds, scale ppm. + pub const fn position_vector(p: [f64; 7]) -> Helmert { + Helmert { + tx_m: p[0], + ty_m: p[1], + tz_m: p[2], + rx_arcsec: p[3], + ry_arcsec: p[4], + rz_arcsec: p[5], + scale_ppm: p[6], + convention: RotationConvention::PositionVector, + accuracy_m: None, + rates: None, + } + } + + /// Rotations in the position-vector convention, arc-seconds. + pub fn position_vector_rotations(&self) -> [f64; 3] { + let r = [self.rx_arcsec, self.ry_arcsec, self.rz_arcsec]; + match self.convention { + RotationConvention::PositionVector => r, + // 0 − v keeps a zero rotation +0, so it never prints as "-0". + RotationConvention::CoordinateFrame => r.map(|v| 0.0 - v), + } + } + + /// The seven `+towgs84` values, position-vector convention. + pub fn to_towgs84(&self) -> [f64; 7] { + let [rx, ry, rz] = self.position_vector_rotations(); + [self.tx_m, self.ty_m, self.tz_m, rx, ry, rz, self.scale_ppm] + } + + /// The seven rates in `+towgs84` order and position-vector convention, + /// per year, and the reference epoch. `None` for a static + /// transformation. + pub fn position_vector_rates(&self) -> Option<([f64; 7], f64)> { + self.rates.map(|r| { + let mut v = r.values(); + if self.convention == RotationConvention::CoordinateFrame { + for rate in &mut v[3..6] { + // 0 − v keeps a zero rate +0. + *rate = 0.0 - *rate; + } + } + (v, r.reference_epoch) + }) + } + + /// Whether every parameter and every rate is zero. + pub fn is_zero(&self) -> bool { + self.to_towgs84().iter().all(|v| *v == 0.0) + && self + .rates + .is_none_or(|r| r.values().iter().all(|v| *v == 0.0)) + } +} + +impl DatumShift { + /// Whether the shift moves no coordinate: `Identity` or an all-zero + /// Helmert. + pub fn is_null(&self) -> bool { + match self { + DatumShift::Identity => true, + DatumShift::Helmert(h) => h.is_zero(), + } + } + + /// Whether two shifts move coordinates the same way. + pub fn same_effect(&self, other: &DatumShift) -> bool { + match (self, other) { + (DatumShift::Helmert(a), DatumShift::Helmert(b)) => { + a.to_towgs84() == b.to_towgs84() + && a.position_vector_rates() == b.position_vector_rates() + } + (DatumShift::Identity, DatumShift::Helmert(_)) + | (DatumShift::Helmert(_), DatumShift::Identity) + | (DatumShift::Identity, DatumShift::Identity) => self.is_null() && other.is_null(), + } + } +} + +impl Datum { + /// The WGS 84 datum. + pub const WGS84: Datum = Datum { + ellipsoid: Ellipsoid::WGS84, + prime_meridian_deg: 0.0, + to_wgs84: DatumShift::Identity, + epsg_code: None, + }; + + /// Whether the datum's relation to WGS 84 is known: it carries a + /// Helmert shift, or it is WGS 84 itself. + pub fn is_known(&self) -> bool { + match self.to_wgs84 { + DatumShift::Helmert(_) => true, + DatumShift::Identity => self.ellipsoid == Ellipsoid::WGS84, + } + } + + /// Whether two datums are one datum, so a transform between them runs + /// no shift. + /// + /// Equal EPSG codes name one datum, even when the two `to_wgs84` + /// shifts differ: the EPSG table picks each CRS's best shift for its + /// own area of use. PROJ, given the two EPSG codes, applies no shift + /// between a projected CRS and its geographic base either. Without + /// both codes, two datums are one when the ellipsoid matches and the + /// shifts move coordinates the same way. + pub fn same_datum(&self, other: &Datum) -> bool { + match (self.epsg_code, other.epsg_code) { + (Some(a), Some(b)) if a == b => true, + (Some(_) | None, Some(_) | None) => { + self.ellipsoid == other.ellipsoid && self.to_wgs84.same_effect(&other.to_wgs84) + } + } + } +} + +/// A PROJ `+datum` name: its ellipsoid and its `+towgs84` values. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct NamedDatum { + pub ellipsoid: Ellipsoid, + pub shift: DatumShift, +} + +/// The datum PROJ calls `name` in `+datum`. Grid-based datums such as +/// NAD27 are absent: this crate applies no grid shifts. +pub fn datum_by_proj_name(name: &str) -> Option { + let (ellipsoid, shift) = match name { + "WGS84" => (Ellipsoid::WGS84, DatumShift::Identity), + "NAD83" => ( + Ellipsoid::GRS80, + DatumShift::Helmert(Helmert::translation(0.0, 0.0, 0.0)), + ), + "GGRS87" => ( + Ellipsoid::GRS80, + DatumShift::Helmert(Helmert::translation(-199.87, 74.79, 246.62)), + ), + "potsdam" => ( + Ellipsoid::BESSEL_1841, + DatumShift::Helmert(Helmert::position_vector([ + 598.1, 73.7, 418.2, 0.202, 0.045, -2.455, 6.7, + ])), + ), + "hermannskogel" => ( + Ellipsoid::BESSEL_1841, + DatumShift::Helmert(Helmert::position_vector([ + 577.326, 90.129, 463.919, 5.137, 1.474, 5.297, 2.4232, + ])), + ), + "ire65" => ( + Ellipsoid::AIRY_MODIFIED, + DatumShift::Helmert(Helmert::position_vector([ + 482.530, -130.596, 564.557, -1.042, -0.214, -0.631, 8.15, + ])), + ), + "nzgd49" => ( + Ellipsoid::INTERNATIONAL_1924, + DatumShift::Helmert(Helmert::position_vector([ + 59.47, -5.04, 187.44, 0.47, -0.1, 1.024, -4.5993, + ])), + ), + "OSGB36" => ( + Ellipsoid::AIRY_1830, + DatumShift::Helmert(Helmert::position_vector([ + 446.448, -125.157, 542.060, 0.1502, 0.2470, 0.8421, -20.4894, + ])), + ), + _ => return None, + }; + Some(NamedDatum { ellipsoid, shift }) +} + +/// Seconds in a degree. +const SECONDS_PER_DEGREE: f64 = 3600.0; + +/// Minutes in a degree. +const MINUTES_PER_DEGREE: f64 = 60.0; + +/// Degrees from degrees, minutes and seconds. `east` false gives a west +/// (negative) longitude. +const fn dms(d: f64, m: f64, s: f64, east: bool) -> f64 { + let deg = d + m / MINUTES_PER_DEGREE + s / SECONDS_PER_DEGREE; + if east { deg } else { -deg } +} + +/// PROJ `+pm` names, degrees east of Greenwich, from PROJ's DMS values. +const PRIME_MERIDIANS: &[(&str, f64)] = &[ + ("greenwich", 0.0), + ("lisbon", dms(9.0, 7.0, 54.862, false)), + ("paris", dms(2.0, 20.0, 14.025, true)), + ("bogota", dms(74.0, 4.0, 51.3, false)), + ("madrid", dms(3.0, 41.0, 16.58, false)), + ("rome", dms(12.0, 27.0, 8.4, true)), + ("bern", dms(7.0, 26.0, 22.5, true)), + ("jakarta", dms(106.0, 48.0, 27.79, true)), + ("ferro", dms(17.0, 40.0, 0.0, false)), + ("brussels", dms(4.0, 22.0, 4.71, true)), + ("stockholm", dms(18.0, 3.0, 29.8, true)), + ("athens", dms(23.0, 42.0, 58.815, true)), + ("oslo", dms(10.0, 43.0, 22.5, true)), + ("copenhagen", dms(12.0, 34.0, 40.35, true)), +]; + +/// The prime meridian PROJ calls `name` in `+pm`, degrees east. +pub fn prime_meridian_by_proj_name(name: &str) -> Option { + PRIME_MERIDIANS + .iter() + .find(|(n, _)| *n == name) + .map(|(_, deg)| *deg) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn coordinate_frame_negates_rotations() { + let mut h = Helmert::position_vector([1.0, 2.0, 3.0, 0.1, 0.2, 0.3, 4.0]); + h.convention = RotationConvention::CoordinateFrame; + assert_eq!(h.to_towgs84(), [1.0, 2.0, 3.0, -0.1, -0.2, -0.3, 4.0]); + } + + #[test] + fn at_epoch_adds_rate_times_elapsed_years() { + let mut h = Helmert::position_vector([1.0, 2.0, 3.0, 0.1, 0.2, 0.3, 4.0]); + h.rates = Some(HelmertRates::new( + [0.5, -1.0, 0.25, 0.01, -0.02, 0.03, 0.5], + 2010.0, + )); + assert_eq!(h.at_epoch(2010.0).to_towgs84(), h.to_towgs84()); + let later = h.at_epoch(2012.0); + assert_eq!(later.rates, None); + let want = [2.0, 0.0, 3.5, 0.12, 0.16, 0.36, 5.0]; + for (got, want) in later.to_towgs84().iter().zip(want) { + assert!((got - want).abs() < 1e-12, "{got} {want}"); + } + let fixed = Helmert::translation(1.0, 2.0, 3.0); + assert_eq!(fixed.at_epoch(1950.0), fixed); + } + + #[test] + fn rates_decide_null_and_same_effect() { + let mut moving = Helmert::translation(0.0, 0.0, 0.0); + moving.rates = Some(HelmertRates::new( + [0.0, 0.0, 0.001, 0.0, 0.0, 0.0, 0.0], + 2000.0, + )); + assert!(!moving.is_zero()); + assert!(!DatumShift::Helmert(moving).same_effect(&DatumShift::Identity)); + let mut cf = moving; + cf.convention = RotationConvention::CoordinateFrame; + assert!(DatumShift::Helmert(moving).same_effect(&DatumShift::Helmert(cf))); + let mut rotating = moving; + rotating.rates = Some(HelmertRates::new( + [0.0, 0.0, 0.0, 0.1, 0.0, 0.0, 0.0], + 2000.0, + )); + let mut negated = rotating; + negated.convention = RotationConvention::CoordinateFrame; + negated.rates = Some(HelmertRates::new( + [0.0, 0.0, 0.0, -0.1, 0.0, 0.0, 0.0], + 2000.0, + )); + assert!(DatumShift::Helmert(rotating).same_effect(&DatumShift::Helmert(negated))); + assert!(!DatumShift::Helmert(rotating).same_effect(&DatumShift::Helmert(moving))); + } + + #[test] + fn null_shifts_have_the_same_effect() { + let zero = DatumShift::Helmert(Helmert::translation(0.0, 0.0, 0.0)); + assert!(zero.same_effect(&DatumShift::Identity)); + let moved = DatumShift::Helmert(Helmert::translation(1.0, 0.0, 0.0)); + assert!(!moved.same_effect(&DatumShift::Identity)); + } + + #[test] + fn known_datums() { + assert!(Datum::WGS84.is_known()); + let sphere = Datum { + ellipsoid: Ellipsoid::sphere(6_371_007.181), + ..Datum::WGS84 + }; + assert!(!sphere.is_known()); + } + + #[test] + fn epsg_code_decides_same_datum() { + let shifted = |tx: f64, code: Option| Datum { + ellipsoid: Ellipsoid::INTERNATIONAL_1924, + prime_meridian_deg: 0.0, + to_wgs84: DatumShift::Helmert(Helmert::translation(tx, 0.0, 0.0)), + epsg_code: code, + }; + assert!(shifted(-87.0, Some(6230)).same_datum(&shifted(-90.0, Some(6230)))); + assert!(!shifted(-87.0, Some(6230)).same_datum(&shifted(-90.0, Some(6231)))); + assert!(!shifted(-87.0, None).same_datum(&shifted(-90.0, Some(6230)))); + assert!(shifted(-87.0, Some(6230)).same_datum(&shifted(-87.0, None))); + let wgs84_ellipsoid = Datum { + ellipsoid: Ellipsoid::WGS84, + ..shifted(-191.9, Some(6756)) + }; + assert!(!wgs84_ellipsoid.same_datum(&Datum::WGS84)); + } + + #[test] + fn paris_meridian() { + let paris = prime_meridian_by_proj_name("paris").unwrap(); + assert!((paris - 2.337_229_166_667).abs() < 1e-11); + assert!(datum_by_proj_name("NAD27").is_none()); + } +} diff --git a/nodedb-crs/src/def/ellipsoid.rs b/nodedb-crs/src/def/ellipsoid.rs new file mode 100644 index 000000000..09b25d46e --- /dev/null +++ b/nodedb-crs/src/def/ellipsoid.rs @@ -0,0 +1,349 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Reference ellipsoids and the PROJ `+ellps` names. + +use serde::{Deserialize, Serialize}; + +/// An ellipsoid of revolution, defined by its semi-major axis and inverse +/// flattening. `inv_f == 0` marks a sphere. +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Serialize, + Deserialize, + zerompk::ToMessagePack, + zerompk::FromMessagePack, +)] +pub struct Ellipsoid { + /// Semi-major axis in metres. + pub a: f64, + /// Inverse flattening 1/f, or 0 for a sphere. + pub inv_f: f64, +} + +/// Build an ellipsoid from a and b, the form PROJ uses for some names. +const fn from_a_b(a: f64, b: f64) -> Ellipsoid { + if a == b { + Ellipsoid { a, inv_f: 0.0 } + } else { + Ellipsoid { + a, + inv_f: a / (a - b), + } + } +} + +impl Ellipsoid { + /// WGS 84 (EPSG:7030). + pub const WGS84: Ellipsoid = Ellipsoid { + a: 6_378_137.0, + inv_f: 298.257_223_563, + }; + /// GRS 1980 (EPSG:7019). + pub const GRS80: Ellipsoid = Ellipsoid { + a: 6_378_137.0, + inv_f: 298.257_222_101, + }; + /// WGS 72 (EPSG:7043). + pub const WGS72: Ellipsoid = Ellipsoid { + a: 6_378_135.0, + inv_f: 298.26, + }; + /// Everest 1830 (1937 adjustment), PROJ `evrst30`. + pub const EVEREST_1830: Ellipsoid = Ellipsoid { + a: 6_377_276.345, + inv_f: 300.8017, + }; + /// Everest (Sabah and Sarawak), PROJ `evrstSS`. + pub const EVEREST_SABAH_SARAWAK: Ellipsoid = Ellipsoid { + a: 6_377_298.556, + inv_f: 300.8017, + }; + /// Everest 1830 Modified, the Kertau ellipsoid (EPSG:7018), PROJ `evrst48`. + pub const EVEREST_1830_MODIFIED: Ellipsoid = Ellipsoid { + a: 6_377_304.063, + inv_f: 300.8017, + }; + /// Everest 1956, PROJ `evrst56`. + pub const EVEREST_1956: Ellipsoid = Ellipsoid { + a: 6_377_301.243, + inv_f: 300.8017, + }; + /// Everest 1969, PROJ `evrst69`. + pub const EVEREST_1969: Ellipsoid = Ellipsoid { + a: 6_377_295.664, + inv_f: 300.8017, + }; + /// International 1924, PROJ `intl`. + pub const INTERNATIONAL_1924: Ellipsoid = Ellipsoid { + a: 6_378_388.0, + inv_f: 297.0, + }; + /// Clarke 1866, PROJ `clrk66`. + pub const CLARKE_1866: Ellipsoid = from_a_b(6_378_206.4, 6_356_583.8); + /// Clarke 1880 (modified), PROJ `clrk80`. + pub const CLARKE_1880: Ellipsoid = Ellipsoid { + a: 6_378_249.145, + inv_f: 293.4663, + }; + /// Clarke 1880 (IGN), PROJ `clrk80ign`. + pub const CLARKE_1880_IGN: Ellipsoid = Ellipsoid { + a: 6_378_249.2, + inv_f: 293.466_021_293_626_9, + }; + /// Bessel 1841, PROJ `bessel`. + pub const BESSEL_1841: Ellipsoid = Ellipsoid { + a: 6_377_397.155, + inv_f: 299.152_812_8, + }; + /// Bessel 1841 (Namibia), PROJ `bess_nam`. + pub const BESSEL_NAMIBIA: Ellipsoid = Ellipsoid { + a: 6_377_483.865, + inv_f: 299.152_812_8, + }; + /// Krassovsky 1940, PROJ `krass`. + pub const KRASSOVSKY: Ellipsoid = Ellipsoid { + a: 6_378_245.0, + inv_f: 298.3, + }; + /// Airy 1830, PROJ `airy`. + pub const AIRY_1830: Ellipsoid = Ellipsoid { + a: 6_377_563.396, + inv_f: 299.324_964_6, + }; + /// Airy Modified 1849, PROJ `mod_airy`. + pub const AIRY_MODIFIED: Ellipsoid = from_a_b(6_377_340.189, 6_356_034.446); + /// Australian National and South American 1969, PROJ `aust_SA`. + pub const AUSTRALIAN_SA: Ellipsoid = Ellipsoid { + a: 6_378_160.0, + inv_f: 298.25, + }; + /// Helmert 1906, PROJ `helmert`. + pub const HELMERT_1906: Ellipsoid = Ellipsoid { + a: 6_378_200.0, + inv_f: 298.3, + }; + /// GRS 1967, PROJ `GRS67`. + pub const GRS67: Ellipsoid = Ellipsoid { + a: 6_378_160.0, + inv_f: 298.247_167_427, + }; + /// WGS 66, PROJ `WGS66`. + pub const WGS66: Ellipsoid = Ellipsoid { + a: 6_378_145.0, + inv_f: 298.25, + }; + /// IAU 1976, PROJ `IAU76`. + pub const IAU76: Ellipsoid = Ellipsoid { + a: 6_378_140.0, + inv_f: 298.257, + }; + /// PZ-90, PROJ `PZ90`. + pub const PZ90: Ellipsoid = Ellipsoid { + a: 6_378_136.0, + inv_f: 298.257_84, + }; + /// GSK-2011, PROJ `GSK2011`. + pub const GSK2011: Ellipsoid = Ellipsoid { + a: 6_378_136.5, + inv_f: 298.256_415_1, + }; + /// Fischer (Mercury Datum) 1960 modified, PROJ `fschr60m`. + pub const FISCHER_1960_MODIFIED: Ellipsoid = Ellipsoid { + a: 6_378_155.0, + inv_f: 298.3, + }; + /// Naval Weapons Lab 1965, PROJ `NWL9D`. Same values as WGS 66. + pub const NWL9D: Ellipsoid = Ellipsoid { + a: 6_378_145.0, + inv_f: 298.25, + }; + /// Hough, PROJ `hough`. + pub const HOUGH: Ellipsoid = Ellipsoid { + a: 6_378_270.0, + inv_f: 297.0, + }; + /// Andrae 1876, PROJ `andrae`. + pub const ANDRAE: Ellipsoid = Ellipsoid { + a: 6_377_104.43, + inv_f: 300.0, + }; + /// New International 1967, PROJ `new_intl`. + pub const NEW_INTERNATIONAL_1967: Ellipsoid = from_a_b(6_378_157.5, 6_356_772.2); + /// Southeast Asia, PROJ `SEasia`. + pub const SOUTHEAST_ASIA: Ellipsoid = from_a_b(6_378_155.0, 6_356_773.320_5); + /// Walbeck, PROJ `walbeck`. + pub const WALBECK: Ellipsoid = from_a_b(6_376_896.0, 6_355_834.846_7); + /// The PROJ `sphere`, radius 6 370 997 m. + pub const SPHERE: Ellipsoid = Ellipsoid { + a: 6_370_997.0, + inv_f: 0.0, + }; + + /// A sphere of radius `r` metres. + pub const fn sphere(r: f64) -> Ellipsoid { + Ellipsoid { a: r, inv_f: 0.0 } + } + + /// Whether this is a sphere. + pub fn is_sphere(&self) -> bool { + self.inv_f == 0.0 + } + + /// Flattening f, 0 for a sphere. + pub fn f(&self) -> f64 { + if self.is_sphere() { + 0.0 + } else { + 1.0 / self.inv_f + } + } + + /// Semi-minor axis b = a(1 − f). + pub fn b(&self) -> f64 { + self.a * (1.0 - self.f()) + } + + /// First eccentricity squared, e² = f(2 − f). + pub fn e2(&self) -> f64 { + let f = self.f(); + f * (2.0 - f) + } + + /// First eccentricity e. + pub fn e(&self) -> f64 { + self.e2().sqrt() + } + + /// Third flattening n = f / (2 − f). + pub fn n(&self) -> f64 { + let f = self.f(); + f / (2.0 - f) + } + + /// The ellipsoid PROJ calls `name` in `+ellps`. + pub fn by_proj_name(name: &str) -> Option { + NAMED + .iter() + .find(|(n, _)| *n == name) + .map(|(_, ellipsoid)| *ellipsoid) + } + + /// The ellipsoid with EPSG code `code`, for the ellipsoids this crate + /// names. + pub fn by_epsg_code(code: u16) -> Option { + BY_EPSG_CODE + .iter() + .find(|(c, _)| *c == code) + .map(|(_, ellipsoid)| *ellipsoid) + } + + /// A PROJ `+ellps` name of this exact ellipsoid, if it has one. Where + /// two names share values, the first in the table wins. + pub fn proj_name(&self) -> Option<&'static str> { + NAMED + .iter() + .find(|(_, ellipsoid)| ellipsoid == self) + .map(|(n, _)| *n) + } +} + +/// EPSG ellipsoid codes of the named ellipsoids. +const BY_EPSG_CODE: &[(u16, Ellipsoid)] = &[ + (7001, Ellipsoid::AIRY_1830), + (7003, Ellipsoid::AUSTRALIAN_SA), + (7004, Ellipsoid::BESSEL_1841), + (7008, Ellipsoid::CLARKE_1866), + (7015, Ellipsoid::EVEREST_1830), + (7016, Ellipsoid::EVEREST_SABAH_SARAWAK), + (7018, Ellipsoid::EVEREST_1830_MODIFIED), + (7019, Ellipsoid::GRS80), + (7022, Ellipsoid::INTERNATIONAL_1924), + (7024, Ellipsoid::KRASSOVSKY), + (7030, Ellipsoid::WGS84), + (7036, Ellipsoid::GRS67), + (7043, Ellipsoid::WGS72), + (7044, Ellipsoid::EVEREST_1956), + (7056, Ellipsoid::EVEREST_1969), +]; + +/// PROJ `+ellps` names with their exact defining values. +const NAMED: &[(&str, Ellipsoid)] = &[ + ("WGS84", Ellipsoid::WGS84), + ("GRS80", Ellipsoid::GRS80), + ("WGS72", Ellipsoid::WGS72), + ("evrst30", Ellipsoid::EVEREST_1830), + ("evrstSS", Ellipsoid::EVEREST_SABAH_SARAWAK), + ("evrst48", Ellipsoid::EVEREST_1830_MODIFIED), + ("evrst56", Ellipsoid::EVEREST_1956), + ("evrst69", Ellipsoid::EVEREST_1969), + ("intl", Ellipsoid::INTERNATIONAL_1924), + ("clrk66", Ellipsoid::CLARKE_1866), + ("clrk80", Ellipsoid::CLARKE_1880), + ("clrk80ign", Ellipsoid::CLARKE_1880_IGN), + ("bessel", Ellipsoid::BESSEL_1841), + ("bess_nam", Ellipsoid::BESSEL_NAMIBIA), + ("krass", Ellipsoid::KRASSOVSKY), + ("airy", Ellipsoid::AIRY_1830), + ("mod_airy", Ellipsoid::AIRY_MODIFIED), + ("aust_SA", Ellipsoid::AUSTRALIAN_SA), + ("helmert", Ellipsoid::HELMERT_1906), + ("GRS67", Ellipsoid::GRS67), + ("WGS66", Ellipsoid::WGS66), + ("IAU76", Ellipsoid::IAU76), + ("PZ90", Ellipsoid::PZ90), + ("GSK2011", Ellipsoid::GSK2011), + ("NWL9D", Ellipsoid::NWL9D), + ("fschr60m", Ellipsoid::FISCHER_1960_MODIFIED), + ("hough", Ellipsoid::HOUGH), + ("andrae", Ellipsoid::ANDRAE), + ("new_intl", Ellipsoid::NEW_INTERNATIONAL_1967), + ("SEasia", Ellipsoid::SOUTHEAST_ASIA), + ("walbeck", Ellipsoid::WALBECK), + ("sphere", Ellipsoid::SPHERE), +]; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn grs80_and_wgs84_differ_only_in_flattening() { + assert_eq!(Ellipsoid::GRS80.a, Ellipsoid::WGS84.a); + assert!((Ellipsoid::GRS80.e2() - 0.006_694_380_022_90).abs() < 1e-14); + assert!((Ellipsoid::WGS84.e2() - 0.006_694_379_990_14).abs() < 1e-14); + } + + #[test] + fn sphere_has_no_flattening() { + let s = Ellipsoid::sphere(6_371_007.181); + assert_eq!(s.f(), 0.0); + assert_eq!(s.e(), 0.0); + assert_eq!(s.b(), s.a); + } + + #[test] + fn names_round_trip() { + for (name, ellipsoid) in NAMED { + assert_eq!(Ellipsoid::by_proj_name(name), Some(*ellipsoid)); + let canonical = ellipsoid.proj_name().and_then(Ellipsoid::by_proj_name); + assert_eq!(canonical, Some(*ellipsoid)); + } + assert_eq!(Ellipsoid::by_proj_name("nope"), None); + } + + #[test] + fn epsg_codes() { + assert_eq!(Ellipsoid::by_epsg_code(7030), Some(Ellipsoid::WGS84)); + assert_eq!(Ellipsoid::by_epsg_code(7019), Some(Ellipsoid::GRS80)); + assert_eq!(Ellipsoid::by_epsg_code(7000), None); + } + + #[test] + fn a_b_form_keeps_b() { + let c = Ellipsoid::CLARKE_1866; + assert!((c.b() - 6_356_583.8).abs() < 1e-6); + } +} diff --git a/nodedb-crs/src/def/epoch.rs b/nodedb-crs/src/def/epoch.rs new file mode 100644 index 000000000..45ce409f5 --- /dev/null +++ b/nodedb-crs/src/def/epoch.rs @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! The coordinate epoch: the decimal year a set of coordinates refers to. + +use crate::error::InvalidEpoch; + +/// Earliest coordinate epoch accepted, decimal year. +pub const MIN_COORDINATE_EPOCH: f64 = 1900.0; + +/// Latest coordinate epoch accepted, decimal year. +pub const MAX_COORDINATE_EPOCH: f64 = 2200.0; + +/// A coordinate epoch, a decimal year in +/// [`MIN_COORDINATE_EPOCH`]`..=`[`MAX_COORDINATE_EPOCH`]. +/// +/// A time-dependent Helmert transformation evaluates its parameters at this +/// epoch. +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] +pub struct CoordinateEpoch(f64); + +impl CoordinateEpoch { + /// The epoch `year`. A non-finite or out-of-range year is refused. + pub fn new(year: f64) -> Result { + if (MIN_COORDINATE_EPOCH..=MAX_COORDINATE_EPOCH).contains(&year) { + Ok(CoordinateEpoch(year)) + } else { + Err(InvalidEpoch { epoch: year }) + } + } + + /// The decimal year. + pub fn year(self) -> f64 { + self.0 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn range_is_inclusive() { + assert_eq!( + CoordinateEpoch::new(1900.0).map(CoordinateEpoch::year), + Ok(1900.0) + ); + assert_eq!( + CoordinateEpoch::new(2200.0).map(CoordinateEpoch::year), + Ok(2200.0) + ); + assert_eq!( + CoordinateEpoch::new(2020.5).map(CoordinateEpoch::year), + Ok(2020.5) + ); + } + + #[test] + fn out_of_range_and_non_finite_are_refused() { + for year in [1899.999, 2200.001, f64::NAN, f64::INFINITY, -2020.0] { + let err = CoordinateEpoch::new(year).unwrap_err(); + assert!(err.epoch.is_nan() || err.epoch == year); + assert!(err.to_string().contains("1900"), "{err}"); + } + } +} diff --git a/nodedb-crs/src/def/helmert_rates.rs b/nodedb-crs/src/def/helmert_rates.rs new file mode 100644 index 000000000..ceb0c3dcb --- /dev/null +++ b/nodedb-crs/src/def/helmert_rates.rs @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Rates of change of the seven Helmert parameters: the time-dependent +//! (15-parameter) Helmert transformation, IOGP Guidance Note 7-2 §4.2.5, +//! EPSG methods 1053 to 1058. + +use serde::{Deserialize, Serialize}; + +/// Yearly rates of the seven Helmert parameters and the epoch the static +/// parameters hold at. +/// +/// At coordinate epoch t each parameter p takes the value +/// p + ṗ (t − t0), where t0 is `reference_epoch`. +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Serialize, + Deserialize, + zerompk::ToMessagePack, + zerompk::FromMessagePack, +)] +pub struct HelmertRates { + pub dtx_m_per_yr: f64, + pub dty_m_per_yr: f64, + pub dtz_m_per_yr: f64, + pub drx_arcsec_per_yr: f64, + pub dry_arcsec_per_yr: f64, + pub drz_arcsec_per_yr: f64, + pub dscale_ppm_per_yr: f64, + /// Parameter reference epoch t0, decimal year. + pub reference_epoch: f64, +} + +impl HelmertRates { + /// Rates from the seven values in `+towgs84` order: tx, ty, tz metres + /// per year, rx, ry, rz arc-seconds per year, scale ppm per year. + pub const fn new(rates: [f64; 7], reference_epoch: f64) -> HelmertRates { + HelmertRates { + dtx_m_per_yr: rates[0], + dty_m_per_yr: rates[1], + dtz_m_per_yr: rates[2], + drx_arcsec_per_yr: rates[3], + dry_arcsec_per_yr: rates[4], + drz_arcsec_per_yr: rates[5], + dscale_ppm_per_yr: rates[6], + reference_epoch, + } + } + + /// The seven rates in `+towgs84` order, in the stored rotation + /// convention. + pub fn values(&self) -> [f64; 7] { + [ + self.dtx_m_per_yr, + self.dty_m_per_yr, + self.dtz_m_per_yr, + self.drx_arcsec_per_yr, + self.dry_arcsec_per_yr, + self.drz_arcsec_per_yr, + self.dscale_ppm_per_yr, + ] + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn values_follow_towgs84_order() { + let r = HelmertRates::new([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0], 2010.0); + assert_eq!(r.values(), [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0]); + assert_eq!(r.reference_epoch, 2010.0); + } +} diff --git a/nodedb-crs/src/def/mod.rs b/nodedb-crs/src/def/mod.rs new file mode 100644 index 000000000..f88cfa7b8 --- /dev/null +++ b/nodedb-crs/src/def/mod.rs @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! CRS definition types. + +pub mod area; +pub mod axis; +pub mod crs_def; +pub mod datum; +pub mod ellipsoid; +pub mod epoch; +pub mod helmert_rates; +pub mod unit; + +pub use area::AreaOfUse; +pub use axis::{AxisDirection, AxisOrder}; +pub use crs_def::{CrsDef, CrsKind}; +pub use datum::{ + Datum, DatumShift, Helmert, NamedDatum, RotationConvention, datum_by_proj_name, + prime_meridian_by_proj_name, +}; +pub use ellipsoid::Ellipsoid; +pub use epoch::{CoordinateEpoch, MAX_COORDINATE_EPOCH, MIN_COORDINATE_EPOCH}; +pub use helmert_rates::HelmertRates; +pub use unit::LinearUnit; diff --git a/nodedb-crs/src/def/unit.rs b/nodedb-crs/src/def/unit.rs new file mode 100644 index 000000000..d036e01f2 --- /dev/null +++ b/nodedb-crs/src/def/unit.rs @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Linear units of projected coordinates and the PROJ `+units` names. + +use serde::{Deserialize, Serialize}; + +/// A linear unit, as a length in metres. +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Serialize, + Deserialize, + zerompk::ToMessagePack, + zerompk::FromMessagePack, +)] +pub struct LinearUnit { + pub metres_per_unit: f64, +} + +impl LinearUnit { + /// The metre. + pub const METRE: LinearUnit = LinearUnit { + metres_per_unit: 1.0, + }; + + /// The unit PROJ calls `name` in `+units`. + pub fn by_proj_name(name: &str) -> Option { + NAMED + .iter() + .find(|(n, _)| *n == name) + .map(|(_, metres_per_unit)| LinearUnit { + metres_per_unit: *metres_per_unit, + }) + } + + /// The unit with EPSG code `code`, for the length units this crate + /// names. + pub fn by_epsg_code(code: u16) -> Option { + BY_EPSG_CODE + .iter() + .find(|(c, _)| *c == code) + .map(|(_, metres_per_unit)| LinearUnit { + metres_per_unit: *metres_per_unit, + }) + } + + /// The PROJ `+units` name of this exact unit, if it has one. + pub fn proj_name(&self) -> Option<&'static str> { + NAMED + .iter() + .find(|(_, m)| *m == self.metres_per_unit) + .map(|(n, _)| *n) + } +} + +impl Default for LinearUnit { + fn default() -> Self { + LinearUnit::METRE + } +} + +/// Metres per US survey inch: 100/3937, the US survey foot divided by 12. +const US_INCH: f64 = 100.0 / 3937.0; + +/// PROJ `+units` names with metres per unit. +const NAMED: &[(&str, f64)] = &[ + ("m", 1.0), + ("km", 1000.0), + ("dm", 0.1), + ("cm", 0.01), + ("mm", 0.001), + ("kmi", 1852.0), + ("in", 0.0254), + ("ft", 0.3048), + ("yd", 0.9144), + ("mi", 1609.344), + ("fath", 1.8288), + ("ch", 20.1168), + ("link", 0.201_168), + ("us-in", US_INCH), + ("us-ft", 1200.0 / 3937.0), + ("us-yd", 36.0 * US_INCH), + ("us-ch", 792.0 * US_INCH), + ("us-mi", 63_360.0 * US_INCH), + ("ind-yd", 0.914_395_23), + ("ind-ft", 0.304_798_41), + ("ind-ch", 20.116_695_06), +]; + +/// EPSG length unit codes with metres per unit: metre, foot, US survey +/// foot and kilometre. +const BY_EPSG_CODE: &[(u16, f64)] = &[ + (9001, 1.0), + (9002, 0.3048), + (9003, 1200.0 / 3937.0), + (9036, 1000.0), +]; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn epsg_codes_name_their_length() { + let metres = |code| LinearUnit::by_epsg_code(code).map(|u| u.metres_per_unit); + assert_eq!(metres(9001), Some(1.0)); + assert_eq!(metres(9002), Some(0.3048)); + assert_eq!(metres(9003), Some(1200.0 / 3937.0)); + assert_eq!(metres(9036), Some(1000.0)); + assert_eq!(metres(9999), None); + } + + #[test] + fn us_survey_foot_is_1200_over_3937() { + let ft = LinearUnit::by_proj_name("us-ft").map(|u| u.metres_per_unit); + assert!(ft.is_some_and(|m| (m - 1200.0 / 3937.0).abs() < 1e-15)); + } + + #[test] + fn names_round_trip() { + for (name, m) in NAMED { + let unit = LinearUnit { + metres_per_unit: *m, + }; + assert_eq!(LinearUnit::by_proj_name(name), Some(unit)); + assert_eq!(unit.proj_name(), Some(*name)); + } + } +} diff --git a/nodedb-crs/src/error.rs b/nodedb-crs/src/error.rs new file mode 100644 index 000000000..c81eff6ea --- /dev/null +++ b/nodedb-crs/src/error.rs @@ -0,0 +1,133 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Errors for SRID resolution, PROJ-string parsing and coordinate transforms. + +pub use nodedb_srid::{CrsParseError, NotUserSrid, UnknownSrid}; + +use crate::srid::Srid; + +/// Any error a CRS operation reports. +#[derive(Debug, Clone, PartialEq, thiserror::Error)] +pub enum CrsError { + /// The coordinate lies outside the region where a projection is defined + /// or accurate. + #[error("coordinate ({x}, {y}) is outside the domain of SRID {srid}: {reason}")] + OutOfDomain { + srid: Srid, + x: f64, + y: f64, + reason: DomainReason, + }, + #[error(transparent)] + Unknown(#[from] UnknownSrid), + #[error(transparent)] + Parse(#[from] CrsParseError), + #[error(transparent)] + Epoch(#[from] InvalidEpoch), + #[error(transparent)] + ProjExport(#[from] ProjExportError), +} + +/// Why a coordinate is outside a projection's domain. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DomainReason { + /// An ordinate is NaN or infinite. + NonFinite, + /// Latitude is beyond ±90°. + LatitudeRange, + /// Longitude is too far from a Transverse Mercator central meridian for + /// the Krüger series to hold. + CentralMeridianOffset, + /// The Hotine Oblique Mercator U term reaches ±1, where the projection + /// diverges. + ObliqueSingularity, + /// The point is a pole or a singular point the projection maps to + /// infinity. + Singularity, + /// Latitude is beyond the projection's limit, such as ±85.06° for + /// Pseudo-Mercator. + LatitudeLimit, + /// A geostationary view does not see the point. + NotVisible, + /// The point is the antipode of an azimuthal projection's centre. + Antipode, + /// Projected coordinates fall outside the area the projection maps. + OutsideProjection, + /// An iterative inverse did not converge. + NoConvergence, +} + +impl core::fmt::Display for DomainReason { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.write_str(match self { + DomainReason::NonFinite => "non-finite ordinate", + DomainReason::LatitudeRange => "latitude beyond ±90°", + DomainReason::CentralMeridianOffset => "too far from the central meridian", + DomainReason::ObliqueSingularity => "on the oblique projection's singular line", + DomainReason::Singularity => "at a point the projection maps to infinity", + DomainReason::LatitudeLimit => "latitude beyond the projection's limit", + DomainReason::NotVisible => "not visible from the geostationary satellite", + DomainReason::Antipode => "at the antipode of the projection centre", + DomainReason::OutsideProjection => "outside the area the projection maps", + DomainReason::NoConvergence => "the inverse projection did not converge", + }) + } +} + +/// A coordinate epoch outside the accepted range, or not a finite number. +#[derive(Debug, Clone, Copy, PartialEq, thiserror::Error)] +#[error( + "coordinate epoch {epoch} is outside {min}..={max} decimal years", + min = crate::def::MIN_COORDINATE_EPOCH, + max = crate::def::MAX_COORDINATE_EPOCH +)] +pub struct InvalidEpoch { + /// The epoch given, decimal year. + pub epoch: f64, +} + +/// A definition a PROJ string cannot express. +#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] +pub enum ProjExportError { + /// `+towgs84` has no rates, so a time-dependent Helmert shift has no + /// PROJ-string form. PROJ refuses the same export. + #[error( + "SRID {srid} has a time-dependent datum shift, which a PROJ string cannot carry; \ + use WKT2 or PROJJSON" + )] + TimeDependentShift { srid: Srid }, +} + +/// A change to a [`CustomCrsSet`](crate::CustomCrsSet) that the set refuses. +#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] +pub enum CustomCrsError { + /// User-defined CRSs take codes from the user range only. + #[error(transparent)] + NotUserCode(#[from] NotUserSrid), + /// A live CRS already has this code. + #[error("SRID {srid} is already defined")] + Exists { srid: Srid }, + /// The code belonged to a dropped CRS. Dropped codes stay retired, so + /// stored data never changes meaning. + #[error("SRID {srid} was dropped and cannot be reused")] + WasDropped { srid: Srid }, + /// No live CRS has this code. + #[error("SRID {srid} is not a live user-defined CRS")] + NotFound { srid: Srid }, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn messages_name_the_input() { + let err = CrsError::from(UnknownSrid::Undefined { code: 2154 }); + assert!(err.to_string().contains("2154")); + let err = CrsError::from(InvalidEpoch { epoch: 2300.0 }); + assert!(err.to_string().contains("2300"), "{err}"); + let srid = Srid::new(910_001).unwrap(); + let err = CrsError::from(ProjExportError::TimeDependentShift { srid }); + assert!(err.to_string().contains("910001"), "{err}"); + } +} diff --git a/nodedb-crs/src/geo_transform.rs b/nodedb-crs/src/geo_transform.rs new file mode 100644 index 000000000..edb8e658b --- /dev/null +++ b/nodedb-crs/src/geo_transform.rs @@ -0,0 +1,174 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! The GDAL-order affine geotransform between pixel and world positions. +//! Arrays, raster readers and raster loads share this one type. + +use serde::{Deserialize, Serialize}; + +/// Affine pixel-to-world transform in GDAL coefficient order. +/// +/// For a continuous pixel position `(u, v)`, `u` along the column axis and +/// `v` along the row axis: +/// +/// - `X = GT0 + u·GT1 + v·GT2` +/// - `Y = GT3 + u·GT4 + v·GT5` +/// +/// Pixel `(i, j)` covers `[i, i+1) × [j, j+1)`. Its centre is `(i+0.5, j+0.5)`. +#[derive( + Debug, Clone, Copy, Serialize, Deserialize, zerompk::ToMessagePack, zerompk::FromMessagePack, +)] +pub struct GeoTransform(pub [f64; 6]); + +impl PartialEq for GeoTransform { + /// Bitwise equality. Validation admits finite coefficients only, so + /// this is equality of the stored values. + fn eq(&self, other: &Self) -> bool { + self.0 + .iter() + .zip(other.0.iter()) + .all(|(a, b)| a.to_bits() == b.to_bits()) + } +} + +impl Eq for GeoTransform {} + +impl GeoTransform { + /// `GT1·GT5 − GT2·GT4`. Zero means the transform has no inverse. + pub fn determinant(&self) -> f64 { + let [_, gt1, gt2, _, gt4, gt5] = self.0; + gt1 * gt5 - gt2 * gt4 + } + + /// Whether every coefficient is finite and the determinant is finite + /// and non-zero. + pub fn is_invertible(&self) -> bool { + let det = self.determinant(); + self.0.iter().all(|c| c.is_finite()) && det != 0.0 && det.is_finite() + } + + /// World position of the continuous pixel position `(u, v)`. + pub fn pixel_to_world(&self, u: f64, v: f64) -> (f64, f64) { + let [gt0, gt1, gt2, gt3, gt4, gt5] = self.0; + (gt0 + u * gt1 + v * gt2, gt3 + u * gt4 + v * gt5) + } + + /// World position of the centre of pixel `(i, j)`. + pub fn cell_centre(&self, i: i64, j: i64) -> (f64, f64) { + self.pixel_to_world(i as f64 + 0.5, j as f64 + 0.5) + } + + /// Continuous pixel position of the world position `(x, y)`. + /// + /// Returns `None` when the determinant is zero or not finite. + pub fn world_to_pixel(&self, x: f64, y: f64) -> Option<(f64, f64)> { + let det = self.determinant(); + if det == 0.0 || !det.is_finite() { + return None; + } + let [gt0, gt1, gt2, gt3, gt4, gt5] = self.0; + let dx = x - gt0; + let dy = y - gt3; + Some(((gt5 * dx - gt2 * dy) / det, (gt1 * dy - gt4 * dx) / det)) + } + + /// Index of the pixel that holds the world position `(x, y)`. + /// + /// Returns `None` when the transform has no inverse or the index falls + /// outside `i64`. + pub fn world_to_cell(&self, x: f64, y: f64) -> Option<(i64, i64)> { + let (u, v) = self.world_to_pixel(x, y)?; + Some((floor_to_i64(u)?, floor_to_i64(v)?)) + } +} + +/// `floor(value)` as `i64`. `None` for a non-finite or out-of-range value. +fn floor_to_i64(value: f64) -> Option { + let floored = value.floor(); + // `i64::MAX as f64` rounds up to 2^63, which is outside `i64`. + if floored.is_finite() && floored >= i64::MIN as f64 && floored < i64::MAX as f64 { + Some(floored as i64) + } else { + None + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const EPS: f64 = 1e-9; + + fn assert_close(a: (f64, f64), b: (f64, f64), eps: f64) { + assert!( + (a.0 - b.0).abs() <= eps && (a.1 - b.1).abs() <= eps, + "{a:?} != {b:?}" + ); + } + + #[test] + fn north_up_centre_is_index_plus_half() { + let gt = GeoTransform([100_000.0, 10.0, 0.0, 200_000.0, 0.0, -10.0]); + assert_close(gt.cell_centre(0, 0), (100_005.0, 199_995.0), EPS); + assert_close(gt.cell_centre(3, 2), (100_035.0, 199_975.0), EPS); + } + + #[test] + fn centre_inverse_round_trip_north_up() { + let gt = GeoTransform([100_000.0, 10.0, 0.0, 200_000.0, 0.0, -10.0]); + for (i, j) in [(0, 0), (7, 3), (255, 191), (-4, 9)] { + let (x, y) = gt.cell_centre(i, j); + let (u, v) = gt.world_to_pixel(x, y).unwrap(); + assert_close((u, v), (i as f64 + 0.5, j as f64 + 0.5), EPS); + assert_eq!(gt.world_to_cell(x, y), Some((i, j))); + } + } + + #[test] + fn centre_inverse_round_trip_rotated() { + let gt = GeoTransform([500_000.0, 8.660254, 5.0, 4_000_000.0, 5.0, -8.660254]); + assert!(gt.determinant().abs() > 0.0); + for (i, j) in [(0, 0), (12, 40), (1023, 511)] { + let (x, y) = gt.cell_centre(i, j); + let (u, v) = gt.world_to_pixel(x, y).unwrap(); + assert_close((u, v), (i as f64 + 0.5, j as f64 + 0.5), 1e-6); + assert_eq!(gt.world_to_cell(x, y), Some((i, j))); + } + } + + #[test] + fn centre_inverse_round_trip_geographic_exponent() { + let res = 8.983152841195214e-05; + let gt = GeoTransform([100.5, res, 0.0, 6.5, 0.0, -res]); + let (x, y) = gt.cell_centre(0, 0); + assert_close((x, y), (100.5 + res / 2.0, 6.5 - res / 2.0), 1e-15); + for (i, j) in [(0, 0), (100, 200), (11_131, 5_000)] { + let (x, y) = gt.cell_centre(i, j); + let (u, v) = gt.world_to_pixel(x, y).unwrap(); + assert_close((u, v), (i as f64 + 0.5, j as f64 + 0.5), 1e-6); + assert_eq!(gt.world_to_cell(x, y), Some((i, j))); + } + } + + #[test] + fn singular_transform_has_no_inverse() { + let gt = GeoTransform([0.0, 1.0, 2.0, 0.0, 2.0, 4.0]); + assert_eq!(gt.determinant(), 0.0); + assert!(gt.world_to_pixel(1.0, 1.0).is_none()); + assert!(gt.world_to_cell(1.0, 1.0).is_none()); + } + + #[test] + fn out_of_range_index_is_none() { + let gt = GeoTransform([0.0, 1e-10, 0.0, 0.0, 0.0, -1e-10]); + assert!(gt.world_to_pixel(1e10, -1.0).is_some()); + assert!(gt.world_to_cell(1e10, -1.0).is_none()); + } + + #[test] + fn invertible_needs_finite_terms_and_a_non_zero_determinant() { + assert!(GeoTransform([0.0, 1.0, 0.0, 0.0, 0.0, -1.0]).is_invertible()); + assert!(!GeoTransform([0.0, 1.0, 2.0, 0.0, 2.0, 4.0]).is_invertible()); + assert!(!GeoTransform([f64::NAN, 1.0, 0.0, 0.0, 0.0, -1.0]).is_invertible()); + assert!(!GeoTransform([0.0; 6]).is_invertible()); + } +} diff --git a/nodedb-crs/src/lib.rs b/nodedb-crs/src/lib.rs new file mode 100644 index 000000000..4ff2d5bc6 --- /dev/null +++ b/nodedb-crs/src/lib.rs @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Coordinate reference systems for NodeDB. +//! +//! An [`Srid`] names a CRS. A [`CrsContext`] resolves it to a [`CrsDef`]: +//! user-defined CRSs first, then the built-in EPSG table. A [`Transformer`] +//! maps coordinates between two definitions, at a [`CoordinateEpoch`] when a +//! datum shift is time-dependent. A definition reads from and +//! writes to PROJ strings, WKT2 and PROJJSON. +//! +//! The crate does no file or network I/O, so it builds for +//! `wasm32-unknown-unknown`. + +pub mod datum; +pub mod def; +pub mod error; +pub mod geo_transform; +pub mod method; +pub mod parse; +pub mod registry; +pub mod transform; + +pub use nodedb_srid::srid; + +pub use def::{ + AreaOfUse, AxisDirection, AxisOrder, CoordinateEpoch, CrsDef, CrsKind, Datum, DatumShift, + Ellipsoid, Helmert, HelmertRates, LinearUnit, MAX_COORDINATE_EPOCH, MIN_COORDINATE_EPOCH, + RotationConvention, +}; +pub use error::{ + CrsError, CrsParseError, CustomCrsError, DomainReason, InvalidEpoch, ProjExportError, + UnknownSrid, +}; +pub use geo_transform::GeoTransform; +pub use method::{ + AeaParams, AeqdParams, CassParams, CeaParams, EqEarthParams, EqcParams, GeosParams, HomParams, + HomVariant, KrovakParams, LabrdParams, LaeaParams, LccParams, MercParams, NzmgParams, + PolyParams, ProjectionDef, Projector, SinuParams, SomercParams, StereParams, StereaParams, + SweepAxis, TmParams, WebMercParams, +}; +pub use nodedb_srid::{ + InvalidSridCode, NODEDB_AUTHORITY, NotUserSrid, Srid, SridAuthority, SridResolver, + code_from_name, +}; +pub use parse::{parse_proj_string, parse_projjson, parse_wkt2}; +pub use registry::{ + BuiltinEntry, CrsContext, CustomCrs, CustomCrsSet, CustomCrsState, DuplicateCustomSrid, + builtin_codes, builtin_entries, builtin_lookup, geographic_crs_on_datum, +}; +pub use transform::Transformer; diff --git a/nodedb-crs/src/method/aea.rs b/nodedb-crs/src/method/aea.rs new file mode 100644 index 000000000..4040f00b2 --- /dev/null +++ b/nodedb-crs/src/method/aea.rs @@ -0,0 +1,358 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Albers Equal Area, EPSG method 9822, PROJ `aea`. +//! +//! Follows PROJ 9.8.1 `src/projections/aea.cpp` (Snyder 1987 pp. 98–103, +//! eqs. 14-1 to 14-21; IOGP Guidance Note 7-2 §3.4.1), ellipsoidal and +//! spherical. + +use core::f64::consts::FRAC_PI_2; +use core::fmt::Write; + +use serde::{Deserialize, Serialize}; + +use super::angle::wrap_pi; +use super::authalic::Authalic; +use crate::def::Ellipsoid; +use crate::error::{CrsParseError, DomainReason}; +use crate::parse::tokens::ProjParams; + +/// PROJ `EPS10`: |φ1 + φ2| below this, radians, has no cone. +const EPS10: f64 = 1e-10; + +/// PROJ `TOL7`: |qp − |q|| below this puts the inverse on a pole. +const TOL7: f64 = 1e-7; + +/// Largest absolute latitude, degrees. +const MAX_LAT_DEG: f64 = 90.0; + +/// Defining parameters of one Albers Equal Area projection. +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Serialize, + Deserialize, + zerompk::ToMessagePack, + zerompk::FromMessagePack, +)] +pub struct AeaParams { + /// Latitude of false origin φF, degrees (`+lat_0`). + pub lat0_deg: f64, + /// Longitude of false origin λF, degrees (`+lon_0`). + pub lon0_deg: f64, + /// Latitude of first standard parallel φ1, degrees (`+lat_1`). + pub lat1_deg: f64, + /// Latitude of second standard parallel φ2, degrees (`+lat_2`). + pub lat2_deg: f64, + /// Easting at false origin, metres (`+x_0`). + pub false_easting: f64, + /// Northing at false origin, metres (`+y_0`). + pub false_northing: f64, +} + +impl AeaParams { + /// Read from PROJ-string parameters. `+lat_1` is required; `+lat_2`, + /// `+lat_0`, `+lon_0`, `+x_0`, `+y_0` default to 0 as in PROJ. + pub fn from_proj(p: &ProjParams<'_>) -> Result { + let lat1_deg = p.number("lat_1")?.ok_or(CrsParseError::Missing { + key: "lat_1", + method: "aea", + })?; + let lat2_deg = p.number_or("lat_2", 0.0)?; + if lat1_deg.abs() > MAX_LAT_DEG { + return Err(CrsParseError::invalid("lat_1", &lat1_deg.to_string())); + } + if lat2_deg.abs() > MAX_LAT_DEG { + return Err(CrsParseError::invalid("lat_2", &lat2_deg.to_string())); + } + // PROJ: |lat_1 + lat_2| must exceed EPS10, else the cone is a plane. + if (lat1_deg + lat2_deg).to_radians().abs() < EPS10 { + return Err(CrsParseError::invalid("lat_2", &lat2_deg.to_string())); + } + Ok(Self { + lat0_deg: p.number_or("lat_0", 0.0)?, + lon0_deg: p.number_or("lon_0", 0.0)?, + lat1_deg, + lat2_deg, + false_easting: p.number_or("x_0", 0.0)?, + false_northing: p.number_or("y_0", 0.0)?, + }) + } + + /// Append the defining parameters as PROJ-string tokens. + pub fn write_proj(&self, out: &mut String) { + let _ = write!( + out, + " +proj=aea +lat_0={} +lon_0={} +lat_1={} +lat_2={} +x_0={} +y_0={}", + self.lat0_deg, + self.lon0_deg, + self.lat1_deg, + self.lat2_deg, + self.false_easting, + self.false_northing + ); + } +} + +/// Albers Equal Area with its cone constants computed. +#[derive(Debug, Clone, PartialEq)] +pub struct Albers { + params: AeaParams, + a: f64, + lon0: f64, + /// Authalic conversion; `None` on a sphere. + auth: Option, + /// Cone constant n. + n: f64, + /// 2n, used on the sphere. + n2: f64, + /// C, Snyder eq. 14-3 (ellipsoid) or 14-13 (sphere). + c: f64, + /// 1 / n. + dd: f64, + /// ρ0 / a. + rho0: f64, +} + +impl Albers { + pub fn new(p: &AeaParams, ell: &Ellipsoid) -> Self { + let phi0 = p.lat0_deg.to_radians(); + let phi1 = p.lat1_deg.to_radians(); + let phi2 = p.lat2_deg.to_radians(); + let secant = (phi1 - phi2).abs() >= EPS10; + let (sinphi1, cosphi1) = phi1.sin_cos(); + let mut n = sinphi1; + let (auth, n2, c, rho0); + if ell.is_sphere() { + if secant { + n = 0.5 * (n + phi2.sin()); + } + n2 = n + n; + c = cosphi1 * cosphi1 + n2 * sinphi1; + rho0 = (c - n2 * phi0.sin()).sqrt() / n; + auth = None; + } else { + let es = ell.e2(); + let a = Authalic::new(es); + let m1 = msfn(sinphi1, cosphi1, es); + let ml1 = a.q(sinphi1); + if secant { + let (sinphi2, cosphi2) = phi2.sin_cos(); + let m2 = msfn(sinphi2, cosphi2, es); + let ml2 = a.q(sinphi2); + n = (m1 * m1 - m2 * m2) / (ml2 - ml1); + } + n2 = n + n; + c = m1 * m1 + n * ml1; + rho0 = (c - n * a.q(phi0.sin())).sqrt() / n; + auth = Some(a); + } + Self { + params: *p, + a: ell.a, + lon0: p.lon0_deg.to_radians(), + auth, + n, + n2, + c, + dd: 1.0 / n, + rho0, + } + } + + pub fn forward(&self, lam: f64, phi: f64) -> Result<(f64, f64), DomainReason> { + let lam = wrap_pi(lam - self.lon0); + let sinphi = phi.sin(); + let rho2 = self.c + - match &self.auth { + Some(a) => self.n * a.q(sinphi), + None => self.n2 * sinphi, + }; + if rho2 < 0.0 { + return Err(DomainReason::OutsideProjection); + } + let rho = self.dd * rho2.sqrt(); + let (s, c) = (lam * self.n).sin_cos(); + Ok(( + self.params.false_easting + self.a * rho * s, + self.params.false_northing + self.a * (self.rho0 - rho * c), + )) + } + + pub fn inverse(&self, x: f64, y: f64) -> Result<(f64, f64), DomainReason> { + let mut x = (x - self.params.false_easting) / self.a; + let mut y = self.rho0 - (y - self.params.false_northing) / self.a; + let mut rho = x.hypot(y); + if rho == 0.0 { + let phi = if self.n > 0.0 { FRAC_PI_2 } else { -FRAC_PI_2 }; + return Ok((self.lon0, phi)); + } + if self.n < 0.0 { + rho = -rho; + x = -x; + y = -y; + } + let r = rho / self.dd; + let phi = match &self.auth { + Some(a) => { + let qs = (self.c - r * r) / self.n; + let qp = a.qp(); + if (qp - qs.abs()).abs() > TOL7 { + // PROJ fails |qs| > 2; asin also needs |qs| ≤ qp. + if qs.abs() > qp { + return Err(DomainReason::OutsideProjection); + } + a.from_authalic((qs / qp).asin()) + } else if qs < 0.0 { + -FRAC_PI_2 + } else { + FRAC_PI_2 + } + } + None => { + let qs_div_2 = (self.c - r * r) / self.n2; + if qs_div_2.abs() <= 1.0 { + qs_div_2.asin() + } else if qs_div_2 < 0.0 { + -FRAC_PI_2 + } else { + FRAC_PI_2 + } + } + }; + let lam = x.atan2(y) / self.n; + Ok((wrap_pi(lam + self.lon0), phi)) + } +} + +/// m = cos φ / √(1 − e² sin² φ), Snyder eq. 14-15, PROJ `pj_msfn`. +fn msfn(sinphi: f64, cosphi: f64, es: f64) -> f64 { + cosphi / (1.0 - es * sinphi * sinphi).sqrt() +} + +#[cfg(test)] +mod tests { + use super::*; + + const MM: f64 = 1e-3; + const DEG_TOL: f64 = 1e-9; + + fn check(p: &AeaParams, ell: &Ellipsoid, pts: &[[f64; 4]]) { + let aea = Albers::new(p, ell); + for [lon, lat, x, y] in pts.iter().copied() { + let (gx, gy) = aea.forward(lon.to_radians(), lat.to_radians()).unwrap(); + assert!( + (gx - x).abs() < MM && (gy - y).abs() < MM, + "{lon} {lat}: {gx} {gy}" + ); + let (glam, gphi) = aea.inverse(x, y).unwrap(); + assert!( + (glam.to_degrees() - lon).abs() < DEG_TOL + && (gphi.to_degrees() - lat).abs() < DEG_TOL, + "{x} {y}: {} {}", + glam.to_degrees(), + gphi.to_degrees() + ); + } + } + + /// The GN 7-2 §3.4.1 parameter set (Clarke 1866, φF = 0°, λF = 132°E, + /// φ1 = 18°S, φ2 = 36°S). PROJ 9.8.1 values via pyproj 3.8.0 for + /// `+proj=aea +lat_0=0 +lon_0=132 +lat_1=-18 +lat_2=-36 +ellps=clrk66`. + #[test] + fn gn7_2_parameters() { + let p = AeaParams { + lat0_deg: 0.0, + lon0_deg: 132.0, + lat1_deg: -18.0, + lat2_deg: -36.0, + false_easting: 0.0, + false_northing: 0.0, + }; + check( + &p, + &Ellipsoid::CLARKE_1866, + &[[ + 140.0, + -20.0, + 832_812.274_151_099_5, + -2_170_059.232_002_873_4, + ]], + ); + } + + /// PROJ 9.8.1 via pyproj 3.8.0, EPSG:3577 GDA94 / Australian Albers: + /// `+proj=aea +lat_0=0 +lon_0=132 +lat_1=-18 +lat_2=-36 +x_0=0 +y_0=0 + /// +ellps=GRS80`. + #[test] + fn australian_albers() { + let p = AeaParams { + lat0_deg: 0.0, + lon0_deg: 132.0, + lat1_deg: -18.0, + lat2_deg: -36.0, + false_easting: 0.0, + false_northing: 0.0, + }; + check(&p, &Ellipsoid::GRS80, AU_ALBERS); + } + + /// (lon, lat, x, y). + const AU_ALBERS: &[[f64; 4]] = &[ + [147.0, -35.0, 1362365.5637248761, -3902190.4297908046], + [115.86, -31.95, -1508261.2956576233, -3577137.7414186457], + [130.84, -12.46, -128368.21938505347, -1314806.1986117077], + [153.0, -27.5, 2040429.2689670695, -3150988.844969211], + ]; + + /// PROJ 9.8.1 via pyproj 3.8.0: `+proj=aea +lat_0=40 +lon_0=-96 + /// +lat_1=20 +lat_2=60 +R=6370997`. + #[test] + fn sphere() { + let p = AeaParams { + lat0_deg: 40.0, + lon0_deg: -96.0, + lat1_deg: 20.0, + lat2_deg: 60.0, + false_easting: 0.0, + false_northing: 0.0, + }; + check(&p, &Ellipsoid::sphere(6_370_997.0), SPHERE_PTS); + } + + /// (lon, lat, x, y). + const SPHERE_PTS: &[[f64; 4]] = &[ + [-120.0, 35.0, -2050766.2665282101, -328037.05230039987], + [-75.0, 45.0, 1539757.8618297249, 762574.6118662546], + [-96.0, 40.0, 0.0, 0.0], + [-100.0, 60.0, -222323.84540856257, 2334542.866764218], + ]; + + #[test] + fn parses_and_writes() { + let p = ProjParams::tokenize( + "+proj=aea +lat_1=29.5 +lat_2=45.5 +lat_0=23 +lon_0=-96 +x_0=0 +y_0=0", + ) + .unwrap(); + let a = AeaParams::from_proj(&p).unwrap(); + assert_eq!(a.lat1_deg, 29.5); + let mut s = String::new(); + a.write_proj(&mut s); + assert_eq!( + s, + " +proj=aea +lat_0=23 +lon_0=-96 +lat_1=29.5 +lat_2=45.5 +x_0=0 +y_0=0" + ); + let bad = ProjParams::tokenize("+proj=aea +lat_1=10 +lat_2=-10").unwrap(); + assert!(AeaParams::from_proj(&bad).is_err()); + let missing = ProjParams::tokenize("+proj=aea +lat_2=10").unwrap(); + assert_eq!( + AeaParams::from_proj(&missing), + Err(CrsParseError::Missing { + key: "lat_1", + method: "aea" + }) + ); + } +} diff --git a/nodedb-crs/src/method/aeqd.rs b/nodedb-crs/src/method/aeqd.rs new file mode 100644 index 000000000..e0d1564cc --- /dev/null +++ b/nodedb-crs/src/method/aeqd.rs @@ -0,0 +1,576 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Azimuthal Equidistant, EPSG method 1125 (and 9831 Guam), PROJ `aeqd`. +//! +//! Follows PROJ 9.8.1 `src/projections/aeqd.cpp`. The ellipsoidal oblique +//! and equatorial aspects solve the geodesic problem exactly (Karney 2013, +//! [`Geodesic`]); the polar aspects use the meridian arc (Snyder 1987 +//! eq. 25-14); `+guam` selects the Guam series (Snyder eqs. 25-15 to +//! 25-17, IOGP Guidance Note 7-2 §3.9.2); spheres use Snyder eqs. 25-2 to +//! 25-5. + +use core::f64::consts::{FRAC_PI_2, PI}; +use core::fmt::Write; + +use serde::{Deserialize, Serialize}; + +use super::angle::wrap_pi; +use super::geodesic::Geodesic; +use super::meridian::MeridianArc; +use crate::def::Ellipsoid; +use crate::error::{CrsParseError, DomainReason}; +use crate::parse::tokens::ProjParams; + +/// PROJ `EPS10`, radians. +const EPS10: f64 = 1e-10; + +/// PROJ `TOL`: |cos c| this close to 1 counts as the centre or antipode. +const TOL: f64 = 1e-14; + +/// PROJ `aasin` tolerance: |v| up to this rounds to ±1. +const ONE_TOL: f64 = 1.000_000_000_000_01; + +/// Fixed-point iterations of the Guam inverse, PROJ `e_guam_inv`. +const GUAM_ITERS: usize = 3; + +/// Radians per degree, PROJ `DEG_TO_RAD`. +const DEG_TO_RAD: f64 = PI / 180.0; + +/// Largest absolute latitude of origin, degrees. +const MAX_LAT_DEG: f64 = 90.0; + +/// Defining parameters of one Azimuthal Equidistant projection. +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Serialize, + Deserialize, + zerompk::ToMessagePack, + zerompk::FromMessagePack, +)] +pub struct AeqdParams { + /// Latitude of natural origin φ0, degrees (`+lat_0`). + pub lat0_deg: f64, + /// Longitude of natural origin λ0, degrees (`+lon_0`). + pub lon0_deg: f64, + /// False easting, metres (`+x_0`). + pub false_easting: f64, + /// False northing, metres (`+y_0`). + pub false_northing: f64, + /// PROJ `+guam`: the Guam series, EPSG 9831. Ellipsoid only. + pub guam: bool, +} + +impl AeqdParams { + /// Read from PROJ-string parameters. Every number defaults to 0 as in + /// PROJ. + pub fn from_proj(p: &ProjParams<'_>) -> Result { + let lat0_deg = p.number_or("lat_0", 0.0)?; + if lat0_deg.abs() > MAX_LAT_DEG { + return Err(CrsParseError::invalid("lat_0", &lat0_deg.to_string())); + } + Ok(Self { + lat0_deg, + lon0_deg: p.number_or("lon_0", 0.0)?, + false_easting: p.number_or("x_0", 0.0)?, + false_northing: p.number_or("y_0", 0.0)?, + guam: p.has("guam"), + }) + } + + /// Append the defining parameters as PROJ-string tokens. + pub fn write_proj(&self, out: &mut String) { + out.push_str(" +proj=aeqd"); + if self.guam { + out.push_str(" +guam"); + } + let _ = write!( + out, + " +lat_0={} +lon_0={} +x_0={} +y_0={}", + self.lat0_deg, self.lon0_deg, self.false_easting, self.false_northing + ); + } +} + +/// Aspect of the projection, from φ0. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Mode { + NorthPole, + SouthPole, + Equatorial, + Oblique, +} + +/// Which formulas apply. +#[derive(Debug, Clone, PartialEq)] +enum Kind { + Sphere, + /// Geodesic (oblique, equatorial) or meridian arc (polar). + Ellipsoid(MeridianArc), + /// The Guam series, with the arc to φ0 in units of a. + Guam { + arc: MeridianArc, + m1: f64, + }, +} + +/// Azimuthal Equidistant with its constants computed. +#[derive(Debug, Clone, PartialEq)] +pub struct AzimuthalEquidistant { + params: AeqdParams, + a: f64, + e: f64, + lon0: f64, + phi0: f64, + mode: Mode, + sinph0: f64, + cosph0: f64, + kind: Kind, + /// Geodesic on the ellipsoid scaled to a = 1, PROJ `geod_init(1, f)`. + geod: Geodesic, + /// Polar: meridian arc to the pole, units of a. + mp: f64, +} + +impl AzimuthalEquidistant { + pub fn new(p: &AeqdParams, ell: &Ellipsoid) -> Self { + let phi0 = p.lat0_deg.to_radians(); + let (mode, sinph0, cosph0) = if (phi0.abs() - FRAC_PI_2).abs() < EPS10 { + if phi0 < 0.0 { + (Mode::SouthPole, -1.0, 0.0) + } else { + (Mode::NorthPole, 1.0, 0.0) + } + } else if phi0.abs() < EPS10 { + (Mode::Equatorial, 0.0, 1.0) + } else { + let (s, c) = phi0.sin_cos(); + (Mode::Oblique, s, c) + }; + let es = ell.e2(); + let mut mp = 0.0; + let kind = if ell.is_sphere() { + Kind::Sphere + } else { + let arc = MeridianArc::new(es); + if p.guam { + let m1 = arc.arc(phi0, sinph0, cosph0); + Kind::Guam { arc, m1 } + } else { + match mode { + Mode::NorthPole => mp = arc.arc(FRAC_PI_2, 1.0, 0.0), + Mode::SouthPole => mp = arc.arc(-FRAC_PI_2, -1.0, 0.0), + Mode::Equatorial | Mode::Oblique => {} + } + Kind::Ellipsoid(arc) + } + }; + Self { + params: *p, + a: ell.a, + e: ell.e(), + lon0: p.lon0_deg.to_radians(), + phi0, + mode, + sinph0, + cosph0, + kind, + geod: Geodesic::new(1.0, ell.f()), + mp, + } + } + + pub fn forward(&self, lam: f64, phi: f64) -> Result<(f64, f64), DomainReason> { + let lam = wrap_pi(lam - self.lon0); + let (x, y) = match &self.kind { + Kind::Sphere => self.s_forward(lam, phi)?, + Kind::Ellipsoid(arc) => self.e_forward(arc, lam, phi), + Kind::Guam { arc, m1 } => { + let (sinphi, cosphi) = phi.sin_cos(); + let t = 1.0 / (1.0 - self.e * self.e * sinphi * sinphi).sqrt(); + ( + lam * cosphi * t, + arc.arc(phi, sinphi, cosphi) - m1 + 0.5 * lam * lam * cosphi * sinphi * t, + ) + } + }; + Ok(( + self.params.false_easting + self.a * x, + self.params.false_northing + self.a * y, + )) + } + + pub fn inverse(&self, x: f64, y: f64) -> Result<(f64, f64), DomainReason> { + let x = (x - self.params.false_easting) / self.a; + let y = (y - self.params.false_northing) / self.a; + let (lam, phi) = match &self.kind { + Kind::Sphere => self.s_inverse(x, y)?, + Kind::Ellipsoid(arc) => self.e_inverse(arc, x, y)?, + Kind::Guam { arc, m1 } => { + let x2 = 0.5 * x * x; + let mut phi = self.phi0; + let mut t = 0.0; + for _ in 0..GUAM_ITERS { + let es = self.e * phi.sin(); + t = (1.0 - es * es).sqrt(); + phi = arc.inverse(m1 + y - x2 * phi.tan() * t)?; + } + (x * t / phi.cos(), phi) + } + }; + if phi.abs() > FRAC_PI_2 { + return Err(DomainReason::OutsideProjection); + } + Ok((wrap_pi(lam + self.lon0), phi)) + } + + /// PROJ `aeqd_e_forward`. + fn e_forward(&self, arc: &MeridianArc, lam: f64, phi: f64) -> (f64, f64) { + match self.mode { + Mode::NorthPole | Mode::SouthPole => { + let (sinlam, coslam) = lam.sin_cos(); + let coslam = if self.mode == Mode::NorthPole { + -coslam + } else { + coslam + }; + let rho = (self.mp - arc.arc(phi, phi.sin(), phi.cos())).abs(); + (rho * sinlam, rho * coslam) + } + Mode::Equatorial | Mode::Oblique => self.geodesic_forward(lam, phi), + } + } + + /// Distance and azimuth from the centre by the inverse geodesic + /// problem, PROJ `aeqd_e_forward`. + fn geodesic_forward(&self, lam: f64, phi: f64) -> (f64, f64) { + if lam.abs() < EPS10 && (phi - self.phi0).abs() < EPS10 { + return (0.0, 0.0); + } + let (s12, azi1, _) = self.geod.inverse( + self.phi0 / DEG_TO_RAD, + 0.0, + phi / DEG_TO_RAD, + lam / DEG_TO_RAD, + ); + let (s, c) = (azi1 * DEG_TO_RAD).sin_cos(); + (s12 * s, s12 * c) + } + + /// PROJ `aeqd_s_forward` (Snyder eqs. 25-2 to 25-5). + fn s_forward(&self, lam: f64, phi: f64) -> Result<(f64, f64), DomainReason> { + let (sinphi, cosphi) = phi.sin_cos(); + let (sinlam, coslam) = lam.sin_cos(); + match self.mode { + Mode::Equatorial | Mode::Oblique => { + let cosphi_coslam = cosphi * coslam; + let cos_c = if self.mode == Mode::Equatorial { + cosphi_coslam + } else { + self.sinph0 * sinphi + self.cosph0 * cosphi_coslam + }; + if (cos_c.abs() - 1.0).abs() < TOL { + if cos_c < 0.0 { + return Err(DomainReason::Antipode); + } + return Ok(self.geodesic_forward(lam, phi)); + } + let c = cos_c.acos(); + let k = c / c.sin(); + let y = if self.mode == Mode::Equatorial { + sinphi + } else { + self.cosph0 * sinphi - self.sinph0 * cosphi_coslam + }; + Ok((k * cosphi * sinlam, k * y)) + } + Mode::NorthPole | Mode::SouthPole => { + let (phi, coslam) = if self.mode == Mode::NorthPole { + (-phi, -coslam) + } else { + (phi, coslam) + }; + if (phi - FRAC_PI_2).abs() < EPS10 { + return Err(DomainReason::Antipode); + } + let r = FRAC_PI_2 + phi; + Ok((r * sinlam, r * coslam)) + } + } + } + + /// PROJ `aeqd_e_inverse`. + fn e_inverse(&self, arc: &MeridianArc, x: f64, y: f64) -> Result<(f64, f64), DomainReason> { + let s12 = x.hypot(y); + if s12 < EPS10 { + return Ok((0.0, self.phi0)); + } + match self.mode { + Mode::Equatorial | Mode::Oblique => { + let azi1 = x.atan2(y) / DEG_TO_RAD; + let (lat2, lon2, _) = self.geod.direct(self.phi0 / DEG_TO_RAD, 0.0, azi1, s12); + Ok((lon2 * DEG_TO_RAD, lat2 * DEG_TO_RAD)) + } + Mode::NorthPole => Ok(((x).atan2(-y), arc.inverse(self.mp - s12)?)), + Mode::SouthPole => Ok((x.atan2(y), arc.inverse(self.mp + s12)?)), + } + } + + /// PROJ `aeqd_s_inverse`. + fn s_inverse(&self, x: f64, y: f64) -> Result<(f64, f64), DomainReason> { + let (mut x, mut y) = (x, y); + let mut c_rh = x.hypot(y); + if c_rh > PI { + if c_rh - EPS10 > PI { + return Err(DomainReason::OutsideProjection); + } + c_rh = PI; + } else if c_rh < EPS10 { + return Ok((0.0, self.phi0)); + } + match self.mode { + Mode::Equatorial | Mode::Oblique => { + let (sinc, cosc) = c_rh.sin_cos(); + let phi; + if self.mode == Mode::Equatorial { + phi = aasin(y * sinc / c_rh)?; + x *= sinc; + y = cosc * c_rh; + } else { + phi = aasin(cosc * self.sinph0 + y * sinc * self.cosph0 / c_rh)?; + y = (cosc - self.sinph0 * phi.sin()) * c_rh; + x *= sinc * self.cosph0; + } + let lam = if y == 0.0 { 0.0 } else { x.atan2(y) }; + Ok((lam, phi)) + } + Mode::NorthPole => Ok((x.atan2(-y), FRAC_PI_2 - c_rh)), + Mode::SouthPole => Ok((x.atan2(y), c_rh - FRAC_PI_2)), + } + } +} + +/// asin that rounds |v| ≤ [`ONE_TOL`] to ±π/2, PROJ `aasin`. +fn aasin(v: f64) -> Result { + if v.abs() >= 1.0 { + if v.abs() > ONE_TOL { + return Err(DomainReason::OutsideProjection); + } + return Ok(FRAC_PI_2.copysign(v)); + } + Ok(v.asin()) +} + +#[cfg(test)] +mod tests { + use super::*; + + const MM: f64 = 1e-3; + const DEG_TOL: f64 = 1e-9; + + fn params(lat0_deg: f64, lon0_deg: f64, false_easting: f64, false_northing: f64) -> AeqdParams { + AeqdParams { + lat0_deg, + lon0_deg, + false_easting, + false_northing, + guam: false, + } + } + + fn check(p: &AeqdParams, ell: &Ellipsoid, pts: &[[f64; 4]]) { + let aeqd = AzimuthalEquidistant::new(p, ell); + for [lon, lat, x, y] in pts.iter().copied() { + let (gx, gy) = aeqd.forward(lon.to_radians(), lat.to_radians()).unwrap(); + assert!( + (gx - x).abs() < MM && (gy - y).abs() < MM, + "{lon} {lat}: {gx} {gy}" + ); + let (glam, gphi) = aeqd.inverse(x, y).unwrap(); + assert!( + (glam.to_degrees() - lon).abs() < DEG_TOL + && (gphi.to_degrees() - lat).abs() < DEG_TOL, + "{x} {y}: {} {}", + glam.to_degrees(), + gphi.to_degrees() + ); + } + } + + fn dms(d: f64, m: f64, s: f64) -> f64 { + d + m / 60.0 + s / 3600.0 + } + + /// GN 7-2 §3.9.2 example, Guam 1963 / Yap Islands (EPSG:3295), Clarke + /// 1866, φ0 = 9°32′48.15″N, λ0 = 138°10′07.48″E, FE = 40 000 m, + /// FN = 60 000 m. φ = 9°35′47.493″N, λ = 138°11′34.908″E → + /// E = 42 665.90 m, N = 65 509.82 m (Modified Azimuthal Equidistant; + /// PROJ's exact geodesic agrees to 1 mm). + #[test] + fn gn7_2_yap_islands() { + let p = params( + dms(9.0, 32.0, 48.15), + dms(138.0, 10.0, 7.48), + 40_000.0, + 60_000.0, + ); + let aeqd = AzimuthalEquidistant::new(&p, &Ellipsoid::CLARKE_1866); + let lon = dms(138.0, 11.0, 34.908); + let lat = dms(9.0, 35.0, 47.493); + let (x, y) = aeqd.forward(lon.to_radians(), lat.to_radians()).unwrap(); + assert!( + (x - 42_665.90).abs() < 0.005 && (y - 65_509.82).abs() < 0.005, + "{x} {y}" + ); + let (lam, phi) = aeqd.inverse(42_665.90, 65_509.82).unwrap(); + assert!((lam.to_degrees() - lon).abs() < 1e-7 && (phi.to_degrees() - lat).abs() < 1e-7); + // PROJ 9.8.1 via pyproj 3.8.0, EPSG:3295 PROJ string. + check( + &p, + &Ellipsoid::CLARKE_1866, + &[[lon, lat, 42_665.904_012_062_79, 65_509.822_493_638_305]], + ); + } + + /// GN 7-2 §3.9.2 with PROJ's `+guam` series: PROJ 9.8.1 via pyproj + /// 3.8.0 gives E = 42 665.903 680 579 635, N = 65 509.822 816 551 77. + #[test] + fn guam_series() { + let mut p = params( + dms(9.0, 32.0, 48.15), + dms(138.0, 10.0, 7.48), + 40_000.0, + 60_000.0, + ); + p.guam = true; + let lon = dms(138.0, 11.0, 34.908); + let lat = dms(9.0, 35.0, 47.493); + check( + &p, + &Ellipsoid::CLARKE_1866, + &[[lon, lat, 42_665.903_680_579_635, 65_509.822_816_551_77]], + ); + } + + /// PROJ 9.8.1 via pyproj 3.8.0, EPSG:27704 WGS 84 / Equi7 Europe: + /// `+proj=aeqd +lat_0=53 +lon_0=24 +x_0=5837287.82 +y_0=2121415.696 + /// +datum=WGS84`. + #[test] + fn equi7_europe() { + check( + ¶ms(53.0, 24.0, 5_837_287.82, 2_121_415.696), + &Ellipsoid::WGS84, + EQUI7_EUROPE, + ); + } + + const EQUI7_EUROPE: &[[f64; 4]] = &[ + [24.0, 53.0, 5837287.82, 2121415.696], + [0.0, 40.0, 3802691.4752739277, 1001915.8778322181], + [-20.0, 65.0, 3899068.361612137, 4116725.3950709505], + [40.0, 35.0, 7312517.666956626, 273807.5616523754], + ]; + + /// PROJ 9.8.1 via pyproj 3.8.0, EPSG:27702 WGS 84 / Equi7 Antarctica: + /// `+proj=aeqd +lat_0=-90 +lon_0=0 +x_0=3714266.977 +y_0=3402016.506 + /// +datum=WGS84`. + #[test] + fn equi7_antarctica() { + check( + ¶ms(-90.0, 0.0, 3_714_266.977, 3_402_016.506), + &Ellipsoid::WGS84, + EQUI7_ANTARCTICA, + ); + } + + const EQUI7_ANTARCTICA: &[[f64; 4]] = &[ + [0.0, -90.0, 3714266.977, 3402016.506], + [60.0, -70.0, 5648088.7146054655, 4518509.0067712655], + [-150.0, -80.0, 3155854.0483120745, 2434816.9419091768], + [170.0, -60.0, 4295622.4798146635, 104985.61215248471], + ]; + + /// PROJ 9.8.1 via pyproj 3.8.0, `+proj=aeqd +lat_0=90 +lon_0=0 +ellps=WGS84`. + #[test] + fn north_polar_ellipsoid() { + check(¶ms(90.0, 0.0, 0.0, 0.0), &Ellipsoid::WGS84, NORTH_POLAR); + } + + const NORTH_POLAR: &[[f64; 4]] = &[ + [45.0, 60.0, 2367317.7792216484, -2367317.779221649], + [-120.0, 75.0, -1450616.9230538863, 837514.0710161875], + [170.0, 10.0, 1544793.4454269276, 8760958.982114341], + ]; + + /// PROJ 9.8.1 via pyproj 3.8.0, `+proj=aeqd +lat_0=0 +lon_0=0 +ellps=WGS84`. + #[test] + fn equatorial_ellipsoid() { + check(¶ms(0.0, 0.0, 0.0, 0.0), &Ellipsoid::WGS84, EQUATORIAL); + } + + const EQUATORIAL: &[[f64; 4]] = &[ + [10.0, 10.0, 1101896.9072405538, 1111480.8582554795], + [-60.0, 30.0, -5962674.986276318, 3954631.424151443], + [120.0, -40.0, 8995111.25339604, -8710143.057967292], + [0.0, 0.0, 0.0, 0.0], + ]; + + /// PROJ 9.8.1 via pyproj 3.8.0, `+proj=aeqd +lat_0=40 +lon_0=-100 +R=6370997`. + #[test] + fn sphere_oblique() { + check( + ¶ms(40.0, -100.0, 0.0, 0.0), + &Ellipsoid::sphere(6_370_997.0), + SPHERE_OBLIQUE, + ); + } + + const SPHERE_OBLIQUE: &[[f64; 4]] = &[ + [-120.0, 35.0, -1810140.3394241622, -357945.7072990199], + [-75.0, 45.0, 1939281.2513566797, 841944.9606152419], + [10.0, 10.0, 10237736.198016878, 3866778.9215264153], + ]; + + /// PROJ 9.8.1 via pyproj 3.8.0, `+proj=aeqd +lat_0=-90 +lon_0=0 +R=6370997`. + #[test] + fn sphere_south_polar() { + check( + ¶ms(-90.0, 0.0, 0.0, 0.0), + &Ellipsoid::sphere(6_370_997.0), + SPHERE_SOUTH, + ); + } + + const SPHERE_SOUTH: &[[f64; 4]] = &[ + [45.0, -60.0, 2358799.4891965115, 2358799.4891965115], + [-120.0, -75.0, -1444463.7885172633, -833961.5571351082], + [170.0, 10.0, 1930878.7285438292, -10950557.429077182], + ]; + + #[test] + fn antipode_is_out_of_domain() { + let aeqd = AzimuthalEquidistant::new(¶ms(30.0, 0.0, 0.0, 0.0), &Ellipsoid::sphere(1.0)); + assert_eq!( + aeqd.forward(PI, -30f64.to_radians()), + Err(DomainReason::Antipode) + ); + assert_eq!(aeqd.inverse(4.0, 0.0), Err(DomainReason::OutsideProjection)); + } + + #[test] + fn parses_and_writes() { + let p = + ProjParams::tokenize("+proj=aeqd +lat_0=53 +lon_0=24 +x_0=5837287.82 +y_0=2121415.696") + .unwrap(); + let q = AeqdParams::from_proj(&p).unwrap(); + assert!(!q.guam); + let mut s = String::new(); + q.write_proj(&mut s); + assert_eq!( + s, + " +proj=aeqd +lat_0=53 +lon_0=24 +x_0=5837287.82 +y_0=2121415.696" + ); + } +} diff --git a/nodedb-crs/src/method/angle.rs b/nodedb-crs/src/method/angle.rs new file mode 100644 index 000000000..bfe6d0051 --- /dev/null +++ b/nodedb-crs/src/method/angle.rs @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Angle helpers shared by the projection methods. + +use core::f64::consts::{PI, TAU}; + +/// Wrap an angle in radians into [−π, π]. +pub fn wrap_pi(angle: f64) -> f64 { + let a = angle % TAU; + if a > PI { + a - TAU + } else if a < -PI { + a + TAU + } else { + a + } +} + +/// Half a turn in degrees. +const HALF_TURN_DEG: f64 = 180.0; + +/// A full turn in degrees. +const FULL_TURN_DEG: f64 = 360.0; + +/// Wrap a longitude in degrees into [−180, 180]. A value already inside +/// comes back bit-identical. +pub fn wrap_lon_deg(lon: f64) -> f64 { + if (-HALF_TURN_DEG..=HALF_TURN_DEG).contains(&lon) { + return lon; + } + let d = lon % FULL_TURN_DEG; + if d > HALF_TURN_DEG { + d - FULL_TURN_DEG + } else if d < -HALF_TURN_DEG { + d + FULL_TURN_DEG + } else { + d + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn wrap_keeps_offset_in_half_turn() { + assert!((wrap_pi(200f64.to_radians()) - (-160f64).to_radians()).abs() < 1e-15); + assert!((wrap_pi((-200f64).to_radians()) - 160f64.to_radians()).abs() < 1e-15); + assert_eq!(wrap_pi(0.5), 0.5); + assert_eq!(wrap_lon_deg(200.0), -160.0); + assert_eq!(wrap_lon_deg(-200.0), 160.0); + assert_eq!(wrap_lon_deg(101.7117), 101.7117); + } +} diff --git a/nodedb-crs/src/method/authalic.rs b/nodedb-crs/src/method/authalic.rs new file mode 100644 index 000000000..b4ab45b10 --- /dev/null +++ b/nodedb-crs/src/method/authalic.rs @@ -0,0 +1,227 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Authalic latitude, as PROJ `pj_authalic_lat_q`, `pj_authalic_lat` and +//! `pj_authalic_lat_inverse` (PROJ 9.8.1 `src/latitudes.cpp`). +//! +//! For |n| < 0.01 both directions use Fourier series in n to order 6 +//! (Karney, "On auxiliary latitudes", Survey Review 56 (2024)). For larger +//! |n| the forward uses β = asin(q / qp) (Snyder 1987 eqs. 3-11, 3-12) and +//! the inverse refines the series start by Newton's method. + +use super::meridian::{AUX_ORDER, clenshaw, full_series, third_flattening}; + +/// Eccentricity below which q(φ) uses the spherical limit 2 sin φ, PROJ +/// `pj_authalic_lat_q`. +const SPHERE_ECCENTRICITY: f64 = 1e-7; + +/// |n| below which the series are accurate to double precision, PROJ +/// `PROJ_AUTHALIC_SERIES_VALID`. +const SERIES_VALID_N: f64 = 0.01; + +/// Newton iterations for the inverse at large |n|, PROJ +/// `pj_authalic_lat_inverse`. +const NEWTON_MAX_ITERS: usize = 10; + +/// Newton step below which the inverse stops. +const NEWTON_TOLERANCE: f64 = 1e-15; + +/// Karney 2024 matrix C[ξ,φ] (φ → ξ), as PROJ `pj_auxlat_coeffs`. +const C_XI_PHI: [f64; 21] = [ + -4.0 / 3.0, + -4.0 / 45.0, + 88.0 / 315.0, + 538.0 / 4725.0, + 20824.0 / 467_775.0, + -44732.0 / 2_837_835.0, + 34.0 / 45.0, + 8.0 / 105.0, + -2482.0 / 14175.0, + -37192.0 / 467_775.0, + -12_467_764.0 / 212_837_625.0, + -1532.0 / 2835.0, + -898.0 / 14175.0, + 54968.0 / 467_775.0, + 100_320_856.0 / 1_915_538_625.0, + 6007.0 / 14175.0, + 24496.0 / 467_775.0, + -5_884_124.0 / 70_945_875.0, + -23356.0 / 66825.0, + -839_792.0 / 19_348_875.0, + 570_284_222.0 / 1_915_538_625.0, +]; + +/// Karney 2024 matrix C[φ,ξ] (ξ → φ). +const C_PHI_XI: [f64; 21] = [ + 4.0 / 3.0, + 4.0 / 45.0, + -16.0 / 35.0, + -2582.0 / 14175.0, + 60136.0 / 467_775.0, + 28_112_932.0 / 212_837_625.0, + 46.0 / 45.0, + 152.0 / 945.0, + -11966.0 / 14175.0, + -21016.0 / 51975.0, + 251_310_128.0 / 638_512_875.0, + 3044.0 / 2835.0, + 3802.0 / 14175.0, + -94388.0 / 66825.0, + -8_797_648.0 / 10_945_935.0, + 6059.0 / 4725.0, + 41072.0 / 93555.0, + -1_472_637_812.0 / 638_512_875.0, + 768_272.0 / 467_775.0, + 455_935_736.0 / 638_512_875.0, + 4_210_684_958.0 / 1_915_538_625.0, +]; + +/// q(φ) (Snyder 3-12, PROJ pj_qsfn). +/// +/// Returns +∞ where 1 − e² sin² φ is zero, as PROJ does. +pub fn qsfn(sinphi: f64, e: f64, one_es: f64) -> f64 { + if e < SPHERE_ECCENTRICITY { + return 2.0 * sinphi; + } + let e_sinphi = e * sinphi; + let one_minus = 1.0 - e_sinphi * e_sinphi; + if one_minus == 0.0 { + return f64::INFINITY; + } + one_es * (sinphi / one_minus + e_sinphi.atanh() / e) +} + +/// Geodetic ↔ authalic latitude conversion for one ellipsoid. +#[derive(Debug, Clone, PartialEq)] +pub struct Authalic { + e: f64, + es: f64, + one_es: f64, + /// Whether |n| is small enough for the series in both directions. + series: bool, + /// q at φ = 90°. + qp: f64, + /// φ → ξ Fourier coefficients. + to_xi: [f64; AUX_ORDER], + /// ξ → φ Fourier coefficients. + from_xi: [f64; AUX_ORDER], +} + +impl Authalic { + pub fn new(e2: f64) -> Self { + let n = third_flattening(e2); + let e = e2.sqrt(); + let one_es = 1.0 - e2; + Self { + e, + es: e2, + one_es, + series: n.abs() < SERIES_VALID_N, + qp: qsfn(1.0, e, one_es), + to_xi: full_series(n, &C_XI_PHI), + from_xi: full_series(n, &C_PHI_XI), + } + } + + /// q at the pole, qp = q(90°). + pub fn qp(&self) -> f64 { + self.qp + } + + /// q(φ) of this ellipsoid from sin φ. + pub fn q(&self, sinphi: f64) -> f64 { + qsfn(sinphi, self.e, self.one_es) + } + + /// Geodetic → authalic latitude, PROJ `pj_authalic_lat`. + pub fn to_authalic(&self, phi: f64) -> f64 { + let (sinphi, cosphi) = phi.sin_cos(); + if self.series { + return phi + clenshaw(sinphi, cosphi, &self.to_xi); + } + // q / qp is ill-conditioned near the poles. Rounding past ±1 clamps. + (self.q(sinphi) / self.qp).clamp(-1.0, 1.0).asin() + } + + /// Authalic → geodetic latitude (PROJ pj_authlat, `pj_authalic_lat_inverse`). + pub fn from_authalic(&self, beta: f64) -> f64 { + let mut phi = beta + clenshaw(beta.sin(), beta.cos(), &self.from_xi); + if self.series { + return phi; + } + // Newton on qp sin β / (1 − e²) − q(φ) / (1 − e²) = 0, PROJ + // `pj_authalic_lat_inverse`. + let q_div_one_es = beta.sin() * self.qp / self.one_es; + for _ in 0..NEWTON_MAX_ITERS { + let (sinphi, cosphi) = phi.sin_cos(); + let w = 1.0 - self.es * sinphi * sinphi; + let dphi = w * w / (2.0 * cosphi) + * (q_div_one_es - sinphi / w - (self.e * sinphi).atanh() / self.e); + if dphi.is_nan() || dphi.abs() < NEWTON_TOLERANCE { + break; + } + phi += dphi; + } + phi + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::def::Ellipsoid; + + #[test] + fn round_trip_on_grs80() { + let a = Authalic::new(Ellipsoid::GRS80.e2()); + for deg in [-90.0, -60.0, -1.0, 0.0, 10.5, 45.0, 89.999, 90.0] { + let phi = f64::to_radians(deg); + let back = a.from_authalic(a.to_authalic(phi)); + assert!((back - phi).abs() < 1e-14, "{deg}"); + } + } + + #[test] + fn series_matches_closed_form() { + let e = Ellipsoid::WGS84; + let a = Authalic::new(e.e2()); + for deg in [5.0, 30.0, 52.0, 75.0] { + let phi = f64::to_radians(deg); + let closed = (a.q(phi.sin()) / a.qp()).asin(); + assert!((a.to_authalic(phi) - closed).abs() < 1e-14, "{deg}"); + } + } + + /// An ellipsoid with |n| ≥ 0.01 takes the closed form and Newton path. + #[test] + fn large_flattening_round_trips() { + let a = Authalic::new( + Ellipsoid { + a: 1.0, + inv_f: 30.0, + } + .e2(), + ); + assert!(!a.series); + for deg in [-80.0, -20.0, 0.0, 33.0, 70.0] { + let phi = f64::to_radians(deg); + let back = a.from_authalic(a.to_authalic(phi)); + assert!((back - phi).abs() < 1e-13, "{deg}"); + } + } + + #[test] + fn sphere_is_identity() { + let a = Authalic::new(0.0); + assert_eq!(a.qp(), 2.0); + assert_eq!(a.to_authalic(0.7), 0.7); + assert_eq!(a.from_authalic(0.7), 0.7); + assert_eq!(qsfn(0.5, 0.0, 1.0), 1.0); + } + + /// qp for GRS 80, Snyder 1987 eq. 3-12 at φ = 90°. + #[test] + fn grs80_qp() { + let a = Authalic::new(Ellipsoid::GRS80.e2()); + assert!((a.qp() - 1.995_531_087_7).abs() < 1e-9, "{}", a.qp()); + } +} diff --git a/nodedb-crs/src/method/cass.rs b/nodedb-crs/src/method/cass.rs new file mode 100644 index 000000000..953f3e135 --- /dev/null +++ b/nodedb-crs/src/method/cass.rs @@ -0,0 +1,592 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Cassini-Soldner, EPSG method 9806 (and 9833 hyperbolic), PROJ `cass`. +//! +//! Follows PROJ 9.8.1 `src/projections/cass.cpp`: the ellipsoidal series +//! of Snyder 1987 pp. 92–95 (eqs. 13-5 to 13-11; IOGP Guidance Note 7-2 +//! §3.2.2), refined on inverse by PROJ's 2-D Newton solver +//! (`src/generic_inverse.cpp`), and the exact spherical formulas. + +use core::f64::consts::{FRAC_PI_2, PI}; +use core::fmt::Write; + +use serde::{Deserialize, Serialize}; + +use super::angle::wrap_pi; +use super::meridian::MeridianArc; +use crate::def::Ellipsoid; +use crate::error::{CrsParseError, DomainReason}; +use crate::parse::tokens::ProjParams; + +/// Series coefficients 1/6, 1/120, 1/24, 1/3, 1/15 (Snyder eqs. 13-7, +/// 13-8, 13-10, 13-11). +const C1: f64 = 1.0 / 6.0; +const C2: f64 = 1.0 / 120.0; +const C3: f64 = 1.0 / 24.0; +const C4: f64 = 1.0 / 3.0; +const C5: f64 = 1.0 / 15.0; + +/// Largest absolute latitude of origin, degrees. +const MAX_LAT_DEG: f64 = 90.0; + +/// Newton iterations of PROJ `pj_generic_inverse_2d`. +const NEWTON_MAX_ITERS: usize = 15; + +/// Residual in units of a at which the Newton inverse stops, PROJ +/// `cass_e_inverse` `deltaXYTolerance`. +const NEWTON_TOLERANCE: f64 = 1e-12; + +/// Residual above which the Jacobian is recomputed, PROJ +/// `pj_generic_inverse_2d`. +const JACOBIAN_REFRESH: f64 = 1e-6; + +/// Finite-difference step for the Jacobian, radians. +const JACOBIAN_STEP: f64 = 1e-6; + +/// Largest Newton correction per step, radians. +const MAX_STEP: f64 = 0.3; + +/// Defining parameters of one Cassini-Soldner projection. +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Serialize, + Deserialize, + zerompk::ToMessagePack, + zerompk::FromMessagePack, +)] +pub struct CassParams { + /// Latitude of natural origin φ0, degrees (`+lat_0`). + pub lat0_deg: f64, + /// Longitude of natural origin λ0, degrees (`+lon_0`). + pub lon0_deg: f64, + /// False easting, metres (`+x_0`). + pub false_easting: f64, + /// False northing, metres (`+y_0`). + pub false_northing: f64, + /// EPSG 9833 Hyperbolic Cassini-Soldner (`+hyperbolic`), the + /// Vanua Levu Grid variant. Ellipsoid only. + pub hyperbolic: bool, +} + +impl CassParams { + /// Read from PROJ-string parameters. Every number defaults to 0 as in + /// PROJ. + pub fn from_proj(p: &ProjParams<'_>) -> Result { + let lat0_deg = p.number_or("lat_0", 0.0)?; + if lat0_deg.abs() > MAX_LAT_DEG { + return Err(CrsParseError::invalid("lat_0", &lat0_deg.to_string())); + } + Ok(Self { + lat0_deg, + lon0_deg: p.number_or("lon_0", 0.0)?, + false_easting: p.number_or("x_0", 0.0)?, + false_northing: p.number_or("y_0", 0.0)?, + hyperbolic: p.has("hyperbolic"), + }) + } + + /// Append the defining parameters as PROJ-string tokens. + pub fn write_proj(&self, out: &mut String) { + let _ = write!( + out, + " +proj=cass +lat_0={} +lon_0={} +x_0={} +y_0={}", + self.lat0_deg, self.lon0_deg, self.false_easting, self.false_northing + ); + if self.hyperbolic { + out.push_str(" +hyperbolic"); + } + } +} + +/// Cassini-Soldner with its constants computed. +#[derive(Debug, Clone, PartialEq)] +pub struct Cassini { + params: CassParams, + a: f64, + es: f64, + lon0: f64, + phi0: f64, + /// Meridian arc; `None` on a sphere. + arc: Option, + /// Arc to the origin latitude, units of a. + m0: f64, +} + +impl Cassini { + pub fn new(p: &CassParams, ell: &Ellipsoid) -> Self { + let phi0 = p.lat0_deg.to_radians(); + let es = ell.e2(); + let (arc, m0) = if ell.is_sphere() { + (None, 0.0) + } else { + let arc = MeridianArc::new(es); + let m0 = arc.arc(phi0, phi0.sin(), phi0.cos()); + (Some(arc), m0) + }; + Self { + params: *p, + a: ell.a, + es, + lon0: p.lon0_deg.to_radians(), + phi0, + arc, + m0, + } + } + + pub fn forward(&self, lam: f64, phi: f64) -> Result<(f64, f64), DomainReason> { + let lam = wrap_pi(lam - self.lon0); + let (x, y) = match &self.arc { + Some(arc) => self.e_forward(arc, lam, phi), + None => ( + (phi.cos() * lam.sin()).asin(), + phi.tan().atan2(lam.cos()) - self.phi0, + ), + }; + Ok(( + self.params.false_easting + self.a * x, + self.params.false_northing + self.a * y, + )) + } + + pub fn inverse(&self, x: f64, y: f64) -> Result<(f64, f64), DomainReason> { + let x = (x - self.params.false_easting) / self.a; + let y = (y - self.params.false_northing) / self.a; + let (lam, phi) = match &self.arc { + Some(arc) => self.e_inverse(arc, x, y)?, + None => { + let dd = y + self.phi0; + ((x.tan()).atan2(dd.cos()), (dd.sin() * x.cos()).asin()) + } + }; + Ok((wrap_pi(lam + self.lon0), phi)) + } + + /// Ellipsoidal series, PROJ `cass_e_forward` (Snyder eqs. 13-5, 13-6). + fn e_forward(&self, arc: &MeridianArc, lam: f64, phi: f64) -> (f64, f64) { + let (sinphi, cosphi) = phi.sin_cos(); + let m = arc.arc(phi, sinphi, cosphi); + let nu_square = 1.0 / (1.0 - self.es * sinphi * sinphi); + let nu = nu_square.sqrt(); + let tanphi = phi.tan(); + let t = tanphi * tanphi; + let a = lam * cosphi; + let c = self.es * (cosphi * cosphi) / (1.0 - self.es); + let a2 = a * a; + let x = nu * a * (1.0 - a2 * t * (C1 + (8.0 - t + 8.0 * c) * a2 * C2)); + let mut y = m - self.m0 + nu * tanphi * a2 * (0.5 + (5.0 - t + 6.0 * c) * a2 * C3); + if self.params.hyperbolic { + // GN 7-2 §3.2.2 hyperbolic correction X − X³ / (6 ρ ν). + let rho = nu_square * (1.0 - self.es) * nu; + y -= y * y * y / (6.0 * rho * nu); + } + (x, y) + } + + /// Ellipsoidal inverse, PROJ `cass_e_inverse`: Snyder eqs. 13-9 to + /// 13-11 as the start of PROJ `pj_generic_inverse_2d`. + fn e_inverse(&self, arc: &MeridianArc, x: f64, y: f64) -> Result<(f64, f64), DomainReason> { + let phi1 = arc.inverse(self.m0 + y)?; + let tanphi1 = phi1.tan(); + let t1 = tanphi1 * tanphi1; + let sinphi1 = phi1.sin(); + let nu1_square = 1.0 / (1.0 - self.es * sinphi1 * sinphi1); + let nu1 = nu1_square.sqrt(); + let rho1 = nu1_square * (1.0 - self.es) * nu1; + let d = x / nu1; + let d2 = d * d; + let phi = phi1 - (nu1 * tanphi1 / rho1) * d2 * (0.5 - (1.0 + 3.0 * t1) * d2 * C3); + let lam = d * (1.0 + t1 * d2 * (-C4 + (1.0 + 3.0 * t1) * d2 * C5)) / phi1.cos(); + self.newton(arc, x, y, lam, phi) + } + + /// 2-D Newton refinement with a finite-difference Jacobian, PROJ + /// `pj_generic_inverse_2d`. + fn newton( + &self, + arc: &MeridianArc, + x: f64, + y: f64, + lam: f64, + phi: f64, + ) -> Result<(f64, f64), DomainReason> { + let (mut lam, mut phi) = (lam, phi); + let (mut d_lam_x, mut d_lam_y, mut d_phi_x, mut d_phi_y) = (0.0, 0.0, 0.0, 0.0); + for i in 0..NEWTON_MAX_ITERS { + let (ax, ay) = self.e_forward(arc, lam, phi); + let dx = ax - x; + let dy = ay - y; + if dx.abs() < NEWTON_TOLERANCE && dy.abs() < NEWTON_TOLERANCE { + return Ok((lam, phi)); + } + if i == 0 || dx.abs() > JACOBIAN_REFRESH || dy.abs() > JACOBIAN_REFRESH { + let h_lam = if lam > 0.0 { + -JACOBIAN_STEP + } else { + JACOBIAN_STEP + }; + let (x2, y2) = self.e_forward(arc, lam + h_lam, phi); + let dx_lam = (x2 - ax) / h_lam; + let dy_lam = (y2 - ay) / h_lam; + let h_phi = if phi > 0.0 { + -JACOBIAN_STEP + } else { + JACOBIAN_STEP + }; + let (x2, y2) = self.e_forward(arc, lam, phi + h_phi); + let dx_phi = (x2 - ax) / h_phi; + let dy_phi = (y2 - ay) / h_phi; + let det = dx_lam * dy_phi - dx_phi * dy_lam; + if det != 0.0 { + d_lam_x = dy_phi / det; + d_lam_y = -dx_phi / det; + d_phi_x = -dy_lam / det; + d_phi_y = dx_lam / det; + } + } + let step_lam = (dx * d_lam_x + dy * d_lam_y).clamp(-MAX_STEP, MAX_STEP); + lam = (lam - step_lam).clamp(-PI, PI); + let step_phi = (dx * d_phi_x + dy * d_phi_y).clamp(-MAX_STEP, MAX_STEP); + phi = (phi - step_phi).clamp(-FRAC_PI_2, FRAC_PI_2); + } + Err(DomainReason::NoConvergence) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const MM: f64 = 1e-3; + const DEG_TOL: f64 = 1e-9; + + fn params(lat0_deg: f64, lon0_deg: f64, false_easting: f64, false_northing: f64) -> CassParams { + CassParams { + lat0_deg, + lon0_deg, + false_easting, + false_northing, + hyperbolic: false, + } + } + + fn check(p: &CassParams, ell: &Ellipsoid, pts: &[[f64; 4]]) { + let cass = Cassini::new(p, ell); + for [lon, lat, x, y] in pts.iter().copied() { + let (gx, gy) = cass.forward(lon.to_radians(), lat.to_radians()).unwrap(); + assert!( + (gx - x).abs() < MM && (gy - y).abs() < MM, + "{lon} {lat}: {gx} {gy}" + ); + let (glam, gphi) = cass.inverse(x, y).unwrap(); + assert!( + (glam.to_degrees() - lon).abs() < DEG_TOL + && (gphi.to_degrees() - lat).abs() < DEG_TOL, + "{x} {y}: {} {}", + glam.to_degrees(), + gphi.to_degrees() + ); + } + } + + /// GN 7-2 §3.2.2 example, Trinidad 1903 / Trinidad Grid, in Clarke's + /// links: Clarke 1858 a = 31 706 587.88 links, 1/f = 294.2606764, + /// φ0 = 10°26′30″N, λ0 = 61°20′00″W, FE = 430 000 links, + /// FN = 325 000 links. φ = 10°N, λ = 62°W → E = 66 644.94 links, + /// N = 82 536.22 links. + #[test] + fn gn7_2_trinidad() { + let lat0 = 10.0 + 26.0 / 60.0 + 30.0 / 3600.0; + let lon0 = -(61.0 + 20.0 / 60.0); + let ell = Ellipsoid { + a: 31_706_587.88, + inv_f: 294.260_676_4, + }; + let cass = Cassini::new(¶ms(lat0, lon0, 430_000.0, 325_000.0), &ell); + let (x, y) = cass + .forward((-62f64).to_radians(), 10f64.to_radians()) + .unwrap(); + assert!( + (x - 66_644.94).abs() < 0.005 && (y - 82_536.22).abs() < 0.005, + "{x} {y}" + ); + let (lam, phi) = cass.inverse(66_644.94, 82_536.22).unwrap(); + assert!((lam.to_degrees() + 62.0).abs() < 1e-7 && (phi.to_degrees() - 10.0).abs() < 1e-7); + } + + /// PROJ 9.8.1 via pyproj 3.8.0 for the GDM2000 state grids, EPSG + /// 3377–3385, each `+proj=cass +lat_0=… +lon_0=… +x_0=… +y_0=… + /// +ellps=GRS80` as in the EPSG table. Rows: (lat0, lon0, x0, y0, + /// lon, lat, x, y). + #[test] + fn gdm2000_state_grids() { + for row in GDM2000 { + let [lat0, lon0, x0, y0, lon, lat, x, y] = *row; + check( + ¶ms(lat0, lon0, x0, y0), + &Ellipsoid::GRS80, + &[[lon, lat, x, y]], + ); + } + } + + const GDM2000: &[[f64; 8]] = &[ + // EPSG:3377 GDM2000 / Johor Grid + [ + 2.12167974444444, + 103.427936236111, + -14810.562, + 8758.32, + 103.52, + 2.08, + -4568.778015175592, + 4149.848894692858, + ], + // EPSG:3377 GDM2000 / Johor Grid + [ + 2.12167974444444, + 103.427936236111, + -14810.562, + 8758.32, + 102.54, + 2.85, + -113533.72065212582, + 89331.37648655649, + ], + // EPSG:3378 GDM2000 / Sembilan and Melaka Grid + [ + 2.68234763611111, + 101.974905041667, + 3673.785, + -4240.573, + 102.205, + 2.655, + 29260.52717390083, + -7262.203986713459, + ], + // EPSG:3378 GDM2000 / Sembilan and Melaka Grid + [ + 2.68234763611111, + 101.974905041667, + 3673.785, + -4240.573, + 101.8, + 3.18, + -15766.774675108012, + 50790.071188531205, + ], + // EPSG:3379 GDM2000 / Pahang Grid + [ + 3.76938808888889, + 102.368298983333, + -7368.228, + 6485.858, + 102.5, + 3.615, + 7263.685344680928, + -10585.143806558506, + ], + // EPSG:3379 GDM2000 / Pahang Grid + [ + 3.76938808888889, + 102.368298983333, + -7368.228, + 6485.858, + 101.43, + 4.68, + -111473.23475130659, + 107251.17740146085, + ], + // EPSG:3380 GDM2000 / Selangor Grid + [ + 3.68464905, + 101.389107913889, + -34836.161, + 56464.049, + 101.365, + 3.205, + -37515.672158265, + 3425.3081720405753, + ], + // EPSG:3380 GDM2000 / Selangor Grid + [ + 3.68464905, + 101.389107913889, + -34836.161, + 56464.049, + 100.86, + 3.77, + -93609.57369879901, + 65919.91306226248, + ], + // EPSG:3381 GDM2000 / Terengganu Grid + [ + 4.9762852, + 103.070275625, + 19594.245, + 3371.895, + 103.05, + 4.895, + 17345.350054870152, + -5616.791316690804, + ], + // EPSG:3381 GDM2000 / Terengganu Grid + [ + 4.9762852, + 103.070275625, + 19594.245, + 3371.895, + 102.48, + 5.8, + -45780.77549408487, + 94495.6759236236, + ], + // EPSG:3382 GDM2000 / Pinang Grid + [ + 5.42151754166667, + 100.344376963889, + -23.414, + 62.283, + 100.34, + 5.355, + -508.54300377559804, + -7293.495598518345, + ], + // EPSG:3382 GDM2000 / Pinang Grid + [ + 5.42151754166667, + 100.344376963889, + -23.414, + 62.283, + 100.22, + 5.49, + -13805.905549187662, + 7636.799863656235, + ], + // EPSG:3383 GDM2000 / Kedah and Perlis Grid + [ + 5.96467271388889, + 100.636371111111, + 0.0, + 0.0, + 100.355, + 5.9, + -31157.270629031478, + -7144.041483735553, + ], + // EPSG:3383 GDM2000 / Kedah and Perlis Grid + [ + 5.96467271388889, + 100.636371111111, + 0.0, + 0.0, + 99.69, + 6.62, + -104651.73329365306, + 72570.73845025174, + ], + // EPSG:3384 GDM2000 / Perak Grid + [ + 4.85906302222222, + 100.815410586111, + -1.769, + 133454.779, + 101.035, + 4.79, + 24358.006918962714, + 125821.54086360031, + ], + // EPSG:3384 GDM2000 / Perak Grid + [ + 4.85906302222222, + 100.815410586111, + -1.769, + 133454.779, + 100.17, + 5.82, + -71480.64811689194, + 239759.78035247474, + ], + // EPSG:3385 GDM2000 / Kelantan Grid + [ + 5.97254365833333, + 102.295241669444, + 13227.851, + 8739.894, + 102.0, + 5.415, + -19492.60336433349, + -52908.23524820403, + ], + // EPSG:3385 GDM2000 / Kelantan Grid + [ + 5.97254365833333, + 102.295241669444, + 13227.851, + 8739.894, + 101.43, + 6.19, + -82532.54050499732, + 32865.65315694644, + ], + ]; + + /// PROJ 9.8.1 via pyproj 3.8.0, `+proj=cass +lat_0=48 +lon_0=16 +R=6370997`. + #[test] + fn sphere() { + check( + ¶ms(48.0, 16.0, 0.0, 0.0), + &Ellipsoid::sphere(6_370_997.0), + SPHERE_PTS, + ); + } + + const SPHERE_PTS: &[[f64; 4]] = &[ + [17.0, 49.0, 72948.29160438642, 111675.35102138421], + [14.0, 46.0, -154468.66333483803, -220449.84087588376], + [16.0, 48.0, 0.0, 0.0], + ]; + + /// EPSG 9833, PROJ 9.8.1 via pyproj 3.8.0, EPSG:3139 Vanua Levu Grid + /// in metres instead of links: `+proj=cass +hyperbolic +lat_0=-16.25 + /// +lon_0=179.333333333333 +x_0=251727.9155424 +y_0=334519.953768 + /// +a=6378306.3696 +b=6356571.996`. + #[test] + fn hyperbolic_vanua_levu() { + let mut p = params( + -16.25, + 179.333_333_333_333, + 251_727.915_542_4, + 334_519.953_768, + ); + p.hyperbolic = true; + let a = 6_378_306.369_6; + let ell = Ellipsoid { + a, + inv_f: a / (a - 6_356_571.996), + }; + check(&p, &ell, HYPERBOLIC_PTS); + } + + const HYPERBOLIC_PTS: &[[f64; 4]] = &[ + [179.3, -16.4, 248167.17721095833, 317921.5387187793], + [178.9, -16.6, 205485.89162969342, 295740.8647022293], + [-179.9, -16.2, 333707.8206480166, 339899.5818423088], + ]; + + #[test] + fn parses_and_writes() { + let p = ProjParams::tokenize("+proj=cass +lat_0=2.12167974444444 +lon_0=103.427936236111 +x_0=-14810.562 +y_0=8758.32").unwrap(); + let c = CassParams::from_proj(&p).unwrap(); + assert!(!c.hyperbolic); + let mut s = String::new(); + c.write_proj(&mut s); + assert_eq!( + s, + " +proj=cass +lat_0=2.12167974444444 +lon_0=103.427936236111 +x_0=-14810.562 +y_0=8758.32" + ); + } +} diff --git a/nodedb-crs/src/method/cea.rs b/nodedb-crs/src/method/cea.rs new file mode 100644 index 000000000..efd9e2d5b --- /dev/null +++ b/nodedb-crs/src/method/cea.rs @@ -0,0 +1,259 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Lambert Cylindrical Equal Area, PROJ `cea`. +//! +//! Covers EPSG methods 9835 (ellipsoidal) and 9834 (spherical). Follows +//! PROJ 9.8.1 `src/projections/cea.cpp`, IOGP Guidance Note 7-2 §3.5.5 and +//! Snyder 1987 eqs. 10-15 to 10-19. + +use core::f64::consts::FRAC_PI_2; +use core::fmt::Write; + +use serde::{Deserialize, Serialize}; + +use super::angle::wrap_pi; +use super::authalic::Authalic; +use crate::def::Ellipsoid; +use crate::error::{CrsParseError, DomainReason}; +use crate::parse::tokens::ProjParams; + +/// PROJ `cea.cpp` EPS: slack on |sin φ| ≤ 1 in the spherical inverse. +const SPHERE_EPS: f64 = 1e-10; + +/// Largest absolute standard parallel, degrees, exclusive. +const MAX_LAT_TS_DEG: f64 = 90.0; + +/// Defining parameters of one Lambert Cylindrical Equal Area projection. +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Serialize, + Deserialize, + zerompk::ToMessagePack, + zerompk::FromMessagePack, +)] +pub struct CeaParams { + /// Longitude of natural origin λ0, degrees. + pub lon0_deg: f64, + /// Latitude of the standard parallel φ1, degrees. When absent, `k0` + /// sets the scale on the equator, as PROJ reads `+k_0`. + pub lat_ts_deg: Option, + /// Scale factor on the equator. Ignored when `lat_ts_deg` is set. + pub k0: f64, + /// False easting, metres. + pub false_easting: f64, + /// False northing, metres. + pub false_northing: f64, +} + +impl CeaParams { + /// Read from PROJ-string parameters. + pub fn from_proj(p: &ProjParams<'_>) -> Result { + let lat_ts_deg = p.number("lat_ts")?; + if let Some(lat_ts) = lat_ts_deg + && lat_ts.abs() >= MAX_LAT_TS_DEG + { + return Err(CrsParseError::invalid("lat_ts", &lat_ts.to_string())); + } + let k0 = p.number_any(&["k_0", "k"])?.unwrap_or(1.0); + if k0 <= 0.0 { + return Err(CrsParseError::invalid("k_0", &k0.to_string())); + } + Ok(Self { + lon0_deg: p.number_or("lon_0", 0.0)?, + lat_ts_deg, + k0, + false_easting: p.number_or("x_0", 0.0)?, + false_northing: p.number_or("y_0", 0.0)?, + }) + } + + /// Append ` +proj=cea` and every defining parameter. + pub fn write_proj(&self, out: &mut String) { + out.push_str(" +proj=cea"); + let _ = match self.lat_ts_deg { + Some(lat_ts) => write!(out, " +lat_ts={lat_ts}"), + None => write!(out, " +k_0={}", self.k0), + }; + let _ = write!( + out, + " +lon_0={} +x_0={} +y_0={}", + self.lon0_deg, self.false_easting, self.false_northing + ); + } +} + +/// A Lambert Cylindrical Equal Area projection with its constants computed. +#[derive(Debug, Clone, PartialEq)] +pub struct CylindricalEqualArea { + /// λ0 in radians. + lon0: f64, + /// Semi-major axis a, metres. + a: f64, + /// Scale factor on the equator. + k0: f64, + /// Authalic latitude conversion. `None` on a sphere. + authalic: Option, + false_easting: f64, + false_northing: f64, +} + +impl CylindricalEqualArea { + pub fn new(p: &CeaParams, ell: &Ellipsoid) -> Self { + let sphere = ell.is_sphere(); + // GN 7-2 §3.5.5: k0 = cos φ1 / sqrt(1 − e² sin² φ1). + let k0 = match p.lat_ts_deg { + Some(lat_ts) => { + let (s, c) = lat_ts.to_radians().sin_cos(); + if sphere { + c + } else { + c / (1.0 - ell.e2() * s * s).sqrt() + } + } + None => p.k0, + }; + Self { + lon0: p.lon0_deg.to_radians(), + a: ell.a, + k0, + authalic: if sphere { + None + } else { + Some(Authalic::new(ell.e2())) + }, + false_easting: p.false_easting, + false_northing: p.false_northing, + } + } + + /// Project λ, φ radians to easting, northing metres. GN 7-2 §3.5.5: + /// E = FE + a k0 (λ − λ0), N = FN + a q / (2 k0). + pub fn forward(&self, lam: f64, phi: f64) -> Result<(f64, f64), DomainReason> { + let x = self.k0 * wrap_pi(lam - self.lon0); + let y = match &self.authalic { + Some(auth) => 0.5 * auth.q(phi.sin()) / self.k0, + None => phi.sin() / self.k0, + }; + Ok(( + self.false_easting + self.a * x, + self.false_northing + self.a * y, + )) + } + + /// Unproject easting, northing metres to λ, φ radians. + pub fn inverse(&self, x: f64, y: f64) -> Result<(f64, f64), DomainReason> { + let yk = (y - self.false_northing) / self.a * self.k0; + let phi = match &self.authalic { + // β = asin(2 N k0 / (a qp)), PROJ cea_e_inverse. + Some(auth) => { + let ratio = 2.0 * yk / auth.qp(); + if ratio.abs() > 1.0 { + return Err(DomainReason::OutsideProjection); + } + auth.from_authalic(ratio.asin()) + } + None => { + let t = yk.abs(); + if t - SPHERE_EPS > 1.0 { + return Err(DomainReason::OutsideProjection); + } + if t >= 1.0 { + FRAC_PI_2.copysign(yk) + } else { + yk.asin() + } + } + }; + let lam = (x - self.false_easting) / (self.a * self.k0); + Ok((wrap_pi(self.lon0 + lam), phi)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// One millimetre, the tolerance for every PROJ reference value. + const MM: f64 = 1e-3; + + /// Inverse tolerance in degrees. + const DEG_TOL: f64 = 1e-9; + + fn build(text: &str, ell: Ellipsoid) -> CylindricalEqualArea { + let p = CeaParams::from_proj(&ProjParams::tokenize(text).unwrap()).unwrap(); + CylindricalEqualArea::new(&p, &ell) + } + + fn check(m: &CylindricalEqualArea, lon: f64, lat: f64, want: [f64; 2]) { + let (x, y) = m.forward(lon.to_radians(), lat.to_radians()).unwrap(); + assert!( + (x - want[0]).abs() < MM && (y - want[1]).abs() < MM, + "({lon}, {lat}): got ({x}, {y}), want {want:?}" + ); + // The references carry 0.1 mm, so the inverse starts from (x, y). + let (lam, phi) = m.inverse(x, y).unwrap(); + assert!( + (lam.to_degrees() - lon).abs() < DEG_TOL && (phi.to_degrees() - lat).abs() < DEG_TOL, + "inverse ({lon}, {lat}): got ({}, {})", + lam.to_degrees(), + phi.to_degrees() + ); + } + + /// EPSG 9835, WGS 84 / NSIDC EASE-Grid 2.0 Global (EPSG:6933). pyproj + /// 3.8.0 / PROJ 9.8.1, "+proj=cea +lat_ts=30 +lon_0=0 +datum=WGS84". + #[test] + fn ease_grid_2_ellipsoidal() { + let m = build( + "+proj=cea +lat_ts=30 +lon_0=0 +x_0=0 +y_0=0", + Ellipsoid::WGS84, + ); + check(&m, 10.0, 5.0, [964_862.802_5, 637_078.744_0]); + check(&m, -120.0, 60.0, [-11_578_353.630_1, 6_351_419.997_3]); + check(&m, 170.0, -80.0, [16_402_667.642_7, -7_229_704.847_6]); + } + + /// EPSG 9834, NSIDC EASE-Grid Global (EPSG:3410). pyproj, "+proj=cea + /// +lat_ts=30 +lon_0=0 +R=6371228". + #[test] + fn ease_grid_spherical() { + let m = build( + "+proj=cea +lat_ts=30 +lon_0=0 +x_0=0 +y_0=0", + Ellipsoid::sphere(6_371_228.0), + ); + check(&m, 10.0, 5.0, [963_010.774_6, 641_192.632_6]); + check(&m, -120.0, 60.0, [-11_556_129.295_8, 6_371_228.000_0]); + check(&m, 170.0, -80.0, [16_371_183.169_0, -7_245_093.161_5]); + } + + #[test] + fn beyond_the_pole_is_outside() { + let m = build("+proj=cea +lat_ts=30", Ellipsoid::WGS84); + assert_eq!(m.inverse(0.0, 1e7), Err(DomainReason::OutsideProjection)); + let s = build("+proj=cea +lat_ts=30", Ellipsoid::sphere(6_371_228.0)); + assert_eq!(s.inverse(0.0, 1e7), Err(DomainReason::OutsideProjection)); + } + + #[test] + fn parse_and_round_trip() { + for text in [ + "+proj=cea +lat_ts=30 +lon_0=-5 +x_0=1", + "+proj=cea +k_0=0.9 +y_0=7", + ] { + let p = CeaParams::from_proj(&ProjParams::tokenize(text).unwrap()).unwrap(); + let mut out = String::new(); + p.write_proj(&mut out); + assert_eq!( + CeaParams::from_proj(&ProjParams::tokenize(&out).unwrap()), + Ok(p), + "{out}" + ); + } + assert!( + CeaParams::from_proj(&ProjParams::tokenize("+proj=cea +lat_ts=95").unwrap()).is_err() + ); + } +} diff --git a/nodedb-crs/src/method/conformal.rs b/nodedb-crs/src/method/conformal.rs new file mode 100644 index 000000000..cbe3ac6b1 --- /dev/null +++ b/nodedb-crs/src/method/conformal.rs @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Conformal-latitude helpers shared by the conformal projections. +//! +//! Follows PROJ 9.8.1 `src/msfn.cpp`, `src/tsfn.cpp` and `src/phi2.cpp`, +//! and Snyder, "Map Projections: A Working Manual" (USGS PP 1395, 1987). + +use crate::error::DomainReason; + +/// Newton iterations allowed in [`tanphi_from_sinhpsi`]. PROJ `phi2.cpp` +/// uses 5. Two reach double precision for terrestrial flattening. +pub const SINHPSI_MAX_ITERS: usize = 5; + +/// τ' above which the Newton start uses τ = exp(e·atanh e)·τ'. PROJ +/// `phi2.cpp`: 70 corresponds to χ = 89.18°. +const LARGE_TAUP: f64 = 70.0; + +/// m(φ) = cos φ / sqrt(1 − e² sin² φ) (Snyder 14-15, PROJ `pj_msfn`). +pub fn msfn(sinphi: f64, cosphi: f64, e2: f64) -> f64 { + cosphi / (1.0 - e2 * sinphi * sinphi).sqrt() +} + +/// t(φ) = tan(π/4 − φ/2) / ((1 − e sin φ)/(1 + e sin φ))^(e/2) (Snyder +/// 15-9, PROJ `pj_tsfn`). Computed as exp(−ψ), ψ the isometric latitude, +/// in the form PROJ uses for full accuracy on both hemispheres. +pub fn tsfn(phi: f64, sinphi: f64, e: f64) -> f64 { + let cosphi = phi.cos(); + let geo = if sinphi > 0.0 { + cosphi / (1.0 + sinphi) + } else { + (1.0 - sinphi) / cosphi + }; + (e * (e * sinphi).atanh()).exp() * geo +} + +/// tan φ from τ' = sinh ψ = tan χ, by Newton's method (Karney 2011 eq. 7, +/// PROJ `pj_sinhpsi2tanphi`). Err(NoConvergence) when the iteration does +/// not settle. +pub fn tanphi_from_sinhpsi(taup: f64, e: f64) -> Result { + let rooteps = f64::EPSILON.sqrt(); + let tol = rooteps / 10.0; + let tmax = 2.0 / rooteps; + let e2m = 1.0 - e * e; + let stol = tol * taup.abs().max(1.0); + let mut tau = if taup.abs() > LARGE_TAUP { + taup * (e * e.atanh()).exp() + } else { + taup / e2m + }; + // Covers ±∞, NaN and e = 1, as in PROJ. + if tau.is_nan() || tau.abs() >= tmax { + return Ok(tau); + } + for _ in 0..SINHPSI_MAX_ITERS { + let tau1 = (1.0 + tau * tau).sqrt(); + let sig = (e * (e * tau / tau1).atanh()).sinh(); + let taupa = (1.0 + sig * sig).sqrt() * tau - sig * tau1; + let dtau = + (taup - taupa) * (1.0 + e2m * tau * tau) / (e2m * tau1 * (1.0 + taupa * taupa).sqrt()); + tau += dtau; + if dtau.is_nan() || dtau.abs() < stol { + return Ok(tau); + } + } + Err(DomainReason::NoConvergence) +} + +/// Inverse of [`tsfn`] (Snyder 7-9, PROJ `pj_phi2`): φ from t = exp(−ψ). +/// Err(NoConvergence) if the iteration fails. +pub fn phi_from_ts(ts: f64, e: f64) -> Result { + Ok(tanphi_from_sinhpsi((1.0 / ts - ts) / 2.0, e)?.atan()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::def::Ellipsoid; + + #[test] + fn ts_round_trips() { + let e = Ellipsoid::WGS84.e(); + for deg in [-89.9, -60.0, -1.0, 0.0, 0.5, 45.0, 85.0, 89.99] { + let phi = f64::to_radians(deg); + let ts = tsfn(phi, phi.sin(), e); + let back = phi_from_ts(ts, e).unwrap(); + assert!((back - phi).abs() < 1e-14, "{deg}: {back}"); + } + } + + #[test] + fn sphere_ts_is_tan_half_colatitude() { + let phi = 0.7_f64; + let want = (core::f64::consts::FRAC_PI_4 - phi / 2.0).tan(); + assert!((tsfn(phi, phi.sin(), 0.0) - want).abs() < 1e-15); + assert!((phi_from_ts(want, 0.0).unwrap() - phi).abs() < 1e-15); + } + + #[test] + fn msfn_on_equator_is_one() { + assert_eq!(msfn(0.0, 1.0, Ellipsoid::GRS80.e2()), 1.0); + let phi = 0.5_f64; + assert_eq!(msfn(phi.sin(), phi.cos(), 0.0), phi.cos()); + } + + #[test] + fn pole_and_infinite_tau_pass_through() { + let e = Ellipsoid::WGS84.e(); + assert_eq!(tanphi_from_sinhpsi(f64::INFINITY, e), Ok(f64::INFINITY)); + assert_eq!(tanphi_from_sinhpsi(0.0, e), Ok(0.0)); + } +} diff --git a/nodedb-crs/src/method/eqc.rs b/nodedb-crs/src/method/eqc.rs new file mode 100644 index 000000000..37e2fc6bc --- /dev/null +++ b/nodedb-crs/src/method/eqc.rs @@ -0,0 +1,245 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Equidistant Cylindrical, PROJ `eqc`. +//! +//! Covers EPSG methods 1028 (ellipsoidal) and 1029 (spherical). Follows +//! PROJ 9.8.1 `src/projections/eqc.cpp` and IOGP Guidance Note 7-2 §3.2.5: +//! E = FE + ν1 cos φ1 (λ − λ0), N = FN + M(φ) − M(φ0). PROJ picks the +//! spherical form when the ellipsoid is a sphere. + +use core::f64::consts::FRAC_PI_2; +use core::fmt::Write; + +use serde::{Deserialize, Serialize}; + +use super::angle::wrap_pi; +use super::meridian::MeridianArc; +use crate::def::Ellipsoid; +use crate::error::{CrsParseError, DomainReason}; +use crate::parse::tokens::ProjParams; + +/// Largest absolute latitude, degrees. +const MAX_LAT_DEG: f64 = 90.0; + +/// Defining parameters of one Equidistant Cylindrical projection. +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Serialize, + Deserialize, + zerompk::ToMessagePack, + zerompk::FromMessagePack, +)] +pub struct EqcParams { + /// Latitude of the standard parallel φ1, degrees. + pub lat_ts_deg: f64, + /// Latitude of natural origin φ0, degrees. + pub lat0_deg: f64, + /// Longitude of natural origin λ0, degrees. + pub lon0_deg: f64, + /// False easting, metres. + pub false_easting: f64, + /// False northing, metres. + pub false_northing: f64, +} + +impl EqcParams { + /// Read from PROJ-string parameters. PROJ defaults `+lat_ts` and + /// `+lat_0` to 0 and ignores `+k`. + pub fn from_proj(p: &ProjParams<'_>) -> Result { + let lat_ts_deg = p.number_or("lat_ts", 0.0)?; + if lat_ts_deg.abs() >= MAX_LAT_DEG { + return Err(CrsParseError::invalid("lat_ts", &lat_ts_deg.to_string())); + } + let lat0_deg = p.number_or("lat_0", 0.0)?; + if lat0_deg.abs() > MAX_LAT_DEG { + return Err(CrsParseError::invalid("lat_0", &lat0_deg.to_string())); + } + Ok(Self { + lat_ts_deg, + lat0_deg, + lon0_deg: p.number_or("lon_0", 0.0)?, + false_easting: p.number_or("x_0", 0.0)?, + false_northing: p.number_or("y_0", 0.0)?, + }) + } + + /// Append ` +proj=eqc` and every defining parameter. + pub fn write_proj(&self, out: &mut String) { + let _ = write!( + out, + " +proj=eqc +lat_ts={} +lat_0={} +lon_0={} +x_0={} +y_0={}", + self.lat_ts_deg, self.lat0_deg, self.lon0_deg, self.false_easting, self.false_northing + ); + } +} + +/// An Equidistant Cylindrical projection with its constants computed. +#[derive(Debug, Clone, PartialEq)] +pub struct EquidistantCylindrical { + /// λ0 in radians. + lon0: f64, + /// φ0 in radians. + phi0: f64, + /// Semi-major axis a, metres. + a: f64, + /// ν1 cos φ1 over a, or cos φ1 on a sphere. + rc: f64, + /// Meridian arc on an ellipsoid, `None` on a sphere. + arc: Option, + /// M(φ0) over a. + m0: f64, + false_easting: f64, + false_northing: f64, +} + +impl EquidistantCylindrical { + pub fn new(p: &EqcParams, ell: &Ellipsoid) -> Self { + let phi0 = p.lat0_deg.to_radians(); + let (sin1, cos1) = p.lat_ts_deg.to_radians().sin_cos(); + let (rc, arc, m0) = if ell.is_sphere() { + (cos1, None, 0.0) + } else { + let e2 = ell.e2(); + let arc = MeridianArc::new(e2); + let m0 = arc.arc(phi0, phi0.sin(), phi0.cos()); + (cos1 / (1.0 - e2 * sin1 * sin1).sqrt(), Some(arc), m0) + }; + Self { + lon0: p.lon0_deg.to_radians(), + phi0, + a: ell.a, + rc, + arc, + m0, + false_easting: p.false_easting, + false_northing: p.false_northing, + } + } + + /// Project λ, φ radians to easting, northing metres. + pub fn forward(&self, lam: f64, phi: f64) -> Result<(f64, f64), DomainReason> { + let x = self.rc * wrap_pi(lam - self.lon0); + let y = match &self.arc { + Some(arc) => arc.arc(phi, phi.sin(), phi.cos()) - self.m0, + None => phi - self.phi0, + }; + Ok(( + self.false_easting + self.a * x, + self.false_northing + self.a * y, + )) + } + + /// Unproject easting, northing metres to λ, φ radians. + pub fn inverse(&self, x: f64, y: f64) -> Result<(f64, f64), DomainReason> { + let dy = (y - self.false_northing) / self.a; + let phi = match &self.arc { + Some(arc) => arc.inverse(dy + self.m0)?, + None => dy + self.phi0, + }; + if phi.abs() > FRAC_PI_2 { + return Err(DomainReason::OutsideProjection); + } + let lam = (x - self.false_easting) / (self.a * self.rc); + Ok((wrap_pi(self.lon0 + lam), phi)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// One millimetre, the tolerance for every PROJ reference value. + const MM: f64 = 1e-3; + + /// Inverse tolerance in degrees. + const DEG_TOL: f64 = 1e-9; + + fn build(text: &str, ell: Ellipsoid) -> EquidistantCylindrical { + let p = EqcParams::from_proj(&ProjParams::tokenize(text).unwrap()).unwrap(); + EquidistantCylindrical::new(&p, &ell) + } + + fn check(m: &EquidistantCylindrical, lon: f64, lat: f64, want: [f64; 2]) { + let (x, y) = m.forward(lon.to_radians(), lat.to_radians()).unwrap(); + assert!( + (x - want[0]).abs() < MM && (y - want[1]).abs() < MM, + "({lon}, {lat}): got ({x}, {y}), want {want:?}" + ); + // The references carry 0.1 mm, so the inverse starts from (x, y). + let (lam, phi) = m.inverse(x, y).unwrap(); + assert!( + (lam.to_degrees() - lon).abs() < DEG_TOL && (phi.to_degrees() - lat).abs() < DEG_TOL, + "inverse ({lon}, {lat}): got ({}, {})", + lam.to_degrees(), + phi.to_degrees() + ); + } + + /// GN 7-2 §3.2.5 example, EPSG 1028: WGS 84 / World Equidistant + /// Cylindrical (EPSG:4087), point 55°N, 10°E. GN 7-2 prints + /// E = 1113194.91 m, N = 6097230.31 m. The other points are pyproj + /// 3.8.0 / PROJ 9.8.1 on the same string. + #[test] + fn gn_1028_world_equidistant_cylindrical() { + let m = build( + "+proj=eqc +lat_ts=0 +lat_0=0 +lon_0=0 +x_0=0 +y_0=0", + Ellipsoid::WGS84, + ); + let (x, y) = m.forward(10f64.to_radians(), 55f64.to_radians()).unwrap(); + assert!((x - 1_113_194.91).abs() < 0.005 && (y - 6_097_230.31).abs() < 0.005); + check(&m, 10.0, 55.0, [1_113_194.907_9, 6_097_230.313_1]); + check(&m, -120.0, -30.0, [-13_358_338.895_2, -3_320_113.397_9]); + check(&m, 170.0, 89.0, [18_924_313.434_9, 9_890_271.864_4]); + } + + /// pyproj, "+proj=eqc +lat_ts=30 +lat_0=10 +lon_0=5 +x_0=100 +y_0=200 + /// +ellps=intl". + #[test] + fn ellipsoidal_with_offsets() { + let m = build( + "+proj=eqc +lat_ts=30 +lat_0=10 +lon_0=5 +x_0=100 +y_0=200", + Ellipsoid::INTERNATIONAL_1924, + ); + check(&m, 20.0, 40.0, [1_447_456.286_2, 3_323_937.630_0]); + check(&m, -10.0, -20.0, [-1_447_256.286_2, -3_318_061.358_9]); + } + + /// EPSG 1029 form. pyproj, "+proj=eqc +lat_ts=30 +lat_0=10 +lon_0=5 + /// +R=6371007". + #[test] + fn spherical() { + let m = build( + "+proj=eqc +lat_ts=30 +lat_0=10 +lon_0=5", + Ellipsoid::sphere(6_371_007.0), + ); + check(&m, 20.0, 40.0, [1_444_466.055_8, 3_335_851.464_5]); + check(&m, -10.0, -20.0, [-1_444_466.055_8, -3_335_851.464_5]); + } + + #[test] + fn beyond_the_pole_is_outside() { + let m = build("+proj=eqc", Ellipsoid::WGS84); + assert_eq!(m.inverse(0.0, 1.1e7), Err(DomainReason::OutsideProjection)); + } + + #[test] + fn parse_and_round_trip() { + let p = EqcParams::from_proj( + &ProjParams::tokenize("+proj=eqc +lat_ts=30 +lat_0=10 +lon_0=5 +x_0=1").unwrap(), + ) + .unwrap(); + let mut out = String::new(); + p.write_proj(&mut out); + assert_eq!( + EqcParams::from_proj(&ProjParams::tokenize(&out).unwrap()), + Ok(p), + "{out}" + ); + assert!( + EqcParams::from_proj(&ProjParams::tokenize("+proj=eqc +lat_ts=90").unwrap()).is_err() + ); + } +} diff --git a/nodedb-crs/src/method/eqearth.rs b/nodedb-crs/src/method/eqearth.rs new file mode 100644 index 000000000..6120a1964 --- /dev/null +++ b/nodedb-crs/src/method/eqearth.rs @@ -0,0 +1,255 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Equal Earth, EPSG method 1078, PROJ `eqearth`. +//! +//! Follows PROJ 9.8.1 `src/projections/eqearth.cpp` (B. Šavrič, +//! T. Patterson, B. Jenny, "The Equal Earth map projection", IJGIS 33 +//! (2019), eqs. 1–2; IOGP Guidance Note 7-2 §3.12.3). The ellipsoidal form +//! applies the spherical formulas to the authalic latitude and scales by +//! the authalic radius. + +use core::f64::consts::PI; +use core::fmt::Write; + +use serde::{Deserialize, Serialize}; + +use super::angle::wrap_pi; +use super::authalic::Authalic; +use crate::def::Ellipsoid; +use crate::error::{CrsParseError, DomainReason}; +use crate::parse::tokens::ProjParams; + +/// Polynomial coefficients A1 to A4, Šavrič et al. 2019 eq. 1. +const A1: f64 = 1.340_264; +const A2: f64 = -0.081_106; +const A3: f64 = 0.000_893; +const A4: f64 = 0.003_796; + +/// y at 90° latitude on the unit sphere, PROJ `MAX_Y`. +const MAX_Y: f64 = 1.317_362_759_157_4; + +/// Newton step below which the inverse stops, PROJ `EPS`. +const EPS: f64 = 1e-11; + +/// Newton iterations of the inverse, PROJ `MAX_ITER`. +const MAX_ITER: usize = 12; + +/// |λ − λ0| beyond π by more than this is outside the map. +const LON_TOL: f64 = 1e-10; + +/// M = √3 / 2, Šavrič et al. 2019 eq. 1. +fn m() -> f64 { + 3f64.sqrt() / 2.0 +} + +/// Defining parameters of one Equal Earth projection. +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Serialize, + Deserialize, + zerompk::ToMessagePack, + zerompk::FromMessagePack, +)] +pub struct EqEarthParams { + /// Longitude of natural origin λ0, degrees (`+lon_0`). + pub lon0_deg: f64, + /// False easting, metres (`+x_0`). + pub false_easting: f64, + /// False northing, metres (`+y_0`). + pub false_northing: f64, +} + +impl EqEarthParams { + /// Read from PROJ-string parameters. Every key defaults to 0 as in PROJ. + pub fn from_proj(p: &ProjParams<'_>) -> Result { + Ok(Self { + lon0_deg: p.number_or("lon_0", 0.0)?, + false_easting: p.number_or("x_0", 0.0)?, + false_northing: p.number_or("y_0", 0.0)?, + }) + } + + /// Append the defining parameters as PROJ-string tokens. + pub fn write_proj(&self, out: &mut String) { + let _ = write!( + out, + " +proj=eqearth +lon_0={} +x_0={} +y_0={}", + self.lon0_deg, self.false_easting, self.false_northing + ); + } +} + +/// Equal Earth with its constants computed. +#[derive(Debug, Clone, PartialEq)] +pub struct EqualEarth { + params: EqEarthParams, + a: f64, + lon0: f64, + /// Authalic conversion; `None` on a sphere. + auth: Option, + /// Authalic radius over a, √(qp / 2); 1 on a sphere. + rqda: f64, +} + +impl EqualEarth { + pub fn new(p: &EqEarthParams, ell: &Ellipsoid) -> Self { + let (auth, rqda) = if ell.is_sphere() { + (None, 1.0) + } else { + let a = Authalic::new(ell.e2()); + let rqda = (0.5 * a.qp()).sqrt(); + (Some(a), rqda) + }; + Self { + params: *p, + a: ell.a, + lon0: p.lon0_deg.to_radians(), + auth, + rqda, + } + } + + pub fn forward(&self, lam: f64, phi: f64) -> Result<(f64, f64), DomainReason> { + let lam = wrap_pi(lam - self.lon0); + let mut sbeta = phi.sin(); + if let Some(a) = &self.auth { + // sin β = q / qp; rounding past ±1 clamps. + sbeta = (a.q(sbeta) / a.qp()).clamp(-1.0, 1.0); + } + let m = m(); + let psi = (m * sbeta).asin(); + let psi2 = psi * psi; + let psi6 = psi2 * psi2 * psi2; + let x = + lam * psi.cos() / (m * (A1 + 3.0 * A2 * psi2 + psi6 * (7.0 * A3 + 9.0 * A4 * psi2))); + let y = psi * (A1 + A2 * psi2 + psi6 * (A3 + A4 * psi2)); + Ok(( + self.params.false_easting + self.a * x * self.rqda, + self.params.false_northing + self.a * y * self.rqda, + )) + } + + pub fn inverse(&self, x: f64, y: f64) -> Result<(f64, f64), DomainReason> { + let x = (x - self.params.false_easting) / self.a / self.rqda; + let y = ((y - self.params.false_northing) / self.a / self.rqda).clamp(-MAX_Y, MAX_Y); + let mut yc = y; + let mut converged = false; + for _ in 0..MAX_ITER { + let y2 = yc * yc; + let y6 = y2 * y2 * y2; + let f = yc * (A1 + A2 * y2 + y6 * (A3 + A4 * y2)) - y; + let fder = A1 + 3.0 * A2 * y2 + y6 * (7.0 * A3 + 9.0 * A4 * y2); + let tol = f / fder; + yc -= tol; + if tol.abs() < EPS { + converged = true; + break; + } + } + if !converged { + return Err(DomainReason::NoConvergence); + } + let m = m(); + let y2 = yc * yc; + let y6 = y2 * y2 * y2; + let lam = m * x * (A1 + 3.0 * A2 * y2 + y6 * (7.0 * A3 + 9.0 * A4 * y2)) / yc.cos(); + if lam.abs() > PI + LON_TOL { + return Err(DomainReason::OutsideProjection); + } + let beta = (yc.sin() / m).asin(); + let phi = match &self.auth { + Some(a) => a.from_authalic(beta), + None => beta, + }; + Ok((wrap_pi(lam + self.lon0), phi)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const MM: f64 = 1e-3; + const DEG_TOL: f64 = 1e-9; + + fn params(lon0_deg: f64) -> EqEarthParams { + EqEarthParams { + lon0_deg, + false_easting: 0.0, + false_northing: 0.0, + } + } + + fn check(p: &EqEarthParams, ell: &Ellipsoid, pts: &[[f64; 4]]) { + let ee = EqualEarth::new(p, ell); + for [lon, lat, x, y] in pts.iter().copied() { + let (gx, gy) = ee.forward(lon.to_radians(), lat.to_radians()).unwrap(); + assert!( + (gx - x).abs() < MM && (gy - y).abs() < MM, + "{lon} {lat}: {gx} {gy}" + ); + let (glam, gphi) = ee.inverse(x, y).unwrap(); + assert!( + (glam.to_degrees() - lon).abs() < DEG_TOL + && (gphi.to_degrees() - lat).abs() < DEG_TOL, + "{x} {y}: {} {}", + glam.to_degrees(), + gphi.to_degrees() + ); + } + } + + /// EPSG:8857 WGS 84 / Equal Earth Greenwich, PROJ 9.8.1 via pyproj + /// 3.8.0, `+proj=eqearth +lon_0=0 +x_0=0 +y_0=0 +datum=WGS84`. + #[test] + fn greenwich() { + check(¶ms(0.0), &Ellipsoid::WGS84, GREENWICH); + } + + const GREENWICH: &[[f64; 4]] = &[ + [0.0, 0.0, 0.0, 0.0], + [10.0, 52.0, 778005.2988841359, 6184897.936534862], + [-120.0, -35.0, -10496394.36359727, -4352308.175397291], + [179.0, 80.0, 10585770.166707683, 8205603.096488446], + ]; + + /// EPSG:8859 WGS 84 / Equal Earth Asia-Pacific, PROJ 9.8.1 via pyproj + /// 3.8.0, `+proj=eqearth +lon_0=150 +x_0=0 +y_0=0 +datum=WGS84`. + #[test] + fn asia_pacific() { + check(¶ms(150.0), &Ellipsoid::WGS84, ASIA_PACIFIC); + } + + const ASIA_PACIFIC: &[[f64; 4]] = &[ + [-170.0, 10.0, 3804392.9698225083, 1281605.5100848149], + [100.0, -40.0, -4248749.153614973, -4921020.061780324], + [150.0, 0.0, 0.0, 0.0], + ]; + + /// PROJ 9.8.1 via pyproj 3.8.0, `+proj=eqearth +lon_0=0 +R=6371008.7714`. + #[test] + fn sphere() { + check( + ¶ms(0.0), + &Ellipsoid::sphere(6_371_008.771_4), + SPHERE_PTS, + ); + } + + const SPHERE_PTS: &[[f64; 4]] = &[ + [30.0, 45.0, 2463147.729110954, 5480539.791291671], + [-150.0, -70.0, -9751649.563443083, -7692885.362673924], + ]; + + #[test] + fn parses_and_writes() { + let p = ProjParams::tokenize("+proj=eqearth +lon_0=-90 +x_0=0 +y_0=0").unwrap(); + let q = EqEarthParams::from_proj(&p).unwrap(); + let mut s = String::new(); + q.write_proj(&mut s); + assert_eq!(s, " +proj=eqearth +lon_0=-90 +x_0=0 +y_0=0"); + } +} diff --git a/nodedb-crs/src/method/geodesic/inverse.rs b/nodedb-crs/src/method/geodesic/inverse.rs new file mode 100644 index 000000000..0ee77d1c4 --- /dev/null +++ b/nodedb-crs/src/method/geodesic/inverse.rs @@ -0,0 +1,506 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! The inverse geodesic problem, ported from GeographicLib's C library +//! (PROJ 9.8.1 `src/geodesic.c`, `geod_geninverse_int`). + +use super::line::GeodesicSolution; +use super::math::{ + DEGREE, HD, QD, ang_diff, ang_round, atan2_d, lat_fix, norm2, sin_cos_d, sin_cos_de, sq, +}; +use super::model::{Arc, Geodesic, MAXIT1, MAXIT2, TOL0, TOLB, tiny, tol2}; +use super::series::{ORDER, c4f, sin_cos_series}; +use super::start::{Ends, Lambda}; + +/// Multiplier of tol0 in the Newton exit test after `tripn`. +const TRIPN_TOL_FACTOR: f64 = 8.0; + +/// |v| below which Newton's quadratic convergence is trusted, in tol0. +const TRIPN_SET_FACTOR: f64 = 16.0; + +/// cos ω12 above which the area uses the tan(Γ/2) formula: ω12 < 3π/4. +/// GeographicLib writes the threshold as −0.7071. +const AREA_MAX_COMG12: f64 = -core::f64::consts::FRAC_1_SQRT_2; + +/// sin β2 − sin β1 below which the area uses the tan(Γ/2) formula. +const AREA_MAX_DSBET: f64 = 1.75; + +/// Inverse solution in sine/cosine form, before conversion to degrees. +struct Raw { + a12: f64, + s12: f64, + salp1: f64, + calp1: f64, + salp2: f64, + calp2: f64, + m12: f64, + big_m12: f64, + big_m21: f64, + area_s12: f64, +} + +impl Geodesic { + /// Solve the inverse problem between (`lat1`, `lon1`) and (`lat2`, + /// `lon2`): return (s12, azi1, azi2). Degrees and the units of the + /// semi-major axis. + pub fn inverse(&self, lat1: f64, lon1: f64, lat2: f64, lon2: f64) -> (f64, f64, f64) { + let sol = self.inverse_full(lat1, lon1, lat2, lon2); + (sol.s12, sol.azi1, sol.azi2) + } + + /// Solve the inverse problem with every output. + pub fn inverse_full(&self, lat1: f64, lon1: f64, lat2: f64, lon2: f64) -> GeodesicSolution { + let r = self.gen_inverse(lat1, lon1, lat2, lon2); + GeodesicSolution { + lat1, + lon1, + azi1: atan2_d(r.salp1, r.calp1), + lat2, + lon2, + azi2: atan2_d(r.salp2, r.calp2), + s12: r.s12, + a12: r.a12, + m12: r.m12, + big_m12: r.big_m12, + big_m21: r.big_m21, + area_s12: r.area_s12, + } + } + + fn gen_inverse(&self, lat1: f64, lon1: f64, lat2: f64, lon2: f64) -> Raw { + let tiny = tiny(); + let tol2 = tol2(); + // λ12 in [−180°, 180°], −180° only for west-going geodesics. + let (mut lon12, mut lon12s) = ang_diff(lon1, lon2); + let mut lonsign: f64 = if lon12.is_sign_negative() { -1.0 } else { 1.0 }; + lon12 *= lonsign; + lon12s *= lonsign; + let lam12 = lon12 * DEGREE; + let (slam12, clam12) = sin_cos_de(lon12, lon12s); + // The supplementary longitude difference. + let lon12s = (HD - lon12) - lon12s; + + // Treat points very close to the equator as on it. + let mut lat1 = ang_round(lat_fix(lat1)); + let mut lat2 = ang_round(lat_fix(lat2)); + // Point 1 has the larger |latitude|. A NaN latitude becomes lat1. + let swapp: f64 = if lat1.abs() < lat2.abs() || lat2.is_nan() { + -1.0 + } else { + 1.0 + }; + if swapp < 0.0 { + lonsign = -lonsign; + core::mem::swap(&mut lat1, &mut lat2); + } + // lat1 ≤ −0. + let latsign: f64 = if lat1.is_sign_negative() { 1.0 } else { -1.0 }; + lat1 *= latsign; + lat2 *= latsign; + // Canonical form: 0 ≤ λ12 ≤ 180, −90 ≤ lat1 ≤ −0, lat1 ≤ lat2 ≤ −lat1. + + let (sbet1, cbet1) = sin_cos_d(lat1); + let (sbet1, cbet1) = norm2(sbet1 * self.f1, cbet1); + let cbet1 = cbet1.max(tiny); + let (sbet2, cbet2) = sin_cos_d(lat2); + let (mut sbet2, cbet2) = norm2(sbet2 * self.f1, cbet2); + let mut cbet2 = cbet2.max(tiny); + + // Force β2 = ±β1 exactly when the sensitive measure vanishes. + if cbet1 < -sbet1 { + if cbet2 == cbet1 { + sbet2 = sbet1.copysign(sbet2); + } + } else if sbet2.abs() == -sbet1 { + cbet2 = cbet1; + } + + let dn1 = (1.0 + self.ep2 * sq(sbet1)).sqrt(); + let dn2 = (1.0 + self.ep2 * sq(sbet2)).sqrt(); + let ends = Ends { + sbet1, + cbet1, + dn1, + sbet2, + cbet2, + dn2, + }; + + let mut a12 = 0.0; + let mut sig12; + let (mut salp1, mut calp1, mut salp2, mut calp2) = (0.0, 0.0, 0.0, 0.0); + let (mut s12x, mut m12x) = (0.0, 0.0); + let (mut big_m12, mut big_m21) = (0.0, 0.0); + // somg12 = 2 marks "not computed yet". + let (mut omg12, mut somg12, mut comg12) = (0.0, 2.0, 0.0); + + let mut meridian = lat1 == -QD || slam12 == 0.0; + if meridian { + // Both endpoints lie on one full meridian. + calp1 = clam12; + salp1 = slam12; + calp2 = 1.0; + salp2 = 0.0; + let ssig1 = sbet1; + let csig1 = calp1 * cbet1; + let ssig2 = sbet2; + let csig2 = calp2 * cbet2; + sig12 = ((csig1 * ssig2 - ssig1 * csig2).max(0.0) + 0.0) + .atan2(csig1 * csig2 + ssig1 * ssig2); + let l = self.lengths( + &Arc { + eps: self.n, + sig12, + ssig1, + csig1, + dn1, + ssig2, + csig2, + dn2, + cbet1, + cbet2, + }, + true, + ); + s12x = l.s12b; + m12x = l.m12b; + big_m12 = l.big_m12; + big_m21 = l.big_m21; + // Zero-length geodesics can give m12 < 0, hence the σ12 test. + if sig12 < tol2 || m12x >= 0.0 { + if sig12 < 3.0 * tiny || (sig12 < TOL0 && (s12x < 0.0 || m12x < 0.0)) { + sig12 = 0.0; + m12x = 0.0; + s12x = 0.0; + } + m12x *= self.b; + s12x *= self.b; + a12 = sig12 / DEGREE; + } else { + // m12 < 0: prolate and too close to antipodal. + meridian = false; + } + } + + if !meridian && sbet1 == 0.0 && (self.f <= 0.0 || lon12s >= self.f * HD) { + // The geodesic runs along the equator. + calp1 = 0.0; + calp2 = 0.0; + salp1 = 1.0; + salp2 = 1.0; + s12x = self.a * lam12; + sig12 = lam12 / self.f1; + omg12 = sig12; + m12x = self.b * sig12.sin(); + big_m12 = sig12.cos(); + big_m21 = big_m12; + a12 = lon12 / self.f1; + } else if !meridian { + let start = self.inverse_start(&ends, lam12, slam12, clam12); + sig12 = start.sig12; + salp1 = start.salp1; + calp1 = start.calp1; + if sig12 >= 0.0 { + // Short line: the starting guess is the solution. + salp2 = start.salp2; + calp2 = start.calp2; + let dnm = start.dnm; + s12x = sig12 * self.b * dnm; + m12x = sq(dnm) * self.b * (sig12 / dnm).sin(); + big_m12 = (sig12 / dnm).cos(); + big_m21 = big_m12; + a12 = sig12 / DEGREE; + omg12 = lam12 / (self.f1 * dnm); + } else { + let lam = self.newton(&ends, &mut salp1, &mut calp1, slam12, clam12); + salp2 = lam.salp2; + calp2 = lam.calp2; + sig12 = lam.arc.sig12; + let l = self.lengths(&lam.arc, true); + m12x = l.m12b * self.b; + s12x = l.s12b * self.b; + big_m12 = l.big_m12; + big_m21 = l.big_m21; + a12 = sig12 / DEGREE; + // ω12 = λ12 − Δω12. + let (sdomg12, cdomg12) = lam.domg12.sin_cos(); + somg12 = slam12 * cdomg12 - clam12 * sdomg12; + comg12 = clam12 * cdomg12 + slam12 * sdomg12; + } + } + + // Convert −0 to 0. + let s12 = 0.0 + s12x; + let m12 = 0.0 + m12x; + + // Area, Karney 2013 §6. + let salp0 = salp1 * cbet1; + let calp0 = calp1.hypot(salp1 * sbet1); + let mut area_s12 = if calp0 != 0.0 && salp0 != 0.0 { + let (ssig1, csig1) = norm2(sbet1, calp1 * cbet1); + let (ssig2, csig2) = norm2(sbet2, calp2 * cbet2); + let k2 = sq(calp0) * self.ep2; + let eps = k2 / (2.0 * (1.0 + (1.0 + k2).sqrt()) + k2); + let a4 = sq(self.a) * calp0 * salp0 * self.e2; + let c4a = c4f(&self.c4x, eps); + let b41 = sin_cos_series(false, ssig1, csig1, &c4a, ORDER); + let b42 = sin_cos_series(false, ssig2, csig2, &c4a, ORDER); + a4 * (b42 - b41) + } else { + // σ1, σ2 are indeterminate on the equator. + 0.0 + }; + if !meridian && somg12 == 2.0 { + (somg12, comg12) = omg12.sin_cos(); + } + let alp12 = if !meridian && comg12 > AREA_MAX_COMG12 && sbet2 - sbet1 < AREA_MAX_DSBET { + // tan(Γ/2) = tan(ω12/2) (tan(β1/2) + tan(β2/2)) / (1 + tan(β1/2) tan(β2/2)). + let domg12 = 1.0 + comg12; + let dbet1 = 1.0 + cbet1; + let dbet2 = 1.0 + cbet2; + 2.0 * (somg12 * (sbet1 * dbet2 + sbet2 * dbet1)) + .atan2(domg12 * (sbet1 * sbet2 + dbet1 * dbet2)) + } else { + let mut salp12 = salp2 * calp1 - calp2 * salp1; + let mut calp12 = calp2 * calp1 + salp2 * salp1; + // Attach the sign to zero so α12 = ±180° comes out right. + if salp12 == 0.0 && calp12 < 0.0 { + salp12 = tiny * calp1; + calp12 = -1.0; + } + salp12.atan2(calp12) + }; + area_s12 += self.c2 * alp12; + area_s12 *= swapp * lonsign * latsign; + area_s12 += 0.0; + + // Undo the canonical transformation. + if swapp < 0.0 { + core::mem::swap(&mut salp1, &mut salp2); + core::mem::swap(&mut calp1, &mut calp2); + core::mem::swap(&mut big_m12, &mut big_m21); + } + salp1 *= swapp * lonsign; + calp1 *= swapp * latsign; + salp2 *= swapp * lonsign; + calp2 *= swapp * latsign; + + Raw { + a12, + s12, + salp1, + calp1, + salp2, + calp2, + m12, + big_m12, + big_m21, + area_s12, + } + } + + /// Solve λ12(α1) = λ12 by Newton's method with a bracketing fallback, + /// Karney 2013 §5. Returns the state of the final evaluation. + fn newton( + &self, + ends: &Ends, + salp1: &mut f64, + calp1: &mut f64, + slam12: f64, + clam12: f64, + ) -> Lambda { + let tiny = tiny(); + let (mut salp1a, mut calp1a, mut salp1b, mut calp1b) = (tiny, 1.0, tiny, -1.0); + let mut tripn = false; + let mut tripb = false; + let mut numit = 0u32; + loop { + let lam = self.lambda12(ends, *salp1, *calp1, slam12, clam12, numit < MAXIT1); + let v = lam.lam12; + let dv = lam.dlam12; + let tol = if tripn { TRIPN_TOL_FACTOR } else { 1.0 } * TOL0; + // The reversed comparison lets NaN escape. + if tripb || v.is_nan() || v.abs() < tol || numit == MAXIT2 { + return lam; + } + // Shrink the bracket. + if v > 0.0 && (numit > MAXIT1 || *calp1 / *salp1 > calp1b / salp1b) { + salp1b = *salp1; + calp1b = *calp1; + } else if v < 0.0 && (numit > MAXIT1 || *calp1 / *salp1 < calp1a / salp1a) { + salp1a = *salp1; + calp1a = *calp1; + } + numit += 1; + if numit - 1 < MAXIT1 && dv > 0.0 { + let dalp1 = -v / dv; + if dalp1.abs() < core::f64::consts::PI { + let (sdalp1, cdalp1) = dalp1.sin_cos(); + let nsalp1 = *salp1 * cdalp1 + *calp1 * sdalp1; + if nsalp1 > 0.0 { + let ncalp1 = *calp1 * cdalp1 - *salp1 * sdalp1; + (*salp1, *calp1) = norm2(nsalp1, ncalp1); + // Slope → 0 in some regimes: test against ε, not √ε. + tripn = v.abs() <= TRIPN_SET_FACTOR * TOL0; + continue; + } + } + } + // dv ≤ 0 or the step left (0, π): bisect the bracket. + (*salp1, *calp1) = norm2((salp1a + salp1b) / 2.0, (calp1a + calp1b) / 2.0); + tripn = false; + tripb = (salp1a - *salp1).abs() + (calp1a - *calp1) < TOLB + || (*salp1 - salp1b).abs() + (*calp1 - calp1b) < TOLB; + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + // Reference values: GeographicLib as bundled in PROJ 9.8.1, via pyproj + // 3.8.0 `Geod(ellps="WGS84").inv`. pyproj returns the back azimuth at + // point 2; the forward azimuth below is that value ± 180°. + + /// Distance tolerance against PROJ, 100 nm. GeographicLib's round-off + /// is about 15 nm. + const DIST_TOL_M: f64 = 1e-7; + + /// Azimuth tolerance against PROJ, degrees. + const ANGLE_TOL_DEG: f64 = 1e-11; + + /// Area tolerance against PROJ, m². + const AREA_TOL_M2: f64 = 1e-2; + + fn wgs84() -> Geodesic { + Geodesic::new(6_378_137.0, 1.0 / 298.257_223_563) + } + + /// (lat1, lon1, lat2, lon2, s12, azi1, azi2). + const INVERSE: &[[f64; 7]] = &[ + [ + 40.6, + -73.8, + 51.6, + -0.5, + 5_551_759.400_318_678_5, + 51.198_882_845_579_83, + 107.821_776_735_514_26, + ], + [ + 0.0, + 0.0, + 0.5, + 179.5, + 19_936_288.578_965_314, + 25.671_872_868_291_88, + 154.327_085_469_941_6, + ], + [ + -30.0, + 0.0, + 29.9, + 179.8, + 19_989_832.827_609_53, + 161.890_524_736_326_97, + 18.090_737_245_739_49, + ], + [ + 10.0, + 20.0, + 10.0, + 20.000_000_1, + 0.010_963_936_534_940_925, + 89.999_999_991_317_6, + 90.000_000_008_682_4, + ], + [0.0, 0.0, 0.0, 90.0, 10_018_754.171_394_622, 90.0, 90.0], + [-90.0, 0.0, 45.0, 30.0, 14_986_910.107_290_467, 30.0, 0.0], + [20.0, 5.0, -40.0, 5.0, 6_641_895.284_522_149, 180.0, 180.0], + [ + 0.0, + 0.0, + 0.0, + 179.9, + 20_003_008.421_509_41, + 9.545_672_694_738_908, + 170.454_327_305_261_1, + ], + [ + 52.0, + 24.0, + -33.9, + 151.2, + 15_388_185.664_646_542, + 85.262_458_179_209_96, + 132.269_847_457_975_7, + ], + ]; + + fn angle_close(got: f64, want: f64, tol: f64) -> bool { + let d = (got - want).rem_euclid(360.0); + d.min(360.0 - d) < tol + } + + #[test] + fn inverse_matches_geographiclib() { + let g = wgs84(); + for row in INVERSE { + let [lat1, lon1, lat2, lon2, s12, azi1, azi2] = *row; + let (s, a1, a2) = g.inverse(lat1, lon1, lat2, lon2); + assert!((s - s12).abs() < DIST_TOL_M, "{row:?}: s12 {s}"); + assert!(angle_close(a1, azi1, ANGLE_TOL_DEG), "{row:?}: azi1 {a1}"); + assert!(angle_close(a2, azi2, ANGLE_TOL_DEG), "{row:?}: azi2 {a2}"); + } + } + + #[test] + fn direct_inverts_inverse() { + let g = wgs84(); + for row in INVERSE { + let [lat1, lon1, lat2, lon2, ..] = *row; + let (s, a1, _) = g.inverse(lat1, lon1, lat2, lon2); + let (la, lo, _) = g.direct(lat1, lon1, a1, s); + assert!((la - lat2).abs() < 1e-9, "{row:?}: {la}"); + assert!( + lat2.abs() == 90.0 || angle_close(lo, lon2, 1e-9), + "{row:?}: {lo}" + ); + } + } + + /// Σ S12 around a ring, reduced modulo the ellipsoid area 4π c² and + /// negated to the counter-clockwise-positive sense, as GeographicLib + /// `geod_polygon_compute` does for a ring that crosses no meridian ±180°. + fn ring_area(g: &Geodesic, ring: &[[f64; 2]]) -> f64 { + let mut sum = 0.0; + for (i, p) in ring.iter().enumerate() { + let q = ring[(i + 1) % ring.len()]; + sum += g.inverse_full(p[0], p[1], q[0], q[1]).area_s12; + } + let total = 4.0 * core::f64::consts::PI * g.c2; + -(sum - (sum / total).round() * total) + } + + /// pyproj `polygon_area_perimeter` of the octant (0,0)–(90,0)–(0,90) + /// and of the 1° square at the origin, (lat, lon) vertices. + #[test] + fn area_matches_geographiclib() { + let g = wgs84(); + let octant = ring_area(&g, &[[0.0, 0.0], [90.0, 0.0], [0.0, 90.0]]); + assert!((octant - -63_758_202_715_511.055).abs() < 1.0, "{octant}"); + let square = ring_area(&g, &[[0.0, 0.0], [0.0, 1.0], [1.0, 1.0], [1.0, 0.0]]); + assert!( + (square - 12_308_778_361.469_452).abs() < AREA_TOL_M2, + "{square}" + ); + } + + #[test] + fn sphere_great_circle() { + let g = Geodesic::new(1.0, 0.0); + let (s, a1, a2) = g.inverse(0.0, 0.0, 0.0, 90.0); + assert!((s - core::f64::consts::FRAC_PI_2).abs() < 1e-15); + assert_eq!((a1, a2), (90.0, 90.0)); + } +} diff --git a/nodedb-crs/src/method/geodesic/line.rs b/nodedb-crs/src/method/geodesic/line.rs new file mode 100644 index 000000000..8e1c61682 --- /dev/null +++ b/nodedb-crs/src/method/geodesic/line.rs @@ -0,0 +1,405 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! The direct geodesic problem: a geodesic line from a point and azimuth, +//! ported from GeographicLib's C library (PROJ 9.8.1 `src/geodesic.c`, +//! `geod_lineinit_int`, `geod_genposition`, `geod_direct`). + +use super::math::{DEGREE, ang_normalize, ang_round, atan2_d, lat_fix, norm2, sin_cos_d, sq}; +use super::model::{Geodesic, tiny}; +use super::series::{Coeffs, ORDER, a1m1f, a2m1f, a3f, c1f, c1pf, c2f, c3f, c4f, sin_cos_series}; + +/// |f| above which the reverted distance series takes one Newton step, +/// Karney 2013 §4. +const REVERTED_SERIES_MAX_F: f64 = 0.01; + +/// Every output of a direct or inverse geodesic solution. Angles in +/// degrees, lengths in the units of the semi-major axis, area in their +/// square. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct GeodesicSolution { + pub lat1: f64, + pub lon1: f64, + pub azi1: f64, + pub lat2: f64, + pub lon2: f64, + pub azi2: f64, + /// Distance s12. + pub s12: f64, + /// Arc length a12 on the auxiliary sphere, degrees. + pub a12: f64, + /// Reduced length m12. + pub m12: f64, + /// Geodesic scale of point 2 relative to point 1. + pub big_m12: f64, + /// Geodesic scale of point 1 relative to point 2. + pub big_m21: f64, + /// Area between the geodesic and the equator, S12. + pub area_s12: f64, +} + +/// A geodesic line starting at a point with a given azimuth. +#[derive(Debug, Clone, PartialEq)] +pub struct GeodesicLine { + lat1: f64, + lon1: f64, + azi1: f64, + a: f64, + f: f64, + b: f64, + c2: f64, + f1: f64, + salp1: f64, + calp1: f64, + dn1: f64, + salp0: f64, + calp0: f64, + ssig1: f64, + csig1: f64, + somg1: f64, + comg1: f64, + k2: f64, + a1m1: f64, + a2m1: f64, + a3c: f64, + b11: f64, + b21: f64, + b31: f64, + a4: f64, + b41: f64, + stau1: f64, + ctau1: f64, + c1a: Coeffs, + c1pa: Coeffs, + c2a: Coeffs, + c3a: Coeffs, + c4a: Coeffs, +} + +impl GeodesicLine { + /// The line from (`lat1`, `lon1`) with azimuth `azi1`, degrees. + pub fn new(g: &Geodesic, lat1: f64, lon1: f64, azi1: f64) -> Self { + let azi1 = ang_normalize(azi1); + // Rounding guards against underflow in salp0. + let (salp1, calp1) = sin_cos_d(ang_round(azi1)); + Self::with_sin_cos(g, lat1, lon1, azi1, salp1, calp1) + } + + /// The line from a point whose azimuth is given by its sine and cosine + /// as well, GeographicLib `geod_lineinit_int` with every capability. + pub(super) fn with_sin_cos( + g: &Geodesic, + lat1: f64, + lon1: f64, + azi1: f64, + salp1: f64, + calp1: f64, + ) -> Self { + let lat1 = lat_fix(lat1); + let (sbet1, cbet1) = sin_cos_d(ang_round(lat1)); + let (sbet1, cbet1) = norm2(sbet1 * g.f1, cbet1); + // cos β1 = +ε at the poles. + let cbet1 = cbet1.max(tiny()); + let dn1 = (1.0 + g.ep2 * sq(sbet1)).sqrt(); + // sin α0 = sin α1 cos β1, α0 in [0, π/2 − |β1|]. + let salp0 = salp1 * cbet1; + let calp0 = calp1.hypot(salp1 * sbet1); + // tan β1 = tan σ1 cos α1 and tan ω1 = sin α0 tan σ1. + let somg1 = salp0 * sbet1; + let c = if sbet1 != 0.0 || calp1 != 0.0 { + cbet1 * calp1 + } else { + 1.0 + }; + let (ssig1, csig1) = norm2(sbet1, c); + let comg1 = c; + let k2 = sq(calp0) * g.ep2; + let eps = k2 / (2.0 * (1.0 + (1.0 + k2).sqrt()) + k2); + + let a1m1 = a1m1f(eps); + let c1a = c1f(eps); + let b11 = sin_cos_series(true, ssig1, csig1, &c1a, ORDER); + let (s, c) = b11.sin_cos(); + // τ1 = σ1 + B11. + let stau1 = ssig1 * c + csig1 * s; + let ctau1 = csig1 * c - ssig1 * s; + let c1pa = c1pf(eps); + let a2m1 = a2m1f(eps); + let c2a = c2f(eps); + let b21 = sin_cos_series(true, ssig1, csig1, &c2a, ORDER); + let c3a = c3f(&g.c3x, eps); + let a3c = -g.f * salp0 * a3f(&g.a3x, eps); + let b31 = sin_cos_series(true, ssig1, csig1, &c3a, ORDER - 1); + let c4a = c4f(&g.c4x, eps); + // a² e² cos α0 sin α0. + let a4 = sq(g.a) * calp0 * salp0 * g.e2; + let b41 = sin_cos_series(false, ssig1, csig1, &c4a, ORDER); + Self { + lat1, + lon1, + azi1, + a: g.a, + f: g.f, + b: g.b, + c2: g.c2, + f1: g.f1, + salp1, + calp1, + dn1, + salp0, + calp0, + ssig1, + csig1, + somg1, + comg1, + k2, + a1m1, + a2m1, + a3c, + b11, + b21, + b31, + a4, + b41, + stau1, + ctau1, + c1a, + c1pa, + c2a, + c3a, + c4a, + } + } + + /// The point at distance `s12` along the line. Longitude reduces to + /// [−180°, 180°]. + pub fn position(&self, s12: f64) -> GeodesicSolution { + self.gen_position(false, s12, false) + } + + /// The point at `s12_a12` along the line: a distance, or an arc length + /// in degrees when `arc_mode`. With `unroll`, the longitude counts + /// whole turns instead of reducing to [−180°, 180°]. + pub fn gen_position(&self, arc_mode: bool, s12_a12: f64, unroll: bool) -> GeodesicSolution { + let mut b12 = 0.0; + let (mut sig12, mut ssig12, mut csig12); + if arc_mode { + sig12 = s12_a12 * DEGREE; + (ssig12, csig12) = sin_cos_d(s12_a12); + } else { + let tau12 = s12_a12 / (self.b * (1.0 + self.a1m1)); + let (s, c) = tau12.sin_cos(); + // τ2 = τ1 + τ12. + b12 = -sin_cos_series( + true, + self.stau1 * c + self.ctau1 * s, + self.ctau1 * c - self.stau1 * s, + &self.c1pa, + ORDER, + ); + sig12 = tau12 - (b12 - self.b11); + (ssig12, csig12) = sig12.sin_cos(); + if self.f.abs() > REVERTED_SERIES_MAX_F { + let ssig2 = self.ssig1 * csig12 + self.csig1 * ssig12; + let csig2 = self.csig1 * csig12 - self.ssig1 * ssig12; + b12 = sin_cos_series(true, ssig2, csig2, &self.c1a, ORDER); + let serr = (1.0 + self.a1m1) * (sig12 + (b12 - self.b11)) - s12_a12 / self.b; + sig12 -= serr / (1.0 + self.k2 * sq(ssig2)).sqrt(); + (ssig12, csig12) = sig12.sin_cos(); + } + } + + // σ2 = σ1 + σ12. + let ssig2 = self.ssig1 * csig12 + self.csig1 * ssig12; + let mut csig2 = self.csig1 * csig12 - self.ssig1 * ssig12; + let dn2 = (1.0 + self.k2 * sq(ssig2)).sqrt(); + if arc_mode || self.f.abs() > REVERTED_SERIES_MAX_F { + b12 = sin_cos_series(true, ssig2, csig2, &self.c1a, ORDER); + } + let ab1 = (1.0 + self.a1m1) * (b12 - self.b11); + // sin β2 = cos α0 sin σ2. + let sbet2 = self.calp0 * ssig2; + let mut cbet2 = self.salp0.hypot(self.calp0 * csig2); + if cbet2 == 0.0 { + // sin α0 = 0 and cos σ2 = 0: break the degeneracy. + cbet2 = tiny(); + csig2 = tiny(); + } + // tan α0 = cos σ2 tan α2. + let salp2 = self.salp0; + let calp2 = self.calp0 * csig2; + + let s12 = if arc_mode { + self.b * ((1.0 + self.a1m1) * sig12 + ab1) + } else { + s12_a12 + }; + + let e = 1.0f64.copysign(self.salp0); + // tan ω2 = sin α0 tan σ2. + let somg2 = self.salp0 * ssig2; + let comg2 = csig2; + let omg12 = if unroll { + e * (sig12 - (ssig2.atan2(csig2) - self.ssig1.atan2(self.csig1)) + + ((e * somg2).atan2(comg2) - (e * self.somg1).atan2(self.comg1))) + } else { + (somg2 * self.comg1 - comg2 * self.somg1).atan2(comg2 * self.comg1 + somg2 * self.somg1) + }; + let lam12 = omg12 + + self.a3c + * (sig12 + (sin_cos_series(true, ssig2, csig2, &self.c3a, ORDER - 1) - self.b31)); + let lon12 = lam12 / DEGREE; + let lon2 = if unroll { + self.lon1 + lon12 + } else { + ang_normalize(ang_normalize(self.lon1) + ang_normalize(lon12)) + }; + let lat2 = atan2_d(sbet2, self.f1 * cbet2); + let azi2 = atan2_d(salp2, calp2); + + let b22 = sin_cos_series(true, ssig2, csig2, &self.c2a, ORDER); + let ab2 = (1.0 + self.a2m1) * (b22 - self.b21); + let j12 = (self.a1m1 - self.a2m1) * sig12 + (ab1 - ab2); + // Parenthesised products keep the cancellation exact for + // coincident points. + let m12 = self.b + * ((dn2 * (self.csig1 * ssig2) - self.dn1 * (self.ssig1 * csig2)) + - self.csig1 * csig2 * j12); + let t = self.k2 * (ssig2 - self.ssig1) * (ssig2 + self.ssig1) / (self.dn1 + dn2); + let big_m12 = csig12 + (t * ssig2 - csig2 * j12) * self.ssig1 / self.dn1; + let big_m21 = csig12 - (t * self.ssig1 - self.csig1 * j12) * ssig2 / dn2; + + let b42 = sin_cos_series(false, ssig2, csig2, &self.c4a, ORDER); + let (salp12, calp12) = if self.calp0 == 0.0 || self.salp0 == 0.0 { + // α12 = α2 − α1, used in atan2 so no need to normalise. + ( + salp2 * self.calp1 - calp2 * self.salp1, + calp2 * self.calp1 + salp2 * self.salp1, + ) + } else { + // tan(α2 − α1) from tan α = tan α0 sec σ, Karney 2013 eq. 65. + let num = if csig12 <= 0.0 { + self.csig1 * (1.0 - csig12) + ssig12 * self.ssig1 + } else { + ssig12 * (self.csig1 * ssig12 / (1.0 + csig12) + self.ssig1) + }; + ( + self.calp0 * self.salp0 * num, + sq(self.salp0) + sq(self.calp0) * self.csig1 * csig2, + ) + }; + let area_s12 = self.c2 * salp12.atan2(calp12) + self.a4 * (b42 - self.b41); + + GeodesicSolution { + lat1: self.lat1, + lon1: self.lon1, + azi1: self.azi1, + lat2, + lon2, + azi2, + s12, + a12: if arc_mode { s12_a12 } else { sig12 / DEGREE }, + m12, + big_m12, + big_m21, + area_s12, + } + } + + /// Semi-major axis of the line's ellipsoid. + pub fn a(&self) -> f64 { + self.a + } +} + +impl Geodesic { + /// Solve the direct problem: from (`lat1`, `lon1`) along azimuth `azi1` + /// for distance `s12`, return (lat2, lon2, azi2). Degrees and the units + /// of the semi-major axis. + pub fn direct(&self, lat1: f64, lon1: f64, azi1: f64, s12: f64) -> (f64, f64, f64) { + let sol = self.direct_full(lat1, lon1, azi1, s12); + (sol.lat2, sol.lon2, sol.azi2) + } + + /// Solve the direct problem with every output. + pub fn direct_full(&self, lat1: f64, lon1: f64, azi1: f64, s12: f64) -> GeodesicSolution { + GeodesicLine::new(self, lat1, lon1, azi1).position(s12) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Latitude, longitude and azimuth tolerance against PROJ, degrees. + const ANGLE_TOL_DEG: f64 = 1e-11; + + /// (lat1, lon1, azi1, s12, lat2, lon2, azi2) from GeographicLib as + /// bundled in PROJ 9.8.1, via pyproj 3.8.0 `Geod(ellps="WGS84").fwd`. + const DIRECT: &[[f64; 7]] = &[ + [ + 40.6, + -73.8, + 51.4, + 5_000_000.0, + 52.724_376_645_671_35, + -8.361_446_565_212_93, + 101.745_597_701_112_46, + ], + [ + -30.0, + 0.0, + 179.0, + 19_000_000.0, + 20.939_069_684_509_27, + 179.823_126_134_612_55, + 0.927_634_299_928_172_4, + ], + [ + 89.0, + 10.0, + -120.0, + 500_000.0, + 84.948_710_822_340_83, + -40.149_256_704_217_166, + -170.115_400_931_919_4, + ], + [ + 0.0, + 0.0, + 90.0, + 20_000_000.0, + 0.0, + 179.663_056_823_904_3, + 90.0, + ], + ]; + + fn angle_close(got: f64, want: f64) -> bool { + let d = (got - want).rem_euclid(360.0); + d.min(360.0 - d) < ANGLE_TOL_DEG + } + + #[test] + fn direct_matches_geographiclib() { + let g = Geodesic::new(6_378_137.0, 1.0 / 298.257_223_563); + for row in DIRECT { + let [lat1, lon1, azi1, s12, lat2, lon2, azi2] = *row; + let (la, lo, az) = g.direct(lat1, lon1, azi1, s12); + assert!((la - lat2).abs() < ANGLE_TOL_DEG, "{row:?}: lat2 {la}"); + assert!(angle_close(lo, lon2), "{row:?}: lon2 {lo}"); + assert!(angle_close(az, azi2), "{row:?}: azi2 {az}"); + } + } + + #[test] + fn arc_mode_and_unroll() { + let g = Geodesic::new(1.0, 0.0); + let line = GeodesicLine::new(&g, 0.0, 170.0, 90.0); + let p = line.gen_position(true, 30.0, true); + assert!((p.lon2 - 200.0).abs() < 1e-12 && (p.s12 - 30f64.to_radians()).abs() < 1e-15); + let p = line.gen_position(true, 30.0, false); + assert!((p.lon2 + 160.0).abs() < 1e-12); + let (lat, lon, _) = g.direct(0.0, 0.0, 0.0, core::f64::consts::FRAC_PI_4); + assert!((lat - 45.0).abs() < 1e-12 && lon == 0.0); + } +} diff --git a/nodedb-crs/src/method/geodesic/math.rs b/nodedb-crs/src/method/geodesic/math.rs new file mode 100644 index 000000000..88ac6934a --- /dev/null +++ b/nodedb-crs/src/method/geodesic/math.rs @@ -0,0 +1,211 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Angle and summation helpers for the geodesic solver, ported from +//! GeographicLib's C library (PROJ 9.8.1 `src/geodesic.c`). +//! +//! Angles in degrees reduce exactly to [−45°, 45°] before conversion to +//! radians, so sin 30° and similar values come out exact. + +/// A quarter turn in degrees. +pub(super) const QD: f64 = 90.0; + +/// A half turn in degrees. +pub(super) const HD: f64 = 180.0; + +/// A full turn in degrees. +pub(super) const TD: f64 = 360.0; + +/// Radians per degree. +pub(super) const DEGREE: f64 = core::f64::consts::PI / HD; + +/// Angles below this many degrees round to it in [`ang_round`]. +const ANG_ROUND_Z: f64 = 1.0 / 16.0; + +/// x². +pub(super) fn sq(x: f64) -> f64 { + x * x +} + +/// Error-free sum: returns (s, t) with s = round(u + v) and u + v = s + t. +pub(super) fn sum_x(u: f64, v: f64) -> (f64, f64) { + let s = u + v; + let mut up = s - v; + let mut vpp = s - up; + up -= u; + vpp -= v; + let t = if s != 0.0 { 0.0 - (up + vpp) } else { s }; + (s, t) +} + +/// Σ p[i] x^(N−i) by Horner's method, highest power first, as +/// GeographicLib `polyvalx`. +pub(super) fn polyval(p: &[f64], x: f64) -> f64 { + match p.split_first() { + None => 0.0, + Some((first, rest)) => rest.iter().fold(*first, |y, c| y * x + c), + } +} + +/// Scale (sin x, cos x) to unit length. +pub(super) fn norm2(sinx: f64, cosx: f64) -> (f64, f64) { + let r = sinx.hypot(cosx); + (sinx / r, cosx / r) +} + +/// IEEE remainder of `x` by a positive `y`, with the two low bits of the +/// rounded quotient as C `remquo` returns them, reduced into 0..4. +/// +/// Every step is exact: `%` is exact, and each subtraction pairs values +/// within a factor of two of each other. +pub(super) fn remquo(x: f64, y: f64) -> (f64, u32) { + let mut m = x.abs() % (4.0 * y); + let mut k = 0u32; + if m >= 2.0 * y { + m -= 2.0 * y; + k = 2; + } + if m >= y { + m -= y; + k += 1; + } + // Round the quotient to nearest, ties to even. + if m > y / 2.0 || (m == y / 2.0 && k % 2 == 1) { + m -= y; + k += 1; + } + k %= 4; + if x.is_sign_negative() { + (-m, (4 - k) % 4) + } else { + (m, k) + } +} + +/// IEEE remainder of `x` by a positive `y`. +pub(super) fn remainder(x: f64, y: f64) -> f64 { + remquo(x, y).0 +} + +/// Reduce an angle to [−180°, 180°], keeping the sign of ±180°. +pub(super) fn ang_normalize(x: f64) -> f64 { + let y = remainder(x, TD); + if y.abs() == HD { HD.copysign(x) } else { y } +} + +/// NaN for a latitude beyond ±90°. +pub(super) fn lat_fix(x: f64) -> f64 { + if x.abs() > QD { f64::NAN } else { x } +} + +/// y − x reduced to [−180°, 180°], with its rounding error. +pub(super) fn ang_diff(x: f64, y: f64) -> (f64, f64) { + let (d, t) = sum_x(remainder(-x, TD), remainder(y, TD)); + let (mut d, t) = sum_x(remainder(d, TD), t); + if d == 0.0 || d.abs() == HD { + d = d.copysign(if t == 0.0 { y - x } else { -t }); + } + (d, t) +} + +/// Round tiny angles to multiples of 1/16 · 2⁻⁵³ so an angle of +/// −1e−300 behaves as −0. +pub(super) fn ang_round(x: f64) -> f64 { + let y = x.abs(); + let w = ANG_ROUND_Z - y; + let y = if w > 0.0 { ANG_ROUND_Z - w } else { y }; + y.copysign(x) +} + +/// Map a reduced angle r in radians with quadrant q to (sin, cos). +fn quadrant_sin_cos(r: f64, q: u32) -> (f64, f64) { + let (s, c) = r.sin_cos(); + match q { + 0 => (s, c), + 1 => (c, -s), + 2 => (-s, -c), + _ => (-c, s), + } +} + +/// Fix the signs of zero results as C99 F.10.1.12–13 specifies. +fn signed_zero(sinx: f64, cosx: f64, x: f64) -> (f64, f64) { + let cosx = cosx + 0.0; + let sinx = if sinx == 0.0 { sinx.copysign(x) } else { sinx }; + (sinx, cosx) +} + +/// sin and cos of `x` degrees. +pub(super) fn sin_cos_d(x: f64) -> (f64, f64) { + let (r, q) = remquo(x, QD); + let (s, c) = quadrant_sin_cos(r * DEGREE, q); + signed_zero(s, c, x) +} + +/// sin and cos of `x + t` degrees, where `t` is a small correction. +pub(super) fn sin_cos_de(x: f64, t: f64) -> (f64, f64) { + let (r, q) = remquo(x, QD); + let (s, c) = quadrant_sin_cos(ang_round(r + t) * DEGREE, q); + signed_zero(s, c, x) +} + +/// atan2(y, x) in degrees, computed in the octant [−45°, 45°] first. +pub(super) fn atan2_d(y: f64, x: f64) -> f64 { + let (mut x, mut y) = (x, y); + let mut q = 0; + if y.abs() > x.abs() { + core::mem::swap(&mut x, &mut y); + q = 2; + } + if x.is_sign_negative() { + x = -x; + q += 1; + } + let ang = y.atan2(x) / DEGREE; + match q { + 1 => HD.copysign(y) - ang, + 2 => QD - ang, + 3 => -QD + ang, + _ => ang, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn remquo_matches_c() { + assert_eq!(remquo(135.0, 90.0), (-45.0, 2)); + assert_eq!(remquo(45.0, 90.0), (45.0, 0)); + assert_eq!(remquo(-135.0, 90.0), (45.0, 2)); + assert_eq!(remquo(100.0, 90.0), (10.0, 1)); + assert_eq!(remquo(-100.0, 90.0), (-10.0, 3)); + assert_eq!(remquo(-0.0, 90.0).0.to_bits(), (-0.0f64).to_bits()); + assert_eq!(remainder(540.0, 360.0), -180.0); + assert_eq!(remainder(-540.0, 360.0), 180.0); + assert_eq!(remainder(190.0, 360.0), -170.0); + } + + #[test] + fn exact_special_angles() { + assert_eq!(sin_cos_d(30.0).0, (30.0 * DEGREE).sin()); + assert_eq!( + sin_cos_d(120.0), + ((30.0 * DEGREE).cos(), -(30.0 * DEGREE).sin()) + ); + assert_eq!(sin_cos_d(90.0), (1.0, 0.0)); + assert_eq!(sin_cos_d(-180.0).1, -1.0); + assert_eq!(atan2_d(1.0, 1.0), 45.0); + assert_eq!(atan2_d(0.0, -1.0), 180.0); + assert_eq!(atan2_d(-0.0, -1.0), -180.0); + assert_eq!(ang_normalize(-180.0), -180.0); + assert_eq!(ang_normalize(540.0), 180.0); + } + + #[test] + fn ang_diff_is_exact_near_half_turn() { + assert_eq!(ang_diff(0.0, 180.0).0, 180.0); + assert_eq!(ang_diff(10.0, -170.0).0, -180.0); + assert_eq!(ang_diff(-170.0, 170.0).0, -20.0); + } +} diff --git a/nodedb-crs/src/method/geodesic/mod.rs b/nodedb-crs/src/method/geodesic/mod.rs new file mode 100644 index 000000000..bb6b4d87f --- /dev/null +++ b/nodedb-crs/src/method/geodesic/mod.rs @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Geodesics on an ellipsoid of revolution: the direct and inverse +//! problems by C. F. F. Karney, "Algorithms for geodesics", J. Geodesy 87 +//! (2013), ported from GeographicLib's C library as bundled in PROJ 9.8.1 +//! (`src/geodesic.c`). Accurate to about 15 nm for |f| ≤ 1/50. + +pub mod line; +pub mod model; +pub mod polygon; + +mod inverse; +mod math; +mod series; +mod start; + +pub use line::{GeodesicLine, GeodesicSolution}; +pub use model::Geodesic; +pub use polygon::PolygonMeasure; diff --git a/nodedb-crs/src/method/geodesic/model.rs b/nodedb-crs/src/method/geodesic/model.rs new file mode 100644 index 000000000..ffb172fba --- /dev/null +++ b/nodedb-crs/src/method/geodesic/model.rs @@ -0,0 +1,219 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! The geodesic model of one ellipsoid and the shared length integrals, +//! ported from GeographicLib's C library (PROJ 9.8.1 `src/geodesic.c`, +//! `geod_init` and `Lengths`). + +use super::math::sq; +use super::series::{ + Coeffs, NC3X, NC4X, ORDER, a1m1f, a2m1f, a3_coeff, c1f, c2f, c3_coeff, c4_coeff, sin_cos_series, +}; + +/// Newton iterations before the inverse solver falls back to bisection. +pub(super) const MAXIT1: u32 = 20; + +/// Total iteration cap of the inverse solver: MAXIT1 + mantissa digits + 10. +pub(super) const MAXIT2: u32 = MAXIT1 + f64::MANTISSA_DIGITS + 10; + +/// √(smallest normal f64), keeps cos β above zero at the poles. +pub(super) fn tiny() -> f64 { + f64::MIN_POSITIVE.sqrt() +} + +/// Machine epsilon, GeographicLib `tol0`. +pub(super) const TOL0: f64 = f64::EPSILON; + +/// GeographicLib `tol1` = 200 · tol0. +pub(super) const TOL1: f64 = 200.0 * TOL0; + +/// GeographicLib `tol2` = √tol0. +pub(super) fn tol2() -> f64 { + TOL0.sqrt() +} + +/// Bisection interval check, GeographicLib `tolb`. +pub(super) const TOLB: f64 = TOL0; + +/// GeographicLib `xthresh` = 1000 · tol2. +pub(super) fn xthresh() -> f64 { + 1000.0 * tol2() +} + +/// Safety factor in the "really short line" threshold `etol2`. +const ETOL2_SAFETY: f64 = 0.1; + +/// Floor on |f| in `etol2`, keeps it bounded for near-spheres. +const ETOL2_MIN_F: f64 = 0.001; + +/// An ellipsoid prepared for geodesic calculations (Karney 2013). +#[derive(Debug, Clone, PartialEq)] +pub struct Geodesic { + /// Semi-major axis. + pub(super) a: f64, + /// Flattening. + pub(super) f: f64, + /// 1 − f. + pub(super) f1: f64, + /// e². + pub(super) e2: f64, + /// e′² = e² / (1 − e²). + pub(super) ep2: f64, + /// Third flattening. + pub(super) n: f64, + /// Semi-minor axis. + pub(super) b: f64, + /// Authalic radius squared. + pub(super) c2: f64, + /// σ12 threshold for "really short" lines. + pub(super) etol2: f64, + pub(super) a3x: [f64; ORDER], + pub(super) c3x: [f64; NC3X], + pub(super) c4x: [f64; NC4X], +} + +/// The distance and reduced-length integrals of [`Geodesic::lengths`]. +#[derive(Debug, Clone, Copy, Default)] +pub(super) struct Lengths { + /// s12 / b. + pub(super) s12b: f64, + /// m12 / b. + pub(super) m12b: f64, + /// Secular coefficient of the reduced length. + pub(super) m0: f64, + /// Geodesic scale M12. + pub(super) big_m12: f64, + /// Geodesic scale M21. + pub(super) big_m21: f64, +} + +/// The auxiliary-sphere state that [`Geodesic::lengths`] integrates over. +#[derive(Debug, Clone, Copy)] +pub(super) struct Arc { + pub(super) eps: f64, + pub(super) sig12: f64, + pub(super) ssig1: f64, + pub(super) csig1: f64, + pub(super) dn1: f64, + pub(super) ssig2: f64, + pub(super) csig2: f64, + pub(super) dn2: f64, + pub(super) cbet1: f64, + pub(super) cbet2: f64, +} + +impl Geodesic { + /// The geodesic model of the ellipsoid with semi-major axis `a` and + /// flattening `f` (0 for a sphere, negative for a prolate spheroid). + pub fn new(a: f64, f: f64) -> Self { + let f1 = 1.0 - f; + let e2 = f * (2.0 - f); + let ep2 = e2 / sq(f1); + let n = f / (2.0 - f); + let b = a * f1; + let c2 = (sq(a) + + sq(b) + * if e2 == 0.0 { + 1.0 + } else { + (if e2 > 0.0 { + e2.sqrt().atanh() + } else { + (-e2).sqrt().atan() + }) / e2.abs().sqrt() + }) + / 2.0; + let etol2 = ETOL2_SAFETY * tol2() + / (f.abs().max(ETOL2_MIN_F) * (1.0 - f / 2.0).min(1.0) / 2.0).sqrt(); + Self { + a, + f, + f1, + e2, + ep2, + n, + b, + c2, + etol2, + a3x: a3_coeff(n), + c3x: c3_coeff(n), + c4x: c4_coeff(n), + } + } + + /// Semi-major axis. + pub fn a(&self) -> f64 { + self.a + } + + /// Flattening. + pub fn f(&self) -> f64 { + self.f + } + + /// Distance s12/b, reduced length m12/b, m0 and the geodesic scales + /// along an arc, GeographicLib `Lengths`. Without `with_distance`, + /// `s12b` stays 0 and J12 comes from the combined C1/C2 series, as + /// GeographicLib does when the distance is not requested. + pub(super) fn lengths(&self, arc: &Arc, with_distance: bool) -> Lengths { + let Arc { + eps, + sig12, + ssig1, + csig1, + dn1, + ssig2, + csig2, + dn2, + cbet1, + cbet2, + } = *arc; + let ca: Coeffs = c1f(eps); + let mut cb: Coeffs = c2f(eps); + let a1m1 = a1m1f(eps); + let a2m1 = a2m1f(eps); + let m0 = a1m1 - a2m1; + let a1 = 1.0 + a1m1; + let a2 = 1.0 + a2m1; + let (s12b, j12) = if with_distance { + let b1 = sin_cos_series(true, ssig2, csig2, &ca, ORDER) + - sin_cos_series(true, ssig1, csig1, &ca, ORDER); + let b2 = sin_cos_series(true, ssig2, csig2, &cb, ORDER) + - sin_cos_series(true, ssig1, csig1, &cb, ORDER); + (a1 * (sig12 + b1), m0 * sig12 + (a1 * b1 - a2 * b2)) + } else { + for (c, a) in cb.iter_mut().zip(ca.iter()).skip(1) { + *c = a1 * a - a2 * *c; + } + let j12 = m0 * sig12 + + (sin_cos_series(true, ssig2, csig2, &cb, ORDER) + - sin_cos_series(true, ssig1, csig1, &cb, ORDER)); + (0.0, j12) + }; + // Parenthesised products keep the cancellation exact for + // coincident points. + let m12b = dn2 * (csig1 * ssig2) - dn1 * (ssig1 * csig2) - csig1 * csig2 * j12; + let csig12 = csig1 * csig2 + ssig1 * ssig2; + let t = self.ep2 * (cbet1 - cbet2) * (cbet1 + cbet2) / (dn1 + dn2); + Lengths { + s12b, + m12b, + m0, + big_m12: csig12 + (t * ssig2 - csig2 * j12) * ssig1 / dn1, + big_m21: csig12 - (t * ssig1 - csig1 * j12) * ssig2 / dn2, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn wgs84_constants() { + let g = Geodesic::new(6_378_137.0, 1.0 / 298.257_223_563); + assert!((g.b - 6_356_752.314_245).abs() < 1e-6); + // Authalic radius of WGS 84, 6 371 007.181 m. + assert!((g.c2.sqrt() - 6_371_007.180_9).abs() < 1e-3); + assert_eq!(MAXIT2, 83); + } +} diff --git a/nodedb-crs/src/method/geodesic/polygon.rs b/nodedb-crs/src/method/geodesic/polygon.rs new file mode 100644 index 000000000..baa2de34a --- /dev/null +++ b/nodedb-crs/src/method/geodesic/polygon.rs @@ -0,0 +1,177 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Area and perimeter of a geodesic polygon, ported from GeographicLib's C +//! library (PROJ 9.8.1 `src/geodesic.c`, `geod_polygon_*`, `transit`, +//! `areareduceA`). Each edge is a geodesic. The area is exact to the +//! precision of the inverse solution for polygons of any size. + +use super::math::{ang_diff, ang_normalize, remainder, sum_x}; +use super::model::Geodesic; + +/// A sum carried with its rounding error (GeographicLib `accadd`). +#[derive(Debug, Clone, Copy, Default)] +struct Accumulator { + s: f64, + t: f64, +} + +impl Accumulator { + fn add(&mut self, y: f64) { + let (z, u) = sum_x(y, self.t); + let (s, t) = sum_x(z, self.s); + self.s = s; + self.t = t; + if self.s == 0.0 { + self.s = u; + } else { + self.t += u; + } + } + + fn remainder(&mut self, y: f64) { + self.s = remainder(self.s, y); + self.add(0.0); + } + + fn negate(&mut self) { + self.s = -self.s; + self.t = -self.t; + } + + fn value(&self) -> f64 { + self.s + self.t + } +} + +/// 1 or −1 when the edge from `lon1` to `lon2` crosses the prime meridian +/// eastward or westward, else 0. +fn transit(lon1: f64, lon2: f64) -> i32 { + let (lon12, _) = ang_diff(lon1, lon2); + let lon1 = ang_normalize(lon1); + let lon2 = ang_normalize(lon2); + if lon12 > 0.0 && ((lon1 < 0.0 && lon2 >= 0.0) || (lon1 > 0.0 && lon2 == 0.0)) { + 1 + } else if lon12 < 0.0 && lon1 >= 0.0 && lon2 < 0.0 { + -1 + } else { + 0 + } +} + +/// The area and perimeter of a closed geodesic polygon. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct PolygonMeasure { + /// Signed area in the square of the semi-major axis units, positive + /// for counter-clockwise traversal, in (−area0/2, area0/2]. + pub area: f64, + /// Perimeter in the units of the semi-major axis. + pub perimeter: f64, +} + +impl Geodesic { + /// Total area of the ellipsoid, 4π c². + pub fn ellipsoid_area(&self) -> f64 { + 4.0 * core::f64::consts::PI * self.c2 + } + + /// Area and perimeter of the polygon through `vertices` (latitude, + /// longitude degrees), closed back to the first vertex. A repeated + /// closing vertex adds a zero-length edge and changes nothing. Fewer + /// than three distinct vertices give zero area. + pub fn polygon_measure(&self, vertices: &[(f64, f64)]) -> PolygonMeasure { + if vertices.len() < 2 { + return PolygonMeasure { + area: 0.0, + perimeter: 0.0, + }; + } + let mut perimeter = Accumulator::default(); + let mut area = Accumulator::default(); + let mut crossings = 0i32; + let n = vertices.len(); + for i in 0..n { + let (lat1, lon1) = vertices[i]; + let (lat2, lon2) = vertices[(i + 1) % n]; + let sol = self.inverse_full(lat1, lon1, lat2, lon2); + perimeter.add(sol.s12); + area.add(sol.area_s12); + crossings += transit(lon1, lon2); + } + let area0 = self.ellipsoid_area(); + area.remainder(area0); + if crossings & 1 != 0 { + area.add(if area.s < 0.0 { 1.0 } else { -1.0 } * area0 / 2.0); + } + // The sum runs clockwise-positive. Report counter-clockwise + // positive, then centre the value on zero. + area.negate(); + if area.s > area0 / 2.0 { + area.add(-area0); + } else if area.s <= -area0 / 2.0 { + area.add(area0); + } + PolygonMeasure { + area: area.value(), + perimeter: perimeter.value(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const WGS84_A: f64 = 6_378_137.0; + const WGS84_F: f64 = 1.0 / 298.257_223_563; + + fn wgs84() -> Geodesic { + Geodesic::new(WGS84_A, WGS84_F) + } + + /// GeographicLib Planimeter reference: the polygon (0,0) (0,1) (1,1) + /// (1,0) degrees on WGS 84 has area 12308778361.469 m² and perimeter + /// 443770.917248 m. + #[test] + fn one_degree_square_matches_planimeter() { + let g = wgs84(); + let m = g.polygon_measure(&[(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0)]); + assert!( + (m.area.abs() - 12_308_778_361.469).abs() < 1.0, + "{}", + m.area + ); + assert!( + (m.perimeter - 443_770.917_248).abs() < 1e-3, + "{}", + m.perimeter + ); + } + + /// Reversing the traversal flips the sign only. + #[test] + fn orientation_sets_the_sign() { + let g = wgs84(); + let ccw = [(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0)]; + let mut cw = ccw; + cw.reverse(); + let a = g.polygon_measure(&ccw).area; + let b = g.polygon_measure(&cw).area; + assert!((a + b).abs() < 1e-3, "{a} {b}"); + } + + /// A polygon across the antimeridian measures its true extent. + #[test] + fn antimeridian_polygon() { + let g = wgs84(); + let across = g.polygon_measure(&[(0.0, 179.5), (0.0, -179.5), (1.0, -179.5), (1.0, 179.5)]); + let plain = g.polygon_measure(&[(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0)]); + assert!((across.area.abs() - plain.area.abs()).abs() < 1.0); + } + + #[test] + fn transit_detects_prime_meridian_crossings() { + assert_eq!(transit(-1.0, 1.0), 1); + assert_eq!(transit(1.0, -1.0), -1); + assert_eq!(transit(10.0, 20.0), 0); + } +} diff --git a/nodedb-crs/src/method/geodesic/series.rs b/nodedb-crs/src/method/geodesic/series.rs new file mode 100644 index 000000000..1f40cc50f --- /dev/null +++ b/nodedb-crs/src/method/geodesic/series.rs @@ -0,0 +1,255 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Series coefficients of the geodesic integrals to order 6, ported from +//! GeographicLib's C library (PROJ 9.8.1 `src/geodesic.c`). +//! +//! C. F. F. Karney, "Algorithms for geodesics", J. Geodesy 87 (2013): +//! A1, C1 (eqs. 17–18), C1′ (eq. 21), A2, C2 (eqs. 42–43), A3, C3 +//! (eqs. 24–25) and C4 (eq. 64). Each table stores a polynomial's +//! coefficients, highest power first, followed by its common denominator. + +use super::math::{polyval, sq}; + +/// Series order, GeographicLib `GEOGRAPHICLIB_GEODESIC_ORDER`. +pub(super) const ORDER: usize = 6; + +/// Length of a Fourier coefficient array, index 0 to [`ORDER`]. +pub(super) const NC: usize = ORDER + 1; + +/// Number of stored C3 coefficients, nC3 (nC3 − 1) / 2. +pub(super) const NC3X: usize = ORDER * (ORDER - 1) / 2; + +/// Number of stored C4 coefficients, nC4 (nC4 + 1) / 2. +pub(super) const NC4X: usize = ORDER * (ORDER + 1) / 2; + +/// A Fourier coefficient array. Index 0 is unused by the sine series. +pub(super) type Coeffs = [f64; NC]; + +/// (1 − ε) A1 − 1 as a polynomial in ε². +const A1M1_COEFF: [f64; 5] = [1.0, 4.0, 64.0, 0.0, 256.0]; + +/// C1[l] / ε^l as polynomials in ε². +const C1_COEFF: [f64; 18] = [ + -1.0, 6.0, -16.0, 32.0, -9.0, 64.0, -128.0, 2048.0, 9.0, -16.0, 768.0, 3.0, -5.0, 512.0, -7.0, + 1280.0, -7.0, 2048.0, +]; + +/// C1′[l] / ε^l as polynomials in ε². +const C1P_COEFF: [f64; 18] = [ + 205.0, -432.0, 768.0, 1536.0, 4005.0, -4736.0, 3840.0, 12288.0, -225.0, 116.0, 384.0, -7173.0, + 2695.0, 7680.0, 3467.0, 7680.0, 38081.0, 61440.0, +]; + +/// (1 + ε) A2 − 1 as a polynomial in ε². +const A2M1_COEFF: [f64; 5] = [-11.0, -28.0, -192.0, 0.0, 256.0]; + +/// C2[l] / ε^l as polynomials in ε². +const C2_COEFF: [f64; 18] = [ + 1.0, 2.0, 16.0, 32.0, 35.0, 64.0, 384.0, 2048.0, 15.0, 80.0, 768.0, 7.0, 35.0, 512.0, 63.0, + 1280.0, 77.0, 2048.0, +]; + +/// A3 coefficients of ε^5 … ε^0 as polynomials in n. +const A3_COEFF: [f64; 18] = [ + -3.0, 128.0, -2.0, -3.0, 64.0, -1.0, -3.0, -1.0, 16.0, 3.0, -1.0, -2.0, 8.0, 1.0, -1.0, 2.0, + 1.0, 1.0, +]; + +/// C3[l] coefficients of ε^j as polynomials in n. +const C3_COEFF: [f64; 45] = [ + 3.0, 128.0, 2.0, 5.0, 128.0, -1.0, 3.0, 3.0, 64.0, -1.0, 0.0, 1.0, 8.0, -1.0, 1.0, 4.0, 5.0, + 256.0, 1.0, 3.0, 128.0, -3.0, -2.0, 3.0, 64.0, 1.0, -3.0, 2.0, 32.0, 7.0, 512.0, -10.0, 9.0, + 384.0, 5.0, -9.0, 5.0, 192.0, 7.0, 512.0, -14.0, 7.0, 512.0, 21.0, 2560.0, +]; + +/// C4[l] coefficients of ε^j as polynomials in n. +const C4_COEFF: [f64; 77] = [ + 97.0, 15015.0, 1088.0, 156.0, 45045.0, -224.0, -4784.0, 1573.0, 45045.0, -10656.0, 14144.0, + -4576.0, -858.0, 45045.0, 64.0, 624.0, -4576.0, 6864.0, -3003.0, 15015.0, 100.0, 208.0, 572.0, + 3432.0, -12012.0, 30030.0, 45045.0, 1.0, 9009.0, -2944.0, 468.0, 135_135.0, 5792.0, 1040.0, + -1287.0, 135_135.0, 5952.0, -11648.0, 9152.0, -2574.0, 135_135.0, -64.0, -624.0, 4576.0, + -6864.0, 3003.0, 135_135.0, 8.0, 10725.0, 1856.0, -936.0, 225_225.0, -8448.0, 4992.0, -1144.0, + 225_225.0, -1440.0, 4160.0, -4576.0, 1716.0, 225_225.0, -136.0, 63063.0, 1024.0, -208.0, + 105_105.0, 3584.0, -3328.0, 1144.0, 315_315.0, -128.0, 135_135.0, -2560.0, 832.0, 405_405.0, + 128.0, 99099.0, +]; + +/// Evaluate `table[o..=o+m]` at `x` and divide by `table[o+m+1]`. +fn ratio(table: &[f64], o: usize, m: usize, x: f64) -> f64 { + polyval(&table[o..=o + m], x) / table[o + m + 1] +} + +/// Fill c[1..=ORDER] with ε^l · P_l(ε²) / d_l from a C1/C1′/C2 table. +fn fourier_in_eps2(table: &[f64; 18], eps: f64) -> Coeffs { + let eps2 = sq(eps); + let mut c = [0.0; NC]; + let mut d = eps; + let mut o = 0; + for (l, slot) in c.iter_mut().enumerate().skip(1) { + let m = (ORDER - l) / 2; + *slot = d * ratio(table, o, m, eps2); + o += m + 2; + d *= eps; + } + c +} + +/// A1 − 1, Karney 2013 eq. 17. +pub(super) fn a1m1f(eps: f64) -> f64 { + let m = ORDER / 2; + let t = ratio(&A1M1_COEFF, 0, m, sq(eps)); + (t + eps) / (1.0 - eps) +} + +/// C1[l], Karney 2013 eq. 18. +pub(super) fn c1f(eps: f64) -> Coeffs { + fourier_in_eps2(&C1_COEFF, eps) +} + +/// C1′[l], Karney 2013 eq. 21. +pub(super) fn c1pf(eps: f64) -> Coeffs { + fourier_in_eps2(&C1P_COEFF, eps) +} + +/// A2 − 1, Karney 2013 eq. 42. +pub(super) fn a2m1f(eps: f64) -> f64 { + let m = ORDER / 2; + let t = ratio(&A2M1_COEFF, 0, m, sq(eps)); + (t - eps) / (1.0 + eps) +} + +/// C2[l], Karney 2013 eq. 43. +pub(super) fn c2f(eps: f64) -> Coeffs { + fourier_in_eps2(&C2_COEFF, eps) +} + +/// The n-dependent A3 coefficients, highest power of ε first. +pub(super) fn a3_coeff(n: f64) -> [f64; ORDER] { + let mut out = [0.0; ORDER]; + let mut o = 0; + for (k, slot) in out.iter_mut().enumerate() { + let j = ORDER - 1 - k; + let m = (ORDER - j - 1).min(j); + *slot = ratio(&A3_COEFF, o, m, n); + o += m + 2; + } + out +} + +/// The n-dependent C3 coefficients. +pub(super) fn c3_coeff(n: f64) -> [f64; NC3X] { + let mut out = [0.0; NC3X]; + let (mut o, mut k) = (0, 0); + for l in 1..ORDER { + for j in (l..ORDER).rev() { + let m = (ORDER - j - 1).min(j); + out[k] = ratio(&C3_COEFF, o, m, n); + k += 1; + o += m + 2; + } + } + out +} + +/// The n-dependent C4 coefficients. +pub(super) fn c4_coeff(n: f64) -> [f64; NC4X] { + let mut out = [0.0; NC4X]; + let (mut o, mut k) = (0, 0); + for l in 0..ORDER { + for j in (l..ORDER).rev() { + let m = ORDER - j - 1; + out[k] = ratio(&C4_COEFF, o, m, n); + k += 1; + o += m + 2; + } + } + out +} + +/// A3(ε) from the prepared coefficients. +pub(super) fn a3f(a3x: &[f64; ORDER], eps: f64) -> f64 { + polyval(a3x, eps) +} + +/// C3[1..ORDER) at ε from the prepared coefficients. +pub(super) fn c3f(c3x: &[f64; NC3X], eps: f64) -> Coeffs { + let mut c = [0.0; NC]; + let mut mult = 1.0; + let mut o = 0; + for (l, slot) in c.iter_mut().enumerate().take(ORDER).skip(1) { + let m = ORDER - l - 1; + mult *= eps; + *slot = mult * polyval(&c3x[o..=o + m], eps); + o += m + 1; + } + c +} + +/// C4[0..ORDER) at ε from the prepared coefficients. +pub(super) fn c4f(c4x: &[f64; NC4X], eps: f64) -> Coeffs { + let mut c = [0.0; NC]; + let mut mult = 1.0; + let mut o = 0; + for (l, slot) in c.iter_mut().enumerate().take(ORDER) { + let m = ORDER - l - 1; + *slot = mult * polyval(&c4x[o..=o + m], eps); + o += m + 1; + mult *= eps; + } + c +} + +/// Clenshaw sum: Σ c[i] sin(2i x), i = 1..=n, when `sinp`, else +/// Σ c[i] cos((2i + 1) x), i = 0..n. +pub(super) fn sin_cos_series(sinp: bool, sinx: f64, cosx: f64, c: &Coeffs, n: usize) -> f64 { + let mut idx = n + usize::from(sinp); + let ar = 2.0 * (cosx - sinx) * (cosx + sinx); + let mut y0 = if n % 2 == 1 { + idx -= 1; + c[idx] + } else { + 0.0 + }; + let mut y1 = 0.0; + for _ in 0..n / 2 { + idx -= 1; + y1 = ar * y0 - y1 + c[idx]; + idx -= 1; + y0 = ar * y1 - y0 + c[idx]; + } + if sinp { + 2.0 * sinx * cosx * y0 + } else { + cosx * (y0 - y1) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn table_lengths_match_loops() { + let n = 0.001_679_220_386_383_705; + assert_eq!(a3_coeff(n)[ORDER - 1], 1.0); + assert!((a3_coeff(n)[ORDER - 2] - (n - 1.0) / 2.0).abs() < 1e-17); + let _ = c3_coeff(n); + let _ = c4_coeff(n); + } + + #[test] + fn zero_eps_gives_zero_series() { + assert_eq!(a1m1f(0.0), 0.0); + assert_eq!(a2m1f(0.0), 0.0); + assert!(c1f(0.0).iter().all(|c| *c == 0.0)); + } + + #[test] + fn sine_series_single_term() { + let mut c = [0.0; NC]; + c[1] = 1.0; + let x: f64 = 0.3; + let got = sin_cos_series(true, x.sin(), x.cos(), &c, ORDER); + assert!((got - (2.0 * x).sin()).abs() < 1e-15); + } +} diff --git a/nodedb-crs/src/method/geodesic/start.rs b/nodedb-crs/src/method/geodesic/start.rs new file mode 100644 index 000000000..44ae3ed6d --- /dev/null +++ b/nodedb-crs/src/method/geodesic/start.rs @@ -0,0 +1,344 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! The starting guess and the λ12(α1) function of the inverse geodesic +//! problem, ported from GeographicLib's C library (PROJ 9.8.1 +//! `src/geodesic.c`, `Astroid`, `InverseStart`, `Lambda12`). + +use core::f64::consts::PI; + +use super::math::{norm2, sq}; +use super::model::{Arc, Geodesic, TOL1, tiny, xthresh}; +use super::series::{ORDER, a3f, c3f, sin_cos_series}; + +/// |n| above which the astroid estimate is skipped, Karney 2013 §5. +const ASTROID_MAX_N: f64 = 0.1; + +/// Multiplier of |n| π cos² β1 in the "no astroid needed" test. +const ASTROID_SIG_FACTOR: f64 = 6.0; + +/// Threshold on sin β12, cos β2 · λ12 for a "short line", Karney 2013 §5. +const SHORT_LINE: f64 = 0.5; + +/// x below which the f < 0 branch scales β by x itself. +const PROLATE_X_SCALE: f64 = -0.01; + +/// Solve k⁴ + 2k³ − (x² + y² − 1)k² − 2y²k − y² = 0 for the positive root, +/// Karney 2013 eq. 55. +pub(super) fn astroid(x: f64, y: f64) -> f64 { + let p = sq(x); + let q = sq(y); + let r = (p + q - 1.0) / 6.0; + if q == 0.0 && r <= 0.0 { + // y = 0 with |x| ≤ 1. + return 0.0; + } + // S = r³ s and the discriminant of the quadratic for T³. + let s = p * q / 4.0; + let r2 = sq(r); + let r3 = r * r2; + let disc = s * (s + 2.0 * r3); + let mut u = r; + if disc >= 0.0 { + let mut t3 = s + r3; + // The sign of the root maximises |T³|, avoiding cancellation. + t3 += if t3 < 0.0 { -disc.sqrt() } else { disc.sqrt() }; + let t = t3.cbrt(); + u += t + if t != 0.0 { r2 / t } else { 0.0 }; + } else { + // T is complex, u stays real. + let ang = (-disc).sqrt().atan2(-(s + r3)); + u += 2.0 * r * (ang / 3.0).cos(); + } + let v = (sq(u) + q).sqrt(); + let uv = if u < 0.0 { q / (v - u) } else { u + v }; + let w = (uv - q) / (2.0 * v); + uv / ((uv + sq(w)).sqrt() + w) +} + +/// Reduced latitudes of the two endpoints, with dn = √(1 + e′² sin² β). +#[derive(Debug, Clone, Copy)] +pub(super) struct Ends { + pub(super) sbet1: f64, + pub(super) cbet1: f64, + pub(super) dn1: f64, + pub(super) sbet2: f64, + pub(super) cbet2: f64, + pub(super) dn2: f64, +} + +/// The starting guess of [`Geodesic::inverse_start`]. +#[derive(Debug, Clone, Copy)] +pub(super) struct Start { + /// σ12 when the short-line solution is final, else negative. + pub(super) sig12: f64, + pub(super) salp1: f64, + pub(super) calp1: f64, + /// Only set when `sig12 >= 0`. + pub(super) salp2: f64, + pub(super) calp2: f64, + /// Only set for short lines. + pub(super) dnm: f64, +} + +/// The state [`Geodesic::lambda12`] computes for one α1. +#[derive(Debug, Clone, Copy)] +pub(super) struct Lambda { + pub(super) lam12: f64, + pub(super) salp2: f64, + pub(super) calp2: f64, + pub(super) arc: Arc, + pub(super) domg12: f64, + /// dλ12/dα1, when requested. + pub(super) dlam12: f64, +} + +impl Geodesic { + /// Starting guess for Newton's method, GeographicLib `InverseStart`. + pub(super) fn inverse_start(&self, e: &Ends, lam12: f64, slam12: f64, clam12: f64) -> Start { + let Ends { + sbet1, + cbet1, + dn1, + sbet2, + cbet2, + dn2, + } = *e; + let mut out = Start { + sig12: -1.0, + salp1: 0.0, + calp1: 0.0, + salp2: 0.0, + calp2: 0.0, + dnm: 0.0, + }; + // β12 = β2 − β1 in [0, π), β12a = β2 + β1 in (−π, 0]. + let sbet12 = sbet2 * cbet1 - cbet2 * sbet1; + let cbet12 = cbet2 * cbet1 + sbet2 * sbet1; + let sbet12a = sbet2 * cbet1 + cbet2 * sbet1; + let shortline = cbet12 >= 0.0 && sbet12 < SHORT_LINE && cbet2 * lam12 < SHORT_LINE; + let (mut somg12, mut comg12); + if shortline { + let mut sbetm2 = sq(sbet1 + sbet2); + sbetm2 /= sbetm2 + sq(cbet1 + cbet2); + out.dnm = (1.0 + self.ep2 * sbetm2).sqrt(); + let omg12 = lam12 / (self.f1 * out.dnm); + (somg12, comg12) = omg12.sin_cos(); + } else { + (somg12, comg12) = (slam12, clam12); + } + + let mut salp1 = cbet2 * somg12; + let mut calp1 = if comg12 >= 0.0 { + sbet12 + cbet2 * sbet1 * sq(somg12) / (1.0 + comg12) + } else { + sbet12a - cbet2 * sbet1 * sq(somg12) / (1.0 - comg12) + }; + let ssig12 = salp1.hypot(calp1); + let csig12 = sbet1 * sbet2 + cbet1 * cbet2 * comg12; + + if shortline && ssig12 < self.etol2 { + // Really short line. + let salp2 = cbet1 * somg12; + let calp2 = sbet12 + - cbet1 + * sbet2 + * if comg12 >= 0.0 { + sq(somg12) / (1.0 + comg12) + } else { + 1.0 - comg12 + }; + (out.salp2, out.calp2) = norm2(salp2, calp2); + out.sig12 = ssig12.atan2(csig12); + } else if self.n.abs() > ASTROID_MAX_N + || csig12 >= 0.0 + || ssig12 >= ASTROID_SIG_FACTOR * self.n.abs() * PI * sq(cbet1) + { + // The zeroth-order spherical approximation is good enough. + } else { + // Scale λ12 and β2 so the antipode is at the origin and the + // singular point at y = 0, x = −1. + let lam12x = (-slam12).atan2(-clam12); + let (x, y, lamscale); + if self.f >= 0.0 { + let k2 = sq(sbet1) * self.ep2; + let eps = k2 / (2.0 * (1.0 + (1.0 + k2).sqrt()) + k2); + lamscale = self.f * cbet1 * a3f(&self.a3x, eps) * PI; + let betscale = lamscale * cbet1; + x = lam12x / lamscale; + y = sbet12a / betscale; + } else { + let cbet12a = cbet2 * cbet1 - sbet2 * sbet1; + let bet12a = sbet12a.atan2(cbet12a); + let l = self.lengths( + &Arc { + eps: self.n, + sig12: PI + bet12a, + ssig1: sbet1, + csig1: -cbet1, + dn1, + ssig2: sbet2, + csig2: cbet2, + dn2, + cbet1, + cbet2, + }, + false, + ); + x = -1.0 + l.m12b / (cbet1 * cbet2 * l.m0 * PI); + let betscale = if x < PROLATE_X_SCALE { + sbet12a / x + } else { + -self.f * sq(cbet1) * PI + }; + lamscale = betscale / cbet1; + y = lam12x / lamscale; + } + + if y > -TOL1 && x > -1.0 - xthresh() { + // Strip near the cut. + if self.f >= 0.0 { + salp1 = (-x).min(1.0); + calp1 = -(1.0 - sq(salp1)).sqrt(); + } else { + calp1 = x.max(if x > -TOL1 { 0.0 } else { -1.0 }); + salp1 = (1.0 - sq(calp1)).sqrt(); + } + } else { + // Estimate ω12 from the astroid, then α1 from the spherical + // formula, Karney 2013 eqs. 56–57. + let k = astroid(x, y); + let omg12a = lamscale + * if self.f >= 0.0 { + -x * k / (1.0 + k) + } else { + -y * (1.0 + k) / k + }; + somg12 = omg12a.sin(); + comg12 = -omg12a.cos(); + salp1 = cbet2 * somg12; + calp1 = sbet12a - cbet2 * sbet1 * sq(somg12) / (1.0 - comg12); + } + } + // The reversed test lets NaN through to norm2. + if salp1 > 0.0 || salp1.is_nan() { + (out.salp1, out.calp1) = norm2(salp1, calp1); + } else { + (out.salp1, out.calp1) = (1.0, 0.0); + } + out + } + + /// λ12 for a trial α1, GeographicLib `Lambda12`. + pub(super) fn lambda12( + &self, + e: &Ends, + salp1: f64, + calp1: f64, + slam120: f64, + clam120: f64, + diffp: bool, + ) -> Lambda { + let Ends { + sbet1, + cbet1, + dn1, + sbet2, + cbet2, + dn2, + } = *e; + // Break the degeneracy of the equatorial line, handled elsewhere. + let calp1 = if sbet1 == 0.0 && calp1 == 0.0 { + -tiny() + } else { + calp1 + }; + + // sin α1 cos β1 = sin α0. + let salp0 = salp1 * cbet1; + let calp0 = calp1.hypot(salp1 * sbet1); + + // tan β1 = tan σ1 cos α1, tan ω1 = sin α0 tan σ1. + let somg1 = salp0 * sbet1; + let comg1 = calp1 * cbet1; + let (ssig1, csig1) = norm2(sbet1, comg1); + + // Enforce symmetry for |β2| = −β1: sin α2 cos β2 = sin α0. + let salp2 = if cbet2 != cbet1 { salp0 / cbet2 } else { salp1 }; + let calp2 = if cbet2 != cbet1 || sbet2.abs() != -sbet1 { + (sq(calp1 * cbet1) + + if cbet1 < -sbet1 { + (cbet2 - cbet1) * (cbet1 + cbet2) + } else { + (sbet1 - sbet2) * (sbet1 + sbet2) + }) + .sqrt() + / cbet2 + } else { + calp1.abs() + }; + // tan β2 = tan σ2 cos α2, tan ω2 = sin α0 tan σ2. + let somg2 = salp0 * sbet2; + let comg2 = calp2 * cbet2; + let (ssig2, csig2) = norm2(sbet2, comg2); + + // σ12 = σ2 − σ1 in [0, π]. + let sig12 = + ((csig1 * ssig2 - ssig1 * csig2).max(0.0) + 0.0).atan2(csig1 * csig2 + ssig1 * ssig2); + // ω12 = ω2 − ω1 in [0, π]. + let somg12 = (comg1 * somg2 - somg1 * comg2).max(0.0) + 0.0; + let comg12 = comg1 * comg2 + somg1 * somg2; + // η = ω12 − λ120. + let eta = (somg12 * clam120 - comg12 * slam120).atan2(comg12 * clam120 + somg12 * slam120); + let k2 = sq(calp0) * self.ep2; + let eps = k2 / (2.0 * (1.0 + (1.0 + k2).sqrt()) + k2); + let c3a = c3f(&self.c3x, eps); + let b312 = sin_cos_series(true, ssig2, csig2, &c3a, ORDER - 1) + - sin_cos_series(true, ssig1, csig1, &c3a, ORDER - 1); + let domg12 = -self.f * a3f(&self.a3x, eps) * salp0 * (sig12 + b312); + let arc = Arc { + eps, + sig12, + ssig1, + csig1, + dn1, + ssig2, + csig2, + dn2, + cbet1, + cbet2, + }; + let dlam12 = if !diffp { + 0.0 + } else if calp2 == 0.0 { + -2.0 * self.f1 * dn1 / sbet1 + } else { + self.lengths(&arc, false).m12b * self.f1 / (calp2 * cbet2) + }; + Lambda { + lam12: eta + domg12, + salp2, + calp2, + arc, + domg12, + dlam12, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn astroid_root_solves_quartic() { + for (x, y) in [(-0.5, -0.3), (-1.2, -0.01), (-0.9, -2.0), (0.3, -0.4)] { + let k: f64 = astroid(x, y); + let r = k.powi(4) + 2.0 * k.powi(3) + - (x * x + y * y - 1.0) * k * k + - 2.0 * y * y * k + - y * y; + assert!(k > 0.0 && r.abs() < 1e-12, "{x} {y} {k} {r}"); + } + assert_eq!(astroid(0.5, 0.0), 0.0); + } +} diff --git a/nodedb-crs/src/method/geos.rs b/nodedb-crs/src/method/geos.rs new file mode 100644 index 000000000..684285d2d --- /dev/null +++ b/nodedb-crs/src/method/geos.rs @@ -0,0 +1,376 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Geostationary Satellite View, by PROJ `src/projections/geos.cpp`. +//! +//! x and y are the satellite's scanning angles times its height above the +//! ellipsoid. `+sweep=y` (the default, Meteosat convention) scans y first; +//! `+sweep=x` (GOES convention) scans x first. + +use core::fmt::Write; + +use serde::{Deserialize, Serialize}; + +use super::angle::wrap_pi; +use crate::def::Ellipsoid; +use crate::error::{CrsParseError, DomainReason}; +use crate::parse::tokens::ProjParams; + +/// The scanning axis that sweeps first. +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Eq, + Serialize, + Deserialize, + zerompk::ToMessagePack, + zerompk::FromMessagePack, +)] +pub enum SweepAxis { + /// `+sweep=x`, GOES. + X, + /// `+sweep=y`, Meteosat and Himawari. The PROJ default. + Y, +} + +/// Defining parameters of one geostationary projection. +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Serialize, + Deserialize, + zerompk::ToMessagePack, + zerompk::FromMessagePack, +)] +pub struct GeosParams { + /// Satellite height above the ellipsoid, metres. + pub h: f64, + /// Sub-satellite longitude λ0, degrees. + pub lon0_deg: f64, + pub sweep: SweepAxis, + /// False easting, metres. + pub false_easting: f64, + /// False northing, metres. + pub false_northing: f64, +} + +impl GeosParams { + /// Read from PROJ-string parameters. `+h` is required. + pub fn from_proj(p: &ProjParams<'_>) -> Result { + let h = p.number("h")?.ok_or(CrsParseError::Missing { + key: "h", + method: "geos", + })?; + if h <= 0.0 { + return Err(CrsParseError::invalid("h", &h.to_string())); + } + let sweep = match p.text("sweep")? { + None | Some("y") => SweepAxis::Y, + Some("x") => SweepAxis::X, + Some(other) => return Err(CrsParseError::invalid("sweep", other)), + }; + Ok(Self { + h, + lon0_deg: p.number_or("lon_0", 0.0)?, + sweep, + false_easting: p.number_or("x_0", 0.0)?, + false_northing: p.number_or("y_0", 0.0)?, + }) + } + + /// Append the PROJ form of the defining parameters. + pub fn write_proj(&self, out: &mut String) { + let _ = write!( + out, + " +proj=geos +h={} +lon_0={} +x_0={} +y_0={}", + self.h, self.lon0_deg, self.false_easting, self.false_northing + ); + match self.sweep { + SweepAxis::X => out.push_str(" +sweep=x"), + SweepAxis::Y => {} + } + } +} + +/// A geostationary projection with its constants computed. Lengths are in +/// units of the semi-major axis a. +#[derive(Debug, Clone, PartialEq)] +pub struct GeostationarySatellite { + params: GeosParams, + /// λ0, radians. + lon0: f64, + /// a, metres. + a: f64, + /// Polar radius b/a (1 on a sphere). + radius_p: f64, + /// (b/a)². + radius_p2: f64, + /// (a/b)². + radius_p_inv2: f64, + /// Distance from the Earth centre to the satellite, 1 + h/a. + radius_g: f64, + /// h/a. + radius_g_1: f64, + /// radius_g² − 1. + c: f64, +} + +impl GeostationarySatellite { + pub fn new(p: &GeosParams, ell: &Ellipsoid) -> Self { + let radius_g_1 = p.h / ell.a; + let radius_g = 1.0 + radius_g_1; + let one_es = 1.0 - ell.e2(); + Self { + params: *p, + lon0: p.lon0_deg.to_radians(), + a: ell.a, + radius_p: one_es.sqrt(), + radius_p2: one_es, + radius_p_inv2: 1.0 / one_es, + radius_g, + radius_g_1, + c: radius_g * radius_g - 1.0, + } + } + + /// Project λ, φ radians to x, y metres. A point the satellite does not + /// see returns `NotVisible`. + pub fn forward(&self, lam: f64, phi: f64) -> Result<(f64, f64), DomainReason> { + let dlam = wrap_pi(lam - self.lon0); + // Geocentric latitude and the geocentric radius, in units of a. + let phi_c = (self.radius_p2 * phi.tan()).atan(); + let (sin_c, cos_c) = phi_c.sin_cos(); + let r = self.radius_p / (self.radius_p * cos_c).hypot(sin_c); + let vx = r * dlam.cos() * cos_c; + let vy = r * dlam.sin() * cos_c; + let vz = r * sin_c; + // PROJ geos.cpp visibility test. PROJ applies it on the ellipsoid + // only; on a sphere it reduces to the same test with b = a. + if (self.radius_g - vx) * vx - vy * vy - vz * vz * self.radius_p_inv2 < 0.0 { + return Err(DomainReason::NotVisible); + } + let tmp = self.radius_g - vx; + let (x, y) = match self.params.sweep { + SweepAxis::X => ( + self.radius_g_1 * (vy / vz.hypot(tmp)).atan(), + self.radius_g_1 * (vz / tmp).atan(), + ), + SweepAxis::Y => ( + self.radius_g_1 * (vy / tmp).atan(), + self.radius_g_1 * (vz / vy.hypot(tmp)).atan(), + ), + }; + Ok(( + self.a * x + self.params.false_easting, + self.a * y + self.params.false_northing, + )) + } + + /// Unproject x, y metres to λ, φ radians. A ray that misses the + /// ellipsoid returns `OutsideProjection`. + pub fn inverse(&self, x: f64, y: f64) -> Result<(f64, f64), DomainReason> { + let x = (x - self.params.false_easting) / self.a; + let y = (y - self.params.false_northing) / self.a; + // Direction of the ray from the satellite. + let vx = -1.0; + let (vy, vz) = match self.params.sweep { + SweepAxis::X => { + let vz = (y / self.radius_g_1).tan(); + ((x / self.radius_g_1).tan() * 1f64.hypot(vz), vz) + } + SweepAxis::Y => { + let vy = (x / self.radius_g_1).tan(); + (vy, (y / self.radius_g_1).tan() * 1f64.hypot(vy)) + } + }; + // Nearest intersection of the ray with the ellipsoid. + let vz_scaled = vz / self.radius_p; + let qa = vy * vy + vz_scaled * vz_scaled + vx * vx; + let qb = 2.0 * self.radius_g * vx; + let det = qb * qb - 4.0 * qa * self.c; + if det < 0.0 { + return Err(DomainReason::OutsideProjection); + } + let k = (-qb - det.sqrt()) / (2.0 * qa); + let px = self.radius_g + k * vx; + let py = k * vy; + let pz = k * vz; + let lam = py.atan2(px); + let phi = (self.radius_p_inv2 * pz / px.hypot(py)).atan(); + Ok((adjlon(self.lon0 + lam), phi)) + } +} + +/// Longitude past ±π by more than this wraps into [−π, π] (PROJ `adjlon`). +const ADJLON_SLACK: f64 = 1e-12; + +/// Wrap λ into [−π, π] unless it is within [`ADJLON_SLACK`] of the range, +/// as PROJ `adjlon` does on the inverse output. +fn adjlon(lam: f64) -> f64 { + if lam.abs() < core::f64::consts::PI + ADJLON_SLACK { + lam + } else { + wrap_pi(lam) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// One millimetre, the tolerance for PROJ reference values. + const MM: f64 = 1e-3; + + /// Inverse tolerance, degrees. + const DEG_TOL: f64 = 1e-9; + + /// NodeDB 990002, Himawari: `+proj=geos +h=35785863 +lon_0=140.7 + /// +ellps=WGS84`. + const HIMAWARI: GeosParams = GeosParams { + h: 35_785_863.0, + lon0_deg: 140.7, + sweep: SweepAxis::Y, + false_easting: 0.0, + false_northing: 0.0, + }; + + /// Input points shared by the reference sets. + const POINTS: [[f64; 2]; 4] = [[100.0, -30.0], [180.0, 40.0], [120.0, 60.0], [150.0, 10.0]]; + + /// PROJ 9.8.1 (pyproj 3.8.0): HIMAWARI. + const PROJ_SWEEP_Y: [[f64; 2]; 4] = [ + [-3_387_194.248_597_037, -2_967_586.956_076_014_3], + [2_883_394.569_923_46, 3_776_151.720_974_31], + [-1_032_087.201_384_981_2, 4_990_147.993_494_502], + [1_009_858.536_176_618_2, 1_094_000.736_002_136_3], + ]; + + /// PROJ 9.8.1: HIMAWARI with `+sweep=x`. + const PROJ_SWEEP_X: [[f64; 2]; 4] = [ + [-3_375_519.754_397_492_4, -2_980_868.497_043_092], + [2_867_322.184_380_393_5, 3_788_351.068_362_223_5], + [-1_022_066.338_940_185_6, 4_992_197.247_323_077], + [1_009_386.555_539_076_1, 1_094_436.206_817_044_6], + ]; + + /// PROJ 9.8.1: `+proj=geos +h=35785863 +lon_0=140.7 +R=6378137`. + const PROJ_SPHERE: [[f64; 2]; 4] = [ + [-3_384_064.730_060_460_5, -2_984_755.273_870_914_2], + [2_879_029.962_482_346_7, 3_795_711.914_534_45], + [-1_029_297.017_993_590_4, 5_009_940.871_180_367], + [1_009_739.101_464_030_2, 1_101_238.970_240_290_5], + ]; + + fn check(p: GeosParams, ell: Ellipsoid, refs: &[[f64; 2]; 4]) { + let g = GeostationarySatellite::new(&p, &ell); + for (ll, xy) in POINTS.iter().zip(refs) { + let (x, y) = g.forward(ll[0].to_radians(), ll[1].to_radians()).unwrap(); + assert!( + (x - xy[0]).abs() < MM && (y - xy[1]).abs() < MM, + "{ll:?}: got {x}, {y}" + ); + let (lam, phi) = g.inverse(xy[0], xy[1]).unwrap(); + assert!((lam.to_degrees() - ll[0]).abs() < DEG_TOL, "{ll:?}"); + assert!((phi.to_degrees() - ll[1]).abs() < DEG_TOL, "{ll:?}"); + } + } + + #[test] + fn matches_proj_sweep_y() { + check(HIMAWARI, Ellipsoid::WGS84, &PROJ_SWEEP_Y); + } + + #[test] + fn matches_proj_sweep_x() { + check( + GeosParams { + sweep: SweepAxis::X, + ..HIMAWARI + }, + Ellipsoid::WGS84, + &PROJ_SWEEP_X, + ); + } + + #[test] + fn matches_proj_sphere() { + check(HIMAWARI, Ellipsoid::sphere(6_378_137.0), &PROJ_SPHERE); + } + + #[test] + fn sub_satellite_point_is_origin() { + let g = GeostationarySatellite::new(&HIMAWARI, &Ellipsoid::WGS84); + let (x, y) = g.forward(140.7f64.to_radians(), 0.0).unwrap(); + assert!(x.abs() < MM && y.abs() < MM); + } + + #[test] + fn far_side_and_off_disk_are_out_of_domain() { + for ell in [Ellipsoid::WGS84, Ellipsoid::sphere(6_378_137.0)] { + let g = GeostationarySatellite::new(&HIMAWARI, &ell); + assert_eq!( + g.forward((-39.3f64).to_radians(), 0.0), + Err(DomainReason::NotVisible) + ); + assert_eq!( + g.forward(140.7f64.to_radians(), 85f64.to_radians()), + Err(DomainReason::NotVisible) + ); + assert_eq!( + g.inverse(6_000_000.0, 0.0), + Err(DomainReason::OutsideProjection) + ); + } + } + + /// PROJ 9.8.1 wraps the inverse longitude: 140.7° + 79.3° comes back + /// as −140° (pyproj gives −140.0000000000027). + #[test] + fn inverse_wraps_longitude_like_proj() { + let g = GeostationarySatellite::new(&HIMAWARI, &Ellipsoid::WGS84); + let (x, y) = g + .forward((-140f64).to_radians(), 10f64.to_radians()) + .unwrap(); + assert!((x - 5_347_786.938_853_634).abs() < MM && (y - 949_453.684_845_179_2).abs() < MM); + let (lam, phi) = g.inverse(x, y).unwrap(); + assert!( + (lam.to_degrees() + 140.0).abs() < DEG_TOL && (phi.to_degrees() - 10.0).abs() < DEG_TOL + ); + } + + #[test] + fn parses_and_writes_proj() { + let text = "+proj=geos +h=35785863 +lon_0=140.7 +ellps=WGS84 +units=m +no_defs"; + let p = GeosParams::from_proj(&ProjParams::tokenize(text).unwrap()).unwrap(); + assert_eq!(p, HIMAWARI); + let mut out = String::new(); + p.write_proj(&mut out); + assert_eq!( + GeosParams::from_proj(&ProjParams::tokenize(&out).unwrap()).unwrap(), + p + ); + let x = GeosParams::from_proj(&ProjParams::tokenize("+proj=geos +h=1 +sweep=x").unwrap()) + .unwrap(); + assert_eq!(x.sweep, SweepAxis::X); + let mut out = String::new(); + x.write_proj(&mut out); + assert_eq!( + GeosParams::from_proj(&ProjParams::tokenize(&out).unwrap()).unwrap(), + x + ); + assert!( + GeosParams::from_proj(&ProjParams::tokenize("+proj=geos +h=1 +sweep=z").unwrap()) + .is_err() + ); + assert_eq!( + GeosParams::from_proj(&ProjParams::tokenize("+proj=geos +lon_0=0").unwrap()), + Err(CrsParseError::Missing { + key: "h", + method: "geos" + }) + ); + } +} diff --git a/nodedb-crs/src/method/krovak.rs b/nodedb-crs/src/method/krovak.rs new file mode 100644 index 000000000..53e173d1e --- /dev/null +++ b/nodedb-crs/src/method/krovak.rs @@ -0,0 +1,534 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Krovak and Krovak Modified, by IOGP Guidance Note 7-2 §3.1.2 and PROJ +//! `src/projections/krovak.cpp`. +//! +//! The native output follows PROJ: easting and northing (EPSG 1041 and +//! 1043, North Orientated) by default, or westing and southing with +//! `+czech`. EPSG 9819 and 1042 (south-orientated) rows carry +//! `+axis=swu`, which the transformer applies to this native output. +//! +//! PROJ fixes the ellipsoid to Bessel 1841, the cone-axis co-latitude αc to +//! 30°17'17.30311" and the pseudo standard parallel to 78°30'N. It ignores +//! `+ellps` and `+alpha` for this method, and so does this file. + +use core::f64::consts::{FRAC_PI_2, FRAC_PI_4}; +use core::fmt::Write; + +use serde::{Deserialize, Serialize}; + +use super::angle::wrap_pi; +use crate::def::Ellipsoid; +use crate::error::{CrsParseError, DomainReason}; +use crate::parse::tokens::ProjParams; + +/// Bessel 1841 semi-major axis, metres, as PROJ `krovak.cpp` fixes it. +const BESSEL_A: f64 = 6_377_397.155; + +/// Bessel 1841 e², as PROJ `krovak.cpp` fixes it. +const BESSEL_E2: f64 = 0.006_674_372_230_614; + +/// Complement of αc, radians: 59°42'42.69689" (PROJ `UQ`). +const UQ: f64 = 1.042_168_563_804_74; + +/// Latitude of the pseudo standard parallel φp, radians: 78°30'N (PROJ `S0`). +const S0: f64 = 1.370_083_462_815_55; + +/// αc in degrees, the value PROJ uses in place of `+alpha`. +pub const KROVAK_ALPHA_DEG: f64 = 30.288_139_752_777_8; + +/// Default latitude of the projection centre φc, degrees: 49°30'N. +pub const KROVAK_DEFAULT_LAT0_DEG: f64 = 49.5; + +/// Longitude of origin east of Ferro, degrees: 42°30'. +const FERRO_LON0_DEG: f64 = 42.5; + +/// Ferro meridian west of Greenwich, degrees: 17°40'. +const FERRO_WEST_OF_GREENWICH_DEG: f64 = 17.0 + 40.0 / 60.0; + +/// Default longitude of origin λ0, degrees east of Greenwich (PROJ default). +pub const KROVAK_DEFAULT_LON0_DEG: f64 = FERRO_LON0_DEG - FERRO_WEST_OF_GREENWICH_DEG; + +/// Default scale factor on the pseudo standard parallel. +pub const KROVAK_DEFAULT_K: f64 = 0.9999; + +/// cos s below which PROJ maps the point to the cone apex. +const APEX_COS_EPS: f64 = 1e-12; + +/// Latitude iteration step below which the inverse stops (PROJ `EPS`). +const LAT_TOLERANCE: f64 = 1e-15; + +/// Latitude iterations allowed (PROJ `MAX_ITER`). +const LAT_MAX_ITERS: usize = 100; + +/// Krovak Modified evaluation point X0 (southing), metres. GN 7-2 §3.1.2.3. +const MOD_X0: f64 = 1_089_000.0; + +/// Krovak Modified evaluation point Y0 (westing), metres. GN 7-2 §3.1.2.3. +const MOD_Y0: f64 = 654_000.0; + +/// Krovak Modified polynomial C1..C10. GN 7-2 §3.1.2.3. +const MOD_C: [f64; 10] = [ + 2.946_529_277e-2, + 2.515_965_696e-2, + 1.193_845_912e-7, + -4.668_270_147e-7, + 9.233_980_362e-12, + 1.523_735_715e-12, + 1.696_780_024e-18, + 4.408_314_235e-18, + -8.331_083_518e-24, + -3.689_471_323e-24, +]; + +/// Defining parameters of one Krovak projection. +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Serialize, + Deserialize, + zerompk::ToMessagePack, + zerompk::FromMessagePack, +)] +pub struct KrovakParams { + /// Latitude of the projection centre φc, degrees. + pub lat0_deg: f64, + /// Longitude of origin λ0, degrees. + pub lon0_deg: f64, + /// `+alpha`, degrees. Kept for writing only: the math uses + /// [`KROVAK_ALPHA_DEG`], as PROJ does. + pub alpha_deg: f64, + /// Scale factor on the pseudo standard parallel kp. + pub k: f64, + /// False easting FE, metres. It offsets the westing. + pub false_easting: f64, + /// False northing FN, metres. It offsets the southing. + pub false_northing: f64, + /// PROJ `+czech`: output westing, southing instead of easting, northing. + pub czech: bool, + /// PROJ `mod_krovak` (EPSG 1042 / 1043): apply the GN 7-2 §3.1.2.3 + /// correction polynomial. + pub modified: bool, +} + +impl KrovakParams { + /// Read from PROJ-string parameters. `+proj=mod_krovak` selects the + /// modified method. Every parameter has a PROJ default. + pub fn from_proj(p: &ProjParams<'_>) -> Result { + let k = p.number_any(&["k", "k_0"])?.unwrap_or(KROVAK_DEFAULT_K); + if k <= 0.0 { + return Err(CrsParseError::invalid("k", &k.to_string())); + } + Ok(Self { + lat0_deg: p.number_or("lat_0", KROVAK_DEFAULT_LAT0_DEG)?, + lon0_deg: p.number_or("lon_0", KROVAK_DEFAULT_LON0_DEG)?, + alpha_deg: p.number_or("alpha", KROVAK_ALPHA_DEG)?, + k, + false_easting: p.number_or("x_0", 0.0)?, + false_northing: p.number_or("y_0", 0.0)?, + czech: p.has("czech"), + modified: p.text("proj")? == Some("mod_krovak"), + }) + } + + /// Append the PROJ form of the defining parameters. + pub fn write_proj(&self, out: &mut String) { + let name = if self.modified { + "mod_krovak" + } else { + "krovak" + }; + let _ = write!( + out, + " +proj={name} +lat_0={} +lon_0={} +alpha={} +k={} +x_0={} +y_0={}", + self.lat0_deg, + self.lon0_deg, + self.alpha_deg, + self.k, + self.false_easting, + self.false_northing + ); + if self.czech { + out.push_str(" +czech"); + } + } +} + +/// A Krovak projection with its constants computed. +#[derive(Debug, Clone, PartialEq)] +pub struct Krovak { + params: KrovakParams, + /// λ0, radians. + lon0: f64, + /// e of the fixed Bessel ellipsoid. + e: f64, + /// GN 7-2 B (PROJ `alpha`). + b: f64, + /// GN 7-2 t0 (PROJ `k`). + t0: f64, + /// GN 7-2 n = sin φp. + n: f64, + /// GN 7-2 r0 in units of a. + rho0: f64, + /// rho0 · tan(φp/2 + π/4)^n, the forward r numerator. + rho_num: f64, + /// αc, radians. + ad: f64, +} + +impl Krovak { + /// Prepare the constants. The ellipsoid is ignored: PROJ fixes Bessel. + pub fn new(p: &KrovakParams, _ell: &Ellipsoid) -> Self { + let e = BESSEL_E2.sqrt(); + let phi0 = p.lat0_deg.to_radians(); + let (sin_p0, cos_p0) = phi0.sin_cos(); + // GN 7-2 §3.1.2.1 projection constants. + let b = (1.0 + BESSEL_E2 * cos_p0.powi(4) / (1.0 - BESSEL_E2)).sqrt(); + let u0 = (sin_p0 / b).asin(); + let g = ((1.0 + e * sin_p0) / (1.0 - e * sin_p0)).powf(b * e / 2.0); + let t0 = (u0 / 2.0 + FRAC_PI_4).tan() / (phi0 / 2.0 + FRAC_PI_4).tan().powf(b) * g; + let n0 = (1.0 - BESSEL_E2).sqrt() / (1.0 - BESSEL_E2 * sin_p0 * sin_p0); + let n = S0.sin(); + let rho0 = p.k * n0 / S0.tan(); + Self { + params: *p, + lon0: p.lon0_deg.to_radians(), + e, + b, + t0, + n, + rho0, + rho_num: rho0 * (S0 / 2.0 + FRAC_PI_4).tan().powf(n), + ad: FRAC_PI_2 - UQ, + } + } + + /// Project λ, φ radians to the native x, y metres (see the module doc). + pub fn forward(&self, lam: f64, phi: f64) -> Result<(f64, f64), DomainReason> { + let dlam = wrap_pi(lam - self.lon0); + let es = self.e * phi.sin(); + let gfi = ((1.0 + es) / (1.0 - es)).powf(self.b * self.e / 2.0); + let u = + 2.0 * ((self.t0 * (phi / 2.0 + FRAC_PI_4).tan().powf(self.b) / gfi).atan() - FRAC_PI_4); + let dv = -dlam * self.b; + let (sin_ad, cos_ad) = self.ad.sin_cos(); + let (sin_u, cos_u) = u.sin_cos(); + let s = clamped_asin(cos_ad * sin_u + sin_ad * cos_u * dv.cos()); + let cos_s = s.cos(); + // Southing Xp and westing Yp in units of a. The cone apex maps to 0. + let (xp, yp) = if cos_s < APEX_COS_EPS { + (0.0, 0.0) + } else { + let d = clamped_asin(cos_u * dv.sin() / cos_s); + let eps = self.n * d; + let rho = self.rho_num / (s / 2.0 + FRAC_PI_4).tan().powf(self.n); + (rho * eps.cos(), rho * eps.sin()) + }; + let (mut south, mut west) = (xp * BESSEL_A, yp * BESSEL_A); + if self.params.modified { + let (dx, dy) = modified_correction(south - MOD_X0, west - MOD_Y0); + south -= dx; + west -= dy; + } + let south = south + self.params.false_northing; + let west = west + self.params.false_easting; + Ok(if self.params.czech { + (west, south) + } else { + (-west, -south) + }) + } + + /// Unproject native x, y metres to λ, φ radians. + /// + /// For north-orientated output PROJ 9.8.1 subtracts `+y_0` from the + /// westing and `+x_0` from the southing, which inverts its own forward + /// only when `+x_0 == +y_0`. This inverse follows GN 7-2 (westing − FE, + /// southing − FN), so it agrees with PROJ on every EPSG row. + pub fn inverse(&self, x: f64, y: f64) -> Result<(f64, f64), DomainReason> { + let (west, south) = if self.params.czech { (x, y) } else { (-x, -y) }; + let mut south = south - self.params.false_northing; + let mut west = west - self.params.false_easting; + if self.params.modified { + let (dx, dy) = modified_correction(south - MOD_X0, west - MOD_Y0); + south += dx; + west += dy; + } + let (xp, yp) = (south / BESSEL_A, west / BESSEL_A); + let rho = xp.hypot(yp); + let eps = yp.atan2(xp); + let d = eps / S0.sin(); + let s = if rho == 0.0 { + FRAC_PI_2 + } else { + 2.0 * (((self.rho0 / rho).powf(1.0 / self.n) * (S0 / 2.0 + FRAC_PI_4).tan()).atan() + - FRAC_PI_4) + }; + let (sin_ad, cos_ad) = self.ad.sin_cos(); + let (sin_s, cos_s) = s.sin_cos(); + let u = clamped_asin(cos_ad * sin_s - sin_ad * cos_s * d.cos()); + let dv = clamped_asin(cos_s * d.sin() / u.cos()); + let lam = self.lon0 - dv / self.b; + // GN 7-2 §3.1.2.1 latitude iteration, started at U'. + let base = self.t0.powf(-1.0 / self.b) * (u / 2.0 + FRAC_PI_4).tan().powf(1.0 / self.b); + let mut fi1 = u; + for _ in 0..LAT_MAX_ITERS { + let es = self.e * fi1.sin(); + let phi = + 2.0 * ((base * ((1.0 + es) / (1.0 - es)).powf(self.e / 2.0)).atan() - FRAC_PI_4); + if (fi1 - phi).abs() < LAT_TOLERANCE { + return Ok((lam, phi)); + } + fi1 = phi; + } + Err(DomainReason::NoConvergence) + } +} + +/// asin with its argument clamped to [−1, 1], so rounding past ±1 at the +/// cone apex gives ±π/2 instead of NaN. +fn clamped_asin(v: f64) -> f64 { + v.clamp(-1.0, 1.0).asin() +} + +/// The Krovak Modified correction (dX, dY) in metres for the reduced +/// southing Xr and westing Yr. GN 7-2 §3.1.2.3. +fn modified_correction(xr: f64, yr: f64) -> (f64, f64) { + let [c1, c2, c3, c4, c5, c6, c7, c8, c9, c10] = MOD_C; + let (xr2, yr2) = (xr * xr, yr * yr); + let (xr4, yr4) = (xr2 * xr2, yr2 * yr2); + let dx = c1 + c3 * xr - c4 * yr - 2.0 * c6 * xr * yr + + c5 * (xr2 - yr2) + + c7 * xr * (xr2 - 3.0 * yr2) + - c8 * yr * (3.0 * xr2 - yr2) + + 4.0 * c9 * xr * yr * (xr2 - yr2) + + c10 * (xr4 + yr4 - 6.0 * xr2 * yr2); + let dy = c2 + + c3 * yr + + c4 * xr + + 2.0 * c5 * xr * yr + + c6 * (xr2 - yr2) + + c8 * xr * (xr2 - 3.0 * yr2) + + c7 * yr * (3.0 * xr2 - yr2) + - 4.0 * c10 * xr * yr * (xr2 - yr2) + + c9 * (xr4 + yr4 - 6.0 * xr2 * yr2); + (dx, dy) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// One millimetre, the tolerance for PROJ reference values. + const MM: f64 = 1e-3; + + /// GN 7-2 prints grid coordinates to the centimetre. + const CM: f64 = 1e-2; + + /// Inverse tolerance, degrees. + const DEG_TOL: f64 = 1e-9; + + /// Ferro meridian in degrees east of Greenwich. + const FERRO_DEG: f64 = -FERRO_WEST_OF_GREENWICH_DEG; + + /// GN 7-2 §3.1.2.1 example point: 50°12'32.442"N, 16°50'59.179"E. + const GN_LAT: f64 = 50.0 + 12.0 / 60.0 + 32.442 / 3600.0; + const GN_LON_GREENWICH: f64 = 16.0 + 50.0 / 60.0 + 59.179 / 3600.0; + + /// EPSG:5514 S-JTSK / Krovak East North, Greenwich λ0. + const EAST_NORTH: KrovakParams = KrovakParams { + lat0_deg: 49.5, + lon0_deg: 24.833_333_333_333_3, + alpha_deg: KROVAK_ALPHA_DEG, + k: 0.9999, + false_easting: 0.0, + false_northing: 0.0, + czech: false, + modified: false, + }; + + /// EPSG:5221 S-JTSK (Ferro) / Krovak East North: λ0 east of Ferro. + const FERRO: KrovakParams = KrovakParams { + lon0_deg: FERRO_LON0_DEG, + ..EAST_NORTH + }; + + fn kro(p: KrovakParams) -> Krovak { + Krovak::new(&p, &Ellipsoid::BESSEL_1841) + } + + fn fwd(k: &Krovak, lon: f64, lat: f64) -> [f64; 2] { + let (x, y) = k.forward(lon.to_radians(), lat.to_radians()).unwrap(); + [x, y] + } + + fn inv(k: &Krovak, x: f64, y: f64) -> [f64; 2] { + let (lam, phi) = k.inverse(x, y).unwrap(); + [lam.to_degrees(), phi.to_degrees()] + } + + fn assert_xy(got: [f64; 2], want: [f64; 2], tol: f64) { + assert!( + (got[0] - want[0]).abs() < tol && (got[1] - want[1]).abs() < tol, + "got {got:?}, want {want:?}" + ); + } + + fn assert_ll(got: [f64; 2], want: [f64; 2]) { + assert!( + (got[0] - want[0]).abs() < DEG_TOL && (got[1] - want[1]).abs() < DEG_TOL, + "got {got:?}, want {want:?}" + ); + } + + /// GN 7-2 §3.1.2.1 and §3.1.2.2: southing 1050538.64, westing 568991.00 + /// on S-JTSK (Ferro); east-north is their negation. + #[test] + fn gn7_2_krovak_example() { + let lon_ferro = GN_LON_GREENWICH - FERRO_DEG; + let en = kro(FERRO); + let got = fwd(&en, lon_ferro, GN_LAT); + assert_xy(got, [-568_991.00, -1_050_538.64], CM); + assert_ll(inv(&en, got[0], got[1]), [lon_ferro, GN_LAT]); + let czech = kro(KrovakParams { + czech: true, + ..FERRO + }); + let got = fwd(&czech, lon_ferro, GN_LAT); + assert_xy(got, [568_991.00, 1_050_538.64], CM); + assert_ll(inv(&czech, got[0], got[1]), [lon_ferro, GN_LAT]); + } + + /// GN 7-2 §3.1.2.3: southing 6050538.71, westing 5568990.91. + #[test] + fn gn7_2_krovak_modified_example() { + let lon_ferro = GN_LON_GREENWICH - FERRO_DEG; + let m = kro(KrovakParams { + alpha_deg: 30.288_139_722_222_2, + false_easting: 5_000_000.0, + false_northing: 5_000_000.0, + czech: true, + modified: true, + ..FERRO + }); + let got = fwd(&m, lon_ferro, GN_LAT); + assert_xy(got, [5_568_990.91, 6_050_538.71], CM); + assert_ll(inv(&m, got[0], got[1]), [lon_ferro, GN_LAT]); + } + + /// PROJ 9.8.1 (pyproj 3.8.0): `+proj=krovak +lat_0=49.5 + /// +lon_0=24.8333333333333 +alpha=30.2881397527778 +k=0.9999 +x_0=0 + /// +y_0=0 +ellps=bessel`. + const PROJ_EAST_NORTH: [([f64; 2], [f64; 2]); 4] = [ + ( + [14.42, 50.09], + [-742_949.430_991_364, -1_042_796.966_162_558_8], + ), + ( + [17.25, 49.6], + [-547_275.172_457_898_4, -1_120_838.490_282_355], + ), + ( + [21.26, 48.72], + [-262_783.267_288_195_7, -1_239_668.514_791_595_3], + ), + ( + [12.5, 50.3], + [-875_002.719_143_145_3, -999_278.244_061_490_6], + ), + ]; + + #[test] + fn matches_proj_east_north_and_czech() { + let en = kro(EAST_NORTH); + let czech = kro(KrovakParams { + czech: true, + ..EAST_NORTH + }); + for (ll, xy) in PROJ_EAST_NORTH { + assert_xy(fwd(&en, ll[0], ll[1]), xy, MM); + assert_ll(inv(&en, xy[0], xy[1]), ll); + assert_xy(fwd(&czech, ll[0], ll[1]), [-xy[0], -xy[1]], MM); + assert_ll(inv(&czech, -xy[0], -xy[1]), ll); + } + } + + /// PROJ 9.8.1: the same with `+x_0=1000 +y_0=2000`. The false easting + /// offsets the westing, so the easting moves by −x_0. + #[test] + fn matches_proj_false_origin() { + let k = kro(KrovakParams { + false_easting: 1000.0, + false_northing: 2000.0, + ..EAST_NORTH + }); + let got = fwd(&k, 14.42, 50.09); + assert_xy(got, [-743_949.430_991_364, -1_044_796.966_162_558_8], MM); + assert_ll(inv(&k, got[0], got[1]), [14.42, 50.09]); + } + + /// PROJ 9.8.1: `+proj=mod_krovak +lat_0=49.5 +lon_0=24.8333333333333 + /// +alpha=30.2881397222222 +k=0.9999 +x_0=5000000 +y_0=5000000 + /// +ellps=bessel` (EPSG:5516). + #[test] + fn matches_proj_modified_east_north() { + let m = kro(KrovakParams { + alpha_deg: 30.288_139_722_222_2, + false_easting: 5_000_000.0, + false_northing: 5_000_000.0, + modified: true, + ..EAST_NORTH + }); + let refs = [ + ( + [14.42, 50.09], + [-5_742_949.453_537_842, -6_042_796.939_892_596], + ), + ( + [17.25, 49.6], + [-5_547_275.256_600_676, -6_120_838.599_420_415], + ), + ( + [21.26, 48.72], + [-5_262_784.792_245_157, -6_239_670.199_519_085], + ), + ( + [12.5, 50.3], + [-5_875_002.997_380_169, -5_999_278.420_538_127], + ), + ]; + for (ll, xy) in refs { + assert_xy(fwd(&m, ll[0], ll[1]), xy, MM); + assert_ll(inv(&m, xy[0], xy[1]), ll); + } + } + + #[test] + fn parses_and_writes_proj() { + let text = "+proj=krovak +axis=swu +lat_0=49.5 +lon_0=42.5 +alpha=30.2881397527778 \ + +k=0.9999 +x_0=0 +y_0=0 +ellps=bessel +pm=-17.6666666666667"; + let p = KrovakParams::from_proj(&ProjParams::tokenize(text).unwrap()).unwrap(); + assert_eq!(p, FERRO); + let mut out = String::new(); + p.write_proj(&mut out); + let again = KrovakParams::from_proj(&ProjParams::tokenize(&out).unwrap()).unwrap(); + assert_eq!(again, p); + let defaults = + KrovakParams::from_proj(&ProjParams::tokenize("+proj=krovak").unwrap()).unwrap(); + assert_eq!(defaults.lat0_deg, KROVAK_DEFAULT_LAT0_DEG); + assert!((defaults.lon0_deg - 24.833_333_333_333_3).abs() < 1e-12); + assert_eq!(defaults.k, KROVAK_DEFAULT_K); + let czech = + KrovakParams::from_proj(&ProjParams::tokenize("+proj=mod_krovak +czech").unwrap()) + .unwrap(); + assert!(czech.czech && czech.modified); + let mut out = String::new(); + czech.write_proj(&mut out); + assert_eq!( + KrovakParams::from_proj(&ProjParams::tokenize(&out).unwrap()).unwrap(), + czech + ); + } +} diff --git a/nodedb-crs/src/method/labrd.rs b/nodedb-crs/src/method/labrd.rs new file mode 100644 index 000000000..defe1edfe --- /dev/null +++ b/nodedb-crs/src/method/labrd.rs @@ -0,0 +1,355 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Laborde Oblique Mercator (EPSG 9813), by PROJ +//! `src/projections/labrd.cpp`. +//! +//! PROJ evaluates Laborde's series expansion of the method in the +//! longitude difference and a cubic complex correction for the azimuth, +//! where IOGP Guidance Note 7-2 §3.2.4.2 gives the closed form. The two +//! agree at the GN 7-2 worked example to within a millimetre. + +use core::f64::consts::FRAC_PI_4; +use core::fmt::Write; + +use serde::{Deserialize, Serialize}; + +use super::angle::wrap_pi; +use crate::def::Ellipsoid; +use crate::error::{CrsParseError, DomainReason}; +use crate::parse::tokens::ProjParams; + +/// Latitude step below which the inverse iteration stops (PROJ `EPS`). +const LAT_TOLERANCE: f64 = 1e-10; + +/// Latitude iterations (PROJ). PROJ keeps the last iterate when the cap is +/// reached, and so does this file. +const LAT_MAX_ITERS: usize = 20; + +/// Largest |φc| in degrees. +const MAX_LAT0_DEG: f64 = 90.0; + +/// Defining parameters of one Laborde Oblique Mercator projection. +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Serialize, + Deserialize, + zerompk::ToMessagePack, + zerompk::FromMessagePack, +)] +pub struct LabrdParams { + /// Latitude of the projection centre φc, degrees. Never zero. + pub lat0_deg: f64, + /// Longitude of the projection centre λc, degrees. + pub lon0_deg: f64, + /// Azimuth of the initial line αc, degrees (PROJ `+azi`). + pub azi_deg: f64, + /// Scale factor on the initial line kc. + pub k0: f64, + /// Easting at the projection centre, metres. + pub false_easting: f64, + /// Northing at the projection centre, metres. + pub false_northing: f64, +} + +impl LabrdParams { + /// Read from PROJ-string parameters. `+lat_0` is required and nonzero, + /// as in PROJ. `+azi` defaults to 0 and `+k` to 1. + pub fn from_proj(p: &ProjParams<'_>) -> Result { + let lat0_deg = p.number("lat_0")?.ok_or(CrsParseError::Missing { + key: "lat_0", + method: "labrd", + })?; + if lat0_deg == 0.0 || lat0_deg.abs() >= MAX_LAT0_DEG { + return Err(CrsParseError::invalid("lat_0", &lat0_deg.to_string())); + } + let k0 = p.number_any(&["k", "k_0"])?.unwrap_or(1.0); + if k0 <= 0.0 { + return Err(CrsParseError::invalid("k", &k0.to_string())); + } + Ok(Self { + lat0_deg, + lon0_deg: p.number_or("lon_0", 0.0)?, + azi_deg: p.number_or("azi", 0.0)?, + k0, + false_easting: p.number_or("x_0", 0.0)?, + false_northing: p.number_or("y_0", 0.0)?, + }) + } + + /// Append the PROJ form of the defining parameters. + pub fn write_proj(&self, out: &mut String) { + let _ = write!( + out, + " +proj=labrd +lat_0={} +lon_0={} +azi={} +k={} +x_0={} +y_0={}", + self.lat0_deg, + self.lon0_deg, + self.azi_deg, + self.k0, + self.false_easting, + self.false_northing + ); + } +} + +/// A Laborde Oblique Mercator projection with its constants computed. +/// Lengths are in units of the semi-major axis a. +#[derive(Debug, Clone, PartialEq)] +pub struct LabordeObliqueMercator { + params: LabrdParams, + /// φc, radians. + phi0: f64, + /// λc, radians. + lon0: f64, + a: f64, + e: f64, + /// 1 − e². + one_es: f64, + /// k0 · √(N R), the Gauss sphere radius. + k_rg: f64, + /// Centre latitude on the Gauss sphere. + p0s: f64, + /// Gauss sphere exponent. + big_a: f64, + /// Gauss sphere constant. + big_c: f64, + /// Azimuth correction coefficients (PROJ `Ca`..`Cd`). + ca: f64, + cb: f64, + cc: f64, + cd: f64, +} + +impl LabordeObliqueMercator { + pub fn new(p: &LabrdParams, ell: &Ellipsoid) -> Self { + let e2 = ell.e2(); + let e = ell.e(); + let one_es = 1.0 - e2; + let phi0 = p.lat0_deg.to_radians(); + let az = p.azi_deg.to_radians(); + let sinp = phi0.sin(); + // Radii of curvature at φc in units of a, PROJ labrd.cpp setup. + let t = 1.0 - e2 * sinp * sinp; + let n = 1.0 / t.sqrt(); + let r = one_es * n / t; + let k_rg = p.k0 * (n * r).sqrt(); + let p0s = ((r / n).sqrt() * phi0.tan()).atan(); + let big_a = sinp / p0s.sin(); + let es = e * sinp; + let big_c = 0.5 * e * big_a * ((1.0 + es) / (1.0 - es)).ln() + - big_a * (FRAC_PI_4 + 0.5 * phi0).tan().ln() + + (FRAC_PI_4 + 0.5 * p0s).tan().ln(); + let cb0 = 1.0 / (12.0 * k_rg * k_rg); + let ca = (1.0 - (2.0 * az).cos()) * cb0; + let cb = cb0 * (2.0 * az).sin(); + Self { + params: *p, + phi0, + lon0: p.lon0_deg.to_radians(), + a: ell.a, + e, + one_es, + k_rg, + p0s, + big_a, + big_c, + ca, + cb, + cc: 3.0 * (ca * ca - cb * cb), + cd: 6.0 * ca * cb, + } + } + + /// Latitude on the Gauss sphere for geodetic latitude φ. + fn sphere_lat(&self, phi: f64) -> f64 { + let v1 = self.big_a * (FRAC_PI_4 + 0.5 * phi).tan().ln(); + let t = self.e * phi.sin(); + let v2 = 0.5 * self.e * self.big_a * ((1.0 + t) / (1.0 - t)).ln(); + 2.0 * ((v1 - v2 + self.big_c).exp().atan() - FRAC_PI_4) + } + + /// Project λ, φ radians to easting, northing metres. + pub fn forward(&self, lam: f64, phi: f64) -> Result<(f64, f64), DomainReason> { + let dlam = wrap_pi(lam - self.lon0); + let ps = self.sphere_lat(phi); + let i1 = ps - self.p0s; + let (sinps, cosps) = ps.sin_cos(); + let (sinps2, cosps2) = (sinps * sinps, cosps * cosps); + let a2 = self.big_a * self.big_a; + // Series terms I1..I6, PROJ labrd_e_forward. + let i4 = self.big_a * cosps; + let i2 = 0.5 * self.big_a * i4 * sinps; + let i3 = i2 * a2 * (5.0 * cosps2 - sinps2) / 12.0; + let i6_base = i4 * a2; + let i5 = i6_base * (cosps2 - sinps2) / 6.0; + let i6 = i6_base * a2 * (5.0 * cosps2 * cosps2 + sinps2 * (sinps2 - 18.0 * cosps2)) / 120.0; + let t = dlam * dlam; + let mut x = self.k_rg * dlam * (i4 + t * (i5 + t * i6)); + let mut y = self.k_rg * (i1 + t * (i2 + t * i3)); + // Cubic azimuth correction. + let (x2, y2) = (x * x, y * y); + let v1 = 3.0 * x * y2 - x * x2; + let v2 = y * y2 - 3.0 * x2 * y; + x += self.ca * v1 + self.cb * v2; + y += self.ca * v2 - self.cb * v1; + Ok(( + self.a * x + self.params.false_easting, + self.a * y + self.params.false_northing, + )) + } + + /// Unproject easting, northing metres to λ, φ radians. + pub fn inverse(&self, x: f64, y: f64) -> Result<(f64, f64), DomainReason> { + let mut x = (x - self.params.false_easting) / self.a; + let mut y = (y - self.params.false_northing) / self.a; + // Remove the azimuth correction to fifth order. + let (x2, y2) = (x * x, y * y); + let v1 = 3.0 * x * y2 - x * x2; + let v2 = y * y2 - 3.0 * x2 * y; + let v3 = x * (5.0 * y2 * y2 + x2 * (-10.0 * y2 + x2)); + let v4 = y * (5.0 * x2 * x2 + y2 * (-10.0 * x2 + y2)); + x += -self.ca * v1 - self.cb * v2 + self.cc * v3 + self.cd * v4; + y += self.cb * v1 - self.ca * v2 - self.cd * v3 + self.cc * v4; + let ps = self.p0s + y / self.k_rg; + // Geodetic latitude whose sphere latitude is ps, by fixed-point + // iteration from ps shifted by φc − p0s. + let mut pe = ps + self.phi0 - self.p0s; + for _ in 0..LAT_MAX_ITERS { + let step = ps - self.sphere_lat(pe); + pe += step; + if step.abs() < LAT_TOLERANCE { + break; + } + } + // Series terms I7..I11, PROJ labrd_e_inverse. + let t = self.e * pe.sin(); + let t = 1.0 - t * t; + let re = self.one_es / (t * t.sqrt()); + let tan_ps = ps.tan(); + let t2 = tan_ps * tan_ps; + let s = self.k_rg * self.k_rg; + let d = re * self.params.k0 * self.k_rg; + let i7 = tan_ps / (2.0 * d); + let i8 = tan_ps * (5.0 + 3.0 * t2) / (24.0 * d * s); + let d = ps.cos() * self.k_rg * self.big_a; + let i9 = 1.0 / d; + let d = d * s; + let i10 = (1.0 + 2.0 * t2) / (6.0 * d); + let i11 = (5.0 + t2 * (28.0 + 24.0 * t2)) / (120.0 * d * s); + let x2 = x * x; + let phi = pe + x2 * (-i7 + i8 * x2); + let dlam = x * (i9 + x2 * (-i10 + x2 * i11)); + Ok((self.lon0 + dlam, phi)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// One millimetre, the tolerance for PROJ reference values. + const MM: f64 = 1e-3; + + /// Inverse tolerance, degrees. + const DEG_TOL: f64 = 1e-9; + + /// EPSG:8441 Tananarive / Laborde Grid, Greenwich λc. + const TANANARIVE: LabrdParams = LabrdParams { + lat0_deg: -18.9, + lon0_deg: 46.437_229_166_666_7, + azi_deg: 18.9, + k0: 0.9995, + false_easting: 400_000.0, + false_northing: 800_000.0, + }; + + fn lab() -> LabordeObliqueMercator { + LabordeObliqueMercator::new(&TANANARIVE, &Ellipsoid::INTERNATIONAL_1924) + } + + /// GN 7-2 §3.2.4.2: 16°11'23.280"S, 44°27'27.260"E gives E = + /// 188333.848 m, N = 1098841.091 m. + #[test] + fn gn7_2_example() { + let lat: f64 = -(16.0 + 11.0 / 60.0 + 23.280 / 3600.0); + let lon: f64 = 44.0 + 27.0 / 60.0 + 27.260 / 3600.0; + let l = lab(); + let (x, y) = l.forward(lon.to_radians(), lat.to_radians()).unwrap(); + assert!( + (x - 188_333.848).abs() < MM && (y - 1_098_841.091).abs() < MM, + "got {x}, {y}" + ); + let (lam, phi) = l.inverse(188_333.848, 1_098_841.091).unwrap(); + // GN 7-2 prints the reverse result to 0.001", about 5e-7°. + assert!((lam.to_degrees() - lon).abs() < 1e-7 && (phi.to_degrees() - lat).abs() < 1e-7); + } + + /// PROJ 9.8.1 (pyproj 3.8.0): `+proj=labrd +lat_0=-18.9 + /// +lon_0=46.4372291666667 +azi=18.9 +k=0.9995 +x_0=400000 +y_0=800000 + /// +ellps=intl`. Columns: input λ, φ; PROJ forward x, y; PROJ inverse + /// of that x, y. The PROJ inverse series misses the input by up to + /// 7e-8°, so the inverse check compares with PROJ's own inverse. + const PROJ_TANANARIVE: [([f64; 2], [f64; 2], [f64; 2]); 4] = [ + ( + [47.5207, -18.9137], + [514_091.950_795_276, 798_136.475_317_906], + [47.520_700_000_000_8, -18.913_700_000_554_54], + ), + ( + [49.4, -12.3], + [722_684.617_565_835_8, 1_527_659.685_043_107_7], + [49.400_000_018_480_554, -12.299_999_931_815_185], + ), + ( + [43.7, -23.35], + [120_070.082_881_767_13, 305_117.680_992_782_47], + [43.699_999_994_561_25, -23.350_000_062_799_186], + ), + ( + [47.0, -25.0], + [456_439.021_317_990_5, 124_525.325_313_277_43], + [46.999_999_999_833_56, -24.999_999_985_024_647], + ), + ]; + + #[test] + fn matches_proj() { + let l = lab(); + for (ll, xy, back) in PROJ_TANANARIVE { + let (x, y) = l.forward(ll[0].to_radians(), ll[1].to_radians()).unwrap(); + assert!( + (x - xy[0]).abs() < MM && (y - xy[1]).abs() < MM, + "{ll:?}: got {x}, {y}" + ); + let (lam, phi) = l.inverse(xy[0], xy[1]).unwrap(); + assert!((lam.to_degrees() - back[0]).abs() < DEG_TOL, "{ll:?}"); + assert!((phi.to_degrees() - back[1]).abs() < DEG_TOL, "{ll:?}"); + } + } + + #[test] + fn parses_and_writes_proj() { + let text = "+proj=labrd +lat_0=-18.9 +lon_0=46.4372291666667 +azi=18.9 +k=0.9995 \ + +x_0=400000 +y_0=800000 +ellps=intl +units=m +no_defs"; + let p = LabrdParams::from_proj(&ProjParams::tokenize(text).unwrap()).unwrap(); + assert_eq!(p, TANANARIVE); + let mut out = String::new(); + p.write_proj(&mut out); + assert_eq!( + LabrdParams::from_proj(&ProjParams::tokenize(&out).unwrap()).unwrap(), + p + ); + assert!( + LabrdParams::from_proj(&ProjParams::tokenize("+proj=labrd +lat_0=0").unwrap()).is_err() + ); + assert_eq!( + LabrdParams::from_proj(&ProjParams::tokenize("+proj=labrd").unwrap()), + Err(CrsParseError::Missing { + key: "lat_0", + method: "labrd" + }) + ); + } +} diff --git a/nodedb-crs/src/method/laea.rs b/nodedb-crs/src/method/laea.rs new file mode 100644 index 000000000..faddc4888 --- /dev/null +++ b/nodedb-crs/src/method/laea.rs @@ -0,0 +1,573 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Lambert Azimuthal Equal Area, EPSG methods 9820 (ellipsoidal) and 1027 +//! (spherical), PROJ `laea`. +//! +//! Follows PROJ 9.8.1 `src/projections/laea.cpp` (Snyder 1987 pp. 182–190, +//! eqs. 24-2 to 24-29; IOGP Guidance Note 7-2 §3.6.2), with its polar, +//! equatorial and oblique aspects. + +use core::f64::consts::{FRAC_PI_2, FRAC_PI_4}; +use core::fmt::Write; + +use serde::{Deserialize, Serialize}; + +use super::angle::wrap_pi; +use super::authalic::Authalic; +use crate::def::Ellipsoid; +use crate::error::{CrsParseError, DomainReason}; +use crate::parse::tokens::ProjParams; + +/// PROJ `EPS10`, radians. +const EPS10: f64 = 1e-10; + +/// q below which a polar forward lands on the origin, PROJ `laea_e_forward`. +const POLAR_Q_MIN: f64 = 1e-15; + +/// Largest absolute latitude, degrees. +const MAX_LAT_DEG: f64 = 90.0; + +/// Defining parameters of one Lambert Azimuthal Equal Area projection. +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Serialize, + Deserialize, + zerompk::ToMessagePack, + zerompk::FromMessagePack, +)] +pub struct LaeaParams { + /// Latitude of natural origin φ0, degrees (`+lat_0`). + pub lat0_deg: f64, + /// Longitude of natural origin λ0, degrees (`+lon_0`). + pub lon0_deg: f64, + /// False easting, metres (`+x_0`). + pub false_easting: f64, + /// False northing, metres (`+y_0`). + pub false_northing: f64, +} + +impl LaeaParams { + /// Read from PROJ-string parameters. Every key defaults to 0 as in PROJ. + pub fn from_proj(p: &ProjParams<'_>) -> Result { + let lat0_deg = p.number_or("lat_0", 0.0)?; + if lat0_deg.abs() > MAX_LAT_DEG { + return Err(CrsParseError::invalid("lat_0", &lat0_deg.to_string())); + } + Ok(Self { + lat0_deg, + lon0_deg: p.number_or("lon_0", 0.0)?, + false_easting: p.number_or("x_0", 0.0)?, + false_northing: p.number_or("y_0", 0.0)?, + }) + } + + /// Append the defining parameters as PROJ-string tokens. + pub fn write_proj(&self, out: &mut String) { + let _ = write!( + out, + " +proj=laea +lat_0={} +lon_0={} +x_0={} +y_0={}", + self.lat0_deg, self.lon0_deg, self.false_easting, self.false_northing + ); + } +} + +/// Aspect of the projection, from φ0. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Mode { + NorthPole, + SouthPole, + Equatorial, + Oblique, +} + +/// Lambert Azimuthal Equal Area with its constants computed. +#[derive(Debug, Clone, PartialEq)] +pub struct LambertAzimuthalEqualArea { + params: LaeaParams, + a: f64, + lon0: f64, + phi0: f64, + mode: Mode, + /// Authalic conversion; `None` on a sphere. + auth: Option, + /// sin and cos of the authalic (sphere: geodetic) origin latitude. + sinb1: f64, + cosb1: f64, + xmf: f64, + ymf: f64, + /// Authalic radius over a, Rq. + rq: f64, + /// D, Snyder eq. 24-20. + dd: f64, +} + +impl LambertAzimuthalEqualArea { + pub fn new(p: &LaeaParams, ell: &Ellipsoid) -> Self { + let phi0 = p.lat0_deg.to_radians(); + let t = phi0.abs(); + let mode = if (t - FRAC_PI_2).abs() < EPS10 { + if phi0 < 0.0 { + Mode::SouthPole + } else { + Mode::NorthPole + } + } else if t < EPS10 { + Mode::Equatorial + } else { + Mode::Oblique + }; + let mut out = Self { + params: *p, + a: ell.a, + lon0: p.lon0_deg.to_radians(), + phi0, + mode, + auth: None, + sinb1: 0.0, + cosb1: 0.0, + xmf: 0.0, + ymf: 0.0, + rq: 0.0, + dd: 0.0, + }; + if ell.is_sphere() { + if mode == Mode::Oblique { + (out.sinb1, out.cosb1) = phi0.sin_cos(); + } + return out; + } + let es = ell.e2(); + let auth = Authalic::new(es); + let qp = auth.qp(); + match mode { + Mode::NorthPole | Mode::SouthPole => out.dd = 1.0, + Mode::Equatorial => { + out.rq = (0.5 * qp).sqrt(); + out.dd = 1.0 / out.rq; + out.xmf = 1.0; + out.ymf = 0.5 * qp; + } + Mode::Oblique => { + out.rq = (0.5 * qp).sqrt(); + let (sinphi, cosphi) = phi0.sin_cos(); + (out.sinb1, out.cosb1) = auth.to_authalic(phi0).sin_cos(); + out.dd = cosphi / ((1.0 - es * sinphi * sinphi).sqrt() * out.rq * out.cosb1); + out.ymf = out.rq / out.dd; + out.xmf = out.rq * out.dd; + } + } + out.auth = Some(auth); + out + } + + pub fn forward(&self, lam: f64, phi: f64) -> Result<(f64, f64), DomainReason> { + let lam = wrap_pi(lam - self.lon0); + let (x, y) = match &self.auth { + Some(a) => self.e_forward(a, lam, phi)?, + None => self.s_forward(lam, phi)?, + }; + Ok(( + self.params.false_easting + self.a * x, + self.params.false_northing + self.a * y, + )) + } + + pub fn inverse(&self, x: f64, y: f64) -> Result<(f64, f64), DomainReason> { + let x = (x - self.params.false_easting) / self.a; + let y = (y - self.params.false_northing) / self.a; + let (lam, phi) = match &self.auth { + Some(a) => self.e_inverse(a, x, y)?, + None => self.s_inverse(x, y)?, + }; + Ok((wrap_pi(lam + self.lon0), phi)) + } + + /// Ellipsoidal forward, PROJ `laea_e_forward`. + fn e_forward(&self, auth: &Authalic, lam: f64, phi: f64) -> Result<(f64, f64), DomainReason> { + let (sinlam, coslam) = lam.sin_cos(); + let xi = auth.to_authalic(phi); + let (sinb, cosb) = xi.sin_cos(); + let qp = auth.qp(); + let mut q = sinb * qp; + let b = match self.mode { + Mode::Oblique => 1.0 + self.sinb1 * sinb + self.cosb1 * cosb * coslam, + Mode::Equatorial => 1.0 + cosb * coslam, + Mode::NorthPole => { + q = qp - q; + FRAC_PI_2 + phi + } + Mode::SouthPole => { + q += qp; + phi - FRAC_PI_2 + } + }; + if b.abs() < EPS10 { + return Err(DomainReason::Antipode); + } + Ok(match self.mode { + Mode::Oblique => { + let b = (2.0 / b).sqrt(); + ( + self.xmf * b * cosb * sinlam, + self.ymf * b * (self.cosb1 * sinb - self.sinb1 * cosb * coslam), + ) + } + Mode::Equatorial => { + let b = (2.0 / (1.0 + cosb * coslam)).sqrt(); + (self.xmf * b * cosb * sinlam, b * sinb * self.ymf) + } + Mode::NorthPole | Mode::SouthPole => { + if q >= POLAR_Q_MIN { + let b = q.sqrt(); + let y = if self.mode == Mode::SouthPole { b } else { -b }; + (b * sinlam, coslam * y) + } else { + (0.0, 0.0) + } + } + }) + } + + /// Spherical forward, PROJ `laea_s_forward`. + fn s_forward(&self, lam: f64, phi: f64) -> Result<(f64, f64), DomainReason> { + let (sinphi, cosphi) = phi.sin_cos(); + let (sinlam, mut coslam) = lam.sin_cos(); + match self.mode { + Mode::Equatorial | Mode::Oblique => { + let denom = if self.mode == Mode::Equatorial { + 1.0 + cosphi * coslam + } else { + 1.0 + self.sinb1 * sinphi + self.cosb1 * cosphi * coslam + }; + if denom <= EPS10 { + return Err(DomainReason::Antipode); + } + let k = (2.0 / denom).sqrt(); + let y = if self.mode == Mode::Equatorial { + sinphi + } else { + self.cosb1 * sinphi - self.sinb1 * cosphi * coslam + }; + Ok((k * cosphi * sinlam, k * y)) + } + Mode::NorthPole | Mode::SouthPole => { + if self.mode == Mode::NorthPole { + coslam = -coslam; + } + if (phi + self.phi0).abs() < EPS10 { + return Err(DomainReason::Antipode); + } + let t = FRAC_PI_4 - phi * 0.5; + let r = 2.0 + * if self.mode == Mode::SouthPole { + t.cos() + } else { + t.sin() + }; + Ok((r * sinlam, r * coslam)) + } + } + } + + /// Ellipsoidal inverse, PROJ `laea_e_inverse`. + fn e_inverse(&self, auth: &Authalic, x: f64, y: f64) -> Result<(f64, f64), DomainReason> { + let qp = auth.qp(); + let (mut x, mut y) = (x, y); + let ab = match self.mode { + Mode::Equatorial | Mode::Oblique => { + x /= self.dd; + y *= self.dd; + let rho = x.hypot(y); + if rho < EPS10 { + return Ok((0.0, self.phi0)); + } + let asin_arg = 0.5 * rho / self.rq; + if asin_arg > 1.0 { + return Err(DomainReason::OutsideProjection); + } + let (s_ce, c_ce) = (2.0 * asin_arg.asin()).sin_cos(); + x *= s_ce; + if self.mode == Mode::Oblique { + let ab = c_ce * self.sinb1 + y * s_ce * self.cosb1 / rho; + y = rho * self.cosb1 * c_ce - y * self.sinb1 * s_ce; + ab + } else { + let ab = y * s_ce / rho; + y = rho * c_ce; + ab + } + } + Mode::NorthPole | Mode::SouthPole => { + if self.mode == Mode::NorthPole { + y = -y; + } + let q = x * x + y * y; + if q == 0.0 { + return Ok((0.0, self.phi0)); + } + let ab = 1.0 - q / qp; + if self.mode == Mode::SouthPole { + -ab + } else { + ab + } + } + }; + if ab.abs() > 1.0 { + return Err(DomainReason::OutsideProjection); + } + Ok((x.atan2(y), auth.from_authalic(ab.asin()))) + } + + /// Spherical inverse, PROJ `laea_s_inverse`. + fn s_inverse(&self, x: f64, y: f64) -> Result<(f64, f64), DomainReason> { + let (mut x, mut y) = (x, y); + let rh = x.hypot(y); + let half = rh * 0.5; + if half > 1.0 { + return Err(DomainReason::OutsideProjection); + } + let z = 2.0 * half.asin(); + let (sinz, cosz) = z.sin_cos(); + let phi = match self.mode { + Mode::Equatorial => { + let phi = if rh.abs() <= EPS10 { + 0.0 + } else { + (y * sinz / rh).asin() + }; + x *= sinz; + y = cosz * rh; + phi + } + Mode::Oblique => { + let phi = if rh.abs() <= EPS10 { + self.phi0 + } else { + (cosz * self.sinb1 + y * sinz * self.cosb1 / rh).asin() + }; + x *= sinz * self.cosb1; + y = (cosz - phi.sin() * self.sinb1) * rh; + phi + } + Mode::NorthPole => { + y = -y; + FRAC_PI_2 - z + } + Mode::SouthPole => z - FRAC_PI_2, + }; + let lam = if y == 0.0 && matches!(self.mode, Mode::Equatorial | Mode::Oblique) { + 0.0 + } else { + x.atan2(y) + }; + Ok((lam, phi)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const MM: f64 = 1e-3; + const DEG_TOL: f64 = 1e-9; + + fn params(lat0_deg: f64, lon0_deg: f64, false_easting: f64, false_northing: f64) -> LaeaParams { + LaeaParams { + lat0_deg, + lon0_deg, + false_easting, + false_northing, + } + } + + fn check(p: &LaeaParams, ell: &Ellipsoid, pts: &[[f64; 4]]) { + let laea = LambertAzimuthalEqualArea::new(p, ell); + for [lon, lat, x, y] in pts.iter().copied() { + let (gx, gy) = laea.forward(lon.to_radians(), lat.to_radians()).unwrap(); + assert!( + (gx - x).abs() < MM && (gy - y).abs() < MM, + "{lon} {lat}: {gx} {gy}" + ); + let (glam, gphi) = laea.inverse(x, y).unwrap(); + assert!( + (glam.to_degrees() - lon).abs() < DEG_TOL + && (gphi.to_degrees() - lat).abs() < DEG_TOL, + "{x} {y}: {} {}", + glam.to_degrees(), + gphi.to_degrees() + ); + } + } + + /// GN 7-2 §3.6.2 example, ETRS89 / LAEA Europe (EPSG:3035): + /// φ = 50°N, λ = 5°E → E = 3 962 799.45 m, N = 2 999 718.85 m. + #[test] + fn gn7_2_etrs89_laea() { + let laea = LambertAzimuthalEqualArea::new( + ¶ms(52.0, 10.0, 4_321_000.0, 3_210_000.0), + &Ellipsoid::GRS80, + ); + let (x, y) = laea.forward(5f64.to_radians(), 50f64.to_radians()).unwrap(); + assert!( + (x - 3_962_799.45).abs() < 0.005 && (y - 2_999_718.85).abs() < 0.005, + "{x} {y}" + ); + let (lam, phi) = laea.inverse(3_962_799.45, 2_999_718.85).unwrap(); + assert!((lam.to_degrees() - 5.0).abs() < 1e-7 && (phi.to_degrees() - 50.0).abs() < 1e-7); + } + + /// PROJ 9.8.1 via pyproj 3.8.0, EPSG:3035 + /// `+proj=laea +lat_0=52 +lon_0=10 +x_0=4321000 +y_0=3210000 +ellps=GRS80`. + #[test] + fn oblique_ellipsoid() { + check( + ¶ms(52.0, 10.0, 4_321_000.0, 3_210_000.0), + &Ellipsoid::GRS80, + OBLIQUE, + ); + } + + /// PROJ 9.8.1 via pyproj 3.8.0, EPSG:6931 + /// `+proj=laea +lat_0=90 +lon_0=0 +x_0=0 +y_0=0 +datum=WGS84`. + #[test] + fn north_polar_ellipsoid() { + check(¶ms(90.0, 0.0, 0.0, 0.0), &Ellipsoid::WGS84, NORTH_POLAR); + } + + /// PROJ 9.8.1 via pyproj 3.8.0, EPSG:6932 + /// `+proj=laea +lat_0=-90 +lon_0=0 +x_0=0 +y_0=0 +datum=WGS84`. + #[test] + fn south_polar_ellipsoid() { + check( + ¶ms(-90.0, 0.0, 0.0, 0.0), + &Ellipsoid::WGS84, + SOUTH_POLAR, + ); + } + + /// PROJ 9.8.1 via pyproj 3.8.0, `+proj=laea +lat_0=0 +lon_0=20 +ellps=WGS84`. + #[test] + fn equatorial_ellipsoid() { + check(¶ms(0.0, 20.0, 0.0, 0.0), &Ellipsoid::WGS84, EQUATORIAL); + } + + /// EPSG 1027 (spherical), PROJ 9.8.1 via pyproj 3.8.0, EPSG:3408 + /// `+proj=laea +lat_0=90 +lon_0=0 +x_0=0 +y_0=0 +R=6371228`. + #[test] + fn sphere_north_polar() { + check( + ¶ms(90.0, 0.0, 0.0, 0.0), + &Ellipsoid::sphere(6_371_228.0), + SPHERE_NORTH, + ); + } + + /// PROJ 9.8.1 via pyproj 3.8.0, EPSG:3409 + /// `+proj=laea +lat_0=-90 +lon_0=0 +x_0=0 +y_0=0 +R=6371228`. + #[test] + fn sphere_south_polar() { + check( + ¶ms(-90.0, 0.0, 0.0, 0.0), + &Ellipsoid::sphere(6_371_228.0), + SPHERE_SOUTH, + ); + } + + /// PROJ 9.8.1 via pyproj 3.8.0, `+proj=laea +lat_0=45 +lon_0=-100 +R=6370997`. + #[test] + fn sphere_oblique() { + check( + ¶ms(45.0, -100.0, 0.0, 0.0), + &Ellipsoid::sphere(6_370_997.0), + SPHERE_OBLIQUE, + ); + } + + /// PROJ 9.8.1 via pyproj 3.8.0, `+proj=laea +lat_0=0 +lon_0=0 +R=6370997`. + #[test] + fn sphere_equatorial() { + check( + ¶ms(0.0, 0.0, 0.0, 0.0), + &Ellipsoid::sphere(6_370_997.0), + SPHERE_EQUATORIAL, + ); + } + + // (lon, lat, x, y) reference points. + const OBLIQUE: &[[f64; 4]] = &[ + [5.0, 50.0, 3962799.4509550678, 2999718.853159564], + [-9.0, 38.7, 2676732.112534873, 1941329.0843069698], + [30.0, 60.0, 5422492.77496154, 4256803.184504329], + [10.0, 52.0, 4321000.0, 3210000.0], + [25.0, 35.0, 5695390.183788649, 1461721.553816132], + ]; + const NORTH_POLAR: &[[f64; 4]] = &[ + [0.0, 90.0, 0.0, 0.0], + [45.0, 60.0, 2340395.8490303955, -2340395.849030396], + [-120.0, 75.0, -1446478.9424851332, 835125.0068209164], + [170.0, 10.0, 1422897.3773335486, 8069652.027253477], + ]; + const SOUTH_POLAR: &[[f64; 4]] = &[ + [45.0, -60.0, 2340395.8490303955, 2340395.849030396], + [-120.0, -75.0, -1446478.9424851332, -835125.0068209164], + [170.0, -10.0, 1422897.3773335486, -8069652.027253477], + ]; + const EQUATORIAL: &[[f64; 4]] = &[ + [20.0, 0.0, 0.0, 0.0], + [50.0, 30.0, 2955093.6416210765, 3389407.875813428], + [-30.0, -45.0, -4058883.8196492754, -5263034.2804662995], + [100.0, 10.0, 8085103.626887971, 1437923.67613473], + ]; + const SPHERE_NORTH: &[[f64; 4]] = &[ + [45.0, 60.0, 2332031.3013027217, -2332031.3013027217], + [-120.0, 75.0, -1440394.4623998066, 831612.1306057742], + [170.0, 10.0, 1422298.8844147702, 8066257.80524404], + ]; + const SPHERE_SOUTH: &[[f64; 4]] = &[ + [45.0, -60.0, 2332031.301302723, 2332031.301302723], + [-120.0, -75.0, -1440394.462399806, -831612.1306057739], + ]; + const SPHERE_OBLIQUE: &[[f64; 4]] = &[ + [-120.0, 35.0, -1807736.3959958896, -895049.1532893647], + [-75.0, 45.0, 1926581.688418818, 302014.4119897752], + [10.0, 10.0, 8865066.189534225, 3457815.856604664], + ]; + const SPHERE_EQUATORIAL: &[[f64; 4]] = &[ + [0.0, 0.0, 0.0, 0.0], + [30.0, 30.0, 2949198.539095313, 3405441.1408806597], + [-120.0, -45.0, -6862331.619017425, -7923938.015016379], + [89.0, 1.0, 8929633.072795173, 155891.06803553604], + ]; + + #[test] + fn antipode_is_out_of_domain() { + let laea = LambertAzimuthalEqualArea::new(¶ms(90.0, 0.0, 0.0, 0.0), &Ellipsoid::WGS84); + assert_eq!(laea.forward(0.0, -FRAC_PI_2), Err(DomainReason::Antipode)); + let laea = + LambertAzimuthalEqualArea::new(¶ms(0.0, 0.0, 0.0, 0.0), &Ellipsoid::sphere(1.0)); + assert_eq!( + laea.forward(core::f64::consts::PI, 0.0), + Err(DomainReason::Antipode) + ); + assert_eq!(laea.inverse(3.0, 0.0), Err(DomainReason::OutsideProjection)); + } + + #[test] + fn parses_and_writes() { + let p = ProjParams::tokenize("+proj=laea +lat_0=52 +lon_0=10 +x_0=4321000 +y_0=3210000") + .unwrap(); + let l = LaeaParams::from_proj(&p).unwrap(); + let mut s = String::new(); + l.write_proj(&mut s); + assert_eq!( + s, + " +proj=laea +lat_0=52 +lon_0=10 +x_0=4321000 +y_0=3210000" + ); + let bad = ProjParams::tokenize("+proj=laea +lat_0=91").unwrap(); + assert!(LaeaParams::from_proj(&bad).is_err()); + } +} diff --git a/nodedb-crs/src/method/lcc.rs b/nodedb-crs/src/method/lcc.rs new file mode 100644 index 000000000..fc82e2dc8 --- /dev/null +++ b/nodedb-crs/src/method/lcc.rs @@ -0,0 +1,401 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Lambert Conformal Conic, PROJ `lcc`. +//! +//! Covers EPSG methods 9801 (1SP), 9802 (2SP), 9803 (2SP Belgium) and 9826 +//! (West Orientated, which differs only in axis order). Follows PROJ 9.8.1 +//! `src/projections/lcc.cpp` and IOGP Guidance Note 7-2 §3.2.1. PROJ +//! expresses 9803 through adjusted standard parallels, so it needs no +//! special case here. + +use core::f64::consts::{FRAC_PI_2, FRAC_PI_4}; +use core::fmt::Write; + +use serde::{Deserialize, Serialize}; + +use super::angle::wrap_pi; +use super::conformal::{msfn, phi_from_ts, tsfn}; +use crate::def::Ellipsoid; +use crate::error::{CrsParseError, DomainReason}; +use crate::parse::tokens::ProjParams; + +/// PROJ `lcc.cpp` EPS10: tolerance in radians for poles and degenerate +/// parallels. +const EPS10: f64 = 1e-10; + +/// Largest absolute latitude, degrees. +const MAX_LAT_DEG: f64 = 90.0; + +/// Method name in parse errors. +const METHOD: &str = "lcc"; + +/// Defining parameters of one Lambert Conformal Conic projection. The 1SP +/// form has `lat2_deg == lat1_deg`. +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Serialize, + Deserialize, + zerompk::ToMessagePack, + zerompk::FromMessagePack, +)] +pub struct LccParams { + /// Latitude of the false origin φ0, degrees. + pub lat0_deg: f64, + /// Longitude of the false origin λ0, degrees. + pub lon0_deg: f64, + /// First standard parallel φ1, degrees. + pub lat1_deg: f64, + /// Second standard parallel φ2, degrees. Equal to φ1 for the 1SP form. + pub lat2_deg: f64, + /// Scale factor k0 (on φ1 for the 1SP form). + pub k0: f64, + /// False easting, metres. + pub false_easting: f64, + /// False northing, metres. + pub false_northing: f64, +} + +impl LccParams { + /// Read from PROJ-string parameters. Without `+lat_2` the cone is + /// tangent at `+lat_1`, and `+lat_0` defaults to `+lat_1`, as in PROJ. + pub fn from_proj(p: &ProjParams<'_>) -> Result { + let lat1 = p.number("lat_1")?; + let lat2 = p.number("lat_2")?; + let (lat1_deg, lat2_deg, lat0_default) = match (lat1, lat2) { + (None, None) => { + return Err(CrsParseError::Missing { + key: "lat_1", + method: METHOD, + }); + } + (l1, Some(l2)) => (l1.unwrap_or(0.0), l2, 0.0), + (Some(l1), None) => (l1, l1, l1), + }; + let lat0_deg = p.number_or("lat_0", lat0_default)?; + if lat1_deg.abs() >= MAX_LAT_DEG { + return Err(CrsParseError::invalid("lat_1", &lat1_deg.to_string())); + } + if lat2_deg.abs() >= MAX_LAT_DEG { + return Err(CrsParseError::invalid("lat_2", &lat2_deg.to_string())); + } + if (lat1_deg + lat2_deg).to_radians().abs() < EPS10 { + return Err(CrsParseError::invalid("lat_2", &lat2_deg.to_string())); + } + if lat0_deg.abs() > MAX_LAT_DEG { + return Err(CrsParseError::invalid("lat_0", &lat0_deg.to_string())); + } + let k0 = p.number_any(&["k_0", "k"])?.unwrap_or(1.0); + if k0 <= 0.0 { + return Err(CrsParseError::invalid("k_0", &k0.to_string())); + } + Ok(Self { + lat0_deg, + lon0_deg: p.number_or("lon_0", 0.0)?, + lat1_deg, + lat2_deg, + k0, + false_easting: p.number_or("x_0", 0.0)?, + false_northing: p.number_or("y_0", 0.0)?, + }) + } + + /// Append ` +proj=lcc` and every defining parameter. + pub fn write_proj(&self, out: &mut String) { + let _ = write!( + out, + " +proj=lcc +lat_0={} +lon_0={} +lat_1={} +lat_2={} +k_0={} +x_0={} +y_0={}", + self.lat0_deg, + self.lon0_deg, + self.lat1_deg, + self.lat2_deg, + self.k0, + self.false_easting, + self.false_northing + ); + } +} + +/// A Lambert Conformal Conic projection with its constants computed. +#[derive(Debug, Clone, PartialEq)] +pub struct LambertConformalConic { + /// λ0 in radians. + lon0: f64, + e: f64, + sphere: bool, + /// Cone constant n. + n: f64, + /// a · k0 · F, the radius scale of the cone. + c: f64, + /// a · k0 · ρ0, the radius at the false origin. + rho0: f64, + false_easting: f64, + false_northing: f64, +} + +impl LambertConformalConic { + pub fn new(p: &LccParams, ell: &Ellipsoid) -> Self { + let phi0 = p.lat0_deg.to_radians(); + let phi1 = p.lat1_deg.to_radians(); + let phi2 = p.lat2_deg.to_radians(); + let (sin1, cos1) = phi1.sin_cos(); + let secant = (phi1 - phi2).abs() >= EPS10; + let at_pole = (phi0.abs() - FRAC_PI_2).abs() < EPS10; + let sphere = ell.is_sphere(); + let (e, e2) = (ell.e(), ell.e2()); + let (n, c, rho0) = if sphere { + // PROJ lcc.cpp spherical set-up, Snyder 15-3 and 15-2. + let n = if secant { + (cos1 / phi2.cos()).ln() + / ((FRAC_PI_4 + 0.5 * phi2).tan() / (FRAC_PI_4 + 0.5 * phi1).tan()).ln() + } else { + sin1 + }; + let c = cos1 * (FRAC_PI_4 + 0.5 * phi1).tan().powf(n) / n; + let rho0 = if at_pole { + 0.0 + } else { + c * (FRAC_PI_4 + 0.5 * phi0).tan().powf(-n) + }; + (n, c, rho0) + } else { + // GN 7-2 §3.2.1.1, Snyder 15-8 to 15-10. + let m1 = msfn(sin1, cos1, e2); + let t1 = tsfn(phi1, sin1, e); + let n = if secant { + let sin2 = phi2.sin(); + (m1 / msfn(sin2, phi2.cos(), e2)).ln() / (t1 / tsfn(phi2, sin2, e)).ln() + } else { + sin1 + }; + let c = m1 * t1.powf(-n) / n; + let rho0 = if at_pole { + 0.0 + } else { + c * tsfn(phi0, phi0.sin(), e).powf(n) + }; + (n, c, rho0) + }; + let scale = ell.a * p.k0; + Self { + lon0: p.lon0_deg.to_radians(), + e, + sphere, + n, + c: c * scale, + rho0: rho0 * scale, + false_easting: p.false_easting, + false_northing: p.false_northing, + } + } + + /// Project λ, φ radians to easting, northing metres. + pub fn forward(&self, lam: f64, phi: f64) -> Result<(f64, f64), DomainReason> { + let rho = if (phi.abs() - FRAC_PI_2).abs() < EPS10 { + // The pole opposite the cone apex maps to infinity. + if phi * self.n <= 0.0 { + return Err(DomainReason::Singularity); + } + 0.0 + } else if self.sphere { + self.c * (FRAC_PI_4 + 0.5 * phi).tan().powf(-self.n) + } else { + self.c * tsfn(phi, phi.sin(), self.e).powf(self.n) + }; + let theta = wrap_pi(lam - self.lon0) * self.n; + let (sin_t, cos_t) = theta.sin_cos(); + Ok(( + self.false_easting + rho * sin_t, + self.false_northing + self.rho0 - rho * cos_t, + )) + } + + /// Unproject easting, northing metres to λ, φ radians. + pub fn inverse(&self, x: f64, y: f64) -> Result<(f64, f64), DomainReason> { + let mut dx = x - self.false_easting; + let mut dy = self.rho0 - (y - self.false_northing); + let mut rho = dx.hypot(dy); + if rho == 0.0 { + let phi = if self.n > 0.0 { FRAC_PI_2 } else { -FRAC_PI_2 }; + return Ok((wrap_pi(self.lon0), phi)); + } + if self.n < 0.0 { + rho = -rho; + dx = -dx; + dy = -dy; + } + let phi = if self.sphere { + 2.0 * (self.c / rho).powf(1.0 / self.n).atan() - FRAC_PI_2 + } else { + phi_from_ts((rho / self.c).powf(1.0 / self.n), self.e)? + }; + let lam = dx.atan2(dy) / self.n; + Ok((wrap_pi(self.lon0 + lam), phi)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// One millimetre, the tolerance for every PROJ reference value. + const MM: f64 = 1e-3; + + /// Inverse tolerance in degrees. + const DEG_TOL: f64 = 1e-9; + + /// Metres per US survey foot. + const US_FT: f64 = 1200.0 / 3937.0; + + fn build(text: &str, ell: Ellipsoid) -> LambertConformalConic { + let p = LccParams::from_proj(&ProjParams::tokenize(text).unwrap()).unwrap(); + LambertConformalConic::new(&p, &ell) + } + + fn check(lcc: &LambertConformalConic, lon: f64, lat: f64, want: [f64; 2]) { + let (x, y) = lcc.forward(lon.to_radians(), lat.to_radians()).unwrap(); + assert!( + (x - want[0]).abs() < MM && (y - want[1]).abs() < MM, + "({lon}, {lat}): got ({x}, {y}), want {want:?}" + ); + // The references carry 0.1 mm, so the inverse starts from (x, y). + let (lam, phi) = lcc.inverse(x, y).unwrap(); + assert!( + (lam.to_degrees() - lon).abs() < DEG_TOL && (phi.to_degrees() - lat).abs() < DEG_TOL, + "inverse ({lon}, {lat}): got ({}, {})", + lam.to_degrees(), + phi.to_degrees() + ); + } + + fn dms(d: f64, m: f64, s: f64) -> f64 { + d + m / 60.0 + s / 3600.0 + } + + /// GN 7-2 §3.2.1.1 example, EPSG 9801: JAD69 / Jamaica National Grid. + /// GN 7-2 prints E = 255966.58 m, N = 142493.51 m. + #[test] + fn gn_9801_jamaica() { + let lcc = build( + "+proj=lcc +lat_1=18 +lat_0=18 +lon_0=-77 +k_0=1 +x_0=250000 +y_0=150000", + Ellipsoid::CLARKE_1866, + ); + let lon = -dms(76.0, 56.0, 37.26); + let lat = dms(17.0, 55.0, 55.80); + let (x, y) = lcc.forward(lon.to_radians(), lat.to_radians()).unwrap(); + assert!((x - 255_966.58).abs() < 0.005 && (y - 142_493.51).abs() < 0.005); + check(&lcc, lon, lat, [255_966.581_8, 142_493.511_0]); + } + + /// GN 7-2 §3.2.1.1 example, EPSG 9802: NAD27 / Texas South Central. + /// GN 7-2 prints E = 2963503.91 ftUS, N = 254759.80 ftUS. + #[test] + fn gn_9802_texas() { + let lcc = build( + "+proj=lcc +lat_0=27.8333333333333 +lon_0=-99 +lat_1=28.3833333333333 \ + +lat_2=30.2833333333333 +x_0=609601.218950438 +y_0=0", + Ellipsoid::CLARKE_1866, + ); + let (x, y) = lcc + .forward((-96f64).to_radians(), 28.5f64.to_radians()) + .unwrap(); + assert!((x / US_FT - 2_963_503.91).abs() < 0.01); + assert!((y / US_FT - 254_759.80).abs() < 0.01); + check(&lcc, -96.0, 28.5, [903_277.798_9, 77_650.942_5]); + } + + /// EPSG 9803, Belgian Lambert 72 (EPSG:31370) as PROJ writes it. GN 7-2 + /// §3.2.1.2 prints E = 251763.20, N = 153034.13 for this point. PROJ's + /// adjusted parallels give N 4.6 cm higher, and the reference is PROJ. + #[test] + fn belgium_72_matches_proj() { + let lcc = build( + "+proj=lcc +lat_0=90 +lon_0=4.36748666666667 +lat_1=51.1666672333333 \ + +lat_2=49.8333339 +x_0=150000.013 +y_0=5400088.438", + Ellipsoid::INTERNATIONAL_1924, + ); + check( + &lcc, + dms(5.0, 48.0, 26.533), + dms(50.0, 40.0, 46.461), + [251_763.205_0, 153_034.175_7], + ); + check(&lcc, 4.3, 50.8, [145_242.297_7, 165_446.068_1]); + check(&lcc, 2.6, 51.3, [26_730.420_1, 222_536.757_0]); + check(&lcc, 6.3, 49.6, [289_697.742_3, 33_782.385_3]); + } + + /// pyproj 3.8.0 / PROJ 9.8.1, "+proj=lcc +lat_0=46.5 +lon_0=3 +lat_1=49 + /// +lat_2=44 +x_0=700000 +y_0=6600000 +ellps=GRS80" (Lambert-93). + #[test] + fn lambert_93() { + let lcc = build( + "+proj=lcc +lat_0=46.5 +lon_0=3 +lat_1=49 +lat_2=44 +x_0=700000 +y_0=6600000", + Ellipsoid::GRS80, + ); + check(&lcc, 2.35, 48.85, [652_301.564_8, 6_861_302.725_9]); + check(&lcc, -4.5, 48.4, [145_709.788_9, 6_837_422.082_6]); + check(&lcc, 7.7, 43.7, [1_078_729.648_2, 6_300_271.150_7]); + } + + /// pyproj, "+proj=lcc +lat_1=-30 +lat_0=-30 +lon_0=25 +k_0=0.9995 + /// +x_0=1000000 +y_0=2000000 +ellps=WGS84": a southern cone, n < 0. + #[test] + fn southern_1sp_with_scale() { + let lcc = build( + "+proj=lcc +lat_1=-30 +lat_0=-30 +lon_0=25 +k_0=0.9995 +x_0=1000000 +y_0=2000000", + Ellipsoid::WGS84, + ); + check(&lcc, 20.0, -25.0, [493_776.987_9, 2_543_428.162_0]); + check(&lcc, 30.0, -35.0, [1_457_832.233_9, 1_435_097.123_5]); + } + + /// pyproj, "+proj=lcc +lat_1=33 +lat_2=45 +lat_0=23 +lon_0=-96 + /// +R=6370997". + #[test] + fn sphere_secant() { + let lcc = build( + "+proj=lcc +lat_1=33 +lat_2=45 +lat_0=23 +lon_0=-96 +x_0=0 +y_0=0", + Ellipsoid::sphere(6_370_997.0), + ); + check(&lcc, -75.0, 40.0, [1_763_412.764_0, 2_107_212.051_0]); + check(&lcc, -120.0, 35.0, [-2_154_255.795_6, 1_635_373.287_8]); + } + + #[test] + fn opposite_pole_is_singular() { + let lcc = build( + "+proj=lcc +lat_1=49 +lat_2=44 +lat_0=46.5", + Ellipsoid::GRS80, + ); + assert_eq!(lcc.forward(0.0, -FRAC_PI_2), Err(DomainReason::Singularity)); + assert!(lcc.forward(0.0, FRAC_PI_2).is_ok()); + } + + #[test] + fn parse_defaults_and_round_trip() { + let p = LccParams::from_proj(&ProjParams::tokenize("+proj=lcc +lat_1=20 +k=0.99").unwrap()) + .unwrap(); + assert_eq!((p.lat0_deg, p.lat2_deg, p.k0), (20.0, 20.0, 0.99)); + let mut out = String::new(); + p.write_proj(&mut out); + let again = LccParams::from_proj(&ProjParams::tokenize(&out).unwrap()).unwrap(); + assert_eq!(again, p, "{out}"); + assert_eq!( + LccParams::from_proj(&ProjParams::tokenize("+proj=lcc +lat_0=10").unwrap()), + Err(CrsParseError::Missing { + key: "lat_1", + method: "lcc" + }) + ); + assert!( + LccParams::from_proj(&ProjParams::tokenize("+proj=lcc +lat_1=10 +lat_2=-10").unwrap()) + .is_err() + ); + assert!( + LccParams::from_proj(&ProjParams::tokenize("+proj=lcc +lat_1=90").unwrap()).is_err() + ); + } +} diff --git a/nodedb-crs/src/method/merc.rs b/nodedb-crs/src/method/merc.rs new file mode 100644 index 000000000..150ddeeef --- /dev/null +++ b/nodedb-crs/src/method/merc.rs @@ -0,0 +1,268 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Mercator, PROJ `merc`. +//! +//! Covers EPSG methods 9804 (variant A, scale factor k0) and 9805 (variant +//! B, standard parallel). Follows PROJ 9.8.1 `src/projections/merc.cpp` and +//! IOGP Guidance Note 7-2 §3.5.1, with the spherical branch PROJ uses for +//! `+R` definitions. + +use core::f64::consts::FRAC_PI_2; +use core::fmt::Write; + +use serde::{Deserialize, Serialize}; + +use super::angle::wrap_pi; +use super::conformal::{msfn, tanphi_from_sinhpsi}; +use crate::def::Ellipsoid; +use crate::error::{CrsParseError, DomainReason}; +use crate::parse::tokens::ProjParams; + +/// Tolerance in radians for a latitude at a pole, where y is infinite. +const POLE_EPS: f64 = 1e-10; + +/// Largest absolute standard parallel, degrees, exclusive (PROJ merc.cpp). +const MAX_LAT_TS_DEG: f64 = 90.0; + +/// Defining parameters of one Mercator projection. +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Serialize, + Deserialize, + zerompk::ToMessagePack, + zerompk::FromMessagePack, +)] +pub struct MercParams { + /// Longitude of natural origin λ0, degrees. + pub lon0_deg: f64, + /// Scale factor on the equator (variant A). Ignored when `lat_ts_deg` + /// is set, as in PROJ. + pub k0: f64, + /// Latitude of the standard parallel φ1, degrees (variant B). The scale + /// factor then follows from it. + pub lat_ts_deg: Option, + /// False easting, metres. + pub false_easting: f64, + /// False northing, metres. + pub false_northing: f64, +} + +impl MercParams { + /// Read from PROJ-string parameters. + pub fn from_proj(p: &ProjParams<'_>) -> Result { + let lat_ts_deg = p.number("lat_ts")?; + if let Some(lat_ts) = lat_ts_deg + && lat_ts.abs() >= MAX_LAT_TS_DEG + { + return Err(CrsParseError::invalid("lat_ts", &lat_ts.to_string())); + } + let k0 = p.number_any(&["k_0", "k"])?.unwrap_or(1.0); + if k0 <= 0.0 { + return Err(CrsParseError::invalid("k_0", &k0.to_string())); + } + Ok(Self { + lon0_deg: p.number_or("lon_0", 0.0)?, + k0, + lat_ts_deg, + false_easting: p.number_or("x_0", 0.0)?, + false_northing: p.number_or("y_0", 0.0)?, + }) + } + + /// Append ` +proj=merc` and every defining parameter. + pub fn write_proj(&self, out: &mut String) { + out.push_str(" +proj=merc"); + let _ = match self.lat_ts_deg { + Some(lat_ts) => write!(out, " +lat_ts={lat_ts}"), + None => write!(out, " +k_0={}", self.k0), + }; + let _ = write!( + out, + " +lon_0={} +x_0={} +y_0={}", + self.lon0_deg, self.false_easting, self.false_northing + ); + } +} + +/// A Mercator projection with its constants computed. +#[derive(Debug, Clone, PartialEq)] +pub struct Mercator { + /// λ0 in radians. + lon0: f64, + e: f64, + sphere: bool, + /// a · k0. + ak0: f64, + false_easting: f64, + false_northing: f64, +} + +impl Mercator { + pub fn new(p: &MercParams, ell: &Ellipsoid) -> Self { + let sphere = ell.is_sphere(); + // GN 7-2 §3.5.1.2: k0 = cos φ1 / sqrt(1 − e² sin² φ1) for variant B. + let k0 = match p.lat_ts_deg { + Some(lat_ts) => { + let (s, c) = lat_ts.abs().to_radians().sin_cos(); + if sphere { c } else { msfn(s, c, ell.e2()) } + } + None => p.k0, + }; + Self { + lon0: p.lon0_deg.to_radians(), + e: ell.e(), + sphere, + ak0: ell.a * k0, + false_easting: p.false_easting, + false_northing: p.false_northing, + } + } + + /// Project λ, φ radians to easting, northing metres. + pub fn forward(&self, lam: f64, phi: f64) -> Result<(f64, f64), DomainReason> { + if (phi.abs() - FRAC_PI_2).abs() < POLE_EPS { + return Err(DomainReason::Singularity); + } + let x = self.ak0 * wrap_pi(lam - self.lon0); + // ψ = asinh(tan φ) − e atanh(e sin φ), PROJ merc_e_forward. + let psi = if self.sphere { + phi.tan().asinh() + } else { + let (s, c) = phi.sin_cos(); + (s / c).asinh() - self.e * (self.e * s).atanh() + }; + Ok((self.false_easting + x, self.false_northing + self.ak0 * psi)) + } + + /// Unproject easting, northing metres to λ, φ radians. + pub fn inverse(&self, x: f64, y: f64) -> Result<(f64, f64), DomainReason> { + let sinh_psi = ((y - self.false_northing) / self.ak0).sinh(); + let phi = if self.sphere { + sinh_psi.atan() + } else { + tanphi_from_sinhpsi(sinh_psi, self.e)?.atan() + }; + let lam = (x - self.false_easting) / self.ak0; + Ok((wrap_pi(self.lon0 + lam), phi)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// One millimetre, the tolerance for every PROJ reference value. + const MM: f64 = 1e-3; + + /// Inverse tolerance in degrees. + const DEG_TOL: f64 = 1e-9; + + fn build(text: &str, ell: Ellipsoid) -> Mercator { + let p = MercParams::from_proj(&ProjParams::tokenize(text).unwrap()).unwrap(); + Mercator::new(&p, &ell) + } + + fn check(m: &Mercator, lon: f64, lat: f64, want: [f64; 2]) { + let (x, y) = m.forward(lon.to_radians(), lat.to_radians()).unwrap(); + assert!( + (x - want[0]).abs() < MM && (y - want[1]).abs() < MM, + "({lon}, {lat}): got ({x}, {y}), want {want:?}" + ); + // The references carry 0.1 mm, so the inverse starts from (x, y). + let (lam, phi) = m.inverse(x, y).unwrap(); + assert!( + (lam.to_degrees() - lon).abs() < DEG_TOL && (phi.to_degrees() - lat).abs() < DEG_TOL, + "inverse ({lon}, {lat}): got ({}, {})", + lam.to_degrees(), + phi.to_degrees() + ); + } + + /// GN 7-2 §3.5.1.1 example, EPSG 9804: Batavia / NEIEZ. GN 7-2 prints + /// E = 5009726.58 m, N = 569150.82 m. + #[test] + fn gn_9804_batavia() { + let m = build( + "+proj=merc +lon_0=110 +k=0.997 +x_0=3900000 +y_0=900000", + Ellipsoid::BESSEL_1841, + ); + let (x, y) = m + .forward(120f64.to_radians(), (-3f64).to_radians()) + .unwrap(); + assert!((x - 5_009_726.58).abs() < 0.005 && (y - 569_150.82).abs() < 0.005); + check(&m, 120.0, -3.0, [5_009_726.583_3, 569_150.818_6]); + check(&m, 100.0, 5.0, [2_790_273.416_7, 1_451_870.161_7]); + } + + /// GN 7-2 §3.5.1.2 example, EPSG 9805: Pulkovo 1942 / Caspian Sea + /// Mercator. GN 7-2 prints E = 165704.29 m, N = 5171848.07 m. + #[test] + fn gn_9805_caspian() { + let m = build( + "+proj=merc +lat_ts=42 +lon_0=51 +x_0=0 +y_0=0", + Ellipsoid::KRASSOVSKY, + ); + let (x, y) = m.forward(53f64.to_radians(), 53f64.to_radians()).unwrap(); + assert!((x - 165_704.29).abs() < 0.005 && (y - 5_171_848.07).abs() < 0.005); + check(&m, 53.0, 53.0, [165_704.293_3, 5_171_848.072_9]); + check(&m, 47.0, 38.0, [-331_408.586_6, 3_388_766.544_7]); + } + + /// pyproj 3.8.0 / PROJ 9.8.1, "+proj=merc +lat_ts=-41 +lon_0=100 + /// +datum=WGS84" (EPSG:3994). Longitudes cross the antimeridian. + #[test] + fn southern_standard_parallel_across_antimeridian() { + let m = build( + "+proj=merc +lat_ts=-41 +lon_0=100 +x_0=0 +y_0=0", + Ellipsoid::WGS84, + ); + check(&m, 175.0, -41.0, [6_310_138.883_6, -3_767_131.992_2]); + check(&m, -170.0, -30.0, [7_572_166.660_3, -2_631_835.819_7]); + } + + /// pyproj, "+proj=merc +lon_0=0 +k=1 +datum=WGS84" (EPSG:3395). + #[test] + fn world_mercator() { + let m = build("+proj=merc +lon_0=0 +k=1 +x_0=0 +y_0=0", Ellipsoid::WGS84); + check(&m, 101.7117, 3.1579, [11_322_494.651_7, 349_361.795_7]); + check(&m, -60.0, -75.0, [-6_679_169.447_6, -12_890_914.137_3]); + } + + /// pyproj, "+proj=merc +lat_ts=20 +lon_0=10 +R=6371000". + #[test] + fn sphere_with_standard_parallel() { + let m = build( + "+proj=merc +lat_ts=20 +lon_0=10", + Ellipsoid::sphere(6_371_000.0), + ); + check(&m, 30.0, 60.0, [2_089_781.040_7, 7_884_339.419_9]); + check(&m, -10.0, -40.0, [-2_089_781.040_7, -4_567_373.533_8]); + } + + #[test] + fn pole_is_singular() { + let m = build("+proj=merc", Ellipsoid::WGS84); + assert_eq!(m.forward(0.0, FRAC_PI_2), Err(DomainReason::Singularity)); + } + + #[test] + fn parse_and_round_trip() { + for text in [ + "+proj=merc +lat_ts=-41 +lon_0=100", + "+proj=merc +lon_0=110 +k=0.997 +x_0=3900000", + ] { + let p = MercParams::from_proj(&ProjParams::tokenize(text).unwrap()).unwrap(); + let mut out = String::new(); + p.write_proj(&mut out); + let again = MercParams::from_proj(&ProjParams::tokenize(&out).unwrap()).unwrap(); + assert_eq!(again, p, "{out}"); + } + assert!( + MercParams::from_proj(&ProjParams::tokenize("+proj=merc +lat_ts=90").unwrap()).is_err() + ); + assert!(MercParams::from_proj(&ProjParams::tokenize("+proj=merc +k=0").unwrap()).is_err()); + } +} diff --git a/nodedb-crs/src/method/meridian.rs b/nodedb-crs/src/method/meridian.rs new file mode 100644 index 000000000..17bf1fb2a --- /dev/null +++ b/nodedb-crs/src/method/meridian.rs @@ -0,0 +1,189 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Meridian arc length and its inverse, as PROJ `pj_enfn` / `pj_mlfn` / +//! `pj_inv_mlfn` (PROJ 9.8.1 `src/mlfn.cpp`, `src/latitudes.cpp`). +//! +//! The arc is A · μ, with A the rectifying radius over a and μ the +//! rectifying latitude. Both conversions φ ↔ μ are Fourier series whose +//! coefficients are Taylor series in the third flattening n to order 6 +//! (C. F. F. Karney, "On auxiliary latitudes", Survey Review 56 (2024), +//! eqs. A1–A28). Full double precision for |f| ≤ 1/150. + +use crate::error::DomainReason; + +/// Number of Fourier terms, PROJ `AuxLat::ORDER`. +pub(crate) const AUX_ORDER: usize = 6; + +/// Karney 2024 matrix C[μ,φ] (φ → μ), even powers of n only, as PROJ +/// `pj_auxlat_coeffs`. +const C_MU_PHI: [f64; 12] = [ + -3.0 / 2.0, + 9.0 / 16.0, + -3.0 / 32.0, + 15.0 / 16.0, + -15.0 / 32.0, + 135.0 / 2048.0, + -35.0 / 48.0, + 105.0 / 256.0, + 315.0 / 512.0, + -189.0 / 512.0, + -693.0 / 1280.0, + 1001.0 / 2048.0, +]; + +/// Karney 2024 matrix C[φ,μ] (μ → φ), even powers of n only. +const C_PHI_MU: [f64; 12] = [ + 3.0 / 2.0, + -27.0 / 32.0, + 269.0 / 512.0, + 21.0 / 16.0, + -55.0 / 32.0, + 6759.0 / 4096.0, + 151.0 / 96.0, + -417.0 / 128.0, + 1097.0 / 512.0, + -15543.0 / 2560.0, + 8011.0 / 2560.0, + 293_393.0 / 61440.0, +]; + +/// Rectifying-radius series in n², ((2k − 3)!! / (2k)!!)², PROJ +/// `pj_rectifying_radius`. +const RECTIFYING_RADIUS: [f64; 4] = [1.0, 1.0 / 4.0, 1.0 / 64.0, 1.0 / 256.0]; + +/// Meridian arc length from the equator (PROJ pj_enfn / pj_mlfn / pj_inv_mlfn). +#[derive(Debug, Clone, PartialEq)] +pub struct MeridianArc { + /// Rectifying radius over a. + radius: f64, + /// φ → μ Fourier coefficients. + to_mu: [f64; AUX_ORDER], + /// μ → φ Fourier coefficients. + from_mu: [f64; AUX_ORDER], +} + +impl MeridianArc { + pub fn new(e2: f64) -> Self { + let n = third_flattening(e2); + Self { + radius: polyval(n * n, &RECTIFYING_RADIUS) / (1.0 + n), + to_mu: even_series(n, &C_MU_PHI), + from_mu: even_series(n, &C_PHI_MU), + } + } + + /// Arc length in units of the semi-major axis. + pub fn arc(&self, phi: f64, sinphi: f64, cosphi: f64) -> f64 { + self.radius * (phi + clenshaw(sinphi, cosphi, &self.to_mu)) + } + + /// Latitude of an arc length. Err(NoConvergence) if it fails. + /// + /// The series inverse is closed-form, so the only failure is a + /// non-finite input. + pub fn inverse(&self, arc: f64) -> Result { + let mu = arc / self.radius; + let phi = mu + clenshaw(mu.sin(), mu.cos(), &self.from_mu); + if phi.is_finite() { + Ok(phi) + } else { + Err(DomainReason::NoConvergence) + } + } +} + +/// Third flattening n = (a − b)/(a + b) from e², written e² / (1 + √(1 − e²))² +/// to keep precision for small e². +pub(crate) fn third_flattening(e2: f64) -> f64 { + let s = 1.0 + (1.0 - e2).sqrt(); + e2 / (s * s) +} + +/// Σ p[i] xⁱ by Horner's method, PROJ `pj_polyval`. +pub(crate) fn polyval(x: f64, p: &[f64]) -> f64 { + match p.split_last() { + None => 0.0, + Some((last, rest)) => rest.iter().rev().fold(*last, |y, c| y * x + c), + } +} + +/// Fourier coefficients F[l] = n^(l+1) · P_l(n²) for a conversion among φ +/// and μ, PROJ `pj_auxlat_coeffs` (the n² branch). +fn even_series(n: f64, coeffs: &[f64; 12]) -> [f64; AUX_ORDER] { + let n2 = n * n; + let mut out = [0.0; AUX_ORDER]; + let mut d = n; + let mut o = 0; + for (l, slot) in out.iter_mut().enumerate() { + let m = (AUX_ORDER - l - 1) / 2; + *slot = d * polyval(n2, &coeffs[o..=o + m]); + o += m + 1; + d *= n; + } + out +} + +/// Fourier coefficients F[l] = n^(l+1) · P_l(n) for a conversion that +/// involves the authalic or conformal latitude, PROJ `pj_auxlat_coeffs`. +pub(crate) fn full_series(n: f64, coeffs: &[f64; 21]) -> [f64; AUX_ORDER] { + let mut out = [0.0; AUX_ORDER]; + let mut d = n; + let mut o = 0; + for (l, slot) in out.iter_mut().enumerate() { + let m = AUX_ORDER - l - 1; + *slot = d * polyval(n, &coeffs[o..=o + m]); + o += m + 1; + d *= n; + } + out +} + +/// Σ F[k] sin((2k + 2) ζ) by Clenshaw summation, PROJ `pj_clenshaw`. +pub(crate) fn clenshaw(szeta: f64, czeta: f64, f: &[f64; AUX_ORDER]) -> f64 { + let x = 2.0 * (czeta - szeta) * (czeta + szeta); + let (mut u0, mut u1) = (0.0, 0.0); + for c in f.iter().rev() { + let t = x * u0 - u1 + c; + u1 = u0; + u0 = t; + } + 2.0 * szeta * czeta * u0 +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::def::Ellipsoid; + use core::f64::consts::FRAC_PI_2; + + /// GRS 80 quarter meridian, 10 001 965.729 m (Moritz, Geodetic Reference + /// System 1980, Bulletin Géodésique 54 (1980)). + const GRS80_QUARTER_MERIDIAN_M: f64 = 10_001_965.729_3; + + #[test] + fn grs80_quarter_meridian() { + let e = Ellipsoid::GRS80; + let m = MeridianArc::new(e.e2()); + let q = m.arc(FRAC_PI_2, 1.0, 0.0) * e.a; + assert!((q - GRS80_QUARTER_MERIDIAN_M).abs() < 1e-3, "{q}"); + } + + #[test] + fn inverse_recovers_latitude() { + let m = MeridianArc::new(Ellipsoid::WGS84.e2()); + for deg in [-89.9, -45.0, -3.0, 0.0, 2.121_679_744, 33.3, 80.0, 90.0] { + let phi = f64::to_radians(deg); + let s = m.arc(phi, phi.sin(), phi.cos()); + let back = m.inverse(s).unwrap(); + assert!((back - phi).abs() < 1e-15, "{deg}"); + } + } + + #[test] + fn sphere_arc_is_latitude() { + let m = MeridianArc::new(0.0); + assert_eq!(m.arc(0.3, 0.3f64.sin(), 0.3f64.cos()), 0.3); + assert_eq!(m.inverse(0.3), Ok(0.3)); + assert_eq!(m.inverse(f64::NAN), Err(DomainReason::NoConvergence)); + } +} diff --git a/nodedb-crs/src/method/mod.rs b/nodedb-crs/src/method/mod.rs new file mode 100644 index 000000000..0dc120913 --- /dev/null +++ b/nodedb-crs/src/method/mod.rs @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Map projection methods. + +pub mod aea; +pub mod aeqd; +pub mod angle; +pub mod authalic; +pub mod cass; +pub mod cea; +pub mod conformal; +pub mod eqc; +pub mod eqearth; +pub mod geodesic; +pub mod geos; +pub mod krovak; +pub mod labrd; +pub mod laea; +pub mod lcc; +pub mod merc; +pub mod meridian; +pub mod nzmg; +pub mod omerc; +pub mod poly; +pub mod projection; +pub mod projector; +pub mod sinu; +pub mod somerc; +pub mod stere; +pub mod sterea; +pub mod tmerc; +pub mod webmerc; + +pub use aea::{AeaParams, Albers}; +pub use aeqd::{AeqdParams, AzimuthalEquidistant}; +pub use cass::{CassParams, Cassini}; +pub use cea::{CeaParams, CylindricalEqualArea}; +pub use eqc::{EqcParams, EquidistantCylindrical}; +pub use eqearth::{EqEarthParams, EqualEarth}; +pub use geos::{GeosParams, GeostationarySatellite, SweepAxis}; +pub use krovak::{Krovak, KrovakParams}; +pub use labrd::{LabordeObliqueMercator, LabrdParams}; +pub use laea::{LaeaParams, LambertAzimuthalEqualArea}; +pub use lcc::{LambertConformalConic, LccParams}; +pub use merc::{MercParams, Mercator}; +pub use nzmg::{NewZealandMapGrid, NzmgParams}; +pub use omerc::{HomParams, HomVariant, HotineObliqueMercator, alpha_from_gamma, gamma_from_alpha}; +pub use poly::{PolyParams, Polyconic}; +pub use projection::ProjectionDef; +pub use projector::Projector; +pub use sinu::{SinuParams, Sinusoidal}; +pub use somerc::{SomercParams, SwissObliqueMercator}; +pub use stere::{StereParams, Stereographic}; +pub use sterea::{ObliqueStereographic, StereaParams}; +pub use tmerc::{ + TAU_NEWTON_MAX_ITERS, TAU_TOLERANCE, TM_MAX_LON_OFFSET_DEG, TmParams, TransverseMercator, +}; +pub use webmerc::{WebMercParams, WebMercator}; diff --git a/nodedb-crs/src/method/nzmg.rs b/nodedb-crs/src/method/nzmg.rs new file mode 100644 index 000000000..3bad2407e --- /dev/null +++ b/nodedb-crs/src/method/nzmg.rs @@ -0,0 +1,293 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! New Zealand Map Grid (EPSG 9811), by PROJ `src/projections/nzmg.cpp`. +//! +//! A complex-polynomial conformal projection fitted to the International +//! 1924 ellipsoid (Stirling, Technical Circular 1973/32; IOGP Guidance Note +//! 7-2 §3.4.1). PROJ fixes a = 6378388 m, φ0 = 41°S, λ0 = 173°E, FE = +//! 2510000 m and FN = 6023150 m, ignoring the PROJ-string values, and so +//! does this file. + +use core::fmt::Write; + +use serde::{Deserialize, Serialize}; + +use super::angle::wrap_pi; +use crate::def::Ellipsoid; +use crate::error::{CrsParseError, DomainReason}; +use crate::parse::tokens::ProjParams; + +/// International 1924 semi-major axis, metres, as PROJ fixes it. +const NZMG_A: f64 = 6_378_388.0; + +/// Latitude of origin φ0, degrees. +pub const NZMG_LAT0_DEG: f64 = -41.0; + +/// Longitude of origin λ0, degrees. +pub const NZMG_LON0_DEG: f64 = 173.0; + +/// False easting, metres. +pub const NZMG_FALSE_EASTING: f64 = 2_510_000.0; + +/// False northing, metres. +pub const NZMG_FALSE_NORTHING: f64 = 6_023_150.0; + +/// Arc-seconds in one radian. +const ARCSEC_PER_RAD: f64 = 180.0 * 3600.0 / core::f64::consts::PI; + +/// Arc-seconds in one series unit: the series take δφ in 10⁵ arc-seconds. +const ARCSEC_PER_SERIES_UNIT: f64 = 1e5; + +/// Radians to series units (PROJ `RAD_TO_SEC5`). +const RAD_TO_SEC5: f64 = ARCSEC_PER_RAD / ARCSEC_PER_SERIES_UNIT; + +/// Series units to radians (PROJ `SEC5_TO_RAD`). +const SEC5_TO_RAD: f64 = ARCSEC_PER_SERIES_UNIT / ARCSEC_PER_RAD; + +/// Complex coefficients B1..B6 of the ζ → z polynomial (PROJ `bf`). +const BF: [(f64, f64); 6] = [ + (0.755_785_322_8, 0.0), + (0.249_204_646, 0.003_371_507), + (-0.001_541_739, 0.041_058_560), + (-0.101_629_07, 0.017_276_09), + (-0.266_234_89, -0.362_492_18), + (-0.687_098_3, -1.165_196_7), +]; + +/// Coefficients of the latitude → isometric latitude series (PROJ `tpsi`). +const TPSI: [f64; 10] = [ + 0.639_917_507_3, + -0.135_879_761_3, + 0.063_294_409, + -0.025_268_53, + 0.011_787_9, + -0.005_516_1, + 0.002_690_6, + -0.001_333, + 0.000_67, + -0.000_34, +]; + +/// Coefficients of the isometric latitude → latitude series (PROJ `tphi`). +const TPHI: [f64; 9] = [ + 1.562_701_424_3, + 0.518_540_639_8, + -0.033_330_98, + -0.105_290_6, + -0.036_859_4, + 0.007_317, + 0.012_20, + 0.003_94, + -0.001_3, +]; + +/// Newton step size, |Δre| + |Δim| in units of a, below which the inverse +/// stops (PROJ `EPSLN`). +const NEWTON_TOLERANCE: f64 = 1e-10; + +/// Newton iterations allowed (PROJ). +const NEWTON_MAX_ITERS: usize = 20; + +/// Defining parameters of the New Zealand Map Grid. [`NzmgParams::from_proj`] +/// always yields the fixed NZMG values, as PROJ does. +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Serialize, + Deserialize, + zerompk::ToMessagePack, + zerompk::FromMessagePack, +)] +pub struct NzmgParams { + /// Latitude of origin φ0, degrees. + pub lat0_deg: f64, + /// Longitude of origin λ0, degrees. + pub lon0_deg: f64, + /// False easting, metres. + pub false_easting: f64, + /// False northing, metres. + pub false_northing: f64, +} + +impl NzmgParams { + /// The fixed NZMG parameters. + pub const NZMG: NzmgParams = NzmgParams { + lat0_deg: NZMG_LAT0_DEG, + lon0_deg: NZMG_LON0_DEG, + false_easting: NZMG_FALSE_EASTING, + false_northing: NZMG_FALSE_NORTHING, + }; + + /// Read from PROJ-string parameters. PROJ overrides `+lat_0`, `+lon_0`, + /// `+x_0` and `+y_0` with the NZMG values, so this returns + /// [`NzmgParams::NZMG`] for any input. + pub fn from_proj(_p: &ProjParams<'_>) -> Result { + Ok(Self::NZMG) + } + + /// Append the PROJ form of the defining parameters. + pub fn write_proj(&self, out: &mut String) { + let _ = write!( + out, + " +proj=nzmg +lat_0={} +lon_0={} +x_0={} +y_0={}", + self.lat0_deg, self.lon0_deg, self.false_easting, self.false_northing + ); + } +} + +/// The New Zealand Map Grid with its origin prepared. +#[derive(Debug, Clone, PartialEq)] +pub struct NewZealandMapGrid { + params: NzmgParams, + /// φ0, radians. + lat0: f64, + /// λ0, radians. + lon0: f64, +} + +impl NewZealandMapGrid { + /// Prepare the origin. The ellipsoid is ignored: PROJ fixes a = 6378388 m. + pub fn new(p: &NzmgParams, _ell: &Ellipsoid) -> Self { + Self { + params: *p, + lat0: p.lat0_deg.to_radians(), + lon0: p.lon0_deg.to_radians(), + } + } + + /// Project λ, φ radians to easting, northing metres. + pub fn forward(&self, lam: f64, phi: f64) -> Result<(f64, f64), DomainReason> { + let dphi = (phi - self.lat0) * RAD_TO_SEC5; + // δψ = δφ Σ tpsi[k] δφ^k, Horner form. + let dpsi = dphi * TPSI.iter().rev().fold(0.0, |acc, c| c + dphi * acc); + let zeta = (dpsi, wrap_pi(lam - self.lon0)); + let (re, im) = zpoly(zeta); + Ok(( + NZMG_A * im + self.params.false_easting, + NZMG_A * re + self.params.false_northing, + )) + } + + /// Unproject easting, northing metres to λ, φ radians. + pub fn inverse(&self, x: f64, y: f64) -> Result<(f64, f64), DomainReason> { + let target = ( + (y - self.params.false_northing) / NZMG_A, + (x - self.params.false_easting) / NZMG_A, + ); + // Newton's method on f(ζ) = z, started at ζ = z. + let mut zeta = target; + for _ in 0..NEWTON_MAX_ITERS { + let (f, fp) = zpoly_with_derivative(zeta); + let resid = (f.0 - target.0, f.1 - target.1); + let den = fp.0 * fp.0 + fp.1 * fp.1; + let step = ( + -(resid.0 * fp.0 + resid.1 * fp.1) / den, + -(resid.1 * fp.0 - resid.0 * fp.1) / den, + ); + zeta = (zeta.0 + step.0, zeta.1 + step.1); + if step.0.abs() + step.1.abs() <= NEWTON_TOLERANCE { + let dpsi = zeta.0; + let series = TPHI.iter().rev().fold(0.0, |acc, c| c + dpsi * acc); + let phi = self.lat0 + dpsi * series * SEC5_TO_RAD; + return Ok((self.lon0 + zeta.1, phi)); + } + } + Err(DomainReason::NoConvergence) + } +} + +/// Complex product. +fn cmul(a: (f64, f64), b: (f64, f64)) -> (f64, f64) { + (a.0 * b.0 - a.1 * b.1, a.0 * b.1 + a.1 * b.0) +} + +/// z = Σ BF[k] ζ^(k+1), Horner form (PROJ `pj_zpoly1`). +fn zpoly(zeta: (f64, f64)) -> (f64, f64) { + let inner = BF.iter().rev().fold((0.0, 0.0), |acc, c| { + let m = cmul(acc, zeta); + (c.0 + m.0, c.1 + m.1) + }); + cmul(inner, zeta) +} + +/// z and dz/dζ = Σ (k+1) BF[k] ζ^k (PROJ `pj_zpolyd1`). +fn zpoly_with_derivative(zeta: (f64, f64)) -> ((f64, f64), (f64, f64)) { + let deriv = BF.iter().enumerate().rev().fold((0.0, 0.0), |acc, (k, c)| { + let m = cmul(acc, zeta); + let scale = (k + 1) as f64; + (scale * c.0 + m.0, scale * c.1 + m.1) + }); + (zpoly(zeta), deriv) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// One millimetre, the tolerance for PROJ reference values. + const MM: f64 = 1e-3; + + /// Inverse tolerance, degrees. + const DEG_TOL: f64 = 1e-9; + + /// PROJ 9.8.1 (pyproj 3.8.0): `+proj=nzmg +lat_0=-41 +lon_0=173 + /// +x_0=2510000 +y_0=6023150 +ellps=intl`. GN 7-2 §3.4.1 prints no + /// worked example for this method. + const PROJ_NZMG: [([f64; 2], [f64; 2]); 4] = [ + ( + [174.7633, -36.8485], + [2_667_665.932_422_868, 6_482_380.313_793_957], + ), + ( + [172.6362, -43.5321], + [2_480_614.526_881_576, 5_741_827.025_504_868_5], + ), + ( + [168.35, -46.41], + [2_152_383.175_675_591_5, 5_412_054.517_847_946], + ), + ( + [178.0, -38.6], + [2_945_419.582_789_707_5, 6_277_359.260_284_454], + ), + ]; + + #[test] + fn matches_proj() { + let g = NewZealandMapGrid::new(&NzmgParams::NZMG, &Ellipsoid::INTERNATIONAL_1924); + for (ll, xy) in PROJ_NZMG { + let (x, y) = g.forward(ll[0].to_radians(), ll[1].to_radians()).unwrap(); + assert!( + (x - xy[0]).abs() < MM && (y - xy[1]).abs() < MM, + "{ll:?}: got {x}, {y}" + ); + let (lam, phi) = g.inverse(xy[0], xy[1]).unwrap(); + assert!((lam.to_degrees() - ll[0]).abs() < DEG_TOL, "{ll:?}"); + assert!((phi.to_degrees() - ll[1]).abs() < DEG_TOL, "{ll:?}"); + } + } + + #[test] + fn origin_maps_to_false_origin() { + let g = NewZealandMapGrid::new(&NzmgParams::NZMG, &Ellipsoid::INTERNATIONAL_1924); + let (x, y) = g + .forward(NZMG_LON0_DEG.to_radians(), NZMG_LAT0_DEG.to_radians()) + .unwrap(); + assert!((x - NZMG_FALSE_EASTING).abs() < MM && (y - NZMG_FALSE_NORTHING).abs() < MM); + } + + #[test] + fn parses_and_writes_proj() { + let text = "+proj=nzmg +lat_0=-41 +lon_0=173 +x_0=2510000 +y_0=6023150 +ellps=intl"; + let p = NzmgParams::from_proj(&ProjParams::tokenize(text).unwrap()).unwrap(); + assert_eq!(p, NzmgParams::NZMG); + let mut out = String::new(); + p.write_proj(&mut out); + assert_eq!( + NzmgParams::from_proj(&ProjParams::tokenize(&out).unwrap()).unwrap(), + p + ); + } +} diff --git a/nodedb-spatial/src/crs/hom.rs b/nodedb-crs/src/method/omerc.rs similarity index 50% rename from nodedb-spatial/src/crs/hom.rs rename to nodedb-crs/src/method/omerc.rs index 920e085b6..aa2f71a89 100644 --- a/nodedb-spatial/src/crs/hom.rs +++ b/nodedb-crs/src/method/omerc.rs @@ -4,21 +4,32 @@ //! //! Variant A (EPSG method 9812) measures u from the natural origin, the PROJ //! `+no_uoff` form. Variant B (EPSG method 9815) measures u from the -//! projection centre. +//! projection centre, the PROJ default. -use std::f64::consts::FRAC_PI_4; +use core::f64::consts::{FRAC_PI_2, FRAC_PI_4}; +use core::fmt::Write; -use nodedb_types::geometry::Srid; +use serde::{Deserialize, Serialize}; -use super::ellipsoid::Ellipsoid; -use super::error::{CrsError, DomainReason, check_finite, check_lon_lat}; +use crate::def::Ellipsoid; +use crate::error::DomainReason; /// |cos αc| below which the initial-line azimuth counts as 90°, where /// GN7-2 replaces the uc formula with uc = A (λc − λ0). const AZIMUTH_RIGHT_ANGLE_EPS: f64 = 1e-12; /// Where u is measured from. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Eq, + Serialize, + Deserialize, + zerompk::ToMessagePack, + zerompk::FromMessagePack, +)] pub enum HomVariant { /// EPSG 9812: from the natural origin. False easting and northing apply /// at the natural origin. @@ -29,7 +40,16 @@ pub enum HomVariant { } /// Defining parameters of one Hotine Oblique Mercator projection. -#[derive(Debug, Clone, Copy, PartialEq)] +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Serialize, + Deserialize, + zerompk::ToMessagePack, + zerompk::FromMessagePack, +)] pub struct HomParams { pub variant: HomVariant, /// Latitude of the projection centre φc, degrees. @@ -46,13 +66,63 @@ pub struct HomParams { pub false_easting: f64, /// False northing, metres. pub false_northing: f64, - pub ellipsoid: Ellipsoid, + /// PROJ `+no_rot`: output the rectified (u, v) as (x, y) without the + /// rotation by γc. + pub no_rot: bool, +} + +impl HomParams { + /// Append the PROJ-string parameters, leading space included. + pub fn write_proj(&self, out: &mut String) { + out.push_str(" +proj=omerc"); + match self.variant { + HomVariant::A => out.push_str(" +no_uoff"), + HomVariant::B => {} + } + let _ = write!( + out, + " +lat_0={} +lonc={} +alpha={} +gamma={} +k={} +x_0={} +y_0={}", + self.lat_c_deg, + self.lon_c_deg, + self.alpha_c_deg, + self.gamma_c_deg, + self.k_c, + self.false_easting, + self.false_northing + ); + if self.no_rot { + out.push_str(" +no_rot"); + } + } +} + +/// The GN7-2 constant D at the projection centre. +fn centre_d(lat_c_deg: f64, ellipsoid: &Ellipsoid) -> f64 { + let e2 = ellipsoid.e2(); + let (sin_pc, cos_pc) = lat_c_deg.to_radians().sin_cos(); + let b = (1.0 + e2 * cos_pc.powi(4) / (1.0 - e2)).sqrt(); + b * (1.0 - e2).sqrt() / (cos_pc * (1.0 - e2 * sin_pc * sin_pc).sqrt()) +} + +/// γ0 from αc, degrees: asin(sin αc / D). PROJ uses it as γc when `+gamma` +/// is absent. NaN when no real angle exists. +pub fn gamma_from_alpha(lat_c_deg: f64, alpha_c_deg: f64, ellipsoid: &Ellipsoid) -> f64 { + (alpha_c_deg.to_radians().sin() / centre_d(lat_c_deg, ellipsoid)) + .asin() + .to_degrees() +} + +/// αc from γ0, degrees: asin(D sin γ0). PROJ uses it when `+alpha` is +/// absent. NaN when no real angle exists. +pub fn alpha_from_gamma(lat_c_deg: f64, gamma_deg: f64, ellipsoid: &Ellipsoid) -> f64 { + (centre_d(lat_c_deg, ellipsoid) * gamma_deg.to_radians().sin()) + .asin() + .to_degrees() } /// A Hotine Oblique Mercator projection with its constants computed. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct HotineObliqueMercator { - srid: Srid, params: HomParams, e: f64, e2: f64, @@ -70,9 +140,8 @@ pub struct HotineObliqueMercator { } impl HotineObliqueMercator { - pub fn new(srid: Srid, params: HomParams) -> Self { - let el = params.ellipsoid; - let (e, e2) = (el.e(), el.e2()); + pub fn new(params: &HomParams, ellipsoid: &Ellipsoid) -> Self { + let (e, e2) = (ellipsoid.e(), ellipsoid.e2()); let phi_c = params.lat_c_deg.to_radians(); let lam_c = params.lon_c_deg.to_radians(); let alpha_c = params.alpha_c_deg.to_radians(); @@ -81,9 +150,9 @@ impl HotineObliqueMercator { let sign_pc = if phi_c < 0.0 { -1.0 } else { 1.0 }; let b = (1.0 + e2 * cos_pc.powi(4) / (1.0 - e2)).sqrt(); - let a = el.a * b * params.k_c * (1.0 - e2).sqrt() / (1.0 - e2 * sin_pc * sin_pc); + let a = ellipsoid.a * b * params.k_c * (1.0 - e2).sqrt() / (1.0 - e2 * sin_pc * sin_pc); let t0 = isometric_t(phi_c, e); - let d = b * (1.0 - e2).sqrt() / (cos_pc * (1.0 - e2 * sin_pc * sin_pc).sqrt()); + let d = centre_d(params.lat_c_deg, ellipsoid); // GN7-2: when D < 1, take D² = 1 so F stays real. let d2 = (d * d).max(1.0); let f = d + (d2 - 1.0).sqrt() * sign_pc; @@ -105,8 +174,7 @@ impl HotineObliqueMercator { let (sin_g0, cos_g0) = gamma0.sin_cos(); let (sin_gc, cos_gc) = gamma_c.sin_cos(); Self { - srid, - params, + params: *params, e, e2, b, @@ -121,11 +189,8 @@ impl HotineObliqueMercator { } } - /// Project `[lon, lat]` degrees to `[easting, northing]` metres. - pub fn forward(&self, coord: [f64; 2]) -> Result<[f64; 2], CrsError> { - check_lon_lat(self.srid, coord)?; - let phi = coord[1].to_radians(); - let lam = coord[0].to_radians(); + /// Project λ, φ radians to easting, northing metres. + pub fn forward(&self, lam: f64, phi: f64) -> Result<(f64, f64), DomainReason> { let t = isometric_t(phi, self.e); let q = self.h / t.powf(self.b); let s = (q - 1.0 / q) / 2.0; @@ -133,49 +198,51 @@ impl HotineObliqueMercator { let (v_sin, b_dlam_cos) = (self.b * (lam - self.lon0)).sin_cos(); let u_big = (-v_sin * self.cos_g0 + s * self.sin_g0) / tt; if !u_big.is_finite() || u_big.abs() >= 1.0 { - return Err(CrsError::out_of_domain( - self.srid, - coord, - DomainReason::ObliqueSingularity, - )); + return Err(DomainReason::ObliqueSingularity); } let v = self.a * ((1.0 - u_big) / (1.0 + u_big)).ln() / (2.0 * self.b); let u = self.a * (s * self.cos_g0 + v_sin * self.sin_g0).atan2(b_dlam_cos) / self.b - self.u_offset; - let out = [ - v * self.cos_gc + u * self.sin_gc + self.params.false_easting, - u * self.cos_gc - v * self.sin_gc + self.params.false_northing, - ]; - check_finite(self.srid, out)?; - Ok(out) + let (x, y) = if self.params.no_rot { + (u, v) + } else { + ( + v * self.cos_gc + u * self.sin_gc, + u * self.cos_gc - v * self.sin_gc, + ) + }; + Ok(( + x + self.params.false_easting, + y + self.params.false_northing, + )) } - /// Unproject `[easting, northing]` metres to `[lon, lat]` degrees. - pub fn inverse(&self, coord: [f64; 2]) -> Result<[f64; 2], CrsError> { - check_finite(self.srid, coord)?; - let de = coord[0] - self.params.false_easting; - let dn = coord[1] - self.params.false_northing; - let v = de * self.cos_gc - dn * self.sin_gc; - let u = dn * self.cos_gc + de * self.sin_gc + self.u_offset; + /// Unproject easting, northing metres to λ, φ radians. + pub fn inverse(&self, x: f64, y: f64) -> Result<(f64, f64), DomainReason> { + let de = x - self.params.false_easting; + let dn = y - self.params.false_northing; + let (v, u_rect) = if self.params.no_rot { + (dn, de) + } else { + ( + de * self.cos_gc - dn * self.sin_gc, + dn * self.cos_gc + de * self.sin_gc, + ) + }; + let u = u_rect + self.u_offset; let q = (-self.b * v / self.a).exp(); let s = (q - 1.0 / q) / 2.0; let tt = (q + 1.0 / q) / 2.0; let (v_sin, bu_cos) = (self.b * u / self.a).sin_cos(); let u_big = (v_sin * self.cos_g0 + s * self.sin_g0) / tt; if !u_big.is_finite() || u_big.abs() >= 1.0 { - return Err(CrsError::out_of_domain( - self.srid, - coord, - DomainReason::ObliqueSingularity, - )); + return Err(DomainReason::ObliqueSingularity); } let t = (self.h / ((1.0 + u_big) / (1.0 - u_big)).sqrt()).powf(1.0 / self.b); - let chi = std::f64::consts::FRAC_PI_2 - 2.0 * t.atan(); + let chi = FRAC_PI_2 - 2.0 * t.atan(); let phi = latitude_from_conformal(chi, self.e2); let lam = self.lon0 - (s * self.cos_g0 - v_sin * self.sin_g0).atan2(bu_cos) / self.b; - let out = [lam.to_degrees(), phi.to_degrees()]; - check_finite(self.srid, out)?; - Ok(out) + Ok((lam, phi)) } } @@ -200,17 +267,46 @@ fn latitude_from_conformal(chi: f64, e2: f64) -> f64 { #[cfg(test)] mod tests { use super::*; - use crate::crs::params::{BORNEO_RSO, PENINSULAR_RSO}; /// One millimetre, the tolerance for every PROJ reference value. const MM: f64 = 1e-3; - fn rso_peninsular() -> HotineObliqueMercator { - HotineObliqueMercator::new(Srid::PeninsularRso, PENINSULAR_RSO) + /// RSO scale factor on the initial line, both Malaysian RSO grids. + const RSO_K: f64 = 0.99984; + + /// EPSG:3375, GDM2000 Peninsula RSO. + const PENINSULAR_RSO: HomParams = HomParams { + variant: HomVariant::A, + lat_c_deg: 4.0, + lon_c_deg: 102.25, + alpha_c_deg: 323.025_796_466_667, + gamma_c_deg: 323.130_102_361_111, + k_c: RSO_K, + false_easting: 804_671.0, + false_northing: 0.0, + no_rot: false, + }; + + /// EPSG:3376, GDM2000 East Malaysia (Borneo) RSO. + const BORNEO_RSO: HomParams = HomParams { + variant: HomVariant::A, + lat_c_deg: 4.0, + lon_c_deg: 115.0, + alpha_c_deg: 53.315_809_95, + gamma_c_deg: 53.130_102_361_111_1, + k_c: RSO_K, + false_easting: 0.0, + false_northing: 0.0, + no_rot: false, + }; + + fn hom(params: HomParams) -> HotineObliqueMercator { + HotineObliqueMercator::new(¶ms, &Ellipsoid::GRS80) } - fn rso_borneo() -> HotineObliqueMercator { - HotineObliqueMercator::new(Srid::BorneoRso, BORNEO_RSO) + fn fwd(h: &HotineObliqueMercator, lon: f64, lat: f64) -> Result<[f64; 2], DomainReason> { + h.forward(lon.to_radians(), lat.to_radians()) + .map(|(x, y)| [x, y]) } fn assert_close(got: [f64; 2], want: [f64; 2]) { @@ -224,35 +320,35 @@ mod tests { #[test] fn peninsular_rso_klcc() { - let got = rso_peninsular().forward([101.7117, 3.1579]).unwrap(); + let got = fwd(&hom(PENINSULAR_RSO), 101.7117, 3.1579).unwrap(); assert_close(got, [412_834.468_1, 349_472.917_7]); } #[test] fn peninsular_rso_105_5() { - let got = rso_peninsular().forward([105.0, 5.0]).unwrap(); + let got = fwd(&hom(PENINSULAR_RSO), 105.0, 5.0).unwrap(); assert_close(got, [778_104.319_5, 553_022.237_0]); } #[test] fn borneo_rso_kota_kinabalu() { - let got = rso_borneo().forward([116.0735, 5.9804]).unwrap(); + let got = fwd(&hom(BORNEO_RSO), 116.0735, 5.9804).unwrap(); assert_close(got, [708_697.150_6, 662_326.916_8]); } #[test] fn borneo_rso_105_5() { - let got = rso_borneo().forward([105.0, 5.0]).unwrap(); + let got = fwd(&hom(BORNEO_RSO), 105.0, 5.0).unwrap(); assert_close(got, [-521_596.204_7, 554_604.058_6]); } #[test] fn inverse_recovers_input_to_a_millimetre() { - for hom in [rso_peninsular(), rso_borneo()] { - for input in [[101.7117, 3.1579], [116.0735, 5.9804], [105.0, 5.0]] { - let projected = hom.forward(input).unwrap(); - let back = hom.inverse(projected).unwrap(); - let again = hom.forward(back).unwrap(); + for h in [hom(PENINSULAR_RSO), hom(BORNEO_RSO)] { + for [lon, lat] in [[101.7117, 3.1579], [116.0735, 5.9804], [105.0, 5.0]] { + let projected = fwd(&h, lon, lat).unwrap(); + let (lam, phi) = h.inverse(projected[0], projected[1]).unwrap(); + let again = h.forward(lam, phi).map(|(x, y)| [x, y]).unwrap(); assert_close(again, projected); } } @@ -268,17 +364,36 @@ mod tests { false_northing: 0.0, ..PENINSULAR_RSO }; - let hom = HotineObliqueMercator::new(Srid::PeninsularRso, params); - let centre = hom.forward([params.lon_c_deg, params.lat_c_deg]).unwrap(); + let h = hom(params); + let centre = fwd(&h, params.lon_c_deg, params.lat_c_deg).unwrap(); assert_close(centre, [0.0, 0.0]); - let back = hom.inverse(centre).unwrap(); - assert!((back[0] - params.lon_c_deg).abs() < 1e-9); - assert!((back[1] - params.lat_c_deg).abs() < 1e-9); + let (lam, phi) = h.inverse(centre[0], centre[1]).unwrap(); + assert!((lam.to_degrees() - params.lon_c_deg).abs() < 1e-9); + assert!((phi.to_degrees() - params.lat_c_deg).abs() < 1e-9); + } + + #[test] + fn no_rot_inverts() { + let h = hom(HomParams { + no_rot: true, + ..BORNEO_RSO + }); + let p = fwd(&h, 116.0735, 5.9804).unwrap(); + let (lam, phi) = h.inverse(p[0], p[1]).unwrap(); + assert!((lam.to_degrees() - 116.0735).abs() < 1e-9); + assert!((phi.to_degrees() - 5.9804).abs() < 1e-9); } #[test] fn pole_is_out_of_domain() { - assert!(rso_peninsular().forward([102.0, 90.0]).is_err()); - assert!(rso_peninsular().forward([102.0, 90.5]).is_err()); + assert!(fwd(&hom(PENINSULAR_RSO), 102.0, 90.0).is_err()); + } + + /// The RSO γc is the rectified-grid angle of a square grid, close to γ0. + #[test] + fn gamma_and_alpha_invert_each_other() { + let g = gamma_from_alpha(4.0, 53.315_809_95, &Ellipsoid::GRS80); + let a = alpha_from_gamma(4.0, g, &Ellipsoid::GRS80); + assert!((a - 53.315_809_95).abs() < 1e-9); } } diff --git a/nodedb-crs/src/method/poly.rs b/nodedb-crs/src/method/poly.rs new file mode 100644 index 000000000..c4809cbb0 --- /dev/null +++ b/nodedb-crs/src/method/poly.rs @@ -0,0 +1,353 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! American Polyconic, EPSG method 9818, PROJ `poly`. +//! +//! Follows PROJ 9.8.1 `src/projections/poly.cpp` (Snyder 1987 pp. 124–133, +//! eqs. 18-1 to 18-21; IOGP Guidance Note 7-2 §3.5.1), ellipsoidal and +//! spherical, with Newton–Raphson inverses. + +use core::fmt::Write; + +use serde::{Deserialize, Serialize}; + +use super::angle::wrap_pi; +use super::meridian::MeridianArc; +use crate::def::Ellipsoid; +use crate::error::{CrsParseError, DomainReason}; +use crate::parse::tokens::ProjParams; + +/// |φ| at or below which a point counts as on the equator, PROJ `TOL`. +const TOL: f64 = 1e-10; + +/// Spherical inverse convergence, PROJ `CONV`. +const CONV: f64 = 1e-10; + +/// Spherical inverse iterations, PROJ `N_ITER`. +const N_ITER: usize = 10; + +/// Ellipsoidal inverse iterations, PROJ `I_ITER`. +const I_ITER: usize = 20; + +/// Ellipsoidal inverse convergence and pole guard, PROJ `ITOL`. +const ITOL: f64 = 1e-12; + +/// Largest absolute latitude of origin, degrees. +const MAX_LAT_DEG: f64 = 90.0; + +/// Defining parameters of one American Polyconic projection. +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Serialize, + Deserialize, + zerompk::ToMessagePack, + zerompk::FromMessagePack, +)] +pub struct PolyParams { + /// Latitude of natural origin φ0, degrees (`+lat_0`). + pub lat0_deg: f64, + /// Longitude of natural origin λ0, degrees (`+lon_0`). + pub lon0_deg: f64, + /// False easting, metres (`+x_0`). + pub false_easting: f64, + /// False northing, metres (`+y_0`). + pub false_northing: f64, +} + +impl PolyParams { + /// Read from PROJ-string parameters. Every key defaults to 0 as in PROJ. + pub fn from_proj(p: &ProjParams<'_>) -> Result { + let lat0_deg = p.number_or("lat_0", 0.0)?; + if lat0_deg.abs() > MAX_LAT_DEG { + return Err(CrsParseError::invalid("lat_0", &lat0_deg.to_string())); + } + Ok(Self { + lat0_deg, + lon0_deg: p.number_or("lon_0", 0.0)?, + false_easting: p.number_or("x_0", 0.0)?, + false_northing: p.number_or("y_0", 0.0)?, + }) + } + + /// Append the defining parameters as PROJ-string tokens. + pub fn write_proj(&self, out: &mut String) { + let _ = write!( + out, + " +proj=poly +lat_0={} +lon_0={} +x_0={} +y_0={}", + self.lat0_deg, self.lon0_deg, self.false_easting, self.false_northing + ); + } +} + +/// American Polyconic with its constants computed. +#[derive(Debug, Clone, PartialEq)] +pub struct Polyconic { + params: PolyParams, + a: f64, + es: f64, + lon0: f64, + phi0: f64, + /// Meridian arc; `None` on a sphere. + arc: Option, + /// Ellipsoid: arc to φ0 in units of a. Sphere: −φ0. + ml0: f64, +} + +impl Polyconic { + pub fn new(p: &PolyParams, ell: &Ellipsoid) -> Self { + let phi0 = p.lat0_deg.to_radians(); + let es = ell.e2(); + let (arc, ml0) = if ell.is_sphere() { + (None, -phi0) + } else { + let arc = MeridianArc::new(es); + let ml0 = arc.arc(phi0, phi0.sin(), phi0.cos()); + (Some(arc), ml0) + }; + Self { + params: *p, + a: ell.a, + es, + lon0: p.lon0_deg.to_radians(), + phi0, + arc, + ml0, + } + } + + pub fn forward(&self, lam: f64, phi: f64) -> Result<(f64, f64), DomainReason> { + let lam = wrap_pi(lam - self.lon0); + let (x, y) = match &self.arc { + Some(arc) => self.e_forward(arc, lam, phi), + None => self.s_forward(lam, phi), + }; + Ok(( + self.params.false_easting + self.a * x, + self.params.false_northing + self.a * y, + )) + } + + pub fn inverse(&self, x: f64, y: f64) -> Result<(f64, f64), DomainReason> { + let x = (x - self.params.false_easting) / self.a; + let y = (y - self.params.false_northing) / self.a; + let (lam, phi) = match &self.arc { + Some(arc) => self.e_inverse(arc, x, y)?, + None => self.s_inverse(x, y)?, + }; + Ok((wrap_pi(lam + self.lon0), phi)) + } + + /// PROJ `poly_e_forward` (Snyder eqs. 18-2, 18-3). + fn e_forward(&self, arc: &MeridianArc, lam: f64, phi: f64) -> (f64, f64) { + if phi.abs() <= TOL { + return (lam, -self.ml0); + } + let (sp, cp) = phi.sin_cos(); + let ms = if cp.abs() > TOL { + cp / (1.0 - self.es * sp * sp).sqrt() / sp + } else { + 0.0 + }; + let e = lam * sp; + let (sin_e, cos_e) = e.sin_cos(); + ( + ms * sin_e, + (arc.arc(phi, sp, cp) - self.ml0) + ms * (1.0 - cos_e), + ) + } + + /// PROJ `poly_s_forward` (Snyder eqs. 18-1). + fn s_forward(&self, lam: f64, phi: f64) -> (f64, f64) { + if phi.abs() <= TOL { + return (lam, self.ml0); + } + let cot = 1.0 / phi.tan(); + let e = lam * phi.sin(); + (e.sin() * cot, phi - self.phi0 + cot * (1.0 - e.cos())) + } + + /// PROJ `poly_e_inverse` (Snyder eqs. 18-17 to 18-21). + fn e_inverse(&self, arc: &MeridianArc, x: f64, y: f64) -> Result<(f64, f64), DomainReason> { + let y = y + self.ml0; + if y.abs() <= TOL { + return Ok((x, 0.0)); + } + let r = y * y + x * x; + let mut phi = y; + let mut converged = false; + for _ in 0..I_ITER { + let (sp, cp) = phi.sin_cos(); + let s2ph = sp * cp; + if cp.abs() < ITOL { + return Err(DomainReason::OutsideProjection); + } + let mlp_root = (1.0 - self.es * sp * sp).sqrt(); + let c = sp * mlp_root / cp; + let ml = arc.arc(phi, sp, cp); + let mlb = ml * ml + r; + let mlp = (1.0 - self.es) / (mlp_root * mlp_root * mlp_root); + let dphi = (ml + ml + c * mlb - 2.0 * y * (c * ml + 1.0)) + / (self.es * s2ph * (mlb - 2.0 * y * ml) / c + + 2.0 * (y - ml) * (c * mlp - 1.0 / s2ph) + - mlp + - mlp); + phi += dphi; + if dphi.abs() <= ITOL { + converged = true; + break; + } + } + if !converged { + return Err(DomainReason::NoConvergence); + } + let sp = phi.sin(); + let arg = x * phi.tan() * (1.0 - self.es * sp * sp).sqrt(); + if arg.abs() > 1.0 { + return Err(DomainReason::OutsideProjection); + } + Ok((arg.asin() / sp, phi)) + } + + /// PROJ `poly_s_inverse` (Snyder eqs. 18-9 to 18-14). + fn s_inverse(&self, x: f64, y: f64) -> Result<(f64, f64), DomainReason> { + let y = self.phi0 + y; + if y.abs() <= TOL { + return Ok((x, 0.0)); + } + let mut phi = y; + let b = x * x + y * y; + let mut converged = false; + for _ in 0..N_ITER { + let tp = phi.tan(); + let dphi = + (y * (phi * tp + 1.0) - phi - 0.5 * (phi * phi + b) * tp) / ((phi - y) / tp - 1.0); + phi -= dphi; + if dphi.is_nan() || dphi.abs() <= CONV { + converged = true; + break; + } + } + if !converged { + return Err(DomainReason::NoConvergence); + } + let arg = x * phi.tan(); + if arg.abs() > 1.0 { + return Err(DomainReason::OutsideProjection); + } + Ok((arg.asin() / phi.sin(), phi)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const MM: f64 = 1e-3; + const DEG_TOL: f64 = 1e-9; + + fn params(lat0_deg: f64, lon0_deg: f64, false_easting: f64, false_northing: f64) -> PolyParams { + PolyParams { + lat0_deg, + lon0_deg, + false_easting, + false_northing, + } + } + + fn check(p: &PolyParams, ell: &Ellipsoid, pts: &[[f64; 4]]) { + let poly = Polyconic::new(p, ell); + for [lon, lat, x, y] in pts.iter().copied() { + let (gx, gy) = poly.forward(lon.to_radians(), lat.to_radians()).unwrap(); + assert!( + (gx - x).abs() < MM && (gy - y).abs() < MM, + "{lon} {lat}: {gx} {gy}" + ); + let (glam, gphi) = poly.inverse(x, y).unwrap(); + assert!( + (glam.to_degrees() - lon).abs() < DEG_TOL + && (gphi.to_degrees() - lat).abs() < DEG_TOL, + "{x} {y}: {} {}", + glam.to_degrees(), + gphi.to_degrees() + ); + } + } + + /// The GN 7-2 §3.5.1 parameter set, SAD69 / Brazil Polyconic + /// (EPSG:29101) without the datum shift. PROJ 9.8.1 values via pyproj + /// 3.8.0 for `+proj=poly +lat_0=0 +lon_0=-54 +x_0=5000000 +y_0=10000000 + /// +ellps=aust_SA`. + #[test] + fn brazil_polyconic() { + check( + ¶ms(0.0, -54.0, 5_000_000.0, 10_000_000.0), + &Ellipsoid::AUSTRALIAN_SA, + BRAZIL, + ); + } + + const BRAZIL: &[[f64; 4]] = &[ + [-45.0, -6.0, 5996382.3037087675, 9328347.629558984], + [-60.0, -20.0, 4372249.445673196, 7776383.064872811], + [-54.0, 0.0, 5000000.0, 10000000.0], + [-40.0, 5.0, 6552470.132945759, 10569418.81021988], + ]; + + /// PROJ 9.8.1 via pyproj 3.8.0, EPSG:5472 Panama Polyconic in metres: + /// `+proj=poly +lat_0=8.25 +lon_0=-81 +x_0=914391.7962 + /// +y_0=999404.721715486 +ellps=clrk66`. + #[test] + fn panama_polyconic() { + check( + ¶ms(8.25, -81.0, 914_391.796_2, 999_404.721_715_486), + &Ellipsoid::CLARKE_1866, + PANAMA, + ); + } + + const PANAMA: &[[f64; 4]] = &[ + [-80.0, 9.0, 1024350.9263183008, 1082499.190405663], + [-82.5, 8.3, 749148.4770122074, 1005246.4922782447], + [-79.0, 7.5, 1135140.4370462974, 916966.3856309258], + ]; + + /// PROJ 9.8.1 via pyproj 3.8.0, `+proj=poly +lat_0=30 +lon_0=-96 +R=6370997`. + #[test] + fn sphere() { + check( + ¶ms(30.0, -96.0, 0.0, 0.0), + &Ellipsoid::sphere(6_370_997.0), + SPHERE_PTS, + ); + } + + const SPHERE_PTS: &[[f64; 4]] = &[ + [-100.0, 35.0, -364244.67434682354, 563268.1052725625], + [-80.0, 25.0, 1608688.0876570228, -460937.5409812575], + [-96.0, 30.0, 0.0, 0.0], + ]; + + #[test] + fn equator_maps_to_straight_line() { + let poly = Polyconic::new(¶ms(0.0, 0.0, 0.0, 0.0), &Ellipsoid::GRS80); + let (x, y) = poly.forward(0.1, 0.0).unwrap(); + assert!((x - 0.1 * Ellipsoid::GRS80.a).abs() < 1e-6 && y.abs() < 1e-9); + let (lam, phi) = poly.inverse(x, y).unwrap(); + assert!((lam - 0.1).abs() < 1e-15 && phi == 0.0); + } + + #[test] + fn parses_and_writes() { + let p = ProjParams::tokenize("+proj=poly +lat_0=0 +lon_0=-54 +x_0=5000000 +y_0=10000000") + .unwrap(); + let q = PolyParams::from_proj(&p).unwrap(); + let mut s = String::new(); + q.write_proj(&mut s); + assert_eq!( + s, + " +proj=poly +lat_0=0 +lon_0=-54 +x_0=5000000 +y_0=10000000" + ); + } +} diff --git a/nodedb-crs/src/method/projection.rs b/nodedb-crs/src/method/projection.rs new file mode 100644 index 000000000..e7b04cd48 --- /dev/null +++ b/nodedb-crs/src/method/projection.rs @@ -0,0 +1,197 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! The defining parameters of every implemented projection method. +//! +//! A new method adds one file under `method/`, one variant here and one in +//! [`Projector`], and one arm in each exhaustive match. + +use serde::{Deserialize, Serialize}; + +use super::aea::{AeaParams, Albers}; +use super::aeqd::{AeqdParams, AzimuthalEquidistant}; +use super::cass::{CassParams, Cassini}; +use super::cea::{CeaParams, CylindricalEqualArea}; +use super::eqc::{EqcParams, EquidistantCylindrical}; +use super::eqearth::{EqEarthParams, EqualEarth}; +use super::geos::{GeosParams, GeostationarySatellite}; +use super::krovak::{Krovak, KrovakParams}; +use super::labrd::{LabordeObliqueMercator, LabrdParams}; +use super::laea::{LaeaParams, LambertAzimuthalEqualArea}; +use super::lcc::{LambertConformalConic, LccParams}; +use super::merc::{MercParams, Mercator}; +use super::nzmg::{NewZealandMapGrid, NzmgParams}; +use super::omerc::{HomParams, HotineObliqueMercator}; +use super::poly::{PolyParams, Polyconic}; +use super::projector::Projector; +use super::sinu::{SinuParams, Sinusoidal}; +use super::somerc::{SomercParams, SwissObliqueMercator}; +use super::stere::{StereParams, Stereographic}; +use super::sterea::{ObliqueStereographic, StereaParams}; +use super::tmerc::{TmParams, TransverseMercator}; +use super::webmerc::{WebMercParams, WebMercator}; +use crate::def::Ellipsoid; +use crate::error::DomainReason; + +/// A map projection with its defining parameters. +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Serialize, + Deserialize, + zerompk::ToMessagePack, + zerompk::FromMessagePack, +)] +pub enum ProjectionDef { + /// EPSG 9807 and 9808 (South Orientated, through `+axis=wsu`), PROJ + /// `tmerc` and `utm`. + TransverseMercator(TmParams), + /// EPSG 9812 / 9815, PROJ `omerc`. + HotineObliqueMercator(HomParams), + /// EPSG 9801, 9802, 9803, 9826 (West Orientated, through `+axis=wnu`), + /// 1051 and 1102, PROJ `lcc`. + LambertConicConformal(LccParams), + /// EPSG 9804, 9805 and 1026, PROJ `merc`. + Mercator(MercParams), + /// EPSG 1024, PROJ `webmerc`. + PseudoMercator(WebMercParams), + /// EPSG 9834 and 9835, PROJ `cea`. + LambertCylindricalEqualArea(CeaParams), + /// EPSG 1028 and 1029, PROJ `eqc`. + EquidistantCylindrical(EqcParams), + /// EPSG 9810, 9829 and 9830 plus the oblique and equatorial aspects, + /// PROJ `stere` and `ups`. + Stereographic(StereParams), + /// EPSG 9809, PROJ `sterea`. + ObliqueStereographic(StereaParams), + /// EPSG 9822, PROJ `aea`. + AlbersEqualArea(AeaParams), + /// EPSG 9820 and 1027, PROJ `laea`. + LambertAzimuthalEqualArea(LaeaParams), + /// EPSG 9806 and 9833 (hyperbolic), PROJ `cass`. + CassiniSoldner(CassParams), + /// EPSG 9818, PROJ `poly`. + AmericanPolyconic(PolyParams), + /// EPSG 1125, 9831 (Guam) and 9832, PROJ `aeqd`. + AzimuthalEquidistant(AeqdParams), + /// No EPSG method, PROJ `sinu`. + Sinusoidal(SinuParams), + /// EPSG 1078, PROJ `eqearth`. + EqualEarth(EqEarthParams), + /// EPSG 9819, 1041, 1042 and 1043, PROJ `krovak` and `mod_krovak`. + Krovak(KrovakParams), + /// EPSG 9814 and the 9815 rows PROJ writes as `somerc`, PROJ `somerc`. + SwissObliqueMercator(SomercParams), + /// EPSG 9811, PROJ `nzmg`. + NewZealandMapGrid(NzmgParams), + /// No EPSG method, PROJ `geos`. + GeostationarySatellite(GeosParams), + /// EPSG 9813, PROJ `labrd`. + LabordeObliqueMercator(LabrdParams), +} + +impl ProjectionDef { + /// Compute the constants of this projection on `ellipsoid`. + pub fn prepare(&self, ellipsoid: &Ellipsoid) -> Projector { + let e = ellipsoid; + match self { + ProjectionDef::TransverseMercator(p) => { + Projector::TransverseMercator(TransverseMercator::new(p, e)) + } + ProjectionDef::HotineObliqueMercator(p) => { + Projector::HotineObliqueMercator(HotineObliqueMercator::new(p, e)) + } + ProjectionDef::LambertConicConformal(p) => { + Projector::LambertConicConformal(LambertConformalConic::new(p, e)) + } + ProjectionDef::Mercator(p) => Projector::Mercator(Mercator::new(p, e)), + ProjectionDef::PseudoMercator(p) => Projector::PseudoMercator(WebMercator::new(p, e)), + ProjectionDef::LambertCylindricalEqualArea(p) => { + Projector::LambertCylindricalEqualArea(CylindricalEqualArea::new(p, e)) + } + ProjectionDef::EquidistantCylindrical(p) => { + Projector::EquidistantCylindrical(EquidistantCylindrical::new(p, e)) + } + ProjectionDef::Stereographic(p) => Projector::Stereographic(Stereographic::new(p, e)), + ProjectionDef::ObliqueStereographic(p) => { + Projector::ObliqueStereographic(ObliqueStereographic::new(p, e)) + } + ProjectionDef::AlbersEqualArea(p) => Projector::AlbersEqualArea(Albers::new(p, e)), + ProjectionDef::LambertAzimuthalEqualArea(p) => { + Projector::LambertAzimuthalEqualArea(LambertAzimuthalEqualArea::new(p, e)) + } + ProjectionDef::CassiniSoldner(p) => Projector::CassiniSoldner(Cassini::new(p, e)), + ProjectionDef::AmericanPolyconic(p) => { + Projector::AmericanPolyconic(Polyconic::new(p, e)) + } + ProjectionDef::AzimuthalEquidistant(p) => { + Projector::AzimuthalEquidistant(Box::new(AzimuthalEquidistant::new(p, e))) + } + ProjectionDef::Sinusoidal(p) => Projector::Sinusoidal(Sinusoidal::new(p, e)), + ProjectionDef::EqualEarth(p) => Projector::EqualEarth(EqualEarth::new(p, e)), + ProjectionDef::Krovak(p) => Projector::Krovak(Krovak::new(p, e)), + ProjectionDef::SwissObliqueMercator(p) => { + Projector::SwissObliqueMercator(SwissObliqueMercator::new(p, e)) + } + ProjectionDef::NewZealandMapGrid(p) => { + Projector::NewZealandMapGrid(NewZealandMapGrid::new(p, e)) + } + ProjectionDef::GeostationarySatellite(p) => { + Projector::GeostationarySatellite(GeostationarySatellite::new(p, e)) + } + ProjectionDef::LabordeObliqueMercator(p) => { + Projector::LabordeObliqueMercator(LabordeObliqueMercator::new(p, e)) + } + } + } + + /// Append this projection's PROJ-string parameters, each with a leading + /// space, such as ` +proj=tmerc +lat_0=0`. + pub fn write_proj(&self, out: &mut String) { + match self { + ProjectionDef::TransverseMercator(p) => p.write_proj(out), + ProjectionDef::HotineObliqueMercator(p) => p.write_proj(out), + ProjectionDef::LambertConicConformal(p) => p.write_proj(out), + ProjectionDef::Mercator(p) => p.write_proj(out), + ProjectionDef::PseudoMercator(p) => p.write_proj(out), + ProjectionDef::LambertCylindricalEqualArea(p) => p.write_proj(out), + ProjectionDef::EquidistantCylindrical(p) => p.write_proj(out), + ProjectionDef::Stereographic(p) => p.write_proj(out), + ProjectionDef::ObliqueStereographic(p) => p.write_proj(out), + ProjectionDef::AlbersEqualArea(p) => p.write_proj(out), + ProjectionDef::LambertAzimuthalEqualArea(p) => p.write_proj(out), + ProjectionDef::CassiniSoldner(p) => p.write_proj(out), + ProjectionDef::AmericanPolyconic(p) => p.write_proj(out), + ProjectionDef::AzimuthalEquidistant(p) => p.write_proj(out), + ProjectionDef::Sinusoidal(p) => p.write_proj(out), + ProjectionDef::EqualEarth(p) => p.write_proj(out), + ProjectionDef::Krovak(p) => p.write_proj(out), + ProjectionDef::SwissObliqueMercator(p) => p.write_proj(out), + ProjectionDef::NewZealandMapGrid(p) => p.write_proj(out), + ProjectionDef::GeostationarySatellite(p) => p.write_proj(out), + ProjectionDef::LabordeObliqueMercator(p) => p.write_proj(out), + } + } + + /// Project λ, φ radians (λ from the CRS prime meridian) to x, y metres. + /// Prepares the constants per call. A [`Projector`] reuses them. + pub fn forward( + &self, + lam_rad: f64, + phi_rad: f64, + ellipsoid: &Ellipsoid, + ) -> Result<(f64, f64), DomainReason> { + self.prepare(ellipsoid).forward(lam_rad, phi_rad) + } + + /// Unproject x, y metres to λ, φ radians. + pub fn inverse( + &self, + x_m: f64, + y_m: f64, + ellipsoid: &Ellipsoid, + ) -> Result<(f64, f64), DomainReason> { + self.prepare(ellipsoid).inverse(x_m, y_m) + } +} diff --git a/nodedb-crs/src/method/projector.rs b/nodedb-crs/src/method/projector.rs new file mode 100644 index 000000000..351af0ba4 --- /dev/null +++ b/nodedb-crs/src/method/projector.rs @@ -0,0 +1,162 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! A projection with its constants computed for one ellipsoid. + +use core::f64::consts::FRAC_PI_2; + +use super::aea::Albers; +use super::aeqd::AzimuthalEquidistant; +use super::cass::Cassini; +use super::cea::CylindricalEqualArea; +use super::eqc::EquidistantCylindrical; +use super::eqearth::EqualEarth; +use super::geos::GeostationarySatellite; +use super::krovak::Krovak; +use super::labrd::LabordeObliqueMercator; +use super::laea::LambertAzimuthalEqualArea; +use super::lcc::LambertConformalConic; +use super::merc::Mercator; +use super::nzmg::NewZealandMapGrid; +use super::omerc::HotineObliqueMercator; +use super::poly::Polyconic; +use super::sinu::Sinusoidal; +use super::somerc::SwissObliqueMercator; +use super::stere::Stereographic; +use super::sterea::ObliqueStereographic; +use super::tmerc::TransverseMercator; +use super::webmerc::WebMercator; +use crate::error::DomainReason; + +/// A prepared projection. Built by +/// [`ProjectionDef::prepare`](super::ProjectionDef::prepare). +#[derive(Debug, Clone, PartialEq)] +pub enum Projector { + TransverseMercator(TransverseMercator), + HotineObliqueMercator(HotineObliqueMercator), + LambertConicConformal(LambertConformalConic), + Mercator(Mercator), + PseudoMercator(WebMercator), + LambertCylindricalEqualArea(CylindricalEqualArea), + EquidistantCylindrical(EquidistantCylindrical), + Stereographic(Stereographic), + ObliqueStereographic(ObliqueStereographic), + AlbersEqualArea(Albers), + LambertAzimuthalEqualArea(LambertAzimuthalEqualArea), + CassiniSoldner(Cassini), + AmericanPolyconic(Polyconic), + /// Boxed: the geodesic series coefficients make it the largest by far. + AzimuthalEquidistant(Box), + Sinusoidal(Sinusoidal), + EqualEarth(EqualEarth), + Krovak(Krovak), + SwissObliqueMercator(SwissObliqueMercator), + NewZealandMapGrid(NewZealandMapGrid), + GeostationarySatellite(GeostationarySatellite), + LabordeObliqueMercator(LabordeObliqueMercator), +} + +impl Projector { + /// Project λ, φ radians (λ from the CRS prime meridian) to x, y metres, + /// false easting and northing included. + pub fn forward(&self, lam: f64, phi: f64) -> Result<(f64, f64), DomainReason> { + if !lam.is_finite() || !phi.is_finite() { + return Err(DomainReason::NonFinite); + } + if phi.abs() > FRAC_PI_2 { + return Err(DomainReason::LatitudeRange); + } + let out = match self { + Projector::TransverseMercator(p) => p.forward(lam, phi), + Projector::HotineObliqueMercator(p) => p.forward(lam, phi), + Projector::LambertConicConformal(p) => p.forward(lam, phi), + Projector::Mercator(p) => p.forward(lam, phi), + Projector::PseudoMercator(p) => p.forward(lam, phi), + Projector::LambertCylindricalEqualArea(p) => p.forward(lam, phi), + Projector::EquidistantCylindrical(p) => p.forward(lam, phi), + Projector::Stereographic(p) => p.forward(lam, phi), + Projector::ObliqueStereographic(p) => p.forward(lam, phi), + Projector::AlbersEqualArea(p) => p.forward(lam, phi), + Projector::LambertAzimuthalEqualArea(p) => p.forward(lam, phi), + Projector::CassiniSoldner(p) => p.forward(lam, phi), + Projector::AmericanPolyconic(p) => p.forward(lam, phi), + Projector::AzimuthalEquidistant(p) => p.forward(lam, phi), + Projector::Sinusoidal(p) => p.forward(lam, phi), + Projector::EqualEarth(p) => p.forward(lam, phi), + Projector::Krovak(p) => p.forward(lam, phi), + Projector::SwissObliqueMercator(p) => p.forward(lam, phi), + Projector::NewZealandMapGrid(p) => p.forward(lam, phi), + Projector::GeostationarySatellite(p) => p.forward(lam, phi), + Projector::LabordeObliqueMercator(p) => p.forward(lam, phi), + }?; + finite(out) + } + + /// Unproject x, y metres to λ, φ radians. + pub fn inverse(&self, x: f64, y: f64) -> Result<(f64, f64), DomainReason> { + if !x.is_finite() || !y.is_finite() { + return Err(DomainReason::NonFinite); + } + let out = match self { + Projector::TransverseMercator(p) => p.inverse(x, y), + Projector::HotineObliqueMercator(p) => p.inverse(x, y), + Projector::LambertConicConformal(p) => p.inverse(x, y), + Projector::Mercator(p) => p.inverse(x, y), + Projector::PseudoMercator(p) => p.inverse(x, y), + Projector::LambertCylindricalEqualArea(p) => p.inverse(x, y), + Projector::EquidistantCylindrical(p) => p.inverse(x, y), + Projector::Stereographic(p) => p.inverse(x, y), + Projector::ObliqueStereographic(p) => p.inverse(x, y), + Projector::AlbersEqualArea(p) => p.inverse(x, y), + Projector::LambertAzimuthalEqualArea(p) => p.inverse(x, y), + Projector::CassiniSoldner(p) => p.inverse(x, y), + Projector::AmericanPolyconic(p) => p.inverse(x, y), + Projector::AzimuthalEquidistant(p) => p.inverse(x, y), + Projector::Sinusoidal(p) => p.inverse(x, y), + Projector::EqualEarth(p) => p.inverse(x, y), + Projector::Krovak(p) => p.inverse(x, y), + Projector::SwissObliqueMercator(p) => p.inverse(x, y), + Projector::NewZealandMapGrid(p) => p.inverse(x, y), + Projector::GeostationarySatellite(p) => p.inverse(x, y), + Projector::LabordeObliqueMercator(p) => p.inverse(x, y), + }?; + finite(out) + } +} + +/// Fail unless both outputs are finite. +fn finite(out: (f64, f64)) -> Result<(f64, f64), DomainReason> { + if out.0.is_finite() && out.1.is_finite() { + Ok(out) + } else { + Err(DomainReason::NonFinite) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::def::Ellipsoid; + use crate::method::{ProjectionDef, TmParams}; + + fn utm48() -> Projector { + ProjectionDef::TransverseMercator(TmParams { + lat0_deg: 0.0, + lon0_deg: 105.0, + k0: 0.9996, + false_easting: 500_000.0, + false_northing: 0.0, + }) + .prepare(&Ellipsoid::WGS84) + } + + #[test] + fn bad_latitude_and_non_finite_are_out_of_domain() { + let p = utm48(); + assert_eq!( + p.forward(105f64.to_radians(), 91f64.to_radians()), + Err(DomainReason::LatitudeRange) + ); + assert_eq!(p.forward(f64::NAN, 0.0), Err(DomainReason::NonFinite)); + assert_eq!(p.inverse(f64::INFINITY, 1.0), Err(DomainReason::NonFinite)); + } +} diff --git a/nodedb-crs/src/method/sinu.rs b/nodedb-crs/src/method/sinu.rs new file mode 100644 index 000000000..f1450aee6 --- /dev/null +++ b/nodedb-crs/src/method/sinu.rs @@ -0,0 +1,238 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Sinusoidal (Sanson-Flamsteed), PROJ `sinu`. +//! +//! Follows PROJ 9.8.1 `src/projections/gn_sinu.cpp` (Snyder 1987 +//! pp. 243–248, eqs. 30-1 to 30-11), spherical and ellipsoidal. NodeDB +//! SRID 990001 is the MODIS sinusoidal grid on a sphere of radius +//! 6 371 007.181 m. + +use core::f64::consts::{FRAC_PI_2, PI}; +use core::fmt::Write; + +use serde::{Deserialize, Serialize}; + +use super::angle::wrap_pi; +use super::meridian::MeridianArc; +use crate::def::Ellipsoid; +use crate::error::{CrsParseError, DomainReason}; +use crate::parse::tokens::ProjParams; + +/// PROJ `EPS10`: an inverse latitude this far past ±90° rounds to the pole. +const EPS10: f64 = 1e-10; + +/// Defining parameters of one Sinusoidal projection. +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Serialize, + Deserialize, + zerompk::ToMessagePack, + zerompk::FromMessagePack, +)] +pub struct SinuParams { + /// Central meridian λ0, degrees (`+lon_0`). + pub lon0_deg: f64, + /// False easting, metres (`+x_0`). + pub false_easting: f64, + /// False northing, metres (`+y_0`). + pub false_northing: f64, +} + +impl SinuParams { + /// Read from PROJ-string parameters. Every key defaults to 0 as in PROJ. + pub fn from_proj(p: &ProjParams<'_>) -> Result { + Ok(Self { + lon0_deg: p.number_or("lon_0", 0.0)?, + false_easting: p.number_or("x_0", 0.0)?, + false_northing: p.number_or("y_0", 0.0)?, + }) + } + + /// Append the defining parameters as PROJ-string tokens. + pub fn write_proj(&self, out: &mut String) { + let _ = write!( + out, + " +proj=sinu +lon_0={} +x_0={} +y_0={}", + self.lon0_deg, self.false_easting, self.false_northing + ); + } +} + +/// Sinusoidal with its constants computed. +#[derive(Debug, Clone, PartialEq)] +pub struct Sinusoidal { + params: SinuParams, + a: f64, + es: f64, + lon0: f64, + /// Meridian arc; `None` on a sphere. + arc: Option, +} + +impl Sinusoidal { + pub fn new(p: &SinuParams, ell: &Ellipsoid) -> Self { + let es = ell.e2(); + Self { + params: *p, + a: ell.a, + es, + lon0: p.lon0_deg.to_radians(), + arc: if ell.is_sphere() { + None + } else { + Some(MeridianArc::new(es)) + }, + } + } + + /// Ellipsoid: x = λ cos φ / √(1 − e² sin² φ), y = M(φ), Snyder eqs. + /// 30-8, 30-9. Sphere: x = λ cos φ, y = φ, Snyder eqs. 30-1, 30-2. + pub fn forward(&self, lam: f64, phi: f64) -> Result<(f64, f64), DomainReason> { + let lam = wrap_pi(lam - self.lon0); + let (s, c) = phi.sin_cos(); + let (x, y) = match &self.arc { + Some(arc) => (lam * c / (1.0 - self.es * s * s).sqrt(), arc.arc(phi, s, c)), + None => (lam * c, phi), + }; + Ok(( + self.params.false_easting + self.a * x, + self.params.false_northing + self.a * y, + )) + } + + /// PROJ `gn_sinu_e_inverse` and `gn_sinu_s_inverse`. A point past the + /// map's outline (|λ − λ0| > 180°) is outside the projection. + pub fn inverse(&self, x: f64, y: f64) -> Result<(f64, f64), DomainReason> { + let x = (x - self.params.false_easting) / self.a; + let y = (y - self.params.false_northing) / self.a; + let (lam, phi) = match &self.arc { + Some(arc) => { + let phi = arc.inverse(y)?; + let s = phi.abs(); + if s < FRAC_PI_2 { + let sp = phi.sin(); + (x * (1.0 - self.es * sp * sp).sqrt() / phi.cos(), phi) + } else if s - EPS10 < FRAC_PI_2 { + (0.0, phi.clamp(-FRAC_PI_2, FRAC_PI_2)) + } else { + return Err(DomainReason::OutsideProjection); + } + } + None => { + if y.abs() > FRAC_PI_2 { + return Err(DomainReason::OutsideProjection); + } + (x / y.cos(), y) + } + }; + if lam.abs() > PI + EPS10 { + return Err(DomainReason::OutsideProjection); + } + Ok((wrap_pi(lam + self.lon0), phi)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const MM: f64 = 1e-3; + const DEG_TOL: f64 = 1e-9; + + fn check(p: &SinuParams, ell: &Ellipsoid, pts: &[[f64; 4]]) { + let sinu = Sinusoidal::new(p, ell); + for [lon, lat, x, y] in pts.iter().copied() { + let (gx, gy) = sinu.forward(lon.to_radians(), lat.to_radians()).unwrap(); + assert!( + (gx - x).abs() < MM && (gy - y).abs() < MM, + "{lon} {lat}: {gx} {gy}" + ); + let (glam, gphi) = sinu.inverse(x, y).unwrap(); + assert!( + (glam.to_degrees() - lon).abs() < DEG_TOL + && (gphi.to_degrees() - lat).abs() < DEG_TOL, + "{x} {y}: {} {}", + glam.to_degrees(), + gphi.to_degrees() + ); + } + } + + fn modis() -> SinuParams { + SinuParams { + lon0_deg: 0.0, + false_easting: 0.0, + false_northing: 0.0, + } + } + + /// NodeDB SRID 990001 rows of `tests/fixtures/proj_reference.tsv` + /// (PROJ 9.8.1 via pyproj 3.8.0), `+proj=sinu +lon_0=0 +x_0=0 +y_0=0 + /// +R=6371007.181`. (lon, lat, x, y). + #[test] + fn modis_sinusoidal() { + check( + &modis(), + &Ellipsoid::sphere(6_371_007.181), + &[ + [ + -170.0, + -60.0, + -9_451_579.418_015_448, + -6_671_703.118_599_138, + ], + [ + -120.0, + -30.0, + -11_555_728.774_429_437, + -3_335_851.559_299_569, + ], + [-60.0, 0.5, -6_671_449.080_587_624, 55_597.525_988_326_16], + [0.0, 0.0, 0.0, 0.0], + [30.0, 45.0, 2_358_803.258_612_444, 5_003_777.338_949_354], + ], + ); + } + + /// PROJ 9.8.1 via pyproj 3.8.0, `+proj=sinu +lon_0=-90 +x_0=100 + /// +y_0=-200 +ellps=WGS84`. + #[test] + fn ellipsoid() { + let p = SinuParams { + lon0_deg: -90.0, + false_easting: 100.0, + false_northing: -200.0, + }; + check(&p, &Ellipsoid::WGS84, ELLIPSOID_PTS); + } + + const ELLIPSOID_PTS: &[[f64; 4]] = &[ + [-100.0, 40.0, -853838.5695861845, 4429329.030350515], + [-60.0, -30.0, 2894688.407526896, -3320313.3979403824], + [0.0, 75.0, 2601280.5228825966, 8326737.587280346], + [-90.0, 0.0, 100.0, -200.0], + ]; + + #[test] + fn outside_outline_is_out_of_domain() { + let sinu = Sinusoidal::new(&modis(), &Ellipsoid::sphere(1.0)); + assert_eq!(sinu.inverse(3.5, 0.0), Err(DomainReason::OutsideProjection)); + assert_eq!(sinu.inverse(0.0, 1.6), Err(DomainReason::OutsideProjection)); + } + + #[test] + fn parses_and_writes() { + let p = ProjParams::tokenize( + "+proj=sinu +lon_0=0 +x_0=0 +y_0=0 +R=6371007.181 +units=m +no_defs", + ) + .unwrap(); + let q = SinuParams::from_proj(&p).unwrap(); + assert_eq!(q, modis()); + let mut s = String::new(); + q.write_proj(&mut s); + assert_eq!(s, " +proj=sinu +lon_0=0 +x_0=0 +y_0=0"); + } +} diff --git a/nodedb-crs/src/method/somerc.rs b/nodedb-crs/src/method/somerc.rs new file mode 100644 index 000000000..640e7e99e --- /dev/null +++ b/nodedb-crs/src/method/somerc.rs @@ -0,0 +1,331 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Swiss Oblique Mercator, by PROJ `src/projections/somerc.cpp`. +//! +//! A conformal projection of the ellipsoid onto the Gauss sphere, then an +//! oblique Mercator with the initial line at azimuth 90° through the +//! projection centre (swisstopo, "Formulas and constants for the +//! calculation of the Swiss conformal cylindrical projection", 2016). EPSG +//! lists these CRSs under method 9815, Hotine Oblique Mercator (variant B). + +use core::f64::consts::{FRAC_PI_2, FRAC_PI_4}; +use core::fmt::Write; + +use serde::{Deserialize, Serialize}; + +use super::angle::wrap_pi; +use crate::def::Ellipsoid; +use crate::error::{CrsParseError, DomainReason}; +use crate::parse::tokens::ProjParams; + +/// Newton step on the latitude below which the inverse stops (PROJ `EPS`). +const LAT_TOLERANCE: f64 = 1e-10; + +/// Newton iterations allowed (PROJ `NITER`). +const LAT_MAX_ITERS: usize = 6; + +/// |sin| beyond 1 that PROJ `aasin` still rounds to ±π/2 (PROJ `ONE_TOL`). +const ASIN_ONE_TOL: f64 = 1.000_000_000_000_01; + +/// cos of the oblique latitude below which the point is on the oblique pole. +const OBLIQUE_POLE_COS_EPS: f64 = 1e-15; + +/// Largest |φ0| in degrees: the Gauss sphere needs cos φ0 > 0. +const MAX_LAT0_DEG: f64 = 90.0; + +/// Defining parameters of one Swiss Oblique Mercator projection. +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Serialize, + Deserialize, + zerompk::ToMessagePack, + zerompk::FromMessagePack, +)] +pub struct SomercParams { + /// Latitude of the projection centre φ0, degrees. + pub lat0_deg: f64, + /// Longitude of the projection centre λ0, degrees. + pub lon0_deg: f64, + /// Scale factor at the projection centre. + pub k0: f64, + /// False easting, metres. + pub false_easting: f64, + /// False northing, metres. + pub false_northing: f64, +} + +impl SomercParams { + /// Read from PROJ-string parameters. Every parameter has a PROJ + /// default: φ0 = λ0 = 0, k0 = 1, no false origin. + pub fn from_proj(p: &ProjParams<'_>) -> Result { + let lat0_deg = p.number_or("lat_0", 0.0)?; + if lat0_deg.abs() >= MAX_LAT0_DEG { + return Err(CrsParseError::invalid("lat_0", &lat0_deg.to_string())); + } + let k0 = p.number_any(&["k_0", "k"])?.unwrap_or(1.0); + if k0 <= 0.0 { + return Err(CrsParseError::invalid("k_0", &k0.to_string())); + } + Ok(Self { + lat0_deg, + lon0_deg: p.number_or("lon_0", 0.0)?, + k0, + false_easting: p.number_or("x_0", 0.0)?, + false_northing: p.number_or("y_0", 0.0)?, + }) + } + + /// Append the PROJ form of the defining parameters. + pub fn write_proj(&self, out: &mut String) { + let _ = write!( + out, + " +proj=somerc +lat_0={} +lon_0={} +k_0={} +x_0={} +y_0={}", + self.lat0_deg, self.lon0_deg, self.k0, self.false_easting, self.false_northing + ); + } +} + +/// A Swiss Oblique Mercator projection with its constants computed. +#[derive(Debug, Clone, PartialEq)] +pub struct SwissObliqueMercator { + params: SomercParams, + /// λ0, radians. + lon0: f64, + e: f64, + /// 1 / (1 − e²). + rone_es: f64, + /// e / 2. + hlf_e: f64, + /// Gauss sphere exponent α (PROJ `c`). + c: f64, + /// Gauss sphere constant K. + k: f64, + /// k0 · R / a, the sphere radius in units of a (PROJ `kR`). + k_r: f64, + /// sin and cos of the centre latitude on the sphere b0. + sinp0: f64, + cosp0: f64, + /// Semi-major axis, metres. + a: f64, +} + +impl SwissObliqueMercator { + pub fn new(p: &SomercParams, ell: &Ellipsoid) -> Self { + let e2 = ell.e2(); + let e = ell.e(); + let hlf_e = 0.5 * e; + let phi0 = p.lat0_deg.to_radians(); + let (sin_p0, cos_p0) = phi0.sin_cos(); + let rone_es = 1.0 / (1.0 - e2); + // Gauss sphere constants, PROJ somerc.cpp setup. + let c = (1.0 + e2 * cos_p0.powi(4) * rone_es).sqrt(); + let sinp0 = sin_p0 / c; + let phip0 = sinp0.asin(); + let es0 = e * sin_p0; + let k = (FRAC_PI_4 + 0.5 * phip0).tan().ln() + - c * ((FRAC_PI_4 + 0.5 * phi0).tan().ln() - hlf_e * ((1.0 + es0) / (1.0 - es0)).ln()); + let k_r = p.k0 * (1.0 - e2).sqrt() / (1.0 - es0 * es0); + Self { + params: *p, + lon0: p.lon0_deg.to_radians(), + e, + rone_es, + hlf_e, + c, + k, + k_r, + sinp0, + cosp0: phip0.cos(), + a: ell.a, + } + } + + /// Project λ, φ radians to easting, northing metres. + pub fn forward(&self, lam: f64, phi: f64) -> Result<(f64, f64), DomainReason> { + let dlam = wrap_pi(lam - self.lon0); + let sp = self.e * phi.sin(); + // Latitude and longitude on the Gauss sphere. + let phip = 2.0 + * (self.c + * ((FRAC_PI_4 + 0.5 * phi).tan().ln() + - self.hlf_e * ((1.0 + sp) / (1.0 - sp)).ln()) + + self.k) + .exp() + .atan() + - FRAC_PI_2; + let lamp = self.c * dlam; + let cp = phip.cos(); + // Rotation to the oblique sphere. + let phipp = aasin(self.cosp0 * phip.sin() - self.sinp0 * cp * lamp.cos())?; + let cos_phipp = phipp.cos(); + if cos_phipp < OBLIQUE_POLE_COS_EPS { + return Err(DomainReason::Singularity); + } + let lampp = aasin(cp * lamp.sin() / cos_phipp)?; + let x = self.k_r * lampp; + let y = self.k_r * (FRAC_PI_4 + 0.5 * phipp).tan().ln(); + Ok(( + self.a * x + self.params.false_easting, + self.a * y + self.params.false_northing, + )) + } + + /// Unproject easting, northing metres to λ, φ radians. + pub fn inverse(&self, x: f64, y: f64) -> Result<(f64, f64), DomainReason> { + let x = (x - self.params.false_easting) / self.a; + let y = (y - self.params.false_northing) / self.a; + let phipp = 2.0 * ((y / self.k_r).exp().atan() - FRAC_PI_4); + let lampp = x / self.k_r; + let cp = phipp.cos(); + let mut phip = aasin(self.cosp0 * phipp.sin() + self.sinp0 * cp * lampp.cos())?; + let lamp = aasin(cp * lampp.sin() / phip.cos())?; + let con = (self.k - (FRAC_PI_4 + 0.5 * phip).tan().ln()) / self.c; + // Newton iteration from the sphere latitude to the ellipsoid latitude. + for _ in 0..LAT_MAX_ITERS { + let esp = self.e * phip.sin(); + let delp = (con + (FRAC_PI_4 + 0.5 * phip).tan().ln() + - self.hlf_e * ((1.0 + esp) / (1.0 - esp)).ln()) + * (1.0 - esp * esp) + * phip.cos() + * self.rone_es; + phip -= delp; + if delp.abs() < LAT_TOLERANCE { + return Ok((self.lon0 + lamp / self.c, phip)); + } + } + Err(DomainReason::NoConvergence) + } +} + +/// asin that rounds an argument just past ±1 to ±π/2 and refuses one +/// further out, as PROJ `aasin` does. +fn aasin(v: f64) -> Result { + if v.abs() < 1.0 { + Ok(v.asin()) + } else if v.abs() <= ASIN_ONE_TOL { + Ok(FRAC_PI_2.copysign(v)) + } else { + Err(DomainReason::OutsideProjection) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// One millimetre, the tolerance for PROJ reference values. + const MM: f64 = 1e-3; + + /// Inverse tolerance, degrees. + const DEG_TOL: f64 = 1e-9; + + /// EPSG:2056 CH1903+ / LV95. + const LV95: SomercParams = SomercParams { + lat0_deg: 46.952_405_555_555_6, + lon0_deg: 7.439_583_333_333_33, + k0: 1.0, + false_easting: 2_600_000.0, + false_northing: 1_200_000.0, + }; + + /// EPSG:23700 HD72 / EOV. + const EOV: SomercParams = SomercParams { + lat0_deg: 47.144_393_722_222_2, + lon0_deg: 19.048_571_777_777_8, + k0: 0.99993, + false_easting: 650_000.0, + false_northing: 200_000.0, + }; + + /// PROJ 9.8.1 (pyproj 3.8.0): `+proj=somerc +lat_0=46.9524055555556 + /// +lon_0=7.43958333333333 +k_0=1 +x_0=2600000 +y_0=1200000 + /// +ellps=bessel`. + const PROJ_LV95: [([f64; 2], [f64; 2]); 4] = [ + ( + [8.5417, 47.3769], + [2_683_220.754_828_522_4, 1_247_772.848_570_866_7], + ), + ( + [6.1432, 46.2044], + [2_499_956.536_283_736, 1_117_683.860_689_401_9], + ), + ( + [9.8, 46.5], + [2_781_147.794_173_625, 1_152_440.159_175_334_7], + ), + ( + [7.0, 45.9], + [2_565_885.713_119_430_5, 1_083_117.132_843_990_6], + ), + ]; + + /// PROJ 9.8.1: `+proj=somerc +lat_0=47.1443937222222 + /// +lon_0=19.0485717777778 +k_0=0.99993 +x_0=650000 +y_0=200000 + /// +ellps=GRS67`. + const PROJ_EOV: [([f64; 2], [f64; 2]); 3] = [ + ( + [19.0402, 47.4979], + [649_369.245_120_057_7, 239_299.475_175_236_32], + ), + ( + [21.6, 46.3], + [846_544.024_531_169_1, 109_343.850_162_499_37], + ), + ([16.5, 47.7], [458_749.226_525_301_1, 264_886.966_444_310_9]), + ]; + + fn check(p: SomercParams, ell: Ellipsoid, refs: &[([f64; 2], [f64; 2])]) { + let s = SwissObliqueMercator::new(&p, &ell); + for (ll, xy) in refs { + let (x, y) = s.forward(ll[0].to_radians(), ll[1].to_radians()).unwrap(); + assert!( + (x - xy[0]).abs() < MM && (y - xy[1]).abs() < MM, + "{ll:?}: got {x}, {y}" + ); + let (lam, phi) = s.inverse(xy[0], xy[1]).unwrap(); + assert!((lam.to_degrees() - ll[0]).abs() < DEG_TOL, "{ll:?}"); + assert!((phi.to_degrees() - ll[1]).abs() < DEG_TOL, "{ll:?}"); + } + } + + #[test] + fn matches_proj_lv95() { + check(LV95, Ellipsoid::BESSEL_1841, &PROJ_LV95); + } + + #[test] + fn matches_proj_eov() { + check(EOV, Ellipsoid::GRS67, &PROJ_EOV); + } + + /// The projection centre maps to the false origin. + #[test] + fn centre_maps_to_false_origin() { + let s = SwissObliqueMercator::new(&LV95, &Ellipsoid::BESSEL_1841); + let (x, y) = s + .forward(LV95.lon0_deg.to_radians(), LV95.lat0_deg.to_radians()) + .unwrap(); + assert!((x - LV95.false_easting).abs() < MM && (y - LV95.false_northing).abs() < MM); + } + + #[test] + fn parses_and_writes_proj() { + let text = "+proj=somerc +lat_0=46.9524055555556 +lon_0=7.43958333333333 +k_0=1 \ + +x_0=2600000 +y_0=1200000 +ellps=bessel +units=m +no_defs"; + let p = SomercParams::from_proj(&ProjParams::tokenize(text).unwrap()).unwrap(); + assert_eq!(p, LV95); + let mut out = String::new(); + p.write_proj(&mut out); + assert_eq!( + SomercParams::from_proj(&ProjParams::tokenize(&out).unwrap()).unwrap(), + p + ); + assert!( + SomercParams::from_proj(&ProjParams::tokenize("+proj=somerc +lat_0=90").unwrap()) + .is_err() + ); + } +} diff --git a/nodedb-crs/src/method/stere.rs b/nodedb-crs/src/method/stere.rs new file mode 100644 index 000000000..23c44d537 --- /dev/null +++ b/nodedb-crs/src/method/stere.rs @@ -0,0 +1,642 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Stereographic, PROJ `stere` and `ups`. +//! +//! Covers Polar Stereographic EPSG methods 9810 (variant A, `+k_0`), 9829 +//! (variant B, `+lat_ts`) and 9830 (variant C, written by PROJ as variant B +//! with a shifted false northing), plus the oblique and equatorial aspects +//! of PROJ `stere`. Follows PROJ 9.8.1 `src/projections/stere.cpp`, IOGP +//! Guidance Note 7-2 §3.3.2 and Snyder 1987 eqs. 21-1 to 21-44. + +use core::f64::consts::{FRAC_PI_2, FRAC_PI_4}; +use core::fmt::Write; + +use serde::{Deserialize, Serialize}; + +use super::angle::wrap_pi; +use super::conformal::tsfn; +use crate::def::Ellipsoid; +use crate::error::{CrsParseError, DomainReason}; +use crate::parse::tokens::ProjParams; + +/// PROJ `stere.cpp` EPS10: aspect and degenerate-value tolerance. +const EPS10: f64 = 1e-10; + +/// PROJ `stere.cpp` TOL: spherical tolerance at the opposite pole. +const SPHERE_POLE_TOL: f64 = 1e-8; + +/// PROJ `stere.cpp`: |φ − 90°| below which the pole maps to x = 0. +const POLE_EXACT_TOL: f64 = 1e-15; + +/// PROJ `stere.cpp` NITER: inverse iterations allowed. +const MAX_ITERS: usize = 8; + +/// PROJ `stere.cpp` CONV: inverse convergence on φ, radians. +const CONV: f64 = 1e-10; + +/// Largest absolute latitude, degrees. +const MAX_LAT_DEG: f64 = 90.0; + +/// UPS scale factor at the pole (PROJ `ups`). +const UPS_K0: f64 = 0.994; + +/// UPS false easting and northing, metres (PROJ `ups`). +const UPS_FALSE_ORIGIN: f64 = 2_000_000.0; + +/// Defining parameters of one Stereographic projection. +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Serialize, + Deserialize, + zerompk::ToMessagePack, + zerompk::FromMessagePack, +)] +pub struct StereParams { + /// Latitude of natural origin φ0, degrees. ±90 selects the polar aspect. + pub lat0_deg: f64, + /// Longitude of natural origin λ0, degrees. + pub lon0_deg: f64, + /// Latitude of true scale φc, degrees. Used by the polar aspect only. + /// When set (and not ±90), PROJ ignores `k0` on the polar aspect. + pub lat_ts_deg: Option, + /// Scale factor at the natural origin. + pub k0: f64, + /// False easting, metres. + pub false_easting: f64, + /// False northing, metres. + pub false_northing: f64, +} + +impl StereParams { + /// Read `+proj=stere` parameters. + pub fn from_proj(p: &ProjParams<'_>) -> Result { + let lat0_deg = p.number_or("lat_0", 0.0)?; + if lat0_deg.abs() > MAX_LAT_DEG { + return Err(CrsParseError::invalid("lat_0", &lat0_deg.to_string())); + } + let lat_ts_deg = p.number("lat_ts")?; + if let Some(lat_ts) = lat_ts_deg + && lat_ts.abs() > MAX_LAT_DEG + { + return Err(CrsParseError::invalid("lat_ts", &lat_ts.to_string())); + } + let k0 = p.number_any(&["k_0", "k"])?.unwrap_or(1.0); + if k0 <= 0.0 { + return Err(CrsParseError::invalid("k_0", &k0.to_string())); + } + Ok(Self { + lat0_deg, + lon0_deg: p.number_or("lon_0", 0.0)?, + lat_ts_deg, + k0, + false_easting: p.number_or("x_0", 0.0)?, + false_northing: p.number_or("y_0", 0.0)?, + }) + } + + /// Read `+proj=ups` parameters. PROJ fixes every value except the + /// hemisphere (`+south`). PROJ refuses `ups` on a sphere, a check the + /// caller makes with the ellipsoid in hand. + pub fn from_proj_ups(p: &ProjParams<'_>) -> Self { + Self { + lat0_deg: if p.has("south") { + -MAX_LAT_DEG + } else { + MAX_LAT_DEG + }, + lon0_deg: 0.0, + lat_ts_deg: None, + k0: UPS_K0, + false_easting: UPS_FALSE_ORIGIN, + false_northing: UPS_FALSE_ORIGIN, + } + } + + /// Append ` +proj=stere` and every defining parameter. + pub fn write_proj(&self, out: &mut String) { + let _ = write!(out, " +proj=stere +lat_0={}", self.lat0_deg); + if let Some(lat_ts) = self.lat_ts_deg { + let _ = write!(out, " +lat_ts={lat_ts}"); + } + let _ = write!( + out, + " +lon_0={} +k_0={} +x_0={} +y_0={}", + self.lon0_deg, self.k0, self.false_easting, self.false_northing + ); + } +} + +/// Aspect of the projection, PROJ `stere.cpp` Mode. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Aspect { + SouthPole, + NorthPole, + Oblique, + Equatorial, +} + +/// A Stereographic projection with its constants computed. +#[derive(Debug, Clone, PartialEq)] +pub struct Stereographic { + aspect: Aspect, + sphere: bool, + e: f64, + /// Semi-major axis a, metres. + a: f64, + /// PROJ `akm1`, the radius scale over a. + akm1: f64, + /// sin and cos of the conformal latitude of φ0 (of φ0 on a sphere). + sin_x1: f64, + cos_x1: f64, + /// φ0 in radians. + phi0: f64, + /// λ0 in radians. + lon0: f64, + false_easting: f64, + false_northing: f64, +} + +/// PROJ `ssfn_`: tan((π/2 + φ)/2) · ((1 − e sin φ)/(1 + e sin φ))^(e/2), +/// so 2 atan(ssfn) − π/2 is the conformal latitude (Snyder 3-1). +fn ssfn(phi: f64, sinphi: f64, e: f64) -> f64 { + let es = e * sinphi; + (0.5 * (FRAC_PI_2 + phi)).tan() * ((1.0 - es) / (1.0 + es)).powf(0.5 * e) +} + +/// Conformal latitude χ of φ (Snyder 3-1). +fn conformal_lat(phi: f64, sinphi: f64, e: f64) -> f64 { + 2.0 * ssfn(phi, sinphi, e).atan() - FRAC_PI_2 +} + +impl Stereographic { + pub fn new(p: &StereParams, ell: &Ellipsoid) -> Self { + let phi0 = p.lat0_deg.to_radians(); + let aspect = if (phi0.abs() - FRAC_PI_2).abs() < EPS10 { + if phi0 < 0.0 { + Aspect::SouthPole + } else { + Aspect::NorthPole + } + } else if phi0.abs() > EPS10 { + Aspect::Oblique + } else { + Aspect::Equatorial + }; + let phits = p.lat_ts_deg.map_or(FRAC_PI_2, |d| d.to_radians().abs()); + let ts_at_pole = (phits - FRAC_PI_2).abs() < EPS10; + let sphere = ell.is_sphere(); + let e = ell.e(); + let (mut sin_x1, mut cos_x1) = (0.0, 0.0); + let akm1 = match (sphere, aspect) { + (false, Aspect::NorthPole | Aspect::SouthPole) => { + if ts_at_pole { + // GN 7-2 §3.3.2.1: ρ = 2 a k0 t / sqrt((1+e)^(1+e) (1−e)^(1−e)). + 2.0 * p.k0 / ((1.0 + e).powf(1.0 + e) * (1.0 - e).powf(1.0 - e)).sqrt() + } else { + // GN 7-2 §3.3.2.2: mc / tc. + let t = phits.sin(); + let et = e * t; + phits.cos() / tsfn(phits, t, e) / (1.0 - et * et).sqrt() + } + } + (false, Aspect::Oblique | Aspect::Equatorial) => { + let t = phi0.sin(); + let x1 = conformal_lat(phi0, t, e); + (sin_x1, cos_x1) = x1.sin_cos(); + let et = e * t; + 2.0 * p.k0 * phi0.cos() / (1.0 - et * et).sqrt() + } + (true, Aspect::Oblique | Aspect::Equatorial) => { + if aspect == Aspect::Oblique { + (sin_x1, cos_x1) = phi0.sin_cos(); + } + 2.0 * p.k0 + } + (true, Aspect::NorthPole | Aspect::SouthPole) => { + if ts_at_pole { + 2.0 * p.k0 + } else { + phits.cos() / (FRAC_PI_4 - 0.5 * phits).tan() + } + } + }; + Self { + aspect, + sphere, + e, + a: ell.a, + akm1, + sin_x1, + cos_x1, + phi0, + lon0: p.lon0_deg.to_radians(), + false_easting: p.false_easting, + false_northing: p.false_northing, + } + } + + /// Project λ, φ radians to easting, northing metres. + pub fn forward(&self, lam: f64, phi: f64) -> Result<(f64, f64), DomainReason> { + let dlam = wrap_pi(lam - self.lon0); + let (x, y) = if self.sphere { + self.forward_sphere(dlam, phi)? + } else { + self.forward_ellipsoid(dlam, phi)? + }; + Ok(( + self.false_easting + self.a * x, + self.false_northing + self.a * y, + )) + } + + /// PROJ `stere_e_forward`, in units of a. + fn forward_ellipsoid(&self, lam: f64, phi: f64) -> Result<(f64, f64), DomainReason> { + let (sinlam, mut coslam) = lam.sin_cos(); + let mut sinphi = phi.sin(); + let mut phi = phi; + let (x, y) = match self.aspect { + Aspect::Oblique | Aspect::Equatorial => { + let (sin_x, cos_x) = conformal_lat(phi, sinphi, self.e).sin_cos(); + if self.aspect == Aspect::Oblique { + let denom = + self.cos_x1 * (1.0 + self.sin_x1 * sin_x + self.cos_x1 * cos_x * coslam); + if denom == 0.0 { + return Err(DomainReason::Antipode); + } + let a = self.akm1 / denom; + ( + a * cos_x, + a * (self.cos_x1 * sin_x - self.sin_x1 * cos_x * coslam), + ) + } else { + let denom = 1.0 + cos_x * coslam; + if denom == 0.0 { + return Err(DomainReason::Antipode); + } + let a = self.akm1 / denom; + (a * cos_x, a * sin_x) + } + } + Aspect::NorthPole | Aspect::SouthPole => { + if self.aspect == Aspect::SouthPole { + phi = -phi; + coslam = -coslam; + sinphi = -sinphi; + } + if (phi + FRAC_PI_2).abs() < EPS10 { + return Err(DomainReason::Singularity); + } + // GN 7-2 §3.3.2.1: ρ = akm1 · t(φ). + let rho = if (phi - FRAC_PI_2).abs() < POLE_EXACT_TOL { + 0.0 + } else { + self.akm1 * tsfn(phi, sinphi, self.e) + }; + (rho, -rho * coslam) + } + }; + Ok((x * sinlam, y)) + } + + /// PROJ `stere_s_forward`, in units of a. + fn forward_sphere(&self, lam: f64, phi: f64) -> Result<(f64, f64), DomainReason> { + let (sinphi, cosphi) = phi.sin_cos(); + let (sinlam, mut coslam) = lam.sin_cos(); + match self.aspect { + Aspect::Oblique | Aspect::Equatorial => { + let oblique = self.aspect == Aspect::Oblique; + let d = if oblique { + 1.0 + self.sin_x1 * sinphi + self.cos_x1 * cosphi * coslam + } else { + 1.0 + cosphi * coslam + }; + if d <= EPS10 { + return Err(DomainReason::Antipode); + } + let a = self.akm1 / d; + let y = if oblique { + self.cos_x1 * sinphi - self.sin_x1 * cosphi * coslam + } else { + sinphi + }; + Ok((a * cosphi * sinlam, a * y)) + } + Aspect::NorthPole | Aspect::SouthPole => { + let mut phi = phi; + if self.aspect == Aspect::NorthPole { + coslam = -coslam; + phi = -phi; + } + if (phi - FRAC_PI_2).abs() < SPHERE_POLE_TOL { + return Err(DomainReason::Singularity); + } + let rho = self.akm1 * (FRAC_PI_4 + 0.5 * phi).tan(); + Ok((sinlam * rho, rho * coslam)) + } + } + } + + /// Unproject easting, northing metres to λ, φ radians. + pub fn inverse(&self, x: f64, y: f64) -> Result<(f64, f64), DomainReason> { + let x = (x - self.false_easting) / self.a; + let y = (y - self.false_northing) / self.a; + let (lam, phi) = if self.sphere { + self.inverse_sphere(x, y) + } else { + self.inverse_ellipsoid(x, y)? + }; + Ok((wrap_pi(self.lon0 + lam), phi)) + } + + /// PROJ `stere_e_inverse`, in units of a. + fn inverse_ellipsoid(&self, mut x: f64, mut y: f64) -> Result<(f64, f64), DomainReason> { + let rho = x.hypot(y); + let (tp, mut phi_l, halfpi, halfe) = match self.aspect { + Aspect::Oblique | Aspect::Equatorial => { + let (sin_c, cos_c) = (2.0 * (rho * self.cos_x1).atan2(self.akm1)).sin_cos(); + let phi_l = if rho == 0.0 { + (cos_c * self.sin_x1).asin() + } else { + (cos_c * self.sin_x1 + y * sin_c * self.cos_x1 / rho).asin() + }; + x *= sin_c; + y = rho * self.cos_x1 * cos_c - y * self.sin_x1 * sin_c; + ( + (0.5 * (FRAC_PI_2 + phi_l)).tan(), + phi_l, + FRAC_PI_2, + 0.5 * self.e, + ) + } + Aspect::NorthPole | Aspect::SouthPole => { + if self.aspect == Aspect::NorthPole { + y = -y; + } + let tp = -rho / self.akm1; + (tp, FRAC_PI_2 - 2.0 * tp.atan(), -FRAC_PI_2, -0.5 * self.e) + } + }; + // Snyder 7-9 fixed-point iteration on φ. + for _ in 0..MAX_ITERS { + let es = self.e * phi_l.sin(); + let phi = 2.0 * (tp * ((1.0 + es) / (1.0 - es)).powf(halfe)).atan() - halfpi; + if (phi_l - phi).abs() < CONV { + let phi = if self.aspect == Aspect::SouthPole { + -phi + } else { + phi + }; + let lam = if x == 0.0 && y == 0.0 { + 0.0 + } else { + x.atan2(y) + }; + return Ok((lam, phi)); + } + phi_l = phi; + } + Err(DomainReason::NoConvergence) + } + + /// PROJ `stere_s_inverse`, in units of a. + fn inverse_sphere(&self, x: f64, mut y: f64) -> (f64, f64) { + let rh = x.hypot(y); + let (sinc, cosc) = (2.0 * (rh / self.akm1).atan()).sin_cos(); + match self.aspect { + Aspect::Equatorial => { + let phi = if rh <= EPS10 { + 0.0 + } else { + (y * sinc / rh).asin() + }; + let lam = if cosc != 0.0 || x != 0.0 { + (x * sinc).atan2(cosc * rh) + } else { + 0.0 + }; + (lam, phi) + } + Aspect::Oblique => { + let phi = if rh <= EPS10 { + self.phi0 + } else { + (cosc * self.sin_x1 + y * sinc * self.cos_x1 / rh).asin() + }; + let c = cosc - self.sin_x1 * phi.sin(); + let lam = if c != 0.0 || x != 0.0 { + (x * sinc * self.cos_x1).atan2(c * rh) + } else { + 0.0 + }; + (lam, phi) + } + Aspect::NorthPole | Aspect::SouthPole => { + let south = self.aspect == Aspect::SouthPole; + if !south { + y = -y; + } + let phi = if rh <= EPS10 { + self.phi0 + } else if south { + (-cosc).asin() + } else { + cosc.asin() + }; + let lam = if x == 0.0 && y == 0.0 { + 0.0 + } else { + x.atan2(y) + }; + (lam, phi) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// One millimetre, the tolerance for every PROJ reference value. + const MM: f64 = 1e-3; + + /// Inverse tolerance in degrees. + const DEG_TOL: f64 = 1e-9; + + fn build(text: &str, ell: Ellipsoid) -> Stereographic { + let p = StereParams::from_proj(&ProjParams::tokenize(text).unwrap()).unwrap(); + Stereographic::new(&p, &ell) + } + + fn dms(d: f64, m: f64, s: f64) -> f64 { + d + m / 60.0 + s / 3600.0 + } + + fn check(m: &Stereographic, lon: f64, lat: f64, want: [f64; 2]) { + let (x, y) = m.forward(lon.to_radians(), lat.to_radians()).unwrap(); + assert!( + (x - want[0]).abs() < MM && (y - want[1]).abs() < MM, + "({lon}, {lat}): got ({x}, {y}), want {want:?}" + ); + // The references carry 0.1 mm, so the inverse starts from (x, y). + let (lam, phi) = m.inverse(x, y).unwrap(); + assert!( + (lam.to_degrees() - lon).abs() < DEG_TOL && (phi.to_degrees() - lat).abs() < DEG_TOL, + "inverse ({lon}, {lat}): got ({}, {})", + lam.to_degrees(), + phi.to_degrees() + ); + } + + fn assert_gn(m: &Stereographic, lon: f64, lat: f64, want: [f64; 2]) { + let (x, y) = m.forward(lon.to_radians(), lat.to_radians()).unwrap(); + assert!( + (x - want[0]).abs() < 0.005 && (y - want[1]).abs() < 0.005, + "got ({x}, {y}), GN 7-2 prints {want:?}" + ); + } + + /// GN 7-2 §3.3.2.1 example, EPSG 9810: WGS 84 / UPS North, point 73°N, + /// 44°E. GN 7-2 prints E = 3320416.75 m, N = 632668.43 m. + #[test] + fn gn_9810_ups_north() { + let m = build( + "+proj=stere +lat_0=90 +lon_0=0 +k=0.994 +x_0=2000000 +y_0=2000000", + Ellipsoid::WGS84, + ); + assert_gn(&m, 44.0, 73.0, [3_320_416.75, 632_668.43]); + check(&m, 44.0, 73.0, [3_320_416.747_4, 632_668.431_3]); + check(&m, -150.0, 85.0, [1_722_271.304_3, 2_481_040.211_7]); + } + + /// GN 7-2 §3.3.2.2 example, EPSG 9829: WGS 84 / Australian Antarctic + /// Polar Stereographic, point 75°S, 120°E. GN 7-2 prints + /// E = 7255380.79 m, N = 7053389.56 m. + #[test] + fn gn_9829_australian_antarctic() { + let m = build( + "+proj=stere +lat_0=-90 +lat_ts=-71 +lon_0=70 +x_0=6000000 +y_0=6000000", + Ellipsoid::WGS84, + ); + assert_gn(&m, 120.0, -75.0, [7_255_380.79, 7_053_389.56]); + check(&m, 120.0, -75.0, [7_255_380.793_3, 7_053_389.560_6]); + check(&m, 60.0, -65.0, [5_520_991.162_9, 8_716_594.109_1]); + } + + /// GN 7-2 §3.3.2.3 example, EPSG 9830: Petrels 1972 / Terre Adelie + /// Polar Stereographic, point 66°36'18.820"S, 140°04'17.040"E, in the + /// variant B form PROJ writes. GN 7-2 prints E = 303169.52 m, + /// N = 244055.72 m. + #[test] + fn gn_9830_terre_adelie() { + let m = build( + "+proj=stere +lat_0=-90 +lat_ts=-67 +lon_0=140 +x_0=300000 +y_0=-2299363.4878305923", + Ellipsoid::INTERNATIONAL_1924, + ); + let (lon, lat) = (dms(140.0, 4.0, 17.040), -dms(66.0, 36.0, 18.820)); + assert_gn(&m, lon, lat, [303_169.52, 244_055.72]); + check(&m, lon, lat, [303_169.521_9, 244_055.720_5]); + } + + /// pyproj 3.8.0 / PROJ 9.8.1, "+proj=stere +lat_0=90 +lat_ts=70 + /// +lon_0=-45 +a=6378273 +b=6356889.449" (EPSG:3411). + #[test] + fn nsidc_north() { + let ell = Ellipsoid { + a: 6_378_273.0, + inv_f: 6_378_273.0 / (6_378_273.0 - 6_356_889.449), + }; + let m = build( + "+proj=stere +lat_0=90 +lat_ts=70 +lon_0=-45 +x_0=0 +y_0=0", + ell, + ); + check(&m, -30.0, 75.0, [422_888.049_4, -1_578_239.686_2]); + check(&m, 100.0, 65.0, [1_577_510.572_2, 2_252_918.579_3]); + } + + /// pyproj, "+proj=ups +south +datum=WGS84". + #[test] + fn ups_south() { + let p = StereParams::from_proj_ups(&ProjParams::tokenize("+proj=ups +south").unwrap()); + let m = Stereographic::new(&p, &Ellipsoid::WGS84); + check(&m, 44.0, -73.0, [3_320_416.747_4, 3_367_331.568_7]); + check(&m, -150.0, -85.0, [1_722_271.304_3, 1_518_959.788_3]); + } + + /// pyproj, "+proj=stere +lat_0=40 +lon_0=-100 +k=0.9999 +x_0=1000 + /// +y_0=2000 +ellps=GRS80" and "+proj=stere +lat_0=0 +lon_0=10 +k=1 + /// +ellps=GRS80". + #[test] + fn ellipsoidal_oblique_and_equatorial() { + let m = build( + "+proj=stere +lat_0=40 +lon_0=-100 +k=0.9999 +x_0=1000 +y_0=2000", + Ellipsoid::GRS80, + ); + check(&m, -90.0, 45.0, [790_389.133_4, 604_325.093_6]); + check(&m, -110.0, 30.0, [-970_617.194_7, -1_061_081.187_5]); + let m = build("+proj=stere +lat_0=0 +lon_0=10 +k=1", Ellipsoid::GRS80); + check(&m, 30.0, 20.0, [2_178_140.976_1, 2_302_414.126_0]); + check(&m, -20.0, -10.0, [-3_390_375.947_5, -1_187_625.777_8]); + } + + /// pyproj on R = 6371000: "+proj=stere +lat_0=40 +lon_0=-100 +k=0.9999", + /// "+proj=stere +lat_0=-90 +lat_ts=-71 +lon_0=0" and + /// "+proj=stere +lat_0=90 +lon_0=0 +k=0.994". + #[test] + fn spherical_aspects() { + let r = Ellipsoid::sphere(6_371_000.0); + let m = build("+proj=stere +lat_0=40 +lon_0=-100 +k=0.9999", r); + check(&m, -90.0, 45.0, [786_938.100_1, 602_829.558_7]); + check(&m, -110.0, 30.0, [-970_258.655_7, -1_065_794.116_1]); + let m = build("+proj=stere +lat_0=-90 +lat_ts=-71 +lon_0=0", r); + check(&m, 30.0, -75.0, [815_909.695_2, 1_413_197.046_4]); + check(&m, -100.0, -60.0, [-3_270_746.597_5, -576_720.872_2]); + let m = build("+proj=stere +lat_0=90 +lon_0=0 +k=0.994", r); + check(&m, 30.0, 75.0, [833_725.513_8, -1_444_054.949_4]); + check(&m, -100.0, 60.0, [-3_342_165.074_8, 589_313.876_7]); + } + + #[test] + fn opposite_pole_is_singular() { + let m = build("+proj=stere +lat_0=90 +lat_ts=70", Ellipsoid::WGS84); + assert_eq!(m.forward(0.0, -FRAC_PI_2), Err(DomainReason::Singularity)); + let s = build("+proj=stere +lat_0=-90", Ellipsoid::sphere(6_371_000.0)); + assert_eq!(s.forward(0.0, FRAC_PI_2), Err(DomainReason::Singularity)); + } + + #[test] + fn pole_maps_to_false_origin() { + let m = build( + "+proj=stere +lat_0=-90 +lat_ts=-71 +x_0=5 +y_0=7", + Ellipsoid::WGS84, + ); + let (x, y) = m.forward(0.3, -FRAC_PI_2).unwrap(); + assert!((x - 5.0).abs() < 1e-9 && (y - 7.0).abs() < 1e-9); + let (_, phi) = m.inverse(5.0, 7.0).unwrap(); + assert!((phi + FRAC_PI_2).abs() < 1e-12); + } + + #[test] + fn parse_and_round_trip() { + for text in [ + "+proj=stere +lat_0=-90 +lat_ts=-71 +lon_0=70 +x_0=6000000 +y_0=6000000", + "+proj=stere +lat_0=90 +lon_0=-33 +k=0.994 +x_0=2000000 +y_0=2000000", + ] { + let p = StereParams::from_proj(&ProjParams::tokenize(text).unwrap()).unwrap(); + let mut out = String::new(); + p.write_proj(&mut out); + assert_eq!( + StereParams::from_proj(&ProjParams::tokenize(&out).unwrap()), + Ok(p), + "{out}" + ); + } + assert!( + StereParams::from_proj(&ProjParams::tokenize("+proj=stere +lat_0=91").unwrap()) + .is_err() + ); + } +} diff --git a/nodedb-crs/src/method/sterea.rs b/nodedb-crs/src/method/sterea.rs new file mode 100644 index 000000000..72a4433c0 --- /dev/null +++ b/nodedb-crs/src/method/sterea.rs @@ -0,0 +1,320 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Oblique Stereographic, PROJ `sterea`, EPSG method 9809. +//! +//! A Gauss conformal mapping of the ellipsoid onto a sphere, then the +//! spherical oblique stereographic. Follows PROJ 9.8.1 +//! `src/projections/sterea.cpp` and `src/gauss.cpp`, which match IOGP +//! Guidance Note 7-2 §3.3.1. + +use core::f64::consts::{FRAC_PI_2, FRAC_PI_4}; +use core::fmt::Write; + +use serde::{Deserialize, Serialize}; + +use super::angle::wrap_pi; +use crate::def::Ellipsoid; +use crate::error::{CrsParseError, DomainReason}; +use crate::parse::tokens::ProjParams; + +/// PROJ `gauss.cpp` MAX_ITER: inverse Gauss iterations allowed. +const GAUSS_MAX_ITERS: usize = 20; + +/// PROJ `gauss.cpp` DEL_TOL: inverse Gauss convergence on φ, radians. +const GAUSS_TOL: f64 = 1e-14; + +/// PROJ `gauss.cpp`: φ0/2 + π/4 below which K uses the south-pole form. +const SOUTH_POLE_EPS: f64 = 1e-10; + +/// Largest absolute latitude, degrees. +const MAX_LAT_DEG: f64 = 90.0; + +/// Defining parameters of one Oblique Stereographic projection. +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Serialize, + Deserialize, + zerompk::ToMessagePack, + zerompk::FromMessagePack, +)] +pub struct StereaParams { + /// Latitude of natural origin φ0, degrees. + pub lat0_deg: f64, + /// Longitude of natural origin λ0, degrees. + pub lon0_deg: f64, + /// Scale factor at the natural origin. + pub k0: f64, + /// False easting, metres. + pub false_easting: f64, + /// False northing, metres. + pub false_northing: f64, +} + +impl StereaParams { + /// Read from PROJ-string parameters. + pub fn from_proj(p: &ProjParams<'_>) -> Result { + let lat0_deg = p.number_or("lat_0", 0.0)?; + if lat0_deg.abs() > MAX_LAT_DEG { + return Err(CrsParseError::invalid("lat_0", &lat0_deg.to_string())); + } + let k0 = p.number_any(&["k_0", "k"])?.unwrap_or(1.0); + if k0 <= 0.0 { + return Err(CrsParseError::invalid("k_0", &k0.to_string())); + } + Ok(Self { + lat0_deg, + lon0_deg: p.number_or("lon_0", 0.0)?, + k0, + false_easting: p.number_or("x_0", 0.0)?, + false_northing: p.number_or("y_0", 0.0)?, + }) + } + + /// Append ` +proj=sterea` and every defining parameter. + pub fn write_proj(&self, out: &mut String) { + let _ = write!( + out, + " +proj=sterea +lat_0={} +lon_0={} +k_0={} +x_0={} +y_0={}", + self.lat0_deg, self.lon0_deg, self.k0, self.false_easting, self.false_northing + ); + } +} + +/// PROJ `srat`: ((1 − e sin φ)/(1 + e sin φ))^ratexp. +fn srat(esinp: f64, ratexp: f64) -> f64 { + ((1.0 - esinp) / (1.0 + esinp)).powf(ratexp) +} + +/// The Gauss conformal sphere of one ellipsoid at φ0 (PROJ `gauss.cpp`, +/// GN 7-2 §3.3.1: n = C, c = K, χ0 = phic0). +#[derive(Debug, Clone, PartialEq)] +struct Gauss { + c: f64, + k: f64, + e: f64, + ratexp: f64, +} + +impl Gauss { + /// PROJ `pj_gauss_ini`. Returns the sphere, χ0 and the conformal sphere + /// radius over a, R = sqrt(1 − e²) / (1 − e² sin² φ0). + fn new(e: f64, phi0: f64) -> (Self, f64, f64) { + let es = e * e; + let sphi = phi0.sin(); + let cphi2 = phi0.cos().powi(2); + let rc = (1.0 - es).sqrt() / (1.0 - es * sphi * sphi); + let c = (1.0 + es * cphi2 * cphi2 / (1.0 - es)).sqrt(); + let chi = (sphi / c).asin(); + let ratexp = 0.5 * c * e; + let srat_val = srat(e * sphi, ratexp); + let k = if 0.5 * phi0 + FRAC_PI_4 < SOUTH_POLE_EPS { + 1.0 / srat_val + } else { + (0.5 * chi + FRAC_PI_4).tan() / ((0.5 * phi0 + FRAC_PI_4).tan().powf(c) * srat_val) + }; + (Self { c, k, e, ratexp }, chi, rc) + } + + /// Ellipsoid → sphere, PROJ `pj_gauss`. + fn forward(&self, lam: f64, phi: f64) -> (f64, f64) { + let chi = 2.0 + * (self.k + * (0.5 * phi + FRAC_PI_4).tan().powf(self.c) + * srat(self.e * phi.sin(), self.ratexp)) + .atan() + - FRAC_PI_2; + (self.c * lam, chi) + } + + /// Sphere → ellipsoid, PROJ `pj_inv_gauss`. + fn inverse(&self, lam: f64, chi: f64) -> Result<(f64, f64), DomainReason> { + let num = ((0.5 * chi + FRAC_PI_4).tan() / self.k).powf(1.0 / self.c); + let mut prev = chi; + for _ in 0..GAUSS_MAX_ITERS { + let phi = 2.0 * (num * srat(self.e * prev.sin(), -0.5 * self.e)).atan() - FRAC_PI_2; + if (phi - prev).abs() < GAUSS_TOL { + return Ok((lam / self.c, phi)); + } + prev = phi; + } + Err(DomainReason::NoConvergence) + } +} + +/// An Oblique Stereographic projection with its constants computed. +#[derive(Debug, Clone, PartialEq)] +pub struct ObliqueStereographic { + gauss: Gauss, + /// χ0, the conformal latitude of the origin. + phic0: f64, + sinc0: f64, + cosc0: f64, + /// 2 R / a, the sphere diameter over a. + r2: f64, + /// a · k0. + ak0: f64, + /// λ0 in radians. + lon0: f64, + false_easting: f64, + false_northing: f64, +} + +impl ObliqueStereographic { + pub fn new(p: &StereaParams, ell: &Ellipsoid) -> Self { + let (gauss, phic0, rc) = Gauss::new(ell.e(), p.lat0_deg.to_radians()); + let (sinc0, cosc0) = phic0.sin_cos(); + Self { + gauss, + phic0, + sinc0, + cosc0, + r2: 2.0 * rc, + ak0: ell.a * p.k0, + lon0: p.lon0_deg.to_radians(), + false_easting: p.false_easting, + false_northing: p.false_northing, + } + } + + /// Project λ, φ radians to easting, northing metres. + pub fn forward(&self, lam: f64, phi: f64) -> Result<(f64, f64), DomainReason> { + let (lam_s, chi) = self.gauss.forward(wrap_pi(lam - self.lon0), phi); + let (sinc, cosc) = chi.sin_cos(); + let (sinl, cosl) = lam_s.sin_cos(); + let denom = 1.0 + self.sinc0 * sinc + self.cosc0 * cosc * cosl; + if denom == 0.0 { + return Err(DomainReason::Antipode); + } + let k = self.ak0 * self.r2 / denom; + Ok(( + self.false_easting + k * cosc * sinl, + self.false_northing + k * (self.cosc0 * sinc - self.sinc0 * cosc * cosl), + )) + } + + /// Unproject easting, northing metres to λ, φ radians. + pub fn inverse(&self, x: f64, y: f64) -> Result<(f64, f64), DomainReason> { + let x = (x - self.false_easting) / self.ak0; + let y = (y - self.false_northing) / self.ak0; + let rho = x.hypot(y); + let (lam_s, chi) = if rho != 0.0 { + let (sinc, cosc) = (2.0 * rho.atan2(self.r2)).sin_cos(); + ( + (x * sinc).atan2(rho * self.cosc0 * cosc - y * self.sinc0 * sinc), + (cosc * self.sinc0 + y * sinc * self.cosc0 / rho).asin(), + ) + } else { + (0.0, self.phic0) + }; + let (dlam, phi) = self.gauss.inverse(lam_s, chi)?; + Ok((wrap_pi(self.lon0 + dlam), phi)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// One millimetre, the tolerance for every PROJ reference value. + const MM: f64 = 1e-3; + + /// Inverse tolerance in degrees. + const DEG_TOL: f64 = 1e-9; + + fn build(text: &str, ell: Ellipsoid) -> ObliqueStereographic { + let p = StereaParams::from_proj(&ProjParams::tokenize(text).unwrap()).unwrap(); + ObliqueStereographic::new(&p, &ell) + } + + fn check(m: &ObliqueStereographic, lon: f64, lat: f64, want: [f64; 2]) { + let (x, y) = m.forward(lon.to_radians(), lat.to_radians()).unwrap(); + assert!( + (x - want[0]).abs() < MM && (y - want[1]).abs() < MM, + "({lon}, {lat}): got ({x}, {y}), want {want:?}" + ); + // The references carry 0.1 mm, so the inverse starts from (x, y). + let (lam, phi) = m.inverse(x, y).unwrap(); + assert!( + (lam.to_degrees() - lon).abs() < DEG_TOL && (phi.to_degrees() - lat).abs() < DEG_TOL, + "inverse ({lon}, {lat}): got ({}, {})", + lam.to_degrees(), + phi.to_degrees() + ); + } + + /// GN 7-2 §3.3.1 example, EPSG 9809: Amersfoort / RD New, point 53°N, + /// 6°E. GN 7-2 prints E = 196105.283 m, N = 557057.739 m. The other + /// points are pyproj 3.8.0 / PROJ 9.8.1 on the same string. + #[test] + fn gn_9809_rd_new() { + let m = build( + "+proj=sterea +lat_0=52.1561605555556 +lon_0=5.38763888888889 +k=0.9999079 \ + +x_0=155000 +y_0=463000", + Ellipsoid::BESSEL_1841, + ); + let (x, y) = m.forward(6f64.to_radians(), 53f64.to_radians()).unwrap(); + assert!((x - 196_105.283).abs() < 0.0005 && (y - 557_057.739).abs() < 0.0005); + check(&m, 6.0, 53.0, [196_105.283_0, 557_057.739_4]); + check(&m, 4.9, 52.37, [121_793.919_4, 486_901.274_2]); + check(&m, 3.4, 51.4, [16_710.909_4, 380_768.054_2]); + } + + /// pyproj, "+proj=sterea +lat_0=46 +lon_0=25 +k=0.99975 +x_0=500000 + /// +y_0=500000 +ellps=krass" (Stereo 70). + #[test] + fn stereo_70() { + let m = build( + "+proj=sterea +lat_0=46 +lon_0=25 +k=0.99975 +x_0=500000 +y_0=500000", + Ellipsoid::KRASSOVSKY, + ); + check(&m, 26.1, 44.43, [587_581.703_1, 326_143.042_5]); + check(&m, 21.2, 45.75, [204_443.178_2, 479_257.212_9]); + } + + /// pyproj, the same string on "+R=6371000". + #[test] + fn sphere() { + let m = build( + "+proj=sterea +lat_0=46 +lon_0=25 +k=0.99975 +x_0=500000 +y_0=500000", + Ellipsoid::sphere(6_371_000.0), + ); + check(&m, 26.1, 44.43, [587_338.675_0, 326_051.814_3]); + check(&m, 21.2, 45.75, [205_286.441_3, 479_226.094_8]); + } + + #[test] + fn origin_maps_to_false_origin() { + let m = build( + "+proj=sterea +lat_0=46 +lon_0=25 +k=0.99975 +x_0=500000 +y_0=500000", + Ellipsoid::KRASSOVSKY, + ); + let (x, y) = m.forward(25f64.to_radians(), 46f64.to_radians()).unwrap(); + assert!((x - 500_000.0).abs() < 1e-6 && (y - 500_000.0).abs() < 1e-6); + let (lam, phi) = m.inverse(500_000.0, 500_000.0).unwrap(); + assert!((lam.to_degrees() - 25.0).abs() < 1e-12 && (phi.to_degrees() - 46.0).abs() < 1e-12); + } + + #[test] + fn parse_and_round_trip() { + let p = StereaParams::from_proj( + &ProjParams::tokenize( + "+proj=sterea +lat_0=46 +lon_0=25 +k=0.99975 +x_0=500000 +y_0=500000", + ) + .unwrap(), + ) + .unwrap(); + let mut out = String::new(); + p.write_proj(&mut out); + assert_eq!( + StereaParams::from_proj(&ProjParams::tokenize(&out).unwrap()), + Ok(p), + "{out}" + ); + assert!( + StereaParams::from_proj(&ProjParams::tokenize("+proj=sterea +k=-1").unwrap()).is_err() + ); + } +} diff --git a/nodedb-spatial/src/crs/tm.rs b/nodedb-crs/src/method/tmerc.rs similarity index 52% rename from nodedb-spatial/src/crs/tm.rs rename to nodedb-crs/src/method/tmerc.rs index b8fd47c3d..0a75c8aa3 100644 --- a/nodedb-spatial/src/crs/tm.rs +++ b/nodedb-crs/src/method/tmerc.rs @@ -6,32 +6,40 @@ //! nanometers", J. Geodesy 85 (2011), eqs. 7–9 and 35–36. Within 35° of the //! central meridian the series errors stay far below a millimetre. -use nodedb_types::geometry::Srid; +use core::fmt::Write; -use super::ellipsoid::Ellipsoid; -use super::error::{CrsError, DomainReason, check_finite, check_lon_lat}; +use serde::{Deserialize, Serialize}; + +use super::angle::wrap_pi; +use crate::def::Ellipsoid; +use crate::error::DomainReason; /// Largest |λ − λ0| in degrees that the projection accepts. Karney 2011 /// bounds the n⁶ series error below 5 nm inside 35° of the central meridian. pub const TM_MAX_LON_OFFSET_DEG: f64 = 35.0; /// Newton iterations allowed when inverting τ' → τ. Karney 2011 §2.2 reports -/// convergence to double precision in two; the cap only bounds bad input. +/// convergence to double precision in two. The cap only bounds bad input. pub const TAU_NEWTON_MAX_ITERS: usize = 10; /// Newton step on τ below which the inverse stops. A step this small moves /// latitude by under 1e-14 rad, about 64 nm on the ground. pub const TAU_TOLERANCE: f64 = 1e-14; -/// Half a turn in degrees, for wrapping a longitude difference. -const HALF_TURN_DEG: f64 = 180.0; - -/// A full turn in degrees. -const FULL_TURN_DEG: f64 = 360.0; - /// Defining parameters of one Transverse Mercator projection. -#[derive(Debug, Clone, Copy, PartialEq)] +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Serialize, + Deserialize, + zerompk::ToMessagePack, + zerompk::FromMessagePack, +)] pub struct TmParams { + /// Latitude of natural origin φ0, degrees. + pub lat0_deg: f64, /// Central meridian λ0, degrees. pub lon0_deg: f64, /// Scale factor on the central meridian. @@ -40,18 +48,31 @@ pub struct TmParams { pub false_easting: f64, /// False northing, metres. pub false_northing: f64, - pub ellipsoid: Ellipsoid, +} + +impl TmParams { + /// Append the PROJ-string parameters, leading space included. + pub fn write_proj(&self, out: &mut String) { + let _ = write!( + out, + " +proj=tmerc +lat_0={} +lon_0={} +k={} +x_0={} +y_0={}", + self.lat0_deg, self.lon0_deg, self.k0, self.false_easting, self.false_northing + ); + } } /// A Transverse Mercator projection with its series coefficients computed. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct TransverseMercator { - srid: Srid, params: TmParams, + /// λ0 in radians. + lon0: f64, e: f64, e2: f64, /// k0 · A, where A is the rectifying radius. k0_a: f64, + /// ξ at the natural origin, so φ0 maps to the false northing. + xi0: f64, /// Krüger α coefficients (forward). alpha: [f64; 6], /// Krüger β coefficients (inverse). @@ -59,15 +80,15 @@ pub struct TransverseMercator { } impl TransverseMercator { - pub fn new(srid: Srid, params: TmParams) -> Self { - let n = params.ellipsoid.n(); + pub fn new(params: &TmParams, ellipsoid: &Ellipsoid) -> Self { + let n = ellipsoid.n(); let n2 = n * n; let n3 = n2 * n; let n4 = n3 * n; let n5 = n4 * n; let n6 = n5 * n; // Rectifying radius A, Karney 2011 eq. 14. - let a_rect = params.ellipsoid.a / (1.0 + n) * (1.0 + n2 / 4.0 + n4 / 64.0 + n6 / 256.0); + let a_rect = ellipsoid.a / (1.0 + n) * (1.0 + n2 / 4.0 + n4 / 64.0 + n6 / 256.0); // Karney 2011 eq. 35. let alpha = [ n / 2.0 - 2.0 / 3.0 * n2 + 5.0 / 16.0 * n3 + 41.0 / 180.0 * n4 - 127.0 / 288.0 * n5 @@ -92,31 +113,34 @@ impl TransverseMercator { 4583.0 / 161_280.0 * n5 - 108_847.0 / 3_991_680.0 * n6, 20_648_693.0 / 638_668_800.0 * n6, ]; + let e = ellipsoid.e(); + // On the central meridian η' = 0, so ξ = ξ' + Σ αj sin(2j ξ'). + let xi0_p = conformal_tau(params.lat0_deg.to_radians().tan(), e).atan(); + let xi0 = xi0_p + + alpha + .iter() + .enumerate() + .map(|(j, a)| a * (2.0 * (j + 1) as f64 * xi0_p).sin()) + .sum::(); Self { - srid, - params, - e: params.ellipsoid.e(), - e2: params.ellipsoid.e2(), + params: *params, + lon0: params.lon0_deg.to_radians(), + e, + e2: ellipsoid.e2(), k0_a: params.k0 * a_rect, + xi0, alpha, beta, } } - /// Project `[lon, lat]` degrees to `[easting, northing]` metres. - pub fn forward(&self, coord: [f64; 2]) -> Result<[f64; 2], CrsError> { - check_lon_lat(self.srid, coord)?; - let [lon, lat] = coord; - let dlon_deg = wrap_lon_offset(lon - self.params.lon0_deg); - if dlon_deg.abs() > TM_MAX_LON_OFFSET_DEG { - return Err(CrsError::out_of_domain( - self.srid, - coord, - DomainReason::CentralMeridianOffset, - )); + /// Project λ, φ radians to easting, northing metres. + pub fn forward(&self, lam: f64, phi: f64) -> Result<(f64, f64), DomainReason> { + let dlon = wrap_pi(lam - self.lon0); + if dlon.abs() > TM_MAX_LON_OFFSET_DEG.to_radians() { + return Err(DomainReason::CentralMeridianOffset); } - let dlon = dlon_deg.to_radians(); - let tau_p = conformal_tau(lat.to_radians().tan(), self.e); + let tau_p = conformal_tau(phi.tan(), self.e); let xi_p = tau_p.atan2(dlon.cos()); let eta_p = (dlon.sin() / tau_p.hypot(dlon.cos())).asinh(); let mut xi = xi_p; @@ -126,19 +150,16 @@ impl TransverseMercator { xi += a * (k * xi_p).sin() * (k * eta_p).cosh(); eta += a * (k * xi_p).cos() * (k * eta_p).sinh(); } - let out = [ + Ok(( self.params.false_easting + self.k0_a * eta, - self.params.false_northing + self.k0_a * xi, - ]; - check_finite(self.srid, out)?; - Ok(out) + self.params.false_northing + self.k0_a * (xi - self.xi0), + )) } - /// Unproject `[easting, northing]` metres to `[lon, lat]` degrees. - pub fn inverse(&self, coord: [f64; 2]) -> Result<[f64; 2], CrsError> { - check_finite(self.srid, coord)?; - let xi = (coord[1] - self.params.false_northing) / self.k0_a; - let eta = (coord[0] - self.params.false_easting) / self.k0_a; + /// Unproject easting, northing metres to λ, φ radians. + pub fn inverse(&self, x: f64, y: f64) -> Result<(f64, f64), DomainReason> { + let xi = (y - self.params.false_northing) / self.k0_a + self.xi0; + let eta = (x - self.params.false_easting) / self.k0_a; let mut xi_p = xi; let mut eta_p = eta; for (j, b) in self.beta.iter().enumerate() { @@ -148,18 +169,11 @@ impl TransverseMercator { } let tau_p = xi_p.sin() / eta_p.sinh().hypot(xi_p.cos()); let dlon = eta_p.sinh().atan2(xi_p.cos()); - let tau = self.tau_from_conformal(tau_p); - let dlon_deg = dlon.to_degrees(); - if dlon_deg.abs() > TM_MAX_LON_OFFSET_DEG { - return Err(CrsError::out_of_domain( - self.srid, - coord, - DomainReason::CentralMeridianOffset, - )); + if dlon.abs() > TM_MAX_LON_OFFSET_DEG.to_radians() { + return Err(DomainReason::CentralMeridianOffset); } - let out = [self.params.lon0_deg + dlon_deg, tau.atan().to_degrees()]; - check_finite(self.srid, out)?; - Ok(out) + let tau = self.tau_from_conformal(tau_p); + Ok((self.lon0 + dlon, tau.atan())) } /// Solve τ' = τ'(τ) for τ by Newton's method, Karney 2011 eqs. 19–21. @@ -187,32 +201,27 @@ fn conformal_tau(tau: f64, e: f64) -> f64 { tau * (1.0 + sigma * sigma).sqrt() - sigma * tau1 } -/// Wrap a longitude difference into [-180°, 180°]. -fn wrap_lon_offset(dlon_deg: f64) -> f64 { - let d = dlon_deg % FULL_TURN_DEG; - if d > HALF_TURN_DEG { - d - FULL_TURN_DEG - } else if d < -HALF_TURN_DEG { - d + FULL_TURN_DEG - } else { - d - } -} - #[cfg(test)] mod tests { use super::*; - use crate::crs::params::{UTM_47N, UTM_48N}; /// One millimetre, the tolerance for every PROJ reference value. const MM: f64 = 1e-3; - fn utm47() -> TransverseMercator { - TransverseMercator::new(Srid::Utm47N, UTM_47N) + fn utm_north(lon0_deg: f64) -> TransverseMercator { + let params = TmParams { + lat0_deg: 0.0, + lon0_deg, + k0: 0.9996, + false_easting: 500_000.0, + false_northing: 0.0, + }; + TransverseMercator::new(¶ms, &Ellipsoid::WGS84) } - fn utm48() -> TransverseMercator { - TransverseMercator::new(Srid::Utm48N, UTM_48N) + fn fwd(tm: &TransverseMercator, lon: f64, lat: f64) -> [f64; 2] { + let (x, y) = tm.forward(lon.to_radians(), lat.to_radians()).unwrap(); + [x, y] } fn assert_close(got: [f64; 2], want: [f64; 2]) { @@ -226,67 +235,91 @@ mod tests { #[test] fn utm47n_klcc() { - let got = utm47().forward([101.7117, 3.1579]).unwrap(); - assert_close(got, [801_401.794_9, 349_439.510_6]); + assert_close( + fwd(&utm_north(99.0), 101.7117, 3.1579), + [801_401.794_9, 349_439.510_6], + ); } #[test] fn utm48n_klcc() { - let got = utm48().forward([101.7117, 3.1579]).unwrap(); - assert_close(got, [134_445.592_4, 349_624.727_8]); + assert_close( + fwd(&utm_north(105.0), 101.7117, 3.1579), + [134_445.592_4, 349_624.727_8], + ); } #[test] - fn utm48n_origin_on_central_meridian() { - assert_close(utm48().forward([105.0, 0.0]).unwrap(), [500_000.0, 0.0]); + fn utm48n_origin_and_five_north() { + let tm = utm_north(105.0); + assert_close(fwd(&tm, 105.0, 0.0), [500_000.0, 0.0]); + assert_close(fwd(&tm, 105.0, 5.0), [500_000.0, 552_664.296_9]); } + /// Landsat keeps north zones south of the equator, so northings go + /// negative and still invert. #[test] - fn utm48n_central_meridian_five_north() { - let got = utm48().forward([105.0, 5.0]).unwrap(); - assert_close(got, [500_000.0, 552_664.296_9]); + fn north_zone_accepts_negative_northing() { + let tm = utm_north(105.0); + let [x, y] = fwd(&tm, 105.0, -5.0); + assert_close([x, y], [500_000.0, -552_664.296_9]); + let (lam, phi) = tm.inverse(x, y).unwrap(); + assert!((lam.to_degrees() - 105.0).abs() < 1e-9); + assert!((phi.to_degrees() + 5.0).abs() < 1e-9); + } + + #[test] + fn origin_latitude_maps_to_false_northing() { + let params = TmParams { + lat0_deg: 4.0, + lon0_deg: 102.0, + k0: 0.99995, + false_easting: 40_000.0, + false_northing: 10_000.0, + }; + let tm = TransverseMercator::new(¶ms, &Ellipsoid::GRS80); + assert_close(fwd(&tm, 102.0, 4.0), [40_000.0, 10_000.0]); + let (lam, phi) = tm.inverse(40_000.0, 10_000.0).unwrap(); + assert!((lam.to_degrees() - 102.0).abs() < 1e-12); + assert!((phi.to_degrees() - 4.0).abs() < 1e-12); } #[test] fn inverse_recovers_input() { - let tm = utm48(); - for input in [ + let tm = utm_north(105.0); + for [lon, lat] in [ [101.7117, 3.1579], [105.0, 0.0], [109.5, 6.2], [100.0, -2.0], ] { - let back = tm.inverse(tm.forward(input).unwrap()).unwrap(); + let [x, y] = fwd(&tm, lon, lat); + let (lam, phi) = tm.inverse(x, y).unwrap(); assert!( - (back[0] - input[0]).abs() < 1e-9 && (back[1] - input[1]).abs() < 1e-9, - "{input:?} -> {back:?}" + (lam.to_degrees() - lon).abs() < 1e-9 && (phi.to_degrees() - lat).abs() < 1e-9, + "{lon} {lat}" ); } } #[test] fn far_from_central_meridian_is_out_of_domain() { - let err = utm48().forward([150.0, 3.0]).unwrap_err(); - assert!(matches!( - err, - CrsError::OutOfDomain { - reason: DomainReason::CentralMeridianOffset, - .. - } - )); - } - - #[test] - fn bad_latitude_and_non_finite_are_out_of_domain() { - assert!(utm48().forward([105.0, 91.0]).is_err()); - assert!(utm48().forward([f64::NAN, 1.0]).is_err()); - assert!(utm48().inverse([f64::INFINITY, 1.0]).is_err()); + let err = utm_north(105.0).forward(150f64.to_radians(), 3f64.to_radians()); + assert_eq!(err, Err(DomainReason::CentralMeridianOffset)); } #[test] - fn wrap_keeps_offset_in_half_turn() { - assert_eq!(wrap_lon_offset(200.0), -160.0); - assert_eq!(wrap_lon_offset(-200.0), 160.0); - assert_eq!(wrap_lon_offset(10.0), 10.0); + fn sphere_is_accepted() { + let params = TmParams { + lat0_deg: 0.0, + lon0_deg: 0.0, + k0: 1.0, + false_easting: 0.0, + false_northing: 0.0, + }; + let tm = TransverseMercator::new(¶ms, &Ellipsoid::sphere(6_371_000.0)); + let (x, y) = tm.forward(0.1, 0.2).unwrap(); + let (lam, phi) = tm.inverse(x, y).unwrap(); + assert!((lam - 0.1).abs() < 1e-12 && (phi - 0.2).abs() < 1e-12); } } diff --git a/nodedb-crs/src/method/webmerc.rs b/nodedb-crs/src/method/webmerc.rs new file mode 100644 index 000000000..68239958f --- /dev/null +++ b/nodedb-crs/src/method/webmerc.rs @@ -0,0 +1,194 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Popular Visualisation Pseudo-Mercator, PROJ `webmerc`, EPSG method 1024. +//! +//! Spherical Mercator formulas on the ellipsoid's semi-major axis, with the +//! geodetic latitude used directly (IOGP Guidance Note 7-2 §3.5.1.3, PROJ +//! 9.8.1 `src/projections/merc.cpp`). PROJ fixes k0 at 1. + +use core::fmt::Write; + +use serde::{Deserialize, Serialize}; + +use super::angle::wrap_pi; +use crate::def::Ellipsoid; +use crate::error::{CrsParseError, DomainReason}; +use crate::parse::tokens::ProjParams; + +/// Largest |φ| in radians: atan(sinh π) ≈ 85.0511°, where y reaches ±πa +/// and the map is square. +pub fn webmerc_max_lat() -> f64 { + core::f64::consts::PI.sinh().atan() +} + +/// Slack in radians on the inverse latitude limit, so a point projected at +/// the limit unprojects without a rounding error tripping the check. +const LIMIT_SLACK: f64 = 1e-12; + +/// Defining parameters of one Pseudo-Mercator projection. +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Serialize, + Deserialize, + zerompk::ToMessagePack, + zerompk::FromMessagePack, +)] +pub struct WebMercParams { + /// Longitude of natural origin λ0, degrees. + pub lon0_deg: f64, + /// False easting, metres. + pub false_easting: f64, + /// False northing, metres. + pub false_northing: f64, +} + +impl WebMercParams { + /// Read from PROJ-string parameters. PROJ ignores `+k` for webmerc. + pub fn from_proj(p: &ProjParams<'_>) -> Result { + Ok(Self { + lon0_deg: p.number_or("lon_0", 0.0)?, + false_easting: p.number_or("x_0", 0.0)?, + false_northing: p.number_or("y_0", 0.0)?, + }) + } + + /// Append ` +proj=webmerc` and every defining parameter. + pub fn write_proj(&self, out: &mut String) { + let _ = write!( + out, + " +proj=webmerc +lon_0={} +x_0={} +y_0={}", + self.lon0_deg, self.false_easting, self.false_northing + ); + } +} + +/// A Pseudo-Mercator projection with its constants computed. +#[derive(Debug, Clone, PartialEq)] +pub struct WebMercator { + /// λ0 in radians. + lon0: f64, + /// Semi-major axis a, metres. + a: f64, + /// Latitude limit in radians. + max_lat: f64, + false_easting: f64, + false_northing: f64, +} + +impl WebMercator { + pub fn new(p: &WebMercParams, ell: &Ellipsoid) -> Self { + Self { + lon0: p.lon0_deg.to_radians(), + a: ell.a, + max_lat: webmerc_max_lat(), + false_easting: p.false_easting, + false_northing: p.false_northing, + } + } + + /// Project λ, φ radians to easting, northing metres. GN 7-2 §3.5.1.3: + /// E = FE + a(λ − λ0), N = FN + a ln tan(π/4 + φ/2). + pub fn forward(&self, lam: f64, phi: f64) -> Result<(f64, f64), DomainReason> { + if phi.abs() > self.max_lat { + return Err(DomainReason::LatitudeLimit); + } + Ok(( + self.false_easting + self.a * wrap_pi(lam - self.lon0), + self.false_northing + self.a * phi.tan().asinh(), + )) + } + + /// Unproject easting, northing metres to λ, φ radians. + pub fn inverse(&self, x: f64, y: f64) -> Result<(f64, f64), DomainReason> { + let phi = ((y - self.false_northing) / self.a).sinh().atan(); + if phi.abs() > self.max_lat + LIMIT_SLACK { + return Err(DomainReason::LatitudeLimit); + } + let lam = (x - self.false_easting) / self.a; + Ok((wrap_pi(self.lon0 + lam), phi)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// One millimetre, the tolerance for every PROJ reference value. + const MM: f64 = 1e-3; + + /// Inverse tolerance in degrees. + const DEG_TOL: f64 = 1e-9; + + fn epsg_3857() -> WebMercator { + let p = + WebMercParams::from_proj(&ProjParams::tokenize("+proj=webmerc +datum=WGS84").unwrap()) + .unwrap(); + WebMercator::new(&p, &Ellipsoid::WGS84) + } + + fn check(m: &WebMercator, lon: f64, lat: f64, want: [f64; 2]) { + let (x, y) = m.forward(lon.to_radians(), lat.to_radians()).unwrap(); + assert!( + (x - want[0]).abs() < MM && (y - want[1]).abs() < MM, + "({lon}, {lat}): got ({x}, {y}), want {want:?}" + ); + // The references carry 0.1 mm, so the inverse starts from (x, y). + let (lam, phi) = m.inverse(x, y).unwrap(); + assert!( + (lam.to_degrees() - lon).abs() < DEG_TOL && (phi.to_degrees() - lat).abs() < DEG_TOL, + "inverse ({lon}, {lat})" + ); + } + + /// GN 7-2 §3.5.1.3 example, EPSG 1024: WGS 84 / Pseudo-Mercator, point + /// 24°22'54.433"N, 100°20'00.000"W. GN 7-2 prints E = −11169055.58 m, + /// N = 2800000.00 m. + #[test] + fn gn_1024_pseudo_mercator() { + let m = epsg_3857(); + let lon: f64 = -(100.0 + 20.0 / 60.0); + let lat: f64 = 24.0 + 22.0 / 60.0 + 54.433 / 3600.0; + let (x, y) = m.forward(lon.to_radians(), lat.to_radians()).unwrap(); + assert!((x + 11_169_055.58).abs() < 0.005 && (y - 2_800_000.00).abs() < 0.005); + check(&m, lon, lat, [-11_169_055.576_3, 2_800_000.003_1]); + } + + /// pyproj 3.8.0 / PROJ 9.8.1, "+proj=webmerc +datum=WGS84". + #[test] + fn proj_reference_points() { + let m = epsg_3857(); + check(&m, 101.7117, 3.1579, [11_322_494.651_7, 351_713.934_7]); + check(&m, 179.9, 85.0, [20_026_376.393_7, 19_971_868.880_4]); + } + + #[test] + fn beyond_limit_is_refused() { + let m = epsg_3857(); + assert_eq!( + m.forward(0.0, 85.06f64.to_radians()), + Err(DomainReason::LatitudeLimit) + ); + assert_eq!(m.inverse(0.0, 2.1e7), Err(DomainReason::LatitudeLimit)); + let edge = webmerc_max_lat(); + let (x, y) = m.forward(0.0, edge).unwrap(); + assert!(m.inverse(x, y).is_ok()); + } + + #[test] + fn round_trip_writer() { + let p = WebMercParams { + lon0_deg: 10.0, + false_easting: 1.5, + false_northing: -2.0, + }; + let mut out = String::new(); + p.write_proj(&mut out); + assert_eq!( + WebMercParams::from_proj(&ProjParams::tokenize(&out).unwrap()), + Ok(p) + ); + } +} diff --git a/nodedb-crs/src/parse/helmert_params.rs b/nodedb-crs/src/parse/helmert_params.rs new file mode 100644 index 000000000..464da9569 --- /dev/null +++ b/nodedb-crs/src/parse/helmert_params.rs @@ -0,0 +1,512 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Helmert transformation parameters as WKT2 and PROJJSON name them. +//! +//! A static Helmert has seven parameters, IOGP Guidance Note 7-2 §4.2.3. A +//! time-dependent Helmert, GN 7-2 §4.2.5, adds the seven yearly rates and +//! the parameter reference epoch. Unit factors follow the EPSG convention: +//! SI base unit per unit, with rates per second, so "millimetres per year" +//! is 0.001 / [`SECONDS_PER_YEAR`] metre per second. + +use super::model::{ARC_SECOND_RAD, PPM, convert, normalize_name}; +use crate::def::{DatumShift, Helmert, HelmertRates, RotationConvention}; +use crate::error::CrsParseError; + +/// Seconds per year, EPSG unit of measure 1029 ("year"), factor +/// 31556925.445 s. Rate units in EPSG and PROJ divide by this year. +pub(crate) const SECONDS_PER_YEAR: f64 = 31_556_925.445; + +/// The unit a Helmert parameter holds in a [`Helmert`] value. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum HelmertUnit { + /// Metre. + Length, + /// Arc-second. + Angle, + /// Part per million. + Scale, + /// Metre per year. + LengthRate, + /// Arc-second per year. + AngleRate, + /// Part per million per year. + ScaleRate, + /// Year. + Time, +} + +impl HelmertUnit { + /// SI factor of the unit: metres, radians, unity or seconds per unit, + /// per second for a rate. + pub(crate) const fn factor(self) -> f64 { + match self { + HelmertUnit::Length => 1.0, + HelmertUnit::Angle => ARC_SECOND_RAD, + HelmertUnit::Scale => PPM, + HelmertUnit::LengthRate => 1.0 / SECONDS_PER_YEAR, + HelmertUnit::AngleRate => ARC_SECOND_RAD / SECONDS_PER_YEAR, + HelmertUnit::ScaleRate => PPM / SECONDS_PER_YEAR, + HelmertUnit::Time => SECONDS_PER_YEAR, + } + } + + /// The unit's EPSG name: units of measure 9001, 9104, 9202, 1042, + /// 1043, 1041 and 1029. + pub(crate) const fn name(self) -> &'static str { + match self { + HelmertUnit::Length => "metre", + HelmertUnit::Angle => "arc-second", + HelmertUnit::Scale => "parts per million", + HelmertUnit::LengthRate => "metres per year", + HelmertUnit::AngleRate => "arc-seconds per year", + HelmertUnit::ScaleRate => "parts per million per year", + HelmertUnit::Time => "year", + } + } + + /// The WKT2 unit keyword. + pub(crate) const fn wkt_keyword(self) -> &'static str { + match self { + HelmertUnit::Length | HelmertUnit::LengthRate => "LENGTHUNIT", + HelmertUnit::Angle | HelmertUnit::AngleRate => "ANGLEUNIT", + HelmertUnit::Scale | HelmertUnit::ScaleRate => "SCALEUNIT", + HelmertUnit::Time => "TIMEUNIT", + } + } + + /// The PROJJSON unit type. + pub(crate) const fn projjson_type(self) -> &'static str { + match self { + HelmertUnit::Length | HelmertUnit::LengthRate => "LinearUnit", + HelmertUnit::Angle | HelmertUnit::AngleRate => "AngularUnit", + HelmertUnit::Scale | HelmertUnit::ScaleRate => "ScaleUnit", + HelmertUnit::Time => "TimeUnit", + } + } +} + +/// Number of Helmert parameters: seven values, seven rates, one epoch. +pub(crate) const HELMERT_PARAM_COUNT: usize = 15; + +/// Index of the parameter reference epoch in [`HELMERT_PARAMS`]. +const EPOCH_SLOT: usize = 14; + +/// First rate slot in [`HELMERT_PARAMS`]. +const FIRST_RATE_SLOT: usize = 7; + +/// EPSG code, name and unit of every Helmert parameter: the seven values +/// in `+towgs84` order (8605 to 8611), their rates (1040 to 1046), and the +/// parameter reference epoch (1047). +pub(crate) const HELMERT_PARAMS: [(u32, &str, HelmertUnit); HELMERT_PARAM_COUNT] = [ + (8605, "X-axis translation", HelmertUnit::Length), + (8606, "Y-axis translation", HelmertUnit::Length), + (8607, "Z-axis translation", HelmertUnit::Length), + (8608, "X-axis rotation", HelmertUnit::Angle), + (8609, "Y-axis rotation", HelmertUnit::Angle), + (8610, "Z-axis rotation", HelmertUnit::Angle), + (8611, "Scale difference", HelmertUnit::Scale), + ( + 1040, + "Rate of change of X-axis translation", + HelmertUnit::LengthRate, + ), + ( + 1041, + "Rate of change of Y-axis translation", + HelmertUnit::LengthRate, + ), + ( + 1042, + "Rate of change of Z-axis translation", + HelmertUnit::LengthRate, + ), + ( + 1043, + "Rate of change of X-axis rotation", + HelmertUnit::AngleRate, + ), + ( + 1044, + "Rate of change of Y-axis rotation", + HelmertUnit::AngleRate, + ), + ( + 1045, + "Rate of change of Z-axis rotation", + HelmertUnit::AngleRate, + ), + ( + 1046, + "Rate of change of Scale difference", + HelmertUnit::ScaleRate, + ), + (1047, "Parameter reference epoch", HelmertUnit::Time), +]; + +/// The parameter slot a transformation parameter fills, by EPSG code or +/// name. +pub(crate) fn helmert_slot(code: Option, name: &str) -> Option { + let normalized = normalize_name(name); + HELMERT_PARAMS + .iter() + .position(|(c, n, _)| code == Some(*c) || normalize_name(n) == normalized) +} + +/// The values a transformation's parameters give, in [`Helmert`] units. +#[derive(Debug, Clone, Copy, Default)] +pub(crate) struct HelmertValues { + values: [f64; HELMERT_PARAM_COUNT], + given: [bool; HELMERT_PARAM_COUNT], +} + +impl HelmertValues { + /// Record parameter `slot` as `value` in a unit of SI factor `factor`. + /// `None` takes the parameter's own unit. + pub(crate) fn set(&mut self, slot: usize, value: f64, factor: Option) { + let Some((_, _, unit)) = HELMERT_PARAMS.get(slot) else { + return; + }; + let canonical = unit.factor(); + self.values[slot] = convert(value, factor.unwrap_or(canonical), canonical); + self.given[slot] = true; + } + + /// The datum shift these values give under the EPSG method + /// `method_code` / `method_name`. + pub(crate) fn into_shift( + self, + method_code: Option, + method_name: &str, + accuracy_m: Option, + format: &'static str, + ) -> Result { + let invalid = |detail: String| CrsParseError::content(format, detail); + let (convention, time_dependent) = + method_kind(method_code, method_name).ok_or_else(|| { + invalid(format!( + "transformation method '{method_name}' is not a Helmert transformation" + )) + })?; + let mut values = [0.0; 7]; + values.copy_from_slice(&self.values[..FIRST_RATE_SLOT]); + let mut helmert = Helmert::position_vector(values); + helmert.convention = convention; + helmert.accuracy_m = accuracy_m; + let any_rate = self.given[FIRST_RATE_SLOT..].iter().any(|g| *g); + if time_dependent { + if !self.given[EPOCH_SLOT] { + return Err(invalid(format!( + "time-dependent method '{method_name}' has no parameter reference epoch" + ))); + } + let mut rates = [0.0; 7]; + rates.copy_from_slice(&self.values[FIRST_RATE_SLOT..EPOCH_SLOT]); + helmert.rates = Some(HelmertRates::new(rates, self.values[EPOCH_SLOT])); + } else if any_rate { + return Err(invalid(format!( + "method '{method_name}' is static, but the transformation has rates; use a \ + time-dependent method such as EPSG 1057" + ))); + } + Ok(DatumShift::Helmert(helmert)) + } +} + +/// Rotation convention and whether the method is time-dependent, from the +/// EPSG method code or name, IOGP GN 7-2 §4.2.3, §4.2.5 and §4.3.1. +fn method_kind(code: Option, name: &str) -> Option<(RotationConvention, bool)> { + use RotationConvention::{CoordinateFrame, PositionVector}; + // Geocentric translations: 9603 geog2D, 1031 geocentric, 1035 geog3D. + // Position vector: 9606, 1033, 1037. Coordinate frame: 9607, 1032, 1038. + // Time-dependent position vector: 1054 geog2D, 1053 geocentric, 1055 + // geog3D. Time-dependent coordinate frame: 1057, 1056, 1058. + match code { + Some(9603 | 1031 | 1035 | 9606 | 1033 | 1037) => return Some((PositionVector, false)), + Some(9607 | 1032 | 1038) => return Some((CoordinateFrame, false)), + Some(1053..=1055) => return Some((PositionVector, true)), + Some(1056..=1058) => return Some((CoordinateFrame, true)), + Some(_) | None => {} + } + let normalized = normalize_name(name); + let prefixes = [ + ("timedependentpositionvector", PositionVector, true), + ("timedependentcoordinateframe", CoordinateFrame, true), + ("positionvector", PositionVector, false), + ("geocentrictranslations", PositionVector, false), + ("coordinateframe", CoordinateFrame, false), + ]; + prefixes + .iter() + .find(|(prefix, _, _)| normalized.starts_with(prefix)) + .map(|(_, convention, td)| (*convention, *td)) +} + +/// The EPSG method name and code a writer gives `h`: the geog2D domain +/// variant of its convention, time-dependent when it has rates. +pub(crate) fn writer_method(h: &Helmert) -> (&'static str, u32) { + match (h.convention, h.rates.is_some()) { + (RotationConvention::PositionVector, false) => { + ("Position Vector transformation (geog2D domain)", 9606) + } + (RotationConvention::CoordinateFrame, false) => { + ("Coordinate Frame rotation (geog2D domain)", 9607) + } + (RotationConvention::PositionVector, true) => ( + "Time-dependent Position Vector transformation (geog2D domain)", + 1054, + ), + (RotationConvention::CoordinateFrame, true) => ( + "Time-dependent Coordinate Frame rotation (geog2D domain)", + 1057, + ), + } +} + +/// The parameters a writer emits for `h`, in the stored convention: the +/// seven values, then the rates and epoch when it has them. +pub(crate) fn writer_params( + h: &Helmert, +) -> impl Iterator { + let mut values = [0.0; HELMERT_PARAM_COUNT]; + values[..FIRST_RATE_SLOT].copy_from_slice(&[ + h.tx_m, + h.ty_m, + h.tz_m, + h.rx_arcsec, + h.ry_arcsec, + h.rz_arcsec, + h.scale_ppm, + ]); + let count = match h.rates { + None => FIRST_RATE_SLOT, + Some(r) => { + values[FIRST_RATE_SLOT..EPOCH_SLOT].copy_from_slice(&r.values()); + values[EPOCH_SLOT] = r.reference_epoch; + HELMERT_PARAM_COUNT + } + }; + HELMERT_PARAMS + .into_iter() + .zip(values) + .take(count) + .map(|((code, name, unit), value)| (code, name, unit, value)) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn shift(code: Option, name: &str, values: HelmertValues) -> Result { + match values.into_shift(code, name, None, "test") { + Ok(DatumShift::Helmert(h)) => Ok(h), + Ok(DatumShift::Identity) => Err("identity".into()), + Err(e) => Err(e.to_string()), + } + } + + fn seven() -> HelmertValues { + let mut v = HelmertValues::default(); + for (slot, value) in [1.0, 2.0, 3.0, 0.1, 0.2, 0.3, 4.0].into_iter().enumerate() { + v.set(slot, value, None); + } + v + } + + #[test] + fn static_methods_pick_the_convention() { + let cf = shift(Some(9607), "x", seven()).unwrap(); + assert_eq!(cf.convention, RotationConvention::CoordinateFrame); + assert_eq!(cf.rates, None); + let pv = shift(None, "Position Vector transformation", seven()).unwrap(); + assert_eq!(pv.convention, RotationConvention::PositionVector); + assert_eq!(pv.to_towgs84(), [1.0, 2.0, 3.0, 0.1, 0.2, 0.3, 4.0]); + for code in [9603, 1031, 1035, 1037] { + assert_eq!( + shift(Some(code), "x", seven()).map(|h| h.convention), + Ok(RotationConvention::PositionVector) + ); + } + assert!(shift(None, "NTv2", seven()).is_err()); + } + + /// Rates in the EPSG per-second factors PROJ writes: millimetres per + /// year 3.16887651727315e-11 m/s, milliarc-seconds per year + /// 1.53631468932076e-16 rad/s, parts per billion per year + /// 3.16887651727315e-17 /s, year 31556925.445 s. + #[test] + fn time_dependent_rates_convert_from_epsg_units() { + let mut v = seven(); + let slot = |name: &str| helmert_slot(None, name).unwrap(); + v.set( + slot("Rate of change of X-axis translation"), + 1.42, + Some(3.168_876_517_273_15e-11), + ); + v.set( + slot("Rate of change of Z-axis rotation"), + 1.1551, + Some(1.536_314_689_320_76e-16), + ); + v.set( + slot("Rate of change of Scale difference"), + 0.109, + Some(3.168_876_517_273_15e-17), + ); + v.set( + helmert_slot(Some(1047), "").unwrap(), + 1994.0, + Some(SECONDS_PER_YEAR), + ); + let h = shift(None, "Time-dependent Coordinate Frame rotation (geocen)", v).unwrap(); + assert_eq!(h.convention, RotationConvention::CoordinateFrame); + let r = h.rates.unwrap(); + assert!((r.dtx_m_per_yr - 0.001_42).abs() < 1e-15, "{r:?}"); + assert!((r.drz_arcsec_per_yr - 0.001_155_1).abs() < 1e-15, "{r:?}"); + assert!((r.dscale_ppm_per_yr - 0.000_109).abs() < 1e-15, "{r:?}"); + assert_eq!(r.reference_epoch, 1994.0); + assert_eq!( + shift(Some(1053), "x", v).map(|h| h.convention), + Ok(RotationConvention::PositionVector) + ); + } + + #[test] + fn rates_need_a_time_dependent_method_and_an_epoch() { + let mut v = seven(); + v.set(FIRST_RATE_SLOT, 0.001, None); + let err = shift(Some(9606), "x", v).unwrap_err(); + assert!(err.contains("static"), "{err}"); + let err = shift(Some(1054), "x", v).unwrap_err(); + assert!(err.contains("reference epoch"), "{err}"); + } + + /// A GDA94 `BoundCRS` carrying EPSG transformation 6276 (ITRF2008 to + /// GDA94, method 1056) with the parameter units PROJ 9.8.1 writes. + const TIME_DEPENDENT_PROJJSON: &str = r#"{"type": "BoundCRS", + "source_crs": {"type": "GeographicCRS", "name": "GDA94", + "datum": {"type": "GeodeticReferenceFrame", "name": "Geocentric Datum of Australia 1994", + "ellipsoid": {"name": "GRS 1980", "semi_major_axis": 6378137, "inverse_flattening": 298.257222101}}, + "coordinate_system": {"subtype": "ellipsoidal", "axis": [ + {"name": "Geodetic latitude", "abbreviation": "Lat", "direction": "north", "unit": "degree"}, + {"name": "Geodetic longitude", "abbreviation": "Lon", "direction": "east", "unit": "degree"}]}, + "id": {"authority": "EPSG", "code": 4283}}, + "target_crs": {"type": "GeographicCRS", "name": "WGS 84", + "datum": {"type": "GeodeticReferenceFrame", "name": "World Geodetic System 1984", + "ellipsoid": {"name": "WGS 84", "semi_major_axis": 6378137, "inverse_flattening": 298.257223563}}, + "coordinate_system": {"subtype": "ellipsoidal", "axis": [ + {"name": "Geodetic latitude", "abbreviation": "Lat", "direction": "north", "unit": "degree"}, + {"name": "Geodetic longitude", "abbreviation": "Lon", "direction": "east", "unit": "degree"}]}}, + "transformation": {"name": "ITRF2008 to GDA94 (1)", + "method": {"name": "Time-dependent Coordinate Frame rotation (geocen)", "id": {"authority": "EPSG", "code": 1056}}, + "parameters": [ + {"name": "X-axis translation", "value": -84.68, "unit": {"type": "LinearUnit", "name": "millimetre", "conversion_factor": 0.001}, "id": {"authority": "EPSG", "code": 8605}}, + {"name": "Y-axis translation", "value": -19.42, "unit": {"type": "LinearUnit", "name": "millimetre", "conversion_factor": 0.001}, "id": {"authority": "EPSG", "code": 8606}}, + {"name": "Z-axis translation", "value": 32.01, "unit": {"type": "LinearUnit", "name": "millimetre", "conversion_factor": 0.001}, "id": {"authority": "EPSG", "code": 8607}}, + {"name": "X-axis rotation", "value": -0.4254, "unit": {"type": "AngularUnit", "name": "milliarc-second", "conversion_factor": 4.84813681109536e-09}, "id": {"authority": "EPSG", "code": 8608}}, + {"name": "Y-axis rotation", "value": 2.2578, "unit": {"type": "AngularUnit", "name": "milliarc-second", "conversion_factor": 4.84813681109536e-09}, "id": {"authority": "EPSG", "code": 8609}}, + {"name": "Z-axis rotation", "value": 2.4015, "unit": {"type": "AngularUnit", "name": "milliarc-second", "conversion_factor": 4.84813681109536e-09}, "id": {"authority": "EPSG", "code": 8610}}, + {"name": "Scale difference", "value": 9.71, "unit": {"type": "ScaleUnit", "name": "parts per billion", "conversion_factor": 1e-09}, "id": {"authority": "EPSG", "code": 8611}}, + {"name": "Rate of change of X-axis translation", "value": 1.42, "unit": {"type": "LinearUnit", "name": "millimetres per year", "conversion_factor": 3.16887651727315e-11}, "id": {"authority": "EPSG", "code": 1040}}, + {"name": "Rate of change of Y-axis translation", "value": 1.34, "unit": {"type": "LinearUnit", "name": "millimetres per year", "conversion_factor": 3.16887651727315e-11}, "id": {"authority": "EPSG", "code": 1041}}, + {"name": "Rate of change of Z-axis translation", "value": 0.9, "unit": {"type": "LinearUnit", "name": "millimetres per year", "conversion_factor": 3.16887651727315e-11}, "id": {"authority": "EPSG", "code": 1042}}, + {"name": "Rate of change of X-axis rotation", "value": 1.5461, "unit": {"type": "AngularUnit", "name": "milliarc-seconds per year", "conversion_factor": 1.53631468932076e-16}, "id": {"authority": "EPSG", "code": 1043}}, + {"name": "Rate of change of Y-axis rotation", "value": 1.182, "unit": {"type": "AngularUnit", "name": "milliarc-seconds per year", "conversion_factor": 1.53631468932076e-16}, "id": {"authority": "EPSG", "code": 1044}}, + {"name": "Rate of change of Z-axis rotation", "value": 1.1551, "unit": {"type": "AngularUnit", "name": "milliarc-seconds per year", "conversion_factor": 1.53631468932076e-16}, "id": {"authority": "EPSG", "code": 1045}}, + {"name": "Rate of change of Scale difference", "value": 0.109, "unit": {"type": "ScaleUnit", "name": "parts per billion per year", "conversion_factor": 3.16887651727315e-17}, "id": {"authority": "EPSG", "code": 1046}}, + {"name": "Parameter reference epoch", "value": 1994, "unit": {"type": "TimeUnit", "name": "year", "conversion_factor": 31556925.445}, "id": {"authority": "EPSG", "code": 1047}}]}}"#; + + /// Relative agreement of a parameter converted from PROJ's 15-digit + /// unit factors. + const FACTOR_REL_TOL: f64 = 1e-12; + + fn close(got: f64, want: f64) -> bool { + (got - want).abs() <= want.abs() * FACTOR_REL_TOL + } + + #[test] + fn projjson_time_dependent_bound_crs_reads_and_round_trips() { + use crate::parse::{parse_projjson, parse_wkt2}; + use crate::srid::Srid; + let srid = Srid::new(910_010).unwrap(); + let def = parse_projjson(srid, "td", TIME_DEPENDENT_PROJJSON).unwrap(); + let DatumShift::Helmert(h) = def.datum.to_wgs84 else { + panic!("expected a Helmert shift"); + }; + assert_eq!(h.convention, RotationConvention::CoordinateFrame); + let want = [ + -0.084_68, + -0.019_42, + 0.032_01, + -0.000_425_4, + 0.002_257_8, + 0.002_401_5, + 0.009_71, + ]; + for (got, want) in h.to_towgs84().iter().zip(want).take(3) { + assert!(close(*got, want), "{got} {want}"); + } + assert!( + close(h.rx_arcsec, want[3]) && close(h.scale_ppm, want[6]), + "{h:?}" + ); + let r = h.rates.unwrap(); + let want_rates = [ + 0.001_42, + 0.001_34, + 0.000_90, + 0.001_546_1, + 0.001_182, + 0.001_155_1, + 0.000_109, + ]; + for (got, want) in r.values().iter().zip(want_rates) { + assert!(close(*got, want), "{got} {want}"); + } + assert_eq!(r.reference_epoch, 1994.0); + let json = parse_projjson(srid, "td", &def.to_projjson()).unwrap(); + assert_eq!(json, def); + let wkt = parse_wkt2(srid, "td", &def.to_wkt2()).unwrap(); + assert_eq!(wkt, def); + } + + #[test] + fn wkt2_time_dependent_bound_crs_reads_named_units() { + use crate::parse::parse_wkt2; + use crate::srid::Srid; + let text = r#"BOUNDCRS[ + SOURCECRS[GEOGCRS["GDA94",DATUM["GDA94",ELLIPSOID["GRS 1980",6378137,298.257222101]], + CS[ellipsoidal,2],AXIS["latitude",north],AXIS["longitude",east], + ANGLEUNIT["degree",0.0174532925199433]]], + TARGETCRS[GEOGCRS["WGS 84",DATUM["World Geodetic System 1984",ELLIPSOID["WGS 84",6378137,298.257223563]], + CS[ellipsoidal,2],AXIS["latitude",north],AXIS["longitude",east],ANGLEUNIT["degree",0.0174532925199433]]], + ABRIDGEDTRANSFORMATION["t", + METHOD["Time-dependent Position Vector transformation (geog2D domain)",ID["EPSG",1054]], + PARAMETER["X-axis translation",0.5], + PARAMETER["Rate of change of Z-axis translation",2,LENGTHUNIT["millimetres per year",3.16887651727315E-11]], + PARAMETER["Parameter reference epoch",2020,TIMEUNIT["year",31556925.445]]]]"#; + let def = parse_wkt2(Srid::new(910_011).unwrap(), "td", text).unwrap(); + let DatumShift::Helmert(h) = def.datum.to_wgs84 else { + panic!("expected a Helmert shift"); + }; + assert_eq!(h.convention, RotationConvention::PositionVector); + assert_eq!(h.tx_m, 0.5); + let r = h.rates.unwrap(); + assert!(close(r.dtz_m_per_yr, 0.002), "{r:?}"); + assert_eq!(r.reference_epoch, 2020.0); + } + + #[test] + fn writers_emit_rates_only_when_present() { + let static_h = Helmert::translation(1.0, 2.0, 3.0); + assert_eq!(writer_params(&static_h).count(), 7); + assert_eq!(writer_method(&static_h).1, 9606); + let mut td = static_h; + td.convention = RotationConvention::CoordinateFrame; + td.rates = Some(HelmertRates::new([0.0; 7], 2020.0)); + let params: Vec<_> = writer_params(&td).collect(); + assert_eq!(params.len(), HELMERT_PARAM_COUNT); + assert_eq!(params[EPOCH_SLOT].0, 1047); + assert_eq!(params[EPOCH_SLOT].3, 2020.0); + assert_eq!(writer_method(&td).1, 1057); + } +} diff --git a/nodedb-crs/src/parse/json.rs b/nodedb-crs/src/parse/json.rs new file mode 100644 index 000000000..f310cdea7 --- /dev/null +++ b/nodedb-crs/src/parse/json.rs @@ -0,0 +1,347 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! A small JSON reader and string writer for PROJJSON. +//! +//! The crate takes no JSON dependency, so it builds for every target with +//! no allocator or SIMD assumptions. The reader nests at most +//! [`MAX_DEPTH`] levels, so hostile input cannot exhaust the stack. + +use crate::error::CrsParseError; + +/// Format name reported by PROJJSON errors. +pub const PROJJSON_FORMAT: &str = "PROJJSON"; + +/// Deepest nesting the reader accepts. +const MAX_DEPTH: usize = 64; + +/// One JSON value. Object members keep their order. +#[derive(Debug, Clone, PartialEq)] +pub(crate) enum Json { + Null, + Bool(bool), + Number(f64), + String(String), + Array(Vec), + Object(Vec<(String, Json)>), +} + +impl Json { + /// Member `key` of an object. + pub(crate) fn get(&self, key: &str) -> Option<&Json> { + match self { + Json::Object(members) => members.iter().find(|(k, _)| k == key).map(|(_, v)| v), + Json::Null | Json::Bool(_) | Json::Number(_) | Json::String(_) | Json::Array(_) => None, + } + } + + pub(crate) fn as_str(&self) -> Option<&str> { + match self { + Json::String(s) => Some(s), + Json::Null | Json::Bool(_) | Json::Number(_) | Json::Array(_) | Json::Object(_) => None, + } + } + + pub(crate) fn as_f64(&self) -> Option { + match self { + Json::Number(n) => Some(*n), + Json::Null | Json::Bool(_) | Json::String(_) | Json::Array(_) | Json::Object(_) => None, + } + } + + pub(crate) fn as_array(&self) -> Option<&[Json]> { + match self { + Json::Array(items) => Some(items), + Json::Null | Json::Bool(_) | Json::Number(_) | Json::String(_) | Json::Object(_) => { + None + } + } + } +} + +/// Parse `text` as one JSON value. Only whitespace may follow it. +pub(crate) fn parse_json(text: &str) -> Result { + let mut reader = Reader { + text, + bytes: text.as_bytes(), + pos: 0, + }; + reader.skip_space(); + let value = reader.value(0)?; + reader.skip_space(); + if reader.pos != reader.bytes.len() { + return Err(reader.malformed()); + } + Ok(value) +} + +struct Reader<'a> { + text: &'a str, + bytes: &'a [u8], + pos: usize, +} + +impl Reader<'_> { + fn malformed(&self) -> CrsParseError { + CrsParseError::Malformed { + format: PROJJSON_FORMAT, + offset: self.pos, + } + } + + fn peek(&self) -> Option { + self.bytes.get(self.pos).copied() + } + + fn skip_space(&mut self) { + while matches!(self.peek(), Some(b' ' | b'\t' | b'\n' | b'\r')) { + self.pos += 1; + } + } + + fn expect(&mut self, byte: u8) -> Result<(), CrsParseError> { + if self.peek() == Some(byte) { + self.pos += 1; + Ok(()) + } else { + Err(self.malformed()) + } + } + + fn literal(&mut self, word: &str, value: Json) -> Result { + if self.text[self.pos..].starts_with(word) { + self.pos += word.len(); + Ok(value) + } else { + Err(self.malformed()) + } + } + + fn value(&mut self, depth: usize) -> Result { + if depth >= MAX_DEPTH { + return Err(self.malformed()); + } + match self.peek() { + Some(b'{') => self.object(depth), + Some(b'[') => self.array(depth), + Some(b'"') => self.string().map(Json::String), + Some(b't') => self.literal("true", Json::Bool(true)), + Some(b'f') => self.literal("false", Json::Bool(false)), + Some(b'n') => self.literal("null", Json::Null), + Some(b) if b == b'-' || b.is_ascii_digit() => self.number(), + _ => Err(self.malformed()), + } + } + + fn object(&mut self, depth: usize) -> Result { + self.pos += 1; + let mut members = Vec::new(); + self.skip_space(); + if self.peek() == Some(b'}') { + self.pos += 1; + return Ok(Json::Object(members)); + } + loop { + self.skip_space(); + let key = self.string()?; + self.skip_space(); + self.expect(b':')?; + self.skip_space(); + members.push((key, self.value(depth + 1)?)); + self.skip_space(); + match self.peek() { + Some(b',') => self.pos += 1, + Some(b'}') => { + self.pos += 1; + return Ok(Json::Object(members)); + } + _ => return Err(self.malformed()), + } + } + } + + fn array(&mut self, depth: usize) -> Result { + self.pos += 1; + let mut items = Vec::new(); + self.skip_space(); + if self.peek() == Some(b']') { + self.pos += 1; + return Ok(Json::Array(items)); + } + loop { + self.skip_space(); + items.push(self.value(depth + 1)?); + self.skip_space(); + match self.peek() { + Some(b',') => self.pos += 1, + Some(b']') => { + self.pos += 1; + return Ok(Json::Array(items)); + } + _ => return Err(self.malformed()), + } + } + } + + fn string(&mut self) -> Result { + self.expect(b'"')?; + let mut out = String::new(); + loop { + let rest = &self.text[self.pos..]; + let Some(stop) = rest.find(['"', '\\']) else { + return Err(self.malformed()); + }; + let run = &rest[..stop]; + if run.chars().any(|c| c < ' ') { + return Err(self.malformed()); + } + out.push_str(run); + self.pos += stop; + match self.peek() { + Some(b'"') => { + self.pos += 1; + return Ok(out); + } + _ => { + self.pos += 1; + self.escape(&mut out)?; + } + } + } + } + + fn escape(&mut self, out: &mut String) -> Result<(), CrsParseError> { + let c = match self.peek() { + Some(b'"') => '"', + Some(b'\\') => '\\', + Some(b'/') => '/', + Some(b'b') => '\u{8}', + Some(b'f') => '\u{c}', + Some(b'n') => '\n', + Some(b'r') => '\r', + Some(b't') => '\t', + Some(b'u') => { + self.pos += 1; + let first = self.hex4()?; + let code = if (0xD800..0xDC00).contains(&first) { + self.expect(b'\\')?; + self.expect(b'u')?; + let second = self.hex4()?; + if !(0xDC00..0xE000).contains(&second) { + return Err(self.malformed()); + } + 0x10000 + ((first - 0xD800) << 10) + (second - 0xDC00) + } else { + first + }; + let c = char::from_u32(code).ok_or_else(|| self.malformed())?; + out.push(c); + return Ok(()); + } + _ => return Err(self.malformed()), + }; + self.pos += 1; + out.push(c); + Ok(()) + } + + fn hex4(&mut self) -> Result { + let digits = self + .text + .get(self.pos..self.pos + 4) + .ok_or_else(|| self.malformed())?; + let value = u32::from_str_radix(digits, 16).map_err(|_| self.malformed())?; + self.pos += 4; + Ok(value) + } + + fn number(&mut self) -> Result { + let start = self.pos; + while self + .peek() + .is_some_and(|b| b.is_ascii_digit() || matches!(b, b'-' | b'+' | b'.' | b'e' | b'E')) + { + self.pos += 1; + } + let value: f64 = + self.text[start..self.pos] + .parse() + .map_err(|_| CrsParseError::Malformed { + format: PROJJSON_FORMAT, + offset: start, + })?; + if value.is_finite() { + Ok(Json::Number(value)) + } else { + Err(CrsParseError::Malformed { + format: PROJJSON_FORMAT, + offset: start, + }) + } + } +} + +/// Append `text` as a JSON string. +pub(crate) fn push_json_string(out: &mut String, text: &str) { + out.push('"'); + for c in text.chars() { + match c { + '"' => out.push_str("\\\""), + '\\' => out.push_str("\\\\"), + '\n' => out.push_str("\\n"), + '\r' => out.push_str("\\r"), + '\t' => out.push_str("\\t"), + c if c < ' ' => { + let _ = core::fmt::Write::write_fmt(out, format_args!("\\u{:04x}", u32::from(c))); + } + c => out.push(c), + } + } + out.push('"'); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn values_and_escapes() { + let v = parse_json(r#" {"a": [1, -2.5e1, true, null], "b": "x\"é😀\n"} "#).unwrap(); + assert_eq!( + v.get("a").and_then(Json::as_array).map(<[Json]>::len), + Some(4) + ); + assert_eq!( + v.get("a").and_then(|a| a.as_array()?.get(1)?.as_f64()), + Some(-25.0) + ); + assert_eq!(v.get("b").and_then(Json::as_str), Some("x\"é😀\n")); + let mut out = String::new(); + push_json_string(&mut out, "x\"é\n\u{1}"); + assert_eq!( + parse_json(&out).unwrap(), + Json::String("x\"é\n\u{1}".into()) + ); + } + + #[test] + fn malformed_input_names_the_offset() { + for bad in [ + "", + "{", + "{\"a\" 1}", + "[1,]", + "\"x", + "tru", + "1e999", + "{} x", + "\"\u{1}\"", + ] { + assert!( + matches!(parse_json(bad), Err(CrsParseError::Malformed { .. })), + "{bad}" + ); + } + let deep = "[".repeat(70) + &"]".repeat(70); + assert!(parse_json(&deep).is_err()); + } +} diff --git a/nodedb-crs/src/parse/methods/azimuthal.rs b/nodedb-crs/src/parse/methods/azimuthal.rs new file mode 100644 index 000000000..c95130ace --- /dev/null +++ b/nodedb-crs/src/parse/methods/azimuthal.rs @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Azimuthal and perspective methods: stereographic, Lambert azimuthal +//! equal area, azimuthal equidistant and geostationary satellite. + +use super::params::{ + FE, FN, GEOS_PARAMS, K0_NAT, LAT_NAT, LAT_STD, LON_NAT, LON_ORIG, NATURAL_ORIGIN, +}; +use super::spec::{Derive, MethodSpec, method}; + +pub(super) const POLAR_A: MethodSpec = method( + "Polar Stereographic (variant A)", + Some(9810), + "stere", + &[], + &[LAT_NAT, LON_NAT, K0_NAT, FE, FN], +); +pub(super) const POLAR_B: MethodSpec = MethodSpec { + name: "Polar Stereographic (variant B)", + code: Some(9829), + proj: "stere", + flags: &[], + params: &[LAT_STD, LON_ORIG, FE, FN, LAT_NAT, K0_NAT], + derive: Derive::PolarLatitudeFromStandardParallel, +}; +pub(super) const STERE: MethodSpec = method( + "Stereographic", + None, + "stere", + &[], + &[LAT_NAT, LON_NAT, K0_NAT, FE, FN, LAT_STD], +); +pub(super) const STEREA: MethodSpec = method( + "Oblique Stereographic", + Some(9809), + "sterea", + &[], + &[LAT_NAT, LON_NAT, K0_NAT, FE, FN], +); +pub(super) const LAEA: MethodSpec = method( + "Lambert Azimuthal Equal Area", + Some(9820), + "laea", + &[], + NATURAL_ORIGIN, +); +pub(super) const AEQD: MethodSpec = method( + "Azimuthal Equidistant", + Some(1125), + "aeqd", + &[], + NATURAL_ORIGIN, +); +pub(super) const GUAM: MethodSpec = method( + "Guam Projection", + Some(9831), + "aeqd", + &["guam"], + NATURAL_ORIGIN, +); +pub(super) const GEOS_Y: MethodSpec = method( + "Geostationary Satellite (Sweep Y)", + None, + "geos", + &[], + GEOS_PARAMS, +); +pub(super) const GEOS_X: MethodSpec = method( + "Geostationary Satellite (Sweep X)", + None, + "geos", + &["sweep=x"], + GEOS_PARAMS, +); + +/// Methods readers accept and writers never pick: aliases of a writer +/// method with their own parameter sets. +pub(super) const READ_ONLY: &[MethodSpec] = &[ + method( + "Lambert Azimuthal Equal Area (Spherical)", + Some(1027), + "laea", + &[], + NATURAL_ORIGIN, + ), + method( + "Modified Azimuthal Equidistant", + Some(9832), + "aeqd", + &[], + NATURAL_ORIGIN, + ), +]; diff --git a/nodedb-crs/src/parse/methods/conic.rs b/nodedb-crs/src/parse/methods/conic.rs new file mode 100644 index 000000000..63d56a2e0 --- /dev/null +++ b/nodedb-crs/src/parse/methods/conic.rs @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Conic methods: Lambert Conic Conformal, Albers, American Polyconic and +//! Krovak. + +use super::params::{ + E_FO, K0_NAT, KROVAK_PARAMS, LAT_1, LAT_2, LAT_FO, LCC_1SP_PARAMS, LCC_2SP_PARAMS, LON_FO, + N_FO, NATURAL_ORIGIN, +}; +use super::spec::{Angle, MethodSpec, Scale, method, param}; + +pub(super) const LCC_2SP: MethodSpec = method( + "Lambert Conic Conformal (2SP)", + Some(9802), + "lcc", + &[], + LCC_2SP_PARAMS, +); +pub(super) const AEA: MethodSpec = method( + "Albers Equal Area", + Some(9822), + "aea", + &[], + &[LAT_FO, LON_FO, LAT_1, LAT_2, E_FO, N_FO], +); +pub(super) const POLY: MethodSpec = method( + "American Polyconic", + Some(9818), + "poly", + &[], + NATURAL_ORIGIN, +); +pub(super) const KROVAK: MethodSpec = + method("Krovak", Some(9819), "krovak", &["czech"], KROVAK_PARAMS); +pub(super) const KROVAK_NORTH: MethodSpec = method( + "Krovak (North Orientated)", + Some(1041), + "krovak", + &[], + KROVAK_PARAMS, +); +pub(super) const KROVAK_MOD: MethodSpec = method( + "Krovak Modified", + Some(1042), + "mod_krovak", + &["czech"], + KROVAK_PARAMS, +); +pub(super) const KROVAK_MOD_NORTH: MethodSpec = method( + "Krovak Modified (North Orientated)", + Some(1043), + "mod_krovak", + &[], + KROVAK_PARAMS, +); + +/// Methods readers accept and writers never pick: aliases of a writer +/// method with their own parameter sets. +pub(super) const READ_ONLY: &[MethodSpec] = &[ + method( + "Lambert Conic Conformal (1SP)", + Some(9801), + "lcc", + &[], + LCC_1SP_PARAMS, + ), + method( + "Lambert Conic Conformal (West Orientated)", + Some(9826), + "lcc", + &[], + LCC_1SP_PARAMS, + ), + method( + "Lambert Conic Conformal (2SP Michigan)", + Some(1051), + "lcc", + &[], + &[ + LAT_FO, + LON_FO, + LAT_1, + LAT_2, + E_FO, + N_FO, + param("Ellipsoid scaling factor", 1038, Scale, &["k_0"]), + ], + ), + method( + "Lambert Conic Conformal (1SP variant B)", + Some(1102), + "lcc", + &[], + &[ + param("Latitude of natural origin", 8801, Angle, &["lat_1"]), + K0_NAT, + LAT_FO, + LON_FO, + E_FO, + N_FO, + ], + ), +]; diff --git a/nodedb-crs/src/parse/methods/cylindrical.rs b/nodedb-crs/src/parse/methods/cylindrical.rs new file mode 100644 index 000000000..30d2416ed --- /dev/null +++ b/nodedb-crs/src/parse/methods/cylindrical.rs @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Equal-area and equidistant cylindrical methods, Cassini-Soldner and the +//! pseudocylindrical Sinusoidal and Equal Earth. + +use super::params::{FE, FN, K0_NAT, LAT_NAT, LAT_TS_1, LON_NAT, LONGITUDE_ONLY, NATURAL_ORIGIN}; +use super::spec::{MethodSpec, method}; + +pub(super) const CEA: MethodSpec = method( + "Lambert Cylindrical Equal Area", + Some(9835), + "cea", + &[], + &[LAT_TS_1, LON_NAT, FE, FN, K0_NAT], +); +pub(super) const EQC: MethodSpec = method( + "Equidistant Cylindrical", + Some(1028), + "eqc", + &[], + &[LAT_TS_1, LAT_NAT, LON_NAT, FE, FN], +); +pub(super) const CASS: MethodSpec = + method("Cassini-Soldner", Some(9806), "cass", &[], NATURAL_ORIGIN); +pub(super) const HYPERBOLIC_CASS: MethodSpec = method( + "Hyperbolic Cassini-Soldner", + Some(9833), + "cass", + &["hyperbolic"], + NATURAL_ORIGIN, +); +pub(super) const SINU: MethodSpec = method("Sinusoidal", None, "sinu", &[], LONGITUDE_ONLY); +pub(super) const EQEARTH: MethodSpec = + method("Equal Earth", Some(1078), "eqearth", &[], LONGITUDE_ONLY); + +/// Methods readers accept and writers never pick: aliases of a writer +/// method with their own parameter sets. +pub(super) const READ_ONLY: &[MethodSpec] = &[ + method( + "Lambert Cylindrical Equal Area (Spherical)", + Some(9834), + "cea", + &[], + &[LAT_TS_1, LON_NAT, FE, FN], + ), + method( + "Equidistant Cylindrical (Spherical)", + Some(1029), + "eqc", + &[], + &[LAT_TS_1, LAT_NAT, LON_NAT, FE, FN], + ), +]; diff --git a/nodedb-crs/src/parse/methods/lookup.rs b/nodedb-crs/src/parse/methods/lookup.rs new file mode 100644 index 000000000..39d377064 --- /dev/null +++ b/nodedb-crs/src/parse/methods/lookup.rs @@ -0,0 +1,169 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Method lookup: readers by EPSG code or name, writers by projection +//! shape. + +use super::super::model::normalize_name; +use super::azimuthal::{AEQD, GEOS_X, GEOS_Y, GUAM, LAEA, POLAR_A, POLAR_B, STERE, STEREA}; +use super::conic::{AEA, KROVAK, KROVAK_MOD, KROVAK_MOD_NORTH, KROVAK_NORTH, LCC_2SP, POLY}; +use super::cylindrical::{CASS, CEA, EQC, EQEARTH, HYPERBOLIC_CASS, SINU}; +use super::mercator::{HOM_A, HOM_B, LABRD, MERC_A, MERC_B, NZMG, SOMERC, TMERC, WEBMERC}; +use super::spec::MethodSpec; +use super::{azimuthal, conic, cylindrical, mercator}; +use crate::method::{HomVariant, ProjectionDef, SweepAxis}; + +/// Every method a writer picks. +const WRITERS: &[MethodSpec] = &[ + TMERC, + HOM_A, + HOM_B, + LCC_2SP, + MERC_A, + MERC_B, + WEBMERC, + CEA, + EQC, + POLAR_A, + POLAR_B, + STERE, + STEREA, + AEA, + LAEA, + CASS, + HYPERBOLIC_CASS, + POLY, + AEQD, + GUAM, + SINU, + EQEARTH, + KROVAK, + KROVAK_NORTH, + KROVAK_MOD, + KROVAK_MOD_NORTH, + SOMERC, + NZMG, + GEOS_Y, + GEOS_X, + LABRD, +]; + +/// The read-only aliases of every family. +const READ_ONLY: &[&[MethodSpec]] = &[ + mercator::READ_ONLY, + conic::READ_ONLY, + cylindrical::READ_ONLY, + azimuthal::READ_ONLY, +]; + +/// Every method: the writer methods, then the read-only aliases. +fn all_methods() -> impl Iterator { + WRITERS + .iter() + .chain(READ_ONLY.iter().flat_map(|family| family.iter())) +} + +/// The method with EPSG `code`, else the one named `name`. +pub(crate) fn method_by(code: Option, name: &str) -> Option<&'static MethodSpec> { + if let Some(code) = code + && let Some(found) = all_methods().find(|m| m.code == Some(code)) + { + return Some(found); + } + let normalized = normalize_name(name); + all_methods().find(|m| normalize_name(m.name) == normalized) +} + +/// The method a writer names `projection` by. +pub(crate) fn writer_spec(projection: &ProjectionDef) -> &'static MethodSpec { + match projection { + ProjectionDef::TransverseMercator(_) => &TMERC, + ProjectionDef::HotineObliqueMercator(p) => match p.variant { + HomVariant::A => &HOM_A, + HomVariant::B => &HOM_B, + }, + ProjectionDef::LambertConicConformal(_) => &LCC_2SP, + ProjectionDef::Mercator(p) => match p.lat_ts_deg { + Some(_) => &MERC_B, + None => &MERC_A, + }, + ProjectionDef::PseudoMercator(_) => &WEBMERC, + ProjectionDef::LambertCylindricalEqualArea(_) => &CEA, + ProjectionDef::EquidistantCylindrical(_) => &EQC, + ProjectionDef::Stereographic(p) => match (p.lat0_deg.abs() == 90.0, p.lat_ts_deg) { + (true, None) => &POLAR_A, + (true, Some(_)) => &POLAR_B, + (false, _) => &STERE, + }, + ProjectionDef::ObliqueStereographic(_) => &STEREA, + ProjectionDef::AlbersEqualArea(_) => &AEA, + ProjectionDef::LambertAzimuthalEqualArea(_) => &LAEA, + ProjectionDef::CassiniSoldner(p) => { + if p.hyperbolic { + &HYPERBOLIC_CASS + } else { + &CASS + } + } + ProjectionDef::AmericanPolyconic(_) => &POLY, + ProjectionDef::AzimuthalEquidistant(p) => { + if p.guam { + &GUAM + } else { + &AEQD + } + } + ProjectionDef::Sinusoidal(_) => &SINU, + ProjectionDef::EqualEarth(_) => &EQEARTH, + ProjectionDef::Krovak(p) => match (p.modified, p.czech) { + (false, true) => &KROVAK, + (false, false) => &KROVAK_NORTH, + (true, true) => &KROVAK_MOD, + (true, false) => &KROVAK_MOD_NORTH, + }, + ProjectionDef::SwissObliqueMercator(_) => &SOMERC, + ProjectionDef::NewZealandMapGrid(_) => &NZMG, + ProjectionDef::GeostationarySatellite(p) => match p.sweep { + SweepAxis::X => &GEOS_X, + SweepAxis::Y => &GEOS_Y, + }, + ProjectionDef::LabordeObliqueMercator(_) => &LABRD, + } +} + +#[cfg(test)] +mod tests { + use super::super::spec::Target; + use super::*; + + #[test] + fn codes_and_names_are_unique() { + let all: Vec<&MethodSpec> = all_methods().collect(); + for (i, a) in all.iter().enumerate() { + for b in &all[i + 1..] { + assert_ne!(normalize_name(a.name), normalize_name(b.name), "{}", a.name); + if a.code.is_some() { + assert_ne!(a.code, b.code, "{}", a.name); + } + } + } + } + + #[test] + fn lookup_by_code_then_name() { + assert_eq!( + method_by(Some(9807), "anything").map(|m| m.proj), + Some("tmerc") + ); + assert_eq!( + method_by(None, "hotine_oblique_mercator (variant b)").map(|m| m.code), + Some(Some(9815)) + ); + assert!(method_by(Some(1), "Bonne").is_none()); + let hom = method_by(Some(9812), "").unwrap(); + assert_eq!( + hom.param(None, "Azimuth at projection centre") + .map(|p| p.target), + Some(Target::Keys(&["alpha"])) + ); + } +} diff --git a/nodedb-crs/src/parse/methods/mercator.rs b/nodedb-crs/src/parse/methods/mercator.rs new file mode 100644 index 000000000..e462ba9f3 --- /dev/null +++ b/nodedb-crs/src/parse/methods/mercator.rs @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Mercator, transverse and oblique Mercator, and the other conformal +//! grids: Swiss, Laborde and the New Zealand Map Grid. + +use super::params::{ + AZI, FE, FN, HOM_A_PARAMS, HOM_B_PARAMS, K_IL, K_NAT, K0_IL, K0_NAT, LAT_NAT, LAT_PC, LAT_TS_1, + LON_NAT, LON_PC, NATURAL_ORIGIN, +}; +use super::spec::{Angle, MethodSpec, fixed, method}; + +pub(super) const TMERC: MethodSpec = method( + "Transverse Mercator", + Some(9807), + "tmerc", + &[], + &[LAT_NAT, LON_NAT, K_NAT, FE, FN], +); +pub(super) const HOM_A: MethodSpec = method( + "Hotine Oblique Mercator (variant A)", + Some(9812), + "omerc", + &["no_uoff"], + HOM_A_PARAMS, +); +pub(super) const HOM_B: MethodSpec = method( + "Hotine Oblique Mercator (variant B)", + Some(9815), + "omerc", + &[], + HOM_B_PARAMS, +); +pub(super) const MERC_A: MethodSpec = method( + "Mercator (variant A)", + Some(9804), + "merc", + &[], + &[ + fixed("Latitude of natural origin", 8801, Angle, 0.0), + LON_NAT, + K0_NAT, + FE, + FN, + ], +); +pub(super) const MERC_B: MethodSpec = method( + "Mercator (variant B)", + Some(9805), + "merc", + &[], + &[LAT_TS_1, LON_NAT, FE, FN], +); +pub(super) const WEBMERC: MethodSpec = method( + "Popular Visualisation Pseudo Mercator", + Some(1024), + "webmerc", + &[], + &[ + fixed("Latitude of natural origin", 8801, Angle, 0.0), + LON_NAT, + FE, + FN, + ], +); +pub(super) const SOMERC: MethodSpec = method( + "Swiss Oblique Cylindrical", + Some(9814), + "somerc", + &[], + &[LAT_PC, LON_PC, FE, FN, K0_IL], +); +pub(super) const LABRD: MethodSpec = method( + "Laborde Oblique Mercator", + Some(9813), + "labrd", + &[], + &[LAT_PC, LON_PC, AZI, K_IL, FE, FN], +); +pub(super) const NZMG: MethodSpec = method( + "New Zealand Map Grid", + Some(9811), + "nzmg", + &[], + NATURAL_ORIGIN, +); + +/// Methods readers accept and writers never pick: aliases of a writer +/// method with their own parameter sets. +pub(super) const READ_ONLY: &[MethodSpec] = &[ + method( + "Transverse Mercator (South Orientated)", + Some(9808), + "tmerc", + &[], + &[LAT_NAT, LON_NAT, K_NAT, FE, FN], + ), + method( + "Mercator (Spherical)", + Some(1026), + "merc", + &[], + &[ + fixed("Latitude of natural origin", 8801, Angle, 0.0), + LON_NAT, + FE, + FN, + ], + ), +]; diff --git a/nodedb-crs/src/parse/methods/mod.rs b/nodedb-crs/src/parse/methods/mod.rs new file mode 100644 index 000000000..8ca01e3cb --- /dev/null +++ b/nodedb-crs/src/parse/methods/mod.rs @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! EPSG projection methods and their parameters, mapped to PROJ keys. +//! +//! WKT2 and PROJJSON name a projection by its EPSG method and parameters. +//! Each [`MethodSpec`] maps one method to the PROJ `+proj` value and flags +//! the PROJ-string parser reads, and each [`ParamSpec`](spec::ParamSpec) maps one parameter +//! to the PROJ keys it sets. Readers look a method up by EPSG code, then by +//! name. Writers pick one method per projection shape. Each method family +//! has its own table file. + +pub(super) mod azimuthal; +pub(super) mod conic; +pub(super) mod cylindrical; +pub(super) mod lookup; +pub(super) mod mercator; +pub(super) mod params; +pub(super) mod spec; + +pub(crate) use lookup::{method_by, writer_spec}; +pub(crate) use spec::{Derive, FLAG_KEYS, MethodSpec, ParamKind, Target}; diff --git a/nodedb-crs/src/parse/methods/params.rs b/nodedb-crs/src/parse/methods/params.rs new file mode 100644 index 000000000..16eba5464 --- /dev/null +++ b/nodedb-crs/src/parse/methods/params.rs @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! EPSG parameters and parameter sets the method families share. + +use super::spec::{Angle, Length, ParamSpec, Scale, Target, fixed, param}; + +pub(super) const LAT_NAT: ParamSpec = param("Latitude of natural origin", 8801, Angle, &["lat_0"]); +pub(super) const LON_NAT: ParamSpec = param("Longitude of natural origin", 8802, Angle, &["lon_0"]); +pub(super) const K_NAT: ParamSpec = param("Scale factor at natural origin", 8805, Scale, &["k"]); +pub(super) const K0_NAT: ParamSpec = param("Scale factor at natural origin", 8805, Scale, &["k_0"]); +pub(super) const FE: ParamSpec = param("False easting", 8806, Length, &["x_0"]); +pub(super) const FN: ParamSpec = param("False northing", 8807, Length, &["y_0"]); +pub(super) const LAT_FO: ParamSpec = param("Latitude of false origin", 8821, Angle, &["lat_0"]); +pub(super) const LON_FO: ParamSpec = param("Longitude of false origin", 8822, Angle, &["lon_0"]); +pub(super) const LAT_1: ParamSpec = + param("Latitude of 1st standard parallel", 8823, Angle, &["lat_1"]); +pub(super) const LAT_2: ParamSpec = + param("Latitude of 2nd standard parallel", 8824, Angle, &["lat_2"]); +pub(super) const LAT_TS_1: ParamSpec = param( + "Latitude of 1st standard parallel", + 8823, + Angle, + &["lat_ts"], +); +pub(super) const E_FO: ParamSpec = param("Easting at false origin", 8826, Length, &["x_0"]); +pub(super) const N_FO: ParamSpec = param("Northing at false origin", 8827, Length, &["y_0"]); +pub(super) const LAT_PC: ParamSpec = + param("Latitude of projection centre", 8811, Angle, &["lat_0"]); +pub(super) const LONC_PC: ParamSpec = + param("Longitude of projection centre", 8812, Angle, &["lonc"]); +pub(super) const LON_PC: ParamSpec = + param("Longitude of projection centre", 8812, Angle, &["lon_0"]); +pub(super) const ALPHA: ParamSpec = param("Azimuth of initial line", 8813, Angle, &["alpha"]); +pub(super) const AZI: ParamSpec = param("Azimuth of initial line", 8813, Angle, &["azi"]); +pub(super) const GAMMA: ParamSpec = + param("Angle from Rectified to Skew Grid", 8814, Angle, &["gamma"]); +pub(super) const K_IL: ParamSpec = param("Scale factor on initial line", 8815, Scale, &["k"]); +pub(super) const K0_IL: ParamSpec = param("Scale factor on initial line", 8815, Scale, &["k_0"]); +pub(super) const E_PC: ParamSpec = param("Easting at projection centre", 8816, Length, &["x_0"]); +pub(super) const N_PC: ParamSpec = param("Northing at projection centre", 8817, Length, &["y_0"]); +pub(super) const LAT_STD: ParamSpec = + param("Latitude of standard parallel", 8832, Angle, &["lat_ts"]); +pub(super) const LON_ORIG: ParamSpec = param("Longitude of origin", 8833, Angle, &["lon_0"]); +pub(super) const SAT_HEIGHT: ParamSpec = ParamSpec { + name: "Satellite Height", + code: None, + kind: Length, + target: Target::Keys(&["h"]), +}; + +pub(super) const NATURAL_ORIGIN: &[ParamSpec] = &[LAT_NAT, LON_NAT, FE, FN]; +pub(super) const LONGITUDE_ONLY: &[ParamSpec] = &[LON_NAT, FE, FN]; +pub(super) const HOM_A_PARAMS: &[ParamSpec] = &[LAT_PC, LONC_PC, ALPHA, GAMMA, K_IL, FE, FN]; +pub(super) const HOM_B_PARAMS: &[ParamSpec] = &[LAT_PC, LONC_PC, ALPHA, GAMMA, K_IL, E_PC, N_PC]; +pub(super) const LCC_2SP_PARAMS: &[ParamSpec] = &[LAT_FO, LON_FO, LAT_1, LAT_2, E_FO, N_FO, K0_NAT]; +pub(super) const LCC_1SP_PARAMS: &[ParamSpec] = &[ + param( + "Latitude of natural origin", + 8801, + Angle, + &["lat_1", "lat_0"], + ), + LON_NAT, + K0_NAT, + FE, + FN, +]; +pub(super) const KROVAK_PARAMS: &[ParamSpec] = &[ + LAT_PC, + LON_ORIG, + param("Co-latitude of cone axis", 1036, Angle, &["alpha"]), + fixed("Latitude of pseudo standard parallel", 8818, Angle, 78.5), + param( + "Scale factor on pseudo standard parallel", + 8819, + Scale, + &["k"], + ), + FE, + FN, +]; +pub(super) const GEOS_PARAMS: &[ParamSpec] = &[SAT_HEIGHT, LON_NAT, FE, FN]; diff --git a/nodedb-crs/src/parse/methods/spec.rs b/nodedb-crs/src/parse/methods/spec.rs new file mode 100644 index 000000000..a30892c51 --- /dev/null +++ b/nodedb-crs/src/parse/methods/spec.rs @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! The method and parameter specs every family table builds from. + +use super::super::model::{DEGREE_RAD, normalize_name}; + +/// What a parameter measures. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum ParamKind { + /// Degrees in PROJ. + Angle, + /// Metres in PROJ. + Length, + /// A unitless factor. + Scale, +} + +impl ParamKind { + /// The factor of the unit PROJ reads this kind in: degree, metre or + /// unity. + pub(crate) fn canonical_factor(self) -> f64 { + match self { + ParamKind::Angle => DEGREE_RAD, + ParamKind::Length | ParamKind::Scale => 1.0, + } + } +} + +/// Where a parameter's value goes. +#[derive(Debug, Clone, Copy, PartialEq)] +pub(crate) enum Target { + /// Each PROJ key takes the value. Writers emit the first key. + Keys(&'static [&'static str]), + /// PROJ fixes the value. A reader refuses any other. + Fixed(f64), +} + +/// One EPSG parameter of a method. +#[derive(Debug, Clone, Copy, PartialEq)] +pub(crate) struct ParamSpec { + pub name: &'static str, + pub code: Option, + pub kind: ParamKind, + pub target: Target, +} + +/// A value a reader derives when the text leaves it out. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum Derive { + None, + /// Polar Stereographic (variant B) has no origin latitude: the pole on + /// the side of the standard parallel. + PolarLatitudeFromStandardParallel, +} + +/// One EPSG method. +#[derive(Debug, Clone, Copy, PartialEq)] +pub(crate) struct MethodSpec { + pub name: &'static str, + pub code: Option, + /// The PROJ `+proj` value. + pub proj: &'static str, + /// PROJ tokens the method implies, such as `no_uoff` or `sweep=x`. + pub flags: &'static [&'static str], + pub params: &'static [ParamSpec], + pub derive: Derive, +} + +impl MethodSpec { + /// The parameter with EPSG `code`, else the one named `name`. + pub(crate) fn param(&self, code: Option, name: &str) -> Option<&'static ParamSpec> { + let code = code.or_else(|| param_code_alias(name)); + if let Some(code) = code + && let Some(found) = self.params.iter().find(|p| p.code == Some(code)) + { + return Some(found); + } + let normalized = normalize_name(name); + self.params + .iter() + .find(|p| normalize_name(p.name) == normalized) + } +} + +/// PROJ flags a parameter can carry under the `PROJ ` prefix. +pub(crate) const FLAG_KEYS: &[&str] = &["no_rot"]; + +/// The EPSG code of a parameter name PROJ wrote in another version. +fn param_code_alias(name: &str) -> Option { + match normalize_name(name).as_str() { + "azimuthatprojectioncentre" => Some(8813), + "scalefactoratprojectioncentre" => Some(8815), + _ => None, + } +} + +pub(super) const fn param( + name: &'static str, + code: u32, + kind: ParamKind, + key: &'static [&'static str], +) -> ParamSpec { + ParamSpec { + name, + code: Some(code), + kind, + target: Target::Keys(key), + } +} + +pub(super) const fn fixed(name: &'static str, code: u32, kind: ParamKind, value: f64) -> ParamSpec { + ParamSpec { + name, + code: Some(code), + kind, + target: Target::Fixed(value), + } +} + +pub(super) use ParamKind::{Angle, Length, Scale}; + +pub(super) const fn method( + name: &'static str, + code: Option, + proj: &'static str, + flags: &'static [&'static str], + params: &'static [ParamSpec], +) -> MethodSpec { + MethodSpec { + name, + code, + proj, + flags, + params, + derive: Derive::None, + } +} diff --git a/nodedb-crs/src/parse/mod.rs b/nodedb-crs/src/parse/mod.rs new file mode 100644 index 000000000..506740170 --- /dev/null +++ b/nodedb-crs/src/parse/mod.rs @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! CRS definition text: PROJ strings, WKT2 and PROJJSON, read and written. + +mod helmert_params; +mod json; +mod methods; +mod model; +pub mod proj_string; +pub mod proj_writer; +pub mod projjson; +pub mod projjson_writer; +#[cfg(test)] +mod round_trip_cases; +pub mod tokens; +mod unit_names; +pub mod wkt2; +mod wkt2_tree; +pub mod wkt2_writer; + +pub use json::PROJJSON_FORMAT; +pub use proj_string::{IMPLEMENTED_METHODS, parse_proj_string}; +pub use projjson::parse_projjson; +pub use tokens::{PROJ_FORMAT, ProjParams}; +pub use wkt2::parse_wkt2; +pub use wkt2_tree::WKT2_FORMAT; diff --git a/nodedb-crs/src/parse/model.rs b/nodedb-crs/src/parse/model.rs new file mode 100644 index 000000000..ffb4a6054 --- /dev/null +++ b/nodedb-crs/src/parse/model.rs @@ -0,0 +1,374 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! The CRS model WKT2 and PROJJSON share: a datum, an optional conversion +//! named by its EPSG method and parameters, a linear unit, an axis order +//! and an area of use. +//! +//! Both parsers read text into this model, and [`build_def`] turns it into +//! a [`CrsDef`] through the PROJ-string parser, so every method, datum and +//! unit that parser supports reads the same way from every format. Both +//! writers go the other way through [`model_of`]. + +use core::fmt::Write; + +use super::methods::{Derive, FLAG_KEYS, MethodSpec, ParamKind, Target, method_by, writer_spec}; +use super::proj_string::parse_proj_string; +use crate::def::{AreaOfUse, AxisOrder, CrsDef, CrsKind, DatumShift, Ellipsoid}; +use crate::error::CrsParseError; +use crate::srid::Srid; + +/// Radians per degree. +pub(crate) const DEGREE_RAD: f64 = core::f64::consts::PI / 180.0; + +/// Radians per arc-second. +pub(crate) const ARC_SECOND_RAD: f64 = DEGREE_RAD / 3600.0; + +/// Parts per million as a scale. +pub(crate) const PPM: f64 = 1e-6; + +/// Relative distance within which a unit factor equals a canonical one. +/// Text forms print π-derived factors to 15 digits. An exact compare +/// misses the canonical factor and scales every angle by a rounding error. +const FACTOR_TOLERANCE: f64 = 1e-9; + +/// Name prefix of a parameter that carries a PROJ key no EPSG parameter +/// names. +pub(crate) const PROJ_PARAM_PREFIX: &str = "PROJ "; + +/// One CRS in the shared model. +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct CrsModel { + pub name: String, + pub kind: CrsKind, + pub datum: DatumModel, + pub conversion: Option, + /// Metres per unit of projected coordinates. + pub metres_per_unit: f64, + pub axis: AxisOrder, + pub area_of_use: Option, +} + +/// The geodetic datum of a model. +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct DatumModel { + pub ellipsoid: Ellipsoid, + pub prime_meridian_deg: f64, + pub epsg_code: Option, + pub to_wgs84: DatumShift, +} + +/// A map projection as an EPSG method and its parameters. +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct ConversionModel { + pub method_name: String, + pub method_code: Option, + pub params: Vec, + /// Radians per unit of an angle parameter written without a unit. + pub default_angle_rad: f64, + /// Metres per unit of a length parameter written without a unit. + pub default_length_m: f64, +} + +/// One conversion parameter as written. +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct ParamModel { + pub name: String, + pub code: Option, + pub value: f64, + /// The unit's factor to radians, metres or unity. `None` when the text + /// gives no unit. + pub factor: Option, + /// What the parameter measures, when the method names it. Writers name + /// the unit from it. + pub kind: Option, +} + +/// `value` in a unit of `factor`, expressed in the unit of `canonical`. +/// A factor within [`FACTOR_TOLERANCE`] of the canonical one keeps the +/// value as written. +pub(crate) fn convert(value: f64, factor: f64, canonical: f64) -> f64 { + if ((factor / canonical) - 1.0).abs() <= FACTOR_TOLERANCE { + value + } else { + value * factor / canonical + } +} + +/// Lower-case `text` with every character but letters and digits dropped, +/// so names compare without case, spaces or punctuation. +pub(crate) fn normalize_name(text: &str) -> String { + text.chars() + .filter(char::is_ascii_alphanumeric) + .map(|c| c.to_ascii_lowercase()) + .collect() +} + +/// Append formatted text. Writing to a `String` cannot fail. +fn push(out: &mut String, args: core::fmt::Arguments<'_>) { + let _ = out.write_fmt(args); +} + +/// Build the definition `model` describes, named `name` under `srid`. +pub(crate) fn build_def( + srid: Srid, + name: &str, + model: &CrsModel, + format: &'static str, +) -> Result { + let mut proj = String::new(); + match (&model.conversion, model.kind) { + (None, CrsKind::Geographic) => proj.push_str("+proj=longlat"), + (Some(conversion), CrsKind::Projected) => write_conversion(&mut proj, conversion, format)?, + (None, CrsKind::Projected) => { + return Err(CrsParseError::content( + format, + "a projected CRS needs a conversion", + )); + } + (Some(_), CrsKind::Geographic) => { + return Err(CrsParseError::content( + format, + "a geographic CRS has no conversion", + )); + } + } + let e = &model.datum.ellipsoid; + if e.is_sphere() { + push(&mut proj, format_args!(" +R={}", e.a)); + } else { + push(&mut proj, format_args!(" +a={} +rf={}", e.a, e.inv_f)); + } + if model.datum.prime_meridian_deg != 0.0 { + push( + &mut proj, + format_args!(" +pm={}", model.datum.prime_meridian_deg), + ); + } + if model.kind == CrsKind::Projected { + push( + &mut proj, + format_args!(" +to_meter={}", model.metres_per_unit), + ); + } + if !model.axis.is_enu() { + push(&mut proj, format_args!(" +axis={}", model.axis)); + } + let mut def = parse_proj_string(srid, name, &proj)?; + def.datum.to_wgs84 = model.datum.to_wgs84; + def.datum.epsg_code = model.datum.epsg_code; + def.area_of_use = model.area_of_use; + Ok(def) +} + +/// Append the PROJ parameters of `conversion`. +fn write_conversion( + out: &mut String, + conversion: &ConversionModel, + format: &'static str, +) -> Result<(), CrsParseError> { + let spec = method_by(conversion.method_code, &conversion.method_name).ok_or_else(|| { + CrsParseError::content( + format, + format!( + "projection method '{}' is not implemented", + conversion.method_name + ), + ) + })?; + push(out, format_args!("+proj={}", spec.proj)); + for flag in spec.flags { + push(out, format_args!(" +{flag}")); + } + let mut written: Vec<(&str, f64)> = Vec::new(); + for param in &conversion.params { + write_param(out, spec, conversion, param, format, &mut written)?; + } + match spec.derive { + Derive::None => {} + Derive::PolarLatitudeFromStandardParallel => { + let has_lat0 = written.iter().any(|(k, _)| *k == "lat_0"); + let lat_ts = written + .iter() + .find(|(k, _)| *k == "lat_ts") + .map(|(_, v)| *v); + if let (false, Some(lat_ts)) = (has_lat0, lat_ts) { + let pole = if lat_ts < 0.0 { -90.0 } else { 90.0 }; + push(out, format_args!(" +lat_0={pole}")); + } + } + } + Ok(()) +} + +/// Append one parameter, normalized to degrees, metres or unity. +fn write_param<'s>( + out: &mut String, + spec: &'s MethodSpec, + conversion: &ConversionModel, + param: &ParamModel, + format: &'static str, + written: &mut Vec<(&'s str, f64)>, +) -> Result<(), CrsParseError> { + if let Some(key) = param.name.strip_prefix(PROJ_PARAM_PREFIX) { + return write_proj_param(out, key, param.value, format); + } + let found = spec.param(param.code, ¶m.name).ok_or_else(|| { + CrsParseError::content( + format, + format!("method '{}' has no parameter '{}'", spec.name, param.name), + ) + })?; + let value = match found.kind { + ParamKind::Angle => convert( + param.value, + param.factor.unwrap_or(conversion.default_angle_rad), + DEGREE_RAD, + ), + ParamKind::Length => convert( + param.value, + param.factor.unwrap_or(conversion.default_length_m), + 1.0, + ), + ParamKind::Scale => convert(param.value, param.factor.unwrap_or(1.0), 1.0), + }; + match found.target { + Target::Keys(keys) => { + for &key in keys { + push(out, format_args!(" +{key}={value}")); + written.push((key, value)); + } + } + Target::Fixed(expected) => { + if (value - expected).abs() > FACTOR_TOLERANCE * expected.abs().max(1.0) { + return Err(CrsParseError::content( + format, + format!( + "parameter '{}' of method '{}' must be {expected}, found {value}", + found.name, spec.name + ), + )); + } + } + } + Ok(()) +} + +/// Append a parameter that carries a PROJ key directly. A flag key is +/// written when its value is not zero. +fn write_proj_param( + out: &mut String, + key: &str, + value: f64, + format: &'static str, +) -> Result<(), CrsParseError> { + if FLAG_KEYS.contains(&key) { + if value != 0.0 { + push(out, format_args!(" +{key}")); + } + return Ok(()); + } + if key.is_empty() || !key.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'_') { + return Err(CrsParseError::content( + format, + format!("parameter '{PROJ_PARAM_PREFIX}{key}' names no PROJ key"), + )); + } + push(out, format_args!(" +{key}={value}")); + Ok(()) +} + +/// The model of `def`, for a writer. +pub(crate) fn model_of(def: &CrsDef) -> CrsModel { + CrsModel { + name: def.name.to_string(), + kind: def.kind, + datum: DatumModel { + ellipsoid: def.datum.ellipsoid, + prime_meridian_deg: def.datum.prime_meridian_deg, + epsg_code: def.datum.epsg_code, + to_wgs84: def.datum.to_wgs84, + }, + conversion: def.projection.as_ref().map(conversion_of), + metres_per_unit: def.unit.metres_per_unit, + axis: def.axis, + area_of_use: def.area_of_use, + } +} + +/// The conversion of `projection`: its writer method, each PROJ parameter +/// under its EPSG name, and every fixed parameter of the method. +fn conversion_of(projection: &crate::method::ProjectionDef) -> ConversionModel { + let spec = writer_spec(projection); + let mut text = String::new(); + projection.write_proj(&mut text); + let mut params = Vec::new(); + for token in text.split_whitespace() { + let token = token.trim_start_matches('+'); + let (key, value) = match token.split_once('=') { + Some((k, v)) => (k, Some(v)), + None => (token, None), + }; + if key == "proj" || spec.flags.contains(&token) { + continue; + } + let number = value.and_then(|v| v.parse::().ok()); + let found = spec.params.iter().find(|p| match p.target { + Target::Keys(keys) => keys.first() == Some(&key), + Target::Fixed(_) => false, + }); + match (found, number) { + (Some(p), Some(v)) => params.push(ParamModel { + name: p.name.to_string(), + code: p.code, + value: v, + factor: Some(p.kind.canonical_factor()), + kind: Some(p.kind), + }), + _ => params.push(ParamModel { + name: format!("{PROJ_PARAM_PREFIX}{key}"), + code: None, + value: number.unwrap_or(1.0), + factor: None, + kind: None, + }), + } + } + for p in spec.params { + if let Target::Fixed(v) = p.target { + params.push(ParamModel { + name: p.name.to_string(), + code: p.code, + value: v, + factor: Some(p.kind.canonical_factor()), + kind: Some(p.kind), + }); + } + } + ConversionModel { + method_name: spec.name.to_string(), + method_code: spec.code, + params, + default_angle_rad: DEGREE_RAD, + default_length_m: 1.0, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn near_canonical_factors_keep_the_value() { + assert_eq!(convert(102.25, 0.0174532925199433, DEGREE_RAD), 102.25); + assert_eq!(convert(1.5, 4.84813681109536e-6, ARC_SECOND_RAD), 1.5); + let grads = convert(100.0, core::f64::consts::PI / 200.0, DEGREE_RAD); + assert!((grads - 90.0).abs() < 1e-12); + assert_eq!(convert(1000.0, 0.3048, 1.0), 304.8); + } + + #[test] + fn names_normalize() { + assert_eq!(normalize_name("Transverse Mercator"), "transversemercator"); + assert_eq!(normalize_name("Transverse_Mercator"), "transversemercator"); + } +} diff --git a/nodedb-crs/src/parse/proj_string.rs b/nodedb-crs/src/parse/proj_string.rs new file mode 100644 index 000000000..8197b48c0 --- /dev/null +++ b/nodedb-crs/src/parse/proj_string.rs @@ -0,0 +1,513 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! PROJ-string parser. +//! +//! Follows PROJ's defaults: GRS 1980 when no ellipsoid is given, the first +//! occurrence of a key wins, and explicit `+ellps` / `+towgs84` override +//! what `+datum` supplies. + +use super::tokens::{ProjParams, parse_number}; +use crate::def::{ + AxisOrder, CrsDef, CrsKind, Datum, DatumShift, Ellipsoid, Helmert, LinearUnit, + datum_by_proj_name, prime_meridian_by_proj_name, +}; +use crate::error::CrsParseError; +use crate::method::{ + AeaParams, AeqdParams, CassParams, CeaParams, EqEarthParams, EqcParams, GeosParams, HomParams, + HomVariant, KrovakParams, LabrdParams, LaeaParams, LccParams, MercParams, NzmgParams, + PolyParams, ProjectionDef, SinuParams, SomercParams, StereParams, StereaParams, TmParams, + WebMercParams, alpha_from_gamma, gamma_from_alpha, +}; +use crate::srid::Srid; + +/// The `+proj` values this parser implements, for error messages. +pub const IMPLEMENTED_METHODS: &str = "longlat, latlong, utm, tmerc, omerc, lcc, merc, webmerc, \ + cea, eqc, stere, ups, sterea, aea, laea, cass, poly, aeqd, sinu, eqearth, krovak, mod_krovak, \ + somerc, nzmg, geos, labrd"; + +/// UTM scale factor on the central meridian. +const UTM_K0: f64 = 0.9996; + +/// UTM false easting, metres. +const UTM_FALSE_EASTING: f64 = 500_000.0; + +/// UTM false northing in the southern hemisphere, metres. +const UTM_FALSE_NORTHING_SOUTH: f64 = 10_000_000.0; + +/// Number of UTM zones. +const UTM_ZONES: f64 = 60.0; + +/// Width of a UTM zone, degrees. +const UTM_ZONE_WIDTH_DEG: f64 = 6.0; + +/// Central meridian of zone 0, so zone z has λ0 = z · 6° − 183°. +const UTM_ZONE0_MERIDIAN_DEG: f64 = -183.0; + +/// Largest absolute latitude, degrees. +const MAX_LATITUDE_DEG: f64 = 90.0; + +/// Number of values in a 3-parameter `+towgs84`. +const TOWGS84_TRANSLATION_ONLY: usize = 3; + +/// Number of values in a 7-parameter `+towgs84`. +const TOWGS84_FULL: usize = 7; + +/// Parse a PROJ string into a CRS definition. The area of use stays +/// `None`: PROJ strings do not carry one. +pub fn parse_proj_string(srid: Srid, name: &str, text: &str) -> Result { + let p = ProjParams::tokenize(text)?; + let axis = match p.text("axis")? { + None => AxisOrder::ENU, + Some(v) => AxisOrder::parse(v)?, + }; + check_type(&p)?; + let method = p.text("proj")?.ok_or(CrsParseError::Missing { + key: "proj", + method: "crs", + })?; + let datum = parse_datum(&p)?; + let projection = match method { + "longlat" | "latlong" => None, + other => Some(parse_projection(&p, other, &datum.ellipsoid)?), + }; + let (kind, unit) = match projection { + None => (CrsKind::Geographic, LinearUnit::METRE), + Some(_) => (CrsKind::Projected, parse_unit(&p)?), + }; + Ok(CrsDef { + srid, + name: name.into(), + kind, + datum, + projection, + unit, + axis, + area_of_use: None, + }) +} + +/// The projection `+proj=method` names, read from `p`. +fn parse_projection( + p: &ProjParams<'_>, + method: &str, + ellipsoid: &Ellipsoid, +) -> Result { + Ok(match method { + "utm" => parse_utm(p)?, + "tmerc" => parse_tmerc(p)?, + "omerc" => parse_omerc(p, ellipsoid)?, + "lcc" => ProjectionDef::LambertConicConformal(LccParams::from_proj(p)?), + "merc" => ProjectionDef::Mercator(MercParams::from_proj(p)?), + "webmerc" => ProjectionDef::PseudoMercator(WebMercParams::from_proj(p)?), + "cea" => ProjectionDef::LambertCylindricalEqualArea(CeaParams::from_proj(p)?), + "eqc" => ProjectionDef::EquidistantCylindrical(EqcParams::from_proj(p)?), + "stere" => ProjectionDef::Stereographic(StereParams::from_proj(p)?), + // PROJ defines UPS on an ellipsoid only. + "ups" if ellipsoid.is_sphere() => return Err(CrsParseError::invalid("proj", "ups")), + "ups" => ProjectionDef::Stereographic(StereParams::from_proj_ups(p)), + "sterea" => ProjectionDef::ObliqueStereographic(StereaParams::from_proj(p)?), + "aea" => ProjectionDef::AlbersEqualArea(AeaParams::from_proj(p)?), + "laea" => ProjectionDef::LambertAzimuthalEqualArea(LaeaParams::from_proj(p)?), + "cass" => ProjectionDef::CassiniSoldner(CassParams::from_proj(p)?), + "poly" => ProjectionDef::AmericanPolyconic(PolyParams::from_proj(p)?), + "aeqd" => ProjectionDef::AzimuthalEquidistant(AeqdParams::from_proj(p)?), + "sinu" => ProjectionDef::Sinusoidal(SinuParams::from_proj(p)?), + "eqearth" => ProjectionDef::EqualEarth(EqEarthParams::from_proj(p)?), + "krovak" | "mod_krovak" => ProjectionDef::Krovak(KrovakParams::from_proj(p)?), + "somerc" => ProjectionDef::SwissObliqueMercator(SomercParams::from_proj(p)?), + "nzmg" => ProjectionDef::NewZealandMapGrid(NzmgParams::from_proj(p)?), + "geos" => ProjectionDef::GeostationarySatellite(GeosParams::from_proj(p)?), + "labrd" => ProjectionDef::LabordeObliqueMercator(LabrdParams::from_proj(p)?), + other => { + return Err(CrsParseError::UnsupportedMethod { + method: other.into(), + implemented: IMPLEMENTED_METHODS, + }); + } + }) +} + +/// Accept `+type=crs` only. +fn check_type(p: &ProjParams<'_>) -> Result<(), CrsParseError> { + match p.text("type")? { + None | Some("crs") => Ok(()), + Some(other) => Err(CrsParseError::invalid("type", other)), + } +} + +/// Fail unless `value` is finite and above zero. +fn positive(key: &str, value: f64) -> Result { + if value > 0.0 && value.is_finite() { + Ok(value) + } else { + Err(CrsParseError::invalid(key, &value.to_string())) + } +} + +/// Fail unless `value` is a latitude strictly inside ±90°. +fn open_latitude(key: &str, value: f64) -> Result { + if value.abs() < MAX_LATITUDE_DEG { + Ok(value) + } else { + Err(CrsParseError::invalid(key, &value.to_string())) + } +} + +/// Fail unless `value` is a latitude within ±90°, poles included. +fn closed_latitude(key: &str, value: f64) -> Result { + if value.abs() <= MAX_LATITUDE_DEG { + Ok(value) + } else { + Err(CrsParseError::invalid(key, &value.to_string())) + } +} + +fn parse_datum(p: &ProjParams<'_>) -> Result { + let named = match p.text("datum")? { + None => None, + Some(n) => Some(datum_by_proj_name(n).ok_or_else(|| CrsParseError::invalid("datum", n))?), + }; + let ellipsoid = parse_ellipsoid(p, named.map(|d| d.ellipsoid))?; + let to_wgs84 = match p.text("towgs84")? { + Some(v) => DatumShift::Helmert(parse_towgs84(v)?), + None => named.map_or(DatumShift::Identity, |d| d.shift), + }; + let prime_meridian_deg = match p.text("pm")? { + None => 0.0, + Some(v) => match prime_meridian_by_proj_name(v) { + Some(deg) => deg, + None => parse_number("pm", v)?, + }, + }; + Ok(Datum { + ellipsoid, + prime_meridian_deg, + to_wgs84, + // PROJ strings carry no EPSG datum code. + epsg_code: None, + }) +} + +/// Resolve the ellipsoid: `+R` wins, then `+ellps` (or the datum's), then +/// `+a` and a shape key (`+rf`, `+f`, `+b`) override parts of it. +fn parse_ellipsoid( + p: &ProjParams<'_>, + datum_ellipsoid: Option, +) -> Result { + if let Some(r) = p.number("R")? { + return Ok(Ellipsoid::sphere(positive("R", r)?)); + } + let explicit_a = p.number("a")?; + let named = match p.text("ellps")? { + None => None, + Some(n) => { + Some(Ellipsoid::by_proj_name(n).ok_or_else(|| CrsParseError::invalid("ellps", n))?) + } + }; + let base = match (named.or(datum_ellipsoid), explicit_a) { + (Some(e), _) => e, + // PROJ reads a bare `+a` with no shape as a sphere. + (None, Some(a)) => Ellipsoid::sphere(a), + (None, None) => Ellipsoid::GRS80, + }; + let a = positive("a", explicit_a.unwrap_or(base.a))?; + let inv_f = if let Some(rf) = p.number("rf")? { + positive("rf", rf)? + } else if let Some(f) = p.number("f")? { + if f == 0.0 { + 0.0 + } else { + 1.0 / positive("f", f)? + } + } else if let Some(b) = p.number("b")? { + if b == a { + 0.0 + } else if b > 0.0 && b < a { + a / (a - b) + } else { + return Err(CrsParseError::invalid("b", &b.to_string())); + } + } else { + base.inv_f + }; + Ok(Ellipsoid { a, inv_f }) +} + +/// Parse 3 or 7 comma-separated `+towgs84` values, position-vector +/// convention. +fn parse_towgs84(text: &str) -> Result { + let mut values = [0.0; TOWGS84_FULL]; + let mut count = 0; + for part in text.split(',') { + let slot = values + .get_mut(count) + .ok_or_else(|| CrsParseError::invalid("towgs84", text))?; + *slot = parse_number("towgs84", part.trim())?; + count += 1; + } + if count != TOWGS84_TRANSLATION_ONLY && count != TOWGS84_FULL { + return Err(CrsParseError::invalid("towgs84", text)); + } + Ok(Helmert::position_vector(values)) +} + +fn parse_unit(p: &ProjParams<'_>) -> Result { + if let Some(m) = p.number("to_meter")? { + return Ok(LinearUnit { + metres_per_unit: positive("to_meter", m)?, + }); + } + match p.text("units")? { + None => Ok(LinearUnit::METRE), + Some(n) => LinearUnit::by_proj_name(n).ok_or_else(|| CrsParseError::invalid("units", n)), + } +} + +fn parse_utm(p: &ProjParams<'_>) -> Result { + let zone_text = p.text("zone")?.ok_or(CrsParseError::Missing { + key: "zone", + method: "utm", + })?; + let zone = parse_number("zone", zone_text)?; + if zone.fract() != 0.0 || !(1.0..=UTM_ZONES).contains(&zone) { + return Err(CrsParseError::invalid("zone", zone_text)); + } + let false_northing = if p.has("south") { + UTM_FALSE_NORTHING_SOUTH + } else { + 0.0 + }; + Ok(ProjectionDef::TransverseMercator(TmParams { + lat0_deg: 0.0, + lon0_deg: zone * UTM_ZONE_WIDTH_DEG + UTM_ZONE0_MERIDIAN_DEG, + k0: UTM_K0, + false_easting: UTM_FALSE_EASTING, + false_northing, + })) +} + +fn parse_tmerc(p: &ProjParams<'_>) -> Result { + Ok(ProjectionDef::TransverseMercator(TmParams { + // PROJ accepts a pole as the TM origin latitude, as EPSG:2081 uses. + lat0_deg: closed_latitude("lat_0", p.number_or("lat_0", 0.0)?)?, + lon0_deg: p.number_or("lon_0", 0.0)?, + k0: positive("k", p.number_any(&["k", "k_0"])?.unwrap_or(1.0))?, + false_easting: p.number_or("x_0", 0.0)?, + false_northing: p.number_or("y_0", 0.0)?, + })) +} + +fn parse_omerc(p: &ProjParams<'_>, ellipsoid: &Ellipsoid) -> Result { + let lat_c_deg = open_latitude("lat_0", p.number_or("lat_0", 0.0)?)?; + let (alpha_c_deg, gamma_c_deg) = match (p.number("alpha")?, p.number("gamma")?) { + (Some(alpha), Some(gamma)) => (alpha, gamma), + (Some(alpha), None) => (alpha, gamma_from_alpha(lat_c_deg, alpha, ellipsoid)), + (None, Some(gamma)) => (alpha_from_gamma(lat_c_deg, gamma, ellipsoid), gamma), + (None, None) => { + return Err(CrsParseError::Missing { + key: "alpha", + method: "omerc", + }); + } + }; + if !alpha_c_deg.is_finite() || !gamma_c_deg.is_finite() { + return Err(CrsParseError::invalid("alpha", &alpha_c_deg.to_string())); + } + let variant = if p.has("no_uoff") || p.has("no_off") { + HomVariant::A + } else { + HomVariant::B + }; + Ok(ProjectionDef::HotineObliqueMercator(HomParams { + variant, + lat_c_deg, + lon_c_deg: p.number_or("lonc", 0.0)?, + alpha_c_deg, + gamma_c_deg, + k_c: positive("k", p.number_any(&["k", "k_0"])?.unwrap_or(1.0))?, + false_easting: p.number_or("x_0", 0.0)?, + false_northing: p.number_or("y_0", 0.0)?, + no_rot: p.has("no_rot"), + })) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn parse(text: &str) -> Result { + parse_proj_string(Srid::WGS84, "test", text) + } + + #[test] + fn wgs84_geographic() { + let def = parse("+proj=longlat +datum=WGS84 +no_defs").unwrap(); + assert_eq!(def.kind, CrsKind::Geographic); + assert_eq!(def.datum, Datum::WGS84); + assert!(def.projection.is_none()); + } + + #[test] + fn utm_zone_48_north_and_south() { + let def = parse("+proj=utm +zone=48 +datum=WGS84 +units=m +no_defs").unwrap(); + let Some(ProjectionDef::TransverseMercator(tm)) = def.projection else { + panic!("expected tmerc"); + }; + assert_eq!(tm.lon0_deg, 105.0); + assert_eq!(tm.false_northing, 0.0); + let def = parse("+proj=utm +zone=1 +south +ellps=WGS84").unwrap(); + let Some(ProjectionDef::TransverseMercator(tm)) = def.projection else { + panic!("expected tmerc"); + }; + assert_eq!(tm.lon0_deg, -177.0); + assert_eq!(tm.false_northing, 10_000_000.0); + assert!(parse("+proj=utm +zone=61").is_err()); + assert!(parse("+proj=utm +zone=4.5").is_err()); + assert_eq!( + parse("+proj=utm"), + Err(CrsParseError::Missing { + key: "zone", + method: "utm" + }) + ); + } + + #[test] + fn rso_omerc_matches_epsg_3375() { + let def = parse( + "+proj=omerc +no_uoff +lat_0=4 +lonc=102.25 +alpha=323.025796466667 \ + +gamma=323.130102361111 +k=0.99984 +x_0=804671 +y_0=0 +ellps=GRS80 +units=m +no_defs", + ) + .unwrap(); + let Some(ProjectionDef::HotineObliqueMercator(hom)) = def.projection else { + panic!("expected omerc"); + }; + assert_eq!(hom.variant, HomVariant::A); + assert_eq!(hom.k_c, 0.99984); + assert_eq!(def.datum.ellipsoid, Ellipsoid::GRS80); + } + + #[test] + fn omerc_without_gamma_uses_gamma0() { + let def = parse("+proj=omerc +lat_0=4 +lonc=115 +alpha=53.31580995 +k=0.99984").unwrap(); + let Some(ProjectionDef::HotineObliqueMercator(hom)) = def.projection else { + panic!("expected omerc"); + }; + assert_eq!(hom.variant, HomVariant::B); + assert!( + (hom.gamma_c_deg - 53.13).abs() < 0.01, + "{}", + hom.gamma_c_deg + ); + assert!(parse("+proj=omerc +lat_0=4").is_err()); + } + + #[test] + fn towgs84_and_ellipsoid_forms() { + let def = parse("+proj=longlat +ellps=evrst48 +towgs84=-11,851,5 +no_defs").unwrap(); + assert_eq!(def.datum.ellipsoid, Ellipsoid::EVEREST_1830_MODIFIED); + assert_eq!( + def.datum.to_wgs84, + DatumShift::Helmert(Helmert::translation(-11.0, 851.0, 5.0)) + ); + assert!(parse("+proj=longlat +towgs84=1,2").is_err()); + assert!(parse("+proj=longlat +towgs84=1,2,3,4,5,6,7,8").is_err()); + let def = parse("+proj=longlat +a=6378137 +rf=298.257223563").unwrap(); + assert_eq!(def.datum.ellipsoid, Ellipsoid::WGS84); + let def = parse("+proj=longlat +R=6371007.181").unwrap(); + assert!(def.datum.ellipsoid.is_sphere()); + let def = parse("+proj=longlat +a=6378206.4 +b=6356583.8").unwrap(); + assert_eq!(def.datum.ellipsoid, Ellipsoid::CLARKE_1866); + let def = parse("+proj=tmerc").unwrap(); + assert_eq!(def.datum.ellipsoid, Ellipsoid::GRS80); + } + + #[test] + fn units_pm_axis_type() { + let def = parse("+proj=tmerc +units=us-ft +pm=paris +axis=enu +type=crs").unwrap(); + assert_eq!(def.unit.metres_per_unit, 1200.0 / 3937.0); + assert!((def.datum.prime_meridian_deg - 2.337_229_166_667).abs() < 1e-11); + let def = parse("+proj=tmerc +units=ft +to_meter=0.3047972654").unwrap(); + assert_eq!(def.unit.metres_per_unit, 0.304_797_265_4); + assert_eq!(def.axis, AxisOrder::ENU); + let def = parse("+proj=tmerc +axis=wsu").unwrap(); + assert_eq!(def.axis.to_string(), "wsu"); + let def = parse("+proj=longlat +axis=neu").unwrap(); + assert_eq!(def.axis.to_string(), "neu"); + for bad in [ + "+proj=tmerc +axis=ewu", + "+proj=tmerc +axis=en", + "+proj=tmerc +axis", + ] { + assert!( + matches!(parse(bad), Err(CrsParseError::InvalidValue { .. })), + "{bad}" + ); + } + assert!(parse("+proj=tmerc +units=furlong").is_err()); + } + + #[test] + fn unsupported_method_lists_implemented() { + let err = parse("+proj=bonne +lat_1=10").unwrap_err(); + assert_eq!( + err, + CrsParseError::UnsupportedMethod { + method: "bonne".into(), + implemented: IMPLEMENTED_METHODS + } + ); + assert!(matches!( + parse("+proj=tmerc +nadgrids=@null"), + Err(CrsParseError::UnknownParameter { .. }) + )); + } + + #[test] + fn every_method_parses() { + let cases = [ + "+proj=lcc +lat_1=33 +lat_2=45 +lat_0=23 +lon_0=-96", + "+proj=merc +lat_ts=10", + "+proj=webmerc +datum=WGS84", + "+proj=cea +lat_ts=30 +ellps=WGS84", + "+proj=eqc +lat_ts=0", + "+proj=stere +lat_0=90 +lat_ts=70", + "+proj=ups +south +ellps=WGS84", + "+proj=sterea +lat_0=52.15 +lon_0=5.38 +k=0.9999079", + "+proj=aea +lat_1=29.5 +lat_2=45.5", + "+proj=laea +lat_0=52 +lon_0=10", + "+proj=cass +lat_0=4 +lon_0=101 +hyperbolic", + "+proj=poly +lon_0=-54", + "+proj=aeqd +guam +lat_0=13.47 +lon_0=144.75", + "+proj=sinu +R=6371007.181", + "+proj=eqearth", + "+proj=krovak +czech +ellps=bessel", + "+proj=mod_krovak +ellps=bessel", + "+proj=somerc +lat_0=46.95 +lon_0=7.44 +k_0=1", + "+proj=nzmg +ellps=intl", + "+proj=geos +h=35785831 +sweep=x", + "+proj=labrd +lat_0=-18.9 +lon_0=46.43 +azi=18.9 +k=0.9995", + ]; + for text in cases { + let def = parse(text).unwrap_or_else(|e| panic!("{text}: {e}")); + assert_eq!(def.kind, CrsKind::Projected, "{text}"); + } + let Some(ProjectionDef::Krovak(k)) = parse("+proj=mod_krovak").unwrap().projection else { + panic!("expected krovak"); + }; + assert!(k.modified); + } + + #[test] + fn ups_refuses_a_sphere() { + assert_eq!( + parse("+proj=ups +R=6371000"), + Err(CrsParseError::invalid("proj", "ups")) + ); + let Some(ProjectionDef::Stereographic(s)) = parse("+proj=ups +south").unwrap().projection + else { + panic!("expected stere"); + }; + assert_eq!(s.lat0_deg, -90.0); + } +} diff --git a/nodedb-crs/src/parse/proj_writer.rs b/nodedb-crs/src/parse/proj_writer.rs new file mode 100644 index 000000000..86d89b074 --- /dev/null +++ b/nodedb-crs/src/parse/proj_writer.rs @@ -0,0 +1,194 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! PROJ-string writer. +//! +//! Numbers print in Rust's shortest round-trip form, so +//! [`parse_proj_string`](super::parse_proj_string) reads back the same +//! values. PROJ strings carry neither the Helmert rotation convention nor +//! its accuracy: the writer emits position-vector rotations, and the +//! accuracy and area of use are dropped. +//! +//! `+towgs84` has no rates, so a definition with a time-dependent Helmert +//! has no PROJ string: the writer refuses it with +//! [`ProjExportError::TimeDependentShift`], as PROJ refuses the same export. +//! WKT2 and PROJJSON keep the rates and the epoch. + +use core::fmt::Write; + +use crate::def::{CrsDef, CrsKind, DatumShift, Ellipsoid, LinearUnit}; +use crate::error::ProjExportError; + +impl CrsDef { + /// This definition as a PROJ string. The EPSG datum code is dropped: + /// PROJ strings do not carry it. A time-dependent datum shift fails with + /// [`ProjExportError::TimeDependentShift`]. + pub fn to_proj_string(&self) -> Result { + // Every parameter is written with a leading space, the first one's + // stripped at the end. + let mut out = String::new(); + match &self.projection { + None => out.push_str(" +proj=longlat"), + Some(p) => p.write_proj(&mut out), + } + if !self.axis.is_enu() { + push(&mut out, format_args!(" +axis={}", self.axis)); + } + write_ellipsoid(&mut out, &self.datum.ellipsoid); + if self.datum.prime_meridian_deg != 0.0 { + push( + &mut out, + format_args!(" +pm={}", self.datum.prime_meridian_deg), + ); + } + match &self.datum.to_wgs84 { + DatumShift::Identity => {} + DatumShift::Helmert(h) if h.rates.is_some() => { + return Err(ProjExportError::TimeDependentShift { srid: self.srid }); + } + DatumShift::Helmert(h) => { + let [tx, ty, tz, rx, ry, rz, s] = h.to_towgs84(); + push( + &mut out, + format_args!(" +towgs84={tx},{ty},{tz},{rx},{ry},{rz},{s}"), + ); + } + } + match self.kind { + CrsKind::Geographic => {} + CrsKind::Projected => write_unit(&mut out, &self.unit), + } + out.push_str(" +no_defs"); + Ok(out.split_off(1)) + } +} + +/// Append formatted text. Writing to a `String` cannot fail, so the +/// `fmt::Result` carries nothing. +fn push(out: &mut String, args: core::fmt::Arguments<'_>) { + let _ = out.write_fmt(args); +} + +fn write_ellipsoid(out: &mut String, e: &Ellipsoid) { + match e.proj_name() { + Some(name) => push(out, format_args!(" +ellps={name}")), + None if e.is_sphere() => push(out, format_args!(" +R={}", e.a)), + None => push(out, format_args!(" +a={} +rf={}", e.a, e.inv_f)), + } +} + +fn write_unit(out: &mut String, unit: &LinearUnit) { + match unit.proj_name() { + Some(name) => push(out, format_args!(" +units={name}")), + None => push(out, format_args!(" +to_meter={}", unit.metres_per_unit)), + } +} + +#[cfg(test)] +mod tests { + use crate::def::{CrsDef, Datum, DatumShift, Ellipsoid, Helmert}; + use crate::parse::parse_proj_string; + use crate::srid::Srid; + + fn round_trip(text: &str) { + let srid = Srid::new(910_000).unwrap(); + let def = parse_proj_string(srid, "rt", text).unwrap(); + let written = def.to_proj_string().unwrap(); + let again = parse_proj_string(srid, "rt", &written).unwrap(); + assert_eq!(again, def, "{text} -> {written}"); + } + + #[test] + fn round_trips() { + round_trip("+proj=longlat +datum=WGS84 +no_defs"); + round_trip("+proj=utm +zone=48 +datum=WGS84 +units=m +no_defs"); + round_trip("+proj=utm +zone=50 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs"); + round_trip( + "+proj=omerc +no_uoff +lat_0=4 +lonc=102.25 +alpha=323.025796466667 \ + +gamma=323.130102361111 +k=0.99984 +x_0=804671 +y_0=0 +ellps=GRS80 +units=m +no_defs", + ); + round_trip("+proj=omerc +lat_0=4 +lonc=115 +alpha=53.31580995 +k=0.99984 +no_rot"); + round_trip( + "+proj=tmerc +lat_0=4 +lon_0=102 +k=0.99995 +x_0=40000 +y_0=-10000 \ + +a=6377304.063 +rf=300.8017 +pm=paris +towgs84=-11,851,5,0.1,-0.2,0.3,1.5 \ + +to_meter=0.3047972654", + ); + round_trip("+proj=longlat +R=6371007.181 +no_defs"); + round_trip("+proj=tmerc +a=6378000 +rf=297.5 +units=us-ft"); + round_trip("+proj=tmerc +axis=wsu +lon_0=17 +ellps=WGS84"); + round_trip("+proj=longlat +axis=neu +datum=WGS84"); + } + + #[test] + fn every_method_round_trips() { + for text in [ + "+proj=lcc +lat_1=33 +lat_2=45 +lat_0=23 +lon_0=-96 +x_0=10 +y_0=-20", + "+proj=lcc +axis=wnu +lat_1=65 +lat_0=65 +lon_0=-18 +k_0=1 +x_0=-500000 +ellps=intl", + "+proj=merc +lat_ts=10 +lon_0=3", + "+proj=merc +k=0.997 +lon_0=110", + "+proj=webmerc +datum=WGS84", + "+proj=cea +lat_ts=30 +ellps=WGS84", + "+proj=cea +k_0=1 +R=6371000", + "+proj=eqc +lat_ts=12 +lat_0=1", + "+proj=stere +lat_0=90 +lat_ts=70 +lon_0=-45", + "+proj=stere +lat_0=52 +lon_0=5 +k=0.9999", + "+proj=ups +south +ellps=WGS84", + "+proj=sterea +lat_0=52.15 +lon_0=5.38 +k=0.9999079 +x_0=155000 +y_0=463000", + "+proj=aea +lat_1=29.5 +lat_2=45.5 +lat_0=23 +lon_0=-96", + "+proj=laea +lat_0=52 +lon_0=10 +x_0=4321000 +y_0=3210000", + "+proj=cass +lat_0=4 +lon_0=101 +hyperbolic", + "+proj=cass +axis=swu +lat_0=48 +lon_0=31.8 +pm=-17.6666666666667", + "+proj=poly +lat_0=1 +lon_0=-54", + "+proj=aeqd +guam +lat_0=13.47 +lon_0=144.75", + "+proj=aeqd +lat_0=90", + "+proj=sinu +R=6371007.181", + "+proj=eqearth +lon_0=150", + "+proj=krovak +axis=swu +czech +ellps=bessel", + "+proj=mod_krovak +ellps=bessel +x_0=5000000 +y_0=5000000", + "+proj=somerc +lat_0=46.95 +lon_0=7.44 +k_0=1 +x_0=2600000 +y_0=1200000", + "+proj=nzmg +ellps=intl", + "+proj=geos +h=35785831 +sweep=x +lon_0=-75", + "+proj=labrd +lat_0=-18.9 +lon_0=46.43 +azi=18.9 +k=0.9995 +x_0=400000", + ] { + round_trip(text); + } + } + + /// `+towgs84` has no rates, so a time-dependent shift is refused. + #[test] + fn time_dependent_shift_has_no_proj_string() { + let srid = Srid::new(910_000).unwrap(); + let mut def = parse_proj_string(srid, "td", "+proj=longlat +ellps=GRS80").unwrap(); + let mut h = Helmert::position_vector([1.0, 2.0, 3.0, 0.5, 0.0, 0.0, 0.25]); + h.rates = Some(crate::def::HelmertRates::new([0.1; 7], 2010.0)); + def.datum.to_wgs84 = DatumShift::Helmert(h); + assert_eq!( + def.to_proj_string(), + Err(crate::error::ProjExportError::TimeDependentShift { srid }) + ); + } + + #[test] + fn coordinate_frame_writes_position_vector_rotations() { + let mut h = Helmert::position_vector([1.0, 2.0, 3.0, 0.5, 0.0, 0.0, 0.0]); + h.convention = crate::def::RotationConvention::CoordinateFrame; + let def = CrsDef { + srid: Srid::WGS84, + name: "cf".into(), + kind: crate::def::CrsKind::Geographic, + datum: Datum { + ellipsoid: Ellipsoid::INTERNATIONAL_1924, + prime_meridian_deg: 0.0, + to_wgs84: DatumShift::Helmert(h), + epsg_code: Some(6230), + }, + projection: None, + unit: crate::def::LinearUnit::METRE, + axis: crate::def::AxisOrder::ENU, + area_of_use: None, + }; + assert_eq!( + def.to_proj_string().as_deref(), + Ok("+proj=longlat +ellps=intl +towgs84=1,2,3,-0.5,0,0,0 +no_defs") + ); + } +} diff --git a/nodedb-crs/src/parse/projjson.rs b/nodedb-crs/src/parse/projjson.rs new file mode 100644 index 000000000..2235616fe --- /dev/null +++ b/nodedb-crs/src/parse/projjson.rs @@ -0,0 +1,442 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! PROJJSON parser. +//! +//! Reads a `GeographicCRS` (or a `GeodeticCRS` with an ellipsoidal +//! coordinate system), a `ProjectedCRS`, and a `BoundCRS` whose source is +//! one of those and whose transformation to WGS 84 is a static or +//! time-dependent Helmert transformation. A datum may be a `datum` or a +//! `datum_ensemble`. + +use super::helmert_params::{HelmertValues, helmert_slot}; +use super::json::{Json, PROJJSON_FORMAT, parse_json}; +use super::model::{ + ConversionModel, CrsModel, DEGREE_RAD, DatumModel, ParamModel, build_def, convert, +}; +use super::unit_names::{angular_factor, any_factor, linear_factor}; +use super::wkt2::order_from_directions; +use crate::def::{AreaOfUse, AxisDirection, CrsDef, CrsKind, DatumShift, Ellipsoid}; +use crate::error::CrsParseError; +use crate::srid::Srid; + +/// Parse PROJJSON text into a CRS definition named `name` under `srid`. +pub fn parse_projjson(srid: Srid, name: &str, text: &str) -> Result { + let root = parse_json(text)?; + let model = model_of_crs(&root)?; + build_def(srid, name, &model, PROJJSON_FORMAT) +} + +fn invalid(detail: impl Into>) -> CrsParseError { + CrsParseError::content(PROJJSON_FORMAT, detail) +} + +/// A required member of `object`. +fn member<'a>(object: &'a Json, key: &str, owner: &str) -> Result<&'a Json, CrsParseError> { + object + .get(key) + .ok_or_else(|| invalid(format!("{owner} has no \"{key}\""))) +} + +fn type_of(object: &Json) -> &str { + object + .get("type") + .and_then(Json::as_str) + .unwrap_or_default() +} + +fn model_of_crs(crs: &Json) -> Result { + match type_of(crs) { + "BoundCRS" => bound(crs), + "ProjectedCRS" => projected(crs), + "GeographicCRS" | "GeodeticCRS" => geographic(crs), + other => Err(invalid(format!( + "type \"{other}\" is not a supported CRS; supported: GeographicCRS, ProjectedCRS, \ + BoundCRS" + ))), + } +} + +fn bound(crs: &Json) -> Result { + let target = model_of_crs(member(crs, "target_crs", "BoundCRS")?)?; + if target.kind != CrsKind::Geographic || target.datum.ellipsoid != Ellipsoid::WGS84 { + return Err(invalid("BoundCRS must transform to WGS 84")); + } + let mut model = model_of_crs(member(crs, "source_crs", "BoundCRS")?)?; + if model.datum.to_wgs84 != DatumShift::Identity { + return Err(invalid("BoundCRS source is itself bound")); + } + let transformation = member(crs, "transformation", "BoundCRS")?; + let method = member(transformation, "method", "transformation")?; + // A parameter with no unit is in the parameter's own unit. A rate unit + // factor is SI per second, as EPSG and PROJ write it. + let mut values = HelmertValues::default(); + for param in parameters(transformation)? { + let name = param.get("name").and_then(Json::as_str).unwrap_or_default(); + let slot = helmert_slot(epsg_id(param), name) + .ok_or_else(|| invalid(format!("Helmert parameter '{name}' is not supported")))?; + let value = number(member(param, "value", name)?, name)?; + let factor = match param.get("unit") { + None => None, + Some(unit) => Some(unit_factor(unit, any_factor)?), + }; + values.set(slot, value, factor); + } + let accuracy = match transformation.get("accuracy") { + None => None, + Some(Json::Number(n)) => Some(*n), + Some(Json::String(s)) => Some( + s.trim() + .parse::() + .map_err(|_| invalid(format!("accuracy '{s}' is not a number")))?, + ), + Some(Json::Null | Json::Bool(_) | Json::Array(_) | Json::Object(_)) => { + return Err(invalid("accuracy is not a number")); + } + }; + model.datum.to_wgs84 = values.into_shift( + epsg_id(method), + method + .get("name") + .and_then(Json::as_str) + .unwrap_or_default(), + accuracy, + PROJJSON_FORMAT, + )?; + Ok(model) +} + +fn geographic(crs: &Json) -> Result { + let cs = crs.get("coordinate_system"); + if let Some(subtype) = cs.and_then(|c| c.get("subtype")).and_then(Json::as_str) + && subtype != "ellipsoidal" + { + return Err(invalid(format!( + "a {subtype} geodetic CRS is not supported; supported: ellipsoidal" + ))); + } + let angle_rad = axis_unit(cs, angular_factor)?.unwrap_or(DEGREE_RAD); + Ok(CrsModel { + name: crs + .get("name") + .and_then(Json::as_str) + .unwrap_or_default() + .to_string(), + kind: CrsKind::Geographic, + datum: datum(crs, angle_rad)?, + conversion: None, + metres_per_unit: 1.0, + axis: axis_order(cs)?, + area_of_use: area(crs)?, + }) +} + +fn projected(crs: &Json) -> Result { + let base = member(crs, "base_crs", "ProjectedCRS")?; + let angle_rad = axis_unit(base.get("coordinate_system"), angular_factor)?.unwrap_or(DEGREE_RAD); + let cs = crs.get("coordinate_system"); + let metres_per_unit = axis_unit(cs, linear_factor)?.unwrap_or(1.0); + let conversion = member(crs, "conversion", "ProjectedCRS")?; + let method = member(conversion, "method", "conversion")?; + let mut params = Vec::new(); + for param in parameters(conversion)? { + let name = param.get("name").and_then(Json::as_str).unwrap_or_default(); + params.push(ParamModel { + name: name.to_string(), + code: epsg_id(param), + value: number(member(param, "value", name)?, name)?, + factor: param + .get("unit") + .map(|u| unit_factor(u, any_factor)) + .transpose()?, + kind: None, + }); + } + Ok(CrsModel { + name: crs + .get("name") + .and_then(Json::as_str) + .unwrap_or_default() + .to_string(), + kind: CrsKind::Projected, + datum: datum(base, angle_rad)?, + conversion: Some(ConversionModel { + method_name: method + .get("name") + .and_then(Json::as_str) + .unwrap_or_default() + .to_string(), + method_code: epsg_id(method), + params, + default_angle_rad: angle_rad, + default_length_m: metres_per_unit, + }), + metres_per_unit, + axis: axis_order(cs)?, + area_of_use: area(crs)?, + }) +} + +/// The `parameters` array of a conversion or transformation. Absent is +/// empty. +fn parameters(object: &Json) -> Result<&[Json], CrsParseError> { + match object.get("parameters") { + None => Ok(&[]), + Some(p) => p + .as_array() + .ok_or_else(|| invalid("\"parameters\" is not an array")), + } +} + +/// A plain number. +fn number(value: &Json, what: &str) -> Result { + value + .as_f64() + .ok_or_else(|| invalid(format!("'{what}' is not a number"))) +} + +/// A measure that is a number in its default unit, or a +/// `{"value": n, "unit": u}` object. +fn measure( + value: &Json, + what: &str, + factor_of: fn(&str) -> Option, + default_factor: f64, + canonical: f64, +) -> Result { + match value { + Json::Number(n) => Ok(convert(*n, default_factor, canonical)), + Json::Object(_) => { + let n = number(member(value, "value", what)?, what)?; + let factor = match value.get("unit") { + None => default_factor, + Some(unit) => unit_factor(unit, factor_of)?, + }; + Ok(convert(n, factor, canonical)) + } + Json::Null | Json::Bool(_) | Json::String(_) | Json::Array(_) => { + Err(invalid(format!("'{what}' is not a number"))) + } + } +} + +/// The factor of a unit: a name `factor_of` knows, or an object with a +/// `conversion_factor`. +fn unit_factor(unit: &Json, factor_of: fn(&str) -> Option) -> Result { + match unit { + Json::String(name) => { + factor_of(name).ok_or_else(|| invalid(format!("unit '{name}' is not supported"))) + } + Json::Object(_) => unit + .get("conversion_factor") + .and_then(Json::as_f64) + .filter(|f| *f > 0.0) + .ok_or_else(|| invalid("unit has no positive \"conversion_factor\"")), + Json::Null | Json::Bool(_) | Json::Number(_) | Json::Array(_) => { + Err(invalid("unit is neither a name nor an object")) + } + } +} + +/// The unit of a coordinate system's first axis. +fn axis_unit( + cs: Option<&Json>, + factor_of: fn(&str) -> Option, +) -> Result, CrsParseError> { + let Some(unit) = cs + .and_then(|c| c.get("axis")) + .and_then(Json::as_array) + .and_then(<[Json]>::first) + .and_then(|axis| axis.get("unit")) + else { + return Ok(None); + }; + unit_factor(unit, factor_of).map(Some) +} + +/// The datum of a geographic CRS object. +fn datum(crs: &Json, angle_rad: f64) -> Result { + let datum = crs + .get("datum") + .or_else(|| crs.get("datum_ensemble")) + .ok_or_else(|| invalid("geographic CRS has no \"datum\" or \"datum_ensemble\""))?; + let ellipsoid = member(datum, "ellipsoid", "datum")?; + let length = |key: &str| -> Result, CrsParseError> { + ellipsoid + .get(key) + .map(|v| measure(v, key, linear_factor, 1.0, 1.0)) + .transpose() + }; + let ellipsoid = if let Some(r) = length("radius")? { + Ellipsoid::sphere(r) + } else { + let a = length("semi_major_axis")? + .ok_or_else(|| invalid("ellipsoid has no \"semi_major_axis\" or \"radius\""))?; + let inv_f = match ( + ellipsoid.get("inverse_flattening"), + length("semi_minor_axis")?, + ) { + (Some(rf), _) => number(rf, "inverse_flattening")?, + (None, Some(b)) if b == a => 0.0, + (None, Some(b)) if b > 0.0 && b < a => a / (a - b), + (None, Some(_) | None) => { + return Err(invalid( + "ellipsoid needs \"inverse_flattening\" or a smaller \"semi_minor_axis\"", + )); + } + }; + Ellipsoid { a, inv_f } + }; + if ellipsoid.a <= 0.0 || ellipsoid.inv_f < 0.0 { + return Err(invalid("ellipsoid axes are not valid")); + } + let prime_meridian_deg = match datum + .get("prime_meridian") + .or_else(|| crs.get("prime_meridian")) + .and_then(|pm| pm.get("longitude")) + { + None => 0.0, + Some(lon) => measure(lon, "longitude", angular_factor, angle_rad, DEGREE_RAD)?, + }; + Ok(DatumModel { + ellipsoid, + prime_meridian_deg, + epsg_code: epsg_id(datum), + to_wgs84: DatumShift::Identity, + }) +} + +/// The EPSG code of an object's `id`, or of the first of its `ids`. +fn epsg_id(object: &Json) -> Option { + let id = object + .get("id") + .or_else(|| object.get("ids").and_then(Json::as_array)?.first())?; + if !id.get("authority")?.as_str()?.eq_ignore_ascii_case("EPSG") { + return None; + } + match id.get("code")? { + Json::Number(n) if n.fract() == 0.0 && *n >= 0.0 && *n <= f64::from(u32::MAX) => { + Some(*n as u32) + } + Json::String(s) => s.trim().parse().ok(), + Json::Number(_) | Json::Null | Json::Bool(_) | Json::Array(_) | Json::Object(_) => None, + } +} + +/// The axis order of a coordinate system. +fn axis_order(cs: Option<&Json>) -> Result { + let axes = cs + .and_then(|c| c.get("axis")) + .and_then(Json::as_array) + .unwrap_or_default(); + let mut directions = Vec::new(); + for axis in axes.iter().take(2) { + let word = axis + .get("direction") + .and_then(Json::as_str) + .ok_or_else(|| invalid("axis has no \"direction\""))?; + directions.push(match word { + "east" => AxisDirection::East, + "west" => AxisDirection::West, + "north" => AxisDirection::North, + "south" => AxisDirection::South, + other => { + return Err(invalid(format!( + "axis direction '{other}' is not supported; supported: east, west, north, south" + ))); + } + }); + } + order_from_directions(&directions) +} + +/// The area of use: `bbox`, or the `bbox` of the first usage that has +/// one. +fn area(crs: &Json) -> Result, CrsParseError> { + let bbox = crs.get("bbox").or_else(|| { + crs.get("usages")? + .as_array()? + .iter() + .find_map(|u| u.get("bbox")) + }); + let Some(bbox) = bbox else { + return Ok(None); + }; + let side = |key: &str| number(member(bbox, key, "bbox")?, key); + Ok(Some(AreaOfUse { + south: side("south_latitude")?, + west: side("west_longitude")?, + north: side("north_latitude")?, + east: side("east_longitude")?, + })) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn parse(text: &str) -> Result { + parse_projjson(Srid::new(910_001).unwrap(), "test", text) + } + + /// EPSG:3375 GDM2000 / Peninsula RSO as PROJ 9 writes it, trimmed. + const RSO: &str = r#"{ + "type": "ProjectedCRS", "name": "GDM2000 / Peninsula RSO", + "base_crs": {"name": "GDM2000", + "datum": {"type": "GeodeticReferenceFrame", "name": "Geodetic Datum of Malaysia 2000", + "ellipsoid": {"name": "GRS 1980", "semi_major_axis": 6378137, "inverse_flattening": 298.257222101}}, + "coordinate_system": {"subtype": "ellipsoidal", "axis": [ + {"name": "Geodetic latitude", "abbreviation": "Lat", "direction": "north", "unit": "degree"}, + {"name": "Geodetic longitude", "abbreviation": "Lon", "direction": "east", "unit": "degree"}]}, + "id": {"authority": "EPSG", "code": 4742}}, + "conversion": {"name": "Peninsular RSO", + "method": {"name": "Hotine Oblique Mercator (variant A)", "id": {"authority": "EPSG", "code": 9812}}, + "parameters": [ + {"name": "Latitude of projection centre", "value": 4, "unit": "degree", "id": {"authority": "EPSG", "code": 8811}}, + {"name": "Longitude of projection centre", "value": 102.25, "unit": "degree", "id": {"authority": "EPSG", "code": 8812}}, + {"name": "Azimuth at projection centre", "value": 323.025796466667, "unit": "degree", "id": {"authority": "EPSG", "code": 8813}}, + {"name": "Angle from Rectified to Skew Grid", "value": 323.130102361111, "unit": "degree", "id": {"authority": "EPSG", "code": 8814}}, + {"name": "Scale factor at projection centre", "value": 0.99984, "unit": "unity", "id": {"authority": "EPSG", "code": 8815}}, + {"name": "False easting", "value": 804671, "unit": "metre", "id": {"authority": "EPSG", "code": 8806}}, + {"name": "False northing", "value": 0, "unit": "metre", "id": {"authority": "EPSG", "code": 8807}}]}, + "coordinate_system": {"subtype": "Cartesian", "axis": [ + {"name": "Easting", "abbreviation": "E", "direction": "east", "unit": "metre"}, + {"name": "Northing", "abbreviation": "N", "direction": "north", "unit": "metre"}]}, + "bbox": {"south_latitude": 1.13, "west_longitude": 98.02, "north_latitude": 7.81, "east_longitude": 105.82}, + "id": {"authority": "EPSG", "code": 3375}}"#; + + #[test] + fn rso_matches_the_builtin_projection() { + let def = parse(RSO).unwrap(); + let builtin = crate::CrsContext::builtin_only() + .resolve(Srid::new(3375).unwrap()) + .unwrap(); + assert_eq!(def.projection, builtin.projection); + assert_eq!(def.datum.ellipsoid, builtin.datum.ellipsoid); + assert_eq!(def.area_of_use, builtin.area_of_use); + } + + #[test] + fn units_as_objects_and_spheres() { + let text = r#"{"type": "ProjectedCRS", "name": "x", + "base_crs": {"name": "s", "datum": {"name": "d", "ellipsoid": {"name": "e", "radius": 6371007.181}}}, + "conversion": {"name": "c", "method": {"name": "Sinusoidal"}, + "parameters": [{"name": "Longitude of natural origin", "value": 0, "unit": "degree"}]}, + "coordinate_system": {"subtype": "Cartesian", "axis": [ + {"name": "E", "direction": "east", "unit": {"type": "LinearUnit", "name": "US survey foot", "conversion_factor": 0.304800609601219}}, + {"name": "N", "direction": "north", "unit": {"type": "LinearUnit", "name": "US survey foot", "conversion_factor": 0.304800609601219}}]}}"#; + let def = parse(text).unwrap(); + assert!(def.datum.ellipsoid.is_sphere()); + assert_eq!(def.unit.metres_per_unit, 0.304800609601219); + } + + #[test] + fn refusals_name_the_problem() { + assert!(matches!(parse("{"), Err(CrsParseError::Malformed { .. }))); + let err = parse(r#"{"type": "VerticalCRS"}"#).unwrap_err(); + assert!(err.to_string().contains("VerticalCRS"), "{err}"); + let err = + parse(r#"{"type": "GeographicCRS", "datum": {"ellipsoid": {"semi_major_axis": 1}}}"#) + .unwrap_err(); + assert!(err.to_string().contains("inverse_flattening"), "{err}"); + } +} diff --git a/nodedb-crs/src/parse/projjson_writer.rs b/nodedb-crs/src/parse/projjson_writer.rs new file mode 100644 index 000000000..a1fbdc63d --- /dev/null +++ b/nodedb-crs/src/parse/projjson_writer.rs @@ -0,0 +1,328 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! PROJJSON writer (schema v0.7). +//! +//! Numbers print in Rust's shortest round-trip form and every parameter +//! names its unit, so [`parse_projjson`](super::parse_projjson) reads back +//! the same definition. A datum with a Helmert shift is written as a +//! `BoundCRS` to WGS 84. A time-dependent shift adds its rates and its +//! parameter reference epoch, each with a unit object carrying the EPSG +//! per-second factor. A PROJ parameter no EPSG parameter names is +//! written as `{"name": "PROJ ", "value": v}`, which this crate reads +//! back. + +use core::fmt::Write; + +use super::helmert_params::{HelmertUnit, writer_method, writer_params}; +use super::json::push_json_string; +use super::methods::ParamKind; +use super::model::{ConversionModel, CrsModel, DatumModel, model_of}; +use super::unit_names::{ellipsoid_name, linear_name}; +use crate::def::{AxisDirection, CrsDef, CrsKind, DatumShift}; +use crate::srid::SridAuthority; + +/// The PROJJSON schema this writer follows. +const SCHEMA: &str = "https://proj.org/schemas/v0.7/projjson.schema.json"; + +/// WGS 84 as a target CRS. +const WGS84_JSON: &str = "{\"type\":\"GeographicCRS\",\"name\":\"WGS 84\",\ + \"datum\":{\"type\":\"GeodeticReferenceFrame\",\"name\":\"World Geodetic System 1984\",\ + \"ellipsoid\":{\"name\":\"WGS 84\",\"semi_major_axis\":6378137,\ + \"inverse_flattening\":298.257223563},\"id\":{\"authority\":\"EPSG\",\"code\":6326}},\ + \"coordinate_system\":{\"subtype\":\"ellipsoidal\",\"axis\":[\ + {\"name\":\"Geodetic longitude\",\"abbreviation\":\"Lon\",\"direction\":\"east\",\"unit\":\"degree\"},\ + {\"name\":\"Geodetic latitude\",\"abbreviation\":\"Lat\",\"direction\":\"north\",\"unit\":\"degree\"}]},\ + \"id\":{\"authority\":\"EPSG\",\"code\":4326}}"; + +/// Append formatted text. Writing to a `String` cannot fail. +fn push(out: &mut String, args: core::fmt::Arguments<'_>) { + let _ = out.write_fmt(args); +} + +impl CrsDef { + /// This definition as PROJJSON text. + pub fn to_projjson(&self) -> String { + let model = model_of(self); + let id = match self.srid.authority() { + SridAuthority::Epsg => Some(self.srid.code()), + SridAuthority::NodeDb | SridAuthority::User | SridAuthority::Reserved => None, + }; + match &model.datum.to_wgs84 { + DatumShift::Identity => { + let mut out = String::new(); + write_crs(&mut out, &model, Some(SCHEMA), id); + out + } + DatumShift::Helmert(h) => { + let mut out = + format!("{{\"$schema\":\"{SCHEMA}\",\"type\":\"BoundCRS\",\"source_crs\":"); + write_crs(&mut out, &model, None, id); + out.push_str(",\"target_crs\":"); + out.push_str(WGS84_JSON); + let (method, code) = writer_method(h); + push( + &mut out, + format_args!( + ",\"transformation\":{{\"name\":\"Transformation to WGS 84\",\ + \"method\":{{\"name\":\"{method}\",\"id\":{{\"authority\":\"EPSG\",\"code\":{code}}}}},\ + \"parameters\":[" + ), + ); + for (i, (code, name, unit, value)) in writer_params(h).enumerate() { + if i > 0 { + out.push(','); + } + push( + &mut out, + format_args!("{{\"name\":\"{name}\",\"value\":{value},\"unit\":"), + ); + write_helmert_unit(&mut out, unit); + push( + &mut out, + format_args!(",\"id\":{{\"authority\":\"EPSG\",\"code\":{code}}}}}"), + ); + } + out.push(']'); + if let Some(accuracy) = h.accuracy_m { + push(&mut out, format_args!(",\"accuracy\":\"{accuracy}\"")); + } + out.push_str("}}"); + out + } + } + } +} + +/// A Helmert parameter unit: a name PROJ knows for the static units, a unit +/// object with its SI factor for the rates and the year. +fn write_helmert_unit(out: &mut String, unit: HelmertUnit) { + match unit { + HelmertUnit::Length | HelmertUnit::Angle | HelmertUnit::Scale => { + push(out, format_args!("\"{}\"", unit.name())); + } + HelmertUnit::LengthRate + | HelmertUnit::AngleRate + | HelmertUnit::ScaleRate + | HelmertUnit::Time => push( + out, + format_args!( + "{{\"type\":\"{}\",\"name\":\"{}\",\"conversion_factor\":{}}}", + unit.projjson_type(), + unit.name(), + unit.factor() + ), + ), + } +} + +fn write_crs(out: &mut String, model: &CrsModel, schema: Option<&str>, id: Option) { + out.push('{'); + if let Some(schema) = schema { + push(out, format_args!("\"$schema\":\"{schema}\",")); + } + match &model.conversion { + Some(conversion) => { + out.push_str("\"type\":\"ProjectedCRS\",\"name\":"); + push_json_string(out, &model.name); + out.push_str(",\"base_crs\":{\"name\":\"unknown\","); + write_datum(out, &model.datum); + out.push_str(",\"coordinate_system\":{\"subtype\":\"ellipsoidal\",\"axis\":["); + out.push_str("{\"name\":\"Geodetic longitude\",\"abbreviation\":\"Lon\",\"direction\":\"east\",\"unit\":\"degree\"},"); + out.push_str("{\"name\":\"Geodetic latitude\",\"abbreviation\":\"Lat\",\"direction\":\"north\",\"unit\":\"degree\"}]}},"); + write_conversion(out, conversion); + out.push_str(",\"coordinate_system\":{\"subtype\":\"Cartesian\",\"axis\":["); + write_axes(out, model, &linear_unit(model.metres_per_unit)); + out.push_str("]}"); + } + None => { + out.push_str("\"type\":\"GeographicCRS\",\"name\":"); + push_json_string(out, &model.name); + out.push(','); + write_datum(out, &model.datum); + out.push_str(",\"coordinate_system\":{\"subtype\":\"ellipsoidal\",\"axis\":["); + write_axes(out, model, "\"degree\""); + out.push_str("]}"); + } + } + if let Some(area) = &model.area_of_use { + push( + out, + format_args!( + ",\"bbox\":{{\"south_latitude\":{},\"west_longitude\":{},\ + \"north_latitude\":{},\"east_longitude\":{}}}", + area.south, area.west, area.north, area.east + ), + ); + } + if let Some(code) = id { + push( + out, + format_args!(",\"id\":{{\"authority\":\"EPSG\",\"code\":{code}}}"), + ); + } + out.push('}'); +} + +/// The `"datum"` member, with its ellipsoid and prime meridian. +fn write_datum(out: &mut String, datum: &DatumModel) { + out.push_str("\"datum\":{\"type\":\"GeodeticReferenceFrame\",\"name\":\"unknown\",\"ellipsoid\":{\"name\":"); + push_json_string(out, ellipsoid_name(&datum.ellipsoid)); + if datum.ellipsoid.is_sphere() { + push(out, format_args!(",\"radius\":{}}}", datum.ellipsoid.a)); + } else { + push( + out, + format_args!( + ",\"semi_major_axis\":{},\"inverse_flattening\":{}}}", + datum.ellipsoid.a, datum.ellipsoid.inv_f + ), + ); + } + if datum.prime_meridian_deg != 0.0 { + push( + out, + format_args!( + ",\"prime_meridian\":{{\"name\":\"unknown\",\"longitude\":{}}}", + datum.prime_meridian_deg + ), + ); + } + if let Some(code) = datum.epsg_code { + push( + out, + format_args!(",\"id\":{{\"authority\":\"EPSG\",\"code\":{code}}}"), + ); + } + out.push('}'); +} + +fn write_conversion(out: &mut String, conversion: &ConversionModel) { + out.push_str("\"conversion\":{\"name\":\"unknown\",\"method\":{\"name\":"); + push_json_string(out, &conversion.method_name); + if let Some(code) = conversion.method_code { + push( + out, + format_args!(",\"id\":{{\"authority\":\"EPSG\",\"code\":{code}}}"), + ); + } + out.push_str("},\"parameters\":["); + for (i, param) in conversion.params.iter().enumerate() { + if i > 0 { + out.push(','); + } + out.push_str("{\"name\":"); + push_json_string(out, ¶m.name); + push(out, format_args!(",\"value\":{}", param.value)); + let unit = match param.kind { + Some(ParamKind::Angle) => Some("degree"), + Some(ParamKind::Length) => Some("metre"), + Some(ParamKind::Scale) => Some("unity"), + None => None, + }; + if let Some(unit) = unit { + push(out, format_args!(",\"unit\":\"{unit}\"")); + } + if let Some(code) = param.code { + push( + out, + format_args!(",\"id\":{{\"authority\":\"EPSG\",\"code\":{code}}}"), + ); + } + out.push('}'); + } + out.push_str("]}"); +} + +/// A linear unit: its name when it is the metre, else an object with its +/// conversion factor. +fn linear_unit(metres_per_unit: f64) -> String { + if metres_per_unit == 1.0 { + return "\"metre\"".to_string(); + } + let mut out = String::from("{\"type\":\"LinearUnit\",\"name\":"); + push_json_string(&mut out, linear_name(metres_per_unit).unwrap_or("unknown")); + push( + &mut out, + format_args!(",\"conversion_factor\":{metres_per_unit}}}"), + ); + out +} + +/// Two axis objects in the model's axis order, each with `unit`. +fn write_axes(out: &mut String, model: &CrsModel, unit: &str) { + let geographic = model.kind == CrsKind::Geographic; + for (i, direction) in [model.axis.x, model.axis.y].into_iter().enumerate() { + if i > 0 { + out.push(','); + } + let (name, abbreviation, word) = axis_name(direction, geographic); + push( + out, + format_args!( + "{{\"name\":\"{name}\",\"abbreviation\":\"{abbreviation}\",\ + \"direction\":\"{word}\",\"unit\":{unit}}}" + ), + ); + } +} + +/// The name, abbreviation and direction of one axis. +fn axis_name( + direction: AxisDirection, + geographic: bool, +) -> (&'static str, &'static str, &'static str) { + match (direction, geographic) { + (AxisDirection::East, true) => ("Geodetic longitude", "Lon", "east"), + (AxisDirection::West, true) => ("Geodetic longitude", "Lon", "west"), + (AxisDirection::North, true) => ("Geodetic latitude", "Lat", "north"), + (AxisDirection::South, true) => ("Geodetic latitude", "Lat", "south"), + (AxisDirection::East, false) => ("Easting", "X", "east"), + (AxisDirection::West, false) => ("Westing", "X", "west"), + (AxisDirection::North, false) => ("Northing", "Y", "north"), + (AxisDirection::South, false) => ("Southing", "Y", "south"), + (AxisDirection::Up, _) => ("Height", "h", "up"), + (AxisDirection::Down, _) => ("Depth", "d", "down"), + } +} + +#[cfg(test)] +mod tests { + use crate::CrsContext; + use crate::parse::{parse_proj_string, parse_projjson}; + use crate::srid::Srid; + + #[test] + fn every_method_round_trips() { + let srid = Srid::new(910_000).unwrap(); + for text in crate::parse::round_trip_cases::CASES { + let def = parse_proj_string(srid, "rt", text).unwrap(); + let json = def.to_projjson(); + let back = + parse_projjson(srid, "rt", &json).unwrap_or_else(|e| panic!("{text}\n{json}\n{e}")); + assert_eq!(back, def, "{text}\n{json}"); + } + } + + #[test] + fn builtins_round_trip_with_datum_code_and_area() { + let ctx = CrsContext::builtin_only(); + for code in [4326, 4742, 3375, 32648, 2263, 3857, 5514, 990_001, 990_002] { + let srid = Srid::new(code).unwrap(); + let def = ctx.resolve(srid).unwrap(); + let json = def.to_projjson(); + let back = + parse_projjson(srid, &def.name, &json).unwrap_or_else(|e| panic!("{json}\n{e}")); + assert_eq!(back, *def, "{json}"); + } + } + + #[test] + fn names_are_escaped() { + let srid = Srid::new(910_000).unwrap(); + let def = + parse_proj_string(srid, "a \"quoted\" name", "+proj=longlat +datum=WGS84").unwrap(); + let json = def.to_projjson(); + assert!(json.contains(r#""name":"a \"quoted\" name""#), "{json}"); + assert!(parse_projjson(srid, "x", &json).is_ok()); + } +} diff --git a/nodedb-crs/src/parse/round_trip_cases.rs b/nodedb-crs/src/parse/round_trip_cases.rs new file mode 100644 index 000000000..15762bd83 --- /dev/null +++ b/nodedb-crs/src/parse/round_trip_cases.rs @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! PROJ strings that cover every method, flag, unit, axis order and datum +//! form, for the writer round-trip tests. + +pub(crate) const CASES: &[&str] = &[ + "+proj=longlat +datum=WGS84 +no_defs", + "+proj=utm +zone=48 +datum=WGS84 +units=m +no_defs", + "+proj=utm +zone=50 +south +ellps=GRS80 +towgs84=0,0,0 +units=m +no_defs", + "+proj=omerc +no_uoff +lat_0=4 +lonc=102.25 +alpha=323.025796466667 \ + +gamma=323.130102361111 +k=0.99984 +x_0=804671 +y_0=0 +ellps=GRS80 +units=m +no_defs", + "+proj=omerc +lat_0=4 +lonc=115 +alpha=53.31580995 +k=0.99984 +no_rot", + "+proj=tmerc +lat_0=4 +lon_0=102 +k=0.99995 +x_0=40000 +y_0=-10000 \ + +a=6377304.063 +rf=300.8017 +pm=paris +towgs84=-11,851,5,0.1,-0.2,0.3,1.5 \ + +to_meter=0.3047972654", + "+proj=longlat +R=6371007.181 +no_defs", + "+proj=tmerc +a=6378000 +rf=297.5 +units=us-ft", + "+proj=tmerc +axis=wsu +lon_0=17 +ellps=WGS84", + "+proj=longlat +axis=wnu +datum=WGS84", + "+proj=lcc +lat_1=33 +lat_2=45 +lat_0=23 +lon_0=-96 +x_0=10 +y_0=-20", + "+proj=lcc +axis=wnu +lat_1=65 +lat_0=65 +lon_0=-18 +k_0=1 +x_0=-500000 +ellps=intl", + "+proj=merc +lat_ts=10 +lon_0=3", + "+proj=merc +k=0.997 +lon_0=110", + "+proj=webmerc +datum=WGS84", + "+proj=cea +lat_ts=30 +ellps=WGS84", + "+proj=cea +k_0=1 +R=6371000", + "+proj=eqc +lat_ts=12 +lat_0=1", + "+proj=stere +lat_0=90 +lat_ts=70 +lon_0=-45", + "+proj=stere +lat_0=-90 +lon_0=0 +k=0.994", + "+proj=stere +lat_0=52 +lon_0=5 +k=0.9999", + "+proj=ups +south +ellps=WGS84", + "+proj=sterea +lat_0=52.15 +lon_0=5.38 +k=0.9999079 +x_0=155000 +y_0=463000", + "+proj=aea +lat_1=29.5 +lat_2=45.5 +lat_0=23 +lon_0=-96", + "+proj=laea +lat_0=52 +lon_0=10 +x_0=4321000 +y_0=3210000", + "+proj=cass +lat_0=4 +lon_0=101 +hyperbolic", + "+proj=cass +axis=swu +lat_0=48 +lon_0=31.8 +pm=-17.6666666666667", + "+proj=poly +lat_0=1 +lon_0=-54", + "+proj=aeqd +guam +lat_0=13.47 +lon_0=144.75", + "+proj=aeqd +lat_0=90", + "+proj=sinu +R=6371007.181", + "+proj=eqearth +lon_0=150", + "+proj=krovak +axis=swu +czech +ellps=bessel", + "+proj=krovak +ellps=bessel", + "+proj=mod_krovak +ellps=bessel +x_0=5000000 +y_0=5000000", + "+proj=mod_krovak +czech +ellps=bessel", + "+proj=somerc +lat_0=46.95 +lon_0=7.44 +k_0=1 +x_0=2600000 +y_0=1200000", + "+proj=nzmg +ellps=intl", + "+proj=geos +h=35785831 +sweep=x +lon_0=-75", + "+proj=geos +h=35785863 +lon_0=140.7 +ellps=WGS84", + "+proj=labrd +lat_0=-18.9 +lon_0=46.43 +azi=18.9 +k=0.9995 +x_0=400000", +]; diff --git a/nodedb-crs/src/parse/tokens.rs b/nodedb-crs/src/parse/tokens.rs new file mode 100644 index 000000000..604b7cea2 --- /dev/null +++ b/nodedb-crs/src/parse/tokens.rs @@ -0,0 +1,203 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Tokenizing a PROJ string into `+key[=value]` parameters. + +use crate::error::CrsParseError; + +/// Format name reported by [`CrsParseError::Malformed`]. +pub const PROJ_FORMAT: &str = "PROJ"; + +/// Every key the parser understands. Any other key is refused. +const KNOWN_KEYS: &[&str] = &[ + "proj", + "zone", + "south", + "lat_0", + "lon_0", + "lat_1", + "lat_2", + "lat_ts", + "lonc", + "alpha", + "gamma", + "k", + "k_0", + "x_0", + "y_0", + "h", + "ellps", + "a", + "b", + "rf", + "f", + "R", + "datum", + "towgs84", + "pm", + "units", + "to_meter", + "axis", + "no_defs", + "type", + "no_uoff", + "no_off", + "no_rot", + "azi", + "sweep", + "czech", + "guam", + "hyperbolic", +]; + +/// One `+key[=value]` token. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct Param<'a> { + key: &'a str, + value: Option<&'a str>, +} + +/// The parameters of one PROJ string. The first occurrence of a key wins, +/// as in PROJ. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ProjParams<'a> { + params: Vec>, +} + +impl<'a> ProjParams<'a> { + /// Split `text` into parameters and refuse unknown keys. + pub fn tokenize(text: &'a str) -> Result { + let mut params = Vec::new(); + let mut rest = text; + loop { + let trimmed = rest.trim_start(); + if trimmed.is_empty() { + break; + } + let offset = text.len() - trimmed.len(); + let end = trimmed.find(char::is_whitespace).unwrap_or(trimmed.len()); + let token = &trimmed[..end]; + rest = &trimmed[end..]; + let body = token + .strip_prefix('+') + .filter(|b| !b.is_empty() && !b.starts_with('=')) + .ok_or(CrsParseError::Malformed { + format: PROJ_FORMAT, + offset, + })?; + let (key, value) = match body.split_once('=') { + Some((k, v)) => (k, Some(v)), + None => (body, None), + }; + if !KNOWN_KEYS.contains(&key) { + return Err(CrsParseError::UnknownParameter { key: key.into() }); + } + params.push(Param { key, value }); + } + Ok(Self { params }) + } + + fn find(&self, key: &str) -> Option<&Param<'a>> { + self.params.iter().find(|p| p.key == key) + } + + /// Whether `key` is present, with or without a value. + pub fn has(&self, key: &str) -> bool { + self.find(key).is_some() + } + + /// The text value of `key`. A bare flag has no value and reads as + /// `InvalidValue`. + pub fn text(&self, key: &str) -> Result, CrsParseError> { + match self.find(key) { + None => Ok(None), + Some(Param { value: Some(v), .. }) => Ok(Some(*v)), + Some(Param { value: None, .. }) => Err(CrsParseError::invalid(key, "")), + } + } + + /// The numeric value of `key`. + pub fn number(&self, key: &str) -> Result, CrsParseError> { + match self.text(key)? { + None => Ok(None), + Some(v) => parse_number(key, v).map(Some), + } + } + + /// The numeric value of `key`, or `default` when absent. + pub fn number_or(&self, key: &str, default: f64) -> Result { + Ok(self.number(key)?.unwrap_or(default)) + } + + /// The first present key among `keys`, as a number. + pub fn number_any(&self, keys: &[&str]) -> Result, CrsParseError> { + for key in keys { + if let Some(v) = self.number(key)? { + return Ok(Some(v)); + } + } + Ok(None) + } +} + +/// Parse a finite decimal number, or a fraction `a/b` as PROJ accepts for +/// `+to_meter`. +pub fn parse_number(key: &str, text: &str) -> Result { + let bad = || CrsParseError::invalid(key, text); + let value = match text.split_once('/') { + Some((num, den)) => { + let num: f64 = num.parse().map_err(|_| bad())?; + let den: f64 = den.parse().map_err(|_| bad())?; + num / den + } + None => text.parse().map_err(|_| bad())?, + }; + if value.is_finite() { + Ok(value) + } else { + Err(bad()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn splits_flags_and_values() { + let p = ProjParams::tokenize(" +proj=utm +zone=48 +south +no_defs").unwrap(); + assert_eq!(p.text("proj"), Ok(Some("utm"))); + assert_eq!(p.number("zone"), Ok(Some(48.0))); + assert!(p.has("south")); + assert!(!p.has("lat_0")); + assert!(p.text("south").is_err()); + } + + #[test] + fn first_occurrence_wins() { + let p = ProjParams::tokenize("+proj=tmerc +k=1 +k=2").unwrap(); + assert_eq!(p.number("k"), Ok(Some(1.0))); + } + + #[test] + fn refuses_unknown_and_malformed() { + assert_eq!( + ProjParams::tokenize("+proj=tmerc +wat=1"), + Err(CrsParseError::UnknownParameter { key: "wat".into() }) + ); + assert_eq!( + ProjParams::tokenize("+proj=tmerc lat_0=1"), + Err(CrsParseError::Malformed { + format: PROJ_FORMAT, + offset: 12 + }) + ); + assert!(ProjParams::tokenize("+ +proj=tmerc").is_err()); + } + + #[test] + fn numbers_and_fractions() { + assert_eq!(parse_number("to_meter", "1200/3937"), Ok(1200.0 / 3937.0)); + assert!(parse_number("k", "abc").is_err()); + assert!(parse_number("k", "inf").is_err()); + } +} diff --git a/nodedb-crs/src/parse/unit_names.rs b/nodedb-crs/src/parse/unit_names.rs new file mode 100644 index 000000000..e1a8869e1 --- /dev/null +++ b/nodedb-crs/src/parse/unit_names.rs @@ -0,0 +1,149 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Unit and ellipsoid names as WKT2 and PROJJSON write them. + +use super::helmert_params::SECONDS_PER_YEAR; +use super::model::{ARC_SECOND_RAD, DEGREE_RAD, PPM}; +use crate::def::Ellipsoid; + +/// Linear units with metres per unit. +const LINEAR: &[(&str, f64)] = &[ + ("metre", 1.0), + ("millimetre", 0.001), + ("kilometre", 1000.0), + ("foot", 0.3048), + ("US survey foot", 1200.0 / 3937.0), + ("Clarke's foot", 0.304_797_265_4), + ("link", 0.201_168), + ("chain", 20.1168), + ("yard", 0.9144), +]; + +/// Angular units with radians per unit. +const ANGULAR: &[(&str, f64)] = &[ + ("degree", DEGREE_RAD), + ("radian", 1.0), + ("grad", core::f64::consts::PI / 200.0), + ("arc-second", ARC_SECOND_RAD), + ("milliarc-second", ARC_SECOND_RAD / 1000.0), + ("arc-minute", DEGREE_RAD / 60.0), + ("microradian", 1e-6), +]; + +/// Scale units as factors. +const SCALE: &[(&str, f64)] = &[ + ("unity", 1.0), + ("parts per million", PPM), + ("parts per billion", PPM / 1000.0), +]; + +/// Rate and time units of a time-dependent Helmert transformation, SI per +/// second for a rate and seconds for the year: EPSG units of measure 1042, +/// 1027, 1043, 1032, 1041, 1030 and 1029. +const RATE: &[(&str, f64)] = &[ + ("metres per year", 1.0 / SECONDS_PER_YEAR), + ("millimetres per year", 0.001 / SECONDS_PER_YEAR), + ("arc-seconds per year", ARC_SECOND_RAD / SECONDS_PER_YEAR), + ( + "milliarc-seconds per year", + ARC_SECOND_RAD / 1000.0 / SECONDS_PER_YEAR, + ), + ("parts per million per year", PPM / SECONDS_PER_YEAR), + ( + "parts per billion per year", + PPM / 1000.0 / SECONDS_PER_YEAR, + ), + ("year", SECONDS_PER_YEAR), +]; + +/// WKT names of the ellipsoids PROJ names, keyed by the PROJ name. +const ELLIPSOIDS: &[(&str, &str)] = &[ + ("WGS84", "WGS 84"), + ("GRS80", "GRS 1980"), + ("WGS72", "WGS 72"), + ("intl", "International 1924"), + ("bessel", "Bessel 1841"), + ("clrk66", "Clarke 1866"), + ("clrk80ign", "Clarke 1880 (IGN)"), + ("krass", "Krassowsky 1940"), + ("airy", "Airy 1830"), + ("mod_airy", "Airy Modified 1849"), + ("evrst30", "Everest 1830 (1937 Adjustment)"), + ("evrstSS", "Everest 1830 (1967 Definition)"), + ("evrst48", "Everest 1830 Modified"), + ("evrst56", "Everest 1830 (1956)"), + ("evrst69", "Everest 1830 (1962)"), + ("aust_SA", "Australian National Spheroid"), + ("GRS67", "GRS 1967"), + ("helmert", "Helmert 1906"), +]; + +/// Metres per unit of the linear unit named `name`, compared without case. +pub(crate) fn linear_factor(name: &str) -> Option { + lookup(LINEAR, name).or(match name.to_ascii_lowercase().as_str() { + "meter" | "m" => Some(1.0), + _ => None, + }) +} + +/// Radians per unit of the angular unit named `name`. +pub(crate) fn angular_factor(name: &str) -> Option { + lookup(ANGULAR, name) +} + +/// The factor of the scale unit named `name`. +pub(crate) fn scale_factor(name: &str) -> Option { + lookup(SCALE, name) +} + +/// The factor of `name` in any unit table. +pub(crate) fn any_factor(name: &str) -> Option { + linear_factor(name) + .or_else(|| angular_factor(name)) + .or_else(|| scale_factor(name)) + .or_else(|| lookup(RATE, name)) +} + +fn lookup(table: &[(&str, f64)], name: &str) -> Option { + table + .iter() + .find(|(n, _)| n.eq_ignore_ascii_case(name)) + .map(|(_, f)| *f) +} + +/// The name of the linear unit of exactly `metres_per_unit`, if it has one. +pub(crate) fn linear_name(metres_per_unit: f64) -> Option<&'static str> { + LINEAR + .iter() + .find(|(_, f)| *f == metres_per_unit) + .map(|(n, _)| *n) +} + +/// The WKT name of `ellipsoid`, else `unknown`. +pub(crate) fn ellipsoid_name(ellipsoid: &Ellipsoid) -> &'static str { + ellipsoid + .proj_name() + .and_then(|proj| ELLIPSOIDS.iter().find(|(p, _)| *p == proj)) + .map_or("unknown", |(_, wkt)| *wkt) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn names_and_factors() { + assert_eq!(linear_factor("US survey foot"), Some(1200.0 / 3937.0)); + assert_eq!(linear_factor("Metre"), Some(1.0)); + assert_eq!(linear_name(0.3048), Some("foot")); + assert_eq!(angular_factor("degree"), Some(DEGREE_RAD)); + assert_eq!(scale_factor("parts per million"), Some(PPM)); + assert_eq!(any_factor("grad"), Some(core::f64::consts::PI / 200.0)); + // EPSG unit 1027 prints 3.16887651727315e-11 m/s. + let mm_per_yr = any_factor("millimetres per year").unwrap(); + assert!((mm_per_yr / 3.168_876_517_273_15e-11 - 1.0).abs() < 1e-14); + assert_eq!(any_factor("year"), Some(31_556_925.445)); + assert_eq!(ellipsoid_name(&Ellipsoid::WGS84), "WGS 84"); + assert_eq!(ellipsoid_name(&Ellipsoid::sphere(6_371_000.0)), "unknown"); + } +} diff --git a/nodedb-crs/src/parse/wkt2.rs b/nodedb-crs/src/parse/wkt2.rs new file mode 100644 index 000000000..2d500ff71 --- /dev/null +++ b/nodedb-crs/src/parse/wkt2.rs @@ -0,0 +1,433 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! WKT2 (ISO 19162:2019) parser. +//! +//! Reads a geographic CRS (`GEOGCRS`, or `GEODCRS` with an ellipsoidal +//! coordinate system), a projected CRS (`PROJCRS`), and a `BOUNDCRS` whose +//! source is one of those and whose transformation to WGS 84 is a static or +//! time-dependent Helmert transformation. A datum may be a `DATUM` or a +//! datum `ENSEMBLE`. WKT1 keywords are refused. + +use super::helmert_params::{HelmertValues, helmert_slot}; +use super::model::{ + ConversionModel, CrsModel, DEGREE_RAD, DatumModel, ParamModel, build_def, convert, +}; +use super::wkt2_tree::{WKT2_FORMAT, WktNode, WktValue, parse_tree}; +use crate::def::{AreaOfUse, AxisDirection, AxisOrder, CrsDef, CrsKind, DatumShift, Ellipsoid}; +use crate::error::CrsParseError; +use crate::srid::Srid; + +const GEOGRAPHIC: &[&str] = &["GEOGCRS", "GEOGRAPHICCRS", "GEODCRS", "GEODETICCRS"]; +const BASE_GEOGRAPHIC: &[&str] = &["BASEGEOGCRS", "BASEGEODCRS"]; +const DATUMS: &[&str] = &["DATUM", "GEODETICDATUM", "TRF", "ENSEMBLE"]; +const ELLIPSOIDS: &[&str] = &["ELLIPSOID", "SPHEROID"]; +const PRIME_MERIDIANS: &[&str] = &["PRIMEM", "PRIMEMERIDIAN"]; +const IDS: &[&str] = &["ID", "AUTHORITY"]; +const ANGLE_UNITS: &[&str] = &["ANGLEUNIT", "UNIT"]; +const LENGTH_UNITS: &[&str] = &["LENGTHUNIT", "UNIT"]; +const ANY_UNITS: &[&str] = &["ANGLEUNIT", "LENGTHUNIT", "SCALEUNIT", "UNIT"]; +/// Units a Helmert parameter carries: the reference epoch is in years. +const HELMERT_UNITS: &[&str] = &["ANGLEUNIT", "LENGTHUNIT", "SCALEUNIT", "TIMEUNIT", "UNIT"]; +const WKT1: &[&str] = &["PROJCS", "GEOGCS", "GEOCCS", "COMPD_CS", "VERT_CS"]; + +/// Parse WKT2 text into a CRS definition named `name` under `srid`. +pub fn parse_wkt2(srid: Srid, name: &str, text: &str) -> Result { + let root = parse_tree(text)?; + let model = model_of_crs(&root)?; + build_def(srid, name, &model, WKT2_FORMAT) +} + +fn invalid(detail: impl Into>) -> CrsParseError { + CrsParseError::content(WKT2_FORMAT, detail) +} + +fn model_of_crs(node: &WktNode) -> Result { + let keyword = node.keyword.as_str(); + if keyword == "BOUNDCRS" { + return bound(node); + } + if keyword == "PROJCRS" || keyword == "PROJECTEDCRS" { + return projected(node); + } + if GEOGRAPHIC.contains(&keyword) { + return geographic(node); + } + if WKT1.contains(&keyword) { + return Err(invalid(format!( + "{keyword} is WKT1; write the CRS as WKT2 (GEOGCRS or PROJCRS)" + ))); + } + Err(invalid(format!( + "{keyword} is not a supported CRS; supported: GEOGCRS, PROJCRS, BOUNDCRS" + ))) +} + +/// A `BOUNDCRS`: the source CRS with its Helmert shift to WGS 84. +fn bound(node: &WktNode) -> Result { + let source = node + .child(&["SOURCECRS"]) + .and_then(WktNode::first_node) + .ok_or_else(|| invalid("BOUNDCRS has no SOURCECRS"))?; + let target = node + .child(&["TARGETCRS"]) + .and_then(WktNode::first_node) + .ok_or_else(|| invalid("BOUNDCRS has no TARGETCRS"))?; + let target = model_of_crs(target)?; + if target.kind != CrsKind::Geographic || target.datum.ellipsoid != Ellipsoid::WGS84 { + return Err(invalid("BOUNDCRS must transform to WGS 84")); + } + let mut model = model_of_crs(source)?; + if model.datum.to_wgs84 != DatumShift::Identity { + return Err(invalid("BOUNDCRS source is itself bound")); + } + let transform = node + .child(&["ABRIDGEDTRANSFORMATION"]) + .ok_or_else(|| invalid("BOUNDCRS has no ABRIDGEDTRANSFORMATION"))?; + model.datum.to_wgs84 = transformation(transform)?; + Ok(model) +} + +/// The Helmert shift of an `ABRIDGEDTRANSFORMATION`. A parameter with no +/// unit is in the parameter's own unit: metre, arc-second, ppm, their rates +/// per year, or year for the reference epoch. A rate unit factor is SI per +/// second, as EPSG and PROJ write it. +fn transformation(node: &WktNode) -> Result { + let method = node + .child(&["METHOD"]) + .ok_or_else(|| invalid("ABRIDGEDTRANSFORMATION has no METHOD"))?; + let mut values = HelmertValues::default(); + for param in node.children(&["PARAMETER"]) { + let name = param.text(0).unwrap_or_default(); + let slot = helmert_slot(epsg_id(param), name) + .ok_or_else(|| invalid(format!("Helmert parameter '{name}' is not supported")))?; + let value = param + .number(1) + .ok_or_else(|| invalid(format!("parameter '{name}' has no value")))?; + values.set(slot, value, unit_factor(param, HELMERT_UNITS)?); + } + let accuracy = node.child(&["OPERATIONACCURACY"]).and_then(|a| a.number(0)); + values.into_shift( + epsg_id(method), + method.text(0).unwrap_or_default(), + accuracy, + WKT2_FORMAT, + ) +} + +fn geographic(node: &WktNode) -> Result { + let cs_type = node.child(&["CS"]).and_then(|cs| cs.word(0)); + if let Some(cs) = cs_type + && !cs.eq_ignore_ascii_case("ellipsoidal") + { + return Err(invalid(format!( + "a {cs} geodetic CRS is not supported; supported: ellipsoidal" + ))); + } + let angle_rad = crs_unit(node, ANGLE_UNITS)?.unwrap_or(DEGREE_RAD); + Ok(CrsModel { + name: node.text(0).unwrap_or_default().to_string(), + kind: CrsKind::Geographic, + datum: datum(node, angle_rad)?, + conversion: None, + metres_per_unit: 1.0, + axis: axis_order(node)?, + area_of_use: area(node), + }) +} + +fn projected(node: &WktNode) -> Result { + let base = node + .child(BASE_GEOGRAPHIC) + .ok_or_else(|| invalid("PROJCRS has no BASEGEOGCRS"))?; + let angle_rad = crs_unit(base, ANGLE_UNITS)?.unwrap_or(DEGREE_RAD); + let metres_per_unit = crs_unit(node, LENGTH_UNITS)?.unwrap_or(1.0); + let conversion = node + .child(&["CONVERSION"]) + .ok_or_else(|| invalid("PROJCRS has no CONVERSION"))?; + let method = conversion + .child(&["METHOD", "PROJECTION"]) + .ok_or_else(|| invalid("CONVERSION has no METHOD"))?; + let mut params = Vec::new(); + for param in conversion.children(&["PARAMETER"]) { + let name = param.text(0).unwrap_or_default(); + params.push(ParamModel { + name: name.to_string(), + code: epsg_id(param), + value: param + .number(1) + .ok_or_else(|| invalid(format!("parameter '{name}' has no value")))?, + factor: unit_factor(param, ANY_UNITS)?, + kind: None, + }); + } + Ok(CrsModel { + name: node.text(0).unwrap_or_default().to_string(), + kind: CrsKind::Projected, + datum: datum(base, angle_rad)?, + conversion: Some(ConversionModel { + method_name: method.text(0).unwrap_or_default().to_string(), + method_code: epsg_id(method), + params, + default_angle_rad: angle_rad, + default_length_m: metres_per_unit, + }), + metres_per_unit, + axis: axis_order(node)?, + area_of_use: area(node), + }) +} + +/// The datum of a geographic CRS node, its prime meridian read in +/// `angle_rad` radians per unit when it names no unit. +fn datum(crs: &WktNode, angle_rad: f64) -> Result { + let datum = crs + .child(DATUMS) + .ok_or_else(|| invalid(format!("{} has no DATUM", crs.keyword)))?; + let ellipsoid = datum + .child(ELLIPSOIDS) + .ok_or_else(|| invalid("DATUM has no ELLIPSOID"))?; + let a = ellipsoid + .number(1) + .ok_or_else(|| invalid("ELLIPSOID has no semi-major axis"))?; + let inv_f = ellipsoid + .number(2) + .ok_or_else(|| invalid("ELLIPSOID has no inverse flattening"))?; + let a = a * unit_factor(ellipsoid, LENGTH_UNITS)?.unwrap_or(1.0); + if a <= 0.0 || inv_f < 0.0 { + return Err(invalid(format!("ELLIPSOID [{a}, {inv_f}] is not valid"))); + } + let prime_meridian_deg = match crs + .child(PRIME_MERIDIANS) + .or_else(|| datum.child(PRIME_MERIDIANS)) + { + None => 0.0, + Some(pm) => { + let value = pm + .number(1) + .ok_or_else(|| invalid("PRIMEM has no longitude"))?; + let factor = unit_factor(pm, ANGLE_UNITS)?.unwrap_or(angle_rad); + convert(value, factor, DEGREE_RAD) + } + }; + Ok(DatumModel { + ellipsoid: Ellipsoid { a, inv_f }, + prime_meridian_deg, + epsg_code: epsg_id(datum), + to_wgs84: DatumShift::Identity, + }) +} + +/// The factor of the unit node of `node`, among `keywords`. +fn unit_factor(node: &WktNode, keywords: &[&str]) -> Result, CrsParseError> { + match node.child(keywords) { + None => Ok(None), + Some(unit) => unit + .number(1) + .filter(|f| *f > 0.0) + .map(Some) + .ok_or_else(|| invalid(format!("{} has no positive factor", unit.keyword))), + } +} + +/// The unit of a CRS's coordinates: from its first axis, else the unit +/// node of the CRS itself. +fn crs_unit(crs: &WktNode, keywords: &[&str]) -> Result, CrsParseError> { + if let Some(axis) = crs.child(&["AXIS"]) + && let Some(factor) = unit_factor(axis, keywords)? + { + return Ok(Some(factor)); + } + unit_factor(crs, keywords) +} + +/// The EPSG code an `ID` or `AUTHORITY` child names. +fn epsg_id(node: &WktNode) -> Option { + node.children(IDS).find_map(|id| { + if !id.text(0)?.eq_ignore_ascii_case("EPSG") { + return None; + } + match id.args.get(1)? { + WktValue::Number(n) if n.fract() == 0.0 && *n >= 0.0 && *n <= f64::from(u32::MAX) => { + Some(*n as u32) + } + WktValue::Text(t) => t.trim().parse().ok(), + WktValue::Number(_) | WktValue::Node(_) | WktValue::Word(_) => None, + } + }) +} + +/// The axis order of a CRS's `AXIS` children. Two axes along east and +/// north, in either order, read as east-north, the order NodeDB keeps +/// every coordinate in. +fn axis_order(crs: &WktNode) -> Result { + let mut directions = Vec::new(); + for axis in crs.children(&["AXIS"]).take(2) { + let word = axis + .word(1) + .ok_or_else(|| invalid("AXIS has no direction"))?; + let direction = match word.to_ascii_lowercase().as_str() { + "east" => AxisDirection::East, + "west" => AxisDirection::West, + "north" => AxisDirection::North, + "south" => AxisDirection::South, + other => { + return Err(invalid(format!( + "axis direction '{other}' is not supported; supported: east, west, north, south" + ))); + } + }; + directions.push(direction); + } + order_from_directions(&directions) +} + +/// The axis order of two horizontal directions. None or east-north in +/// either order is east, north, up. +pub(crate) fn order_from_directions( + directions: &[AxisDirection], +) -> Result { + match directions { + [] => Ok(AxisOrder::ENU), + [AxisDirection::East, AxisDirection::North] + | [AxisDirection::North, AxisDirection::East] => Ok(AxisOrder::ENU), + [x, y] => { + let text: String = [x.letter(), y.letter(), 'u'].iter().collect(); + AxisOrder::parse(&text) + } + _ => Err(CrsParseError::content( + WKT2_FORMAT, + "a CRS needs two horizontal axes", + )), + } +} + +/// The area of use: the first `BBOX`, directly or inside a `USAGE`. +fn area(crs: &WktNode) -> Option { + let bbox = crs + .child(&["BBOX"]) + .or_else(|| crs.children(&["USAGE"]).find_map(|u| u.child(&["BBOX"])))?; + Some(AreaOfUse { + south: bbox.number(0)?, + west: bbox.number(1)?, + north: bbox.number(2)?, + east: bbox.number(3)?, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::method::ProjectionDef; + + fn parse(text: &str) -> Result { + parse_wkt2(Srid::new(910_001).unwrap(), "test", text) + } + + /// EPSG:32648 as PROJ 9 writes it. + const UTM_48N: &str = r#"PROJCRS["WGS 84 / UTM zone 48N", + BASEGEOGCRS["WGS 84", + ENSEMBLE["World Geodetic System 1984 ensemble", + MEMBER["World Geodetic System 1984 (Transit)"], + ELLIPSOID["WGS 84",6378137,298.257223563,LENGTHUNIT["metre",1]], + ENSEMBLEACCURACY[2.0]], + PRIMEM["Greenwich",0,ANGLEUNIT["degree",0.0174532925199433]], + ID["EPSG",4326]], + CONVERSION["UTM zone 48N", + METHOD["Transverse Mercator",ID["EPSG",9807]], + PARAMETER["Latitude of natural origin",0,ANGLEUNIT["degree",0.0174532925199433],ID["EPSG",8801]], + PARAMETER["Longitude of natural origin",105,ANGLEUNIT["degree",0.0174532925199433],ID["EPSG",8802]], + PARAMETER["Scale factor at natural origin",0.9996,SCALEUNIT["unity",1],ID["EPSG",8805]], + PARAMETER["False easting",500000,LENGTHUNIT["metre",1],ID["EPSG",8806]], + PARAMETER["False northing",0,LENGTHUNIT["metre",1],ID["EPSG",8807]]], + CS[Cartesian,2], + AXIS["(E)",east,ORDER[1],LENGTHUNIT["metre",1]], + AXIS["(N)",north,ORDER[2],LENGTHUNIT["metre",1]], + USAGE[SCOPE["Navigation and medium accuracy spatial referencing."], + AREA["Between 102°E and 108°E, northern hemisphere"], + BBOX[0,102,84,108]], + ID["EPSG",32648]]"#; + + #[test] + fn utm_48n_matches_the_builtin() { + let def = parse(UTM_48N).unwrap(); + let builtin = crate::CrsContext::builtin_only() + .resolve(Srid::new(32648).unwrap()) + .unwrap(); + assert_eq!(def.projection, builtin.projection); + assert_eq!(def.datum.ellipsoid, builtin.datum.ellipsoid); + assert_eq!(def.unit, builtin.unit); + assert_eq!(def.area_of_use, builtin.area_of_use); + assert_eq!(&*def.name, "test"); + } + + #[test] + fn bound_crs_carries_its_helmert_shift() { + let text = r#"BOUNDCRS[ + SOURCECRS[GEOGCRS["Kertau",DATUM["Kertau 1968",ELLIPSOID["Everest 1830 Modified",6377304.063,300.8017,LENGTHUNIT["metre",1]],ID["EPSG",6245]], + PRIMEM["Greenwich",0,ANGLEUNIT["degree",0.0174532925199433]],CS[ellipsoidal,2], + AXIS["latitude",north,ORDER[1],ANGLEUNIT["degree",0.0174532925199433]], + AXIS["longitude",east,ORDER[2],ANGLEUNIT["degree",0.0174532925199433]]]], + TARGETCRS[GEOGCRS["WGS 84",DATUM["World Geodetic System 1984",ELLIPSOID["WGS 84",6378137,298.257223563]], + CS[ellipsoidal,2],AXIS["latitude",north],AXIS["longitude",east],ANGLEUNIT["degree",0.0174532925199433]]], + ABRIDGEDTRANSFORMATION["Kertau to WGS 84", + METHOD["Coordinate Frame rotation (geog2D domain)",ID["EPSG",9607]], + PARAMETER["X-axis translation",-11,ID["EPSG",8605]], + PARAMETER["Y-axis translation",851,ID["EPSG",8606]], + PARAMETER["Z-axis translation",5,ID["EPSG",8607]], + PARAMETER["X-axis rotation",0.5,ANGLEUNIT["arc-second",4.84813681109536E-06],ID["EPSG",8608]], + OPERATIONACCURACY[10]]]"#; + let def = parse(text).unwrap(); + assert!(def.is_geographic()); + assert!(def.axis.is_enu()); + assert_eq!(def.datum.epsg_code, Some(6245)); + let DatumShift::Helmert(h) = def.datum.to_wgs84 else { + panic!("expected a Helmert shift"); + }; + assert_eq!( + h.convention, + crate::def::RotationConvention::CoordinateFrame + ); + assert_eq!( + [h.tx_m, h.ty_m, h.tz_m, h.rx_arcsec], + [-11.0, 851.0, 5.0, 0.5] + ); + assert_eq!(h.accuracy_m, Some(10.0)); + } + + #[test] + fn feet_and_south_west_axes() { + let text = r#"PROJCRS["x",BASEGEOGCRS["y",DATUM["z",ELLIPSOID["GRS 1980",6378137,298.257222101]]], + CONVERSION["c",METHOD["Lambert Conic Conformal (2SP)",ID["EPSG",9802]], + PARAMETER["Latitude of false origin",40.1666666666667,ID["EPSG",8821]], + PARAMETER["Longitude of false origin",-74,ID["EPSG",8822]], + PARAMETER["Latitude of 1st standard parallel",41.0333333333333,ID["EPSG",8823]], + PARAMETER["Latitude of 2nd standard parallel",40.6666666666667,ID["EPSG",8824]], + PARAMETER["Easting at false origin",984250,ID["EPSG",8826]], + PARAMETER["Northing at false origin",0,ID["EPSG",8827]]], + CS[Cartesian,2],AXIS["x",west],AXIS["y",south],LENGTHUNIT["US survey foot",0.304800609601219]]"#; + let def = parse(text).unwrap(); + assert_eq!(def.unit.metres_per_unit, 0.304800609601219); + assert_eq!(def.axis.to_string(), "wsu"); + let Some(ProjectionDef::LambertConicConformal(lcc)) = def.projection else { + panic!("expected lcc"); + }; + // A length parameter with no unit is in the CRS's unit. + assert!((lcc.false_easting - 984_250.0 * 0.304800609601219).abs() < 1e-6); + } + + #[test] + fn refusals_name_the_problem() { + assert!(matches!( + parse(r#"PROJCS["x",GEOGCS["y"]]"#), + Err(CrsParseError::Invalid { .. }) + )); + let bonne = r#"PROJCRS["x",BASEGEOGCRS["y",DATUM["z",ELLIPSOID["e",6378137,298.257223563]]], + CONVERSION["c",METHOD["Bonne",ID["EPSG",9827]]],CS[Cartesian,2]]"#; + let err = parse(bonne).unwrap_err(); + assert!(err.to_string().contains("Bonne"), "{err}"); + assert!(matches!( + parse("GEOGCRS["), + Err(CrsParseError::Malformed { .. }) + )); + } +} diff --git a/nodedb-crs/src/parse/wkt2_tree.rs b/nodedb-crs/src/parse/wkt2_tree.rs new file mode 100644 index 000000000..2dfb5d1ce --- /dev/null +++ b/nodedb-crs/src/parse/wkt2_tree.rs @@ -0,0 +1,285 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! WKT text as a tree of `KEYWORD[value, ...]` nodes. +//! +//! The parser reads the shared WKT grammar: square or round brackets, +//! double-quoted text with `""` for a quote, numbers, and bare words such +//! as axis directions. It nests at most [`MAX_DEPTH`] levels, so hostile +//! input cannot exhaust the stack. + +use crate::error::CrsParseError; + +/// Format name reported by WKT2 errors. +pub const WKT2_FORMAT: &str = "WKT2"; + +/// Deepest node nesting the parser accepts. +const MAX_DEPTH: usize = 32; + +/// One argument of a node. +#[derive(Debug, Clone, PartialEq)] +pub(crate) enum WktValue { + Node(WktNode), + Text(String), + Number(f64), + Word(String), +} + +/// A `KEYWORD[...]` node. The keyword is upper-cased. +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct WktNode { + pub keyword: String, + pub args: Vec, +} + +impl WktNode { + /// Argument `i` as text. + pub(crate) fn text(&self, i: usize) -> Option<&str> { + match self.args.get(i)? { + WktValue::Text(t) => Some(t), + WktValue::Node(_) | WktValue::Number(_) | WktValue::Word(_) => None, + } + } + + /// Argument `i` as a number. + pub(crate) fn number(&self, i: usize) -> Option { + match self.args.get(i)? { + WktValue::Number(n) => Some(*n), + WktValue::Node(_) | WktValue::Text(_) | WktValue::Word(_) => None, + } + } + + /// Argument `i` as a bare word. + pub(crate) fn word(&self, i: usize) -> Option<&str> { + match self.args.get(i)? { + WktValue::Word(w) => Some(w), + WktValue::Node(_) | WktValue::Text(_) | WktValue::Number(_) => None, + } + } + + /// Every child node whose keyword is in `keywords`. + pub(crate) fn children<'a>( + &'a self, + keywords: &'a [&'a str], + ) -> impl Iterator + 'a { + self.args.iter().filter_map(move |arg| match arg { + WktValue::Node(n) if keywords.contains(&n.keyword.as_str()) => Some(n), + WktValue::Node(_) | WktValue::Text(_) | WktValue::Number(_) | WktValue::Word(_) => None, + }) + } + + /// The first child node whose keyword is in `keywords`. + pub(crate) fn child<'a>(&'a self, keywords: &'a [&'a str]) -> Option<&'a WktNode> { + self.children(keywords).next() + } + + /// The first child node of any keyword. + pub(crate) fn first_node(&self) -> Option<&WktNode> { + self.args.iter().find_map(|arg| match arg { + WktValue::Node(n) => Some(n), + WktValue::Text(_) | WktValue::Number(_) | WktValue::Word(_) => None, + }) + } +} + +/// Parse `text` into its root node. Only whitespace may follow it. +pub(crate) fn parse_tree(text: &str) -> Result { + let mut parser = Parser { + bytes: text.as_bytes(), + text, + pos: 0, + }; + parser.skip_space(); + let root = parser.node(0)?; + parser.skip_space(); + if parser.pos != parser.bytes.len() { + return Err(parser.malformed()); + } + Ok(root) +} + +struct Parser<'a> { + bytes: &'a [u8], + text: &'a str, + pos: usize, +} + +impl Parser<'_> { + fn malformed(&self) -> CrsParseError { + CrsParseError::Malformed { + format: WKT2_FORMAT, + offset: self.pos, + } + } + + fn peek(&self) -> Option { + self.bytes.get(self.pos).copied() + } + + fn skip_space(&mut self) { + while self.peek().is_some_and(|b| b.is_ascii_whitespace()) { + self.pos += 1; + } + } + + /// A run of word characters: letters, digits and `_`. + fn word(&mut self) -> &str { + let start = self.pos; + while self + .peek() + .is_some_and(|b| b.is_ascii_alphanumeric() || b == b'_') + { + self.pos += 1; + } + &self.text[start..self.pos] + } + + fn node(&mut self, depth: usize) -> Result { + if depth >= MAX_DEPTH { + return Err(self.malformed()); + } + let keyword = self.word().to_ascii_uppercase(); + if keyword.is_empty() || !keyword.as_bytes()[0].is_ascii_alphabetic() { + return Err(self.malformed()); + } + self.skip_space(); + let close = match self.peek() { + Some(b'[') => b']', + Some(b'(') => b')', + _ => return Err(self.malformed()), + }; + self.pos += 1; + let mut args = Vec::new(); + self.skip_space(); + if self.peek() == Some(close) { + self.pos += 1; + return Ok(WktNode { keyword, args }); + } + loop { + self.skip_space(); + args.push(self.value(depth)?); + self.skip_space(); + match self.peek() { + Some(b',') => self.pos += 1, + Some(b) if b == close => { + self.pos += 1; + return Ok(WktNode { keyword, args }); + } + _ => return Err(self.malformed()), + } + } + } + + fn value(&mut self, depth: usize) -> Result { + match self.peek() { + Some(b'"') => self.quoted().map(WktValue::Text), + Some(b) if b == b'-' || b == b'+' || b == b'.' || b.is_ascii_digit() => { + self.number().map(WktValue::Number) + } + Some(b) if b.is_ascii_alphabetic() => { + let start = self.pos; + let word = self.word().to_string(); + self.skip_space(); + if matches!(self.peek(), Some(b'[' | b'(')) { + self.pos = start; + self.node(depth + 1).map(WktValue::Node) + } else { + Ok(WktValue::Word(word)) + } + } + _ => Err(self.malformed()), + } + } + + fn quoted(&mut self) -> Result { + let start = self.pos; + self.pos += 1; + let mut out = String::new(); + loop { + let rest = &self.text[self.pos..]; + let Some(quote) = rest.find('"') else { + self.pos = start; + return Err(self.malformed()); + }; + out.push_str(&rest[..quote]); + self.pos += quote + 1; + if self.peek() == Some(b'"') { + out.push('"'); + self.pos += 1; + } else { + return Ok(out); + } + } + } + + fn number(&mut self) -> Result { + let start = self.pos; + while self + .peek() + .is_some_and(|b| b.is_ascii_digit() || matches!(b, b'-' | b'+' | b'.' | b'e' | b'E')) + { + self.pos += 1; + } + let value: f64 = + self.text[start..self.pos] + .parse() + .map_err(|_| CrsParseError::Malformed { + format: WKT2_FORMAT, + offset: start, + })?; + if value.is_finite() { + Ok(value) + } else { + Err(CrsParseError::Malformed { + format: WKT2_FORMAT, + offset: start, + }) + } + } +} + +/// Append `text` as a WKT quoted string. +pub(crate) fn push_quoted(out: &mut String, text: &str) { + out.push('"'); + out.push_str(&text.replace('"', "\"\"")); + out.push('"'); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn nodes_text_numbers_and_words() { + let root = parse_tree( + r#"PROJCRS["a ""b""", CS[Cartesian, 2], AXIS["x", east], LENGTHUNIT["metre", 1.5E0]]"#, + ) + .unwrap(); + assert_eq!(root.keyword, "PROJCRS"); + assert_eq!(root.text(0), Some(r#"a "b""#)); + let cs = root.child(&["CS"]).unwrap(); + assert_eq!(cs.word(0), Some("Cartesian")); + assert_eq!(cs.number(1), Some(2.0)); + assert_eq!(root.child(&["LENGTHUNIT"]).unwrap().number(1), Some(1.5)); + let round = parse_tree("unit(\"m\",1)").unwrap(); + assert_eq!(round.keyword, "UNIT"); + } + + #[test] + fn malformed_input_names_the_offset() { + for bad in ["", "A[", "A[1,]", "A[\"x]", "A[1] B", "[1]", "A[1e999]"] { + assert!( + matches!(parse_tree(bad), Err(CrsParseError::Malformed { .. })), + "{bad}" + ); + } + let deep = "A[".repeat(40) + &"]".repeat(40); + assert!(parse_tree(&deep).is_err()); + } + + #[test] + fn quoting_round_trips() { + let mut out = String::new(); + push_quoted(&mut out, r#"x"y"#); + assert_eq!(out, r#""x""y""#); + } +} diff --git a/nodedb-crs/src/parse/wkt2_writer.rs b/nodedb-crs/src/parse/wkt2_writer.rs new file mode 100644 index 000000000..63b6b82ca --- /dev/null +++ b/nodedb-crs/src/parse/wkt2_writer.rs @@ -0,0 +1,256 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! WKT2 (ISO 19162:2019) writer. +//! +//! Numbers print in Rust's shortest round-trip form and every parameter +//! names its unit, so [`parse_wkt2`](super::parse_wkt2) reads back the same +//! definition. A datum with a Helmert shift is written as a `BOUNDCRS` to +//! WGS 84. A time-dependent shift adds its rates, each with its EPSG +//! per-second unit factor, and its parameter reference epoch in +//! `TIMEUNIT["year",31556925.445]`. A PROJ parameter no EPSG parameter names is written as +//! `PARAMETER["PROJ ", value]`, which this crate reads back. + +use core::fmt::Write; + +use super::helmert_params::{writer_method, writer_params}; +use super::methods::ParamKind; +use super::model::{ConversionModel, CrsModel, DEGREE_RAD, DatumModel, model_of}; +use super::unit_names::{ellipsoid_name, linear_name}; +use super::wkt2_tree::push_quoted; +use crate::def::{AxisDirection, CrsDef, CrsKind, DatumShift}; +use crate::srid::SridAuthority; + +/// Append formatted text. Writing to a `String` cannot fail. +fn push(out: &mut String, args: core::fmt::Arguments<'_>) { + let _ = out.write_fmt(args); +} + +impl CrsDef { + /// This definition as WKT2:2019 text. + pub fn to_wkt2(&self) -> String { + let model = model_of(self); + let mut crs = String::new(); + write_crs(&mut crs, &model); + if let Some(code) = epsg_crs_code(self) { + // The `ID` closes the CRS node: drop its `]` and append. + crs.pop(); + push(&mut crs, format_args!(",ID[\"EPSG\",{code}]]")); + } + match &model.datum.to_wgs84 { + DatumShift::Identity => crs, + DatumShift::Helmert(h) => { + let mut out = String::from("BOUNDCRS[SOURCECRS["); + out.push_str(&crs); + out.push_str("],TARGETCRS["); + out.push_str(WGS84_WKT); + out.push_str("],ABRIDGEDTRANSFORMATION[\"Transformation to WGS 84\","); + let (method, code) = writer_method(h); + push( + &mut out, + format_args!("METHOD[\"{method}\",ID[\"EPSG\",{code}]]"), + ); + for (code, name, unit, value) in writer_params(h) { + push( + &mut out, + format_args!( + ",PARAMETER[\"{name}\",{value},{}[\"{}\",{}],ID[\"EPSG\",{code}]]", + unit.wkt_keyword(), + unit.name(), + unit.factor() + ), + ); + } + if let Some(accuracy) = h.accuracy_m { + push(&mut out, format_args!(",OPERATIONACCURACY[{accuracy}]")); + } + out.push_str("]]"); + out + } + } + } +} + +/// The EPSG code of an EPSG-authority CRS. +fn epsg_crs_code(def: &CrsDef) -> Option { + match def.srid.authority() { + SridAuthority::Epsg => Some(def.srid.code()), + SridAuthority::NodeDb | SridAuthority::User | SridAuthority::Reserved => None, + } +} + +const METRE: &str = "LENGTHUNIT[\"metre\",1]"; + +/// WGS 84 as a target CRS. +const WGS84_WKT: &str = "GEOGCRS[\"WGS 84\",DATUM[\"World Geodetic System 1984\",\ + ELLIPSOID[\"WGS 84\",6378137,298.257223563,LENGTHUNIT[\"metre\",1]],ID[\"EPSG\",6326]],\ + PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433]],CS[ellipsoidal,2],\ + AXIS[\"longitude\",east,ORDER[1],ANGLEUNIT[\"degree\",0.0174532925199433]],\ + AXIS[\"latitude\",north,ORDER[2],ANGLEUNIT[\"degree\",0.0174532925199433]],\ + ID[\"EPSG\",4326]]"; + +fn degree() -> String { + format!("ANGLEUNIT[\"degree\",{DEGREE_RAD}]") +} + +fn write_crs(out: &mut String, model: &CrsModel) { + match &model.conversion { + Some(conversion) => { + out.push_str("PROJCRS["); + push_quoted(out, &model.name); + out.push_str(",BASEGEOGCRS[\"unknown\","); + write_datum(out, &model.datum); + out.push_str(",ANGLEUNIT[\"degree\","); + push(out, format_args!("{DEGREE_RAD}]],")); + write_conversion(out, conversion); + out.push_str(",CS[Cartesian,2]"); + let unit = linear_unit(model.metres_per_unit); + write_axes(out, model, &unit); + } + None => { + out.push_str("GEOGCRS["); + push_quoted(out, &model.name); + out.push(','); + write_datum(out, &model.datum); + out.push_str(",CS[ellipsoidal,2]"); + write_axes(out, model, °ree()); + } + } + if let Some(area) = &model.area_of_use { + push( + out, + format_args!( + ",USAGE[SCOPE[\"unknown\"],BBOX[{},{},{},{}]]", + area.south, area.west, area.north, area.east + ), + ); + } + out.push(']'); +} + +/// `DATUM[...]` then `PRIMEM[...]`. +fn write_datum(out: &mut String, datum: &DatumModel) { + out.push_str("DATUM[\"unknown\",ELLIPSOID["); + push_quoted(out, ellipsoid_name(&datum.ellipsoid)); + push( + out, + format_args!(",{},{},{METRE}]", datum.ellipsoid.a, datum.ellipsoid.inv_f), + ); + if let Some(code) = datum.epsg_code { + push(out, format_args!(",ID[\"EPSG\",{code}]")); + } + out.push(']'); + let pm_name = if datum.prime_meridian_deg == 0.0 { + "Greenwich" + } else { + "unknown" + }; + push( + out, + format_args!( + ",PRIMEM[\"{pm_name}\",{},{}]", + datum.prime_meridian_deg, + degree() + ), + ); +} + +fn write_conversion(out: &mut String, conversion: &ConversionModel) { + out.push_str("CONVERSION[\"unknown\",METHOD["); + push_quoted(out, &conversion.method_name); + if let Some(code) = conversion.method_code { + push(out, format_args!(",ID[\"EPSG\",{code}]")); + } + out.push(']'); + for param in &conversion.params { + out.push_str(",PARAMETER["); + push_quoted(out, ¶m.name); + push(out, format_args!(",{}", param.value)); + match param.kind { + Some(ParamKind::Angle) => push(out, format_args!(",{}", degree())), + Some(ParamKind::Length) => push(out, format_args!(",{METRE}")), + Some(ParamKind::Scale) => out.push_str(",SCALEUNIT[\"unity\",1]"), + None => {} + } + if let Some(code) = param.code { + push(out, format_args!(",ID[\"EPSG\",{code}]")); + } + out.push(']'); + } + out.push(']'); +} + +/// `LENGTHUNIT[...]` of `metres_per_unit`. +fn linear_unit(metres_per_unit: f64) -> String { + let name = linear_name(metres_per_unit).unwrap_or("unknown"); + let mut out = String::from("LENGTHUNIT["); + push_quoted(&mut out, name); + push(&mut out, format_args!(",{metres_per_unit}]")); + out +} + +/// Two `AXIS[...]` nodes in the model's axis order, each with `unit`. +fn write_axes(out: &mut String, model: &CrsModel, unit: &str) { + let geographic = model.kind == CrsKind::Geographic; + for (order, direction) in [model.axis.x, model.axis.y].into_iter().enumerate() { + let (name, word) = axis_name(direction, geographic); + push( + out, + format_args!(",AXIS[\"{name}\",{word},ORDER[{}],{unit}]", order + 1), + ); + } +} + +/// The name and WKT direction word of one axis. +fn axis_name(direction: AxisDirection, geographic: bool) -> (&'static str, &'static str) { + match (direction, geographic) { + (AxisDirection::East, true) => ("longitude", "east"), + (AxisDirection::West, true) => ("longitude", "west"), + (AxisDirection::North, true) => ("latitude", "north"), + (AxisDirection::South, true) => ("latitude", "south"), + (AxisDirection::East, false) => ("easting (X)", "east"), + (AxisDirection::West, false) => ("westing (X)", "west"), + (AxisDirection::North, false) => ("northing (Y)", "north"), + (AxisDirection::South, false) => ("southing (Y)", "south"), + (AxisDirection::Up, _) => ("height", "up"), + (AxisDirection::Down, _) => ("depth", "down"), + } +} + +#[cfg(test)] +mod tests { + use crate::CrsContext; + use crate::parse::{parse_proj_string, parse_wkt2}; + use crate::srid::Srid; + + fn round_trip(srid: Srid, text: &str) { + let def = parse_proj_string(srid, "rt", text).unwrap(); + let wkt = def.to_wkt2(); + let back = parse_wkt2(srid, "rt", &wkt).unwrap_or_else(|e| panic!("{text}\n{wkt}\n{e}")); + assert_eq!(back, def, "{text}\n{wkt}"); + } + + #[test] + fn every_method_round_trips() { + let srid = Srid::new(910_000).unwrap(); + for text in crate::parse::round_trip_cases::CASES { + round_trip(srid, text); + } + } + + /// A built-in with an EPSG datum code, a shift and an area keeps all + /// three, and names its EPSG code. + #[test] + fn builtins_round_trip_with_datum_code_and_area() { + let ctx = CrsContext::builtin_only(); + for code in [4326, 4742, 3375, 32648, 2263, 3857, 5514, 990_001, 990_002] { + let srid = Srid::new(code).unwrap(); + let def = ctx.resolve(srid).unwrap(); + let wkt = def.to_wkt2(); + let back = parse_wkt2(srid, &def.name, &wkt).unwrap_or_else(|e| panic!("{wkt}\n{e}")); + assert_eq!(back, *def, "{wkt}"); + if code < 900_000 { + assert!(wkt.contains(&format!("ID[\"EPSG\",{code}]")), "{wkt}"); + } + } + } +} diff --git a/nodedb-crs/src/registry/builtin.rs b/nodedb-crs/src/registry/builtin.rs new file mode 100644 index 000000000..9aba1f402 --- /dev/null +++ b/nodedb-crs/src/registry/builtin.rs @@ -0,0 +1,406 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! The built-in CRS table: NodeDB's own codes, then every EPSG CRS. +//! +//! The EPSG table is two tab-separated files compiled into the binary: +//! `epsg.tsv` from the pinned EPSG dataset, and `epsg_supplement.tsv` with +//! EPSG CRSs published after it. No code is in both. One CRS per line, +//! sorted ascending by code, `#` starts a comment line: +//! +//! ```text +//! codekindnameareaaccuracy_mdatumproj +//! ``` +//! +//! `datum` is the EPSG code of the CRS's datum, or empty. CRSs with one +//! datum code share a datum, whatever their `+towgs84`. +//! +//! A line that does not start with a digit is not data, so a column-header +//! line is skipped like a comment. +//! +//! Lookup bisects the text by byte offset. It allocates nothing and keeps +//! no state. + +use crate::def::{AreaOfUse, CrsDef, CrsKind, DatumShift}; +use crate::error::CrsParseError; +use crate::parse::parse_proj_string; +use crate::srid::Srid; + +/// The EPSG table from the pinned EPSG dataset. +const EPSG_TSV: &str = include_str!("../../data/epsg.tsv"); + +/// EPSG CRSs newer than the pinned dataset, such as GDM2020. +const EPSG_SUPPLEMENT_TSV: &str = include_str!("../../data/epsg_supplement.tsv"); + +/// Number of tab-separated fields per data line. +const FIELDS: usize = 7; + +/// One row of the built-in table. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct BuiltinEntry { + pub code: u32, + pub kind: CrsKind, + pub name: &'static str, + /// `west,south,east,north` degrees, or empty. + pub area: &'static str, + /// Transformation accuracy in metres, or empty. + pub accuracy_m: &'static str, + /// EPSG datum code, or empty. + pub datum: &'static str, + pub proj: &'static str, +} + +impl BuiltinEntry { + /// The parsed area of use. + pub fn area_of_use(&self) -> Result, CrsParseError> { + AreaOfUse::parse(self.area) + } + + /// The parsed transformation accuracy, metres. + pub fn accuracy(&self) -> Result, CrsParseError> { + if self.accuracy_m.is_empty() { + return Ok(None); + } + self.accuracy_m + .parse::() + .ok() + .filter(|v| v.is_finite() && *v >= 0.0) + .map(Some) + .ok_or_else(|| CrsParseError::invalid("accuracy_m", self.accuracy_m)) + } + + /// The parsed EPSG datum code. + pub fn datum_code(&self) -> Result, CrsParseError> { + if self.datum.is_empty() { + return Ok(None); + } + self.datum + .parse::() + .map(Some) + .map_err(|_| CrsParseError::invalid("datum", self.datum)) + } + + /// The definition this row describes, under `srid`: its PROJ string + /// with its area, transformation accuracy and EPSG datum code. + pub fn definition(&self, srid: Srid) -> Result { + let mut def = parse_proj_string(srid, self.name, self.proj)?; + if def.kind != self.kind { + return Err(CrsParseError::invalid("kind", self.kind.as_str())); + } + def.area_of_use = self.area_of_use()?; + def.datum.epsg_code = self.datum_code()?; + let accuracy = self.accuracy()?; + match &mut def.datum.to_wgs84 { + DatumShift::Helmert(h) => h.accuracy_m = accuracy, + DatumShift::Identity => {} + } + Ok(def) + } +} + +/// NodeDB's own CRSs, checked before the EPSG table. +const NODEDB_BUILTINS: &[BuiltinEntry] = &[ + BuiltinEntry { + code: 990_001, + kind: CrsKind::Projected, + name: "MODIS Sinusoidal (SR-ORG:6842)", + area: "-180,-90,180,90", + accuracy_m: "", + datum: "", + proj: "+proj=sinu +lon_0=0 +x_0=0 +y_0=0 +R=6371007.181 +units=m +no_defs", + }, + BuiltinEntry { + code: 990_002, + kind: CrsKind::Projected, + name: "Himawari geostationary, 140.7 E", + area: "", + accuracy_m: "", + datum: "", + proj: "+proj=geos +h=35785863 +lon_0=140.7 +ellps=WGS84 +units=m +no_defs", + }, +]; + +/// The built-in CRS with `code`: NodeDB's own first, then EPSG. +pub fn builtin_lookup(code: u32) -> Option { + NODEDB_BUILTINS + .iter() + .find(|e| e.code == code) + .copied() + .or_else(|| lookup_in(EPSG_TSV, code)) + .or_else(|| lookup_in(EPSG_SUPPLEMENT_TSV, code)) +} + +/// Bisect `table` for `code`. +/// +/// Invariant: `lo` and `hi` sit on line starts (or the end), and the line +/// with `code`, if present, lies in `lo..hi`. +pub fn lookup_in(table: &'static str, code: u32) -> Option { + let bytes = table.as_bytes(); + let (mut lo, mut hi) = (0, bytes.len()); + while lo < hi { + let mid = lo + (hi - lo) / 2; + let start = line_start(bytes, lo, mid); + match first_data_line(table, start, hi) { + None => hi = start, + Some((line_at, line, next)) => { + let row_code: u32 = line.split('\t').next()?.parse().ok()?; + match row_code.cmp(&code) { + core::cmp::Ordering::Equal => return parse_row(line), + core::cmp::Ordering::Less => lo = next, + core::cmp::Ordering::Greater => hi = line_at, + } + } + } + } + None +} + +/// Start of the line holding byte `at`, never before `floor`. +fn line_start(bytes: &[u8], floor: usize, at: usize) -> usize { + bytes[floor..at] + .iter() + .rposition(|b| *b == b'\n') + .map_or(floor, |i| floor + i + 1) +} + +/// The first data line starting at or after `from` and +/// before `hi`: its start, its text without the line break, and the start +/// of the line after it. +fn first_data_line( + table: &'static str, + from: usize, + hi: usize, +) -> Option<(usize, &'static str, usize)> { + let mut at = from; + while at < hi { + let end = table[at..].find('\n').map_or(table.len(), |i| at + i); + let next = (end + 1).min(table.len()); + let line = table[at..end].trim_end_matches('\r'); + if is_data_line(line) { + return Some((at, line, next)); + } + at = next; + } + None +} + +/// Whether `line` is a data row: it starts with a code digit. Comments +/// (`#`), blank lines and a column-header line are not. +fn is_data_line(line: &str) -> bool { + line.as_bytes().first().is_some_and(u8::is_ascii_digit) +} + +/// Split one data line into a [`BuiltinEntry`]. +fn parse_row(line: &'static str) -> Option { + let mut f = line.splitn(FIELDS, '\t'); + let code = f.next()?.parse().ok()?; + let kind = CrsKind::from_name(f.next()?)?; + Some(BuiltinEntry { + code, + kind, + name: f.next()?, + area: f.next()?, + accuracy_m: f.next()?, + datum: f.next()?, + proj: f.next()?, + }) +} + +/// Every data line of `table`, in file order. +fn table_rows(table: &'static str) -> impl Iterator { + table + .lines() + .map(|l| l.trim_end_matches('\r')) + .filter(|l| is_data_line(l)) +} + +/// Every EPSG row of both files, split into its fields, ascending by code. +fn epsg_entries() -> impl Iterator { + let mut main = table_rows(EPSG_TSV).filter_map(parse_row).peekable(); + let mut newer = table_rows(EPSG_SUPPLEMENT_TSV) + .filter_map(parse_row) + .peekable(); + core::iter::from_fn( + move || match (main.peek().map(|e| e.code), newer.peek().map(|e| e.code)) { + (Some(a), Some(b)) if b < a => newer.next(), + (Some(_), Some(_) | None) => main.next(), + (None, Some(_) | None) => newer.next(), + }, + ) +} + +/// Every built-in CRS row: NodeDB's own first, then every EPSG CRS +/// ascending. +pub fn builtin_entries() -> impl Iterator { + NODEDB_BUILTINS.iter().copied().chain(epsg_entries()) +} + +/// The code of every built-in CRS of `kind`: NodeDB's own first, then +/// every EPSG CRS ascending. +pub fn builtin_codes(kind: CrsKind) -> impl Iterator { + builtin_entries() + .filter(move |e| e.kind == kind) + .filter_map(|e| Srid::new(e.code).ok()) +} + +/// The lowest-coded EPSG geographic CRS on the EPSG datum `epsg_datum`. +pub fn geographic_crs_on_datum(epsg_datum: u32) -> Option { + epsg_entries() + .filter(|e| e.kind == CrsKind::Geographic) + .find(|e| e.datum_code().ok().flatten() == Some(epsg_datum)) + .and_then(|e| Srid::new(e.code).ok()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::parse::parse_proj_string; + + const TABLE: &str = "# EPSG 12.0 PROJ 9.8.1\n\ + code\tkind\tname\tarea\taccuracy_m\tdatum\tproj\n\ + # second comment\n\ + 3375\tprojected\tGDM2000 / Peninsula RSO\t99.59,1.21,104.6,7.81\t\t6742\t+proj=omerc +no_uoff +lat_0=4 +lonc=102.25 +alpha=323.025796466667 +gamma=323.130102361111 +k=0.99984 +x_0=804671 +y_0=0 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs\n\ + 4326\tgeographic\tWGS 84\t-180,-90,180,90\t\t6326\t+proj=longlat +datum=WGS84 +no_defs\n\ + # a comment between rows\n\ + \n\ + 4742\tgeographic\tGDM2000\t98.02,0.85,119.61,7.81\t\t6742\t+proj=longlat +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +no_defs\n\ + 32647\tprojected\tWGS 84 / UTM zone 47N\t96,0,102,84\t\t6326\t+proj=utm +zone=47 +datum=WGS84 +units=m +no_defs\n\ + 32648\tprojected\tWGS 84 / UTM zone 48N\t102,0,108,84\t\t6326\t+proj=utm +zone=48 +datum=WGS84 +units=m +no_defs\n"; + + const CRLF_TABLE: &str = "# h\r\n10\tgeographic\tA\t\t1.5\t6326\t+proj=longlat\r\n20\tgeographic\tB\t\t\t\t+proj=longlat\r\n"; + + #[test] + fn finds_first_last_and_middle() { + for code in [3375, 4326, 4742, 32647, 32648] { + let entry = lookup_in(TABLE, code).unwrap(); + assert_eq!(entry.code, code); + } + let last = lookup_in(TABLE, 32648).unwrap(); + assert_eq!(last.kind, CrsKind::Projected); + assert_eq!(last.name, "WGS 84 / UTM zone 48N"); + assert_eq!( + last.proj, + "+proj=utm +zone=48 +datum=WGS84 +units=m +no_defs" + ); + assert_eq!(last.area_of_use().unwrap().unwrap().west, 102.0); + } + + #[test] + fn missing_codes_return_none() { + for code in [1, 3374, 3376, 4000, 4743, 32649, 999_999] { + assert!(lookup_in(TABLE, code).is_none(), "{code}"); + } + } + + #[test] + fn comment_only_and_empty_tables() { + assert!(lookup_in("", 4326).is_none()); + assert!(lookup_in("# only\n# comments\n", 4326).is_none()); + } + + #[test] + fn crlf_lines_and_accuracy() { + let a = lookup_in(CRLF_TABLE, 10).unwrap(); + assert_eq!(a.accuracy(), Ok(Some(1.5))); + assert_eq!(a.datum_code(), Ok(Some(6326))); + let b = lookup_in(CRLF_TABLE, 20).unwrap(); + assert_eq!(b.proj, "+proj=longlat"); + assert_eq!(b.accuracy(), Ok(None)); + assert_eq!(b.datum_code(), Ok(None)); + } + + #[test] + fn nodedb_builtins_come_first() { + let modis = builtin_lookup(990_001).unwrap(); + assert!(modis.proj.contains("+R=6371007.181")); + assert_eq!( + builtin_lookup(990_002).map(|e| e.kind), + Some(CrsKind::Projected) + ); + } + + #[test] + fn builtin_codes_list_nodedb_first_then_epsg_by_kind() { + let projected: Vec = builtin_codes(CrsKind::Projected) + .take(3) + .map(Srid::code) + .collect(); + assert_eq!(&projected[..2], &[990_001, 990_002]); + assert!(projected[2] < 900_000); + assert!(builtin_codes(CrsKind::Geographic).any(|s| s == Srid::WGS84)); + assert!(!builtin_codes(CrsKind::Geographic).any(|s| s.code() == 32649)); + assert!(!builtin_codes(CrsKind::Projected).any(|s| s == Srid::WGS84)); + } + + #[test] + fn geographic_crs_on_a_datum() { + assert_eq!(geographic_crs_on_datum(6326), Some(Srid::WGS84)); + assert_eq!(geographic_crs_on_datum(6742).map(Srid::code), Some(4742)); + assert_eq!(geographic_crs_on_datum(1), None); + } + + /// Every row of both EPSG files is well formed, the codes ascend + /// strictly, and no code is in both files. + #[test] + fn epsg_tables_are_well_formed_and_sorted() { + for table in [EPSG_TSV, EPSG_SUPPLEMENT_TSV] { + let mut previous = 0u32; + let mut rows = 0usize; + for line in table_rows(table) { + let entry = parse_row(line).unwrap_or_else(|| panic!("malformed row: {line}")); + assert!(entry.code > previous, "unsorted at {}", entry.code); + previous = entry.code; + entry.area_of_use().unwrap(); + entry.accuracy().unwrap(); + entry.datum_code().unwrap(); + assert_eq!(lookup_in(table, entry.code), Some(entry)); + assert_eq!(builtin_lookup(entry.code), Some(entry)); + rows += 1; + } + assert!(rows > 0); + assert!(table.starts_with("# EPSG ")); + } + for line in table_rows(EPSG_SUPPLEMENT_TSV) { + let code = parse_row(line).map(|e| e.code).unwrap_or_default(); + assert!( + lookup_in(EPSG_TSV, code).is_none(), + "{code} is in both files" + ); + } + let codes: Vec = epsg_entries().map(|e| e.code).collect(); + assert!(codes.windows(2).all(|w| w[0] < w[1]), "merged codes ascend"); + assert_eq!( + codes.len(), + table_rows(EPSG_TSV).count() + table_rows(EPSG_SUPPLEMENT_TSV).count() + ); + } + + /// GDM2020 resolves from the supplement with its EPSG datum, 1471. + #[test] + fn gdm2020_is_built_in() { + let gdm = builtin_lookup(12287).unwrap(); + assert_eq!(gdm.kind, CrsKind::Geographic); + assert_eq!(gdm.datum_code(), Ok(Some(1471))); + assert_eq!(geographic_crs_on_datum(1471).map(Srid::code), Some(12287)); + for code in 25314..=25322 { + let grid = builtin_lookup(code).unwrap(); + assert_eq!(grid.kind, CrsKind::Projected, "{code}"); + assert!(grid.name.starts_with("GDM2020 / "), "{code}"); + assert!(builtin_codes(CrsKind::Projected).any(|s| s.code() == code)); + } + } + + /// Every EPSG row parses to a definition of its listed kind. + #[test] + fn every_epsg_row_parses() { + for line in table_rows(EPSG_TSV).chain(table_rows(EPSG_SUPPLEMENT_TSV)) { + let entry = parse_row(line).unwrap_or_else(|| panic!("malformed row: {line}")); + let Ok(srid) = Srid::new(entry.code) else { + panic!("code out of range: {}", entry.code); + }; + match parse_proj_string(srid, entry.name, entry.proj) { + Ok(def) => assert_eq!(def.kind, entry.kind, "{line}"), + Err(e) => panic!("{}: {e}", entry.code), + } + } + } +} diff --git a/nodedb-crs/src/registry/context.rs b/nodedb-crs/src/registry/context.rs new file mode 100644 index 000000000..0654a11d7 --- /dev/null +++ b/nodedb-crs/src/registry/context.rs @@ -0,0 +1,331 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Per-statement CRS resolution. + +use core::cell::RefCell; +use std::sync::Arc; + +use nodedb_srid::{SridResolver, code_from_name}; + +use super::builtin::builtin_lookup; +use super::custom::{CustomCrsSet, CustomCrsState}; +use crate::def::{CrsDef, CrsKind}; +use crate::error::{CrsError, UnknownSrid}; +use crate::srid::Srid; +use crate::transform::Transformer; + +/// The user-defined set a context resolves against: borrowed for the +/// context's life, or shared through an `Arc`. +#[derive(Debug, Clone)] +enum CustomSource<'a> { + Borrowed(&'a CustomCrsSet), + Shared(Arc), +} + +impl CustomSource<'_> { + fn set(&self) -> &CustomCrsSet { + match self { + CustomSource::Borrowed(set) => set, + CustomSource::Shared(set) => set, + } + } +} + +/// Resolves SRIDs for one statement: user-defined CRSs first, then the +/// built-in table. Parsed built-ins are memoized for the context's life. +/// +/// One context serves one statement. It never crosses a plane. +#[derive(Debug)] +pub struct CrsContext<'a> { + custom: CustomSource<'a>, + memo: RefCell)>>, +} + +impl CrsContext<'static> { + /// A context with no user-defined CRSs. + pub fn builtin_only() -> CrsContext<'static> { + CrsContext::new(CustomCrsSet::empty()) + } + + /// A context that owns a share of `custom`, so it borrows nothing and a + /// caller holds it across any other borrow. + pub fn shared(custom: Arc) -> CrsContext<'static> { + CrsContext::with_source(CustomSource::Shared(custom)) + } +} + +impl<'a> CrsContext<'a> { + pub fn new(custom: &'a CustomCrsSet) -> Self { + CrsContext::with_source(CustomSource::Borrowed(custom)) + } + + fn with_source(custom: CustomSource<'a>) -> Self { + Self { + custom, + memo: RefCell::new(Vec::new()), + } + } + + /// The user-defined CRSs this context resolves first. + pub fn custom_set(&self) -> &CustomCrsSet { + self.custom.set() + } + + /// A context on the same user-defined set with its own memo. A scan + /// thread takes a fork, since a context is not `Sync`. + pub fn fork(&self) -> CrsContext<'a> { + CrsContext::with_source(self.custom.clone()) + } + + /// The definition of `srid`. + pub fn resolve(&self, srid: Srid) -> Result, UnknownSrid> { + let code = srid.code(); + if let Some(state) = self.custom_set().get(srid) { + return match state { + CustomCrsState::Live(def) => Ok(self.memoize(srid, || (**def).clone())), + CustomCrsState::Dropped => Err(UnknownSrid::Dropped { code }), + }; + } + if let Some(def) = self.memo.borrow().iter().find(|(s, _)| *s == srid) { + return Ok(Arc::clone(&def.1)); + } + let entry = builtin_lookup(code).ok_or(UnknownSrid::Undefined { code })?; + let def = entry + .definition(srid) + .map_err(|cause| UnknownSrid::Unsupported { code, cause })?; + Ok(self.memoize(srid, || def)) + } + + /// The memoized definition of `srid`, built by `make` on first use. + fn memoize(&self, srid: Srid, make: impl FnOnce() -> CrsDef) -> Arc { + let mut memo = self.memo.borrow_mut(); + if let Some((_, def)) = memo.iter().find(|(s, _)| *s == srid) { + return Arc::clone(def); + } + let def = Arc::new(make()); + memo.push((srid, Arc::clone(&def))); + def + } + + /// Check `code` names a defined CRS, without parsing it. + pub fn resolve_code(&self, code: i64) -> Result { + let code = u32::try_from(code).map_err(|_| UnknownSrid::OutOfRange { code })?; + self.resolve_u32(code) + } + + /// Check `code` names a defined CRS, without parsing it. + pub fn resolve_u32(&self, code: u32) -> Result { + let srid = Srid::new(code).map_err(|_| UnknownSrid::OutOfRange { + code: i64::from(code), + })?; + match self.custom_set().get(srid) { + Some(CustomCrsState::Live(_)) => Ok(srid), + Some(CustomCrsState::Dropped) => Err(UnknownSrid::Dropped { code: srid.code() }), + None => builtin_lookup(srid.code()) + .map(|_| srid) + .ok_or(UnknownSrid::Undefined { code: srid.code() }), + } + } + + /// Resolve a CRS name: `EPSG:n`, `NODEDB:n`, `urn:ogc:def:crs:EPSG:[v]:n`, + /// `http://www.opengis.net/def/crs/EPSG/0/n`, the OGC CRS84 URN or URL + /// (WGS 84), or `SR-ORG:6842` (MODIS sinusoidal). + pub fn resolve_name(&self, name: &str) -> Result { + let code = + code_from_name(name).ok_or_else(|| UnknownSrid::UnknownName { name: name.into() })?; + self.resolve_code(code) + } + + /// Whether `srid` is geographic or projected, without parsing a + /// built-in definition. + pub fn kind(&self, srid: Srid) -> Result { + let code = srid.code(); + match self.custom_set().get(srid) { + Some(CustomCrsState::Live(def)) => Ok(def.kind), + Some(CustomCrsState::Dropped) => Err(UnknownSrid::Dropped { code }), + None => builtin_lookup(code) + .map(|e| e.kind) + .ok_or(UnknownSrid::Undefined { code }), + } + } + + /// A transformer from `from` to `to`. [`Transformer::apply_at`] maps + /// coordinates at a given coordinate epoch, for time-dependent datum + /// shifts. + pub fn transformer(&self, from: Srid, to: Srid) -> Result { + let source = self.resolve(from)?; + let target = self.resolve(to)?; + Ok(Transformer::new(source, target)) + } +} + +impl SridResolver for CrsContext<'_> { + fn resolve_u32(&self, code: u32) -> Result { + CrsContext::resolve_u32(self, code) + } + + fn kind(&self, srid: Srid) -> Result { + CrsContext::kind(self, srid) + } + + fn resolve_name(&self, name: &str) -> Result { + CrsContext::resolve_name(self, name) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::method::ProjectionDef; + use crate::parse::parse_proj_string; + + fn custom_set() -> CustomCrsSet { + let mut set = CustomCrsSet::new(); + let live = Srid::new(910_001).unwrap(); + set.define(parse_proj_string(live, "user", "+proj=tmerc +lon_0=102").unwrap()) + .unwrap(); + let dropped = Srid::new(910_002).unwrap(); + set.define(parse_proj_string(dropped, "gone", "+proj=longlat").unwrap()) + .unwrap(); + set.drop_crs(dropped).unwrap(); + set + } + + #[test] + fn custom_live_and_dropped() { + let set = custom_set(); + let ctx = CrsContext::new(&set); + let live = ctx.resolve(Srid::new(910_001).unwrap()).unwrap(); + assert_eq!(&*live.name, "user"); + assert_eq!( + ctx.resolve(Srid::new(910_002).unwrap()), + Err(UnknownSrid::Dropped { code: 910_002 }) + ); + assert_eq!( + ctx.resolve_code(910_002), + Err(UnknownSrid::Dropped { code: 910_002 }) + ); + assert_eq!(ctx.resolve_code(910_001).map(Srid::code), Ok(910_001)); + assert_eq!( + ctx.kind(Srid::new(910_001).unwrap()), + Ok(CrsKind::Projected) + ); + } + + #[test] + fn shared_context_resolves_its_set() { + let ctx = CrsContext::shared(Arc::new(custom_set())); + assert_eq!(ctx.resolve_u32(910_001).map(Srid::code), Ok(910_001)); + assert_eq!( + ctx.resolve_u32(910_002), + Err(UnknownSrid::Dropped { code: 910_002 }) + ); + let fork = ctx.fork(); + assert_eq!(fork.custom_set().len(), 2); + let resolver: &dyn SridResolver = &ctx; + assert_eq!(resolver.resolve_name("EPSG:4326"), Ok(Srid::WGS84)); + assert_eq!( + resolver.kind(Srid::new(910_001).unwrap()), + Ok(CrsKind::Projected) + ); + } + + #[test] + fn memo_returns_the_same_arc() { + let set = custom_set(); + let ctx = CrsContext::new(&set); + let srid = Srid::new(910_001).unwrap(); + let a = ctx.resolve(srid).unwrap(); + let b = ctx.resolve(srid).unwrap(); + assert!(Arc::ptr_eq(&a, &b)); + } + + #[test] + fn codes_out_of_range_and_undefined() { + let ctx = CrsContext::builtin_only(); + assert_eq!( + ctx.resolve_code(0), + Err(UnknownSrid::OutOfRange { code: 0 }) + ); + assert_eq!( + ctx.resolve_code(-4326), + Err(UnknownSrid::OutOfRange { code: -4326 }) + ); + assert_eq!( + ctx.resolve_code(i64::from(Srid::MAX_CODE) + 1), + Err(UnknownSrid::OutOfRange { + code: i64::from(Srid::MAX_CODE) + 1 + }) + ); + assert_eq!( + ctx.resolve_code(910_777), + Err(UnknownSrid::Undefined { code: 910_777 }) + ); + assert!(matches!( + ctx.resolve_name("ESRI:102100"), + Err(UnknownSrid::UnknownName { .. }) + )); + } + + #[test] + fn builtin_wgs84_and_names() { + let ctx = CrsContext::builtin_only(); + let def = ctx.resolve(Srid::WGS84).unwrap(); + assert!(def.is_geographic()); + assert_eq!( + ctx.resolve_name("urn:ogc:def:crs:OGC:1.3:CRS84"), + Ok(Srid::WGS84) + ); + assert_eq!(ctx.resolve_name("SR-ORG:6842"), Ok(Srid::MODIS_SINUSOIDAL)); + assert_eq!(ctx.resolve_name("EPSG:32648").map(Srid::code), Ok(32648)); + } + + /// NodeDB's own built-ins resolve: MODIS sinusoidal and Himawari + /// geostationary. + #[test] + fn nodedb_builtins_resolve() { + let ctx = CrsContext::builtin_only(); + assert_eq!(ctx.kind(Srid::MODIS_SINUSOIDAL), Ok(CrsKind::Projected)); + let modis = ctx.resolve(Srid::MODIS_SINUSOIDAL).unwrap(); + assert!(matches!( + modis.projection, + Some(ProjectionDef::Sinusoidal(_)) + )); + assert!(modis.datum.ellipsoid.is_sphere()); + let himawari = ctx.resolve(Srid::new(990_002).unwrap()).unwrap(); + let Some(ProjectionDef::GeostationarySatellite(geos)) = himawari.projection else { + panic!("expected geos: {himawari:?}"); + }; + assert_eq!(geos.h, 35_785_863.0); + assert_eq!(geos.lon0_deg, 140.7); + // The sub-satellite point maps to the grid origin. + let t = ctx + .transformer(Srid::WGS84, Srid::new(990_002).unwrap()) + .unwrap(); + let origin = t.apply([140.7, 0.0, 0.0]).unwrap(); + assert!( + origin[0].abs() < 1e-6 && origin[1].abs() < 1e-6, + "{origin:?}" + ); + let t = ctx + .transformer(Srid::WGS84, Srid::MODIS_SINUSOIDAL) + .unwrap(); + let origin = t.apply([0.0, 0.0, 0.0]).unwrap(); + assert!( + origin[0].abs() < 1e-6 && origin[1].abs() < 1e-6, + "{origin:?}" + ); + } + + /// A built-in carries its EPSG datum code, so a projected CRS and its + /// geographic base share a datum. + #[test] + fn builtin_datum_codes() { + let ctx = CrsContext::builtin_only(); + let gdm2000 = ctx.resolve(Srid::new(4742).unwrap()).unwrap(); + let rso = ctx.resolve(Srid::new(3375).unwrap()).unwrap(); + assert!(gdm2000.datum.epsg_code.is_some()); + assert_eq!(gdm2000.datum.epsg_code, rso.datum.epsg_code); + assert!(gdm2000.datum.same_datum(&rso.datum)); + } +} diff --git a/nodedb-crs/src/registry/custom.rs b/nodedb-crs/src/registry/custom.rs new file mode 100644 index 000000000..3d4b2066b --- /dev/null +++ b/nodedb-crs/src/registry/custom.rs @@ -0,0 +1,231 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! User-defined CRSs. + +use serde::{Deserialize, Serialize}; + +use crate::def::CrsDef; +use crate::error::CustomCrsError; +use crate::srid::Srid; + +/// What a user-defined code holds. +#[derive( + Debug, + Clone, + PartialEq, + Serialize, + Deserialize, + zerompk::ToMessagePack, + zerompk::FromMessagePack, +)] +pub enum CustomCrsState { + Live(Box), + /// The CRS was dropped. The code stays retired, so stored data that + /// names it fails to resolve instead of changing meaning. + Dropped, +} + +/// One user-defined code and its state. +#[derive( + Debug, + Clone, + PartialEq, + Serialize, + Deserialize, + zerompk::ToMessagePack, + zerompk::FromMessagePack, +)] +pub struct CustomCrs { + pub srid: Srid, + pub state: CustomCrsState, +} + +/// Every user-defined CRS of one database, sorted by SRID with no +/// duplicates. +#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)] +#[serde(try_from = "Vec", into = "Vec")] +pub struct CustomCrsSet { + entries: Vec, +} + +/// The set [`CustomCrsSet::empty`] returns. +static EMPTY: CustomCrsSet = CustomCrsSet::new(); + +/// A decoded entry list with a repeated SRID. +#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] +#[error("SRID {srid} appears twice in the custom CRS set")] +pub struct DuplicateCustomSrid { + pub srid: Srid, +} + +impl CustomCrsSet { + /// An empty set. + pub const fn new() -> Self { + Self { + entries: Vec::new(), + } + } + + /// The empty set, borrowed for the program's life. + pub fn empty() -> &'static CustomCrsSet { + &EMPTY + } + + /// Build a set from entries in any order. Fails on a repeated SRID. + pub fn from_entries(mut entries: Vec) -> Result { + entries.sort_by_key(|e| e.srid); + if let Some(pair) = entries.windows(2).find(|w| w[0].srid == w[1].srid) { + return Err(DuplicateCustomSrid { srid: pair[0].srid }); + } + Ok(Self { entries }) + } + + fn position(&self, srid: Srid) -> Result { + self.entries.binary_search_by_key(&srid, |e| e.srid) + } + + /// The state of `srid`, if the set has ever held it. + pub fn get(&self, srid: Srid) -> Option<&CustomCrsState> { + self.position(srid).ok().map(|i| &self.entries[i].state) + } + + /// Add a live CRS. Its code must be in the user range and never used. + pub fn define(&mut self, def: CrsDef) -> Result<(), CustomCrsError> { + let srid = Srid::new_user(u64::from(def.srid.code()))?; + match self.position(srid) { + Ok(i) => Err(match self.entries[i].state { + CustomCrsState::Live(_) => CustomCrsError::Exists { srid }, + CustomCrsState::Dropped => CustomCrsError::WasDropped { srid }, + }), + Err(i) => { + self.entries.insert( + i, + CustomCrs { + srid, + state: CustomCrsState::Live(Box::new(def)), + }, + ); + Ok(()) + } + } + } + + /// Retire a live CRS. + pub fn drop_crs(&mut self, srid: Srid) -> Result<(), CustomCrsError> { + let i = self + .position(srid) + .map_err(|_| CustomCrsError::NotFound { srid })?; + match self.entries[i].state { + CustomCrsState::Live(_) => { + self.entries[i].state = CustomCrsState::Dropped; + Ok(()) + } + CustomCrsState::Dropped => Err(CustomCrsError::NotFound { srid }), + } + } + + /// Every entry, ascending by SRID. + pub fn entries(&self) -> &[CustomCrs] { + &self.entries + } + + pub fn len(&self) -> usize { + self.entries.len() + } + + pub fn is_empty(&self) -> bool { + self.entries.is_empty() + } +} + +impl TryFrom> for CustomCrsSet { + type Error = DuplicateCustomSrid; + + fn try_from(entries: Vec) -> Result { + CustomCrsSet::from_entries(entries) + } +} + +impl From for Vec { + fn from(set: CustomCrsSet) -> Self { + set.entries + } +} + +impl zerompk::ToMessagePack for CustomCrsSet { + fn write(&self, writer: &mut W) -> zerompk::Result<()> { + self.entries.write(writer) + } +} + +impl<'a> zerompk::FromMessagePack<'a> for CustomCrsSet { + fn read>(reader: &mut R) -> zerompk::Result { + let entries = Vec::::read(reader)?; + CustomCrsSet::from_entries(entries) + .map_err(|e| zerompk::Error::KeyDuplicated(e.srid.to_string())) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::parse::parse_proj_string; + + fn def(code: u32) -> CrsDef { + let srid = Srid::new(code).unwrap(); + parse_proj_string(srid, "user tm", "+proj=tmerc +lon_0=102 +ellps=GRS80").unwrap() + } + + #[test] + fn define_drop_and_refuse_reuse() { + let mut set = CustomCrsSet::new(); + set.define(def(910_002)).unwrap(); + set.define(def(910_001)).unwrap(); + assert_eq!(set.entries()[0].srid.code(), 910_001); + let srid = Srid::new(910_001).unwrap(); + assert_eq!( + set.define(def(910_001)), + Err(CustomCrsError::Exists { srid }) + ); + set.drop_crs(srid).unwrap(); + assert_eq!(set.get(srid), Some(&CustomCrsState::Dropped)); + assert_eq!( + set.define(def(910_001)), + Err(CustomCrsError::WasDropped { srid }) + ); + assert_eq!(set.drop_crs(srid), Err(CustomCrsError::NotFound { srid })); + } + + #[test] + fn refuses_codes_outside_user_range() { + let mut set = CustomCrsSet::new(); + let err = set.define(def(4326)); + assert_eq!( + err, + Err(CustomCrsError::NotUserCode(crate::NotUserSrid { + code: 4326 + })) + ); + } + + #[test] + fn msgpack_round_trip_is_exact() { + let mut set = CustomCrsSet::new(); + set.define(def(910_005)).unwrap(); + set.define(def(910_003)).unwrap(); + set.drop_crs(Srid::new(910_003).unwrap()).unwrap(); + let bytes = zerompk::to_msgpack_vec(&set).unwrap(); + let back: CustomCrsSet = zerompk::from_msgpack(&bytes).unwrap(); + assert_eq!(back, set); + } + + #[test] + fn decode_refuses_duplicates() { + let entry = CustomCrs { + srid: Srid::new(910_001).unwrap(), + state: CustomCrsState::Dropped, + }; + let bytes = zerompk::to_msgpack_vec(&vec![entry.clone(), entry]).unwrap(); + assert!(zerompk::from_msgpack::(&bytes).is_err()); + } +} diff --git a/nodedb-crs/src/registry/mod.rs b/nodedb-crs/src/registry/mod.rs new file mode 100644 index 000000000..ec1792472 --- /dev/null +++ b/nodedb-crs/src/registry/mod.rs @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Where CRS definitions come from: the built-in table and user-defined +//! sets, resolved through a per-statement context. + +pub mod builtin; +pub mod context; +pub mod custom; + +pub use builtin::{ + BuiltinEntry, builtin_codes, builtin_entries, builtin_lookup, geographic_crs_on_datum, + lookup_in, +}; +pub use context::CrsContext; +pub use custom::{CustomCrs, CustomCrsSet, CustomCrsState, DuplicateCustomSrid}; +pub use nodedb_srid::code_from_name; diff --git a/nodedb-crs/src/transform.rs b/nodedb-crs/src/transform.rs new file mode 100644 index 000000000..d0cb0ce7f --- /dev/null +++ b/nodedb-crs/src/transform.rs @@ -0,0 +1,529 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Coordinate transformation between two CRS definitions. +//! +//! Pipeline: source axis order to native east, north, up → source units → +//! metres → inverse projection to (λ, φ, h) on the source ellipsoid (a +//! geographic source reads degrees directly) → prime meridian to Greenwich +//! → datum shift through ECEF and WGS 84 when both datums are known and +//! differ → target prime meridian → forward projection → target units → +//! target axis order. +//! +//! Two datums differ unless [`Datum::same_datum`] holds: equal EPSG datum +//! codes, or one ellipsoid with shifts of the same effect. A projected CRS +//! and its own geographic base share an EPSG datum code, so no shift runs +//! between them even when their `to_wgs84` differ, as in PROJ. +//! +//! A datum with no known shift to WGS 84 is passed through: its geodetic +//! coordinates carry over unchanged, as PROJ's ballpark transformation does. +//! +//! A time-dependent Helmert shift takes its parameters at the coordinate +//! epoch passed to [`Transformer::apply_at`]. [`Transformer::apply`] takes +//! each shift at its own parameter reference epoch, where only the static +//! parameters act. + +use std::sync::Arc; + +use crate::datum::{ecef_to_geodetic, geodetic_to_ecef}; +use crate::def::{CoordinateEpoch, CrsDef, Datum, DatumShift}; +use crate::error::{CrsError, DomainReason}; +use crate::method::Projector; +use crate::method::angle::{wrap_lon_deg, wrap_pi}; +use crate::srid::Srid; + +/// Largest absolute latitude, degrees. +const MAX_LATITUDE_DEG: f64 = 90.0; + +/// Which steps a transform runs. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Path { + /// Source and target are the same CRS. + Identity, + /// One datum, or one side unknown: no ECEF step. + SameDatum, + /// Both datums are known and differ: shift through ECEF and WGS 84. + DatumShift, +} + +/// A prepared transform from one CRS to another. +#[derive(Debug, Clone)] +pub struct Transformer { + source: Arc, + target: Arc, + path: Path, + source_projector: Option, + target_projector: Option, +} + +/// One side of a transform. A side with no projector is geographic. +struct Side<'a> { + def: &'a CrsDef, + projector: Option<&'a Projector>, +} + +impl Transformer { + pub fn new(source: Arc, target: Arc) -> Self { + let path = if Arc::ptr_eq(&source, &target) || *source == *target { + Path::Identity + } else if needs_shift(&source.datum, &target.datum) { + Path::DatumShift + } else { + Path::SameDatum + }; + let prepare = |def: &CrsDef| def.projection.map(|p| p.prepare(&def.datum.ellipsoid)); + Self { + source_projector: prepare(&source), + target_projector: prepare(&target), + source, + target, + path, + } + } + + pub fn source(&self) -> &Arc { + &self.source + } + + pub fn target(&self) -> &Arc { + &self.target + } + + /// Map one coordinate. x, y are in source units (degrees for a + /// geographic CRS), in the source axis order. z is ellipsoidal height + /// in metres. 2D callers pass z = 0. The output follows the target + /// axis order. + /// + /// A time-dependent datum shift runs at its parameter reference epoch. + /// Use [`Transformer::apply_at`] for coordinates at another epoch. + pub fn apply(&self, xyz: [f64; 3]) -> Result<[f64; 3], CrsError> { + self.run(xyz, None) + } + + /// Map one coordinate that refers to coordinate epoch `epoch`. Every + /// time-dependent datum shift takes its parameters at `epoch`, IOGP + /// GN 7-2 §4.2.5. Static shifts and same-datum paths ignore the epoch. + /// Arguments and output are as for [`Transformer::apply`]. + pub fn apply_at(&self, xyz: [f64; 3], epoch: CoordinateEpoch) -> Result<[f64; 3], CrsError> { + self.run(xyz, Some(epoch)) + } + + fn run(&self, xyz: [f64; 3], epoch: Option) -> Result<[f64; 3], CrsError> { + let [x, y, z] = xyz; + let fail = |srid: Srid, reason: DomainReason| CrsError::OutOfDomain { srid, x, y, reason }; + if !(x.is_finite() && y.is_finite() && z.is_finite()) { + return Err(fail(self.source.srid, DomainReason::NonFinite)); + } + if self.path == Path::Identity { + return Ok(xyz); + } + let source = Side { + def: &self.source, + projector: self.source_projector.as_ref(), + }; + let target = Side { + def: &self.target, + projector: self.target_projector.as_ref(), + }; + let [east, north, up] = self.source.axis.to_native(xyz); + let (lam, phi) = + to_geodetic(&source, east, north).map_err(|r| fail(self.source.srid, r))?; + let (lam, phi, h) = match self.path { + Path::Identity | Path::SameDatum => (lam, phi, up), + Path::DatumShift => shift( + &self.source.datum, + &self.target.datum, + epoch, + [lam, phi, up], + ), + }; + let (out_x, out_y) = + from_geodetic(&target, lam, phi).map_err(|r| fail(self.target.srid, r))?; + Ok(self.target.axis.from_native([out_x, out_y, h])) + } +} + +/// Whether a transform between the two datums runs the ECEF step. +fn needs_shift(source: &Datum, target: &Datum) -> bool { + source.is_known() && target.is_known() && !source.same_datum(target) +} + +/// Source coordinates to λ (from Greenwich), φ radians. +fn to_geodetic(side: &Side<'_>, x: f64, y: f64) -> Result<(f64, f64), DomainReason> { + let pm = side.def.datum.prime_meridian_deg; + match side.projector { + None => { + if y.abs() > MAX_LATITUDE_DEG { + return Err(DomainReason::LatitudeRange); + } + Ok(((x + pm).to_radians(), y.to_radians())) + } + Some(p) => { + let m = side.def.unit.metres_per_unit; + let (lam, phi) = p.inverse(x * m, y * m)?; + Ok((lam + pm.to_radians(), phi)) + } + } +} + +/// λ (from Greenwich), φ radians to target coordinates. +fn from_geodetic(side: &Side<'_>, lam: f64, phi: f64) -> Result<(f64, f64), DomainReason> { + let pm = side.def.datum.prime_meridian_deg; + match side.projector { + None => { + let out = (wrap_lon_deg(lam.to_degrees() - pm), phi.to_degrees()); + if out.0.is_finite() && out.1.is_finite() { + Ok(out) + } else { + Err(DomainReason::NonFinite) + } + } + Some(p) => { + let m = side.def.unit.metres_per_unit; + // Oblique methods scale λ − λ0 by a constant, so λ must sit in + // [−π, π] before it reaches them. + let (x, y) = p.forward(wrap_pi(lam - pm.to_radians()), phi)?; + Ok((x / m, y / m)) + } + } +} + +/// Shift λ, φ, h from the source datum to the target datum through ECEF. +/// A time-dependent shift takes its parameters at `epoch`, or at its own +/// reference epoch when `epoch` is `None`. +fn shift( + source: &Datum, + target: &Datum, + epoch: Option, + [lam, phi, h]: [f64; 3], +) -> (f64, f64, f64) { + let local = geodetic_to_ecef(lam, phi, h, &source.ellipsoid); + let wgs84 = match &source.to_wgs84 { + DatumShift::Identity => local, + DatumShift::Helmert(t) => match epoch { + None => t.forward(local), + Some(e) => t.at_epoch(e.year()).forward(local), + }, + }; + let target_xyz = match &target.to_wgs84 { + DatumShift::Identity => wgs84, + DatumShift::Helmert(t) => match epoch { + None => t.inverse(wgs84), + Some(e) => t.at_epoch(e.year()).inverse(wgs84), + }, + }; + ecef_to_geodetic(target_xyz, &target.ellipsoid) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::parse::parse_proj_string; + + /// One millimetre, the tolerance for PROJ reference values. + const MM: f64 = 1e-3; + + /// Metres per degree of latitude, rounded up. Converts a metre + /// tolerance to a conservative degree tolerance. + const METRES_PER_DEGREE: f64 = 111_320.0; + + fn def(code: u32, proj: &str) -> Arc { + let srid = Srid::new(code).unwrap(); + Arc::new(parse_proj_string(srid, "t", proj).unwrap()) + } + + fn wgs84() -> Arc { + def(4326, "+proj=longlat +datum=WGS84 +no_defs") + } + + fn utm48n() -> Arc { + def(32648, "+proj=utm +zone=48 +datum=WGS84 +units=m +no_defs") + } + + fn kertau_geographic() -> Arc { + def( + 4245, + "+proj=longlat +ellps=evrst48 +towgs84=-11,851,5,0,0,0,0 +no_defs", + ) + } + + #[test] + fn wgs84_to_utm48n_and_back() { + let fwd = Transformer::new(wgs84(), utm48n()); + let inv = Transformer::new(utm48n(), wgs84()); + for [lon, lat] in [ + [101.7117, 3.1579], + [105.0, 0.0], + [103.5, -1.5], + [109.0, 6.0], + ] { + let p = fwd.apply([lon, lat, 0.0]).unwrap(); + let back = inv.apply(p).unwrap(); + let tol = MM / METRES_PER_DEGREE; + assert!( + (back[0] - lon).abs() < tol && (back[1] - lat).abs() < tol, + "{lon} {lat} -> {back:?}" + ); + } + // PROJ 9.8.1 via pyproj, EPSG:4326 → EPSG:32648, always_xy. + let klcc = fwd.apply([101.7117, 3.1579, 0.0]).unwrap(); + assert!((klcc[0] - 134_445.592_4).abs() < MM && (klcc[1] - 349_624.727_8).abs() < MM); + } + + #[test] + fn identity_returns_input() { + let t = Transformer::new(utm48n(), utm48n()); + assert_eq!(t.apply([1.0, 2.0, 3.0]), Ok([1.0, 2.0, 3.0])); + } + + #[test] + fn helmert_datum_round_trip_below_a_micrometre() { + let to_kertau = Transformer::new(wgs84(), kertau_geographic()); + let back = Transformer::new(kertau_geographic(), wgs84()); + assert_eq!(to_kertau.path, Path::DatumShift); + let input = [101.7117, 3.1579, 30.0]; + let k = to_kertau.apply(input).unwrap(); + // The shift moves the point by hundreds of metres. + assert!((k[0] - input[0]).abs() > 1e-4 || (k[1] - input[1]).abs() > 1e-4); + let w = back.apply(k).unwrap(); + let tol = 1e-6 / METRES_PER_DEGREE; + assert!( + (w[0] - input[0]).abs() < tol && (w[1] - input[1]).abs() < tol, + "{w:?}" + ); + assert!((w[2] - input[2]).abs() < 1e-6, "{w:?}"); + } + + /// A translation-only shift between datums on one ellipsoid moves the + /// ECEF point by exactly the translation. + #[test] + fn translation_moves_ecef_by_the_vector() { + let src = def(910_001, "+proj=longlat +ellps=WGS84 +towgs84=100,-50,25"); + let t = Transformer::new(src, wgs84()); + let input = [10.0, 45.0, 0.0]; + let out = t.apply(input).unwrap(); + let ell = crate::def::Ellipsoid::WGS84; + let a = geodetic_to_ecef(input[0].to_radians(), input[1].to_radians(), input[2], &ell); + let b = geodetic_to_ecef(out[0].to_radians(), out[1].to_radians(), out[2], &ell); + for (i, want) in [100.0, -50.0, 25.0].into_iter().enumerate() { + assert!((b[i] - a[i] - want).abs() < 1e-6, "{i}: {}", b[i] - a[i]); + } + } + + /// An unknown datum (a sphere with no shift) passes geodetic + /// coordinates through, as PROJ does for MODIS sinusoidal. + #[test] + fn unknown_datum_skips_the_shift() { + let sphere = def(990_100, "+proj=longlat +R=6371007.181 +no_defs"); + let t = Transformer::new(wgs84(), sphere); + assert_eq!(t.path, Path::SameDatum); + let out = t.apply([101.5, 3.25, 7.0]).unwrap(); + assert!( + (out[0] - 101.5).abs() < 1e-12 && (out[1] - 3.25).abs() < 1e-12, + "{out:?}" + ); + assert_eq!(out[2], 7.0); + } + + #[test] + fn units_and_prime_meridian() { + let feet = def(910_002, "+proj=utm +zone=48 +datum=WGS84 +units=us-ft"); + let metres = Transformer::new(wgs84(), utm48n()) + .apply([104.0, 2.0, 0.0]) + .unwrap(); + let in_feet = Transformer::new(wgs84(), feet) + .apply([104.0, 2.0, 0.0]) + .unwrap(); + let m = 1200.0 / 3937.0; + assert!((in_feet[0] * m - metres[0]).abs() < 1e-6); + assert!((in_feet[1] * m - metres[1]).abs() < 1e-6); + let paris = def(910_003, "+proj=longlat +datum=WGS84 +pm=paris"); + let out = Transformer::new(wgs84(), paris) + .apply([2.337_229_166_667, 48.0, 0.0]) + .unwrap(); + assert!(out[0].abs() < 1e-9, "{out:?}"); + } + + #[test] + fn errors_name_the_failing_crs() { + let t = Transformer::new(wgs84(), utm48n()); + let err = t.apply([179.0, 3.0, 0.0]).unwrap_err(); + assert_eq!( + err, + CrsError::OutOfDomain { + srid: Srid::new(32648).unwrap(), + x: 179.0, + y: 3.0, + reason: DomainReason::CentralMeridianOffset + } + ); + let err = t.apply([100.0, 95.0, 0.0]).unwrap_err(); + assert!( + matches!(err, CrsError::OutOfDomain { srid, reason: DomainReason::LatitudeRange, .. } if srid == Srid::WGS84) + ); + assert!(t.apply([f64::NAN, 0.0, 0.0]).is_err()); + } + + /// A GRS 1980 geographic CRS whose shift to WGS 84 is the EPSG 6276 + /// time-dependent Coordinate Frame rotation, IOGP GN 7-2 §4.2.5. + fn time_dependent_geographic() -> Arc { + use crate::def::{Helmert, HelmertRates, RotationConvention}; + let mut d = (*def(910_005, "+proj=longlat +ellps=GRS80 +no_defs")).clone(); + let mut h = Helmert::position_vector([ + -0.084_68, + -0.019_42, + 0.032_01, + -0.000_425_4, + 0.002_257_8, + 0.002_401_5, + 0.009_71, + ]); + h.convention = RotationConvention::CoordinateFrame; + h.rates = Some(HelmertRates::new( + [ + 0.001_42, + 0.001_34, + 0.000_90, + 0.001_546_1, + 0.001_182_0, + 0.001_155_1, + 0.000_109, + ], + 1994.0, + )); + d.datum.to_wgs84 = DatumShift::Helmert(h); + Arc::new(d) + } + + /// PROJ 9.8.1 pipeline: unitconvert, cart GRS80, `+proj=helmert` with + /// the EPSG 6276 parameters and `+t_epoch=1994 + /// +convention=coordinate_frame`, inverse cart WGS84, on (128, −16, + /// 100 m), as (epoch, lon, lat, h). + const PROJ_TIME_DEPENDENT: [[f64; 4]; 3] = [ + [ + 2013.9, + 127.999_993_192_190_9, + -16.000_010_577_429_9, + 100.103_043_247_014_28, + ], + [ + 1994.0, + 127.999_999_906_555_56, + -15.999_999_923_389_051, + 100.088_547_978_550_2, + ], + [ + 2030.25, + 127.999_987_675_614_36, + -16.000_019_330_875_343, + 100.114_952_898_584_3, + ], + ]; + + /// Height agreement with PROJ, metres. + const HEIGHT_TOL_M: f64 = 1e-6; + + #[test] + fn apply_at_matches_proj_time_dependent_helmert() { + let t = Transformer::new(time_dependent_geographic(), wgs84()); + let tol = 1e-6 / METRES_PER_DEGREE; + for [epoch, lon, lat, h] in PROJ_TIME_DEPENDENT { + let at = CoordinateEpoch::new(epoch).unwrap(); + let out = t.apply_at([128.0, -16.0, 100.0], at).unwrap(); + assert!( + (out[0] - lon).abs() < tol && (out[1] - lat).abs() < tol, + "{epoch}: {out:?}" + ); + assert!((out[2] - h).abs() < HEIGHT_TOL_M, "{epoch}: {out:?}"); + let back = Transformer::new(wgs84(), time_dependent_geographic()) + .apply_at(out, at) + .unwrap(); + assert!( + (back[0] - 128.0).abs() < tol && (back[1] + 16.0).abs() < tol, + "{epoch}: {back:?}" + ); + } + } + + /// Without an epoch the shift runs at its reference epoch, 1994.0. + #[test] + fn apply_uses_the_reference_epoch() { + let t = Transformer::new(time_dependent_geographic(), wgs84()); + let input = [128.0, -16.0, 100.0]; + let at_reference = t + .apply_at(input, CoordinateEpoch::new(1994.0).unwrap()) + .unwrap(); + assert_eq!(t.apply(input).unwrap(), at_reference); + let [_, lon, lat, _] = PROJ_TIME_DEPENDENT[1]; + let out = t.apply(input).unwrap(); + let tol = 1e-6 / METRES_PER_DEGREE; + assert!((out[0] - lon).abs() < tol && (out[1] - lat).abs() < tol); + } + + /// A static shift gives the same result at every epoch. + #[test] + fn static_shift_ignores_the_epoch() { + let t = Transformer::new(kertau_geographic(), wgs84()); + let input = [101.7117, 3.1579, 30.0]; + let at = CoordinateEpoch::new(2150.0).unwrap(); + assert_eq!(t.apply_at(input, at), t.apply(input)); + } + + /// EPSG:2046 Hartebeesthoek94 / Lo15 is west-south: x is westing and y + /// southing. PROJ 9.8.1 reference row from the fixture. + #[test] + fn axis_order_applies_on_both_sides() { + let lo15 = def( + 2046, + "+proj=tmerc +axis=wsu +lat_0=0 +lon_0=15 +k=1 +x_0=0 +y_0=0 +ellps=WGS84 \ + +towgs84=0,0,0 +units=m +no_defs", + ); + let fwd = Transformer::new(wgs84(), Arc::clone(&lo15)); + let p = fwd.apply([14.475, -22.915, 0.0]).unwrap(); + assert!((p[0] - 53_858.502_884_905_3).abs() < MM, "{p:?}"); + assert!((p[1] - 2_535_220.466_812_430_4).abs() < MM, "{p:?}"); + let back = Transformer::new(lo15, wgs84()).apply(p).unwrap(); + let tol = MM / METRES_PER_DEGREE; + assert!( + (back[0] - 14.475).abs() < tol && (back[1] + 22.915).abs() < tol, + "{back:?}" + ); + let neu = def(910_004, "+proj=longlat +axis=neu +datum=WGS84"); + let swapped = Transformer::new(wgs84(), neu) + .apply([101.5, 3.25, 7.0]) + .unwrap(); + assert_eq!(swapped, [3.25, 101.5, 7.0]); + } + + /// Equal EPSG datum codes skip the shift although the `+towgs84` + /// values differ. Different codes run it. + #[test] + fn epsg_datum_code_decides_the_path() { + let coded = |code: u32, text: &str, datum: u32| { + let mut d = (*def(code, text)).clone(); + d.datum.epsg_code = Some(datum); + Arc::new(d) + }; + let base = coded( + 4230, + "+proj=longlat +ellps=intl +towgs84=-87,-98,-121", + 6230, + ); + let grid = coded( + 23031, + "+proj=utm +zone=31 +ellps=intl +towgs84=-84,-107,-120 +units=m", + 6230, + ); + assert_eq!( + Transformer::new(Arc::clone(&base), Arc::clone(&grid)).path, + Path::SameDatum + ); + let other = coded( + 4231, + "+proj=longlat +ellps=intl +towgs84=-83,-96,-113", + 6231, + ); + assert_eq!(Transformer::new(other, grid).path, Path::DatumShift); + let wgs = coded(4326, "+proj=longlat +datum=WGS84", 6326); + assert_eq!(Transformer::new(base, wgs).path, Path::DatumShift); + } +} diff --git a/nodedb-crs/tests/fixtures/proj_reference.tsv b/nodedb-crs/tests/fixtures/proj_reference.tsv new file mode 100644 index 000000000..b24575577 --- /dev/null +++ b/nodedb-crs/tests/fixtures/proj_reference.tsv @@ -0,0 +1,28755 @@ +# EPSG v12.029 PROJ 9.8.1 pyproj 3.8.0 +# Built from the PROJ strings in epsg.tsv and epsg_supplement.tsv. +# Forward rows use the CRS's own geographic base. +# A forward row applies no datum shift, even when the base row has another +towgs84. +src dst x_in y_in x_out y_out +4600 2000 -63.07 18.22 286871.6885770898 2014479.544391806 +4600 2000 -63.205 18.121 272525.0337998599 2003614.964394131 +4600 2000 -62.935 18.319 301202.02260099887 2025353.9771599248 +4601 2001 -61.78 17.08 423405.7820363475 1888064.0945938756 +2001 4326 423405.7820363475 1888064.0945938756 -61.782177573252035 17.079129419744902 +4601 2001 -61.933 16.954 407132.8806060881 1874115.825936658 +2001 4326 407132.8806060881 1874115.825936658 -61.9351788008882 16.953137646164045 +4601 2001 -61.627 17.206 439656.8483911668 1902025.543732126 +2001 4326 439656.8483911668 1902025.543732126 -61.629176338918384 17.20512124466366 +4602 2002 -61.375 15.415 467056.70287295897 1704003.7804212302 +2002 4326 467056.70287295897 1704003.7804212302 -61.36601388739811 15.41866513748327 +4602 2002 -61.5325 15.1675 450216.9347002575 1676589.8143592475 +2002 4326 450216.9347002575 1676589.8143592475 -61.52353095315326 15.171190900996944 +4602 2002 -61.2175 15.6625 483855.6544691729 1731432.1272333697 +2002 4326 483855.6544691729 1731432.1272333697 -61.20849668213522 15.666139190720717 +4603 2003 -61.595 12.254999999999999 444042.86963298905 1354527.1457709942 +2003 4326 444042.86963298905 1354527.1457709942 -61.593483539010286 12.254809513825945 +4603 2003 -61.8155 11.971499999999999 420085.0323471887 1323157.4856262794 +2003 4326 420085.0323471887 1323157.4856262794 -61.81399058093881 11.971333297838946 +4603 2003 -61.374500000000005 12.5385 467949.0174421381 1385918.6111268182 +2003 4326 467949.0174421381 1385918.6111268182 -61.37297646755198 12.538285770483846 +4604 2004 -62.185 16.745 380283.0729363072 1851007.6261405195 +2004 4326 380283.0729363072 1851007.6261405195 -62.18198564419486 16.74703543443475 +4604 2004 -62.2795 16.6325 370193.9665940874 1838576.554911524 +2004 4326 370193.9665940874 1838576.554911524 -62.27649106438744 16.63454501324665 +4604 2004 -62.0905 16.8575 390360.38412486436 1863443.5096490711 +2004 4326 390360.38412486436 1863443.5096490711 -62.08748021426472 16.859525848044576 +4605 2005 -62.71 17.259999999999998 324534.54176759796 1908098.9220918003 +2005 4326 324534.54176759796 1908098.9220918003 -62.70913574478199 17.260676265203205 +4605 2005 -62.899 17.08 304352.38924343575 1888271.3011303751 +2005 4326 304352.38924343575 1888271.3011303751 -62.898141494964555 17.08069112206941 +4605 2005 -62.521 17.44 344677.76826740603 1927945.2168554221 +2005 4326 344677.76826740603 1927945.2168554221 -62.52012998434602 17.440661433290384 +4606 2006 -60.975 13.91 510729.9677754257 1537720.0617661602 +2006 4326 510729.9677754257 1537720.0617661602 -60.97555101042338 13.91167519569839 +4606 2006 -61.1145 13.685 495750.4537367327 1512778.081879602 +2006 4326 495750.4537367327 1512778.081879602 -61.11505516285379 13.686692289461998 +4606 2006 -60.8355 14.135 525679.2217463588 1562673.2494545372 +2006 4326 525679.2217463588 1562673.2494545372 -60.836046855151125 14.136658150792003 +4607 2007 -61.295 12.989999999999998 476449.87260741374 1435863.4024487808 +2007 4326 476449.87260741374 1435863.4024487808 -61.29194590886501 12.991440257494165 +4607 2007 -61.4975 12.584999999999999 454577.18235072494 1391031.3758896766 +2007 4326 454577.18235072494 1391031.3758896766 -61.494457221084474 12.586474375696648 +4607 2007 -61.0925 13.395 498248.4532729144 1480717.7982985815 +2007 4326 498248.4532729144 1480717.7982985815 -61.08943444612674 13.396406121692596 +4609 2009 -58.55 51.105000000000004 301298.277109251 5662983.064347334 +4609 2009 -59.855 50.2905 208247.00563102035 5573262.034878505 +4609 2009 -57.245000000000005 51.9195 391138.0816868866 5754337.279559224 +4609 2010 -61.5 49.584999999999994 304800.0 5493920.120368958 +4609 2010 -62.85 47.402499999999996 202905.82215561616 5252134.621448245 +4609 2010 -60.15 51.7675 397986.70179898554 5737544.765922811 +4609 2011 -64.5 54.185 304800.0 6005690.804414152 +4609 2011 -65.85 48.5735 205185.33552386172 5382321.191415609 +4609 2011 -63.15 59.7965 380584.0533583963 6631302.213192502 +4609 2012 -67.5 53.155 304800.0 5891063.468818495 +4609 2012 -68.85 47.8945 203858.3987639225 5306829.886663586 +4609 2012 -66.15 58.4155 383694.3598077466 6477495.744214486 +4609 2013 -70.5 53.405 304800.0 5918883.792504978 +4609 2013 -71.85 45.8495 199948.98783813813 5079518.391728662 +4609 2013 -69.15 60.960499999999996 377927.7750313737 6760964.873775857 +4609 2014 -73.5 53.760000000000005 304800.0 5958390.687442133 +4609 2014 -74.85 45.867000000000004 199981.87891334924 5081463.269145968 +4609 2014 -72.15 61.653 376332.89322991413 6838115.515753451 +4609 2015 -76.5 53.995 304800.0 5984544.449073666 +4609 2015 -77.85 46.232499999999995 200671.07792590704 5122084.730091778 +4609 2015 -75.15 61.75749999999999 376091.29615050694 6849758.398583927 +4609 2016 -78.925 54.34 342195.53480076825 6023094.742529878 +4609 2016 -79.7575 47.041 285231.9250245699 5211097.411062304 +4609 2016 -78.0925 61.639 379413.0617455402 6836620.268004988 +4608 2017 -74.675 45.315 212670.54414528905 5019904.034246545 +4608 2017 -74.9675 45.01349999999999 189127.58309621044 4986776.574974535 +4608 2017 -74.3825 45.6165 235972.7755545804 5053116.556020024 +4608 2018 -76.5 44.94 304800.0 4977561.476425124 +4608 2018 -77.85 43.761 196095.23408615243 4847452.23384974 +4608 2018 -75.15 46.119 409143.39952444297 5109470.138009823 +4608 2019 -79.495 44.795 305195.6094941829 4961449.500818744 +4608 2019 -80.8495 42.5135 193903.38767093729 4708872.95543081 +4608 2019 -78.14049999999999 47.076499999999996 408042.6273555514 5215908.426183349 +4608 2020 -82.3 43.835 320884.4823253863 4854806.936800196 +4608 2020 -83.47 41.8865 224297.52074379657 4638807.840996753 +4608 2020 -81.13 45.783500000000004 411330.146989047 5072209.938851471 +4608 2021 -81.0 50.605000000000004 304800.0 5607364.5002996335 +4608 2021 -82.35 46.4605 201103.16509352683 5147425.782717796 +4608 2021 -79.65 54.7495 391721.20708874875 6069358.050988529 +4608 2022 -84.0 50.795 304800.0 5628498.5319415415 +4608 2022 -85.35 46.4795 201139.24709312624 5149537.580895894 +4608 2022 -82.65 55.1105 390946.2223932393 6109538.022881932 +4608 2023 -87.0 51.935 304800.0 5755317.3860620195 +4608 2023 -88.35 47.6465 203377.29638031535 5279259.352450241 +4608 2023 -85.65 56.2235 388535.24076887575 6233431.366719592 +4608 2024 -90.0 52.435 304800.0 5810947.584176097 +4608 2024 -91.35 48.4165 204877.2579740556 5364865.150797635 +4608 2024 -88.65 56.4535 388033.00060203194 6259036.4505017465 +4608 2025 -93.0 51.63 304800.0 5721385.323461537 +4608 2025 -94.35 48.417 204878.23791363457 5364920.742557531 +4608 2025 -91.65 54.843 391520.8173358601 6079764.554609955 +4608 2026 -94.83 50.965 386986.40777427686 5648060.372518874 +4608 2026 -95.127 48.9175 368779.73109343176 5420059.189522774 +4608 2026 -94.533 53.0125 403252.4708007841 5876213.266574059 +4608 2027 -92.58 52.115 528760.2041967438 5773694.12477007 +4608 2027 -94.902 48.438500000000005 359324.4584081748 5366567.993567605 +4608 2027 -90.258 55.7915 671914.2198365786 6186063.277210205 +4608 2028 -87.0 51.504999999999995 500000.0000000009 5705766.842996875 +4608 2028 -89.7 46.649499999999996 293390.44010320306 5169538.375556644 +4608 2028 -84.3 56.3605 666804.5461502231 6249263.079070621 +4608 2029 -81.0 48.519999999999996 500000.0 5373879.716751074 +4608 2029 -83.7 42.355000000000004 277625.6634909289 4692510.00420456 +4608 2029 -78.3 54.684999999999995 674050.3648086437 6062871.130675041 +4608 2030 -76.175 44.94 407292.66804562527 4976739.579814981 +4608 2030 -77.8175 43.761 273194.0510148079 4848970.902475055 +4608 2030 -74.5325 46.119 536123.0021198352 5107157.520155124 +4609 2031 -78.925 54.34 634899.9106470298 6023120.28657707 +4609 2031 -79.7575 47.041 594391.8880794017 5210250.835287107 +4609 2031 -78.0925 61.639 654055.6178917697 6837203.523625387 +4609 2032 -75.0 53.805 500000.0 5961609.575340553 +4609 2032 -77.7 45.871500000000005 290445.2696218877 5083097.889498504 +4609 2032 -72.3 61.7385 642605.8375542256 6847807.283986892 +4609 2033 -69.0 53.405 500000.0 5917107.949782955 +4609 2033 -71.7 45.8495 290362.5495364092 5080653.692715694 +4609 2033 -66.3 60.960499999999996 646190.2100934563 6761195.6647290485 +4609 2034 -63.0 53.79 500000.0 5959940.70980145 +4609 2034 -65.7 47.82299999999999 297905.1165065209 5299938.936448989 +4609 2034 -60.3 59.757000000000005 651681.01679834 6627230.578658542 +4609 2035 -58.55 51.105000000000004 391481.9437933321 5662425.358903818 +4609 2035 -59.855 50.2905 296634.7407186967 5574611.0718351565 +4609 2035 -57.245000000000005 51.9195 483149.903497044 5751895.033849622 +4141 2039 34.93 31.365000000000002 193412.21592306579 585981.7010273987 +2039 4326 193412.21592306579 585981.7010273987 34.9306939788724 31.36537323141648 +4141 2039 34.246 29.6415 126712.39623179393 395262.90677270875 +2039 4326 126712.39623179393 395262.90677270875 34.246680222561594 29.641887096646848 +4141 2039 35.614 33.0885 257759.23041669643 777145.3288331792 +2039 4326 257759.23041669643 777145.3288331792 35.6147084941712 33.08885933026556 +4142 2040 -4.24 7.69 363238.86914864846 850147.5650600246 +2040 4326 363238.86914864846 850147.5650600246 -4.239604684035248 7.693504586280619 +4142 2040 -5.824 5.197 186911.50093495834 575087.0145870089 +2040 4326 186911.50093495834 575087.0145870089 -5.823638818135821 5.200743491458792 +4142 2040 -2.656 10.183 537680.509712256 1125563.7724574704 +2040 4326 537680.509712256 1125563.7724574704 -2.6555697044572413 10.186262598592485 +4143 2041 -4.24 7.69 363238.86914864846 850147.5650600246 +2041 4326 363238.86914864846 850147.5650600246 -4.239604523213577 7.69350241482906 +4143 2041 -5.824 5.197 186911.50093495834 575087.0145870089 +2041 4326 186911.50093495834 575087.0145870089 -5.823638598480421 5.200741404551574 +4143 2041 -2.656 10.183 537680.509712256 1125563.7724574704 +2041 4326 537680.509712256 1125563.7724574704 -2.655569602979688 10.186260346643934 +4142 2042 -7.305 7.515000000000001 687031.7161541724 830966.9831037621 +4142 2042 -8.4795 4.6125 557734.1149080383 509805.10165693087 +4142 2042 -6.1305 10.4175 814204.2784705246 1152892.716356168 +4143 2043 -7.305 7.515000000000001 687031.7161541724 830966.9831037621 +2043 4326 687031.7161541724 830966.9831037621 -7.304667422011673 7.518521756274736 +4143 2043 -8.4795 4.6125 557734.1149080383 509805.10165693087 +2043 4326 557734.1149080383 509805.10165693087 -8.479193369146312 4.616297853356947 +4143 2043 -6.1305 10.4175 814204.2784705246 1152892.716356168 +2043 4326 814204.2784705246 1152892.716356168 -6.130140413980236 10.420741978234386 +4147 2044 105.07 15.864999999999998 18507497.545308415 1754736.9974448597 +2044 4326 18507497.545308415 1754736.9974448597 105.07042080639204 15.864717625351574 +4147 2044 102.433 9.0835 18217707.789892867 1005502.3341171553 +2044 4326 18217707.789892867 1005502.3341171553 102.43336768327677 9.083096186361018 +4147 2044 107.707 22.646499999999996 18778323.960844956 2507968.0017169593 +2044 4326 18778323.960844956 2507968.0017169593 107.70748280072283 22.64633762780511 +4147 2045 108.765 15.995 19260716.33791353 1770408.0172302916 +4147 2045 108.0765 10.9865 19180348.374315146 1216549.621952728 +4147 2045 109.4535 21.0035 19339196.201987028 2324283.79176053 +4148 2046 14.475 -22.915 53858.5028849053 2535220.4668124304 +2046 4326 53858.5028849053 2535220.4668124304 14.475 -22.914999999999996 +4148 2046 14.362499999999999 -23.1265 65297.99180743733 2558689.4898976833 +2046 4326 65297.99180743733 2558689.4898976833 14.3625 -23.1265 +4148 2046 14.5875 -22.7035 42382.60828390351 2511761.4778066603 +2046 4326 42382.60828390351 2511761.4778066603 14.587499999999999 -22.703500000000002 +4148 2047 17.225 -30.565 -21585.409550544653 3382769.2805914446 +2047 4326 -21585.409550544653 3382769.2805914446 17.225 -30.564999999999994 +4148 2047 16.5275 -32.8465 44233.15521686242 3635823.3454771345 +2047 4326 44233.15521686242 3635823.3454771345 16.5275 -32.84649999999999 +4148 2047 17.9225 -28.2835 -90502.40712720166 3130204.954118362 +2047 4326 -90502.40712720166 3130204.954118362 17.9225 -28.283499999999997 +4148 2048 18.994999999999997 -31.630000000000003 474.3531491869338 3500825.5209269095 +2048 4326 474.3531491869338 3500825.5209269095 18.994999999999997 -31.63 +4148 2048 18.0905 -34.555 83474.82916618735 3825601.9594404716 +2048 4326 83474.82916618735 3825601.9594404716 18.0905 -34.55500000000001 +4148 2048 19.8995 -28.705 -87895.95279966475 3176904.890009175 +2048 4326 -87895.95279966475 3176904.890009175 19.8995 -28.705000000000002 +4148 2049 21.0 -29.82 -0.0 3300160.2309778067 +2049 4326 -0.0 3300160.2309778067 21.0 -29.820000000000004 +4148 2049 20.090999999999998 -34.374 83609.22437651109 3805522.1545581757 +2049 4326 83609.22437651109 3805522.1545581757 20.090999999999998 -34.374 +4148 2049 21.909000000000002 -25.266000000000002 -91567.54216088743 2795830.3288933304 +2049 4326 -91567.54216088743 2795830.3288933304 21.909000000000002 -25.266000000000002 +4148 2050 23.005000000000003 -29.759999999999998 -483.5880101254088 3293509.306791971 +2050 4326 -483.5880101254088 3293509.306791971 23.005000000000003 -29.76 +4148 2050 22.1005 -33.809999999999995 83285.98820567515 3742950.3129432877 +2050 4326 83285.98820567515 3742950.3129432877 22.1005 -33.809999999999995 +4148 2050 23.9095 -25.71 -91281.87324812298 2845021.0119952434 +2050 4326 -91281.87324812298 2845021.0119952434 23.9095 -25.709999999999994 +4148 2051 25.005000000000003 -29.485 -484.9028595597877 3263026.627525048 +2051 4326 -484.9028595597877 3263026.627525048 25.005000000000003 -29.485 +4148 2051 24.1005 -33.7825 83312.62830328572 3739899.9039001586 +2051 4326 83312.62830328572 3739899.9039001586 24.1005 -33.7825 +4148 2051 25.9095 -25.1875 -91676.75354447133 2787134.0300849075 +2051 4326 -91676.75354447133 2787134.0300849075 25.9095 -25.187499999999996 +4148 2052 27.0 -28.375 -0.0 3140000.178563702 +2052 4326 -0.0 3140000.178563702 27.0 -28.375 +4148 2052 26.1 -33.2845 83838.3029900546 3684662.463451074 +2052 4326 83838.3029900546 3684662.463451074 26.1 -33.284499999999994 +4148 2052 27.9 -23.465500000000002 -91953.44761809887 2596378.031059858 +2052 4326 -91953.44761809887 2596378.031059858 27.9 -23.465500000000002 +4148 2053 29.0 -27.58 -0.0 3051898.9956012717 +2053 4326 -0.0 3051898.9956012717 29.000000000000004 -27.580000000000002 +4148 2053 28.090999999999998 -32.485 85440.58032689162 3595998.6295322096 +2053 4326 85440.58032689162 3595998.6295322096 28.091 -32.485 +4148 2053 29.909000000000002 -22.674999999999997 -93417.37379106684 2508832.1991560827 +2053 4326 -93417.37379106684 2508832.1991560827 29.909000000000006 -22.674999999999997 +4148 2054 31.005000000000003 -26.799999999999997 -497.14939590579314 2965469.9145068447 +2054 4326 -497.14939590579314 2965469.9145068447 31.005000000000006 -26.799999999999997 +4148 2054 30.0915 -30.922 86837.8955276258 3422680.357825478 +2054 4326 86837.8955276258 3422680.357825478 30.0915 -30.922 +4148 2054 31.9185 -22.677999999999997 -94391.6882699413 2509170.451118898 +2054 4326 -94391.6882699413 2509170.451118898 31.9185 -22.677999999999994 +4148 2055 32.45 -27.87 54164.284411030145 3084156.884690234 +2055 4326 54164.284411030145 3084156.884690234 32.45 -27.870000000000005 +4148 2055 32.0 -28.833000000000002 97597.78325834683 3191170.747748948 +2055 4326 97597.78325834683 3191170.747748948 32.0 -28.833 +4148 2055 32.900000000000006 -26.907 9933.6440418863 2977329.5582556757 +2055 4326 9933.6440418863 2977329.5582556757 32.900000000000006 -26.906999999999996 +4150 2056 8.225 46.81 2659938.9861671347 1184470.9666418221 +2056 4326 2659938.9861671347 1184470.9666418221 8.223931247363716 46.80870115234433 +4150 2056 6.1775 45.910000000000004 2502076.4878549897 1084921.3267029845 +2056 4326 2502076.4878549897 1084921.3267029845 6.176757645760158 45.908792469374355 +4150 2056 10.2725 47.71 2812536.5028959154 1288060.4848261354 +2056 4326 2812536.5028959154 1288060.4848261354 10.271094893135745 47.70861591021921 +4153 2057 52.605000000000004 27.5 658521.7761583934 3042882.9205706646 +2057 4326 658521.7761583934 3042882.9205706646 52.60510640261842 27.498915746540742 +4153 2057 52.5105 27.401 649176.39173207 3031917.02706183 +2057 4326 649176.39173207 3031917.02706183 52.51060286740864 27.399913448984954 +4153 2057 52.6995 27.599 667850.4521992946 3053856.08245947 +2057 4326 667850.4521992946 3053856.08245947 52.699609944187976 27.59791805047942 +4154 2058 46.015 35.385 592190.1828769579 3916274.5288317744 +2058 4326 592190.1828769579 3916274.5288317744 46.01499150739057 35.38395313559022 +4154 2058 44.228500000000004 31.429499999999997 426675.9212045552 3477512.1017371356 +2058 4326 426675.9212045552 3477512.1017371356 44.2284700958106 31.42837211184905 +4154 2058 47.8015 39.3405 741449.769121952 4358380.831520764 +2058 4326 741449.769121952 4358380.831520764 47.80151512539784 39.339548396939946 +4154 2059 51.0 32.59 500000.0000000018 3605890.493774677 +2059 4326 500000.0000000018 3605890.493774677 51.00013612160107 32.58891895566219 +4154 2059 48.3 26.182 230135.240643544 2898686.049656932 +2059 4326 230135.240643544 2898686.049656932 48.30010038517365 26.180815790586184 +4154 2059 53.7 38.998 733833.0189647672 4320095.973939791 +2059 4326 733833.0189647672 4320095.973939791 53.7001792053778 38.99705838436907 +4154 2060 57.0 31.805 499999.9999999988 3518874.447805136 +2060 4326 499999.9999999988 3518874.447805136 57.000291616548715 31.80392301853071 +4154 2060 54.3 25.9685 229644.78035189136 2875025.35028729 +2060 4326 229644.78035189136 2875025.35028729 54.30024681193959 25.967335753141388 +4154 2060 59.7 37.6415 738235.3726748805 4169538.1746202386 +2060 4326 738235.3726748805 4169538.1746202386 59.700345543684215 37.64053890475722 +4154 2061 61.67 31.04 373072.31783032086 3434845.461130434 +2061 4326 373072.31783032086 3434845.461130434 61.670409876808314 31.03892046176364 +4154 2061 60.167 25.622 215493.75441878097 2836905.59595516 +2061 4326 215493.75441878097 2836905.59595516 60.16738819277016 25.620849277465002 +4154 2061 63.173 36.458 515502.1184465992 4034828.4239340485 +2061 4326 515502.1184465992 4034828.4239340485 63.17343859566934 36.45701759027924 +4903 2062 0.6973750000000005 39.885000000000005 659582.2489917885 587479.4989844582 +4903 2062 -5.044625 36.343500000000006 146999.40662473667 207167.89104002173 +4903 2062 6.439375 43.4265 1121320.1077671025 999187.1283381851 +4818 2065 34.9916666666667 49.394999999999996 1144058.330762452 544115.4743788175 +4818 2065 30.2801666666667 47.896499999999996 1264448.8861158604 909824.6302594767 +4818 2065 39.7031666666667 50.8935 1000407.5204986329 196913.1563122908 +4157 2066 -60.67 11.245000000000001 196190.1160625715 176053.13138314814 +4157 2066 -60.876999999999995 11.0965 83771.44210731283 94434.99017107161 +4157 2066 -60.463 11.3935 308493.6770700626 257751.41340002068 +4158 2067 -61.415 10.445 673495.0504094011 1155062.1506468735 +2067 4326 673495.0504094011 1155062.1506468735 -61.413377639964615 10.446769165856084 +4158 2067 -61.9235 10.0355 617977.9935319175 1109543.188796885 +2067 4326 617977.9935319175 1109543.188796885 -61.92190625276707 10.037263888290695 +4158 2067 -60.9065 10.8545 728870.1719784006 1200692.8175855603 +2067 4326 728870.1719784006 1200692.8175855603 -60.9048489976268 10.856274152946733 +4159 2068 9.66 27.93 264957.80975941103 3090594.117915018 +2068 4326 264957.80975941103 3090594.117915018 9.659205369988957 27.928656351343154 +4159 2068 9.345 25.626 234647.36952465904 2835201.0447796467 +2068 4326 234647.36952465904 2835201.0447796467 9.3442141014776 25.624626360174226 +4159 2068 9.975 30.233999999999998 293849.52832586743 3346169.687146767 +2068 4326 293849.52832586743 3346169.687146767 9.974194971884526 30.232692053829563 +4159 2069 11.0 28.369999999999997 200000.0 3139176.614078398 +2069 4326 200000.0 3139176.614078398 10.99923354514006 28.36867006162357 +4159 2069 10.1 23.996000000000002 108423.37411350451 2654907.8737067953 +2069 4326 108423.37411350451 2654907.8737067953 10.099241267708335 23.994613432940117 +4159 2069 11.9 32.744 284346.6309721004 3624408.0914872186 +2069 4326 284346.6309721004 3624408.0914872186 11.899220691930568 32.742747640514494 +4159 2070 13.0 27.93 200000.0 3090418.8731548646 +2070 4326 200000.0 3090418.8731548646 12.99928416218713 27.928674671544716 +4159 2070 12.1 23.313000000000002 107946.59762881725 2579263.246247439 +2070 4326 107946.59762881725 2579263.246247439 12.099290499666525 23.31161645245924 +4159 2070 13.9 32.547000000000004 284531.76167136244 3602561.6936279256 +2070 4326 284531.76167136244 3602561.6936279256 13.899272581747248 32.54575590978478 +4159 2071 15.0 27.7 200000.0 3064933.119823866 +2071 4326 200000.0 3064933.119823866 14.999333929515991 27.698681433763223 +4159 2071 14.1 23.119 107813.55030156816 2557778.2541597453 +2071 4326 107813.55030156816 2557778.2541597453 14.099337624060167 23.11762335586789 +4159 2071 15.9 32.281 284780.1474667561 3573064.57673603 +2071 4326 284780.1474667561 3573064.57673603 15.899325632653865 32.27976197484992 +4159 2072 17.005000000000003 26.925 200496.5745868574 2979063.454280142 +2072 4326 200496.5745868574 2979063.454280142 17.004387396984857 26.9236798170553 +4159 2072 16.1005 22.951500000000003 107750.80593938466 2539228.1699021705 +2072 4326 107750.80593938466 2539228.1699021705 16.099885303704355 22.95012983014437 +4159 2072 17.9095 30.8985 286949.7824340614 3419784.0601147385 +2072 4326 286949.7824340614 3419784.0601147385 17.9088867452102 30.897246583886584 +4159 2073 19.0 26.855 200000.0 2971307.9428992183 +2073 4326 200000.0 2971307.9428992183 18.999437100372724 26.85368715199311 +4159 2073 18.1 22.0715 107113.41296467702 2441779.2227869253 +2073 4326 107113.41296467702 2441779.2227869253 18.09943646855277 22.070128893946094 +4159 2073 19.9 31.6385 285372.54785007116 3501821.8577331626 +2073 4326 285372.54785007116 3501821.8577331626 19.899433830255955 31.637269329750527 +4159 2074 21.0 26.77 200000.0 2961890.651801325 +2074 4326 200000.0 2961890.651801325 20.999487617207407 26.768693559828957 +4159 2074 20.1 21.163 106531.24140863668 2341184.695926597 +2074 4326 106531.24140863668 2341184.695926597 20.099487664206343 21.161627341066048 +4159 2074 21.9 32.376999999999995 284690.7151290169 3583710.011918356 +2074 4326 284690.7151290169 3583710.011918356 21.899482523613916 32.37579220781244 +4159 2075 23.0 26.235 200000.0 2902619.707991238 +2075 4326 200000.0 2902619.707991238 22.999540471747732 26.233692921343113 +4159 2075 22.1 20.1735 105923.82066154227 2231633.3822638667 +2075 4326 105923.82066154227 2231633.3822638667 22.099538970268934 20.172124845432794 +4159 2075 23.9 32.296499999999995 284765.7239905858 3574783.3602053425 +2075 4326 284765.7239905858 3574783.3602053425 23.899536815408432 32.29529837287091 +4159 2076 24.605 26.07 160479.0649128316 2884400.6506117717 +2076 4326 160479.0649128316 2884400.6506117717 24.60458198391298 26.068695536834333 +4159 2076 24.0605 20.598 102063.10638183577 2278653.146492501 +2076 4326 102063.10638183577 2278653.146492501 24.060085433206122 20.59663325228854 +4159 2076 25.1495 31.541999999999998 214195.69573521355 3490780.745644901 +2076 4326 214195.69573521355 3490780.745644901 25.149074228744542 31.540787871016793 +4159 2077 10.655000000000001 28.369999999999997 662183.9364280155 3139347.9693738846 +2077 4326 662183.9364280155 3139347.9693738846 10.654225412324127 28.368668111712296 +4159 2077 9.4445 23.996000000000002 545214.1605856076 2653890.2228224752 +2077 4326 545214.1605856076 2653890.2228224752 9.443726540936586 23.994610158375842 +4159 2077 11.8655 32.744 768511.9053250477 3626596.045152006 +2077 4326 768511.9053250477 3626596.045152006 11.864719832526506 32.74274742589733 +4159 2078 15.005 27.785000000000004 500492.60580437584 3073429.2807531436 +2078 4326 500492.60580437584 3073429.2807531436 15.00433353411363 27.783682664759894 +4159 2078 12.3005 23.0375 223344.61700935662 2550256.0938288346 +2078 4326 223344.61700935662 2550256.0938288346 12.299796531165871 23.036114820888585 +4159 2078 17.709500000000002 32.5325 754487.9505355189 3602753.7998491894 +2078 4326 754487.9505355189 3602753.7998491894 17.70887066942191 32.531276187976054 +4159 2079 20.994999999999997 26.25 499500.6545082105 2903410.0875388095 +2079 4326 499500.6545082105 2903410.0875388095 20.994489793651876 26.24868636001052 +4159 2079 18.290499999999998 20.175 216794.05272419978 2233185.77713441 +2079 4326 216794.05272419978 2233185.77713441 18.28994807273601 20.17361388733475 +4159 2079 23.6995 32.325 754130.2776536702 3579717.0999777634 +2079 4326 754130.2776536702 3579717.0999777634 23.699031245279052 32.32379820401749 +4159 2080 24.605 26.07 260403.0071543721 2885676.948278699 +2080 4326 260403.0071543721 2885676.948278699 24.60458198391298 26.068695536834326 +4159 2080 24.0605 20.598 193576.3300165864 2280454.1056873207 +2080 4326 193576.3300165864 2280454.1056873207 24.060085433206122 20.596633252288537 +4159 2080 25.1495 31.541999999999998 324325.60071612033 3491208.0929650413 +2080 4326 324325.60071612033 3491208.0929650413 25.149074228744542 31.540787871016793 +4160 2081 -69.0 -37.66 2500000.0 5832459.23022921 +2081 4326 2500000.0 5832459.23022921 -68.99916351563444 -37.659120876106186 +4160 2081 -70.35900000000001 -42.834999999999994 2388883.862737214 5256917.199956322 +2081 4326 2388883.862737214 5256917.199956322 -70.3581405227568 -42.834265428687324 +4160 2081 -67.64099999999999 -32.485 2627746.2693358697 6405785.476568531 +2081 4326 2627746.2693358697 6405785.476568531 -67.64017753480779 -32.484004435192965 +4161 2082 -68.995 -46.415000000000006 2500384.4286414236 4859974.981133953 +2082 4326 2500384.4286414236 4859974.981133953 -68.99780336925514 -46.41368763239112 +4161 2082 -70.3495 -49.947500000000005 2403138.171781007 4466296.062627522 +2082 4326 2403138.171781007 4466296.062627522 -70.35253165240486 -49.94626902067853 +4161 2082 -67.64049999999999 -42.8825 2611071.8346475577 5251639.491236043 +2082 4326 2611071.8346475577 5251639.491236043 -67.64311143648585 -42.88111626547046 +4254 2083 -68.07 -53.745000000000005 2561359.7431948 4044225.667833963 +2083 4326 2561359.7431948 4044225.667833963 -68.06865029152547 -53.744945211985794 +4254 2083 -68.583 -54.7845 2526829.616843695 3928836.317528636 +2083 4326 2526829.616843695 3928836.317528636 -68.58163765110696 -54.78448992498566 +4254 2083 -67.557 -52.7055 2597538.9084653663 4159340.9331456856 +2083 4326 2597538.9084653663 4159340.9331456856 -67.5556619948357 -52.70540133207415 +4254 2084 -67.31 -53.129999999999995 613076.4702859066 4111809.083900963 +2084 4326 613076.4702859066 4111809.083900963 -67.30864993031894 -53.129965228168345 +4254 2084 -68.489 -54.461999999999996 533125.3084201551 3964823.5734128803 +2084 4326 533125.3084201551 3964823.5734128803 -68.48766217434382 -54.46202720501509 +4254 2084 -66.131 -51.798 697833.822823736 4257415.70917476 +2084 4326 697833.822823736 4257415.70917476 -66.1296382602414 -51.7979044606084 +4159 2087 12.155000000000001 27.919999999999998 515251.88946144393 3088393.553101568 +2087 4326 515251.88946144393 3088393.553101568 12.154264055432872 27.918670078601174 +4159 2087 9.5945 23.141 253676.8936371724 2561195.018728406 +2087 4326 253676.8936371724 2561195.018728406 9.593734870961015 23.1396031545289 +4159 2087 14.715499999999999 32.699 754579.5851392292 3621234.414145476 +2087 4326 754579.5851392292 3621234.414145476 14.714792106487005 32.69776348137812 +4223 2088 10.74 35.815 476511.3315614785 3963173.411216192 +2088 4326 476511.3315614785 3963173.411216192 10.740596131266924 35.816514685681724 +4223 2088 8.103 33.4795 230793.19869201427 3707925.5272736223 +2088 4326 230793.19869201427 3707925.5272736223 8.103453040238058 33.48113106385304 +4223 2088 13.376999999999999 38.150499999999994 708289.3298178321 4224887.48436594 +2088 4326 708289.3298178321 4224887.48436594 13.377747582583556 38.15190635700744 +4163 2089 45.004999999999995 14.76 500538.13220164913 1631781.212645938 +2089 4326 500538.13220164913 1631781.212645938 45.004999999999995 14.759999999999998 +4163 2089 42.3005 11.889 205914.94650784833 1315707.5705682698 +2089 4326 205914.94650784833 1315707.5705682698 42.3005 11.888999999999998 +4163 2089 47.7095 17.631 787513.5286613621 1951421.7643301375 +2089 4326 787513.5286613621 1951421.7643301375 47.7095 17.630999999999997 +4163 2090 51.004999999999995 14.225 500539.42441122205 1572610.4271394792 +2090 4326 500539.42441122205 1572610.4271394792 51.004999999999995 14.224999999999998 +4163 2090 48.3005 9.927499999999998 203977.85911960172 1098599.5737113478 +2090 4326 203977.85911960172 1098599.5737113478 48.30050000000001 9.927499999999997 +4163 2090 53.7095 18.5225 786063.8328160088 2050144.3664177973 +2090 4326 786063.8328160088 2050144.3664177973 53.7095 18.5225 +4147 2093 106.535 10.035 558652.5700841213 1109793.6710197562 +2093 4326 558652.5700841213 1109793.6710197562 106.53543432321847 10.034608665418201 +4147 2093 105.5945 9.1305 455427.02786334 1009726.9063143742 +2093 4326 455427.02786334 1009726.9063143742 105.59491835405406 9.130093624717425 +4147 2093 107.4755 10.939499999999999 661305.971003987 1210189.6965115739 +2093 4326 661305.971003987 1210189.6965115739 107.47595037182445 10.9391235360641 +4324 2094 108.27000000000001 9.57 749163.8402299217 1058691.426093197 +2094 4326 749163.8402299217 1058691.426093197 108.27022611111111 9.570017571260275 +4324 2094 106.71300000000001 8.148 578545.3577178553 900728.5813002558 +2094 4326 578545.3577178553 900728.5813002558 106.71322611111111 8.148017547081471 +4324 2094 109.827 10.992 918377.8235041164 1217761.67689089 +2094 4326 918377.8235041164 1217761.67689089 109.82722611111112 10.992017583254547 +4165 2095 -15.205 11.78 477665.01732124627 1302249.743052327 +2095 4326 477665.01732124627 1302249.743052327 -15.203176261377472 11.780340360775643 +4165 2095 -16.6135 10.960999999999999 323683.13533841807 1212153.1259635454 +2095 4326 323683.13533841807 1212153.1259635454 -16.611734476515792 10.961341511980027 +4165 2095 -13.796500000000002 12.599 630729.5232669406 1393106.9218579012 +2095 4326 630729.5232669406 1393106.9218579012 -13.794618434229863 12.599337841954677 +4162 2096 128.825 36.565 184336.53760519394 340770.7264278396 +4162 2096 128.0825 34.697500000000005 115944.7834786083 133958.31980120065 +4162 2096 129.5675 38.4325 249545.006804363 548155.1135912484 +4162 2097 127.0 36.144999999999996 200000.0 294155.88559960783 +4162 2097 126.1 34.1785 117036.63016664276 76374.49585443141 +4162 2097 127.9 38.1115 278920.70216633455 512757.47722328233 +4162 2098 125.265 36.015 223886.02436532968 279765.0356625893 +4162 2098 124.6035 34.1925 163456.51494464334 77632.28684232815 +4162 2098 125.9265 37.8375 281547.1343212763 482369.7237587442 +4286 2099 51.185 25.375 142633.2177365754 99252.07851645934 +4286 2099 50.7395 24.6325 97783.76575786929 16934.927048319136 +4286 2099 51.6305 26.1175 186926.87854073453 181732.87516872483 +4121 2100 23.935000000000002 38.315 494317.67042823415 4240767.862084222 +2100 4326 494317.67042823415 4240767.862084222 23.936708874203983 38.317594059822476 +4121 2100 20.0065 35.2235 136480.92178987793 3905142.6281277435 +2100 4326 136480.92178987793 3905142.6281277435 20.008023100059482 35.22615923566251 +4121 2100 27.863500000000002 41.4065 822952.7705507652 4591091.468444976 +2100 4326 822952.7705507652 4591091.468444976 27.86540796174123 41.40900957768219 +4249 2101 -71.875 10.5 -29490.29354318377 -15802.072659985497 +4249 2101 -72.2125 10.05 -66530.72869426847 -65527.267533736056 +4249 2101 -71.5375 10.95 7446.666113638093 33965.298237651 +4249 2102 -71.875 10.5 170509.70645681623 184197.9273400145 +4249 2102 -72.2125 10.05 133469.27130573153 134472.73246626393 +4249 2102 -71.5375 10.95 207446.6661136381 233965.298237651 +4249 2103 -71.875 10.5 470509.70645681623 484197.9273400145 +4249 2103 -72.2125 10.05 433469.2713057315 434472.73246626393 +4249 2103 -71.5375 10.95 507446.6661136381 533965.298237651 +4249 2104 -71.33500000000001 10.254999999999999 12604.147702670718 -13357.01076601938 +4249 2104 -71.4835 10.0255 -3655.557059384413 -38751.87731337094 +4249 2104 -71.1865 10.4845 28840.618984901375 12045.794501715383 +4167 2105 174.055 -36.555 336515.8147472073 835797.5811507426 +2105 4326 336515.8147472073 835797.5811507426 174.055 -36.55500000000001 +4167 2105 172.19650000000001 -38.7645 176842.8466119753 587691.549900343 +2105 4326 176842.8466119753 587691.549900343 172.19650000000004 -38.76449999999999 +4167 2105 175.9135 -34.3455 505741.1220809753 1080550.2426906875 +2105 4326 505741.1220809753 1080550.2426906875 175.9135 -34.345499999999994 +4167 2106 176.49 -38.175 402093.2239860244 754059.7715592113 +2106 4326 402093.2239860244 754059.7715592113 176.49 -38.175 +4167 2106 175.824 -39.0345 344403.00254808704 658452.496920676 +2106 4326 344403.00254808704 658452.496920676 175.824 -39.03450000000001 +4167 2106 177.15599999999998 -37.3155 461153.9065543371 849234.1856703261 +2106 4326 461153.9065543371 849234.1856703261 177.15599999999998 -37.3155 +4167 2107 177.68 -38.265 382010.7027528192 839880.1135822588 +2107 4326 382010.7027528192 839880.1135822588 177.68 -38.265 +4167 2107 176.825 -38.9625 308078.29999524925 761936.9391458615 +2107 4326 308078.29999524925 761936.9391458615 176.825 -38.96249999999999 +4167 2107 178.535 -37.5675 457375.99763715157 917120.7741934074 +2107 4326 457375.99763715157 917120.7741934074 178.535 -37.5675 +4167 2108 176.935 -39.72 422411.9210425665 792287.852915927 +2108 4326 422411.9210425665 792287.852915927 176.935 -39.72 +4167 2108 175.9135 -40.485 335552.4312443859 707099.9592393189 +2108 4326 335552.4312443859 707099.9592393189 175.9135 -40.485 +4167 2108 177.9565 -38.955 511204.37890825595 876469.5042522687 +2108 4326 511204.37890825595 876469.5042522687 177.9565 -38.955000000000005 +4167 2109 174.56 -39.09 428742.7552384626 805004.9263695315 +2109 4326 428742.7552384626 805004.9263695315 174.56 -39.089999999999996 +4167 2109 173.768 -39.711 360572.7063519611 736011.0275111309 +2109 4326 360572.7063519611 736011.0275111309 173.768 -39.711 +4167 2109 175.352 -38.469 498112.47099398973 873396.6370971012 +2109 4326 498112.47099398973 873396.6370971012 175.352 -38.46900000000001 +4167 2110 175.60500000000002 -39.209999999999994 396977.058561328 833552.8472279198 +2110 4326 396977.058561328 833552.8472279198 175.60500000000002 -39.209999999999994 +4167 2110 174.9525 -39.516 340879.0616182061 799354.8716014384 +2110 4326 340879.0616182061 799354.8716014384 174.9525 -39.516 +4167 2110 176.25750000000002 -38.903999999999996 453564.2343327487 867343.225210219 +2110 4326 453564.2343327487 867343.225210219 176.25750000000002 -38.903999999999996 +4167 2111 175.335 -40.215 386971.0767193924 802980.6489779007 +2111 4326 386971.0767193924 802980.6489779007 175.335 -40.215 +4167 2111 174.4935 -40.8945 316188.99212552386 727060.3580087132 +2111 4326 316188.99212552386 727060.3580087132 174.4935 -40.8945 +4167 2111 176.1765 -39.5355 459185.59891525994 878211.9409771434 +2111 4326 459185.59891525994 878211.9409771434 176.1765 -39.5355 +4167 2112 175.78 -40.980000000000004 411174.6604818671 793914.4421288853 +2112 4326 411174.6604818671 793914.4421288853 175.78 -40.98 +4167 2112 175.087 -41.601 353296.2509260169 724803.3675364014 +2112 4326 353296.2509260169 724803.3675364014 175.087 -41.601 +4167 2112 176.473 -40.359 470146.0743030845 862556.0916986568 +2112 4326 470146.0743030845 862556.0916986568 176.473 -40.359 +4167 2113 174.94 -41.205 413722.7150071147 810661.0779184673 +2113 4326 413722.7150071147 810661.0779184673 174.94 -41.205 +4167 2113 174.562 -41.4705 382091.22293263936 781165.2489710341 +2113 4326 382091.22293263936 781165.2489710341 174.56200000000004 -41.4705 +4167 2113 175.318 -40.939499999999995 445610.29046258825 840017.9082566105 +2113 4326 445610.29046258825 840017.9082566105 175.318 -40.9395 +4167 2114 172.64499999999998 -40.83 397727.20951861335 787198.1098044643 +2114 4326 397727.20951861335 787198.1098044643 172.64499999999998 -40.83 +4167 2114 172.2085 -41.181 361114.8210051538 748114.9499839351 +2114 4326 361114.8210051538 748114.9499839351 172.2085 -41.18099999999999 +4167 2114 173.0815 -40.479 434727.9788308511 826095.4638867199 +2114 4326 434727.9788308511 826095.4638867199 173.0815 -40.479 +4167 2115 173.24 -41.42 395053.7350710565 783832.8193774155 +2115 4326 395053.7350710565 783832.8193774155 173.24 -41.42 +4167 2115 172.484 -42.104 332572.669353496 707541.9310980976 +2115 4326 332572.669353496 707541.9310980976 172.48400000000004 -42.104 +4167 2115 173.996 -40.736 458861.72428613337 859562.8264593633 +2115 4326 458861.72428613337 859562.8264593633 173.996 -40.736 +4167 2116 172.32999999999998 -41.120000000000005 418569.44954677764 818825.424683038 +2116 4326 418569.44954677764 818825.424683038 172.32999999999998 -41.120000000000005 +4167 2116 171.997 -41.453 390650.9542934405 781860.1484035666 +2116 4326 390650.9542934405 781860.1484035666 171.997 -41.452999999999996 +4167 2116 172.66299999999998 -40.787 446770.1079929208 855681.9005236634 +2116 4326 446770.1079929208 855681.9005236634 172.66299999999998 -40.787 +4167 2117 171.84 -41.805 421514.51052110683 800584.6521359016 +2117 4326 421514.51052110683 800584.6521359016 171.84 -41.80500000000001 +4167 2117 171.327 -42.1515 378996.706219042 762099.0498289983 +2117 4326 378996.706219042 762099.0498289983 171.327 -42.1515 +4167 2117 172.353 -41.4585 464491.1374561656 838813.8462108157 +2117 4326 464491.1374561656 838813.8462108157 172.353 -41.4585 +4167 2118 171.95 -42.120000000000005 433100.9656914794 823649.8804467513 +2118 4326 433100.9656914794 823649.8804467513 171.95 -42.120000000000005 +4167 2118 171.23000000000002 -42.678000000000004 373793.8074269163 761694.644153778 +2118 4326 373793.8074269163 761694.644153778 171.23000000000002 -42.678000000000004 +4167 2118 172.67 -41.562 493450.35879395204 885098.4511593182 +2118 4326 493450.35879395204 885098.4511593182 172.67 -41.562 +4167 2119 172.715 -42.52 375759.011409281 818718.8514723941 +2119 4326 375759.011409281 818718.8514723941 172.715 -42.52 +4167 2119 171.9635 -42.907000000000004 314538.4374770057 775238.9138581259 +2119 4326 314538.4374770057 775238.9138581259 171.9635 -42.907000000000004 +4167 2119 173.4665 -42.133 437742.5537982445 861647.9762747823 +2119 4326 437742.5537982445 861647.9762747823 173.4665 -42.133 +4167 2120 173.70499999999998 -41.75 391936.7170346071 777165.1342078419 +2120 4326 391936.7170346071 777165.1342078419 173.70499999999998 -41.75 +4167 2120 173.0255 -42.56 336237.9189663021 686905.6542521232 +2120 4326 336237.9189663021 686905.6542521232 173.0255 -42.56000000000001 +4167 2120 174.3845 -40.94 449057.9607552456 866965.3241650916 +2120 4326 449057.9607552456 866965.3241650916 174.3845 -40.94 +4167 2121 171.14 -42.81999999999999 413107.29130969686 807331.814081461 +2121 4326 413107.29130969686 807331.814081461 171.14 -42.82 +4167 2121 170.46499999999997 -43.189 358157.88618175825 766222.3892498035 +2121 4326 358157.88618175825 766222.3892498035 170.46499999999997 -43.18900000000001 +4167 2121 171.815 -42.45099999999999 468712.81243409147 847996.8031386905 +2121 4326 468712.81243409147 847996.8031386905 171.815 -42.451 +4167 2122 170.05 -43.425 382927.4064350715 764982.5534290344 +2122 4326 382927.4064350715 764982.5534290344 170.05 -43.42499999999999 +4167 2122 169.294 -43.807500000000005 322203.5373181722 722052.1813131393 +2122 4326 322203.5373181722 722052.1813131393 169.294 -43.807500000000005 +4167 2122 170.80599999999998 -43.0425 444422.8140435511 807354.6092302902 +2122 4326 444422.8140435511 807354.6092302902 170.80599999999998 -43.0425 +4167 2123 169.015 -44.035 432776.1459193682 793560.6091429461 +2123 4326 432776.1459193682 793560.6091429461 169.015 -44.035 +4167 2123 168.11950000000002 -44.3635 361209.8380084681 757025.0775782506 +2123 4326 361209.8380084681 757025.0775782506 168.11950000000002 -44.36350000000001 +4167 2123 169.91049999999998 -43.7065 505134.8436494877 829314.4653101808 +2123 4326 505134.8436494877 829314.4653101808 169.91049999999998 -43.706500000000005 +4167 2124 172.245 -43.325 360909.51277603937 829390.8575264853 +2124 4326 360909.51277603937 829390.8575264853 172.245 -43.324999999999996 +4167 2124 171.2235 -43.8965 279204.46235164837 764908.3673870876 +2124 4326 279204.46235164837 764908.3673870876 171.2235 -43.89650000000001 +4167 2124 173.2665 -42.753499999999995 444171.39765337727 892852.4098734867 +2124 4326 444171.39765337727 892852.4098734867 173.2665 -42.753499999999995 +4167 2125 171.47 -43.69 408823.698237144 806506.2695659474 +2125 4326 408823.698237144 806506.2695659474 171.47 -43.69 +4167 2125 170.75900000000001 -44.194 351908.9833775081 750336.1029678929 +2125 4326 351908.9833775081 750336.1029678929 170.75900000000004 -44.194 +4167 2125 172.18099999999998 -43.186 466697.8341982997 862180.3094086726 +2125 4326 466697.8341982997 862180.3094086726 172.18099999999998 -43.186 +4167 2126 170.685 -44.165 370228.32697682217 826261.3620558524 +2126 4326 370228.32697682217 826261.3620558524 170.685 -44.16499999999999 +4167 2126 169.9065 -44.8985 309109.1158089626 744176.0016037982 +2126 4326 309109.1158089626 744176.0016037982 169.9065 -44.8985 +4167 2126 171.4635 -43.4315 432895.53641045204 907746.4996409477 +2126 4326 432895.53641045204 907746.4996409477 171.4635 -43.43150000000001 +4167 2127 169.43 -44.555 397020.4465195666 820001.7871949198 +2127 4326 397020.4465195666 820001.7871949198 169.43 -44.55500000000001 +4167 2127 168.701 -45.3155 339896.5130327664 735201.8685092966 +2127 4326 339896.5130327664 735201.8685092966 168.701 -45.31549999999999 +4167 2127 170.15900000000002 -43.7945 455653.71446711686 904273.4943392512 +2127 4326 455653.71446711686 904273.4943392512 170.15900000000002 -43.7945 +4167 2128 168.41500000000002 -44.935 401293.6722269621 821979.3962793241 +2128 4326 401293.6722269621 821979.3962793241 168.41500000000002 -44.935 +4167 2128 167.7895 -45.515499999999996 352406.1338516105 757284.481759177 +2128 4326 352406.1338516105 757284.481759177 167.7895 -45.515499999999996 +4167 2128 169.0405 -44.3545 451175.96087886585 886287.0842517263 +2128 4326 451175.96087886585 886287.0842517263 169.0405 -44.354499999999994 +4167 2129 167.29000000000002 -45.43 364894.00794519007 814751.8283694303 +2129 4326 364894.00794519007 814751.8283694303 167.29000000000002 -45.43 +4167 2129 166.46200000000002 -46.239999999999995 301538.2636761943 724027.4760932514 +2129 4326 301538.2636761943 724027.4760932514 166.46200000000002 -46.239999999999995 +4167 2129 168.118 -44.620000000000005 430110.68104189594 904796.8499400922 +2129 4326 430110.68104189594 904796.8499400922 168.118 -44.62000000000001 +4167 2130 170.505 -45.215 390311.9938990716 866801.2018470246 +2130 4326 390311.9938990716 866801.2018470246 170.505 -45.215 +4167 2130 169.8435 -45.7595 338941.4068364543 805992.5583621799 +2130 4326 338941.4068364543 805992.5583621799 169.8435 -45.759499999999996 +4167 2130 171.1665 -44.6705 442675.2442269481 927178.343380334 +2130 4326 442675.2442269481 927178.343380334 171.1665 -44.6705 +4167 2131 169.755 -45.98 359125.0437474381 786681.622547958 +2131 4326 359125.0437474381 786681.622547958 169.755 -45.97999999999999 +4167 2131 168.7515 -46.654999999999994 282816.33977748966 710649.8924254779 +2131 4326 282816.33977748966 710649.8924254779 168.7515 -46.654999999999994 +4167 2131 170.7585 -45.305 437329.94540917233 861726.1233830295 +2131 4326 437329.94540917233 861726.1233830295 170.7585 -45.305 +4167 2132 168.13 -46.33 383615.79436499235 829991.3023536066 +2132 4326 383615.79436499235 829991.3023536066 168.13 -46.32999999999999 +4167 2132 167.374 -47.23 326635.6930236762 729508.088323087 +2132 4326 326635.6930236762 729508.088323087 167.374 -47.22999999999999 +4167 2132 168.886 -45.43 442509.7669847175 929903.8220657824 +2132 4326 442509.7669847175 929903.8220657824 168.886 -45.43 +4167 2133 165.0 -47.785 499999.9999999981 4707595.68314499 +2133 4326 499999.9999999981 4707595.68314499 165.0 -47.785 +4167 2133 162.3 -55.0795 327648.59016135975 3893031.0655304687 +2133 4326 327648.59016135975 3893031.0655304687 162.3 -55.0795 +4167 2133 167.7 -40.4905 728827.1238390332 5514297.465650975 +2133 4326 728827.1238390332 5514297.465650975 167.7 -40.49049999999999 +4167 2134 171.0 -43.365 500000.0 5198651.277140197 +2134 4326 500000.0 5198651.277140197 171.0 -43.364999999999995 +4167 2134 168.3 -54.691500000000005 325983.6760600326 3936190.291665267 +2134 4326 325983.6760600326 3936190.291665267 168.3 -54.6915 +4167 2134 173.7 -32.0385 754964.3708943359 6452108.517323148 +2134 4326 754964.3708943359 6452108.517323148 173.7 -32.0385 +4167 2135 177.0 -40.370000000000005 500000.00000000215 5531175.084525192 +2135 4326 500000.00000000215 5531175.084525192 177.0 -40.370000000000005 +4167 2135 174.3 -52.925 318506.47284296114 4132659.5909271445 +2135 4326 318506.47284296114 4132659.5909271445 174.3 -52.925000000000004 +4167 2135 179.7 -27.815 765978.7795752222 6920365.061669568 +2135 4326 765978.7795752222 6920365.061669568 179.7 -27.814999999999994 +4168 2136 -1.0099999999999998 7.915 896383.155906605 1178241.4548231286 +2136 4326 896383.155906605 1178241.4548231286 -1.009727945813916 7.917729669703686 +4168 2136 -3.026 4.9945 162864.47306331564 120041.06725395692 +2136 4326 162864.47306331564 120041.06725395692 -3.0257837696334486 4.997316029707967 +4168 2136 1.0060000000000002 10.8355 1619634.5262578372 2240094.6318019023 +2136 4326 1619634.5262578372 2240094.6318019023 1.0063290388438364 10.838137973798187 +4168 2137 -0.8449999999999998 3.73 517211.789982511 412273.85754069744 +2137 4326 517211.789982511 412273.85754069744 -0.8446409762116144 3.732844107488875 +4168 2137 -3.4955 1.633 222329.99608139734 180663.81579416385 +2137 4326 222329.99608139734 180663.81579416385 -3.495212573235707 1.6359001658607089 +4168 2137 1.8055000000000003 5.827 810710.0406061932 644837.1819850976 +2137 4326 810710.0406061932 644837.1819850976 1.8059308532688114 5.829784261940542 +4609 2138 -68.475 53.805 1634.9142691118725 1086417.2019230954 +4609 2138 -78.71249999999999 45.871500000000005 -790466.7867871705 264970.44477040955 +4609 2138 -58.2375 61.7385 542962.1728097897 2006133.7837669177 +4171 2154 0.26000000000000156 46.355000000000004 489353.58775158145 6587552.204045072 +2154 4326 489353.58775158145 6587552.204045072 0.2600000000000025 46.35500000000001 +4171 2154 -8.847999999999999 41.6705 -285448.9014183822 6137327.142426368 +2154 4326 -285448.9014183822 6137327.142426368 -8.847999999999999 41.67049999999999 +4171 2154 9.368000000000002 51.039500000000004 1147159.6761796405 7122909.254414408 +2154 4326 1147159.6761796405 7122909.254414408 9.368000000000004 51.03950000000001 +4173 2157 -7.95 53.41 603324.3765559805 739986.4340746693 +2157 4326 603324.3765559805 739986.4340746693 -7.95 53.41 +4173 2157 -10.299000000000001 51.592 440716.2796208438 540225.1320380733 +2157 4326 440716.2796208438 540225.1320380733 -10.299000000000001 51.592 +4173 2157 -5.601 55.228 752607.0965257625 944937.0589330781 +2157 4326 752607.0965257625 944937.0589330781 -5.601000000000001 55.228 +4173 2158 -7.95 53.41 569795.4214857608 5918394.747211814 +2158 4326 569795.4214857608 5918394.747211814 -7.95 53.41 +4173 2158 -10.299000000000001 51.592 410016.2599528065 5716460.2510340065 +2158 4326 410016.2599528065 5716460.2510340065 -10.299000000000001 51.59199999999999 +4173 2158 -5.601 55.228 716150.0691200581 6125432.585373661 +2158 4326 716150.0691200581 6125432.585373661 -5.600999999999999 55.228 +4174 2159 -13.235 8.435 53753.06597255362 642312.0083072458 +4174 2159 -13.3295 8.3315 19474.24593412516 604859.8793418903 +4174 2159 -13.140500000000001 8.5385 88013.97117200772 679770.0237077142 +4174 2160 -13.235 8.435 353753.0659725547 1242312.0083072446 +4174 2160 -13.3295 8.3315 319474.2459341262 1204859.8793418892 +4174 2160 -13.140500000000001 8.5385 388013.9711720087 1279770.0237077128 +4175 2161 -12.675 8.545 755933.7440557618 945235.1174186447 +2161 4326 755933.7440557618 945235.1174186447 -12.675139941305462 8.54509369731198 +4175 2161 -13.282499999999999 7.2895 689611.4927557674 806039.3054485028 +2161 4326 689611.4927557674 806039.3054485028 -13.282647838235386 7.289712113374038 +4175 2161 -12.0675 9.8005 821722.3458767163 1084659.2886989396 +2161 4326 821722.3458767163 1084659.2886989396 -12.067632043765064 9.800476617811158 +4175 2162 -11.129999999999999 8.44 265477.82677853166 933496.103820868 +2162 4326 265477.82677853166 933496.103820868 -11.130118616909094 8.440104080416509 +4175 2162 -11.913 7.036 178139.511104021 778660.2886433047 +2162 4326 178139.511104021 778660.2886433047 -11.913128989149811 7.036236607260261 +4175 2162 -10.347 9.844 352289.2495038761 1088362.8733662937 +2162 4326 352289.2495038761 1088362.8733662937 -10.347108218273737 9.843973191683812 +4142 2164 -5.33 3.105 463331.95659793774 343173.2671579781 +2164 4326 463331.95659793774 343173.2671579781 -5.329629718386245 3.1089396151967095 +4142 2164 -7.327999999999999 1.2285 240934.75739438718 135886.729010449 +2164 4326 240934.75739438718 135886.729010449 -7.32767092993252 1.2326126925373144 +4142 2164 -3.3320000000000007 4.9815 684939.4639797449 550802.5542511485 +2164 4326 684939.4639797449 550802.5542511485 -3.3315884169913157 4.985262397722554 +4143 2165 -5.33 3.105 463331.95659793774 343173.2671579781 +2165 4326 463331.95659793774 343173.2671579781 -5.329626339218703 3.1089242482835564 +4143 2165 -7.327999999999999 1.2285 240934.75739438718 135886.729010449 +2165 4326 240934.75739438718 135886.729010449 -7.327666970147283 1.2325978643758004 +4143 2165 -3.3320000000000007 4.9815 684939.4639797449 550802.5542511485 +2165 4326 684939.4639797449 550802.5542511485 -3.331585621004881 4.985246502278216 +4181 2169 6.130000000000001 49.815 77360.95613843501 97961.43488797324 +2169 4326 77360.95613843501 97961.43488797324 6.131457615025153 49.816066601271274 +4181 2169 5.7700000000000005 49.4775 51252.329577650205 60497.59219230453 +2169 4326 51252.329577650205 60497.59219230453 5.771428988110075 49.47855979409032 +4181 2169 6.49 50.152499999999996 103109.29723989256 135551.90114010315 +2169 4326 103109.29723989256 135551.90114010315 6.491486694731072 50.153573384207974 +4179 2172 21.475 52.92 4601131.97346487 5796882.797642315 +2172 4326 4601131.97346487 5796882.797642315 21.473153480816254 52.91974932686706 +4179 2172 19.2475 51.489 4446373.829882415 5640110.434375746 +2172 4326 4446373.829882415 5640110.434375746 19.245706859332042 51.48869599630478 +4179 2172 23.7025 54.351 4746009.20941356 5958340.121722751 +2172 4326 4746009.20941356 5958340.121722751 23.70059822978542 54.3508040821999 +4179 2173 17.07 53.525 3505088.9846872664 5992510.65183276 +2173 4326 3505088.9846872664 5992510.65183276 17.068107419070255 53.52468091210808 +4179 2173 14.433 52.296499999999995 3325339.153528841 5858965.89418037 +2173 4326 3325339.153528841 5858965.89418037 14.431160263770874 52.296123568029856 +4179 2173 19.707 54.7535 3674710.9643533435 6132547.221412771 +2173 4326 3674710.9643533435 6132547.221412771 19.705054952765902 54.753239781658024 +4179 2174 16.615000000000002 51.365 3699015.433664189 5592981.2450401755 +2174 4326 3699015.433664189 5592981.2450401755 16.613204271653995 51.36465090190774 +4179 2174 14.387500000000001 49.5875 3537791.048554231 5397811.716909547 +2174 4326 3537791.048554231 5397811.716909547 14.385772784922064 49.587096816369844 +4179 2174 18.8425 53.142500000000005 3848208.1725583747 5792918.334216638 +2174 4326 3848208.1725583747 5792918.334216638 18.840631218631685 53.14220715286626 +4179 2175 19.0 50.365 239964.64415638015 881450.994857192 +2175 4326 239964.64415638015 881450.994857192 18.99825232374836 50.36467977553822 +4179 2175 18.397 49.4875 196328.70537689014 783999.3768240372 +2175 4326 196328.70537689014 783999.3768240372 18.395284473940066 49.48716066335673 +4179 2175 19.603 51.24250000000001 282017.57800612407 979264.7227681745 +2175 4326 282017.57800612407 979264.7227681745 19.60121867678351 51.24219920149008 +9702 2176 15.32 52.805 5521578.889627086 5852517.238459874 +2176 4326 5521578.889627086 5852517.238459874 15.320000000000002 52.804999999999986 +9702 2176 14.258000000000001 50.5145 5447375.965810243 5597908.8836801555 +2176 4326 5447375.965810243 5597908.8836801555 14.258 50.5145 +9702 2176 16.382 55.0955 5588219.963154412 6108264.11386956 +2176 4326 5588219.963154412 6108264.11386956 16.382000000000005 55.09549999999999 +9702 2177 18.0 52.66 6500000.0 5836334.701093706 +2177 4326 6500000.0 5836334.701093706 18.0 52.65999999999999 +9702 2177 16.65 49.717 6402652.730340217 5509820.724625859 +2177 4326 6402652.730340217 5509820.724625859 16.649999999999995 49.717000000000006 +9702 2177 19.35 55.603 6585082.237983475 6164714.231834076 +2177 4326 6585082.237983475 6164714.231834076 19.349999999999998 55.602999999999994 +9702 2178 21.0 51.82 7500000.0 5742873.498941285 +2178 4326 7500000.0 5742873.498941285 21.0 51.82 +9702 2178 19.65 49.363 7401946.851850614 5470453.523533324 +2178 4326 7401946.851850614 5470453.523533324 19.65 49.36299999999999 +9702 2178 22.35 54.277 7587929.186353482 6017125.991854813 +2178 4326 7587929.186353482 6017125.991854813 22.35000000000001 54.276999999999994 +9702 2179 23.325 51.705 8453342.081782231 5730294.963535688 +2179 4326 8453342.081782231 5730294.963535688 23.325000000000006 51.705 +9702 2179 22.5825 49.2705 8396851.322643971 5460257.182785936 +2179 4326 8396851.322643971 5460257.182785936 22.5825 49.27049999999999 +9702 2179 24.0675 54.1395 8504411.212632133 6000983.2521624565 +2179 4326 8504411.212632133 6000983.2521624565 24.067500000000006 54.13949999999999 +9702 2180 19.145 52.465 509847.90053546167 511024.242654006 +2180 4326 509847.90053546167 511024.242654006 19.145 52.464999999999996 +9702 2180 14.640500000000001 49.3465 183494.04324446945 173478.58354824875 +2180 4326 183494.04324446945 173478.58354824875 14.640500000000001 49.34649999999999 +9702 2180 23.649499999999996 55.5835 792885.7852911141 867689.5094626667 +2180 4326 792885.7852911141 867689.5094626667 23.649499999999993 55.583499999999994 +4182 2188 -31.18 39.535 656415.3937782953 4377804.932583513 +2188 4326 656415.3937782953 4377804.932583513 -31.184266907402556 39.536335709143735 +4182 2188 -31.323999999999998 39.323499999999996 644475.214720059 4354089.768182517 +2188 4326 644475.214720059 4354089.768182517 -31.328261941453007 39.32482647187323 +4182 2188 -31.036 39.746500000000005 668278.4186599532 4401541.412859161 +2188 4326 668278.4186599532 4401541.412859161 -31.040271935114607 39.7478449978163 +4183 2189 -27.935 38.730000000000004 418725.4677602699 4287301.9640168855 +2189 4326 418725.4677602699 4287301.9640168855 -27.9338635549726 38.729888891500046 +4183 2189 -28.8035 38.361 342423.77447880665 4247480.203752405 +2189 4326 342423.77447880665 4247480.203752405 -28.802398979764515 38.36089045734287 +4183 2189 -27.066499999999998 39.099000000000004 494249.42248779646 4327838.003868563 +2189 4326 494249.42248779646 4327838.003868563 -27.06532797877091 39.09888707422427 +4184 2190 -25.32 37.415 648675.7450674111 4142303.8589758445 +2190 4326 648675.7450674111 4142303.8589758445 -25.319540957151876 37.41592009476636 +4184 2190 -25.860000000000003 36.924499999999995 601539.3143419191 4087170.4402770614 +2190 4326 601539.3143419191 4087170.4402770614 -25.859569723259096 36.92541351532873 +4184 2190 -24.779999999999998 37.9055 695180.7321004497 4197723.316122828 +2190 4326 695180.7321004497 4197723.316122828 -24.779511820465636 37.9064266137446 +4167 2193 172.5 -40.715 1557768.5448432215 5492759.65743377 +2193 4326 1557768.5448432215 5492759.65743377 172.5 -40.71500000000001 +4167 2193 166.983 -46.6685 1139781.9428593111 4814063.9318894325 +2193 4326 1139781.9428593111 4814063.9318894325 166.983 -46.66849999999999 +4167 2193 178.017 -34.7615 2059340.6317736416 6141921.427604995 +2193 4326 2059340.6317736416 6141921.427604995 178.017 -34.76149999999999 +4152 2195 -169.485 -14.35 663372.1955144025 8413029.452880211 +2195 4326 663372.1955144025 8413029.452880211 -169.485 -14.350000000000007 +4152 2195 -170.7405 -14.565999999999999 527953.7447252881 8389659.50634633 +2195 4326 527953.7447252881 8389659.50634633 -170.7405 -14.565999999999995 +4152 2195 -168.2295 -14.134 799117.6450683003 8435686.928591615 +2195 4326 799117.6450683003 8435686.928591615 -168.2295 -14.133999999999997 +4258 2196 9.645 56.235 208991.52512576568 6234426.565365828 +2196 4326 208991.52512576568 6234426.565365828 9.645 56.23500000000001 +4258 2196 8.1645 54.8265 114174.3533430018 6078429.739258859 +2196 4326 114174.3533430018 6078429.739258859 8.1645 54.826499999999996 +4258 2196 11.125499999999999 57.643499999999996 297063.4727165276 6392421.288902079 +2196 4326 297063.4727165276 6392421.288902079 11.125499999999999 57.6435 +4258 2197 11.829999999999998 55.65 489298.1127984397 6169299.115539661 +2197 4326 489298.1127984397 6169299.115539661 11.829999999999998 55.650000000000006 +4258 2197 10.893999999999998 54.623999999999995 428567.20152485557 6055633.383762761 +2197 4326 428567.20152485557 6055633.383762761 10.893999999999998 54.623999999999995 +4258 2197 12.765999999999998 56.676 546952.3862018584 6283782.295867055 +2197 4326 546952.3862018584 6283782.295867055 12.765999999999998 56.676 +4258 2198 14.92 55.16 894900.8626330065 6115045.235168036 +2198 4326 894900.8626330065 6115045.235168036 14.92 55.16 +4258 2198 14.623 54.961999999999996 875851.4768809304 6093065.081747199 +2198 4326 875851.4768809304 6093065.081747199 14.623 54.96199999999999 +4258 2198 15.217 55.358000000000004 913762.7975117388 6137106.757271641 +2198 4326 913762.7975117388 6137106.757271641 15.216999999999999 55.358 +4122 2200 -66.375 46.315 309626.9905133954 779444.9377680834 +4122 2200 -68.7825 44.7355 119191.96005449325 606463.1113785943 +4122 2200 -63.9675 47.8945 489366.009101532 958098.3142979173 +4189 2201 -72.685 9.32 754289.3322530482 1031063.0951517058 +2201 4326 754289.3322530482 1031063.0951517058 -72.685 9.32 +4189 2201 -73.31049999999999 7.25 686532.2795007885 801732.7915149125 +2201 4326 686532.2795007885 801732.7915149125 -73.31049999999999 7.250000000000001 +4189 2201 -72.0595 11.389999999999999 820932.6339763846 1260729.9035017171 +2201 4326 820932.6339763846 1260729.9035017171 -72.0595 11.389999999999997 +4189 2202 -69.0 8.185 500000.0 904749.9795088829 +2202 4326 500000.0 904749.9795088829 -69.0 8.184999999999999 +4189 2202 -71.7 1.4755 199544.72832472937 163269.83645050752 +2202 4326 199544.72832472937 163269.83645050752 -71.7 1.4755 +4189 2202 -66.3 14.8945 790505.9469774769 1648417.6366105552 +2202 4326 790505.9469774769 1648417.6366105552 -66.3 14.894500000000004 +4189 2203 -62.475 8.695 557753.1411820705 961172.5037920127 +2203 4326 557753.1411820705 961172.5037920127 -62.47500000000001 8.694999999999997 +4189 2203 -65.6475 1.4455 205387.22682748467 159943.32065971397 +2203 4326 205387.22682748467 159943.32065971397 -65.6475 1.4455 +4189 2203 -59.3025 15.9445 895945.3185899712 1766309.4269501464 +2203 4326 895945.3185899712 1766309.4269501464 -59.3025 15.944499999999996 +4267 2204 -85.98 35.83 2005928.746324735 523441.241576924 +2204 4326 2005928.746324735 523441.241576924 -85.97996418137805 35.83009790828433 +4267 2204 -89.87700000000001 35.065 839985.2862502916 267970.306201955 +2204 4326 839985.2862502916 267970.306201955 -89.87708393883386 35.065112617314384 +4267 2204 -82.083 36.595 3149692.04987555 824936.7452041993 +2204 4326 3149692.04987555 824936.7452041993 -82.08284226355963 36.595080318096194 +4269 2205 -84.215 38.43 503055.9840861851 103227.84047299267 +2205 4326 503055.9840861851 103227.84047299267 -84.215 38.43000000000001 +4269 2205 -85.7855 37.782000000000004 364736.787111394 32428.687076877144 +2205 4326 364736.787111394 32428.687076877144 -85.7855 37.78200000000001 +4269 2205 -82.6445 39.077999999999996 638921.4422613666 176371.6164450688 +2205 4326 638921.4422613666 176371.6164450688 -82.6445 39.07799999999999 +4230 2206 27.060000000000002 39.305 9505175.429384556 4352440.064054994 +2206 4326 9505175.429384556 4352440.064054994 27.059522538247148 39.303981156269145 +4230 2206 25.764 36.7805 9389658.787294628 4072930.682445483 +2206 4326 9389658.787294628 4072930.682445483 25.763512487316532 36.77942218985678 +4230 2206 28.356 41.829499999999996 9612651.362384992 4633670.128174798 +2206 4326 9612651.362384992 4633670.128174798 28.35553243222134 41.82854710888551 +4230 2207 30.0 38.760000000000005 10500000.0 4291932.892534684 +2207 4326 10500000.0 4291932.892534684 29.999599500225692 38.75898353808558 +4230 2207 28.65 36.330000000000005 10378779.410084378 4023071.177238889 +2207 4326 10378779.410084378 4023071.177238889 28.64958412350762 36.32892760932368 +4230 2207 31.35 41.19 10613262.270281408 4562632.456819124 +2207 4326 10613262.270281408 4562632.456819124 31.34961527003125 41.1890458325545 +4230 2208 33.0 39.019999999999996 11500000.0 4320797.170080892 +2208 4326 11500000.0 4320797.170080892 32.999673065826194 39.019001968674566 +4230 2208 31.65 36.275 11378694.248420063 4016967.4547359454 +2208 4326 11378694.248420063 4016967.4547359454 31.64965667206723 36.273939338813605 +4230 2208 34.35 41.765 11612265.42521855 4626497.864396563 +2208 4326 11612265.42521855 4626497.864396563 34.349689947267734 41.764072585823726 +4230 2209 36.0 38.980000000000004 12500000.0 4316356.427731224 +2209 4326 12500000.0 4316356.427731224 35.99975003537722 38.97901183234034 +4230 2209 34.65 36.127 12378465.642823776 4000543.157354169 +2209 4326 12378465.642823776 4000543.157354169 34.649730886916366 36.125947395221665 +4230 2209 37.35 41.833 12612146.784243083 4634051.04519292 +2209 4326 12612146.784243083 4634051.04519292 37.34977002003097 41.83208476316438 +4230 2210 39.0 38.925 13500000.0 4310250.457054633 +2210 4326 13500000.0 4310250.457054633 38.999828022742946 38.924019354696725 +4230 2210 37.65 36.8865 13379647.377127968 4084832.7611922882 +2210 4326 13379647.377127968 4084832.7611922882 37.64980211819477 36.885471734737436 +4230 2210 40.35 40.963499999999996 13613651.799710399 4537476.707245884 +2210 4326 13613651.799710399 4537476.707245884 40.34985523173121 40.96257113156767 +4230 2211 42.0 39.31 14500000.0 4352993.469796902 +2211 4326 14500000.0 4352993.469796902 41.99990532490977 39.309034968524415 +4230 2211 40.65 37.249 14380218.893044675 4125066.7587728184 +2211 4326 14380218.893044675 4125066.7587728184 40.64987696492658 37.24798661411629 +4230 2211 43.35 41.371 14612949.712116871 4582735.52095337 +2211 4326 14612949.712116871 4582735.52095337 43.34993522751082 41.370087476758506 +4230 2212 44.165 38.995000000000005 15427658.070083633 4318353.418230682 +2212 4326 15427658.070083633 4318353.418230682 44.16496343992102 38.99403132831345 +4230 2212 43.5665 37.1725 15372681.305085275 4116684.6763743437 +2212 4326 15372681.305085275 4116684.6763743437 43.56645209547583 37.171490308701784 +4230 2212 44.7635 40.8175 15480046.331422277 4520411.732129788 +2212 4326 15480046.331422277 4520411.732129788 44.76347522680109 40.816575725967546 +4258 2213 30.025 44.32 501993.54861211684 4907415.839012424 +2213 4326 501993.54861211684 4907415.839012424 30.024999999999995 44.32 +4258 2213 28.7785 43.528 401293.9023830724 4820175.7076104805 +2213 4326 401293.9023830724 4820175.7076104805 28.7785 43.528 +4258 2213 31.2715 45.112 600018.201100815 4996178.643872659 +2213 4326 600018.201100815 4996178.643872659 31.271499999999996 45.112 +4193 2215 9.425 3.575 547201.7178293705 395124.6062408859 +2215 4326 547201.7178293705 395124.6062408859 9.42375666311677 3.574287698195411 +4193 2215 8.5475 2.3015000000000003 449687.0099552437 254370.80747114535 +2215 4326 449687.0099552437 254370.80747114535 8.546245164694557 2.3009067493455824 +4193 2215 10.3025 4.8485 644435.4091578941 536007.3028597799 +2215 4326 644435.4091578941 536007.3028597799 10.30126787431279 4.847670421773883 +4194 2216 -51.0 66.84 500000.0000000003 7413733.716471699 +2216 4326 500000.0000000003 7413733.716471699 -50.995052019562486 66.8388113399504 +4194 2216 -53.7 61.251000000000005 355143.4677806153 6793900.132430102 +2216 4326 355143.4677806153 6793900.132430102 -53.69591268467823 61.249664661102564 +4194 2216 -48.3 72.429 590954.1826806946 8039048.35286501 +2216 4326 590954.1826806946 8039048.35286501 -48.29363569734584 72.42797933221131 +4194 2217 -45.260000000000005 60.894999999999996 485890.6061802692 6751279.851736975 +2217 4326 485890.6061802692 6751279.851736975 -45.25598100258671 60.89341216777338 +4194 2217 -47.726 59.8555 347308.3731893056 6638615.573459297 +2217 4326 347308.3731893056 6638615.573459297 -47.72208450090837 59.853945053155805 +4194 2217 -42.794000000000004 61.9345 615782.1705034296 6869015.473955953 +2217 4326 615782.1705034296 6869015.473955953 -42.78987649643093 61.93287945649066 +4195 2218 -25.505000000000003 70.5 56088.873475497625 694.4313680290898 +2218 4326 56088.873475497625 694.4313680290898 -25.495666552663153 70.49956175486003 +4195 2218 -29.2715 69.15 209281.1328267195 -141545.75287523007 +2218 4326 209281.1328267195 -141545.75287523007 -29.262825773625615 69.14969461069661 +4195 2218 -21.738500000000002 71.85 -78667.66896949719 152113.45858075522 +2218 4326 -78667.66896949719 152113.45858075522 -21.72845007101485 71.84942523966218 +4122 2219 -67.5 45.855000000000004 616451.079333155 5079029.5519644 +4122 2219 -68.85 43.8615 512054.05351834936 4856499.530410357 +4122 2219 -66.15 47.8485 713210.1714890139 5303393.02680338 +4122 2220 -62.864999999999995 45.69499999999999 510510.64565496286 5060168.065025054 +4122 2220 -65.6865 43.63849999999999 283304.97667740926 4835228.793287003 +4122 2220 -60.043499999999995 47.75149999999999 721589.2334656441 5292913.186288889 +4195 2221 -26.064999999999998 68.83 -239006.35813683944 159811.2441378038 +2221 4326 -239006.35813683944 159811.2441378038 -26.056366680364956 68.82952392406922 +4195 2221 -26.897499999999997 68.67699999999999 -206965.91361103955 139813.53680091267 +2221 4326 -206965.91361103955 139813.53680091267 -26.888942545221163 68.67655906811264 +4195 2221 -25.2325 68.983 -270547.36255772633 180215.2067514127 +2221 4326 -270547.36255772633 180215.2067514127 -25.223791489111473 68.9824886251678 +4269 2222 -110.375 34.17 636986.5507547572 1153309.1963629022 +2222 4326 636986.5507547572 1153309.1963629022 -110.375 34.17 +4269 2222 -111.5765 31.613999999999997 261130.53021934442 226158.4601393042 +2222 4326 261130.53021934442 226158.4601393042 -111.5765 31.613999999999994 +4269 2222 -109.1735 36.726 991049.9575596204 2085058.2958645122 +2222 4326 991049.9575596204 2085058.2958645122 -109.1735 36.72599999999999 +4269 2223 -111.895 34.17 706553.3934041469 1153245.5485358275 +2223 4326 706553.3934041469 1153245.5485358275 -111.895 34.17 +4269 2223 -113.2045 31.613999999999997 299111.1629138048 225689.67780187496 +2223 4326 299111.1629138048 225689.67780187496 -113.2045 31.613999999999994 +4269 2223 -110.5855 36.726 1090106.1703268215 2086259.9578863946 +2223 4326 1090106.1703268215 2086259.9578863946 -110.5855 36.726000000000006 +4269 2224 -113.66499999999999 34.525 725602.1919125565 1282483.876830491 +2224 4326 725602.1919125565 1282483.876830491 -113.66499999999999 34.525 +4269 2224 -114.6955 32.2975 407843.2005554682 473263.5546537977 +2224 4326 407843.2005554682 473263.5546537977 -114.69550000000001 32.2975 +4269 2224 -112.6345 36.7525 1026799.7977149363 2095170.4724058916 +2224 4326 1026799.7977149363 2095170.4724058916 -112.6345 36.7525 +4269 2225 -122.22 40.8 6500762.569637412 2174791.409085322 +2225 4326 6500762.569637412 2174791.409085322 -122.22 40.8 +4269 2225 -124.227 39.711000000000006 5935135.343353516 1785967.7581478895 +2225 4326 5935135.343353516 1785967.7581478895 -124.227 39.71100000000001 +4269 2225 -120.213 41.888999999999996 7048248.886329139 2576456.9439488114 +2225 4326 7048248.886329139 2576456.9439488114 -120.21300000000001 41.88899999999998 +4269 2226 -121.80000000000001 39.09 6618431.056367773 2158843.097341253 +2226 4326 6618431.056367773 2158843.097341253 -121.80000000000001 39.089999999999996 +4269 2226 -123.834 38.127 6034094.980629644 1813394.5703542582 +2226 4326 6034094.980629644 1813394.5703542582 -123.834 38.127 +4269 2226 -119.766 40.053 7187039.698377048 2517236.023450566 +2226 4326 7187039.698377048 2517236.023450566 -119.76600000000002 40.053000000000004 +4269 2227 -120.425 37.72 6583359.320656051 2084639.0553885268 +2227 4326 6583359.320656051 2084639.0553885268 -120.42499999999998 37.71999999999999 +4269 2227 -122.7605 36.82899999999999 5900078.18062494 1768202.0916701525 +2227 4326 5900078.18062494 1768202.0916701525 -122.7605 36.82899999999999 +4269 2227 -118.0895 38.611000000000004 7250435.109600073 2417966.60498926 +2227 4326 7250435.109600073 2417966.60498926 -118.08950000000002 38.611 +4269 2228 -118.815 36.68 6615915.005361133 2130717.8888657917 +2228 4326 6615915.005361133 2130717.8888657917 -118.81499999999998 36.67999999999997 +4269 2228 -121.6905 35.870000000000005 5764562.874933041 1846956.821754023 +2228 4326 5764562.874933041 1846956.821754023 -121.6905 35.87000000000002 +4269 2228 -115.93950000000001 37.489999999999995 7449562.145374354 2439733.1965191932 +2228 4326 7449562.145374354 2439733.1965191932 -115.93950000000002 37.489999999999995 +4269 2229 -117.77000000000001 34.285 6631142.589076952 1926178.466198221 +2229 4326 6631142.589076952 1926178.466198221 -117.77 34.28500000000002 +4269 2229 -121.055 32.9125 5623806.9733687285 1440834.4849815036 +2229 4326 5623806.9733687285 1440834.4849815036 -121.055 32.91250000000001 +4269 2229 -114.485 35.6575 7605759.772572422 2443890.0068066833 +2229 4326 7605759.772572422 2443890.0068066833 -114.485 35.6575 +4269 2230 -116.285 33.305 6550973.0757064605 2054604.0248969968 +2230 4326 6550973.0757064605 2054604.0248969968 -116.285 33.305 +4269 2230 -117.96350000000001 32.6075 6033992.027722262 1805152.9211946672 +2230 4326 6033992.027722262 1805152.9211946672 -117.96350000000001 32.6075 +4269 2230 -114.6065 34.0025 7059785.79330495 2312340.457103632 +2230 4326 7059785.79330495 2312340.457103632 -114.6065 34.0025 +4269 2231 -105.55000000000001 40.285 2986050.860887343 1346670.6799931568 +2231 4326 2986050.860887343 1346670.6799931568 -105.55000000000001 40.285 +4269 2231 -108.709 39.6325 2096339.886818099 1125330.4468983235 +2231 4326 2096339.886818099 1125330.4468983235 -108.709 39.63249999999999 +4269 2231 -102.391 40.9375 3858846.993252231 1599441.7875605011 +2231 4326 3858846.993252231 1599441.7875605011 -102.391 40.93749999999999 +4269 2232 -105.55000000000001 39.115 2985813.6016209614 1466782.1270270192 +2232 4326 2985813.6016209614 1466782.1270270192 -105.55000000000001 39.115 +4269 2232 -108.709 38.2375 2078419.9419787095 1163477.0673063204 +2232 4326 2078419.9419787095 1163477.0673063204 -108.709 38.237500000000004 +4269 2232 -102.391 39.99250000000001 3871001.6992834485 1801312.7143666178 +2232 4326 3871001.6992834485 1801312.7143666178 -102.391 39.992500000000014 +4269 2233 -105.55000000000001 37.83 2985559.408843806 1423596.4157699181 +2233 4326 2985559.408843806 1423596.4157699181 -105.55000000000001 37.82999999999999 +4269 2233 -108.709 37.065 2063817.719543651 1161124.7599010922 +2233 4326 2063817.719543651 1161124.7599010922 -108.709 37.06500000000002 +4269 2233 -102.391 38.595 3888479.6750570713 1716965.8322500188 +2233 4326 3888479.6750570713 1716965.8322500188 -102.391 38.595000000000006 +4269 2234 -72.755 41.515 998630.6540483504 748374.5158726999 +2234 4326 998630.6540483504 748374.5158726999 -72.755 41.51499999999999 +4269 2234 -73.63250000000001 41.0335 756522.9195895719 574175.8351510088 +2234 4326 756522.9195895719 574175.8351510088 -73.63250000000001 41.03349999999999 +4269 2234 -71.8775 41.9965 1237175.245325916 925028.0858586932 +2234 4326 1237175.245325916 925028.0858586932 -71.8775 41.9965 +4269 2235 -75.38499999999999 39.144999999999996 665148.1059946908 417004.58964761323 +2235 4326 665148.1059946908 417004.58964761323 -75.38499999999999 39.144999999999996 +4269 2235 -75.7585 38.5105 558349.8385451131 186093.02122606646 +2235 4326 558349.8385451131 186093.02122606646 -75.75850000000001 38.51049999999999 +4269 2235 -75.0115 39.7795 770043.2556581295 648377.5682590795 +2235 4326 770043.2556581295 648377.5682590795 -75.01150000000001 39.7795 +4269 2236 -81.15 27.619999999999997 607594.4137482274 1194584.9374015871 +2236 4326 607594.4137482274 1194584.9374015871 -81.15 27.62 +4269 2236 -82.212 24.731 253886.22883335204 146285.6407118301 +2236 4326 253886.22883335204 146285.6407118301 -82.212 24.730999999999998 +4269 2236 -80.088 30.508999999999997 943369.5550423709 2246203.11233973 +2236 4326 943369.5550423709 2246203.11233973 -80.088 30.509 +4269 2237 -82.235 27.935000000000002 580288.9628426317 1309145.6717984562 +2237 4326 580288.9628426317 1309145.6717984562 -82.235 27.935 +4269 2237 -83.2295 26.4365 253822.43823780073 766265.8122870891 +2237 4326 253822.43823780073 766265.8122870891 -83.2295 26.4365 +4269 2237 -81.2405 29.433500000000002 897933.73025287 1854707.8988105578 +2237 4326 897933.73025287 1854707.8988105578 -81.2405 29.433500000000006 +4269 2238 -84.83500000000001 30.11 1862576.6509201073 403830.911351752 +2238 4326 1862576.6509201073 403830.911351752 -84.83500000000002 30.109999999999992 +4269 2238 -87.3505 29.3 1059931.7593801322 120462.22483245008 +2238 4326 1059931.7593801322 120462.22483245008 -87.35050000000001 29.299999999999983 +4269 2238 -82.3195 30.92 2652275.477143252 704817.4257324567 +2238 4326 2652275.477143252 704817.4257324567 -82.3195 30.920000000000016 +4269 2239 -82.12 32.519999999999996 670550.4079571866 916584.6819237713 +2239 4326 670550.4079571866 916584.6819237713 -82.12 32.51999999999999 +4269 2239 -83.335 30.576 288503.2986552846 211379.75685448793 +2239 4326 288503.2986552846 211379.75685448793 -83.335 30.576000000000004 +4269 2239 -80.905 34.464 1036458.0121469488 1626278.7144503533 +2239 4326 1036458.0121469488 1626278.7144503533 -80.905 34.464000000000006 +4269 2240 -84.3 32.815 2255621.7222289215 1023929.3357603531 +2240 4326 2255621.7222289215 1023929.3357603531 -84.3 32.815 +4269 2240 -85.479 30.8395 1884722.1334243573 307723.6949145625 +2240 4326 1884722.1334243573 307723.6949145625 -85.479 30.839500000000005 +4269 2240 -83.121 34.790499999999994 2610532.679871406 1744363.2918229178 +2240 4326 2610532.679871406 1744363.2918229178 -83.121 34.7905 +4269 2241 -112.14 43.370000000000005 663257.2675185056 620741.4988298989 +2241 4326 663257.2675185056 620741.4988298989 -112.14 43.37000000000001 +4269 2241 -113.13 42.128 394851.7665072292 169577.62331781015 +2241 4326 394851.7665072292 169577.62331781015 -113.13 42.128 +4269 2241 -111.15 44.612 920916.5599694549 1075125.1509005656 +2241 4326 920916.5599694549 1075125.1509005656 -111.15 44.611999999999995 +4269 2242 -113.985 43.845 1644373.8672963725 793876.5285069791 +2242 4326 1644373.8672963725 793876.5285069791 -113.985 43.845 +4269 2242 -115.1685 42.1755 1323684.2431755583 187581.75456631123 +2242 4326 1323684.2431755583 187581.75456631123 -115.16850000000001 42.17550000000001 +4269 2242 -112.8015 45.514500000000005 1947644.896681241 1404810.2444999106 +2242 4326 1947644.896681241 1404810.2444999106 -112.8015 45.514500000000005 +4269 2243 -115.78 45.5 2616974.4724351414 1397212.5070425826 +2243 4326 2616974.4724351414 1397212.5070425826 -115.78 45.50000000000001 +4269 2243 -117.094 42.341 2261319.223667356 248590.18829063742 +2243 4326 2261319.223667356 248590.18829063742 -117.09399999999998 42.340999999999994 +4269 2243 -114.466 48.659 2934984.2274919054 2551952.4858849845 +2243 4326 2934984.2274919054 2551952.4858849845 -114.46600000000001 48.65899999999999 +4269 2246 -84.215 38.43 1650442.841456092 338673.33995181014 +2246 4326 1650442.841456092 338673.33995181014 -84.215 38.43000000000001 +4269 2246 -85.7855 37.782000000000004 1196640.609381298 106393.11751805444 +2246 4326 1196640.609381298 106393.11751805444 -85.7855 37.78200000000001 +4269 2246 -82.6445 39.077999999999996 2096194.7654858336 578645.8782868632 +2246 4326 2096194.7654858336 578645.8782868632 -82.6445 39.07799999999999 +4269 2247 -85.75999999999999 37.33 1637509.1700943413 2003290.6007795555 +2247 4326 1637509.1700943413 2003290.6007795555 -85.75999999999999 37.33 +4269 2247 -89.189 36.574000000000005 630732.12259348 1746419.471613442 +2247 4326 630732.12259348 1746419.471613442 -89.189 36.57400000000001 +4269 2247 -82.331 38.086 2624312.697881749 2296380.458240027 +2247 4326 2624312.697881749 2296380.458240027 -82.331 38.086 +4269 2248 -77.22999999999999 38.849999999999994 1246831.3253763365 431036.13126735657 +2248 4326 1246831.3253763365 431036.13126735657 -77.22999999999999 38.84999999999999 +4269 2248 -79.264 38.058 660478.5982032857 150603.77057397808 +2248 4326 660478.5982032857 150603.77057397808 -79.264 38.05799999999997 +4269 2248 -75.196 39.641999999999996 1820363.7570766064 724444.4117298055 +2248 4326 1820363.7570766064 724444.4117298055 -75.196 39.641999999999996 +4269 2249 -71.68 42.175 607375.19799223 2888847.4136804407 +2249 4326 607375.19799223 2888847.4136804407 -71.68 42.17500000000001 +4269 2249 -73.318 41.5315 158412.21237031757 2659609.7996061267 +2249 4326 158412.21237031757 2659609.7996061267 -73.318 41.531499999999994 +4269 2249 -70.042 42.8185 1047350.1276101986 3126655.496255163 +2249 4326 1047350.1276101986 3126655.496255163 -70.042 42.8185 +4269 2250 -70.4 41.349999999999994 1667873.4412726075 127542.84624277307 +2250 4326 1667873.4412726075 127542.84624277307 -70.4 41.34999999999999 +4269 2250 -70.859 41.205999999999996 1541629.7694533693 75262.73455290835 +2250 4326 1541629.7694533693 75262.73455290835 -70.859 41.205999999999996 +4269 2250 -69.941 41.494 1793560.5809032158 180491.00065109914 +2250 4326 1793560.5809032158 180491.00065109914 -69.941 41.49399999999999 +4269 2251 -86.93 46.7 26264282.366583254 698921.7310785229 +2251 4326 26264282.366583254 698921.7310785229 -86.93 46.7 +4269 2251 -90.071 45.242 25455797.155465834 182580.20413098446 +2251 4326 25455797.155465834 182580.20413098446 -90.07100000000001 45.242 +4269 2251 -83.789 48.158 27030608.932740275 1246683.7520799034 +2251 4326 27030608.932740275 1246683.7520799034 -83.78900000000002 48.157999999999994 +4269 2252 -84.66499999999999 44.86 19607684.49687459 562789.7398522291 +2252 4326 19607684.49687459 562789.7398522291 -84.66499999999999 44.86000000000001 +4269 2252 -86.8205 43.906 19038360.163045596 224644.31437373898 +2252 4326 19038360.163045596 224644.31437373898 -86.8205 43.906 +4269 2252 -82.5095 45.814 20158579.468606047 915905.2930722877 +2252 4326 20158579.468606047 915905.2930722877 -82.5095 45.814 +4269 2253 -84.66499999999999 42.955 13043497.920191662 530383.8281247463 +2253 4326 13043497.920191662 530383.8281247463 -84.66499999999999 42.95499999999999 +4269 2253 -86.9465 41.8165 12420152.374608975 126120.25837059683 +2253 4326 12420152.374608975 126120.25837059683 -86.9465 41.816500000000005 +4269 2253 -82.3835 44.0935 13644416.679989409 951369.2285057498 +2253 4326 13644416.679989409 951369.2285057498 -82.38350000000001 44.0935 +4269 2254 -89.03 32.51 923623.0145268167 1094873.3762410795 +2254 4326 923623.0145268167 1094873.3762410795 -89.03000000000002 32.51 +4269 2254 -89.876 30.26 655059.4682251292 277893.84981060866 +2254 4326 655059.4682251292 277893.84981060866 -89.876 30.26000000000001 +4269 2254 -88.184 34.76 1179284.0030438225 1914173.6953409754 +2254 4326 1179284.0030438225 1914173.6953409754 -88.184 34.76 +4269 2255 -90.51 33.004999999999995 2242422.280215017 1274957.169199412 +2255 4326 2242422.280215017 1274957.169199412 -90.51 33.004999999999995 +4269 2255 -91.536 31.200499999999998 1920544.1600448415 620499.1297669145 +2255 4326 1920544.1600448415 620499.1297669145 -91.536 31.20049999999999 +4269 2255 -89.48400000000001 34.8095 2551538.4111436997 1932637.7032956902 +2255 4326 2551538.4111436997 1932637.7032956902 -89.48400000000001 34.8095 +4269 2256 -110.055 46.68 1829274.9252737954 886402.5258938809 +2256 4326 1829274.9252737954 886402.5258938809 -110.055 46.68000000000001 +4269 2256 -115.46849999999999 44.583 414467.64907519735 180687.97554161298 +2256 4326 414467.64907519735 180687.97554161298 -115.46849999999999 44.583000000000006 +4269 2256 -104.64150000000001 48.777 3139154.9208227736 1686810.7423968795 +2256 4326 3139154.9208227736 1686810.7423968795 -104.64150000000001 48.77700000000001 +4269 2257 -104.35499999999999 34.5 534809.6738687864 1273342.2745110546 +2257 4326 534809.6738687864 1273342.2745110546 -104.355 34.5 +4269 2257 -105.5835 32.25 154842.65894785128 456933.58544315636 +2257 4326 154842.65894785128 456933.58544315636 -105.5835 32.24999999999999 +4269 2257 -103.1265 36.75 894897.835348036 2094529.8232941285 +2257 4326 894897.835348036 2094529.8232941285 -103.1265 36.75 +4269 2258 -106.285 34.39 1629857.9819224146 1233301.9486272205 +2258 4326 1629857.9819224146 1233301.9486272205 -106.28500000000001 34.39000000000001 +4269 2258 -107.58550000000001 32.041000000000004 1226599.187120218 381209.5387058881 +2258 4326 1226599.187120218 381209.5387058881 -107.58550000000002 32.041 +4269 2258 -104.9845 36.739 2011214.6405028943 2090727.895403737 +2258 4326 2011214.6405028943 2090727.895403737 -104.9845 36.739000000000004 +4269 2259 -107.69 34.165 2766448.093591952 1151472.736769464 +2259 4326 2766448.093591952 1151472.736769464 -107.69 34.165 +4269 2259 -108.923 31.6135 2383886.902199309 224840.0554118961 +2259 4326 2383886.902199309 224840.0554118961 -108.923 31.6135 +4269 2259 -106.457 36.716499999999996 3126490.517471582 2083018.8457889084 +2259 4326 3126490.517471582 2083018.8457889084 -106.45700000000001 36.7165 +4269 2260 -74.55000000000001 42.95 478739.3814187781 1499731.5761545855 +2260 4326 478739.3814187781 1499731.5761545855 -74.55000000000001 42.95000000000001 +4269 2260 -75.738 41.087 150875.69764539006 823315.0615341639 +2260 4326 150875.69764539006 823315.0615341639 -75.738 41.087 +4269 2260 -73.36200000000001 44.813 787433.2344381205 2180852.0632972782 +2260 4326 787433.2344381205 2180852.0632972782 -73.36200000000001 44.813 +4269 2261 -76.405 43.2 867758.4852460751 1166018.0672500338 +2261 4326 867758.4852460751 1166018.0672500338 -76.405 43.2 +4269 2261 -77.6155 42.111000000000004 540149.3326531219 770792.6591996523 +2261 4326 540149.3326531219 770792.6591996523 -77.6155 42.111000000000004 +4269 2261 -75.1945 44.288999999999994 1183869.9012600197 1565987.6461074888 +2261 4326 1183869.9012600197 1565987.6461074888 -75.19450000000002 44.288999999999994 +4269 2262 -78.565 42.815 1153210.6242120317 1025652.9812480954 +2262 4326 1153210.6242120317 1025652.9812480954 -78.565 42.81499999999999 +4269 2262 -79.64949999999999 42.072500000000005 858832.283273011 756876.4538784421 +2262 4326 858832.283273011 756876.4538784421 -79.64949999999999 42.0725 +4269 2262 -77.4805 43.5575 1440625.485810547 1298207.173722629 +2262 4326 1440625.485810547 1298207.173722629 -77.4805 43.557500000000005 +4269 2263 -73.03 40.885 1252460.5525647984 263192.27519998315 +2263 4326 1252460.5525647984 263192.27519998315 -73.03 40.885000000000005 +4269 2263 -74.137 40.5115 946155.1353312435 125659.67980443442 +2263 4326 946155.1353312435 125659.67980443442 -74.137 40.511500000000005 +4269 2263 -71.923 41.2585 1555283.8917496786 404564.1584891662 +2263 4326 1555283.8917496786 404564.1584891662 -71.923 41.2585 +4269 2264 -79.85499999999999 35.21 1744617.9959167782 532455.9538610925 +2264 4326 1744617.9959167782 532455.9538610925 -79.85499999999999 35.21000000000001 +4269 2264 -83.8825 33.967999999999996 519981.51040541707 115749.34535515642 +2264 4326 519981.51040541707 115749.34535515642 -83.8825 33.96799999999999 +4269 2264 -75.8275 36.452000000000005 2933006.304381549 998362.8710786074 +2264 4326 2933006.304381549 998362.8710786074 -75.8275 36.45200000000001 +4269 2265 -100.44999999999999 48.08 1980725.9056554495 393951.8534337165 +2265 4326 1980725.9056554495 393951.8534337165 -100.44999999999999 48.080000000000005 +4269 2265 -103.708 47.242999999999995 1171852.468078186 105237.278882084 +2265 4326 1171852.468078186 105237.278882084 -103.708 47.242999999999995 +4269 2265 -97.192 48.917 2763745.8760258164 716386.7939064692 +2265 4326 2763745.8760258164 716386.7939064692 -97.192 48.91700000000001 +4269 2266 -100.3 46.879999999999995 2018517.8260990612 442551.6599465382 +2266 4326 2018517.8260990612 442551.6599465382 -100.3 46.88000000000001 +4269 2266 -103.675 46.025 1162149.5097543914 146978.76648637702 +2266 4326 1162149.5097543914 146978.76648637702 -103.675 46.025000000000006 +4269 2266 -96.925 47.735 2848006.7882052157 774362.3047026764 +2266 4326 2848006.7882052157 774362.3047026764 -96.925 47.73499999999999 +4269 2267 -98.71000000000001 36.14 1758858.593240887 415746.68485871307 +2267 4326 1758858.593240887 415746.68485871307 -98.71000000000001 36.14000000000001 +4269 2267 -102.571 35.357 605894.9084370023 162038.6840351809 +2267 4326 605894.9084370023 162038.6840351809 -102.571 35.357000000000006 +4269 2267 -94.849 36.923 2889489.353144984 714986.4863933682 +2267 4326 2889489.353144984 714986.4863933682 -94.849 36.92300000000001 +4269 2268 -97.21000000000001 34.595 2206248.2204550225 460077.23201637913 +2268 4326 2206248.2204550225 460077.23201637913 -97.21000000000001 34.595 +4269 2268 -99.721 33.7175 1445145.5172335398 144270.90169678623 +2268 4326 1445145.5172335398 144270.90169678623 -99.721 33.717499999999994 +4269 2268 -94.699 35.4725 2951316.764172193 794603.0416681629 +2268 4326 2951316.764172193 794603.0416681629 -94.699 35.4725 +4269 2269 -120.32 45.105000000000004 8248572.7967865 524426.316329996 +2269 4326 8248572.7967865 524426.316329996 -120.32 45.105000000000004 +4269 2269 -123.785 44.0655 7338794.745199684 162963.63331663187 +2269 4326 7338794.745199684 162963.63331663187 -123.78500000000001 44.06550000000001 +4269 2269 -116.855 46.1445 9125766.198555255 924240.7538895387 +2269 4326 9125766.198555255 924240.7538895387 -116.855 46.1445 +4269 2270 -120.75 43.269999999999996 4854679.729683393 584424.3756901834 +2270 4326 4854679.729683393 584424.3756901834 -120.75 43.269999999999996 +4269 2270 -124.21499999999999 42.108999999999995 3913439.4614197942 183569.23273580152 +2270 4326 3913439.4614197942 183569.23273580152 -124.21499999999999 42.10900000000001 +4269 2270 -117.28500000000001 44.431000000000004 5761028.256343373 1023660.1799985798 +2270 4326 5761028.256343373 1023660.1799985798 -117.28500000000001 44.431000000000004 +4269 2271 -77.61500000000001 41.565 2005443.0074909471 509512.7482276559 +2271 4326 2005443.0074909471 509512.7482276559 -77.61500000000001 41.56500000000001 +4269 2271 -80.2385 40.6965 1278519.8962936779 202959.08828575598 +2271 4326 1278519.8962936779 202959.08828575598 -80.2385 40.6965 +4269 2271 -74.9915 42.4335 2713162.781002238 837841.9191798603 +2271 4326 2713162.781002238 837841.9191798603 -74.9915 42.43350000000001 +4269 2272 -77.625 40.445 2003290.5670151063 404990.8114069708 +2272 4326 2003290.5670151063 404990.8114069708 -77.625 40.445 +4269 2272 -80.2395 39.783500000000004 1268910.3859655724 173851.8334754427 +2272 4326 1268910.3859655724 173851.8334754427 -80.2395 39.783500000000025 +4269 2272 -75.0105 41.1065 2723373.094812954 657678.7196798646 +2272 4326 2723373.094812954 657678.7196798646 -75.0105 41.1065 +4269 2273 -80.94 33.629999999999995 2018260.684692299 653690.4562668455 +2273 4326 2018260.684692299 653690.4562668455 -80.94 33.63 +4269 2273 -83.118 32.208 1344841.0557837302 143046.4451570349 +2273 4326 1344841.0557837302 143046.4451570349 -83.118 32.208000000000006 +4269 2273 -78.762 35.052 2669865.503823055 1178401.605605033 +2273 4326 2669865.503823055 1178401.605605033 -78.762 35.052 +4269 2274 -85.98 35.83 1974428.6061747835 544794.8859764934 +2274 4326 1974428.6061747835 544794.8859764934 -85.98 35.830000000000005 +4269 2274 -89.87700000000001 35.065 808512.1567861795 289316.0841076882 +2274 4326 808512.1567861795 289316.0841076882 -89.87700000000001 35.06499999999998 +4269 2274 -82.083 36.595 3118164.3311864496 846296.7747777214 +2274 4326 3118164.3311864496 846296.7747777214 -82.083 36.59500000000002 +4269 2275 -101.50999999999999 35.4 653186.5676299729 3790383.7365547083 +2275 4326 653186.5676299729 3790383.7365547083 -101.50999999999999 35.4 +4269 2275 -102.878 34.41 240500.23425651703 3432961.649287385 +2275 4326 240500.23425651703 3432961.649287385 -102.878 34.40999999999999 +4269 2275 -100.142 36.39 1055901.4972496596 4153505.143594256 +2275 4326 1055901.4972496596 4153505.143594256 -100.142 36.38999999999999 +4269 2276 -98.535 33.15 1957788.3895362925 7101321.754525732 +2276 4326 1957788.3895362925 7101321.754525732 -98.535 33.149999999999984 +4269 2276 -102.61649999999999 31.863 690641.5541047071 6658138.523761735 +2276 4326 690641.5541047071 6658138.523761735 -102.61649999999999 31.863000000000007 +4269 2276 -94.4535 34.437 3188573.937926682 7593156.547892988 +2276 4326 3188573.937926682 7593156.547892988 -94.45350000000002 34.437000000000005 +4269 2277 -100.08 31.025000000000002 2375931.0078311823 10336611.703458842 +2277 4326 2375931.0078311823 10336611.703458842 -100.08 31.025000000000023 +4269 2277 -106.002 29.904500000000002 501084.05560280825 9974761.259641787 +2277 4326 501084.05560280825 9974761.259641787 -106.002 29.90449999999999 +4269 2277 -94.158 32.145500000000006 4207180.662936552 10797158.523251537 +2277 4326 4207180.662936552 10797158.523251537 -94.158 32.14550000000003 +4269 2278 -99.38 29.225 1847301.8679563636 13629544.66048115 +2278 4326 1847301.8679563636 13629544.66048115 -99.38 29.225000000000026 +4269 2278 -104.438 27.924500000000002 212733.5223560003 13197312.465758525 +2278 4326 212733.5223560003 13197312.465758525 -104.43800000000002 27.924499999999995 +4269 2278 -94.322 30.5255 3441205.422198952 14131739.879325563 +2278 4326 3441205.422198952 14131739.879325563 -94.322 30.525500000000008 +4269 2279 -98.525 27.02 976111.3278539426 16896088.26345105 +2279 4326 976111.3278539426 16896088.26345105 -98.525 27.02 +4269 2279 -100.0325 25.948999999999998 480634.4621353835 16509854.80714216 +2279 4326 480634.4621353835 16509854.80714216 -100.0325 25.94900000000002 +4269 2279 -97.0175 28.091 1462288.4670416447 17288247.517767303 +2279 4326 1462288.4670416447 17288247.517767303 -97.0175 28.091 +4269 2280 -111.54 41.28 1629425.9167406955 3625748.2152042827 +2280 4326 1629425.9167406955 3625748.2152042827 -111.54 41.28000000000001 +4269 2280 -113.79 40.623 1004777.0584341529 3394751.5548022916 +2280 4326 1004777.0584341529 3394751.5548022916 -113.79 40.62299999999999 +4269 2280 -109.29 41.937 2241686.854663251 3872791.005300402 +2280 4326 2241686.854663251 3872791.005300402 -109.29 41.937000000000005 +4269 2281 -111.545 39.785 1627774.4115609722 7090428.804545079 +2281 4326 1627774.4115609722 7090428.804545079 -111.545 39.785000000000004 +4269 2281 -113.7995 38.6195 983391.7680608317 6674363.952587393 +2281 4326 983391.7680608317 6674363.952587393 -113.79950000000001 38.61950000000001 +4269 2281 -109.29050000000001 40.9505 2250765.3260073643 7522550.713730533 +2281 4326 2250765.3260073643 7522550.713730533 -109.29050000000001 40.950500000000005 +4269 2282 -111.545 37.785 1627415.4240844045 10249729.178458244 +2282 4326 1627415.4240844045 10249729.178458244 -111.545 37.785000000000004 +4269 2282 -113.7995 37.069500000000005 969550.1494510765 9997448.321856607 +2282 4326 969550.1494510765 9997448.321856607 -113.79950000000001 37.069500000000026 +4269 2282 -109.29050000000001 38.500499999999995 2272727.0324092847 10517754.31046072 +2282 4326 2272727.0324092847 10517754.31046072 -109.29050000000001 38.5005 +4269 2283 -78.285 38.615 11544347.656693192 6907090.8251683945 +2283 4326 11544347.656693192 6907090.8251683945 -78.285 38.614999999999995 +4269 2283 -79.88250000000001 37.8545 11083745.13895743 6633075.237706761 +2283 4326 11083745.13895743 6633075.237706761 -79.88250000000001 37.85450000000001 +4269 2283 -76.6875 39.3755 11995292.747702997 7189062.41980253 +2283 4326 11995292.747702997 7189062.41980253 -76.6875 39.3755 +4269 2284 -79.495 37.41 11193933.021261292 3674360.1158166635 +2284 4326 11193933.021261292 3674360.1158166635 -79.495 37.41000000000001 +4269 2284 -83.26150000000001 36.627 10086194.333201073 3422986.0149178724 +2284 4326 10086194.333201073 3422986.0149178724 -83.26150000000001 36.626999999999995 +4269 2284 -75.7285 38.193 12279389.374483127 3969652.6620305968 +2284 4326 12279389.374483127 3969652.6620305968 -75.7285 38.19300000000001 +4269 2285 -120.905 48.065 1622893.336592662 388488.2872352653 +2285 4326 1622893.336592662 388488.2872352653 -120.90500000000002 48.065 +4269 2285 -124.4015 47.1785 753283.1432020159 85682.59365874318 +2285 4326 753283.1432020159 85682.59365874318 -124.4015 47.178500000000014 +4269 2285 -117.4085 48.951499999999996 2463163.581283079 730203.9302677546 +2285 4326 2463163.581283079 730203.9302677546 -117.4085 48.951499999999996 +4269 2286 -120.655 46.575 1601437.7114832425 452829.2448668882 +2286 4326 1601437.7114832425 452829.2448668882 -120.655 46.575 +4269 2286 -124.02550000000001 45.643499999999996 738951.1663367104 133258.40356679758 +2286 4326 738951.1663367104 133258.40356679758 -124.02550000000001 45.64349999999999 +4269 2286 -117.2845 47.5065 2434972.0471441783 808725.3564848994 +2286 4326 2434972.0471441783 808725.3564848994 -117.2845 47.5065 +4269 2287 -90.47 46.34 1849792.6205940612 428206.0430836391 +2287 4326 1849792.6205940612 428206.0430836391 -90.47 46.339999999999996 +4269 2287 -92.648 45.467 1289206.367828562 120839.77133789152 +2287 4326 1289206.367828562 120839.77133789152 -92.648 45.467 +4269 2287 -88.292 47.213 2393010.4543253123 750822.4822996728 +2287 4326 2393010.4543253123 750822.4822996728 -88.292 47.212999999999994 +4269 2288 -89.57 44.89 2079939.3338098514 385515.57975344936 +2288 4326 2079939.3338098514 385515.57975344936 -89.57000000000001 44.88999999999999 +4269 2288 -92.558 44.071 1296268.2423315719 97233.35493690065 +2288 4326 1296268.2423315719 97233.35493690065 -92.558 44.071 +4269 2288 -86.582 45.708999999999996 2841490.0879038563 702216.019093216 +2288 4326 2841490.0879038563 702216.019093216 -86.582 45.708999999999996 +4269 2289 -89.19 43.405 2183746.5303250942 513124.4753014687 +2289 4326 2183746.5303250942 513124.4753014687 -89.19 43.404999999999994 +4269 2289 -91.206 42.5725 1643639.9265706283 211011.95786028268 +2289 4326 1643639.9265706283 211011.95786028268 -91.206 42.57250000000001 +4269 2289 -87.174 44.2375 2709056.968022254 828089.9825322251 +2289 4326 2709056.968022254 828089.9825322251 -87.174 44.23750000000001 +4122 2290 -63.19499999999999 46.495000000000005 685030.6051247243 316092.6869030101 +4122 2290 -64.3605 45.9595 594533.1843764923 257460.57403218484 +4122 2290 -62.0295 47.0305 773763.0099565941 376057.46583844506 +4122 2294 -61.364999999999995 45.86 4510482.836694555 5080024.132179123 +4122 2294 -62.8365 44.762 4394196.379282401 4958867.166611322 +4122 2294 -59.893499999999996 46.958 4622266.336516277 5203308.8554451 +4122 2295 -64.64 44.715 5488907.969974017 4952785.25102928 +4122 2295 -66.116 43.540499999999994 5369402.869775871 4823553.418503422 +4122 2295 -63.164 45.889500000000005 5603686.107047289 5084161.86687722 +4196 2296 -37.835 65.715 -99405.5010398147 137177.2375697416 +2296 4326 -99405.5010398147 137177.2375697416 -37.82820125647335 65.7167527722183 +4196 2296 -38.7575 65.53949999999999 -57439.622110589466 116470.75081060779 +2296 4326 -57439.622110589466 116470.75081060779 -38.750847811959424 65.5412854714645 +4196 2296 -36.9125 65.8905 -140781.76445991412 158481.3630583012 +2296 4326 -140781.76445991412 158481.3630583012 -36.90555435274059 65.8922193485981 +4194 2299 -69.265 78.5 117090.44187045928 -106370.87701700252 +2299 4326 117090.44187045928 -106370.87701700252 -69.25587785947896 78.49993256834172 +4194 2299 -72.8875 78.05 204790.5652710266 -146271.7747913213 +2299 4326 204790.5652710266 -146271.7747913213 -72.87889293070326 78.05001746191611 +4194 2299 -65.6425 78.95 35156.90747007614 -60918.59831095058 +2299 4326 35156.90747007614 -60918.59831095058 -65.63285784036708 78.94984524071617 +4194 2301 -64.55000000000001 76.5 14338.859124053408 66.92052328953325 +2301 4326 14338.859124053408 66.92052328953325 -64.54200840111216 76.49968412136722 +4194 2301 -71.96600000000001 75.15 227363.39506196923 -135324.50640758817 +2301 4326 227363.39506196923 -135324.50640758817 -71.95896183563434 75.1498381029963 +4194 2301 -57.134 77.85 -161074.6254282519 160128.9011395208 +2301 4326 -161074.6254282519 160128.9011395208 -57.12498675188246 77.84952185929362 +4194 2303 -55.115 73.5 98750.12861986719 2574.414167481958 +2303 4326 98750.12861986719 2574.414167481958 -55.10824498044689 73.49924151769486 +4194 2303 -57.9005 72.15 201689.25023464896 -140709.44583619176 +2303 4326 201689.25023464896 -140709.44583619176 -57.89424705627786 72.14926232903044 +4194 2303 -52.3295 74.85 9619.383232280923 150721.86834068084 +2303 4326 9619.383232280923 150721.86834068084 -52.32216892348902 74.84922044555485 +4194 2304 -52.585 70.5 21803.8786442654 104.9269135806139 +2304 4326 21803.8786442654 104.9269135806139 -52.57921057263862 70.49902163107643 +4194 2304 -55.712500000000006 69.15 147481.26019724988 -146121.29878321086 +2304 4326 147481.26019724988 -146121.29878321086 -55.70706640616302 69.14905498572772 +4194 2304 -49.457499999999996 71.85 -88437.04046357764 152499.72744850858 +2304 4326 -88437.04046357764 152499.72744850858 -49.451318437777395 71.84898804603951 +4194 2305 -51.745000000000005 67.5 -10894.731528186643 22.398557129013806 +2305 4326 -10894.731528186643 22.398557129013806 -51.739920408628805 67.49886201607141 +4194 2305 -53.855500000000006 66.15 83767.94521870193 -149318.60396424824 +2305 4326 83767.94521870193 -149318.60396424824 -53.85068234868391 66.14886687926038 +4194 2305 -49.6345 68.85 -95297.571230758 152415.37442766177 +2305 4326 -95297.571230758 152415.37442766177 -49.62913097898297 68.84885753033039 +4194 2306 -51.435 64.5 -27152.378726405677 120.83557637651799 +2306 4326 -27152.378726405677 120.83557637651799 -51.430459227002274 64.49872679392531 +4194 2306 -53.4735 63.15 74300.3978157826 -149648.87933650656 +2306 4326 74300.3978157826 -149648.87933650656 -53.469161996222425 63.1487319444158 +4194 2306 -49.3965 65.85 -118911.40711781805 152978.35199218948 +2306 4326 -118911.40711781805 152978.35199218948 -49.3917385959623 65.84872209508191 +4194 2307 -46.620000000000005 61.370000000000005 -73796.62614611 -13706.319840273842 +2307 4326 -73796.62614611 -13706.319840273842 -46.615913826597826 61.36846759298627 +4194 2307 -50.31 59.903 129303.88026787952 -175683.74979648332 +2307 4326 129303.88026787952 -175683.74979648332 -50.30607039363252 59.90151922230509 +4194 2307 -42.93000000000001 62.837 -258166.5908960949 159069.81637632835 +2307 4326 -258166.5908960949 159069.81637632835 -42.92575597700727 62.835416063259274 +4211 2308 107.89 -5.48 377052.2244279434 9394221.208264098 +2308 4326 377052.2244279434 9394221.208264098 107.89137971423116 -5.479874253787102 +4211 2308 105.982 -6.749 166371.5353508533 9253036.436372293 +2308 4326 166371.5353508533 9253036.436372293 105.98361099903742 -6.748753796999428 +4211 2308 109.798 -4.211 588551.8748147851 9534548.92745914 +2308 4326 588551.8748147851 9534548.92745914 109.79914854840504 -4.210997374584179 +4326 2309 114.905 -7.630000000000001 379218.4536799919 9156452.60453265 +4326 2309 113.0105 -8.377 170725.20836313715 9072771.5024337 +4326 2309 116.79950000000001 -6.883 588330.2111615072 9239111.187111234 +4326 2310 132.855 -2.455 595056.8936273811 9728616.687791126 +4326 2310 131.98649999999998 -2.8914999999999997 498499.68774514465 9680399.639616836 +4326 2310 133.7235 -2.0185 691693.2286862351 9776792.6015752 +4326 2311 5.575 4.15 452830.88175176876 458720.3983699258 +4326 2311 2.9515000000000002 2.143 160853.25410008803 237204.62032398567 +4326 2311 8.1985 6.157 743294.9638360711 681060.5551183342 +4197 2312 13.545 9.395 340232.99793454097 1038759.6270484249 +4197 2312 12.964500000000001 8.9675 276200.8154212781 991788.7624669424 +4197 2312 14.125499999999999 9.8225 404101.8024715168 1085813.9552022684 +4198 2313 14.629999999999999 12.235 459756.30044968944 1352445.7472552252 +4198 2313 14.216 11.753499999999999 414573.5565275442 1299298.4999531491 +4198 2313 15.044 12.7165 504776.8747811173 1405659.2759951334 +4302 2314 -61.045 10.67 387300.25904785877 297405.693941205 +2314 4326 387300.25904785877 297405.693941205 -61.044234609427846 10.673706279954718 +4302 2314 -61.9855 9.914 49140.1947652107 23255.096147302906 +2314 4326 49140.1947652107 23255.096147302906 -61.98477826058431 9.91775068732127 +4302 2314 -60.1045 11.426 723773.7489552818 572638.0490714883 +2314 4326 723773.7489552818 572638.0490714883 -60.103690816070305 11.429661263792346 +4221 2315 -67.31 -53.129999999999995 613076.4702859066 4111809.083900963 +2315 4326 613076.4702859066 4111809.083900963 -67.31125619947368 -53.13003823450168 +4221 2315 -68.489 -54.461999999999996 533125.3084201551 3964823.5734128803 +2315 4326 533125.3084201551 3964823.5734128803 -68.49035429845044 -54.46207441102089 +4221 2315 -66.131 -51.798 697833.822823736 4257415.70917476 +2315 4326 697833.822823736 4257415.70917476 -66.13216413607071 -51.79800231549117 +4221 2316 -63.745000000000005 -53.144999999999996 450168.4144668201 4111215.4096910385 +2316 4326 450168.4144668201 4111215.4096910385 -63.74608455114407 -53.14507383415932 +4221 2316 -65.7745 -54.7515 321438.65618108225 3929197.504401317 +2316 4326 321438.65618108225 3929197.504401317 -65.77572943502865 -54.7516142241334 +4221 2316 -61.715500000000006 -51.5385 589088.12175685 4289389.239636066 +2316 4326 589088.12175685 4289389.239636066 -61.71644961246772 -51.53853276412927 +4248 2317 -66.59 6.444999999999999 934818.1907710684 1049180.5024165895 +2317 4326 934818.1907710684 1049180.5024165895 -66.59182591337297 6.441762781340399 +4248 2317 -72.701 1.2205 252643.84478656168 476161.45228655584 +2317 4326 252643.84478656168 476161.45228655584 -72.70306850530125 1.2171592001059952 +4248 2317 -60.479 11.6695 1604111.6002792686 1630270.84097842 +2317 4326 1604111.6002792686 1630270.84097842 -60.48057253008846 11.666430255645858 +4204 2318 45.09 24.265 -292646.5282433285 -87298.41803862446 +2318 4326 -292646.5282433285 -87298.41803862446 45.08935652284713 24.265435593903618 +4204 2318 35.568 17.1595 -1320396.9966975104 -811466.0683219883 +2318 4326 -1320396.9966975104 -811466.0683219883 35.56697749328454 17.15977229928963 +4204 2318 54.612 31.370499999999996 626515.1267405587 706085.0827936294 +2318 4326 626515.1267405587 706085.0827936294 54.611788826333324 31.37111462693132 +4230 2319 27.060000000000002 39.305 505175.4293845561 4352440.064054994 +2319 4326 505175.4293845561 4352440.064054994 27.05952253824716 39.303981156269145 +4230 2319 25.764 36.7805 389658.7872946277 4072930.682445483 +2319 4326 389658.7872946277 4072930.682445483 25.76351248731653 36.77942218985678 +4230 2319 28.356 41.829499999999996 612651.3623849915 4633670.128174798 +2319 4326 612651.3623849915 4633670.128174798 28.35553243222133 41.82854710888551 +4230 2320 30.0 38.760000000000005 500000.0 4291932.892534684 +2320 4326 500000.0 4291932.892534684 29.999599500225692 38.75898353808558 +4230 2320 28.65 36.330000000000005 378779.41008437856 4023071.177238889 +2320 4326 378779.41008437856 4023071.177238889 28.649584123507626 36.32892760932367 +4230 2320 31.35 41.19 613262.2702814089 4562632.456819124 +2320 4326 613262.2702814089 4562632.456819124 31.349615270031258 41.1890458325545 +4230 2321 33.0 39.019999999999996 500000.0 4320797.170080892 +2321 4326 500000.0 4320797.170080892 32.999673065826194 39.019001968674566 +4230 2321 31.65 36.275 378694.24842006346 4016967.4547359454 +2321 4326 378694.24842006346 4016967.4547359454 31.64965667206723 36.273939338813605 +4230 2321 34.35 41.765 612265.4252185497 4626497.864396563 +2321 4326 612265.4252185497 4626497.864396563 34.349689947267734 41.764072585823726 +4230 2322 36.0 38.980000000000004 500000.0 4316356.427731224 +2322 4326 500000.0 4316356.427731224 35.99975003537722 38.97901183234034 +4230 2322 34.65 36.127 378465.6428237766 4000543.157354169 +2322 4326 378465.6428237766 4000543.157354169 34.64973088691637 36.12594739522167 +4230 2322 37.35 41.833 612146.7842430831 4634051.04519292 +2322 4326 612146.7842430831 4634051.04519292 37.34977002003097 41.83208476316438 +4230 2323 39.0 38.925 500000.0 4310250.457054633 +2323 4326 500000.0 4310250.457054633 38.999828022742946 38.924019354696725 +4230 2323 37.65 36.8865 379647.3771279679 4084832.7611922882 +2323 4326 379647.3771279679 4084832.7611922882 37.64980211819477 36.885471734737436 +4230 2323 40.35 40.963499999999996 613651.7997103988 4537476.707245884 +2323 4326 613651.7997103988 4537476.707245884 40.34985523173121 40.96257113156767 +4230 2324 42.0 39.31 500000.0 4352993.469796902 +2324 4326 500000.0 4352993.469796902 41.99990532490977 39.309034968524415 +4230 2324 40.65 37.249 380218.89304467506 4125066.7587728184 +2324 4326 380218.89304467506 4125066.7587728184 40.649876964926584 37.24798661411629 +4230 2324 43.35 41.371 612949.7121168717 4582735.52095337 +2324 4326 612949.7121168717 4582735.52095337 43.34993522751083 41.370087476758506 +4230 2325 44.165 38.995000000000005 427658.0700836332 4318353.418230682 +2325 4326 427658.0700836332 4318353.418230682 44.16496343992102 38.99403132831345 +4230 2325 43.5665 37.1725 372681.30508527573 4116684.6763743437 +2325 4326 372681.30508527573 4116684.6763743437 43.56645209547583 37.17149030870179 +4230 2325 44.7635 40.8175 480046.33142227726 4520411.732129788 +2325 4326 480046.33142227726 4520411.732129788 44.7634752268011 40.816575725967546 +4611 2326 114.135 22.354999999999997 832207.5038205493 823817.3611366774 +2326 4326 832207.5038205493 823817.3611366774 114.1374537483437 22.35347093287418 +4611 2326 113.7975 22.1525 797385.8089023897 801442.0440629409 +2326 4326 797385.8089023897 801442.0440629409 113.7999376187216 22.150976247169567 +4611 2326 114.47250000000001 22.557499999999997 866928.6618134802 846270.9842850824 +2326 4326 866928.6618134802 846270.9842850824 114.47496985006404 22.55596555189677 +4610 2327 75.815 38.245000000000005 13571345.250657804 4235008.818657861 +4610 2327 73.8395 35.7025 13394971.097569454 3953155.8044916936 +4610 2327 77.79050000000001 40.7875 13735545.582083406 4520725.513101159 +4610 2328 80.99000000000001 38.195 14499124.005826326 4229144.711549568 +4610 2328 78.281 30.0635 14237771.484325873 3330272.6065883264 +4610 2328 83.699 46.326499999999996 14707836.926492717 5135922.208550304 +4610 2329 87.0 38.25 15500000.0 4235249.705157654 +4610 2329 84.3 28.413 15235388.295915188 3147181.090018248 +4610 2329 89.7 48.087 15701141.61559494 5331631.744571889 +4610 2330 93.005 37.805 16500440.32283383 4185856.029482364 +4610 2330 90.3005 28.7195 16236203.652319929 3181169.5965863992 +4610 2330 95.7095 46.8905 16706489.472547794 5198642.0357886 +4610 2331 99.005 32.155 17500471.669382494 3559041.76978484 +4610 2331 96.3005 22.232499999999998 17221626.361011192 2462029.2913352987 +4610 2331 101.7095 42.0775 17724220.30477846 4663801.518843819 +4610 2332 105.0 32.0 18500000.0 3541854.084933069 +4610 2332 102.3 22.577 18222261.021682594 2500208.9751151656 +4610 2332 107.7 41.423 18725717.148376863 4591073.037755179 +4610 2333 111.0 31.61 19500000.0 3498609.5369066605 +4610 2333 108.3 19.46 19216419.533092003 2154815.9094360094 +4610 2333 113.7 43.76 19717431.45547606 4850702.946278921 +4610 2334 117.0 36.83 20500000.0 4077646.947601763 +4610 2334 114.3 23.609 20224376.644181263 2614586.7028427026 +4610 2334 119.7 50.051 20693361.273710195 5550016.202951275 +4610 2335 123.0 39.95 21500000.0 4423979.386178028 +4610 2335 120.3 27.701 21233637.016470857 3068227.7412225064 +4610 2335 125.7 52.199 21684590.422711518 5788926.532028645 +4610 2336 129.0 46.84 22500000.0 5189462.346371031 +4610 2336 126.3 41.485 22274497.85999311 4597959.884360498 +4610 2336 131.7 52.195 22684607.00258415 5788481.570115338 +4610 2337 133.385 46.71 23376507.436775956 5176277.722460048 +4610 2337 132.1385 45.189 23275123.154263176 5009936.441354034 +4610 2337 134.63150000000002 48.231 23472623.553209472 5344181.234955628 +4610 2338 75.815 38.245000000000005 571345.250657805 4235008.818657861 +4610 2338 73.8395 35.7025 394971.09756945376 3953155.8044916936 +4610 2338 77.79050000000001 40.7875 735545.5820834065 4520725.513101159 +4610 2339 80.99000000000001 38.195 499124.0058263265 4229144.711549568 +4610 2339 78.281 30.0635 237771.48432587233 3330272.6065883264 +4610 2339 83.699 46.326499999999996 707836.9264927164 5135922.208550304 +4610 2340 87.0 38.25 500000.0 4235249.705157654 +4610 2340 84.3 28.413 235388.29591518908 3147181.090018248 +4610 2340 89.7 48.087 701141.6155949405 5331631.744571889 +4610 2341 93.005 37.805 500440.32283383055 4185856.029482364 +4610 2341 90.3005 28.7195 236203.6523199294 3181169.5965863992 +4610 2341 95.7095 46.8905 706489.472547794 5198642.0357886 +4610 2342 99.005 32.155 500471.66938249447 3559041.76978484 +4610 2342 96.3005 22.232499999999998 221626.36101119395 2462029.2913352987 +4610 2342 101.7095 42.0775 724220.3047784602 4663801.518843819 +4610 2343 105.0 32.0 500000.0 3541854.084933069 +4610 2343 102.3 22.577 222261.02168259275 2500208.9751151656 +4610 2343 107.7 41.423 725717.1483768641 4591073.037755179 +4610 2344 111.0 31.61 500000.0 3498609.5369066605 +4610 2344 108.3 19.46 216419.53309200198 2154815.9094360094 +4610 2344 113.7 43.76 717431.4554760591 4850702.946278921 +4610 2345 117.0 36.83 500000.0 4077646.947601763 +4610 2345 114.3 23.609 224376.64418126113 2614586.7028427026 +4610 2345 119.7 50.051 693361.273710196 5550016.202951275 +4610 2346 123.0 39.95 500000.0 4423979.386178028 +4610 2346 120.3 27.701 233637.01647085644 3068227.7412225064 +4610 2346 125.7 52.199 684590.4227115195 5788926.532028645 +4610 2347 129.0 46.84 500000.0 5189462.346371031 +4610 2347 126.3 41.485 274497.85999311064 4597959.884360498 +4610 2347 131.7 52.195 684607.0025841484 5788481.570115338 +4610 2348 133.385 46.71 376507.4367759548 5176277.722460048 +4610 2348 132.1385 45.189 275123.15426317527 5009936.441354034 +4610 2348 134.63150000000002 48.231 472623.5532094711 5344181.234955628 +4610 2349 75.06 38.230000000000004 25505253.448643483 4233031.385844433 +4610 2349 73.76400000000001 36.052 25388628.157680415 3992021.4597235457 +4610 2349 76.356 40.408 25615103.754962932 4475717.861885255 +4610 2350 78.0 36.43 26500000.0 4033258.6378218005 +4610 2350 76.65 31.57 26371837.053786915 3494964.923564048 +4610 2350 79.35 41.29 26613084.63080394 4573661.068576075 +4610 2351 81.005 37.915 27500439.668921795 4198065.35641591 +4610 2351 79.6505 30.7465 27370771.512882173 3403649.1848779754 +4610 2351 82.35950000000001 45.0835 27607036.50293452 4995125.604770829 +4610 2352 84.0 37.745 28500000.0 4179196.48119099 +4610 2352 82.65 29.2085 28368715.85139857 3233135.2859830335 +4610 2352 85.35 46.281499999999994 28604043.49696246 5128264.162000983 +4610 2353 87.0 38.49 29500000.0 4261890.555249193 +4610 2353 85.65 28.869 29368285.345435295 3195500.7445130963 +4610 2353 88.35 48.111 29600526.977949504 5331654.051476515 +4610 2354 90.0 37.870000000000005 30500000.0 4193070.5923365303 +4610 2354 88.64099999999999 28.375 30366784.874901075 3140752.5484572803 +4610 2354 91.35900000000001 47.365 30602653.365315687 5248723.830699005 +4610 2355 93.0 36.42 31500000.0 4032148.9683192875 +4610 2355 91.65 28.581 31367923.7677554 3163576.5294280793 +4610 2355 94.35 44.259 31607806.549651965 4903489.849537769 +4610 2356 96.005 36.365 32500448.732087124 4026045.8309534066 +4610 2356 94.6505 29.0435 32368554.751517516 3214843.7845557197 +4610 2356 97.35950000000001 43.686499999999995 32609613.368111134 4839890.2149727205 +4610 2357 99.0 32.095 33500000.0 3552388.415763171 +4610 2357 97.65 22.4965 33361077.349725455 2489407.1940446063 +4610 2357 100.35 41.6935 33612384.926086396 4618476.72611028 +4610 2358 102.0 31.909999999999997 34500000.0 3531874.338258858 +4610 2358 100.65 22.208 34360790.97431925 2457453.767730824 +4610 2358 103.35 41.611999999999995 34612526.70678151 4609424.500535976 +4610 2359 105.005 32.355000000000004 35500470.636472546 3581220.0366228293 +4610 2359 103.6505 23.485500000000002 35362136.77188357 2598953.7592391116 +4610 2359 106.35950000000001 41.2245 35613994.279077254 4566398.856200186 +4610 2360 108.005 30.33 36500480.82747432 3356697.1984122195 +4610 2360 106.6505 19.404 36358244.51767182 2146944.3876509992 +4610 2360 109.35950000000001 41.256 36613939.546410866 4569897.333182056 +4610 2361 111.0 31.61 37500000.0 3498609.5369066605 +4610 2361 109.65 19.46 37358240.60123307 2153145.1988011487 +4610 2361 112.35 43.76 37608714.37893617 4848044.164383709 +4610 2362 114.0 33.485 38500000.0 3706540.530376147 +4610 2362 112.65 22.7165 38361298.09077554 2513774.4834409636 +4610 2362 115.35 44.2535 38607816.600744836 4902878.699716278 +4610 2363 117.0 36.24 39500000.0 4012175.235580028 +4610 2363 115.65 23.964000000000002 39362588.27490841 2651960.198146494 +4610 2363 118.35 48.516000000000005 39599734.43183913 5376687.193502496 +4610 2364 120.0 38.879999999999995 40500000.0 4305184.272219103 +4610 2364 118.65 25.875 40364690.497254826 2863683.2821460436 +4610 2364 121.35 51.885 40592950.71673524 5751412.242580876 +4610 2365 123.0 40.89 41500000.0 4528359.590621818 +4610 2365 121.65 29.487 41369072.44682357 3264009.1505943863 +4610 2365 124.35 52.293 41592106.69332327 5796806.956986351 +4610 2366 126.0 46.695 42500000.0 5173343.224546207 +4610 2366 124.65 40.8405 42386142.53273014 4523739.820513939 +4610 2366 127.35 52.5495 42591573.65911539 5825347.072391469 +4610 2367 129.0 45.81 43500000.0 5074969.89720496 +4610 2367 127.65 41.814 43387825.119173355 4631860.914516358 +4610 2367 130.35 49.806 43597176.740956746 5520146.116749244 +4610 2368 132.0 45.650000000000006 44500000.0 5057186.521951529 +4610 2368 130.65 42.743 44389461.14318846 4735055.843450882 +4610 2368 133.35 48.557 44599653.91816555 5381246.271956358 +4610 2369 134.135 47.125 45434365.303493276 5221509.050196059 +4610 2369 133.5635 45.9775 45388679.20372939 5094590.96471234 +4610 2369 134.7065 48.2725 45478213.05179191 5348771.8239661725 +4610 2370 75.06 38.230000000000004 505253.44864348357 4233031.385844433 +4610 2370 73.76400000000001 36.052 388628.15768041427 3992021.4597235457 +4610 2370 76.356 40.408 615103.7549629308 4475717.861885255 +4610 2371 78.0 36.43 500000.0 4033258.6378218005 +4610 2371 76.65 31.57 371837.0537869159 3494964.923564048 +4610 2371 79.35 41.29 613084.6308039381 4573661.068576075 +4610 2372 81.005 37.915 500439.6689217947 4198065.35641591 +4610 2372 79.6505 30.7465 370771.5128821739 3403649.1848779754 +4610 2372 82.35950000000001 45.0835 607036.5029345183 4995125.604770829 +4610 2373 84.0 37.745 500000.0 4179196.48119099 +4610 2373 82.65 29.2085 368715.85139856936 3233135.2859830335 +4610 2373 85.35 46.281499999999994 604043.4969624619 5128264.162000983 +4610 2374 87.0 38.49 500000.0 4261890.555249193 +4610 2374 85.65 28.869 368285.3454352962 3195500.7445130963 +4610 2374 88.35 48.111 600526.9779495029 5331654.051476515 +4610 2375 90.0 37.870000000000005 500000.0 4193070.5923365303 +4610 2375 88.64099999999999 28.375 366784.87490107387 3140752.5484572803 +4610 2375 91.35900000000001 47.365 602653.3653156867 5248723.830699005 +4610 2376 93.0 36.42 500000.0 4032148.9683192875 +4610 2376 91.65 28.581 367923.76775540144 3163576.5294280793 +4610 2376 94.35 44.259 607806.5496519671 4903489.849537769 +4610 2377 96.005 36.365 500448.73208712274 4026045.8309534066 +4610 2377 94.6505 29.0435 368554.7515175141 3214843.7845557197 +4610 2377 97.35950000000001 43.686499999999995 609613.3681111338 4839890.2149727205 +4610 2378 99.0 32.095 500000.0 3552388.415763171 +4610 2378 97.65 22.4965 361077.3497254554 2489407.1940446063 +4610 2378 100.35 41.6935 612384.9260863985 4618476.72611028 +4610 2379 102.0 31.909999999999997 500000.0 3531874.338258858 +4610 2379 100.65 22.208 360790.9743192494 2457453.767730824 +4610 2379 103.35 41.611999999999995 612526.7067815047 4609424.500535976 +4610 2380 105.005 32.355000000000004 500470.6364725469 3581220.0366228293 +4610 2380 103.6505 23.485500000000002 362136.77188357105 2598953.7592391116 +4610 2380 106.35950000000001 41.2245 613994.2790772517 4566398.856200186 +4610 2381 108.005 30.33 500480.82747432083 3356697.1984122195 +4610 2381 106.6505 19.404 358244.51767182356 2146944.3876509992 +4610 2381 109.35950000000001 41.256 613939.5464108662 4569897.333182056 +4610 2382 111.0 31.61 500000.0 3498609.5369066605 +4610 2382 109.65 19.46 358240.601233072 2153145.1988011487 +4610 2382 112.35 43.76 608714.3789361705 4848044.164383709 +4610 2383 114.0 33.485 500000.0 3706540.530376147 +4610 2383 112.65 22.7165 361298.09077553905 2513774.4834409636 +4610 2383 115.35 44.2535 607816.6007448363 4902878.699716278 +4610 2384 117.0 36.24 500000.0 4012175.235580028 +4610 2384 115.65 23.964000000000002 362588.2749084106 2651960.198146494 +4610 2384 118.35 48.516000000000005 599734.4318391307 5376687.193502496 +4610 2385 120.0 38.879999999999995 500000.0 4305184.272219103 +4610 2385 118.65 25.875 364690.49725482846 2863683.2821460436 +4610 2385 121.35 51.885 592950.7167352348 5751412.242580876 +4610 2386 123.0 40.89 500000.0 4528359.590621818 +4610 2386 121.65 29.487 369072.4468235636 3264009.1505943863 +4610 2386 124.35 52.293 592106.6933232703 5796806.956986351 +4610 2387 126.0 46.695 500000.0 5173343.224546207 +4610 2387 124.65 40.8405 386142.53273013985 4523739.820513939 +4610 2387 127.35 52.5495 591573.6591153868 5825347.072391469 +4610 2388 129.0 45.81 500000.0 5074969.89720496 +4610 2388 127.65 41.814 387825.1191733576 4631860.914516358 +4610 2388 130.35 49.806 597176.7409567473 5520146.116749244 +4610 2389 132.0 45.650000000000006 500000.0 5057186.521951529 +4610 2389 130.65 42.743 389461.1431884597 4735055.843450882 +4610 2389 133.35 48.557 599653.9181655485 5381246.271956358 +4610 2390 134.135 47.125 434365.3034932788 5221509.050196059 +4610 2390 133.5635 45.9775 388679.20372938935 5094590.96471234 +4610 2390 134.7065 48.2725 478213.0517919089 5348771.8239661725 +4123 2391 21.0 64.545 1500000.0 7160789.552828479 +2391 4326 1500000.0 7160789.552828479 20.996061539396976 64.54524529353982 +4123 2391 19.65 60.238499999999995 1425212.6254788171 6681566.757746877 +2391 4326 1425212.6254788171 6681566.757746877 19.64656184696919 60.23852715420943 +4123 2391 22.35 68.8515 1554377.0993658146 7641658.2264754465 +2391 4326 1554377.0993658146 7641658.2264754465 22.3453711279682 68.85197468555896 +4123 2392 24.0 64.325 2500000.0 7136261.94646822 +2392 4326 2500000.0 7136261.94646822 23.996270664553556 64.32531863031436 +4123 2392 22.65 60.2075 2425141.983816879 6678113.196635558 +2392 4326 2425141.983816879 6678113.196635558 22.64671367394419 60.2076098290862 +4123 2392 25.35 68.44250000000001 2555378.272647468 7596043.669441385 +2392 4326 2555378.272647468 7596043.669441385 25.3456709781469 68.44303758532845 +4123 2393 27.005000000000003 65.135 3500234.6985476133 7226571.789272731 +2393 4326 3500234.6985476133 7226571.789272731 27.00134232388093 65.1354329251207 +4123 2393 25.6505 60.6755 3426238.1216247417 6730251.904482641 +2393 4326 3426238.1216247417 6730251.904482641 25.647319893050742 60.67570904772343 +4123 2393 28.3595 69.59450000000001 3552920.9290734655 7724536.3607214615 +2393 4326 3552920.9290734655 7724536.3607214615 28.355171280202438 69.59516810465588 +4123 2394 30.0 65.375 4500000.0 7253332.156760518 +2394 4326 4500000.0 7253332.156760518 29.996508873772306 65.37551815844058 +4123 2394 28.65 61.3835 4427837.170109958 6809136.433095777 +2394 4326 4427837.170109958 6809136.433095777 28.646911720772422 61.383814461497266 +4123 2394 31.35 69.3665 4553112.456144316 7699097.6333929 +2394 4326 4553112.456144316 7699097.6333929 31.345962700971445 69.36723049817161 +4164 2395 45.69 15.245 8574127.473700216 1686246.3039332384 +2395 4326 8574127.473700216 1686246.3039332384 45.68960889284199 15.245957386935585 +4164 2395 43.602 12.8105 8348207.828790396 1417182.4679428688 +2395 4326 8348207.828790396 1417182.4679428688 43.60156229828455 12.811402143436569 +4164 2395 47.778 17.679499999999997 8794830.772811938 1957716.2107270996 +2395 4326 8794830.772811938 1957716.2107270996 47.77765638749471 17.6805114376577 +4164 2396 50.57 16.47 9454083.203315603 1821735.3451761077 +2396 4326 9454083.203315603 1821735.3451761077 50.569728917116116 16.470990594521567 +4164 2396 48.257 14.193 9203806.514773361 1571466.617139675 +2396 4326 9203806.514773361 1571466.617139675 48.25667415437224 14.1939389815052 +4164 2396 52.883 18.747 9698594.482178858 2074749.4939064279 +2396 4326 9698594.482178858 2074749.4939064279 52.882784990429 18.748040691089724 +4178 2397 10.21 50.955 3585021.4425204406 5647876.100302413 +2397 4326 3585021.4425204406 5647876.100302413 10.208188971464955 50.954547866650444 +4178 2397 9.949 50.4105 3567458.726292648 5587036.941448476 +2397 4326 3567458.726292648 5587036.941448476 9.947209084564912 50.41003787702326 +4178 2397 10.471 51.499500000000005 3602147.3032476585 5708783.181510262 +2397 4326 3602147.3032476585 5708783.181510262 10.469168261549225 51.49905795517159 +4178 2398 12.0 51.855000000000004 4500000.0 5747310.9234197 +2398 4326 4500000.0 5747310.9234197 11.998156690048944 51.854585403636335 +4178 2398 10.65 49.2585 4401729.994885185 5459349.248434758 +2398 4326 4401729.994885185 5459349.248434758 10.648250857483418 49.258035601092274 +4178 2398 13.35 54.4515 4587565.41501431 6037117.381924261 +2398 4326 4587565.41501431 6037117.381924261 13.348048229140222 54.45113744381812 +4178 2399 15.0 50.629999999999995 5500000.0 5611023.792190646 +2399 4326 5500000.0 5611023.792190646 14.998253051864838 50.62962218214674 +4178 2399 13.65 46.949 5397225.661667873 5202553.803352359 +2399 4326 5397225.661667873 5202553.803352359 13.648374899750717 46.94856692336571 +4178 2399 16.35 54.311 5587865.001007058 6021479.161634348 +2399 4326 5587865.001007058 6021479.161634348 16.348103677053906 54.310680734260664 +4214 2401 75.06 38.230000000000004 25505253.534156736 4233104.345126611 +4214 2401 73.76400000000001 36.052 25388626.342851546 3992090.3444911274 +4214 2401 76.356 40.408 25615105.62648906 4475794.910050794 +4214 2402 78.0 36.43 26500000.0 4033328.221021122 +2402 4326 26500000.0 4033328.221021122 77.99946965563925 36.43021717384928 +4214 2402 76.65 31.57 26371834.960865583 3495025.3664513687 +2402 4326 26371834.960865583 3495025.3664513687 76.64946251378134 31.57008143392789 +4214 2402 79.35 41.29 26613086.468665898 4573739.763394554 +2402 4326 26613086.468665898 4573739.763394554 79.34947397115253 41.29035156388806 +4214 2403 81.005 37.915 27500439.67607966 4198137.725422131 +2403 4326 27500439.67607966 4198137.725422131 81.00454799623964 37.91526924191749 +4214 2403 79.6505 30.7465 27370769.40176551 3403708.0713205994 +2403 4326 27370769.40176551 3403708.0713205994 79.65004800019238 30.746570725124716 +4214 2403 82.35950000000001 45.0835 27607038.239076577 4995211.360493926 +2403 4326 27607038.239076577 4995211.360493926 82.35904035477802 45.083963162562895 +4214 2404 84.0 37.745 28500000.0 4179268.5315508978 +2404 4326 28500000.0 4179268.5315508978 83.99963859075714 37.74527501994896 +4214 2404 82.65 29.2085 28368713.70522621 3233191.261619523 +2404 4326 28368713.70522621 3233191.261619523 82.64963572642732 29.20853792991507 +4214 2404 85.35 46.281499999999994 28604045.183503162 5128352.139812531 +2404 4326 28604045.183503162 5128352.139812531 85.34963324656667 46.28200489441978 +4214 2405 87.0 38.49 29500000.0 4261964.001512302 +2405 4326 29500000.0 4261964.001512302 86.9997265241741 38.49030326698031 +4214 2405 85.65 28.869 29368283.19190522 3195556.076930756 +2405 4326 29368283.19190522 3195556.076930756 85.64971847663219 28.869035753495158 +4214 2405 88.35 48.111 29600528.605935283 5331745.416058843 +2405 4326 29600528.605935283 5331745.416058843 88.34972819993169 48.11156023955168 +4214 2406 90.0 37.870000000000005 30500000.0 4193142.877001827 +4214 2406 88.64099999999999 28.375 30366782.696371876 3140806.9446686734 +4214 2406 91.35900000000001 47.365 30602655.02838273 5248813.815491257 +4214 2407 93.0 36.42 31500000.0 4032218.532741143 +4214 2407 91.65 28.581 31367921.608043224 3163631.3160088337 +4214 2407 94.35 44.259 31607808.299036328 4903574.07351398 +4214 2408 96.005 36.365 32500448.739398196 4026115.292095146 +4214 2408 94.6505 29.0435 32368552.602556057 3214899.447603491 +4214 2408 97.35950000000001 43.686499999999995 32609615.147345375 4839973.374704856 +4214 2409 99.0 32.095 33500000.0 3552449.837102678 +4214 2409 97.65 22.4965 33361075.072530005 2489450.410268307 +4214 2409 100.35 41.6935 33612386.752196155 4618556.173658257 +4214 2410 102.0 31.909999999999997 34500000.0 3531935.4103309913 +4214 2410 100.65 22.208 34360788.69219168 2457496.433621891 +4214 2410 103.35 41.611999999999995 34612528.535272025 4609503.796078316 +4214 2411 105.005 32.355000000000004 35500470.64415534 3581281.948695771 +4214 2411 103.6505 23.485500000000002 35362134.512879685 2598998.8609195156 +4214 2411 106.35950000000001 41.2245 35613996.13178544 4566477.428991941 +4214 2412 108.005 30.33 36500480.83533077 3356755.284480762 +2412 4326 36500480.83533077 3356755.284480762 108.00537988230064 30.33008526779518 +4214 2412 106.6505 19.404 36358242.19157751 2146981.6974238595 +2412 4326 36358242.19157751 2146981.6974238595 106.65081373690506 19.403788397169066 +4214 2412 109.35950000000001 41.256 36613941.398199424 4569975.964756812 +2412 4326 36613941.398199424 4569975.964756812 109.3599783425978 41.25637287181591 +4214 2413 111.0 31.61 37500000.0 3498670.0424377522 +2413 4326 37500000.0 3498670.0424377522 111.00046215981715 31.61013728885437 +4214 2413 109.65 19.46 37358238.27511643 2153182.615674606 +2413 4326 37358238.27511643 2153182.615674606 109.65040662276868 19.459818999724288 +4214 2413 112.35 43.76 37608716.14351061 4848127.460617051 +2413 4326 37608716.14351061 4848127.460617051 112.350546888293 43.76044326948034 +4214 2414 114.0 33.485 38500000.0 3706604.573484267 +2414 4326 38500000.0 3706604.573484267 114.00054898743991 33.48517927466875 +4214 2414 112.65 22.7165 38361295.817380816 2513818.1192309842 +2414 4326 38361295.817380816 2513818.1192309842 112.6504849564967 22.716400907177125 +4214 2414 115.35 44.2535 38607818.35029731 4902962.91347027 +2414 4326 38607818.35029731 4902962.91347027 115.35064439975594 44.253945639699715 +4214 2415 117.0 36.24 39500000.0 4012244.4619669584 +2415 4326 39500000.0 4012244.4619669584 117.00064366966107 36.24024052491322 +4214 2415 115.65 23.964000000000002 39362586.02371076 2652006.2114303806 +2415 4326 39362586.02371076 2652006.2114303806 115.6505617236915 23.963927865559803 +4214 2415 118.35 48.516000000000005 39599736.046650216 5376779.306687062 +2415 4326 39599736.046650216 5376779.306687062 118.35077957724553 48.51653462936076 +4214 2416 120.0 38.879999999999995 40500000.0 4305258.448688156 +2416 4326 40500000.0 4305258.448688156 120.00072697538491 38.88033493453784 +4214 2416 118.65 25.875 40364688.28216211 2863732.9318247084 +2416 4326 40364688.28216211 2863732.9318247084 118.6506346365904 25.875105914351117 +4214 2416 121.35 51.885 40592952.219102316 5751510.567213301 +2416 4326 40592952.219102316 5751510.567213301 121.35089353495286 51.88553860987831 +4214 2417 123.0 40.89 41500000.0 4528437.524576554 +2417 4326 41500000.0 4528437.524576554 123.00080714912804 40.890355017181 +4214 2417 121.65 29.487 41369070.30674335 3264065.6536962623 +2417 4326 41369070.30674335 3264065.6536962623 121.65070564923951 29.487162425076633 +4214 2417 124.35 52.293 41592108.18173952 5796906.031949949 +2417 4326 41592108.18173952 5796906.031949949 124.35097948609888 52.29352659106767 +4214 2418 126.0 46.695 42500000.0 5173431.954215716 +4214 2418 124.65 40.8405 42386140.68187937 4523817.676313308 +4214 2418 127.35 52.5495 42591575.13872549 5825446.618862379 +4214 2419 129.0 45.81 43500000.0 5075056.986440469 +4214 2419 127.65 41.814 43387823.29659013 4631940.5867785495 +4214 2419 130.35 49.806 43597178.31330539 5520240.61164158 +4214 2420 132.0 45.650000000000006 44500000.0 5057273.314402805 +4214 2420 130.65 42.743 44389459.34805132 4735137.246940653 +4214 2420 133.35 48.557 44599655.53163869 5381338.460902488 +4214 2421 134.135 47.125 45434364.2400247 5221598.582090681 +4214 2421 133.5635 45.9775 45388677.39893806 5094678.3808455635 +4214 2421 134.7065 48.2725 45478212.698992066 5348863.473519839 +4214 2422 75.06 38.230000000000004 505253.5341567345 4233104.345126611 +4214 2422 73.76400000000001 36.052 388626.3428515459 3992090.3444911274 +4214 2422 76.356 40.408 615105.6264890609 4475794.910050794 +4214 2423 78.0 36.43 500000.0 4033328.221021122 +2423 4326 500000.0 4033328.221021122 77.99946965563925 36.43021717384928 +4214 2423 76.65 31.57 371834.96086558374 3495025.3664513687 +2423 4326 371834.96086558374 3495025.3664513687 76.64946251378134 31.57008143392789 +4214 2423 79.35 41.29 613086.4686658963 4573739.763394554 +2423 4326 613086.4686658963 4573739.763394554 79.34947397115252 41.29035156388805 +4214 2424 81.005 37.915 500439.676079662 4198137.725422131 +2424 4326 500439.676079662 4198137.725422131 81.00454799623965 37.91526924191749 +4214 2424 79.6505 30.7465 370769.4017655096 3403708.0713205994 +2424 4326 370769.4017655096 3403708.0713205994 79.65004800019238 30.746570725124716 +4214 2424 82.35950000000001 45.0835 607038.2390765786 4995211.360493926 +2424 4326 607038.2390765786 4995211.360493926 82.35904035477803 45.083963162562895 +4214 2425 84.0 37.745 500000.0 4179268.5315508978 +2425 4326 500000.0 4179268.5315508978 83.99963859075714 37.74527501994896 +4214 2425 82.65 29.2085 368713.70522620826 3233191.261619523 +2425 4326 368713.70522620826 3233191.261619523 82.6496357264273 29.20853792991507 +4214 2425 85.35 46.281499999999994 604045.1835031626 5128352.139812531 +2425 4326 604045.1835031626 5128352.139812531 85.34963324656667 46.28200489441978 +4214 2426 87.0 38.49 500000.0 4261964.001512302 +2426 4326 500000.0 4261964.001512302 86.9997265241741 38.49030326698031 +4214 2426 85.65 28.869 368283.1919052191 3195556.076930756 +2426 4326 368283.1919052191 3195556.076930756 85.64971847663219 28.869035753495158 +4214 2426 88.35 48.111 600528.6059352813 5331745.416058843 +2426 4326 600528.6059352813 5331745.416058843 88.34972819993168 48.11156023955168 +4214 2427 90.0 37.870000000000005 500000.0 4193142.877001827 +4214 2427 88.64099999999999 28.375 366782.69637187547 3140806.9446686734 +4214 2427 91.35900000000001 47.365 602655.0283827311 5248813.815491257 +4214 2428 93.0 36.42 500000.0 4032218.532741143 +4214 2428 91.65 28.581 367921.6080432253 3163631.3160088337 +4214 2428 94.35 44.259 607808.2990363287 4903574.07351398 +4214 2429 96.005 36.365 500448.73939819523 4026115.292095146 +4214 2429 94.6505 29.0435 368552.6025560578 3214899.447603491 +4214 2429 97.35950000000001 43.686499999999995 609615.147345376 4839973.374704856 +4214 2430 99.0 32.095 500000.0 3552449.837102678 +4214 2430 97.65 22.4965 361075.0725300068 2489450.410268307 +4214 2430 100.35 41.6935 612386.7521961518 4618556.173658257 +4214 2431 102.0 31.909999999999997 500000.0 3531935.4103309913 +4214 2431 100.65 22.208 360788.69219168415 2457496.433621891 +4214 2431 103.35 41.611999999999995 612528.5352720255 4609503.796078316 +4214 2432 105.005 32.355000000000004 500470.64415533753 3581281.948695771 +4214 2432 103.6505 23.485500000000002 362134.51287968305 2598998.8609195156 +4214 2432 106.35950000000001 41.2245 613996.1317854408 4566477.428991941 +4214 2433 108.005 30.33 500480.83533077006 3356755.284480762 +2433 4326 500480.83533077006 3356755.284480762 108.00537988230066 30.33008526779518 +4214 2433 106.6505 19.404 358242.1915775105 2146981.6974238595 +2433 4326 358242.1915775105 2146981.6974238595 106.65081373690508 19.403788397169066 +4214 2433 109.35950000000001 41.256 613941.3981994218 4569975.964756812 +2433 4326 613941.3981994218 4569975.964756812 109.35997834259778 41.25637287181591 +4214 2434 111.0 31.61 500000.0 3498670.0424377522 +2434 4326 500000.0 3498670.0424377522 111.00046215981715 31.61013728885437 +4214 2434 109.65 19.46 358238.275116427 2153182.615674606 +2434 4326 358238.275116427 2153182.615674606 109.65040662276868 19.459818999724288 +4214 2434 112.35 43.76 608716.1435106108 4848127.460617051 +2434 4326 608716.1435106108 4848127.460617051 112.35054688829301 43.76044326948035 +4214 2435 114.0 33.485 500000.0 3706604.573484267 +2435 4326 500000.0 3706604.573484267 114.00054898743991 33.48517927466875 +4214 2435 112.65 22.7165 361295.81738081516 2513818.1192309842 +2435 4326 361295.81738081516 2513818.1192309842 112.6504849564967 22.716400907177125 +4214 2435 115.35 44.2535 607818.350297313 4902962.91347027 +2435 4326 607818.350297313 4902962.91347027 115.35064439975598 44.253945639699715 +4214 2436 117.0 36.24 500000.0 4012244.4619669584 +2436 4326 500000.0 4012244.4619669584 117.00064366966107 36.24024052491322 +4214 2436 115.65 23.964000000000002 362586.0237107602 2652006.2114303806 +2436 4326 362586.0237107602 2652006.2114303806 115.65056172369151 23.963927865559803 +4214 2436 118.35 48.516000000000005 599736.0466502166 5376779.306687062 +2436 4326 599736.0466502166 5376779.306687062 118.35077957724553 48.51653462936076 +4214 2437 120.0 38.879999999999995 500000.0 4305258.448688156 +2437 4326 500000.0 4305258.448688156 120.00072697538491 38.88033493453784 +4214 2437 118.65 25.875 364688.28216211067 2863732.9318247084 +2437 4326 364688.28216211067 2863732.9318247084 118.65063463659042 25.875105914351117 +4214 2437 121.35 51.885 592952.2191023126 5751510.567213301 +2437 4326 592952.2191023126 5751510.567213301 121.35089353495282 51.88553860987831 +4214 2438 123.0 40.89 500000.0 4528437.524576554 +2438 4326 500000.0 4528437.524576554 123.00080714912804 40.890355017181 +4214 2438 121.65 29.487 369070.3067433529 3264065.6536962623 +2438 4326 369070.3067433529 3264065.6536962623 121.65070564923951 29.487162425076633 +4214 2438 124.35 52.293 592108.1817395184 5796906.031949949 +2438 4326 592108.1817395184 5796906.031949949 124.35097948609891 52.29352659106767 +4214 2439 126.0 46.695 500000.0 5173431.954215716 +4214 2439 124.65 40.8405 386140.681879375 4523817.676313308 +4214 2439 127.35 52.5495 591575.1387254914 5825446.618862379 +4214 2440 129.0 45.81 500000.0 5075056.986440469 +4214 2440 127.65 41.814 387823.2965901297 4631940.5867785495 +4214 2440 130.35 49.806 597178.3133053896 5520240.61164158 +4214 2441 132.0 45.650000000000006 500000.0 5057273.314402805 +4214 2441 130.65 42.743 389459.3480513134 4735137.246940653 +4214 2441 133.35 48.557 599655.5316386878 5381338.460902488 +4214 2442 134.135 47.125 434364.2400247033 5221598.582090681 +4214 2442 133.5635 45.9775 388677.3989380567 5094678.3808455635 +4214 2442 134.7065 48.2725 478212.6989920673 5348863.473519839 +4612 2443 129.315 30.85 -17694.12918193351 -238365.4573224007 +2443 4326 -17694.12918193351 -238365.4573224007 129.315 30.85 +4612 2443 128.28449999999998 27.349 -120262.86799967856 -625797.608309971 +2443 4326 -120262.86799967856 -625797.608309971 128.28449999999998 27.348999999999997 +4612 2443 130.34550000000002 34.351 77781.84923412865 150157.13190014462 +2443 4326 77781.84923412865 150157.13190014462 130.34550000000002 34.351 +4612 2444 130.905 32.085 -8967.662029594969 -101456.07083030338 +2444 4326 -8967.662029594969 -101456.07083030338 130.905 32.085 +4612 2444 129.87449999999998 30.3705 -108182.2569405884 -290996.2422846366 +2444 4326 -108182.2569405884 -290996.2422846366 129.87449999999998 30.370500000000007 +4612 2444 131.93550000000002 33.7995 86621.30933465871 89058.36020065419 +2444 4326 86621.30933465871 89058.36020065419 131.93550000000002 33.7995 +4612 2445 132.15 35.05 -1520.3913637287199 -105392.0498223429 +2445 4326 -1520.3913637287199 -105392.0498223429 132.15 35.04999999999999 +4612 2445 130.94400000000002 33.853 -113141.96909569166 -237490.2563337995 +2445 4326 -113141.96909569166 -237490.2563337995 130.94400000000002 33.853 +4612 2445 133.356 36.247 106890.92757591169 28060.710695339 +2445 4326 106890.92757591169 28060.710695339 133.356 36.247 +4612 2446 133.38 33.57 -11140.611022283041 63218.568582534346 +2446 4326 -11140.611022283041 63218.568582534346 133.38 33.57 +4612 2446 132.093 32.778 -131809.32105295264 -23741.600554621888 +2446 4326 -131809.32105295264 -23741.600554621888 132.093 32.778 +4612 2446 134.667 34.362 107345.53531015589 151670.45950121942 +2446 4326 107345.53531015589 151670.45950121942 134.667 34.362 +4612 2447 134.3 34.92 -3045.593094469217 -119812.46646359657 +2447 4326 -3045.593094469217 -119812.46646359657 134.3 34.92 +4612 2447 133.24699999999999 34.209 -100106.42589702891 -198144.5986105343 +2447 4326 -100106.42589702891 -198144.5986105343 133.24699999999999 34.208999999999996 +4612 2447 135.353 35.631 92355.63179588626 -40459.73391294459 +2447 4326 92355.63179588626 -40459.73391294459 135.353 35.631 +4612 2448 135.925 34.864999999999995 -6857.153430803463 -125911.42130509262 +2448 4326 -6857.153430803463 -125911.42130509262 135.925 34.864999999999995 +4612 2448 134.96650000000002 33.5465 -95976.51455007782 -271677.0789730092 +2448 4326 -95976.51455007782 -271677.0789730092 134.96650000000002 33.54650000000001 +4612 2448 136.8835 36.183499999999995 79467.67980275753 20720.988311085046 +2448 4326 79467.67980275753 20720.988311085046 136.8835 36.183499999999995 +4612 2449 137.03 36.045 -12314.13837266254 5001.316246591893 +2449 4326 -12314.13837266254 5001.316246591893 137.03 36.045 +4612 2449 136.301 34.6635 -79340.41347689279 -147924.500285497 +2449 4326 -79340.41347689279 -147924.500285497 136.301 34.6635 +4612 2449 137.75900000000001 37.4265 52423.637802688085 158450.8366085486 +2449 4326 52423.637802688085 158450.8366085486 137.75900000000001 37.4265 +4612 2450 138.615 36.56 10293.994412923666 62139.89291487604 +2450 4326 10293.994412923666 62139.89291487604 138.615 36.56 +4612 2450 137.4495 34.742 -96190.53805913808 -139055.30904895783 +2450 4326 -96190.53805913808 -139055.30904895783 137.4495 34.742000000000004 +4612 2450 139.7805 38.378 111880.68448409403 264663.23889834405 +2450 4326 111880.68448409403 264663.23889834405 139.7805 38.37800000000001 +4612 2451 139.755 33.644999999999996 -7266.047747636758 -261228.66136330654 +2451 4326 -7266.047747636758 -261228.66136330654 139.755 33.644999999999996 +4612 2451 138.5355 29.743499999999997 -125536.82383970382 -693087.5289930385 +2451 4326 -125536.82383970382 -693087.5289930385 138.5355 29.743499999999997 +4612 2451 140.9745 37.546499999999995 100837.20925875574 172215.23425634272 +2451 4326 100837.20925875574 172215.23425634272 140.9745 37.546499999999995 +4612 2452 140.815 39.655 -1573.2470265054255 -38301.81422441857 +2452 4326 -1573.2470265054255 -38301.81422441857 140.815 39.65499999999999 +4612 2452 139.6225 37.9225 -106453.60744848949 -229918.7365203989 +2452 4326 -106453.60744848949 -229918.7365203989 139.6225 37.92250000000001 +4612 2452 142.0075 41.387499999999996 98198.95234487754 154728.9910797522 +2452 4326 98198.95234487754 154728.9910797522 142.0075 41.38749999999999 +4612 2453 140.4 42.38 12352.167120475582 -179947.3434793202 +2453 4326 12352.167120475582 -179947.3434793202 140.4 42.38 +4612 2453 139.446 41.444 -67182.27670751311 -283598.84054292215 +2453 4326 -67182.27670751311 -283598.84054292215 139.44600000000003 41.443999999999996 +4612 2453 141.354 43.316 89549.91330541894 -75396.70991294812 +2453 4326 89549.91330541894 -75396.70991294812 141.354 43.316 +4612 2454 142.25 43.845 0.0 -17220.44078117744 +2454 4326 0.0 -17220.44078117744 142.25 43.845 +4612 2454 141.02599999999998 42.3195 -100891.1088259258 -185952.30694077082 +2454 4326 -100891.1088259258 -185952.30694077082 141.02599999999998 42.3195 +4612 2454 143.47400000000002 45.3705 95874.8566985425 153011.29943193644 +2454 4326 95874.8566985425 153011.29943193644 143.47400000000002 45.3705 +4612 2455 144.24 43.135 -813.5408685454717 -96095.12970504239 +2455 4326 -813.5408685454717 -96095.12970504239 144.24 43.135000000000005 +4612 2455 142.77300000000002 41.9965 -122366.48753635101 -221496.57531629794 +2455 4326 -122366.48753635101 -221496.57531629794 142.77300000000002 41.996500000000005 +4612 2455 145.707 44.2735 116310.97386241013 31419.373029238384 +2455 4326 116310.97386241013 31419.373029238384 145.707 44.2735 +4612 2456 141.765 26.235 -23478.34683664922 26054.282932958602 +2456 4326 -23478.34683664922 26054.282932958602 141.765 26.234999999999996 +4612 2456 141.2565 24.826500000000003 -75155.30730020948 -129781.52685226209 +2456 4326 -75155.30730020948 -129781.52685226209 141.2565 24.826500000000003 +4612 2456 142.2735 27.6435 26987.345735629453 182112.73747284504 +2456 4326 26987.345735629453 182112.73747284504 142.27350000000004 27.643500000000003 +4612 2457 127.515 26.465 1495.6545360844798 51513.01529828106 +2457 4326 1495.6545360844798 51513.01529828106 127.515 26.464999999999996 +4612 2457 126.71849999999999 26.0645 -78192.89680749233 7379.460140749547 +2457 4326 -78192.89680749233 7379.460140749547 126.7185 26.0645 +4612 2457 128.3115 26.8655 80634.4910127214 96141.25857342333 +2457 4326 80634.4910127214 96141.25857342333 128.3115 26.865499999999997 +4612 2458 124.17 24.46 17234.053724502493 -170567.9567367425 +2458 4326 17234.053724502493 -170567.9567367425 124.17000000000002 24.459999999999997 +4612 2458 122.964 24.028 -105385.34443706245 -218034.8587424919 +2458 4326 -105385.34443706245 -218034.8587424919 122.964 24.028000000000002 +4612 2458 125.376 24.892000000000003 139023.28742866468 -122028.76923696398 +2458 4326 139023.28742866468 -122028.76923696398 125.37599999999999 24.892000000000003 +4612 2459 131.25 25.205 25193.0183649557 -88039.43474560729 +2459 4326 25193.0183649557 -88039.43474560729 131.25 25.205000000000002 +4612 2459 131.133 24.4805 13480.924431529565 -168301.602720175 +2459 4326 13480.924431529565 -168301.602720175 131.133 24.4805 +4612 2459 131.367 25.9295 36761.584812905174 -7758.252281296523 +2459 4326 36761.584812905174 -7758.252281296523 131.367 25.929499999999997 +4612 2460 136.09 20.425 9391.766519188244 47048.325427261516 +2460 4326 9391.766519188244 47048.325427261516 136.09 20.425 +4612 2460 136.02700000000002 20.375500000000002 2818.429240383388 41566.41886628239 +2460 4326 2818.429240383388 41566.41886628239 136.02700000000002 20.3755 +4612 2460 136.153 20.4745 15960.900135895556 52532.800221791236 +2460 4326 15960.900135895556 52532.800221791236 136.153 20.474500000000003 +4612 2461 153.98000000000002 24.285 -2030.3256859181972 -189960.09433244 +2461 4326 -2030.3256859181972 -189960.09433244 153.98000000000002 24.284999999999997 +4612 2461 153.917 24.226499999999998 -8429.70912083693 -196436.65941094185 +2461 4326 -8429.70912083693 -196436.65941094185 153.917 24.226499999999994 +4612 2461 154.043 24.343500000000002 4363.198381945305 -183480.59051934036 +2461 4326 4363.198381945305 -183480.59051934036 154.043 24.3435 +4191 2462 20.14 41.155 4427811.936531614 4558223.983177318 +2462 4326 4427811.936531614 4558223.983177318 20.13843188868264 41.154533551947 +4191 2462 19.311999999999998 39.7915 4355411.469184696 4407819.989936727 +2462 4326 4355411.469184696 4407819.989936727 19.310463135034805 39.7910095502003 +4191 2462 20.968 42.5185 4497370.363206361 4709314.653392539 +2462 4326 4497370.363206361 4709314.653392539 20.966399206947735 42.518057470973105 +4200 2463 21.22 54.82 514141.887962946 6077321.106840183 +2463 4326 514141.887962946 6077321.106840183 21.218000168948336 54.819781174608 +4200 2463 19.735 54.37 417784.76521286654 6027943.58067649 +2463 4326 417784.76521286654 6027943.58067649 19.73301337496357 54.36975076297361 +4200 2463 22.705000000000002 55.27 608372.2804645535 6128721.082449393 +2463 4326 608372.2804645535 6128721.082449393 22.702987920541858 55.26981174545414 +4200 2464 28.305 62.58 567074.0397727002 6942371.260837219 +2464 4326 567074.0397727002 6942371.260837219 28.302568326748556 62.580005607855405 +4200 2464 26.7795 56.379 486377.11921526626 6250894.172139804 +2464 4326 486377.11921526626 6250894.172139804 26.777466276548896 56.37889454418472 +4200 2464 29.8305 68.781 614342.3530131603 7635762.354308665 +2464 4326 614342.3530131603 7635762.354308665 29.827424851300435 68.78111849054713 +4200 2465 33.0 60.18 500000.0 6674243.916115953 +2465 4326 500000.0 6674243.916115953 32.99781794242752 60.18005228897135 +4200 2465 30.3 51.324000000000005 311801.4211408057 5691694.092067871 +2465 4326 311801.4211408057 5691694.092067871 30.29823781553074 51.32388078802876 +4200 2465 35.7 69.036 607823.0147247844 7663946.908246142 +2465 4326 607823.0147247844 7663946.908246142 35.69701759871923 69.03622769281283 +4200 2466 39.004999999999995 56.205 500310.3156621364 6231497.6277802335 +2466 4326 500310.3156621364 6231497.6277802335 39.00314905443574 56.205088009936674 +4200 2466 36.3005 44.4825 285240.6060750514 4931069.50217726 +2466 4326 285240.6060750514 4931069.50217726 36.2990280222276 44.48236497104047 +4200 2466 41.7095 67.92750000000001 613641.3720829593 7540418.099849011 +2466 4326 613641.3720829593 7540418.099849011 41.70681884720197 67.92781396009973 +4200 2467 45.0 61.05 500000.0 6771183.262177025 +2467 4326 500000.0 6771183.262177025 44.99800256726887 61.050257257340455 +4200 2467 42.3 43.175999999999995 280459.84491934377 4785897.735686462 +2467 4326 280459.84491934377 4785897.735686462 42.29864290339989 43.17592002865416 +4200 2467 47.7 78.92399999999999 557909.4989024077 8766488.51784769 +2467 4326 557909.4989024077 8766488.51784769 47.695086461837676 78.92457497241807 +4200 2468 51.0 61.395 500000.0 6809628.224716597 +2468 4326 500000.0 6809628.224716597 50.99813864717582 61.39535013138184 +4200 2468 48.3 43.3905 281230.4116231663 4809730.342741161 +2468 4326 281230.4116231663 4809730.342741161 48.29873595311192 43.39049449035379 +4200 2468 53.7 79.3995 555452.9322953868 8819526.123378675 +2468 4326 555452.9322953868 8819526.123378675 53.6953042471077 79.40017550092794 +4200 2469 57.0 66.19 500000.0 7344153.685754918 +2469 4326 500000.0 7344153.685754918 56.99799886410718 66.19051399956604 +4200 2469 54.3 52.042 314756.50490782555 5771560.60961814 +2469 4326 314756.50490782555 5771560.60961814 54.2986333012739 52.042224797735486 +4200 2469 59.7 80.338 550593.1644214592 8924210.708048513 +2469 4326 550593.1644214592 8924210.708048513 59.69538558172602 80.33877415122741 +4200 2470 63.0 66.215 500000.0 7346941.491984668 +2470 4326 500000.0 7346941.491984668 62.99823193980998 66.21558884736703 +4200 2470 60.3 52.2155 315474.9969159855 5790860.97655006 +2470 4326 315474.9969159855 5790860.97655006 60.29877627798769 52.21579592609235 +4200 2470 65.7 80.21449999999999 551233.4915869082 8910434.688238192 +2470 4326 551233.4915869082 8910434.688238192 65.69602321443385 80.2153479264253 +4200 2471 69.0 65.585 500000.0 7276691.558163891 +2471 4326 500000.0 7276691.558163891 68.99852438075514 65.58564228129114 +4200 2471 66.3 55.2485 328304.7407371914 6128327.034424901 +2471 4326 328304.7407371914 6128327.034424901 66.29885654323189 55.24891716280268 +4200 2471 71.7 75.9215 573320.7845586325 8431619.925727122 +2471 4326 573320.7845586325 8431619.925727122 71.69766775623087 75.92234680701002 +4200 2472 75.0 66.44 500000.0 7372032.155122787 +2472 4326 500000.0 7372032.155122787 74.99874715306994 66.44071269326217 +4200 2472 72.3 54.497 325079.2244686774 6044699.115933595 +2472 4326 325079.2244686774 6044699.115933595 72.29906211412263 54.497455052006195 +4200 2472 77.7 78.383 560699.5242586047 8706146.590604123 +2472 4326 560699.5242586047 8706146.590604123 77.69773290397788 78.38393872850747 +4200 2473 81.0 65.86 500000.0 7307355.496511509 +2473 4326 500000.0 7307355.496511509 80.99905818444867 65.86074571539577 +4200 2473 78.3 52.207 315439.75724112405 5789915.415068631 +2473 4326 315439.75724112405 5789915.415068631 78.29929730905346 52.20744942706851 +4200 2473 83.7 79.513 554865.9798419666 8832186.181052761 +2473 4326 554865.9798419666 8832186.181052761 83.69813282647974 79.51399782213117 +4200 2474 87.0 65.17 500000.0 7230419.008135735 +2474 4326 500000.0 7230419.008135735 86.99936864884313 65.17076492832118 +4200 2474 84.3 50.68 309176.2173298135 5620065.466081332 +2474 4326 309176.2173298135 5620065.466081332 84.29950767652322 50.680448173439785 +4200 2474 89.7 79.66 554105.4614323742 8848583.012840003 +2474 4326 554105.4614323742 8848583.012840003 89.69878110829825 79.661029549359 +4200 2475 93.0 65.62 500000.0 7280594.179302066 +2475 4326 500000.0 7280594.179302066 92.99965444920409 65.62079457620393 +4200 2475 90.3 51.463 312371.19563156937 5707155.118561674 +2475 4326 312371.19563156937 5707155.118561674 90.29969386999117 51.463488159384646 +4200 2475 95.7 79.77699999999999 553499.892234066 8861633.636235029 +2475 4326 553499.892234066 8861633.636235029 95.69946349009274 79.77804820662341 +4200 2476 99.0 65.52499999999999 500000.0 7270001.392661486 +2476 4326 500000.0 7270001.392661486 98.99995625692779 65.52580182672459 +4200 2476 96.3 51.3095 311742.0484541096 5690081.267726861 +2476 4326 311742.0484541096 5690081.267726861 96.29989328538964 51.30999710444767 +4200 2476 101.7 79.7405 553688.8335448579 8857562.279994301 +2476 4326 553688.8335448579 8857562.279994301 101.70016589588217 79.74155169540556 +4200 2477 105.0 64.56 500000.0 7162408.624891741 +2477 4326 500000.0 7162408.624891741 105.00024968799015 64.5607800226099 +4200 2477 102.3 51.132 311016.2305254827 5670338.326859696 +2477 4326 311016.2305254827 5670338.326859696 102.30009290380566 51.13249525999032 +4200 2477 107.7 77.988 562733.1490418914 8662090.29101531 +2477 4326 562733.1490418914 8662090.29101531 107.70074537583363 77.98901547520622 +4200 2478 111.0 62.975 500000.0 6985719.868823676 +2478 4326 500000.0 6985719.868823676 111.00051057221933 62.9757339240016 +4200 2478 108.3 50.5235 308541.9152511464 5602659.755338392 +2478 4326 308541.9152511464 5602659.755338392 108.30028805491435 50.52397340831288 +4200 2478 113.7 75.4265 575843.0570478045 8376418.5363391265 +2478 4326 575843.0570478045 8376418.5363391265 113.70111113480463 75.42745245378673 +4200 2479 117.0 62.735 500000.0 6958969.176764962 +2479 4326 500000.0 6958969.176764962 117.00077187896638 62.73570331241798 +4200 2479 114.3 50.832499999999996 309795.68677515734 5637026.654582188 +2479 4326 309795.68677515734 5637026.654582188 114.30048411105328 50.8329623906828 +4200 2479 119.7 74.63749999999999 579851.4573872517 8288434.675508902 +2479 4326 579851.4573872517 8288434.675508902 119.70150982860773 74.6384058304563 +4200 2480 123.0 62.754999999999995 500000.0 6961198.365997623 +2480 4326 500000.0 6961198.365997623 123.00102965078561 62.75566676042915 +4200 2480 120.3 52.634499999999996 317217.16375144455 5837472.916796084 +2480 4326 317217.16375144455 5837472.916796084 120.30069946803451 52.6349740116131 +4200 2480 125.7 72.8755 588746.4610820294 8091966.483516503 +2480 4326 588746.4610820294 8091966.483516503 125.70175488378631 72.87633103885827 +4200 2481 129.0 57.93 500000.0 6423598.670127823 +2481 4326 500000.0 6423598.670127823 129.0011054464138 57.93052420710647 +4200 2481 126.3 43.818 282775.33302377217 4857230.979267927 +2481 4326 282775.33302377217 4857230.979267927 126.30075416549118 43.818242322185654 +4200 2481 131.7 72.042 592925.0420301072 7999037.963814366 +2481 4326 592925.0420301072 7999037.963814366 131.70203471177607 72.04276082321903 +4200 2482 135.0 59.39 500000.0 6586229.628407109 +2482 4326 500000.0 6586229.628407109 135.0013604047437 59.39049726608047 +4200 2482 132.3 44.306000000000004 284553.7391129919 4911457.322981693 +2482 4326 284553.7391129919 4911457.322981693 132.30091328885933 44.306211182169626 +4200 2482 137.7 74.474 580680.1997764028 8270202.881338667 +2482 4326 580680.1997764028 8270202.881338667 137.7027240999751 74.47473362945344 +4200 2483 141.0 61.055 500000.0 6771740.421450425 +2483 4326 500000.0 6771740.421450425 141.00163426725572 61.055462614188215 +4200 2483 138.3 47.36150000000001 296040.70601865585 5251065.475993207 +2483 4326 296040.70601865585 5251065.475993207 138.3011143264255 47.36172439854223 +4200 2483 143.7 74.74849999999999 579288.4486590752 8300812.343378751 +2483 4326 579288.4486590752 8300812.343378751 143.7031341640171 74.74916037218297 +4200 2484 147.0 60.209999999999994 500000.0 6677586.4401542945 +2484 4326 500000.0 6677586.4401542945 147.0017755976505 60.21037244216521 +4200 2484 144.3 45.261 288079.34678485343 5017586.979982033 +2484 4326 288079.34678485343 5017586.979982033 144.30120845361577 45.26112299736569 +4200 2484 149.7 75.15899999999999 577203.7252867639 8346588.240981594 +2484 4326 577203.7252867639 8346588.240981594 149.70355563309317 75.15957972764325 +4200 2485 153.0 61.015 500000.0 6767283.158904388 +2485 4326 500000.0 6767283.158904388 153.0019871458978 61.0153035923279 +4200 2485 150.3 47.294500000000006 295782.4466302481 5243617.138046312 +2485 4326 295782.4466302481 5243617.138046312 150.30137899356055 47.29459587357376 +4200 2485 155.7 74.7355 579354.4022646449 8299362.701628698 +2485 4326 579354.4022646449 8299362.701628698 155.70375139104775 74.73597826818597 +4200 2486 159.0 63.735 500000.0 7070436.407700245 +2486 4326 500000.0 7070436.407700245 159.00233247571654 63.73525666005456 +4200 2486 156.3 51.6165 313001.6996699431 5724229.319586072 +2486 4326 313001.6996699431 5724229.319586072 156.30162448026763 51.61659706725653 +4200 2486 161.7 75.8535 573667.611802031 8424036.598154908 +2486 4326 573667.611802031 8424036.598154908 161.70430793661777 75.85389175601911 +4200 2487 165.0 62.25 500000.0 6904913.303640777 +2487 4326 500000.0 6904913.303640777 165.00234707610946 62.25013600624102 +4200 2487 162.3 55.248 328302.5847169872 6128271.391067221 +2487 4326 328302.5847169872 6128271.391067221 162.3018797669474 55.24807094372626 +4200 2487 167.7 69.252 606762.0374198339 7688022.353117352 +2487 4326 606762.0374198339 7688022.353117352 167.70313770086122 69.25219126753747 +4200 2488 171.0 62.32 500000.0 6912714.948260343 +2488 4326 500000.0 6912714.948260343 171.00245569934953 62.3200348040961 +4200 2488 168.3 55.237 328255.1556102404 6127047.238102845 +2488 4326 328255.1556102404 6127047.238102845 168.30197284507864 55.23697777392646 +4200 2488 173.7 69.40299999999999 606019.4218558781 7704853.15403719 +2488 4326 606019.4218558781 7704853.15403719 173.70328012982165 69.40308342083833 +4200 2489 177.0 66.62 500000.0 7392105.210780848 +2489 4326 500000.0 7392105.210780848 177.00295778639511 66.61998500680816 +4200 2489 174.3 62.147 359233.8772994867 6896367.144476949 +2489 4326 359233.8772994867 6896367.144476949 174.3024871707695 62.146974491563526 +4200 2489 179.7 71.093 597658.3381320384 7893239.748026698 +2489 4326 597658.3381320384 7893239.748026698 179.70365027657212 71.09299131899247 +4200 2490 -177.0 68.0 500000.0 7546013.844298534 +2490 4326 500000.0 7546013.844298534 -176.99680785631665 67.99988914928979 +4200 2490 -179.7 64.71499999999999 371298.71546732273 7182432.038391958 +2490 4326 371298.71546732273 7182432.038391958 -179.6972159432352 64.71489921957169 +4200 2490 -174.3 71.28500000000001 596702.8548685303 7914643.984382171 +2490 4326 596702.8548685303 7914643.984382171 -174.2962606437836 71.2848765599116 +4200 2491 -171.485 65.69 477711.5325981353 7288485.446295194 +2491 4326 477711.5325981353 7288485.446295194 -171.48206827516273 65.68975651608112 +4200 2491 -173.7485 64.349 367220.71177890955 7141756.667395102 +2491 4326 367220.71177890955 7141756.667395102 -173.7457160996516 64.34878372334664 +4200 2491 -169.2215 67.031 577471.7906795319 7439047.596853634 +2491 4326 577471.7906795319 7439047.596853634 -169.2184073796692 67.03072845013044 +4284 2494 21.785 53.695 551852.9086016514 5952359.55362 +2494 4326 551852.9086016514 5952359.55362 21.783033061265474 53.69472630246123 +4284 2494 19.7915 48.5245 410732.50789391523 5377549.622905095 +2494 4326 410732.50789391523 5377549.622905095 19.78975676645579 48.524128373175294 +4284 2494 23.7785 58.8655 660290.8932240324 6531128.48004474 +2494 4326 660290.8932240324 6531128.48004474 23.77622904611787 58.86532950007166 +4284 2495 27.005000000000003 57.325 500301.2121796233 6356218.055120438 +2495 4326 500301.2121796233 6356218.055120438 27.002888791531646 57.324873911333256 +4284 2495 24.3005 46.3945 292379.117343356 5143570.13347466 +2495 4326 292379.117343356 5143570.13347466 24.298881357401225 46.39417854657764 +4284 2495 29.709500000000002 68.2555 612036.33641593 7576973.310199547 +2495 4326 612036.33641593 7576973.310199547 29.706357096422174 68.2555788892998 +4284 2496 33.0 57.17 500000.0 6338956.286202614 +2496 4326 500000.0 6338956.286202614 32.99797166629755 57.169982463225324 +4284 2496 30.3 45.605 289363.8540554984 5055819.28226941 +2496 4326 289363.8540554984 5055819.28226941 30.298457314429925 45.604764475195296 +4284 2496 35.7 68.735 609298.9139779271 7630398.127579256 +2496 4326 609298.9139779271 7630398.127579256 35.69690809655146 68.73520565464733 +4284 2497 39.0 55.33 500000.0 6134075.5797153525 +2497 4326 500000.0 6134075.5797153525 38.99817197334615 55.33005962925359 +4284 2497 36.3 42.82 279187.7826901206 4746344.926457373 +2497 4326 279187.7826901206 4746344.926457373 36.29860678298642 42.819813334853336 +4284 2497 41.7 67.84 613669.1755335709 7530649.032519 +2497 4326 613669.1755335709 7530649.032519 41.69719020330312 67.84030732643805 +4284 2498 45.004999999999995 59.875 500280.05647272046 6640262.465565657 +2498 4326 500280.05647272046 6640262.465565657 45.00299118914558 59.87523582791658 +4284 2498 42.3005 40.9435 272667.3993890653 4537889.80114546 +2498 4326 272667.3993890653 4537889.80114546 42.29920979020801 40.94336903330199 +4284 2498 47.7095 78.8065 558721.6674180112 8753392.219536956 +2498 4326 558721.6674180112 8753392.219536956 47.70411379676524 78.80707632438926 +4284 2499 51.0 59.370000000000005 500000.0 6584001.55719021 +2499 4326 500000.0 6584001.55719021 50.99817532538931 59.37032616873129 +4284 2499 48.3 39.543000000000006 267886.60433289857 4382348.994620828 +2499 4326 267886.60433289857 4382348.994620828 48.29883660075837 39.54292222313768 +4284 2499 53.7 79.197 556499.5881056477 8796938.973915378 +2499 4326 556499.5881056477 8796938.973915378 53.69484598212388 79.19768943261295 +4284 2500 57.0 59.48 500000.0 6596256.033963211 +2500 4326 500000.0 6596256.033963211 56.998342046649 59.48041976678641 +4284 2500 54.3 39.293 267055.2679200559 4354586.619266492 +2500 4326 267055.2679200559 4354586.619266492 54.29895116914863 39.29298976696036 +4284 2500 59.7 79.667 554069.2372122618 8849363.817288546 +2500 4326 554069.2372122618 8849363.817288546 59.69511806925094 79.66779516238226 +4284 2501 63.0 58.455 500000.0 6482074.698219859 +2501 4326 500000.0 6482074.698219859 62.99858624107984 58.45548335902813 +4284 2501 60.3 37.4715 261133.40756946793 4152337.9224953735 +2501 4326 261133.40756946793 4152337.9224953735 60.2991049118664 37.47151735930999 +4284 2501 65.7 79.4385 555251.2729837452 8823876.26732618 +2501 4326 555251.2729837452 8823876.26732618 65.69578441053547 79.43938095641529 +4284 2502 69.0 56.87 500000.0 6305547.652461898 +2502 4326 500000.0 6305547.652461898 68.99885498993974 56.8705247830778 +4284 2502 66.3 38.69 265068.42044412857 4287627.595211889 +2502 4326 265068.42044412857 4287627.595211889 66.29921910245224 38.69010309902671 +4284 2502 71.7 75.04999999999998 577757.6779998946 8334433.247982746 +2502 4326 577757.6779998946 8334433.247982746 71.69750015646298 75.05089111826645 +4284 2503 75.0 58.25 500000.0 6459240.625442509 +2503 4326 500000.0 6459240.625442509 74.99902096181162 58.250616202614715 +4284 2503 72.3 38.936 265875.8357000015 4314943.5598596735 +2503 4326 265875.8357000015 4314943.5598596735 72.29936100137701 38.936161556029795 +4284 2503 77.7 77.564 564912.7080289265 8614800.564080589 +2503 4326 564912.7080289265 8614800.564080589 77.697511732975 77.56499664445953 +4284 2504 81.005 61.035 500270.24414710334 6769511.797167683 +2504 4326 500270.24414710334 6769511.797167683 81.0041673530589 61.03572708616913 +4284 2504 78.3005 43.0395 280011.8411263862 4770730.583375289 +2504 4326 280011.8411263862 4770730.583375289 78.29996484935782 43.03980519934033 +4284 2504 83.7095 79.03049999999999 557561.3173123519 8778376.844888298 +2504 4326 557561.3173123519 8778376.844888298 83.70731269658245 79.03157291614595 +4284 2505 87.0 64.045 500000.0 7104994.433405277 +2505 4326 500000.0 7104994.433405277 86.9993884949609 64.0458261475768 +4284 2505 84.3 48.5425 300638.7541203402 5382367.500632766 +2505 4326 300638.7541203402 5382367.500632766 84.29960344770687 48.54296890085974 +4284 2505 89.7 79.54749999999999 554687.5235626507 8836034.406479053 +2505 4326 554687.5235626507 8836034.406479053 89.69849610461027 79.54861881549702 +4284 2506 93.0 65.62 500000.0 7280594.179302066 +2506 4326 500000.0 7280594.179302066 92.99965591516786 65.62088783828742 +4284 2506 90.3 51.463 312371.19563156937 5707155.118561674 +2506 4326 312371.19563156937 5707155.118561674 90.29976966849114 51.463569218327855 +4284 2506 95.7 79.77699999999999 553499.892234066 8861633.636235029 +2506 4326 553499.892234066 8861633.636235029 95.6992095333462 79.77814803884354 +4284 2507 99.0 65.52499999999999 500000.0 7270001.392661486 +2507 4326 500000.0 7270001.392661486 98.9999832998074 65.5259034655547 +4284 2507 96.3 51.3095 311742.0484541096 5690081.267726861 +2507 4326 311742.0484541096 5690081.267726861 96.29998542819108 51.31008710735066 +4284 2507 101.7 79.7405 553688.8335448579 8857562.279994301 +2507 4326 553688.8335448579 8857562.279994301 101.69997361268366 79.74165930035332 +4284 2508 105.0 64.56 500000.0 7162408.624891741 +2508 4326 500000.0 7162408.624891741 105.00030935930965 64.56088868438572 +4284 2508 102.3 51.132 311016.2305254827 5670338.326859696 +2508 4326 311016.2305254827 5670338.326859696 102.30020235037838 51.13259331820935 +4284 2508 107.7 77.988 562733.1490418914 8662090.29101531 +2508 4326 562733.1490418914 8662090.29101531 107.70066481128028 77.98912969936808 +4284 2509 111.0 62.975 500000.0 6985719.868823676 +2509 4326 500000.0 6985719.868823676 111.00060481075904 62.97584828674728 +4284 2509 108.3 50.5235 308541.9152511464 5602659.755338392 +2509 4326 308541.9152511464 5602659.755338392 108.3004166669608 50.52407827122059 +4284 2509 113.7 75.4265 575843.0570478045 8376418.5363391265 +2509 4326 575843.0570478045 8376418.5363391265 113.70112759073662 75.42757200421018 +4284 2510 117.0 62.735 500000.0 6958969.176764962 +2510 4326 500000.0 6958969.176764962 117.00089326346644 62.7358227410701 +4284 2510 114.3 50.832499999999996 309795.68677515734 5637026.654582188 +2510 4326 309795.68677515734 5637026.654582188 114.30062981280754 50.83307365705783 +4284 2510 119.7 74.63749999999999 579851.4573872517 8288434.675508902 +2510 4326 579851.4573872517 8288434.675508902 119.70158276216472 74.63852954201049 +4284 2511 123.0 62.754999999999995 500000.0 6961198.365997623 +2511 4326 500000.0 6961198.365997623 123.00117770561728 62.75579012404397 +4284 2511 120.3 52.634499999999996 317217.16375144455 5837472.916796084 +2511 4326 317217.16375144455 5837472.916796084 120.30086006153296 52.63509136853386 +4284 2511 125.7 72.8755 588746.4610820294 8091966.483516503 +2511 4326 588746.4610820294 8091966.483516503 125.70188447975737 72.87645756539818 +4284 2512 129.0 57.93 500000.0 6423598.670127823 +2512 4326 500000.0 6423598.670127823 129.00128766961262 57.93064877696723 +4284 2512 126.3 43.818 282775.33302377217 4857230.979267927 +2512 4326 282775.33302377217 4857230.979267927 126.30094586619992 43.81835852779464 +4284 2512 131.7 72.042 592925.0420301072 7999037.963814366 +2512 4326 592925.0420301072 7999037.963814366 131.70220951019087 72.04288893926793 +4284 2513 135.0 59.39 500000.0 6586229.628407109 +2513 4326 500000.0 6586229.628407109 135.00156471315282 59.390623875020964 +4284 2513 132.3 44.306000000000004 284553.7391129919 4911457.322981693 +2513 4326 284553.7391129919 4911457.322981693 132.30112010466385 44.30633092477097 +4284 2513 137.7 74.474 580680.1997764028 8270202.881338667 +2513 4326 580680.1997764028 8270202.881338667 137.70294271391128 74.47486204542867 +4284 2514 141.0 61.055 500000.0 6771740.421450425 +2514 4326 500000.0 6771740.421450425 141.0018629529222 61.055589769842825 +4284 2514 138.3 47.36150000000001 296040.70601865585 5251065.475993207 +2514 4326 296040.70601865585 5251065.475993207 138.30133525498115 47.36184750636767 +4284 2514 143.7 74.74849999999999 579288.4486590752 8300812.343378751 +2514 4326 579288.4486590752 8300812.343378751 143.7034019974815 74.74928762926334 +4284 2515 147.0 60.209999999999994 500000.0 6677586.4401542945 +2515 4326 500000.0 6677586.4401542945 147.00202807483538 60.210498348293974 +4284 2515 144.3 45.261 288079.34678485343 5017586.979982033 +2515 4326 288079.34678485343 5017586.979982033 144.30144570262993 45.26124594077783 +4284 2515 149.7 75.15899999999999 577203.7252867639 8346588.240981594 +2515 4326 577203.7252867639 8346588.240981594 149.70387467792418 75.15970444088252 +4284 2516 153.0 61.015 500000.0 6767283.158904388 +2516 4326 500000.0 6767283.158904388 153.00226464438146 61.015427119610706 +4284 2516 150.3 47.294500000000006 295782.4466302481 5243617.138046312 +2516 4326 295782.4466302481 5243617.138046312 150.3016319190933 47.294718792411615 +4284 2516 155.7 74.7355 579354.4022646449 8299362.701628698 +2516 4326 579354.4022646449 8299362.701628698 155.70411361037145 74.73609920407031 +4284 2517 159.0 63.735 500000.0 7070436.407700245 +2517 4326 500000.0 7070436.407700245 159.00264236061773 63.73537628119616 +4284 2517 156.3 51.6165 313001.6996699431 5724229.319586072 +2517 4326 313001.6996699431 5724229.319586072 156.30189703452837 51.61671845658234 +4284 2517 161.7 75.8535 573667.611802031 8424036.598154908 +2517 4326 573667.611802031 8424036.598154908 161.70473132075196 75.85400738069063 +4284 2518 165.0 62.25 500000.0 6904913.303640777 +2518 4326 500000.0 6904913.303640777 165.00267438112695 62.25025068045316 +4284 2518 162.3 55.248 328302.5847169872 6128271.391067221 +2518 4326 328302.5847169872 6128271.391067221 162.30217578230696 55.248188600283825 +4284 2518 167.7 69.252 606762.0374198339 7688022.353117352 +2518 4326 606762.0374198339 7688022.353117352 167.70352010873455 69.25230195906407 +4284 2519 171.0 62.32 500000.0 6912714.948260343 +2519 4326 500000.0 6912714.948260343 171.00280460099492 62.320143235987665 +4284 2519 168.3 55.237 328255.1556102404 6127047.238102845 +2519 4326 328255.1556102404 6127047.238102845 168.30228568922055 55.237090314358284 +4284 2519 173.7 69.40299999999999 606019.4218558781 7704853.15403719 +2519 4326 606019.4218558781 7704853.15403719 173.70369238388395 69.40318686929055 +4284 2520 177.0 66.62 500000.0 7392105.210780848 +2520 4326 500000.0 7392105.210780848 177.0033572842467 66.62008484268067 +4284 2520 174.3 62.147 359233.8772994867 6896367.144476949 +2520 4326 359233.8772994867 6896367.144476949 174.3028460450905 62.14707906288871 +4284 2520 179.7 71.093 597658.3381320384 7893239.748026698 +2520 4326 597658.3381320384 7893239.748026698 179.70411033585785 71.09308594445899 +4284 2521 -177.0 68.0 500000.0 7546013.844298534 +2521 4326 500000.0 7546013.844298534 -176.99637291121957 67.99997982620903 +4284 2521 -179.7 64.71499999999999 371298.71546732273 7182432.038391958 +2521 4326 371298.71546732273 7182432.038391958 -179.69682063024806 64.71499503698772 +4284 2521 -174.3 71.28500000000001 596702.8548685303 7914643.984382171 +2521 4326 596702.8548685303 7914643.984382171 -174.29577206325075 71.2849617561507 +4284 2522 -171.485 65.69 477711.5325981353 7288485.446295194 +2522 4326 477711.5325981353 7288485.446295194 -171.48163968472628 65.68983928245953 +4284 2522 -173.7485 64.349 367220.71177890955 7141756.667395102 +2522 4326 367220.71177890955 7141756.667395102 -173.74530593397034 64.34887069809866 +4284 2522 -169.2215 67.031 577471.7906795319 7439047.596853634 +2522 4326 577471.7906795319 7439047.596853634 -169.21795844385346 67.03080683159655 +4284 2523 21.035 53.67 7502313.304206331 5949291.33238347 +2523 4326 7502313.304206331 5949291.33238347 21.03303037972336 53.66971222595396 +4284 2523 19.7165 48.783 7405676.145614638 5406385.465037597 +2523 4326 7405676.145614638 5406385.465037597 19.714746341771026 48.782630111244885 +4284 2523 22.3535 58.557 7578787.665179446 6494230.324666152 +2523 4326 7578787.665179446 6494230.324666152 22.35124089290801 58.556797992696204 +4284 2524 24.0 53.730000000000004 8500000.0 5955968.84064012 +2524 4326 8500000.0 5955968.84064012 23.99806968645784 53.72976973937217 +4284 2524 22.65 48.312 8399864.10807568 5354095.192894682 +2524 4326 8399864.10807568 5354095.192894682 22.64830163891125 48.31167570538094 +4284 2524 25.35 59.147999999999996 8577256.317763945 6560051.84760618 +2524 4326 8577256.317763945 6560051.84760618 25.347748132627267 59.147867404062765 +4284 2525 27.0 57.095 9500000.0 6330603.976743402 +2525 4326 9500000.0 6330603.976743402 26.997903367127847 57.09487063493642 +4284 2525 25.641 46.4435 9395570.365429489 5146371.72887164 +2525 4326 9395570.365429489 5146371.72887164 25.63938913165608 46.44320115243908 +4284 2525 28.359 67.7465 9557453.804098958 7518370.171618687 +2525 4326 9557453.804098958 7518370.171618687 28.355908803677664 67.7465443885544 +4284 2526 30.0 57.515 10500000.0 6377378.202191002 +2526 4326 10500000.0 6377378.202191002 29.997910166758235 57.51493244722986 +4284 2526 28.65 46.4135 10396205.007186998 5143025.075466804 +2526 4326 10396205.007186998 5143025.075466804 28.64841501090226 46.41324967528119 +4284 2526 31.35 68.6165 10554950.760076277 7615382.177144757 +2526 4326 10554950.760076277 7615382.177144757 31.346832796120285 68.61661716776976 +4284 2527 33.0 57.17 11500000.0 6338956.286202614 +2527 4326 11500000.0 6338956.286202614 32.99797166629755 57.169982463225324 +4284 2527 31.65 45.605 11394681.344383817 5053158.39054489 +2527 4326 11394681.344383817 5053158.39054489 31.648471126360747 45.60478579713451 +4284 2527 34.35 68.735 11554660.639777908 7628597.7609414905 +2527 4326 11554660.639777908 7628597.7609414905 34.34687704748242 68.73517896236058 +4284 2528 36.0 57.0 12500000.0 6320024.529201341 +2528 4326 12500000.0 6320024.529201341 35.998027703999476 57.000033850954864 +4284 2528 34.65 45.849 12395138.965421073 5080278.296049854 +2528 4326 12395138.965421073 5080278.296049854 34.64849756789288 45.84883645574105 +4284 2528 37.35 68.151 12556088.108187674 7563469.517061494 +2528 4326 12556088.108187674 7563469.517061494 37.347032377141275 68.15122929193949 +4284 2529 39.0 55.935 13500000.0 6201434.471523716 +2529 4326 13500000.0 6201434.471523716 38.99813953235876 55.9350694554073 +4284 2529 37.65 44.3565 13392370.04821004 4914408.366928867 +2529 4326 13392370.04821004 4914408.366928867 37.64858138326694 44.356358176135956 +4284 2529 40.35 67.5135 13557639.585595706 7492379.900590431 +2529 4326 13557639.585595706 7492379.900590431 40.347195190664095 67.51377701201694 +4284 2530 42.004999999999995 54.875 14500320.969945366 6083421.67012953 +2530 4326 14500320.969945366 6083421.67012953 42.00321976422327 54.87510183743795 +4284 2530 40.6505 42.396499999999996 14388886.860949045 4696644.2738888245 +2530 4326 14388886.860949045 4696644.2738888245 40.64914991754452 42.3963709478751 +4284 2530 43.3595 67.3535 14558436.136575637 7474547.544804178 +2530 4326 14558436.136575637 7474547.544804178 43.356761311765354 67.3538265561743 +4284 2531 45.0 59.82 15500000.0 6634134.81890289 +2531 4326 15500000.0 6634134.81890289 44.9979947858554 59.82023482739177 +4284 2531 43.65 40.938 15386307.722621337 4534645.846822697 +2531 4326 15386307.722621337 4534645.846822697 43.64873024225554 40.93788720528176 +4284 2531 46.35 78.702 15529535.088096837 8740703.393105498 +2531 4326 15529535.088096837 8740703.393105498 46.34457938684354 78.70254935470136 +4284 2532 48.0 59.61 16500000.0 6610738.865179384 +2532 4326 16500000.0 6610738.865179384 47.99808178866238 59.61028170739264 +4284 2532 46.65 40.440000000000005 16385458.009287085 4479340.7018074915 +2532 4326 16385458.009287085 4479340.7018074915 46.64878951840585 40.439917864510505 +4284 2532 49.35 78.78 16529333.852563867 8749410.077863732 +2532 4326 16529333.852563867 8749410.077863732 49.34473501265106 78.78060623021614 +4284 2533 51.0 59.44 17500000.0 6591799.836535772 +2533 4326 17500000.0 6591799.836535772 50.998171043360124 59.440327401770595 +4284 2533 49.65 39.837999999999994 17384442.45368887 4412491.912956868 +2533 4326 17384442.45368887 4412491.912956868 49.648853910713896 39.83794463914226 +4284 2533 52.35 79.042 17528657.504543435 8778655.92340142 +2533 4326 17528657.504543435 8778655.92340142 52.34481981151368 79.04266371428004 +4284 2534 54.0 59.37 18500000.0 6584001.557190209 +2534 4326 18500000.0 6584001.557190209 53.99825911369662 59.37037292646763 +4284 2534 52.65 39.534 18383934.475280672 4378736.883691058 +2534 4326 18383934.475280672 4378736.883691058 52.64891491280133 39.53397519622565 +4284 2534 55.35 79.20599999999999 18528233.830264606 8796962.726478616 +2534 4326 18528233.830264606 8796962.726478616 55.34497120453828 79.20671771676884 +4284 2535 57.0 59.765 19500000.0 6628007.234396709 +2535 4326 19500000.0 6628007.234396709 56.998325795408455 59.76542501771851 +4284 2535 55.65 39.8525 19384466.76464391 4414101.980867032 +2535 4326 19384466.76464391 4414101.980867032 55.648967032099506 39.85251669930715 +4284 2535 58.35 79.67750000000001 19527014.467029564 8849595.80167952 +2535 4326 19527014.467029564 8849595.80167952 58.34499218340281 79.67827384394437 +4284 2536 60.0 58.709999999999994 20500000.0 6510479.082543071 +2536 4326 20500000.0 6510479.082543071 59.99847685608911 58.710447991453506 +4284 2536 58.65 37.83 20381148.235995602 4189561.9094953467 +2536 4326 20381148.235995602 4189561.9094953467 58.64906460671127 37.830007815621585 +4284 2536 61.35 79.59 20527240.89723357 8839828.151750296 +2536 4326 20527240.89723357 8839828.151750296 61.345305552770746 79.59081960520142 +4284 2537 63.0 58.455 21500000.0 6482074.698219859 +2537 4326 21500000.0 6482074.698219859 62.99858624107984 58.45548335902813 +4284 2537 61.65 37.4715 21380575.39424042 4149768.5164054274 +2537 4326 21380575.39424042 4149768.5164054274 61.649133673700156 37.47153102484422 +4284 2537 64.35 79.4385 21527632.7916749 8822916.283554997 +2537 4326 21527632.7916749 8822916.283554997 64.34565459338822 79.43936184018762 +4284 2538 66.0 58.760000000000005 22500000.0 6516048.702591137 +2538 4326 22500000.0 6516048.702591137 65.99867342570914 58.76052730825238 +4284 2538 64.65 38.519000000000005 22382262.26593295 4266047.150425227 +2538 4326 22382262.26593295 4266047.150425227 64.64918368988833 38.519083569036134 +4284 2538 67.35 79.001 22528763.385898113 8774079.251713157 +2538 4326 22528763.385898113 8774079.251713157 67.34611445682809 79.0018971213398 +4284 2539 69.0 57.0 23500000.0 6320024.529201341 +2539 4326 23500000.0 6320024.529201341 68.99885008203168 57.00052743205564 +4284 2539 67.65 38.937 23382946.45811018 4312453.087373567 +2539 4326 23382946.45811018 4312453.087373567 67.64924719811765 38.93712119063493 +4284 2539 70.35 75.06299999999999 23538855.165733863 8334556.146998051 +2539 4326 23538855.165733863 8334556.146998051 70.34739811381961 75.06387500056401 +4284 2540 72.0 55.12 24500000.0 6110696.373866957 +2540 4326 24500000.0 6110696.373866957 71.99901711543247 55.120520843645096 +4284 2540 70.65 38.515 24382255.748957906 4265603.089644279 +2540 4326 24382255.748957906 4265603.089644279 70.64932579428348 38.515137967936504 +4284 2540 73.35 71.725 24547265.07700449 7962110.452154538 +2540 4326 24547265.07700449 7962110.452154538 73.34808625366881 71.72585478613388 +4284 2541 75.0 58.464999999999996 25500000.0 6483188.57425185 +2541 4326 25500000.0 6483188.57425185 74.99901339340963 58.46562064462709 +4284 2541 73.65 39.3445 25383619.483029503 4357696.354864248 +2541 4326 25383619.483029503 4357696.354864248 73.64938975567 39.34468207527749 +4284 2541 76.35 77.5855 25532409.301960163 8616079.919088842 +2541 4326 25532409.301960163 8616079.919088842 76.3473800896382 77.58648321361144 +4284 2542 78.0 60.735 26500000.0 6736083.067799823 +2542 4326 26500000.0 6736083.067799823 77.99905030294669 60.735695297976754 +4284 2542 76.65 42.4695 26388974.636924773 4704754.2373690875 +2542 4326 26388974.636924773 4704754.2373690875 76.64942677443652 42.469779307884664 +4284 2542 79.35 79.0005 26528764.677041702 8774023.43871601 +2542 4326 26528764.677041702 8774023.43871601 79.347340003376 79.00153475711971 +4284 2543 81.0 61.425 27500000.0 6812971.357972408 +2543 4326 27500000.0 6812971.357972408 80.99919516569001 61.42573083915098 +4284 2543 79.65 43.7805 27391320.989803866 4850405.237036408 +2543 4326 27391320.989803866 4850405.237036408 79.64952580604825 43.78082541419658 +4284 2543 82.35 79.0695 27528586.47818664 8781725.648760239 +2543 4326 27528586.47818664 8781725.648760239 82.34773446252879 79.07056153421478 +4284 2544 84.0 61.335 28500000.0 6802942.002768118 +2544 4326 28500000.0 6802942.002768118 83.99933070002568 61.33575123421406 +4284 2544 82.65 46.7325 28396811.922323894 5178485.996629995 +2544 4326 28396811.922323894 5178485.996629995 82.649575943349 46.73291580330272 +4284 2544 85.35 75.9375 28536628.54897211 8432148.564629411 +2544 4326 28536628.54897211 8432148.564629411 85.34854816040804 75.93853414270663 +4284 2545 87.0 62.105 29500000.0 6888753.006427115 +2545 4326 29500000.0 6888753.006427115 86.99944521940921 62.10578653534026 +4284 2545 85.65 48.555499999999995 29400341.521783497 5381171.662034284 +2545 4326 29400341.521783497 5381171.662034284 85.64964719818043 48.55597717704787 +4284 2545 88.35 75.6545 29537350.0879231 8400565.58925793 +2545 4326 29537350.0879231 8400565.58925793 88.34884370274929 75.65554871829634 +4284 2546 90.0 65.36 30500000.0 7251603.712863622 +2546 4326 30500000.0 7251603.712863622 89.99950243509471 65.36086911309647 +4284 2546 88.65 51.032 30405298.583410114 5656612.565386054 +2546 4326 30405298.583410114 5656612.565386054 88.64971683963398 51.032551360333755 +4284 2546 91.35 79.688 30526987.29108325 8850767.923054414 +2546 4326 30526987.29108325 8850767.923054414 91.34867613679852 79.6891294108006 +4284 2547 93.0 65.71000000000001 31500000.0 7290629.573861116 +2547 4326 31500000.0 7290629.573861116 92.99965380400627 65.7108896258562 +4284 2547 91.65 51.714999999999996 31406697.50033778 5732596.67227399 +2547 4326 31406697.50033778 5732596.67227399 91.6498149763301 51.71558088778133 +4284 2547 94.35 79.705 31526943.28997574 8852665.644907387 +2547 4326 31526943.28997574 8852665.644907387 94.34904734179261 79.70614235214137 +4284 2548 96.0 65.53999999999999 32500000.0 7271673.929040066 +2548 4326 32500000.0 7271673.929040066 95.99981991122304 65.54089661598087 +4284 2548 94.65 51.311 32405868.400453772 5687650.444429479 +2548 4326 32405868.400453772 5687650.444429479 94.6499262109476 51.311582336334894 +4284 2548 97.35 79.76899999999999 32526777.617117506 8859810.02646717 +2548 4326 32526777.617117506 8859810.02646717 97.34941928207886 79.77015255871679 +4284 2549 99.0 65.345 33500000.0 7249931.216590782 +2549 4326 33500000.0 7249931.216590782 98.9999852157738 65.34589986834845 +4284 2549 97.65 51.3455 33405939.018448576 5691488.559383896 +2549 4326 33405939.018448576 5691488.559383896 97.65003380968832 51.346091261425215 +4284 2549 100.35 79.34450000000001 33527875.84858289 8812423.188445145 +2549 4326 33527875.84858289 8812423.188445145 100.34981796832243 79.34565173948701 +4284 2551 102.0 64.94 34500000.0 7204775.08913092 +2551 4326 34500000.0 7204775.08913092 102.00014962231847 64.9408956557825 +4284 2551 100.65 51.647 34406557.62006173 5725031.259833232 +2551 4326 34406557.62006173 5725031.259833232 100.65014118114296 51.647603666621855 +4284 2551 103.35 78.23299999999999 34530743.89294062 8688352.587361421 +2551 4326 34530743.89294062 8688352.587361421 103.35019044113253 78.23413652433204 +4284 2552 105.0 64.67 35500000.0 7174672.376113227 +2552 4326 35500000.0 7174672.376113227 105.00030954296194 64.67089090358678 +4284 2552 103.65 51.584 35406428.143638335 5718022.201994142 +2552 4326 35406428.143638335 5718022.201994142 103.65025056392167 51.58460471640155 +4284 2552 106.35 77.756 35531971.165515326 8635110.48047606 +2552 4326 35531971.165515326 8635110.48047606 106.35051284670173 77.75712767734723 +4284 2553 108.0 63.825 36500000.0 7080469.229135362 +2553 4326 36500000.0 7080469.229135362 108.00046193969406 63.82587128109139 +4284 2553 106.65 50.7075 36404638.690165125 5620514.724274666 +2553 4326 36404638.690165125 5620514.724274666 106.65035842565999 50.708083898741634 +4284 2553 109.35 76.9425 36534058.967837825 8544312.772368127 +2553 4326 36534058.967837825 8544312.772368127 109.35079623733208 76.94360984391183 +4284 2554 111.0 62.975 37500000.0 6985719.868823676 +2554 4326 37500000.0 6985719.868823676 111.00060481075904 62.97584828674728 +4284 2554 109.65 50.5235 37404265.883025005 5600047.15044294 +2554 4326 37404265.883025005 5600047.15044294 109.65046439860778 50.524076585649325 +4284 2554 112.35 75.4265 37537930.72489836 8375121.167485982 +2554 4326 37537930.72489836 8375121.167485982 112.35100765452619 75.42757673989031 +4284 2555 114.0 63.095 38500000.0 6999095.558554125 +2555 4326 38500000.0 6999095.558554125 114.00075435486437 63.09584195310667 +4284 2555 112.65 50.850500000000004 38404929.11151414 5636422.012520418 +2555 4326 38404929.11151414 5636422.012520418 112.65057187585138 50.85107844050717 +4284 2555 115.35 75.3395 38538152.12316031 8365412.235560505 +2555 4326 38538152.12316031 8365412.235560505 115.3512675038626 75.34056383176454 +4284 2556 117.0 61.97 39500000.0 6873707.519296429 +2556 4326 39500000.0 6873707.519296429 117.00087746891691 61.97080715726947 +4284 2556 115.65 50.756 39404737.12264642 5625909.81198809 +2556 4326 39404737.12264642 5625909.81198809 115.65067642292615 50.75656770356181 +4284 2556 118.35 73.184 39543607.57144355 8124888.018222148 +2556 4326 39543607.57144355 8124888.018222148 118.35136810697176 73.18501251719654 +4284 2557 120.0 61.75 40500000.0 6849189.584093224 +2557 4326 40500000.0 6849189.584093224 120.00101149853334 61.750787843054894 +4284 2557 118.65 51.058 40405351.58911423 5659504.924788193 +2557 4326 40405351.58911423 5659504.924788193 118.65078347068562 51.058563313398224 +4284 2557 121.35 72.442 40545471.40450936 8042101.79342143 +2557 4326 40545471.40450936 8042101.79342143 121.35153831122665 72.4429817810191 +4284 2558 123.0 63.59 41500000.0 7054272.683313027 +2558 4326 41500000.0 7054272.683313027 123.00120448163428 63.59080665111138 +4284 2558 121.65 54.221 41411943.373765595 6011461.978227262 +2558 4326 41411943.373765595 6011461.978227262 121.65093166581178 54.22161953389662 +4284 2558 124.35 72.959 41544173.55017843 8099783.769696419 +2558 4326 41544173.55017843 8099783.769696419 124.35179516220852 72.95996955551738 +4284 2559 126.0 61.94 42500000.0 6870364.117988872 +2559 4326 42500000.0 6870364.117988872 126.00128773905809 61.940752961897786 +4284 2559 124.65 51.086000000000006 42405408.69417232 5662619.787043772 +2559 4326 42405408.69417232 5662619.787043772 124.65098806185799 51.0865331434005 +4284 2559 127.35 72.794 42544588.161487184 8081374.322282203 +2559 4326 42544588.161487184 8081374.322282203 127.3519932759244 72.79494240265178 +4284 2560 129.0 58.13 43500000.0 6445874.681044001 +2560 4326 43500000.0 6445874.681044001 129.0012933855904 58.130652866937346 +4284 2560 127.65 44.216 43392113.14502077 4898795.928046418 +2560 4326 43392113.14502077 4898795.928046418 127.65098890826958 44.21635979322098 +4284 2560 130.35 72.044 43546467.967599645 7997698.656164544 +2560 4326 43546467.967599645 7997698.656164544 130.35212173710588 72.04490226719803 +4284 2561 132.0 57.12 44500000.0 6333388.0687238285 +2561 4326 44500000.0 6333388.0687238285 132.00137624173857 57.12060692303777 +4284 2561 130.65 43.737 44391242.21138428 4845571.915807717 +2561 4326 44391242.21138428 4845571.915807717 130.65106714714474 43.73732973001166 +4284 2561 133.35 70.50299999999999 44550304.58228892 7825792.539013257 +2561 4326 44550304.58228892 7825792.539013257 133.35215769686042 70.50384568401239 +4284 2562 135.0 59.32000000000001 45500000.0 6578431.409257122 +2562 4326 45500000.0 6578431.409257122 135.00156203037548 59.320622507215326 +4284 2562 133.65 44.398 45392446.05527696 4919019.93990623 +2562 4326 45392446.05527696 4919019.93990623 133.65115816923324 44.39832294867807 +4284 2562 136.35 74.242 45540937.191698745 8242940.659761529 +2562 4326 45540937.191698745 8242940.659761529 136.35280994218306 74.24287448296927 +4284 2563 138.0 60.515 46500000.0 6711569.630751212 +2563 4326 46500000.0 6711569.630751212 138.0017249720011 60.51561403404302 +4284 2563 136.65 46.335499999999996 46396057.100101076 5134354.665550572 +2563 4326 46396057.100101076 5134354.665550572 136.6512710746778 46.3358405689911 +4284 2563 139.35 74.69449999999999 46539790.698132046 8293434.3976194095 +2563 4326 46539790.698132046 8293434.3976194095 139.35309678639027 74.69534499483011 +4284 2564 141.0 61.035000000000004 47500000.0 6769511.786851006 +2564 4326 47500000.0 6769511.786851006 141.00186193927297 61.03558939964067 +4284 2564 139.65 47.359500000000004 47398014.19509624 5248190.514654343 +2564 4326 47398014.19509624 5248190.514654343 139.65137148014844 47.35983480126917 +4284 2564 142.35 74.7105 47539750.11249035 8295219.847347124 +2564 4326 47539750.11249035 8295219.847347124 142.3533047666016 74.71130571587523 +4284 2565 144.0 59.795 48500000.0 6631349.546819799 +2565 4326 48500000.0 6631349.546819799 144.0019062274426 59.79552980845937 +4284 2565 142.65 45.2285 48393978.99510848 5011313.64329941 +2565 4326 48393978.99510848 5011313.64329941 142.6514050769463 45.22876187827467 +4284 2565 145.35 74.3615 48540634.66546933 8256275.278797705 +2565 4326 48540634.66546933 8256275.278797705 145.35343025611581 74.36225812766193 +4284 2566 147.0 60.18000000000001 49500000.0 6674243.9161159545 +2566 4326 49500000.0 6674243.9161159545 147.0020264527326 60.180497812633845 +4284 2566 145.65 45.258 49394033.86048691 5014592.220913456 +2566 4326 49394033.86048691 5014592.220913456 145.65147759747333 45.25823185089947 +4284 2566 148.35 75.102 49538756.057791665 8338908.319397172 +2566 4326 49538756.057791665 8338908.319397172 148.35377761294026 75.10272459706738 +4284 2567 150.0 61.015 50500000.0 6767283.158904388 +2567 4326 50500000.0 6767283.158904388 150.00217146783544 61.01547098804115 +4284 2567 148.65 46.7905 50396922.61716172 5184933.63842068 +2567 4326 50396922.61716172 5184933.63842068 148.65158189164822 46.790728595177185 +4284 2567 151.35 75.23949999999999 50538406.493328564 8354252.628865748 +2567 4326 50538406.493328564 8354252.628865748 151.35399424110366 75.24017929013634 +4284 2568 153.0 61.49 51500000.0 6820214.864260193 +2568 4326 51500000.0 6820214.864260193 153.00229537262493 61.49043511703583 +4284 2568 151.65 48.197 51399639.26863816 5341307.916347078 +2568 4326 51399639.26863816 5341307.916347078 151.65168621679993 48.19721910046296 +4284 2568 154.35 74.783 51539566.16933026 8303310.196740806 +2568 4326 51539566.16933026 8303310.196740806 154.35405284510077 74.78362293192991 +4284 2569 156.0 63.11 52500000.0 7000767.5358309625 +2569 4326 52500000.0 7000767.5358309625 156.00250281284855 63.11041530950698 +4284 2569 154.65 50.429 52404074.80100888 5589535.510239827 +2569 4326 52404074.80100888 5589535.510239827 154.65182005105873 50.42922046472141 +4284 2569 157.35 75.791 52537002.1811373 8415798.985367114 +2569 4326 52537002.1811373 8415798.985367114 157.35448714530284 75.79158555974833 +4284 2570 159.0 61.24 53500000.0 6792355.606366323 +2570 4326 53500000.0 6792355.606366323 159.00244960469269 61.24033689547714 +4284 2570 157.65 52.348 53408005.95938851 5803025.74679606 +2570 4326 53408005.95938851 5803025.74679606 157.65195335948593 52.3482117114869 +4284 2570 160.35 70.132 53551222.84047558 7784409.744017398 +2570 4326 53551222.84047558 7784409.744017398 160.35341904114134 70.13245021527631 +4284 2571 162.0 62.660000000000004 54500000.0 6950609.774137258 +2571 4326 54500000.0 6950609.774137258 162.00263472887644 62.660308924969314 +4284 2571 160.65 55.172000000000004 54413979.217210464 6117317.375717458 +2571 4326 54413979.217210464 6117317.375717458 160.6521379713491 55.1722133015478 +4284 2571 163.35 70.148 54551183.28379403 7786194.413216086 +2571 4326 54551183.28379403 7786194.413216086 163.35352635585596 70.14839614926863 +4284 2572 165.0 62.254999999999995 55500000.0 6905470.561343054 +2572 4326 55500000.0 6905470.561343054 165.00267478561778 62.255250754743635 +4284 2572 163.65 55.4465 55414571.32964776 6147874.7795773605 +2572 4326 55414571.32964776 6147874.7795773605 163.65221240297404 55.44667011267831 +4284 2572 166.35 69.06349999999999 55553855.144410476 7665234.286115813 +2572 4326 55553855.144410476 7665234.286115813 166.3534537530268 69.0638248706224 +4284 2573 168.0 62.26 56500000.0 6906027.819449784 +2573 4326 56500000.0 6906027.819449784 168.00274078835366 62.26019731639685 +4284 2573 166.65 55.231 56414106.315726385 6123885.159243279 +2573 4326 56414106.315726385 6123885.159243279 166.6522572234698 55.23111777249621 +4284 2573 169.35 69.28899999999999 56553301.16667772 7690384.335419543 +2573 4326 56553301.16667772 7690384.335419543 169.35357022782088 69.28927080631968 +4284 2574 171.0 65.025 57500000.0 7214252.097012479 +2574 4326 57500000.0 7214252.097012479 171.0030625482431 65.02518057241727 +4284 2574 169.65 60.3765 57425529.86040934 6696900.434513191 +2574 4326 57425529.86040934 6696900.434513191 169.65262567578492 60.37664067217935 +4284 2574 172.35 69.67349999999999 57552354.64972791 7733269.117284499 +2574 4326 57552354.64972791 7733269.117284499 172.3537058169254 69.67371729373235 +4284 2575 174.0 65.505 58500000.0 7267771.349342988 +2575 4326 58500000.0 7267771.349342988 174.00317277325598 65.50512942938381 +4284 2575 172.65 61.441500000000005 58427973.33410805 6815555.439263968 +2575 4326 58427973.33410805 6815555.439263968 172.65276027387094 61.44160053942982 +4284 2575 175.35 69.5685 58552613.36597603 7721557.879740137 +2575 4326 58552613.36597603 7721557.879740137 175.35375394229854 69.56865591610733 +4284 2576 177.0 66.595 59500000.0 7389317.258583589 +2576 4326 59500000.0 7389317.258583589 177.00335412632597 66.5950845230114 +4284 2576 175.65 62.5405 59430521.24033901 6938017.003109125 +2576 4326 59430521.24033901 6938017.003109125 175.65290133801645 62.54055854184846 +4284 2576 178.35 70.64949999999999 59549941.38921676 7842134.168207353 +2576 4326 59549941.38921676 7842134.168207353 178.3540015157485 70.64960842541734 +4284 2578 -177.0 68.175 61500000.0 7565533.128007671 +2578 4326 61500000.0 7565533.128007671 -176.99634694660293 68.17498191105814 +4284 2578 -178.35 65.0835 61436514.64138311 7221452.919002606 +2578 4326 61436514.64138311 7221452.919002606 -178.3467642584832 65.08347243671847 +4284 2578 -175.65 71.2665 61548408.15041371 7910961.358646199 +2578 4326 61548408.15041371 7910961.358646199 -175.64578987224337 71.26649023247916 +4284 2579 -174.0 65.99000000000001 62500000.0 7321851.563124144 +2579 4326 62500000.0 7321851.563124144 -173.9966175923129 65.98989446527256 +4284 2579 -175.35 64.379 62434841.045073986 7142921.782737626 +2579 4326 62434841.045073986 7142921.782737626 -175.34681411741292 64.37890351968426 +4284 2579 -172.65 67.601 62557427.06234874 7502136.985763806 +2579 4326 62557427.06234874 7502136.985763806 -172.6463936551253 67.60088474739696 +4284 2580 -171.035 65.705 63498392.47244824 7290072.496315905 +2580 4326 63498392.47244824 7290072.496315905 -171.0316358810247 65.7048301581793 +4284 2580 -172.3535 64.4855 63434925.152851224 7154796.54233314 +2580 4326 63434925.152851224 7154796.54233314 -172.35028298963678 64.48534381714241 +4284 2580 -169.7165 66.9245 63556157.363313794 7426641.888505799 +2580 4326 63556157.363313794 7426641.888505799 -169.7129743754214 66.92431602515701 +4284 2581 -169.095 65.78 64449855.09013895 7298871.971787686 +2581 4326 64449855.09013895 7298871.971787686 -169.0916203845996 65.7797908401432 +4284 2581 -169.2075 65.708 64444549.4376292 7290939.162325591 +2581 4326 64444549.4376292 7290939.162325591 -169.2041294663817 65.70779240415932 +4284 2581 -168.9825 65.852 64455132.19876915 7306814.480001371 +2581 4326 64455132.19876915 7306814.480001371 -168.97911125872193 65.85178927369687 +4284 2582 21.035 53.67 502313.3042063311 5949291.33238347 +2582 4326 502313.3042063311 5949291.33238347 21.033030379723353 53.66971222595396 +4284 2582 19.7165 48.783 405676.14561463776 5406385.465037597 +2582 4326 405676.14561463776 5406385.465037597 19.714746341771022 48.782630111244885 +4284 2582 22.3535 58.557 578787.6651794461 6494230.324666152 +2582 4326 578787.6651794461 6494230.324666152 22.351240892908002 58.556797992696204 +4284 2583 24.0 53.730000000000004 500000.0 5955968.84064012 +2583 4326 500000.0 5955968.84064012 23.99806968645784 53.72976973937217 +4284 2583 22.65 48.312 399864.1080756806 5354095.192894682 +2583 4326 399864.1080756806 5354095.192894682 22.64830163891126 48.31167570538094 +4284 2583 25.35 59.147999999999996 577256.3177639446 6560051.84760618 +2583 4326 577256.3177639446 6560051.84760618 25.347748132627256 59.147867404062765 +4284 2584 27.0 57.095 500000.0 6330603.976743402 +2584 4326 500000.0 6330603.976743402 26.997903367127847 57.09487063493642 +4284 2584 25.641 46.4435 395570.3654294889 5146371.72887164 +2584 4326 395570.3654294889 5146371.72887164 25.63938913165608 46.44320115243908 +4284 2584 28.359 67.7465 557453.8040989588 7518370.171618687 +2584 4326 557453.8040989588 7518370.171618687 28.355908803677682 67.7465443885544 +4284 2585 30.0 57.515 500000.0 6377378.202191002 +2585 4326 500000.0 6377378.202191002 29.997910166758235 57.51493244722986 +4284 2585 28.65 46.4135 396205.00718699774 5143025.075466804 +2585 4326 396205.00718699774 5143025.075466804 28.64841501090226 46.41324967528119 +4284 2585 31.35 68.6165 554950.7600762765 7615382.177144757 +2585 4326 554950.7600762765 7615382.177144757 31.346832796120275 68.61661716776976 +4284 2586 33.0 57.17 500000.0 6338956.286202614 +2586 4326 500000.0 6338956.286202614 32.99797166629755 57.169982463225324 +4284 2586 31.65 45.605 394681.34438381734 5053158.39054489 +2586 4326 394681.34438381734 5053158.39054489 31.648471126360747 45.60478579713451 +4284 2586 34.35 68.735 554660.6397779081 7628597.7609414905 +2586 4326 554660.6397779081 7628597.7609414905 34.34687704748242 68.73517896236058 +4284 2587 36.0 57.0 500000.0 6320024.529201341 +2587 4326 500000.0 6320024.529201341 35.998027703999476 57.000033850954864 +4284 2587 34.65 45.849 395138.96542107355 5080278.296049854 +2587 4326 395138.96542107355 5080278.296049854 34.64849756789288 45.84883645574106 +4284 2587 37.35 68.151 556088.1081876728 7563469.517061494 +2587 4326 556088.1081876728 7563469.517061494 37.34703237714125 68.15122929193949 +4284 2588 39.0 55.935 500000.0 6201434.471523716 +2588 4326 500000.0 6201434.471523716 38.99813953235876 55.9350694554073 +4284 2588 37.65 44.3565 392370.04821004043 4914408.366928867 +2588 4326 392370.04821004043 4914408.366928867 37.64858138326694 44.356358176135956 +4284 2588 40.35 67.5135 557639.5855957068 7492379.900590431 +2588 4326 557639.5855957068 7492379.900590431 40.347195190664095 67.51377701201696 +4284 2589 42.004999999999995 54.875 500320.9699453647 6083421.67012953 +2589 4326 500320.9699453647 6083421.67012953 42.003219764223246 54.87510183743795 +4284 2589 40.6505 42.396499999999996 388886.8609490452 4696644.2738888245 +2589 4326 388886.8609490452 4696644.2738888245 40.64914991754452 42.3963709478751 +4284 2589 43.3595 67.3535 558436.1365756376 7474547.544804178 +2589 4326 558436.1365756376 7474547.544804178 43.35676131176536 67.3538265561743 +4284 2590 45.0 59.82 500000.0 6634134.81890289 +2590 4326 500000.0 6634134.81890289 44.9979947858554 59.82023482739177 +4284 2590 43.65 40.938 386307.7226213374 4534645.846822697 +2590 4326 386307.7226213374 4534645.846822697 43.648730242255546 40.937887205281754 +4284 2590 46.35 78.702 529535.0880968368 8740703.393105498 +2590 4326 529535.0880968368 8740703.393105498 46.34457938684354 78.70254935470138 +4284 2591 48.0 59.61 500000.0 6610738.865179384 +2591 4326 500000.0 6610738.865179384 47.99808178866238 59.61028170739264 +4284 2591 46.65 40.440000000000005 385458.0092870846 4479340.7018074915 +2591 4326 385458.0092870846 4479340.7018074915 46.64878951840585 40.439917864510505 +4284 2591 49.35 78.78 529333.8525638671 8749410.077863732 +2591 4326 529333.8525638671 8749410.077863732 49.344735012651036 78.78060623021614 +4284 2592 51.0 59.44 500000.0 6591799.836535772 +2592 4326 500000.0 6591799.836535772 50.998171043360124 59.440327401770595 +4284 2592 49.65 39.837999999999994 384442.4536888724 4412491.912956868 +2592 4326 384442.4536888724 4412491.912956868 49.6488539107139 39.83794463914225 +4284 2592 52.35 79.042 528657.5045434341 8778655.92340142 +2592 4326 528657.5045434341 8778655.92340142 52.34481981151365 79.04266371428004 +4284 2593 54.0 59.37 500000.0 6584001.557190209 +2593 4326 500000.0 6584001.557190209 53.99825911369662 59.37037292646763 +4284 2593 52.65 39.534 383934.47528067324 4378736.883691058 +2593 4326 383934.47528067324 4378736.883691058 52.64891491280134 39.53397519622565 +4284 2593 55.35 79.20599999999999 528233.8302646062 8796962.726478616 +2593 4326 528233.8302646062 8796962.726478616 55.344971204538304 79.20671771676884 +4284 2594 57.0 59.765 500000.0 6628007.234396709 +2594 4326 500000.0 6628007.234396709 56.998325795408455 59.76542501771851 +4284 2594 55.65 39.8525 384466.76464391185 4414101.980867032 +2594 4326 384466.76464391185 4414101.980867032 55.648967032099506 39.85251669930715 +4284 2594 58.35 79.67750000000001 527014.4670295651 8849595.80167952 +2594 4326 527014.4670295651 8849595.80167952 58.34499218340286 79.67827384394437 +4284 2595 60.0 58.709999999999994 500000.0 6510479.082543071 +2595 4326 500000.0 6510479.082543071 59.99847685608911 58.710447991453506 +4284 2595 58.65 37.83 381148.23599560175 4189561.9094953467 +2595 4326 381148.23599560175 4189561.9094953467 58.64906460671127 37.830007815621585 +4284 2595 61.35 79.59 527240.8972335726 8839828.151750296 +2595 4326 527240.8972335726 8839828.151750296 61.34530555277078 79.59081960520142 +4284 2596 63.0 58.455 500000.0 6482074.698219859 +2596 4326 500000.0 6482074.698219859 62.99858624107984 58.45548335902813 +4284 2596 61.65 37.4715 380575.3942404202 4149768.5164054274 +2596 4326 380575.3942404202 4149768.5164054274 61.649133673700156 37.47153102484422 +4284 2596 64.35 79.4385 527632.7916748998 8822916.283554997 +2596 4326 527632.7916748998 8822916.283554997 64.34565459338818 79.43936184018762 +4284 2597 66.0 58.760000000000005 500000.0 6516048.702591137 +2597 4326 500000.0 6516048.702591137 65.99867342570914 58.76052730825238 +4284 2597 64.65 38.519000000000005 382262.2659329528 4266047.150425227 +2597 4326 382262.2659329528 4266047.150425227 64.64918368988835 38.519083569036134 +4284 2597 67.35 79.001 528763.3858981133 8774079.251713157 +2597 4326 528763.3858981133 8774079.251713157 67.34611445682809 79.0018971213398 +4284 2598 69.0 57.0 500000.0 6320024.529201341 +2598 4326 500000.0 6320024.529201341 68.99885008203168 57.00052743205564 +4284 2598 67.65 38.937 382946.4581101811 4312453.087373567 +2598 4326 382946.4581101811 4312453.087373567 67.64924719811766 38.93712119063496 +4284 2598 70.35 75.06299999999999 538855.1657338615 8334556.146998051 +2598 4326 538855.1657338615 8334556.146998051 70.34739811381957 75.06387500056402 +4284 2599 72.0 55.12 500000.0 6110696.373866957 +2599 4326 500000.0 6110696.373866957 71.99901711543247 55.120520843645096 +4284 2599 70.65 38.515 382255.7489579075 4265603.089644279 +2599 4326 382255.7489579075 4265603.089644279 70.6493257942835 38.51513796793651 +4284 2599 73.35 71.725 547265.0770044902 7962110.452154538 +2599 4326 547265.0770044902 7962110.452154538 73.34808625366885 71.72585478613388 +4284 2601 75.0 58.464999999999996 500000.0 6483188.57425185 +2601 4326 500000.0 6483188.57425185 74.99901339340963 58.46562064462709 +4284 2601 73.65 39.3445 383619.48302950425 4357696.354864248 +2601 4326 383619.48302950425 4357696.354864248 73.64938975567001 39.34468207527749 +4284 2601 76.35 77.5855 532409.301960162 8616079.919088842 +2601 4326 532409.301960162 8616079.919088842 76.34738008963815 77.58648321361144 +4284 2602 78.0 60.735 500000.0 6736083.067799823 +2602 4326 500000.0 6736083.067799823 77.99905030294669 60.735695297976754 +4284 2602 76.65 42.4695 388974.63692477404 4704754.2373690875 +2602 4326 388974.63692477404 4704754.2373690875 76.64942677443653 42.46977930788467 +4284 2602 79.35 79.0005 528764.6770417026 8774023.43871601 +2602 4326 528764.6770417026 8774023.43871601 79.34734000337605 79.00153475711971 +4284 2603 81.0 61.425 500000.0 6812971.357972408 +2603 4326 500000.0 6812971.357972408 80.99919516569001 61.42573083915098 +4284 2603 79.65 43.7805 391320.98980386736 4850405.237036408 +2603 4326 391320.98980386736 4850405.237036408 79.64952580604826 43.78082541419657 +4284 2603 82.35 79.0695 528586.4781866398 8781725.648760239 +2603 4326 528586.4781866398 8781725.648760239 82.34773446252875 79.07056153421476 +4284 2604 84.0 61.335 500000.0 6802942.002768118 +2604 4326 500000.0 6802942.002768118 83.99933070002568 61.33575123421406 +4284 2604 82.65 46.7325 396811.9223238922 5178485.996629995 +2604 4326 396811.9223238922 5178485.996629995 82.64957594334898 46.73291580330271 +4284 2604 85.35 75.9375 536628.5489721105 8432148.564629411 +2604 4326 536628.5489721105 8432148.564629411 85.34854816040801 75.93853414270663 +4284 2605 87.0 62.105 500000.0 6888753.006427115 +2605 4326 500000.0 6888753.006427115 86.99944521940921 62.10578653534026 +4284 2605 85.65 48.555499999999995 400341.5217834984 5381171.662034284 +2605 4326 400341.5217834984 5381171.662034284 85.64964719818045 48.55597717704787 +4284 2605 88.35 75.6545 537350.0879230971 8400565.58925793 +2605 4326 537350.0879230971 8400565.58925793 88.34884370274924 75.65554871829634 +4284 2606 90.0 65.36 500000.0 7251603.712863622 +2606 4326 500000.0 7251603.712863622 89.99950243509471 65.36086911309647 +4284 2606 88.65 51.032 405298.58341011347 5656612.565386054 +2606 4326 405298.58341011347 5656612.565386054 88.64971683963397 51.032551360333755 +4284 2606 91.35 79.688 526987.2910832507 8850767.923054414 +2606 4326 526987.2910832507 8850767.923054414 91.34867613679855 79.6891294108006 +4284 2607 93.0 65.71000000000001 500000.0 7290629.573861116 +2607 4326 500000.0 7290629.573861116 92.99965380400627 65.7108896258562 +4284 2607 91.65 51.714999999999996 406697.5003377808 5732596.67227399 +2607 4326 406697.5003377808 5732596.67227399 91.64981497633012 51.71558088778133 +4284 2607 94.35 79.705 526943.289975741 8852665.644907387 +2607 4326 526943.289975741 8852665.644907387 94.34904734179267 79.70614235214137 +4284 2608 96.0 65.53999999999999 500000.0 7271673.929040066 +2608 4326 500000.0 7271673.929040066 95.99981991122304 65.54089661598087 +4284 2608 94.65 51.311 405868.40045377356 5687650.444429479 +2608 4326 405868.40045377356 5687650.444429479 94.64992621094761 51.311582336334894 +4284 2608 97.35 79.76899999999999 526777.6171175041 8859810.02646717 +2608 4326 526777.6171175041 8859810.02646717 97.3494192820788 79.77015255871679 +4284 2609 99.0 65.345 500000.0 7249931.216590782 +2609 4326 500000.0 7249931.216590782 98.9999852157738 65.34589986834845 +4284 2609 97.65 51.3455 405939.01844857686 5691488.559383896 +2609 4326 405939.01844857686 5691488.559383896 97.65003380968834 51.34609126142522 +4284 2609 100.35 79.34450000000001 527875.8485828904 8812423.188445145 +2609 4326 527875.8485828904 8812423.188445145 100.34981796832245 79.34565173948701 +4284 2610 102.0 64.94 500000.0 7204775.08913092 +2610 4326 500000.0 7204775.08913092 102.00014962231847 64.9408956557825 +4284 2610 100.65 51.647 406557.6200617314 5725031.259833232 +2610 4326 406557.6200617314 5725031.259833232 100.65014118114293 51.64760366662185 +4284 2610 103.35 78.23299999999999 530743.8929406154 8688352.587361421 +2610 4326 530743.8929406154 8688352.587361421 103.35019044113241 78.23413652433204 +4284 2611 105.0 64.67 500000.0 7174672.376113227 +2611 4326 500000.0 7174672.376113227 105.00030954296194 64.67089090358678 +4284 2611 103.65 51.584 406428.14363833517 5718022.201994142 +2611 4326 406428.14363833517 5718022.201994142 103.65025056392167 51.58460471640155 +4284 2611 106.35 77.756 531971.1655153282 8635110.48047606 +2611 4326 531971.1655153282 8635110.48047606 106.35051284670183 77.75712767734723 +4284 2612 108.0 63.825 500000.0 7080469.229135362 +2612 4326 500000.0 7080469.229135362 108.00046193969406 63.82587128109139 +4284 2612 106.65 50.7075 404638.69016512326 5620514.724274666 +2612 4326 404638.69016512326 5620514.724274666 106.65035842565996 50.708083898741634 +4284 2612 109.35 76.9425 534058.9678378225 8544312.772368127 +2612 4326 534058.9678378225 8544312.772368127 109.35079623733196 76.94360984391183 +4284 2613 111.0 62.975 500000.0 6985719.868823676 +2613 4326 500000.0 6985719.868823676 111.00060481075904 62.97584828674728 +4284 2613 109.65 50.5235 404265.88302500197 5600047.15044294 +2613 4326 404265.88302500197 5600047.15044294 109.65046439860774 50.524076585649325 +4284 2613 112.35 75.4265 537930.7248983607 8375121.167485982 +2613 4326 537930.7248983607 8375121.167485982 112.35100765452619 75.42757673989031 +4284 2614 114.0 63.095 500000.0 6999095.558554125 +2614 4326 500000.0 6999095.558554125 114.00075435486437 63.09584195310667 +4284 2614 112.65 50.850500000000004 404929.1115141405 5636422.012520418 +2614 4326 404929.1115141405 5636422.012520418 112.65057187585134 50.85107844050717 +4284 2614 115.35 75.3395 538152.1231603118 8365412.235560505 +2614 4326 538152.1231603118 8365412.235560505 115.35126750386266 75.34056383176454 +4284 2615 117.0 61.97 500000.0 6873707.519296429 +2615 4326 500000.0 6873707.519296429 117.00087746891691 61.97080715726947 +4284 2615 115.65 50.756 404737.12264641805 5625909.81198809 +2615 4326 404737.12264641805 5625909.81198809 115.65067642292607 50.75656770356181 +4284 2615 118.35 73.184 543607.5714435482 8124888.018222148 +2615 4326 543607.5714435482 8124888.018222148 118.3513681069717 73.18501251719654 +4284 2616 120.0 61.75 500000.0 6849189.584093224 +2616 4326 500000.0 6849189.584093224 120.00101149853334 61.750787843054894 +4284 2616 118.65 51.058 405351.5891142243 5659504.924788193 +2616 4326 405351.5891142243 5659504.924788193 118.6507834706856 51.058563313398224 +4284 2616 121.35 72.442 545471.4045093557 8042101.79342143 +2616 4326 545471.4045093557 8042101.79342143 121.35153831122658 72.4429817810191 +4284 2617 123.0 63.59 500000.0 7054272.683313027 +2617 4326 500000.0 7054272.683313027 123.00120448163428 63.59080665111138 +4284 2617 121.65 54.221 411943.37376559194 6011461.978227262 +2617 4326 411943.37376559194 6011461.978227262 121.65093166581173 54.22161953389662 +4284 2617 124.35 72.959 544173.5501784316 8099783.769696419 +2617 4326 544173.5501784316 8099783.769696419 124.35179516220855 72.95996955551738 +4284 2618 126.0 61.94 500000.0 6870364.117988872 +2618 4326 500000.0 6870364.117988872 126.00128773905809 61.940752961897786 +4284 2618 124.65 51.086000000000006 405408.69417232287 5662619.787043772 +2618 4326 405408.69417232287 5662619.787043772 124.65098806185799 51.0865331434005 +4284 2618 127.35 72.794 544588.1614871816 8081374.322282203 +2618 4326 544588.1614871816 8081374.322282203 127.35199327592434 72.79494240265178 +4284 2619 129.0 58.13 500000.0 6445874.681044001 +2619 4326 500000.0 6445874.681044001 129.0012933855904 58.130652866937346 +4284 2619 127.65 44.216 392113.14502076776 4898795.928046418 +2619 4326 392113.14502076776 4898795.928046418 127.65098890826958 44.21635979322098 +4284 2619 130.35 72.044 546467.9675996465 7997698.656164544 +2619 4326 546467.9675996465 7997698.656164544 130.3521217371059 72.04490226719803 +4284 2620 132.0 57.12 500000.0 6333388.0687238285 +2620 4326 500000.0 6333388.0687238285 132.00137624173857 57.12060692303777 +4284 2620 130.65 43.737 391242.21138428024 4845571.915807717 +2620 4326 391242.21138428024 4845571.915807717 130.65106714714472 43.737329730011666 +4284 2620 133.35 70.50299999999999 550304.5822889181 7825792.539013257 +2620 4326 550304.5822889181 7825792.539013257 133.35215769686036 70.50384568401239 +4284 2621 135.0 59.32000000000001 500000.0 6578431.409257122 +2621 4326 500000.0 6578431.409257122 135.00156203037548 59.320622507215326 +4284 2621 133.65 44.398 392446.0552769603 4919019.93990623 +2621 4326 392446.0552769603 4919019.93990623 133.65115816923324 44.39832294867807 +4284 2621 136.35 74.242 540937.1916987415 8242940.659761529 +2621 4326 540937.1916987415 8242940.659761529 136.35280994218297 74.24287448296927 +4284 2622 138.0 60.515 500000.0 6711569.630751212 +2622 4326 500000.0 6711569.630751212 138.0017249720011 60.51561403404302 +4284 2622 136.65 46.335499999999996 396057.10010107514 5134354.665550572 +2622 4326 396057.10010107514 5134354.665550572 136.6512710746778 46.3358405689911 +4284 2622 139.35 74.69449999999999 539790.698132045 8293434.3976194095 +2622 4326 539790.698132045 8293434.3976194095 139.35309678639024 74.69534499483011 +4284 2623 141.0 61.035000000000004 500000.0 6769511.786851006 +2623 4326 500000.0 6769511.786851006 141.00186193927297 61.03558939964067 +4284 2623 139.65 47.359500000000004 398014.19509623817 5248190.514654343 +2623 4326 398014.19509623817 5248190.514654343 139.6513714801484 47.35983480126918 +4284 2623 142.35 74.7105 539750.1124903485 8295219.847347124 +2623 4326 539750.1124903485 8295219.847347124 142.3533047666016 74.71130571587523 +4284 2624 144.0 59.795 500000.0 6631349.546819799 +2624 4326 500000.0 6631349.546819799 144.0019062274426 59.79552980845937 +4284 2624 142.65 45.2285 393978.9951084814 5011313.64329941 +2624 4326 393978.9951084814 5011313.64329941 142.65140507694636 45.22876187827467 +4284 2624 145.35 74.3615 540634.6654693343 8256275.278797705 +2624 4326 540634.6654693343 8256275.278797705 145.35343025611584 74.36225812766193 +4284 2625 147.0 60.18000000000001 500000.0 6674243.9161159545 +2625 4326 500000.0 6674243.9161159545 147.0020264527326 60.180497812633845 +4284 2625 145.65 45.258 394033.8604869064 5014592.220913456 +2625 4326 394033.8604869064 5014592.220913456 145.6514775974733 45.25823185089946 +4284 2625 148.35 75.102 538756.0577916622 8338908.319397172 +2625 4326 538756.0577916622 8338908.319397172 148.35377761294018 75.1027245970674 +4284 2626 150.0 61.015 500000.0 6767283.158904388 +2626 4326 500000.0 6767283.158904388 150.00217146783544 61.01547098804115 +4284 2626 148.65 46.7905 396922.61716172076 5184933.63842068 +2626 4326 396922.61716172076 5184933.63842068 148.65158189164822 46.790728595177185 +4284 2626 151.35 75.23949999999999 538406.4933285617 8354252.628865748 +2626 4326 538406.4933285617 8354252.628865748 151.3539942411036 75.24017929013634 +4284 2627 153.0 61.49 500000.0 6820214.864260193 +2627 4326 500000.0 6820214.864260193 153.00229537262493 61.49043511703583 +4284 2627 151.65 48.197 399639.2686381557 5341307.916347078 +2627 4326 399639.2686381557 5341307.916347078 151.65168621679993 48.19721910046296 +4284 2627 154.35 74.783 539566.1693302606 8303310.196740806 +2627 4326 539566.1693302606 8303310.196740806 154.35405284510074 74.78362293192991 +4284 2628 156.0 63.11 500000.0 7000767.5358309625 +2628 4326 500000.0 7000767.5358309625 156.00250281284855 63.11041530950698 +4284 2628 154.65 50.429 404074.80100888363 5589535.510239827 +2628 4326 404074.80100888363 5589535.510239827 154.6518200510588 50.42922046472141 +4284 2628 157.35 75.791 537002.1811373039 8415798.985367114 +2628 4326 537002.1811373039 8415798.985367114 157.35448714530295 75.79158555974833 +4284 2629 159.0 61.24 500000.0 6792355.606366323 +2629 4326 500000.0 6792355.606366323 159.00244960469269 61.24033689547714 +4284 2629 157.65 52.348 408005.9593885065 5803025.74679606 +2629 4326 408005.9593885065 5803025.74679606 157.65195335948587 52.3482117114869 +4284 2629 160.35 70.132 551222.8404755801 7784409.744017398 +2629 4326 551222.8404755801 7784409.744017398 160.35341904114128 70.13245021527631 +4284 2630 162.0 62.660000000000004 500000.0 6950609.774137258 +2630 4326 500000.0 6950609.774137258 162.00263472887644 62.660308924969314 +4284 2630 160.65 55.172000000000004 413979.21721046756 6117317.375717458 +2630 4326 413979.21721046756 6117317.375717458 160.65213797134916 55.1722133015478 +4284 2630 163.35 70.148 551183.2837940312 7786194.413216086 +2630 4326 551183.2837940312 7786194.413216086 163.353526355856 70.14839614926863 +4284 2631 165.0 62.254999999999995 500000.0 6905470.561343054 +2631 4326 500000.0 6905470.561343054 165.00267478561778 62.255250754743635 +4284 2631 163.65 55.4465 414571.329647756 6147874.7795773605 +2631 4326 414571.329647756 6147874.7795773605 163.65221240297404 55.44667011267831 +4284 2631 166.35 69.06349999999999 553855.1444104797 7665234.286115813 +2631 4326 553855.1444104797 7665234.286115813 166.3534537530269 69.0638248706224 +4284 2632 168.0 62.26 500000.0 6906027.819449784 +2632 4326 500000.0 6906027.819449784 168.00274078835366 62.26019731639685 +4284 2632 166.65 55.231 414106.31572638184 6123885.159243279 +2632 4326 414106.31572638184 6123885.159243279 166.65225722346975 55.2311177724962 +4284 2632 169.35 69.28899999999999 553301.1666777187 7690384.335419543 +2632 4326 553301.1666777187 7690384.335419543 169.35357022782082 69.28927080631968 +4284 2633 171.0 65.025 500000.0 7214252.097012479 +2633 4326 500000.0 7214252.097012479 171.0030625482431 65.02518057241727 +4284 2633 169.65 60.3765 425529.8604093439 6696900.434513191 +2633 4326 425529.8604093439 6696900.434513191 169.65262567578498 60.37664067217935 +4284 2633 172.35 69.67349999999999 552354.6497279132 7733269.117284499 +2633 4326 552354.6497279132 7733269.117284499 172.35370581692547 69.67371729373234 +4284 2634 174.0 65.505 500000.0 7267771.349342988 +2634 4326 500000.0 7267771.349342988 174.00317277325598 65.50512942938381 +4284 2634 172.65 61.441500000000005 427973.33410805056 6815555.439263968 +2634 4326 427973.33410805056 6815555.439263968 172.652760273871 61.44160053942982 +4284 2634 175.35 69.5685 552613.3659760265 7721557.879740137 +2634 4326 552613.3659760265 7721557.879740137 175.3537539422985 69.56865591610733 +4284 2635 177.0 66.595 500000.0 7389317.258583589 +2635 4326 500000.0 7389317.258583589 177.00335412632597 66.5950845230114 +4284 2635 175.65 62.5405 430521.24033900944 6938017.003109125 +2635 4326 430521.24033900944 6938017.003109125 175.65290133801645 62.54055854184846 +4284 2635 178.35 70.64949999999999 549941.3892167619 7842134.168207353 +2635 4326 549941.3892167619 7842134.168207353 178.35400151574862 70.64960842541734 +4284 2636 180.0 66.94500000000001 500000.0 7428349.400943698 +2636 4326 500000.0 7428349.400943698 -179.99655651928995 66.94502853230013 +4284 2636 178.65 62.7105 430917.70371809055 6956961.751694089 +2636 4326 430917.70371809055 6956961.751694089 178.65295774063134 62.71050273615956 +4284 2636 -178.65 71.1795 548624.6942458189 7901256.126737621 +2636 4326 548624.6942458189 7901256.126737621 -178.6458464852022 71.1795525029407 +4284 2637 -177.0 68.175 500000.0 7565533.128007671 +2637 4326 500000.0 7565533.128007671 -176.99634694660293 68.17498191105814 +4284 2637 -178.35 65.0835 436514.64138310903 7221452.919002606 +2637 4326 436514.64138310903 7221452.919002606 -178.34676425848326 65.08347243671847 +4284 2637 -175.65 71.2665 548408.1504137059 7910961.358646199 +2637 4326 548408.1504137059 7910961.358646199 -175.6457898722434 71.26649023247916 +4284 2638 -174.0 65.99000000000001 500000.0 7321851.563124144 +2638 4326 500000.0 7321851.563124144 -173.9966175923129 65.98989446527256 +4284 2638 -175.35 64.379 434841.0450739842 7142921.782737626 +2638 4326 434841.0450739842 7142921.782737626 -175.34681411741295 64.37890351968424 +4284 2638 -172.65 67.601 557427.062348739 7502136.985763806 +2638 4326 557427.062348739 7502136.985763806 -172.6463936551253 67.60088474739696 +4284 2639 -171.035 65.705 498392.4724482402 7290072.496315905 +2639 4326 498392.4724482402 7290072.496315905 -171.0316358810246 65.70483015817929 +4284 2639 -172.3535 64.4855 434925.1528512238 7154796.54233314 +2639 4326 434925.1528512238 7154796.54233314 -172.35028298963678 64.48534381714241 +4284 2639 -169.7165 66.9245 556157.3633137962 7426641.888505799 +2639 4326 556157.3633137962 7426641.888505799 -169.71297437542137 66.92431602515701 +4284 2640 -169.095 65.78 449855.09013895085 7298871.971787686 +2640 4326 449855.09013895085 7298871.971787686 -169.09162038459957 65.7797908401432 +4284 2640 -169.2075 65.708 444549.43762920203 7290939.162325591 +2640 4326 444549.43762920203 7290939.162325591 -169.20412946638166 65.70779240415932 +4284 2640 -168.9825 65.852 455132.19876915094 7306814.480001371 +2640 4326 455132.19876915094 7306814.480001371 -168.97911125872196 65.85178927369687 +4200 2641 21.035 54.82 7502249.8476194255 6077299.477121525 +2641 4326 7502249.8476194255 6077299.477121525 21.032998945894164 54.81977807105466 +4200 2641 19.7165 54.37 7416582.472050618 6027965.31747827 +2641 4326 7416582.472050618 6027965.31747827 19.714513269945048 54.369750453011086 +4200 2641 22.3535 55.27 7586032.074357969 6128230.922690229 +2641 4326 7586032.074357969 6128230.922690229 22.351485262164175 55.26980584543191 +4200 2642 22.685000000000002 54.705 8415232.496494982 6065291.003988076 +2642 4326 8415232.496494982 6065291.003988076 22.68301633690293 54.704804219742414 +4200 2642 22.5185 54.3765 8403730.077294935 6028941.189715643 +2642 4326 8403730.077294935 6028941.189715643 22.516531255551147 54.37629728030351 +4200 2642 22.8515 55.033500000000004 8426564.455311611 6101669.789053393 +2642 4326 8426564.455311611 6101669.789053393 22.849501124682874 55.03331118332861 +4200 2643 27.560000000000002 62.49 9528870.89073428 6931787.26696081 +2643 4326 9528870.89073428 6931787.26696081 27.557565752804404 62.48999121656011 +4200 2643 26.705 56.693999999999996 9481925.053050887 6285987.560729266 +2643 4326 9481925.053050887 6285987.560729266 26.702948237471727 56.69389756704355 +4200 2643 28.415000000000003 68.286 9558442.937149255 7578584.647363143 +2643 4326 9558442.937149255 7578584.647363143 28.411967093384295 68.2860857035241 +4200 2644 30.0 61.349999999999994 10500000.0 6804613.552681751 +2644 4326 10500000.0 6804613.552681751 29.997689144804415 61.350018061459316 +4200 2644 28.65 53.7 10410838.404840672 5953476.4157073535 +2644 4326 10410838.404840672 5953476.4157073535 28.64811933478087 53.69988834019822 +4200 2644 31.35 69.0 10554010.990036933 7658152.2349943165 +2644 4326 10554010.990036933 7658152.2349943165 31.346924104289883 69.00014927030618 +4200 2645 33.0 60.629999999999995 11500000.0 6724383.371553015 +2645 4326 11500000.0 6724383.371553015 32.997787105699665 60.63005885628091 +4200 2645 31.65 52.179 11407655.50780267 5784221.70457315 +2645 4326 11407655.50780267 5784221.70457315 31.6482195118281 52.17891383420998 +4200 2645 34.35 69.08099999999999 11553812.18300902 7667186.041281402 +2645 4326 11553812.18300902 7667186.041281402 34.34697884842499 69.08120442288146 +4200 2646 36.004999999999995 57.0 12500303.865968525 6320024.540320983 +2646 4326 12500303.865968525 6320024.540320983 36.00305707545824 57.00005374823122 +4200 2646 34.6505 45.849 12395177.802638605 5080277.639473217 +2646 4326 12395177.802638605 5080277.639473217 34.64897075467502 45.84886369531828 +4200 2646 37.3595 68.151 12556482.748916762 7563478.179615375 +2646 4326 12556482.748916762 7563478.179615375 37.35667473492259 68.15124375751037 +4200 2647 39.0 56.065 13500000.0 6215909.15389191 +2647 4326 13500000.0 6215909.15389191 38.99815580989609 56.06508571952529 +4200 2647 37.65 44.6035 13392823.263381291 4941856.028629727 +2647 4326 13392823.263381291 4941856.028629727 37.64854245070497 44.603384599015115 +4200 2647 40.35 67.5265 13557608.019334149 7493829.518324723 +2647 4326 13557608.019334149 7493829.518324723 40.34732733573633 67.52678527593744 +4200 2648 42.0 55.80499999999999 14500000.0 6186960.096571183 +2648 4326 14500000.0 6186960.096571183 41.99822379450004 55.80512649730738 +4200 2648 40.65 44.1635 14392017.316577181 4892962.192036058 +2648 4326 14392017.316577181 4892962.192036058 40.648595263926566 44.16341603839401 +4200 2648 43.35 67.44649999999999 14557802.225437904 7484908.829004266 +2648 4326 14557802.225437904 7484908.829004266 43.347420919171995 67.44683341392764 +4200 2649 45.0 61.345 15500000.0 6804056.368964202 +2649 4326 15500000.0 6804056.368964202 44.997983464028046 61.34526208941662 +4200 2649 43.65 43.8355 15391420.684888551 4856516.382781016 +2649 4326 15391420.684888551 4856516.382781016 43.648648545654545 43.835447659797396 +4200 2649 46.35 78.8545 15529141.595145045 8757726.118397158 +2649 4326 15529141.595145045 8757726.118397158 46.345030625003886 78.85505181043081 +4200 2650 48.0 61.05 16500000.0 6771183.262177025 +2650 4326 16500000.0 6771183.262177025 47.99807825241212 61.05030159807151 +4200 2650 46.65 43.175999999999995 16390231.906988624 4783241.863263335 +2650 4326 16390231.906988624 4783241.863263335 46.6487122070099 43.175971706288614 +4200 2650 49.35 78.92399999999999 16528962.19567539 8765484.070724573 +2650 4326 16528962.19567539 8765484.070724573 49.34519633136468 78.92460155639388 +4200 2651 51.0 61.79 17500000.0 6853647.331680207 +2651 4326 17500000.0 6853647.331680207 50.99811434293064 61.790356906153534 +4200 2651 49.65 44.303 17392272.147020917 4908463.373954848 +2651 4326 17392272.147020917 4908463.373954848 49.64874039371047 44.30302650000121 +4200 2651 52.35 79.27699999999999 17528050.337039724 8804888.289955985 +2651 4326 17528050.337039724 8804888.289955985 52.3452557983687 79.27765336903354 +4200 2652 54.0 65.965 18500000.0 7319063.838803409 +2652 4326 18500000.0 7319063.838803409 53.997908551983585 65.96546970066728 +4200 2652 52.65 52.0645 18407418.532363534 5771481.97861049 +2652 4326 18407418.532363534 5771481.97861049 52.648594545603935 52.06470530309628 +4200 2652 55.35 79.8655 18526527.749254394 8870582.464611532 +2652 4326 18526527.749254394 8870582.464611532 55.345224517520265 79.86620649210107 +4200 2653 57.0 66.21000000000001 19500000.0 7346383.93001327 +2653 4326 19500000.0 7346383.93001327 56.99799725282227 66.21051434221566 +4200 2653 55.65 52.098 19407487.82720737 5775209.297687181 +2653 4326 19407487.82720737 5775209.297687181 55.64866356893759 52.098241746392695 +4200 2653 58.35 80.322 19525344.705351148 8921543.041187154 +2653 4326 19525344.705351148 8921543.041187154 58.34526816764751 80.32275554377892 +4200 2654 60.0 66.19 20500000.0 7344153.685754918 +2654 4326 20500000.0 7344153.685754918 59.99811365814905 66.1905523392699 +4200 2654 58.65 52.042 20407372.008983627 5768978.56665066 +2654 4326 20407372.008983627 5768978.56665066 58.6487390578652 52.04227465373937 +4200 2654 61.35 80.338 20525303.210339304 8923329.196767924 +2654 4326 20525303.210339304 8923329.196767924 61.34554214503579 80.33879594288597 +4200 2655 63.0 66.4 21500000.0 7367571.539354253 +2655 4326 21500000.0 7367571.539354253 62.99821862937634 66.40059211712493 +4200 2655 61.65 52.567 21408461.296374556 5827393.875248381 +2655 4326 21408461.296374556 5827393.875248381 61.648801873449855 52.56731708774765 +4200 2655 64.35 80.23299999999999 21525575.484526504 8911607.579656515 +2655 4326 21525575.484526504 8911607.579656515 64.34588097839453 80.23383209013927 +4200 2656 66.0 67.78 22500000.0 7521475.909866676 +2656 4326 22500000.0 7521475.909866676 65.99824555235229 67.78065037654332 +4200 2656 64.65 55.657000000000004 22415026.73387843 6171308.571786339 +2656 4326 22415026.73387843 6171308.571786339 64.64879445842784 55.65740895090615 +4200 2656 67.35 79.90299999999999 22526430.629629467 8874768.661463732 +2656 4326 22526430.629629467 8874768.661463732 67.34630802193034 79.90386230159835 +4200 2657 69.0 65.96 23500000.0 7318506.295035432 +2657 4326 23500000.0 7318506.295035432 68.99850225644998 65.96064913620017 +4200 2657 67.65 55.961 23415686.464602932 6205152.5465141665 +2657 4326 23415686.464602932 6205152.5465141665 67.64887680006656 55.961444281223564 +4200 2657 70.35 75.95899999999999 23536573.69501819 8434548.00681435 +2657 4326 23536573.69501819 8434548.00681435 70.34756083497548 75.95983395209669 +4200 2658 72.0 63.5 24500000.0 7044240.192187758 +2658 4326 24500000.0 7044240.192187758 71.99875667936197 63.50063168532059 +4200 2658 70.65 54.437 24412403.64220688 6035503.4570619175 +2658 4326 24412403.64220688 6035503.4570619175 70.64901190892807 54.43744017236203 +4200 2658 73.35 72.56299999999999 24545167.984841775 8055601.576949048 +2658 4326 24545167.984841775 8055601.576949048 73.34821452153798 72.56380707155006 +4200 2659 75.0 66.59 25500000.0 7388759.669218529 +2659 4326 25500000.0 7388759.669218529 74.99873937124013 66.59071547236132 +4200 2659 73.65 54.782 25413141.39965034 6073904.735410867 +2659 4326 25413141.39965034 6073904.735410867 73.64909848584315 54.78247162828502 +4200 2659 76.35 78.398 25530318.855334803 8706770.065080138 +2659 4326 25530318.855334803 8706770.065080138 76.34760192025117 78.39892795552781 +4200 2660 78.0 66.26 26500000.0 7351959.566037701 +2660 4326 26500000.0 7351959.566037701 77.99889796937562 66.26073271022797 +4200 2660 76.65 52.967 26409296.442732714 5871904.082080564 +2660 4326 26409296.442732714 5871904.082080564 76.64923192668851 52.96745504715386 +4200 2660 79.35 79.553 26527336.62543219 8835697.846417997 +2660 4326 26527336.62543219 8835697.846417997 79.34765385859053 79.55396935469354 +4200 2661 81.0 65.86500000000001 27500000.0 7307913.032958216 +2661 4326 27500000.0 7307913.032958216 80.99905799418951 65.86574581048166 +4200 2661 79.65 52.2165 27407733.200820554 5788394.154990532 +2661 4326 27407733.200820554 5788394.154990532 79.64933982837648 52.21695784167105 +4200 2661 82.35 79.5135 27527438.808940224 8831288.476428032 +2661 4326 27527438.808940224 8831288.476428032 82.34798557099879 79.51448949106835 +4200 2662 84.0 63.57 28500000.0 7052043.229877064 +2662 4326 28500000.0 7052043.229877064 83.99927087499233 63.57071891935182 +4200 2662 82.65 50.979 28405190.59426698 5650716.63985008 +2662 4326 28405190.59426698 5650716.63985008 82.64945210679502 50.97944697210118 +4200 2662 85.35 76.161 28536058.068142373 8457091.800012317 +2662 4326 28536058.068142373 8457091.800012317 85.34872570445476 76.16195404419156 +4200 2663 87.0 63.114999999999995 29500000.0 7001324.862382083 +2663 4326 29500000.0 7001324.862382083 86.99941622946625 63.11572444615894 +4200 2663 85.65 50.4745 29404166.77063554 5594596.650102233 +2663 4326 29404166.77063554 5594596.650102233 85.64955253656555 50.474949429927726 +4200 2663 88.35 75.7555 29537092.68303198 8411837.17045296 +2663 4326 29537092.68303198 8411837.17045296 88.34900872018672 75.75646176531002 +4200 2664 90.0 65.36 30500000.0 7251603.712863622 +2664 4326 30500000.0 7251603.712863622 89.99951030793609 65.3607805211338 +4200 2664 88.65 51.032 30405298.583410114 5656612.565386054 +2664 4326 30405298.583410114 5656612.565386054 88.64964341621153 51.03247326131721 +4200 2664 91.35 79.688 30526987.29108325 8850767.923054414 +2664 4326 30526987.29108325 8850767.923054414 91.34896660800258 79.68903588411565 +4200 2665 93.0 65.71000000000001 31500000.0 7290629.573861116 +2665 4326 31500000.0 7290629.573861116 92.99965312273676 65.71079632945461 +4200 2665 91.65 51.714999999999996 31406697.50033778 5732596.67227399 +2665 4326 31406697.50033778 5732596.67227399 91.64973672907394 51.715497531091565 +4200 2665 94.35 79.705 31526943.28997574 8852665.644907387 +2665 4326 31526943.28997574 8852665.644907387 94.34931098869208 79.70604441854644 +4200 2666 96.0 65.53999999999999 32500000.0 7271673.929040066 +2666 4326 32500000.0 7271673.929040066 95.99980559945483 65.54079905332775 +4200 2666 94.65 51.311 32405868.400453772 5687650.444429479 +2666 4326 32405868.400453772 5687650.444429479 94.6498385055063 51.31149474029309 +4200 2666 97.35 79.76899999999999 32526777.617117506 8859810.02646717 +2666 4326 32526777.617117506 8859810.02646717 97.34965677158547 79.77005047922174 +4200 2667 99.0 65.345 33500000.0 7249931.216590782 +2667 4326 33500000.0 7249931.216590782 98.99995680327349 65.34579829658881 +4200 2667 97.65 51.3455 33405939.018448576 5691488.559383896 +2667 4326 33405939.018448576 5691488.559383896 97.64993811347583 51.345999311281105 +4200 2667 100.35 79.34450000000001 33527875.84858289 8812423.188445145 +2667 4326 33527875.84858289 8812423.188445145 100.35000918578663 79.34554578551518 +4200 2668 102.0 64.94 34500000.0 7204775.08913092 +2668 4326 34500000.0 7204775.08913092 102.00010545098752 64.94079041096502 +4200 2668 100.65 51.647 34406557.62006173 5725031.259833232 +2668 4326 34406557.62006173 5725031.259833232 100.65003815183584 51.64750739723527 +4200 2668 103.35 78.23299999999999 34530743.89294062 8688352.587361421 +2668 4326 34530743.89294062 8688352.587361421 103.35031774796191 78.23402699237691 +4200 2669 105.0 64.67 35500000.0 7174672.376113227 +2669 4326 35500000.0 7174672.376113227 105.00025055283302 64.67078220093609 +4200 2669 103.65 51.584 35406428.143638335 5718022.201994142 +2669 4326 35406428.143638335 5718022.201994142 103.65013869116548 51.58450462857853 +4200 2669 106.35 77.756 35531971.165515326 8635110.48047606 +2669 4326 35531971.165515326 8635110.48047606 106.35060049425347 77.75701485168281 +4200 2670 108.0 63.825 36500000.0 7080469.229135362 +2670 4326 36500000.0 7080469.229135362 108.0003849402514 63.82575960783388 +4200 2670 106.65 50.7075 36404638.690165125 5620514.724274666 +2670 4326 36404638.690165125 5620514.724274666 106.65023514624465 50.70798079871621 +4200 2670 109.35 76.9425 36534058.967837825 8544312.772368127 +2670 4326 36534058.967837825 8544312.772368127 109.35084000467891 76.94349403839327 +4200 2671 111.0 62.975 37500000.0 6985719.868823676 +2671 4326 37500000.0 6985719.868823676 111.00051057221933 62.9757339240016 +4200 2671 109.65 50.5235 37404265.883025005 5600047.15044294 +2671 4326 37404265.883025005 5600047.15044294 109.65033182449504 50.52397023325749 +4200 2671 112.35 75.4265 37537930.72489836 8375121.167485982 +2671 4326 37537930.72489836 8375121.167485982 112.35100209187365 75.42745831226547 +4200 2672 114.0 63.095 38500000.0 6999095.558554125 +2672 4326 38500000.0 6999095.558554125 114.0006475827362 63.0957248216685 +4200 2672 112.65 50.850500000000004 38404929.11151414 5636422.012520418 +2672 4326 38404929.11151414 5636422.012520418 112.65043120393891 50.85096875695348 +4200 2672 115.35 75.3395 38538152.12316031 8365412.235560505 +2672 4326 38538152.12316031 8365412.235560505 115.35123658532898 75.34044300195583 +4200 2673 117.0 61.97 39500000.0 6873707.519296429 +2673 4326 39500000.0 6873707.519296429 117.00075338706388 61.97068800256242 +4200 2673 115.65 50.756 39404737.12264642 5625909.81198809 +2673 4326 39404737.12264642 5625909.81198809 115.65052644757426 50.75645524279672 +4200 2673 118.35 73.184 39543607.57144355 8124888.018222148 +2673 4326 39543607.57144355 8124888.018222148 118.35129357709853 73.18488977084482 +4200 2674 120.0 61.75 40500000.0 6849189.584093224 +2674 4326 40500000.0 6849189.584093224 120.00087383564612 61.75066662691099 +4200 2674 118.65 51.058 40405351.58911423 5659504.924788193 +2674 4326 40405351.58911423 5659504.924788193 118.65062494090476 51.05844811537116 +4200 2674 121.35 72.442 40545471.40450936 8042101.79342143 +2674 4326 40545471.40450936 8042101.79342143 121.3514374160854 72.44285731683618 +4200 2675 123.0 63.59 41500000.0 7054272.683313027 +2675 4326 41500000.0 7054272.683313027 123.00105866080067 63.59068304066194 +4200 2675 121.65 54.221 41411943.373765595 6011461.978227262 +2675 4326 41411943.373765595 6011461.978227262 121.65076972451723 54.22150040608274 +4200 2675 124.35 72.959 41544173.55017843 8099783.769696419 +2675 4326 41544173.55017843 8099783.769696419 124.35167575822915 72.95984357388727 +4200 2676 126.0 61.94 42500000.0 6870364.117988872 +2676 4326 42500000.0 6870364.117988872 126.0011244657868 61.94062834413447 +4200 2676 124.65 51.086000000000006 42405408.69417232 5662619.787043772 +2676 4326 42405408.69417232 5662619.787043772 124.65081092999098 51.08641370384656 +4200 2676 127.35 72.794 42544588.161487184 8081374.322282203 +2676 4326 42544588.161487184 8081374.322282203 127.3518513938019 72.79481529919312 +4200 2677 129.0 58.13 43500000.0 6445874.681044001 +2677 4326 43500000.0 6445874.681044001 129.00111143256788 58.13052822812526 +4200 2677 127.65 44.216 43392113.14502077 4898795.928046418 +2677 4326 43392113.14502077 4898795.928046418 127.65079415397331 44.21624250375078 +4200 2677 130.35 72.044 43546467.967599645 7997698.656164544 +2677 4326 43546467.967599645 7997698.656164544 130.3519563421641 72.04477440691363 +4200 2678 132.0 57.12 44500000.0 6333388.0687238285 +2678 4326 44500000.0 6333388.0687238285 132.00118175373927 57.12048163037603 +4200 2678 130.65 43.737 44391242.21138428 4845571.915807717 +2678 4326 44391242.21138428 4845571.915807717 130.65086409031457 43.73721108673673 +4200 2678 133.35 70.50299999999999 44550304.58228892 7825792.539013257 +2678 4326 44550304.58228892 7825792.539013257 133.35196980578704 70.50371743967673 +4200 2679 135.0 59.32000000000001 45500000.0 6578431.409257122 +2679 4326 45500000.0 6578431.409257122 135.00135768135473 59.32049591625752 +4200 2679 133.65 44.398 45392446.05527696 4919019.93990623 +2679 4326 45392446.05527696 4919019.93990623 133.65094791758972 44.39820259439123 +4200 2679 136.35 74.242 45540937.191698745 8242940.659761529 +2679 4326 45540937.191698745 8242940.659761529 136.3526022891478 74.24274599431195 +4200 2680 138.0 60.515 46500000.0 6711569.630751212 +2680 4326 46500000.0 6711569.630751212 138.00150884676677 60.515486894578395 +4200 2680 136.65 46.335499999999996 46396057.100101076 5134354.665550572 +2680 4326 46396057.100101076 5134354.665550572 136.65105415420865 46.335718279264206 +4200 2680 139.35 74.69449999999999 46539790.698132046 8293434.3976194095 +2680 4326 46539790.698132046 8293434.3976194095 139.35286446477096 74.69521676520215 +4200 2681 141.0 61.035000000000004 47500000.0 6769511.786851006 +2681 4326 47500000.0 6769511.786851006 141.00163326085365 61.035462247247 +4200 2681 139.65 47.359500000000004 47398014.19509624 5248190.514654343 +2681 4326 47398014.19509624 5248190.514654343 139.65114683888666 47.359711466884164 +4200 2681 142.35 74.7105 47539750.11249035 8295219.847347124 +2681 4326 47539750.11249035 8295219.847347124 142.3530480165275 74.71117807922867 +4200 2682 144.0 59.795 48500000.0 6631349.546819799 +2682 4326 48500000.0 6631349.546819799 144.00166601006518 59.795403246092924 +4200 2682 142.65 45.2285 48393978.99510848 5011313.64329941 +2682 4326 48393978.99510848 5011313.64329941 142.65117200957587 45.228639026567116 +4200 2682 145.35 74.3615 48540634.66546933 8256275.278797705 +2682 4326 48540634.66546933 8256275.278797705 145.3531511040118 74.36213139688192 +4200 2683 147.0 60.18000000000001 49500000.0 6674243.9161159545 +2683 4326 49500000.0 6674243.9161159545 147.0017740109587 60.18037190966411 +4200 2683 145.65 45.258 49394033.86048691 5014592.220913456 +2683 4326 49394033.86048691 5014592.220913456 145.651236956298 45.25810891230877 +4200 2683 148.35 75.102 49538756.057791665 8338908.319397172 +2683 4326 49538756.057791665 8338908.319397172 148.35346997615162 75.1025991955537 +4200 2684 150.0 61.015 50500000.0 6767283.158904388 +2684 4326 50500000.0 6767283.158904388 150.00190587120736 61.01534607045187 +4200 2684 148.65 46.7905 50396922.61716172 5184933.63842068 +2684 4326 50396922.61716172 5184933.63842068 148.65133338288143 46.79060548647275 +4200 2684 151.35 75.23949999999999 50538406.493328564 8354252.628865748 +2684 4326 50538406.493328564 8354252.628865748 151.35366113537737 75.24005551173641 +4200 2685 153.0 61.49 51500000.0 6820214.864260193 +2685 4326 51500000.0 6820214.864260193 153.00201681039383 61.49031158061583 +4200 2685 151.65 48.197 51399639.26863816 5341307.916347078 +2685 4326 51399639.26863816 5341307.916347078 151.65142931823542 48.19709633772861 +4200 2685 154.35 74.783 51539566.16933026 8303310.196740806 +2685 4326 51539566.16933026 8303310.196740806 154.3537004554838 74.78350102921233 +4200 2686 156.0 63.11 52500000.0 7000767.5358309625 +2686 4326 52500000.0 7000767.5358309625 156.00220761990903 63.110293544123934 +4200 2686 154.65 50.429 52404074.80100888 5589535.510239827 +2686 4326 52404074.80100888 5589535.510239827 154.6515533718803 50.42909842640603 +4200 2686 157.35 75.791 52537002.1811373 8415798.985367114 +2686 4326 52537002.1811373 8415798.985367114 157.35409929166775 75.79146606838573 +4200 2687 159.0 61.24 53500000.0 6792355.606366323 +2687 4326 53500000.0 6792355.606366323 159.00214840990543 61.24021710926745 +4200 2687 157.65 52.348 53408005.95938851 5803025.74679606 +2687 4326 53408005.95938851 5803025.74679606 157.6516760534432 52.348090989507355 +4200 2687 160.35 70.132 53551222.84047558 7784409.744017398 +2687 4326 53551222.84047558 7784409.744017398 160.35306789100713 70.13233239703393 +4200 2688 162.0 62.660000000000004 54500000.0 6950609.774137258 +2688 4326 54500000.0 6950609.774137258 162.00231703103756 62.66019161639717 +4200 2688 160.65 55.172000000000004 54413979.217210464 6117317.375717458 +2688 4326 54413979.217210464 6117317.375717458 160.6518469799797 55.172094442010575 +4200 2688 163.35 70.148 54551183.28379403 7786194.413216086 +2688 4326 54551183.28379403 7786194.413216086 163.35315863286394 70.14828109955012 +4200 2689 165.0 62.254999999999995 55500000.0 6905470.561343054 +2689 4326 55500000.0 6905470.561343054 165.00234745894116 62.25513608122539 +4200 2689 163.65 55.4465 55414571.32964776 6147874.7795773605 +2689 4326 55414571.32964776 6147874.7795773605 163.65191198971428 55.44655352069689 +4200 2689 166.35 69.06349999999999 55553855.144410476 7665234.286115813 +2689 4326 55553855.144410476 7665234.286115813 166.35307959999483 69.06371267845 +4200 2690 168.0 62.26 56500000.0 6906027.819449784 +2690 4326 56500000.0 6906027.819449784 168.00240260781678 62.26008561459176 +4200 2690 166.65 55.231 56414106.315726385 6123885.159243279 +2690 4326 56414106.315726385 6123885.159243279 166.65194889679248 55.23100370855908 +4200 2690 169.35 69.28899999999999 56553301.16667772 7690384.335419543 +2690 4326 56553301.16667772 7690384.335419543 169.35317946951838 69.28916199223086 +4200 2691 171.0 65.025 57500000.0 7214252.097012479 +2691 4326 57500000.0 7214252.097012479 171.0026978299059 65.02507273340763 +4200 2691 169.65 60.3765 57425529.86040934 6696900.434513191 +2691 4326 57425529.86040934 6696900.434513191 169.65229051789748 60.37653036206056 +4200 2691 172.35 69.67349999999999 57552354.64972791 7733269.117284499 +2691 4326 57552354.64972791 7733269.117284499 172.35329699637893 69.67361219353786 +4200 2692 174.0 65.505 58500000.0 7267771.349342988 +2692 4326 58500000.0 7267771.349342988 174.0027933815762 65.50502534879931 +4200 2692 172.65 61.441500000000005 58427973.33410805 6815555.439263968 +2692 4326 58427973.33410805 6815555.439263968 172.65241047128427 61.441493812906174 +4200 2692 175.35 69.5685 58552613.36597603 7721557.879740137 +2692 4326 58552613.36597603 7721557.879740137 175.3533324688083 69.56855468869736 +4200 2693 177.0 66.595 59500000.0 7389317.258583589 +2693 4326 59500000.0 7389317.258583589 177.0029548439716 66.59498467981057 +4200 2693 175.65 62.5405 59430521.24033901 6938017.003109125 +2693 4326 59430521.24033901 6938017.003109125 175.65253586364912 62.540455766364175 +4200 2693 178.35 70.64949999999999 59549941.38921676 7842134.168207353 +2693 4326 59549941.38921676 7842134.168207353 178.35355356947372 70.64951169041663 +4200 2695 -177.0 68.175 61500000.0 7565533.128007671 +2695 4326 61500000.0 7565533.128007671 -176.99678382340008 68.17489129609088 +4200 2695 -178.35 65.0835 61436514.64138311 7221452.919002606 +2695 4326 61436514.64138311 7221452.919002606 -178.34716686204575 65.08337870479434 +4200 2695 -175.65 71.2665 61548408.15041371 7910961.358646199 +2695 4326 61548408.15041371 7910961.358646199 -175.646272644824 71.2664028414059 +4200 2696 -174.0 65.99000000000001 62500000.0 7321851.563124144 +2696 4326 62500000.0 7321851.563124144 -173.99704178763102 65.98980774046488 +4200 2696 -175.35 64.379 62434841.045073986 7142921.782737626 +2696 4326 62434841.045073986 7142921.782737626 -175.34722000598504 64.37881405517382 +4200 2696 -172.65 67.601 62557427.06234874 7502136.985763806 +2696 4326 62557427.06234874 7502136.985763806 -172.6468388030079 67.60080084484042 +4200 2697 -171.035 65.705 63498392.47244824 7290072.496315905 +2697 4326 63498392.47244824 7290072.496315905 -171.03206586421686 65.70474814320379 +4200 2697 -172.3535 64.4855 63434925.152851224 7154796.54233314 +2697 4326 63434925.152851224 7154796.54233314 -172.35069813134993 64.48525912889072 +4200 2697 -169.7165 66.9245 63556157.363313794 7426641.888505799 +2697 4326 63556157.363313794 7426641.888505799 -169.7134207321257 66.92423675113882 +4200 2698 -169.095 65.78 64449855.09013895 7298871.971787686 +2698 4326 64449855.09013895 7298871.971787686 -169.0920563241622 65.77971212353485 +4200 2698 -169.2075 65.708 64444549.4376292 7290939.162325591 +2698 4326 64444549.4376292 7290939.162325591 -169.2045643863727 65.70771346385851 +4200 2698 -168.9825 65.852 64455132.19876915 7306814.480001371 +2698 4326 64455132.19876915 7306814.480001371 -168.97954822339892 65.8517107812033 +4200 2699 21.035 54.82 502249.8476194251 6077299.477121525 +2699 4326 502249.8476194251 6077299.477121525 21.032998945894157 54.819778071054664 +4200 2699 19.7165 54.37 416582.47205061815 6027965.31747827 +2699 4326 416582.47205061815 6027965.31747827 19.71451326994504 54.369750453011086 +4200 2699 22.3535 55.27 586032.0743579689 6128230.922690229 +2699 4326 586032.0743579689 6128230.922690229 22.35148526216418 55.26980584543191 +4200 2700 22.685000000000002 54.705 415232.4964949828 6065291.003988076 +2700 4326 415232.4964949828 6065291.003988076 22.683016336902938 54.70480421974242 +4200 2700 22.5185 54.3765 403730.0772949341 6028941.189715643 +2700 4326 403730.0772949341 6028941.189715643 22.516531255551136 54.37629728030352 +4200 2700 22.8515 55.033500000000004 426564.4553116106 6101669.789053393 +2700 4326 426564.4553116106 6101669.789053393 22.84950112468286 55.03331118332861 +4200 2701 27.560000000000002 62.49 528870.890734279 6931787.26696081 +2701 4326 528870.890734279 6931787.26696081 27.557565752804393 62.4899912165601 +4200 2701 26.705 56.693999999999996 481925.0530508865 6285987.560729266 +2701 4326 481925.0530508865 6285987.560729266 26.702948237471713 56.69389756704355 +4200 2701 28.415000000000003 68.286 558442.937149255 7578584.647363143 +2701 4326 558442.937149255 7578584.647363143 28.4119670933843 68.2860857035241 +4200 2702 30.0 61.349999999999994 500000.0 6804613.552681751 +2702 4326 500000.0 6804613.552681751 29.997689144804415 61.350018061459316 +4200 2702 28.65 53.7 410838.40484067245 5953476.4157073535 +2702 4326 410838.40484067245 5953476.4157073535 28.64811933478087 53.69988834019822 +4200 2702 31.35 69.0 554010.9900369331 7658152.2349943165 +2702 4326 554010.9900369331 7658152.2349943165 31.346924104289883 69.00014927030618 +4200 2703 33.0 60.629999999999995 500000.0 6724383.371553015 +2703 4326 500000.0 6724383.371553015 32.997787105699665 60.63005885628091 +4200 2703 31.65 52.179 407655.5078026715 5784221.70457315 +2703 4326 407655.5078026715 5784221.70457315 31.648219511828106 52.17891383420997 +4200 2703 34.35 69.08099999999999 553812.1830090197 7667186.041281402 +2703 4326 553812.1830090197 7667186.041281402 34.346978848425 69.08120442288146 +4200 2704 36.004999999999995 57.0 500303.86596852564 6320024.540320983 +2704 4326 500303.86596852564 6320024.540320983 36.003057075458244 57.00005374823121 +4200 2704 34.6505 45.849 395177.8026386053 5080277.639473217 +2704 4326 395177.8026386053 5080277.639473217 34.64897075467502 45.84886369531828 +4200 2704 37.3595 68.151 556482.7489167613 7563478.179615375 +2704 4326 556482.7489167613 7563478.179615375 37.35667473492258 68.15124375751039 +4200 2705 39.0 56.065 500000.0 6215909.15389191 +2705 4326 500000.0 6215909.15389191 38.99815580989609 56.06508571952529 +4200 2705 37.65 44.6035 392823.26338129205 4941856.028629727 +2705 4326 392823.26338129205 4941856.028629727 37.64854245070499 44.60338459901512 +4200 2705 40.35 67.5265 557608.019334149 7493829.518324723 +2705 4326 557608.019334149 7493829.518324723 40.34732733573634 67.52678527593744 +4200 2706 42.0 55.80499999999999 500000.0 6186960.096571183 +2706 4326 500000.0 6186960.096571183 41.99822379450004 55.80512649730738 +4200 2706 40.65 44.1635 392017.31657718145 4892962.192036058 +2706 4326 392017.31657718145 4892962.192036058 40.648595263926566 44.16341603839401 +4200 2706 43.35 67.44649999999999 557802.2254379031 7484908.829004266 +2706 4326 557802.2254379031 7484908.829004266 43.347420919171974 67.44683341392764 +4200 2707 45.0 61.345 500000.0 6804056.368964202 +2707 4326 500000.0 6804056.368964202 44.997983464028046 61.34526208941662 +4200 2707 43.65 43.8355 391420.68488855136 4856516.382781016 +2707 4326 391420.68488855136 4856516.382781016 43.64864854565455 43.835447659797396 +4200 2707 46.35 78.8545 529141.5951450444 8757726.118397158 +2707 4326 529141.5951450444 8757726.118397158 46.34503062500387 78.85505181043081 +4200 2708 48.0 61.05 500000.0 6771183.262177025 +2708 4326 500000.0 6771183.262177025 47.99807825241212 61.05030159807151 +4200 2708 46.65 43.175999999999995 390231.9069886238 4783241.863263335 +2708 4326 390231.9069886238 4783241.863263335 46.6487122070099 43.175971706288614 +4200 2708 49.35 78.92399999999999 528962.1956753904 8765484.070724573 +2708 4326 528962.1956753904 8765484.070724573 49.3451963313647 78.92460155639388 +4200 2709 51.0 61.79 500000.0 6853647.331680207 +2709 4326 500000.0 6853647.331680207 50.99811434293064 61.790356906153534 +4200 2709 49.65 44.303 392272.14702091896 4908463.373954848 +2709 4326 392272.14702091896 4908463.373954848 49.64874039371049 44.30302650000121 +4200 2709 52.35 79.27699999999999 528050.3370397235 8804888.289955985 +2709 4326 528050.3370397235 8804888.289955985 52.34525579836867 79.27765336903354 +4200 2710 54.0 65.965 500000.0 7319063.838803409 +2710 4326 500000.0 7319063.838803409 53.997908551983585 65.96546970066728 +4200 2710 52.65 52.0645 407418.5323635341 5771481.97861049 +2710 4326 407418.5323635341 5771481.97861049 52.648594545603935 52.06470530309628 +4200 2710 55.35 79.8655 526527.7492543929 8870582.464611532 +2710 4326 526527.7492543929 8870582.464611532 55.34522451752019 79.86620649210107 +4200 2711 57.0 66.21000000000001 500000.0 7346383.93001327 +2711 4326 500000.0 7346383.93001327 56.99799725282227 66.21051434221566 +4200 2711 55.65 52.098 407487.82720737194 5775209.297687181 +2711 4326 407487.82720737194 5775209.297687181 55.64866356893759 52.098241746392695 +4200 2711 58.35 80.322 525344.7053511491 8921543.041187154 +2711 4326 525344.7053511491 8921543.041187154 58.345268167647575 80.32275554377892 +4200 2712 60.0 66.19 500000.0 7344153.685754918 +2712 4326 500000.0 7344153.685754918 59.99811365814905 66.1905523392699 +4200 2712 58.65 52.042 407372.00898362615 5768978.56665066 +2712 4326 407372.00898362615 5768978.56665066 58.648739057865185 52.04227465373937 +4200 2712 61.35 80.338 525303.210339303 8923329.196767924 +2712 4326 525303.210339303 8923329.196767924 61.34554214503574 80.33879594288597 +4200 2713 63.0 66.4 500000.0 7367571.539354253 +2713 4326 500000.0 7367571.539354253 62.99821862937634 66.40059211712493 +4200 2713 61.65 52.567 408461.2963745547 5827393.875248381 +2713 4326 408461.2963745547 5827393.875248381 61.64880187344983 52.567317087747654 +4200 2713 64.35 80.23299999999999 525575.4845265042 8911607.579656515 +2713 4326 525575.4845265042 8911607.579656515 64.34588097839453 80.23383209013927 +4200 2714 66.0 67.78 500000.0 7521475.909866676 +2714 4326 500000.0 7521475.909866676 65.99824555235229 67.78065037654332 +4200 2714 64.65 55.657000000000004 415026.7338784304 6171308.571786339 +2714 4326 415026.7338784304 6171308.571786339 64.64879445842786 55.65740895090616 +4200 2714 67.35 79.90299999999999 526430.6296294682 8874768.661463732 +2714 4326 526430.6296294682 8874768.661463732 67.34630802193043 79.90386230159835 +4200 2715 69.0 65.96 500000.0 7318506.295035432 +2715 4326 500000.0 7318506.295035432 68.99850225644998 65.96064913620017 +4200 2715 67.65 55.961 415686.4646029326 6205152.5465141665 +2715 4326 415686.4646029326 6205152.5465141665 67.64887680006656 55.961444281223564 +4200 2715 70.35 75.95899999999999 536573.6950181916 8434548.00681435 +2715 4326 536573.6950181916 8434548.00681435 70.34756083497551 75.95983395209669 +4200 2716 72.0 63.5 500000.0 7044240.192187758 +2716 4326 500000.0 7044240.192187758 71.99875667936197 63.50063168532059 +4200 2716 70.65 54.437 412403.64220688195 6035503.4570619175 +2716 4326 412403.64220688195 6035503.4570619175 70.64901190892809 54.43744017236204 +4200 2716 73.35 72.56299999999999 545167.9848417758 8055601.576949048 +2716 4326 545167.9848417758 8055601.576949048 73.348214521538 72.56380707155007 +4200 2717 75.0 66.59 500000.0 7388759.669218529 +2717 4326 500000.0 7388759.669218529 74.99873937124013 66.59071547236132 +4200 2717 73.65 54.782 413141.39965033793 6073904.735410867 +2717 4326 413141.39965033793 6073904.735410867 73.64909848584314 54.78247162828502 +4200 2717 76.35 78.398 530318.8553348031 8706770.065080138 +2717 4326 530318.8553348031 8706770.065080138 76.34760192025115 78.39892795552781 +4200 2718 78.0 66.26 500000.0 7351959.566037701 +2718 4326 500000.0 7351959.566037701 77.99889796937562 66.26073271022797 +4200 2718 76.65 52.967 409296.44273271563 5871904.082080564 +2718 4326 409296.44273271563 5871904.082080564 76.64923192668854 52.96745504715387 +4200 2718 79.35 79.553 527336.6254321879 8835697.846417997 +2718 4326 527336.6254321879 8835697.846417997 79.34765385859045 79.55396935469355 +4200 2719 81.0 65.86500000000001 500000.0 7307913.032958216 +2719 4326 500000.0 7307913.032958216 80.99905799418951 65.86574581048166 +4200 2719 79.65 52.2165 407733.2008205557 5788394.154990532 +2719 4326 407733.2008205557 5788394.154990532 79.64933982837651 52.21695784167105 +4200 2719 82.35 79.5135 527438.8089402226 8831288.476428032 +2719 4326 527438.8089402226 8831288.476428032 82.3479855709987 79.51448949106835 +4200 2720 84.0 63.57 500000.0 7052043.229877064 +2720 4326 500000.0 7052043.229877064 83.99927087499233 63.57071891935182 +4200 2720 82.65 50.979 405190.59426698246 5650716.63985008 +2720 4326 405190.59426698246 5650716.63985008 82.64945210679505 50.97944697210117 +4200 2720 85.35 76.161 536058.0681423742 8457091.800012317 +2720 4326 536058.0681423742 8457091.800012317 85.34872570445481 76.16195404419156 +4200 2721 87.0 63.114999999999995 500000.0 7001324.862382083 +2721 4326 500000.0 7001324.862382083 86.99941622946625 63.11572444615894 +4200 2721 85.65 50.4745 404166.7706355422 5594596.650102233 +2721 4326 404166.7706355422 5594596.650102233 85.64955253656557 50.474949429927726 +4200 2721 88.35 75.7555 537092.6830319815 8411837.17045296 +2721 4326 537092.6830319815 8411837.17045296 88.34900872018677 75.75646176531002 +4200 2722 90.0 65.36 500000.0 7251603.712863622 +2722 4326 500000.0 7251603.712863622 89.99951030793609 65.3607805211338 +4200 2722 88.65 51.032 405298.58341011347 5656612.565386054 +2722 4326 405298.58341011347 5656612.565386054 88.64964341621152 51.03247326131721 +4200 2722 91.35 79.688 526987.2910832507 8850767.923054414 +2722 4326 526987.2910832507 8850767.923054414 91.34896660800261 79.68903588411565 +4200 2723 93.0 65.71000000000001 500000.0 7290629.573861116 +2723 4326 500000.0 7290629.573861116 92.99965312273676 65.71079632945461 +4200 2723 91.65 51.714999999999996 406697.5003377808 5732596.67227399 +2723 4326 406697.5003377808 5732596.67227399 91.64973672907394 51.715497531091565 +4200 2723 94.35 79.705 526943.289975741 8852665.644907387 +2723 4326 526943.289975741 8852665.644907387 94.34931098869212 79.70604441854644 +4200 2724 96.0 65.53999999999999 500000.0 7271673.929040066 +2724 4326 500000.0 7271673.929040066 95.99980559945483 65.54079905332775 +4200 2724 94.65 51.311 405868.40045377356 5687650.444429479 +2724 4326 405868.40045377356 5687650.444429479 94.64983850550631 51.31149474029309 +4200 2724 97.35 79.76899999999999 526777.6171175041 8859810.02646717 +2724 4326 526777.6171175041 8859810.02646717 97.3496567715854 79.77005047922174 +4200 2725 99.0 65.345 500000.0 7249931.216590782 +2725 4326 500000.0 7249931.216590782 98.99995680327349 65.34579829658881 +4200 2725 97.65 51.3455 405939.01844857686 5691488.559383896 +2725 4326 405939.01844857686 5691488.559383896 97.64993811347584 51.345999311281105 +4200 2725 100.35 79.34450000000001 527875.8485828904 8812423.188445145 +2725 4326 527875.8485828904 8812423.188445145 100.35000918578666 79.34554578551518 +4200 2726 102.0 64.94 500000.0 7204775.08913092 +2726 4326 500000.0 7204775.08913092 102.00010545098752 64.94079041096502 +4200 2726 100.65 51.647 406557.6200617314 5725031.259833232 +2726 4326 406557.6200617314 5725031.259833232 100.65003815183582 51.647507397235266 +4200 2726 103.35 78.23299999999999 530743.8929406154 8688352.587361421 +2726 4326 530743.8929406154 8688352.587361421 103.3503177479618 78.23402699237691 +4200 2727 105.0 64.67 500000.0 7174672.376113227 +2727 4326 500000.0 7174672.376113227 105.00025055283302 64.67078220093609 +4200 2727 103.65 51.584 406428.14363833517 5718022.201994142 +2727 4326 406428.14363833517 5718022.201994142 103.65013869116548 51.58450462857853 +4200 2727 106.35 77.756 531971.1655153282 8635110.48047606 +2727 4326 531971.1655153282 8635110.48047606 106.35060049425356 77.75701485168281 +4200 2728 108.0 63.825 500000.0 7080469.229135362 +2728 4326 500000.0 7080469.229135362 108.0003849402514 63.82575960783388 +4200 2728 106.65 50.7075 404638.69016512326 5620514.724274666 +2728 4326 404638.69016512326 5620514.724274666 106.65023514624464 50.707980798716214 +4200 2728 109.35 76.9425 534058.9678378225 8544312.772368127 +2728 4326 534058.9678378225 8544312.772368127 109.35084000467879 76.94349403839327 +4200 2729 111.0 62.975 500000.0 6985719.868823676 +2729 4326 500000.0 6985719.868823676 111.00051057221933 62.9757339240016 +4200 2729 109.65 50.5235 404265.88302500197 5600047.15044294 +2729 4326 404265.88302500197 5600047.15044294 109.65033182449498 50.52397023325749 +4200 2729 112.35 75.4265 537930.7248983607 8375121.167485982 +2729 4326 537930.7248983607 8375121.167485982 112.35100209187365 75.42745831226547 +4200 2730 114.0 63.095 500000.0 6999095.558554125 +2730 4326 500000.0 6999095.558554125 114.0006475827362 63.0957248216685 +4200 2730 112.65 50.850500000000004 404929.1115141405 5636422.012520418 +2730 4326 404929.1115141405 5636422.012520418 112.65043120393888 50.85096875695348 +4200 2730 115.35 75.3395 538152.1231603118 8365412.235560505 +2730 4326 538152.1231603118 8365412.235560505 115.35123658532903 75.34044300195583 +4200 2731 117.0 61.97 500000.0 6873707.519296429 +2731 4326 500000.0 6873707.519296429 117.00075338706388 61.97068800256242 +4200 2731 115.65 50.756 404737.12264641805 5625909.81198809 +2731 4326 404737.12264641805 5625909.81198809 115.65052644757422 50.756455242796726 +4200 2731 118.35 73.184 543607.5714435482 8124888.018222148 +2731 4326 543607.5714435482 8124888.018222148 118.35129357709849 73.18488977084482 +4200 2732 120.0 61.75 500000.0 6849189.584093224 +2732 4326 500000.0 6849189.584093224 120.00087383564612 61.75066662691099 +4200 2732 118.65 51.058 405351.5891142243 5659504.924788193 +2732 4326 405351.5891142243 5659504.924788193 118.65062494090473 51.05844811537116 +4200 2732 121.35 72.442 545471.4045093557 8042101.79342143 +2732 4326 545471.4045093557 8042101.79342143 121.35143741608529 72.4428573168362 +4200 2733 123.0 63.59 500000.0 7054272.683313027 +2733 4326 500000.0 7054272.683313027 123.00105866080067 63.59068304066194 +4200 2733 121.65 54.221 411943.37376559194 6011461.978227262 +2733 4326 411943.37376559194 6011461.978227262 121.65076972451718 54.22150040608274 +4200 2733 124.35 72.959 544173.5501784316 8099783.769696419 +2733 4326 544173.5501784316 8099783.769696419 124.35167575822918 72.95984357388727 +4200 2734 126.0 61.94 500000.0 6870364.117988872 +2734 4326 500000.0 6870364.117988872 126.0011244657868 61.94062834413447 +4200 2734 124.65 51.086000000000006 405408.69417232287 5662619.787043772 +2734 4326 405408.69417232287 5662619.787043772 124.65081092999098 51.08641370384656 +4200 2734 127.35 72.794 544588.1614871816 8081374.322282203 +2734 4326 544588.1614871816 8081374.322282203 127.35185139380181 72.79481529919312 +4200 2735 129.0 58.13 500000.0 6445874.681044001 +2735 4326 500000.0 6445874.681044001 129.00111143256788 58.13052822812526 +4200 2735 127.65 44.216 392113.14502076776 4898795.928046418 +2735 4326 392113.14502076776 4898795.928046418 127.65079415397331 44.21624250375078 +4200 2735 130.35 72.044 546467.9675996465 7997698.656164544 +2735 4326 546467.9675996465 7997698.656164544 130.35195634216413 72.04477440691363 +4127 2736 33.105000000000004 -19.14 511042.13268459094 7883805.06889301 +2736 4326 511042.13268459094 7883805.06889301 33.10461908814661 -19.140978171691334 +4127 2736 30.4995 -26.097 249908.13181886537 7111335.420054686 +2736 4326 249908.13181886537 7111335.420054686 30.499044550810506 -26.097630191000512 +4127 2736 35.710499999999996 -12.183000000000002 794966.6488990097 8651821.061249334 +2736 4326 794966.6488990097 8651821.061249334 35.71018296299838 -12.184365333633904 +4127 2737 38.445 -14.7 440249.36855046125 8374881.389085944 +2737 4326 440249.36855046125 8374881.389085944 38.444734633601826 -14.701227020021747 +4127 2737 36.2355 -18.552 208171.65960358642 7946623.601806333 +2737 4326 208171.65960358642 7946623.601806333 36.23518389705009 -18.553015030659957 +4127 2737 40.6545 -10.848 680861.6286104177 8800410.67071036 +2737 4326 680861.6286104177 8800410.67071036 40.654282833342904 -10.849445870336407 +4200 2738 132.0 57.12 500000.0 6333388.0687238285 +2738 4326 500000.0 6333388.0687238285 132.00118175373927 57.12048163037603 +4200 2738 130.65 43.737 391242.21138428024 4845571.915807717 +2738 4326 391242.21138428024 4845571.915807717 130.65086409031454 43.73721108673673 +4200 2738 133.35 70.50299999999999 550304.5822889181 7825792.539013257 +2738 4326 550304.5822889181 7825792.539013257 133.35196980578695 70.50371743967673 +4200 2739 135.0 59.32000000000001 500000.0 6578431.409257122 +2739 4326 500000.0 6578431.409257122 135.00135768135473 59.32049591625752 +4200 2739 133.65 44.398 392446.0552769603 4919019.93990623 +2739 4326 392446.0552769603 4919019.93990623 133.65094791758972 44.39820259439123 +4200 2739 136.35 74.242 540937.1916987415 8242940.659761529 +2739 4326 540937.1916987415 8242940.659761529 136.35260228914768 74.24274599431195 +4200 2740 138.0 60.515 500000.0 6711569.630751212 +2740 4326 500000.0 6711569.630751212 138.00150884676677 60.515486894578395 +4200 2740 136.65 46.335499999999996 396057.10010107514 5134354.665550572 +2740 4326 396057.10010107514 5134354.665550572 136.65105415420865 46.335718279264206 +4200 2740 139.35 74.69449999999999 539790.698132045 8293434.3976194095 +2740 4326 539790.698132045 8293434.3976194095 139.35286446477093 74.69521676520215 +4200 2741 141.0 61.035000000000004 500000.0 6769511.786851006 +2741 4326 500000.0 6769511.786851006 141.00163326085365 61.035462247247 +4200 2741 139.65 47.359500000000004 398014.19509623817 5248190.514654343 +2741 4326 398014.19509623817 5248190.514654343 139.65114683888663 47.359711466884164 +4200 2741 142.35 74.7105 539750.1124903485 8295219.847347124 +2741 4326 539750.1124903485 8295219.847347124 142.3530480165275 74.71117807922867 +4200 2742 144.0 59.795 500000.0 6631349.546819799 +2742 4326 500000.0 6631349.546819799 144.00166601006518 59.795403246092924 +4200 2742 142.65 45.2285 393978.9951084814 5011313.64329941 +2742 4326 393978.9951084814 5011313.64329941 142.6511720095759 45.22863902656712 +4200 2742 145.35 74.3615 540634.6654693343 8256275.278797705 +2742 4326 540634.6654693343 8256275.278797705 145.35315110401183 74.36213139688192 +4200 2743 147.0 60.18000000000001 500000.0 6674243.9161159545 +2743 4326 500000.0 6674243.9161159545 147.0017740109587 60.18037190966411 +4200 2743 145.65 45.258 394033.8604869064 5014592.220913456 +2743 4326 394033.8604869064 5014592.220913456 145.65123695629794 45.25810891230877 +4200 2743 148.35 75.102 538756.0577916622 8338908.319397172 +2743 4326 538756.0577916622 8338908.319397172 148.3534699761515 75.10259919555371 +4200 2744 150.0 61.015 500000.0 6767283.158904388 +2744 4326 500000.0 6767283.158904388 150.00190587120736 61.01534607045187 +4200 2744 148.65 46.7905 396922.61716172076 5184933.63842068 +2744 4326 396922.61716172076 5184933.63842068 148.65133338288143 46.79060548647275 +4200 2744 151.35 75.23949999999999 538406.4933285617 8354252.628865748 +2744 4326 538406.4933285617 8354252.628865748 151.35366113537728 75.24005551173641 +4200 2745 153.0 61.49 500000.0 6820214.864260193 +2745 4326 500000.0 6820214.864260193 153.00201681039383 61.49031158061583 +4200 2745 151.65 48.197 399639.2686381557 5341307.916347078 +2745 4326 399639.2686381557 5341307.916347078 151.65142931823542 48.19709633772861 +4200 2745 154.35 74.783 539566.1693302606 8303310.196740806 +2745 4326 539566.1693302606 8303310.196740806 154.35370045548376 74.78350102921233 +4200 2746 156.0 63.11 500000.0 7000767.5358309625 +2746 4326 500000.0 7000767.5358309625 156.00220761990903 63.110293544123934 +4200 2746 154.65 50.429 404074.80100888363 5589535.510239827 +2746 4326 404074.80100888363 5589535.510239827 154.65155337188037 50.42909842640603 +4200 2746 157.35 75.791 537002.1811373039 8415798.985367114 +2746 4326 537002.1811373039 8415798.985367114 157.35409929166786 75.79146606838573 +4200 2747 159.0 61.24 500000.0 6792355.606366323 +2747 4326 500000.0 6792355.606366323 159.00214840990543 61.24021710926745 +4200 2747 157.65 52.348 408005.9593885065 5803025.74679606 +2747 4326 408005.9593885065 5803025.74679606 157.65167605344314 52.348090989507355 +4200 2747 160.35 70.132 551222.8404755801 7784409.744017398 +2747 4326 551222.8404755801 7784409.744017398 160.35306789100707 70.13233239703393 +4200 2748 162.0 62.660000000000004 500000.0 6950609.774137258 +2748 4326 500000.0 6950609.774137258 162.00231703103756 62.66019161639717 +4200 2748 160.65 55.172000000000004 413979.21721046756 6117317.375717458 +2748 4326 413979.21721046756 6117317.375717458 160.65184697997975 55.172094442010575 +4200 2748 163.35 70.148 551183.2837940312 7786194.413216086 +2748 4326 551183.2837940312 7786194.413216086 163.35315863286397 70.14828109955012 +4200 2749 165.0 62.254999999999995 500000.0 6905470.561343054 +2749 4326 500000.0 6905470.561343054 165.00234745894116 62.25513608122539 +4200 2749 163.65 55.4465 414571.329647756 6147874.7795773605 +2749 4326 414571.329647756 6147874.7795773605 163.65191198971428 55.44655352069689 +4200 2749 166.35 69.06349999999999 553855.1444104797 7665234.286115813 +2749 4326 553855.1444104797 7665234.286115813 166.3530795999949 69.06371267845 +4200 2750 168.0 62.26 500000.0 6906027.819449784 +2750 4326 500000.0 6906027.819449784 168.00240260781678 62.26008561459176 +4200 2750 166.65 55.231 414106.31572638184 6123885.159243279 +2750 4326 414106.31572638184 6123885.159243279 166.65194889679245 55.23100370855908 +4200 2750 169.35 69.28899999999999 553301.1666777187 7690384.335419543 +2750 4326 553301.1666777187 7690384.335419543 169.35317946951835 69.28916199223087 +4200 2751 171.0 65.025 500000.0 7214252.097012479 +2751 4326 500000.0 7214252.097012479 171.0026978299059 65.02507273340763 +4200 2751 169.65 60.3765 425529.8604093439 6696900.434513191 +2751 4326 425529.8604093439 6696900.434513191 169.6522905178975 60.37653036206057 +4200 2751 172.35 69.67349999999999 552354.6497279132 7733269.117284499 +2751 4326 552354.6497279132 7733269.117284499 172.353296996379 69.67361219353786 +4200 2752 174.0 65.505 500000.0 7267771.349342988 +2752 4326 500000.0 7267771.349342988 174.0027933815762 65.50502534879931 +4200 2752 172.65 61.441500000000005 427973.33410805056 6815555.439263968 +2752 4326 427973.33410805056 6815555.439263968 172.65241047128433 61.441493812906174 +4200 2752 175.35 69.5685 552613.3659760265 7721557.879740137 +2752 4326 552613.3659760265 7721557.879740137 175.35333246880828 69.56855468869736 +4200 2753 177.0 66.595 500000.0 7389317.258583589 +2753 4326 500000.0 7389317.258583589 177.0029548439716 66.59498467981057 +4200 2753 175.65 62.5405 430521.24033900944 6938017.003109125 +2753 4326 430521.24033900944 6938017.003109125 175.6525358636491 62.540455766364175 +4200 2753 178.35 70.64949999999999 549941.3892167619 7842134.168207353 +2753 4326 549941.3892167619 7842134.168207353 178.3535535694738 70.64951169041663 +4200 2754 180.0 66.94500000000001 500000.0 7428349.400943698 +2754 4326 500000.0 7428349.400943698 -179.99696997435834 66.94493301360656 +4200 2754 178.65 62.7105 430917.70371809055 6956961.751694089 +2754 4326 430917.70371809055 6956961.751694089 178.65258186398748 62.710403968416465 +4200 2754 -178.65 71.1795 548624.6942458189 7901256.126737621 +2754 4326 548624.6942458189 7901256.126737621 -178.6463151331184 71.17946038611474 +4200 2755 -177.0 68.175 500000.0 7565533.128007671 +2755 4326 500000.0 7565533.128007671 -176.99678382340008 68.17489129609088 +4200 2755 -178.35 65.0835 436514.64138310903 7221452.919002606 +2755 4326 436514.64138310903 7221452.919002606 -178.3471668620458 65.08337870479434 +4200 2755 -175.65 71.2665 548408.1504137059 7910961.358646199 +2755 4326 548408.1504137059 7910961.358646199 -175.646272644824 71.2664028414059 +4200 2756 -174.0 65.99000000000001 500000.0 7321851.563124144 +2756 4326 500000.0 7321851.563124144 -173.99704178763102 65.98980774046488 +4200 2756 -175.35 64.379 434841.0450739842 7142921.782737626 +2756 4326 434841.0450739842 7142921.782737626 -175.34722000598506 64.37881405517382 +4200 2756 -172.65 67.601 557427.062348739 7502136.985763806 +2756 4326 557427.062348739 7502136.985763806 -172.64683880300788 67.60080084484042 +4200 2757 -171.035 65.705 498392.4724482402 7290072.496315905 +2757 4326 498392.4724482402 7290072.496315905 -171.03206586421678 65.70474814320379 +4200 2757 -172.3535 64.4855 434925.1528512238 7154796.54233314 +2757 4326 434925.1528512238 7154796.54233314 -172.35069813134993 64.48525912889072 +4200 2757 -169.7165 66.9245 556157.3633137962 7426641.888505799 +2757 4326 556157.3633137962 7426641.888505799 -169.71342073212566 66.92423675113884 +4200 2758 -169.095 65.78 449855.09013895085 7298871.971787686 +2758 4326 449855.09013895085 7298871.971787686 -169.0920563241622 65.77971212353484 +4200 2758 -169.2075 65.708 444549.43762920203 7290939.162325591 +2758 4326 444549.43762920203 7290939.162325591 -169.20456438637268 65.70771346385851 +4200 2758 -168.9825 65.852 455132.19876915094 7306814.480001371 +2758 4326 455132.19876915094 7306814.480001371 -168.97954822339895 65.8517107812033 +4152 2759 -85.84 32.995 199376.9683514333 276640.7418256493 +2759 4326 199376.9683514333 276640.7418256493 -85.8400044673814 32.99500621147099 +4152 2759 -86.69500000000001 31.1905 117873.04652490633 76870.30876444188 +2759 4326 117873.04652490633 76870.30876444188 -86.69500462657179 31.190505787713562 +4152 2759 -84.985 34.7995 277629.11731132085 477116.87613607576 +2759 4326 277629.11731132085 477116.87613607576 -84.98500431603797 34.79950662702571 +4152 2760 -87.39 32.58 610327.5962043395 286042.60682333034 +2760 4326 610327.5962043395 286042.60682333034 -87.39000488704052 32.58000606743971 +4152 2760 -88.37100000000001 30.384 516289.87988403835 42887.583717217465 +2760 4326 516289.87988403835 42887.583717217465 -88.37100506175527 30.384005555924997 +4152 2760 -86.40899999999999 34.776 699861.512213123 530136.2878085349 +2760 4326 699861.512213123 530136.2878085349 -86.409004725373 34.77600656730427 +4152 2761 -110.375 34.17 194153.50067005004 351528.6430514126 +2761 4326 194153.50067005004 351528.6430514126 -110.3750106626597 34.17000533355354 +4152 2761 -111.5765 31.613999999999997 79592.58561085619 68933.09865045993 +2761 4326 79592.58561085619 68933.09865045993 -111.57651072996339 31.61400490679809 +4152 2761 -109.1735 36.726 302072.0270641723 635525.7685795034 +2761 4326 302072.0270641723 635525.7685795034 -109.17351063142083 36.726005759351416 +4152 2762 -111.895 34.17 215357.474309584 351509.2431937203 +2762 4326 215357.474309584 351509.2431937203 -111.89501096944797 34.170005252214466 +4152 2762 -113.2045 31.613999999999997 91169.08245612771 68790.21379401149 +2762 4326 91169.08245612771 68790.21379401149 -113.2045110476346 31.61400483453871 +4152 2762 -110.5855 36.726 332264.3607156152 635892.0351637731 +2762 4326 332264.3607156152 635892.0351637731 -110.58551092685303 36.726005671862836 +4152 2763 -113.66499999999999 34.525 221163.54809494727 390901.08565793367 +2763 4326 221163.54809494727 390901.08565793367 -113.66501134259887 34.525005202084955 +4152 2763 -114.6955 32.2975 124310.60752930673 144250.73145847756 +2763 4326 124310.60752930673 144250.73145847756 -114.6955113720565 32.29750485855689 +4152 2763 -112.6345 36.7525 312968.5783435126 638607.9599893158 +2763 4326 312968.5783435126 638607.9599893158 -112.63451134282093 36.752505546103826 +4152 2764 -92.13 35.585 388218.3760538642 138869.51677349143 +2764 4326 388218.3760538642 138869.51677349143 -92.1300063766645 35.58500648656259 +4152 2764 -94.37100000000001 34.7615 182942.28431090515 50115.795887343964 +2764 4326 182942.28431090515 50115.795887343964 -94.37100692280187 34.76150622770002 +4152 2764 -89.889 36.408500000000004 589342.0332958896 232263.49768251824 +2764 4326 589342.0332958896 232263.49768251824 -89.88900581479095 36.408506747129636 +4152 2765 -92.44 34.055 359380.3456728528 554070.3461636706 +2765 4326 359380.3456728528 554070.3461636706 -92.44000636182047 34.05500617569817 +4152 2765 -94.27600000000001 33.1145 187583.30831943935 452033.15065267665 +2765 4326 187583.30831943935 452033.15065267665 -94.27600679522018 33.11450591422384 +4152 2765 -90.604 34.9955 527448.8375440208 659180.0131279876 +2765 4326 527448.8375440208 659180.0131279876 -90.60400591938794 34.99550643953686 +4152 2766 -122.22 40.8 1981436.393996671 662877.7471431005 +2766 4326 1981436.393996671 662877.7471431005 -122.22001352643227 40.800005244222184 +4152 2766 -124.227 39.711000000000006 1809032.8706182928 544364.0613099996 +2766 4326 1809032.8706182928 544364.0613099996 -124.22701367330524 39.71100499847447 +4152 2766 -120.213 41.888999999999996 2148310.5570726353 785305.6470252918 +2766 4326 2148310.5570726353 785305.6470252918 -120.21301336809474 41.88900550102047 +4152 2767 -121.80000000000001 39.09 2017301.8204829379 658016.692001398 +2767 4326 2017301.8204829379 658016.692001398 -121.80001322791112 39.09000514272674 +4152 2767 -123.834 38.127 1839195.8283859724 552723.7703899188 +2767 4326 1839195.8283859724 552723.7703899188 -123.83401341497996 38.12700491139173 +4152 2767 -119.766 40.053 2190614.0811918867 767255.0743562812 +2767 4326 2190614.0811918867 767255.0743562812 -119.76601302639789 40.053005384673746 +4152 2768 -120.425 37.72 2006611.9340582322 635399.2547793326 +2768 4326 2006611.9340582322 635399.2547793326 -120.42501284523769 37.720005125177764 +4152 2768 -122.7605 36.82899999999999 1798347.426047734 538949.0753376134 +2768 4326 1798347.426047734 538949.0753376134 -122.76051311081645 36.829004882510716 +4152 2768 -118.0895 38.611000000000004 2209937.041178584 736997.6950945166 +2768 4326 2209937.041178584 736997.6950945166 -118.08951255738364 38.6110053793831 +4152 2769 -118.815 36.68 2016534.9266023266 649444.111312916 +2769 4326 2016534.9266023266 649444.111312916 -118.81501246738881 36.680005135239306 +4152 2769 -121.6905 35.870000000000005 1757042.2782625472 562953.5650761565 +2769 4326 1757042.2782625472 562953.5650761565 -121.69051285083924 35.87000486825691 +4152 2769 -115.93950000000001 37.489999999999995 2270631.083070669 743632.1654617812 +2769 4326 2270631.083070669 743632.1654617812 -115.93951204699096 37.490005414870545 +4152 2770 -117.77000000000001 34.285 2021176.3034016618 587100.3705963591 +2770 4326 2021176.3034016618 587100.3705963591 -117.77001206875867 34.285004941962676 +4152 2770 -121.055 32.9125 1714139.7936607758 439167.22925522097 +2770 4326 1714139.7936607758 439167.22925522097 -121.05501250164174 32.91250461442096 +4152 2770 -114.485 35.6575 2318240.215058904 744899.1637714047 +2770 4326 2318240.215058904 744899.1637714047 -114.48501159572905 35.6575052960395 +4152 2771 -116.285 33.305 1996740.5868549028 626244.559176123 +2771 4326 1996740.5868549028 626244.559176123 -116.28501173183126 33.30500490721454 +4152 2771 -117.96350000000001 32.6075 1839164.448277042 550211.7107019561 +2771 4326 1839164.448277042 550211.7107019561 -117.96351197479895 32.607504737828265 +4152 2771 -114.6065 34.0025 2151827.0133517752 704802.7808291488 +2771 4326 2151827.0133517752 704802.7808291488 -114.60651147853586 34.00250508348426 +4152 2772 -105.55000000000001 40.285 910150.1227950495 410466.04419278353 +2772 4326 910150.1227950495 410466.04419278353 -105.55001020940664 40.285006484943565 +4152 2772 -108.709 39.6325 638965.6755298494 343001.4062162024 +2772 4326 638965.6755298494 343001.4062162024 -108.70901084128926 39.63250617567785 +4152 2772 -102.391 40.9375 1176178.9159974535 487510.83186888543 +2772 4326 1176178.9159974535 487510.83186888543 -102.39100953405021 40.93750679615229 +4152 2773 -105.55000000000001 39.115 910077.8060260228 447076.0864687894 +2773 4326 910077.8060260228 447076.0864687894 -105.55001007503591 39.11500632367665 +4152 2773 -108.709 38.2375 633503.6654187832 354628.51937078615 +2773 4326 633503.6654187832 354628.51937078615 -108.7090106842675 38.2375059934246 +4152 2773 -102.391 39.99250000000001 1179883.6778052924 549041.2134201529 +2773 4326 1179883.6778052924 549041.2134201529 -102.39100942408092 39.99250665988343 +4152 2774 -105.55000000000001 37.83 910000.3279125895 433913.0553515627 +2774 4326 910000.3279125895 433913.0553515627 -105.5500099404933 37.83000614046445 +4152 2774 -108.709 37.065 629052.8991190449 353911.53463970317 +2774 4326 629052.8991190449 353911.53463970317 -108.70901056486461 37.065005834690815 +4152 2774 -102.391 38.595 1185210.975475688 523332.2323330514 +2774 4326 1185210.975475688 523332.2323330514 -102.39100927477543 38.59500645175865 +4152 2775 -72.755 41.515 304383.2321191824 228105.00864740624 +2775 4326 304383.2321191824 228105.00864740624 -72.75500094218829 41.51500843776168 +4152 2775 -73.63250000000001 41.0335 230588.64706697664 175009.14457170662 +2775 4326 230588.64706697664 175009.14457170662 -73.63250119885029 41.03350831518279 +4152 2775 -71.8775 41.9965 377091.7689576581 281949.1244673686 +2775 4326 377091.7689576581 281949.1244673686 -71.87750068300454 41.996508558677014 +4152 2776 -75.38499999999999 39.144999999999996 202737.5480806781 127103.25313109877 +2776 4326 202737.5480806781 127103.25313109877 -75.38500165125696 39.145007879108 +4152 2776 -75.7585 38.5105 170185.3710576928 56721.266312237676 +2776 4326 170185.3710576928 56721.266312237676 -75.75850173398861 38.5105077364937 +4152 2776 -75.0115 39.7795 234709.65364230535 197625.8780571235 +2776 4326 234709.65364230535 197625.8780571235 -75.0115015686061 39.779508020087896 +4152 2777 -81.15 27.619999999999997 185195.14759915508 364110.21714043804 +2777 4326 185195.14759915508 364110.21714043804 -81.15000290517028 27.62000510243377 +4152 2777 -82.212 24.731 77384.67721616034 44587.952464870745 +2777 4326 77384.67721616034 44587.952464870745 -82.21200314076943 24.731004355213116 +4152 2777 -80.088 30.508999999999997 287539.61535454553 684644.0779293055 +2777 4326 287539.61535454553 684644.0779293055 -80.08800268711973 30.509005826799324 +4152 2778 -82.235 27.935000000000002 176872.42951769338 399028.39882096706 +2778 4326 176872.42951769338 399028.39882096706 -82.23500323051101 27.935005154164728 +4152 2778 -83.2295 26.4365 77365.23380374948 233558.28670167812 +2778 4326 77365.23380374948 233558.28670167812 -83.22950347547183 26.43650476398478 +4152 2778 -81.2405 29.433500000000002 273690.74826097145 565316.0981896543 +2778 4326 273690.74826097145 565316.0981896543 -81.24050298864435 29.433505540434343 +4152 2779 -84.83500000000001 30.11 567714.4986294459 123087.90795582991 +2779 4326 567714.4986294459 123087.90795582991 -84.83500405385746 30.110005600408027 +4152 2779 -87.3505 29.3 323067.84639475704 36716.95956284991 +2779 4326 323067.84639475704 36716.95956284991 -87.35050473485067 29.300005342775385 +4152 2779 -82.3195 30.92 808415.1822636275 214828.78102081481 +2779 4326 808415.1822636275 214828.78102081481 -82.31950335837459 30.920005860195275 +4152 2780 -82.12 32.519999999999996 204384.1730120967 279375.5698015051 +2780 4326 204384.1730120967 279375.5698015051 -82.1200033665047 32.52000623096461 +4152 2780 -83.335 30.576 87935.98120049336 64428.67874660541 +2780 4326 87935.98120049336 64428.67874660541 -83.33500364018025 30.5760057512362 +4152 2780 -80.905 34.464 315913.03382685786 495690.7435459547 +2780 4326 315913.03382685786 495690.7435459547 -80.9050030976861 34.46400670141546 +4152 2781 -84.3 32.815 687514.8760667271 312094.28572832706 +2781 4326 687514.8760667271 312094.28572832706 -84.30000401557892 32.81500622528612 +4152 2781 -85.479 30.8395 574464.4552982544 93794.36979869824 +2781 4326 574464.4552982544 93794.36979869824 -85.4790042667027 30.839505747350383 +4152 2781 -83.121 34.790499999999994 795691.9523103088 531682.9947136147 +2781 4326 795691.9523103088 531682.9947136147 -83.12100377096391 34.79050669425869 +4152 2782 -155.42000000000002 19.6 508392.43257363234 84865.06216114241 +2782 4326 508392.43257363234 84865.06216114241 -155.42000000000002 19.599999999999998 +4152 2782 -156.03199999999998 18.943 443966.8337234195 12223.129253569174 +2782 4326 443966.8337234195 12223.129253569174 -156.03199999999998 18.942999999999998 +4152 2782 -154.80800000000002 20.256999999999998 572296.4680864818 157743.97569391556 +2782 4326 572296.4680864818 157743.97569391556 -154.80800000000002 20.256999999999998 +4152 2783 -156.645 20.855 502254.7853232328 57752.88297264607 +2783 4326 502254.7853232328 57752.88297264607 -156.645 20.855000000000004 +4152 2783 -157.2885 20.4905 435132.0824620548 17522.483149056337 +2783 4326 435132.0824620548 17522.483149056337 -157.2885 20.4905 +4152 2783 -156.00150000000002 21.2195 569054.8823151813 98253.00576796383 +2783 4326 569054.8823151813 98253.00576796383 -156.00150000000002 21.2195 +4152 2784 -157.97000000000003 21.475 503109.1061699619 34139.0008563712 +2784 4326 503109.1061699619 34139.0008563712 -157.97000000000003 21.475 +4152 2784 -158.294 21.2275 469479.4657924552 6763.726930458374 +2784 4326 469479.4657924552 6763.726930458374 -158.294 21.2275 +4152 2784 -157.64600000000002 21.7225 536625.296198369 61584.71779271407 +2784 4326 536625.296198369 61584.71779271407 -157.64600000000002 21.722499999999997 +4152 2785 -159.54 22.049999999999997 495871.0045759173 23991.690169427377 +2785 4326 495871.0045759173 23991.690169427377 -159.54 22.049999999999997 +4152 2785 -159.819 21.834 467021.380951611 107.96252484768765 +2785 4326 467021.380951611 107.96252484768765 -159.819 21.834 +4152 2785 -159.261 22.266 524633.1681034412 47928.577949660204 +2785 4326 524633.1681034412 47928.577949660204 -159.261 22.266 +4152 2786 -160.145 21.9 502238.9118068838 25836.542382528078 +2786 4326 502238.9118068838 25836.542382528078 -160.145 21.899999999999995 +4152 2786 -160.2845 21.747 487810.8059930551 8899.659826272124 +2786 4326 487810.8059930551 8899.659826272124 -160.2845 21.746999999999996 +4152 2786 -160.0055 22.053 516636.24116117816 42786.85294238828 +2786 4326 516636.24116117816 42786.85294238828 -160.0055 22.052999999999997 +4152 2787 -112.14 43.370000000000005 202161.21936047942 189202.38724812766 +2787 4326 202161.21936047942 189202.38724812766 -112.14001209160897 43.37000633807429 +4152 2787 -113.13 42.128 120351.05903192172 51687.362961994455 +2787 4326 120351.05903192172 51687.362961994455 -113.13001210091448 42.128006125769296 +4152 2787 -111.15 44.612 280695.92876894755 327698.8013920951 +2787 4326 280695.92876894755 327698.8013920951 -111.15001209124347 44.61200655180666 +4152 2788 -113.985 43.845 501206.1570626486 241974.0498370269 +2788 4326 501206.1570626486 241974.0498370269 -113.98501254671349 43.84500622337054 +4152 2788 -115.1685 42.1755 403459.76413783856 57175.03314187794 +2788 4326 403459.76413783856 57175.03314187794 -115.16851250863147 42.17550595988559 +4152 2788 -112.8015 45.514500000000005 593643.3516935457 428187.0188976105 +2788 4326 593643.3516935457 428187.0188976105 -112.80151260509575 45.51450648870013 +4152 2789 -115.78 45.5 797655.41440746 425871.2238890269 +2789 4326 797655.41440746 425871.2238890269 -115.78001320219224 45.50000620448675 +4152 2789 -117.094 42.341 689251.4777751659 75770.44093186814 +2789 4326 689251.4777751659 75770.44093186814 -117.09401289462863 42.34100581047419 +4152 2789 -114.466 48.659 894584.9816078962 777836.67337109 +2789 4326 894584.9816078962 777836.67337109 -114.46601362245677 48.659006589097146 +4152 2790 -88.145 39.78 316133.6048133849 345590.73500594514 +2790 4326 316133.6048133849 345590.73500594514 -88.14500557851211 39.78000746320871 +4152 2790 -89.1575 37.332 226960.888561657 74153.85203580958 +2790 4326 226960.888561657 74153.85203580958 -89.15750567494628 37.33200695805758 +4152 2790 -87.1325 42.228 399132.2493135867 618125.4239692872 +2790 4326 399132.2493135867 618125.4239692872 -87.13250550400706 42.22800794954199 +4152 2791 -90.22 39.739999999999995 695428.7036032164 341122.5008347094 +2791 4326 695428.7036032164 341122.5008347094 -90.22000617139152 39.7400073484703 +4152 2791 -91.39 37.247 591466.4497448911 65101.11165521788 +2791 4326 591466.4497448911 65101.11165521788 -91.39000629374664 37.2470068353021 +4152 2791 -89.05 42.233 792173.5648998992 618565.43078374 +2791 4326 792173.5648998992 618565.43078374 -89.05000607188076 42.23300784442371 +4152 2792 -85.685 39.86 98431.30571691465 511973.6556178053 +2792 4326 98431.30571691465 511973.6556178053 -85.68500486291144 39.86000759904122 +4152 2792 -86.4995 38.141000000000005 26992.295511845572 321471.9125532987 +2792 4326 26992.295511845572 321471.9125532987 -86.4995049704354 38.14100723589421 +4152 2792 -84.8705 41.579 166394.06102200958 703165.6599238857 +2792 4326 166394.06102200958 703165.6599238857 -84.87050476373865 41.5790079526695 +4152 2793 -87.16999999999999 39.77 892574.6756391372 501984.6231915935 +2793 4326 892574.6756391372 501984.6231915935 -87.17000529338411 39.770007510184335 +4152 2793 -88.00699999999999 37.970000000000006 818840.8874181742 302566.75752018945 +2793 4326 818840.8874181742 302566.75752018945 -88.0070053940648 37.97000713343579 +4152 2793 -86.333 41.57 962580.5798687736 702131.8622874243 +2793 4326 962580.5798687736 702131.8622874243 -86.33300520290537 41.57000787692553 +4152 2794 -93.4 42.68 1508195.834348411 1131075.1318394763 +2794 4326 1508195.834348411 1131075.1318394763 -93.40000737057251 42.680007656691394 +4152 2794 -96.325 41.933 1265738.527574641 1052010.8755992097 +2794 4326 1265738.527574641 1052010.8755992097 -96.32500808904744 41.933007352301004 +4152 2794 -90.47500000000001 43.427 1744902.3804289864 1218437.3308182764 +2794 4326 1744902.3804289864 1218437.3308182764 -90.47500661839601 43.42700795712506 +4152 2795 -93.14 41.2 530195.2801683228 133320.7160985994 +2795 4326 530195.2801683228 133320.7160985994 -93.1400071345966 41.20000743414056 +4152 2795 -95.84 40.444 301495.59603204846 51976.20620905145 +2795 4326 301495.59603204846 51976.20620905145 -95.84000779395888 40.44400714775602 +4152 2795 -90.44 41.955999999999996 753654.5170276335 221683.68954160615 +2795 4326 753654.5170276335 221683.68954160615 -90.44000644765974 41.956007718368234 +4152 2796 -98.32 39.265 372384.49819812784 103474.51018951085 +2796 4326 372384.49819812784 103474.51018951085 -98.32000832547315 39.26500680696949 +4152 2796 -101.686 38.594500000000004 78962.18575527694 35527.07937257582 +2796 4326 78962.18575527694 35527.07937257582 -101.68600910336376 38.59450649548409 +4152 2796 -94.954 39.9355 660312.5113567541 182246.34859156059 +2796 4326 660312.5113567541 182246.34859156059 -94.95400750360324 39.93550711744209 +4152 2797 -98.32499999999999 37.935 415383.2434573826 540780.1749808954 +2797 4326 415383.2434573826 540780.1749808954 -98.32500819978237 37.93500658966191 +4152 2797 -101.6775 37.084500000000006 117521.86511747976 451187.8656569628 +2797 4326 117521.86511747976 451187.8656569628 -101.67750895811709 37.08450625920603 +4152 2797 -94.9725 38.7855 706437.5117094776 640969.3042826953 +2797 4326 706437.5117094776 640969.3042826953 -94.9725073990636 38.785506922530466 +4152 2798 -84.215 38.43 503055.9840861851 103227.84047299267 +2798 4326 503055.9840861851 103227.84047299267 -84.21500431836401 38.43000739378459 +4152 2798 -85.7855 37.782000000000004 364736.787111394 32428.687076877144 +2798 4326 364736.787111394 32428.687076877144 -85.78550473619858 37.78200719839844 +4152 2798 -82.6445 39.077999999999996 638921.4422613666 176371.6164450688 +2798 4326 638921.4422613666 176371.6164450688 -82.64450389304774 39.07800758787213 +4152 2799 -85.75999999999999 37.33 499113.7931707417 610604.196224401 +2799 4326 499113.7931707417 610604.196224401 -85.76000469754477 37.33000711080938 +4152 2799 -89.189 36.574000000000005 192247.5353599636 532309.7194656162 +2799 4326 192247.5353599636 532309.7194656162 -89.18900562968464 36.574006810914064 +4152 2799 -82.331 38.086 799892.1099969771 699938.1634462873 +2799 4326 799892.1099969771 699938.1634462873 -82.33100373116714 38.08600740539495 +4152 2800 -92.455 31.94 1004254.595522245 159660.69532187734 +2800 4326 1004254.595522245 159660.69532187734 -92.45500624779022 31.940005749251707 +4152 2800 -93.8905 30.959000000000003 867141.0681850316 51745.95904705165 +2800 4326 867141.0681850316 51745.95904705165 -93.8905065804254 30.959005495611727 +4152 2800 -91.0195 32.921 1138485.7486137254 269389.5708371389 +2800 4326 1138485.7486137254 269389.5708371389 -91.01950591142374 32.92100600494784 +4152 2801 -91.345 29.96 998873.9589036357 161832.71462681066 +2801 4326 998873.9589036357 161832.71462681066 -91.34500585820699 29.96000536975566 +4152 2801 -93.6805 28.961000000000002 771204.4762175585 53445.368118892235 +2801 4326 771204.4762175585 53445.368118892235 -93.68050643949914 28.961005083140446 +4152 2801 -89.0095 30.959 1222030.1736550392 274830.1880059256 +2801 4326 1222030.1736550392 274830.1880059256 -89.00950526332946 30.95900566172793 +4152 2802 -68.47 45.675 302337.23350621155 223154.85963413733 +2802 4326 302337.23350621155 223154.85963413733 -68.46999974731882 45.67500933693338 +4152 2802 -69.874 44.0595 189917.5275568 44561.156556155605 +2802 4326 189917.5275568 44561.156556155605 -69.874000133923 44.05950900713355 +4152 2802 -67.066 47.2905 408460.01410141285 403714.07063095423 +2802 4326 408460.01410141285 403714.07063095423 -67.06599934915188 47.29050965131231 +4152 2803 -70.17500000000001 44.81 899340.7970898099 219617.70745510195 +2803 4326 899340.7970898099 219617.70745510195 -70.17500027855704 44.81000913442751 +4152 2803 -70.9985 43.217 832412.7890298931 42957.32044205438 +2803 4326 832412.7890298931 42957.32044205438 -70.99850046201176 43.217008817534975 +4152 2803 -69.3515 46.403 962683.305256669 396986.5141183069 +2803 4326 962683.305256669 396986.5141183069 -69.35150009311965 46.40300943842052 +4152 2804 -77.22999999999999 38.849999999999994 380034.9481462034 131380.0755704414 +2804 4326 380034.9481462034 131380.0755704414 -77.23000221370286 38.850007755072276 +4152 2804 -79.264 38.058 201314.2794625202 45904.12107919067 +2804 4326 201314.2794625202 45904.12107919067 -79.26400279672983 38.05800751948876 +4152 2804 -75.196 39.641999999999996 554847.9829545154 220811.09831744133 +2804 4326 554847.9829545154 220811.09831744133 -75.19600161935715 39.64200798617263 +4152 2805 -71.68 42.175 185128.3305030929 880522.4527347037 +2805 4326 185128.3305030929 880522.4527347037 -71.68000062822291 42.17500859912396 +4152 2805 -73.318 41.5315 48284.138797150605 810650.6882213238 +2805 4326 48284.138797150605 810650.6882213238 -73.31800112595734 41.53150842266727 +4152 2805 -70.042 42.8185 319232.9572599032 953006.5012715762 +2805 4326 319232.9572599032 953006.5012715762 -70.04200012256888 42.818508770722275 +4152 2806 -70.4 41.349999999999994 508368.84153597394 38875.137285071796 +2806 4326 508368.84153597394 38875.137285071796 -70.40000016506394 41.3500084766361 +4152 2806 -70.859 41.205999999999996 469889.6934071739 22940.12737198121 +2806 4326 469889.6934071739 22940.12737198121 -70.85900030785926 41.20600843509089 +4152 2806 -69.941 41.494 546678.3583144168 55013.767025989066 +2806 4326 546678.3583144168 55013.767025989066 -69.94100002174876 41.49400851783567 +4152 2807 -86.93 46.7 8005353.265366577 213031.3436327338 +2807 4326 8005353.265366577 213031.3436327338 -86.93000594747525 46.7000086845763 +4152 2807 -90.071 45.242 7758926.973017988 55650.446219124075 +2807 4326 7758926.973017988 55650.446219124075 -90.07100671902082 45.24200826622652 +4152 2807 -83.789 48.158 8238929.602731237 379989.2076339546 +2807 4326 8238929.602731237 379989.2076339546 -83.7890051334677 48.15800909108516 +4152 2808 -84.66499999999999 44.86 5976422.234671376 171538.31270695946 +2808 4326 5976422.234671376 171538.31270695946 -84.6650050216364 44.860008526296625 +4152 2808 -86.8205 43.906 5802892.177720298 68471.58702111564 +2808 4326 5802892.177720298 68471.58702111564 -86.82050558252318 43.906008249619795 +4152 2808 -82.5095 45.814 6144335.022055124 279167.9333284333 +2808 4326 6144335.022055124 279167.9333284333 -82.50950444229349 45.81400879751056 +4152 2809 -84.66499999999999 42.955 3975658.1660904195 161660.9908124227 +2809 4326 3975658.1660904195 161660.9908124227 -84.66500482762356 42.9550082054715 +4152 2809 -86.9465 41.8165 3785662.443796816 38441.45475135792 +2809 4326 3785662.443796816 38441.45475135792 -86.94650540897692 41.816507888131625 +4152 2809 -82.3835 44.0935 4158818.2040767726 289977.3408485526 +2809 4326 4158818.2040767726 289977.3408485526 -82.3835042272599 44.09350851732397 +4152 2810 -93.35499999999999 48.010000000000005 780976.0080242507 267903.8812841848 +2810 4326 780976.0080242507 267903.8812841848 -93.35500810075281 48.010008434468325 +4152 2810 -96.8335 46.777 514956.0367357529 137681.0978387199 +2810 4326 514956.0367357529 137681.0978387199 -96.8335089023631 46.77700800742111 +4152 2810 -89.8765 49.243 1034700.1274336235 409882.0127727458 +2810 4326 1034700.1274336235 409882.0127727458 -89.87650724286569 49.243008851819795 +4152 2811 -94.575 46.379999999999995 774999.3390451781 253433.09136305802 +2811 4326 774999.3390451781 253433.09136305802 -94.57500819832896 46.380008123590216 +4152 2811 -96.6315 45.39 613521.9818043811 146151.76135058573 +2811 4326 613521.9818043811 146151.76135058573 -96.63150862970046 45.39000783595432 +4152 2811 -92.5185 47.37 930779.8796238166 364865.75352756435 +2811 4326 930779.8796238166 364865.75352756435 -92.51850774931546 47.370008409059466 +4152 2812 -94.03 44.540000000000006 797615.9303731995 271111.9496617395 +2812 4326 797615.9303731995 271111.9496617395 -94.03000778023568 44.540007899170384 +4152 2812 -96.568 43.595 592659.1578750131 169370.35030780142 +2812 4326 592659.1578750131 169370.35030780142 -96.56800836114256 43.59500758545568 +4152 2812 -91.49199999999999 45.48500000000001 996055.3728436409 379136.3411765582 +2812 4326 996055.3728436409 379136.3411765582 -91.49200717246406 45.485008210230845 +4152 2813 -89.03 32.51 281520.8578694894 333718.072514426 +2813 4326 281520.8578694894 333718.072514426 -89.03000534242128 32.5100059937542 +4152 2813 -89.876 30.26 199662.52524006984 84702.21482670316 +2813 4326 199662.52524006984 84702.21482670316 -89.87600547161422 30.260005481358696 +4152 2813 -88.184 34.76 359446.4830207231 583441.3092225477 +2813 4326 359446.4830207231 583441.3092225477 -88.18400522936615 34.760006493360244 +4152 2814 -90.51 33.004999999999995 683491.678094493 388607.7223874255 +2814 4326 683491.678094493 388607.7223874255 -90.51000577666683 33.00500604180072 +4152 2814 -91.536 31.200499999999998 585383.0308493291 189128.51300998154 +2814 4326 585383.0308493291 189128.51300998154 -91.53600596460582 31.200505628223606 +4152 2814 -89.48400000000001 34.8095 777710.4632391258 589069.1501028265 +2814 4326 777710.4632391258 589069.1501028265 -89.48400559705959 34.80950644993109 +4152 2815 -90.535 38.295 246938.423324011 273175.40948622045 +2815 4326 246938.423324011 273175.40948622045 -90.53500613775934 38.295007071304546 +4152 2815 -91.8265 36.211499999999994 130722.74568677916 42774.13741293872 +2815 4326 130722.74568677916 42774.13741293872 -91.82650633750758 36.21150661994057 +4152 2815 -89.2435 40.3785 356696.8903372759 505231.6112297185 +2815 4326 356696.8903372759 505231.6112297185 -89.24350594906211 40.37850751347751 +4152 2816 -92.6 38.545 491282.7335977044 300928.83009349275 +2816 4326 491282.7335977044 300928.83009349275 -92.60000673294647 38.54500700984763 +4152 2816 -93.671 36.686499999999995 395345.8443743689 95303.31149016054 +2816 4326 395345.8443743689 95303.31149016054 -93.6710068736193 36.68650661952312 +4152 2816 -91.529 40.4035 582422.4658619608 507702.20165428886 +2816 4326 582422.4658619608 507702.20165428886 -91.5290066033391 40.40350739314097 +4152 2817 -94.625 38.535 839101.8219779618 262839.6964433915 +2817 4326 839101.8219779618 262839.6964433915 -94.62500728312082 38.535006898735936 +4152 2817 -95.65549999999999 36.6855 746729.0116193696 58192.123500483096 +2817 4326 746729.0116193696 58192.123500483096 -95.65550740152078 36.685506519185786 +4152 2817 -93.59450000000001 40.3845 926884.6909838021 468553.285440557 +2817 4326 926884.6909838021 468553.285440557 -93.59450717724293 40.384507271904575 +4152 2818 -110.055 46.68 557562.9972258529 270175.48989245493 +2818 4326 557562.9972258529 270175.48989245493 -110.05501223676005 46.68000685748381 +4152 2818 -115.46849999999999 44.583 126329.73944052018 55073.69494508364 +2818 4326 126329.73944052018 55073.69494508364 -115.46851296896516 44.58300615558948 +4152 2818 -104.64150000000001 48.777 956814.4198691815 514139.9142825689 +2818 4326 956814.4198691815 514139.9142825689 -104.64151137247981 48.7770075766658 +4152 2819 -99.68 41.5 526709.5912096216 185091.48420576085 +2819 4326 526709.5912096216 185091.48420576085 -99.68000892094494 41.50000706362558 +4152 2819 -103.622 40.141000000000005 191449.57910202077 40625.804804552994 +2819 4326 191449.57910202077 40625.804804552994 -103.62200973837096 40.14100659840942 +4152 2819 -95.738 42.858999999999995 848158.6906421972 344540.8856728787 +2819 4326 848158.6906421972 344540.8856728787 -95.7380080407106 42.85900753244584 +4152 2820 -115.52000000000001 38.495000000000005 205524.57150390767 8415545.609160446 +2820 4326 205524.57150390767 8415545.609160446 -115.52001208197812 38.49500555407226 +4152 2820 -116.861 35.3405 83857.77529178846 8066253.469782333 +2820 4326 83857.77529178846 8066253.469782333 -116.86101200085497 35.34050511495299 +4152 2820 -114.179 41.6495 316976.07839282975 8766724.77413967 +2820 4326 316976.07839282975 8766724.77413967 -114.17901223844567 41.649505989878776 +4152 2821 -116.59 38.5 506687.1770771124 6416101.467797132 +2821 4326 506687.1770771124 6416101.467797132 -116.5900122785843 38.50000547753878 +4152 2821 -118.03 36.25 377474.7274872544 6167270.088553119 +2821 4326 377474.7274872544 6167270.088553119 -118.03001229048208 36.25000514192926 +4152 2821 -115.14999999999999 40.75 628075.3950645399 6666992.146123811 +2821 4326 628075.3950645399 6666992.146123811 -115.15001229815151 40.75000581944547 +4152 2822 -118.495 39.475 807599.8137248502 4524331.450272928 +2822 4326 807599.8137248502 4524331.450272928 -118.4950127345972 39.4750054320086 +4152 2822 -119.8495 37.2025 687604.7269785614 4272849.753804591 +2822 4326 687604.7269785614 4272849.753804591 -119.84951269441324 37.202505116838935 +4152 2822 -117.14049999999999 41.7475 920000.7132682288 4777661.662819959 +2822 4326 920000.7132682288 4777661.662819959 -117.14051281165082 41.747505752669305 +4152 2823 -71.595 44.0 305747.9189590065 166643.36188109402 +2823 4326 305747.9189590065 166643.36188109402 -71.59500070694952 44.000008943714704 +4152 2823 -72.4635 42.821 234839.1555409388 35965.449943250715 +2823 4326 234839.1555409388 35965.449943250715 -72.46350092318538 42.82100869701777 +4152 2823 -70.7265 45.179 373895.511671885 298081.46545753366 +2823 4326 373895.511671885 298081.46545753366 -70.72650048772996 45.17900918335992 +4152 2824 -74.74 40.114999999999995 129541.93200635779 142309.84863179177 +2824 4326 129541.93200635779 142309.84863179177 -74.74000150132949 40.115008096459 +4152 2824 -75.514 38.994499999999995 62161.845770000305 18379.059719253342 +2824 4326 62161.845770000305 18379.059719253342 -75.51400168360887 38.9945078440669 +4152 2824 -73.966 41.2355 194763.50353423748 266836.18215498014 +2824 4326 194763.50353423748 266836.18215498014 -73.96600131824013 41.235508343555686 +4152 2825 -104.35499999999999 34.5 163010.31461583532 388115.5015019724 +2825 4326 163010.31461583532 388115.5015019724 -104.35500937799638 34.50000569819044 +4152 2825 -105.5835 32.25 47196.13683957874 139273.63539034483 +2825 4326 47196.13683957874 139273.63539034483 -105.5835095032702 32.250005271595846 +4152 2825 -103.1265 36.75 272765.40574489284 638413.9669679842 +2825 4326 272765.40574489284 638413.9669679842 -103.12650927537491 36.75000612302637 +4152 2826 -106.285 34.39 496781.7063517648 375911.1857639483 +2826 4326 496781.7063517648 375911.1857639483 -106.28500980565612 34.39000558127364 +4152 2826 -107.58550000000001 32.041000000000004 373868.1798690023 116192.89978335424 +2826 4326 373868.1798690023 116192.89978335424 -107.5855099295942 32.041005148843226 +4152 2826 -104.9845 36.739 613019.4483625791 637255.137029333 +2826 4326 613019.4483625791 637255.137029333 -104.98450970728973 36.73900601332066 +4152 2827 -107.69 34.165 843215.0652553578 350969.5921065168 +2827 4326 843215.0652553578 350969.5921065168 -107.69001009659374 34.1650054738478 +4152 2827 -108.923 31.6135 726610.1809091113 68531.38595231783 +2827 4326 726610.1809091113 68531.38595231783 -108.92301018810653 31.613505022795174 +4152 2827 -106.457 36.716499999999996 952956.2155361695 634905.4140072872 +2827 4326 952956.2155361695 634905.4140072872 -106.45701003864123 36.716505922721204 +4152 2828 -74.55000000000001 42.95 145920.05529655414 457119.0986501149 +2828 4326 145920.05529655414 457119.0986501149 -74.55000161560929 42.95000864949415 +4152 2828 -75.738 41.087 45987.00461632412 250946.93264947843 +2828 4326 45987.00461632412 250946.93264947843 -75.73800187840901 41.08700825285989 +4152 2828 -73.36200000000001 44.813 240010.12987699886 664725.038343087 +2828 4326 240010.12987699886 664725.038343087 -73.36200135086408 44.81300903037544 +4152 2829 -76.405 43.2 264493.31539123424 355403.0177038457 +2829 4326 264493.31539123424 355403.0177038457 -76.40500223712196 43.20000862567463 +4152 2829 -77.6155 42.111000000000004 164637.84596996327 234938.07240019878 +2829 4326 164637.84596996327 234938.07240019878 -77.6155025474734 42.11100837640432 +4152 2829 -75.1945 44.288999999999994 360844.2676941891 477313.98916154087 +2829 4326 360844.2676941891 477313.98916154087 -75.19450192156799 44.28900886935674 +4152 2830 -78.565 42.815 351499.30115682975 312619.6539237273 +2830 4326 351499.30115682975 312619.6539237273 -78.56500290357539 42.815008467291705 +4152 2830 -79.64949999999999 42.072500000000005 261772.6033852207 230696.4045349582 +2830 4326 261772.6033852207 230696.4045349582 -79.64950319015514 42.07250828484654 +4152 2830 -77.4805 43.5575 439103.5261805072 395694.3379393331 +2830 4326 439103.5261805072 395694.3379393331 -77.48050261280203 43.557508647100825 +4152 2831 -73.03 40.885 381750.7399232304 80221.1659232867 +2831 4326 381750.7399232304 80221.1659232867 -73.03000099593335 40.8850083055393 +4152 2831 -74.137 40.5115 288388.66202628706 38301.14700668562 +2831 4326 288388.66202628706 38301.14700668562 -74.1370013311976 40.51150819554129 +4152 2831 -71.923 41.2585 474051.4783082586 123311.4021303021 +2831 4326 474051.4783082586 123311.4021303021 -71.92300065741367 41.25850841371884 +4152 2832 -100.44999999999999 48.08 603725.2560461811 120076.52492659682 +2832 4326 603725.2560461811 120076.52492659682 -100.45001012966675 48.08000787478018 +4152 2832 -103.708 47.242999999999995 357180.63227263116 32076.32260325921 +2832 4326 357180.63227263116 32076.32260325921 -103.70801082570809 47.243007495763436 +4152 2832 -97.192 48.917 842389.7430150689 218354.69478269183 +2832 4326 842389.7430150689 218354.69478269183 -97.19200938163863 48.91700824831343 +4152 2833 -100.3 46.879999999999995 615244.233397394 134889.74595170486 +2833 4326 615244.233397394 134889.74595170486 -100.30000986985306 46.88000774365385 +4152 2833 -103.675 46.025 354223.17057553853 44799.12802504772 +2833 4326 354223.17057553853 44799.12802504772 -103.67501059551807 46.025007358538076 +4152 2833 -96.925 47.735 868072.4690473499 236025.6304733758 +2833 4326 868072.4690473499 236025.6304733758 -96.92500909016327 47.73500812378931 +4152 2834 -82.66 41.215 586582.9888405734 171948.99910634552 +2834 4326 586582.9888405734 171948.99910634552 -82.66000405914379 41.215007992324054 +4152 2834 -84.595 40.2115 421661.2948510517 62644.95346345006 +2834 4326 421661.2948510517 62644.95346345006 -84.59500456577199 40.21150771613819 +4152 2834 -80.72500000000001 42.2185 746566.5265375796 284886.53487657424 +2834 4326 746566.5265375796 284886.53487657424 -80.7250035400007 42.21850826444878 +4152 2835 -82.765 39.379999999999995 577168.794808187 153231.72928756103 +2835 4326 577168.794808187 153231.72928756103 -82.76500395099885 39.38000764121306 +4152 2835 -84.62349999999999 38.498 414763.0625386159 57464.14699698818 +2835 4326 414763.0625386159 57464.14699698818 -84.62350444454509 38.49800738909418 +4152 2835 -80.90650000000001 40.262 735553.8037072645 252321.37590929342 +2835 4326 735553.8037072645 252321.37590929342 -80.90650344678056 40.262007890562415 +4152 2836 -98.71000000000001 36.14 536101.1714221651 126719.8429846217 +2836 4326 536101.1714221651 126719.8429846217 -98.71000814466498 36.14000626517363 +4152 2836 -102.571 35.357 184677.13744587317 49389.48967290248 +2836 4326 184677.13744587317 49389.48967290248 -102.57100902726768 35.35700593036932 +4152 2836 -94.849 36.923 880718.1162748237 217928.31690933244 +2836 4326 880718.1162748237 217928.31690933244 -94.84900720716108 36.923006602982234 +4152 2837 -97.21000000000001 34.595 672465.8025262959 140231.8207822339 +2837 4326 672465.8025262959 140231.8207822339 -97.21000764983027 34.59500606565183 +4152 2837 -99.721 33.7175 440481.2346152521 43973.85878489801 +2837 4326 440481.2346152521 43973.85878489801 -99.72100822026755 33.71750579026642 +4152 2837 -94.699 35.4725 899563.148845982 242195.49149143902 +2837 4326 899563.148845982 242195.49149143902 -94.6990070584319 35.47250634578627 +4152 2838 -120.32 45.105000000000004 2514164.9883181253 159845.1412173828 +2838 4326 2514164.9883181253 159845.1412173828 -120.32001395163995 45.105005731048735 +4152 2838 -123.785 44.0655 2236864.6381944637 49671.3154349094 +2838 4326 2236864.6381944637 49671.3154349094 -123.78501430704941 44.06550532370581 +4152 2838 -116.855 46.1445 2781533.537177242 281708.5817855314 +2838 4326 2781533.537177242 281708.5817855314 -116.85501353758906 46.14450615108872 +4152 2839 -120.75 43.269999999999996 1479706.3814610983 178132.5497103679 +2839 4326 1479706.3814610983 178132.5497103679 -120.75001368440185 43.27000555881456 +4152 2839 -124.21499999999999 42.108999999999995 1192816.3476943534 55951.90213787231 +2839 4326 1192816.3476943534 55951.90213787231 -124.21501402865582 42.10900516181532 +4152 2839 -117.28500000000001 44.431000000000004 1755961.4123870605 312011.62286356714 +2839 4326 1755961.4123870605 312011.62286356714 -117.28501328506809 44.431005972740245 +4152 2840 -71.465 41.575 102918.97351274508 54604.88640063912 +2840 4326 102918.97351274508 54604.88640063912 -71.46500052503204 41.57500848957802 +4152 2840 -71.8115 41.1745 73861.27843044055 10171.357680009327 +2840 4326 73861.27843044055 10171.357680009327 -71.81150061664178 41.17450840060679 +4152 2840 -71.1185 41.975500000000004 131619.5144680358 99157.52522727985 +2840 4326 131619.5144680358 99157.52522727985 -71.11850043299016 41.975508577753324 +4152 2841 -100.25999999999999 45.045 579517.1582990598 134676.18923571636 +2841 4326 579517.1582990598 134676.18923571636 -100.26000955663959 45.045007514417705 +4152 2841 -103.689 44.2305 305357.195159725 50848.92774272213 +2841 4326 305357.195159725 50848.92774272213 -103.68901030397049 44.23050713960787 +4152 2841 -96.831 45.859500000000004 846048.4784497482 229981.1518169474 +2841 4326 846048.4784497482 229981.1518169474 -96.83100875604335 45.85950788528206 +4152 2842 -100.245 43.635 607127.5043806202 144607.50092678124 +2842 4326 607127.5043806202 144607.50092678124 -100.24500934456164 43.635007326922235 +4152 2842 -103.6785 42.595499999999994 325505.86952982674 34653.81023614486 +2842 4326 325505.86952982674 34653.81023614486 -103.67851006254774 42.59550693037905 +4152 2842 -96.81150000000001 44.6745 879191.0064309249 266024.15353638545 +2842 4326 879191.0064309249 266024.15353638545 -96.81150857536615 44.6745077230717 +4152 2843 -85.98 35.83 601807.0427761595 166053.81335326185 +2843 4326 601807.0427761595 166053.81335326185 -85.98000466448423 35.830006800932686 +4152 2843 -89.87700000000001 35.065 246434.998258424 88183.71880346097 +2843 4326 246434.998258424 88183.71880346097 -89.87700572237117 35.06500648441918 +4152 2843 -82.083 36.595 950418.3889824077 257951.77285579516 +2843 4326 950418.3889824077 257951.77285579516 -82.08300356427127 36.59500711206894 +4152 2844 -101.50999999999999 35.4 199091.66389534375 1155311.273626022 +2844 4326 199091.66389534375 1155311.273626022 -101.51000877684982 35.40000599319395 +4152 2844 -102.878 34.41 73304.61790902242 1046368.8035420016 +2844 4326 73304.61790902242 1046368.8035420016 -102.87800902804939 34.4100057581895 +4152 2844 -100.142 36.39 321839.4199389363 1265990.8998509285 +2844 4326 321839.4199389363 1265990.8998509285 -100.14200852238555 36.39000623131893 +4152 2845 -98.535 33.15 596735.0946008511 2164487.199652242 +2845 4326 596735.0946008511 2164487.199652242 -98.53500789013707 33.15000574042905 +4152 2845 -102.61649999999999 31.863 210507.96670704812 2029404.6807503384 +2845 4326 210507.96670704812 2029404.6807503384 -102.616508801225 31.863005333084832 +4152 2845 -94.4535 34.437 971879.2800386127 2314398.7444936717 +2845 4326 971879.2800386127 2314398.7444936717 -94.45350692326356 34.43700616236057 +4152 2846 -100.08 31.025000000000002 724185.2196589834 3150605.5484253517 +2846 4326 724185.2196589834 3150605.5484253517 -100.08000815039709 31.025005281578768 +4152 2846 -106.002 29.904500000000002 152730.7257107871 3040313.3125654417 +2846 4326 152730.7257107871 3040313.3125654417 -106.00200946823989 29.90450485856436 +4152 2846 -94.158 32.145500000000006 1282351.2308671223 3290980.4998480678 +2846 4326 1282351.2308671223 3290980.4998480678 -94.1580067103587 32.14550572672265 +4152 2847 -99.38 29.225 563058.7354705705 4154293.521203296 +2847 4326 563058.7354705705 4154293.521203296 -99.38000789596624 29.225004960286075 +4152 2847 -104.438 27.924500000000002 64841.30729672348 4022548.884762567 +2847 4326 64841.30729672348 4022548.884762567 -104.43800903523291 27.924504556573833 +4152 2847 -94.322 30.5255 1048881.5104492614 4307362.930045891 +2847 4326 1048881.5104492614 4307362.930045891 -94.32200667333103 30.5255053913143 +4152 2848 -98.525 27.02 297519.3277685372 5149938.002474285 +2848 4326 297519.3277685372 5149938.002474285 -98.52500760513941 27.020004539962915 +4152 2848 -100.0325 25.948999999999998 146497.67705421898 5032213.809542949 +2848 4326 146497.67705421898 5032213.809542949 -100.03250794189627 25.949004283094194 +4152 2848 -97.0175 28.091 445706.4161671256 5269468.382250638 +2848 4326 445706.4161671256 5269468.382250638 -97.0175072654353 28.09100480251846 +4152 2849 -111.54 41.28 496649.01927216403 1105128.0559982655 +2849 4326 496649.01927216403 1105128.0559982655 -111.54001165412147 41.280006162212494 +4152 2849 -113.79 40.623 306256.0472603298 1034720.2739077386 +2849 4326 306256.0472603298 1034720.2739077386 -113.79001201836172 40.623005913442896 +4152 2849 -109.29 41.937 683266.1531509589 1180426.6984195628 +2849 4326 683266.1531509589 1180426.6984195628 -109.2900112670251 41.9370064151121 +4152 2850 -111.545 39.785 496145.6404933844 2161162.6996333404 +2850 4326 496145.6404933844 2161162.6996333404 -111.5450114594936 39.78500598942574 +4152 2850 -113.7995 38.6195 299737.81075454154 2034346.1327566376 +2850 4326 299737.81075454154 2034346.1327566376 -113.79951176992728 38.61950569079333 +4152 2850 -109.29050000000001 40.9505 686033.2712166447 2292873.457553067 +2850 4326 686033.2712166447 2292873.457553067 -109.29051113244297 40.95050629731785 +4152 2851 -111.545 37.785 496036.2211105266 3124117.453606073 +2851 4326 496036.2211105266 3124117.453606073 -111.54501123019124 37.78500574623371 +4152 2851 -113.7995 37.069500000000005 295518.8854022882 3047222.248513894 +2851 4326 295518.8854022882 3047222.248513894 -113.79951160149827 37.069505509610316 +4152 2851 -109.29050000000001 38.500499999999995 692727.1993279501 3205811.513840428 +2851 4326 692727.1993279501 3205811.513840428 -109.29051083754662 38.50050598913477 +4152 2852 -72.47 43.875 502411.1456667513 152752.52373386305 +2852 4326 502411.1456667513 152752.52373386305 -72.47000099064276 43.87500889227031 +4152 2852 -73.343 42.835499999999996 431080.14350458205 37612.8404625171 +2852 4326 431080.14350458205 37612.8404625171 -73.3430012130703 42.835508670437314 +4152 2852 -71.597 44.914500000000004 571301.9692705146 268653.87743555265 +2852 4326 571301.9692705146 268653.87743555265 -71.59700076518057 44.91450910861833 +4152 2853 -78.285 38.615 3518724.203106891 2105285.4939807146 +2853 4326 3518724.203106891 2105285.4939807146 -78.28500252796418 38.61500766912454 +4152 2853 -79.88250000000001 37.8545 3378332.274917174 2021765.3758821723 +2853 4326 3378332.274917174 2021765.3758821723 -79.88250297400525 37.85450745474152 +4152 2853 -76.6875 39.3755 3656172.5417433567 2191230.607915427 +2853 4326 3656172.5417433567 2191230.607915427 -76.6875020746976 39.37550788064867 +4152 2854 -79.495 37.41 3411917.6086140587 1119947.2032969252 +2854 4326 3411917.6086140587 1119947.2032969252 -79.49500282952783 37.41000737771253 +4152 2854 -83.26150000000001 36.627 3074278.1812144495 1043328.2241050153 +2854 4326 3074278.1812144495 1043328.2241050153 -83.26150391710189 36.62700707285759 +4152 2854 -75.7285 38.193 3742765.366771591 1209952.5513936281 +2854 4326 3742765.366771591 1209952.5513936281 -75.72850170752896 38.193007671743395 +4152 2855 -120.905 48.065 494658.8782095999 118411.46677224239 +2855 4326 494658.8782095999 118411.46677224239 -120.9050147012095 48.06500585790535 +4152 2855 -124.4015 47.1785 229601.1611486969 26116.106779398477 +2855 4326 229601.1611486969 26116.106779398477 -124.40151505418174 47.17850542878113 +4152 2855 -117.4085 48.951499999999996 750773.7610210045 222566.60307881774 +2855 4326 750773.7610210045 222566.60307881774 -117.40851428307445 48.95150629481487 +4152 2856 -120.655 46.575 488119.19059687364 138022.62988068728 +2856 4326 488119.19059687364 138022.62988068728 -120.65501431417754 46.57500579449115 +4152 2856 -124.02550000000001 45.643499999999996 225232.76586336124 40617.24264164518 +2856 4326 225232.76586336124 40617.24264164518 -124.02551465649795 45.64350538981438 +4152 2856 -117.2845 47.5065 742180.9642298741 246499.98165656062 +2856 4326 742180.9642298741 246499.98165656062 -117.28451391386133 47.50650620877364 +4152 2857 -79.74000000000001 39.7 579417.2957110632 133248.46017368144 +2857 4326 579417.2957110632 133248.46017368144 -79.7400030472736 39.70000782967337 +4152 2857 -81.558 38.854 421367.18441514135 41346.53591224026 +2857 4326 421367.18441514135 41346.53591224026 -81.55800354821939 38.85400758953746 +4152 2857 -77.922 40.546 733674.570779342 228331.12197136818 +2857 4326 733674.570779342 228331.12197136818 -77.92200253628518 40.54600806621145 +4152 2858 -80.85 38.185 613140.720628098 131531.86202500196 +2858 4326 613140.720628098 131531.86202500196 -80.85000328930039 38.18500748458883 +4152 2858 -82.47 37.298500000000004 469665.8086919263 34164.20925580738 +2858 4326 469665.8086919263 34164.20925580738 -82.47000372262399 37.298507241192965 +4152 2858 -79.23 39.0715 753171.9602462114 231389.43782599163 +2858 4326 753171.9602462114 231389.43782599163 -79.23000284854531 39.071507725423324 +4152 2859 -90.47 46.34 563817.9183929065 130517.46296681912 +2859 4326 563817.9183929065 130517.46296681912 -90.47000698486964 46.340008404400656 +4152 2859 -92.648 45.467 392950.88681591925 36832.035967861266 +2859 4326 392950.88681591925 36832.035967861266 -92.64800750866706 45.467008129746326 +4152 2859 -88.292 47.213 729391.0452604456 228851.15030724084 +2859 4326 729391.0452604456 228851.15030724084 -88.29200644059216 47.21300867507545 +4152 2860 -89.57 44.89 633966.7768787964 117505.3837196188 +2860 4326 633966.7768787964 117505.3837196188 -89.57000652512149 44.8900082436984 +4152 2860 -92.558 44.071 395103.35046936397 29636.785858339033 +2860 4326 395103.35046936397 29636.785858339033 -92.55800729919869 44.07100792678273 +4152 2860 -86.582 45.708999999999996 866087.9109689173 214035.8706913536 +2860 4326 866087.9109689173 214035.8706913536 -86.58200571521746 45.7090085531398 +4152 2861 -89.19 43.405 665607.2736576359 156400.65287319338 +2861 4326 665607.2736576359 156400.65287319338 -89.19000623814526 43.405008030614795 +4152 2861 -91.206 42.5725 500982.45158363064 64316.57338896093 +2861 4326 500982.45158363064 64316.57338896093 -91.20600673431078 42.57250777486334 +4152 2861 -87.174 44.2375 825722.2152976134 252402.33148048513 +2861 4326 825722.2152976134 252402.33148048513 -87.17400572629828 44.237508283738315 +4152 2862 -105.19 43.0 198097.49632915662 277654.042474903 +2862 4326 198097.49632915662 277654.042474903 -105.19001048355334 43.0000068666047 +4152 2862 -106.216 41.191 111974.33693520293 77262.27621200448 +2862 4326 111974.33693520293 77262.27621200448 -106.21601047607007 41.19100655727374 +4152 2862 -104.164 44.809 279314.3848616645 479129.0114329825 +2862 4326 279314.3848616645 479129.0114329825 -104.16401051256005 44.80900717243314 +4152 2863 -107.315 43.0 401494.82431166724 377653.9413779187 +2863 4326 401494.82431166724 377653.9413779187 -107.31501097968342 43.00000669835129 +4152 2863 -108.49849999999999 41.191 302257.2284902079 177385.96133674524 +2863 4326 302257.2284902079 177385.96133674524 -108.49851099096762 41.191006386658806 +4152 2863 -106.1315 44.809 495069.171304143 579342.6387105695 +2863 4326 495069.171304143 579342.6387105695 -106.13151098867684 44.809007008740004 +4152 2864 -109.28 43.0 556785.943962579 277790.0915190472 +2864 4326 556785.943962579 277790.0915190472 -109.28001142106993 43.000006538694244 +4152 2864 -110.882 41.191 421148.2266105671 78923.38133064652 +2864 4326 421148.2266105671 78923.38133064652 -110.88201150453627 41.19100620356625 +4152 2864 -107.678 44.809 684798.8953224984 479198.9978457161 +2864 4326 684798.8953224984 479198.9978457161 -107.67801135116785 44.80900687697224 +4152 2865 -110.05000000000001 42.83 802725.3431980808 358770.01389699115 +2865 4326 802725.3431980808 358770.01389699115 -110.0500115633401 42.83000645617076 +4152 2865 -110.959 41.174 726523.9197376369 175213.1974278146 +2865 4326 726523.9197376369 175213.1974278146 -110.9590115183874 41.17400619561758 +4152 2865 -109.141 44.486000000000004 874956.6699701425 543180.753202716 +2865 4326 874956.6699701425 543180.753202716 -109.14101162906883 44.486006715156215 +4152 2866 -66.24000000000001 18.095 220463.90309500165 228972.23173933866 +2866 4326 220463.90309500165 228972.23173933866 -66.24000000000001 18.09500000000005 +4152 2866 -67.797 17.6675 55307.87163008732 182184.18422951712 +2866 4326 55307.87163008732 182184.18422951712 -67.797 17.667500000000008 +4152 2866 -64.683 18.5225 384813.8289925946 277161.9505647868 +2866 4326 384813.8289925946 277161.9505647868 -64.683 18.522500000000022 +4152 2867 -110.375 34.17 636986.5507547572 1153309.1963629022 +2867 4326 636986.5507547572 1153309.1963629022 -110.3750106626597 34.17000533355354 +4152 2867 -111.5765 31.613999999999997 261130.53021934442 226158.4601393042 +2867 4326 261130.53021934442 226158.4601393042 -111.57651072996339 31.61400490679809 +4152 2867 -109.1735 36.726 991049.9575596204 2085058.2958645122 +2867 4326 991049.9575596204 2085058.2958645122 -109.17351063142083 36.726005759351416 +4152 2868 -111.895 34.17 706553.3934041469 1153245.5485358275 +2868 4326 706553.3934041469 1153245.5485358275 -111.89501096944797 34.170005252214466 +4152 2868 -113.2045 31.613999999999997 299111.1629138048 225689.67780187496 +2868 4326 299111.1629138048 225689.67780187496 -113.2045110476346 31.61400483453871 +4152 2868 -110.5855 36.726 1090106.1703268215 2086259.9578863946 +2868 4326 1090106.1703268215 2086259.9578863946 -110.58551092685303 36.726005671862836 +4152 2869 -113.66499999999999 34.525 725602.1919125565 1282483.876830491 +2869 4326 725602.1919125565 1282483.876830491 -113.66501134259887 34.525005202084955 +4152 2869 -114.6955 32.2975 407843.2005554682 473263.5546537977 +2869 4326 407843.2005554682 473263.5546537977 -114.6955113720565 32.29750485855689 +4152 2869 -112.6345 36.7525 1026799.7977149363 2095170.4724058916 +2869 4326 1026799.7977149363 2095170.4724058916 -112.63451134282093 36.752505546103826 +4152 2870 -122.22 40.8 6500762.569637412 2174791.409085322 +2870 4326 6500762.569637412 2174791.409085322 -122.22001352643227 40.800005244222184 +4152 2870 -124.227 39.711000000000006 5935135.343353516 1785967.7581478895 +2870 4326 5935135.343353516 1785967.7581478895 -124.22701367330524 39.71100499847447 +4152 2870 -120.213 41.888999999999996 7048248.886329139 2576456.9439488114 +2870 4326 7048248.886329139 2576456.9439488114 -120.21301336809474 41.88900550102047 +4152 2871 -121.80000000000001 39.09 6618431.056367773 2158843.097341253 +2871 4326 6618431.056367773 2158843.097341253 -121.80001322791112 39.09000514272674 +4152 2871 -123.834 38.127 6034094.980629644 1813394.5703542582 +2871 4326 6034094.980629644 1813394.5703542582 -123.83401341497996 38.12700491139173 +4152 2871 -119.766 40.053 7187039.698377048 2517236.023450566 +2871 4326 7187039.698377048 2517236.023450566 -119.76601302639789 40.053005384673746 +4152 2872 -120.425 37.72 6583359.320656051 2084639.0553885268 +2872 4326 6583359.320656051 2084639.0553885268 -120.42501284523769 37.720005125177764 +4152 2872 -122.7605 36.82899999999999 5900078.18062494 1768202.0916701525 +2872 4326 5900078.18062494 1768202.0916701525 -122.76051311081645 36.829004882510716 +4152 2872 -118.0895 38.611000000000004 7250435.109600073 2417966.60498926 +2872 4326 7250435.109600073 2417966.60498926 -118.08951255738364 38.6110053793831 +4152 2873 -118.815 36.68 6615915.005361133 2130717.8888657917 +2873 4326 6615915.005361133 2130717.8888657917 -118.81501246738881 36.680005135239306 +4152 2873 -121.6905 35.870000000000005 5764562.874933041 1846956.821754023 +2873 4326 5764562.874933041 1846956.821754023 -121.69051285083924 35.87000486825691 +4152 2873 -115.93950000000001 37.489999999999995 7449562.145374354 2439733.1965191932 +2873 4326 7449562.145374354 2439733.1965191932 -115.93951204699096 37.490005414870545 +4152 2874 -117.77000000000001 34.285 6631142.589076952 1926178.466198221 +2874 4326 6631142.589076952 1926178.466198221 -117.77001206875867 34.285004941962676 +4152 2874 -121.055 32.9125 5623806.9733687285 1440834.4849815036 +2874 4326 5623806.9733687285 1440834.4849815036 -121.05501250164174 32.91250461442096 +4152 2874 -114.485 35.6575 7605759.772572422 2443890.0068066833 +2874 4326 7605759.772572422 2443890.0068066833 -114.48501159572905 35.6575052960395 +4152 2875 -116.285 33.305 6550973.0757064605 2054604.0248969968 +2875 4326 6550973.0757064605 2054604.0248969968 -116.28501173183126 33.30500490721454 +4152 2875 -117.96350000000001 32.6075 6033992.027722262 1805152.9211946672 +2875 4326 6033992.027722262 1805152.9211946672 -117.96351197479895 32.607504737828265 +4152 2875 -114.6065 34.0025 7059785.79330495 2312340.457103632 +2875 4326 7059785.79330495 2312340.457103632 -114.60651147853586 34.00250508348426 +4152 2876 -105.55000000000001 40.285 2986050.860887343 1346670.6799931568 +2876 4326 2986050.860887343 1346670.6799931568 -105.55001020940664 40.285006484943565 +4152 2876 -108.709 39.6325 2096339.886818099 1125330.4468983235 +2876 4326 2096339.886818099 1125330.4468983235 -108.70901084128926 39.63250617567785 +4152 2876 -102.391 40.9375 3858846.993252231 1599441.7875605011 +2876 4326 3858846.993252231 1599441.7875605011 -102.39100953405021 40.93750679615229 +4152 2877 -105.55000000000001 39.115 2985813.6016209614 1466782.1270270192 +2877 4326 2985813.6016209614 1466782.1270270192 -105.55001007503591 39.11500632367665 +4152 2877 -108.709 38.2375 2078419.9419787095 1163477.0673063204 +2877 4326 2078419.9419787095 1163477.0673063204 -108.7090106842675 38.2375059934246 +4152 2877 -102.391 39.99250000000001 3871001.6992834485 1801312.7143666178 +2877 4326 3871001.6992834485 1801312.7143666178 -102.39100942408092 39.99250665988343 +4152 2878 -105.55000000000001 37.83 2985559.408843806 1423596.4157699181 +2878 4326 2985559.408843806 1423596.4157699181 -105.5500099404933 37.83000614046445 +4152 2878 -108.709 37.065 2063817.719543651 1161124.7599010922 +2878 4326 2063817.719543651 1161124.7599010922 -108.70901056486461 37.065005834690815 +4152 2878 -102.391 38.595 3888479.6750570713 1716965.8322500188 +2878 4326 3888479.6750570713 1716965.8322500188 -102.39100927477543 38.59500645175865 +4152 2879 -72.755 41.515 998630.6540483504 748374.5158726999 +2879 4326 998630.6540483504 748374.5158726999 -72.75500094218829 41.51500843776168 +4152 2879 -73.63250000000001 41.0335 756522.9195895719 574175.8351510088 +2879 4326 756522.9195895719 574175.8351510088 -73.63250119885029 41.03350831518279 +4152 2879 -71.8775 41.9965 1237175.245325916 925028.0858586932 +2879 4326 1237175.245325916 925028.0858586932 -71.87750068300454 41.996508558677014 +4152 2880 -75.38499999999999 39.144999999999996 665148.1059946908 417004.58964761323 +2880 4326 665148.1059946908 417004.58964761323 -75.38500165125696 39.145007879108 +4152 2880 -75.7585 38.5105 558349.8385451131 186093.02122606646 +2880 4326 558349.8385451131 186093.02122606646 -75.75850173398861 38.5105077364937 +4152 2880 -75.0115 39.7795 770043.2556581295 648377.5682590795 +2880 4326 770043.2556581295 648377.5682590795 -75.0115015686061 39.779508020087896 +4152 2881 -81.15 27.619999999999997 607594.4137482274 1194584.9374015871 +2881 4326 607594.4137482274 1194584.9374015871 -81.15000290517028 27.62000510243377 +4152 2881 -82.212 24.731 253886.22883335204 146285.6407118301 +2881 4326 253886.22883335204 146285.6407118301 -82.21200314076943 24.731004355213116 +4152 2881 -80.088 30.508999999999997 943369.5550423709 2246203.11233973 +2881 4326 943369.5550423709 2246203.11233973 -80.08800268711973 30.509005826799324 +4152 2882 -82.235 27.935000000000002 580288.9628426317 1309145.6717984562 +2882 4326 580288.9628426317 1309145.6717984562 -82.23500323051101 27.935005154164728 +4152 2882 -83.2295 26.4365 253822.43823780073 766265.8122870891 +2882 4326 253822.43823780073 766265.8122870891 -83.22950347547183 26.43650476398478 +4152 2882 -81.2405 29.433500000000002 897933.73025287 1854707.8988105578 +2882 4326 897933.73025287 1854707.8988105578 -81.24050298864435 29.433505540434343 +4152 2883 -84.83500000000001 30.11 1862576.6509201073 403830.911351752 +2883 4326 1862576.6509201073 403830.911351752 -84.83500405385746 30.110005600408027 +4152 2883 -87.3505 29.3 1059931.7593801322 120462.22483245008 +2883 4326 1059931.7593801322 120462.22483245008 -87.35050473485067 29.300005342775385 +4152 2883 -82.3195 30.92 2652275.477143252 704817.4257324567 +2883 4326 2652275.477143252 704817.4257324567 -82.31950335837459 30.920005860195275 +4152 2884 -82.12 32.519999999999996 670550.4079571866 916584.6819237713 +2884 4326 670550.4079571866 916584.6819237713 -82.1200033665047 32.52000623096461 +4152 2884 -83.335 30.576 288503.2986552846 211379.75685448793 +2884 4326 288503.2986552846 211379.75685448793 -83.33500364018025 30.5760057512362 +4152 2884 -80.905 34.464 1036458.0121469488 1626278.7144503533 +2884 4326 1036458.0121469488 1626278.7144503533 -80.9050030976861 34.46400670141546 +4152 2885 -84.3 32.815 2255621.7222289215 1023929.3357603531 +2885 4326 2255621.7222289215 1023929.3357603531 -84.30000401557892 32.81500622528612 +4152 2885 -85.479 30.8395 1884722.1334243573 307723.6949145625 +2885 4326 1884722.1334243573 307723.6949145625 -85.4790042667027 30.839505747350383 +4152 2885 -83.121 34.790499999999994 2610532.679871406 1744363.2918229178 +2885 4326 2610532.679871406 1744363.2918229178 -83.12100377096391 34.79050669425869 +4152 2886 -112.14 43.370000000000005 663257.2675185056 620741.4988298989 +2886 4326 663257.2675185056 620741.4988298989 -112.14001209160897 43.37000633807429 +4152 2886 -113.13 42.128 394851.7665072292 169577.62331781015 +2886 4326 394851.7665072292 169577.62331781015 -113.13001210091448 42.128006125769296 +4152 2886 -111.15 44.612 920916.5599694549 1075125.1509005656 +2886 4326 920916.5599694549 1075125.1509005656 -111.15001209124347 44.61200655180666 +4152 2887 -113.985 43.845 1644373.8672963725 793876.5285069791 +2887 4326 1644373.8672963725 793876.5285069791 -113.98501254671349 43.84500622337054 +4152 2887 -115.1685 42.1755 1323684.2431755583 187581.75456631123 +2887 4326 1323684.2431755583 187581.75456631123 -115.16851250863147 42.17550595988559 +4152 2887 -112.8015 45.514500000000005 1947644.896681241 1404810.2444999106 +2887 4326 1947644.896681241 1404810.2444999106 -112.80151260509575 45.51450648870013 +4152 2888 -115.78 45.5 2616974.4724351414 1397212.5070425826 +2888 4326 2616974.4724351414 1397212.5070425826 -115.78001320219224 45.50000620448675 +4152 2888 -117.094 42.341 2261319.223667356 248590.18829063742 +2888 4326 2261319.223667356 248590.18829063742 -117.09401289462863 42.34100581047419 +4152 2888 -114.466 48.659 2934984.2274919054 2551952.4858849845 +2888 4326 2934984.2274919054 2551952.4858849845 -114.46601362245677 48.659006589097146 +4152 2891 -84.215 38.43 1650442.841456092 338673.33995181014 +2891 4326 1650442.841456092 338673.33995181014 -84.21500431836401 38.43000739378459 +4152 2891 -85.7855 37.782000000000004 1196640.609381298 106393.11751805444 +2891 4326 1196640.609381298 106393.11751805444 -85.78550473619858 37.78200719839844 +4152 2891 -82.6445 39.077999999999996 2096194.7654858336 578645.8782868632 +2891 4326 2096194.7654858336 578645.8782868632 -82.64450389304774 39.07800758787213 +4152 2892 -85.75999999999999 37.33 1637509.1700943413 2003290.6007795555 +2892 4326 1637509.1700943413 2003290.6007795555 -85.76000469754477 37.33000711080938 +4152 2892 -89.189 36.574000000000005 630732.12259348 1746419.471613442 +2892 4326 630732.12259348 1746419.471613442 -89.18900562968464 36.574006810914064 +4152 2892 -82.331 38.086 2624312.697881749 2296380.458240027 +2892 4326 2624312.697881749 2296380.458240027 -82.33100373116714 38.08600740539495 +4152 2893 -77.22999999999999 38.849999999999994 1246831.3253763365 431036.13126735657 +2893 4326 1246831.3253763365 431036.13126735657 -77.23000221370286 38.850007755072276 +4152 2893 -79.264 38.058 660478.5982032857 150603.77057397808 +2893 4326 660478.5982032857 150603.77057397808 -79.26400279672983 38.05800751948876 +4152 2893 -75.196 39.641999999999996 1820363.7570766064 724444.4117298055 +2893 4326 1820363.7570766064 724444.4117298055 -75.19600161935715 39.64200798617263 +4152 2894 -71.68 42.175 607375.19799223 2888847.4136804407 +2894 4326 607375.19799223 2888847.4136804407 -71.68000062822291 42.17500859912396 +4152 2894 -73.318 41.5315 158412.21237031757 2659609.7996061267 +2894 4326 158412.21237031757 2659609.7996061267 -73.31800112595734 41.53150842266727 +4152 2894 -70.042 42.8185 1047350.1276101986 3126655.496255163 +2894 4326 1047350.1276101986 3126655.496255163 -70.04200012256888 42.818508770722275 +4152 2895 -70.4 41.349999999999994 1667873.4412726075 127542.84624277307 +2895 4326 1667873.4412726075 127542.84624277307 -70.40000016506394 41.3500084766361 +4152 2895 -70.859 41.205999999999996 1541629.7694533693 75262.73455290835 +2895 4326 1541629.7694533693 75262.73455290835 -70.85900030785926 41.20600843509089 +4152 2895 -69.941 41.494 1793560.5809032158 180491.00065109914 +2895 4326 1793560.5809032158 180491.00065109914 -69.94100002174876 41.49400851783567 +4152 2896 -86.93 46.7 26264282.366583254 698921.7310785229 +2896 4326 26264282.366583254 698921.7310785229 -86.93000594747525 46.7000086845763 +4152 2896 -90.071 45.242 25455797.155465834 182580.20413098446 +2896 4326 25455797.155465834 182580.20413098446 -90.07100671902083 45.24200826622652 +4152 2896 -83.789 48.158 27030608.932740275 1246683.7520799034 +2896 4326 27030608.932740275 1246683.7520799034 -83.78900513346771 48.15800909108517 +4152 2897 -84.66499999999999 44.86 19607684.49687459 562789.7398522291 +2897 4326 19607684.49687459 562789.7398522291 -84.6650050216364 44.860008526296625 +4152 2897 -86.8205 43.906 19038360.163045596 224644.31437373898 +2897 4326 19038360.163045596 224644.31437373898 -86.82050558252318 43.906008249619795 +4152 2897 -82.5095 45.814 20158579.468606047 915905.2930722877 +2897 4326 20158579.468606047 915905.2930722877 -82.50950444229349 45.81400879751056 +4152 2898 -84.66499999999999 42.955 13043497.920191662 530383.8281247463 +2898 4326 13043497.920191662 530383.8281247463 -84.66500482762356 42.9550082054715 +4152 2898 -86.9465 41.8165 12420152.374608975 126120.25837059683 +2898 4326 12420152.374608975 126120.25837059683 -86.94650540897692 41.816507888131625 +4152 2898 -82.3835 44.0935 13644416.679989409 951369.2285057498 +2898 4326 13644416.679989409 951369.2285057498 -82.38350422725992 44.09350851732397 +4152 2899 -89.03 32.51 923623.0145268167 1094873.3762410795 +2899 4326 923623.0145268167 1094873.3762410795 -89.03000534242128 32.5100059937542 +4152 2899 -89.876 30.26 655059.4682251292 277893.84981060866 +2899 4326 655059.4682251292 277893.84981060866 -89.87600547161422 30.260005481358696 +4152 2899 -88.184 34.76 1179284.0030438225 1914173.6953409754 +2899 4326 1179284.0030438225 1914173.6953409754 -88.18400522936615 34.760006493360244 +4152 2900 -90.51 33.004999999999995 2242422.280215017 1274957.169199412 +2900 4326 2242422.280215017 1274957.169199412 -90.51000577666683 33.00500604180072 +4152 2900 -91.536 31.200499999999998 1920544.1600448415 620499.1297669145 +2900 4326 1920544.1600448415 620499.1297669145 -91.53600596460582 31.200505628223606 +4152 2900 -89.48400000000001 34.8095 2551538.4111436997 1932637.7032956902 +2900 4326 2551538.4111436997 1932637.7032956902 -89.48400559705959 34.80950644993109 +4152 2901 -110.055 46.68 1829274.9252737954 886402.5258938809 +2901 4326 1829274.9252737954 886402.5258938809 -110.05501223676005 46.68000685748381 +4152 2901 -115.46849999999999 44.583 414467.64907519735 180687.97554161298 +2901 4326 414467.64907519735 180687.97554161298 -115.46851296896516 44.58300615558948 +4152 2901 -104.64150000000001 48.777 3139154.9208227736 1686810.7423968795 +2901 4326 3139154.9208227736 1686810.7423968795 -104.64151137247981 48.7770075766658 +4152 2902 -104.35499999999999 34.5 534809.6738687864 1273342.2745110546 +2902 4326 534809.6738687864 1273342.2745110546 -104.35500937799638 34.50000569819044 +4152 2902 -105.5835 32.25 154842.65894785128 456933.58544315636 +2902 4326 154842.65894785128 456933.58544315636 -105.5835095032702 32.250005271595846 +4152 2902 -103.1265 36.75 894897.835348036 2094529.8232941285 +2902 4326 894897.835348036 2094529.8232941285 -103.12650927537491 36.75000612302637 +4152 2903 -106.285 34.39 1629857.9819224146 1233301.9486272205 +2903 4326 1629857.9819224146 1233301.9486272205 -106.28500980565612 34.39000558127364 +4152 2903 -107.58550000000001 32.041000000000004 1226599.187120218 381209.5387058881 +2903 4326 1226599.187120218 381209.5387058881 -107.5855099295942 32.041005148843226 +4152 2903 -104.9845 36.739 2011214.6405028943 2090727.895403737 +2903 4326 2011214.6405028943 2090727.895403737 -104.98450970728973 36.73900601332066 +4152 2904 -107.69 34.165 2766448.093591952 1151472.736769464 +2904 4326 2766448.093591952 1151472.736769464 -107.69001009659374 34.1650054738478 +4152 2904 -108.923 31.6135 2383886.902199309 224840.0554118961 +2904 4326 2383886.902199309 224840.0554118961 -108.92301018810653 31.613505022795174 +4152 2904 -106.457 36.716499999999996 3126490.517471582 2083018.8457889084 +2904 4326 3126490.517471582 2083018.8457889084 -106.45701003864123 36.716505922721204 +4152 2905 -74.55000000000001 42.95 478739.3814187781 1499731.5761545855 +2905 4326 478739.3814187781 1499731.5761545855 -74.55000161560929 42.95000864949415 +4152 2905 -75.738 41.087 150875.69764539006 823315.0615341639 +2905 4326 150875.69764539006 823315.0615341639 -75.73800187840901 41.08700825285989 +4152 2905 -73.36200000000001 44.813 787433.2344381205 2180852.0632972782 +2905 4326 787433.2344381205 2180852.0632972782 -73.36200135086408 44.81300903037544 +4152 2906 -76.405 43.2 867758.4852460751 1166018.0672500338 +2906 4326 867758.4852460751 1166018.0672500338 -76.40500223712196 43.20000862567463 +4152 2906 -77.6155 42.111000000000004 540149.3326531219 770792.6591996523 +2906 4326 540149.3326531219 770792.6591996523 -77.6155025474734 42.11100837640432 +4152 2906 -75.1945 44.288999999999994 1183869.9012600197 1565987.6461074888 +2906 4326 1183869.9012600197 1565987.6461074888 -75.19450192156799 44.28900886935674 +4152 2907 -78.565 42.815 1153210.6242120317 1025652.9812480954 +2907 4326 1153210.6242120317 1025652.9812480954 -78.56500290357539 42.815008467291705 +4152 2907 -79.64949999999999 42.072500000000005 858832.283273011 756876.4538784421 +2907 4326 858832.283273011 756876.4538784421 -79.64950319015514 42.07250828484654 +4152 2907 -77.4805 43.5575 1440625.485810547 1298207.173722629 +2907 4326 1440625.485810547 1298207.173722629 -77.48050261280203 43.557508647100825 +4152 2908 -73.03 40.885 1252460.5525647984 263192.27519998315 +2908 4326 1252460.5525647984 263192.27519998315 -73.03000099593335 40.8850083055393 +4152 2908 -74.137 40.5115 946155.1353312435 125659.67980443442 +2908 4326 946155.1353312435 125659.67980443442 -74.1370013311976 40.51150819554129 +4152 2908 -71.923 41.2585 1555283.8917496786 404564.1584891662 +2908 4326 1555283.8917496786 404564.1584891662 -71.92300065741367 41.25850841371884 +4152 2909 -100.44999999999999 48.08 1980725.9056554495 393951.8534337165 +2909 4326 1980725.9056554495 393951.8534337165 -100.45001012966675 48.08000787478018 +4152 2909 -103.708 47.242999999999995 1171852.468078186 105237.278882084 +2909 4326 1171852.468078186 105237.278882084 -103.70801082570809 47.243007495763436 +4152 2909 -97.192 48.917 2763745.8760258164 716386.7939064692 +2909 4326 2763745.8760258164 716386.7939064692 -97.19200938163863 48.91700824831343 +4152 2910 -100.3 46.879999999999995 2018517.8260990612 442551.6599465382 +2910 4326 2018517.8260990612 442551.6599465382 -100.30000986985306 46.88000774365385 +4152 2910 -103.675 46.025 1162149.5097543914 146978.76648637702 +2910 4326 1162149.5097543914 146978.76648637702 -103.67501059551807 46.025007358538076 +4152 2910 -96.925 47.735 2848006.7882052157 774362.3047026764 +2910 4326 2848006.7882052157 774362.3047026764 -96.92500909016327 47.73500812378931 +4152 2911 -98.71000000000001 36.14 1758858.593240887 415746.68485871307 +2911 4326 1758858.593240887 415746.68485871307 -98.71000814466498 36.14000626517363 +4152 2911 -102.571 35.357 605894.9084370023 162038.6840351809 +2911 4326 605894.9084370023 162038.6840351809 -102.57100902726768 35.35700593036932 +4152 2911 -94.849 36.923 2889489.353144984 714986.4863933682 +2911 4326 2889489.353144984 714986.4863933682 -94.84900720716108 36.923006602982234 +4152 2912 -97.21000000000001 34.595 2206248.2204550225 460077.23201637913 +2912 4326 2206248.2204550225 460077.23201637913 -97.21000764983027 34.59500606565183 +4152 2912 -99.721 33.7175 1445145.5172335398 144270.90169678623 +2912 4326 1445145.5172335398 144270.90169678623 -99.72100822026755 33.71750579026642 +4152 2912 -94.699 35.4725 2951316.764172193 794603.0416681629 +2912 4326 2951316.764172193 794603.0416681629 -94.6990070584319 35.47250634578627 +4152 2913 -120.32 45.105000000000004 8248572.7967865 524426.316329996 +2913 4326 8248572.7967865 524426.316329996 -120.32001395163995 45.105005731048735 +4152 2913 -123.785 44.0655 7338794.745199684 162963.63331663187 +2913 4326 7338794.745199684 162963.63331663187 -123.78501430704941 44.06550532370581 +4152 2913 -116.855 46.1445 9125766.198555255 924240.7538895387 +2913 4326 9125766.198555255 924240.7538895387 -116.85501353758906 46.14450615108872 +4152 2914 -120.75 43.269999999999996 4854679.729683393 584424.3756901834 +2914 4326 4854679.729683393 584424.3756901834 -120.75001368440185 43.27000555881456 +4152 2914 -124.21499999999999 42.108999999999995 3913439.4614197942 183569.23273580152 +2914 4326 3913439.4614197942 183569.23273580152 -124.21501402865582 42.10900516181532 +4152 2914 -117.28500000000001 44.431000000000004 5761028.256343373 1023660.1799985798 +2914 4326 5761028.256343373 1023660.1799985798 -117.28501328506809 44.431005972740245 +4152 2915 -85.98 35.83 1974428.6061747835 544794.8859764934 +2915 4326 1974428.6061747835 544794.8859764934 -85.98000466448423 35.830006800932686 +4152 2915 -89.87700000000001 35.065 808512.1567861795 289316.0841076882 +2915 4326 808512.1567861795 289316.0841076882 -89.87700572237117 35.06500648441918 +4152 2915 -82.083 36.595 3118164.3311864496 846296.7747777214 +2915 4326 3118164.3311864496 846296.7747777214 -82.08300356427127 36.59500711206894 +4152 2916 -101.50999999999999 35.4 653186.5676299729 3790383.7365547083 +2916 4326 653186.5676299729 3790383.7365547083 -101.51000877684982 35.40000599319395 +4152 2916 -102.878 34.41 240500.23425651703 3432961.649287385 +2916 4326 240500.23425651703 3432961.649287385 -102.87800902804939 34.4100057581895 +4152 2916 -100.142 36.39 1055901.4972496596 4153505.143594256 +2916 4326 1055901.4972496596 4153505.143594256 -100.14200852238555 36.39000623131893 +4152 2917 -98.535 33.15 1957788.3895362925 7101321.754525732 +2917 4326 1957788.3895362925 7101321.754525732 -98.53500789013707 33.15000574042905 +4152 2917 -102.61649999999999 31.863 690641.5541047071 6658138.523761735 +2917 4326 690641.5541047071 6658138.523761735 -102.616508801225 31.863005333084832 +4152 2917 -94.4535 34.437 3188573.937926682 7593156.547892988 +2917 4326 3188573.937926682 7593156.547892988 -94.45350692326356 34.43700616236057 +4152 2918 -100.08 31.025000000000002 2375931.0078311823 10336611.703458842 +2918 4326 2375931.0078311823 10336611.703458842 -100.08000815039709 31.025005281578768 +4152 2918 -106.002 29.904500000000002 501084.05560280825 9974761.259641787 +2918 4326 501084.05560280825 9974761.259641787 -106.00200946823989 29.90450485856436 +4152 2918 -94.158 32.145500000000006 4207180.662936552 10797158.523251537 +2918 4326 4207180.662936552 10797158.523251537 -94.1580067103587 32.14550572672265 +4152 2919 -99.38 29.225 1847301.8679563636 13629544.66048115 +2919 4326 1847301.8679563636 13629544.66048115 -99.38000789596624 29.225004960286075 +4152 2919 -104.438 27.924500000000002 212733.5223560003 13197312.465758525 +2919 4326 212733.5223560003 13197312.465758525 -104.43800903523291 27.924504556573833 +4152 2919 -94.322 30.5255 3441205.422198952 14131739.879325563 +2919 4326 3441205.422198952 14131739.879325563 -94.32200667333103 30.525505391314308 +4152 2920 -98.525 27.02 976111.3278539426 16896088.26345105 +2920 4326 976111.3278539426 16896088.26345105 -98.52500760513941 27.020004539962933 +4152 2920 -100.0325 25.948999999999998 480634.4621353835 16509854.80714216 +2920 4326 480634.4621353835 16509854.80714216 -100.03250794189627 25.949004283094204 +4152 2920 -97.0175 28.091 1462288.4670416447 17288247.517767303 +2920 4326 1462288.4670416447 17288247.517767303 -97.0175072654353 28.09100480251846 +4152 2921 -111.54 41.28 1629425.9167406955 3625748.2152042827 +2921 4326 1629425.9167406955 3625748.2152042827 -111.54001165412147 41.280006162212494 +4152 2921 -113.79 40.623 1004777.0584341529 3394751.5548022916 +2921 4326 1004777.0584341529 3394751.5548022916 -113.79001201836172 40.623005913442896 +4152 2921 -109.29 41.937 2241686.854663251 3872791.005300402 +2921 4326 2241686.854663251 3872791.005300402 -109.2900112670251 41.9370064151121 +4152 2922 -111.545 39.785 1627774.4115609722 7090428.804545079 +2922 4326 1627774.4115609722 7090428.804545079 -111.5450114594936 39.78500598942574 +4152 2922 -113.7995 38.6195 983391.7680608317 6674363.952587393 +2922 4326 983391.7680608317 6674363.952587393 -113.79951176992728 38.61950569079333 +4152 2922 -109.29050000000001 40.9505 2250765.3260073643 7522550.713730533 +2922 4326 2250765.3260073643 7522550.713730533 -109.29051113244297 40.95050629731785 +4152 2923 -111.545 37.785 1627415.4240844045 10249729.178458244 +2923 4326 1627415.4240844045 10249729.178458244 -111.54501123019124 37.78500574623371 +4152 2923 -113.7995 37.069500000000005 969550.1494510765 9997448.321856607 +2923 4326 969550.1494510765 9997448.321856607 -113.79951160149827 37.069505509610316 +4152 2923 -109.29050000000001 38.500499999999995 2272727.0324092847 10517754.31046072 +2923 4326 2272727.0324092847 10517754.31046072 -109.29051083754662 38.50050598913477 +4152 2924 -78.285 38.615 11544347.656693192 6907090.8251683945 +2924 4326 11544347.656693192 6907090.8251683945 -78.28500252796417 38.61500766912454 +4152 2924 -79.88250000000001 37.8545 11083745.13895743 6633075.237706761 +2924 4326 11083745.13895743 6633075.237706761 -79.88250297400525 37.85450745474152 +4152 2924 -76.6875 39.3755 11995292.747702997 7189062.41980253 +2924 4326 11995292.747702997 7189062.41980253 -76.6875020746976 39.37550788064867 +4152 2925 -79.495 37.41 11193933.021261292 3674360.1158166635 +2925 4326 11193933.021261292 3674360.1158166635 -79.49500282952783 37.41000737771253 +4152 2925 -83.26150000000001 36.627 10086194.333201073 3422986.0149178724 +2925 4326 10086194.333201073 3422986.0149178724 -83.26150391710189 36.62700707285759 +4152 2925 -75.7285 38.193 12279389.374483127 3969652.6620305968 +2925 4326 12279389.374483127 3969652.6620305968 -75.72850170752896 38.193007671743395 +4152 2926 -120.905 48.065 1622893.336592662 388488.2872352653 +2926 4326 1622893.336592662 388488.2872352653 -120.9050147012095 48.06500585790535 +4152 2926 -124.4015 47.1785 753283.1432020159 85682.59365874318 +2926 4326 753283.1432020159 85682.59365874318 -124.40151505418174 47.17850542878113 +4152 2926 -117.4085 48.951499999999996 2463163.581283079 730203.9302677546 +2926 4326 2463163.581283079 730203.9302677546 -117.40851428307445 48.95150629481487 +4152 2927 -120.655 46.575 1601437.7114832425 452829.2448668882 +2927 4326 1601437.7114832425 452829.2448668882 -120.65501431417754 46.57500579449115 +4152 2927 -124.02550000000001 45.643499999999996 738951.1663367104 133258.40356679758 +2927 4326 738951.1663367104 133258.40356679758 -124.02551465649795 45.64350538981438 +4152 2927 -117.2845 47.5065 2434972.0471441783 808725.3564848994 +2927 4326 2434972.0471441783 808725.3564848994 -117.28451391386133 47.50650620877364 +4152 2928 -90.47 46.34 1849792.6205940612 428206.0430836391 +2928 4326 1849792.6205940612 428206.0430836391 -90.47000698486964 46.340008404400656 +4152 2928 -92.648 45.467 1289206.367828562 120839.77133789152 +2928 4326 1289206.367828562 120839.77133789152 -92.64800750866706 45.467008129746326 +4152 2928 -88.292 47.213 2393010.4543253123 750822.4822996728 +2928 4326 2393010.4543253123 750822.4822996728 -88.29200644059216 47.21300867507545 +4152 2929 -89.57 44.89 2079939.3338098514 385515.57975344936 +2929 4326 2079939.3338098514 385515.57975344936 -89.57000652512149 44.8900082436984 +4152 2929 -92.558 44.071 1296268.2423315719 97233.35493690065 +2929 4326 1296268.2423315719 97233.35493690065 -92.55800729919869 44.07100792678273 +4152 2929 -86.582 45.708999999999996 2841490.0879038563 702216.019093216 +2929 4326 2841490.0879038563 702216.019093216 -86.58200571521746 45.7090085531398 +4152 2930 -89.19 43.405 2183746.5303250942 513124.4753014687 +2930 4326 2183746.5303250942 513124.4753014687 -89.19000623814526 43.405008030614795 +4152 2930 -91.206 42.5725 1643639.9265706283 211011.95786028268 +2930 4326 1643639.9265706283 211011.95786028268 -91.20600673431078 42.57250777486334 +4152 2930 -87.174 44.2375 2709056.968022254 828089.9825322251 +2930 4326 2709056.968022254 828089.9825322251 -87.17400572629828 44.237508283738315 +4213 2931 11.355 14.75 322921.3570828067 1631177.9037758943 +2931 4326 322921.3570828067 1631177.9037758943 11.354401650745494 14.750370753105027 +4213 2931 8.1645 12.995000000000001 -24953.588323029107 1441444.1666062027 +2931 4326 -24953.588323029107 1441444.1666062027 8.163844949280797 12.99551396972231 +4213 2931 14.5455 16.505 664954.2804930811 1825268.9259319704 +2931 4326 664954.2804930811 1825268.9259319704 14.544960576104842 16.505232593916755 +4614 2932 51.185 25.375 196812.91617469914 402464.96981607616 +2932 4326 196812.91617469914 402464.96981607616 51.18422459623016 25.375699820365885 +4614 2932 50.7395 24.6325 151685.78749691183 320298.833374604 +2932 4326 151685.78749691183 320298.833374604 50.738709487107954 24.633175327268823 +4614 2932 51.6305 26.1175 241392.29427306505 484788.34421075555 +2932 4326 241392.29427306505 484788.34421075555 51.62973983830989 26.1182246187858 +4613 2933 117.35499999999999 -0.62 539495.9869677778 9931476.589551838 +2933 4326 539495.9869677778 9931476.589551838 117.35539927177163 -0.619523983428034 +4613 2933 116.7835 -1.178 475916.7644059967 9869806.967576027 +2933 4326 475916.7644059967 9869806.967576027 116.78397056026996 -1.1774657950472722 +4613 2933 117.92649999999999 -0.062000000000000055 603088.7573653554 9993146.89201726 +2933 4326 603088.7573653554 9993146.89201726 117.92682799609933 -0.06158230025150396 +4284 2935 41.515 42.480000000000004 1298492.5127480766 4692136.988007941 +2935 4326 1298492.5127480766 4692136.988007941 41.513687280824755 42.47988229768603 +4284 2935 40.1425 41.480999999999995 1183833.4458046895 4582107.084124254 +2935 4326 1183833.4458046895 4582107.084124254 40.14119238865831 41.480845996329826 +4284 2935 42.8875 43.479000000000006 1409560.8424134469 4804010.949548841 +2935 4326 1409560.8424134469 4804010.949548841 42.88618238174412 43.47891902711077 +4284 2936 44.535 40.959999999999994 2300140.312395096 4523310.769084462 +2936 4326 2300140.312395096 4523310.769084462 44.53374350310985 40.95989945865282 +4284 2936 43.1805 39.079 2182935.1251101303 4315321.582487015 +2936 4326 2182935.1251101303 4315321.582487015 43.1792648503455 39.0788482835016 +4284 2936 45.8895 42.840999999999994 2410870.504249256 4733131.897723213 +2936 4326 2410870.504249256 4733131.897723213 45.88822007387443 42.84095157304653 +4284 2937 47.535 40.24 3300141.825946999 4443356.132448241 +2937 4326 3300141.825946999 4443356.132448241 47.53380873289464 40.23992556272492 +4284 2937 46.1805 38.566 3182091.9301118106 4258367.954833917 +2937 4326 3182091.9301118106 4258367.954833917 46.17932174533357 38.565877517245504 +4284 2937 48.8895 41.914 3412513.634997032 4630155.69510136 +2937 4326 3412513.634997032 4630155.69510136 48.888294595411146 41.91397435869891 +4284 2938 50.379999999999995 40.24 4286952.010265056 4443367.409695779 +2938 4326 4286952.010265056 4443367.409695779 50.37885853824318 40.239961396594275 +4284 2938 49.165 38.125 4180015.453603092 4209431.527478022 +2938 4326 4180015.453603092 4209431.527478022 49.16387990111668 38.124906059296514 +4284 2938 51.595 42.355000000000004 4387471.237327415 4678797.430214866 +2938 4326 4387471.237327415 4678797.430214866 51.59383451308696 42.35501752342724 +4284 2939 50.765 46.46 2299871.9664193117 5132564.796763968 +2939 4326 2299871.9664193117 5132564.796763968 50.7637127063615 46.46008543373442 +4284 2939 49.4105 41.681 2187078.0048607597 4602432.328647688 +2939 4326 2187078.0048607597 4602432.328647688 49.40930933947887 41.68097668718546 +4284 2939 52.1195 51.239000000000004 2394476.726583564 5664900.673429441 +2939 4326 2394476.726583564 5664900.673429441 52.118087250463994 51.23919481418751 +4284 2940 53.765 46.625 3299872.3537135664 5150906.8268164 +2940 4326 3299872.3537135664 5150906.8268164 53.76380090166013 46.625126173771434 +4284 2940 52.4105 41.9765 3187596.222001188 4635255.101720608 +2940 4326 3187596.222001188 4635255.101720608 52.40938787114054 41.97651674479832 +4284 2940 55.1195 51.2735 3394406.032394567 5668738.742582975 +2940 4326 3394406.032394567 5668738.742582975 55.11818851195995 51.27373582740643 +4284 2941 56.765 46.2 4299871.358309375 5103663.282628399 +2941 4326 4299871.358309375 5103663.282628399 56.763878910538736 46.20015582027392 +4284 2941 55.4105 41.754 4187205.7444748757 4610540.682205409 +2941 4326 4187205.7444748757 4610540.682205409 55.409453077705145 41.75404848526971 +4284 2941 58.1195 50.646 4395686.4226686545 5598933.746510169 +2941 4326 4395686.4226686545 5598933.746510169 58.11828398713186 50.646263152900524 +4615 2942 -16.77 32.75 334171.00412245234 3625013.5176809514 +2942 4326 334171.00412245234 3625013.5176809514 -16.77408040983978 32.75361270973591 +4615 2942 -17.256 32.39 287782.8966886513 3585958.5374633074 +2942 4326 287782.8966886513 3585958.5374633074 -17.260100589101686 32.39359174798246 +4615 2942 -16.284 33.11 380193.8333341968 3664271.081612069 +2942 4326 380193.8333341968 3664271.081612069 -16.288059934691805 33.11363378003607 +4616 2943 -15.95 30.095 408456.0652234812 3329741.2831421304 +2943 4326 408456.0652234812 3329741.2831421304 -15.952060737188324 30.095857097200234 +4616 2943 -16.094 29.991500000000002 394469.48264857155 3318395.403635959 +2943 4326 394469.48264857155 3318395.403635959 -16.096064939355312 29.99235339632354 +4616 2943 -15.806 30.1985 422413.73517715855 3341104.355797154 +2943 4326 422413.73517715855 3341104.355797154 -15.808056519957608 30.199360809189482 +4617 2945 -58.55 51.44 301323.7648052591 5700467.898256849 +2945 4326 301323.7648052591 5700467.898256849 -58.54999641658792 51.44001044323566 +4617 2945 -59.855 47.894 203486.75376547498 5306999.7403105125 +2945 4326 203486.75376547498 5306999.7403105125 -59.85499681945365 47.894009881352645 +4617 2945 -57.245000000000005 54.986000000000004 385130.37002827233 6095782.81105129 +2945 4326 385130.37002827233 6095782.81105129 -57.24499596017485 54.986010929633245 +4617 2946 -61.5 53.04 304800.0 5878483.934355941 +2946 4326 304800.0 5878483.934355941 -61.49999758041778 53.04001063908652 +4617 2946 -62.85 47.748 203577.13712667965 5290761.97413933 +2946 4326 203577.13712667965 5290761.97413933 -62.84999788102746 47.748009816233946 +4617 2946 -60.15 58.332 383877.9519267481 6468407.153778225 +2946 4326 383877.9519267481 6468407.153778225 -60.14999722714843 58.33201130059383 +4617 2947 -64.5 54.235 304800.0 6011472.0107470285 +2947 4326 304800.0 6011472.0107470285 -64.49999880325238 54.23501074713336 +4617 2947 -65.85 48.578500000000005 205198.3311343926 5383096.143461107 +2947 4326 205198.3311343926 5383096.143461107 -65.84999898732151 48.578509892495795 +4617 2947 -63.15 59.8915 380365.5001219588 6642090.663385094 +2947 4326 380365.5001219588 6642090.663385094 -63.149998613492556 59.891511419490584 +4617 2948 -67.5 53.155 304800.0 5891280.786565086 +2948 4326 304800.0 5891280.786565086 -67.49999988844363 53.15501053012279 +4617 2948 -68.85 47.8945 203861.5683617214 5307048.778975857 +2948 4326 203861.5683617214 5307048.778975857 -68.85000001177592 47.89450970317532 +4617 2948 -66.15 58.4155 383691.36466289486 6477705.645014932 +2948 4326 383691.36466289486 6477705.645014932 -66.14999979345954 58.41551120319859 +4617 2949 -70.5 53.405 304800.0 5919100.887776715 +2949 4326 304800.0 5919100.887776715 -70.50000105340162 53.4050104691951 +4617 2949 -71.85 45.8495 199952.14056091162 5079736.298233002 +2949 4326 199952.14056091162 5079736.298233002 -71.85000091319596 45.84950926436181 +4617 2949 -69.15 60.960499999999996 377924.8926710408 6761169.233849469 +2949 4326 377924.8926710408 6761169.233849469 -69.15000140582629 60.96051136352718 +4617 2950 -73.5 53.754999999999995 304800.0 5958050.99760996 +2950 4326 304800.0 5958050.99760996 -73.50000222886214 53.755010399008334 +4617 2950 -74.85 45.857499999999995 199967.17535427015 5080625.389969004 +2950 4326 199967.17535427015 5080625.389969004 -74.8500019280226 45.85750915651665 +4617 2950 -72.15 61.6525 376331.2017690007 6838262.451047374 +2950 4326 376331.2017690007 6838262.451047374 -72.15000287483912 61.652511307762914 +4617 2951 -76.5 53.14 304800.0 5889611.617824443 +2951 4326 304800.0 5889611.617824443 -76.50000326994461 53.14001018225975 +4617 2951 -77.85 44.581 197594.40722045692 4938774.634699871 +2951 4326 197594.40722045692 4938774.634699871 -77.850002816103 44.58100881268658 +4617 2951 -75.15 61.699 376223.72955384525 6843443.115298966 +2951 4326 376223.72955384525 6843443.115298966 -75.15000423369062 61.699011169452675 +4617 2952 -79.5 52.355000000000004 304800.0 5802264.371886736 +2952 4326 304800.0 5802264.371886736 -79.50000424846525 52.35500992387255 +4617 2952 -80.85 43.2695 195212.3497160519 4793065.827792506 +2952 4326 195212.3497160519 4793065.827792506 -80.85000366755658 43.26950844772227 +4617 2952 -78.15 61.4405 376820.5765459477 6814643.503802963 +2952 4326 376820.5765459477 6814643.503802963 -78.15000549538196 61.44051098121094 +4617 2953 -66.375 46.315 2509626.993519572 7479444.931300374 +2953 4326 2509626.993519572 7479444.931300374 -66.37499905754164 46.315009501889456 +4617 2953 -68.7825 44.7355 2319191.9035818283 7306463.050455528 +2953 4326 2319191.9035818283 7306463.050455528 -68.7824998024783 44.73550916101564 +4617 2953 -63.9675 47.8945 2689366.068220979 7658098.364020295 +2953 4326 2689366.068220979 7658098.364020295 -63.96749828391825 47.894509820486746 +4617 2954 -63.19499999999999 46.495000000000005 385030.60045041575 716092.6605064794 +2954 4326 385030.60045041575 716092.6605064794 -63.19499795667182 46.49500959817647 +4617 2954 -64.3605 45.9595 294533.1514411849 657460.5291834144 +2954 4326 294533.1514411849 657460.5291834144 -64.36049834216387 45.95950948340248 +4617 2954 -62.0295 47.0305 473763.0329879282 776057.4583061151 +2954 4326 473763.0329879282 776057.4583061151 -62.029497565976605 47.03050970882691 +4617 2955 -117.0 66.245 500000.0 7347218.876508807 +2955 4326 500000.0 7347218.876508807 -117.0000229205062 66.24500696458442 +4617 2955 -119.7 50.7155 309399.7445906481 5621665.37102995 +2955 4326 309399.7445906481 5621665.37102995 -119.70001519173344 50.71550614067253 +4617 2955 -114.3 81.77449999999999 543110.4436380728 9080662.632243542 +2955 4326 543110.4436380728 9080662.632243542 -114.3000653585317 81.7745070567852 +4617 2956 -111.0 66.495 500000.0 7375085.928094508 +2956 4326 500000.0 7375085.928094508 -111.00002131309523 66.49500785384772 +4617 2956 -113.7 50.740500000000004 309501.21335740504 5624444.723641862 +2956 4326 309501.21335740504 5624444.723641862 -113.70001399366477 50.740506824511414 +4617 2956 -108.3 82.2495 540636.8512974724 9133629.104307733 +2956 4326 540636.8512974724 9133629.104307733 -108.30006399602482 82.24950800327703 +4617 2957 -105.0 66.495 499999.9999999994 7375085.928094508 +2957 4326 499999.9999999994 7375085.928094508 -105.00001919948558 66.49500867610777 +4617 2957 -107.7 50.740500000000004 309501.2133574041 5624444.723641862 +2957 4326 309501.2133574041 5624444.723641862 -107.70001259954 50.74050746421949 +4617 2957 -102.3 82.2495 540636.8512974723 9133629.104307733 +2957 4326 540636.8512974723 9133629.104307733 -102.30005776747427 82.24950889295488 +4617 2958 -81.0 62.835 500000.0 6967205.614257044 +2958 4326 500000.0 6967205.614257044 -81.00000715848398 62.83501090820811 +4617 2958 -83.7 43.786500000000004 282751.64517490444 4851703.725921316 +2958 4326 282751.64517490444 4851703.725921316 -83.70000461003711 43.78650839811487 +4617 2958 -78.3 81.8835 542543.079010169 9092816.959001321 +2958 4326 542543.079010169 9092816.959001321 -78.30002561088041 81.88351147602843 +4617 2959 -75.0 63.815 500000.0 7076399.368382286 +2959 4326 500000.0 7076399.368382286 -75.0000046887308 63.81501134177077 +4617 2959 -77.7 45.648500000000006 289614.5290814074 5058540.583399849 +2959 4326 289614.5290814074 5058540.583399849 -77.70000285968364 45.64850900244455 +4617 2959 -72.3 81.9815 542032.8378381082 9103744.745874293 +2959 4326 542032.8378381082 9103744.745874293 -72.30001749163492 81.98151183255924 +4617 2960 -69.0 62.4 500000.0 6918741.843632851 +2960 4326 500000.0 6918741.843632851 -69.00000155829402 62.4000114934502 +4617 2960 -71.7 42.959999999999994 279778.81803513714 4759910.374057942 +2960 4326 279778.81803513714 4759910.374057942 -71.70000067960262 42.96000874761273 +4617 2960 -66.3 81.84 542769.5232055377 9087966.372905465 +2960 4326 542769.5232055377 9087966.372905465 -66.30000858872855 81.84001207445607 +4617 2961 -62.995000000000005 61.105000000000004 500269.5360667032 6774482.828320475 +2961 4326 500269.5360667032 6774482.828320475 -62.99499863142134 61.105011536920145 +4617 2961 -65.6995 40.4995 271245.84974719933 4486700.41001962 +2961 4326 271245.84974719933 4486700.41001962 -65.69949858560784 40.49950842204722 +4617 2961 -60.2905 81.7105 543596.2492053718 9073533.311533127 +2961 4326 543596.2492053718 9073533.311533127 -60.29049984717633 81.71051219069528 +4617 2962 -57.0 61.28 500000.0000000007 6793975.671579958 +2962 4326 500000.0000000007 6793975.671579958 -56.9999958876005 61.28001160557 +4617 2962 -59.7 40.832 272337.899245773 4523616.464762851 +2962 4326 272337.899245773 4523616.464762851 -59.69999661304671 40.832008587605344 +4617 2962 -54.3 81.72800000000001 543352.4364263249 9075477.545223918 +2962 4326 543352.4364263249 9075477.545223918 -54.29999127236791 81.72801217578834 +4267 2964 -158.78500000000003 61.35 -836300.6331388989 4174807.575987167 +2964 4326 -836300.6331388989 4174807.575987167 -158.7873856172306 61.34928812704083 +4267 2964 175.2995 52.305 -6656792.749618161 2402508.2678126073 +2964 4326 -6656792.749618161 2402508.2678126073 175.29753356552675 52.3037603189447 +4267 2964 -132.86950000000002 70.39500000000001 2602885.54471815 7859069.322781744 +2964 4326 2602885.54471815 7859069.322781744 -132.8720494355539 70.39497236210343 +4269 2965 -85.685 39.86 322936.7085062442 1679700.2348060838 +2965 4326 322936.7085062442 1679700.2348060838 -85.685 39.85999999999999 +4269 2965 -86.4995 38.141000000000005 88557.22252511335 1054695.7661019482 +2965 4326 88557.22252511335 1054695.7661019482 -86.4995 38.141 +4269 2965 -84.8705 41.579 545911.1815363766 2306969.3356002825 +2965 4326 545911.1815363766 2306969.3356002825 -84.8705 41.579 +4269 2966 -87.16999999999999 39.77 2928388.7483260697 1646927.8842544204 +2966 4326 2928388.7483260697 1646927.8842544204 -87.16999999999999 39.77 +4269 2966 -88.00699999999999 37.970000000000006 2686480.4781377935 992671.1032974891 +2966 4326 2686480.4781377935 992671.1032974891 -88.00699999999999 37.97000000000001 +4269 2966 -86.333 41.57 3158066.4524528016 2303577.617854659 +2966 4326 3158066.4524528016 2303577.617854659 -86.333 41.57 +4152 2967 -85.685 39.86 322936.7085062442 1679700.2348060838 +2967 4326 322936.7085062442 1679700.2348060838 -85.68500486291144 39.86000759904122 +4152 2967 -86.4995 38.141000000000005 88557.22252511335 1054695.7661019482 +2967 4326 88557.22252511335 1054695.7661019482 -86.4995049704354 38.14100723589421 +4152 2967 -84.8705 41.579 545911.1815363766 2306969.3356002825 +2967 4326 545911.1815363766 2306969.3356002825 -84.87050476373865 41.5790079526695 +4152 2968 -87.16999999999999 39.77 2928388.7483260697 1646927.8842544204 +2968 4326 2928388.7483260697 1646927.8842544204 -87.17000529338411 39.770007510184335 +4152 2968 -88.00699999999999 37.970000000000006 2686480.4781377935 992671.1032974891 +2968 4326 2686480.4781377935 992671.1032974891 -88.0070053940648 37.97000713343579 +4152 2968 -86.333 41.57 3158066.4524528016 2303577.617854659 +2968 4326 3158066.4524528016 2303577.617854659 -86.33300520290537 41.57000787692553 +4621 2969 -62.97 17.995 503176.09750867356 1989656.9721944635 +2969 4326 503176.09750867356 1989656.9721944635 -62.96778357353299 17.991263977753302 +4621 2969 -63.186 17.8375 480290.7660675296 1972241.2170684086 +2969 4326 480290.7660675296 1972241.2170684086 -63.183791188184074 17.833763003899566 +4621 2969 -62.754 18.152500000000003 526020.8522096544 2007099.7003304474 +2969 4326 526020.8522096544 2007099.7003304474 -62.75177596019172 18.148764943014147 +4622 2970 -61.41 16.175 669993.1749763899 1788971.6792048626 +2970 4326 669993.1749763899 1788971.6792048626 -61.413907392299436 16.172481209170392 +4622 2970 -61.806000000000004 15.8375 627863.7698180027 1751345.2007872793 +2970 4326 627863.7698180027 1751345.2007872793 -61.80991335871278 15.83496464374687 +4622 2970 -61.013999999999996 16.5125 711979.5970680584 1826693.0868159984 +2970 4326 711979.5970680584 1826693.0868159984 -61.01790134986963 16.509998176093326 +4623 2971 -52.805 4.62 299756.132397795 510919.63219667407 +2971 4326 299756.132397795 510919.63219667407 -52.80508221536522 4.621074636401792 +4623 2971 -53.8805 3.549 179935.73129778565 392778.80550367513 +2971 4326 179935.73129778565 392778.80550367513 -53.880632038984835 3.5500564897026154 +4623 2971 -51.7295 5.690999999999999 419217.01054318843 629105.1498922051 +2971 4326 419217.01054318843 629105.1498922051 -51.729532241548135 5.692092517656085 +4624 2972 -51.725 5.525 419695.8329195396 610746.3962355925 +2972 4326 419695.8329195396 610746.3962355925 -51.725 5.525 +4624 2972 -53.7725 2.5055 191662.41825084813 277261.1172179439 +2972 4326 191662.41825084813 277261.1172179439 -53.7725 2.5055 +4624 2972 -49.6775 8.5445 645550.9544133474 944743.5920086084 +2972 4326 645550.9544133474 944743.5920086084 -49.6775 8.544500000000001 +4625 2973 -61.025 14.64 712723.8705159333 1619455.0032798427 +2973 4326 712723.8705159333 1619455.0032798427 -61.02132093997964 14.641673452188433 +4625 2973 -61.2635 14.379 687247.4122916895 1590365.9762488788 +2973 4326 687247.4122916895 1590365.9762488788 -61.25984903977021 14.38067509906909 +4625 2973 -60.7865 14.901 738138.2621526483 1648578.97848274 +2973 4326 738138.2621526483 1648578.97848274 -60.78279276992478 14.902671653544244 +4627 2975 56.120000000000005 -21.5 408848.05172735854 7622257.793568539 +2975 4326 408848.05172735854 7622257.793568539 56.120000000000005 -21.5 +4627 2975 54.212 -24.398 217234.431555892 7298864.867636135 +2975 4326 217234.431555892 7298864.867636135 54.21200000000001 -24.397999999999996 +4627 2975 58.028 -18.602 608456.1930996997 7942899.039114159 +2975 4326 608456.1930996997 7942899.039114159 58.028 -18.602000000000004 +4628 2976 -149.54500000000002 -17.67 229999.04500941955 8044478.9303066805 +2976 4326 229999.04500941955 8044478.9303066805 -149.54517667738662 -17.668743626460643 +4628 2976 -149.9545 -17.904 186934.05287078256 8017928.261517083 +2976 4326 186934.05287078256 8017928.261517083 -149.95469032209567 -17.9027465192471 +4628 2976 -149.1355 -17.436 273170.01637262764 8070920.948771764 +2976 4326 273170.01637262764 8070920.948771764 -149.13566306183753 -17.434740741115384 +4629 2977 -151.39999999999998 -16.565 670722.8045897741 8167864.03686033 +2977 4326 670722.8045897741 8167864.03686033 -151.40251990690763 -16.564457885068652 +4629 2977 -151.859 -16.9205 621514.4658648348 8128864.832278743 +2977 4326 621514.4658648348 8128864.832278743 -151.86153834503577 -16.919961328857738 +4629 2977 -150.94099999999997 -16.209500000000002 720113.8063296934 8206764.801209679 +2977 4326 720113.8063296934 8206764.801209679 -150.94350148123914 -16.208954244355287 +4630 2978 -139.875 -9.145 623616.1034382964 8988912.711398948 +2978 4326 623616.1034382964 8988912.711398948 -139.87641362361418 -9.144507389847984 +4630 2978 -140.2665 -9.5275 580507.0131301195 8946730.884364227 +2978 4326 580507.0131301195 8946730.884364227 -140.26793011920253 -9.527010362239173 +4630 2978 -139.4835 -8.762500000000001 666817.1747799615 9031057.773963332 +2978 4326 666817.1747799615 9031057.773963332 -139.48489715982998 -8.762004331120972 +4632 2980 45.165 -12.83 517905.66460200143 8581641.674532494 +2980 4326 517905.66460200143 8581641.674532494 45.1671121851282 -12.832577181366922 +4632 2980 44.9985 -13.028 499837.34969419055 8559751.159352869 +2980 4326 499837.34969419055 8559751.159352869 45.000605505660296 -13.030581015021044 +4632 2980 45.3315 -12.632 536002.1637134952 8603520.53253925 +2980 4326 536002.1637134952 8603520.53253925 45.33361886140144 -12.634573364195953 +4633 2981 167.25 -20.93 734003.4135101903 7683928.084856516 +2981 4326 734003.4135101903 7683928.084856516 167.24720117261947 -20.9322960878491 +4633 2981 167.007 -21.209 708333.6357766178 7653371.134325919 +2981 4326 708333.6357766178 7653371.134325919 167.00418461434185 -21.21129377268236 +4633 2981 167.493 -20.651 759764.689519601 7714453.48959056 +2981 4326 759764.689519601 7714453.48959056 167.49021765392635 -20.653298268999386 +4636 2985 140.47 -66.44 321014.9793047901 262429.3064700635 +2985 4326 321014.9793047901 262429.3064700635 140.46144013511324 -66.44010210551644 +4636 2985 139.543 -66.746 279838.85795337375 228259.82257171953 +2985 4326 279838.85795337375 228259.82257171953 139.53427712097962 -66.74606730444378 +4636 2985 141.397 -66.134 363292.26601968554 295955.817694054 +2985 4326 363292.26601968554 295955.817694054 141.38860117782735 -66.13413638171744 +4637 2986 139.0 -66.37 255152.51630918606 269947.13205227954 +2986 4326 255152.51630918606 269947.13205227954 138.99263740863577 -66.36996353367641 +4637 2986 136.3 -67.054 139098.9124349217 188781.0187749425 +2986 4326 139098.9124349217 188781.0187749425 136.29228286352878 -67.05387051548799 +4637 2986 141.7 -65.68599999999999 378504.18958334054 345718.1252507535 +2986 4326 378504.18958334054 345718.1252507535 141.69298801869164 -65.68605287046822 +4638 2987 -56.275 46.94 555181.8166525714 5198533.1471474655 +2987 4326 555181.8166525714 5198533.1471474655 -56.271782910133 46.94243295342969 +4638 2987 -56.4595 46.714999999999996 541311.0459063028 5173417.336021397 +2987 4326 541311.0459063028 5173417.336021397 -56.456310914755214 46.71743842799688 +4638 2987 -56.0905 47.165 568934.0228125058 5223682.848892273 +2987 4326 568934.0228125058 5223682.848892273 -56.08725465318381 47.16742747904703 +4639 2988 -176.16 -13.285 590992.2654814116 8531176.49888923 +2988 4326 590992.2654814116 8531176.49888923 -176.1586282063242 -13.286254874831673 +4639 2988 -176.24099999999999 -13.3975 582179.4886844294 8518762.210232632 +2988 4326 582179.4886844294 8518762.210232632 -176.2396307522725 -13.398756098417282 +4639 2988 -176.079 -13.1725 599812.9447379032 8543588.292344488 +2988 4326 599812.9447379032 8543588.292344488 -176.07762566020284 -13.17375365675664 +4269 2991 -120.535 44.12 397199.10422283644 263312.33359227417 +2991 4326 397199.10422283644 263312.33359227417 -120.53500000000001 44.11999999999999 +4269 2991 -124.1935 42.193999999999996 94902.84668831312 56206.91245575882 +2991 4326 94902.84668831312 56206.91245575882 -124.19350000000001 42.19400000000001 +4269 2991 -116.8765 46.046 680436.0888007582 483525.7810091159 +2991 4326 680436.0888007582 483525.7810091159 -116.87649999999998 46.04600000000001 +4269 2992 -120.535 44.12 1303146.667392508 863885.6088985372 +2992 4326 1303146.667392508 863885.6088985372 -120.53500000000001 44.11999999999999 +4269 2992 -124.1935 42.193999999999996 311361.04556008236 184405.88076036357 +2992 4326 311361.04556008236 184405.88076036357 -124.19350000000001 42.19400000000001 +4269 2992 -116.8765 46.046 2232401.8661389705 1586370.672602086 +2992 4326 2232401.8661389705 1586370.672602086 -116.87649999999998 46.04600000000001 +4152 2993 -120.535 44.12 397199.10422283644 263312.33359227417 +2993 4326 397199.10422283644 263312.33359227417 -120.53501380040314 44.12000564076461 +4152 2993 -124.1935 42.193999999999996 94902.84668831312 56206.91245575882 +2993 4326 94902.84668831312 56206.91245575882 -124.19351403939558 42.19400516920929 +4152 2993 -116.8765 46.046 680436.0888007582 483525.7810091159 +2993 4326 680436.0888007582 483525.7810091159 -116.87651352145681 46.04600614130111 +4152 2994 -120.535 44.12 1303146.667392508 863885.6088985372 +2994 4326 1303146.667392508 863885.6088985372 -120.53501380040314 44.12000564076461 +4152 2994 -124.1935 42.193999999999996 311361.04556008236 184405.88076036357 +2994 4326 311361.04556008236 184405.88076036357 -124.19351403939558 42.19400516920929 +4152 2994 -116.8765 46.046 2232401.8661389705 1586370.672602086 +2994 4326 2232401.8661389705 1586370.672602086 -116.87651352145681 46.04600614130111 +4641 2995 167.875 -21.515 797862.6827606957 7618081.368077131 +2995 4326 797862.6827606957 7618081.368077131 167.8727394100598 -21.516383597349122 +4641 2995 167.7625 -21.6905 785855.0229742635 7598850.600140711 +2995 4326 785855.0229742635 7598850.600140711 167.76023206064406 -21.691883239649396 +4641 2995 167.9875 -21.3395 809896.5252227916 7637306.213710393 +2995 4326 809896.5252227916 7637306.213710393 167.98524672943805 -21.34088393075943 +4642 2996 167.485 -22.61 755459.7279088891 7497488.802817803 +2996 4326 755459.7279088891 7497488.802817803 167.48833160297227 -22.607199284415522 +4642 2996 167.3725 -22.718 743698.8697971927 7485714.943490788 +2996 4326 743698.8697971927 7485714.943490788 167.3758330065949 -22.715202380030725 +4642 2996 167.59750000000003 -22.502 767238.4993992813 7509255.213645328 +2996 4326 767238.4993992813 7509255.213645328 167.60083020050038 -22.4991962252791 +4643 2997 163.64499999999998 -19.675 357955.1647462358 7823889.763433401 +2997 4326 357955.1647462358 7823889.763433401 163.64819608578063 -19.672251886746388 +4643 2997 163.5505 -19.832500000000003 348196.2161492978 7806375.027710902 +2997 4326 348196.2161492978 7806375.027710902 163.55370198756856 -19.82973351134177 +4643 2997 163.7395 -19.5175 367734.2621032732 7841397.643397255 +2997 4326 367734.2621032732 7841397.643397255 163.7426902834418 -19.514770308924415 +4644 2998 166.445 -22.28 648877.029837769 7535436.668860604 +2998 4326 648877.029837769 7535436.668860604 166.4483286516378 -22.277234939538673 +4644 2998 166.3595 -22.361 639985.9901017585 7526550.55655035 +2998 4326 639985.9901017585 7526550.55655035 166.36282952307113 -22.35823733114959 +4644 2998 166.5305 -22.199 657778.1695453619 7544318.191337052 +2998 4326 657778.1695453619 7544318.191337052 166.53382778042933 -22.196232568461642 +4646 2999 43.355 -11.65 320669.15380406857 8711613.500215193 +2999 4326 320669.15380406857 8711613.500215193 43.36446395509777 -11.653491727838354 +4646 2999 43.1795 -11.956 301751.3193374812 8677643.990138229 +2999 4326 301751.3193374812 8677643.990138229 43.18896457053481 -11.959502501117278 +4646 2999 43.530499999999996 -11.344000000000001 339632.4614590368 8745564.986949768 +2999 4326 339632.4614590368 8745564.986949768 43.539963520449945 -11.347481173134037 +4613 3000 116.805 0.025000000000000355 4655168.939921245 902755.7997260778 +3000 4326 4655168.939921245 902755.7997260778 116.80552481694154 0.025416224797574265 +4613 3000 114.7755 -3.8135000000000003 4429949.929844805 479315.4622554415 +3000 4326 4429949.929844805 479315.4622554415 114.7762624330949 -3.81271669384989 +4613 3000 118.8345 3.8635 4880387.949997684 1326208.6943478482 +3000 4326 4880387.949997684 1326208.6943478482 118.83478887129043 3.863544731844111 +4211 3001 105.465 -1.4700000000000006 3396738.994483051 737940.9638813554 +3001 4326 3396738.994483051 737940.9638813554 105.46663332289683 -1.4703071785069337 +4211 3001 96.1905 -8.166 2367523.0748211006 -3258.196323510143 +3001 4326 2367523.0748211006 -3258.196323510143 96.19323477272364 -8.165694297792832 +4211 3001 114.73949999999999 5.225999999999999 4425954.914145 1476883.4588891638 +3001 4326 4425954.914145 1476883.4588891638 114.74001781246199 5.2250190752833285 +4257 3002 119.745 -4.21 4981428.55540522 435499.60194314946 +3002 4326 4981428.55540522 435499.60194314946 119.74727419237819 -4.2082799783573 +4257 3002 118.81349999999999 -6.307 4878057.524172795 203339.79288318683 +3002 4326 4878057.524172795 203339.79288318683 118.81589066386499 -6.305086994303584 +4257 3002 120.6765 -2.1130000000000004 5084799.586637642 667026.2947649076 +3002 4326 5084799.586637642 667026.2947649076 120.67866076288934 -2.1114778230789577 +4265 3003 8.965 41.785 1497091.5136763148 4625987.728111994 +3003 4326 1497091.5136763148 4625987.728111994 8.964685635256435 41.78562450931655 +4265 3003 6.233499999999999 37.0555 1253989.3789316146 4104676.6672394215 +3003 4326 1253989.3789316146 4104676.6672394215 6.23314155335178 37.0560670488343 +4265 3003 11.6965 46.5145 1706858.2670374007 5154845.787141643 +3003 4326 1706858.2670374007 5154845.787141643 11.696242241989744 46.515198202763386 +4265 3004 15.495 40.93 2561676.2386281057 4531183.260829372 +3004 4326 2561676.2386281057 4531183.260829372 15.494929334593289 40.930643719015414 +4265 3004 12.349499999999999 35.376999999999995 2279212.8788050013 3918140.1759064733 +3004 4326 2279212.8788050013 3918140.1759064733 12.349351908128424 35.37758931037423 +4265 3004 18.6405 46.483000000000004 2799433.0090940637 5154254.369246698 +3004 4326 2799433.0090940637 5154254.369246698 18.640529344323085 46.483719519457956 +4269 3005 -126.56 54.129999999999995 963492.2540470979 1014264.2610207349 +3005 4326 963492.2540470979 1014264.2610207349 -126.56000000000002 54.13 +4269 3005 -137.792 48.838 137055.92070801044 496681.58307666087 +3005 4326 137055.92070801044 496681.58307666087 -137.792 48.83799999999998 +4269 3005 -115.328 59.422 1604477.6599223828 1649926.3765193184 +3005 4326 1604477.6599223828 1649926.3765193184 -115.328 59.422000000000004 +4619 3006 17.1 62.015 609924.4489727435 6877630.37144294 +3006 4326 609924.4489727435 6877630.37144294 17.099999999999998 62.014999999999986 +4619 3006 10.737 55.6655 231924.2386330617 6177093.086720681 +3006 4326 231924.2386330617 6177093.086720681 10.737000000000002 55.6655 +4619 3006 23.463 68.36449999999999 847297.715038483 7607401.145789399 +3006 4326 847297.715038483 7607401.145789399 23.462999999999997 68.36449999999998 +4619 3007 12.02 58.435 151168.28067387955 6479733.786659077 +3007 4326 151168.28067387955 6479733.786659077 12.02 58.435 +4619 3007 11.039 56.9095 91457.34560761286 6310247.2342757555 +3007 4326 91457.34560761286 6310247.2342757555 11.039 56.9095 +4619 3007 13.001 59.9605 205920.95455766117 6650094.940058209 +3007 4326 205920.95455766117 6650094.940058209 13.001 59.9605 +4619 3008 13.454999999999998 58.78 147397.13250546544 6518163.451093328 +3008 4326 147397.13250546544 6518163.451093328 13.454999999999998 58.78000000000001 +4619 3008 12.253499999999999 55.63 71488.2722169713 6168072.753683795 +3008 4326 71488.2722169713 6168072.753683795 12.253499999999999 55.63 +4619 3008 14.6565 61.93 210735.26116546494 6869670.658137377 +3008 4326 210735.26116546494 6869670.658137377 14.656500000000001 61.93 +4619 3009 14.844999999999999 58.785 141035.86069809148 6518729.903527869 +3009 4326 141035.86069809148 6518729.903527869 14.844999999999999 58.785 +4619 3009 13.670499999999999 56.2335 67552.46179644667 6235357.081814296 +3009 4326 67552.46179644667 6235357.081814296 13.670499999999999 56.2335 +4619 3009 16.019499999999997 61.3365 204576.5231164005 6803416.4525958365 +3009 4326 204576.5231164005 6803416.4525958365 16.019499999999997 61.3365 +4619 3010 16.52 59.205 151142.66582960103 6565505.620848638 +3010 4326 151142.66582960103 6565505.620848638 16.52 59.20499999999999 +4619 3010 15.521 56.4555 89638.98810713782 6259710.916975292 +3010 4326 89638.98810713782 6259710.916975292 15.521000000000003 56.45550000000001 +4619 3010 17.519 61.954499999999996 203471.85000004002 6872279.839844245 +3010 4326 203471.85000004002 6872279.839844245 17.519 61.954499999999996 +4619 3011 18.345 59.68 169436.55093583668 6618472.273037763 +3011 4326 169436.55093583668 6618472.273037763 18.345000000000002 59.67999999999999 +4619 3011 17.2065 58.762 104079.70072296215 6516429.421870317 +3011 4326 104079.70072296215 6516429.421870317 17.2065 58.762 +4619 3011 19.4835 60.598 231276.13051983458 6721617.142327454 +3011 4326 231276.13051983458 6721617.142327454 19.4835 60.59799999999998 +4619 3012 13.74 62.97 124130.54365610614 6985143.2851961935 +3012 4326 124130.54365610614 6985143.2851961935 13.74 62.97 +4619 3012 12.111 61.692 36804.813410740564 6844467.022442642 +3012 4326 36804.813410740564 6844467.022442642 12.111 61.692 +4619 3012 15.369 64.248 204266.65811971 7127978.271511012 +3012 4326 204266.65811971 7127978.271511012 15.369000000000002 64.248 +4619 3013 15.335 62.785 128816.37905029746 6964488.905308066 +3013 4326 128816.37905029746 6964488.905308066 15.335 62.78499999999999 +4619 3013 13.8275 60.674499999999995 44925.91530965887 6730761.391471256 +3013 4326 44925.91530965887 6730761.391471256 13.8275 60.6745 +4619 3013 16.8425 64.8955 201738.51773220647 7200134.911574385 +3013 4326 201738.51773220647 7200134.911574385 16.8425 64.89550000000001 +4619 3014 16.675 64.655 122525.00974195829 7172999.661263187 +3014 4326 122525.00974195829 7172999.661263187 16.675 64.655 +4619 3014 14.5465 62.3735 10108.145261199883 6921481.867274005 +3014 4326 10108.145261199883 6921481.867274005 14.546500000000002 62.373499999999986 +4619 3014 18.8035 66.9365 217934.41773366983 7428119.671028178 +3014 4326 217934.41773366983 7428119.671028178 18.803500000000003 66.9365 +4619 3015 18.7 61.515 147338.53632143553 6822882.759007674 +3015 4326 147338.53632143553 6822882.759007674 18.7 61.51499999999999 +4619 3015 17.332 57.3255 64582.43593952907 6357052.267376485 +3015 4326 64582.43593952907 6357052.267376485 17.332 57.32549999999999 +4619 3015 20.067999999999998 65.7045 210531.5348406141 7290523.965204186 +3015 4326 210531.5348406141 7290523.965204186 20.067999999999998 65.70449999999998 +4619 3016 19.68 66.25999999999999 124383.34672026377 7351948.247889037 +3016 4326 124383.34672026377 7351948.247889037 19.68 66.25999999999999 +4619 3016 16.439999999999998 63.731 -38135.7096780375 7075479.373062914 +3016 4326 -38135.7096780375 7075479.373062914 16.44 63.73099999999999 +4619 3016 22.92 68.78899999999999 257821.73632466904 7636231.141723197 +3016 4326 257821.73632466904 7636231.141723197 22.92 68.78899999999997 +4619 3017 21.27 65.72 127967.2137075571 7291701.798851329 +3017 4326 127967.2137075571 7291701.798851329 21.27 65.72 +4619 3017 19.794 65.081 58015.69486635701 7221794.194794069 +3017 4326 58015.69486635701 7221794.194794069 19.794 65.08099999999999 +4619 3017 22.746 66.35900000000001 194584.9710337921 7363226.299164091 +3017 4326 194584.9710337921 7363226.299164091 22.746 66.35900000000001 +4619 3018 23.01 66.815 139451.7546358816 7413742.860812734 +3018 4326 139451.7546358816 7413742.860812734 23.01 66.815 +4619 3018 21.966 65.6225 90843.10890339734 7281349.957482098 +3018 4326 90843.10890339734 7281349.957482098 21.966 65.62249999999999 +4619 3018 24.054000000000002 68.0075 183612.8321087059 7546937.847089904 +3018 4326 183612.8321087059 7546937.847089904 24.054000000000002 68.0075 +4124 3019 11.92 58.51 1535652.4328597744 6487572.764793137 +3019 4326 1535652.4328597744 6487572.764793137 11.917452980473096 58.50956472654145 +4124 3019 11.029 57.412 1483217.7722779105 6365167.415354467 +3019 4326 1483217.7722779105 6365167.415354467 11.026591218172186 57.41169715256086 +4124 3019 12.811 59.608 1584826.67282345 6610668.599969928 +3019 4326 1584826.67282345 6610668.599969928 12.80830529623368 59.60743272650727 +4124 3020 13.625 59.835 1503741.161275669 6634997.385358886 +3020 4326 1503741.161275669 6634997.385358886 13.622180741843444 59.83440916724369 +4124 3020 11.9915 55.7355 1401594.3818559009 6179584.90705912 +3020 4326 1401594.3818559009 6179584.90705912 11.988981482651921 55.735406276910304 +4124 3020 15.2585 63.9345 1583371.6673643568 7092914.644244794 +3020 4326 1583371.6673643568 7092914.644244794 15.255287372279579 63.93340858072235 +4124 3021 15.695 61.565000000000005 1493980.7377639648 6827741.45239362 +3021 4326 1493980.7377639648 6827741.45239362 15.691814077851811 61.56420913624837 +4124 3021 13.8635 56.511500000000005 1380291.102520311 6266560.3801023085 +3021 4326 1380291.102520311 6266560.3801023085 13.860721308345626 56.511320890729 +4124 3021 17.5265 66.61850000000001 1576104.1181331587 7392071.665748311 +3021 4326 1576104.1181331587 7392071.665748311 17.522732570378633 66.61709337136782 +4124 3022 18.15 62.7 1504694.8717980688 6954217.599443447 +3022 4326 1504694.8717980688 6954217.599443447 18.146374137116297 62.69909257705334 +4124 3022 16.287 57.444 1393660.9940883305 6370081.902565132 +3022 4326 1393660.9940883305 6370081.902565132 16.28387075684885 57.443724952459625 +4124 3022 20.012999999999998 67.956 1581883.0667000425 7541466.621423695 +3022 4326 1581883.0667000425 7541466.621423695 20.00864175809526 67.9544572510814 +4124 3023 20.299999999999997 66.22 1499627.4366907573 7346591.41249846 +3023 4326 1499627.4366907573 7346591.41249846 20.295755539077383 66.21867980925619 +4124 3023 18.59 63.654999999999994 1414904.8583028046 7061793.633611912 +3023 4326 1414904.8583028046 7061793.633611912 18.58624597622722 63.65397841402312 +4124 3023 22.009999999999998 68.785 1568734.7763199625 7633579.427998653 +3023 4326 1568734.7763199625 7633579.427998653 22.005149339340257 68.78338457493457 +4124 3024 22.755000000000003 66.91 1508611.6267164848 7423541.4272754975 +3024 4326 1508611.6267164848 7423541.4272754975 22.750235496586733 66.90863307603938 +4124 3024 21.4815 65.407 1449984.767210036 7256376.396013917 +3024 4326 1449984.767210036 7256376.396013917 21.47711964826812 65.4058000266925 +4124 3024 24.0285 68.413 1560377.0169922581 7591858.9105581725 +3024 4326 1560377.0169922581 7591858.9105581725 24.023299632485386 68.41146821220835 +4308 3025 11.92 58.51 1535652.4328597744 6487572.764793137 +4308 3025 11.029 57.412 1483217.7722779105 6365167.415354467 +4308 3025 12.811 59.608 1584826.67282345 6610668.599969928 +4308 3026 13.625 59.835 1503741.161275669 6634997.385358886 +4308 3026 11.9915 55.7355 1401594.3818559009 6179584.90705912 +4308 3026 15.2585 63.9345 1583371.6673643568 7092914.644244794 +4308 3027 15.695 61.565000000000005 1493980.7377639648 6827741.45239362 +4308 3027 13.8635 56.511500000000005 1380291.102520311 6266560.3801023085 +4308 3027 17.5265 66.61850000000001 1576104.1181331587 7392071.665748311 +4308 3028 18.15 62.7 1504694.8717980688 6954217.599443447 +4308 3028 16.287 57.444 1393660.9940883305 6370081.902565132 +4308 3028 20.012999999999998 67.956 1581883.0667000425 7541466.621423695 +4308 3029 20.299999999999997 66.22 1499627.4366907573 7346591.41249846 +4308 3029 18.59 63.654999999999994 1414904.8583028046 7061793.633611912 +4308 3029 22.009999999999998 68.785 1568734.7763199625 7633579.427998653 +4308 3030 22.755000000000003 66.91 1508611.6267164848 7423541.4272754975 +4308 3030 21.4815 65.407 1449984.767210036 7256376.396013917 +4308 3030 24.0285 68.413 1560377.0169922581 7591858.9105581725 +4326 3031 0.0 -75.0 0.0 1638783.2384072365 +4326 3031 -162.0 -88.5 -50365.9371102881 -155010.4154787378 +4326 3031 162.0 -61.5 976334.1123644955 -3004847.424406821 +4326 3032 102.5 -75.0 6880517.592161548 7382135.764814202 +4326 3032 50.75 -88.5 5946264.514226011 6153874.800069803 +4326 3032 154.25 -61.5 9143586.672513038 6316542.533207281 +4326 3033 102.5 -70.0 7180830.371683375 3401160.8344946685 +4326 3033 50.75 -79.0 5593217.662054066 2655646.245299493 +4326 3033 154.25 -61.0 9260898.589834303 2001935.99244742 +4258 3034 4.625 54.665000000000006 3664622.8977630353 3098904.2726719077 +3034 4326 3664622.8977630353 3098904.2726719077 4.6250000000000036 54.66500000000001 +4258 3034 -31.5595 27.6065 -42147.6549985474 1288673.141606746 +3034 4326 -42147.6549985474 1288673.141606746 -31.559499999999993 27.606500000000004 +4258 3034 40.8095 81.7235 4592736.2509427 6363147.430581379 +3034 4326 4592736.2509427 6363147.430581379 40.809500000000014 81.7235 +4258 3035 4.625 54.665000000000006 3974432.430312099 3519463.6853382704 +3035 4326 3974432.430312099 3519463.6853382704 4.625000000000003 54.66500000000001 +4258 3035 -31.5595 27.6065 337644.84454841167 1606735.1498515895 +3035 4326 337644.84454841167 1606735.1498515895 -31.559499999999993 27.606500000000008 +4258 3035 40.8095 81.7235 4811084.938733961 6603466.831664722 +3035 4326 4811084.938733961 6603466.831664722 40.809500000000014 81.72350000000002 +4130 3036 33.105000000000004 -19.494999999999997 511018.1549780833 7844397.046678033 +3036 4326 511018.1549780833 7844397.046678033 33.105000000000004 -19.494999999999994 +4130 3036 30.4995 -26.7715 251365.72538783847 7036428.004837874 +3036 4326 251365.72538783847 7036428.004837874 30.4995 -26.771499999999993 +4130 3036 35.710499999999996 -12.2185 794923.6266126034 8647807.656155594 +3036 4326 794923.6266126034 8647807.656155594 35.710499999999996 -12.2185 +4130 3037 39.0 -18.9 500000.0000000007 7910237.267898128 +3037 4326 500000.0000000007 7910237.267898128 39.0 -18.899999999999995 +4130 3037 36.3 -26.829 231655.71902018768 7029649.339629745 +3037 4326 231655.71902018768 7029649.339629745 36.3 -26.82899999999999 +4130 3037 41.7 -10.971 795089.1710549947 8785903.162563954 +3037 4326 795089.1710549947 8785903.162563954 41.7 -10.971000000000007 +4258 3040 -14.045000000000002 53.685 563070.5815559236 5948899.088365438 +3040 4326 563070.5815559236 5948899.088365438 -14.045000000000003 53.684999999999995 +4258 3040 -15.8945 36.8055 420207.72232269973 4073669.3786720587 +3040 4326 420207.72232269973 4073669.3786720587 -15.894500000000003 36.8055 +4258 3040 -12.195500000000001 70.5645 604118.1890119871 7831230.264494488 +3040 4326 604118.1890119871 7831230.264494488 -12.195500000000003 70.5645 +4258 3041 -9.004999999999999 54.519999999999996 499676.3504694062 6041379.636906929 +3041 4326 499676.3504694062 6041379.636906929 -9.004999999999999 54.519999999999996 +4258 3041 -11.7095 36.870999999999995 258487.26392929876 4083990.0127365724 +3041 4326 258487.26392929876 4083990.0127365724 -11.7095 36.871 +4258 3041 -6.3005 72.169 592234.1079876372 8009852.093112804 +3041 4326 592234.1079876372 8009852.093112804 -6.300500000000003 72.16900000000001 +4258 3042 -3.005 57.875 499703.4247519125 6414793.670332485 +3042 4326 499703.4247519125 6414793.670332485 -3.005 57.875 +4258 3042 -5.7095 37.521499999999996 260552.53027900925 4156176.3858976658 +3042 4326 260552.53027900925 4156176.3858976658 -5.709499999999999 37.52149999999998 +4258 3042 -0.30050000000000043 78.2285 561458.3226445296 8685288.159146436 +3042 4326 561458.3226445296 8685288.159146436 -0.30049999999999993 78.2285 +4258 3043 3.005 59.725 500281.1997118188 6620785.720680849 +3043 4326 500281.1997118188 6620785.720680849 3.005 59.725 +4258 3043 0.3005 39.2725 267127.53852953465 4350490.967358435 +3043 4326 267127.53852953465 4350490.967358435 0.30049999999999993 39.2725 +4258 3043 5.709499999999999 80.17750000000001 551584.6058092709 8902599.700418724 +3043 4326 551584.6058092709 8902599.700418724 5.709499999999996 80.17750000000001 +4258 3044 9.004999999999999 60.27 500276.6127925036 6681481.103924277 +3044 4326 500276.6127925036 6681481.103924277 9.004999999999999 60.26999999999999 +4258 3044 6.3004999999999995 38.904 265911.53731539764 4309588.114474267 +3044 4326 265911.53731539764 4309588.114474267 6.3004999999999995 38.904 +4258 3044 11.709499999999998 81.636 543985.2429991267 9065226.109379351 +3044 4326 543985.2429991267 9065226.109379351 11.709499999999995 81.63599999999998 +4258 3045 15.005 59.25 500285.1764730466 6567890.210533129 +3045 4326 500285.1764730466 6567890.210533129 15.005000000000003 59.25000000000001 +4258 3045 12.3005 36.966 259677.372609922 4094506.5412004623 +3045 4326 259677.372609922 4094506.5412004623 12.3005 36.966 +4258 3045 17.709500000000002 81.534 544517.7025196872 9053852.53006163 +3045 4326 544517.7025196872 9053852.53006163 17.7095 81.534 +4258 3046 21.005000000000003 58.800000000000004 500288.92561857135 6517782.2612296995 +3046 4326 500288.92561857135 6517782.2612296995 21.005000000000003 58.800000000000004 +4258 3046 18.3005 36.111000000000004 257014.2064328766 3999635.0825871276 +3046 4326 257014.2064328766 3999635.0825871276 18.3005 36.111000000000004 +4258 3046 23.709500000000002 81.489 544752.5656948156 9048834.789206645 +3046 4326 544752.5656948156 9048834.789206645 23.7095 81.489 +4258 3047 27.005000000000003 58.635000000000005 500290.2957942121 6499410.226728465 +3047 4326 500290.2957942121 6499410.226728465 27.005000000000003 58.635000000000005 +4258 3047 24.3005 35.7975 256051.26609403372 3964851.5323817893 +3047 4326 256051.26609403372 3964851.5323817893 24.3005 35.7975 +4258 3047 29.709500000000002 81.4725 544838.6751776864 9046994.953166328 +3047 4326 544838.6751776864 9046994.953166328 29.709500000000006 81.47249999999998 +4258 3048 33.004999999999995 58.96 500287.5946453913 6535598.019277634 +3048 4326 500287.5946453913 6535598.019277634 33.004999999999995 58.96000000000001 +4258 3048 30.3005 36.415 257954.92821979342 4033365.941643329 +3048 4326 257954.92821979342 4033365.941643329 30.300500000000003 36.415 +4258 3048 35.7095 81.505 544669.0619915491 9050618.87380583 +3048 4326 544669.0619915491 9050618.87380583 35.70949999999999 81.50499999999998 +4258 3049 37.004999999999995 76.03 446247.5544441172 8439441.106222108 +3049 4326 446247.5544441172 8439441.106222108 37.004999999999995 76.03 +4258 3049 36.1005 73.294 407000.1760679933 8135531.226743587 +3049 4326 407000.1760679933 8135531.226743587 36.1005 73.294 +4258 3049 37.9095 78.76599999999999 476284.7618366149 8744079.260121493 +3049 4326 476284.7618366149 8744079.260121493 37.9095 78.76599999999999 +4657 3052 -19.02 64.965 -100.35997164774766 -3901.5117576673974 +3052 4326 -100.35997164774766 -3901.5117576673974 -19.01620954403936 64.96664328375233 +4657 3052 -24.096 63.502500000000005 252443.43957060616 -156795.28168291633 +3052 4326 252443.43957060616 -156795.28168291633 -24.092580972390653 63.50429628251511 +4657 3052 -13.944 66.42750000000001 -226489.13459660037 168259.46586807963 +3052 4326 -226489.13459660037 168259.46586807963 -13.939824023592616 66.4289786635344 +4658 3053 -19.02 64.965 548182.3701193396 496486.2940775795 +3053 4326 548182.3701193396 496486.2940775795 -19.01956299207984 64.96474052983156 +4658 3053 -24.096 63.502500000000005 803226.6314369001 347661.54575421545 +3053 4326 803226.6314369001 347661.54575421545 -24.095736930879944 63.502200981242154 +4658 3053 -13.944 66.42750000000001 318984.15432462905 665003.6780875893 +3053 4326 318984.15432462905 665003.6780875893 -13.943372283541677 66.42727574403297 +4658 3054 -24.33 65.28 624565.8692296789 7242479.386581436 +3054 4326 624565.8692296789 7242479.386581436 -24.32972686624858 65.27976443520714 +4658 3054 -24.627 64.767 612861.7478278357 7184781.25380799 +3054 4326 612861.7478278357 7184781.25380799 -24.626741418758478 64.76674655797822 +4658 3054 -24.033 65.79299999999999 635719.2292916584 7300227.89437918 +3054 4326 635719.2292916584 7300227.89437918 -24.032711721931157 65.79278245672675 +4658 3055 -21.0 64.93 499999.99999999953 7200833.158815482 +3055 4326 499999.99999999953 7200833.158815482 -20.999625423555436 64.92974466508161 +4658 3055 -23.7 63.499 365600.84742940275 7044195.658467285 +3055 4326 365600.84742940275 7044195.658467285 -23.69972505953305 63.498700115803544 +4658 3055 -18.3 66.36099999999999 620784.8910492338 7362944.123753731 +3055 4326 620784.8910492338 7362944.123753731 -18.299515188082555 66.36078885282677 +4658 3056 -15.690000000000001 65.015 467478.9967128641 7210484.138795842 +3056 4326 467478.9967128641 7210484.138795842 -15.689459021286748 65.01473141110559 +4658 3056 -17.769 63.606500000000004 362686.1587191809 7056312.771525581 +3056 4326 362686.1587191809 7056312.771525581 -17.7685469606028 63.60618880760547 +4658 3056 -13.611 66.4235 561993.8283050848 7367992.234063001 +3056 4326 561993.8283050848 7367992.234063001 -13.610361698491664 66.42327424786389 +4659 3057 -18.21 64.775 537577.9596630577 475151.4256193623 +3057 4326 537577.9596630577 475151.4256193623 -18.210000000000004 64.775 +4659 3057 -29.604 60.4415 -82768.41392567102 40442.577772214834 +3057 4326 -82768.41392567102 40442.577772214834 -29.604 60.4415 +4659 3057 -6.8160000000000025 69.1085 983352.8402514628 1005282.0216167318 +3057 4326 983352.8402514628 1005282.0216167318 -6.816000000000002 69.1085 +4660 3058 -8.52 70.995 49272.757592116825 80203.35028890986 +3058 4326 49272.757592116825 80203.35028890986 -8.515521729138229 70.99373796692547 +4660 3058 -9.105 70.7745 27755.775406952132 55711.07064825762 +3058 4326 27755.775406952132 55711.07064825762 -9.100617936580432 70.77325336733 +4660 3058 -7.9350000000000005 71.21549999999999 70314.79183451364 104901.70313727949 +3058 4326 70314.79183451364 104901.70313727949 -7.9304234465619245 71.21422248770693 +4661 3059 23.65 56.88 478669.909704032 304082.77423819713 +3059 4326 478669.909704032 304082.77423819713 23.65 56.879999999999995 +4661 3059 19.519 55.791000000000004 219128.93832996086 191909.4644314982 +3059 4326 219128.93832996086 191909.4644314982 19.519000000000002 55.791 +4661 3059 27.781 57.969 723614.0114121912 431517.38856511377 +3059 4326 723614.0114121912 431517.38856511377 27.780999999999995 57.96899999999999 +4662 3060 165.505 -21.240000000000002 552402.3575551521 7651176.885697747 +3060 4326 552402.3575551521 7651176.885697747 165.50827935305202 -21.23723472942627 +4662 3060 164.0785 -22.329 405096.09509044426 7530434.659055328 +3060 4326 405096.09509044426 7530434.659055328 164.08178483427847 -22.326272070977115 +4662 3060 166.9315 -20.151 701889.2846149195 7770608.2478469275 +3060 4326 701889.2846149195 7770608.2478469275 166.93477310600278 -20.148201546928703 +4663 3061 -16.77 32.75 334171.00412245234 3625013.5176809514 +3061 4326 334171.00412245234 3625013.5176809514 -16.774075037355 32.753622874597276 +4663 3061 -17.256 32.39 287782.8966886513 3585958.5374633074 +3061 4326 287782.8966886513 3585958.5374633074 -17.26009108171659 32.393597056812965 +4663 3061 -16.284 33.11 380193.8333341968 3664271.081612069 +3061 4326 380193.8333341968 3664271.081612069 -16.28805833018334 33.11364854427926 +4664 3062 -25.32 37.415 648675.7450674111 4142303.8589758445 +3062 4326 648675.7450674111 4142303.8589758445 -25.319557344419103 37.41594209781144 +4664 3062 -25.860000000000003 36.924499999999995 601539.3143419191 4087170.4402770614 +3062 4326 601539.3143419191 4087170.4402770614 -25.859586039587523 36.925435257229914 +4664 3062 -24.779999999999998 37.9055 695180.7321004497 4197723.316122828 +3062 4326 695180.7321004497 4197723.316122828 -24.77952827625818 37.90644889035663 +4665 3063 -27.935 38.730000000000004 418725.4677602699 4287301.9640168855 +3063 4326 418725.4677602699 4287301.9640168855 -27.93388203531084 38.72989911804723 +4665 3063 -28.8035 38.361 342423.77447880665 4247480.203752405 +3063 4326 342423.77447880665 4247480.203752405 -28.802415921044364 38.36090041133095 +4665 3063 -27.066499999999998 39.099000000000004 494249.42248779646 4327838.003868563 +3063 4326 494249.42248779646 4327838.003868563 -27.065348008945648 39.098897589386475 +4670 3064 8.965 41.785 497091.64645042346 4625906.061436742 +3064 4326 497091.64645042346 4625906.061436742 8.965 41.78499999999999 +4670 3064 6.233499999999999 37.0555 254000.3275612877 4104609.5557094966 +3064 4326 254000.3275612877 4104609.5557094966 6.233499999999999 37.0555 +4670 3064 11.6965 46.5145 706848.5806319211 5154747.393047125 +3064 4326 706848.5806319211 5154747.393047125 11.6965 46.5145 +4670 3065 15.0 40.945 500000.00000000134 4532651.569272315 +3065 4326 500000.00000000134 4532651.569272315 14.999999999999998 40.94500000000001 +4670 3065 12.3 35.405499999999996 254812.08870369926 3921361.4559117802 +3065 4326 254812.08870369926 3921361.4559117802 12.300000000000002 35.40549999999999 +4670 3065 17.7 46.4845 707231.0799165128 5151423.26514731 +3065 4326 707231.0799165128 5151423.26514731 17.7 46.4845 +4230 3066 37.095 31.28 509044.86976174393 461381.3397564776 +3066 4326 509044.86976174393 461381.3397564776 37.09478547949071 31.278918983720505 +4230 3066 35.1015 29.39 315722.2723256838 253391.51149100857 +3066 4326 315722.2723256838 253391.51149100857 35.10123383493543 29.388880732286612 +4230 3066 39.0885 33.17 694788.8598742238 672866.8453455786 +3066 4326 694788.8598742238 672866.8453455786 39.088339253298216 33.16896024526104 +10690 3067 25.335 64.465 419924.39978573413 7149882.0006198855 +3067 4326 419924.39978573413 7149882.0006198855 25.335 64.465 +10690 3067 19.705499999999997 59.4025 86356.51130369824 6607583.548826504 +3067 4326 86356.51130369824 6607583.548826504 19.705499999999994 59.40250000000002 +10690 3067 30.9645 69.5275 654657.4899668262 7718197.781634642 +3067 4326 654657.4899668262 7718197.781634642 30.9645 69.52749999999999 +4314 3068 13.425 52.489999999999995 26266.884385921527 17958.04324882241 +3068 4326 26266.884385921527 17958.04324882241 13.423246748572339 52.48859345391538 +4314 3068 13.1235 52.346 5678.445230122175 2036.4634343310308 +3068 4326 5678.445230122175 2036.4634343310308 13.121797411780356 52.34460810976833 +4314 3068 13.7265 52.634 46721.89241202686 33965.64214865047 +3068 4326 46721.89241202686 33965.64214865047 13.724695773667237 52.63257892785633 +4267 3069 -89.57 44.894999999999996 533953.4206948427 471159.1695781499 +3069 4326 533953.4206948427 471159.1695781499 -89.57008607391465 44.89499764887488 +4267 3069 -92.558 42.7215 290550.93221985316 232847.11380825005 +3069 4326 290550.93221985316 232847.11380825005 -92.55818475581232 42.7215036181376 +4267 3069 -86.582 47.0685 759520.1632695475 718228.6381176226 +3069 4326 759520.1632695475 718228.6381176226 -86.58197954894474 47.068498330158604 +4269 3070 -89.57 44.894999999999996 553952.4209604628 491376.26325869653 +3070 4326 553952.4209604628 491376.26325869653 -89.57000000000001 44.895 +4269 3070 -92.558 42.7215 310556.80412441783 253061.62620366085 +3070 4326 310556.80412441783 253061.62620366085 -92.558 42.721500000000006 +4269 3070 -86.582 47.0685 779512.1515750884 738447.0279873684 +3070 4326 779512.1515750884 738447.0279873684 -86.582 47.068499999999986 +4152 3071 -89.57 44.894999999999996 553952.4209604628 491376.26325869653 +3071 4326 553952.4209604628 491376.26325869653 -89.57000652573907 44.89500824447411 +4152 3071 -92.558 42.7215 310556.80412441783 253061.62620366085 +3071 4326 310556.80412441783 253061.62620366085 -92.55800713748563 42.72150771610742 +4152 3071 -86.582 47.0685 779512.1515750884 738447.0279873684 +3071 4326 779512.1515750884 738447.0279873684 -86.58200588647034 47.06850876138836 +4269 3072 -67.745 45.775 710110.7661583305 215777.34469428813 +3072 4326 710110.7661583305 215777.34469428813 -67.745 45.775 +4269 3072 -68.4965 44.3395 650437.9411447884 56428.91664329373 +3072 4326 650437.9411447884 56428.91664329373 -68.4965 44.3395 +4269 3072 -66.9935 47.210499999999996 766778.0604996847 375714.4525110518 +3072 4326 766778.0604996847 375714.4525110518 -66.9935 47.210499999999996 +4269 3074 -70.345 44.825 302372.5469386635 221287.9296204905 +3074 4326 302372.5469386635 221287.9296204905 -70.345 44.82500000000001 +4269 3074 -71.0155 43.2455 247982.39365818878 45987.32700179099 +3074 4326 247982.39365818878 45987.32700179099 -71.0155 43.24550000000001 +4269 3074 -69.6745 46.4045 353865.1265871693 397074.0751520627 +3074 4326 353865.1265871693 397074.0751520627 -69.6745 46.40449999999999 +4152 3075 -67.745 45.775 710110.7661583305 215777.34469428813 +3075 4326 710110.7661583305 215777.34469428813 -67.74499950361017 45.77500937381117 +4152 3075 -68.4965 44.3395 650437.9411447884 56428.91664329373 +3075 4326 650437.9411447884 56428.91664329373 -68.49649968457703 44.33950909685663 +4152 3075 -66.9935 47.210499999999996 766778.0604996847 375714.4525110518 +3075 4326 766778.0604996847 375714.4525110518 -66.99349931939533 47.21050963969486 +4152 3077 -70.345 44.825 302372.5469386635 221287.9296204905 +3077 4326 302372.5469386635 221287.9296204905 -70.3450003369457 44.82500913199337 +4152 3077 -71.0155 43.2455 247982.39365818878 45987.32700179099 +3077 4326 247982.39365818878 45987.32700179099 -71.01550046928344 43.24550882234725 +4152 3077 -69.6745 46.4045 353865.1265871693 397074.0751520627 +3077 4326 353865.1265871693 397074.0751520627 -69.6745002046303 46.40450942911949 +4269 3078 -86.275 45.004999999999995 478167.87845010986 494847.00078408886 +3078 4326 478167.87845010986 494847.00078408886 -86.275 45.004999999999995 +4269 3078 -90.0055 42.021499999999996 168036.26552634127 171146.5263229655 +3078 4326 168036.26552634127 171146.5263229655 -90.0055 42.02149999999998 +4269 3078 -82.5445 47.9885 757726.9104425479 832165.3397596804 +3078 4326 757726.9104425479 832165.3397596804 -82.5445 47.988499999999995 +4152 3079 -86.275 45.004999999999995 478167.87845010986 494847.00078408886 +3079 4326 478167.87845010986 494847.00078408886 -86.27500553762705 45.0050084594736 +4152 3079 -90.0055 42.021499999999996 168036.26552634127 171146.5263229655 +3079 4326 168036.26552634127 171146.5263229655 -90.00550632875827 42.02150775438353 +4152 3079 -82.5445 47.9885 757726.9104425479 832165.3397596804 +3079 4326 757726.9104425479 832165.3397596804 -82.54450470423923 47.988509136430785 +4267 3080 -100.08 31.165 2975029.328937744 2999404.083711441 +3080 4326 2975029.328937744 2999404.083711441 -100.08037450877276 31.165177302094346 +4267 3080 -106.002 26.3635 1032912.8001718213 1308579.5303062028 +3080 4326 1032912.8001718213 1308579.5303062028 -106.0025162143612 26.36381369264416 +4267 3080 -94.158 35.966499999999996 4730505.082907125 4790585.051264586 +3080 4326 4730505.082907125 4790585.051264586 -94.15821626736648 35.966576498759395 +4269 3081 -100.08 31.165 992389.099001464 999818.3584082259 +3081 4326 992389.099001464 999818.3584082259 -100.08 31.165000000000006 +4269 3081 -106.002 26.3635 400442.6948601634 484434.5488528351 +3081 4326 400442.6948601634 484434.5488528351 -106.002 26.363499999999984 +4269 3081 -94.158 35.966499999999996 1527445.4748833128 1545785.7816438782 +3081 4326 1527445.4748833128 1545785.7816438782 -94.158 35.96649999999998 +4269 3082 -100.08 31.165 1492389.085956109 6467970.2720677685 +3082 4326 1492389.085956109 6467970.2720677685 -100.08 31.165000000000003 +4269 3082 -106.002 26.3635 900372.0614798032 5952594.840398136 +3082 4326 900372.0614798032 5952594.840398136 -106.002 26.363500000000013 +4269 3082 -94.158 35.966499999999996 2027379.438878832 7013937.4695115145 +3082 4326 2027379.438878832 7013937.4695115145 -94.158 35.96649999999998 +4269 3083 -100.08 31.165 1492389.0677626291 7449402.239574473 +3083 4326 1492389.0677626291 7449402.239574473 -100.08 31.165000000000003 +4269 3083 -106.002 26.3635 900399.2687562371 6933023.245166092 +3083 4326 900399.2687562371 6933023.245166092 -106.002 26.363499999999995 +4269 3083 -94.158 35.966499999999996 2027403.945952803 7996433.036043674 +3083 4326 2027403.945952803 7996433.036043674 -94.158 35.966499999999996 +4152 3084 -100.08 31.165 1492389.085956109 6467970.2720677685 +3084 4326 1492389.085956109 6467970.2720677685 -100.08000815763849 31.165005307835337 +4152 3084 -106.002 26.3635 900372.0614798032 5952594.840398136 +3084 4326 900372.0614798032 5952594.840398136 -106.00200933252084 26.36350422765772 +4152 3084 -94.158 35.966499999999996 2027379.438878832 7013937.4695115145 +3084 4326 2027379.438878832 7013937.4695115145 -94.15800695029776 35.96650646319317 +4152 3085 -100.08 31.165 1492389.0677626291 7449402.239574473 +3085 4326 1492389.0677626291 7449402.239574473 -100.08000815763849 31.165005307835337 +4152 3085 -106.002 26.3635 900399.2687562371 6933023.245166092 +3085 4326 900399.2687562371 6933023.245166092 -106.00200933252084 26.363504227657703 +4152 3085 -94.158 35.966499999999996 2027403.945952803 7996433.036043674 +3085 4326 2027403.945952803 7996433.036043674 -94.15800695029776 35.966506463193184 +4269 3086 -83.8 27.71 419682.8006494569 411606.1169059885 +3086 4326 419682.8006494569 411606.1169059885 -83.8 27.709999999999994 +4269 3086 -87.247 24.740000000000002 71808.96482317086 86317.42506656054 +3086 4326 71808.96482317086 86317.42506656054 -87.247 24.73999999999999 +4269 3086 -80.353 30.68 749112.5376311431 746513.1936236266 +3086 4326 749112.5376311431 746513.1936236266 -80.353 30.679999999999993 +4152 3087 -83.8 27.71 419682.8006494569 411606.1169059885 +3087 4326 419682.8006494569 411606.1169059885 -83.8000036754432 27.710005062225637 +4152 3087 -87.247 24.740000000000002 71808.96482317086 86317.42506656054 +3087 4326 71808.96482317086 86317.42506656054 -87.247004572498 24.74000426558684 +4152 3087 -80.353 30.68 749112.5376311431 746513.1936236266 +3087 4326 749112.5376311431 746513.1936236266 -80.35300277171139 30.680005859889057 +4269 3088 -85.75999999999999 37.82 1499119.6161673432 1164993.7957933834 +3088 4326 1499119.6161673432 1164993.7957933834 -85.75999999999999 37.81999999999999 +4269 3088 -89.189 36.623000000000005 1192410.1993640265 1037818.2979744445 +3088 4326 1192410.1993640265 1037818.2979744445 -89.189 36.623 +4269 3088 -82.331 39.016999999999996 1796069.135715349 1303285.8838335248 +3088 4326 1796069.135715349 1303285.8838335248 -82.331 39.017 +4269 3089 -85.75999999999999 37.82 4918361.607375693 3822150.478032126 +3089 4326 4918361.607375693 3822150.478032126 -85.75999999999999 37.81999999999999 +4269 3089 -89.189 36.623000000000005 3912099.129080144 3404908.865604491 +3089 4326 3912099.129080144 3404908.865604491 -89.189 36.623 +4269 3089 -82.331 39.016999999999996 5892603.489426109 4275863.770210491 +3089 4326 5892603.489426109 4275863.770210491 -82.331 39.017 +4152 3090 -85.75999999999999 37.82 1499119.6161673432 1164993.7957933834 +3090 4326 1499119.6161673432 1164993.7957933834 -85.76000473140303 37.82000720694108 +4152 3090 -89.189 36.623000000000005 1192410.1993640265 1037818.2979744445 +3090 4326 1192410.1993640265 1037818.2979744445 -89.18900563309782 36.623006820406374 +4152 3090 -82.331 39.016999999999996 1796069.135715349 1303285.8838335248 +3090 4326 1796069.135715349 1303285.8838335248 -82.33100379393781 39.017007589310836 +4152 3091 -85.75999999999999 37.82 4918361.607375693 3822150.478032126 +3091 4326 4918361.607375693 3822150.478032126 -85.76000473140303 37.82000720694108 +4152 3091 -89.189 36.623000000000005 3912099.129080144 3404908.865604491 +3091 4326 3912099.129080144 3404908.865604491 -89.18900563309782 36.623006820406374 +4152 3091 -82.331 39.016999999999996 5892603.489426109 4275863.770210491 +3091 4326 5892603.489426109 4275863.770210491 -82.33100379393781 39.017007589310836 +4301 3092 124.565 24.46 658601.2954280704 2705789.426394146 +3092 4326 658601.2954280704 2705789.426394146 124.56335019196909 24.46417156592626 +4301 3092 123.7145 24.028 572649.5490031302 2657251.746606723 +3092 4326 572649.5490031302 2657251.746606723 123.71292901653155 24.032206986317743 +4301 3092 125.41550000000001 24.892000000000003 743978.2969373739 2754885.7846696647 +3092 4326 743978.2969373739 2754885.7846696647 125.41377109867072 24.896136553136113 +4301 3093 129.315 29.65 530483.5312586135 3279722.248808662 +3093 4326 530483.5312586135 3279722.248808662 129.31284999547705 29.653684464237354 +4301 3093 126.8985 24.924999999999997 287803.79559608066 2758014.3559082407 +3093 4326 287803.79559608066 2758014.3559082407 126.89664353875062 24.929150976477914 +4301 3093 131.7315 34.375 751139.0685034795 3806740.589389105 +3093 4326 751139.0685034795 3806740.589389105 131.72901626554483 34.37819810042261 +4301 3094 135.0 28.975 500000.0 3204900.791452905 +3094 4326 500000.0 3204900.791452905 134.99738134581239 28.97885840967205 +4301 3094 132.3 21.2305 219781.1030460932 2349822.003961382 +3094 4326 219781.1030460932 2349822.003961382 132.29775396673213 21.235083973054294 +4301 3094 137.7 36.7195 741111.7861254577 4066749.522616468 +3094 4326 741111.7861254577 4066749.522616468 137.6969026184647 36.72256604923565 +4301 3095 141.0 35.105000000000004 500000.0000000023 3884301.332046768 +3095 4326 500000.0000000023 3884301.332046768 140.99668597195483 35.10833657587323 +4301 3095 138.3 25.713500000000003 229107.22305268107 2846448.9378633373 +3095 4326 229107.22305268107 2846448.9378633373 138.29719598076713 25.717752774216816 +4301 3095 143.7 44.4965 714632.3156976164 4930067.411680213 +3095 4326 714632.3156976164 4930067.411680213 143.6959488230456 44.49882773757543 +4301 3096 144.935 43.620000000000005 333405.7862314546 4831252.059350936 +3096 4326 333405.7862314546 4831252.059350936 144.93089735210276 43.622477706675156 +4301 3096 144.0935 42.918000000000006 262802.26192922145 4755328.138253017 +3096 4326 262802.26192922145 4755328.138253017 144.08951865532148 42.92053201731233 +4301 3096 145.7765 44.321999999999996 402450.6521157896 4907868.8820336275 +3096 4326 402450.6521157896 4907868.8820336275 145.77227346132278 44.3244240951405 +4612 3097 124.19 25.405 619691.4110113059 2810327.34502011 +3097 4326 619691.4110113059 2810327.34502011 124.19000000000001 25.404999999999994 +4612 3097 122.56099999999999 21.5305 454538.0827993267 2380925.286207869 +3097 4326 454538.0827993267 2380925.286207869 122.56099999999999 21.5305 +4612 3097 125.819 29.2795 773886.5629422502 3242249.3828698383 +3097 4326 773886.5629422502 3242249.3828698383 125.819 29.279500000000006 +4612 3098 129.0 29.875 500000.00000000244 3304934.471297173 +3098 4326 500000.00000000244 3304934.471297173 129.0 29.875 +4612 3098 126.3 21.9955 221220.18393471796 2434790.286095488 +3098 4326 221220.18393471796 2434790.286095488 126.30000000000001 21.9955 +4612 3098 131.7 37.7545 737863.0975470991 4182009.825930984 +3098 4326 737863.0975470991 4182009.825930984 131.7 37.75449999999999 +4612 3099 135.0 30.32 500000.0 3354244.80937834 +3099 4326 500000.0 3354244.80937834 135.0 30.320000000000004 +4612 3099 132.3 18.413 214758.6089544095 2038003.2273633787 +3099 4326 214758.6089544095 2038003.2273633787 132.3 18.413 +4612 3099 137.7 42.227 722819.20735252 4678510.055377883 +3099 4326 722819.20735252 4678510.055377883 137.7 42.22699999999998 +4612 3100 141.0 31.839999999999996 500000.0000000024 3522701.124135246 +3100 4326 500000.0000000024 3522701.124135246 141.0 31.839999999999996 +4612 3100 138.3 19.051 215828.64715265244 2108657.1661159582 +3100 4326 215828.64715265244 2108657.1661159582 138.3 19.050999999999995 +4612 3100 143.7 44.62899999999999 714171.2341030437 4945284.762636639 +3100 4326 714171.2341030437 4945284.762636639 143.7 44.62899999999999 +4612 3101 145.93 34.34 401581.14578903903 3800373.9875940597 +3101 4326 401581.14578903903 3800373.9875940597 145.93 34.34 +4612 3101 144.193 24.161 214776.9353027034 2674912.8964886284 +3101 4326 214776.9353027034 2674912.8964886284 144.193 24.160999999999998 +4612 3101 147.667 44.519 553007.7980395162 4929735.99372344 +3101 4326 553007.7980395162 4929735.99372344 147.667 44.519 +4169 3102 -170.13 -14.27 453976.3050104875 311011.8616462023 +3102 4326 453976.3050104875 311011.8616462023 -170.1312599981501 -14.265036900786587 +4169 3102 -170.805 -14.414 215188.6493475804 258262.67296902544 +3102 4326 215188.6493475804 258262.67296902544 -170.80625054785642 -14.409024258345433 +4169 3102 -169.45499999999998 -14.126 693067.846148874 363066.78901986417 +3102 4326 693067.846148874 363066.78901986417 -169.45626926960273 -14.121049552859533 +4682 3106 90.34 23.58 534688.3713121953 2607553.5678988174 +3106 4326 534688.3713121953 2607553.5678988174 90.33717773112295 23.580722793728786 +4682 3106 88.24300000000001 20.826 317190.0573409684 2303699.4587135655 +3106 4326 317190.0573409684 2303699.4587135655 88.24049223084691 20.826914789103768 +4682 3106 92.437 26.334 743206.084410262 2914724.4376907675 +3106 4326 743206.084410262 2914724.4376907675 92.43384703459026 26.334532840306636 +4283 3107 135.0 -32.06 1000000.0 1993362.8919968458 +3107 4326 1000000.0 1993362.8919968458 135.0 -32.06 +4283 3107 129.591 -37.523 520975.9977354317 1375844.7065937442 +3107 4326 520975.9977354317 1375844.7065937442 129.591 -37.52300000000002 +4283 3107 140.409 -26.597 1539604.9560378764 2584795.614858562 +3107 4326 1539604.9560378764 2584795.614858562 140.409 -26.596999999999998 +4258 3108 -2.545 49.575 37718.21948834363 58349.39848987826 +3108 4326 37718.21948834363 58349.39848987826 -2.545 49.57500000000001 +4258 3108 -3.0085 49.2465 3908.8445522998736 21975.177337943645 +3108 4326 3908.8445522998736 21975.177337943645 -3.0085 49.2465 +4258 3108 -2.0815 49.9035 71077.99606376658 94932.34382218876 +3108 4326 71077.99606376658 94932.34382218876 -2.0815 49.9035 +4258 3109 -2.185 49.155 36352.7770195862 62216.265417917784 +3109 4326 36352.7770195862 62216.265417917784 -2.185 49.155 +4258 3109 -2.5225 48.8985 11588.391635136079 33762.031959826134 +3109 4326 11588.391635136079 33762.031959826134 -2.5225 48.8985 +4258 3109 -1.8475000000000001 49.4115 60863.02578841313 90781.51301000631 +3109 4326 60863.02578841313 90781.51301000631 -1.8475 49.4115 +4202 3110 145.5 -36.59 2544737.975659137 4545375.428688888 +3110 4326 2544737.975659137 4545375.428688888 145.50128584943727 -36.58848319474753 +4202 3110 141.41400000000002 -38.939 2189032.24632735 4278912.269272096 +3110 4326 2189032.24632735 4278912.269272096 141.41538628512032 -38.937540511597994 +4202 3110 149.58599999999998 -34.241 2922733.3455817327 4796004.441618165 +3110 4326 2922733.3455817327 4796004.441618165 149.58718814064883 -34.23943540491892 +4283 3111 145.5 -36.59 2544737.813041602 2545375.268505794 +3111 4326 2544737.813041602 2545375.268505794 145.5 -36.59000000000001 +4283 3111 141.41400000000002 -38.939 2189033.37764663 2278913.0514758993 +3111 4326 2189033.37764663 2278913.0514758993 141.41400000000002 -38.939000000000014 +4283 3111 149.58599999999998 -34.241 2922731.8103748485 2796003.3990888703 +3111 4326 2922731.8103748485 2796003.3990888703 149.58599999999998 -34.241000000000014 +4283 3112 133.26999999999998 -26.78 -71707.68538801056 -3036901.4555975227 +3112 4326 -71707.68538801056 -3036901.4555975227 133.26999999999998 -26.779999999999998 +4283 3112 114.892 -42.008 -1614114.5719233775 -4847489.270308387 +3112 4326 -1614114.5719233775 -4847489.270308387 114.892 -42.00800000000001 +4283 3112 151.648 -11.552 1963804.4971919449 -1489224.9978584847 +3112 4326 1963804.4971919449 -1489224.9978584847 151.648 -11.551999999999998 +4283 3113 153.03 -27.0 52977.61701066545 210809.9906914401 +3113 4326 52977.61701066545 210809.9906914401 153.03 -27.000000000000004 +4283 3113 152.436 -29.124 -4889.747957929503 -24701.58149011733 +3113 4326 -4889.747957929503 -24701.58149011733 152.436 -29.124 +4283 3113 153.624 -24.876 113056.11113807865 445975.3971938018 +3113 4326 113056.11113807865 445975.3971938018 153.624 -24.876 +4686 3114 -78.84 1.855 1137697.7972252036 696931.6769637578 +3114 4326 1137697.7972252036 696931.6769637578 -78.84 1.8550000000000002 +4686 3114 -79.074 1.2925 1111687.501135434 634707.0929375545 +3114 4326 1111687.501135434 634707.0929375545 -79.074 1.2925 +4686 3114 -78.606 2.4175 1163680.7486300017 759171.1122867132 +3114 4326 1163680.7486300017 759171.1122867132 -78.606 2.4175000000000004 +4686 3115 -77.08500000000001 5.120000000000001 999169.2906916202 1057922.9399547542 +3115 4326 999169.2906916202 1057922.9399547542 -77.08500000000001 5.12 +4686 3115 -78.43950000000001 0.5390000000000001 848376.0245772583 551384.0422383924 +3115 4326 848376.0245772583 551384.0422383924 -78.43950000000001 0.5389999999999999 +4686 3115 -75.73049999999999 9.701 1147831.0360293668 1564843.799208126 +3115 4326 1147831.0360293668 1564843.799208126 -75.73049999999999 9.701000000000002 +4686 3116 -74.08500000000001 4.655 999168.7178232059 1006502.1503822868 +3116 4326 999168.7178232059 1006502.1503822868 -74.08500000000001 4.655 +4686 3116 -75.43950000000001 -1.7934999999999999 848443.1699814878 293395.53487538325 +3116 4326 848443.1699814878 293395.53487538325 -75.43950000000001 -1.7935 +4686 3116 -72.73049999999999 11.1035 1147172.2458590663 1720015.4262290467 +3116 4326 1147172.2458590663 1720015.4262290467 -72.73049999999999 11.103499999999999 +4686 3117 -71.08500000000001 4.145 999168.1520639523 950105.9324001247 +3117 4326 999168.1520639523 950105.9324001247 -71.08500000000001 4.145 +4686 3117 -72.43950000000001 -3.3925000000000005 848633.4040423809 116533.42723466211 +3117 4326 848633.4040423809 116533.42723466211 -72.43950000000001 -3.392499999999999 +4686 3117 -69.73049999999999 11.6825 1146874.6743963847 1784079.4629146848 +3117 4326 1146874.6743963847 1784079.4629146848 -69.73049999999999 11.6825 +4686 3118 -68.23 2.0299999999999994 983035.2210097343 716235.0568886292 +3118 4326 983035.2210097343 716235.0568886292 -68.23 2.0299999999999994 +4686 3118 -69.45400000000001 -1.822 846831.7493169191 290242.01528700616 +3118 4326 846831.7493169191 290242.01528700616 -69.45400000000001 -1.8219999999999992 +4686 3118 -67.006 5.881999999999998 1118662.7038174167 1142302.0407685423 +3118 4326 1118662.7038174167 1142302.0407685423 -67.006 5.881999999999998 +4192 3119 9.425 3.575 880670.6052292119 1394986.9995986714 +3119 4326 880670.6052292119 1394986.9995986714 9.423752603956167 3.574236722934031 +4192 3119 8.5475 2.3015000000000003 782989.3300720847 1254384.8830549987 +3119 4326 782989.3300720847 1254384.8830549987 8.546222230872102 2.300724880356401 +4192 3119 10.3025 4.8485 978113.5494010978 1535605.1415537577 +3119 4326 978113.5494010978 1535605.1415537577 10.301282740884728 4.847749920175732 +4179 3120 21.075 50.67 4636410.987503263 5472004.985804065 +3120 4326 4636410.987503263 5472004.985804065 21.07324879904421 50.66971704406711 +4179 3120 18.3075 49.167 4434601.538896874 5308603.01760198 +3120 4326 4434601.538896874 5308603.01760198 18.305796260203415 49.16665586060892 +4179 3120 23.842499999999998 52.173 4825732.152613975 5642755.517939234 +3120 4326 4825732.152613975 5642755.517939234 23.840701425365477 52.17278017477282 +4683 3121 117.02000000000001 12.425 502174.500105857 1373945.6192639633 +3121 4326 502174.500105857 1373945.6192639633 117.02141466511856 12.423546224055787 +4683 3121 116.138 6.8315 404719.5195292097 755423.6295117124 +3121 4326 404719.5195292097 755423.6295117124 116.13955264930185 6.830409506381212 +4683 3121 117.902 18.0185 595516.1438237311 1993042.2868290956 +3121 4326 595516.1438237311 1993042.2868290956 117.90328282092284 18.016717531673695 +4683 3122 119.035 11.72 503815.3382339658 1295965.148752814 +3122 4326 503815.3382339658 1295965.148752814 119.03643064311524 11.718640412716615 +4683 3122 118.1035 3.89 400430.28872450266 430144.49605445075 +3122 4326 400430.28872450266 430144.49605445075 118.10512884174474 3.8891739307170425 +4683 3122 119.9665 19.55 601425.9249103947 2162599.697327467 +3122 4326 601425.9249103947 2162599.697327467 119.96774561906908 19.548172866580394 +4683 3123 120.955 12.31 495105.2345774761 1361225.4297761563 +3123 4326 495105.2345774761 1361225.4297761563 120.95641424301064 12.308645570000433 +4683 3123 119.8255 3.931 369556.77330574486 434716.55495880666 +3123 4326 369556.77330574486 434716.55495880666 119.82712677834512 3.930216756846166 +4683 3123 122.08449999999999 20.689 612989.012034662 2288772.460543498 +3123 4326 612989.012034662 2288772.460543498 122.08571635389981 20.68715261315119 +4683 3124 123.015 12.809999999999999 501628.43962046114 1416532.4641169563 +3124 4326 501628.43962046114 1416532.4641169563 123.01639867719794 12.808659660266656 +4683 3124 121.86749999999999 4.377 374292.65589384997 484032.8533284756 +3124 4326 374292.65589384997 484032.8533284756 121.86911246362959 4.376237695452895 +4683 3124 124.16250000000001 21.243 620670.9017438162 2350169.110299919 +3124 4326 620670.9017438162 2350169.110299919 124.16369948990051 21.241163926270346 +4683 3125 125.19 13.365 520580.83523996384 1477934.0008413163 +3125 4326 520580.83523996384 1477934.0008413163 125.19138030555284 13.363672171353535 +4683 3125 123.876 5.6205 375471.3725654749 621551.7740258112 +3125 4326 375471.3725654749 621551.7740258112 123.87757604324065 5.619697088172642 +4683 3125 126.504 21.109499999999997 656265.1756400255 2335684.7334106495 +3125 4326 656265.1756400255 2335684.7334106495 126.50519651749177 21.107718115174997 +10690 3126 19.369999999999997 60.21 520514.94169885473 6677527.260658987 +3126 4326 520514.94169885473 6677527.260658987 19.369999999999997 60.21000000000001 +10690 3126 19.253 60.092999999999996 514077.7358396014 6664461.178097184 +3126 4326 514077.7358396014 6664461.178097184 19.253 60.092999999999996 +10690 3126 19.487 60.327000000000005 526905.8139563954 6690604.896490097 +3126 4326 526905.8139563954 6690604.896490097 19.487 60.327000000000005 +10690 3127 20.0 60.2 500000.0 6676355.616312669 +3127 4326 500000.0 6676355.616312669 20.0 60.20000000000001 +10690 3127 19.55 59.948 474850.7330324101 6648364.8886981215 +3127 4326 474850.7330324101 6648364.8886981215 19.55 59.948 +10690 3127 20.45 60.452 524766.5569271637 6704517.513746917 +3127 4326 524766.5569271637 6704517.513746917 20.45 60.45199999999999 +10690 3128 21.0 64.58500000000001 500000.0 7165070.984975636 +3128 4326 500000.0 7165070.984975636 21.0 64.58500000000001 +10690 3128 20.55 60.3145 475128.8391778159 6689197.673625805 +3128 4326 475128.8391778159 6689197.673625805 20.55 60.31450000000001 +10690 3128 21.45 68.85549999999999 518122.596409577 7641372.776043423 +3128 4326 518122.596409577 7641372.776043423 21.45 68.85549999999998 +10690 3129 22.0 64.535 500000.0 7159496.687667551 +3129 4326 500000.0 7159496.687667551 22.0 64.53499999999998 +10690 3129 21.55 60.2375 475070.3243672019 6680618.701709228 +3129 4326 475070.3243672019 6680618.701709228 21.55 60.23749999999999 +10690 3129 22.45 68.8325 518141.3883174655 7638807.252067053 +3129 4326 518141.3883174655 7638807.252067053 22.45 68.83249999999998 +10690 3130 23.0 64.245 500000.0 7127166.516923541 +3130 4326 500000.0 7127166.516923541 23.0 64.245 +10690 3130 22.55 60.1995 475041.46373762976 6676384.960082177 +3130 4326 475041.46373762976 6676384.960082177 22.55 60.19949999999999 +10690 3130 23.45 68.2905 518583.3569009903 7578352.150334439 +3130 4326 518583.3569009903 7578352.150334439 23.450000000000003 68.29049999999998 +10690 3131 24.0 64.35 500000.0 7138872.119601392 +3131 4326 500000.0 7138872.119601392 24.000000000000004 64.35 +10690 3131 23.55 60.309 475124.6580374912 6688584.8866154235 +3131 4326 475124.6580374912 6688584.8866154235 23.55 60.30899999999999 +10690 3131 24.45 68.391 518501.5316184957 7589561.702908624 +3131 4326 518501.5316184957 7589561.702908624 24.45 68.391 +10690 3132 25.0 64.42 500000.0 7146675.948656818 +3132 4326 500000.0 7146675.948656818 25.0 64.41999999999999 +10690 3132 24.55 60.388 475184.7366934357 6697386.785107177 +3132 4326 475184.7366934357 6697386.785107177 24.55 60.388000000000005 +10690 3132 25.45 68.452 518451.8382883313 7596365.57688542 +3132 4326 518451.8382883313 7596365.57688542 25.45 68.452 +10690 3133 26.0 65.06 500000.0 7218028.700916069 +3133 4326 500000.0 7218028.700916069 26.0 65.06 +10690 3133 25.55 60.668 475398.05797506555 6728584.228499791 +3133 4326 475398.05797506555 6728584.228499791 25.55 60.667999999999985 +10690 3133 26.45 69.452 517634.2043470836 7707911.494540701 +3133 4326 517634.2043470836 7707911.494540701 26.45 69.452 +10690 3134 27.0 65.205 500000.0 7234195.418049183 +3134 4326 500000.0 7234195.418049183 27.0 65.205 +10690 3134 26.55 60.8445 475532.8329143718 6748250.431504073 +3134 4326 475532.8329143718 6748250.431504073 26.55 60.844500000000004 +10690 3134 27.45 69.5655 517541.0530563436 7720572.777543769 +3134 4326 517541.0530563436 7720572.777543769 27.45 69.5655 +10690 3135 28.0 65.255 500000.0 7239770.22109902 +3135 4326 500000.0 7239770.22109902 28.0 65.255 +10690 3135 27.55 60.9035 475577.9377112635 6754824.5184650915 +3135 4326 475577.9377112635 6754824.5184650915 27.55 60.90349999999999 +10690 3135 28.45 69.6065 517507.38646414754 7725146.497413838 +3135 4326 517507.38646414754 7725146.497413838 28.45 69.6065 +10690 3136 29.0 65.375 500000.0 7253149.900692343 +3136 4326 500000.0 7253149.900692343 29.000000000000004 65.375 +10690 3136 28.55 61.3835 475945.8633078706 6808310.753929716 +3136 4326 475945.8633078706 6808310.753929716 28.55 61.3835 +10690 3136 29.45 69.3665 517704.32913935016 7698373.809114772 +3136 4326 517704.32913935016 7698373.809114772 29.45 69.3665 +10690 3137 30.0 64.705 500000.0 7178449.453771112 +3137 4326 500000.0 7178449.453771112 29.999999999999996 64.705 +10690 3137 29.55 61.7575 476233.726410252 6849988.067773847 +3137 4326 476233.726410252 6849988.067773847 29.55 61.7575 +10690 3137 30.45 67.6525 519101.43896740885 7507194.19544731 +3137 4326 519101.43896740885 7507194.19544731 30.450000000000003 67.65249999999999 +10690 3138 31.045 63.175 502266.6161199273 7007891.3962724535 +3138 4326 502266.6161199273 7007891.3962724535 31.045 63.175000000000004 +10690 3138 30.5545 62.189499999999995 476801.83091857855 6898129.919650225 +3138 4326 476801.83091857855 6898129.919650225 30.554500000000004 62.189499999999995 +10690 3138 31.5355 64.1605 526052.308601862 7117855.991242048 +3138 4326 526052.308601862 7117855.991242048 31.535499999999995 64.1605 +4748 3139 179.325 -16.585 1246910.9213221075 1478618.027654105 +3139 4326 1246910.9213221075 1478618.027654105 179.3213308281889 -16.583707457232197 +4748 3139 178.51049999999998 -17.0215 815812.3708195495 1237602.4158131378 +3139 4326 815812.3708195495 1237602.4158131378 178.506816611454 -17.020155083979517 +4748 3139 -179.8605 -16.148500000000002 1679958.4588530231 1717880.0471291183 +3139 4326 1679958.4588530231 1717880.0471291183 -179.8641544906895 -16.147259449301465 +4752 3140 177.97 -17.785 528186.957593395 822280.0064899398 +3140 4326 528186.957593395 822280.0064899398 177.966291520334 -17.783588593705314 +4752 3140 177.268 -18.2665 159210.04048550606 556612.0596289269 +3140 4326 159210.04048550606 556612.0596289269 177.26427196261955 -18.265037684175986 +4752 3140 178.672 -17.3035 899147.9310247395 1086547.2851588053 +3140 4326 899147.9310247395 1086547.2851588053 178.668311268701 -17.302139262110355 +4721 3141 178.405 -17.66 649037.7460085102 8046851.842284549 +3141 4326 649037.7460085102 8046851.842284549 178.40130410330565 -17.661891408381898 +4721 3141 176.9695 -19.064 496790.97631338093 7892064.639113881 +3141 4326 496790.97631338093 7892064.639113881 176.96571477076333 -19.065869392414495 +4721 3141 179.8405 -16.256 803637.4767341327 8200617.282566345 +3141 4326 803637.4767341327 8200617.282566345 179.83689217044346 -16.25790854128818 +4721 3142 -179.885 -16.57 192097.42114373 8165778.591147177 +3142 4326 192097.42114373 8165778.591147177 -179.88860173331713 -16.571915469408093 +4721 3142 -179.9885 -16.993 181749.19734224584 8118769.365799929 +3142 4326 181749.19734224584 8118769.365799929 -179.9921142144151 -16.994916651165365 +4721 3142 -179.78150000000002 -16.147000000000002 202510.01372607198 8212773.484474999 +3142 4326 202510.01372607198 8212773.484474999 -179.78508948987442 -16.14891416311262 +4741 3144 -6.91 61.87 390034.519912245 487282.57800699264 +3144 4326 390034.519912245 487282.57800699264 -6.915594439268875 61.87080381181611 +4741 3144 -7.432 61.384 416184.70456909423 432360.98057666776 +3144 4326 416184.70456909423 432360.98057666776 -7.437542334587762 61.384765282560856 +4741 3144 -6.388 62.355999999999995 364762.50403396215 542399.8086942924 +3144 4326 364762.50403396215 542399.8086942924 -6.393648164657063 62.356842483804435 +4258 3145 -6.91 61.87 390040.06322393555 487283.12746954104 +3145 4326 390040.06322393555 487283.12746954104 -6.910000000000001 61.86999999999999 +4258 3145 -7.432 61.384 416188.92113455955 432363.9457981475 +3145 4326 416188.92113455955 432363.9457981475 -7.432 61.384 +4258 3145 -6.388 62.355999999999995 364769.33483987895 542397.9159958865 +3145 4326 364769.33483987895 542397.9159958865 -6.387999999999999 62.35600000000001 +4131 3148 105.07 15.864999999999998 507493.3925354304 1753863.1606297838 +3148 4326 507493.3925354304 1753863.1606297838 105.0660762919968 15.866651974619035 +4131 3148 102.433 9.0835 217863.76143819257 1005002.9619173191 +3148 4326 217863.76143819257 1005002.9619173191 102.42951571911529 9.085679342973123 +4131 3148 107.707 22.646499999999996 778169.2281503465 2506713.628290241 +3148 4326 778169.2281503465 2506713.628290241 107.70255803824706 22.64760961763224 +4131 3149 108.765 15.995 260848.8842704911 1769526.2443358132 +3149 4326 260848.8842704911 1769526.2443358132 108.76060070585746 15.996712376966075 +4131 3149 108.0765 10.9865 180525.0903487875 1215945.027471397 +3149 4326 180525.0903487875 1215945.027471397 108.07227651518669 10.988595819748108 +4131 3149 109.4535 21.0035 339285.5098309949 2323122.709126247 +3149 4326 339285.5098309949 2323122.709126247 109.4488821238499 21.004805038030362 +4619 3152 17.975 59.325 95464.74644482673 78223.73096821923 +3152 4326 95464.74644482673 78223.73096821923 17.975 59.325 +4619 3152 17.7725 59.2305 83890.90133894447 67728.41097605042 +3152 4326 83890.90133894447 67728.41097605042 17.7725 59.23050000000001 +4619 3152 18.1775 59.4195 106974.63175901676 88754.30083347112 +3152 4326 106974.63175901676 88754.30083347112 18.1775 59.4195 +4617 3153 -126.56 54.129999999999995 963492.2540470979 1014264.2610207349 +3153 4326 963492.2540470979 1014264.2610207349 -126.56001749646673 54.13000540557788 +4617 3153 -137.792 48.838 137055.92070801044 496681.58307666087 +3153 4326 137055.92070801044 496681.58307666087 -137.79201699966563 48.838003914768336 +4617 3153 -115.328 59.422 1604477.6599223828 1649926.3765193184 +3153 4326 1604477.6599223828 1649926.3765193184 -115.32801768628744 59.42200707600895 +4617 3154 -139.505 62.29 577553.7482862907 6907382.944374962 +3154 4326 577553.7482862907 6907382.944374962 -139.50502351145155 62.290003387180604 +4617 3154 -140.8595 53.074 509412.8752931231 5880511.6950232545 +3154 4326 509412.8752931231 5880511.6950232545 -140.85951858488576 53.07400345609418 +4617 3154 -138.1505 71.506 600849.7762664382 7936211.238253396 +3154 4326 600849.7762664382 7936211.238253396 -138.15053438200366 71.50600323030262 +4617 3155 -135.0 63.74 500000.0 7068042.170895341 +3155 4326 500000.0 7068042.170895341 -135.00002424252526 63.74000408965688 +4617 3155 -137.7 49.628 305021.49189101974 5500772.589930895 +3155 4326 305021.49189101974 5500772.589930895 -137.70001721961106 49.62800392196697 +4617 3155 -132.3 77.852 563406.2209931078 8643313.609433059 +3155 4326 563406.2209931078 8643313.609433059 -132.30005117040452 77.85200400505913 +4617 3156 -129.0 63.72500000000001 500000.0 7066370.74190974 +3156 4326 500000.0 7066370.74190974 -129.00002337554528 63.725005077267184 +4617 3156 -131.7 48.240500000000004 299538.3307894394 5346555.905977527 +3156 4326 299538.3307894394 5346555.905977527 -131.70001627488972 48.24050463840386 +4617 3156 -126.3 79.2095 556411.4973791244 8794662.018231109 +3156 4326 556411.4973791244 8794662.018231109 -126.30005559716399 79.20950506041318 +4617 3157 -123.0 64.965 500000.0 7204553.9102763105 +3157 4326 500000.0 7204553.9102763105 -123.00002326629863 64.9650060297184 +4617 3157 -125.7 49.813500000000005 305763.35086729913 5521392.586308251 +3157 4326 305763.35086729913 5521392.586308251 -125.70001592998933 49.81350539193366 +4617 3157 -120.3 80.1165 551719.885517777 8895789.811727198 +3157 4326 551719.885517777 8895789.811727198 -120.30005778987828 80.11650610265912 +4617 3158 -99.0 66.495 500000.0 7375085.928094508 +3158 4326 500000.0 7375085.928094508 -99.0000168400363 66.4950094234008 +4617 3158 -101.7 50.740500000000004 309501.2133574041 5624444.723641862 +3158 4326 309501.2133574041 5624444.723641862 -101.70001103188612 50.740508054105405 +4617 3158 -96.3 82.2495 540636.8512974723 9133629.104307733 +3158 4326 540636.8512974723 9133629.104307733 -96.30005087052324 82.24950969198301 +4617 3159 -93.0 66.015 499999.9999999994 7321581.988534806 +3159 4326 499999.9999999994 7321581.988534806 -93.00001395613482 66.01501006723561 +4617 3159 -95.7 49.8285 305823.42915909307 5523059.994057171 +3159 4326 305823.42915909307 5523059.994057171 -95.70000912350926 49.82850848062027 +4617 3159 -90.3 82.2015 540886.9459985999 9128276.65995035 +3159 4326 540886.9459985999 9128276.65995035 -90.30004309785345 82.20151039350597 +4617 3160 -87.0 65.055 500000.0000000006 7214584.245044757 +3160 4326 500000.0000000006 7214584.245044757 -87.00001074615945 65.05501058725861 +4617 3160 -89.7 48.0045 298617.3852891464 5320328.070626806 +3160 4326 298617.3852891464 5320328.070626806 -89.70000699174449 48.00450869477358 +4617 3160 -84.3 82.1055 541387.0478100256 9117571.799824689 +3160 4326 541387.0478100256 9117571.799824689 -84.30003468503867 82.10551098953695 +4269 3161 -84.755 49.285 947769.8385562148 12518066.446554366 +3161 4326 947769.8385562148 12518066.446554366 -84.755 49.285000000000004 +4269 3161 -94.1195 42.4315 178864.91928440868 11802247.578280732 +3161 4326 178864.91928440868 11802247.578280732 -94.1195 42.4315 +4269 3161 -75.3905 56.1385 1528770.4944857555 13318409.609850004 +3161 4326 1528770.4944857555 13318409.609850004 -75.39050000000002 56.13850000000001 +4617 3162 -84.755 49.285 947769.8385562148 12518066.446554366 +3162 4326 947769.8385562148 12518066.446554366 -84.75500560391868 49.285009196801326 +4617 3162 -94.1195 42.4315 178864.91928440868 11802247.578280732 +3162 4326 178864.91928440868 11802247.578280732 -94.11950754293277 42.43150757194957 +4617 3162 -75.3905 56.1385 1528770.4944857555 13318409.609850004 +3162 4326 1528770.4944857555 13318409.609850004 -75.39050328328447 56.1385106045279 +4749 3163 165.865 -21.115000000000002 385975.9358495856 342617.387136278 +3163 4326 385975.9358495856 342617.387136278 165.865 -21.11500000000001 +4749 3163 163.77249999999998 -22.5685 170903.81819307042 180057.5801144105 +3163 4326 170903.81819307042 180057.5801144105 163.77249999999998 -22.56850000000002 +4749 3163 167.9575 -19.6615 605358.7790883179 502270.9563636172 +3163 4326 605358.7790883179 502270.9563636172 167.9575 -19.661499999999965 +4750 3164 166.575 -20.555 664183.8046927562 7726308.000003106 +3164 4326 664183.8046927562 7726308.000003106 166.57497475589003 -20.555007867413877 +4750 3164 166.4535 -20.7485 651324.3653432089 7705006.0565709835 +3164 4326 651324.3653432089 7705006.0565709835 166.45347591457733 -20.748505934909733 +4750 3164 166.69650000000001 -20.3615 677073.8771008988 7747601.951748264 +3164 4326 677073.8771008988 7747601.951748264 166.69647359994332 -20.36150980042817 +4644 3165 166.445 -22.28 265.9685559806203 -1140.475289330319 +3165 4326 265.9685559806203 -1140.475289330319 166.4483286516378 -22.277234939538634 +4644 3165 166.3595 -22.361 -8540.952195876016 -10112.475842951735 +3165 4326 -8540.952195876016 -10112.475842951735 166.36282952307113 -22.358237331149613 +4644 3165 166.5305 -22.199 9083.034133211115 7826.449782194289 +3165 4326 9083.034133211115 7826.449782194289 166.53382778042933 -22.196232568461614 +4644 3166 166.445 -22.28 265.96916777513087 -1140.4747949938287 +3166 4326 265.96916777513087 -1140.4747949938287 166.4483286516378 -22.277234939538666 +4644 3166 166.3595 -22.361 -8540.94717064987 -10112.479672578023 +3166 4326 -8540.94717064987 -10112.479672578023 166.36282952307113 -22.358237331149617 +4644 3166 166.5305 -22.199 9083.03034724089 7826.45460571399 +3166 4326 9083.03034724089 7826.45460571399 166.53382778042933 -22.19623256846164 +4751 3167 102.095 3.965 22649.735792819927 21801.93890785653 +3167 4326 22649.735792819927 21801.93890785653 102.09349122159195 3.964751760670095 +4751 3167 99.8405 1.4855 10144.430224993042 8204.499441319023 +3167 4326 10144.430224993042 8204.499441319023 99.83929043811793 1.4854340001090145 +4751 3167 104.34949999999999 6.4445 35079.40106713317 35431.8388091362 +3167 4326 35079.40106713317 35431.8388091362 104.34768974221431 6.444074701963213 +4751 3168 102.095 3.965 455639.208408625 438584.28533625626 +3168 4326 455639.208408625 438584.28533625626 102.09349122159195 3.964751760670095 +4751 3168 99.8405 1.4855 204073.02759521012 165047.91336315108 +3168 4326 204073.02759521012 165047.91336315108 99.83929043811793 1.4854340001090145 +4751 3168 104.34949999999999 6.4445 705683.7518936575 712773.6559547235 +3168 4326 705683.7518936575 712773.6559547235 104.34768974221431 6.444074701963225 +4749 3169 159.13 -20.685000000000002 513538.8974309316 7712708.207789224 +3169 4326 513538.8974309316 7712708.207789224 159.13 -20.684999999999995 +4749 3169 156.538 -25.4955 252517.6418744477 7177894.75008237 +3169 4326 252517.6418744477 7177894.75008237 156.538 -25.495499999999993 +4749 3169 161.72199999999998 -15.874500000000001 791506.0347748799 8243051.580018548 +3169 4326 791506.0347748799 8243051.580018548 161.72199999999998 -15.874500000000006 +4749 3170 165.0 -20.64 499999.9999999973 7717693.710540362 +3170 4326 499999.9999999973 7717693.710540362 165.0 -20.64 +4749 3170 162.3 -25.869 229428.86486812617 7136040.499022016 +3170 4326 229428.86486812617 7136040.499022016 162.3 -25.869 +4749 3170 167.7 -15.411000000000001 789801.0439407314 8294399.228510025 +3170 4326 789801.0439407314 8294399.228510025 167.7 -15.411 +4749 3171 171.14 -22.85 514363.228105076 7473078.222053771 +3171 4326 514363.228105076 7473078.222053771 171.14 -22.85 +4749 3171 168.314 -25.64 230315.15969478886 7161446.219384035 +3171 4326 230315.15969478886 7161446.219384035 168.314 -25.640000000000004 +4749 3171 173.966 -20.060000000000002 810248.0691406042 7779122.946307896 +3171 4326 810248.0691406042 7779122.946307896 173.966 -20.06 +4641 3172 168.095 -21.53 199058.17168763588 7616361.993662431 +3172 4326 199058.17168763588 7616361.993662431 168.0927482311946 -21.5313866731356 +4641 3172 168.0095 -21.692 190540.36000592803 7598245.986594532 +3172 4326 190540.36000592803 7598245.986594532 168.00724219590694 -21.69338661979815 +4641 3172 168.1805 -21.368000000000002 207597.63629921008 7634470.867159105 +3172 4326 207597.63629921008 7634470.867159105 168.17825424019404 -21.369386710227182 +4753 3173 -6.91 61.87 590034.519912245 687282.5780069926 +4753 3173 -7.432 61.384 616184.7045690942 632360.9805766678 +4753 3173 -6.388 62.355999999999995 564762.5040339621 742399.8086942924 +4269 3174 -83.82 45.865 1049293.592424492 1033156.0764169473 +3174 4326 1049293.592424492 1033156.0764169473 -83.82 45.86500000000001 +4269 3174 -92.235 41.4775 350821.23840753443 576436.4304853533 +3174 4326 350821.23840753443 576436.4304853533 -92.235 41.47749999999999 +4269 3174 -75.405 50.2525 1645223.9508320438 1557482.9667874957 +3174 4326 1645223.9508320438 1557482.9667874957 -75.405 50.252500000000005 +4269 3175 -73.96000000000001 46.605000000000004 1708877.319332227 1156364.7480445416 +3175 4326 1708877.319332227 1156364.7480445416 -73.96000000000001 46.605 +4269 3175 -91.249 41.551500000000004 333227.2552575765 586422.0709842005 +3175 4326 333227.2552575765 586422.0709842005 -91.249 41.551500000000004 +4269 3175 -56.671 51.6585 2813632.83789334 1979676.5182426488 +3175 4326 2813632.83789334 1979676.5182426488 -56.67100000000001 51.65849999999998 +4131 3176 108.27000000000001 9.57 749130.0986103803 1058608.2042141524 +3176 4326 749130.0986103803 1058608.2042141524 108.2658127641701 9.572389613051298 +4131 3176 106.71300000000001 8.148 578534.7389056084 900658.0057752058 +3176 4326 578534.7389056084 900658.0057752058 106.70903053370337 8.15048773510211 +4131 3176 109.827 10.992 918321.0533990358 1217665.5500249073 +3176 4326 918321.0533990358 1217665.5500249073 109.82259370579838 10.994295442986958 +4754 3177 17.759999999999998 27.365 1074929.9321393424 3017746.9260398787 +3177 4326 1074929.9321393424 3017746.9260398787 17.759584857794774 27.365269791101237 +4754 3177 10.155000000000001 20.2865 286170.088227947 2251090.677265789 +3177 4326 286170.088227947 2251090.677265789 10.154316348949573 20.28664587248446 +4754 3177 25.364999999999995 34.4435 1767086.6878484795 3831377.3895375333 +3177 4326 1767086.6878484795 3831377.3895375333 25.364891105261126 34.443913551841234 +4747 3178 -73.5 76.78 538289.0707762385 8522711.038211726 +3178 4326 538289.0707762385 8522711.038211726 -73.5 76.78 +4747 3178 -74.85 74.746 504405.18397537933 8295272.96522508 +3178 4326 504405.18397537933 8295272.96522508 -74.85 74.74599999999998 +4747 3178 -72.15 78.81400000000001 561697.8352564909 8750720.678720579 +3178 4326 561697.8352564909 8750720.678720579 -72.15 78.814 +4747 3179 -69.0 77.07 500000.0 8554584.63942836 +3179 4326 500000.0 8554584.63942836 -69.0 77.06999999999998 +4747 3179 -71.7 73.62299999999999 415052.5929528638 8171900.0913808355 +3179 4326 415052.5929528638 8171900.0913808355 -71.7 73.62299999999999 +4747 3179 -66.3 80.51700000000001 549643.972935441 8940445.747991234 +3179 4326 549643.972935441 8940445.747991234 -66.30000000000001 80.517 +4747 3180 -63.0 75.57 500000.0 8387205.421968994 +3180 4326 500000.0 8387205.421968994 -63.0 75.57 +4747 3180 -65.7 69.58500000000001 394920.4263892819 7721915.41390065 +3180 4326 394920.4263892819 7721915.41390065 -65.7 69.585 +4747 3180 -60.3 81.55499999999999 544252.4978110739 9056186.874778235 +3180 4326 544252.4978110739 9056186.874778235 -60.300000000000004 81.55499999999998 +4747 3181 -57.0 71.455 500000.00000000047 7928144.05826439 +3181 4326 500000.00000000047 7928144.05826439 -57.00000000000001 71.455 +4747 3181 -59.7 60.1645 350176.53405712434 6672794.498472621 +3181 4326 350176.53405712434 6672794.498472621 -59.7 60.16449999999998 +4747 3181 -54.3 82.74549999999999 538050.8761024377 9188938.2391538 +3181 4326 538050.8761024377 9188938.2391538 -54.300000000000004 82.74549999999998 +4747 3182 -51.0 70.45 500000.00000000023 7816057.237076965 +3182 4326 500000.00000000023 7816057.237076965 -51.0 70.44999999999999 +4747 3182 -53.7 58.254999999999995 341567.45399161207 6460276.104224575 +3182 4326 341567.45399161207 6460276.104224575 -53.7 58.25499999999999 +4747 3182 -48.3 82.645 538575.0891829791 9177731.37023381 +3182 4326 538575.0891829791 9177731.37023381 -48.3 82.645 +4747 3183 -45.0 70.19 500000.0 7787061.668929534 +3183 4326 500000.0 7787061.668929534 -45.0 70.18999999999998 +4747 3183 -47.7 57.761 339368.78144102194 6405304.269258134 +3183 4326 339368.78144102194 6405304.269258134 -47.7 57.760999999999996 +4747 3183 -42.3 82.619 538710.6869410763 9174832.08715213 +3183 4326 538710.6869410763 9174832.08715213 -42.300000000000004 82.61899999999999 +4747 3184 -39.0 70.28 499999.99999999977 7797098.500921228 +3184 4326 499999.99999999977 7797098.500921228 -39.0 70.28 +4747 3184 -41.7 57.932 340128.50421514304 6424332.602071338 +3184 4326 340128.50421514304 6424332.602071338 -41.7 57.932 +4747 3184 -36.3 82.628 538663.7501701437 9175835.684843129 +3184 4326 538663.7501701437 9175835.684843129 -36.3 82.628 +4747 3185 -33.0 72.08 500000.00000000023 7997855.8686510315 +3185 4326 500000.00000000023 7997855.8686510315 -33.0 72.08 +4747 3185 -35.7 61.352 355615.8520996929 6804982.051249911 +3185 4326 355615.8520996929 6804982.051249911 -35.7 61.35199999999998 +4747 3185 -30.3 82.80799999999999 537724.8128858197 9195907.704626404 +3185 4326 537724.8128858197 9195907.704626404 -30.299999999999997 82.80799999999999 +4747 3186 -27.0 74.47999999999999 500000.0 8265589.813091837 +3186 4326 500000.0 8265589.813091837 -27.0 74.47999999999999 +4747 3186 -29.7 65.91199999999999 377064.49928880425 7312746.387061062 +3186 4326 377064.49928880425 7312746.387061062 -29.699999999999996 65.91199999999998 +4747 3186 -24.3 83.048 536472.3100089354 9222670.589379458 +3186 4326 536472.3100089354 9222670.589379458 -24.3 83.048 +4747 3187 -21.0 75.85 499999.99999999977 8418447.981524585 +3187 4326 499999.99999999977 8418447.981524585 -21.0 75.85 +4747 3187 -23.7 68.515 389670.25863388175 7602703.528236304 +3187 4326 389670.25863388175 7602703.528236304 -23.7 68.51499999999999 +4747 3187 -18.3 83.185 535757.0468356095 9237947.831832321 +3187 4326 535757.0468356095 9237947.831832321 -18.300000000000004 83.185 +4747 3188 -15.0 76.33500000000001 499999.9999999996 8472566.28101638 +3188 4326 499999.9999999996 8472566.28101638 -14.999999999999998 76.33500000000001 +4747 3188 -17.7 69.4365 394189.5206898229 7705369.890225201 +3188 4326 394189.5206898229 7705369.890225201 -17.7 69.4365 +4747 3188 -12.3 83.23349999999999 535503.7832605033 9243356.215031182 +3188 4326 535503.7832605033 9243356.215031182 -12.3 83.23349999999999 +4747 3189 -9.0 78.215 500000.0 8682363.88358801 +3189 4326 500000.0 8682363.88358801 -9.0 78.21499999999999 +4747 3189 -11.7 73.00850000000001 411958.77510598185 8103412.298741782 +3189 4326 411958.77510598185 8103412.298741782 -11.699999999999998 73.0085 +4747 3189 -6.300000000000001 83.4215 534521.818052858 9264320.748829093 +3189 4326 534521.818052858 9264320.748829093 -6.300000000000001 83.4215 +4754 3190 9.66 27.93 264961.05797472055 3090748.66307543 +3190 4326 264961.05797472055 3090748.66307543 9.659254777556308 27.93024464147966 +4754 3190 9.345 25.626 234649.10206638946 2835342.8190093087 +3190 4326 234649.10206638946 2835342.8190093087 9.344257427100946 25.626209621409142 +4754 3190 9.975 30.233999999999998 293854.22127157834 3346337.0123636466 +3190 4326 293854.22127157834 3346337.0123636466 9.974250805155105 30.234282744035923 +4754 3191 11.0 28.369999999999997 200000.0 3139333.588606555 +3191 4326 200000.0 3139333.588606555 10.99930534176855 28.370258439064532 +4754 3191 10.1 23.996000000000002 108418.79482428127 2655040.632376347 +3191 4326 108418.79482428127 2655040.632376347 10.09929608049074 23.996191252165808 +4754 3191 11.9 32.744 284350.8487254244 3624589.330015646 +3191 4326 284350.8487254244 3624589.330015646 11.899311346617754 32.74433698138446 +4754 3192 13.0 27.93 200000.0 3090573.4095521625 +3192 4326 200000.0 3090573.4095521625 12.999388569018636 27.930261096980136 +4754 3192 12.1 23.313000000000002 107941.99449838567 2579392.222307358 +3192 4326 107941.99449838567 2579392.222307358 12.099376732669398 23.31319081512907 +4754 3192 13.9 32.547000000000004 284535.9886821471 3602741.8397272173 +3192 4326 284535.9886821471 3602741.8397272173 13.89939746175598 32.54734367252754 +4754 3193 15.0 27.7 200000.0 3065086.3818060555 +3193 4326 200000.0 3065086.3818060555 14.999470823526583 27.700265737590506 +4754 3193 14.1 23.119 107808.9405181049 2557906.155862624 +3193 4326 107808.9405181049 2557906.155862624 14.099455286026384 23.11919566601532 +4754 3193 15.9 32.281 284784.3868980726 3573243.247831977 +3193 4326 284784.3868980726 3573243.247831977 15.899484318151357 32.281347658723014 +4754 3194 17.005000000000003 26.925 200496.59941806985 2979212.4223496635 +3194 4326 200496.59941806985 2979212.4223496635 17.004555738500052 26.925260530519882 +4754 3194 16.1005 22.951500000000003 107746.19301838953 2539355.1440080763 +3194 4326 107746.19301838953 2539355.1440080763 16.1000341994215 22.951699797732804 +4754 3194 17.9095 30.8985 286954.1303579755 3419955.066418375 +3194 4326 286954.1303579755 3419955.066418375 17.909076743087184 30.898829957812097 +4754 3195 19.0 26.855 200000.0 2971456.523154389 +3195 4326 200000.0 2971456.523154389 18.999637347706734 26.855265127857617 +4754 3195 18.1 22.0715 107108.76817084585 2441901.3239581822 +3195 4326 107108.76817084585 2441901.3239581822 18.099615410208134 22.071693216220975 +4754 3195 19.9 31.6385 285376.8169043691 3501996.9663369097 +3195 4326 285376.8169043691 3501996.9663369097 19.89965866613331 31.638849313356186 +4754 3196 21.0 26.77 200000.0 2962038.761144849 +3196 4326 200000.0 2962038.761144849 20.9997195375684 26.770268323549676 +4754 3196 20.1 21.163 106526.56750331659 2341301.766868488 +3196 4326 106526.56750331659 2341301.766868488 20.09969607489826 21.16318529879297 +4754 3196 21.9 32.376999999999995 284694.95008826925 3583889.2153392946 +3196 4326 284694.95008826925 3583889.2153392946 21.899742672148502 32.37736785295942 +4754 3197 23.0 26.235 200000.0 2902764.853491188 +3197 4326 200000.0 2902764.853491188 22.999802714584256 26.23526319944108 +4754 3197 22.1 20.1735 105919.1163821474 2231744.9750922625 +3197 4326 105919.1163821474 2231744.9750922625 22.09977616918738 20.173675480541952 +4754 3197 23.9 32.296499999999995 284769.96270065627 3574962.117249057 +3197 4326 284769.96270065627 3574962.117249057 23.899829890189544 32.29686971106306 +4754 3198 24.605 26.07 160477.0886684528 2884544.885067748 +3198 4326 160477.0886684528 2884544.885067748 24.60486878751115 26.070262431391875 +4754 3198 24.0605 20.598 102058.2090474214 2278767.0905442303 +3198 4326 102058.2090474214 2278767.0905442303 24.060352620703068 20.59818309046261 +4754 3198 25.1495 31.541999999999998 214196.40559098587 3490955.3021378326 +3198 4326 214196.40559098587 3490955.3021378326 25.149385289627066 31.542356896152043 +4754 3199 10.655000000000001 28.715000000000003 661656.1519142877 3177577.9384145197 +3199 4326 661656.1519142877 3177577.9384145197 10.654289187721188 28.715262053553747 +4754 3199 9.4445 24.0305 545202.1009161142 2657710.0053336094 +3199 4326 545202.1009161142 2657710.0053336094 9.443770697688954 24.030688639880147 +4754 3199 11.8655 33.3995 766525.9432300858 3699304.424948763 +3199 4326 766525.9432300858 3699304.424948763 11.864804754687256 33.399848817584875 +4132 3200 50.370000000000005 29.715 2019375.7876993867 870772.9391712327 +3200 4326 2019375.7876993867 870772.9391712327 50.370843902352405 29.71664492755634 +4132 3200 47.454 26.5605 1745483.298312109 510338.0778426647 +3200 4326 1745483.298312109 510338.0778426647 47.45467546548551 26.562324318596513 +4132 3200 53.286 32.8695 2273813.0672463705 1237204.8694464334 +3200 4326 2273813.0672463705 1237204.8694464334 53.287023606272676 32.87097829467244 +4754 3201 15.0 28.869999999999997 500000.0000000016 3193628.505986281 +3201 4326 500000.0000000016 3193628.505986281 14.9994650068843 28.870284599822284 +4754 3201 12.3 23.146 223515.68217713526 2562277.608260173 +3201 4326 223515.68217713526 2562277.608260173 12.29938524187525 23.146189405879674 +4754 3201 17.7 34.593999999999994 747633.9188071603 3831394.893039671 +3201 4326 747633.9188071603 3831394.893039671 17.69954964617055 34.59439901299088 +4754 3202 21.0 27.265 500000.00000000093 3015829.01427655 +3202 4326 500000.00000000093 3015829.01427655 20.99971830588918 27.26527626301654 +4754 3202 18.290999999999997 20.2765 217030.24625454674 2244427.005142026 +3202 4326 217030.24625454674 2244427.005142026 18.290627435599035 20.27667032171513 +4754 3202 23.709000000000003 34.2535 749469.9499214486 3793644.186098525 +3202 4326 749469.9499214486 3793644.186098525 23.708817545859578 34.25390774326694 +4754 3203 25.105 26.795 311619.0035709859 2965175.636507625 +3203 4326 311619.0035709859 2965175.636507625 25.104888613321204 26.795274403922615 +4754 3203 24.110500000000002 20.670499999999997 198934.73670163343 2288391.9753555334 +3203 4326 198934.73670163343 2288391.9753555334 24.11035451988933 20.6706840874823 +4754 3203 26.0995 32.9195 415797.6380079011 3642777.933788736 +3203 4326 415797.6380079011 3642777.933788736 26.099425277548153 32.91988383898785 +4326 3204 -66.0 -62.0 0.0 3398971.648970895 +4326 3204 -71.4 -63.8 -265871.6824507585 3187295.195751669 +4326 3204 -60.6 -60.2 299219.1226767111 3587067.503366566 +4326 3205 -54.0 -62.0 0.0 3398971.648970895 +4326 3205 -59.4 -63.8 -265871.6824507579 3187295.195751669 +4326 3205 -48.6 -60.2 299219.1226767105 3587067.503366566 +4326 3206 -42.0 -62.0 0.0 3398971.648970895 +4326 3206 -47.4 -63.8 -265871.6824507579 3187295.195751669 +4326 3206 -36.6 -60.2 299219.12267671083 3587067.503366566 +4326 3207 -174.0 -66.0 0.0 2846666.914788812 +4326 3207 -179.4 -67.8 -227554.69202721244 2636148.545211098 +4326 3207 -168.6 -64.2 262074.08624964627 3036044.7198421992 +4326 3208 -66.0 -66.0 0.0 2846666.914788812 +4326 3208 -71.4 -67.8 -227554.69202721195 2636148.545211098 +4326 3208 -60.6 -64.2 262074.0862496469 3036044.719842199 +4326 3209 -54.0 -66.0 0.0 2846666.914788812 +4326 3209 -59.4 -67.8 -227554.6920272114 2636148.545211098 +4326 3209 -48.6 -64.2 262074.08624964627 3036044.7198421992 +4326 3210 42.0 -66.0 0.0 2846666.914788812 +4326 3210 36.6 -67.8 -227554.69202721168 2636148.545211098 +4326 3210 47.4 -64.2 262074.08624964627 3036044.7198421992 +4326 3211 54.0 -66.0 0.0 2846666.914788812 +4326 3211 48.6 -67.8 -227554.6920272114 2636148.545211098 +4326 3211 59.4 -64.2 262074.08624964627 3036044.7198421992 +4326 3212 66.0 -66.0 0.0 2846666.914788812 +4326 3212 60.6 -67.8 -227554.69202721195 2636148.545211098 +4326 3212 71.4 -64.2 262074.0862496469 3036044.719842199 +4326 3213 78.0 -66.0 0.0 2846666.914788812 +4326 3213 72.6 -67.8 -227554.6920272114 2636148.545211098 +4326 3213 83.4 -64.2 262074.0862496469 3036044.719842199 +4326 3214 90.0 -66.0 0.0 2846666.914788812 +4326 3214 84.6 -67.8 -227554.69202721195 2636148.545211098 +4326 3214 95.4 -64.2 262074.0862496469 3036044.719842199 +4326 3215 102.0 -66.0 0.0 2846666.914788812 +4326 3215 96.6 -67.8 -227554.69202721195 2636148.545211098 +4326 3215 107.4 -64.2 262074.0862496469 3036044.719842199 +4326 3216 114.0 -66.0 0.0 2846666.914788812 +4326 3216 108.6 -67.8 -227554.69202721195 2636148.545211098 +4326 3216 119.4 -64.2 262074.08624964627 3036044.7198421992 +4326 3217 126.0 -66.0 0.0 2846666.914788812 +4326 3217 120.6 -67.8 -227554.6920272114 2636148.545211098 +4326 3217 131.4 -64.2 262074.08624964752 3036044.719842199 +4326 3218 138.0 -66.0 0.0 2846666.914788812 +4326 3218 132.6 -67.8 -227554.6920272114 2636148.545211098 +4326 3218 143.4 -64.2 262074.08624964752 3036044.719842199 +4326 3219 150.0 -66.0 0.0 2846666.914788812 +4326 3219 144.6 -67.8 -227554.69202721244 2636148.545211098 +4326 3219 155.4 -64.2 262074.08624964627 3036044.7198421992 +4326 3220 162.0 -66.0 0.0 2846666.914788812 +4326 3220 156.6 -67.8 -227554.6920272114 2636148.545211098 +4326 3220 167.4 -64.2 262074.08624964752 3036044.719842199 +4326 3221 -102.0 -70.0 0.0 2327502.8280621273 +4326 3221 -107.4 -71.8 -188119.16796137515 2118356.1209359663 +4326 3221 -96.6 -68.2 223641.3788897689 2518361.573675326 +4326 3222 -90.0 -70.0 0.0 2327502.8280621273 +4326 3222 -95.4 -71.8 -188119.16796137515 2118356.1209359663 +4326 3222 -84.6 -68.2 223641.3788897689 2518361.573675326 +4326 3223 -78.0 -70.0 0.0 2327502.8280621273 +4326 3223 -83.4 -71.8 -188119.16796137515 2118356.1209359663 +4326 3223 -72.6 -68.2 223641.37888976838 2518361.573675326 +4326 3224 -66.0 -70.0 0.0 2327502.8280621273 +4326 3224 -71.4 -71.8 -188119.16796137515 2118356.1209359663 +4326 3224 -60.6 -68.2 223641.3788897689 2518361.573675326 +4326 3225 -18.0 -70.0 0.0 2327502.8280621273 +4326 3225 -23.4 -71.8 -188119.16796137483 2118356.1209359667 +4326 3225 -12.600000000000001 -68.2 223641.37888976853 2518361.573675326 +4326 3226 -6.0 -70.0 0.0 2327502.8280621273 +4326 3226 -11.4 -71.8 -188119.1679613749 2118356.1209359667 +4326 3226 -0.6000000000000014 -68.2 223641.37888976856 2518361.573675326 +4326 3227 6.0 -70.0 0.0 2327502.8280621273 +4326 3227 0.6000000000000001 -71.8 -188119.16796137491 2118356.1209359667 +4326 3227 11.399999999999999 -68.2 223641.37888976853 2518361.573675326 +4326 3228 18.0 -70.0 0.0 2327502.8280621273 +4326 3228 12.6 -71.8 -188119.1679613749 2118356.1209359667 +4326 3228 23.4 -68.2 223641.37888976853 2518361.573675326 +4326 3229 30.0 -70.0 0.0 2327502.8280621273 +4326 3229 24.6 -71.8 -188119.1679613747 2118356.1209359667 +4326 3229 35.4 -68.2 223641.37888976865 2518361.573675326 +4326 3230 42.0 -70.0 0.0 2327502.8280621273 +4326 3230 36.6 -71.8 -188119.16796137491 2118356.1209359667 +4326 3230 47.4 -68.2 223641.37888976838 2518361.573675326 +4326 3231 54.0 -70.0 0.0 2327502.8280621273 +4326 3231 48.6 -71.8 -188119.1679613747 2118356.1209359667 +4326 3231 59.4 -68.2 223641.37888976838 2518361.573675326 +4326 3232 66.0 -70.0 0.0 2327502.8280621273 +4326 3232 60.6 -71.8 -188119.16796137515 2118356.1209359663 +4326 3232 71.4 -68.2 223641.3788897689 2518361.573675326 +4326 3233 78.0 -70.0 0.0 2327502.8280621273 +4326 3233 72.6 -71.8 -188119.1679613747 2118356.1209359667 +4326 3233 83.4 -68.2 223641.3788897689 2518361.573675326 +4326 3234 90.0 -70.0 0.0 2327502.8280621273 +4326 3234 84.6 -71.8 -188119.16796137515 2118356.1209359663 +4326 3234 95.4 -68.2 223641.3788897689 2518361.573675326 +4326 3235 102.0 -70.0 0.0 2327502.8280621273 +4326 3235 96.6 -71.8 -188119.16796137515 2118356.1209359663 +4326 3235 107.4 -68.2 223641.3788897689 2518361.573675326 +4326 3236 114.0 -70.0 0.0 2327502.8280621273 +4326 3236 108.6 -71.8 -188119.16796137515 2118356.1209359663 +4326 3236 119.4 -68.2 223641.37888976838 2518361.573675326 +4326 3237 126.0 -70.0 0.0 2327502.8280621273 +4326 3237 120.6 -71.8 -188119.1679613747 2118356.1209359667 +4326 3237 131.4 -68.2 223641.37888976946 2518361.573675326 +4326 3238 138.0 -70.0 0.0 2327502.8280621273 +4326 3238 132.6 -71.8 -188119.1679613747 2118356.1209359667 +4326 3238 143.4 -68.2 223641.37888976946 2518361.573675326 +4326 3239 150.0 -70.0 0.0 2327502.8280621273 +4326 3239 144.6 -71.8 -188119.16796137558 2118356.1209359663 +4326 3239 155.4 -68.2 223641.37888976838 2518361.573675326 +4326 3240 162.0 -70.0 0.0 2327502.8280621273 +4326 3240 156.6 -71.8 -188119.1679613747 2118356.1209359667 +4326 3240 167.4 -68.2 223641.37888976946 2518361.573675326 +4326 3241 174.0 -70.0 0.0 2327502.8280621273 +4326 3241 168.6 -71.8 -188119.1679613747 2118356.1209359667 +4326 3241 179.4 -68.2 223641.37888976946 2518361.573675326 +4326 3242 -153.0 -74.0 0.0 1833921.3768257075 +4326 3242 -161.1 -75.8 -221258.61408278032 1617973.1022123485 +4326 3242 -144.9 -72.2 275690.95472422545 2016014.3871286944 +4326 3243 -135.0 -74.0 0.0 1833921.3768257075 +4326 3243 -143.1 -75.8 -221258.61408278032 1617973.1022123485 +4326 3243 -126.9 -72.2 275690.95472422545 2016014.3871286944 +4326 3244 -117.0 -74.0 0.0 1833921.3768257075 +4326 3244 -125.1 -75.8 -221258.61408278032 1617973.1022123485 +4326 3244 -108.9 -72.2 275690.95472422586 2016014.3871286944 +4326 3245 -99.0 -74.0 0.0 1833921.3768257075 +4326 3245 -107.1 -75.8 -221258.6140827799 1617973.1022123485 +4326 3245 -90.9 -72.2 275690.95472422586 2016014.3871286944 +4326 3246 -81.0 -74.0 0.0 1833921.3768257075 +4326 3246 -89.1 -75.8 -221258.6140827799 1617973.1022123485 +4326 3246 -72.9 -72.2 275690.95472422586 2016014.3871286944 +4326 3247 -63.0 -74.0 0.0 1833921.3768257075 +4326 3247 -71.1 -75.8 -221258.6140827799 1617973.1022123485 +4326 3247 -54.900000000000006 -72.2 275690.95472422586 2016014.3871286944 +4326 3248 -27.0 -74.0 0.0 1833921.3768257075 +4326 3248 -35.1 -75.8 -221258.61408278032 1617973.1022123485 +4326 3248 -18.900000000000002 -72.2 275690.9547242261 2016014.3871286944 +4326 3249 -9.0 -74.0 0.0 1833921.3768257075 +4326 3249 -17.1 -75.8 -221258.61408278017 1617973.1022123485 +4326 3249 -0.9000000000000021 -72.2 275690.9547242261 2016014.3871286944 +4326 3250 9.0 -74.0 0.0 1833921.3768257075 +4326 3250 0.9 -75.8 -221258.61408278014 1617973.1022123485 +4326 3250 17.099999999999998 -72.2 275690.9547242261 2016014.3871286944 +4326 3251 27.0 -74.0 0.0 1833921.3768257075 +4326 3251 18.9 -75.8 -221258.61408278017 1617973.1022123485 +4326 3251 35.099999999999994 -72.2 275690.95472422586 2016014.3871286944 +4326 3252 45.0 -74.0 0.0 1833921.3768257075 +4326 3252 36.9 -75.8 -221258.61408278008 1617973.1022123485 +4326 3252 53.099999999999994 -72.2 275690.95472422586 2016014.3871286944 +4326 3253 63.0 -74.0 0.0 1833921.3768257075 +4326 3253 54.9 -75.8 -221258.61408278008 1617973.1022123485 +4326 3253 71.1 -72.2 275690.95472422586 2016014.3871286944 +4326 3254 81.0 -74.0 0.0 1833921.3768257075 +4326 3254 72.9 -75.8 -221258.6140827799 1617973.1022123485 +4326 3254 89.1 -72.2 275690.95472422586 2016014.3871286944 +4326 3255 99.0 -74.0 0.0 1833921.3768257075 +4326 3255 90.9 -75.8 -221258.6140827799 1617973.1022123485 +4326 3255 107.1 -72.2 275690.95472422586 2016014.3871286944 +4326 3256 117.0 -74.0 0.0 1833921.3768257075 +4326 3256 108.9 -75.8 -221258.6140827799 1617973.1022123485 +4326 3256 125.1 -72.2 275690.9547242264 2016014.3871286944 +4326 3257 135.0 -74.0 0.0 1833921.3768257075 +4326 3257 126.9 -75.8 -221258.61408277956 1617973.1022123485 +4326 3257 143.1 -72.2 275690.9547242264 2016014.3871286944 +4326 3258 153.0 -74.0 0.0 1833921.3768257075 +4326 3258 144.9 -75.8 -221258.61408277956 1617973.1022123485 +4326 3258 161.1 -72.2 275690.9547242264 2016014.3871286944 +4326 3259 171.0 -74.0 0.0 1833921.3768257075 +4326 3259 162.9 -75.8 -221258.61408277956 1617973.1022123485 +4326 3259 179.1 -72.2 275690.9547242264 2016014.3871286944 +4326 3260 -168.0 -78.0 0.0 1359585.7264625672 +4326 3260 -178.8 -79.8 -212435.5119692957 1138991.99036319 +4326 3260 -157.2 -76.2 286119.99810479296 1534057.9505896945 +4326 3261 -144.0 -78.0 0.0 1359585.7264625672 +4326 3261 -154.8 -79.8 -212435.5119692957 1138991.99036319 +4326 3261 -133.2 -76.2 286119.99810479296 1534057.9505896945 +4326 3262 -120.0 -78.0 0.0 1359585.7264625672 +4326 3262 -130.8 -79.8 -212435.5119692957 1138991.99036319 +4326 3262 -109.2 -76.2 286119.9981047924 1534057.9505896948 +4326 3263 -96.0 -78.0 0.0 1359585.7264625672 +4326 3263 -106.8 -79.8 -212435.5119692952 1138991.99036319 +4326 3263 -85.2 -76.2 286119.9981047927 1534057.9505896948 +4326 3264 -72.0 -78.0 0.0 1359585.7264625672 +4326 3264 -82.8 -79.8 -212435.51196929548 1138991.99036319 +4326 3264 -61.2 -76.2 286119.9981047924 1534057.9505896948 +4326 3265 -48.0 -78.0 0.0 1359585.7264625672 +4326 3265 -58.8 -79.8 -212435.5119692952 1138991.99036319 +4326 3265 -37.2 -76.2 286119.9981047927 1534057.9505896948 +4326 3266 -24.0 -78.0 0.0 1359585.7264625672 +4326 3266 -34.8 -79.8 -212435.51196929533 1138991.99036319 +4326 3266 -13.200000000000003 -76.2 286119.9981047926 1534057.9505896948 +4326 3267 0.0 -78.0 0.0 1359585.7264625672 +4326 3267 -10.8 -79.8 -212435.51196929542 1138991.99036319 +4326 3267 10.799999999999997 -76.2 286119.9981047926 1534057.9505896948 +4326 3268 24.0 -78.0 0.0 1359585.7264625672 +4326 3268 13.2 -79.8 -212435.51196929548 1138991.99036319 +4326 3268 34.8 -76.2 286119.99810479255 1534057.9505896948 +4326 3269 48.0 -78.0 0.0 1359585.7264625672 +4326 3269 37.2 -79.8 -212435.51196929548 1138991.99036319 +4326 3269 58.8 -76.2 286119.9981047924 1534057.9505896948 +4326 3270 72.0 -78.0 0.0 1359585.7264625672 +4326 3270 61.2 -79.8 -212435.5119692952 1138991.99036319 +4326 3270 82.8 -76.2 286119.9981047927 1534057.9505896948 +4326 3271 96.0 -78.0 0.0 1359585.7264625672 +4326 3271 85.2 -79.8 -212435.51196929548 1138991.99036319 +4326 3271 106.8 -76.2 286119.9981047924 1534057.9505896948 +4326 3272 120.0 -78.0 0.0 1359585.7264625672 +4326 3272 109.2 -79.8 -212435.5119692952 1138991.99036319 +4326 3272 130.8 -76.2 286119.99810479296 1534057.9505896945 +4326 3273 144.0 -78.0 0.0 1359585.7264625672 +4326 3273 133.2 -79.8 -212435.5119692957 1138991.99036319 +4326 3273 154.8 -76.2 286119.99810479296 1534057.9505896945 +4326 3274 168.0 -78.0 0.0 1359585.7264625672 +4326 3274 157.2 -79.8 -212435.5119692957 1138991.99036319 +4326 3274 178.8 -76.2 286119.99810479296 1534057.9505896945 +4326 3275 -165.0 -82.0 0.0 888469.933942899 +4326 3275 -178.5 -83.8 -160641.87188652667 669121.5520358437 +4326 3275 -151.5 -80.2 254275.24768943497 1059132.6307404875 +4326 3276 -135.0 -82.0 0.0 888469.933942899 +4326 3276 -148.5 -83.8 -160641.87188652667 669121.5520358437 +4326 3276 -121.5 -80.2 254275.24768943497 1059132.6307404875 +4326 3277 -105.0 -82.0 0.0 888469.933942899 +4326 3277 -118.5 -83.8 -160641.87188652667 669121.5520358437 +4326 3277 -91.5 -80.2 254275.2476894352 1059132.6307404875 +4326 3278 -75.0 -82.0 0.0 888469.933942899 +4326 3278 -88.5 -83.8 -160641.87188652667 669121.5520358437 +4326 3278 -61.5 -80.2 254275.24768943497 1059132.6307404875 +4326 3279 -45.0 -82.0 0.0 888469.933942899 +4326 3279 -58.5 -83.8 -160641.87188652667 669121.5520358437 +4326 3279 -31.5 -80.2 254275.24768943497 1059132.6307404875 +4326 3280 -15.0 -82.0 0.0 888469.933942899 +4326 3280 -28.5 -83.8 -160641.8718865267 669121.5520358437 +4326 3280 -1.5 -80.2 254275.2476894349 1059132.6307404875 +4326 3281 15.0 -82.0 0.0 888469.933942899 +4326 3281 1.5 -83.8 -160641.87188652664 669121.5520358437 +4326 3281 28.5 -80.2 254275.247689435 1059132.6307404875 +4326 3282 45.0 -82.0 0.0 888469.933942899 +4326 3282 31.5 -83.8 -160641.87188652667 669121.5520358437 +4326 3282 58.5 -80.2 254275.24768943497 1059132.6307404875 +4326 3283 75.0 -82.0 0.0 888469.933942899 +4326 3283 61.5 -83.8 -160641.87188652667 669121.5520358437 +4326 3283 88.5 -80.2 254275.24768943497 1059132.6307404875 +4326 3284 105.0 -82.0 0.0 888469.933942899 +4326 3284 91.5 -83.8 -160641.87188652682 669121.5520358437 +4326 3284 118.5 -80.2 254275.24768943497 1059132.6307404875 +4326 3285 135.0 -82.0 0.0 888469.933942899 +4326 3285 121.5 -83.8 -160641.87188652667 669121.5520358437 +4326 3285 148.5 -80.2 254275.24768943497 1059132.6307404875 +4326 3286 165.0 -82.0 0.0 888469.933942899 +4326 3286 151.5 -83.8 -160641.87188652667 669121.5520358437 +4326 3286 178.5 -80.2 254275.24768943497 1059132.6307404875 +4326 3287 -150.0 -86.0 0.0 443715.05583747267 +4326 3287 -177.0 -87.8 -110763.20483050271 217385.02942376348 +4326 3287 -123.0 -84.2 292217.1643369565 573508.476616636 +4326 3288 -90.0 -86.0 0.0 443715.05583747267 +4326 3288 -117.0 -87.8 -110763.20483050271 217385.02942376348 +4326 3288 -63.0 -84.2 292217.1643369565 573508.476616636 +4326 3289 -30.0 -86.0 0.0 443715.05583747267 +4326 3289 -57.0 -87.8 -110763.20483050274 217385.02942376348 +4326 3289 -3.0 -84.2 292217.1643369565 573508.476616636 +4326 3290 30.0 -86.0 0.0 443715.05583747267 +4326 3290 3.0 -87.8 -110763.20483050271 217385.02942376348 +4326 3290 57.0 -84.2 292217.1643369566 573508.4766166359 +4326 3291 90.0 -86.0 0.0 443715.05583747267 +4326 3291 63.0 -87.8 -110763.20483050271 217385.02942376348 +4326 3291 117.0 -84.2 292217.1643369565 573508.476616636 +4326 3292 150.0 -86.0 0.0 443715.05583747267 +4326 3292 123.0 -87.8 -110763.20483050271 217385.02942376348 +4326 3292 177.0 -84.2 292217.1643369565 573508.476616636 +4326 3293 0.0 -89.0 0.0 110888.22729414183 +4326 3293 -162.0 -89.9 -3426.552038029497 -10545.842796717692 +4326 3293 162.0 -88.1 65110.198383675466 -200388.5856676527 +4326 3294 161.92000000000002 -74.3 -2421.3889555242395 413187.4112742951 +4326 3294 151.03900000000002 -79.43 -223253.01725314988 -180584.64161451792 +4326 3294 172.801 -69.16999999999999 430583.1903280519 948805.881514961 +4675 3295 138.13 9.54 35746.19479622158 59258.30972954662 +4675 3295 138.00400000000002 9.405000000000001 21905.400652677534 44331.685505928595 +4675 3295 138.256 9.674999999999999 49576.11892524077 74190.081797792 +4687 3296 -154.065 -19.6 388311.7184532385 7832433.469718762 +3296 4326 388311.7184532385 7832433.469718762 -154.0649950133481 -19.60000391992694 +4687 3296 -157.7235 -25.99 26910.83253536583 7116865.197202846 +3296 4326 26910.83253536583 7116865.197202846 -157.72349535987465 -25.990003504204253 +4687 3296 -150.4065 -13.21 781090.880481495 8538186.44581632 +3296 4326 781090.880481495 8538186.44581632 -150.40649468128 -13.21000436688684 +4687 3297 -147.0 -19.244999999999997 500000.0000000027 7872063.47898478 +3297 4326 500000.0000000027 7872063.47898478 -146.99999510094 -19.245004301163622 +4687 3297 -149.7 -30.0045 239554.46372939443 6677645.856268875 +3297 4326 239554.46372939443 6677645.856268875 -149.69999563534415 -30.00450368671899 +4687 3297 -144.3 -8.485500000000002 797281.1498217777 9060994.575075602 +3297 4326 797281.1498217777 9060994.575075602 -144.29999453926536 -8.485504928068085 +4687 3298 -141.0 -17.88 499999.9999999973 8023090.709975702 +3298 4326 499999.9999999973 8023090.709975702 -140.99999515917992 -17.880004658977594 +4687 3298 -143.7 -29.904 239292.0942858476 6688788.238351712 +3298 4326 239292.0942858476 6688788.238351712 -143.69999569560653 -29.90400391470414 +4687 3298 -138.3 -5.856000000000002 798994.051037119 9351994.281928847 +3298 4326 798994.051037119 9351994.281928847 -138.29999458581736 -5.856005409042962 +4687 3299 -134.985 -16.049999999999997 501604.47387673473 8225533.83189811 +3299 4326 501604.47387673473 8225533.83189811 -134.9849952403333 -16.05000503456198 +4687 3299 -137.6985 -25.526999999999997 228804.66253748228 7173942.754932168 +3299 4326 228804.66253748228 7173942.754932168 -137.69849559967466 -25.52700437376559 +4687 3299 -132.2715 -6.573 801743.0854696617 9272631.18946059 +3299 4326 801743.0854696617 9272631.18946059 -132.27149487443893 -6.573005705448832 +4133 3300 24.97 58.635000000000005 556334.159417373 6499867.461578698 +3300 4326 556334.159417373 6499867.461578698 24.96998176020764 58.6350027202132 +4133 3300 22.063 57.6315 384290.516942687 6389362.414732903 +3300 4326 384290.516942687 6389362.414732903 22.062981983136748 57.631502247850364 +4133 3300 27.877 59.6385 718587.438536788 6617563.962210844 +3300 4326 718587.438536788 6617563.962210844 27.876981568488176 59.63850318729984 +4180 3301 24.285 58.760000000000005 516493.16075571533 6513417.968162374 +3301 4326 516493.16075571533 6513417.968162374 24.285 58.76000000000001 +4180 3301 20.7615 57.644000000000005 306659.00308107294 6393752.060520198 +3301 4326 306659.00308107294 6393752.060520198 20.7615 57.644000000000005 +4180 3301 27.8085 59.876 713227.8836589579 6643762.550706423 +3301 4326 713227.8836589579 6643762.550706423 27.8085 59.876 +4689 3302 -138.99 -9.86 720432.2299090311 8909391.72585747 +3302 4326 720432.2299090311 8909391.72585747 -138.98793422260417 -9.859565824581606 +4689 3302 -139.206 -10.058 696617.5696275681 8887624.526913123 +3302 4326 696617.5696275681 8887624.526913123 -139.20394147610548 -10.057576547128026 +4689 3302 -138.774 -9.662 744275.592882414 8931148.856725054 +3302 4326 744275.592882414 8931148.856725054 -138.77192698565085 -9.661555162254144 +4688 3303 -138.64499999999998 -10.48 757788.1500179791 8840539.057916224 +3303 4326 757788.1500179791 8840539.057916224 -138.64659678903428 -10.479064148260859 +4688 3303 -138.7395 -10.588 747352.3915618816 8828664.787212797 +3303 4326 747352.3915618816 8828664.787212797 -138.74110827128018 -10.587093729121541 +4688 3303 -138.5505 -10.372 768230.8512455707 8852411.669891462 +3303 4326 768230.8512455707 8852411.669891462 -138.5520854514122 -10.371034554326073 +4690 3304 -149.39499999999998 -17.685000000000002 245941.6027097677 8043026.695535241 +3304 4326 245941.6027097677 8043026.695535241 -149.39517193488527 -17.68374683665952 +4690 3304 -149.6695 -17.9055 217154.52431444556 8018218.461476605 +3304 4326 217154.52431444556 8018218.461476605 -149.66968753822178 -17.904248483062457 +4690 3304 -149.1205 -17.4645 274798.9289540936 8067783.912016643 +3304 4326 274798.9289540936 8067783.912016643 -149.1206563628738 -17.46324520729912 +4691 3305 -149.865 -17.52 195776.74255918234 8060603.807786423 +3305 4326 195776.74255918234 8060603.807786423 -149.86518870171633 -17.518733654824466 +4691 3305 -149.9865 -17.619 183038.3720635522 8049440.244774051 +3305 4326 183038.3720635522 8049440.244774051 -149.98669623091013 -17.617733585677104 +4691 3305 -149.74349999999998 -17.421 208528.95224155678 8071757.091385268 +3305 4326 208528.95224155678 8071757.091385268 -149.74368117888693 -17.419733727478135 +4692 3306 -152.265 -16.455 578463.3731820591 8180569.5576066645 +3306 4326 578463.3731820591 8180569.5576066645 -152.26476975907917 -16.45494450511528 +4692 3306 -152.3775 -16.5585 566417.8909509258 8169159.98731746 +3306 4326 566417.8909509258 8169159.98731746 -152.37727392275636 -16.558445869637275 +4692 3306 -152.15249999999997 -16.3515 590521.5968326437 8191972.822007502 +3306 4326 590521.5968326437 8191972.822007502 -152.15226559996694 -16.35144314429299 +4693 3307 52.405 27.75 638468.5671453262 3070299.694660869 +3307 4326 638468.5671453262 3070299.694660869 52.40499907177892 27.750005929955023 +4693 3307 51.860499999999995 27.345 585114.4482607549 3024941.7101866417 +3307 4326 585114.4482607549 3024941.7101866417 51.86049906380439 27.345005940012797 +4693 3307 52.9495 28.155 691425.4214696259 3115909.7664513844 +3307 4326 691425.4214696259 3115909.7664513844 52.949499079850284 28.155005919651824 +4283 3308 147.34 -32.84 9331801.963511344 4545376.744955079 +3308 4326 9331801.963511344 4545376.744955079 147.34 -32.84000000000001 +4283 3308 141.625 -37.061 8821546.029924097 4064972.0775362113 +3308 4326 8821546.029924097 4064972.0775362113 141.625 -37.060999999999986 +4283 3308 153.055 -28.619 9893154.747533873 4996304.810499958 +3308 4326 9893154.747533873 4996304.810499958 153.05500000000004 -28.618999999999996 +4267 3309 -119.285 37.269999999999996 63317.02356629366 -82911.64144928427 +3309 4326 63317.02356629366 -82911.64144928427 -119.28595554817244 37.269925889548524 +4267 3309 -123.93350000000001 33.004 -367878.95066671236 -549115.1093782233 +3309 4326 -367878.95066671236 -549115.1093782233 -123.93452083799218 33.00399210403721 +4267 3309 -114.63650000000001 41.536 447920.2431714803 403459.0207883576 +3309 4326 447920.2431714803 403459.0207883576 -114.63738139891444 41.53589678732039 +4269 3310 -119.285 37.269999999999996 63315.465344822995 -82731.13159650704 +3310 4326 63315.465344822995 -82731.13159650704 -119.28500000000001 37.27 +4269 3310 -123.93350000000001 33.004 -367870.9056195842 -548947.5140136816 +3310 4326 -367870.9056195842 -548947.5140136816 -123.93350000000002 33.004 +4269 3310 -114.63650000000001 41.536 447908.0593044227 403648.150371247 +3310 4326 447908.0593044227 403648.150371247 -114.63650000000003 41.536 +4152 3311 -119.285 37.269999999999996 63315.465344822995 -82731.13159650704 +3311 4326 63315.465344822995 -82731.13159650704 -119.2850126083194 37.2700051623423 +4152 3311 -123.93350000000001 33.004 -367870.9056195842 -548947.5140136816 +3311 4326 -367870.9056195842 -548947.5140136816 -123.93351293772591 33.00400447266086 +4152 3311 -114.63650000000001 41.536 447908.0593044227 403648.150371247 +3311 4326 447908.0593044227 403648.150371247 -114.63651231090904 41.536005940812785 +4623 3312 -54.225 5.265000000000001 807625.0964743092 582647.5061454223 +3312 4326 807625.0964743092 582647.5061454223 -54.2251483640107 5.266085105889071 +4623 3312 -54.4275 4.8825 785329.4214311894 540227.4332051742 +3312 4326 785329.4214311894 540227.4332051742 -54.427657686482654 4.883578724669185 +4623 3312 -54.0225 5.6475 829883.3517736198 625089.6525360097 +3312 4326 829883.3517736198 625089.6525360097 -54.02263903485635 5.648591461419538 +4624 3313 -54.305 3.9000000000000004 799306.3080985161 431552.89905931166 +3313 4326 799306.3080985161 431552.89905931166 -54.305 3.9 +4624 3313 -54.579499999999996 2.2889999999999997 769207.9431286091 253231.8457373831 +3313 4326 769207.9431286091 253231.8457373831 -54.579499999999996 2.2889999999999997 +4624 3313 -54.0305 5.511 829059.4342739235 609969.3913214618 +3313 4326 829059.4342739235 609969.3913214618 -54.0305 5.511 +4696 3316 22.255000000000003 -6.31 528213.4243750381 9302374.861482648 +4696 3316 21.5755 -7.21 453119.46493801044 9202848.160606727 +4696 3316 22.9345 -5.41 603563.8105345309 9401810.44830539 +4696 3317 24.0 -6.0 500000.0 9336657.21839213 +4696 3317 23.1 -6.891 400535.52017688187 9238048.058997506 +4696 3317 24.9 -5.109 599788.0391318959 9435099.596904851 +4697 3318 12.59 -5.355 565389.5415019116 9407939.621901296 +4697 3318 12.212 -5.9715 523470.65677927586 9339803.811660232 +4697 3318 12.968 -4.7385 607387.6793596965 9476057.423248868 +4697 3319 14.004999999999999 -5.095000000000001 500554.36741768004 9436717.258291334 +4697 3319 13.1005 -5.8285 400386.4485839067 9355539.704448275 +4697 3319 14.9095 -4.3615 600949.7640001159 9517752.594941415 +4697 3320 16.0 -4.58 500000.0 9493656.199310785 +4697 3320 15.1 -5.741 400315.75254352996 9365215.18533831 +4697 3320 16.9 -3.419 600006.2971811411 9621967.821527999 +4697 3321 18.0 -4.39 500000.0 9514662.583228674 +4697 3321 17.1 -5.281 400239.0136734909 9416080.563456243 +4697 3321 18.9 -3.4989999999999997 599997.9131930318 9613122.161005005 +4697 3322 20.0 -5.65 500000.0 9375354.842548719 +4697 3322 19.1 -7.126 400585.33191451075 9212061.020523632 +4697 3322 20.9 -4.1739999999999995 599919.4637928791 9538486.270175802 +4697 3323 22.0 -6.31 500000.0 9302381.761878246 +4697 3323 21.1 -7.21 400603.540211363 9202771.973604523 +4697 3323 22.9 -5.41 599740.1105085525 9401816.220166804 +4697 3324 24.0 -6.0 500000.0 9336657.21839213 +4697 3324 23.1 -6.891 400535.52017688187 9238048.058997506 +4697 3324 24.9 -5.109 599788.0391318959 9435099.596904851 +4697 3325 26.0 -5.745 500000.0 9364851.249659834 +4697 3325 25.1 -7.108499999999999 400581.56524597784 9213996.234416788 +4697 3325 26.9 -4.381500000000001 599892.578670348 9515542.398517823 +4697 3326 28.0 -5.800000000000001 500000.0 9358770.206052154 +4697 3326 27.1 -7.204000000000001 400602.2325773525 9203435.478071284 +4697 3326 28.9 -4.396000000000001 599890.6512891002 9513939.087180477 +4697 3327 29.32 -5.1899999999999995 424615.4519685858 9426173.395485977 +4697 3327 29.032 -5.955 392824.1111310784 9341538.719576547 +4697 3327 29.608 -4.425 456495.201461004 9510781.51203296 +4179 3328 19.145 51.945 498510.5491769809 475342.32794471586 +3328 4326 498510.5491769809 475342.32794471586 19.1431867628623 51.94469924423894 +4179 3328 14.640500000000001 49.2945 170788.4409401355 190585.8952231023 +3328 4326 170788.4409401355 190585.8952231023 14.638784140700704 49.294098127731914 +4179 3328 23.649499999999996 54.5955 789667.6535837437 779382.6162066369 +3328 4326 789667.6535837437 779382.6162066369 23.647585497998403 54.59530612469054 +4179 3329 15.0 50.629999999999995 5500000.0 5611023.792190646 +3329 4326 5500000.0 5611023.792190646 14.998232165746973 50.629616831128416 +4179 3329 13.65 46.949 5397225.661667873 5202553.803352359 +3329 4326 5397225.661667873 5202553.803352359 13.648366366622433 46.94856074330477 +4179 3329 16.35 54.311 5587865.001007058 6021479.161634348 +3329 4326 5587865.001007058 6021479.161634348 16.348066760984306 54.31067665213039 +4179 3330 18.0 47.515 6500000.0 5264595.283497713 +3330 4326 6500000.0 5264595.283497713 17.998354192244392 47.51463386895717 +4179 3330 16.65 40.8775 6386204.032731752 4527926.8077761615 +3330 4326 6386204.032731752 4527926.8077761615 16.64854040575838 40.877049221992486 +4179 3330 19.35 54.1525 6588202.3272282975 6003837.883944861 +3330 4326 6588202.3272282975 6003837.883944861 19.348084112461528 54.15222692329593 +4179 3331 21.0 47.075 7500000.0 5215676.744975802 +3331 4326 7500000.0 5215676.744975802 20.99837990860627 47.074675682862456 +4179 3331 19.65 40.3835 7385362.153813135 4473066.407458271 +3331 4326 7385362.153813135 4473066.407458271 19.648559820268307 40.383085194680156 +4179 3331 22.35 53.76649999999999 7589020.970566208 5960877.37762987 +3331 4326 7589020.970566208 5960877.37762987 22.34811832136995 53.76627401136893 +4179 3332 24.0 47.825 8500000.0 5299062.882105386 +3332 4326 8500000.0 5299062.882105386 23.99837714656721 47.82473423809585 +4179 3332 22.65 41.8985 8387970.889679719 4641326.495791376 +3332 4326 8387970.889679719 4641326.495791376 22.64853157681852 41.89811712163136 +4179 3332 25.35 53.75149999999999 8589052.70098631 5959207.9809044255 +3332 4326 8589052.70098631 5959207.9809044255 25.34816125381659 53.75135577696288 +4179 3333 15.0 50.335 3500000.0 5578207.920000927 +3333 4326 3500000.0 5578207.920000927 14.998244078785875 50.33461396693544 +4179 3333 12.3 46.2355 3291737.9470710987 5125897.987267141 +3333 4326 3291737.9470710987 5125897.987267141 12.298389569764929 46.2350336960067 +4179 3333 17.7 54.4345 3675187.672529111 6037744.399011723 +3333 4326 3675187.672529111 6037744.399011723 17.698063953723977 54.43420140622084 +4179 3334 21.0 47.265 4500000.0 5236800.196437218 +3334 4326 4500000.0 5236800.196437218 20.99837360885973 47.26467778418062 +4179 3334 18.3 40.4025 4270778.463486699 4477803.162091579 +3334 4326 4270778.463486699 4477803.162091579 18.29855528805015 40.40206716492694 +4179 3334 23.7 54.1275 4676495.611811341 6003583.649441298 +3334 4326 4676495.611811341 6003583.649441298 23.698109348144225 54.12730134154544 +4179 3335 26.869999999999997 46.085 5489945.0349793 5105632.058506797 +3335 4326 5489945.0349793 5105632.058506797 26.868465489350616 46.08474658404921 +4179 3335 24.287 41.7245 5274245.4027223885 4624677.23705965 +3335 4326 5274245.4027223885 4624677.23705965 24.28555219534074 41.72413633062761 +4179 3335 29.453 50.445499999999996 5674232.066902611 5593376.234667524 +3335 4326 5674232.066902611 5593376.234667524 29.451355883109272 50.4453620896271 +4698 3336 69.655 -49.19 547727.9739022221 4551107.55817069 +3336 4326 547727.9739022221 4551107.55817069 69.65224300915459 -49.18943168086619 +4698 3336 68.7865 -49.721000000000004 484609.9879575797 4492256.649878464 +3336 4326 484609.9879575797 4492256.649878464 68.7836871260759 -49.720426337198305 +4698 3336 70.5235 -48.659 612195.0030060179 4609224.057907651 +3336 4326 612195.0030060179 4609224.057907651 70.52079809924787 -48.65843675694071 +4699 3337 57.55 -20.255000000000003 1002943.4371074704 993365.4927723234 +3337 4326 1002943.4371074704 993365.4927723234 57.557033710879814 -20.258049938280728 +4699 3337 57.28 -20.5385 974779.5422982101 961963.3339046477 +3337 4326 974779.5422982101 961963.3339046477 57.28703388073816 -20.541541110677628 +4699 3337 57.82 -19.971500000000002 1031209.4329624703 1024720.5510753744 +3337 4326 1031209.4329624703 1024720.5510753744 57.82703355700717 -19.974559110364407 +4269 3338 -158.78500000000003 61.35 -254894.82358153717 1272467.9326409101 +3338 4326 -254894.82358153717 1272467.9326409101 -158.78500000000003 61.35 +4269 3338 175.2995 52.305 -2028925.2498783472 732268.5502162899 +3338 4326 -2028925.2498783472 732268.5502162899 175.29950000000005 52.304999999999986 +4269 3338 -132.86950000000002 70.39500000000001 793326.4022635117 2395398.489500744 +3338 4326 793326.4022635117 2395398.489500744 -132.86950000000002 70.395 +4701 3339 12.59 -5.355 565389.5415019116 9407939.621901296 +3339 4326 565389.5415019116 9407939.621901296 12.588765891078031 -5.356030010946597 +4701 3339 12.212 -5.9715 523470.65677927586 9339803.811660232 +3339 4326 523470.65677927586 9339803.811660232 12.210757910936142 -5.97247190937314 +4701 3339 12.968 -4.7385 607387.6793596965 9476057.423248868 +3339 4326 607387.6793596965 9476057.423248868 12.966773767103245 -4.739588014993363 +4701 3340 14.004999999999999 -5.095000000000001 500554.36741768004 9436717.258291334 +3340 4326 500554.36741768004 9436717.258291334 14.003791811137804 -5.096056867199237 +4701 3340 13.1005 -5.8285 400386.4485839067 9355539.704448275 +3340 4326 400386.4485839067 9355539.704448275 13.099273991845404 -5.829487146654664 +4701 3340 14.9095 -4.3615 600949.7640001159 9517752.594941415 +3340 4326 600949.7640001159 9517752.594941415 14.90830969436018 -4.36262617389964 +4701 3341 15.635000000000002 -5.145 459534.0502035087 9431177.601575684 +3341 4326 459534.0502035087 9431177.601575684 15.633821904595058 -5.146055297210629 +4701 3341 15.0545 -5.7975 395286.0843511411 9358959.336342422 +3341 4326 395286.0843511411 9358959.336342422 15.053309754339823 -5.798494187645239 +4701 3341 16.215500000000002 -4.4925 523914.2293649916 9503326.683003496 +3341 4326 523914.2293649916 9503326.683003496 16.21433399930631 -4.493616204408356 +4701 3342 14.225000000000001 -5.16 414104.92150150903 9429649.63764926 +3342 4326 414104.92150150903 9429649.63764926 14.223795706867747 -5.161051275232086 +4701 3342 12.3755 -5.952 209417.35740634537 9341471.298581675 +3342 4326 209417.35740634537 9341471.298581675 12.374260833740452 -5.95297408905263 +4701 3342 16.0745 -4.368 619229.0686550504 9517154.624568485 +3342 4326 619229.0686550504 9517154.624568485 16.07333151867373 -4.3691274082974125 +4702 3343 -15.0 19.09 499999.9999999983 2110785.8015988586 +3343 4326 499999.9999999983 2110785.8015988586 -14.999999999999998 19.089999999999996 +4702 3343 -17.7 15.157 209849.36941423704 1677479.479389508 +3343 4326 209849.36941423704 1677479.479389508 -17.7 15.156999999999996 +4702 3343 -12.3 23.023 776724.7977777289 2548617.1929762624 +3343 4326 776724.7977777289 2548617.1929762624 -12.3 23.022999999999996 +4702 3344 -9.0 21.025 500000.0 2324914.4608593876 +3344 4326 500000.0 2324914.4608593876 -9.0 21.025000000000006 +4702 3344 -11.7 15.3775 210152.52419218846 1701891.8325240973 +3344 4326 210152.52419218846 1701891.8325240973 -11.7 15.377500000000001 +4702 3344 -6.300000000000001 26.6725 768712.455073565 2953005.9261992495 +3344 4326 768712.455073565 2953005.9261992495 -6.300000000000001 26.6725 +4702 3345 -5.4 20.615 249881.4631915816 2281384.7391369063 +3345 4326 249881.4631915816 2281384.7391369063 -5.400000000000001 20.614999999999995 +4702 3345 -5.94 16.0025 185330.7868069358 1771438.9544797826 +3345 4326 185330.7868069358 1771438.9544797826 -5.94 16.0025 +4702 3345 -4.859999999999999 25.2275 312633.9192196112 2791435.4079277376 +3345 4326 312633.9192196112 2791435.4079277376 -4.859999999999999 25.2275 +4669 3346 22.92 55.17 431194.03974992706 6115464.679789118 +3346 4326 431194.03974992706 6115464.679789118 22.92 55.16999999999999 +4669 3346 19.41 54.018 199296.90939968452 5996477.419814255 +3346 4326 199296.90939968452 5996477.419814255 19.41 54.018 +4669 3346 26.43 56.322 650304.0306769495 6245820.157113666 +3346 4326 650304.0306769495 6245820.157113666 26.43 56.321999999999996 +4269 3347 -90.86999999999999 62.334999999999994 6250097.062608002 2886287.1012505433 +3347 4326 6250097.062608002 2886287.1012505433 -90.86999999999999 62.334999999999994 +4269 3347 -135.99599999999998 40.6225 2617185.940828924 1775491.2279609214 +3347 4326 2617185.940828924 1775491.2279609214 -135.99599999999998 40.62250000000001 +4269 3347 -45.744 84.04749999999999 6722808.739871014 5493208.0653623305 +3347 4326 6722808.739871014 5493208.0653623305 -45.74399999999997 84.04749999999999 +4617 3348 -90.86999999999999 62.334999999999994 6250097.062608002 2886287.1012505433 +3348 4326 6250097.062608002 2886287.1012505433 -90.87001115625111 62.33501007550968 +4617 3348 -135.99599999999998 40.6225 2617185.940828924 1775491.2279609214 +3348 4326 2617185.940828924 1775491.2279609214 -135.99601513338072 40.62250404191895 +4617 3348 -45.744 84.04749999999999 6722808.739871014 5493208.0653623305 +3348 4326 6722808.739871014 5493208.0653623305 -45.743972467788836 84.04751184449566 +4284 3350 21.509999999999998 56.72 223059.39994748117 6277872.795399608 +3350 4326 223059.39994748117 6277872.795399608 21.507858324658383 56.719758774994354 +4284 3350 19.764 54.425000000000004 108124.19792378921 6024472.286497644 +3350 4326 108124.19792378921 6024472.286497644 19.76198416779728 54.42469790337248 +4284 3350 23.256 59.015 325028.2847624989 6534129.940345149 +3350 4326 325028.2847624989 6534129.940345149 23.25371426918204 59.01482130469463 +4284 3351 24.95 56.805 1250000.0 6287251.702196075 +3351 4326 1250000.0 6287251.702196075 24.947902506772284 56.80482825875133 +4284 3351 23.599999999999998 54.1815 1161859.3408688176 5996007.96703415 +3351 4326 1161859.3408688176 5996007.96703415 23.598043102203388 54.181268385047225 +4284 3351 26.299999999999997 59.4285 1326623.2790143015 6580238.331435351 +3351 4326 1326623.2790143015 6580238.331435351 26.29773681011247 59.428389462711394 +4284 3352 27.345 57.379999999999995 2213608.068268288 6351447.492658605 +3352 4326 2213608.068268288 6351447.492658605 27.34288874639723 57.37988104576959 +4284 3352 26.5395 55.373 2160577.27531198 6128711.030470317 +3352 4326 2160577.27531198 6128711.030470317 26.537501912685954 55.37283833162839 +4284 3352 28.150499999999997 59.387 2261394.3992178827 6574854.9497642 +3352 4326 2261394.3992178827 6574854.9497642 28.148259920185808 59.38692428886539 +4704 3353 11.815 -5.21 812090.1632562343 9423419.613904214 +4704 3353 10.6585 -5.957 683592.0971275886 9341265.660414724 +4704 3353 12.971499999999999 -4.463 940966.1424481636 9505498.297689773 +4705 3354 11.45 -5.545 771448.8824818352 9386523.919549571 +4705 3354 10.622 -5.9905 679539.638682245 9337573.00805905 +4705 3354 12.277999999999999 -5.0995 863536.0605730759 9435405.804451713 +4706 3355 33.30500000000001 28.6 840491.2826430164 656993.2813527648 +3355 4326 840491.2826430164 656993.2813527648 33.3067831722493 28.600314357170845 +4706 3355 32.4365 27.331000000000003 757169.885803598 515008.99865376926 +3355 4326 757169.885803598 515008.99865376926 32.43825298778417 27.331314425986655 +4706 3355 34.173500000000004 29.869 921683.1546992694 799710.876142954 +3355 4326 921683.1546992694 799710.876142954 34.175314561094694 29.86931311359911 +4152 3358 -79.85499999999999 35.21 531760.6294742532 162292.8993226596 +3358 4326 531760.6294742532 162292.8993226596 -79.8550028200936 35.210006899141526 +4152 3358 -83.8825 33.967999999999996 158490.68215049576 35280.47102519372 +3358 4326 158490.68215049576 35280.47102519372 -83.88250395103799 33.96800649061343 +4152 3358 -75.8275 36.452000000000005 893982.110337277 304301.6117079829 +3358 4326 893982.110337277 304301.6117079829 -75.8275016502862 36.45200729964803 +4152 3360 -80.94 33.629999999999995 615165.8566942128 199244.85107013452 +3360 4326 615165.8566942128 199244.85107013452 -80.94000306819026 33.63000651673196 +4152 3360 -83.118 32.208 409907.55380288104 43600.556483864246 +3360 4326 409907.55380288104 43600.556483864246 -83.11800364468398 32.20800612923544 +4152 3360 -78.762 35.052 813775.0055652673 359176.8093884141 +3360 4326 813775.0055652673 359176.8093884141 -78.76200248205454 35.05200690115356 +4152 3361 -80.94 33.629999999999995 2018260.684692299 653690.4562668455 +3361 4326 2018260.684692299 653690.4562668455 -80.94000306819026 33.63000651673196 +4152 3361 -83.118 32.208 1344841.0557837302 143046.4451570349 +3361 4326 1344841.0557837302 143046.4451570349 -83.11800364468398 32.20800612923544 +4152 3361 -78.762 35.052 2669865.503823055 1178401.605605033 +3361 4326 2669865.503823055 1178401.605605033 -78.76200248205454 35.05200690115356 +4152 3362 -77.61500000000001 41.565 611260.251203743 155299.796259382 +3362 4326 611260.251203743 155299.796259382 -77.61500250856552 41.56500827367169 +4152 3362 -80.2385 40.6965 389693.64377760055 61862.05383360608 +3362 4326 389693.64377760055 61862.05383360608 -80.23850327138477 40.69650800174656 +4152 3362 -74.9915 42.4335 826973.6695968212 255374.7277154768 +3362 4326 826973.6695968212 255374.7277154768 -74.99150172500075 42.43350853700031 +4152 3363 -77.61500000000001 41.565 2005443.0074909471 509512.7482276559 +3363 4326 2005443.0074909471 509512.7482276559 -77.61500250856552 41.56500827367169 +4152 3363 -80.2385 40.6965 1278519.8962936779 202959.08828575598 +3363 4326 1278519.8962936779 202959.08828575598 -80.23850327138477 40.69650800174656 +4152 3363 -74.9915 42.4335 2713162.781002238 837841.9191798603 +3363 4326 2713162.781002238 837841.9191798603 -74.99150172500075 42.43350853700031 +4152 3364 -77.625 40.445 610604.1860345764 123441.44619973708 +3364 4326 610604.1860345764 123441.44619973708 -77.62500243611011 40.44500805795568 +4152 3364 -80.2395 39.783500000000004 386764.6591716248 52990.14482360457 +3364 4326 386764.6591716248 52990.14482360457 -80.23950320732905 39.7835078258107 +4152 3364 -75.0105 41.1065 830085.7794705472 200460.87468017207 +3364 4326 830085.7794705472 200460.87468017207 -75.01050164660057 41.10650828254451 +4152 3365 -77.625 40.445 2003290.5670151063 404990.8114069708 +3365 4326 2003290.5670151063 404990.8114069708 -77.62500243611011 40.44500805795568 +4152 3365 -80.2395 39.783500000000004 1268910.3859655724 173851.8334754427 +3365 4326 1268910.3859655724 173851.8334754427 -80.23950320732905 39.7835078258107 +4152 3365 -75.0105 41.1065 2723373.094812954 657678.7196798646 +3365 4326 2723373.094812954 657678.7196798646 -75.01050164660057 41.10650828254451 +4700 3367 -14.54 19.09 548390.4065205671 2110667.050113763 +4700 3367 -16.825999999999997 15.157 303802.27865391446 1676360.319556011 +4700 3367 -12.254000000000001 23.023 781449.1791367165 2548491.778994887 +4700 3368 -9.0 21.025 500000.0 2324716.5452737296 +4700 3368 -11.7 15.3775 210146.30492497527 1701741.5671810126 +4700 3368 -6.300000000000001 26.6725 768720.145395387 2952767.1965544443 +4700 3369 -5.4 20.615 249875.36590474492 2281190.117043464 +4700 3369 -5.94 16.0025 185323.93540554494 1771283.1469952841 +4700 3369 -4.859999999999999 25.2275 312628.7628293057 2791206.377391087 +4267 3370 170.82999999999998 52.644999999999996 488497.48946100293 5832576.790446573 +3370 4326 488497.48946100293 5832576.790446573 170.8270200445895 52.643290217083106 +4267 3370 167.96800000000002 49.3735 279903.26177902124 5473180.191110341 +3370 4326 279903.26177902124 5473180.191110341 167.9652470620094 49.371722462765135 +4267 3370 173.69199999999998 55.9165 668239.1020905122 6199846.610178231 +3370 4326 668239.1020905122 6199846.610178231 173.6887537966913 55.914890465774775 +4267 3371 177.0 52.295 500000.00000000175 5793631.860456524 +3371 4326 500000.00000000175 5793631.860456524 176.99701259511795 52.29344445058719 +4267 3371 174.3 48.3575 299989.8299425249 5359340.161240539 +3371 4326 299989.8299425249 5359340.161240539 174.29725830144488 48.35587890760175 +4267 3371 179.7 56.2325 667363.249381097 6235022.219871975 +3371 4326 667363.249381097 6235022.219871975 179.69671061156575 56.231053950587345 +4269 3372 170.82999999999998 52.644999999999996 488497.8856865507 5832794.455588982 +3372 4326 488497.8856865507 5832794.455588982 170.8300047088856 52.64500770527696 +4269 3372 167.96800000000002 49.3735 279910.38483448746 5473399.051220152 +3372 4326 279910.38483448746 5473399.051220152 167.96800574081428 49.37351006863911 +4269 3372 173.69199999999998 55.9165 668232.9652947485 6200060.567462479 +3372 4326 668232.9652947485 6200060.567462479 173.692003514406 55.91650534477953 +4269 3373 177.0 52.295 500000.00000000175 5793849.77786767 +3373 4326 500000.00000000175 5793849.77786767 177.00000153420078 52.29500778365649 +4269 3373 174.3 48.3575 299996.1712407819 5359559.00128083 +3373 4326 299996.1712407819 5359559.00128083 174.300002680393 48.35751052721077 +4269 3373 179.7 56.2325 667357.1124991814 6235235.713225893 +3373 4326 667357.1124991814 6235235.713225893 179.70000016885382 56.232505034954634 +4741 3374 -6.91 61.87 609926.6807960146 6861631.103938562 +3374 4326 609926.6807960146 6861631.103938562 -6.915594439268875 61.870803811816124 +4741 3374 -7.432 61.384 583779.4621402769 6806728.872156911 +3374 4326 583779.4621402769 6806728.872156911 -7.437542334587762 61.384765282560856 +4741 3374 -6.388 62.355999999999995 635190.6285045962 6916737.33756838 +3374 4326 635190.6285045962 6916737.33756838 -6.39364816465706 62.35684248380445 +4742 3375 101.91999999999999 4.47 436306.0884087617 494492.74517072795 +3375 4326 436306.0884087617 494492.74517072795 101.91999999999999 4.469999999999998 +4742 3375 98.41 1.464 44445.37311104708 163374.0109137873 +3375 4326 44445.37311104708 163374.0109137873 98.41 1.4639999999999944 +4742 3375 105.42999999999999 7.475999999999999 824996.5200279515 827514.2433709753 +3375 4326 824996.5200279515 827514.2433709753 105.42999999999999 7.475999999999988 +4742 3376 114.46000000000001 4.26 530488.9882947727 471462.7346361128 +3376 4326 530488.9882947727 471462.7346361128 114.46000000000001 4.259999999999996 +4742 3376 109.825 1.191 15494.012640313713 131696.84548904267 +3376 4326 15494.012640313713 131696.84548904267 109.825 1.1910000000000005 +4742 3376 119.095 7.329 1041675.9098561626 813848.2178567764 +3376 4326 1041675.9098561626 813848.2178567764 119.095 7.328999999999998 +4742 3377 103.52 2.08 -4568.778015175592 4149.848894692858 +3377 4326 -4568.778015175592 4149.848894692858 103.52 2.0800000000000005 +4742 3377 102.548 1.297 -112739.68546725121 -82413.85315341243 +3377 4326 -112739.68546725121 -82413.85315341243 102.548 1.2969999999986452 +4742 3377 104.49199999999999 2.863 103493.59714815686 90785.71608654285 +3377 4326 103493.59714815686 90785.71608654285 104.49199999999993 2.862999999992237 +4742 3378 102.205 2.655 29260.52717390083 -7262.203986713459 +3378 4326 29260.52717390083 -7262.203986713459 102.205 2.655000000000002 +4742 3378 101.7505 2.0925 -21290.323732251378 -69461.90542871218 +3378 4326 -21290.323732251378 -69461.90542871218 101.7505 2.092500000000001 +4742 3378 102.6595 3.2175 79763.21344980552 54960.605337162095 +3378 4326 79763.21344980552 54960.605337162095 102.65949999999998 3.2174999999997342 +4742 3379 102.5 3.615 7263.685344680928 -10585.143806558506 +3379 4326 7263.685344680928 -10585.143806558506 102.5 3.615000000000001 +4742 3379 101.447 2.5665 -109824.56629143991 -126489.8592520335 +3379 4326 -109824.56629143991 -126489.8592520335 101.44700000000003 2.566499999997119 +4742 3379 103.553 4.6635 124078.32719812443 105467.52971423313 +3379 4326 124078.32719812443 105467.52971423313 103.55299999999973 4.663499999983943 +4742 3380 101.36500000000001 3.205 -37515.67215826359 3425.3081720405753 +3380 4326 -37515.67215826359 3425.3081720405753 101.36500000000001 3.2050000000000005 +4742 3380 100.82050000000001 2.6065 -98068.25385955005 -62740.87195198145 +3380 4326 -98068.25385955005 -62740.87195198145 100.82050000000001 2.6064999999999223 +4742 3380 101.9095 3.8035 22966.873814585604 69623.88297794873 +3380 4326 22966.873814585604 69623.88297794873 101.9095 3.8035000000001085 +4742 3381 103.05 4.8950000000000005 17345.350054870152 -5616.791316690627 +3381 4326 17345.350054870152 -5616.791316690627 103.05 4.8950000000000005 +4742 3381 102.447 3.9905 -49621.384295334865 -105611.10465718665 +3381 4326 -49621.384295334865 -105611.10465718665 102.44700000000002 3.9905000000001314 +4742 3381 103.65299999999999 5.7995 84132.99615997539 94439.51508401585 +3381 4326 84132.99615997539 94439.51508401585 103.65299999999998 5.799500000001061 +4742 3382 100.34 5.355 -508.54300377559804 -7293.495598518345 +3382 4326 -508.54300377559804 -7293.495598518345 100.34 5.355 +4742 3382 100.14200000000001 5.1435 -22461.80206107009 -30678.370013820895 +3382 4326 -22461.80206107009 -30678.370013820895 100.14200000000001 5.143500000000015 +4742 3382 100.538 5.5665 21429.636460419188 16098.605334066113 +3382 4326 21429.636460419188 16098.605334066113 100.538 5.566500000000016 +4742 3383 100.355 5.9 -31157.270629031478 -7144.041483735553 +3383 4326 -31157.270629031478 -7144.041483735553 100.355 5.900000000000085 +4742 3383 99.6665 5.162 -107530.55321406804 -88681.45121350487 +3383 4326 -107530.55321406804 -88681.45121350487 99.66650000000011 5.161999999998599 +4742 3383 101.04350000000001 6.638 45019.569025524564 74480.18254767823 +3383 4326 45019.569025524564 74480.18254767823 101.04350000000001 6.638000000000494 +4742 3384 101.035 4.79 24358.006918962714 125821.54086360031 +3384 4326 24358.006918962714 125821.54086360031 101.035 4.790000000000018 +4742 3384 100.1665 3.773 -72082.63706643075 13384.14213780727 +3384 4326 -72082.63706643075 13384.14213780727 100.16650000000001 3.773000000000013 +4742 3384 101.9035 5.807 120506.27224945562 238397.13263121547 +3384 4326 120506.27224945562 238397.13263121547 101.90349999999972 5.806999999997086 +4742 3385 102.0 5.415 -19492.60336433349 -52908.23524820403 +3385 4326 -19492.60336433349 -52908.23524820403 102.0 5.415000000000077 +4742 3385 101.39699999999999 4.6275 -86440.15290195952 -139937.1090099564 +3385 4326 -86440.15290195952 -139937.1090099564 101.39700000000006 4.627499999998877 +4742 3385 102.60300000000001 6.2025 47288.13405124613 34179.90540723996 +3385 4326 47288.13405124613 34179.90540723996 102.60300000000001 6.2025000000001365 +4123 3386 19.369999999999997 60.21 575961.1371838559 6678414.554711669 +3386 4326 575961.1371838559 6678414.554711669 19.366551721077855 60.21001799390611 +4123 3386 19.253 60.092999999999996 569721.7704586906 6665251.147645472 +3386 4326 569721.7704586906 6665251.147645472 19.24955965671587 60.093009942693335 +4123 3386 19.487 60.327000000000005 582153.8112302409 6691589.035455811 +3386 4326 582153.8112302409 6691589.035455811 19.4835437408469 60.32702605153248 +4123 3387 31.545 62.915 5426057.659931899 6979916.151138205 +3387 4326 5426057.659931899 6979916.151138205 31.541919360104234 62.91544850767909 +4123 3387 31.5045 62.838499999999996 5423801.643727303 6971437.98681261 +3387 4326 5423801.643727303 6971437.98681261 31.501425379145857 62.8389443082967 +4123 3387 31.5855 62.9915 5428303.097678106 6988395.877727407 +3387 4326 5428303.097678106 6988395.877727407 31.582413311339078 62.991952710210604 +4284 3388 50.44 42.16 -46397.202126942095 3837692.453719598 +3388 4326 -46397.202126942095 3837692.453719598 50.43881719603954 42.15999903936092 +4284 3388 47.299 37.831 -306635.7947710925 3371092.2781291045 +3388 4326 -306635.7947710925 3371092.2781291045 47.29785443402185 37.830877843754784 +4284 3388 53.581 46.489 213841.3905172088 4337672.810257215 +3388 4326 213841.3905172088 4337672.810257215 53.5797702804466 46.48912381416951 +4284 3389 180.0 66.94500000000001 60500000.0 7428349.400943698 +3389 4326 60500000.0 7428349.400943698 -179.99655651928995 66.94502853230013 +4284 3389 178.65 62.7105 60430917.70371809 6956961.751694089 +3389 4326 60430917.70371809 6956961.751694089 178.65295774063128 62.71050273615956 +4284 3389 -178.65 71.1795 60548624.694245815 7901256.126737621 +3389 4326 60548624.694245815 7901256.126737621 -178.6458464852023 71.1795525029407 +4200 3390 180.0 66.94500000000001 60500000.0 7428349.400943698 +3390 4326 60500000.0 7428349.400943698 -179.99696997435834 66.94493301360656 +4200 3390 178.65 62.7105 60430917.70371809 6956961.751694089 +3390 4326 60430917.70371809 6956961.751694089 178.65258186398742 62.710403968416465 +4200 3390 -178.65 71.1795 60548624.694245815 7901256.126737621 +3390 4326 60548624.694245815 7901256.126737621 -178.64631513311852 71.17946038611474 +4743 3391 40.394999999999996 33.945 628917.4928982623 3756654.7242055763 +3391 4326 628917.4928982623 3756654.7242055763 40.391734905523634 33.94487366193487 +4743 3391 38.9505 31.4205 495295.14029697503 3475939.4764342452 +3391 4326 495295.14029697503 3475939.4764342452 38.94728313378166 31.42045566691535 +4743 3391 41.8395 36.4695 754432.0830817239 4039484.130850202 +3391 4326 754432.0830817239 4039484.130850202 41.836179333701885 36.46931333583948 +4743 3392 45.0 33.225 500000.0 3675954.8351221993 +3392 4326 500000.0 3675954.8351221993 44.99691327646092 33.225022721436716 +4743 3392 42.3 29.476499999999998 238176.9425811259 3263560.4359008833 +3392 4326 238176.9425811259 3263560.4359008833 42.2969456784752 29.476637889094167 +4743 3392 47.7 36.9735 740352.5309914062 4095048.2955903644 +3392 4326 740352.5309914062 4095048.2955903644 47.696871312419546 36.97345566861504 +4743 3393 48.305 30.435000000000002 241159.5977938771 3369811.9340208373 +3393 4326 241159.5977938771 3369811.9340208373 48.30212216264915 30.4352249433892 +4743 3393 48.0305 29.9265 213313.91254956595 3314090.6176048997 +3393 4326 213313.91254956595 3314090.6176048997 48.02762683463952 29.926743258763256 +4743 3393 48.579499999999996 30.9435 268748.2058762467 3425588.5034682946 +3393 4326 268748.2058762467 3425588.5034682946 48.576617280422624 30.94370741990952 +4744 3394 44.925 33.225 1493016.5427673776 1246507.4465663333 +3394 4326 1493016.5427673776 1246507.4465663333 44.92573426056257 33.22626009863143 +4744 3394 39.4035 29.476499999999998 957350.476268962 845499.0845997697 +3394 4326 957350.476268962 845499.0845997697 39.40393077711932 29.477950070629074 +4744 3394 50.4465 36.9735 1985687.5809898037 1674765.052079015 +3394 4326 1985687.5809898037 1674765.052079015 50.44756098530801 36.97458267828896 +4326 3395 0.0 2.0 0.0 221194.07716771495 +4326 3395 -162.0 -71.8 -18033757.508510318 -11640877.666645138 +4326 3395 162.0 75.79999999999998 18033757.508510318 13244148.388311565 +4746 3396 10.21 50.955 3585009.6504283533 5647197.1803419385 +4746 3396 9.949 50.4105 3567449.3761094348 5586365.988756228 +4746 3396 10.471 51.499500000000005 3602133.126765381 5708096.268628192 +4746 3397 11.530000000000001 50.92 4466954.496988681 5642711.992223711 +4746 3397 10.603 50.272000000000006 4400421.194840638 5571464.861977598 +4746 3397 12.457 51.568 4531683.121037275 5714789.434192788 +4745 3398 12.7 50.93 4549206.018907146 5643952.490357189 +4745 3398 11.971 50.273 4497932.872648965 5570642.746779436 +4745 3398 13.429 51.586999999999996 4599027.021717923 5717771.840552784 +4745 3399 14.27 51.099999999999994 5448872.460334022 5662882.7029204015 +4745 3399 13.577 50.668 5399411.818354078 5615542.658050387 +4745 3399 14.963 51.532 5497432.814261514 5710686.244340632 +4269 3400 -114.99000000000001 54.495000000000005 500647.43517429335 6036181.518013937 +3400 4326 500647.43517429335 6036181.518013937 -114.99000000000002 54.495 +4269 3400 -119.499 49.5405 174690.16881907388 5495074.582453144 +3400 4326 174690.16881907388 5495074.582453144 -119.49899999999998 49.54050000000001 +4269 3400 -110.48100000000001 59.4495 756004.1431056475 6596169.835333343 +3400 4326 756004.1431056475 6596169.835333343 -110.48100000000001 59.4495 +4269 3401 -114.99000000000001 54.495000000000005 647.4351742933312 6036181.518013937 +3401 4326 647.4351742933312 6036181.518013937 -114.99000000000002 54.495 +4269 3401 -119.499 49.5405 -325309.8311809261 5495074.582453144 +3401 4326 -325309.8311809261 5495074.582453144 -119.49899999999998 49.54050000000001 +4269 3401 -110.48100000000001 59.4495 256004.14310564753 6596169.835333343 +3401 4326 256004.14310564753 6596169.835333343 -110.48100000000001 59.4495 +4617 3402 -114.99000000000001 54.495000000000005 500647.43517429335 6036181.518013937 +3402 4326 500647.43517429335 6036181.518013937 -114.99001546185603 54.4950069062855 +4617 3402 -119.499 49.5405 174690.16881907388 5495074.582453144 +3402 4326 174690.16881907388 5495074.582453144 -119.49901482807674 49.54050609909861 +4617 3402 -110.48100000000001 59.4495 756004.1431056475 6596169.835333343 +3402 4326 756004.1431056475 6596169.835333343 -110.48101644712936 59.449507715814086 +4617 3403 -114.99000000000001 54.495000000000005 647.4351742933312 6036181.518013937 +3403 4326 647.4351742933312 6036181.518013937 -114.99001546185603 54.4950069062855 +4617 3403 -119.499 49.5405 -325309.8311809261 5495074.582453144 +3403 4326 -325309.8311809261 5495074.582453144 -119.49901482807674 49.54050609909861 +4617 3403 -110.48100000000001 59.4495 256004.14310564753 6596169.835333343 +3403 4326 256004.14310564753 6596169.835333343 -110.48101644712936 59.449507715814086 +4152 3404 -79.85499999999999 35.21 1744617.9959167782 532455.9538610925 +3404 4326 1744617.9959167782 532455.9538610925 -79.8550028200936 35.210006899141526 +4152 3404 -83.8825 33.967999999999996 519981.51040541707 115749.34535515642 +3404 4326 519981.51040541707 115749.34535515642 -83.88250395103799 33.96800649061343 +4152 3404 -75.8275 36.452000000000005 2933006.304381549 998362.8710786074 +3404 4326 2933006.304381549 998362.8710786074 -75.8275016502862 36.45200729964803 +4756 3405 105.07 15.864999999999998 507494.41965696745 1754003.7735309522 +3405 4326 507494.41965696745 1754003.7735309522 105.07182838203781 15.86400272710402 +4756 3405 102.433 9.0835 217825.48133610882 1005082.1575492431 +3405 4326 217825.48133610882 1005082.1575492431 102.43478381474563 9.082502101810555 +4756 3405 107.707 22.646499999999996 778207.9402666617 2506920.130966481 +3405 4326 778207.9402666617 2506920.130966481 107.70889852002495 22.645499155605922 +4756 3406 108.765 15.995 260816.0926847657 1769668.2473961576 +3406 4326 260816.0926847657 1769668.2473961576 108.76681859596123 15.99397142826774 +4756 3406 108.0765 10.9865 180481.6396325248 1216041.2605547567 +3406 4326 180481.6396325248 1216041.2605547567 108.07828313813768 10.985473425340164 +4756 3406 109.4535 21.0035 339263.23531879263 2323312.642836075 +3406 4326 339263.23531879263 2323312.642836075 109.45536980955094 21.00247389138636 +4738 3407 114.135 22.354999999999997 117314.30062664364 78141.08868944374 +4738 3407 113.7975 22.1525 3070.872045913769 4735.321490472661 +4738 3407 114.47250000000001 22.557499999999997 231228.346415406 151803.7823079323 +10346 3408 0.0 45.0 0.0 -4876326.798841133 +10346 3408 -162.0 4.5 -2672869.881863064 8226247.632421515 +10346 3408 162.0 85.5 154590.8424533616 475781.69081676414 +10346 3409 0.0 -45.0 0.0 4876326.798841133 +10346 3409 -162.0 -85.5 -154590.8424533618 -475781.6908167648 +10346 3409 162.0 -4.5 2672869.8818630646 -8226247.632421517 +10346 3410 0.0 0.0 0.0 0.0 +10346 3410 -162.0 -77.4 -15600774.549318185 -7179683.381298528 +10346 3410 162.0 77.4 15600774.549318185 7179683.381298528 +10345 3411 0.0 75.0 1155351.6368184176 -1155351.6368184176 +10345 3411 -162.0 61.5 -2806756.072831591 1430113.649630734 +10345 3411 162.0 88.5 -73774.95966498797 144791.51088172407 +10345 3412 0.0 -75.0 0.0 1633913.9540985606 +10345 3412 -162.0 -88.5 -50216.28494183591 -154549.8334635899 +10345 3412 162.0 -61.5 973433.1927144071 -2995919.3117570756 +4326 3413 0.0 75.0 1155327.2723032606 -1155327.2723032609 +4326 3413 -162.0 61.5 -2806696.7599037155 1430083.4281844972 +4326 3413 162.0 88.5 -73773.40512252382 144788.4599203527 +4757 3414 103.83 1.2999999999999998 27630.671900054072 31372.9137690611 +3414 4326 27630.671900054072 31372.9137690611 103.83 1.2999999999999998 +4757 3414 103.614 1.1469999999999998 3590.3666011245186 14455.90707902472 +3414 4326 3590.3666011245186 14455.90707902472 103.614 1.1469999999999998 +4757 3414 104.04599999999999 1.4529999999999998 51668.08053399522 48291.989583099945 +3414 4326 51668.08053399522 48291.989583099945 104.04599999999999 1.453 +4324 3415 113.445 20.6 442221.5886016338 455874.52253814327 +3415 4326 442221.5886016338 455874.52253814327 113.44522611111111 20.600017333697178 +4324 3415 110.4615 18.538999999999998 126606.54155409907 231984.9704134633 +3415 4326 126606.54155409907 231984.9704134633 110.46172611111112 18.53901743675887 +4324 3415 116.4285 22.661 749346.8848503915 685588.4818409065 +3415 4326 749346.8848503915 685588.4818409065 116.4287261111111 22.661017203047784 +4258 3416 13.350000000000001 47.71 401250.2908077099 423340.5570879366 +3416 4326 401250.2908077099 423340.5570879366 13.350000000000001 47.71000000000001 +4258 3416 9.911999999999999 46.531 137655.6062539182 298083.27304178855 +3416 4326 137655.6062539182 298083.27304178855 9.912 46.53099999999997 +4258 3416 16.788 48.889 653249.5070182307 560041.3831983929 +3416 4326 653249.5070182307 560041.3831983929 16.788 48.88899999999999 +4269 3417 -93.4 42.68 4948139.1665247455 3710868.9950100156 +3417 4326 4948139.1665247455 3710868.9950100156 -93.4 42.680000000000014 +4269 3417 -96.325 41.933 4152677.152551135 3451472.3476617406 +3417 4326 4152677.152551135 3451472.3476617406 -96.325 41.933000000000014 +4269 3417 -90.47500000000001 43.427 5724733.8931241 3997489.8094929624 +3417 4326 5724733.8931241 3997489.8094929624 -90.47500000000001 43.42700000000001 +4269 3418 -93.14 41.2 1739482.3483855727 437403.0494001549 +3418 4326 1739482.3483855727 437403.0494001549 -93.14000000000001 41.20000000000001 +4269 3418 -95.84 40.444 989156.8013484788 170525.269870863 +3418 4326 989156.8013484788 170525.269870863 -95.84 40.444 +4269 3418 -90.44 41.955999999999996 2472614.8613148276 727307.2381044195 +3418 4326 2472614.8613148276 727307.2381044195 -90.44 41.95599999999999 +4269 3419 -98.32 39.265 1221731.4744716913 339482.6221800869 +3419 4326 1221731.4744716913 339482.6221800869 -98.32 39.26500000000003 +4269 3419 -101.686 38.594500000000004 259061.77106543793 116558.42624152584 +3419 4326 259061.77106543793 116558.42624152584 -101.686 38.5945 +4269 3419 -94.954 39.9355 2166375.297642951 597919.8953374784 +3419 4326 2166375.297642951 597919.8953374784 -94.954 39.93550000000001 +4269 3420 -98.32499999999999 37.935 1362803.191209763 1774209.624049821 +3420 4326 1362803.191209763 1774209.624049821 -98.32499999999999 37.935 +4269 3420 -101.6775 37.084500000000006 385569.65243959834 1480272.1892095525 +3420 4326 385569.65243959834 1480272.1892095525 -101.6775 37.08449999999999 +4269 3420 -94.9725 38.7855 2317703.7363001783 2102913.45910081 +3420 4326 2317703.7363001783 2102913.45910081 -94.9725 38.7855 +4269 3421 -115.52000000000001 38.495000000000005 674291.8650424038 27610002.552753896 +3421 4326 674291.8650424038 27610002.552753896 -115.52000000000001 38.495 +4269 3421 -116.861 35.3405 275123.3844698093 26464033.258810874 +3421 4326 275123.3844698093 26464033.258810874 -116.86100000000002 35.340500000000006 +4269 3421 -114.179 41.6495 1039945.6838938089 28762162.86318991 +3421 4326 1039945.6838938089 28762162.86318991 -114.179 41.64950000000001 +4269 3422 -116.59 38.5 1662356.1801604931 21050159.56559776 +3422 4326 1662356.1801604931 21050159.56559776 -116.59 38.50000000000002 +4269 3422 -118.03 36.25 1238431.6684644339 20233785.282194696 +3422 4326 1238431.6684644339 20233785.282194696 -118.03000000000002 36.250000000000014 +4269 3422 -115.14999999999999 40.75 2060610.6920075782 21873290.06607454 +3422 4326 2060610.6920075782 21873290.06607454 -115.14999999999999 40.75000000000001 +4269 3423 -118.495 39.475 2649600.388895613 14843577.433070434 +3423 4326 2649600.388895613 14843577.433070434 -118.495 39.475 +4269 3423 -119.8495 37.2025 2255916.5084621636 14018507.900573896 +3423 4326 2255916.5084621636 14018507.900573896 -119.8495 37.20250000000001 +4269 3423 -117.14049999999999 41.7475 3018369.006814181 15674711.638735153 +3423 4326 3018369.006814181 15674711.638735153 -117.14049999999999 41.74750000000001 +4269 3424 -74.74 40.114999999999995 425005.4885908589 466894.89505280356 +3424 4326 425005.4885908589 466894.89505280356 -74.74 40.115 +4269 3424 -75.514 38.994499999999995 203942.65566374268 60298.63176225035 +3424 4326 203942.65566374268 60298.63176225035 -75.514 38.99449999999999 +4269 3424 -73.966 41.2355 638986.5945119108 875445.0409534641 +3424 4326 638986.5945119108 875445.0409534641 -73.966 41.23550000000001 +4152 3425 -93.4 42.68 4948139.1665247455 3710868.9950100156 +3425 4326 4948139.1665247455 3710868.9950100156 -93.40000737057251 42.680007656691394 +4152 3425 -96.325 41.933 4152677.152551135 3451472.3476617406 +3425 4326 4152677.152551135 3451472.3476617406 -96.32500808904744 41.933007352301004 +4152 3425 -90.47500000000001 43.427 5724733.8931241 3997489.8094929624 +3425 4326 5724733.8931241 3997489.8094929624 -90.47500661839601 43.42700795712506 +4152 3426 -93.14 41.2 1739482.3483855727 437403.0494001549 +3426 4326 1739482.3483855727 437403.0494001549 -93.1400071345966 41.20000743414056 +4152 3426 -95.84 40.444 989156.8013484788 170525.269870863 +3426 4326 989156.8013484788 170525.269870863 -95.84000779395888 40.44400714775602 +4152 3426 -90.44 41.955999999999996 2472614.8613148276 727307.2381044195 +3426 4326 2472614.8613148276 727307.2381044195 -90.44000644765974 41.956007718368234 +4152 3427 -98.32 39.265 1221731.4744716913 339482.6221800869 +3427 4326 1221731.4744716913 339482.6221800869 -98.32000832547315 39.26500680696949 +4152 3427 -101.686 38.594500000000004 259061.77106543793 116558.42624152584 +3427 4326 259061.77106543793 116558.42624152584 -101.68600910336376 38.59450649548409 +4152 3427 -94.954 39.9355 2166375.297642951 597919.8953374784 +3427 4326 2166375.297642951 597919.8953374784 -94.95400750360324 39.93550711744209 +4152 3428 -98.32499999999999 37.935 1362803.191209763 1774209.624049821 +3428 4326 1362803.191209763 1774209.624049821 -98.32500819978237 37.93500658966191 +4152 3428 -101.6775 37.084500000000006 385569.65243959834 1480272.1892095525 +3428 4326 385569.65243959834 1480272.1892095525 -101.67750895811709 37.08450625920603 +4152 3428 -94.9725 38.7855 2317703.7363001783 2102913.45910081 +3428 4326 2317703.7363001783 2102913.45910081 -94.9725073990636 38.785506922530466 +4152 3429 -115.52000000000001 38.495000000000005 674291.8650424038 27610002.552753896 +3429 4326 674291.8650424038 27610002.552753896 -115.52001208197812 38.49500555407225 +4152 3429 -116.861 35.3405 275123.3844698093 26464033.258810874 +3429 4326 275123.3844698093 26464033.258810874 -116.86101200085497 35.340505114953004 +4152 3429 -114.179 41.6495 1039945.6838938089 28762162.86318991 +3429 4326 1039945.6838938089 28762162.86318991 -114.17901223844567 41.649505989878776 +4152 3430 -116.59 38.5 1662356.1801604931 21050159.56559776 +3430 4326 1662356.1801604931 21050159.56559776 -116.5900122785843 38.50000547753879 +4152 3430 -118.03 36.25 1238431.6684644339 20233785.282194696 +3430 4326 1238431.6684644339 20233785.282194696 -118.03001229048208 36.25000514192927 +4152 3430 -115.14999999999999 40.75 2060610.6920075782 21873290.06607454 +3430 4326 2060610.6920075782 21873290.06607454 -115.15001229815151 40.750005819445484 +4152 3431 -118.495 39.475 2649600.388895613 14843577.433070434 +3431 4326 2649600.388895613 14843577.433070434 -118.4950127345972 39.4750054320086 +4152 3431 -119.8495 37.2025 2255916.5084621636 14018507.900573896 +3431 4326 2255916.5084621636 14018507.900573896 -119.84951269441324 37.202505116838935 +4152 3431 -117.14049999999999 41.7475 3018369.006814181 15674711.638735153 +3431 4326 3018369.006814181 15674711.638735153 -117.14051281165082 41.74750575266931 +4152 3432 -74.74 40.114999999999995 425005.4885908589 466894.89505280356 +3432 4326 425005.4885908589 466894.89505280356 -74.74000150132949 40.115008096459 +4152 3432 -75.514 38.994499999999995 203942.65566374268 60298.63176225035 +3432 4326 203942.65566374268 60298.63176225035 -75.51400168360887 38.9945078440669 +4152 3432 -73.966 41.2355 638986.5945119108 875445.0409534641 +3432 4326 638986.5945119108 875445.0409534641 -73.96600131824013 41.235508343555686 +4269 3433 -92.13 35.585 1273679.7887367196 455607.7396143632 +3433 4326 1273679.7887367196 455607.7396143632 -92.13 35.58500000000002 +4269 3433 -94.37100000000001 34.7615 600203.1444100282 164421.57367372766 +3433 4326 600203.1444100282 164421.57367372766 -94.37100000000001 34.7615 +4269 3433 -89.889 36.408500000000004 1933532.987538265 762017.8253133954 +3433 4326 1933532.987538265 762017.8253133954 -89.889 36.40850000000002 +4269 3434 -92.44 34.055 1179067.0173950184 1817812.4606719767 +3434 4326 1179067.0173950184 1817812.4606719767 -92.44 34.055 +4269 3434 -94.27600000000001 33.1145 615429.5706780275 1483045.428399657 +3434 4326 615429.5706780275 1483045.428399657 -94.27600000000001 33.1145 +4269 3434 -90.604 34.9955 1730471.727809009 2162659.7597040734 +3434 4326 1730471.727809009 2162659.7597040734 -90.604 34.9955 +4269 3435 -88.145 39.78 1037181.6684585804 1133825.6030986719 +3435 4326 1037181.6684585804 1133825.6030986719 -88.145 39.78000000000001 +4269 3435 -89.1575 37.332 744620.8485560364 243286.42955415198 +3435 4326 744620.8485560364 243286.42955415198 -89.1575 37.332 +4269 3435 -87.1325 42.228 1309486.387956326 2027966.4951392366 +3435 4326 1309486.387956326 2027966.4951392366 -87.1325 42.227999999999994 +4269 3436 -90.22 39.739999999999995 2281585.671704886 1119166.0714885425 +3436 4326 2281585.671704886 1119166.0714885425 -90.22 39.739999999999995 +4269 3436 -91.39 37.247 1940502.8438380305 213585.89715549402 +3436 4326 1940502.8438380305 213585.89715549402 -91.39 37.247 +4269 3436 -89.05 42.233 2598989.437475753 2029410.0841629873 +3436 4326 2598989.437475753 2029410.0841629873 -89.05 42.233 +4269 3437 -71.595 44.0 1003107.9641180072 546729.0964382227 +3437 4326 1003107.9641180072 546729.0964382227 -71.595 43.99999999999999 +4269 3437 -72.4635 42.821 770468.1294705635 117996.6470221484 +3437 4326 770468.1294705635 117996.6470221484 -72.4635 42.821 +4269 3437 -70.7265 45.179 1226688.8578768428 977955.6079219251 +3437 4326 1226688.8578768428 977955.6079219251 -70.7265 45.17900000000001 +4269 3438 -71.465 41.575 337659.99889973126 179149.53146609687 +3438 4326 337659.99889973126 179149.53146609687 -71.465 41.57500000000001 +4269 3438 -71.8115 41.1745 242326.54428387046 33370.5293218306 +3438 4326 242326.54428387046 33370.5293218306 -71.8115 41.174499999999995 +4269 3438 -71.1185 41.975500000000004 431821.6903505476 325319.31401650066 +3438 4326 431821.6903505476 325319.31401650066 -71.1185 41.975500000000004 +4134 3439 52.995000000000005 18.130000000000003 711080.0903464175 2005537.9224610017 +3439 4326 711080.0903464175 2005537.9224610017 52.99757703865964 18.1305787304078 +4134 3439 52.0905 16.744 616241.2503143994 1851391.7531347068 +3439 4326 616241.2503143994 1851391.7531347068 52.09301830644987 16.744664379776104 +4134 3439 53.8995 19.516000000000002 804327.8914330881 2160311.540139401 +3439 4326 804327.8914330881 2160311.540139401 53.90213679149033 19.516495787390802 +4134 3440 56.955 21.735 495346.3646746813 2403292.8649226585 +3440 4326 495346.3646746813 2403292.8649226585 56.95781408898463 21.735350994455082 +4134 3440 54.2955 17.3745 212607.38273271243 1922843.582121351 +3440 4326 212607.38273271243 1922843.582121351 54.2981312519446 17.37510665897705 +4134 3440 59.6145 26.095499999999998 761504.5326953568 2888650.601891067 +3440 4326 761504.5326953568 2888650.601891067 59.61750958923685 26.095627368292934 +4152 3441 -92.13 35.585 1273679.7887367196 455607.7396143632 +3441 4326 1273679.7887367196 455607.7396143632 -92.1300063766645 35.58500648656259 +4152 3441 -94.37100000000001 34.7615 600203.1444100282 164421.57367372766 +3441 4326 600203.1444100282 164421.57367372766 -94.37100692280187 34.76150622770002 +4152 3441 -89.889 36.408500000000004 1933532.987538265 762017.8253133954 +3441 4326 1933532.987538265 762017.8253133954 -89.88900581479095 36.408506747129636 +4152 3442 -92.44 34.055 1179067.0173950184 1817812.4606719767 +3442 4326 1179067.0173950184 1817812.4606719767 -92.44000636182047 34.05500617569817 +4152 3442 -94.27600000000001 33.1145 615429.5706780275 1483045.428399657 +3442 4326 615429.5706780275 1483045.428399657 -94.27600679522018 33.11450591422384 +4152 3442 -90.604 34.9955 1730471.727809009 2162659.7597040734 +3442 4326 1730471.727809009 2162659.7597040734 -90.60400591938794 34.99550643953686 +4152 3443 -88.145 39.78 1037181.6684585804 1133825.6030986719 +3443 4326 1037181.6684585804 1133825.6030986719 -88.14500557851211 39.78000746320871 +4152 3443 -89.1575 37.332 744620.8485560364 243286.42955415198 +3443 4326 744620.8485560364 243286.42955415198 -89.15750567494628 37.33200695805758 +4152 3443 -87.1325 42.228 1309486.387956326 2027966.4951392366 +3443 4326 1309486.387956326 2027966.4951392366 -87.13250550400706 42.22800794954199 +4152 3444 -90.22 39.739999999999995 2281585.671704886 1119166.0714885425 +3444 4326 2281585.671704886 1119166.0714885425 -90.22000617139152 39.7400073484703 +4152 3444 -91.39 37.247 1940502.8438380305 213585.89715549402 +3444 4326 1940502.8438380305 213585.89715549402 -91.39000629374664 37.2470068353021 +4152 3444 -89.05 42.233 2598989.437475753 2029410.0841629873 +3444 4326 2598989.437475753 2029410.0841629873 -89.05000607188076 42.23300784442371 +4152 3445 -71.595 44.0 1003107.9641180072 546729.0964382227 +3445 4326 1003107.9641180072 546729.0964382227 -71.59500070694952 44.000008943714704 +4152 3445 -72.4635 42.821 770468.1294705635 117996.6470221484 +3445 4326 770468.1294705635 117996.6470221484 -72.46350092318538 42.82100869701777 +4152 3445 -70.7265 45.179 1226688.8578768428 977955.6079219251 +3445 4326 1226688.8578768428 977955.6079219251 -70.72650048772996 45.17900918335992 +4152 3446 -71.465 41.575 337659.99889973126 179149.53146609687 +3446 4326 337659.99889973126 179149.53146609687 -71.46500052503204 41.57500848957802 +4152 3446 -71.8115 41.1745 242326.54428387046 33370.5293218306 +3446 4326 242326.54428387046 33370.5293218306 -71.81150061664178 41.17450840060679 +4152 3446 -71.1185 41.975500000000004 431821.6903505476 325319.31401650066 +3446 4326 431821.6903505476 325319.31401650066 -71.11850043299016 41.975508577753324 +4258 3447 4.45 50.504999999999995 156767.96956138918 133694.78214910452 +3447 4326 156767.96956138918 133694.78214910452 4.45 50.504999999999995 +4258 3447 2.695 49.6005 30028.450544112275 34434.13492272611 +3447 4326 30028.450544112275 34434.13492272611 2.6950000000000003 49.6005 +4258 3447 6.205 51.4095 278748.1865885115 235907.85662637034 +3447 4326 278748.1865885115 235907.85662637034 6.205 51.4095 +4758 3448 -77.30000000000001 18.11 718248.2181634707 662200.6066681766 +3448 4326 718248.2181634707 662200.6066681766 -77.30000000000001 18.109999999999975 +4758 3448 -78.31700000000001 17.687 610278.2443310753 615853.651355864 +3448 4326 610278.2443310753 615853.651355864 -78.31700000000001 17.68699999999999 +4758 3448 -76.283 18.532999999999998 825705.5473042108 709141.506386575 +3448 4326 825705.5473042108 709141.506386575 -76.283 18.533000000000026 +4758 3449 -79.29499999999999 16.759999999999998 681737.6410191094 1853785.2672509893 +3449 4326 681737.6410191094 1853785.2672509893 -79.29499999999999 16.759999999999998 +4758 3449 -80.4695 14.42 557184.2526606515 1594242.9595443816 +3449 4326 557184.2526606515 1594242.9595443816 -80.4695 14.42 +4758 3449 -78.12049999999999 19.1 802986.483259507 2114385.0654517417 +3449 4326 802986.483259507 2114385.0654517417 -78.12049999999999 19.1 +4758 3450 -76.255 16.64 366152.31427291845 1840150.555291985 +3450 4326 366152.31427291845 1840150.555291985 -76.255 16.64 +4758 3450 -77.8255 14.336 195211.46474703238 1586748.6836550059 +3450 4326 195211.46474703238 1586748.6836550059 -77.8255 14.336 +4758 3450 -74.6845 18.944 533217.566903239 2094660.8850867462 +3450 4326 533217.566903239 2094660.8850867462 -74.6845 18.944 +4269 3451 -92.455 31.94 3294791.9521092325 523820.13123519265 +3451 4326 3294791.9521092325 523820.13123519265 -92.455 31.93999999999999 +4269 3451 -93.8905 30.959000000000003 2844945.3211703915 169769.86730686863 +3451 4326 2844945.3211703915 169769.86730686863 -93.8905 30.959000000000017 +4269 3451 -91.0195 32.921 3735181.9935435313 883822.2836548467 +3451 4326 3735181.9935435313 883822.2836548467 -91.0195 32.921 +4269 3452 -91.345 29.96 3277138.980136345 530946.1645714614 +3452 4326 3277138.980136345 530946.1645714614 -91.345 29.96000000000001 +4269 3452 -93.6805 28.961000000000002 2530193.3523571067 175345.3452367323 +3452 4326 2530193.3523571067 175345.3452367323 -93.6805 28.961000000000002 +4269 3452 -89.0095 30.959 4009277.3280332414 901672.0418161077 +3452 4326 4009277.3280332414 901672.0418161077 -89.0095 30.959 +4269 3453 -89.955 27.465 3727769.8618742893 716709.8416963745 +3453 4326 3727769.8618742893 716709.8416963745 -89.955 27.46499999999999 +4269 3453 -93.0375 25.7685 2719921.3674573544 101393.22179840723 +3453 4326 2719921.3674573544 101393.22179840723 -93.03750000000001 25.768500000000003 +4269 3453 -86.8725 29.1615 4705221.40093656 1356407.4647260855 +3453 4326 4705221.40093656 1356407.4647260855 -86.8725 29.16149999999999 +4269 3455 -100.245 43.635 1991884.1539554184 474433.10929061484 +3455 4326 1991884.1539554184 474433.10929061484 -100.245 43.63499999999999 +4269 3455 -103.6785 42.595499999999994 1067930.5069491067 113693.37574975195 +3455 4326 1067930.5069491067 113693.37574975195 -103.6785 42.595499999999994 +4269 3455 -96.81150000000001 44.6745 2884479.1602654597 872780.910393958 +3455 4326 2884479.1602654597 872780.910393958 -96.81150000000001 44.6745 +4152 3456 -92.455 31.94 3294791.9521092325 523820.13123519265 +3456 4326 3294791.9521092325 523820.13123519265 -92.45500624779022 31.940005749251707 +4152 3456 -93.8905 30.959000000000003 2844945.3211703915 169769.86730686863 +3456 4326 2844945.3211703915 169769.86730686863 -93.8905065804254 30.959005495611727 +4152 3456 -91.0195 32.921 3735181.9935435313 883822.2836548467 +3456 4326 3735181.9935435313 883822.2836548467 -91.01950591142374 32.92100600494784 +4152 3457 -91.345 29.96 3277138.980136345 530946.1645714614 +3457 4326 3277138.980136345 530946.1645714614 -91.34500585820699 29.96000536975566 +4152 3457 -93.6805 28.961000000000002 2530193.3523571067 175345.3452367323 +3457 4326 2530193.3523571067 175345.3452367323 -93.68050643949914 28.961005083140446 +4152 3457 -89.0095 30.959 4009277.3280332414 901672.0418161077 +3457 4326 4009277.3280332414 901672.0418161077 -89.00950526332946 30.95900566172793 +4152 3458 -100.25999999999999 45.045 1901299.2101861658 441850.1308508461 +3458 4326 1901299.2101861658 441850.1308508461 -100.26000955663959 45.045007514417705 +4152 3458 -103.689 44.2305 1001826.0644531978 166826.8571025809 +3458 4326 1001826.0644531978 166826.8571025809 -103.68901030397049 44.23050713960787 +4152 3458 -96.831 45.859500000000004 2775744.0497138826 754529.8289194349 +3458 4326 2775744.0497138826 754529.8289194349 -96.83100875604335 45.85950788528206 +4152 3459 -100.245 43.635 1991884.1539554184 474433.10929061484 +3459 4326 1991884.1539554184 474433.10929061484 -100.24500934456164 43.635007326922235 +4152 3459 -103.6785 42.595499999999994 1067930.5069491067 113693.37574975195 +3459 4326 1067930.5069491067 113693.37574975195 -103.67851006254774 42.59550693037905 +4152 3459 -96.81150000000001 44.6745 2884479.1602654597 872780.910393958 +3459 4326 2884479.1602654597 872780.910393958 -96.81150857536615 44.6745077230717 +4720 3460 179.32999999999998 -16.615 2061878.0841204945 4042510.9065416185 +3460 4326 2061878.0841204945 4042510.9065416185 179.3301538888889 -16.614962026705175 +4720 3460 177.062 -20.3905 1823802.4557620166 3623861.819042939 +3460 4326 1823802.4557620166 3623861.819042939 177.06215388888893 -20.390463082314078 +4720 3460 -178.40200000000002 -12.839500000000001 2309231.005496373 4458574.358127251 +3460 4326 2309231.005496373 4458574.358127251 -178.40184611111107 -12.839461125226611 +4155 3461 -13.565000000000001 10.844999999999999 656864.6517899282 1199102.4871203515 +3461 4326 656864.6517899282 1199102.4871203515 -13.564849098004656 10.845088865471366 +4155 3461 -14.973500000000001 9.1935 502911.2116853281 1016152.8863213988 +3461 4326 502911.2116853281 1016152.8863213988 -14.9733698921163 9.193742759903326 +4155 3461 -12.156500000000001 12.4965 809084.8022980315 1382993.3000157778 +3461 4326 809084.8022980315 1382993.3000157778 -12.156328040785022 12.49643749333308 +4155 3462 -9.825 9.85 409537.8476094216 1088840.7542522482 +3462 4326 409537.8476094216 1088840.7542522482 -9.824796751764408 9.850178992972408 +4155 3462 -11.7825 7.456 192854.86137441744 825051.6772918587 +3462 4326 192854.86137441744 825051.6772918587 -11.7823254106063 7.456405329727512 +4155 3462 -7.867500000000001 12.244 623180.7467921197 1353671.5901930365 +3462 4326 623180.7467921197 1353671.5901930365 -7.867267560214956 12.24395716239674 +4269 3463 -69.18 45.61 495709.77078438434 234466.4915910657 +3463 4326 495709.77078438434 234466.4915910657 -69.18 45.61000000000001 +4269 3463 -69.94500000000001 43.936 434161.4947354724 48768.47616865049 +3463 4326 434161.4947354724 48768.47616865049 -69.94500000000001 43.936 +4269 3463 -68.41499999999999 47.284 553711.7737722797 420787.73281540995 +3463 4326 553711.7737722797 420787.73281540995 -68.41499999999999 47.284 +4152 3464 -69.18 45.61 495709.77078438434 234466.4915910657 +3464 4326 495709.77078438434 234466.4915910657 -69.17999998692659 45.610009305669514 +4152 3464 -69.94500000000001 43.936 434161.4947354724 48768.47616865049 +3464 4326 434161.4947354724 48768.47616865049 -69.94500015087189 43.93600898237428 +4152 3464 -68.41499999999999 47.284 553711.7737722797 420787.73281540995 +3464 4326 553711.7737722797 420787.73281540995 -68.414999821287 47.28400961444854 +4759 3465 -85.84 32.995 199376.9683514333 276640.7418256493 +3465 4326 199376.9683514333 276640.7418256493 -85.84 32.995000000000005 +4759 3465 -86.69500000000001 31.1905 117873.04652490633 76870.30876444188 +3465 4326 117873.04652490633 76870.30876444188 -86.69500000000001 31.1905 +4759 3465 -84.985 34.7995 277629.11731132085 477116.87613607576 +3465 4326 277629.11731132085 477116.87613607576 -84.985 34.7995 +4759 3466 -87.39 32.58 610327.5962043395 286042.60682333034 +3466 4326 610327.5962043395 286042.60682333034 -87.39 32.580000000000005 +4759 3466 -88.37100000000001 30.384 516289.87988403835 42887.583717217465 +3466 4326 516289.87988403835 42887.583717217465 -88.37100000000001 30.383999999999997 +4759 3466 -86.40899999999999 34.776 699861.512213123 530136.2878085349 +3466 4326 699861.512213123 530136.2878085349 -86.40899999999999 34.776 +4759 3467 -158.78500000000003 61.35 -254894.82358153717 1272467.9326409101 +3467 4326 -254894.82358153717 1272467.9326409101 -158.78500000000003 61.35 +4759 3467 175.2995 52.305 -2028925.2498783472 732268.5502162899 +3467 4326 -2028925.2498783472 732268.5502162899 175.29950000000005 52.304999999999986 +4759 3467 -132.86950000000002 70.39500000000001 793326.4022635117 2395398.489500744 +3467 4326 793326.4022635117 2395398.489500744 -132.86950000000002 70.395 +4759 3468 -135.495 57.480000000000004 709022.2275555171 630020.6004242897 +3468 4326 709022.2275555171 630020.6004242897 -135.49500000000003 57.47999999999999 +4759 3468 -140.4495 54.897 383709.09014116693 362275.91215463355 +3468 4326 383709.09014116693 362275.91215463355 -140.4495 54.89700000000002 +4759 3468 -130.5405 60.063 992788.9149156073 920483.9307453353 +3468 4326 992788.9149156073 920483.9307453353 -130.54049999999998 60.06300000000001 +4759 3469 -142.495 64.94 476598.2733494 1218703.2590226599 +3469 4326 476598.2733494 1218703.2590226599 -142.495 64.94 +4759 3469 -143.8585 60.242 397073.46798556915 696498.9178886266 +3469 4326 397073.46798556915 696498.9178886266 -143.8585 60.242 +4759 3469 -141.1315 69.63799999999999 533735.232831844 1742745.5220906406 +3469 4326 533735.232831844 1742745.5220906406 -141.1315 69.63799999999999 +4759 3470 -146.0 65.05 500000.0 1230874.6384843953 +3470 4326 500000.0 1230874.6384843953 -146.0 65.05 +4759 3470 -147.8 60.253 400346.16334001743 697634.2588449224 +3470 4326 400346.16334001743 697634.2588449224 -147.8 60.252999999999986 +4759 3470 -144.2 69.847 569223.8152984743 1766840.1544494042 +3470 4326 569223.8152984743 1766840.1544494042 -144.2 69.847 +4759 3471 -150.0 64.87 500000.0 1210808.0839250025 +3471 4326 500000.0 1210808.0839250025 -150.0 64.87 +4759 3471 -151.809 59.686 398121.27172606904 634499.4378558247 +3471 4326 398121.27172606904 634499.4378558247 -151.80900000000003 59.686 +4759 3471 -148.191 70.054 568885.014310113 1789932.4850946558 +3471 4326 568885.014310113 1789932.4850946558 -148.191 70.054 +4759 3472 -153.93 63.5 503485.94536649715 1058097.4330165344 +3472 4326 503485.94536649715 1058097.4330165344 -153.93 63.50000000000001 +4759 3472 -155.793 56.498 389590.6245938524 279510.3993634731 +3472 4326 389590.6245938524 279510.3993634731 -155.793 56.49799999999999 +4759 3472 -152.067 70.502 572018.4257739565 1840031.82643212 +3472 4326 572018.4257739565 1840031.82643212 -152.067 70.502 +4759 3473 -157.995 63.145 500252.0814158846 1018528.7945634346 +3473 4326 500252.0814158846 1018528.7945634346 -157.99500000000003 63.145 +4759 3473 -159.7995 55.7155 386918.9132892374 192419.31067588035 +3473 4326 386918.9132892374 192419.31067588035 -159.7995 55.7155 +4759 3473 -156.19050000000001 70.5745 567177.5517000024 1847974.9441158164 +3473 4326 567177.5517000024 1847974.9441158164 -156.19050000000001 70.5745 +4759 3474 -162.005 62.53 499742.59696889075 949988.3243515419 +3474 4326 499742.59696889075 949988.3243515419 -162.005 62.53 +4759 3474 -163.80949999999999 55.141 384627.2292582616 128493.51815071332 +3474 4326 384627.2292582616 128493.51815071332 -163.80949999999999 55.14099999999999 +4759 3474 -160.2005 69.919 568967.7430237273 1774868.1191782567 +3474 4326 568967.7430237273 1774868.1191782567 -160.2005 69.91900000000001 +4759 3475 -166.13 61.695 493120.92207579594 856945.9191454424 +3475 4326 493120.92207579594 856945.9191454424 -166.13 61.69499999999999 +4759 3475 -168.047 55.075500000000005 369272.75018676487 121622.30920951095 +3475 4326 369272.75018676487 121622.30920951095 -168.047 55.07550000000001 +4759 3475 -164.213 68.3145 573703.4914816638 1595953.9075491133 +3475 4326 573703.4914816638 1595953.9075491133 -164.213 68.3145 +4759 3476 -170.57999999999998 61.155 468774.10340198886 796907.9866779154 +3476 4326 468774.10340198886 796907.9866779154 -170.57999999999998 61.155 +4759 3476 -172.902 56.9565 323481.636023819 332869.3282945642 +3476 4326 323481.636023819 332869.3282945642 -172.902 56.9565 +4759 3476 -168.258 65.3535 581075.9216982052 1265830.5148130064 +3476 4326 581075.9216982052 1265830.5148130064 -168.258 65.3535 +4759 3477 -176.21 52.82 985844.7880543226 202527.91516819393 +3477 4326 985844.7880543226 202527.91516819393 -176.21 52.82 +4759 3477 173.557 51.452 276522.2880442635 102933.70544811105 +3477 4326 276522.2880442635 102933.70544811105 173.557 51.452 +4759 3477 -165.977 54.188 1652267.2955383519 400290.4365906697 +3477 4326 1652267.2955383519 400290.4365906697 -165.977 54.188 +4759 3478 -111.895 34.17 215357.474309584 351509.2431937203 +3478 4326 215357.474309584 351509.2431937203 -111.895 34.17 +4759 3478 -113.2045 31.613999999999997 91169.08245612771 68790.21379401149 +3478 4326 91169.08245612771 68790.21379401149 -113.2045 31.613999999999994 +4759 3478 -110.5855 36.726 332264.3607156152 635892.0351637731 +3478 4326 332264.3607156152 635892.0351637731 -110.5855 36.726000000000006 +4759 3479 -111.895 34.17 706553.3934041469 1153245.5485358275 +3479 4326 706553.3934041469 1153245.5485358275 -111.895 34.17 +4759 3479 -113.2045 31.613999999999997 299111.1629138048 225689.67780187496 +3479 4326 299111.1629138048 225689.67780187496 -113.2045 31.613999999999994 +4759 3479 -110.5855 36.726 1090106.1703268215 2086259.9578863946 +3479 4326 1090106.1703268215 2086259.9578863946 -110.5855 36.726000000000006 +4759 3480 -110.375 34.17 194153.50067005004 351528.6430514126 +3480 4326 194153.50067005004 351528.6430514126 -110.375 34.17 +4759 3480 -111.5765 31.613999999999997 79592.58561085619 68933.09865045993 +3480 4326 79592.58561085619 68933.09865045993 -111.5765 31.613999999999994 +4759 3480 -109.1735 36.726 302072.0270641723 635525.7685795034 +3480 4326 302072.0270641723 635525.7685795034 -109.1735 36.72599999999999 +4759 3481 -110.375 34.17 636986.5507547572 1153309.1963629022 +3481 4326 636986.5507547572 1153309.1963629022 -110.375 34.17 +4759 3481 -111.5765 31.613999999999997 261130.53021934442 226158.4601393042 +3481 4326 261130.53021934442 226158.4601393042 -111.5765 31.613999999999994 +4759 3481 -109.1735 36.726 991049.9575596204 2085058.2958645122 +3481 4326 991049.9575596204 2085058.2958645122 -109.1735 36.72599999999999 +4759 3482 -113.66499999999999 34.525 221163.54809494727 390901.08565793367 +3482 4326 221163.54809494727 390901.08565793367 -113.66499999999999 34.525 +4759 3482 -114.6955 32.2975 124310.60752930673 144250.73145847756 +3482 4326 124310.60752930673 144250.73145847756 -114.69550000000001 32.2975 +4759 3482 -112.6345 36.7525 312968.5783435126 638607.9599893158 +3482 4326 312968.5783435126 638607.9599893158 -112.6345 36.7525 +4759 3483 -113.66499999999999 34.525 725602.1919125565 1282483.876830491 +3483 4326 725602.1919125565 1282483.876830491 -113.66499999999999 34.525 +4759 3483 -114.6955 32.2975 407843.2005554682 473263.5546537977 +3483 4326 407843.2005554682 473263.5546537977 -114.69550000000001 32.2975 +4759 3483 -112.6345 36.7525 1026799.7977149363 2095170.4724058916 +3483 4326 1026799.7977149363 2095170.4724058916 -112.6345 36.7525 +4759 3484 -92.13 35.585 388218.3760538642 138869.51677349143 +3484 4326 388218.3760538642 138869.51677349143 -92.13 35.58500000000002 +4759 3484 -94.37100000000001 34.7615 182942.28431090515 50115.795887343964 +3484 4326 182942.28431090515 50115.795887343964 -94.37100000000001 34.7615 +4759 3484 -89.889 36.408500000000004 589342.0332958896 232263.49768251824 +3484 4326 589342.0332958896 232263.49768251824 -89.889 36.40850000000002 +4759 3485 -92.13 35.585 1273679.7887367196 455607.7396143632 +3485 4326 1273679.7887367196 455607.7396143632 -92.13 35.58500000000002 +4759 3485 -94.37100000000001 34.7615 600203.1444100282 164421.57367372766 +3485 4326 600203.1444100282 164421.57367372766 -94.37100000000001 34.7615 +4759 3485 -89.889 36.408500000000004 1933532.987538265 762017.8253133954 +3485 4326 1933532.987538265 762017.8253133954 -89.889 36.40850000000002 +4759 3486 -92.44 34.055 359380.3456728528 554070.3461636706 +3486 4326 359380.3456728528 554070.3461636706 -92.44 34.055 +4759 3486 -94.27600000000001 33.1145 187583.30831943935 452033.15065267665 +3486 4326 187583.30831943935 452033.15065267665 -94.27600000000001 33.1145 +4759 3486 -90.604 34.9955 527448.8375440208 659180.0131279876 +3486 4326 527448.8375440208 659180.0131279876 -90.604 34.9955 +4759 3487 -92.44 34.055 1179067.0173950184 1817812.4606719767 +3487 4326 1179067.0173950184 1817812.4606719767 -92.44 34.055 +4759 3487 -94.27600000000001 33.1145 615429.5706780275 1483045.428399657 +3487 4326 615429.5706780275 1483045.428399657 -94.27600000000001 33.1145 +4759 3487 -90.604 34.9955 1730471.727809009 2162659.7597040734 +3487 4326 1730471.727809009 2162659.7597040734 -90.604 34.9955 +4759 3488 -119.285 37.269999999999996 63315.465344822995 -82731.13159650704 +3488 4326 63315.465344822995 -82731.13159650704 -119.28500000000001 37.27 +4759 3488 -123.93350000000001 33.004 -367870.9056195842 -548947.5140136816 +3488 4326 -367870.9056195842 -548947.5140136816 -123.93350000000002 33.004 +4759 3488 -114.63650000000001 41.536 447908.0593044227 403648.150371247 +3488 4326 447908.0593044227 403648.150371247 -114.63650000000003 41.536 +4759 3489 -122.22 40.8 1981436.393996671 662877.7471431005 +3489 4326 1981436.393996671 662877.7471431005 -122.22 40.8 +4759 3489 -124.227 39.711000000000006 1809032.8706182928 544364.0613099996 +3489 4326 1809032.8706182928 544364.0613099996 -124.227 39.71100000000001 +4759 3489 -120.213 41.888999999999996 2148310.5570726353 785305.6470252918 +3489 4326 2148310.5570726353 785305.6470252918 -120.21300000000001 41.88899999999998 +4759 3490 -122.22 40.8 6500762.569637412 2174791.409085322 +3490 4326 6500762.569637412 2174791.409085322 -122.22 40.8 +4759 3490 -124.227 39.711000000000006 5935135.343353516 1785967.7581478895 +3490 4326 5935135.343353516 1785967.7581478895 -124.227 39.71100000000001 +4759 3490 -120.213 41.888999999999996 7048248.886329139 2576456.9439488114 +3490 4326 7048248.886329139 2576456.9439488114 -120.21300000000001 41.88899999999998 +4759 3491 -121.80000000000001 39.09 2017301.8204829379 658016.692001398 +3491 4326 2017301.8204829379 658016.692001398 -121.80000000000001 39.089999999999996 +4759 3491 -123.834 38.127 1839195.8283859724 552723.7703899188 +3491 4326 1839195.8283859724 552723.7703899188 -123.834 38.127 +4759 3491 -119.766 40.053 2190614.0811918867 767255.0743562812 +3491 4326 2190614.0811918867 767255.0743562812 -119.76600000000002 40.053000000000004 +4759 3492 -121.80000000000001 39.09 6618431.056367773 2158843.097341253 +3492 4326 6618431.056367773 2158843.097341253 -121.80000000000001 39.089999999999996 +4759 3492 -123.834 38.127 6034094.980629644 1813394.5703542582 +3492 4326 6034094.980629644 1813394.5703542582 -123.834 38.127 +4759 3492 -119.766 40.053 7187039.698377048 2517236.023450566 +3492 4326 7187039.698377048 2517236.023450566 -119.76600000000002 40.053000000000004 +4759 3493 -120.425 37.72 2006611.9340582322 635399.2547793326 +3493 4326 2006611.9340582322 635399.2547793326 -120.42499999999998 37.71999999999999 +4759 3493 -122.7605 36.82899999999999 1798347.426047734 538949.0753376134 +3493 4326 1798347.426047734 538949.0753376134 -122.7605 36.82899999999999 +4759 3493 -118.0895 38.611000000000004 2209937.041178584 736997.6950945166 +3493 4326 2209937.041178584 736997.6950945166 -118.08950000000002 38.611 +4759 3494 -120.425 37.72 6583359.320656051 2084639.0553885268 +3494 4326 6583359.320656051 2084639.0553885268 -120.42499999999998 37.71999999999999 +4759 3494 -122.7605 36.82899999999999 5900078.18062494 1768202.0916701525 +3494 4326 5900078.18062494 1768202.0916701525 -122.7605 36.82899999999999 +4759 3494 -118.0895 38.611000000000004 7250435.109600073 2417966.60498926 +3494 4326 7250435.109600073 2417966.60498926 -118.08950000000002 38.611 +4759 3495 -118.815 36.68 2016534.9266023266 649444.111312916 +3495 4326 2016534.9266023266 649444.111312916 -118.81499999999998 36.67999999999997 +4759 3495 -121.6905 35.870000000000005 1757042.2782625472 562953.5650761565 +3495 4326 1757042.2782625472 562953.5650761565 -121.6905 35.87000000000002 +4759 3495 -115.93950000000001 37.489999999999995 2270631.083070669 743632.1654617812 +3495 4326 2270631.083070669 743632.1654617812 -115.93950000000002 37.489999999999995 +4759 3496 -118.815 36.68 6615915.005361133 2130717.8888657917 +3496 4326 6615915.005361133 2130717.8888657917 -118.81499999999998 36.67999999999997 +4759 3496 -121.6905 35.870000000000005 5764562.874933041 1846956.821754023 +3496 4326 5764562.874933041 1846956.821754023 -121.6905 35.87000000000002 +4759 3496 -115.93950000000001 37.489999999999995 7449562.145374354 2439733.1965191932 +3496 4326 7449562.145374354 2439733.1965191932 -115.93950000000002 37.489999999999995 +4759 3497 -117.77000000000001 34.285 2021176.3034016618 587100.3705963591 +3497 4326 2021176.3034016618 587100.3705963591 -117.77 34.28500000000002 +4759 3497 -121.055 32.9125 1714139.7936607758 439167.22925522097 +3497 4326 1714139.7936607758 439167.22925522097 -121.055 32.91250000000001 +4759 3497 -114.485 35.6575 2318240.215058904 744899.1637714047 +3497 4326 2318240.215058904 744899.1637714047 -114.485 35.6575 +4759 3498 -117.77000000000001 34.285 6631142.589076952 1926178.466198221 +3498 4326 6631142.589076952 1926178.466198221 -117.77 34.28500000000002 +4759 3498 -121.055 32.9125 5623806.9733687285 1440834.4849815036 +3498 4326 5623806.9733687285 1440834.4849815036 -121.055 32.91250000000001 +4759 3498 -114.485 35.6575 7605759.772572422 2443890.0068066833 +3498 4326 7605759.772572422 2443890.0068066833 -114.485 35.6575 +4759 3499 -116.285 33.305 1996740.5868549028 626244.559176123 +3499 4326 1996740.5868549028 626244.559176123 -116.285 33.305 +4759 3499 -117.96350000000001 32.6075 1839164.448277042 550211.7107019561 +3499 4326 1839164.448277042 550211.7107019561 -117.96350000000001 32.6075 +4759 3499 -114.6065 34.0025 2151827.0133517752 704802.7808291488 +3499 4326 2151827.0133517752 704802.7808291488 -114.6065 34.0025 +4759 3500 -116.285 33.305 6550973.0757064605 2054604.0248969968 +3500 4326 6550973.0757064605 2054604.0248969968 -116.285 33.305 +4759 3500 -117.96350000000001 32.6075 6033992.027722262 1805152.9211946672 +3500 4326 6033992.027722262 1805152.9211946672 -117.96350000000001 32.6075 +4759 3500 -114.6065 34.0025 7059785.79330495 2312340.457103632 +3500 4326 7059785.79330495 2312340.457103632 -114.6065 34.0025 +4759 3501 -105.55000000000001 39.115 910077.8060260228 447076.0864687894 +3501 4326 910077.8060260228 447076.0864687894 -105.55000000000001 39.115 +4759 3501 -108.709 38.2375 633503.6654187832 354628.51937078615 +3501 4326 633503.6654187832 354628.51937078615 -108.709 38.237500000000004 +4759 3501 -102.391 39.99250000000001 1179883.6778052924 549041.2134201529 +3501 4326 1179883.6778052924 549041.2134201529 -102.391 39.992500000000014 +4759 3502 -105.55000000000001 39.115 2985813.6016209614 1466782.1270270192 +3502 4326 2985813.6016209614 1466782.1270270192 -105.55000000000001 39.115 +4759 3502 -108.709 38.2375 2078419.9419787095 1163477.0673063204 +3502 4326 2078419.9419787095 1163477.0673063204 -108.709 38.237500000000004 +4759 3502 -102.391 39.99250000000001 3871001.6992834485 1801312.7143666178 +3502 4326 3871001.6992834485 1801312.7143666178 -102.391 39.992500000000014 +4759 3503 -105.55000000000001 40.285 910150.1227950495 410466.04419278353 +3503 4326 910150.1227950495 410466.04419278353 -105.55000000000001 40.285 +4759 3503 -108.709 39.6325 638965.6755298494 343001.4062162024 +3503 4326 638965.6755298494 343001.4062162024 -108.709 39.63249999999999 +4759 3503 -102.391 40.9375 1176178.9159974535 487510.83186888543 +3503 4326 1176178.9159974535 487510.83186888543 -102.391 40.93749999999999 +4759 3504 -105.55000000000001 40.285 2986050.860887343 1346670.6799931568 +3504 4326 2986050.860887343 1346670.6799931568 -105.55000000000001 40.285 +4759 3504 -108.709 39.6325 2096339.886818099 1125330.4468983235 +3504 4326 2096339.886818099 1125330.4468983235 -108.709 39.63249999999999 +4759 3504 -102.391 40.9375 3858846.993252231 1599441.7875605011 +3504 4326 3858846.993252231 1599441.7875605011 -102.391 40.93749999999999 +4759 3505 -105.55000000000001 37.83 910000.3279125895 433913.0553515627 +3505 4326 910000.3279125895 433913.0553515627 -105.55000000000001 37.82999999999999 +4759 3505 -108.709 37.065 629052.8991190449 353911.53463970317 +3505 4326 629052.8991190449 353911.53463970317 -108.709 37.06500000000002 +4759 3505 -102.391 38.595 1185210.975475688 523332.2323330514 +3505 4326 1185210.975475688 523332.2323330514 -102.391 38.595000000000006 +4759 3506 -105.55000000000001 37.83 2985559.408843806 1423596.4157699181 +3506 4326 2985559.408843806 1423596.4157699181 -105.55000000000001 37.82999999999999 +4759 3506 -108.709 37.065 2063817.719543651 1161124.7599010922 +3506 4326 2063817.719543651 1161124.7599010922 -108.709 37.06500000000002 +4759 3506 -102.391 38.595 3888479.6750570713 1716965.8322500188 +3506 4326 3888479.6750570713 1716965.8322500188 -102.391 38.595000000000006 +4759 3507 -72.755 41.515 304383.2321191824 228105.00864740624 +3507 4326 304383.2321191824 228105.00864740624 -72.755 41.51499999999999 +4759 3507 -73.63250000000001 41.0335 230588.64706697664 175009.14457170662 +3507 4326 230588.64706697664 175009.14457170662 -73.63250000000001 41.03349999999999 +4759 3507 -71.8775 41.9965 377091.7689576581 281949.1244673686 +3507 4326 377091.7689576581 281949.1244673686 -71.8775 41.9965 +4759 3508 -72.755 41.515 998630.6540483504 748374.5158726999 +3508 4326 998630.6540483504 748374.5158726999 -72.755 41.51499999999999 +4759 3508 -73.63250000000001 41.0335 756522.9195895719 574175.8351510088 +3508 4326 756522.9195895719 574175.8351510088 -73.63250000000001 41.03349999999999 +4759 3508 -71.8775 41.9965 1237175.245325916 925028.0858586932 +3508 4326 1237175.245325916 925028.0858586932 -71.8775 41.9965 +4759 3509 -75.38499999999999 39.144999999999996 202737.5480806781 127103.25313109877 +3509 4326 202737.5480806781 127103.25313109877 -75.38499999999999 39.144999999999996 +4759 3509 -75.7585 38.5105 170185.3710576928 56721.266312237676 +3509 4326 170185.3710576928 56721.266312237676 -75.75850000000001 38.51049999999999 +4759 3509 -75.0115 39.7795 234709.65364230535 197625.8780571235 +3509 4326 234709.65364230535 197625.8780571235 -75.01150000000001 39.7795 +4759 3510 -75.38499999999999 39.144999999999996 665148.1059946908 417004.58964761323 +3510 4326 665148.1059946908 417004.58964761323 -75.38499999999999 39.144999999999996 +4759 3510 -75.7585 38.5105 558349.8385451131 186093.02122606646 +3510 4326 558349.8385451131 186093.02122606646 -75.75850000000001 38.51049999999999 +4759 3510 -75.0115 39.7795 770043.2556581295 648377.5682590795 +3510 4326 770043.2556581295 648377.5682590795 -75.01150000000001 39.7795 +4759 3511 -81.15 27.619999999999997 185195.14759915508 364110.21714043804 +3511 4326 185195.14759915508 364110.21714043804 -81.15 27.62 +4759 3511 -82.212 24.731 77384.67721616034 44587.952464870745 +3511 4326 77384.67721616034 44587.952464870745 -82.212 24.730999999999998 +4759 3511 -80.088 30.508999999999997 287539.61535454553 684644.0779293055 +3511 4326 287539.61535454553 684644.0779293055 -80.088 30.509 +4759 3512 -81.15 27.619999999999997 607594.4137482274 1194584.9374015871 +3512 4326 607594.4137482274 1194584.9374015871 -81.15 27.62 +4759 3512 -82.212 24.731 253886.22883335204 146285.6407118301 +3512 4326 253886.22883335204 146285.6407118301 -82.212 24.730999999999998 +4759 3512 -80.088 30.508999999999997 943369.5550423709 2246203.11233973 +3512 4326 943369.5550423709 2246203.11233973 -80.088 30.509 +4759 3513 -83.8 27.71 419682.8006494569 411606.1169059885 +3513 4326 419682.8006494569 411606.1169059885 -83.8 27.709999999999994 +4759 3513 -87.247 24.740000000000002 71808.96482317086 86317.42506656054 +3513 4326 71808.96482317086 86317.42506656054 -87.247 24.73999999999999 +4759 3513 -80.353 30.68 749112.5376311431 746513.1936236266 +3513 4326 749112.5376311431 746513.1936236266 -80.353 30.679999999999993 +4759 3514 -84.83500000000001 30.11 567714.4986294459 123087.90795582991 +3514 4326 567714.4986294459 123087.90795582991 -84.83500000000002 30.109999999999992 +4759 3514 -87.3505 29.3 323067.84639475704 36716.95956284991 +3514 4326 323067.84639475704 36716.95956284991 -87.35050000000001 29.299999999999983 +4759 3514 -82.3195 30.92 808415.1822636275 214828.78102081481 +3514 4326 808415.1822636275 214828.78102081481 -82.3195 30.920000000000016 +4759 3515 -84.83500000000001 30.11 1862576.6509201073 403830.911351752 +3515 4326 1862576.6509201073 403830.911351752 -84.83500000000002 30.109999999999992 +4759 3515 -87.3505 29.3 1059931.7593801322 120462.22483245008 +3515 4326 1059931.7593801322 120462.22483245008 -87.35050000000001 29.299999999999983 +4759 3515 -82.3195 30.92 2652275.477143252 704817.4257324567 +3515 4326 2652275.477143252 704817.4257324567 -82.3195 30.920000000000016 +4759 3516 -82.235 27.935000000000002 176872.42951769338 399028.39882096706 +3516 4326 176872.42951769338 399028.39882096706 -82.235 27.935 +4759 3516 -83.2295 26.4365 77365.23380374948 233558.28670167812 +3516 4326 77365.23380374948 233558.28670167812 -83.2295 26.4365 +4759 3516 -81.2405 29.433500000000002 273690.74826097145 565316.0981896543 +3516 4326 273690.74826097145 565316.0981896543 -81.2405 29.433500000000006 +4759 3517 -82.235 27.935000000000002 580288.9628426317 1309145.6717984562 +3517 4326 580288.9628426317 1309145.6717984562 -82.235 27.935 +4759 3517 -83.2295 26.4365 253822.43823780073 766265.8122870891 +3517 4326 253822.43823780073 766265.8122870891 -83.2295 26.4365 +4759 3517 -81.2405 29.433500000000002 897933.73025287 1854707.8988105578 +3517 4326 897933.73025287 1854707.8988105578 -81.2405 29.433500000000006 +4759 3518 -82.12 32.519999999999996 204384.1730120967 279375.5698015051 +3518 4326 204384.1730120967 279375.5698015051 -82.12 32.51999999999999 +4759 3518 -83.335 30.576 87935.98120049336 64428.67874660541 +3518 4326 87935.98120049336 64428.67874660541 -83.335 30.576000000000004 +4759 3518 -80.905 34.464 315913.03382685786 495690.7435459547 +3518 4326 315913.03382685786 495690.7435459547 -80.905 34.464000000000006 +4759 3519 -82.12 32.519999999999996 670550.4079571866 916584.6819237713 +3519 4326 670550.4079571866 916584.6819237713 -82.12 32.51999999999999 +4759 3519 -83.335 30.576 288503.2986552846 211379.75685448793 +3519 4326 288503.2986552846 211379.75685448793 -83.335 30.576000000000004 +4759 3519 -80.905 34.464 1036458.0121469488 1626278.7144503533 +3519 4326 1036458.0121469488 1626278.7144503533 -80.905 34.464000000000006 +4759 3520 -84.3 32.815 687514.8760667271 312094.28572832706 +3520 4326 687514.8760667271 312094.28572832706 -84.3 32.815 +4759 3520 -85.479 30.8395 574464.4552982544 93794.36979869824 +3520 4326 574464.4552982544 93794.36979869824 -85.479 30.839500000000005 +4759 3520 -83.121 34.790499999999994 795691.9523103088 531682.9947136147 +3520 4326 795691.9523103088 531682.9947136147 -83.121 34.7905 +4759 3521 -84.3 32.815 2255621.7222289215 1023929.3357603531 +3521 4326 2255621.7222289215 1023929.3357603531 -84.3 32.815 +4759 3521 -85.479 30.8395 1884722.1334243573 307723.6949145625 +3521 4326 1884722.1334243573 307723.6949145625 -85.479 30.839500000000005 +4759 3521 -83.121 34.790499999999994 2610532.679871406 1744363.2918229178 +3521 4326 2610532.679871406 1744363.2918229178 -83.121 34.7905 +4759 3522 -113.985 43.845 501206.1570626486 241974.0498370269 +3522 4326 501206.1570626486 241974.0498370269 -113.985 43.845 +4759 3522 -115.1685 42.1755 403459.76413783856 57175.03314187794 +3522 4326 403459.76413783856 57175.03314187794 -115.16850000000001 42.17550000000001 +4759 3522 -112.8015 45.514500000000005 593643.3516935457 428187.0188976105 +3522 4326 593643.3516935457 428187.0188976105 -112.8015 45.514500000000005 +4759 3523 -113.985 43.845 1644373.8672963725 793876.5285069791 +3523 4326 1644373.8672963725 793876.5285069791 -113.985 43.845 +4759 3523 -115.1685 42.1755 1323684.2431755583 187581.75456631123 +3523 4326 1323684.2431755583 187581.75456631123 -115.16850000000001 42.17550000000001 +4759 3523 -112.8015 45.514500000000005 1947644.896681241 1404810.2444999106 +3523 4326 1947644.896681241 1404810.2444999106 -112.8015 45.514500000000005 +4759 3524 -112.14 43.370000000000005 202161.21936047942 189202.38724812766 +3524 4326 202161.21936047942 189202.38724812766 -112.14 43.37000000000001 +4759 3524 -113.13 42.128 120351.05903192172 51687.362961994455 +3524 4326 120351.05903192172 51687.362961994455 -113.13 42.128 +4759 3524 -111.15 44.612 280695.92876894755 327698.8013920951 +3524 4326 280695.92876894755 327698.8013920951 -111.15 44.611999999999995 +4759 3525 -112.14 43.370000000000005 663257.2675185056 620741.4988298989 +3525 4326 663257.2675185056 620741.4988298989 -112.14 43.37000000000001 +4759 3525 -113.13 42.128 394851.7665072292 169577.62331781015 +3525 4326 394851.7665072292 169577.62331781015 -113.13 42.128 +4759 3525 -111.15 44.612 920916.5599694549 1075125.1509005656 +3525 4326 920916.5599694549 1075125.1509005656 -111.15 44.611999999999995 +4759 3526 -115.78 45.5 797655.41440746 425871.2238890269 +3526 4326 797655.41440746 425871.2238890269 -115.78 45.50000000000001 +4759 3526 -117.094 42.341 689251.4777751659 75770.44093186814 +3526 4326 689251.4777751659 75770.44093186814 -117.09399999999998 42.340999999999994 +4759 3526 -114.466 48.659 894584.9816078962 777836.67337109 +3526 4326 894584.9816078962 777836.67337109 -114.46600000000001 48.65899999999999 +4759 3527 -115.78 45.5 2616974.4724351414 1397212.5070425826 +3527 4326 2616974.4724351414 1397212.5070425826 -115.78 45.50000000000001 +4759 3527 -117.094 42.341 2261319.223667356 248590.18829063742 +3527 4326 2261319.223667356 248590.18829063742 -117.09399999999998 42.340999999999994 +4759 3527 -114.466 48.659 2934984.2274919054 2551952.4858849845 +3527 4326 2934984.2274919054 2551952.4858849845 -114.46600000000001 48.65899999999999 +4759 3528 -88.145 39.78 316133.6048133849 345590.73500594514 +3528 4326 316133.6048133849 345590.73500594514 -88.145 39.78000000000001 +4759 3528 -89.1575 37.332 226960.888561657 74153.85203580958 +3528 4326 226960.888561657 74153.85203580958 -89.1575 37.332 +4759 3528 -87.1325 42.228 399132.2493135867 618125.4239692872 +3528 4326 399132.2493135867 618125.4239692872 -87.1325 42.227999999999994 +4759 3529 -88.145 39.78 1037181.6684585804 1133825.6030986719 +3529 4326 1037181.6684585804 1133825.6030986719 -88.145 39.78000000000001 +4759 3529 -89.1575 37.332 744620.8485560364 243286.42955415198 +3529 4326 744620.8485560364 243286.42955415198 -89.1575 37.332 +4759 3529 -87.1325 42.228 1309486.387956326 2027966.4951392366 +3529 4326 1309486.387956326 2027966.4951392366 -87.1325 42.227999999999994 +4759 3530 -90.22 39.739999999999995 695428.7036032164 341122.5008347094 +3530 4326 695428.7036032164 341122.5008347094 -90.22 39.739999999999995 +4759 3530 -91.39 37.247 591466.4497448911 65101.11165521788 +3530 4326 591466.4497448911 65101.11165521788 -91.39 37.247 +4759 3530 -89.05 42.233 792173.5648998992 618565.43078374 +3530 4326 792173.5648998992 618565.43078374 -89.05 42.233 +4759 3531 -90.22 39.739999999999995 2281585.671704886 1119166.0714885425 +3531 4326 2281585.671704886 1119166.0714885425 -90.22 39.739999999999995 +4759 3531 -91.39 37.247 1940502.8438380305 213585.89715549402 +3531 4326 1940502.8438380305 213585.89715549402 -91.39 37.247 +4759 3531 -89.05 42.233 2598989.437475753 2029410.0841629873 +3531 4326 2598989.437475753 2029410.0841629873 -89.05 42.233 +4759 3532 -85.685 39.86 98431.30571691465 511973.6556178053 +3532 4326 98431.30571691465 511973.6556178053 -85.685 39.85999999999999 +4759 3532 -86.4995 38.141000000000005 26992.295511845572 321471.9125532987 +3532 4326 26992.295511845572 321471.9125532987 -86.4995 38.141 +4759 3532 -84.8705 41.579 166394.06102200958 703165.6599238857 +3532 4326 166394.06102200958 703165.6599238857 -84.8705 41.579 +4759 3533 -85.685 39.86 322936.7085062442 1679700.2348060838 +3533 4326 322936.7085062442 1679700.2348060838 -85.685 39.85999999999999 +4759 3533 -86.4995 38.141000000000005 88557.22252511335 1054695.7661019482 +3533 4326 88557.22252511335 1054695.7661019482 -86.4995 38.141 +4759 3533 -84.8705 41.579 545911.1815363766 2306969.3356002825 +3533 4326 545911.1815363766 2306969.3356002825 -84.8705 41.579 +4759 3534 -87.16999999999999 39.77 892574.6756391372 501984.6231915935 +3534 4326 892574.6756391372 501984.6231915935 -87.16999999999999 39.77 +4759 3534 -88.00699999999999 37.970000000000006 818840.8874181742 302566.75752018945 +3534 4326 818840.8874181742 302566.75752018945 -88.00699999999999 37.97000000000001 +4759 3534 -86.333 41.57 962580.5798687736 702131.8622874243 +3534 4326 962580.5798687736 702131.8622874243 -86.333 41.57 +4759 3535 -87.16999999999999 39.77 2928388.7483260697 1646927.8842544204 +3535 4326 2928388.7483260697 1646927.8842544204 -87.16999999999999 39.77 +4759 3535 -88.00699999999999 37.970000000000006 2686480.4781377935 992671.1032974891 +3535 4326 2686480.4781377935 992671.1032974891 -88.00699999999999 37.97000000000001 +4759 3535 -86.333 41.57 3158066.4524528016 2303577.617854659 +3535 4326 3158066.4524528016 2303577.617854659 -86.333 41.57 +4759 3536 -93.4 42.68 1508195.834348411 1131075.1318394763 +3536 4326 1508195.834348411 1131075.1318394763 -93.4 42.680000000000014 +4759 3536 -96.325 41.933 1265738.527574641 1052010.8755992097 +3536 4326 1265738.527574641 1052010.8755992097 -96.325 41.933000000000014 +4759 3536 -90.47500000000001 43.427 1744902.3804289864 1218437.3308182764 +3536 4326 1744902.3804289864 1218437.3308182764 -90.47500000000001 43.42700000000001 +4759 3537 -93.4 42.68 4948139.1665247455 3710868.9950100156 +3537 4326 4948139.1665247455 3710868.9950100156 -93.4 42.680000000000014 +4759 3537 -96.325 41.933 4152677.152551135 3451472.3476617406 +3537 4326 4152677.152551135 3451472.3476617406 -96.325 41.933000000000014 +4759 3537 -90.47500000000001 43.427 5724733.8931241 3997489.8094929624 +3537 4326 5724733.8931241 3997489.8094929624 -90.47500000000001 43.42700000000001 +4759 3538 -93.14 41.2 530195.2801683228 133320.7160985994 +3538 4326 530195.2801683228 133320.7160985994 -93.14000000000001 41.20000000000001 +4759 3538 -95.84 40.444 301495.59603204846 51976.20620905145 +3538 4326 301495.59603204846 51976.20620905145 -95.84 40.444 +4759 3538 -90.44 41.955999999999996 753654.5170276335 221683.68954160615 +3538 4326 753654.5170276335 221683.68954160615 -90.44 41.95599999999999 +4759 3539 -93.14 41.2 1739482.3483855727 437403.0494001549 +3539 4326 1739482.3483855727 437403.0494001549 -93.14000000000001 41.20000000000001 +4759 3539 -95.84 40.444 989156.8013484788 170525.269870863 +3539 4326 989156.8013484788 170525.269870863 -95.84 40.444 +4759 3539 -90.44 41.955999999999996 2472614.8613148276 727307.2381044195 +3539 4326 2472614.8613148276 727307.2381044195 -90.44 41.95599999999999 +4759 3540 -98.32 39.265 372384.49819812784 103474.51018951085 +3540 4326 372384.49819812784 103474.51018951085 -98.32 39.26500000000003 +4759 3540 -101.686 38.594500000000004 78962.18575527694 35527.07937257582 +3540 4326 78962.18575527694 35527.07937257582 -101.686 38.5945 +4759 3540 -94.954 39.9355 660312.5113567541 182246.34859156059 +3540 4326 660312.5113567541 182246.34859156059 -94.954 39.93550000000001 +4759 3541 -98.32 39.265 1221731.4744716913 339482.6221800869 +3541 4326 1221731.4744716913 339482.6221800869 -98.32 39.26500000000003 +4759 3541 -101.686 38.594500000000004 259061.77106543793 116558.42624152584 +3541 4326 259061.77106543793 116558.42624152584 -101.686 38.5945 +4759 3541 -94.954 39.9355 2166375.297642951 597919.8953374784 +3541 4326 2166375.297642951 597919.8953374784 -94.954 39.93550000000001 +4759 3542 -98.32499999999999 37.935 415383.2434573826 540780.1749808954 +3542 4326 415383.2434573826 540780.1749808954 -98.32499999999999 37.935 +4759 3542 -101.6775 37.084500000000006 117521.86511747976 451187.8656569628 +3542 4326 117521.86511747976 451187.8656569628 -101.6775 37.08449999999999 +4759 3542 -94.9725 38.7855 706437.5117094776 640969.3042826953 +3542 4326 706437.5117094776 640969.3042826953 -94.9725 38.7855 +4759 3543 -98.32499999999999 37.935 1362803.191209763 1774209.624049821 +3543 4326 1362803.191209763 1774209.624049821 -98.32499999999999 37.935 +4759 3543 -101.6775 37.084500000000006 385569.65243959834 1480272.1892095525 +3543 4326 385569.65243959834 1480272.1892095525 -101.6775 37.08449999999999 +4759 3543 -94.9725 38.7855 2317703.7363001783 2102913.45910081 +3543 4326 2317703.7363001783 2102913.45910081 -94.9725 38.7855 +4759 3544 -84.215 38.43 503055.9840861851 103227.84047299267 +3544 4326 503055.9840861851 103227.84047299267 -84.215 38.43000000000001 +4759 3544 -85.7855 37.782000000000004 364736.787111394 32428.687076877144 +3544 4326 364736.787111394 32428.687076877144 -85.7855 37.78200000000001 +4759 3544 -82.6445 39.077999999999996 638921.4422613666 176371.6164450688 +3544 4326 638921.4422613666 176371.6164450688 -82.6445 39.07799999999999 +4759 3545 -84.215 38.43 1650442.841456092 338673.33995181014 +3545 4326 1650442.841456092 338673.33995181014 -84.215 38.43000000000001 +4759 3545 -85.7855 37.782000000000004 1196640.609381298 106393.11751805444 +3545 4326 1196640.609381298 106393.11751805444 -85.7855 37.78200000000001 +4759 3545 -82.6445 39.077999999999996 2096194.7654858336 578645.8782868632 +3545 4326 2096194.7654858336 578645.8782868632 -82.6445 39.07799999999999 +4759 3546 -85.75999999999999 37.82 1499119.6161673432 1164993.7957933834 +3546 4326 1499119.6161673432 1164993.7957933834 -85.75999999999999 37.81999999999999 +4759 3546 -89.189 36.623000000000005 1192410.1993640265 1037818.2979744445 +3546 4326 1192410.1993640265 1037818.2979744445 -89.189 36.623 +4759 3546 -82.331 39.016999999999996 1796069.135715349 1303285.8838335248 +3546 4326 1796069.135715349 1303285.8838335248 -82.331 39.017 +4759 3547 -85.75999999999999 37.82 4918361.607375693 3822150.478032126 +3547 4326 4918361.607375693 3822150.478032126 -85.75999999999999 37.81999999999999 +4759 3547 -89.189 36.623000000000005 3912099.129080144 3404908.865604491 +3547 4326 3912099.129080144 3404908.865604491 -89.189 36.623 +4759 3547 -82.331 39.016999999999996 5892603.489426109 4275863.770210491 +3547 4326 5892603.489426109 4275863.770210491 -82.331 39.017 +4759 3548 -85.75999999999999 37.33 499113.7931707417 610604.196224401 +3548 4326 499113.7931707417 610604.196224401 -85.75999999999999 37.33 +4759 3548 -89.189 36.574000000000005 192247.5353599636 532309.7194656162 +3548 4326 192247.5353599636 532309.7194656162 -89.189 36.57400000000001 +4759 3548 -82.331 38.086 799892.1099969771 699938.1634462873 +3548 4326 799892.1099969771 699938.1634462873 -82.331 38.086 +4759 3549 -85.75999999999999 37.33 1637509.1700943413 2003290.6007795555 +3549 4326 1637509.1700943413 2003290.6007795555 -85.75999999999999 37.33 +4759 3549 -89.189 36.574000000000005 630732.12259348 1746419.471613442 +3549 4326 630732.12259348 1746419.471613442 -89.189 36.57400000000001 +4759 3549 -82.331 38.086 2624312.697881749 2296380.458240027 +3549 4326 2624312.697881749 2296380.458240027 -82.331 38.086 +4759 3550 -92.455 31.94 1004254.595522245 159660.69532187734 +3550 4326 1004254.595522245 159660.69532187734 -92.455 31.93999999999999 +4759 3550 -93.8905 30.959000000000003 867141.0681850316 51745.95904705165 +3550 4326 867141.0681850316 51745.95904705165 -93.8905 30.959000000000017 +4759 3550 -91.0195 32.921 1138485.7486137254 269389.5708371389 +3550 4326 1138485.7486137254 269389.5708371389 -91.0195 32.921 +4759 3551 -92.455 31.94 3294791.9521092325 523820.13123519265 +3551 4326 3294791.9521092325 523820.13123519265 -92.455 31.93999999999999 +4759 3551 -93.8905 30.959000000000003 2844945.3211703915 169769.86730686863 +3551 4326 2844945.3211703915 169769.86730686863 -93.8905 30.959000000000017 +4759 3551 -91.0195 32.921 3735181.9935435313 883822.2836548467 +3551 4326 3735181.9935435313 883822.2836548467 -91.0195 32.921 +4759 3552 -91.345 29.96 998873.9589036357 161832.71462681066 +3552 4326 998873.9589036357 161832.71462681066 -91.345 29.96000000000001 +4759 3552 -93.6805 28.961000000000002 771204.4762175585 53445.368118892235 +3552 4326 771204.4762175585 53445.368118892235 -93.6805 28.961000000000002 +4759 3552 -89.0095 30.959 1222030.1736550392 274830.1880059256 +3552 4326 1222030.1736550392 274830.1880059256 -89.0095 30.959 +4759 3553 -91.345 29.96 3277138.980136345 530946.1645714614 +3553 4326 3277138.980136345 530946.1645714614 -91.345 29.96000000000001 +4759 3553 -93.6805 28.961000000000002 2530193.3523571067 175345.3452367323 +3553 4326 2530193.3523571067 175345.3452367323 -93.6805 28.961000000000002 +4759 3553 -89.0095 30.959 4009277.3280332414 901672.0418161077 +3553 4326 4009277.3280332414 901672.0418161077 -89.0095 30.959 +4759 3554 -69.18 45.61 495709.77078438434 234466.4915910657 +3554 4326 495709.77078438434 234466.4915910657 -69.18 45.61000000000001 +4759 3554 -69.94500000000001 43.936 434161.4947354724 48768.47616865049 +3554 4326 434161.4947354724 48768.47616865049 -69.94500000000001 43.936 +4759 3554 -68.41499999999999 47.284 553711.7737722797 420787.73281540995 +3554 4326 553711.7737722797 420787.73281540995 -68.41499999999999 47.284 +4759 3555 -67.745 45.775 710110.7661583305 215777.34469428813 +3555 4326 710110.7661583305 215777.34469428813 -67.745 45.775 +4759 3555 -68.4965 44.3395 650437.9411447884 56428.91664329373 +3555 4326 650437.9411447884 56428.91664329373 -68.4965 44.3395 +4759 3555 -66.9935 47.210499999999996 766778.0604996847 375714.4525110518 +3555 4326 766778.0604996847 375714.4525110518 -66.9935 47.210499999999996 +4759 3556 -70.345 44.825 302372.5469386635 221287.9296204905 +3556 4326 302372.5469386635 221287.9296204905 -70.345 44.82500000000001 +4759 3556 -71.0155 43.2455 247982.39365818878 45987.32700179099 +3556 4326 247982.39365818878 45987.32700179099 -71.0155 43.24550000000001 +4759 3556 -69.6745 46.4045 353865.1265871693 397074.0751520627 +3556 4326 353865.1265871693 397074.0751520627 -69.6745 46.40449999999999 +4759 3557 -68.47 45.675 302337.23350621155 223154.85963413733 +3557 4326 302337.23350621155 223154.85963413733 -68.47 45.675 +4759 3557 -69.874 44.0595 189917.5275568 44561.156556155605 +3557 4326 189917.5275568 44561.156556155605 -69.874 44.0595 +4759 3557 -67.066 47.2905 408460.01410141285 403714.07063095423 +3557 4326 408460.01410141285 403714.07063095423 -67.066 47.2905 +4759 3558 -70.17500000000001 44.81 899340.7970898099 219617.70745510195 +3558 4326 899340.7970898099 219617.70745510195 -70.17500000000001 44.81000000000001 +4759 3558 -70.9985 43.217 832412.7890298931 42957.32044205438 +3558 4326 832412.7890298931 42957.32044205438 -70.9985 43.217000000000006 +4759 3558 -69.3515 46.403 962683.305256669 396986.5141183069 +3558 4326 962683.305256669 396986.5141183069 -69.3515 46.403000000000006 +4759 3559 -77.22999999999999 38.849999999999994 380034.9481462034 131380.0755704414 +3559 4326 380034.9481462034 131380.0755704414 -77.22999999999999 38.84999999999999 +4759 3559 -79.264 38.058 201314.2794625202 45904.12107919067 +3559 4326 201314.2794625202 45904.12107919067 -79.264 38.05799999999997 +4759 3559 -75.196 39.641999999999996 554847.9829545154 220811.09831744133 +3559 4326 554847.9829545154 220811.09831744133 -75.196 39.641999999999996 +4269 3560 -111.54 41.28 1629422.6574287582 3625740.963687643 +3560 4326 1629422.6574287582 3625740.963687643 -111.54 41.28000000000001 +4269 3560 -113.79 40.623 1004775.0484199319 3394744.7652789727 +3560 4326 1004775.0484199319 3394744.7652789727 -113.79 40.62299999999999 +4269 3560 -109.29 41.937 2241682.370829438 3872783.259698183 +3560 4326 2241682.370829438 3872783.259698183 -109.29 41.937000000000005 +4135 3561 -155.42000000000002 19.6 527534.5872916179 278413.6973959471 +3561 4326 527534.5872916179 278413.6973959471 -155.41728773970925 19.596913735030736 +4135 3561 -156.03199999999998 18.943 316161.79983116704 40099.974539052295 +3561 4326 316161.79983116704 40099.974539052295 -156.02929228291822 18.93996093330673 +4135 3561 -154.80800000000002 20.256999999999998 737196.3004602343 517505.11775406654 +3561 4326 737196.3004602343 517505.11775406654 -154.80528311343966 20.25386691315264 +4135 3562 -156.645 20.855 507397.6901906655 189468.09036903922 +3562 4326 507397.6901906655 189468.09036903922 -156.6422535560914 20.851862871686077 +4135 3562 -157.2885 20.4905 287175.8886146761 57485.47968555212 +3562 4326 287175.8886146761 57485.47968555212 -157.28575413888987 20.487392807036134 +4135 3562 -156.00150000000002 21.2195 726561.1284262593 322335.69315752 +3562 4326 726561.1284262593 322335.69315752 -155.99875320670256 21.216332917540814 +4135 3563 -157.97000000000003 21.475 510200.6209698151 111998.86625043619 +3563 4326 510200.6209698151 111998.86625043619 -157.96725886190427 21.471836712258696 +4135 3563 -158.294 21.2275 399865.6364975074 22189.57238038111 +3563 4326 399865.6364975074 22189.57238038111 -158.29126064910974 21.22435483124437 +4135 3563 -157.64600000000002 21.7225 620163.4120026868 202039.3049310987 +3563 4326 620163.4120026868 202039.3049310987 -157.6432571053803 21.71931862407527 +4135 3564 -159.54 22.049999999999997 486453.23580621905 78708.93290292888 +3564 4326 486453.23580621905 78708.93290292888 -159.53721554252374 22.04686794938807 +4135 3564 -159.819 21.834 391800.913664138 354.197105748717 +3564 4326 391800.913664138 354.197105748717 -159.81621698696569 21.830883926533023 +4135 3564 -159.261 22.266 580818.6297450511 157238.10389873225 +3564 4326 580818.6297450511 157238.10389873225 -159.25821412029902 22.262851997647413 +4135 3565 -160.145 21.9 507345.6144007806 84761.27487332825 +3565 4326 507345.6144007806 84761.27487332825 -160.14220548246718 21.896866988970228 +4135 3565 -160.2845 21.747 460008.64678140666 29196.881275310865 +3565 4326 460008.64678140666 29196.881275310865 -160.28170753176104 21.74387684590053 +4135 3565 -160.0055 22.053 554581.6144740639 140369.73562164884 +3565 4326 554581.6144740639 140369.73562164884 -160.00270342549032 22.04985715581933 +4269 3566 -111.545 39.785 1627771.1555520452 7090414.623747053 +3566 4326 1627771.1555520452 7090414.623747053 -111.545 39.785000000000004 +4269 3566 -113.7995 38.6195 983389.8008171918 6674350.603919069 +3566 4326 983389.8008171918 6674350.603919069 -113.79950000000001 38.61950000000001 +4269 3566 -109.29050000000001 40.9505 2250760.8240166083 7522535.668688687 +3566 4326 2250760.8240166083 7522535.668688687 -109.29050000000001 40.950500000000005 +4269 3567 -111.545 37.785 1627412.1687934527 10249708.679039259 +3567 4326 1627412.1687934527 10249708.679039259 -111.545 37.785000000000004 +4269 3567 -113.7995 37.069500000000005 969548.2098906735 9997428.326999336 +3567 4326 969548.2098906735 9997428.326999336 -113.79950000000001 37.069500000000026 +4269 3567 -109.29050000000001 38.500499999999995 2272722.4864951163 10517733.274991473 +3567 4326 2272722.4864951163 10517733.274991473 -109.29050000000001 38.5005 +4152 3568 -111.54 41.28 1629422.6574287582 3625740.963687643 +3568 4326 1629422.6574287582 3625740.963687643 -111.54001165412147 41.280006162212494 +4152 3568 -113.79 40.623 1004775.0484199319 3394744.7652789727 +3568 4326 1004775.0484199319 3394744.7652789727 -113.79001201836172 40.623005913442896 +4152 3568 -109.29 41.937 2241682.370829438 3872783.259698183 +3568 4326 2241682.370829438 3872783.259698183 -109.2900112670251 41.9370064151121 +4152 3569 -111.545 39.785 1627771.1555520452 7090414.623747053 +3569 4326 1627771.1555520452 7090414.623747053 -111.5450114594936 39.78500598942574 +4152 3569 -113.7995 38.6195 983389.8008171918 6674350.603919069 +3569 4326 983389.8008171918 6674350.603919069 -113.79951176992728 38.61950569079333 +4152 3569 -109.29050000000001 40.9505 2250760.8240166083 7522535.668688687 +3569 4326 2250760.8240166083 7522535.668688687 -109.29051113244297 40.95050629731785 +4152 3570 -111.545 37.785 1627412.1687934527 10249708.679039259 +3570 4326 1627412.1687934527 10249708.679039259 -111.54501123019124 37.78500574623371 +4152 3570 -113.7995 37.069500000000005 969548.2098906735 9997428.326999336 +3570 4326 969548.2098906735 9997428.326999336 -113.79951160149827 37.069505509610316 +4152 3570 -109.29050000000001 38.500499999999995 2272722.4864951163 10517733.274991473 +3570 4326 2272722.4864951163 10517733.274991473 -109.29051083754662 38.50050598913477 +4326 3571 0.0 67.5 -3.056412248319017e-10 2495749.9994667936 +4326 3571 -162.0 47.25 1439194.4863566603 -4429385.177453288 +4326 3571 162.0 87.75 -77654.11685663325 -238994.79704354348 +4326 3572 0.0 67.5 1247874.9997333966 2161382.9010332427 +4326 3572 -162.0 47.25 -968313.6025552789 -4555557.329999121 +4326 3572 162.0 87.75 -186747.83642806145 -168148.5071836982 +4326 3573 0.0 67.5 2457833.9490551124 433382.439319651 +4326 3573 -162.0 47.25 -4112179.363969225 -2186484.552506138 +4326 3573 162.0 87.75 -248848.42493855 34973.36535523926 +4326 3574 0.0 67.5 1604237.1765324418 -1911855.4185057294 +4326 3574 -162.0 47.25 -3949640.8494371786 2468009.5178624145 +4326 3574 162.0 87.75 -94136.3896158033 232995.74036614894 +4326 3575 0.0 67.5 -433382.43931965105 -2457833.9490551124 +4326 3575 -162.0 47.25 -648175.2240067865 4612006.363697481 +4326 3575 162.0 87.75 117975.38731221762 221879.43317757157 +4326 3576 0.0 67.5 -2495749.9994667936 -1.5282061241595086e-10 +4326 3576 -162.0 47.25 4429385.177453288 1439194.4863566598 +4326 3576 162.0 87.75 238994.79704354348 -77654.11685663326 +4283 3577 133.26999999999998 -26.78 124755.68441287114 -2902763.199199531 +3577 4326 124755.68441287114 -2902763.199199531 133.26999999999998 -26.78 +4283 3577 114.892 -42.008 -1449883.0908134487 -4689803.131157475 +3577 4326 -1449883.0908134487 -4689803.131157475 114.892 -42.008 +4283 3577 151.648 -11.552 2180849.7695957995 -1383754.6914001105 +3577 4326 2180849.7695957995 -1383754.6914001105 151.648 -11.552000000000003 +4269 3578 -132.45999999999998 64.845 501895.0627250963 1151320.5322528991 +3578 4326 501895.0627250963 1151320.5322528991 -132.45999999999998 64.845 +4269 3578 -140.155 60.475500000000004 79522.60360319691 689457.3969845565 +3578 4326 79522.60360319691 689457.3969845565 -140.155 60.47550000000001 +4269 3578 -124.765 69.2145 806214.0664411895 1657585.1112246686 +3578 4326 806214.0664411895 1657585.1112246686 -124.765 69.21449999999997 +4617 3579 -132.45999999999998 64.845 501895.0627250963 1151320.5322528991 +3579 4326 501895.0627250963 1151320.5322528991 -132.46002489179546 64.84500448599368 +4617 3579 -140.155 60.475500000000004 79522.60360319691 689457.3969845565 +3579 4326 79522.60360319691 689457.3969845565 -140.15502227199545 60.47550334547232 +4617 3579 -124.765 69.2145 806214.0664411895 1657585.1112246686 +3579 4326 806214.0664411895 1657585.1112246686 -124.76502830489244 69.21450569130798 +4269 3580 -119.23 69.395 -283243.28468413843 9184579.914765095 +3580 4326 -283243.28468413843 9184579.914765095 -119.23000000000002 69.39499999999998 +4269 3580 -134.737 60.921499999999995 -1208095.6054981728 8446427.114589997 +3580 4326 -1208095.6054981728 8446427.114589997 -134.737 60.921499999999995 +4269 3580 -103.723 77.8685 198399.1893788343 10135339.60133175 +3580 4326 198399.1893788343 10135339.60133175 -103.723 77.8685 +4617 3581 -119.23 69.395 -283243.28468413843 9184579.914765095 +3581 4326 -283243.28468413843 9184579.914765095 -119.23002705194789 69.39500660436002 +4617 3581 -134.737 60.921499999999995 -1208095.6054981728 8446427.114589997 +3581 4326 -1208095.6054981728 8446427.114589997 -134.737022080289 60.92150420048216 +4617 3581 -103.723 77.8685 198399.1893788343 10135339.60133175 +3581 4326 198399.1893788343 10135339.60133175 -103.72303717174928 77.86850884610462 +4759 3582 -77.22999999999999 38.849999999999994 1246831.3253763365 431036.13126735657 +3582 4326 1246831.3253763365 431036.13126735657 -77.22999999999999 38.84999999999999 +4759 3582 -79.264 38.058 660478.5982032857 150603.77057397808 +3582 4326 660478.5982032857 150603.77057397808 -79.264 38.05799999999997 +4759 3582 -75.196 39.641999999999996 1820363.7570766064 724444.4117298055 +3582 4326 1820363.7570766064 724444.4117298055 -75.196 39.641999999999996 +4759 3583 -70.4 41.349999999999994 508368.84153597394 38875.137285071796 +3583 4326 508368.84153597394 38875.137285071796 -70.4 41.34999999999999 +4759 3583 -70.859 41.205999999999996 469889.6934071739 22940.12737198121 +3583 4326 469889.6934071739 22940.12737198121 -70.859 41.205999999999996 +4759 3583 -69.941 41.494 546678.3583144168 55013.767025989066 +3583 4326 546678.3583144168 55013.767025989066 -69.941 41.49399999999999 +4759 3584 -70.4 41.349999999999994 1667873.4412726075 127542.84624277307 +3584 4326 1667873.4412726075 127542.84624277307 -70.4 41.34999999999999 +4759 3584 -70.859 41.205999999999996 1541629.7694533693 75262.73455290835 +3584 4326 1541629.7694533693 75262.73455290835 -70.859 41.205999999999996 +4759 3584 -69.941 41.494 1793560.5809032158 180491.00065109914 +3584 4326 1793560.5809032158 180491.00065109914 -69.941 41.49399999999999 +4759 3585 -71.68 42.175 185128.3305030929 880522.4527347037 +3585 4326 185128.3305030929 880522.4527347037 -71.68 42.17500000000001 +4759 3585 -73.318 41.5315 48284.138797150605 810650.6882213238 +3585 4326 48284.138797150605 810650.6882213238 -73.318 41.531499999999994 +4759 3585 -70.042 42.8185 319232.9572599032 953006.5012715762 +3585 4326 319232.9572599032 953006.5012715762 -70.042 42.8185 +4759 3586 -71.68 42.175 607375.19799223 2888847.4136804407 +3586 4326 607375.19799223 2888847.4136804407 -71.68 42.17500000000001 +4759 3586 -73.318 41.5315 158412.21237031757 2659609.7996061267 +3586 4326 158412.21237031757 2659609.7996061267 -73.318 41.531499999999994 +4759 3586 -70.042 42.8185 1047350.1276101986 3126655.496255163 +3586 4326 1047350.1276101986 3126655.496255163 -70.042 42.8185 +4759 3587 -84.66499999999999 44.86 5976422.234671376 171538.31270695946 +3587 4326 5976422.234671376 171538.31270695946 -84.66499999999999 44.86000000000001 +4759 3587 -86.8205 43.906 5802892.177720298 68471.58702111564 +3587 4326 5802892.177720298 68471.58702111564 -86.8205 43.906 +4759 3587 -82.5095 45.814 6144335.022055124 279167.9333284333 +3587 4326 6144335.022055124 279167.9333284333 -82.5095 45.814 +4759 3588 -84.66499999999999 44.86 19607684.49687459 562789.7398522291 +3588 4326 19607684.49687459 562789.7398522291 -84.66499999999999 44.86000000000001 +4759 3588 -86.8205 43.906 19038360.163045596 224644.31437373898 +3588 4326 19038360.163045596 224644.31437373898 -86.8205 43.906 +4759 3588 -82.5095 45.814 20158579.468606047 915905.2930722877 +3588 4326 20158579.468606047 915905.2930722877 -82.5095 45.814 +4759 3589 -86.93 46.7 8005353.265366577 213031.3436327338 +3589 4326 8005353.265366577 213031.3436327338 -86.93 46.7 +4759 3589 -90.071 45.242 7758926.973017988 55650.446219124075 +3589 4326 7758926.973017988 55650.446219124075 -90.07100000000001 45.242 +4759 3589 -83.789 48.158 8238929.602731237 379989.2076339546 +3589 4326 8238929.602731237 379989.2076339546 -83.789 48.157999999999994 +4759 3590 -86.93 46.7 26264282.366583254 698921.7310785229 +3590 4326 26264282.366583254 698921.7310785229 -86.93 46.7 +4759 3590 -90.071 45.242 25455797.155465834 182580.20413098446 +3590 4326 25455797.155465834 182580.20413098446 -90.07100000000001 45.242 +4759 3590 -83.789 48.158 27030608.932740275 1246683.7520799034 +3590 4326 27030608.932740275 1246683.7520799034 -83.78900000000002 48.157999999999994 +4759 3591 -86.275 45.004999999999995 478167.87845010986 494847.00078408886 +3591 4326 478167.87845010986 494847.00078408886 -86.275 45.004999999999995 +4759 3591 -90.0055 42.021499999999996 168036.26552634127 171146.5263229655 +3591 4326 168036.26552634127 171146.5263229655 -90.0055 42.02149999999998 +4759 3591 -82.5445 47.9885 757726.9104425479 832165.3397596804 +3591 4326 757726.9104425479 832165.3397596804 -82.5445 47.988499999999995 +4759 3592 -84.66499999999999 42.955 3975658.1660904195 161660.9908124227 +3592 4326 3975658.1660904195 161660.9908124227 -84.66499999999999 42.95499999999999 +4759 3592 -86.9465 41.8165 3785662.443796816 38441.45475135792 +3592 4326 3785662.443796816 38441.45475135792 -86.9465 41.816500000000005 +4759 3592 -82.3835 44.0935 4158818.2040767726 289977.3408485526 +3592 4326 4158818.2040767726 289977.3408485526 -82.3835 44.0935 +4759 3593 -84.66499999999999 42.955 13043497.920191662 530383.8281247463 +3593 4326 13043497.920191662 530383.8281247463 -84.66499999999999 42.95499999999999 +4759 3593 -86.9465 41.8165 12420152.374608975 126120.25837059683 +3593 4326 12420152.374608975 126120.25837059683 -86.9465 41.816500000000005 +4759 3593 -82.3835 44.0935 13644416.679989409 951369.2285057498 +3593 4326 13644416.679989409 951369.2285057498 -82.38350000000001 44.0935 +4759 3594 -94.575 46.379999999999995 774999.3390451781 253433.09136305802 +3594 4326 774999.3390451781 253433.09136305802 -94.575 46.37999999999999 +4759 3594 -96.6315 45.39 613521.9818043811 146151.76135058573 +3594 4326 613521.9818043811 146151.76135058573 -96.6315 45.39 +4759 3594 -92.5185 47.37 930779.8796238166 364865.75352756435 +3594 4326 930779.8796238166 364865.75352756435 -92.5185 47.37 +4759 3595 -93.35499999999999 48.010000000000005 780976.0080242507 267903.8812841848 +3595 4326 780976.0080242507 267903.8812841848 -93.35499999999999 48.01000000000001 +4759 3595 -96.8335 46.777 514956.0367357529 137681.0978387199 +3595 4326 514956.0367357529 137681.0978387199 -96.8335 46.77700000000001 +4759 3595 -89.8765 49.243 1034700.1274336235 409882.0127727458 +3595 4326 1034700.1274336235 409882.0127727458 -89.8765 49.243 +4759 3596 -94.03 44.540000000000006 797615.9303731995 271111.9496617395 +3596 4326 797615.9303731995 271111.9496617395 -94.03 44.54000000000001 +4759 3596 -96.568 43.595 592659.1578750131 169370.35030780142 +3596 4326 592659.1578750131 169370.35030780142 -96.568 43.59499999999999 +4759 3596 -91.49199999999999 45.48500000000001 996055.3728436409 379136.3411765582 +3596 4326 996055.3728436409 379136.3411765582 -91.49199999999999 45.48500000000001 +4759 3597 -89.03 32.51 281520.8578694894 333718.072514426 +3597 4326 281520.8578694894 333718.072514426 -89.03000000000002 32.51 +4759 3597 -89.876 30.26 199662.52524006984 84702.21482670316 +3597 4326 199662.52524006984 84702.21482670316 -89.876 30.26000000000001 +4759 3597 -88.184 34.76 359446.4830207231 583441.3092225477 +3597 4326 359446.4830207231 583441.3092225477 -88.184 34.76 +4759 3598 -89.03 32.51 923623.0145268167 1094873.3762410795 +3598 4326 923623.0145268167 1094873.3762410795 -89.03000000000002 32.51 +4759 3598 -89.876 30.26 655059.4682251292 277893.84981060866 +3598 4326 655059.4682251292 277893.84981060866 -89.876 30.26000000000001 +4759 3598 -88.184 34.76 1179284.0030438225 1914173.6953409754 +3598 4326 1179284.0030438225 1914173.6953409754 -88.184 34.76 +4759 3599 -90.51 33.004999999999995 683491.678094493 388607.7223874255 +3599 4326 683491.678094493 388607.7223874255 -90.51 33.004999999999995 +4759 3599 -91.536 31.200499999999998 585383.0308493291 189128.51300998154 +3599 4326 585383.0308493291 189128.51300998154 -91.536 31.20049999999999 +4759 3599 -89.48400000000001 34.8095 777710.4632391258 589069.1501028265 +3599 4326 777710.4632391258 589069.1501028265 -89.48400000000001 34.8095 +4759 3600 -90.51 33.004999999999995 2242422.280215017 1274957.169199412 +3600 4326 2242422.280215017 1274957.169199412 -90.51 33.004999999999995 +4759 3600 -91.536 31.200499999999998 1920544.1600448415 620499.1297669145 +3600 4326 1920544.1600448415 620499.1297669145 -91.536 31.20049999999999 +4759 3600 -89.48400000000001 34.8095 2551538.4111436997 1932637.7032956902 +3600 4326 2551538.4111436997 1932637.7032956902 -89.48400000000001 34.8095 +4759 3601 -92.6 38.545 491282.7335977044 300928.83009349275 +3601 4326 491282.7335977044 300928.83009349275 -92.6 38.545 +4759 3601 -93.671 36.686499999999995 395345.8443743689 95303.31149016054 +3601 4326 395345.8443743689 95303.31149016054 -93.671 36.68649999999999 +4759 3601 -91.529 40.4035 582422.4658619608 507702.20165428886 +3601 4326 582422.4658619608 507702.20165428886 -91.529 40.4035 +4759 3602 -90.535 38.295 246938.423324011 273175.40948622045 +3602 4326 246938.423324011 273175.40948622045 -90.53500000000001 38.295 +4759 3602 -91.8265 36.211499999999994 130722.74568677916 42774.13741293872 +3602 4326 130722.74568677916 42774.13741293872 -91.82650000000001 36.211499999999994 +4759 3602 -89.2435 40.3785 356696.8903372759 505231.6112297185 +3602 4326 356696.8903372759 505231.6112297185 -89.2435 40.3785 +4759 3603 -94.625 38.535 839101.8219779618 262839.6964433915 +3603 4326 839101.8219779618 262839.6964433915 -94.625 38.535 +4759 3603 -95.65549999999999 36.6855 746729.0116193696 58192.123500483096 +3603 4326 746729.0116193696 58192.123500483096 -95.6555 36.6855 +4759 3603 -93.59450000000001 40.3845 926884.6909838021 468553.285440557 +3603 4326 926884.6909838021 468553.285440557 -93.59450000000001 40.384499999999996 +4759 3604 -110.055 46.68 557562.9972258529 270175.48989245493 +3604 4326 557562.9972258529 270175.48989245493 -110.055 46.68000000000001 +4759 3604 -115.46849999999999 44.583 126329.73944052018 55073.69494508364 +3604 4326 126329.73944052018 55073.69494508364 -115.46849999999999 44.583000000000006 +4759 3604 -104.64150000000001 48.777 956814.4198691815 514139.9142825689 +3604 4326 956814.4198691815 514139.9142825689 -104.64150000000001 48.77700000000001 +4759 3605 -110.055 46.68 1829274.9252737954 886402.5258938809 +3605 4326 1829274.9252737954 886402.5258938809 -110.055 46.68000000000001 +4759 3605 -115.46849999999999 44.583 414467.64907519735 180687.97554161298 +3605 4326 414467.64907519735 180687.97554161298 -115.46849999999999 44.583000000000006 +4759 3605 -104.64150000000001 48.777 3139154.9208227736 1686810.7423968795 +3605 4326 3139154.9208227736 1686810.7423968795 -104.64150000000001 48.77700000000001 +4759 3606 -99.68 41.5 526709.5912096216 185091.48420576085 +3606 4326 526709.5912096216 185091.48420576085 -99.68 41.5 +4759 3606 -103.622 40.141000000000005 191449.57910202077 40625.804804552994 +3606 4326 191449.57910202077 40625.804804552994 -103.622 40.14100000000001 +4759 3606 -95.738 42.858999999999995 848158.6906421972 344540.8856728787 +3606 4326 848158.6906421972 344540.8856728787 -95.738 42.858999999999995 +4759 3607 -116.59 38.5 506687.1770771124 6416101.467797132 +3607 4326 506687.1770771124 6416101.467797132 -116.59 38.50000000000001 +4759 3607 -118.03 36.25 377474.7274872544 6167270.088553119 +3607 4326 377474.7274872544 6167270.088553119 -118.03000000000002 36.25 +4759 3607 -115.14999999999999 40.75 628075.3950645399 6666992.146123811 +3607 4326 628075.3950645399 6666992.146123811 -115.14999999999999 40.75 +4759 3608 -116.59 38.5 1662356.1801604931 21050159.56559776 +3608 4326 1662356.1801604931 21050159.56559776 -116.59 38.50000000000002 +4759 3608 -118.03 36.25 1238431.6684644339 20233785.282194696 +3608 4326 1238431.6684644339 20233785.282194696 -118.03000000000002 36.250000000000014 +4759 3608 -115.14999999999999 40.75 2060610.6920075782 21873290.06607454 +3608 4326 2060610.6920075782 21873290.06607454 -115.14999999999999 40.75000000000001 +4759 3609 -115.52000000000001 38.495000000000005 205524.57150390767 8415545.609160446 +3609 4326 205524.57150390767 8415545.609160446 -115.52000000000001 38.495000000000005 +4759 3609 -116.861 35.3405 83857.77529178846 8066253.469782333 +3609 4326 83857.77529178846 8066253.469782333 -116.86100000000002 35.3405 +4759 3609 -114.179 41.6495 316976.07839282975 8766724.77413967 +3609 4326 316976.07839282975 8766724.77413967 -114.179 41.64950000000001 +4759 3610 -115.52000000000001 38.495000000000005 674291.8650424038 27610002.552753896 +3610 4326 674291.8650424038 27610002.552753896 -115.52000000000001 38.495 +4759 3610 -116.861 35.3405 275123.3844698093 26464033.258810874 +3610 4326 275123.3844698093 26464033.258810874 -116.86100000000002 35.340500000000006 +4759 3610 -114.179 41.6495 1039945.6838938089 28762162.86318991 +3610 4326 1039945.6838938089 28762162.86318991 -114.179 41.64950000000001 +4759 3611 -118.495 39.475 807599.8137248502 4524331.450272928 +3611 4326 807599.8137248502 4524331.450272928 -118.495 39.475 +4759 3611 -119.8495 37.2025 687604.7269785614 4272849.753804591 +3611 4326 687604.7269785614 4272849.753804591 -119.8495 37.20250000000001 +4759 3611 -117.14049999999999 41.7475 920000.7132682288 4777661.662819959 +3611 4326 920000.7132682288 4777661.662819959 -117.14049999999999 41.747499999999995 +4759 3612 -118.495 39.475 2649600.388895613 14843577.433070434 +3612 4326 2649600.388895613 14843577.433070434 -118.495 39.475 +4759 3612 -119.8495 37.2025 2255916.5084621636 14018507.900573896 +3612 4326 2255916.5084621636 14018507.900573896 -119.8495 37.20250000000001 +4759 3612 -117.14049999999999 41.7475 3018369.006814181 15674711.638735153 +3612 4326 3018369.006814181 15674711.638735153 -117.14049999999999 41.74750000000001 +4759 3613 -71.595 44.0 305747.9189590065 166643.36188109402 +3613 4326 305747.9189590065 166643.36188109402 -71.595 43.99999999999999 +4759 3613 -72.4635 42.821 234839.1555409388 35965.449943250715 +3613 4326 234839.1555409388 35965.449943250715 -72.4635 42.821 +4759 3613 -70.7265 45.179 373895.511671885 298081.46545753366 +3613 4326 373895.511671885 298081.46545753366 -70.7265 45.17900000000001 +4759 3614 -71.595 44.0 1003107.9641180072 546729.0964382227 +3614 4326 1003107.9641180072 546729.0964382227 -71.595 43.99999999999999 +4759 3614 -72.4635 42.821 770468.1294705635 117996.6470221484 +3614 4326 770468.1294705635 117996.6470221484 -72.4635 42.821 +4759 3614 -70.7265 45.179 1226688.8578768428 977955.6079219251 +3614 4326 1226688.8578768428 977955.6079219251 -70.7265 45.17900000000001 +4759 3615 -74.74 40.114999999999995 129541.93200635779 142309.84863179177 +3615 4326 129541.93200635779 142309.84863179177 -74.74 40.115 +4759 3615 -75.514 38.994499999999995 62161.845770000305 18379.059719253342 +3615 4326 62161.845770000305 18379.059719253342 -75.514 38.99449999999999 +4759 3615 -73.966 41.2355 194763.50353423748 266836.18215498014 +3615 4326 194763.50353423748 266836.18215498014 -73.966 41.23550000000001 +4759 3616 -74.74 40.114999999999995 425005.4885908589 466894.89505280356 +3616 4326 425005.4885908589 466894.89505280356 -74.74 40.115 +4759 3616 -75.514 38.994499999999995 203942.65566374268 60298.63176225035 +3616 4326 203942.65566374268 60298.63176225035 -75.514 38.99449999999999 +4759 3616 -73.966 41.2355 638986.5945119108 875445.0409534641 +3616 4326 638986.5945119108 875445.0409534641 -73.966 41.23550000000001 +4759 3617 -106.285 34.39 496781.7063517648 375911.1857639483 +3617 4326 496781.7063517648 375911.1857639483 -106.28500000000001 34.39000000000001 +4759 3617 -107.58550000000001 32.041000000000004 373868.1798690023 116192.89978335424 +3617 4326 373868.1798690023 116192.89978335424 -107.58550000000002 32.041 +4759 3617 -104.9845 36.739 613019.4483625791 637255.137029333 +3617 4326 613019.4483625791 637255.137029333 -104.9845 36.739000000000004 +4759 3618 -106.285 34.39 1629857.9819224146 1233301.9486272205 +3618 4326 1629857.9819224146 1233301.9486272205 -106.28500000000001 34.39000000000001 +4759 3618 -107.58550000000001 32.041000000000004 1226599.187120218 381209.5387058881 +3618 4326 1226599.187120218 381209.5387058881 -107.58550000000002 32.041 +4759 3618 -104.9845 36.739 2011214.6405028943 2090727.895403737 +3618 4326 2011214.6405028943 2090727.895403737 -104.9845 36.739000000000004 +4759 3619 -104.35499999999999 34.5 163010.31461583532 388115.5015019724 +3619 4326 163010.31461583532 388115.5015019724 -104.355 34.5 +4759 3619 -105.5835 32.25 47196.13683957874 139273.63539034483 +3619 4326 47196.13683957874 139273.63539034483 -105.5835 32.24999999999999 +4759 3619 -103.1265 36.75 272765.40574489284 638413.9669679842 +3619 4326 272765.40574489284 638413.9669679842 -103.1265 36.75 +4759 3620 -104.35499999999999 34.5 534809.6738687864 1273342.2745110546 +3620 4326 534809.6738687864 1273342.2745110546 -104.355 34.5 +4759 3620 -105.5835 32.25 154842.65894785128 456933.58544315636 +3620 4326 154842.65894785128 456933.58544315636 -105.5835 32.24999999999999 +4759 3620 -103.1265 36.75 894897.835348036 2094529.8232941285 +3620 4326 894897.835348036 2094529.8232941285 -103.1265 36.75 +4759 3621 -107.69 34.165 843215.0652553578 350969.5921065168 +3621 4326 843215.0652553578 350969.5921065168 -107.69 34.165 +4759 3621 -108.923 31.6135 726610.1809091113 68531.38595231783 +3621 4326 726610.1809091113 68531.38595231783 -108.923 31.6135 +4759 3621 -106.457 36.716499999999996 952956.2155361695 634905.4140072872 +3621 4326 952956.2155361695 634905.4140072872 -106.45700000000001 36.7165 +4759 3622 -107.69 34.165 2766448.093591952 1151472.736769464 +3622 4326 2766448.093591952 1151472.736769464 -107.69 34.165 +4759 3622 -108.923 31.6135 2383886.902199309 224840.0554118961 +3622 4326 2383886.902199309 224840.0554118961 -108.923 31.6135 +4759 3622 -106.457 36.716499999999996 3126490.517471582 2083018.8457889084 +3622 4326 3126490.517471582 2083018.8457889084 -106.45700000000001 36.7165 +4759 3623 -76.405 43.2 264493.31539123424 355403.0177038457 +3623 4326 264493.31539123424 355403.0177038457 -76.405 43.2 +4759 3623 -77.6155 42.111000000000004 164637.84596996327 234938.07240019878 +3623 4326 164637.84596996327 234938.07240019878 -77.6155 42.111000000000004 +4759 3623 -75.1945 44.288999999999994 360844.2676941891 477313.98916154087 +3623 4326 360844.2676941891 477313.98916154087 -75.19450000000002 44.288999999999994 +4759 3624 -76.405 43.2 867758.4852460751 1166018.0672500338 +3624 4326 867758.4852460751 1166018.0672500338 -76.405 43.2 +4759 3624 -77.6155 42.111000000000004 540149.3326531219 770792.6591996523 +3624 4326 540149.3326531219 770792.6591996523 -77.6155 42.111000000000004 +4759 3624 -75.1945 44.288999999999994 1183869.9012600197 1565987.6461074888 +3624 4326 1183869.9012600197 1565987.6461074888 -75.19450000000002 44.288999999999994 +4759 3625 -74.55000000000001 42.95 145920.05529655414 457119.0986501149 +3625 4326 145920.05529655414 457119.0986501149 -74.55000000000001 42.95000000000001 +4759 3625 -75.738 41.087 45987.00461632412 250946.93264947843 +3625 4326 45987.00461632412 250946.93264947843 -75.738 41.087 +4759 3625 -73.36200000000001 44.813 240010.12987699886 664725.038343087 +3625 4326 240010.12987699886 664725.038343087 -73.36200000000001 44.813 +4759 3626 -74.55000000000001 42.95 478739.3814187781 1499731.5761545855 +3626 4326 478739.3814187781 1499731.5761545855 -74.55000000000001 42.95000000000001 +4759 3626 -75.738 41.087 150875.69764539006 823315.0615341639 +3626 4326 150875.69764539006 823315.0615341639 -75.738 41.087 +4759 3626 -73.36200000000001 44.813 787433.2344381205 2180852.0632972782 +3626 4326 787433.2344381205 2180852.0632972782 -73.36200000000001 44.813 +4759 3627 -73.03 40.885 381750.7399232304 80221.1659232867 +3627 4326 381750.7399232304 80221.1659232867 -73.03 40.885000000000005 +4759 3627 -74.137 40.5115 288388.66202628706 38301.14700668562 +3627 4326 288388.66202628706 38301.14700668562 -74.137 40.511500000000005 +4759 3627 -71.923 41.2585 474051.4783082586 123311.4021303021 +3627 4326 474051.4783082586 123311.4021303021 -71.923 41.2585 +4759 3628 -73.03 40.885 1252460.5525647984 263192.27519998315 +3628 4326 1252460.5525647984 263192.27519998315 -73.03 40.885000000000005 +4759 3628 -74.137 40.5115 946155.1353312435 125659.67980443442 +3628 4326 946155.1353312435 125659.67980443442 -74.137 40.511500000000005 +4759 3628 -71.923 41.2585 1555283.8917496786 404564.1584891662 +3628 4326 1555283.8917496786 404564.1584891662 -71.923 41.2585 +4759 3629 -78.565 42.815 351499.30115682975 312619.6539237273 +3629 4326 351499.30115682975 312619.6539237273 -78.565 42.81499999999999 +4759 3629 -79.64949999999999 42.072500000000005 261772.6033852207 230696.4045349582 +3629 4326 261772.6033852207 230696.4045349582 -79.64949999999999 42.0725 +4759 3629 -77.4805 43.5575 439103.5261805072 395694.3379393331 +3629 4326 439103.5261805072 395694.3379393331 -77.4805 43.557500000000005 +4759 3630 -78.565 42.815 1153210.6242120317 1025652.9812480954 +3630 4326 1153210.6242120317 1025652.9812480954 -78.565 42.81499999999999 +4759 3630 -79.64949999999999 42.072500000000005 858832.283273011 756876.4538784421 +3630 4326 858832.283273011 756876.4538784421 -79.64949999999999 42.0725 +4759 3630 -77.4805 43.5575 1440625.485810547 1298207.173722629 +3630 4326 1440625.485810547 1298207.173722629 -77.4805 43.557500000000005 +4759 3631 -79.85499999999999 35.21 531760.6294742532 162292.8993226596 +3631 4326 531760.6294742532 162292.8993226596 -79.85499999999999 35.21000000000001 +4759 3631 -83.8825 33.967999999999996 158490.68215049576 35280.47102519372 +3631 4326 158490.68215049576 35280.47102519372 -83.8825 33.96799999999999 +4759 3631 -75.8275 36.452000000000005 893982.110337277 304301.6117079829 +3631 4326 893982.110337277 304301.6117079829 -75.8275 36.45200000000001 +4759 3632 -79.85499999999999 35.21 1744617.9959167782 532455.9538610925 +3632 4326 1744617.9959167782 532455.9538610925 -79.85499999999999 35.21000000000001 +4759 3632 -83.8825 33.967999999999996 519981.51040541707 115749.34535515642 +3632 4326 519981.51040541707 115749.34535515642 -83.8825 33.96799999999999 +4759 3632 -75.8275 36.452000000000005 2933006.304381549 998362.8710786074 +3632 4326 2933006.304381549 998362.8710786074 -75.8275 36.45200000000001 +4759 3633 -100.44999999999999 48.08 603725.2560461811 120076.52492659682 +3633 4326 603725.2560461811 120076.52492659682 -100.44999999999999 48.080000000000005 +4759 3633 -103.708 47.242999999999995 357180.63227263116 32076.32260325921 +3633 4326 357180.63227263116 32076.32260325921 -103.708 47.242999999999995 +4759 3633 -97.192 48.917 842389.7430150689 218354.69478269183 +3633 4326 842389.7430150689 218354.69478269183 -97.192 48.91700000000001 +4759 3634 -100.44999999999999 48.08 1980725.9056554495 393951.8534337165 +3634 4326 1980725.9056554495 393951.8534337165 -100.44999999999999 48.080000000000005 +4759 3634 -103.708 47.242999999999995 1171852.468078186 105237.278882084 +3634 4326 1171852.468078186 105237.278882084 -103.708 47.242999999999995 +4759 3634 -97.192 48.917 2763745.8760258164 716386.7939064692 +3634 4326 2763745.8760258164 716386.7939064692 -97.192 48.91700000000001 +4759 3635 -100.3 46.879999999999995 615244.233397394 134889.74595170486 +3635 4326 615244.233397394 134889.74595170486 -100.3 46.88000000000001 +4759 3635 -103.675 46.025 354223.17057553853 44799.12802504772 +3635 4326 354223.17057553853 44799.12802504772 -103.675 46.025000000000006 +4759 3635 -96.925 47.735 868072.4690473499 236025.6304733758 +3635 4326 868072.4690473499 236025.6304733758 -96.925 47.73499999999999 +4759 3636 -100.3 46.879999999999995 2018517.8260990612 442551.6599465382 +3636 4326 2018517.8260990612 442551.6599465382 -100.3 46.88000000000001 +4759 3636 -103.675 46.025 1162149.5097543914 146978.76648637702 +3636 4326 1162149.5097543914 146978.76648637702 -103.675 46.025000000000006 +4759 3636 -96.925 47.735 2848006.7882052157 774362.3047026764 +3636 4326 2848006.7882052157 774362.3047026764 -96.925 47.73499999999999 +4759 3637 -82.66 41.215 586582.9888405734 171948.99910634552 +3637 4326 586582.9888405734 171948.99910634552 -82.66 41.21500000000002 +4759 3637 -84.595 40.2115 421661.2948510517 62644.95346345006 +3637 4326 421661.2948510517 62644.95346345006 -84.595 40.211499999999994 +4759 3637 -80.72500000000001 42.2185 746566.5265375796 284886.53487657424 +3637 4326 746566.5265375796 284886.53487657424 -80.72500000000001 42.218499999999985 +4759 3638 -82.765 39.379999999999995 577168.794808187 153231.72928756103 +3638 4326 577168.794808187 153231.72928756103 -82.765 39.379999999999995 +4759 3638 -84.62349999999999 38.498 414763.0625386159 57464.14699698818 +3638 4326 414763.0625386159 57464.14699698818 -84.62349999999999 38.498 +4759 3638 -80.90650000000001 40.262 735553.8037072645 252321.37590929342 +3638 4326 735553.8037072645 252321.37590929342 -80.90650000000001 40.261999999999986 +4759 3639 -98.71000000000001 36.14 536101.1714221651 126719.8429846217 +3639 4326 536101.1714221651 126719.8429846217 -98.71000000000001 36.14000000000001 +4759 3639 -102.571 35.357 184677.13744587317 49389.48967290248 +3639 4326 184677.13744587317 49389.48967290248 -102.571 35.357000000000006 +4759 3639 -94.849 36.923 880718.1162748237 217928.31690933244 +3639 4326 880718.1162748237 217928.31690933244 -94.849 36.92300000000001 +4759 3640 -98.71000000000001 36.14 1758858.593240887 415746.68485871307 +3640 4326 1758858.593240887 415746.68485871307 -98.71000000000001 36.14000000000001 +4759 3640 -102.571 35.357 605894.9084370023 162038.6840351809 +3640 4326 605894.9084370023 162038.6840351809 -102.571 35.357000000000006 +4759 3640 -94.849 36.923 2889489.353144984 714986.4863933682 +3640 4326 2889489.353144984 714986.4863933682 -94.849 36.92300000000001 +4759 3641 -97.21000000000001 34.595 672465.8025262959 140231.8207822339 +3641 4326 672465.8025262959 140231.8207822339 -97.21000000000001 34.595 +4759 3641 -99.721 33.7175 440481.2346152521 43973.85878489801 +3641 4326 440481.2346152521 43973.85878489801 -99.721 33.717499999999994 +4759 3641 -94.699 35.4725 899563.148845982 242195.49149143902 +3641 4326 899563.148845982 242195.49149143902 -94.699 35.4725 +4759 3642 -97.21000000000001 34.595 2206248.2204550225 460077.23201637913 +3642 4326 2206248.2204550225 460077.23201637913 -97.21000000000001 34.595 +4759 3642 -99.721 33.7175 1445145.5172335398 144270.90169678623 +3642 4326 1445145.5172335398 144270.90169678623 -99.721 33.717499999999994 +4759 3642 -94.699 35.4725 2951316.764172193 794603.0416681629 +3642 4326 2951316.764172193 794603.0416681629 -94.699 35.4725 +4759 3643 -120.535 44.12 397199.10422283644 263312.33359227417 +3643 4326 397199.10422283644 263312.33359227417 -120.53500000000001 44.11999999999999 +4759 3643 -124.1935 42.193999999999996 94902.84668831312 56206.91245575882 +3643 4326 94902.84668831312 56206.91245575882 -124.19350000000001 42.19400000000001 +4759 3643 -116.8765 46.046 680436.0888007582 483525.7810091159 +3643 4326 680436.0888007582 483525.7810091159 -116.87649999999998 46.04600000000001 +4759 3644 -120.535 44.12 1303146.667392508 863885.6088985372 +3644 4326 1303146.667392508 863885.6088985372 -120.53500000000001 44.11999999999999 +4759 3644 -124.1935 42.193999999999996 311361.04556008236 184405.88076036357 +3644 4326 311361.04556008236 184405.88076036357 -124.19350000000001 42.19400000000001 +4759 3644 -116.8765 46.046 2232401.8661389705 1586370.672602086 +3644 4326 2232401.8661389705 1586370.672602086 -116.87649999999998 46.04600000000001 +4759 3645 -120.32 45.105000000000004 2514164.9883181253 159845.1412173828 +3645 4326 2514164.9883181253 159845.1412173828 -120.32 45.105000000000004 +4759 3645 -123.785 44.0655 2236864.6381944637 49671.3154349094 +3645 4326 2236864.6381944637 49671.3154349094 -123.78500000000001 44.06550000000001 +4759 3645 -116.855 46.1445 2781533.537177242 281708.5817855314 +3645 4326 2781533.537177242 281708.5817855314 -116.855 46.1445 +4759 3646 -120.32 45.105000000000004 8248572.7967865 524426.316329996 +3646 4326 8248572.7967865 524426.316329996 -120.32 45.105000000000004 +4759 3646 -123.785 44.0655 7338794.745199684 162963.63331663187 +3646 4326 7338794.745199684 162963.63331663187 -123.78500000000001 44.06550000000001 +4759 3646 -116.855 46.1445 9125766.198555255 924240.7538895387 +3646 4326 9125766.198555255 924240.7538895387 -116.855 46.1445 +4759 3647 -120.75 43.269999999999996 1479706.3814610983 178132.5497103679 +3647 4326 1479706.3814610983 178132.5497103679 -120.75 43.269999999999996 +4759 3647 -124.21499999999999 42.108999999999995 1192816.3476943534 55951.90213787231 +3647 4326 1192816.3476943534 55951.90213787231 -124.21499999999999 42.10900000000001 +4759 3647 -117.28500000000001 44.431000000000004 1755961.4123870605 312011.62286356714 +3647 4326 1755961.4123870605 312011.62286356714 -117.28500000000001 44.431000000000004 +4759 3648 -120.75 43.269999999999996 4854679.729683393 584424.3756901834 +3648 4326 4854679.729683393 584424.3756901834 -120.75 43.269999999999996 +4759 3648 -124.21499999999999 42.108999999999995 3913439.4614197942 183569.23273580152 +3648 4326 3913439.4614197942 183569.23273580152 -124.21499999999999 42.10900000000001 +4759 3648 -117.28500000000001 44.431000000000004 5761028.256343373 1023660.1799985798 +3648 4326 5761028.256343373 1023660.1799985798 -117.28500000000001 44.431000000000004 +4759 3649 -77.61500000000001 41.565 611260.251203743 155299.796259382 +3649 4326 611260.251203743 155299.796259382 -77.61500000000001 41.56500000000001 +4759 3649 -80.2385 40.6965 389693.64377760055 61862.05383360608 +3649 4326 389693.64377760055 61862.05383360608 -80.2385 40.6965 +4759 3649 -74.9915 42.4335 826973.6695968212 255374.7277154768 +3649 4326 826973.6695968212 255374.7277154768 -74.9915 42.43350000000001 +4759 3650 -77.61500000000001 41.565 2005443.0074909471 509512.7482276559 +3650 4326 2005443.0074909471 509512.7482276559 -77.61500000000001 41.56500000000001 +4759 3650 -80.2385 40.6965 1278519.8962936779 202959.08828575598 +3650 4326 1278519.8962936779 202959.08828575598 -80.2385 40.6965 +4759 3650 -74.9915 42.4335 2713162.781002238 837841.9191798603 +3650 4326 2713162.781002238 837841.9191798603 -74.9915 42.43350000000001 +4759 3651 -77.625 40.445 610604.1860345764 123441.44619973708 +3651 4326 610604.1860345764 123441.44619973708 -77.625 40.445 +4759 3651 -80.2395 39.783500000000004 386764.6591716248 52990.14482360457 +3651 4326 386764.6591716248 52990.14482360457 -80.2395 39.783500000000025 +4759 3651 -75.0105 41.1065 830085.7794705472 200460.87468017207 +3651 4326 830085.7794705472 200460.87468017207 -75.0105 41.1065 +4759 3652 -77.625 40.445 2003290.5670151063 404990.8114069708 +3652 4326 2003290.5670151063 404990.8114069708 -77.625 40.445 +4759 3652 -80.2395 39.783500000000004 1268910.3859655724 173851.8334754427 +3652 4326 1268910.3859655724 173851.8334754427 -80.2395 39.783500000000025 +4759 3652 -75.0105 41.1065 2723373.094812954 657678.7196798646 +3652 4326 2723373.094812954 657678.7196798646 -75.0105 41.1065 +4759 3653 -71.465 41.575 102918.97351274508 54604.88640063912 +3653 4326 102918.97351274508 54604.88640063912 -71.465 41.57500000000001 +4759 3653 -71.8115 41.1745 73861.27843044055 10171.357680009327 +3653 4326 73861.27843044055 10171.357680009327 -71.8115 41.174499999999995 +4759 3653 -71.1185 41.975500000000004 131619.5144680358 99157.52522727985 +3653 4326 131619.5144680358 99157.52522727985 -71.1185 41.975500000000004 +4759 3654 -71.465 41.575 337659.99889973126 179149.53146609687 +3654 4326 337659.99889973126 179149.53146609687 -71.465 41.57500000000001 +4759 3654 -71.8115 41.1745 242326.54428387046 33370.5293218306 +3654 4326 242326.54428387046 33370.5293218306 -71.8115 41.174499999999995 +4759 3654 -71.1185 41.975500000000004 431821.6903505476 325319.31401650066 +3654 4326 431821.6903505476 325319.31401650066 -71.1185 41.975500000000004 +4759 3655 -80.94 33.629999999999995 615165.8566942128 199244.85107013452 +3655 4326 615165.8566942128 199244.85107013452 -80.94 33.63 +4759 3655 -83.118 32.208 409907.55380288104 43600.556483864246 +3655 4326 409907.55380288104 43600.556483864246 -83.118 32.208000000000006 +4759 3655 -78.762 35.052 813775.0055652673 359176.8093884141 +3655 4326 813775.0055652673 359176.8093884141 -78.762 35.052 +4759 3656 -80.94 33.629999999999995 2018260.684692299 653690.4562668455 +3656 4326 2018260.684692299 653690.4562668455 -80.94 33.63 +4759 3656 -83.118 32.208 1344841.0557837302 143046.4451570349 +3656 4326 1344841.0557837302 143046.4451570349 -83.118 32.208000000000006 +4759 3656 -78.762 35.052 2669865.503823055 1178401.605605033 +3656 4326 2669865.503823055 1178401.605605033 -78.762 35.052 +4759 3657 -100.25999999999999 45.045 579517.1582990598 134676.18923571636 +3657 4326 579517.1582990598 134676.18923571636 -100.25999999999999 45.04500000000001 +4759 3657 -103.689 44.2305 305357.195159725 50848.92774272213 +3657 4326 305357.195159725 50848.92774272213 -103.689 44.23049999999999 +4759 3657 -96.831 45.859500000000004 846048.4784497482 229981.1518169474 +3657 4326 846048.4784497482 229981.1518169474 -96.831 45.859500000000004 +4759 3658 -100.25999999999999 45.045 1901299.2101861658 441850.1308508461 +3658 4326 1901299.2101861658 441850.1308508461 -100.25999999999999 45.04500000000001 +4759 3658 -103.689 44.2305 1001826.0644531978 166826.8571025809 +3658 4326 1001826.0644531978 166826.8571025809 -103.689 44.23049999999999 +4759 3658 -96.831 45.859500000000004 2775744.0497138826 754529.8289194349 +3658 4326 2775744.0497138826 754529.8289194349 -96.831 45.859500000000004 +4759 3659 -100.245 43.635 607127.5043806202 144607.50092678124 +3659 4326 607127.5043806202 144607.50092678124 -100.245 43.63499999999999 +4759 3659 -103.6785 42.595499999999994 325505.86952982674 34653.81023614486 +3659 4326 325505.86952982674 34653.81023614486 -103.6785 42.595499999999994 +4759 3659 -96.81150000000001 44.6745 879191.0064309249 266024.15353638545 +3659 4326 879191.0064309249 266024.15353638545 -96.81150000000001 44.6745 +4759 3660 -100.245 43.635 1991884.1539554184 474433.10929061484 +3660 4326 1991884.1539554184 474433.10929061484 -100.245 43.63499999999999 +4759 3660 -103.6785 42.595499999999994 1067930.5069491067 113693.37574975195 +3660 4326 1067930.5069491067 113693.37574975195 -103.6785 42.595499999999994 +4759 3660 -96.81150000000001 44.6745 2884479.1602654597 872780.910393958 +3660 4326 2884479.1602654597 872780.910393958 -96.81150000000001 44.6745 +4759 3661 -85.98 35.83 601807.0427761595 166053.81335326185 +3661 4326 601807.0427761595 166053.81335326185 -85.98 35.830000000000005 +4759 3661 -89.87700000000001 35.065 246434.998258424 88183.71880346097 +3661 4326 246434.998258424 88183.71880346097 -89.87700000000001 35.06499999999998 +4759 3661 -82.083 36.595 950418.3889824077 257951.77285579516 +3661 4326 950418.3889824077 257951.77285579516 -82.083 36.59500000000002 +4759 3662 -85.98 35.83 1974428.6061747835 544794.8859764934 +3662 4326 1974428.6061747835 544794.8859764934 -85.98 35.830000000000005 +4759 3662 -89.87700000000001 35.065 808512.1567861795 289316.0841076882 +3662 4326 808512.1567861795 289316.0841076882 -89.87700000000001 35.06499999999998 +4759 3662 -82.083 36.595 3118164.3311864496 846296.7747777214 +3662 4326 3118164.3311864496 846296.7747777214 -82.083 36.59500000000002 +4759 3663 -100.08 31.025000000000002 724185.2196589834 3150605.5484253517 +3663 4326 724185.2196589834 3150605.5484253517 -100.08 31.025000000000023 +4759 3663 -106.002 29.904500000000002 152730.7257107871 3040313.3125654417 +3663 4326 152730.7257107871 3040313.3125654417 -106.002 29.90449999999999 +4759 3663 -94.158 32.145500000000006 1282351.2308671223 3290980.4998480678 +3663 4326 1282351.2308671223 3290980.4998480678 -94.158 32.14550000000003 +4759 3664 -100.08 31.025000000000002 2375931.0078311823 10336611.703458842 +3664 4326 2375931.0078311823 10336611.703458842 -100.08 31.025000000000023 +4759 3664 -106.002 29.904500000000002 501084.05560280825 9974761.259641787 +3664 4326 501084.05560280825 9974761.259641787 -106.002 29.90449999999999 +4759 3664 -94.158 32.145500000000006 4207180.662936552 10797158.523251537 +3664 4326 4207180.662936552 10797158.523251537 -94.158 32.14550000000003 +4759 3665 -100.08 31.165 1492389.0677626291 7449402.239574473 +3665 4326 1492389.0677626291 7449402.239574473 -100.08 31.165000000000003 +4759 3665 -106.002 26.3635 900399.2687562371 6933023.245166092 +3665 4326 900399.2687562371 6933023.245166092 -106.002 26.363499999999995 +4759 3665 -94.158 35.966499999999996 2027403.945952803 7996433.036043674 +3665 4326 2027403.945952803 7996433.036043674 -94.158 35.966499999999996 +4759 3666 -100.08 31.165 1492389.085956109 6467970.2720677685 +3666 4326 1492389.085956109 6467970.2720677685 -100.08 31.165000000000003 +4759 3666 -106.002 26.3635 900372.0614798032 5952594.840398136 +3666 4326 900372.0614798032 5952594.840398136 -106.002 26.363500000000013 +4759 3666 -94.158 35.966499999999996 2027379.438878832 7013937.4695115145 +3666 4326 2027379.438878832 7013937.4695115145 -94.158 35.96649999999998 +4759 3667 -101.50999999999999 35.4 199091.66389534375 1155311.273626022 +3667 4326 199091.66389534375 1155311.273626022 -101.50999999999999 35.4 +4759 3667 -102.878 34.41 73304.61790902242 1046368.8035420016 +3667 4326 73304.61790902242 1046368.8035420016 -102.878 34.40999999999999 +4759 3667 -100.142 36.39 321839.4199389363 1265990.8998509285 +3667 4326 321839.4199389363 1265990.8998509285 -100.142 36.38999999999999 +4759 3668 -101.50999999999999 35.4 653186.5676299729 3790383.7365547083 +3668 4326 653186.5676299729 3790383.7365547083 -101.50999999999999 35.4 +4759 3668 -102.878 34.41 240500.23425651703 3432961.649287385 +3668 4326 240500.23425651703 3432961.649287385 -102.878 34.40999999999999 +4759 3668 -100.142 36.39 1055901.4972496596 4153505.143594256 +3668 4326 1055901.4972496596 4153505.143594256 -100.142 36.38999999999999 +4759 3669 -98.535 33.15 596735.0946008511 2164487.199652242 +3669 4326 596735.0946008511 2164487.199652242 -98.535 33.149999999999984 +4759 3669 -102.61649999999999 31.863 210507.96670704812 2029404.6807503384 +3669 4326 210507.96670704812 2029404.6807503384 -102.61649999999999 31.863000000000007 +4759 3669 -94.4535 34.437 971879.2800386127 2314398.7444936717 +3669 4326 971879.2800386127 2314398.7444936717 -94.45350000000002 34.437000000000005 +4759 3670 -98.535 33.15 1957788.3895362925 7101321.754525732 +3670 4326 1957788.3895362925 7101321.754525732 -98.535 33.149999999999984 +4759 3670 -102.61649999999999 31.863 690641.5541047071 6658138.523761735 +3670 4326 690641.5541047071 6658138.523761735 -102.61649999999999 31.863000000000007 +4759 3670 -94.4535 34.437 3188573.937926682 7593156.547892988 +3670 4326 3188573.937926682 7593156.547892988 -94.45350000000002 34.437000000000005 +4759 3671 -98.525 27.02 297519.3277685372 5149938.002474285 +3671 4326 297519.3277685372 5149938.002474285 -98.525 27.01999999999998 +4759 3671 -100.0325 25.948999999999998 146497.67705421898 5032213.809542949 +3671 4326 146497.67705421898 5032213.809542949 -100.0325 25.949000000000012 +4759 3671 -97.0175 28.091 445706.4161671256 5269468.382250638 +3671 4326 445706.4161671256 5269468.382250638 -97.0175 28.091 +4759 3672 -98.525 27.02 976111.3278539426 16896088.26345105 +3672 4326 976111.3278539426 16896088.26345105 -98.525 27.02 +4759 3672 -100.0325 25.948999999999998 480634.4621353835 16509854.80714216 +3672 4326 480634.4621353835 16509854.80714216 -100.0325 25.94900000000002 +4759 3672 -97.0175 28.091 1462288.4670416447 17288247.517767303 +3672 4326 1462288.4670416447 17288247.517767303 -97.0175 28.091 +4759 3673 -99.38 29.225 563058.7354705705 4154293.521203296 +3673 4326 563058.7354705705 4154293.521203296 -99.38 29.225000000000026 +4759 3673 -104.438 27.924500000000002 64841.30729672348 4022548.884762567 +3673 4326 64841.30729672348 4022548.884762567 -104.43800000000002 27.924499999999995 +4759 3673 -94.322 30.5255 1048881.5104492614 4307362.930045891 +3673 4326 1048881.5104492614 4307362.930045891 -94.322 30.5255 +4759 3674 -99.38 29.225 1847301.8679563636 13629544.66048115 +3674 4326 1847301.8679563636 13629544.66048115 -99.38 29.225000000000026 +4759 3674 -104.438 27.924500000000002 212733.5223560003 13197312.465758525 +3674 4326 212733.5223560003 13197312.465758525 -104.43800000000002 27.924499999999995 +4759 3674 -94.322 30.5255 3441205.422198952 14131739.879325563 +3674 4326 3441205.422198952 14131739.879325563 -94.322 30.525500000000008 +4759 3675 -111.545 39.785 496145.6404933844 2161162.6996333404 +3675 4326 496145.6404933844 2161162.6996333404 -111.545 39.785000000000004 +4759 3675 -113.7995 38.6195 299737.81075454154 2034346.1327566376 +3675 4326 299737.81075454154 2034346.1327566376 -113.79950000000001 38.61950000000001 +4759 3675 -109.29050000000001 40.9505 686033.2712166447 2292873.457553067 +3675 4326 686033.2712166447 2292873.457553067 -109.29050000000001 40.950500000000005 +4759 3676 -111.545 39.785 1627774.4115609722 7090428.804545079 +3676 4326 1627774.4115609722 7090428.804545079 -111.545 39.785000000000004 +4759 3676 -113.7995 38.6195 983391.7680608317 6674363.952587393 +3676 4326 983391.7680608317 6674363.952587393 -113.79950000000001 38.61950000000001 +4759 3676 -109.29050000000001 40.9505 2250765.3260073643 7522550.713730533 +3676 4326 2250765.3260073643 7522550.713730533 -109.29050000000001 40.950500000000005 +4759 3677 -111.545 39.785 1627771.1555520452 7090414.623747053 +3677 4326 1627771.1555520452 7090414.623747053 -111.545 39.785000000000004 +4759 3677 -113.7995 38.6195 983389.8008171918 6674350.603919069 +3677 4326 983389.8008171918 6674350.603919069 -113.79950000000001 38.61950000000001 +4759 3677 -109.29050000000001 40.9505 2250760.8240166083 7522535.668688687 +3677 4326 2250760.8240166083 7522535.668688687 -109.29050000000001 40.950500000000005 +4759 3678 -111.54 41.28 496649.01927216403 1105128.0559982655 +3678 4326 496649.01927216403 1105128.0559982655 -111.54 41.28000000000001 +4759 3678 -113.79 40.623 306256.0472603298 1034720.2739077386 +3678 4326 306256.0472603298 1034720.2739077386 -113.79 40.62299999999999 +4759 3678 -109.29 41.937 683266.1531509589 1180426.6984195628 +3678 4326 683266.1531509589 1180426.6984195628 -109.29 41.937000000000005 +4759 3679 -111.54 41.28 1629425.9167406955 3625748.2152042827 +3679 4326 1629425.9167406955 3625748.2152042827 -111.54 41.28000000000001 +4759 3679 -113.79 40.623 1004777.0584341529 3394751.5548022916 +3679 4326 1004777.0584341529 3394751.5548022916 -113.79 40.62299999999999 +4759 3679 -109.29 41.937 2241686.854663251 3872791.005300402 +3679 4326 2241686.854663251 3872791.005300402 -109.29 41.937000000000005 +4759 3680 -111.54 41.28 1629422.6574287582 3625740.963687643 +3680 4326 1629422.6574287582 3625740.963687643 -111.54 41.28000000000001 +4759 3680 -113.79 40.623 1004775.0484199319 3394744.7652789727 +3680 4326 1004775.0484199319 3394744.7652789727 -113.79 40.62299999999999 +4759 3680 -109.29 41.937 2241682.370829438 3872783.259698183 +3680 4326 2241682.370829438 3872783.259698183 -109.29 41.937000000000005 +4759 3681 -111.545 37.785 496036.2211105266 3124117.453606073 +3681 4326 496036.2211105266 3124117.453606073 -111.545 37.785000000000004 +4759 3681 -113.7995 37.069500000000005 295518.8854022882 3047222.248513894 +3681 4326 295518.8854022882 3047222.248513894 -113.79950000000001 37.069500000000026 +4759 3681 -109.29050000000001 38.500499999999995 692727.1993279501 3205811.513840428 +3681 4326 692727.1993279501 3205811.513840428 -109.29050000000001 38.5005 +4759 3682 -111.545 37.785 1627415.4240844045 10249729.178458244 +3682 4326 1627415.4240844045 10249729.178458244 -111.545 37.785000000000004 +4759 3682 -113.7995 37.069500000000005 969550.1494510765 9997448.321856607 +3682 4326 969550.1494510765 9997448.321856607 -113.79950000000001 37.069500000000026 +4759 3682 -109.29050000000001 38.500499999999995 2272727.0324092847 10517754.31046072 +3682 4326 2272727.0324092847 10517754.31046072 -109.29050000000001 38.5005 +4759 3683 -111.545 37.785 1627412.1687934527 10249708.679039259 +3683 4326 1627412.1687934527 10249708.679039259 -111.545 37.785000000000004 +4759 3683 -113.7995 37.069500000000005 969548.2098906735 9997428.326999336 +3683 4326 969548.2098906735 9997428.326999336 -113.79950000000001 37.069500000000026 +4759 3683 -109.29050000000001 38.500499999999995 2272722.4864951163 10517733.274991473 +3683 4326 2272722.4864951163 10517733.274991473 -109.29050000000001 38.5005 +4759 3684 -72.47 43.875 502411.1456667513 152752.52373386305 +3684 4326 502411.1456667513 152752.52373386305 -72.47 43.87500000000001 +4759 3684 -73.343 42.835499999999996 431080.14350458205 37612.8404625171 +3684 4326 431080.14350458205 37612.8404625171 -73.343 42.835499999999996 +4759 3684 -71.597 44.914500000000004 571301.9692705146 268653.87743555265 +3684 4326 571301.9692705146 268653.87743555265 -71.597 44.91450000000001 +4759 3685 -78.285 38.615 3518724.203106891 2105285.4939807146 +3685 4326 3518724.203106891 2105285.4939807146 -78.285 38.614999999999995 +4759 3685 -79.88250000000001 37.8545 3378332.274917174 2021765.3758821723 +3685 4326 3378332.274917174 2021765.3758821723 -79.88250000000001 37.85450000000001 +4759 3685 -76.6875 39.3755 3656172.5417433567 2191230.607915427 +3685 4326 3656172.5417433567 2191230.607915427 -76.6875 39.3755 +4759 3686 -78.285 38.615 11544347.656693192 6907090.8251683945 +3686 4326 11544347.656693192 6907090.8251683945 -78.285 38.614999999999995 +4759 3686 -79.88250000000001 37.8545 11083745.13895743 6633075.237706761 +3686 4326 11083745.13895743 6633075.237706761 -79.88250000000001 37.85450000000001 +4759 3686 -76.6875 39.3755 11995292.747702997 7189062.41980253 +3686 4326 11995292.747702997 7189062.41980253 -76.6875 39.3755 +4759 3687 -79.495 37.41 3411917.6086140587 1119947.2032969252 +3687 4326 3411917.6086140587 1119947.2032969252 -79.495 37.41000000000001 +4759 3687 -83.26150000000001 36.627 3074278.1812144495 1043328.2241050153 +3687 4326 3074278.1812144495 1043328.2241050153 -83.26150000000001 36.626999999999995 +4759 3687 -75.7285 38.193 3742765.366771591 1209952.5513936281 +3687 4326 3742765.366771591 1209952.5513936281 -75.7285 38.19300000000001 +4759 3688 -79.495 37.41 11193933.021261292 3674360.1158166635 +3688 4326 11193933.021261292 3674360.1158166635 -79.495 37.41000000000001 +4759 3688 -83.26150000000001 36.627 10086194.333201073 3422986.0149178724 +3688 4326 10086194.333201073 3422986.0149178724 -83.26150000000001 36.626999999999995 +4759 3688 -75.7285 38.193 12279389.374483127 3969652.6620305968 +3688 4326 12279389.374483127 3969652.6620305968 -75.7285 38.19300000000001 +4759 3689 -120.905 48.065 494658.8782095999 118411.46677224239 +3689 4326 494658.8782095999 118411.46677224239 -120.90500000000002 48.065 +4759 3689 -124.4015 47.1785 229601.1611486969 26116.106779398477 +3689 4326 229601.1611486969 26116.106779398477 -124.4015 47.178500000000014 +4759 3689 -117.4085 48.951499999999996 750773.7610210045 222566.60307881774 +3689 4326 750773.7610210045 222566.60307881774 -117.4085 48.951499999999996 +4759 3690 -120.905 48.065 1622893.336592662 388488.2872352653 +3690 4326 1622893.336592662 388488.2872352653 -120.90500000000002 48.065 +4759 3690 -124.4015 47.1785 753283.1432020159 85682.59365874318 +3690 4326 753283.1432020159 85682.59365874318 -124.4015 47.178500000000014 +4759 3690 -117.4085 48.951499999999996 2463163.581283079 730203.9302677546 +3690 4326 2463163.581283079 730203.9302677546 -117.4085 48.951499999999996 +4759 3691 -120.655 46.575 488119.19059687364 138022.62988068728 +3691 4326 488119.19059687364 138022.62988068728 -120.655 46.575 +4759 3691 -124.02550000000001 45.643499999999996 225232.76586336124 40617.24264164518 +3691 4326 225232.76586336124 40617.24264164518 -124.02550000000001 45.64349999999999 +4759 3691 -117.2845 47.5065 742180.9642298741 246499.98165656062 +3691 4326 742180.9642298741 246499.98165656062 -117.2845 47.5065 +4759 3692 -120.655 46.575 1601437.7114832425 452829.2448668882 +3692 4326 1601437.7114832425 452829.2448668882 -120.655 46.575 +4759 3692 -124.02550000000001 45.643499999999996 738951.1663367104 133258.40356679758 +3692 4326 738951.1663367104 133258.40356679758 -124.02550000000001 45.64349999999999 +4759 3692 -117.2845 47.5065 2434972.0471441783 808725.3564848994 +3692 4326 2434972.0471441783 808725.3564848994 -117.2845 47.5065 +4759 3693 -79.74000000000001 39.7 579417.2957110632 133248.46017368144 +3693 4326 579417.2957110632 133248.46017368144 -79.74000000000001 39.70000000000001 +4759 3693 -81.558 38.854 421367.18441514135 41346.53591224026 +3693 4326 421367.18441514135 41346.53591224026 -81.558 38.854000000000006 +4759 3693 -77.922 40.546 733674.570779342 228331.12197136818 +3693 4326 733674.570779342 228331.12197136818 -77.922 40.546000000000014 +4759 3694 -80.85 38.185 613140.720628098 131531.86202500196 +3694 4326 613140.720628098 131531.86202500196 -80.85 38.18500000000001 +4759 3694 -82.47 37.298500000000004 469665.8086919263 34164.20925580738 +3694 4326 469665.8086919263 34164.20925580738 -82.47 37.298500000000004 +4759 3694 -79.23 39.0715 753171.9602462114 231389.43782599163 +3694 4326 753171.9602462114 231389.43782599163 -79.23 39.07150000000001 +4759 3695 -89.57 44.89 633966.7768787964 117505.3837196188 +3695 4326 633966.7768787964 117505.3837196188 -89.57000000000001 44.88999999999999 +4759 3695 -92.558 44.071 395103.35046936397 29636.785858339033 +3695 4326 395103.35046936397 29636.785858339033 -92.558 44.071 +4759 3695 -86.582 45.708999999999996 866087.9109689173 214035.8706913536 +3695 4326 866087.9109689173 214035.8706913536 -86.582 45.708999999999996 +4759 3696 -89.57 44.89 2079939.3338098514 385515.57975344936 +3696 4326 2079939.3338098514 385515.57975344936 -89.57000000000001 44.88999999999999 +4759 3696 -92.558 44.071 1296268.2423315719 97233.35493690065 +3696 4326 1296268.2423315719 97233.35493690065 -92.558 44.071 +4759 3696 -86.582 45.708999999999996 2841490.0879038563 702216.019093216 +3696 4326 2841490.0879038563 702216.019093216 -86.582 45.708999999999996 +4759 3697 -90.47 46.34 563817.9183929065 130517.46296681912 +3697 4326 563817.9183929065 130517.46296681912 -90.47 46.339999999999996 +4759 3697 -92.648 45.467 392950.88681591925 36832.035967861266 +3697 4326 392950.88681591925 36832.035967861266 -92.648 45.467 +4759 3697 -88.292 47.213 729391.0452604456 228851.15030724084 +3697 4326 729391.0452604456 228851.15030724084 -88.292 47.212999999999994 +4759 3698 -90.47 46.34 1849792.6205940612 428206.0430836391 +3698 4326 1849792.6205940612 428206.0430836391 -90.47 46.339999999999996 +4759 3698 -92.648 45.467 1289206.367828562 120839.77133789152 +3698 4326 1289206.367828562 120839.77133789152 -92.648 45.467 +4759 3698 -88.292 47.213 2393010.4543253123 750822.4822996728 +3698 4326 2393010.4543253123 750822.4822996728 -88.292 47.212999999999994 +4759 3699 -89.19 43.405 665607.2736576359 156400.65287319338 +3699 4326 665607.2736576359 156400.65287319338 -89.19 43.404999999999994 +4759 3699 -91.206 42.5725 500982.45158363064 64316.57338896093 +3699 4326 500982.45158363064 64316.57338896093 -91.206 42.57250000000001 +4759 3699 -87.174 44.2375 825722.2152976134 252402.33148048513 +3699 4326 825722.2152976134 252402.33148048513 -87.174 44.23750000000001 +4759 3700 -89.19 43.405 2183746.5303250942 513124.4753014687 +3700 4326 2183746.5303250942 513124.4753014687 -89.19 43.404999999999994 +4759 3700 -91.206 42.5725 1643639.9265706283 211011.95786028268 +3700 4326 1643639.9265706283 211011.95786028268 -91.206 42.57250000000001 +4759 3700 -87.174 44.2375 2709056.968022254 828089.9825322251 +3700 4326 2709056.968022254 828089.9825322251 -87.174 44.23750000000001 +4759 3701 -89.57 44.894999999999996 553952.4209604628 491376.26325869653 +3701 4326 553952.4209604628 491376.26325869653 -89.57000000000001 44.895 +4759 3701 -92.558 42.7215 310556.80412441783 253061.62620366085 +3701 4326 310556.80412441783 253061.62620366085 -92.558 42.721500000000006 +4759 3701 -86.582 47.0685 779512.1515750884 738447.0279873684 +3701 4326 779512.1515750884 738447.0279873684 -86.582 47.068499999999986 +4759 3702 -105.19 43.0 198097.49632915662 277654.042474903 +3702 4326 198097.49632915662 277654.042474903 -105.19 43.0 +4759 3702 -106.216 41.191 111974.33693520293 77262.27621200448 +3702 4326 111974.33693520293 77262.27621200448 -106.216 41.19100000000001 +4759 3702 -104.164 44.809 279314.3848616645 479129.0114329825 +3702 4326 279314.3848616645 479129.0114329825 -104.164 44.809 +4759 3703 -107.315 43.0 401494.82431166724 377653.9413779187 +3703 4326 401494.82431166724 377653.9413779187 -107.315 43.0 +4759 3703 -108.49849999999999 41.191 302257.2284902079 177385.96133674524 +3703 4326 302257.2284902079 177385.96133674524 -108.49849999999999 41.191 +4759 3703 -106.1315 44.809 495069.171304143 579342.6387105695 +3703 4326 495069.171304143 579342.6387105695 -106.1315 44.809 +4759 3704 -109.28 43.0 556785.943962579 277790.0915190472 +3704 4326 556785.943962579 277790.0915190472 -109.28000000000002 43.0 +4759 3704 -110.882 41.191 421148.2266105671 78923.38133064652 +3704 4326 421148.2266105671 78923.38133064652 -110.882 41.191 +4759 3704 -107.678 44.809 684798.8953224984 479198.9978457161 +3704 4326 684798.8953224984 479198.9978457161 -107.678 44.809000000000005 +4759 3705 -110.05000000000001 42.83 802725.3431980808 358770.01389699115 +3705 4326 802725.3431980808 358770.01389699115 -110.05000000000001 42.83 +4759 3705 -110.959 41.174 726523.9197376369 175213.1974278146 +3705 4326 726523.9197376369 175213.1974278146 -110.959 41.174 +4759 3705 -109.141 44.486000000000004 874956.6699701425 543180.753202716 +3705 4326 874956.6699701425 543180.753202716 -109.141 44.48600000000001 +4759 3706 170.82999999999998 52.644999999999996 488497.8856865507 5832794.455588982 +3706 4326 488497.8856865507 5832794.455588982 170.82999999999998 52.644999999999996 +4759 3706 167.96800000000002 49.3735 279910.38483448746 5473399.051220152 +3706 4326 279910.38483448746 5473399.051220152 167.96800000000002 49.3735 +4759 3706 173.69199999999998 55.9165 668232.9652947485 6200060.567462479 +3706 4326 668232.9652947485 6200060.567462479 173.69199999999998 55.91649999999999 +4759 3707 177.0 52.295 500000.00000000175 5793849.77786767 +3707 4326 500000.00000000175 5793849.77786767 177.0 52.295 +4759 3707 174.3 48.3575 299996.1712407819 5359559.00128083 +3707 4326 299996.1712407819 5359559.00128083 174.3 48.3575 +4759 3707 179.7 56.2325 667357.1124991814 6235235.713225893 +3707 4326 667357.1124991814 6235235.713225893 179.7 56.232499999999995 +4759 3708 -176.995 55.545 500315.49276570586 6155441.215371645 +3708 4326 500315.49276570586 6155441.215371645 -176.995 55.544999999999995 +4759 3708 -179.6995 48.6465 301167.493751609 5391677.341764835 +3708 4326 301167.493751609 5391677.341764835 -179.6995 48.646499999999996 +4759 3708 -174.2905 62.4435 639819.2265721997 6926519.820266791 +3708 4326 639819.2265721997 6926519.820266791 -174.2905 62.44350000000002 +4759 3709 -170.995 60.855 500271.661002635 6746636.777194004 +3709 4326 500271.661002635 6746636.777194004 -170.995 60.85499999999999 +4759 3709 -173.6995 49.87949999999999 306063.71185274015 5528727.910589498 +3709 4326 306063.71185274015 5528727.910589498 -173.6995 49.87949999999999 +4759 3709 -168.2905 71.8305 594273.4174823904 7972144.773295547 +3709 4326 594273.4174823904 7972144.773295547 -168.2905 71.83049999999999 +4759 3710 -164.995 61.905 500262.70150924154 6863597.136707529 +3710 4326 500262.70150924154 6863597.136707529 -164.995 61.905 +4759 3710 -167.6995 50.758500000000005 309609.5523102755 5626444.5756313205 +3710 4326 309609.5523102755 5626444.5756313205 -167.6995 50.7585 +4759 3710 -162.2905 73.0515 588133.8862965786 8108218.624054418 +3710 4326 588133.8862965786 8108218.624054418 -162.2905 73.05149999999999 +4759 3711 -158.995 62.845 500254.6046334522 6968319.769418479 +3711 4326 500254.6046334522 6968319.769418479 -158.995 62.845 +4759 3711 -161.6995 52.1665 315383.0576533729 5782993.124320009 +3711 4326 315383.0576533729 5782993.124320009 -161.6995 52.16649999999999 +4759 3711 -156.2905 73.52349999999998 585749.5068505375 8160823.92657466 +3711 4326 585749.5068505375 8160823.92657466 -156.2905 73.52349999999998 +4759 3712 -152.995 63.42999999999999 500249.53038246953 7033500.029426013 +3712 4326 500249.53038246953 7033500.029426013 -152.995 63.43 +4759 3712 -155.6995 53.278 320020.29264672974 5906596.009073531 +3712 4326 320020.29264672974 5906596.009073531 -155.6995 53.278 +4759 3712 -150.2905 73.582 585453.5709085178 8167343.993562271 +3712 4326 585453.5709085178 8167343.993562271 -150.2905 73.582 +4759 3713 -146.995 64.09 500243.7739232476 7107043.182421333 +3713 4326 500243.7739232476 7107043.182421333 -146.995 64.09 +4759 3713 -149.6995 55.053999999999995 327570.85274766205 6104131.138473459 +3713 4326 327570.85274766205 6104131.138473459 -149.6995 55.05399999999999 +4759 3713 -144.2905 73.12599999999999 587757.9371418224 8116521.666334169 +3713 4326 587757.9371418224 8116521.666334169 -144.2905 73.12599999999999 +4759 3714 -140.995 63.53 500248.6603325604 7044642.494469673 +3714 4326 500248.6603325604 7044642.494469673 -140.995 63.53000000000001 +4759 3714 -143.6995 54.476 325094.82230164204 6039838.232701388 +3714 4326 325094.82230164204 6039838.232701388 -143.6995 54.47599999999999 +4759 3714 -138.2905 72.584 590489.5558254714 8056116.729180485 +3714 4326 590489.5558254714 8056116.729180485 -138.2905 72.58400000000002 +4759 3715 -134.995 63.32000000000001 500250.4865463568 7021243.499326773 +3715 4326 500250.4865463568 7021243.499326773 -134.995 63.32000000000001 +4759 3715 -137.6995 54.572 325504.829562229 6050516.306136905 +3715 4326 325504.829562229 6050516.306136905 -137.6995 54.571999999999996 +4759 3715 -132.2905 72.06800000000001 593082.5128518728 7998611.830386628 +3715 4326 593082.5128518728 7998611.830386628 -132.2905 72.06800000000001 +4759 3716 -129.0 46.11 500000.0 5106269.364479603 +3716 4326 500000.0 5106269.364479603 -129.0 46.11 +4759 3716 -131.7 36.453 258028.1796662494 4037583.6488569877 +3716 4326 258028.1796662494 4037583.6488569877 -131.7 36.45299999999999 +4759 3716 -126.3 55.767 669381.8037290876 6183448.368927643 +3716 4326 669381.8037290876 6183448.368927643 -126.30000000000001 55.766999999999996 +4759 3717 -122.995 39.815 500427.9480454584 4407224.36870956 +3717 4326 500427.9480454584 4407224.36870956 -122.995 39.81499999999999 +4759 3717 -125.6995 31.4675 243511.68383341894 3484570.1475524465 +3717 4326 243511.68383341894 3484570.1475524465 -125.6995 31.4675 +4759 3717 -120.2905 48.1625 701472.7717525429 5337912.152125345 +3717 4326 701472.7717525429 5337912.152125345 -120.2905 48.162499999999994 +4759 3718 -117.0 39.945 500000.0 4421652.785645828 +3718 4326 500000.0 4421652.785645828 -117.0 39.94499999999999 +4759 3718 -119.7 31.7865 244338.95886527732 3519945.785868588 +3718 4326 244338.95886527732 3519945.785868588 -119.7 31.786499999999997 +4759 3718 -114.3 48.1035 700996.7036172554 5331330.322624897 +3718 4326 700996.7036172554 5331330.322624897 -114.3 48.10349999999999 +4759 3719 -111.0 40.17 500000.0 4446625.833486382 +3719 4326 500000.0 4446625.833486382 -111.0 40.17 +4759 3719 -113.7 32.214 245523.7262897845 3567354.175285182 +3719 4326 245523.7262897845 3567354.175285182 -113.7 32.214 +4759 3719 -108.3 48.126 700908.9122900425 5333830.855137835 +3719 4326 700908.9122900425 5333830.855137835 -108.3 48.12599999999999 +4759 3720 -105.0 38.995 499999.9999999989 4316221.727961586 +3720 4326 499999.9999999989 4316221.727961586 -105.00000000000001 38.99499999999999 +4759 3720 -107.7 29.9815 239494.3482462007 3319804.134164058 +3720 4326 239494.3482462007 3319804.134164058 -107.7 29.981499999999993 +4759 3720 -102.3 48.0085 701367.0340646396 5320772.6031839885 +3720 4326 701367.0340646396 5320772.6031839885 -102.3 48.00849999999999 +4759 3721 -99.0 37.42 500000.0 4141465.9905510056 +3721 4326 500000.0 4141465.9905510056 -99.0 37.42 +4759 3721 -101.7 26.988999999999997 232034.19468521152 2988083.604299114 +3721 4326 232034.19468521152 2988083.604299114 -101.7 26.988999999999994 +4759 3721 -96.3 47.851 701979.7750945212 5303269.316841059 +3721 4326 701979.7750945212 5303269.316841059 -96.3 47.851 +4759 3722 -93.005 37.495000000000005 499558.0201628704 4149786.64043638 +3722 4326 499558.0201628704 4149786.64043638 -93.005 37.495000000000005 +4759 3722 -95.7095 26.7985 230638.96187599917 2966990.4779571556 +3722 4326 230638.96187599917 2966990.4779571556 -95.7095 26.798499999999997 +4759 3722 -90.3005 48.191500000000005 700616.009579512 5341108.920825521 +3722 4326 700616.009579512 5341108.920825521 -90.3005 48.1915 +4759 3723 -87.0 36.144999999999996 500000.00000000116 4000031.267611221 +3723 4326 500000.00000000116 4000031.267611221 -87.0 36.14499999999998 +4759 3723 -89.7 25.1875 227894.03767243755 2788439.5189798116 +3723 4326 227894.03767243755 2788439.5189798116 -89.7 25.187499999999996 +4759 3723 -84.3 47.1025 704870.6841148983 5220092.172714736 +3723 4326 704870.6841148983 5220092.172714736 -84.3 47.102499999999985 +4759 3724 -81.0 34.97 500000.0 3869716.186715929 +3724 4326 500000.0 3869716.186715929 -81.0 34.97 +4759 3724 -83.7 24.926 227315.29907310876 2759463.007309322 +3724 4326 227315.29907310876 2759463.007309322 -83.7 24.92599999999999 +4759 3724 -78.3 45.014 712749.5387233787 4988052.390542265 +3724 4326 712749.5387233787 4988052.390542265 -78.3 45.01400000000001 +4759 3725 -75.0 36.655 500000.0 4056601.5122795035 +3725 4326 500000.0 4056601.5122795035 -75.0 36.655 +4759 3725 -77.7 29.1175 237266.58570995316 3224017.4637190658 +3725 4326 237266.58570995316 3224017.4637190658 -77.7 29.117500000000003 +4759 3725 -72.3 44.1925 715771.344711273 4896798.997111282 +3725 4326 715771.344711273 4896798.997111282 -72.3 44.192499999999995 +4759 3726 -68.995 40.54 500423.4175416514 4487694.7526666885 +3726 4326 500423.4175416514 4487694.7526666885 -68.995 40.53999999999999 +4759 3726 -71.6995 34.303 251561.85420346117 3799052.6504672226 +3726 4326 251561.85420346117 3799052.6504672226 -71.6995 34.303000000000004 +4759 3726 -66.2905 46.777 706842.1012096816 5183948.567322707 +3726 4326 706842.1012096816 5183948.567322707 -66.2905 46.776999999999994 +4626 3727 55.535 -21.115000000000002 160173.15805688707 50184.53300948528 +3727 4326 160173.15805688707 50184.53300948528 55.52908968431331 -21.127449561428005 +4626 3727 55.1975 -21.3895 125173.07779205836 19754.08038012142 +3727 4326 125173.07779205836 19754.08038012142 55.19153745131264 -21.4019409337601 +4626 3727 55.872499999999995 -20.8405 195302.22800352276 80539.60145660925 +3727 4326 195302.22800352276 80539.60145660925 55.86664179530096 -20.852957563069786 +4759 3728 -82.66 41.215 1924481.0225544479 564136.007901402 +3728 4326 1924481.0225544479 564136.007901402 -82.66 41.21500000000002 +4759 3728 -84.595 40.2115 1383400.4315238255 205527.65148800242 +3728 4326 1383400.4315238255 205527.65148800242 -84.595 40.211499999999994 +4759 3728 -80.72500000000001 42.2185 2449360.345815376 934665.2398408941 +3728 4326 2449360.345815376 934665.2398408941 -80.72500000000001 42.218499999999985 +4759 3729 -82.765 39.379999999999995 1893594.6209665271 502727.76517093985 +3729 4326 1893594.6209665271 502727.76517093985 -82.765 39.379999999999995 +4759 3729 -84.62349999999999 38.498 1360768.4810121092 188530.2889392854 +3729 4326 1360768.4810121092 188530.2889392854 -84.62349999999999 38.498 +4759 3729 -80.90650000000001 40.262 2413229.437662917 827824.3807957403 +3729 4326 2413229.437662917 827824.3807957403 -80.90650000000001 40.261999999999986 +4759 3730 -105.19 43.0 649924.8692399081 910936.637686411 +3730 4326 649924.8692399081 910936.637686411 -105.19 43.0 +4759 3730 -106.216 41.191 367369.1371282449 253484.6512055514 +3730 4326 367369.1371282449 253484.6512055514 -106.216 41.19100000000001 +4759 3730 -104.164 44.809 916383.9443669778 1571942.431676377 +3730 4326 916383.9443669778 1571942.431676377 -104.164 44.809 +4759 3731 -107.315 43.0 1317237.6027291953 1239019.6393040551 +3731 4326 1317237.6027291953 1239019.6393040551 -107.315 43.0 +4759 3731 -108.49849999999999 41.191 991655.5904382907 581973.7747856386 +3731 4326 991655.5904382907 581973.7747856386 -108.49849999999999 41.191 +4759 3731 -106.1315 44.809 1624239.4394870093 1900726.6404695935 +3731 4326 1624239.4394870093 1900726.6404695935 -106.1315 44.809 +4759 3732 -109.28 43.0 1826721.8844838948 911382.9919254076 +3732 4326 1826721.8844838948 911382.9919254076 -109.28000000000002 43.0 +4759 3732 -110.882 41.191 1381717.140138169 258934.4602489628 +3732 4326 1381717.140138169 258934.4602489628 -110.882 41.191 +4759 3732 -107.678 44.809 2246711.0424038973 1572172.0454321536 +3732 4326 2246711.0424038973 1572172.0454321536 -107.678 44.809000000000005 +4759 3733 -110.05000000000001 42.83 2633608.0635090373 1177064.6205603785 +3733 4326 2633608.0635090373 1177064.6205603785 -110.05000000000001 42.83 +4759 3733 -110.959 41.174 2383603.893372564 574845.2985277551 +3733 4326 2383603.893372564 574845.2985277551 -110.959 41.174 +4759 3733 -109.141 44.486000000000004 2870587.0080937096 1782085.5210992442 +3733 4326 2870587.0080937096 1782085.5210992442 -109.141 44.48600000000001 +4269 3734 -82.66 41.215 1924481.0225544479 564136.007901402 +3734 4326 1924481.0225544479 564136.007901402 -82.66 41.21500000000002 +4269 3734 -84.595 40.2115 1383400.4315238255 205527.65148800242 +3734 4326 1383400.4315238255 205527.65148800242 -84.595 40.211499999999994 +4269 3734 -80.72500000000001 42.2185 2449360.345815376 934665.2398408941 +3734 4326 2449360.345815376 934665.2398408941 -80.72500000000001 42.218499999999985 +4269 3735 -82.765 39.379999999999995 1893594.6209665271 502727.76517093985 +3735 4326 1893594.6209665271 502727.76517093985 -82.765 39.379999999999995 +4269 3735 -84.62349999999999 38.498 1360768.4810121092 188530.2889392854 +3735 4326 1360768.4810121092 188530.2889392854 -84.62349999999999 38.498 +4269 3735 -80.90650000000001 40.262 2413229.437662917 827824.3807957403 +3735 4326 2413229.437662917 827824.3807957403 -80.90650000000001 40.261999999999986 +4269 3736 -105.19 43.0 649924.8692399081 910936.637686411 +3736 4326 649924.8692399081 910936.637686411 -105.19 43.0 +4269 3736 -106.216 41.191 367369.1371282449 253484.6512055514 +3736 4326 367369.1371282449 253484.6512055514 -106.216 41.19100000000001 +4269 3736 -104.164 44.809 916383.9443669778 1571942.431676377 +3736 4326 916383.9443669778 1571942.431676377 -104.164 44.809 +4269 3737 -107.315 43.0 1317237.6027291953 1239019.6393040551 +3737 4326 1317237.6027291953 1239019.6393040551 -107.315 43.0 +4269 3737 -108.49849999999999 41.191 991655.5904382907 581973.7747856386 +3737 4326 991655.5904382907 581973.7747856386 -108.49849999999999 41.191 +4269 3737 -106.1315 44.809 1624239.4394870093 1900726.6404695935 +3737 4326 1624239.4394870093 1900726.6404695935 -106.1315 44.809 +4269 3738 -109.28 43.0 1826721.8844838948 911382.9919254076 +3738 4326 1826721.8844838948 911382.9919254076 -109.28000000000002 43.0 +4269 3738 -110.882 41.191 1381717.140138169 258934.4602489628 +3738 4326 1381717.140138169 258934.4602489628 -110.882 41.191 +4269 3738 -107.678 44.809 2246711.0424038973 1572172.0454321536 +3738 4326 2246711.0424038973 1572172.0454321536 -107.678 44.809000000000005 +4269 3739 -110.05000000000001 42.83 2633608.0635090373 1177064.6205603785 +3739 4326 2633608.0635090373 1177064.6205603785 -110.05000000000001 42.83 +4269 3739 -110.959 41.174 2383603.893372564 574845.2985277551 +3739 4326 2383603.893372564 574845.2985277551 -110.959 41.174 +4269 3739 -109.141 44.486000000000004 2870587.0080937096 1782085.5210992442 +3739 4326 2870587.0080937096 1782085.5210992442 -109.141 44.48600000000001 +4152 3740 -122.39 41.45 550951.551068326 4588892.835878831 +3740 4326 550951.551068326 4588892.835878831 -122.39001364995049 41.45000527809671 +4152 3740 -124.55000000000001 34.61 357885.9306572019 3830886.9245571555 +3740 4326 357885.9306572019 3830886.9245571555 -124.55001315452733 34.61000457894889 +4152 3740 -120.22999999999999 48.29 705459.7498599625 5352242.337239634 +3740 4326 705459.7498599625 5352242.337239634 -120.23001463880793 48.29000594493182 +4152 3741 -117.0 40.635 500000.0 4498239.885530386 +3741 4326 500000.0 4498239.885530386 -117.00001262528407 40.635005659996786 +4152 3741 -119.7 33.0975 248017.09638482795 3665339.4480837076 +3741 4326 248017.09638482795 3665339.4480837076 -119.70001229926363 33.09750470470849 +4152 3741 -114.3 48.1725 700727.378092514 5338998.646615704 +3741 4326 700727.378092514 5338998.646615704 -114.30001347360133 48.17250656879712 +4152 3742 -111.0 40.17 500000.0 4446625.833486382 +3742 4326 500000.0 4446625.833486382 -111.00001139499217 40.170006075322945 +4152 3742 -113.7 32.214 245523.7262897845 3567354.175285182 +3742 4326 245523.7262897845 3567354.175285182 -113.7000111799151 32.214004894561455 +4152 3742 -108.3 48.126 700908.9122900425 5333830.855137835 +3742 4326 700908.9122900425 5333830.855137835 -108.3000121317675 48.12600716452807 +4152 3743 -105.0 38.995 499999.9999999989 4316221.727961586 +3743 4326 499999.9999999989 4316221.727961586 -105.00000993487357 38.995006343190354 +4152 3743 -107.7 29.9815 239494.3482462007 3319804.134164058 +3743 4326 239494.3482462007 3319804.134164058 -107.70000984100449 29.981504809284797 +4152 3743 -102.3 48.0085 701367.0340646396 5320772.6031839885 +3743 4326 701367.0340646396 5320772.6031839885 -102.30001060918907 48.008507705924806 +4152 3744 -98.995 37.42 500442.4214390528 4141466.002281309 +3744 4326 500442.4214390528 4141466.002281309 -98.99500832390645 37.420006466242576 +4152 3744 -101.6995 26.988999999999997 232083.83973180928 2988082.5419050795 +3744 4326 232083.83973180928 2988082.5419050795 -101.69950837045359 26.989004452934033 +4152 3744 -96.2905 47.851 702690.3924782698 5303294.206463164 +3744 4326 702690.3924782698 5303294.206463164 -96.29050892978495 47.8510081887574 +4152 3745 -92.995 38.900000000000006 500433.56754178915 4305679.582833987 +3745 4326 500433.56754178915 4305679.582833987 -92.9950068725602 38.90000705132631 +4152 3745 -95.6995 29.468000000000004 238211.6594325168 3262873.072987039 +3745 4326 238211.6594325168 3262873.072987039 -95.69950698381065 29.46800512745252 +4152 3745 -90.2905 48.332 700807.6941376636 5356749.820301056 +3745 4326 700807.6941376636 5356749.820301056 -90.290507224681 48.33200869932918 +4152 3746 -86.995 38.585 500435.4765086187 4270725.239612379 +3746 4326 500435.4765086187 4270725.239612379 -86.99500514792244 38.58500729765909 +4152 3746 -89.6995 29.823500000000003 239130.8524134633 3302285.7308274503 +3746 4326 239130.8524134633 3302285.7308274503 -89.69950540535358 29.823505391018315 +4152 3746 -84.2905 47.3465 704649.6263732009 5247230.69490993 +3746 4326 704649.6263732009 5247230.69490993 -84.29050519256532 47.34650894002524 +4152 3747 -81.005 35.27 499545.24551424995 3902985.7173645953 +3747 4326 499545.24551424995 3902985.7173645953 -81.00500316843107 35.27000687237788 +4152 3747 -83.7095 25.496 227628.63806144887 2822644.4996753535 +3747 4326 227628.63806144887 2822644.4996753535 -83.70950358967298 25.496004520277253 +4152 3747 -78.3005 45.044 712598.9734686548 4991383.713687879 +3747 4326 712598.9734686548 4991383.713687879 -78.30050300324844 45.04400887299441 +4152 3748 -75.0 39.435 500000.0 4365050.807317216 +3748 4326 500000.0 4365050.807317216 -75.0000015455974 39.43500795094683 +4152 3748 -77.7 34.3995 251800.6804294904 3809758.5649808547 +3748 4326 251800.6804294904 3809758.5649808547 -77.70000212939114 34.39950679074445 +4152 3748 -72.3 44.4705 714753.7087969125 4927678.485256798 +3748 4326 714753.7087969125 4927678.485256798 -72.30000097240861 44.47050900592781 +4152 3749 -69.455 44.215 463652.7572418525 4895853.512055859 +3749 4326 463652.7572418525 4895853.512055859 -69.45500000154486 44.21500904765371 +4152 3749 -71.74549999999999 41.2855 270086.92604058876 4574087.114458173 +3749 4326 270086.92604058876 4574087.114458173 -71.7455006010758 41.285508424432756 +4152 3749 -67.1645 47.1445 639166.5260081452 5222856.4607292935 +3749 4326 639166.5260081452 5222856.4607292935 -67.16449937573145 47.14450962428001 +4152 3750 -158.145 20.9 588920.6626141642 2311317.1234786245 +3750 4326 588920.6626141642 2311317.1234786245 -158.145 20.899999999999995 +4152 3750 -160.08450000000002 19.649 386301.0107171199 2173002.2708532047 +3750 4326 386301.0107171199 2173002.2708532047 -160.08450000000002 19.649000000000004 +4152 3750 -156.2055 22.151 788226.6273887353 2452192.3684225455 +3750 4326 788226.6273887353 2452192.3684225455 -156.2055 22.151000000000003 +4152 3751 -155.37 19.865000000000002 251820.14414430902 2198287.2017789544 +3751 4326 251820.14414430902 2198287.2017789544 -155.37 19.865000000000002 +4152 3751 -155.937 18.9695 190717.23376067448 2100031.073463214 +3751 4326 190717.23376067448 2100031.073463214 -155.937 18.969500000000004 +4152 3751 -154.803 20.7605 312295.71302144264 2296688.9862106 +3751 4326 312295.71302144264 2296688.9862106 -154.803 20.7605 +4152 3753 -82.66 41.215 1924481.0225544479 564136.007901402 +3753 4326 1924481.0225544479 564136.007901402 -82.66000405914379 41.215007992324054 +4152 3753 -84.595 40.2115 1383400.4315238255 205527.65148800242 +3753 4326 1383400.4315238255 205527.65148800242 -84.59500456577199 40.21150771613819 +4152 3753 -80.72500000000001 42.2185 2449360.345815376 934665.2398408941 +3753 4326 2449360.345815376 934665.2398408941 -80.7250035400007 42.21850826444878 +4152 3754 -82.765 39.379999999999995 1893594.6209665271 502727.76517093985 +3754 4326 1893594.6209665271 502727.76517093985 -82.76500395099885 39.38000764121306 +4152 3754 -84.62349999999999 38.498 1360768.4810121092 188530.2889392854 +3754 4326 1360768.4810121092 188530.2889392854 -84.62350444454509 38.49800738909418 +4152 3754 -80.90650000000001 40.262 2413229.437662917 827824.3807957403 +3754 4326 2413229.437662917 827824.3807957403 -80.90650344678056 40.262007890562415 +4152 3755 -105.19 43.0 649924.8692399081 910936.637686411 +3755 4326 649924.8692399081 910936.637686411 -105.19001048355334 43.0000068666047 +4152 3755 -106.216 41.191 367369.1371282449 253484.6512055514 +3755 4326 367369.1371282449 253484.6512055514 -106.21601047607007 41.19100655727374 +4152 3755 -104.164 44.809 916383.9443669778 1571942.431676377 +3755 4326 916383.9443669778 1571942.431676377 -104.16401051256005 44.80900717243314 +4152 3756 -107.315 43.0 1317237.6027291953 1239019.6393040551 +3756 4326 1317237.6027291953 1239019.6393040551 -107.31501097968342 43.00000669835129 +4152 3756 -108.49849999999999 41.191 991655.5904382907 581973.7747856386 +3756 4326 991655.5904382907 581973.7747856386 -108.49851099096762 41.191006386658806 +4152 3756 -106.1315 44.809 1624239.4394870093 1900726.6404695935 +3756 4326 1624239.4394870093 1900726.6404695935 -106.13151098867684 44.809007008740004 +4152 3757 -109.28 43.0 1826721.8844838948 911382.9919254076 +3757 4326 1826721.8844838948 911382.9919254076 -109.28001142106993 43.000006538694244 +4152 3757 -110.882 41.191 1381717.140138169 258934.4602489628 +3757 4326 1381717.140138169 258934.4602489628 -110.88201150453627 41.19100620356625 +4152 3757 -107.678 44.809 2246711.0424038973 1572172.0454321536 +3757 4326 2246711.0424038973 1572172.0454321536 -107.67801135116785 44.80900687697224 +4152 3758 -110.05000000000001 42.83 2633608.0635090373 1177064.6205603785 +3758 4326 2633608.0635090373 1177064.6205603785 -110.0500115633401 42.83000645617076 +4152 3758 -110.959 41.174 2383603.893372564 574845.2985277551 +3758 4326 2383603.893372564 574845.2985277551 -110.9590115183874 41.17400619561758 +4152 3758 -109.141 44.486000000000004 2870587.0080937096 1782085.5210992442 +3758 4326 2870587.0080937096 1782085.5210992442 -109.14101162906883 44.486006715156215 +4269 3759 -157.97000000000003 21.475 1650617.1258592834 112004.37197627786 +3759 4326 1650617.1258592834 112004.37197627786 -157.9700053252301 21.474995900015404 +4269 3759 -158.294 21.2275 1540283.8807207467 22190.660771012186 +3759 4326 1540283.8807207467 22190.660771012186 -158.29400538713924 21.22749582810886 +4269 3759 -157.64600000000002 21.7225 1760578.1593108154 202049.1949582628 +3759 4326 1760578.1593108154 202049.1949582628 -157.64600526304005 21.7224959720843 +4152 3760 -157.97000000000003 21.475 1650617.1258592834 112004.37197627786 +3760 4326 1650617.1258592834 112004.37197627786 -157.97000000000003 21.475 +4152 3760 -158.294 21.2275 1540283.8807207467 22190.660771012186 +3760 4326 1540283.8807207467 22190.660771012186 -158.294 21.2275 +4152 3760 -157.64600000000002 21.7225 1760578.1593108154 202049.1949582628 +3760 4326 1760578.1593108154 202049.1949582628 -157.64600000000002 21.722499999999997 +4617 3761 -50.995000000000005 48.575 500368.8140584385 5380212.31033272 +3761 4326 500368.8140584385 5380212.31033272 -50.99499366097559 48.575010001834606 +4617 3761 -53.6995 40.4075 270933.38613038993 4476486.695823391 +3761 4326 270933.38613038993 4476486.695823391 -53.69949462790906 40.40750854528627 +4617 3761 -48.2905 56.74249999999999 665706.9021386893 6292000.1815662505 +3761 4326 665706.9021386893 6292000.1815662505 -48.290492302049465 56.74251105572509 +4326 3762 -36.91 -54.44 5839.285182743556 62334.59923917501 +4326 3762 -37.963 -54.899 -61780.39272113682 10821.879716987449 +4326 3762 -35.857 -53.981 74984.11597856566 112820.40336187076 +4258 3763 -7.875 39.555 22183.286919778304 -12542.933773017621 +3763 4326 22183.286919778304 -12542.933773017621 -7.875 39.555 +4258 3763 -9.3915 37.2105 -111704.44795307111 -272081.1879175769 +3763 4326 -111704.44795307111 -272081.1879175769 -9.3915 37.210499999999996 +4258 3763 -6.358500000000001 41.899499999999996 147261.36161220903 249302.10568829722 +3763 4326 147261.36161220903 249302.10568829722 -6.358500000000001 41.899499999999996 +4167 3764 -176.39499999999998 -43.97 408425.8927618545 803327.9981826178 +3764 4326 408425.8927618545 803327.9981826178 -176.39499999999998 -43.97 +4167 3764 -177.1645 -44.573 347218.570864429 736114.667746884 +3764 4326 347218.570864429 736114.667746884 -177.1645 -44.57300000000001 +4167 3764 -175.6255 -43.367 470881.8728556228 869958.6831228347 +3764 4326 470881.8728556228 869958.6831228347 -175.6255 -43.367000000000004 +4761 3765 16.43 44.44 494427.7816159777 4922223.758357072 +3765 4326 494427.7816159777 4922223.758357072 16.43 44.43999999999999 +4761 3765 13.73 42.550000000000004 272505.26803624094 4715979.106272023 +3765 4326 272505.26803624094 4715979.106272023 13.73 42.550000000000004 +4761 3765 19.13 46.33 702490.4700523833 5135616.388258144 +3765 4326 702490.4700523833 5135616.388258144 19.13 46.33 +4761 3766 16.215 44.08 -22821.65408547129 5366223.373191329 +3766 4326 -22821.65408547129 5366223.373191329 16.215 44.08 +4761 3766 13.3215 41.866 -264021.12302143016 5125332.672859581 +3766 4326 -264021.12302143016 5125332.672859581 13.3215 41.866000000000014 +4761 3766 19.1085 46.294 200993.38991905752 5615402.794012015 +3766 4326 200993.38991905752 5615402.794012015 19.1085 46.293999999999976 +4761 3767 15.5 44.085 540029.7978159776 4881435.19282539 +3767 4326 540029.7978159776 4881435.19282539 15.5 44.085 +4761 3767 13.25 41.8755 354784.496954896 4637433.858478495 +3767 4326 354784.496954896 4637433.858478495 13.25 41.8755 +4761 3767 17.75 46.2945 711802.7384192995 5130445.128610627 +3767 4326 711802.7384192995 5130445.128610627 17.75 46.294500000000006 +4761 3768 18.715 43.769999999999996 316093.7712768562 4848865.269318848 +3768 4326 316093.7712768562 4848865.269318848 18.715 43.76999999999998 +4761 3768 18.0715 41.835 256831.20665350763 4635603.586554581 +3768 4326 256831.20665350763 4635603.586554581 18.0715 41.83500000000001 +4761 3768 19.3585 45.705 372221.4767051735 5062582.112584593 +3768 4326 372221.4767051735 5062582.112584593 19.3585 45.70499999999999 +4216 3769 -64.75 32.32 335265.21421379945 3577062.7106407387 +3769 4326 335265.21421379945 3577062.7106407387 -64.74972212607341 32.321408100962685 +4216 3769 -64.876 32.221000000000004 323210.5917383053 3566287.399181918 +3769 4326 323210.5917383053 3566287.399181918 -64.8757266219262 32.22240896088101 +4216 3769 -64.624 32.419 347294.14065077447 3587851.510147293 +3769 4326 347294.14065077447 3587851.510147293 -64.62371762998676 32.42040725808747 +4762 3770 -64.765 32.32 548587.547719713 135484.77550285993 +3770 4326 548587.547719713 135484.77550285993 -64.765 32.31999999999999 +4762 3770 -68.4235 29.251 192797.58107745502 -199162.5646523228 +3770 4326 192797.58107745502 -199162.5646523228 -68.4235 29.25100000000001 +4762 3770 -61.106500000000004 35.388999999999996 881100.731751971 481999.32082856906 +3770 4326 881100.731751971 481999.32082856906 -61.106500000000004 35.388999999999996 +4267 3771 -111.24000000000001 54.495000000000005 -15549.868682169968 6040220.721191527 +3771 4326 -15549.868682169968 6040220.721191527 -111.24100645168707 54.49504337534685 +4267 3771 -112.374 49.5405 -99437.45457483846 5489878.549607305 +3771 4326 -99437.45457483846 5489878.549607305 -112.37494144402606 49.54049003498778 +4267 3771 -110.10600000000001 59.4495 50707.51959111842 6592216.531025967 +3771 4326 50707.51959111842 6592216.531025967 -110.10709763449023 59.44964244942687 +4267 3772 -114.0 54.495000000000005 0.0 6040194.2090137275 +3772 4326 0.0 6040194.2090137275 -114.00111561095993 54.49501915011418 +4267 3772 -115.35 49.5405 -97700.60665370025 5489847.129627422 +3772 4326 -97700.60665370025 5489847.129627422 -115.35104572084934 49.540464475619196 +4267 3772 -112.65 59.4495 76569.83671898389 6592652.734505618 +3772 4326 76569.83671898389 6592652.734505618 -112.65121370856453 59.44961994399919 +4267 3773 -117.0 55.385000000000005 0.0 6139262.351265922 +3773 4326 0.0 6139262.351265922 -117.00108268834208 55.385019431842004 +4267 3773 -118.35 51.231500000000004 -94286.3618966099 5677919.969539227 +3773 4326 -94286.3618966099 5677919.969539227 -118.35103055600636 51.231462481224675 +4267 3773 -115.65 59.5385 76368.56576126456 6602565.525198437 +3773 4326 76368.56576126456 6602565.525198437 -115.6511528227646 59.53860840893395 +4269 3775 -111.24000000000001 54.495000000000005 -15549.31506636425 6040436.693252089 +3775 4326 -15549.31506636425 6040436.693252089 -111.24000000000001 54.495 +4269 3775 -112.374 49.5405 -99434.22623297655 5490097.576352304 +3775 4326 -99434.22623297655 5490097.576352304 -112.374 49.5405 +4269 3775 -110.10600000000001 59.4495 50705.564205906085 6592424.346893215 +3775 4326 50705.564205906085 6592424.346893215 -110.10600000000001 59.4495 +4269 3776 -114.0 54.495000000000005 0.0 6040410.182018194 +3776 4326 0.0 6040410.182018194 -114.00000000000001 54.495000000000005 +4269 3776 -115.35 49.5405 -97697.43470476051 5490066.157392819 +3776 4326 -97697.43470476051 5490066.157392819 -115.35 49.5405 +4269 3776 -112.65 59.4495 76566.88401019845 6592860.533550745 +3776 4326 76566.88401019845 6592860.533550745 -112.65 59.44949999999997 +4269 3777 -117.0 55.385000000000005 0.0 6139477.2256889725 +3777 4326 0.0 6139477.2256889725 -117.0 55.385 +4269 3777 -118.35 51.231500000000004 -94283.19848592878 5678138.529527494 +3777 4326 -94283.19848592878 5678138.529527494 -118.35 51.231500000000004 +4269 3777 -115.65 59.5385 76365.61692193565 6602773.133696755 +3777 4326 76365.61692193565 6602773.133696755 -115.65 59.5385 +4617 3779 -111.24000000000001 54.495000000000005 -15549.31506636425 6040436.693252089 +3779 4326 -15549.31506636425 6040436.693252089 -111.24001458769246 54.49500735616427 +4617 3779 -112.374 49.5405 -99434.22623297655 5490097.576352304 +3779 4326 -99434.22623297655 5490097.576352304 -112.37401339015251 49.54050687769696 +4617 3779 -110.10600000000001 59.4495 50705.564205906085 6592424.346893215 +3779 4326 50705.564205906085 6592424.346893215 -110.10601634415856 59.44950776369696 +4617 3780 -114.0 54.495000000000005 0.0 6040410.182018194 +3780 4326 0.0 6040410.182018194 -114.00001523870947 54.49500702689406 +4617 3780 -115.35 49.5405 -97697.43470476051 5490066.157392819 +3780 4326 -97697.43470476051 5490066.157392819 -115.35001402335388 49.54050655911568 +4617 3780 -112.65 59.4495 76566.88401019845 6592860.533550745 +3780 4326 76566.88401019845 6592860.533550745 -112.65001702603205 59.44950743440576 +4617 3781 -117.0 55.385000000000005 0.0 6139477.2256889725 +3781 4326 0.0 6139477.2256889725 -117.00001623663339 55.38500669840909 +4617 3781 -118.35 51.231500000000004 -94283.19848592878 5678138.529527494 +3781 4326 -94283.19848592878 5678138.529527494 -118.3500150883082 51.23150632639599 +4617 3781 -115.65 59.5385 76365.61692193565 6602773.133696755 +3781 4326 76365.61692193565 6602773.133696755 -115.65001782552419 59.53850703613731 +4763 3783 -130.08499999999998 -25.07 17021.680785850935 15339.375259661041 +3783 4326 17021.680785850935 15339.375259661041 -130.08499999999998 -25.070000000000004 +4763 3783 -130.1525 -25.133 10213.457871110451 8360.295751430585 +3783 4326 10213.457871110451 8360.295751430585 -130.1525 -25.133000000000003 +4763 3783 -130.01749999999998 -25.007 23836.870276715556 22315.001122789447 +3783 4326 23836.870276715556 22315.001122789447 -130.01749999999998 -25.007000000000005 +4729 3784 -130.08499999999998 -25.07 390566.2714225887 7226824.7536314 +3784 4326 390566.2714225887 7226824.7536314 -130.08465018008846 -25.069963345769164 +4729 3784 -130.1525 -25.133 383817.1516642665 7219791.379531009 +3784 4326 383817.1516642665 7219791.379531009 -130.15215286761116 -25.13296472513781 +4729 3784 -130.01749999999998 -25.007 397322.39494797215 7233854.468670746 +3784 4326 397322.39494797215 7233854.468670746 -130.01714749537743 -25.00696196813888 +4167 3788 166.24 -49.465 3517397.161193351 4518630.054435464 +3788 4326 3517397.161193351 4518630.054435464 166.24 -49.464999999999996 +4167 3788 165.619 -50.9635 3473233.746710506 4351905.700963394 +3788 4326 3473233.746710506 4351905.700963394 165.619 -50.963499999999996 +4167 3788 166.86100000000002 -47.966499999999996 3564293.768771407 4684938.447018764 +3788 4326 3564293.768771407 4684938.447018764 166.86100000000002 -47.96649999999999 +4167 3789 169.125 -52.545 3508480.1077388055 4176005.5819595614 +3789 4326 3508480.1077388055 4176005.5819595614 169.125 -52.54499999999998 +4167 3789 168.6975 -52.8015 3479597.994018029 4147426.6775855077 +3789 4326 3479597.994018029 4147426.6775855077 168.6975 -52.801500000000004 +4167 3789 169.5525 -52.2885 3537700.0481229634 4204411.228488627 +3789 4326 3537700.0481229634 4204411.228488627 169.5525 -52.2885 +4167 3790 178.885 -48.730000000000004 3491539.854577135 4600391.90924153 +3790 4326 3491539.854577135 4600391.90924153 178.885 -48.73 +4167 3790 178.4485 -49.801 3460296.923858581 4481141.209420987 +3790 4326 3460296.923858581 4481141.209420987 178.4485 -49.801 +4167 3790 179.32150000000001 -47.659 3524149.718086437 4719437.092050357 +3790 4326 3524149.718086437 4719437.092050357 179.32150000000001 -47.659 +4167 3791 -178.345 -30.295 3466811.028954166 6647133.990665928 +3791 4326 3466811.028954166 6647133.990665928 -178.345 -30.294999999999998 +4167 3791 -178.9975 -31.4335 3405165.9740105146 6520531.604920638 +3791 4326 3405165.9740105146 6520531.604920638 -178.9975 -31.43350000000001 +4167 3791 -177.6925 -29.1565 3529917.2962567625 6773345.563694371 +3791 4326 3529917.2962567625 6773345.563694371 -177.6925 -29.156499999999994 +4167 3793 -176.39499999999998 -43.97 3508425.8927618545 5129505.628586459 +3793 4326 3508425.8927618545 5129505.628586459 -176.39499999999998 -43.96999999999999 +4167 3793 -177.1645 -44.573 3447218.570864429 5062292.298150725 +3793 4326 3447218.570864429 5062292.298150725 -177.1645 -44.57300000000001 +4167 3793 -175.6255 -43.367 3570881.872855623 5196136.313526676 +3793 4326 3570881.872855623 5196136.313526676 -175.6255 -43.367 +4765 3794 14.995000000000001 46.150000000000006 499613.7700829212 112247.58014518023 +3794 4326 499613.7700829212 112247.58014518023 14.995000000000003 46.15 +4765 3794 13.541500000000001 45.493 386003.9333674895 40265.60257693473 +3794 4326 386003.9333674895 40265.60257693473 13.541500000000001 45.492999999999995 +4765 3794 16.4485 46.807 610551.4091565845 186291.7260091491 +3794 4326 610551.4091565845 186291.7260091491 16.4485 46.80700000000001 +4267 3795 -80.96000000000001 22.314999999999998 504121.10578610544 276421.27844351594 +3795 4326 504121.10578610544 276421.27844351594 -80.95984071103503 22.315484956614156 +4267 3795 -84.605 21.473499999999998 126391.7272800376 187718.85629687697 +3795 4326 126391.7272800376 187718.85629687697 -84.60493106655699 21.474034254588968 +4267 3795 -77.315 23.1565 877340.3066734297 374215.8975487636 +3795 4326 877340.3066734297 374215.8975487636 -77.31475073628164 23.156934716190808 +4267 3796 -76.38 20.634999999999998 547245.1650034637 220152.4235366802 +3796 4326 547245.1650034637 220152.4235366802 -76.37974028673847 20.63554337225657 +4267 3796 -78.459 19.8565 329715.0829400245 134760.4431432897 +3796 4326 329715.0829400245 134760.4431432897 -78.45879181193114 19.85708717025302 +4267 3796 -74.30099999999999 21.4135 762554.9980373618 308325.5171306875 +3796 4326 762554.9980373618 308325.5171306875 -74.30068886759611 21.41399953311303 +4267 3797 -68.475 53.805 900940.8182122742 1092212.2199822983 +3797 4326 900940.8182122742 1092212.2199822983 -68.47441957124278 53.8051063439705 +4267 3797 -78.71249999999999 45.871500000000005 124905.80088909063 246347.64443459507 +3797 4326 124905.80088909063 246347.64443459507 -78.71237451769805 45.871591570788986 +4267 3797 -58.2375 61.7385 1439112.5525187515 2037999.9900902587 +3797 4326 1439112.5525187515 2037999.9900902587 -58.236260203515194 61.738689965514276 +4269 3798 -68.475 53.805 900937.2813111262 1092212.6596049047 +3798 4326 900937.2813111262 1092212.6596049047 -68.475 53.805 +4269 3798 -78.71249999999999 45.871500000000005 124926.10662470723 246348.1933624733 +3798 4326 124926.10662470723 246348.1933624733 -78.71249999999999 45.87149999999999 +4269 3798 -58.2375 61.7385 1439087.5333369633 2037984.3831757545 +3798 4326 1439087.5333369633 2037984.3831757545 -58.237500000000004 61.73849999999999 +4617 3799 -68.475 53.805 900937.2813111262 1092212.6596049047 +3799 4326 900937.2813111262 1092212.6596049047 -68.47500031565099 53.80501058856846 +4617 3799 -78.71249999999999 45.871500000000005 124926.10662470723 246348.1933624733 +3799 4326 124926.10662470723 246348.1933624733 -78.71250321398003 45.871508994276375 +4617 3799 -58.2375 61.7385 1439087.5333369633 2037984.3831757545 +3799 4326 1439087.5333369633 2037984.3831757545 -58.23749646438637 61.738511643516546 +4267 3800 -119.25 56.44 46258.10310109847 6256968.351434958 +3800 4326 46258.10310109847 6256968.351434958 -119.25120295957628 56.440015048041985 +4267 3800 -119.925 53.236000000000004 5007.465852338448 5900079.7492714105 +3800 4326 5007.465852338448 5900079.7492714105 -119.92613602595611 53.23596537304847 +4267 3800 -118.575 59.644 80358.75364966448 6614404.637186731 +3800 4326 80358.75364966448 6614404.637186731 -118.57628618329926 59.644083518181944 +4269 3801 -119.25 56.44 46256.40122584206 6257181.704696059 +3801 4326 46256.40122584206 6257181.704696059 -119.25000000000001 56.44 +4269 3801 -119.925 53.236000000000004 5007.291499956597 5900296.996278018 +3801 4326 5007.291499956597 5900296.996278018 -119.925 53.236000000000004 +4269 3801 -118.575 59.644 80355.6458864481 6614612.014429 +3801 4326 80355.6458864481 6614612.014429 -118.575 59.64399999999999 +4617 3802 -119.25 56.44 46256.40122584206 6257181.704696059 +3802 4326 46256.40122584206 6257181.704696059 -119.25001714385165 56.4400064469356 +4617 3802 -119.925 53.236000000000004 5007.291499956597 5900296.996278018 +3802 4326 5007.291499956597 5900296.996278018 -119.92501603342153 53.23600623482592 +4617 3802 -118.575 59.644 80355.6458864481 6614612.014429 +3802 4326 80355.6458864481 6614612.014429 -118.57501856287413 59.64400663462978 +4258 3812 4.45 50.504999999999995 655767.9695613892 632694.7821491045 +3812 4326 655767.9695613892 632694.7821491045 4.45 50.504999999999995 +4258 3812 2.695 49.6005 529028.4505441122 533434.1349227261 +3812 4326 529028.4505441122 533434.1349227261 2.6949999999999994 49.6005 +4258 3812 6.205 51.4095 777748.1865885115 734907.8566263703 +3812 4326 777748.1865885115 734907.8566263703 6.205 51.4095 +4269 3814 -89.87 32.51 488725.91080891585 1301115.1174180263 +3814 4326 488725.91080891585 1301115.1174180263 -89.87 32.51 +4269 3814 -91.47200000000001 30.26 334300.6688884102 1052933.9393655262 +3814 4326 334300.6688884102 1052933.9393655262 -91.47200000000001 30.26000000000001 +4269 3814 -88.268 34.76 635665.6434956547 1551628.392431662 +3814 4326 635665.6434956547 1551628.392431662 -88.268 34.76 +4152 3815 -89.87 32.51 488725.91080891585 1301115.1174180263 +3815 4326 488725.91080891585 1301115.1174180263 -89.87000557473034 32.51000596304434 +4152 3815 -91.47200000000001 30.26 334300.6688884102 1052933.9393655262 +3815 4326 334300.6688884102 1052933.9393655262 -91.47200590511257 30.260005430311278 +4152 3815 -88.268 34.76 635665.6434956547 1551628.392431662 +3815 4326 635665.6434956547 1551628.392431662 -88.26800525306292 34.76000648995324 +4759 3816 -89.87 32.51 488725.91080891585 1301115.1174180263 +3816 4326 488725.91080891585 1301115.1174180263 -89.87 32.51 +4759 3816 -91.47200000000001 30.26 334300.6688884102 1052933.9393655262 +3816 4326 334300.6688884102 1052933.9393655262 -91.47200000000001 30.26000000000001 +4759 3816 -88.268 34.76 635665.6434956547 1551628.392431662 +3816 4326 635665.6434956547 1551628.392431662 -88.268 34.76 +3824 3825 119.0 21.64 250000.0 2393699.0561337546 +3825 4326 250000.0 2393699.0561337546 119.0 21.640000000000004 +3824 3825 118.1 18.930999999999997 155204.58230972697 2094062.5501032795 +3825 4326 155204.58230972697 2094062.5501032795 118.10000000000001 18.930999999999994 +3824 3825 119.9 24.348999999999997 341321.30270667264 2693970.48873876 +3825 4326 341321.30270667264 2693970.48873876 119.89999999999999 24.348999999999997 +3824 3826 121.025 23.564999999999998 252552.01656398323 2606849.700130799 +3826 4326 252552.01656398323 2606849.700130799 121.025 23.56499999999999 +3824 3826 120.09349999999999 20.7255 155586.06128049566 2292720.5941477986 +3826 4326 155586.06128049566 2292720.5941477986 120.09349999999998 20.7255 +3824 3826 121.9565 26.4045 345425.4132534874 2921711.6468027835 +3826 4326 345425.4132534874 2921711.6468027835 121.9565 26.4045 +3821 3827 119.515 23.475 302607.9677948664 2596986.0133840735 +3821 3827 119.2765 23.1645 278310.361440331 2562534.6094957376 +3821 3827 119.7535 23.7855 326790.6081780867 2631480.946190183 +3821 3828 121.025 23.605 252551.25246673924 2611288.373748388 +3821 3828 120.09349999999999 22.0435 156427.63244769265 2438659.6491932594 +3821 3828 121.9565 25.1665 346421.8156835597 2784571.5179447476 +4236 3829 120.655 23.605 260709.532658413 2612491.685807027 +3829 4326 260709.532658413 2612491.685807027 120.66311175124626 23.603249479998198 +4236 3829 119.3905 22.0435 127344.42118738918 2442082.761192118 +3829 4326 127344.42118738918 2442082.761192118 119.39848630136093 22.041789554908767 +4236 3829 121.9195 25.1665 391105.73893036763 2783858.511151588 +3829 4326 391105.73893036763 2783858.511151588 121.9277422925854 25.164703575359642 +4326 3832 -164.655 3.335000000000001 5047782.310020985 368976.4062133366 +4326 3832 108.3555 -53.6665 -4635844.534340481 -7072800.277954165 +4326 3832 -77.66550000000001 60.3365 14731409.15438246 8437875.278129047 +4179 3833 10.96 52.215 2633958.823488199 5789179.22084104 +4179 3833 10.024 50.401500000000006 2572803.71381943 5586106.566101754 +4179 3833 11.896 54.028499999999994 2689755.8405398126 5993075.783405378 +4178 3834 10.96 52.215 2633958.823488199 5789179.22084104 +3834 4326 2633958.823488199 5789179.22084104 10.958140310716777 52.214573608568806 +4178 3834 10.024 50.401500000000006 2572803.71381943 5586106.566101754 +3834 4326 2572803.71381943 5586106.566101754 10.022209482351567 50.40103890512051 +4178 3834 11.896 54.028499999999994 2689755.8405398126 5993075.783405378 +3834 4326 2689755.8405398126 5993075.783405378 11.894063911152717 54.028109409575514 +4178 3835 15.005 50.260000000000005 3500356.5477141365 5569865.18032614 +3835 4326 3500356.5477141365 5569865.18032614 15.003266627980128 50.259618570339754 +4178 3835 12.3005 46.228 3291748.1279653674 5125063.029669655 +3835 4326 3291748.1279653674 5125063.029669655 12.298895747855356 46.22754129637887 +4178 3835 17.709500000000002 54.292 3676413.80376588 6021911.577356551 +3835 4326 3676413.80376588 6021911.577356551 17.707608402605082 54.291701876997436 +4178 3836 20.45 47.900000000000006 4458876.17653118 5307548.551759836 +3836 4326 4458876.17653118 5307548.551759836 20.44835286223754 47.89966695809953 +4178 3836 18.245 45.956 4286418.595787477 5094977.431710276 +3836 4326 4286418.595787477 5094977.431710276 18.24340461874473 45.95561571760172 +4178 3836 22.654999999999998 49.844 4619039.189395658 5524906.837360613 +3836 4326 4619039.189395658 5524906.837360613 22.65329762688514 49.8437202420883 +4179 3837 10.21 50.955 3585021.4425204406 5647876.100302413 +4179 3837 9.949 50.4105 3567458.726292648 5587036.941448476 +4179 3837 10.471 51.499500000000005 3602147.3032476585 5708783.181510262 +4179 3838 12.0 51.855000000000004 4500000.0 5747310.9234197 +4179 3838 10.65 49.2585 4401729.994885185 5459349.248434758 +4179 3838 13.35 54.4515 4587565.41501431 6037117.381924261 +4179 3839 27.0 44.775000000000006 9500000.0 4960027.7056529485 +3839 4326 9500000.0 4960027.7056529485 26.99850451719254 44.774725557172424 +4179 3839 25.65 41.6295 9387501.889668146 4611447.547879248 +3839 4326 9387501.889668146 4611447.547879248 25.648569093476855 41.62915291088174 +4179 3839 28.35 47.920500000000004 9600899.654397704 5310563.8497952 +3839 4326 9600899.654397704 5310563.8497952 28.348427795679246 47.92030040031664 +4179 3840 29.119999999999997 44.39 10429881.293723451 4917621.074596031 +3840 4326 10429881.293723451 4917621.074596031 29.118541869037003 44.38974808324991 +4179 3840 28.562 43.445 10383591.192363856 4813247.665570016 +3840 4326 10383591.192363856 4813247.665570016 28.56055924329608 43.44472356405779 +4179 3840 29.677999999999997 45.335 10474759.28056855 5022313.616660592 +3840 4326 10474759.28056855 5022313.616660592 29.676523588595032 45.33477284873751 +4178 3841 18.0 48.095 6500000.0 5329084.572472365 +3841 4326 6500000.0 5329084.572472365 17.998338951784746 48.09463369626989 +4178 3841 16.65 45.975500000000004 6395376.969737264 5094338.843498589 +3841 4326 6395376.969737264 5094338.843498589 16.64840062338829 45.97509361317548 +4178 3841 19.35 50.2145 6596357.947224965 5565676.35740091 +3841 4326 6596357.947224965 5565676.35740091 19.348270562304734 50.21417540505664 +4179 3844 25.835 45.855000000000004 564834.9495593779 484226.4412308502 +3844 4326 564834.9495593779 484226.4412308502 25.833459760316064 45.8547277494283 +4179 3844 20.817500000000003 43.6815 162762.91463824344 251058.28037829718 +3844 4326 162762.91463824344 251058.28037829718 20.81596896189681 43.68112024056895 +4179 3844 30.8525 48.0285 936309.0466379289 741799.2640761927 +3844 4326 936309.0466379289 741799.2640761927 30.85095979918976 48.02833868521102 +4619 3845 11.92 58.51 1535800.3808107197 6487622.600829633 +3845 4326 1535800.3808107197 6487622.600829633 11.919999999999998 58.509999999999984 +4619 3845 11.029 57.412 1483362.6134855987 6365200.55230243 +3845 4326 1483362.6134855987 6365200.55230243 11.028999999999998 57.412 +4619 3845 12.811 59.608 1584977.265627268 6610735.225169814 +3845 4326 1584977.265627268 6610735.225169814 12.811000000000002 59.60799999999999 +4619 3846 13.625 59.835 1503899.1190630596 6635063.37939133 +3846 4326 1503899.1190630596 6635063.37939133 13.625 59.834999999999994 +4619 3846 11.9915 55.7355 1401752.7322830688 6179591.80889616 +3846 4326 1401752.7322830688 6179591.80889616 11.9915 55.73550000000001 +4619 3846 15.2585 63.9345 1583525.8060682965 7093040.458948618 +3846 4326 1583525.8060682965 7093040.458948618 15.258499999999996 63.9345 +4619 3847 15.695 61.565000000000005 1494150.1621192116 6827829.289432215 +3847 4326 1494150.1621192116 6827829.289432215 15.694999999999997 61.565 +4619 3847 13.8635 56.511500000000005 1380462.6702051037 6266575.546719264 +3847 4326 1380462.6702051037 6266575.546719264 13.863499999999998 56.511500000000005 +4619 3847 17.5265 66.61850000000001 1576266.572170012 7392233.160720764 +3847 4326 1576266.572170012 7392233.160720764 17.5265 66.61850000000001 +4619 3848 18.15 62.7 1504880.2472013063 6954318.9860490635 +3848 4326 1504880.2472013063 6954318.9860490635 18.15 62.699999999999996 +4619 3848 16.287 57.444 1393849.580523166 6370107.674423577 +3848 4326 1393849.580523166 6370107.674423577 16.287 57.44399999999999 +4619 3848 20.012999999999998 67.956 1582060.0736426278 7541644.585765662 +3848 4326 1582060.0736426278 7541644.585765662 20.012999999999995 67.956 +4619 3849 20.299999999999997 66.22 1499818.4298337605 7346738.684169639 +3849 4326 1499818.4298337605 7346738.684169639 20.3 66.22 +4619 3849 18.59 63.654999999999994 1415093.7374445489 7061902.474677345 +3849 4326 1415093.7374445489 7061902.474677345 18.59 63.654999999999994 +4619 3849 22.009999999999998 68.785 1568925.6787814435 7633765.3066388145 +3849 4326 1568925.6787814435 7633765.3066388145 22.009999999999994 68.785 +4619 3850 22.755000000000003 66.91 1508819.717219685 7423694.633569596 +3850 4326 1508819.717219685 7423694.633569596 22.755 66.90999999999998 +4619 3850 21.4815 65.407 1450190.471981906 7256506.725439628 +3850 4326 1450190.471981906 7256506.725439628 21.4815 65.40699999999997 +4619 3850 24.0285 68.413 1560586.5378415247 7592035.063350355 +3850 4326 1560586.5378415247 7592035.063350355 24.028500000000005 68.413 +4167 3851 174.7 -40.915 3142938.475197602 7008029.855772331 +3851 4326 3142938.475197602 7008029.855772331 174.7 -40.91499999999997 +4167 3851 162.01 -54.4465 2268749.6567721413 5447056.017734335 +3851 4326 2268749.6567721413 5447056.017734335 162.01 -54.4465 +4167 3851 -172.60999999999999 -27.3835 4452690.985970442 8401352.92989572 +3851 4326 4452690.985970442 8401352.92989572 -172.61 -27.383499999999994 +4764 3852 157.0 -78.5 500000.0 1303776.6596786033 +3852 4326 500000.0 1303776.6596786033 157.0 -78.5 +4764 3852 146.2 -80.75 307025.10880222055 1034652.13220682 +3852 4326 307025.10880222055 1034652.13220682 146.2 -80.75 +4764 3852 167.8 -76.25 785096.4250607286 1528569.9722007094 +3852 4326 785096.4250607286 1528569.9722007094 167.8 -76.25 +4619 3854 18.43 59.480000000000004 121272.41418603428 95547.02896563243 +3854 4326 121272.41418603428 95547.02896563243 18.43 59.480000000000004 +4619 3854 17.368 58.769 60267.64980046275 16490.43878957443 +3854 4326 60267.64980046275 16490.43878957443 17.368 58.769 +4619 3854 19.492 60.191 179740.87677706056 175563.78685078304 +3854 4326 179740.87677706056 175563.78685078304 19.492 60.19100000000001 +4326 3857 0.0 0.0 0.0 0.0 +4326 3857 -162.0 -76.554 -18033757.508510318 -13637019.713389529 +4326 3857 162.0 76.554 18033757.508510318 13637019.713389529 +10690 3873 19.369999999999997 60.21 19520514.941698853 6677527.260658987 +3873 4326 19520514.941698853 6677527.260658987 19.369999999999965 60.21000000000001 +10690 3873 19.253 60.092999999999996 19514077.7358396 6664461.178097184 +3873 4326 19514077.7358396 6664461.178097184 19.253000000000004 60.092999999999996 +10690 3873 19.487 60.327000000000005 19526905.813956395 6690604.896490097 +3873 4326 19526905.813956395 6690604.896490097 19.486999999999988 60.327000000000005 +10690 3874 20.0 60.2 20500000.0 6676355.616312669 +3874 4326 20500000.0 6676355.616312669 20.0 60.20000000000001 +10690 3874 19.55 59.948 20474850.73303241 6648364.8886981215 +3874 4326 20474850.73303241 6648364.8886981215 19.549999999999983 59.948 +10690 3874 20.45 60.452 20524766.556927163 6704517.513746917 +3874 4326 20524766.556927163 6704517.513746917 20.449999999999992 60.45199999999999 +10690 3875 21.0 64.58500000000001 21500000.0 7165070.984975636 +3875 4326 21500000.0 7165070.984975636 21.0 64.58500000000001 +10690 3875 20.55 60.3145 21475128.839177817 6689197.673625805 +3875 4326 21475128.839177817 6689197.673625805 20.550000000000022 60.31450000000001 +10690 3875 21.45 68.85549999999999 21518122.596409578 7641372.776043423 +3875 4326 21518122.596409578 7641372.776043423 21.45000000000002 68.85549999999998 +10690 3876 22.0 64.535 22500000.0 7159496.687667551 +3876 4326 22500000.0 7159496.687667551 22.0 64.53499999999998 +10690 3876 21.55 60.2375 22475070.324367203 6680618.701709228 +3876 4326 22475070.324367203 6680618.701709228 21.55000000000002 60.23749999999999 +10690 3876 22.45 68.8325 22518141.388317466 7638807.252067053 +3876 4326 22518141.388317466 7638807.252067053 22.450000000000006 68.83249999999998 +10690 3877 23.0 64.245 23500000.0 7127166.516923541 +3877 4326 23500000.0 7127166.516923541 23.0 64.245 +10690 3877 22.55 60.1995 23475041.46373763 6676384.960082177 +3877 4326 23475041.46373763 6676384.960082177 22.549999999999994 60.19949999999999 +10690 3877 23.45 68.2905 23518583.35690099 7578352.150334439 +3877 4326 23518583.35690099 7578352.150334439 23.449999999999996 68.29049999999998 +10690 3878 24.0 64.35 24500000.0 7138872.119601392 +3878 4326 24500000.0 7138872.119601392 24.000000000000004 64.35 +10690 3878 23.55 60.309 24475124.65803749 6688584.8866154235 +3878 4326 24475124.65803749 6688584.8866154235 23.55 60.30899999999999 +10690 3878 24.45 68.391 24518501.531618495 7589561.702908624 +3878 4326 24518501.531618495 7589561.702908624 24.44999999999997 68.391 +10690 3879 25.0 64.42 25500000.0 7146675.948656818 +3879 4326 25500000.0 7146675.948656818 25.0 64.41999999999999 +10690 3879 24.55 60.388 25475184.736693434 6697386.785107177 +3879 4326 25475184.736693434 6697386.785107177 24.549999999999976 60.388000000000005 +10690 3879 25.45 68.452 25518451.83828833 7596365.57688542 +3879 4326 25518451.83828833 7596365.57688542 25.44999999999996 68.452 +10690 3880 26.0 65.06 26500000.0 7218028.700916069 +3880 4326 26500000.0 7218028.700916069 26.0 65.06 +10690 3880 25.55 60.668 26475398.057975065 6728584.228499791 +3880 4326 26475398.057975065 6728584.228499791 25.54999999999999 60.667999999999985 +10690 3880 26.45 69.452 26517634.204347085 7707911.494540701 +3880 4326 26517634.204347085 7707911.494540701 26.450000000000042 69.452 +10690 3881 27.0 65.205 27500000.0 7234195.418049183 +3881 4326 27500000.0 7234195.418049183 27.0 65.205 +10690 3881 26.55 60.8445 27475532.83291437 6748250.431504073 +3881 4326 27475532.83291437 6748250.431504073 26.54999999999999 60.844500000000004 +10690 3881 27.45 69.5655 27517541.053056344 7720572.777543769 +3881 4326 27517541.053056344 7720572.777543769 27.45000000000002 69.5655 +10690 3882 28.0 65.255 28500000.0 7239770.22109902 +3882 4326 28500000.0 7239770.22109902 28.0 65.255 +10690 3882 27.55 60.9035 28475577.937711265 6754824.5184650915 +3882 4326 28475577.937711265 6754824.5184650915 27.550000000000026 60.90349999999999 +10690 3882 28.45 69.6065 28517507.38646415 7725146.497413838 +3882 4326 28517507.38646415 7725146.497413838 28.45000000000003 69.6065 +10690 3883 29.0 65.375 29500000.0 7253149.900692343 +3883 4326 29500000.0 7253149.900692343 29.000000000000004 65.375 +10690 3883 28.55 61.3835 29475945.86330787 6808310.753929716 +3883 4326 29475945.86330787 6808310.753929716 28.550000000000008 61.3835 +10690 3883 29.45 69.3665 29517704.32913935 7698373.809114772 +3883 4326 29517704.32913935 7698373.809114772 29.450000000000045 69.3665 +10690 3884 30.0 64.705 30500000.0 7178449.453771112 +3884 4326 30500000.0 7178449.453771112 29.999999999999996 64.705 +10690 3884 29.55 61.7575 30476233.72641025 6849988.067773847 +3884 4326 30476233.72641025 6849988.067773847 29.549999999999983 61.7575 +10690 3884 30.45 67.6525 30519101.43896741 7507194.19544731 +3884 4326 30519101.43896741 7507194.19544731 30.45000000000004 67.65249999999999 +10690 3885 31.045 63.175 31502266.61611993 7007891.3962724535 +3885 4326 31502266.61611993 7007891.3962724535 31.045000000000027 63.175000000000004 +10690 3885 30.5545 62.189499999999995 31476801.83091858 6898129.919650225 +3885 4326 31476801.83091858 6898129.919650225 30.554500000000033 62.189499999999995 +10690 3885 31.5355 64.1605 31526052.308601864 7117855.991242048 +3885 4326 31526052.308601864 7117855.991242048 31.535500000000027 64.1605 +3889 3890 40.394999999999996 33.945 628913.0275874406 3756934.13489945 +3890 4326 628913.0275874406 3756934.13489945 40.394999999999996 33.94500000000001 +3889 3890 38.9505 31.4205 495295.292915851 3476206.4774848316 +3890 4326 495295.292915851 3476206.4774848316 38.9505 31.420499999999997 +3889 3890 41.8395 36.4695 754422.6881289941 4039774.0008925465 +3890 4326 754422.6881289941 4039774.0008925465 41.8395 36.4695 +3889 3891 45.0 33.225 500000.0 3676230.9160077395 +3891 4326 500000.0 3676230.9160077395 45.0 33.225 +3889 3891 42.3 29.476499999999998 238185.01698557095 3263816.5353348264 +3891 4326 238185.01698557095 3263816.5353348264 42.3 29.476499999999994 +3889 3891 47.7 36.9735 740343.5453584031 4095340.0565520297 +3891 4326 740343.5453584031 4095340.0565520297 47.7 36.973499999999994 +3889 3892 48.375 30.3 247546.11830411004 3354947.5483895447 +3892 4326 247546.11830411004 3354947.5483895447 48.375 30.29999999999999 +3889 3892 48.0375 29.67 213267.5093029985 3285891.0365788094 +3892 4326 213267.5093029985 3285891.0365788094 48.0375 29.669999999999995 +3889 3892 48.7125 30.93 281434.4401366301 3424087.5279359827 +3892 4326 281434.4401366301 3424087.5279359827 48.7125 30.92999999999999 +4230 3893 43.7 33.225 539100.5946692345 468746.94502889836 +3893 4326 539100.5946692345 468746.94502889836 43.69996378010999 33.22391522058213 +4230 3893 39.281 29.476499999999998 99267.50690348505 71670.16024369517 +3893 4326 99267.50690348505 71670.16024369517 39.28086654915739 29.4753513001113 +4230 3893 48.119 36.9735 944085.158529257 882105.7005046973 +3893 4326 944085.158529257 882105.7005046973 48.11906997704849 36.97249068830743 +3906 3912 14.995000000000001 46.150000000000006 499613.81690065074 111728.30486025754 +3912 4326 499613.81690065074 111728.30486025754 14.99020681466189 46.149695542628805 +3906 3912 13.541500000000001 45.493 386017.7385578714 39754.33198758587 +3912 4326 386017.7385578714 39754.33198758587 13.536908276225576 45.492727314145455 +3906 3912 16.4485 46.807 610537.9956136482 185764.17085667234 +3912 4326 610537.9956136482 185764.17085667234 16.443501275694995 46.80666579958892 +4139 3920 -64.565 18.53 334808.06166330964 2049418.2858415279 +3920 4326 334808.06166330964 2049418.2858415279 -64.56461304145932 18.528016051537456 +4139 3920 -64.8485 18.305 304621.88374744105 2024798.3965573835 +3920 4326 304621.88374744105 2024798.3965573835 -64.84811637079555 18.303026910284416 +4139 3920 -64.2815 18.755000000000003 364915.1076382387 2074080.4490663602 +3920 4326 364915.1076382387 2074080.4490663602 -64.28110970693601 18.75300526567353 +4171 3942 4.285 42.19 1806133.464641465 1221898.9166579738 +3942 4326 1806133.464641465 1221898.9166579738 4.284999999999999 42.190000000000005 +4171 3942 -0.5255 41.398 1405265.0577709535 1139210.4191105175 +3942 4326 1405265.0577709535 1139210.4191105175 -0.5254999999999994 41.39800000000002 +4171 3942 9.0955 42.982 2196788.6567191663 1326769.65018143 +3942 4326 2196788.6567191663 1326769.65018143 9.0955 42.981999999999985 +4171 3943 2.9300000000000006 43.17 1694308.3555044443 2218886.832639388 +3943 4326 1694308.3555044443 2218886.832639388 2.93 43.17 +4171 3943 -1.318 42.414 1344746.925290598 2144039.2831746098 +3943 4326 1344746.925290598 2144039.2831746098 -1.3179999999999987 42.414 +4171 3943 7.178 43.925999999999995 2035394.4909423827 2311217.6582577066 +3943 4326 2035394.4909423827 2311217.6582577066 7.177999999999999 43.926 +4171 3944 2.96 43.96 1696789.8710242882 3195556.6834046254 +3944 4326 1696789.8710242882 3195556.6834046254 2.96 43.96 +4171 3944 -1.315 43.024 1348427.416655981 3100766.4661240424 +3944 4326 1348427.416655981 3100766.4661240424 -1.3149999999999986 43.024 +4171 3944 7.235 44.896 2034385.148431069 3308146.661596619 +3944 4326 2034385.148431069 3308146.661596619 7.2349999999999985 44.89599999999998 +4171 3945 3.125 45.0 1709855.0089266587 4200007.601723443 +3945 4326 1709855.0089266587 4200007.601723443 3.1250000000000013 45.00000000000001 +4171 3945 -1.0015 44.1 1379712.718559944 4107904.098701538 +3945 4326 1379712.718559944 4107904.098701538 -1.0014999999999992 44.09999999999999 +4171 3945 7.251499999999999 45.9 2029789.0888838554 4308676.189895353 +3945 4326 2029789.0888838554 4308676.189895353 7.2515 45.9 +4171 3946 2.4750000000000005 46.0 1659335.5353398635 5200134.0201074025 +3946 4326 1659335.5353398635 5200134.0201074025 2.475000000000001 46.0 +4171 3946 -1.7414999999999998 45.1 1327005.0597660886 5111081.736375026 +3946 4326 1327005.0597660886 5111081.736375026 -1.7414999999999992 45.1 +4171 3946 6.6915000000000004 46.9 1981194.0581817513 5306557.143128601 +3946 4326 1981194.0581817513 5306557.143128601 6.6915 46.900000000000006 +4171 3947 1.4299999999999997 47.0 1580610.273100607 6201196.378340442 +3947 4326 1580610.273100607 6201196.378340442 1.4299999999999988 47.0 +4171 3947 -4.1499999999999995 46.1 1147882.6448475446 6125171.695773245 +3947 4326 1147882.6448475446 6125171.695773245 -4.1499999999999995 46.1 +4171 3947 7.009999999999998 47.9 1999705.9347921829 6307729.436931695 +3947 4326 1999705.9347921829 6307729.436931695 7.009999999999999 47.89999999999999 +4171 3948 1.6800000000000006 48.0 1601507.759418362 7200843.179309284 +3948 4326 1601507.759418362 7200843.179309284 1.6800000000000022 47.99999999999999 +4171 3948 -4.215 47.1 1153059.3437116276 7125552.062888663 +3948 4326 1153059.3437116276 7125552.062888663 -4.215000000000001 47.099999999999994 +4171 3948 7.575 48.9 2035246.468431578 7310024.549210299 +3948 4326 2035246.468431578 7310024.549210299 7.574999999999999 48.89999999999999 +4171 3949 1.6800000000000006 49.0 1603426.3502429728 8200839.6210153755 +3949 4326 1603426.3502429728 8200839.6210153755 1.6799999999999995 49.000000000000014 +4171 3949 -4.215 48.1 1163408.0042460614 8125441.575353767 +3949 4326 1163408.0042460614 8125441.575353767 -4.215 48.100000000000016 +4171 3949 7.575 49.9 2028501.4374461283 8309993.614527015 +3949 4326 2028501.4374461283 8309993.614527015 7.574999999999999 49.900000000000006 +4171 3950 3.025 50.07 1701789.6383209554 9207785.717349734 +3950 4326 1701789.6383209554 9207785.717349734 3.025 50.06999999999999 +4171 3950 -1.5244999999999997 49.107 1369833.7253760097 9110674.247290418 +3950 4326 1369833.7253760097 9110674.247290418 -1.5244999999999995 49.107 +4171 3950 7.5745000000000005 51.033 2020716.1643811618 9324717.284941081 +3950 4326 2020716.1643811618 9324717.284941081 7.574500000000002 51.03300000000002 +4269 3968 -79.495 38.0 439.0625047402158 221966.39757750247 +3968 4326 439.0625047402158 221966.39757750247 -79.495 37.99999999999999 +4269 3968 -83.26150000000001 36.686 -336141.30723252974 82978.97219532465 +3968 4326 -336141.30723252974 82978.97219532465 -83.26150000000001 36.68599999999999 +4269 3968 -75.7285 39.314 325151.16044344066 374431.10978461226 +3968 4326 325151.16044344066 374431.10978461226 -75.7285 39.31400000000002 +4152 3969 -79.495 38.0 439.0625047402158 221966.39757750247 +3969 4326 439.0625047402158 221966.39757750247 -79.49500286413488 38.00000749901961 +4152 3969 -83.26150000000001 36.686 -336141.30723252974 82978.97219532465 +3969 4326 -336141.30723252974 82978.97219532465 -83.26150392069589 36.686007084935035 +4152 3969 -75.7285 39.314 325151.16044344066 374431.10978461226 +3969 4326 325151.16044344066 374431.10978461226 -75.72850176916877 39.314007901758174 +4759 3970 -79.495 38.0 439.0625047402158 221966.39757750247 +3970 4326 439.0625047402158 221966.39757750247 -79.495 37.99999999999999 +4759 3970 -83.26150000000001 36.686 -336141.30723252974 82978.97219532465 +3970 4326 -336141.30723252974 82978.97219532465 -83.26150000000001 36.68599999999999 +4759 3970 -75.7285 39.314 325151.16044344066 374431.10978461226 +3970 4326 325151.16044344066 374431.10978461226 -75.7285 39.31400000000002 +4326 3976 0.0 -75.0 0.0 1633879.4974707854 +4326 3976 -162.0 -88.5 -50215.226813879104 -154546.57688059701 +4326 3976 162.0 -61.5 973412.6219304653 -2995856.0013939915 +4269 3978 -90.86999999999999 62.334999999999994 207455.49567943427 1463644.0685560266 +3978 4326 207455.49567943427 1463644.0685560266 -90.86999999999999 62.334999999999994 +4269 3978 -135.99599999999998 40.6225 -3366351.200617633 175305.45298727223 +3978 4326 -3366351.200617633 175305.45298727223 -135.99599999999998 40.62250000000001 +4269 3978 -45.744 84.04749999999999 551224.8417345536 4090679.6373405783 +3978 4326 551224.8417345536 4090679.6373405783 -45.74400000000002 84.04749999999999 +4617 3979 -90.86999999999999 62.334999999999994 207455.49567943427 1463644.0685560266 +3979 4326 207455.49567943427 1463644.0685560266 -90.87001115625111 62.33501007550968 +4617 3979 -135.99599999999998 40.6225 -3366351.200617633 175305.45298727223 +3979 4326 -3366351.200617633 175305.45298727223 -135.99601513338072 40.62250404191895 +4617 3979 -45.744 84.04749999999999 551224.8417345536 4090679.6373405783 +3979 4326 551224.8417345536 4090679.6373405783 -45.74397246778888 84.04751184449566 +4695 3986 29.64 -9.225000000000001 160439.17713325185 475096.11632642325 +4695 3986 28.614 -13.0365 49646.95013942162 53117.876421171706 +4695 3986 30.666 -5.413500000000001 273812.7746159054 896573.4707234243 +4695 3987 28.0 -9.215 200000.0 476222.001083812 +3987 4326 200000.0 476222.001083812 28.000365970001877 -9.216745556503458 +4695 3987 26.65 -13.0175 53541.71844635776 55241.306010428234 +3987 4326 53541.71844635776 55241.306010428234 26.650349765996232 -13.019010614251352 +4695 3987 29.35 -5.412500000000001 349631.0948463635 896558.221718261 +3987 4326 349631.0948463635 896558.221718261 29.350383217145296 -5.414482638917146 +4695 3988 26.0 -8.535 200000.0 551425.8858444415 +3988 4326 200000.0 551425.8858444415 26.000334606078624 -8.536789961300846 +4695 3988 24.65 -11.7255 52819.93711993625 198197.08014054253 +3988 4326 52819.93711993625 198197.08014054253 24.65031677348239 -11.727092811356512 +4695 3988 27.35 -5.344500000000001 349647.7093315269 904079.5277555231 +3988 4326 349647.7093315269 904079.5277555231 27.350352984188522 -5.346488085388846 +4695 3989 23.625 -9.02 158767.2603257698 497766.9499880716 +4695 3989 21.9285 -11.450000000000001 -26087.32349309529 228211.66079987306 +4695 3989 25.3215 -6.590000000000001 346157.54033975804 766323.6729062337 +4139 3991 -66.58 18.22 449102.8394784068 140421.53367241652 +3991 4326 449102.8394784068 140421.53367241652 -66.57963401469212 18.218034201006123 +4139 3991 -67.831 17.905 14104.859494598848 27876.827360764197 +3991 4326 14104.859494598848 27876.827360764197 -67.83064746517508 17.90305104186642 +4139 3991 -65.329 18.535 882540.4021416706 255936.93864731272 +3991 4326 882540.4021416706 255936.93864731272 -65.32862067948967 18.53301737300624 +4139 3992 -64.80000000000001 18.03 1067417.1864461189 173941.2047499886 +3992 4326 1067417.1864461189 173941.2047499886 -64.79961648737051 18.0280395012715 +4139 3992 -65.061 17.661 977752.2047329186 39214.93308236931 +3992 4326 977752.2047329186 39214.93308236931 -65.06061987662923 17.659057240699504 +4139 3992 -64.539 18.399 1156699.0679645992 308795.418644897 +3992 4326 1156699.0679645992 308795.418644897 -64.53861307606064 18.397021977137577 +4675 3993 144.79500000000002 13.44 55008.42199885627 46408.781283443444 +3993 4326 55008.42199885627 46408.781283443444 144.79740361923152 13.441436267699602 +4675 3993 144.60150000000002 13.206 34038.61662821271 20526.31399499078 +3993 4326 34038.61662821271 20526.31399499078 144.6038993884153 13.207453776096274 +4675 3993 144.9885 13.674 75937.5042302348 72308.30855880122 +3993 4326 75937.5042302348 72308.30855880122 144.99090787073612 13.675418718907094 +4326 3994 172.505 -42.5 6100221.596695778 -3935671.9712001598 +4326 3994 156.7505 -58.25 4774713.822816132 -6034038.235342437 +4326 3994 -171.7405 -26.75 7425729.370575423 -2322605.241709476 +4326 3995 0.0 75.0 0.0 -1638783.2384072365 +4326 3995 -162.0 61.5 -976334.1123644955 3004847.424406821 +4326 3995 162.0 88.5 50365.9371102881 155010.4154787378 +4326 3996 0.0 75.0 0.0 -1655962.9523645048 +4326 3996 -162.0 61.5 -986569.2309476265 3036347.8803711594 +4326 3996 162.0 88.5 50893.9339633591 156635.42270111476 +4326 3997 55.195 25.095 486045.9855810632 2776584.804935466 +4326 3997 54.8755 24.8745 453734.5267278231 2752230.042628663 +4326 3997 55.5145 25.3155 518241.83212554915 2801016.090118691 +4023 4026 28.38 46.955 198477.6947134945 201932.40022953507 +4026 4326 198477.6947134945 201932.40022953507 28.38 46.955 +4023 4026 26.805 45.591499999999996 75547.7429909096 51616.82732373569 +4026 4326 75547.7429909096 51616.82732373569 26.805 45.5915 +4023 4026 29.955 48.3185 315317.8200112481 354690.32147447206 +4026 4326 315317.8200112481 354690.32147447206 29.955000000000002 48.31850000000001 +4326 4037 28.314999999999998 46.955 600056.952675886 5201002.645197061 +4326 4037 26.7985 45.591499999999996 484282.96923557576 5048681.7132783 +4326 4037 29.8315 48.3185 709904.0086913186 5355576.300391559 +4326 4038 30.064999999999998 46.42 274466.6008432164 5144900.473950092 +4326 4038 30.0065 46.375 269782.16351151955 5140069.262590865 +4326 4038 30.1235 46.464999999999996 279143.77491886966 5149735.073434047 +4046 4048 12.395 -5.355 543776.5309848284 9407902.600892814 +4048 4326 543776.5309848284 9407902.600892814 12.395 -5.355000000000007 +4046 4048 11.850499999999998 -5.9715 483449.0780342124 9339745.541092498 +4048 4326 483449.0780342124 9339745.541092498 11.850499999999998 -5.971499999999998 +4046 4048 12.9395 -4.7385 604223.7977605445 9476013.61819796 +4048 4326 604223.7977605445 9476013.61819796 12.9395 -4.738500000000002 +4046 4049 14.004999999999999 -5.095000000000001 500554.357431986 9436665.513101995 +4049 4326 500554.357431986 9436665.513101995 14.004999999999999 -5.094999999999998 +4046 4049 13.1005 -5.8285 400388.25653073913 9355480.596255967 +4049 4326 400388.25653073913 9355480.596255967 13.100500000000002 -5.828499999999993 +4046 4049 14.9095 -4.3615 600947.9567380309 9517708.245189715 +4049 4326 600947.9567380309 9517708.245189715 14.9095 -4.3614999999999995 +4046 4050 16.0 -5.359999999999999 500000.0 9407363.819410194 +4050 4326 500000.0 9407363.819410194 16.0 -5.359999999999996 +4046 4050 15.1 -7.114999999999999 400584.7948797267 9213205.501405671 +4050 4326 400584.7948797267 9213205.501405671 15.1 -7.114999999999992 +4046 4050 16.9 -3.6050000000000004 599984.7260641269 9601364.940720798 +4050 4326 599984.7260641269 9601364.940720798 16.9 -3.6049999999999978 +4046 4051 18.0 -5.77 500000.0 9362028.616923308 +4051 4326 500000.0 9362028.616923308 18.0 -5.769999999999998 +4046 4051 17.1 -7.8759999999999994 400757.26483206905 9129042.404045083 +4051 4326 400757.26483206905 9129042.404045083 17.1 -7.87599999999999 +4046 4051 18.9 -3.6639999999999997 599978.2289460214 9594840.643798705 +4051 4326 599978.2289460214 9594840.643798705 18.9 -3.6639999999999957 +4046 4056 20.0 -5.885 500000.0 9349312.530424139 +4056 4326 500000.0 9349312.530424139 20.0 -5.884999999999995 +4046 4056 19.1 -7.7885 400736.54823574243 9138719.64723652 +4056 4326 400736.54823574243 9138719.64723652 19.1 -7.788500000000003 +4046 4056 20.9 -3.9814999999999996 599941.4572171296 9559730.932388742 +4056 4326 599941.4572171296 9559730.932388742 20.9 -3.9814999999999974 +4046 4057 22.005000000000003 -7.71 500551.5437743098 9147506.098630197 +4057 4326 500551.5437743098 9147506.098630197 22.005000000000003 -7.709999999999995 +4046 4057 21.1005 -10.887 401664.6470394399 8796007.994552802 +4057 4326 401664.6470394399 8796007.994552802 21.1005 -10.886999999999999 +4046 4057 22.9095 -4.533 600924.6111234345 9498743.131700467 +4057 4326 600924.6111234345 9498743.131700467 22.9095 -4.5329999999999915 +4046 4058 24.0 -8.025 500000.0 9112672.133496704 +4058 4326 500000.0 9112672.133496704 24.000000000000004 -8.024999999999991 +4046 4058 23.1 -11.1255 401689.1054036759 8769625.812453697 +4058 4326 401689.1054036759 8769625.812453697 23.1 -11.125500000000004 +4046 4058 24.9 -4.9245 599814.2691384965 9455450.68783842 +4058 4326 599814.2691384965 9455450.68783842 24.9 -4.924499999999998 +4046 4059 26.0 -8.49 500000.0 9061249.583402816 +4059 4326 500000.0 9061249.583402816 26.0 -8.49 +4046 4059 25.1 -11.64 401865.56064160937 8712712.25813025 +4059 4326 401865.56064160937 8712712.25813025 25.1 -11.639999999999993 +4046 4059 26.9 -5.340000000000001 599749.6993535166 9409502.361602914 +4059 4326 599749.6993535166 9409502.361602914 26.9 -5.339999999999999 +4046 4060 28.0 -9.91 500000.0 8904209.428896554 +4060 4326 500000.0 8904209.428896554 28.0 -9.909999999999995 +4046 4060 27.1 -13.042 402386.2964171069 8557613.923230046 +4060 4326 402386.2964171069 8557613.923230046 27.100000000000005 -13.041999999999993 +4046 4060 28.9 -6.7780000000000005 599486.0145963375 9250475.273828141 +4060 4326 599486.0145963375 9250475.273828141 28.899999999999995 -6.777999999999997 +4046 4061 15.005 -5.76 500553.5843528123 9363325.43751306 +4061 4326 500553.5843528123 9363325.43751306 15.005000000000003 -5.759999999999994 +4046 4061 12.3005 -7.874999999999999 202320.6535848297 9128559.702180313 +4061 4326 202320.6535848297 9128559.702180313 12.3005 -7.874999999999994 +4046 4061 17.709500000000002 -3.6450000000000005 801005.735547163 9596659.798544155 +4061 4326 801005.735547163 9596659.798544155 17.709500000000002 -3.645000000000004 +4046 4062 21.0 -7.405 500000.00000000105 9181479.396906206 +4062 4326 500000.00000000105 9181479.396906206 21.0 -7.405000000000006 +4046 4062 18.3 -10.8565 204797.6713543182 8798576.557741057 +4062 4326 204797.6713543182 8798576.557741057 18.3 -10.856500000000002 +4046 4062 23.7 -3.953500000000001 799842.8978244264 9562525.129792422 +4062 4326 799842.8978244264 9562525.129792422 23.7 -3.9535000000000085 +4046 4063 26.905 -9.125 489561.7758608402 8991326.7517922 +4063 4326 489561.7758608402 8991326.7517922 26.905 -9.124999999999996 +4046 4063 24.2905 -13.0265 206110.25047301792 8558366.569090977 +4063 4326 206110.25047301792 8558366.569090977 24.2905 -13.02649999999999 +4046 4063 29.5195 -5.2235 779289.8589295131 9422070.940565977 +4063 4326 779289.8589295131 9422070.940565977 29.519500000000004 -5.2235 +4224 4071 -47.6 -15.655 221256.05489177135 8267497.897163531 +4071 4326 221256.05489177135 8267497.897163531 -47.599466695826614 -15.655535779403985 +4224 4071 -48.050000000000004 -15.9115 173387.28705761948 8238448.053624943 +4071 4326 173387.28705761948 8238448.053624943 -48.04948633525851 -15.912040977082112 +4224 4071 -47.15 -15.398499999999999 269236.4226681853 8296427.091701422 +4071 4326 269236.4226681853 8296427.091701422 -47.14944712754302 -15.39903058935738 +4081 4082 -19.965 27.895000000000003 601865.4194242978 3086001.5527166696 +4082 4326 601865.4194242978 3086001.5527166696 -19.965000000000003 27.895 +4081 4082 -21.7335 24.9295 425939.23489774624 2757341.2456817697 +4082 4326 425939.23489774624 2757341.2456817697 -21.7335 24.929500000000004 +4081 4082 -18.1965 30.860500000000002 768078.9685110757 3417507.9030677476 +4082 4326 768078.9685110757 3417507.9030677476 -18.1965 30.860500000000002 +4081 4083 -14.875 29.005 512174.41077460896 3208546.0128176045 +4083 4326 512174.41077460896 3208546.0128176045 -14.875 29.005000000000003 +4081 4083 -17.6875 25.6255 230131.85540123016 2836950.0247675763 +4083 4326 230131.85540123016 2836950.0247675763 -17.6875 25.625499999999995 +4081 4083 -12.0625 32.384499999999996 776350.4342578333 3586852.1892963825 +4083 4326 776350.4342578333 3586852.1892963825 -12.0625 32.38449999999998 +4326 4087 0.0 0.0 0.0 0.0 +4326 4087 -162.0 -81.0 -18033757.508510318 -8996803.0728629 +4326 4087 162.0 81.0 18033757.508510318 8996803.0728629 +4258 4093 9.0 56.22 200000.0 1232933.9928398319 +4093 4326 200000.0 1232933.9928398319 9.0 56.21999999999999 +4258 4093 8.1 54.942 142324.1915506381 1091022.5766282408 +4093 4326 142324.1915506381 1091022.5766282408 8.1 54.94199999999999 +4258 4093 9.9 57.498 253960.28636769674 1375603.247833616 +4093 4326 253960.28636769674 1375603.247833616 9.9 57.498000000000005 +4258 4094 10.145 56.235 408991.7948850075 1234613.6075148964 +4094 4326 408991.7948850075 1234613.6075148964 10.145 56.23500000000001 +4258 4094 9.1145 54.8265 343090.8387729233 1078153.8783539245 +4094 4326 343090.8387729233 1078153.8783539245 9.1145 54.8265 +4258 4094 11.1755 57.643499999999996 470196.64816673275 1392058.1489922553 +4094 4326 470196.64816673275 1392058.1489922553 11.1755 57.64350000000001 +4258 4095 11.829999999999998 55.65 605036.3353914137 1169473.9984174808 +4095 4326 605036.3353914137 1169473.9984174808 11.829999999999998 55.64999999999999 +4258 4095 10.893999999999998 54.623999999999995 544711.7441591437 1055589.6235507354 +4095 4326 544711.7441591437 1055589.6235507354 10.893999999999998 54.623999999999995 +4258 4095 12.765999999999998 56.676 662277.5828397309 1284169.9518092526 +4095 4326 662277.5828397309 1284169.9518092526 12.765999999999998 56.675999999999995 +4258 4096 14.92 55.16 794900.8626330065 1115045.235168036 +4096 4326 794900.8626330065 1115045.235168036 14.92 55.16 +4258 4096 14.623 54.961999999999996 775851.4768809304 1093065.0817471994 +4096 4326 775851.4768809304 1093065.0817471994 14.623 54.96199999999999 +4258 4096 15.217 55.358000000000004 813762.7975117388 1137106.757271641 +4096 4326 813762.7975117388 1137106.757271641 15.216999999999999 55.358 +4269 4217 170.82999999999998 52.644999999999996 1602680.1499566252 19136426.476378188 +4217 4326 1602680.1499566252 19136426.476378188 170.8300047088856 52.64500770527696 +4269 4217 167.96800000000002 49.3735 918339.324244481 17957310.053878117 +4217 4326 918339.324244481 17957310.053878117 167.96800574081428 49.37351006863913 +4269 4217 173.69199999999998 55.9165 2192360.990304521 20341365.378416486 +4217 4326 2192360.990304521 20341365.378416486 173.692003514406 55.91650534477955 +4245 4390 103.52 2.08 -4756.09935606662 4137.133663853907 +4390 4326 -4756.09935606662 4137.133663853907 103.51830776234476 2.0798928462785784 +4245 4390 102.548 1.297 -112918.88023772111 -82414.33409409034 +4390 4326 -112918.88023772111 -82414.33409409034 102.54643519635141 1.2969495998448801 +4245 4390 104.49199999999999 2.863 103300.94614036885 90753.80910070734 +4390 4326 103300.94614036885 90753.80910070734 104.49018037563911 2.8628369148901447 +4245 4391 102.205 2.655 29092.63806045365 -7279.153038860095 +4391 4326 29092.63806045365 -7279.153038860095 102.20347880069966 2.6548489612527018 +4245 4391 101.7505 2.0925 -21450.091907281396 -69475.507466429 +4391 4326 -21450.091907281396 -69475.507466429 101.74903893889329 2.092390141375244 +4245 4391 102.6595 3.2175 79586.8470153683 54940.88386091279 +4391 4326 79586.8470153683 54940.88386091279 102.65791855811874 3.217308012952466 +4245 4392 102.5 3.615 7089.688398294816 -10611.459326749453 +4392 4326 7089.688398294816 -10611.459326749453 102.498438779054 3.614778374426726 +4245 4392 101.447 2.5665 -109990.66812397185 -126501.16013601533 +4392 4326 -109990.66812397185 -126501.16013601533 101.44557826289766 2.5663546338188037 +4245 4392 103.553 4.6635 123898.9760159487 105421.11536464683 +4392 4326 123898.9760159487 105421.11536464683 103.55129898223673 4.663203200718088 +4245 4393 101.36500000000001 3.205 -37678.48139811098 3403.1337860281783 +4393 4326 -37678.48139811098 3403.1337860281783 101.36358823887969 3.204807036112956 +4245 4393 100.82050000000001 2.6065 -98230.12569585163 -62752.63279893491 +4393 4326 -98230.12569585163 -62752.63279893491 100.81916052871182 2.6063509699123557 +4245 4393 101.9095 3.8035 22804.570975539576 69588.66796390011 +4393 4326 22804.570975539576 69588.66796390011 101.908015839418 3.8032633845182757 +4245 4394 103.05 4.8950000000000005 17166.655107125964 -5652.971601971062 +4394 4326 17166.655107125964 -5652.971601971062 103.04836416150523 4.8946849100288485 +4245 4394 102.447 3.9905 -49767.65721696393 -105654.46202440312 +4394 4326 -49767.65721696393 -105654.46202440312 102.44544504448088 3.990250491789649 +4245 4394 103.65299999999999 5.7995 83917.06776072856 94416.86089020204 +4394 4326 83917.06776072856 94416.86089020204 103.65128283678277 5.799119818281945 +4245 4395 100.34 5.355 -650.4657687289514 -7333.939224475056 +4395 4326 -650.4657687289514 -7333.939224475056 100.33871952688783 5.354644460807117 +4245 4395 100.14200000000001 5.1435 -22600.908642493938 -30717.02290587829 +4395 4326 -22600.908642493938 -30717.02290587829 100.14074612075257 5.143159956138711 +4245 4395 100.538 5.5665 21284.900890725938 16056.36451241458 +4395 4326 21284.900890725938 16056.36451241458 100.53669291268754 5.566128999264882 +4245 4396 100.355 5.9 -31288.638928706765 -7195.910710107349 +4396 4326 -31288.638928706765 -7195.910710107349 100.35371635030815 5.89960351853431 +4245 4396 99.6665 5.162 -107652.10023239571 -88727.11395801787 +4396 4326 -107652.10023239571 -88727.11395801787 99.66530897670826 5.161657650213079 +4245 4396 101.04350000000001 6.638 44878.4165839951 74422.03610871546 +4396 4326 44878.4165839951 74422.03610871546 101.04212345229759 6.637549798747273 +4245 4397 101.035 4.79 24206.175117533192 125785.83875202521 +4397 4326 24206.175117533192 125785.83875202521 101.03362897820313 4.789688236889231 +4245 4397 100.1665 3.773 -72222.05948210656 13356.937495070451 +4397 4326 -72222.05948210656 13356.937495070451 100.16524520933402 3.772762895933242 +4245 4397 101.9035 5.807 120342.10018623727 238352.8068942242 +4397 4326 120342.10018623727 238352.8068942242 101.90201228868855 5.806614307961935 +4245 4398 102.0 5.415 -19645.95651313649 -52954.82916962652 +4398 4326 -19645.95651313649 -52954.82916962652 101.99850059451413 5.4146437602681745 +4245 4398 101.39699999999999 4.6275 -86569.07167535031 -139987.61752949265 +4398 4326 -86569.07167535031 -139987.61752949265 101.3955816432658 4.627201098799644 +4245 4398 102.60300000000001 6.2025 47107.89540114641 34140.93120632059 +4398 4326 47107.89540114641 34140.93120632059 102.60141922015265 6.20208683088461 +4267 4399 170.82999999999998 52.644999999999996 1602678.8500066404 19135712.353323467 +4399 4326 1602678.8500066404 19135712.353323467 170.8270200445895 52.643290217083106 +4267 4399 167.96800000000002 49.3735 918315.9546866721 17956592.01033451 +4399 4326 918315.9546866721 17956592.01033451 167.9652470620094 49.371722462765135 +4267 4399 173.69199999999998 55.9165 2192381.1241086223 20340663.420226417 +4399 4326 2192381.1241086223 20340663.420226417 173.6887537966913 55.91489046577478 +4267 4400 177.0 52.295 1640416.67 19007940.528847784 +4400 4326 1640416.67 19007940.528847784 176.99701259511795 52.293444450587195 +4267 4400 174.3 48.3575 984216.6370697607 17583101.845670003 +4400 4326 984216.6370697607 17583101.845670003 174.29725830144488 48.35587890760175 +4267 4400 179.7 56.2325 2189507.597344477 20456068.733029973 +4400 4326 2189507.597344477 20456068.733029973 179.69671061156575 56.23105395058736 +4267 4401 -176.995 55.545 1641451.7867027908 20194272.669377316 +4401 4326 1641451.7867027908 20194272.669377316 -176.99740623313252 55.54380514351933 +4267 4401 -179.6995 48.6465 988059.5507171631 17688476.646831032 +4401 4326 988059.5507171631 17688476.646831032 -179.70156279256133 48.645254088878175 +4267 4401 -174.2905 62.4435 2099158.7030369285 22724099.556443606 +4401 4326 2099158.7030369285 22724099.556443606 -174.293432885212 62.44246849265357 +4267 4402 -170.995 60.855 1641307.9795514594 22133919.625574604 +4402 4326 1641307.9795514594 22133919.625574604 -170.997767687503 60.854002566940096 +4267 4402 -173.6995 49.87949999999999 1004123.2321181557 18138116.869718418 +4402 4326 1004123.2321181557 18138116.869718418 -173.70160500780796 49.87835499228297 +4267 4402 -168.2905 71.8305 1949725.839674469 26154727.342699233 +4402 4326 1949725.839674469 26154727.342699233 -168.2947862314142 71.82989546588087 +4267 4403 -164.995 61.905 1641278.5842884246 22517655.666952107 +4403 4326 1641278.5842884246 22517655.666952107 -164.99750540693282 61.90418811377297 +4267 4403 -167.6995 50.758500000000005 1015756.5715970445 18458709.721271623 +4403 4326 1015756.5715970445 18458709.721271623 -167.70138434969607 50.75754727293723 +4267 4403 -162.2905 73.0515 1929582.2956952031 26601179.93138586 +4403 4326 1929582.2956952031 26601179.93138586 -162.2944974453929 73.05106812816416 +4267 4404 -158.995 62.845 1641252.0191501947 22861241.425599553 +4404 4326 1641252.0191501947 22861241.425599553 -158.9975091255922 62.84431625730801 +4267 4404 -161.6995 52.1665 1034698.5673492067 18972321.761113856 +4404 4326 1034698.5673492067 18972321.761113856 -161.70139613178415 52.165641981380205 +4267 4404 -156.2905 73.52349999999998 1921759.2395132212 26773775.881771114 +4404 4326 1921759.2395132212 26773775.881771114 -156.2944662944442 73.52320128149425 +4267 4405 -152.995 63.42999999999999 1641235.370958135 23075092.39402275 +4405 4326 1641235.370958135 23075092.39402275 -152.99745459678476 63.429435094304125 +4267 4405 -155.6995 53.278 1049912.6686963392 19377845.046181012 +4405 4326 1049912.6686963392 19377845.046181012 -155.7013753243537 53.277240294934145 +4267 4405 -150.2905 73.582 1920788.2849251933 26795167.973944176 +4405 4326 1920788.2849251933 26795167.973944176 -150.2942936337356 73.58181408183388 +4267 4406 -146.995 64.09 1641216.4844795535 23316381.549271535 +4406 4326 1641216.4844795535 23316381.549271535 -146.99737698535284 64.08955255128797 +4267 4406 -149.6995 55.053999999999995 1074685.0379522366 20025931.145028222 +4406 4326 1074685.0379522366 20025931.145028222 -149.70136327197739 55.05334553547477 +4267 4406 -144.2905 73.12599999999999 1928348.821413957 26628421.884097304 +4406 4326 1928348.821413957 26628421.884097304 -144.29397205047184 73.12590052173901 +4267 4407 -140.995 63.53 1641232.5163957875 23111649.90742997 +4407 4326 1641232.5163957875 23111649.90742997 -140.9971718316216 63.529632207729684 +4267 4407 -143.6995 54.476 1066561.5070394154 19814994.563725937 +4407 4326 1066561.5070394154 19814994.563725937 -143.70122398516068 54.475429021434955 +4267 4407 -138.2905 72.584 1937311.1517717366 26430235.730047144 +4407 4326 1937311.1517717366 26430235.730047144 -138.29361507368134 72.58397629118289 +4267 4408 -134.995 63.32000000000001 1641238.508052864 23034879.73123471 +4408 4326 1641238.508052864 23034879.73123471 -134.99737012306267 63.31987613484862 +4267 4408 -137.6995 54.572 1067906.6864477631 19850027.90487299 +4408 4326 1067906.6864477631 19850027.90487299 -137.70141098117946 54.571676724950386 +4267 4408 -132.2905 72.06800000000001 1945818.5337873357 26241564.5966432 +4408 4326 1945818.5337873357 26241564.5966432 -132.29380776773493 72.06820256975814 +4267 4409 -129.0 46.11 1640416.67 16752103.374197265 +4409 4326 1640416.67 16752103.374197265 -129.00138667885318 46.10978256392645 +4267 4409 -131.7 36.453 846528.375315285 13245977.46141794 +4409 4326 846528.375315285 13245977.46141794 -131.7012557559517 36.45290424511228 +4267 4409 -126.3 55.767 2196150.358554595 20286160.872006457 +4409 4326 2196150.358554595 20286160.872006457 -126.30161862266202 55.766856272379364 +4267 4410 -122.995 39.815 1641820.7329281147 14458680.722703049 +4410 4326 1641820.7329281147 14458680.722703049 -122.99609595983047 39.814859857255314 +4267 4410 -125.6995 31.4675 798903.5520510068 11431684.933706293 +4410 4326 798903.5520510068 11431684.933706293 -125.70055079381721 31.467535828035604 +4267 4410 -120.2905 48.1625 2301436.129313071 17512082.25892185 +4410 4326 2301436.129313071 17512082.25892185 -120.29167769210751 48.16232703696681 +4267 4411 -117.0 39.945 1640416.67 14506017.103371209 +4411 4326 1640416.67 14506017.103371209 -117.00092804276997 39.94489834289562 +4267 4411 -119.7 31.7865 801617.6053468202 11547742.658516696 +4411 4326 801617.6053468202 11547742.658516696 -119.7009051417378 31.786560281410804 +4267 4411 -114.3 48.1035 2299874.154515511 17490488.41081589 +4411 4326 2299874.154515511 17490488.41081589 -114.30097645097464 48.10337115295107 +4267 4412 -111.0 40.17 1640416.67 14587948.066990161 +4412 4326 1640416.67 14587948.066990161 -111.0007566103728 40.16993892837275 +4267 4412 -113.7 32.214 805504.5037820628 11703276.626314808 +4412 4326 805504.5037820628 11703276.626314808 -113.70075562821866 32.21408713737044 +4267 4412 -108.3 48.126 2299586.1263402384 17498692.22565443 +4412 4326 2299586.1263402384 17498692.22565443 -108.30077267221499 48.12591691482501 +4267 4413 -105.0 38.995 1640416.67 14160121.86249745 +4413 4326 1640416.67 14160121.86249745 -105.00056422097914 38.994989253140034 +4267 4413 -107.7 29.9815 785723.7911063571 10891133.871924167 +4413 4326 785723.7911063571 10891133.871924167 -107.70057990164848 29.98168445309288 +4267 4413 -102.3 48.0085 2301089.144585885 17455850.362697296 +4413 4326 2301089.144585885 17455850.362697296 -102.30055872742756 48.0084513314395 +4152 4414 144.79500000000002 13.44 104873.0712287864 193362.37181528643 +4414 4326 104873.0712287864 193362.37181528643 144.79500000000002 13.440000000000001 +4152 4414 144.60150000000002 13.206 83903.38825029787 167478.50837094785 +4414 4326 83903.38825029787 167478.50837094785 144.60150000000002 13.206 +4152 4414 144.9885 13.674 125802.08044039052 219263.286407388 +4414 4326 125802.08044039052 219263.286407388 144.9885 13.674000000000001 +4695 4415 26.259999999999998 -9.225000000000001 528544.7218723877 475129.37273003056 +4415 4326 528544.7218723877 475129.37273003056 26.26033925326335 -9.226746636335415 +4695 4415 22.192 -13.0365 86321.37292352063 51431.79554722598 +4415 4326 86321.37292352063 51431.79554722598 22.192279198329203 -13.038014859338869 +4695 4415 30.328000000000003 -5.413500000000001 980129.368903818 893658.4887465502 +4415 4326 980129.368903818 893658.4887465502 30.328397814831213 -5.4154819459568415 +4178 4417 21.0 47.845 7500000.0 5301286.662384523 +4417 4326 7500000.0 5301286.662384523 20.998356736756797 47.84467425077262 +4178 4417 19.65 46.2745 7395941.564322419 5127574.038562783 +4417 4326 7395941.564322419 5127574.038562783 19.6483994740688 46.27413858987782 +4178 4417 22.35 49.4155 7597957.874606853 5476809.773762325 +4417 4326 7597957.874606853 5476809.773762325 22.348310995582818 49.41521097648766 +4267 4418 -75.0 36.655 1640416.67 13308369.848759094 +4418 4326 1640416.67 13308369.848759094 -74.9996232745175 36.655063313361076 +4267 4418 -77.7 29.1175 778415.1410815748 10576885.30631691 +4418 4326 778415.1410815748 10576885.30631691 -77.69973010246282 29.117759281539364 +4267 4418 -72.3 44.1925 2348347.0152389826 16064871.734748468 +4418 4326 2348347.0152389826 16064871.734748468 -72.29948685384205 44.19246709051999 +4267 4419 -68.995 40.54 1641805.8693587363 14722686.048317777 +4419 4326 1641805.8693587363 14722686.048317777 -68.99441117464255 40.53998008414052 +4267 4419 -71.6995 34.303 825314.005301331 12463417.691849299 +4419 4326 825314.005301331 12463417.691849299 -71.69903674653324 34.30310067142183 +4267 4419 -66.2905 46.777 2319051.9494843846 17006954.99207305 +4419 4326 2319051.9494843846 17006954.99207305 -66.28975350297118 46.776929118631145 +4269 4420 177.0 52.295 1640416.67 19008655.479554184 +4420 4326 1640416.67 19008655.479554184 177.00000153420078 52.29500778365649 +4269 4420 174.3 48.3575 984237.4418124591 17583819.82336886 +4420 4326 984237.4418124591 17583819.82336886 174.300002680393 48.35751052721077 +4269 4420 179.7 56.2325 2189487.463257726 20456769.169141952 +4420 4326 2189487.463257726 20456769.169141952 179.70000016885382 56.232505034954634 +4269 4421 -176.995 55.545 1641451.7491821533 20194976.72076514 +4421 4326 1641451.7491821533 20194976.72076514 -176.99500166096058 55.54500553348903 +4269 4421 -179.6995 48.6465 988080.3557500707 17689194.74544013 +4421 4326 988080.3557500707 17689194.74544013 -179.69950014234806 48.6465102644127 +4269 4421 -174.2905 62.4435 2099140.2491789586 22724757.1103253 +4421 4326 2099140.2491789586 22724757.1103253 -174.29050385338212 62.44350077245478 +4269 4422 -170.995 60.855 1641307.9444728117 22134590.826510664 +4422 4326 1641307.9444728117 22134590.826510664 -170.995 60.85499999999999 +4269 4422 -173.6995 49.87949999999999 1004144.0313035317 18138834.81999238 +4422 4326 1004144.0313035317 18138834.81999238 -173.6995 49.87949999999999 +4269 4422 -168.2905 71.8305 1949712.0405234757 26155278.310387142 +4422 4326 1949712.0405234757 26155278.310387142 -168.2905 71.83049999999999 +4269 4423 -164.995 61.905 1641278.5498682365 22518318.27268129 +4423 4326 1641278.5498682365 22518318.27268129 -164.995 61.905 +4269 4423 -167.6995 50.758500000000005 1015777.3428712956 18459426.91188376 +4423 4326 1015777.3428712956 18459426.91188376 -167.6995 50.758500000000005 +4269 4423 -162.2905 73.0515 1929569.2619580247 26601713.935751874 +4423 4326 1929569.2619580247 26601713.935751874 -162.2905 73.0515 +4269 4424 -158.995 62.845 1641251.985368251 22861895.776833795 +4424 4326 1641251.985368251 22861895.776833795 -158.995 62.845 +4269 4424 -161.6995 52.1665 1034719.2516511077 18973036.608706564 +4424 4326 1034719.2516511077 18973036.608706564 -161.6995 52.16649999999999 +4269 4424 -156.2905 73.52349999999998 1921746.5103921383 26774303.165770367 +4424 4326 1921746.5103921383 26774303.165770367 -156.2905 73.52349999999998 +4269 4425 -152.995 63.42999999999999 1641235.3375964854 23075741.346541848 +4425 4326 1641235.3375964854 23075741.346541848 -152.995 63.43 +4269 4425 -155.6995 53.278 1049933.2467918126 19378557.07310208 +4425 4326 1049933.2467918126 19378557.07310208 -155.6995 53.278 +4269 4425 -150.2905 73.582 1920775.5938890288 26795694.418878887 +4425 4326 1920775.5938890288 26795694.418878887 -150.2905 73.582 +4269 4426 -146.995 64.09 1641216.451613184 23317024.174327325 +4426 4326 1641216.451613184 23317024.174327325 -146.995 64.09 +4269 4426 -149.6995 55.053999999999995 1074705.3760562828 20026636.910141673 +4426 4326 1074705.3760562828 20026636.910141673 -149.6995 55.05399999999999 +4269 4426 -144.2905 73.12599999999999 1928335.83543946 26628954.833631355 +4426 4326 1928335.83543946 26628954.833631355 -144.2905 73.12599999999999 +4269 4427 -140.995 63.53 1641232.4831077459 23112297.91727259 +4427 4326 1641232.4831077459 23112297.91727259 -140.995 63.53000000000001 +4269 4427 -143.6995 54.476 1066581.9328346422 19815702.601787806 +4427 4326 1066581.9328346422 19815702.601787806 -143.6995 54.47599999999999 +4269 4427 -138.2905 72.584 1937297.821070737 26430776.302319646 +4427 4326 1937297.821070737 26430776.302319646 -138.2905 72.58400000000002 +4269 4428 -134.995 63.32000000000001 1641238.474610839 23035529.714041255 +4428 4326 1641238.474610839 23035529.714041255 -134.995 63.32000000000001 +4269 4428 -137.6995 54.572 1067927.0983220795 19850735.58105083 +4428 4326 1067927.0983220795 19850735.58105083 -137.6995 54.571999999999996 +4269 4428 -132.2905 72.06800000000001 1945804.8809148527 26242112.313526798 +4428 4326 1945804.8809148527 26242112.313526798 -132.2905 72.06800000000001 +4269 4429 -129.0 46.11 1640416.67 16752818.7399635 +4429 4326 1640416.67 16752818.7399635 -129.0 46.11 +4269 4429 -131.7 36.453 846547.4561216865 13246639.021291636 +4429 4326 846547.4561216865 13246639.021291636 -131.7 36.453 +4269 4429 -126.3 55.767 2196130.137734515 20286863.523723442 +4429 4326 2196130.137734515 20286863.523723442 -126.30000000000001 55.766999999999996 +4269 4430 -122.995 39.815 1641820.6962124747 14459368.616341282 +4430 4326 1641820.6962124747 14459368.616341282 -122.995 39.81499999999999 +4269 4430 -125.6995 31.4675 798921.2527101418 11432293.89242832 +4430 4326 798921.2527101418 11432293.89242832 -125.6995 31.4675 +4269 4430 -120.2905 48.1625 2301415.255324801 17512800.119097903 +4430 4326 2301415.255324801 17512800.119097903 -120.2905 48.162499999999994 +4269 4431 -117.0 39.945 1640416.67 14506705.847573021 +4431 4326 1640416.67 14506705.847573021 -117.0 39.94499999999999 +4269 4431 -119.7 31.7865 801635.4042104973 11548355.46580386 +4431 4326 801635.4042104973 11548355.46580386 -119.7 31.786499999999997 +4269 4431 -114.3 48.1035 2299853.355117612 17491206.233478516 +4431 4326 2299853.355117612 17491206.233478516 -114.3 48.10349999999999 +4269 4432 -111.0 40.17 1640416.67 14588638.255363239 +4432 4326 1640416.67 14588638.255363239 -111.0 40.17 +4269 4432 -113.7 32.214 805522.4286690679 11703894.490081469 +4432 4326 805522.4286690679 11703894.490081469 -113.7 32.214 +4269 4432 -108.3 48.126 2299565.3264049147 17499410.06389805 +4432 4326 2299565.3264049147 17499410.06389805 -108.3 48.12599999999999 +4269 4433 -105.0 38.995 1640416.67 14160804.11915397 +4433 4326 1640416.67 14160804.11915397 -105.00000000000001 38.99499999999999 +4269 4433 -107.7 29.9815 785741.0442044141 10891724.063503249 +4433 4326 785741.0442044141 10891724.063503249 -107.7 29.981499999999993 +4269 4433 -102.3 48.0085 2301068.3475937415 17456568.115612805 +4433 4326 2301068.3475937415 17456568.115612805 -102.3 48.0085 +4178 4434 22.7 48.43 8403795.33536818 5367152.274784391 +4434 4326 8403795.33536818 5367152.274784391 22.698345595915754 48.42970505045028 +4178 4434 22.52 47.827 8389185.081867252 5300346.082032026 +4434 4326 8389185.081867252 5300346.082032026 22.518363983053526 47.82669576386191 +4178 4434 22.88 49.033 8418101.1082323 5433997.613401763 +4434 4326 8418101.1082323 5433997.613401763 22.87832659800007 49.03271440654978 +4759 4437 -66.24000000000001 18.095 220463.90309500165 228972.23173933866 +4437 4326 220463.90309500165 228972.23173933866 -66.24000000000001 18.09500000000005 +4759 4437 -67.797 17.6675 55307.87163008732 182184.18422951712 +4437 4326 55307.87163008732 182184.18422951712 -67.797 17.667500000000008 +4759 4437 -64.683 18.5225 384813.8289925946 277161.9505647868 +4437 4326 384813.8289925946 277161.9505647868 -64.683 18.522500000000022 +4269 4438 -75.0 36.655 1640416.67 13309033.461537005 +4438 4326 1640416.67 13309033.461537005 -75.0 36.655 +4269 4438 -77.7 29.1175 778432.1266167379 10577463.962218303 +4438 4326 778432.1266167379 10577463.962218303 -77.7 29.117500000000007 +4269 4438 -72.3 44.1925 2348326.4901069016 16065581.376355933 +4438 4326 2348326.4901069016 16065581.376355933 -72.3 44.192499999999995 +4269 4439 -68.995 40.54 1641805.832384568 14723378.534373961 +4439 4326 1641805.832384568 14723378.534373961 -68.995 40.53999999999999 +4269 4439 -71.6995 34.303 825332.5199991887 12464058.570741214 +4439 4326 825332.5199991887 12464058.570741214 -71.6995 34.303000000000004 +4269 4439 -66.2905 46.777 2319031.1303854305 17007671.257957917 +4439 4326 2319031.1303854305 17007671.257957917 -66.2905 46.777 +4267 4455 -77.625 40.445 2034791.4909395748 404983.54256514704 +4455 4326 2034791.4909395748 404983.54256514704 -77.62468792496308 40.445014858120814 +4267 4455 -80.2395 39.783500000000004 1300392.1064740785 173849.0426141648 +4455 4326 1300392.1064740785 173849.0426141648 -80.23927539014353 39.78352865426403 +4267 4455 -75.0105 41.1065 2754893.4616665156 657667.8525512023 +4455 4326 2754893.4616665156 657667.8525512023 -75.01009934189818 41.10649979423773 +4267 4456 -73.03 40.885 2268217.751007635 241749.83609030797 +4456 4326 2268217.751007635 241749.83609030797 -73.02953669531674 40.884994652684284 +4267 4456 -74.137 40.5115 1961904.1221115906 104219.43188374177 +4456 4326 1961904.1221115906 104219.43188374177 -74.13657477813064 40.51150298692365 +4267 4456 -71.923 41.2585 2571049.35474014 383119.73081027414 +4456 4326 2571049.35474014 383119.73081027414 -71.92249833591427 41.25848618060871 +4269 4457 -100.25999999999999 45.045 1901299.2101861658 441850.1308508461 +4457 4326 1901299.2101861658 441850.1308508461 -100.25999999999999 45.04500000000001 +4269 4457 -103.689 44.2305 1001826.0644531978 166826.8571025809 +4457 4326 1001826.0644531978 166826.8571025809 -103.689 44.23049999999999 +4269 4457 -96.831 45.859500000000004 2775744.0497138826 754529.8289194349 +4457 4326 2775744.0497138826 754529.8289194349 -96.831 45.859500000000004 +4326 4462 133.26999999999998 -26.78 124750.30480670104 23447.503113648545 +4326 4462 114.892 -42.008 -1446274.714948704 -1762365.3722407492 +4326 4462 151.648 -11.552 2184634.2405376025 1538535.102802745 +4463 4467 -56.5 45.39 539139.3867387371 5026397.521210511 +4467 4326 539139.3867387371 5026397.521210511 -56.5 45.39 +4463 4467 -57.04 43.608 496771.99664593703 4828336.540524501 +4467 4326 496771.99664593703 4828336.540524501 -57.04 43.608000000000004 +4463 4467 -55.96 47.172 578812.1815546087 5224802.744751576 +4467 4326 578812.1815546087 5224802.744751576 -55.96 47.17199999999999 +4470 4471 45.19 -12.91 520611.2921526474 8572809.36921248 +4471 4326 520611.2921526474 8572809.36921248 45.19 -12.910000000000007 +4470 4471 43.831 -14.332 373934.3954965897 8415237.254319478 +4471 4326 373934.3954965897 8415237.254319478 43.831 -14.33199999999999 +4470 4471 46.549 -11.488 668953.342576192 8729606.363971211 +4471 4326 668953.342576192 8729606.363971211 46.549 -11.487999999999998 +4483 4484 -118.095 23.865000000000002 388505.9026551563 2639711.37001155 +4484 4326 388505.9026551563 2639711.37001155 -118.09500000000001 23.865000000000002 +4483 4484 -121.7805 15.8955 -12246.927675235318 1763238.8948801474 +4484 4326 -12246.927675235318 1763238.8948801474 -121.7805 15.895499999999997 +4483 4484 -114.4095 31.8345 745162.3921794384 3525016.083807699 +4484 4326 745162.3921794384 3525016.083807699 -114.4095 31.834500000000006 +4483 4485 -111.0 23.68 500000.0 2618798.8219067925 +4485 4326 500000.0 2618798.8219067925 -111.0 23.68 +4483 4485 -113.7 15.949 210958.14795963367 1765166.42438099 +4485 4326 210958.14795963367 1765166.42438099 -113.7 15.948999999999996 +4483 4485 -108.3 31.411 756689.9510171973 3478306.0767350667 +4485 4326 756689.9510171973 3478306.0767350667 -108.3 31.41099999999999 +4483 4486 -105.0 22.92 499999.9999999987 2534663.831269058 +4486 4326 499999.9999999987 2534663.831269058 -105.00000000000001 22.92000000000001 +4483 4486 -107.7 14.937000000000001 209551.16850779124 1653122.8520861657 +4486 4326 209551.16850779124 1653122.8520861657 -107.7 14.937000000000001 +4483 4486 -102.3 30.903 758064.3626219081 3421976.282493674 +4486 4326 758064.3626219081 3421976.282493674 -102.3 30.90299999999999 +4483 4487 -99.0 21.055 500000.0 2328234.6586782136 +4487 4326 500000.0 2328234.6586782136 -99.0 21.054999999999996 +4483 4487 -101.7 13.175500000000001 207317.68646860588 1458116.4671444155 +4487 4326 207317.68646860588 1458116.4671444155 -101.7 13.1755 +4483 4487 -96.3 28.934499999999996 763197.6182969778 3203730.5537319253 +4487 4326 763197.6182969778 3203730.5537319253 -96.3 28.934499999999996 +4483 4488 -93.0 19.05 499999.99999999866 2106359.8563538482 +4488 4326 499999.99999999866 2106359.8563538482 -93.0 19.049999999999997 +4483 4488 -95.7 12.795 206871.34917723015 1415996.1330994398 +4488 4326 206871.34917723015 1415996.1330994398 -95.7 12.795000000000002 +4483 4488 -90.3 25.305 771844.0736431255 2801459.816646112 +4488 4326 771844.0736431255 2801459.816646112 -90.3 25.304999999999996 +4483 4489 -87.32 21.79 466920.794456622 2409617.4884034996 +4489 4326 466920.794456622 2409617.4884034996 -87.32000000000001 21.789999999999996 +4483 4489 -89.732 18.208 211035.57419919752 2015351.931870287 +4489 4326 211035.57419919752 2015351.931870287 -89.732 18.208 +4483 4489 -84.908 25.372 710492.8715466689 2807786.909432056 +4489 4326 710492.8715466689 2807786.909432056 -84.908 25.372000000000003 +4490 4491 75.815 38.245000000000005 13571345.21703189 4235006.843605045 +4490 4491 73.8395 35.7025 13394971.14705971 3953153.9613602757 +4490 4491 77.79050000000001 40.7875 13735545.471042003 4520723.404214654 +4490 4492 80.99000000000001 38.195 14499124.006239193 4229142.739243363 +4490 4492 78.281 30.0635 14237771.607831085 3330271.0546798166 +4490 4492 83.699 46.326499999999996 14707836.828463662 5135919.81119613 +4490 4493 87.0 38.25 15500000.0 4235247.729993024 +4490 4493 84.3 28.413 15235388.420526639 3147179.6236381195 +4490 4493 89.7 48.087 15701141.520708388 5331629.2553552175 +4490 4494 93.005 37.805 16500440.32262631 4185854.077443189 +4490 4494 90.3005 28.7195 16236203.776550366 3181168.1143316315 +4490 4494 95.7095 46.8905 16706489.375149202 5198639.608999724 +4490 4495 99.005 32.155 17500471.66916031 3559040.1109740064 +4490 4495 96.3005 22.232499999999998 17221626.492045704 2462028.144730641 +4490 4495 101.7095 42.0775 17724220.199063525 4663799.342908683 +4490 4496 105.0 30.11 18500000.0 3332307.268557305 +4490 4496 102.3 18.986 18215604.263079982 2102299.6289863624 +4490 4496 107.7 41.233999999999995 18726370.832123913 4570077.489092871 +4490 4497 111.0 30.905 19500000.0 3420441.8018143713 +4490 4497 108.3 18.1205 19214165.480817843 2006414.7474997963 +4490 4497 113.7 43.689499999999995 19717686.69185404 4842867.25016424 +4490 4498 117.0 35.269999999999996 20500000.0 3904547.5249169567 +4490 4498 114.3 20.645 20218542.718609087 2286112.522688117 +4490 4498 119.7 49.894999999999996 20693987.657463264 5532665.27522241 +4490 4499 123.0 39.1 21500000.0 4329605.628234109 +4490 4499 120.3 26.086000000000002 21229817.539126825 2889163.033544337 +4490 4499 125.7 52.114000000000004 21684942.462518487 5779468.442656943 +4490 4500 129.0 41.245 22500000.0 4567782.07890968 +4490 4500 126.3 30.854499999999998 22241702.159304623 3417965.7064717133 +4490 4500 131.7 51.63549999999999 22686917.053603437 5726242.180892778 +4490 4501 133.385 46.71 23376507.4950249 5176275.3061727965 +4490 4501 132.1385 45.189 23275123.260318175 5009934.103110318 +4490 4501 134.63150000000002 48.231 23472623.566124197 5344178.739854652 +4490 4502 75.815 38.245000000000005 571345.21703189 4235006.843605045 +4490 4502 73.8395 35.7025 394971.1470597114 3953153.9613602757 +4490 4502 77.79050000000001 40.7875 735545.4710420043 4520723.404214654 +4490 4503 80.99000000000001 38.195 499124.0062391917 4229142.739243363 +4490 4503 78.281 30.0635 237771.60783108458 3330271.0546798166 +4490 4503 83.699 46.326499999999996 707836.8284636616 5135919.81119613 +4490 4504 87.0 38.25 500000.0 4235247.729993024 +4490 4504 84.3 28.413 235388.4205266395 3147179.6236381195 +4490 4504 89.7 48.087 701141.5207083869 5331629.2553552175 +4490 4505 93.005 37.805 500440.32262630906 4185854.077443189 +4490 4505 90.3005 28.7195 236203.7765503655 3181168.1143316315 +4490 4505 95.7095 46.8905 706489.3751492011 5198639.608999724 +4490 4506 99.005 32.155 500471.6691603086 3559040.1109740064 +4490 4506 96.3005 22.232499999999998 221626.49204570416 2462028.144730641 +4490 4506 101.7095 42.0775 724220.1990635264 4663799.342908683 +4490 4507 105.0 30.11 500000.0 3332307.268557305 +4490 4507 102.3 18.986 215604.26307998173 2102299.6289863624 +4490 4507 107.7 41.233999999999995 726370.8321239124 4570077.489092871 +4490 4508 111.0 30.905 500000.0 3420441.8018143713 +4490 4508 108.3 18.1205 214165.4808178426 2006414.7474997963 +4490 4508 113.7 43.689499999999995 717686.6918540406 4842867.25016424 +4490 4509 117.0 35.269999999999996 500000.0 3904547.5249169567 +4490 4509 114.3 20.645 218542.71860908717 2286112.522688117 +4490 4509 119.7 49.894999999999996 693987.6574632633 5532665.27522241 +4490 4510 123.0 39.1 500000.0 4329605.628234109 +4490 4510 120.3 26.086000000000002 229817.5391268246 2889163.033544337 +4490 4510 125.7 52.114000000000004 684942.4625184854 5779468.442656943 +4490 4511 129.0 41.245 500000.0 4567782.07890968 +4490 4511 126.3 30.854499999999998 241702.15930462393 3417965.7064717133 +4490 4511 131.7 51.63549999999999 686917.0536034368 5726242.180892778 +4490 4512 133.385 46.71 376507.49502490164 5176275.3061727965 +4490 4512 132.1385 45.189 275123.2603181744 5009934.103110318 +4490 4512 134.63150000000002 48.231 472623.56612419867 5344178.739854652 +4490 4513 75.06 38.230000000000004 25505253.44616747 4233029.411718444 +4490 4513 73.76400000000001 36.052 25388628.21016112 3992019.598405965 +4490 4513 76.356 40.408 25615103.700702414 4475715.774093929 +4490 4514 78.0 36.43 26500000.0 4033256.7572091157 +4490 4514 76.65 31.57 26371837.1141568 3494963.294701828 +4490 4514 79.35 41.29 26613084.577490937 4573658.934893523 +4490 4515 81.005 37.915 27500439.66871458 4198063.398660751 +4490 4515 79.6505 30.7465 27370771.573749866 3403647.5986920954 +4490 4515 82.35950000000001 45.0835 27607036.452455174 4995123.273482752 +4490 4516 84.0 37.745 28500000.0 4179194.5322695132 +4490 4516 82.65 29.2085 28368715.913226884 3233133.7794636637 +4490 4516 85.35 46.281499999999994 28604043.447889198 5128261.768246826 +4490 4517 87.0 38.49 29500000.0 4261888.567610511 +4490 4517 85.65 28.869 29368285.407464705 3195499.2555731516 +4490 4517 88.35 48.111 29600526.930526827 5331651.562255311 +4490 4518 90.0 37.870000000000005 30500000.0 4193068.6369197676 +4490 4518 88.64099999999999 28.375 30366784.9376347 3140751.0850881445 +4490 4518 91.35900000000001 47.365 30602653.31689324 5248721.380409396 +4490 4519 93.0 36.42 31500000.0 4032147.0882259104 +4490 4519 91.65 28.581 31367923.829953693 3163575.0553991105 +4490 4519 94.35 44.259 31607806.498813346 4903487.561231352 +4490 4520 96.005 36.365 32500448.731875665 4026043.953716178 +4490 4520 94.6505 29.0435 32368554.813420895 3214842.28658065 +4490 4520 97.35950000000001 43.686499999999995 32609613.316423208 4839887.956491769 +4490 4521 99.0 32.095 33500000.0 3552386.760062522 +4490 4521 97.65 22.4965 33361077.415119343 2489406.034678087 +4490 4521 100.35 41.6935 33612384.8731013 4618474.5714256335 +4490 4522 102.0 31.909999999999997 34500000.0 3531872.69214745 +4490 4522 100.65 22.208 34360791.03984671 2457452.6232682797 +4490 4522 103.35 41.611999999999995 34612526.65372996 4609422.350093691 +4490 4523 105.005 32.355000000000004 35500470.636250846 3581218.367444173 +4490 4523 103.6505 23.485500000000002 35362136.83678303 2598952.5487704924 +4490 4523 106.35950000000001 41.2245 35613994.225335725 4566396.725920691 +4490 4524 108.005 30.33 36500480.82724786 3356695.6341688256 +4490 4524 106.6505 19.404 36358244.58438646 2146943.387976246 +4490 4524 109.35950000000001 41.256 36613939.49269499 4569895.201263173 +4490 4525 111.0 31.61 37500000.0 3498607.906343834 +4490 4525 109.65 19.46 37358240.667949766 2153144.1962357345 +4490 4525 112.35 43.76 37608714.32767181 4848041.902079204 +4490 4526 114.0 33.485 38500000.0 3706538.802603186 +4490 4526 112.65 22.7165 38361298.15606646 2513773.3127082847 +4490 4526 115.35 44.2535 38607816.5499015 4902876.411696486 +4490 4527 117.0 36.24 39500000.0 4012173.3648338104 +4490 4527 115.65 23.964000000000002 39362588.339597434 2651958.962947342 +4490 4527 118.35 48.516000000000005 39599734.38478857 5376684.683137083 +4490 4528 120.0 38.879999999999995 40500000.0 4305182.264307256 +4490 4528 118.65 25.875 40364690.56096279 2863681.9481411087 +4490 4528 121.35 51.885 40592950.67287148 5751409.556180853 +4490 4529 123.0 40.89 41500000.0 4528357.478169169 +4490 4529 121.65 29.487 41369072.508485295 3264007.6296525 +4490 4529 124.35 52.293 41592106.64985622 5796804.249250238 +4490 4530 126.0 46.695 42500000.0 5173340.809639158 +4490 4530 124.65 40.8405 42386142.58640528 4523737.7102233665 +4490 4530 127.35 52.5495 42591573.61589889 5825344.351239932 +4490 4531 129.0 45.81 43500000.0 5074967.528460148 +4490 4531 127.65 41.814 43387825.17206001 4631858.753558982 +4490 4531 130.35 49.806 43597176.69510738 5520143.539010394 +4490 4532 132.0 45.650000000000006 44500000.0 5057184.161550448 +4490 4532 130.65 42.743 44389461.195308246 4735053.634122378 +4490 4532 133.35 48.557 44599653.871152796 5381243.759450208 +4490 4533 134.135 47.125 45434365.334453024 5221506.61268192 +4490 4533 133.5635 45.9775 45388679.256233595 5094588.586758707 +4490 4533 134.7065 48.2725 45478213.062069856 5348769.32670989 +4490 4534 75.06 38.230000000000004 505253.4461674707 4233029.411718444 +4490 4534 73.76400000000001 36.052 388628.21016111854 3992019.598405965 +4490 4534 76.356 40.408 615103.7007024148 4475715.774093929 +4490 4535 78.0 36.43 500000.0 4033256.7572091157 +4490 4535 76.65 31.57 371837.114156803 3494963.294701828 +4490 4535 79.35 41.29 613084.5774909381 4573658.934893523 +4490 4536 81.005 37.915 500439.66871457937 4198063.398660751 +4490 4536 79.6505 30.7465 370771.5737498673 3403647.5986920954 +4490 4536 82.35950000000001 45.0835 607036.4524551744 4995123.273482752 +4490 4537 84.0 37.745 500000.0 4179194.5322695132 +4490 4537 82.65 29.2085 368715.9132268833 3233133.7794636637 +4490 4537 85.35 46.281499999999994 604043.4478891981 5128261.768246826 +4490 4538 87.0 38.49 500000.0 4261888.567610511 +4490 4538 85.65 28.869 368285.407464704 3195499.2555731516 +4490 4538 88.35 48.111 600526.9305268253 5331651.562255311 +4490 4539 90.0 37.870000000000005 500000.0 4193068.6369197676 +4490 4539 88.64099999999999 28.375 366784.9376347002 3140751.0850881445 +4490 4539 91.35900000000001 47.365 602653.3168932397 5248721.380409396 +4490 4540 93.0 36.42 500000.0 4032147.0882259104 +4490 4540 91.65 28.581 367923.82995369344 3163575.0553991105 +4490 4540 94.35 44.259 607806.4988133477 4903487.561231352 +4490 4541 96.005 36.365 500448.7318756652 4026043.953716178 +4490 4541 94.6505 29.0435 368554.8134208948 3214842.28658065 +4490 4541 97.35950000000001 43.686499999999995 609613.3164232087 4839887.956491769 +4490 4542 99.0 32.095 500000.0 3552386.760062522 +4490 4542 97.65 22.4965 361077.4151193422 2489406.034678087 +4490 4542 100.35 41.6935 612384.8731013039 4618474.5714256335 +4490 4543 102.0 31.909999999999997 500000.0 3531872.69214745 +4490 4543 100.65 22.208 360791.0398467104 2457452.6232682797 +4490 4543 103.35 41.611999999999995 612526.6537299638 4609422.350093691 +4490 4544 105.005 32.355000000000004 500470.6362508439 3581218.367444173 +4490 4544 103.6505 23.485500000000002 362136.8367830304 2598952.5487704924 +4490 4544 106.35950000000001 41.2245 613994.2253357271 4566396.725920691 +4490 4545 108.005 30.33 500480.82724785485 3356695.6341688256 +4490 4545 106.6505 19.404 358244.5843864592 2146943.387976246 +4490 4545 109.35950000000001 41.256 613939.4926949894 4569895.201263173 +4490 4546 111.0 31.61 500000.0 3498607.906343834 +4490 4546 109.65 19.46 358240.6679497673 2153144.1962357345 +4490 4546 112.35 43.76 608714.3276718128 4848041.902079204 +4490 4547 114.0 33.485 500000.0 3706538.802603186 +4490 4547 112.65 22.7165 361298.1560664604 2513773.3127082847 +4490 4547 115.35 44.2535 607816.5499015028 4902876.411696486 +4490 4548 117.0 36.24 500000.0 4012173.3648338104 +4490 4548 115.65 23.964000000000002 362588.33959743136 2651958.962947342 +4490 4548 118.35 48.516000000000005 599734.3847885742 5376684.683137083 +4490 4549 120.0 38.879999999999995 500000.0 4305182.264307256 +4490 4549 118.65 25.875 364690.560962789 2863681.9481411087 +4490 4549 121.35 51.885 592950.6728714798 5751409.556180853 +4490 4550 123.0 40.89 500000.0 4528357.478169169 +4490 4550 121.65 29.487 369072.5084852958 3264007.6296525 +4490 4550 124.35 52.293 592106.6498562174 5796804.249250238 +4490 4551 126.0 46.695 500000.0 5173340.809639158 +4490 4551 124.65 40.8405 386142.5864052744 4523737.7102233665 +4490 4551 127.35 52.5495 591573.6158988895 5825344.351239932 +4490 4552 129.0 45.81 500000.0 5074967.528460148 +4490 4552 127.65 41.814 387825.1720600107 4631858.753558982 +4490 4552 130.35 49.806 597176.6951073759 5520143.539010394 +4490 4553 132.0 45.650000000000006 500000.0 5057184.161550448 +4490 4553 130.65 42.743 389461.1953082485 4735053.634122378 +4490 4553 133.35 48.557 599653.8711527977 5381243.759450208 +4490 4554 134.135 47.125 434365.33445302077 5221506.61268192 +4490 4554 133.5635 45.9775 388679.2562335946 5094588.586758707 +4490 4554 134.7065 48.2725 478213.0620698525 5348769.32670989 +4558 4559 -61.83 16.2 625062.2474287438 1791414.7488421197 +4559 4326 625062.2474287438 1791414.7488421197 -61.83 16.199999999999996 +4558 4559 -63.477 14.292 448553.5799329723 1580073.314818596 +4559 4326 448553.5799329723 1580073.314818596 -63.477000000000004 14.291999999999996 +4558 4559 -60.183 18.108 798130.5083052822 2004413.514689054 +4559 4326 798130.5083052822 2004413.514689054 -60.183 18.107999999999997 +4555 4568 75.815 38.245000000000005 13571346.41197371 4235081.811126468 +4555 4568 73.8395 35.7025 13394969.385805406 3953224.0311303884 +4555 4568 77.79050000000001 40.7875 13735549.41116041 4520803.316619453 +4555 4569 80.99000000000001 38.195 14499123.99156704 4229217.605233472 +4555 4569 78.281 30.0635 14237767.19920143 3330330.2394273262 +4555 4569 83.699 46.326499999999996 14707840.295428693 5136010.312818566 +4555 4570 87.0 38.25 15500000.0 4235322.701880132 +4555 4570 84.3 28.413 15235383.968709951 3147235.594520566 +4555 4570 89.7 48.087 15701144.873015929 5331723.107633635 +4555 4571 93.005 37.805 16500440.33000274 4185928.1923137438 +4555 4571 90.3005 28.7195 16236199.339020254 3181224.6823198963 +4555 4571 95.7095 46.8905 16706492.81865827 5198731.185353191 +4555 4572 99.005 32.155 17500471.677082866 3559103.3043840607 +4555 4572 96.3005 22.232499999999998 17221621.797579724 2462072.0344894384 +4555 4572 101.7095 42.0775 17724223.947326213 4663881.725776615 +4555 4573 105.0 32.0 18500000.0 3541915.326928574 +4555 4573 102.3 22.577 18222256.469222542 2500252.3757774034 +4555 4573 107.7 41.423 18725720.816481937 4591152.023636594 +4555 4574 111.0 31.61 19500000.0 3498670.0424377522 +4555 4574 108.3 19.46 19216414.87990756 2154853.3537214193 +4555 4574 113.7 43.76 19717434.984652735 4850786.28566631 +4555 4575 117.0 36.83 20500000.0 4077717.2817049567 +4555 4575 114.3 23.609 20224372.12812555 2614632.0718334657 +4555 4575 119.7 50.051 20693364.40194895 5550111.192110945 +4555 4576 123.0 39.95 21500000.0 4424055.564131719 +4555 4576 120.3 27.701 21233632.659300923 3068280.8947124053 +4555 4576 125.7 52.199 21684593.40577218 5789025.475825784 +4555 4577 129.0 46.84 22500000.0 5189551.344625954 +4555 4577 126.3 41.485 22274494.19549935 4598038.985919056 +4555 4577 131.7 52.195 22684609.985918805 5788580.506558918 +4555 4578 133.385 46.71 23376505.435415108 5176366.500450324 +4555 4578 132.1385 45.189 23275119.506957907 5010022.442977971 +4555 4578 134.63150000000002 48.231 23472623.10988836 5344272.808180529 +4555 4579 75.815 38.245000000000005 571346.4119737085 4235081.811126468 +4555 4579 73.8395 35.7025 394969.38580540614 3953224.0311303884 +4555 4579 77.79050000000001 40.7875 735549.411160409 4520803.316619453 +4555 4580 80.99000000000001 38.195 499123.9915670393 4229217.605233472 +4555 4580 78.281 30.0635 237767.19920142955 3330330.2394273262 +4555 4580 83.699 46.326499999999996 707840.2954286941 5136010.312818566 +4555 4581 87.0 38.25 500000.0 4235322.701880132 +4555 4581 84.3 28.413 235383.96870995132 3147235.594520566 +4555 4581 89.7 48.087 701144.873015929 5331723.107633635 +4555 4582 93.005 37.805 500440.3300027402 4185928.1923137438 +4555 4582 90.3005 28.7195 236199.33902025357 3181224.6823198963 +4555 4582 95.7095 46.8905 706492.8186582705 5198731.185353191 +4555 4583 99.005 32.155 500471.67708286527 3559103.3043840607 +4555 4583 96.3005 22.232499999999998 221621.79757972556 2462072.0344894384 +4555 4583 101.7095 42.0775 724223.947326214 4663881.725776615 +4555 4584 105.0 32.0 500000.0 3541915.326928574 +4555 4584 102.3 22.577 222256.46922254318 2500252.3757774034 +4555 4584 107.7 41.423 725720.8164819366 4591152.023636594 +4555 4585 111.0 31.61 500000.0 3498670.0424377522 +4555 4585 108.3 19.46 216414.87990756042 2154853.3537214193 +4555 4585 113.7 43.76 717434.9846527361 4850786.28566631 +4555 4586 117.0 36.83 500000.0 4077717.2817049567 +4555 4586 114.3 23.609 224372.12812554726 2614632.0718334657 +4555 4586 119.7 50.051 693364.401948951 5550111.192110945 +4555 4587 123.0 39.95 500000.0 4424055.564131719 +4555 4587 120.3 27.701 233632.65930092405 3068280.8947124053 +4555 4587 125.7 52.199 684593.4057721786 5789025.475825784 +4555 4588 129.0 46.84 500000.0 5189551.344625954 +4555 4588 126.3 41.485 274494.19549934776 4598038.985919056 +4555 4588 131.7 52.195 684609.9859188057 5788580.506558918 +4555 4589 133.385 46.71 376505.4354151068 5176366.500450324 +4555 4589 132.1385 45.189 275119.5069579055 5010022.442977971 +4555 4589 134.63150000000002 48.231 472623.10988835886 5344272.808180529 +4258 4647 9.004999999999999 51.370000000000005 32500348.05019652 5690971.606736347 +4647 4326 32500348.05019652 5690971.606736347 9.004999999999987 51.36999999999999 +4258 4647 6.3004999999999995 47.68 32297394.22618539 5284264.866075603 +4647 4326 32297394.22618539 5284264.866075603 6.300499999999981 47.68 +4258 4647 11.709499999999998 55.06 32673041.836988475 6104823.288213569 +4647 4326 32673041.836988475 6104823.288213569 11.709499999999982 55.05999999999999 +4555 4652 75.06 38.230000000000004 25505253.534156736 4233104.345126611 +4555 4652 73.76400000000001 36.052 25388626.342851546 3992090.3444911274 +4555 4652 76.356 40.408 25615105.62648906 4475794.910050794 +4555 4653 78.0 36.43 26500000.0 4033328.221021122 +4555 4653 76.65 31.57 26371834.960865583 3495025.3664513687 +4555 4653 79.35 41.29 26613086.468665898 4573739.763394554 +4555 4654 81.005 37.915 27500439.67607966 4198137.725422131 +4555 4654 79.6505 30.7465 27370769.40176551 3403708.0713205994 +4555 4654 82.35950000000001 45.0835 27607038.239076577 4995211.360493926 +4555 4655 84.0 37.745 28500000.0 4179268.5315508978 +4555 4655 82.65 29.2085 28368713.70522621 3233191.261619523 +4555 4655 85.35 46.281499999999994 28604045.183503162 5128352.139812531 +4555 4656 87.0 38.49 29500000.0 4261964.001512302 +4555 4656 85.65 28.869 29368283.19190522 3195556.076930756 +4555 4656 88.35 48.111 29600528.605935283 5331745.416058843 +4555 4766 90.0 37.870000000000005 30500000.0 4193142.877001827 +4555 4766 88.64099999999999 28.375 30366782.696371876 3140806.9446686734 +4555 4766 91.35900000000001 47.365 30602655.02838273 5248813.815491257 +4555 4767 93.0 36.42 31500000.0 4032218.532741143 +4555 4767 91.65 28.581 31367921.608043224 3163631.3160088337 +4555 4767 94.35 44.259 31607808.299036328 4903574.07351398 +4555 4768 96.005 36.365 32500448.739398196 4026115.292095146 +4555 4768 94.6505 29.0435 32368552.602556057 3214899.447603491 +4555 4768 97.35950000000001 43.686499999999995 32609615.147345375 4839973.374704856 +4555 4769 99.0 32.095 33500000.0 3552449.837102678 +4555 4769 97.65 22.4965 33361075.072530005 2489450.410268307 +4555 4769 100.35 41.6935 33612386.752196155 4618556.173658257 +4555 4770 102.0 31.909999999999997 34500000.0 3531935.4103309913 +4555 4770 100.65 22.208 34360788.69219168 2457496.433621891 +4555 4770 103.35 41.611999999999995 34612528.535272025 4609503.796078316 +4555 4771 105.005 32.355000000000004 35500470.64415534 3581281.948695771 +4555 4771 103.6505 23.485500000000002 35362134.512879685 2598998.8609195156 +4555 4771 106.35950000000001 41.2245 35613996.13178544 4566477.428991941 +4555 4772 108.005 30.33 36500480.83533077 3356755.284480762 +4555 4772 106.6505 19.404 36358242.19157751 2146981.6974238595 +4555 4772 109.35950000000001 41.256 36613941.398199424 4569975.964756812 +4555 4773 111.0 31.61 37500000.0 3498670.0424377522 +4555 4773 109.65 19.46 37358238.27511643 2153182.615674606 +4555 4773 112.35 43.76 37608716.14351061 4848127.460617051 +4555 4774 114.0 33.485 38500000.0 3706604.573484267 +4555 4774 112.65 22.7165 38361295.817380816 2513818.1192309842 +4555 4774 115.35 44.2535 38607818.35029731 4902962.91347027 +4555 4775 117.0 36.24 39500000.0 4012244.4619669584 +4555 4775 115.65 23.964000000000002 39362586.02371076 2652006.2114303806 +4555 4775 118.35 48.516000000000005 39599736.046650216 5376779.306687062 +4555 4776 120.0 38.879999999999995 40500000.0 4305258.448688156 +4555 4776 118.65 25.875 40364688.28216211 2863732.9318247084 +4555 4776 121.35 51.885 40592952.219102316 5751510.567213301 +4555 4777 123.0 40.89 41500000.0 4528437.524576554 +4555 4777 121.65 29.487 41369070.30674335 3264065.6536962623 +4555 4777 124.35 52.293 41592108.18173952 5796906.031949949 +4555 4778 126.0 46.695 42500000.0 5173431.954215716 +4555 4778 124.65 40.8405 42386140.68187937 4523817.676313308 +4555 4778 127.35 52.5495 42591575.13872549 5825446.618862379 +4555 4779 129.0 45.81 43500000.0 5075056.986440469 +4555 4779 127.65 41.814 43387823.29659013 4631940.5867785495 +4555 4779 130.35 49.806 43597178.31330539 5520240.61164158 +4555 4780 132.0 45.650000000000006 44500000.0 5057273.314402805 +4555 4780 130.65 42.743 44389459.34805132 4735137.246940653 +4555 4780 133.35 48.557 44599655.53163869 5381338.460902488 +4555 4781 134.135 47.125 45434364.2400247 5221598.582090681 +4555 4781 133.5635 45.9775 45388677.39893806 5094678.3808455635 +4555 4781 134.7065 48.2725 45478212.698992066 5348863.473519839 +4555 4782 75.06 38.230000000000004 505253.5341567345 4233104.345126611 +4555 4782 73.76400000000001 36.052 388626.3428515459 3992090.3444911274 +4555 4782 76.356 40.408 615105.6264890609 4475794.910050794 +4555 4783 78.0 36.43 500000.0 4033328.221021122 +4555 4783 76.65 31.57 371834.96086558374 3495025.3664513687 +4555 4783 79.35 41.29 613086.4686658963 4573739.763394554 +4555 4784 81.005 37.915 500439.676079662 4198137.725422131 +4555 4784 79.6505 30.7465 370769.4017655096 3403708.0713205994 +4555 4784 82.35950000000001 45.0835 607038.2390765786 4995211.360493926 +4555 4785 84.0 37.745 500000.0 4179268.5315508978 +4555 4785 82.65 29.2085 368713.70522620826 3233191.261619523 +4555 4785 85.35 46.281499999999994 604045.1835031626 5128352.139812531 +4555 4786 87.0 38.49 500000.0 4261964.001512302 +4555 4786 85.65 28.869 368283.1919052191 3195556.076930756 +4555 4786 88.35 48.111 600528.6059352813 5331745.416058843 +4555 4787 90.0 37.870000000000005 500000.0 4193142.877001827 +4555 4787 88.64099999999999 28.375 366782.69637187547 3140806.9446686734 +4555 4787 91.35900000000001 47.365 602655.0283827311 5248813.815491257 +4555 4788 93.0 36.42 500000.0 4032218.532741143 +4555 4788 91.65 28.581 367921.6080432253 3163631.3160088337 +4555 4788 94.35 44.259 607808.2990363287 4903574.07351398 +4555 4789 96.005 36.365 500448.73939819523 4026115.292095146 +4555 4789 94.6505 29.0435 368552.6025560578 3214899.447603491 +4555 4789 97.35950000000001 43.686499999999995 609615.147345376 4839973.374704856 +4555 4790 99.0 32.095 500000.0 3552449.837102678 +4555 4790 97.65 22.4965 361075.0725300068 2489450.410268307 +4555 4790 100.35 41.6935 612386.7521961518 4618556.173658257 +4555 4791 102.0 31.909999999999997 500000.0 3531935.4103309913 +4555 4791 100.65 22.208 360788.69219168415 2457496.433621891 +4555 4791 103.35 41.611999999999995 612528.5352720255 4609503.796078316 +4555 4792 105.005 32.355000000000004 500470.64415533753 3581281.948695771 +4555 4792 103.6505 23.485500000000002 362134.51287968305 2598998.8609195156 +4555 4792 106.35950000000001 41.2245 613996.1317854408 4566477.428991941 +4555 4793 108.005 30.33 500480.83533077006 3356755.284480762 +4555 4793 106.6505 19.404 358242.1915775105 2146981.6974238595 +4555 4793 109.35950000000001 41.256 613941.3981994218 4569975.964756812 +4555 4794 111.0 31.61 500000.0 3498670.0424377522 +4555 4794 109.65 19.46 358238.275116427 2153182.615674606 +4555 4794 112.35 43.76 608716.1435106108 4848127.460617051 +4555 4795 114.0 33.485 500000.0 3706604.573484267 +4555 4795 112.65 22.7165 361295.81738081516 2513818.1192309842 +4555 4795 115.35 44.2535 607818.350297313 4902962.91347027 +4555 4796 117.0 36.24 500000.0 4012244.4619669584 +4555 4796 115.65 23.964000000000002 362586.0237107602 2652006.2114303806 +4555 4796 118.35 48.516000000000005 599736.0466502166 5376779.306687062 +4555 4797 120.0 38.879999999999995 500000.0 4305258.448688156 +4555 4797 118.65 25.875 364688.28216211067 2863732.9318247084 +4555 4797 121.35 51.885 592952.2191023126 5751510.567213301 +4555 4798 123.0 40.89 500000.0 4528437.524576554 +4555 4798 121.65 29.487 369070.3067433529 3264065.6536962623 +4555 4798 124.35 52.293 592108.1817395184 5796906.031949949 +4555 4799 126.0 46.695 500000.0 5173431.954215716 +4555 4799 124.65 40.8405 386140.681879375 4523817.676313308 +4555 4799 127.35 52.5495 591575.1387254914 5825446.618862379 +4555 4800 129.0 45.81 500000.0 5075056.986440469 +4555 4800 127.65 41.814 387823.2965901297 4631940.5867785495 +4555 4800 130.35 49.806 597178.3133053896 5520240.61164158 +4555 4812 132.0 45.650000000000006 500000.0 5057273.314402805 +4555 4812 130.65 42.743 389459.3480513134 4735137.246940653 +4555 4812 133.35 48.557 599655.5316386878 5381338.460902488 +4555 4822 134.135 47.125 434364.2400247033 5221598.582090681 +4555 4822 133.5635 45.9775 388677.3989380567 5094678.3808455635 +4555 4822 134.7065 48.2725 478212.6989920673 5348863.473519839 +4326 4826 -24.19 16.005 141253.85281791238 147514.69831448165 +4326 4826 -28.384 11.9235 -316990.78696722793 -299358.3191856711 +4326 4826 -19.996000000000002 20.0865 581453.2311673638 603639.1268635269 +4258 4839 10.45 51.18000000000001 -3492.965726516758 20007.21063306094 +4839 4326 -3492.965726516758 20007.21063306094 10.45 51.180000000000014 +4258 4839 6.319 47.661 -314159.39613523596 -362306.42951899226 +4839 4326 -314159.39613523596 -362306.42951899226 6.319 47.66100000000001 +4258 4839 14.581 54.699000000000005 263232.4181509587 418801.51530074736 +4839 4326 263232.4181509587 418801.51530074736 14.581 54.699000000000005 +5013 5014 -32.79 39.16 518143.2512194912 4334553.195886239 +5014 4326 518143.2512194912 4334553.195886239 -32.79 39.15999999999999 +5013 5014 -35.301 35.641 291662.37053178536 3946569.7652490884 +5014 4326 291662.37053178536 3946569.7652490884 -35.301 35.64099999999999 +5013 5014 -30.279 42.679 722942.1700274812 4728759.328935108 +5014 4326 722942.1700274812 4728759.328935108 -30.279000000000003 42.678999999999995 +5013 5015 -27.0 38.24 500000.0 4232444.063726697 +5015 4326 500000.0 4232444.063726697 -27.0 38.24 +5013 5015 -29.7 33.992000000000004 250602.605087473 3764555.710214711 +5015 4326 250602.605087473 3764555.710214711 -29.699999999999996 33.992 +5013 5015 -24.3 42.488 721898.3027412741 4707493.4105971735 +5015 4326 721898.3027412741 4707493.4105971735 -24.3 42.488 +5013 5016 -15.24 32.85 477542.3126122713 3634683.6434582025 +5016 4326 477542.3126122713 3634683.6434582025 -15.24 32.85000000000001 +5013 5016 -17.724 29.601 236180.63674593024 3277673.5179250673 +5016 4326 236180.63674593024 3277673.5179250673 -17.724 29.601000000000003 +5013 5016 -12.756 36.099000000000004 702009.2306821264 3997260.5096747587 +5016 4326 702009.2306821264 3997260.5096747587 -12.756 36.099000000000004 +4207 5018 -7.875 39.555 22080.9569503784 -12366.85735186827 +5018 4326 22080.9569503784 -12366.85735186827 -7.876182020711381 39.556590667029 +4207 5018 -9.3915 37.2105 -111816.15111472325 -271910.4641045907 +5018 4326 -111816.15111472325 -271910.4641045907 -9.392745831719148 37.21205016820614 +4207 5018 -6.358500000000001 41.899499999999996 147168.31744329393 249484.02976895115 +5018 4326 147168.31744329393 249484.02976895115 -6.359615687907505 41.90113331723073 +4326 5041 0.0 75.0 2000000.0 325449.80628614617 +4326 5041 -162.0 61.5 1002357.0911193731 5070429.156641536 +4326 5041 162.0 88.5 2051465.1893966065 2158393.5661435425 +4326 5042 0.0 -75.0 2000000.0 3674550.193713854 +4326 5042 -162.0 -88.5 1948534.8106033935 1841606.4338564575 +4326 5042 162.0 -61.5 2997642.908880627 -1070429.1566415364 +10690 5048 25.335 64.465 419924.39978573413 7149882.0006198855 +5048 4326 419924.39978573413 7149882.0006198855 25.335 64.465 +10690 5048 19.705499999999997 59.4025 86356.51130369824 6607583.548826504 +5048 4326 86356.51130369824 6607583.548826504 19.705499999999994 59.40250000000002 +10690 5048 30.9645 69.5275 654657.4899668262 7718197.781634642 +5048 4326 654657.4899668262 7718197.781634642 30.9645 69.52749999999999 +4267 5069 -95.85 36.895 13242.977000675113 1538946.815115391 +5069 4326 13242.977000675113 1538946.815115391 -95.85027224667773 36.895068305722724 +4267 5069 -121.896 25.6585 -2594144.9724414875 645896.8206771244 +5069 4326 -2594144.9724414875 645896.8206771244 -121.89690964401794 25.658773569338436 +4267 5069 -69.804 48.1315 1942365.7419270903 3062004.3483187193 +5069 4326 1942365.7419270903 3062004.3483187193 -69.80335878323075 48.131452923953965 +4269 5070 -95.85 36.895 13242.64920946045 1539001.754247737 +5070 4326 13242.64920946045 1539001.754247737 -95.85 36.894999999999996 +4269 5070 -121.896 25.6585 -2594100.0835105157 645903.0610183614 +5070 4326 -2594100.0835105157 645903.0610183614 -121.89600000000002 25.658499999999997 +4269 5070 -69.804 48.1315 1942305.234662188 3062066.943272104 +5070 4326 1942305.234662188 3062066.943272104 -69.804 48.131499999999996 +4152 5071 -95.85 36.895 13242.64920946045 1539001.754247737 +5071 4326 13242.64920946045 1539001.754247737 -95.85000746935613 36.8950065463108 +4152 5071 -121.896 25.6585 -2594100.0835105157 645903.0610183614 +5071 4326 -2594100.0835105157 645903.0610183614 -121.89601228528721 25.658503769317367 +4152 5071 -69.804 48.1315 1942305.234662188 3062066.943272104 +5071 4326 1942305.234662188 3062066.943272104 -69.80400036331353 48.13150971319875 +4759 5072 -95.85 36.895 13242.64920946045 1539001.754247737 +5072 4326 13242.64920946045 1539001.754247737 -95.85 36.894999999999996 +4759 5072 -121.896 25.6585 -2594100.0835105157 645903.0610183614 +5072 4326 -2594100.0835105157 645903.0610183614 -121.89600000000002 25.658499999999997 +4759 5072 -69.804 48.1315 1942305.234662188 3062066.943272104 +5072 4326 1942305.234662188 3062066.943272104 -69.804 48.131499999999996 +10875 5105 5.199999999999999 60.480000000000004 83503.1262081567 1276307.5792926387 +5105 4326 83503.1262081567 1276307.5792926387 5.199999999999999 60.47999999999999 +10875 5105 4.471 58.536 40065.667515230474 1060160.0255158877 +5105 4326 40065.667515230474 1060160.0255158877 4.471 58.53600000000002 +10875 5105 5.928999999999999 62.424 122165.7166695889 1492976.1858482577 +5105 4326 122165.7166695889 1492976.1858482577 5.928999999999999 62.424 +10875 5106 6.505 60.480000000000004 100274.94854215291 1276270.0074865245 +5106 4326 100274.94854215291 1276270.0074865245 6.505 60.480000000000004 +10875 5106 6.0504999999999995 58.185 73557.12625457643 1020693.3083514574 +5106 4326 73557.12625457643 1020693.3083514574 6.0504999999999995 58.18500000000001 +10875 5106 6.9595 62.775 123463.03953881885 1532107.0886426922 +5106 4326 123463.03953881885 1532107.0886426922 6.9595 62.775 +10875 5107 7.5 60.745000000000005 100000.0 1305796.994477089 +5107 4326 100000.0 1305796.994477089 7.499999999999999 60.745 +10875 5107 7.041 58.1845 72997.89299228335 1020641.3837676253 +5107 4326 72997.89299228335 1020641.3837676253 7.041 58.1845 +10875 5107 7.959 63.3055 123015.28844156748 1591236.536223419 +5107 4326 123015.28844156748 1591236.536223419 7.9590000000000005 63.305499999999995 +10875 5108 8.504999999999999 61.05 100270.11204524066 1339782.3619821658 +5108 4326 100270.11204524066 1339782.3619821658 8.504999999999999 61.05 +10875 5108 8.0505 58.305 73646.28859277641 1034058.9601439905 +5108 4326 73646.28859277641 1034058.9601439905 8.0505 58.305 +10875 5108 8.9595 63.794999999999995 122648.56742959315 1645800.3872135552 +5108 4326 122648.56742959315 1645800.3872135552 8.9595 63.795 +10875 5109 9.504999999999999 61.345 100267.5982722209 1372654.9168346124 +5109 4326 100267.5982722209 1372654.9168346124 9.504999999999999 61.345 +10875 5109 9.0505 58.7305 73963.3841602426 1081453.3531328035 +5109 4326 73963.3841602426 1081453.3531328035 9.0505 58.730500000000006 +10875 5109 9.9595 63.9595 122516.52492430487 1664137.7362095045 +5109 4326 122516.52492430487 1664137.7362095045 9.9595 63.9595 +10875 5110 10.504999999999999 62.084999999999994 100261.26103235596 1455121.1908302978 +5110 4326 100261.26103235596 1455121.1908302978 10.504999999999999 62.08499999999998 +10875 5110 10.0505 59.2185 74328.84605945279 1135813.174475435 +5110 4326 74328.84605945279 1135813.174475435 10.0505 59.218500000000006 +10875 5110 10.9595 64.9515 121716.30241379386 1774727.9621338812 +5110 4326 121716.30241379386 1774727.9621338812 10.9595 64.9515 +10875 5111 11.504999999999999 63.230000000000004 100251.36871306294 1582738.5075932695 +5111 4326 100251.36871306294 1582738.5075932695 11.505 63.23000000000001 +10875 5111 11.0505 59.315000000000005 74401.33890231284 1146563.0911221886 +5111 4326 74401.33890231284 1146563.0911221886 11.0505 59.315 +10875 5111 11.9595 67.145 119923.74457803537 2019315.6058093728 +5111 4326 119923.74457803537 2019315.6058093728 11.9595 67.145 +10875 5112 12.504999999999999 64.02 100244.48382023071 1670800.9917936982 +5112 4326 100244.48382023071 1670800.9917936982 12.504999999999999 64.02 +10875 5112 12.0505 60.294000000000004 75140.90775739649 1255630.7914835778 +5112 4326 75140.90775739649 1255630.7914835778 12.0505 60.294000000000004 +10875 5112 12.9595 67.746 119427.30890427013 2086342.4481775304 +5112 4326 119427.30890427013 2086342.4481775304 12.9595 67.74600000000001 +10875 5113 13.504999999999999 66.20500000000001 100225.19981039569 1914415.8486069785 +5113 4326 100225.19981039569 1914415.8486069785 13.504999999999999 66.20500000000001 +10875 5113 13.0505 64.2295 78185.89978835454 1694232.948190754 +5113 4326 78185.89978835454 1694232.948190754 13.0505 64.2295 +10875 5113 13.9595 68.18050000000001 119067.04885493545 2134803.3765796972 +5113 4326 119067.04885493545 2134803.3765796972 13.959500000000002 68.18050000000001 +10875 5114 14.504999999999999 66.55 100222.12382786235 1952887.8054330742 +5114 4326 100222.12382786235 1952887.8054330742 14.504999999999997 66.54999999999998 +10875 5114 14.0505 64.282 78227.25871551989 1700085.6037540315 +5114 4326 78227.25871551989 1700085.6037540315 14.0505 64.28200000000001 +10875 5114 14.9595 68.818 118536.46279262136 2205910.024418145 +5114 4326 118536.46279262136 2205910.024418145 14.9595 68.818 +10875 5115 15.505 67.75999999999999 100211.27167604417 2087831.8098866416 +5115 4326 100211.27167604417 2087831.8098866416 15.505000000000003 67.75999999999999 +10875 5115 15.0505 66.302 79832.35136986128 1925304.8588325349 +5115 4326 79832.35136986128 1925304.8588325349 15.050500000000001 66.302 +10875 5115 15.959500000000002 69.21799999999999 118202.35078450703 2250528.700318536 +5115 4326 118202.35078450703 2250528.700318536 15.9595 69.21799999999999 +10875 5116 16.5 68.185 100000.0 2135234.3185734022 +5116 4326 100000.0 2135234.3185734022 16.5 68.185 +10875 5116 16.041 67.0105 79987.2593554192 2004315.9492918951 +5116 4326 79987.2593554192 2004315.9492918951 16.041 67.0105 +10875 5116 16.959000000000003 69.3595 118064.26431124001 2266312.907640985 +5116 4326 118064.26431124001 2266312.907640985 16.959000000000003 69.3595 +10875 5117 17.505000000000003 68.94999999999999 100200.50547413217 2220565.007047128 +5117 4326 100200.50547413217 2220565.007047128 17.505000000000003 68.94999999999997 +10875 5117 17.0505 68.041 81234.62976682652 2119241.2237787414 +5117 4326 81234.62976682652 2119241.2237787414 17.0505 68.041 +10875 5117 17.959500000000002 69.859 117665.0623782904 2322034.4399270094 +5117 4326 117665.0623782904 2322034.4399270094 17.959500000000002 69.85899999999998 +10875 5118 18.505000000000003 69.165 100198.55080403379 2244548.2307738094 +5118 4326 100198.55080403379 2244548.2307738094 18.505000000000003 69.165 +10875 5118 18.0505 68.1525 81325.1533328958 2131677.3267943184 +5118 4326 81325.1533328958 2131677.3267943184 18.0505 68.1525 +10875 5118 18.959500000000002 70.17750000000001 117397.25422646326 2357566.141972793 +5118 4326 117397.25422646326 2357566.141972793 18.959500000000002 70.17750000000001 +10875 5119 19.505000000000003 69.34 100196.9576936376 2264069.9057778167 +5119 4326 100196.9576936376 2264069.9057778167 19.505000000000003 69.34 +10875 5119 19.0505 68.431 81551.57251560222 2162740.4323193505 +5119 4326 81551.57251560222 2162740.4323193505 19.0505 68.43099999999998 +10875 5119 19.959500000000002 70.249 117337.05858862799 2365542.820964153 +5119 4326 117337.05858862799 2365542.820964153 19.959500000000002 70.249 +10875 5120 20.505000000000003 69.37 100196.68440098621 2267416.518639803 +5120 4326 100196.68440098621 2267416.518639803 20.505000000000003 69.37 +10875 5120 20.0505 68.47 81583.31481986576 2167090.4654365797 +5120 4326 81583.31481986576 2167090.4654365797 20.0505 68.47 +10875 5120 20.959500000000002 70.27000000000001 117319.37353014838 2367885.6338696997 +5120 4326 117319.37353014838 2367885.6338696997 20.959500000000002 70.27 +10875 5121 21.505000000000003 69.88 100192.03009868936 2324310.7118097413 +5121 4326 100192.03009868936 2324310.7118097413 21.505000000000003 69.87999999999998 +10875 5121 21.0505 69.115 82109.5304170614 2239036.2419008287 +5121 4326 82109.5304170614 2239036.2419008287 21.0505 69.115 +10875 5121 21.959500000000002 70.64500000000001 117003.17279793943 2409722.4962451905 +5121 4326 117003.17279793943 2409722.4962451905 21.959500000000002 70.64500000000001 +10875 5122 22.505000000000003 69.81 100192.66984901413 2316501.507694771 +5122 4326 100192.66984901413 2316501.507694771 22.505000000000003 69.81 +10875 5122 22.0505 68.80199999999999 81853.88103814461 2204122.335323387 +5122 4326 81853.88103814461 2204122.335323387 22.0505 68.80199999999999 +10875 5122 22.959500000000002 70.81800000000001 116857.04831045828 2429023.816502646 +5122 4326 116857.04831045828 2429023.816502646 22.959500000000002 70.81800000000001 +10875 5123 23.505000000000003 69.885 100191.98439107188 2324868.5144880367 +5123 4326 100191.98439107188 2324868.5144880367 23.505000000000003 69.885 +10875 5123 23.0505 68.7465 81808.60747832063 2197931.6651101694 +5123 4326 81808.60747832063 2197931.6651101694 23.0505 68.7465 +10875 5123 23.959500000000002 71.02350000000001 116683.26966877512 2451951.5788910976 +5123 4326 116683.26966877512 2451951.5788910976 23.959500000000002 71.02350000000001 +10875 5124 24.505000000000003 69.875 100192.07580481983 2323752.909449078 +5124 4326 100192.07580481983 2323752.909449078 24.505000000000003 69.875 +10875 5124 24.0505 68.70949999999999 81778.43474208904 2193804.5742265056 +5124 4326 81778.43474208904 2193804.5742265056 24.0505 68.70949999999999 +10875 5124 24.959500000000002 71.04050000000001 116668.88401211574 2453848.302228167 +5124 4326 116668.88401211574 2453848.302228167 24.959500000000006 71.04050000000001 +10875 5125 25.505000000000003 69.91499999999999 100191.71011415363 2328215.337224465 +5125 4326 100191.71011415363 2328215.337224465 25.505000000000003 69.91499999999998 +10875 5125 25.0505 68.7225 81789.03509417988 2195254.6311240597 +5125 4326 81789.03509417988 2195254.6311240597 25.0505 68.72249999999998 +10875 5125 25.959500000000002 71.1075 116612.17307805848 2461323.6573925614 +5125 4326 116612.17307805848 2461323.6573925614 25.959500000000002 71.10749999999999 +10875 5126 26.505000000000003 70.455 100186.76406296129 2388460.0853027105 +5126 4326 100186.76406296129 2388460.0853027105 26.505000000000003 70.455 +10875 5126 26.0505 69.7845 82658.17111390592 2313720.5693414463 +5126 4326 82658.17111390592 2313720.5693414463 26.0505 69.7845 +10875 5126 26.959500000000002 71.1255 116596.93336349477 2463331.970933611 +5126 4326 116596.93336349477 2463331.970933611 26.959500000000002 71.1255 +10875 5127 27.505000000000003 70.545 100185.93806851355 2398501.230515779 +5127 4326 100185.93806851355 2398501.230515779 27.505000000000003 70.545 +10875 5127 27.0505 69.9645 82806.09104597905 2333800.9758036714 +5127 4326 82806.09104597905 2333800.9758036714 27.0505 69.9645 +10875 5127 27.959500000000002 71.1255 116596.93336349465 2463331.970933611 +5127 4326 116596.93336349465 2463331.970933611 27.959500000000002 71.1255 +10875 5128 28.505000000000003 70.09 100190.10910256648 2347738.6969746617 +5128 4326 100190.10910256648 2347738.6969746617 28.505000000000003 70.09 +10875 5128 28.0505 69.136 82126.70212830459 2241378.7543083066 +5128 4326 82126.70212830459 2241378.7543083066 28.0505 69.136 +10875 5128 28.959500000000002 71.04400000000001 116665.9220739318 2454238.8045227034 +5128 4326 116665.9220739318 2454238.8045227034 28.9595 71.044 +10875 5129 29.505000000000003 69.97999999999999 100191.11566422686 2335466.8256392446 +5129 4326 100191.11566422686 2335466.8256392446 29.505000000000003 69.97999999999999 +10875 5129 29.0505 69.116 82110.34806220964 2239147.7899805466 +5129 4326 82110.34806220964 2239147.7899805466 29.0505 69.116 +10875 5129 29.959500000000002 70.844 116835.07385959689 2431924.6244080933 +5129 4326 116835.07385959689 2431924.6244080933 29.959500000000002 70.844 +10875 5130 30.66 70.115 106076.16158554434 2350535.750223879 +5130 4326 106076.16158554434 2350535.750223879 30.66 70.11500000000001 +10875 5130 30.066 69.5255 83050.9505097632 2284823.4448326435 +5130 4326 83050.9505097632 2284823.4448326435 30.066 69.5255 +10875 5130 31.254 70.7045 127817.91261620981 2416469.3692129515 +5130 4326 127817.91261620981 2416469.3692129515 31.254000000000005 70.7045 +4162 5167 130.86 37.504999999999995 187622.74201442648 445074.15148826875 +4162 5167 130.725 37.4015 175653.98638667032 433614.59470438876 +4162 5167 130.995 37.6085 199558.56616360653 456550.99613755546 +4162 5168 126.55000000000001 33.375 158129.66826603498 36985.347697260964 +4162 5168 126.13600000000001 33.1635 119414.03020110371 13772.439570994582 +4162 5168 126.964 33.5865 196658.5171831613 60351.1200661491 +5132 5169 125.13499999999999 37.445 211944.75809399047 438415.019298771 +5132 5169 124.3565 34.3355 140791.73254310095 93609.68155799649 +5132 5169 125.9135 40.5545 277363.86280847027 783973.0646942208 +5132 5170 127.0 37.47 200000.0 441180.82253439684 +5132 5170 126.1 33.573 116448.57857258127 9216.248789936944 +5132 5170 127.9 41.367 275291.4291541394 874183.7572387799 +5132 5171 129.0 38.75 200000.0 583243.7456242335 +5132 5171 128.1 34.916000000000004 117765.40813763064 158182.07622253028 +5132 5171 129.9 42.583999999999996 273871.3615824492 1009346.3831054707 +5132 5172 130.505 40.185 157849.3113057473 742664.0814147395 +5132 5172 130.0505 37.6695 116239.24431507966 463744.7808531736 +5132 5172 130.9595 42.7005 196681.99669657077 1021894.6407723676 +4162 5173 125.265 36.015 223625.50588903035 279764.3310260633 +4162 5173 124.6035 34.1925 163190.13044016383 77633.3266036525 +4162 5173 125.9265 37.8375 281292.7371474503 482367.2040172197 +4162 5174 127.0 36.144999999999996 199739.91086579429 294155.88946894545 +4162 5174 126.1 34.1785 116770.19177260896 76376.85096064431 +4162 5174 127.9 38.1115 278667.24967194494 512755.02378023876 +4162 5175 126.55000000000001 33.375 157860.73962318894 36986.513387312065 +4162 5175 126.13600000000001 33.1635 119144.44337250991 13774.667288552853 +4162 5175 126.964 33.5865 196390.24456956092 60351.21705631097 +4162 5176 128.825 36.565 184077.84160770156 340771.20103119296 +4162 5176 128.0825 34.697500000000005 115679.98747851078 133960.73756657145 +4162 5176 129.5675 38.4325 249292.6721790625 548153.5639450836 +4162 5177 130.86 37.504999999999995 187367.2153509089 445074.5355482204 +4162 5177 130.725 37.4015 175398.1067958343 433615.3445947183 +4162 5177 130.995 37.6085 199303.3928817089 456551.0136546339 +4162 5178 127.77 35.89 1024365.3250401412 1765991.5736903585 +4162 5178 124.854 33.415 753979.9713957107 1494664.0997571624 +4162 5178 130.68599999999998 38.365 1278330.0587556285 2045300.2127344462 +4737 5179 128.495 34.435 1091416.7370317057 1605023.4891069229 +5179 4326 1091416.7370317057 1605023.4891069229 128.495 34.435 +4737 5179 123.2885 29.183500000000002 590331.9675601999 1029851.0911930249 +5179 4326 590331.9675601999 1029851.0911930249 123.28850000000001 29.18350000000001 +4737 5179 133.7015 39.6865 1531963.459465834 2205571.5419439645 +5179 4326 1531963.459465834 2205571.5419439645 133.7015 39.6865 +4737 5180 125.265 36.015 223888.8782125321 279741.52208200446 +5180 4326 223888.8782125321 279741.52208200446 125.265 36.015 +4737 5180 124.6035 34.1925 163452.15980109817 77587.3851994754 +5180 4326 163452.15980109817 77587.3851994754 124.60350000000001 34.1925 +4737 5180 125.9265 37.8375 281556.9024555754 482367.83834112575 +5180 4326 281556.9024555754 482367.83834112575 125.9265 37.837500000000006 +4737 5181 127.0 36.144999999999996 200000.0 294133.90133589634 +5181 4326 200000.0 294133.90133589634 127.00000000000001 36.14499999999999 +4737 5181 126.1 34.1785 117026.74300291153 76329.46576396789 +5181 4326 117026.74300291153 76329.46576396789 126.1 34.1785 +4737 5181 127.9 38.1115 278930.15937288816 512758.85089636315 +5181 4326 278930.15937288816 512758.85089636315 127.9 38.1115 +4737 5182 126.55000000000001 33.375 158124.68383127602 36930.913340072264 +5182 4326 158124.68383127602 36930.913340072264 126.55000000000001 33.37500000000001 +4737 5182 126.13600000000001 33.1635 119404.43959962815 13715.57320211723 +5182 4326 119404.43959962815 13715.57320211723 126.136 33.1635 +4737 5182 126.964 33.5865 196658.11928415333 60299.13829363504 +5182 4326 196658.11928415333 60299.13829363504 126.964 33.58650000000001 +4737 5183 128.825 36.565 184334.66472896753 340753.70393490215 +5183 4326 184334.66472896753 340753.70393490215 128.825 36.565000000000005 +4737 5183 128.0825 34.697500000000005 115934.75905724421 133919.3642330495 +5183 4326 115934.75905724421 133919.3642330495 128.0825 34.69750000000001 +4737 5183 129.5675 38.4325 249550.9465748574 548160.286500724 +5183 4326 249550.9465748574 548160.286500724 129.5675 38.4325 +4737 5184 130.86 37.504999999999995 187621.26010583123 445068.26655582164 +5184 4326 187621.26010583123 445068.26655582164 130.86000000000004 37.50499999999999 +4737 5184 130.725 37.4015 175651.07190517266 433607.4840141237 +5184 4326 175651.07190517266 433607.4840141237 130.725 37.4015 +4737 5184 130.995 37.6085 199558.5133037192 456546.3396341577 +5184 4326 199558.5133037192 456546.3396341577 130.99500000000003 37.6085 +4737 5185 125.265 36.015 223888.8782125321 379741.52208200446 +5185 4326 223888.8782125321 379741.52208200446 125.265 36.015 +4737 5185 124.6035 34.1925 163452.15980109817 177587.3851994754 +5185 4326 163452.15980109817 177587.3851994754 124.60350000000001 34.1925 +4737 5185 125.9265 37.8375 281556.9024555754 582367.8383411258 +5185 4326 281556.9024555754 582367.8383411258 125.9265 37.837500000000006 +4737 5186 127.0 35.735 200000.0 348641.16630314605 +5186 4326 200000.0 348641.16630314605 127.00000000000001 35.73500000000001 +4737 5186 126.1 33.3995 116271.83132303639 89919.78645303479 +5186 4326 116271.83132303639 89919.78645303479 126.1 33.3995 +4737 5186 127.9 38.070499999999996 278974.26479557867 608207.7892007965 +5186 4326 278974.26479557867 608207.7892007965 127.9 38.0705 +4737 5187 128.825 36.565 184334.66472896753 440753.70393490215 +5187 4326 184334.66472896753 440753.70393490215 128.825 36.565000000000005 +4737 5187 128.0825 34.697500000000005 115934.75905724421 233919.3642330495 +5187 4326 115934.75905724421 233919.3642330495 128.0825 34.69750000000001 +4737 5187 129.5675 38.4325 249550.9465748574 648160.286500724 +5187 4326 249550.9465748574 648160.286500724 129.5675 38.4325 +4737 5188 130.86 37.504999999999995 187621.26010583123 545068.2665558216 +5188 4326 187621.26010583123 545068.2665558216 130.86000000000004 37.50499999999999 +4737 5188 130.725 37.4015 175651.07190517266 533607.4840141237 +5188 4326 175651.07190517266 533607.4840141237 130.725 37.4015 +4737 5188 130.995 37.6085 199558.5133037192 556546.3396341577 +5188 4326 199558.5133037192 556546.3396341577 130.99500000000003 37.6085 +4818 5221 34.9916666666667 49.394999999999996 -544115.4743788175 -1144058.330762452 +4818 5221 30.2801666666667 47.896499999999996 -909824.6302594767 -1264448.8861158604 +4818 5221 39.7031666666667 50.8935 -196913.1563122908 -1000407.5204986329 +4326 5223 11.585 -0.8300000000000001 453825.2969111297 408257.57489238586 +4326 5223 8.9435 -3.665 160418.15062684793 94322.4293246012 +4326 5223 14.2265 2.0049999999999994 747665.6462115846 721782.0751507422 +5229 5224 33.1416666666667 49.82 6082359.166271513 5671775.905165402 +5229 5224 30.0951666666667 48.704 6173586.514484828 5908896.694015449 +5229 5224 36.1881666666667 50.936 5981050.132071087 5443530.68968815 +5229 5225 33.1416666666667 49.82 -5671775.905165402 -6082359.166271513 +5229 5225 30.0951666666667 48.704 -5908896.694015449 -6173586.514484828 +5229 5225 36.1881666666667 50.936 -5443530.68968815 -5981050.132071087 +4244 5234 80.795 7.870000000000001 202567.7069487761 296147.8092206919 +5234 4326 202567.7069487761 296147.8092206919 80.79700990787711 7.870276956186294 +4244 5234 79.7555 6.061 87522.03461715997 96225.5917711666 +5234 4326 87522.03461715997 96225.5917711666 79.75762666392372 6.061398156848199 +4244 5234 81.8345 9.679 316617.6806240241 496374.1268771953 +5234 4326 316617.6806240241 496374.1268771953 81.836393470732 9.679152205981627 +5233 5235 80.795 7.870000000000001 502567.4895022098 596148.7767274189 +5235 4326 502567.4895022098 596148.7767274189 80.79701476957871 7.870275855845422 +5233 5235 79.7555 6.061 387521.81630185666 396226.5596988827 +5235 4326 387521.81630185666 396226.5596988827 79.75764950697788 6.061385538249744 +5233 5235 81.8345 9.679 616617.4641924852 796375.0937209746 +5235 4326 616617.4641924852 796375.0937209746 81.83637959551672 9.679163618484218 +4258 5243 10.45 51.18000000000001 -3492.965726516758 20007.21063306094 +5243 4326 -3492.965726516758 20007.21063306094 10.45 51.180000000000014 +4258 5243 6.319 47.661 -314159.39613523596 -362306.42951899226 +5243 4326 -314159.39613523596 -362306.42951899226 6.319 47.66100000000001 +4258 5243 14.581 54.699000000000005 263232.4181509587 418801.51530074736 +5243 4326 263232.4181509587 418801.51530074736 14.581 54.699000000000005 +5246 5247 113.87 5.16 464814.1368795763 570847.0254481743 +5247 4326 464814.1368795763 570847.0254481743 113.87 5.159999999999991 +5246 5247 112.52000000000001 4.125 315121.573740434 456197.99284757714 +5247 4326 315121.573740434 456197.99284757714 112.52000000000001 4.125000000000004 +5246 5247 115.22 6.194999999999999 614117.4758810892 685693.1969823268 +5247 4326 614117.4758810892 685693.1969823268 115.22000000000001 6.195000000000004 +5252 5253 27.060000000000002 39.305 505175.19627030275 4352366.311126694 +5253 4326 505175.19627030275 4352366.311126694 27.060000000000002 39.304999999999986 +5252 5253 25.764 36.7805 389663.6903709623 4072864.472043873 +5253 4326 389663.6903709623 4072864.472043873 25.764 36.78050000000001 +5252 5253 28.356 41.829499999999996 612646.2184540816 4633588.241025421 +5253 4326 612646.2184540816 4633588.241025421 28.356 41.82949999999999 +5252 5254 30.0 38.760000000000005 500000.0 4291860.818444361 +5254 4326 500000.0 4291860.818444361 29.999999999999996 38.760000000000005 +5252 5254 28.65 36.330000000000005 378784.7836554139 4023006.2588247578 +5254 4326 378784.7836554139 4023006.2588247578 28.65 36.330000000000005 +5252 5254 31.35 41.19 613257.1163049961 4562552.67019949 +5254 4326 613257.1163049961 4562552.67019949 31.35 41.190000000000005 +5252 5255 33.0 39.019999999999996 500000.0 4320724.2981298985 +5255 4326 500000.0 4320724.2981298985 33.0 39.01999999999999 +5252 5255 31.65 36.275 378699.6241874654 4016902.693698952 +5255 4326 378699.6241874654 4016902.693698952 31.65 36.275 +5252 5255 34.35 41.765 612260.3006979598 4626416.190962563 +5255 4326 612260.3006979598 4626416.190962563 34.35 41.765 +5252 5256 36.0 38.980000000000004 500000.0 4316283.678885887 +5256 4326 500000.0 4316283.678885887 36.0 38.98 +5252 5256 34.65 36.127 378471.0244705966 4000478.818615236 +5256 4326 378471.0244705966 4000478.818615236 34.65 36.127 +5252 5256 37.35 41.833 612141.6632563721 4633969.146814773 +5256 4326 612141.6632563721 4633969.146814773 37.35 41.83299999999999 +5252 5257 39.0 38.925 500000.0 4310177.877267003 +5257 4326 500000.0 4310177.877267003 39.0 38.92499999999999 +5252 5257 37.65 36.8865 379652.7281245715 4084766.236895666 +5257 4326 379652.7281245715 4084766.236895666 37.65 36.8865 +5252 5257 40.35 40.963499999999996 613646.6343391453 4537397.656371705 +5257 4326 613646.6343391453 4537397.656371705 40.35 40.96349999999999 +5252 5258 42.0 39.31 500000.0 4352919.701432433 +5258 4326 500000.0 4352919.701432433 42.0 39.31 +5252 5258 40.65 37.249 380224.228989825 4124999.175141325 +5258 4326 380224.228989825 4124999.175141325 40.65 37.24900000000001 +5252 5258 43.35 41.371 612944.5673306523 4582655.143343801 +5258 4326 612944.5673306523 4582655.143343801 43.35 41.371 +5252 5259 44.165 38.995000000000005 427661.3231210481 4318280.608319119 +5259 4326 427661.3231210481 4318280.608319119 44.165 38.995000000000005 +5252 5259 43.5665 37.1725 372686.9745012755 4116617.312324447 +5259 4326 372686.9745012755 4116617.312324447 43.5665 37.17250000000001 +5252 5259 44.7635 40.8175 480047.2375658519 4520333.191886302 +5259 4326 480047.2375658519 4520333.191886302 44.7635 40.8175 +5264 5266 90.435 27.515 292977.68977672467 3044771.5747607923 +5266 4326 292977.68977672467 3044771.5747607923 90.435 27.514999999999997 +5264 5266 88.9095 26.781499999999998 141550.21531754296 2963885.168860346 +5266 4326 141550.21531754296 2963885.168860346 88.9095 26.781499999999998 +5264 5266 91.9605 28.2485 442415.1260470247 3127539.341525073 +5266 4326 442415.1260470247 3127539.341525073 91.9605 28.248499999999996 +5252 5269 27.060000000000002 39.305 9505175.196270302 4352366.311126694 +5269 4326 9505175.196270302 4352366.311126694 27.05999999999999 39.304999999999986 +5252 5269 25.764 36.7805 9389663.690370962 4072864.472043873 +5269 4326 9389663.690370962 4072864.472043873 25.763999999999996 36.78050000000001 +5252 5269 28.356 41.829499999999996 9612646.218454082 4633588.241025421 +5269 4326 9612646.218454082 4633588.241025421 28.356 41.82949999999999 +5252 5270 30.0 38.760000000000005 10500000.0 4291860.818444361 +5270 4326 10500000.0 4291860.818444361 29.999999999999996 38.760000000000005 +5252 5270 28.65 36.330000000000005 10378784.783655414 4023006.2588247578 +5270 4326 10378784.783655414 4023006.2588247578 28.650000000000006 36.330000000000005 +5252 5270 31.35 41.19 10613257.116304995 4562552.67019949 +5270 4326 10613257.116304995 4562552.67019949 31.349999999999998 41.190000000000005 +5252 5271 33.0 39.019999999999996 11500000.0 4320724.2981298985 +5271 4326 11500000.0 4320724.2981298985 33.0 39.01999999999999 +5252 5271 31.65 36.275 11378699.624187466 4016902.693698952 +5271 4326 11378699.624187466 4016902.693698952 31.650000000000002 36.275 +5252 5271 34.35 41.765 11612260.30069796 4626416.190962563 +5271 4326 11612260.30069796 4626416.190962563 34.35 41.765 +5252 5272 36.0 38.980000000000004 12500000.0 4316283.678885887 +5272 4326 12500000.0 4316283.678885887 36.0 38.98 +5252 5272 34.65 36.127 12378471.024470598 4000478.818615236 +5272 4326 12378471.024470598 4000478.818615236 34.65000000000001 36.127 +5252 5272 37.35 41.833 12612141.663256371 4633969.146814773 +5272 4326 12612141.663256371 4633969.146814773 37.35 41.83299999999999 +5252 5273 39.0 38.925 13500000.0 4310177.877267003 +5273 4326 13500000.0 4310177.877267003 39.0 38.92499999999999 +5252 5273 37.65 36.8865 13379652.728124572 4084766.236895666 +5273 4326 13379652.728124572 4084766.236895666 37.650000000000006 36.8865 +5252 5273 40.35 40.963499999999996 13613646.634339144 4537397.656371705 +5273 4326 13613646.634339144 4537397.656371705 40.34999999999999 40.96349999999999 +5252 5274 42.0 39.31 14500000.0 4352919.701432433 +5274 4326 14500000.0 4352919.701432433 42.0 39.31 +5252 5274 40.65 37.249 14380224.228989825 4124999.175141325 +5274 4326 14380224.228989825 4124999.175141325 40.64999999999999 37.24900000000001 +5252 5274 43.35 41.371 14612944.567330653 4582655.143343801 +5274 4326 14612944.567330653 4582655.143343801 43.35000000000001 41.371 +5252 5275 44.165 38.995000000000005 15427661.323121049 4318280.608319119 +5275 4326 15427661.323121049 4318280.608319119 44.165000000000006 38.995000000000005 +5252 5275 43.5665 37.1725 15372686.974501276 4116617.312324447 +5275 4326 15372686.974501276 4116617.312324447 43.566500000000005 37.17250000000001 +5252 5275 44.7635 40.8175 15480047.237565853 4520333.191886302 +5275 4326 15480047.237565853 4520333.191886302 44.763500000000015 40.8175 +5264 5292 90.74 27.71 250657.49292207314 566304.8008957007 +5292 4326 250657.49292207314 566304.8008957007 90.74 27.71 +5264 5292 90.488 27.368 225729.19004877863 528430.9459950537 +5292 4326 225729.19004877863 528430.9459950537 90.488 27.367999999999995 +5264 5292 90.99199999999999 28.052 275430.76089891954 604231.3765084236 +5292 4326 275430.76089891954 604231.3765084236 90.99199999999999 28.052 +5264 5293 89.545 27.015 249503.79141296505 489292.3589963368 +5293 4326 249503.79141296505 489292.3589963368 89.545 27.015 +5264 5293 89.2885 26.7405 223985.48004568674 458904.02130224137 +5293 4326 223985.48004568674 458904.02130224137 89.2885 26.7405 +5264 5293 89.8015 27.2895 274898.40951655817 519733.6290783142 +5293 4326 274898.40951655817 519733.6290783142 89.8015 27.289500000000004 +5264 5294 89.85499999999999 26.994999999999997 250496.29639873246 487076.28701238474 +5294 4326 250496.29639873246 487076.28701238474 89.85499999999999 26.994999999999997 +5264 5294 89.65249999999999 26.729499999999998 230350.44997844248 457673.7489492181 +5294 4326 230350.44997844248 457673.7489492181 89.65249999999999 26.729499999999998 +5264 5294 90.0575 27.2605 270547.74616925116 516512.18901961995 +5294 4326 270547.74616925116 516512.18901961995 90.0575 27.260500000000004 +5264 5295 89.955 28.025 242296.76462705282 601214.7127923588 +5295 4326 242296.76462705282 601214.7127923588 89.955 28.025 +5264 5295 89.4915 27.7505 196581.55729486758 570910.4058534275 +5295 4326 196581.55729486758 570910.4058534275 89.4915 27.750499999999995 +5264 5295 90.4185 28.2995 287780.6053076551 631693.1011515926 +5295 4326 287780.6053076551 631693.1011515926 90.4185 28.2995 +5264 5296 89.14500000000001 27.32 150529.76850586268 523488.64360162104 +5296 4326 150529.76850586268 523488.64360162104 89.14500000000001 27.320000000000004 +5264 5296 88.92450000000001 27.05 128411.50033488126 493761.8970371471 +5296 4326 128411.50033488126 493761.8970371471 88.92450000000001 27.05 +5264 5296 89.3655 27.59 172543.6340786467 553252.7235891186 +5296 4326 172543.6340786467 553252.7235891186 89.3655 27.59 +5264 5297 91.13 27.740000000000002 249671.3435184967 569629.2384821293 +5297 4326 249671.3435184967 569629.2384821293 91.13 27.739999999999995 +5264 5297 90.806 27.425 217633.4951968032 534765.8008256033 +5297 4326 217633.4951968032 534765.8008256033 90.806 27.424999999999997 +5264 5297 91.454 28.055 281525.4355403665 604578.332843224 +5297 4326 281525.4355403665 604578.332843224 91.45400000000001 28.054999999999996 +5264 5298 91.225 27.27 249174.8605024002 517547.84731169185 +5298 4326 249174.8605024002 517547.84731169185 91.225 27.269999999999996 +5264 5298 90.9775 26.964 224599.15946919378 483667.0917909187 +5298 4326 224599.15946919378 483667.0917909187 90.9775 26.964 +5264 5298 91.4725 27.576 273616.3559591173 551478.5625496027 +5298 4326 273616.3559591173 551478.5625496027 91.4725 27.575999999999993 +5264 5299 89.34 27.485 249011.74469833507 541371.8996413252 +5299 4326 249011.74469833507 541371.8996413252 89.34 27.484999999999996 +5264 5299 89.14200000000001 27.2105 229393.53625169388 510971.8880629325 +5299 4326 229393.53625169388 510971.8880629325 89.14200000000001 27.2105 +5264 5299 89.538 27.7595 268532.9431547752 571804.2965197987 +5299 4326 268532.9431547752 571804.2965197987 89.538 27.759499999999996 +5264 5300 91.28 26.98 243050.92793061846 485416.15317369346 +5300 4326 243050.92793061846 485416.15317369346 91.28000000000002 26.980000000000004 +5264 5300 91.028 26.8 217983.47819547256 465510.468472471 +5300 4326 217983.47819547256 465510.468472471 91.028 26.8 +5264 5300 91.532 27.16 268038.77115339244 505372.13760248106 +5300 4326 268038.77115339244 505372.13760248106 91.532 27.16 +5264 5301 89.85499999999999 27.665 250493.3218912428 561318.1437374284 +5301 4326 250493.3218912428 561318.1437374284 89.85499999999999 27.665 +5264 5301 89.65249999999999 27.4805 230481.14227347335 540888.7331799348 +5301 4326 230481.14227347335 540888.7331799348 89.65249999999999 27.480499999999996 +5264 5301 90.0575 27.849500000000003 270438.39938292693 581780.8914018604 +5301 4326 270438.39938292693 581780.8914018604 90.0575 27.849500000000003 +5264 5302 91.75999999999999 27.02 269185.9043404914 489861.0737021272 +5302 4326 269185.9043404914 489861.0737021272 91.75999999999999 27.019999999999992 +5264 5302 91.427 26.813 236114.5354792495 466917.9449716094 +5302 4326 236114.5354792495 466917.9449716094 91.427 26.813 +5264 5302 92.09299999999999 27.227 302136.28001996817 512892.66270122584 +5302 4326 302136.28001996817 512892.66270122584 92.09299999999999 27.227 +5264 5303 89.06 27.04 249338.5350162885 492062.4654833046 +5303 4326 249338.5350162885 492062.4654833046 89.06 27.04 +5264 5303 88.77199999999999 26.824 220707.4206581396 468163.108774262 +5303 4326 220707.4206581396 468163.108774262 88.77199999999999 26.823999999999998 +5264 5303 89.348 27.256 277860.2641065016 516027.8373551206 +5303 4326 277860.2641065016 516027.8373551206 89.348 27.256 +5264 5304 90.39500000000001 26.98 262739.9698950513 485420.7003165069 +5304 4326 262739.9698950513 485420.7003165069 90.39500000000001 26.979999999999993 +5264 5304 90.0485 26.755 228299.1298955356 460502.5026098988 +5304 4326 228299.1298955356 460502.5026098988 90.0485 26.755 +5264 5304 90.7415 27.205000000000002 297044.0829722276 510434.46901098266 +5304 4326 297044.0829722276 510434.46901098266 90.7415 27.205000000000002 +5264 5305 89.495 27.575000000000003 244569.0203548174 551346.138808948 +5305 4326 244569.0203548174 551346.138808948 89.495 27.575000000000003 +5264 5305 89.2475 27.183500000000002 220024.20250323857 507999.15614446625 +5305 4326 220024.20250323857 507999.15614446625 89.2475 27.1835 +5264 5305 89.74249999999999 27.9665 268940.5321487677 594744.2259098291 +5305 4326 268940.5321487677 594744.2259098291 89.74249999999999 27.9665 +5264 5306 91.75 27.25 250000.0 515331.66733369883 +5306 4326 250000.0 515331.66733369883 91.75 27.25 +5264 5306 91.408 27.034000000000002 216064.92339458398 491443.65728400275 +5306 4326 216064.92339458398 491443.65728400275 91.408 27.034000000000002 +5264 5306 92.092 27.465999999999998 283804.24553023215 539312.9826633385 +5306 4326 283804.24553023215 539312.9826633385 92.092 27.465999999999998 +5264 5307 90.51 27.46 250988.4783531876 538601.6246574963 +5307 4326 250988.4783531876 538601.6246574963 90.51 27.46 +5264 5307 90.28500000000001 27.163 228691.02375838484 505709.7297780169 +5307 4326 228691.02375838484 505709.7297780169 90.28500000000001 27.162999999999993 +5264 5307 90.735 27.756999999999998 273166.72149742447 571535.2222539252 +5307 4326 273166.72149742447 571535.2222539252 90.735 27.75699999999999 +5264 5308 90.175 27.005 250827.08750182297 488184.33969673514 +5308 4326 250827.08750182297 488184.33969673514 90.175 27.004999999999995 +5264 5308 90.0175 26.8295 235172.20514874696 468747.2343055997 +5308 4326 235172.20514874696 468747.2343055997 90.0175 26.829499999999996 +5264 5308 90.3325 27.1805 266433.4211399427 507641.4501643544 +5308 4326 266433.4211399427 507641.4501643544 90.3325 27.180500000000006 +5264 5309 90.125 27.595 250822.72636806377 553561.2157034124 +5309 4326 250822.72636806377 553561.2157034124 90.125 27.595000000000002 +5264 5309 89.7515 27.1585 213806.23280335363 505245.497326802 +5309 4326 213806.23280335363 505245.497326802 89.7515 27.158499999999993 +5264 5309 90.4985 28.031499999999998 287547.06892702106 601991.3655755981 +5309 4326 287547.06892702106 601991.3655755981 90.4985 28.031499999999998 +5264 5310 91.555 27.685000000000002 248849.12518467542 563534.4727882622 +5310 4326 248849.12518467542 563534.4727882622 91.555 27.684999999999995 +5264 5310 91.3615 27.401500000000002 229709.0005984325 532135.8999851542 +5310 4326 229709.0005984325 532135.8999851542 91.3615 27.401500000000002 +5264 5310 91.74849999999999 27.9685 267890.6815674949 594964.2566012726 +5310 4326 267890.6815674949 594964.2566012726 91.74849999999999 27.968499999999995 +5264 5311 90.86 27.08 249338.769623601 496494.6440993678 +5311 4326 249338.769623601 496494.6440993678 90.86 27.08 +5264 5311 90.563 26.801 219806.64033995295 465616.7821773635 +5311 4326 219806.64033995295 465616.7821773635 90.563 26.801000000000002 +5264 5311 91.157 27.359 278725.0001282481 527443.2219350189 +5311 4326 278725.0001282481 527443.2219350189 91.157 27.359 +4258 5316 -7.195 62.82 190058.04966284925 968315.8730812976 +5316 4326 190058.04966284925 968315.8730812976 -7.195000000000001 62.82 +4258 5316 -13.2385 60.227999999999994 -145362.23955046368 695799.6027778713 +5316 4326 -145362.23955046368 695799.6027778713 -13.238499999999998 60.22799999999999 +4258 5316 -1.1515000000000004 65.412 471339.81583301304 1269861.1640295405 +5316 4326 471339.81583301304 1269861.1640295405 -1.1514999999999993 65.412 +4269 5320 -84.755 49.285 945280.0221393809 6103826.911247029 +5320 4326 945280.0221393809 6103826.911247029 -84.755 49.285000000000004 +4269 5320 -94.1195 42.4315 166759.65024790284 5398809.591705416 +5320 4326 166759.65024790284 5398809.591705416 -94.1195 42.4315 +4269 5320 -75.3905 56.1385 1535716.961648337 6895638.030085483 +5320 4326 1535716.961648337 6895638.030085483 -75.39050000000002 56.138499999999986 +4617 5321 -84.755 49.285 945280.0221393809 6103826.911247029 +5321 4326 945280.0221393809 6103826.911247029 -84.75500560391868 49.285009196801326 +4617 5321 -94.1195 42.4315 166759.65024790284 5398809.591705416 +5321 4326 166759.65024790284 5398809.591705416 -94.11950754293277 42.43150757194957 +4617 5321 -75.3905 56.1385 1535716.961648337 6895638.030085483 +5321 4326 1535716.961648337 6895638.030085483 -75.39050328328447 56.13851060452789 +5324 5325 -18.21 64.775 1737577.9596630577 275151.4256193623 +5325 4326 1737577.9596630577 275151.4256193623 -18.210000000000004 64.775 +5324 5325 -29.604 60.4415 1117231.586074329 -159557.42222778517 +5325 4326 1117231.586074329 -159557.42222778517 -29.604 60.4415 +5324 5325 -6.8160000000000025 69.1085 2183352.8402514625 805282.0216167318 +5325 4326 2183352.8402514625 805282.0216167318 -6.816000000000008 69.1085 +4820 5329 9.997280555556003 0.025000000000000355 4655168.939921293 902755.7997260778 +4820 5329 7.96778055555599 -3.8135000000000003 4429949.9298448535 479315.4622554415 +4820 5329 12.026780555556002 3.8635 4880387.94999773 1326208.6943478482 +4813 5330 -1.3427194444440005 -1.4700000000000006 3396738.9944830993 737940.9638813554 +4813 5330 -10.617219444444004 -8.166 2367523.074821149 -3258.196323510143 +4813 5330 7.931780555555989 5.225999999999999 4425954.914145049 1476883.4588891638 +4804 5331 12.937280555556 -4.21 4981428.555405268 435499.60194314946 +4804 5331 12.005780555555987 -6.307 4878057.524172843 203339.79288318683 +4804 5331 13.868780555556 -2.1130000000000004 5084799.586637691 667026.2947649076 +4208 5337 -33.0 -10.055 500000.0000000007 8888493.940152954 +5337 4326 500000.0000000007 8888493.940152954 -32.998559095941644 -10.056477462042976 +4208 5337 -35.7 -19.104499999999998 215908.3616283437 7885391.614904958 +5337 4326 215908.3616283437 7885391.614904958 -35.69862763055271 -19.106109682025174 +4208 5337 -30.3 -1.0055000000000014 800520.1996019785 9888736.387938004 +5337 4326 800520.1996019785 9888736.387938004 -30.298462328940673 -1.0068481977168888 +5340 5343 -72.045 -44.08 1496395.5745160165 5119253.333074568 +5343 4326 1496395.5745160165 5119253.333074568 -72.04500309904554 -44.0800057938293 +5340 5343 -73.4355 -51.208 1399684.6963956135 4325760.386603653 +5343 4326 1399684.6963956135 4325760.386603653 -73.4355037468338 -51.20800587885227 +5340 5343 -70.6545 -36.952 1619843.5292985232 5909935.657918259 +5343 4326 1619843.5292985232 5909935.657918259 -70.65450263248768 -36.95200560711303 +5340 5344 -68.995 -39.489999999999995 2500430.128716888 5629061.85323763 +5344 4326 2500430.128716888 5629061.85323763 -68.9950025325959 -39.49000573442205 +5340 5344 -70.3495 -53.359 2410153.9984302735 4086543.308855072 +5344 4326 2410153.9984302735 4086543.308855072 -70.34950347605921 -53.3590059942089 +5340 5344 -67.64049999999999 -25.621 2636551.882860192 7166418.092791674 +5344 4326 2636551.882860192 7166418.092791674 -67.64050203294576 -25.621005118029554 +5340 5345 -65.995 -38.445 3500436.4952991144 5745072.390114447 +5345 4326 3500436.4952991144 5745072.390114447 -65.99500214668497 -38.445005757426344 +5340 5345 -67.3495 -53.4435 3410331.8458527247 4077139.734150365 +5345 4326 3410331.8458527247 4077139.734150365 -67.3495030283741 -53.44350607668339 +5340 5345 -64.64049999999999 -23.446500000000004 3638925.707792303 7407323.661403973 +5345 4326 3638925.707792303 7407323.661403973 -64.64050169770537 -23.446505023600515 +5340 5346 -63.0 -38.449999999999996 4500000.0 5744517.377148977 +5346 4326 4500000.0 5744517.377148977 -63.000001792549206 -38.450005807971195 +5340 5346 -64.35 -53.263999999999996 4409920.926454235 4097114.5940152276 +5346 4326 4409920.926454235 4097114.5940152276 -64.35000255481677 -53.264006145064144 +5340 5346 -61.65 -23.635999999999996 4637757.205516055 7386341.366204695 +5346 4326 4637757.205516055 7386341.366204695 -61.65000139550878 -23.63600506533824 +5340 5347 -60.004999999999995 -31.215000000000003 5499523.553412868 6547154.062451034 +5347 4326 5499523.553412868 6547154.062451034 -60.005001313086154 -31.215005520022018 +5340 5347 -61.3595 -38.2755 5381037.242415195 5763013.112556886 +5347 4326 5381037.242415195 5763013.112556886 -61.359501592525554 -38.27550582491798 +5340 5347 -58.6505 -24.154500000000002 5637158.082468391 7328903.714567608 +5347 4326 5637158.082468391 7328903.714567608 -58.650501090482976 -24.15450512269083 +5340 5348 -56.995000000000005 -31.715000000000003 6500473.921296752 6491715.31532605 +5348 4326 6500473.921296752 6491715.31532605 -56.99500098400131 -31.715005572918933 +5340 5348 -58.3495 -37.9025 6381310.61709509 5804431.016382167 +5348 4326 6381310.61709509 5804431.016382167 -58.34950122389435 -37.90250584615986 +5340 5348 -55.6405 -25.5275 6636658.017608241 7176778.006047958 +5348 4326 6636658.017608241 7176778.006047958 -55.640500784399 -25.527505228314144 +5340 5349 -54.575 -26.799999999999997 7442827.245918538 7036366.472780928 +5349 4326 7442827.245918538 7036366.472780928 -54.57500067852662 -26.800005311937145 +5340 5349 -55.4075 -27.979 7361520.987407277 6905053.10750343 +5349 4326 7361520.987407277 6905053.10750343 -55.40750077614469 -27.979005377698446 +5340 5349 -53.7425 -25.621 7525862.526141999 7167093.5790157635 +5349 4326 7525862.526141999 7167093.5790157635 -53.742500583048965 -25.621005243026154 +5354 5355 -63.0 -16.27 500000.0 8201198.35277386 +5355 4326 500000.0 8201198.35277386 -63.0 -16.27 +5354 5355 -65.7 -22.21 221641.8211117722 7541448.237201465 +5355 4326 221641.8211117722 7541448.237201465 -65.7 -22.21 +5354 5355 -60.3 -10.33 795707.5431674824 8856851.108460218 +5355 4326 795707.5431674824 8856851.108460218 -60.300000000000004 -10.329999999999997 +5354 5356 -67.83 -16.34 624973.6207069298 8193096.09934568 +5356 4326 624973.6207069298 8193096.09934568 -67.83 -16.34 +5354 5356 -69.477 -22.253 450851.06196695747 7539091.578249639 +5356 4326 450851.06196695747 7539091.578249639 -69.477 -22.252999999999997 +5354 5356 -66.18299999999999 -10.427 808435.8880008359 8846003.189472778 +5356 4326 808435.8880008359 8846003.189472778 -66.18299999999999 -10.426999999999998 +5354 5357 -58.760000000000005 -18.22 313890.4905188028 7984580.298072841 +5357 4326 313890.4905188028 7984580.298072841 -58.76000000000001 -18.219999999999995 +5354 5357 -59.876 -19.975 199010.33727416967 7788703.081300434 +5357 4326 199010.33727416967 7788703.081300434 -59.876 -19.974999999999998 +5354 5357 -57.644000000000005 -16.465 431257.8167083038 8179518.295375724 +5357 4326 431257.8167083038 8179518.295375724 -57.644000000000005 -16.464999999999996 +5360 5361 -69.0 -38.685 500000.0 5718178.35995209 +5361 4326 500000.0 5718178.35995209 -69.0 -38.684999999999995 +5360 5361 -71.7 -57.7515 339326.61691853346 3595752.8485343764 +5361 4326 339326.61691853346 3595752.8485343764 -71.7 -57.7515 +5360 5361 -66.3 -19.618500000000004 783190.0307251445 7828493.0824960265 +5361 4326 783190.0307251445 7828493.0824960265 -66.3 -19.618500000000004 +5360 5362 -74.995 -38.855000000000004 500433.8410580684 5699314.0104445405 +5362 4326 500433.8410580684 5699314.0104445405 -74.995 -38.855000000000004 +5360 5362 -77.6995 -57.3095 337399.9150358017 3644936.4607056994 +5362 4326 337399.9150358017 3644936.4607056994 -77.6995 -57.30950000000001 +5360 5362 -72.2905 -20.4005 782784.4819780085 7741866.394019629 +5362 4326 782784.4819780085 7741866.394019629 -72.2905 -20.400499999999997 +5365 5367 -83.965 6.989999999999999 503867.0286528629 772875.4301913698 +5367 4326 503867.0286528629 772875.4301913698 -83.96499723054268 6.9900004445613195 +5365 5367 -86.2465 2.6879999999999997 250154.89092720457 297425.91493363 +5367 4326 250154.89092720457 297425.91493363 -86.246493880657 2.6879983799833647 +5365 5367 -81.68350000000001 11.291999999999998 752950.9576257474 1249642.6219946425 +5367 4326 752950.9576257474 1249642.6219946425 -81.68350066618652 11.292002493541155 +5381 5382 -56.245000000000005 -33.36 570241.9473002348 6308547.766970579 +5382 4326 570241.9473002348 6308547.766970579 -56.245000000000005 -33.36000000000001 +5381 5382 -58.2655 -36.303000000000004 386378.60785348504 5981700.593305729 +5382 4326 386378.60785348504 5981700.593305729 -58.26550000000001 -36.303 +5381 5382 -54.2245 -30.417 766615.489189496 6631735.154877899 +5382 4326 766615.489189496 6631735.154877899 -54.2245 -30.417000000000005 +5381 5383 -52.004999999999995 -34.835 408106.72509430745 6144794.17204334 +5383 4326 408106.72509430745 6144794.17204334 -52.004999999999995 -34.834999999999994 +5381 5383 -53.8005 -37.4765 252360.20815695106 5848581.841621745 +5383 4326 252360.20815695106 5848581.841621745 -53.8005 -37.4765 +5381 5383 -50.2095 -32.1935 574510.7432059548 6437842.100983005 +5383 4326 574510.7432059548 6437842.100983005 -50.2095 -32.19349999999999 +5373 5387 -75.0 -10.540000000000001 500000.0 8834882.223718649 +5387 4326 500000.0 8834882.223718649 -75.0 -10.540000000000006 +5373 5387 -77.7 -19.999000000000002 217483.45000667387 7786351.5751357395 +5387 4326 217483.45000667387 7786351.5751357395 -77.7 -19.999000000000002 +5373 5387 -72.3 -1.081000000000003 800501.1957396579 9880383.224529458 +5387 4326 800501.1957396579 9880383.224529458 -72.3 -1.0810000000000044 +5373 5389 -70.33500000000001 -11.290000000000001 354291.6302970048 8751622.447944459 +5389 4326 354291.6302970048 8751622.447944459 -70.33500000000001 -11.289999999999994 +5373 5389 -71.8335 -19.525000000000002 202627.3944875921 7838621.841422372 +5389 4326 202627.3944875921 7838621.841422372 -71.8335 -19.525 +5373 5389 -68.8365 -3.0549999999999997 518167.7979651336 9662326.116137443 +5389 4326 518167.7979651336 9662326.116137443 -68.83650000000002 -3.055000000000004 +4674 5396 -27.64 -9.8 429815.02400929376 8916633.359449212 +5396 4326 429815.02400929376 8916633.359449212 -27.64 -9.799999999999995 +4674 5396 -29.764 -22.454 215535.93663470924 7514298.160177203 +5396 4326 215535.93663470924 7514298.160177203 -29.764000000000003 -22.454000000000008 +4674 5396 -25.516000000000002 2.8539999999999957 664947.0684257969 10315561.7480939 +5396 4326 664947.0684257969 10315561.7480939 -25.516000000000005 2.8540000000000005 +5451 5456 -84.25 10.375 509125.6725037654 261683.46445326827 +5456 4326 509125.6725037654 261683.46445326827 -84.24805523742312 10.37355424796027 +5451 5456 -85.798 9.6145 339217.14795401064 177943.66677027562 +5456 4326 339217.14795401064 177943.66677027562 -85.79605749762167 9.61311624891193 +5451 5456 -82.702 11.1355 678208.3047281669 346257.30625185306 +5456 4326 678208.3047281669 346257.30625185306 -82.70005404789539 11.133991564958896 +5451 5457 -84.13499999999999 8.96 448498.9841717824 323596.7702671423 +5457 4326 448498.9841717824 323596.7702671423 -84.13306357226557 8.958653331781255 +5451 5457 -85.5795 8.078000000000001 289143.73382179165 226572.25343445223 +5457 4326 289143.73382179165 226572.25343445223 -85.57756576645505 8.076723400833579 +5451 5457 -82.6905 9.841999999999999 607085.6325179951 421252.4022278692 +5457 4326 607085.6325179951 421252.4022278692 -82.68856214412088 9.840582592771861 +5451 5459 -90.24000000000001 14.774999999999999 510047.6077151018 312166.0620765796 +5459 4326 510047.6077151018 312166.0620765796 -90.23809964208002 14.773309460146933 +5451 5459 -92.08500000000001 13.798499999999999 310580.62124469946 204876.26048845693 +5459 4326 310580.62124469946 204876.26048845693 -92.08313763917046 13.796869464128227 +5451 5459 -88.395 15.7515 707724.8630335388 421104.9232524243 +5459 4326 707724.8630335388 421104.9232524243 -88.39306273182686 15.749748682976934 +5451 5460 -88.9 13.77 510813.84112898353 294336.40224640106 +5460 4326 510813.84112898353 294336.40224640106 -88.89808762281238 13.768346384496455 +5451 5460 -89.989 13.167 392777.0727276978 227846.75422040262 +5460 4326 392777.0727276978 227846.75422040262 -89.98710888628766 13.16538394059708 +5451 5460 -87.81099999999999 14.373 628246.2212727608 361362.2050984141 +5460 4326 628246.2212727608 361362.2050984141 -87.80906673148561 14.37130854616388 +5451 5461 -85.41 13.915 509725.8101802779 365240.3399364582 +5461 4326 509725.8101802779 365240.3399364582 -85.40803814326942 13.913311635414363 +5451 5461 -87.50699999999999 12.9115 282185.0542477813 255143.83048660617 +5461 4326 282185.0542477813 255143.83048660617 -87.50507430818523 12.909878324833189 +5451 5461 -83.313 14.9185 735318.2979011319 477334.7017207885 +5461 4326 735318.2979011319 477334.7017207885 -83.31100370333802 14.916743779317686 +5451 5462 -85.525 11.75 497275.1284622484 290719.86655425886 +5462 4326 497275.1284622484 290719.86655425886 -85.5230563156002 11.74842442833596 +5451 5462 -87.4195 10.805 290073.89485316805 186911.8828251881 +5462 4326 290073.89485316805 186911.8828251881 -87.41758781052216 10.803486962494276 +5451 5462 -83.6305 12.695 703070.8108195028 395923.91167875443 +5462 4326 703070.8108195028 395923.91167875443 -83.62852621012254 12.693360822924717 +4618 5463 -79.09 1.35 712516.9327667943 149299.83045344506 +5463 4326 712516.9327667943 149299.83045344506 -79.09050122204799 1.3496315691570768 +4618 5463 -80.07100000000001 0.135 603379.0886311304 14923.582735278154 +5463 4326 603379.0886311304 14923.582735278154 -80.07150282195413 0.13462941540570075 +4618 5463 -78.109 2.565 821513.7476394335 283875.68690748594 +5463 4326 821513.7476394335 283875.68690748594 -78.10949969755492 2.5646342576244687 +5467 5469 -80.11500000000001 8.415 487336.3038376421 294682.86303378554 +5467 5469 -82.7475 7.2765 196566.5862449092 169845.70863105133 +5467 5469 -77.4825 9.5535 776413.17191177 421477.33781585394 +5467 5472 -80.11500000000001 8.415 1106590.1453674084 1113048.4357952578 +5467 5472 -82.7475 7.2765 788956.7149714222 975643.3868713201 +5467 5472 -77.4825 9.5535 1422323.5836741603 1252779.7947443626 +4764 5479 163.0 -78.5 7000000.0 4944190.933194589 +5479 4326 7000000.0 4944190.933194589 163.0 -78.5 +4764 5479 154.0 -80.75 6838908.3616202185 4680507.455570858 +5479 4326 6838908.3616202185 4680507.455570858 154.0 -80.75 +4764 5479 172.0 -76.25 7237992.880442571 5177022.720829331 +5479 4326 7237992.880442571 5177022.720829331 172.0 -76.25 +4764 5480 165.0 -74.5 5000000.0 3000000.0 +5480 4326 5000000.0 3000000.0 165.0 -74.5 +4764 5480 157.8 -75.85 4803889.967083701 2837426.0819811802 +5480 4326 4803889.967083701 2837426.0819811802 157.8 -75.85000000000001 +4764 5480 172.2 -73.15 5232513.888703379 3136570.8646911876 +5480 4326 5232513.888703379 3136570.8646911876 172.2 -73.14999999999999 +4764 5481 166.0 -71.25 3000000.0 1027891.872148146 +5481 4326 3000000.0 1027891.872148146 166.0 -71.25 +4764 5481 160.6 -72.825 2822155.708528901 844195.0015422751 +5481 4326 2822155.708528901 844195.0015422751 160.6 -72.825 +4764 5481 171.4 -69.675 3209217.796178878 1194270.1588938585 +5481 4326 3209217.796178878 1194270.1588938585 171.4 -69.67500000000001 +4764 5482 180.0 -83.0 5000000.0 1778095.750335048 +5482 4326 5000000.0 1778095.750335048 180.0 -82.99999999999996 +4764 5482 153.0 -89.3 4964716.948579589 1069246.8873839774 +5482 4326 4964716.948579589 1069246.8873839774 153.00000000000006 -89.3 +4764 5482 -153.0 -76.7 5673274.765633131 2321376.128123102 +5482 4326 5673274.765633131 2321376.128123102 -153.0 -76.7 +5489 5490 -61.83 16.2 625062.2474287438 1791414.7488421197 +5490 4326 625062.2474287438 1791414.7488421197 -61.83 16.199999999999996 +5489 5490 -63.477 14.292 448553.5799329723 1580073.314818596 +5490 4326 448553.5799329723 1580073.314818596 -63.477000000000004 14.291999999999996 +5489 5490 -60.183 18.108 798130.5083052822 2004413.514689054 +5490 4326 798130.5083052822 2004413.514689054 -60.183 18.107999999999997 +4156 5513 17.325 49.394999999999996 1144058.330762452 544115.4743788177 +5513 4326 1144058.330762452 544115.4743788177 17.323583231079304 49.394407254052396 +4156 5513 12.6135 47.896499999999996 1264448.8861158604 909824.6302594767 +5513 4326 1264448.8861158604 909824.6302594767 12.612771741816465 47.896043442654836 +4156 5513 22.036499999999997 50.8935 1000407.520498633 196913.15631229055 +5513 4326 1000407.520498633 196913.15631229055 22.03436037576218 50.89279987326586 +4156 5514 17.325 49.394999999999996 -544115.4743788177 -1144058.330762452 +5514 4326 -544115.4743788177 -1144058.330762452 17.323583231079304 49.394407254052396 +4156 5514 12.6135 47.896499999999996 -909824.6302594767 -1264448.8861158604 +5514 4326 -909824.6302594767 -1264448.8861158604 12.612771741816465 47.896043442654836 +4156 5514 22.036499999999997 50.8935 -196913.15631229055 -1000407.520498633 +5514 4326 -196913.15631229055 -1000407.520498633 22.03436037576218 50.89279987326586 +5228 5515 15.475 49.82 6082359.166271513 5671775.905165402 +5515 4326 6082359.166271513 5671775.905165402 15.473779812350982 49.81928345057403 +5228 5515 12.4285 48.704 6173586.514484828 5908896.694015449 +5515 4326 6173586.514484828 5908896.694015449 12.427788000225306 48.70331717445003 +5228 5515 18.5215 50.936 5981050.132071087 5443530.68968815 +5515 4326 5981050.132071087 5443530.68968815 18.51975496308913 50.93526141675714 +5228 5516 15.475 49.82 -5671775.905165402 -6082359.166271513 +5516 4326 -5671775.905165402 -6082359.166271513 15.473779812350982 49.81928345057403 +5228 5516 12.4285 48.704 -5908896.694015449 -6173586.514484828 +5516 4326 -5908896.694015449 -6173586.514484828 12.427788000225306 48.70331717445003 +5228 5516 18.5215 50.936 -5443530.68968815 -5981050.132071087 +5516 4326 -5443530.68968815 -5981050.132071087 18.51975496308913 50.93526141675714 +4672 5518 -176.39499999999998 -43.97 358426.28198646865 653328.1026164149 +5518 4326 358426.28198646865 653328.1026164149 -176.39439027989843 -43.969522783416835 +4672 5518 -177.1645 -44.573 297216.12477063877 586112.6439653018 +5518 4326 297216.12477063877 586112.6439653018 -177.1639131947667 -44.57254554068089 +4672 5518 -175.6255 -43.367 420885.1365769333 719960.8588879727 +5518 4326 420885.1365769333 719960.8588879727 -175.6248678656968 -43.36650024259234 +4673 5519 -176.39499999999998 -43.97 358426.28198646865 653328.1026164149 +5519 4326 358426.28198646865 653328.1026164149 -176.39439271436677 -43.969524515830706 +4673 5519 -177.1645 -44.573 297216.12477063877 586112.6439653018 +5519 4326 297216.12477063877 586112.6439653018 -177.16391719432553 -44.57254616955884 +4673 5519 -175.6255 -43.367 420885.1365769333 719960.8588879727 +5519 4326 420885.1365769333 719960.8588879727 -175.62486875575996 -43.3665030558065 +4314 5520 3.925 55.58 1558329.554596546 6161550.583843893 +4314 5520 3.3985 55.274 1525324.4017495331 6127171.106288493 +4314 5520 4.451499999999999 55.886 1590814.7496855438 6196179.771366924 +4326 5523 10.775 -2.0250000000000004 1419373.5349731045 5276157.695316525 +4326 5523 7.4045000000000005 -5.9355 1046319.5655977491 4842246.505587592 +4326 5523 14.145499999999998 1.8854999999999995 1794324.6721555511 5708628.845058716 +5527 5530 -49.645 -14.335 5469772.458410315 8410163.71911692 +5530 4326 5469772.458410315 8410163.71911692 -49.64545248094854 -14.335436315677986 +5527 5530 -71.57350000000001 -33.5725 3376194.138547222 6145704.511332272 +5530 4326 3376194.138547222 6145704.511332272 -71.57417500243233 -33.572906657968424 +5527 5530 -27.716500000000003 4.902499999999996 7914496.845611783 10599242.1587075 +5530 4326 7914496.845611783 10599242.1587075 -27.71675145072816 4.902202196182892 +5527 5531 -56.995000000000005 -13.7 500540.64893143915 8485446.569823608 +5531 4326 500540.64893143915 8485446.569823608 -56.99550260324278 -13.700418657977526 +5527 5531 -59.6995 -30.089 239822.9470693028 6668266.880365953 +5531 4326 239822.9470693028 6668266.880365953 -59.70008290387743 -30.089462495916973 +5527 5531 -54.2905 2.6889999999999965 801283.4105300197 10297552.908145657 +5531 4326 801283.4105300197 10297552.908145657 -54.29097143771253 2.68867224929946 +5527 5533 -45.0 -14.184999999999999 500000.0 8431808.048099866 +5533 4326 500000.0 8431808.048099866 -45.00041576206403 -14.185443917250732 +5527 5533 -47.7 -31.5685 243739.34545035547 6504216.646767427 +5533 4326 243739.34545035547 6504216.646767427 -47.700497225591874 -31.569016470321298 +5527 5533 -42.3 3.198500000000003 800089.7253904666 10353929.989616534 +5533 4326 800089.7253904666 10353929.989616534 -42.30038199618091 3.198180752718546 +5527 5534 -39.0 -12.805000000000001 499999.9999999993 8584423.552965969 +5534 4326 499999.9999999993 8584423.552965969 -39.00036262347952 -12.805444321614715 +5527 5534 -41.7 -24.9955 227467.57336339535 7232826.286409084 +5534 4326 227467.57336339535 7232826.286409084 -41.700415106249814 -24.996010273026414 +5527 5534 -36.3 -0.6145000000000032 800538.2972096154 9932003.038325077 +5534 4326 800538.2972096154 9932003.038325077 -36.300330107318565 -0.6148510039524908 +5527 5535 -32.995 -9.805 500548.30307648884 8916143.531790104 +5535 4326 500548.30307648884 8916143.531790104 -32.99530466020983 -9.805428927964055 +5527 5535 -35.6995 -22.4005 222070.04547576688 7520337.336957349 +5535 4326 222070.04547576688 7520337.336957349 -35.69985107747348 -22.401011337473207 +5527 5535 -30.290499999999998 2.7905000000000015 801258.0028855967 10308784.587203722 +5535 4326 801258.0028855967 10308784.587203722 -30.290775390059878 2.7901807102678924 +5524 5536 -56.08 -22.744999999999997 594465.960214065 7484382.180922411 +5524 5536 -57.952 -27.0245 405561.0568954095 7010453.136224851 +5524 5536 -54.208 -18.4655 794889.1660762655 7956009.9824267365 +5524 5537 -50.995000000000005 -21.245 500518.81273809535 7650707.152394813 +5524 5537 -53.6995 -26.8745 231801.49875332683 7024566.324214213 +5524 5537 -48.2905 -15.6155 790546.514434797 8271724.311176973 +5524 5538 -45.0 -20.145 500000.0 7772444.840734845 +5524 5538 -47.7 -24.7755 226973.36563145305 7257176.473599203 +5524 5538 -42.3 -15.5145 789668.6616699893 8282919.73826155 +5524 5539 -40.41 -18.975 351557.544384996 7901318.7558150105 +5524 5539 -41.841 -22.561500000000002 207820.86320023314 7502207.455030259 +5524 5539 -38.979 -15.3885 502253.61998764164 8298683.1283895625 +5546 5550 141.6 -4.42 566568.9465740811 9511420.61314327 +5550 4326 566568.9465740811 9511420.61314327 141.6 -4.419999999999996 +5546 5550 139.44 -10.477 329266.4067851084 8841425.213969905 +5550 4326 329266.4067851084 8841425.213969905 139.44 -10.477000000000004 +5546 5550 143.76 1.6370000000000005 807113.7964347235 10181149.615640536 +5550 4326 807113.7964347235 10181149.615640536 143.76 1.6369999999999953 +5546 5551 147.005 -5.65 500553.6898348712 9375484.949205004 +5551 4326 500553.6898348712 9375484.949205004 147.005 -5.649999999999997 +5546 5551 144.3005 -13.057 207231.56614258362 8555001.84724071 +5551 4326 207231.56614258362 8555001.84724071 144.3005 -13.057000000000002 +5546 5551 149.7095 1.7569999999999997 801471.6414937989 10194420.582966864 +5551 4326 801471.6414937989 10194420.582966864 149.7095 1.7570000000000003 +5546 5552 153.0 -6.385 500000.0 9294236.390745863 +5552 4326 500000.0 9294236.390745863 153.0 -6.385000000000007 +5546 5552 150.3 -13.913499999999999 208220.0090891212 8460186.116927173 +5552 4326 208220.0090891212 8460186.116927173 150.3 -13.913500000000003 +5546 5552 155.7 1.1434999999999995 800494.8647868033 10126532.652927637 +5552 4326 800494.8647868033 10126532.652927637 155.7 1.1435000000000075 +5451 5559 -90.1 16.84 524866.0917584652 294806.1254311741 +5559 4326 524866.0917584652 294806.1254311741 -90.09807805757877 16.83821580981108 +5451 5559 -91.684 15.949 355389.5403061538 196693.4904476797 +5559 4326 355389.5403061538 196693.4904476797 -91.68211238488908 15.947268868936042 +5451 5559 -88.516 17.730999999999998 692763.8674712925 394276.3122392414 +5559 4326 692763.8674712925 394276.3122392414 -88.51404441868227 17.72916233563242 +5561 5562 23.075 49.805 4649366.61686024 5521321.203734357 +5562 4326 4649366.61686024 5521321.203734357 23.073322931879378 49.804757039288624 +5561 5562 22.2425 48.1355 4592479.6965875225 5334334.772797889 +5562 4326 4592479.6965875225 5334334.772797889 22.240873234589344 48.13522601136111 +5561 5562 23.9075 51.4745 4701994.938222456 5708986.510612748 +5562 4326 4701994.938222456 5708986.510612748 23.905767923478958 51.474288749744545 +5561 5563 27.0 48.53 5500000.0 5377455.832182147 +5563 4326 5500000.0 5377455.832182147 26.998392769140974 48.52979759881091 +5561 5563 24.3 45.443 5288757.986939003 5037814.310936699 +5563 4326 5288757.986939003 5037814.310936699 24.298464216965087 45.44272354364688 +5561 5563 29.7 51.617000000000004 5686996.244358798 5724284.936447799 +5563 4326 5686996.244358798 5724284.936447799 29.698310853451005 51.61687503370183 +5561 5564 33.0 47.78 6500000.0 5294059.404939604 +5564 4326 6500000.0 5294059.404939604 32.998471489016836 47.77986976347204 +5561 5564 30.3 43.64 6282130.586303919 4837452.599120673 +5564 4326 6282130.586303919 4837452.599120673 30.298555220935175 43.63977975706663 +5561 5564 35.7 51.92 6685747.69249026 5757989.436660982 +5564 4326 6685747.69249026 5757989.436660982 35.69836822563834 51.9199639699719 +5561 5565 38.09 46.935 7430704.186921975 5200514.597829374 +5565 4326 7430704.186921975 5200514.597829374 38.08855495833393 46.93492387377722 +5561 5565 36.209 43.780499999999996 7275313.217306974 4853307.225229278 +5565 4326 7275313.217306974 4853307.225229278 36.2076111037884 43.78035497434027 +5561 5565 39.971 50.0895 7569487.825386327 5551351.380976856 +5565 4326 7569487.825386327 5551351.380976856 39.96948876170366 50.08949475302959 +5561 5566 23.075 49.805 649366.6168602395 5521321.203734357 +5566 4326 649366.6168602395 5521321.203734357 23.073322931879375 49.804757039288624 +5561 5566 22.2425 48.1355 592479.6965875223 5334334.772797889 +5566 4326 592479.6965875223 5334334.772797889 22.240873234589344 48.13522601136111 +5561 5566 23.9075 51.4745 701994.9382224561 5708986.510612748 +5566 4326 701994.9382224561 5708986.510612748 23.90576792347896 51.474288749744545 +5561 5567 27.0 48.53 500000.0 5377455.832182147 +5567 4326 500000.0 5377455.832182147 26.998392769140974 48.52979759881091 +5561 5567 24.3 45.443 288757.98693900276 5037814.310936699 +5567 4326 288757.98693900276 5037814.310936699 24.298464216965087 45.44272354364688 +5561 5567 29.7 51.617000000000004 686996.2443587973 5724284.936447799 +5567 4326 686996.2443587973 5724284.936447799 29.698310853451005 51.61687503370183 +5561 5568 33.0 47.78 500000.0 5294059.404939604 +5568 4326 500000.0 5294059.404939604 32.998471489016836 47.77986976347204 +5561 5568 30.3 43.64 282130.58630391845 4837452.599120673 +5568 4326 282130.58630391845 4837452.599120673 30.298555220935175 43.63977975706663 +5561 5568 35.7 51.92 685747.6924902601 5757989.436660982 +5568 4326 685747.6924902601 5757989.436660982 35.69836822563834 51.9199639699719 +5561 5569 38.09 46.935 430704.1869219744 5200514.597829374 +5569 4326 430704.1869219744 5200514.597829374 38.08855495833392 46.93492387377723 +5561 5569 36.209 43.780499999999996 275313.21730697434 4853307.225229278 +5569 4326 275313.21730697434 4853307.225229278 36.2076111037884 43.78035497434027 +5561 5569 39.971 50.0895 569487.8253863266 5551351.380976856 +5569 4326 569487.8253863266 5551351.380976856 39.96948876170366 50.08949475302959 +4267 5588 -66.375 46.315 1031585.5836779373 932562.4654378915 +5588 4326 1031585.5836779373 932562.4654378915 -66.37442920748694 46.315053203914815 +4267 5588 -68.7825 44.7355 406780.1825627857 365040.87631585554 +5588 4326 406780.1825627857 365040.87631585554 -68.78202786790189 44.73556890478313 +4267 5588 -63.9675 47.8945 1621299.25796665 1518693.8509368661 +5588 4326 1621299.25796665 1518693.8509368661 -63.9668252882837 47.89453993055091 +5464 5589 -88.47 17.185 273752.6837777275 490431.9606658591 +5464 5589 -89.145 16.0105 37063.456115213754 64220.40497193131 +5464 5589 -87.795 18.359499999999997 507455.96624554886 917536.7412375201 +5593 5596 11.335 54.58 1000107.7680813042 6050476.058735509 +5596 4326 1000107.7680813042 6050476.058735509 11.335 54.580000000000005 +5593 5596 10.727500000000001 54.355 960611.0252773727 6025599.708780167 +5596 4326 960611.0252773727 6025599.708780167 10.727500000000001 54.35499999999999 +5593 5596 11.942499999999999 54.805 1039171.6745777453 6075692.747109062 +5596 4326 1039171.6745777453 6075692.747109062 11.942499999999999 54.805 +4267 5623 -83.5 43.864999999999995 543955.2685392414 861923.8979557757 +5623 4326 543955.2685392414 861923.8979557757 -83.49987356295755 43.864999869372134 +4267 5623 -84.733 41.9075 209737.1336252175 150284.17423248393 +5623 4326 209737.1336252175 150284.17423248393 -84.73291899120447 41.90751304748778 +4267 5623 -82.267 45.8225 856842.2261913121 1578653.494404907 +5623 4326 856842.2261913121 1578653.494404907 -82.26682496678151 45.8224936228978 +4267 5624 -86.10499999999999 43.93 406480.2053704506 885743.8230983029 +5624 4326 406480.2053704506 885743.8230983029 -86.10496404736911 43.930001449275295 +4267 5624 -87.4595 41.968 35113.799820321954 175157.81993982982 +5624 4326 35113.799820321954 175157.81993982982 -87.45951085810412 41.96801339756291 +4267 5624 -84.75049999999999 45.891999999999996 754495.0027604959 1602409.392243644 +5624 4326 754495.0027604959 1602409.392243644 -84.7504140300328 45.89199643478732 +4267 5625 -86.93 46.705 956600.7865534367 1902557.3606137547 +5625 4326 956600.7865534367 1902557.3606137547 -86.92999240992518 46.7049977605489 +4267 5625 -90.071 45.2515 159798.6334546179 1370070.9376131303 +5625 4326 159798.6334546179 1370070.9376131303 -90.07110443557893 45.251496559891336 +4267 5625 -83.789 48.158500000000004 1710654.9750486691 2466501.4252432385 +5625 4326 1710654.9750486691 2466501.4252432385 -83.7888742218918 48.158499928873596 +4230 5627 6.710000000000001 42.445 558391.865663346 4699514.026939237 +5627 4326 558391.865663346 4699514.026939237 6.7089483726308305 42.44397946176941 +4230 5627 3.407 41.2795 282828.930224651 4573108.213787132 +5627 4326 282828.930224651 4573108.213787132 3.4059038497654983 41.27842524842483 +4230 5627 10.013000000000002 43.6105 823864.092188136 4836530.834921729 +5627 4326 823864.092188136 4836530.834921729 10.011997692395964 43.60953452623173 +4130 5629 38.445 -14.7 -207037.99381044751 8364558.966302271 +5629 4326 -207037.99381044751 8364558.966302271 38.445 -14.699999999999996 +4130 5629 36.2355 -18.552 -427810.0305699558 7926037.764202792 +5629 4326 -427810.0305699558 7926037.764202792 36.2355 -18.551999999999996 +4130 5629 40.6545 -10.848 24612.84283580014 8797429.50589699 +5629 4326 24612.84283580014 8797429.50589699 40.6545 -10.847999999999995 +4179 5631 10.96 52.215 2633958.823488199 5789179.22084104 +4179 5631 10.024 50.401500000000006 2572803.71381943 5586106.566101754 +4179 5631 11.896 54.028499999999994 2689755.8405398126 5993075.783405378 +5013 5632 -24.03 36.155 1058927.6532207387 1773091.5591083039 +5632 4326 1058927.6532207387 1773091.5591083039 -24.03 36.154999999999994 +5013 5632 -34.425 29.9315 -137295.80779344728 1669519.0069186192 +5632 4326 -137295.80779344728 1669519.0069186192 -34.42499999999999 29.931500000000003 +5013 5632 -13.635000000000002 42.3785 2133100.910408017 2065066.0068744565 +5632 4326 2133100.910408017 2065066.0068744565 -13.634999999999996 42.3785 +5013 5633 -24.03 36.155 1343120.7866520286 2131459.0135310637 +5633 4326 1343120.7866520286 2131459.0135310637 -24.03000000000001 36.155 +5013 5633 -34.425 29.9315 210830.5675597242 1990003.6220562067 +5633 4326 210830.5675597242 1990003.6220562067 -34.42499999999998 29.9315 +5013 5633 -13.635000000000002 42.3785 2404279.095692049 2446244.09195547 +5633 4326 2404279.095692049 2446244.09195547 -13.634999999999996 42.37850000000001 +4081 5634 -16.84 28.68 1350543.9014840615 730796.8745567342 +5634 4326 1350543.9014840615 730796.8745567342 -16.83999999999999 28.679999999999996 +4081 5634 -21.421 25.008000000000003 748498.9280295447 521291.54228935996 +5634 4326 748498.9280295447 521291.54228935996 -21.421 25.007999999999992 +4081 5634 -12.259 32.352 1910944.8676329863 977382.3182970355 +5634 4326 1910944.8676329863 977382.3182970355 -12.258999999999999 32.35200000000001 +4081 5635 -16.84 28.68 1700347.3081001863 1089102.8655930809 +5635 4326 1700347.3081001863 1089102.8655930809 -16.839999999999996 28.680000000000003 +4081 5635 -21.421 25.008000000000003 1152392.2613910139 874323.418997461 +5635 4326 1152392.2613910139 874323.418997461 -21.421 25.008000000000003 +4081 5635 -12.259 32.352 2227376.5129910354 1339433.7358514501 +5635 4326 2227376.5129910354 1339433.7358514501 -12.258999999999999 32.352000000000004 +5252 5636 35.225 38.935 6476668.262627877 2122891.563650336 +5636 4326 6476668.262627877 2122891.563650336 35.224999999999994 38.93500000000001 +5252 5636 26.5805 34.871500000000005 5840462.885727768 1477468.227199273 +5636 4326 5840462.885727768 1477468.227199273 26.580500000000004 34.871500000000005 +5252 5636 43.8695 42.9985 6984308.131386645 2827663.468397159 +5636 4326 6984308.131386645 2827663.468397159 43.869499999999995 42.99850000000001 +5252 5637 35.225 38.935 6113214.712321862 1753357.3904717825 +5637 4326 6113214.712321862 1753357.3904717825 35.225 38.93500000000001 +5252 5637 26.5805 34.871500000000005 5504107.161648534 1111002.6586643956 +5637 4326 5504107.161648534 1111002.6586643956 26.580500000000004 34.87150000000002 +5252 5637 43.8695 42.9985 6597469.094722068 2437003.0909386734 +5637 4326 6597469.094722068 2437003.0909386734 43.86950000000001 42.99849999999998 +5324 5638 -18.21 64.775 3013773.000721653 4899380.463074724 +5638 4326 3013773.000721653 4899380.463074724 -18.209999999999987 64.775 +5324 5638 -29.604 60.4415 2267814.24831495 4747070.920957485 +5638 4326 2267814.24831495 4747070.920957485 -29.603999999999992 60.44150000000001 +5324 5638 -6.8160000000000025 69.1085 3651810.1081470903 5191145.124202191 +5638 4326 3651810.1081470903 5191145.124202191 -6.8159999999999945 69.1085 +5324 5639 -18.21 64.775 2691929.977472239 4443141.16925533 +5639 4326 2691929.977472239 4443141.16925533 -18.21 64.775 +5324 5639 -29.604 60.4415 1963119.6915789302 4272041.328963477 +5639 4326 1963119.6915789302 4272041.328963477 -29.603999999999985 60.4415 +5324 5639 -6.8160000000000025 69.1085 3318213.5570691726 4757378.547099611 +5639 4326 3318213.5570691726 4757378.547099611 -6.815999999999998 69.1085 +4674 5641 -42.035 0.6500000000000004 5107358.306916902 10071831.350295652 +5641 4326 5107358.306916902 10071831.350295652 -42.035 0.6499999999999965 +4674 5641 -50.6795 -5.101 4145639.2559913513 9435545.796577863 +5641 4326 4145639.2559913513 9435545.796577863 -50.6795 -5.100999999999991 +4674 5641 -33.3905 6.401 6069077.357842453 10708853.418614555 +5641 4326 6069077.357842453 10708853.418614555 -33.3905 6.4009999999999945 +4230 5643 10.165000000000001 53.25 826013.0175870309 584899.138615918 +5643 4326 826013.0175870309 584899.138615918 10.16378490757191 53.249302608634146 +4230 5643 -0.4864999999999997 50.775 77209.05818031414 363779.2612397643 +5643 4326 77209.05818031414 363779.2612397643 -0.48789973426069116 50.774106960813896 +4230 5643 20.816499999999998 55.725 1492529.2200763826 911943.0287466248 +5643 4326 1492529.2200763826 911943.0287466248 20.815534231461296 55.72448526514629 +4627 5644 52.915 -21.445 698455.0606823685 7627388.852012594 +5644 4326 698455.0606823685 7627388.852012594 52.915 -21.444999999999997 +4627 5644 51.9385 -24.0775 595401.4571459391 7336874.308217956 +5644 4326 595401.4571459391 7336874.308217956 51.9385 -24.077500000000004 +4627 5644 53.8915 -18.8125 804772.4739720179 7917437.384055579 +5644 4326 804772.4739720179 7917437.384055579 53.8915 -18.812500000000004 +4269 5646 -72.47 43.875 1648327.2337749999 501155.5716168491 +5646 4326 1648327.2337749999 501155.5716168491 -72.47 43.87500000000001 +4269 5646 -73.343 42.835499999999996 1414302.104181283 123401.46075077487 +5646 4326 1414302.104181283 123401.46075077487 -73.343 42.835499999999996 +4269 5646 -71.597 44.914500000000004 1874346.544215013 881408.5962198091 +5646 4326 1874346.544215013 881408.5962198091 -71.597 44.91450000000001 +4258 5649 4.675 53.445 31611246.103145666 5923081.350788312 +5649 4326 31611246.103145666 5923081.350788312 4.67500000000002 53.445 +4258 5649 3.4735 51.2175 31533069.64460152 5674118.682907582 +5649 4326 31533069.64460152 5674118.682907582 3.473499999999982 51.2175 +4258 5649 5.8765 55.6725 31680887.497756355 6173381.806914216 +5649 4326 31680887.497756355 6173381.806914216 5.8764999999999965 55.67249999999999 +4258 5650 13.305 51.245000000000005 33381693.719118673 5678435.226906829 +5650 4326 33381693.719118673 5678435.226906829 13.305000000000021 51.24500000000001 +4258 5650 11.743500000000001 47.8385 33256335.41081962 5303485.919889525 +5650 4326 33256335.41081962 5303485.919889525 11.743500000000012 47.838499999999996 +4258 5650 14.866499999999998 54.6515 33491386.34549611 6056019.993634921 +5650 4326 33491386.34549611 6056019.993634921 14.86649999999998 54.65149999999999 +4258 5651 4.675 53.445 31611246.103145666 5923081.350788312 +5651 4326 31611246.103145666 5923081.350788312 4.67500000000002 53.445 +4258 5651 3.4735 51.2175 31533069.64460152 5674118.682907582 +5651 4326 31533069.64460152 5674118.682907582 3.473499999999982 51.2175 +4258 5651 5.8765 55.6725 31680887.497756355 6173381.806914216 +5651 4326 31680887.497756355 6173381.806914216 5.8764999999999965 55.67249999999999 +4258 5652 9.004999999999999 51.370000000000005 32500348.05019652 5690971.606736347 +5652 4326 32500348.05019652 5690971.606736347 9.004999999999987 51.36999999999999 +4258 5652 6.3004999999999995 47.68 32297394.22618539 5284264.866075603 +5652 4326 32297394.22618539 5284264.866075603 6.300499999999981 47.68 +4258 5652 11.709499999999998 55.06 32673041.836988475 6104823.288213569 +5652 4326 32673041.836988475 6104823.288213569 11.709499999999982 55.05999999999999 +4258 5653 13.305 51.245000000000005 33381693.719118673 5678435.226906829 +5653 4326 33381693.719118673 5678435.226906829 13.305000000000021 51.24500000000001 +4258 5653 11.743500000000001 47.8385 33256335.41081962 5303485.919889525 +5653 4326 33256335.41081962 5303485.919889525 11.743500000000012 47.838499999999996 +4258 5653 14.866499999999998 54.6515 33491386.34549611 6056019.993634921 +5653 4326 33491386.34549611 6056019.993634921 14.86649999999998 54.65149999999999 +4152 5654 -72.47 43.875 1648327.2337749999 501155.5716168491 +5654 4326 1648327.2337749999 501155.5716168491 -72.47000099064276 43.87500889227031 +4152 5654 -73.343 42.835499999999996 1414302.104181283 123401.46075077487 +5654 4326 1414302.104181283 123401.46075077487 -73.3430012130703 42.835508670437314 +4152 5654 -71.597 44.914500000000004 1874346.544215013 881408.5962198091 +5654 4326 1874346.544215013 881408.5962198091 -71.59700076518057 44.91450910861833 +4759 5655 -72.47 43.875 1648327.2337749999 501155.5716168491 +5655 4326 1648327.2337749999 501155.5716168491 -72.47 43.87500000000001 +4759 5655 -73.343 42.835499999999996 1414302.104181283 123401.46075077487 +5655 4326 1414302.104181283 123401.46075077487 -73.343 42.835499999999996 +4759 5655 -71.597 44.914500000000004 1874346.544215013 881408.5962198091 +5655 4326 1874346.544215013 881408.5962198091 -71.597 44.91450000000001 +4265 5659 10.975 44.435 657243.3366926182 922356.9846171467 +5659 4326 657243.3366926182 922356.9846171467 10.974734199913442 44.43566632367846 +4265 5659 9.3685 43.8005 529697.3522640592 850049.6828592978 +5659 4326 529697.3522640592 850049.6828592978 9.368179060618727 43.80114971868453 +4265 5659 12.5815 45.0695 782001.3770317137 997186.7569674561 +5659 4326 782001.3770317137 997186.7569674561 12.581291399665636 45.070182143409156 +4179 5663 15.0 50.335 3500000.0 5578207.920000927 +5663 4326 3500000.0 5578207.920000927 14.998244078785875 50.33461396693544 +4179 5663 12.3 46.2355 3291737.9470710987 5125897.987267141 +5663 4326 3291737.9470710987 5125897.987267141 12.298389569764929 46.2350336960067 +4179 5663 17.7 54.4345 3675187.672529111 6037744.399011723 +5663 4326 3675187.672529111 6037744.399011723 17.698063953723977 54.43420140622084 +4178 5664 10.96 52.215 2633958.823488199 5789179.22084104 +5664 4326 2633958.823488199 5789179.22084104 10.958140310716777 52.214573608568806 +4178 5664 10.024 50.401500000000006 2572803.71381943 5586106.566101754 +5664 4326 2572803.71381943 5586106.566101754 10.022209482351567 50.40103890512051 +4178 5664 11.896 54.028499999999994 2689755.8405398126 5993075.783405378 +5664 4326 2689755.8405398126 5993075.783405378 11.894063911152717 54.028109409575514 +4178 5665 15.005 50.260000000000005 3500356.5477141365 5569865.18032614 +5665 4326 3500356.5477141365 5569865.18032614 15.003266627980128 50.259618570339754 +4178 5665 12.3005 46.228 3291748.1279653674 5125063.029669655 +5665 4326 3291748.1279653674 5125063.029669655 12.298895747855356 46.22754129637887 +4178 5665 17.709500000000002 54.292 3676413.80376588 6021911.577356551 +5665 4326 3676413.80376588 6021911.577356551 17.707608402605082 54.291701876997436 +4746 5666 10.21 50.955 3585009.6504283533 5647197.1803419385 +4746 5666 9.949 50.4105 3567449.3761094348 5586365.988756228 +4746 5666 10.471 51.499500000000005 3602133.126765381 5708096.268628192 +4746 5667 11.530000000000001 50.92 4466954.496988681 5642711.992223711 +4746 5667 10.603 50.272000000000006 4400421.194840638 5571464.861977598 +4746 5667 12.457 51.568 4531683.121037275 5714789.434192788 +4745 5668 12.7 50.93 4549206.018907146 5643952.490357189 +4745 5668 11.971 50.273 4497932.872648965 5570642.746779436 +4745 5668 13.429 51.586999999999996 4599027.021717923 5717771.840552784 +4745 5669 14.27 51.099999999999994 5448872.460334022 5662882.7029204015 +4745 5669 13.577 50.668 5399411.818354078 5615542.658050387 +4745 5669 14.963 51.532 5497432.814261514 5710686.244340632 +4179 5670 10.21 50.955 3585021.4425204406 5647876.100302413 +4179 5670 9.949 50.4105 3567458.726292648 5587036.941448476 +4179 5670 10.471 51.499500000000005 3602147.3032476585 5708783.181510262 +4179 5671 12.0 51.855000000000004 4500000.0 5747310.9234197 +4179 5671 10.65 49.2585 4401729.994885185 5459349.248434758 +4179 5671 13.35 54.4515 4587565.41501431 6037117.381924261 +4179 5672 15.0 50.629999999999995 5500000.0 5611023.792190646 +5672 4326 5500000.0 5611023.792190646 14.998232165746973 50.629616831128416 +4179 5672 13.65 46.949 5397225.661667873 5202553.803352359 +5672 4326 5397225.661667873 5202553.803352359 13.648366366622433 46.94856074330477 +4179 5672 16.35 54.311 5587865.001007058 6021479.161634348 +5672 4326 5587865.001007058 6021479.161634348 16.348066760984306 54.31067665213039 +4178 5673 10.21 50.955 3585021.4425204406 5647876.100302413 +5673 4326 3585021.4425204406 5647876.100302413 10.208188971464955 50.954547866650444 +4178 5673 9.949 50.4105 3567458.726292648 5587036.941448476 +5673 4326 3567458.726292648 5587036.941448476 9.947209084564912 50.41003787702326 +4178 5673 10.471 51.499500000000005 3602147.3032476585 5708783.181510262 +5673 4326 3602147.3032476585 5708783.181510262 10.469168261549225 51.49905795517159 +4178 5674 12.0 51.855000000000004 4500000.0 5747310.9234197 +5674 4326 4500000.0 5747310.9234197 11.998156690048944 51.854585403636335 +4178 5674 10.65 49.2585 4401729.994885185 5459349.248434758 +5674 4326 4401729.994885185 5459349.248434758 10.648250857483418 49.258035601092274 +4178 5674 13.35 54.4515 4587565.41501431 6037117.381924261 +5674 4326 4587565.41501431 6037117.381924261 13.348048229140222 54.45113744381812 +4178 5675 15.0 50.629999999999995 5500000.0 5611023.792190646 +5675 4326 5500000.0 5611023.792190646 14.998253051864838 50.62962218214674 +4178 5675 13.65 46.949 5397225.661667873 5202553.803352359 +5675 4326 5397225.661667873 5202553.803352359 13.648374899750717 46.94856692336571 +4178 5675 16.35 54.311 5587865.001007058 6021479.161634348 +5675 4326 5587865.001007058 6021479.161634348 16.348103677053906 54.310680734260664 +4314 5676 6.6850000000000005 51.46 2547602.3140389803 5702898.528463662 +5676 4326 2547602.3140389803 5702898.528463662 6.684298277567375 51.45868126906761 +4314 5676 5.9425 49.345 2495822.2669741916 5467438.493669685 +5676 4326 2495822.2669741916 5467438.493669685 5.941908751059255 49.343923254507395 +4314 5676 7.4275 53.575 2594545.831218262 5938947.830974243 +5676 4326 2594545.831218262 5938947.830974243 7.426677164135287 53.573439548231455 +4314 5677 9.004999999999999 51.18000000000001 3500349.5859045233 5671528.29132669 +5677 4326 3500349.5859045233 5671528.29132669 9.003949551872092 51.178718107244805 +4314 5677 7.6505 47.661 3398648.2705067503 5281079.407655729 +5677 4326 3398648.2705067503 5281079.407655729 7.649668106573623 47.660116655680355 +4314 5677 10.3595 54.699000000000005 3587636.6123899953 6063943.872055777 +5677 4326 3587636.6123899953 6063943.872055777 10.358193911326715 54.69732092630811 +4314 5678 12.004999999999999 50.99 4500351.020978886 5650393.221354948 +5678 4326 4500351.020978886 5650393.221354948 12.003502614424583 50.98875623891162 +4314 5678 10.6505 47.75 4398820.678964211 5290973.469232433 +5678 4326 4398820.678964211 5290973.469232433 10.649247120828502 47.74911730283645 +4314 5678 13.3595 54.230000000000004 4588644.616662983 6011748.581694027 +5678 4326 4588644.616662983 6011748.581694027 13.357719375165813 54.228397053904274 +4314 5679 13.67 48.745 5402198.71253722 5401571.519510001 +5679 4326 5402198.71253722 5401571.519510001 13.668304879586517 48.74402615384391 +4314 5679 13.517 48.5335 5390491.252891952 5378263.45025934 +5679 4326 5390491.252891952 5378263.45025934 13.515330959782458 48.53254881806341 +4314 5679 13.823 48.9565 5413813.141910217 5424903.380188386 +5679 4326 5413813.141910217 5424903.380188386 13.8212785738042 48.95550351729651 +4314 5680 3.925 55.58 1558329.554596546 6161550.583843893 +4314 5680 3.3985 55.274 1525324.4017495331 6127171.106288493 +4314 5680 4.451499999999999 55.886 1590814.7496855438 6196179.771366924 +5681 5682 6.6850000000000005 51.46 2547602.3140389803 5702898.528463662 +5681 5682 5.9425 49.345 2495822.2669741916 5467438.493669685 +5681 5682 7.4275 53.575 2594545.831218262 5938947.830974243 +5681 5683 9.004999999999999 51.18000000000001 3500349.5859045233 5671528.29132669 +5681 5683 7.6505 47.661 3398648.2705067503 5281079.407655729 +5681 5683 10.3595 54.699000000000005 3587636.6123899953 6063943.872055777 +5681 5684 12.004999999999999 51.065 4500350.454966779 5658735.929858743 +5681 5684 10.6505 47.7575 4398835.219009595 5291807.245100884 +5681 5684 13.3595 54.3725 4588338.978269264 6027607.086036904 +5681 5685 14.27 51.614999999999995 5449442.5920807 5720171.52127164 +5681 5685 13.577 48.820499999999996 5395517.015104717 5410089.787059241 +5681 5685 14.963 54.4095 5497597.866019965 6030873.855350526 +4167 5700 -177.0 -39.425 500000.0 5636058.985583618 +5700 4326 500000.0 5636058.985583618 -177.0 -39.42499999999999 +4167 5700 -179.7 -51.6155 313075.49862275366 4278272.065348794 +5700 4326 313075.49862275366 4278272.065348794 -179.7 -51.61549999999999 +4167 5700 -174.3 -27.2345 767381.0235405806 6984706.800266228 +5700 4326 767381.0235405806 6984706.800266228 -174.3 -27.2345 +4202 5825 149.07999999999998 -35.525 206413.6240492002 577000.0818809586 +5825 4326 206413.6240492002 577000.0818809586 149.08121376772215 -35.523443815313925 +4202 5825 148.792 -35.8895 180378.8279355026 536534.3676040933 +5825 4326 180378.8279355026 536534.3676040933 148.7932236328181 -35.88794777448243 +4202 5825 149.368 -35.1605 232684.471714804 617387.215529306 +5825 4326 232684.471714804 617387.215529306 149.36920401865717 -35.15893997642714 +4163 5836 41.54 15.545 772441.9960059872 1720226.0215223967 +5836 4326 772441.9960059872 1720226.0215223967 41.54 15.545000000000003 +4163 5836 41.126 14.8115 728805.8269582123 1638562.7824559414 +5836 4326 728805.8269582123 1638562.7824559414 41.126 14.811499999999993 +4163 5836 41.954 16.2785 815730.3073473925 1802024.7832955704 +5836 4326 815730.3073473925 1802024.7832955704 41.954 16.2785 +4163 5837 55.980000000000004 11.95 388937.90631837427 1321229.4537378957 +5837 4326 388937.90631837427 1321229.4537378957 55.980000000000004 11.949999999999998 +4163 5837 54.198 9.25 192118.18510674784 1023702.5173577119 +5837 4326 192118.18510674784 1023702.5173577119 54.198 9.250000000000002 +4163 5837 57.762 14.649999999999999 582054.5436160624 1619753.0358247987 +5837 4326 582054.5436160624 1619753.0358247987 57.762 14.649999999999999 +5373 5839 -81.0 -10.219999999999999 500000.0 8870263.372353284 +5839 4326 500000.0 8870263.372353284 -81.0 -10.219999999999994 +5373 5839 -83.7 -16.619 211939.10330174997 8160650.066810287 +5839 4326 211939.10330174997 8160650.066810287 -83.7 -16.618999999999996 +5373 5839 -78.3 -3.8209999999999997 799889.7741129887 9577187.272723554 +5839 4326 799889.7741129887 9577187.272723554 -78.3 -3.8210000000000033 +4326 5842 11.625 -6.529999999999999 458540.517168803 9278192.068442028 +4326 5842 10.5315 -7.862 338101.3629545769 9130674.365796063 +4326 5842 12.718499999999999 -5.198 579626.5212736763 9425403.78356527 +4046 5844 29.405 -12.805 435406.9524966141 8583929.25558554 +5844 4326 435406.9524966141 8583929.25558554 29.405000000000005 -12.804999999999994 +4046 5844 29.0405 -13.3945 396081.76829720265 8518591.35110554 +5844 4326 396081.76829720265 8518591.35110554 29.040499999999998 -13.3945 +4046 5844 29.769499999999997 -12.2155 474920.5012665677 8649200.660985578 +5844 4326 474920.5012665677 8649200.660985578 29.7695 -12.2155 +5527 5858 -51.0 -14.335 500000.0000000007 8415218.377211729 +5858 4326 500000.0000000007 8415218.377211729 -51.0004625622172 -14.335433703514052 +5527 5858 -53.708999999999996 -33.5725 248545.89646746297 6281941.106502407 +5858 4326 248545.89646746297 6281941.106502407 -53.70955995734463 -33.57299650904997 +5527 5858 -48.291000000000004 4.902499999999996 800460.5631268972 10542496.04849642 +5858 4326 800460.5631268972 10542496.04849642 -48.29143003834556 4.902191594274142 +5527 5875 -73.0 -7.3 720802.450435209 9192594.358684313 +5875 4326 720802.450435209 9192594.358684313 -73.00057300238038 -7.300368327261597 +5527 5875 -73.909 -9.738999999999999 619670.178529329 8923247.904268343 +5875 4326 619670.178529329 8923247.904268343 -73.90957995524958 -9.739373032028134 +5527 5875 -72.091 -4.861 822681.0962589919 9462004.077393858 +5875 4326 822681.0962589919 9462004.077393858 -72.09156700794908 -4.861362175278749 +5527 5876 -68.995 -4.495 500554.67690929613 9503155.506783374 +5876 4326 500554.67690929613 9503155.506783374 -68.99555401104108 -4.49536340421103 +5527 5876 -71.6995 -10.4755 204483.29772784072 8840743.370753663 +5876 4326 204483.29772784072 8840743.370753663 -71.70007295843645 -10.475878674072119 +5527 5876 -66.2905 1.4855 801512.9549058347 10164378.241049027 +5876 4326 801512.9549058347 10164378.241049027 -66.29104011551162 1.48516137193414 +5527 5877 -62.995000000000005 -5.5 500553.8324037613 9392063.93399826 +5877 4326 500553.8324037613 9392063.93399826 -62.995525626976274 -5.500372505385923 +5527 5877 -65.6995 -15.202000000000002 209963.59741419856 8317533.299937487 +5877 4326 209963.59741419856 8317533.299937487 -65.70005641493584 -15.20240467179517 +5527 5877 -60.2905 4.202000000000002 800807.2264955372 10464978.65826382 +5877 4326 800807.2264955372 10464978.65826382 -60.29100957337929 4.2016788632840845 +4475 5879 45.165 -12.83 517905.66460200143 8581641.674532494 +5879 4326 517905.66460200143 8581641.674532494 45.1671205357028 -12.832527797879026 +4475 5879 44.9985 -13.028 499837.34969419055 8559751.159352869 +5879 4326 499837.34969419055 8559751.159352869 45.00061389527035 -13.030531637986448 +4475 5879 45.3315 -12.632 536002.1637134952 8603520.53253925 +5879 4326 536002.1637134952 8603520.53253925 45.33362717303024 -12.634523975036108 +4674 5880 -49.645 -14.335 5469770.7620535465 8410169.201811712 +5880 4326 5469770.7620535465 8410169.201811712 -49.645 -14.334999999999996 +4674 5880 -71.57350000000001 -33.5725 3376200.034343361 6145717.9046798255 +5880 4326 3376200.034343361 6145717.9046798255 -71.57350000000001 -33.5725 +4674 5880 -27.716500000000003 4.902499999999996 7914486.334051472 10599240.08577326 +5880 4326 7914486.334051472 10599240.08577326 -27.716500000000007 4.902499999999989 +5886 5887 -175.18 -19.91 1690514.258027318 2797447.3681043945 +5887 4326 1690514.258027318 2797447.3681043945 -175.18 -19.910000000000004 +5886 5887 -178.69 -25.103 1329589.108598591 2222580.6956646354 +5887 4326 1329589.108598591 2222580.6956646354 -178.69 -25.103 +5886 5887 -171.67000000000002 -14.717 2074488.2865285964 3366171.9961576466 +5887 4326 2074488.2865285964 3366171.9961576466 -171.67000000000002 -14.717000000000006 +4756 5896 102.825 16.009999999999998 588292.7492064342 1770748.0733179494 +5896 4326 588292.7492064342 1770748.0733179494 102.82683266806956 16.00902252313611 +4756 5896 102.2085 9.881 522865.83676924993 1092590.4261665116 +5896 4326 522865.83676924993 1092590.4261665116 102.2102881091784 9.880005305304255 +4756 5896 103.4415 22.139 648703.3300779583 2449652.2469681124 +5896 4326 648703.3300779583 2449652.2469681124 103.4434014274097 22.13804718949846 +4756 5897 105.005 15.864999999999998 500535.47623526555 1754528.9397213303 +5897 4326 500535.47623526555 1754528.9397213303 105.00682850709161 15.864003281877915 +4756 5897 103.6505 9.0835 351647.6173777889 1004660.9286033465 +5897 4326 351647.6173777889 1004660.9286033465 103.65228284833378 9.082496036175677 +4756 5897 106.35950000000001 22.646499999999996 639735.0013829842 2505778.310298751 +5897 4326 639735.0013829842 2505778.310298751 106.36140277674552 22.645515242338913 +4756 5898 108.015 15.76 501607.2575074395 1742911.191416216 +5898 4326 501607.2575074395 1742911.191416216 108.01681930329161 15.75897747863342 +4756 5898 106.6515 9.289 351842.9984507377 1027393.0926206446 +5898 4326 351842.9984507377 1027393.0926206446 106.65327807774952 9.28798114110877 +4756 5898 109.3785 22.230999999999998 642110.8259023023 2459780.4729505284 +5898 4326 642110.8259023023 2459780.4729505284 109.38038611744486 22.229978029739794 +4756 5899 107.595 15.120000000000001 483340.65583655256 1672106.1539976194 +5899 4326 483340.65583655256 1672106.1539976194 107.59681511953451 15.118979973154275 +4756 5899 106.54650000000001 9.225 367752.3053546154 1020256.4308169113 +5899 4326 367752.3053546154 1020256.4308169113 106.54827803568477 9.223981658689357 +4756 5899 108.6435 21.015 592881.7958480939 2324764.8822819805 +5899 4326 592881.7958480939 2324764.8822819805 108.64537341628836 21.013982898428942 +4326 5921 -111.0 81.005 0.0 -34778.650833612235 +4326 5921 -151.5 75.6005 -1037200.3190198627 -259724.02174514803 +4326 5921 -70.5 86.40950000000001 261541.6366551685 663496.4830133852 +4326 5922 -38.995 81.005 87.0921250690979 -34778.647077053196 +4326 5922 -79.4995 75.6005 -1037189.6692475929 -259732.96924570663 +4326 5922 1.5095000000000027 86.40950000000001 261592.65667671795 663539.3554452604 +4326 5923 33.005 81.005 87.0921250690979 -34778.647077053196 +4326 5923 -7.499499999999999 75.6005 -1037189.6692475929 -259732.96924570663 +4326 5923 73.5095 86.40950000000001 261592.65667671795 663539.3554452604 +4326 5924 105.005 81.005 87.0921250688763 -34778.647077053196 +4326 5924 64.5005 75.6005 -1037189.6692475931 -259732.96924570645 +4326 5924 145.5095 86.40950000000001 261592.65667671795 663539.3554452604 +4326 5925 177.0 81.005 0.0 -34778.650833612235 +4326 5925 136.5 75.6005 -1037200.3190198628 -259724.02174514785 +4326 5925 -142.5 86.40950000000001 261541.63665516867 663496.4830133853 +4326 5926 -111.0 73.005 0.0 -16781.261297234454 +4326 5926 -151.5 67.6005 -1597422.5021992063 -57211.1667476478 +4326 5926 -70.5 78.40950000000001 842933.4281016288 882446.5324442197 +4326 5927 -38.995 73.005 162.78812889759138 -16781.25449900885 +4326 5927 -79.4995 67.6005 -1597405.8856503947 -57224.508730898735 +4326 5927 1.5095000000000027 78.40950000000001 843100.0142088351 882580.3128912459 +4326 5928 33.005 73.005 162.78812889759138 -16781.25449900885 +4326 5928 -7.499499999999999 67.6005 -1597405.8856503947 -57224.508730898735 +4326 5928 73.5095 78.40950000000001 843100.0142088351 882580.3128912459 +4326 5929 105.005 73.005 162.78812889717716 -16781.25449900885 +4326 5929 64.5005 67.6005 -1597405.885650395 -57224.508730898386 +4326 5929 145.5095 78.40950000000001 843100.0142088352 882580.3128912462 +4326 5930 177.0 73.005 0.0 -16781.261297234454 +4326 5930 136.5 67.6005 -1597422.5021992065 -57211.1667476478 +4326 5930 -142.5 78.40950000000001 842933.4281016293 882446.5324442202 +4326 5931 -111.0 65.0 0.0 -11263.498530333203 +4326 5931 -151.5 59.6 -2137652.7063676133 97247.3376234556 +4326 5931 -70.5 70.4 1418141.4064247587 1061292.9791688686 +4326 5932 -39.0 65.0 0.0 -11263.498530333203 +4326 5932 -79.5 59.6 -2137652.7063676147 97247.33762345595 +4326 5932 1.5 70.4 1418141.4064247587 1061292.9791688686 +4326 5933 33.005 65.0 235.3026867744985 -11263.4892176013 +4326 5933 -7.499499999999999 59.6 -2137630.034953137 97230.4170509092 +4326 5933 73.5095 70.4 1418427.1582065455 1061506.2822359027 +4326 5934 105.005 65.0 235.30268677389984 -11263.4892176013 +4326 5934 64.5005 59.6 -2137630.0349531374 97230.4170509099 +4326 5934 145.5095 70.4 1418427.1582065457 1061506.2822359027 +4326 5935 177.0 65.0 0.0 -11263.498530333203 +4326 5935 136.5 59.6 -2137652.7063676137 97247.33762345595 +4326 5935 -142.5 70.4 1418141.4064247597 1061292.979168869 +4326 5936 0.0 75.0 2837275.0968569266 3450203.0076683504 +4326 5936 -162.0 61.5 1328769.524675259 -1157891.1046123132 +4326 5936 162.0 88.5 1876233.05550019 1888559.7426219846 +4326 5937 0.0 75.0 3649110.0135774976 2290782.5895502158 +4326 5937 -162.0 61.5 -850543.5654458641 484339.1008904942 +4326 5937 162.0 88.5 1835075.9516825832 2023178.5634130272 +4326 5938 0.0 75.0 2912025.40158854 595604.0379848825 +4326 5938 -162.0 61.5 -508969.3179528904 4031723.293454417 +4326 5938 162.0 88.5 1956895.020603663 2160869.973166339 +4326 5939 0.0 75.0 1482535.532208559 407408.1264051276 +4326 5939 -162.0 61.5 1999999.9999999995 5228440.270408336 +4326 5939 162.0 88.5 2097892.6074760137 2134737.6150777666 +4326 5940 0.0 75.0 382508.7204744264 2433405.482113261 +4326 5940 -162.0 61.5 5224015.805220808 2168963.508725049 +4326 5940 162.0 88.5 2139676.265080423 1909293.1728673046 +4747 6050 -30.0 85.33500000000001 25500000.0 1488599.63475686 +6050 4326 25500000.0 1488599.63475686 -29.999999999999996 85.33500000000001 +4747 6050 -57.0 83.0805 25149803.220098753 1320651.3563972434 +6050 4326 25149803.220098753 1320651.3563972434 -57.00000000000004 83.0805 +4747 6050 -3.0 87.5895 25622279.019944042 1769621.5813966722 +6050 4326 25622279.019944042 1769621.5813966722 -2.999999999999883 87.5895 +4747 6051 -52.0 82.005 18500000.0 2494036.4556477773 +6051 4326 18500000.0 2494036.4556477773 -52.0 82.005 +4747 6051 -70.0 79.7505 18147931.839843396 2297546.853694125 +6051 4326 18147931.839843396 2297546.853694125 -69.99999999999996 79.7505 +4747 6051 -34.0 84.2595 18697938.734054413 2776817.082481822 +6051 4326 18697938.734054413 2776817.082481822 -33.99999999999988 84.2595 +4747 6052 -11.995000000000001 82.005 20500077.637682203 2494036.4590028715 +6052 4326 20500077.637682203 2494036.4590028715 -11.994999999999983 82.005 +4747 6052 -29.9995 79.7505 20147941.301875196 2297543.810826408 +6052 4326 20147941.301875196 2297543.810826408 -29.999499999999962 79.7505 +4747 6052 6.009499999999996 84.2595 20698039.80584705 2776849.5955095273 +6052 4326 20698039.80584705 2776849.5955095273 6.009500000000073 84.2595 +4747 6053 -64.5 78.66499999999999 29598635.795574117 3499075.3029469335 +6053 4326 29598635.795574117 3499075.3029469335 -64.50000000000004 78.66499999999999 +4747 6053 -73.95 76.4105 29370228.215559065 3249121.348787156 +6053 4326 29370228.215559065 3249121.348787156 -73.95000000000002 76.4105 +4747 6053 -55.05 80.9195 29743649.19695414 3776192.613579528 +6053 4326 29743649.19695414 3776192.613579528 -55.049999999999976 80.9195 +4747 6054 -39.0 78.66499999999999 31500000.0 3495274.995885591 +6054 4326 31500000.0 3495274.995885591 -39.0 78.66499999999999 +4747 6054 -52.5 76.4105 31148797.7602268 3284376.3377638357 +6054 4326 31148797.7602268 3284376.3377638357 -52.500000000000036 76.4105 +4747 6054 -25.5 80.9195 31735932.315428797 3774345.753656459 +6054 4326 31735932.315428797 3774345.753656459 -25.499999999999996 80.9195 +4747 6055 -11.055 78.66499999999999 33476853.765299667 3495483.9743222697 +6055 4326 33476853.765299667 3495483.9743222697 -11.055000000000048 78.66499999999999 +4747 6055 -22.7055 76.4105 33169129.369208004 3279738.791734199 +6055 4326 33169129.369208004 3279738.791734199 -22.705500000000068 76.4105 +4747 6055 0.5954999999999977 80.9195 33685807.540626615 3763860.900792783 +6055 4326 33685807.540626615 3763860.900792783 0.5955000000000523 80.9195 +4747 6056 -63.5 75.33500000000001 20514129.411981855 4496778.991163726 +6056 4326 20514129.411981855 4496778.991163726 -63.49999999999996 75.33500000000001 +4747 6056 -74.75 73.0805 20152458.981151663 4276767.759092814 +6056 4326 20152458.981151663 4276767.759092814 -74.75 73.0805 +4747 6056 -52.25 77.5895 20780268.09680362 4776249.353036755 +6056 4326 20780268.09680362 4776249.353036755 -52.250000000000014 77.5895 +4747 6057 -38.5 75.33500000000001 22514129.411981855 4496778.991163726 +6057 4326 22514129.411981855 4496778.991163726 -38.49999999999996 75.33500000000001 +4747 6057 -49.75 73.0805 22152458.981151663 4276767.759092814 +6057 4326 22152458.981151663 4276767.759092814 -49.74999999999999 73.0805 +4747 6057 -27.25 77.5895 22780268.09680362 4776249.353036755 +6057 4326 22780268.09680362 4776249.353036755 -27.25000000000001 77.5895 +4747 6058 -14.0 75.33500000000001 24500000.0 4496719.340053453 +6058 4326 24500000.0 4496719.340053453 -14.0 75.33500000000001 +4747 6058 -24.8 73.0805 24150860.431400087 4277061.879797715 +6058 4326 24150860.431400087 4277061.879797715 -24.800000000000054 73.0805 +4747 6058 -3.200000000000003 77.5895 24757871.184748057 4771932.676834721 +6058 4326 24757871.184748057 4771932.676834721 -3.199999999999998 77.5895 +4747 6059 -61.94 71.995 41502069.933536164 5496653.831016769 +6059 4326 41502069.933536164 5496653.831016769 -61.93999999999991 71.995 +4747 6059 -70.895 69.73150000000001 41157094.544053115 5269513.174005573 +6059 4326 41157094.544053115 5269513.174005573 -70.89500000000002 69.73150000000001 +4747 6059 -52.985 74.2585 41772189.885921344 5769621.243089696 +6059 4326 41772189.885921344 5769621.243089696 -52.98499999999995 74.2585 +4747 6060 -42.0 72.005 43500000.0 5497768.190454762 +6060 4326 43500000.0 5497768.190454762 -42.0 72.005 +4747 6060 -51.0 69.7505 43153392.37286827 5272211.126975045 +6060 4326 43153392.37286827 5272211.126975045 -51.00000000000008 69.75049999999999 +4747 6060 -33.0 74.2595 43771723.748701595 5769663.926273582 +6060 4326 43771723.748701595 5769663.926273582 -33.00000000000001 74.2595 +4747 6061 -22.0 72.005 45500000.0 5497768.190454762 +6061 4326 45500000.0 5497768.190454762 -22.0 72.005 +4747 6061 -31.0 69.7505 45153392.37286827 5272211.126975045 +6061 4326 45153392.37286827 5272211.126975045 -31.000000000000075 69.75049999999999 +4747 6061 -13.0 74.2595 45771723.748701595 5769663.926273582 +6061 4326 45771723.748701595 5769663.926273582 -13.000000000000004 74.2595 +4747 6062 -56.0 68.66499999999999 26500000.0 6497494.028068438 +6062 4326 26500000.0 6497494.028068438 -56.0 68.66499999999999 +4747 6062 -64.1 66.4105 26139064.994401634 6269895.137831682 +6062 4326 26139064.994401634 6269895.137831682 -64.09999999999998 66.4105 +4747 6062 -47.900000000000006 70.9195 26794896.542878013 6768410.22582616 +6062 4326 26794896.542878013 6768410.22582616 -47.90000000000003 70.91950000000001 +4747 6063 -38.0 68.66499999999999 28500000.0 6497494.028068438 +6063 4326 28500000.0 6497494.028068438 -38.0 68.66499999999999 +4747 6063 -46.1 66.4105 28139064.994401634 6269895.137831682 +6063 4326 28139064.994401634 6269895.137831682 -46.09999999999997 66.4105 +4747 6063 -29.900000000000002 70.9195 28794896.542878013 6768410.22582616 +6063 4326 28794896.542878013 6768410.22582616 -29.90000000000003 70.91950000000001 +4747 6064 -20.0 68.66499999999999 30500000.0 6497494.028068438 +6064 4326 30500000.0 6497494.028068438 -20.0 68.66499999999999 +4747 6064 -28.1 66.4105 30139064.994401634 6269895.137831682 +6064 4326 30139064.994401634 6269895.137831682 -28.099999999999977 66.4105 +4747 6064 -11.900000000000002 70.9195 30794896.542878013 6768410.22582616 +6064 4326 30794896.542878013 6768410.22582616 -11.900000000000029 70.91950000000001 +4747 6065 -50.5 65.33500000000001 11523281.865805842 7498304.739788385 +6065 4326 11523281.865805842 7498304.739788385 -50.50000000000001 65.33500000000002 +4747 6065 -58.15 63.0805 11139338.778724842 7267396.2268491145 +6065 4326 11139338.778724842 7267396.2268491145 -58.15 63.0805 +4747 6065 -42.85 67.5895 11846035.07550973 7771978.841088924 +6065 4326 11846035.07550973 7771978.841088924 -42.84999999999998 67.58949999999999 +4747 6066 -33.5 65.33500000000001 13523281.865805842 7498304.739788385 +6066 4326 13523281.865805842 7498304.739788385 -33.50000000000001 65.33500000000002 +4747 6066 -41.15 63.0805 13139338.778724842 7267396.2268491145 +6066 4326 13139338.778724842 7267396.2268491145 -41.15 63.0805 +4747 6066 -25.85 67.5895 13846035.07550973 7771978.841088924 +6066 4326 13846035.07550973 7771978.841088924 -25.849999999999984 67.58949999999999 +4747 6067 -51.5 62.005 20526183.501266457 8498952.715529013 +6067 4326 20526183.501266457 8498952.715529013 -51.49999999999999 62.004999999999995 +4747 6067 -58.25 59.7505 20149051.50574242 8264597.5995477075 +6067 4326 20149051.50574242 8264597.5995477075 -58.25000000000003 59.75049999999999 +4747 6067 -44.75 64.2595 20850858.014716968 8769728.602937866 +6067 4326 20850858.014716968 8769728.602937866 -44.74999999999999 64.25949999999999 +4747 6068 -36.5 62.005 22526183.501266457 8498952.715529013 +6068 4326 22526183.501266457 8498952.715529013 -36.499999999999986 62.004999999999995 +4747 6068 -43.25 59.7505 22149051.50574242 8264597.5995477075 +6068 4326 22149051.50574242 8264597.5995477075 -43.25000000000003 59.75049999999999 +4747 6068 -29.75 64.2595 22850858.014716968 8769728.602937866 +6068 4326 22850858.014716968 8769728.602937866 -29.74999999999999 64.25949999999999 +4258 6069 16.005 82.005 22500077.637682203 2494036.4590028715 +6069 4326 22500077.637682203 2494036.4590028715 16.005000000000017 82.005 +4258 6069 -1.9994999999999998 79.7505 22147941.301875196 2297543.810826408 +6069 4326 22147941.301875196 2297543.810826408 -1.9994999999999596 79.7505 +4258 6069 34.009499999999996 84.2595 22698039.80584705 2776849.5955095273 +6069 4326 22698039.80584705 2776849.5955095273 34.009500000000074 84.2595 +4258 6070 17.33 78.66499999999999 11419530.383332541 3497803.115900361 +6070 4326 11419530.383332541 3497803.115900361 17.32999999999999 78.66499999999999 +4258 6070 -1.282 76.4105 10929218.16569153 3353797.5891288444 +6070 4326 10929218.16569153 3353797.5891288444 -1.2820000000000074 76.4105 +4258 6070 35.94199999999999 80.9195 11760608.937159697 3780473.482019154 +6070 4326 11760608.937159697 3780473.482019154 35.942 80.9195 +4258 6071 10.005 75.33500000000001 26500141.295798533 4496719.3460186 +6071 4326 26500141.295798533 4496719.3460186 10.005 75.33500000000001 +4258 6071 -0.7994999999999999 73.0805 26150876.415667396 4277058.931909654 +6071 4326 26150876.415667396 4277058.931909654 -0.7995000000000485 73.0805 +4258 6071 20.8095 77.5895 26758095.49199988 4771974.0641369885 +6071 4326 26758095.49199988 4771974.0641369885 20.80950000000007 77.5895 +4258 6072 34.004999999999995 75.33500000000001 28500141.295798533 4496719.3460186 +6072 4326 28500141.295798533 4496719.3460186 34.005 75.33500000000001 +4258 6072 23.200499999999998 73.0805 28150876.415667396 4277058.931909654 +6072 4326 28150876.415667396 4277058.931909654 23.200499999999955 73.0805 +4258 6072 44.8095 77.5895 28758095.49199988 4771974.064136988 +6072 4326 28758095.49199988 4771974.064136988 44.80950000000007 77.58949999999999 +4258 6073 14.005 72.005 11500172.401904354 5497768.197610053 +6073 4326 11500172.401904354 5497768.197610053 14.005000000000024 72.005 +4258 6073 5.000500000000001 69.7505 11153411.485354474 5272208.249964922 +6073 4326 11153411.485354474 5272208.249964922 5.00050000000001 69.7505 +4258 6073 23.0095 74.2595 11772008.42731354 5769706.803223966 +6073 4326 11772008.42731354 5769706.803223966 23.009500000000006 74.2595 +4258 6074 34.004999999999995 72.005 13500172.401904352 5497768.197610053 +6074 4326 13500172.401904352 5497768.197610053 34.004999999999974 72.005 +4258 6074 25.0005 69.7505 13153411.485354474 5272208.249964922 +6074 4326 13153411.485354474 5272208.249964922 25.000500000000017 69.7505 +4258 6074 43.009499999999996 74.2595 13772008.42731354 5769706.803223966 +6074 4326 13772008.42731354 5769706.803223966 43.00950000000001 74.2595 +4326 6075 53.005 82.005 24500077.637682203 2494036.459002964 +4326 6075 35.0005 79.7505 24147941.301880818 2297543.8108294536 +4326 6075 71.0095 84.2595 24698039.805843815 2776849.595505142 +4326 6076 93.005 82.005 26500077.637682203 2494036.459002964 +4326 6076 75.0005 79.7505 26147941.301880818 2297543.8108294536 +4326 6076 111.0095 84.2595 26698039.805843815 2776849.595505142 +4326 6077 51.0 78.66499999999999 13478060.319973107 3495462.753584967 +4326 6077 36.591 76.4105 13100218.237448674 3296645.986111101 +4326 6077 65.409 80.9195 13734370.07443395 3773979.505426659 +4326 6078 82.505 78.66499999999999 15489139.458069893 3495321.0020638206 +4326 6078 68.5505 76.4105 15124585.131690096 3290280.6186496657 +4326 6078 96.4595 80.9195 15735237.102798073 3774182.452594295 +4326 6079 113.505 78.66499999999999 17489139.45806989 3495321.0020638206 +4326 6079 99.5505 76.4105 17124585.131690096 3290280.6186496657 +4326 6079 127.45949999999999 80.9195 17735237.10279807 3774182.452594295 +4326 6080 144.505 78.66499999999999 19489139.45806989 3495321.0020638206 +4326 6080 130.5505 76.4105 19124585.131690096 3290280.6186496657 +4326 6080 158.4595 80.9195 19735237.10279807 3774182.452594295 +4326 6081 58.005 75.33500000000001 30500141.29579853 4496719.346018643 +4326 6081 47.2005 73.0805 30150876.415672664 4277058.931912436 +4326 6081 68.8095 77.5895 30758095.49199582 4771974.064133198 +4326 6082 82.005 75.33500000000001 32500141.29579853 4496719.346018643 +4326 6082 71.2005 73.0805 32150876.415672664 4277058.931912435 +4326 6082 92.8095 77.5895 32758095.49199582 4771974.064133198 +4326 6083 106.005 75.33500000000001 34500141.29579853 4496719.346018643 +4326 6083 95.2005 73.0805 34150876.41567267 4277058.931912435 +4326 6083 116.8095 77.5895 34758095.49199582 4771974.064133198 +4326 6084 130.005 75.33500000000001 36500141.29579853 4496719.346018643 +4326 6084 119.2005 73.0805 36150876.41567267 4277058.931912435 +4326 6084 140.80949999999999 77.5895 36758095.49199582 4771974.064133198 +4326 6085 154.005 75.33500000000001 38500141.29579853 4496719.346018643 +4326 6085 143.2005 73.0805 38150876.41567267 4277058.931912435 +4326 6085 164.80949999999999 77.5895 38758095.49199582 4771974.064133198 +4326 6086 178.505 75.33500000000001 40486011.87883063 4496777.804113586 +4326 6086 167.2505 73.0805 40120552.51210949 4282903.15387643 +4326 6086 -170.2405 77.5895 40756914.853126705 4771756.639952782 +4326 6087 54.005 72.005 15500172.40190435 5497768.19761008 +4326 6087 45.0005 69.7505 15153411.4853595 5272208.249967376 +4326 6087 63.0095 74.2595 15772008.427309386 5769706.803220589 +4326 6088 74.505 72.005 17517412.388381127 5497841.181152688 +4326 6088 65.0505 69.7505 17155322.854356945 5271921.3501569815 +4326 6088 83.9595 74.2595 17800440.941829212 5774221.046263 +4326 6089 95.505 72.005 19517412.388381127 5497841.181152688 +4326 6089 86.0505 69.7505 19155322.854356945 5271921.3501569815 +4326 6089 104.9595 74.2595 19800440.941829212 5774221.046263 +4326 6090 116.505 72.005 21517412.388381127 5497841.181152688 +4326 6090 107.0505 69.7505 21155322.854356945 5271921.3501569815 +4326 6090 125.9595 74.2595 21800440.941829212 5774221.046263 +4326 6091 137.5 72.005 23517239.992475536 5497839.742955841 +4326 6091 128.05 69.7505 23155303.73948921 5271924.211301394 +4326 6091 146.95 74.2595 23800156.9748207 5774173.68506532 +4326 6092 158.505 72.005 25517412.388381127 5497841.181152688 +4326 6092 149.0505 69.7505 25155322.854356945 5271921.350156981 +4326 6092 167.9595 74.2595 25800440.941829212 5774221.046263 +4326 6093 180.0 72.005 27534478.79709157 5498054.395528933 +4326 6093 170.1 69.7505 27157215.34360699 5271638.882293978 +4326 6093 -170.1 74.2595 27828515.542808905 5779131.2947539035 +4759 6094 -163.0 72.005 29500000.0 5497768.190454762 +6094 4326 29500000.0 5497768.190454762 -163.0 72.005 +4759 6094 -172.0 69.7505 29153392.372868273 5272211.126975045 +6094 4326 29153392.372868273 5272211.126975045 -172.0 69.75049999999999 +4759 6094 -154.0 74.2595 29771723.748701595 5769663.926273582 +6094 4326 29771723.748701595 5769663.926273582 -154.0 74.2595 +4759 6095 -147.0 72.005 31500000.0 5497768.190454762 +6095 4326 31500000.0 5497768.190454762 -147.0 72.005 +4759 6095 -156.0 69.7505 31153392.372868273 5272211.126975045 +6095 4326 31153392.372868273 5272211.126975045 -155.99999999999997 69.75049999999999 +4759 6095 -138.0 74.2595 31771723.748701595 5769663.926273582 +6095 4326 31771723.748701595 5769663.926273582 -138.0 74.2595 +4759 6096 -165.0 68.66499999999999 14500000.0 6497494.028068438 +6096 4326 14500000.0 6497494.028068438 -165.0 68.66499999999999 +4759 6096 -173.1 66.4105 14139064.994401632 6269895.137831682 +6096 4326 14139064.994401632 6269895.137831682 -173.1 66.4105 +4759 6096 -156.9 70.9195 14794896.542878013 6768410.22582616 +6096 4326 14794896.542878013 6768410.22582616 -156.9 70.91950000000001 +4759 6097 -147.0 68.66499999999999 16500000.0 6497494.028068438 +6097 4326 16500000.0 6497494.028068438 -147.0 68.66499999999999 +4759 6097 -155.1 66.4105 16139064.994401632 6269895.137831682 +6097 4326 16139064.994401632 6269895.137831682 -155.1 66.4105 +4759 6097 -138.9 70.9195 16794896.542878013 6768410.22582616 +6097 4326 16794896.542878013 6768410.22582616 -138.9 70.91950000000001 +4617 6098 -90.0 85.33500000000001 23500000.0 1488599.63475686 +6098 4326 23500000.0 1488599.63475686 -90.000073225826 85.33501027659338 +4617 6098 -117.0 83.0805 23149803.220098753 1320651.3563972434 +6098 4326 23149803.220098753 1320651.3563972434 -117.00008056303403 83.08050652684142 +4617 6098 -63.0 87.5895 23622279.019944042 1769621.5813966722 +6098 4326 23622279.019944042 1769621.5813966722 -63.00002079411696 87.58951186921334 +4617 6099 -115.0 82.005 14500000.0 2494036.4556477773 +6099 4326 14500000.0 2494036.4556477773 -115.00006787279413 82.0050069272854 +4617 6099 -133.0 79.7505 14147931.839843396 2297546.8536941255 +6099 4326 14147931.839843396 2297546.8536941255 -133.00006107828457 79.75050376356619 +4617 6099 -97.0 84.2595 14697938.73405441 2776817.082481822 +6099 4326 14697938.73405441 2776817.082481822 -97.00007069653628 84.25950950857899 +4617 6100 -75.0 82.005 16500000.0 2494036.4556477773 +6100 4326 16500000.0 2494036.4556477773 -75.00002140715377 82.00501168477112 +4617 6100 -93.0 79.7505 16147931.839843396 2297546.8536941255 +6100 4326 16147931.839843396 2297546.8536941255 -93.00003481975101 79.75051017523657 +4617 6100 -57.0 84.2595 16697938.73405441 2776817.082481822 +6100 4326 16697938.73405441 2776817.082481822 -56.99999451620756 84.25951209874258 +4617 6101 -129.0 78.66499999999999 25500000.0 3495274.995885591 +6101 4326 25500000.0 3495274.995885591 -129.00005386530594 78.66500458541502 +4617 6101 -142.5 76.4105 25148797.7602268 3284376.3377638357 +6101 4326 25148797.7602268 3284376.3377638357 -142.50004735829364 76.41050212161633 +4617 6101 -115.5 80.9195 25735932.315428797 3774345.753656459 +6101 4326 25735932.315428797 3774345.753656459 -115.5000598847473 80.9195068961779 +4617 6102 -99.0 78.66499999999999 27500000.0 3495274.995885591 +6102 4326 27500000.0 3495274.995885591 -99.00003627291238 78.66500946840027 +4617 6102 -112.5 76.4105 27148797.7602268 3284376.3377638357 +6102 4326 27148797.7602268 3284376.3377638357 -112.50003802805459 76.4105075599336 +4617 6102 -85.5 80.9195 27735932.315428797 3774345.753656459 +6102 4326 27735932.315428797 3774345.753656459 -85.50003120614384 80.9195109203395 +4617 6103 -74.39 78.66499999999999 29381907.02095899 3500726.0169807207 +6103 4326 29381907.02095899 3500726.0169807207 -74.39001362253791 78.66501180142097 +4617 6103 -83.039 76.4105 29135041.25339371 3287679.7240958856 +6103 4326 29135041.25339371 3287679.7240958856 -83.03901807564709 76.410511217294 +4617 6103 -65.741 80.9195 29557435.97874264 3748569.1145230476 +6103 4326 29557435.97874264 3748569.1145230476 -65.74100666749348 80.91951211732486 +4617 6104 -128.5 75.33500000000001 14514129.411981853 4496778.991163726 +6104 4326 14514129.411981853 4496778.991163726 -128.5000412995909 75.3350048375056 +4617 6104 -139.75 73.0805 14152458.981151663 4276767.759092814 +6104 4326 14152458.981151663 4276767.759092814 -139.75003774862046 73.08050284856374 +4617 6104 -117.25 77.5895 14780268.09680362 4776249.353036755 +6104 4326 14780268.09680362 4776249.353036755 -117.25004429004609 77.58950673974559 +4617 6105 -103.5 75.33500000000001 16514129.411981855 4496778.991163726 +6105 4326 16514129.411981855 4496778.991163726 -103.50003035313343 75.33500892757317 +4617 6105 -114.75 73.0805 16152458.981151663 4276767.759092814 +6105 4326 16152458.981151663 4276767.759092814 -114.75003128337305 73.08050727483254 +4617 6105 -92.25 77.5895 16780268.09680362 4776249.353036755 +6105 4326 16780268.09680362 4776249.353036755 -92.25002777916009 77.58951030750788 +4617 6106 -79.0 75.33500000000001 18500000.0 4496719.340053453 +6106 4326 18500000.0 4496719.340053453 -79.00001349906404 75.33501152796264 +4617 6106 -89.8 73.0805 18150860.431400087 4277061.879797715 +6106 4326 18150860.431400087 4277061.879797715 -89.80001838006326 73.08051058339255 +4617 6106 -68.2 77.5895 18757871.184748057 4771932.676834721 +6106 4326 18757871.184748057 4771932.676834721 -68.20000636249716 77.58951209671513 +4617 6107 -131.0 72.005 33500000.0 5497768.190454762 +6107 4326 33500000.0 5497768.190454762 -131.00003415724632 72.00500451800679 +4617 6107 -140.0 69.7505 33153392.372868273 5272211.126975045 +6107 4326 33153392.372868273 5272211.126975045 -140.00003163786482 69.75050297774374 +4617 6107 -122.0 74.2595 33771723.748701595 5769663.926273582 +6107 4326 33771723.748701595 5769663.926273582 -122.00003647662969 74.2595060371454 +4617 6108 -111.0 72.005 35500000.0 5497768.190454762 +6108 4326 35500000.0 5497768.190454762 -111.0000278921774 72.0050078720123 +4617 6108 -120.0 69.7505 35153392.37286827 5272211.126975045 +6108 4326 35153392.37286827 5272211.126975045 -120.00002775279309 69.75050647494963 +4617 6108 -102.0 74.2595 35771723.748701595 5769663.926273582 +6108 4326 35771723.748701595 5769663.926273582 -102.0000273602673 74.25950914388419 +4617 6109 -91.0 72.005 37500000.0 5497768.190454762 +6109 4326 37500000.0 5497768.190454762 -91.00001787223981 72.00501044774285 +4617 6109 -100.0 69.7505 37153392.37286827 5272211.126975045 +6109 4326 37153392.37286827 5272211.126975045 -100.00002012966581 69.75050938303708 +4617 6109 -82.0 74.2595 37771723.748701595 5769663.926273582 +6109 4326 37771723.748701595 5769663.926273582 -82.00001455319101 74.25951129804058 +4617 6110 -71.0 72.005 39500000.0 5497768.190454762 +6110 4326 39500000.0 5497768.190454762 -71.00000530598622 72.00501193452673 +4617 6110 -80.0 69.7505 39153392.37286827 5272211.126975045 +6110 4326 39153392.37286827 5272211.126975045 -80.0000096879441 69.75051135124728 +4617 6110 -62.0 74.2595 39771723.748701595 5769663.926273582 +6110 4326 39771723.748701595 5769663.926273582 -61.99999960012482 74.25951223979092 +4617 6111 -131.5 68.66499999999999 18520300.40045174 6497576.54809471 +6111 4326 18520300.40045174 6497576.54809471 -131.50002896914864 68.66500454533698 +4617 6111 -140.05 66.4105 18141280.210252836 6269602.603657585 +6111 4326 18141280.210252836 6269602.603657585 -140.05002732520126 66.41050312737755 +4617 6111 -122.95 70.9195 18829246.417031027 6773230.798926938 +6111 4326 18829246.417031027 6773230.798926938 -122.95003031704579 70.91950596300889 +4617 6112 -112.5 68.66499999999999 20520300.40045174 6497576.54809471 +6112 4326 20520300.40045174 6497576.54809471 -112.5000240003041 68.66500765417966 +4617 6112 -121.05 66.4105 20141280.210252836 6269602.603657585 +6112 4326 20141280.210252836 6269602.603657585 -121.05002415651333 66.41050633401025 +4617 6112 -103.95 70.9195 20829246.417031027 6773230.798926938 +6112 4326 20829246.417031027 6773230.798926938 -103.9500232784564 70.91950888348562 +4617 6113 -93.5 68.66499999999999 22520300.40045174 6497576.54809471 +6113 4326 22520300.40045174 6497576.54809471 -93.50001606339823 68.66501011121592 +4617 6113 -102.05 66.4105 22141280.210252836 6269602.603657585 +6113 4326 22141280.210252836 6269602.603657585 -102.05001800274393 66.41050905095445 +4617 6113 -84.95 70.9195 22829246.417031027 6773230.798926938 +6113 4326 22829246.417031027 6773230.798926938 -84.95001335045946 70.91951099967098 +4617 6114 -74.5 68.66499999999999 24520300.40045174 6497576.54809471 +6114 4326 24520300.40045174 6497576.54809471 -74.50000602325899 68.66501164871957 +4617 6114 -83.05 66.4105 24141280.210252836 6269602.603657585 +6114 4326 24141280.210252836 6269602.603657585 -83.05000953442476 66.41051098216371 +4617 6114 -65.95 70.9195 24829246.417031027 6773230.798926938 +6114 4326 24829246.417031027 6773230.798926938 -65.95000161483853 70.9195120809789 +4326 6115 30.005 85.33500000000001 27500045.399734467 1488599.6367317028 +4326 6115 3.0005 83.0805 27149809.221071247 1320648.310068512 +4326 6115 57.009499999999996 87.5895 27622318.830241572 1769641.7950536767 +4326 6116 90.005 85.33500000000001 29500045.399734467 1488599.6367317028 +4326 6116 63.0005 83.0805 29149809.221071247 1320648.310068512 +4326 6116 117.0095 87.5895 29622318.830241572 1769641.7950536767 +4326 6117 150.0 85.33 31500000.0 1488041.441120854 +4326 6117 123.0 83.08 31149777.9362898 1320601.5503649951 +4326 6117 177.0 87.58 31622759.53915318 1768675.0170771922 +4326 6118 -150.0 85.33500000000001 21500000.0 1488599.634757044 +4326 6118 -177.0 83.0805 21149803.220104445 1320651.356400088 +4326 6118 -123.0 87.5895 21622279.01994203 1769621.5813922724 +4326 6119 133.005 82.005 28500077.637682203 2494036.459002964 +4326 6119 115.0005 79.7505 28147941.301880818 2297543.810829453 +4326 6119 151.0095 84.2595 28698039.805843815 2776849.595505142 +4326 6120 166.005 82.005 10500077.637682201 2494036.459002964 +4326 6120 148.0005 79.7505 10147941.301880816 2297543.8108294536 +4326 6120 -175.9905 84.2595 10698039.805843815 2776849.595505142 +4326 6121 -154.495 82.005 12492313.96324097 2494069.3387259548 +4326 6121 -172.0495 79.7505 12146995.228998698 2297848.5023743254 +4326 6121 -136.94050000000001 84.2595 12687906.645780614 2773680.5930516995 +4326 6122 175.505 78.66499999999999 21489139.458069894 3495321.0020638206 +4326 6122 161.5505 76.4105 21124585.131690096 3290280.6186496657 +4326 6122 -170.5405 80.9195 21735237.10279807 3774182.452594295 +4326 6123 -153.5 78.66499999999999 23489029.758290764 3495321.9361698898 +4326 6123 -167.45 76.4105 23124572.407048635 3290283.8313904954 +4326 6123 -139.55 80.9195 23735074.011852898 3774144.217187753 +4326 6124 -155.0 75.33500000000001 12500000.0 4496719.340053497 +4326 6124 -167.6 73.0805 12093487.256109722 4288547.377526531 +4326 6124 -142.4 77.5895 12800246.469635265 4780415.756872272 +4258 6125 -4.995000000000001 72.005 47500172.40190435 5497768.197610053 +6125 4326 47500172.40190435 5497768.197610053 -4.99500000000003 72.005 +4258 6125 -13.9995 69.7505 47153411.485354476 5272208.249964922 +6125 4326 47153411.485354476 5272208.249964922 -13.99949999999994 69.7505 +4258 6125 4.009499999999996 74.2595 47772008.42731354 5769706.803223966 +6125 4326 47772008.42731354 5769706.803223966 4.009499999999885 74.2595 +4723 6128 -81.25 19.310000000000002 1554252.55460802 7004659.353607102 +6128 4326 1554252.55460802 7004659.353607102 -81.24921444998319 19.31012838476929 +4723 6128 -81.439 19.220000000000002 1489026.623750742 6972117.280115741 +6128 4326 1489026.623750742 6972117.280115741 -81.4382187517098 19.22014076635213 +4723 6128 -81.061 19.4 1619406.6809199944 7037272.072399986 +6128 4326 1619406.6809199944 7037272.072399986 -81.06021011950433 19.400116015532216 +4726 6129 -79.91499999999999 19.705 2013496.0939861175 7149180.908577989 +6129 4326 2013496.0939861175 7149180.908577989 -79.91440176809657 19.705234509108987 +4726 6129 -80.1175 19.6375 1943988.9721471178 7124271.235000692 +6129 4326 1943988.9721471178 7124271.235000692 -80.11689942893528 19.637737401722713 +4726 6129 -79.71249999999999 19.7725 2082947.8767510452 7174176.351522031 +6129 4326 2082947.8767510452 7174176.351522031 -79.71190410460677 19.772731621396243 +4267 6201 -84.66499999999999 44.86 1913996.5263179347 562838.1772938652 +6201 4326 1913996.5263179347 562838.1772938652 -84.6649125578243 44.859997697540074 +4267 6201 -86.8205 43.906 1344496.312685588 224918.01157233762 +6201 4326 1344496.312685588 224918.01157233762 -86.82048896336775 43.90600169538299 +4267 6201 -82.5095 45.814 2465073.0221269457 915737.671308783 +6201 4326 2465073.0221269457 915737.671308783 -82.50933368728191 45.8139939890751 +4267 6202 -84.66499999999999 42.955 1911209.5771414547 530429.3024771563 +6202 4326 1911209.5771414547 530429.3024771563 -84.66491530305169 42.95500562320555 +4267 6202 -86.9465 41.8165 1287664.4132710847 126403.66988304957 +6202 4326 1287664.4132710847 126403.66988304957 -86.94649356942173 41.81651463033279 +4267 6202 -82.3835 44.0935 2512334.0410179235 951187.6347387079 +6202 4326 2512334.0410179235 951187.6347387079 -82.38333419916542 44.09349751580652 +3906 6204 21.744999999999997 41.605000000000004 562090.5877099717 4607108.143773476 +6204 4326 562090.5877099717 4607108.143773476 21.73960330051661 41.605424946316695 +3906 6204 20.5795 40.9255 464589.1052727838 4531476.37301055 +6204 4326 464589.1052727838 4531476.37301055 20.57428293768151 40.92598309306493 +3906 6204 22.9105 42.2845 657547.4263396836 4684065.475046453 +6204 4326 657547.4263396836 4684065.475046453 22.904919978460526 42.28486804711173 +4674 6210 -44.995000000000005 2.565 500555.82110658236 283511.470464043 +6210 4326 500555.82110658236 283511.470464043 -44.995000000000005 2.5650000000000004 +4674 6210 -47.6995 0.2565 199504.28603197454 28382.667338997962 +6210 4326 199504.28603197454 28382.667338997962 -47.6995 0.25650000000000006 +4674 6210 -42.2905 4.8735 800527.9173611738 539285.2401819509 +6210 4326 800527.9173611738 539285.2401819509 -42.2905 4.873499999999999 +4674 6211 -40.11 0.37 376479.5709775466 40903.850047567095 +6211 4326 376479.5709775466 40903.850047567095 -40.11 0.37 +4674 6211 -41.811 0.037 187079.73865240248 4094.5717675757264 +6211 4326 187079.73865240248 4094.5717675757264 -41.811 0.036999999999999984 +4674 6211 -38.409 0.703 565759.7597789201 77706.82286289851 +6211 4326 565759.7597789201 77706.82286289851 -38.409 0.7029999999999998 +6783 6307 -66.24000000000001 18.095 220463.90309500165 228972.23173933866 +6783 6307 -67.797 17.6675 55307.87163008732 182184.18422951712 +6783 6307 -64.683 18.5225 384813.8289925946 277161.9505647868 +6311 6312 33.425 35.165000000000006 238717.61451774646 392786.404068334 +6312 4326 238717.61451774646 392786.404068334 33.42495856544724 35.16499951283473 +6311 6312 32.322500000000005 34.6475 137890.79606271337 335504.48398566386 +6312 4326 137890.79606271337 335504.48398566386 32.32245945725084 34.64749939314675 +6311 6312 34.527499999999996 35.682500000000005 338273.3339808874 451191.86668683914 +6312 4326 338273.3339808874 451191.86668683914 34.52745769522127 35.68249965428006 +3906 6316 21.005000000000003 44.019999999999996 7500400.807697359 4875064.013037513 +6316 4326 7500400.807697359 4875064.013037513 20.999587475249395 44.02017756049606 +3906 6316 19.6505 42.067 7388333.810765585 4659024.918193451 +6316 4326 7388333.810765585 4659024.918193451 19.64541901618809 42.06734079381771 +3906 6316 22.3595 45.973 7605338.942345403 5092957.110269407 +6316 4326 7605338.942345403 5092957.110269407 22.353730005562735 45.97301864138949 +6318 6328 170.82999999999998 52.644999999999996 488497.8856865507 5832794.455588982 +6328 4326 488497.8856865507 5832794.455588982 170.82999999999998 52.644999999999996 +6318 6328 167.96800000000002 49.3735 279910.38483448746 5473399.051220152 +6328 4326 279910.38483448746 5473399.051220152 167.96800000000002 49.3735 +6318 6328 173.69199999999998 55.9165 668232.9652947485 6200060.567462479 +6328 4326 668232.9652947485 6200060.567462479 173.69199999999998 55.91649999999999 +6318 6329 177.0 52.295 500000.00000000175 5793849.77786767 +6329 4326 500000.00000000175 5793849.77786767 177.0 52.295 +6318 6329 174.3 48.3575 299996.1712407819 5359559.00128083 +6329 4326 299996.1712407819 5359559.00128083 174.3 48.3575 +6318 6329 179.7 56.2325 667357.1124991814 6235235.713225893 +6329 4326 667357.1124991814 6235235.713225893 179.7 56.232499999999995 +6318 6330 -176.995 55.545 500315.49276570586 6155441.215371645 +6330 4326 500315.49276570586 6155441.215371645 -176.995 55.544999999999995 +6318 6330 -179.6995 48.6465 301167.493751609 5391677.341764835 +6330 4326 301167.493751609 5391677.341764835 -179.6995 48.646499999999996 +6318 6330 -174.2905 62.4435 639819.2265721997 6926519.820266791 +6330 4326 639819.2265721997 6926519.820266791 -174.2905 62.44350000000002 +6318 6331 -170.995 60.855 500271.661002635 6746636.777194004 +6331 4326 500271.661002635 6746636.777194004 -170.995 60.85499999999999 +6318 6331 -173.6995 49.87949999999999 306063.71185274015 5528727.910589498 +6331 4326 306063.71185274015 5528727.910589498 -173.6995 49.87949999999999 +6318 6331 -168.2905 71.8305 594273.4174823904 7972144.773295547 +6331 4326 594273.4174823904 7972144.773295547 -168.2905 71.83049999999999 +6318 6332 -164.995 61.905 500262.70150924154 6863597.136707529 +6332 4326 500262.70150924154 6863597.136707529 -164.995 61.905 +6318 6332 -167.6995 50.758500000000005 309609.5523102755 5626444.5756313205 +6332 4326 309609.5523102755 5626444.5756313205 -167.6995 50.7585 +6318 6332 -162.2905 73.0515 588133.8862965786 8108218.624054418 +6332 4326 588133.8862965786 8108218.624054418 -162.2905 73.05149999999999 +6318 6333 -158.995 62.845 500254.6046334522 6968319.769418479 +6333 4326 500254.6046334522 6968319.769418479 -158.995 62.845 +6318 6333 -161.6995 52.1665 315383.0576533729 5782993.124320009 +6333 4326 315383.0576533729 5782993.124320009 -161.6995 52.16649999999999 +6318 6333 -156.2905 73.52349999999998 585749.5068505375 8160823.92657466 +6333 4326 585749.5068505375 8160823.92657466 -156.2905 73.52349999999998 +6318 6334 -152.995 63.42999999999999 500249.53038246953 7033500.029426013 +6334 4326 500249.53038246953 7033500.029426013 -152.995 63.43 +6318 6334 -155.6995 53.278 320020.29264672974 5906596.009073531 +6334 4326 320020.29264672974 5906596.009073531 -155.6995 53.278 +6318 6334 -150.2905 73.582 585453.5709085178 8167343.993562271 +6334 4326 585453.5709085178 8167343.993562271 -150.2905 73.582 +6318 6335 -146.995 64.09 500243.7739232476 7107043.182421333 +6335 4326 500243.7739232476 7107043.182421333 -146.995 64.09 +6318 6335 -149.6995 55.053999999999995 327570.85274766205 6104131.138473459 +6335 4326 327570.85274766205 6104131.138473459 -149.6995 55.05399999999999 +6318 6335 -144.2905 73.12599999999999 587757.9371418224 8116521.666334169 +6335 4326 587757.9371418224 8116521.666334169 -144.2905 73.12599999999999 +6318 6336 -140.995 63.53 500248.6603325604 7044642.494469673 +6336 4326 500248.6603325604 7044642.494469673 -140.995 63.53000000000001 +6318 6336 -143.6995 54.476 325094.82230164204 6039838.232701388 +6336 4326 325094.82230164204 6039838.232701388 -143.6995 54.47599999999999 +6318 6336 -138.2905 72.584 590489.5558254714 8056116.729180485 +6336 4326 590489.5558254714 8056116.729180485 -138.2905 72.58400000000002 +6318 6337 -134.995 63.32000000000001 500250.4865463568 7021243.499326773 +6337 4326 500250.4865463568 7021243.499326773 -134.995 63.32000000000001 +6318 6337 -137.6995 54.572 325504.829562229 6050516.306136905 +6337 4326 325504.829562229 6050516.306136905 -137.6995 54.571999999999996 +6318 6337 -132.2905 72.06800000000001 593082.5128518728 7998611.830386628 +6337 4326 593082.5128518728 7998611.830386628 -132.2905 72.06800000000001 +6318 6338 -129.0 46.11 500000.0 5106269.364479603 +6338 4326 500000.0 5106269.364479603 -129.0 46.11 +6318 6338 -131.7 36.453 258028.1796662494 4037583.6488569877 +6338 4326 258028.1796662494 4037583.6488569877 -131.7 36.45299999999999 +6318 6338 -126.3 55.767 669381.8037290876 6183448.368927643 +6338 4326 669381.8037290876 6183448.368927643 -126.30000000000001 55.766999999999996 +6318 6339 -122.995 39.815 500427.9480454584 4407224.36870956 +6339 4326 500427.9480454584 4407224.36870956 -122.995 39.81499999999999 +6318 6339 -125.6995 31.4675 243511.68383341894 3484570.1475524465 +6339 4326 243511.68383341894 3484570.1475524465 -125.6995 31.4675 +6318 6339 -120.2905 48.1625 701472.7717525429 5337912.152125345 +6339 4326 701472.7717525429 5337912.152125345 -120.2905 48.162499999999994 +6318 6340 -117.0 39.945 500000.0 4421652.785645828 +6340 4326 500000.0 4421652.785645828 -117.0 39.94499999999999 +6318 6340 -119.7 31.7865 244338.95886527732 3519945.785868588 +6340 4326 244338.95886527732 3519945.785868588 -119.7 31.786499999999997 +6318 6340 -114.3 48.1035 700996.7036172554 5331330.322624897 +6340 4326 700996.7036172554 5331330.322624897 -114.3 48.10349999999999 +6318 6341 -111.0 40.17 500000.0 4446625.833486382 +6341 4326 500000.0 4446625.833486382 -111.0 40.17 +6318 6341 -113.7 32.214 245523.7262897845 3567354.175285182 +6341 4326 245523.7262897845 3567354.175285182 -113.7 32.214 +6318 6341 -108.3 48.126 700908.9122900425 5333830.855137835 +6341 4326 700908.9122900425 5333830.855137835 -108.3 48.12599999999999 +6318 6342 -105.0 38.995 499999.9999999989 4316221.727961586 +6342 4326 499999.9999999989 4316221.727961586 -105.00000000000001 38.99499999999999 +6318 6342 -107.7 29.9815 239494.3482462007 3319804.134164058 +6342 4326 239494.3482462007 3319804.134164058 -107.7 29.981499999999993 +6318 6342 -102.3 48.0085 701367.0340646396 5320772.6031839885 +6342 4326 701367.0340646396 5320772.6031839885 -102.3 48.00849999999999 +6318 6343 -99.0 37.42 500000.0 4141465.9905510056 +6343 4326 500000.0 4141465.9905510056 -99.0 37.42 +6318 6343 -101.7 26.988999999999997 232034.19468521152 2988083.604299114 +6343 4326 232034.19468521152 2988083.604299114 -101.7 26.988999999999994 +6318 6343 -96.3 47.851 701979.7750945212 5303269.316841059 +6343 4326 701979.7750945212 5303269.316841059 -96.3 47.851 +6318 6344 -93.005 37.495000000000005 499558.0201628704 4149786.64043638 +6344 4326 499558.0201628704 4149786.64043638 -93.005 37.495000000000005 +6318 6344 -95.7095 26.7985 230638.96187599917 2966990.4779571556 +6344 4326 230638.96187599917 2966990.4779571556 -95.7095 26.798499999999997 +6318 6344 -90.3005 48.191500000000005 700616.009579512 5341108.920825521 +6344 4326 700616.009579512 5341108.920825521 -90.3005 48.1915 +6318 6345 -87.0 36.144999999999996 500000.00000000116 4000031.267611221 +6345 4326 500000.00000000116 4000031.267611221 -87.0 36.14499999999998 +6318 6345 -89.7 25.1875 227894.03767243755 2788439.5189798116 +6345 4326 227894.03767243755 2788439.5189798116 -89.7 25.187499999999996 +6318 6345 -84.3 47.1025 704870.6841148983 5220092.172714736 +6345 4326 704870.6841148983 5220092.172714736 -84.3 47.102499999999985 +6318 6346 -81.0 34.97 500000.0 3869716.186715929 +6346 4326 500000.0 3869716.186715929 -81.0 34.97 +6318 6346 -83.7 24.926 227315.29907310876 2759463.007309322 +6346 4326 227315.29907310876 2759463.007309322 -83.7 24.92599999999999 +6318 6346 -78.3 45.014 712749.5387233787 4988052.390542265 +6346 4326 712749.5387233787 4988052.390542265 -78.3 45.01400000000001 +6318 6347 -75.0 36.655 500000.0 4056601.5122795035 +6347 4326 500000.0 4056601.5122795035 -75.0 36.655 +6318 6347 -77.7 29.1175 237266.58570995316 3224017.4637190658 +6347 4326 237266.58570995316 3224017.4637190658 -77.7 29.117500000000003 +6318 6347 -72.3 44.1925 715771.344711273 4896798.997111282 +6347 4326 715771.344711273 4896798.997111282 -72.3 44.192499999999995 +6318 6348 -68.995 40.54 500423.4175416514 4487694.7526666885 +6348 4326 500423.4175416514 4487694.7526666885 -68.995 40.53999999999999 +6318 6348 -71.6995 34.303 251561.85420346117 3799052.6504672226 +6348 4326 251561.85420346117 3799052.6504672226 -71.6995 34.303000000000004 +6318 6348 -66.2905 46.777 706842.1012096816 5183948.567322707 +6348 4326 706842.1012096816 5183948.567322707 -66.2905 46.776999999999994 +6318 6350 -95.85 36.895 13242.64920946045 1539001.754247737 +6350 4326 13242.64920946045 1539001.754247737 -95.85 36.894999999999996 +6318 6350 -121.896 25.6585 -2594100.0835105157 645903.0610183614 +6350 4326 -2594100.0835105157 645903.0610183614 -121.89600000000002 25.658499999999997 +6318 6350 -69.804 48.1315 1942305.234662188 3062066.943272104 +6350 4326 1942305.234662188 3062066.943272104 -69.804 48.131499999999996 +6318 6351 -163.0 72.005 29500000.0 5497768.190454762 +6351 4326 29500000.0 5497768.190454762 -163.0 72.005 +6318 6351 -172.0 69.7505 29153392.372868273 5272211.126975045 +6351 4326 29153392.372868273 5272211.126975045 -172.0 69.75049999999999 +6318 6351 -154.0 74.2595 29771723.748701595 5769663.926273582 +6351 4326 29771723.748701595 5769663.926273582 -154.0 74.2595 +6318 6352 -147.0 72.005 31500000.0 5497768.190454762 +6352 4326 31500000.0 5497768.190454762 -147.0 72.005 +6318 6352 -156.0 69.7505 31153392.372868273 5272211.126975045 +6352 4326 31153392.372868273 5272211.126975045 -155.99999999999997 69.75049999999999 +6318 6352 -138.0 74.2595 31771723.748701595 5769663.926273582 +6352 4326 31771723.748701595 5769663.926273582 -138.0 74.2595 +6318 6353 -165.0 68.66499999999999 14500000.0 6497494.028068438 +6353 4326 14500000.0 6497494.028068438 -165.0 68.66499999999999 +6318 6353 -173.1 66.4105 14139064.994401632 6269895.137831682 +6353 4326 14139064.994401632 6269895.137831682 -173.1 66.4105 +6318 6353 -156.9 70.9195 14794896.542878013 6768410.22582616 +6353 4326 14794896.542878013 6768410.22582616 -156.9 70.91950000000001 +6318 6354 -147.0 68.66499999999999 16500000.0 6497494.028068438 +6354 4326 16500000.0 6497494.028068438 -147.0 68.66499999999999 +6318 6354 -155.1 66.4105 16139064.994401632 6269895.137831682 +6354 4326 16139064.994401632 6269895.137831682 -155.1 66.4105 +6318 6354 -138.9 70.9195 16794896.542878013 6768410.22582616 +6354 4326 16794896.542878013 6768410.22582616 -138.9 70.91950000000001 +6318 6355 -85.84 32.995 199376.9683514333 276640.7418256493 +6355 4326 199376.9683514333 276640.7418256493 -85.84 32.995000000000005 +6318 6355 -86.69500000000001 31.1905 117873.04652490633 76870.30876444188 +6355 4326 117873.04652490633 76870.30876444188 -86.69500000000001 31.1905 +6318 6355 -84.985 34.7995 277629.11731132085 477116.87613607576 +6355 4326 277629.11731132085 477116.87613607576 -84.985 34.7995 +6318 6356 -87.39 32.58 610327.5962043395 286042.60682333034 +6356 4326 610327.5962043395 286042.60682333034 -87.39 32.580000000000005 +6318 6356 -88.37100000000001 30.384 516289.87988403835 42887.583717217465 +6356 4326 516289.87988403835 42887.583717217465 -88.37100000000001 30.383999999999997 +6318 6356 -86.40899999999999 34.776 699861.512213123 530136.2878085349 +6356 4326 699861.512213123 530136.2878085349 -86.40899999999999 34.776 +4483 6362 -103.41499999999999 22.409999999999997 2355075.698164465 1154999.025211383 +6362 4326 2355075.698164465 1154999.025211383 -103.41499999999999 22.409999999999986 +4483 6362 -120.3125 13.131 498642.88048104546 254629.39903276745 +6362 4326 498642.88048104546 254629.39903276745 -120.31250000000001 13.131000000000014 +4483 6362 -86.51750000000001 31.688999999999993 3972159.5071823364 2259465.044877162 +6362 4326 3972159.5071823364 2259465.044877162 -86.51750000000001 31.689000000000036 +6365 6366 -118.095 23.865000000000002 388505.9026551563 2639711.37001155 +6366 4326 388505.9026551563 2639711.37001155 -118.09500000000001 23.865000000000002 +6365 6366 -121.7805 15.8955 -12246.927675235318 1763238.8948801474 +6366 4326 -12246.927675235318 1763238.8948801474 -121.7805 15.895499999999997 +6365 6366 -114.4095 31.8345 745162.3921794384 3525016.083807699 +6366 4326 745162.3921794384 3525016.083807699 -114.4095 31.834500000000006 +6365 6367 -111.0 23.68 500000.0 2618798.8219067925 +6367 4326 500000.0 2618798.8219067925 -111.0 23.68 +6365 6367 -113.7 15.949 210958.14795963367 1765166.42438099 +6367 4326 210958.14795963367 1765166.42438099 -113.7 15.948999999999996 +6365 6367 -108.3 31.411 756689.9510171973 3478306.0767350667 +6367 4326 756689.9510171973 3478306.0767350667 -108.3 31.41099999999999 +6365 6368 -105.0 22.92 499999.9999999987 2534663.831269058 +6368 4326 499999.9999999987 2534663.831269058 -105.00000000000001 22.92000000000001 +6365 6368 -107.7 14.937000000000001 209551.16850779124 1653122.8520861657 +6368 4326 209551.16850779124 1653122.8520861657 -107.7 14.937000000000001 +6365 6368 -102.3 30.903 758064.3626219081 3421976.282493674 +6368 4326 758064.3626219081 3421976.282493674 -102.3 30.90299999999999 +6365 6369 -99.0 21.055 500000.0 2328234.6586782136 +6369 4326 500000.0 2328234.6586782136 -99.0 21.054999999999996 +6365 6369 -101.7 13.175500000000001 207317.68646860588 1458116.4671444155 +6369 4326 207317.68646860588 1458116.4671444155 -101.7 13.1755 +6365 6369 -96.3 28.934499999999996 763197.6182969778 3203730.5537319253 +6369 4326 763197.6182969778 3203730.5537319253 -96.3 28.934499999999996 +6365 6370 -93.0 19.05 499999.99999999866 2106359.8563538482 +6370 4326 499999.99999999866 2106359.8563538482 -93.0 19.049999999999997 +6365 6370 -95.7 12.795 206871.34917723015 1415996.1330994398 +6370 4326 206871.34917723015 1415996.1330994398 -95.7 12.795000000000002 +6365 6370 -90.3 25.305 771844.0736431255 2801459.816646112 +6370 4326 771844.0736431255 2801459.816646112 -90.3 25.304999999999996 +6365 6371 -87.32 21.79 466920.794456622 2409617.4884034996 +6371 4326 466920.794456622 2409617.4884034996 -87.32000000000001 21.789999999999996 +6365 6371 -89.732 18.208 211035.57419919752 2015351.931870287 +6371 4326 211035.57419919752 2015351.931870287 -89.732 18.208 +6365 6371 -84.908 25.372 710492.8715466689 2807786.909432056 +6371 4326 710492.8715466689 2807786.909432056 -84.908 25.372000000000003 +6365 6372 -103.41499999999999 22.409999999999997 2355075.698164465 1154999.025211383 +6372 4326 2355075.698164465 1154999.025211383 -103.41499999999999 22.409999999999986 +6365 6372 -120.3125 13.131 498642.88048104546 254629.39903276745 +6372 4326 498642.88048104546 254629.39903276745 -120.31250000000001 13.131000000000014 +6365 6372 -86.51750000000001 31.688999999999993 3972159.5071823364 2259465.044877162 +6372 4326 3972159.5071823364 2259465.044877162 -86.51750000000001 31.689000000000036 +5561 6381 22.325 48.61 397707.87239004456 5387199.756018947 +6381 4326 397707.87239004456 5387199.756018947 22.32335851144811 48.60973256159191 +5561 6381 22.167499999999997 48.277 386658.4471994536 5349981.235207608 +6381 4326 386658.4471994536 5349981.235207608 22.165868332930472 48.2767265486758 +5561 6381 22.4825 48.943 408601.0754257401 5424443.688337173 +6381 4326 408601.0754257401 5424443.688337173 22.48084851615173 48.94273860215402 +5561 6382 24.0 49.835 300000.0 5522591.621526032 +6382 4326 300000.0 5522591.621526032 23.998327799765523 49.83477075440412 +5561 6382 22.65 47.9225 199104.2352935137 5310786.224979882 +6382 4326 199104.2352935137 5310786.224979882 22.648382209296155 47.922229347655616 +5561 6382 25.35 51.7475 393235.59826039826 5736212.523779124 +6382 4326 393235.59826039826 5736212.523779124 25.348267929644482 51.74731330056201 +5561 6383 27.0 48.599999999999994 300000.0 5385240.056317545 +6383 4326 300000.0 5385240.056317545 26.998390549607553 48.59979846399192 +5561 6383 25.65 45.594 194660.7591570768 5051935.7977409465 +6383 4326 194660.7591570768 5051935.7977409465 25.648468960036624 45.59374344476348 +5561 6383 28.35 51.605999999999995 393526.65533816663 5720469.801373365 +6383 4326 393526.65533816663 5720469.801373365 28.34829843384321 51.60585539729059 +5561 6384 30.0 47.769999999999996 300000.0 5292947.5264772875 +6384 4326 300000.0 5292947.5264772875 29.99844191911134 47.76982928449684 +5561 6384 28.65 43.855000000000004 191456.05549648078 4858683.0751191275 +6384 4326 191456.05549648078 4858683.0751191275 28.648536200707483 43.854761496738796 +5561 6384 31.35 51.684999999999995 393364.2279106922 5729258.980057842 +6384 4326 393364.2279106922 5729258.980057842 31.348325304478056 51.684899558860494 +5561 6385 33.0 47.78 300000.0 5294059.404939604 +6385 4326 300000.0 5294059.404939604 32.998471489016836 47.77986976347204 +5561 6385 31.65 43.64 191066.77168015047 4834794.290633724 +6385 4326 191066.77168015047 4834794.290633724 31.648567395520864 43.6397967478503 +5561 6385 34.35 51.92 392879.999887649 5755404.668880226 +6385 4326 392879.999887649 5755404.668880226 34.348351174907535 51.91994515746113 +5561 6386 36.0 47.245000000000005 300000.0 5234576.642089423 +6386 4326 300000.0 5234576.642089423 35.998520659470415 47.24490166678952 +5561 6386 34.65 43.6405 191067.67520383763 4834849.84495799 +6386 4326 191067.67520383763 4834849.84495799 34.64859727542083 43.640333966913325 +5561 6386 37.35 50.8495 395072.92148093076 5636310.7714564605 +6386 4326 395072.92148093076 5636310.7714564605 37.348427418791005 50.84947121741212 +5561 6387 38.84 48.58 288194.1968715136 5383028.343622829 +6387 4326 288194.1968715136 5383028.343622829 38.83851869532708 48.57995764968446 +5561 6387 37.634 46.951 196011.48510449557 5202797.243089944 +6387 4326 196011.48510449557 5202797.243089944 37.6325487448017 46.950918339874086 +5561 6387 40.046 50.209 374668.6451897094 5564715.901444403 +6387 4326 374668.6451897094 5564715.901444403 40.044486080428975 50.20899751984718 +6135 6391 -81.16 19.13 2745191.7543389983 1826508.716889636 +6391 4326 2745191.7543389983 1826508.716889636 -81.16 19.129999999999992 +6135 6391 -83.356 17.735 1978971.2866894912 1327369.4368585942 +6391 4326 1978971.2866894912 1327369.4368585942 -83.356 17.73500000000003 +6135 6391 -78.964 20.525 3498470.68606855 2335393.075759998 +6391 4326 3498470.68606855 2335393.075759998 -78.964 20.525000000000013 +6318 6393 -158.78500000000003 61.35 -254894.82358153717 1272467.9326409101 +6393 4326 -254894.82358153717 1272467.9326409101 -158.78500000000003 61.35 +6318 6393 175.2995 52.305 -2028925.2498783472 732268.5502162899 +6393 4326 -2028925.2498783472 732268.5502162899 175.29950000000005 52.304999999999986 +6318 6393 -132.86950000000002 70.39500000000001 793326.4022635117 2395398.489500744 +6393 4326 793326.4022635117 2395398.489500744 -132.86950000000002 70.395 +6318 6394 -135.495 57.480000000000004 709022.2275555171 630020.6004242897 +6394 4326 709022.2275555171 630020.6004242897 -135.49500000000003 57.47999999999999 +6318 6394 -140.4495 54.897 383709.09014116693 362275.91215463355 +6394 4326 383709.09014116693 362275.91215463355 -140.4495 54.89700000000002 +6318 6394 -130.5405 60.063 992788.9149156073 920483.9307453353 +6394 4326 992788.9149156073 920483.9307453353 -130.54049999999998 60.06300000000001 +6318 6395 -142.495 64.94 476598.2733494 1218703.2590226599 +6395 4326 476598.2733494 1218703.2590226599 -142.495 64.94 +6318 6395 -143.8585 60.242 397073.46798556915 696498.9178886266 +6395 4326 397073.46798556915 696498.9178886266 -143.8585 60.242 +6318 6395 -141.1315 69.63799999999999 533735.232831844 1742745.5220906406 +6395 4326 533735.232831844 1742745.5220906406 -141.1315 69.63799999999999 +6318 6396 -146.0 65.05 500000.0 1230874.6384843953 +6396 4326 500000.0 1230874.6384843953 -146.0 65.05 +6318 6396 -147.8 60.253 400346.16334001743 697634.2588449224 +6396 4326 400346.16334001743 697634.2588449224 -147.8 60.252999999999986 +6318 6396 -144.2 69.847 569223.8152984743 1766840.1544494042 +6396 4326 569223.8152984743 1766840.1544494042 -144.2 69.847 +6318 6397 -150.0 64.87 500000.0 1210808.0839250025 +6397 4326 500000.0 1210808.0839250025 -150.0 64.87 +6318 6397 -151.809 59.686 398121.27172606904 634499.4378558247 +6397 4326 398121.27172606904 634499.4378558247 -151.80900000000003 59.686 +6318 6397 -148.191 70.054 568885.014310113 1789932.4850946558 +6397 4326 568885.014310113 1789932.4850946558 -148.191 70.054 +6318 6398 -153.93 63.5 503485.94536649715 1058097.4330165344 +6398 4326 503485.94536649715 1058097.4330165344 -153.93 63.50000000000001 +6318 6398 -155.793 56.498 389590.6245938524 279510.3993634731 +6398 4326 389590.6245938524 279510.3993634731 -155.793 56.49799999999999 +6318 6398 -152.067 70.502 572018.4257739565 1840031.82643212 +6398 4326 572018.4257739565 1840031.82643212 -152.067 70.502 +6318 6399 -157.995 63.145 500252.0814158846 1018528.7945634346 +6399 4326 500252.0814158846 1018528.7945634346 -157.99500000000003 63.145 +6318 6399 -159.7995 55.7155 386918.9132892374 192419.31067588035 +6399 4326 386918.9132892374 192419.31067588035 -159.7995 55.7155 +6318 6399 -156.19050000000001 70.5745 567177.5517000024 1847974.9441158164 +6399 4326 567177.5517000024 1847974.9441158164 -156.19050000000001 70.5745 +6318 6400 -162.005 62.53 499742.59696889075 949988.3243515419 +6400 4326 499742.59696889075 949988.3243515419 -162.005 62.53 +6318 6400 -163.80949999999999 55.141 384627.2292582616 128493.51815071332 +6400 4326 384627.2292582616 128493.51815071332 -163.80949999999999 55.14099999999999 +6318 6400 -160.2005 69.919 568967.7430237273 1774868.1191782567 +6400 4326 568967.7430237273 1774868.1191782567 -160.2005 69.91900000000001 +6318 6401 -166.13 61.695 493120.92207579594 856945.9191454424 +6401 4326 493120.92207579594 856945.9191454424 -166.13 61.69499999999999 +6318 6401 -168.047 55.075500000000005 369272.75018676487 121622.30920951095 +6401 4326 369272.75018676487 121622.30920951095 -168.047 55.07550000000001 +6318 6401 -164.213 68.3145 573703.4914816638 1595953.9075491133 +6401 4326 573703.4914816638 1595953.9075491133 -164.213 68.3145 +6318 6402 -170.57999999999998 61.155 468774.10340198886 796907.9866779154 +6402 4326 468774.10340198886 796907.9866779154 -170.57999999999998 61.155 +6318 6402 -172.902 56.9565 323481.636023819 332869.3282945642 +6402 4326 323481.636023819 332869.3282945642 -172.902 56.9565 +6318 6402 -168.258 65.3535 581075.9216982052 1265830.5148130064 +6402 4326 581075.9216982052 1265830.5148130064 -168.258 65.3535 +6318 6403 -176.21 52.82 985844.7880543226 202527.91516819393 +6403 4326 985844.7880543226 202527.91516819393 -176.21 52.82 +6318 6403 173.557 51.452 276522.2880442635 102933.70544811105 +6403 4326 276522.2880442635 102933.70544811105 173.557 51.452 +6318 6403 -165.977 54.188 1652267.2955383519 400290.4365906697 +6403 4326 1652267.2955383519 400290.4365906697 -165.977 54.188 +6318 6404 -111.895 34.17 215357.474309584 351509.2431937203 +6404 4326 215357.474309584 351509.2431937203 -111.895 34.17 +6318 6404 -113.2045 31.613999999999997 91169.08245612771 68790.21379401149 +6404 4326 91169.08245612771 68790.21379401149 -113.2045 31.613999999999994 +6318 6404 -110.5855 36.726 332264.3607156152 635892.0351637731 +6404 4326 332264.3607156152 635892.0351637731 -110.5855 36.726000000000006 +6318 6405 -111.895 34.17 706553.3934041469 1153245.5485358275 +6405 4326 706553.3934041469 1153245.5485358275 -111.895 34.17 +6318 6405 -113.2045 31.613999999999997 299111.1629138048 225689.67780187496 +6405 4326 299111.1629138048 225689.67780187496 -113.2045 31.613999999999994 +6318 6405 -110.5855 36.726 1090106.1703268215 2086259.9578863946 +6405 4326 1090106.1703268215 2086259.9578863946 -110.5855 36.726000000000006 +6318 6406 -110.375 34.17 194153.50067005004 351528.6430514126 +6406 4326 194153.50067005004 351528.6430514126 -110.375 34.17 +6318 6406 -111.5765 31.613999999999997 79592.58561085619 68933.09865045993 +6406 4326 79592.58561085619 68933.09865045993 -111.5765 31.613999999999994 +6318 6406 -109.1735 36.726 302072.0270641723 635525.7685795034 +6406 4326 302072.0270641723 635525.7685795034 -109.1735 36.72599999999999 +6318 6407 -110.375 34.17 636986.5507547572 1153309.1963629022 +6407 4326 636986.5507547572 1153309.1963629022 -110.375 34.17 +6318 6407 -111.5765 31.613999999999997 261130.53021934442 226158.4601393042 +6407 4326 261130.53021934442 226158.4601393042 -111.5765 31.613999999999994 +6318 6407 -109.1735 36.726 991049.9575596204 2085058.2958645122 +6407 4326 991049.9575596204 2085058.2958645122 -109.1735 36.72599999999999 +6318 6408 -113.66499999999999 34.525 221163.54809494727 390901.08565793367 +6408 4326 221163.54809494727 390901.08565793367 -113.66499999999999 34.525 +6318 6408 -114.6955 32.2975 124310.60752930673 144250.73145847756 +6408 4326 124310.60752930673 144250.73145847756 -114.69550000000001 32.2975 +6318 6408 -112.6345 36.7525 312968.5783435126 638607.9599893158 +6408 4326 312968.5783435126 638607.9599893158 -112.6345 36.7525 +6318 6409 -113.66499999999999 34.525 725602.1919125565 1282483.876830491 +6409 4326 725602.1919125565 1282483.876830491 -113.66499999999999 34.525 +6318 6409 -114.6955 32.2975 407843.2005554682 473263.5546537977 +6409 4326 407843.2005554682 473263.5546537977 -114.69550000000001 32.2975 +6318 6409 -112.6345 36.7525 1026799.7977149363 2095170.4724058916 +6409 4326 1026799.7977149363 2095170.4724058916 -112.6345 36.7525 +6318 6410 -92.13 35.585 388218.3760538642 138869.51677349143 +6410 4326 388218.3760538642 138869.51677349143 -92.13 35.58500000000002 +6318 6410 -94.37100000000001 34.7615 182942.28431090515 50115.795887343964 +6410 4326 182942.28431090515 50115.795887343964 -94.37100000000001 34.7615 +6318 6410 -89.889 36.408500000000004 589342.0332958896 232263.49768251824 +6410 4326 589342.0332958896 232263.49768251824 -89.889 36.40850000000002 +6318 6411 -92.13 35.585 1273679.7887367196 455607.7396143632 +6411 4326 1273679.7887367196 455607.7396143632 -92.13 35.58500000000002 +6318 6411 -94.37100000000001 34.7615 600203.1444100282 164421.57367372766 +6411 4326 600203.1444100282 164421.57367372766 -94.37100000000001 34.7615 +6318 6411 -89.889 36.408500000000004 1933532.987538265 762017.8253133954 +6411 4326 1933532.987538265 762017.8253133954 -89.889 36.40850000000002 +6318 6412 -92.44 34.055 359380.3456728528 554070.3461636706 +6412 4326 359380.3456728528 554070.3461636706 -92.44 34.055 +6318 6412 -94.27600000000001 33.1145 187583.30831943935 452033.15065267665 +6412 4326 187583.30831943935 452033.15065267665 -94.27600000000001 33.1145 +6318 6412 -90.604 34.9955 527448.8375440208 659180.0131279876 +6412 4326 527448.8375440208 659180.0131279876 -90.604 34.9955 +6318 6413 -92.44 34.055 1179067.0173950184 1817812.4606719767 +6413 4326 1179067.0173950184 1817812.4606719767 -92.44 34.055 +6318 6413 -94.27600000000001 33.1145 615429.5706780275 1483045.428399657 +6413 4326 615429.5706780275 1483045.428399657 -94.27600000000001 33.1145 +6318 6413 -90.604 34.9955 1730471.727809009 2162659.7597040734 +6413 4326 1730471.727809009 2162659.7597040734 -90.604 34.9955 +6318 6414 -119.285 37.269999999999996 63315.465344822995 -82731.13159650704 +6414 4326 63315.465344822995 -82731.13159650704 -119.28500000000001 37.27 +6318 6414 -123.93350000000001 33.004 -367870.9056195842 -548947.5140136816 +6414 4326 -367870.9056195842 -548947.5140136816 -123.93350000000002 33.004 +6318 6414 -114.63650000000001 41.536 447908.0593044227 403648.150371247 +6414 4326 447908.0593044227 403648.150371247 -114.63650000000003 41.536 +6318 6415 -122.22 40.8 1981436.393996671 662877.7471431005 +6415 4326 1981436.393996671 662877.7471431005 -122.22 40.8 +6318 6415 -124.227 39.711000000000006 1809032.8706182928 544364.0613099996 +6415 4326 1809032.8706182928 544364.0613099996 -124.227 39.71100000000001 +6318 6415 -120.213 41.888999999999996 2148310.5570726353 785305.6470252918 +6415 4326 2148310.5570726353 785305.6470252918 -120.21300000000001 41.88899999999998 +6318 6416 -122.22 40.8 6500762.569637412 2174791.409085322 +6416 4326 6500762.569637412 2174791.409085322 -122.22 40.8 +6318 6416 -124.227 39.711000000000006 5935135.343353516 1785967.7581478895 +6416 4326 5935135.343353516 1785967.7581478895 -124.227 39.71100000000001 +6318 6416 -120.213 41.888999999999996 7048248.886329139 2576456.9439488114 +6416 4326 7048248.886329139 2576456.9439488114 -120.21300000000001 41.88899999999998 +6318 6417 -121.80000000000001 39.09 2017301.8204829379 658016.692001398 +6417 4326 2017301.8204829379 658016.692001398 -121.80000000000001 39.089999999999996 +6318 6417 -123.834 38.127 1839195.8283859724 552723.7703899188 +6417 4326 1839195.8283859724 552723.7703899188 -123.834 38.127 +6318 6417 -119.766 40.053 2190614.0811918867 767255.0743562812 +6417 4326 2190614.0811918867 767255.0743562812 -119.76600000000002 40.053000000000004 +6318 6418 -121.80000000000001 39.09 6618431.056367773 2158843.097341253 +6418 4326 6618431.056367773 2158843.097341253 -121.80000000000001 39.089999999999996 +6318 6418 -123.834 38.127 6034094.980629644 1813394.5703542582 +6418 4326 6034094.980629644 1813394.5703542582 -123.834 38.127 +6318 6418 -119.766 40.053 7187039.698377048 2517236.023450566 +6418 4326 7187039.698377048 2517236.023450566 -119.76600000000002 40.053000000000004 +6318 6419 -120.425 37.72 2006611.9340582322 635399.2547793326 +6419 4326 2006611.9340582322 635399.2547793326 -120.42499999999998 37.71999999999999 +6318 6419 -122.7605 36.82899999999999 1798347.426047734 538949.0753376134 +6419 4326 1798347.426047734 538949.0753376134 -122.7605 36.82899999999999 +6318 6419 -118.0895 38.611000000000004 2209937.041178584 736997.6950945166 +6419 4326 2209937.041178584 736997.6950945166 -118.08950000000002 38.611 +6318 6420 -120.425 37.72 6583359.320656051 2084639.0553885268 +6420 4326 6583359.320656051 2084639.0553885268 -120.42499999999998 37.71999999999999 +6318 6420 -122.7605 36.82899999999999 5900078.18062494 1768202.0916701525 +6420 4326 5900078.18062494 1768202.0916701525 -122.7605 36.82899999999999 +6318 6420 -118.0895 38.611000000000004 7250435.109600073 2417966.60498926 +6420 4326 7250435.109600073 2417966.60498926 -118.08950000000002 38.611 +6318 6421 -118.815 36.68 2016534.9266023266 649444.111312916 +6421 4326 2016534.9266023266 649444.111312916 -118.81499999999998 36.67999999999997 +6318 6421 -121.6905 35.870000000000005 1757042.2782625472 562953.5650761565 +6421 4326 1757042.2782625472 562953.5650761565 -121.6905 35.87000000000002 +6318 6421 -115.93950000000001 37.489999999999995 2270631.083070669 743632.1654617812 +6421 4326 2270631.083070669 743632.1654617812 -115.93950000000002 37.489999999999995 +6318 6422 -118.815 36.68 6615915.005361133 2130717.8888657917 +6422 4326 6615915.005361133 2130717.8888657917 -118.81499999999998 36.67999999999997 +6318 6422 -121.6905 35.870000000000005 5764562.874933041 1846956.821754023 +6422 4326 5764562.874933041 1846956.821754023 -121.6905 35.87000000000002 +6318 6422 -115.93950000000001 37.489999999999995 7449562.145374354 2439733.1965191932 +6422 4326 7449562.145374354 2439733.1965191932 -115.93950000000002 37.489999999999995 +6318 6423 -117.77000000000001 34.285 2021176.3034016618 587100.3705963591 +6423 4326 2021176.3034016618 587100.3705963591 -117.77 34.28500000000002 +6318 6423 -121.055 32.9125 1714139.7936607758 439167.22925522097 +6423 4326 1714139.7936607758 439167.22925522097 -121.055 32.91250000000001 +6318 6423 -114.485 35.6575 2318240.215058904 744899.1637714047 +6423 4326 2318240.215058904 744899.1637714047 -114.485 35.6575 +6318 6424 -117.77000000000001 34.285 6631142.589076952 1926178.466198221 +6424 4326 6631142.589076952 1926178.466198221 -117.77 34.28500000000002 +6318 6424 -121.055 32.9125 5623806.9733687285 1440834.4849815036 +6424 4326 5623806.9733687285 1440834.4849815036 -121.055 32.91250000000001 +6318 6424 -114.485 35.6575 7605759.772572422 2443890.0068066833 +6424 4326 7605759.772572422 2443890.0068066833 -114.485 35.6575 +6318 6425 -116.285 33.305 1996740.5868549028 626244.559176123 +6425 4326 1996740.5868549028 626244.559176123 -116.285 33.305 +6318 6425 -117.96350000000001 32.6075 1839164.448277042 550211.7107019561 +6425 4326 1839164.448277042 550211.7107019561 -117.96350000000001 32.6075 +6318 6425 -114.6065 34.0025 2151827.0133517752 704802.7808291488 +6425 4326 2151827.0133517752 704802.7808291488 -114.6065 34.0025 +6318 6426 -116.285 33.305 6550973.0757064605 2054604.0248969968 +6426 4326 6550973.0757064605 2054604.0248969968 -116.285 33.305 +6318 6426 -117.96350000000001 32.6075 6033992.027722262 1805152.9211946672 +6426 4326 6033992.027722262 1805152.9211946672 -117.96350000000001 32.6075 +6318 6426 -114.6065 34.0025 7059785.79330495 2312340.457103632 +6426 4326 7059785.79330495 2312340.457103632 -114.6065 34.0025 +6318 6427 -105.55000000000001 39.115 910077.8060260228 447076.0864687894 +6427 4326 910077.8060260228 447076.0864687894 -105.55000000000001 39.115 +6318 6427 -108.709 38.2375 633503.6654187832 354628.51937078615 +6427 4326 633503.6654187832 354628.51937078615 -108.709 38.237500000000004 +6318 6427 -102.391 39.99250000000001 1179883.6778052924 549041.2134201529 +6427 4326 1179883.6778052924 549041.2134201529 -102.391 39.992500000000014 +6318 6428 -105.55000000000001 39.115 2985813.6016209614 1466782.1270270192 +6428 4326 2985813.6016209614 1466782.1270270192 -105.55000000000001 39.115 +6318 6428 -108.709 38.2375 2078419.9419787095 1163477.0673063204 +6428 4326 2078419.9419787095 1163477.0673063204 -108.709 38.237500000000004 +6318 6428 -102.391 39.99250000000001 3871001.6992834485 1801312.7143666178 +6428 4326 3871001.6992834485 1801312.7143666178 -102.391 39.992500000000014 +6318 6429 -105.55000000000001 40.285 910150.1227950495 410466.04419278353 +6429 4326 910150.1227950495 410466.04419278353 -105.55000000000001 40.285 +6318 6429 -108.709 39.6325 638965.6755298494 343001.4062162024 +6429 4326 638965.6755298494 343001.4062162024 -108.709 39.63249999999999 +6318 6429 -102.391 40.9375 1176178.9159974535 487510.83186888543 +6429 4326 1176178.9159974535 487510.83186888543 -102.391 40.93749999999999 +6318 6430 -105.55000000000001 40.285 2986050.860887343 1346670.6799931568 +6430 4326 2986050.860887343 1346670.6799931568 -105.55000000000001 40.285 +6318 6430 -108.709 39.6325 2096339.886818099 1125330.4468983235 +6430 4326 2096339.886818099 1125330.4468983235 -108.709 39.63249999999999 +6318 6430 -102.391 40.9375 3858846.993252231 1599441.7875605011 +6430 4326 3858846.993252231 1599441.7875605011 -102.391 40.93749999999999 +6318 6431 -105.55000000000001 37.83 910000.3279125895 433913.0553515627 +6431 4326 910000.3279125895 433913.0553515627 -105.55000000000001 37.82999999999999 +6318 6431 -108.709 37.065 629052.8991190449 353911.53463970317 +6431 4326 629052.8991190449 353911.53463970317 -108.709 37.06500000000002 +6318 6431 -102.391 38.595 1185210.975475688 523332.2323330514 +6431 4326 1185210.975475688 523332.2323330514 -102.391 38.595000000000006 +6318 6432 -105.55000000000001 37.83 2985559.408843806 1423596.4157699181 +6432 4326 2985559.408843806 1423596.4157699181 -105.55000000000001 37.82999999999999 +6318 6432 -108.709 37.065 2063817.719543651 1161124.7599010922 +6432 4326 2063817.719543651 1161124.7599010922 -108.709 37.06500000000002 +6318 6432 -102.391 38.595 3888479.6750570713 1716965.8322500188 +6432 4326 3888479.6750570713 1716965.8322500188 -102.391 38.595000000000006 +6318 6433 -72.755 41.515 304383.2321191824 228105.00864740624 +6433 4326 304383.2321191824 228105.00864740624 -72.755 41.51499999999999 +6318 6433 -73.63250000000001 41.0335 230588.64706697664 175009.14457170662 +6433 4326 230588.64706697664 175009.14457170662 -73.63250000000001 41.03349999999999 +6318 6433 -71.8775 41.9965 377091.7689576581 281949.1244673686 +6433 4326 377091.7689576581 281949.1244673686 -71.8775 41.9965 +6318 6434 -72.755 41.515 998630.6540483504 748374.5158726999 +6434 4326 998630.6540483504 748374.5158726999 -72.755 41.51499999999999 +6318 6434 -73.63250000000001 41.0335 756522.9195895719 574175.8351510088 +6434 4326 756522.9195895719 574175.8351510088 -73.63250000000001 41.03349999999999 +6318 6434 -71.8775 41.9965 1237175.245325916 925028.0858586932 +6434 4326 1237175.245325916 925028.0858586932 -71.8775 41.9965 +6318 6435 -75.38499999999999 39.144999999999996 202737.5480806781 127103.25313109877 +6435 4326 202737.5480806781 127103.25313109877 -75.38499999999999 39.144999999999996 +6318 6435 -75.7585 38.5105 170185.3710576928 56721.266312237676 +6435 4326 170185.3710576928 56721.266312237676 -75.75850000000001 38.51049999999999 +6318 6435 -75.0115 39.7795 234709.65364230535 197625.8780571235 +6435 4326 234709.65364230535 197625.8780571235 -75.01150000000001 39.7795 +6318 6436 -75.38499999999999 39.144999999999996 665148.1059946908 417004.58964761323 +6436 4326 665148.1059946908 417004.58964761323 -75.38499999999999 39.144999999999996 +6318 6436 -75.7585 38.5105 558349.8385451131 186093.02122606646 +6436 4326 558349.8385451131 186093.02122606646 -75.75850000000001 38.51049999999999 +6318 6436 -75.0115 39.7795 770043.2556581295 648377.5682590795 +6436 4326 770043.2556581295 648377.5682590795 -75.01150000000001 39.7795 +6318 6437 -81.15 27.619999999999997 185195.14759915508 364110.21714043804 +6437 4326 185195.14759915508 364110.21714043804 -81.15 27.62 +6318 6437 -82.212 24.731 77384.67721616034 44587.952464870745 +6437 4326 77384.67721616034 44587.952464870745 -82.212 24.730999999999998 +6318 6437 -80.088 30.508999999999997 287539.61535454553 684644.0779293055 +6437 4326 287539.61535454553 684644.0779293055 -80.088 30.509 +6318 6438 -81.15 27.619999999999997 607594.4137482274 1194584.9374015871 +6438 4326 607594.4137482274 1194584.9374015871 -81.15 27.62 +6318 6438 -82.212 24.731 253886.22883335204 146285.6407118301 +6438 4326 253886.22883335204 146285.6407118301 -82.212 24.730999999999998 +6318 6438 -80.088 30.508999999999997 943369.5550423709 2246203.11233973 +6438 4326 943369.5550423709 2246203.11233973 -80.088 30.509 +6318 6439 -83.8 27.71 419682.8006494569 411606.1169059885 +6439 4326 419682.8006494569 411606.1169059885 -83.8 27.709999999999994 +6318 6439 -87.247 24.740000000000002 71808.96482317086 86317.42506656054 +6439 4326 71808.96482317086 86317.42506656054 -87.247 24.73999999999999 +6318 6439 -80.353 30.68 749112.5376311431 746513.1936236266 +6439 4326 749112.5376311431 746513.1936236266 -80.353 30.679999999999993 +6318 6440 -84.83500000000001 30.11 567714.4986294459 123087.90795582991 +6440 4326 567714.4986294459 123087.90795582991 -84.83500000000002 30.109999999999992 +6318 6440 -87.3505 29.3 323067.84639475704 36716.95956284991 +6440 4326 323067.84639475704 36716.95956284991 -87.35050000000001 29.299999999999983 +6318 6440 -82.3195 30.92 808415.1822636275 214828.78102081481 +6440 4326 808415.1822636275 214828.78102081481 -82.3195 30.920000000000016 +6318 6441 -84.83500000000001 30.11 1862576.6509201073 403830.911351752 +6441 4326 1862576.6509201073 403830.911351752 -84.83500000000002 30.109999999999992 +6318 6441 -87.3505 29.3 1059931.7593801322 120462.22483245008 +6441 4326 1059931.7593801322 120462.22483245008 -87.35050000000001 29.299999999999983 +6318 6441 -82.3195 30.92 2652275.477143252 704817.4257324567 +6441 4326 2652275.477143252 704817.4257324567 -82.3195 30.920000000000016 +6318 6442 -82.235 27.935000000000002 176872.42951769338 399028.39882096706 +6442 4326 176872.42951769338 399028.39882096706 -82.235 27.935 +6318 6442 -83.2295 26.4365 77365.23380374948 233558.28670167812 +6442 4326 77365.23380374948 233558.28670167812 -83.2295 26.4365 +6318 6442 -81.2405 29.433500000000002 273690.74826097145 565316.0981896543 +6442 4326 273690.74826097145 565316.0981896543 -81.2405 29.433500000000006 +6318 6443 -82.235 27.935000000000002 580288.9628426317 1309145.6717984562 +6443 4326 580288.9628426317 1309145.6717984562 -82.235 27.935 +6318 6443 -83.2295 26.4365 253822.43823780073 766265.8122870891 +6443 4326 253822.43823780073 766265.8122870891 -83.2295 26.4365 +6318 6443 -81.2405 29.433500000000002 897933.73025287 1854707.8988105578 +6443 4326 897933.73025287 1854707.8988105578 -81.2405 29.433500000000006 +6318 6444 -82.12 32.519999999999996 204384.1730120967 279375.5698015051 +6444 4326 204384.1730120967 279375.5698015051 -82.12 32.51999999999999 +6318 6444 -83.335 30.576 87935.98120049336 64428.67874660541 +6444 4326 87935.98120049336 64428.67874660541 -83.335 30.576000000000004 +6318 6444 -80.905 34.464 315913.03382685786 495690.7435459547 +6444 4326 315913.03382685786 495690.7435459547 -80.905 34.464000000000006 +6318 6445 -82.12 32.519999999999996 670550.4079571866 916584.6819237713 +6445 4326 670550.4079571866 916584.6819237713 -82.12 32.51999999999999 +6318 6445 -83.335 30.576 288503.2986552846 211379.75685448793 +6445 4326 288503.2986552846 211379.75685448793 -83.335 30.576000000000004 +6318 6445 -80.905 34.464 1036458.0121469488 1626278.7144503533 +6445 4326 1036458.0121469488 1626278.7144503533 -80.905 34.464000000000006 +6318 6446 -84.3 32.815 687514.8760667271 312094.28572832706 +6446 4326 687514.8760667271 312094.28572832706 -84.3 32.815 +6318 6446 -85.479 30.8395 574464.4552982544 93794.36979869824 +6446 4326 574464.4552982544 93794.36979869824 -85.479 30.839500000000005 +6318 6446 -83.121 34.790499999999994 795691.9523103088 531682.9947136147 +6446 4326 795691.9523103088 531682.9947136147 -83.121 34.7905 +6318 6447 -84.3 32.815 2255621.7222289215 1023929.3357603531 +6447 4326 2255621.7222289215 1023929.3357603531 -84.3 32.815 +6318 6447 -85.479 30.8395 1884722.1334243573 307723.6949145625 +6447 4326 1884722.1334243573 307723.6949145625 -85.479 30.839500000000005 +6318 6447 -83.121 34.790499999999994 2610532.679871406 1744363.2918229178 +6447 4326 2610532.679871406 1744363.2918229178 -83.121 34.7905 +6318 6448 -113.985 43.845 501206.1570626486 241974.0498370269 +6448 4326 501206.1570626486 241974.0498370269 -113.985 43.845 +6318 6448 -115.1685 42.1755 403459.76413783856 57175.03314187794 +6448 4326 403459.76413783856 57175.03314187794 -115.16850000000001 42.17550000000001 +6318 6448 -112.8015 45.514500000000005 593643.3516935457 428187.0188976105 +6448 4326 593643.3516935457 428187.0188976105 -112.8015 45.514500000000005 +6318 6449 -113.985 43.845 1644373.8672963725 793876.5285069791 +6449 4326 1644373.8672963725 793876.5285069791 -113.985 43.845 +6318 6449 -115.1685 42.1755 1323684.2431755583 187581.75456631123 +6449 4326 1323684.2431755583 187581.75456631123 -115.16850000000001 42.17550000000001 +6318 6449 -112.8015 45.514500000000005 1947644.896681241 1404810.2444999106 +6449 4326 1947644.896681241 1404810.2444999106 -112.8015 45.514500000000005 +6318 6450 -112.14 43.370000000000005 202161.21936047942 189202.38724812766 +6450 4326 202161.21936047942 189202.38724812766 -112.14 43.37000000000001 +6318 6450 -113.13 42.128 120351.05903192172 51687.362961994455 +6450 4326 120351.05903192172 51687.362961994455 -113.13 42.128 +6318 6450 -111.15 44.612 280695.92876894755 327698.8013920951 +6450 4326 280695.92876894755 327698.8013920951 -111.15 44.611999999999995 +6318 6451 -112.14 43.370000000000005 663257.2675185056 620741.4988298989 +6451 4326 663257.2675185056 620741.4988298989 -112.14 43.37000000000001 +6318 6451 -113.13 42.128 394851.7665072292 169577.62331781015 +6451 4326 394851.7665072292 169577.62331781015 -113.13 42.128 +6318 6451 -111.15 44.612 920916.5599694549 1075125.1509005656 +6451 4326 920916.5599694549 1075125.1509005656 -111.15 44.611999999999995 +6318 6452 -115.78 45.5 797655.41440746 425871.2238890269 +6452 4326 797655.41440746 425871.2238890269 -115.78 45.50000000000001 +6318 6452 -117.094 42.341 689251.4777751659 75770.44093186814 +6452 4326 689251.4777751659 75770.44093186814 -117.09399999999998 42.340999999999994 +6318 6452 -114.466 48.659 894584.9816078962 777836.67337109 +6452 4326 894584.9816078962 777836.67337109 -114.46600000000001 48.65899999999999 +6318 6453 -115.78 45.5 2616974.4724351414 1397212.5070425826 +6453 4326 2616974.4724351414 1397212.5070425826 -115.78 45.50000000000001 +6318 6453 -117.094 42.341 2261319.223667356 248590.18829063742 +6453 4326 2261319.223667356 248590.18829063742 -117.09399999999998 42.340999999999994 +6318 6453 -114.466 48.659 2934984.2274919054 2551952.4858849845 +6453 4326 2934984.2274919054 2551952.4858849845 -114.46600000000001 48.65899999999999 +6318 6454 -88.145 39.78 316133.6048133849 345590.73500594514 +6454 4326 316133.6048133849 345590.73500594514 -88.145 39.78000000000001 +6318 6454 -89.1575 37.332 226960.888561657 74153.85203580958 +6454 4326 226960.888561657 74153.85203580958 -89.1575 37.332 +6318 6454 -87.1325 42.228 399132.2493135867 618125.4239692872 +6454 4326 399132.2493135867 618125.4239692872 -87.1325 42.227999999999994 +6318 6455 -88.145 39.78 1037181.6684585804 1133825.6030986719 +6455 4326 1037181.6684585804 1133825.6030986719 -88.145 39.78000000000001 +6318 6455 -89.1575 37.332 744620.8485560364 243286.42955415198 +6455 4326 744620.8485560364 243286.42955415198 -89.1575 37.332 +6318 6455 -87.1325 42.228 1309486.387956326 2027966.4951392366 +6455 4326 1309486.387956326 2027966.4951392366 -87.1325 42.227999999999994 +6318 6456 -90.22 39.739999999999995 695428.7036032164 341122.5008347094 +6456 4326 695428.7036032164 341122.5008347094 -90.22 39.739999999999995 +6318 6456 -91.39 37.247 591466.4497448911 65101.11165521788 +6456 4326 591466.4497448911 65101.11165521788 -91.39 37.247 +6318 6456 -89.05 42.233 792173.5648998992 618565.43078374 +6456 4326 792173.5648998992 618565.43078374 -89.05 42.233 +6318 6457 -90.22 39.739999999999995 2281585.671704886 1119166.0714885425 +6457 4326 2281585.671704886 1119166.0714885425 -90.22 39.739999999999995 +6318 6457 -91.39 37.247 1940502.8438380305 213585.89715549402 +6457 4326 1940502.8438380305 213585.89715549402 -91.39 37.247 +6318 6457 -89.05 42.233 2598989.437475753 2029410.0841629873 +6457 4326 2598989.437475753 2029410.0841629873 -89.05 42.233 +6318 6458 -85.685 39.86 98431.30571691465 511973.6556178053 +6458 4326 98431.30571691465 511973.6556178053 -85.685 39.85999999999999 +6318 6458 -86.4995 38.141000000000005 26992.295511845572 321471.9125532987 +6458 4326 26992.295511845572 321471.9125532987 -86.4995 38.141 +6318 6458 -84.8705 41.579 166394.06102200958 703165.6599238857 +6458 4326 166394.06102200958 703165.6599238857 -84.8705 41.579 +6318 6459 -85.685 39.86 322936.7085062442 1679700.2348060838 +6459 4326 322936.7085062442 1679700.2348060838 -85.685 39.85999999999999 +6318 6459 -86.4995 38.141000000000005 88557.22252511335 1054695.7661019482 +6459 4326 88557.22252511335 1054695.7661019482 -86.4995 38.141 +6318 6459 -84.8705 41.579 545911.1815363766 2306969.3356002825 +6459 4326 545911.1815363766 2306969.3356002825 -84.8705 41.579 +6318 6460 -87.16999999999999 39.77 892574.6756391372 501984.6231915935 +6460 4326 892574.6756391372 501984.6231915935 -87.16999999999999 39.77 +6318 6460 -88.00699999999999 37.970000000000006 818840.8874181742 302566.75752018945 +6460 4326 818840.8874181742 302566.75752018945 -88.00699999999999 37.97000000000001 +6318 6460 -86.333 41.57 962580.5798687736 702131.8622874243 +6460 4326 962580.5798687736 702131.8622874243 -86.333 41.57 +6318 6461 -87.16999999999999 39.77 2928388.7483260697 1646927.8842544204 +6461 4326 2928388.7483260697 1646927.8842544204 -87.16999999999999 39.77 +6318 6461 -88.00699999999999 37.970000000000006 2686480.4781377935 992671.1032974891 +6461 4326 2686480.4781377935 992671.1032974891 -88.00699999999999 37.97000000000001 +6318 6461 -86.333 41.57 3158066.4524528016 2303577.617854659 +6461 4326 3158066.4524528016 2303577.617854659 -86.333 41.57 +6318 6462 -93.4 42.68 1508195.834348411 1131075.1318394763 +6462 4326 1508195.834348411 1131075.1318394763 -93.4 42.680000000000014 +6318 6462 -96.325 41.933 1265738.527574641 1052010.8755992097 +6462 4326 1265738.527574641 1052010.8755992097 -96.325 41.933000000000014 +6318 6462 -90.47500000000001 43.427 1744902.3804289864 1218437.3308182764 +6462 4326 1744902.3804289864 1218437.3308182764 -90.47500000000001 43.42700000000001 +6318 6463 -93.4 42.68 4948139.1665247455 3710868.9950100156 +6463 4326 4948139.1665247455 3710868.9950100156 -93.4 42.680000000000014 +6318 6463 -96.325 41.933 4152677.152551135 3451472.3476617406 +6463 4326 4152677.152551135 3451472.3476617406 -96.325 41.933000000000014 +6318 6463 -90.47500000000001 43.427 5724733.8931241 3997489.8094929624 +6463 4326 5724733.8931241 3997489.8094929624 -90.47500000000001 43.42700000000001 +6318 6464 -93.14 41.2 530195.2801683228 133320.7160985994 +6464 4326 530195.2801683228 133320.7160985994 -93.14000000000001 41.20000000000001 +6318 6464 -95.84 40.444 301495.59603204846 51976.20620905145 +6464 4326 301495.59603204846 51976.20620905145 -95.84 40.444 +6318 6464 -90.44 41.955999999999996 753654.5170276335 221683.68954160615 +6464 4326 753654.5170276335 221683.68954160615 -90.44 41.95599999999999 +6318 6465 -93.14 41.2 1739482.3483855727 437403.0494001549 +6465 4326 1739482.3483855727 437403.0494001549 -93.14000000000001 41.20000000000001 +6318 6465 -95.84 40.444 989156.8013484788 170525.269870863 +6465 4326 989156.8013484788 170525.269870863 -95.84 40.444 +6318 6465 -90.44 41.955999999999996 2472614.8613148276 727307.2381044195 +6465 4326 2472614.8613148276 727307.2381044195 -90.44 41.95599999999999 +6318 6466 -98.32 39.265 372384.49819812784 103474.51018951085 +6466 4326 372384.49819812784 103474.51018951085 -98.32 39.26500000000003 +6318 6466 -101.686 38.594500000000004 78962.18575527694 35527.07937257582 +6466 4326 78962.18575527694 35527.07937257582 -101.686 38.5945 +6318 6466 -94.954 39.9355 660312.5113567541 182246.34859156059 +6466 4326 660312.5113567541 182246.34859156059 -94.954 39.93550000000001 +6318 6467 -98.32 39.265 1221731.4744716913 339482.6221800869 +6467 4326 1221731.4744716913 339482.6221800869 -98.32 39.26500000000003 +6318 6467 -101.686 38.594500000000004 259061.77106543793 116558.42624152584 +6467 4326 259061.77106543793 116558.42624152584 -101.686 38.5945 +6318 6467 -94.954 39.9355 2166375.297642951 597919.8953374784 +6467 4326 2166375.297642951 597919.8953374784 -94.954 39.93550000000001 +6318 6468 -98.32499999999999 37.935 415383.2434573826 540780.1749808954 +6468 4326 415383.2434573826 540780.1749808954 -98.32499999999999 37.935 +6318 6468 -101.6775 37.084500000000006 117521.86511747976 451187.8656569628 +6468 4326 117521.86511747976 451187.8656569628 -101.6775 37.08449999999999 +6318 6468 -94.9725 38.7855 706437.5117094776 640969.3042826953 +6468 4326 706437.5117094776 640969.3042826953 -94.9725 38.7855 +6318 6469 -98.32499999999999 37.935 1362803.191209763 1774209.624049821 +6469 4326 1362803.191209763 1774209.624049821 -98.32499999999999 37.935 +6318 6469 -101.6775 37.084500000000006 385569.65243959834 1480272.1892095525 +6469 4326 385569.65243959834 1480272.1892095525 -101.6775 37.08449999999999 +6318 6469 -94.9725 38.7855 2317703.7363001783 2102913.45910081 +6469 4326 2317703.7363001783 2102913.45910081 -94.9725 38.7855 +6318 6470 -84.215 38.43 503055.9840861851 103227.84047299267 +6470 4326 503055.9840861851 103227.84047299267 -84.215 38.43000000000001 +6318 6470 -85.7855 37.782000000000004 364736.787111394 32428.687076877144 +6470 4326 364736.787111394 32428.687076877144 -85.7855 37.78200000000001 +6318 6470 -82.6445 39.077999999999996 638921.4422613666 176371.6164450688 +6470 4326 638921.4422613666 176371.6164450688 -82.6445 39.07799999999999 +6318 6471 -84.215 38.43 1650442.841456092 338673.33995181014 +6471 4326 1650442.841456092 338673.33995181014 -84.215 38.43000000000001 +6318 6471 -85.7855 37.782000000000004 1196640.609381298 106393.11751805444 +6471 4326 1196640.609381298 106393.11751805444 -85.7855 37.78200000000001 +6318 6471 -82.6445 39.077999999999996 2096194.7654858336 578645.8782868632 +6471 4326 2096194.7654858336 578645.8782868632 -82.6445 39.07799999999999 +6318 6472 -85.75999999999999 37.82 1499119.6161673432 1164993.7957933834 +6472 4326 1499119.6161673432 1164993.7957933834 -85.75999999999999 37.81999999999999 +6318 6472 -89.189 36.623000000000005 1192410.1993640265 1037818.2979744445 +6472 4326 1192410.1993640265 1037818.2979744445 -89.189 36.623 +6318 6472 -82.331 39.016999999999996 1796069.135715349 1303285.8838335248 +6472 4326 1796069.135715349 1303285.8838335248 -82.331 39.017 +6318 6473 -85.75999999999999 37.82 4918361.607375693 3822150.478032126 +6473 4326 4918361.607375693 3822150.478032126 -85.75999999999999 37.81999999999999 +6318 6473 -89.189 36.623000000000005 3912099.129080144 3404908.865604491 +6473 4326 3912099.129080144 3404908.865604491 -89.189 36.623 +6318 6473 -82.331 39.016999999999996 5892603.489426109 4275863.770210491 +6473 4326 5892603.489426109 4275863.770210491 -82.331 39.017 +6318 6474 -85.75999999999999 37.33 499113.7931707417 610604.196224401 +6474 4326 499113.7931707417 610604.196224401 -85.75999999999999 37.33 +6318 6474 -89.189 36.574000000000005 192247.5353599636 532309.7194656162 +6474 4326 192247.5353599636 532309.7194656162 -89.189 36.57400000000001 +6318 6474 -82.331 38.086 799892.1099969771 699938.1634462873 +6474 4326 799892.1099969771 699938.1634462873 -82.331 38.086 +6318 6475 -85.75999999999999 37.33 1637509.1700943413 2003290.6007795555 +6475 4326 1637509.1700943413 2003290.6007795555 -85.75999999999999 37.33 +6318 6475 -89.189 36.574000000000005 630732.12259348 1746419.471613442 +6475 4326 630732.12259348 1746419.471613442 -89.189 36.57400000000001 +6318 6475 -82.331 38.086 2624312.697881749 2296380.458240027 +6475 4326 2624312.697881749 2296380.458240027 -82.331 38.086 +6318 6476 -92.455 31.94 1004254.595522245 159660.69532187734 +6476 4326 1004254.595522245 159660.69532187734 -92.455 31.93999999999999 +6318 6476 -93.8905 30.959000000000003 867141.0681850316 51745.95904705165 +6476 4326 867141.0681850316 51745.95904705165 -93.8905 30.959000000000017 +6318 6476 -91.0195 32.921 1138485.7486137254 269389.5708371389 +6476 4326 1138485.7486137254 269389.5708371389 -91.0195 32.921 +6318 6477 -92.455 31.94 3294791.9521092325 523820.13123519265 +6477 4326 3294791.9521092325 523820.13123519265 -92.455 31.93999999999999 +6318 6477 -93.8905 30.959000000000003 2844945.3211703915 169769.86730686863 +6477 4326 2844945.3211703915 169769.86730686863 -93.8905 30.959000000000017 +6318 6477 -91.0195 32.921 3735181.9935435313 883822.2836548467 +6477 4326 3735181.9935435313 883822.2836548467 -91.0195 32.921 +6318 6478 -91.345 29.96 998873.9589036357 161832.71462681066 +6478 4326 998873.9589036357 161832.71462681066 -91.345 29.96000000000001 +6318 6478 -93.6805 28.961000000000002 771204.4762175585 53445.368118892235 +6478 4326 771204.4762175585 53445.368118892235 -93.6805 28.961000000000002 +6318 6478 -89.0095 30.959 1222030.1736550392 274830.1880059256 +6478 4326 1222030.1736550392 274830.1880059256 -89.0095 30.959 +6318 6479 -91.345 29.96 3277138.980136345 530946.1645714614 +6479 4326 3277138.980136345 530946.1645714614 -91.345 29.96000000000001 +6318 6479 -93.6805 28.961000000000002 2530193.3523571067 175345.3452367323 +6479 4326 2530193.3523571067 175345.3452367323 -93.6805 28.961000000000002 +6318 6479 -89.0095 30.959 4009277.3280332414 901672.0418161077 +6479 4326 4009277.3280332414 901672.0418161077 -89.0095 30.959 +6318 6480 -69.18 45.61 495709.77078438434 234466.4915910657 +6480 4326 495709.77078438434 234466.4915910657 -69.18 45.61000000000001 +6318 6480 -69.94500000000001 43.936 434161.4947354724 48768.47616865049 +6480 4326 434161.4947354724 48768.47616865049 -69.94500000000001 43.936 +6318 6480 -68.41499999999999 47.284 553711.7737722797 420787.73281540995 +6480 4326 553711.7737722797 420787.73281540995 -68.41499999999999 47.284 +6318 6481 -67.745 45.775 710110.7661583305 215777.34469428813 +6481 4326 710110.7661583305 215777.34469428813 -67.745 45.775 +6318 6481 -68.4965 44.3395 650437.9411447884 56428.91664329373 +6481 4326 650437.9411447884 56428.91664329373 -68.4965 44.3395 +6318 6481 -66.9935 47.210499999999996 766778.0604996847 375714.4525110518 +6481 4326 766778.0604996847 375714.4525110518 -66.9935 47.210499999999996 +6318 6482 -70.345 44.825 302372.5469386635 221287.9296204905 +6482 4326 302372.5469386635 221287.9296204905 -70.345 44.82500000000001 +6318 6482 -71.0155 43.2455 247982.39365818878 45987.32700179099 +6482 4326 247982.39365818878 45987.32700179099 -71.0155 43.24550000000001 +6318 6482 -69.6745 46.4045 353865.1265871693 397074.0751520627 +6482 4326 353865.1265871693 397074.0751520627 -69.6745 46.40449999999999 +6318 6483 -68.47 45.675 302337.23350621155 223154.85963413733 +6483 4326 302337.23350621155 223154.85963413733 -68.47 45.675 +6318 6483 -69.874 44.0595 189917.5275568 44561.156556155605 +6483 4326 189917.5275568 44561.156556155605 -69.874 44.0595 +6318 6483 -67.066 47.2905 408460.01410141285 403714.07063095423 +6483 4326 408460.01410141285 403714.07063095423 -67.066 47.2905 +6318 6484 -68.47 45.675 991918.0735949625 732133.9019829989 +6484 4326 991918.0735949625 732133.9019829989 -68.47 45.675 +6318 6484 -69.874 44.0595 623087.7549926013 146197.72780132052 +6484 4326 623087.7549926013 146197.72780132052 -69.874 44.0595 +6318 6484 -67.066 47.2905 1340089.2295977187 1324518.5800617225 +6484 4326 1340089.2295977187 1324518.5800617225 -67.066 47.2905 +6318 6485 -70.17500000000001 44.81 899340.7970898099 219617.70745510195 +6485 4326 899340.7970898099 219617.70745510195 -70.17500000000001 44.81000000000001 +6318 6485 -70.9985 43.217 832412.7890298931 42957.32044205438 +6485 4326 832412.7890298931 42957.32044205438 -70.9985 43.217000000000006 +6318 6485 -69.3515 46.403 962683.305256669 396986.5141183069 +6485 4326 962683.305256669 396986.5141183069 -69.3515 46.403000000000006 +6318 6486 -70.17500000000001 44.81 2950587.2651188183 720529.0952089471 +6486 4326 2950587.2651188183 720529.0952089471 -70.17500000000001 44.81000000000001 +6318 6486 -70.9985 43.217 2731007.6253422415 140935.8088169734 +6486 4326 2731007.6253422415 140935.8088169734 -70.9985 43.217000000000006 +6318 6486 -69.3515 46.403 3158403.4773295885 1302446.5884031455 +6486 4326 3158403.4773295885 1302446.5884031455 -69.3515 46.403000000000006 +6318 6487 -77.22999999999999 38.849999999999994 380034.9481462034 131380.0755704414 +6487 4326 380034.9481462034 131380.0755704414 -77.22999999999999 38.84999999999999 +6318 6487 -79.264 38.058 201314.2794625202 45904.12107919067 +6487 4326 201314.2794625202 45904.12107919067 -79.264 38.05799999999997 +6318 6487 -75.196 39.641999999999996 554847.9829545154 220811.09831744133 +6487 4326 554847.9829545154 220811.09831744133 -75.196 39.641999999999996 +6318 6488 -77.22999999999999 38.849999999999994 1246831.3253763365 431036.13126735657 +6488 4326 1246831.3253763365 431036.13126735657 -77.22999999999999 38.84999999999999 +6318 6488 -79.264 38.058 660478.5982032857 150603.77057397808 +6488 4326 660478.5982032857 150603.77057397808 -79.264 38.05799999999997 +6318 6488 -75.196 39.641999999999996 1820363.7570766064 724444.4117298055 +6488 4326 1820363.7570766064 724444.4117298055 -75.196 39.641999999999996 +6318 6489 -70.4 41.349999999999994 508368.84153597394 38875.137285071796 +6489 4326 508368.84153597394 38875.137285071796 -70.4 41.34999999999999 +6318 6489 -70.859 41.205999999999996 469889.6934071739 22940.12737198121 +6489 4326 469889.6934071739 22940.12737198121 -70.859 41.205999999999996 +6318 6489 -69.941 41.494 546678.3583144168 55013.767025989066 +6489 4326 546678.3583144168 55013.767025989066 -69.941 41.49399999999999 +6318 6490 -70.4 41.349999999999994 1667873.4412726075 127542.84624277307 +6490 4326 1667873.4412726075 127542.84624277307 -70.4 41.34999999999999 +6318 6490 -70.859 41.205999999999996 1541629.7694533693 75262.73455290835 +6490 4326 1541629.7694533693 75262.73455290835 -70.859 41.205999999999996 +6318 6490 -69.941 41.494 1793560.5809032158 180491.00065109914 +6490 4326 1793560.5809032158 180491.00065109914 -69.941 41.49399999999999 +6318 6491 -71.68 42.175 185128.3305030929 880522.4527347037 +6491 4326 185128.3305030929 880522.4527347037 -71.68 42.17500000000001 +6318 6491 -73.318 41.5315 48284.138797150605 810650.6882213238 +6491 4326 48284.138797150605 810650.6882213238 -73.318 41.531499999999994 +6318 6491 -70.042 42.8185 319232.9572599032 953006.5012715762 +6491 4326 319232.9572599032 953006.5012715762 -70.042 42.8185 +6318 6492 -71.68 42.175 607375.19799223 2888847.4136804407 +6492 4326 607375.19799223 2888847.4136804407 -71.68 42.17500000000001 +6318 6492 -73.318 41.5315 158412.21237031757 2659609.7996061267 +6492 4326 158412.21237031757 2659609.7996061267 -73.318 41.531499999999994 +6318 6492 -70.042 42.8185 1047350.1276101986 3126655.496255163 +6492 4326 1047350.1276101986 3126655.496255163 -70.042 42.8185 +6318 6493 -84.66499999999999 44.86 5976422.234671376 171538.31270695946 +6493 4326 5976422.234671376 171538.31270695946 -84.66499999999999 44.86000000000001 +6318 6493 -86.8205 43.906 5802892.177720298 68471.58702111564 +6493 4326 5802892.177720298 68471.58702111564 -86.8205 43.906 +6318 6493 -82.5095 45.814 6144335.022055124 279167.9333284333 +6493 4326 6144335.022055124 279167.9333284333 -82.5095 45.814 +6318 6494 -84.66499999999999 44.86 19607684.49687459 562789.7398522291 +6494 4326 19607684.49687459 562789.7398522291 -84.66499999999999 44.86000000000001 +6318 6494 -86.8205 43.906 19038360.163045596 224644.31437373898 +6494 4326 19038360.163045596 224644.31437373898 -86.8205 43.906 +6318 6494 -82.5095 45.814 20158579.468606047 915905.2930722877 +6494 4326 20158579.468606047 915905.2930722877 -82.5095 45.814 +6318 6495 -86.93 46.7 8005353.265366577 213031.3436327338 +6495 4326 8005353.265366577 213031.3436327338 -86.93 46.7 +6318 6495 -90.071 45.242 7758926.973017988 55650.446219124075 +6495 4326 7758926.973017988 55650.446219124075 -90.07100000000001 45.242 +6318 6495 -83.789 48.158 8238929.602731237 379989.2076339546 +6495 4326 8238929.602731237 379989.2076339546 -83.789 48.157999999999994 +6318 6496 -86.93 46.7 26264282.366583254 698921.7310785229 +6496 4326 26264282.366583254 698921.7310785229 -86.93 46.7 +6318 6496 -90.071 45.242 25455797.155465834 182580.20413098446 +6496 4326 25455797.155465834 182580.20413098446 -90.07100000000001 45.242 +6318 6496 -83.789 48.158 27030608.932740275 1246683.7520799034 +6496 4326 27030608.932740275 1246683.7520799034 -83.78900000000002 48.157999999999994 +6318 6497 -86.275 45.004999999999995 478167.87845010986 494847.00078408886 +6497 4326 478167.87845010986 494847.00078408886 -86.275 45.004999999999995 +6318 6497 -90.0055 42.021499999999996 168036.26552634127 171146.5263229655 +6497 4326 168036.26552634127 171146.5263229655 -90.0055 42.02149999999998 +6318 6497 -82.5445 47.9885 757726.9104425479 832165.3397596804 +6497 4326 757726.9104425479 832165.3397596804 -82.5445 47.988499999999995 +6318 6498 -84.66499999999999 42.955 3975658.1660904195 161660.9908124227 +6498 4326 3975658.1660904195 161660.9908124227 -84.66499999999999 42.95499999999999 +6318 6498 -86.9465 41.8165 3785662.443796816 38441.45475135792 +6498 4326 3785662.443796816 38441.45475135792 -86.9465 41.816500000000005 +6318 6498 -82.3835 44.0935 4158818.2040767726 289977.3408485526 +6498 4326 4158818.2040767726 289977.3408485526 -82.3835 44.0935 +6318 6499 -84.66499999999999 42.955 13043497.920191662 530383.8281247463 +6499 4326 13043497.920191662 530383.8281247463 -84.66499999999999 42.95499999999999 +6318 6499 -86.9465 41.8165 12420152.374608975 126120.25837059683 +6499 4326 12420152.374608975 126120.25837059683 -86.9465 41.816500000000005 +6318 6499 -82.3835 44.0935 13644416.679989409 951369.2285057498 +6499 4326 13644416.679989409 951369.2285057498 -82.38350000000001 44.0935 +6318 6500 -94.575 46.379999999999995 774999.3390451781 253433.09136305802 +6500 4326 774999.3390451781 253433.09136305802 -94.575 46.37999999999999 +6318 6500 -96.6315 45.39 613521.9818043811 146151.76135058573 +6500 4326 613521.9818043811 146151.76135058573 -96.6315 45.39 +6318 6500 -92.5185 47.37 930779.8796238166 364865.75352756435 +6500 4326 930779.8796238166 364865.75352756435 -92.5185 47.37 +6318 6501 -94.575 46.379999999999995 2542643.6648840555 831471.7338802997 +6501 4326 2542643.6648840555 831471.7338802997 -94.575 46.37999999999999 +6318 6501 -96.6315 45.39 2012863.368669874 479499.5703310468 +6501 4326 2012863.368669874 479499.5703310468 -96.6315 45.39 +6318 6501 -92.5185 47.37 3053733.6550991386 1197063.726331684 +6501 4326 3053733.6550991386 1197063.726331684 -92.5185 47.37 +6318 6502 -93.35499999999999 48.010000000000005 780976.0080242507 267903.8812841848 +6502 4326 780976.0080242507 267903.8812841848 -93.35499999999999 48.01000000000001 +6318 6502 -96.8335 46.777 514956.0367357529 137681.0978387199 +6502 4326 514956.0367357529 137681.0978387199 -96.8335 46.77700000000001 +6318 6502 -89.8765 49.243 1034700.1274336235 409882.0127727458 +6502 4326 1034700.1274336235 409882.0127727458 -89.8765 49.243 +6318 6503 -93.35499999999999 48.010000000000005 2562252.1196928965 878947.9838131967 +6503 4326 2562252.1196928965 878947.9838131967 -93.35499999999999 48.01000000000001 +6318 6503 -96.8335 46.777 1689484.930557216 451708.7351258669 +6503 4326 1689484.930557216 451708.7351258669 -96.8335 46.77700000000001 +6318 6503 -89.8765 49.243 3394678.668121814 1344754.5702052503 +6503 4326 3394678.668121814 1344754.5702052503 -89.8765 49.243 +6318 6504 -94.03 44.540000000000006 797615.9303731995 271111.9496617395 +6504 4326 797615.9303731995 271111.9496617395 -94.03 44.54000000000001 +6318 6504 -96.568 43.595 592659.1578750131 169370.35030780142 +6504 4326 592659.1578750131 169370.35030780142 -96.568 43.59499999999999 +6318 6504 -91.49199999999999 45.48500000000001 996055.3728436409 379136.3411765582 +6504 4326 996055.3728436409 379136.3411765582 -91.49199999999999 45.48500000000001 +6318 6505 -94.03 44.540000000000006 2616844.9315994056 889473.1214818907 +6505 4326 2616844.9315994056 889473.1214818907 -94.03 44.54000000000001 +6318 6505 -96.568 43.595 1944415.920494939 555675.8909348453 +6505 4326 1944415.920494939 555675.8909348453 -96.568 43.59499999999999 +6318 6505 -91.49199999999999 45.48500000000001 3267891.6691045123 1243883.1459767583 +6505 4326 3267891.6691045123 1243883.1459767583 -91.49199999999999 45.48500000000001 +6318 6506 -89.03 32.51 281520.8578694894 333718.072514426 +6506 4326 281520.8578694894 333718.072514426 -89.03000000000002 32.51 +6318 6506 -89.876 30.26 199662.52524006984 84702.21482670316 +6506 4326 199662.52524006984 84702.21482670316 -89.876 30.26000000000001 +6318 6506 -88.184 34.76 359446.4830207231 583441.3092225477 +6506 4326 359446.4830207231 583441.3092225477 -88.184 34.76 +6318 6507 -89.03 32.51 923623.0145268167 1094873.3762410795 +6507 4326 923623.0145268167 1094873.3762410795 -89.03000000000002 32.51 +6318 6507 -89.876 30.26 655059.4682251292 277893.84981060866 +6507 4326 655059.4682251292 277893.84981060866 -89.876 30.26000000000001 +6318 6507 -88.184 34.76 1179284.0030438225 1914173.6953409754 +6507 4326 1179284.0030438225 1914173.6953409754 -88.184 34.76 +6318 6508 -89.87 32.51 488725.91080891585 1301115.1174180263 +6508 4326 488725.91080891585 1301115.1174180263 -89.87 32.51 +6318 6508 -91.47200000000001 30.26 334300.6688884102 1052933.9393655262 +6508 4326 334300.6688884102 1052933.9393655262 -91.47200000000001 30.26000000000001 +6318 6508 -88.268 34.76 635665.6434956547 1551628.392431662 +6508 4326 635665.6434956547 1551628.392431662 -88.268 34.76 +6318 6509 -90.51 33.004999999999995 683491.678094493 388607.7223874255 +6509 4326 683491.678094493 388607.7223874255 -90.51 33.004999999999995 +6318 6509 -91.536 31.200499999999998 585383.0308493291 189128.51300998154 +6509 4326 585383.0308493291 189128.51300998154 -91.536 31.20049999999999 +6318 6509 -89.48400000000001 34.8095 777710.4632391258 589069.1501028265 +6509 4326 777710.4632391258 589069.1501028265 -89.48400000000001 34.8095 +6318 6510 -90.51 33.004999999999995 2242422.280215017 1274957.169199412 +6510 4326 2242422.280215017 1274957.169199412 -90.51 33.004999999999995 +6318 6510 -91.536 31.200499999999998 1920544.1600448415 620499.1297669145 +6510 4326 1920544.1600448415 620499.1297669145 -91.536 31.20049999999999 +6318 6510 -89.48400000000001 34.8095 2551538.4111436997 1932637.7032956902 +6510 4326 2551538.4111436997 1932637.7032956902 -89.48400000000001 34.8095 +6318 6511 -92.6 38.545 491282.7335977044 300928.83009349275 +6511 4326 491282.7335977044 300928.83009349275 -92.6 38.545 +6318 6511 -93.671 36.686499999999995 395345.8443743689 95303.31149016054 +6511 4326 395345.8443743689 95303.31149016054 -93.671 36.68649999999999 +6318 6511 -91.529 40.4035 582422.4658619608 507702.20165428886 +6511 4326 582422.4658619608 507702.20165428886 -91.529 40.4035 +6318 6512 -90.535 38.295 246938.423324011 273175.40948622045 +6512 4326 246938.423324011 273175.40948622045 -90.53500000000001 38.295 +6318 6512 -91.8265 36.211499999999994 130722.74568677916 42774.13741293872 +6512 4326 130722.74568677916 42774.13741293872 -91.82650000000001 36.211499999999994 +6318 6512 -89.2435 40.3785 356696.8903372759 505231.6112297185 +6512 4326 356696.8903372759 505231.6112297185 -89.2435 40.3785 +6318 6513 -94.625 38.535 839101.8219779618 262839.6964433915 +6513 4326 839101.8219779618 262839.6964433915 -94.625 38.535 +6318 6513 -95.65549999999999 36.6855 746729.0116193696 58192.123500483096 +6513 4326 746729.0116193696 58192.123500483096 -95.6555 36.6855 +6318 6513 -93.59450000000001 40.3845 926884.6909838021 468553.285440557 +6513 4326 926884.6909838021 468553.285440557 -93.59450000000001 40.384499999999996 +6318 6514 -110.055 46.68 557562.9972258529 270175.48989245493 +6514 4326 557562.9972258529 270175.48989245493 -110.055 46.68000000000001 +6318 6514 -115.46849999999999 44.583 126329.73944052018 55073.69494508364 +6514 4326 126329.73944052018 55073.69494508364 -115.46849999999999 44.583000000000006 +6318 6514 -104.64150000000001 48.777 956814.4198691815 514139.9142825689 +6514 4326 956814.4198691815 514139.9142825689 -104.64150000000001 48.77700000000001 +6318 6515 -110.055 46.68 1829274.9252737954 886402.5258938809 +6515 4326 1829274.9252737954 886402.5258938809 -110.055 46.68000000000001 +6318 6515 -115.46849999999999 44.583 414467.64907519735 180687.97554161298 +6515 4326 414467.64907519735 180687.97554161298 -115.46849999999999 44.583000000000006 +6318 6515 -104.64150000000001 48.777 3139154.9208227736 1686810.7423968795 +6515 4326 3139154.9208227736 1686810.7423968795 -104.64150000000001 48.77700000000001 +6318 6516 -99.68 41.5 526709.5912096216 185091.48420576085 +6516 4326 526709.5912096216 185091.48420576085 -99.68 41.5 +6318 6516 -103.622 40.141000000000005 191449.57910202077 40625.804804552994 +6516 4326 191449.57910202077 40625.804804552994 -103.622 40.14100000000001 +6318 6516 -95.738 42.858999999999995 848158.6906421972 344540.8856728787 +6516 4326 848158.6906421972 344540.8856728787 -95.738 42.858999999999995 +6318 6518 -116.59 38.5 506687.1770771124 6416101.467797132 +6518 4326 506687.1770771124 6416101.467797132 -116.59 38.50000000000001 +6318 6518 -118.03 36.25 377474.7274872544 6167270.088553119 +6518 4326 377474.7274872544 6167270.088553119 -118.03000000000002 36.25 +6318 6518 -115.14999999999999 40.75 628075.3950645399 6666992.146123811 +6518 4326 628075.3950645399 6666992.146123811 -115.14999999999999 40.75 +6318 6519 -116.59 38.5 1662356.1801604931 21050159.56559776 +6519 4326 1662356.1801604931 21050159.56559776 -116.59 38.50000000000002 +6318 6519 -118.03 36.25 1238431.6684644339 20233785.282194696 +6519 4326 1238431.6684644339 20233785.282194696 -118.03000000000002 36.250000000000014 +6318 6519 -115.14999999999999 40.75 2060610.6920075782 21873290.06607454 +6519 4326 2060610.6920075782 21873290.06607454 -115.14999999999999 40.75000000000001 +6318 6520 -115.52000000000001 38.495000000000005 205524.57150390767 8415545.609160446 +6520 4326 205524.57150390767 8415545.609160446 -115.52000000000001 38.495000000000005 +6318 6520 -116.861 35.3405 83857.77529178846 8066253.469782333 +6520 4326 83857.77529178846 8066253.469782333 -116.86100000000002 35.3405 +6318 6520 -114.179 41.6495 316976.07839282975 8766724.77413967 +6520 4326 316976.07839282975 8766724.77413967 -114.179 41.64950000000001 +6318 6521 -115.52000000000001 38.495000000000005 674291.8650424038 27610002.552753896 +6521 4326 674291.8650424038 27610002.552753896 -115.52000000000001 38.495 +6318 6521 -116.861 35.3405 275123.3844698093 26464033.258810874 +6521 4326 275123.3844698093 26464033.258810874 -116.86100000000002 35.340500000000006 +6318 6521 -114.179 41.6495 1039945.6838938089 28762162.86318991 +6521 4326 1039945.6838938089 28762162.86318991 -114.179 41.64950000000001 +6318 6522 -118.495 39.475 807599.8137248502 4524331.450272928 +6522 4326 807599.8137248502 4524331.450272928 -118.495 39.475 +6318 6522 -119.8495 37.2025 687604.7269785614 4272849.753804591 +6522 4326 687604.7269785614 4272849.753804591 -119.8495 37.20250000000001 +6318 6522 -117.14049999999999 41.7475 920000.7132682288 4777661.662819959 +6522 4326 920000.7132682288 4777661.662819959 -117.14049999999999 41.747499999999995 +6318 6523 -118.495 39.475 2649600.388895613 14843577.433070434 +6523 4326 2649600.388895613 14843577.433070434 -118.495 39.475 +6318 6523 -119.8495 37.2025 2255916.5084621636 14018507.900573896 +6523 4326 2255916.5084621636 14018507.900573896 -119.8495 37.20250000000001 +6318 6523 -117.14049999999999 41.7475 3018369.006814181 15674711.638735153 +6523 4326 3018369.006814181 15674711.638735153 -117.14049999999999 41.74750000000001 +6318 6524 -71.595 44.0 305747.9189590065 166643.36188109402 +6524 4326 305747.9189590065 166643.36188109402 -71.595 43.99999999999999 +6318 6524 -72.4635 42.821 234839.1555409388 35965.449943250715 +6524 4326 234839.1555409388 35965.449943250715 -72.4635 42.821 +6318 6524 -70.7265 45.179 373895.511671885 298081.46545753366 +6524 4326 373895.511671885 298081.46545753366 -70.7265 45.17900000000001 +6318 6525 -71.595 44.0 1003107.9641180072 546729.0964382227 +6525 4326 1003107.9641180072 546729.0964382227 -71.595 43.99999999999999 +6318 6525 -72.4635 42.821 770468.1294705635 117996.6470221484 +6525 4326 770468.1294705635 117996.6470221484 -72.4635 42.821 +6318 6525 -70.7265 45.179 1226688.8578768428 977955.6079219251 +6525 4326 1226688.8578768428 977955.6079219251 -70.7265 45.17900000000001 +6318 6526 -74.74 40.114999999999995 129541.93200635779 142309.84863179177 +6526 4326 129541.93200635779 142309.84863179177 -74.74 40.115 +6318 6526 -75.514 38.994499999999995 62161.845770000305 18379.059719253342 +6526 4326 62161.845770000305 18379.059719253342 -75.514 38.99449999999999 +6318 6526 -73.966 41.2355 194763.50353423748 266836.18215498014 +6526 4326 194763.50353423748 266836.18215498014 -73.966 41.23550000000001 +6318 6527 -74.74 40.114999999999995 425005.4885908589 466894.89505280356 +6527 4326 425005.4885908589 466894.89505280356 -74.74 40.115 +6318 6527 -75.514 38.994499999999995 203942.65566374268 60298.63176225035 +6527 4326 203942.65566374268 60298.63176225035 -75.514 38.99449999999999 +6318 6527 -73.966 41.2355 638986.5945119108 875445.0409534641 +6527 4326 638986.5945119108 875445.0409534641 -73.966 41.23550000000001 +6318 6528 -106.285 34.39 496781.7063517648 375911.1857639483 +6528 4326 496781.7063517648 375911.1857639483 -106.28500000000001 34.39000000000001 +6318 6528 -107.58550000000001 32.041000000000004 373868.1798690023 116192.89978335424 +6528 4326 373868.1798690023 116192.89978335424 -107.58550000000002 32.041 +6318 6528 -104.9845 36.739 613019.4483625791 637255.137029333 +6528 4326 613019.4483625791 637255.137029333 -104.9845 36.739000000000004 +6318 6529 -106.285 34.39 1629857.9819224146 1233301.9486272205 +6529 4326 1629857.9819224146 1233301.9486272205 -106.28500000000001 34.39000000000001 +6318 6529 -107.58550000000001 32.041000000000004 1226599.187120218 381209.5387058881 +6529 4326 1226599.187120218 381209.5387058881 -107.58550000000002 32.041 +6318 6529 -104.9845 36.739 2011214.6405028943 2090727.895403737 +6529 4326 2011214.6405028943 2090727.895403737 -104.9845 36.739000000000004 +6318 6530 -104.35499999999999 34.5 163010.31461583532 388115.5015019724 +6530 4326 163010.31461583532 388115.5015019724 -104.355 34.5 +6318 6530 -105.5835 32.25 47196.13683957874 139273.63539034483 +6530 4326 47196.13683957874 139273.63539034483 -105.5835 32.24999999999999 +6318 6530 -103.1265 36.75 272765.40574489284 638413.9669679842 +6530 4326 272765.40574489284 638413.9669679842 -103.1265 36.75 +6318 6531 -104.35499999999999 34.5 534809.6738687864 1273342.2745110546 +6531 4326 534809.6738687864 1273342.2745110546 -104.355 34.5 +6318 6531 -105.5835 32.25 154842.65894785128 456933.58544315636 +6531 4326 154842.65894785128 456933.58544315636 -105.5835 32.24999999999999 +6318 6531 -103.1265 36.75 894897.835348036 2094529.8232941285 +6531 4326 894897.835348036 2094529.8232941285 -103.1265 36.75 +6318 6532 -107.69 34.165 843215.0652553578 350969.5921065168 +6532 4326 843215.0652553578 350969.5921065168 -107.69 34.165 +6318 6532 -108.923 31.6135 726610.1809091113 68531.38595231783 +6532 4326 726610.1809091113 68531.38595231783 -108.923 31.6135 +6318 6532 -106.457 36.716499999999996 952956.2155361695 634905.4140072872 +6532 4326 952956.2155361695 634905.4140072872 -106.45700000000001 36.7165 +6318 6533 -107.69 34.165 2766448.093591952 1151472.736769464 +6533 4326 2766448.093591952 1151472.736769464 -107.69 34.165 +6318 6533 -108.923 31.6135 2383886.902199309 224840.0554118961 +6533 4326 2383886.902199309 224840.0554118961 -108.923 31.6135 +6318 6533 -106.457 36.716499999999996 3126490.517471582 2083018.8457889084 +6533 4326 3126490.517471582 2083018.8457889084 -106.45700000000001 36.7165 +6318 6534 -76.405 43.2 264493.31539123424 355403.0177038457 +6534 4326 264493.31539123424 355403.0177038457 -76.405 43.2 +6318 6534 -77.6155 42.111000000000004 164637.84596996327 234938.07240019878 +6534 4326 164637.84596996327 234938.07240019878 -77.6155 42.111000000000004 +6318 6534 -75.1945 44.288999999999994 360844.2676941891 477313.98916154087 +6534 4326 360844.2676941891 477313.98916154087 -75.19450000000002 44.288999999999994 +6318 6535 -76.405 43.2 867758.4852460751 1166018.0672500338 +6535 4326 867758.4852460751 1166018.0672500338 -76.405 43.2 +6318 6535 -77.6155 42.111000000000004 540149.3326531219 770792.6591996523 +6535 4326 540149.3326531219 770792.6591996523 -77.6155 42.111000000000004 +6318 6535 -75.1945 44.288999999999994 1183869.9012600197 1565987.6461074888 +6535 4326 1183869.9012600197 1565987.6461074888 -75.19450000000002 44.288999999999994 +6318 6536 -74.55000000000001 42.95 145920.05529655414 457119.0986501149 +6536 4326 145920.05529655414 457119.0986501149 -74.55000000000001 42.95000000000001 +6318 6536 -75.738 41.087 45987.00461632412 250946.93264947843 +6536 4326 45987.00461632412 250946.93264947843 -75.738 41.087 +6318 6536 -73.36200000000001 44.813 240010.12987699886 664725.038343087 +6536 4326 240010.12987699886 664725.038343087 -73.36200000000001 44.813 +6318 6537 -74.55000000000001 42.95 478739.3814187781 1499731.5761545855 +6537 4326 478739.3814187781 1499731.5761545855 -74.55000000000001 42.95000000000001 +6318 6537 -75.738 41.087 150875.69764539006 823315.0615341639 +6537 4326 150875.69764539006 823315.0615341639 -75.738 41.087 +6318 6537 -73.36200000000001 44.813 787433.2344381205 2180852.0632972782 +6537 4326 787433.2344381205 2180852.0632972782 -73.36200000000001 44.813 +6318 6538 -73.03 40.885 381750.7399232304 80221.1659232867 +6538 4326 381750.7399232304 80221.1659232867 -73.03 40.885000000000005 +6318 6538 -74.137 40.5115 288388.66202628706 38301.14700668562 +6538 4326 288388.66202628706 38301.14700668562 -74.137 40.511500000000005 +6318 6538 -71.923 41.2585 474051.4783082586 123311.4021303021 +6538 4326 474051.4783082586 123311.4021303021 -71.923 41.2585 +6318 6539 -73.03 40.885 1252460.5525647984 263192.27519998315 +6539 4326 1252460.5525647984 263192.27519998315 -73.03 40.885000000000005 +6318 6539 -74.137 40.5115 946155.1353312435 125659.67980443442 +6539 4326 946155.1353312435 125659.67980443442 -74.137 40.511500000000005 +6318 6539 -71.923 41.2585 1555283.8917496786 404564.1584891662 +6539 4326 1555283.8917496786 404564.1584891662 -71.923 41.2585 +6318 6540 -78.565 42.815 351499.30115682975 312619.6539237273 +6540 4326 351499.30115682975 312619.6539237273 -78.565 42.81499999999999 +6318 6540 -79.64949999999999 42.072500000000005 261772.6033852207 230696.4045349582 +6540 4326 261772.6033852207 230696.4045349582 -79.64949999999999 42.0725 +6318 6540 -77.4805 43.5575 439103.5261805072 395694.3379393331 +6540 4326 439103.5261805072 395694.3379393331 -77.4805 43.557500000000005 +6318 6541 -78.565 42.815 1153210.6242120317 1025652.9812480954 +6541 4326 1153210.6242120317 1025652.9812480954 -78.565 42.81499999999999 +6318 6541 -79.64949999999999 42.072500000000005 858832.283273011 756876.4538784421 +6541 4326 858832.283273011 756876.4538784421 -79.64949999999999 42.0725 +6318 6541 -77.4805 43.5575 1440625.485810547 1298207.173722629 +6541 4326 1440625.485810547 1298207.173722629 -77.4805 43.557500000000005 +6318 6542 -79.85499999999999 35.21 531760.6294742532 162292.8993226596 +6542 4326 531760.6294742532 162292.8993226596 -79.85499999999999 35.21000000000001 +6318 6542 -83.8825 33.967999999999996 158490.68215049576 35280.47102519372 +6542 4326 158490.68215049576 35280.47102519372 -83.8825 33.96799999999999 +6318 6542 -75.8275 36.452000000000005 893982.110337277 304301.6117079829 +6542 4326 893982.110337277 304301.6117079829 -75.8275 36.45200000000001 +6318 6543 -79.85499999999999 35.21 1744617.9959167782 532455.9538610925 +6543 4326 1744617.9959167782 532455.9538610925 -79.85499999999999 35.21000000000001 +6318 6543 -83.8825 33.967999999999996 519981.51040541707 115749.34535515642 +6543 4326 519981.51040541707 115749.34535515642 -83.8825 33.96799999999999 +6318 6543 -75.8275 36.452000000000005 2933006.304381549 998362.8710786074 +6543 4326 2933006.304381549 998362.8710786074 -75.8275 36.45200000000001 +6318 6544 -100.44999999999999 48.08 603725.2560461811 120076.52492659682 +6544 4326 603725.2560461811 120076.52492659682 -100.44999999999999 48.080000000000005 +6318 6544 -103.708 47.242999999999995 357180.63227263116 32076.32260325921 +6544 4326 357180.63227263116 32076.32260325921 -103.708 47.242999999999995 +6318 6544 -97.192 48.917 842389.7430150689 218354.69478269183 +6544 4326 842389.7430150689 218354.69478269183 -97.192 48.91700000000001 +6318 6545 -100.44999999999999 48.08 1980725.9056554495 393951.8534337165 +6545 4326 1980725.9056554495 393951.8534337165 -100.44999999999999 48.080000000000005 +6318 6545 -103.708 47.242999999999995 1171852.468078186 105237.278882084 +6545 4326 1171852.468078186 105237.278882084 -103.708 47.242999999999995 +6318 6545 -97.192 48.917 2763745.8760258164 716386.7939064692 +6545 4326 2763745.8760258164 716386.7939064692 -97.192 48.91700000000001 +6318 6546 -100.3 46.879999999999995 615244.233397394 134889.74595170486 +6546 4326 615244.233397394 134889.74595170486 -100.3 46.88000000000001 +6318 6546 -103.675 46.025 354223.17057553853 44799.12802504772 +6546 4326 354223.17057553853 44799.12802504772 -103.675 46.025000000000006 +6318 6546 -96.925 47.735 868072.4690473499 236025.6304733758 +6546 4326 868072.4690473499 236025.6304733758 -96.925 47.73499999999999 +6318 6547 -100.3 46.879999999999995 2018517.8260990612 442551.6599465382 +6547 4326 2018517.8260990612 442551.6599465382 -100.3 46.88000000000001 +6318 6547 -103.675 46.025 1162149.5097543914 146978.76648637702 +6547 4326 1162149.5097543914 146978.76648637702 -103.675 46.025000000000006 +6318 6547 -96.925 47.735 2848006.7882052157 774362.3047026764 +6547 4326 2848006.7882052157 774362.3047026764 -96.925 47.73499999999999 +6318 6548 -82.66 41.215 586582.9888405734 171948.99910634552 +6548 4326 586582.9888405734 171948.99910634552 -82.66 41.21500000000002 +6318 6548 -84.595 40.2115 421661.2948510517 62644.95346345006 +6548 4326 421661.2948510517 62644.95346345006 -84.595 40.211499999999994 +6318 6548 -80.72500000000001 42.2185 746566.5265375796 284886.53487657424 +6548 4326 746566.5265375796 284886.53487657424 -80.72500000000001 42.218499999999985 +6318 6549 -82.66 41.215 1924481.0225544479 564136.007901402 +6549 4326 1924481.0225544479 564136.007901402 -82.66 41.21500000000002 +6318 6549 -84.595 40.2115 1383400.4315238255 205527.65148800242 +6549 4326 1383400.4315238255 205527.65148800242 -84.595 40.211499999999994 +6318 6549 -80.72500000000001 42.2185 2449360.345815376 934665.2398408941 +6549 4326 2449360.345815376 934665.2398408941 -80.72500000000001 42.218499999999985 +6318 6550 -82.765 39.379999999999995 577168.794808187 153231.72928756103 +6550 4326 577168.794808187 153231.72928756103 -82.765 39.379999999999995 +6318 6550 -84.62349999999999 38.498 414763.0625386159 57464.14699698818 +6550 4326 414763.0625386159 57464.14699698818 -84.62349999999999 38.498 +6318 6550 -80.90650000000001 40.262 735553.8037072645 252321.37590929342 +6550 4326 735553.8037072645 252321.37590929342 -80.90650000000001 40.261999999999986 +6318 6551 -82.765 39.379999999999995 1893594.6209665271 502727.76517093985 +6551 4326 1893594.6209665271 502727.76517093985 -82.765 39.379999999999995 +6318 6551 -84.62349999999999 38.498 1360768.4810121092 188530.2889392854 +6551 4326 1360768.4810121092 188530.2889392854 -84.62349999999999 38.498 +6318 6551 -80.90650000000001 40.262 2413229.437662917 827824.3807957403 +6551 4326 2413229.437662917 827824.3807957403 -80.90650000000001 40.261999999999986 +6318 6552 -98.71000000000001 36.14 536101.1714221651 126719.8429846217 +6552 4326 536101.1714221651 126719.8429846217 -98.71000000000001 36.14000000000001 +6318 6552 -102.571 35.357 184677.13744587317 49389.48967290248 +6552 4326 184677.13744587317 49389.48967290248 -102.571 35.357000000000006 +6318 6552 -94.849 36.923 880718.1162748237 217928.31690933244 +6552 4326 880718.1162748237 217928.31690933244 -94.849 36.92300000000001 +6318 6553 -98.71000000000001 36.14 1758858.593240887 415746.68485871307 +6553 4326 1758858.593240887 415746.68485871307 -98.71000000000001 36.14000000000001 +6318 6553 -102.571 35.357 605894.9084370023 162038.6840351809 +6553 4326 605894.9084370023 162038.6840351809 -102.571 35.357000000000006 +6318 6553 -94.849 36.923 2889489.353144984 714986.4863933682 +6553 4326 2889489.353144984 714986.4863933682 -94.849 36.92300000000001 +6318 6554 -97.21000000000001 34.595 672465.8025262959 140231.8207822339 +6554 4326 672465.8025262959 140231.8207822339 -97.21000000000001 34.595 +6318 6554 -99.721 33.7175 440481.2346152521 43973.85878489801 +6554 4326 440481.2346152521 43973.85878489801 -99.721 33.717499999999994 +6318 6554 -94.699 35.4725 899563.148845982 242195.49149143902 +6554 4326 899563.148845982 242195.49149143902 -94.699 35.4725 +6318 6555 -97.21000000000001 34.595 2206248.2204550225 460077.23201637913 +6555 4326 2206248.2204550225 460077.23201637913 -97.21000000000001 34.595 +6318 6555 -99.721 33.7175 1445145.5172335398 144270.90169678623 +6555 4326 1445145.5172335398 144270.90169678623 -99.721 33.717499999999994 +6318 6555 -94.699 35.4725 2951316.764172193 794603.0416681629 +6555 4326 2951316.764172193 794603.0416681629 -94.699 35.4725 +6318 6556 -120.535 44.12 397199.10422283644 263312.33359227417 +6556 4326 397199.10422283644 263312.33359227417 -120.53500000000001 44.11999999999999 +6318 6556 -124.1935 42.193999999999996 94902.84668831312 56206.91245575882 +6556 4326 94902.84668831312 56206.91245575882 -124.19350000000001 42.19400000000001 +6318 6556 -116.8765 46.046 680436.0888007582 483525.7810091159 +6556 4326 680436.0888007582 483525.7810091159 -116.87649999999998 46.04600000000001 +6318 6557 -120.535 44.12 1303146.667392508 863885.6088985372 +6557 4326 1303146.667392508 863885.6088985372 -120.53500000000001 44.11999999999999 +6318 6557 -124.1935 42.193999999999996 311361.04556008236 184405.88076036357 +6557 4326 311361.04556008236 184405.88076036357 -124.19350000000001 42.19400000000001 +6318 6557 -116.8765 46.046 2232401.8661389705 1586370.672602086 +6557 4326 2232401.8661389705 1586370.672602086 -116.87649999999998 46.04600000000001 +6318 6558 -120.32 45.105000000000004 2514164.9883181253 159845.1412173828 +6558 4326 2514164.9883181253 159845.1412173828 -120.32 45.105000000000004 +6318 6558 -123.785 44.0655 2236864.6381944637 49671.3154349094 +6558 4326 2236864.6381944637 49671.3154349094 -123.78500000000001 44.06550000000001 +6318 6558 -116.855 46.1445 2781533.537177242 281708.5817855314 +6558 4326 2781533.537177242 281708.5817855314 -116.855 46.1445 +6318 6559 -120.32 45.105000000000004 8248572.7967865 524426.316329996 +6559 4326 8248572.7967865 524426.316329996 -120.32 45.105000000000004 +6318 6559 -123.785 44.0655 7338794.745199684 162963.63331663187 +6559 4326 7338794.745199684 162963.63331663187 -123.78500000000001 44.06550000000001 +6318 6559 -116.855 46.1445 9125766.198555255 924240.7538895387 +6559 4326 9125766.198555255 924240.7538895387 -116.855 46.1445 +6318 6560 -120.75 43.269999999999996 1479706.3814610983 178132.5497103679 +6560 4326 1479706.3814610983 178132.5497103679 -120.75 43.269999999999996 +6318 6560 -124.21499999999999 42.108999999999995 1192816.3476943534 55951.90213787231 +6560 4326 1192816.3476943534 55951.90213787231 -124.21499999999999 42.10900000000001 +6318 6560 -117.28500000000001 44.431000000000004 1755961.4123870605 312011.62286356714 +6560 4326 1755961.4123870605 312011.62286356714 -117.28500000000001 44.431000000000004 +6318 6561 -120.75 43.269999999999996 4854679.729683393 584424.3756901834 +6561 4326 4854679.729683393 584424.3756901834 -120.75 43.269999999999996 +6318 6561 -124.21499999999999 42.108999999999995 3913439.4614197942 183569.23273580152 +6561 4326 3913439.4614197942 183569.23273580152 -124.21499999999999 42.10900000000001 +6318 6561 -117.28500000000001 44.431000000000004 5761028.256343373 1023660.1799985798 +6561 4326 5761028.256343373 1023660.1799985798 -117.28500000000001 44.431000000000004 +6318 6562 -77.61500000000001 41.565 611260.251203743 155299.796259382 +6562 4326 611260.251203743 155299.796259382 -77.61500000000001 41.56500000000001 +6318 6562 -80.2385 40.6965 389693.64377760055 61862.05383360608 +6562 4326 389693.64377760055 61862.05383360608 -80.2385 40.6965 +6318 6562 -74.9915 42.4335 826973.6695968212 255374.7277154768 +6562 4326 826973.6695968212 255374.7277154768 -74.9915 42.43350000000001 +6318 6563 -77.61500000000001 41.565 2005443.0074909471 509512.7482276559 +6563 4326 2005443.0074909471 509512.7482276559 -77.61500000000001 41.56500000000001 +6318 6563 -80.2385 40.6965 1278519.8962936779 202959.08828575598 +6563 4326 1278519.8962936779 202959.08828575598 -80.2385 40.6965 +6318 6563 -74.9915 42.4335 2713162.781002238 837841.9191798603 +6563 4326 2713162.781002238 837841.9191798603 -74.9915 42.43350000000001 +6318 6564 -77.625 40.445 610604.1860345764 123441.44619973708 +6564 4326 610604.1860345764 123441.44619973708 -77.625 40.445 +6318 6564 -80.2395 39.783500000000004 386764.6591716248 52990.14482360457 +6564 4326 386764.6591716248 52990.14482360457 -80.2395 39.783500000000025 +6318 6564 -75.0105 41.1065 830085.7794705472 200460.87468017207 +6564 4326 830085.7794705472 200460.87468017207 -75.0105 41.1065 +6318 6565 -77.625 40.445 2003290.5670151063 404990.8114069708 +6565 4326 2003290.5670151063 404990.8114069708 -77.625 40.445 +6318 6565 -80.2395 39.783500000000004 1268910.3859655724 173851.8334754427 +6565 4326 1268910.3859655724 173851.8334754427 -80.2395 39.783500000000025 +6318 6565 -75.0105 41.1065 2723373.094812954 657678.7196798646 +6565 4326 2723373.094812954 657678.7196798646 -75.0105 41.1065 +6318 6566 -66.24000000000001 18.095 220463.90309500165 228972.23173933866 +6566 4326 220463.90309500165 228972.23173933866 -66.24000000000001 18.09500000000005 +6318 6566 -67.797 17.6675 55307.87163008732 182184.18422951712 +6566 4326 55307.87163008732 182184.18422951712 -67.797 17.667500000000008 +6318 6566 -64.683 18.5225 384813.8289925946 277161.9505647868 +6566 4326 384813.8289925946 277161.9505647868 -64.683 18.522500000000022 +6318 6567 -71.465 41.575 102918.97351274508 54604.88640063912 +6567 4326 102918.97351274508 54604.88640063912 -71.465 41.57500000000001 +6318 6567 -71.8115 41.1745 73861.27843044055 10171.357680009327 +6567 4326 73861.27843044055 10171.357680009327 -71.8115 41.174499999999995 +6318 6567 -71.1185 41.975500000000004 131619.5144680358 99157.52522727985 +6567 4326 131619.5144680358 99157.52522727985 -71.1185 41.975500000000004 +6318 6568 -71.465 41.575 337659.99889973126 179149.53146609687 +6568 4326 337659.99889973126 179149.53146609687 -71.465 41.57500000000001 +6318 6568 -71.8115 41.1745 242326.54428387046 33370.5293218306 +6568 4326 242326.54428387046 33370.5293218306 -71.8115 41.174499999999995 +6318 6568 -71.1185 41.975500000000004 431821.6903505476 325319.31401650066 +6568 4326 431821.6903505476 325319.31401650066 -71.1185 41.975500000000004 +6318 6569 -80.94 33.629999999999995 615165.8566942128 199244.85107013452 +6569 4326 615165.8566942128 199244.85107013452 -80.94 33.63 +6318 6569 -83.118 32.208 409907.55380288104 43600.556483864246 +6569 4326 409907.55380288104 43600.556483864246 -83.118 32.208000000000006 +6318 6569 -78.762 35.052 813775.0055652673 359176.8093884141 +6569 4326 813775.0055652673 359176.8093884141 -78.762 35.052 +6318 6570 -80.94 33.629999999999995 2018260.684692299 653690.4562668455 +6570 4326 2018260.684692299 653690.4562668455 -80.94 33.63 +6318 6570 -83.118 32.208 1344841.0557837302 143046.4451570349 +6570 4326 1344841.0557837302 143046.4451570349 -83.118 32.208000000000006 +6318 6570 -78.762 35.052 2669865.503823055 1178401.605605033 +6570 4326 2669865.503823055 1178401.605605033 -78.762 35.052 +6318 6571 -100.25999999999999 45.045 579517.1582990598 134676.18923571636 +6571 4326 579517.1582990598 134676.18923571636 -100.25999999999999 45.04500000000001 +6318 6571 -103.689 44.2305 305357.195159725 50848.92774272213 +6571 4326 305357.195159725 50848.92774272213 -103.689 44.23049999999999 +6318 6571 -96.831 45.859500000000004 846048.4784497482 229981.1518169474 +6571 4326 846048.4784497482 229981.1518169474 -96.831 45.859500000000004 +6318 6572 -100.25999999999999 45.045 1901299.2101861658 441850.1308508461 +6572 4326 1901299.2101861658 441850.1308508461 -100.25999999999999 45.04500000000001 +6318 6572 -103.689 44.2305 1001826.0644531978 166826.8571025809 +6572 4326 1001826.0644531978 166826.8571025809 -103.689 44.23049999999999 +6318 6572 -96.831 45.859500000000004 2775744.0497138826 754529.8289194349 +6572 4326 2775744.0497138826 754529.8289194349 -96.831 45.859500000000004 +6318 6573 -100.245 43.635 607127.5043806202 144607.50092678124 +6573 4326 607127.5043806202 144607.50092678124 -100.245 43.63499999999999 +6318 6573 -103.6785 42.595499999999994 325505.86952982674 34653.81023614486 +6573 4326 325505.86952982674 34653.81023614486 -103.6785 42.595499999999994 +6318 6573 -96.81150000000001 44.6745 879191.0064309249 266024.15353638545 +6573 4326 879191.0064309249 266024.15353638545 -96.81150000000001 44.6745 +6318 6574 -100.245 43.635 1991884.1539554184 474433.10929061484 +6574 4326 1991884.1539554184 474433.10929061484 -100.245 43.63499999999999 +6318 6574 -103.6785 42.595499999999994 1067930.5069491067 113693.37574975195 +6574 4326 1067930.5069491067 113693.37574975195 -103.6785 42.595499999999994 +6318 6574 -96.81150000000001 44.6745 2884479.1602654597 872780.910393958 +6574 4326 2884479.1602654597 872780.910393958 -96.81150000000001 44.6745 +6318 6575 -85.98 35.83 601807.0427761595 166053.81335326185 +6575 4326 601807.0427761595 166053.81335326185 -85.98 35.830000000000005 +6318 6575 -89.87700000000001 35.065 246434.998258424 88183.71880346097 +6575 4326 246434.998258424 88183.71880346097 -89.87700000000001 35.06499999999998 +6318 6575 -82.083 36.595 950418.3889824077 257951.77285579516 +6575 4326 950418.3889824077 257951.77285579516 -82.083 36.59500000000002 +6318 6576 -85.98 35.83 1974428.6061747835 544794.8859764934 +6576 4326 1974428.6061747835 544794.8859764934 -85.98 35.830000000000005 +6318 6576 -89.87700000000001 35.065 808512.1567861795 289316.0841076882 +6576 4326 808512.1567861795 289316.0841076882 -89.87700000000001 35.06499999999998 +6318 6576 -82.083 36.595 3118164.3311864496 846296.7747777214 +6576 4326 3118164.3311864496 846296.7747777214 -82.083 36.59500000000002 +6318 6577 -100.08 31.025000000000002 724185.2196589834 3150605.5484253517 +6577 4326 724185.2196589834 3150605.5484253517 -100.08 31.025000000000023 +6318 6577 -106.002 29.904500000000002 152730.7257107871 3040313.3125654417 +6577 4326 152730.7257107871 3040313.3125654417 -106.002 29.90449999999999 +6318 6577 -94.158 32.145500000000006 1282351.2308671223 3290980.4998480678 +6577 4326 1282351.2308671223 3290980.4998480678 -94.158 32.14550000000003 +6318 6578 -100.08 31.025000000000002 2375931.0078311823 10336611.703458842 +6578 4326 2375931.0078311823 10336611.703458842 -100.08 31.025000000000023 +6318 6578 -106.002 29.904500000000002 501084.05560280825 9974761.259641787 +6578 4326 501084.05560280825 9974761.259641787 -106.002 29.90449999999999 +6318 6578 -94.158 32.145500000000006 4207180.662936552 10797158.523251537 +6578 4326 4207180.662936552 10797158.523251537 -94.158 32.14550000000003 +6318 6579 -100.08 31.165 1492389.0677626291 7449402.239574473 +6579 4326 1492389.0677626291 7449402.239574473 -100.08 31.165000000000003 +6318 6579 -106.002 26.3635 900399.2687562371 6933023.245166092 +6579 4326 900399.2687562371 6933023.245166092 -106.002 26.363499999999995 +6318 6579 -94.158 35.966499999999996 2027403.945952803 7996433.036043674 +6579 4326 2027403.945952803 7996433.036043674 -94.158 35.966499999999996 +6318 6580 -100.08 31.165 1492389.085956109 6467970.2720677685 +6580 4326 1492389.085956109 6467970.2720677685 -100.08 31.165000000000003 +6318 6580 -106.002 26.3635 900372.0614798032 5952594.840398136 +6580 4326 900372.0614798032 5952594.840398136 -106.002 26.363500000000013 +6318 6580 -94.158 35.966499999999996 2027379.438878832 7013937.4695115145 +6580 4326 2027379.438878832 7013937.4695115145 -94.158 35.96649999999998 +6318 6581 -101.50999999999999 35.4 199091.66389534375 1155311.273626022 +6581 4326 199091.66389534375 1155311.273626022 -101.50999999999999 35.4 +6318 6581 -102.878 34.41 73304.61790902242 1046368.8035420016 +6581 4326 73304.61790902242 1046368.8035420016 -102.878 34.40999999999999 +6318 6581 -100.142 36.39 321839.4199389363 1265990.8998509285 +6581 4326 321839.4199389363 1265990.8998509285 -100.142 36.38999999999999 +6318 6582 -101.50999999999999 35.4 653186.5676299729 3790383.7365547083 +6582 4326 653186.5676299729 3790383.7365547083 -101.50999999999999 35.4 +6318 6582 -102.878 34.41 240500.23425651703 3432961.649287385 +6582 4326 240500.23425651703 3432961.649287385 -102.878 34.40999999999999 +6318 6582 -100.142 36.39 1055901.4972496596 4153505.143594256 +6582 4326 1055901.4972496596 4153505.143594256 -100.142 36.38999999999999 +6318 6583 -98.535 33.15 596735.0946008511 2164487.199652242 +6583 4326 596735.0946008511 2164487.199652242 -98.535 33.149999999999984 +6318 6583 -102.61649999999999 31.863 210507.96670704812 2029404.6807503384 +6583 4326 210507.96670704812 2029404.6807503384 -102.61649999999999 31.863000000000007 +6318 6583 -94.4535 34.437 971879.2800386127 2314398.7444936717 +6583 4326 971879.2800386127 2314398.7444936717 -94.45350000000002 34.437000000000005 +6318 6584 -98.535 33.15 1957788.3895362925 7101321.754525732 +6584 4326 1957788.3895362925 7101321.754525732 -98.535 33.149999999999984 +6318 6584 -102.61649999999999 31.863 690641.5541047071 6658138.523761735 +6584 4326 690641.5541047071 6658138.523761735 -102.61649999999999 31.863000000000007 +6318 6584 -94.4535 34.437 3188573.937926682 7593156.547892988 +6584 4326 3188573.937926682 7593156.547892988 -94.45350000000002 34.437000000000005 +6318 6585 -98.525 27.02 297519.3277685372 5149938.002474285 +6585 4326 297519.3277685372 5149938.002474285 -98.525 27.01999999999998 +6318 6585 -100.0325 25.948999999999998 146497.67705421898 5032213.809542949 +6585 4326 146497.67705421898 5032213.809542949 -100.0325 25.949000000000012 +6318 6585 -97.0175 28.091 445706.4161671256 5269468.382250638 +6585 4326 445706.4161671256 5269468.382250638 -97.0175 28.091 +6318 6586 -98.525 27.02 976111.3278539426 16896088.26345105 +6586 4326 976111.3278539426 16896088.26345105 -98.525 27.02 +6318 6586 -100.0325 25.948999999999998 480634.4621353835 16509854.80714216 +6586 4326 480634.4621353835 16509854.80714216 -100.0325 25.94900000000002 +6318 6586 -97.0175 28.091 1462288.4670416447 17288247.517767303 +6586 4326 1462288.4670416447 17288247.517767303 -97.0175 28.091 +6318 6587 -99.38 29.225 563058.7354705705 4154293.521203296 +6587 4326 563058.7354705705 4154293.521203296 -99.38 29.225000000000026 +6318 6587 -104.438 27.924500000000002 64841.30729672348 4022548.884762567 +6587 4326 64841.30729672348 4022548.884762567 -104.43800000000002 27.924499999999995 +6318 6587 -94.322 30.5255 1048881.5104492614 4307362.930045891 +6587 4326 1048881.5104492614 4307362.930045891 -94.322 30.5255 +6318 6588 -99.38 29.225 1847301.8679563636 13629544.66048115 +6588 4326 1847301.8679563636 13629544.66048115 -99.38 29.225000000000026 +6318 6588 -104.438 27.924500000000002 212733.5223560003 13197312.465758525 +6588 4326 212733.5223560003 13197312.465758525 -104.43800000000002 27.924499999999995 +6318 6588 -94.322 30.5255 3441205.422198952 14131739.879325563 +6588 4326 3441205.422198952 14131739.879325563 -94.322 30.525500000000008 +6318 6589 -72.47 43.875 502411.1456667513 152752.52373386305 +6589 4326 502411.1456667513 152752.52373386305 -72.47 43.87500000000001 +6318 6589 -73.343 42.835499999999996 431080.14350458205 37612.8404625171 +6589 4326 431080.14350458205 37612.8404625171 -73.343 42.835499999999996 +6318 6589 -71.597 44.914500000000004 571301.9692705146 268653.87743555265 +6589 4326 571301.9692705146 268653.87743555265 -71.597 44.91450000000001 +6318 6590 -72.47 43.875 1648327.2337749999 501155.5716168491 +6590 4326 1648327.2337749999 501155.5716168491 -72.47 43.87500000000001 +6318 6590 -73.343 42.835499999999996 1414302.104181283 123401.46075077487 +6590 4326 1414302.104181283 123401.46075077487 -73.343 42.835499999999996 +6318 6590 -71.597 44.914500000000004 1874346.544215013 881408.5962198091 +6590 4326 1874346.544215013 881408.5962198091 -71.597 44.91450000000001 +6318 6591 -79.495 38.0 439.0625047402158 221966.39757750247 +6591 4326 439.0625047402158 221966.39757750247 -79.495 37.99999999999999 +6318 6591 -83.26150000000001 36.686 -336141.30723252974 82978.97219532465 +6591 4326 -336141.30723252974 82978.97219532465 -83.26150000000001 36.68599999999999 +6318 6591 -75.7285 39.314 325151.16044344066 374431.10978461226 +6591 4326 325151.16044344066 374431.10978461226 -75.7285 39.31400000000002 +6318 6592 -78.285 38.615 3518724.203106891 2105285.4939807146 +6592 4326 3518724.203106891 2105285.4939807146 -78.285 38.614999999999995 +6318 6592 -79.88250000000001 37.8545 3378332.274917174 2021765.3758821723 +6592 4326 3378332.274917174 2021765.3758821723 -79.88250000000001 37.85450000000001 +6318 6592 -76.6875 39.3755 3656172.5417433567 2191230.607915427 +6592 4326 3656172.5417433567 2191230.607915427 -76.6875 39.3755 +6318 6593 -78.285 38.615 11544347.656693192 6907090.8251683945 +6593 4326 11544347.656693192 6907090.8251683945 -78.285 38.614999999999995 +6318 6593 -79.88250000000001 37.8545 11083745.13895743 6633075.237706761 +6593 4326 11083745.13895743 6633075.237706761 -79.88250000000001 37.85450000000001 +6318 6593 -76.6875 39.3755 11995292.747702997 7189062.41980253 +6593 4326 11995292.747702997 7189062.41980253 -76.6875 39.3755 +6318 6594 -79.495 37.41 3411917.6086140587 1119947.2032969252 +6594 4326 3411917.6086140587 1119947.2032969252 -79.495 37.41000000000001 +6318 6594 -83.26150000000001 36.627 3074278.1812144495 1043328.2241050153 +6594 4326 3074278.1812144495 1043328.2241050153 -83.26150000000001 36.626999999999995 +6318 6594 -75.7285 38.193 3742765.366771591 1209952.5513936281 +6594 4326 3742765.366771591 1209952.5513936281 -75.7285 38.19300000000001 +6318 6595 -79.495 37.41 11193933.021261292 3674360.1158166635 +6595 4326 11193933.021261292 3674360.1158166635 -79.495 37.41000000000001 +6318 6595 -83.26150000000001 36.627 10086194.333201073 3422986.0149178724 +6595 4326 10086194.333201073 3422986.0149178724 -83.26150000000001 36.626999999999995 +6318 6595 -75.7285 38.193 12279389.374483127 3969652.6620305968 +6595 4326 12279389.374483127 3969652.6620305968 -75.7285 38.19300000000001 +6318 6596 -120.905 48.065 494658.8782095999 118411.46677224239 +6596 4326 494658.8782095999 118411.46677224239 -120.90500000000002 48.065 +6318 6596 -124.4015 47.1785 229601.1611486969 26116.106779398477 +6596 4326 229601.1611486969 26116.106779398477 -124.4015 47.178500000000014 +6318 6596 -117.4085 48.951499999999996 750773.7610210045 222566.60307881774 +6596 4326 750773.7610210045 222566.60307881774 -117.4085 48.951499999999996 +6318 6597 -120.905 48.065 1622893.336592662 388488.2872352653 +6597 4326 1622893.336592662 388488.2872352653 -120.90500000000002 48.065 +6318 6597 -124.4015 47.1785 753283.1432020159 85682.59365874318 +6597 4326 753283.1432020159 85682.59365874318 -124.4015 47.178500000000014 +6318 6597 -117.4085 48.951499999999996 2463163.581283079 730203.9302677546 +6597 4326 2463163.581283079 730203.9302677546 -117.4085 48.951499999999996 +6318 6598 -120.655 46.575 488119.19059687364 138022.62988068728 +6598 4326 488119.19059687364 138022.62988068728 -120.655 46.575 +6318 6598 -124.02550000000001 45.643499999999996 225232.76586336124 40617.24264164518 +6598 4326 225232.76586336124 40617.24264164518 -124.02550000000001 45.64349999999999 +6318 6598 -117.2845 47.5065 742180.9642298741 246499.98165656062 +6598 4326 742180.9642298741 246499.98165656062 -117.2845 47.5065 +6318 6599 -120.655 46.575 1601437.7114832425 452829.2448668882 +6599 4326 1601437.7114832425 452829.2448668882 -120.655 46.575 +6318 6599 -124.02550000000001 45.643499999999996 738951.1663367104 133258.40356679758 +6599 4326 738951.1663367104 133258.40356679758 -124.02550000000001 45.64349999999999 +6318 6599 -117.2845 47.5065 2434972.0471441783 808725.3564848994 +6599 4326 2434972.0471441783 808725.3564848994 -117.2845 47.5065 +6318 6600 -79.74000000000001 39.7 579417.2957110632 133248.46017368144 +6600 4326 579417.2957110632 133248.46017368144 -79.74000000000001 39.70000000000001 +6318 6600 -81.558 38.854 421367.18441514135 41346.53591224026 +6600 4326 421367.18441514135 41346.53591224026 -81.558 38.854000000000006 +6318 6600 -77.922 40.546 733674.570779342 228331.12197136818 +6600 4326 733674.570779342 228331.12197136818 -77.922 40.546000000000014 +6318 6601 -79.74000000000001 39.7 1900971.5776787135 437165.98975315323 +6601 4326 1900971.5776787135 437165.98975315323 -79.74000000000001 39.70000000000001 +6318 6601 -81.558 38.854 1382435.5042020096 135651.0932387416 +6601 4326 1382435.5042020096 135651.0932387416 -81.558 38.854000000000006 +6318 6601 -77.922 40.546 2407063.9876318914 749116.3560010638 +6601 4326 2407063.9876318914 749116.3560010638 -77.922 40.546000000000014 +6318 6602 -80.85 38.185 613140.720628098 131531.86202500196 +6602 4326 613140.720628098 131531.86202500196 -80.85 38.18500000000001 +6318 6602 -82.47 37.298500000000004 469665.8086919263 34164.20925580738 +6602 4326 469665.8086919263 34164.20925580738 -82.47 37.298500000000004 +6318 6602 -79.23 39.0715 753171.9602462114 231389.43782599163 +6602 4326 753171.9602462114 231389.43782599163 -79.23 39.07150000000001 +6318 6603 -80.85 38.185 2011612.514260685 431534.1173270273 +6603 4326 2011612.514260685 431534.1173270273 -80.85 38.18500000000001 +6318 6603 -82.47 37.298500000000004 1540895.2406834285 112087.07653342806 +6603 4326 1540895.2406834285 112087.07653342806 -82.47 37.298500000000004 +6318 6603 -79.23 39.0715 2471031.672907779 759150.1806007742 +6603 4326 2471031.672907779 759150.1806007742 -79.23 39.07150000000001 +6318 6605 -89.57 44.89 2079939.3338098514 385515.57975344936 +6605 4326 2079939.3338098514 385515.57975344936 -89.57000000000001 44.88999999999999 +6318 6605 -92.558 44.071 1296268.2423315719 97233.35493690065 +6605 4326 1296268.2423315719 97233.35493690065 -92.558 44.071 +6318 6605 -86.582 45.708999999999996 2841490.0879038563 702216.019093216 +6605 4326 2841490.0879038563 702216.019093216 -86.582 45.708999999999996 +6318 6606 -90.47 46.34 563817.9183929065 130517.46296681912 +6606 4326 563817.9183929065 130517.46296681912 -90.47 46.339999999999996 +6318 6606 -92.648 45.467 392950.88681591925 36832.035967861266 +6606 4326 392950.88681591925 36832.035967861266 -92.648 45.467 +6318 6606 -88.292 47.213 729391.0452604456 228851.15030724084 +6606 4326 729391.0452604456 228851.15030724084 -88.292 47.212999999999994 +6318 6607 -90.47 46.34 1849792.6205940612 428206.0430836391 +6607 4326 1849792.6205940612 428206.0430836391 -90.47 46.339999999999996 +6318 6607 -92.648 45.467 1289206.367828562 120839.77133789152 +6607 4326 1289206.367828562 120839.77133789152 -92.648 45.467 +6318 6607 -88.292 47.213 2393010.4543253123 750822.4822996728 +6607 4326 2393010.4543253123 750822.4822996728 -88.292 47.212999999999994 +6318 6608 -89.19 43.405 665607.2736576359 156400.65287319338 +6608 4326 665607.2736576359 156400.65287319338 -89.19 43.404999999999994 +6318 6608 -91.206 42.5725 500982.45158363064 64316.57338896093 +6608 4326 500982.45158363064 64316.57338896093 -91.206 42.57250000000001 +6318 6608 -87.174 44.2375 825722.2152976134 252402.33148048513 +6608 4326 825722.2152976134 252402.33148048513 -87.174 44.23750000000001 +6318 6609 -89.19 43.405 2183746.5303250942 513124.4753014687 +6609 4326 2183746.5303250942 513124.4753014687 -89.19 43.404999999999994 +6318 6609 -91.206 42.5725 1643639.9265706283 211011.95786028268 +6609 4326 1643639.9265706283 211011.95786028268 -91.206 42.57250000000001 +6318 6609 -87.174 44.2375 2709056.968022254 828089.9825322251 +6609 4326 2709056.968022254 828089.9825322251 -87.174 44.23750000000001 +6318 6610 -89.57 44.894999999999996 553952.4209604628 491376.26325869653 +6610 4326 553952.4209604628 491376.26325869653 -89.57000000000001 44.895 +6318 6610 -92.558 42.7215 310556.80412441783 253061.62620366085 +6610 4326 310556.80412441783 253061.62620366085 -92.558 42.721500000000006 +6318 6610 -86.582 47.0685 779512.1515750884 738447.0279873684 +6610 4326 779512.1515750884 738447.0279873684 -86.582 47.068499999999986 +6318 6611 -105.19 43.0 198097.49632915662 277654.042474903 +6611 4326 198097.49632915662 277654.042474903 -105.19 43.0 +6318 6611 -106.216 41.191 111974.33693520293 77262.27621200448 +6611 4326 111974.33693520293 77262.27621200448 -106.216 41.19100000000001 +6318 6611 -104.164 44.809 279314.3848616645 479129.0114329825 +6611 4326 279314.3848616645 479129.0114329825 -104.164 44.809 +6318 6612 -105.19 43.0 649924.8692399081 910936.637686411 +6612 4326 649924.8692399081 910936.637686411 -105.19 43.0 +6318 6612 -106.216 41.191 367369.1371282449 253484.6512055514 +6612 4326 367369.1371282449 253484.6512055514 -106.216 41.19100000000001 +6318 6612 -104.164 44.809 916383.9443669778 1571942.431676377 +6612 4326 916383.9443669778 1571942.431676377 -104.164 44.809 +6318 6613 -107.315 43.0 401494.82431166724 377653.9413779187 +6613 4326 401494.82431166724 377653.9413779187 -107.315 43.0 +6318 6613 -108.49849999999999 41.191 302257.2284902079 177385.96133674524 +6613 4326 302257.2284902079 177385.96133674524 -108.49849999999999 41.191 +6318 6613 -106.1315 44.809 495069.171304143 579342.6387105695 +6613 4326 495069.171304143 579342.6387105695 -106.1315 44.809 +6318 6614 -107.315 43.0 1317237.6027291953 1239019.6393040551 +6614 4326 1317237.6027291953 1239019.6393040551 -107.315 43.0 +6318 6614 -108.49849999999999 41.191 991655.5904382907 581973.7747856386 +6614 4326 991655.5904382907 581973.7747856386 -108.49849999999999 41.191 +6318 6614 -106.1315 44.809 1624239.4394870093 1900726.6404695935 +6614 4326 1624239.4394870093 1900726.6404695935 -106.1315 44.809 +6318 6615 -110.05000000000001 42.83 802725.3431980808 358770.01389699115 +6615 4326 802725.3431980808 358770.01389699115 -110.05000000000001 42.83 +6318 6615 -110.959 41.174 726523.9197376369 175213.1974278146 +6615 4326 726523.9197376369 175213.1974278146 -110.959 41.174 +6318 6615 -109.141 44.486000000000004 874956.6699701425 543180.753202716 +6615 4326 874956.6699701425 543180.753202716 -109.141 44.48600000000001 +6318 6616 -110.05000000000001 42.83 2633608.0635090373 1177064.6205603785 +6616 4326 2633608.0635090373 1177064.6205603785 -110.05000000000001 42.83 +6318 6616 -110.959 41.174 2383603.893372564 574845.2985277551 +6616 4326 2383603.893372564 574845.2985277551 -110.959 41.174 +6318 6616 -109.141 44.486000000000004 2870587.0080937096 1782085.5210992442 +6616 4326 2870587.0080937096 1782085.5210992442 -109.141 44.48600000000001 +6318 6617 -109.28 43.0 556785.943962579 277790.0915190472 +6617 4326 556785.943962579 277790.0915190472 -109.28000000000002 43.0 +6318 6617 -110.882 41.191 421148.2266105671 78923.38133064652 +6617 4326 421148.2266105671 78923.38133064652 -110.882 41.191 +6318 6617 -107.678 44.809 684798.8953224984 479198.9978457161 +6617 4326 684798.8953224984 479198.9978457161 -107.678 44.809000000000005 +6318 6618 -109.28 43.0 1826721.8844838948 911382.9919254076 +6618 4326 1826721.8844838948 911382.9919254076 -109.28000000000002 43.0 +6318 6618 -110.882 41.191 1381717.140138169 258934.4602489628 +6618 4326 1381717.140138169 258934.4602489628 -110.882 41.191 +6318 6618 -107.678 44.809 2246711.0424038973 1572172.0454321536 +6618 4326 2246711.0424038973 1572172.0454321536 -107.678 44.809000000000005 +6318 6619 -111.545 39.785 496145.6404933844 2161162.6996333404 +6619 4326 496145.6404933844 2161162.6996333404 -111.545 39.785000000000004 +6318 6619 -113.7995 38.6195 299737.81075454154 2034346.1327566376 +6619 4326 299737.81075454154 2034346.1327566376 -113.79950000000001 38.61950000000001 +6318 6619 -109.29050000000001 40.9505 686033.2712166447 2292873.457553067 +6619 4326 686033.2712166447 2292873.457553067 -109.29050000000001 40.950500000000005 +6318 6620 -111.54 41.28 496649.01927216403 1105128.0559982655 +6620 4326 496649.01927216403 1105128.0559982655 -111.54 41.28000000000001 +6318 6620 -113.79 40.623 306256.0472603298 1034720.2739077386 +6620 4326 306256.0472603298 1034720.2739077386 -113.79 40.62299999999999 +6318 6620 -109.29 41.937 683266.1531509589 1180426.6984195628 +6620 4326 683266.1531509589 1180426.6984195628 -109.29 41.937000000000005 +6318 6621 -111.545 37.785 496036.2211105266 3124117.453606073 +6621 4326 496036.2211105266 3124117.453606073 -111.545 37.785000000000004 +6318 6621 -113.7995 37.069500000000005 295518.8854022882 3047222.248513894 +6621 4326 295518.8854022882 3047222.248513894 -113.79950000000001 37.069500000000026 +6318 6621 -109.29050000000001 38.500499999999995 692727.1993279501 3205811.513840428 +6621 4326 692727.1993279501 3205811.513840428 -109.29050000000001 38.5005 +8237 6622 -68.475 53.805 1634.8564338814983 1086417.5610523205 +8237 6622 -78.71249999999999 45.871500000000005 -790443.0169708299 264970.4427806088 +8237 6622 -58.2375 61.7385 542940.5911125129 2006118.281500356 +4269 6623 -68.475 53.805 1634.8124973975605 1094272.9550778144 +6623 4326 1634.8124973975605 1094272.9550778144 -68.475 53.80499999999999 +4269 6623 -78.71249999999999 45.871500000000005 -790475.4158596823 263484.59089336975 +6623 4326 -790475.4158596823 263484.59089336975 -78.71249999999999 45.87149999999999 +4269 6623 -58.2375 61.7385 543380.6932897941 2019467.9869383355 +6623 4326 543380.6932897941 2019467.9869383355 -58.237500000000004 61.73849999999997 +8237 6624 -68.475 53.805 1634.8124973975605 1094272.9550778144 +8237 6624 -78.71249999999999 45.871500000000005 -790475.4158596823 263484.59089336975 +8237 6624 -58.2375 61.7385 543380.6932897941 2019467.9869383355 +6318 6625 -111.545 39.785 1627771.1555520452 7090414.623747053 +6625 4326 1627771.1555520452 7090414.623747053 -111.545 39.785000000000004 +6318 6625 -113.7995 38.6195 983389.8008171918 6674350.603919069 +6625 4326 983389.8008171918 6674350.603919069 -113.79950000000001 38.61950000000001 +6318 6625 -109.29050000000001 40.9505 2250760.8240166083 7522535.668688687 +6625 4326 2250760.8240166083 7522535.668688687 -109.29050000000001 40.950500000000005 +6318 6626 -111.54 41.28 1629422.6574287582 3625740.963687643 +6626 4326 1629422.6574287582 3625740.963687643 -111.54 41.28000000000001 +6318 6626 -113.79 40.623 1004775.0484199319 3394744.7652789727 +6626 4326 1004775.0484199319 3394744.7652789727 -113.79 40.62299999999999 +6318 6626 -109.29 41.937 2241682.370829438 3872783.259698183 +6626 4326 2241682.370829438 3872783.259698183 -109.29 41.937000000000005 +6318 6627 -111.545 37.785 1627412.1687934527 10249708.679039259 +6627 4326 1627412.1687934527 10249708.679039259 -111.545 37.785000000000004 +6318 6627 -113.7995 37.069500000000005 969548.2098906735 9997428.326999336 +6627 4326 969548.2098906735 9997428.326999336 -113.79950000000001 37.069500000000026 +6318 6627 -109.29050000000001 38.500499999999995 2272722.4864951163 10517733.274991473 +6627 4326 2272722.4864951163 10517733.274991473 -109.29050000000001 38.5005 +6322 6628 -155.42000000000002 19.6 508392.43257363234 84865.06216114241 +6322 6628 -156.03199999999998 18.943 443966.8337234195 12223.129253569174 +6322 6628 -154.80800000000002 20.256999999999998 572296.4680864818 157743.97569391556 +6322 6629 -156.645 20.855 502254.7853232328 57752.88297264607 +6322 6629 -157.2885 20.4905 435132.0824620548 17522.483149056337 +6322 6629 -156.00150000000002 21.2195 569054.8823151813 98253.00576796383 +6322 6630 -157.97000000000003 21.475 503109.1061699619 34139.0008563712 +6322 6630 -158.294 21.2275 469479.4657924552 6763.726930458374 +6322 6630 -157.64600000000002 21.7225 536625.296198369 61584.71779271407 +6322 6631 -159.54 22.049999999999997 495871.0045759173 23991.690169427377 +6322 6631 -159.819 21.834 467021.380951611 107.96252484768765 +6322 6631 -159.261 22.266 524633.1681034412 47928.577949660204 +6322 6632 -160.145 21.9 502238.9118068838 25836.542382528078 +6322 6632 -160.2845 21.747 487810.8059930551 8899.659826272124 +6322 6632 -160.0055 22.053 516636.24116117816 42786.85294238828 +6322 6633 -157.97000000000003 21.475 1650617.1258592834 112004.37197627786 +6322 6633 -158.294 21.2275 1540283.8807207467 22190.660771012186 +6322 6633 -157.64600000000002 21.7225 1760578.1593108154 202049.1949582628 +6322 6634 -158.145 20.9 588920.6626141642 2311317.1234786245 +6322 6634 -160.08450000000002 19.649 386301.0107171199 2173002.2708532047 +6322 6634 -156.2055 22.151 788226.6273887353 2452192.3684225455 +6322 6635 -155.37 19.865000000000002 251820.14414430902 2198287.2017789544 +6322 6635 -155.937 18.9695 190717.23376067448 2100031.073463214 +6322 6635 -154.803 20.7605 312295.71302144264 2296688.9862106 +6322 6636 -169.485 -14.35 663372.1955144025 8413029.452880211 +6322 6636 -170.7405 -14.565999999999999 527953.7447252881 8389659.50634633 +6322 6636 -168.2295 -14.134 799117.6450683003 8435686.928591615 +6325 6637 144.79500000000002 13.44 104873.0712287864 193362.37181528643 +6325 6637 144.60150000000002 13.206 83903.38825029787 167478.50837094785 +6325 6637 144.9885 13.674 125802.08044039052 219263.286407388 +4743 6646 43.7 33.225 539103.0976997755 468714.0309966183 +6646 4326 539103.0976997755 468714.0309966183 43.696868481813134 33.22499023230213 +4743 6646 39.281 29.476499999999998 99275.80546255712 71666.21312576647 +6646 4326 99275.80546255712 71666.21312576647 39.27785567626176 29.476567556671988 +4743 6646 48.119 36.9735 944084.1343876927 882046.6419089702 +6646 4326 944084.1343876927 882046.6419089702 48.1158878757257 36.97346668101088 +6668 6669 129.315 30.85 -17694.12918193351 -238365.4573224007 +6669 4326 -17694.12918193351 -238365.4573224007 129.315 30.85 +6668 6669 128.28449999999998 27.349 -120262.86799967856 -625797.608309971 +6669 4326 -120262.86799967856 -625797.608309971 128.28449999999998 27.348999999999997 +6668 6669 130.34550000000002 34.351 77781.84923412865 150157.13190014462 +6669 4326 77781.84923412865 150157.13190014462 130.34550000000002 34.351 +6668 6670 130.905 32.085 -8967.662029594969 -101456.07083030338 +6670 4326 -8967.662029594969 -101456.07083030338 130.905 32.085 +6668 6670 129.87449999999998 30.3705 -108182.2569405884 -290996.2422846366 +6670 4326 -108182.2569405884 -290996.2422846366 129.87449999999998 30.370500000000007 +6668 6670 131.93550000000002 33.7995 86621.30933465871 89058.36020065419 +6670 4326 86621.30933465871 89058.36020065419 131.93550000000002 33.7995 +6668 6671 132.15 35.05 -1520.3913637287199 -105392.0498223429 +6671 4326 -1520.3913637287199 -105392.0498223429 132.15 35.04999999999999 +6668 6671 130.94400000000002 33.853 -113141.96909569166 -237490.2563337995 +6671 4326 -113141.96909569166 -237490.2563337995 130.94400000000002 33.853 +6668 6671 133.356 36.247 106890.92757591169 28060.710695339 +6671 4326 106890.92757591169 28060.710695339 133.356 36.247 +6668 6672 133.38 33.57 -11140.611022283041 63218.568582534346 +6672 4326 -11140.611022283041 63218.568582534346 133.38 33.57 +6668 6672 132.093 32.778 -131809.32105295264 -23741.600554621888 +6672 4326 -131809.32105295264 -23741.600554621888 132.093 32.778 +6668 6672 134.667 34.362 107345.53531015589 151670.45950121942 +6672 4326 107345.53531015589 151670.45950121942 134.667 34.362 +6668 6673 134.3 34.92 -3045.593094469217 -119812.46646359657 +6673 4326 -3045.593094469217 -119812.46646359657 134.3 34.92 +6668 6673 133.24699999999999 34.209 -100106.42589702891 -198144.5986105343 +6673 4326 -100106.42589702891 -198144.5986105343 133.24699999999999 34.208999999999996 +6668 6673 135.353 35.631 92355.63179588626 -40459.73391294459 +6673 4326 92355.63179588626 -40459.73391294459 135.353 35.631 +6668 6674 135.925 34.864999999999995 -6857.153430803463 -125911.42130509262 +6674 4326 -6857.153430803463 -125911.42130509262 135.925 34.864999999999995 +6668 6674 134.96650000000002 33.5465 -95976.51455007782 -271677.0789730092 +6674 4326 -95976.51455007782 -271677.0789730092 134.96650000000002 33.54650000000001 +6668 6674 136.8835 36.183499999999995 79467.67980275753 20720.988311085046 +6674 4326 79467.67980275753 20720.988311085046 136.8835 36.183499999999995 +6668 6675 137.03 36.045 -12314.13837266254 5001.316246591893 +6675 4326 -12314.13837266254 5001.316246591893 137.03 36.045 +6668 6675 136.301 34.6635 -79340.41347689279 -147924.500285497 +6675 4326 -79340.41347689279 -147924.500285497 136.301 34.6635 +6668 6675 137.75900000000001 37.4265 52423.637802688085 158450.8366085486 +6675 4326 52423.637802688085 158450.8366085486 137.75900000000001 37.4265 +6668 6676 138.615 36.56 10293.994412923666 62139.89291487604 +6676 4326 10293.994412923666 62139.89291487604 138.615 36.56 +6668 6676 137.4495 34.742 -96190.53805913808 -139055.30904895783 +6676 4326 -96190.53805913808 -139055.30904895783 137.4495 34.742000000000004 +6668 6676 139.7805 38.378 111880.68448409403 264663.23889834405 +6676 4326 111880.68448409403 264663.23889834405 139.7805 38.37800000000001 +6668 6677 139.755 33.644999999999996 -7266.047747636758 -261228.66136330654 +6677 4326 -7266.047747636758 -261228.66136330654 139.755 33.644999999999996 +6668 6677 138.5355 29.743499999999997 -125536.82383970382 -693087.5289930385 +6677 4326 -125536.82383970382 -693087.5289930385 138.5355 29.743499999999997 +6668 6677 140.9745 37.546499999999995 100837.20925875574 172215.23425634272 +6677 4326 100837.20925875574 172215.23425634272 140.9745 37.546499999999995 +6668 6678 140.815 39.655 -1573.2470265054255 -38301.81422441857 +6678 4326 -1573.2470265054255 -38301.81422441857 140.815 39.65499999999999 +6668 6678 139.6225 37.9225 -106453.60744848949 -229918.7365203989 +6678 4326 -106453.60744848949 -229918.7365203989 139.6225 37.92250000000001 +6668 6678 142.0075 41.387499999999996 98198.95234487754 154728.9910797522 +6678 4326 98198.95234487754 154728.9910797522 142.0075 41.38749999999999 +6668 6679 140.4 42.38 12352.167120475582 -179947.3434793202 +6679 4326 12352.167120475582 -179947.3434793202 140.4 42.38 +6668 6679 139.446 41.444 -67182.27670751311 -283598.84054292215 +6679 4326 -67182.27670751311 -283598.84054292215 139.44600000000003 41.443999999999996 +6668 6679 141.354 43.316 89549.91330541894 -75396.70991294812 +6679 4326 89549.91330541894 -75396.70991294812 141.354 43.316 +6668 6680 142.25 43.845 0.0 -17220.44078117744 +6680 4326 0.0 -17220.44078117744 142.25 43.845 +6668 6680 141.02599999999998 42.3195 -100891.1088259258 -185952.30694077082 +6680 4326 -100891.1088259258 -185952.30694077082 141.02599999999998 42.3195 +6668 6680 143.47400000000002 45.3705 95874.8566985425 153011.29943193644 +6680 4326 95874.8566985425 153011.29943193644 143.47400000000002 45.3705 +6668 6681 144.24 43.135 -813.5408685454717 -96095.12970504239 +6681 4326 -813.5408685454717 -96095.12970504239 144.24 43.135000000000005 +6668 6681 142.77300000000002 41.9965 -122366.48753635101 -221496.57531629794 +6681 4326 -122366.48753635101 -221496.57531629794 142.77300000000002 41.996500000000005 +6668 6681 145.707 44.2735 116310.97386241013 31419.373029238384 +6681 4326 116310.97386241013 31419.373029238384 145.707 44.2735 +6668 6682 141.765 26.235 -23478.34683664922 26054.282932958602 +6682 4326 -23478.34683664922 26054.282932958602 141.765 26.234999999999996 +6668 6682 141.2565 24.826500000000003 -75155.30730020948 -129781.52685226209 +6682 4326 -75155.30730020948 -129781.52685226209 141.2565 24.826500000000003 +6668 6682 142.2735 27.6435 26987.345735629453 182112.73747284504 +6682 4326 26987.345735629453 182112.73747284504 142.27350000000004 27.643500000000003 +6668 6683 127.515 26.465 1495.6545360844798 51513.01529828106 +6683 4326 1495.6545360844798 51513.01529828106 127.515 26.464999999999996 +6668 6683 126.71849999999999 26.0645 -78192.89680749233 7379.460140749547 +6683 4326 -78192.89680749233 7379.460140749547 126.7185 26.0645 +6668 6683 128.3115 26.8655 80634.4910127214 96141.25857342333 +6683 4326 80634.4910127214 96141.25857342333 128.3115 26.865499999999997 +6668 6684 124.17 24.46 17234.053724502493 -170567.9567367425 +6684 4326 17234.053724502493 -170567.9567367425 124.17000000000002 24.459999999999997 +6668 6684 122.964 24.028 -105385.34443706245 -218034.8587424919 +6684 4326 -105385.34443706245 -218034.8587424919 122.964 24.028000000000002 +6668 6684 125.376 24.892000000000003 139023.28742866468 -122028.76923696398 +6684 4326 139023.28742866468 -122028.76923696398 125.37599999999999 24.892000000000003 +6668 6685 131.25 25.205 25193.0183649557 -88039.43474560729 +6685 4326 25193.0183649557 -88039.43474560729 131.25 25.205000000000002 +6668 6685 131.133 24.4805 13480.924431529565 -168301.602720175 +6685 4326 13480.924431529565 -168301.602720175 131.133 24.4805 +6668 6685 131.367 25.9295 36761.584812905174 -7758.252281296523 +6685 4326 36761.584812905174 -7758.252281296523 131.367 25.929499999999997 +6668 6686 136.09 20.425 9391.766519188244 47048.325427261516 +6686 4326 9391.766519188244 47048.325427261516 136.09 20.425 +6668 6686 136.02700000000002 20.375500000000002 2818.429240383388 41566.41886628239 +6686 4326 2818.429240383388 41566.41886628239 136.02700000000002 20.3755 +6668 6686 136.153 20.4745 15960.900135895556 52532.800221791236 +6686 4326 15960.900135895556 52532.800221791236 136.153 20.474500000000003 +6668 6687 153.98000000000002 24.285 -2030.3256859181972 -189960.09433244 +6687 4326 -2030.3256859181972 -189960.09433244 153.98000000000002 24.284999999999997 +6668 6687 153.917 24.226499999999998 -8429.70912083693 -196436.65941094185 +6687 4326 -8429.70912083693 -196436.65941094185 153.917 24.226499999999994 +6668 6687 154.043 24.343500000000002 4363.198381945305 -183480.59051934036 +6687 4326 4363.198381945305 -183480.59051934036 154.043 24.3435 +6668 6688 124.19 25.405 619691.4110113059 2810327.34502011 +6688 4326 619691.4110113059 2810327.34502011 124.19000000000001 25.404999999999994 +6668 6688 122.56099999999999 21.5305 454538.0827993267 2380925.286207869 +6688 4326 454538.0827993267 2380925.286207869 122.56099999999999 21.5305 +6668 6688 125.819 29.2795 773886.5629422502 3242249.3828698383 +6688 4326 773886.5629422502 3242249.3828698383 125.819 29.279500000000006 +6668 6689 129.0 29.875 500000.00000000244 3304934.471297173 +6689 4326 500000.00000000244 3304934.471297173 129.0 29.875 +6668 6689 126.3 21.9955 221220.18393471796 2434790.286095488 +6689 4326 221220.18393471796 2434790.286095488 126.30000000000001 21.9955 +6668 6689 131.7 37.7545 737863.0975470991 4182009.825930984 +6689 4326 737863.0975470991 4182009.825930984 131.7 37.75449999999999 +6668 6690 135.0 30.32 500000.0 3354244.80937834 +6690 4326 500000.0 3354244.80937834 135.0 30.320000000000004 +6668 6690 132.3 18.413 214758.6089544095 2038003.2273633787 +6690 4326 214758.6089544095 2038003.2273633787 132.3 18.413 +6668 6690 137.7 42.227 722819.20735252 4678510.055377883 +6690 4326 722819.20735252 4678510.055377883 137.7 42.22699999999998 +6668 6691 141.0 31.839999999999996 500000.0000000024 3522701.124135246 +6691 4326 500000.0000000024 3522701.124135246 141.0 31.839999999999996 +6668 6691 138.3 19.051 215828.64715265244 2108657.1661159582 +6691 4326 215828.64715265244 2108657.1661159582 138.3 19.050999999999995 +6668 6691 143.7 44.62899999999999 714171.2341030437 4945284.762636639 +6691 4326 714171.2341030437 4945284.762636639 143.7 44.62899999999999 +6668 6692 145.93 34.34 401581.14578903903 3800373.9875940597 +6692 4326 401581.14578903903 3800373.9875940597 145.93 34.34 +6668 6692 144.193 24.161 214776.9353027034 2674912.8964886284 +6692 4326 214776.9353027034 2674912.8964886284 144.193 24.160999999999998 +6668 6692 147.667 44.519 553007.7980395162 4929735.99372344 +6692 4326 553007.7980395162 4929735.99372344 147.667 44.519 +4326 6703 -60.0 -51.965 500000.0 4242854.575397736 +4326 6703 -62.708999999999996 -55.8215 330291.0002810756 3810466.5821381807 +4326 6703 -57.291000000000004 -48.1085 701647.0674582777 4668090.448559392 +6706 6707 8.965 41.785 497091.64645042346 4625906.061436742 +6707 4326 497091.64645042346 4625906.061436742 8.965 41.78499999999999 +6706 6707 6.233499999999999 37.0555 254000.3275612877 4104609.5557094966 +6707 4326 254000.3275612877 4104609.5557094966 6.233499999999999 37.0555 +6706 6707 11.6965 46.5145 706848.5806319211 5154747.393047125 +6707 4326 706848.5806319211 5154747.393047125 11.6965 46.5145 +6706 6708 15.0 40.945 500000.00000000134 4532651.569272315 +6708 4326 500000.00000000134 4532651.569272315 14.999999999999998 40.94500000000001 +6706 6708 12.3 35.405499999999996 254812.08870369926 3921361.4559117802 +6708 4326 254812.08870369926 3921361.4559117802 12.300000000000002 35.40549999999999 +6706 6708 17.7 46.4845 707231.0799165128 5151423.26514731 +6708 4326 707231.0799165128 5151423.26514731 17.7 46.4845 +6706 6709 18.49 38.2 280211.8499703541 4230984.037387538 +6709 4326 280211.8499703541 4230984.037387538 18.49 38.199999999999996 +6706 6709 18.04 35.104 230196.3330072955 3888586.2343832804 +6709 4326 230196.3330072955 3888586.2343832804 18.04 35.10399999999999 +6706 6709 18.939999999999998 41.296 327523.36855843413 4573663.423681971 +6709 4326 327523.36855843413 4573663.423681971 18.939999999999998 41.296 +4326 6720 105.625 -10.495000000000001 50000.0 139365.64320887928 +4326 6720 105.4945 -10.6165 35719.50130629939 125923.0212541332 +4326 6720 105.7555 -10.3735 64291.65019685397 152802.23573857686 +4283 6721 105.625 -10.495000000000001 50000.0 139364.3201551109 +6721 4326 50000.0 139364.3201551109 105.62500000000001 -10.495 +4283 6721 105.4945 -10.6165 35719.485026913644 125921.68287656526 +6721 4326 35719.485026913644 125921.68287656526 105.49450000000002 -10.6165 +4283 6721 105.7555 -10.3735 64291.66648895177 152800.92800173513 +6721 4326 64291.66648895177 152800.92800173513 105.7555 -10.3735 +4326 6722 96.875 -12.015 50000.0 71256.19516685628 +4326 6722 96.7715 -12.2445 38738.83026226805 45865.98604851635 +4326 6722 96.9785 -11.785499999999999 61280.26330271132 96641.75022692955 +4283 6723 96.875 -12.015 50000.0 171264.34040925326 +6723 4326 50000.0 171264.34040925326 96.875 -12.014999999999999 +4283 6723 96.7715 -12.2445 38738.89929323025 145874.28693367727 +6723 4326 38738.89929323025 145874.28693367727 96.7715 -12.2445 +4283 6723 96.9785 -11.785499999999999 61280.19415470499 196649.7398550897 +6723 4326 61280.19415470499 196649.7398550897 96.9785 -11.785499999999997 +4283 6736 94.71000000000001 -12.015 686164.1589146144 8671209.265994793 +6736 4326 686164.1589146144 8671209.265994793 94.71000000000001 -12.015000000000004 +4283 6736 93.53999999999999 -15.1155 558023.5011035556 8328827.826709125 +6736 4326 558023.5011035556 8328827.826709125 93.53999999999999 -15.115499999999997 +4283 6736 95.88000000000001 -8.9145 816754.1205866318 9013366.039271686 +6736 4326 816754.1205866318 9013366.039271686 95.88000000000001 -8.914500000000007 +4283 6737 98.17 -12.015 409649.0483432067 8671651.456028556 +6737 4326 409649.0483432067 8671651.456028556 98.17 -12.014999999999999 +4283 6737 96.217 -15.2055 200992.27437954425 8317038.868694795 +6737 4326 200992.27437954425 8317038.868694795 96.217 -15.205500000000004 +4283 6737 100.123 -8.8245 623499.9094394216 9024364.83362359 +6737 4326 623499.9094394216 9024364.83362359 100.123 -8.8245 +4283 6738 170.675 -29.044999999999998 468358.6517043048 6786985.184161477 +6738 4326 468358.6517043048 6786985.184161477 170.675 -29.044999999999995 +4283 6738 168.2675 -32.1365 242241.1413549263 6441163.117441116 +6738 4326 242241.1413549263 6441163.117441116 168.2675 -32.1365 +4283 6738 173.08249999999998 -25.9535 708522.2486715973 7127806.7884851955 +6738 4326 708522.2486715973 7127806.7884851955 173.08249999999998 -25.953499999999988 +6783 6784 -117.76 44.894999999999996 45793.57896023025 43904.357243155195 +6783 6784 -118.111 44.6295 17962.701891061206 14430.27994462784 +6783 6784 -117.409 45.1605 73369.13760594158 73499.71183175976 +6783 6785 -117.76 44.894999999999996 150241.40078763204 144043.16680825193 +6783 6785 -118.111 44.6295 58932.748985896345 47343.43813854278 +6783 6785 -117.409 45.1605 240712.3937197558 241140.7868496055 +6318 6786 -117.76 44.894999999999996 45793.57896023025 43904.357243155195 +6786 4326 45793.57896023025 43904.357243155195 -117.76 44.895 +6318 6786 -118.111 44.6295 17962.701891061206 14430.27994462784 +6786 4326 17962.701891061206 14430.27994462784 -118.11100000000002 44.6295 +6318 6786 -117.409 45.1605 73369.13760594158 73499.71183175976 +6786 4326 73369.13760594158 73499.71183175976 -117.40900000000002 45.16049999999999 +6318 6787 -117.76 44.894999999999996 150241.40078763204 144043.16680825193 +6787 4326 150241.40078763204 144043.16680825193 -117.76 44.895 +6318 6787 -118.111 44.6295 58932.748985896345 47343.43813854278 +6787 4326 58932.748985896345 47343.43813854278 -118.11100000000002 44.6295 +6318 6787 -117.409 45.1605 240712.3937197558 241140.7868496055 +6787 4326 240712.3937197558 241140.7868496055 -117.40900000000002 45.16049999999999 +6783 6788 -121.61 42.885000000000005 91439.29043981324 126109.90884760738 +6783 6788 -122.366 41.9805 28938.03636496191 25790.512885572305 +6783 6788 -120.854 43.789500000000004 152132.89368666062 227000.14204685355 +6783 6789 -121.61 42.885000000000005 299997.67204558145 413746.42010369874 +6783 6789 -122.366 41.9805 94941.0641884577 84614.54358783564 +6783 6789 -120.854 43.789500000000004 499123.6669499365 744751.122200963 +6318 6790 -121.61 42.885000000000005 91439.29043981324 126109.90884760738 +6790 4326 91439.29043981324 126109.90884760738 -121.61 42.885000000000005 +6318 6790 -122.366 41.9805 28938.03636496191 25790.512885572305 +6790 4326 28938.03636496191 25790.512885572305 -122.366 41.9805 +6318 6790 -120.854 43.789500000000004 152132.89368666062 227000.14204685355 +6790 4326 152132.89368666062 227000.14204685355 -120.854 43.789500000000004 +6318 6791 -121.61 42.885000000000005 299997.67204558145 413746.42010369874 +6791 4326 299997.67204558145 413746.42010369874 -121.61 42.885000000000005 +6318 6791 -122.366 41.9805 94941.0641884577 84614.54358783564 +6791 4326 94941.0641884577 84614.54358783564 -122.366 41.9805 +6318 6791 -120.854 43.789500000000004 499123.6669499365 744751.122200963 +6791 4326 499123.6669499365 744751.122200963 -120.854 43.789500000000004 +6783 6792 -120.83500000000001 44.37 113082.288495397 97113.81951633212 +6783 6792 -121.7755 43.821 37715.78757922242 36153.703943303 +6783 6792 -119.89450000000001 44.919 187036.30854680753 158934.77105294933 +6783 6793 -120.83500000000001 44.37 371004.8835140321 318614.8934745804 +6783 6793 -121.7755 43.821 123739.46056070349 118614.51429797572 +6783 6793 -119.89450000000001 44.919 613636.1828953002 521439.53762358695 +6318 6794 -120.83500000000001 44.37 113082.288495397 97113.81951633212 +6794 4326 113082.288495397 97113.81951633212 -120.83500000000002 44.37 +6318 6794 -121.7755 43.821 37715.78757922242 36153.703943303 +6794 4326 37715.78757922242 36153.703943303 -121.7755 43.821000000000005 +6318 6794 -119.89450000000001 44.919 187036.30854680753 158934.77105294933 +6794 4326 187036.30854680753 158934.77105294933 -119.89450000000001 44.919 +6318 6795 -120.83500000000001 44.37 371004.8835140321 318614.8934745804 +6795 4326 371004.8835140321 318614.8934745804 -120.83500000000002 44.37 +6318 6795 -121.7755 43.821 123739.46056070349 118614.51429797572 +6795 4326 123739.46056070349 118614.51429797572 -121.7755 43.821000000000005 +6318 6795 -119.89450000000001 44.919 613636.1828953002 521439.53762358695 +6795 4326 613636.1828953002 521439.53762358695 -119.89450000000001 44.919 +6783 6796 -119.875 43.81 109940.24784176826 75936.1343174594 +6783 6796 -120.8425 43.387 31461.42246120893 29504.69217721203 +6783 6796 -118.9075 44.233000000000004 187324.30348598075 123281.38976432997 +6783 6797 -119.875 43.81 360696.3511853289 249134.29894100846 +6783 6797 -120.8425 43.387 103219.88996302143 96800.17118429144 +6783 6797 -118.9075 44.233000000000004 614581.0481807766 404466.5018506889 +6318 6798 -119.875 43.81 109940.24784176826 75936.1343174594 +6798 4326 109940.24784176826 75936.1343174594 -119.875 43.81000000000001 +6318 6798 -120.8425 43.387 31461.42246120893 29504.69217721203 +6798 4326 31461.42246120893 29504.69217721203 -120.8425 43.38699999999999 +6318 6798 -118.9075 44.233000000000004 187324.30348598075 123281.38976432997 +6798 4326 187324.30348598075 123281.38976432997 -118.9075 44.233 +6318 6799 -119.875 43.81 360696.3511853289 249134.29894100846 +6799 4326 360696.3511853289 249134.29894100846 -119.875 43.81000000000001 +6318 6799 -120.8425 43.387 103219.88996302143 96800.17118429144 +6799 4326 103219.88996302143 96800.17118429144 -120.8425 43.38699999999999 +6318 6799 -118.9075 44.233000000000004 614581.0481807766 404466.5018506889 +6799 4326 614581.0481807766 404466.5018506889 -118.9075 44.233 +6783 6800 -123.13 42.865 56617.41476194597 40569.48901035372 +6783 6800 -123.76 42.5275 4941.311622276407 3143.2399164128924 +6783 6800 -122.5 43.2025 107732.1286673199 78383.28519043862 +6783 6801 -123.13 42.865 185752.67310297233 133101.99806546493 +6783 6801 -123.76 42.5275 16211.652303531524 10312.466917365131 +6783 6801 -122.5 43.2025 353451.8657059052 257163.0091549823 +6318 6802 -123.13 42.865 56617.41476194597 40569.48901035372 +6802 4326 56617.41476194597 40569.48901035372 -123.13 42.865 +6318 6802 -123.76 42.5275 4941.311622276407 3143.2399164128924 +6802 4326 4941.311622276407 3143.2399164128924 -123.76 42.52750000000001 +6318 6802 -122.5 43.2025 107732.1286673199 78383.28519043862 +6802 4326 107732.1286673199 78383.28519043862 -122.50000000000001 43.20250000000001 +6318 6803 -123.13 42.865 185752.67310297233 133101.99806546493 +6803 4326 185752.67310297233 133101.99806546493 -123.13 42.865 +6318 6803 -123.76 42.5275 16211.652303531524 10312.466917365131 +6803 4326 16211.652303531524 10312.466917365131 -123.76 42.52750000000001 +6318 6803 -122.5 43.2025 353451.8657059052 257163.0091549823 +6803 4326 353451.8657059052 257163.0091549823 -122.50000000000001 43.20250000000001 +6783 6804 -120.47 45.785 152332.90694223306 43152.82354977006 +6783 6804 -121.892 45.5195 41245.783474503856 14588.234294754106 +6783 6804 -119.048 46.0505 262371.604834867 73681.6836118316 +6783 6805 -120.47 45.785 499779.8784674313 141577.50513415373 +6783 6805 -121.892 45.5195 135320.81197225678 47861.6611216342 +6783 6805 -119.048 46.0505 860799.2285088813 241737.80717503806 +6318 6806 -120.47 45.785 152332.90694223306 43152.82354977006 +6806 4326 152332.90694223306 43152.82354977006 -120.47000000000001 45.78499999999998 +6318 6806 -121.892 45.5195 41245.783474503856 14588.234294754106 +6806 4326 41245.783474503856 14588.234294754106 -121.892 45.519499999999994 +6318 6806 -119.048 46.0505 262371.604834867 73681.6836118316 +6806 4326 262371.604834867 73681.6836118316 -119.048 46.05050000000001 +6318 6807 -120.47 45.785 499779.8784674313 141577.50513415373 +6807 4326 499779.8784674313 141577.50513415373 -120.47000000000001 45.78499999999998 +6318 6807 -121.892 45.5195 135320.81197225678 47861.6611216342 +6807 4326 135320.81197225678 47861.6611216342 -121.892 45.519499999999994 +6318 6807 -119.048 46.0505 860799.2285088813 241737.80717503806 +6807 4326 860799.2285088813 241737.80717503806 -119.048 46.05050000000001 +6783 6808 -122.9 45.865 176065.5662717428 179935.99146010308 +6783 6808 -124.187 45.2395 75095.09040943626 111100.9729531738 +6783 6808 -121.613 46.490500000000004 274791.62024997734 250387.0417069951 +6783 6809 -122.9 45.865 577642.9339705475 590341.1793704171 +6783 6809 -124.187 45.2395 246374.9685430327 364504.50447891693 +6783 6809 -121.613 46.490500000000004 901547.3105395584 821479.7956659946 +6318 6810 -122.9 45.865 176065.5662717428 179935.99146010308 +6810 4326 176065.5662717428 179935.99146010308 -122.89999999999999 45.865000000000016 +6318 6810 -124.187 45.2395 75095.09040943626 111100.9729531738 +6810 4326 75095.09040943626 111100.9729531738 -124.18700000000001 45.239499999999985 +6318 6810 -121.613 46.490500000000004 274791.62024997734 250387.0417069951 +6810 4326 274791.62024997734 250387.0417069951 -121.613 46.490500000000004 +6318 6811 -122.9 45.865 577642.9339705475 590341.1793704171 +6811 4326 577642.9339705475 590341.1793704171 -122.89999999999999 45.865000000000016 +6318 6811 -124.187 45.2395 246374.9685430327 364504.50447891693 +6811 4326 246374.9685430327 364504.50447891693 -124.18700000000001 45.239499999999985 +6318 6811 -121.613 46.490500000000004 901547.3105395584 821479.7956659946 +6811 4326 901547.3105395584 821479.7956659946 -121.613 46.490500000000004 +6783 6812 -123.18 43.35 62432.03490876068 57411.576278684945 +6783 6812 -123.88199999999999 42.873 5168.074158007905 4552.715570127785 +6783 6812 -122.47800000000001 43.827000000000005 118803.74607446113 110753.73276639481 +6783 6813 -123.18 43.35 204829.51090485786 188358.18989069862 +6783 6813 -123.88199999999999 42.873 16955.623927322522 14936.730873122653 +6783 6813 -122.47800000000001 43.827000000000005 389776.0698474446 363365.26498161023 +6318 6814 -123.18 43.35 62432.03490876068 57411.576278684945 +6814 4326 62432.03490876068 57411.576278684945 -123.17999999999999 43.35 +6318 6814 -123.88199999999999 42.873 5168.074158007905 4552.715570127785 +6814 4326 5168.074158007905 4552.715570127785 -123.882 42.873 +6318 6814 -122.47800000000001 43.827000000000005 118803.74607446113 110753.73276639481 +6814 4326 118803.74607446113 110753.73276639481 -122.47800000000002 43.82700000000001 +6318 6815 -123.18 43.35 204829.51090485786 188358.18989069862 +6815 4326 204829.51090485786 188358.18989069862 -123.17999999999999 43.35 +6318 6815 -123.88199999999999 42.873 16955.623927322522 14936.730873122653 +6815 4326 16955.623927322522 14936.730873122653 -123.882 42.873 +6318 6815 -122.47800000000001 43.827000000000005 389776.0698474446 363365.26498161023 +6815 4326 389776.0698474446 363365.26498161023 -122.47800000000002 43.82700000000001 +6783 6816 -121.205 45.09 63859.94816560956 65593.04647297243 +6783 6816 -121.8755 44.676 10573.991107712543 19932.883623470312 +6783 6816 -120.5345 45.504 116383.99779478104 111694.04494354177 +6783 6817 -121.205 45.09 209514.26563415208 215200.28370397777 +6783 6817 -121.8755 44.676 34691.57187464746 65396.59981453514 +6783 6817 -120.5345 45.504 381837.2631051871 366450.27868616063 +6318 6818 -121.205 45.09 63859.94816560956 65593.04647297243 +6818 4326 63859.94816560956 65593.04647297243 -121.205 45.09000000000001 +6318 6818 -121.8755 44.676 10573.991107712543 19932.883623470312 +6818 4326 10573.991107712543 19932.883623470312 -121.87549999999999 44.675999999999995 +6318 6818 -120.5345 45.504 116383.99779478104 111694.04494354177 +6818 4326 116383.99779478104 111694.04494354177 -120.5345 45.504 +6318 6819 -121.205 45.09 209514.26563415208 215200.28370397777 +6819 4326 209514.26563415208 215200.28370397777 -121.205 45.09000000000001 +6318 6819 -121.8755 44.676 34691.57187464746 65396.59981453514 +6819 4326 34691.57187464746 65396.59981453514 -121.87549999999999 44.675999999999995 +6318 6819 -120.5345 45.504 381837.2631051871 366450.27868616063 +6819 4326 381837.2631051871 366450.27868616063 -120.5345 45.504 +6783 6820 -122.99000000000001 44.225 64116.31481990403 52794.17024731537 +6783 6820 -123.719 43.7885 5541.682747749379 4426.002648017479 +6783 6820 -122.26100000000001 44.661500000000004 121829.27409356808 101683.05206538601 +6783 6821 -122.99000000000001 44.225 210355.3636317061 173209.21997150712 +6783 6821 -123.719 43.7885 18181.373893665943 14521.006063049472 +6783 6821 -122.26100000000001 44.661500000000004 399702.3428760107 333605.813862815 +6318 6822 -122.99000000000001 44.225 64116.31481990403 52794.17024731537 +6822 4326 64116.31481990403 52794.17024731537 -122.99000000000001 44.22500000000001 +6318 6822 -123.719 43.7885 5541.682747749379 4426.002648017479 +6822 4326 5541.682747749379 4426.002648017479 -123.71900000000001 43.788500000000006 +6318 6822 -122.26100000000001 44.661500000000004 121829.27409356808 101683.05206538601 +6822 4326 121829.27409356808 101683.05206538601 -122.26100000000001 44.66150000000001 +6318 6823 -122.99000000000001 44.225 210355.3636317061 173209.21997150712 +6823 4326 210355.3636317061 173209.21997150712 -122.99000000000001 44.22500000000001 +6318 6823 -123.719 43.7885 18181.373893665943 14521.006063049472 +6823 4326 18181.373893665943 14521.006063049472 -123.71900000000001 43.788500000000006 +6318 6823 -122.26100000000001 44.661500000000004 399702.3428760107 333605.813862815 +6823 4326 399702.3428760107 333605.813862815 -122.26100000000001 44.66150000000001 +6783 6824 -123.16 42.365 64279.05449027244 68328.24862796377 +6783 6824 -123.87100000000001 41.9285 5402.0856214874075 19966.70333226475 +6783 6824 -122.449 42.801500000000004 122344.42203308806 117183.57599231426 +6783 6825 -123.16 42.365 210889.28643475208 224174.04405499922 +6783 6825 -123.87100000000001 41.9285 17723.378072596483 65507.55686438566 +6783 6825 -122.449 42.801500000000004 401392.4607878217 384460.5511558867 +6318 6826 -123.16 42.365 64279.05449027244 68328.24862796377 +6826 4326 64279.05449027244 68328.24862796377 -123.16 42.365 +6318 6826 -123.87100000000001 41.9285 5402.0856214874075 19966.70333226475 +6826 4326 5402.0856214874075 19966.70333226475 -123.87100000000001 41.92849999999999 +6318 6826 -122.449 42.801500000000004 122344.42203308806 117183.57599231426 +6826 4326 122344.42203308806 117183.57599231426 -122.44900000000001 42.801500000000004 +6318 6827 -123.16 42.365 210889.28643475208 224174.04405499922 +6827 4326 210889.28643475208 224174.04405499922 -123.16 42.365 +6318 6827 -123.87100000000001 41.9285 17723.378072596483 65507.55686438566 +6827 4326 17723.378072596483 65507.55686438566 -123.87100000000001 41.92849999999999 +6318 6827 -122.449 42.801500000000004 401392.4607878217 384460.5511558867 +6827 4326 401392.4607878217 384460.5511558867 -122.44900000000001 42.801500000000004 +6783 6828 -122.055 45.285 31837.725238320116 31712.62651701483 +6783 6828 -122.39250000000001 45.0465 5338.437451142741 5169.610468045655 +6783 6828 -121.7175 45.5235 58114.704100439405 58367.57971854889 +6783 6829 -122.055 45.285 104454.4791782156 104044.05025267332 +6783 6829 -122.39250000000001 45.0465 17514.55861661004 16960.66426524165 +6783 6829 -121.7175 45.5235 190665.0397494731 191494.68411597403 +6318 6830 -122.055 45.285 31837.725238320116 31712.62651701483 +6830 4326 31837.725238320116 31712.62651701483 -122.05500000000002 45.285 +6318 6830 -122.39250000000001 45.0465 5338.437451142741 5169.610468045655 +6830 4326 5338.437451142741 5169.610468045655 -122.39250000000001 45.0465 +6318 6830 -121.7175 45.5235 58114.704100439405 58367.57971854889 +6830 4326 58114.704100439405 58367.57971854889 -121.7175 45.52349999999999 +6318 6831 -122.055 45.285 104454.4791782156 104044.05025267332 +6831 4326 104454.4791782156 104044.05025267332 -122.05500000000002 45.285 +6318 6831 -122.39250000000001 45.0465 17514.55861661004 16960.66426524165 +6831 4326 17514.55861661004 16960.66426524165 -122.39250000000001 45.0465 +6318 6831 -121.7175 45.5235 190665.0397494731 191494.68411597403 +6831 4326 190665.0397494731 191494.68411597403 -121.7175 45.52349999999999 +6783 6832 -117.655 45.465 66984.73849497794 51743.01948292207 +6783 6832 -118.1185 45.1635 30682.049917344222 18179.50188733122 +6783 6832 -117.1915 45.7665 102900.12302202139 85517.31097035269 +6783 6833 -117.655 45.465 219766.20241082 169760.56260801203 +6783 6833 -118.1185 45.1635 100662.89342908209 59644.03506342263 +6783 6833 -117.1915 45.7665 337598.8288118812 280568.6055457765 +6318 6834 -117.655 45.465 66984.73849497794 51743.01948292207 +6834 4326 66984.73849497794 51743.01948292207 -117.65500000000002 45.46500000000001 +6318 6834 -118.1185 45.1635 30682.049917344222 18179.50188733122 +6834 4326 30682.049917344222 18179.50188733122 -118.1185 45.1635 +6318 6834 -117.1915 45.7665 102900.12302202139 85517.31097035269 +6834 4326 102900.12302202139 85517.31097035269 -117.19150000000002 45.7665 +6318 6835 -117.655 45.465 219766.20241082 169760.56260801203 +6835 4326 219766.20241082 169760.56260801203 -117.65500000000002 45.46500000000001 +6318 6835 -118.1185 45.1635 100662.89342908209 59644.03506342263 +6835 4326 100662.89342908209 59644.03506342263 -118.1185 45.1635 +6318 6835 -117.1915 45.7665 337598.8288118812 280568.6055457765 +6835 4326 337598.8288118812 280568.6055457765 -117.19150000000002 45.7665 +6783 6836 -117.30000000000001 44.03 55947.860695393574 86714.50030339813 +6783 6836 -117.84 43.471999999999994 12025.272962100687 25009.445886746093 +6783 6836 -116.76 44.588 99060.26893748676 148708.95759627043 +6783 6837 -117.30000000000001 44.03 183555.97340903402 284496.39207151614 +6783 6837 -117.84 43.471999999999994 39452.99528143271 82051.98781740843 +6783 6837 -116.76 44.588 325000.8823397859 487890.28082765883 +6318 6838 -117.30000000000001 44.03 55947.860695393574 86714.50030339813 +6838 4326 55947.860695393574 86714.50030339813 -117.30000000000001 44.03 +6318 6838 -117.84 43.471999999999994 12025.272962100687 25009.445886746093 +6838 4326 12025.272962100687 25009.445886746093 -117.84 43.47199999999999 +6318 6838 -116.76 44.588 99060.26893748676 148708.95759627043 +6838 4326 99060.26893748676 148708.95759627043 -116.76000000000002 44.587999999999994 +6318 6839 -117.30000000000001 44.03 183555.97340903402 284496.39207151614 +6839 4326 183555.97340903402 284496.39207151614 -117.30000000000001 44.03 +6318 6839 -117.84 43.471999999999994 39452.99528143271 82051.98781740843 +6839 4326 39452.99528143271 82051.98781740843 -117.84 43.47199999999999 +6318 6839 -116.76 44.588 325000.8823397859 487890.28082765883 +6839 4326 325000.8823397859 487890.28082765883 -116.76000000000002 44.587999999999994 +6783 6840 -124.095 44.144999999999996 131142.74669074855 301911.8261758303 +6783 6840 -124.7655 42.1155 75562.49240677798 76695.37133014761 +6783 6840 -123.4245 46.174499999999995 183045.17527350673 527648.1609180942 +6783 6841 -124.095 44.144999999999996 430258.3552885451 990524.3640542983 +6783 6841 -124.7655 42.1155 247908.4396587204 251625.2339831608 +6783 6841 -123.4245 46.174499999999995 600541.9136309276 1731129.1368307546 +6318 6842 -124.095 44.144999999999996 131142.74669074855 301911.8261758303 +6842 4326 131142.74669074855 301911.8261758303 -124.09499999999998 44.144999999999996 +6318 6842 -124.7655 42.1155 75562.49240677798 76695.37133014761 +6842 4326 75562.49240677798 76695.37133014761 -124.76550000000002 42.11549999999999 +6318 6842 -123.4245 46.174499999999995 183045.17527350673 527648.1609180942 +6842 4326 183045.17527350673 527648.1609180942 -123.4245 46.17449999999997 +6318 6843 -124.095 44.144999999999996 430258.3552885451 990524.3640542983 +6843 4326 430258.3552885451 990524.3640542983 -124.09499999999998 44.144999999999996 +6318 6843 -124.7655 42.1155 247908.4396587204 251625.2339831608 +6843 4326 247908.4396587204 251625.2339831608 -124.76550000000002 42.11549999999999 +6318 6843 -123.4245 46.174499999999995 600541.9136309276 1731129.1368307546 +6843 4326 600541.9136309276 1731129.1368307546 -123.4245 46.17449999999997 +6783 6844 -118.765 45.74 91261.23794189063 54540.23913647877 +6783 6844 -119.3005 45.488 49537.153391764215 26460.983541328576 +6783 6844 -118.2295 45.992000000000004 132609.6436863756 82899.61779712347 +6783 6845 -118.765 45.74 299413.5103072527 178937.79244251564 +6783 6845 -119.3005 45.488 162523.4691322973 86814.25046367642 +6783 6845 -118.2295 45.992000000000004 435071.0094689488 271980.3733501426 +6318 6846 -118.765 45.74 91261.23794189063 54540.23913647877 +6846 4326 91261.23794189063 54540.23913647877 -118.76499999999999 45.74 +6318 6846 -119.3005 45.488 49537.153391764215 26460.983541328576 +6846 4326 49537.153391764215 26460.983541328576 -119.3005 45.488 +6318 6846 -118.2295 45.992000000000004 132609.6436863756 82899.61779712347 +6846 4326 132609.6436863756 82899.61779712347 -118.2295 45.992000000000004 +6318 6847 -118.765 45.74 299413.5103072527 178937.79244251564 +6847 4326 299413.5103072527 178937.79244251564 -118.76499999999999 45.74 +6318 6847 -119.3005 45.488 162523.4691322973 86814.25046367642 +6847 4326 162523.4691322973 86814.25046367642 -119.3005 45.488 +6318 6847 -118.2295 45.992000000000004 435071.0094689488 271980.3733501426 +6847 4326 435071.0094689488 271980.3733501426 -118.2295 45.992000000000004 +6783 6848 -118.36500000000001 45.385000000000005 27519.527227590832 33532.4883663339 +6783 6848 -118.6125 45.1555 8044.3702155471 8059.506310696492 +6783 6848 -118.1175 45.6145 46837.83520185425 59066.112969494374 +6783 6849 -118.36500000000001 45.385000000000005 90287.16286978619 110014.72561133168 +6783 6849 -118.6125 45.1555 26392.29078302854 26441.94983824308 +6783 6849 -118.1175 45.6145 153667.43837589974 193786.4598736692 +6318 6850 -118.36500000000001 45.385000000000005 27519.527227590832 33532.4883663339 +6850 4326 27519.527227590832 33532.4883663339 -118.36500000000002 45.38500000000001 +6318 6850 -118.6125 45.1555 8044.3702155471 8059.506310696492 +6850 4326 8044.3702155471 8059.506310696492 -118.61249999999998 45.1555 +6318 6850 -118.1175 45.6145 46837.83520185425 59066.112969494374 +6850 4326 46837.83520185425 59066.112969494374 -118.11750000000002 45.61449999999999 +6318 6851 -118.36500000000001 45.385000000000005 90287.16286978619 110014.72561133168 +6851 4326 90287.16286978619 110014.72561133168 -118.36500000000002 45.38500000000001 +6318 6851 -118.6125 45.1555 26392.29078302854 26441.94983824308 +6851 4326 26392.29078302854 26441.94983824308 -118.61249999999998 45.1555 +6318 6851 -118.1175 45.6145 153667.43837589974 193786.4598736692 +6851 4326 153667.43837589974 193786.4598736692 -118.11750000000002 45.61449999999999 +6783 6852 -122.82 45.62 94540.5472907335 63339.5416298317 +6783 6852 -123.459 45.269 44359.95097146731 24572.245159698265 +6783 6852 -122.181 45.971 144100.84727480705 102506.71954525218 +6783 6853 -122.82 45.62 310172.399246501 207806.8951603402 +6783 6853 -123.459 45.269 145537.89688670376 80617.60227932502 +6783 6853 -122.181 45.971 472771.80864306766 336308.1350403287 +6318 6854 -122.82 45.62 94540.5472907335 63339.5416298317 +6854 4326 94540.5472907335 63339.5416298317 -122.82 45.620000000000005 +6318 6854 -123.459 45.269 44359.95097146731 24572.245159698265 +6854 4326 44359.95097146731 24572.245159698265 -123.45899999999999 45.269 +6318 6854 -122.181 45.971 144100.84727480705 102506.71954525218 +6854 4326 144100.84727480705 102506.71954525218 -122.181 45.971 +6318 6855 -122.82 45.62 310172.399246501 207806.8951603402 +6855 4326 310172.399246501 207806.8951603402 -122.82 45.620000000000005 +6318 6855 -123.459 45.269 145537.89688670376 80617.60227932502 +6855 4326 145537.89688670376 80617.60227932502 -123.45899999999999 45.269 +6318 6855 -122.181 45.971 472771.80864306766 336308.1350403287 +6855 4326 472771.80864306766 336308.1350403287 -122.181 45.971 +6783 6856 -122.81 44.81 71622.79310841832 53005.703169563865 +6783 6856 -123.638 44.369 5788.486627128354 4112.935481183218 +6783 6856 -121.982 45.251 136457.50217064342 102569.38780887499 +6783 6857 -122.81 44.81 234982.91707171363 173903.22562192866 +6783 6857 -123.638 44.369 18991.09790737649 13493.882812280897 +6783 6857 -121.982 45.251 447695.21714463056 336513.7395304297 +6318 6858 -122.81 44.81 71622.79310841832 53005.703169563865 +6858 4326 71622.79310841832 53005.703169563865 -122.80999999999999 44.81 +6318 6858 -123.638 44.369 5788.486627128354 4112.935481183218 +6858 4326 5788.486627128354 4112.935481183218 -123.638 44.36900000000001 +6318 6858 -121.982 45.251 136457.50217064342 102569.38780887499 +6858 4326 136457.50217064342 102569.38780887499 -121.98199999999999 45.251 +6318 6859 -122.81 44.81 234982.91707171363 173903.22562192866 +6859 4326 234982.91707171363 173903.22562192866 -122.80999999999999 44.81 +6318 6859 -123.638 44.369 18991.09790737649 13493.882812280897 +6859 4326 18991.09790737649 13493.882812280897 -123.638 44.36900000000001 +6318 6859 -121.982 45.251 447695.21714463056 336513.7395304297 +6859 4326 447695.21714463056 336513.7395304297 -121.98199999999999 45.251 +6783 6860 -122.1 44.379999999999995 31881.95146790145 33047.587840407024 +6783 6860 -122.46900000000001 44.128 2481.4248878673916 4964.342313288497 +6783 6860 -121.731 44.632 61029.56718431992 61264.60903388951 +6783 6861 -122.1 44.379999999999995 104599.57830676327 108423.84462075794 +6783 6861 -122.46900000000001 44.128 8141.157768593804 16287.212313938637 +6783 6861 -121.731 44.632 200228.23879370053 200999.3734707661 +6318 6862 -122.1 44.379999999999995 31881.95146790145 33047.587840407024 +6862 4326 31881.95146790145 33047.587840407024 -122.1 44.38 +6318 6862 -122.46900000000001 44.128 2481.4248878673916 4964.342313288497 +6862 4326 2481.4248878673916 4964.342313288497 -122.46900000000001 44.128 +6318 6862 -121.731 44.632 61029.56718431992 61264.60903388951 +6862 4326 61029.56718431992 61264.60903388951 -121.731 44.632 +6318 6863 -122.1 44.379999999999995 104599.57830676327 108423.84462075794 +6863 4326 104599.57830676327 108423.84462075794 -122.1 44.38 +6318 6863 -122.46900000000001 44.128 8141.157768593804 16287.212313938637 +6863 4326 8141.157768593804 16287.212313938637 -122.46900000000001 44.128 +6318 6863 -121.731 44.632 200228.23879370053 200999.3734707661 +6863 4326 200228.23879370053 200999.3734707661 -121.731 44.632 +6783 6867 -120.535 44.12 397199.10422283644 263312.33359227417 +6783 6867 -124.1935 42.193999999999996 94902.84668831312 56206.91245575882 +6783 6867 -116.8765 46.046 680436.0888007582 483525.7810091159 +6783 6868 -120.535 44.12 1303146.667392508 863885.6088985372 +6783 6868 -124.1935 42.193999999999996 311361.04556008236 184405.88076036357 +6783 6868 -116.8765 46.046 2232401.8661389705 1586370.672602086 +4258 6870 20.14 41.155 511751.29028047656 4557796.327358871 +6870 4326 511751.29028047656 4557796.327358871 20.14 41.155 +4258 6870 19.311999999999998 39.7915 441070.46912932984 4406605.169044593 +6870 4326 441070.46912932984 4406605.169044593 19.311999999999998 39.7915 +4258 6870 20.968 42.5185 579545.5179293917 4709685.135934042 +6870 4326 579545.5179293917 4709685.135934042 20.968 42.5185 +6706 6875 12.459999999999999 40.93 7038685.047080272 4526102.104025962 +6875 4326 7038685.047080272 4526102.104025962 12.46 40.92999999999999 +6706 6875 6.583 35.376999999999995 6508266.154277894 3924025.7764995354 +6875 4326 6508266.154277894 3924025.7764995354 6.582999999999999 35.376999999999995 +6706 6875 18.336999999999996 46.483000000000004 7485814.593868981 5161570.967588263 +6875 4326 7485814.593868981 5161570.967588263 18.336999999999993 46.483000000000004 +6706 6876 12.459999999999999 40.93 3038743.1618230063 4532901.456210277 +6876 4326 3038743.1618230063 4532901.456210277 12.46 40.92999999999999 +6706 6876 6.583 35.376999999999995 2507527.445446063 3929920.657485764 +6876 4326 2507527.445446063 3929920.657485764 6.582999999999999 35.376999999999995 +6706 6876 18.336999999999996 46.483000000000004 3486544.4104847084 5169324.955020794 +6876 4326 3486544.4104847084 5169324.955020794 18.337 46.48300000000001 +6318 6879 -89.57 44.89 633966.7768787964 117505.3837196188 +6879 4326 633966.7768787964 117505.3837196188 -89.57000000000001 44.88999999999999 +6318 6879 -92.558 44.071 395103.35046936397 29636.785858339033 +6879 4326 395103.35046936397 29636.785858339033 -92.558 44.071 +6318 6879 -86.582 45.708999999999996 866087.9109689173 214035.8706913536 +6879 4326 866087.9109689173 214035.8706913536 -86.582 45.708999999999996 +6318 6880 -99.68 41.5 1728046.3838602335 607254.3110984005 +6880 4326 1728046.3838602335 607254.3110984005 -99.68 41.5 +6318 6880 -103.622 40.141000000000005 628114.1608038797 133286.49459627096 +6880 4326 628114.1608038797 133286.49459627096 -103.622 40.14100000000001 +6318 6880 -95.738 42.858999999999995 2782667.3042486086 1130381.2224117697 +6880 4326 2782667.3042486086 1130381.2224117697 -95.738 42.858999999999995 +6783 6884 -120.32 45.105000000000004 2514164.9883181253 159845.1412173828 +6783 6884 -123.785 44.0655 2236864.6381944637 49671.3154349094 +6783 6884 -116.855 46.1445 2781533.537177242 281708.5817855314 +6783 6885 -120.32 45.105000000000004 8248572.7967865 524426.316329996 +6783 6885 -123.785 44.0655 7338794.745199684 162963.63331663187 +6783 6885 -116.855 46.1445 9125766.198555255 924240.7538895387 +6783 6886 -120.75 43.269999999999996 1479706.3814610983 178132.5497103679 +6783 6886 -124.21499999999999 42.108999999999995 1192816.3476943534 55951.90213787231 +6783 6886 -117.28500000000001 44.431000000000004 1755961.4123870605 312011.62286356714 +6783 6887 -120.75 43.269999999999996 4854679.729683393 584424.3756901834 +6783 6887 -124.21499999999999 42.108999999999995 3913439.4614197942 183569.23273580152 +6783 6887 -117.28500000000001 44.431000000000004 5761028.256343373 1023660.1799985798 +6892 6915 55.58 -4.26 342404.3043202306 -470968.47599402897 +6915 4326 342404.3043202306 -470968.47599402897 55.57939694268717 -4.262066305157701 +6892 6915 55.193 -4.800000000000001 299590.05548700865 -530772.4191639916 +6915 4326 299590.05548700865 -530772.4191639916 55.19240554170932 -4.802022865317941 +6892 6915 55.967 -3.72 385285.28878776246 -411206.81613854354 +6915 4326 385285.28878776246 -411206.81613854354 55.96638798071399 -3.722109873856336 +4269 6922 -98.32 38.5 393894.6331655367 277503.3759678225 +6922 4326 393894.6331655367 277503.3759678225 -98.32 38.5 +4269 6922 -101.686 37.141000000000005 94753.50812485645 132368.8975042541 +6922 4326 94753.50812485645 132368.8975042541 -101.686 37.141000000000005 +4269 6922 -94.954 39.858999999999995 682012.3855078344 433417.31139474135 +6922 4326 682012.3855078344 433417.31139474135 -94.954 39.85899999999999 +4269 6923 -98.32 38.5 1292302.6422772652 910442.3259877644 +6923 4326 1292302.6422772652 910442.3259877644 -98.32 38.5 +4269 6923 -101.686 37.141000000000005 310870.4678729667 434280.29122854036 +6923 4326 310870.4678729667 434280.29122854036 -101.686 37.141000000000005 +4269 6923 -94.954 39.858999999999995 2237568.9680869537 1421969.9624675808 +6923 4326 2237568.9680869537 1421969.9624675808 -94.954 39.85899999999999 +6318 6924 -98.32 38.5 393894.6331655367 277503.3759678225 +6924 4326 393894.6331655367 277503.3759678225 -98.32 38.5 +6318 6924 -101.686 37.141000000000005 94753.50812485645 132368.8975042541 +6924 4326 94753.50812485645 132368.8975042541 -101.686 37.141000000000005 +6318 6924 -94.954 39.858999999999995 682012.3855078344 433417.31139474135 +6924 4326 682012.3855078344 433417.31139474135 -94.954 39.85899999999999 +6318 6925 -98.32 38.5 1292302.6422772652 910442.3259877644 +6925 4326 1292302.6422772652 910442.3259877644 -98.32 38.5 +6318 6925 -101.686 37.141000000000005 310870.4678729667 434280.29122854036 +6925 4326 310870.4678729667 434280.29122854036 -101.686 37.141000000000005 +6318 6925 -94.954 39.858999999999995 2237568.9680869537 1421969.9624675808 +6925 4326 2237568.9680869537 1421969.9624675808 -94.954 39.85899999999999 +4326 6931 0.0 45.0 0.0 -4889334.802954874 +4326 6931 -162.0 4.5 -2673282.664934577 8227518.048085134 +4326 6931 162.0 85.5 155275.88057094577 477890.02135359053 +4326 6932 0.0 -45.0 0.0 4889334.802954874 +4326 6932 -162.0 -85.5 -155275.88057094577 -477890.02135359053 +4326 6932 162.0 -4.5 2673282.664934577 -8227518.048085134 +4326 6933 0.0 0.0 0.0 0.0 +4326 6933 -162.0 -77.4 -15630777.400645236 -7163872.523580175 +4326 6933 162.0 77.4 15630777.400645236 7163872.523580175 +4258 6962 20.14 41.155 11744.189078855517 17212.57320812194 +6962 4326 11744.189078855517 17212.57320812194 20.14 41.154999999999994 +4258 6962 19.311999999999998 39.7915 -58906.091178807976 -133891.15890187214 +6962 4326 -58906.091178807976 -133891.15890187214 19.311999999999998 39.79149999999999 +4258 6962 20.968 42.5185 79522.939425488 169015.5276017618 +6962 4326 79522.939425488 169015.5276017618 20.968 42.518499999999996 +4267 6966 -86.93 46.7 2017564.380262956 698942.4528425623 +6966 4326 2017564.380262956 698942.4528425623 -86.92999241062586 46.69999775392682 +4267 6966 -90.071 45.242 1209025.8974608001 182585.93267491585 +6966 4326 1209025.8974608001 182585.93267491585 -90.0711044181727 45.24199657458538 +4267 6966 -83.789 48.158 2783942.8867329783 1246723.0709411998 +6966 4326 2783942.8867329783 1246723.0709411998 -83.78887422311396 48.15799992680592 +6983 6984 34.93 31.365000000000002 193412.21592306579 585981.7010273987 +6983 6984 34.246 29.6415 126712.39623179393 395262.90677270875 +6983 6984 35.614 33.0885 257759.23041669643 777145.3288331792 +6990 6991 34.93 31.365000000000002 193412.21592306579 585981.7010273987 +6991 4326 193412.21592306579 585981.7010273987 34.9306939788724 31.36537323141648 +6990 6991 34.246 29.6415 126712.39623179393 395262.90677270875 +6991 4326 126712.39623179393 395262.90677270875 34.246680222561594 29.641887096646848 +6990 6991 35.614 33.0885 257759.23041669643 777145.3288331792 +6991 4326 257759.23041669643 777145.3288331792 35.6147084941712 33.08885933026556 +4744 7005 40.394999999999996 33.945 628917.4928982623 3756654.7242055763 +7005 4326 628917.4928982623 3756654.7242055763 40.39550413373839 33.94625457648279 +4744 7005 38.9505 31.4205 495295.14029697503 3475939.4764342452 +7005 4326 495295.14029697503 3475939.4764342452 38.95091618843168 31.42186293627322 +4744 7005 41.8395 36.4695 754432.0830817239 4039484.130850202 +7005 4326 754432.0830817239 4039484.130850202 41.84009811346179 36.470659066400884 +4744 7006 45.0 33.225 500000.0 3675954.8351221993 +7006 4326 500000.0 3675954.8351221993 45.00073810503768 33.22625965481736 +4744 7006 42.3 29.476499999999998 238176.9425811259 3263560.4359008833 +7006 4326 238176.9425811259 3263560.4359008833 42.30057565638703 29.477939114244013 +4744 7006 47.7 36.9735 740352.5309914062 4095048.2955903644 +7006 4326 740352.5309914062 4095048.2955903644 47.7009166449972 36.97460555761335 +4744 7007 48.375 30.3 247538.16145391518 3354686.741453908 +7007 4326 247538.16145391518 3354686.741453908 48.37588152223688 30.301366363898072 +4744 7007 48.0375 29.67 213258.6196166955 3285633.835037287 +7007 4326 213258.6196166955 3285633.835037287 48.038359579433106 29.671398759085452 +4744 7007 48.7125 30.93 281427.43724884687 3423823.226856111 +7007 4326 281427.43724884687 3423823.226856111 48.713403823910795 30.93133466816513 +6318 7057 -95.285 43.205 11490667.421889808 9601824.49455068 +7057 4326 11490667.421889808 9601824.49455068 -95.28499999999998 43.205 +6318 7057 -96.46849999999999 42.930499999999995 11173647.852970185 9504142.506676914 +7057 4326 11173647.852970185 9504142.506676914 -96.46849999999999 42.93049999999999 +6318 7057 -94.1015 43.4795 11804859.31447466 9703974.670763452 +7057 4326 11804859.31447466 9703974.670763452 -94.10149999999999 43.479500000000016 +6318 7058 -92.78999999999999 43.205 12489334.290188799 9813975.213139622 +7058 4326 12489334.290188799 9813975.213139622 -92.78999999999999 43.205 +6318 7058 -93.861 42.930499999999995 12202441.345833128 9715891.732707642 +7058 4326 12202441.345833128 9715891.732707642 -93.86100000000002 42.93049999999999 +6318 7058 -91.719 43.4795 12773669.998747833 9915716.916255146 +7058 4326 12773669.998747833 9915716.916255146 -91.719 43.479500000000016 +6318 7059 -91.255 42.9 13485261.91743645 9265661.413331639 +7059 4326 13485261.91743645 9265661.413331639 -91.255 42.89999999999999 +6318 7059 -91.5835 42.351 13396328.335732734 9065798.726993332 +7059 4326 13396328.335732734 9065798.726993332 -91.5835 42.351000000000006 +6318 7059 -90.9265 43.449 13572634.896082535 9465886.686700799 +7059 4326 13572634.896082535 9465886.686700799 -90.9265 43.449000000000005 +6318 7060 -94.825 42.56 14502245.295215368 8609719.155898971 +7060 4326 14502245.295215368 8609719.155898971 -94.825 42.559999999999995 +6318 7060 -96.4675 42.236000000000004 14057448.239352256 8495901.818843491 +7060 4326 14057448.239352256 8495901.818843491 -96.4675 42.23599999999999 +6318 7060 -93.1825 42.884 14942464.856927251 8732119.121233117 +7060 4326 14942464.856927251 8732119.121233117 -93.1825 42.88400000000001 +6318 7061 -92.31 42.645 15483856.047527583 8898183.39755797 +7061 4326 15483856.047527583 8898183.39755797 -92.31 42.644999999999996 +6318 7061 -92.958 42.2445 15308281.562113559 8753015.578126889 +7061 4326 15308281.562113559 8753015.578126889 -92.958 42.244499999999995 +6318 7061 -91.662 43.045500000000004 15657194.51033742 9044698.956031153 +7061 4326 15657194.51033742 9044698.956031153 -91.662 43.0455 +6318 7062 -95.70500000000001 41.4 16507773.779787218 7019006.643860236 +7062 4326 16507773.779787218 7019006.643860236 -95.70500000000001 41.4 +6318 7062 -96.3035 40.662 16341807.07403674 6750616.487281718 +7062 4326 16341807.07403674 6750616.487281718 -96.3035 40.66199999999999 +6318 7062 -95.10650000000001 42.138 16670023.887655746 7288565.7584176455 +7062 4326 16670023.887655746 7288565.7584176455 -95.10650000000001 42.13799999999999 +6318 7063 -94.66 41.685 17492715.51261783 7322863.07414939 +7063 4326 17492715.51261783 7322863.07414939 -94.66 41.68500000000001 +6318 7063 -95.11 41.2035 17368823.43477961 7147765.698736717 +7063 4326 17368823.43477961 7147765.698736717 -95.10999999999999 41.2035 +6318 7063 -94.21 42.1665 17614775.083282925 7498617.029078516 +7063 4326 17614775.083282925 7498617.029078516 -94.20999999999998 42.16649999999999 +6318 7064 -93.76 41.685 18488162.84996268 7522858.649614666 +7064 4326 18488162.84996268 7522858.649614666 -93.76 41.68500000000001 +6318 7064 -94.23700000000001 41.2035 18356808.236369874 7347830.39928132 +7064 4326 18356808.236369874 7347830.39928132 -94.23700000000001 41.2035 +6318 7064 -93.283 42.1665 18617575.274273954 7698622.711254255 +7064 4326 18617575.274273954 7698622.711254255 -93.283 42.1665 +6318 7065 -92.82 41.73 19499090.09019861 7739250.739919425 +7065 4326 19499090.09019861 7739250.739919425 -92.81999999999998 41.72999999999999 +6318 7065 -93.297 41.217 19367843.929824274 7552684.042911359 +7065 4326 19367843.929824274 7552684.042911359 -93.297 41.217000000000006 +6318 7065 -92.343 42.242999999999995 19628264.4515607 7926555.598094452 +7065 4326 19628264.4515607 7926555.598094452 -92.343 42.242999999999995 +6318 7066 -91.6 41.86 20518161.332498748 8009724.659505493 +7066 4326 20518161.332498748 8009724.659505493 -91.6 41.86 +6318 7066 -92.239 41.464 20343124.703581847 7865937.382889708 +7066 4326 20343124.703581847 7865937.382889708 -92.239 41.464 +6318 7066 -90.961 42.256 20691050.115028713 8154815.571517399 +7066 4326 20691050.115028713 8154815.571517399 -90.961 42.25599999999999 +6318 7067 -90.64 42.06 21471032.384997055 8259526.093469115 +7067 4326 21471032.384997055 8259526.093469115 -90.64 42.06 +6318 7067 -91.09 41.501999999999995 21347508.673204225 8056658.785559784 +7067 4326 21347508.673204225 8056658.785559784 -91.09 41.50199999999999 +6318 7067 -90.19 42.618 21592418.813482746 8463055.489042325 +7067 4326 21592418.813482746 8463055.489042325 -90.19 42.618 +6318 7068 -93.78 40.870000000000005 22491702.43568468 6182998.137121465 +7068 4326 22491702.43568468 6182998.137121465 -93.77999999999999 40.870000000000005 +6318 7068 -95.229 40.6 22089286.4509336 6088094.701486638 +7068 4326 22089286.4509336 6088094.701486638 -95.229 40.600000000000016 +6318 7068 -92.331 41.14 22890861.87480347 6284545.110758906 +7068 4326 22890861.87480347 6284545.110758906 -92.33099999999999 41.14000000000001 +6318 7069 -91.95 41.05500000000001 23490806.351550214 6693291.143318868 +7069 4326 23490806.351550214 6693291.143318868 -91.95 41.05500000000001 +6318 7069 -92.373 40.636500000000005 23373344.486312106 6541138.702096961 +7069 4326 23373344.486312106 6541138.702096961 -92.373 40.636500000000005 +6318 7069 -91.527 41.4735 23606789.864768114 6846020.161845024 +7069 4326 23606789.864768114 6846020.161845024 -91.52699999999999 41.47350000000001 +6318 7070 -91.25 40.980000000000004 24500000.0 6465962.022978187 +7070 4326 24500000.0 6465962.022978187 -91.24999999999999 40.980000000000004 +6318 7070 -91.673 40.422 24382221.50030746 6262943.898035953 +7070 4326 24382221.50030746 6262943.898035953 -91.67299999999999 40.422000000000004 +6318 7070 -90.827 41.538000000000004 24615809.80827831 6669565.283580399 +7070 4326 24615809.80827831 6669565.283580399 -90.827 41.538 +7073 7074 39.9 -23.305 592027.463625548 7422430.205362182 +7074 4326 592027.463625548 7422430.205362182 39.9 -23.304999999999993 +7073 7074 38.172 -25.4605 416759.02986831364 7183801.579992598 +7074 4326 416759.02986831364 7183801.579992598 38.172 -25.4605 +7073 7074 41.628 -21.1495 772923.2971842798 7659047.234348451 +7074 4326 772923.2971842798 7659047.234348451 41.628 -21.1495 +7073 7075 46.69 -46.25 630269.2023199567 4876787.16933931 +7075 4326 630269.2023199567 4876787.16933931 46.69 -46.25 +7073 7075 45.501999999999995 -49.067 536668.2064188081 4564974.745896775 +7075 4326 536668.2064188081 4564974.745896775 45.501999999999995 -49.06699999999999 +7073 7075 47.878 -43.433 732932.5971998764 5187075.688943923 +7075 4326 732932.5971998764 5187075.688943923 47.87800000000001 -43.43299999999999 +7073 7076 51.004999999999995 -46.215 500385.6592776187 4882064.070112407 +7076 4326 500385.6592776187 4882064.070112407 51.004999999999995 -46.214999999999996 +7073 7076 48.3005 -49.4595 304385.6308220684 4517958.553005814 +7076 4326 304385.6308220684 4517958.553005814 48.30050000000001 -49.45949999999999 +7073 7076 53.7095 -42.9705 720958.464016226 5238898.5839212565 +7076 4326 720958.464016226 5238898.5839212565 53.7095 -42.9705 +7073 7077 55.58 -46.455 390951.4118983629 4854417.291785344 +7077 4326 390951.4118983629 4854417.291785344 55.58 -46.455 +7073 7077 54.158 -49.2945 293369.8572609597 4535918.870233675 +7077 4326 293369.8572609597 4535918.870233675 54.158 -49.2945 +7073 7077 57.001999999999995 -43.6155 500161.38011169294 5170831.2830333235 +7077 4326 500161.38011169294 5170831.2830333235 57.001999999999995 -43.61549999999999 +7073 7078 64.48 -49.379999999999995 607423.2198848942 4529246.804939629 +7078 4326 607423.2198848942 4529246.804939629 64.48 -49.379999999999995 +7073 7078 63.112 -52.665 507574.4079272485 4164988.5169796813 +7078 4326 507574.4079272485 4164988.5169796813 63.112 -52.665 +7073 7078 65.848 -46.095 720146.1545383298 4891453.574370583 +7078 4326 720146.1545383298 4891453.574370583 65.848 -46.095000000000006 +7073 7079 69.005 -49.175 500364.4296532368 4553089.990947758 +7079 4326 500364.4296532368 4553089.990947758 69.005 -49.17499999999999 +7073 7079 66.3005 -52.8335 318157.5246461885 4142836.2030987404 +7079 4326 318157.5246461885 4142836.2030987404 66.3005 -52.833499999999994 +7073 7079 71.7095 -45.5165 711621.4066880543 4956099.123286956 +7079 4326 711621.4066880543 4956099.123286956 71.7095 -45.516499999999986 +7073 7080 75.005 -42.83 500408.66349884187 5258063.1213805955 +7080 4326 500408.66349884187 5258063.1213805955 75.005 -42.82999999999999 +7073 7080 72.3005 -50.354 307972.1638149336 4418524.314551632 +7080 4326 307972.1638149336 4418524.314551632 72.3005 -50.353999999999985 +7073 7080 77.7095 -35.306 746353.1215623603 6089653.855757009 +7080 4326 746353.1215623603 6089653.855757009 77.7095 -35.306 +7073 7081 79.91499999999999 -38.269999999999996 405088.88402851345 5763670.591906743 +7081 4326 405088.88402851345 5763670.591906743 79.91499999999999 -38.269999999999996 +7073 7081 78.1915 -41.663 266172.4771324983 5383828.214063905 +7081 4326 266172.4771324983 5383828.214063905 78.1915 -41.663000000000004 +7073 7081 81.6385 -34.877 558351.6552564817 6140411.108401426 +7081 4326 558351.6552564817 6140411.108401426 81.6385 -34.877 +6318 7109 -113.485 48.78 77597.76441102507 31609.89880936406 +7109 4326 77597.76441102507 31609.89880936406 -113.485 48.78 +6318 7109 -113.9215 48.573 45084.60964381302 9094.887550889036 +7109 4326 45084.60964381302 9094.887550889036 -113.92150000000001 48.573 +6318 7109 -113.0485 48.986999999999995 109848.48820084153 54310.40494994534 +7109 4326 109848.48820084153 54310.40494994534 -113.0485 48.98699999999999 +6318 7110 -112.92 48.504999999999995 68958.59690618758 56249.47902038165 +7110 4326 68958.59690618758 56249.47902038165 -112.92 48.504999999999995 +6318 7110 -113.748 48.0505 6941.581657147326 6370.0237686928185 +7110 4326 6941.581657147326 6370.0237686928185 -113.748 48.0505 +6318 7110 -112.092 48.9595 129883.93212295117 106796.5835604135 +7110 4326 129883.93212295117 106796.5835604135 -112.09200000000001 48.959500000000006 +6318 7111 -110.62 48.394999999999996 178144.49802799025 188392.3040274316 +7111 4326 178144.49802799025 188392.3040274316 -110.62 48.39499999999999 +6318 7111 -112.312 47.8415 51776.14596238687 127609.06406300592 +7111 4326 51776.14596238687 127609.06406300592 -112.312 47.841499999999996 +6318 7111 -108.928 48.948499999999996 301776.3066566672 251938.44042195796 +7111 4326 301776.3066566672 251938.44042195796 -108.928 48.94849999999998 +6318 7112 -108.91499999999999 48.394999999999996 169264.03823774488 138406.06324651657 +7112 4326 169264.03823774488 138406.06324651657 -108.91499999999999 48.39499999999999 +6318 7112 -110.64750000000001 47.8415 39243.07057773802 79025.09596317164 +7112 4326 39243.07057773802 79025.09596317164 -110.64750000000001 47.841499999999996 +6318 7112 -107.18249999999999 48.948499999999996 296512.9583795263 200712.7583182947 +7112 4326 296512.9583795263 200712.7583182947 -107.18249999999999 48.94849999999998 +6318 7113 -106.175 48.445 150057.16435517 112638.36225250404 +7113 4326 150057.16435517 112638.36225250404 -106.175 48.445 +6318 7113 -107.4305 48.0535 56080.987505600235 70691.64468716682 +7113 4326 56080.987505600235 70691.64468716682 -107.4305 48.05350000000001 +6318 7113 -104.9195 48.8365 242621.43614971248 156121.70862079377 +7113 4326 242621.43614971248 156121.70862079377 -104.9195 48.836500000000015 +6318 7114 -105.9 48.379999999999995 70367.18713491467 55266.94386640357 +7114 4326 70367.18713491467 55266.94386640357 -105.9 48.38 +6318 7114 -107.57400000000001 47.813 -55332.894796017936 -5762.469963580013 +7114 4326 -55332.894796017936 -5762.469963580013 -107.57400000000001 47.81299999999999 +6318 7114 -104.226 48.946999999999996 193329.20187927276 119024.01235581952 +7114 4326 193329.20187927276 119024.01235581952 -104.226 48.94699999999998 +6318 7115 -107.75 45.540000000000006 200000.0 87809.3366584343 +7115 4326 200000.0 87809.3366584343 -107.75 45.540000000000006 +6318 7115 -108.73100000000001 45.045 122700.38542845736 33256.45338941763 +7115 4326 122700.38542845736 33256.45338941763 -108.73100000000001 45.045 +6318 7115 -106.76899999999999 46.035000000000004 275954.67930935556 143303.3657114293 +7115 4326 275954.67930935556 143303.3657114293 -106.76899999999999 46.035000000000004 +6318 7116 -111.545 45.975 77133.59839303621 69469.53426423542 +7116 4326 77133.59839303621 69469.53426423542 -111.545 45.975 +6318 7116 -112.26050000000001 45.4215 20890.90815056242 8397.50048756288 +7116 4326 20890.90815056242 8397.50048756288 -112.26050000000001 45.4215 +6318 7116 -110.8295 46.5285 132267.9986583401 131049.14316657188 +7116 4326 132267.9986583401 131049.14316657188 -110.8295 46.52850000000001 +6318 7117 -108.705 46.2 177741.44732576743 96360.54603397418 +7117 4326 177741.44732576743 96360.54603397418 -108.70500000000001 46.199999999999996 +6318 7117 -109.3485 45.111000000000004 126655.42863052966 -24309.033584623656 +7117 4326 126655.42863052966 -24309.033584623656 -109.3485 45.111 +6318 7117 -108.0615 47.288999999999994 226879.8730998577 217477.00659160764 +7117 4326 226879.8730998577 217477.00659160764 -108.0615 47.288999999999994 +6318 7118 -108.72 43.275 68604.25242825452 67669.91048262845 +7118 4326 68604.25242825452 67669.91048262845 -108.72 43.27499999999999 +6318 7118 -109.422 42.7485 10846.045622499267 9667.768112238657 +7118 4326 10846.045622499267 9667.768112238657 -109.422 42.74850000000001 +6318 7118 -108.018 43.8015 125381.95114116183 126155.52379324354 +7118 4326 125381.95114116183 126155.52379324354 -108.018 43.8015 +6318 7119 -113.485 48.78 254585.8412915521 103707.01709108942 +7119 4326 254585.8412915521 103707.01709108942 -113.485 48.78 +6318 7119 -113.9215 48.573 147915.38601854668 29838.869917614946 +7119 4326 147915.38601854668 29838.869917614946 -113.92150000000001 48.573 +6318 7119 -113.0485 48.986999999999995 360395.30254423077 178183.74327409887 +7119 4326 360395.30254423077 178183.74327409887 -113.0485 48.98699999999999 +6318 7120 -112.92 48.504999999999995 226242.1158326364 184545.5348437718 +7120 4326 226242.1158326364 184545.5348437718 -112.92 48.504999999999995 +6318 7120 -113.748 48.0505 22774.21803394791 20899.028112509244 +7120 4326 22774.21803394791 20899.028112509244 -113.748 48.0505 +6318 7120 -112.092 48.9595 426128.38622884237 350382.4919961073 +7120 4326 426128.38622884237 350382.4919961073 -112.09200000000001 48.959500000000006 +6318 7121 -110.62 48.394999999999996 584463.5762553486 618084.9869640144 +7121 4326 584463.5762553486 618084.9869640144 -110.62 48.39499999999999 +6318 7121 -112.312 47.8415 169869.245331453 418664.9083405705 +7121 4326 169869.245331453 418664.9083405705 -112.312 47.841499999999996 +6318 7121 -108.928 48.948499999999996 990079.7462969394 826569.6864211218 +7121 4326 990079.7462969394 826569.6864211218 -108.928 48.94849999999998 +6318 7122 -108.91499999999999 48.394999999999996 555328.2094387955 454088.13405891263 +7122 4326 555328.2094387955 454088.13405891263 -108.91499999999999 48.39499999999999 +6318 7122 -110.64750000000001 47.8415 128750.23155163389 259268.68759124554 +7122 4326 128750.23155163389 259268.68759124554 -110.64750000000001 47.841499999999996 +6318 7122 -107.18249999999999 48.948499999999996 972811.5432372908 658506.4249768199 +7122 4326 972811.5432372908 658506.4249768199 -107.18249999999999 48.94849999999998 +6318 7123 -106.175 48.445 492313.53134635824 369548.43258564314 +7123 4326 492313.53134635824 369548.43258564314 -106.175 48.445 +6318 7123 -107.4305 48.0535 183992.74115748104 231927.96813243703 +7123 4326 183992.74115748104 231927.96813243703 -107.4305 48.05350000000001 +6318 7123 -104.9195 48.8365 796002.0871027311 512210.3301194021 +7123 4326 796002.0871027311 512210.3301194021 -104.9195 48.836500000000015 +6318 7124 -105.9 48.379999999999995 230863.47485076988 181321.99336169148 +7124 4326 230863.47485076988 181321.99336169148 -105.9 48.38 +6318 7124 -107.57400000000001 47.813 -181538.368754652 -18905.74230098429 +7124 4326 -181538.368754652 -18905.74230098429 -107.57400000000001 47.81299999999999 +6318 7124 -104.226 48.946999999999996 634282.1583952517 390498.7272508514 +7124 4326 634282.1583952517 390498.7272508514 -104.226 48.94699999999998 +6318 7125 -107.75 45.540000000000006 656167.9789999999 288088.3748636296 +7125 4326 656167.9789999999 288088.3748636296 -107.75 45.540000000000006 +6318 7125 -108.73100000000001 45.045 402560.3196445451 109109.09904664574 +7125 4326 402560.3196445451 109109.09904664574 -108.73100000000001 45.045 +6318 7125 -106.76899999999999 46.035000000000004 905363.1210910614 470155.39931571286 +7125 4326 905363.1210910614 470155.39931571286 -106.76899999999999 46.035000000000004 +6318 7126 -111.545 45.975 253062.98685248097 227918.41950077235 +7126 4326 253062.98685248097 227918.41950077235 -111.545 45.975 +6318 7126 -112.26050000000001 45.4215 68539.72490210766 27550.854616676104 +7126 4326 68539.72490210766 27550.854616676104 -112.26050000000001 45.4215 +6318 7126 -110.8295 46.5285 433950.1268305121 429951.2571068631 +7126 4326 433950.1268305121 429951.2571068631 -110.8295 46.52850000000001 +6318 7127 -108.705 46.2 583141.2313811267 316143.52378285496 +7127 4326 583141.2313811267 316143.52378285496 -108.70500000000001 46.199999999999996 +6318 7127 -109.3485 45.111000000000004 415536.1831684043 -79754.04714430332 +7127 4326 415536.1831684043 -79754.04714430332 -109.3485 45.111 +6318 7127 -108.0615 47.288999999999994 744356.5390389032 713507.2395231221 +7127 4326 744356.5390389032 713507.2395231221 -108.0615 47.288999999999994 +6318 7128 -108.72 43.275 225079.11814169845 222013.69797509018 +7128 4326 225079.11814169845 222013.69797509018 -108.72 43.27499999999999 +6318 7128 -109.422 42.7485 35584.06797981641 31718.335881569663 +7128 4326 35584.06797981641 31718.335881569663 -109.422 42.74850000000001 +6318 7128 -108.018 43.8015 411357.2846689619 413895.2476449999 +7128 4326 411357.2846689619 413895.2476449999 -108.018 43.8015 +6318 7131 -122.38 37.86 54159.98573353197 36211.60179306699 +7131 4326 54159.98573353197 36211.60179306699 -122.38 37.86 +6318 7131 -123.44200000000001 36.951 -40358.06139356531 -64217.18607368905 +7131 4326 -40358.06139356531 -64217.18607368905 -123.44200000000001 36.95100000000001 +6318 7131 -121.318 38.769 146381.22721287742 137719.82711818503 +7131 4326 146381.22721287742 137719.82711818503 -121.31799999999998 38.769 +6318 7132 -122.38 37.86 177689.88652742948 118804.23021608731 +7132 4326 177689.88652742948 118804.23021608731 -122.38 37.86 +6318 7132 -123.44200000000001 36.951 -132408.0730887222 -210685.8846434282 +7132 4326 -132408.0730887222 -210685.8846434282 -123.44200000000001 36.95100000000001 +6318 7132 -121.318 38.769 480252.4096142487 451835.79947024543 +7132 4326 480252.4096142487 451835.79947024543 -121.31799999999998 38.769 +4281 7142 36.74 31.28 315760.1518952198 77527.94727917865 +7142 4326 315760.1518952198 77527.94727917865 36.74076381855877 31.280255495334735 +4281 7142 34.427 29.39 94038.87569729448 -132736.36499778018 +7142 4326 94038.87569729448 -132736.36499778018 34.427541383818124 29.39051148908693 +4281 7142 39.053000000000004 33.17 528631.4536817487 292674.3787621665 +7142 4326 528631.4536817487 292674.3787621665 39.05399047482211 33.1700024464358 +6318 7257 -84.94 40.745000000000005 240844.6147915262 57654.9690128276 +7257 4326 240844.6147915262 57654.9690128276 -84.94 40.745 +6318 7257 -85.066 40.578500000000005 230178.0753267658 39171.37179782869 +7257 4326 230178.0753267658 39171.37179782869 -85.066 40.578500000000005 +6318 7257 -84.814 40.9115 251458.06767397604 76154.3784430646 +7257 4326 251458.06767397604 76154.3784430646 -84.814 40.9115 +6318 7258 -84.94 40.745000000000005 790171.0403618657 189156.34416958524 +7258 4326 790171.0403618657 189156.34416958524 -84.94 40.745 +6318 7258 -85.066 40.578500000000005 755175.9021345642 128514.74230670964 +7258 4326 755175.9021345642 128514.74230670964 -85.066 40.578500000000005 +6318 7258 -84.814 40.9115 824992.0103603698 249849.8232752878 +7258 4326 824992.0103603698 249849.8232752878 -84.814 40.9115 +6318 7259 -85.07 41.095 238319.66258730966 57656.366735402786 +7259 4326 238319.66258730966 57656.366735402786 -85.07000000000001 41.095 +6318 7259 -85.313 40.9285 217847.85374800477 39198.394547398784 +7259 4326 217847.85374800477 39198.394547398784 -85.313 40.9285 +6318 7259 -84.827 41.2615 258688.38240793327 76171.78168885218 +7259 4326 258688.38240793327 76171.78168885218 -84.827 41.2615 +6318 7260 -85.07 41.095 781887.0930051985 189160.92986440065 +7260 4326 781887.0930051985 189160.92986440065 -85.07000000000001 41.095 +6318 7260 -85.313 40.9285 714722.5001715791 128603.39944425752 +7260 4326 714722.5001715791 128603.39944425752 -85.313 40.9285 +6318 7260 -84.827 41.2615 848713.4679500278 249906.92042417589 +7260 4326 848713.4679500278 249906.92042417589 -84.827 41.2615 +6318 7261 -85.885 39.195 236976.33697050344 57649.52827672522 +7261 4326 236976.33697050344 57649.52827672522 -85.885 39.195 +6318 7261 -86.0695 39.0465 220997.45259353044 41185.30459175019 +7261 4326 220997.45259353044 41185.30459175019 -86.0695 39.0465 +6318 7261 -85.7005 39.3435 252888.1338822507 74146.59573702916 +7261 4326 252888.1338822507 74146.59573702916 -85.7005 39.3435 +6318 7262 -85.885 39.195 777479.8655440601 189138.4940212227 +7262 4326 777479.8655440601 189138.4940212227 -85.885 39.195 +6318 7262 -86.0695 39.0465 725055.8090506078 135122.12014810042 +7262 4326 725055.8090506078 135122.12014810042 -86.0695 39.0465 +6318 7262 -85.7005 39.3435 829683.8192453509 243262.62284723652 +7262 4326 829683.8192453509 243262.62284723652 -85.7005 39.3435 +6318 7263 -87.31 40.605000000000004 239153.62087088247 53212.489118477635 +7263 4326 239153.62087088247 53212.489118477635 -87.31 40.605000000000004 +6318 7263 -87.508 40.4835 222363.46849436394 39740.852692380126 +7263 4326 222363.46849436394 39740.852692380126 -87.508 40.4835 +6318 7263 -87.11200000000001 40.7265 255883.08205194576 66722.09934867578 +7263 4326 255883.08205194576 66722.09934867578 -87.11200000000001 40.7265 +6318 7264 -87.31 40.605000000000004 784623.1711405537 174581.30804953873 +7264 4326 784623.1711405537 174581.30804953873 -87.31 40.605000000000004 +6318 7264 -87.508 40.4835 729537.4795519258 130383.11420825048 +7264 4326 729537.4795519258 130383.11420825048 -87.508 40.4835 +6318 7264 -87.11200000000001 40.7265 839509.7450320922 218904.0876131138 +7264 4326 839509.7450320922 218904.0876131138 -87.11200000000001 40.7265 +6318 7265 -85.39 40.32 240849.97136495874 65981.49860829025 +7265 4326 240849.97136495874 65981.49860829025 -85.39 40.32 +6318 7265 -85.56099999999999 40.095 226270.13570315682 41009.23513173265 +7265 4326 226270.13570315682 41009.23513173265 -85.56099999999999 40.095 +6318 7265 -85.21900000000001 40.545 255333.29508489312 90982.80869001398 +7265 4326 255333.29508489312 90982.80869001398 -85.21900000000001 40.545 +6318 7266 -85.39 40.32 790188.6143865356 216474.3000173656 +7266 4326 790188.6143865356 216474.3000173656 -85.39 40.32 +6318 7266 -85.56099999999999 40.095 742354.6035527737 134544.46559469288 +7266 4326 742354.6035527737 134544.46559469288 -85.56099999999999 40.095 +6318 7266 -85.21900000000001 40.545 837705.9856243536 298499.4315104876 +7266 4326 837705.9856243536 298499.4315104876 -85.21900000000001 40.545 +6318 7267 -86.47 39.894999999999996 242565.82763702804 68755.39645825062 +7267 4326 242565.82763702804 68755.39645825062 -86.47 39.894999999999996 +6318 7267 -86.677 39.6295 224803.36440411306 39290.39301978492 +7267 4326 224803.36440411306 39290.39301978492 -86.677 39.6295 +6318 7267 -86.26299999999999 40.1605 260191.6176406798 98262.79633256831 +7267 4326 260191.6176406798 98262.79633256831 -86.26299999999999 40.160500000000006 +6318 7268 -86.47 39.894999999999996 795818.0528391496 225574.99654677726 +7268 4326 795818.0528391496 225574.99654677726 -86.47 39.894999999999996 +6318 7268 -86.677 39.6295 737542.3713824943 128905.2310990777 +7268 4326 737542.3713824943 128905.2310990777 -86.677 39.6295 +6318 7268 -86.26299999999999 40.1605 853645.3322094637 322383.85763443453 +7268 4326 853645.3322094637 322383.85763443453 -86.26299999999999 40.160500000000006 +6318 7269 -86.22999999999999 39.195 246047.35047748015 57651.3657781009 +7269 4326 246047.35047748015 57651.3657781009 -86.22999999999999 39.19500000000001 +6318 7269 -86.374 39.0555 233594.46451764644 42164.17974107121 +7269 4326 233594.46451764644 42164.17974107121 -86.374 39.0555 +6318 7269 -86.086 39.3345 258451.01021206187 73158.70164260617 +7269 4326 258451.01021206187 73158.70164260617 -86.086 39.3345 +6318 7270 -86.22999999999999 39.195 807240.3490248662 189144.52255698608 +7270 4326 807240.3490248662 189144.52255698608 -86.22999999999999 39.19500000000001 +6318 7270 -86.374 39.0555 766384.5056716452 138333.64636716447 +7270 4326 766384.5056716452 138333.64636716447 -86.374 39.0555 +6318 7270 -86.086 39.3345 847934.6893374064 240021.50697245044 +7270 4326 847934.6893374064 240021.50697245044 -86.086 39.3345 +6318 7271 -86.575 40.585 246349.7164770158 56546.39851852849 +7271 4326 246349.7164770158 56546.39851852849 -86.575 40.585 +6318 7271 -86.7595 40.4455 230710.18014577415 41058.336047771794 +7271 4326 230710.18014577415 41058.336047771794 -86.7595 40.4455 +6318 7271 -86.3905 40.7245 261924.31821539503 72067.5799100821 +7271 4326 261924.31821539503 72067.5799100821 -86.3905 40.7245 +6318 7272 -86.575 40.585 808232.3614750095 185519.3091395389 +7272 4326 808232.3614750095 185519.3091395389 -86.575 40.585 +6318 7272 -86.7595 40.4455 756921.6493615941 134705.55751673132 +7272 4326 756921.6493615941 134705.55751673132 -86.7595 40.4455 +6318 7272 -86.3905 40.7245 859330.0340116753 236441.71842166106 +7272 4326 859330.0340116753 236441.71842166106 -86.3905 40.7245 +6318 7273 -86.375 40.74 242111.68244377244 57099.831427333396 +7273 4326 242111.68244377244 57099.831427333396 -86.375 40.739999999999995 +6318 7273 -86.5685 40.578 225732.78538864432 39123.00733246906 +7273 4326 225732.78538864432 39123.00733246906 -86.5685 40.578 +6318 7273 -86.1815 40.902 258411.2618721639 75113.19370304723 +7273 4326 258411.2618721639 75113.19370304723 -86.1815 40.90200000000001 +6318 7274 -86.375 40.74 794328.0781509435 187335.03027450969 +7274 4326 794328.0781509435 187335.03027450969 -86.375 40.739999999999995 +6318 7274 -86.5685 40.578 740591.646729244 128356.06655660893 +7274 4326 740591.646729244 128356.06655660893 -86.5685 40.578 +6318 7274 -86.1815 40.902 847804.2816589244 246433.86967408084 +7274 4326 847804.2816589244 246433.86967408084 -86.1815 40.90200000000001 +6318 7275 -85.725 38.505 229096.42712697797 75414.1852665742 +7275 4326 229096.42712697797 75414.1852665742 -85.725 38.505 +6318 7275 -86.00850000000001 38.203500000000005 204218.99138537078 42017.50225525601 +7275 4326 204218.99138537078 42017.50225525601 -86.00850000000001 38.20350000000001 +6318 7275 -85.44149999999999 38.8065 253767.89680909406 108888.60737307266 +7275 4326 253767.89680909406 108888.60737307266 -85.44149999999999 38.8065 +6318 7276 -85.725 38.505 751627.1946657603 247421.37282875224 +7276 4326 751627.1946657603 247421.37282875224 -85.725 38.505 +6318 7276 -86.00850000000001 38.203500000000005 670008.4742368374 137852.42198245245 +7276 4326 670008.4742368374 137852.42198245245 -86.00850000000001 38.20350000000001 +6318 7276 -85.44149999999999 38.8065 832570.1747811695 357245.37268982263 +7276 4326 832570.1747811695 357245.37268982263 -85.44149999999999 38.8065 +6318 7277 -87.09 39.385 245169.42362775744 62092.18251899119 +7277 4326 245169.42362775744 62092.18251899119 -87.09 39.38499999999999 +6318 7277 -87.234 39.1825 232741.93719918458 39611.55431167766 +7277 4326 232741.93719918458 39611.55431167766 -87.234 39.182500000000005 +6318 7277 -86.94600000000001 39.5875 257525.1433497159 84593.40364218285 +7277 4326 257525.1433497159 84593.40364218285 -86.94600000000001 39.5875 +6318 7278 -87.09 39.385 804360.0173520676 203714.10214772364 +7278 4326 804360.0173520676 203714.10214772364 -87.09 39.38499999999999 +6318 7278 -87.234 39.1825 763587.5056276582 129958.9077708958 +7278 4326 763587.5056276582 129958.9077708958 -87.234 39.182500000000005 +6318 7278 -86.94600000000001 39.5875 844897.0744731929 277536.85844939493 +7278 4326 844897.0744731929 277536.85844939493 -86.94600000000001 39.5875 +6318 7279 -86.47 40.305 251052.00803027442 53219.70732025201 +7279 4326 251052.00803027442 53219.70732025201 -86.47 40.30500000000001 +6318 7279 -86.677 40.1835 233442.09737973608 39722.72993286492 +7279 4326 233442.09737973608 39722.72993286492 -86.677 40.1835 +6318 7279 -86.26299999999999 40.4265 268598.8314593814 66758.13208317777 +7279 4326 268598.8314593814 66758.13208317777 -86.26299999999999 40.426500000000004 +6318 7280 -86.47 40.305 823659.7963459921 174604.98976652682 +7280 4326 823659.7963459921 174604.98976652682 -86.47 40.30500000000001 +6318 7280 -86.677 40.1835 765884.6144866842 130323.656454741 +7280 4326 765884.6144866842 130323.656454741 -86.677 40.1835 +6318 7280 -86.26299999999999 40.4265 881227.9995463205 219022.3050095591 +7280 4326 881227.9995463205 219022.3050095591 -86.26299999999999 40.426500000000004 +6318 7281 -86.465 38.55 243051.11134275488 85953.01615412845 +7281 4326 243051.11134275488 85953.01615412845 -86.465 38.55 +6318 7281 -86.6675 38.145 225316.70545874376 41008.32727351997 +7281 4326 225316.70545874376 41008.32727351997 -86.6675 38.14500000000001 +6318 7281 -86.26249999999999 38.955 260587.32365454632 130939.74517987165 +7281 4326 260587.32365454632 130939.74517987165 -86.26249999999999 38.955000000000005 +6318 7282 -86.465 38.55 797410.1877970217 281997.5204990031 +7282 4326 797410.1877970217 281997.5204990031 -86.465 38.55 +6318 7282 -86.6675 38.145 739226.5578258953 134541.4870632068 +7282 4326 739226.5578258953 134541.4870632068 -86.6675 38.14500000000001 +6318 7282 -86.26249999999999 38.955 854943.5776899575 429591.48064429563 +7282 4326 854943.5776899575 429591.48064429563 -86.26249999999999 38.955000000000005 +6318 7283 -86.98 38.835 250419.45772827396 78745.94940761258 +7283 4326 250419.45772827396 78745.94940761258 -86.98 38.834999999999994 +6318 7283 -87.25 38.5245 226919.2797517546 44280.739584127834 +7283 4326 226919.2797517546 44280.739584127834 -87.25 38.5245 +6318 7283 -86.71000000000001 39.1455 273715.6558738069 113282.41679774417 +7283 4326 273715.6558738069 113282.41679774417 -86.71000000000001 39.145500000000006 +6318 7284 -86.98 38.835 821584.5042301789 258352.33568147564 +7284 4326 821584.5042301789 258352.33568147564 -86.98 38.834999999999994 +6318 7284 -87.25 38.5245 744484.3369855483 145277.7264522594 +7284 4326 744484.3369855483 145277.7264522594 -87.25 38.5245 +6318 7284 -86.71000000000001 39.1455 898015.4476459817 371660.72911059903 +7284 4326 898015.4476459817 371660.72911059903 -86.71000000000001 39.145500000000006 +6318 7285 -84.995 38.995000000000005 231769.6729961434 74304.57771224977 +7285 4326 231769.6729961434 74304.57771224977 -84.995 38.995 +6318 7285 -85.18849999999999 38.7115 214906.35963375116 42866.78673480888 +7285 4326 214906.35963375116 42866.78673480888 -85.18849999999999 38.711499999999994 +6318 7285 -84.8015 39.2785 248499.3971895879 105779.45139510225 +7285 4326 248499.3971895879 105779.45139510225 -84.8015 39.27850000000001 +6318 7286 -84.995 38.995000000000005 760397.6688215139 243780.93537760613 +7286 4326 760397.6688215139 243780.93537760613 -84.995 38.995 +6318 7286 -85.18849999999999 38.7115 705071.948231732 140638.78281245215 +7286 4326 705071.948231732 140638.78281245215 -85.18849999999999 38.711499999999994 +6318 7286 -84.8015 39.2785 815285.1056128398 347044.7501187647 +7286 4326 815285.1056128398 347044.7501187647 -84.8015 39.27850000000001 +6318 7287 -85.49000000000001 39.46 253770.5312813665 75981.1575048809 +7287 4326 253770.5312813665 75981.1575048809 -85.49000000000001 39.46 +6318 7287 -85.67 39.163000000000004 238271.3918406153 42994.57579353751 +7287 4326 238271.3918406153 42994.57579353751 -85.67 39.163 +6318 7287 -85.31 39.757 269137.6465809472 109000.46928463018 +7287 4326 269137.6465809472 109000.46928463018 -85.31 39.757 +6318 7288 -85.49000000000001 39.46 832578.8180456167 249281.51424726346 +7288 4326 832578.8180456167 249281.51424726346 -85.49000000000001 39.46 +6318 7288 -85.67 39.163000000000004 781728.7247304188 141058.03741596433 +7288 4326 781728.7247304188 141058.03741596433 -85.67 39.163 +6318 7288 -85.31 39.757 882995.7621576578 357612.3729779909 +7288 4326 882995.7621576578 357612.3729779909 -85.31 39.757 +6318 7289 -85.0 41.4 235818.62517177817 52660.835546498034 +7289 4326 235818.62517177817 52660.835546498034 -85.0 41.39999999999999 +6318 7289 -85.18 41.274 220728.56579282897 38691.02728048979 +7289 4326 220728.56579282897 38691.02728048979 -85.18 41.274 +6318 7289 -84.82 41.525999999999996 250850.55114976736 66662.21488212023 +7289 4326 250850.55114976736 66662.21488212023 -84.82 41.52599999999999 +6318 7290 -85.0 41.4 773681.6060844089 172771.42462213564 +7290 4326 773681.6060844089 172771.42462213564 -85.0 41.39999999999999 +6318 7290 -85.18 41.274 724173.6362719731 126938.81200274026 +7290 4326 724173.6362719731 126938.81200274026 -85.18 41.274 +6318 7290 -84.82 41.525999999999996 822998.8498971951 218707.6166590895 +7290 4326 822998.8498971951 218707.6166590895 -84.82 41.52599999999999 +6318 7291 -86.875 38.555 246537.61056327922 75409.74467766352 +7291 4326 246537.61056327922 75409.74467766352 -86.875 38.55499999999999 +6318 7291 -87.0595 38.2355 230412.99009920371 39946.269878087514 +7291 4326 230412.99009920371 39946.269878087514 -87.0595 38.23550000000001 +6318 7291 -86.6905 38.8745 262519.6758330893 110907.50330937572 +7291 4326 262519.6758330893 110907.50330937572 -86.6905 38.874500000000005 +6318 7292 -86.875 38.555 808848.8106563586 247406.80399663444 +7292 4326 808848.8106563586 247406.80399663444 -86.875 38.55499999999999 +6318 7292 -87.0595 38.2355 755946.6183504709 131057.0537583588 +7292 4326 755946.6183504709 131057.0537583588 -87.0595 38.23550000000001 +6318 7292 -86.6905 38.8745 861283.3031290605 363869.0337741769 +7292 4326 861283.3031290605 363869.0337741769 -86.6905 38.874500000000005 +6318 7293 -85.85499999999999 41.21 239580.64826358843 98191.49588629106 +7293 4326 239580.64826358843 98191.49588629106 -85.85499999999999 41.21 +6318 7293 -86.0575 40.705999999999996 222464.02127980342 42239.584970099095 +7293 4326 222464.02127980342 42239.584970099095 -86.0575 40.705999999999996 +6318 7293 -85.65249999999999 41.714000000000006 256436.63718333724 154187.85745055735 +7293 4326 256436.63718333724 154187.85745055735 -85.65249999999999 41.71400000000001 +6318 7294 -85.85499999999999 41.21 786024.1768447898 322149.93275360664 +7294 4326 786024.1768447898 322149.93275360664 -85.85499999999999 41.21 +6318 7294 -86.0575 40.705999999999996 729867.3764821552 138581.0383560668 +7294 4326 729867.3764821552 138581.0383560668 -86.0575 40.705999999999996 +6318 7294 -85.65249999999999 41.714000000000006 841325.867158999 505864.66231903696 +7294 4326 841325.867158999 505864.66231903696 -85.65249999999999 41.71400000000001 +6318 7295 -85.06 39.525 239140.1413580086 66532.50174438741 +7295 4326 239140.1413580086 66532.50174438741 -85.06 39.525 +6318 7295 -85.285 39.2865 219724.36500172285 40078.73486100769 +7295 4326 219724.36500172285 40078.73486100769 -85.285 39.2865 +6318 7295 -84.83500000000001 39.7635 258423.5692672691 93035.70127318778 +7295 4326 258423.5692672691 93035.70127318778 -84.83500000000002 39.76349999999999 +6318 7296 -85.06 39.525 784578.9471054 218282.04947304438 +7296 4326 784578.9471054 218282.04947304438 -85.06 39.525 +6318 7296 -85.285 39.2865 720879.0208431524 131491.64928982273 +7296 4326 720879.0208431524 131491.64928982273 -85.285 39.2865 +6318 7296 -84.83500000000001 39.7635 847844.6601710322 305234.6299271169 +7296 4326 847844.6601710322 305234.6299271169 -84.83500000000002 39.76349999999999 +6318 7297 -87.315 40.215 238723.08655804524 65425.44151082285 +7297 4326 238723.08655804524 65425.44151082285 -87.315 40.215 +6318 7297 -87.51750000000001 39.9765 221419.9983683497 38965.12993605695 +7297 4326 221419.9983683497 38965.12993605695 -87.51750000000001 39.9765 +6318 7297 -87.1125 40.4535 255905.3267541238 91926.175069083 +7297 4326 255905.3267541238 91926.175069083 -87.1125 40.4535 +6318 7298 -87.315 40.215 783210.6598158535 214649.969356758 +7298 4326 783210.6598158535 214649.969356758 -87.315 40.215 +6318 7298 -87.51750000000001 39.9765 726442.1113134942 127838.09713188017 +7298 4326 726442.1113134942 127838.09713188017 -87.51750000000001 39.9765 +6318 7298 -87.1125 40.4535 839582.7261924879 301594.45937248314 +7298 4326 839582.7261924879 301594.45937248314 -87.1125 40.4535 +6318 7299 -86.235 41.335 245441.1727618051 84312.97457575286 +7299 4326 245441.1727618051 84312.97457575286 -86.235 41.334999999999994 +6318 7299 -86.5005 40.9435 223115.9701784578 40850.28821780505 +7299 4326 223115.9701784578 40850.28821780505 -86.5005 40.9435 +6318 7299 -85.9695 41.7265 267499.98656718404 127846.70090303732 +7299 4326 267499.98656718404 127846.70090303732 -85.9695 41.7265 +6318 7300 -86.235 41.335 805251.5809693557 276616.8174206159 +7300 4326 805251.5809693557 276616.8174206159 -86.235 41.334999999999994 +6318 7300 -86.5005 40.9435 732006.3121604903 134022.98726124875 +7300 4326 732006.3121604903 134022.98726124875 -86.5005 40.9435 +6318 7300 -85.9695 41.7265 877622.8725958364 419443.7178793816 +7300 4326 877622.8725958364 419443.7178793816 -85.9695 41.7265 +6318 7301 -87.65 38.349999999999994 240000.0 58200.53119844892 +7301 4326 240000.0 58200.53119844892 -87.65 38.349999999999994 +6318 7301 -87.95599999999999 38.178999999999995 213188.34804287236 39263.285556812145 +7301 4326 213188.34804287236 39263.285556812145 -87.95599999999999 38.179 +6318 7301 -87.34400000000001 38.521 266685.85011331644 77226.96890551932 +7301 4326 266685.85011331644 77226.96890551932 -87.34400000000001 38.52100000000001 +6318 7302 -87.65 38.349999999999994 787400.0000000001 190946.24277357786 +7302 4326 787400.0000000001 190946.24277357786 -87.65 38.349999999999994 +6318 7302 -87.95599999999999 38.178999999999995 699435.4385373238 128816.29603097452 +7302 4326 699435.4385373238 128816.29603097452 -87.95599999999999 38.179 +6318 7302 -87.34400000000001 38.521 874951.8265801058 253368.81381752464 +7302 4326 874951.8265801058 253368.81381752464 -87.34400000000001 38.52100000000001 +6318 7303 -85.655 40.515 243813.82934069893 54323.68356729944 +7303 4326 243813.82934069893 54323.68356729944 -85.655 40.515 +6318 7303 -85.8485 40.384499999999996 227389.99311222555 39841.656672441764 +7303 4326 227389.99311222555 39841.656672441764 -85.8485 40.384499999999996 +6318 7303 -85.4615 40.6455 260174.06234804835 68842.03102314509 +7303 4326 260174.06234804835 68842.03102314509 -85.4615 40.645500000000006 +6318 7304 -85.655 40.515 799912.5384286098 178226.95183704828 +7304 4326 799912.5384286098 178226.95183704828 -85.655 40.515 +6318 7304 -85.8485 40.384499999999996 746028.6690690267 130713.83526616936 +7304 4326 746028.6690690267 130713.83526616936 -85.8485 40.384499999999996 +6318 7304 -85.4615 40.6455 853587.7362202221 225859.23011510185 +7304 4326 853587.7362202221 225859.23011510185 -85.4615 40.645500000000006 +6318 7305 -86.055 40.165 235314.50245145394 65426.794187536914 +7305 4326 235314.50245145394 65426.794187536914 -86.055 40.165 +6318 7305 -86.2305 39.944500000000005 220300.1104657553 40966.58454058905 +7305 4326 220300.1104657553 40966.58454058905 -86.2305 39.94450000000001 +6318 7305 -85.8795 40.38549999999999 250232.21053738566 89917.45161376917 +7305 4326 250232.21053738566 89917.45161376917 -85.8795 40.3855 +6318 7306 -86.055 40.165 772027.6634594785 214654.40726361072 +7306 4326 772027.6634594785 214654.40726361072 -86.055 40.165 +6318 7306 -86.2305 39.944500000000005 722767.9457530656 134404.53611358258 +7306 4326 722767.9457530656 134404.53611358258 -86.2305 39.94450000000001 +6318 7306 -85.8795 40.38549999999999 820970.1774047395 295004.17250284104 +7306 4326 820970.1774047395 295004.17250284104 -85.8795 40.3855 +6318 7307 -85.76499999999999 40.035 242987.3660605025 78749.29498376898 +7307 4326 242987.3660605025 78749.29498376898 -85.76499999999999 40.035 +6318 7307 -85.9405 39.7245 227953.64956866903 44281.37004893662 +7307 4326 227953.64956866903 44281.37004893662 -85.9405 39.724500000000006 +6318 7307 -85.58949999999999 40.3455 257885.13478887724 113248.60844761164 +7307 4326 257885.13478887724 113248.60844761164 -85.58949999999999 40.3455 +6318 7308 -85.76499999999999 40.035 797201.0501501655 258363.31195924876 +7308 4326 797201.0501501655 258363.31195924876 -85.76499999999999 40.035 +6318 7308 -85.9405 39.7245 747877.931959875 145279.79490221955 +7308 4326 747877.931959875 145279.79490221955 -85.9405 39.724500000000006 +6318 7308 -85.58949999999999 40.3455 846078.1463865081 371549.8095485392 +7308 4326 846078.1463865081 371549.8095485392 -85.58949999999999 40.3455 +6318 7309 -86.08500000000001 38.370000000000005 245680.460104343 82623.05208740858 +7309 4326 245680.460104343 82623.05208740858 -86.08500000000001 38.370000000000005 +6318 7309 -86.3055 37.992000000000004 226340.1958234174 40673.3648838159 +7309 4326 226340.1958234174 40673.3648838159 -86.3055 37.99200000000001 +6318 7309 -85.8645 38.748 264820.01874708774 124621.5661221339 +7309 4326 264820.01874708774 124621.5661221339 -85.8645 38.748 +6318 7310 -86.08500000000001 38.370000000000005 806036.6428589987 271072.46339010634 +7310 4326 806036.6428589987 271072.46339010634 -86.08500000000001 38.370000000000005 +6318 7310 -86.3055 37.992000000000004 742584.459130662 133442.5312896527 +7310 4326 742584.459130662 133442.5312896527 -86.3055 37.99200000000001 +6318 7310 -85.8645 38.748 868830.3448394038 408862.588185701 +7310 4326 868830.3448394038 408862.588185701 -85.8645 38.748 +6318 7311 -85.4 39.93 244274.2333756247 55987.73715326969 +7311 4326 244274.2333756247 55987.73715326969 -85.4 39.93 +6318 7311 -85.58 39.795 228865.1923747643 41004.66177041218 +7311 4326 228865.1923747643 41004.66177041218 -85.58 39.79500000000001 +6318 7311 -85.22 40.065 259622.80452989793 71002.20438282553 +7311 4326 259622.80452989793 71002.20438282553 -85.22 40.065000000000005 +6318 7312 -85.4 39.93 801423.0473331955 183686.43431035234 +7312 4326 801423.0473331955 183686.43431035234 -85.4 39.93 +6318 7312 -85.58 39.795 750868.5519828727 134529.46115842732 +7312 4326 750868.5519828727 134529.46115842732 -85.58 39.79500000000001 +6318 7312 -85.22 40.065 851779.1511951735 232946.3988793201 +7312 4326 851779.1511951735 232946.3988793201 -85.22 40.065000000000005 +6318 7313 -86.12 40.685 242536.11251999944 73201.52504545037 +7313 4326 242536.11251999944 73201.52504545037 -86.12000000000002 40.685 +6318 7313 -86.354 40.4015 222681.56707566048 41738.81594924405 +7313 4326 222681.56707566048 41738.81594924405 -86.354 40.401500000000006 +6318 7313 -85.886 40.9685 262222.96030183195 104718.47001177665 +7313 4326 262222.96030183195 104718.47001177665 -85.886 40.96849999999999 +6318 7314 -86.12 40.685 795720.5624926983 240162.00341994845 +7314 4326 795720.5624926983 240162.00341994845 -86.12000000000002 40.685 +6318 7314 -86.354 40.4015 730581.1079807295 136938.09866014487 +7314 4326 730581.1079807295 136938.09866014487 -86.354 40.401500000000006 +6318 7314 -85.886 40.9685 860309.8289235937 343563.8470303039 +7314 4326 860309.8289235937 343563.8470303039 -85.886 40.96849999999999 +6318 7315 -85.495 40.974999999999994 240420.84914511224 72092.58205066147 +7315 4326 240420.84914511224 72092.58205066147 -85.495 40.974999999999994 +6318 7315 -85.6705 40.6825 225585.84223468535 39623.14566819795 +7315 4326 225585.84223468535 39623.14566819795 -85.6705 40.682500000000005 +6318 7315 -85.31949999999999 41.2675 255125.35424496012 104593.34267361196 +7315 4326 255125.35424496012 104593.34267361196 -85.31949999999999 41.2675 +6318 7316 -85.495 40.974999999999994 788780.7359035892 236523.74627787853 +7316 4326 788780.7359035892 236523.74627787853 -85.495 40.974999999999994 +6318 7316 -85.6705 40.6825 740109.5507316303 129996.93707974612 +7316 4326 740109.5507316303 129996.93707974612 -85.6705 40.682500000000005 +6318 7316 -85.31949999999999 41.2675 837023.7663853401 343153.32508834195 +7316 4326 837023.7663853401 343153.32508834195 -85.31949999999999 41.2675 +6318 7317 -86.055 38.9 230891.23672175602 58208.06323800067 +7317 4326 230891.23672175602 58208.06323800067 -86.055 38.9 +6318 7317 -86.2935 38.738 210133.70771709143 40274.50053519342 +7317 4326 210133.70771709143 40274.50053519342 -86.2935 38.73800000000001 +6318 7317 -85.8165 39.062 251554.78184332277 76196.14979307108 +7317 4326 251554.78184332277 76196.14979307108 -85.8165 39.06199999999999 +6318 7318 -86.055 38.9 757515.6658112947 190970.95414000723 +7318 4326 757515.6658112947 190970.95414000723 -86.055 38.9 +6318 7318 -86.2935 38.738 689413.6727351575 132133.92383921376 +7318 4326 689413.6727351575 132133.92383921376 -86.2935 38.73800000000001 +6318 7318 -85.8165 39.062 825309.3134309682 249986.86811276738 +7318 4326 825309.3134309682 249986.86811276738 -85.8165 39.06199999999999 +6318 7319 -87.1 41.25 240000.0 97081.03341063419 +7319 4326 240000.0 97081.03341063419 -87.10000000000001 41.25 +6318 7319 -87.262 40.782 226324.91911963178 45118.88387176985 +7319 4326 226324.91911963178 45118.88387176985 -87.262 40.782000000000004 +6318 7319 -86.938 41.718 253481.28584157478 149072.73263914546 +7319 4326 253481.28584157478 149072.73263914546 -86.938 41.718 +6318 7320 -87.1 41.25 787400.0000000001 318506.6904480557 +7320 4326 787400.0000000001 318506.6904480557 -87.10000000000001 41.25 +6318 7320 -87.262 40.782 742534.3388116587 148027.53816929826 +7320 4326 742534.3388116587 148027.53816929826 -87.262 40.782000000000004 +6318 7320 -86.938 41.718 831629.8519652333 489082.79033359647 +7320 4326 831629.8519652333 489082.79033359647 -86.938 41.718 +6318 7321 -85.00999999999999 40.44 239151.53537834002 51546.48395751958 +7321 4326 239151.53537834002 51546.48395751958 -85.00999999999999 40.44 +6318 7321 -85.199 40.314 223084.06698149865 37573.63238150893 +7321 4326 223084.06698149865 37573.63238150893 -85.199 40.314 +6318 7321 -84.821 40.565999999999995 255159.1293082372 65553.95062670321 +7321 4326 255159.1293082372 65553.95062670321 -84.821 40.565999999999995 +6318 7322 -85.00999999999999 40.44 784616.328987104 169115.4227839622 +7322 4326 784616.328987104 169115.4227839622 -85.00999999999999 40.44 +6318 7322 -85.199 40.314 731901.6430884668 123272.82557166721 +7322 4326 731901.6430884668 123272.82557166721 -85.199 40.314 +6318 7322 -84.821 40.565999999999995 837134.576738775 215071.58634777548 +7322 4326 837134.576738775 215071.58634777548 -84.821 40.565999999999995 +6318 7323 -85.445 38.75 231741.37844623925 58206.669511036336 +7323 4326 231741.37844623925 58206.669511036336 -85.445 38.75 +6318 7323 -85.6655 38.597 212514.21114369325 41264.70136746776 +7323 4326 212514.21114369325 41264.70136746776 -85.6655 38.59700000000001 +6318 7323 -85.2245 38.903 250886.748629879 75195.20979716003 +7323 4326 250886.748629879 75195.20979716003 -85.2245 38.903000000000006 +6318 7324 -85.445 38.75 760304.8391190367 190966.38155412508 +7324 4326 760304.8391190367 190966.38155412508 -85.445 38.75 +6318 7324 -85.6655 38.597 697223.707727267 135382.60773643383 +7324 4326 697223.707727267 135382.60773643383 -85.6655 38.59700000000001 +6318 7324 -85.2245 38.903 823117.6077965281 246702.9508095159 +7324 4326 823117.6077965281 246702.9508095159 -85.2245 38.903000000000006 +6318 7325 -85.61500000000001 39.0 256026.29122925934 58219.55248533096 +7325 4326 256026.29122925934 58219.55248533096 -85.61500000000001 39.00000000000001 +6318 7325 -85.7815 38.82 241606.68117479834 38220.4552741332 +7325 4326 241606.68117479834 38220.4552741332 -85.7815 38.81999999999999 +6318 7325 -85.44850000000001 39.18 270372.6794086018 78245.72391163622 +7325 4326 270372.6794086018 78245.72391163622 -85.44850000000001 39.18 +6318 7326 -85.61500000000001 39.0 839979.5904746618 191008.64844562334 +7326 4326 839979.5904746618 191008.64844562334 -85.61500000000001 39.00000000000001 +6318 7326 -85.7815 38.82 792671.2531543176 125394.94367855202 +7326 4326 792671.2531543176 125394.94367855202 -85.7815 38.81999999999999 +6318 7326 -85.44850000000001 39.18 887047.6990263878 256711.17920009317 +7326 4326 887047.6990263878 256711.17920009317 -85.44850000000001 39.18 +6318 7327 -86.13 39.635000000000005 241716.98890293433 73194.52422905219 +7327 4326 241716.98890293433 73194.52422905219 -86.13 39.635000000000005 +6318 7327 -86.31 39.3695 226211.7219536525 43728.4716095124 +7327 4326 226211.7219536525 43728.4716095124 -86.31 39.3695 +6318 7327 -85.95 39.9005 257104.0708069241 102692.90681831077 +7327 4326 257104.0708069241 102692.90681831077 -85.95 39.90050000000001 +6318 7328 -86.13 39.635000000000005 793033.1544257104 240139.03490814875 +7328 4326 793033.1544257104 240139.03490814875 -86.13 39.635000000000005 +6318 7328 -86.31 39.3695 742162.957776275 143465.8272722086 +7328 4326 742162.957776275 143465.8272722086 -86.31 39.3695 +6318 7328 -85.95 39.9005 843515.6056390502 336918.31178640795 +7328 4326 843515.6056390502 336918.31178640795 -85.95 39.90050000000001 +6318 7329 -87.42500000000001 38.66 242176.01899361296 64862.42721742375 +7329 4326 242176.01899361296 64862.42721742375 -87.42500000000001 38.660000000000004 +6318 7329 -87.7265 38.434999999999995 215858.0959565764 39921.42338905253 +7329 4326 215858.0959565764 39921.42338905253 -87.7265 38.434999999999995 +6318 7329 -87.1235 38.885 268329.7054536012 89890.68478790278 +7329 4326 268329.7054536012 89890.68478790278 -87.1235 38.885 +6318 7330 -87.42500000000001 38.66 794539.155648212 212802.81329583112 +7330 4326 794539.155648212 212802.81329583112 -87.42500000000001 38.660000000000004 +6318 7330 -87.7265 38.434999999999995 708194.4364842012 130975.53656891652 +7330 4326 708194.4364842012 130975.53656891652 -87.7265 38.434999999999995 +6318 7330 -87.1235 38.885 880345.04197569 294916.35500831105 +7330 4326 880345.04197569 294916.35500831105 -87.1235 38.885 +6318 7331 -85.425 41.515 242086.99970426006 65432.63756853817 +7331 4326 242086.99970426006 65432.63756853817 -85.42500000000001 41.51499999999999 +6318 7331 -85.6365 41.2855 224376.10961178236 39959.52130172501 +7331 4326 224376.10961178236 39959.52130172501 -85.6365 41.28549999999999 +6318 7331 -85.2135 41.7445 259673.125989927 90949.98235729794 +7331 4326 259673.125989927 90949.98235729794 -85.2135 41.7445 +6318 7332 -85.425 41.515 794247.0981963933 214673.578422779 +7332 4326 794247.0981963933 214673.578422779 -85.42500000000001 41.51499999999999 +6318 7332 -85.6365 41.2855 736140.6196179894 131100.52947074283 +7332 4326 736140.6196179894 131100.52947074283 -85.6365 41.28549999999999 +6318 7332 -85.2135 41.7445 851944.2475186189 298391.73378390167 +7332 4326 851944.2475186189 298391.73378390167 -85.2135 41.7445 +6318 7333 -87.37 41.25 242514.55975292216 97081.40638534892 +7333 4326 242514.55975292216 97081.40638534892 -87.37 41.24999999999999 +6318 7333 -87.514 40.782 230376.80552286093 45112.50022805105 +7333 4326 230376.80552286093 45112.50022805105 -87.514 40.782 +6318 7333 -87.226 41.718 254479.88547313557 149074.56811389118 +7333 4326 254479.88547313557 149074.56811389118 -87.226 41.718 +6318 7334 -87.37 41.25 795649.8514560455 318507.9141159323 +7334 4326 795649.8514560455 318507.9141159323 -87.37 41.24999999999999 +6318 7334 -87.514 40.782 755827.902786253 148006.59449819752 +7334 4326 755827.902786253 148006.59449819752 -87.514 40.782 +6318 7334 -87.226 41.718 834906.0909231124 489088.8122203247 +7334 4326 834906.0909231124 489088.8122203247 -87.226 41.718 +6318 7335 -86.69999999999999 41.335 244185.50071997909 84311.9490662753 +7335 4326 244185.50071997909 84311.9490662753 -86.69999999999999 41.335 +6318 7335 -86.916 40.9435 226021.25944203587 40844.179734077006 +7335 4326 226021.25944203587 40844.179734077006 -86.916 40.94349999999999 +6318 7335 -86.484 41.7265 262133.0272583257 127827.73927953823 +7335 4326 262133.0272583257 127827.73927953823 -86.484 41.7265 +6318 7336 -86.69999999999999 41.335 801131.9302787981 276613.45289493824 +7336 4326 801131.9302787981 276613.45289493824 -86.69999999999999 41.335 +6318 7336 -86.916 40.9435 741538.0820194128 134002.94634421766 +7336 4326 741538.0820194128 134002.94634421766 -86.916 40.94349999999999 +6318 7336 -86.484 41.7265 860014.7735966903 419381.5079529517 +7336 4326 860014.7735966903 419381.5079529517 -86.484 41.7265 +6318 7337 -86.465 39.315 243018.5192894487 76523.29294139793 +7337 4326 243018.5192894487 76523.29294139793 -86.465 39.315 +6318 7337 -86.6675 39.0225 225494.26345887803 44062.17883540989 +7337 4326 225494.26345887803 44062.17883540989 -86.6675 39.0225 +6318 7337 -86.26249999999999 39.6075 260397.26426183086 109025.17676129314 +7337 4326 260397.26426183086 109025.17676129314 -86.26249999999999 39.6075 +6318 7338 -86.465 39.315 797303.2587021331 251060.17025856973 +7338 4326 797303.2587021331 251060.17025856973 -86.465 39.315 +6318 7338 -86.6675 39.0225 739809.0960313358 144560.6650625073 +7338 4326 739809.0960313358 144560.6650625073 -86.6675 39.0225 +6318 7338 -86.26249999999999 39.6075 854320.0244990236 357693.4340910093 +7338 4326 854320.0244990236 357693.4340910093 -86.26249999999999 39.6075 +6318 7339 -86.87 39.845 246847.14240929903 79860.42982463486 +7339 4326 246847.14240929903 79860.42982463486 -86.87 39.845 +6318 7339 -87.077 39.5075 229077.12956578285 42391.81566236334 +7339 4326 229077.12956578285 42391.81566236334 -87.077 39.50750000000001 +6318 7339 -86.663 40.1825 264443.4421260338 117372.30732404756 +7339 4326 264443.4421260338 117372.30732404756 -86.663 40.182500000000005 +6318 7340 -86.87 39.845 809864.3330545087 262008.7601829896 +7340 4326 809864.3330545087 262008.7601829896 -86.87 39.845 +6318 7340 -87.077 39.5075 751563.8825837394 139080.48188560375 +7340 4326 751563.8825837394 139080.48188560375 -87.077 39.50750000000001 +6318 7340 -86.663 40.1825 867594.8597084961 385078.9782789794 +7340 4326 867594.8597084961 385078.9782789794 -86.663 40.182500000000005 +6318 7341 -86.845 39.31999999999999 244743.04041339085 54875.3280273485 +7341 4326 244743.04041339085 54875.3280273485 -86.845 39.32 +6318 7341 -87.0385 39.175999999999995 228031.70946026922 38895.69632151704 +7341 4326 228031.70946026922 38895.69632151704 -87.0385 39.175999999999995 +6318 7341 -86.6515 39.464 261385.92595374986 70891.08616658044 +7341 4326 261385.92595374986 70891.08616658044 -86.6515 39.464000000000006 +6318 7342 -86.845 39.31999999999999 802961.1250895999 180036.8053697259 +7342 4326 802961.1250895999 180036.8053697259 -86.845 39.32 +6318 7342 -87.0385 39.175999999999995 748134.0334542333 127610.29701484383 +7342 4326 748134.0334542333 127610.29701484383 -87.0385 39.175999999999995 +6318 7342 -86.6515 39.464 857563.658733261 232581.8385315227 +7342 4326 857563.658733261 232581.8385315227 -86.6515 39.464000000000006 +6318 7343 -87.27000000000001 39.875 246844.09989557712 66536.87304633649 +7343 4326 246844.09989557712 66536.87304633649 -87.27000000000001 39.87499999999999 +6318 7343 -87.513 39.6275 226005.15180963345 39066.012205415704 +7343 4326 226005.15180963345 39066.012205415704 -87.513 39.6275 +6318 7343 -87.027 40.1225 267533.49520341214 94065.49331538982 +7343 4326 267533.49520341214 94065.49331538982 -87.027 40.12250000000001 +6318 7344 -87.27000000000001 39.875 809854.3510740726 218296.390986189 +7344 4326 809854.3510740726 218296.390986189 -87.27000000000001 39.87499999999999 +6318 7344 -87.513 39.6275 741485.2355621058 128169.0750439347 +7344 4326 741485.2355621058 128169.0750439347 -87.513 39.6275 +6318 7344 -87.027 40.1225 877732.8088465281 308613.2059855748 +7344 4326 877732.8088465281 308613.2059855748 -87.027 40.12250000000001 +6318 7345 -86.625 38.05500000000001 246582.64378213478 64306.973738112756 +7345 4326 246582.64378213478 64306.973738112756 -86.625 38.055000000000014 +6318 7345 -86.8005 37.86150000000001 231156.08416775404 42830.983524769654 +7345 4326 231156.08416775404 42830.983524769654 -86.8005 37.8615 +6318 7345 -86.4495 38.2485 261928.034886943 85812.7974165176 +7345 4326 261928.034886943 85812.7974165176 -86.4495 38.24850000000001 +6318 7346 -86.625 38.05500000000001 808996.5571418873 210980.46300579162 +7346 4326 808996.5571418873 210980.46300579162 -86.625 38.055000000000014 +6318 7346 -86.8005 37.86150000000001 758384.5861403731 140521.31844751514 +7346 4326 758384.5861403731 140521.31844751514 -86.8005 37.8615 +6318 7346 -86.4495 38.2485 859342.227791579 281537.48619069153 +7346 4326 859342.227791579 281537.48619069153 -86.4495 38.24850000000001 +6318 7347 -87.245 38.215 244816.72009604864 76515.97668314897 +7347 4326 244816.72009604864 76515.97668314897 -87.245 38.215 +6318 7347 -87.4565 37.9045 226236.202635422 42060.82039588607 +7347 4326 226236.202635422 42060.82039588607 -87.4565 37.904500000000006 +6318 7347 -87.0335 38.5255 263239.70129351865 111015.3130352864 +7347 4326 263239.70129351865 111015.3130352864 -87.0335 38.5255 +6318 7348 -87.245 38.215 803202.855848453 251036.16683463127 +7348 4326 803202.855848453 251036.16683463127 -87.245 38.215 +6318 7348 -87.4565 37.9045 742243.2748130471 137994.54158216956 +7348 4326 742243.2748130471 137994.54158216956 -87.4565 37.904500000000006 +6318 7348 -87.0335 38.5255 863645.5866604858 364222.73951660213 +7348 4326 863645.5866604858 364222.73951660213 -87.0335 38.5255 +6318 7349 -87.89 38.005 245269.65857419127 64305.57773442198 +7349 4326 245269.65857419127 64305.57773442198 -87.89 38.005 +6318 7349 -88.079 37.7935 228637.76476949474 40836.06029771412 +7349 4326 228637.76476949474 40836.06029771412 -88.079 37.79350000000001 +6318 7349 -87.70100000000001 38.2165 261806.12975991104 87809.69541199751 +7349 4326 261806.12975991104 87809.69541199751 -87.70100000000001 38.2165 +6318 7350 -87.89 38.005 804688.8715054926 210975.88295034948 +7350 4326 804688.8715054926 210975.88295034948 -87.89 38.005 +6318 7350 -88.079 37.7935 750122.3999145841 133976.30782675042 +7350 4326 750122.3999145841 133976.30782675042 -88.079 37.79350000000001 +6318 7350 -87.70100000000001 38.2165 858942.2773873082 288088.9756975285 +7350 4326 858942.2773873082 288088.9756975285 -87.70100000000001 38.2165 +6318 7351 -85.015 40.015 242988.2624238469 70977.17297656872 +7351 4326 242988.2624238469 70977.17297656872 -85.015 40.015 +6318 7351 -85.2085 39.7405 226413.3768985029 40508.89732243988 +7351 4326 226413.3768985029 40508.89732243988 -85.2085 39.7405 +6318 7351 -84.8215 40.289500000000004 259430.70789230813 101482.79240449189 +7351 4326 259430.70789230813 101482.79240449189 -84.8215 40.289500000000004 +6318 7352 -85.015 40.015 797203.9909689045 232864.27500729257 +7352 4326 797203.9909689045 232864.27500729257 -85.015 40.015 +6318 7352 -85.2085 39.7405 742824.5540411717 132902.9406320382 +7352 4326 742824.5540411717 132902.9406320382 -85.2085 39.7405 +6318 7352 -84.8215 40.289500000000004 851148.9141433476 332948.12808040384 +7352 4326 851148.9141433476 332948.12808040384 -84.8215 40.289500000000004 +6318 7353 -85.255 39.114999999999995 243892.0180720719 59870.23049366199 +7353 4326 243892.0180720719 59870.23049366199 -85.255 39.114999999999995 +6318 7353 -85.43050000000001 38.930499999999995 228683.77124914 39394.14958087378 +7353 4326 228683.77124914 39394.14958087378 -85.43050000000001 38.930499999999995 +6318 7353 -85.0795 39.2995 259021.06623744627 80376.31321486452 +7353 4326 259021.06623744627 80376.31321486452 -85.0795 39.2995 +6318 7354 -85.255 39.114999999999995 800169.0626247893 196424.24787795608 +7354 4326 800169.0626247893 196424.24787795608 -85.255 39.114999999999995 +6318 7354 -85.43050000000001 38.930499999999995 750273.3395065536 129245.63908325008 +7354 4326 750273.3395065536 129245.63908325008 -85.43050000000001 38.930499999999995 +6318 7354 -85.0795 39.2995 849804.9481473551 263701.28760576807 +7354 4326 849804.9481473551 263701.28760576807 -85.0795 39.2995 +6318 7355 -85.78999999999999 39.52 249459.04876168133 60431.640557642444 +7355 4326 249459.04876168133 60431.640557642444 -85.78999999999999 39.52000000000001 +6318 7355 -85.943 39.358000000000004 236293.7971177434 42440.337610541 +7355 4326 236293.7971177434 42440.337610541 -85.943 39.358000000000004 +6318 7355 -85.637 39.682 262563.10857314896 78445.83811198024 +7355 4326 262563.10857314896 78445.83811198024 -85.637 39.682 +6318 7356 -85.78999999999999 39.52 818433.5624789495 198266.14072953194 +7356 4326 818433.5624789495 198266.14072953194 -85.78999999999999 39.52000000000001 +6318 7356 -85.943 39.358000000000004 775240.5660437966 139239.67431058327 +7356 4326 775240.5660437966 139239.67431058327 -85.943 39.358000000000004 +6318 7356 -85.637 39.682 861425.7987104063 257367.72053905518 +7356 4326 861425.7987104063 257367.72053905518 -85.637 39.682 +6318 7357 -87.015 37.995000000000005 243074.38793330934 63194.50486986931 +7357 4326 243074.38793330934 63194.50486986931 -87.015 37.995000000000005 +6318 7357 -87.2445 37.801500000000004 222870.39343943656 41734.00094631732 +7357 4326 222870.39343943656 41734.00094631732 -87.2445 37.80150000000001 +6318 7357 -86.7855 38.1885 263172.438327984 84705.45012676212 +7357 4326 263172.438327984 84705.45012676212 -86.7855 38.1885 +6318 7358 -87.015 37.995000000000005 797486.5544111992 207330.63806056292 +7358 4326 797486.5544111992 207330.63806056292 -87.015 37.995000000000005 +6318 7358 -87.2445 37.801500000000004 731200.6158092183 136922.30143804275 +7358 4326 731200.6158092183 136922.30143804275 -87.2445 37.80150000000001 +6318 7358 -86.7855 38.1885 863424.908081061 277904.46429088543 +7358 4326 863424.908081061 277904.46429088543 -86.7855 38.1885 +6318 7359 -85.0 41.645 240000.0 52105.08379705108 +7359 4326 240000.0 52105.08379705108 -85.0 41.644999999999996 +6318 7359 -85.18 41.532500000000006 224977.58711108615 39625.37072253953 +7359 4326 224977.58711108615 39625.37072253953 -85.18 41.5325 +6318 7359 -84.82 41.7575 254970.24082845086 64616.34893618979 +7359 4326 254970.24082845086 64616.34893618979 -84.82 41.7575 +6318 7360 -85.0 41.645 787400.0000000001 170948.09575749177 +7360 4326 787400.0000000001 170948.09575749177 -85.0 41.644999999999996 +6318 7360 -85.18 41.532500000000006 738113.9670469553 130004.23711219845 +7360 4326 738113.9670469553 130004.23711219845 -85.18 41.5325 +6318 7360 -84.82 41.7575 836514.8651180093 211995.47146814936 +7360 4326 836514.8651180093 211995.47146814936 -84.82 41.7575 +6318 7361 -87.44999999999999 39.08 244326.51217352267 55984.28206954789 +7361 4326 244326.51217352267 55984.28206954789 -87.44999999999999 39.08 +6318 7361 -87.639 38.918 227944.8364950096 38007.46764800307 +7361 4326 227944.8364950096 38007.46764800307 -87.639 38.918 +6318 7361 -87.261 39.242 260633.3627307886 73995.62719739416 +7361 4326 260633.3627307886 73995.62719739416 -87.261 39.242 +6318 7362 -87.44999999999999 39.08 801594.5653559658 183675.09875650838 +7362 4326 801594.5653559658 183675.09875650838 -87.44999999999999 39.08 +6318 7362 -87.639 38.918 747849.0177340441 124696.16677515674 +7362 4326 747849.0177340441 124696.16677515674 -87.639 38.918 +6318 7362 -87.261 39.242 855094.624225929 242767.3202301174 +7362 4326 855094.624225929 242767.3202301174 -87.261 39.242 +6318 7363 -86.84 40.565 245081.28589850038 76533.11129665218 +7363 4326 245081.28589850038 76533.11129665218 -86.84 40.565 +6318 7363 -87.074 40.2455 225194.42829833564 41066.92644543726 +7363 4326 225194.42829833564 41066.92644543726 -87.074 40.24550000000001 +6318 7363 -86.606 40.8845 264779.5340321548 112053.94115129331 +7363 4326 264779.5340321548 112053.94115129331 -86.606 40.8845 +6318 7364 -86.84 40.565 804070.8521519968 251092.38264576637 +7364 4326 804070.8521519968 251092.38264576637 -86.84 40.565 +6318 7364 -87.074 40.2455 738825.3868421229 134733.74117973875 +7364 4326 738825.3868421229 134733.74117973875 -87.074 40.24550000000001 +6318 7364 -86.606 40.8845 868697.5212371614 367630.30526053486 +7364 4326 868697.5212371614 367630.30526053486 -86.606 40.8845 +6318 7365 -87.57499999999999 37.995000000000005 237804.0064394735 57644.55452802539 +7365 4326 237804.0064394735 57644.55452802539 -87.57499999999999 37.995000000000005 +6318 7365 -87.6965 37.8375 227104.00593481588 40172.41528740521 +7365 4326 227104.00593481588 40172.41528740521 -87.6965 37.837500000000006 +6318 7365 -87.45349999999999 38.1525 248458.3782522195 75131.08708221534 +7365 4326 248458.3782522195 75131.08708221534 -87.45349999999999 38.15250000000001 +6318 7366 -87.57499999999999 37.995000000000005 780195.3111268395 189122.17598069663 +7366 4326 780195.3111268395 189122.17598069663 -87.57499999999999 37.995000000000005 +6318 7366 -87.6965 37.8375 745090.3928044752 131798.9991554286 +7366 4326 745090.3928044752 131798.9991554286 -87.6965 37.837500000000006 +6318 7366 -87.45349999999999 38.1525 815150.5293158236 246492.57486890152 +7366 4326 815150.5293158236 246492.57486890152 -87.45349999999999 38.15250000000001 +6318 7367 -87.41 39.43 243444.05429140222 55985.11917994828 +7367 4326 243444.05429140222 55985.11917994828 -87.41 39.43 +6318 7367 -87.60799999999999 39.268 226364.53479960372 38010.30757805188 +7367 4326 226364.53479960372 38010.30757805188 -87.60799999999999 39.268 +6318 7367 -87.212 39.592 260444.59912172784 73997.8679295847 +7367 4326 260444.59912172784 73997.8679295847 -87.212 39.592000000000006 +6318 7368 -87.41 39.43 798699.3681210423 183677.84517621365 +7368 4326 798699.3681210423 183677.84517621365 -87.41 39.43 +6318 7368 -87.60799999999999 39.268 742664.3112550332 124705.48411232521 +7368 4326 742664.3112550332 124705.48411232521 -87.60799999999999 39.268 +6318 7368 -87.212 39.592 854475.3222852022 242774.67169897916 +7368 4326 854475.3222852022 242774.67169897916 -87.212 39.592000000000006 +6318 7369 -85.255 40.74 239577.66097923234 57099.66938363295 +7369 4326 239577.66097923234 57099.66938363295 -85.255 40.74 +6318 7369 -85.43050000000001 40.578 224716.6304230192 39125.03902103714 +7369 4326 224716.6304230192 39125.03902103714 -85.43050000000001 40.578 +6318 7369 -85.0795 40.902 254366.76683126233 75104.43833273098 +7369 4326 254366.76683126233 75104.43833273098 -85.0795 40.90200000000001 +6318 7370 -85.255 40.74 786014.3760626982 187334.4986361358 +7370 4326 786014.3760626982 187334.4986361358 -85.255 40.74 +6318 7370 -85.43050000000001 40.578 737257.811646189 128362.73218818604 +7370 4326 737257.811646189 128362.73218818604 -85.43050000000001 40.578 +6318 7370 -85.0795 40.902 834534.9675122333 246405.14476330156 +7370 4326 834534.9675122333 246405.14476330156 -85.0795 40.90200000000001 +7373 7374 53.0 17.305 712574.897789995 1914400.0945327408 +7374 4326 712574.897789995 1914400.0945327408 53.0 17.305 +7373 7374 52.091 15.176499999999999 617200.0386551527 1678139.971911333 +7374 4326 617200.0386551527 1678139.971911333 52.091 15.176499999999997 +7373 7374 53.909 19.433500000000002 805473.2431298404 2151376.0437958874 +7374 4326 805473.2431298404 2151376.0437958874 53.909 19.433500000000002 +7373 7375 57.004999999999995 20.535 500521.2364145107 2270686.217731219 +7375 4326 500521.2364145107 2270686.217731219 57.004999999999995 20.535 +7373 7375 54.3005 14.9505 209623.16259984206 1654616.7980691977 +7375 4326 209623.16259984206 1654616.7980691977 54.3005 14.950499999999995 +7373 7375 59.7095 26.1195 770947.7828167134 2891739.6443194244 +7375 4326 770947.7828167134 2891739.6443194244 59.7095 26.119499999999995 +7373 7376 61.69 20.23 363158.14818984084 2237474.446515913 +7376 4326 363158.14818984084 2237474.446515913 61.690000000000005 20.229999999999993 +7373 7376 60.169 16.756 198168.87878652924 1854713.881113435 +7376 4326 198168.87878652924 1854713.881113435 60.169 16.756 +7373 7376 63.211 23.704 521509.87091534515 2621471.7720680675 +7376 4326 521509.87091534515 2621471.7720680675 63.211 23.704 +6318 7528 -89.955 43.945 150831.43734829218 64259.36391885925 +7528 4326 150831.43734829218 64259.36391885925 -89.955 43.94499999999999 +6318 7528 -90.28349999999999 43.670500000000004 124353.93769781904 33797.0456092559 +7528 4326 124353.93769781904 33797.0456092559 -90.28349999999999 43.670500000000004 +6318 7528 -89.62650000000001 44.2195 177066.1404045827 94828.0926468275 +7528 4326 177066.1404045827 94828.0926468275 -89.62650000000001 44.2195 +6318 7529 -90.61500000000001 46.535 173376.01422607686 92138.63843222527 +7529 4326 173376.01422607686 92138.63843222527 -90.61500000000001 46.535 +6318 7529 -90.89850000000001 46.0355 151433.19156667285 36650.11144519595 +7529 4326 151433.19156667285 36650.11144519595 -90.89850000000001 46.0355 +6318 7529 -90.33149999999999 47.0345 194919.96672922006 147710.128259689 +7529 4326 194919.96672922006 147710.128259689 -90.33149999999999 47.03450000000001 +6318 7530 -91.845 45.425 93541.32785733524 32416.61890283633 +7530 4326 93541.32785733524 32416.61890283633 -91.845 45.42499999999999 +6318 7530 -92.1285 45.222500000000004 71275.24656327034 9947.783653935769 +7530 4326 71275.24656327034 9947.783653935769 -92.1285 45.22250000000001 +6318 7530 -91.5615 45.6275 115648.74588149578 54964.45908745633 +7530 4326 115648.74588149578 54964.45908745633 -91.5615 45.6275 +6318 7531 -91.155 46.58 228430.11846790204 138585.52276925923 +7531 4326 228430.11846790204 138585.52276925923 -91.155 46.57999999999999 +6318 7531 -91.51950000000001 46.193 200290.0664978327 95630.91384788198 +7531 4326 200290.0664978327 95630.91384788198 -91.51950000000001 46.19299999999999 +6318 7531 -90.7905 46.967 256172.00257233038 181671.90868332676 +7531 4326 256172.00257233038 181671.90868332676 -90.7905 46.967 +6318 7532 -88.01 44.46 30804.144909606264 166819.4761173327 +7532 4326 30804.144909606264 166819.4761173327 -88.01 44.46000000000001 +6318 7532 -88.235 44.262 12834.308862850765 144844.22717938322 +7532 4326 12834.308862850765 144844.22717938322 -88.235 44.262000000000015 +6318 7532 -87.78500000000001 44.658 48652.95405922533 188844.74190878862 +7532 4326 48652.95405922533 188844.74190878862 -87.78500000000001 44.657999999999994 +6318 7533 -91.805 44.31 174639.75775956767 92070.71010485994 +7533 4326 174639.75775956767 92070.71010485994 -91.80500000000002 44.309999999999995 +6318 7533 -92.06150000000001 44.04900000000001 154080.2750486548 63102.32250271554 +7533 4326 154080.2750486548 63102.32250271554 -92.06150000000001 44.04900000000001 +6318 7533 -91.54849999999999 44.571 195017.85598315235 121104.42711645935 +7533 4326 195017.85598315235 121104.42711645935 -91.54849999999999 44.571 +6318 7534 -92.46000000000001 45.894999999999996 63835.65499249906 59032.98610292785 +7534 4326 63835.65499249906 59032.98610292785 -92.46000000000001 45.89499999999998 +6318 7534 -92.847 45.6565 33670.34936498799 32597.34003068061 +7534 4326 33670.34936498799 32597.34003068061 -92.847 45.6565 +6318 7534 -92.07300000000001 46.1335 93743.79434247228 85615.43131705209 +7534 4326 93743.79434247228 85615.43131705209 -92.07300000000001 46.133500000000005 +6318 7535 -88.465 44.07 247558.8838323784 150052.20867291963 +7535 4326 247558.8838323784 150052.20867291963 -88.465 44.06999999999999 +6318 7535 -88.8475 43.592999999999996 216692.6049851458 97109.80254276369 +7535 4326 216692.6049851458 97109.80254276369 -88.8475 43.592999999999996 +6318 7535 -88.08250000000001 44.547000000000004 277932.75170286186 203141.34214695933 +7535 4326 277932.75170286186 203141.34214695933 -88.08250000000001 44.547000000000004 +6318 7536 -91.295 45.075 60001.971613199334 54887.59785025609 +7536 4326 60001.971613199334 54887.59785025609 -91.295 45.075 +6318 7536 -91.63250000000001 44.8725 33331.048291201965 32438.33085819582 +7536 4326 33331.048291201965 32438.33085819582 -91.63250000000001 44.8725 +6318 7536 -90.9575 45.277499999999996 86485.48926116206 77448.45837036707 +7536 4326 86485.48926116206 77448.45837036707 -90.95750000000001 45.27750000000002 +6318 7537 -90.62 44.730000000000004 206946.959351274 125570.09422627848 +7537 4326 206946.959351274 125570.09422627848 -90.62000000000002 44.730000000000004 +6318 7537 -90.899 44.451 184772.16457891185 94579.01356631645 +7537 4326 184772.16457891185 94579.01356631645 -90.899 44.451 +6318 7537 -90.34100000000001 45.009 228909.07612849842 156638.44179029798 +7537 4326 228909.07612849842 156638.44179029798 -90.34100000000001 45.00900000000001 +6318 7538 -89.39500000000001 43.465 169119.37909351726 111842.19509287545 +7538 4326 169119.37909351726 111842.19509287545 -89.39500000000001 43.46500000000001 +6318 7538 -89.7505 43.298500000000004 140271.12137693443 93405.09885119235 +7538 4326 140271.12137693443 93405.09885119235 -89.7505 43.29850000000001 +6318 7538 -89.0395 43.631499999999996 197809.72763118503 130402.62404450399 +7538 4326 197809.72763118503 130402.62404450399 -89.0395 43.6315 +6318 7539 -90.94 43.205 113600.32495609725 54252.44576377582 +7539 4326 113600.32495609725 54252.44576377582 -90.94 43.20499999999999 +6318 7539 -91.192 43.0025 93051.72798609662 31786.134707409907 +7539 4326 93051.72798609662 31786.134707409907 -91.192 43.0025 +6318 7539 -90.688 43.4075 134013.44954849902 76781.22781410564 +7539 4326 134013.44954849902 76781.22781410564 -90.688 43.40750000000001 +6318 7540 -89.42 43.07 247374.2976802199 146645.75944198362 +7540 4326 247374.2976802199 146645.75944198362 -89.42 43.07000000000001 +6318 7540 -89.798 42.863 216482.74474507145 123717.53386590895 +7540 4326 216482.74474507145 123717.53386590895 -89.798 42.86300000000001 +6318 7540 -89.042 43.277 278058.6303939868 169713.52504772405 +7540 4326 278058.6303939868 169713.52504772405 -89.042 43.27700000000001 +6318 7541 -88.71000000000001 43.24 268627.396803292 196373.992331028 +7541 4326 268627.396803292 196373.992331028 -88.71000000000001 43.24 +6318 7541 -88.98899999999999 42.88 245863.43102127124 156398.96784185743 +7541 4326 245863.43102127124 156398.96784185743 -88.98899999999999 42.88 +6318 7541 -88.43100000000001 43.6 291124.31489684095 236427.16268499364 +7541 4326 291124.31489684095 236427.16268499364 -88.43100000000001 43.60000000000001 +6318 7542 -87.27 45.05 158976.1836141684 72233.52169345116 +7542 4326 158976.1836141684 72233.52169345116 -87.27 45.05 +6318 7542 -87.693 44.708 125455.41301779557 34312.687284688676 +7542 4326 125455.41301779557 34312.687284688676 -87.693 44.708000000000006 +6318 7542 -86.847 45.392 192099.76266700553 110330.7460366925 +7542 4326 192099.76266700553 110330.7460366925 -86.847 45.39200000000001 +6318 7543 -91.925 46.455 58491.077802991575 63545.879606245966 +7543 4326 58491.077802991575 63545.879606245966 -91.925 46.455 +6318 7543 -92.2625 46.180499999999995 32428.148811234645 33090.07670866631 +7543 4326 32428.148811234645 33090.07670866631 -92.2625 46.1805 +6318 7543 -91.58749999999999 46.7295 84293.44880820067 94113.86205887252 +7543 4326 84293.44880820067 94113.86205887252 -91.58749999999999 46.72950000000001 +6318 7544 -91.9 44.945 51377.62916544574 59639.79493893713 +7544 4326 51377.62916544574 59639.79493893713 -91.9 44.945 +6318 7544 -92.134 44.7065 32830.94623954667 33162.49576190656 +7544 4326 32830.94623954667 33162.49576190656 -92.134 44.7065 +6318 7544 -91.666 45.1835 69771.37572553608 86171.48761657182 +7544 4326 69771.37572553608 86171.48761657182 -91.666 45.1835 +6318 7545 -91.28999999999999 44.725 120003.41117131937 75320.2985839772 +7545 4326 120003.41117131937 75320.2985839772 -91.28999999999999 44.724999999999994 +6318 7545 -91.62299999999999 44.603500000000004 93565.63533400463 61872.58544759037 +7545 4326 93565.63533400463 61872.58544759037 -91.62299999999999 44.6035 +6318 7545 -90.95700000000001 44.8465 146330.45585210953 88876.40879954014 +7545 4326 146330.45585210953 88876.40879954014 -90.95700000000001 44.846500000000006 +6318 7546 -88.37 45.870000000000005 115772.8607114832 47943.66661035 +7546 4326 115772.8607114832 47943.66661035 -88.37000000000002 45.86999999999999 +6318 7546 -88.658 45.726 93306.61294903542 32041.887942218942 +7546 4326 93306.61294903542 32041.887942218942 -88.658 45.726000000000006 +6318 7546 -88.082 46.014 138123.73439436252 63926.55095605564 +7546 4326 138123.73439436252 63926.55095605564 -88.082 46.01400000000001 +6318 7547 -88.735 45.724999999999994 267929.66134175326 191098.29571486198 +7547 4326 267929.66134175326 191098.29571486198 -88.735 45.724999999999994 +6318 7547 -89.0185 45.405499999999996 245688.3282205834 155652.9252145825 +7547 4326 245688.3282205834 155652.9252145825 -89.0185 45.4055 +6318 7547 -88.4515 46.0445 289919.61357178347 226623.86266139807 +7547 4326 289919.61357178347 226623.86266139807 -88.4515 46.0445 +6318 7548 -90.78999999999999 42.86 243133.7711769257 160942.35308115307 +7548 4326 243133.7711769257 160942.35308115307 -90.78999999999999 42.85999999999999 +6318 7548 -91.12299999999999 42.536 215780.49940160464 124999.47291851623 +7548 4326 215780.49940160464 124999.47291851623 -91.12299999999999 42.536 +6318 7548 -90.45700000000001 43.184 270202.42030199757 196994.86989571538 +7548 4326 270202.42030199757 196994.86989571538 -90.45700000000001 43.184 +6318 7549 -89.89500000000001 42.68 165479.5338558115 50546.245446923145 +7549 4326 165479.5338558115 50546.245446923145 -89.89500000000001 42.67999999999999 +6318 7549 -90.37650000000001 42.518 125898.79200794577 32688.64687273291 +7549 4326 125898.79200794577 32688.64687273291 -90.37650000000001 42.51799999999999 +6318 7549 -89.4135 42.842 204855.53842466595 68629.18605840289 +7549 4326 204855.53842466595 68629.18605840289 -89.4135 42.841999999999985 +6318 7550 -89.24 43.81 151010.4092544449 79504.10474308397 +7550 4326 151010.4092544449 79504.10474308397 -89.24 43.81000000000002 +6318 7550 -89.564 43.648 124869.88785109254 61554.7742651461 +7550 4326 124869.88785109254 61554.7742651461 -89.564 43.647999999999996 +6318 7550 -88.916 43.972 177010.00604817714 97555.99950475246 +7550 4326 177010.00604817714 97555.99950475246 -88.916 43.97200000000001 +6318 7551 -90.13 43.010000000000005 115617.54495897249 52337.49425961594 +7551 4326 115617.54495897249 52337.49425961594 -90.13 43.01 +6318 7551 -90.4 42.830000000000005 93547.40326518498 32367.506472060628 +7551 4326 93547.40326518498 32367.506472060628 -90.40000000000002 42.83000000000001 +6318 7551 -89.86 43.19 137559.10283822444 72378.88023279075 +7551 4326 137559.10283822444 72378.88023279075 -89.86 43.19 +6318 7552 -90.24000000000001 46.29 222179.19700080663 95223.887661356 +7552 4326 222179.19700080663 95223.887661356 -90.24000000000001 46.29 +6318 7552 -90.528 46.010999999999996 199878.7527957133 64245.72792822126 +7552 4326 199878.7527957133 64245.72792822126 -90.528 46.010999999999996 +6318 7552 -89.952 46.569 244254.229839873 126284.20104371077 +7552 4326 244254.229839873 126284.20104371077 -89.952 46.56900000000001 +6318 7553 -90.74000000000001 44.335 35318.308969897436 34080.0475169196 +7553 4326 35318.308969897436 34080.0475169196 -90.74000000000001 44.335 +6318 7553 -91.127 44.0965 4361.4112038700805 7611.4113214011195 +7553 4326 4361.4112038700805 7611.4113214011195 -91.127 44.0965 +6318 7553 -90.35300000000001 44.5735 66024.86263987594 60695.50411638409 +7553 4326 66024.86263987594 60695.50411638409 -90.35300000000001 44.57350000000001 +6318 7554 -88.03 43.019999999999996 174878.3385872995 89249.77410380432 +7554 4326 174878.3385872995 89249.77410380432 -88.03 43.019999999999996 +6318 7554 -88.282 42.543 154092.67492351195 36323.07047178617 +7554 4326 154092.67492351195 36323.07047178617 -88.282 42.543 +6318 7554 -87.778 43.497 195346.72479043846 142242.4634264023 +7554 4326 195346.72479043846 142242.4634264023 -87.778 43.49700000000001 +6318 7555 -87.77000000000001 44.11 62244.61920498541 93725.25532570129 +7555 4326 62244.61920498541 93725.25532570129 -87.77000000000001 44.11 +6318 7555 -88.13 43.597 33023.31574282605 36864.742114742636 +7555 4326 33023.31574282605 36864.742114742636 -88.13 43.59700000000001 +6318 7555 -87.41000000000001 44.623 90968.72564780439 150716.83853008063 +7555 4326 90968.72564780439 150716.83853008063 -87.41000000000001 44.623 +6318 7556 -91.17 43.91 142236.37983530728 50997.40492583605 +7556 4326 142236.37983530728 50997.40492583605 -91.17 43.91 +6318 7556 -91.40400000000001 43.739 123419.12703486366 31990.438486454506 +7556 4326 123419.12703486366 31990.438486454506 -91.40400000000001 43.739 +6318 7556 -90.93599999999999 44.081 160945.89902043613 70058.19873105803 +7556 4326 160945.89902043613 70058.19873105803 -90.93599999999999 44.081 +6318 7557 -89.03 45.25 198686.89111147964 115923.13951664549 +7557 4326 198686.89111147964 115923.13951664549 -89.03000000000002 45.25 +6318 7557 -89.39 45.043000000000006 170322.4780794877 92978.8252964652 +7557 4326 170322.4780794877 92978.8252964652 -89.39 45.043 +6318 7557 -88.67 45.456999999999994 226846.31684581377 138994.32779111413 +7557 4326 226846.31684581377 138994.32779111413 -88.67 45.45699999999998 +6318 7558 -89.735 45.335 115998.38110218298 54520.44370781562 +7558 4326 115998.38110218298 54520.44370781562 -89.735 45.335 +6318 7558 -90.0185 45.1325 93695.0823234387 32053.552017911992 +7558 4326 93695.0823234387 32053.552017911992 -90.0185 45.1325 +6318 7558 -89.4515 45.5375 138143.269801968 77066.34440334715 +7558 4326 138143.269801968 77066.34440334715 -89.4515 45.5375 +6318 7559 -89.77 44.905 74676.1493 55504.43207077539 +7559 4326 74676.1493 55504.43207077539 -89.77 44.905 +6318 7559 -90.26499999999999 44.7025 35443.442159860344 33119.40919742037 +7559 4326 35443.442159860344 33119.40919742037 -90.26499999999999 44.7025 +6318 7559 -89.275 45.1075 113634.36897393092 78128.68215159154 +7559 4326 113634.36897393092 78128.68215159154 -89.275 45.1075 +6318 7560 -87.95500000000001 45.379999999999995 219556.09802063712 76526.9285989131 +7560 4326 219556.09802063712 76526.9285989131 -87.95500000000001 45.379999999999995 +6318 7560 -88.38250000000001 45.002 185722.5862566855 34707.10316846787 +7560 4326 185722.5862566855 34707.10316846787 -88.38250000000001 45.002 +6318 7560 -87.5275 45.757999999999996 252944.2158813607 118527.38996109835 +7560 4326 252944.2158813607 118527.38996109835 -87.5275 45.75799999999999 +6318 7561 -88.735 44.985 80353.9777629856 29869.96780132863 +7561 4326 80353.9777629856 29869.96780132863 -88.735 44.985 +6318 7561 -88.9645 44.8635 62162.07644156228 16463.866525918686 +7561 4326 62162.07644156228 16463.866525918686 -88.9645 44.8635 +6318 7561 -88.5055 45.1065 98469.5192258494 43327.610591040524 +7561 4326 98469.5192258494 43327.610591040524 -88.5055 45.1065 +6318 7562 -90.64500000000001 43.945 204253.59643231475 115804.36675757566 +7562 4326 204253.59643231475 115804.36675757566 -90.64500000000001 43.945 +6318 7562 -90.9465 43.7425 179964.93694112342 93349.05616159303 +7562 4326 179964.93694112342 93349.05616159303 -90.9465 43.74250000000001 +6318 7562 -90.3435 44.1475 228377.75863512588 138348.88975844346 +7562 4326 228377.75863512588 138348.88975844346 -90.34350000000002 44.14750000000001 +6318 7563 -88.225 45.025000000000006 157922.47211109192 69812.96262260486 +7563 4326 157922.47211109192 69812.96262260486 -88.225 45.025000000000006 +6318 7563 -88.6435 44.7055 124617.2335173268 34520.52394219011 +7563 4326 124617.2335173268 34520.52394219011 -88.6435 44.70550000000001 +6318 7563 -87.8065 45.344500000000004 190861.53189384157 105277.81500574289 +7563 4326 190861.53189384157 105277.81500574289 -87.8065 45.344500000000004 +6318 7564 -89.545 45.685 70060.8584041478 55451.25522176298 +7564 4326 70060.8584041478 55451.25522176298 -89.545 45.68500000000001 +6318 7564 -89.9995 45.4825 34524.345178017575 33044.28136738057 +7564 4326 34524.345178017575 33044.28136738057 -89.9995 45.48250000000001 +6318 7564 -89.0905 45.887499999999996 105341.78346369747 78060.04386742058 +7564 4326 105341.78346369747 78060.04386742058 -89.0905 45.887499999999996 +6318 7565 -92.23 44.635 167464.0049459691 85905.41736851723 +7565 4326 167464.0049459691 85905.41736851723 -92.23 44.635000000000005 +6318 7565 -92.75200000000001 44.4235 125893.18021329632 62536.266380278954 +7565 4326 125893.18021329632 62536.266380278954 -92.75200000000001 44.42350000000001 +6318 7565 -91.708 44.8465 208733.93714367726 109540.58340938539 +7565 4326 208733.93714367726 109540.58340938539 -91.708 44.84649999999999 +6318 7566 -92.52000000000001 45.465 150596.0670698155 89348.72252863385 +7566 4326 150596.0670698155 89348.72252863385 -92.52000000000001 45.46500000000001 +6318 7566 -92.853 45.2265 124479.88777953551 62858.260965098765 +7566 4326 124479.88777953551 62858.260965098765 -92.853 45.2265 +6318 7566 -92.18700000000001 45.7035 176492.44351015982 115948.1804730309 +7566 4326 176492.44351015982 115948.1804730309 -92.18700000000001 45.70349999999999 +6318 7567 -89.535 44.465 53602.80129683441 55376.358190904946 +7567 4326 53602.80129683441 55376.358190904946 -89.535 44.465 +6318 7567 -89.8185 44.2625 30954.2950551315 32922.65061408731 +7567 4326 30954.2950551315 32922.65061408731 -89.8185 44.26250000000001 +6318 7567 -89.2515 44.6675 76095.4867006141 77909.12851157493 +7567 4326 76095.4867006141 77909.12851157493 -89.2515 44.66750000000001 +6318 7568 -90.36000000000001 45.68 238033.0642163163 124980.30709108707 +7568 4326 238033.0642163163 124980.30709108707 -90.36000000000001 45.68 +6318 7568 -90.64800000000001 45.400999999999996 215532.45422454795 93973.81615128319 +7568 4326 215532.45422454795 93973.81615128319 -90.64800000000001 45.40099999999999 +6318 7568 -90.072 45.959 260310.3773565128 156068.99629324913 +7568 4326 260310.3773565128 156068.99629324913 -90.072 45.959 +6318 7569 -90.435 43.36 202027.3098280338 138442.60308798568 +7569 4326 202027.3098280338 138442.60308798568 -90.435 43.36 +6318 7569 -90.6555 43.18 184098.32893153312 118468.81851521392 +7569 4326 184098.32893153312 118468.81851521392 -90.6555 43.18000000000001 +6318 7569 -90.2145 43.54 219850.6828738155 158464.26436310308 +7569 4326 219850.6828738155 158464.26436310308 -90.2145 43.53999999999999 +6318 7570 -89.07 42.67 146486.4675905452 80596.89779002538 +7570 4326 146486.4675905452 80596.89779002538 -89.07 42.67 +6318 7570 -89.34 42.508 124295.28052012608 62635.31306669469 +7570 4326 124295.28052012608 62635.31306669469 -89.34 42.508 +6318 7570 -88.8 42.832 168562.69272600365 98629.68853114081 +7570 4326 168562.69272600365 98629.68853114081 -88.8 42.832 +6318 7571 -91.11 45.465 247156.9860412441 171760.49800407718 +7571 4326 247156.9860412441 171760.49800407718 -91.11000000000001 45.46500000000001 +6318 7571 -91.506 45.3075 216090.13882339618 154348.18281791292 +7571 4326 216090.13882339618 154348.18281791292 -91.506 45.3075 +6318 7571 -90.714 45.6225 278051.3705382823 189325.8850733364 +7571 4326 278051.3705382823 189325.8850733364 -90.714 45.6225 +6318 7572 -89.955 43.394999999999996 181167.48675613225 63945.10947191308 +7572 4326 181167.48675613225 63945.10947191308 -89.955 43.395 +6318 7572 -90.28349999999999 43.1655 154435.48878378325 38517.05542833977 +7572 4326 154435.48878378325 38517.05542833977 -90.28349999999999 43.1655 +6318 7572 -89.62650000000001 43.6245 207698.669461132 89479.00787716228 +7572 4326 207698.669461132 89479.00787716228 -89.62650000000001 43.6245 +6318 7573 -91.11500000000001 45.894999999999996 216842.5905086011 120167.3669777322 +7573 4326 216842.5905086011 120167.3669777322 -91.11500000000001 45.89499999999998 +6318 7573 -91.5155 45.6565 185625.73451730172 93734.92082876162 +7573 4326 185625.73451730172 93734.92082876162 -91.5155 45.656499999999994 +6318 7573 -90.7145 46.1335 247793.29058794878 146756.94503121637 +7573 4326 247793.29058794878 146756.94503121637 -90.7145 46.13350000000001 +6318 7574 -88.735 44.805 252192.15151972816 85450.19226465457 +7574 4326 252192.15151972816 85450.19226465457 -88.735 44.80500000000001 +6318 7574 -89.18050000000001 44.6025 216786.95039811463 63099.123138107054 +7574 4326 216786.95039811463 63099.123138107054 -89.18050000000001 44.6025 +6318 7574 -88.28949999999999 45.0075 287350.8553946622 107995.17985522524 +7574 4326 287350.8553946622 107995.17985522524 -88.28949999999999 45.0075 +6318 7575 -92.47 45.035 178377.69509159477 111016.46175525412 +7575 4326 178377.69509159477 111016.46175525412 -92.47 45.035 +6318 7575 -92.776 44.868500000000004 154231.78382915264 92509.39415938122 +7575 4326 154231.78382915264 92509.39415938122 -92.776 44.86850000000002 +6318 7575 -92.164 45.201499999999996 202383.64861606393 129615.18909583936 +7575 4326 202383.64861606393 129615.18909583936 -92.164 45.201499999999996 +6318 7576 -90.48500000000001 45.21 187016.6360814733 111323.07816826794 +7576 4326 187016.6360814733 111323.07816826794 -90.48500000000001 45.21000000000001 +6318 7576 -90.88550000000001 45.048 155462.64910328545 93397.10648608232 +7576 4326 155462.64910328545 93397.10648608232 -90.88550000000001 45.04800000000001 +6318 7576 -90.0845 45.372 218392.08744944903 129405.59628145487 +7576 4326 218392.08744944903 129405.59628145487 -90.0845 45.372 +6318 7577 -91.385 44.29 255483.59520380863 125432.032180537 +7577 4326 255483.59520380863 125432.032180537 -91.385 44.28999999999999 +6318 7577 -91.5965 44.010999999999996 238515.59507102403 94455.29718510677 +7577 4326 238515.59507102403 94455.29718510677 -91.5965 44.01099999999999 +6318 7577 -91.1735 44.569 272291.7997750066 156453.7979659698 +7577 4326 272291.7997750066 156453.7979659698 -91.1735 44.569 +6318 7578 -90.795 43.58 221562.09212049408 48084.00910444407 +7578 4326 221562.09212049408 48084.00910444407 -90.795 43.58000000000001 +6318 7578 -91.2315 43.436 186218.53958121262 32182.31673777935 +7578 4326 186218.53958121262 32182.31673777935 -91.2315 43.43600000000001 +6318 7578 -90.3585 43.724000000000004 256737.6313897176 64171.2825725015 +7578 4326 256737.6313897176 64171.2825725015 -90.3585 43.72400000000002 +6318 7579 -89.49000000000001 46.075 134331.10865659447 50020.95805712175 +7579 4326 134331.10865659447 50020.95805712175 -89.49000000000001 46.074999999999996 +6318 7579 -89.994 45.8725 95196.87110817048 27635.749981578272 +7579 4326 95196.87110817048 27635.749981578272 -89.994 45.8725 +6318 7579 -88.986 46.277499999999996 173180.10594581225 72654.01472945852 +7579 4326 173180.10594581225 72654.01472945852 -88.986 46.277499999999996 +6318 7580 -88.53999999999999 42.67 232699.49675116193 111147.9796036397 +7580 4326 232699.49675116193 111147.9796036397 -88.53999999999999 42.67 +6318 7580 -88.756 42.508 214946.4253669212 93173.8994645169 +7580 4326 214946.4253669212 93173.8994645169 -88.756 42.50800000000002 +6318 7580 -88.324 42.832 250360.59987751397 129167.81402406926 +7580 4326 250360.59987751397 129167.81402406926 -88.324 42.831999999999994 +6318 7581 -91.80000000000001 45.894999999999996 232793.31095490142 180998.06074234744 +7581 4326 232793.31095490142 180998.06074234744 -91.80000000000001 45.89499999999998 +6318 7581 -92.034 45.6565 214548.4248304589 154518.6880060815 +7581 4326 214548.4248304589 154518.6880060815 -92.034 45.656499999999994 +6318 7581 -91.566 46.1335 250882.54385993723 207531.77122876202 +7581 4326 250882.54385993723 207531.77122876202 -91.566 46.1335 +6318 7582 -88.225 43.37 107032.89816574582 50223.50121082372 +7582 4326 107032.89816574582 50223.50121082372 -88.225 43.37 +6318 7582 -88.40050000000001 43.208 92735.57925815211 32267.301834735827 +7582 4326 92735.57925815211 32267.301834735827 -88.40050000000001 43.208 +6318 7582 -88.0495 43.532 121254.59573686423 68210.11434851718 +7582 4326 121254.59573686423 68210.11434851718 -88.0495 43.532 +6318 7583 -88.305 43.02 202267.0308394392 50056.49047637261 +7583 4326 202267.0308394392 50056.49047637261 -88.305 43.02 +6318 7583 -88.5255 42.858000000000004 184228.14811595134 32099.738536205317 +7583 4326 184228.14811595134 32099.738536205317 -88.5255 42.858000000000004 +6318 7583 -88.0845 43.182 220211.44826269647 68060.93381792572 +7583 4326 220211.44826269647 68060.93381792572 -88.0845 43.18200000000001 +6318 7584 -88.91499999999999 44.465 177188.62577816856 116088.84361179899 +7584 4326 177188.62577816856 116088.84361179899 -88.91499999999999 44.464999999999996 +6318 7584 -89.19850000000001 44.2625 154522.89713683596 93652.6335236477 +7584 4326 154522.89713683596 93652.6335236477 -89.19850000000001 44.2625 +6318 7584 -88.63149999999999 44.6675 199698.46018319903 138604.04019553747 +7584 4326 199698.46018319903 138604.04019553747 -88.63149999999999 44.6675 +6318 7585 -89.24 44.114999999999995 120224.86394850594 45187.096446580705 +7585 4326 120224.86394850594 45187.096446580705 -89.24 44.115 +6318 7585 -89.564 43.9935 94234.48371341742 31736.91825279137 +7585 4326 94234.48371341742 31736.91825279137 -89.564 43.993500000000004 +6318 7585 -88.916 44.2365 146108.95528541488 58739.65880112782 +7585 4326 146108.95528541488 58739.65880112782 -88.916 44.23650000000001 +6318 7586 -90.02 44.465 206892.0052426591 145969.65813851162 +7586 4326 206892.0052426591 145969.65813851162 -90.02 44.465 +6318 7586 -90.28999999999999 44.2625 185325.65484324246 123507.81440724643 +7586 4326 185325.65484324246 123507.81440724643 -90.28999999999999 44.26250000000001 +6318 7586 -89.75 44.6675 228310.0830855039 168503.277439127 +7586 4326 228310.0830855039 168503.277439127 -89.75 44.66750000000001 +6318 7587 -89.955 43.945 494852.80714568775 210824.26298470743 +7587 4326 494852.80714568775 210824.26298470743 -89.955 43.94499999999999 +6318 7587 -90.28349999999999 43.670500000000004 407984.54370909376 110882.47366395043 +7587 4326 407984.54370909376 110882.47366395043 -90.28349999999999 43.670500000000004 +6318 7587 -89.62650000000001 44.2195 580924.4954228676 311115.1671530499 +7587 4326 580924.4954228676 311115.1671530499 -89.62650000000001 44.2195 +6318 7588 -90.61500000000001 46.535 568817.806176971 302291.5166789758 +7588 4326 568817.806176971 302291.5166789758 -90.61500000000001 46.535 +6318 7588 -90.89850000000001 46.0355 496827.06216857623 120242.90772236373 +7588 4326 496827.06216857623 120242.90772236373 -90.89850000000001 46.0355 +6318 7588 -90.33149999999999 47.0345 639499.9236810333 484612.312887913 +7588 4326 639499.9236810333 484612.312887913 -90.33149999999999 47.03450000000001 +6318 7589 -91.845 45.425 306893.5064786074 106353.52433597221 +7589 4326 306893.5064786074 106353.52433597221 -91.845 45.42499999999999 +6318 7589 -92.1285 45.222500000000004 233842.20476632947 32637.020690204274 +7589 4326 233842.20476632947 32637.020690204274 -92.1285 45.22250000000001 +6318 7589 -91.5615 45.6275 379424.2604462074 180329.23000834632 +7589 4326 379424.2604462074 180329.23000834632 -91.5615 45.6275 +6318 7590 -91.155 46.58 749441.1470255253 454676.0021797276 +7590 4326 749441.1470255253 454676.0021797276 -91.155 46.57999999999999 +6318 7590 -91.51950000000001 46.193 657118.3265203895 313749.08941017574 +7590 4326 657118.3265203895 313749.08941017574 -91.51950000000001 46.19299999999999 +6318 7590 -90.7905 46.967 840457.645124804 596035.2532994642 +7590 4326 840457.645124804 596035.2532994642 -90.7905 46.967 +6318 7591 -88.01 44.46 101063.26509093324 547306.8975616158 +7591 4326 101063.26509093324 547306.8975616158 -88.01 44.46000000000001 +6318 7591 -88.235 44.262 42107.227994202905 475209.7683376932 +7591 4326 42107.227994202905 475209.7683376932 -88.235 44.262000000000015 +6318 7591 -87.78500000000001 44.658 159622.23310930846 619568.1237457508 +7591 4326 159622.23310930846 619568.1237457508 -87.78500000000001 44.657999999999994 +6318 7592 -91.805 44.31 572963.9386350136 302068.6549876947 +7592 4326 572963.9386350136 302068.6549876947 -91.80500000000002 44.309999999999995 +6318 7592 -92.06150000000001 44.04900000000001 505511.7024409602 207028.20332965927 +7592 4326 505511.7024409602 207028.20332965927 -92.06150000000001 44.04900000000001 +6318 7592 -91.54849999999999 44.571 639821.082556891 397323.4415499171 +7592 4326 639821.082556891 397323.4415499171 -91.54849999999999 44.571 +6318 7593 -92.46000000000001 45.894999999999996 209434.14512022396 193677.38821510584 +7593 4326 209434.14512022396 193677.38821510584 -92.46000000000001 45.89499999999998 +6318 7593 -92.847 45.6565 110466.80490729805 106946.43939307469 +7593 4326 110466.80490729805 106946.43939307469 -92.847 45.6565 +6318 7593 -92.07300000000001 46.1335 307557.7656375944 280889.9605551118 +7593 4326 307557.7656375944 280889.9605551118 -92.07300000000001 46.133500000000005 +6318 7594 -88.465 44.07 812199.4377283102 492296.28787832055 +7594 4326 812199.4377283102 492296.28787832055 -88.465 44.06999999999999 +6318 7594 -88.8475 43.592999999999996 710932.3212103479 318601.07709963387 +7594 4326 710932.3212103479 318601.07709963387 -88.8475 43.592999999999996 +6318 7594 -88.08250000000001 44.547000000000004 911851.0359000548 666472.8866177325 +7594 4326 911851.0359000548 666472.8866177325 -88.08250000000001 44.547000000000004 +6318 7595 -91.295 45.075 196856.4688343049 180077.0602635486 +7595 4326 196856.4688343049 180077.0602635486 -91.295 45.075 +6318 7595 -91.63250000000001 44.8725 109353.61456871848 106424.75680709751 +7595 4326 109353.61456871848 106424.75680709751 -91.63250000000001 44.8725 +6318 7595 -90.9575 45.277499999999996 283744.4763176626 254095.4834866127 +7595 4326 283744.4763176626 254095.4834866127 -90.95750000000001 45.27750000000002 +6318 7596 -90.62 44.730000000000004 678958.4827472217 411974.5505922153 +7596 4326 678958.4827472217 411974.5505922153 -90.62000000000002 44.730000000000004 +6318 7596 -90.899 44.451 606206.6768982302 310297.98012698995 +7596 4326 606206.6768982302 310297.98012698995 -90.899 44.451 +6318 7596 -90.34100000000001 45.009 751012.5275404988 513904.62089183606 +7596 4326 751012.5275404988 513904.62089183606 -90.34100000000001 45.00900000000001 +6318 7597 -89.39500000000001 43.465 554852.4959928978 366935.60210404213 +7597 4326 554852.4959928978 366935.60210404213 -89.39500000000001 43.46500000000001 +6318 7597 -89.7505 43.298500000000004 460206.17046774225 306446.5621844535 +7597 4326 460206.17046774225 306446.5621844535 -89.7505 43.29850000000001 +6318 7597 -89.0395 43.631499999999996 648980.7478202294 427829.2760895101 +7597 4326 648980.7478202294 427829.2760895101 -89.0395 43.6315 +6318 7598 -90.94 43.205 372703.7327319632 177993.23294857124 +7598 4326 372703.7327319632 177993.23294857124 -90.94 43.20499999999999 +6318 7598 -91.192 43.0025 305287.21083955286 104285.01075781073 +7598 4326 305287.21083955286 104285.01075781073 -91.192 43.0025 +6318 7598 -90.688 43.4075 439675.7923322014 251906.41205869502 +7598 4326 439675.7923322014 251906.41205869502 -90.688 43.40750000000001 +6318 7599 -89.42 43.07 811593.841595189 481120.2958899071 +7599 4326 811593.841595189 481120.2958899071 -89.42 43.07000000000001 +6318 7599 -89.798 42.863 710243.8050071227 405896.60914573545 +7599 4326 710243.8050071227 405896.60914573545 -89.798 42.86300000000001 +6318 7599 -89.042 43.277 912264.023173606 556801.7902147406 +7599 4326 912264.023173606 556801.7902147406 -89.042 43.27700000000001 +6318 7600 -88.71000000000001 43.24 881321.717976218 644270.3399050478 +7600 4326 881321.717976218 644270.3399050478 -88.71000000000001 43.24 +6318 7600 -88.98899999999999 42.88 806636.9402397049 513118.94706016063 +7600 4326 806636.9402397049 513118.94706016063 -88.98899999999999 42.88 +6318 7600 -88.43100000000001 43.6 955130.3567548032 775678.1163080167 +7600 4326 955130.3567548032 775678.1163080167 -88.43100000000001 43.60000000000001 +6318 7601 -87.27 45.05 521574.3624148169 236986.14621001438 +7601 4326 521574.3624148169 236986.14621001438 -87.27 45.05 +6318 7601 -87.693 44.708 411598.300883217 112574.20865393277 +7601 4326 411598.300883217 112574.20865393277 -87.693 44.708000000000006 +6318 7601 -86.847 45.392 630247.3046906667 361976.78974279866 +7601 4326 630247.3046906667 361976.78974279866 -86.847 45.39200000000001 +6318 7602 -91.925 46.455 191899.4776360648 208483.439556742 +7602 4326 191899.4776360648 208483.439556742 -91.925 46.455 +6318 7602 -92.2625 46.180499999999995 106391.35143560893 108563.02621693273 +7602 4326 106391.35143560893 108563.02621693273 -92.2625 46.1805 +6318 7602 -91.58749999999999 46.7295 276552.7565089883 308771.8953200676 +7602 4326 276552.7565089883 308771.8953200676 -91.58749999999999 46.72950000000001 +6318 7603 -91.9 44.945 168561.43814696668 195668.22738632958 +7603 4326 168561.43814696668 195668.22738632958 -91.9 44.945 +6318 7603 -92.134 44.7065 107712.8625809128 108800.62166968847 +7603 4326 107712.8625809128 108800.62166968847 -92.134 44.7065 +6318 7603 -91.666 45.1835 228908.2549861964 282714.2891128694 +7603 4326 228908.2549861964 282714.2891128694 -91.666 45.1835 +6318 7604 -91.28999999999999 44.725 393711.1914284025 247113.3462756819 +7604 4326 393711.1914284025 247113.3462756819 -91.28999999999999 44.724999999999994 +6318 7604 -91.62299999999999 44.603500000000004 306973.2552021457 202993.64076071943 +7604 4326 306973.2552021457 202993.64076071943 -91.62299999999999 44.6035 +6318 7604 -90.95700000000001 44.8465 480085.83718529483 291588.68454124127 +7604 4326 480085.83718529483 291588.68454124127 -90.95700000000001 44.846500000000006 +6318 7605 -88.37 45.870000000000005 379831.4602700064 157295.17986820664 +7605 4326 379831.4602700064 157295.17986820664 -88.37000000000002 45.86999999999999 +6318 7605 -88.658 45.726 306123.4457360423 105124.09435451332 +7605 4326 306123.4457360423 105124.09435451332 -88.658 45.726000000000006 +6318 7605 -88.082 46.014 453160.95167791966 209732.35959240922 +7605 4326 453160.95167791966 209732.35959240922 -88.082 46.01400000000001 +6318 7606 -88.735 45.724999999999994 879032.5636336508 626961.6590154264 +7606 4326 879032.5636336508 626961.6590154264 -88.735 45.724999999999994 +6318 7606 -89.0185 45.405499999999996 806062.456551946 510671.3059657595 +7606 4326 806062.456551946 510671.3059657595 -89.0185 45.4055 +6318 7606 -88.4515 46.0445 951177.9319083416 743515.1232391869 +7606 4326 951177.9319083416 743515.1232391869 -88.4515 46.0445 +6318 7607 -90.78999999999999 42.86 797681.3806848802 528025.0369254164 +7607 4326 797681.3806848802 528025.0369254164 -90.78999999999999 42.85999999999999 +6318 7607 -91.12299999999999 42.536 707939.854868681 410102.4375918321 +7607 4326 707939.854868681 410102.4375918321 -91.12299999999999 42.536 +6318 7607 -90.45700000000001 43.184 886489.1070227203 646307.335841193 +7607 4326 886489.1070227203 646307.335841193 -90.45700000000001 43.184 +6318 7608 -89.89500000000001 42.68 542910.7701910238 165833.80740886374 +7608 4326 542910.7701910238 165833.80740886374 -89.89500000000001 42.67999999999999 +6318 7608 -90.37650000000001 42.518 413052.9529784844 107246.0027533746 +7608 4326 413052.9529784844 107246.0027533746 -90.37650000000001 42.51799999999999 +6318 7608 -89.4135 42.842 672096.8785140072 225160.92173169355 +7608 4326 672096.8785140072 225160.92173169355 -89.4135 42.841999999999985 +6318 7609 -89.24 43.81 495439.9842067898 260839.71656835152 +7609 4326 495439.9842067898 260839.71656835152 -89.24 43.81000000000002 +6318 7609 -89.564 43.648 409677.2902359579 201950.95482531702 +7609 4326 409677.2902359579 201950.95482531702 -89.564 43.647999999999996 +6318 7609 -88.916 43.972 580740.3280208929 320064.9746322589 +7609 4326 580740.3280208929 320064.9746322589 -88.916 43.97200000000001 +6318 7610 -90.13 43.010000000000005 379321.8956231446 171710.59598633996 +7610 4326 379321.8956231446 171710.59598633996 -90.13 43.01 +6318 7610 -90.4 42.830000000000005 306913.4390827767 106192.39438666892 +7610 4326 306913.4390827767 106192.39438666892 -90.40000000000002 42.83000000000001 +6318 7610 -89.86 43.19 451308.490098657 237463.04313333103 +7610 4326 451308.490098657 237463.04313333103 -89.86 43.19 +6318 7611 -90.24000000000001 46.29 728932.9156932301 312413.7048818822 +7611 4326 728932.9156932301 312413.7048818822 -90.24000000000001 46.29 +6318 7611 -90.528 46.010999999999996 655768.8749970198 210779.52582408927 +7611 4326 655768.8749970198 210779.52582408927 -90.528 46.010999999999996 +6318 7611 -89.952 46.569 801357.419266067 414317.41637049115 +7611 4326 801357.419266067 414317.41637049115 -89.952 46.56900000000001 +6318 7612 -90.74000000000001 44.335 115873.48534540519 111810.95556176042 +7612 4326 115873.48534540519 111810.95556176042 -90.74000000000001 44.335 +6318 7612 -91.127 44.0965 14309.063258030425 24971.77164363019 +7612 4326 14309.063258030425 24971.77164363019 -91.127 44.0965 +6318 7612 -90.35300000000001 44.5735 216616.57017765965 199131.83275517015 +7612 4326 216616.57017765965 199131.83275517015 -90.35300000000001 44.57350000000001 +6318 7613 -88.03 43.019999999999996 573746.6824205001 292813.63391948136 +7613 4326 573746.6824205001 292813.63391948136 -88.03 43.019999999999996 +6318 7613 -88.282 42.543 505552.3842168903 119169.94042010182 +7613 4326 505552.3842168903 119169.94042010182 -88.282 42.543 +6318 7613 -87.778 43.497 640900.0461552984 466673.8154720383 +7613 4326 640900.0461552984 466673.8154720383 -87.778 43.49700000000001 +6318 7614 -87.77000000000001 44.11 204214.22198185636 307496.94191073836 +7614 4326 204214.22198185636 307496.94191073836 -87.77000000000001 44.11 +6318 7614 -88.13 43.597 108343.9955397552 120947.07481778481 +7614 4326 108343.9955397552 120947.07481778481 -88.13 43.59700000000001 +6318 7614 -87.41000000000001 44.623 298453.22786967165 494476.82780710625 +7614 4326 298453.22786967165 494476.82780710625 -87.41000000000001 44.623 +6318 7615 -91.17 43.91 466653.8558158361 167313.98616743047 +7615 4326 466653.8558158361 167313.98616743047 -91.17 43.91 +6318 7615 -91.40400000000001 43.739 404917.5855863807 104955.29710755951 +7615 4326 404917.5855863807 104955.29710755951 -91.40400000000001 43.739 +6318 7615 -90.93599999999999 44.081 528036.6700090464 229849.27384339625 +7615 4326 528036.6700090464 229849.27384339625 -90.93599999999999 44.081 +6318 7616 -89.03 45.25 651858.5747640805 380324.5001664452 +7616 4326 651858.5747640805 380324.5001664452 -89.03000000000002 45.25 +6318 7616 -89.39 45.043000000000006 558799.6630082869 305048.0292624037 +7616 4326 558799.6630082869 305048.0292624037 -89.39 45.043 +6318 7616 -88.67 45.456999999999994 744244.9573608083 456017.22369693115 +7616 4326 744244.9573608083 456017.22369693115 -88.67 45.45699999999998 +6318 7617 -89.735 45.335 380571.3551951636 178872.48903589175 +7617 4326 380571.3551951636 178872.48903589175 -89.735 45.335 +6318 7617 -90.0185 45.1325 307397.94911856676 105162.36188326628 +7617 4326 307397.94911856676 105162.36188326628 -90.0185 45.1325 +6318 7617 -89.4515 45.5375 453225.044204375 252841.8315678148 +7617 4326 453225.044204375 252841.8315678148 -89.4515 45.5375 +6318 7618 -89.77 44.905 245000.0 182100.79106445223 +7618 4326 245000.0 182100.79106445223 -89.77 44.905 +6318 7618 -90.26499999999999 44.7025 116284.02665772513 108659.26185411998 +7618 4326 116284.02665772513 108659.26185411998 -90.26499999999999 44.7025 +6318 7618 -89.275 45.1075 372815.4257135551 256327.18487126322 +7618 4326 372815.4257135551 256327.18487126322 -89.275 45.1075 +6318 7619 -87.95500000000001 45.379999999999995 720326.9650912074 251072.09774626742 +7619 4326 720326.9650912074 251072.09774626742 -87.95500000000001 45.379999999999995 +6318 7619 -88.38250000000001 45.002 609324.8519123094 113868.22047988168 +7619 4326 609324.8519123094 113868.22047988168 -88.38250000000001 45.002 +6318 7619 -87.5275 45.757999999999996 829867.8151059314 388868.61139870354 +7619 4326 829867.8151059314 388868.61139870354 -87.5275 45.75799999999999 +6318 7620 -88.735 44.985 263628.00884597766 97998.3865137757 +7620 4326 263628.00884597766 97998.3865137757 -88.735 44.985 +6318 7620 -88.9645 44.8635 203943.41259394132 54015.202579368226 +7620 4326 203943.41259394132 54015.202579368226 -88.9645 44.8635 +6318 7620 -88.5055 45.1065 323062.08112872334 142150.66956635547 +7620 4326 323062.08112872334 142150.66956635547 -88.5055 45.1065 +6318 7621 -90.64500000000001 43.945 670122.0077675191 379934.82687406166 +7621 4326 670122.0077675191 379934.82687406166 -90.64500000000001 43.945 +6318 7621 -90.9465 43.7425 590434.9640868355 306262.6953604087 +7621 4326 590434.9640868355 306262.6953604087 -90.9465 43.74250000000001 +6318 7621 -90.3435 44.1475 749269.3632612418 453899.6494194088 +7621 4326 749269.3632612418 453899.6494194088 -90.34350000000002 44.14750000000001 +6318 7622 -88.225 45.025000000000006 518117.31055014016 229044.69504424612 +7622 4326 518117.31055014016 229044.69504424612 -88.225 45.025000000000006 +6318 7622 -88.6435 44.7055 408848.3735970957 113256.08580691875 +7622 4326 408848.3735970957 113256.08580691875 -88.6435 44.70550000000001 +6318 7622 -87.8065 45.344500000000004 626184.8758540447 345398.9649045915 +7622 4326 626184.8758540447 345398.9649045915 -87.8065 45.344500000000004 +6318 7623 -89.545 45.685 229857.99996952486 181926.32698990076 +7623 4326 229857.99996952486 181926.32698990076 -89.545 45.68500000000001 +6318 7623 -89.9995 45.4825 113268.62282679594 108412.78026931446 +7623 4326 113268.62282679594 108412.78026931446 -89.9995 45.48250000000001 +6318 7623 -89.0905 45.887499999999996 345608.83493573073 256101.9944048624 +7623 4326 345608.83493573073 256101.9944048624 -89.0905 45.887499999999996 +6318 7624 -92.23 44.635 549421.489168735 281841.35724887706 +7624 4326 549421.489168735 281841.35724887706 -92.23 44.635000000000005 +6318 7624 -92.75200000000001 44.4235 413034.54169162444 205171.06771496532 +7624 4326 413034.54169162444 205171.06771496532 -92.75200000000001 44.42350000000001 +6318 7624 -91.708 44.8465 684821.2583873826 359384.3978346254 +7624 4326 684821.2583873826 359384.3978346254 -91.708 44.84649999999999 +6318 7625 -92.52000000000001 45.465 494080.5965323048 293138.2668057763 +7625 4326 494080.5965323048 293138.2668057763 -92.52000000000001 45.46500000000001 +6318 7625 -92.853 45.2265 408397.7649774445 206227.4774927449 +7625 4326 408397.7649774445 206227.4774927449 -92.853 45.2265 +6318 7625 -92.18700000000001 45.7035 579042.2915703345 380406.65507835225 +7625 4326 579042.2915703345 380406.65507835225 -92.18700000000001 45.70349999999999 +6318 7626 -89.535 44.465 175861.8571766974 181680.60200316063 +7626 4326 175861.8571766974 181680.60200316063 -89.535 44.465 +6318 7626 -89.8185 44.2625 101555.88294871042 108013.72972821812 +7626 4326 101555.88294871042 108013.72972821812 -89.8185 44.26250000000001 +6318 7626 -89.2515 44.6675 249656.6092055979 255606.86596355875 +7626 4326 249656.6092055979 255606.86596355875 -89.2515 44.66750000000001 +6318 7627 -90.36000000000001 45.68 780946.8110495314 410039.55775359157 +7627 4326 780946.8110495314 410039.55775359157 -90.36000000000001 45.68 +6318 7627 -90.64800000000001 45.400999999999996 707126.0597682047 308312.42872858496 +7627 4326 707126.0597682047 308312.42872858496 -90.64800000000001 45.40099999999999 +6318 7627 -90.072 45.959 854034.9625769928 512036.36557768495 +7627 4326 854034.9625769928 512036.36557768495 -90.072 45.959 +6318 7628 -90.435 43.36 662817.9322461424 454207.10745941673 +7628 4326 662817.9322461424 454207.10745941673 -90.435 43.36 +6318 7628 -90.6555 43.18 603995.9340882065 388676.44924024807 +7628 4326 603995.9340882065 388676.44924024807 -90.6555 43.18000000000001 +6318 7628 -90.2145 43.54 721293.4486471779 519894.841159531 +7628 4326 721293.4486471779 519894.841159531 -90.2145 43.53999999999999 +6318 7629 -89.07 42.67 480597.68578147975 264424.9885231083 +7629 4326 480597.68578147975 264424.9885231083 -89.07 42.67 +6318 7629 -89.34 42.508 407792.099534613 205496.02264331418 +7629 4326 407792.099534613 205496.02264331418 -89.34 42.508 +6318 7629 -88.8 42.832 553026.101080063 323587.5694795845 +7629 4326 553026.101080063 323587.5694795845 -88.8 42.832 +6318 7630 -91.11 45.465 810880.8786885657 563517.5674302098 +7630 4326 810880.8786885657 563517.5674302098 -91.11000000000001 45.46500000000001 +6318 7630 -91.506 45.3075 708955.7307746763 506390.66335693595 +7630 4326 708955.7307746763 506390.66335693595 -91.506 45.3075 +6318 7630 -90.714 45.6225 912240.2051592652 621146.6748399378 +7630 4326 912240.2051592652 621146.6748399378 -90.714 45.6225 +6318 7631 -89.955 43.394999999999996 594380.3293080769 209793.2469268515 +7631 4326 594380.3293080769 209793.2469268515 -89.955 43.395 +6318 7631 -90.28349999999999 43.1655 506677.09929379524 126368.03961889475 +7631 4326 506677.09929379524 126368.03961889475 -90.28349999999999 43.1655 +6318 7631 -89.62650000000001 43.6245 681424.717899397 293565.71194474 +7631 4326 681424.717899397 293565.71194474 -89.62650000000001 43.6245 +6318 7632 -91.11500000000001 45.894999999999996 711424.3994576373 394249.10338885867 +7632 4326 711424.3994576373 394249.10338885867 -91.11500000000001 45.89499999999998 +6318 7632 -91.5155 45.6565 609007.0977595159 307528.6529817777 +7632 4326 609007.0977595159 307528.6529817777 -91.5155 45.656499999999994 +6318 7632 -90.7145 46.1335 812968.4879679638 481485.077385998 +7632 4326 812968.4879679638 481485.077385998 -90.7145 46.13350000000001 +6318 7633 -88.735 44.805 827400.4166892236 280347.8386256209 +7633 4326 827400.4166892236 280347.8386256209 -88.735 44.80500000000001 +6318 7633 -89.18050000000001 44.6025 711241.8526760632 207017.70599960626 +7633 4326 711241.8526760632 207017.70599960626 -89.18050000000001 44.6025 +6318 7633 -88.28949999999999 45.0075 942750.264318903 354314.1854123515 +7633 4326 942750.264318903 354314.1854123515 -88.28949999999999 45.0075 +6318 7634 -92.47 45.035 585227.4876485065 364226.5084827796 +7634 4326 585227.4876485065 364226.5084827796 -92.47 45.035 +6318 7634 -92.776 44.868500000000004 506008.7771149777 303507.90421198664 +7634 4326 506008.7771149777 303507.90421198664 -92.776 44.86850000000002 +6318 7634 -92.164 45.201499999999996 663987.0201700359 425245.83309934975 +7634 4326 663987.0201700359 425245.83309934975 -92.164 45.201499999999996 +6318 7635 -90.48500000000001 45.21 613570.4131999684 365232.46544755815 +7635 4326 613570.4131999684 365232.46544755815 -90.48500000000001 45.21000000000001 +6318 7635 -90.88550000000001 45.048 510047.04092236375 306420.3400202542 +7635 4326 510047.04092236375 306420.3400202542 -90.88550000000001 45.04800000000001 +6318 7635 -90.0845 45.372 716508.0398964022 424558.1936239056 +7635 4326 716508.0398964022 424558.1936239056 -90.0845 45.372 +6318 7636 -91.385 44.29 838199.0955723296 411521.59179422853 +7636 4326 838199.0955723296 411521.59179422853 -91.385 44.28999999999999 +6318 7636 -91.5965 44.010999999999996 782529.9151366854 309892.0870633878 +7636 4326 782529.9151366854 309892.0870633878 -91.5965 44.01099999999999 +6318 7636 -91.1735 44.569 893344.0134030017 513298.8350419361 +7636 4326 893344.0134030017 513298.8350419361 -91.1735 44.569 +6318 7637 -90.795 43.58 726908.296933071 157755.62036399692 +7637 4326 726908.296933071 157755.62036399692 -90.795 43.58000000000001 +6318 7637 -91.2315 43.436 610951.9916437784 105584.81799103109 +7637 4326 610951.9916437784 105584.81799103109 -91.2315 43.43600000000001 +6318 7637 -90.3585 43.724000000000004 842313.3786855151 210535.2834004487 +7637 4326 842313.3786855151 210535.2834004487 -90.3585 43.72400000000002 +6318 7638 -89.49000000000001 46.075 440717.97876809473 164110.4267920735 +7638 4326 440717.97876809473 164110.4267920735 -89.49000000000001 46.074999999999996 +6318 7638 -89.994 45.8725 312325.06774464034 90668.28996422791 +7638 4326 312325.06774464034 90668.28996422791 -89.994 45.8725 +6318 7638 -88.986 46.277499999999996 568175.0640411368 238365.71355789836 +7638 4326 568175.0640411368 238365.71355789836 -88.986 46.277499999999996 +6318 7639 -88.53999999999999 42.67 763448.2656755197 364657.9964256086 +7639 4326 763448.2656755197 364657.9964256086 -88.53999999999999 42.67 +6318 7639 -88.756 42.508 705203.3973090566 305688.03516916983 +7639 4326 705203.3973090566 305688.03516916983 -88.756 42.50800000000002 +6318 7639 -88.324 42.832 821391.401515893 423778.0698533013 +7639 4326 821391.401515893 423778.0698533013 -88.324 42.831999999999994 +6318 7640 -91.80000000000001 45.894999999999996 763756.0542717048 593824.471423352 +7640 4326 763756.0542717048 593824.471423352 -91.80000000000001 45.89499999999998 +6318 7640 -92.034 45.6565 703897.623711763 506950.06270445295 +7640 4326 703897.623711763 506950.06270445295 -92.034 45.656499999999994 +6318 7640 -91.566 46.1335 823103.8125609764 680877.1532441973 +7640 4326 823103.8125609764 680877.1532441973 -91.566 46.1335 +6318 7641 -88.225 43.37 351157.09974420175 164774.9370466775 +7641 4326 351157.09974420175 164774.9370466775 -88.225 43.37 +6318 7641 -88.40050000000001 43.208 304249.97929487145 105863.63959362914 +7641 4326 304249.97929487145 105863.63959362914 -88.40050000000001 43.208 +6318 7641 -88.0495 43.532 397816.11919211276 223786.0169825935 +7641 4326 397816.11919211276 223786.0169825935 -88.0495 43.532 +6318 7642 -88.305 43.02 663604.416624061 164227.00234973247 +7642 4326 663604.416624061 164227.00234973247 -88.305 43.02 +6318 7642 -88.5255 42.858000000000004 604421.8488887512 105313.89202603363 +7642 4326 604421.8488887512 105313.89202603363 -88.5255 42.858000000000004 +6318 7642 -88.0845 43.182 722477.0594535309 223296.5802128113 +7642 4326 722477.0594535309 223296.5802128113 -88.0845 43.18200000000001 +6318 7643 -88.91499999999999 44.465 581326.3498794585 380868.1477838772 +7643 4326 581326.3498794585 380868.1477838772 -88.91499999999999 44.464999999999996 +6318 7643 -89.19850000000001 44.2625 506963.8718286866 307258.6818530009 +7643 4326 506963.8718286866 307258.6818530009 -89.19850000000001 44.2625 +6318 7643 -88.63149999999999 44.6675 655177.364923296 454736.75524235924 +7643 4326 655177.364923296 454736.75524235924 -88.63149999999999 44.6675 +6318 7644 -89.24 44.114999999999995 394437.74108155543 148251.3325902402 +7644 4326 394437.74108155543 148251.3325902402 -89.24 44.115 +6318 7644 -89.564 43.9935 309167.63526026916 104123.53963278302 +7644 4326 309167.63526026916 104123.53963278302 -89.564 43.993500000000004 +6318 7644 -88.916 44.2365 479359.1307427308 192715.03091511686 +7644 4326 479359.1307427308 192715.03091511686 -88.916 44.23650000000001 +6318 7645 -90.02 44.465 678778.1871085422 478902.1198277674 +7645 4326 678778.1871085422 478902.1198277674 -90.02 44.465 +6318 7645 -90.28999999999999 44.2625 608022.5858397895 405208.55418610823 +7645 4326 608022.5858397895 405208.55418610823 -90.28999999999999 44.26250000000001 +6318 7645 -89.75 44.6675 749047.3308312756 552831.1691498698 +7645 4326 749047.3308312756 552831.1691498698 -89.75 44.66750000000001 +7686 7692 69.63 39.864999999999995 1395259.8690312996 4429876.307064979 +7692 4326 1395259.8690312996 4429876.307064979 69.63 39.86499999999999 +7686 7692 69.279 39.5455 1365528.4933038591 4394086.837905082 +7692 4326 1365528.4933038591 4394086.837905082 69.279 39.545500000000004 +7686 7692 69.981 40.1845 1424710.1797213883 4465787.164885141 +7692 4326 1424710.1797213883 4465787.164885141 69.981 40.18449999999999 +7686 7693 71.515 41.01 2299859.7958871266 4556427.332497741 +7693 4326 2299859.7958871266 4556427.332497741 71.515 41.01 +7686 7693 70.1605 39.372 2183135.645545392 4375424.006816448 +7693 4326 2183135.645545392 4375424.006816448 70.1605 39.372 +7686 7693 72.8695 42.647999999999996 2410939.8059561034 4739247.360405397 +7693 4326 2410939.8059561034 4739247.360405397 72.8695 42.647999999999996 +7686 7694 74.515 41.285 3299860.380447175 4586967.941594607 +7694 4326 3299860.380447175 4586967.941594607 74.515 41.285 +7686 7694 73.1605 39.5435 3183422.1288238056 4394465.700048494 +7694 4326 3183422.1288238056 4394465.700048494 73.1605 39.5435 +7686 7694 75.8695 43.0265 3410264.645121636 4781295.666558218 +7694 4326 3410264.645121636 4781295.666558218 75.8695 43.0265 +7686 7695 77.515 41.675 4299861.214998864 4630282.588697472 +7695 4326 4299861.214998864 4630282.588697472 77.515 41.67499999999999 +7686 7695 76.1605 40.4825 4185009.208205229 4498732.581525842 +7695 4326 4185009.208205229 4498732.581525842 76.1605 40.482499999999995 +7686 7695 78.8695 42.8675 4410548.857163798 4763631.724249347 +7695 4326 4410548.857163798 4763631.724249347 78.8695 42.86750000000001 +7686 7696 79.65 42.23 5228454.888508259 4692291.4425116945 +7696 4326 5228454.888508259 4692291.4425116945 79.65 42.230000000000004 +7686 7696 79.07400000000001 41.717 5179944.356893823 4635953.332857546 +7696 4326 5179944.356893823 4635953.332857546 79.07400000000001 41.717 +7686 7696 80.226 42.742999999999995 5276200.20929949 4748954.167443692 +7696 4326 5276200.20929949 4748954.167443692 80.226 42.742999999999995 +4326 7755 81.50999999999999 19.689999999999998 4155671.08950544 3532449.905994735 +4326 7755 67.19099999999999 5.452 2537947.248280095 2019453.4443315214 +4326 7755 95.82900000000001 33.928 5454372.518230997 5166282.479413344 +4326 7756 80.78 16.265 989854.8494976773 1001060.0525322463 +4326 7756 77.153 12.9755 595930.2662886409 640911.9721633589 +4326 7756 84.407 19.5545 1370887.633202475 1368195.3042697778 +4326 7757 94.485 28.06 998525.6126800922 1006473.3262883477 +4326 7757 91.84349999999999 26.790999999999997 735846.1346663609 868741.5659696601 +4326 7757 97.12650000000001 29.329 1255118.7269931263 1149854.7652096497 +4326 7758 92.86 26.055 1011004.8542346645 1005810.9268460992 +4326 7758 90.007 24.322499999999998 721571.767056358 816834.5766433119 +4326 7758 95.713 27.7875 1292053.9729301655 1201062.4583717266 +4326 7759 85.805 26.07 992997.5372949764 1021350.8869987233 +4326 7759 83.5595 24.459 765233.4439136885 844984.2177827323 +4326 7759 88.0505 27.680999999999997 1214658.8154225517 1201619.7528128899 +4326 7760 77.08500000000001 28.645 1008310.3685215118 1002208.3050798685 +4326 7760 76.85549999999999 28.4245 985842.8497200669 977776.488256208 +4326 7760 77.31450000000001 28.8655 1030684.0659991936 1026683.9411407148 +4326 7761 71.305 22.380000000000003 992793.8023940495 1000215.1802137584 +4326 7761 68.44749999999999 20.283 694129.9770415456 771042.6264986038 +4326 7761 74.16250000000001 24.477 1282643.9381395676 1235037.9286677705 +4326 7762 76.03 29.295 1002914.228273299 1004736.4155411053 +4326 7762 74.61699999999999 27.8145 863722.0761715933 841475.4460199786 +4326 7762 77.443 30.7755 1138151.2920421518 1169690.1404010335 +4326 7763 77.285 31.82 991480.3314408708 1007560.7252171321 +4326 7763 75.74149999999999 30.523999999999997 843218.2442496013 865054.0849278615 +4326 7763 78.8285 33.116 1135670.0892393503 1152175.2877652291 +4326 7764 76.66499999999999 33.89 1015258.4030163321 904017.7968109315 +4326 7764 74.0505 32.432 769563.8214025139 745112.4764745068 +4326 7764 79.2795 35.348 1252531.4011216934 1069175.3320630922 +4326 7765 85.65 23.655 1002550.0767881557 1003153.1544247167 +4326 7765 83.553 22.1295 786208.8260860436 835775.7860634786 +4326 7765 87.747 25.180500000000002 1213934.7613134452 1173702.6858372511 +4326 7766 78.42 23.975 1004576.9402191319 996646.9924510206 +4326 7766 74.469 21.360500000000002 594745.7555903045 712810.307407688 +4326 7766 82.37100000000001 26.5895 1398163.4869506527 1291843.8336661807 +4326 7767 76.75 18.82 1000000.0 993346.2245220277 +4326 7767 73.015 15.921999999999999 599881.1549498705 676936.5108400639 +4326 7767 80.485 21.718 1386591.226120326 1318137.48879192 +4326 7768 93.86500000000001 24.77 986347.3980206526 1002154.5052544827 +4326 7768 93.0595 23.933 904250.5018848627 909771.0632597638 +4326 7768 94.6705 25.607 1067353.4045624335 1095029.3184345148 +4326 7769 91.315 25.575000000000003 993971.6438970361 994435.0005748795 +4326 7769 89.9695 25.084500000000002 858211.9590212558 940849.7857632752 +4326 7769 92.6605 26.0655 1128628.2467825077 1049396.9128308005 +4326 7770 94.28999999999999 26.125 991499.745622188 999911.9652637637 +4326 7770 93.426 25.2925 904425.3751708564 908035.0399465176 +4326 7770 95.154 26.9575 1077349.6770122177 1092374.0668924546 +4326 7771 93.555 25.705 1005514.5785864348 1007801.0881097418 +4326 7771 90.0765 22.3165 647084.7007606344 637232.717757564 +4326 7771 97.0335 29.0935 1344231.5909301427 1387725.9252473873 +4326 7772 84.44 20.185000000000002 1006791.2259996237 992470.6938579798 +4326 7772 81.68599999999999 18.0385 715205.1926418992 757218.694715675 +4326 7772 87.194 22.3315 1290469.2747299299 1232554.9565124668 +4326 7773 75.405 31.06 1002862.8998678066 1006454.0094781166 +4326 7773 74.0235 29.692 869187.1869831615 855603.8471948013 +4326 7773 76.7865 32.428 1132771.2374360391 1158976.3849168487 +4326 7774 73.875 26.63 1000000.0 971768.4028082504 +4326 7774 69.9195 23.416999999999998 595473.4475993575 622296.65245421 +4326 7774 77.8305 29.843 1382313.605715685 1333598.1320635579 +4326 7775 80.86 27.145000000000003 998514.2312742973 1001361.042464815 +4326 7775 77.458 24.1975 652706.6265173338 679660.7186340799 +4326 7775 84.262 30.0925 1326643.0329029069 1332330.191683125 +4326 7776 79.28999999999999 30.095 991807.7205559801 1010342.6216792151 +4326 7776 77.733 28.8485 839770.1713894878 873333.4519472062 +4326 7776 80.847 31.3415 1140091.9947300798 1149424.789541393 +4326 7777 93.24000000000001 10.215 998904.397426632 996128.9120513202 +4326 7777 92.259 7.0515 890512.2133156771 646383.4181561233 +4326 7777 94.221 13.378499999999999 1105175.2886843118 1346264.5062884865 +4326 7778 82.31 20.945 1006239.4664024926 993912.7666992052 +4326 7778 80.438 18.096500000000002 808210.8360089895 679581.6245721118 +4326 7778 84.182 23.793499999999998 1196889.3379588374 1310628.501930644 +4326 7779 73.97999999999999 15.33 997852.3432172821 995020.7955766029 +4326 7779 73.647 14.907 962018.438975695 948246.3146218971 +4326 7779 74.31299999999999 15.753 1033542.4609231021 1041851.8217855055 +4326 7780 76.28999999999999 15.015 990860.6603898675 987832.7445300735 +4326 7780 74.229 11.9145 766217.6480548496 645780.3387506653 +4326 7780 78.351 18.1155 1209123.6772814314 1332000.6249600432 +4326 7781 76.105 10.525 1011492.2340541263 1002766.9771112419 +4326 7781 74.93950000000001 8.4775 883230.2561770554 776479.9982404611 +4326 7781 77.2705 12.572500000000002 1138060.9258376327 1229571.8540228806 +4326 7782 72.9 9.985 975331.0972425067 998349.3598408131 +4326 7782 72.126 8.387500000000001 889973.1865581874 821801.3555279193 +4326 7782 73.674 11.5825 1059876.054387541 1175094.9851216052 +4326 7783 92.85 23.235 1010234.2402734077 1012185.4238614334 +4326 7783 92.31 22.0695 954587.3168716185 883183.1854271339 +4326 7783 93.39 24.4005 1064917.7492810988 1141415.0519093093 +4326 7784 88.465 27.61 996545.0451642963 998338.3016950551 +4326 7784 88.05550000000001 27.133 955933.2900744304 945560.0787955318 +4326 7784 88.8745 28.087 1036806.7665800899 1051253.7721059988 +4326 7785 78.31 10.805 992893.1420116515 992259.4000652124 +4326 7785 76.429 8.298499999999999 785629.8597126368 715609.7837191148 +4326 7785 80.191 13.311499999999999 1196748.8734854294 1270195.9909380933 +4326 7786 91.745 23.740000000000002 999490.2332894068 998892.4831545604 +4326 7786 91.2095 23.02 944595.1485301852 919256.5476260855 +4326 7786 92.2805 24.46 1053785.262804593 1078741.1225639042 +4326 7787 87.85 24.36 997463.6911658242 998339.0101403189 +4326 7787 86.023 21.776999999999997 808463.7317231982 713473.4106227737 +4326 7787 89.67699999999999 26.943 1178939.575442663 1285725.4942693058 +6706 7791 8.965 41.785 497091.64645042346 4625906.061436742 +7791 4326 497091.64645042346 4625906.061436742 8.965 41.78499999999999 +6706 7791 6.233499999999999 37.0555 254000.3275612877 4104609.5557094966 +7791 4326 254000.3275612877 4104609.5557094966 6.233499999999999 37.0555 +6706 7791 11.6965 46.5145 706848.5806319211 5154747.393047125 +7791 4326 706848.5806319211 5154747.393047125 11.6965 46.5145 +6706 7792 15.0 40.945 500000.00000000134 4532651.569272315 +7792 4326 500000.00000000134 4532651.569272315 14.999999999999998 40.94500000000001 +6706 7792 12.3 35.405499999999996 254812.08870369926 3921361.4559117802 +7792 4326 254812.08870369926 3921361.4559117802 12.300000000000002 35.40549999999999 +6706 7792 17.7 46.4845 707231.0799165128 5151423.26514731 +7792 4326 707231.0799165128 5151423.26514731 17.7 46.4845 +6706 7793 18.49 38.2 280211.8499703541 4230984.037387538 +7793 4326 280211.8499703541 4230984.037387538 18.49 38.199999999999996 +6706 7793 18.04 35.104 230196.3330072955 3888586.2343832804 +7793 4326 230196.3330072955 3888586.2343832804 18.04 35.10399999999999 +6706 7793 18.939999999999998 41.296 327523.36855843413 4573663.423681971 +7793 4326 327523.36855843413 4573663.423681971 18.939999999999998 41.296 +6706 7794 12.459999999999999 40.93 7038685.047080272 4526102.104025962 +7794 4326 7038685.047080272 4526102.104025962 12.46 40.92999999999999 +6706 7794 6.583 35.376999999999995 6508266.154277894 3924025.7764995354 +7794 4326 6508266.154277894 3924025.7764995354 6.582999999999999 35.376999999999995 +6706 7794 18.336999999999996 46.483000000000004 7485814.593868981 5161570.967588263 +7794 4326 7485814.593868981 5161570.967588263 18.336999999999993 46.483000000000004 +6706 7795 12.459999999999999 40.93 3038743.1618230063 4532901.456210277 +7795 4326 3038743.1618230063 4532901.456210277 12.46 40.92999999999999 +6706 7795 6.583 35.376999999999995 2507527.445446063 3929920.657485764 +7795 4326 2507527.445446063 3929920.657485764 6.582999999999999 35.376999999999995 +6706 7795 18.336999999999996 46.483000000000004 3486544.4104847084 5169324.955020794 +7795 4326 3486544.4104847084 5169324.955020794 18.337 46.48300000000001 +7798 7799 23.18 42.775 678338.5933823967 4738134.076972821 +7799 4326 678338.5933823967 4738134.076972821 23.18 42.775 +7798 7799 22.442 41.4655 620418.7570640909 4591437.589417864 +7799 4326 620418.7570640909 4591437.589417864 22.442 41.4655 +7798 7799 23.918 44.0845 733619.1368765456 4885398.770768928 +7799 4326 733619.1368765456 4885398.770768928 23.917999999999996 44.08449999999999 +7798 7800 27.005000000000003 42.695 500409.5517170227 4726945.9824498 +7800 4326 500409.5517170227 4726945.9824498 27.005000000000003 42.695 +7798 7800 24.3005 41.3855 274285.1909772019 4585069.428936362 +7800 4326 274285.1909772019 4585069.428936362 24.3005 41.38549999999999 +7798 7800 29.709500000000002 44.0045 717218.2653763022 4875942.1279491065 +7800 4326 717218.2653763022 4875942.1279491065 29.709500000000006 44.0045 +7798 7801 25.52 42.735 501637.7008809756 4733280.685306847 +7801 4326 501637.7008809756 4733280.685306847 25.52 42.73499999999999 +7798 7801 22.676 41.3895 263808.12810714834 4587773.502761255 +7801 4326 263808.12810714834 4587773.502761255 22.676 41.38949999999999 +7798 7801 28.363999999999997 44.0805 729410.3886458923 4886658.6795120165 +7801 4326 729410.3886458923 4886658.6795120165 28.363999999999997 44.0805 +7798 7803 23.18 42.775 678338.5933823967 4738134.076972821 +7803 4326 678338.5933823967 4738134.076972821 23.18 42.775 +7798 7803 22.442 41.4655 620418.7570640909 4591437.589417864 +7803 4326 620418.7570640909 4591437.589417864 22.442 41.4655 +7798 7803 23.918 44.0845 733619.1368765456 4885398.770768928 +7803 4326 733619.1368765456 4885398.770768928 23.917999999999996 44.08449999999999 +7798 7805 30.675 43.115 310843.38832473865 4776209.286847497 +7805 4326 310843.38832473865 4776209.286847497 30.675 43.115 +7798 7805 30.0675 42.615500000000004 259482.8708484768 4722287.501912707 +7805 4326 259482.8708484768 4722287.501912707 30.0675 42.61550000000001 +7798 7805 31.282500000000002 43.6145 361411.493525849 4830490.7112555895 +7805 4326 361411.493525849 4830490.7112555895 31.282500000000002 43.6145 +4284 7825 23.58 49.815 1305757.73520758 5511155.452707492 +7825 4326 1305757.73520758 5511155.452707492 23.57824780216191 49.814710967735536 +4284 7825 22.293 47.920500000000004 1209788.0664991243 5301172.124065054 +7825 4326 1209788.0664991243 5301172.124065054 22.291314348172058 47.920164707388906 +4284 7825 24.867 51.7095 1394488.8300980728 5722791.943599453 +7825 4326 1394488.8300980728 5722791.943599453 24.865174532981833 51.70925845426643 +4284 7826 26.505000000000003 49.84 2300359.673051118 5513933.084688483 +7826 4326 2300359.673051118 5513933.084688483 26.50326865750456 49.839761679710385 +4284 7826 25.1505 47.932 2199160.0742786713 5302627.166647912 +7826 4326 2199160.0742786713 5302627.166647912 25.14883247160002 47.93171292125681 +4284 7826 27.8595 51.748 2393890.6353491917 5727065.648688594 +7826 4326 2393890.6353491917 5727065.648688594 27.857698391843773 51.7478116274727 +4284 7827 29.505000000000003 48.635000000000005 3300368.530995627 5379917.528144995 +7827 4326 3300368.530995627 5379917.528144995 29.503343601952437 48.634795448413485 +4284 7827 28.1505 45.5255 3194571.719168422 5035107.0844636895 +7827 4326 3194571.719168422 5035107.0844636895 28.14893991985893 45.52522913912529 +4284 7827 30.8595 51.7445 3393897.8923440333 5726676.24870657 +7827 4326 3393897.8923440333 5726676.24870657 30.857729769823354 51.74436373176291 +4284 7828 32.504999999999995 48.35 4300370.602135492 5348225.016772299 +7828 4326 4300370.602135492 5348225.016772299 32.50338646131313 48.349840736230945 +4284 7828 31.1505 44.723 4193082.8649748396 4945920.422291468 +7828 4326 4193082.8649748396 4945920.422291468 31.14899351974651 44.72276489771242 +4284 7828 33.8595 51.977000000000004 4393415.053347531 5752544.011326725 +7828 4326 4393415.053347531 5752544.011326725 33.857756129542985 51.976918681542756 +4284 7829 35.504999999999995 48.29 5300371.036988554 5341553.110107769 +7829 4326 5300371.036988554 5341553.110107769 35.50342578836391 48.289888376166985 +4284 7829 34.1505 44.726 5193088.391662897 4946253.808236763 +7829 4326 5193088.391662897 4946253.808236763 34.149026348160675 44.72581124996234 +4284 7829 36.8595 51.854 5393670.684677176 5738859.00954907 +7829 4326 5393670.684677176 5738859.00954907 36.857803722668 51.85396731635289 +4284 7830 38.504999999999995 48.62 6300368.6402326645 5378249.461923084 +7830 4326 6300368.6402326645 5378249.461923084 38.50345609190277 48.619941433414986 +4284 7830 37.1505 46.982 6197326.89650217 5197007.069972888 +7830 4326 6197326.89650217 5197007.069972888 37.14899322570569 46.98189340205585 +4284 7830 39.8595 50.257999999999996 6396947.731651002 5561312.49021878 +7830 4326 6396947.731651002 5561312.49021878 39.85791594182816 50.25799014828518 +4284 7831 40.09 49.209999999999994 7197262.013728979 5444820.530262541 +7831 4326 7197262.013728979 5444820.530262541 40.08845914700457 49.209976216026504 +4284 7831 40.009 48.840999999999994 7190553.817785529 5403898.328932875 +7831 4326 7190553.817785529 5403898.328932875 40.00747123578549 48.8409687383298 +4284 7831 40.171 49.579 7203886.643857019 5485751.9429679485 +7831 4326 7203886.643857019 5485751.9429679485 40.16944680791327 49.578983699965136 +7844 7845 133.26999999999998 -26.78 -71707.68538801056 -3036901.4555975227 +7845 4326 -71707.68538801056 -3036901.4555975227 133.26999999999998 -26.779999999999998 +7844 7845 114.892 -42.008 -1614114.5719233775 -4847489.270308387 +7845 4326 -1614114.5719233775 -4847489.270308387 114.892 -42.00800000000001 +7844 7845 151.648 -11.552 1963804.4971919449 -1489224.9978584847 +7845 4326 1963804.4971919449 -1489224.9978584847 151.648 -11.551999999999998 +7844 7846 94.71000000000001 -12.015 686164.1589146144 8671209.265994793 +7846 4326 686164.1589146144 8671209.265994793 94.71000000000001 -12.015000000000004 +7844 7846 93.53999999999999 -15.1155 558023.5011035556 8328827.826709125 +7846 4326 558023.5011035556 8328827.826709125 93.53999999999999 -15.115499999999997 +7844 7846 95.88000000000001 -8.9145 816754.1205866318 9013366.039271686 +7846 4326 816754.1205866318 9013366.039271686 95.88000000000001 -8.914500000000007 +7844 7847 98.17 -12.015 409649.0483432067 8671651.456028556 +7847 4326 409649.0483432067 8671651.456028556 98.17 -12.014999999999999 +7844 7847 96.217 -15.2055 200992.27437954425 8317038.868694795 +7847 4326 200992.27437954425 8317038.868694795 96.217 -15.205500000000004 +7844 7847 100.123 -8.8245 623499.9094394216 9024364.83362359 +7847 4326 623499.9094394216 9024364.83362359 100.123 -8.8245 +7844 7848 105.075 -21.775 507753.7174763942 7592075.165138347 +7848 4326 507753.7174763942 7592075.165138347 105.075 -21.775 +7844 7848 102.4335 -33.3895 261275.51969203193 6302588.07154193 +7848 4326 261275.51969203193 6302588.07154193 102.4335 -33.3895 +7844 7848 107.71650000000001 -10.160499999999999 797674.2425833207 8875596.44564535 +7848 4326 797674.2425833207 8875596.44564535 107.71650000000001 -10.160499999999994 +7844 7849 111.005 -24.28 500507.448992137 7314772.988566413 +7849 4326 500507.448992137 7314772.988566413 111.00500000000001 -24.28 +7844 7849 108.3005 -36.484 258169.39904554875 5958977.854632109 +7849 4326 258169.39904554875 5958977.854632109 108.3005 -36.483999999999995 +7844 7849 113.7095 -12.076 794971.826734399 8663582.464576721 +7849 4326 794971.826734399 8663582.464576721 113.7095 -12.076000000000004 +7844 7850 117.005 -25.295 500503.33718633483 7202386.6635158025 +7850 4326 500503.33718633483 7202386.6635158025 117.005 -25.294999999999995 +7844 7850 114.3005 -37.2065 260436.1764596865 5878805.476842016 +7850 4326 260436.1764596865 5878805.476842016 114.30050000000001 -37.20649999999999 +7844 7850 119.7095 -13.383500000000005 793462.5074147208 8518846.821110332 +7850 4326 793462.5074147208 8518846.821110332 119.70950000000002 -13.383500000000003 +7844 7851 123.005 -24.265 500507.50856923853 7316433.756566911 +7851 4326 500507.50856923853 7316433.756566911 123.00500000000001 -24.265 +7844 7851 120.3005 -36.6895 258810.2300880482 5936175.273307532 +7851 4326 258810.2300880482 5936175.273307532 120.30049999999999 -36.68950000000001 +7844 7851 125.7095 -11.840500000000002 795227.4433792764 8689650.334980171 +7851 4326 795227.4433792764 8689650.334980171 125.7095 -11.840500000000004 +7844 7852 128.995 -23.240000000000002 499488.5027524061 7429911.893493047 +7852 4326 499488.5027524061 7429911.893493047 128.995 -23.24 +7844 7852 126.2905 -35.966 255665.94256330913 6016428.15543524 +7852 4326 255665.94256330913 6016428.15543524 126.2905 -35.966 +7844 7852 131.6995 -10.514000000000003 795479.038805508 8836486.096057687 +7852 4326 795479.038805508 8836486.096057687 131.6995 -10.514000000000003 +7844 7853 135.005 -24.795 500505.3824881319 7257751.289519885 +7853 4326 500505.3824881319 7257751.289519885 135.005 -24.795 +7844 7853 132.3005 -39.1185 266618.18080348126 5666603.0077016875 +7853 4326 266618.18080348126 5666603.0077016875 132.3005 -39.1185 +7844 7853 137.7095 -10.471500000000002 796614.9159729548 8841180.689241666 +7853 4326 796614.9159729548 8841180.689241666 137.7095 -10.471500000000011 +7844 7854 141.005 -28.634999999999998 500488.70064734353 6832452.150132753 +7854 4326 500488.70064734353 6832452.150132753 141.005 -28.634999999999994 +7844 7854 138.3005 -46.2345 291859.4911454697 4876355.083101071 +7854 4326 291859.4911454697 4876355.083101071 138.3005 -46.2345 +7844 7854 143.7095 -11.035499999999999 796063.682372214 8778754.575019982 +7854 4326 796063.682372214 8778754.575019982 143.7095 -11.035499999999999 +7844 7855 147.005 -30.060000000000002 500481.9480697148 6674566.120553608 +7855 4326 500481.9480697148 6674566.120553608 147.005 -30.06 +7844 7855 144.3005 -48.807 301799.636594196 4590484.040062539 +7855 4326 301799.636594196 4590484.040062539 144.3005 -48.80700000000001 +7844 7855 149.7095 -11.313000000000002 795781.9772796432 8748038.943825705 +7855 4326 795781.9772796432 8748038.943825705 149.7095 -11.313 +7844 7856 153.0 -36.415 500000.0 5970020.382468974 +7856 4326 500000.0 5970020.382468974 153.0 -36.415 +7844 7856 150.3 -56.7055 334711.61871741887 3712139.428408703 +7856 4326 334711.61871741887 3712139.428408703 150.3 -56.70549999999999 +7844 7856 155.7 -16.124499999999998 788788.7017833702 8215402.2766512865 +7856 4326 788788.7017833702 8215402.2766512865 155.7 -16.124499999999998 +7844 7857 159.005 -37.32 500443.0090585269 5869628.0173232835 +7857 4326 500443.0090585269 5869628.0173232835 159.005 -37.32 +7844 7857 156.3005 -58.236000000000004 341512.01405727677 3541839.4355010362 +7857 4326 341512.01405727677 3541839.4355010362 156.3005 -58.23599999999999 +7844 7857 161.7095 -16.404000000000003 789395.273472464 8184442.074905855 +7857 4326 789395.273472464 8184442.074905855 161.7095 -16.403999999999996 +7844 7858 165.0 -42.665 499999.9999999979 5276385.291762875 +7858 4326 499999.9999999979 5276385.291762875 165.0 -42.665 +7844 7858 162.3 -57.7175 339175.74886605825 3599536.2072792435 +7858 4326 339175.74886605825 3599536.2072792435 162.3 -57.717499999999994 +7844 7858 167.7 -27.6125 766471.0403257085 6942810.329173721 +7858 4326 766471.0403257085 6942810.329173721 167.7 -27.612499999999994 +7844 7859 170.675 -29.044999999999998 468358.6517043048 6786985.184161477 +7859 4326 468358.6517043048 6786985.184161477 170.675 -29.044999999999995 +7844 7859 168.2675 -32.1365 242241.1413549263 6441163.117441116 +7859 4326 242241.1413549263 6441163.117441116 168.2675 -32.1365 +7844 7859 173.08249999999998 -25.9535 708522.2486715973 7127806.7884851955 +7859 4326 708522.2486715973 7127806.7884851955 173.08249999999998 -25.953499999999988 +4710 7877 -5.72 -15.965 299643.1422482233 2000184.4303049454 +7877 4326 299643.1422482233 2000184.4303049454 -5.715176238288724 -15.969769388490334 +4710 7877 -5.837 -16.068499999999997 287124.0641690978 1988727.3370383133 +7877 4326 287124.0641690978 1988727.3370383133 -5.832180955906471 -16.07327315620052 +4710 7877 -5.603 -15.8615 312175.08601434 2011634.3756690368 +7877 4326 312175.08601434 2011634.3756690368 -5.598171529962129 -15.86626563828474 +4710 7878 -5.72 -15.965 208827.09228889085 8233013.005507114 +7878 4326 208827.09228889085 8233013.005507114 -5.715176238288724 -15.969769388490342 +4710 7878 -5.837 -16.068499999999997 196450.51322678727 8221384.939655367 +7878 4326 196450.51322678727 8221384.939655367 -5.832180955906471 -16.073273156200518 +4710 7878 -5.603 -15.8615 221216.50691270083 8244632.01763027 +7878 4326 221216.50691270083 8244632.01763027 -5.598171529962129 -15.866265638284744 +7881 7882 -5.72 -15.965 299126.8936732141 2000712.3067075985 +7882 4326 299126.8936732141 2000712.3067075985 -5.719999337409662 -15.964999462815808 +7881 7882 -5.837 -16.068499999999997 286608.3218182142 1989255.3751244734 +7882 4326 286608.3218182142 1989255.3751244734 -5.836999338681976 -16.068499464895872 +7881 7882 -5.603 -15.8615 311658.33103407436 2012162.0911390178 +7882 4326 311658.33103407436 2012162.0911390178 -5.602999336139267 -15.861499460741154 +7881 7883 -5.72 -15.965 208838.86483196286 8233034.180705197 +7883 4326 208838.86483196286 8233034.180705197 -5.719999337409663 -15.964999462815811 +7881 7883 -5.837 -16.068499999999997 196462.79053044005 8221406.28361172 +7883 4326 196462.79053044005 8221406.28361172 -5.836999338681976 -16.06849946489588 +7881 7883 -5.603 -15.8615 221227.77456225065 8244653.024904505 +7883 4326 221227.77456225065 8244653.024904505 -5.602999336139267 -15.861499460741154 +7886 7887 -5.72 -15.965 208838.86483159882 8233034.180761196 +7887 4326 208838.86483159882 8233034.180761196 -5.719999999999999 -15.965000000000002 +7886 7887 -5.837 -16.068499999999997 196462.7905300554 8221406.283668054 +7887 4326 196462.7905300554 8221406.283668054 -5.837000000000001 -16.068499999999986 +7886 7887 -5.603 -15.8615 221227.77456190664 8244653.02496017 +7887 4326 221227.77456190664 8244653.02496017 -5.603 -15.861499999999992 +7844 7899 145.5 -36.59 2544737.813041602 2545375.268505794 +7899 4326 2544737.813041602 2545375.268505794 145.5 -36.59000000000001 +7844 7899 141.41400000000002 -38.939 2189033.37764663 2278913.0514758993 +7899 4326 2189033.37764663 2278913.0514758993 141.41400000000002 -38.939000000000014 +7844 7899 149.58599999999998 -34.241 2922731.8103748485 2796003.3990888703 +7899 4326 2922731.8103748485 2796003.3990888703 149.58599999999998 -34.241000000000014 +4267 7991 -79.495 44.795 305195.6094941829 4961449.500818744 +7991 4326 305195.6094941829 4961449.500818744 -79.4947307431111 44.79498977380152 +4267 7991 -80.8495 42.5135 193903.38767093729 4708872.95543081 +7991 4326 193903.38767093729 4708872.95543081 -80.84928649115359 42.513503515457884 +4267 7991 -78.14049999999999 47.076499999999996 408042.6273555514 5215908.426183349 +7991 4326 408042.6273555514 5215908.426183349 -78.14017012820473 47.076485412754934 +4259 7992 12.079999999999998 -5.915 176644.82516291866 9345334.617562447 +7992 4326 176644.82516291866 9345334.617562447 12.080432906213334 -5.915965830483329 +4259 7992 11.818999999999999 -6.0275 147786.11030381144 9332720.706056243 +7992 4326 147786.11030381144 9332720.706056243 11.819422720013927 -6.02846708939046 +4259 7992 12.341 -5.8025 205509.90988042258 9357929.13930254 +7992 4326 205509.90988042258 9357929.13930254 12.341443080972239 -5.803464572582078 +7844 8013 117.88499999999999 -34.980000000000004 50152.184629889736 227608.86652404815 +8013 4326 50152.184629889736 227608.86652404815 117.88499999999999 -34.98000000000001 +7844 8013 117.5835 -35.187 22691.01763044766 204602.5751090506 +8013 4326 22691.01763044766 204602.5751090506 117.58350000000002 -35.187 +7844 8013 118.1865 -34.773 77751.91044729637 250531.3257979718 +8013 4326 77751.91044729637 250531.3257979718 118.18650000000001 -34.773 +7844 8014 115.245 -21.205 59480.859980841684 354220.6976669589 +8014 4326 59480.859980841684 354220.6976669589 115.245 -21.205 +7844 8014 114.9345 -22.1005 27443.60682483571 255031.9585399516 +8014 4326 27443.60682483571 255031.9585399516 114.93450000000001 -22.100499999999993 +7844 8014 115.55550000000001 -20.3095 91906.92263784888 453335.7007890865 +8014 4326 91906.92263784888 453335.7007890865 115.55550000000001 -20.3095 +7844 8015 122.35 -17.42 51770.76860547363 373204.9705892424 +8015 4326 51770.76860547363 373204.9705892424 122.35 -17.42 +7844 8015 122.107 -18.023 26033.1594690485 306451.8262528246 +8015 4326 26033.1594690485 306451.8262528246 122.10700000000001 -18.022999999999996 +7844 8015 122.593 -16.817 77677.6774977568 439921.445997549 +8015 4326 77677.6774977568 439921.445997549 122.59300000000002 -16.817 +7844 8016 115.525 -33.575 58510.51369655677 283490.3425040613 +8016 4326 58510.51369655677 283490.3425040613 115.52500000000002 -33.574999999999996 +7844 8016 115.2145 -33.7325 29720.0421984437 266003.3789695124 +8016 4326 29720.0421984437 266003.3789695124 115.21449999999999 -33.7325 +7844 8016 115.83550000000001 -33.4175 87405.72963360423 300890.58408987336 +8016 4326 87405.72963360423 300890.58408987336 115.83550000000001 -33.41749999999999 +7844 8017 113.66499999999999 -24.25 49830.74327200439 367026.8111893707 +8017 4326 49830.74327200439 367026.8111893707 113.66499999999999 -24.25 +7844 8017 113.3635 -25.375 19488.813364081732 242376.08278136328 +8017 4326 19488.813364081732 242376.08278136328 113.3635 -25.375 +7844 8017 113.9665 -23.125 80711.33052014143 491592.97043461306 +8017 4326 80711.33052014143 491592.97043461306 113.9665 -23.125 +7844 8018 105.625 -10.495000000000001 50000.0 239364.3201551109 +8018 4326 50000.0 239364.3201551109 105.62500000000001 -10.495 +7844 8018 105.4945 -10.6165 35719.485026913644 225921.68287656526 +8018 4326 35719.485026913644 225921.68287656526 105.49450000000002 -10.6165 +7844 8018 105.7555 -10.3735 64291.66648895177 252800.92800173513 +8018 4326 64291.66648895177 252800.92800173513 105.7555 -10.3735 +7844 8019 96.875 -12.015 50000.0 271264.34040925326 +8019 4326 50000.0 271264.34040925326 96.875 -12.014999999999999 +7844 8019 96.7715 -12.2445 38738.89929323025 245874.28693367727 +8019 4326 38738.89929323025 245874.28693367727 96.7715 -12.2445 +7844 8019 96.9785 -11.785499999999999 61280.19415470499 296649.7398550897 +8019 4326 61280.19415470499 296649.7398550897 96.9785 -11.785499999999997 +7844 8020 116.065 -33.96 52169.93071709448 340696.2023526919 +8020 4326 52169.93071709448 340696.2023526919 116.06500000000001 -33.96 +7844 8020 115.76350000000001 -34.599000000000004 24420.66715579713 269806.916053012 +8020 4326 24420.66715579713 269806.916053012 115.7635 -34.599000000000004 +7844 8020 116.3665 -33.321 80334.5152017647 411496.8731972282 +8020 4326 80334.5152017647 411496.8731972282 116.36650000000002 -33.321 +7844 8021 121.88 -33.915 49691.74262734053 295746.2422318342 +8021 4326 49691.74262734053 295746.2422318342 121.88 -33.91499999999999 +7844 8021 121.592 -34.4415 23225.087009679086 237305.0919104484 +8021 4326 23225.087009679086 237305.0919104484 121.59199999999998 -34.441500000000005 +7844 8021 122.168 -33.3885 76486.00228929553 354107.6941802441 +8021 4326 76486.00228929553 354107.6941802441 122.16800000000002 -33.38849999999999 +7844 8022 114.0 -22.085 43119.94938493575 306780.1293811621 +8022 4326 43119.94938493575 306780.1293811621 114.00000000000001 -22.084999999999997 +7844 8022 113.775 -22.3865 19964.0043897283 273366.2981828754 +8022 4326 19964.0043897283 273366.2981828754 113.775 -22.3865 +7844 8022 114.225 -21.7835 66374.5947414219 340158.227604385 +8022 4326 66374.5947414219 340158.227604385 114.225 -21.783499999999997 +7844 8023 114.755 -28.79 66760.82811540275 263973.72297256347 +8023 4326 66760.82811540275 263973.72297256347 114.75499999999998 -28.789999999999992 +7844 8023 114.53450000000001 -29.069000000000003 45244.86664560678 233061.78236150416 +8023 4326 45244.86664560678 233061.78236150416 114.53450000000001 -29.068999999999996 +7844 8023 114.9755 -28.511 88391.15312146995 294844.870943516 +8023 4326 88391.15312146995 294844.870943516 114.9755 -28.511 +7844 8024 121.42 -30.5 52319.70451734229 424288.49023650866 +8024 4326 52319.70451734229 424288.49023650866 121.42 -30.499999999999986 +7844 8024 121.042 -32.075 16755.064127458725 249563.5229824446 +8024 4326 16755.064127458725 249563.5229824446 121.042 -32.074999999999996 +7844 8024 121.798 -28.925 89059.23879238927 598848.3462376283 +8024 4326 89059.23879238927 598848.3462376283 121.79800000000002 -28.92499999999999 +7844 8025 115.08500000000001 -29.909999999999997 59818.31126226454 339848.51634149393 +8025 4326 59818.31126226454 339848.51634149393 115.08500000000002 -29.90999999999999 +7844 8025 114.85549999999999 -30.657 37747.84896911859 257035.2708847914 +8025 4326 37747.84896911859 257035.2708847914 114.85549999999999 -30.657000000000004 +7844 8025 115.31450000000001 -29.162999999999997 82217.95930685548 422608.7213088032 +8025 4326 82217.95930685548 422608.7213088032 115.31450000000001 -29.162999999999997 +7844 8026 114.325 -27.83 55957.80592258246 620354.1742460937 +8026 4326 55957.80592258246 620354.1742460937 114.325 -27.82999999999999 +7844 8026 113.94250000000001 -28.433 18479.841645954126 553471.2971182945 +8026 4326 18479.841645954126 553471.2971182945 113.94250000000001 -28.433 +7844 8026 114.7075 -27.227 93852.22103208618 687113.8787259557 +8026 4326 93852.22103208618 687113.8787259557 114.7075 -27.226999999999997 +7844 8027 116.91499999999999 -20.585 48088.65915055942 272871.98169108434 +8027 4326 48088.65915055942 272871.98169108434 116.915 -20.584999999999994 +7844 8027 116.6135 -20.8865 16721.633315327075 239458.85711382888 +8027 4326 16721.633315327075 239458.85711382888 116.6135 -20.8865 +7844 8027 117.2165 -20.2835 79579.21452681327 306225.7058153432 +8027 4326 79579.21452681327 306225.7058153432 117.21649999999998 -20.2835 +7844 8028 128.75 -15.75 50000.0 357992.37645386904 +8028 4326 50000.0 357992.37645386904 128.75 -15.75 +7844 8028 128.525 -16.65 25996.213053105865 258382.53048776276 +8028 4326 25996.213053105865 258382.53048776276 128.52500000000003 -16.65 +7844 8028 128.975 -14.85 74216.10057910885 457568.3206593152 +8028 4326 74216.10057910885 457568.3206593152 128.975 -14.849999999999996 +7844 8029 115.385 -31.1 51749.08443727538 307933.0955977845 +8029 4326 51749.08443727538 307933.0955977845 115.385 -31.099999999999994 +7844 8029 115.1735 -31.450999999999997 31639.10958490129 269000.180986125 +8029 4326 31639.10958490129 269000.180986125 115.1735 -31.450999999999993 +7844 8029 115.5965 -30.749000000000002 72007.40807507106 346825.4609060171 +8029 4326 72007.40807507106 346825.4609060171 115.5965 -30.749 +7844 8030 115.1 -33.965 43838.45498596713 290198.14890363533 +8030 4326 43838.45498596713 290198.14890363533 115.1 -33.964999999999996 +7844 8030 114.97399999999999 -34.374500000000005 32278.91364343737 244759.10456161946 +8030 4326 32278.91364343737 244759.10456161946 114.97399999999999 -34.3745 +7844 8030 115.226 -33.555499999999995 55509.91398276949 335619.772046261 +8030 4326 55509.91398276949 335619.772046261 115.226 -33.555499999999995 +7844 8031 115.765 -32.375 45137.83363506339 316565.9737860821 +8031 4326 45137.83363506339 316565.9737860821 115.765 -32.37499999999999 +7844 8031 115.4725 -33.3155 17951.43236337875 212211.31170323258 +8031 4326 17951.43236337875 212211.31170323258 115.47250000000001 -33.31550000000001 +7844 8031 116.0575 -31.4345 72895.6704943908 420829.4426977611 +8031 4326 72895.6704943908 420829.4426977611 116.05749999999999 -31.4345 +7844 8032 118.61 -20.445 51043.50013562476 238366.0092649036 +8032 4326 51043.50013562476 238366.0092649036 118.61 -20.444999999999997 +7844 8032 118.286 -20.755499999999998 17300.257442140646 203958.20365920383 +8032 4326 17300.257442140646 203958.20365920383 118.286 -20.755499999999994 +7844 8032 118.934 -20.134500000000003 84922.5432700046 272705.8607336134 +8032 4326 84922.5432700046 272705.8607336134 118.934 -20.134500000000003 +4326 8035 -61.040000000000006 11.085 2342827.608571013 4022571.4017728874 +4326 8035 -61.98500000000001 9.9555 2005442.3516996808 3611091.2335769166 +4326 8035 -60.095 12.2145 2677512.955382191 4435586.073980995 +4326 8036 -58.64 11.07 1052687.1730773586 4016434.7318953983 +4326 8036 -59.864 10.062 610413.9436206332 3653664.5487653455 +4326 8036 -57.416000000000004 12.078 1491884.5328078847 4380611.76756239 +8042 8044 32.1166666666667 48.995000000000005 -106383.66628147729 -22858.2387389989 +8042 8044 29.9746666666667 47.1365 98630.46061260923 138723.07192291415 +8042 8044 34.2586666666667 50.853500000000004 -315862.43860056007 -172739.55657492243 +8043 8045 34.3016666666667 48.935 -80768.35348137293 -19096.270615571684 +8043 8045 32.2991666666667 47.5715 69365.95102724644 130988.75967534533 +8043 8045 36.3041666666667 50.298500000000004 -234791.59811433044 -161168.28655267548 +7844 8058 147.34 -32.84 9331801.963511344 4545376.744955079 +8058 4326 9331801.963511344 4545376.744955079 147.34 -32.84000000000001 +7844 8058 141.625 -37.061 8821546.029924097 4064972.0775362113 +8058 4326 8821546.029924097 4064972.0775362113 141.625 -37.060999999999986 +7844 8058 153.055 -28.619 9893154.747533873 4996304.810499958 +8058 4326 9893154.747533873 4996304.810499958 153.05500000000004 -28.618999999999996 +7844 8059 135.0 -32.06 1000000.0 1993362.8919968458 +8059 4326 1000000.0 1993362.8919968458 135.0 -32.06 +7844 8059 129.591 -37.523 520975.9977354317 1375844.7065937442 +8059 4326 520975.9977354317 1375844.7065937442 129.591 -37.52300000000002 +7844 8059 140.409 -26.597 1539604.9560378764 2584795.614858562 +8059 4326 1539604.9560378764 2584795.614858562 140.409 -26.596999999999998 +6318 8065 -111.005 31.975 282504.7980127173 700165.6387489444 +8065 4326 282504.7980127173 700165.6387489444 -111.00500000000001 31.97499999999999 +6318 8065 -111.5135 31.4845 124608.25671014556 521500.7911289913 +8065 4326 124608.25671014556 521500.7911289913 -111.5135 31.484499999999997 +6318 8065 -110.4965 32.465500000000006 438704.8880232261 879591.7080167155 +8065 4326 438704.8880232261 879591.7080167155 -110.4965 32.46549999999999 +6318 8066 -112.03999999999999 32.004999999999995 1839270.2204466 1274702.059091061 +8066 4326 1839270.2204466 1274702.059091061 -112.03999999999999 32.004999999999995 +6318 8066 -112.472 31.5505 1704873.8677613235 1109454.6348740002 +8066 4326 1704873.8677613235 1109454.6348740002 -112.472 31.550500000000007 +6318 8066 -111.608 32.4595 1972343.4477881347 1440498.919911626 +8066 4326 1972343.4477881347 1440498.919911626 -111.608 32.4595 +6318 8067 -112.92 32.155 676346.2044014641 238384.32797916365 +8067 4326 676346.2044014641 238384.32797916365 -112.92 32.155 +6318 8067 -113.289 31.8355 562004.9454274153 122077.25535676772 +8067 4326 562004.9454274153 122077.25535676772 -113.289 31.835499999999996 +6318 8067 -112.551 32.4745 789889.5604906781 355091.5384091238 +8067 4326 789889.5604906781 355091.5384091238 -112.551 32.4745 +6318 8068 -110.74000000000001 32.41 33087.39500245721 74792.2770971437 +8068 4326 33087.39500245721 74792.2770971437 -110.74000000000001 32.410000000000004 +6318 8068 -110.857 32.338 -3059.9586138357167 48603.72173324539 +8068 4326 -3059.9586138357167 48603.72173324539 -110.85700000000001 32.338000000000015 +6318 8068 -110.623 32.482 69180.4273155653 101019.66911172266 +8068 4326 69180.4273155653 101019.66911172266 -110.623 32.481999999999985 +8252 8082 -61.364999999999995 45.86 24510482.83996819 5080025.743433987 +8252 8082 -62.8365 44.762 24394196.346236415 4958868.739724381 +8252 8082 -59.893499999999996 46.958 24622266.37469238 5203310.505488707 +8252 8083 -64.64 44.715 25488907.966509584 4952786.822229502 +8252 8083 -66.116 43.540499999999994 25369402.828979086 4823554.948998742 +8252 8083 -63.164 45.889500000000005 25603686.139426794 5084163.479432224 +8086 8088 -18.21 64.775 2737577.9596630577 275151.4256193623 +8088 4326 2737577.9596630577 275151.4256193623 -18.210000000000004 64.775 +8086 8088 -29.604 60.4415 2117231.586074329 -159557.42222778517 +8088 4326 2117231.586074329 -159557.42222778517 -29.604 60.4415 +8086 8088 -6.8160000000000025 69.1085 3183352.8402514625 805282.0216167318 +8088 4326 3183352.8402514625 805282.0216167318 -6.816000000000008 69.1085 +4152 8090 -88.37 45.870000000000005 115772.8607114832 47943.66661035 +8090 4326 115772.8607114832 47943.66661035 -88.37000628557269 45.87000846940272 +4152 8090 -88.658 45.726 93306.61294903542 32041.887942218942 +8090 4326 93306.61294903542 32041.887942218942 -88.65800635500302 45.72600842942701 +4152 8090 -88.082 46.014 138123.73439436252 63926.55095605564 +8090 4326 138123.73439436252 63926.55095605564 -88.08200621580097 46.01400850929284 +4152 8091 -88.37 45.870000000000005 379831.4602700064 157295.17986820664 +8091 4326 379831.4602700064 157295.17986820664 -88.37000628557269 45.87000846940272 +4152 8091 -88.658 45.726 306123.4457360423 105124.09435451332 +8091 4326 306123.4457360423 105124.09435451332 -88.65800635500302 45.72600842942701 +4152 8091 -88.082 46.014 453160.95167791966 209732.35959240922 +8091 4326 453160.95167791966 209732.35959240922 -88.08200621580097 46.01400850929284 +4152 8092 -91.28999999999999 44.725 120003.41117131937 75320.2985839772 +8092 4326 120003.41117131937 75320.2985839772 -91.29000701370708 44.72500810902264 +4152 8092 -91.62299999999999 44.603500000000004 93565.63533400463 61872.58544759037 +8092 4326 93565.63533400463 61872.58544759037 -91.62300709565926 44.60350806891991 +4152 8092 -90.95700000000001 44.8465 146330.45585210953 88876.40879954014 +8092 4326 146330.45585210953 88876.40879954014 -90.9570069312979 44.84650814905524 +4152 8093 -91.28999999999999 44.725 393711.1914284025 247113.3462756819 +8093 4326 393711.1914284025 247113.3462756819 -91.29000701370708 44.72500810902264 +4152 8093 -91.62299999999999 44.603500000000004 306973.2552021457 202993.64076071943 +8093 4326 306973.2552021457 202993.64076071943 -91.62300709565926 44.60350806891991 +4152 8093 -90.95700000000001 44.8465 480085.83718529483 291588.68454124127 +8093 4326 480085.83718529483 291588.68454124127 -90.9570069312979 44.84650814905524 +4152 8095 -90.02 44.465 206892.0052426591 145969.65813851162 +8095 4326 206892.0052426591 145969.65813851162 -90.02000660701177 44.46500814946014 +4152 8095 -90.28999999999999 44.2625 185325.65484324246 123507.81440724643 +8095 4326 185325.65484324246 123507.81440724643 -90.29000666233294 44.26250810095192 +4152 8095 -89.75 44.6675 228310.0830855039 168503.277439127 +8095 4326 228310.0830855039 168503.277439127 -89.75000655148274 44.66750819787824 +4152 8096 -90.02 44.465 678778.1871085422 478902.1198277674 +8096 4326 678778.1871085422 478902.1198277674 -90.02000660701177 44.46500814946014 +4152 8096 -90.28999999999999 44.2625 608022.5858397895 405208.55418610823 +8096 4326 608022.5858397895 405208.55418610823 -90.29000666233294 44.26250810095192 +4152 8096 -89.75 44.6675 749047.3308312756 552831.1691498698 +8096 4326 749047.3308312756 552831.1691498698 -89.75000655148274 44.66750819787824 +4152 8097 -89.24 44.114999999999995 120224.86394850594 45187.096446580705 +8097 4326 120224.86394850594 45187.096446580705 -89.24000633372972 44.11500814206045 +4152 8097 -89.564 43.9935 94234.48371341742 31736.91825279137 +8097 4326 94234.48371341742 31736.91825279137 -89.56400641580836 43.99350810306515 +4152 8097 -88.916 44.2365 146108.95528541488 58739.65880112782 +8097 4326 146108.95528541488 58739.65880112782 -88.91600625123424 44.23650818098051 +4152 8098 -89.24 44.114999999999995 394437.74108155543 148251.3325902402 +8098 4326 394437.74108155543 148251.3325902402 -89.24000633372972 44.11500814206045 +4152 8098 -89.564 43.9935 309167.63526026916 104123.53963278302 +8098 4326 309167.63526026916 104123.53963278302 -89.56400641580836 43.99350810306515 +4152 8098 -88.916 44.2365 479359.1307427308 192715.03091511686 +8098 4326 479359.1307427308 192715.03091511686 -88.91600625123424 44.23650818098051 +4152 8099 -88.91499999999999 44.465 177188.62577816856 116088.84361179899 +8099 4326 177188.62577816856 116088.84361179899 -88.91500627774569 44.46500821735897 +4152 8099 -89.19850000000001 44.2625 154522.89713683596 93652.6335236477 +8099 4326 154522.89713683596 93652.6335236477 -89.19850633861888 44.26250816804665 +4152 8099 -88.63149999999999 44.6675 199698.46018319903 138604.04019553747 +8099 4326 199698.46018319903 138604.04019553747 -88.63150621661961 44.667508266569186 +4152 8100 -88.91499999999999 44.465 581326.3498794585 380868.1477838772 +8100 4326 581326.3498794585 380868.1477838772 -88.91500627774569 44.46500821735897 +4152 8100 -89.19850000000001 44.2625 506963.8718286866 307258.6818530009 +8100 4326 506963.8718286866 307258.6818530009 -89.19850633861888 44.26250816804665 +4152 8100 -88.63149999999999 44.6675 655177.364923296 454736.75524235924 +8100 4326 655177.364923296 454736.75524235924 -88.63150621661961 44.667508266569186 +4152 8101 -88.305 43.02 202267.0308394392 50056.49047637261 +8101 4326 202267.0308394392 50056.49047637261 -88.30500593390425 43.02000801858682 +4152 8101 -88.5255 42.858000000000004 184228.14811595134 32099.738536205317 +8101 4326 184228.14811595134 32099.738536205317 -88.52550598223058 42.858007979027356 +4152 8101 -88.0845 43.182 220211.44826269647 68060.93381792572 +8101 4326 220211.44826269647 68060.93381792572 -88.08450588543651 43.18200805808717 +4152 8102 -88.305 43.02 663604.416624061 164227.00234973247 +8102 4326 663604.416624061 164227.00234973247 -88.30500593390425 43.02000801858682 +4152 8102 -88.5255 42.858000000000004 604421.8488887512 105313.89202603363 +8102 4326 604421.8488887512 105313.89202603363 -88.52550598223058 42.858007979027356 +4152 8102 -88.0845 43.182 722477.0594535309 223296.5802128113 +8102 4326 722477.0594535309 223296.5802128113 -88.08450588543651 43.18200805808717 +4152 8103 -88.225 43.37 107032.89816574582 50223.50121082372 +8103 4326 107032.89816574582 50223.50121082372 -88.22500594754436 43.370008081140064 +4152 8103 -88.40050000000001 43.208 92735.57925815211 32267.301834735827 +8103 4326 92735.57925815211 32267.301834735827 -88.40050598237492 43.20800804429469 +4152 8103 -88.0495 43.532 121254.59573686423 68210.11434851718 +8103 4326 121254.59573686423 68210.11434851718 -88.0495059126593 43.53200811792214 +4152 8104 -88.225 43.37 351157.09974420175 164774.9370466775 +8104 4326 351157.09974420175 164774.9370466775 -88.22500594754436 43.370008081140064 +4152 8104 -88.40050000000001 43.208 304249.97929487145 105863.63959362914 +8104 4326 304249.97929487145 105863.63959362914 -88.40050598237492 43.20800804429469 +4152 8104 -88.0495 43.532 397816.11919211276 223786.0169825935 +8104 4326 397816.11919211276 223786.0169825935 -88.0495059126593 43.53200811792214 +4152 8105 -91.80000000000001 45.894999999999996 232793.31095490142 180998.06074234744 +8105 4326 232793.31095490142 180998.06074234744 -91.800007319343 45.895008249965386 +4152 8105 -92.034 45.6565 214548.4248304589 154518.6880060815 +8105 4326 214548.4248304589 154518.6880060815 -92.03400735525048 45.6565081992103 +4152 8105 -91.566 46.1335 250882.54385993723 207531.77122876202 +8105 4326 250882.54385993723 207531.77122876202 -91.56600728342724 46.133508300590705 +4152 8106 -91.80000000000001 45.894999999999996 763756.0542717048 593824.471423352 +8106 4326 763756.0542717048 593824.471423352 -91.800007319343 45.895008249965386 +4152 8106 -92.034 45.6565 703897.623711763 506950.06270445295 +8106 4326 703897.623711763 506950.06270445295 -92.03400735525048 45.6565081992103 +4152 8106 -91.566 46.1335 823103.8125609764 680877.1532441973 +8106 4326 823103.8125609764 680877.1532441973 -91.56600728342724 46.133508300590705 +4152 8107 -88.53999999999999 42.67 232699.49675116193 111147.9796036397 +8107 4326 232699.49675116193 111147.9796036397 -88.54000596682613 42.67000794677827 +4152 8107 -88.756 42.508 214946.4253669212 93173.8994645169 +8107 4326 214946.4253669212 93173.8994645169 -88.75600601377533 42.50800790735352 +4152 8107 -88.324 42.832 250360.59987751397 129167.81402406926 +8107 4326 250360.59987751397 129167.81402406926 -88.32400591974763 42.83200798614777 +4152 8108 -88.53999999999999 42.67 763448.2656755197 364657.9964256086 +8108 4326 763448.2656755197 364657.9964256086 -88.54000596682613 42.67000794677827 +4152 8108 -88.756 42.508 705203.3973090566 305688.03516916983 +8108 4326 705203.3973090566 305688.03516916983 -88.75600601377533 42.50800790735352 +4152 8108 -88.324 42.832 821391.401515893 423778.0698533013 +8108 4326 821391.401515893 423778.0698533013 -88.32400591974763 42.83200798614777 +4152 8109 -89.49000000000001 46.075 134331.10865659447 50020.95805712175 +8109 4326 134331.10865659447 50020.95805712175 -89.49000665304487 46.07500842936646 +4152 8109 -89.994 45.8725 95196.87110817048 27635.749981578272 +8109 4326 95196.87110817048 27635.749981578272 -89.9940067782025 45.87250836645091 +4152 8109 -88.986 46.277499999999996 173180.10594581225 72654.01472945852 +8109 4326 173180.10594581225 72654.01472945852 -88.98600652680572 46.277508492056484 +4152 8110 -89.49000000000001 46.075 440717.97876809473 164110.4267920735 +8110 4326 440717.97876809473 164110.4267920735 -89.49000665304487 46.07500842936646 +4152 8110 -89.994 45.8725 312325.06774464034 90668.28996422791 +8110 4326 312325.06774464034 90668.28996422791 -89.9940067782025 45.87250836645091 +4152 8110 -88.986 46.277499999999996 568175.0640411368 238365.71355789836 +8110 4326 568175.0640411368 238365.71355789836 -88.98600652680572 46.277508492056484 +4152 8111 -90.795 43.58 221562.09212049408 48084.00910444407 +8111 4326 221562.09212049408 48084.00910444407 -90.79500672947161 43.58000796193997 +4152 8111 -91.2315 43.436 186218.53958121262 32182.31673777935 +8111 4326 186218.53958121262 32182.31673777935 -91.2315068395598 43.436007912094205 +4152 8111 -90.3585 43.724000000000004 256737.6313897176 64171.2825725015 +8111 4326 256737.6313897176 64171.2825725015 -90.35850661863009 43.72400801167418 +4152 8112 -90.795 43.58 726908.296933071 157755.62036399692 +8112 4326 726908.296933071 157755.62036399692 -90.79500672947161 43.58000796193997 +4152 8112 -91.2315 43.436 610951.9916437784 105584.81799103109 +8112 4326 610951.9916437784 105584.81799103109 -91.2315068395598 43.436007912094205 +4152 8112 -90.3585 43.724000000000004 842313.3786855151 210535.2834004487 +8112 4326 842313.3786855151 210535.2834004487 -90.35850661863009 43.72400801167418 +4152 8113 -91.385 44.29 255483.59520380863 125432.032180537 +8113 4326 255483.59520380863 125432.032180537 -91.38500698687811 44.2900080361415 +4152 8113 -91.5965 44.010999999999996 238515.59507102403 94455.29718510677 +8113 4326 238515.59507102403 94455.29718510677 -91.59650701408675 44.01100797944581 +4152 8113 -91.1735 44.569 272291.7997750066 156453.7979659698 +8113 4326 272291.7997750066 156453.7979659698 -91.1735069598367 44.56900809264872 +4152 8114 -91.385 44.29 838199.0955723296 411521.59179422853 +8114 4326 838199.0955723296 411521.59179422853 -91.38500698687811 44.2900080361415 +4152 8114 -91.5965 44.010999999999996 782529.9151366854 309892.0870633878 +8114 4326 782529.9151366854 309892.0870633878 -91.59650701408675 44.01100797944581 +4152 8114 -91.1735 44.569 893344.0134030017 513298.8350419361 +8114 4326 893344.0134030017 513298.8350419361 -91.1735069598367 44.56900809264872 +4152 8115 -90.48500000000001 45.21 187016.6360814733 111323.07816826794 +8115 4326 187016.6360814733 111323.07816826794 -90.48500683820794 45.210008234809514 +4152 8115 -90.88550000000001 45.048 155462.64910328545 93397.10648608232 +8115 4326 155462.64910328545 93397.10648608232 -90.88550693599649 45.04800818435295 +4152 8115 -90.0845 45.372 218392.08744944903 129405.59628145487 +8115 4326 218392.08744944903 129405.59628145487 -90.0845067397541 45.372008285149484 +4152 8116 -90.48500000000001 45.21 613570.4131999684 365232.46544755815 +8116 4326 613570.4131999684 365232.46544755815 -90.48500683820794 45.210008234809514 +4152 8116 -90.88550000000001 45.048 510047.04092236375 306420.3400202542 +8116 4326 510047.04092236375 306420.3400202542 -90.88550693599649 45.04800818435295 +4152 8116 -90.0845 45.372 716508.0398964022 424558.1936239056 +8116 4326 716508.0398964022 424558.1936239056 -90.0845067397541 45.372008285149484 +4152 8117 -92.47 45.035 178377.69509159477 111016.46175525412 +8117 4326 178377.69509159477 111016.46175525412 -92.47000739839211 45.03500807804187 +4152 8117 -92.776 44.868500000000004 154231.78382915264 92509.39415938122 +8117 4326 154231.78382915264 92509.39415938122 -92.77600746486624 44.86850803274343 +4152 8117 -92.164 45.201499999999996 202383.64861606393 129615.18909583936 +8117 4326 202383.64861606393 129615.18909583936 -92.1640073315612 45.201508123284555 +4152 8118 -92.47 45.035 585227.4876485065 364226.5084827796 +8118 4326 585227.4876485065 364226.5084827796 -92.47000739839211 45.03500807804187 +4152 8118 -92.776 44.868500000000004 506008.7771149777 303507.90421198664 +8118 4326 506008.7771149777 303507.90421198664 -92.77600746486624 44.86850803274343 +4152 8118 -92.164 45.201499999999996 663987.0201700359 425245.83309934975 +8118 4326 663987.0201700359 425245.83309934975 -92.1640073315612 45.201508123284555 +4152 8119 -88.735 44.805 252192.15151972816 85450.19226465457 +8119 4326 252192.15151972816 85450.19226465457 -88.73500626422872 44.80500828194016 +4152 8119 -89.18050000000001 44.6025 216786.95039811463 63099.123138107054 +8119 4326 216786.95039811463 63099.123138107054 -89.18050637362052 44.60250822287778 +4152 8119 -88.28949999999999 45.0075 287350.8553946622 107995.17985522524 +8119 4326 287350.8553946622 107995.17985522524 -88.28950615404007 45.00750834083525 +4152 8120 -88.735 44.805 827400.4166892236 280347.8386256209 +8120 4326 827400.4166892236 280347.8386256209 -88.73500626422872 44.80500828194016 +4152 8120 -89.18050000000001 44.6025 711241.8526760632 207017.70599960626 +8120 4326 711241.8526760632 207017.70599960626 -89.18050637362052 44.60250822287778 +4152 8120 -88.28949999999999 45.0075 942750.264318903 354314.1854123515 +8120 4326 942750.264318903 354314.1854123515 -88.28950615404007 45.00750834083525 +4152 8121 -91.11500000000001 45.894999999999996 216842.5905086011 120167.3669777322 +8121 4326 216842.5905086011 120167.3669777322 -91.11500711643531 45.895008296019995 +4152 8121 -91.5155 45.6565 185625.73451730172 93734.92082876162 +8121 4326 185625.73451730172 93734.92082876162 -91.51550720257455 45.65650823407684 +4152 8121 -90.7145 46.1335 247793.29058794878 146756.94503121637 +8121 4326 247793.29058794878 146756.94503121637 -90.71450702969288 46.133508357817824 +4152 8122 -91.11500000000001 45.894999999999996 711424.3994576373 394249.10338885867 +8122 4326 711424.3994576373 394249.10338885867 -91.11500711643531 45.895008296019995 +4152 8122 -91.5155 45.6565 609007.0977595159 307528.6529817777 +8122 4326 609007.0977595159 307528.6529817777 -91.51550720257455 45.65650823407684 +4152 8122 -90.7145 46.1335 812968.4879679638 481485.077385998 +8122 4326 812968.4879679638 481485.077385998 -90.71450702969288 46.133508357817824 +4152 8123 -89.955 43.394999999999996 181167.48675613225 63945.10947191308 +8123 4326 181167.48675613225 63945.10947191308 -89.95500646247613 43.395007983430844 +4152 8123 -90.28349999999999 43.1655 154435.48878378325 38517.05542833977 +8123 4326 154435.48878378325 38517.05542833977 -90.28350653281836 43.16550792658719 +4152 8123 -89.62650000000001 43.6245 207698.669461132 89479.00787716228 +8123 4326 207698.669461132 89479.00787716228 -89.6265063918074 43.624508040172195 +4152 8124 -89.955 43.394999999999996 594380.3293080769 209793.2469268515 +8124 4326 594380.3293080769 209793.2469268515 -89.95500646247613 43.395007983430844 +4152 8124 -90.28349999999999 43.1655 506677.09929379524 126368.03961889475 +8124 4326 506677.09929379524 126368.03961889475 -90.28350653281836 43.16550792658719 +4152 8124 -89.62650000000001 43.6245 681424.717899397 293565.71194474 +8124 4326 681424.717899397 293565.71194474 -89.6265063918074 43.624508040172195 +4152 8125 -91.11 45.465 247156.9860412441 171760.49800407718 +8125 4326 247156.9860412441 171760.49800407718 -91.11000705694622 45.465008232524816 +4152 8125 -91.506 45.3075 216090.13882339618 154348.18281791292 +8125 4326 216090.13882339618 154348.18281791292 -91.50600715287607 45.307508182853276 +4152 8125 -90.714 45.6225 278051.3705382823 189325.8850733364 +8125 4326 278051.3705382823 189325.8850733364 -90.71400696035742 45.62250828208579 +4152 8126 -91.11 45.465 810880.8786885657 563517.5674302098 +8126 4326 810880.8786885657 563517.5674302098 -91.11000705694622 45.465008232524816 +4152 8126 -91.506 45.3075 708955.7307746763 506390.66335693595 +8126 4326 708955.7307746763 506390.66335693595 -91.50600715287607 45.307508182853276 +4152 8126 -90.714 45.6225 912240.2051592652 621146.6748399378 +8126 4326 912240.2051592652 621146.6748399378 -90.71400696035742 45.62250828208579 +4152 8127 -89.07 42.67 146486.4675905452 80596.89779002538 +8127 4326 146486.4675905452 80596.89779002538 -89.07000612318411 42.67000791652615 +4152 8127 -89.34 42.508 124295.28052012608 62635.31306669469 +8127 4326 124295.28052012608 62635.31306669469 -89.34000618542328 42.50800787400578 +4152 8127 -88.8 42.832 168562.69272600365 98629.68853114081 +8127 4326 168562.69272600365 98629.68853114081 -88.80000606069838 42.83200795899224 +4152 8128 -89.07 42.67 480597.68578147975 264424.9885231083 +8128 4326 480597.68578147975 264424.9885231083 -89.07000612318411 42.67000791652615 +4152 8128 -89.34 42.508 407792.099534613 205496.02264331418 +8128 4326 407792.099534613 205496.02264331418 -89.34000618542328 42.50800787400578 +4152 8128 -88.8 42.832 553026.101080063 323587.5694795845 +8128 4326 553026.101080063 323587.5694795845 -88.80000606069838 42.83200795899224 +4152 8129 -90.435 43.36 202027.3098280338 138442.60308798568 +8129 4326 202027.3098280338 138442.60308798568 -90.43500659905389 43.36000794880495 +4152 8129 -90.6555 43.18 184098.32893153312 118468.81851521392 +8129 4326 184098.32893153312 118468.81851521392 -90.65550664287527 43.18000790647151 +4152 8129 -90.2145 43.54 219850.6828738155 158464.26436310308 +8129 4326 219850.6828738155 158464.26436310308 -90.21450655512234 43.540007991077964 +4152 8130 -90.435 43.36 662817.9322461424 454207.10745941673 +8130 4326 662817.9322461424 454207.10745941673 -90.43500659905389 43.36000794880495 +4152 8130 -90.6555 43.18 603995.9340882065 388676.44924024807 +8130 4326 603995.9340882065 388676.44924024807 -90.65550664287527 43.18000790647151 +4152 8130 -90.2145 43.54 721293.4486471779 519894.841159531 +8130 4326 721293.4486471779 519894.841159531 -90.21450655512234 43.540007991077964 +4152 8131 -90.36000000000001 45.68 238033.0642163163 124980.30709108707 +8131 4326 238033.0642163163 124980.30709108707 -90.36000686238403 45.68000831382217 +4152 8131 -90.64800000000001 45.400999999999996 215532.45422454795 93973.81615128319 +8131 4326 215532.45422454795 93973.81615128319 -90.64800691146957 45.401008253153876 +4152 8131 -90.072 45.959 260310.3773565128 156068.99629324913 +8131 4326 260310.3773565128 156068.99629324913 -90.07200681321011 45.959008374298534 +4152 8132 -90.36000000000001 45.68 780946.8110495314 410039.55775359157 +8132 4326 780946.8110495314 410039.55775359157 -90.36000686238403 45.68000831382217 +4152 8132 -90.64800000000001 45.400999999999996 707126.0597682047 308312.42872858496 +8132 4326 707126.0597682047 308312.42872858496 -90.64800691146957 45.401008253153876 +4152 8132 -90.072 45.959 854034.9625769928 512036.36557768495 +8132 4326 854034.9625769928 512036.36557768495 -90.07200681321011 45.959008374298534 +4152 8133 -89.535 44.465 53602.80129683441 55376.358190904946 +8133 4326 53602.80129683441 55376.358190904946 -89.535006462936 44.4650081794858 +4152 8133 -89.8185 44.2625 30954.2950551315 32922.65061408731 +8133 4326 30954.2950551315 32922.65061408731 -89.81850652293075 44.2625081301501 +4152 8133 -89.2515 44.6675 76095.4867006141 77909.12851157493 +8133 4326 76095.4867006141 77909.12851157493 -89.25150640269216 44.66750822872585 +4152 8134 -89.535 44.465 175861.8571766974 181680.60200316063 +8134 4326 175861.8571766974 181680.60200316063 -89.535006462936 44.4650081794858 +4152 8134 -89.8185 44.2625 101555.88294871042 108013.72972821812 +8134 4326 101555.88294871042 108013.72972821812 -89.81850652293075 44.2625081301501 +4152 8134 -89.2515 44.6675 249656.6092055979 255606.86596355875 +8134 4326 249656.6092055979 255606.86596355875 -89.25150640269216 44.66750822872585 +4152 8135 -92.52000000000001 45.465 150596.0670698155 89348.72252863385 +8135 4326 150596.0670698155 89348.72252863385 -92.52000747111114 45.46500813816727 +4152 8135 -92.853 45.2265 124479.88777953551 62858.260965098765 +8135 4326 124479.88777953551 62858.260965098765 -92.85300753526255 45.22650808050997 +4152 8135 -92.18700000000001 45.7035 176492.44351015982 115948.1804730309 +8135 4326 176492.44351015982 115948.1804730309 -92.18700740663388 45.70350819571958 +4152 8136 -92.52000000000001 45.465 494080.5965323048 293138.2668057763 +8136 4326 494080.5965323048 293138.2668057763 -92.52000747111114 45.46500813816727 +4152 8136 -92.853 45.2265 408397.7649774445 206227.4774927449 +8136 4326 408397.7649774445 206227.4774927449 -92.85300753526255 45.22650808050997 +4152 8136 -92.18700000000001 45.7035 579042.2915703345 380406.65507835225 +8136 4326 579042.2915703345 380406.65507835225 -92.18700740663388 45.70350819571958 +4152 8137 -92.23 44.635 167464.0049459691 85905.41736851723 +8137 4326 167464.0049459691 85905.41736851723 -92.23000727626841 44.63500803402172 +4152 8137 -92.75200000000001 44.4235 125893.18021329632 62536.266380278954 +8137 4326 125893.18021329632 62536.266380278954 -92.75200739976162 44.423507967627074 +4152 8137 -91.708 44.8465 208733.93714367726 109540.58340938539 +8137 4326 208733.93714367726 109540.58340938539 -91.70800715165757 44.84650810027066 +4152 8138 -92.23 44.635 549421.489168735 281841.35724887706 +8138 4326 549421.489168735 281841.35724887706 -92.23000727626841 44.63500803402172 +4152 8138 -92.75200000000001 44.4235 413034.54169162444 205171.06771496532 +8138 4326 413034.54169162444 205171.06771496532 -92.75200739976162 44.423507967627074 +4152 8138 -91.708 44.8465 684821.2583873826 359384.3978346254 +8138 4326 684821.2583873826 359384.3978346254 -91.70800715165757 44.84650810027066 +4152 8139 -89.545 45.685 70060.8584041478 55451.25522176298 +8139 4326 70060.8584041478 55451.25522176298 -89.54500661835587 45.685008367134046 +4152 8139 -89.9995 45.4825 34524.345178017575 33044.28136738057 +8139 4326 34524.345178017575 33044.28136738057 -89.99950672862151 45.48250830736469 +4152 8139 -89.0905 45.887499999999996 105341.78346369747 78060.04386742058 +8139 4326 105341.78346369747 78060.04386742058 -89.09050650722986 45.88750842672566 +4152 8140 -89.545 45.685 229857.99996952486 181926.32698990076 +8140 4326 229857.99996952486 181926.32698990076 -89.54500661835587 45.685008367134046 +4152 8140 -89.9995 45.4825 113268.62282679594 108412.78026931446 +8140 4326 113268.62282679594 108412.78026931446 -89.99950672862151 45.48250830736469 +4152 8140 -89.0905 45.887499999999996 345608.83493573073 256101.9944048624 +8140 4326 345608.83493573073 256101.9944048624 -89.09050650722986 45.88750842672566 +4152 8141 -88.225 45.025000000000006 157922.47211109192 69812.96262260486 +8141 4326 157922.47211109192 69812.96262260486 -88.22500613659611 45.02500834749267 +4152 8141 -88.6435 44.7055 124617.2335173268 34520.52394219011 +8141 4326 124617.2335173268 34520.52394219011 -88.64350622475806 44.70550827184038 +4152 8141 -87.8065 45.344500000000004 190861.53189384157 105277.81500574289 +8141 4326 190861.53189384157 105277.81500574289 -87.80650604790725 45.344508422863605 +4152 8142 -88.225 45.025000000000006 518117.31055014016 229044.69504424612 +8142 4326 518117.31055014016 229044.69504424612 -88.22500613659611 45.02500834749267 +4152 8142 -88.6435 44.7055 408848.3735970957 113256.08580691875 +8142 4326 408848.3735970957 113256.08580691875 -88.64350622475806 44.70550827184038 +4152 8142 -87.8065 45.344500000000004 626184.8758540447 345398.9649045915 +8142 4326 626184.8758540447 345398.9649045915 -87.80650604790725 45.344508422863605 +4152 8143 -90.64500000000001 43.945 204253.59643231475 115804.36675757566 +8143 4326 204253.59643231475 115804.36675757566 -90.64500672864673 43.94500802897659 +4152 8143 -90.9465 43.7425 179964.93694112342 93349.05616159303 +8143 4326 179964.93694112342 93349.05616159303 -90.94650679272749 43.742507978299194 +4152 8143 -90.3435 44.1475 228377.75863512588 138348.88975844346 +8143 4326 228377.75863512588 138348.88975844346 -90.34350666427639 44.14750807957498 +4152 8144 -90.64500000000001 43.945 670122.0077675191 379934.82687406166 +8144 4326 670122.0077675191 379934.82687406166 -90.64500672864673 43.94500802897659 +4152 8144 -90.9465 43.7425 590434.9640868355 306262.6953604087 +8144 4326 590434.9640868355 306262.6953604087 -90.94650679272749 43.742507978299194 +4152 8144 -90.3435 44.1475 749269.3632612418 453899.6494194088 +8144 4326 749269.3632612418 453899.6494194088 -90.34350666427639 44.14750807957498 +4152 8145 -88.735 44.985 80353.9777629856 29869.96780132863 +8145 4326 80353.9777629856 29869.96780132863 -88.7350062860124 44.98500831014266 +4152 8145 -88.9645 44.8635 62162.07644156228 16463.866525918686 +8145 4326 62162.07644156228 16463.866525918686 -88.96450634035358 44.86350827706361 +4152 8145 -88.5055 45.1065 98469.5192258494 43327.610591040524 +8145 4326 98469.5192258494 43327.610591040524 -88.50550623146498 45.10650834317221 +4152 8146 -88.735 44.985 263628.00884597766 97998.3865137757 +8146 4326 263628.00884597766 97998.3865137757 -88.7350062860124 44.98500831014266 +4152 8146 -88.9645 44.8635 203943.41259394132 54015.202579368226 +8146 4326 203943.41259394132 54015.202579368226 -88.96450634035358 44.86350827706361 +4152 8146 -88.5055 45.1065 323062.08112872334 142150.66956635547 +8146 4326 323062.08112872334 142150.66956635547 -88.50550623146498 45.10650834317221 +4152 8147 -87.95500000000001 45.379999999999995 219556.09802063712 76526.9285989131 +8147 4326 219556.09802063712 76526.9285989131 -87.95500609757708 45.380008419393235 +4152 8147 -88.38250000000001 45.002 185722.5862566855 34707.10316846787 +8147 4326 185722.5862566855 34707.10316846787 -88.38250618154478 45.00200833432461 +4152 8147 -87.5275 45.757999999999996 252944.2158813607 118527.38996109835 +8147 4326 252944.2158813607 118527.38996109835 -87.52750601319434 45.75800850405487 +4152 8148 -87.95500000000001 45.379999999999995 720326.9650912074 251072.09774626742 +8148 4326 720326.9650912074 251072.09774626742 -87.95500609757708 45.380008419393235 +4152 8148 -88.38250000000001 45.002 609324.8519123094 113868.22047988168 +8148 4326 609324.8519123094 113868.22047988168 -88.38250618154478 45.00200833432461 +4152 8148 -87.5275 45.757999999999996 829867.8151059314 388868.61139870354 +8148 4326 829867.8151059314 388868.61139870354 -87.52750601319434 45.75800850405487 +4152 8149 -89.77 44.905 74676.1493 55504.43207077539 +8149 4326 74676.1493 55504.43207077539 -89.770006586711 44.90500823350796 +4152 8149 -90.26499999999999 44.7025 35443.442159860344 33119.40919742037 +8149 4326 35443.442159860344 33119.40919742037 -90.26500670875615 44.70250817103648 +4152 8149 -89.275 45.1075 113634.36897393092 78128.68215159154 +8149 4326 113634.36897393092 78128.68215159154 -89.27500646366481 45.10750829579519 +4152 8150 -89.77 44.905 245000.0 182100.79106445223 +8150 4326 245000.0 182100.79106445223 -89.770006586711 44.90500823350796 +4152 8150 -90.26499999999999 44.7025 116284.02665772513 108659.26185411998 +8150 4326 116284.02665772513 108659.26185411998 -90.26500670875615 44.70250817103648 +4152 8150 -89.275 45.1075 372815.4257135551 256327.18487126322 +8150 4326 372815.4257135551 256327.18487126322 -89.27500646366481 45.10750829579519 +4152 8151 -89.735 45.335 115998.38110218298 54520.44370781562 +8151 4326 115998.38110218298 54520.44370781562 -89.73500663039074 45.33500830181294 +4152 8151 -90.0185 45.1325 93695.0823234387 32053.552017911992 +8151 4326 93695.0823234387 32053.552017911992 -90.01850668940753 45.132508252834754 +4152 8151 -89.4515 45.5375 138143.269801968 77066.34440334715 +8151 4326 138143.269801968 77066.34440334715 -89.45150657111803 45.53750835068806 +4152 8152 -89.735 45.335 380571.3551951636 178872.48903589175 +8152 4326 380571.3551951636 178872.48903589175 -89.73500663039074 45.33500830181294 +4152 8152 -90.0185 45.1325 307397.94911856676 105162.36188326628 +8152 4326 307397.94911856676 105162.36188326628 -90.01850668940753 45.132508252834754 +4152 8152 -89.4515 45.5375 453225.044204375 252841.8315678148 +8152 4326 453225.044204375 252841.8315678148 -89.45150657111803 45.53750835068806 +4152 8153 -89.03 45.25 198686.89111147964 115923.13951664549 +8153 4326 198686.89111147964 115923.13951664549 -89.03000640765812 45.25000833308452 +4152 8153 -89.39 45.043000000000006 170322.4780794877 92978.8252964652 +8153 4326 170322.4780794877 92978.8252964652 -89.39000649017191 45.043008278641054 +4152 8153 -88.67 45.456999999999994 226846.31684581377 138994.32779111413 +8153 4326 226846.31684581377 138994.32779111413 -88.67000632464843 45.45700838739652 +4152 8154 -89.03 45.25 651858.5747640805 380324.5001664452 +8154 4326 651858.5747640805 380324.5001664452 -89.03000640765812 45.25000833308452 +4152 8154 -89.39 45.043000000000006 558799.6630082869 305048.0292624037 +8154 4326 558799.6630082869 305048.0292624037 -89.39000649017191 45.043008278641054 +4152 8154 -88.67 45.456999999999994 744244.9573608083 456017.22369693115 +8154 4326 744244.9573608083 456017.22369693115 -88.67000632464843 45.45700838739652 +4152 8155 -91.17 43.91 142236.37983530728 50997.40492583605 +8155 4326 142236.37983530728 50997.40492583605 -91.17000687783293 43.910007990667005 +4152 8155 -91.40400000000001 43.739 123419.12703486366 31990.438486454506 +8155 4326 123419.12703486366 31990.438486454506 -91.40400692535874 43.73900794915378 +4152 8155 -90.93599999999999 44.081 160945.89902043613 70058.19873105803 +8155 4326 160945.89902043613 70058.19873105803 -90.93600683015303 44.0810080321284 +4152 8156 -91.17 43.91 466653.8558158361 167313.98616743047 +8156 4326 466653.8558158361 167313.98616743047 -91.17000687783293 43.910007990667005 +4152 8156 -91.40400000000001 43.739 404917.5855863807 104955.29710755951 +8156 4326 404917.5855863807 104955.29710755951 -91.40400692535874 43.73900794915378 +4152 8156 -90.93599999999999 44.081 528036.6700090464 229849.27384339625 +8156 4326 528036.6700090464 229849.27384339625 -90.93600683015303 44.0810080321284 +4152 8157 -87.77000000000001 44.11 62244.61920498541 93725.25532570129 +8157 4326 62244.61920498541 93725.25532570129 -87.77000589252677 44.110008228509244 +4152 8157 -88.13 43.597 33023.31574282605 36864.742114742636 +8157 4326 33023.31574282605 36864.742114742636 -88.13000594385805 43.59700812394714 +4152 8157 -87.41000000000001 44.623 90968.72564780439 150716.83853008063 +8157 4326 90968.72564780439 150716.83853008063 -87.41000584168607 44.6230083322833 +4152 8158 -87.77000000000001 44.11 204214.22198185636 307496.94191073836 +8158 4326 204214.22198185636 307496.94191073836 -87.77000589252677 44.110008228509244 +4152 8158 -88.13 43.597 108343.9955397552 120947.07481778481 +8158 4326 108343.9955397552 120947.07481778481 -88.13000594385805 43.59700812394714 +4152 8158 -87.41000000000001 44.623 298453.22786967165 494476.82780710625 +8158 4326 298453.22786967165 494476.82780710625 -87.41000584168607 44.6230083322833 +4152 8159 -88.03 43.019999999999996 174878.3385872995 89249.77410380432 +8159 4326 174878.3385872995 89249.77410380432 -88.03000585203436 43.02000803421955 +4152 8159 -88.282 42.543 154092.67492351195 36323.07047178617 +8159 4326 154092.67492351195 36323.07047178617 -88.28200587733852 42.543007939982026 +4152 8159 -87.778 43.497 195346.72479043846 142242.4634264023 +8159 4326 195346.72479043846 142242.4634264023 -87.77800582748642 43.49700812773744 +4152 8160 -88.03 43.019999999999996 573746.6824205001 292813.63391948136 +8160 4326 573746.6824205001 292813.63391948136 -88.03000585203436 43.02000803421955 +4152 8160 -88.282 42.543 505552.3842168903 119169.94042010182 +8160 4326 505552.3842168903 119169.94042010182 -88.28200587733852 42.543007939982026 +4152 8160 -87.778 43.497 640900.0461552984 466673.8154720383 +8160 4326 640900.0461552984 466673.8154720383 -87.77800582748642 43.49700812773744 +4152 8161 -90.74000000000001 44.335 35318.308969897436 34080.0475169196 +8161 4326 35318.308969897436 34080.0475169196 -90.74000680360865 44.33500808406871 +4152 8161 -91.127 44.0965 4361.4112038700805 7611.4113214011195 +8161 4326 4361.4112038700805 7611.4113214011195 -91.1270068878438 44.09650802251446 +4152 8161 -90.35300000000001 44.5735 66024.86263987594 60695.50411638409 +8161 4326 66024.86263987594 60695.50411638409 -90.35300671885128 44.573508145504064 +4152 8162 -90.74000000000001 44.335 115873.48534540519 111810.95556176042 +8162 4326 115873.48534540519 111810.95556176042 -90.74000680360865 44.33500808406871 +4152 8162 -91.127 44.0965 14309.063258030425 24971.77164363019 +8162 4326 14309.063258030425 24971.77164363019 -91.1270068878438 44.09650802251446 +4152 8162 -90.35300000000001 44.5735 216616.57017765965 199131.83275517015 +8162 4326 216616.57017765965 199131.83275517015 -90.35300671885128 44.573508145504064 +4152 8163 -90.24000000000001 46.29 222179.19700080663 95223.887661356 +8163 4326 222179.19700080663 95223.887661356 -90.24000690872343 46.29000841231023 +4152 8163 -90.528 46.010999999999996 199878.7527957133 64245.72792822126 +8163 4326 199878.7527957133 64245.72792822126 -90.52800695709085 46.01100835210695 +4152 8163 -89.952 46.569 244254.229839873 126284.20104371077 +8163 4326 244254.229839873 126284.20104371077 -89.95200686026575 46.569008472311225 +4152 8164 -90.24000000000001 46.29 728932.9156932301 312413.7048818822 +8164 4326 728932.9156932301 312413.7048818822 -90.24000690872343 46.29000841231023 +4152 8164 -90.528 46.010999999999996 655768.8749970198 210779.52582408927 +8164 4326 655768.8749970198 210779.52582408927 -90.52800695709085 46.01100835210695 +4152 8164 -89.952 46.569 801357.419266067 414317.41637049115 +8164 4326 801357.419266067 414317.41637049115 -89.95200686026575 46.569008472311225 +4152 8165 -90.13 43.010000000000005 115617.54495897249 52337.49425961594 +8165 4326 115617.54495897249 52337.49425961594 -90.13000647067248 43.010007910519946 +4152 8165 -90.4 42.830000000000005 93547.40326518498 32367.506472060628 +8165 4326 93547.40326518498 32367.506472060628 -90.40000652950191 42.8300078650837 +4152 8165 -89.86 43.19 137559.10283822444 72378.88023279075 +8165 4326 137559.10283822444 72378.88023279075 -89.86000641161526 43.19000795589806 +4152 8166 -90.13 43.010000000000005 379321.8956231446 171710.59598633996 +8166 4326 379321.8956231446 171710.59598633996 -90.13000647067248 43.010007910519946 +4152 8166 -90.4 42.830000000000005 306913.4390827767 106192.39438666892 +8166 4326 306913.4390827767 106192.39438666892 -90.40000652950191 42.8300078650837 +4152 8166 -89.86 43.19 451308.490098657 237463.04313333103 +8166 4326 451308.490098657 237463.04313333103 -89.86000641161526 43.19000795589806 +4152 8167 -89.24 43.81 151010.4092544449 79504.10474308397 +8167 4326 151010.4092544449 79504.10474308397 -89.24000629856047 43.81000809320857 +4152 8167 -89.564 43.648 124869.88785109254 61554.7742651461 +8167 4326 124869.88785109254 61554.7742651461 -89.5640063760276 43.64800804770935 +4152 8167 -88.916 43.972 177010.00604817714 97555.99950475246 +8167 4326 177010.00604817714 97555.99950475246 -88.91600622069416 43.972008138631395 +4152 8168 -89.24 43.81 495439.9842067898 260839.71656835152 +8168 4326 495439.9842067898 260839.71656835152 -89.24000629856047 43.81000809320857 +4152 8168 -89.564 43.648 409677.2902359579 201950.95482531702 +8168 4326 409677.2902359579 201950.95482531702 -89.5640063760276 43.64800804770935 +4152 8168 -88.916 43.972 580740.3280208929 320064.9746322589 +8168 4326 580740.3280208929 320064.9746322589 -88.91600622069416 43.972008138631395 +4152 8169 -89.89500000000001 42.68 165479.5338558115 50546.245446923145 +8169 4326 165479.5338558115 50546.245446923145 -89.89500636604862 42.68000787031583 +4152 8169 -90.37650000000001 42.518 125898.79200794577 32688.64687273291 +8169 4326 125898.79200794577 32688.64687273291 -90.37650648872311 42.51800781532479 +4152 8169 -89.4135 42.842 204855.53842466595 68629.18605840289 +8169 4326 204855.53842466595 68629.18605840289 -89.41350624249216 42.842007925178216 +4152 8170 -89.89500000000001 42.68 542910.7701910238 165833.80740886374 +8170 4326 542910.7701910238 165833.80740886374 -89.89500636604862 42.68000787031583 +4152 8170 -90.37650000000001 42.518 413052.9529784844 107246.0027533746 +8170 4326 413052.9529784844 107246.0027533746 -90.37650648872311 42.51800781532479 +4152 8170 -89.4135 42.842 672096.8785140072 225160.92173169355 +8170 4326 672096.8785140072 225160.92173169355 -89.41350624249216 42.842007925178216 +4152 8171 -90.78999999999999 42.86 243133.7711769257 160942.35308115307 +8171 4326 243133.7711769257 160942.35308115307 -90.79000664601593 42.86000784661244 +4152 8171 -91.12299999999999 42.536 215780.49940160464 124999.47291851623 +8171 4326 215780.49940160464 124999.47291851623 -91.1230067064124 42.53600777390138 +4152 8171 -90.45700000000001 43.184 270202.42030199757 196994.86989571538 +8171 4326 270202.42030199757 196994.86989571538 -90.45700658551934 43.184007919127176 +4152 8172 -90.78999999999999 42.86 797681.3806848802 528025.0369254164 +8172 4326 797681.3806848802 528025.0369254164 -90.79000664601593 42.86000784661244 +4152 8172 -91.12299999999999 42.536 707939.854868681 410102.4375918321 +8172 4326 707939.854868681 410102.4375918321 -91.1230067064124 42.53600777390138 +4152 8172 -90.45700000000001 43.184 886489.1070227203 646307.335841193 +8172 4326 886489.1070227203 646307.335841193 -90.45700658551934 43.184007919127176 +4152 8173 -88.735 45.724999999999994 267929.66134175326 191098.29571486198 +8173 4326 267929.66134175326 191098.29571486198 -88.73500637829216 45.725008424454494 +4152 8173 -89.0185 45.405499999999996 245688.3282205834 155652.9252145825 +8173 4326 245688.3282205834 155652.9252145825 -89.01850642368237 45.4055083577611 +4152 8173 -88.4515 46.0445 289919.61357178347 226623.86266139807 +8173 4326 289919.61357178347 226623.86266139807 -88.45150633292684 46.04450849085597 +4152 8177 -88.735 45.724999999999994 879032.5636336508 626961.6590154264 +8177 4326 879032.5636336508 626961.6590154264 -88.73500637829216 45.725008424454494 +4152 8177 -89.0185 45.405499999999996 806062.456551946 510671.3059657595 +8177 4326 806062.456551946 510671.3059657595 -89.01850642368237 45.4055083577611 +4152 8177 -88.4515 46.0445 951177.9319083416 743515.1232391869 +8177 4326 951177.9319083416 743515.1232391869 -88.45150633292684 46.04450849085597 +4152 8179 -91.9 44.945 51377.62916544574 59639.79493893713 +8179 4326 51377.62916544574 59639.79493893713 -91.90000722055694 44.94500810249406 +4152 8179 -92.134 44.7065 32830.94623954667 33162.49576190656 +8179 4326 32830.94623954667 33162.49576190656 -92.13400725765253 44.70650805114093 +4152 8179 -91.666 45.1835 69771.37572553608 86171.48761657182 +8179 4326 69771.37572553608 86171.48761657182 -91.66600718345147 45.18350815372784 +4152 8180 -91.9 44.945 168561.43814696668 195668.22738632958 +8180 4326 168561.43814696668 195668.22738632958 -91.90000722055694 44.94500810249406 +4152 8180 -92.134 44.7065 107712.8625809128 108800.62166968847 +8180 4326 107712.8625809128 108800.62166968847 -92.13400725765253 44.70650805114093 +4152 8180 -91.666 45.1835 228908.2549861964 282714.2891128694 +8180 4326 228908.2549861964 282714.2891128694 -91.66600718345147 45.18350815372784 +4152 8181 -91.925 46.455 58491.077802991575 63545.879606245966 +8181 4326 58491.077802991575 63545.879606245966 -91.92500743571122 46.455008322396246 +4152 8181 -92.2625 46.180499999999995 32428.148811234645 33090.07670866631 +8181 4326 32428.148811234645 33090.07670866631 -92.26250749596359 46.18050825970519 +4152 8181 -91.58749999999999 46.7295 84293.44880820067 94113.86205887252 +8181 4326 84293.44880820067 94113.86205887252 -91.58750737519583 46.7295083849191 +4152 8182 -91.925 46.455 191899.4776360648 208483.439556742 +8182 4326 191899.4776360648 208483.439556742 -91.92500743571122 46.455008322396246 +4152 8182 -92.2625 46.180499999999995 106391.35143560893 108563.02621693273 +8182 4326 106391.35143560893 108563.02621693273 -92.26250749596359 46.18050825970519 +4152 8182 -91.58749999999999 46.7295 276552.7565089883 308771.8953200676 +8182 4326 276552.7565089883 308771.8953200676 -91.58750737519583 46.7295083849191 +4152 8184 -87.27 45.05 158976.1836141684 72233.52169345116 +8184 4326 158976.1836141684 72233.52169345116 -87.27000584862928 45.050008408615554 +4152 8184 -87.693 44.708 125455.41301779557 34312.687284688676 +8184 4326 125455.41301779557 34312.687284688676 -87.69300593756955 44.708008329203 +4152 8184 -86.847 45.392 192099.76266700553 110330.7460366925 +8184 4326 192099.76266700553 110330.7460366925 -86.84700575917857 45.39200848768329 +4152 8185 -87.27 45.05 521574.3624148169 236986.14621001438 +8185 4326 521574.3624148169 236986.14621001438 -87.27000584862928 45.050008408615554 +4152 8185 -87.693 44.708 411598.300883217 112574.20865393277 +8185 4326 411598.300883217 112574.20865393277 -87.69300593756955 44.708008329203 +4152 8185 -86.847 45.392 630247.3046906667 361976.78974279866 +8185 4326 630247.3046906667 361976.78974279866 -86.84700575917857 45.39200848768329 +4152 8187 -88.71000000000001 43.24 268627.396803292 196373.992331028 +8187 4326 268627.396803292 196373.992331028 -88.71000607780381 43.240008031690714 +4152 8187 -88.98899999999999 42.88 245863.43102127124 156398.96784185743 +8187 4326 245863.43102127124 156398.96784185743 -88.98900612160251 42.88000795608311 +4152 8187 -88.43100000000001 43.6 291124.31489684095 236427.16268499364 +8187 4326 291124.31489684095 236427.16268499364 -88.43100603417194 43.60000810695637 +4152 8189 -88.71000000000001 43.24 881321.717976218 644270.3399050478 +8189 4326 881321.717976218 644270.3399050478 -88.71000607780381 43.240008031690714 +4152 8189 -88.98899999999999 42.88 806636.9402397049 513118.94706016063 +8189 4326 806636.9402397049 513118.94706016063 -88.98900612160251 42.88000795608311 +4152 8189 -88.43100000000001 43.6 955130.3567548032 775678.1163080167 +8189 4326 955130.3567548032 775678.1163080167 -88.43100603417194 43.60000810695637 +4152 8191 -89.42 43.07 247374.2976802199 146645.75944198362 +8191 4326 247374.2976802199 146645.75944198362 -89.42000626911643 43.07000796231751 +4152 8191 -89.798 42.863 216482.74474507145 123717.53386590895 +8191 4326 216482.74474507145 123717.53386590895 -89.79800635749234 42.86300790612821 +4152 8191 -89.042 43.277 278058.6303939868 169713.52504772405 +8191 4326 278058.6303939868 169713.52504772405 -89.0420061802279 43.277008018410015 +4152 8193 -89.42 43.07 811593.841595189 481120.2958899071 +8193 4326 811593.841595189 481120.2958899071 -89.42000626911643 43.07000796231751 +4152 8193 -89.798 42.863 710243.8050071227 405896.60914573545 +8193 4326 710243.8050071227 405896.60914573545 -89.79800635749234 42.86300790612821 +4152 8193 -89.042 43.277 912264.023173606 556801.7902147406 +8193 4326 912264.023173606 556801.7902147406 -89.0420061802279 43.277008018410015 +4152 8196 -90.94 43.205 113600.32495609725 54252.44576377582 +8196 4326 113600.32495609725 54252.44576377582 -90.94000672837129 43.2050078931741 +4152 8196 -91.192 43.0025 93051.72798609662 31786.134707409907 +8196 4326 93051.72798609662 31786.134707409907 -91.19200677829494 43.00250784527152 +4152 8196 -90.688 43.4075 134013.44954849902 76781.22781410564 +8196 4326 134013.44954849902 76781.22781410564 -90.68800667830217 43.407507941006855 +4152 8197 -90.94 43.205 372703.7327319632 177993.23294857124 +8197 4326 372703.7327319632 177993.23294857124 -90.94000672837129 43.2050078931741 +4152 8197 -91.192 43.0025 305287.21083955286 104285.01075781073 +8197 4326 305287.21083955286 104285.01075781073 -91.19200677829494 43.00250784527152 +4152 8197 -90.688 43.4075 439675.7923322014 251906.41205869502 +8197 4326 439675.7923322014 251906.41205869502 -90.68800667830217 43.407507941006855 +4152 8198 -89.39500000000001 43.465 169119.37909351726 111842.19509287545 +8198 4326 169119.37909351726 111842.19509287545 -89.39500630543864 43.46500802822627 +4152 8198 -89.7505 43.298500000000004 140271.12137693443 93405.09885119235 +8198 4326 140271.12137693443 93405.09885119235 -89.7505063915611 43.298507980048775 +4152 8198 -89.0395 43.631499999999996 197809.72763118503 130402.62404450399 +8198 4326 197809.72763118503 130402.62404450399 -89.03950621883473 43.63150807632123 +4152 8200 -89.39500000000001 43.465 554852.4959928978 366935.60210404213 +8200 4326 554852.4959928978 366935.60210404213 -89.39500630543864 43.46500802822627 +4152 8200 -89.7505 43.298500000000004 460206.17046774225 306446.5621844535 +8200 4326 460206.17046774225 306446.5621844535 -89.7505063915611 43.298507980048775 +4152 8200 -89.0395 43.631499999999996 648980.7478202294 427829.2760895101 +8200 4326 648980.7478202294 427829.2760895101 -89.03950621883473 43.63150807632123 +4152 8201 -90.62 44.730000000000004 206946.959351274 125570.09422627848 +8201 4326 206946.959351274 125570.09422627848 -90.62000681719077 44.73000815277889 +4152 8201 -90.899 44.451 184772.16457891185 94579.01356631645 +8201 4326 184772.16457891185 94579.01356631645 -90.89900686459113 44.45100809199259 +4152 8201 -90.34100000000001 45.009 228909.07612849842 156638.44179029798 +8201 4326 228909.07612849842 156638.44179029798 -90.34100676973705 45.00900821338829 +4152 8202 -90.62 44.730000000000004 678958.4827472217 411974.5505922153 +8202 4326 678958.4827472217 411974.5505922153 -90.62000681719077 44.73000815277889 +4152 8202 -90.899 44.451 606206.6768982302 310297.98012698995 +8202 4326 606206.6768982302 310297.98012698995 -90.89900686459113 44.45100809199259 +4152 8202 -90.34100000000001 45.009 751012.5275404988 513904.62089183606 +8202 4326 751012.5275404988 513904.62089183606 -90.34100676973705 45.00900821338829 +4152 8203 -91.295 45.075 60001.971613199334 54887.59785025609 +8203 4326 60001.971613199334 54887.59785025609 -91.29500706021855 45.07500816181188 +4152 8203 -91.63250000000001 44.8725 33331.048291201965 32438.33085819582 +8203 4326 33331.048291201965 32438.33085819582 -91.63250713294721 44.87250810914558 +4152 8203 -90.9575 45.277499999999996 86485.48926116206 77448.45837036707 +8203 4326 86485.48926116206 77448.45837036707 -90.95750698707668 45.2775082143876 +4152 8204 -91.295 45.075 196856.4688343049 180077.0602635486 +8204 4326 196856.4688343049 180077.0602635486 -91.29500706021855 45.07500816181188 +4152 8204 -91.63250000000001 44.8725 109353.61456871848 106424.75680709751 +8204 4326 109353.61456871848 106424.75680709751 -91.63250713294721 44.87250810914558 +4152 8204 -90.9575 45.277499999999996 283744.4763176626 254095.4834866127 +8204 4326 283744.4763176626 254095.4834866127 -90.95750698707668 45.2775082143876 +4152 8205 -88.465 44.07 247558.8838323784 150052.20867291963 +8205 4326 247558.8838323784 150052.20867291963 -88.46500609705156 44.070008181219364 +4152 8205 -88.8475 43.592999999999996 216692.6049851458 97109.80254276369 +8205 4326 216692.6049851458 97109.80254276369 -88.84750615748322 43.59300808140994 +4152 8205 -88.08250000000001 44.547000000000004 277932.75170286186 203141.34214695933 +8205 4326 277932.75170286186 203141.34214695933 -88.0825060368519 44.54700828040289 +4152 8206 -88.465 44.07 812199.4377283102 492296.28787832055 +8206 4326 812199.4377283102 492296.28787832055 -88.46500609705156 44.070008181219364 +4152 8206 -88.8475 43.592999999999996 710932.3212103479 318601.07709963387 +8206 4326 710932.3212103479 318601.07709963387 -88.84750615748322 43.59300808140994 +4152 8206 -88.08250000000001 44.547000000000004 911851.0359000548 666472.8866177325 +8206 4326 911851.0359000548 666472.8866177325 -88.0825060368519 44.54700828040289 +4152 8207 -92.46000000000001 45.894999999999996 63835.65499249906 59032.98610292785 +8207 4326 63835.65499249906 59032.98610292785 -92.46000751341812 45.895008204910184 +4152 8207 -92.847 45.6565 33670.34936498799 32597.34003068061 +8207 4326 33670.34936498799 32597.34003068061 -92.84700759289251 45.656508143721005 +4152 8207 -92.07300000000001 46.1335 93743.79434247228 85615.43131705209 +8207 4326 93743.79434247228 85615.43131705209 -92.07300743340545 46.13350826598032 +4152 8208 -92.46000000000001 45.894999999999996 209434.14512022396 193677.38821510584 +8208 4326 209434.14512022396 193677.38821510584 -92.46000751341812 45.895008204910184 +4152 8208 -92.847 45.6565 110466.80490729805 106946.43939307469 +8208 4326 110466.80490729805 106946.43939307469 -92.84700759289251 45.656508143721005 +4152 8208 -92.07300000000001 46.1335 307557.7656375944 280889.9605551118 +8208 4326 307557.7656375944 280889.9605551118 -92.07300743340545 46.13350826598032 +4152 8209 -91.805 44.31 174639.75775956767 92070.71010485994 +8209 4326 174639.75775956767 92070.71010485994 -91.80500711160624 44.31000801224052 +4152 8209 -92.06150000000001 44.04900000000001 154080.2750486548 63102.32250271554 +8209 4326 154080.2750486548 63102.32250271554 -92.06150715340662 44.04900795555695 +4152 8209 -91.54849999999999 44.571 195017.85598315235 121104.42711645935 +8209 4326 195017.85598315235 121104.42711645935 -91.54850706978964 44.57100806878774 +4152 8210 -91.805 44.31 572963.9386350136 302068.6549876947 +8210 4326 572963.9386350136 302068.6549876947 -91.80500711160624 44.31000801224052 +4152 8210 -92.06150000000001 44.04900000000001 505511.7024409602 207028.20332965927 +8210 4326 505511.7024409602 207028.20332965927 -92.06150715340662 44.04900795555695 +4152 8210 -91.54849999999999 44.571 639821.082556891 397323.4415499171 +8210 4326 639821.082556891 397323.4415499171 -91.54850706978964 44.57100806878774 +4152 8212 -88.01 44.46 30804.144909606264 166819.4761173327 +8212 4326 30804.144909606264 166819.4761173327 -88.01000600485557 44.460008270804956 +4152 8212 -88.235 44.262 12834.308862850765 144844.22717938322 +8212 4326 12834.308862850765 144844.22717938322 -88.2350060499173 44.262008225727094 +4152 8212 -87.78500000000001 44.658 48652.95405922533 188844.74190878862 +8212 4326 48652.95405922533 188844.74190878862 -87.78500595968168 44.65800831577884 +4152 8213 -88.01 44.46 101063.26509093324 547306.8975616158 +8213 4326 101063.26509093324 547306.8975616158 -88.01000600485557 44.460008270804956 +4152 8213 -88.235 44.262 42107.227994202905 475209.7683376932 +8213 4326 42107.227994202905 475209.7683376932 -88.2350060499173 44.262008225727094 +4152 8213 -87.78500000000001 44.658 159622.23310930846 619568.1237457508 +8213 4326 159622.23310930846 619568.1237457508 -87.78500595968168 44.65800831577884 +4152 8214 -91.155 46.58 228430.11846790204 138585.52276925923 +8214 4326 228430.11846790204 138585.52276925923 -91.15500722426617 46.58000839315878 +4152 8214 -91.51950000000001 46.193 200290.0664978327 95630.91384788198 +8214 4326 200290.0664978327 95630.91384788198 -91.51950727799304 46.19300831239315 +4152 8214 -90.7905 46.967 256172.00257233038 181671.90868332676 +8214 4326 256172.00257233038 181671.90868332676 -90.79050717057585 46.96700847354312 +4152 8216 -91.155 46.58 749441.1470255253 454676.0021797276 +8216 4326 749441.1470255253 454676.0021797276 -91.15500722426617 46.58000839315878 +4152 8216 -91.51950000000001 46.193 657118.3265203895 313749.08941017574 +8216 4326 657118.3265203895 313749.08941017574 -91.51950727799304 46.19300831239315 +4152 8216 -90.7905 46.967 840457.645124804 596035.2532994642 +8216 4326 840457.645124804 596035.2532994642 -90.79050717057585 46.96700847354312 +4152 8218 -91.845 45.425 93541.32785733524 32416.61890283633 +8218 4326 93541.32785733524 32416.61890283633 -91.84500726823188 45.42500817778892 +4152 8218 -92.1285 45.222500000000004 71275.24656327034 9947.783653935769 +8218 4326 71275.24656327034 9947.783653935769 -92.12850732401024 45.22250812877963 +4152 8218 -91.5615 45.6275 115648.74588149578 54964.45908745633 +8218 4326 115648.74588149578 54964.45908745633 -91.5615072122117 45.627508226715996 +4152 8220 -91.845 45.425 306893.5064786074 106353.52433597221 +8220 4326 306893.5064786074 106353.52433597221 -91.84500726823188 45.42500817778892 +4152 8220 -92.1285 45.222500000000004 233842.20476632947 32637.020690204274 +8220 4326 233842.20476632947 32637.020690204274 -92.12850732401024 45.22250812877963 +4152 8220 -91.5615 45.6275 379424.2604462074 180329.23000834632 +8220 4326 379424.2604462074 180329.23000834632 -91.5615072122117 45.627508226715996 +4152 8222 -90.61500000000001 46.535 173376.01422607686 92138.63843222527 +8222 4326 173376.01422607686 92138.63843222527 -90.61500705576826 46.53500842316355 +4152 8222 -90.89850000000001 46.0355 151433.19156667285 36650.11144519595 +8222 4326 151433.19156667285 36650.11144519595 -90.89850707120934 46.035508331148556 +4152 8222 -90.33149999999999 47.0345 194919.96672922006 147710.128259689 +8222 4326 194919.96672922006 147710.128259689 -90.3315070414221 47.034508514393096 +4152 8224 -90.61500000000001 46.535 568817.806176971 302291.5166789758 +8224 4326 568817.806176971 302291.5166789758 -90.61500705576826 46.53500842316355 +4152 8224 -90.89850000000001 46.0355 496827.06216857623 120242.90772236373 +8224 4326 496827.06216857623 120242.90772236373 -90.89850707120934 46.035508331148556 +4152 8224 -90.33149999999999 47.0345 639499.9236810333 484612.312887913 +8224 4326 639499.9236810333 484612.312887913 -90.3315070414221 47.034508514393096 +4152 8225 -89.955 43.945 150831.43734829218 64259.36391885925 +8225 4326 150831.43734829218 64259.36391885925 -89.95500652576588 43.945008071523695 +4152 8225 -90.28349999999999 43.670500000000004 124353.93769781904 33797.0456092559 +8225 4326 124353.93769781904 33797.0456092559 -90.28350659041296 43.670508007740736 +4152 8225 -89.62650000000001 44.2195 177066.1404045827 94828.0926468275 +8225 4326 177066.1404045827 94828.0926468275 -89.62650646087792 44.21950813514733 +4152 8226 -89.955 43.945 494852.80714568775 210824.26298470743 +8226 4326 494852.80714568775 210824.26298470743 -89.95500652576588 43.945008071523695 +4152 8226 -90.28349999999999 43.670500000000004 407984.54370909376 110882.47366395043 +8226 4326 407984.54370909376 110882.47366395043 -90.28350659041296 43.670508007740736 +4152 8226 -89.62650000000001 44.2195 580924.4954228676 311115.1671530499 +8226 4326 580924.4954228676 311115.1671530499 -89.62650646087792 44.21950813514733 +6318 8311 -118.05 43.84 59167.46352330869 37852.70208003176 +8311 4326 59167.46352330869 37852.70208003176 -118.05000000000001 43.84 +6318 8311 -118.554 43.525 18254.84864338336 3160.5671714885957 +8311 4326 18254.84864338336 3160.5671714885957 -118.55399999999999 43.525000000000006 +6318 8311 -117.54599999999999 44.155 99654.33412197899 72793.58365989846 +8311 4326 99654.33412197899 72793.58365989846 -117.546 44.155 +6318 8312 -118.05 43.84 194118.97486282376 124188.65511821443 +8312 4326 194118.97486282376 124188.65511821443 -118.05000000000001 43.84 +6318 8312 -118.554 43.525 59891.23575545722 10369.314867088568 +8312 4326 59891.23575545722 10369.314867088568 -118.55399999999999 43.525000000000006 +6318 8312 -117.54599999999999 44.155 326949.91514717514 238824.09337237026 +8312 4326 326949.91514717514 238824.09337237026 -117.546 44.155 +6318 8313 -118.87 43.94 30439.6035433023 48905.95505260842 +8313 4326 30439.6035433023 48905.95505260842 -118.87 43.94 +6318 8313 -119.185 43.562000000000005 5049.843702412043 6906.540018146743 +8313 4326 5049.843702412043 6906.540018146743 -119.18500000000002 43.562000000000005 +6318 8313 -118.55499999999999 44.318 55508.39017360368 91004.69459942657 +8313 4326 55508.39017360368 91004.69459942657 -118.555 44.318000000000005 +6318 8314 -118.87 43.94 99867.46569298653 160452.6084403163 +8314 4326 99867.46569298653 160452.6084403163 -118.87 43.94 +6318 8314 -119.185 43.562000000000005 16567.728682191744 22659.252028040493 +8314 4326 16567.728682191744 22659.252028040493 -119.18500000000002 43.562000000000005 +6318 8314 -118.55499999999999 44.318 182114.1409892509 298571.832675284 +8314 4326 182114.1409892509 298571.832675284 -118.555 44.318000000000005 +6318 8315 -123.41 45.69 30519.32264112211 31855.945305462017 +8315 4326 30519.32264112211 31855.945305462017 -123.41000000000001 45.69000000000001 +6318 8315 -123.77 45.429 2348.2086947391763 2907.2432511746592 +8315 4326 2348.2086947391763 2907.2432511746592 -123.76999999999998 45.42899999999998 +6318 8315 -123.05000000000001 45.95099999999999 58430.03645614952 60932.05802112527 +8315 4326 58430.03645614952 60932.05802112527 -123.05000000000001 45.95099999999999 +6318 8316 -123.41 45.69 100129.011339377 104514.25625125333 +8316 4326 100129.011339377 104514.25625125333 -123.41000000000001 45.69000000000001 +6318 8316 -123.77 45.429 7704.096817123282 9538.199642699015 +8316 4326 7704.096817123282 9538.199642699015 -123.76999999999998 45.42899999999998 +6318 8316 -123.05000000000001 45.95099999999999 191699.59472201287 199908.32684069968 +8316 4326 191699.59472201287 199908.32684069968 -123.05000000000001 45.95099999999999 +6318 8317 -119.25 44.59 50443.05215015474 37856.97905933531 +8317 4326 50443.05215015474 37856.97905933531 -119.25000000000001 44.59 +6318 8317 -119.826 44.275 4616.648003731378 2796.3237102525777 +8317 4326 4616.648003731378 2796.3237102525777 -119.826 44.27499999999999 +6318 8317 -118.674 44.905 95774.39716374571 73242.48187499332 +8317 4326 95774.39716374571 73242.48187499332 -118.674 44.905 +6318 8318 -119.25 44.59 165495.5779201927 124202.68720254366 +8318 4326 165495.5779201927 124202.68720254366 -119.25000000000001 44.59 +6318 8318 -119.826 44.275 15146.482951612134 9174.290387967774 +8318 4326 15146.482951612134 9174.290387967774 -119.826 44.27499999999999 +6318 8318 -118.674 44.905 314220.4631353861 240296.85654525366 +8318 4326 314220.4631353861 240296.85654525366 -118.674 44.905 +6318 8319 -118.53999999999999 42.71 69894.19397628534 106662.28245253193 +8319 4326 69894.19397628534 106662.28245253193 -118.54 42.71000000000001 +6318 8319 -119.323 41.963 4851.495591629806 24059.95340385055 +8319 4326 4851.495591629806 24059.95340385055 -119.323 41.963 +6318 8319 -117.757 43.457 133399.70566219013 189869.60371191628 +8319 4326 133399.70566219013 189869.60371191628 -117.757 43.457 +6318 8320 -118.53999999999999 42.71 229311.660026133 349941.87156342494 +8320 4326 229311.660026133 349941.87156342494 -118.54 42.71000000000001 +6318 8320 -119.323 41.963 15916.980286449503 78936.85499950967 +8320 4326 15916.980286449503 78936.85499950967 -119.323 41.963 +6318 8320 -117.757 43.457 437663.0763184715 622931.7707083867 +8320 4326 437663.0763184715 622931.7707083867 -117.757 43.457 +6318 8321 -117.12 44.945 50260.90242651026 36109.45325658588 +8321 4326 50260.90242651026 36109.45325658588 -117.12000000000002 44.945000000000014 +6318 8321 -117.56099999999999 44.6435 15323.642084488914 2639.7898374503857 +8321 4326 15323.642084488914 2639.7898374503857 -117.561 44.64350000000001 +6318 8321 -116.679 45.2465 84831.51734034086 69769.63887418322 +8321 4326 84831.51734034086 69769.63887418322 -116.67900000000002 45.2465 +6318 8322 -117.12 44.945 164897.97383973183 118469.33488039985 +8322 4326 164897.97383973183 118469.33488039985 -117.12000000000002 44.945000000000014 +6318 8322 -117.56099999999999 44.6435 50274.41628716835 8660.72786223878 +8322 4326 50274.41628716835 8660.72786223878 -117.561 44.64350000000001 +6318 8322 -116.679 45.2465 278318.62644416286 228903.01472816014 +8322 4326 278318.62644416286 228903.01472816014 -116.67900000000002 45.2465 +6318 8323 -122.22 42.935 62449.230469873786 43871.21197856558 +8323 4326 62449.230469873786 43871.21197856558 -122.22 42.935 +6318 8323 -122.679 42.5705 24773.258651907716 3462.2958797172687 +8323 4326 24773.258651907716 3462.2958797172687 -122.67899999999999 42.5705 +6318 8323 -121.761 43.2995 99690.93401296083 84487.65900893949 +8323 4326 99690.93401296083 84487.65900893949 -121.761 43.29950000000001 +6318 8324 -122.22 42.935 204885.92673764363 143934.42250264296 +8324 4326 204885.92673764363 143934.42250264296 -122.22 42.935 +6318 8324 -122.679 42.5705 81277.09531387046 11359.238451303365 +8324 4326 81277.09531387046 11359.238451303365 -122.67899999999999 42.5705 +6318 8324 -121.761 43.2995 327069.9934800552 277190.4823135809 +8324 4326 327069.9934800552 277190.4823135809 -121.761 43.29950000000001 +6318 8325 -120.19 44.58 34765.93948021344 21143.12137682014 +8325 4326 34765.93948021344 21143.12137682014 -120.19 44.57999999999999 +6318 8325 -120.523 44.400000000000006 8245.30142664022 1173.0273101775674 +8325 4326 8245.30142664022 1173.0273101775674 -120.52299999999998 44.4 +6318 8325 -119.857 44.76 61116.42577876067 41223.560703810916 +8325 4326 61116.42577876067 41223.560703810916 -119.857 44.76 +6318 8326 -120.19 44.58 114061.48128390235 69367.19616437062 +8326 4326 114061.48128390235 69367.19616437062 -120.19 44.57999999999999 +6318 8326 -120.523 44.400000000000006 27051.513916536154 3848.5148433648064 +8326 4326 27051.513916536154 3848.5148433648064 -120.52299999999998 44.4 +6318 8326 -119.857 44.76 200513.20798517278 135247.9026177524 +8326 4326 200513.20798517278 135247.9026177524 -119.857 44.76 +6318 8327 -120.41 45.42 107044.77245900771 57011.68840087189 +8327 4326 107044.77245900771 57011.68840087189 -120.41 45.42000000000001 +6318 8327 -121.652 44.943 9061.100445536766 4647.7211850057065 +8327 4326 9061.100445536766 4647.7211850057065 -121.65200000000002 44.942999999999984 +6318 8327 -119.168 45.897000000000006 203368.6958767194 110892.68150267887 +8327 4326 203368.6958767194 110892.68150267887 -119.16799999999999 45.897 +6318 8328 -120.41 45.42 351196.76003480214 187046.22178580018 +8328 4326 351196.76003480214 187046.22178580018 -120.41 45.42000000000001 +6318 8328 -121.652 44.943 29728.019833125858 15248.429082827162 +8328 4326 29728.019833125858 15248.429082827162 -121.65200000000002 44.942999999999984 +6318 8328 -119.168 45.897000000000006 667220.1308278195 363821.1335371354 +8328 4326 667220.1308278195 363821.1335371354 -119.16799999999999 45.897 +6318 8329 -120.66 44.41 27253.343865522562 21133.91248450322 +8329 4326 27253.343865522562 21133.91248450322 -120.66 44.41 +6318 8329 -120.97500000000001 44.230000000000004 2044.3876169109208 1225.374088596538 +8329 4326 2044.3876169109208 1225.374088596538 -120.97500000000001 44.23000000000004 +6318 8329 -120.345 44.589999999999996 52311.06846406986 41139.347380224994 +8329 4326 52311.06846406986 41139.347380224994 -120.345 44.59 +6318 8330 -120.66 44.41 89413.85782599266 69336.98321792393 +8330 4326 89413.85782599266 69336.98321792393 -120.66 44.41 +6318 8330 -120.97500000000001 44.230000000000004 6707.308453907225 4020.256197232725 +8330 4326 6707.308453907225 4020.256197232725 -120.97500000000001 44.23000000000004 +6318 8330 -120.345 44.589999999999996 171624.24036715832 134971.61214089562 +8330 4326 171624.24036715832 134971.61214089562 -120.345 44.59 +6318 8331 -117.495 42.71 77237.86956371716 106657.62293761526 +8331 4326 77237.86956371716 106657.62293761526 -117.49499999999999 42.71000000000001 +6318 8331 -118.0755 41.963 29192.605523710066 23779.46796974502 +8331 4326 29192.605523710066 23779.46796974502 -118.0755 41.963 +6318 8331 -116.91449999999999 43.457 124141.20355591053 189873.62262076145 +8331 4326 124141.20355591053 189873.62262076145 -116.91449999999999 43.456999999999994 +6318 8332 -117.495 42.71 253405.08391954444 349926.5844409949 +8332 4326 253405.08391954444 349926.5844409949 -117.49499999999999 42.71000000000001 +6318 8332 -118.0755 41.963 95776.26489064979 78016.62719732618 +8332 4326 95776.26489064979 78016.62719732618 -118.0755 41.963 +6318 8332 -116.91449999999999 43.457 407287.4132902575 622944.9561048603 +8332 4326 407287.4132902575 622944.9561048603 -116.91449999999999 43.456999999999994 +6318 8333 -118.88 45.515 59377.30994459695 57572.13827080242 +8333 4326 59377.30994459695 57572.13827080242 -118.88000000000001 45.51500000000001 +6318 8333 -119.57300000000001 45.042500000000004 4844.834462810417 5207.882531852185 +8333 4326 4844.834462810417 5207.882531852185 -119.573 45.042500000000025 +6318 8333 -118.187 45.9875 112992.6237938066 110406.97904282529 +8333 4326 112992.6237938066 110406.97904282529 -118.187 45.9875 +6318 8334 -118.88 45.515 194807.44737413694 188884.96812835438 +8334 4326 194807.44737413694 188884.96812835438 -118.88000000000001 45.51500000000001 +6318 8334 -119.57300000000001 45.042500000000004 15895.126239666724 17086.228827336578 +8334 4326 15895.126239666724 17086.228827336578 -119.573 45.042500000000025 +6318 8334 -118.187 45.9875 370710.7080342736 362227.62157987297 +8334 4326 370710.7080342736 362227.62157987297 -118.187 45.9875 +6318 8335 -118.125 44.59 50071.900153825896 65579.4479384816 +8335 4326 50071.900153825896 65579.4479384816 -118.125 44.59 +6318 8335 -118.7055 44.202999999999996 3598.567341321075 22801.286394891456 +8335 4326 3598.567341321075 22801.286394891456 -118.70550000000001 44.20299999999998 +6318 8335 -117.5445 44.977000000000004 95940.26059983231 108688.94590276176 +8335 4326 95940.26059983231 108688.94590276176 -117.5445 44.97700000000002 +6318 8336 -118.125 44.59 164277.88764299834 215155.66908950653 +8336 4326 164277.88764299834 215155.66908950653 -118.125 44.59 +6318 8336 -118.7055 44.202999999999996 11806.323297510096 74807.37006197983 +8336 4326 11806.323297510096 74807.37006197983 -118.70550000000001 44.20299999999998 +6318 8336 -117.5445 44.977000000000004 314764.6345130981 356591.0298647039 +8336 4326 314764.6345130981 356591.0298647039 -117.5445 44.97700000000002 +6318 8337 -120.06 42.665000000000006 92413.38879164802 101693.82881759042 +8337 4326 92413.38879164802 101693.82881759042 -120.06000000000002 42.66500000000001 +6318 8337 -120.879 41.9585 24751.938211158697 23307.228618390865 +8337 4326 24751.938211158697 23307.228618390865 -120.879 41.95850000000001 +6318 8337 -119.241 43.371500000000005 158550.77459724422 180741.4447347516 +8337 4326 158550.77459724422 180741.4447347516 -119.241 43.37150000000001 +6318 8338 -120.06 42.665000000000006 303193.53283007874 333641.17066138587 +8338 4326 303193.53283007874 333641.17066138587 -120.06000000000002 42.66500000000001 +6318 8338 -120.879 41.9585 81207.14641115055 76467.28549340834 +8338 4326 81207.14641115055 76467.28549340834 -120.879 41.95850000000001 +6318 8338 -119.241 43.371500000000005 520179.70673295343 592983.7425680826 +8338 4326 520179.70673295343 592983.7425680826 -119.241 43.37150000000001 +6318 8339 -122.335 42.205 30511.754038430656 27256.332209608947 +8339 4326 30511.754038430656 27256.332209608947 -122.335 42.205 +6318 8339 -122.6725 41.975500000000004 2608.225749195067 1733.9774032947535 +8339 4326 2608.225749195067 1733.9774032947535 -122.67249999999999 41.97550000000002 +6318 8339 -121.99749999999999 42.4345 58212.02830204596 52889.53043317292 +8339 4326 58212.02830204596 52889.53043317292 -121.99749999999999 42.434499999999986 +6318 8340 -122.335 42.205 100104.17996597983 89423.66210422883 +8340 4326 100104.17996597983 89423.66210422883 -122.335 42.205 +6318 8340 -122.6725 41.975500000000004 8557.17114302843 5688.902240993293 +8340 4326 8557.17114302843 5688.902240993293 -122.67249999999999 41.97550000000002 +6318 8340 -121.99749999999999 42.4345 190984.34487285418 173522.08147287703 +8340 4326 190984.34487285418 173522.08147287703 -121.99749999999999 42.434499999999986 +6318 8341 -118.995 44.865 30395.22304029397 47207.53484867883 +8341 4326 30395.22304029397 47207.53484867883 -118.995 44.86500000000002 +6318 8341 -119.3145 44.554500000000004 5006.0013259909865 12745.186774602887 +8341 4326 5006.0013259909865 12745.186774602887 -119.3145 44.55449999999999 +6318 8341 -118.6755 45.1755 55511.093587808005 81770.51680696096 +8341 4326 55511.093587808005 81770.51680696096 -118.67549999999999 45.17549999999999 +6318 8342 -118.995 44.865 99721.86041802484 154880.3637255867 +8342 4326 99721.86041802484 154880.3637255867 -118.995 44.86500000000002 +6318 8342 -119.3145 44.554500000000004 16423.88891440612 41814.917288329685 +8342 4326 16423.88891440612 41814.917288329685 -119.3145 44.55449999999999 +6318 8342 -118.6755 45.1755 182123.01050829396 268275.97382493754 +8342 4326 182123.01050829396 268275.97382493754 -118.67549999999999 45.17549999999999 +6318 8343 -117.28999999999999 45.66 76369.83231972226 45598.01579291725 +8343 4326 76369.83231972226 45598.01579291725 -117.28999999999998 45.660000000000004 +6318 8343 -118.073 45.309000000000005 15055.50450700245 6718.147648362097 +8343 4326 15055.50450700245 6718.147648362097 -118.073 45.30900000000001 +6318 8343 -116.507 46.010999999999996 136920.67829211085 85076.74859827443 +8343 4326 136920.67829211085 85076.74859827443 -116.507 46.010999999999996 +6318 8344 -117.28999999999999 45.66 250557.19264921997 149599.78934684134 +8344 4326 250557.19264921997 149599.78934684134 -117.28999999999998 45.660000000000004 +6318 8344 -118.073 45.309000000000005 49394.69982533612 22041.166825334964 +8344 4326 49394.69982533612 22041.166825334964 -118.073 45.30900000000001 +6318 8344 -116.507 46.010999999999996 449214.8237922271 279123.19093922054 +8344 4326 449214.8237922271 279123.19093922054 -116.507 46.010999999999996 +6318 8345 -119.86 42.19 51567.534242017646 25566.14783995728 +8345 4326 51567.534242017646 25566.14783995728 -119.86 42.19000000000002 +6318 8345 -120.364 41.974000000000004 9821.486979538444 1622.6633554556902 +8345 4326 9821.486979538444 1622.6633554556902 -120.364 41.97400000000001 +6318 8345 -119.356 42.406 93027.93280987794 49757.0577811922 +8345 4326 93027.93280987794 49757.0577811922 -119.356 42.406000000000006 +6318 8346 -119.86 42.19 169184.82362814186 83878.43779434804 +8346 4326 169184.82362814186 83878.43779434804 -119.86 42.19000000000002 +6318 8346 -120.364 41.974000000000004 32222.726310296734 5323.698671967495 +8346 4326 32222.726310296734 5323.698671967495 -120.364 41.97400000000001 +6318 8346 -119.356 42.406 305209.75331272284 163244.94022622114 +8346 4326 305209.75331272284 163244.94022622114 -119.356 42.406000000000006 +6318 8347 -121.87 43.585 30501.54889722829 65015.2931180604 +8347 4326 30501.54889722829 65015.2931180604 -121.87 43.585 +6318 8347 -122.221 43.0495 1989.9002533890307 5524.051197089503 +8347 4326 1989.9002533890307 5524.051197089503 -122.22100000000002 43.0495 +6318 8347 -121.519 44.1205 58509.61298823514 124631.94776340137 +8347 4326 58509.61298823514 124631.94776340137 -121.51900000000002 44.1205 +6318 8348 -121.87 43.585 100070.69848145764 213304.7674477047 +8348 4326 100070.69848145764 213304.7674477047 -121.87 43.585 +6318 8348 -122.221 43.0495 6528.54413815299 18123.52754950624 +8348 4326 6528.54413815299 18123.52754950624 -122.22100000000002 43.0495 +6318 8348 -121.519 44.1205 191960.67253331735 408897.4664153588 +8348 4326 191960.67253331735 408897.4664153588 -121.51900000000002 44.1205 +8351 8352 19.7 48.67 1238703.6944728189 377730.1286698622 +8352 4326 1238703.6944728189 377730.1286698622 19.69832113690915 48.6695768305335 +8351 8352 17.126 47.824 1316409.8276397535 576063.2019863827 +8352 4326 1316409.8276397535 576063.2019863827 17.124734596687098 47.82356489863311 +8351 8352 22.273999999999997 49.516 1154182.5905085132 185297.76449334432 +8352 4326 1154182.5905085132 185297.76449334432 22.27189879465251 49.51559623655877 +8351 8353 19.7 48.67 -377730.1286698622 -1238703.6944728189 +8353 4326 -377730.1286698622 -1238703.6944728189 19.69832113690915 48.6695768305335 +8351 8353 17.126 47.824 -576063.2019863827 -1316409.8276397535 +8353 4326 -576063.2019863827 -1316409.8276397535 17.124734596687098 47.82356489863311 +8351 8353 22.273999999999997 49.516 -185297.76449334432 -1154182.5905085132 +8353 4326 -185297.76449334432 -1154182.5905085132 22.27189879465251 49.51559623655877 +4269 8379 -115.10499999999999 36.155 87550.56086920926 189466.350319117 +8379 4326 87550.56086920926 189466.350319117 -115.10499999999999 36.15500000000001 +4269 8379 -115.4605 35.9075 55417.59475888784 162104.92199775006 +8379 4326 55417.59475888784 162104.92199775006 -115.46049999999998 35.9075 +4269 8379 -114.7495 36.4025 119482.52718487798 216945.77548282448 +8379 4326 119482.52718487798 216945.77548282448 -114.7495 36.40250000000001 +4269 8380 -115.10499999999999 36.155 287238.7984183975 621607.517705303 +8380 4326 287238.7984183975 621607.517705303 -115.10499999999999 36.15500000000001 +4269 8380 -115.4605 35.9075 181815.8921047846 531839.2316209517 +8380 4326 181815.8921047846 531839.2316209517 -115.46049999999998 35.9075 +4269 8380 -114.7495 36.4025 392002.2579057206 711762.9317632334 +8380 4326 392002.2579057206 711762.9317632334 -114.7495 36.40250000000001 +4269 8381 -115.10499999999999 36.155 287550.1251824084 389465.98167824233 +8381 4326 287550.1251824084 389465.98167824233 -115.10499999999999 36.15500000000001 +4269 8381 -115.4605 35.9075 255416.03453072722 362103.59580263926 +8381 4326 255416.03453072722 362103.59580263926 -115.46049999999998 35.9075 +4269 8381 -114.7495 36.4025 319483.20900514745 416946.36852566176 +8381 4326 319483.20900514745 416946.36852566176 -114.7495 36.40250000000001 +4269 8382 -115.10499999999999 36.155 943404.0357026183 1277772.9748560337 +8382 4326 943404.0357026183 1277772.9748560337 -115.10499999999999 36.15500000000001 +4269 8382 -115.4605 35.9075 837977.4399562277 1188001.547195826 +8382 4326 837977.4399562277 1188001.547195826 -115.46049999999998 35.9075 +4269 8382 -114.7495 36.4025 1048171.161544388 1367931.5440379423 +8382 4326 1048171.161544388 1367931.5440379423 -114.7495 36.40250000000001 +6318 8383 -115.10499999999999 36.155 87550.56086920926 189466.350319117 +8383 4326 87550.56086920926 189466.350319117 -115.10499999999999 36.15500000000001 +6318 8383 -115.4605 35.9075 55417.59475888784 162104.92199775006 +8383 4326 55417.59475888784 162104.92199775006 -115.46049999999998 35.9075 +6318 8383 -114.7495 36.4025 119482.52718487798 216945.77548282448 +8383 4326 119482.52718487798 216945.77548282448 -114.7495 36.40250000000001 +6318 8384 -115.10499999999999 36.155 287238.7984183975 621607.517705303 +8384 4326 287238.7984183975 621607.517705303 -115.10499999999999 36.15500000000001 +6318 8384 -115.4605 35.9075 181815.8921047846 531839.2316209517 +8384 4326 181815.8921047846 531839.2316209517 -115.46049999999998 35.9075 +6318 8384 -114.7495 36.4025 392002.2579057206 711762.9317632334 +8384 4326 392002.2579057206 711762.9317632334 -114.7495 36.40250000000001 +6318 8385 -115.10499999999999 36.155 287550.1251824084 389465.98167824233 +8385 4326 287550.1251824084 389465.98167824233 -115.10499999999999 36.15500000000001 +6318 8385 -115.4605 35.9075 255416.03453072722 362103.59580263926 +8385 4326 255416.03453072722 362103.59580263926 -115.46049999999998 35.9075 +6318 8385 -114.7495 36.4025 319483.20900514745 416946.36852566176 +8385 4326 319483.20900514745 416946.36852566176 -114.7495 36.40250000000001 +6318 8387 -115.10499999999999 36.155 943404.0357026183 1277772.9748560337 +8387 4326 943404.0357026183 1277772.9748560337 -115.10499999999999 36.15500000000001 +6318 8387 -115.4605 35.9075 837977.4399562277 1188001.547195826 +8387 4326 837977.4399562277 1188001.547195826 -115.46049999999998 35.9075 +6318 8387 -114.7495 36.4025 1048171.161544388 1367931.5440379423 +8387 4326 1048171.161544388 1367931.5440379423 -114.7495 36.40250000000001 +4283 8391 142.005 -12.495000000000001 409240.7638622285 618047.3300731988 +8391 4326 409240.7638622285 618047.3300731988 142.005 -12.495000000000001 +4283 8391 141.5505 -13.3995 359620.4815300247 518130.46907171025 +8391 4326 359620.4815300247 518130.46907171025 141.5505 -13.399500000000002 +4283 8391 142.4595 -11.5905 459183.9261169875 717898.29884885 +8391 4326 459183.9261169875 717898.29884885 142.4595 -11.5905 +4258 8395 9.370000000000001 53.675 524451.578417395 5949806.49615931 +8395 4326 524451.578417395 5949806.49615931 9.37 53.67499999999999 +4258 8395 8.434 53.3915 462345.12844228547 5918339.69145626 +8395 4326 462345.12844228547 5918339.69145626 8.434 53.39149999999999 +4258 8395 10.306000000000001 53.9585 585724.8401104046 5982086.943088973 +8395 4326 585724.8401104046 5982086.943088973 10.306000000000001 53.9585 +8428 8433 113.6 22.145 26553.883697886664 12538.167494759826 +8428 8433 113.52799999999999 22.0685 19125.811450458314 4065.7027517907063 +8428 8433 113.67200000000001 22.2215 33973.926293627854 21014.24735423395 +4297 8441 46.870000000000005 -18.765 445610.1476866329 814880.5474928864 +8441 4326 445610.1476866329 814880.5474928864 46.86981363327775 -18.76632584696089 +4297 8441 43.549 -24.9525 108046.14966821123 127439.37913616933 +8441 4326 108046.14966821123 127439.37913616933 43.54862724649123 -24.953932437302587 +4297 8441 50.191 -12.5775 808198.3181131508 1495706.7558497624 +8441 4326 808198.3181131508 1495706.7558497624 50.190985405892555 -12.578715688442395 +7073 8455 137.005 -66.17 590381.4770634106 2659694.2856762437 +8455 4326 590381.4770634106 2659694.2856762437 137.005 -66.17 +7073 8455 136.1005 -66.674 548624.1670524891 2604531.8897866 +8455 4326 548624.1670524891 2604531.8897866 136.1005 -66.674 +7073 8455 137.90949999999998 -65.666 633738.9979620724 2714222.8514424367 +8455 4326 633738.9979620724 2714222.8514424367 137.90949999999998 -65.666 +7073 8456 140.0 -66.615 455710.5881495178 2611182.8052810645 +8456 4326 455710.5881495178 2611182.8052810645 140.0 -66.615 +7073 8456 138.2 -67.07849999999999 378340.7080391872 2557130.2094415417 +8456 4326 378340.7080391872 2557130.2094415417 138.2 -67.07849999999999 +7073 8456 141.8 -66.1515 536092.9389125342 2662972.7016151715 +8456 4326 536092.9389125342 2662972.7016151715 141.8 -66.1515 +6318 8518 -101.72 39.349999999999994 1466058.5456059414 673874.424572726 +8518 4326 1466058.5456059414 673874.424572726 -101.72 39.349999999999994 +6318 8518 -102.026 38.756 1378493.9820471846 457750.2995523893 +8518 4326 1378493.9820471846 457750.2995523893 -102.026 38.75600000000001 +6318 8518 -101.414 39.943999999999996 1552161.0159535452 890312.7572633934 +8518 4326 1552161.0159535452 890312.7572633934 -101.414 39.943999999999996 +6318 8519 -101.1 39.349999999999994 2457574.111791158 673872.278184222 +8519 4326 2457574.111791158 673872.278184222 -101.1 39.349999999999994 +6318 8519 -101.451 38.756 2357105.1104274085 457848.56261722825 +8519 4326 2357105.1104274085 457848.56261722825 -101.451 38.75600000000001 +6318 8519 -100.749 39.943999999999996 2556366.312560799 890302.2943072445 +8519 4326 2556366.312560799 890302.2943072445 -100.749 39.943999999999996 +6318 8520 -100.47999999999999 39.349999999999994 3463231.5607143794 673851.3862394487 +8520 4326 3463231.5607143794 673851.3862394487 -100.47999999999999 39.34999999999999 +6318 8520 -100.78599999999999 38.756 3375646.6897569746 457745.4235532183 +8520 4326 3375646.6897569746 457745.4235532183 -100.78599999999999 38.756000000000014 +6318 8520 -100.174 39.943999999999996 3549354.6846849886 890271.4626363043 +8520 4326 3549354.6846849886 890271.4626363043 -100.174 39.943999999999996 +6318 8521 -99.61 39.349999999999994 4454748.070738545 673842.0927859746 +8521 4326 4454748.070738545 673842.0927859746 -99.61 39.349999999999994 +6318 8521 -100.132 38.756 4305490.061022646 458158.3874992018 +8521 4326 4305490.061022646 458158.3874992018 -100.13200000000002 38.75600000000001 +6318 8521 -99.088 39.943999999999996 4601510.2534983475 890398.4173901296 +8521 4326 4601510.2534983475 890398.4173901296 -99.088 39.943999999999996 +6318 8522 -98.77 39.135 5470685.564152019 595498.6368154042 +8522 4326 5470685.564152019 595498.6368154042 -98.77 39.135000000000005 +6318 8522 -99.03999999999999 38.3475 5392920.00247974 308862.91863654676 +8522 4326 5392920.00247974 308862.91863654676 -99.03999999999999 38.3475 +6318 8522 -98.5 39.9225 5546750.23882239 882399.9334499664 +8522 4326 5546750.23882239 882399.9334499664 -98.5 39.9225 +6318 8523 -98.215 39.08 6481546.123618024 575448.5837134242 +8523 4326 6481546.123618024 575448.5837134242 -98.215 39.08 +6318 8523 -98.4805 38.242999999999995 6405070.448820826 270753.5404104823 +8523 4326 6405070.448820826 270753.5404104823 -98.4805 38.242999999999995 +6318 8523 -97.9495 39.917 6556244.480779236 880406.9402093778 +8523 4326 6556244.480779236 880406.9402093778 -97.9495 39.91700000000001 +6318 8524 -97.37 39.045 7489585.177878958 562684.5746481904 +8524 4326 7489585.177878958 562684.5746481904 -97.37000000000002 39.044999999999995 +6318 8524 -97.883 38.1765 7341978.097824617 246828.67786123639 +8524 4326 7341978.097824617 246828.67786123639 -97.883 38.1765 +6318 8524 -96.857 39.9135 7633626.040873594 879408.4234193016 +8524 4326 7633626.040873594 879408.4234193016 -96.857 39.913500000000006 +6318 8525 -96.455 39.15 8512762.819755431 593932.3447609519 +8525 4326 8512762.819755431 593932.3447609519 -96.455 39.14999999999999 +6318 8525 -96.9185 38.772 8380671.040204306 456521.7520909452 +8525 4326 8380671.040204306 456521.7520909452 -96.9185 38.772 +6318 8525 -95.9915 39.528 8643447.322442392 732022.575757657 +8525 4326 8643447.322442392 732022.575757657 -95.9915 39.52799999999999 +6318 8526 -96.44 38.480000000000004 9517177.375930566 292721.4060487534 +8526 4326 9517177.375930566 292721.4060487534 -96.44 38.48000000000001 +6318 8526 -96.89 38.12 9387791.819545921 161841.32087091697 +8526 4326 9387791.819545921 161841.32087091697 -96.88999999999999 38.120000000000005 +6318 8526 -95.99 38.84 9645280.411315652 424238.3294571244 +8526 4326 9645280.411315652 424238.3294571244 -95.99 38.84 +6318 8527 -95.83 39.61 10477459.071598988 691510.2440188004 +8527 4326 10477459.071598988 691510.2440188004 -95.83000000000001 39.61000000000001 +6318 8527 -96.712 39.25 10227546.049136084 561822.8260694041 +8527 4326 10227546.049136084 561822.8260694041 -96.712 39.249999999999986 +6318 8527 -94.948 39.97 10724798.910769489 823647.9789369666 +8527 4326 10724798.910769489 823647.9789369666 -94.948 39.97 +6318 8528 -95.31 39.08 11482966.249695556 592720.8058208977 +8528 4326 11482966.249695556 592720.8058208977 -95.31 39.08 +6318 8528 -95.96700000000001 38.765 11295543.283953637 478788.7134644227 +8528 4326 11295543.283953637 478788.7134644227 -95.96700000000001 38.765 +6318 8528 -94.65299999999999 39.394999999999996 11668730.615774559 708008.8658541312 +8528 4326 11668730.615774559 708008.8658541312 -94.65299999999999 39.39500000000001 +6318 8529 -101.555 37.85 12460050.699623957 400615.0843116021 +8529 4326 12460050.699623957 400615.0843116021 -101.55499999999999 37.849999999999994 +6318 8529 -102.0005 37.076 12329646.899419382 119234.46182202359 +8529 4326 12329646.899419382 119234.46182202359 -102.0005 37.076 +6318 8529 -101.1095 38.624 12587771.38240308 682643.7944051004 +8529 4326 12587771.38240308 682643.7944051004 -101.1095 38.623999999999995 +6318 8531 -100.60499999999999 37.85 13440799.842575975 400638.06064375944 +8531 4326 13440799.842575975 400638.06064375944 -100.60499999999999 37.85 +6318 8531 -101.0775 37.076 13302322.285092669 119412.13356454893 +8531 4326 13302322.285092669 119412.13356454893 -101.0775 37.07599999999999 +6318 8531 -100.1325 38.624 13576434.436196111 682587.155092676 +8531 4326 13576434.436196111 682587.155092676 -100.1325 38.623999999999995 +6318 8533 -99.89500000000001 37.845 14434057.943516117 398827.9508038179 +8533 4326 14434057.943516117 398827.9508038179 -99.89500000000001 37.84499999999999 +6318 8533 -100.2145 37.075500000000005 14340157.02989073 118984.72396423912 +8533 4326 14340157.02989073 118984.72396423912 -100.2145 37.075500000000005 +6318 8533 -99.5755 38.6145 14526052.722975736 679020.2950438099 +8533 4326 14526052.722975736 679020.2950438099 -99.5755 38.6145 +6318 8534 -99.25 37.845 15485560.287090616 398747.21451206936 +8534 4326 15485560.287090616 398747.21451206936 -99.25 37.845 +6318 8534 -99.556 37.075500000000005 15396130.106089707 118717.37990728617 +8534 4326 15396130.106089707 118717.37990728617 -99.55599999999998 37.075500000000005 +6318 8534 -98.944 38.6145 15573156.505212205 679102.5657080241 +8534 4326 15573156.505212205 679102.5657080241 -98.944 38.61449999999999 +6318 8535 -98.685 37.63 16460900.557737958 320467.7916034891 +8535 4326 16460900.557737958 320467.7916034891 -98.685 37.63 +6318 8535 -98.9955 37.054 16369982.26717497 110996.53287396865 +8535 4326 16369982.26717497 110996.53287396865 -98.9955 37.053999999999995 +6318 8535 -98.3745 38.206 16550434.426515747 530255.6886360052 +8535 4326 16550434.426515747 530255.6886360052 -98.3745 38.20600000000001 +6318 8536 -97.5 37.78 17500000.000000015 404855.5782729567 +8536 4326 17500000.000000015 404855.5782729567 -97.5 37.78 +6318 8536 -98.382 37.42 17243835.019521926 274965.61888428906 +8536 4326 17243835.019521926 274965.61888428906 -98.382 37.42 +6318 8536 -96.618 38.14 17753692.99284889 537157.2221076384 +8536 4326 17753692.99284889 537157.2221076384 -96.618 38.14 +6318 8538 -97.435 37.235 18518924.57960057 218819.97940736305 +8538 4326 18518924.57960057 218819.97940736305 -97.435 37.235 +6318 8538 -98.2585 37.0145 18278524.67347446 139409.42587370766 +8538 4326 18278524.67347446 139409.42587370766 -98.2585 37.01449999999999 +6318 8538 -96.61149999999999 37.4555 18757928.17482618 300315.4982464337 +8538 4326 18757928.17482618 300315.4982464337 -96.61149999999999 37.4555 +6318 8539 -96.015 37.935 19486059.330679696 431500.8168899642 +8539 4326 19486059.330679696 431500.8168899642 -96.015 37.935 +6318 8539 -96.4785 37.084500000000006 19350687.995880008 122195.75637248112 +8539 4326 19350687.995880008 122195.75637248112 -96.4785 37.084500000000006 +6318 8539 -95.5515 38.7855 19618352.865214236 741514.2809226837 +8539 4326 19618352.865214236 741514.2809226837 -95.5515 38.785500000000006 +6318 8540 -95.065 37.865 20505292.834652074 406004.80570995616 +8540 4326 20505292.834652074 406004.80570995616 -95.065 37.864999999999995 +6318 8540 -95.4835 37.0775 20383253.175390415 119490.23055461983 +8540 4326 20383253.175390415 119490.23055461983 -95.4835 37.07749999999999 +6318 8540 -94.64649999999999 38.6525 20624760.11902179 693099.6519025342 +8540 4326 20624760.11902179 693099.6519025342 -94.64649999999999 38.6525 +3906 8677 14.940000000000001 44.915000000000006 5495263.247403372 4974498.434168149 +8677 4326 5495263.247403372 4974498.434168149 14.935289488871083 44.914802881930214 +3906 8677 13.536000000000001 43.1465 5380933.447492704 4779075.352140953 +8677 4326 5380933.447492704 4779075.352140953 13.531611593493938 43.14646033639745 +3906 8677 16.344 46.6835 5602798.019621097 5171896.612161219 +8677 4326 5602798.019621097 5171896.612161219 16.338945252073465 46.683149898039254 +3906 8678 18.005000000000003 44.17 6500399.7958104275 4891727.620446666 +8678 4326 6500399.7958104275 4891727.620446666 17.999950257569346 44.17002269084858 +3906 8678 16.6505 42.028 6388265.4761095345 4654693.805135344 +8678 4326 6388265.4761095345 4654693.805135344 16.645791861551587 42.02821739474103 +3906 8678 19.3595 46.312 6604694.267022721 5130629.973249198 +8678 4326 6604694.267022721 5130629.973249198 19.354078545174083 46.31183225063586 +3906 8679 22.77 42.905 8399571.816561338 4751945.496885973 +8679 4326 8399571.816561338 4751945.496885973 22.764475858798683 42.90538935340928 +3906 8679 22.527 41.2895 8376638.122761672 4572853.680539366 +8679 4326 8376638.122761672 4572853.680539366 22.52164221260179 41.29005996119055 +3906 8679 23.012999999999998 44.520500000000006 8421549.095994972 4931140.421768345 +8679 4326 8421549.095994972 4931140.421768345 23.007296148985716 44.52071788226261 +8685 8682 20.91 44.209999999999994 492809.8293023745 4895201.446140192 +8682 4326 492809.8293023745 4895201.446140192 20.91 44.21 +8685 8682 19.02 42.428 337121.5696149337 4699197.480457048 +8682 4326 337121.5696149337 4699197.480457048 19.02 42.428 +8685 8682 22.8 45.992 639397.489077989 5094733.784221265 +8682 4326 639397.489077989 5094733.784221265 22.8 45.99199999999999 +3906 8686 14.995000000000001 46.150000000000006 499613.81690065074 5111728.304860258 +8686 4326 499613.81690065074 5111728.304860258 14.99020681466189 46.149695542628805 +3906 8686 13.541500000000001 45.493 386017.7385578714 5039754.331987586 +8686 4326 386017.7385578714 5039754.331987586 13.536908276225576 45.492727314145455 +3906 8686 16.4485 46.807 610537.9956136482 5185764.170856672 +8686 4326 610537.9956136482 5185764.170856672 16.443501275694995 46.80666579958892 +4765 8687 14.995000000000001 46.150000000000006 499613.8859634856 5110713.75248837 +8687 4326 499613.8859634856 5110713.75248837 14.995000000000003 46.14999999999999 +4765 8687 13.541500000000001 45.493 386038.1356077045 5038753.371673072 +8687 4326 386038.1356077045 5038753.371673072 13.541500000000001 45.493 +4765 8687 16.4485 46.807 610518.2404169647 5184735.682887034 +8687 4326 610518.2404169647 5184735.682887034 16.4485 46.80700000000001 +6325 8692 142.6 17.424999999999997 669938.9046734368 1927282.3252916008 +6325 8692 141.331 11.5975 536085.1187407505 1282067.5607142646 +6325 8692 143.869 23.252499999999998 793554.7914425882 2574375.0654679416 +6325 8693 146.775 17.475 476111.3710798957 1932117.2244854828 +6325 8693 144.2775 11.6925 203196.2311867373 1293981.1158726104 +6325 8693 149.2725 23.2575 732487.4069242007 2573846.585855004 +4269 8826 -114.14 45.5 2489062.248105151 1588729.810774318 +8826 4326 2489062.248105151 1588729.810774318 -114.14 45.5 +4269 8826 -116.92999999999999 42.341 2258634.2854685807 1242020.6382058566 +8826 4326 2258634.2854685807 1242020.6382058566 -116.93 42.341 +4269 8826 -111.35000000000001 48.659 2695138.62684035 1943162.5658296163 +8826 4326 2695138.62684035 1943162.5658296163 -111.35000000000001 48.65899999999999 +8818 8836 35.224999999999994 27.15 720492.7040521107 3005003.5726890224 +8836 4326 720492.7040521107 3005003.5726890224 35.224999999999994 27.15 +8818 8836 34.518499999999996 25.143 653065.0968453363 2781644.0151644135 +8836 4326 653065.0968453363 2781644.0151644135 34.518499999999996 25.143 +8818 8836 35.9315 29.157 785161.1861426497 3228936.2303633266 +8836 4326 785161.1861426497 3228936.2303633266 35.9315 29.157 +8818 8837 39.0 24.224999999999998 500000.00000000064 2679137.535683546 +8837 4326 500000.00000000064 2679137.535683546 39.0 24.22499999999999 +8818 8837 36.3 17.0835 212642.23056858336 1890782.31351636 +8837 4326 212642.23056858336 1890782.31351636 36.3 17.0835 +8818 8837 41.7 31.366499999999995 756811.1555608156 3473371.536976828 +8837 4326 756811.1555608156 3473371.536976828 41.7 31.366499999999984 +8818 8838 44.995000000000005 23.75 499490.467028527 2626548.518286022 +8838 4326 499490.467028527 2626548.518286022 44.995000000000005 23.749999999999996 +8818 8838 42.2905 17.09 211640.5379965359 1891516.0928051912 +8838 4326 211640.5379965359 1891516.0928051912 42.2905 17.09 +8818 8838 47.6995 30.409999999999997 759330.7397447096 3367311.9163983064 +8838 4326 759330.7397447096 3367311.9163983064 47.6995 30.409999999999993 +8818 8839 51.0 23.44 500000.0000000019 2592228.9955780194 +8839 4326 500000.0000000019 2592228.9955780194 51.0 23.44000000000001 +8818 8839 48.291000000000004 18.490000000000002 213934.9339567909 2046544.4754638572 +8839 4326 213934.9339567909 2046544.4754638572 48.291000000000004 18.49 +8818 8839 53.708999999999996 28.39 765445.1623114006 3143391.0293677365 +8839 4326 765445.1623114006 3143391.0293677365 53.708999999999996 28.390000000000004 +8818 8840 54.835 21.215 275278.1681571158 2347479.475925416 +8840 4326 275278.1681571158 2347479.475925416 54.835 21.214999999999996 +8818 8840 54.0835 19.8155 194462.6627620496 2193702.4581302837 +8840 4326 194462.6627620496 2193702.4581302837 54.08350000000001 19.815499999999997 +8818 8840 55.5865 22.6145 354723.8245380474 2501535.229214023 +8840 4326 354723.8245380474 2501535.229214023 55.5865 22.614499999999996 +4326 8857 0.0 0.0 0.0 0.0 +4326 8857 -162.0 -81.0 -9510094.875380825 -8240535.466466919 +4326 8857 162.0 81.0 9510094.875380825 8240535.466466919 +4326 8858 -90.0 0.0 0.0 0.0 +4326 8858 108.00900000000001 -81.0 -9509566.536776636 -8240535.466466919 +4326 8858 71.99099999999999 81.0 9509566.536776636 8240535.466466919 +4326 8859 150.0 0.0 0.0 0.0 +4326 8859 -11.990999999999996 -81.0 -9509566.536776638 -8240535.466466919 +4326 8859 -48.009000000000015 81.0 9509566.536776636 8240535.466466919 +8900 8903 -177.39 -12.89 457689.02397287247 8574996.588249907 +8903 4326 457689.02397287247 8574996.588249907 -177.39 -12.889999999999999 +8900 8903 179.80200000000002 -15.635 157073.4727586318 8268856.9866816625 +8903 4326 157073.4727586318 8268856.9866816625 179.80200000000008 -15.634999999999994 +8900 8903 -174.58200000000002 -10.145 764957.8854885472 8877570.514045794 +8903 4326 764957.8854885472 8877570.514045794 -174.58200000000002 -10.144999999999996 +8907 8908 -84.25 9.600000000000001 472559.9624966815 1061516.0732838772 +8907 8908 -85.798 8.142000000000001 301840.1503990955 900706.7911366953 +8907 8908 -82.702 11.058 641824.5765838791 1223067.562130935 +8907 8909 -86.685 6.629999999999999 534818.9293767875 732858.0149493598 +8907 8909 -90.0735 2.598 158181.17983286676 287574.90880885307 +8907 8909 -83.29650000000001 10.661999999999999 905303.2647433224 1181033.0598187565 +8907 8910 -82.515 10.684999999999999 334304.2368647787 1181556.3289905842 +8907 8910 -83.4915 9.708499999999999 226621.12431829725 1074186.3330090242 +8907 8910 -81.5385 11.6615 441306.4564728617 1289179.1687382942 +8949 8950 -74.995 -38.855000000000004 500433.8410580684 5699314.0104445405 +8950 4326 500433.8410580684 5699314.0104445405 -74.995 -38.855000000000004 +8949 8950 -77.6995 -57.3095 337399.9150358017 3644936.4607056994 +8950 4326 337399.9150358017 3644936.4607056994 -77.6995 -57.30950000000001 +8949 8950 -72.2905 -20.4005 782784.4819780085 7741866.394019629 +8950 4326 782784.4819780085 7741866.394019629 -72.2905 -20.400499999999997 +8949 8951 -69.0 -38.685 500000.0 5718178.35995209 +8951 4326 500000.0 5718178.35995209 -69.0 -38.684999999999995 +8949 8951 -71.7 -57.7515 339326.61691853346 3595752.8485343764 +8951 4326 339326.61691853346 3595752.8485343764 -71.7 -57.7515 +8949 8951 -66.3 -19.618500000000004 783190.0307251445 7828493.0824960265 +8951 4326 783190.0307251445 7828493.0824960265 -66.3 -19.618500000000004 +8086 9039 -18.21 64.775 3013773.000721653 4899380.463074724 +9039 4326 3013773.000721653 4899380.463074724 -18.209999999999987 64.775 +8086 9039 -29.604 60.4415 2267814.24831495 4747070.920957485 +9039 4326 2267814.24831495 4747070.920957485 -29.603999999999992 60.44150000000001 +8086 9039 -6.8160000000000025 69.1085 3651810.1081470903 5191145.124202191 +9039 4326 3651810.1081470903 5191145.124202191 -6.8159999999999945 69.1085 +8086 9040 -18.21 64.775 2691929.977472239 4443141.16925533 +9040 4326 2691929.977472239 4443141.16925533 -18.21 64.775 +8086 9040 -29.604 60.4415 1963119.6915789302 4272041.328963477 +9040 4326 1963119.6915789302 4272041.328963477 -29.603999999999985 60.4415 +8086 9040 -6.8160000000000025 69.1085 3318213.5570691726 4757378.547099611 +9040 4326 3318213.5570691726 4757378.547099611 -6.815999999999998 69.1085 +9140 9141 20.884999999999998 42.55 7490555.592649365 4712265.2608367065 +9141 4326 7490555.592649365 4712265.2608367065 20.88499999999999 42.55 +9140 9141 20.0615 41.92 7422154.649418124 4642712.766338751 +9141 4326 7422154.649418124 4642712.766338751 20.061500000000002 41.92 +9140 9141 21.7085 43.18 7557597.185594524 4782482.312740578 +9141 4326 7557597.185594524 4782482.312740578 21.708499999999997 43.18 +9148 9149 -74.995 -38.855000000000004 500433.8410580684 5699314.0104445405 +9149 4326 500433.8410580684 5699314.0104445405 -74.995 -38.855000000000004 +9148 9149 -77.6995 -57.3095 337399.9150358017 3644936.4607056994 +9149 4326 337399.9150358017 3644936.4607056994 -77.6995 -57.30950000000001 +9148 9149 -72.2905 -20.4005 782784.4819780085 7741866.394019629 +9149 4326 782784.4819780085 7741866.394019629 -72.2905 -20.400499999999997 +9148 9150 -69.0 -38.685 500000.0 5718178.35995209 +9150 4326 500000.0 5718178.35995209 -69.0 -38.684999999999995 +9148 9150 -71.7 -57.7515 339326.61691853346 3595752.8485343764 +9150 4326 339326.61691853346 3595752.8485343764 -71.7 -57.7515 +9148 9150 -66.3 -19.618500000000004 783190.0307251445 7828493.0824960265 +9150 4326 783190.0307251445 7828493.0824960265 -66.3 -19.618500000000004 +9153 9154 -74.995 -38.855000000000004 500433.8410580684 5699314.0104445405 +9154 4326 500433.8410580684 5699314.0104445405 -74.995 -38.855000000000004 +9153 9154 -77.6995 -57.3095 337399.9150358017 3644936.4607056994 +9154 4326 337399.9150358017 3644936.4607056994 -77.6995 -57.30950000000001 +9153 9154 -72.2905 -20.4005 782784.4819780085 7741866.394019629 +9154 4326 782784.4819780085 7741866.394019629 -72.2905 -20.400499999999997 +9153 9155 -69.0 -38.685 500000.0 5718178.35995209 +9155 4326 500000.0 5718178.35995209 -69.0 -38.684999999999995 +9153 9155 -71.7 -57.7515 339326.61691853346 3595752.8485343764 +9155 4326 339326.61691853346 3595752.8485343764 -71.7 -57.7515 +9153 9155 -66.3 -19.618500000000004 783190.0307251445 7828493.0824960265 +9155 4326 783190.0307251445 7828493.0824960265 -66.3 -19.618500000000004 +8699 9156 10.105 -11.155000000000001 620657.9052397518 8766656.941152845 +9156 4326 620657.9052397518 8766656.941152845 10.105 -11.154999999999996 +8699 9156 8.3905 -16.6675 435008.5031738323 8157128.026695272 +9156 4326 435008.5031738323 8157128.026695272 8.3905 -16.66749999999999 +8699 9156 11.8195 -5.642500000000002 812354.1964518211 9375557.907974854 +9156 4326 812354.1964518211 9375557.907974854 11.819499999999998 -5.64250000000001 +8699 9157 15.0 -10.91 500000.00000000175 8793971.87940101 +9157 4326 500000.00000000175 8793971.87940101 14.999999999999998 -10.909999999999995 +8699 9157 12.3 -16.787000000000003 212191.23435196036 8142048.25170163 +9157 4326 212191.23435196036 8142048.25170163 12.3 -16.787000000000006 +8699 9157 17.7 -5.0329999999999995 799401.5659952423 9443068.697864026 +9157 4326 799401.5659952423 9443068.697864026 17.7 -5.033000000000004 +8699 9158 21.04 -12.465 504346.7579704274 8622026.384780783 +9158 4326 504346.7579704274 8622026.384780783 21.04 -12.465000000000005 +8699 9158 18.294999999999998 -17.4645 212701.63859343395 8067022.06373421 +9158 4326 212701.63859343395 8067022.06373421 18.294999999999998 -17.464500000000005 +8699 9158 23.785 -7.4655000000000005 807408.9669788058 9173819.904265475 +9158 4326 807408.9669788058 9173819.904265475 23.785 -7.465499999999997 +8699 9159 11.03 -11.635000000000002 394262.0769175155 8713626.304886878 +9159 4326 394262.0769175155 8713626.304886878 11.03 -11.635000000000007 +8699 9159 8.482999999999999 -16.6975 124845.71029296773 8150597.326212599 +9159 4326 124845.71029296773 8150597.326212599 8.482999999999999 -16.6975 +8699 9159 13.576999999999998 -6.5725 674356.554311768 9273234.659275249 +9159 4326 674356.554311768 9273234.659275249 13.576999999999998 -6.572499999999997 +4326 9191 172.505 -42.5 -202054.00328197575 -284665.01180973946 +4326 9191 156.7505 -58.25 -1119518.2172637784 -2135487.5870838836 +4326 9191 -171.7405 -26.75 1327125.9860600932 1381241.8909674534 +4756 9205 103.065 21.855 506718.1708320395 2417504.373916227 +9205 4326 506718.1708320395 2417504.373916227 103.06689798086484 21.85404991796494 +4756 9205 102.2325 20.9865 420201.7380761835 2321541.457671951 +9205 4326 420201.7380761835 2321541.457671951 102.23438721716404 20.985554189555874 +4756 9205 103.8975 22.7235 592193.7654277282 2513944.943549436 +9205 4326 592193.7654277282 2513944.943549436 103.89940890978907 22.722545149506335 +4756 9206 104.12 21.305 512449.6881502483 2356615.2215447407 +9206 4326 512449.6881502483 2356615.2215447407 104.12188956129528 21.30403477521003 +4756 9206 103.30099999999999 20.6435 427159.44341668405 2283535.362189107 +9206 4326 427159.44341668405 2283535.362189107 103.30288219823463 20.642540383995563 +4756 9206 104.93900000000001 21.9665 596979.3646218231 2430145.291961354 +9206 4326 596979.3646218231 2430145.291961354 104.9408968551037 21.965528760198005 +4756 9207 104.47 9.440000000000001 496705.6729236095 1043811.2558175275 +9207 4326 496705.6729236095 1043811.2558175275 104.47178357743125 9.438992434307249 +4756 9207 103.507 8.441 390654.94249902264 933470.7398652002 +9207 4326 390654.94249902264 933470.7398652002 103.50877988099147 8.43999565905588 +4756 9207 105.433 10.439 602147.2713575873 1154447.5495415465 +9207 4326 602147.2713575873 1154447.5495415465 105.4347873308025 10.437988503703528 +4756 9208 104.695 16.515 494129.01064167044 1826451.9721814194 +9208 4326 494129.01064167044 1826451.9721814194 104.69683513179422 16.514008123201833 +4756 9208 103.6465 10.8135 379331.08896661946 1195934.9483043752 +9208 4326 379331.08896661946 1195934.9483043752 103.64829243152097 10.812499507031289 +4756 9208 105.7435 22.2165 602427.9424793534 2457863.920094654 +9208 4326 602427.9424793534 2457863.920094654 105.74539847437921 22.21552056588701 +4756 9209 105.50999999999999 16.294999999999998 501068.65242313594 1802107.7936400233 +9209 4326 501068.65242313594 1802107.7936400233 105.51183144901408 16.294000229298767 +4756 9209 104.448 9.9005 384629.8700776206 1094922.0310667993 +9209 4326 384629.8700776206 1094922.0310667993 104.44978608632293 9.8994932307308 +4756 9209 106.572 22.689499999999995 610147.4583344461 2510298.724913072 +9209 4326 610147.4583344461 2510298.724913072 106.5739027722812 22.688512892770245 +4756 9210 106.53 16.435000000000002 583297.6448208487 1817759.389344938 +9210 4326 583297.6448208487 1817759.389344938 106.53183024226607 16.433991655340602 +4756 9210 105.387 10.4185 460256.61587052746 1152052.324144318 +9210 4326 460256.61587052746 1152052.324144318 105.38878730779284 10.417488740482648 +4756 9210 107.673 22.451500000000003 697945.1094010101 2484817.1195433326 +9210 4326 697945.1094010101 2484817.1195433326 107.67489595584955 22.450498814401154 +4756 9211 105.93 19.810000000000002 492666.6869812476 2191115.0490198773 +9211 4326 492666.6869812476 2191115.0490198773 105.93186758087887 19.809008209894483 +4756 9211 104.94 17.209000000000003 387256.80999709724 1903555.503628997 +9211 4326 387256.80999709724 1903555.503628997 104.94184146337513 17.208008348752042 +4756 9211 106.92 22.411 594718.4305500255 2479353.7705319044 +9211 4326 594718.4305500255 2479353.7705319044 106.9218978912275 22.410007558890364 +4756 9212 106.92 14.26 572294.8822814242 1577058.5825047803 +9212 4326 572294.8822814242 1577058.5825047803 106.92181006664725 14.258983944998358 +4756 9212 106.461 11.596 523009.8566128294 1282273.978422731 +9212 4326 523009.8566128294 1282273.978422731 106.46279198760189 11.594983628656122 +4756 9212 107.379 16.924 620265.3342106255 1872054.0966668348 +9212 4326 620265.3342106255 1872054.0966668348 107.38083239589542 16.922985228238183 +4756 9213 105.84 22.035 431869.441307009 2437579.5017804108 +9213 4326 431869.441307009 2437579.5017804108 105.84189579308514 22.03401859615865 +4756 9213 105.471 21.3915 393302.35296985775 2366536.4314717147 +9213 4326 393302.35296985775 2366536.4314717147 105.47288818422726 21.390519895807614 +4756 9213 106.209 22.6785 470098.6542432453 2508712.482885218 +9213 4326 470098.6542432453 2508712.482885218 106.21090363130511 22.677517187893493 +4756 9214 107.06 18.81 506324.024733235 2080429.84478141 +9214 4326 506324.024733235 2080429.84478141 107.06185302703459 18.808993175992974 +4756 9214 105.99799999999999 16.272 392903.9152786719 1799825.217403198 +9214 4326 392903.9152786719 1799825.217403198 105.99983009769907 16.270995896424463 +4756 9214 108.122 21.348 616376.1216600086 2361785.8653664426 +9214 4326 616376.1216600086 2361785.8653664426 108.12387968349904 21.346989747091655 +4756 9215 106.73 21.895 446269.0810268835 2422022.581063429 +9215 4326 446269.0810268835 2422022.581063429 106.7318915099018 21.894007652214924 +4756 9215 106.154 21.3775 386343.66092847637 2365033.267255149 +9215 4326 386343.66092847637 2365033.267255149 106.15588634316364 21.376512094139105 +4756 9215 107.30600000000001 22.412499999999998 505765.2315028921 2479230.9955453696 +9215 4326 505765.2315028921 2479230.9955453696 107.30789667660254 22.411503002101696 +4756 9216 107.94 14.67 547389.6779124424 1622359.8798504805 +9216 4326 547389.6779124424 1622359.8798504805 107.94181013044117 14.668976589863334 +4756 9216 107.39099999999999 13.995 488225.2116072414 1547640.378023876 +9216 4326 488225.2116072414 1547640.378023876 107.39280649950787 13.99397997339235 +4756 9216 108.489 15.344999999999999 606188.9179068008 1697236.8004779697 +9216 4326 606188.9179068008 1697236.8004779697 108.49081386764794 15.343972971610325 +4756 9217 109.03999999999999 12.98 585703.7018117933 1435487.36522366 +9217 4326 585703.7018117933 1435487.36522366 109.0417927816803 12.978967279911123 +4756 9217 108.599 11.423 538082.3524055448 1263153.3465573983 +9217 4326 538082.3524055448 1263153.3465573983 108.60078393064654 11.421970216155607 +4756 9217 109.481 14.537 632670.9390834058 1607957.274167095 +9217 4326 632670.9390834058 1607957.274167095 109.48280296063027 14.535964408711585 +4756 9218 108.36 12.52 484784.9841077959 1384474.7101344003 +9218 4326 484784.9841077959 1384474.7101344003 108.36179215465748 12.518971853686342 +4756 9218 107.316 10.639 370452.94022833725 1176664.068411469 +9218 4326 370452.94022833725 1176664.068411469 107.31778358511505 10.637977846198961 +4756 9218 109.404 14.401 597484.8236695603 1592744.747020578 +9218 4326 597484.8236695603 1592744.747020578 109.40580218411796 14.399964953716403 +4148 9221 24.935000000000002 -30.15 1393798.4563071479 1283207.7736053288 +9221 4326 1393798.4563071479 1283207.7736053288 24.935000000000006 -30.150000000000002 +4148 9221 14.4905 -37.368 471825.60635154974 434676.67720338504 +9221 4326 471825.60635154974 434676.67720338504 14.4905 -37.368 +4148 9221 35.3795 -22.932000000000002 2461116.997455813 2040973.9811402983 +9221 4326 2461116.997455813 2040973.9811402983 35.3795 -22.931999999999995 +4148 9222 37.78 -46.79 728684.2334881447 746033.4322129877 +9222 4326 728684.2334881447 746033.4322129877 37.78 -46.790000000000006 +4148 9222 33.217 -49.967 428450.650942557 360207.9469795022 +9222 4326 428450.650942557 360207.9469795022 33.217000000000006 -49.967 +4148 9222 42.343 -43.613 1067517.7388061336 1117797.9719107682 +9222 4326 1067517.7388061336 1117797.9719107682 42.343 -43.61300000000001 +9248 9249 -71.89 -51.165 1507694.6930197156 4331724.22075312 +9249 4326 1507694.6930197156 4331724.22075312 -71.8923203434814 -51.16430839527608 +9248 9249 -73.141 -51.9165 1421490.1260220478 4247500.691128678 +9249 4326 1421490.1260220478 4247500.691128678 -73.14339709716693 -51.91580917292772 +9248 9249 -70.639 -50.4135 1596741.368431325 4414447.501179161 +9249 4326 1596741.368431325 4414447.501179161 -70.6412453524607 -50.41280721945601 +9248 9250 -69.4 -51.379999999999995 2472149.6225583814 4307733.638047268 +9250 4326 2472149.6225583814 4307733.638047268 -69.40225273171569 -51.37936450496108 +9248 9250 -70.39 -52.325 2405228.1896069734 4201750.722257689 +9250 4326 2405228.1896069734 4201750.722257689 -70.39233280341082 -52.32437875095329 +9248 9250 -68.41 -50.434999999999995 2541919.460109237 4412774.989730486 +9250 4326 2541919.460109237 4412774.989730486 -68.41217583868735 -50.434350310161214 +9251 9252 -66.185 -53.85 2685245.161710862 4029264.3827369674 +9252 4326 2685245.161710862 4029264.3827369674 -66.18437801989403 -53.84932823105707 +9251 9252 -68.3945 -54.984 2538765.512047433 3906538.8280291697 +9252 4326 2538765.512047433 3906538.8280291697 -68.39393110658392 -54.98338174373653 +9251 9252 -63.9755 -52.716 2839440.77404577 4147294.5078872186 +9252 4326 2839440.77404577 4147294.5078872186 -63.97482838871102 -52.71527496029554 +9253 9254 -66.185 -53.85 2685245.161710862 4029264.3827369674 +9254 4326 2685245.161710862 4029264.3827369674 -66.18546230797178 -53.84941378702204 +9253 9254 -68.3945 -54.984 2538765.512047433 3906538.8280291697 +9254 4326 2538765.512047433 3906538.8280291697 -68.39504985859057 -54.98344713940583 +9253 9254 -63.9755 -52.716 2839440.77404577 4147294.5078872186 +9254 4326 2839440.77404577 4147294.5078872186 -63.97587887498269 -52.715379984949415 +5340 9265 -67.31 -53.129999999999995 613070.9924550846 4111933.6602944406 +9265 4326 613070.9924550846 4111933.6602944406 -67.31000300028919 -53.13000607467736 +5340 9265 -68.489 -54.461999999999996 533123.6932541344 3964953.814713777 +9265 4326 533123.6932541344 3964953.814713777 -68.4890032810754 -54.46200605509067 +5340 9265 -66.131 -51.798 697824.3021880249 4257534.8274480635 +9265 4326 697824.3021880249 4257534.8274480635 -66.13100273670712 -51.798006088198164 +4312 9271 10.684999999999999 47.19 26648.147619022297 5227897.564097625 +9271 4326 26648.147619022297 5227897.564097625 10.684642612005641 47.1894402796731 +4312 9271 9.6455 46.812000000000005 -52490.619535398226 5186049.418390765 +9271 4326 -52490.619535398226 5186049.418390765 9.645306910978896 46.811456190799966 +4312 9271 11.724499999999999 47.568 104666.42295480872 5270796.171989164 +9271 4326 104666.42295480872 5270796.171989164 11.723976524772661 47.56742565036281 +4312 9272 13.335 47.595 125.33069089944287 5272859.809756746 +9272 4326 125.33069089944287 5272859.809756746 13.33425411679331 47.59447105940481 +4312 9272 11.9805 46.5195 -103796.77543417958 5154196.997281072 +9272 4326 -103796.77543417958 5154196.997281072 11.98000748809481 46.5190558844523 +4312 9272 14.689499999999999 48.6705 99872.7447774754 5393321.962378488 +9272 4326 99872.7447774754 5393321.962378488 14.688491119914028 48.669888237644024 +4312 9273 16.0 47.790000000000006 -24972.853214554038 5294592.12501042 +9273 4326 -24972.853214554038 5294592.12501042 15.998878186169419 47.78953545177158 +4312 9273 14.947000000000001 46.683 -106047.5007568458 5172414.42582348 +9273 4326 -106047.5007568458 5172414.42582348 14.946099631750625 46.68262935756291 +4312 9273 17.053 48.897000000000006 52761.19126411724 5417869.216581406 +9273 4326 52761.19126411724 5417869.216581406 17.051647645337514 48.8964426995179 +4161 9284 -72.045 -47.64 1496618.4035849858 4723784.491795317 +9284 4326 1496618.4035849858 4723784.491795317 -72.04792782040727 -47.638649429621076 +4161 9284 -73.4355 -50.07 1397227.2367908105 4452555.879723924 +9284 4326 1397227.2367908105 4452555.879723924 -73.43859868750194 -50.06869164352761 +4161 9284 -70.6545 -45.21 1605705.0310820497 4993031.382859972 +9284 4326 1605705.0310820497 4993031.382859972 -70.65727553317528 -45.20861097516421 +4161 9285 -66.485 -46.315 3462642.3387968857 4870976.866104535 +9285 4326 3462642.3387968857 4870976.866104535 -66.48774447629606 -46.3137453812524 +4161 9285 -67.3985 -48.7765 3397208.6244509523 4596465.063059483 +9285 4326 3397208.6244509523 4596465.063059483 -67.40139721921656 -48.7753044332155 +4161 9285 -65.5715 -43.8535 3534454.411183586 5144565.685163153 +9285 4326 3534454.411183586 5144565.685163153 -65.57410906910799 -43.85219117716295 +9294 9295 53.0 17.305 712574.897789995 1914400.0945327408 +9295 4326 712574.897789995 1914400.0945327408 52.99999244148888 17.30498919636455 +9294 9295 52.091 15.176499999999999 617200.0386551527 1678139.971911333 +9295 4326 617200.0386551527 1678139.971911333 52.09099198359472 15.176488996704311 +9294 9295 53.909 19.433500000000002 805473.2431298404 2151376.0437958874 +9295 4326 805473.2431298404 2151376.0437958874 53.90899289870486 19.433489437427372 +9294 9296 57.004999999999995 20.535 500521.2364145107 2270686.217731219 +9296 4326 500521.2364145107 2270686.217731219 57.00499347621228 20.53498919312375 +9294 9296 54.3005 14.9505 209623.16259984206 1654616.7980691977 +9296 4326 209623.16259984206 1654616.7980691977 54.30049217629067 14.950488594588803 +9294 9296 59.7095 26.1195 770947.7828167134 2891739.6443194244 +9296 4326 770947.7828167134 2891739.6443194244 59.709494797264014 26.119490074809033 +9294 9297 61.69 20.23 363158.14818984084 2237474.446515913 +9297 4326 363158.14818984084 2237474.446515913 61.68999408884117 20.22998839496157 +9294 9297 60.169 16.756 198168.87878652924 1854713.881113435 +9297 4326 198168.87878652924 1854713.881113435 60.16899322823225 16.755987912488717 +9294 9297 63.211 23.704 521509.87091534515 2621471.7720680675 +9297 4326 521509.87091534515 2621471.7720680675 63.21099495285665 23.70398898118465 +9299 9300 -1.375 52.375 207385.83677778576 383417.2736250177 +9299 9300 -2.6125 51.542500000000004 121693.39703930172 291367.28279628296 +9299 9300 -0.13750000000000018 53.207499999999996 289905.8995494461 476919.31450531934 +4326 9354 0.0 -70.0 0.0 2150307.5575983347 +4326 9354 -162.0 -88.0 -65805.21387394598 -202527.6234001401 +4326 9354 162.0 -52.0 1296262.0814411377 -3989484.4679159154 +9333 9356 35.224999999999994 27.15 720492.7040521107 3005003.5726890224 +9356 4326 720492.7040521107 3005003.5726890224 35.224998054565866 27.14999847859519 +9333 9356 34.518499999999996 25.143 653065.0968453363 2781644.0151644135 +9356 4326 653065.0968453363 2781644.0151644135 34.5184979861434 25.142998500284627 +9333 9356 35.9315 29.157 785161.1861426497 3228936.2303633266 +9356 4326 785161.1861426497 3228936.2303633266 35.93149812402782 29.156998457186642 +9333 9357 39.0 24.224999999999998 500000.00000000064 2679137.535683546 +9357 4326 500000.00000000064 2679137.535683546 38.99999789679887 24.22499836312981 +9333 9357 36.3 17.0835 212642.23056858336 1890782.31351636 +9357 4326 212642.23056858336 1890782.31351636 36.29999768153243 17.083498442946716 +9333 9357 41.7 31.366499999999995 756811.1555608156 3473371.536976828 +9357 4326 756811.1555608156 3473371.536976828 41.69999811577887 31.36649828763412 +9333 9358 44.995000000000005 23.75 499490.467028527 2626548.518286022 +9358 4326 499490.467028527 2626548.518286022 44.994997801843596 23.74999819559899 +9333 9358 42.2905 17.09 211640.5379965359 1891516.0928051912 +9358 4326 211640.5379965359 1891516.0928051912 42.29049762948704 17.089998267188577 +9333 9358 47.6995 30.409999999999997 759330.7397447096 3367311.9163983064 +9358 4326 759330.7397447096 3367311.9163983064 47.69949797061176 30.409998128664775 +9333 9359 51.0 23.44 500000.0000000019 2592228.9955780194 +9359 4326 500000.0000000019 2592228.9955780194 50.99999770778957 23.439998047595854 +9333 9359 48.291000000000004 18.490000000000002 213934.9339567909 2046544.4754638572 +9359 4326 213934.9339567909 2046544.4754638572 48.290997609209334 18.489998110425784 +9333 9359 53.708999999999996 28.39 765445.1623114006 3143391.0293677365 +9359 4326 765445.1623114006 3143391.0293677365 53.70899779634146 28.389997989518235 +9333 9360 54.835 21.215 275278.1681571158 2347479.475925416 +9360 4326 275278.1681571158 2347479.475925416 54.83499759769833 21.214997963555188 +9333 9360 54.0835 19.8155 194462.6627620496 2193702.4581302837 +9360 4326 194462.6627620496 2193702.4581302837 54.0834975747348 19.81549797878759 +9333 9360 55.5865 22.6145 354723.8245380474 2501535.229214023 +9360 4326 354723.8245380474 2501535.229214023 55.58649761972122 22.614497948705324 +9364 9367 -2.24 53.61 203914.04668182752 419755.44747175457 +9364 9367 -3.0500000000000003 53.349000000000004 149976.98862556848 391005.73742963566 +9364 9367 -1.4300000000000002 53.870999999999995 257189.85869431053 449116.49648384645 +9372 9373 -0.865 52.44 48330.1173380955 107285.55992328037 +9372 9373 -1.7874999999999999 51.558 -15667.005496156387 9564.306593023939 +9372 9373 0.057499999999999885 53.322 109822.21447629895 205822.0589419175 +20046 9377 -75.82 5.640000000000001 4687714.042252737 2181965.4430747647 +9377 4326 4687714.042252737 2181965.4430747647 -75.82 5.64 +20046 9377 -83.875 -3.2430000000000003 3784973.5763168256 1193156.7685326128 +9377 4326 3784973.5763168256 1193156.7685326128 -83.875 -3.2430000000000003 +20046 9377 -67.765 14.523 5564495.635042322 3169455.8956396114 +9377 4326 5564495.635042322 3169455.8956396114 -67.765 14.522999999999996 +9384 9387 -3.205 57.405 155528.14955520857 115782.55336495768 +9384 9387 -4.1995 57.1305 95300.935365865 85656.38253555889 +9384 9387 -2.2105 57.679500000000004 214861.28897138181 146784.27245720616 +7798 9391 27.005000000000003 42.695 500409.5517170227 4726945.9824498 +9391 4326 500409.5517170227 4726945.9824498 27.005000000000003 42.695 +7798 9391 24.3005 41.3855 274285.1909772019 4585069.428936362 +9391 4326 274285.1909772019 4585069.428936362 24.3005 41.38549999999999 +7798 9391 29.709500000000002 44.0045 717218.2653763022 4875942.1279491065 +9391 4326 717218.2653763022 4875942.1279491065 29.709500000000006 44.0045 +9403 9404 -18.11 27.735 784925.2469177078 3071236.5937412465 +9404 4326 784925.2469177078 3071236.5937412465 -18.111854656505308 27.73644171058722 +9403 9404 -18.209 27.6045 775487.8299735991 3056545.5354374032 +9404 4326 775487.8299735991 3056545.5354374032 -18.210857058304967 27.605938976510984 +9403 9404 -18.011 27.865499999999997 794338.3130039473 3085937.1794644073 +9404 4326 794338.3130039473 3085937.1794644073 -18.012852253056387 27.866944455662175 +9403 9405 -15.715 28.525 430039.08348463126 3155614.8531968594 +9405 4326 430039.08348463126 3155614.8531968594 -15.716754311342198 28.526482254401493 +9403 9405 -17.825499999999998 27.6745 221282.90387746738 3064382.3916019504 +9405 4326 221282.90387746738 3064382.3916019504 -17.827340364366634 27.675944848578055 +9403 9405 -13.6045 29.3755 635440.2403357886 3250444.95539919 +9405 4326 635440.2403357886 3250444.95539919 -13.606164855252365 29.37701968025402 +4728 9406 -18.11 27.735 784925.2469177078 3071236.5937412465 +9406 4326 784925.2469177078 3071236.5937412465 -18.111854656505308 27.73644171058722 +4728 9406 -18.209 27.6045 775487.8299735991 3056545.5354374032 +9406 4326 775487.8299735991 3056545.5354374032 -18.210857058304967 27.605938976510984 +4728 9406 -18.011 27.865499999999997 794338.3130039473 3085937.1794644073 +9406 4326 794338.3130039473 3085937.1794644073 -18.012852253056387 27.866944455662175 +4728 9407 -17.07 28.24 296891.8784567069 3125569.587744983 +9407 4326 296891.8784567069 3125569.587744983 -17.071814337881353 28.241461185616114 +4728 9407 -17.961 27.645999999999997 207834.1915350798 3061536.881709019 +9407 4326 207834.1915350798 3061536.881709019 -17.962846211060445 27.64744273570585 +4728 9407 -16.179 28.834 384973.2075238591 3190210.831204027 +9407 4326 384973.2075238591 3190210.831204027 -16.180781864301974 28.835479874322758 +9453 9456 -4.35 55.75 93720.394 113870.493 +9453 9456 -4.62 55.57 76687.80681899945 93863.18066853675 +9453 9456 -4.08 55.93 110596.8552690218 133944.4369533475 +7844 9473 133.26999999999998 -26.78 124755.68441287114 -2902763.199199531 +9473 4326 124755.68441287114 -2902763.199199531 133.26999999999998 -26.78 +7844 9473 114.892 -42.008 -1449883.0908134487 -4689803.131157475 +9473 4326 -1449883.0908134487 -4689803.131157475 114.892 -42.008 +7844 9473 151.648 -11.552 2180849.7695957995 -1383754.6914001105 +9473 4326 2180849.7695957995 -1383754.6914001105 151.648 -11.552000000000003 +9470 9476 94.005 3.895 611580.4614499211 430587.662232115 +9470 9476 92.2095 0.3895 412036.3514000601 43055.584914235646 +9470 9476 95.8005 7.4005 809166.70941382 818996.9276198028 +9470 9477 99.0 3.745 500000.0 413940.93706540315 +9470 9477 96.3 0.37450000000000006 199451.97758791543 41439.82046499721 +9470 9477 101.7 7.1155 798251.5827747089 787387.9897303757 +9470 9478 105.0 3.47 499999.99999999715 383543.9653706045 +9470 9478 102.3 0.34700000000000003 199451.07404461666 38396.84239069991 +9470 9478 107.7 6.593 798577.0214658263 729564.9950449365 +9470 9479 111.0 3.685 500000.00000000425 407308.85470879084 +9470 9479 108.3 0.36850000000000005 199451.77457679214 40775.897963733085 +9470 9479 113.7 7.0015 798324.6936129571 774771.9798608653 +9470 9480 117.0 2.185 500000.0 241509.32604739987 +9470 9480 114.3 0.21850000000000003 199447.7648931467 24177.837404515525 +9470 9480 119.7 4.1514999999999995 799769.8755467529 459385.1595616792 +9470 9481 123.0 2.74 500000.00000000285 302854.64359800844 +9470 9481 120.3 0.274 199449.0096387621 30319.119415552566 +9470 9481 125.7 5.206 799321.0031112172 576075.5841922571 +9470 9482 129.0 3.34 500000.00000000285 369174.55031701503 +9470 9482 126.3 0.334 199450.67089087534 36958.3437133856 +9470 9482 131.7 6.345999999999999 798722.2659477657 702230.8196814049 +9470 9487 99.0 -4.43 500000.0 9510342.109385315 +9470 9487 96.3 -8.417 202666.30642037274 9068575.632148456 +9470 9487 101.7 -0.4429999999999996 800545.4723105452 9950980.397114987 +9470 9488 105.005 -5.365 500553.9536958637 9406988.927061724 +9470 9488 102.3005 -10.1935 204220.35359973076 8871959.539162327 +9470 9488 107.7095 -0.5365000000000002 801599.5545456714 9940633.800820835 +9470 9489 111.0 -6.035 500000.00000000425 9332926.454039818 +9470 9489 108.3 -11.4665 205414.0382586188 8731058.045834707 +9470 9489 113.7 -0.6035000000000004 800537.8235501552 9933220.46438451 +9470 9490 117.005 -6.53 500552.7892346102 9278207.495387472 +9470 9490 114.3005 -12.407 206483.87263161637 8626954.010963067 +9470 9490 119.7095 -0.6530000000000005 801593.2217325555 9927742.530640636 +9470 9491 123.005 -6.97 500552.2903320688 9229567.525173506 +9470 9491 120.3005 -13.251999999999999 207463.19545093627 8533415.656963557 +9470 9491 125.7095 -0.6880000000000006 801591.076861578 9923869.615981659 +9470 9492 129.0 -4.725 500000.0000000028 9477733.659548106 +9470 9492 126.3 -8.9775 203108.6672598187 9006543.154231394 +9470 9492 131.7 -0.47250000000000014 800544.2424421762 9947716.11092834 +9470 9493 135.005 -5.03 500554.2464370432 9444019.54515837 +9470 9493 132.3005 -9.557 203650.68370084552 8942406.77438083 +9470 9493 137.7095 -0.5030000000000001 801601.1459386016 9944340.731845351 +9470 9494 139.73000000000002 -5.42 359297.03318262857 9400761.926005661 +9470 9494 138.173 -10.298 190341.53170711617 8860272.786072135 +9470 9494 141.287 -0.5419999999999998 531934.6294746828 9940091.94046816 +5340 9498 -58.415 -34.605000000000004 24431.066643904716 72690.92737484179 +9498 4326 24431.066643904716 72690.92737484179 -58.41500118107089 -34.605005702325734 +5340 9498 -58.527499999999996 -34.6995 14118.717569949098 62206.828810000436 +9498 4326 14118.717569949098 62206.828810000436 -58.52750119540956 -34.69950570559979 +5340 9498 -58.3025 -34.5105 34766.78944969042 83163.36900616114 +9498 4326 34766.78944969042 83163.36900616114 -58.302501166763314 -34.510505699018736 +9547 9549 6.265000000000001 45.379999999999995 106785.0235145053 72005.2346979715 +9547 9549 4.8115000000000006 44.921 -8309.422056257987 22781.680279010172 +9547 9549 7.7185 45.839 220082.7735260349 123272.3866051995 +4269 9674 -120.63 45.515 550820.6852091048 1272374.5047888714 +9674 4326 550820.6852091048 1272374.5047888714 -120.63000000000001 45.515000000000015 +4269 9674 -124.37400000000001 42.333499999999994 239491.10477273073 928335.8141290998 +9674 4326 239491.10477273073 928335.8141290998 -124.37400000000001 42.33349999999999 +4269 9674 -116.886 48.6965 829327.4721564035 1630473.2832614158 +9674 4326 829327.4721564035 1630473.2832614158 -116.886 48.696499999999965 +4682 9678 90.34 23.58 534688.3713121953 607553.5678988174 +9678 4326 534688.3713121953 607553.5678988174 90.33717773112295 23.580722793728786 +4682 9678 88.24300000000001 20.826 317190.0573409684 303699.4587135655 +9678 4326 317190.0573409684 303699.4587135655 88.24049223084691 20.826914789103768 +4682 9678 92.437 26.334 743206.084410262 914724.4376907675 +9678 4326 743206.084410262 914724.4376907675 92.43384703459026 26.334532840306636 +4326 9680 90.34 22.6 534945.6863011605 2499280.837905281 +4326 9680 88.24300000000001 18.964 315013.18209302146 2097766.083420849 +4326 9680 92.437 26.236 743444.9294899238 2904109.4134905105 +9696 9697 -109.33500000000001 -27.130000000000003 665016.567489218 6998072.364323039 +9696 9697 -109.4925 -27.238 649260.8115704275 6986304.776188426 +9696 9697 -109.1775 -27.022000000000002 680802.5093375971 7009821.3116214955 +9696 9698 -74.995 -38.855000000000004 500433.8410580684 5699314.0104445405 +9696 9698 -77.6995 -57.3095 337399.9150358017 3644936.4607056994 +9696 9698 -72.2905 -20.4005 782784.4819780085 7741866.394019629 +9696 9699 -69.0 -38.685 500000.0 5718178.35995209 +9696 9699 -71.7 -57.7515 339326.61691853346 3595752.8485343764 +9696 9699 -66.3 -19.618500000000004 783190.0307251445 7828493.0824960265 +4617 9709 -45.0 47.16 500000.0 5222944.579285258 +9709 4326 500000.0 5222944.579285258 -44.999991613286085 47.1600096949268 +4617 9709 -47.7 40.581 271480.82296725106 4495749.832928928 +9709 4326 271480.82296725106 4495749.832928928 -47.69999267313484 40.58100856766685 +4617 9709 -42.3 53.739 678069.2260187293 5957867.468179671 +9709 4326 678069.2260187293 5957867.468179671 -42.299990223051324 53.73901054351908 +4269 9712 -41.364999999999995 47.53 321989.62909671717 5266774.525326466 +4269 9712 -41.9365 45.730000000000004 271519.0094015377 5068243.702103879 +4269 9712 -40.793499999999995 49.33 369690.89929869404 5465688.514009107 +4617 9713 -41.364999999999995 47.53 321989.62909671717 5266774.525326466 +9713 4326 321989.62909671717 5266774.525326466 -41.364990361505804 47.5300096682034 +4617 9713 -41.9365 45.730000000000004 271519.0094015377 5068243.702103879 +9713 4326 271519.0094015377 5068243.702103879 -41.936490647712986 45.730009400574865 +4617 9713 -40.793499999999995 49.33 369690.89929869404 5465688.514009107 +9713 4326 369690.89929869404 5465688.514009107 -40.79349004946634 49.33000991587495 +4670 9716 18.49 38.2 280211.8499703541 4230984.037387538 +9716 4326 280211.8499703541 4230984.037387538 18.49 38.199999999999996 +4670 9716 18.04 35.104 230196.3330072955 3888586.2343832804 +9716 4326 230196.3330072955 3888586.2343832804 18.04 35.10399999999999 +4670 9716 18.939999999999998 41.296 327523.36855843413 4573663.423681971 +9716 4326 327523.36855843413 4573663.423681971 18.939999999999998 41.296 +9739 9741 -2.75 56.379999999999995 74996.927 136848.80337643728 +9739 9741 -3.479 55.632999999999996 29082.965983557027 53917.48303281775 +9739 9741 -2.021 57.127 119148.33761605129 220267.22953054332 +6318 9748 -85.84 32.995 654122.6039996601 907612.1671396511 +9748 4326 654122.6039996601 907612.1671396511 -85.84 32.995000000000005 +6318 9748 -86.69500000000001 31.1905 386721.82047379616 252198.67133800645 +9748 4326 386721.82047379616 252198.67133800645 -86.69500000000001 31.1905 +6318 9748 -84.985 34.7995 910854.8627122245 1565340.9511231089 +9748 4326 910854.8627122245 1565340.9511231089 -84.985 34.7995 +6318 9749 -87.39 32.58 2002383.1218804042 938458.1192195431 +9749 4326 2002383.1218804042 938458.1192195431 -87.39 32.580000000000005 +6318 9749 -88.37100000000001 30.384 1693861.0475862161 140707.014245571 +9749 4326 1693861.0475862161 140707.014245571 -88.37100000000001 30.383999999999997 +6318 9749 -86.40899999999999 34.776 2296128.9779858883 1739288.8042518352 +9749 4326 2296128.9779858883 1739288.8042518352 -86.40899999999999 34.776 +9758 9761 -1.6 55.075 109049.11607526963 405098.0027770547 +9758 9761 -1.8699999999999999 54.8725 91699.88063422605 382600.8623419146 +9758 9761 -1.33 55.277499999999996 126224.21495008752 427662.59384336305 +9763 9766 -0.895 51.97 192863.5910781111 149168.02427301442 +9763 9766 -1.3765 51.727000000000004 159596.2920541182 122238.22635087513 +9763 9766 -0.4135000000000002 52.213 225772.9175138608 176317.97311043882 +9777 9793 0.26000000000000156 46.355000000000004 489353.58775158145 6587552.204045072 +9793 4326 489353.58775158145 6587552.204045072 0.2600000000000025 46.35500000000001 +9777 9793 -8.847999999999999 41.6705 -285448.9014183822 6137327.142426368 +9793 4326 -285448.9014183822 6137327.142426368 -8.847999999999999 41.67049999999999 +9777 9793 9.368000000000002 51.039500000000004 1147159.6761796405 7122909.254414408 +9793 4326 1147159.6761796405 7122909.254414408 9.368000000000004 51.03950000000001 +9782 9794 0.26000000000000156 46.355000000000004 489353.58775158145 6587552.204045072 +9794 4326 489353.58775158145 6587552.204045072 0.2600000000000025 46.35500000000001 +9782 9794 -8.847999999999999 41.6705 -285448.9014183822 6137327.142426368 +9794 4326 -285448.9014183822 6137327.142426368 -8.847999999999999 41.67049999999999 +9782 9794 9.368000000000002 51.039500000000004 1147159.6761796405 7122909.254414408 +9794 4326 1147159.6761796405 7122909.254414408 9.368000000000004 51.03950000000001 +5561 9821 30.71 50.36 314943.62449326896 5581009.95009136 +9821 4326 314943.62449326896 5581009.95009136 30.708365810734314 50.3598729585452 +5561 9821 29.405 49.289 220340.86959949107 5462441.15903394 +9821 4326 220340.86959949107 5462441.15903394 29.403389026159324 49.288840738432 +5561 9821 32.01499999999999 51.431 405360.19661393634 5701224.8487996645 +9821 4326 405360.19661393634 5701224.8487996645 32.013341804130526 51.430905605807865 +9777 9822 4.285 42.19 1806133.464641465 1221898.9166579738 +9822 4326 1806133.464641465 1221898.9166579738 4.284999999999999 42.190000000000005 +9777 9822 -0.5255 41.398 1405265.0577709535 1139210.4191105175 +9822 4326 1405265.0577709535 1139210.4191105175 -0.5254999999999994 41.39800000000002 +9777 9822 9.0955 42.982 2196788.6567191663 1326769.65018143 +9822 4326 2196788.6567191663 1326769.65018143 9.0955 42.981999999999985 +9777 9823 2.9300000000000006 43.17 1694308.3555044443 2218886.832639388 +9823 4326 1694308.3555044443 2218886.832639388 2.93 43.17 +9777 9823 -1.318 42.414 1344746.925290598 2144039.2831746098 +9823 4326 1344746.925290598 2144039.2831746098 -1.3179999999999987 42.414 +9777 9823 7.178 43.925999999999995 2035394.4909423827 2311217.6582577066 +9823 4326 2035394.4909423827 2311217.6582577066 7.177999999999999 43.926 +9777 9824 2.96 43.96 1696789.8710242882 3195556.6834046254 +9824 4326 1696789.8710242882 3195556.6834046254 2.96 43.96 +9777 9824 -1.315 43.024 1348427.416655981 3100766.4661240424 +9824 4326 1348427.416655981 3100766.4661240424 -1.3149999999999986 43.024 +9777 9824 7.235 44.896 2034385.148431069 3308146.661596619 +9824 4326 2034385.148431069 3308146.661596619 7.2349999999999985 44.89599999999998 +9777 9825 3.125 45.0 1709855.0089266587 4200007.601723443 +9825 4326 1709855.0089266587 4200007.601723443 3.1250000000000013 45.00000000000001 +9777 9825 -1.0015 44.1 1379712.718559944 4107904.098701538 +9825 4326 1379712.718559944 4107904.098701538 -1.0014999999999992 44.09999999999999 +9777 9825 7.251499999999999 45.9 2029789.0888838554 4308676.189895353 +9825 4326 2029789.0888838554 4308676.189895353 7.2515 45.9 +9777 9826 2.4750000000000005 46.0 1659335.5353398635 5200134.0201074025 +9826 4326 1659335.5353398635 5200134.0201074025 2.475000000000001 46.0 +9777 9826 -1.7414999999999998 45.1 1327005.0597660886 5111081.736375026 +9826 4326 1327005.0597660886 5111081.736375026 -1.7414999999999992 45.1 +9777 9826 6.6915000000000004 46.9 1981194.0581817513 5306557.143128601 +9826 4326 1981194.0581817513 5306557.143128601 6.6915 46.900000000000006 +9777 9827 1.4299999999999997 47.0 1580610.273100607 6201196.378340442 +9827 4326 1580610.273100607 6201196.378340442 1.4299999999999988 47.0 +9777 9827 -4.1499999999999995 46.1 1147882.6448475446 6125171.695773245 +9827 4326 1147882.6448475446 6125171.695773245 -4.1499999999999995 46.1 +9777 9827 7.009999999999998 47.9 1999705.9347921829 6307729.436931695 +9827 4326 1999705.9347921829 6307729.436931695 7.009999999999999 47.89999999999999 +9777 9828 1.6800000000000006 48.0 1601507.759418362 7200843.179309284 +9828 4326 1601507.759418362 7200843.179309284 1.6800000000000022 47.99999999999999 +9777 9828 -4.215 47.1 1153059.3437116276 7125552.062888663 +9828 4326 1153059.3437116276 7125552.062888663 -4.215000000000001 47.099999999999994 +9777 9828 7.575 48.9 2035246.468431578 7310024.549210299 +9828 4326 2035246.468431578 7310024.549210299 7.574999999999999 48.89999999999999 +9777 9829 1.6800000000000006 49.0 1603426.3502429728 8200839.6210153755 +9829 4326 1603426.3502429728 8200839.6210153755 1.6799999999999995 49.000000000000014 +9777 9829 -4.215 48.1 1163408.0042460614 8125441.575353767 +9829 4326 1163408.0042460614 8125441.575353767 -4.215 48.100000000000016 +9777 9829 7.575 49.9 2028501.4374461283 8309993.614527015 +9829 4326 2028501.4374461283 8309993.614527015 7.574999999999999 49.900000000000006 +9777 9830 3.025 50.07 1701789.6383209554 9207785.717349734 +9830 4326 1701789.6383209554 9207785.717349734 3.025 50.06999999999999 +9777 9830 -1.5244999999999997 49.107 1369833.7253760097 9110674.247290418 +9830 4326 1369833.7253760097 9110674.247290418 -1.5244999999999995 49.107 +9777 9830 7.5745000000000005 51.033 2020716.1643811618 9324717.284941081 +9830 4326 2020716.1643811618 9324717.284941081 7.574500000000002 51.03300000000002 +5561 9831 34.56 45.31 304705.3088593588 5019486.423618314 +9831 4326 304705.3088593588 5019486.423618314 34.558555719070455 45.309856044389974 +5561 9831 32.679 44.473 155106.9407975781 4928080.9157965565 +9831 4326 155106.9407975781 4928080.9157965565 32.67755712274834 44.47282081047819 +5561 9831 36.440999999999995 46.147000000000006 449958.9882223526 5114347.520255098 +9831 4326 449958.9882223526 5114347.520255098 36.43955553838678 46.146891633486156 +5561 9832 28.695 48.975 302074.27277439873 5426943.262462542 +9832 4326 302074.27277439873 5426943.262462542 28.6933927752978 48.97482682190458 +5561 9832 27.5025 48.1515 213377.51804625185 5336022.649349511 +9832 4326 213377.51804625185 5336022.649349511 27.50090863910594 48.15129988951006 +5561 9832 29.8875 49.798500000000004 387894.21251944714 5519247.077364774 +9832 4326 387894.21251944714 5519247.077364774 29.885876747515475 49.79835407387025 +5561 9833 24.855 51.125 301516.884542319 5666091.687983271 +9833 4326 301516.884542319 5666091.687983271 24.85328766124502 51.124798520079175 +5561 9833 23.7255 50.3645 221174.7808320822 5582076.338560804 +9833 4326 221174.7808320822 5582076.338560804 23.72380744838891 50.364273002960395 +5561 9833 25.9845 51.8855 379261.4442455756 5751331.067378957 +9833 4326 379261.4442455756 5751331.067378957 25.982767580282143 51.88532432806475 +5561 9834 34.945 48.32 295920.98432501504 5354105.1927955225 +9834 4326 295920.98432501504 5354105.1927955225 34.94347726165488 48.31990304917168 +5561 9834 33.1495 47.537000000000006 160674.50296561877 5268701.382927287 +9834 4326 160674.50296561877 5268701.382927287 33.14798015635711 47.536868439690366 +5561 9834 36.7405 49.102999999999994 427092.03733378276 5442637.411448943 +9834 4326 427092.03733378276 5442637.411448943 36.738975504414356 49.10293790469504 +5561 9835 37.82 48.05 323857.41379096196 5324130.40821461 +9835 4326 323857.41379096196 5324130.40821461 37.818520535466135 48.04993673562869 +5561 9835 36.668 46.979 236695.71492458432 5205340.155987544 +9835 4326 236695.71492458432 5205340.155987544 36.66653603450224 46.97890644718985 +5561 9835 38.972 49.121 407447.8350003505 5444223.603473037 +9835 4326 407447.8350003505 5444223.603473037 38.970504458594405 49.12096735839862 +5561 9836 28.465 50.629999999999995 297523.5511093392 5611024.37692859 +9836 4326 297523.5511093392 5611024.37692859 28.46333494497022 50.62984463812736 +5561 9836 27.317500000000003 49.684999999999995 214666.68722030064 5506579.154416396 +9836 4326 214666.68722030064 5506579.154416396 27.315857625553075 49.68481640144207 +5561 9836 29.612499999999997 51.575 377125.8948393208 5716743.687625636 +9836 4326 377125.8948393208 5716743.687625636 29.610811551363508 51.57487322997823 +5561 9837 23.38 48.495000000000005 291130.8115839372 5373570.711394519 +9837 4326 291130.8115839372 5373570.711394519 23.37836842342908 48.49474623857662 +5561 9837 22.255 47.9505 207001.7851583039 5313767.574156322 +9837 4326 207001.7851583039 5313767.574156322 22.253379118003036 47.95022409593002 +5561 9837 24.505 49.039500000000004 373480.18190992833 5434602.716201723 +9837 4326 373480.18190992833 5434602.716201723 24.50335798297281 49.03926859402672 +5561 9838 35.71 47.11 277988.70866766124 5219608.666981202 +9838 4326 277988.70866766124 5219608.666981202 35.70852097533723 47.10989600628808 +5561 9838 34.324 46.174 170577.6417661813 5116882.5300028995 +9838 4326 170577.6417661813 5116882.5300028995 34.32253067318187 46.17386503054524 +5561 9838 37.096000000000004 48.046 381717.491760605 5324217.366221906 +9838 4326 381717.491760605 5324217.366221906 37.09451140746456 48.04592732601492 +5561 9839 24.595 48.64 288576.6698396945 5389699.824606149 +9839 4326 288576.6698396945 5389699.824606149 24.59337159598304 48.63976513727775 +5561 9839 23.6365 47.812 216602.33582202758 5298217.916275376 +9839 4326 216602.33582202758 5298217.916275376 23.634891502805136 47.811741937554245 +5561 9839 25.5535 49.468 358241.3916077587 5482082.722650272 +9839 4326 358241.3916077587 5482082.722650272 25.551851141305807 49.467788654925656 +5561 9840 31.82 48.495000000000005 286696.2188396779 5373579.405922486 +9840 4326 286696.2188396779 5373579.405922486 31.818437692665565 48.49486354253787 +5561 9840 29.947999999999997 47.8155 146324.2007897861 5300046.114610315 +9840 4326 146324.2007897861 5300046.114610315 29.946440077815918 47.815329168356165 +5561 9840 33.692 49.1745 423373.05707299116 5450508.532673956 +9840 4326 423373.05707299116 5450508.532673956 33.69043670865352 49.17439813869845 +5561 9841 39.03 48.955 302197.16728523397 5424719.090185474 +9841 4326 302197.16728523397 5424719.090185474 39.02851022185756 48.95496564985621 +5561 9841 37.949999999999996 47.9335 221541.86762043947 5311660.707733827 +9841 4326 221541.86762043947 5311660.707733827 37.94852554268009 47.93343670561411 +5561 9841 40.11 49.9765 379621.54194173374 5538921.195479557 +9841 4326 379621.54194173374 5538921.195479557 40.10849431510718 49.97649486593825 +9782 9842 4.285 42.19 1806133.464641465 1221898.9166579738 +9842 4326 1806133.464641465 1221898.9166579738 4.284999999999999 42.190000000000005 +9782 9842 -0.5255 41.398 1405265.0577709535 1139210.4191105175 +9842 4326 1405265.0577709535 1139210.4191105175 -0.5254999999999994 41.39800000000002 +9782 9842 9.0955 42.982 2196788.6567191663 1326769.65018143 +9842 4326 2196788.6567191663 1326769.65018143 9.0955 42.981999999999985 +9782 9843 2.9300000000000006 43.17 1694308.3555044443 2218886.832639388 +9843 4326 1694308.3555044443 2218886.832639388 2.93 43.17 +9782 9843 -1.318 42.414 1344746.925290598 2144039.2831746098 +9843 4326 1344746.925290598 2144039.2831746098 -1.3179999999999987 42.414 +9782 9843 7.178 43.925999999999995 2035394.4909423827 2311217.6582577066 +9843 4326 2035394.4909423827 2311217.6582577066 7.177999999999999 43.926 +9782 9844 2.96 43.96 1696789.8710242882 3195556.6834046254 +9844 4326 1696789.8710242882 3195556.6834046254 2.96 43.96 +9782 9844 -1.315 43.024 1348427.416655981 3100766.4661240424 +9844 4326 1348427.416655981 3100766.4661240424 -1.3149999999999986 43.024 +9782 9844 7.235 44.896 2034385.148431069 3308146.661596619 +9844 4326 2034385.148431069 3308146.661596619 7.2349999999999985 44.89599999999998 +9782 9845 3.125 45.0 1709855.0089266587 4200007.601723443 +9845 4326 1709855.0089266587 4200007.601723443 3.1250000000000013 45.00000000000001 +9782 9845 -1.0015 44.1 1379712.718559944 4107904.098701538 +9845 4326 1379712.718559944 4107904.098701538 -1.0014999999999992 44.09999999999999 +9782 9845 7.251499999999999 45.9 2029789.0888838554 4308676.189895353 +9845 4326 2029789.0888838554 4308676.189895353 7.2515 45.9 +9782 9846 2.4750000000000005 46.0 1659335.5353398635 5200134.0201074025 +9846 4326 1659335.5353398635 5200134.0201074025 2.475000000000001 46.0 +9782 9846 -1.7414999999999998 45.1 1327005.0597660886 5111081.736375026 +9846 4326 1327005.0597660886 5111081.736375026 -1.7414999999999992 45.1 +9782 9846 6.6915000000000004 46.9 1981194.0581817513 5306557.143128601 +9846 4326 1981194.0581817513 5306557.143128601 6.6915 46.900000000000006 +9782 9847 1.4299999999999997 47.0 1580610.273100607 6201196.378340442 +9847 4326 1580610.273100607 6201196.378340442 1.4299999999999988 47.0 +9782 9847 -4.1499999999999995 46.1 1147882.6448475446 6125171.695773245 +9847 4326 1147882.6448475446 6125171.695773245 -4.1499999999999995 46.1 +9782 9847 7.009999999999998 47.9 1999705.9347921829 6307729.436931695 +9847 4326 1999705.9347921829 6307729.436931695 7.009999999999999 47.89999999999999 +9782 9848 1.6800000000000006 48.0 1601507.759418362 7200843.179309284 +9848 4326 1601507.759418362 7200843.179309284 1.6800000000000022 47.99999999999999 +9782 9848 -4.215 47.1 1153059.3437116276 7125552.062888663 +9848 4326 1153059.3437116276 7125552.062888663 -4.215000000000001 47.099999999999994 +9782 9848 7.575 48.9 2035246.468431578 7310024.549210299 +9848 4326 2035246.468431578 7310024.549210299 7.574999999999999 48.89999999999999 +9782 9849 1.6800000000000006 49.0 1603426.3502429728 8200839.6210153755 +9849 4326 1603426.3502429728 8200839.6210153755 1.6799999999999995 49.000000000000014 +9782 9849 -4.215 48.1 1163408.0042460614 8125441.575353767 +9849 4326 1163408.0042460614 8125441.575353767 -4.215 48.100000000000016 +9782 9849 7.575 49.9 2028501.4374461283 8309993.614527015 +9849 4326 2028501.4374461283 8309993.614527015 7.574999999999999 49.900000000000006 +9782 9850 3.025 50.07 1701789.6383209554 9207785.717349734 +9850 4326 1701789.6383209554 9207785.717349734 3.025 50.06999999999999 +9782 9850 -1.5244999999999997 49.107 1369833.7253760097 9110674.247290418 +9850 4326 1369833.7253760097 9110674.247290418 -1.5244999999999995 49.107 +9782 9850 7.5745000000000005 51.033 2020716.1643811618 9324717.284941081 +9850 4326 2020716.1643811618 9324717.284941081 7.574500000000002 51.03300000000002 +5561 9851 24.035 49.68 302526.0097234787 5505352.135758316 +9851 4326 302526.0097234787 5505352.135758316 24.033333352672784 49.67976943351177 +5561 9851 22.7795 48.807 210348.67407582604 5408978.257688591 +9851 4326 210348.67407582604 5408978.257688591 22.777854708948396 48.80674129229118 +5561 9851 25.2905 50.553 391457.79187564255 5603253.551780226 +9851 4326 391457.79187564255 5603253.551780226 25.288811635068942 50.55279797993168 +5561 9852 31.695 47.295 289536.7956494153 5240144.824570251 +9852 4326 289536.7956494153 5240144.824570251 31.693472131074806 47.29484584346425 +5561 9852 30.3495 46.4535 185998.76751444006 5147655.824432913 +9852 4326 185998.76751444006 5147655.824432913 30.347983056231918 46.45331686763234 +5561 9852 33.040499999999994 48.1365 389848.1223070578 5334404.091003745 +9852 4326 389848.1223070578 5334404.091003745 33.038961372353874 48.13637515718144 +5561 9853 29.755000000000003 46.72 281268.8495581063 5176240.285508349 +9853 4326 281268.8495581063 5176240.285508349 29.753470199677984 46.71981240333606 +5561 9853 28.3645 45.352000000000004 171836.0704526021 5025454.104485083 +9853 4326 171836.0704526021 5025454.104485083 28.36299578688837 45.35177655470669 +5561 9853 31.145500000000002 48.088 385338.68188318424 5328941.0906315725 +9853 4326 385338.68188318424 5328941.0906315725 31.143943308196203 48.087848984383285 +5561 9854 33.785 49.644999999999996 296509.19773913943 5501459.813775754 +9854 4326 296509.19773913943 5501459.813775754 33.7834227582249 49.64490588047975 +5561 9854 32.250499999999995 48.8305 183788.76450138993 5412081.553776661 +9854 4326 183788.76450138993 5412081.553776661 32.248931774873775 48.83037389373062 +5561 9854 35.319500000000005 50.4595 405532.3663203582 5593112.722339137 +9854 4326 405532.3663203582 5593112.722339137 35.3179142591184 50.4594381348527 +5561 9855 26.409999999999997 50.975 258560.5784565541 5649569.551413704 +9855 4326 258560.5784565541 5649569.551413704 26.408305046861543 50.974819362707315 +5561 9855 25.212999999999997 50.0975 172140.50202205067 5553319.4051322285 +9855 4326 172140.50202205067 5553319.4051322285 25.21132713073601 50.097291368573465 +5561 9855 27.607 51.852500000000006 341824.9390505936 5747206.992053709 +9855 4326 341824.9390505936 5747206.992053709 27.605282459503133 51.85234770637754 +5561 9856 34.3 51.235 286031.2775247982 5678348.37198926 +9856 4326 286031.2775247982 5678348.37198926 34.298375172233754 51.23493495303763 +5561 9856 33.067 50.2135 197715.90126170052 5565675.708286866 +9856 4326 197715.90126170052 5565675.708286866 33.06539591117075 50.213403827597226 +5561 9856 35.533 52.256499999999996 370538.4170847408 5792488.799711384 +9856 4326 370538.4170847408 5792488.799711384 35.531353730203165 52.25646639847096 +5561 9857 25.58 49.385000000000005 305808.5888811408 5472544.15377507 +9857 4326 305808.5888811408 5472544.15377507 25.578354115167503 49.38478802913576 +5561 9857 24.797 48.5885 248137.10254114922 5384199.839344226 +9857 4326 248137.10254114922 5384199.839344226 24.7953746250707 48.58826737948394 +5561 9857 26.363 50.1815 361640.79582194495 5561489.775275575 +9857 4326 361640.79582194495 5561489.775275575 26.361332917549124 50.181308926666716 +5561 9858 36.47 49.49 297826.42435484007 5484219.6189916665 +9858 4326 297826.42435484007 5484219.6189916665 36.46846007555544 49.48993986797483 +5561 9858 35.003 48.617000000000004 189624.14068146684 5388212.529285927 +9858 4326 189624.14068146684 5388212.529285927 35.001469032164664 48.616907981752675 +5561 9858 37.937 50.363 402248.70039301604 5582310.107411163 +9858 4326 402248.70039301604 5582310.107411163 37.93545145524382 50.36297201423047 +5561 9859 33.31 46.675 285461.72813970986 5171226.158265012 +9859 4326 285461.72813970986 5171226.158265012 33.308506188016004 46.6748587885356 +5561 9859 31.69 45.8425 159392.4520417025 5080262.97386778 +9859 4326 159392.4520417025 5080262.97386778 31.688512332239597 45.842326448730496 +5561 9859 34.93 47.5075 407726.7893287774 5264752.777836323 +9859 4326 407726.7893287774 5264752.777836323 34.92850074230243 47.50739147016842 +5561 9860 27.015 49.52 301086.12354073534 5487555.936736676 +9860 4326 301086.12354073534 5487555.936736676 27.013360687384168 49.51981006285202 +5561 9860 26.2185 48.557 242310.08465098086 5380753.242804375 +9860 4326 242310.08465098086 5380753.242804375 26.216885865568425 48.55678699706581 +5561 9860 27.8115 50.483000000000004 357596.7312631654 5594985.930973871 +9860 4326 357596.7312631654 5594985.930973871 27.809834347010415 50.48283343332356 +5561 9861 31.245 49.34 281468.228303356 5467567.51940102 +9861 4326 281468.228303356 5467567.51940102 31.243405202482972 49.33986692690199 +5561 9861 29.7645 48.539 171842.77306964496 5379911.428621257 +9861 4326 171842.77306964496 5379911.428621257 29.762916264721774 48.53883603151587 +5561 9861 32.7255 50.141 387606.1814529005 5557347.4652494285 +9861 4326 387606.1814529005 5557347.4652494285 32.723894538021696 50.14089812361318 +5561 9862 26.22 48.2 316354.2895237699 5340783.463641577 +9862 4326 316354.2895237699 5340783.463641577 26.21839712341783 48.1997826676099 +5561 9862 25.032 47.768 227438.4967170618 5293179.021954447 +9862 4326 227438.4967170618 5293179.021954447 25.03040195356794 47.767760912852474 +5561 9862 27.407999999999998 48.632 403783.1728311699 5389755.690664669 +9862 4326 403783.1728311699 5389755.690664669 27.40639281365362 48.631804558571154 +5561 9863 31.994999999999997 51.36 299651.72889314743 5692236.356816419 +9863 4326 299651.72889314743 5692236.356816419 31.99334414966988 51.35990436930934 +5561 9863 30.6315 50.442 202786.94796243965 5591005.586563398 +9863 4326 202786.94796243965 5591005.586563398 30.629862196521287 50.44187293392704 +5561 9863 33.3585 52.278000000000006 392719.40542738547 5795247.875898942 +9863 4326 392719.40542738547 5795247.875898942 33.356825885502275 52.277936120496655 +5561 9864 30.53 50.400000000000006 302133.00976529816 5585438.82253037 +9864 4326 302133.00976529816 5585438.82253037 30.528362625356078 50.399870953352824 +5561 9864 30.26 50.229 282874.614310543 5566444.430594099 +9864 4326 282874.614310543 5566444.430594099 30.258365812189407 50.22886491380841 +5561 9864 30.799999999999997 50.571000000000005 321253.24186200835 5604503.461868594 +9864 4326 321253.24186200835 5604503.461868594 30.798359437227624 50.570877005535564 +5561 9865 33.65 44.615 351593.3753456363 4942452.816334011 +9865 4326 351593.3753456363 4942452.816334011 33.64856345348299 44.614834978997365 +5561 9865 33.397999999999996 44.4035 331705.46507305175 4918821.574600885 +9865 4326 331705.46507305175 4918821.574600885 33.39656605432521 44.40332892349763 +5561 9865 33.902 44.8265 371335.368303255 4966146.751095001 +9865 4326 371335.368303255 4966146.751095001 33.90056084180736 44.82634105326454 +9866 9869 -1.815 52.305 226263.83370373762 266307.75884344376 +9866 9869 -3.1245 51.4455 135205.189381701 171507.78796750266 +9866 9869 -0.5055000000000001 53.1645 313863.38081256725 362736.72663321765 +5546 9874 159.005 -7.685 500551.41059934004 9150525.621423827 +9874 4326 500551.41059934004 9150525.621423827 159.005 -7.685000000000004 +5546 9874 156.3005 -13.6025 207888.0164389906 8494615.217890812 +9874 4326 207888.0164389906 8494615.217890812 156.3005 -13.60249999999999 +5546 9874 161.7095 -1.7675 801469.9505260227 9804417.535318665 +9874 4326 801469.9505260227 9804417.535318665 161.7095 -1.767500000000002 +5546 9875 162.405 -3.35 211633.014722393 9629338.315969413 +9875 4326 211633.014722393 9629338.315969413 162.405 -3.350000000000004 +5546 9875 162.0405 -4.259 171439.59019524424 9528613.228943802 +9875 4326 171439.59019524424 9528613.228943802 162.0405 -4.259000000000008 +5546 9875 162.7695 -2.441 251962.05068943428 9729988.812159248 +9875 4326 251962.05068943428 9729988.812159248 162.7695 -2.441000000000005 +9871 9880 -1.895 53.4 226906.77869368138 396459.5155191741 +9871 9880 -2.3495 53.265 196581.20759923017 381529.30153930624 +9871 9880 -1.4405000000000001 53.535 257040.97466935817 411582.5871126258 +9939 9943 -3.0949999999999998 51.675 107048.19722932538 111623.51002748062 +9939 9943 -3.2794999999999996 51.5175 94242.5321233813 94115.37855604061 +9939 9943 -2.9105000000000003 51.8325 119765.32192133027 129164.35253263287 +3906 9945 21.744999999999997 41.605000000000004 562090.5877099717 607108.1437734757 +9945 4326 562090.5877099717 607108.1437734757 21.73960330051661 41.605424946316695 +3906 9945 20.5795 40.9255 464589.1052727838 531476.3730105497 +9945 4326 464589.1052727838 531476.3730105497 20.57428293768151 40.92598309306493 +3906 9945 22.9105 42.2845 657547.4263396836 684065.4750464531 +9945 4326 657547.4263396836 684065.4750464531 22.904919978460526 42.28486804711173 +5324 9947 -18.21 64.775 1737580.77300941 1275148.9408133165 +9947 4326 1737580.77300941 1275148.9408133165 -18.21 64.775 +5324 9947 -29.604 60.4415 1118583.4717212128 840426.4717212932 +9947 4326 1118583.4717212128 840426.4717212932 -29.603999999999992 60.4415 +5324 9947 -6.8160000000000025 69.1085 2182185.7016016054 1805054.4936030013 +9947 4326 2182185.7016016054 1805054.4936030013 -6.8160000000000025 69.10850000000002 +9964 9967 -0.985 53.75 138550.85774704049 247512.84991103076 +9964 9967 -1.6285 53.615 95957.360815074 232700.94722699572 +9964 9967 -0.34150000000000014 53.885 180872.26602739343 262709.6430986028 +9969 9972 -3.85 56.625 108600.972 241871.17183599863 +9969 9972 -4.345000000000001 55.792500000000004 77551.4219010022 149286.87459457404 +9969 9972 -3.355 57.457499999999996 138312.93916107662 334687.07266957813 +9974 9977 -3.8 58.019999999999996 139618.9493 185338.35049953902 +9974 9977 -4.52 57.461999999999996 96407.1040562806 123421.07344178492 +9974 9977 -3.08 58.578 181505.87451037212 247714.60239313968 +10158 10160 9.645 56.235 -255288.6449914652 200793.45799830463 +10158 10160 8.1645 54.8265 -352069.5908327842 45968.24866537005 +10158 10160 11.125499999999999 57.643499999999996 -165207.31884520833 357663.48417991214 +10175 10183 -4.154999999999999 52.730000000000004 64521.879222154406 125604.96631403507 +10175 10183 -4.4745 52.478 42811.81707134601 97612.21589235676 +10175 10183 -3.8354999999999997 52.982 85983.06465238146 153694.69990421188 +10185 10188 -3.38 52.57 53989.46795532838 21229.746061786725 +10185 10188 -4.082 52.39 6189.618131488271 1451.7465204495238 +10185 10188 -2.678 52.75 101399.6435429478 41471.51736696556 +10191 10194 -3.495 53.24 212882.69618713894 494118.0320977415 +10191 10194 -4.5885 53.042 139543.53002818866 472638.4988180727 +10191 10194 -2.4015 53.438 285547.87159473146 516714.6422653927 +10196 10199 -2.9050000000000002 52.879999999999995 199331.47649246725 142768.4840599135 +10196 10199 -3.1345 52.538 183756.8880606513 104736.29020538605 +10196 10199 -2.6755 53.222 214662.96479068886 180852.2344545613 +10204 10207 -1.305 51.84 511278.24103155424 323624.8310596697 +10204 10207 -1.4445 51.597 501610.0168708341 296598.04836403305 +10204 10207 -1.1655 52.083 520842.9517846269 350671.1456616777 +10209 10212 -2.1799999999999997 51.655 201379.69657022555 194108.03690266891 +10209 10212 -4.052 51.2905 71057.72427938945 154388.62835810578 +10209 10212 -0.30799999999999983 52.0195 329602.31188810465 237135.84682375682 +10214 10217 -3.3600000000000003 51.58 119638.82065447219 185898.12926323267 +10214 10217 -3.576 51.373000000000005 104375.12707227455 163037.4384462682 +10214 10217 -3.144 51.787 134766.77814385126 208805.12372669444 +10219 10222 -4.38 51.805 49537.346041397774 212105.3517082186 +10219 10222 -5.082 51.5755 286.45854703962686 187964.22834277165 +10219 10222 -3.678 52.0345 98308.71274837991 236724.49690381764 +10224 10227 -1.1549999999999998 52.18 174920.169801067 172462.84337974712 +10224 10227 -2.1494999999999997 51.523 105892.11618008961 99835.7388570599 +10224 10227 -0.16049999999999986 52.836999999999996 241943.49249994423 246030.696292323 +10229 10235 -1.73 51.955 132729.2907738643 122429.26306761308 +10229 10235 -2.2520000000000002 51.680499999999995 96612.27393138228 92031.19564845569 +10229 10235 -1.208 52.2295 168408.1117737505 153086.29888502494 +10237 10240 -2.605 52.905 109758.00871435672 121180.27347393222 +10237 10240 -2.8795 52.6755 91189.21071424213 95677.63903229705 +10237 10240 -2.3305 53.134499999999996 128131.61737729041 146754.48430339023 +10249 10250 11.829999999999998 55.65 -118411.13011144057 136386.94708320498 +10249 10250 10.893999999999998 54.623999999999995 -176479.73248592974 21321.164068842307 +10249 10250 12.765999999999998 56.676 -63463.51874329444 252230.8153853109 +10252 10254 14.92 55.16 -48041.19784044247 55376.57785088571 +10252 10254 14.623 54.961999999999996 -67056.62984676137 33365.445306385554 +10252 10254 15.217 55.358000000000004 -29213.374942851944 77468.93310542828 +10256 10258 10.434999999999999 56.155 3557.018162892465 128575.37113004742 +10256 10258 8.2435 54.674499999999995 -137671.4599797827 -34109.98909126107 +10256 10258 12.6265 57.63549999999999 134329.84231346697 295620.70698324323 +10260 10262 14.92 55.16 -19325.39852289501 23425.80217588661 +10260 10262 14.623 54.961999999999996 -38351.52686482049 1427.4577739922188 +10260 10262 15.217 55.358000000000004 -488.7008721344282 45505.12752686654 +10265 10266 12.48 55.665 132254.42848846046 76038.22725450239 +10265 10266 12.255 55.5255 118524.92062488284 60107.60847737059 +10265 10266 12.705 55.8045 145881.46512201612 92011.9732896604 +10268 10270 9.265 55.135 2040.6801791848823 74186.55579604344 +10268 10270 8.459499999999998 54.8335 -49697.61727677196 40900.268531787275 +10268 10270 10.0705 55.436499999999995 53004.50188522612 108066.83662606428 +10272 10275 -3.8550000000000004 50.754999999999995 110340.83239961718 102645.52389049552 +10272 10275 -4.0845 50.6605 94112.48082102623 92159.32215027601 +10272 10275 -3.6255 50.8495 126503.96462013158 113182.13649585957 +10277 10280 -3.2399999999999998 50.879999999999995 373086.57980789605 221102.03045248063 +10277 10280 -5.391 50.132999999999996 219322.45558707873 140227.16183564757 +10277 10280 -1.0889999999999995 51.627 522010.3582485385 306395.4870404809 +10284 10285 9.370000000000001 53.675 3524451.5784173952 5949806.49615931 +10284 10285 8.434 53.3915 3462345.1284422856 5918339.69145626 +10284 10285 10.306000000000001 53.9585 3585724.840110405 5982086.943088973 +10284 10286 4.675 53.445 31611246.103145666 5923081.350788312 +10284 10286 3.4735 51.2175 31533069.64460152 5674118.682907582 +10284 10286 5.8765 55.6725 31680887.497756355 6173381.806914216 +10284 10287 4.675 53.445 31611246.103145666 5923081.350788312 +10284 10287 3.4735 51.2175 31533069.64460152 5674118.682907582 +10284 10287 5.8765 55.6725 31680887.497756355 6173381.806914216 +10284 10288 9.004999999999999 51.370000000000005 32500348.05019652 5690971.606736347 +10284 10288 6.3004999999999995 47.68 32297394.22618539 5284264.866075603 +10284 10288 11.709499999999998 55.06 32673041.836988475 6104823.288213569 +10284 10289 9.004999999999999 51.370000000000005 32500348.05019652 5690971.606736347 +10284 10289 6.3004999999999995 47.68 32297394.22618539 5284264.866075603 +10284 10289 11.709499999999998 55.06 32673041.836988475 6104823.288213569 +10284 10290 13.305 51.245000000000005 33381693.719118673 5678435.226906829 +10284 10290 11.743500000000001 47.8385 33256335.41081962 5303485.919889525 +10284 10290 14.866499999999998 54.6515 33491386.34549611 6056019.993634921 +10284 10291 13.305 51.245000000000005 33381693.719118673 5678435.226906829 +10284 10291 11.743500000000001 47.8385 33256335.41081962 5303485.919889525 +10284 10291 14.866499999999998 54.6515 33491386.34549611 6056019.993634921 +10305 10306 23.65 56.88 478669.909704032 304082.77423819713 +10306 4326 478669.909704032 304082.77423819713 23.65 56.879999999999995 +10305 10306 19.519 55.791000000000004 219128.93832996086 191909.4644314982 +10306 4326 219128.93832996086 191909.4644314982 19.519000000000002 55.791 +10305 10306 27.781 57.969 723614.0114121912 431517.38856511377 +10306 4326 723614.0114121912 431517.38856511377 27.780999999999995 57.96899999999999 +10310 10314 165.865 -21.115000000000002 2385975.9358495856 2342617.387136278 +10314 4326 2385975.9358495856 2342617.387136278 165.865 -21.11500000000001 +10310 10314 163.77249999999998 -22.5685 2170903.8181930706 2180057.5801144107 +10314 4326 2170903.8181930706 2180057.5801144107 163.77249999999998 -22.56850000000002 +10310 10314 167.9575 -19.6615 2605358.779088318 2502270.956363617 +10314 4326 2605358.779088318 2502270.956363617 167.9575 -19.661499999999965 +10310 10315 159.13 -20.685000000000002 513538.8974309316 7712708.207789224 +10315 4326 513538.8974309316 7712708.207789224 159.13 -20.684999999999995 +10310 10315 156.538 -25.4955 252517.6418744477 7177894.75008237 +10315 4326 252517.6418744477 7177894.75008237 156.538 -25.495499999999993 +10310 10315 161.72199999999998 -15.874500000000001 791506.0347748799 8243051.580018548 +10315 4326 791506.0347748799 8243051.580018548 161.72199999999998 -15.874500000000006 +10310 10316 165.0 -20.64 499999.9999999973 7717693.710540362 +10316 4326 499999.9999999973 7717693.710540362 165.0 -20.64 +10310 10316 162.3 -25.869 229428.86486812617 7136040.499022016 +10316 4326 229428.86486812617 7136040.499022016 162.3 -25.869 +10310 10316 167.7 -15.411000000000001 789801.0439407314 8294399.228510025 +10316 4326 789801.0439407314 8294399.228510025 167.7 -15.411 +10310 10317 171.14 -22.85 514363.228105076 7473078.222053771 +10317 4326 514363.228105076 7473078.222053771 171.14 -22.85 +10310 10317 168.314 -25.64 230315.15969478886 7161446.219384035 +10317 4326 230315.15969478886 7161446.219384035 168.314 -25.640000000000004 +10310 10317 173.966 -20.060000000000002 810248.0691406042 7779122.946307896 +10317 4326 810248.0691406042 7779122.946307896 173.966 -20.06 +10328 10329 17.68 43.915000000000006 474299.9695736248 4863941.239642396 +10329 4326 474299.9695736248 4863941.239642396 17.68 43.91500000000001 +10328 10329 15.934000000000001 42.6955 330720.78715200315 4730490.180979775 +10329 4326 330720.78715200315 4730490.180979775 15.934000000000001 42.6955 +10328 10329 19.426000000000002 45.1345 612160.9693613574 5000381.152704706 +10329 4326 612160.9693613574 5000381.152704706 19.426000000000002 45.134499999999996 +4283 10448 117.88499999999999 -34.980000000000004 50152.184629889736 127608.86652404815 +10448 4326 50152.184629889736 127608.86652404815 117.88499999999999 -34.98000000000001 +4283 10448 117.5835 -35.187 22691.01763044766 104602.57510905061 +10448 4326 22691.01763044766 104602.57510905061 117.58350000000002 -35.187 +4283 10448 118.1865 -34.773 77751.91044729637 150531.3257979718 +10448 4326 77751.91044729637 150531.3257979718 118.18650000000001 -34.773 +4283 10449 115.245 -21.205 59480.859980841684 254220.6976669589 +10449 4326 59480.859980841684 254220.6976669589 115.245 -21.205 +4283 10449 114.9345 -22.1005 27443.60682483571 155031.9585399516 +10449 4326 27443.60682483571 155031.9585399516 114.93450000000001 -22.100499999999993 +4283 10449 115.55550000000001 -20.3095 91906.92263784888 353335.7007890865 +10449 4326 91906.92263784888 353335.7007890865 115.55550000000001 -20.3095 +4283 10450 122.35 -17.42 51770.76860547363 273204.9705892424 +10450 4326 51770.76860547363 273204.9705892424 122.35 -17.42 +4283 10450 122.107 -18.023 26033.1594690485 206451.82625282463 +10450 4326 26033.1594690485 206451.82625282463 122.10700000000001 -18.022999999999996 +4283 10450 122.593 -16.817 77677.6774977568 339921.445997549 +10450 4326 77677.6774977568 339921.445997549 122.59300000000002 -16.817 +4283 10451 115.525 -33.575 58510.51369655677 183490.3425040613 +10451 4326 58510.51369655677 183490.3425040613 115.52500000000002 -33.574999999999996 +4283 10451 115.2145 -33.7325 29720.0421984437 166003.3789695124 +10451 4326 29720.0421984437 166003.3789695124 115.21449999999999 -33.7325 +4283 10451 115.83550000000001 -33.4175 87405.72963360423 200890.58408987336 +10451 4326 87405.72963360423 200890.58408987336 115.83550000000001 -33.41749999999999 +4283 10452 113.66499999999999 -24.25 49830.74327200439 267026.8111893707 +10452 4326 49830.74327200439 267026.8111893707 113.66499999999999 -24.25 +4283 10452 113.3635 -25.375 19488.813364081732 142376.08278136328 +10452 4326 19488.813364081732 142376.08278136328 113.3635 -25.375 +4283 10452 113.9665 -23.125 80711.33052014143 391592.97043461306 +10452 4326 80711.33052014143 391592.97043461306 113.9665 -23.125 +4283 10453 116.065 -33.96 52169.93071709448 240696.2023526919 +10453 4326 52169.93071709448 240696.2023526919 116.06500000000001 -33.96 +4283 10453 115.76350000000001 -34.599000000000004 24420.66715579713 169806.916053012 +10453 4326 24420.66715579713 169806.916053012 115.7635 -34.599000000000004 +4283 10453 116.3665 -33.321 80334.5152017647 311496.8731972282 +10453 4326 80334.5152017647 311496.8731972282 116.36650000000002 -33.321 +4283 10454 121.88 -33.915 49691.74262734053 195746.2422318342 +10454 4326 49691.74262734053 195746.2422318342 121.88 -33.91499999999999 +4283 10454 121.592 -34.4415 23225.087009679086 137305.0919104484 +10454 4326 23225.087009679086 137305.0919104484 121.59199999999998 -34.441500000000005 +4283 10454 122.168 -33.3885 76486.00228929553 254107.6941802441 +10454 4326 76486.00228929553 254107.6941802441 122.16800000000002 -33.38849999999999 +4283 10455 114.0 -22.085 43119.94938493575 206780.1293811621 +10455 4326 43119.94938493575 206780.1293811621 114.00000000000001 -22.084999999999997 +4283 10455 113.775 -22.3865 19964.0043897283 173366.29818287538 +10455 4326 19964.0043897283 173366.29818287538 113.775 -22.3865 +4283 10455 114.225 -21.7835 66374.5947414219 240158.227604385 +10455 4326 66374.5947414219 240158.227604385 114.225 -21.783499999999997 +4283 10456 114.755 -28.79 66760.82811540275 163973.72297256347 +10456 4326 66760.82811540275 163973.72297256347 114.75499999999998 -28.789999999999992 +4283 10456 114.53450000000001 -29.069000000000003 45244.86664560678 133061.78236150416 +10456 4326 45244.86664560678 133061.78236150416 114.53450000000001 -29.068999999999996 +4283 10456 114.9755 -28.511 88391.15312146995 194844.87094351603 +10456 4326 88391.15312146995 194844.87094351603 114.9755 -28.511 +4283 10457 121.42 -30.5 52319.70451734229 324288.49023650866 +10457 4326 52319.70451734229 324288.49023650866 121.42 -30.499999999999986 +4283 10457 121.042 -32.075 16755.064127458725 149563.5229824446 +10457 4326 16755.064127458725 149563.5229824446 121.042 -32.074999999999996 +4283 10457 121.798 -28.925 89059.23879238927 498848.34623762826 +10457 4326 89059.23879238927 498848.34623762826 121.79800000000002 -28.92499999999999 +4283 10458 115.08500000000001 -29.909999999999997 59818.31126226454 239848.51634149393 +10458 4326 59818.31126226454 239848.51634149393 115.08500000000002 -29.90999999999999 +4283 10458 114.85549999999999 -30.657 37747.84896911859 157035.2708847914 +10458 4326 37747.84896911859 157035.2708847914 114.85549999999999 -30.657000000000004 +4283 10458 115.31450000000001 -29.162999999999997 82217.95930685548 322608.7213088032 +10458 4326 82217.95930685548 322608.7213088032 115.31450000000001 -29.162999999999997 +4283 10459 114.325 -27.83 55957.80592258246 520354.1742460937 +10459 4326 55957.80592258246 520354.1742460937 114.325 -27.82999999999999 +4283 10459 113.94250000000001 -28.433 18479.841645954126 453471.2971182945 +10459 4326 18479.841645954126 453471.2971182945 113.94250000000001 -28.433 +4283 10459 114.7075 -27.227 93852.22103208618 587113.8787259557 +10459 4326 93852.22103208618 587113.8787259557 114.7075 -27.226999999999997 +4283 10460 116.91499999999999 -20.585 48088.65915055942 172871.98169108434 +10460 4326 48088.65915055942 172871.98169108434 116.915 -20.584999999999994 +4283 10460 116.6135 -20.8865 16721.633315327075 139458.85711382888 +10460 4326 16721.633315327075 139458.85711382888 116.6135 -20.8865 +4283 10460 117.2165 -20.2835 79579.21452681327 206225.7058153432 +10460 4326 79579.21452681327 206225.7058153432 117.21649999999998 -20.2835 +4283 10461 128.75 -15.75 50000.0 257992.37645386904 +10461 4326 50000.0 257992.37645386904 128.75 -15.75 +4283 10461 128.525 -16.65 25996.213053105865 158382.53048776276 +10461 4326 25996.213053105865 158382.53048776276 128.52500000000003 -16.65 +4283 10461 128.975 -14.85 74216.10057910885 357568.3206593152 +10461 4326 74216.10057910885 357568.3206593152 128.975 -14.849999999999996 +4283 10462 115.385 -31.1 51749.08443727538 207933.0955977845 +10462 4326 51749.08443727538 207933.0955977845 115.385 -31.099999999999994 +4283 10462 115.1735 -31.450999999999997 31639.10958490129 169000.18098612502 +10462 4326 31639.10958490129 169000.18098612502 115.1735 -31.450999999999993 +4283 10462 115.5965 -30.749000000000002 72007.40807507106 246825.4609060171 +10462 4326 72007.40807507106 246825.4609060171 115.5965 -30.749 +4283 10463 115.1 -33.965 43838.45498596713 190198.14890363533 +10463 4326 43838.45498596713 190198.14890363533 115.1 -33.964999999999996 +4283 10463 114.97399999999999 -34.374500000000005 32278.91364343737 144759.10456161946 +10463 4326 32278.91364343737 144759.10456161946 114.97399999999999 -34.3745 +4283 10463 115.226 -33.555499999999995 55509.91398276949 235619.772046261 +10463 4326 55509.91398276949 235619.772046261 115.226 -33.555499999999995 +4283 10464 115.765 -32.375 45137.83363506339 216565.97378608212 +10464 4326 45137.83363506339 216565.97378608212 115.765 -32.37499999999999 +4283 10464 115.4725 -33.3155 17951.43236337875 112211.31170323258 +10464 4326 17951.43236337875 112211.31170323258 115.47250000000001 -33.31550000000001 +4283 10464 116.0575 -31.4345 72895.6704943908 320829.4426977611 +10464 4326 72895.6704943908 320829.4426977611 116.05749999999999 -31.4345 +4283 10465 118.61 -20.445 51043.50013562476 138366.0092649036 +10465 4326 51043.50013562476 138366.0092649036 118.61 -20.444999999999997 +4283 10465 118.286 -20.755499999999998 17300.257442140646 103958.20365920383 +10465 4326 17300.257442140646 103958.20365920383 118.286 -20.755499999999994 +4283 10465 118.934 -20.134500000000003 84922.5432700046 172705.86073361337 +10465 4326 84922.5432700046 172705.86073361337 118.934 -20.134500000000003 +10468 10471 -1.5750000000000002 52.4 115186.39366997789 102195.23102380983 +10468 10471 -1.8225 52.309999999999995 98302.80546000971 92215.24255899004 +10468 10471 -1.3275000000000001 52.49 132001.44412025553 112233.03644463743 +10475 10477 11.475 46.89 15922.643643150852 89908.60462078523 +10477 4326 15922.643643150852 89908.60462078523 11.475 46.89 +10475 10477 11.083499999999999 46.494 -14166.747786206455 45976.849125775254 +10477 4326 -14166.747786206455 45976.849125775254 11.083499999999999 46.494 +10475 10477 11.8665 47.286 45572.98433244634 133992.2984237211 +10477 4326 45572.98433244634 133992.2984237211 11.8665 47.285999999999994 +4269 10481 -100.08 31.165 4896279.799817893 19654023.817911882 +10481 4326 4896279.799817893 19654023.817911882 -100.08 31.165000000000006 +4269 10481 -106.002 26.3635 2954059.934244422 17959870.400423717 +10481 4326 2954059.934244422 17959870.400423717 -106.002 26.363500000000013 +4269 10481 -94.158 35.966499999999996 6651574.446013488 21448740.689327914 +10481 4326 6651574.446013488 21448740.689327914 -94.158 35.966499999999996 +6318 10516 -90.74000000000001 44.335 115873.48389171588 111810.88467134882 +10516 4326 115873.48389171588 111810.88467134882 -90.74000000000001 44.33500000000001 +6318 10516 -91.127 44.0965 14309.061798461325 24971.7007600744 +10516 4326 14309.061798461325 24971.7007600744 -91.127 44.0965 +6318 10516 -90.35300000000001 44.5735 216616.56872987418 199131.76185788887 +10516 4326 216616.56872987418 199131.76185788887 -90.35300000000001 44.5735 +4326 10592 23.809999999999995 -2.5450000000000017 424542.6432342118 -832945.0961972325 +4326 10592 -26.131 -39.7555 -4121837.0750245634 -5049522.24821018 +4326 10592 73.75099999999998 34.665499999999994 4836953.49619085 3797457.6608291026 +4326 10594 111.51 36.435 1030804.7011912387 -879870.3473719592 +4326 10594 39.456 -6.0765 -6920664.4615811985 -3350726.2317644274 +4326 10594 -176.436 78.9465 1320013.3994832747 4687271.504984554 +4326 10596 4.604999999999997 56.455 -940636.4500938618 266368.2421725912 +4326 10596 -37.807500000000005 31.9615 -4981290.7760303095 -461290.53420003067 +4326 10596 47.01749999999999 80.9485 470761.2066584247 2968395.1112007443 +4326 10598 0.0 48.989999999999995 4764758.4668088285 4208228.25346893 +4326 10598 -162.0 12.081 -6452770.616986272 -1618335.2615650683 +4326 10598 162.0 85.899 -483860.29642572615 4420296.4609213 +4326 10601 155.66 -19.495000000000005 2155719.1486364207 -605857.6971049962 +4326 10601 80.699 -56.453500000000005 -3285684.26479122 -5250012.861226562 +4326 10601 -129.37900000000002 17.463499999999996 9387106.139114246 2610788.504154535 +4326 10603 -69.69999999999999 -14.780000000000001 -1042805.9439164252 1666.1383224674123 +4326 10603 -119.30799999999999 -55.361 -3619006.745112857 -5294891.923805865 +4326 10603 -20.092 25.800999999999995 4179818.6913097957 4308726.777009721 +10623 10626 -1.7000000000000002 53.775000000000006 101428.23420761662 260418.9874011635 +10623 10626 -3.2750000000000004 51.682500000000005 -7822.801841625987 28887.309155515482 +10623 10626 -0.12500000000000044 55.8675 200361.2985986746 494335.7816100924 +10628 10632 -2.185 53.66 207586.42024832693 425331.1236584381 +10628 10632 -4.1155 51.626 73797.6824047963 200818.51774464516 +10628 10632 -0.25450000000000017 55.694 329090.5511889529 653379.251409223 +10636 10641 -63.235 17.634999999999998 5044.504289853307 1917.4254833648447 +10636 10641 -63.3025 17.5675 -2128.058077630769 -5540.228710285155 +10636 10641 -63.1675 17.7025 12211.741862490591 9377.627276548184 +4674 10665 -51.155 -30.1 285059.70914381853 1668807.7946863556 +10665 4326 285059.70914381853 1668807.7946863556 -51.15500000000001 -30.099999999999998 +4674 10665 -51.2855 -30.253 272523.3908871154 1651822.6446667784 +10665 4326 272523.3908871154 1651822.6446667784 -51.2855 -30.253 +4674 10665 -51.024499999999996 -29.947 297634.84280475957 1685778.0767030981 +10665 4326 297634.84280475957 1685778.0767030981 -51.024499999999996 -29.947000000000003 +10671 10674 45.19 -12.91 520611.2921526474 8572809.36921248 +10674 4326 520611.2921526474 8572809.36921248 45.19 -12.910000000000007 +10671 10674 43.831 -14.332 373934.3954965897 8415237.254319478 +10674 4326 373934.3954965897 8415237.254319478 43.831 -14.33199999999999 +10671 10674 46.549 -11.488 668953.342576192 8729606.363971211 +10674 4326 668953.342576192 8729606.363971211 46.549 -11.487999999999998 +10690 10699 21.54 64.08500000000001 526332.0661724879 7106597.612647494 +10699 4326 526332.0661724879 7106597.612647494 21.54 64.08500000000001 +10690 10699 19.325999999999997 59.36450000000001 404849.7569181391 6581836.563990274 +10699 4326 404849.7569181391 6581836.563990274 19.325999999999997 59.36450000000001 +10690 10699 23.753999999999998 68.8055 611085.0576138056 7635164.51215666 +10699 4326 611085.0576138056 7635164.51215666 23.753999999999998 68.8055 +10690 10702 30.795 64.72 394948.89271093 7179077.869476136 +10702 4326 394948.89271093 7179077.869476136 30.794999999999998 64.72 +10690 10702 30.0795 62.028999999999996 347214.6126943475 6880850.904816576 +10702 4326 347214.6126943475 6880850.904816576 30.0795 62.028999999999996 +10690 10702 31.5105 67.41099999999999 436157.34214348916 7477964.665819044 +10702 4326 436157.34214348916 7477964.665819044 31.510499999999997 67.41099999999999 +10725 10726 57.995000000000005 43.415 430585.2060770575 379776.375887872 +10726 4326 430585.2060770575 379776.375887872 57.995000000000005 43.415000000000006 +10725 10726 56.1905 41.466499999999996 282374.8514912418 163169.11710531 +10726 4326 282374.8514912418 163169.11710531 56.1905 41.4665 +10725 10726 59.7995 45.3635 569330.2475900869 599626.5066926297 +10726 4326 569330.2475900869 599626.5066926297 59.7995 45.3635 +10725 10727 63.0 41.519999999999996 350000.0 501811.61290072673 +10727 4326 350000.0 501811.61290072673 63.0 41.51999999999999 +10725 10727 60.3 38.55 114615.42977166906 175495.27020327866 +10727 4326 114615.42977166906 175495.27020327866 60.300000000000004 38.550000000000004 +10725 10727 65.7 44.49 564767.4658136375 835304.0396346423 +10727 4326 564767.4658136375 835304.0396346423 65.7 44.49 +10725 10728 69.0 40.09 350000.0 453977.8478447359 +10728 4326 350000.0 453977.8478447359 69.0 40.09000000000001 +10725 10728 66.3 37.471 111136.71521374959 166665.58650225546 +10728 4326 111136.71521374959 166665.58650225546 66.3 37.471000000000004 +10725 10728 71.7 42.709 571202.5663422705 748383.7663395585 +10728 4326 571202.5663422705 748383.7663395585 71.7 42.709 +10725 10729 72.58500000000001 40.735 145993.7444393166 195446.36372075183 +10729 4326 145993.7444393166 195446.36372075183 72.58500000000001 40.735 +10725 10729 72.0585 40.3075 99927.30945817719 149322.97498340995 +10729 4326 99927.30945817719 149322.97498340995 72.0585 40.30749999999999 +10725 10729 73.1115 41.1625 191498.39312432153 241834.39540169804 +10729 4326 191498.39312432153 241834.39540169804 73.1115 41.16250000000001 +10284 10731 4.675 53.445 611246.1031456656 5923081.350788312 +10284 10731 3.4735 51.2175 533069.644601522 5674118.682907582 +10284 10731 5.8765 55.6725 680887.4977563552 6173381.806914216 +10284 10732 9.004999999999999 51.370000000000005 500348.0501965219 5690971.606736347 +10284 10732 6.3004999999999995 47.68 297394.2261853902 5284264.866075603 +10284 10732 11.709499999999998 55.06 673041.8369884761 6104823.288213569 +10284 10733 13.305 51.245000000000005 381693.7191186729 5678435.226906829 +10284 10733 11.743500000000001 47.8385 256335.41081962036 5303485.919889525 +10284 10733 14.866499999999998 54.6515 491386.3454961129 6056019.993634921 +10736 10744 -62.965 17.494999999999997 4294.296127617934 3943.360935310833 +10736 10744 -63.0415 17.4185 -3829.1022951280274 -4519.821952927625 +10736 10744 -62.8885 17.571499999999997 12410.899292292488 12409.880450821482 +10736 10745 -62.965 17.494999999999997 503715.74612761795 1934339.6209353108 +10736 10745 -63.0415 17.4185 495592.34770487197 1925876.4380470724 +10736 10745 -62.8885 17.571499999999997 511832.3492922925 1942806.1404508215 +10758 10759 -68.305 12.165 17419.502469595544 19692.314346896022 +10758 10759 -68.4535 11.989500000000001 1242.3742719090405 285.2135479567296 +10758 10759 -68.1565 12.340499999999999 33575.441560448664 39108.403998155205 +4674 10773 -47.815 -21.215 11712.039249569889 23338.722388536622 +10773 4326 11712.039249569889 23338.722388536622 -47.815 -21.215 +4674 10773 -47.972500000000004 -21.3545 -4622.751906252051 7880.290211749183 +10773 4326 -4622.751906252051 7880.290211749183 -47.972500000000004 -21.354499999999994 +4674 10773 -47.6575 -21.075499999999998 28077.562649637843 38780.59757414083 +10773 4326 28077.562649637843 38780.59757414083 -47.6575 -21.075499999999995 +10791 10792 29.855 0.43 817813.5509098328 47587.39107527689 +10792 4326 817813.5509098328 47587.39107527689 29.855 0.42999999999999994 +10791 10792 29.7245 0.043000000000000003 803283.6389290409 4758.206742113514 +10792 4326 803283.6389290409 4758.206742113514 29.724499999999995 0.04300000000000001 +10791 10792 29.985500000000002 0.817 832329.2271225748 90426.65187954596 +10792 4326 832329.2271225748 90426.65187954596 29.985500000000005 0.817 +10791 10793 32.504999999999995 2.115 444955.4780023569 233780.88879480504 +10793 4326 444955.4780023569 233780.88879480504 32.504999999999995 2.1150000000000007 +10791 10793 30.2505 0.21150000000000002 193933.27804595965 23404.23008832665 +10793 4326 193933.27804595965 23404.23008832665 30.2505 0.21150000000000005 +10791 10793 34.759499999999996 4.0185 695340.7657860297 444382.53796492756 +10793 4326 695340.7657860297 444382.53796492756 34.759499999999996 4.0185 +10791 10794 31.99 -0.725 387615.36657183943 9919853.13893274 +10794 4326 387615.36657183943 9919853.13893274 31.990000000000002 -0.725000000000007 +10791 10794 30.198999999999998 -1.3775 188283.41960849037 9847561.288612192 +10794 4326 188283.41960849037 9847561.288612192 30.199 -1.377500000000002 +10791 10794 33.781 -0.07250000000000001 586908.3864962686 9991985.822072474 +10794 4326 586908.3864962686 9991985.822072474 33.781 -0.07249999999999737 +10791 10795 29.785 -0.74 809997.9757677679 9918110.358294824 +10795 4326 809997.9757677679 9918110.358294824 29.785 -0.7399999999999934 +10791 10795 29.5915 -1.406 788381.7149115794 9844434.334708497 +10795 4326 788381.7149115794 9844434.334708497 29.5915 -1.4059999999999915 +10791 10795 29.9785 -0.07400000000000007 831582.5882791502 9991809.637601899 +10795 4326 831582.5882791502 9991809.637601899 29.978500000000004 -0.07400000000000428 +10800 10801 -12.795 4.76 744580.6368256068 526525.8413007403 +10801 4326 744580.6368256068 526525.8413007403 -12.795000000000002 4.759999999999999 +10800 10801 -13.5105 3.2840000000000003 665492.3430268245 363107.90910444764 +10801 4326 665492.3430268245 363107.90910444764 -13.5105 3.2840000000000003 +10800 10801 -12.0795 6.236000000000001 823205.9658730198 690187.9865787482 +10801 4326 823205.9658730198 690187.9865787482 -12.0795 6.2360000000000015 +10800 10802 -9.68 4.77 424591.59205337404 527277.7468675609 +10802 4326 424591.59205337404 527277.7468675609 -9.68 4.77 +10800 10802 -11.768 1.395 191961.06815770752 154370.9732105688 +10802 4326 191961.06815770752 154370.9732105688 -11.768 1.3949999999999998 +10800 10802 -7.5920000000000005 8.145 655120.2260327064 900597.9597961515 +10802 4326 655120.2260327064 900597.9597961515 -7.592000000000001 8.144999999999998 +4326 10820 -95.78999999999999 50.84 14724.38563065676 1204586.9821220923 +4326 10820 -134.715 42.587 -3049569.255696218 1092025.5770682998 +4326 10820 -56.864999999999995 59.092999999999996 2161952.3565924936 2701769.6191201303 +10831 10833 43.355000000000004 42.315 1288046.4898252266 379460.7218313203 +10833 4326 1288046.4898252266 379460.7218313203 43.35499557740326 42.314997926491436 +10831 10833 40.3265 41.1675 1033705.3554101957 256950.19435296723 +10833 4326 1033705.3554101957 256950.19435296723 40.326495370392436 41.167498186077516 +10831 10833 46.3835 43.462500000000006 1533358.7622769848 510876.70604777755 +10833 4326 1533358.7622769848 510876.70604777755 46.38349571042111 43.46249767908094 +10831 10836 40.485 42.510000000000005 621998.4731157967 4707471.894543324 +10836 4326 621998.4731157967 4707471.894543324 40.484995621482284 42.50999821689063 +10831 10836 39.1215 41.538 510134.7973849364 4598490.089952217 +10836 4326 510134.7973849364 4598490.089952217 39.121495423787515 41.537998328390024 +10831 10836 41.8485 43.482000000000006 730358.6362893705 4818284.382147418 +10836 4326 730358.6362893705 4818284.382147418 41.848495802330234 43.48199810061162 +10831 10837 44.355000000000004 42.14 446699.76784984686 4665521.749307714 +10837 4326 446699.76784984686 4665521.749307714 44.35499553941078 42.139997831172536 +10831 10837 42.2265 41.15 267261.6702116489 4559116.987713172 +10837 4326 267261.6702116489 4559116.987713172 42.2264953834646 41.149997997208146 +10831 10837 46.4835 43.13 620663.310555694 4776319.30575691 +10837 4326 620663.310555694 4776319.30575691 46.48349565593717 43.12999766702097 +10849 10851 -0.58 52.015 214489.56285393468 154223.39204622668 +10849 10851 -1.345 51.731500000000004 161775.82121622495 122725.1619185701 +10849 10851 0.18500000000000005 52.2985 266537.86599414604 186274.70775220895 +4674 10857 -54.375 -14.25 4960116.556767477 9747286.355867246 +10857 4326 4960116.556767477 9747286.355867246 -54.375 -14.249999999999984 +4674 10857 -72.04650000000001 -31.827 3206475.347756338 7758172.207609805 +10857 4326 3206475.347756338 7758172.207609805 -72.04650000000001 -31.82700000000003 +4674 10857 -36.703500000000005 3.326999999999998 6959110.205979304 11640157.498439783 +10857 4326 6959110.205979304 11640157.498439783 -36.703500000000005 3.3269999999999937 +10860 10863 -0.85 54.525000000000006 39630.9213 38799.15613414111 +10860 10863 -1.21 54.3675 16232.10789497097 21327.058511232273 +10860 10863 -0.49 54.682500000000005 62850.31642231259 56390.98127155121 +10898 10899 10.649999999999999 52.135 4407577.2371196775 5778629.874937654 +10898 10899 10.605 52.1125 4404448.510683879 5776185.044898299 +10898 10899 10.694999999999999 52.1575 4410702.886590069 5781076.647708833 +10910 10911 -122.22 40.8 6500762.569637412 2174791.409085322 +10911 4326 6500762.569637412 2174791.409085322 -122.22 40.8 +10910 10911 -124.227 39.711000000000006 5935135.343353516 1785967.7581478895 +10911 4326 5935135.343353516 1785967.7581478895 -124.227 39.71100000000001 +10910 10911 -120.213 41.888999999999996 7048248.886329139 2576456.9439488114 +10911 4326 7048248.886329139 2576456.9439488114 -120.21300000000001 41.88899999999998 +10910 10912 -121.80000000000001 39.09 6618431.056367773 2158843.097341253 +10912 4326 6618431.056367773 2158843.097341253 -121.80000000000001 39.089999999999996 +10910 10912 -123.834 38.127 6034094.980629644 1813394.5703542582 +10912 4326 6034094.980629644 1813394.5703542582 -123.834 38.127 +10910 10912 -119.766 40.053 7187039.698377048 2517236.023450566 +10912 4326 7187039.698377048 2517236.023450566 -119.76600000000002 40.053000000000004 +10910 10913 -120.425 37.72 6583359.320656051 2084639.0553885268 +10913 4326 6583359.320656051 2084639.0553885268 -120.42499999999998 37.71999999999999 +10910 10913 -122.7605 36.82899999999999 5900078.18062494 1768202.0916701525 +10913 4326 5900078.18062494 1768202.0916701525 -122.7605 36.82899999999999 +10910 10913 -118.0895 38.611000000000004 7250435.109600073 2417966.60498926 +10913 4326 7250435.109600073 2417966.60498926 -118.08950000000002 38.611 +10910 10914 -118.815 36.68 6615915.005361133 2130717.8888657917 +10914 4326 6615915.005361133 2130717.8888657917 -118.81499999999998 36.67999999999997 +10910 10914 -121.6905 35.870000000000005 5764562.874933041 1846956.821754023 +10914 4326 5764562.874933041 1846956.821754023 -121.6905 35.87000000000002 +10910 10914 -115.93950000000001 37.489999999999995 7449562.145374354 2439733.1965191932 +10914 4326 7449562.145374354 2439733.1965191932 -115.93950000000002 37.489999999999995 +10910 10915 -117.77000000000001 34.285 6631142.589076952 1926178.466198221 +10915 4326 6631142.589076952 1926178.466198221 -117.77 34.28500000000002 +10910 10915 -121.055 32.9125 5623806.9733687285 1440834.4849815036 +10915 4326 5623806.9733687285 1440834.4849815036 -121.055 32.91250000000001 +10910 10915 -114.485 35.6575 7605759.772572422 2443890.0068066833 +10915 4326 7605759.772572422 2443890.0068066833 -114.485 35.6575 +10910 10916 -116.285 33.305 6550973.0757064605 2054604.0248969968 +10916 4326 6550973.0757064605 2054604.0248969968 -116.285 33.305 +10910 10916 -117.96350000000001 32.6075 6033992.027722262 1805152.9211946672 +10916 4326 6033992.027722262 1805152.9211946672 -117.96350000000001 32.6075 +10910 10916 -114.6065 34.0025 7059785.79330495 2312340.457103632 +10916 4326 7059785.79330495 2312340.457103632 -114.6065 34.0025 +10910 10917 -119.285 37.269999999999996 63315.465344822995 -82731.13159650704 +10917 4326 63315.465344822995 -82731.13159650704 -119.28500000000001 37.27 +10910 10917 -123.93350000000001 33.004 -367870.9056195842 -548947.5140136816 +10917 4326 -367870.9056195842 -548947.5140136816 -123.93350000000002 33.004 +10910 10917 -114.63650000000001 41.536 447908.0593044227 403648.150371247 +10917 4326 447908.0593044227 403648.150371247 -114.63650000000003 41.536 +10910 10921 -122.22 40.8 1981436.393996671 662877.7471431005 +10921 4326 1981436.393996671 662877.7471431005 -122.22 40.8 +10910 10921 -124.227 39.711000000000006 1809032.8706182928 544364.0613099996 +10921 4326 1809032.8706182928 544364.0613099996 -124.227 39.71100000000001 +10910 10921 -120.213 41.888999999999996 2148310.5570726353 785305.6470252918 +10921 4326 2148310.5570726353 785305.6470252918 -120.21300000000001 41.88899999999998 +10910 10922 -121.80000000000001 39.09 2017301.8204829379 658016.692001398 +10922 4326 2017301.8204829379 658016.692001398 -121.80000000000001 39.089999999999996 +10910 10922 -123.834 38.127 1839195.8283859724 552723.7703899188 +10922 4326 1839195.8283859724 552723.7703899188 -123.834 38.127 +10910 10922 -119.766 40.053 2190614.0811918867 767255.0743562812 +10922 4326 2190614.0811918867 767255.0743562812 -119.76600000000002 40.053000000000004 +10910 10923 -120.425 37.72 2006611.9340582322 635399.2547793326 +10923 4326 2006611.9340582322 635399.2547793326 -120.42499999999998 37.71999999999999 +10910 10923 -122.7605 36.82899999999999 1798347.426047734 538949.0753376134 +10923 4326 1798347.426047734 538949.0753376134 -122.7605 36.82899999999999 +10910 10923 -118.0895 38.611000000000004 2209937.041178584 736997.6950945166 +10923 4326 2209937.041178584 736997.6950945166 -118.08950000000002 38.611 +10910 10924 -118.815 36.68 2016534.9266023266 649444.111312916 +10924 4326 2016534.9266023266 649444.111312916 -118.81499999999998 36.67999999999997 +10910 10924 -121.6905 35.870000000000005 1757042.2782625472 562953.5650761565 +10924 4326 1757042.2782625472 562953.5650761565 -121.6905 35.87000000000002 +10910 10924 -115.93950000000001 37.489999999999995 2270631.083070669 743632.1654617812 +10924 4326 2270631.083070669 743632.1654617812 -115.93950000000002 37.489999999999995 +10910 10925 -117.77000000000001 34.285 2021176.3034016618 587100.3705963591 +10925 4326 2021176.3034016618 587100.3705963591 -117.77 34.28500000000002 +10910 10925 -121.055 32.9125 1714139.7936607758 439167.22925522097 +10925 4326 1714139.7936607758 439167.22925522097 -121.055 32.91250000000001 +10910 10925 -114.485 35.6575 2318240.215058904 744899.1637714047 +10925 4326 2318240.215058904 744899.1637714047 -114.485 35.6575 +10910 10926 -116.285 33.305 1996740.5868549028 626244.559176123 +10926 4326 1996740.5868549028 626244.559176123 -116.285 33.305 +10910 10926 -117.96350000000001 32.6075 1839164.448277042 550211.7107019561 +10926 4326 1839164.448277042 550211.7107019561 -117.96350000000001 32.6075 +10910 10926 -114.6065 34.0025 2151827.0133517752 704802.7808291488 +10926 4326 2151827.0133517752 704802.7808291488 -114.6065 34.0025 +10941 10942 47.245000000000005 49.08 8664001.508592393 5440952.880527123 +10942 4326 8664001.508592393 5440952.880527123 47.245000000000005 49.07999999999999 +10941 10942 46.5655 47.829 8617210.117150806 5300601.250875465 +10942 4326 8617210.117150806 5300601.250875465 46.565500000000014 47.829 +10941 10942 47.9245 50.330999999999996 8708214.485653736 5581756.852016644 +10942 4326 8708214.485653736 5581756.852016644 47.92449999999999 50.330999999999996 +10941 10943 51.0 46.47 9500000.0 5148329.20341926 +10943 4326 9500000.0 5148329.20341926 51.0 46.470000000000006 +10941 10943 48.3 41.682 9275182.895840405 4619840.513695257 +10943 4326 9275182.895840405 4619840.513695257 48.29999999999999 41.682 +10941 10943 53.7 51.257999999999996 9688465.592653532 5684253.118260104 +10943 4326 9688465.592653532 5684253.118260104 53.7 51.25799999999999 +10941 10944 57.0 46.22 10500000.0 5120539.68913919 +10944 4326 10500000.0 5120539.68913919 57.00000000000001 46.22 +10941 10944 54.3 41.756 10275440.868859727 4628060.590492099 +10944 4326 10275440.868859727 4628060.590492099 54.3 41.75599999999999 +10941 10944 59.7 50.684 10690804.37576109 5620411.564659001 +10944 4326 10690804.37576109 5620411.564659001 59.69999999999999 50.684000000000005 +10941 10945 63.0 48.79 11500000.0 5406274.0144964475 +10945 4326 11500000.0 5406274.0144964475 63.0 48.79 +10941 10945 60.3 43.461999999999996 11281491.60246818 4817589.667263186 +10945 4326 11281491.60246818 4817589.667263186 60.300000000000004 43.462 +10941 10945 65.7 54.118 11676533.070761586 6002421.308104198 +10945 4326 11676533.070761586 6002421.308104198 65.7 54.11799999999999 +10941 10946 69.0 48.02 12500000.0 5320651.405709876 +10946 4326 12500000.0 5320651.405709876 69.0 48.019999999999996 +10941 10946 66.3 41.333000000000006 12273971.32145528 4581073.968355712 +10946 4326 12273971.32145528 4581073.968355712 66.3 41.333000000000006 +10941 10946 71.7 54.707 12674019.57488002 6067960.995584278 +10946 4326 12674019.57488002 6067960.995584278 71.7 54.70700000000001 +10941 10947 75.0 48.425 13500000.0 5365685.238375756 +10947 4326 13500000.0 5365685.238375756 75.0 48.425 +10941 10947 72.3 43.0025 13279842.505331026 4766536.902307531 +10947 4326 13279842.505331026 4766536.902307531 72.30000000000001 43.00249999999998 +10941 10947 77.7 53.847500000000004 13677681.112879762 5972323.695165818 +10947 4326 13677681.112879762 5972323.695165818 77.7 53.8475 +10941 10948 81.0 47.685 14500000.0 5283403.609154979 +10948 4326 14500000.0 5283403.609154979 81.0 47.685 +10941 10948 78.3 42.7395 14278905.019429961 4737317.719014357 +10948 4326 14278905.019429961 4737317.719014357 78.3 42.73950000000001 +10941 10948 83.7 52.6305 14682796.474948484 5836925.458414521 +10948 4326 14682796.474948484 5836925.458414521 83.7 52.6305 +10941 10949 85.675 48.760000000000005 15402583.52935923 5403784.931919766 +10949 4326 15402583.52935923 5403784.931919766 85.67499999999998 48.760000000000005 +10941 10949 84.1675 47.014 15284633.731731614 5212699.169679676 +10949 4326 15284633.731731614 5212699.169679676 84.1675 47.013999999999996 +10941 10949 87.18249999999999 50.506 15512946.628554529 5597147.324275096 +10949 4326 15512946.628554529 5597147.324275096 87.18249999999999 50.506 +10968 10979 -89.195 27.035 1680054.1383515366 426767.7986196042 +10979 4326 1680054.1383515366 426767.7986196042 -89.195 27.035000000000004 +10968 10979 -96.4175 24.1415 947029.9578127052 122948.6574909853 +10979 4326 947029.9578127052 122948.6574909853 -96.4175 24.141499999999994 +10968 10979 -81.9725 29.9285 2374868.8224607455 772587.0465920082 +10979 4326 2374868.8224607455 772587.0465920082 -81.9725 29.928500000000007 +10968 10980 -89.195 27.035 5511988.642885618 1400156.8196181238 +10980 4326 5511988.642885618 1400156.8196181238 -89.195 27.035000000000004 +10968 10980 -96.4175 24.1415 3107053.667364518 403374.8605347286 +10980 4326 3107053.667364518 403374.8605347286 -96.4175 24.141499999999994 +10968 10980 -81.9725 29.9285 7791564.378152051 2534734.404829423 +10980 4326 7791564.378152051 2534734.404829423 -81.9725 29.928500000000007 +10968 10981 -90.78999999999999 28.92 2173876.3640234973 92450.86574349836 +10981 4326 2173876.3640234973 92450.86574349836 -90.78999999999999 28.92000000000001 +10968 10981 -93.517 27.93 1294535.5190567344 -261315.4986368407 +10981 4326 1294535.5190567344 -261315.4986368407 -93.517 27.930000000000003 +10968 10981 -88.063 29.91 3035984.4854915948 466835.69736403617 +10981 4326 3035984.4854915948 466835.69736403617 -88.063 29.910000000000014 +10968 10982 -95.13499999999999 28.665 3239314.7206613184 322775.78892770875 +10982 4326 3239314.7206613184 322775.78892770875 -95.135 28.664999999999978 +10968 10982 -96.6875 27.877499999999998 2747297.008445991 23346.40662049113 +10982 4326 2747297.008445991 23346.40662049113 -96.6875 27.877499999999998 +10968 10982 -93.5825 29.4525 3723517.3963141693 628546.6680696023 +10982 4326 3723517.3963141693 628546.6680696023 -93.5825 29.452499999999986 +10968 10983 -96.28999999999999 26.905 2720247.763004357 456286.98541056504 +10983 4326 2720247.763004357 456286.98541056504 -96.28999999999999 26.904999999999994 +10968 10983 -97.127 26.063499999999998 2450845.5605736747 146566.6035868948 +10983 4326 2450845.5605736747 146566.6035868948 -97.127 26.063500000000005 +10968 10983 -95.453 27.7465 2985561.1754287863 767774.0759961613 +10983 4326 2985561.1754287863 767774.0759961613 -95.453 27.74650000000001 +10968 10984 -96.195 26.805 2555351.475279702 9736576.391022194 +10984 4326 2555351.475279702 9736576.391022194 -96.195 26.804999999999996 +10968 10984 -96.4875 26.0805 2465063.239640202 9471177.759174066 +10984 4326 2465063.239640202 9471177.759174066 -96.4875 26.080499999999997 +10968 10984 -95.9025 27.5295 2644290.145180805 10002294.661263304 +10984 4326 2644290.145180805 10002294.661263304 -95.9025 27.529499999999995 +10968 10985 -92.89 27.15 1676219.1622161658 9851859.77060839 +10985 4326 1676219.1622161658 9851859.77060839 -92.89 27.149999999999995 +10968 10985 -95.617 25.764 779312.8733275264 9356792.43295495 +10985 4326 779312.8733275264 9356792.43295495 -95.617 25.763999999999996 +10968 10985 -90.163 28.536 2551245.356845614 10366325.02126023 +10985 4326 2551245.356845614 10366325.02126023 -90.163 28.535999999999994 +10968 10986 -86.93 27.1 1663168.985201555 9833681.352848541 +10986 4326 1663168.985201555 9833681.352848541 -86.93 27.099999999999998 +10968 10986 -89.648 24.265 758390.053226964 8812061.645776361 +10986 4326 758390.053226964 8812061.645776361 -89.648 24.264999999999993 +10968 10986 -84.212 29.935 2523351.00811074 10874809.068027826 +10986 4326 2523351.00811074 10874809.068027826 -84.21200000000002 29.935000000000002 +10968 10987 -82.63 26.880000000000003 1109171.087717276 9757138.092334764 +10987 4326 1109171.087717276 9757138.092334764 -82.63 26.88 +10968 10987 -83.944 24.126 658619.337986241 8763488.111567315 +10987 4326 658619.337986241 8763488.111567315 -83.94400000000002 24.125999999999994 +10968 10987 -81.316 29.634 1539998.2952122 10754796.71014615 +10987 4326 1539998.2952122 10754796.71014615 -81.316 29.633999999999997 +10875 11012 -1.675 71.97999999999999 545745.5909756977 7987204.701699016 +11012 4326 545745.5909756977 7987204.701699016 -1.6749999999999994 71.97999999999999 +10875 11012 -3.1825 64.321 491176.084432062 7132797.536213546 +11012 4326 491176.084432062 7132797.536213546 -3.1825 64.321 +10875 11012 -0.16749999999999998 79.639 556848.9719669104 8842675.737357581 +11012 4326 556848.9719669104 8842675.737357581 -0.16749999999999912 79.63900000000001 +10875 11013 3.0 69.265 500000.00000000035 7683911.294158417 +11013 4326 500000.00000000035 7683911.294158417 3.0000000000000004 69.26499999999999 +10875 11013 0.30000000000000004 57.3985 337763.0438618078 6364967.763548184 +11013 4326 337763.0438618078 6364967.763548184 0.30000000000000004 57.398500000000006 +10875 11013 5.699999999999999 81.1315 546454.0815869356 9008964.328321258 +11013 4326 546454.0815869356 9008964.328321258 5.699999999999996 81.1315 +10875 11014 8.995000000000001 70.44 499813.1730774868 7814942.015038704 +11014 4326 499813.1730774868 7814942.015038704 8.995000000000001 70.44 +10875 11014 6.2905 58.236000000000004 340925.10759103973 6458184.136129763 +11014 4326 340925.10759103973 6458184.136129763 6.2905000000000015 58.23599999999999 +10875 11014 11.6995 82.644 538573.1652445634 9177619.525414312 +11014 4326 538573.1652445634 9177619.525414312 11.699499999999999 82.644 +10875 11015 15.0 71.94 500000.0000000005 7982240.0273732785 +11015 4326 500000.0000000005 7982240.0273732785 14.999999999999998 71.94 +10875 11015 12.3 61.086000000000006 354391.9125917915 6775370.445821834 +11015 4326 354391.9125917915 6775370.445821834 12.3 61.086 +10875 11015 17.7 82.794 537797.8550219743 9194346.54306115 +11015 4326 537797.8550219743 9194346.54306115 17.699999999999996 82.794 +10875 11016 21.0 76.02000000000001 500000.00000000023 8437417.03610814 +11016 4326 500000.00000000023 8437417.03610814 21.0 76.02000000000001 +10875 11016 18.3 68.83800000000001 391251.0996430032 7638688.682189899 +11016 4326 391251.0996430032 7638688.682189899 18.3 68.83800000000001 +10875 11016 23.7 83.202 535668.2769881877 9239843.552812582 +11016 4326 535668.2769881877 9239843.552812582 23.699999999999996 83.20199999999998 +10875 11017 26.994999999999997 76.28999999999999 499867.7163106723 8467544.910587056 +11017 4326 499867.7163106723 8467544.910587056 26.994999999999997 76.28999999999998 +10875 11017 24.290499999999998 69.351 393395.4559479171 7695860.294613092 +11017 4326 393395.4559479171 7695860.294613092 24.290499999999998 69.35100000000001 +10875 11017 29.6995 83.229 535520.7086405429 9242854.098074675 +11017 4326 535520.7086405429 9242854.098074675 29.699499999999997 83.22899999999998 +10875 11018 33.0 76.72999999999999 499999.9999999998 8516643.583028043 +11018 4326 499999.9999999998 8516643.583028043 33.0 76.72999999999999 +10875 11018 30.3 70.187 397890.6991023043 7788991.09925475 +11018 4326 397890.6991023043 7788991.09925475 30.299999999999997 70.18699999999998 +10875 11018 35.7 83.273 535297.4979537936 9247760.986976491 +11018 4326 535297.4979537936 9247760.986976491 35.7 83.273 +10875 11019 37.0 76.03 446112.88482169656 8439445.664182661 +11019 4326 446112.88482169656 8439445.664182661 37.0 76.02999999999999 +10875 11019 36.1 73.294 406984.15028630174 8135532.004242168 +11019 4326 406984.15028630174 8135532.004242168 36.10000000000001 73.29399999999998 +10875 11019 37.9 78.76599999999999 476078.18751932355 8744083.13374944 +11019 4326 476078.18751932355 8744083.13374944 37.9 78.76599999999998 +10875 11020 -1.675 71.97999999999999 545745.5909756977 7987204.701699016 +11020 4326 545745.5909756977 7987204.701699016 -1.6749999999999994 71.97999999999999 +10875 11020 -3.1825 64.321 491176.084432062 7132797.536213546 +11020 4326 491176.084432062 7132797.536213546 -3.1825 64.321 +10875 11020 -0.16749999999999998 79.639 556848.9719669104 8842675.737357581 +11020 4326 556848.9719669104 8842675.737357581 -0.16749999999999912 79.63900000000001 +10875 11021 3.0 69.265 500000.00000000035 7683911.294158417 +11021 4326 500000.00000000035 7683911.294158417 3.0000000000000004 69.26499999999999 +10875 11021 0.30000000000000004 57.3985 337763.0438618078 6364967.763548184 +11021 4326 337763.0438618078 6364967.763548184 0.30000000000000004 57.398500000000006 +10875 11021 5.699999999999999 81.1315 546454.0815869356 9008964.328321258 +11021 4326 546454.0815869356 9008964.328321258 5.699999999999996 81.1315 +10875 11022 8.995000000000001 70.44 499813.1730774868 7814942.015038704 +11022 4326 499813.1730774868 7814942.015038704 8.995000000000001 70.44 +10875 11022 6.2905 58.236000000000004 340925.10759103973 6458184.136129763 +11022 4326 340925.10759103973 6458184.136129763 6.2905000000000015 58.23599999999999 +10875 11022 11.6995 82.644 538573.1652445634 9177619.525414312 +11022 4326 538573.1652445634 9177619.525414312 11.699499999999999 82.644 +10875 11023 15.0 71.94 500000.0000000005 7982240.0273732785 +11023 4326 500000.0000000005 7982240.0273732785 14.999999999999998 71.94 +10875 11023 12.3 61.086000000000006 354391.9125917915 6775370.445821834 +11023 4326 354391.9125917915 6775370.445821834 12.3 61.086 +10875 11023 17.7 82.794 537797.8550219743 9194346.54306115 +11023 4326 537797.8550219743 9194346.54306115 17.699999999999996 82.794 +10875 11024 21.0 76.02000000000001 500000.00000000023 8437417.03610814 +11024 4326 500000.00000000023 8437417.03610814 21.0 76.02000000000001 +10875 11024 18.3 68.83800000000001 391251.0996430032 7638688.682189899 +11024 4326 391251.0996430032 7638688.682189899 18.3 68.83800000000001 +10875 11024 23.7 83.202 535668.2769881877 9239843.552812582 +11024 4326 535668.2769881877 9239843.552812582 23.699999999999996 83.20199999999998 +10875 11025 26.994999999999997 76.28999999999999 499867.7163106723 8467544.910587056 +11025 4326 499867.7163106723 8467544.910587056 26.994999999999997 76.28999999999998 +10875 11025 24.290499999999998 69.351 393395.4559479171 7695860.294613092 +11025 4326 393395.4559479171 7695860.294613092 24.290499999999998 69.35100000000001 +10875 11025 29.6995 83.229 535520.7086405429 9242854.098074675 +11025 4326 535520.7086405429 9242854.098074675 29.699499999999997 83.22899999999998 +10875 11026 33.0 76.72999999999999 499999.9999999998 8516643.583028043 +11026 4326 499999.9999999998 8516643.583028043 33.0 76.72999999999999 +10875 11026 30.3 70.187 397890.6991023043 7788991.09925475 +11026 4326 397890.6991023043 7788991.09925475 30.299999999999997 70.18699999999998 +10875 11026 35.7 83.273 535297.4979537936 9247760.986976491 +11026 4326 535297.4979537936 9247760.986976491 35.7 83.273 +10875 11027 37.0 76.03 446112.88482169656 8439445.664182661 +11027 4326 446112.88482169656 8439445.664182661 37.0 76.02999999999999 +10875 11027 36.1 73.294 406984.15028630174 8135532.004242168 +11027 4326 406984.15028630174 8135532.004242168 36.10000000000001 73.29399999999998 +10875 11027 37.9 78.76599999999999 476078.18751932355 8744083.13374944 +11027 4326 476078.18751932355 8744083.13374944 37.9 78.76599999999998 +20046 11114 -78.84 1.855 1137697.7808411838 696931.366710024 +11114 4326 1137697.7808411838 696931.366710024 -78.84 1.8550000000000006 +20046 11114 -79.074 1.2925 1111687.4847483363 634706.7826888026 +11114 4326 1111687.4847483363 634706.7826888026 -79.074 1.2925000000000002 +20046 11114 -78.606 2.4175 1163680.7322503596 759170.8020266877 +11114 4326 1163680.7322503596 759170.8020266877 -78.606 2.4175 +20046 11115 -77.08500000000001 5.120000000000001 999169.2743678644 1057922.6297126675 +11115 4326 999169.2743678644 1057922.6297126675 -77.08500000000001 5.120000000000002 +20046 11115 -78.43950000000001 0.5390000000000001 848376.008184605 551383.7319997817 +11115 4326 848376.008184605 551383.7319997817 -78.43950000000001 0.5390000000000008 +20046 11115 -75.73049999999999 9.701 1147831.0198692288 1564843.4889018177 +11115 4326 1147831.0198692288 1564843.4889018177 -75.73049999999999 9.701 +20046 11116 -74.08500000000001 4.655 999168.7014881916 1006501.8401401829 +11116 4326 999168.7014881916 1006501.8401401829 -74.08500000000001 4.655 +20046 11116 -75.43950000000001 -1.7934999999999999 848443.1535960981 293395.2246209133 +11116 4326 848443.1535960981 293395.2246209133 -75.43950000000001 -1.7935 +20046 11116 -72.73049999999999 11.1035 1147172.2297709943 1720015.1159139161 +11116 4326 1147172.2297709943 1720015.1159139161 -72.73049999999999 11.1035 +20046 11117 -71.08500000000001 4.145 999168.135717822 950105.622158002 +11117 4326 999168.135717822 950105.622158002 -71.08500000000001 4.1450000000000005 +20046 11117 -72.43950000000001 -3.3925000000000005 848633.3876775751 116533.11696936039 +11117 4326 848633.3876775751 116533.11696936039 -72.43950000000001 -3.3925 +20046 11117 -69.73049999999999 11.6825 1146874.6583408664 1784079.152595962 +11117 4326 1146874.6583408664 1784079.152595962 -69.73049999999999 11.6825 +20046 11118 -68.23 2.0299999999999994 983035.2046311806 716234.7466478961 +11118 4326 983035.2046311806 716234.7466478961 -68.23 2.0299999999999994 +20046 11118 -69.45400000000001 -1.822 846831.7329316869 290241.7050322107 +11118 4326 846831.7329316869 290241.7050322107 -69.45400000000001 -1.8219999999999996 +20046 11118 -67.006 5.881999999999998 1118662.687511607 1142301.7304950112 +11118 4326 1118662.687511607 1142301.7304950112 -67.006 5.882000000000001 +20033 20002 -2.625 53.370000000000005 166983.79818240905 118973.17658441774 +20033 20002 -3.0975 53.118 135318.75608369784 91065.35165210096 +20033 20002 -2.1525 53.622 198277.815707671 147090.49942830758 +4200 20004 21.22 54.82 4514141.887962946 6077321.106840183 +20004 4326 4514141.887962946 6077321.106840183 21.218000168948326 54.819781174608 +4200 20004 19.735 54.37 4417784.7652128665 6027943.58067649 +20004 4326 4417784.7652128665 6027943.58067649 19.73301337496357 54.36975076297361 +4200 20004 22.705000000000002 55.27 4608372.280464553 6128721.082449393 +20004 4326 4608372.280464553 6128721.082449393 22.702987920541855 55.26981174545414 +4200 20005 28.305 62.58 5567074.0397727005 6942371.260837219 +20005 4326 5567074.0397727005 6942371.260837219 28.30256832674856 62.580005607855405 +4200 20005 26.7795 56.379 5486377.119215266 6250894.172139804 +20005 4326 5486377.119215266 6250894.172139804 26.777466276548893 56.378894544184725 +4200 20005 29.8305 68.781 5614342.353013161 7635762.354308665 +20005 4326 5614342.353013161 7635762.354308665 29.827424851300442 68.78111849054712 +4200 20006 33.0 60.18 6500000.0 6674243.916115953 +20006 4326 6500000.0 6674243.916115953 32.99781794242752 60.18005228897135 +4200 20006 30.3 51.324000000000005 6311801.421140806 5691694.092067871 +20006 4326 6311801.421140806 5691694.092067871 30.29823781553074 51.32388078802876 +4200 20006 35.7 69.036 6607823.0147247845 7663946.908246142 +20006 4326 6607823.0147247845 7663946.908246142 35.69701759871923 69.03622769281283 +4200 20007 39.004999999999995 56.205 7500310.315662136 6231497.6277802335 +20007 4326 7500310.315662136 6231497.6277802335 39.00314905443574 56.205088009936674 +4200 20007 36.3005 44.4825 7285240.606075051 4931069.50217726 +20007 4326 7285240.606075051 4931069.50217726 36.2990280222276 44.48236497104047 +4200 20007 41.7095 67.92750000000001 7613641.372082959 7540418.099849011 +20007 4326 7613641.372082959 7540418.099849011 41.70681884720197 67.92781396009973 +4200 20008 45.0 61.05 8500000.0 6771183.262177025 +20008 4326 8500000.0 6771183.262177025 44.99800256726887 61.050257257340455 +4200 20008 42.3 43.175999999999995 8280459.8449193435 4785897.735686462 +20008 4326 8280459.8449193435 4785897.735686462 42.29864290339988 43.17592002865417 +4200 20008 47.7 78.92399999999999 8557909.498902408 8766488.51784769 +20008 4326 8557909.498902408 8766488.51784769 47.6950864618377 78.92457497241807 +4200 20009 51.0 61.395 9500000.0 6809628.224716597 +20009 4326 9500000.0 6809628.224716597 50.99813864717582 61.39535013138184 +4200 20009 48.3 43.3905 9281230.411623167 4809730.342741161 +20009 4326 9281230.411623167 4809730.342741161 48.29873595311193 43.390494490353795 +4200 20009 53.7 79.3995 9555452.932295388 8819526.123378675 +20009 4326 9555452.932295388 8819526.123378675 53.69530424710774 79.40017550092794 +4200 20010 57.0 66.19 10500000.0 7344153.685754918 +20010 4326 10500000.0 7344153.685754918 56.99799886410718 66.19051399956604 +4200 20010 54.3 52.042 10314756.504907826 5771560.60961814 +20010 4326 10314756.504907826 5771560.60961814 54.2986333012739 52.042224797735486 +4200 20010 59.7 80.338 10550593.16442146 8924210.708048513 +20010 4326 10550593.16442146 8924210.708048513 59.69538558172604 80.33877415122741 +4200 20011 63.0 66.215 11500000.0 7346941.491984668 +20011 4326 11500000.0 7346941.491984668 62.99823193980998 66.21558884736703 +4200 20011 60.3 52.2155 11315474.996915985 5790860.97655006 +20011 4326 11315474.996915985 5790860.97655006 60.29877627798768 52.215795926092355 +4200 20011 65.7 80.21449999999999 11551233.491586909 8910434.688238192 +20011 4326 11551233.491586909 8910434.688238192 65.69602321443386 80.2153479264253 +4200 20012 69.0 65.585 12500000.0 7276691.558163891 +20012 4326 12500000.0 7276691.558163891 68.99852438075514 65.58564228129114 +4200 20012 66.3 55.2485 12328304.740737192 6128327.034424901 +20012 4326 12328304.740737192 6128327.034424901 66.29885654323189 55.24891716280268 +4200 20012 71.7 75.9215 12573320.784558633 8431619.925727122 +20012 4326 12573320.784558633 8431619.925727122 71.6976677562309 75.92234680701002 +4200 20013 75.0 66.44 13500000.0 7372032.155122787 +20013 4326 13500000.0 7372032.155122787 74.99874715306994 66.44071269326217 +4200 20013 72.3 54.497 13325079.224468678 6044699.115933595 +20013 4326 13325079.224468678 6044699.115933595 72.29906211412265 54.49745505200619 +4200 20013 77.7 78.383 13560699.524258604 8706146.590604123 +20013 4326 13560699.524258604 8706146.590604123 77.69773290397787 78.38393872850747 +4200 20014 81.0 65.86 14500000.0 7307355.496511509 +20014 4326 14500000.0 7307355.496511509 80.99905818444867 65.86074571539577 +4200 20014 78.3 52.207 14315439.757241124 5789915.415068631 +20014 4326 14315439.757241124 5789915.415068631 78.29929730905346 52.20744942706851 +4200 20014 83.7 79.513 14554865.979841966 8832186.181052761 +20014 4326 14554865.979841966 8832186.181052761 83.6981328264797 79.51399782213117 +4200 20015 87.0 65.17 15500000.0 7230419.008135735 +20015 4326 15500000.0 7230419.008135735 86.99936864884313 65.17076492832118 +4200 20015 84.3 50.68 15309176.217329813 5620065.466081332 +20015 4326 15309176.217329813 5620065.466081332 84.29950767652322 50.680448173439785 +4200 20015 89.7 79.66 15554105.461432375 8848583.012840003 +20015 4326 15554105.461432375 8848583.012840003 89.69878110829829 79.661029549359 +4200 20016 93.0 65.62 16500000.0 7280594.179302066 +20016 4326 16500000.0 7280594.179302066 92.99965444920409 65.62079457620393 +4200 20016 90.3 51.463 16312371.19563157 5707155.118561674 +20016 4326 16312371.19563157 5707155.118561674 90.29969386999117 51.463488159384646 +4200 20016 95.7 79.77699999999999 16553499.892234067 8861633.636235029 +20016 4326 16553499.892234067 8861633.636235029 95.69946349009277 79.77804820662341 +4200 20017 99.0 65.52499999999999 17500000.0 7270001.392661486 +20017 4326 17500000.0 7270001.392661486 98.99995625692779 65.52580182672459 +4200 20017 96.3 51.3095 17311742.04845411 5690081.267726861 +20017 4326 17311742.04845411 5690081.267726861 96.29989328538964 51.30999710444767 +4200 20017 101.7 79.7405 17553688.833544858 8857562.279994301 +20017 4326 17553688.833544858 8857562.279994301 101.70016589588217 79.74155169540556 +4200 20018 105.0 64.56 18500000.0 7162408.624891741 +20018 4326 18500000.0 7162408.624891741 105.00024968799015 64.5607800226099 +4200 20018 102.3 51.132 18311016.230525482 5670338.326859696 +20018 4326 18311016.230525482 5670338.326859696 102.30009290380566 51.13249525999032 +4200 20018 107.7 77.988 18562733.14904189 8662090.29101531 +20018 4326 18562733.14904189 8662090.29101531 107.70074537583362 77.98901547520622 +4200 20019 111.0 62.975 19500000.0 6985719.868823676 +20019 4326 19500000.0 6985719.868823676 111.00051057221933 62.9757339240016 +4200 20019 108.3 50.5235 19308541.915251147 5602659.755338392 +20019 4326 19308541.915251147 5602659.755338392 108.30028805491436 50.52397340831288 +4200 20019 113.7 75.4265 19575843.057047803 8376418.5363391265 +20019 4326 19575843.057047803 8376418.5363391265 113.70111113480458 75.42745245378673 +4200 20020 117.0 62.735 20500000.0 6958969.176764962 +20020 4326 20500000.0 6958969.176764962 117.00077187896638 62.73570331241798 +4200 20020 114.3 50.832499999999996 20309795.68677516 5637026.654582188 +20020 4326 20309795.68677516 5637026.654582188 114.30048411105331 50.8329623906828 +4200 20020 119.7 74.63749999999999 20579851.45738725 8288434.675508902 +20020 4326 20579851.45738725 8288434.675508902 119.70150982860767 74.6384058304563 +4200 20021 123.0 62.754999999999995 21500000.0 6961198.365997623 +20021 4326 21500000.0 6961198.365997623 123.00102965078561 62.75566676042915 +4200 20021 120.3 52.634499999999996 21317217.163751446 5837472.916796084 +20021 4326 21317217.163751446 5837472.916796084 120.30069946803454 52.6349740116131 +4200 20021 125.7 72.8755 21588746.46108203 8091966.483516503 +20021 4326 21588746.46108203 8091966.483516503 125.70175488378634 72.87633103885827 +4200 20022 129.0 57.93 22500000.0 6423598.670127823 +20022 4326 22500000.0 6423598.670127823 129.0011054464138 57.93052420710647 +4200 20022 126.3 43.818 22282775.33302377 4857230.979267927 +20022 4326 22282775.33302377 4857230.979267927 126.30075416549118 43.818242322185654 +4200 20022 131.7 72.042 22592925.042030107 7999037.963814366 +20022 4326 22592925.042030107 7999037.963814366 131.70203471177607 72.04276082321903 +4200 20023 135.0 59.39 23500000.0 6586229.628407109 +20023 4326 23500000.0 6586229.628407109 135.0013604047437 59.39049726608047 +4200 20023 132.3 44.306000000000004 23284553.73911299 4911457.322981693 +20023 4326 23284553.73911299 4911457.322981693 132.30091328885933 44.306211182169626 +4200 20023 137.7 74.474 23580680.199776404 8270202.881338667 +20023 4326 23580680.199776404 8270202.881338667 137.70272409997511 74.47473362945344 +4200 20024 141.0 61.055 24500000.0 6771740.421450425 +20024 4326 24500000.0 6771740.421450425 141.00163426725572 61.055462614188215 +4200 20024 138.3 47.36150000000001 24296040.706018656 5251065.475993207 +20024 4326 24296040.706018656 5251065.475993207 138.3011143264255 47.36172439854223 +4200 20024 143.7 74.74849999999999 24579288.448659074 8300812.343378751 +20024 4326 24579288.448659074 8300812.343378751 143.703134164017 74.74916037218298 +4200 20025 147.0 60.209999999999994 25500000.0 6677586.4401542945 +20025 4326 25500000.0 6677586.4401542945 147.0017755976505 60.21037244216521 +4200 20025 144.3 45.261 25288079.346784852 5017586.979982033 +20025 4326 25288079.346784852 5017586.979982033 144.30120845361577 45.26112299736569 +4200 20025 149.7 75.15899999999999 25577203.725286763 8346588.240981594 +20025 4326 25577203.725286763 8346588.240981594 149.70355563309317 75.15957972764325 +4200 20026 153.0 61.015 26500000.0 6767283.158904388 +20026 4326 26500000.0 6767283.158904388 153.0019871458978 61.0153035923279 +4200 20026 150.3 47.294500000000006 26295782.446630247 5243617.138046312 +20026 4326 26295782.446630247 5243617.138046312 150.30137899356052 47.29459587357377 +4200 20026 155.7 74.7355 26579354.402264643 8299362.701628698 +20026 4326 26579354.402264643 8299362.701628698 155.70375139104772 74.73597826818597 +4200 20027 159.0 63.735 27500000.0 7070436.407700245 +20027 4326 27500000.0 7070436.407700245 159.00233247571654 63.73525666005456 +4200 20027 156.3 51.6165 27313001.699669942 5724229.319586072 +20027 4326 27313001.699669942 5724229.319586072 156.30162448026763 51.61659706725653 +4200 20027 161.7 75.8535 27573667.61180203 8424036.598154908 +20027 4326 27573667.61180203 8424036.598154908 161.70430793661774 75.85389175601911 +4200 20028 165.0 62.25 28500000.0 6904913.303640777 +20028 4326 28500000.0 6904913.303640777 165.00234707610946 62.25013600624102 +4200 20028 162.3 55.248 28328302.584716987 6128271.391067221 +20028 4326 28328302.584716987 6128271.391067221 162.3018797669474 55.24807094372626 +4200 20028 167.7 69.252 28606762.037419833 7688022.353117352 +20028 4326 28606762.037419833 7688022.353117352 167.7031377008612 69.25219126753747 +4200 20029 171.0 62.32 29500000.0 6912714.948260343 +20029 4326 29500000.0 6912714.948260343 171.00245569934953 62.3200348040961 +4200 20029 168.3 55.237 29328255.15561024 6127047.238102845 +20029 4326 29328255.15561024 6127047.238102845 168.30197284507864 55.23697777392646 +4200 20029 173.7 69.40299999999999 29606019.421855878 7704853.15403719 +20029 4326 29606019.421855878 7704853.15403719 173.70328012982165 69.40308342083833 +4200 20030 177.0 66.62 30500000.0 7392105.210780848 +20030 4326 30500000.0 7392105.210780848 177.00295778639511 66.61998500680816 +4200 20030 174.3 62.147 30359233.877299488 6896367.144476949 +20030 4326 30359233.877299488 6896367.144476949 174.30248717076952 62.146974491563526 +4200 20030 179.7 71.093 30597658.33813204 7893239.748026698 +20030 4326 30597658.33813204 7893239.748026698 179.70365027657212 71.09299131899247 +4200 20031 -177.0 68.0 31500000.0 7546013.844298534 +20031 4326 31500000.0 7546013.844298534 -176.99680785631665 67.99988914928979 +4200 20031 -179.7 64.71499999999999 31371298.715467323 7182432.038391958 +20031 4326 31371298.715467323 7182432.038391958 -179.6972159432352 64.71489921957169 +4200 20031 -174.3 71.28500000000001 31596702.85486853 7914643.984382171 +20031 4326 31596702.85486853 7914643.984382171 -174.29626064378357 71.2848765599116 +4200 20032 -171.485 65.69 32477711.532598134 7288485.446295194 +20032 4326 32477711.532598134 7288485.446295194 -171.48206827516276 65.68975651608112 +4200 20032 -173.7485 64.349 32367220.71177891 7141756.667395102 +20032 4326 32367220.71177891 7141756.667395102 -173.7457160996516 64.34878372334664 +4200 20032 -169.2215 67.031 32577471.790679533 7439047.596853634 +20032 4326 32577471.790679533 7439047.596853634 -169.21840737966917 67.03072845013044 +20041 20042 -110.60499999999999 -27.13 539145.0155570199 6999104.3966913335 +20041 20042 -112.9495 -30.198999999999998 312333.49123263464 6657557.169748049 +20041 20042 -108.2605 -24.061 778577.3380772215 7336303.2070909105 +7844 20047 153.03 -27.0 152977.61701066545 310809.99069144006 +20047 4326 152977.61701066545 310809.99069144006 153.03 -27.000000000000004 +7844 20047 152.436 -29.124 95110.2520420705 75298.41850988267 +20047 4326 95110.2520420705 75298.41850988267 152.436 -29.124 +7844 20047 153.624 -24.876 213056.11113807865 545975.3971938018 +20047 4326 213056.11113807865 545975.3971938018 153.624 -24.876 +20041 20048 -74.995 -38.855000000000004 500433.8410580684 5699314.0104445405 +20048 4326 500433.8410580684 5699314.0104445405 -74.995 -38.855000000000004 +20041 20048 -77.6995 -57.3095 337399.9150358017 3644936.4607056994 +20048 4326 337399.9150358017 3644936.4607056994 -77.6995 -57.30950000000001 +20041 20048 -72.2905 -20.4005 782784.4819780085 7741866.394019629 +20048 4326 782784.4819780085 7741866.394019629 -72.2905 -20.400499999999997 +20041 20049 -69.0 -38.685 500000.0 5718178.35995209 +20049 4326 500000.0 5718178.35995209 -69.0 -38.684999999999995 +20041 20049 -71.7 -57.7515 339326.61691853346 3595752.8485343764 +20049 4326 339326.61691853346 3595752.8485343764 -71.7 -57.7515 +20041 20049 -66.3 -19.618500000000004 783190.0307251445 7828493.0824960265 +20049 4326 783190.0307251445 7828493.0824960265 -66.3 -19.618500000000004 +6318 20050 -74.495 40.69 1362726.9577221614 1448166.2939536506 +20050 4326 1362726.9577221614 1448166.2939536506 -74.49500000000002 40.69 +6318 20050 -78.1085 38.845 329990.3726462668 802759.3141534372 +20050 4326 329990.3726462668 802759.3141534372 -78.10849999999999 38.84499999999999 +6318 20050 -70.8815 42.535000000000004 2340491.0134991584 2135171.471926971 +20050 4326 2340491.0134991584 2135171.471926971 -70.88149999999999 42.53500000000001 +4201 20135 27.0 13.11 500000.0 1449170.607610868 +20135 4326 500000.0 1449170.607610868 27.000559022552125 13.110719954274948 +4201 20135 24.290999999999997 5.1 199625.251050586 564297.9771804789 +20135 4326 199625.251050586 564297.9771804789 24.291482230369095 5.1014111088329726 +4201 20135 29.709000000000003 21.12 781402.6158055514 2337628.081422052 +20135 4326 781402.6158055514 2337628.081422052 29.709651005531587 21.12008758545601 +4201 20136 32.995 12.865 499457.49061069835 1422079.2875080716 +20136 4326 499457.49061069835 1422079.2875080716 32.99569956776081 12.865725952634538 +4201 20136 30.290499999999998 4.4275 199277.31023725466 489885.7725134682 +20136 4326 199277.31023725466 489885.7725134682 30.291122739404948 4.428965276839773 +4201 20136 35.6995 21.302500000000002 780070.4081405823 2357824.7954235254 +20136 4326 780070.4081405823 2357824.7954235254 35.700295875605335 21.302548754913555 +4201 20137 39.0 12.705000000000002 500000.0000000007 1404387.2658084652 +20137 4326 500000.0000000007 1404387.2658084652 39.00085451557927 12.705722286351788 +4201 20137 36.3 4.3305 200293.83002981637 479149.00480884494 +20137 4326 200293.83002981637 479149.00480884494 36.300776420751106 4.331962483662354 +4201 20137 41.7 21.0795 780543.3602128592 2333126.46568368 +20137 4326 780543.3602128592 2333126.46568368 41.700954897906115 21.079540299878392 +4201 20138 44.995000000000005 8.48 499449.6574206742 937277.8348045768 +20138 4326 499449.6574206742 937277.8348045768 44.99598885840807 8.48108910989854 +4201 20138 42.2995 4.547000000000001 200325.73757712537 503104.67682693247 +20138 4326 200325.73757712537 503104.67682693247 42.30042735772866 4.548451815158561 +4201 20138 47.6905 12.412999999999997 792536.0850001685 1373577.1177069987 +20138 4326 792536.0850001685 1373577.1177069987 47.691554095898844 12.413727475004727 +4202 20249 111.61500000000001 -27.515 560737.8133182325 6956360.545064095 +20249 4326 560737.8133182325 6956360.545064095 111.61640925172087 -27.513787332768242 +4202 20249 109.4685 -36.807500000000005 363385.2776156397 5925373.721950972 +20249 4326 363385.2776156397 5925373.721950972 109.47006815783233 -36.806430281231734 +4202 20249 113.7615 -18.222500000000004 792063.4413046406 7982988.531478607 +20249 4326 792063.4413046406 7982988.531478607 113.76280669018067 -18.221185925758803 +4202 20250 117.0 -25.57 500000.0 7171924.7511040885 +20250 4326 500000.0 7171924.7511040885 117.00137681653247 -25.568712658098512 +4202 20250 114.3 -37.234 260477.97629321108 5875738.186101207 +20250 4326 260477.97629321108 5875738.186101207 114.30157304647737 -37.232870272158316 +4202 20250 119.7 -13.906000000000002 791790.4571975596 8461011.087723939 +20250 4326 791790.4571975596 8461011.087723939 119.70126429221905 -13.90462382368437 +4202 20251 123.005 -24.265 500507.51040629257 7316424.4790419955 +20251 4326 500507.51040629257 7316424.4790419955 123.00634039980856 -24.26364552695866 +4202 20251 120.3005 -36.6895 258809.35335156615 5936161.155917596 +20251 4326 258809.35335156615 5936161.155917596 120.30204175897768 -36.688283381589564 +4202 20251 125.7095 -11.840500000000002 795228.5090123317 8689645.818530632 +20251 4326 795228.5090123317 8689645.818530632 125.71072963332574 -11.839087376199327 +4202 20252 128.995 -23.240000000000002 499488.5009014612 7429903.011164343 +20252 4326 499488.5009014612 7429903.011164343 128.99629500887704 -23.23858751475905 +4202 20252 126.2905 -35.966 255665.05463632802 6016414.321224136 +20252 4326 255665.05463632802 6016414.321224136 126.29199191973274 -35.964699234455026 +4202 20252 131.6995 -10.514000000000003 795480.1051362706 8836482.086552693 +20252 4326 795480.1051362706 8836482.086552693 131.7006884796613 -10.51256137873583 +4202 20253 135.005 -24.795 500505.3843177764 7257741.807484416 +20253 4326 500505.3843177764 7257741.807484416 135.00626204246728 -24.793541528487555 +4202 20253 132.3005 -39.1185 266617.33167014824 5666587.937353768 +20253 4326 266617.33167014824 5666587.937353768 132.30200184370582 -39.117152091056845 +4202 20253 137.7095 -10.471500000000002 796615.9863966499 8841176.69593796 +20253 4326 796615.9863966499 8841176.69593796 137.71064118175488 -10.470037766387973 +4202 20254 141.005 -24.580000000000002 500506.252000122 7281547.539378386 +20254 4326 500506.252000122 7281547.539378386 141.006197981298 -24.578492831568646 +4202 20254 138.3005 -44.425000000000004 285118.40462005755 5077359.762972658 +20254 4326 285118.40462005755 5077359.762972658 138.30205138354478 -44.4236225099234 +4202 20254 143.7095 -4.7350000000000065 800589.7497252915 9476039.452784304 +20254 4326 800589.7497252915 9476039.452784304 143.71056837026893 -4.733558620882463 +4202 20255 147.005 -24.25 500507.569948895 7318085.249553755 +20255 4326 500507.569948895 7318085.249553755 147.0061209326938 -24.248449092832416 +4202 20255 144.3005 -44.905 286885.6684480648 5024040.0369029995 +20255 4326 286885.6684480648 5024040.0369029995 144.30196924012398 -44.9035501037144 +4202 20255 149.7095 -3.594999999999999 801023.3539836409 9602191.329718165 +20255 4326 801023.3539836409 9602191.329718165 149.71049971365622 -3.5935624091827014 +4202 20256 153.0 -24.38 500000.0 7303691.80532887 +20256 4326 500000.0 7303691.80532887 153.001036352469 -24.378409746939393 +4202 20256 150.3 -44.233999999999995 284379.4589440065 5098574.282459913 +20256 4326 284379.4589440065 5098574.282459913 150.30134458284888 -44.23247475975858 +4202 20256 155.7 -4.526000000000003 799623.1023499797 9499171.448012354 +20256 4326 799623.1023499797 9499171.448012354 155.70092306558394 -4.524547055826549 +4202 20257 159.005 -24.605 500506.15147209744 7278779.466011253 +20257 4326 500506.15147209744 7278779.466011253 159.0059413334903 -24.603374547539637 +4202 20257 156.3005 -34.0775 250898.18178189805 6225948.327166893 +20257 4326 250898.18178189805 6225948.327166893 156.30157092714265 -34.07587832082249 +4202 20257 161.7095 -15.1325 791206.7809561745 8325214.605097736 +20257 4326 791206.7809561745 8325214.605097736 161.71035319232965 -15.130924674895777 +4202 20258 162.6 -30.735 270217.1467322924 6597293.890688509 +20258 4326 270217.1467322924 6597293.890688509 162.60092080251357 -30.733336069734204 +4202 20258 162.06 -33.8715 228042.6924176637 6248187.899221126 +20258 4326 228042.6924176637 6248187.899221126 162.06095914818906 -33.86983615815531 +4202 20258 163.14 -27.5985 316427.55789213046 6945881.266145376 +20258 4326 316427.55789213046 6945881.266145376 163.14088749518694 -27.596841356763996 +4203 20349 111.61500000000001 -27.515 560737.8133182325 6956360.545064095 +20349 4326 560737.8133182325 6956360.545064095 111.61642085721687 -27.513780224556733 +4203 20349 109.4685 -36.807500000000005 363385.2776156397 5925373.721950972 +20349 4326 363385.2776156397 5925373.721950972 109.47007970260165 -36.80642416266941 +4203 20349 113.7615 -18.222500000000004 792063.4413046406 7982988.531478607 +20349 4326 792063.4413046406 7982988.531478607 113.76281829929407 -18.22117808879656 +4203 20350 117.0 -25.57 500000.0 7171924.7511040885 +20350 4326 500000.0 7171924.7511040885 117.00138855074731 -25.568705348187354 +4203 20350 114.3 -37.234 260477.97629321108 5875738.186101207 +20350 4326 260477.97629321108 5875738.186101207 114.30158478392791 -37.23286419771174 +4203 20350 119.7 -13.906000000000002 791790.4571975596 8461011.087723939 +20350 4326 791790.4571975596 8461011.087723939 119.70127594996566 -13.90461569565028 +4203 20351 123.005 -24.265 500507.51040629257 7316424.4790419955 +20351 4326 500507.51040629257 7316424.4790419955 123.00635226532269 -24.263638041780965 +4203 20351 120.3005 -36.6895 258809.35335156615 5936161.155917596 +20351 4326 258809.35335156615 5936161.155917596 120.30205373025336 -36.68827720132833 +4203 20351 125.7095 -11.840500000000002 795228.5090123317 8689645.818530632 +20351 4326 795228.5090123317 8689645.818530632 125.71074133350477 -11.839079065626194 +4203 20352 129.0 -23.209999999999997 500000.0000000026 7433224.117533227 +20352 4326 500000.0000000026 7433224.117533227 129.00130666315343 -23.208579691835304 +4203 20352 126.291 -35.666 254788.5218153675 6049700.871023184 +20352 4326 254788.5218153675 6049700.871023184 126.29249848215989 -35.664690075917164 +4203 20352 131.709 -10.753999999999998 796288.7290539298 8809908.891173724 +20352 4326 796288.7290539298 8809908.891173724 131.7102011899907 -10.752552319879511 +4203 20353 135.0 -31.064999999999998 500000.0 6563182.486805453 +20353 4326 500000.0 6563182.486805453 135.00134881056954 -31.063558043222567 +4203 20353 132.3 -35.6325 255501.21603504498 6053440.083017627 +20353 4326 255501.21603504498 6053440.083017627 132.30144717407262 -35.631117038209865 +4203 20353 137.7 -26.4975 769122.7542084513 7066378.641902383 +20353 4326 769122.7542084513 7066378.641902383 137.70126539664545 -26.49601513525203 +4203 20354 141.005 -23.995 500508.57685906615 7346317.84862963 +20354 4326 500508.57685906615 7346317.84862963 141.00620496063115 -23.993484631117425 +4203 20354 138.3005 -36.7165 258893.78171587182 5933165.140145859 +20354 4326 258893.78171587182 5933165.140145859 138.3019000329089 -36.715055172769034 +4203 20354 143.7095 -11.273500000000002 795823.5653314051 8752406.805956962 +20354 4326 795823.5653314051 8752406.805956962 143.71059703765502 -11.272003100800575 +4203 20355 147.0 -21.51 500000.0000000026 7621399.347293811 +20355 4326 500000.0000000026 7621399.347293811 147.00111248546693 -21.508445651504537 +4203 20355 144.3 -28.26 235113.67475685984 6871028.25803055 +20355 4326 235113.67475685984 6871028.25803055 144.30120672267378 -28.258462354798255 +4203 20355 149.7 -14.76 790686.7010098665 8366467.264010318 +20355 4326 790686.7010098665 8366467.264010318 149.70103972854048 -14.758459746287066 +4203 20356 151.805 -25.595 379994.1743267884 7168615.61600166 +20356 4326 379994.1743267884 7168615.61600166 151.80607634306378 -25.593406532605762 +4203 20356 150.1805 -28.8305 224873.25222024892 6807516.2058646185 +20356 4326 224873.25222024892 6807516.2058646185 150.18162981952568 -28.828915617063757 +4203 20356 153.42950000000002 -22.3595 544221.1999388674 7527308.9607045725 +20356 4326 544221.1999388674 7527308.9607045725 153.43052846512455 -22.357905158645597 +4204 20436 35.254999999999995 28.1 721553.734152696 3110378.0816513053 +20436 4326 721553.734152696 3110378.0816513053 35.25387887646198 28.10044715675168 +4204 20436 34.5845 26.948 657297.257641991 2981702.962690568 +20436 4326 657297.257641991 2981702.962690568 34.58336076264156 26.948420795359876 +4204 20436 35.9255 29.252 784326.7834173032 3239501.0970135154 +20436 4326 784326.7834173032 3239501.0970135154 35.92439707880239 29.25247441477707 +4204 20437 39.0 24.375 500000.00000000064 2695781.24572733 +20437 4326 500000.00000000064 2695781.24572733 38.999079255552516 24.37540620991865 +4204 20437 36.3 17.3685 213071.30773184088 1922363.6308813896 +20437 4326 213071.30773184088 1922363.6308813896 36.29900690859183 17.368778945335762 +4204 20437 41.7 31.381499999999996 756781.4102916422 3475086.6413251464 +20437 4326 756781.4102916422 3475086.6413251464 41.69914762993614 31.382061111565573 +4204 20438 45.004999999999995 23.759999999999998 500509.5152746133 2627690.3708225954 +20438 4326 500509.5152746133 2627690.3708225954 45.00435512960039 23.760425737692206 +4204 20438 42.3005 17.109 212722.95677208382 1893628.1393473484 +20438 4326 212722.95677208382 1893628.1393473484 42.299764169746304 17.10930084428456 +4204 20438 47.7095 30.410999999999998 760300.2865775524 3367495.1734619583 +20438 4326 760300.2865775524 3367495.1734619583 47.70894821286627 30.411572554116603 +4204 20439 51.0 23.990000000000002 500000.0000000019 2653154.6025681007 +20439 4326 500000.0000000019 2653154.6025681007 50.99963258581095 23.990449873309576 +4204 20439 48.291000000000004 18.545 214014.79760831216 2052660.6174040784 +20439 4326 214014.79760831216 2052660.6174040784 48.29052386470385 18.545343109946664 +4204 20439 53.708999999999996 29.435 762806.3317561403 3259283.0768093094 +20439 4326 762806.3317561403 3259283.0768093094 53.708748275575175 29.435569253308813 +4204 20440 54.835 21.215 275268.9068503935 2347509.473278258 +20440 4326 275268.9068503935 2347509.473278258 54.834816816616645 21.215403279225796 +4204 20440 54.0835 19.8155 194450.13888036553 2193730.008458294 +20440 4326 194450.13888036553 2193730.008458294 54.08328402633433 19.81587655561814 +4204 20440 55.5865 22.6145 354717.80298979883 2501567.7925489745 +20440 4326 354717.80298979883 2501567.7925489745 55.586350152924446 22.614930681539462 +4204 20499 50.620000000000005 25.935000000000002 461947.80840872606 2868579.89429734 +20499 4326 461947.80840872606 2868579.89429734 50.61960867227196 25.93548771947532 +4204 20499 50.413000000000004 25.570500000000003 441039.31264393515 2828289.612820871 +20499 4326 441039.31264393515 2828289.612820871 50.4126000461889 25.570979771680832 +4204 20499 50.827 26.2995 482729.9574897829 2908903.7561384174 +20499 4326 482729.9574897829 2908903.7561384174 50.82661734087017 26.299995724983496 +4205 20538 45.0 5.76 500000.0 636685.9520080157 +20538 4326 500000.0 636685.9520080157 44.99923390732294 5.760541937074552 +4205 20538 42.3 0.576 199455.61050066032 63737.69663198326 +20538 4326 199455.61050066032 63737.69663198326 42.29917691493051 0.5764203024182734 +4205 20538 47.7 10.943999999999999 795120.952184613 1211129.998353963 +20538 4326 795120.952184613 1211129.998353963 47.699287359126124 10.944665095853955 +4205 20539 49.735 8.235 360665.73442300083 910514.2366868578 +20539 4326 360665.73442300083 910514.2366868578 49.734341622078674 8.235606675125794 +4205 20539 48.1735 4.819500000000001 186454.39295549353 533371.8650549449 +20539 4326 186454.39295549353 533371.8650549449 48.1728089307848 4.8200236524520195 +4205 20539 51.2965 11.6505 532318.3839639936 1287946.999681369 +20539 4326 532318.3839639936 1287946.999681369 51.29587302453914 11.651189361950756 +4803 20790 1.2569061111111104 39.555 222080.95695037852 287633.1426481317 +4803 20790 -0.25959388888889023 37.2105 88183.84888527673 28089.535895409295 +4803 20790 2.7734061111111092 41.899499999999996 347168.317443294 549484.0297689511 +4803 20791 1.2569061111111104 39.555 22080.95695037852 -12366.85735186827 +4803 20791 -0.25959388888889023 37.2105 -111816.15111472327 -271910.4641045907 +4803 20791 2.7734061111111092 41.899499999999996 147168.31744329398 249484.02976895115 +4208 20822 -50.685 -30.36 530272.1421184577 6641231.431637429 +20822 4326 530272.1421184577 6641231.431637429 -50.684236806845945 -30.36196762823085 +4208 20822 -53.1105 -35.175000000000004 307801.0987301836 6105448.45863655 +20822 4326 307801.0987301836 6105448.45863655 -53.10985264120879 -35.17707869359308 +4208 20822 -48.2595 -25.545 775388.7027045684 7171823.132175976 +20822 4326 775388.7027045684 7171823.132175976 -48.25862796958573 -25.546858736912828 +4208 20823 -45.0 -16.75 500000.0 8148078.539380192 +20823 4326 500000.0 8148078.539380192 -44.99900059626987 -16.75167289354023 +4208 20823 -47.708999999999996 -31.825 243581.839278461 6475710.672162289 +20823 4326 243581.839278461 6475710.672162289 -47.70804029835 -31.826976574862886 +4208 20823 -42.291000000000004 -1.6750000000000007 801440.6932997479 9814651.210343994 +20823 4326 801440.6932997479 9814651.210343994 -42.28990332181091 -1.6763628659079668 +4208 20824 -39.004999999999995 -13.17 499458.14289185224 8544047.584299346 +20824 4326 499458.14289185224 8544047.584299346 -39.00382486855153 -13.171547705157828 +4208 20824 -41.7095 -25.032 226578.6709582504 7228734.764380509 +20824 4326 226578.6709582504 7228734.764380509 -41.70837878520059 -25.033737887301964 +4208 20824 -36.3005 -1.3079999999999998 800432.6442489098 9855263.226831224 +20824 4326 800432.6442489098 9855263.226831224 -36.29922988520071 -1.309355926143018 +7683 20904 21.22 54.82 4514141.652029344 6077214.022152633 +20904 4326 4514141.652029344 6077214.022152633 21.22 54.819999523218115 +7683 20904 19.735 54.37 4417786.137135371 6027837.3364612125 +20904 4326 4417786.137135371 6027837.3364612125 19.734999999999996 54.3699995205928 +7683 20904 22.705000000000002 55.27 4608370.472850869 6128613.124097056 +20904 4326 4608370.472850869 6128613.124097056 22.705000000000005 55.26999952596092 +7683 20905 28.305 62.58 5567072.924719685 6942249.539370948 +20905 4326 5567072.924719685 6942249.539370948 28.304999999999996 62.579999586292146 +7683 20905 26.7795 56.379 5486377.346319851 6250784.138048785 +20905 4326 5486377.346319851 6250784.138048785 26.779499999999995 56.378999533217346 +7683 20905 29.8305 68.781 5614340.456738648 7635629.003784563 +20905 4326 5614340.456738648 7635629.003784563 29.8305 68.78099965862144 +7683 20906 33.0 60.18 6500000.0 6674126.715014836 +20906 4326 6500000.0 6674126.715014836 33.0 60.179999563308755 +7683 20906 30.3 51.324000000000005 6311804.56633536 5691593.585084104 +20906 4326 6311804.56633536 5691593.585084104 30.300000000000004 51.3239995059573 +7683 20906 35.7 69.036 6607821.226727251 7663813.086686154 +20906 4326 6607821.226727251 7663813.086686154 35.70000000000002 69.03599966196155 +7683 20907 39.004999999999995 56.205 7500310.310488491 6231387.922952081 +20907 4326 7500310.310488491 6231387.922952081 39.005 56.20499953203235 +7683 20907 36.3005 44.4825 7285244.207666728 4930982.066147137 +20907 4326 7285244.207666728 4930982.066147137 36.3005 44.48249949355794 +7683 20907 41.7095 67.92750000000001 7613639.486860361 7540286.343567541 +20907 4326 7613639.486860361 7540286.343567541 41.709500000000006 67.92749964763945 +7683 20908 45.0 61.05 8500000.0 6771064.425100864 +20908 4326 8500000.0 6771064.425100864 44.99999999999999 61.04999957129522 +7683 20908 42.3 43.175999999999995 8280463.529141829 4785812.809532288 +20908 4326 8280463.529141829 4785812.809532288 42.3 43.17599949446416 +7683 20908 47.7 78.92399999999999 8557908.541213458 8766336.354932534 +20908 4326 8557908.541213458 8766336.354932534 47.699999999999996 78.92399980930054 +7683 20909 51.0 61.395 9500000.0 6809508.739345584 +20909 4326 9500000.0 6809508.739345584 51.00000000000001 61.394999574571905 +7683 20909 48.3 43.3905 9281234.082512995 4809645.004210778 +20909 4326 9281234.082512995 4809645.004210778 48.299999999999976 43.39049949424316 +7683 20909 53.7 79.3995 9555452.015316427 8819373.081529161 +20909 4326 9555452.015316427 8819373.081529161 53.699999999999996 79.3994998171031 +7683 20910 57.0 66.19 10500000.0 7344025.215498133 +20910 4326 10500000.0 7344025.215498133 57.00000000000001 66.18999962625607 +7683 20910 54.3 52.042 10314759.599605555 5771458.737838573 +20910 4326 10314759.599605555 5771458.737838573 54.3 52.04199950891027 +7683 20910 59.7 80.338 10550592.32794638 8924055.9320591 +20910 4326 10550592.32794638 8924055.9320591 59.70000000000002 80.33799983264821 +7683 20911 63.0 66.215 11500000.0 7346812.9750018455 +20911 4326 11500000.0 7346812.9750018455 63.0 66.2149996265542 +7683 20911 60.3 52.2155 11315478.079344034 5790758.775172247 +20911 4326 11315478.079344034 5790758.775172247 60.300000000000004 52.215499509670146 +7683 20911 65.7 80.21449999999999 11551232.644506881 8910280.140403979 +20911 4326 11551232.644506881 8910280.140403979 65.69999999999996 80.21449983059199 +7683 20912 69.0 65.585 12500000.0 7276564.219038882 +20912 4326 12500000.0 7276564.219038882 69.0 65.584999619128 +7683 20912 66.3 55.2485 12328307.604582906 6128219.083412878 +20912 4326 12328307.604582906 6128219.083412878 66.3 55.24849952582722 +7683 20912 71.7 75.9215 12573319.57119551 8431473.318314942 +20912 4326 12573319.57119551 8431473.318314942 71.70000000000003 75.92149976132619 +7683 20913 75.0 66.44 13500000.0 7371903.217658515 +20913 4326 13500000.0 7371903.217658515 75.0 66.43999962925024 +7683 20913 72.3 54.497 13325082.143178653 6044592.587407976 +20913 4326 13325082.143178653 6044592.587407976 72.29999999999998 54.49699952132177 +7683 20913 77.7 78.383 13560698.52031975 8705995.427969886 +20913 4326 13560698.52031975 8705995.427969886 77.70000000000002 78.38299980048693 +7683 20914 81.0 65.86 14500000.0 7307227.643149274 +20914 4326 14500000.0 7307227.643149274 81.0 65.85999962234723 +7683 20914 78.3 52.207 14315442.840270888 5789813.229836512 +20914 4326 14315442.840270888 5789813.229836512 78.3 52.20699950963249 +7683 20914 83.7 79.513 14554865.072588291 8832032.929437224 +20914 4326 14554865.072588291 8832032.929437224 83.69999999999999 79.51299981897303 +7683 20915 87.0 65.17 15500000.0 7230292.445316968 +20915 4326 15500000.0 7230292.445316968 87.0 65.16999961433623 +7683 20915 84.3 50.68 15309179.40742904 5619966.184346194 +20915 4326 15309179.40742904 5619966.184346194 84.29999999999998 50.67999950357233 +7683 20915 89.7 79.66 15554104.566778902 8848429.489563156 +20915 4326 15554104.566778902 8848429.489563156 89.70000000000003 79.65999982139907 +7683 20916 93.0 65.62 16500000.0 7280466.77472064 +20916 4326 16500000.0 7280466.77472064 93.0 65.61999961953579 +7683 20916 90.3 51.463 16312374.33108561 5707054.347260522 +20916 4326 16312374.33108561 5707054.347260522 90.3 51.46299950650483 +7683 20916 95.7 79.77699999999999 16553499.007612878 8861479.896752339 +20916 4326 16553499.007612878 8861479.896752339 95.69999999999997 79.77699982333336 +7683 20917 99.0 65.52499999999999 17500000.0 7269874.165752962 +20917 4326 17500000.0 7269874.165752962 99.0 65.52499961843026 +7683 20917 96.3 51.3095 17311745.194663778 5689980.788318597 +20917 4326 17311745.194663778 5689980.788318597 96.30000000000001 51.30949950590084 +7683 20917 101.7 79.7405 17553687.945793606 8857408.607959082 +20917 4326 17553687.945793606 8857408.607959082 101.70000000000007 79.7404998227296 +7683 20918 105.0 64.56 18500000.0 7162283.203758931 +20918 4326 18500000.0 7162283.203758931 105.00000000000001 64.55999960743962 +7683 20918 102.3 51.132 18311019.389146436 5670238.185056957 +20918 4326 18311019.389146436 5670238.185056957 102.30000000000001 51.13199950522009 +7683 20918 107.7 77.988 18562732.11138221 8661939.85890686 +20918 4326 18562732.11138221 8661939.85890686 107.70000000000005 77.98799979409638 +7683 20919 111.0 62.975 19500000.0 6985597.417676789 +20919 4326 19500000.0 6985597.417676789 111.0 62.974999590356774 +7683 20919 108.3 50.5235 19308545.11620665 5602560.771513605 +20919 4326 19308545.11620665 5602560.771513605 108.30000000000001 50.523499503030536 +7683 20919 113.7 75.4265 19575841.801789045 8376272.845837615 +20919 4326 19575841.801789045 8376272.845837615 113.69999999999997 75.42649975365379 +7683 20920 117.0 62.735 20500000.0 6958847.175778904 +20920 4326 20500000.0 6958847.175778904 117.0 62.73499958787784 +7683 20920 114.3 50.832499999999996 20309798.866274387 5636927.082611161 +20920 4326 20309798.866274387 5636927.082611161 114.29999999999998 50.83249950411447 +7683 20920 119.7 74.63749999999999 20579850.135515243 8288290.447160623 +20920 4326 20579850.135515243 8288290.447160623 119.7 74.63749974157676 +7683 20921 123.0 62.754999999999995 21500000.0 6961076.327493603 +20921 4326 21500000.0 6961076.327493603 123.00000000000001 62.754999588083315 +7683 20921 120.3 52.634499999999996 21317220.21644139 5837369.919756268 +20921 4326 21317220.21644139 5837369.919756268 120.30000000000003 52.6344995115792 +7683 20921 125.7 72.8755 21588744.991236504 8091825.523536156 +20921 4326 21588744.991236504 8091825.523536156 125.69999999999999 72.87549971532427 +7683 20922 129.0 57.93 22500000.0 6423485.7077745665 +20922 4326 22500000.0 6423485.7077745665 129.0 57.9299995445341 +7683 20922 126.3 43.818 22282778.977195468 4857144.819224213 +20922 4326 22282778.977195468 4857144.819224213 126.30000000000003 43.81799949388727 +7683 20922 131.7 72.042 22592923.502593737 7998898.5514845485 +20922 4326 22592923.502593737 7998898.5514845485 131.7 72.04199970327342 +7683 20923 135.0 59.39 23500000.0 6586113.914286063 +20923 4326 23500000.0 6586113.914286063 135.0 59.3899995564045 +7683 20923 132.3 44.306000000000004 23284557.35254951 4911370.225742593 +20923 4326 23284557.35254951 4911370.225742593 132.3 44.305999493618835 +7683 20923 137.7 74.474 23580678.864126854 8270058.956087509 +20923 4326 23580678.864126854 8270058.956087509 137.69999999999996 74.47399973909823 +7683 20924 141.0 61.055 24500000.0 6771621.574976855 +20924 4326 24500000.0 6771621.574976855 141.0 61.054999571342286 +7683 20924 138.3 47.36150000000001 24296044.121458717 5250972.52476633 +20924 4326 24296044.121458717 5250972.52476633 138.30000000000004 47.361499495277656 +7683 20924 143.7 74.74849999999999 24579287.13614586 8300667.90927861 +20924 4326 24579287.13614586 8300667.90927861 143.70000000000005 74.74849974326422 +7683 20925 147.0 60.209999999999994 25500000.0 6677469.182612738 +20925 4326 25500000.0 6677469.182612738 147.0 60.2099995635775 +7683 20925 144.3 45.261 25288082.899355687 5017498.0504696425 +20925 4326 25288082.899355687 5017498.0504696425 144.30000000000004 45.26099949351871 +7683 20925 149.7 75.15899999999999 25577202.447420426 8346443.046112411 +20925 4326 25577202.447420426 8346443.046112411 149.7 75.15899974953791 +7683 20926 153.0 61.015 26500000.0 6767164.387611585 +20926 4326 26500000.0 6767164.387611585 153.0 61.014999570966275 +7683 20926 150.3 47.294500000000006 26295785.866512015 5243524.314924087 +20926 4326 26295785.866512015 5243524.314924087 150.3 47.294499495179615 +7683 20926 155.7 74.7355 26579353.088655137 8299218.291624772 +20926 4326 26579353.088655137 8299218.291624772 155.70000000000002 74.7354997430664 +7683 20927 159.0 63.735 27500000.0 7070312.531824955 +20927 4326 27500000.0 7070312.531824955 159.0 63.73499959839507 +7683 20927 156.3 51.6165 27313004.824347585 5724128.25645185 +20927 4326 27313004.824347585 5724128.25645185 156.3 51.61649950712297 +7683 20927 161.7 75.8535 27573666.392678928 8423890.116683926 +20927 4326 27573666.392678928 8423890.116683926 161.70000000000002 75.85349976026794 +7683 20928 165.0 62.25 28500000.0 6904792.212720458 +20928 4326 28500000.0 6904792.212720458 165.0 62.24999958295649 +7683 20928 162.3 55.248 28328305.448599353 6128163.44100117 +20928 4326 28328305.448599353 6128163.44100117 162.29999999999998 55.24799952582411 +7683 20928 167.7 69.252 28606760.267148446 7687888.129319012 +20928 4326 28606760.267148446 7687888.129319012 167.7 69.25199966481169 +7683 20929 171.0 62.32 29500000.0 6912593.725959956 +20929 4326 29500000.0 6912593.725959956 171.0 62.31999958365945 +7683 20929 168.3 55.237 29328258.020298928 6126939.30884831 +20929 4326 29328258.020298928 6126939.30884831 168.30000000000004 55.236999525755806 +7683 20929 173.7 69.40299999999999 29606017.663989313 7704718.649092706 +20929 4326 29606017.663989313 7704718.649092706 173.70000000000005 69.40299966681545 +7683 20930 177.0 66.62 30500000.0 7391975.936999662 +20930 4326 30500000.0 7391975.936999662 177.0 66.61999963142345 +7683 20930 174.3 62.147 30359236.21785475 6896246.19811984 +20930 4326 30359236.21785475 6896246.19811984 174.30000000000004 62.14699958192663 +7683 20930 179.7 71.093 30597656.719802387 7893102.099115551 +20930 4326 30597656.719802387 7893102.099115551 179.69999999999996 71.09299968985776 +7683 20931 -177.0 68.0 31500000.0 7545881.994064116 +20931 4326 31500000.0 7545881.994064116 -177.0 67.99999964856033 +7683 20931 -179.7 64.71499999999999 31371300.85314118 7182306.28153567 +20931 4326 31371300.85314118 7182306.28153567 -179.7 64.71499960917534 +7683 20931 -174.3 71.28500000000001 31596701.252470255 7914505.978584184 +20931 4326 31596701.252470255 7914505.978584184 -174.3 71.2849996925451 +7683 20932 -171.485 65.69 32477711.902657837 7288357.909380592 +20932 4326 32477711.902657837 7288357.909380592 -171.48499999999999 65.68999962035305 +7683 20932 -173.7485 64.349 32367222.9175116 7141631.593640693 +20932 4326 32367222.9175116 7141631.593640693 -173.74850000000004 64.34899960509523 +7683 20932 -169.2215 67.031 32577470.505058475 7438917.537001014 +20932 4326 32577470.505058475 7438917.537001014 -169.22149999999996 67.03099963643992 +4209 20934 21.994999999999997 -18.869999999999997 604811.3361083484 7913442.698493986 +20934 4326 604811.3361083484 7913442.698493986 21.994716319170585 -18.871071801821564 +4209 20934 20.1905 -26.079 419037.4477990113 7115550.715023275 +20934 4326 419037.4477990113 7115550.715023275 20.190149036203366 -26.07955331077982 +4209 20934 23.7995 -11.661 805244.8806934218 8709539.760364734 +20934 4326 805244.8806934218 8709539.760364734 23.79927401194434 -11.662661406087759 +4209 20935 27.0 -17.075 500000.0 8112313.2347756345 +20935 4326 500000.0 8112313.2347756345 26.99985654320704 -17.076218096077763 +4209 20935 24.3 -24.9635 227390.4915207465 7236608.789278334 +20935 4326 227390.4915207465 7236608.789278334 24.299770451538173 -24.964134736370024 +4209 20935 29.7 -9.186499999999999 796724.7855337631 8983504.27536706 +20935 4326 796724.7855337631 8983504.27536706 29.699933337283223 -9.188374926570988 +4209 20936 32.965 -15.305 496242.5445948959 8308088.503149913 +20936 4326 496242.5445948959 8308088.503149913 32.965021414475935 -15.306362177969824 +4209 20936 30.2965 -21.7085 220292.81662515755 7597198.5025440175 +20936 4326 220292.81662515755 7597198.5025440175 30.2964461835835 -21.709366955720412 +4209 20936 35.6335 -8.9015 789639.6095269357 9015096.637182595 +20936 4326 789639.6095269357 9015096.637182595 35.63359240327541 -8.90339912810197 +7683 21004 21.22 54.82 514141.65202934394 6077214.022152633 +21004 4326 514141.65202934394 6077214.022152633 21.22 54.819999523218115 +7683 21004 19.735 54.37 417786.1371353705 6027837.3364612125 +21004 4326 417786.1371353705 6027837.3364612125 19.734999999999996 54.369999520592785 +7683 21004 22.705000000000002 55.27 608370.4728508693 6128613.124097056 +21004 4326 608370.4728508693 6128613.124097056 22.705000000000002 55.26999952596092 +7683 21005 28.305 62.58 567072.924719685 6942249.539370948 +21005 4326 567072.924719685 6942249.539370948 28.305 62.579999586292146 +7683 21005 26.7795 56.379 486377.34631985135 6250784.138048785 +21005 4326 486377.34631985135 6250784.138048785 26.779499999999995 56.378999533217346 +7683 21005 29.8305 68.781 614340.4567386479 7635629.003784563 +21005 4326 614340.4567386479 7635629.003784563 29.8305 68.78099965862144 +7683 21006 33.0 60.18 500000.0 6674126.715014836 +21006 4326 500000.0 6674126.715014836 33.0 60.179999563308755 +7683 21006 30.3 51.324000000000005 311804.5663353596 5691593.585084104 +21006 4326 311804.5663353596 5691593.585084104 30.300000000000004 51.3239995059573 +7683 21006 35.7 69.036 607821.2267272506 7663813.086686154 +21006 4326 607821.2267272506 7663813.086686154 35.7 69.03599966196155 +7683 21007 39.004999999999995 56.205 500310.3104884912 6231387.922952081 +21007 4326 500310.3104884912 6231387.922952081 39.005 56.20499953203235 +7683 21007 36.3005 44.4825 285244.2076667275 4930982.066147137 +21007 4326 285244.2076667275 4930982.066147137 36.3005 44.48249949355794 +7683 21007 41.7095 67.92750000000001 613639.486860361 7540286.343567541 +21007 4326 613639.486860361 7540286.343567541 41.709500000000006 67.92749964763945 +7683 21008 45.0 61.05 500000.0 6771064.425100864 +21008 4326 500000.0 6771064.425100864 44.99999999999999 61.04999957129522 +7683 21008 42.3 43.175999999999995 280463.5291418296 4785812.809532288 +21008 4326 280463.5291418296 4785812.809532288 42.3 43.17599949446416 +7683 21008 47.7 78.92399999999999 557908.5412134586 8766336.354932534 +21008 4326 557908.5412134586 8766336.354932534 47.699999999999996 78.92399980930054 +7683 21009 51.0 61.395 500000.0 6809508.739345584 +21009 4326 500000.0 6809508.739345584 51.00000000000001 61.394999574571905 +7683 21009 48.3 43.3905 281234.08251299604 4809645.004210778 +21009 4326 281234.08251299604 4809645.004210778 48.300000000000004 43.39049949424317 +7683 21009 53.7 79.3995 555452.015316427 8819373.081529161 +21009 4326 555452.015316427 8819373.081529161 53.7 79.3994998171031 +7683 21010 57.0 66.19 500000.0 7344025.215498133 +21010 4326 500000.0 7344025.215498133 57.00000000000001 66.18999962625607 +7683 21010 54.3 52.042 314759.5996055555 5771458.737838573 +21010 4326 314759.5996055555 5771458.737838573 54.3 52.04199950891027 +7683 21010 59.7 80.338 550592.3279463797 8924055.9320591 +21010 4326 550592.3279463797 8924055.9320591 59.7 80.33799983264821 +7683 21011 63.0 66.215 500000.0 7346812.9750018455 +21011 4326 500000.0 7346812.9750018455 63.0 66.2149996265542 +7683 21011 60.3 52.2155 315478.0793440342 5790758.775172247 +21011 4326 315478.0793440342 5790758.775172247 60.300000000000004 52.215499509670146 +7683 21011 65.7 80.21449999999999 551232.6445068818 8910280.140403979 +21011 4326 551232.6445068818 8910280.140403979 65.7 80.21449983059199 +7683 21012 69.0 65.585 500000.0 7276564.219038882 +21012 4326 500000.0 7276564.219038882 69.0 65.584999619128 +7683 21012 66.3 55.2485 328307.60458290536 6128219.083412878 +21012 4326 328307.60458290536 6128219.083412878 66.3 55.24849952582722 +7683 21012 71.7 75.9215 573319.5711955087 8431473.318314942 +21012 4326 573319.5711955087 8431473.318314942 71.7 75.92149976132619 +7683 21013 75.0 66.44 500000.0 7371903.217658515 +21013 4326 500000.0 7371903.217658515 75.0 66.43999962925024 +7683 21013 72.3 54.497 325082.14317865385 6044592.587407976 +21013 4326 325082.14317865385 6044592.587407976 72.3 54.496999521321776 +7683 21013 77.7 78.383 560698.5203197503 8705995.427969886 +21013 4326 560698.5203197503 8705995.427969886 77.7 78.38299980048693 +7683 21014 81.0 65.86 500000.0 7307227.643149274 +21014 4326 500000.0 7307227.643149274 81.0 65.85999962234723 +7683 21014 78.3 52.207 315442.84027088794 5789813.229836512 +21014 4326 315442.84027088794 5789813.229836512 78.3 52.20699950963249 +7683 21014 83.7 79.513 554865.0725882915 8832032.929437224 +21014 4326 554865.0725882915 8832032.929437224 83.7 79.51299981897303 +7683 21015 87.0 65.17 500000.0 7230292.445316968 +21015 4326 500000.0 7230292.445316968 87.0 65.16999961433623 +7683 21015 84.3 50.68 309179.4074290402 5619966.184346194 +21015 4326 309179.4074290402 5619966.184346194 84.3 50.67999950357233 +7683 21015 89.7 79.66 554104.5667789015 8848429.489563156 +21015 4326 554104.5667789015 8848429.489563156 89.7 79.65999982139907 +7683 21016 93.0 65.62 500000.0 7280466.77472064 +21016 4326 500000.0 7280466.77472064 93.0 65.61999961953579 +7683 21016 90.3 51.463 312374.3310856089 5707054.347260522 +21016 4326 312374.3310856089 5707054.347260522 90.3 51.46299950650483 +7683 21016 95.7 79.77699999999999 553499.0076128789 8861479.896752339 +21016 4326 553499.0076128789 8861479.896752339 95.7 79.77699982333336 +7683 21017 99.0 65.52499999999999 500000.0 7269874.165752962 +21017 4326 500000.0 7269874.165752962 99.0 65.52499961843026 +7683 21017 96.3 51.3095 311745.1946637769 5689980.788318597 +21017 4326 311745.1946637769 5689980.788318597 96.3 51.30949950590084 +7683 21017 101.7 79.7405 553687.9457936051 8857408.607959082 +21017 4326 553687.9457936051 8857408.607959082 101.7 79.7404998227296 +7683 21018 105.0 64.56 500000.0 7162283.203758931 +21018 4326 500000.0 7162283.203758931 105.00000000000001 64.55999960743962 +7683 21018 102.3 51.132 311019.3891464351 5670238.185056957 +21018 4326 311019.3891464351 5670238.185056957 102.3 51.13199950522009 +7683 21018 107.7 77.988 562732.1113822078 8661939.85890686 +21018 4326 562732.1113822078 8661939.85890686 107.7 77.9879997940964 +7683 21019 111.0 62.975 500000.0 6985597.417676789 +21019 4326 500000.0 6985597.417676789 111.0 62.974999590356774 +7683 21019 108.3 50.5235 308545.11620664876 5602560.771513605 +21019 4326 308545.11620664876 5602560.771513605 108.3 50.52349950303054 +7683 21019 113.7 75.4265 575841.801789046 8376272.845837615 +21019 4326 575841.801789046 8376272.845837615 113.7 75.42649975365379 +7683 21020 117.0 62.735 500000.0 6958847.175778904 +21020 4326 500000.0 6958847.175778904 117.0 62.73499958787784 +7683 21020 114.3 50.832499999999996 309798.8662743872 5636927.082611161 +21020 4326 309798.8662743872 5636927.082611161 114.3 50.83249950411447 +7683 21020 119.7 74.63749999999999 579850.1355152428 8288290.447160623 +21020 4326 579850.1355152428 8288290.447160623 119.7 74.63749974157676 +7683 21021 123.0 62.754999999999995 500000.0 6961076.327493603 +21021 4326 500000.0 6961076.327493603 123.00000000000001 62.754999588083315 +7683 21021 120.3 52.634499999999996 317220.2164413879 5837369.919756268 +21021 4326 317220.2164413879 5837369.919756268 120.3 52.6344995115792 +7683 21021 125.7 72.8755 588744.9912365039 8091825.523536156 +21021 4326 588744.9912365039 8091825.523536156 125.69999999999999 72.87549971532427 +7683 21022 129.0 57.93 500000.0 6423485.7077745665 +21022 4326 500000.0 6423485.7077745665 129.0 57.9299995445341 +7683 21022 126.3 43.818 282778.97719546675 4857144.819224213 +21022 4326 282778.97719546675 4857144.819224213 126.30000000000001 43.81799949388727 +7683 21022 131.7 72.042 592923.5025937378 7998898.5514845485 +21022 4326 592923.5025937378 7998898.5514845485 131.7 72.04199970327342 +7683 21023 135.0 59.39 500000.0 6586113.914286063 +21023 4326 500000.0 6586113.914286063 135.0 59.3899995564045 +7683 21023 132.3 44.306000000000004 284557.352549509 4911370.225742593 +21023 4326 284557.352549509 4911370.225742593 132.3 44.305999493618835 +7683 21023 137.7 74.474 580678.8641268543 8270058.956087509 +21023 4326 580678.8641268543 8270058.956087509 137.7 74.47399973909823 +7683 21024 141.0 61.055 500000.0 6771621.574976855 +21024 4326 500000.0 6771621.574976855 141.0 61.054999571342286 +7683 21024 138.3 47.36150000000001 296044.1214587155 5250972.52476633 +21024 4326 296044.1214587155 5250972.52476633 138.3 47.361499495277656 +7683 21024 143.7 74.74849999999999 579287.1361458582 8300667.90927861 +21024 4326 579287.1361458582 8300667.90927861 143.7 74.74849974326422 +7683 21025 147.0 60.209999999999994 500000.0 6677469.182612738 +21025 4326 500000.0 6677469.182612738 147.0 60.2099995635775 +7683 21025 144.3 45.261 288082.8993556862 5017498.0504696425 +21025 4326 288082.8993556862 5017498.0504696425 144.3 45.26099949351871 +7683 21025 149.7 75.15899999999999 577202.4474204255 8346443.046112411 +21025 4326 577202.4474204255 8346443.046112411 149.7 75.15899974953791 +7683 21026 153.0 61.015 500000.0 6767164.387611585 +21026 4326 500000.0 6767164.387611585 153.0 61.014999570966275 +7683 21026 150.3 47.294500000000006 295785.8665120145 5243524.314924087 +21026 4326 295785.8665120145 5243524.314924087 150.3 47.294499495179615 +7683 21026 155.7 74.7355 579353.0886551356 8299218.291624772 +21026 4326 579353.0886551356 8299218.291624772 155.7 74.7354997430664 +7683 21027 159.0 63.735 500000.0 7070312.531824955 +21027 4326 500000.0 7070312.531824955 159.0 63.73499959839507 +7683 21027 156.3 51.6165 313004.82434758556 5724128.25645185 +21027 4326 313004.82434758556 5724128.25645185 156.3 51.61649950712297 +7683 21027 161.7 75.8535 573666.392678927 8423890.116683926 +21027 4326 573666.392678927 8423890.116683926 161.7 75.85349976026794 +7683 21028 165.0 62.25 500000.0 6904792.212720458 +21028 4326 500000.0 6904792.212720458 165.0 62.24999958295649 +7683 21028 162.3 55.248 328305.44859935436 6128163.44100117 +21028 4326 328305.44859935436 6128163.44100117 162.3 55.2479995258241 +7683 21028 167.7 69.252 606760.2671484461 7687888.129319012 +21028 4326 606760.2671484461 7687888.129319012 167.7 69.25199966481169 +7683 21029 171.0 62.32 500000.0 6912593.725959956 +21029 4326 500000.0 6912593.725959956 171.0 62.31999958365945 +7683 21029 168.3 55.237 328258.02029892674 6126939.30884831 +21029 4326 328258.02029892674 6126939.30884831 168.3 55.236999525755806 +7683 21029 173.7 69.40299999999999 606017.6639893112 7704718.649092706 +21029 4326 606017.6639893112 7704718.649092706 173.7 69.40299966681545 +7683 21030 177.0 66.62 500000.0 7391975.936999662 +21030 4326 500000.0 7391975.936999662 177.0 66.61999963142345 +7683 21030 174.3 62.147 359236.21785474865 6896246.19811984 +21030 4326 359236.21785474865 6896246.19811984 174.3 62.14699958192663 +7683 21030 179.7 71.093 597656.7198023883 7893102.099115551 +21030 4326 597656.7198023883 7893102.099115551 179.7 71.09299968985776 +7683 21031 -177.0 68.0 500000.0 7545881.994064116 +21031 4326 500000.0 7545881.994064116 -177.0 67.99999964856033 +7683 21031 -179.7 64.71499999999999 371300.85314118164 7182306.28153567 +21031 4326 371300.85314118164 7182306.28153567 -179.7 64.71499960917534 +7683 21031 -174.3 71.28500000000001 596701.252470255 7914505.978584184 +21031 4326 596701.252470255 7914505.978584184 -174.3 71.2849996925451 +7683 21032 -171.485 65.69 477711.9026578359 7288357.909380592 +21032 4326 477711.9026578359 7288357.909380592 -171.485 65.68999962035305 +7683 21032 -173.7485 64.349 367222.9175116017 7141631.593640693 +21032 4326 367222.9175116017 7141631.593640693 -173.7485 64.34899960509523 +7683 21032 -169.2215 67.031 577470.5050584739 7438917.537001014 +21032 4326 577470.5050584739 7438917.537001014 -169.2215 67.03099963643992 +4210 21035 29.67 -3.405 796695.8074232432 9623264.590650277 +21035 4326 796695.8074232432 9623264.590650277 29.670665809420857 -3.4074289280982875 +4210 21035 29.373 -6.4695 762463.5621309411 9284348.25436425 +21035 4326 762463.5621309411 9284348.25436425 29.373662209805737 -6.471653016486658 +4210 21035 29.967 -0.34050000000000047 830301.4857547245 9962317.129527083 +21035 4326 830301.4857547245 9962317.129527083 29.9676712567164 -0.34320131929862946 +4210 21036 32.995 -5.8 499446.4444656401 9358962.591788301 +21036 4326 499446.4444656401 9358962.591788301 32.9957412730982 -5.802208995565762 +4210 21036 30.290499999999998 -11.029 203923.993512069 8779584.075101288 +21036 4326 203923.993512069 8779584.075101288 30.29119109656294 -11.030740770839554 +4210 21036 35.6995 -0.5710000000000015 800489.1621000683 9936822.562028501 +21036 4326 800489.1621000683 9936822.562028501 35.700294987363144 -0.5736802926689483 +4210 21037 38.8 -5.875 477860.67744966334 9350668.775476964 +21037 4326 477860.67744966334 9350668.775476964 38.800863120135006 -5.877193823817271 +4210 21037 36.28 -11.1625 202910.91487029527 8764798.163312541 +21037 4326 202910.91487029527 8764798.163312541 36.28082250915654 -11.164213705458025 +4210 21037 41.32 -0.5875000000000004 758220.014136463 9935015.940369327 +21037 4326 758220.014136463 9935015.940369327 41.320908569441976 -0.5901779504705413 +4210 21095 29.855 0.43 817819.1544801211 47583.00611878021 +21095 4326 817819.1544801211 47583.00611878021 29.85566877575319 0.42723119693008643 +4210 21095 29.7245 0.043000000000000003 803288.9841487084 4757.7682225126755 +21095 4326 803288.9841487084 4757.7682225126755 29.725165855166857 0.04026502581742509 +4210 21095 29.985500000000002 0.817 832335.0907149289 90418.32137371782 +21095 4326 832335.0907149289 90418.32137371782 29.986171723470033 0.8141974825556479 +4210 21096 32.995 2.315 499444.0664069106 255854.8642662749 +21096 4326 499444.0664069106 255854.8642662749 32.995738102163756 2.3120669330003363 +4210 21096 30.290499999999998 0.2315 198384.4249859284 25614.177013425407 +21096 4326 198384.4249859284 25614.177013425407 30.291178422027222 0.22874851087572035 +4210 21096 35.6995 4.398499999999999 799623.6296870657 486672.91016040783 +21096 4326 799623.6296870657 486672.91016040783 35.700297280478026 4.395387658545354 +4210 21097 38.955 2.245 494996.3554209968 248118.42608856232 +21097 4326 494996.3554209968 248118.42608856232 38.95587340008999 2.2420950707977463 +4210 21097 36.2955 0.22450000000000003 198941.28527908708 24839.563004876098 +21097 4326 198941.28527908708 24839.563004876098 36.29632038733919 0.22177578322237612 +4210 21097 41.6145 4.2655 790233.4174564548 471924.0665624152 +21097 4326 790233.4174564548 471924.0665624152 41.61542575424647 4.262415992958163 +4211 21148 106.53 -5.93 669345.8764666626 9344362.6355716 +21148 4326 669345.8764666626 9344362.6355716 106.53151418211596 -5.929862765986379 +4211 21148 105.20700000000001 -7.604 522830.0980625799 9159555.40938556 +21148 4326 522830.0980625799 9159555.40938556 105.20867804569143 -7.603700738875718 +4211 21148 107.853 -4.256 816691.4474260588 9529035.050020335 +21148 4326 816691.4474260588 9529035.050020335 107.85435177953777 -4.256027033053199 +4211 21149 111.0 -6.47 500000.0000000042 9284908.798302535 +21149 4326 500000.0000000042 9284908.798302535 111.00097554997258 -6.469797985703101 +4211 21149 108.3 -8.45 202726.11993624066 9065013.30340903 +21149 4326 202726.11993624066 9065013.30340903 108.30130861880431 -8.449605075317583 +4211 21149 113.7 -4.49 799602.0104530496 9503204.565902997 +21149 4326 799602.0104530496 9503204.565902997 113.70064406074304 -4.48999559578045 +4211 21150 115.505 -7.12 334919.1522760935 9212794.086063864 +21150 4326 334919.1522760935 9212794.086063864 115.5054257251935 -7.1197252535770055 +4211 21150 114.1505 -8.731 186484.44284494087 9033796.094300633 +21150 4326 186484.44284494087 9033796.094300633 114.15109417095191 -8.730562139299373 +4211 21150 116.85950000000001 -5.509 484439.3908371055 9391127.762176124 +21150 4326 484439.3908371055 9391127.762176124 116.85975854723165 -5.508889313901429 +7683 21207 21.035 54.82 7252249.810084503 6077192.392794833 +21207 4326 7252249.810084503 6077192.392794833 21.035 54.81999952321813 +7683 21207 19.7165 54.37 7166583.86403572 6027859.072900273 +21207 4326 7166583.86403572 6027859.072900273 19.716500000000003 54.369999520592785 +7683 21207 22.3535 55.27 7336030.639371023 6128122.972513546 +21207 4326 7336030.639371023 6128122.972513546 22.3535 55.26999952596093 +7683 21208 22.685000000000002 54.705 8165233.9107763255 6065184.124216 +21208 4326 8165233.9107763255 6065184.124216 22.68499999999999 54.70499952253601 +7683 21208 22.5185 54.3765 8153731.683742058 6028834.928610979 +21208 4326 8153731.683742058 6028834.928610979 22.518500000000007 54.37649952062987 +7683 21208 22.8515 55.033500000000004 8176565.680332794 6101562.290421037 +21208 4326 8176565.680332794 6101562.290421037 22.851499999999998 55.03349952450484 +7683 21209 27.560000000000002 62.49 9278870.410760084 6931665.723556881 +21209 4326 9278870.410760084 6931665.723556881 27.560000000000002 62.489999585376985 +7683 21209 26.705 56.693999999999996 9231925.354330005 6285876.93111335 +21209 4326 9231925.354330005 6285876.93111335 26.704999999999995 56.693999535406256 +7683 21209 28.415000000000003 68.286 9308441.967750723 7578452.25247484 +21209 4326 9308441.967750723 7578452.25247484 28.414999999999996 68.28599965221485 +7683 21210 30.0 61.349999999999994 10250000.0 6804494.151856439 +21210 4326 10250000.0 6804494.151856439 29.999999999999996 61.34999957414101 +7683 21210 28.65 53.7 10160839.893164853 5953371.439720425 +21210 4326 10160839.893164853 5953371.439720425 28.650000000000006 53.69999951690294 +7683 21210 31.35 69.0 10304010.094377024 7658018.510035111 +21210 4326 10304010.094377024 7658018.510035111 31.35000000000001 68.99999966148839 +7683 21211 33.0 60.629999999999995 11250000.0 6724265.32405217 +21211 4326 11250000.0 6724265.32405217 32.99999999999999 60.629999567389994 +7683 21211 31.65 52.179 11157657.050417932 5784119.615608411 +21211 4326 11157657.050417932 5784119.615608411 31.64999999999999 52.178999509508785 +7683 21211 34.35 69.08099999999999 11303811.290671017 7667052.165374452 +21211 4326 11303811.290671017 7667052.165374452 34.350000000000016 69.08099966255374 +7683 21212 36.004999999999995 57.0 12250303.860904321 6319913.333340971 +21212 4326 12250303.860904321 6319913.333340971 36.005 56.99999953758631 +7683 21212 34.6505 45.849 12145179.559323221 5080187.627588669 +21212 4326 12145179.559323221 5080187.627588669 34.6505 45.84899949373698 +7683 21212 37.3595 68.151 12306481.811986752 7563346.037355087 +21212 4326 12306481.811986752 7563346.037355087 37.35950000000002 68.15099965048549 +7683 21213 39.0 56.065 13250000.0 6215799.71374706 +21213 4326 13250000.0 6215799.71374706 38.99999999999999 56.06499953109143 +7683 21213 37.65 44.6035 13142825.060669811 4941768.404974476 +21213 4326 13142825.060669811 4941768.404974476 37.64999999999999 44.60349949352727 +7683 21213 40.35 67.5265 13307607.063521609 7493698.541317327 +21213 4326 13307607.063521609 7493698.541317327 40.350000000000016 67.5264996425868 +7683 21214 42.0 55.80499999999999 14250000.0 6186851.148103099 +21214 4326 14250000.0 6186851.148103099 42.0 55.80499952937366 +7683 21214 40.65 44.1635 14142019.127788292 4892875.413020777 +21214 4326 14142019.127788292 4892875.413020777 40.64999999999999 44.163499493682025 +7683 21214 43.35 67.44649999999999 14307801.266374966 7484778.0013011405 +21214 4326 14307801.266374966 7484778.0013011405 43.350000000000016 67.4464996415871 +7683 21215 45.0 61.345 15250000.0 6803936.977533126 +21215 4326 15250000.0 6803936.977533126 44.99999999999999 61.344999574093194 +7683 21215 43.65 43.8355 15141422.506412264 4856430.233726199 +21215 4326 15141422.506412264 4856430.233726199 43.650000000000006 43.83549949387512 +7683 21215 46.35 78.8545 15279141.113203952 8757574.100677717 +21215 4326 15279141.113203952 8757574.100677717 46.349999999999994 78.85449980816442 +7683 21216 48.0 61.05 16250000.0 6771064.425100864 +21216 4326 16250000.0 6771064.425100864 48.0 61.04999957129522 +7683 21216 46.65 43.175999999999995 16140233.749075068 4783156.98167752 +21216 4326 16140233.749075068 4783156.98167752 46.650000000000006 43.17599949446416 +7683 21216 49.35 78.92399999999999 16278961.716707762 8765331.924420642 +21216 4326 16278961.716707762 8765331.924420642 49.35000000000002 78.92399980930053 +7683 21217 51.0 61.79 17250000.0 6853527.104371437 +21217 4326 17250000.0 6853527.104371437 51.0 61.78999957839896 +7683 21217 49.65 44.303 17142273.953828894 4908376.327097079 +21217 4326 17142273.953828894 4908376.327097079 49.65000000000001 44.302999493620035 +7683 21217 52.35 79.27699999999999 17278049.873183865 8804735.490625886 +21217 4326 17278049.873183865 8804735.490625886 52.34999999999999 79.27699981508808 +7683 21218 54.0 65.965 18250000.0 7318935.789134133 +21218 4326 18250000.0 7318935.789134133 54.0 65.96499962358557 +7683 21218 52.65 52.0645 18157420.079025745 5771380.107209642 +21218 4326 18157420.079025745 5771380.107209642 52.64999999999998 52.06449950900779 +7683 21218 55.35 79.8655 18276527.31062487 8870428.576855911 +21218 4326 18276527.31062487 8870428.576855911 55.350000000000094 79.86549982479843 +7683 21219 57.0 66.21000000000001 19250000.0 7346255.422375561 +21219 4326 19250000.0 7346255.422375561 57.00000000000001 66.20999962649455 +7683 21219 55.65 52.098 19157489.37268613 5775107.362628912 +21219 4326 19157489.37268613 5775107.362628912 55.649999999999984 52.09799950915356 +7683 21219 58.35 80.322 19275344.28631681 8921388.309353145 +21219 4326 19275344.28631681 8921388.309353145 58.350000000000044 80.32199983238162 +7683 21220 60.0 66.19 20250000.0 7344025.215498133 +21220 4326 20250000.0 7344025.215498133 59.99999999999999 66.18999962625607 +7683 21220 58.65 52.042 20157373.55644042 5768876.738006353 +21220 4326 20157373.55644042 5768876.738006353 58.64999999999998 52.04199950891027 +7683 21220 61.35 80.338 20275302.791992173 8923174.435352853 +21220 4326 20275302.791992173 8923174.435352853 61.35 80.33799983264821 +7683 21221 63.0 66.4 21250000.0 7367442.676635296 +21221 4326 21250000.0 7367442.676635296 63.0 66.39999962876928 +7683 21221 61.65 52.567 21158462.825234 5827291.049277403 +21221 4326 21158462.825234 5827291.049277403 61.649999999999984 52.566999511264605 +7683 21221 64.35 80.23299999999999 21275575.061670054 8911453.01237118 +21221 4326 21275575.061670054 8911453.01237118 64.34999999999997 80.2329998308998 +7683 21222 66.0 67.78 22250000.0 7521344.470140394 +21222 4326 22250000.0 7521344.470140394 66.0 67.77999964577288 +7683 21222 64.65 55.657000000000004 22165028.15094071 6171199.889481903 +21222 4326 22165028.15094071 6171199.889481903 64.65 55.656999528413145 +7683 21222 67.35 79.90299999999999 22276430.19260875 8874614.70436288 +21222 4326 22276430.19260875 8874614.70436288 67.34999999999995 79.90299982541971 +7683 21223 69.0 65.96 23250000.0 7318378.254713637 +21223 4326 23250000.0 7318378.254713637 69.0 65.95999962352646 +7683 21223 67.65 55.961 23165687.87045827 6205043.289295399 +21223 4326 23165687.87045827 6205043.289295399 67.64999999999999 55.96099953039969 +7683 21223 70.35 75.95899999999999 23286573.0897769 8434401.350702224 +21223 4326 23286573.0897769 8434401.350702224 70.35000000000001 75.95899976191033 +7683 21224 72.0 63.5 24250000.0 7044116.756727536 +21224 4326 24250000.0 7044116.756727536 72.0 63.49999959587919 +7683 21224 70.65 54.437 24162405.103876114 6035397.084202634 +21224 4326 24162405.103876114 6035397.084202634 70.65000000000003 54.43699952097621 +7683 21224 73.35 72.56299999999999 24295167.23668689 8055461.222324877 +21224 4326 24295167.23668689 8055461.222324877 73.34999999999994 72.56299971077743 +7683 21225 75.0 66.59 25250000.0 7388630.451485969 +21225 4326 25250000.0 7388630.451485969 75.0 66.58999963106025 +7683 21225 73.65 54.782 25163142.848765794 6073797.709108062 +21225 4326 25163142.848765794 6073797.709108062 73.65 54.78199952299189 +7683 21225 76.35 78.398 25280318.353878092 8706618.892066907 +21225 4326 25280318.353878092 8706618.892066907 76.34999999999998 78.39799980073033 +7683 21226 78.0 66.26 26250000.0 7351830.964950977 +21226 4326 26250000.0 7351830.964950977 78.0 66.25999962709157 +7683 21226 76.65 52.967 26159297.95734336 5871800.496697653 +21226 4326 26159297.95734336 5871800.496697653 76.65000000000002 52.966999513168446 +7683 21226 79.35 79.553 26277336.17340218 8835544.536588673 +21226 4326 26277336.17340218 8835544.536588673 79.34999999999995 79.5529998196327 +7683 21227 81.0 65.86500000000001 27250000.0 7307785.17024755 +21227 4326 27250000.0 7307785.17024755 81.0 65.8649996224061 +7683 21227 79.65 52.2165 27157734.74210916 5788291.994778293 +21227 4326 27157734.74210916 5788291.994778293 79.65000000000002 52.21649950967457 +7683 21227 82.35 79.5135 27277438.355217196 8831135.23965494 +21227 4326 27277438.355217196 8831135.23965494 82.34999999999994 79.51349981898127 +7683 21228 84.0 63.57 28250000.0 7051919.663217645 +21228 4326 28250000.0 7051919.663217645 84.0 63.56999959662576 +7683 21228 82.65 50.979 28155192.17901026 5650616.832646719 +21228 4326 28155192.17901026 5650616.832646719 82.65 50.97899950464853 +7683 21228 85.35 76.161 28286057.47146335 8456944.769548189 +21228 4326 28286057.47146335 8456944.769548189 85.35000000000008 76.160999765064 +7683 21229 87.0 63.114999999999995 29250000.0 7001202.148696327 +21229 4326 29250000.0 7001202.148696327 87.0 63.114999591816066 +7683 21229 85.65 50.4745 29154168.37289905 5594497.803260256 +21229 4326 29154168.37289905 5594497.803260256 85.65000000000002 50.47449950286395 +7683 21229 88.35 75.7555 29287092.069171328 8411690.891522804 +21229 4326 29287092.069171328 8411690.891522804 88.35 75.7554997587452 +7683 21230 90.0 65.36 30250000.0 7251476.794586202 +21230 4326 30250000.0 7251476.794586202 90.0 65.35999961652011 +7683 21230 88.65 51.032 30155300.166306183 5656512.657329296 +21230 4326 30155300.166306183 5656512.657329296 88.65000000000003 51.03199950484491 +7683 21230 91.35 79.688 30276986.844840877 8850614.363550046 +21230 4326 30276986.844840877 8850614.363550046 91.35000000000007 79.68799982186174 +7683 21231 93.0 65.71000000000001 31250000.0 7290502.000973968 +21231 4326 31250000.0 7290502.000973968 93.0 65.70999962058698 +7683 21231 91.65 51.714999999999996 31156699.05931761 5732495.465166314 +21231 4326 31156699.05931761 5732495.465166314 91.65000000000002 51.71499950752707 +7683 21231 94.35 79.705 31276942.844462328 8852512.053963637 +21231 4326 31276942.844462328 8852512.053963637 94.34999999999998 79.7049998221427 +7683 21232 96.0 65.53999999999999 32250000.0 7271546.674076771 +21232 4326 32250000.0 7271546.674076771 96.00000000000001 65.53999961860453 +7683 21232 94.65 51.311 32155869.973605204 5687550.005580648 +21232 4326 32155869.973605204 5687550.005580648 94.64999999999999 51.31099950590667 +7683 21232 97.35 79.76899999999999 32276777.174348716 8859656.317165745 +21232 4326 32276777.174348716 8859656.317165745 97.34999999999994 79.768999823201 +7683 21233 99.0 65.345 33250000.0 7249804.326373343 +21233 4326 33250000.0 7249804.326373343 99.0 65.34499961634707 +7683 21233 97.65 51.3455 33155940.590392623 5691388.054912955 +21233 4326 33155940.590392623 5691388.054912955 97.64999999999998 51.34549950604122 +7683 21233 100.35 79.34450000000001 33277875.38761839 8812270.264258115 +21233 4326 33277875.38761839 8812270.264258115 100.34999999999991 79.344499816198 +7683 21234 102.0 64.94 34250000.0 7204648.9566982025 +21234 4326 34250000.0 7204648.9566982025 102.0 64.93999961171521 +7683 21234 100.65 51.647 34156559.18143191 5724930.182008068 +21234 4326 34156559.18143191 5724930.182008068 100.65 51.646999507247465 +7683 21234 103.35 78.23299999999999 34280743.38443665 8688201.719715437 +21234 4326 34280743.38443665 8688201.719715437 103.34999999999984 78.23299979805562 +7683 21235 105.0 64.67 35250000.0 7174546.74904855 +21235 4326 35250000.0 7174546.74904855 105.00000000000001 64.66999960867024 +7683 21235 103.65 51.584 35156429.70722129 5717921.243955666 +21235 4326 35156429.70722129 5717921.243955666 103.65 51.58399950699091 +7683 21235 106.35 77.756 35281970.63665872 8634960.495778184 +21235 4326 35281970.63665872 8634960.495778184 106.35000000000004 77.75599979036106 +7683 21236 108.0 63.825 36250000.0 7080345.184620111 +21236 4326 36250000.0 7080345.184620111 108.0 63.82499959936574 +7683 21236 106.65 50.7075 36154640.28435127 5620415.433817068 +21236 4326 36154640.28435127 5620415.433817068 106.65000000000002 50.70749950366903 +7683 21236 109.35 76.9425 36284058.40434327 8544164.294061225 +21236 4326 36284058.40434327 8544164.294061225 109.34999999999997 76.94249977737297 +7683 21237 111.0 62.975 37250000.0 6985597.417676789 +21237 4326 37250000.0 6985597.417676789 111.0 62.974999590356774 +7683 21237 109.65 50.5235 37154267.48359188 5599948.21029713 +21237 4326 37154267.48359188 5599948.21029713 109.65000000000005 50.52349950303054 +7683 21237 112.35 75.4265 37287930.09711673 8374975.498456879 +21237 4326 37287930.09711673 8374975.498456879 112.34999999999992 75.42649975365379 +7683 21238 114.0 63.095 38250000.0 6998972.88237145 +21238 4326 38250000.0 6998972.88237145 114.00000000000001 63.094999591607 +7683 21238 112.65 50.850500000000004 38154930.700730816 5636322.449868074 +21238 4326 38154930.700730816 5636322.449868074 112.65 50.85049950417938 +7683 21238 115.35 75.3395 38288151.49170039 8365266.727834849 +21238 4326 38288151.49170039 8365266.727834849 115.3499999999999 75.3394997523128 +7683 21239 117.0 61.97 39250000.0 6873586.953999472 +21239 4326 39250000.0 6873586.953999472 117.0 61.96999958016951 +7683 21239 115.65 50.756 39154738.71514814 5625810.429206968 +21239 4326 39154738.71514814 5625810.429206968 115.65000000000003 50.75599950384077 +7683 21239 118.35 73.184 39293606.84926657 8124746.510110616 +21239 4326 39293606.84926657 8124746.510110616 118.35000000000002 73.18399971984603 +7683 21240 120.0 61.75 40250000.0 6849069.431902271 +21240 4326 40250000.0 6849069.431902271 119.99999999999999 61.749999578007746 +7683 21240 118.65 51.058 40155353.17110366 5659404.967258742 +21240 4326 40155353.17110366 5659404.967258742 118.65000000000002 51.05799950494191 +7683 21240 121.35 72.442 40295470.65130146 8041961.663616443 +21240 4326 40295470.65130146 8041961.663616443 121.34999999999995 72.44199970902608 +7683 21241 123.0 63.59 41250000.0 7054149.079169952 +21241 4326 41250000.0 7054149.079169952 123.00000000000001 63.58999959683952 +7683 21241 121.65 54.221 41161944.84326996 6011356.014626871 +21241 4326 41161944.84326996 6011356.014626871 121.65000000000003 54.2209995197495 +7683 21241 124.35 72.959 41294172.81858061 8099642.679450545 +21241 4326 41294172.81858061 8099642.679450545 124.35 72.95899971654494 +7683 21242 126.0 61.94 42250000.0 6870243.609018515 +21242 4326 42250000.0 6870243.609018515 126.0 61.93999957987326 +7683 21242 124.65 51.086000000000006 42155410.27518503 5662519.776237923 +21242 4326 42155410.27518503 5662519.776237923 124.64999999999996 51.08599950504678 +7683 21242 127.35 72.794 42294587.422986895 8081233.538517578 +21242 4326 42294587.422986895 8081233.538517578 127.3499999999999 72.79399971413518 +7683 21243 129.0 58.13 43250000.0 6445761.341450645 +21243 4326 43250000.0 6445761.341450645 129.0 58.12999954609109 +7683 21243 127.65 44.216 43142114.95457598 4898709.0482244035 +21243 4326 43142114.95457598 4898709.0482244035 127.64999999999998 44.2159994936573 +7683 21243 130.35 72.044 43296467.197791584 7997559.266001275 +21243 4326 43296467.197791584 7997559.266001275 130.3499999999999 72.04399970330203 +7683 21244 132.0 57.12 44250000.0 6333276.6351319775 +21244 4326 44250000.0 6333276.6351319775 132.0 57.11999953845563 +7683 21244 130.65 43.737 44141244.03599383 4845485.955986398 +21244 4326 44141244.03599383 4845485.955986398 130.64999999999998 43.73699949394607 +7683 21244 133.35 70.50299999999999 44300303.748515375 7825656.014937646 +21244 4326 44300303.748515375 7825656.014937646 133.35000000000002 70.5029996816871 +7683 21245 135.0 59.32000000000001 45250000.0 6578315.826960636 +21245 4326 45250000.0 6578315.826960636 135.0 59.3199995558089 +7683 21245 133.65 44.398 45142447.85908055 4918932.710675051 +21245 4326 45142447.85908055 4918932.710675051 133.65000000000006 44.397999493584706 +7683 21245 136.35 74.242 45290936.513946585 8242797.18770781 +21245 4326 45290936.513946585 8242797.18770781 136.34999999999994 74.24199973559585 +7683 21246 138.0 60.515 46250000.0 6711451.79951043 +21246 4326 46250000.0 6711451.79951043 138.0 60.514999566336854 +7683 21246 136.65 46.335499999999996 46146058.84161628 5134263.721864204 +21246 4326 46146058.84161628 5134263.721864204 136.64999999999998 46.33549949407878 +7683 21246 139.35 74.69449999999999 46289790.03944128 8293290.086065817 +21246 4326 46289790.03944128 8293290.086065817 139.3499999999999 74.69449974244282 +7683 21247 141.0 61.035000000000004 47250000.0 6769392.977967389 +21247 4326 47250000.0 6769392.977967389 141.0 61.03499957115418 +7683 21247 139.65 47.359500000000004 47148015.90292701 5248097.61167041 +21247 4326 47148015.90292701 5248097.61167041 139.65000000000003 47.3594994952747 +7683 21247 142.35 74.7105 47289749.45447423 8295075.506114522 +21247 4326 47289749.45447423 8295075.506114522 142.3500000000001 74.71049974268611 +7683 21248 144.0 59.795 48250000.0 6631233.070213608 +21248 4326 48250000.0 6631233.070213608 144.0 59.79499955990233 +7683 21248 142.65 45.2285 48143980.77244777 5011224.820716897 +21248 4326 48143980.77244777 5011224.820716897 142.65000000000003 45.22849949351284 +7683 21248 145.35 74.3615 48290633.9927476 8256131.585015278 +21248 4326 48290633.9927476 8256131.585015278 145.3499999999999 74.36149973739775 +7683 21249 147.0 60.18000000000001 49250000.0 6674126.715014838 +21249 4326 49250000.0 6674126.715014838 147.0 60.17999956330877 +7683 21249 145.65 45.258 49144035.636879615 5014503.341766106 +21249 4326 49144035.636879615 5014503.341766106 145.64999999999995 45.25799949351813 +7683 21249 148.35 75.102 49288755.41629677 8338763.252060226 +21249 4326 49288755.41629677 8338763.252060226 148.35 75.1019997486637 +7683 21250 150.0 61.015 50250000.0 6767164.387611585 +21250 4326 50250000.0 6767164.387611585 150.0 61.014999570966275 +7683 21250 148.65 46.7905 50146924.343773775 5184841.82381938 +21250 4326 50146924.343773775 5184841.82381938 148.64999999999998 46.79049949453048 +7683 21250 151.35 75.23949999999999 50288405.857642256 8354107.306558173 +21250 4326 50288405.857642256 8354107.306558173 151.35000000000008 75.23949975077426 +7683 21251 153.0 61.49 51250000.0 6820095.200418049 +21251 4326 51250000.0 6820095.200418049 153.0 61.48999957548499 +7683 21251 151.65 48.197 51149640.948537834 5341213.41288723 +21251 4326 51149640.948537834 5341213.41288723 151.64999999999995 48.19699949673204 +7683 21251 154.35 74.783 51289565.51437167 8303165.721029402 +21251 4326 51289565.51437167 8303165.721029402 154.35000000000008 74.78299974378947 +7683 21252 156.0 63.11 52250000.0 7000644.831520897 +21252 4326 52250000.0 7000644.831520897 156.0 63.10999959176379 +7683 21252 154.65 50.429 52154076.40484687 5589436.750042504 +21252 4326 52154076.40484687 5589436.750042504 154.65000000000006 50.428999502710575 +7683 21252 157.35 75.791 52287001.568779804 8415652.64063498 +21252 4326 52287001.568779804 8415652.64063498 157.3500000000001 75.79099975929648 +7683 21253 159.0 61.24 53250000.0 6792236.412226566 +21253 4326 53250000.0 6792236.412226566 159.0 61.23999957309214 +7683 21253 157.65 52.348 53158007.49602016 5802923.336770041 +21253 4326 53158007.49602016 5802923.336770041 157.64999999999998 52.34799951026254 +7683 21253 160.35 70.132 53301221.99137843 7784273.910525416 +21253 4326 53301221.99137843 7784273.910525416 160.34999999999997 70.13199967661797 +7683 21254 162.0 62.660000000000004 54250000.0 6950487.913850949 +21254 4326 54250000.0 6950487.913850949 162.0 62.65999958710906 +7683 21254 160.65 55.172000000000004 54163980.652077 6117209.61108447 +21254 4326 54163980.652077 6117209.61108447 160.65000000000006 55.17199952535363 +7683 21254 163.35 70.148 54301182.4353571 7786058.549936872 +21254 4326 54301182.4353571 7786058.549936872 163.3499999999999 70.14799967683545 +7683 21255 165.0 62.254999999999995 55250000.0 6905349.461038101 +21255 4326 55250000.0 6905349.461038101 165.0 62.254999583006615 +7683 21255 163.65 55.4465 55164572.75444887 6147766.495492223 +21255 4326 55164572.75444887 6147766.495492223 163.65000000000006 55.446499527068625 +7683 21255 166.35 69.06349999999999 55303854.25135465 7665100.442820075 +21255 4326 55303854.25135465 7665100.442820075 166.35000000000002 69.06349966232335 +7683 21256 168.0 62.26 56250000.0 6905906.709760254 +21256 4326 56250000.0 6905906.709760254 168.0 62.259999583056754 +7683 21256 166.65 55.231 56164107.74843201 6123777.2829459775 +21256 4326 56164107.74843201 6123777.2829459775 166.65000000000003 55.23099952571858 +7683 21256 169.35 69.28899999999999 56303300.28287712 7690250.071945211 +21256 4326 56303300.28287712 7690250.071945211 169.35 69.28899966530182 +7683 21257 171.0 65.025 57250000.0 7214125.805512015 +21257 4326 57250000.0 7214125.805512015 171.0 65.02499961268097 +7683 21257 169.65 60.3765 57175531.09960178 6696782.851071486 +21257 4326 57175531.09960178 6696782.851071486 169.65000000000003 60.37649956507774 +7683 21257 172.35 69.67349999999999 57302353.78173571 7733134.137563069 +21257 4326 57302353.78173571 7733134.137563069 172.35000000000008 69.67349967042797 +7683 21258 174.0 65.505 58250000.0 7267644.159841498 +21258 4326 58250000.0 7267644.159841498 174.0 65.50499961819806 +7683 21258 172.65 61.441500000000005 58177974.53207971 6815435.854136693 +21258 4326 58177974.53207971 6815435.854136693 172.65 61.44149957501825 +7683 21258 175.35 69.5685 58302612.493663445 7721423.095588066 +21258 4326 58302612.493663445 7721423.095588066 175.35000000000002 69.56849966902222 +7683 21259 177.0 66.595 59250000.0 7389188.031509473 +21259 4326 59250000.0 7389188.031509473 177.0 66.59499963112076 +7683 21259 175.65 62.5405 59180522.39538791 6937895.354953286 +21259 4326 59180522.39538791 6937895.354953286 175.65000000000006 62.54049958589001 +7683 21259 178.35 70.64949999999999 59299940.56150254 7841997.371496604 +21259 4326 59299940.56150254 7841997.371496604 178.35000000000002 70.6494996837035 +7683 21260 180.0 66.94500000000001 60250000.0 7428219.520076082 +21260 4326 60250000.0 7428219.520076082 180.0 66.94499963538405 +7683 21260 178.65 62.7105 60180918.8520934 6956839.784644662 +21260 4326 60180918.8520934 6956839.784644662 178.64999999999995 62.71049958762639 +7683 21260 -178.65 71.1795 60298623.88849147 7901118.343988915 +21260 4326 60298623.88849147 7901118.343988915 -178.65000000000003 71.17949969106675 +7683 21261 -177.0 68.175 61250000.0 7565400.95129408 +21261 4326 61250000.0 7565400.95129408 -177.0 68.17499965079237 +7683 21261 -178.35 65.0835 61186515.695693776 7221326.506768895 +21261 4326 61186515.695693776 7221326.506768895 -178.35 65.08349961334758 +7683 21261 -175.65 71.2665 61298407.34826981 7910823.414080942 +21261 4326 61298407.34826981 7910823.414080942 -175.65000000000003 71.26649969228556 +7683 21262 -174.0 65.99000000000001 62250000.0 7321723.466718189 +21262 4326 62250000.0 7321723.466718189 -174.0 65.98999962388115 +7683 21262 -175.35 64.379 62184842.127483614 7142796.689009782 +21262 4326 62184842.127483614 7142796.689009782 -175.34999999999997 64.37899960542725 +7683 21262 -172.65 67.601 62307426.109564595 7502005.869727269 +21262 4326 62307426.109564595 7502005.869727269 -172.64999999999992 67.60099964352025 +7683 21263 -171.035 65.705 63248392.49913818 7289944.93277124 +21263 4326 63248392.49913818 7289944.93277124 -171.0349999999999 65.70499962052847 +7683 21263 -172.3535 64.4855 63184926.23381718 7154671.24916256 +21263 4326 63184926.23381718 7154671.24916256 -172.35349999999994 64.48549960660942 +7683 21263 -169.7165 66.9245 63306156.43136204 7426512.03632176 +21263 4326 63306156.43136204 7426512.03632176 -169.71650000000008 66.92449963513283 +7683 21264 -169.095 65.78 64199855.922675304 7298744.260751339 +21264 4326 64199855.922675304 7298744.260751339 -169.09499999999997 65.77999962140714 +7683 21264 -169.2075 65.708 64194550.35827898 7290811.584335636 +21264 4326 64194550.35827898 7290811.584335636 -169.20749999999995 65.70799962056358 +7683 21264 -168.9825 65.852 64205132.943670936 7306686.635768692 +21264 4326 64205132.943670936 7306686.635768692 -168.98250000000004 65.85199962225312 +4212 21291 -59.54 13.195 666608.6051384729 1459730.9001096727 +21291 4326 666608.6051384729 1459730.9001096727 -59.53833830660966 13.197790652550669 +4212 21291 -59.693 13.0195 650195.9065330669 1440154.577859782 +21291 4326 650195.9065330669 1440154.577859782 -59.691345477724035 13.022305068789029 +4212 21291 -59.387 13.3705 682996.7698567866 1479321.6101423502 +21291 4326 682996.7698567866 1479321.6101423502 -59.38533112159524 13.373276219485213 +4212 21292 -59.54 13.195 32137.918417985875 77058.89448483479 +21292 4326 32137.918417985875 77058.89448483479 -59.53833830660966 13.197790652550669 +4212 21292 -59.693 13.0195 15542.236673424344 57648.443102503035 +21292 4326 15542.236673424344 57648.443102503035 -59.691345477724035 13.022305068789027 +4212 21292 -59.387 13.3705 48709.9258678611 96479.75739617081 +21292 4326 48709.9258678611 96479.75739617081 -59.38533112159524 13.373276219485215 +7683 21307 21.035 54.82 252249.81008450311 6077192.392794833 +21307 4326 252249.81008450311 6077192.392794833 21.035 54.81999952321813 +7683 21307 19.7165 54.37 166583.8640357198 6027859.072900273 +21307 4326 166583.8640357198 6027859.072900273 19.7165 54.369999520592785 +7683 21307 22.3535 55.27 336030.6393710225 6128122.972513546 +21307 4326 336030.6393710225 6128122.972513546 22.3535 55.26999952596093 +7683 21308 22.685000000000002 54.705 165233.91077632594 6065184.124216 +21308 4326 165233.91077632594 6065184.124216 22.685 54.70499952253601 +7683 21308 22.5185 54.3765 153731.683742058 6028834.928610979 +21308 4326 153731.683742058 6028834.928610979 22.5185 54.37649952062987 +7683 21308 22.8515 55.033500000000004 176565.68033279415 6101562.290421037 +21308 4326 176565.68033279415 6101562.290421037 22.8515 55.03349952450484 +7683 21309 27.560000000000002 62.49 278870.4107600842 6931665.723556881 +21309 4326 278870.4107600842 6931665.723556881 27.560000000000002 62.489999585376985 +7683 21309 26.705 56.693999999999996 231925.35433000507 6285876.93111335 +21309 4326 231925.35433000507 6285876.93111335 26.704999999999995 56.693999535406256 +7683 21309 28.415000000000003 68.286 308441.9677507229 7578452.25247484 +21309 4326 308441.9677507229 7578452.25247484 28.415000000000006 68.28599965221487 +7683 21310 30.0 61.349999999999994 250000.0 6804494.151856439 +21310 4326 250000.0 6804494.151856439 29.999999999999996 61.34999957414101 +7683 21310 28.65 53.7 160839.89316485182 5953371.439720425 +21310 4326 160839.89316485182 5953371.439720425 28.64999999999999 53.69999951690295 +7683 21310 31.35 69.0 304010.0943770238 7658018.510035111 +21310 4326 304010.0943770238 7658018.510035111 31.35 68.99999966148839 +7683 21311 33.0 60.629999999999995 250000.0 6724265.32405217 +21311 4326 250000.0 6724265.32405217 32.99999999999999 60.629999567389994 +7683 21311 31.65 52.179 157657.05041793198 5784119.615608411 +21311 4326 157657.05041793198 5784119.615608411 31.65 52.178999509508785 +7683 21311 34.35 69.08099999999999 303811.29067101644 7667052.165374452 +21311 4326 303811.29067101644 7667052.165374452 34.35 69.08099966255374 +7683 21312 36.004999999999995 57.0 250303.86090432073 6319913.333340971 +21312 4326 250303.86090432073 6319913.333340971 36.004999999999995 56.99999953758631 +7683 21312 34.6505 45.849 145179.55932322104 5080187.627588669 +21312 4326 145179.55932322104 5080187.627588669 34.650499999999994 45.84899949373698 +7683 21312 37.3595 68.151 306481.81198675133 7563346.037355087 +21312 4326 306481.81198675133 7563346.037355087 37.3595 68.15099965048549 +7683 21313 39.0 56.065 250000.0 6215799.71374706 +21313 4326 250000.0 6215799.71374706 38.99999999999999 56.06499953109143 +7683 21313 37.65 44.6035 142825.06066981173 4941768.404974476 +21313 4326 142825.06066981173 4941768.404974476 37.65 44.60349949352728 +7683 21313 40.35 67.5265 307607.06352160824 7493698.541317327 +21313 4326 307607.06352160824 7493698.541317327 40.35 67.5264996425868 +7683 21314 42.0 55.80499999999999 250000.0 6186851.148103099 +21314 4326 250000.0 6186851.148103099 42.0 55.80499952937366 +7683 21314 40.65 44.1635 142019.12778829277 4892875.413020777 +21314 4326 142019.12778829277 4892875.413020777 40.64999999999999 44.163499493682025 +7683 21314 43.35 67.44649999999999 307801.2663749655 7484778.0013011405 +21314 4326 307801.2663749655 7484778.0013011405 43.35 67.4464996415871 +7683 21315 45.0 61.345 250000.0 6803936.977533126 +21315 4326 250000.0 6803936.977533126 44.99999999999999 61.344999574093194 +7683 21315 43.65 43.8355 141422.50641226314 4856430.233726199 +21315 4326 141422.50641226314 4856430.233726199 43.65 43.83549949387512 +7683 21315 46.35 78.8545 279141.1132039523 8757574.100677717 +21315 4326 279141.1132039523 8757574.100677717 46.35 78.85449980816442 +7683 21316 48.0 61.05 250000.0 6771064.425100864 +21316 4326 250000.0 6771064.425100864 48.0 61.04999957129522 +7683 21316 46.65 43.175999999999995 140233.74907506802 4783156.98167752 +21316 4326 140233.74907506802 4783156.98167752 46.650000000000006 43.17599949446416 +7683 21316 49.35 78.92399999999999 278961.71670776187 8765331.924420642 +21316 4326 278961.71670776187 8765331.924420642 49.35 78.92399980930053 +7683 21317 51.0 61.79 250000.0 6853527.104371437 +21317 4326 250000.0 6853527.104371437 51.0 61.78999957839896 +7683 21317 49.65 44.303 142273.9538288923 4908376.327097079 +21317 4326 142273.9538288923 4908376.327097079 49.650000000000006 44.302999493620035 +7683 21317 52.35 79.27699999999999 278049.8731838654 8804735.490625886 +21317 4326 278049.8731838654 8804735.490625886 52.349999999999994 79.27699981508809 +7683 21318 54.0 65.965 250000.0 7318935.789134133 +21318 4326 250000.0 7318935.789134133 54.0 65.96499962358557 +7683 21318 52.65 52.0645 157420.0790257467 5771380.107209642 +21318 4326 157420.0790257467 5771380.107209642 52.65 52.06449950900779 +7683 21318 55.35 79.8655 276527.3106248698 8870428.576855911 +21318 4326 276527.3106248698 8870428.576855911 55.35 79.86549982479843 +7683 21319 57.0 66.21000000000001 250000.0 7346255.422375561 +21319 4326 250000.0 7346255.422375561 57.00000000000001 66.20999962649455 +7683 21319 55.65 52.098 157489.37268613055 5775107.362628912 +21319 4326 157489.37268613055 5775107.362628912 55.64999999999999 52.09799950915356 +7683 21319 58.35 80.322 275344.2863168072 8921388.309353145 +21319 4326 275344.2863168072 8921388.309353145 58.349999999999994 80.32199983238162 +7683 21320 60.0 66.19 250000.0 7344025.215498133 +21320 4326 250000.0 7344025.215498133 59.99999999999999 66.18999962625607 +7683 21320 58.65 52.042 157373.5564404221 5768876.738006353 +21320 4326 157373.5564404221 5768876.738006353 58.650000000000006 52.04199950891027 +7683 21320 61.35 80.338 275302.79199217254 8923174.435352853 +21320 4326 275302.79199217254 8923174.435352853 61.35 80.33799983264821 +7683 21321 63.0 66.4 250000.0 7367442.676635296 +21321 4326 250000.0 7367442.676635296 63.0 66.39999962876928 +7683 21321 61.65 52.567 158462.82523400022 5827291.049277403 +21321 4326 158462.82523400022 5827291.049277403 61.64999999999999 52.566999511264605 +7683 21321 64.35 80.23299999999999 275575.06167005433 8911453.01237118 +21321 4326 275575.06167005433 8911453.01237118 64.35 80.2329998308998 +7683 21322 66.0 67.78 250000.0 7521344.470140394 +21322 4326 250000.0 7521344.470140394 66.0 67.77999964577288 +7683 21322 64.65 55.657000000000004 165028.15094070937 6171199.889481903 +21322 4326 165028.15094070937 6171199.889481903 64.65 55.656999528413145 +7683 21322 67.35 79.90299999999999 276430.19260875206 8874614.70436288 +21322 4326 276430.19260875206 8874614.70436288 67.35 79.90299982541971 +7683 21323 69.0 65.96 250000.0 7318378.254713637 +21323 4326 250000.0 7318378.254713637 69.0 65.95999962352646 +7683 21323 67.65 55.961 165687.87045827223 6205043.289295399 +21323 4326 165687.87045827223 6205043.289295399 67.65 55.96099953039969 +7683 21323 70.35 75.95899999999999 286573.08977689926 8434401.350702224 +21323 4326 286573.08977689926 8434401.350702224 70.35 75.95899976191033 +7683 21324 72.0 63.5 250000.0 7044116.756727536 +21324 4326 250000.0 7044116.756727536 72.0 63.49999959587919 +7683 21324 70.65 54.437 162405.10387611223 6035397.084202634 +21324 4326 162405.10387611223 6035397.084202634 70.65 54.436999520976194 +7683 21324 73.35 72.56299999999999 295167.23668689077 8055461.222324877 +21324 4326 295167.23668689077 8055461.222324877 73.35 72.56299971077743 +7683 21325 75.0 66.59 250000.0 7388630.451485969 +21325 4326 250000.0 7388630.451485969 75.0 66.58999963106025 +7683 21325 73.65 54.782 163142.8487657939 6073797.709108062 +21325 4326 163142.8487657939 6073797.709108062 73.65 54.78199952299189 +7683 21325 76.35 78.398 280318.3538780924 8706618.892066907 +21325 4326 280318.3538780924 8706618.892066907 76.35 78.39799980073033 +7683 21326 78.0 66.26 250000.0 7351830.964950977 +21326 4326 250000.0 7351830.964950977 78.0 66.25999962709157 +7683 21326 76.65 52.967 159297.95734335738 5871800.496697653 +21326 4326 159297.95734335738 5871800.496697653 76.65 52.966999513168446 +7683 21326 79.35 79.553 277336.17340218014 8835544.536588673 +21326 4326 277336.17340218014 8835544.536588673 79.35 79.5529998196327 +7683 21327 81.0 65.86500000000001 250000.0 7307785.17024755 +21327 4326 250000.0 7307785.17024755 81.0 65.8649996224061 +7683 21327 79.65 52.2165 157734.74210915976 5788291.994778293 +21327 4326 157734.74210915976 5788291.994778293 79.65 52.21649950967457 +7683 21327 82.35 79.5135 277438.355217197 8831135.23965494 +21327 4326 277438.355217197 8831135.23965494 82.35 79.51349981898127 +7683 21328 84.0 63.57 250000.0 7051919.663217645 +21328 4326 250000.0 7051919.663217645 84.0 63.56999959662576 +7683 21328 82.65 50.979 155192.17901026062 5650616.832646719 +21328 4326 155192.17901026062 5650616.832646719 82.65 50.97899950464853 +7683 21328 85.35 76.161 286057.47146334685 8456944.769548189 +21328 4326 286057.47146334685 8456944.769548189 85.35 76.160999765064 +7683 21329 87.0 63.114999999999995 250000.0 7001202.148696327 +21329 4326 250000.0 7001202.148696327 87.0 63.114999591816066 +7683 21329 85.65 50.4745 154168.37289905077 5594497.803260256 +21329 4326 154168.37289905077 5594497.803260256 85.65 50.47449950286395 +7683 21329 88.35 75.7555 287092.0691713282 8411690.891522804 +21329 4326 287092.0691713282 8411690.891522804 88.35 75.7554997587452 +7683 21330 90.0 65.36 250000.0 7251476.794586202 +21330 4326 250000.0 7251476.794586202 90.0 65.35999961652011 +7683 21330 88.65 51.032 155300.1663061809 5656512.657329296 +21330 4326 155300.1663061809 5656512.657329296 88.65 51.03199950484491 +7683 21330 91.35 79.688 276986.84484087536 8850614.363550046 +21330 4326 276986.84484087536 8850614.363550046 91.35 79.68799982186174 +7683 21331 93.0 65.71000000000001 250000.0 7290502.000973968 +21331 4326 250000.0 7290502.000973968 93.0 65.70999962058698 +7683 21331 91.65 51.714999999999996 156699.0593176104 5732495.465166314 +21331 4326 156699.0593176104 5732495.465166314 91.65 51.71499950752707 +7683 21331 94.35 79.705 276942.8444623279 8852512.053963637 +21331 4326 276942.8444623279 8852512.053963637 94.35 79.7049998221427 +7683 21332 96.0 65.53999999999999 250000.0 7271546.674076771 +21332 4326 250000.0 7271546.674076771 96.00000000000001 65.53999961860453 +7683 21332 94.65 51.311 155869.97360520536 5687550.005580648 +21332 4326 155869.97360520536 5687550.005580648 94.65 51.31099950590667 +7683 21332 97.35 79.76899999999999 276777.17434871686 8859656.317165745 +21332 4326 276777.17434871686 8859656.317165745 97.35 79.768999823201 +7683 21333 99.0 65.345 250000.0 7249804.326373343 +21333 4326 250000.0 7249804.326373343 99.0 65.34499961634707 +7683 21333 97.65 51.3455 155940.5903926245 5691388.054912955 +21333 4326 155940.5903926245 5691388.054912955 97.65 51.34549950604122 +7683 21333 100.35 79.34450000000001 277875.3876183906 8812270.264258115 +21333 4326 277875.3876183906 8812270.264258115 100.35 79.344499816198 +7683 21334 102.0 64.94 250000.0 7204648.9566982025 +21334 4326 250000.0 7204648.9566982025 102.0 64.93999961171521 +7683 21334 100.65 51.647 156559.18143191148 5724930.182008068 +21334 4326 156559.18143191148 5724930.182008068 100.65 51.646999507247465 +7683 21334 103.35 78.23299999999999 280743.3844366555 8688201.719715437 +21334 4326 280743.3844366555 8688201.719715437 103.35 78.23299979805562 +7683 21335 105.0 64.67 250000.0 7174546.74904855 +21335 4326 250000.0 7174546.74904855 105.00000000000001 64.66999960867024 +7683 21335 103.65 51.584 156429.7072212916 5717921.243955666 +21335 4326 156429.7072212916 5717921.243955666 103.65 51.58399950699091 +7683 21335 106.35 77.756 281970.6366587198 8634960.495778184 +21335 4326 281970.6366587198 8634960.495778184 106.35 77.75599979036106 +7683 21336 108.0 63.825 250000.0 7080345.184620111 +21336 4326 250000.0 7080345.184620111 108.0 63.82499959936574 +7683 21336 106.65 50.7075 154640.28435126628 5620415.433817068 +21336 4326 154640.28435126628 5620415.433817068 106.65 50.707499503669034 +7683 21336 109.35 76.9425 284058.4043432705 8544164.294061225 +21336 4326 284058.4043432705 8544164.294061225 109.35 76.94249977737297 +7683 21337 111.0 62.975 250000.0 6985597.417676789 +21337 4326 250000.0 6985597.417676789 111.0 62.974999590356774 +7683 21337 109.65 50.5235 154267.4835918742 5599948.21029713 +21337 4326 154267.4835918742 5599948.21029713 109.65 50.52349950303054 +7683 21337 112.35 75.4265 287930.09711673314 8374975.498456879 +21337 4326 287930.09711673314 8374975.498456879 112.35 75.42649975365379 +7683 21338 114.0 63.095 250000.0 6998972.88237145 +21338 4326 250000.0 6998972.88237145 114.00000000000001 63.094999591607 +7683 21338 112.65 50.850500000000004 154930.70073081582 5636322.449868074 +21338 4326 154930.70073081582 5636322.449868074 112.65 50.85049950417938 +7683 21338 115.35 75.3395 288151.4917003911 8365266.727834849 +21338 4326 288151.4917003911 8365266.727834849 115.35 75.3394997523128 +7683 21339 117.0 61.97 250000.0 6873586.953999472 +21339 4326 250000.0 6873586.953999472 117.0 61.96999958016951 +7683 21339 115.65 50.756 154738.71514814242 5625810.429206968 +21339 4326 154738.71514814242 5625810.429206968 115.65 50.75599950384077 +7683 21339 118.35 73.184 293606.8492665657 8124746.510110616 +21339 4326 293606.8492665657 8124746.510110616 118.35 73.18399971984603 +7683 21340 120.0 61.75 250000.0 6849069.431902271 +21340 4326 250000.0 6849069.431902271 119.99999999999999 61.749999578007746 +7683 21340 118.65 51.058 155353.1711036542 5659404.967258742 +21340 4326 155353.1711036542 5659404.967258742 118.65 51.05799950494191 +7683 21340 121.35 72.442 295470.65130146046 8041961.663616443 +21340 4326 295470.65130146046 8041961.663616443 121.35000000000001 72.44199970902608 +7683 21341 123.0 63.59 250000.0 7054149.079169952 +21341 4326 250000.0 7054149.079169952 123.00000000000001 63.58999959683952 +7683 21341 121.65 54.221 161944.84326995726 6011356.014626871 +21341 4326 161944.84326995726 6011356.014626871 121.65000000000002 54.2209995197495 +7683 21341 124.35 72.959 294172.81858061266 8099642.679450545 +21341 4326 294172.81858061266 8099642.679450545 124.35 72.95899971654494 +7683 21342 126.0 61.94 250000.0 6870243.609018515 +21342 4326 250000.0 6870243.609018515 126.0 61.93999957987326 +7683 21342 124.65 51.086000000000006 155410.27518503653 5662519.776237923 +21342 4326 155410.27518503653 5662519.776237923 124.65 51.08599950504678 +7683 21342 127.35 72.794 294587.4229868981 8081233.538517578 +21342 4326 294587.4229868981 8081233.538517578 127.35 72.7939997141352 +7683 21343 129.0 58.13 250000.0 6445761.341450645 +21343 4326 250000.0 6445761.341450645 129.0 58.12999954609109 +7683 21343 127.65 44.216 142114.9545759812 4898709.0482244035 +21343 4326 142114.9545759812 4898709.0482244035 127.65 44.2159994936573 +7683 21343 130.35 72.044 296467.197791587 7997559.266001275 +21343 4326 296467.197791587 7997559.266001275 130.35000000000002 72.04399970330202 +7683 21344 132.0 57.12 250000.0 6333276.6351319775 +21344 4326 250000.0 6333276.6351319775 132.0 57.11999953845563 +7683 21344 130.65 43.737 141244.03599383184 4845485.955986398 +21344 4326 141244.03599383184 4845485.955986398 130.65 43.736999493946065 +7683 21344 133.35 70.50299999999999 300303.74851537443 7825656.014937646 +21344 4326 300303.74851537443 7825656.014937646 133.35 70.5029996816871 +7683 21345 135.0 59.32000000000001 250000.0 6578315.826960636 +21345 4326 250000.0 6578315.826960636 135.0 59.3199995558089 +7683 21345 133.65 44.398 142447.85908054977 4918932.710675051 +21345 4326 142447.85908054977 4918932.710675051 133.65 44.397999493584706 +7683 21345 136.35 74.242 290936.5139465867 8242797.18770781 +21345 4326 290936.5139465867 8242797.18770781 136.35 74.24199973559585 +7683 21346 138.0 60.515 250000.0 6711451.79951043 +21346 4326 250000.0 6711451.79951043 138.0 60.514999566336854 +7683 21346 136.65 46.335499999999996 146058.84161628198 5134263.721864204 +21346 4326 146058.84161628198 5134263.721864204 136.65 46.33549949407879 +7683 21346 139.35 74.69449999999999 289790.03944128315 8293290.086065817 +21346 4326 289790.03944128315 8293290.086065817 139.35000000000002 74.69449974244282 +7683 21347 141.0 61.035000000000004 250000.0 6769392.977967389 +21347 4326 250000.0 6769392.977967389 141.0 61.03499957115418 +7683 21347 139.65 47.359500000000004 148015.90292700904 5248097.61167041 +21347 4326 148015.90292700904 5248097.61167041 139.65 47.3594994952747 +7683 21347 142.35 74.7105 289749.45447422983 8295075.506114522 +21347 4326 289749.45447422983 8295075.506114522 142.35 74.71049974268611 +7683 21348 144.0 59.795 250000.0 6631233.070213608 +21348 4326 250000.0 6631233.070213608 144.0 59.79499955990233 +7683 21348 142.65 45.2285 143980.7724477707 5011224.820716897 +21348 4326 143980.7724477707 5011224.820716897 142.65 45.22849949351285 +7683 21348 145.35 74.3615 290633.99274759984 8256131.585015278 +21348 4326 290633.99274759984 8256131.585015278 145.35 74.36149973739775 +7683 21349 147.0 60.18000000000001 250000.0 6674126.715014838 +21349 4326 250000.0 6674126.715014838 147.0 60.17999956330877 +7683 21349 145.65 45.258 144035.6368796185 5014503.341766106 +21349 4326 144035.6368796185 5014503.341766106 145.65 45.25799949351812 +7683 21349 148.35 75.102 288755.41629677336 8338763.252060226 +21349 4326 288755.41629677336 8338763.252060226 148.35000000000002 75.1019997486637 +7683 21350 150.0 61.015 250000.0 6767164.387611585 +21350 4326 250000.0 6767164.387611585 150.0 61.014999570966275 +7683 21350 148.65 46.7905 146924.3437737778 5184841.82381938 +21350 4326 146924.3437737778 5184841.82381938 148.65 46.790499494530486 +7683 21350 151.35 75.23949999999999 288405.85764225345 8354107.306558173 +21350 4326 288405.85764225345 8354107.306558173 151.35 75.23949975077426 +7683 21351 153.0 61.49 250000.0 6820095.200418049 +21351 4326 250000.0 6820095.200418049 153.0 61.48999957548499 +7683 21351 151.65 48.197 149640.94853783736 5341213.41288723 +21351 4326 149640.94853783736 5341213.41288723 151.65 48.19699949673204 +7683 21351 154.35 74.783 289565.51437166764 8303165.721029402 +21351 4326 289565.51437166764 8303165.721029402 154.35 74.78299974378947 +7683 21352 156.0 63.11 250000.0 7000644.831520897 +21352 4326 250000.0 7000644.831520897 156.0 63.10999959176379 +7683 21352 154.65 50.429 154076.40484686644 5589436.750042504 +21352 4326 154076.40484686644 5589436.750042504 154.65 50.42899950271057 +7683 21352 157.35 75.791 287001.568779801 8415652.64063498 +21352 4326 287001.568779801 8415652.64063498 157.35000000000002 75.79099975929648 +7683 21353 159.0 61.24 250000.0 6792236.412226566 +21353 4326 250000.0 6792236.412226566 159.0 61.23999957309214 +7683 21353 157.65 52.348 158007.49602016417 5802923.336770041 +21353 4326 158007.49602016417 5802923.336770041 157.65 52.34799951026254 +7683 21353 160.35 70.132 301221.99137842783 7784273.910525416 +21353 4326 301221.99137842783 7784273.910525416 160.35 70.13199967661797 +7683 21354 162.0 62.660000000000004 250000.0 6950487.913850949 +21354 4326 250000.0 6950487.913850949 162.0 62.65999958710906 +7683 21354 160.65 55.172000000000004 163980.6520769933 6117209.61108447 +21354 4326 163980.6520769933 6117209.61108447 160.65 55.171999525353634 +7683 21354 163.35 70.148 301182.43535710406 7786058.549936872 +21354 4326 301182.43535710406 7786058.549936872 163.35 70.14799967683545 +7683 21355 165.0 62.254999999999995 250000.0 6905349.461038101 +21355 4326 250000.0 6905349.461038101 165.0 62.254999583006615 +7683 21355 163.65 55.4465 164572.7544488652 6147766.495492223 +21355 4326 164572.7544488652 6147766.495492223 163.65 55.446499527068625 +7683 21355 166.35 69.06349999999999 303854.25135464943 7665100.442820075 +21355 4326 303854.25135464943 7665100.442820075 166.35000000000002 69.06349966232335 +7683 21356 168.0 62.26 250000.0 6905906.709760254 +21356 4326 250000.0 6905906.709760254 168.0 62.259999583056754 +7683 21356 166.65 55.231 164107.7484320082 6123777.2829459775 +21356 4326 164107.7484320082 6123777.2829459775 166.65 55.23099952571858 +7683 21356 169.35 69.28899999999999 303300.28287711705 7690250.071945211 +21356 4326 303300.28287711705 7690250.071945211 169.35 69.28899966530182 +7683 21357 171.0 65.025 250000.0 7214125.805512015 +21357 4326 250000.0 7214125.805512015 171.0 65.02499961268097 +7683 21357 169.65 60.3765 175531.09960178076 6696782.851071486 +21357 4326 175531.09960178076 6696782.851071486 169.65 60.37649956507772 +7683 21357 172.35 69.67349999999999 302353.7817357078 7733134.137563069 +21357 4326 302353.7817357078 7733134.137563069 172.35 69.67349967042799 +7683 21358 174.0 65.505 250000.0 7267644.159841498 +21358 4326 250000.0 7267644.159841498 174.0 65.50499961819806 +7683 21358 172.65 61.441500000000005 177974.53207971223 6815435.854136693 +21358 4326 177974.53207971223 6815435.854136693 172.65 61.44149957501825 +7683 21358 175.35 69.5685 302612.49366344546 7721423.095588066 +21358 4326 302612.49366344546 7721423.095588066 175.35000000000002 69.56849966902222 +7683 21359 177.0 66.595 250000.0 7389188.031509473 +21359 4326 250000.0 7389188.031509473 177.0 66.59499963112076 +7683 21359 175.65 62.5405 180522.3953879077 6937895.354953286 +21359 4326 180522.3953879077 6937895.354953286 175.65 62.54049958589 +7683 21359 178.35 70.64949999999999 299940.56150253786 7841997.371496604 +21359 4326 299940.56150253786 7841997.371496604 178.35 70.6494996837035 +7683 21360 180.0 66.94500000000001 250000.0 7428219.520076082 +21360 4326 250000.0 7428219.520076082 180.0 66.94499963538405 +7683 21360 178.65 62.7105 180918.85209340113 6956839.784644662 +21360 4326 180918.85209340113 6956839.784644662 178.65 62.71049958762639 +7683 21360 -178.65 71.1795 298623.88849146536 7901118.343988915 +21360 4326 298623.88849146536 7901118.343988915 -178.65000000000003 71.17949969106675 +7683 21361 -177.0 68.175 250000.0 7565400.95129408 +21361 4326 250000.0 7565400.95129408 -177.0 68.17499965079237 +7683 21361 -178.35 65.0835 186515.69569377584 7221326.506768895 +21361 4326 186515.69569377584 7221326.506768895 -178.35 65.08349961334758 +7683 21361 -175.65 71.2665 298407.3482698135 7910823.414080942 +21361 4326 298407.3482698135 7910823.414080942 -175.65 71.26649969228556 +7683 21362 -174.0 65.99000000000001 250000.0 7321723.466718189 +21362 4326 250000.0 7321723.466718189 -174.0 65.98999962388115 +7683 21362 -175.35 64.379 184842.12748361094 7142796.689009782 +21362 4326 184842.12748361094 7142796.689009782 -175.35000000000002 64.37899960542725 +7683 21362 -172.65 67.601 307426.1095645913 7502005.869727269 +21362 4326 307426.1095645913 7502005.869727269 -172.65 67.60099964352025 +7683 21363 -171.035 65.705 248392.49913817327 7289944.93277124 +21363 4326 248392.49913817327 7289944.93277124 -171.035 65.70499962052847 +7683 21363 -172.3535 64.4855 184926.23381718012 7154671.24916256 +21363 4326 184926.23381718012 7154671.24916256 -172.3535 64.48549960660942 +7683 21363 -169.7165 66.9245 306156.4313620434 7426512.03632176 +21363 4326 306156.4313620434 7426512.03632176 -169.7165 66.92449963513283 +7683 21364 -169.095 65.78 199855.9226753026 7298744.260751339 +21364 4326 199855.9226753026 7298744.260751339 -169.095 65.77999962140714 +7683 21364 -169.2075 65.708 194550.358278979 7290811.584335636 +21364 4326 194550.358278979 7290811.584335636 -169.2075 65.70799962056358 +7683 21364 -168.9825 65.852 205132.94367093872 7306686.635768692 +21364 4326 205132.94367093872 7306686.635768692 -168.9825 65.8519996222531 +4214 21413 75.815 38.245000000000005 13571346.41197371 4235081.811126468 +21413 4326 13571346.41197371 4235081.811126468 75.81439278599206 38.245254574680814 +4214 21413 73.8395 35.7025 13394969.385805406 3953224.0311303884 +21413 4326 13394969.385805406 3953224.0311303884 73.83885746300986 35.702677601499566 +4214 21413 77.79050000000001 40.7875 13735549.41116041 4520803.316619453 +21413 4326 13735549.41116041 4520803.316619453 77.78993016464422 40.787831022944495 +4214 21414 80.99000000000001 38.195 14499123.99156704 4229217.605233472 +21414 4326 14499123.99156704 4229217.605233472 80.9895458225167 38.19527666367531 +4214 21414 78.281 30.0635 14237767.19920143 3330330.2394273262 +21414 4326 14237767.19920143 3330330.2394273262 78.28051431974691 30.063547277694592 +4214 21414 83.699 46.326499999999996 14707840.295428693 5136010.312818566 +21414 4326 14707840.295428693 5136010.312818566 83.69857599908855 46.32700034277016 +4214 21415 87.0 38.25 15500000.0 4235322.701880132 +21415 4326 15500000.0 4235322.701880132 86.99972742593468 38.250296766714335 +4214 21415 84.3 28.413 15235383.968709951 3147235.594520566 +21415 4326 15235383.968709951 3147235.594520566 84.29968303936131 28.413020124491126 +4214 21415 89.7 48.087 15701144.873015929 5331723.107633635 +21415 4326 15701144.873015929 5331723.107633635 89.69977706597375 48.087562531645425 +4214 21416 93.005 37.805 16500440.33000274 4185928.1923137438 +4214 21416 90.3005 28.7195 16236199.339020254 3181224.6823198963 +4214 21416 95.7095 46.8905 16706492.81865827 5198731.185353191 +4214 21417 99.005 32.155 17500471.677082866 3559103.3043840607 +4214 21417 96.3005 22.232499999999998 17221621.797579724 2462072.0344894384 +4214 21417 101.7095 42.0775 17724223.947326213 4663881.725776615 +4214 21418 105.0 32.0 18500000.0 3541915.326928574 +21418 4326 18500000.0 3541915.326928574 105.00030233847336 32.00013890855511 +4214 21418 102.3 22.577 18222256.469222542 2500252.3757774034 +21418 4326 18222256.469222542 2500252.3757774034 102.30020757396014 22.576882825460903 +4214 21418 107.7 41.423 18725720.816481937 4591152.023636594 +21418 4326 18725720.816481937 4591152.023636594 107.7004273907785 41.42338376530477 +4214 21419 111.0 31.61 19500000.0 3498670.0424377522 +21419 4326 19500000.0 3498670.0424377522 111.00046215981715 31.61013728885437 +4214 21419 108.3 19.46 19216414.87990756 2154853.3537214193 +21419 4326 19216414.87990756 2154853.3537214193 108.30037371069857 19.45982022128708 +4214 21419 113.7 43.76 19717434.984652735 4850786.28566631 +21419 4326 19717434.984652735 4850786.28566631 113.70058933857247 43.76043922966627 +4214 21420 117.0 36.83 20500000.0 4077717.2817049567 +21420 4326 20500000.0 4077717.2817049567 117.00065024198202 36.83031085950935 +4214 21420 114.3 23.609 20224372.12812555 2614632.0718334657 +21420 4326 20224372.12812555 2614632.0718334657 114.30054637366385 23.609073808656454 +4214 21420 119.7 50.051 20693364.40194895 5550111.192110945 +21420 4326 20693364.40194895 5550111.192110945 119.70082516397359 50.05151958801433 +4214 21421 123.0 39.95 21500000.0 4424055.564131719 +21421 4326 21500000.0 4424055.564131719 123.00079911766122 39.95033929120921 +4214 21421 120.3 27.701 21233632.659300923 3068280.8947124053 +21421 4326 21233632.659300923 3068280.8947124053 120.30067238928194 27.70113459965004 +4214 21421 125.7 52.199 21684593.40577218 5789025.475825784 +21421 4326 21684593.40577218 5789025.475825784 125.7010130865245 52.19951640729513 +4214 21422 129.0 46.84 22500000.0 5189551.344625954 +4214 21422 126.3 41.485 22274494.19549935 4598038.985919056 +4214 21422 131.7 52.195 22684609.985918805 5788580.506558918 +4214 21423 133.385 46.71 23376505.435415108 5176366.500450324 +4214 21423 132.1385 45.189 23275119.506957907 5010022.442977971 +4214 21423 134.63150000000002 48.231 23472623.10988836 5344272.808180529 +4214 21453 75.815 38.245000000000005 571346.4119737085 4235081.811126468 +21453 4326 571346.4119737085 4235081.811126468 75.81439278599206 38.245254574680814 +4214 21453 73.8395 35.7025 394969.38580540614 3953224.0311303884 +21453 4326 394969.38580540614 3953224.0311303884 73.83885746300986 35.702677601499566 +4214 21453 77.79050000000001 40.7875 735549.411160409 4520803.316619453 +21453 4326 735549.411160409 4520803.316619453 77.78993016464422 40.787831022944495 +4214 21454 80.99000000000001 38.195 499123.9915670393 4229217.605233472 +21454 4326 499123.9915670393 4229217.605233472 80.98954582251669 38.19527666367531 +4214 21454 78.281 30.0635 237767.19920142955 3330330.2394273262 +21454 4326 237767.19920142955 3330330.2394273262 78.28051431974691 30.063547277694592 +4214 21454 83.699 46.326499999999996 707840.2954286941 5136010.312818566 +21454 4326 707840.2954286941 5136010.312818566 83.69857599908856 46.32700034277016 +4214 21455 87.0 38.25 500000.0 4235322.701880132 +21455 4326 500000.0 4235322.701880132 86.99972742593468 38.250296766714335 +4214 21455 84.3 28.413 235383.96870995132 3147235.594520566 +21455 4326 235383.96870995132 3147235.594520566 84.29968303936131 28.413020124491126 +4214 21455 89.7 48.087 701144.873015929 5331723.107633635 +21455 4326 701144.873015929 5331723.107633635 89.69977706597375 48.087562531645425 +4214 21456 93.005 37.805 500440.3300027402 4185928.1923137438 +4214 21456 90.3005 28.7195 236199.33902025357 3181224.6823198963 +4214 21456 95.7095 46.8905 706492.8186582705 5198731.185353191 +4214 21457 99.005 32.155 500471.67708286527 3559103.3043840607 +4214 21457 96.3005 22.232499999999998 221621.79757972556 2462072.0344894384 +4214 21457 101.7095 42.0775 724223.947326214 4663881.725776615 +4214 21458 105.0 32.0 500000.0 3541915.326928574 +21458 4326 500000.0 3541915.326928574 105.00030233847336 32.00013890855511 +4214 21458 102.3 22.577 222256.46922254318 2500252.3757774034 +21458 4326 222256.46922254318 2500252.3757774034 102.30020757396015 22.5768828254609 +4214 21458 107.7 41.423 725720.8164819366 4591152.023636594 +21458 4326 725720.8164819366 4591152.023636594 107.7004273907785 41.42338376530477 +4214 21459 111.0 31.61 500000.0 3498670.0424377522 +21459 4326 500000.0 3498670.0424377522 111.00046215981715 31.61013728885437 +4214 21459 108.3 19.46 216414.87990756042 2154853.3537214193 +21459 4326 216414.87990756042 2154853.3537214193 108.30037371069858 19.459820221287078 +4214 21459 113.7 43.76 717434.9846527361 4850786.28566631 +21459 4326 717434.9846527361 4850786.28566631 113.70058933857248 43.76043922966627 +4214 21460 117.0 36.83 500000.0 4077717.2817049567 +21460 4326 500000.0 4077717.2817049567 117.00065024198202 36.83031085950935 +4214 21460 114.3 23.609 224372.12812554726 2614632.0718334657 +21460 4326 224372.12812554726 2614632.0718334657 114.30054637366383 23.60907380865645 +4214 21460 119.7 50.051 693364.401948951 5550111.192110945 +21460 4326 693364.401948951 5550111.192110945 119.70082516397359 50.05151958801433 +4214 21461 123.0 39.95 500000.0 4424055.564131719 +21461 4326 500000.0 4424055.564131719 123.00079911766122 39.95033929120921 +4214 21461 120.3 27.701 233632.65930092405 3068280.8947124053 +21461 4326 233632.65930092405 3068280.8947124053 120.30067238928194 27.70113459965004 +4214 21461 125.7 52.199 684593.4057721786 5789025.475825784 +21461 4326 684593.4057721786 5789025.475825784 125.7010130865245 52.19951640729513 +4214 21462 129.0 46.84 500000.0 5189551.344625954 +4214 21462 126.3 41.485 274494.19549934776 4598038.985919056 +4214 21462 131.7 52.195 684609.9859188057 5788580.506558918 +4214 21463 133.385 46.71 376505.4354151068 5176366.500450324 +4214 21463 132.1385 45.189 275119.5069579055 5010022.442977971 +4214 21463 134.63150000000002 48.231 472623.10988835886 5344272.808180529 +4809 21500 0.08202499999999979 50.504999999999995 155818.89775084096 132543.2354771383 +4809 21500 -1.6729750000000005 49.6005 29061.637079220367 33293.99170823116 +4809 21500 1.8370249999999997 51.4095 277817.0321575145 234745.67564766016 +4801 21780 0.78541666666667 46.81 59938.986167134695 -15529.033358177854 +4801 21780 -1.2620833333333294 45.910000000000004 -97923.51214501043 -115078.67329701554 +4801 21780 2.832916666666671 47.71 212536.50289591542 88060.48482613542 +4149 21781 8.225 46.81 659938.9861671347 184470.96664182213 +21781 4326 659938.9861671347 184470.96664182213 8.223931247363716 46.80870115234433 +4149 21781 6.1775 45.910000000000004 502076.4878549896 84921.32670298446 +21781 4326 502076.4878549896 84921.32670298446 6.176757645760157 45.908792469374355 +4149 21781 10.2725 47.71 812536.5028959154 288060.48482613545 +21781 4326 812536.5028959154 288060.48482613545 10.271094893135745 47.70861591021921 +4149 21782 9.555 47.165 160356.38913357607 25795.304674388073 +21782 4326 160356.38913357607 25795.304674388073 9.55371946701376 47.16367244564321 +4149 21782 9.4785 47.061499999999995 154857.8714233732 14140.393282900644 +21782 4326 154857.8714233732 14140.393282900644 9.477233679587432 47.06018393149724 +4149 21782 9.6315 47.2685 165832.30750143222 37455.55997581765 +21782 4326 165832.30750143222 37455.55997581765 9.63020519719794 47.26716096939607 +4218 21818 -74.685 10.79 534437.4733153105 1192791.077878894 +21818 4326 534437.4733153105 1192791.077878894 -74.68155838134675 10.787232057947877 +4218 21818 -77.1015 8.189 268464.9917667084 905807.3091451974 +21818 4326 268464.9917667084 905807.3091451974 -77.09816842663597 8.18621516980631 +4218 21818 -72.2685 13.390999999999998 795849.614571172 1482026.9655513992 +21818 4326 795849.614571172 1482026.9655513992 -72.26494548875411 13.388243169187401 +4218 21896 -77.34 5.120000000000001 971272.0551675053 1057614.571636168 +21896 4326 971272.0551675053 1057614.571636168 -77.33669762995255 5.1171744673942365 +4218 21896 -78.92399999999999 0.5390000000000001 794794.2189961202 551078.4127435441 +21896 4326 794794.2189961202 551078.4127435441 -78.92076877386937 0.5361291078466718 +4218 21896 -75.756 9.701 1145411.939024312 1564525.9978169953 +21896 4326 1145411.939024312 1564525.9978169953 -75.75260683316031 9.698226281923718 +4218 21897 -74.08500000000001 4.655 999546.916930723 1006187.4124821065 +21897 4326 999546.916930723 1006187.4124821065 -74.08158788664771 4.652153325801047 +4218 21897 -75.43950000000001 -1.7934999999999999 848816.6012601642 293073.27721595694 +21897 4326 848816.6012601642 293073.27721595694 -75.43614258139897 -1.7963843464341094 +4218 21897 -72.73049999999999 11.1035 1147550.6295821194 1719710.7466028826 +21897 4326 1147550.6295821194 1719710.7466028826 -72.72699055291356 11.100714477033682 +4218 21898 -71.08500000000001 4.145 999546.6085773336 949790.5676289925 +21898 4326 999546.6085773336 949790.5676289925 -71.08149677012194 4.142135978342425 +4218 21898 -72.43950000000001 -3.3925000000000005 849006.3608439147 116209.48005387699 +21898 4326 849006.3608439147 116209.48005387699 -72.43604075678037 -3.39537895474061 +4218 21898 -69.73049999999999 11.6825 1147252.3006665872 1783775.6696441865 +21898 4326 1147252.3006665872 1783775.6696441865 -69.72689267731857 11.679685583767428 +4218 21899 -68.23 2.0299999999999994 983413.7931033229 715917.087132789 +21899 4326 983413.7931033229 715917.087132789 -68.22643889511085 2.0270867425617256 +4218 21899 -69.45400000000001 -1.822 847205.1134575529 289919.73068169015 +21899 4326 847205.1134575529 289919.73068169015 -69.45049953502082 -1.8249011490236535 +4218 21899 -67.006 5.881999999999998 1119044.9473351627 1141989.559156711 +21899 4326 1119044.9473351627 1141989.559156711 -67.00236482546711 5.87908358369478 +4220 22032 10.1 -11.645000000000001 619908.5914125791 8712584.642719116 +22032 4326 619908.5914125791 8712584.642719116 10.096943533912668 -11.646002249042134 +4220 22032 8.389999999999999 -16.698500000000003 434964.23129865614 8153860.391429326 +22032 4326 434964.23129865614 8153860.391429326 8.386845413822199 -16.69902783819087 +4220 22032 11.809999999999999 -6.591500000000002 810757.2863644704 9270600.454546241 +22032 4326 810757.2863644704 9270600.454546241 11.80701755125069 -6.592981987463557 +4220 22033 15.0 -11.63 500000.00000000175 8714475.48439153 +22033 4326 500000.00000000175 8714475.48439153 14.997041853039606 -11.631054764915273 +4220 22033 12.3 -16.859 212293.65586765343 8134239.287568692 +22033 4326 212293.65586765343 8134239.287568692 12.296914777379278 -16.85957337619098 +4220 22033 17.7 -6.401000000000002 798695.8683740315 9291747.441004511 +22033 4326 798695.8683740315 9291747.441004511 17.69714660213951 -6.4025330761004735 +4220 22091 11.25 -6.3100000000000005 472348.08240322274 9302584.437827548 +22091 4326 472348.08240322274 9302584.437827548 11.247008655510392 -6.31150479066257 +4220 22091 10.872 -6.562 430571.4590193564 9274693.011795925 +22091 4326 430571.4590193564 9274693.011795925 10.869000223893485 -6.563479115283958 +4220 22091 11.628 -6.058 514164.4524752402 9330443.698577026 +22091 4326 514164.4524752402 9330443.698577026 11.625017151664267 -6.059530298567948 +4220 22092 11.03 -11.635000000000002 394259.98250299285 8713742.128033448 +22092 4326 394259.98250299285 8713742.128033448 11.026960574805267 -11.636012999120107 +4220 22092 8.482999999999999 -16.6975 124837.40124188043 8150759.205527688 +22092 4326 124837.40124188043 8150759.205527688 8.479846972313164 -16.698029342929217 +4220 22092 13.576999999999998 -6.5725 674359.7470032582 9273301.178858966 +22092 4326 674359.7470032582 9273301.178858966 13.574052805037365 -6.573994212026279 +4190 22171 -72.045 -44.08 1496395.574515988 5119253.333111162 +22171 4326 1496395.574515988 5119253.333111162 -72.045 -44.08000000000001 +4190 22171 -73.4355 -51.208 1399684.6963946112 4325760.386645036 +22171 4326 1399684.6963946112 4325760.386645036 -73.4355 -51.208 +4190 22171 -70.6545 -36.952 1619843.5292992345 5909935.657945214 +22171 4326 1619843.5292992345 5909935.657945214 -70.6545 -36.95199999999999 +4190 22172 -68.995 -39.489999999999995 2500430.1287168907 5629061.85326857 +22172 4326 2500430.1287168907 5629061.85326857 -68.995 -39.489999999999995 +4190 22172 -70.3495 -53.359 2410153.998429322 4086543.3088969574 +22172 4326 2410153.998429322 4086543.3088969574 -70.3495 -53.359 +4190 22172 -67.64049999999999 -25.621 2636551.8828606107 7166418.092793973 +22172 4326 2636551.8828606107 7166418.092793973 -67.6405 -25.620999999999995 +4190 22173 -65.995 -38.445 3500436.495299117 5745072.39014382 +22173 4326 3500436.495299117 5745072.39014382 -65.995 -38.44500000000001 +4190 22173 -67.3495 -53.4435 3410331.845851773 4077139.734192263 +22173 4326 3410331.845851773 4077139.734192263 -67.3495 -53.44349999999999 +4190 22173 -64.64049999999999 -23.446500000000004 3638925.707792664 7407323.661400374 +22173 4326 3638925.707792664 7407323.661400374 -64.64049999999999 -23.44650000000002 +4190 22174 -63.0 -38.449999999999996 4500000.0 5744517.377178358 +22174 4326 4500000.0 5744517.377178358 -63.0 -38.449999999999996 +4190 22174 -64.35 -53.263999999999996 4409920.926453283 4097114.5940571013 +22174 4326 4409920.926453283 4097114.5940571013 -64.35 -53.263999999999996 +4190 22174 -61.65 -23.635999999999996 4637757.205516418 7386341.3662016215 +22174 4326 4637757.205516418 7386341.3662016215 -61.64999999999999 -23.636 +4190 22175 -60.004999999999995 -31.215000000000003 5499523.553412865 6547154.062466859 +22175 4326 5499523.553412865 6547154.062466859 -60.004999999999995 -31.214999999999993 +4190 22175 -61.3595 -38.2755 5381037.242414446 5763013.112585991 +22175 4326 5381037.242414446 5763013.112585991 -61.3595 -38.2755 +4190 22175 -58.6505 -24.154500000000002 5637158.082468768 7328903.714565966 +22175 4326 5637158.082468768 7328903.714565966 -58.65050000000001 -24.1545 +4190 22176 -56.995000000000005 -31.715000000000003 6500473.921296755 6491715.315342961 +22176 4326 6500473.921296755 6491715.315342961 -56.995000000000005 -31.715000000000007 +4190 22176 -58.3495 -37.9025 6381310.617094355 5804431.016410681 +22176 4326 6381310.617094355 5804431.016410681 -58.349500000000006 -37.9025 +4190 22176 -55.6405 -25.5275 6636658.0176086575 7176778.00605001 +22176 4326 6636658.0176086575 7176778.00605001 -55.6405 -25.527500000000003 +4190 22177 -54.575 -26.799999999999997 7442827.245918348 7036366.472786285 +22177 4326 7442827.245918348 7036366.472786285 -54.574999999999996 -26.799999999999983 +4190 22177 -55.4075 -27.979 7361520.987406777 6905053.107511732 +22177 4326 7361520.987406777 6905053.107511732 -55.4075 -27.979 +4190 22177 -53.7425 -25.621 7525862.526142078 7167093.579018067 +22177 4326 7525862.526142078 7167093.579018067 -53.7425 -25.621 +4694 22181 -72.045 -44.08 1496395.5745160165 5119253.333074568 +22181 4326 1496395.5745160165 5119253.333074568 -72.045 -44.080000000000005 +4694 22181 -73.4355 -51.208 1399684.6963956135 4325760.386603653 +22181 4326 1399684.6963956135 4325760.386603653 -73.4355 -51.208000000000006 +4694 22181 -70.6545 -36.952 1619843.5292985232 5909935.657918259 +22181 4326 1619843.5292985232 5909935.657918259 -70.6545 -36.95199999999999 +4694 22182 -68.995 -39.489999999999995 2500430.128716888 5629061.85323763 +22182 4326 2500430.128716888 5629061.85323763 -68.995 -39.49 +4694 22182 -70.3495 -53.359 2410153.9984302735 4086543.308855072 +22182 4326 2410153.9984302735 4086543.308855072 -70.3495 -53.35900000000001 +4694 22182 -67.64049999999999 -25.621 2636551.882860192 7166418.092791674 +22182 4326 2636551.882860192 7166418.092791674 -67.6405 -25.621000000000002 +4694 22183 -65.995 -38.445 3500436.4952991144 5745072.390114447 +22183 4326 3500436.4952991144 5745072.390114447 -65.995 -38.445 +4694 22183 -67.3495 -53.4435 3410331.8458527247 4077139.734150365 +22183 4326 3410331.8458527247 4077139.734150365 -67.3495 -53.443500000000014 +4694 22183 -64.64049999999999 -23.446500000000004 3638925.707792303 7407323.661403973 +22183 4326 3638925.707792303 7407323.661403973 -64.64049999999999 -23.44650000000002 +4694 22184 -63.0 -38.449999999999996 4500000.0 5744517.377148977 +22184 4326 4500000.0 5744517.377148977 -63.0 -38.44999999999999 +4694 22184 -64.35 -53.263999999999996 4409920.926454235 4097114.5940152276 +22184 4326 4409920.926454235 4097114.5940152276 -64.35 -53.263999999999996 +4694 22184 -61.65 -23.635999999999996 4637757.205516055 7386341.366204695 +22184 4326 4637757.205516055 7386341.366204695 -61.64999999999999 -23.63599999999999 +4694 22185 -60.004999999999995 -31.215000000000003 5499523.553412868 6547154.062451034 +22185 4326 5499523.553412868 6547154.062451034 -60.004999999999995 -31.215000000000003 +4694 22185 -61.3595 -38.2755 5381037.242415195 5763013.112556886 +22185 4326 5381037.242415195 5763013.112556886 -61.3595 -38.275500000000015 +4694 22185 -58.6505 -24.154500000000002 5637158.082468391 7328903.714567608 +22185 4326 5637158.082468391 7328903.714567608 -58.65050000000001 -24.154500000000002 +4694 22186 -56.995000000000005 -31.715000000000003 6500473.921296752 6491715.31532605 +22186 4326 6500473.921296752 6491715.31532605 -56.99500000000001 -31.71500000000001 +4694 22186 -58.3495 -37.9025 6381310.61709509 5804431.016382167 +22186 4326 6381310.61709509 5804431.016382167 -58.349500000000006 -37.902499999999996 +4694 22186 -55.6405 -25.5275 6636658.017608241 7176778.006047958 +22186 4326 6636658.017608241 7176778.006047958 -55.6405 -25.527500000000003 +4694 22187 -54.575 -26.799999999999997 7442827.245918538 7036366.472780928 +22187 4326 7442827.245918538 7036366.472780928 -54.575 -26.800000000000008 +4694 22187 -55.4075 -27.979 7361520.987407277 6905053.10750343 +22187 4326 7361520.987407277 6905053.10750343 -55.4075 -27.97900000000001 +4694 22187 -53.7425 -25.621 7525862.526141999 7167093.5790157635 +22187 4326 7525862.526141999 7167093.5790157635 -53.7425 -25.621000000000013 +4221 22191 -72.045 -44.08 1496395.407915162 5119486.396432165 +22191 4326 1496395.407915162 5119486.396432165 -72.04623431677076 -44.079691997102124 +4221 22191 -73.4355 -51.208 1399679.883032482 4325966.357727663 +22191 4326 1399679.883032482 4325966.357727663 -73.43697509454091 -51.207899408903806 +4221 22191 -70.6545 -36.952 1619848.859767104 5910191.513025836 +22191 4326 1619848.859767104 5910191.513025836 -70.6555620120514 -36.95152228533974 +4221 22192 -68.995 -38.254999999999995 2500437.656864525 5766415.0107937725 +22192 4326 2500437.656864525 5766415.0107937725 -68.99602156929394 -38.25456762562173 +4221 22192 -70.3495 -51.0125 2405290.9664658704 4347829.201873061 +22192 4326 2405290.9664658704 4347829.201873061 -70.3508343916592 -51.012429792667724 +4221 22192 -67.64049999999999 -25.4975 2636697.732822213 7180386.281133082 +22192 4326 2636697.732822213 7180386.281133082 -67.64134677986667 -25.49683465589523 +4221 22193 -65.995 -35.415 3500454.144400698 6081591.173523927 +22193 4326 3500454.144400698 6081591.173523927 -65.99587940399009 -35.414525284254516 +4221 22193 -67.3495 -47.686499999999995 3398680.561165467 4717732.834960873 +22193 4326 3398680.561165467 4717732.834960873 -67.35062165455376 -47.68635048568333 +4221 22193 -64.64049999999999 -23.143500000000003 3639246.4924895293 7441175.8715589335 +22193 4326 3639246.4924895293 7441175.8715589335 -64.64123710154476 -23.142818890336585 +4221 22194 -62.995000000000005 -32.565 4500469.5659246305 6397727.349266105 +22194 4326 4500469.5659246305 6397727.349266105 -62.99574656475322 -32.56448388777891 +4221 22194 -64.3495 -42.0825 4388331.256878041 5340523.668661421 +22194 4326 4388331.256878041 5340523.668661421 -64.35040088576068 -42.082206882828785 +4221 22194 -61.6405 -23.0475 4639345.486325692 7451809.696366782 +22194 4326 4639345.486325692 7451809.696366782 -61.64114044256377 -23.04683073389922 +4221 22195 -60.004999999999995 -31.215000000000003 5499523.532849754 6547425.380961638 +22195 4326 5499523.532849754 6547425.380961638 -60.00563163441158 -31.214472816137473 +4221 22195 -61.3595 -38.2755 5381031.913356184 5763265.041226612 +22195 4326 5381031.913356184 5763265.041226612 -61.36023945526212 -38.27512551832019 +4221 22195 -58.6505 -24.154500000000002 5637163.805563852 7329190.713152781 +22195 4326 5637163.805563852 7329190.713152781 -58.651047456731085 -24.15385751108298 +4221 22196 -56.995000000000005 -31.715000000000003 6500473.941803159 6491985.388973889 +22196 4326 6500473.941803159 6491985.388973889 -56.995527882754345 -31.714496585380125 +4221 22196 -58.3495 -37.9025 6381305.310949329 5804684.066239107 +22196 4326 6381305.310949329 5804684.066239107 -58.35012107499154 -37.90213339836738 +4221 22196 -55.6405 -25.5275 6636663.755231283 7177062.208891058 +22196 4326 6636663.755231283 7177062.208891058 -55.64095186704731 -25.526888454200833 +4221 22197 -54.575 -26.799999999999997 7442824.83133691 7036648.001995057 +22197 4326 7442824.83133691 7036648.001995057 -54.57542010077051 -26.799412625985052 +4221 22197 -55.4075 -27.979 7361515.1057903655 6905332.012910287 +22197 4326 7361515.1057903655 6905332.012910287 -55.407953571070585 -27.978430564349217 +4221 22197 -53.7425 -25.621 7525863.612408617 7167377.61552737 +22197 4326 7525863.612408617 7167377.61552737 -53.74288740365072 -25.62039539800667 +8237 22207 -139.505 62.29 577553.7482862907 6907382.944374962 +8237 22207 -140.8595 53.074 509412.8752931231 5880511.6950232545 +8237 22207 -138.1505 71.506 600849.7762664382 7936211.238253396 +8237 22208 -135.0 63.74 500000.0 7068042.170895341 +8237 22208 -137.7 49.628 305021.49189101974 5500772.589930895 +8237 22208 -132.3 77.852 563406.2209931078 8643313.609433059 +8237 22209 -129.0 63.72500000000001 500000.0 7066370.74190974 +8237 22209 -131.7 48.240500000000004 299538.3307894394 5346555.905977527 +8237 22209 -126.3 79.2095 556411.4973791244 8794662.018231109 +8237 22210 -123.0 64.965 500000.0 7204553.9102763105 +8237 22210 -125.7 49.813500000000005 305763.35086729913 5521392.586308251 +8237 22210 -120.3 80.1165 551719.885517777 8895789.811727198 +8237 22211 -117.0 66.245 500000.0 7347218.876508807 +8237 22211 -119.7 50.7155 309399.7445906481 5621665.37102995 +8237 22211 -114.3 81.77449999999999 543110.4436380728 9080662.632243542 +8237 22212 -111.0 66.495 500000.0 7375085.928094508 +8237 22212 -113.7 50.740500000000004 309501.21335740504 5624444.723641862 +8237 22212 -108.3 82.2495 540636.8512974724 9133629.104307733 +8237 22213 -105.0 66.495 499999.9999999994 7375085.928094508 +8237 22213 -107.7 50.740500000000004 309501.2133574041 5624444.723641862 +8237 22213 -102.3 82.2495 540636.8512974723 9133629.104307733 +8237 22214 -99.0 66.495 500000.0 7375085.928094508 +8237 22214 -101.7 50.740500000000004 309501.2133574041 5624444.723641862 +8237 22214 -96.3 82.2495 540636.8512974723 9133629.104307733 +8237 22215 -93.0 66.015 499999.9999999994 7321581.988534806 +8237 22215 -95.7 49.8285 305823.42915909307 5523059.994057171 +8237 22215 -90.3 82.2015 540886.9459985999 9128276.65995035 +8237 22216 -87.0 65.055 500000.0000000006 7214584.245044757 +8237 22216 -89.7 48.0045 298617.3852891464 5320328.070626806 +8237 22216 -84.3 82.1055 541387.0478100256 9117571.799824689 +8237 22217 -81.0 62.835 500000.0 6967205.614257044 +8237 22217 -83.7 43.786500000000004 282751.64517490444 4851703.725921316 +8237 22217 -78.3 81.8835 542543.079010169 9092816.959001321 +8237 22218 -75.0 63.815 500000.0 7076399.368382286 +8237 22218 -77.7 45.648500000000006 289614.5290814074 5058540.583399849 +8237 22218 -72.3 81.9815 542032.8378381082 9103744.745874293 +8237 22219 -69.0 62.4 500000.0 6918741.843632851 +8237 22219 -71.7 42.959999999999994 279778.81803513714 4759910.374057942 +8237 22219 -66.3 81.84 542769.5232055377 9087966.372905465 +8237 22220 -62.995000000000005 61.105000000000004 500269.5360667032 6774482.828320475 +8237 22220 -65.6995 40.4995 271245.84974719933 4486700.41001962 +8237 22220 -60.2905 81.7105 543596.2492053718 9073533.311533127 +8237 22221 -57.0 61.28 500000.0000000007 6793975.671579958 +8237 22221 -59.7 40.832 272337.899245773 4523616.464762851 +8237 22221 -54.3 81.72800000000001 543352.4364263249 9075477.545223918 +8237 22222 -50.995000000000005 48.575 500368.8140584385 5380212.31033272 +8237 22222 -53.6995 40.4075 270933.38613038993 4476486.695823391 +8237 22222 -48.2905 56.74249999999999 665706.9021386893 6292000.1815662505 +10299 22229 -7.335 27.79 664036.4137597654 3075051.383107593 +22229 4326 664036.4137597654 3075051.383107593 -7.335 27.789999999999992 +10299 22229 -8.5365 25.936 546411.4191859029 2868678.380544016 +22229 4326 546411.4191859029 2868678.380544016 -8.536500000000002 25.935999999999996 +10299 22229 -6.1335 29.644000000000002 777508.2805300051 3282774.0567531185 +22229 4326 777508.2805300051 3282774.0567531185 -6.133500000000001 29.644000000000002 +10299 22230 -3.0 29.415 499999.9999999991 3253965.4841686296 +22230 4326 499999.9999999991 3253965.4841686296 -3.0000000000000004 29.415000000000003 +10299 22230 -5.7 22.5795 222377.26262957015 2499484.6804941897 +22230 4326 222377.26262957015 2499484.6804941897 -5.7 22.579499999999992 +10299 22230 -0.3000000000000007 36.250499999999995 742599.8990649292 4015114.639387604 +22230 4326 742599.8990649292 4015114.639387604 -0.30000000000000043 36.250499999999995 +10299 22231 3.005 28.87 500487.60776192474 3193582.879621779 +22231 4326 500487.60776192474 3193582.879621779 3.0049999999999994 28.870000000000005 +10299 22231 0.3005 19.96 217466.2091474075 2209328.1516865375 +22231 4326 217466.2091474075 2209328.1516865375 0.3004999999999995 19.959999999999997 +10299 22231 5.709499999999999 37.78 738618.1550594204 4184864.002006159 +22231 4326 738618.1550594204 4184864.002006159 5.709499999999999 37.779999999999994 +10299 22232 8.995000000000001 29.2 499513.94076464575 3230144.2428050614 +22232 4326 499513.94076464575 3230144.2428050614 8.995000000000001 29.19999999999999 +10299 22232 6.2995 20.560000000000002 218446.91035916045 2275784.2782925894 +22232 4326 218446.91035916045 2275784.2782925894 6.2995 20.559999999999995 +10299 22232 11.6905 37.839999999999996 736752.7370381257 4191474.3985150633 +22232 4326 736752.7370381257 4191474.3985150633 11.6905 37.839999999999996 +4222 22234 21.994999999999997 -22.435 602394.8537394234 7518892.7666161 +22234 4326 602394.8537394234 7518892.7666161 21.994522032336807 -22.43579002523694 +4222 22234 20.1905 -26.435499999999998 419284.24174178153 7076069.144715962 +22234 4326 419284.24174178153 7076069.144715962 20.18995437176384 -26.436012780462363 +4222 22234 23.7995 -18.4345 795729.7151509826 7959632.8370937165 +22234 4326 795729.7151509826 7959632.8370937165 23.799084115076028 -18.435593390933622 +4222 22235 26.689999999999998 -21.810000000000002 467958.17022644414 7588374.898070128 +22235 4326 467958.17022644414 7588374.898070128 26.68965757424761 -21.810846679353375 +4222 22235 24.269 -25.437 225324.6189235698 7184079.741911454 +22235 4326 225324.6189235698 7184079.741911454 24.268576928846134 -25.437590975210142 +4222 22235 29.110999999999997 -18.183 723290.4191534596 7988458.473290946 +22235 4326 723290.4191534596 7988458.473290946 29.110733513054367 -18.18412238729262 +8237 22239 -63.19499999999999 46.495000000000005 385030.60045041575 716092.6605064794 +8237 22239 -64.3605 45.9595 294533.1514411849 657460.5291834144 +8237 22239 -62.0295 47.0305 473763.0329879282 776057.4583061151 +8237 22240 -66.375 46.315 2509626.993519572 7479444.931300374 +8237 22240 -68.7825 44.7355 2319191.9035818283 7306463.050455528 +8237 22240 -63.9675 47.8945 2689366.068220979 7658098.364020295 +8237 22243 -58.55 51.105000000000004 301298.3943093495 5663201.709471262 +8237 22243 -59.855 50.2905 208250.18692879705 5573480.930550214 +8237 22243 -57.245000000000005 51.9195 391135.14716385386 5754555.478517898 +8237 22244 -61.5 49.584999999999994 304800.0 5494139.17213953 +8237 22244 -62.85 47.402499999999996 202908.98907567892 5252353.359502568 +8237 22244 -60.15 51.7675 397983.54348093993 5737763.050196165 +8237 22245 -64.5 54.185 304800.0 6005907.121713773 +8237 22245 -65.85 48.5735 205188.5073378514 5382540.210521754 +8237 22245 -63.15 59.7965 380581.1159302807 6631509.260756482 +8237 22246 -67.5 53.155 304800.0 5891280.786565086 +8237 22246 -68.85 47.8945 203861.5683617214 5307048.778975857 +8237 22246 -66.15 58.4155 383691.36466289486 6477705.645014932 +8237 22247 -70.5 53.405 304800.0 5919100.887776715 +8237 22247 -71.85 45.8495 199952.14056091162 5079736.298233002 +8237 22247 -69.15 60.960499999999996 377924.8926710408 6761169.233849469 +8237 22248 -73.5 53.760000000000005 304800.0 5958607.4443509625 +8237 22248 -74.85 45.867000000000004 199985.03184363135 5081681.187939683 +8237 22248 -72.15 61.653 376330.0464075751 6838318.156936404 +8237 22249 -76.5 53.995 304800.0 5984760.967545259 +8237 22249 -77.85 46.232499999999995 200674.2349459119 5122302.890344747 +8237 22249 -75.15 61.75749999999999 376088.45487017557 6849960.772714977 +8237 22250 -78.925 54.34 342194.20700556895 6023310.8847556645 +8237 22250 -79.7575 47.041 285232.52857759316 5211316.028615976 +8237 22250 -78.0925 61.639 379410.092910092 6836822.942244605 +8237 22262 -111.24000000000001 54.495000000000005 -15549.31506636425 6040436.693252089 +8237 22262 -112.374 49.5405 -99434.22623297655 5490097.576352304 +8237 22262 -110.10600000000001 59.4495 50705.564205906085 6592424.346893215 +8237 22263 -114.0 54.495000000000005 0.0 6040410.182018194 +8237 22263 -115.35 49.5405 -97697.43470476051 5490066.157392819 +8237 22263 -112.65 59.4495 76566.88401019845 6592860.533550745 +8237 22264 -117.0 55.385000000000005 0.0 6139477.2256889725 +8237 22264 -118.35 51.231500000000004 -94283.19848592878 5678138.529527494 +8237 22264 -115.65 59.5385 76365.61692193565 6602773.133696755 +8237 22265 -119.25 56.44 46256.40122584206 6257181.704696059 +8237 22265 -119.925 53.236000000000004 5007.291499956597 5900296.996278018 +8237 22265 -118.575 59.644 80355.6458864481 6614612.014429 +4222 22275 14.475 -22.915 53859.8958071834 2535008.0336104366 +4222 22275 14.362499999999999 -23.1265 65299.69010364008 2558475.483840295 +4222 22275 14.5875 -22.7035 42383.69827945956 2511550.627842899 +4222 22277 17.225 -30.565 -21586.09411014236 3382506.8916406613 +22277 4326 -21586.09411014236 3382506.8916406613 17.2243445251919 -30.565254283664036 +4222 22277 16.5275 -32.8465 44234.644270431796 3635549.0788753163 +22277 4326 44234.644270431796 3635549.0788753163 16.5268072787697 -32.84663193179032 +4222 22277 17.9225 -28.2835 -90505.10881520165 3129955.8982975194 +22277 4326 -90505.10881520165 3129955.8982975194 17.921879146100416 -28.283888751311896 +4222 22279 18.994999999999997 -31.630000000000003 474.36861971908496 3500557.402089923 +22279 4326 474.36861971908496 3500557.402089923 18.994390191852958 -31.630206139863898 +4222 22279 18.0905 -34.555 83477.76523398589 3825319.7878735 +22279 4326 83477.76523398589 3825319.7878735 18.089841570126406 -34.55506047433384 +4222 22279 19.8995 -28.705 -87898.60633354832 3176653.2667743242 +22279 4326 -87898.60633354832 3176653.2667743242 19.89893446450939 -28.705372314746327 +4222 22281 21.0 -29.82 -0.0 3299902.036505544 +22281 4326 -0.0 3299902.036505544 20.99946111503327 -29.82031278060056 +4222 22281 20.090999999999998 -34.374 83612.15166922552 3805240.780763062 +22281 4326 83612.15166922552 3805240.780763062 20.090405181817935 -34.37407879751832 +4222 22281 21.909000000000002 -25.266000000000002 -91570.06357375275 2795600.99022971 +22281 4326 -91570.06357375275 2795600.99022971 21.908509053160216 -25.266594894769835 +4222 22283 23.005000000000003 -29.759999999999998 -483.60302343222656 3293251.456815119 +22283 4326 -483.60302343222656 3293251.456815119 23.004521695705293 -29.760323856683303 +4222 22283 22.1005 -33.809999999999995 83288.8624852871 3742671.48683883 +22283 4326 83288.8624852871 3742671.48683883 22.099971897564494 -33.81011353635158 +4222 22283 23.9095 -25.71 -91284.41684428141 2844788.6281806394 +22283 4326 -91284.41684428141 2844788.6281806394 23.909065450556554 -25.710572124834837 +4222 22285 25.005000000000003 -29.485 -484.9178042195572 3262770.3690346573 +22285 4326 -484.9178042195572 3262770.3690346573 25.0045835206986 -29.485346061273596 +4222 22285 24.1005 -33.7825 83315.50147736321 3739621.204424204 +22285 4326 83315.50147736321 3739621.204424204 24.100035178618995 -33.78262284673806 +4222 22285 25.9095 -25.1875 -91679.27266903575 2786905.2348405733 +22285 4326 -91679.27266903575 2786905.2348405733 25.90912578842898 -25.1881117846501 +4222 22287 27.0 -28.375 -0.0 3139750.5511571863 +22287 4326 -0.0 3139750.5511571863 26.999648171056354 -28.37541573674734 +4222 22287 26.1 -33.2845 83841.1575361478 3684386.0954027954 +22287 4326 83841.1575361478 3684386.0954027954 26.09960113455894 -33.284652906209786 +4222 22287 27.9 -23.465500000000002 -91955.86093296066 2596161.5271531083 +22287 4326 -91955.86093296066 2596161.5271531083 27.899688665254196 -23.466232360376097 +4222 22289 29.0 -27.58 -0.0 3051654.318336546 +22289 4326 -0.0 3051654.318336546 28.999711113275758 -27.580467818405833 +4222 22289 28.090999999999998 -32.485 85443.42989470587 3595726.155351387 +22289 4326 85443.42989470587 3595726.155351387 28.0906676445723 -32.485196997107764 +4222 22289 29.909000000000002 -22.674999999999997 -93419.77473992549 2508621.5692689056 +22289 4326 -93419.77473992549 2508621.5692689056 29.90874890970967 -22.675790986361903 +4222 22291 31.005000000000003 -26.799999999999997 -497.1636580369692 2965230.2534558275 +22291 4326 -497.1636580369692 2965230.2534558275 31.004773570099125 -26.80051928557131 +4222 22291 30.0915 -30.922 86840.6757368205 3422416.02364786 +22291 4326 86840.6757368205 3422416.02364786 30.091235762056737 -30.922280940899434 +4222 22291 31.9185 -22.677999999999997 -94394.11445880165 2508959.7988268286 +22291 4326 -94394.11445880165 2508959.7988268286 31.918307659575788 -22.67879355814066 +4222 22293 32.45 -27.87 54165.8834663038 3083910.3861237415 +22293 4326 54165.8834663038 3083910.3861237415 32.449815553004456 -27.870456307952768 +4222 22293 32.0 -28.833000000000002 97600.73981308787 3190918.356736837 +22293 4326 97600.73981308787 3190918.356736837 31.999799988084533 -28.83339929127941 +4222 22293 32.900000000000006 -26.907 9933.929834730512 2977089.199962202 +22293 4326 9933.929834730512 2977089.199962202 32.89983080476748 -26.907515314997912 +8240 22307 -139.505 62.29 577553.7482862907 6907382.944374962 +8240 22307 -140.8595 53.074 509412.8752931231 5880511.6950232545 +8240 22307 -138.1505 71.506 600849.7762664382 7936211.238253396 +8240 22308 -135.0 63.74 500000.0 7068042.170895341 +8240 22308 -137.7 49.628 305021.49189101974 5500772.589930895 +8240 22308 -132.3 77.852 563406.2209931078 8643313.609433059 +8240 22309 -129.0 63.72500000000001 500000.0 7066370.74190974 +8240 22309 -131.7 48.240500000000004 299538.3307894394 5346555.905977527 +8240 22309 -126.3 79.2095 556411.4973791244 8794662.018231109 +8240 22310 -123.0 64.965 500000.0 7204553.9102763105 +8240 22310 -125.7 49.813500000000005 305763.35086729913 5521392.586308251 +8240 22310 -120.3 80.1165 551719.885517777 8895789.811727198 +8240 22311 -117.0 66.245 500000.0 7347218.876508807 +8240 22311 -119.7 50.7155 309399.7445906481 5621665.37102995 +8240 22311 -114.3 81.77449999999999 543110.4436380728 9080662.632243542 +8240 22312 -111.0 66.495 500000.0 7375085.928094508 +8240 22312 -113.7 50.740500000000004 309501.21335740504 5624444.723641862 +8240 22312 -108.3 82.2495 540636.8512974724 9133629.104307733 +8240 22313 -105.0 66.495 499999.9999999994 7375085.928094508 +8240 22313 -107.7 50.740500000000004 309501.2133574041 5624444.723641862 +8240 22313 -102.3 82.2495 540636.8512974723 9133629.104307733 +8240 22314 -99.0 66.495 500000.0 7375085.928094508 +8240 22314 -101.7 50.740500000000004 309501.2133574041 5624444.723641862 +8240 22314 -96.3 82.2495 540636.8512974723 9133629.104307733 +8240 22315 -93.0 66.015 499999.9999999994 7321581.988534806 +8240 22315 -95.7 49.8285 305823.42915909307 5523059.994057171 +8240 22315 -90.3 82.2015 540886.9459985999 9128276.65995035 +8240 22316 -87.0 65.055 500000.0000000006 7214584.245044757 +8240 22316 -89.7 48.0045 298617.3852891464 5320328.070626806 +8240 22316 -84.3 82.1055 541387.0478100256 9117571.799824689 +8240 22317 -81.0 62.835 500000.0 6967205.614257044 +8240 22317 -83.7 43.786500000000004 282751.64517490444 4851703.725921316 +8240 22317 -78.3 81.8835 542543.079010169 9092816.959001321 +8240 22318 -75.0 63.815 500000.0 7076399.368382286 +8240 22318 -77.7 45.648500000000006 289614.5290814074 5058540.583399849 +8240 22318 -72.3 81.9815 542032.8378381082 9103744.745874293 +8240 22319 -69.0 62.4 500000.0 6918741.843632851 +8240 22319 -71.7 42.959999999999994 279778.81803513714 4759910.374057942 +8240 22319 -66.3 81.84 542769.5232055377 9087966.372905465 +8240 22320 -62.995000000000005 61.105000000000004 500269.5360667032 6774482.828320475 +8240 22320 -65.6995 40.4995 271245.84974719933 4486700.41001962 +8240 22320 -60.2905 81.7105 543596.2492053718 9073533.311533127 +8240 22321 -57.0 61.28 500000.0000000007 6793975.671579958 +8240 22321 -59.7 40.832 272337.899245773 4523616.464762851 +8240 22321 -54.3 81.72800000000001 543352.4364263249 9075477.545223918 +8240 22322 -50.995000000000005 48.575 500368.8140584385 5380212.31033272 +8240 22322 -53.6995 40.4075 270933.38613038993 4476486.695823391 +8240 22322 -48.2905 56.74249999999999 665706.9021386893 6292000.1815662505 +4223 22332 10.74 35.815 657200.9091685837 3964539.2741712346 +22332 4326 657200.9091685837 3964539.2741712346 10.740596131266924 35.816514685681724 +4223 22332 8.103 33.4795 416657.98896132986 3704529.1277353046 +22332 4326 416657.98896132986 3704529.1277353046 8.103453040238058 33.48113106385304 +4223 22332 13.376999999999999 38.150499999999994 883606.0430258788 4231278.829915132 +22332 4326 883606.0430258788 4231278.829915132 13.37774758258356 38.15190635700744 +8240 22337 -64.64 44.715 15488907.966509582 4952786.822229502 +8240 22337 -66.116 43.540499999999994 15369402.828979088 4823554.948998742 +8240 22337 -63.164 45.889500000000005 15603686.139426796 5084163.479432224 +8240 22338 -61.364999999999995 45.86 14510482.839968191 5080025.743433987 +8240 22338 -62.8365 44.762 14394196.346236415 4958868.739724381 +8240 22338 -59.893499999999996 46.958 14622266.374692379 5203310.505488707 +8240 22348 -74.675 45.315 212673.28218495188 5020121.539722489 +8240 22348 -74.9675 45.01349999999999 189130.9981501979 4986993.812086513 +8240 22348 -74.3825 45.6165 235974.83456550929 5053334.307750545 +8240 22349 -76.5 44.94 304800.0 4977778.678738295 +8240 22349 -77.85 43.761 196098.35460517107 4847668.195731273 +8240 22349 -75.15 46.119 409140.2437243696 5109688.226389748 +8240 22350 -79.495 44.795 305195.5978716272 4961666.569993048 +8240 22350 -80.8495 42.5135 193906.48108692348 4709087.304122142 +8240 22350 -78.14049999999999 47.076499999999996 408039.4404268192 5216127.032725465 +8240 22351 -82.3 43.835 320884.01985230704 4855023.008696913 +8240 22351 -83.47 41.8865 224299.73349253612 4639021.263440096 +8240 22351 -81.13 45.783500000000004 411326.9483581694 5072427.79761347 +8240 22352 -81.0 50.605000000000004 304800.0 5607583.33382646 +8240 22352 -82.35 46.4605 201106.3244268999 5147644.078890337 +8240 22352 -79.65 54.7495 391718.0987432869 6069573.696578401 +8240 22353 -84.0 50.795 304800.0 5628717.3001815565 +8240 22353 -85.35 46.4795 201142.4066109733 5149755.887885232 +8240 22353 -82.65 55.1105 390943.12263336655 6109753.223535529 +8240 22354 -87.0 51.935 304800.0 5755535.600946693 +8240 22354 -88.35 47.6465 203380.4647399176 5279478.173564699 +8240 22354 -85.65 56.2235 388532.1709837478 6233645.02786236 +8240 22355 -90.0 52.435 304800.0 5811165.4693391705 +8240 22355 -91.35 48.4165 204880.4294295852 5365084.149450656 +8240 22355 -88.65 56.4535 388029.9376740323 6259249.762159773 +8240 22356 -93.0 51.63 304800.0 5721603.713476377 +8240 22356 -94.35 48.417 204881.4093704535 5365139.741284148 +8240 22356 -91.65 54.843 391517.711160886 6079980.087531899 +8240 22357 -94.83 50.965 386983.664340777 5648279.054044446 +8240 22357 -95.127 48.9175 368777.6797239066 5420278.251220682 +8240 22357 -94.533 53.0125 403249.05654057354 5876430.670350219 +4223 22391 9.774999999999999 36.025 488736.90711757133 302780.065199814 +22391 4326 488736.90711757133 302780.065199814 9.775550067386803 36.02651042460225 +4223 22391 8.3395 34.7875 357200.37018061505 166664.25066208627 +22391 4326 357200.37018061505 166664.25066208627 8.339971648069492 34.789070021472135 +4223 22391 11.2105 37.262499999999996 616233.1597902282 440836.0493256934 +22391 4326 616233.1597902282 440836.0493256934 11.211130889126645 37.2639530732339 +4223 22392 9.54 32.445 466155.6612332878 205273.15245034354 +22392 4326 466155.6612332878 205273.15245034354 9.540526473306778 32.44668003727852 +4223 22392 7.695 30.4515 288047.9194967982 -13611.503348048136 +22392 4326 288047.9194967982 -13611.503348048136 7.695428565582953 30.453288997956147 +4223 22392 11.385 34.4385 636458.4819237874 427209.1825532339 +22392 4326 636458.4819237874 427209.1825532339 11.385628884386794 34.44007723831623 +8246 22407 -139.505 62.29 577553.7482862907 6907382.944374962 +8246 22407 -140.8595 53.074 509412.8752931231 5880511.6950232545 +8246 22407 -138.1505 71.506 600849.7762664382 7936211.238253396 +8246 22408 -135.0 63.74 500000.0 7068042.170895341 +8246 22408 -137.7 49.628 305021.49189101974 5500772.589930895 +8246 22408 -132.3 77.852 563406.2209931078 8643313.609433059 +8246 22409 -129.0 63.72500000000001 500000.0 7066370.74190974 +8246 22409 -131.7 48.240500000000004 299538.3307894394 5346555.905977527 +8246 22409 -126.3 79.2095 556411.4973791244 8794662.018231109 +8246 22410 -123.0 64.965 500000.0 7204553.9102763105 +8246 22410 -125.7 49.813500000000005 305763.35086729913 5521392.586308251 +8246 22410 -120.3 80.1165 551719.885517777 8895789.811727198 +8246 22411 -117.0 66.245 500000.0 7347218.876508807 +8246 22411 -119.7 50.7155 309399.7445906481 5621665.37102995 +8246 22411 -114.3 81.77449999999999 543110.4436380728 9080662.632243542 +8246 22412 -111.0 66.495 500000.0 7375085.928094508 +8246 22412 -113.7 50.740500000000004 309501.21335740504 5624444.723641862 +8246 22412 -108.3 82.2495 540636.8512974724 9133629.104307733 +8246 22413 -105.0 66.495 499999.9999999994 7375085.928094508 +8246 22413 -107.7 50.740500000000004 309501.2133574041 5624444.723641862 +8246 22413 -102.3 82.2495 540636.8512974723 9133629.104307733 +8246 22414 -99.0 66.495 500000.0 7375085.928094508 +8246 22414 -101.7 50.740500000000004 309501.2133574041 5624444.723641862 +8246 22414 -96.3 82.2495 540636.8512974723 9133629.104307733 +8246 22415 -93.0 66.015 499999.9999999994 7321581.988534806 +8246 22415 -95.7 49.8285 305823.42915909307 5523059.994057171 +8246 22415 -90.3 82.2015 540886.9459985999 9128276.65995035 +8246 22416 -87.0 65.055 500000.0000000006 7214584.245044757 +8246 22416 -89.7 48.0045 298617.3852891464 5320328.070626806 +8246 22416 -84.3 82.1055 541387.0478100256 9117571.799824689 +8246 22417 -81.0 62.835 500000.0 6967205.614257044 +8246 22417 -83.7 43.786500000000004 282751.64517490444 4851703.725921316 +8246 22417 -78.3 81.8835 542543.079010169 9092816.959001321 +8246 22418 -75.0 63.815 500000.0 7076399.368382286 +8246 22418 -77.7 45.648500000000006 289614.5290814074 5058540.583399849 +8246 22418 -72.3 81.9815 542032.8378381082 9103744.745874293 +8246 22419 -69.0 62.4 500000.0 6918741.843632851 +8246 22419 -71.7 42.959999999999994 279778.81803513714 4759910.374057942 +8246 22419 -66.3 81.84 542769.5232055377 9087966.372905465 +8246 22420 -62.995000000000005 61.105000000000004 500269.5360667032 6774482.828320475 +8246 22420 -65.6995 40.4995 271245.84974719933 4486700.41001962 +8246 22420 -60.2905 81.7105 543596.2492053718 9073533.311533127 +8246 22421 -57.0 61.28 500000.0000000007 6793975.671579958 +8246 22421 -59.7 40.832 272337.899245773 4523616.464762851 +8246 22421 -54.3 81.72800000000001 543352.4364263249 9075477.545223918 +8246 22422 -50.995000000000005 48.575 500368.8140584385 5380212.31033272 +8246 22422 -53.6995 40.4075 270933.38613038993 4476486.695823391 +8246 22422 -48.2905 56.74249999999999 665706.9021386893 6292000.1815662505 +8246 22462 -111.24000000000001 54.495000000000005 -15549.31506636425 6040436.693252089 +8246 22462 -112.374 49.5405 -99434.22623297655 5490097.576352304 +8246 22462 -110.10600000000001 59.4495 50705.564205906085 6592424.346893215 +8246 22463 -114.0 54.495000000000005 0.0 6040410.182018194 +8246 22463 -115.35 49.5405 -97697.43470476051 5490066.157392819 +8246 22463 -112.65 59.4495 76566.88401019845 6592860.533550745 +8246 22464 -117.0 55.385000000000005 0.0 6139477.2256889725 +8246 22464 -118.35 51.231500000000004 -94283.19848592878 5678138.529527494 +8246 22464 -115.65 59.5385 76365.61692193565 6602773.133696755 +8246 22465 -119.25 56.44 46256.40122584206 6257181.704696059 +8246 22465 -119.925 53.236000000000004 5007.291499956597 5900296.996278018 +8246 22465 -118.575 59.644 80355.6458864481 6614612.014429 +4225 22521 -56.08 -24.95 592880.9463307385 7240236.956850792 +22521 4326 592880.9463307385 7240236.956850792 -56.080763216967355 -24.950371323840752 +4225 22521 -57.952 -31.214 409314.04161906894 6546239.408957016 +22521 4326 409314.04161906894 6546239.408957016 -57.95289573306949 -31.214477855765207 +4225 22521 -54.208 -18.686 794509.7786461874 7931589.620726736 +22521 4326 794509.7786461874 7931589.620726736 -54.20865143969032 -18.68627687642561 +4225 22522 -51.0 -24.39 500000.00000000064 7302557.932132347 +22522 4326 500000.00000000064 7302557.932132347 -51.00053457774889 -24.390383306277457 +4225 22522 -53.7 -32.841 247276.01089120907 6363053.511160215 +22522 4326 247276.01089120907 6363053.511160215 -53.70070965077622 -32.84153890162557 +4225 22522 -48.3 -15.939 789067.8816259673 8235919.63120377 +22522 4326 789067.8816259673 8235919.63120377 -48.300391343858955 -15.939250346039747 +4225 22523 -45.0 -20.145 500000.0 7772444.840734845 +22523 4326 500000.0 7772444.840734845 -45.00025544973178 -20.145323490452558 +4225 22523 -47.7 -24.7755 226973.36563145305 7257176.473599203 +22523 4326 226973.36563145305 7257176.473599203 -47.70038708115553 -24.775900489663808 +4225 22523 -42.3 -15.5145 789668.6616699893 8282919.73826155 +22523 4326 789668.6616699893 8282919.73826155 -42.300132438051605 -15.514751020553613 +4225 22524 -39.0 -12.82 499999.9999999993 8582753.261632897 +22524 4326 499999.9999999993 8582753.261632897 -38.999989806242645 -12.820210952551701 +4225 22524 -41.7 -21.946 221111.90804567654 7570661.682771536 +22524 4326 221111.90804567654 7570661.682771536 -41.70011061325863 -21.946358393959397 +4225 22524 -36.3 -3.693999999999999 799945.0310737714 9591236.284563437 +22524 4326 799945.0310737714 9591236.284563437 -36.29987724045493 -3.694083377141045 +4225 22525 -35.370000000000005 -7.545 238463.68096245234 9165282.739052044 +22525 4326 238463.68096245234 9165282.739052044 -35.369837373066325 -7.545134994435195 +4225 22525 -35.937 -9.8445 177819.55905890354 8910355.019798351 +22525 4326 177819.55905890354 8910355.019798351 -35.93686033221466 -9.844667144795594 +4225 22525 -34.803000000000004 -5.245500000000001 300165.32495572383 9419904.50410741 +22525 4326 300165.32495572383 9419904.50410741 -34.80281441199787 -5.245603734630773 +8252 22607 -139.505 62.29 577553.7482862907 6907382.944374962 +8252 22607 -140.8595 53.074 509412.8752931231 5880511.6950232545 +8252 22607 -138.1505 71.506 600849.7762664382 7936211.238253396 +8252 22608 -135.0 63.74 500000.0 7068042.170895341 +8252 22608 -137.7 49.628 305021.49189101974 5500772.589930895 +8252 22608 -132.3 77.852 563406.2209931078 8643313.609433059 +8252 22609 -129.0 63.72500000000001 500000.0 7066370.74190974 +8252 22609 -131.7 48.240500000000004 299538.3307894394 5346555.905977527 +8252 22609 -126.3 79.2095 556411.4973791244 8794662.018231109 +8252 22610 -123.0 64.965 500000.0 7204553.9102763105 +8252 22610 -125.7 49.813500000000005 305763.35086729913 5521392.586308251 +8252 22610 -120.3 80.1165 551719.885517777 8895789.811727198 +8252 22611 -117.0 66.245 500000.0 7347218.876508807 +8252 22611 -119.7 50.7155 309399.7445906481 5621665.37102995 +8252 22611 -114.3 81.77449999999999 543110.4436380728 9080662.632243542 +8252 22612 -111.0 66.495 500000.0 7375085.928094508 +8252 22612 -113.7 50.740500000000004 309501.21335740504 5624444.723641862 +8252 22612 -108.3 82.2495 540636.8512974724 9133629.104307733 +8252 22613 -105.0 66.495 499999.9999999994 7375085.928094508 +8252 22613 -107.7 50.740500000000004 309501.2133574041 5624444.723641862 +8252 22613 -102.3 82.2495 540636.8512974723 9133629.104307733 +8252 22614 -99.0 66.495 500000.0 7375085.928094508 +8252 22614 -101.7 50.740500000000004 309501.2133574041 5624444.723641862 +8252 22614 -96.3 82.2495 540636.8512974723 9133629.104307733 +8252 22615 -93.0 66.015 499999.9999999994 7321581.988534806 +8252 22615 -95.7 49.8285 305823.42915909307 5523059.994057171 +8252 22615 -90.3 82.2015 540886.9459985999 9128276.65995035 +8252 22616 -87.0 65.055 500000.0000000006 7214584.245044757 +8252 22616 -89.7 48.0045 298617.3852891464 5320328.070626806 +8252 22616 -84.3 82.1055 541387.0478100256 9117571.799824689 +8252 22617 -81.0 62.835 500000.0 6967205.614257044 +8252 22617 -83.7 43.786500000000004 282751.64517490444 4851703.725921316 +8252 22617 -78.3 81.8835 542543.079010169 9092816.959001321 +8252 22618 -75.0 63.815 500000.0 7076399.368382286 +8252 22618 -77.7 45.648500000000006 289614.5290814074 5058540.583399849 +8252 22618 -72.3 81.9815 542032.8378381082 9103744.745874293 +8252 22619 -69.0 62.4 500000.0 6918741.843632851 +8252 22619 -71.7 42.959999999999994 279778.81803513714 4759910.374057942 +8252 22619 -66.3 81.84 542769.5232055377 9087966.372905465 +8252 22620 -62.995000000000005 61.105000000000004 500269.5360667032 6774482.828320475 +8252 22620 -65.6995 40.4995 271245.84974719933 4486700.41001962 +8252 22620 -60.2905 81.7105 543596.2492053718 9073533.311533127 +8252 22621 -57.0 61.28 500000.0000000007 6793975.671579958 +8252 22621 -59.7 40.832 272337.899245773 4523616.464762851 +8252 22621 -54.3 81.72800000000001 543352.4364263249 9075477.545223918 +8252 22622 -50.995000000000005 48.575 500368.8140584385 5380212.31033272 +8252 22622 -53.6995 40.4075 270933.38613038993 4476486.695823391 +8252 22622 -48.2905 56.74249999999999 665706.9021386893 6292000.1815662505 +8252 22639 -63.19499999999999 46.495000000000005 385030.60045041575 716092.6605064794 +8252 22639 -64.3605 45.9595 294533.1514411849 657460.5291834144 +8252 22639 -62.0295 47.0305 473763.0329879282 776057.4583061151 +8252 22641 -53.519999999999996 48.225 266167.7742660153 5343042.305810534 +8252 22641 -54.402 46.7265 197637.93505436246 5177279.522066797 +8252 22641 -52.638 49.7235 330899.77029580396 5509604.837755639 +8252 22642 -55.995000000000005 50.760000000000005 305152.7607283704 5624824.161000914 +8252 22642 -57.3495 47.205000000000005 202566.6048048936 5230397.971164339 +8252 22642 -54.6405 54.315 393264.3743449206 6021228.46386013 +8252 22643 -58.489999999999995 49.019999999999996 305531.35177091585 5431308.693763163 +8252 22643 -59.381 47.652 238620.99140340142 5279582.616382039 +8252 22643 -57.599 50.388 368869.6012457025 5583835.068977962 +8252 22644 -61.5 55.46 304800.0 6147826.237482455 +8252 22644 -62.85 52.346000000000004 212812.5369825337 5802121.057718212 +8252 22644 -60.15 58.574 383336.7179211068 6495356.391486117 +8252 22645 -64.5 56.05 304800.0 6213508.713458478 +8252 22645 -65.85 52.027 212151.80839827127 5766631.629906267 +8252 22645 -63.15 60.073 379952.97605128074 6662307.11506013 +8252 22646 -66.905 53.695 344098.0956265261 5951538.114514665 +8252 22646 -67.7195 52.2145 289798.84221734176 5786654.822295822 +8252 22646 -66.0905 55.1755 394593.46811203705 6117062.946834693 +8252 22648 -74.675 45.315 212673.28218495188 5020121.539722489 +8252 22648 -74.9675 45.01349999999999 189130.9981501979 4986993.812086513 +8252 22648 -74.3825 45.6165 235974.83456550929 5053334.307750545 +8252 22649 -76.5 44.94 304800.0 4977778.678738295 +8252 22649 -77.85 43.761 196098.35460517107 4847668.195731273 +8252 22649 -75.15 46.119 409140.2437243696 5109688.226389748 +8252 22650 -79.495 44.795 305195.5978716272 4961666.569993048 +8252 22650 -80.8495 42.5135 193906.48108692348 4709087.304122142 +8252 22650 -78.14049999999999 47.076499999999996 408039.4404268192 5216127.032725465 +8252 22651 -82.3 43.835 320884.01985230704 4855023.008696913 +8252 22651 -83.47 41.8865 224299.73349253612 4639021.263440096 +8252 22651 -81.13 45.783500000000004 411326.9483581694 5072427.79761347 +8252 22652 -81.0 50.605000000000004 304800.0 5607583.33382646 +8252 22652 -82.35 46.4605 201106.3244268999 5147644.078890337 +8252 22652 -79.65 54.7495 391718.0987432869 6069573.696578401 +8252 22653 -84.0 50.795 304800.0 5628717.3001815565 +8252 22653 -85.35 46.4795 201142.4066109733 5149755.887885232 +8252 22653 -82.65 55.1105 390943.12263336655 6109753.223535529 +8252 22654 -87.0 51.935 304800.0 5755535.600946693 +8252 22654 -88.35 47.6465 203380.4647399176 5279478.173564699 +8252 22654 -85.65 56.2235 388532.1709837478 6233645.02786236 +8252 22655 -90.0 52.435 304800.0 5811165.4693391705 +8252 22655 -91.35 48.4165 204880.4294295852 5365084.149450656 +8252 22655 -88.65 56.4535 388029.9376740323 6259249.762159773 +8252 22656 -93.0 51.63 304800.0 5721603.713476377 +8252 22656 -94.35 48.417 204881.4093704535 5365139.741284148 +8252 22656 -91.65 54.843 391517.711160886 6079980.087531899 +8252 22657 -94.83 50.965 386983.664340777 5648279.054044446 +8252 22657 -95.127 48.9175 368777.6797239066 5420278.251220682 +8252 22657 -94.533 53.0125 403249.05654057354 5876430.670350219 +8255 22707 -139.505 62.29 577553.7482862907 6907382.944374962 +8255 22707 -140.8595 53.074 509412.8752931231 5880511.6950232545 +8255 22707 -138.1505 71.506 600849.7762664382 7936211.238253396 +8255 22708 -135.0 63.74 500000.0 7068042.170895341 +8255 22708 -137.7 49.628 305021.49189101974 5500772.589930895 +8255 22708 -132.3 77.852 563406.2209931078 8643313.609433059 +8255 22709 -129.0 63.72500000000001 500000.0 7066370.74190974 +8255 22709 -131.7 48.240500000000004 299538.3307894394 5346555.905977527 +8255 22709 -126.3 79.2095 556411.4973791244 8794662.018231109 +8255 22710 -123.0 64.965 500000.0 7204553.9102763105 +8255 22710 -125.7 49.813500000000005 305763.35086729913 5521392.586308251 +8255 22710 -120.3 80.1165 551719.885517777 8895789.811727198 +8255 22711 -117.0 66.245 500000.0 7347218.876508807 +8255 22711 -119.7 50.7155 309399.7445906481 5621665.37102995 +8255 22711 -114.3 81.77449999999999 543110.4436380728 9080662.632243542 +8255 22712 -111.0 66.495 500000.0 7375085.928094508 +8255 22712 -113.7 50.740500000000004 309501.21335740504 5624444.723641862 +8255 22712 -108.3 82.2495 540636.8512974724 9133629.104307733 +8255 22713 -105.0 66.495 499999.9999999994 7375085.928094508 +8255 22713 -107.7 50.740500000000004 309501.2133574041 5624444.723641862 +8255 22713 -102.3 82.2495 540636.8512974723 9133629.104307733 +8255 22714 -99.0 66.495 500000.0 7375085.928094508 +8255 22714 -101.7 50.740500000000004 309501.2133574041 5624444.723641862 +8255 22714 -96.3 82.2495 540636.8512974723 9133629.104307733 +8255 22715 -93.0 66.015 499999.9999999994 7321581.988534806 +8255 22715 -95.7 49.8285 305823.42915909307 5523059.994057171 +8255 22715 -90.3 82.2015 540886.9459985999 9128276.65995035 +8255 22716 -87.0 65.055 500000.0000000006 7214584.245044757 +8255 22716 -89.7 48.0045 298617.3852891464 5320328.070626806 +8255 22716 -84.3 82.1055 541387.0478100256 9117571.799824689 +8255 22717 -81.0 62.835 500000.0 6967205.614257044 +8255 22717 -83.7 43.786500000000004 282751.64517490444 4851703.725921316 +8255 22717 -78.3 81.8835 542543.079010169 9092816.959001321 +8255 22718 -75.0 63.815 500000.0 7076399.368382286 +8255 22718 -77.7 45.648500000000006 289614.5290814074 5058540.583399849 +8255 22718 -72.3 81.9815 542032.8378381082 9103744.745874293 +8255 22719 -69.0 62.4 500000.0 6918741.843632851 +8255 22719 -71.7 42.959999999999994 279778.81803513714 4759910.374057942 +8255 22719 -66.3 81.84 542769.5232055377 9087966.372905465 +8255 22720 -62.995000000000005 61.105000000000004 500269.5360667032 6774482.828320475 +8255 22720 -65.6995 40.4995 271245.84974719933 4486700.41001962 +8255 22720 -60.2905 81.7105 543596.2492053718 9073533.311533127 +8255 22721 -57.0 61.28 500000.0000000007 6793975.671579958 +8255 22721 -59.7 40.832 272337.899245773 4523616.464762851 +8255 22721 -54.3 81.72800000000001 543352.4364263249 9075477.545223918 +8255 22722 -50.995000000000005 48.575 500368.8140584385 5380212.31033272 +8255 22722 -53.6995 40.4075 270933.38613038993 4476486.695823391 +8255 22722 -48.2905 56.74249999999999 665706.9021386893 6292000.1815662505 +8255 22739 -63.19499999999999 46.495000000000005 385030.60045041575 716092.6605064794 +8255 22739 -64.3605 45.9595 294533.1514411849 657460.5291834144 +8255 22739 -62.0295 47.0305 473763.0329879282 776057.4583061151 +8255 22762 -111.24000000000001 54.495000000000005 -15549.31506636425 6040436.693252089 +8255 22762 -112.374 49.5405 -99434.22623297655 5490097.576352304 +8255 22762 -110.10600000000001 59.4495 50705.564205906085 6592424.346893215 +8255 22763 -114.0 54.495000000000005 0.0 6040410.182018194 +8255 22763 -115.35 49.5405 -97697.43470476051 5490066.157392819 +8255 22763 -112.65 59.4495 76566.88401019845 6592860.533550745 +8255 22764 -117.0 55.385000000000005 0.0 6139477.2256889725 +8255 22764 -118.35 51.231500000000004 -94283.19848592878 5678138.529527494 +8255 22764 -115.65 59.5385 76365.61692193565 6602773.133696755 +8255 22765 -119.25 56.44 46256.40122584206 6257181.704696059 +8255 22765 -119.925 53.236000000000004 5007.291499956597 5900296.996278018 +8255 22765 -118.575 59.644 80355.6458864481 6614612.014429 +4227 22770 38.71 34.805 424400.2106134463 318027.37323825987 +22770 4326 424400.2106134463 318027.37323825987 38.711374165804685 34.804554048531 +4227 22770 35.407 32.5595 117477.10317783663 69936.8655322383 +22770 4326 117477.10317783663 69936.8655322383 35.40824878765553 32.559188183989086 +4227 22770 42.013000000000005 37.0505 714865.0691631313 575919.4828988428 +22770 4326 714865.0691631313 575919.4828988428 42.01450401604168 37.0499275687454 +4227 22780 38.71 34.805 -40245.64255176285 67167.78544760951 +22780 4326 -40245.64255176285 67167.78544760951 38.711374165804685 34.804554048530996 +4227 22780 35.407 32.5595 -351454.70468129 -175550.8010095362 +22780 4326 -351454.70468129 -175550.8010095362 35.40824878765553 32.55918818398908 +4227 22780 42.013000000000005 37.0505 254718.69914466582 319883.95302610385 +22780 4326 254718.69914466582 319883.95302610385 42.01450401604168 37.04992756874538 +10414 22807 -139.505 62.29 577553.7482862907 6907382.944374962 +10414 22807 -140.8595 53.074 509412.8752931231 5880511.6950232545 +10414 22807 -138.1505 71.506 600849.7762664382 7936211.238253396 +10414 22808 -135.0 63.74 500000.0 7068042.170895341 +10414 22808 -137.7 49.628 305021.49189101974 5500772.589930895 +10414 22808 -132.3 77.852 563406.2209931078 8643313.609433059 +10414 22809 -129.0 63.72500000000001 500000.0 7066370.74190974 +10414 22809 -131.7 48.240500000000004 299538.3307894394 5346555.905977527 +10414 22809 -126.3 79.2095 556411.4973791244 8794662.018231109 +10414 22810 -123.0 64.965 500000.0 7204553.9102763105 +10414 22810 -125.7 49.813500000000005 305763.35086729913 5521392.586308251 +10414 22810 -120.3 80.1165 551719.885517777 8895789.811727198 +10414 22811 -117.0 66.245 500000.0 7347218.876508807 +10414 22811 -119.7 50.7155 309399.7445906481 5621665.37102995 +10414 22811 -114.3 81.77449999999999 543110.4436380728 9080662.632243542 +10414 22812 -111.0 66.495 500000.0 7375085.928094508 +10414 22812 -113.7 50.740500000000004 309501.21335740504 5624444.723641862 +10414 22812 -108.3 82.2495 540636.8512974724 9133629.104307733 +10414 22813 -105.0 66.495 499999.9999999994 7375085.928094508 +10414 22813 -107.7 50.740500000000004 309501.2133574041 5624444.723641862 +10414 22813 -102.3 82.2495 540636.8512974723 9133629.104307733 +10414 22814 -99.0 66.495 500000.0 7375085.928094508 +10414 22814 -101.7 50.740500000000004 309501.2133574041 5624444.723641862 +10414 22814 -96.3 82.2495 540636.8512974723 9133629.104307733 +10414 22815 -93.0 66.015 499999.9999999994 7321581.988534806 +10414 22815 -95.7 49.8285 305823.42915909307 5523059.994057171 +10414 22815 -90.3 82.2015 540886.9459985999 9128276.65995035 +10414 22816 -87.0 65.055 500000.0000000006 7214584.245044757 +10414 22816 -89.7 48.0045 298617.3852891464 5320328.070626806 +10414 22816 -84.3 82.1055 541387.0478100256 9117571.799824689 +10414 22817 -81.0 62.835 500000.0 6967205.614257044 +10414 22817 -83.7 43.786500000000004 282751.64517490444 4851703.725921316 +10414 22817 -78.3 81.8835 542543.079010169 9092816.959001321 +10414 22818 -75.0 63.815 500000.0 7076399.368382286 +10414 22818 -77.7 45.648500000000006 289614.5290814074 5058540.583399849 +10414 22818 -72.3 81.9815 542032.8378381082 9103744.745874293 +10414 22819 -69.0 62.4 500000.0 6918741.843632851 +10414 22819 -71.7 42.959999999999994 279778.81803513714 4759910.374057942 +10414 22819 -66.3 81.84 542769.5232055377 9087966.372905465 +10414 22820 -62.995000000000005 61.105000000000004 500269.5360667032 6774482.828320475 +10414 22820 -65.6995 40.4995 271245.84974719933 4486700.41001962 +10414 22820 -60.2905 81.7105 543596.2492053718 9073533.311533127 +10414 22821 -57.0 61.28 500000.0000000007 6793975.671579958 +10414 22821 -59.7 40.832 272337.899245773 4523616.464762851 +10414 22821 -54.3 81.72800000000001 543352.4364263249 9075477.545223918 +10414 22822 -50.995000000000005 48.575 500368.8140584385 5380212.31033272 +10414 22822 -53.6995 40.4075 270933.38613038993 4476486.695823391 +10414 22822 -48.2905 56.74249999999999 665706.9021386893 6292000.1815662505 +4229 22991 34.975 26.665 297511.29279323906 730394.9539430672 +22991 4326 297511.29279323906 730394.9539430672 34.976654011048154 26.66509182793834 +4229 22991 33.1975 22.4395 114425.29574138904 263459.9750271151 +22991 4326 114425.29574138904 263459.9750271151 33.19908561713709 22.439577001846207 +4229 22991 36.752500000000005 30.8905 467576.2463754257 1200037.8608163286 +22991 4326 467576.2463754257 1200037.8608163286 36.75423533888069 30.890599563100572 +4229 22992 31.635 27.905 677515.8129059221 577960.269383878 +22992 4326 677515.8129059221 577960.269383878 31.636643918350163 27.905141368166035 +4229 22992 29.2635 22.5815 436404.35444782587 -10890.176910645678 +22992 4326 436404.35444782587 -10890.176910645678 29.265051106963604 22.58161664163606 +4229 22992 34.0065 33.2285 895295.1143049446 1172012.6046362529 +22992 4326 895295.1143049446 1172012.6046362529 34.00825830387137 33.22865399696457 +4229 22993 26.85 29.93 685516.761684205 192249.74815586777 +22993 4326 685516.761684205 192249.74815586777 26.85162455115951 29.930219021860374 +4229 22993 24.915 28.355 495564.59798543656 19435.477101831348 +22993 4326 495564.59798543656 19435.477101831348 24.916576218180158 28.355224929297556 +4229 22993 28.785 31.505 869584.3172191592 368234.6686975107 +22993 4326 869584.3172191592 368234.6686975107 28.786673795415528 31.50521028428022 +4229 22994 27.0 25.09 700000.0 655904.6599044193 +22994 4326 700000.0 655904.6599044193 27.001556654370898 25.09016569598056 +4229 22994 25.191 22.299999999999997 513570.18520154775 348014.7840799723 +22994 4326 513570.18520154775 348014.7840799723 25.19250297133871 22.30015210349032 +4229 22994 28.809 27.880000000000003 878151.8603346843 966343.0819135242 +22994 4326 878151.8603346843 966343.0819135242 28.81061501419307 27.88017454649997 +4230 23028 -14.05 52.5 564491.2681218324 5817198.046311531 +23028 4326 564491.2681218324 5817198.046311531 -14.051703142259344 52.498989189186965 +4230 23028 -15.895000000000001 48.837 434321.3306366813 5409829.347907837 +23028 4326 434321.3306366813 5409829.347907837 -15.896606078363059 48.83586537926136 +4230 23028 -12.205 56.163 673566.0502334221 6227876.5752441455 +23028 4326 673566.0502334221 6227876.5752441455 -12.20682343664973 56.162128762196744 +4230 23029 -9.0 49.269999999999996 500000.0 5457579.842747385 +23029 4326 500000.0 5457579.842747385 -9.001516993238406 49.26895829893813 +4230 23029 -11.7 37.444 261134.77348977257 4147619.942100885 +23029 4326 261134.77348977257 4147619.942100885 -11.701283770832857 37.4426981754593 +4230 23029 -6.300000000000001 61.096 645569.4464667055 6776644.588016645 +23029 4326 645569.4464667055 6776644.588016645 -6.301982650323823 61.095374880320115 +4230 23030 -2.995 57.875 500296.5899427377 6414939.208861236 +23030 4326 500296.5899427377 6414939.208861236 -2.9967258387800504 57.874304000857705 +4230 23030 -5.6995000000000005 37.521499999999996 261425.78964795906 4156219.4296215638 +23030 4326 261425.78964795906 4156219.4296215638 -5.700700818625927 37.52026234334557 +4230 23030 -0.29050000000000065 78.2285 561689.1020983462 8685551.556858234 +23030 4326 561689.1020983462 8685551.556858234 -0.29482074099352507 78.2287094611114 +4230 23031 3.0 60.505 500000.00000000047 6707811.983479855 +23031 4326 500000.00000000047 6707811.983479855 2.998301821024389 60.50448003782033 +4230 23031 0.30000000000000004 40.7545 272062.4469784768 4515090.588715522 +23031 4326 272062.4469784768 4515090.588715522 0.29884494488841007 40.75337571805275 +4230 23031 5.699999999999999 80.2555 551002.4042871463 8911552.932619713 +23031 4326 551002.4042871463 8911552.932619713 5.695298199190885 80.25589271128561 +4230 23032 8.995000000000001 60.43 499724.7248683263 6699458.461349457 +23032 4326 499724.7248683263 6699458.461349457 8.99348947694828 60.429549311689556 +4230 23032 6.2905 38.92 265086.47665342945 4311462.469196988 +23032 4326 265086.47665342945 4311462.469196988 6.289486724421401 38.91889814149926 +4230 23032 11.6995 81.94 542243.3583541926 9099391.314139228 +23032 4326 542243.3583541926 9099391.314139228 11.694498125184763 81.94055168530959 +4230 23033 15.005 59.455 500283.4767504578 6590871.248359137 +23033 4326 500283.4767504578 6590871.248359137 15.003728158469684 59.45457467880375 +4230 23033 12.3005 36.9865 259731.19799004713 4096848.2699325094 +23033 4326 259731.19799004713 4096848.2699325094 12.299632693288824 36.98541251847047 +4230 23033 17.709500000000002 81.92349999999999 542485.9467530359 9097558.662253365 +23033 4326 542485.9467530359 9097558.662253365 17.705236773048753 81.92411847858999 +4230 23034 21.0 59.065000000000005 500000.0000000005 6547440.924792946 +23034 4326 500000.0000000005 6547440.924792946 20.99894866272867 59.0646129749842 +4230 23034 18.290999999999997 36.1375 256230.12617186838 4002663.656289576 +23034 4326 256230.12617186838 4002663.656289576 18.290269581703473 36.13643828839853 +4230 23034 23.709000000000003 81.9925 542117.6219363157 9105252.707738549 +23034 4326 542117.6219363157 9105252.707738549 23.70548089342342 81.99317836816104 +4230 23035 27.005000000000003 55.22 500318.10580583295 6119406.813279056 +23035 4326 500318.10580583295 6119406.813279056 27.00424878307285 55.21951025459389 +4230 23035 24.3005 28.661 236151.26007626805 3173456.0243219575 +23035 4326 236151.26007626805 3173456.0243219575 24.29995257511289 28.659867036141062 +4230 23035 29.709500000000002 81.779 543240.8223844311 9081445.073909286 +23035 4326 543240.8223844311 9081445.073909286 29.70686986183173 81.77971289297277 +4230 23036 33.0 56.94500000000001 499999.9999999996 6311404.8837468475 +23036 4326 499999.9999999996 6311404.8837468475 32.99959762874275 56.94455755837874 +4230 23036 30.3 31.965500000000002 244822.25099803967 3539849.2569514643 +23036 4326 244822.25099803967 3539849.2569514643 30.29968038747575 31.964384679100373 +4230 23036 35.7 81.92450000000001 542331.8827083315 9097663.213576855 +23036 4326 542331.8827083315 9097663.213576855 35.69880730034522 81.92520639801234 +4230 23037 39.0 54.125 500000.0000000004 5997558.141509704 +23037 4326 500000.0000000004 5997558.141509704 38.999822667482725 54.12446633223304 +4230 23037 36.3 31.6745 244019.84039947166 3507578.26371612 +23037 4326 244019.84039947166 3507578.26371612 36.29981628551554 31.673392473786173 +4230 23037 41.7 76.57549999999999 569954.3959139358 8501249.901996281 +23037 4326 569954.3959139358 8501249.901996281 41.699778554209956 76.57595395895892 +4230 23038 43.415 39.285 363294.7166218683 4349675.203668644 +23038 4326 363294.7166218683 4349675.203668644 43.4149428566469 39.284036890476756 +4230 23038 42.1415 37.201499999999996 246294.7803588427 4121121.4596215845 +23038 4326 246294.7803588427 4121121.4596215845 42.14141535978966 37.200488559078096 +4230 23038 44.6885 41.3685 473947.62033407623 4579792.472226957 +23038 4326 473947.62033407623 4579792.472226957 44.6884718725441 41.367589465723476 +4230 23090 -0.8250000000000002 56.53 449251.1085699571 6265513.884749546 +23090 4326 449251.1085699571 6265513.884749546 -0.8265883840321426 56.52929442031191 +4230 23090 -4.6274999999999995 51.58 179419.1843724309 5724596.932766515 +23090 4326 179419.1843724309 5724596.932766515 -4.628996213665129 51.579086864508156 +4230 23090 2.9774999999999983 61.480000000000004 658573.9718432063 6820038.007237327 +23090 4326 658573.9718432063 6820038.007237327 2.975786106758882 61.47952630923415 +4230 23095 4.45 53.605000000000004 463605.71799555665 5939842.373515722 +23095 4326 463605.71799555665 5939842.373515722 4.448648848936606 53.60425653411581 +4230 23095 2.722 51.6565 342421.88324801024 5725410.0788040105 +23095 4326 342421.88324801024 5725410.0788040105 2.720664203413703 51.65567523016376 +4230 23095 6.178 55.5535 574315.818332304 6157152.23656415 +23095 4326 574315.818332304 6157152.23656415 6.17663165651257 55.55284168410377 +4232 23239 52.995000000000005 18.130000000000003 711080.0903464175 2005537.9224610017 +23239 4326 711080.0903464175 2005537.9224610017 52.997620498967954 18.130628793036298 +4232 23239 52.0905 16.744 616241.2503143994 1851391.7531347068 +23239 4326 616241.2503143994 1851391.7531347068 52.0930700754555 16.744737209611493 +4232 23239 53.8995 19.516000000000002 804327.8914330881 2160311.540139401 +23239 4326 804327.8914330881 2160311.540139401 53.90217260387099 19.516521305690706 +4232 23240 56.955 21.655 495343.7940696246 2394438.9306312893 +23240 4326 495343.7940696246 2394438.9306312893 56.95780969236401 21.65533004127047 +4232 23240 54.2955 17.366500000000002 212594.8906555368 1921957.8022644052 +23240 4326 212594.8906555368 1921957.8022644052 54.298152573194 17.36716481593221 +4232 23240 59.6145 25.9435 761841.9239572585 2871807.68061707 +23240 4326 761841.9239572585 2871807.68061707 59.61748634993847 25.943511328263885 +6318 23301 -90.025 42.215 1783681.7162253496 760475.4836585169 +23301 4326 1783681.7162253496 760475.4836585169 -90.025 42.21500000000001 +6318 23301 -90.59649999999999 41.9495 1628124.7023491373 664376.9480856465 +23301 4326 1628124.7023491373 664376.9480856465 -90.59649999999999 41.949499999999986 +6318 23301 -89.4535 42.4805 1937943.0712899314 857617.1147888781 +23301 4326 1937943.0712899314 857617.1147888781 -89.4535 42.4805 +6318 23302 -89.31 42.045 2805701.8503739145 781704.5620166968 +23302 4326 2805701.8503739145 781704.5620166968 -89.31 42.04500000000001 +6318 23302 -89.652 41.6265 2712088.5911929756 629448.1227424254 +23302 4326 2712088.5911929756 629448.1227424254 -89.652 41.62650000000001 +6318 23302 -88.968 42.463499999999996 2898096.5655074203 934343.2926371696 +23302 4326 2898096.5655074203 934343.2926371696 -88.968 42.463499999999996 +6318 23303 -88.57 41.975 3753964.614782298 756197.198436331 +23303 4326 3753964.614782298 756197.198436331 -88.57 41.97500000000001 +6318 23303 -88.912 41.502500000000005 3660138.7848208994 584275.89778937 +23303 4326 3660138.7848208994 584275.89778937 -88.912 41.50250000000001 +6318 23303 -88.228 42.4475 3846416.8636978213 928503.6748588103 +23303 4326 3846416.8636978213 928503.6748588103 -88.228 42.44749999999999 +6318 23304 -87.895 41.980000000000004 4731168.463835968 758023.9956303487 +23304 4326 4731168.463835968 758023.9956303487 -87.89500000000001 41.980000000000004 +6318 23304 -88.2325 41.512 4638541.251664316 587764.3932239356 +23304 4326 4638541.251664316 587764.3932239356 -88.2325 41.512 +6318 23304 -87.55749999999999 42.448 4822453.389674816 928658.7159938817 +23304 4326 4822453.389674816 928658.7159938817 -87.55749999999999 42.448 +6318 23305 -90.61500000000001 41.555 5736894.324749123 756822.3224352174 +23305 4326 5736894.324749123 756822.3224352174 -90.61500000000001 41.55500000000001 +6318 23305 -91.0335 41.3435 5621959.732129588 680052.7236704293 +23305 4326 5621959.732129588 680052.7236704293 -91.0335 41.3435 +6318 23305 -90.1965 41.7665 5851082.263778765 834149.8696865506 +23305 4326 5851082.263778765 834149.8696865506 -90.1965 41.76650000000001 +6318 23306 -90.03 41.54 6731475.477241812 751356.7212462372 +23306 4326 6731475.477241812 751356.7212462372 -90.03 41.540000000000006 +6318 23306 -90.399 41.18 6629923.208251402 620372.6159603408 +23306 4326 6629923.208251402 620372.6159603408 -90.399 41.17999999999999 +6318 23306 -89.661 41.9 6831906.92243959 882780.1996091105 +23306 4326 6831906.92243959 882780.1996091105 -89.661 41.90000000000002 +6318 23307 -89.055 41.28 7741625.657965563 747712.5164796863 +23307 4326 7741625.657965563 747712.5164796863 -89.055 41.28000000000002 +6318 23307 -89.7885 40.956 7539007.855409097 630525.0721835964 +23307 4326 7539007.855409097 630525.0721835964 -89.7885 40.955999999999996 +6318 23307 -88.3215 41.604 7942248.554518663 866610.0223825974 +23307 4326 7942248.554518663 866610.0223825974 -88.3215 41.604000000000006 +6318 23308 -87.895 41.36 8722825.882543845 787144.6260511864 +23308 4326 8722825.882543845 787144.6260511864 -87.895 41.36 +6318 23308 -88.2325 41.027 8629846.96710887 665877.517831885 +23308 4326 8629846.96710887 665877.517831885 -88.2325 41.027 +6318 23308 -87.55749999999999 41.693 8814859.348797545 908779.7392884862 +23308 4326 8814859.348797545 908779.7392884862 -87.55749999999998 41.693 +6318 23309 -90.81 40.980000000000004 9689044.942870833 776181.6002363896 +23309 4326 9689044.942870833 776181.6002363896 -90.80999999999999 40.980000000000004 +6318 23309 -91.152 40.656 9594203.903979963 658274.6641614239 +23309 4326 9594203.903979963 658274.6641614239 -91.15199999999999 40.65599999999999 +6318 23309 -90.468 41.304 9782961.456834879 894464.9708224139 +23309 4326 9782961.456834879 894464.9708224139 -90.468 41.303999999999995 +6318 23310 -90.04499999999999 40.71 245248.5414553906 1752242.8479810948 +23310 4326 245248.5414553906 1752242.8479810948 -90.04499999999999 40.71 +6318 23310 -90.4185 40.233 141069.9848757298 1578615.0448050483 +23310 4326 141069.9848757298 1578615.0448050483 -90.4185 40.233 +6318 23310 -89.6715 41.187000000000005 347948.2961933776 1926325.605534262 +23310 4326 347948.2961933776 1926325.605534262 -89.6715 41.18700000000001 +6318 23311 -89.625 40.644999999999996 1384937.8902585274 1803938.0043507032 +23311 4326 1384937.8902585274 1803938.0043507032 -89.625 40.644999999999996 +6318 23311 -89.95349999999999 40.3435 1293396.1093450708 1694237.8764341692 +23311 4326 1293396.1093450708 1694237.8764341692 -89.95349999999999 40.3435 +6318 23311 -89.29650000000001 40.9465 1475659.042156468 1913984.4118460552 +23311 4326 1475659.042156468 1913984.4118460552 -89.29650000000001 40.94650000000001 +6318 23312 -89.28 40.870000000000005 2761531.64146295 1672710.49020467 +23312 4326 2761531.64146295 1672710.49020467 -89.28 40.87 +6318 23312 -89.604 40.618 2671601.0370081193 1581041.3757011676 +23312 4326 2671601.0370081193 1581041.3757011676 -89.604 40.61800000000001 +6318 23312 -88.956 41.122 2850782.674165116 1764715.2589637174 +23312 4326 2850782.674165116 1764715.2589637174 -88.956 41.12199999999999 +6318 23313 -88.86 40.519999999999996 3770219.650686617 1746286.7501947628 +23313 4326 3770219.650686617 1746286.7501947628 -88.86000000000001 40.52 +6318 23313 -89.229 40.304 3667287.0190915596 1667819.663567705 +23313 4326 3667287.0190915596 1667819.663567705 -89.229 40.304 +6318 23313 -88.491 40.736 3872494.0266202805 1825186.0805043674 +23313 4326 3872494.0266202805 1825186.0805043674 -88.491 40.736 +6318 23314 -88.58500000000001 40.864999999999995 4747318.897780468 1726249.649453873 +23314 4326 4747318.897780468 1726249.649453873 -88.58500000000001 40.86499999999999 +6318 23314 -88.9045 40.6355 4658605.798188407 1642830.0348782456 +23314 4326 4658605.798188407 1642830.0348782456 -88.9045 40.6355 +6318 23314 -88.2655 41.0945 4835421.538304793 1809995.3024428745 +23314 4326 4835421.538304793 1809995.3024428745 -88.26550000000002 41.0945 +6318 23315 -87.99 40.705 5729909.3083591 1722607.187683914 +23315 4326 5729909.3083591 1722607.187683914 -87.99 40.70500000000001 +6318 23315 -88.413 40.4215 5612081.005182564 1619656.3455737426 +23315 4326 5612081.005182564 1619656.3455737426 -88.413 40.42150000000001 +6318 23315 -87.567 40.9885 5846742.327164559 1826128.382794904 +23315 4326 5846742.327164559 1826128.382794904 -87.567 40.9885 +6318 23316 -91.21000000000001 40.195 6737174.835196999 1746667.9716929088 +23316 4326 6737174.835196999 1746667.9716929088 -91.21000000000001 40.19499999999999 +6318 23316 -91.48899999999999 39.7945 6658839.1696631 1600855.2909600246 +23316 4326 6658839.1696631 1600855.2909600246 -91.48899999999999 39.79449999999999 +6318 23316 -90.93100000000001 40.5955 6814592.920900697 1892735.879808182 +23316 4326 6814592.920900697 1892735.879808182 -90.93100000000001 40.5955 +6318 23317 -90.555 40.235 7722564.312604505 1743037.5297090935 +23317 4326 7722564.312604505 1743037.5297090935 -90.555 40.235 +6318 23317 -90.8835 39.8705 7630421.980329216 1610373.8595996443 +23317 4326 7630421.980329216 1610373.8595996443 -90.88350000000001 39.8705 +6318 23317 -90.2265 40.5995 7813722.6544819465 1876049.4332564517 +23317 4326 7813722.6544819465 1876049.4332564517 -90.2265 40.599500000000006 +6318 23318 -89.86500000000001 40.155 8741830.320475698 1740828.1569940785 +23318 4326 8741830.320475698 1740828.1569940785 -89.865 40.155 +6318 23318 -90.5175 39.8985 8558682.360922394 1648192.4919471478 +23318 4326 8558682.360922394 1648192.4919471478 -90.51750000000001 39.89850000000001 +6318 23318 -89.2125 40.4115 8923606.948199539 1834808.3498839068 +23318 4326 8923606.948199539 1834808.3498839068 -89.2125 40.411500000000004 +6318 23319 -88.84 39.75 9666752.448283108 1748394.2143010115 +23319 4326 9666752.448283108 1748394.2143010115 -88.83999999999999 39.75 +6318 23319 -89.182 39.264 9569834.848083429 1571587.6875867757 +23319 4326 9569834.848083429 1571587.6875867757 -89.182 39.264 +6318 23319 -88.49799999999999 40.236 9762319.32164765 1925582.3367642732 +23319 4326 9762319.32164765 1925582.3367642732 -88.49799999999999 40.236000000000004 +6318 23320 -87.995 40.18 329397.16648559534 2832929.219923902 +23320 4326 329397.16648559534 2832929.219923902 -87.995 40.18000000000001 +6318 23320 -88.4225 39.891999999999996 209441.0246761313 2728292.9350797324 +23320 4326 209441.0246761313 2728292.9350797324 -88.4225 39.891999999999996 +6318 23320 -87.5675 40.468 448343.73794617667 2938145.6656912337 +23320 4326 448343.73794617667 2938145.6656912337 -87.5675 40.468 +6318 23321 -90.65 39.635000000000005 1232917.2268963817 2816539.841065105 +23321 4326 1232917.2268963817 2816539.841065105 -90.65 39.635 +6318 23321 -91.307 39.414500000000004 1047239.2160652851 2737001.7782405587 +23321 4326 1047239.2160652851 2737001.7782405587 -91.307 39.414500000000004 +6318 23321 -89.993 39.8555 1417420.582186836 2897435.510415792 +23321 4326 1417420.582186836 2897435.510415792 -89.993 39.855499999999985 +6318 23322 -89.60499999999999 39.75 2341653.469653488 2887003.1773762465 +23322 4326 2341653.469653488 2887003.1773762465 -89.60499999999999 39.74999999999999 +6318 23322 -89.9605 39.543000000000006 2241429.9336195327 2811747.2857679445 +23322 4326 2241429.9336195327 2811747.2857679445 -89.9605 39.543000000000006 +6318 23322 -89.2495 39.956999999999994 2441278.4630112876 2962658.065432852 +23322 4326 2441278.4630112876 2962658.065432852 -89.2495 39.956999999999994 +6318 23323 -88.005 39.629999999999995 3771591.6197880884 2748714.595156382 +23323 4326 3771591.6197880884 2748714.595156382 -88.005 39.629999999999995 +6318 23323 -88.4325 39.396 3650765.0177725432 2663770.8141569123 +23323 4326 3650765.0177725432 2663770.8141569123 -88.4325 39.39600000000001 +6318 23323 -87.5775 39.864 3891606.6010461 2834235.0483523896 +23323 4326 3891606.6010461 2834235.0483523896 -87.5775 39.86399999999999 +6318 23324 -90.53999999999999 39.135 4745653.1469735205 2780029.171379872 +23324 4326 4745653.1469735205 2780029.171379872 -90.53999999999999 39.135000000000005 +6318 23324 -90.89999999999999 38.8875 4643135.117593572 2690129.046755198 +23324 4326 4643135.117593572 2690129.046755198 -90.89999999999999 38.887499999999996 +6318 23324 -90.18 39.38249999999999 4847456.290206944 2870337.9375517243 +23324 4326 4847456.290206944 2870337.9375517243 -90.18 39.38249999999999 +6318 23325 -90.16 39.260000000000005 5738168.305454528 2741430.230395044 +23325 4326 5738168.305454528 2741430.230395044 -90.16 39.25999999999999 +6318 23325 -90.583 39.017 5617964.42814705 2653213.9834917937 +23325 4326 5617964.42814705 2653213.9834917937 -90.583 39.017 +6318 23325 -89.737 39.503 5857544.465693221 2830210.1163105797 +23325 4326 5857544.465693221 2830210.1163105797 -89.737 39.503 +6318 23326 -89.365 39.41 6735889.802640132 2771045.923101549 +23326 4326 6735889.802640132 2771045.923101549 -89.365 39.41 +6318 23326 -89.6755 39.032000000000004 6647734.446087997 2633477.6979162646 +23326 4326 6647734.446087997 2633477.6979162646 -89.6755 39.032 +6318 23326 -89.05449999999999 39.788 6823097.340241611 2908925.1326538012 +23326 4326 6823097.340241611 2908925.1326538012 -89.05449999999999 39.78799999999999 +6318 23327 -89.0 38.974999999999994 7710000.000000001 2765105.696881969 +23327 4326 7710000.000000001 2765105.696881969 -89.0 38.97499999999999 +6318 23327 -89.576 38.7545 7545729.712017929 2685313.742664001 +23327 4326 7545729.712017929 2685313.742664001 -89.576 38.7545 +6318 23327 -88.424 39.195499999999996 7873255.175586274 2845935.779935367 +23327 4326 7873255.175586274 2845935.779935367 -88.42399999999999 39.1955 +6318 23328 -87.995 39.165000000000006 8695417.752633823 2779675.499914523 +23328 4326 8695417.752633823 2779675.499914523 -87.995 39.165 +6318 23328 -88.4315 38.8725 8571142.389655165 2673429.539316851 +23328 4326 8571142.389655165 2673429.539316851 -88.4315 38.8725 +6318 23328 -87.55850000000001 39.4575 8818669.29093387 2886522.0606035246 +23328 4326 8818669.29093387 2886522.0606035246 -87.55850000000001 39.457499999999996 +6318 23329 -89.985 38.54 9682290.635963647 2745230.675317261 +23329 4326 9682290.635963647 2745230.675317261 -89.985 38.54 +6318 23329 -90.34049999999999 38.126 9580046.684507215 2594636.4105141596 +23329 4326 9580046.684507215 2594636.4105141596 -90.34049999999999 38.126000000000005 +6318 23329 -89.62950000000001 38.954 9783368.605353244 2896228.8169649737 +23329 4326 9783368.605353244 2896228.8169649737 -89.62950000000001 38.95399999999999 +6318 23330 -89.19999999999999 38.474999999999994 313685.03148662986 3782108.7636884623 +23330 4326 313685.03148662986 3782108.7636884623 -89.19999999999999 38.47499999999998 +6318 23330 -89.65899999999999 38.155499999999996 181631.57583877814 3666151.2648479086 +23330 4326 181631.57583877814 3666151.2648479086 -89.65899999999999 38.15549999999999 +6318 23330 -88.741 38.7945 444579.53714810766 3898728.167169111 +23330 4326 444579.53714810766 3898728.167169111 -88.741 38.79449999999999 +6318 23331 -88.1 38.575 1261295.1440836191 3782108.9045220776 +23331 4326 1261295.1440836191 3782108.9045220776 -88.1 38.57500000000001 +6318 23331 -88.649 38.2645 1103721.4008350444 3669411.866318687 +23331 4326 1103721.4008350444 3669411.866318687 -88.649 38.264499999999984 +6318 23331 -87.55099999999999 38.8855 1417517.8321007877 3895748.7613274385 +23331 4326 1417517.8321007877 3895748.7613274385 -87.55099999999999 38.88550000000001 +6318 23332 -89.06 37.91 2363262.9230850665 3776660.308617464 +23332 4326 2363262.9230850665 3776660.308617464 -89.06 37.91000000000002 +6318 23332 -90.095 37.595 2063245.9975988956 3663970.1466823025 +23332 4326 2063245.9975988956 3663970.1466823025 -90.09500000000001 37.595 +6318 23332 -88.02499999999999 38.225 2660738.0286959847 3892673.3204975617 +23332 4326 2660738.0286959847 3892673.3204975617 -88.02499999999999 38.22499999999998 +6318 23333 -88.795 37.29 3672546.899452612 3871787.5447369586 +23333 4326 3672546.899452612 3871787.5447369586 -88.795 37.29 +6318 23333 -89.4565 37.001999999999995 3479486.5363314976 3767383.441781161 +23333 4326 3479486.5363314976 3767383.441781161 -89.4565 37.002 +6318 23333 -88.1335 37.578 3864141.767576316 3977539.2894562115 +23333 4326 3864141.767576316 3977539.2894562115 -88.1335 37.57799999999999 +4237 23700 19.505 47.16 684607.8460018405 201835.94499358293 +23700 4326 684607.8460018405 201835.94499358293 19.50387751389557 47.159735521825255 +4237 23700 16.4495 45.882000000000005 448242.30849233415 63024.061773480964 +23700 4326 448242.30849233415 63024.061773480964 16.448422389851387 45.881707683475945 +4237 23700 22.560499999999998 48.437999999999995 909815.948637447 349672.76311767066 +23700 4326 909815.948637447 349672.76311767066 22.559333126461237 48.437765199596996 +4755 23830 95.58 4.26 319890.1902566875 1971091.9583572196 +23830 4326 319890.1902566875 1971091.9583572196 95.58 4.26 +4755 23830 95.202 2.7209999999999996 278052.91603844904 1800867.487688985 +23830 4326 278052.91603844904 1800867.487688985 95.202 2.720999999999999 +4755 23830 95.958 5.7989999999999995 361479.78111167706 2141385.669986888 +23830 4326 361479.78111167706 2141385.669986888 95.958 5.798999999999997 +4755 23831 97.5 1.8050000000000002 200000.0 1699567.2720177523 +23831 4326 200000.0 1699567.2720177523 97.5 1.805 +4755 23831 96.15 -1.4485000000000001 49767.43472571956 1339804.0894006952 +23831 4326 49767.43472571956 1339804.0894006952 96.15 -1.4485000000000012 +4755 23831 98.85 5.0585 349698.64963887376 2059454.1329869945 +23831 4326 349698.64963887376 2059454.1329869945 98.85 5.0585 +4755 23832 100.5 0.06999999999999984 200000.0 1507739.4253262477 +23832 4326 200000.0 1507739.4253262477 100.50000000000001 0.06999999999999919 +4755 23832 99.15 -3.206 49953.43442222616 1145431.7351898863 +23832 4326 49953.43442222616 1145431.7351898863 99.15 -3.2060000000000004 +4755 23832 101.85 3.3459999999999996 350025.71341195086 1870051.921558711 +23832 4326 350025.71341195086 1870051.921558711 101.85 3.346 +4755 23833 103.5 -2.1550000000000002 200000.0 1261735.1364681586 +23833 4326 200000.0 1261735.1364681586 103.5 -2.155000000000001 +4755 23833 102.15 -5.6065000000000005 50434.09616774152 879935.3194104499 +23833 4326 50434.09616774152 879935.3194104499 102.15 -5.606499999999999 +4755 23833 104.85 1.296499999999999 350242.0542472629 1643385.5123733995 +23833 4326 350242.0542472629 1643385.5123733995 104.85 1.2964999999999982 +4755 23834 106.5 -1.8399999999999999 200000.0 1296562.9760341027 +23834 4326 200000.0 1296562.9760341027 106.5 -1.8399999999999992 +4755 23834 105.15 -7.195 50895.684632965975 704235.7451412512 +23834 4326 50895.684632965975 704235.7451412512 105.15 -7.194999999999999 +4755 23834 107.85 3.5149999999999997 349999.35606051236 1888742.9630602344 +23834 4326 349999.35606051236 1888742.9630602344 107.85 3.5149999999999992 +4755 23835 109.5 -2.0300000000000002 200000.0 1275555.7238427582 +23835 4326 200000.0 1275555.7238427582 109.50000000000001 -2.0300000000000007 +4755 23835 108.15 -7.682 51060.03245289906 650367.8432554147 +23835 4326 51060.03245289906 650367.8432554147 108.15 -7.682000000000002 +4755 23835 110.85 3.622 349981.9977635874 1900576.968968113 +23835 4326 349981.9977635874 1900576.968968113 110.85 3.6220000000000003 +4755 23836 112.5 -3.54 200000.0 1108601.2096211563 +23836 4326 200000.0 1108601.2096211563 112.5 -3.5399999999999996 +4755 23836 111.15 -8.157 51230.63563467894 597826.174136969 +23836 4326 51230.63563467894 597826.174136969 111.15 -8.157 +4755 23836 113.85 1.077 350253.9028404878 1619110.0033748832 +23836 4326 350253.9028404878 1619110.0033748832 113.85 1.076999999999999 +4755 23837 115.505 -2.3900000000000006 200556.06090903658 1235752.3685455457 +23837 4326 200556.06090903658 1235752.3685455457 115.505 -2.390000000000001 +4755 23837 114.1505 -8.474 51405.21253877718 562761.0730057451 +23837 4326 51405.21253877718 562761.0730057451 114.1505 -8.473999999999998 +4755 23837 116.85950000000001 3.693999999999999 351025.56653428444 1908541.6614927985 +23837 4326 351025.56653428444 1908541.6614927985 116.85950000000001 3.6939999999999986 +4755 23838 118.5 -2.8949999999999996 200000.0 1179916.7488955387 +23838 4326 200000.0 1179916.7488955387 118.50000000000001 -2.895 +4755 23838 117.15 -9.4245 51735.59797326679 457617.0200980628 +23838 4326 51735.59797326679 457617.0200980628 117.15000000000002 -9.4245 +4755 23838 119.85 3.634500000000001 349979.9360137746 1901959.448002758 +23838 4326 349979.9360137746 1901959.448002758 119.85 3.634500000000002 +4755 23839 121.5 -4.79 200000.0 970389.8113081094 +23839 4326 200000.0 970389.8113081094 121.5 -4.789999999999999 +4755 23839 120.15 -10.361 52155.051916697004 354016.60739181074 +23839 4326 52155.051916697004 354016.60739181074 120.15 -10.361 +4755 23839 122.85 0.7810000000000006 350266.4109474353 1586374.0607069882 +23839 4326 350266.4109474353 1586374.0607069882 122.85 0.7810000000000009 +4755 23840 124.5 -3.54 200000.0 1108601.2096211563 +23840 4326 200000.0 1108601.2096211563 124.49999999999999 -3.5399999999999996 +4755 23840 123.15 -10.182 52071.838872849185 373818.9257223045 +23840 4326 52071.838872849185 373818.9257223045 123.15 -10.181999999999999 +4755 23840 125.85 3.1019999999999985 350061.479457419 1843066.1458299477 +23840 4326 350061.479457419 1843066.1458299477 125.85000000000001 3.101999999999997 +4755 23841 127.5 -1.8650000000000002 200000.0 1293798.8665686087 +23841 4326 200000.0 1293798.8665686087 127.50000000000001 -1.8649999999999993 +4755 23841 126.15 -7.6745 51057.420298447396 651197.4396064322 +23841 4326 51057.420298447396 651197.4396064322 126.15000000000002 -7.674500000000003 +4755 23841 128.85 3.9444999999999997 349926.535311572 1936245.0469762408 +23841 4326 349926.535311572 1936245.0469762408 128.85 3.944500000000001 +4755 23842 130.5 -4.155 200000.0 1040601.7493931518 +23842 4326 200000.0 1040601.7493931518 130.5 -4.154999999999999 +4755 23842 129.15 -7.984500000000001 51167.50366120582 616907.22680795 +23842 4326 51167.50366120582 616907.22680795 129.15 -7.984500000000002 +4755 23842 131.85 -0.3254999999999999 350277.87412247434 1464001.6093250494 +23842 4326 350277.87412247434 1464001.6093250494 131.85 -0.3254999999999999 +4755 23843 133.5 -3.795 200000.0 1080406.4300389572 +23843 4326 200000.0 1080406.4300389572 133.5 -3.795 +4755 23843 132.15 -6.9495 50816.89476816368 731390.505916989 +23843 4326 50816.89476816368 731390.505916989 132.15 -6.9495 +4755 23843 134.85 -0.6405000000000003 350270.9533450694 1429164.4363060277 +23843 4326 350270.9533450694 1429164.4363060277 134.85000000000002 -0.6405 +4755 23844 136.5 -4.535 200000.0 998585.3031853382 +23844 4326 200000.0 998585.3031853382 136.5 -4.534999999999999 +4755 23844 135.15 -8.0945 51207.60682905922 604739.6165321028 +23844 4326 51207.60682905922 604739.6165321028 135.15 -8.0945 +4755 23844 137.85 -0.9755000000000003 350258.64089246665 1392115.3342724675 +23844 4326 350258.64089246665 1392115.3342724675 137.85 -0.9755 +4755 23845 139.505 -5.34 200554.14245136143 909575.2745602932 +23845 4326 200554.14245136143 909575.2745602932 139.505 -5.339999999999998 +4755 23845 138.1505 -8.805 51534.782231418474 526146.5903104517 +23845 4326 51534.782231418474 526146.5903104517 138.1505 -8.805 +4755 23845 140.8595 -1.8750000000000009 351257.4940786724 1292634.4997751822 +23845 4326 351257.4940786724 1292634.4997751822 140.8595 -1.8750000000000018 +4238 23846 95.58 4.26 786398.9440584984 471347.5238098957 +23846 4326 786398.9440584984 471347.5238098957 95.58021608430722 4.260025640223814 +4238 23846 95.202 2.7209999999999996 744814.3459460195 300978.9523260317 +23846 4326 744814.3459460195 300978.9523260317 95.20221686037512 2.7210223739640496 +4238 23846 95.958 5.7989999999999995 827622.8281630946 641842.9029406216 +23846 4326 827622.8281630946 641842.9029406216 95.95821536290636 5.7990289517128035 +4238 23847 99.0 2.71 500000.0 299539.678102225 +23847 4326 500000.0 299539.678102225 99.00022479032891 2.710026089700266 +4238 23847 96.3 0.271 199447.85134849622 29987.25944270604 +23847 4326 199447.85134849622 29987.25944270604 96.30022163870869 0.2710190787840474 +4238 23847 101.7 5.149 799348.9266753874 569769.8455856977 +23847 4326 799348.9266753874 569769.8455856977 101.70022838798407 5.1490328517766475 +4238 23848 105.0 2.055 499999.99999999715 227140.98927076304 +23848 4326 499999.99999999715 227140.98927076304 105.0002374424938 2.055029808055898 +4238 23848 102.3 0.20550000000000002 199446.43004398537 22739.415730968052 +23848 4326 199446.43004398537 22739.415730968052 102.30023356539705 0.2055245642259752 +4238 23848 107.7 3.9045 799861.5006211781 432054.0983085384 +23848 4326 799861.5006211781 432054.0983085384 107.70024162610027 3.9045347291029517 +4238 23849 111.0 2.125 500000.00000000425 234878.22196126863 +23849 4326 500000.00000000425 234878.22196126863 111.00024896087261 2.1250334991733935 +4238 23849 108.3 0.21250000000000002 199446.5632910254 23513.994423517295 +23849 4326 199446.5632910254 23513.994423517295 108.30024495884328 0.2125289172975138 +4238 23849 113.7 4.0375 799813.4350594829 446771.6427383879 +23849 4326 799813.4350594829 446771.6427383879 113.70025328418446 4.037537716325266 +4238 23850 117.0 2.185 500000.0 241510.1412863181 +23850 4326 500000.0 241510.1412863181 117.00025988242396 2.185035798041704 +4238 23850 114.3 0.21850000000000003 199446.6810547891 24177.919021483496 +23850 4326 199446.6810547891 24177.919021483496 114.30025576876014 0.21853195993374713 +4238 23850 119.7 4.1514999999999995 799770.9567429945 459386.7105799647 +23850 4326 799770.9567429945 459386.7105799647 119.70026432839892 4.1515392328021035 +4238 23851 122.85499999999999 1.92 483874.0527430943 212219.88603570152 +23851 4326 483874.0527430943 212219.88603570152 122.8552698527211 1.9200362458610691 +4238 23851 120.2855 0.192 197830.88694149378 21245.841366726912 +23851 4326 197830.88694149378 21245.841366726912 120.28576585791271 0.19203361293585777 +4238 23851 125.4245 3.6479999999999997 769324.3937408297 403583.16421128664 +23851 4326 769324.3937408297 403583.16421128664 125.42477410007726 3.648038489212763 +4238 23852 128.775 2.295 474982.9265401954 253670.64099042333 +23852 4326 474982.9265401954 253670.64099042333 128.77527910866544 2.2950362111430795 +4238 23852 126.7725 0.2295 252073.21989484542 25386.043986476507 +23852 4326 252073.21989484542 25386.043986476507 126.7727758638995 0.22953396625533382 +4238 23852 130.7775 4.3605 697254.7372916365 482209.92017222196 +23852 4326 697254.7372916365 482209.92017222196 130.77778267686563 4.360538193503432 +4755 23866 94.005 3.895 611580.4614499211 430587.662232115 +23866 4326 611580.4614499211 430587.662232115 94.005 3.8950000000000005 +4755 23866 92.2095 0.3895 412036.3514000601 43055.584914235646 +23866 4326 412036.3514000601 43055.584914235646 92.2095 0.38950000000000007 +4755 23866 95.8005 7.4005 809166.70941382 818996.9276198028 +23866 4326 809166.70941382 818996.9276198028 95.8005 7.400499999999998 +4755 23867 99.0 3.745 500000.0 413940.93706540315 +23867 4326 500000.0 413940.93706540315 99.0 3.745 +4755 23867 96.3 0.37450000000000006 199451.97758791543 41439.82046499721 +23867 4326 199451.97758791543 41439.82046499721 96.3 0.3745000000000001 +4755 23867 101.7 7.1155 798251.5827747089 787387.9897303757 +23867 4326 798251.5827747089 787387.9897303757 101.7 7.115500000000001 +4755 23868 105.0 3.47 499999.99999999715 383543.9653706045 +23868 4326 499999.99999999715 383543.9653706045 105.00000000000001 3.4700000000000006 +4755 23868 102.3 0.34700000000000003 199451.07404461666 38396.84239069991 +23868 4326 199451.07404461666 38396.84239069991 102.3 0.347 +4755 23868 107.7 6.593 798577.0214658263 729564.9950449365 +23868 4326 798577.0214658263 729564.9950449365 107.7 6.592999999999999 +4755 23869 111.0 3.685 500000.00000000425 407308.85470879084 +23869 4326 500000.00000000425 407308.85470879084 111.0 3.684999999999999 +4755 23869 108.3 0.36850000000000005 199451.77457679214 40775.897963733085 +23869 4326 199451.77457679214 40775.897963733085 108.3 0.36850000000000005 +4755 23869 113.7 7.0015 798324.6936129571 774771.9798608653 +23869 4326 798324.6936129571 774771.9798608653 113.7 7.0015 +4755 23870 117.0 2.185 500000.0 241509.32604739987 +23870 4326 500000.0 241509.32604739987 117.0 2.185 +4755 23870 114.3 0.21850000000000003 199447.7648931467 24177.837404515525 +23870 4326 199447.7648931467 24177.837404515525 114.3 0.21850000000000006 +4755 23870 119.7 4.1514999999999995 799769.8755467529 459385.1595616792 +23870 4326 799769.8755467529 459385.1595616792 119.7 4.1514999999999995 +4755 23871 123.0 2.74 500000.00000000285 302854.64359800844 +23871 4326 500000.00000000285 302854.64359800844 123.00000000000001 2.74 +4755 23871 120.3 0.274 199449.0096387621 30319.119415552566 +23871 4326 199449.0096387621 30319.119415552566 120.3 0.27399999999999997 +4755 23871 125.7 5.206 799321.0031112172 576075.5841922571 +23871 4326 799321.0031112172 576075.5841922571 125.69999999999999 5.2059999999999995 +4755 23872 129.0 3.34 500000.00000000285 369174.55031701503 +23872 4326 500000.00000000285 369174.55031701503 129.0 3.3399999999999994 +4755 23872 126.3 0.334 199450.67089087534 36958.3437133856 +23872 4326 199450.67089087534 36958.3437133856 126.30000000000001 0.33399999999999996 +4755 23872 131.7 6.345999999999999 798722.2659477657 702230.8196814049 +23872 4326 798722.2659477657 702230.8196814049 131.7 6.345999999999998 +4755 23877 99.0 -4.43 500000.0 9510342.109385315 +23877 4326 500000.0 9510342.109385315 99.0 -4.43 +4755 23877 96.3 -8.417 202666.30642037274 9068575.632148456 +23877 4326 202666.30642037274 9068575.632148456 96.3 -8.416999999999998 +4755 23877 101.7 -0.4429999999999996 800545.4723105452 9950980.397114987 +23877 4326 800545.4723105452 9950980.397114987 101.7 -0.44300000000000633 +4755 23878 105.005 -5.365 500553.9536958637 9406988.927061724 +23878 4326 500553.9536958637 9406988.927061724 105.005 -5.364999999999995 +4755 23878 102.3005 -10.1935 204220.35359973076 8871959.539162327 +23878 4326 204220.35359973076 8871959.539162327 102.3005 -10.193500000000002 +4755 23878 107.7095 -0.5365000000000002 801599.5545456714 9940633.800820835 +23878 4326 801599.5545456714 9940633.800820835 107.7095 -0.5364999999999946 +4755 23879 111.0 -6.035 500000.00000000425 9332926.454039818 +23879 4326 500000.00000000425 9332926.454039818 111.0 -6.035000000000003 +4755 23879 108.3 -11.4665 205414.0382586188 8731058.045834707 +23879 4326 205414.0382586188 8731058.045834707 108.3 -11.466499999999995 +4755 23879 113.7 -0.6035000000000004 800537.8235501552 9933220.46438451 +23879 4326 800537.8235501552 9933220.46438451 113.7 -0.6034999999999935 +4755 23880 117.005 -6.53 500552.7892346102 9278207.495387472 +23880 4326 500552.7892346102 9278207.495387472 117.005 -6.530000000000006 +4755 23880 114.3005 -12.407 206483.87263161637 8626954.010963067 +23880 4326 206483.87263161637 8626954.010963067 114.30050000000001 -12.406999999999995 +4755 23880 119.7095 -0.6530000000000005 801593.2217325555 9927742.530640636 +23880 4326 801593.2217325555 9927742.530640636 119.70950000000002 -0.6530000000000026 +4755 23881 123.005 -6.97 500552.2903320688 9229567.525173506 +23881 4326 500552.2903320688 9229567.525173506 123.00500000000001 -6.969999999999994 +4755 23881 120.3005 -13.251999999999999 207463.19545093627 8533415.656963557 +23881 4326 207463.19545093627 8533415.656963557 120.30049999999999 -13.251999999999995 +4755 23881 125.7095 -0.6880000000000006 801591.076861578 9923869.615981659 +23881 4326 801591.076861578 9923869.615981659 125.7095 -0.6880000000000063 +4755 23882 129.0 -4.725 500000.0000000028 9477733.659548106 +23882 4326 500000.0000000028 9477733.659548106 129.0 -4.724999999999992 +4755 23882 126.3 -8.9775 203108.6672598187 9006543.154231394 +23882 4326 203108.6672598187 9006543.154231394 126.30000000000001 -8.977499999999997 +4755 23882 131.7 -0.47250000000000014 800544.2424421762 9947716.11092834 +23882 4326 800544.2424421762 9947716.11092834 131.7 -0.4725000000000046 +4755 23883 135.005 -5.03 500554.2464370432 9444019.54515837 +23883 4326 500554.2464370432 9444019.54515837 135.005 -5.030000000000007 +4755 23883 132.3005 -9.557 203650.68370084552 8942406.77438083 +23883 4326 203650.68370084552 8942406.77438083 132.3005 -9.557 +4755 23883 137.7095 -0.5030000000000001 801601.1459386016 9944340.731845351 +23883 4326 801601.1459386016 9944340.731845351 137.7095 -0.5029999999999953 +4755 23884 139.73000000000002 -5.42 359297.03318262857 9400761.926005661 +23884 4326 359297.03318262857 9400761.926005661 139.73000000000002 -5.42000000000001 +4755 23884 138.173 -10.298 190341.53170711617 8860272.786072135 +23884 4326 190341.53170711617 8860272.786072135 138.173 -10.298 +4755 23884 141.287 -0.5419999999999998 531934.6294746828 9940091.94046816 +23884 4326 531934.6294746828 9940091.94046816 141.287 -0.5419999999999963 +4238 23887 100.12 -1.785 624576.3084534009 9802664.630392937 +23887 4326 624576.3084534009 9802664.630392937 100.12023110546798 -1.7849807219417402 +4238 23887 98.428 -3.3914999999999997 436460.16694416135 9625112.926198555 +23887 4326 436460.16694416135 9625112.926198555 98.42822944895751 -3.391485030609175 +4238 23887 101.812 -0.1785000000000001 813031.3536721978 9980246.373629812 +23887 4326 813031.3536721978 9980246.373629812 101.81223293824524 -0.17847650026060485 +4238 23888 105.0 -3.895 499999.99999999715 9569477.36352955 +23888 4326 499999.99999999715 9569477.36352955 105.00024185688785 -3.894979931578407 +4238 23888 102.3 -7.4005 201935.25868940202 9181069.00725207 +23888 4326 201935.25868940202 9181069.00725207 102.30024020216375 -7.400487599727219 +4238 23888 107.7 -0.38949999999999996 800548.5843621775 9956900.227759285 +23888 4326 800548.5843621775 9956900.227759285 107.70024422483704 -0.38947243304534557 +4238 23889 111.0 -4.335 500000.00000000425 9520841.461826459 +23889 4326 500000.00000000425 9520841.461826459 111.0002524114275 -4.3349764112673155 +4238 23889 108.3 -8.2365 202528.80415433255 9088548.817699252 +23889 4326 202528.80415433255 9088548.817699252 108.30025071098042 -8.236483750932022 +4238 23889 113.7 -0.43350000000000044 800546.9353149869 9952031.44595022 +23889 4326 800546.9353149869 9952031.44595022 113.70025498946059 -0.4334692687933979 +4238 23890 117.0 -5.075 500000.0 9439043.41382856 +23890 4326 500000.0 9439043.41382856 117.00026224161724 -5.07497441838653 +4238 23890 114.3 -9.6425 203669.10735390882 8932939.792818505 +23890 4326 203669.10735390882 8932939.792818505 114.30026060208058 -9.642481560744793 +4238 23890 119.7 -0.5075000000000003 800543.7642816685 9943843.039161913 +23890 4326 800543.7642816685 9943843.039161913 119.70026504427862 -0.5074674611240835 +4238 23891 123.0 -5.49 500000.0000000028 9393169.383249326 +23891 4326 500000.0000000028 9393169.383249326 123.00027093307465 -5.489973100551165 +4238 23891 120.3 -10.431000000000001 204386.38090493542 8845668.35030228 +23891 4326 204386.38090493542 8845668.35030228 120.30026896890061 -10.430979475260486 +4238 23891 125.7 -0.5490000000000013 800541.7676595997 9939250.891485602 +23891 4326 800541.7676595997 9939250.891485602 125.70027426001862 -0.5489669386190736 +4238 23892 129.0 -4.205 500000.00000000285 9535211.216343133 +23892 4326 500000.00000000285 9535211.216343133 129.00027861507218 -4.204971191349038 +4238 23892 126.3 -7.9895000000000005 202346.87293133716 9115884.625916194 +23892 4326 202346.87293133716 9115884.625916194 126.30027555485674 -7.989475223647097 +4238 23892 131.7 -0.42050000000000054 800547.4408852636 9953469.949710468 +23892 4326 800547.4408852636 9953469.949710468 131.70028259003104 -0.4204675723944996 +4238 23893 135.0 -4.39 500000.0 9514761.93552727 +23893 4326 500000.0 9514761.93552727 135.0002855362789 -4.389972248234624 +4238 23893 132.3 -8.08 202412.89310308214 9105868.904113956 +23893 4326 202412.89310308214 9105868.904113956 132.3002821625683 -8.079975005643167 +4238 23893 137.7 -0.6999999999999993 800533.1792012923 9922542.108717564 +23893 4326 800533.1792012923 9922542.108717564 137.70028979096023 -0.6999699583050406 +4238 23894 139.505 -5.34 334341.9239982 9409549.25794116 +23894 4326 334341.9239982 9409549.25794116 139.50528969034036 -5.33997442429369 +4238 23894 138.1505 -8.805 186509.01287004555 9025509.074733716 +23894 4326 186509.01287004555 9025509.074733716 138.15078745922787 -8.804976372845207 +4238 23894 140.8595 -1.8750000000000009 484374.1103353246 9792754.094390186 +23894 4326 484374.1103353246 9792754.094390186 140.85979254520052 -1.8749725010949063 +4239 23946 94.105 21.4 614521.9464009928 2366627.020387599 +4239 23946 92.3905 16.234 434872.96363180433 1794772.215191441 +4239 23946 95.8195 26.566000000000003 780832.0978111627 2941212.720012221 +4239 23947 99.0 17.09 500000.0 1889359.2167343306 +23947 4326 500000.0 1889359.2167343306 98.99677664630501 17.091444285437444 +4239 23947 96.291 6.776 200579.07164754614 749763.4618694924 +23947 4326 200579.07164754614 749763.4618694924 96.28823304178451 6.778225505498161 +4239 23947 101.709 27.404 767826.9553751277 3033843.560162937 +23947 4326 767826.9553751277 3033843.560162937 101.70516262752041 27.40458368803888 +4239 23948 103.82 12.23 371665.37237247464 1352159.546687669 +23948 4326 371665.37237247464 1352159.546687669 103.8162566053456 12.231896102985178 +4239 23948 102.182 6.641 188436.25713176624 734892.0198423883 +23948 4326 188436.25713176624 734892.0198423883 102.17851116024927 6.643272700590026 +4239 23948 105.458 17.819000000000003 548528.04979022 1970060.9243530307 +23948 4326 548528.04979022 1970060.9243530307 105.45395790021122 17.820503393668695 +4240 24047 99.67500000000001 13.045000000000002 573176.2583790219 1442094.7860250755 +24047 4326 573176.2583790219 1442094.7860250755 99.67016859333475 13.047160237434428 +4240 24047 97.57350000000001 6.3715 342245.561092642 704434.0354006775 +24047 4326 342245.561092642 704434.0354006775 97.56905434078764 6.374127675653342 +4240 24047 101.7765 19.718500000000002 790997.5747437028 2182533.9613855383 +24047 4326 790997.5747437028 2182533.9613855383 101.77121376376633 19.72017127325769 +4240 24048 103.82 12.23 371665.37237247464 1352159.546687669 +24048 4326 371665.37237247464 1352159.546687669 103.81468634876965 12.232316836256068 +4240 24048 102.182 6.641 188436.25713176624 734892.0198423883 +24048 4326 188436.25713176624 734892.0198423883 102.17698998614182 6.6436777533099995 +4240 24048 105.458 17.819000000000003 548528.04979022 1970060.9243530307 +24048 4326 548528.04979022 1970060.9243530307 105.45233161042214 17.820941588182976 +4241 24100 -77.30000000000001 18.11 445824.17171061406 440025.54133908317 +4241 24100 -78.31700000000001 17.687 91580.80797433558 287979.06702085433 +4241 24100 -76.283 18.532999999999998 798385.7442429627 594020.8226070697 +4242 24200 -77.30000000000001 18.11 218247.7586625612 162199.95945733954 +24200 4326 218247.7586625612 162199.95945733954 -77.29895416776529 18.11260283307424 +4242 24200 -78.31700000000001 17.687 110276.24488561627 115855.50945948531 +24200 4326 110276.24488561627 115855.50945948531 -78.31598952314286 17.689607929123493 +4242 24200 -76.283 18.532999999999998 325706.65508385864 209138.38495628873 +24200 4326 325706.65508385864 209138.38495628873 -76.2819178953898 18.53559697503205 +4144 24305 89.005 24.115000000000002 703748.5111047511 2668194.664858774 +24305 4326 703748.5111047511 2668194.664858774 89.00235047065414 24.11563198554831 +4144 24305 88.10950000000001 21.8425 614638.852436627 2415608.9618283026 +24305 4326 614638.852436627 2415608.9618283026 88.10700521877287 21.843303318255206 +4144 24305 89.9005 26.3875 789350.9590621293 2921610.518901634 +24305 4326 789350.9590621293 2921610.518901634 89.8976865807659 26.38795738523001 +4144 24306 91.33500000000001 22.905 329255.9886251595 2533760.364693516 +24306 4326 329255.9886251595 2533760.364693516 91.33208713512883 22.905772011713992 +4144 24306 90.1335 20.758499999999998 201558.96563075477 2297880.6583460937 +24306 4326 201558.96563075477 2297880.6583460937 90.13077573879404 20.759420612604014 +4144 24306 92.5365 25.051499999999997 453254.08054035 2770499.727575361 +24306 4326 453254.08054035 2770499.727575361 92.53338965425336 25.052122458030347 +4145 24311 63.435 27.425 542989.5208528863 3033340.836738122 +24311 4326 542989.5208528863 3033340.836738122 63.4355682472726 27.425163516130652 +4145 24311 61.1175 25.2245 310388.0442797352 2790912.9645604943 +24311 4326 310388.0442797352 2790912.9645604943 61.1183495269964 25.224858924354383 +4145 24311 65.7525 29.625500000000002 766479.3766791661 3280188.122358264 +24311 4326 766479.3766791661 3280188.122358264 65.75277549107183 29.625465603974696 +4145 24312 69.005 30.1 500481.68764638406 3329594.803829545 +24312 4326 500481.68764638406 3329594.803829545 69.00488787941606 30.10004542722035 +4145 24312 66.3005 24.286 226010.8351539044 2688335.055167811 +24312 4326 226010.8351539044 2688335.055167811 66.30066319280735 24.286478482580552 +4145 24312 71.7095 35.914 744459.8950057067 3977466.3406687705 +24312 4326 744459.8950057067 3977466.3406687705 71.70908309304602 35.913577992338936 +4145 24313 74.91499999999999 32.64 492028.6738802987 3611082.8455131585 +24313 4326 492028.6738802987 3611082.8455131585 74.9139794761939 32.639695547307724 +4145 24313 72.2915 28.653000000000002 235298.82410726196 3172287.143273106 +24313 4326 235298.82410726196 3172287.143273106 72.29086433855696 28.653037939320075 +4145 24313 77.5385 36.627 726953.7784732386 4056152.4757313794 +24313 4326 726953.7784732386 4056152.4757313794 77.53705588335464 36.62634549238248 +4146 24342 70.07 24.43 608455.5838853717 2702039.162901931 +24342 4326 608455.5838853717 2702039.162901931 70.06973917275369 24.430399621759076 +4146 24342 68.324 21.019000000000002 429761.28207824467 2324217.3805987868 +24342 4326 429761.28207824467 2324217.3805987868 68.3239779463589 21.019706226769284 +4146 24342 71.816 27.840999999999998 777306.8124444905 3082524.6206173156 +24342 4326 777306.8124444905 3082524.6206173156 71.8154864864644 27.84108227864612 +4146 24343 75.005 21.765 500516.8802956132 2406627.38592774 +24343 4326 500516.8802956132 2406627.38592774 75.00408609747862 21.765658941737005 +4146 24343 72.3005 9.394499999999999 203550.2530629225 1039529.945401836 +24343 4326 203550.2530629225 1039529.945401836 72.29997855035032 9.39615850374842 +4146 24343 77.7095 34.13549999999999 749818.154736944 3780179.669246835 +24343 4326 749818.154736944 3780179.669246835 77.70807361444295 34.13499839403493 +4146 24344 81.005 21.895 500516.41341134923 2421015.0261751637 +24344 4326 500516.41341134923 2421015.0261751637 81.003293942175 21.895695237474524 +4146 24344 78.3005 9.6505 203771.2469854086 1067861.1591281632 +24344 4326 203771.2469854086 1067861.1591281632 78.29922782238128 9.65215488422761 +4146 24344 83.7095 34.1395 749806.3719332948 3780623.3313335925 +24344 4326 749806.3719332948 3780623.3313335925 83.70719519969028 34.13908921893816 +4146 24345 87.005 23.16 500511.73223384004 2561029.748629199 +24345 4326 500511.73223384004 2561029.748629199 87.0024986260965 23.16066535102541 +4146 24345 84.3005 18.678 215289.79533635906 2067188.7018890625 +24345 4326 215289.79533635906 2067188.7018890625 84.29841028060642 18.679000105385875 +4146 24345 89.7095 27.642 767300.5165132966 3060232.7164625083 +24345 4326 767300.5165132966 3060232.7164625083 89.70654852261833 27.64232673478172 +4146 24346 93.005 25.68 500501.66764742555 2840019.6715827314 +24346 4326 500501.66764742555 2840019.6715827314 93.00168104136692 25.680568521679653 +4146 24346 90.3005 22.314 221936.8603608634 2469877.3972509666 +24346 4326 221936.8603608634 2469877.3972509666 90.29759939026145 22.314791337323392 +4146 24346 95.7095 29.046 763804.1182748459 3215850.479574418 +24346 4326 763804.1182748459 3215850.479574418 95.70573431443717 29.046351943482442 +4146 24347 96.71000000000001 28.285 275434.347798821 3130647.7000188436 +24347 4326 275434.347798821 3130647.7000188436 96.70613685418533 28.285444698298488 +4146 24347 96.071 27.218500000000002 209928.30952725472 3013787.099821373 +24347 4326 209928.30952725472 3013787.099821373 96.0672529593792 27.219016126290125 +4146 24347 97.349 29.351499999999998 339750.3057936844 3247797.0952843297 +24347 4326 339750.3057936844 3247797.0952843297 97.34501754595117 29.351873332577863 +4243 24370 74.095 36.325 2953457.4751007943 2225278.316065246 +4243 24370 71.4715 35.6545 2699386.9633987034 2130288.6808354706 +4243 24370 76.7185 36.9955 3202296.0358102415 2327389.6467713853 +4243 24371 71.25 31.795 3336115.1854172936 919736.79877193 +4243 24371 61.899 28.3795 2345610.5671372297 519393.14761656977 +4243 24371 80.601 35.2105 4251574.34892708 1402518.3018645286 +4243 24372 71.80000000000001 24.505000000000003 2756413.2678016815 821150.9448824096 +4243 24372 62.611000000000004 21.3505 1707275.3915157504 493324.5490464905 +4243 24372 80.989 27.6595 3753094.8601094037 1220992.955764853 +4243 24373 78.64500000000001 18.005000000000003 2843258.853962444 880310.6910131942 +4243 24373 70.9905 15.3005 1941517.8920304286 579615.7022627448 +4243 24373 86.29950000000001 20.709500000000002 3716850.1733785127 1219555.557392268 +4243 24374 77.17 11.515 2662811.669475312 943133.0185831345 +4243 24374 74.263 8.3695 2308570.060492711 568313.2775843426 +4243 24374 80.077 14.660499999999999 3009068.3124213726 1321595.5303639187 +4144 24375 90.34 23.58 2777876.209644704 646648.6692455664 +24375 4326 2777876.209644704 646648.6692455664 90.33719524207174 23.580699557151267 +4144 24375 88.24300000000001 20.826 2559819.229880249 342586.6862727577 +24375 4326 2559819.229880249 342586.6862727577 88.24050590111351 20.826886570197964 +4144 24375 92.437 26.334 2986139.112649862 953616.146956471 +24375 4326 2986139.112649862 953616.146956471 92.43386856726949 26.334514536985495 +4145 24376 69.345 31.795 2870405.5719163837 837124.6081918402 +24376 4326 2870405.5719163837 837124.6081918402 69.34485644414704 31.79491417589562 +4145 24376 61.7085 28.3795 2126161.0143683404 476018.09932256036 +24376 4326 2126161.0143683404 476018.09932256036 61.70922225287456 28.379723105837723 +4145 24376 76.9815 35.2105 3559842.249211966 1249175.914595628 +24376 4326 3559842.249211966 1249175.914595628 76.98042773195051 35.21014024413116 +4145 24377 66.75 25.825000000000003 2017620.9866715968 915165.34076395 +24377 4326 2017620.9866715968 915165.34076395 66.7501227005061 25.82536879116473 +4145 24377 62.106 23.8585 1534110.1734476744 732487.2991035396 +24377 4326 1534110.1734476744 732487.2991035396 62.10662841873641 23.859036566386802 +4145 24377 71.39399999999999 27.7915 2486612.149674452 1115231.7372083692 +24377 4326 2486612.149674452 1115231.7372083692 71.39359966239998 27.791709545955737 +4146 24378 75.995 31.755 3499000.4635220505 860238.4817559908 +24378 4326 3499000.4635220505 860238.4817559908 75.99385888179263 31.754722890632795 +4146 24378 70.91449999999999 28.3755 3029175.8047120445 461274.7679642458 +24378 4326 3029175.8047120445 461274.7679642458 70.91411101473298 28.37552756890564 +4146 24378 81.0755 35.134499999999996 3931546.6718036435 1279281.8891757359 +24378 4326 3931546.6718036435 1279281.8891757359 81.07355516342754 35.13394205030106 +4146 24379 75.07 24.505000000000003 2851529.7766433368 749428.0269337737 +24379 4326 2851529.7766433368 749428.0269337737 75.06905852588747 24.505412611841745 +4146 24379 68.824 21.3505 2205438.0850872463 410210.9627199846 +24379 4326 2205438.0850872463 410210.9627199846 68.82391118917118 21.35117705386283 +4146 24379 81.316 27.6595 3464010.840339479 1118237.2607037593 +24379 4326 3464010.840339479 1118237.2607037593 81.31417072299718 27.659680185103884 +4146 24380 89.71000000000001 25.235 2714012.7896127463 829790.5105161532 +24380 4326 2714012.7896127463 829790.5105161532 89.70710915483099 25.23553647957838 +4146 24380 82.771 21.4235 1992777.1647799038 428417.30672554945 +24380 4326 1992777.1647799038 428417.30672554945 82.76907048179851 21.424255001018707 +4146 24380 96.649 29.046499999999998 3390540.7129588462 1268174.7260886526 +24380 4326 3390540.7129588462 1268174.7260886526 96.64511644564907 29.046878654254286 +4146 24381 78.64500000000001 18.005000000000003 2599871.6311788866 804954.7753995066 +24381 4326 2599871.6311788866 804954.7753995066 78.64363725518852 18.006003943641186 +4146 24381 70.9905 15.3005 1775321.0482221814 529999.7287363396 +24381 4326 1775321.0482221814 529999.7287363396 70.99013520372038 15.301694251916896 +4146 24381 86.29950000000001 20.709500000000002 3398682.2232441986 1115159.7723405259 +24381 4326 3398682.2232441986 1115159.7723405259 86.29712998636914 20.710359369542047 +4243 24382 91.58500000000001 25.235 3174425.9514174676 908493.652776791 +4243 24382 82.9585 21.4235 2200596.159615013 467361.58147767995 +4243 24382 100.2115 29.046499999999998 4086623.4305869476 1411353.6137422908 +4146 24383 77.17 11.515 2434870.996359135 862399.4174943608 +24383 4326 2434870.996359135 862399.4174943608 77.16886152449082 11.516510901994565 +4146 24383 74.263 8.3695 2110953.000476702 519664.8085643562 +24383 4326 2110953.000476702 519664.8085643562 74.26223440829622 8.371238167971633 +4146 24383 80.077 14.660499999999999 2751487.551275411 1208464.9705631835 +24383 4326 2751487.551275411 1208464.9705631835 80.07547976404265 14.661783074543496 +4245 24500 103.83 1.2999999999999998 27440.39334461893 31365.878825968113 +24500 4326 27440.39334461893 31365.878825968113 103.82826811127563 1.299950220670115 +4245 24500 103.614 1.1469999999999998 3403.1637992515352 14450.283114667038 +24500 4326 3403.1637992515352 14450.283114667038 103.61229630065992 1.1469612740023931 +4245 24500 104.04599999999999 1.4529999999999998 51474.74728357092 48283.4991854556 +24500 4326 51474.74728357092 48283.4991854556 104.04423993083658 1.4529392036376911 +4245 24547 100.80000000000001 4.505 699686.792057496 498157.80555960315 +24547 4326 699686.792057496 498157.80555960315 100.79866048788291 4.504709103496609 +4245 24547 99.711 2.5115 579032.7187411323 277599.01422101073 +24547 4326 579032.7187411323 277599.01422101073 99.70980677060544 2.5113569765968946 +4245 24547 101.88900000000001 6.4985 819512.538548731 719169.4282277052 +24547 4326 819512.538548731 719169.4282277052 101.88751227526818 6.498062663233453 +4245 24548 103.91 4.51 379091.9069214145 498554.4570267849 +24548 4326 379091.9069214145 498554.4570267849 103.90825278455446 4.509715340650742 +4245 24548 102.191 1.54 187455.69921261544 170410.16233693928 +24548 4326 187455.69921261544 170410.16233693928 102.18948170877066 1.5399314177945496 +4245 24548 105.62899999999999 7.4799999999999995 569392.5416586698 826800.0194355005 +24548 4326 569392.5416586698 826800.0194355005 105.6270188840647 7.479503977648871 +4246 24600 47.51 29.310000000000002 1743899.0910657654 815668.152318802 +24600 4326 1743899.0910657654 815668.152318802 47.51084565998451 29.31297703029601 +4246 24600 46.637 28.608 1660272.3978040824 736173.708358702 +24600 4326 1660272.3978040824 736173.708358702 46.63778588020399 28.6110183449604 +4246 24600 48.382999999999996 30.012 1826236.4554554103 895783.3240580619 +24600 4326 1826236.4554554103 895783.3240580619 48.38390619368614 30.01493587792737 +4247 24718 -72.685 9.32 754299.4355883249 1031074.7119216796 +4247 24718 -73.31049999999999 7.25 686539.6629182443 801741.6983878984 +4247 24718 -72.0595 11.389999999999999 820945.4445334412 1260744.3552173453 +4247 24719 -69.0 6.49 500000.0 717378.657962724 +24719 4326 500000.0 717378.657962724 -69.00195005365067 6.4868051595656695 +4247 24719 -71.7 1.306 199511.43025795143 144515.522367033 +24719 4326 199511.43025795143 144515.522367033 -71.70202117560325 1.3027659599155927 +4247 24719 -66.3 11.674 794380.4726950533 1291924.6471033925 +24719 4326 794380.4726950533 1291924.6471033925 -66.30188914709947 11.670891489406111 +4247 24720 -62.9 5.935 511068.6900150948 656027.5645122255 +24720 4326 511068.6900150948 656027.5645122255 -62.9017438340174 5.931820114735701 +4247 24720 -65.69 1.1695 200609.84573964274 129410.00030055328 +24720 4326 200609.84573964274 129410.00030055328 -65.69183039849192 1.1662697413753174 +4247 24720 -60.11 10.7005 816167.4135565758 1184358.801520222 +24720 4326 816167.4135565758 1184358.801520222 -60.11166370678889 10.697409893361582 +4248 24817 -79.09 0.725 712566.2696526131 80180.04142966539 +24817 4326 712566.2696526131 80180.04142966539 -79.09215642094203 0.7217055083448829 +4248 24817 -80.07100000000001 0.0725 603382.9871301382 8014.576114856489 +24817 4326 603382.9871301382 8014.576114856489 -80.07319947351978 0.06920524284553373 +4248 24817 -78.109 1.3775 821753.5543513347 152452.35560654607 +24817 4326 821753.5543513347 152452.35560654607 -78.11111471751903 1.3742068427536986 +4248 24818 -74.995 5.81 500553.5575998451 642208.7035362276 +24818 4326 500553.5575998451 642208.7035362276 -74.99706434800534 5.806712742050293 +4248 24818 -77.6995 0.581 199504.8310604727 64290.499405247785 +24818 4326 199504.8310604727 64290.499405247785 -77.70165177066944 0.5776489272150099 +4248 24818 -72.2905 11.038999999999998 796071.979805971 1221646.7825836227 +24818 4326 796071.979805971 1221646.7825836227 -72.2924879348824 11.03582570783906 +4248 24819 -69.0 6.705 500000.0 741146.0402171189 +24819 4326 500000.0 741146.0402171189 -69.00193020106414 6.7017593064271646 +4248 24819 -71.7 1.3275000000000001 199514.00849463453 146894.61301146093 +24819 4326 199514.00849463453 146894.61301146093 -71.702028577669 1.3241613714403027 +4248 24819 -66.3 12.0825 793941.5485258745 1337142.2133416224 +24819 4326 793941.5485258745 1337142.2133416224 -66.30184243091722 12.079407531235944 +4248 24820 -62.995000000000005 5.935 500553.4342246174 656026.5682400708 +24820 4326 500553.4342246174 656026.5682400708 -62.996664299683694 5.931763068517486 +4248 24820 -65.6995 1.1695 199551.73370933416 129411.01678763659 +24820 4326 199551.73370933416 129411.01678763659 -65.70127607107234 1.1661633843455763 +4248 24820 -60.2905 10.7005 796406.35836282 1184179.363787101 +24820 4326 796406.35836282 1184179.363787101 -60.292058406537684 10.697401388055358 +4248 24821 -58.235 4.88 363054.3361635287 539531.1928005893 +24821 4326 363054.3361635287 539531.1928005893 -58.23652960386709 4.876760250313336 +4248 24821 -59.8235 1.55 185789.0956017932 171533.4018417412 +24821 4326 185789.0956017932 171533.4018417412 -59.82509681979319 1.5466898287608182 +4248 24821 -56.646499999999996 8.21 538937.0002231502 907541.0762271946 +24821 4326 538937.0002231502 907541.0762271946 -56.647965703677855 8.206847958632569 +4248 24877 -79.705 -5.265 643514.9095169946 9417887.645206926 +24877 4326 643514.9095169946 9417887.645206926 -79.70727406618141 -5.268420847435365 +4248 24877 -81.23949999999999 -10.003499999999999 473751.0402367174 8894178.510888053 +24877 4326 473751.0402367174 8894178.510888053 -81.2418530617609 -10.006906438817277 +4248 24877 -78.1705 -0.5265000000000004 814980.5940621785 9941733.769441692 +24877 4326 814980.5940621785 9941733.769441692 -78.17271001697439 -0.5299042754329931 +4248 24878 -74.995 -21.75 500517.0252703818 7594813.194091522 +24878 4326 500517.0252703818 7594813.194091522 -74.99725122909989 -21.75340249277169 +4248 24878 -77.6995 -41.325 274065.630426603 5421567.703558719 +24878 4326 274065.630426603 5421567.703558719 -77.70241574142882 -41.32810812292261 +4248 24878 -72.2905 -2.1750000000000043 801408.4149662 9759322.745284466 +24878 4326 801408.4149662 9759322.745284466 -72.29248776530939 -2.1784227440273027 +4248 24879 -69.0 -22.82 500000.0 7476373.032505389 +24879 4326 500000.0 7476373.032505389 -69.00200819829867 -22.823474582044813 +4248 24879 -71.7 -41.432 274394.14664148155 5409685.567381984 +24879 4326 274394.14664148155 5409685.567381984 -71.70261400557511 -41.43525086461636 +4248 24879 -66.3 -4.2080000000000055 799760.2072025507 9534357.53404474 +24879 4326 799760.2072025507 9534357.53404474 -66.30174174284936 -4.211453690538542 +4248 24880 -63.0 -16.27 500000.0 8201176.777311835 +24880 4326 500000.0 8201176.777311835 -63.00145217880478 -16.27365728670692 +4248 24880 -65.7 -22.21 221630.301502841 7541416.362957535 +24880 4326 221630.301502841 7541416.362957535 -65.70163627399602 -22.213631925807316 +4248 24880 -60.3 -10.33 795719.3176739502 8856838.12285005 +24880 4326 795719.3176739502 8856838.12285005 -60.301290872844355 -10.333642024743227 +4248 24881 -58.760000000000005 -18.22 313882.908298519 7984555.535192711 +24881 4326 313882.908298519 7984555.535192711 -58.761260725016044 -18.22369033419546 +4248 24881 -59.876 -19.975 198997.99229513854 7788675.254506124 +24881 4326 198997.99229513854 7788675.254506124 -59.87732981493745 -19.97868525523192 +4248 24881 -57.644000000000005 -16.465 431255.0332923664 8179496.40824974 +24881 4326 431255.0332923664 8179496.40824974 -57.6451936995878 -16.46869156962031 +4248 24882 -49.82 2.2749999999999995 631216.1312698462 10251513.563125016 +24882 4326 631216.1312698462 10251513.563125016 -49.82138778326063 2.2717374718133265 +4248 24882 -51.458 -0.7175 449037.4844161854 9920691.237259248 +24882 4326 449037.4844161854 9920691.237259248 -51.45945546553862 -0.7208057422948481 +4248 24882 -48.182 5.267499999999999 812394.4281437546 10582945.572464466 +24882 4326 812394.4281437546 10582945.572464466 -48.18332245559809 5.264294369098584 +4248 24891 -80.205 -5.85 254665.24497268067 1443411.4574278474 +24891 4326 254665.24497268067 1443411.4574278474 -80.20721376344902 -5.853445003065796 +4248 24891 -81.2895 -8.073 134987.1551624111 1197531.602637557 +24891 4326 134987.1551624111 1197531.602637557 -81.29176156976652 -8.076437169075993 +4248 24891 -79.12049999999999 -3.627 375254.48907764565 1689088.0384942624 +24891 4326 375254.48907764565 1689088.0384942624 -79.12266884287625 -3.6304449835602535 +4248 24892 -76.0 -8.3 720000.0 1172612.8801173063 +24892 4326 720000.0 1172612.8801173063 -76.00207310491938 -8.30346537721343 +4248 24892 -78.7 -15.743 430730.93228195544 347930.2297105412 +24892 4326 430730.93228195544 347930.2297105412 -78.7022328989401 -15.746421263729621 +4248 24892 -73.3 -0.8570000000000011 1020451.6000539928 1995033.1101228797 +24892 4326 1020451.6000539928 1995033.1101228797 -73.30194906015505 -0.8604360491092163 +4248 24893 -70.83500000000001 -10.265 1287316.5572508299 955489.8381132975 +24893 4326 1287316.5572508299 955489.8381132975 -70.83695882328541 -10.268474750396969 +4248 24893 -72.7835 -17.5775 1081647.8948043874 145365.07712675573 +24893 4326 1081647.8948043874 145365.07712675573 -72.78560352977215 -17.58095329369083 +4248 24893 -68.8865 -2.9525000000000006 1503323.6459764505 1763700.680211726 +24893 4326 1503323.6459764505 1763700.680211726 -68.88834963370878 -2.9559354655556236 +4250 25000 -0.8449999999999998 6.279999999999999 291467.22242225596 178351.32156409905 +25000 4326 291467.22242225596 178351.32156409905 -0.8447552798298841 6.282715573927797 +4250 25000 -3.4955 1.888 -3352.1818931566086 -306955.43155240174 +25000 4326 -3352.1818931566086 -306955.43155240174 -3.4953110723235965 1.8911217690011564 +4250 25000 1.8055000000000003 10.672 581304.0275408686 665327.4138254264 +25000 4326 581304.0275408686 665327.4138254264 1.8058023721670011 10.674303151041173 +4252 25231 1.135 7.025 293980.4882983029 776851.7196210403 +4252 25231 -0.02149999999999999 3.3215000000000003 164182.05637593823 367609.2646591889 +4252 25231 2.2914999999999996 10.7285 422527.4112499227 1185941.8325924182 +4253 25391 117.445 8.535 548990.778152396 943742.7519971944 +25391 4326 548990.778152396 943742.7519971944 117.44639442366154 8.533924013080819 +4253 25391 116.9455 7.8285 493989.4869390558 865587.0026102903 +25391 4326 493989.4869390558 865587.0026102903 116.94689136138877 7.8274747894702905 +4253 25391 117.9445 9.2415 603786.0346682151 1021982.6796879688 +25391 4326 603786.0346682151 1021982.6796879688 117.94589760214205 9.240373394657915 +4253 25392 119.035 10.2 503834.8626163925 1127849.5753164322 +25392 4326 503834.8626163925 1127849.5753164322 119.03640232161224 10.198803257676364 +4253 25392 118.1035 8.958 401411.2497056927 990613.1985908459 +25392 4326 401411.2497056927 990613.1985908459 118.10489664051799 8.956892329342901 +4253 25392 119.9665 11.442 605466.6018153768 1265392.6580301207 +25392 4326 605466.6018153768 1265392.6580301207 119.96790833244437 11.440714847684225 +4253 25393 120.955 12.219999999999999 495103.5736172693 1351270.314646917 +25393 4326 495103.5736172693 1351270.314646917 120.95641215064124 12.2186578012063 +4253 25393 119.8255 5.713 369896.39999615424 631792.857941852 +25393 4326 369896.39999615424 631792.857941852 119.82688736171268 5.712116910904572 +4253 25393 122.08449999999999 18.726999999999997 614375.5355251804 2071569.310312648 +25393 4326 614375.5355251804 2071569.310312648 122.08595628953691 18.72524436423894 +4253 25394 123.015 12.465 501630.6254400105 1378370.1370460682 +25394 4326 501630.6254400105 1378370.1370460682 123.01641176315836 12.463630712856004 +4253 25394 121.86749999999999 6.961499999999999 374850.7926551595 769863.4148790501 +25394 4326 374850.7926551595 769863.4148790501 121.86889002676958 6.96052101094963 +4253 25394 124.16250000000001 17.9685 623138.8892430125 1987661.6754884834 +25394 4326 623138.8892430125 1987661.6754884834 124.16394714318992 17.96677298592546 +4253 25395 125.19 9.825 520841.77212147025 1086381.904701225 +25395 4326 520841.77212147025 1086381.904701225 125.19140585124585 9.823618932778814 +4253 25395 123.876 5.9325 375539.51105744205 656057.1169834497 +25395 4326 375539.51105744205 656057.1169834497 123.87739491519619 5.9314035535922995 +4253 25395 126.504 13.7175 662690.3156899897 1517427.2473753188 +25395 4326 662690.3156899897 1517427.2473753188 126.50542283527359 13.715846501073285 +4258 25828 -14.045000000000002 53.685 563070.5815559236 5948899.088365438 +25828 4326 563070.5815559236 5948899.088365438 -14.045000000000003 53.684999999999995 +4258 25828 -15.8945 36.8055 420207.72232269973 4073669.3786720587 +25828 4326 420207.72232269973 4073669.3786720587 -15.894500000000003 36.8055 +4258 25828 -12.195500000000001 70.5645 604118.1890119871 7831230.264494488 +25828 4326 604118.1890119871 7831230.264494488 -12.195500000000003 70.5645 +4258 25829 -9.0 54.519999999999996 500000.0 6041379.62540723 +25829 4326 500000.0 6041379.62540723 -9.0 54.519999999999996 +4258 25829 -11.7 36.870999999999995 259334.22875185 4083966.0046406877 +25829 4326 259334.22875185 4083966.0046406877 -11.7 36.87100000000001 +4258 25829 -6.300000000000001 72.169 592251.1812641958 8009852.859520558 +25829 4326 592251.1812641958 8009852.859520558 -6.300000000000002 72.169 +4258 25830 -2.995 57.875 500296.57524808653 6414793.670332485 +25830 4326 500296.57524808653 6414793.670332485 -2.9949999999999997 57.875 +4258 25830 -5.6995000000000005 37.521499999999996 261436.43383339047 4156150.955452261 +25830 4326 261436.43383339047 4156150.955452261 -5.6995000000000005 37.52149999999999 +4258 25830 -0.29050000000000065 78.2285 561685.8328822566 8685298.680144897 +25830 4326 561685.8328822566 8685298.680144897 -0.2905000000000049 78.2285 +4258 25831 3.005 59.725 500281.1997118188 6620785.720680849 +25831 4326 500281.1997118188 6620785.720680849 3.005 59.725 +4258 25831 0.3005 39.2725 267127.53852953465 4350490.967358435 +25831 4326 267127.53852953465 4350490.967358435 0.30049999999999993 39.2725 +4258 25831 5.709499999999999 80.17750000000001 551584.6058092709 8902599.700418724 +25831 4326 551584.6058092709 8902599.700418724 5.709499999999996 80.17750000000001 +4258 25832 9.004999999999999 60.27 500276.6127925036 6681481.103924277 +25832 4326 500276.6127925036 6681481.103924277 9.004999999999999 60.26999999999999 +4258 25832 6.3004999999999995 38.904 265911.53731539764 4309588.114474267 +25832 4326 265911.53731539764 4309588.114474267 6.3004999999999995 38.904 +4258 25832 11.709499999999998 81.636 543985.2429991267 9065226.109379351 +25832 4326 543985.2429991267 9065226.109379351 11.709499999999995 81.63599999999998 +4258 25833 15.005 59.400000000000006 500283.92278602143 6584593.637362994 +25833 4326 500283.92278602143 6584593.637362994 15.005000000000003 59.400000000000006 +4258 25833 12.3005 37.251 260577.04308186483 4126132.724717411 +25833 4326 260577.04308186483 4126132.724717411 12.3005 37.251 +4258 25833 17.709500000000002 81.549 544439.4085917076 9055525.112356884 +25833 4326 544439.4085917076 9055525.112356884 17.7095 81.54899999999999 +4258 25834 21.005000000000003 59.385000000000005 500284.0482435887 6582923.2772595575 +25834 4326 500284.0482435887 6582923.2772595575 21.005000000000003 59.385 +4258 25834 18.3005 37.2225 260486.80845912802 4122970.052969486 +25834 4326 260486.80845912802 4122970.052969486 18.3005 37.2225 +4258 25834 23.709500000000002 81.5475 544447.2381237531 9055357.854082229 +25834 4326 544447.2381237531 9055357.854082229 23.7095 81.54749999999999 +4258 25835 27.005000000000003 62.625 500256.5059596471 6943808.942379209 +25835 4326 500256.5059596471 6943808.942379209 27.005000000000003 62.624999999999986 +4258 25835 24.3005 43.3785 281318.90635814855 4806387.52155021 +25835 4326 281318.90635814855 4806387.52155021 24.3005 43.3785 +4258 25835 29.709500000000002 81.8715 542755.3501657706 9091485.87127052 +25835 4326 542755.3501657706 9091485.87127052 29.7095 81.87149999999998 +4258 25836 33.004999999999995 63.285000000000004 500250.7905840169 7017343.734243218 +25836 4326 500250.7905840169 7017343.734243218 33.004999999999995 63.28500000000001 +4258 25836 30.3005 44.6325 285881.3060813852 4945672.235231911 +25836 4326 285881.3060813852 4945672.235231911 30.300500000000003 44.6325 +4258 25836 35.7095 81.9375 542410.5343484536 9098845.337949721 +25836 4326 542410.5343484536 9098845.337949721 35.70949999999999 81.93749999999999 +4258 25837 37.004999999999995 76.03 446247.5544441172 8439441.106222108 +25837 4326 446247.5544441172 8439441.106222108 37.004999999999995 76.03 +4258 25837 36.1005 73.294 407000.1760679933 8135531.226743587 +25837 4326 407000.1760679933 8135531.226743587 36.1005 73.294 +4258 25837 37.9095 78.76599999999999 476284.7618366149 8744079.260121493 +25837 4326 476284.7618366149 8744079.260121493 37.9095 78.76599999999999 +4258 25884 23.63 56.945 477490.20149976853 6311324.522135305 +25884 4326 477490.20149976853 6311324.522135305 23.63 56.94499999999999 +4258 25884 19.480999999999998 54.1955 205267.76531127945 6014708.147934948 +25884 4326 205267.76531127945 6014708.147934948 19.480999999999998 54.1955 +4258 25884 27.778999999999996 59.6945 712648.4772249453 6623446.878350759 +25884 4326 712648.4772249453 6623446.878350759 27.778999999999996 59.694499999999984 +4259 25932 11.45 -5.545 771448.8824818352 9386523.919549571 +25932 4326 771448.8824818352 9386523.919549571 11.450407833164496 -5.545962798093093 +4259 25932 10.622 -5.9905 679539.638682245 9337573.00805905 +25932 4326 679539.638682245 9337573.00805905 10.622375464435455 -5.991467613039478 +4259 25932 12.277999999999999 -5.0995 863536.0605730759 9435405.804451713 +25932 4326 863536.0605730759 9435405.804451713 12.27844009274588 -5.1004579977534 +4261 26191 -5.43 33.73 497220.5851460204 347673.8559640251 +26191 4326 497220.5851460204 347673.8559640251 -5.428400096236891 33.7273604584259 +4261 26191 -9.408 31.714 120179.78715799941 131465.33084544778 +26191 4326 120179.78715799941 131465.33084544778 -9.406426986491782 31.711513838090937 +4261 26191 -1.452 35.745999999999995 857225.445525108 578067.7175043394 +26191 4326 857225.445525108 578067.7175043394 -1.4503777841516947 35.7432161869352 +4261 26192 -8.415 29.585 208026.86551931954 291064.57346966374 +26192 4326 208026.86551931954 291064.57346966374 -8.413462511165143 29.582624425225944 +4261 26192 -12.7575 27.8525 -224306.9174362576 118341.43589620496 +26192 4326 -224306.9174362576 118341.43589620496 -12.755984834125984 27.850282781184745 +4261 26192 -4.0725 31.317500000000003 626363.016521203 479989.0162355274 +26192 4326 626363.016521203 479989.0162355274 -4.070946966875999 31.314970591827127 +4261 26194 -12.04 25.98 535628.7742177768 403651.248724818 +26194 4326 535628.7742177768 403651.248724818 -12.038509501461359 25.9779024345747 +4261 26194 -15.082 24.459 219220.69217746414 254744.63626113784 +26194 4326 219220.69217746414 254744.63626113784 -15.080530063519959 24.457043172739247 +4261 26194 -8.998000000000001 27.501 844542.2587501248 560088.5124946835 +26194 4326 844542.2587501248 560088.5124946835 -8.996491597472081 27.49876503465256 +4261 26195 -14.58 22.509999999999998 556409.9251032526 430043.5820873317 +4261 26195 -16.902 20.89 304397.1237910853 267735.75012518483 +4261 26195 -12.258 24.13 803100.0326170116 596426.6573679693 +4262 26237 39.875 15.23 593959.9356382844 1683791.1810463294 +26237 4326 593959.9356382844 1683791.1810463294 39.874079843965035 15.229046293328699 +4262 26237 36.7835 12.647 259277.92964718648 1398984.433527437 +26237 4326 259277.92964718648 1398984.433527437 36.78296369191326 12.646292430959523 +4262 26237 42.9665 17.813000000000002 920570.2468475925 1973764.5797489383 +26237 4326 920570.2468475925 1973764.5797489383 42.96518733858957 17.81180977364666 +4263 26331 4.33 4.029999999999999 647648.0605309424 445522.9177707493 +26331 4326 647648.0605309424 445522.9177707493 4.329312050867561 4.030648127585091 +4263 26331 2.827 2.131 480762.2866047988 235519.9792045833 +26331 4326 480762.2866047988 235519.9792045833 2.826290008849823 2.1318271039437438 +4263 26331 5.833 5.928999999999999 813698.5059422776 656097.585073361 +26331 4326 813698.5059422776 656097.585073361 5.832333917377715 5.929471032084157 +4263 26332 6.91 3.145 267727.5782164441 347820.9724986952 +26332 4326 267727.5782164441 347820.9724986952 6.909353765298741 3.145733475996151 +4263 26332 6.091 2.6635 176503.4093241897 294753.644845618 +26332 4326 176503.4093241897 294753.644845618 6.090340866569015 2.664278355249828 +4263 26332 7.729 3.6265 358836.7583434576 400904.7259640331 +26332 4326 358836.7583434576 400904.7259640331 7.728366762621851 3.6271887622771044 +4263 26391 4.595 8.735 241189.34645072123 523459.9846379839 +26391 4326 241189.34645072123 523459.9846379839 4.594224527583377 8.735280579514145 +4263 26391 2.8805 4.0865 50929.51857437455 9743.015200435904 +26391 4326 50929.51857437455 9743.015200435904 2.87970514873869 4.08721475968067 +4263 26391 6.3095 13.3835 426719.9480506128 1038199.4492223988 +26391 4326 426719.9480506128 1038199.4492223988 6.308739947979508 13.383366688834531 +4263 26392 8.504999999999999 8.549999999999999 671104.305161403 503004.7193539062 +26392 4326 671104.305161403 503004.7193539062 8.504288124701732 8.550305200722873 +4263 26392 6.7005 4.068 470749.5274823273 7739.498458685419 +26392 4326 470749.5274823273 7739.498458685419 6.69976486433749 4.068720153680683 +4263 26392 10.3095 13.031999999999998 866816.4007276075 999307.696409588 +26392 4326 866816.4007276075 999307.696409588 10.30880826105444 13.031908587157279 +4263 26393 12.57 10.075 1118040.9202778852 671618.7445506924 +26393 4326 1118040.9202778852 671618.7445506924 12.569354566948636 10.075174701845075 +4263 26393 10.698 6.794499999999999 911185.4754737923 309293.2258689891 +26393 4326 911185.4754737923 309293.2258689891 10.697327814383229 6.794970318314633 +4263 26393 14.442 13.3555 1320731.1433567563 1035209.9619456109 +26393 4326 1320731.1433567563 1035209.9619456109 14.441380435860173 13.355391309564054 +4266 26632 10.625 1.16 680812.5776745655 128255.12265378107 +26632 4326 680812.5776745655 128255.12265378107 10.623963594489581 1.1602630893215076 +4266 26632 9.3875 0.11599999999999999 543120.049882367 12820.597914813563 +26632 4326 543120.049882367 12820.597914813563 9.386443917811635 0.1163607922162002 +4266 26632 11.8625 2.2039999999999997 818429.15625477 243893.07326313996 +26632 4326 818429.15625477 243893.07326313996 11.861483427391281 2.204166235959836 +4266 26692 9.52 -2.0250000000000004 557828.9057656243 9776187.081454191 +26692 4326 557828.9057656243 9776187.081454191 9.518945371580761 -2.0244384879333235 +4266 26692 7.279 -5.9355 309483.9452180507 9343689.587873545 +26692 4326 309483.9452180507 9343689.587873545 7.277905349043322 -5.934572883370908 +4266 26692 11.761 1.8854999999999995 807189.8312199612 10208629.613501871 +26692 4326 807189.8312199612 10208629.613501871 11.759981938223081 1.885695849042591 +4267 26701 -176.995 55.545 500315.504202019 6155226.620079446 +26701 4326 500315.504202019 6155226.620079446 -176.99740623313252 55.54380514351933 +4267 26701 -179.6995 48.6465 301161.15236489614 5391458.4648710275 +26701 4326 301161.15236489614 5391458.4648710275 -179.70156279256133 48.645254088878175 +4267 26701 -174.2905 62.4435 639824.8513193583 6926319.397442806 +26701 4326 639824.8513193583 6926319.397442806 -174.293432885212 62.44246849265357 +4267 26702 -170.995 60.855 500271.6716946282 6746432.194739528 +26702 4326 500271.6716946282 6746432.194739528 -170.997767687503 60.854002566940096 +4267 26702 -173.6995 49.87949999999999 306057.37224835844 5528509.078908331 +26702 4326 306057.37224835844 5528509.078908331 -173.70160500780796 49.87835499228297 +4267 26702 -168.2905 71.8305 594277.6234720252 7971976.838008402 +26702 4326 594277.6234720252 7971976.838008402 -168.2947862314142 71.82989546588087 +4267 26703 -164.995 61.905 500262.7120005358 6863395.17407735 +26703 4326 500262.7120005358 6863395.17407735 -164.99750540693282 61.90418811377297 +4267 26703 -167.6995 50.758500000000005 309603.2212132216 5626225.9754955415 +26703 4326 309603.2212132216 5626225.9754955415 -167.70138434969607 50.75754727293723 +4267 26703 -162.2905 73.0515 588137.8589876159 8108055.859198128 +26703 4326 588137.8589876159 8108055.859198128 -162.2944974453929 73.05106812816416 +4267 26704 -158.995 62.845 500254.6149302092 6968120.322763388 +26704 4326 500254.6149302092 6968120.322763388 -158.9975091255922 62.84431625730801 +4267 26704 -161.6995 52.1665 315376.75306554436 5782775.23833798 +26704 4326 315376.75306554436 5782775.23833798 -161.70139613178415 52.165641981380205 +4267 26704 -156.2905 73.52349999999998 585753.3866944031 8160663.210090254 +26704 4326 585753.3866944031 8160663.210090254 -156.2944662944442 73.52320128149425 +4267 26705 -152.995 63.42999999999999 500249.54055112065 7033302.228302591 +26705 4326 500249.54055112065 7033302.228302591 -152.99745459678476 63.429435094304125 +4267 26705 -155.6995 53.278 320014.0204306851 5906378.982833938 +26705 4326 320014.0204306851 5906378.982833938 -155.7013753243537 53.277240294934145 +4267 26705 -150.2905 73.582 585457.4391440771 8167183.532825249 +26705 4326 585457.4391440771 8167183.532825249 -150.2942936337356 73.58181408183387 +4267 26706 -146.995 64.09 500243.783940937 7106847.309912583 +26706 4326 500243.783940937 7106847.309912583 -146.99737698535284 64.08955255128797 +4267 26706 -149.6995 55.053999999999995 327564.65368115064 6103916.020836643 +26706 4326 327564.65368115064 6103916.020836643 -149.70136327197739 55.05334553547478 +4267 26706 -144.2905 73.12599999999999 587761.8952747656 8116359.222991304 +26706 4326 587761.8952747656 8116359.222991304 -144.29397205047184 73.12590052173901 +4267 26707 -141.0 63.53 499999.9999999987 7044444.970961778 +26707 4326 499999.9999999987 7044444.970961778 -141.0021719738246 63.529632132000906 +4267 26707 -143.7 54.476 325056.2072929299 6039623.6652935315 +26707 4326 325056.2072929299 6039623.6652935315 -143.70172399537654 54.47542901430401 +4267 26707 -138.3 72.584 590176.5251364703 8055937.669042086 +26707 4326 590176.5251364703 8055937.669042086 -138.30311550071931 72.58397614357206 +4267 26708 -135.0 62.81 500000.0 6964220.7148622945 +26708 4326 500000.0 6964220.7148622945 -135.00232919653862 62.80986320550434 +4267 26708 -137.7 53.603 321349.852554864 5942525.155134478 +26708 4326 321349.852554864 5942525.155134478 -137.70186698265513 53.602668505933686 +4267 26708 -132.3 72.01700000000001 593015.4728947371 7992746.48047081 +26708 4326 593015.4728947371 7992746.48047081 -132.3032992307227 72.01720048025608 +4267 26709 -129.0 60.605000000000004 500000.0 6718586.582807553 +26709 4326 500000.0 6718586.582807553 -129.0017699324871 60.604984630725276 +4267 26709 -131.7 50.3225 307803.13423554006 5577756.440408436 +26709 4326 307803.13423554006 5577756.440408436 -131.70144330166474 50.322315553248444 +4267 26709 -126.3 70.8875 598643.0361591283 7866874.448514229 +26709 4326 598643.0361591283 7866874.448514229 -126.30248667323904 70.88783403698636 +4267 26710 -122.995 55.765 500313.74005772785 6179710.945515224 +26710 4326 500313.74005772785 6179710.945515224 -122.99650433688107 55.764898239613906 +4267 26710 -125.6995 36.5365 258326.99776003795 4046645.694447009 +26710 4326 258326.99776003795 4046645.694447009 -125.70062027180767 36.536461978712815 +4267 26710 -120.2905 74.99349999999998 578290.0947318859 8324515.736389917 +26710 4326 578290.0947318859 8324515.736389917 -120.29355491378527 74.99400144046221 +4267 26711 -117.0 52.53 500000.0 5819771.233317021 +26711 4326 500000.0 5819771.233317021 -117.00118826797579 52.52992761693328 +4267 26711 -119.7 29.49 238214.54231775223 3265135.2828423143 +26711 4326 238214.54231775223 3265135.2828423143 -119.70089681256209 29.49021976967927 +4267 26711 -114.3 75.57 575084.6020215175 8388767.491663048 +26711 4326 575084.6020215175 8388767.491663048 -114.30266409542782 75.5705993077152 +4267 26712 -111.0 48.735 500000.0 5397778.67207275 +26712 4326 500000.0 5397778.67207275 -111.00088102783971 48.73491191951297 +4267 26712 -113.7 21.6675 220578.5205448793 2398315.9670057544 +26712 4326 220578.5205448793 2398315.9670057544 -113.70069214250046 21.66798407011758 +4267 26712 -108.3 75.80250000000001 573900.2742537172 8414685.711221794 +26712 4326 573900.2742537172 8414685.711221794 -108.30211168237436 75.8031676078597 +4267 26713 -105.0 48.64 499999.99999999907 5387218.556757423 +26713 4326 499999.99999999907 5387218.556757423 -105.00066675988236 48.63995186735119 +4267 26713 -107.7 20.938 219194.17975437315 2317514.812624424 +26713 4326 219194.17975437315 2317514.812624424 -107.700540960931 20.9385380083263 +4267 26713 -102.3 76.342 571147.401113389 8474828.65903362 +26713 4326 571147.401113389 8474828.65903362 -102.30158912960646 76.34273726709155 +4267 26714 -99.0 48.16499999999999 500000.0 5334420.636717551 +26714 4326 500000.0 5334420.636717551 -99.00044270041624 48.16497886328717 +4267 26714 -101.7 18.8475 215479.31451611937 2085995.6608092745 +26714 4326 215479.31451611937 2085995.6608092745 -101.70038221010982 18.84814655618957 +4267 26714 -96.3 77.48249999999999 565307.0753572324 8601977.264786147 +26714 4326 565307.0753572324 8601977.264786147 -96.30105397450045 77.4833200842254 +4267 26715 -93.0 47.795 499999.99999999907 5293296.9071360035 +26715 4326 499999.99999999907 5293296.9071360035 -93.00024366453765 47.795047252793374 +4267 26715 -95.7 17.0465 212581.4802056243 1886571.0618120003 +26715 4326 212581.4802056243 1886571.0618120003 -95.70024087435466 17.04732764191609 +4267 26715 -90.3 78.5435 559850.226737205 8720270.522243522 +26715 4326 559850.226737205 8720270.522243522 -90.30048813963495 78.54438692389215 +4267 26716 -87.005 45.905 499612.1647007101 5083274.534190165 +26716 4326 499612.1647007101 5083274.534190165 -87.0050223084172 45.90505396247972 +4267 26716 -89.7095 12.9335 205997.06338515732 1431249.8479173842 +26716 4326 205997.06338515732 1431249.8479173842 -89.70958477191436 12.93453180174897 +4267 26716 -84.3005 78.87650000000001 558122.4251424794 8757398.37224135 +26716 4326 558122.4251424794 8757398.37224135 -84.30023354194023 78.87740423959494 +4267 26717 -81.0 45.504999999999995 500000.0 5038834.377836785 +26717 4326 500000.0 5038834.377836785 -80.99981011357602 45.505050381711335 +4267 26717 -83.7 11.7325 205689.56575710833 1298304.2771692255 +26717 4326 205689.56575710833 1298304.2771692255 -83.69993212929919 11.733592111973362 +4267 26717 -78.3 79.2775 556062.8717119239 8802109.848165615 +26717 4326 556062.8717119239 8802109.848165615 -78.29892918014994 79.27840943188444 +4267 26718 -75.0 50.995 500000.0 5649050.243912906 +26718 4326 500000.0 5649050.243912906 -74.99955509986512 50.995048694618134 +4267 26718 -77.7 22.046499999999998 221315.58263225044 2440297.1072574984 +26718 4326 221315.58263225044 2440297.1072574984 -77.69976858519024 22.04710440622402 +4267 26718 -72.3 79.9435 552618.2889651954 8876369.98796363 +26718 4326 552618.2889651954 8876369.98796363 -72.29802511228537 79.94441154224084 +4267 26719 -69.0 58.39 500000.0 6471921.487043937 +26719 4326 500000.0 6471921.487043937 -68.99919152269125 58.39012539223688 +4267 26719 -71.7 36.088 256892.4949401102 3996883.7415302736 +26719 4326 256892.4949401102 3996883.7415302736 -71.69955457172853 36.08813578151872 +4267 26719 -66.3 80.69200000000001 548738.4205198678 8959831.845657153 +26719 4326 548738.4205198678 8959831.845657153 -66.29699089645781 80.69290260598463 +4267 26720 -62.995000000000005 60.59 500273.91844907746 6716915.890792315 +26720 4326 500273.91844907746 6716915.890792315 -62.993853474665265 60.590130237664155 +4267 26720 -65.6995 40.448 271064.783281156 4480772.129129442 +26720 4326 271064.783281156 4480772.129129442 -65.69884098017704 40.44803248133198 +4267 26720 -60.2905 80.732 548701.4719507277 8964300.101413963 +26720 4326 548701.4719507277 8964300.101413963 -60.28662909881805 80.73284690479586 +4267 26721 -57.0 54.75 500000.0000000008 6066756.63636871 +26721 4326 500000.0000000008 6066756.63636871 -56.99879383924369 54.74996466230851 +4267 26721 -59.7 41.988 276335.4200060306 4651757.214267874 +26721 4326 276335.4200060306 4651757.214267874 -59.69914221406904 41.98797400520287 +4267 26721 -54.3 67.512 615220.7330642001 7490782.5659681 +26721 4326 615220.7330642001 7490782.5659681 -54.298030956404865 67.51224124679227 +4267 26722 -50.995000000000005 48.575 500368.8258020261 5379993.328752754 +26722 4326 500368.8258020261 5379993.328752754 -50.99375769869716 48.57485922918358 +4267 26722 -53.6995 40.4075 270927.3066555245 4476275.9660702115 +26722 4326 270927.3066555245 4476275.9660702115 -53.69849297918474 40.407446275815666 +4267 26722 -48.2905 56.74249999999999 665713.0293990159 6291787.4791483 +26722 4326 665713.0293990159 6291787.4791483 -48.288904012657994 56.74239445341839 +4267 26729 -85.84 32.995 497955.8922004022 907582.227638985 +26729 4326 497955.8922004022 907582.227638985 -85.83996118261857 32.99516051783819 +4267 26729 -86.69500000000001 31.1905 230549.5324558839 252190.01408743506 +26729 4326 230549.5324558839 252190.01408743506 -86.69498701658202 31.190707883497502 +4267 26729 -84.985 34.7995 754694.05490056 1565291.882380743 +26729 4326 754694.05490056 1565291.882380743 -84.98493424435968 34.79961860536723 +4267 26730 -87.39 32.58 533883.8561579336 938426.4170812102 +26730 4326 533883.8561579336 938426.4170812102 -87.39000751630846 32.580171070967516 +4267 26730 -88.37100000000001 30.384 225355.44489665594 140702.05396202544 +26730 4326 225355.44489665594 140702.05396202544 -88.37103587856139 30.38423059620154 +4267 26730 -86.40899999999999 34.776 827636.5107727401 1739233.5197385694 +26730 4326 827636.5107727401 1739233.5197385694 -86.40897774518058 34.77611962404514 +4267 26731 -135.495 57.480000000000004 2325871.1971463854 2067394.2476777893 +26731 4326 2325871.1971463854 2067394.2476777893 -135.49666341528754 57.47958022856898 +4267 26731 -140.4495 54.897 1258535.3040541639 1188958.2899650359 +26731 4326 1258535.3040541639 1188958.2899650359 -140.45117205966835 54.8964786955838 +4267 26731 -130.5405 60.063 3256898.097474802 3020373.324186074 +26731 4326 3256898.097474802 3020373.324186074 -130.54214375431127 60.062693004608576 +4267 26732 -142.495 64.94 423219.6468259021 3998438.3285378832 +26732 4326 423219.6468259021 3998438.3285378832 -142.49732942496814 64.93964791020937 +4267 26732 -143.8585 60.242 162302.02814439518 2285131.285129661 +26732 4326 162302.02814439518 2285131.285129661 -143.86052137955332 60.24151178819983 +4267 26732 -141.1315 69.63799999999999 610684.5151574003 5717787.665671524 +26732 4326 610684.5151574003 5717787.665671524 -141.13428639766832 69.63782073688354 +4267 26733 -146.0 65.05 500000.00000000134 4038371.714790454 +26733 4326 500000.00000000134 4038371.714790454 -146.00243620296428 65.04959511272932 +4267 26733 -147.8 60.253 173039.6130497918 2288856.220521103 +26733 4326 173039.6130497918 2288856.220521103 -147.80211109288447 60.25245064058302 +4267 26733 -144.2 69.847 727121.7499891331 5796840.908538415 +26733 4326 727121.7499891331 5796840.908538415 -144.20292248447913 69.84677847893863 +4267 26734 -150.0 64.87 500000.00000000134 3972534.8460088293 +26734 4326 500000.00000000134 3972534.8460088293 -150.0025189762802 64.86952365380608 +4267 26734 -151.809 59.686 165739.93716231818 2081717.2459321606 +26734 4326 165739.93716231818 2081717.2459321606 -151.8111542704491 59.68537517417872 +4267 26734 -148.191 70.054 726010.1714842877 5872605.649374847 +26734 4326 726010.1714842877 5872605.649374847 -148.1940812616176 70.05371841940733 +4267 26735 -153.93 63.5 511437.2722389706 3471503.184558523 +26735 4326 511437.2722389706 3471503.184558523 -153.93247889669067 63.49942070371368 +4267 26735 -155.793 56.498 137751.90043752594 917037.8405073469 +26735 4326 137751.90043752594 917037.8405073469 -155.79503248408636 56.49726355289722 +4267 26735 -152.067 70.502 736290.8676355229 6036979.064640929 +26735 4326 736290.8676355229 6036979.064640929 -152.07026330522424 70.50166611832334 +4267 26736 -157.995 63.145 500827.07069063577 3341681.761087712 +26736 4326 500827.07069063577 3341681.761087712 -157.99751948368802 63.14434093341962 +4267 26736 -159.7995 55.7155 128986.31391752888 631302.7143385449 +26736 4326 128986.31391752888 631302.7143385449 -159.8015432589187 55.714689744997784 +4267 26736 -156.19050000000001 70.5745 720408.073075615 6063040.037568959 +26736 4326 720408.073075615 6063040.037568959 -156.19388041391312 70.5740934986547 +4267 26737 -162.005 62.53 699155.4695442967 3116806.3320222967 +26737 4326 699155.4695442967 3116806.3320222967 -162.00752375311524 62.529255301676095 +4267 26737 -163.80949999999999 55.141 321467.5401246899 421570.32963515184 +26737 4326 321467.5401246899 421570.32963515184 -163.81155484546642 55.14011818192701 +4267 26737 -160.2005 69.919 926281.5896323748 5823180.242651255 +26737 4326 926281.5896323748 5823180.242651255 -160.20385752427896 69.9184957279343 +4267 26738 -166.13 61.695 477429.99310559436 2811542.503849545 +26738 4326 477429.99310559436 2811542.503849545 -166.13249914905128 61.69416331098054 +4267 26738 -168.047 55.075500000000005 71090.25677492828 399026.81496055046 +26738 4326 71090.25677492828 399026.81496055046 -168.0490838634608 55.07454577900427 +4267 26738 -164.213 68.3145 741819.3026842558 5236172.616598661 +26738 4326 741819.3026842558 5236172.616598661 -164.21618191171487 68.31386730121125 +4267 26739 -170.57999999999998 61.155 497548.9884099415 2614563.6341379792 +26739 4326 497548.9884099415 2614563.6341379792 -170.5824885644313 61.15407181503059 +4267 26739 -172.902 56.9565 20851.17898609949 1092102.2788738816 +26739 4326 20851.17898609949 1092102.2788738816 -172.90421189821953 56.95547467424153 +4267 26739 -168.258 65.3535 866007.6875387554 4153059.4282900086 +26739 4326 866007.6875387554 4153059.4282900086 -168.26086110976382 65.35270622528955 +4267 26740 -176.21 52.82 2953557.5036956836 664463.9259719314 +26740 4326 2953557.5036956836 664463.9259719314 -176.21225161100875 52.81880614015611 +4267 26740 173.557 51.452 626310.2449900615 337714.82551824825 +26740 4326 626310.2449900615 337714.82551824825 173.55483049521212 51.4506147137329 +4267 26740 -165.977 54.188 5140056.062649125 1313299.292007183 +26740 4326 5140056.062649125 1313299.292007183 -165.97926640702642 54.187006850457756 +4267 26741 -122.22 40.8 1939094.271123248 534365.3361145216 +26741 4326 1939094.271123248 534365.3361145216 -122.22108476658921 40.79984094120774 +4267 26741 -124.227 39.711000000000006 1373452.3365106056 145548.73107199126 +26741 4326 1373452.3365106056 145548.73107199126 -124.22812005392704 39.71083803212153 +4267 26741 -120.213 41.888999999999996 2486595.59318426 936025.0000020217 +26741 4326 2486595.59318426 936025.0000020217 -120.2140473535854 41.88884608786964 +4267 26742 -121.80000000000001 39.09 2056765.8476693572 518415.62842447235 +26742 4326 2056765.8476693572 518415.62842447235 -121.80104702712163 39.08987090268046 +4267 26742 -123.834 38.127 1472415.0862489238 172974.38831396337 +26742 4326 1472415.0862489238 172974.38831396337 -123.83508546782599 38.12686974347099 +4267 26742 -119.766 40.053 2625389.4789455985 876802.2452595389 +26742 4326 2625389.4789455985 876802.2452595389 -119.76700648965374 40.052873687844944 +4267 26743 -120.425 37.72 2021693.192085885 444212.06461106497 +26743 4326 2021693.192085885 444212.06461106497 -120.42599151949662 37.71990766327208 +4267 26743 -122.7605 36.82899999999999 1338395.4665599505 127782.73353896152 +26743 4326 1338395.4665599505 127782.73353896152 -122.76153986587818 36.82890578141565 +4267 26743 -118.0895 38.611000000000004 2688785.923314541 777532.9460499984 +26743 4326 2688785.923314541 777532.9460499984 -118.09044057689475 38.610910636738836 +4267 26744 -118.815 36.68 2054249.6494284247 490288.81791481446 +26744 4326 2054249.6494284247 490288.81791481446 -118.81593580066426 36.67994171013069 +4267 26744 -121.6905 35.870000000000005 1202877.348079191 206535.32753786363 +26744 4326 1202877.348079191 206535.32753786363 -121.69150025980606 35.86993626746011 +4267 26744 -115.93950000000001 37.489999999999995 2887917.3828074057 799297.6055457528 +26744 4326 2887917.3828074057 799297.6055457528 -115.94036778316517 37.48994740997829 +4267 26745 -117.775 34.285 2067967.1208725716 285750.0455370634 +26745 4326 2067967.1208725716 285750.0455370634 -117.77588159157362 34.28500493504088 +4267 26745 -121.06450000000001 32.9125 1059204.2357750146 -199486.58525117807 +26745 4326 1059204.2357750146 -199486.58525117807 -121.06545031308177 32.91251735795902 +4267 26745 -114.4855 35.6575 3043969.2085937457 803446.645673265 +26745 4326 3043969.2085937457 803446.645673265 -114.48630815949082 35.65749529990628 +4267 26746 -116.285 33.305 1989306.172336342 414174.41296709655 +26746 4326 1989306.172336342 414174.41296709655 -116.28583307026085 33.30504076521064 +4267 26746 -117.96350000000001 32.6075 1472313.9179287592 164731.23522896066 +26746 4326 1472313.9179287592 164731.23522896066 -117.96436957842103 32.607548517363824 +4267 26746 -114.6065 34.0025 2498130.3435568367 671903.4180725083 +26746 4326 2498130.3435568367 671903.4180725083 -114.60729538005054 34.00253370437601 +4267 26748 -110.375 34.17 436985.25131596345 1153270.5531596951 +26748 4326 436985.25131596345 1153270.5531596951 -110.37568175143582 34.1700550114346 +4267 26748 -111.5765 31.613999999999997 61122.14387380012 226150.52783732614 +26748 4326 61122.14387380012 226150.52783732614 -111.57719462669803 31.614116398957705 +4267 26748 -109.1735 36.726 791056.4174903786 2084993.3267630218 +26748 4326 791056.4174903786 2084993.3267630218 -109.17416916844614 36.72600592809506 +4267 26749 -111.895 34.17 506553.52854516945 1153206.9040200817 +26749 4326 506553.52854516945 1153206.9040200817 -111.89572359222277 34.17004630824436 +4267 26749 -113.2045 31.613999999999997 99103.50260434388 225681.73652829102 +26749 4326 99103.50260434388 225681.73652829102 -113.20523773583665 31.614107269585116 +4267 26749 -110.5855 36.726 890114.8293196111 2086195.0154778548 +26749 4326 890114.8293196111 2086195.0154778548 -110.58620966738135 36.725997749017274 +4267 26750 -113.66499999999999 34.525 525602.7253817049 1282441.3095544646 +26750 4326 525602.7253817049 1282441.3095544646 -113.6657749300963 34.52502685360287 +4267 26750 -114.6955 32.2975 207837.50170664035 473246.9826334749 +26750 4326 207837.50170664035 473246.9826334749 -114.6962824686764 32.29757871980111 +4267 26750 -112.6345 36.7525 826807.0566627803 2095105.2597103384 +26750 4326 826807.0566627803 2095105.2597103384 -112.63526792758974 36.75248450561879 +4267 26751 -92.13 35.585 1961345.5475026958 455595.3311323755 +26751 4326 1961345.5475026958 455595.3311323755 -92.13015340998327 35.58508716548821 +4267 26751 -94.37100000000001 34.7615 1287853.4470813826 164417.40752035138 +26751 4326 1287853.4470813826 164417.40752035138 -94.37121949168525 34.76160091884195 +4267 26751 -89.889 36.408500000000004 2621214.5592622054 761997.9854762653 +26751 4326 2621214.5592622054 761997.9854762653 -89.88908575601394 36.40857349088993 +4267 26752 -92.44 34.055 1866730.6781067208 505463.90875840903 +26752 4326 1866730.6781067208 505463.90875840903 -92.44015989121985 34.05511965026259 +4267 26752 -94.27600000000001 33.1145 1303080.9156269536 170707.21600424385 +26752 4326 1303080.9156269536 170707.21600424385 -94.27621249963454 33.11463921076825 +4267 26752 -90.604 34.9955 2418148.062225481 850301.6731930353 +26752 4326 2418148.062225481 850301.6731930353 -90.60410598535856 34.9956009559666 +4267 26753 -105.55000000000001 40.285 1986050.4918777996 346664.40409948287 +26753 4326 1986050.4918777996 346664.40409948287 -105.55059187805757 40.28496925504695 +4267 26753 -108.709 39.6325 1096316.3621388164 125328.8311410042 +26753 4326 1096316.3621388164 125328.8311410042 -108.70968233809405 39.6324603499735 +4267 26753 -102.391 40.9375 2858870.071509783 599431.9803561919 +26753 4326 2858870.071509783 599431.9803561919 -102.39149792285623 40.93747674356976 +4267 26754 -105.55000000000001 39.115 1985813.2369536823 466772.48375346465 +26754 4326 1985813.2369536823 466772.48375346465 -105.55058196710512 39.11498480952817 +4267 26754 -108.709 38.2375 1078396.771262422 163474.31207793762 +26754 4326 1078396.771262422 163474.31207793762 -108.70966909995191 38.23748145617077 +4267 26754 -102.391 39.99250000000001 2871024.5731753176 801297.5193949398 +26754 4326 2871024.5731753176 801297.5193949398 -102.39149099315858 39.992487622119725 +4267 26755 -105.55000000000001 37.83 1985559.0494250744 423586.681261082 +26755 4326 1985559.0494250744 423586.681261082 -105.5505717285087 37.83000491994927 +4267 26755 -108.709 37.065 1063794.872819252 161121.71428430974 +26755 4326 1063794.872819252 161121.71428430974 -108.70965866212825 37.065002069801864 +4267 26755 -102.391 38.595 2888502.2156827087 716950.57910908 +26755 4326 2888502.2156827087 716950.57910908 -102.39148132836613 38.595006865320734 +4267 26756 -72.755 41.515 598630.616738569 248370.67392585025 +26756 4326 598630.616738569 248370.67392585025 -72.75452330335773 41.51498723665099 +4267 26756 -73.63250000000001 41.0335 356516.36189207307 74174.70636971273 +26756 4326 356516.36189207307 74174.70636971273 -73.6320551096778 41.033495477965474 +4267 26756 -71.8775 41.9965 837181.7811730847 425021.7610643191 +26756 4326 837181.7811730847 425021.7610643191 -71.87699110065027 41.99647916088494 +4267 26757 -75.38499999999999 39.144999999999996 508981.6700122818 416996.05298950966 +26757 4326 508981.6700122818 416996.05298950966 -75.38462254554781 39.145024379444436 +4267 26757 -75.7585 38.5105 402180.69502605224 186089.107547015 +26757 4326 402180.69502605224 186089.107547015 -75.75813761135637 38.51053466947732 +4267 26757 -75.0115 39.7795 613879.5639929197 648364.7000831567 +26757 4326 613879.5639929197 648364.7000831567 -75.01110717605526 39.77951478708774 +4267 26758 -81.15 27.619999999999997 451426.82994999504 1194534.1961127887 +26758 4326 451426.82994999504 1194534.1961127887 -81.14983069603736 27.6203133789114 +4267 26758 -82.212 24.731 97712.56508598257 146279.21675151854 +26758 4326 97712.56508598257 146279.21675151854 -82.21186403689259 24.731417259052964 +4267 26758 -80.088 30.508999999999997 787208.7674832126 2246112.9471548055 +26758 4326 787208.7674832126 2246112.9471548055 -80.08779517420233 30.50922160616364 +4267 26759 -82.235 27.935000000000002 424120.8508358629 1309090.3889906479 +26759 4326 424120.8508358629 1309090.3889906479 -82.23486089188505 27.93530412526941 +4267 26759 -83.2295 26.4365 97648.42611178724 766232.682697271 +26759 4326 97648.42611178724 766232.682697271 -83.22939053338793 26.436856059223764 +4267 26759 -81.2405 29.433500000000002 741771.8660775612 1854631.8237205076 +26759 4326 741771.8660775612 1854631.8237205076 -81.24033036700865 29.433755311796865 +4267 26760 -84.83500000000001 30.11 1894074.5063586289 403816.08084529935 +26760 4326 1894074.5063586289 403816.08084529935 -84.83493321815534 30.110238189692694 +4267 26760 -87.3505 29.3 1091413.7819843867 120458.36605116897 +26760 4326 1091413.7819843867 120458.36605116897 -87.35050612714484 29.300263157295444 +4267 26760 -82.3195 30.92 2683789.6330141616 704792.3805126732 +26760 4326 2683789.6330141616 704792.3805126732 -82.31935922619788 30.920212829330858 +4267 26766 -82.12 32.519999999999996 514384.05215602915 916553.6699356607 +26766 4326 514384.05215602915 916553.6699356607 -82.11985087205366 32.52016962463178 +4267 26766 -83.335 30.576 132329.0906412652 211372.35757997408 +26766 4326 132329.0906412652 211372.35757997408 -83.33488924171583 30.576223607818726 +4267 26766 -80.905 34.464 880300.016788446 1626226.6081364178 +26766 4326 880300.016788446 1626226.6081364178 -80.90481070615836 34.46412164123936 +4267 26767 -84.3 32.815 459037.4958583342 1023894.9569913406 +26767 4326 459037.4958583342 1023894.9569913406 -84.29991531266737 32.81516421612568 +4267 26767 -85.479 30.8395 88130.29060133616 307712.97208164673 +26767 4326 88130.29060133616 307712.97208164673 -85.47895152977198 30.83971741221359 +4267 26767 -83.121 34.790499999999994 813956.5678626323 1744307.8604044768 +26767 4326 813956.5678626323 1744307.8604044768 -83.12087738942364 34.7906173558738 +4267 26768 -112.14 43.370000000000005 507090.80224991543 620733.5198999105 +26768 4326 507090.80224991543 620733.5198999105 -112.14083075631872 43.369899151646955 +4267 26768 -113.13 42.128 238677.87569915268 169575.29986729813 +26768 4326 238677.87569915268 169575.29986729813 -113.13084435732199 42.127902079975975 +4267 26768 -111.15 44.612 764757.6399022901 1075112.7026737134 +26768 4326 764757.6399022901 1075112.7026737134 -111.15081673632695 44.611899345476665 +4267 26769 -113.985 43.845 503957.3141056654 793866.6920786793 +26769 4326 503957.3141056654 793866.6920786793 -113.98589464311364 43.84488212166226 +4267 26769 -115.1685 42.1755 183258.81047962615 187579.21553548548 +26769 4326 183258.81047962615 187579.21553548548 -115.16940605153634 42.17538607297068 +4267 26769 -112.8015 45.514500000000005 807237.4006254991 1404795.2235889325 +26769 4326 807237.4006254991 1404795.2235889325 -112.80238297360519 45.51438389938288 +4267 26770 -115.78 45.5 492307.5759015462 1397197.455005418 +26770 4326 492307.5759015462 1397197.455005418 -115.78097693873865 45.499859687533124 +4267 26770 -117.094 42.341 136642.46166686446 248586.86322162935 +26770 4326 136642.46166686446 248586.86322162935 -117.09496519786185 42.34086876347594 +4267 26770 -114.466 48.659 810327.4587262712 2551932.9812641945 +26770 4326 810327.4587262712 2551932.9812641945 -114.46699269888222 48.65887128194499 +4267 26771 -88.145 39.78 552933.051449889 1133801.6479844162 +26771 4326 552933.051449889 1133801.6479844162 -88.14503287829237 39.780035237489514 +4267 26771 -89.1575 37.332 260364.95982578464 243280.77484362872 +26771 4326 260364.95982578464 243280.77484362872 -89.15756371420949 37.33207025496734 +4267 26771 -87.1325 42.228 825245.400112453 2027928.5141631968 +26771 4326 825245.400112453 2027928.5141631968 -87.13249982744514 42.22801141796822 +4267 26772 -90.22 39.739999999999995 485001.94693320896 1119142.3810052585 +26772 4326 485001.94693320896 1119142.3810052585 -90.22010049776001 39.74003453291872 +4267 26772 -91.39 37.247 143910.7786507152 213580.98222396796 +26772 4326 143910.7786507152 213580.98222396796 -91.39013388828006 37.24706982605938 +4267 26772 -89.05 42.233 802414.4846284731 2029372.0714448618 +26772 4326 802414.4846284731 2029372.0714448618 -89.05006476432054 42.23301084093854 +4267 26773 -85.685 39.86 494853.2407722004 859474.4852934141 +26773 4326 494853.2407722004 859474.4852934141 -85.68495253986644 39.8600341175626 +4267 26773 -86.4995 38.141000000000005 260467.881128252 234482.34818579405 +26773 4326 260467.881128252 234482.34818579405 -86.49947964845882 38.14105778695085 +4267 26773 -84.8705 41.579 717833.7926026767 1486733.3758216104 +26773 4326 717833.7926026767 1486733.3758216104 -84.87042400928 41.57901589308087 +4267 26774 -87.16999999999999 39.77 475638.11197572 826702.7281958578 +26774 4326 475638.11197572 826702.7281958578 -87.17000105749814 39.77003550158658 +4267 26774 -88.00699999999999 37.970000000000006 233723.82762882693 172459.0274345701 +26774 4326 233723.82762882693 172459.0274345701 -88.00702766407896 37.970060376361374 +4267 26774 -86.333 41.57 705322.0538956082 1483341.7031826398 +26774 4326 705322.0538956082 1483341.7031826398 -86.33297302838992 41.5700166261927 +4267 26775 -93.4 42.68 2026889.9195221656 430029.77551064 +26775 4326 2026889.9195221656 430029.77551064 -93.4002124832611 42.67998971941625 +4267 26775 -96.325 41.933 1231406.004959541 170637.09251654038 +26775 4326 1231406.004959541 170637.09251654038 -96.32530707607036 41.9329886485414 +4267 26775 -90.47500000000001 43.427 2803506.7814371167 716647.7838458581 +26775 4326 2803506.7814371167 716647.7838458581 -90.47511507295745 43.42698951613206 +4267 26776 -93.14 41.2 2099068.360835479 437395.80626172596 +26776 4326 2099068.360835479 437395.80626172596 -93.14019906280657 41.200002164298816 +4267 26776 -95.84 40.444 1348722.8418480973 170522.70181911785 +26776 4326 1348722.8418480973 170522.70181911785 -95.84028450626167 40.44400429882547 +4267 26776 -90.44 41.955999999999996 2832221.104992731 727296.3476613486 +26776 4326 2832221.104992731 727296.3476613486 -90.44011121797575 41.95599919241371 +4267 26777 -98.32 39.265 1909395.8036241776 339475.8264698193 +26777 4326 1909395.8036241776 339475.8264698193 -98.32035831629688 39.26501193407521 +4267 26777 -101.686 38.594500000000004 946701.7161489577 116557.00300146725 +26777 4326 946701.7161489577 116557.00300146725 -101.68645959760903 38.59450970697879 +4267 26777 -94.954 39.9355 2854064.3626942555 597908.9527953475 +26777 4326 2854064.3626942555 597908.9527953475 -94.95425388770688 39.93551224378303 +4267 26778 -98.32499999999999 37.935 2050471.1174464868 461865.724383236 +26778 4326 2050471.1174464868 461865.724383236 -98.32535191898177 37.93503157760815 +4267 26778 -101.6775 37.084500000000006 1073213.691204832 167935.63777457768 +26778 4326 1073213.691204832 167935.63777457768 -101.67795006527591 37.08453461969816 +4267 26778 -94.9725 38.7855 3005396.030134327 790563.5775936245 +26778 4326 3005396.030134327 790563.5775936245 -94.97275034442876 38.785527319196206 +4267 26779 -84.215 38.43 2010026.4278014172 338666.0155059941 +26779 4326 2010026.4278014172 338666.0155059941 -84.21490645441851 38.43005234504208 +4267 26779 -85.7855 37.782000000000004 1556212.9122874576 106390.92518681314 +26779 4326 1556212.9122874576 106390.92518681314 -85.78545708849748 37.782063194237026 +4267 26779 -82.6445 39.077999999999996 2455789.8019858515 578633.9062481599 +26779 4326 2455789.8019858515 578633.9062481599 -82.64435496595738 39.07804158052925 +4267 26780 -85.75999999999999 37.33 1997092.431643645 362865.3106716009 +26780 4326 1997092.431643645 362865.3106716009 -85.75995654263355 37.330070579441774 +4267 26780 -89.189 36.574000000000005 990291.1248653902 106001.10298205556 +26780 4326 990291.1248653902 106001.10298205556 -89.18906406933841 36.57408349507388 +4267 26780 -82.331 38.086 2983920.674748843 655949.5270028185 +26780 4326 2983920.674748843 655949.5270028185 -82.33084698875463 38.08605555317739 +4267 26781 -92.455 31.94 2013958.9161212447 463173.2173415951 +26781 4326 2013958.9161212447 463173.2173415951 -92.45515656025684 31.940171925313017 +4267 26781 -93.8905 30.959000000000003 1564102.95458331 109134.96949340802 +26781 4326 1564102.95458331 109134.96949340802 -93.89069645012057 30.959196836418776 +4267 26781 -91.0195 32.921 2454358.5964816287 823164.090907312 +26781 4326 2454358.5964816287 823164.090907312 -91.0196157608659 32.92114827484572 +4267 26782 -90.85 29.445 2153844.320965662 283363.7146855974 +26782 4326 2153844.320965662 283363.7146855974 -90.85010691525878 29.445257228422697 +4267 26782 -93.631 27.9825 1258038.5410889469 -241400.95789248653 +26782 4326 1258038.5410889469 -241400.95789248653 -93.63118415734677 27.982800691566133 +4267 26782 -88.069 30.9075 3023745.8014251296 829489.2593768982 +26782 4326 3023745.8014251296 829489.2593768982 -88.0690272409092 30.907715706708313 +4267 26783 -68.47 45.675 507668.3032844669 671380.1580626857 +26783 4326 507668.3032844669 671380.1580626857 -68.4695090407608 45.67506427112396 +4267 26783 -69.874 44.0595 138827.31674246443 85448.93771073867 +26783 4326 138827.31674246443 85448.93771073867 -69.8735707101464 44.05957662098404 +4267 26783 -67.066 47.2905 855850.2637315943 1263761.931889081 +26783 4326 855850.2637315943 1263761.931889081 -67.06544361515923 47.29055604161177 +4267 26784 -70.17500000000001 44.81 497837.20155709866 720521.6672047519 +26784 4326 497837.20155709866 720521.6672047519 -70.17440917871222 44.8099545599286 +4267 26784 -70.9985 43.217 278251.338579262 140934.18079897345 +26784 4326 278251.338579262 140934.18079897345 -70.99795194936209 43.21696611604998 +4267 26784 -69.3515 46.403 705659.7352544258 1302435.233203568 +26784 4326 705659.7352544258 1302435.233203568 -69.35086363879435 46.402947567412 +4267 26785 -77.22999999999999 38.849999999999994 734496.3197235791 370328.9000557196 +26785 4326 734496.3197235791 370328.9000557196 -77.22968247409102 38.850033999328154 +4267 26785 -79.264 38.058 148128.95016808988 89902.70932041129 +26785 4326 148128.95016808988 89902.70932041129 -79.26374994529188 38.05805072702665 +4267 26785 -75.196 39.641999999999996 1308043.6519889066 663731.775074927 +26785 4326 1308043.6519889066 663731.775074927 -75.19561383281041 39.64201715217652 +4267 26786 -71.68 42.175 551207.180690476 428216.1350018127 +26786 4326 551207.180690476 428216.1350018127 -71.6794832273087 42.17497678649855 +4267 26786 -73.318 41.5315 102231.97904620529 198981.981917604 +26786 4326 102231.97904620529 198981.981917604 -73.31754147604786 41.53148937159881 +4267 26786 -70.042 42.8185 991194.4492156766 666021.2857457938 +26786 4326 991194.4492156766 666021.2857457938 -70.04142414596673 42.81846406674102 +4267 26787 -70.4 41.349999999999994 227457.5194225851 127540.87423708272 +26787 4326 227457.5194225851 127540.87423708272 -70.3994487176882 41.34997830553749 +4267 26787 -70.859 41.205999999999996 101210.43069005736 75261.56868610792 +26787 4326 101210.43069005736 75261.56868610792 -70.85846459868284 41.20598191691502 +4267 26787 -69.941 41.494 353148.08433570835 180488.2552735224 +26787 4326 353148.08433570835 180488.2552735224 -69.94043279850622 41.49397466352981 +4267 26791 -93.35499999999999 48.010000000000005 1937583.488305536 550862.6751795135 +26791 4326 1937583.488305536 550862.6751795135 -93.35523253916348 48.00999486560406 +4267 26791 -96.8335 46.777 1064789.5816662188 123625.60960381699 +26791 4326 1064789.5816662188 123625.60960381699 -96.83385328243803 46.776982867349204 +4267 26791 -89.8765 49.243 2770036.847417478 1016669.3622112131 +26791 4326 2770036.847417478 1016669.3622112131 -89.87660513127338 49.24300617280874 +4267 26792 -94.575 46.379999999999995 1917974.503317682 503385.05753177 +26792 4326 1917974.503317682 503385.05753177 -94.57526851353974 46.379975962593974 +4267 26792 -96.6315 45.39 1388178.4910693793 151415.42624002093 +26792 4326 1388178.4910693793 151415.42624002093 -96.6318359521873 45.38997097303909 +4267 26792 -92.5185 47.37 2429080.313437798 868975.6255626929 +26792 4326 2429080.313437798 868975.6255626929 -92.51869832193351 47.3699817083466 +4267 26793 -94.03 44.540000000000006 1992178.036377256 561383.9431822083 +26793 4326 1992178.036377256 561383.9431822083 -94.03024101429664 44.53997945589104 +4267 26793 -96.568 43.595 1319729.8018092827 227590.40355272585 +26793 4326 1319729.8018092827 227590.40355272585 -96.5683236470786 43.59497697316828 +4267 26793 -91.49199999999999 45.48500000000001 2643244.1881430573 915791.513153699 +26793 4326 2643244.1881430573 915791.513153699 -91.492155250469 45.48498191224382 +4267 26794 -89.03 32.51 439371.0968742339 1034240.5461863845 +26794 4326 439371.0968742339 1034240.5461863845 -89.0300562996683 32.51017243164961 +4267 26794 -89.876 30.26 170799.48344422682 217280.39470797445 +26794 4326 170799.48344422682 217280.39470797445 -89.87607953314755 30.260233543535268 +4267 26794 -88.184 34.76 695040.4355490005 1853524.8045012897 +26794 4326 695040.4355490005 1853524.8045012897 -88.18403195521005 34.760119896836464 +4267 26795 -90.51 33.004999999999995 445838.2377517834 911248.827562222 +26795 4326 445838.2377517834 911248.827562222 -90.51010084681907 33.0051591110087 +4267 26795 -91.536 31.200499999999998 123956.29763137185 256817.85285002715 +26795 4326 123956.29763137185 256817.85285002715 -91.53612891436306 31.200705424621837 +4267 26795 -89.48400000000001 34.8095 754958.6953953586 1568904.4406829881 +26795 4326 754958.6953953586 1568904.4406829881 -89.48407168148083 34.809618281733954 +4267 26796 -90.535 38.295 489955.22425725684 896222.0863788147 +26796 4326 489955.22425725684 896222.0863788147 -90.53510852486174 38.29505394315668 +4267 26796 -91.8265 36.211499999999994 108661.86467177891 140331.40043677777 +26796 4326 108661.86467177891 140331.40043677777 -91.82664563364028 36.21158790280107 +4267 26796 -89.2435 40.3785 850063.9958122408 1657545.4491887665 +26796 4326 850063.9958122408 1657545.4491887665 -89.24356932195397 40.378527823579006 +4267 26797 -92.6 38.545 471399.3770995187 987274.5439035994 +26797 4326 471399.3770995187 987274.5439035994 -92.60017440748258 38.54503509521506 +4267 26797 -93.671 36.686499999999995 156638.8584784505 312666.61936409253 +26797 4326 156638.8584784505 312666.61936409253 -93.67120323218779 36.68656381258948 +4267 26797 -91.529 40.4035 770421.5473723628 1665650.863392669 +26797 4326 770421.5473723628 1665650.863392669 -91.52914418587613 40.40351274234764 +4267 26798 -94.625 38.535 464243.98847653874 862313.5913138185 +26798 4326 464243.98847653874 862313.5913138185 -94.62523849906522 38.53503167060179 +4267 26798 -95.65549999999999 36.6855 161176.9095999316 190914.0760273249 +26798 4326 161176.9095999316 190914.0760273249 -95.65576438780501 36.6855599336308 +4267 26798 -93.59450000000001 40.3845 752252.5458344718 1537212.9780921119 +26798 4326 752252.5458344718 1537212.9780921119 -93.594711426237 40.38450977619287 +4267 26799 -118.29499999999999 34.245 4198278.063346752 4201566.053449584 +26799 4326 4198278.063346752 4201566.053449584 -118.29589463059875 34.24500218038245 +4267 26799 -118.89349999999999 33.7185 4016342.946489196 4010435.3988741175 +26799 4326 4016342.946489196 4010435.3988741175 -118.89440443370088 33.71851146688068 +4267 26799 -117.6965 34.771499999999996 4377966.159439302 4393776.15444962 +26799 4326 4377966.159439302 4393776.15444962 -117.69738468256521 34.77149344007759 +4269 26847 -68.47 45.675 991918.0735949625 732133.9019829989 +26847 4326 991918.0735949625 732133.9019829989 -68.47 45.675 +4269 26847 -69.874 44.0595 623087.7549926013 146197.72780132052 +26847 4326 623087.7549926013 146197.72780132052 -69.874 44.0595 +4269 26847 -67.066 47.2905 1340089.2295977187 1324518.5800617225 +26847 4326 1340089.2295977187 1324518.5800617225 -67.066 47.2905 +4269 26848 -70.17500000000001 44.81 2950587.2651188183 720529.0952089471 +26848 4326 2950587.2651188183 720529.0952089471 -70.17500000000001 44.81000000000001 +4269 26848 -70.9985 43.217 2731007.6253422415 140935.8088169734 +26848 4326 2731007.6253422415 140935.8088169734 -70.9985 43.217000000000006 +4269 26848 -69.3515 46.403 3158403.4773295885 1302446.5884031455 +26848 4326 3158403.4773295885 1302446.5884031455 -69.3515 46.403000000000006 +4269 26849 -93.35499999999999 48.010000000000005 2562252.1196928965 878947.9838131967 +26849 4326 2562252.1196928965 878947.9838131967 -93.35499999999999 48.01000000000001 +4269 26849 -96.8335 46.777 1689484.930557216 451708.7351258669 +26849 4326 1689484.930557216 451708.7351258669 -96.8335 46.77700000000001 +4269 26849 -89.8765 49.243 3394678.668121814 1344754.5702052503 +26849 4326 3394678.668121814 1344754.5702052503 -89.8765 49.243 +4269 26850 -94.575 46.379999999999995 2542643.6648840555 831471.7338802997 +26850 4326 2542643.6648840555 831471.7338802997 -94.575 46.37999999999999 +4269 26850 -96.6315 45.39 2012863.368669874 479499.5703310468 +26850 4326 2012863.368669874 479499.5703310468 -96.6315 45.39 +4269 26850 -92.5185 47.37 3053733.6550991386 1197063.726331684 +26850 4326 3053733.6550991386 1197063.726331684 -92.5185 47.37 +4269 26851 -94.03 44.540000000000006 2616844.9315994056 889473.1214818907 +26851 4326 2616844.9315994056 889473.1214818907 -94.03 44.54000000000001 +4269 26851 -96.568 43.595 1944415.920494939 555675.8909348453 +26851 4326 1944415.920494939 555675.8909348453 -96.568 43.59499999999999 +4269 26851 -91.49199999999999 45.48500000000001 3267891.6691045123 1243883.1459767583 +26851 4326 3267891.6691045123 1243883.1459767583 -91.49199999999999 45.48500000000001 +4269 26852 -99.68 41.5 1728046.3838602335 607254.3110984005 +26852 4326 1728046.3838602335 607254.3110984005 -99.68 41.5 +4269 26852 -103.622 40.141000000000005 628114.1608038797 133286.49459627096 +26852 4326 628114.1608038797 133286.49459627096 -103.622 40.14100000000001 +4269 26852 -95.738 42.858999999999995 2782667.3042486086 1130381.2224117697 +26852 4326 2782667.3042486086 1130381.2224117697 -95.738 42.858999999999995 +4269 26853 -79.74000000000001 39.7 1900971.5776787135 437165.98975315323 +26853 4326 1900971.5776787135 437165.98975315323 -79.74000000000001 39.70000000000001 +4269 26853 -81.558 38.854 1382435.5042020096 135651.0932387416 +26853 4326 1382435.5042020096 135651.0932387416 -81.558 38.854000000000006 +4269 26853 -77.922 40.546 2407063.9876318914 749116.3560010638 +26853 4326 2407063.9876318914 749116.3560010638 -77.922 40.546000000000014 +4269 26854 -80.85 38.185 2011612.514260685 431534.1173270273 +26854 4326 2011612.514260685 431534.1173270273 -80.85 38.18500000000001 +4269 26854 -82.47 37.298500000000004 1540895.2406834285 112087.07653342806 +26854 4326 1540895.2406834285 112087.07653342806 -82.47 37.298500000000004 +4269 26854 -79.23 39.0715 2471031.672907779 759150.1806007742 +26854 4326 2471031.672907779 759150.1806007742 -79.23 39.07150000000001 +4152 26855 -68.47 45.675 991918.0735949625 732133.9019829989 +26855 4326 991918.0735949625 732133.9019829989 -68.46999974731882 45.67500933693338 +4152 26855 -69.874 44.0595 623087.7549926013 146197.72780132052 +26855 4326 623087.7549926013 146197.72780132052 -69.874000133923 44.05950900713355 +4152 26855 -67.066 47.2905 1340089.2295977187 1324518.5800617225 +26855 4326 1340089.2295977187 1324518.5800617225 -67.06599934915188 47.29050965131231 +4152 26856 -70.17500000000001 44.81 2950587.2651188183 720529.0952089471 +26856 4326 2950587.2651188183 720529.0952089471 -70.17500027855704 44.81000913442751 +4152 26856 -70.9985 43.217 2731007.6253422415 140935.8088169734 +26856 4326 2731007.6253422415 140935.8088169734 -70.99850046201176 43.217008817534975 +4152 26856 -69.3515 46.403 3158403.4773295885 1302446.5884031455 +26856 4326 3158403.4773295885 1302446.5884031455 -69.35150009311965 46.40300943842052 +4152 26857 -93.35499999999999 48.010000000000005 2562252.1196928965 878947.9838131967 +26857 4326 2562252.1196928965 878947.9838131967 -93.35500810075281 48.010008434468325 +4152 26857 -96.8335 46.777 1689484.930557216 451708.7351258669 +26857 4326 1689484.930557216 451708.7351258669 -96.8335089023631 46.77700800742111 +4152 26857 -89.8765 49.243 3394678.668121814 1344754.5702052503 +26857 4326 3394678.668121814 1344754.5702052503 -89.87650724286569 49.243008851819795 +4152 26858 -94.575 46.379999999999995 2542643.6648840555 831471.7338802997 +26858 4326 2542643.6648840555 831471.7338802997 -94.57500819832896 46.380008123590216 +4152 26858 -96.6315 45.39 2012863.368669874 479499.5703310468 +26858 4326 2012863.368669874 479499.5703310468 -96.63150862970046 45.39000783595432 +4152 26858 -92.5185 47.37 3053733.6550991386 1197063.726331684 +26858 4326 3053733.6550991386 1197063.726331684 -92.51850774931546 47.370008409059466 +4152 26859 -94.03 44.540000000000006 2616844.9315994056 889473.1214818907 +26859 4326 2616844.9315994056 889473.1214818907 -94.03000778023568 44.540007899170384 +4152 26859 -96.568 43.595 1944415.920494939 555675.8909348453 +26859 4326 1944415.920494939 555675.8909348453 -96.56800836114256 43.59500758545568 +4152 26859 -91.49199999999999 45.48500000000001 3267891.6691045123 1243883.1459767583 +26859 4326 3267891.6691045123 1243883.1459767583 -91.49200717246406 45.485008210230845 +4152 26860 -99.68 41.5 1728046.3838602335 607254.3110984005 +26860 4326 1728046.3838602335 607254.3110984005 -99.68000892094494 41.50000706362558 +4152 26860 -103.622 40.141000000000005 628114.1608038797 133286.49459627096 +26860 4326 628114.1608038797 133286.49459627096 -103.62200973837096 40.14100659840942 +4152 26860 -95.738 42.858999999999995 2782667.3042486086 1130381.2224117697 +26860 4326 2782667.3042486086 1130381.2224117697 -95.7380080407106 42.85900753244584 +4152 26861 -79.74000000000001 39.7 1900971.5776787135 437165.98975315323 +26861 4326 1900971.5776787135 437165.98975315323 -79.7400030472736 39.70000782967337 +4152 26861 -81.558 38.854 1382435.5042020096 135651.0932387416 +26861 4326 1382435.5042020096 135651.0932387416 -81.55800354821939 38.85400758953746 +4152 26861 -77.922 40.546 2407063.9876318914 749116.3560010638 +26861 4326 2407063.9876318914 749116.3560010638 -77.92200253628518 40.54600806621145 +4152 26862 -80.85 38.185 2011612.514260685 431534.1173270273 +26862 4326 2011612.514260685 431534.1173270273 -80.85000328930039 38.18500748458883 +4152 26862 -82.47 37.298500000000004 1540895.2406834285 112087.07653342806 +26862 4326 1540895.2406834285 112087.07653342806 -82.47000372262399 37.298507241192965 +4152 26862 -79.23 39.0715 2471031.672907779 759150.1806007742 +26862 4326 2471031.672907779 759150.1806007742 -79.23000284854531 39.071507725423324 +4759 26863 -68.47 45.675 991918.0735949625 732133.9019829989 +26863 4326 991918.0735949625 732133.9019829989 -68.47 45.675 +4759 26863 -69.874 44.0595 623087.7549926013 146197.72780132052 +26863 4326 623087.7549926013 146197.72780132052 -69.874 44.0595 +4759 26863 -67.066 47.2905 1340089.2295977187 1324518.5800617225 +26863 4326 1340089.2295977187 1324518.5800617225 -67.066 47.2905 +4759 26864 -70.17500000000001 44.81 2950587.2651188183 720529.0952089471 +26864 4326 2950587.2651188183 720529.0952089471 -70.17500000000001 44.81000000000001 +4759 26864 -70.9985 43.217 2731007.6253422415 140935.8088169734 +26864 4326 2731007.6253422415 140935.8088169734 -70.9985 43.217000000000006 +4759 26864 -69.3515 46.403 3158403.4773295885 1302446.5884031455 +26864 4326 3158403.4773295885 1302446.5884031455 -69.3515 46.403000000000006 +4759 26865 -93.35499999999999 48.010000000000005 2562252.1196928965 878947.9838131967 +26865 4326 2562252.1196928965 878947.9838131967 -93.35499999999999 48.01000000000001 +4759 26865 -96.8335 46.777 1689484.930557216 451708.7351258669 +26865 4326 1689484.930557216 451708.7351258669 -96.8335 46.77700000000001 +4759 26865 -89.8765 49.243 3394678.668121814 1344754.5702052503 +26865 4326 3394678.668121814 1344754.5702052503 -89.8765 49.243 +4759 26866 -94.575 46.379999999999995 2542643.6648840555 831471.7338802997 +26866 4326 2542643.6648840555 831471.7338802997 -94.575 46.37999999999999 +4759 26866 -96.6315 45.39 2012863.368669874 479499.5703310468 +26866 4326 2012863.368669874 479499.5703310468 -96.6315 45.39 +4759 26866 -92.5185 47.37 3053733.6550991386 1197063.726331684 +26866 4326 3053733.6550991386 1197063.726331684 -92.5185 47.37 +4759 26867 -94.03 44.540000000000006 2616844.9315994056 889473.1214818907 +26867 4326 2616844.9315994056 889473.1214818907 -94.03 44.54000000000001 +4759 26867 -96.568 43.595 1944415.920494939 555675.8909348453 +26867 4326 1944415.920494939 555675.8909348453 -96.568 43.59499999999999 +4759 26867 -91.49199999999999 45.48500000000001 3267891.6691045123 1243883.1459767583 +26867 4326 3267891.6691045123 1243883.1459767583 -91.49199999999999 45.48500000000001 +4759 26868 -99.68 41.5 1728046.3838602335 607254.3110984005 +26868 4326 1728046.3838602335 607254.3110984005 -99.68 41.5 +4759 26868 -103.622 40.141000000000005 628114.1608038797 133286.49459627096 +26868 4326 628114.1608038797 133286.49459627096 -103.622 40.14100000000001 +4759 26868 -95.738 42.858999999999995 2782667.3042486086 1130381.2224117697 +26868 4326 2782667.3042486086 1130381.2224117697 -95.738 42.858999999999995 +4759 26869 -79.74000000000001 39.7 1900971.5776787135 437165.98975315323 +26869 4326 1900971.5776787135 437165.98975315323 -79.74000000000001 39.70000000000001 +4759 26869 -81.558 38.854 1382435.5042020096 135651.0932387416 +26869 4326 1382435.5042020096 135651.0932387416 -81.558 38.854000000000006 +4759 26869 -77.922 40.546 2407063.9876318914 749116.3560010638 +26869 4326 2407063.9876318914 749116.3560010638 -77.922 40.546000000000014 +4759 26870 -80.85 38.185 2011612.514260685 431534.1173270273 +26870 4326 2011612.514260685 431534.1173270273 -80.85 38.18500000000001 +4759 26870 -82.47 37.298500000000004 1540895.2406834285 112087.07653342806 +26870 4326 1540895.2406834285 112087.07653342806 -82.47 37.298500000000004 +4759 26870 -79.23 39.0715 2471031.672907779 759150.1806007742 +26870 4326 2471031.672907779 759150.1806007742 -79.23 39.07150000000001 +4617 26891 -82.3 43.835 320884.01985230704 4855023.008696913 +26891 4326 320884.01985230704 4855023.008696913 -82.30000417633609 43.83500847709974 +4617 26891 -83.47 41.8865 224299.73349253612 4639021.263440096 +26891 4326 224299.73349253612 4639021.263440096 -83.47000436382233 41.88650807652502 +4617 26891 -81.13 45.783500000000004 411326.9483581694 5072427.79761347 +26891 4326 411326.9483581694 5072427.79761347 -81.13000399455295 45.78350886338444 +4617 26892 -81.0 50.605000000000004 304800.0 5607583.33382646 +26892 4326 304800.0 5607583.33382646 -81.00000452627992 50.605009603194254 +4617 26892 -82.35 46.4605 201106.3244268999 5147644.078890337 +26892 4326 201106.3244268999 5147644.078890337 -82.35000446166018 46.4605089098499 +4617 26892 -79.65 54.7495 391718.0987432869 6069573.696578401 +26892 4326 391718.0987432869 6069573.696578401 -79.65000468224879 54.749510215935565 +4617 26893 -84.0 50.795 304800.0 5628717.3001815565 +26893 4326 304800.0 5628717.3001815565 -84.00000557766087 50.795009452164926 +4617 26893 -85.35 46.4795 201142.4066109733 5149755.887885232 +26893 4326 201142.4066109733 5149755.887885232 -85.3500054231745 46.47950874547503 +4617 26893 -82.65 55.1105 390943.12263336655 6109753.223535529 +26893 4326 390943.12263336655 6109753.223535529 -82.65000585738296 55.110510074761294 +4617 26894 -87.0 51.935 304800.0 5755535.600946693 +26894 4326 304800.0 5755535.600946693 -87.00000677905525 51.935009402243494 +4617 26894 -88.35 47.6465 203380.4647399176 5279478.173564699 +26894 4326 203380.4647399176 5279478.173564699 -88.35000651981574 47.646508734225065 +4617 26894 -85.65 56.2235 388532.1709837478 6233645.02786236 +26894 4326 388532.1709837478 6233645.02786236 -85.65000719617466 56.223509989889436 +4617 26895 -90.0 52.435 304800.0 5811165.4693391705 +26895 4326 304800.0 5811165.4693391705 -90.00000787056405 52.43500924336083 +4617 26895 -91.35 48.4165 204880.4294295852 5365084.149450656 +26895 4326 204880.4294295852 5365084.149450656 -91.35000756337017 48.41650863567881 +4617 26895 -88.65 56.4535 388029.9376740323 6259249.762159773 +26895 4326 388029.9376740323 6259249.762159773 -88.65000833814054 56.45350978422967 +4617 26896 -93.0 51.63 304800.0 5721603.713476377 +26896 4326 304800.0 5721603.713476377 -93.0000086662014 51.63000891317452 +4617 26896 -94.35 48.417 204881.4093704535 5365139.741284148 +26896 4326 204881.4093704535 5365139.741284148 -94.35000846431966 48.41700841247297 +4617 26896 -91.65 54.843 391517.711160886 6079980.087531899 +26896 4326 391517.711160886 6079980.087531899 -91.65000896525594 54.84300937613839 +4617 26897 -94.83 50.965 386983.664340777 5648279.054044446 +26897 4326 386983.664340777 5648279.054044446 -94.83000909230798 50.96500868619813 +4617 26897 -95.127 48.9175 368777.6797239066 5420278.251220682 +26897 4326 368777.6797239066 5420278.251220682 -95.12700878228037 48.917508415398245 +4617 26897 -94.533 53.0125 403249.05654057354 5876430.670350219 +26897 4326 403249.05654057354 5876430.670350219 -94.53300945515981 53.01250893919937 +4617 26898 -53.519999999999996 48.225 266167.7742660153 5343042.305810534 +26898 4326 266167.7742660153 5343042.305810534 -53.5199945636975 48.22500996023101 +4617 26898 -54.402 46.7265 197637.93505436246 5177279.522066797 +26898 4326 197637.93505436246 5177279.522066797 -54.40199487948083 46.72650971442512 +4617 26898 -52.638 49.7235 330899.77029580396 5509604.837755639 +26898 4326 330899.77029580396 5509604.837755639 -52.63799423233777 49.72351019082315 +4617 26899 -55.995000000000005 50.760000000000005 305152.7607283704 5624824.161000914 +26899 4326 305152.7607283704 5624824.161000914 -55.99499545746615 50.760010353862036 +4617 26899 -57.3495 47.205000000000005 202566.6048048936 5230397.971164339 +26899 4326 202566.6048048936 5230397.971164339 -57.34949591834005 47.20500978691756 +4617 26899 -54.6405 54.315 393264.3743449206 6021228.46386013 +26899 4326 393264.3743449206 6021228.46386013 -54.640494930785 54.31501084360853 +4269 26901 -176.995 55.545 500315.49276570586 6155441.215371645 +26901 4326 500315.49276570586 6155441.215371645 -176.99500166096058 55.54500553348903 +4269 26901 -179.6995 48.6465 301167.493751609 5391677.341764835 +26901 4326 301167.493751609 5391677.341764835 -179.69950014234806 48.6465102644127 +4269 26901 -174.2905 62.4435 639819.2265721997 6926519.820266791 +26901 4326 639819.2265721997 6926519.820266791 -174.29050385338212 62.44350077245478 +4269 26902 -170.995 60.855 500271.661002635 6746636.777194004 +26902 4326 500271.661002635 6746636.777194004 -170.995 60.85499999999999 +4269 26902 -173.6995 49.87949999999999 306063.71185274015 5528727.910589498 +26902 4326 306063.71185274015 5528727.910589498 -173.6995 49.87949999999999 +4269 26902 -168.2905 71.8305 594273.4174823904 7972144.773295547 +26902 4326 594273.4174823904 7972144.773295547 -168.2905 71.83049999999999 +4269 26903 -164.995 61.905 500262.70150924154 6863597.136707529 +26903 4326 500262.70150924154 6863597.136707529 -164.995 61.905 +4269 26903 -167.6995 50.758500000000005 309609.5523102755 5626444.5756313205 +26903 4326 309609.5523102755 5626444.5756313205 -167.6995 50.7585 +4269 26903 -162.2905 73.0515 588133.8862965786 8108218.624054418 +26903 4326 588133.8862965786 8108218.624054418 -162.2905 73.05149999999999 +4269 26904 -158.995 45.14 500393.11562183144 4998502.8291628575 +26904 4326 500393.11562183144 4998502.8291628575 -158.995 45.14 +4269 26904 -161.6995 18.527 215000.0425342741 2050626.8474872098 +26904 4326 215000.0425342741 2050626.8474872098 -161.6995 18.527000000000005 +4269 26904 -156.2905 71.75299999999999 594661.6741446377 7963508.2650454845 +26904 4326 594661.6741446377 7963508.2650454845 -156.2905 71.75299999999997 +4269 26905 -152.995 45.135 500393.1499789808 4997947.379109246 +26905 4326 500393.1499789808 4997947.379109246 -152.995 45.13499999999999 +4269 26905 -155.6995 18.5175 214984.28460006637 2049574.8090327897 +26905 4326 214984.28460006637 2049574.8090327897 -155.6995 18.5175 +4269 26905 -150.2905 71.75249999999998 594664.1784571239 7963452.545814638 +26905 4326 594664.1784571239 7963452.545814638 -150.2905 71.75249999999998 +4269 26906 -146.995 64.09 500243.7739232476 7107043.182421333 +26906 4326 500243.7739232476 7107043.182421333 -146.995 64.09 +4269 26906 -149.6995 55.053999999999995 327570.85274766205 6104131.138473459 +26906 4326 327570.85274766205 6104131.138473459 -149.6995 55.05399999999999 +4269 26906 -144.2905 73.12599999999999 587757.9371418224 8116521.666334169 +26906 4326 587757.9371418224 8116521.666334169 -144.2905 73.12599999999999 +4269 26907 -140.995 62.82 500254.8208855695 6965534.409223122 +26907 4326 500254.8208855695 6965534.409223122 -140.995 62.82 +4269 26907 -143.6995 53.126999999999995 319386.2659014032 5889803.189372797 +26907 4326 319386.2659014032 5889803.189372797 -143.6995 53.12699999999999 +4269 26907 -138.2905 72.513 590846.7849352132 8048204.094003608 +26907 4326 590846.7849352132 8048204.094003608 -138.2905 72.513 +4269 26908 -135.0 63.74 500000.0 7068042.170895341 +26908 4326 500000.0 7068042.170895341 -135.0 63.739999999999995 +4269 26908 -137.7 49.628 305021.49189101974 5500772.589930895 +26908 4326 305021.49189101974 5500772.589930895 -137.7 49.628 +4269 26908 -132.3 77.852 563406.2209931078 8643313.609433059 +26908 4326 563406.2209931078 8643313.609433059 -132.3 77.85199999999999 +4269 26909 -129.0 58.155 500000.0 6445967.000447241 +26909 4326 500000.0 6445967.000447241 -129.0 58.15500000000001 +4269 26909 -131.7 37.657500000000006 261826.4123717048 4171244.9766823673 +26909 4326 261826.4123717048 4171244.9766823673 -131.7 37.6575 +4269 26909 -126.3 78.6525 559285.6390359548 8732560.458211271 +26909 4326 559285.6390359548 8732560.458211271 -126.30000000000001 78.65249999999999 +4269 26910 -122.995 56.17 500310.46883742523 6225000.400311399 +26910 4326 500310.46883742523 6225000.400311399 -122.995 56.17 +4269 26910 -125.6995 33.103 248079.48127794085 3665948.2616834007 +26910 4326 248079.48127794085 3665948.2616834007 -125.6995 33.102999999999994 +4269 26910 -120.2905 79.237 556467.3018972605 8797737.30207038 +26910 4326 556467.3018972605 8797737.30207038 -120.2905 79.23700000000001 +4269 26911 -117.0 57.19 500000.0 6338536.156442322 +26911 4326 500000.0 6338536.156442322 -117.0 57.18999999999999 +4269 26911 -119.7 33.510999999999996 249204.69409753956 3711202.704348121 +26911 4326 249204.69409753956 3711202.704348121 -119.7 33.511 +4269 26911 -114.3 80.869 547817.4138364063 8979694.573509105 +26911 4326 547817.4138364063 8979694.573509105 -114.3 80.86899999999999 +4269 26912 -111.0 57.665 500000.0 6391414.561762135 +26912 4326 500000.0 6391414.561762135 -111.0 57.665 +4269 26912 -113.7 33.963499999999996 250519.28524183325 3761394.3710331717 +26912 4326 250519.28524183325 3761394.3710331717 -113.7 33.96349999999999 +4269 26912 -108.3 81.3665 545232.7339301229 9035168.001523199 +26912 4326 545232.7339301229 9035168.001523199 -108.3 81.3665 +4269 26913 -105.0 56.489999999999995 499999.99999999924 6260617.436288856 +26913 4326 499999.99999999924 6260617.436288856 -105.00000000000001 56.48999999999999 +4269 26913 -107.7 31.731 244186.18999411535 3513791.1947232876 +26913 4326 244186.18999411535 3513791.1947232876 -107.7 31.73100000000001 +4269 26913 -102.3 81.249 545843.5056323694 9022066.133052066 +26913 4326 545843.5056323694 9022066.133052066 -102.30000000000001 81.24899999999998 +4269 26914 -99.0 54.915 500000.0 6085332.772336158 +26914 4326 500000.0 6085332.772336158 -99.0 54.915 +4269 26914 -101.7 28.7385 236308.17835076188 3182002.9731624504 +26914 4326 236308.17835076188 3182002.9731624504 -101.7 28.7385 +4269 26914 -96.3 81.0915 546661.892106115 9004504.154166928 +26914 4326 546661.892106115 9004504.154166928 -96.3 81.09149999999998 +4269 26915 -93.0 54.805 499999.9999999992 6073092.366010772 +26915 4326 499999.9999999992 6073092.366010772 -93.0 54.80500000000001 +4269 26915 -95.7 28.5295 235784.5944759639 3158834.817605434 +26915 4326 235784.5944759639 3158834.817605434 -95.7 28.52949999999999 +4269 26915 -90.3 81.0805 546719.0359553315 9003277.607590653 +26915 4326 546719.0359553315 9003277.607590653 -90.3 81.08049999999999 +4269 26916 -87.0 53.985 500000.0000000008 5981852.756746888 +26916 4326 500000.0000000008 5981852.756746888 -87.0 53.98499999999999 +4269 26916 -89.7 26.9715 231992.697174986 2986144.048314531 +26916 4326 231992.697174986 2986144.048314531 -89.7 26.971499999999992 +4269 26916 -84.3 80.9985 547144.9621390222 8994134.278363165 +26916 4326 547144.9621390222 8994134.278363165 -84.3 80.99849999999999 +4269 26917 -81.0 53.905 500000.0 5972952.000252395 +26917 4326 500000.0 5972952.000252395 -81.0 53.904999999999994 +4269 26917 -83.7 26.819499999999998 231633.31274627626 2969297.7765547982 +26917 4326 231633.31274627626 2969297.7765547982 -83.7 26.819499999999998 +4269 26917 -78.3 80.9905 547186.5106783832 8993242.247946905 +26917 4326 547186.5106783832 8993242.247946905 -78.3 80.99049999999998 +4269 26918 -75.0 56.14 500000.0 6221661.385911501 +26918 4326 500000.0 6221661.385911501 -75.0 56.13999999999999 +4269 26918 -77.7 31.066000000000003 242374.43361964216 3440050.2293497124 +26918 4326 242374.43361964216 3440050.2293497124 -77.7 31.066 +4269 26918 -72.3 81.214 546025.399908578 9018163.461110169 +26918 4326 546025.399908578 9018163.461110169 -72.3 81.214 +4269 26919 -69.0 49.46 500000.0 5478593.845487721 +26919 4326 500000.0 5478593.845487721 -69.0 49.46000000000001 +4269 26919 -71.7 18.374 214694.34128144704 2033684.3767610153 +26919 4326 214694.34128144704 2033684.3767610153 -71.7 18.373999999999995 +4269 26919 -66.3 80.54599999999999 549493.5603225846 8943679.29293833 +26919 4326 549493.5603225846 8943679.29293833 -66.3 80.546 +4269 26920 -63.0 49.815000000000005 500000.0 5518061.886659407 +26920 4326 500000.0 5518061.886659407 -63.0 49.815000000000005 +4269 26920 -65.7 19.0485 215824.3855716869 2108380.3016587333 +26920 4326 215824.3855716869 2108380.3016587333 -65.7 19.0485 +4269 26920 -60.3 80.58149999999999 549309.4170185914 8947637.603653427 +26920 4326 549309.4170185914 8947637.603653427 -60.300000000000004 80.58149999999998 +4269 26921 -57.0 61.28 500000.0000000007 6793975.671579958 +26921 4326 500000.0000000007 6793975.671579958 -57.00000000000001 61.28000000000001 +4269 26921 -59.7 40.832 272337.899245773 4523616.464762851 +26921 4326 272337.899245773 4523616.464762851 -59.7 40.831999999999994 +4269 26921 -54.3 81.72800000000001 543352.4364263249 9075477.545223918 +26921 4326 543352.4364263249 9075477.545223918 -54.3 81.72800000000001 +4269 26922 -50.995000000000005 48.575 500368.8140584385 5380212.31033272 +26922 4326 500368.8140584385 5380212.31033272 -50.99500000000001 48.57500000000001 +4269 26922 -53.6995 40.4075 270933.38613038993 4476486.695823391 +26922 4326 270933.38613038993 4476486.695823391 -53.6995 40.407499999999985 +4269 26922 -48.2905 56.74249999999999 665706.9021386893 6292000.1815662505 +26922 4326 665706.9021386893 6292000.1815662505 -48.2905 56.74249999999999 +4269 26923 -45.0 47.16 500000.0 5222944.579285258 +26923 4326 500000.0 5222944.579285258 -45.0 47.15999999999999 +4269 26923 -47.7 40.581 271480.82296725106 4495749.832928928 +26923 4326 271480.82296725106 4495749.832928928 -47.7 40.581 +4269 26923 -42.3 53.739 678069.2260187293 5957867.468179671 +26923 4326 678069.2260187293 5957867.468179671 -42.3 53.739 +4269 26929 -85.84 32.995 199376.9683514333 276640.7418256493 +26929 4326 199376.9683514333 276640.7418256493 -85.84 32.995000000000005 +4269 26929 -86.69500000000001 31.1905 117873.04652490633 76870.30876444188 +26929 4326 117873.04652490633 76870.30876444188 -86.69500000000001 31.1905 +4269 26929 -84.985 34.7995 277629.11731132085 477116.87613607576 +26929 4326 277629.11731132085 477116.87613607576 -84.985 34.7995 +4269 26930 -87.39 32.58 610327.5962043395 286042.60682333034 +26930 4326 610327.5962043395 286042.60682333034 -87.39 32.580000000000005 +4269 26930 -88.37100000000001 30.384 516289.87988403835 42887.583717217465 +26930 4326 516289.87988403835 42887.583717217465 -88.37100000000001 30.383999999999997 +4269 26930 -86.40899999999999 34.776 699861.512213123 530136.2878085349 +26930 4326 699861.512213123 530136.2878085349 -86.40899999999999 34.776 +4269 26931 -135.495 57.480000000000004 709022.2275555171 630020.6004242897 +26931 4326 709022.2275555171 630020.6004242897 -135.49500000000003 57.47999999999999 +4269 26931 -140.4495 54.897 383709.09014116693 362275.91215463355 +26931 4326 383709.09014116693 362275.91215463355 -140.4495 54.89700000000002 +4269 26931 -130.5405 60.063 992788.9149156073 920483.9307453353 +26931 4326 992788.9149156073 920483.9307453353 -130.54049999999998 60.06300000000001 +4269 26932 -142.495 64.94 476598.2733494 1218703.2590226599 +26932 4326 476598.2733494 1218703.2590226599 -142.495 64.94 +4269 26932 -143.8585 60.242 397073.46798556915 696498.9178886266 +26932 4326 397073.46798556915 696498.9178886266 -143.8585 60.242 +4269 26932 -141.1315 69.63799999999999 533735.232831844 1742745.5220906406 +26932 4326 533735.232831844 1742745.5220906406 -141.1315 69.63799999999999 +4269 26933 -146.0 65.05 500000.0 1230874.6384843953 +26933 4326 500000.0 1230874.6384843953 -146.0 65.05 +4269 26933 -147.8 60.253 400346.16334001743 697634.2588449224 +26933 4326 400346.16334001743 697634.2588449224 -147.8 60.252999999999986 +4269 26933 -144.2 69.847 569223.8152984743 1766840.1544494042 +26933 4326 569223.8152984743 1766840.1544494042 -144.2 69.847 +4269 26934 -150.0 64.87 500000.0 1210808.0839250025 +26934 4326 500000.0 1210808.0839250025 -150.0 64.87 +4269 26934 -151.809 59.686 398121.27172606904 634499.4378558247 +26934 4326 398121.27172606904 634499.4378558247 -151.80900000000003 59.686 +4269 26934 -148.191 70.054 568885.014310113 1789932.4850946558 +26934 4326 568885.014310113 1789932.4850946558 -148.191 70.054 +4269 26935 -153.93 63.5 503485.94536649715 1058097.4330165344 +26935 4326 503485.94536649715 1058097.4330165344 -153.93 63.50000000000001 +4269 26935 -155.793 56.498 389590.6245938524 279510.3993634731 +26935 4326 389590.6245938524 279510.3993634731 -155.793 56.49799999999999 +4269 26935 -152.067 70.502 572018.4257739565 1840031.82643212 +26935 4326 572018.4257739565 1840031.82643212 -152.067 70.502 +4269 26936 -157.995 63.145 500252.0814158846 1018528.7945634346 +26936 4326 500252.0814158846 1018528.7945634346 -157.99500000000003 63.145 +4269 26936 -159.7995 55.7155 386918.9132892374 192419.31067588035 +26936 4326 386918.9132892374 192419.31067588035 -159.7995 55.7155 +4269 26936 -156.19050000000001 70.5745 567177.5517000024 1847974.9441158164 +26936 4326 567177.5517000024 1847974.9441158164 -156.19050000000001 70.5745 +4269 26937 -162.005 62.53 499742.59696889075 949988.3243515419 +26937 4326 499742.59696889075 949988.3243515419 -162.005 62.53 +4269 26937 -163.80949999999999 55.141 384627.2292582616 128493.51815071332 +26937 4326 384627.2292582616 128493.51815071332 -163.80949999999999 55.14099999999999 +4269 26937 -160.2005 69.919 568967.7430237273 1774868.1191782567 +26937 4326 568967.7430237273 1774868.1191782567 -160.2005 69.91900000000001 +4269 26938 -166.13 61.695 493120.92207579594 856945.9191454424 +26938 4326 493120.92207579594 856945.9191454424 -166.13 61.69499999999999 +4269 26938 -168.047 55.075500000000005 369272.75018676487 121622.30920951095 +26938 4326 369272.75018676487 121622.30920951095 -168.047 55.07550000000001 +4269 26938 -164.213 68.3145 573703.4914816638 1595953.9075491133 +26938 4326 573703.4914816638 1595953.9075491133 -164.213 68.3145 +4269 26939 -170.57999999999998 61.155 468774.10340198886 796907.9866779154 +26939 4326 468774.10340198886 796907.9866779154 -170.57999999999998 61.155 +4269 26939 -172.902 56.9565 323481.636023819 332869.3282945642 +26939 4326 323481.636023819 332869.3282945642 -172.902 56.9565 +4269 26939 -168.258 65.3535 581075.9216982052 1265830.5148130064 +26939 4326 581075.9216982052 1265830.5148130064 -168.258 65.3535 +4269 26940 -176.21 52.82 985844.7880543226 202527.91516819393 +26940 4326 985844.7880543226 202527.91516819393 -176.21000196094727 52.82000743317968 +4269 26940 173.557 51.452 276522.2880442635 102933.70544811105 +26940 4326 276522.2880442635 102933.70544811105 173.55700322857464 51.452008433488416 +4269 26940 -165.977 54.188 1652267.2955383519 400290.4365906697 +26940 4326 1652267.2955383519 400290.4365906697 -165.9770074237571 54.188006889517624 +4269 26941 -122.22 40.8 1981436.393996671 662877.7471431005 +26941 4326 1981436.393996671 662877.7471431005 -122.22 40.8 +4269 26941 -124.227 39.711000000000006 1809032.8706182928 544364.0613099996 +26941 4326 1809032.8706182928 544364.0613099996 -124.227 39.71100000000001 +4269 26941 -120.213 41.888999999999996 2148310.5570726353 785305.6470252918 +26941 4326 2148310.5570726353 785305.6470252918 -120.21300000000001 41.88899999999998 +4269 26942 -121.80000000000001 39.09 2017301.8204829379 658016.692001398 +26942 4326 2017301.8204829379 658016.692001398 -121.80000000000001 39.089999999999996 +4269 26942 -123.834 38.127 1839195.8283859724 552723.7703899188 +26942 4326 1839195.8283859724 552723.7703899188 -123.834 38.127 +4269 26942 -119.766 40.053 2190614.0811918867 767255.0743562812 +26942 4326 2190614.0811918867 767255.0743562812 -119.76600000000002 40.053000000000004 +4269 26943 -120.425 37.72 2006611.9340582322 635399.2547793326 +26943 4326 2006611.9340582322 635399.2547793326 -120.42499999999998 37.71999999999999 +4269 26943 -122.7605 36.82899999999999 1798347.426047734 538949.0753376134 +26943 4326 1798347.426047734 538949.0753376134 -122.7605 36.82899999999999 +4269 26943 -118.0895 38.611000000000004 2209937.041178584 736997.6950945166 +26943 4326 2209937.041178584 736997.6950945166 -118.08950000000002 38.611 +4269 26944 -118.815 36.68 2016534.9266023266 649444.111312916 +26944 4326 2016534.9266023266 649444.111312916 -118.81499999999998 36.67999999999997 +4269 26944 -121.6905 35.870000000000005 1757042.2782625472 562953.5650761565 +26944 4326 1757042.2782625472 562953.5650761565 -121.6905 35.87000000000002 +4269 26944 -115.93950000000001 37.489999999999995 2270631.083070669 743632.1654617812 +26944 4326 2270631.083070669 743632.1654617812 -115.93950000000002 37.489999999999995 +4269 26945 -117.77000000000001 34.285 2021176.3034016618 587100.3705963591 +26945 4326 2021176.3034016618 587100.3705963591 -117.77 34.28500000000002 +4269 26945 -121.055 32.9125 1714139.7936607758 439167.22925522097 +26945 4326 1714139.7936607758 439167.22925522097 -121.055 32.91250000000001 +4269 26945 -114.485 35.6575 2318240.215058904 744899.1637714047 +26945 4326 2318240.215058904 744899.1637714047 -114.485 35.6575 +4269 26946 -116.285 33.305 1996740.5868549028 626244.559176123 +26946 4326 1996740.5868549028 626244.559176123 -116.285 33.305 +4269 26946 -117.96350000000001 32.6075 1839164.448277042 550211.7107019561 +26946 4326 1839164.448277042 550211.7107019561 -117.96350000000001 32.6075 +4269 26946 -114.6065 34.0025 2151827.0133517752 704802.7808291488 +26946 4326 2151827.0133517752 704802.7808291488 -114.6065 34.0025 +4269 26948 -110.375 34.17 194153.50067005004 351528.6430514126 +26948 4326 194153.50067005004 351528.6430514126 -110.375 34.17 +4269 26948 -111.5765 31.613999999999997 79592.58561085619 68933.09865045993 +26948 4326 79592.58561085619 68933.09865045993 -111.5765 31.613999999999994 +4269 26948 -109.1735 36.726 302072.0270641723 635525.7685795034 +26948 4326 302072.0270641723 635525.7685795034 -109.1735 36.72599999999999 +4269 26949 -111.895 34.17 215357.474309584 351509.2431937203 +26949 4326 215357.474309584 351509.2431937203 -111.895 34.17 +4269 26949 -113.2045 31.613999999999997 91169.08245612771 68790.21379401149 +26949 4326 91169.08245612771 68790.21379401149 -113.2045 31.613999999999994 +4269 26949 -110.5855 36.726 332264.3607156152 635892.0351637731 +26949 4326 332264.3607156152 635892.0351637731 -110.5855 36.726000000000006 +4269 26950 -113.66499999999999 34.525 221163.54809494727 390901.08565793367 +26950 4326 221163.54809494727 390901.08565793367 -113.66499999999999 34.525 +4269 26950 -114.6955 32.2975 124310.60752930673 144250.73145847756 +26950 4326 124310.60752930673 144250.73145847756 -114.69550000000001 32.2975 +4269 26950 -112.6345 36.7525 312968.5783435126 638607.9599893158 +26950 4326 312968.5783435126 638607.9599893158 -112.6345 36.7525 +4269 26951 -92.13 35.585 388218.3760538642 138869.51677349143 +26951 4326 388218.3760538642 138869.51677349143 -92.13 35.58500000000002 +4269 26951 -94.37100000000001 34.7615 182942.28431090515 50115.795887343964 +26951 4326 182942.28431090515 50115.795887343964 -94.37100000000001 34.7615 +4269 26951 -89.889 36.408500000000004 589342.0332958896 232263.49768251824 +26951 4326 589342.0332958896 232263.49768251824 -89.889 36.40850000000002 +4269 26952 -92.44 34.055 359380.3456728528 554070.3461636706 +26952 4326 359380.3456728528 554070.3461636706 -92.44 34.055 +4269 26952 -94.27600000000001 33.1145 187583.30831943935 452033.15065267665 +26952 4326 187583.30831943935 452033.15065267665 -94.27600000000001 33.1145 +4269 26952 -90.604 34.9955 527448.8375440208 659180.0131279876 +26952 4326 527448.8375440208 659180.0131279876 -90.604 34.9955 +4269 26953 -105.55000000000001 40.285 910150.1227950495 410466.04419278353 +26953 4326 910150.1227950495 410466.04419278353 -105.55000000000001 40.285 +4269 26953 -108.709 39.6325 638965.6755298494 343001.4062162024 +26953 4326 638965.6755298494 343001.4062162024 -108.709 39.63249999999999 +4269 26953 -102.391 40.9375 1176178.9159974535 487510.83186888543 +26953 4326 1176178.9159974535 487510.83186888543 -102.391 40.93749999999999 +4269 26954 -105.55000000000001 39.115 910077.8060260228 447076.0864687894 +26954 4326 910077.8060260228 447076.0864687894 -105.55000000000001 39.115 +4269 26954 -108.709 38.2375 633503.6654187832 354628.51937078615 +26954 4326 633503.6654187832 354628.51937078615 -108.709 38.237500000000004 +4269 26954 -102.391 39.99250000000001 1179883.6778052924 549041.2134201529 +26954 4326 1179883.6778052924 549041.2134201529 -102.391 39.992500000000014 +4269 26955 -105.55000000000001 37.83 910000.3279125895 433913.0553515627 +26955 4326 910000.3279125895 433913.0553515627 -105.55000000000001 37.82999999999999 +4269 26955 -108.709 37.065 629052.8991190449 353911.53463970317 +26955 4326 629052.8991190449 353911.53463970317 -108.709 37.06500000000002 +4269 26955 -102.391 38.595 1185210.975475688 523332.2323330514 +26955 4326 1185210.975475688 523332.2323330514 -102.391 38.595000000000006 +4269 26956 -72.755 41.515 304383.2321191824 228105.00864740624 +26956 4326 304383.2321191824 228105.00864740624 -72.755 41.51499999999999 +4269 26956 -73.63250000000001 41.0335 230588.64706697664 175009.14457170662 +26956 4326 230588.64706697664 175009.14457170662 -73.63250000000001 41.03349999999999 +4269 26956 -71.8775 41.9965 377091.7689576581 281949.1244673686 +26956 4326 377091.7689576581 281949.1244673686 -71.8775 41.9965 +4269 26957 -75.38499999999999 39.144999999999996 202737.5480806781 127103.25313109877 +26957 4326 202737.5480806781 127103.25313109877 -75.38499999999999 39.144999999999996 +4269 26957 -75.7585 38.5105 170185.3710576928 56721.266312237676 +26957 4326 170185.3710576928 56721.266312237676 -75.75850000000001 38.51049999999999 +4269 26957 -75.0115 39.7795 234709.65364230535 197625.8780571235 +26957 4326 234709.65364230535 197625.8780571235 -75.01150000000001 39.7795 +4269 26958 -81.15 27.619999999999997 185195.14759915508 364110.21714043804 +26958 4326 185195.14759915508 364110.21714043804 -81.15 27.62 +4269 26958 -82.212 24.731 77384.67721616034 44587.952464870745 +26958 4326 77384.67721616034 44587.952464870745 -82.212 24.730999999999998 +4269 26958 -80.088 30.508999999999997 287539.61535454553 684644.0779293055 +26958 4326 287539.61535454553 684644.0779293055 -80.088 30.509 +4269 26959 -82.235 27.935000000000002 176872.42951769338 399028.39882096706 +26959 4326 176872.42951769338 399028.39882096706 -82.235 27.935 +4269 26959 -83.2295 26.4365 77365.23380374948 233558.28670167812 +26959 4326 77365.23380374948 233558.28670167812 -83.2295 26.4365 +4269 26959 -81.2405 29.433500000000002 273690.74826097145 565316.0981896543 +26959 4326 273690.74826097145 565316.0981896543 -81.2405 29.433500000000006 +4269 26960 -84.83500000000001 30.11 567714.4986294459 123087.90795582991 +26960 4326 567714.4986294459 123087.90795582991 -84.83500000000002 30.109999999999992 +4269 26960 -87.3505 29.3 323067.84639475704 36716.95956284991 +26960 4326 323067.84639475704 36716.95956284991 -87.35050000000001 29.299999999999983 +4269 26960 -82.3195 30.92 808415.1822636275 214828.78102081481 +26960 4326 808415.1822636275 214828.78102081481 -82.3195 30.920000000000016 +4269 26961 -155.42000000000002 19.6 508392.43257363234 84865.06216114241 +26961 4326 508392.43257363234 84865.06216114241 -155.4200047033016 19.599995505531474 +4269 26961 -156.03199999999998 18.943 443966.8337234195 12223.129253569174 +26961 4326 443966.8337234195 12223.129253569174 -156.0320048187296 18.9429953257557 +4269 26961 -154.80800000000002 20.256999999999998 572296.4680864818 157743.97569391556 +26961 4326 572296.4680864818 157743.97569391556 -154.80800458701253 20.256995686451816 +4269 26962 -156.645 20.855 502254.7853232328 57752.88297264607 +26962 4326 502254.7853232328 57752.88297264607 -156.64500501236662 20.854995785680615 +4269 26962 -157.2885 20.4905 435132.0824620548 17522.483149056337 +26962 4326 435132.0824620548 17522.483149056337 -157.28850514162204 20.490495675768894 +4269 26962 -156.00150000000002 21.2195 569054.8823151813 98253.00576796383 +26962 4326 569054.8823151813 98253.00576796383 -156.0015048820558 21.21949589585218 +4269 26963 -157.97000000000003 21.475 503109.1061699619 34139.0008563712 +26963 4326 503109.1061699619 34139.0008563712 -157.9700053252301 21.474995900015404 +4269 26963 -158.294 21.2275 469479.4657924552 6763.726930458374 +26963 4326 469479.4657924552 6763.726930458374 -158.29400538713924 21.22749582810886 +4269 26963 -157.64600000000002 21.7225 536625.296198369 61584.71779271407 +26963 4326 536625.296198369 61584.71779271407 -157.64600526304005 21.7224959720843 +4269 26964 -159.54 22.049999999999997 495871.0045759173 23991.690169427377 +26964 4326 495871.0045759173 23991.690169427377 -159.54000569007522 22.0499959905637 +4269 26964 -159.819 21.834 467021.380951611 107.96252484768765 +26964 4326 467021.380951611 107.96252484768765 -159.81900574201737 21.833995927456293 +4269 26964 -159.261 22.266 524633.1681034412 47928.577949660204 +26964 4326 524633.1681034412 47928.577949660204 -159.26100563792076 22.265996053802695 +4269 26965 -160.145 21.9 502238.9118068838 25836.542382528078 +26965 4326 502238.9118068838 25836.542382528078 -160.1450058152408 21.89999593230731 +4269 26965 -160.2845 21.747 487810.8059930551 8899.659826272124 +26965 4326 487810.8059930551 8899.659826272124 -160.28450583917584 21.74699588970065 +4269 26965 -160.0055 22.053 516636.24116117816 42786.85294238828 +26965 4326 516636.24116117816 42786.85294238828 -160.00550579126147 22.052995974981737 +4269 26966 -82.12 32.519999999999996 204384.1730120967 279375.5698015051 +26966 4326 204384.1730120967 279375.5698015051 -82.12 32.51999999999999 +4269 26966 -83.335 30.576 87935.98120049336 64428.67874660541 +26966 4326 87935.98120049336 64428.67874660541 -83.335 30.576000000000004 +4269 26966 -80.905 34.464 315913.03382685786 495690.7435459547 +26966 4326 315913.03382685786 495690.7435459547 -80.905 34.464000000000006 +4269 26967 -84.3 32.815 687514.8760667271 312094.28572832706 +26967 4326 687514.8760667271 312094.28572832706 -84.3 32.815 +4269 26967 -85.479 30.8395 574464.4552982544 93794.36979869824 +26967 4326 574464.4552982544 93794.36979869824 -85.479 30.839500000000005 +4269 26967 -83.121 34.790499999999994 795691.9523103088 531682.9947136147 +26967 4326 795691.9523103088 531682.9947136147 -83.121 34.7905 +4269 26968 -112.14 43.370000000000005 202161.21936047942 189202.38724812766 +26968 4326 202161.21936047942 189202.38724812766 -112.14 43.37000000000001 +4269 26968 -113.13 42.128 120351.05903192172 51687.362961994455 +26968 4326 120351.05903192172 51687.362961994455 -113.13 42.128 +4269 26968 -111.15 44.612 280695.92876894755 327698.8013920951 +26968 4326 280695.92876894755 327698.8013920951 -111.15 44.611999999999995 +4269 26969 -113.985 43.845 501206.1570626486 241974.0498370269 +26969 4326 501206.1570626486 241974.0498370269 -113.985 43.845 +4269 26969 -115.1685 42.1755 403459.76413783856 57175.03314187794 +26969 4326 403459.76413783856 57175.03314187794 -115.16850000000001 42.17550000000001 +4269 26969 -112.8015 45.514500000000005 593643.3516935457 428187.0188976105 +26969 4326 593643.3516935457 428187.0188976105 -112.8015 45.514500000000005 +4269 26970 -115.78 45.5 797655.41440746 425871.2238890269 +26970 4326 797655.41440746 425871.2238890269 -115.78 45.50000000000001 +4269 26970 -117.094 42.341 689251.4777751659 75770.44093186814 +26970 4326 689251.4777751659 75770.44093186814 -117.09399999999998 42.340999999999994 +4269 26970 -114.466 48.659 894584.9816078962 777836.67337109 +26970 4326 894584.9816078962 777836.67337109 -114.46600000000001 48.65899999999999 +4269 26971 -88.145 39.78 316133.6048133849 345590.73500594514 +26971 4326 316133.6048133849 345590.73500594514 -88.145 39.78000000000001 +4269 26971 -89.1575 37.332 226960.888561657 74153.85203580958 +26971 4326 226960.888561657 74153.85203580958 -89.1575 37.332 +4269 26971 -87.1325 42.228 399132.2493135867 618125.4239692872 +26971 4326 399132.2493135867 618125.4239692872 -87.1325 42.227999999999994 +4269 26972 -90.22 39.739999999999995 695428.7036032164 341122.5008347094 +26972 4326 695428.7036032164 341122.5008347094 -90.22 39.739999999999995 +4269 26972 -91.39 37.247 591466.4497448911 65101.11165521788 +26972 4326 591466.4497448911 65101.11165521788 -91.39 37.247 +4269 26972 -89.05 42.233 792173.5648998992 618565.43078374 +26972 4326 792173.5648998992 618565.43078374 -89.05 42.233 +4269 26973 -85.685 39.86 98431.30571691465 511973.6556178053 +26973 4326 98431.30571691465 511973.6556178053 -85.685 39.85999999999999 +4269 26973 -86.4995 38.141000000000005 26992.295511845572 321471.9125532987 +26973 4326 26992.295511845572 321471.9125532987 -86.4995 38.141 +4269 26973 -84.8705 41.579 166394.06102200958 703165.6599238857 +26973 4326 166394.06102200958 703165.6599238857 -84.8705 41.579 +4269 26974 -87.16999999999999 39.77 892574.6756391372 501984.6231915935 +26974 4326 892574.6756391372 501984.6231915935 -87.16999999999999 39.77 +4269 26974 -88.00699999999999 37.970000000000006 818840.8874181742 302566.75752018945 +26974 4326 818840.8874181742 302566.75752018945 -88.00699999999999 37.97000000000001 +4269 26974 -86.333 41.57 962580.5798687736 702131.8622874243 +26974 4326 962580.5798687736 702131.8622874243 -86.333 41.57 +4269 26975 -93.4 42.68 1508195.834348411 1131075.1318394763 +26975 4326 1508195.834348411 1131075.1318394763 -93.4 42.680000000000014 +4269 26975 -96.325 41.933 1265738.527574641 1052010.8755992097 +26975 4326 1265738.527574641 1052010.8755992097 -96.325 41.933000000000014 +4269 26975 -90.47500000000001 43.427 1744902.3804289864 1218437.3308182764 +26975 4326 1744902.3804289864 1218437.3308182764 -90.47500000000001 43.42700000000001 +4269 26976 -93.14 41.2 530195.2801683228 133320.7160985994 +26976 4326 530195.2801683228 133320.7160985994 -93.14000000000001 41.20000000000001 +4269 26976 -95.84 40.444 301495.59603204846 51976.20620905145 +26976 4326 301495.59603204846 51976.20620905145 -95.84 40.444 +4269 26976 -90.44 41.955999999999996 753654.5170276335 221683.68954160615 +26976 4326 753654.5170276335 221683.68954160615 -90.44 41.95599999999999 +4269 26977 -98.32 39.265 372384.49819812784 103474.51018951085 +26977 4326 372384.49819812784 103474.51018951085 -98.32 39.26500000000003 +4269 26977 -101.686 38.594500000000004 78962.18575527694 35527.07937257582 +26977 4326 78962.18575527694 35527.07937257582 -101.686 38.5945 +4269 26977 -94.954 39.9355 660312.5113567541 182246.34859156059 +26977 4326 660312.5113567541 182246.34859156059 -94.954 39.93550000000001 +4269 26978 -98.32499999999999 37.935 415383.2434573826 540780.1749808954 +26978 4326 415383.2434573826 540780.1749808954 -98.32499999999999 37.935 +4269 26978 -101.6775 37.084500000000006 117521.86511747976 451187.8656569628 +26978 4326 117521.86511747976 451187.8656569628 -101.6775 37.08449999999999 +4269 26978 -94.9725 38.7855 706437.5117094776 640969.3042826953 +26978 4326 706437.5117094776 640969.3042826953 -94.9725 38.7855 +4269 26980 -85.75999999999999 37.33 499113.7931707417 610604.196224401 +26980 4326 499113.7931707417 610604.196224401 -85.75999999999999 37.33 +4269 26980 -89.189 36.574000000000005 192247.5353599636 532309.7194656162 +26980 4326 192247.5353599636 532309.7194656162 -89.189 36.57400000000001 +4269 26980 -82.331 38.086 799892.1099969771 699938.1634462873 +26980 4326 799892.1099969771 699938.1634462873 -82.331 38.086 +4269 26981 -92.455 31.94 1004254.595522245 159660.69532187734 +26981 4326 1004254.595522245 159660.69532187734 -92.455 31.93999999999999 +4269 26981 -93.8905 30.959000000000003 867141.0681850316 51745.95904705165 +26981 4326 867141.0681850316 51745.95904705165 -93.8905 30.959000000000017 +4269 26981 -91.0195 32.921 1138485.7486137254 269389.5708371389 +26981 4326 1138485.7486137254 269389.5708371389 -91.0195 32.921 +4269 26982 -91.345 29.96 998873.9589036357 161832.71462681066 +26982 4326 998873.9589036357 161832.71462681066 -91.345 29.96000000000001 +4269 26982 -93.6805 28.961000000000002 771204.4762175585 53445.368118892235 +26982 4326 771204.4762175585 53445.368118892235 -93.6805 28.961000000000002 +4269 26982 -89.0095 30.959 1222030.1736550392 274830.1880059256 +26982 4326 1222030.1736550392 274830.1880059256 -89.0095 30.959 +4269 26983 -68.47 45.675 302337.23350621155 223154.85963413733 +26983 4326 302337.23350621155 223154.85963413733 -68.47 45.675 +4269 26983 -69.874 44.0595 189917.5275568 44561.156556155605 +26983 4326 189917.5275568 44561.156556155605 -69.874 44.0595 +4269 26983 -67.066 47.2905 408460.01410141285 403714.07063095423 +26983 4326 408460.01410141285 403714.07063095423 -67.066 47.2905 +4269 26984 -70.17500000000001 44.81 899340.7970898099 219617.70745510195 +26984 4326 899340.7970898099 219617.70745510195 -70.17500000000001 44.81000000000001 +4269 26984 -70.9985 43.217 832412.7890298931 42957.32044205438 +26984 4326 832412.7890298931 42957.32044205438 -70.9985 43.217000000000006 +4269 26984 -69.3515 46.403 962683.305256669 396986.5141183069 +26984 4326 962683.305256669 396986.5141183069 -69.3515 46.403000000000006 +4269 26985 -77.22999999999999 38.849999999999994 380034.9481462034 131380.0755704414 +26985 4326 380034.9481462034 131380.0755704414 -77.22999999999999 38.84999999999999 +4269 26985 -79.264 38.058 201314.2794625202 45904.12107919067 +26985 4326 201314.2794625202 45904.12107919067 -79.264 38.05799999999997 +4269 26985 -75.196 39.641999999999996 554847.9829545154 220811.09831744133 +26985 4326 554847.9829545154 220811.09831744133 -75.196 39.641999999999996 +4269 26986 -71.68 42.175 185128.3305030929 880522.4527347037 +26986 4326 185128.3305030929 880522.4527347037 -71.68 42.17500000000001 +4269 26986 -73.318 41.5315 48284.138797150605 810650.6882213238 +26986 4326 48284.138797150605 810650.6882213238 -73.318 41.531499999999994 +4269 26986 -70.042 42.8185 319232.9572599032 953006.5012715762 +26986 4326 319232.9572599032 953006.5012715762 -70.042 42.8185 +4269 26987 -70.4 41.349999999999994 508368.84153597394 38875.137285071796 +26987 4326 508368.84153597394 38875.137285071796 -70.4 41.34999999999999 +4269 26987 -70.859 41.205999999999996 469889.6934071739 22940.12737198121 +26987 4326 469889.6934071739 22940.12737198121 -70.859 41.205999999999996 +4269 26987 -69.941 41.494 546678.3583144168 55013.767025989066 +26987 4326 546678.3583144168 55013.767025989066 -69.941 41.49399999999999 +4269 26988 -86.93 46.7 8005353.265366577 213031.3436327338 +26988 4326 8005353.265366577 213031.3436327338 -86.93 46.7 +4269 26988 -90.071 45.242 7758926.973017988 55650.446219124075 +26988 4326 7758926.973017988 55650.446219124075 -90.07100000000001 45.242 +4269 26988 -83.789 48.158 8238929.602731237 379989.2076339546 +26988 4326 8238929.602731237 379989.2076339546 -83.789 48.157999999999994 +4269 26989 -84.66499999999999 44.86 5976422.234671376 171538.31270695946 +26989 4326 5976422.234671376 171538.31270695946 -84.66499999999999 44.86000000000001 +4269 26989 -86.8205 43.906 5802892.177720298 68471.58702111564 +26989 4326 5802892.177720298 68471.58702111564 -86.8205 43.906 +4269 26989 -82.5095 45.814 6144335.022055124 279167.9333284333 +26989 4326 6144335.022055124 279167.9333284333 -82.5095 45.814 +4269 26990 -84.66499999999999 42.955 3975658.1660904195 161660.9908124227 +26990 4326 3975658.1660904195 161660.9908124227 -84.66499999999999 42.95499999999999 +4269 26990 -86.9465 41.8165 3785662.443796816 38441.45475135792 +26990 4326 3785662.443796816 38441.45475135792 -86.9465 41.816500000000005 +4269 26990 -82.3835 44.0935 4158818.2040767726 289977.3408485526 +26990 4326 4158818.2040767726 289977.3408485526 -82.3835 44.0935 +4269 26991 -93.35499999999999 48.010000000000005 780976.0080242507 267903.8812841848 +26991 4326 780976.0080242507 267903.8812841848 -93.35499999999999 48.01000000000001 +4269 26991 -96.8335 46.777 514956.0367357529 137681.0978387199 +26991 4326 514956.0367357529 137681.0978387199 -96.8335 46.77700000000001 +4269 26991 -89.8765 49.243 1034700.1274336235 409882.0127727458 +26991 4326 1034700.1274336235 409882.0127727458 -89.8765 49.243 +4269 26992 -94.575 46.379999999999995 774999.3390451781 253433.09136305802 +26992 4326 774999.3390451781 253433.09136305802 -94.575 46.37999999999999 +4269 26992 -96.6315 45.39 613521.9818043811 146151.76135058573 +26992 4326 613521.9818043811 146151.76135058573 -96.6315 45.39 +4269 26992 -92.5185 47.37 930779.8796238166 364865.75352756435 +26992 4326 930779.8796238166 364865.75352756435 -92.5185 47.37 +4269 26993 -94.03 44.540000000000006 797615.9303731995 271111.9496617395 +26993 4326 797615.9303731995 271111.9496617395 -94.03 44.54000000000001 +4269 26993 -96.568 43.595 592659.1578750131 169370.35030780142 +26993 4326 592659.1578750131 169370.35030780142 -96.568 43.59499999999999 +4269 26993 -91.49199999999999 45.48500000000001 996055.3728436409 379136.3411765582 +26993 4326 996055.3728436409 379136.3411765582 -91.49199999999999 45.48500000000001 +4269 26994 -89.03 32.51 281520.8578694894 333718.072514426 +26994 4326 281520.8578694894 333718.072514426 -89.03000000000002 32.51 +4269 26994 -89.876 30.26 199662.52524006984 84702.21482670316 +26994 4326 199662.52524006984 84702.21482670316 -89.876 30.26000000000001 +4269 26994 -88.184 34.76 359446.4830207231 583441.3092225477 +26994 4326 359446.4830207231 583441.3092225477 -88.184 34.76 +4269 26995 -90.51 33.004999999999995 683491.678094493 388607.7223874255 +26995 4326 683491.678094493 388607.7223874255 -90.51 33.004999999999995 +4269 26995 -91.536 31.200499999999998 585383.0308493291 189128.51300998154 +26995 4326 585383.0308493291 189128.51300998154 -91.536 31.20049999999999 +4269 26995 -89.48400000000001 34.8095 777710.4632391258 589069.1501028265 +26995 4326 777710.4632391258 589069.1501028265 -89.48400000000001 34.8095 +4269 26996 -90.535 38.295 246938.423324011 273175.40948622045 +26996 4326 246938.423324011 273175.40948622045 -90.53500000000001 38.295 +4269 26996 -91.8265 36.211499999999994 130722.74568677916 42774.13741293872 +26996 4326 130722.74568677916 42774.13741293872 -91.82650000000001 36.211499999999994 +4269 26996 -89.2435 40.3785 356696.8903372759 505231.6112297185 +26996 4326 356696.8903372759 505231.6112297185 -89.2435 40.3785 +4269 26997 -92.6 38.545 491282.7335977044 300928.83009349275 +26997 4326 491282.7335977044 300928.83009349275 -92.6 38.545 +4269 26997 -93.671 36.686499999999995 395345.8443743689 95303.31149016054 +26997 4326 395345.8443743689 95303.31149016054 -93.671 36.68649999999999 +4269 26997 -91.529 40.4035 582422.4658619608 507702.20165428886 +26997 4326 582422.4658619608 507702.20165428886 -91.529 40.4035 +4269 26998 -94.625 38.535 839101.8219779618 262839.6964433915 +26998 4326 839101.8219779618 262839.6964433915 -94.625 38.535 +4269 26998 -95.65549999999999 36.6855 746729.0116193696 58192.123500483096 +26998 4326 746729.0116193696 58192.123500483096 -95.6555 36.6855 +4269 26998 -93.59450000000001 40.3845 926884.6909838021 468553.285440557 +26998 4326 926884.6909838021 468553.285440557 -93.59450000000001 40.384499999999996 +4270 27039 52.28 24.905 629274.0819020389 2754809.9385117656 +27039 4326 629274.0819020389 2754809.9385117656 52.28100489659866 24.906762332598092 +4270 27039 50.723 22.974500000000003 471606.4745351919 2540510.849588067 +27039 4326 471606.4745351919 2540510.849588067 50.72391662708801 22.976385221922893 +4270 27039 53.836999999999996 26.8355 781958.7391737422 2971128.6310606827 +27039 4326 781958.7391737422 2971128.6310606827 53.83809635978351 26.837143301679347 +4270 27040 55.565 24.45 354542.6332282866 2704579.6911111167 +27040 4326 354542.6332282866 2704579.6911111167 55.5661554445207 24.451765123801092 +4270 27040 54.1565 22.811999999999998 208099.86375790992 2525306.669404488 +27040 4326 208099.86375790992 2525306.669404488 54.157576298576664 22.813873516661857 +4270 27040 56.9735 26.088 497349.84618417977 2885194.6790856835 +27040 4326 497349.84618417977 2885194.6790856835 56.97473691110829 26.089659119430554 +4271 27120 -60.67 11.245000000000001 754401.0143616905 1244092.7285673586 +27120 4326 754401.0143616905 1244092.7285673586 -60.668338236867484 11.246786625829841 +4271 27120 -60.876999999999995 11.0965 731907.6294945672 1227490.6884840273 +27120 4326 731907.6294945672 1227490.6884840273 -60.875349907246694 11.098284994107184 +4271 27120 -60.463 11.3935 776872.5607320939 1260715.3818980525 +27120 4326 776872.5607320939 1260715.3818980525 -60.46132656513109 11.39528821390485 +4272 27200 172.5 -40.715 2467748.741420972 6054681.65755736 +27200 4326 2467748.741420972 6054681.65755736 172.50013797403668 -40.71326903766174 +4272 27200 166.983 -46.6685 2049620.6806236678 5376548.561251005 +27200 4326 2049620.6806236678 5376548.561251005 166.98303306233518 -46.66690736028765 +4272 27200 178.017 -34.7615 2970398.6988494033 6702880.952817619 +27200 4326 2970398.6988494033 6702880.952817619 178.0172263781476 -34.75967176619517 +4272 27205 174.055 -36.555 236497.53668241712 735814.2708217665 +27205 4326 236497.53668241712 735814.2708217665 174.05517388830413 -36.553191346567935 +4272 27205 172.19650000000001 -38.7645 76817.81128261014 487701.2415978549 +27205 4326 76817.81128261014 487701.2415978549 172.1966428197376 -38.76272725419437 +4272 27205 175.9135 -34.3455 405729.8708140519 980573.382543104 +27205 4326 405729.8708140519 980573.382543104 175.91370329448696 -34.343661261454685 +4272 27206 176.49 -38.175 302085.7695869534 654073.9286760644 +27206 4326 302085.7695869534 654073.9286760644 176.4901972136688 -38.17322612429084 +4272 27206 175.824 -39.0345 244393.04314100515 558463.9729856254 +27206 4326 244393.04314100515 558463.9729856254 175.8241859943738 -39.032741483177496 +4272 27206 177.15599999999998 -37.3155 361148.9957925866 749250.9445979365 +27206 4326 361148.9957925866 749250.9445979365 177.15620818056632 -37.313711659871025 +4272 27207 177.68 -38.265 282002.83219821117 739909.8649616673 +27207 4326 282002.83219821117 739909.8649616673 177.68021139840565 -38.263230351332915 +4272 27207 176.825 -38.9625 208067.17008877365 661964.4768388512 +27207 4326 208067.17008877365 661964.4768388512 176.82519855859508 -38.960742227523056 +4272 27207 178.535 -37.5675 357371.4261300855 817152.6680330194 +27207 4326 357371.4261300855 817152.6680330194 178.53522400725234 -37.565719021163304 +4272 27208 176.935 -39.72 322406.9806665547 692298.3079505608 +27208 4326 322406.9806665547 692298.3079505608 176.9351972487794 -39.718257999325026 +4272 27208 175.9135 -40.485 235543.62368186144 607107.9117805812 +27208 4326 235543.62368186144 607107.9117805812 175.9136816608945 -40.483272210551846 +4272 27208 177.9565 -38.955 411203.3609817411 776482.3683863506 +27208 4326 411203.3609817411 776482.3683863506 177.95671251720412 -38.95324442128453 +4272 27209 174.56 -39.09 328723.80498963327 705027.6481545779 +27209 4326 328723.80498963327 705027.6481545779 174.56017027373957 -39.08823968140395 +4272 27209 173.768 -39.711 260550.8616728186 636031.638714006 +27209 4326 260550.8616728186 636031.638714006 173.76815803637342 -39.709250549855604 +4272 27209 175.352 -38.469 398096.4499933949 773421.4097582875 +27209 4326 398096.4499933949 773421.4097582875 175.35218231443451 -38.46722923389214 +4272 27210 175.60500000000002 -39.209999999999994 296973.7434341605 733581.3385471909 +27210 4326 296973.7434341605 733581.3385471909 175.60518264925602 -39.20824454129744 +4272 27210 174.9525 -39.516 240873.23025923016 699382.3790166468 +27210 4326 240873.23025923016 699382.3790166468 174.95267343713456 -39.51424932620858 +4272 27210 176.25750000000002 -38.903999999999996 353563.4490801534 767372.6744254205 +27210 4326 353563.4490801534 767372.6744254205 176.2576917928904 -38.90223982943549 +4272 27211 175.335 -40.215 286966.73652324383 702981.0268291539 +27211 4326 286966.73652324383 702981.0268291539 175.33517546033772 -40.2132650175384 +4272 27211 174.4935 -40.8945 216181.4714964857 627058.5012677395 +27211 4326 216181.4714964857 627058.5012677395 174.49366217629887 -40.8927780335231 +4272 27211 176.1765 -39.5355 359184.4805753179 778214.4843974613 +27211 4326 359184.4805753179 778214.4843974613 176.17668849913474 -39.533752508912606 +4272 27212 175.78 -40.980000000000004 311164.44212479924 693942.5846431496 +27212 4326 311164.44212479924 693942.5846431496 175.78017806548604 -40.97828293975711 +4272 27212 175.087 -41.601 253283.49576967792 624829.3903242106 +27212 4326 253283.49576967792 624829.3903242106 175.0871667740285 -41.59929563143242 +4272 27212 176.473 -40.359 370138.4256382496 762586.3006319645 +27212 4326 370138.4256382496 762586.3006319645 176.47318915855723 -40.35727068299646 +4272 27213 174.94 -41.205 313703.69347294763 710684.5875414759 +27213 4326 313703.69347294763 710684.5875414759 174.9401665334531 -41.20328613084296 +4272 27213 174.562 -41.4705 282070.8402985907 681187.8046852337 +27213 4326 282070.8402985907 681187.8046852337 174.56216063633698 -41.4687913572887 +4272 27213 175.318 -40.939499999999995 345592.63784007874 740042.359498657 +27213 4326 345592.63784007874 740042.359498657 175.31817238713035 -40.93778097735068 +4272 27214 172.64499999999998 -40.83 297718.4974162415 687201.8260188064 +27214 4326 297718.4974162415 687201.8260188064 172.64513926693994 -40.828271982044775 +4272 27214 172.2085 -41.181 261104.48821951848 648117.4805857865 +27214 4326 261104.48821951848 648117.4805857865 172.20863219935563 -41.17927877820804 +4272 27214 173.0815 -40.479 334720.89895355835 726100.3470720855 +27214 4326 334720.89895355835 726100.3470720855 173.08164626653732 -40.477265319223214 +4272 27215 173.24 -41.42 295040.95769608306 683843.4718101643 +27215 4326 295040.95769608306 683843.4718101643 173.24014410291397 -41.41828694109583 +4272 27215 172.484 -42.104 232557.1662406258 607550.1986704642 +27215 4326 232557.1662406258 607550.1986704642 172.48413136832758 -42.10230112117821 +4272 27215 173.996 -40.736 358851.7133448363 759575.7992545734 +27215 4326 358851.7133448363 759575.7992545734 173.9961565898109 -40.73427328400673 +4272 27216 172.32999999999998 -41.120000000000005 318558.59448908985 718847.0311340436 +27216 4326 318558.59448908985 718847.0311340436 172.3301340048972 -41.11827770767304 +4272 27216 171.997 -41.453 290638.88783087925 681880.623586577 +27216 4326 290638.88783087925 681880.623586577 171.99712827036421 -41.451284475213406 +4272 27216 172.66299999999998 -40.787 346760.4729222915 755704.6241046841 +27216 4326 346760.4729222915 755704.6241046841 172.6631396848971 -40.78527106678872 +4272 27217 171.84 -41.805 321503.114479274 700612.1752588645 +27217 4326 321503.114479274 700612.1752588645 171.84012461434034 -41.80329229244232 +4272 27217 171.327 -42.1515 278983.4340388095 662125.3478358033 +27217 4326 278983.4340388095 662125.3478358033 171.32711642114583 -42.149799159054346 +4272 27217 172.353 -41.4585 364481.6315380564 738842.5727259523 +27217 4326 364481.6315380564 738842.5727259523 172.35313272992437 -41.45678554552638 +4272 27218 171.95 -42.120000000000005 333098.4204418356 723659.8484519 +27218 4326 333098.4204418356 723659.8484519 171.95012449293267 -42.11830009170848 +4272 27218 171.23000000000002 -42.678000000000004 273788.5819294128 661702.7057732836 +27218 4326 273788.5819294128 661702.7057732836 171.23011254293857 -42.67631173200014 +4272 27218 172.67 -41.562 393450.52532397496 785110.2767712271 +27218 4326 393450.52532397496 785110.2767712271 172.670136253903 -41.56028878024056 +4272 27219 172.715 -42.52 275746.9388837776 718744.6758828256 +27219 4326 275746.9388837776 718744.6758828256 172.71513235588284 -42.518311817932776 +4272 27219 171.9635 -42.907000000000004 214523.61848949624 675263.3090431091 +27219 4326 214523.61848949624 675263.3090431091 171.96362081344896 -42.905319422382526 +4272 27219 173.4665 -42.133 337733.251225535 761675.1920760573 +27219 4326 337733.251225535 761675.1920760573 173.46664377932174 -42.13130433478527 +4272 27220 173.70499999999998 -41.75 291925.5635705656 677169.1340493204 +27220 4326 291925.5635705656 677169.1340493204 173.70514853460486 -41.74829581219813 +4272 27220 173.0255 -42.56 236224.3469774468 586906.8523613336 +27220 4326 236224.3469774468 586906.8523613336 173.02563615769844 -42.558313599789 +4272 27220 174.3845 -40.94 349049.2701852752 766972.0504574886 +27220 4326 349049.2701852752 766972.0504574886 174.3846606147554 -40.938278796903155 +4272 27221 171.14 -42.81999999999999 313085.7073090994 707355.5487014729 +27221 4326 313085.7073090994 707355.5487014729 171.14011066628416 -42.81831500549949 +4272 27221 170.46499999999997 -43.189 258133.90803244227 666244.6833004169 +27221 4326 258133.90803244227 666244.6833004169 170.4651000690075 -43.18732237744391 +4272 27221 171.815 -42.45099999999999 368693.6432815811 748021.9463937931 +27221 4326 368693.6432815811 748021.9463937931 171.81512115896058 -42.44930774902971 +4272 27222 170.05 -43.425 282919.12940088357 664995.68917341 +27222 4326 282919.12940088357 664995.68917341 170.05009345114988 -43.42332720086046 +4272 27222 169.294 -43.807500000000005 222192.50330136527 622063.9061406963 +27222 4326 222192.50330136527 622063.9061406963 169.29408155544905 -43.80583487162326 +4272 27222 170.80599999999998 -43.0425 344417.31817834784 707369.1179850551 +27222 4326 344417.31817834784 707369.1179850551 170.80610522720062 -43.040819642598166 +4272 27223 169.015 -44.035 332765.1640169419 693563.257821238 +27223 4326 332765.1640169419 693563.257821238 169.01507663350802 -44.03334000499239 +4272 27223 168.11950000000002 -44.3635 261195.61509432373 657026.4367600286 +27223 4326 261195.61509432373 657026.4367600286 168.1195630664122 -44.36184598331581 +4272 27223 169.91049999999998 -43.7065 405127.1279704761 729318.3584313095 +27223 4326 405127.1279704761 729318.3584313095 169.91059009891595 -43.70483406300044 +4272 27224 172.245 -43.325 260887.50468877738 729400.7645415534 +27224 4326 260887.50468877738 729400.7645415534 172.24512237871423 -43.323330737260555 +4272 27224 171.2235 -43.8965 179178.86604893784 664916.0048332887 +27224 4326 179178.86604893784 664916.0048332887 171.22360613947112 -43.89484276399781 +4272 27224 173.2665 -42.753499999999995 344153.0277241325 792864.5117905498 +27224 4326 344153.0277241325 792864.5117905498 173.26663835999912 -42.7518189929108 +4272 27225 171.47 -43.69 308808.55105851917 706517.6534837566 +27225 4326 308808.55105851917 706517.6534837566 171.47011043981698 -43.688338050694995 +4272 27225 170.75900000000001 -44.194 251891.3358418558 650345.5887652037 +27225 4326 251891.3358418558 650345.5887652037 170.7590984626115 -44.192349325549 +4272 27225 172.18099999999998 -43.186 366685.218366089 762193.5474188926 +27225 4326 366685.218366089 762193.5474188926 172.18112223911803 -43.18432702995111 +4272 27226 170.685 -44.165 270224.6617381632 726292.8421279577 +27226 4326 270224.6617381632 726292.8421279577 170.6850976522182 -44.16334834473881 +4272 27226 169.9065 -44.8985 209102.63621307554 644204.8402753405 +27226 4326 209102.63621307554 644204.8402753405 169.90658327918024 -44.896865923370456 +4272 27226 171.4635 -43.4315 332894.7348917624 807780.54883027 +27226 4326 332894.7348917624 807780.54883027 171.4636116955559 -43.42983135671333 +4272 27227 169.43 -44.555 297000.03992534993 720032.1956191305 +27227 4326 297000.03992534993 720032.1956191305 169.43007901090527 -44.553355148003 +4272 27227 168.701 -45.3155 239873.71376946586 635229.3687227062 +27227 4326 239873.71376946586 635229.3687227062 168.7010648431078 -45.31387386346283 +4272 27227 170.15900000000002 -43.7945 355635.7521530405 804306.7385810004 +27227 4326 355635.7521530405 804306.7385810004 170.15909283267277 -43.79283707807379 +4272 27228 168.41500000000002 -44.935 301291.35753065854 721993.9748211824 +27228 4326 301291.35753065854 721993.9748211824 168.415063422983 -44.93336242194463 +4272 27228 167.7895 -45.515499999999996 252401.56630492472 657296.9479166273 +27228 4326 252401.56630492472 657296.9479166273 167.78955152897413 -45.5138766281117 +4272 27228 169.0405 -44.3545 351175.9312188394 786303.729369452 +27228 4326 351175.9312188394 786303.729369452 169.04057509632108 -44.3528485792605 +4272 27229 167.29000000000002 -45.43 264872.7567360466 714764.9854510392 +27229 4326 264872.7567360466 714764.9854510392 167.2900454983637 -45.428372614481795 +4272 27229 166.46200000000002 -46.239999999999995 201514.32570803378 624037.4474410438 +27229 4326 201514.32570803378 624037.4474410438 166.46202908494925 -46.23839304876479 +4272 27229 168.118 -44.620000000000005 330092.18142964214 804813.1069905382 +27229 4326 330092.18142964214 804813.1069905382 168.11806147761845 -44.61835289208517 +4272 27230 170.505 -45.215 290290.9750440012 766812.8477862038 +27230 4326 290290.9750440012 766812.8477862038 170.50508939074624 -45.21337641941294 +4272 27230 169.8435 -45.7595 238918.18806736142 706002.0539315238 +27230 4326 238918.18806736142 706002.0539315238 169.84357734995461 -45.75788985620478 +4272 27230 171.1665 -44.6705 342656.4593064864 827192.095155584 +27230 4326 342656.4593064864 827192.095155584 171.16660122266887 -44.66886329171155 +4272 27231 169.755 -45.98 259116.2297908441 686694.9722958091 +27231 4326 259116.2297908441 686694.9722958091 169.75507482556657 -45.97839591818402 +4272 27231 168.7515 -46.654999999999994 182804.02749962924 610660.6216067368 +27231 4326 182804.02749962924 610660.6216067368 168.75155701798414 -46.65341260746295 +4272 27231 170.7585 -45.305 337324.69341414777 761742.0099348222 +27231 4326 337324.69341414777 761742.0099348222 170.75859225714208 -45.303379656482754 +4272 27232 168.13 -46.33 283610.43231743824 729992.928293829 +27232 4326 283610.43231743824 729992.928293829 168.13005075958668 -46.3284011021861 +4272 27232 167.374 -47.23 226627.76899991484 629506.2743916593 +27232 4326 226627.76899991484 629506.2743916593 167.37403442119884 -47.22842546953298 +4272 27232 168.886 -45.43 342507.03241732426 829908.7944541952 +27232 4326 342507.03241732426 829908.7944541952 168.8860665868734 -45.428377643338635 +4272 27258 166.935 -45.120000000000005 652195.9195307008 5001804.422218787 +27258 4326 652195.9195307008 5001804.422218787 166.93504287272089 -45.11836280463634 +4272 27258 165.97650000000002 -47.397 573690.4555987978 4750153.420594992 +27258 4326 573690.4555987978 4750153.420594992 165.97651425275683 -47.39542587465784 +4272 27258 167.8935 -42.843 736462.5761708107 5252472.431865968 +27258 4326 736462.5761708107 5252472.431865968 167.89356920144195 -42.841305981219534 +4272 27259 171.0 -40.765 500000.0 5487251.295790812 +27259 4326 500000.0 5487251.295790812 171.0001190802741 -40.763266115670596 +4272 27259 168.3 -46.9525 294544.5473529464 4796475.548099761 +27259 4326 294544.5473529464 4796475.548099761 168.30004892078932 -46.95092006833279 +4272 27259 173.7 -34.5775 747682.9183984742 6170435.542529574 +27259 4326 747682.9183984742 6170435.542529574 173.70017699044237 -34.575659381605824 +4272 27260 176.635 -39.185 468475.017231868 5662556.644885596 +27260 4326 468475.017231868 5662556.644885596 176.63519543332123 -39.183246308864355 +4272 27260 174.2635 -43.6355 279250.5542067233 5164882.961772807 +27260 4326 279250.5542067233 5164882.961772807 174.26364727431027 -43.63384384386324 +4272 27260 179.00650000000002 -34.734500000000004 683707.2376197517 6154505.595190001 +27260 4326 683707.2376197517 6154505.595190001 179.00673798359097 -34.73267333206704 +4272 27291 175.31 -37.885000000000005 281720.27787091624 535342.3143090325 +27291 4326 281720.27787091624 535342.3143090325 175.3101839576691 -37.88321801902808 +4272 27291 172.322 -41.2915 8846.693185108816 116398.73627881994 +27291 4326 8846.693185108816 116398.73627881994 172.32213311361335 -41.28978160892054 +4272 27291 178.298 -34.478500000000004 581149.3150691913 944863.1543126446 +27291 4326 581149.3150691913 944863.1543126446 178.2982304807949 -34.47666822542697 +4272 27292 170.41500000000002 -43.885 404641.27224388265 513348.4738512847 +27292 4326 404641.27224388265 513348.4738512847 170.4150956618745 -43.88334018322642 +4272 27292 166.77450000000002 -46.9855 106857.74818348326 125245.63520886096 +27292 4326 106857.74818348326 125245.63520886096 166.77452802691434 -46.9839161209409 +4272 27292 174.0555 -40.784499999999994 735921.9380268618 887192.1119490212 +27292 4326 735921.9380268618 887192.1119490212 174.05565713148044 -40.782774511385455 +4817 27391 -4.9129166666667 60.545 -13512.629940406594 283508.3664410137 +4817 27391 -6.1909166666667 58.182500000000005 -89659.8114978418 21338.12498566025 +4817 27391 -3.6349166666667 62.9075 52439.66210829964 547155.3638703818 +4817 27392 -2.3329166666666996 61.065 22.496251673254957 341418.5016158417 +4817 27392 -3.3859166666667004 58.216499999999996 -61857.889952204074 24594.210508638756 +4817 27392 -1.2799166666666988 63.913500000000006 51697.36475104662 659287.0033154404 +4817 27393 0.04208333333330039 63.18 2119.1092350951694 577106.0016255032 +4817 27393 -1.0514166666667002 59.22 -60036.432388415764 136353.04924571596 +4817 27393 1.1355833333332992 67.14 49241.360873566504 1019027.3629020797 +4817 27394 2.7920833333333004 64.49 14039.76514523689 723154.1575667291 +4817 27394 1.401583333333301 60.341 -60651.30847894034 261261.03359881567 +4817 27394 4.1825833333333 68.639 68408.4672876955 1186685.0717255624 +4817 27395 6.247083333333299 68.21000000000001 3332.2778559534145 1137905.534248562 +4817 27395 4.519083333333301 66.35600000000001 -73748.82120675115 932128.1461290229 +4817 27395 7.9750833333333 70.064 68829.02148187376 1345717.6673925433 +4817 27396 10.162083333333298 69.63 -178.1011906694734 1296284.577097221 +4817 27396 8.3575833333333 68.46 -74137.01512583594 1166874.2355524315 +4817 27396 11.966583333333299 70.80000000000001 66075.42617762534 1427781.4418161872 +4817 27397 14.2070833333333 69.91 1549.8557003000285 1327518.2724811567 +4817 27397 12.3620833333333 68.713 -73125.28871164533 1195076.0780929362 +4817 27397 16.0520833333333 71.107 68148.01971877518 1462111.653174188 +4817 27398 18.422083333333298 70.10499999999999 3371.6272290235906 1349272.4860668741 +4817 27398 16.4645833333333 69.1285 -74314.46172565458 1241478.7299844262 +4817 27398 20.3795833333333 71.08149999999999 74055.5449403254 1459456.9404967339 +4274 27429 -7.875 39.555 596655.6587067954 4379047.302034068 +27429 4326 596655.6587067954 4379047.302034068 -7.8740862725659175 39.555802536869194 +4274 27429 -9.3915 37.2105 465260.51016746747 4118363.4166134237 +27429 4326 465260.51016746747 4118363.4166134237 -9.39067579175869 37.211258898478675 +4274 27429 -6.358500000000001 41.899499999999996 719125.1637616432 4642074.526612396 +27429 4326 719125.1637616432 4642074.526612396 -6.357489901534694 41.90034698256766 +4274 27493 -7.875 39.555 22261.554950378402 -12453.84735186827 +27493 4326 22261.554950378402 -12453.84735186827 -7.8740862725659175 39.55580253686918 +4274 27493 -9.3915 37.2105 -111635.55311472325 -271997.4541045907 +27493 4326 -111635.55311472325 -271997.4541045907 -9.39067579175869 37.211258898478675 +4274 27493 -6.358500000000001 41.899499999999996 147348.91544329392 249397.03976895116 +27493 4326 147348.91544329392 249397.03976895116 -6.357489901534694 41.90034698256766 +4901 27500 5.197791666666671 48.245000000000005 484990.2887248626 160543.0133286764 +4901 27500 4.57229166666667 47.502500000000005 437654.64675677114 78333.07046616016 +4901 27500 5.823291666666671 48.9875 530956.7944632018 243129.35988539128 +4807 27561 -0.6572291699999893 49.64 552532.4797472401 215776.07708032225 +4807 27561 -6.55222916999999 48.29 114325.8723605189 86575.20185526076 +4807 27561 5.23777083000001 50.99 967561.0404867153 378517.9510963777 +4807 27562 -0.9222291699999898 46.795 529598.0721708467 199857.26930406177 +4807 27562 -6.5157291699999895 45.5755 91930.97349309025 84969.3263316928 +4807 27562 4.671270830000009 48.0145 948344.998122476 345374.2658396767 +4807 27563 0.6227708300000101 43.894999999999996 650033.9917525556 177414.2724822167 +4807 27563 -3.65222916999999 42.4865 299730.7061846803 27404.893669757817 +4807 27563 4.8977708300000105 45.3035 983958.3748859647 345155.9379360663 +4807 27564 6.727770830000011 42.19 555391.6229053381 210528.5700149827 +4807 27564 6.21927083000001 41.398 519912.5714171522 119616.53184611571 +4807 27564 7.236270830000011 42.982 589805.0583297374 301620.42888655246 +4807 27571 -0.6572291699999893 49.64 552532.4797472401 1215776.0770803222 +4807 27571 -6.55222916999999 48.29 114325.8723605189 1086575.2018552609 +4807 27571 5.23777083000001 50.99 967561.0404867153 1378517.9510963778 +4807 27572 -0.6572291699999893 46.735 549771.5390971856 2192985.1139500216 +4807 27572 -6.55222916999999 42.7705 63113.13093164773 1774300.825679168 +4807 27572 5.23777083000001 50.6995 970626.1583658268 2646284.6554547492 +4807 27573 0.6227708300000101 43.894999999999996 650033.9917525556 3177414.272482217 +4807 27573 -3.65222916999999 42.4865 299730.7061846803 3027404.893669758 +4807 27573 4.8977708300000105 45.3035 983958.3748859647 3345155.937936066 +4807 27574 6.727770830000011 42.19 555391.6229053381 4210528.5700149825 +4807 27574 6.21927083000001 41.398 519912.5714171522 4119616.531846116 +4807 27574 7.236270830000011 42.982 589805.0583297374 4301620.428886552 +4277 27700 -3.5 55.379999999999995 304969.207370822 610584.9936767236 +27700 4326 304969.207370822 610584.9936767236 -3.5013607419148154 55.38000616818721 +4277 27700 -8.459 50.313 -59660.87815010123 65940.8296324799 +27700 4326 -59660.87815010123 65940.8296324799 -8.459619377033384 50.31355088033584 +4277 27700 1.4589999999999996 60.446999999999996 590248.4076028957 1178623.5356505034 +27700 4326 590248.4076028957 1178623.5356505034 1.4566784212354162 60.44647932320407 +4326 27701 23.809999999999995 -2.5450000000000017 5879935.218960623 4769713.016794273 +4326 27701 -26.131 -39.7555 1073873.5657476215 345949.8365808688 +4326 27701 73.75099999999998 34.665499999999994 10469322.113991313 9611986.753561128 +4326 27702 0.0 -62.235 3714266.977 6500861.156581102 +4326 27702 -162.0 -87.2235 3618435.899414181 3107078.776164361 +4326 27702 162.0 -37.246500000000005 5530699.214102222 -2188387.086523923 +4326 27703 111.51 36.435 5908815.685853561 3805715.833684417 +4326 27703 39.456 -6.0765 -2456297.997539295 692544.0671195723 +4326 27703 -176.436 78.9465 5697243.746810641 9533552.112515371 +4326 27704 4.604999999999997 56.455 4655986.83222598 2669688.7263691956 +4326 27704 -37.807500000000005 31.9615 445472.2435522415 2117721.1228805864 +4326 27704 47.01749999999999 80.9485 6248052.3783539 5312968.3742703665 +4326 27705 0.0 48.989999999999995 13201348.05406379 8906819.439555893 +4326 27705 -162.0 12.081 1225374.5921371486 3265885.582025503 +4326 27705 162.0 85.899 7778192.970956077 9182141.031577392 +4326 27706 155.66 -19.495000000000005 9516079.003458042 7474861.369501723 +4326 27706 80.699 -56.453500000000005 3825650.178921563 2733144.841270175 +4326 27706 -129.37900000000002 17.463499999999996 18231839.13102165 10402625.650389625 +4326 27707 -69.69999999999999 -14.780000000000001 6266943.795289688 5486097.123449902 +4326 27707 -119.30799999999999 -55.361 3475041.110694009 -40953.9144977238 +4326 27707 -20.092 25.800999999999995 11644819.265280958 9986027.358036783 +4281 28191 36.74 31.28 315747.4923633336 77527.94658978429 +28191 4326 315747.4923633336 77527.94658978429 36.74076381855878 31.280255495334725 +4281 28191 34.427 29.39 94040.69558748518 -132736.36504622165 +28191 4326 94040.69558748518 -132736.36504622165 34.427541383818124 29.39051148908693 +4281 28191 39.053000000000004 33.17 528442.5172680574 292674.34707964514 +28191 4326 528442.5172680574 292674.34707964514 39.05399047482211 33.1700024464358 +4281 28192 36.74 31.28 315760.1518952198 1077527.9472791785 +28192 4326 315760.1518952198 1077527.9472791785 36.74076381855877 31.280255495334735 +4281 28192 34.427 29.39 94038.87569729448 867263.6350022198 +28192 4326 94038.87569729448 867263.6350022198 34.427541383818124 29.39051148908693 +4281 28192 39.053000000000004 33.17 528631.4536817487 1292674.3787621665 +28192 4326 528631.4536817487 1292674.3787621665 39.05399047482211 33.1700024464358 +4281 28193 34.93 31.365000000000002 143413.75984934124 1085978.8172404617 +28193 4326 143413.75984934124 1085978.8172404617 34.930721227438546 31.36536588308817 +4281 28193 34.246 29.6415 76702.25543772477 895273.5891975701 +28193 4326 76702.25543772477 895273.5891975701 34.24655664738441 29.642007826146806 +4281 28193 35.614 33.0885 207776.1586775701 1277129.4444829368 +28193 4326 207776.1586775701 1277129.4444829368 35.61489345469584 33.08872961372743 +4282 28232 13.745 -1.5949999999999998 1028413.367273927 9823110.28176177 +28232 4326 1028413.367273927 9823110.28176177 13.745761208451011 -1.5974885166726005 +4282 28232 9.3305 -6.3785 536551.0942714714 9295007.78085108 +28232 4326 536551.0942714714 9295007.78085108 9.33117176430586 -6.380557911620465 +4282 28232 18.159499999999998 3.1884999999999994 1522039.1268149684 10356969.824604692 +28232 4326 1522039.1268149684 10356969.824604692 18.16035099059988 3.1855852765131507 +4283 28348 105.075 -21.775 507753.7174763942 7592075.165138347 +28348 4326 507753.7174763942 7592075.165138347 105.075 -21.775 +4283 28348 102.4335 -33.3895 261275.51969203193 6302588.07154193 +28348 4326 261275.51969203193 6302588.07154193 102.4335 -33.3895 +4283 28348 107.71650000000001 -10.160499999999999 797674.2425833207 8875596.44564535 +28348 4326 797674.2425833207 8875596.44564535 107.71650000000001 -10.160499999999994 +4283 28349 111.005 -24.28 500507.448992137 7314772.988566413 +28349 4326 500507.448992137 7314772.988566413 111.00500000000001 -24.28 +4283 28349 108.3005 -36.484 258169.39904554875 5958977.854632109 +28349 4326 258169.39904554875 5958977.854632109 108.3005 -36.483999999999995 +4283 28349 113.7095 -12.076 794971.826734399 8663582.464576721 +28349 4326 794971.826734399 8663582.464576721 113.7095 -12.076000000000004 +4283 28350 117.005 -25.295 500503.33718633483 7202386.6635158025 +28350 4326 500503.33718633483 7202386.6635158025 117.005 -25.294999999999995 +4283 28350 114.3005 -37.2065 260436.1764596865 5878805.476842016 +28350 4326 260436.1764596865 5878805.476842016 114.30050000000001 -37.20649999999999 +4283 28350 119.7095 -13.383500000000005 793462.5074147208 8518846.821110332 +28350 4326 793462.5074147208 8518846.821110332 119.70950000000002 -13.383500000000003 +4283 28351 123.005 -24.265 500507.50856923853 7316433.756566911 +28351 4326 500507.50856923853 7316433.756566911 123.00500000000001 -24.265 +4283 28351 120.3005 -36.6895 258810.2300880482 5936175.273307532 +28351 4326 258810.2300880482 5936175.273307532 120.30049999999999 -36.68950000000001 +4283 28351 125.7095 -11.840500000000002 795227.4433792764 8689650.334980171 +28351 4326 795227.4433792764 8689650.334980171 125.7095 -11.840500000000004 +4283 28352 128.995 -23.240000000000002 499488.5027524061 7429911.893493047 +28352 4326 499488.5027524061 7429911.893493047 128.995 -23.24 +4283 28352 126.2905 -35.966 255665.94256330913 6016428.15543524 +28352 4326 255665.94256330913 6016428.15543524 126.2905 -35.966 +4283 28352 131.6995 -10.514000000000003 795479.038805508 8836486.096057687 +28352 4326 795479.038805508 8836486.096057687 131.6995 -10.514000000000003 +4283 28353 135.005 -24.795 500505.3824881319 7257751.289519885 +28353 4326 500505.3824881319 7257751.289519885 135.005 -24.795 +4283 28353 132.3005 -39.1185 266618.18080348126 5666603.0077016875 +28353 4326 266618.18080348126 5666603.0077016875 132.3005 -39.1185 +4283 28353 137.7095 -10.471500000000002 796614.9159729548 8841180.689241666 +28353 4326 796614.9159729548 8841180.689241666 137.7095 -10.471500000000011 +4283 28354 141.005 -28.634999999999998 500488.70064734353 6832452.150132753 +28354 4326 500488.70064734353 6832452.150132753 141.005 -28.634999999999994 +4283 28354 138.3005 -46.2345 291859.4911454697 4876355.083101071 +28354 4326 291859.4911454697 4876355.083101071 138.3005 -46.2345 +4283 28354 143.7095 -11.035499999999999 796063.682372214 8778754.575019982 +28354 4326 796063.682372214 8778754.575019982 143.7095 -11.035499999999999 +4283 28355 147.005 -30.060000000000002 500481.9480697148 6674566.120553608 +28355 4326 500481.9480697148 6674566.120553608 147.005 -30.06 +4283 28355 144.3005 -48.807 301799.636594196 4590484.040062539 +28355 4326 301799.636594196 4590484.040062539 144.3005 -48.80700000000001 +4283 28355 149.7095 -11.313000000000002 795781.9772796432 8748038.943825705 +28355 4326 795781.9772796432 8748038.943825705 149.7095 -11.313 +4283 28356 153.0 -36.415 500000.0 5970020.382468974 +28356 4326 500000.0 5970020.382468974 153.0 -36.415 +4283 28356 150.3 -56.7055 334711.61871741887 3712139.428408703 +28356 4326 334711.61871741887 3712139.428408703 150.3 -56.70549999999999 +4283 28356 155.7 -16.124499999999998 788788.7017833702 8215402.2766512865 +28356 4326 788788.7017833702 8215402.2766512865 155.7 -16.124499999999998 +4283 28357 159.005 -37.32 500443.0090585269 5869628.0173232835 +28357 4326 500443.0090585269 5869628.0173232835 159.005 -37.32 +4283 28357 156.3005 -58.236000000000004 341512.01405727677 3541839.4355010362 +28357 4326 341512.01405727677 3541839.4355010362 156.3005 -58.23599999999999 +4283 28357 161.7095 -16.404000000000003 789395.273472464 8184442.074905855 +28357 4326 789395.273472464 8184442.074905855 161.7095 -16.403999999999996 +4283 28358 165.0 -42.665 499999.9999999979 5276385.291762875 +28358 4326 499999.9999999979 5276385.291762875 165.0 -42.665 +4283 28358 162.3 -57.7175 339175.74886605825 3599536.2072792435 +28358 4326 339175.74886605825 3599536.2072792435 162.3 -57.717499999999994 +4283 28358 167.7 -27.6125 766471.0403257085 6942810.329173721 +28358 4326 766471.0403257085 6942810.329173721 167.7 -27.612499999999994 +4284 28404 21.785 53.695 4551852.908601651 5952359.55362 +28404 4326 4551852.908601651 5952359.55362 21.783033061265463 53.69472630246123 +4284 28404 19.7915 48.5245 4410732.507893915 5377549.622905095 +28404 4326 4410732.507893915 5377549.622905095 19.78975676645579 48.524128373175294 +4284 28404 23.7785 58.8655 4660290.893224033 6531128.48004474 +28404 4326 4660290.893224033 6531128.48004474 23.77622904611787 58.86532950007166 +4284 28405 27.005000000000003 57.325 5500301.212179624 6356218.055120438 +28405 4326 5500301.212179624 6356218.055120438 27.00288879153165 57.324873911333256 +4284 28405 24.3005 46.3945 5292379.117343356 5143570.13347466 +28405 4326 5292379.117343356 5143570.13347466 24.298881357401225 46.39417854657764 +4284 28405 29.709500000000002 68.2555 5612036.33641593 7576973.310199547 +28405 4326 5612036.33641593 7576973.310199547 29.706357096422167 68.2555788892998 +4284 28406 33.0 57.17 6500000.0 6338956.286202614 +28406 4326 6500000.0 6338956.286202614 32.99797166629755 57.169982463225324 +4284 28406 30.3 45.605 6289363.854055499 5055819.28226941 +28406 4326 6289363.854055499 5055819.28226941 30.298457314429925 45.604764475195296 +4284 28406 35.7 68.735 6609298.913977927 7630398.127579256 +28406 4326 6609298.913977927 7630398.127579256 35.696908096551454 68.73520565464733 +4284 28407 39.0 55.33 7500000.0 6134075.5797153525 +28407 4326 7500000.0 6134075.5797153525 38.99817197334615 55.33005962925359 +4284 28407 36.3 42.82 7279187.782690121 4746344.926457373 +28407 4326 7279187.782690121 4746344.926457373 36.29860678298642 42.819813334853336 +4284 28407 41.7 67.84 7613669.175533571 7530649.032519 +28407 4326 7613669.175533571 7530649.032519 41.69719020330313 67.84030732643807 +4284 28408 45.004999999999995 59.875 8500280.05647272 6640262.465565657 +28408 4326 8500280.05647272 6640262.465565657 45.00299118914558 59.87523582791658 +4284 28408 42.3005 40.9435 8272667.399389065 4537889.80114546 +28408 4326 8272667.399389065 4537889.80114546 42.29920979020801 40.94336903330199 +4284 28408 47.7095 78.8065 8558721.66741801 8753392.219536956 +28408 4326 8558721.66741801 8753392.219536956 47.7041137967652 78.80707632438926 +4284 28409 51.0 59.370000000000005 9500000.0 6584001.55719021 +28409 4326 9500000.0 6584001.55719021 50.99817532538931 59.37032616873129 +4284 28409 48.3 39.543000000000006 9267886.604332898 4382348.994620828 +28409 4326 9267886.604332898 4382348.994620828 48.29883660075836 39.54292222313768 +4284 28409 53.7 79.197 9556499.588105647 8796938.973915378 +28409 4326 9556499.588105647 8796938.973915378 53.69484598212384 79.19768943261295 +4284 28410 57.0 59.48 10500000.0 6596256.033963211 +28410 4326 10500000.0 6596256.033963211 56.998342046649 59.48041976678641 +4284 28410 54.3 39.293 10267055.267920056 4354586.619266492 +28410 4326 10267055.267920056 4354586.619266492 54.29895116914864 39.29298976696036 +4284 28410 59.7 79.667 10554069.237212261 8849363.817288546 +28410 4326 10554069.237212261 8849363.817288546 59.69511806925092 79.66779516238226 +4284 28411 63.0 58.455 11500000.0 6482074.698219859 +28411 4326 11500000.0 6482074.698219859 62.99858624107984 58.45548335902813 +4284 28411 60.3 37.4715 11261133.407569468 4152337.9224953735 +28411 4326 11261133.407569468 4152337.9224953735 60.2991049118664 37.47151735930999 +4284 28411 65.7 79.4385 11555251.272983745 8823876.26732618 +28411 4326 11555251.272983745 8823876.26732618 65.69578441053544 79.43938095641528 +4284 28412 69.0 56.87 12500000.0 6305547.652461898 +28412 4326 12500000.0 6305547.652461898 68.99885498993974 56.8705247830778 +4284 28412 66.3 38.69 12265068.420444129 4287627.595211889 +28412 4326 12265068.420444129 4287627.595211889 66.29921910245224 38.69010309902671 +4284 28412 71.7 75.04999999999998 12577757.677999895 8334433.247982746 +28412 4326 12577757.677999895 8334433.247982746 71.697500156463 75.05089111826645 +4284 28413 75.0 58.25 13500000.0 6459240.625442509 +28413 4326 13500000.0 6459240.625442509 74.99902096181162 58.250616202614715 +4284 28413 72.3 38.936 13265875.835700002 4314943.5598596735 +28413 4326 13265875.835700002 4314943.5598596735 72.29936100137701 38.936161556029795 +4284 28413 77.7 77.564 13564912.708028927 8614800.564080589 +28413 4326 13564912.708028927 8614800.564080589 77.69751173297504 77.56499664445953 +4284 28414 81.005 61.035 14500270.244147103 6769511.797167683 +28414 4326 14500270.244147103 6769511.797167683 81.0041673530589 61.03572708616913 +4284 28414 78.3005 43.0395 14280011.841126386 4770730.583375289 +28414 4326 14280011.841126386 4770730.583375289 78.29996484935782 43.03980519934033 +4284 28414 83.7095 79.03049999999999 14557561.317312352 8778376.844888298 +28414 4326 14557561.317312352 8778376.844888298 83.70731269658248 79.03157291614595 +4284 28415 87.0 64.045 15500000.0 7104994.433405277 +28415 4326 15500000.0 7104994.433405277 86.9993884949609 64.0458261475768 +4284 28415 84.3 48.5425 15300638.75412034 5382367.500632766 +28415 4326 15300638.75412034 5382367.500632766 84.29960344770687 48.54296890085974 +4284 28415 89.7 79.54749999999999 15554687.523562651 8836034.406479053 +28415 4326 15554687.523562651 8836034.406479053 89.6984961046103 79.54861881549702 +4284 28416 93.0 65.62 16500000.0 7280594.179302066 +28416 4326 16500000.0 7280594.179302066 92.99965591516786 65.62088783828742 +4284 28416 90.3 51.463 16312371.19563157 5707155.118561674 +28416 4326 16312371.19563157 5707155.118561674 90.29976966849114 51.463569218327855 +4284 28416 95.7 79.77699999999999 16553499.892234067 8861633.636235029 +28416 4326 16553499.892234067 8861633.636235029 95.69920953334623 79.77814803884354 +4284 28417 99.0 65.52499999999999 17500000.0 7270001.392661486 +28417 4326 17500000.0 7270001.392661486 98.9999832998074 65.5259034655547 +4284 28417 96.3 51.3095 17311742.04845411 5690081.267726861 +28417 4326 17311742.04845411 5690081.267726861 96.29998542819108 51.31008710735066 +4284 28417 101.7 79.7405 17553688.833544858 8857562.279994301 +28417 4326 17553688.833544858 8857562.279994301 101.69997361268366 79.74165930035332 +4284 28418 105.0 64.56 18500000.0 7162408.624891741 +28418 4326 18500000.0 7162408.624891741 105.00030935930965 64.56088868438572 +4284 28418 102.3 51.132 18311016.230525482 5670338.326859696 +28418 4326 18311016.230525482 5670338.326859696 102.30020235037838 51.13259331820935 +4284 28418 107.7 77.988 18562733.14904189 8662090.29101531 +28418 4326 18562733.14904189 8662090.29101531 107.70066481128028 77.98912969936808 +4284 28419 111.0 62.975 19500000.0 6985719.868823676 +28419 4326 19500000.0 6985719.868823676 111.00060481075904 62.97584828674728 +4284 28419 108.3 50.5235 19308541.915251147 5602659.755338392 +28419 4326 19308541.915251147 5602659.755338392 108.30041666696081 50.52407827122059 +4284 28419 113.7 75.4265 19575843.057047803 8376418.5363391265 +28419 4326 19575843.057047803 8376418.5363391265 113.70112759073656 75.42757200421018 +4284 28420 117.0 62.735 20500000.0 6958969.176764962 +28420 4326 20500000.0 6958969.176764962 117.00089326346644 62.7358227410701 +4284 28420 114.3 50.832499999999996 20309795.68677516 5637026.654582188 +28420 4326 20309795.68677516 5637026.654582188 114.30062981280757 50.83307365705784 +4284 28420 119.7 74.63749999999999 20579851.45738725 8288434.675508902 +28420 4326 20579851.45738725 8288434.675508902 119.70158276216468 74.63852954201049 +4284 28421 123.0 62.754999999999995 21500000.0 6961198.365997623 +28421 4326 21500000.0 6961198.365997623 123.00117770561728 62.75579012404397 +4284 28421 120.3 52.634499999999996 21317217.163751446 5837472.916796084 +28421 4326 21317217.163751446 5837472.916796084 120.30086006153299 52.63509136853386 +4284 28421 125.7 72.8755 21588746.46108203 8091966.483516503 +28421 4326 21588746.46108203 8091966.483516503 125.7018844797574 72.87645756539818 +4284 28422 129.0 57.93 22500000.0 6423598.670127823 +28422 4326 22500000.0 6423598.670127823 129.00128766961262 57.93064877696723 +4284 28422 126.3 43.818 22282775.33302377 4857230.979267927 +28422 4326 22282775.33302377 4857230.979267927 126.30094586619992 43.81835852779464 +4284 28422 131.7 72.042 22592925.042030107 7999037.963814366 +28422 4326 22592925.042030107 7999037.963814366 131.70220951019087 72.04288893926793 +4284 28423 135.0 59.39 23500000.0 6586229.628407109 +28423 4326 23500000.0 6586229.628407109 135.00156471315282 59.390623875020964 +4284 28423 132.3 44.306000000000004 23284553.73911299 4911457.322981693 +28423 4326 23284553.73911299 4911457.322981693 132.30112010466385 44.30633092477097 +4284 28423 137.7 74.474 23580680.199776404 8270202.881338667 +28423 4326 23580680.199776404 8270202.881338667 137.7029427139113 74.47486204542867 +4284 28424 141.0 61.055 24500000.0 6771740.421450425 +28424 4326 24500000.0 6771740.421450425 141.0018629529222 61.055589769842825 +4284 28424 138.3 47.36150000000001 24296040.706018656 5251065.475993207 +28424 4326 24296040.706018656 5251065.475993207 138.30133525498115 47.36184750636767 +4284 28424 143.7 74.74849999999999 24579288.448659074 8300812.343378751 +28424 4326 24579288.448659074 8300812.343378751 143.70340199748142 74.74928762926335 +4284 28425 147.0 60.209999999999994 25500000.0 6677586.4401542945 +28425 4326 25500000.0 6677586.4401542945 147.00202807483538 60.210498348293974 +4284 28425 144.3 45.261 25288079.346784852 5017586.979982033 +28425 4326 25288079.346784852 5017586.979982033 144.30144570262993 45.26124594077783 +4284 28425 149.7 75.15899999999999 25577203.725286763 8346588.240981594 +28425 4326 25577203.725286763 8346588.240981594 149.70387467792418 75.15970444088252 +4284 28426 153.0 61.015 26500000.0 6767283.158904388 +28426 4326 26500000.0 6767283.158904388 153.00226464438146 61.015427119610706 +4284 28426 150.3 47.294500000000006 26295782.446630247 5243617.138046312 +28426 4326 26295782.446630247 5243617.138046312 150.30163191909327 47.294718792411615 +4284 28426 155.7 74.7355 26579354.402264643 8299362.701628698 +28426 4326 26579354.402264643 8299362.701628698 155.7041136103714 74.73609920407031 +4284 28427 159.0 63.735 27500000.0 7070436.407700245 +28427 4326 27500000.0 7070436.407700245 159.00264236061773 63.73537628119616 +4284 28427 156.3 51.6165 27313001.699669942 5724229.319586072 +28427 4326 27313001.699669942 5724229.319586072 156.30189703452837 51.61671845658234 +4284 28427 161.7 75.8535 27573667.61180203 8424036.598154908 +28427 4326 27573667.61180203 8424036.598154908 161.70473132075196 75.85400738069065 +4284 28428 165.0 62.25 28500000.0 6904913.303640777 +28428 4326 28500000.0 6904913.303640777 165.00267438112695 62.25025068045316 +4284 28428 162.3 55.248 28328302.584716987 6128271.391067221 +28428 4326 28328302.584716987 6128271.391067221 162.30217578230696 55.248188600283825 +4284 28428 167.7 69.252 28606762.037419833 7688022.353117352 +28428 4326 28606762.037419833 7688022.353117352 167.70352010873452 69.25230195906407 +4284 28429 171.0 62.32 29500000.0 6912714.948260343 +28429 4326 29500000.0 6912714.948260343 171.00280460099492 62.320143235987665 +4284 28429 168.3 55.237 29328255.15561024 6127047.238102845 +28429 4326 29328255.15561024 6127047.238102845 168.30228568922055 55.237090314358284 +4284 28429 173.7 69.40299999999999 29606019.421855878 7704853.15403719 +28429 4326 29606019.421855878 7704853.15403719 173.70369238388395 69.40318686929055 +4284 28430 177.0 66.62 30500000.0 7392105.210780848 +28430 4326 30500000.0 7392105.210780848 177.0033572842467 66.62008484268067 +4284 28430 174.3 62.147 30359233.877299488 6896367.144476949 +28430 4326 30359233.877299488 6896367.144476949 174.30284604509052 62.14707906288871 +4284 28430 179.7 71.093 30597658.33813204 7893239.748026698 +28430 4326 30597658.33813204 7893239.748026698 179.70411033585785 71.09308594445899 +4284 28431 -177.0 68.0 31500000.0 7546013.844298534 +28431 4326 31500000.0 7546013.844298534 -176.99637291121957 67.99997982620903 +4284 28431 -179.7 64.71499999999999 31371298.715467323 7182432.038391958 +28431 4326 31371298.715467323 7182432.038391958 -179.69682063024806 64.71499503698772 +4284 28431 -174.3 71.28500000000001 31596702.85486853 7914643.984382171 +28431 4326 31596702.85486853 7914643.984382171 -174.29577206325072 71.2849617561507 +4284 28432 -171.485 65.69 32477711.532598134 7288485.446295194 +28432 4326 32477711.532598134 7288485.446295194 -171.4816396847263 65.68983928245953 +4284 28432 -173.7485 64.349 32367220.71177891 7141756.667395102 +28432 4326 32367220.71177891 7141756.667395102 -173.74530593397034 64.34887069809866 +4284 28432 -169.2215 67.031 32577471.790679533 7439047.596853634 +28432 4326 32577471.790679533 7439047.596853634 -169.21795844385343 67.03080683159655 +4285 28600 51.185 25.375 196812.91617469914 402464.96981607616 +28600 4326 196812.91617469914 402464.96981607616 51.18422433285383 25.37569992183034 +4285 28600 50.7395 24.6325 151685.78749691183 320298.833374604 +28600 4326 151685.78749691183 320298.833374604 50.73870586607965 24.633180519884203 +4285 28600 51.6305 26.1175 241392.29427306505 484788.34421075555 +28600 4326 241392.29427306505 484788.34421075555 51.62974294315528 26.118219576987936 +4289 28991 5.21 52.225 -12136.02118660164 7673.11249190368 +28991 4326 -12136.02118660164 7673.11249190368 5.209591047240518 52.22400394474389 +4289 28991 3.4010000000000002 50.8975 -139739.63115672246 -138116.21180002365 +28991 4326 -139739.63115672246 -138116.21180002365 3.400844211959775 50.8966643109709 +4289 28991 7.019 53.5525 108107.99614568986 156592.00969232104 +28991 4326 108107.99614568986 156592.00969232104 7.0183220356170954 53.55134732387952 +4289 28992 5.21 52.225 142863.97881339837 470673.11249190365 +28992 4326 142863.97881339837 470673.11249190365 5.209591047240518 52.22400394474389 +4289 28992 3.4010000000000002 50.8975 15260.368843277538 324883.7881999763 +28992 4326 15260.368843277538 324883.7881999763 3.400844211959775 50.8966643109709 +4289 28992 7.019 53.5525 263107.99614568986 619592.0096923211 +28992 4326 263107.99614568986 619592.0096923211 7.0183220356170954 53.55134732387952 +4618 29101 -49.645 -14.335 5469772.458410315 8410163.71911692 +29101 4326 5469772.458410315 8410163.71911692 -49.64545248094854 -14.335436315677986 +4618 29101 -71.57350000000001 -33.5725 3376194.138547222 6145704.511332272 +29101 4326 3376194.138547222 6145704.511332272 -71.57417500243233 -33.572906657968424 +4618 29101 -27.716500000000003 4.902499999999996 7914496.845611783 10599242.1587075 +29101 4326 7914496.845611783 10599242.1587075 -27.71675145072816 4.902202196182892 +4618 29168 -75.0 6.155 500000.0 680340.9963037306 +29168 4326 500000.0 680340.9963037306 -75.00036995869432 6.1546918575871885 +4618 29168 -77.7 0.6155 199461.75880241673 68107.61586932275 +29168 4326 199461.75880241673 68107.61586932275 -77.70037472470175 0.6151758114046194 +4618 29168 -72.3 11.6945 794348.1057443366 1294183.3975427467 +29168 4326 794348.1057443366 1294183.3975427467 -72.30036774451929 11.694214172680624 +4618 29169 -69.0 6.26 500000.0 691948.0538801758 +29169 4326 500000.0 691948.0538801758 -69.00047763998224 6.259651361899296 +4618 29169 -71.7 0.626 199462.3524272964 69269.48493016565 +29169 4326 199462.3524272964 69269.48493016565 -71.7004833512472 0.6256309762863433 +4618 29169 -66.3 11.893999999999998 794135.2443942823 1316266.0888560233 +29169 4326 794135.2443942823 1316266.0888560233 -66.300475385039 11.893679551743997 +4618 29170 -62.995000000000005 5.935 500553.41435849725 656021.5963657644 +29170 4326 500553.41435849725 656021.5963657644 -62.99532928797408 5.93472785567469 +4618 29170 -65.6995 1.1695 199562.47837585205 129410.05064149447 +29170 4326 199562.47837585205 129410.05064149447 -65.69983892205563 1.1692061663988258 +4618 29170 -60.2905 10.7005 796395.6164239624 1184169.953589908 +29170 4326 796395.6164239624 1184169.953589908 -60.29082103374887 10.700253991434401 +4618 29171 -57.0 4.89 500000.0000000014 540506.945891522 +29171 4326 500000.0000000014 540506.945891522 -57.00042608118824 4.8896542503085385 +4618 29171 -59.7 1.551 199554.04938603472 171624.828288578 +29171 4326 199554.04938603472 171624.828288578 -59.700437720310546 1.550636307094973 +4618 29171 -54.3 8.229 797476.8008720189 910621.2047703826 +29171 4326 797476.8008720189 910621.2047703826 -54.30041482058417 8.228675677914184 +4618 29172 -50.325 3.745 574953.2820914373 413971.2012394812 +29172 4326 574953.2820914373 413971.2012394812 -50.325444358949134 3.7446815641400915 +4618 29172 -53.6325 1.8864999999999998 207121.77761175006 208738.01741364066 +29172 4326 207121.77761175006 208738.01741364066 -53.6329667619518 1.8861670064808538 +4618 29172 -47.0175 5.6034999999999995 941401.7077036285 620876.7619557312 +29172 4326 941401.7077036285 620876.7619557312 -47.01792084256453 5.603198040835532 +4618 29187 -79.705 -5.265 643509.7621362573 9417892.042474331 +29187 4326 643509.7621362573 9417892.042474331 -79.70550430336756 -5.265377483866475 +4618 29187 -81.23949999999999 -10.003499999999999 473751.98966777226 8894187.196689527 +29187 4326 473751.98966777226 8894187.196689527 -81.24001243636032 -10.003878393077287 +4618 29187 -78.1705 -0.5265000000000004 814969.3308294599 9941734.204351567 +29187 4326 814969.3308294599 9941734.204351567 -78.17099934358257 -0.5268717286740758 +4618 29188 -74.995 -22.5 500514.2773167853 7511819.7499810625 +29188 4326 500514.2773167853 7511819.7499810625 -74.99553255127586 -22.50039260607029 +4618 29188 -77.6995 -42.75 279070.89878237713 5263395.958564689 +29188 4326 279070.89878237713 5263395.958564689 -77.70017763383092 -42.75034584990661 +4618 29188 -72.2905 -2.25 801382.4685445707 9751025.326736806 +29188 4326 801382.4685445707 9751025.326736806 -72.29098541347386 -2.250376572894024 +4618 29189 -68.995 -21.425 500518.16072592634 7630807.075560726 +29189 4326 500518.16072592634 7630807.075560726 -68.99550981189806 -21.425411543116496 +4618 29189 -71.6995 -42.6425 278689.19767487224 5275334.140894922 +29189 4326 278689.19767487224 5275334.140894922 -71.70015603867654 -42.64288109052349 +4618 29189 -66.2905 -0.20750000000000313 801611.8283055089 9977039.093454089 +29189 4326 801611.8283055089 9977039.093454089 -66.29096521244814 -0.2078715308399601 +4618 29190 -62.995000000000005 -19.86 500523.4888742573 7804003.381538191 +29190 4326 500523.4888742573 7804003.381538191 -62.995480536834265 -19.860427066984844 +4618 29190 -65.6995 -42.486 278134.9109313209 5292713.600325805 +29190 4326 278134.9109313209 5292713.600325805 -65.70012685480236 -42.48641501162425 +4618 29190 -60.2905 2.7659999999999982 801264.2218835811 10306073.490899734 +29190 4326 801264.2218835811 10306073.490899734 -60.29094079022104 2.7656418388760673 +4618 29191 -56.995000000000005 -17.2 500531.65037399787 8098313.180592927 +29191 4326 500531.65037399787 8098313.180592927 -56.99544425949478 -17.200436068399423 +4618 29191 -59.6995 -36.739 258964.17967713668 5930668.452237112 +29191 4326 258964.17967713668 5930668.452237112 -59.700045360506245 -36.739450604032854 +4618 29191 -54.2905 2.3389999999999986 801363.8085016601 10258823.088313082 +29191 4326 801363.8085016601 10258823.088313082 -54.29091086695917 2.3386416642969503 +4618 29192 -50.995000000000005 -14.335 500539.1644881378 8415218.371387033 +29192 4326 500539.1644881378 8415218.371387033 -50.995462525489074 -14.335433713258384 +4618 29192 -53.6995 -33.5725 249427.95879458377 6281964.145729855 +29192 4326 249427.95879458377 6281964.145729855 -53.70005988056261 -33.57299655202547 +4618 29192 -48.2905 4.902499999999996 800516.0601546101 10542496.27293185 +29192 4326 800516.0601546101 10542496.27293185 -48.29093003459198 4.902191594595811 +4618 29193 -45.0 -14.184999999999999 500000.0 8431808.048099866 +29193 4326 500000.0 8431808.048099866 -45.00041576206403 -14.185443917250732 +4618 29193 -47.7 -31.5685 243739.34545035547 6504216.646767427 +29193 4326 243739.34545035547 6504216.646767427 -47.700497225591874 -31.569016470321298 +4618 29193 -42.3 3.198500000000003 800089.7253904666 10353929.989616534 +29193 4326 800089.7253904666 10353929.989616534 -42.30038199618091 3.198180752718546 +4618 29194 -39.0 -12.805000000000001 499999.9999999993 8584423.552965969 +29194 4326 499999.9999999993 8584423.552965969 -39.00036262347952 -12.805444321614715 +4618 29194 -41.7 -24.9955 227467.57336339535 7232826.286409084 +29194 4326 227467.57336339535 7232826.286409084 -41.700415106249814 -24.996010273026414 +4618 29194 -36.3 -0.6145000000000032 800538.2972096154 9932003.038325077 +29194 4326 800538.2972096154 9932003.038325077 -36.300330107318565 -0.6148510039524908 +4618 29195 -32.995 -9.805 500548.30307648884 8916143.531790104 +29195 4326 500548.30307648884 8916143.531790104 -32.99530466020983 -9.805428927964055 +4618 29195 -35.6995 -22.4005 222070.04547576688 7520337.336957349 +29195 4326 222070.04547576688 7520337.336957349 -35.69985107747348 -22.401011337473207 +4618 29195 -30.290499999999998 2.7905000000000015 801258.0028855967 10308784.587203722 +29195 4326 801258.0028855967 10308784.587203722 -30.290775390059878 2.7901807102678924 +4292 29220 -60.769999999999996 -51.644999999999996 654297.2129806735 4275971.220864659 +4292 29220 -61.471999999999994 -52.2615 604287.6505738262 4208655.685899431 +4292 29220 -60.068000000000005 -51.0285 705602.3657583052 4342798.478471724 +4292 29221 -58.8 -51.82 375932.84079370275 4257330.415051678 +29221 4326 375932.84079370275 4257330.415051678 -58.804246023668775 -51.82022741464743 +4292 29221 -59.88 -52.440999999999995 304246.8330485028 4185887.5588701647 +29221 4326 304246.8330485028 4185887.5588701647 -59.884360651889814 -52.44120975617959 +4292 29221 -57.72 -51.199000000000005 449692.1805558461 4327682.367133927 +29221 4326 449692.1805558461 4327682.367133927 -57.724133513055925 -51.19924416118624 +4293 29333 12.350000000000001 -23.939999999999998 230303.9581636103 7350106.5122244945 +29333 4326 230303.9581636103 7350106.5122244945 12.349636573274653 -23.940233025746423 +4293 29333 8.651 -29.97 -113098.38846026047 6667807.101181566 +29333 4326 -113098.38846026047 6667807.101181566 8.651033575113727 -29.96966743611496 +4293 29333 16.049 -17.909999999999997 611099.67162955 8019623.672222733 +29333 4326 611099.67162955 8019623.672222733 16.048272549913438 -17.910787683183855 +4293 29371 11.83 -17.84 -87972.29603568929 -460286.2842371136 +29371 4326 -87972.29603568929 -460286.2842371136 11.829704287408767 -17.840782801522792 +4293 29371 11.677 -18.461000000000002 -71502.03359838593 -391620.9259054317 +29371 4326 -71502.03359838593 -391620.9259054317 11.676718987474425 -18.461727346909733 +4293 29371 11.983 -17.218999999999998 -104545.2766634338 -528938.4608577434 +29371 4326 -104545.2766634338 -528938.4608577434 11.982689656547038 -17.219838045111974 +4293 29373 13.004999999999999 -19.424999999999997 -525.0475578452081 -285058.73529506574 +29373 4326 -525.0475578452081 -285058.73529506574 13.004579989703158 -19.425643778095562 +4293 29373 12.1005 -21.6525 93100.76877864175 -38204.4452176342 +29373 4326 93100.76877864175 -38204.4452176342 12.100168777457109 -21.652941142532633 +4293 29373 13.9095 -17.197499999999998 -96738.91848230186 -531356.1360720254 +29373 4326 -96738.91848230186 -531356.1360720254 13.908993069497447 -17.198343850377228 +4293 29375 15.0 -22.84 0.0 93008.91506549863 +29375 4326 0.0 93008.91506549863 14.99935955064364 -22.84034198543452 +4293 29375 14.1 -27.754 88717.50061802893 637640.1689724845 +29375 4326 88717.50061802893 637640.1689724845 14.099432070431629 -27.753886014947412 +4293 29375 15.9 -17.926000000000002 -95346.20992115558 -450733.5449075059 +29375 4326 -95346.20992115558 -450733.5449075059 15.899287675889774 -17.9267857203939 +4293 29377 17.0 -23.104999999999997 0.0 122353.0286298076 +29377 4326 0.0 122353.0286298076 16.999147435709794 -23.105327351230464 +4293 29377 16.091 -28.257499999999997 89189.02732547055 693442.5693968106 +29377 4326 89189.02732547055 693442.5693968106 16.09020987504105 -28.257348989721542 +4293 29377 17.909000000000002 -17.9525 -96285.40334119053 -447795.8878626205 +29377 4326 -96285.40334119053 -447795.8878626205 17.90808315932059 -17.953291808991864 +4293 29379 19.0 -23.174999999999997 0.0 130104.467491785 +29379 4326 0.0 130104.467491785 18.998937057178768 -23.175333173376618 +4293 29379 18.1 -28.3905 88196.07050554003 708175.1475211219 +29379 4326 88196.07050554003 708175.1475211219 18.098988049983998 -28.390349909655246 +4293 29379 19.9 -17.9595 -95328.26611873522 -447025.76661248534 +29379 4326 -95328.26611873522 -447025.76661248534 19.89888164713877 -17.96030163033919 +4293 29381 20.994999999999997 -19.93 523.4050059635558 -229160.63487908072 +29381 4326 523.4050059635558 -229160.63487908072 20.99375689825716 -19.930635994955505 +4293 29381 20.0905 -21.802000000000003 94038.57873112643 -21644.91766164437 +29381 4326 94038.57873112643 -21644.91766164437 20.089334669634468 -21.802463835258784 +4293 29381 21.8995 -18.058 -95222.38371887848 -436123.97727058677 +29381 4326 -95222.38371887848 -436123.97727058677 21.898179936774095 -18.058805653983192 +4293 29383 22.994999999999997 -18.005 529.448060768124 -442221.1812969365 +29383 4326 529.448060768124 -442221.1812969365 22.993570856350452 -18.00581801586575 +4293 29383 22.0905 -18.441499999999998 96070.04672624644 -393671.6324170631 +29383 4326 96070.04672624644 -393671.6324170631 22.089157867343385 -18.44227373630806 +4293 29383 23.8995 -17.5685 -95482.59792890231 -490301.1098533695 +29383 4326 -95482.59792890231 -490301.1098533695 23.8979845453561 -17.569362299019293 +4293 29385 24.634999999999998 -17.825 38688.958095776106 -462103.7946962086 +29385 4326 38688.958095776106 -462103.7946962086 24.633409562689007 -17.8258461827844 +4293 29385 24.0635 -18.1445 99090.91111941953 -426530.4736481706 +29385 4326 99090.91111941953 -426530.4736481706 24.06196336886568 -18.14531422562731 +4293 29385 25.2065 -17.505499999999998 -21926.998666595297 -497487.31957438047 +29385 4326 -21926.998666595297 -497487.31957438047 25.204856056693767 -17.506378087107784 +4810 29701 44.53277083000002 -18.765 445610.1473353302 814880.5474937382 +4810 29701 41.211770830000006 -24.9525 108046.149331789 127439.37912964937 +4810 29701 47.853770830000016 -12.5775 808198.3177510358 1495706.7558556425 +4810 29702 44.53277083000002 -18.765 445610.1473354374 814880.5474929381 +4810 29702 41.211770830000006 -24.9525 108046.76751989301 127439.1560315229 +4810 29702 47.853770830000016 -12.5775 808197.7009444757 1495707.608392546 +4297 29738 45.265 -19.795 527757.3080939348 7811154.587320898 +29738 4326 527757.3080939348 7811154.587320898 45.26472936329712 -19.796342367468416 +4297 29738 42.8035 -25.9105 279970.30117153184 7132345.011137169 +29738 4326 279970.30117153184 7132345.011137169 42.80308413933607 -25.911947837710134 +4297 29738 47.7265 -13.6795 794951.7156905774 8486040.719074296 +29738 4326 794951.7156905774 8486040.719074296 47.726361374070116 -13.680734952156968 +4297 29739 49.515 -17.95 342729.0021817889 8014693.75848032 +29739 4326 342729.0021817889 8014693.75848032 49.51495025123598 -17.95131277935318 +4297 29739 48.1515 -23.584 209258.2170932322 7388901.627040045 +29739 4326 209258.2170932322 7388901.627040045 48.15137571564664 -23.58541468784269 +4297 29739 50.8785 -12.316 486788.68903063127 8638484.635068849 +29739 4326 486788.68903063127 8638484.635068849 50.878519758306126 -12.317210959677526 +4298 29849 111.655 4.11 572690.9216147728 454282.1536234421 +29849 4326 572690.9216147728 454282.1536234421 111.65846019869358 4.109238343476947 +4298 29849 109.5445 1.1760000000000002 338076.94722749863 130016.04341199927 +29849 4326 338076.94722749863 130016.04341199927 109.54823839873359 1.1754747610851917 +4298 29849 113.7655 7.044 805499.1778217158 779459.2058408507 +29849 4326 805499.1778217158 779459.2058408507 113.76868429627608 7.04298986559719 +4298 29850 116.805 4.55 478372.1159099826 502887.65020750836 +29850 4326 478372.1159099826 502887.65020750836 116.8077425785321 4.549181127831358 +4298 29850 114.2805 1.742 197452.25731058547 192747.96928150856 +29850 4326 197452.25731058547 192747.96928150856 114.28359018200095 1.7414223131208597 +4298 29850 119.3295 7.358 757129.6524830188 813933.5270052288 +29850 4326 757129.6524830188 813933.5270052288 119.3318934967442 7.356929544059524 +4298 29871 114.46000000000001 4.26 26368.68301160744 23434.552260258642 +29871 4326 26368.68301160744 23434.552260258642 114.46307215847507 4.2592144841808475 +4298 29871 109.825 1.191 771.7658787675388 6546.124958776287 +29871 4326 771.7658787675388 6546.124958776287 109.82870064892987 1.1904732188217566 +4298 29871 119.095 7.329 51776.31735979098 40453.17127839591 +29871 4326 51776.31735979098 40453.17127839591 119.09742726904483 7.327933306309233 +4298 29872 114.46000000000001 4.26 1740333.0829660904 1546680.4539770675 +29872 4326 1740333.0829660904 1546680.4539770675 114.46307215847507 4.2592144841808475 +4298 29872 109.825 1.191 50936.55219865806 432044.252079233 +29872 4326 50936.55219865806 432044.252079233 109.82870064892987 1.1904732188217566 +4298 29872 119.095 7.329 3417236.9499462023 2669909.3091741265 +29872 4326 3417236.9499462023 2669909.3091741265 119.09742726904483 7.327933306309233 +4298 29873 114.46000000000001 4.26 530452.6012461411 471427.3804540117 +29873 4326 530452.6012461411 471427.3804540117 114.46307215847507 4.2592144841808475 +4298 29873 109.825 1.191 15525.431448955205 131686.85515767476 +29873 4326 15525.431448955205 131686.85515767476 109.82870064892987 1.1904732188217566 +4298 29873 119.095 7.329 1041572.0137228415 813786.9419350672 +29873 4326 1041572.0137228415 813786.9419350672 119.09742726904483 7.327933306309233 +4298 29874 112.61500000000001 2.9400000000000004 2325773.958720139 5325142.466620276 +29874 4326 2325773.958720139 5325142.466620276 112.6183239241369 2.939328537331134 +4298 29874 109.84750000000001 1.059 2018028.1159837642 5117092.796233746 +29874 4326 2018028.1159837642 5117092.796233746 109.85119744925717 1.0584834562283345 +4298 29874 115.3825 4.821000000000001 2632603.507266872 5533770.8934794385 +29874 4326 2632603.507266872 5533770.8934794385 115.38544494396723 4.820164199753051 +4188 29901 -6.76 54.66 280012.4178126399 379810.28719801526 +29901 4326 280012.4178126399 379810.28719801526 -6.760911993419041 54.6600650021324 +4188 29901 -8.038 54.03 197510.1561207345 308984.72699719435 +29901 4326 197510.1561207345 308984.72699719435 -8.038738218460523 54.03013899544707 +4188 29901 -5.481999999999999 55.29 359939.17162589286 452120.98986976163 +29901 4326 359939.17162589286 452120.98986976163 -5.483090950792867 55.28999272876468 +4299 29902 -8.245000000000001 53.41 183709.22692572532 240012.3249781999 +29902 4326 183709.22692572532 240012.3249781999 -8.245706543509403 53.410222141779265 +4299 29902 -10.3285 51.592 38659.31164931215 40270.634504479065 +29902 4326 38659.31164931215 40270.634504479065 -10.328937583442286 51.59244456256471 +4299 29902 -6.1615 55.228 316966.6404096284 443871.16602462146 +29902 4326 316966.6404096284 443871.16602462146 -6.162499446047629 55.22800302168387 +4300 29903 -7.95 53.41 203324.65036763364 239985.52380060684 +29903 4326 203324.65036763364 239985.52380060684 -7.950744266555294 53.41022299042151 +4300 29903 -10.299000000000001 51.592 40703.10172143782 40205.925440268504 +29903 4326 40703.10172143782 40205.925440268504 -10.299441212853814 51.59244457432241 +4300 29903 -5.601 55.228 352619.61069390766 444954.65131574415 +29903 4326 352619.61069390766 444954.65131574415 -5.602074049068347 55.228006079245446 +4301 30161 129.315 30.85 -17692.030092651257 -238340.6042036596 +30161 4326 -17692.030092651257 -238340.6042036596 129.31282370760118 30.85355224472361 +4301 30161 128.28449999999998 27.349 -120248.66349839047 -625732.8697457158 +30161 4326 -120248.66349839047 -625732.8697457158 128.28248468476707 27.352916445868946 +4301 30161 130.34550000000002 34.351 77772.578497618 150141.34541883352 +30161 4326 77772.578497618 150141.34541883352 130.3431428665251 34.35417346662186 +4301 30162 130.905 32.085 -8966.596450462583 -101445.46291785588 +30162 4326 -8966.596450462583 -101445.46291785588 130.90265316238495 32.08844242183806 +4301 30162 129.87449999999998 30.3705 -108169.43095592297 -290965.9417243918 +30162 4326 -108169.43095592297 -290965.9417243918 129.8722852063478 30.374114721589358 +4301 30162 131.93550000000002 33.7995 86610.99279797764 89049.00629399093 +30162 4326 86610.99279797764 89049.00629399093 131.93301472281976 33.80276823187895 +4301 30163 132.15 35.05 -1520.20997598226 -105380.87703657153 +30163 4326 -1520.20997598226 -105380.87703657153 132.1474579562652 35.053128736764364 +4301 30163 130.94400000000002 33.853 -113128.49295154521 -237465.15882319145 +30163 4326 -113128.49295154521 -237465.15882319145 130.94160241575838 33.85624247692179 +4301 30163 133.356 36.247 106878.15381436438 28057.718895954164 +30163 4326 106878.15381436438 28057.718895954164 133.3533092331481 36.250015525245075 +4301 30164 133.38 33.57 -11139.284601787094 63211.93602495597 +30164 4326 -11139.284601787094 63211.93602495597 133.37739306499958 33.573324081471675 +4301 30164 132.093 32.778 -131793.6442335524 -23739.126728059226 +30164 4326 -131793.6442335524 -23739.126728059226 132.09052957435813 32.781387233597926 +4301 30164 134.667 34.362 107332.74068303776 151654.50942764996 +30164 4326 107332.74068303776 151654.50942764996 134.6642548635049 34.36526229977206 +4301 30165 134.3 34.92 -3045.2298103087187 -119799.76881566456 +30165 4326 -3045.2298103087187 -119799.76881566456 134.29726898169173 34.92319040951955 +4301 30165 133.24699999999999 34.209 -100094.49661491073 -198123.64132839863 +30165 4326 -100094.49661491073 -198123.64132839863 133.24438538298935 34.21224866270175 +4301 30165 135.353 35.631 92344.60453895718 -40455.4451997174 +30165 4326 92344.60453895718 -40455.4451997174 135.3501510603857 35.634133068736105 +4301 30166 135.925 34.864999999999995 -6856.335558489861 -125898.07903811253 +30166 4326 -6856.335558489861 -125898.07903811253 135.92212756872726 34.86823413712399 +4301 30166 134.96650000000002 33.5465 -95965.08773360604 -271648.38520893396 +30166 4326 -95965.08773360604 -271648.38520893396 134.9637548594642 33.549861022679714 +4301 30166 136.8835 36.183499999999995 79458.1840608255 20718.78101121502 +30166 4326 79458.1840608255 20718.78101121502 136.8804954161571 36.18660684991441 +4301 30167 137.03 36.045 -12312.667222157577 5000.784693900735 +30167 4326 -12312.667222157577 5000.784693900735 137.02698782422812 36.048126424065245 +4301 30167 136.301 34.6635 -79330.95291708173 -147908.8372772045 +30167 4326 -79330.95291708173 -147908.8372772045 136.29810180242598 34.666765878553214 +4301 30167 137.75900000000001 37.4265 52417.36264884369 158433.9424356087 +30167 4326 52417.36264884369 158433.9424356087 137.7558685013538 37.42948581820741 +4301 30168 138.615 36.56 10292.76371849342 62133.2818394533 +30168 4326 10292.76371849342 62133.2818394533 138.61182916063555 36.5631084299275 +4301 30168 137.4495 34.742 -96179.0670333579 -139040.58487753308 +30168 4326 -96179.0670333579 -139040.58487753308 137.44649975235816 34.74528482210036 +4301 30168 139.7805 38.378 111867.27410843839 264634.9498897339 +30168 4326 111867.27410843839 264634.9498897339 139.7771484459298 38.38093091513748 +4301 30169 139.755 33.644999999999996 -7265.182551328764 -261201.0586888302 +30169 4326 -7265.182551328764 -261201.0586888302 139.75184518779363 33.64846433525084 +4301 30169 138.5355 29.743499999999997 -125521.95223517908 -693014.9536313188 +30169 4326 -125521.95223517908 -693014.9536313188 138.53257192213582 29.747351596499797 +4301 30169 140.9745 37.546499999999995 100825.13687008806 172196.86161894153 +30169 4326 100825.13687008806 172196.86161894153 140.97108312609396 37.54956111569583 +4301 30170 140.815 39.655 -1573.0581081899195 -38297.669950110525 +30170 4326 -1573.0581081899195 -38297.669950110525 140.81149573436736 39.65781215530025 +4301 30170 139.6225 37.9225 -106440.85585175776 -229893.97021810073 +30170 4326 -106440.85585175776 -229893.97021810073 139.61918301747082 37.925479256189895 +4301 30170 142.0075 41.387499999999996 98187.13089420526 154712.17185415802 +30170 4326 98187.13089420526 154712.17185415802 142.00379711492937 41.390144668971374 +4301 30171 140.4 42.38 12350.67799901601 -179927.55694505727 +30171 4326 12350.67799901601 -179927.55694505727 140.3963865362657 42.38247432931174 +4301 30171 139.446 41.444 -67174.18848135686 -283567.7295919935 +30171 4326 -67174.18848135686 -283567.7295919935 139.44252634732493 41.446558012501676 +4301 30171 141.354 43.316 89539.1028598338 -75388.40710727562 +30171 4326 89539.1028598338 -75388.40710727562 141.3502425419944 43.3183913452748 +4301 30172 142.25 43.845 0.0 -17218.540618700372 +30172 4326 0.0 -17218.540618700372 142.24612611015064 43.84735681021419 +4301 30172 141.02599999999998 42.3195 -100878.9468714226 -185931.87064813753 +30172 4326 -100878.9468714226 -185931.87064813753 141.02233239857634 42.3220015199302 +4301 30172 143.47400000000002 45.3705 95863.24810787095 152994.34659417163 +30172 4326 95863.24810787095 152994.34659417163 143.46990800935075 45.37271274711985 +4301 30173 144.24 43.135 -813.4426841783018 -96084.54420019414 +30173 4326 -813.4426841783018 -96084.54420019414 144.23599154257516 43.13751117009692 +4301 30173 142.77300000000002 41.9965 -122351.74370504459 -221472.25353072406 +30173 4326 -122351.74370504459 -221472.25353072406 142.76919339203798 41.99909717171706 +4301 30173 145.707 44.2735 116296.91325224107 31415.891788647514 +30173 4326 116296.91325224107 31415.891788647514 145.70278311700085 44.27592735687731 +4301 30174 141.765 26.235 -23475.577465148774 26051.63339654971 +30174 4326 -23475.577465148774 26051.63339654971 141.76191844442906 26.239272845666477 +4301 30174 141.2565 24.826500000000003 -75146.45679257542 -129768.37155545768 +30174 4326 -75146.45679257542 -129768.37155545768 141.25349200477984 24.830895811488293 +4301 30174 142.2735 27.6435 26984.157087773212 182094.1658657762 +30174 4326 26984.157087773212 182094.1658657762 142.27034145259054 27.64764768325708 +4301 30175 127.515 26.465 1495.4780693864507 51507.775005307936 +30175 4326 1495.4780693864507 51507.775005307936 127.51306635084461 26.46899899465207 +4301 30175 126.71849999999999 26.0645 -78183.67543784124 7378.706244339066 +30175 4326 -78183.67543784124 7378.706244339066 126.71664145394386 26.06853047879665 +4301 30175 128.3115 26.8655 80624.97268109204 96131.4661096601 +30175 4326 80624.97268109204 96131.4661096601 128.3094910063036 26.869467931607247 +4301 30176 124.17 24.46 17232.02503626402 -170550.67549778894 +30176 4326 17232.02503626402 -170550.67549778894 124.16838428884735 24.464167299578637 +4301 30176 122.964 24.028 -105372.9449998586 -218012.7933479311 +30176 4326 -105372.9449998586 -218012.7933479311 122.96249395391003 24.032199449022105 +4301 30176 125.376 24.892000000000003 139006.91429646642 -122016.40653356702 +30176 4326 139006.91429646642 -122016.40653356702 125.3742744993157 24.896136095964316 +4301 30177 131.25 25.205 25190.050291575757 -88030.50197420763 +30177 4326 25190.050291575757 -88030.50197420763 131.24777302854304 25.209182309789234 +4301 30177 131.133 24.4805 13479.337502543562 -168284.55034056696 +30177 4326 13479.337502543562 -168284.55034056696 131.13079558903027 24.484753332551232 +4301 30177 131.367 25.9295 36757.25017712678 -7757.464755083381 +30177 4326 36757.25017712678 -7757.464755083381 131.36474985120975 25.93361050661433 +4301 30178 136.09 20.425 9390.66565156821 47043.64680564163 +30178 4326 9390.66565156821 47043.64680564163 136.08747066951395 20.42971061027739 +4301 30178 136.02700000000002 20.375500000000002 2818.0988906439275 41562.28576836787 +30178 4326 2818.0988906439275 41562.28576836787 136.02447629593294 20.380214136439225 +4301 30178 136.153 20.4745 15959.029168441239 52527.575682866736 +30178 4326 15959.029168441239 52527.575682866736 136.15046504067138 20.479207085705507 +4301 30179 153.98000000000002 24.285 -2030.0867350771923 -189940.854838399 +30179 4326 -2030.0867350771923 -189940.854838399 153.97614182788573 24.289734400696627 +4301 30179 153.917 24.226499999999998 -8428.717085448781 -196416.76629686556 +30179 4326 -8428.717085448781 -196416.76629686556 153.9131474289485 24.231237583435938 +4301 30179 154.043 24.343500000000002 4362.684839714959 -183462.00513952217 +30179 4326 4362.684839714959 -183462.00513952217 154.0391362223397 24.34823122043958 +4302 30200 -61.045 10.67 586818.5743149376 450614.68778970453 +30200 4326 586818.5743149376 450614.68778970453 -61.044234609427846 10.673706279954718 +4302 30200 -61.9855 9.914 74454.84055334954 35234.99416258016 +30200 4326 74454.84055334954 35234.99416258016 -61.98477826058431 9.91775068732127 +4302 30200 -60.1045 11.426 1096626.8923564877 867633.4076840733 +30200 4326 1096626.8923564877 867633.4076840733 -60.103690816070305 11.429661263792346 +4303 30339 52.785 23.54 682215.7613206664 2604461.548918495 +4303 30339 51.682500000000005 22.838 570028.682830083 2525775.6777778952 +4303 30339 53.887499999999996 24.242 793224.6283978865 2684084.0583707923 +4303 30340 55.010000000000005 23.985 297542.08272778697 2654023.964568082 +4303 30340 54.092 22.7655 201378.05078424746 2520522.34485827 +4303 30340 55.928000000000004 25.2045 391999.4261392766 2788052.3161438406 +4304 30491 3.2100000000000004 35.89 546031.5611749667 287920.0393221071 +30491 4326 546031.5611749667 287920.0393221071 3.207313950518171 35.88912667014021 +4304 30491 -1.677 34.765 99471.02704097738 172019.52560168615 +30491 4326 99471.02704097738 172019.52560168615 -1.6797201446260124 34.76407071070576 +4304 30491 8.097 37.015 979969.7539896388 425883.3889057004 +30491 4326 979969.7539896388 425883.3889057004 8.094367785123488 37.01418898193424 +4304 30492 3.0699999999999994 33.324999999999996 534438.5748057925 302832.6445213123 +30492 4326 534438.5748057925 302832.6445213123 3.067393040960999 33.32424740037101 +4304 30492 -2.3480000000000003 32.1235 23887.8721031663 181089.73882232737 +30492 4326 23887.8721031663 181089.73882232737 -2.3506468993150147 32.12270022827263 +4304 30492 8.488 34.5265 1031070.7368129415 450729.1908384467 +30492 4326 1031070.7368129415 450729.1908384467 8.4854565527615 34.525802107994096 +4671 30493 3.2100000000000004 35.89 546031.5611749667 287920.0393221071 +4671 30493 -1.677 34.765 99471.02704097738 172019.52560168615 +4671 30493 8.097 37.015 979969.7539896388 425883.3889057004 +4671 30494 3.0699999999999994 33.324999999999996 534438.5748057925 302832.6445213123 +4671 30494 -2.3480000000000003 32.1235 23887.8721031663 181089.73882232737 +4671 30494 8.488 34.5265 1031070.7368129415 450729.1908384467 +4307 30729 -7.335 27.79 664041.2480816342 3074805.4302555732 +30729 4326 664041.2480816342 3074805.4302555732 -7.33604559447152 27.790520181368173 +4307 30729 -8.5365 25.936 546412.7203342135 2868444.4736098414 +30729 4326 546412.7203342135 2868444.4736098414 -8.537580926776089 25.936631196068014 +4307 30729 -6.1335 29.644000000000002 777516.8778164403 3282516.997986604 +30729 4326 777516.8778164403 3282516.997986604 -6.134511706551906 29.644416237563746 +4307 30730 -3.0 29.415 499999.9999999991 3253709.6496804617 +30730 4326 499999.9999999991 3253709.6496804617 -3.000910170888582 29.41543601838679 +4307 30730 -5.7 22.5795 222370.13737753348 2499274.8409121535 +30730 4326 222370.13737753348 2499274.8409121535 -5.700984561728789 22.58035082563254 +4307 30730 -0.3000000000000007 36.250499999999995 742608.8085051384 4014825.59594598 +30730 4326 742608.8085051384 4014825.59594598 -0.30085195859794334 36.250613537118284 +4307 30731 3.005 28.87 500487.6225478266 3193330.269850288 +30731 4326 500487.6225478266 3193330.269850288 3.0042897729279017 28.870482714054628 +4307 30731 0.3005 19.96 217459.43446379533 2209138.800892812 +30731 4326 217459.43446379533 2209138.800892812 0.2997238755184156 19.961036135694762 +4307 30731 5.709499999999999 37.78 738627.2537535444 4184569.399000286 +30731 4326 738627.2537535444 4184569.399000286 5.708834951134989 37.78007592978645 +4307 30732 8.995000000000001 29.2 499513.9258956403 3229889.670897729 +30732 4326 499513.9258956403 3229889.670897729 8.994489824756943 29.200484893396542 +4307 30732 6.2995 20.560000000000002 218440.054625645 2275590.1065092175 +30732 4326 218440.054625645 2275590.1065092175 6.29893293570184 20.561009627403372 +4307 30732 11.6905 37.839999999999996 736761.7777088298 4191179.5901367697 +30732 4326 736761.7777088298 4191179.5901367697 11.690032461171741 37.84009656370496 +4307 30791 3.2100000000000004 35.89 546166.5609654302 288010.0395677099 +30791 4326 546166.5609654302 288010.0395677099 3.2092638610290365 35.890140825405425 +4307 30791 -1.677 34.765 99606.02895374002 172109.52836778513 +30791 4326 99606.02895374002 172109.52836778513 -1.677888692026168 34.765172249156095 +4307 30791 8.097 37.015 980104.751910629 425973.3866139632 +30791 4326 980104.751910629 425973.3866139632 8.096418229230455 37.01511392062552 +4307 30792 2.685 33.074999999999996 498734.85499390506 275146.19701496523 +30792 4326 498734.85499390506 275146.19701496523 2.6842609791549554 33.07526427810685 +4307 30792 -3.1965 31.6485 -58900.076047570445 132795.72099830606 +30792 4326 -58900.076047570445 132795.72099830606 -3.1974242059926903 31.64881655983511 +4307 30792 8.5665 34.50149999999999 1038556.6534475875 448452.98174262 +30792 4326 1038556.6534475875 448452.98174262 8.565948117206235 34.501719959881335 +4310 31028 -15.79 13.67 414562.770109509 1511234.8682179393 +31028 4326 414562.770109509 1511234.8682179393 -15.788385510657145 13.669507215836814 +4310 31028 -19.776999999999997 10.943000000000001 -22533.90251492645 1213709.9710662072 +31028 4326 -22533.90251492645 1213709.9710662072 -19.775457196346288 10.942774601523757 +4310 31028 -11.802999999999999 16.397 841525.9976982618 1815382.6065853683 +31028 4326 841525.9976982618 1815382.6065853683 -11.801316386514747 16.396239806356164 +4311 31121 -55.37 5.59 680552.5480500578 618139.630251666 +31121 4326 680552.5480500578 618139.630251666 -55.37135259887663 5.586837996663828 +4311 31121 -57.809 2.206 410038.28201936494 243857.59554713874 +31121 4326 410038.28201936494 243857.59554713874 -57.810441365525776 2.202785944847849 +4311 31121 -52.931 8.974 947650.1850653031 994471.4240517033 +31121 4326 947650.1850653031 994471.4240517033 -52.9322650582176 8.970909304284444 +4311 31154 -54.955 7.345 394589.7141255967 812009.0337568521 +31154 4326 394589.7141255967 812009.0337568521 -54.956340881266826 7.341870071092088 +4311 31154 -57.0205 5.5405 165287.3364600393 613270.0090746217 +31154 4326 165287.3364600393 613270.0090746217 -57.02191673667757 5.537333321102015 +4311 31154 -52.8895 9.1495 622021.1038243576 1011579.954091213 +31154 4326 622021.1038243576 1011579.954091213 -52.890763988419216 9.146413230748207 +4311 31170 -56.015 3.945 463179.0114162597 436060.04649699543 +31170 4326 463179.0114162597 436060.04649699543 -56.01637457251779 3.9418111906094704 +4311 31170 -57.8735 2.0415 256373.7596111269 225816.44242697558 +31170 4326 256373.7596111269 225816.44242697558 -57.87494366833895 2.0382839538545876 +4311 31170 -54.1565 5.8485 669046.2459523664 646694.1533891223 +31170 4326 669046.2459523664 646694.1533891223 -54.15780522229822 5.84534518866741 +4311 31171 -56.015 3.945 463167.96069939784 436190.9168590892 +31171 4326 463167.96069939784 436190.9168590892 -56.01637457251779 3.9418111906094704 +4311 31171 -57.8735 2.0415 256300.64249216267 225884.21446852028 +31171 4326 256300.64249216267 225884.21446852028 -57.87494366833895 2.0382839538545876 +4311 31171 -54.1565 5.8485 669096.980119819 646888.239269491 +31171 4326 669096.980119819 646888.239269491 -54.15780522229822 5.845345188667409 +4805 31251 28.3516666666667 47.19 26648.147619022297 227897.5640976252 +4805 31251 27.3121666666667 46.812000000000005 -52490.61953539835 186049.41839076485 +4805 31251 29.3911666666667 47.568 104666.42295480872 270796.1719891643 +4805 31252 31.0016666666667 47.595 125.33069089944287 272859.8097567456 +4805 31252 29.6471666666667 46.5195 -103796.77543417948 154196.99728107173 +4805 31252 32.356166666666695 48.6705 99872.74477747493 393321.9623784879 +4805 31253 33.6666666666667 47.790000000000006 -24972.853214554274 294592.12501041964 +4805 31253 32.6136666666667 46.683 -106047.5007568458 172414.4258234799 +4805 31253 34.7196666666667 48.897000000000006 52761.19126411677 417869.2165814061 +4312 31254 10.684999999999999 47.19 26648.147619022297 227897.5640976252 +31254 4326 26648.147619022297 227897.5640976252 10.684642612005641 47.1894402796731 +4312 31254 9.6455 46.812000000000005 -52490.619535398226 186049.41839076485 +31254 4326 -52490.619535398226 186049.41839076485 9.645306910978896 46.811456190799966 +4312 31254 11.724499999999999 47.568 104666.42295480872 270796.1719891643 +31254 4326 104666.42295480872 270796.1719891643 11.723976524772661 47.56742565036281 +4312 31255 13.335 47.595 125.33069089944287 272859.8097567456 +31255 4326 125.33069089944287 272859.8097567456 13.33425411679331 47.59447105940481 +4312 31255 11.9805 46.5195 -103796.77543417958 154196.99728107173 +31255 4326 -103796.77543417958 154196.99728107173 11.98000748809481 46.5190558844523 +4312 31255 14.689499999999999 48.6705 99872.7447774754 393321.9623784879 +31255 4326 99872.7447774754 393321.9623784879 14.688491119914028 48.669888237644024 +4312 31256 16.0 47.790000000000006 -24972.853214554038 294592.12501041964 +31256 4326 -24972.853214554038 294592.12501041964 15.998878186169419 47.78953545177158 +4312 31256 14.947000000000001 46.683 -106047.5007568458 172414.4258234799 +31256 4326 -106047.5007568458 172414.4258234799 14.946099631750625 46.68262935756291 +4312 31256 17.053 48.897000000000006 52761.19126411724 417869.2165814061 +31256 4326 52761.19126411724 417869.2165814061 17.051647645337514 48.8964426995179 +4312 31257 10.684999999999999 47.19 176648.1476190223 227897.5640976252 +31257 4326 176648.1476190223 227897.5640976252 10.684642612005641 47.1894402796731 +4312 31257 9.6455 46.812000000000005 97509.38046460177 186049.41839076485 +31257 4326 97509.38046460177 186049.41839076485 9.645306910978896 46.811456190799966 +4312 31257 11.724499999999999 47.568 254666.42295480872 270796.1719891643 +31257 4326 254666.42295480872 270796.1719891643 11.723976524772661 47.56742565036281 +4312 31258 13.335 47.595 450125.33069089946 272859.8097567456 +31258 4326 450125.33069089946 272859.8097567456 13.33425411679331 47.59447105940481 +4312 31258 11.9805 46.5195 346203.2245658204 154196.99728107173 +31258 4326 346203.2245658204 154196.99728107173 11.98000748809481 46.5190558844523 +4312 31258 14.689499999999999 48.6705 549872.7447774754 393321.9623784879 +31258 4326 549872.7447774754 393321.9623784879 14.688491119914028 48.669888237644024 +4312 31259 16.0 47.790000000000006 725027.146785446 294592.12501041964 +31259 4326 725027.146785446 294592.12501041964 15.998878186169419 47.78953545177158 +4312 31259 14.947000000000001 46.683 643952.4992431542 172414.4258234799 +31259 4326 643952.4992431542 172414.4258234799 14.946099631750625 46.68262935756291 +4312 31259 17.053 48.897000000000006 802761.1912641173 417869.2165814061 +31259 4326 802761.1912641173 417869.2165814061 17.051647645337514 48.8964426995179 +4805 31281 28.3516666666667 47.19 26648.147619022297 5227897.564097625 +4805 31281 27.3121666666667 46.812000000000005 -52490.61953539835 5186049.418390765 +4805 31281 29.3911666666667 47.568 104666.42295480872 5270796.171989164 +4805 31282 31.0016666666667 47.595 125.33069089944287 5272859.809756746 +4805 31282 29.6471666666667 46.5195 -103796.77543417948 5154196.997281072 +4805 31282 32.356166666666695 48.6705 99872.74477747493 5393321.962378488 +4805 31283 33.6666666666667 47.790000000000006 -24972.853214554274 5294592.12501042 +4805 31283 32.6136666666667 46.683 -106047.5007568458 5172414.42582348 +4805 31283 34.7196666666667 48.897000000000006 52761.19126411677 5417869.216581406 +4312 31284 10.684999999999999 47.19 176648.1476190223 5227897.564097625 +31284 4326 176648.1476190223 5227897.564097625 10.684642612005641 47.1894402796731 +4312 31284 9.6455 46.812000000000005 97509.38046460177 5186049.418390765 +31284 4326 97509.38046460177 5186049.418390765 9.645306910978896 46.811456190799966 +4312 31284 11.724499999999999 47.568 254666.42295480872 5270796.171989164 +31284 4326 254666.42295480872 5270796.171989164 11.723976524772661 47.56742565036281 +4312 31285 13.335 47.595 450125.33069089946 5272859.809756746 +31285 4326 450125.33069089946 5272859.809756746 13.33425411679331 47.59447105940481 +4312 31285 11.9805 46.5195 346203.2245658204 5154196.997281072 +31285 4326 346203.2245658204 5154196.997281072 11.98000748809481 46.5190558844523 +4312 31285 14.689499999999999 48.6705 549872.7447774754 5393321.962378488 +31285 4326 549872.7447774754 5393321.962378488 14.688491119914028 48.669888237644024 +4312 31286 16.0 47.790000000000006 725027.146785446 5294592.12501042 +31286 4326 725027.146785446 5294592.12501042 15.998878186169419 47.78953545177158 +4312 31286 14.947000000000001 46.683 643952.4992431542 5172414.42582348 +31286 4326 643952.4992431542 5172414.42582348 14.946099631750625 46.68262935756291 +4312 31286 17.053 48.897000000000006 802761.1912641173 5417869.216581406 +31286 4326 802761.1912641173 5417869.216581406 17.051647645337514 48.8964426995179 +4312 31287 13.350000000000001 47.71 401250.13890491024 423337.9362568072 +31287 4326 401250.13890491024 423337.9362568072 13.349244222002858 47.70945805019638 +4312 31287 9.911999999999999 46.531 137687.42496406945 298094.6314776786 +31287 4326 137687.42496406945 298094.6314776786 9.911786501354207 46.53049620194797 +4312 31287 16.788 48.889 653218.6872336554 560023.3120738221 +31287 4326 653218.6872336554 560023.3120738221 16.78668466738728 48.88843421606961 +4805 31288 28.3516666666667 47.19 176648.1476190223 5227897.564097625 +4805 31288 27.3121666666667 46.812000000000005 97509.38046460165 5186049.418390765 +4805 31288 29.3911666666667 47.568 254666.42295480872 5270796.171989164 +4805 31289 31.0016666666667 47.595 450125.33069089946 5272859.809756746 +4805 31289 29.6471666666667 46.5195 346203.22456582054 5154196.997281072 +4805 31289 32.356166666666695 48.6705 549872.7447774749 5393321.962378488 +4805 31290 33.6666666666667 47.790000000000006 725027.1467854457 5294592.12501042 +4805 31290 32.6136666666667 46.683 643952.4992431542 5172414.42582348 +4805 31290 34.7196666666667 48.897000000000006 802761.1912641168 5417869.216581406 +4313 31300 4.45 50.504999999999995 156601.75848411626 132632.59625377692 +31300 4326 156601.75848411626 132632.59625377692 4.451263096909618 50.504445059597316 +4313 31300 2.695 49.6005 29859.24961263979 33364.514945448376 +31300 4326 29859.24961263979 33364.514945448376 2.696201855781717 49.59992961581843 +4313 31300 6.205 51.4095 278584.70223982126 234853.16663083062 +31300 4326 278584.70223982126 234853.16663083062 6.206326354374921 51.40896014749665 +4313 31370 4.45 50.504999999999995 155853.55336673203 132631.7548083486 +31370 4326 155853.55336673203 132631.7548083486 4.451263096909618 50.504445059597316 +4313 31370 2.695 49.6005 29096.945414454793 33381.677397467196 +31370 4326 29096.945414454793 33381.677397467196 2.696201855781716 49.59992961581843 +4313 31370 6.205 51.4095 277851.0155930063 234834.99732954614 +31370 4326 277851.0155930063 234834.99732954614 6.206326354374921 51.40896014749663 +4314 31466 6.6850000000000005 51.46 2547602.3140389803 5702898.528463662 +31466 4326 2547602.3140389803 5702898.528463662 6.684298277567375 51.45868126906761 +4314 31466 5.9425 49.345 2495822.2669741916 5467438.493669685 +31466 4326 2495822.2669741916 5467438.493669685 5.941908751059255 49.343923254507395 +4314 31466 7.4275 53.575 2594545.831218262 5938947.830974243 +31466 4326 2594545.831218262 5938947.830974243 7.426677164135287 53.573439548231455 +4314 31467 9.004999999999999 51.18000000000001 3500349.5859045233 5671528.29132669 +31467 4326 3500349.5859045233 5671528.29132669 9.003949551872092 51.178718107244805 +4314 31467 7.6505 47.661 3398648.2705067503 5281079.407655729 +31467 4326 3398648.2705067503 5281079.407655729 7.649668106573623 47.660116655680355 +4314 31467 10.3595 54.699000000000005 3587636.6123899953 6063943.872055777 +31467 4326 3587636.6123899953 6063943.872055777 10.358193911326715 54.69732092630811 +4314 31468 12.004999999999999 50.99 4500351.020978886 5650393.221354948 +31468 4326 4500351.020978886 5650393.221354948 12.003502614424583 50.98875623891162 +4314 31468 10.6505 47.75 4398820.678964211 5290973.469232433 +31468 4326 4398820.678964211 5290973.469232433 10.649247120828502 47.74911730283645 +4314 31468 13.3595 54.230000000000004 4588644.616662983 6011748.581694027 +31468 4326 4588644.616662983 6011748.581694027 13.357719375165813 54.228397053904274 +4314 31469 13.67 48.745 5402198.71253722 5401571.519510001 +31469 4326 5402198.71253722 5401571.519510001 13.668304879586517 48.74402615384391 +4314 31469 13.517 48.5335 5390491.252891952 5378263.45025934 +31469 4326 5390491.252891952 5378263.45025934 13.515330959782458 48.53254881806341 +4314 31469 13.823 48.9565 5413813.141910217 5424903.380188386 +31469 4326 5413813.141910217 5424903.380188386 13.8212785738042 48.95550351729651 +4315 31528 -13.565000000000001 10.844999999999999 656864.6517899282 1199102.4871203515 +31528 4326 656864.6517899282 1199102.4871203515 -13.56274675957611 10.843897309285195 +4315 31528 -14.973500000000001 9.1935 502911.2116853281 1016152.8863213988 +31528 4326 502911.2116853281 1016152.8863213988 -14.971277393664666 9.192554827603807 +4315 31528 -12.156500000000001 12.4965 809084.8022980315 1382993.3000157778 +31528 4326 809084.8022980315 1382993.3000157778 -12.154215285146694 12.495240437150922 +4315 31529 -9.825 9.85 409537.8476094216 1088840.7542522482 +31529 4326 409537.8476094216 1088840.7542522482 -9.82270921511924 9.848961411700468 +4315 31529 -11.7825 7.456 192854.86137441744 825051.6772918587 +31529 4326 192854.86137441744 825051.6772918587 -11.780245655580991 7.455197162622021 +4315 31529 -7.867500000000001 12.244 623180.7467921197 1353671.5901930365 +31529 4326 623180.7467921197 1353671.5901930365 -7.86517093366629 12.242726553356604 +4316 31600 25.0 45.945 469576.9721509809 505075.09339221264 +31600 4326 469576.9721509809 505075.09339221264 24.998263853794345 45.94192587619835 +4316 31600 20.734 43.8525 125527.89204324223 283255.97806055954 +31600 4326 125527.89204324223 283255.97806055954 20.732377663767597 43.84930424963653 +4316 31600 29.266 48.0375 788805.5560430919 744722.7956751493 +31600 4326 788805.5560430919 744722.7956751493 29.264148718949542 48.034561200636965 +4318 31838 47.269999999999996 29.310000000000002 720465.1733240945 3244470.4123299853 +31838 4326 720465.1733240945 3244470.4123299853 47.26998438327057 29.310050109495883 +4318 31838 46.613 28.608 657706.5286938802 3165619.734671207 +31838 4326 657706.5286938802 3165619.734671207 46.612983743825644 28.60804957261618 +4318 31838 47.927 30.012 782330.2056614563 3323724.107215195 +31838 4326 782330.2056614563 3323724.107215195 47.92698503123593 30.012050638322904 +4318 31839 48.239999999999995 29.29 231875.52500513959 3243276.795533482 +31839 4326 231875.52500513959 3243276.795533482 48.23998549634526 29.290050205837897 +4318 31839 48.024 28.615 208998.91360123717 3168953.650213754 +31839 4326 208998.91360123717 3168953.650213754 48.02398534413213 28.61504973809414 +4318 31839 48.455999999999996 29.965 254510.32116709277 3317630.334252663 +31839 4326 254510.32116709277 3317630.334252663 48.455985648755 29.96505066703031 +4319 31901 47.97 29.310000000000002 497085.59731784544 3243629.5789169846 +31901 4326 497085.59731784544 3243629.5789169846 47.97023667651309 29.310043450943166 +4319 31901 47.799 29.184 480449.54420817614 3229680.10581092 +31901 4326 480449.54420817614 3229680.10581092 47.79923631163763 29.184043296198436 +4319 31901 48.141 29.436 513680.8415484996 3257603.5633223006 +31901 4326 513680.8415484996 3257603.5633223006 48.14123705293231 29.436043635135768 +4674 31965 -117.0 23.865000000000002 500000.0 2639280.2910850556 +31965 4326 500000.0 2639280.2910850556 -117.0 23.865 +4674 31965 -119.7 15.8955 210881.5140836623 1759242.9719442937 +31965 4326 210881.5140836623 1759242.9719442937 -119.7 15.895499999999998 +4674 31965 -114.3 31.8345 755528.7231442316 3525268.709203138 +31965 4326 755528.7231442316 3525268.709203138 -114.3 31.83449999999999 +4674 31966 -111.0 23.68 500000.0 2618798.8219067925 +31966 4326 500000.0 2618798.8219067925 -111.0 23.68 +4674 31966 -113.7 15.949 210958.14795963367 1765166.42438099 +31966 4326 210958.14795963367 1765166.42438099 -113.7 15.948999999999996 +4674 31966 -108.3 31.411 756689.9510171973 3478306.0767350667 +31966 4326 756689.9510171973 3478306.0767350667 -108.3 31.41099999999999 +4674 31967 -105.0 22.92 499999.9999999987 2534663.831269058 +31967 4326 499999.9999999987 2534663.831269058 -105.00000000000001 22.92000000000001 +4674 31967 -107.7 14.937000000000001 209551.16850779124 1653122.8520861657 +31967 4326 209551.16850779124 1653122.8520861657 -107.7 14.937000000000001 +4674 31967 -102.3 30.903 758064.3626219081 3421976.282493674 +31967 4326 758064.3626219081 3421976.282493674 -102.3 30.90299999999999 +4674 31968 -99.005 21.055 499480.546534659 2328234.6668210863 +31968 4326 499480.546534659 2328234.6668210863 -99.005 21.054999999999993 +4674 31968 -101.7095 13.175500000000001 206287.18660347752 1458127.5637781336 +31968 4326 206287.18660347752 1458127.5637781336 -101.7095 13.175500000000001 +4674 31968 -96.3005 28.934499999999996 763148.8586772461 3203729.441331922 +31968 4326 763148.8586772461 3203729.441331922 -96.30050000000001 28.934499999999996 +4674 31969 -93.0 13.0 499999.9999999986 1437135.722125237 +31969 4326 499999.9999999986 1437135.722125237 -93.0 13.0 +4674 31969 -95.7 1.3 199522.54838385014 143850.02066720152 +31969 4326 199522.54838385014 143850.02066720152 -95.7 1.2999999999999996 +4674 31969 -90.3 24.7 773180.3043317504 2734420.846343448 +31969 4326 773180.3043317504 2734420.846343448 -90.3 24.699999999999992 +4674 31970 -86.995 12.885 500542.45537581143 1424418.3665500516 +31970 4326 500542.45537581143 1424418.3665500516 -86.995 12.884999999999998 +4674 31970 -89.6995 1.2885 199576.8785704596 142577.43884477 +31970 4326 199576.8785704596 142577.43884477 -89.6995 1.2885000000000002 +4674 31970 -84.2905 24.481499999999997 774618.7615983661 2710229.147463714 +31970 4326 774618.7615983661 2710229.147463714 -84.2905 24.481499999999993 +4674 31971 -81.0 9.77 500000.0 1079983.0552460102 +31971 4326 500000.0 1079983.0552460102 -81.0 9.769999999999998 +4674 31971 -83.7 0.977 199489.05797611322 108108.7602408464 +31971 4326 199489.05797611322 108108.7602408464 -83.7 0.977 +4674 31971 -78.3 18.563 784992.980153172 2054614.319713186 +31971 4326 784992.980153172 2054614.319713186 -78.3 18.563 +4674 31972 -75.0 7.52 500000.0 831233.7117517306 +31972 4326 500000.0 831233.7117517306 -75.0 7.52 +4674 31972 -77.7 0.752 199471.3428514594 83211.63013978273 +31972 4326 199471.3428514594 83211.63013978273 -77.7 0.7519999999999999 +4674 31972 -72.3 14.287999999999998 791303.6548002439 1581272.9022193805 +31972 4326 791303.6548002439 1581272.9022193805 -72.3 14.287999999999997 +4674 31973 -69.0 7.82 500000.0 864398.6585437744 +31973 4326 500000.0 864398.6585437744 -69.0 7.82 +4674 31973 -71.7 0.782 199473.43849262298 86531.24619900106 +31973 4326 199473.43849262298 86531.24619900106 -71.7 0.7819999999999998 +4674 31973 -66.3 14.858 790554.872006017 1644376.6976574222 +31973 4326 790554.872006017 1644376.6976574222 -66.3 14.857999999999997 +4674 31974 -62.995000000000005 8.695 500550.0225221493 961132.5064885048 +31974 4326 500550.0225221493 961132.5064885048 -62.995 8.694999999999997 +4674 31974 -65.6995 1.4455 199596.42019634345 159950.14197036985 +31974 4326 199596.42019634345 159950.14197036985 -65.6995 1.4454999999999998 +4674 31974 -60.2905 15.9445 790065.9766263247 1764681.3962942627 +31974 4326 790065.9766263247 1764681.3962942627 -60.29050000000001 15.944499999999996 +4674 31975 -57.0 6.685 500000.0000000014 738926.9371525822 +31975 4326 500000.0000000014 738926.9371525822 -57.00000000000001 6.685 +4674 31975 -59.7 1.7305 199581.95309422095 191486.7061479114 +31975 4326 199581.95309422095 191486.7061479114 -59.7 1.7305 +4674 31975 -54.3 11.639499999999998 794405.1019975438 1288091.04001379 +31975 4326 794405.1019975438 1288091.04001379 -54.3 11.639499999999995 +4674 31976 -50.995000000000005 4.62 500554.5790987539 510659.9141430713 +31976 4326 500554.5790987539 510659.9141430713 -50.99500000000001 4.62 +4674 31976 -53.6995 0.462 199511.00863718148 51122.00329309073 +31976 4326 199511.00863718148 51122.00329309073 -53.6995 0.4620000000000001 +4674 31976 -48.2905 8.778 798097.9567758648 971384.8670678922 +31976 4326 798097.9567758648 971384.8670678922 -48.2905 8.778 +4674 31977 -81.0 -28.225 500000.0 6877872.818014253 +31977 4326 500000.0 6877872.818014253 -81.0 -28.224999999999998 +4674 31977 -83.7 -53.627500000000005 321459.56288552994 4054533.3088423153 +31977 4326 321459.56288552994 4054533.3088423153 -83.7 -53.627500000000005 +4674 31977 -78.3 -2.822500000000005 800191.6931258034 9687677.922188584 +31977 4326 800191.6931258034 9687677.922188584 -78.3 -2.822500000000003 +4674 31978 -74.995 -29.68 500483.77807328047 6716672.368894953 +31978 4326 500483.77807328047 6716672.368894953 -74.995 -29.679999999999993 +4674 31978 -77.6995 -56.391999999999996 333370.0578340662 3747020.2584179183 +31978 4326 333370.0578340662 3747020.2584179183 -77.6995 -56.39199999999999 +4674 31978 -72.2905 -2.9680000000000035 801210.2239627475 9671574.900964053 +31978 4326 801210.2239627475 9671574.900964053 -72.2905 -2.9680000000000053 +4674 31979 -69.0 -28.86 500000.0 6807525.022722052 +31979 4326 500000.0 6807525.022722052 -69.0 -28.86 +4674 31979 -71.7 -56.769 334990.21357518237 3705074.3298087036 +31979 4326 334990.21357518237 3705074.3298087036 -71.7 -56.769 +4674 31979 -66.3 -0.9510000000000005 800513.2246959123 9894768.242675146 +31979 4326 800513.2246959123 9894768.242675146 -66.3 -0.9509999999999932 +4674 31980 -63.0 -26.555 500000.0 7062851.04990622 +31980 4326 500000.0 7062851.04990622 -63.0 -26.555 +4674 31980 -65.7 -55.2065 328195.2849303348 3878903.7363721216 +31980 4326 328195.2849303348 3878903.7363721216 -65.7 -55.206499999999984 +4674 31980 -60.3 2.096499999999999 800354.2727747387 10231986.31883165 +31980 4326 800354.2727747387 10231986.31883165 -60.300000000000004 2.0964999999999945 +4674 31981 -57.0 -20.155 500000.00000000134 7771366.241216679 +31981 4326 500000.00000000134 7771366.241216679 -57.00000000000001 -20.155 +4674 31981 -59.7 -42.3535 277626.5527973975 5307442.585117925 +31981 4326 277626.5527973975 5307442.585117925 -59.7 -42.35349999999998 +4674 31981 -54.3 2.0434999999999945 800364.2628508263 10226121.612289045 +31981 4326 800364.2628508263 10226121.612289045 -54.3 2.043499999999991 +4674 31982 -50.995000000000005 -23.57 500510.2308519508 7393379.108034819 +31982 4326 500510.2308519508 7393379.108034819 -50.99500000000001 -23.569999999999997 +4674 31982 -53.6995 -51.119 311076.88975203736 4333476.022416517 +31982 4326 311076.88975203736 4333476.022416517 -53.6995 -51.11899999999999 +4674 31982 -48.2905 3.978999999999999 800889.4469805325 10440300.106225848 +31982 4326 800889.4469805325 10440300.106225848 -48.2905 3.978999999999993 +4674 31983 -45.0 -14.184999999999999 500000.0 8431813.455280587 +31983 4326 500000.0 8431813.455280587 -45.0 -14.185 +4674 31983 -47.7 -31.5685 243740.27524216188 6504228.764683966 +31983 4326 243740.27524216188 6504228.764683966 -47.7 -31.568499999999993 +4674 31983 -42.3 3.198500000000003 800088.6431564928 10353928.770834586 +31983 4326 800088.6431564928 10353928.770834586 -42.3 3.1985000000000094 +4674 31984 -39.0 -12.805000000000001 499999.9999999993 8584428.432651268 +31984 4326 499999.9999999993 8584428.432651268 -39.0 -12.804999999999994 +4674 31984 -41.7 -24.9955 227468.5600836877 7232835.855670219 +31984 4326 227468.5600836877 7232835.855670219 -41.7 -24.995500000000003 +4674 31984 -36.3 -0.6145000000000032 800537.2134307269 9932003.272460753 +31984 4326 800537.2134307269 9932003.272460753 -36.3 -0.614499999999999 +4674 31985 -32.995 -9.805 500548.3010979905 8916147.266206868 +31985 4326 500548.3010979905 8916147.266206868 -32.995 -9.805 +4674 31985 -35.6995 -22.4005 222071.05098800553 7520345.904948138 +31985 4326 222071.05098800553 7520345.904948138 -35.6995 -22.400499999999994 +4674 31985 -30.290499999999998 2.7905000000000015 801256.9164563403 10308783.523901183 +31985 4326 801256.9164563403 10308783.523901183 -30.290499999999998 2.7904999999999953 +4170 31986 -81.0 8.205 500000.0 906961.0315334216 +31986 4326 500000.0 906961.0315334216 -81.0 8.204999999999998 +4170 31986 -83.7 1.6305 199566.64955888118 180421.2604822541 +31986 4326 199566.64955888118 180421.2604822541 -83.7 1.6304999999999996 +4170 31986 -78.3 14.779499999999999 790659.6963360049 1635685.9448710496 +31986 4326 790659.6963360049 1635685.9448710496 -78.3 14.779499999999999 +4170 31987 -75.0 7.52 500000.0 831233.7117517306 +31987 4326 500000.0 831233.7117517306 -75.0 7.52 +4170 31987 -77.7 0.752 199471.3428514594 83211.63013978273 +31987 4326 199471.3428514594 83211.63013978273 -77.7 0.7519999999999999 +4170 31987 -72.3 14.287999999999998 791303.6548002439 1581272.9022193805 +31987 4326 791303.6548002439 1581272.9022193805 -72.3 14.287999999999997 +4170 31988 -69.0 7.82 500000.0 864398.6585437744 +31988 4326 500000.0 864398.6585437744 -69.0 7.82 +4170 31988 -71.7 0.782 199473.43849262298 86531.24619900106 +31988 4326 199473.43849262298 86531.24619900106 -71.7 0.7819999999999998 +4170 31988 -66.3 14.858 790554.872006017 1644376.6976574222 +31988 4326 790554.872006017 1644376.6976574222 -66.3 14.857999999999997 +4170 31989 -63.0 8.375 500000.0 925755.0626782589 +31989 4326 500000.0 925755.0626782589 -63.0 8.375 +4170 31989 -65.7 0.8375 199477.53150269657 92672.53689498059 +31989 4326 199477.53150269657 92672.53689498059 -65.7 0.8374999999999997 +4170 31989 -60.3 15.9125 789094.1621860827 1761125.1880058416 +31989 4326 789094.1621860827 1761125.1880058416 -60.300000000000004 15.912499999999996 +4170 31990 -57.0 5.35 500000.0000000014 591352.9748168344 +31990 4326 500000.0000000014 591352.9748168344 -57.00000000000001 5.349999999999999 +4170 31990 -59.7 0.535 199458.6250510133 59199.750365780295 +31990 4326 199458.6250510133 59199.750365780295 -59.7 0.5349999999999999 +4170 31990 -54.3 10.165 795860.7589116413 1124886.51870193 +31990 4326 795860.7589116413 1124886.51870193 -54.3 10.164999999999996 +4170 31991 -51.0 4.62 500000.0000000007 510659.912193992 +31991 4326 500000.0000000007 510659.912193992 -51.0 4.62 +4170 31991 -53.7 0.462 199455.31072332925 51122.02447097645 +31991 4326 199455.31072332925 51122.02447097645 -53.7 0.46199999999999997 +4170 31991 -48.3 8.778 797052.0266858254 971377.3264165997 +31991 4326 797052.0266858254 971377.3264165997 -48.3 8.778000000000002 +4170 31992 -79.60499999999999 -27.5 637795.6601017192 6957408.510758631 +31992 4326 637795.6601017192 6957408.510758631 -79.605 -27.499999999999993 +4170 31992 -83.5605 -53.555 330392.0782296129 4062937.902997135 +31992 4326 330392.0782296129 4062937.902997135 -83.5605 -53.555 +4170 31992 -75.64949999999999 -1.4450000000000003 1096060.3256099285 9839580.356331306 +31992 4326 1096060.3256099285 9839580.356331306 -75.6495 -1.4450000000000078 +4170 31993 -74.995 -29.68 500483.77807328047 6716672.368894953 +31993 4326 500483.77807328047 6716672.368894953 -74.995 -29.679999999999993 +4170 31993 -77.6995 -56.391999999999996 333370.0578340662 3747020.2584179183 +31993 4326 333370.0578340662 3747020.2584179183 -77.6995 -56.39199999999999 +4170 31993 -72.2905 -2.9680000000000035 801210.2239627475 9671574.900964053 +31993 4326 801210.2239627475 9671574.900964053 -72.2905 -2.9680000000000053 +4170 31994 -69.0 -29.935 500000.0 6688417.138540288 +31994 4326 500000.0 6688417.138540288 -69.0 -29.935000000000002 +4170 31994 -71.7 -56.8765 335462.31669896457 3693113.6011662968 +31994 4326 335462.31669896457 3693113.6011662968 -71.7 -56.8765 +4170 31994 -66.3 -2.9935000000000045 800146.4215879131 9668755.757004535 +31994 4326 800146.4215879131 9668755.757004535 -66.3 -2.9935 +4170 31995 -63.0 -29.195 500000.0 6770409.741014987 +31995 4326 500000.0 6770409.741014987 -63.0 -29.194999999999997 +4170 31995 -65.7 -55.4705 329334.4432651842 3849535.952061505 +31995 4326 329334.4432651842 3849535.952061505 -65.7 -55.47050000000001 +4170 31995 -60.3 -2.9194999999999993 800166.3392556285 9676944.30181177 +31995 4326 800166.3392556285 9676944.30181177 -60.300000000000004 -2.919499999999991 +4170 31996 -57.0 -22.41 500000.0000000013 7521790.651019167 +31996 4326 500000.0000000013 7521790.651019167 -57.00000000000001 -22.409999999999993 +4170 31996 -59.7 -42.579 278423.8671187768 5282401.03918215 +31996 4326 278423.8671187768 5282401.03918215 -59.7 -42.57899999999999 +4170 31996 -54.3 -2.2409999999999997 800325.7369108368 9752024.04077633 +31996 4326 800325.7369108368 9752024.04077633 -54.3 -2.240999999999997 +4170 31997 -51.0 -19.975 500000.00000000064 7791285.188660946 +31997 4326 500000.00000000064 7791285.188660946 -51.0 -19.97499999999999 +4170 31997 -53.7 -37.9525 262772.80695615336 5796016.134995383 +31997 4326 262772.80695615336 5796016.134995383 -53.7 -37.952499999999986 +4170 31997 -48.3 -1.9975000000000023 800372.726224724 9778968.507922292 +31997 4326 800372.726224724 9778968.507922292 -48.3 -1.9974999999999967 +4170 31998 -45.0 -16.75 500000.0 8148100.874165683 +31998 4326 500000.0 8148100.874165683 -45.0 -16.75 +4170 31998 -47.7 -31.825 244445.07467460074 6475784.788522222 +31998 4326 244445.07467460074 6475784.788522222 -47.7 -31.825 +4170 31998 -42.3 -1.6750000000000007 800426.652791603 9814654.617286293 +31998 4326 800426.652791603 9814654.617286293 -42.3 -1.6750000000000058 +4170 31999 -39.0 -13.175 499999.9999999993 8543511.556256955 +31999 4326 499999.9999999993 8543511.556256955 -39.0 -13.174999999999997 +4170 31999 -41.7 -25.032500000000002 227550.31560895586 7228735.946964027 +31999 4326 227550.31560895586 7228735.946964027 -41.7 -25.032500000000002 +4170 31999 -36.3 -1.3175000000000026 800475.3658221702 9854213.53145729 +31999 4326 800475.3658221702 9854213.53145729 -36.3 -1.3175000000000108 +4170 32000 -33.0 -10.055 500000.0000000007 8888506.504981807 +32000 4326 500000.0000000007 8888506.504981807 -33.0 -10.054999999999998 +4170 32000 -35.7 -19.104499999999998 215919.97425537097 7885417.920531512 +32000 4326 215919.97425537097 7885417.920531512 -35.7 -19.104499999999994 +4170 32000 -30.3 -1.0055000000000014 800508.3691406671 9888737.601548018 +32000 4326 800508.3691406671 9888737.601548018 -30.299999999999997 -1.0055000000000018 +4267 32001 -110.055 48.209999999999994 1864670.5200720164 441887.9504901697 +32001 4326 1864670.5200720164 441887.9504901697 -110.05583474199835 48.20990489443599 +4267 32001 -115.46849999999999 47.489999999999995 525710.5423358273 236098.44469056078 +32001 4326 525710.5423358273 236098.44469056078 -115.46950314706137 47.48986011870104 +4267 32001 -104.64150000000001 48.93 3167285.47972944 741030.9014082048 +32001 4326 3167285.47972944 741030.9014082048 -104.64215412954363 48.92994279621269 +4267 32002 -110.05000000000001 47.215 1863319.1341324707 504380.2384335588 +32002 4326 1863319.1341324707 504380.2384335588 -110.05081883293009 47.21490294650349 +4267 32002 -115.459 46.2745 494412.1636752262 218343.1612772662 +32002 4326 494412.1636752262 218343.1612772662 -115.45998044168917 46.274360615625085 +4267 32002 -104.641 48.155499999999996 3185422.847170158 883835.9550126485 +32002 4326 3185422.847170158 883835.9550126485 -104.64164420721154 48.15543957113088 +4267 32003 -109.305 45.61 2049902.1347659854 587077.146795987 +32003 4326 2049902.1347659854 587077.146795987 -109.30577065168067 45.60990898720716 +4267 32003 -114.0435 44.476 814475.2918208161 207176.2884032221 +32003 4326 814475.2918208161 207176.2884032221 -114.04440605134138 44.4758781447775 +4267 32003 -104.5665 46.744 3236288.190698835 1038613.1347650688 +32003 4326 3236288.190698835 1038613.1347650688 -104.56712461442253 46.74393712832114 +4267 32005 -100.065 42.345 1982427.6239027553 368660.8750633061 +32005 4326 1982427.6239027553 368660.8750633061 -100.06543281547732 42.34497331536212 +4267 32005 -103.6605 41.7465 1001329.3355149337 172045.63886974126 +32005 4326 1001329.3355149337 172045.63886974126 -103.6610449066132 41.74646301400893 +4267 32005 -96.4695 42.9435 2945127.789585458 606386.5229700081 +32005 4326 2945127.789585458 606386.5229700081 -96.46981690505099 42.943480921820715 +4267 32006 -99.68 41.0 1950316.5362931294 485791.5468386478 +32006 4326 1950316.5362931294 485791.5468386478 -99.68041149708914 40.999986786743925 +4267 32006 -103.622 40.091 847045.8852854476 181805.04238237234 +32006 4326 847045.8852854476 181805.04238237234 -103.62253027570361 40.09098097690236 +4267 32006 -95.738 41.909 3023801.0232301904 838998.0403485283 +32006 4326 3023801.0232301904 838998.0403485283 -95.73828753997624 41.908990366397425 +4267 32007 -115.52000000000001 38.495000000000005 518125.6570534992 1363302.9699056477 +32007 4326 518125.6570534992 1363302.9699056477 -115.52086797437688 38.49493246517393 +4267 32007 -116.861 35.3405 118947.82492720026 217360.8296988492 +32007 4326 118947.82492720026 217360.8296988492 -116.86186876112238 35.34048596835714 +4267 32007 -114.179 41.6495 883789.5078505643 2515443.1509580202 +32007 4326 883789.5078505643 2515443.1509580202 -114.179869382731 41.64939905321577 +4267 32008 -116.59 38.5 521940.06877368054 1365126.6119870676 +32008 4326 521940.06877368054 1365126.6119870676 -116.59089786134706 38.499924313599415 +4267 32008 -118.03 36.25 98005.3941922101 548771.0234019408 +32008 4326 98005.3941922101 548771.0234019408 -118.0309099120402 36.24995694815231 +4267 32008 -115.14999999999999 40.75 920205.2673386262 2188242.084971963 +32008 4326 920205.2673386262 2188242.084971963 -115.15088588596122 40.749902034687935 +4267 32009 -118.495 39.475 524934.3687898308 1720204.158378115 +32009 4326 524934.3687898308 1720204.158378115 -118.49596330758457 39.47489319710063 +4267 32009 -119.8495 37.2025 131240.80942196288 895151.9926004364 +32009 4326 131240.80942196288 895151.9926004364 -119.8504696064431 37.202422655668215 +4267 32009 -117.14049999999999 41.7475 893713.1270646274 2551324.748918339 +32009 4326 893713.1270646274 2551324.748918339 -117.1414576018926 41.747374458487776 +4267 32010 -71.595 44.0 518858.5083798648 546722.8495401805 +32010 4326 518858.5083798648 546722.8495401805 -71.59446479806647 43.99996470296838 +4267 32010 -72.4635 42.821 286212.1234998525 117995.20400889394 +32010 4326 286212.1234998525 117995.20400889394 -72.46300374805114 42.820975387201344 +4267 32010 -70.7265 45.179 742446.0416197646 977945.6170053146 +32010 4326 742446.0416197646 977945.6170053146 -70.72592416689056 45.178956358736116 +4267 32011 -74.74 40.114999999999995 1979489.1911486348 466839.03843881155 +32011 4326 1979489.1911486348 466839.03843881155 -74.73959655101744 40.11500974112316 +4267 32011 -75.514 38.994499999999995 1759161.4686444215 59817.79315682943 +32011 4326 1759161.4686444215 59817.79315682943 -75.51362742336893 38.99452688645663 +4267 32011 -73.966 41.2355 2192718.484406689 875820.8431118523 +32011 4326 2192718.484406689 875820.8431118523 -73.96556456554514 41.23549472282579 +4267 32012 -104.35499999999999 34.5 493472.02489275596 1273302.5281893616 +32012 4326 493472.02489275596 1273302.5281893616 -104.3555136241986 34.50007664900025 +4267 32012 -105.5835 32.25 113496.85720220875 456918.5351709618 +32012 4326 113496.85720220875 456918.5351709618 -105.58403504473468 32.25012765795471 +4267 32012 -103.1265 36.75 853568.8954106788 2094468.8320428026 +32012 4326 853568.8954106788 2094468.8320428026 -103.12699159347864 36.750034989522625 +4267 32013 -106.28 34.39 490949.4926208654 1233262.847403881 +32013 4326 490949.4926208654 1233262.847403881 -106.28056820368393 34.39007067458864 +4267 32013 -107.58500000000001 32.041000000000004 86328.62746645906 381195.0343952112 +32013 4326 86328.62746645906 381195.0343952112 -107.58558933835054 32.041124476135224 +4267 32013 -104.975 36.739 873590.6958344746 2090703.9285698913 +32013 4326 873590.6958344746 2090703.9285698913 -104.97554653522077 36.73902713229439 +4267 32014 -107.69 34.165 543357.4072531777 1151436.4510010325 +32014 4326 543357.4072531777 1151436.4510010325 -107.69060665231079 34.1650692275249 +4267 32014 -108.923 31.6135 160788.07556425547 224832.55725569683 +32014 4326 160788.07556425547 224832.55725569683 -108.92362317304807 31.61363010475157 +4267 32014 -106.457 36.716499999999996 903408.6090183337 2082958.1622984158 +32014 4326 903408.6090183337 2082958.1622984158 -106.45759005964898 36.71652048245189 +4267 32015 -74.55000000000001 42.95 441990.1413324854 1074942.8097438093 +32015 4326 441990.1413324854 1074942.8097438093 -74.54957218339203 42.94998295050467 +4267 32015 -75.738 41.087 112772.30186057351 399116.39188605704 +32015 4326 112772.30186057351 399116.39188605704 -75.73762316032231 41.08700244400452 +4267 32015 -73.36200000000001 44.813 752082.8176573683 1755469.8796006208 +32015 4326 752082.8176573683 1755469.8796006208 -73.36151775529727 44.81296952754709 +4267 32016 -76.405 43.2 547551.499811404 1166001.001339487 +32016 4326 547551.499811404 1166001.001339487 -76.40463293619212 43.199987857466006 +4267 32016 -77.6155 42.111000000000004 219933.26072620318 770780.6339783253 +32016 4326 219933.26072620318 770780.6339783253 -77.61517956996038 42.11099897354036 +4267 32016 -75.1945 44.288999999999994 863672.1331823185 1565966.5160139431 +32016 4326 863672.1331823185 1565966.5160139431 -75.19408464447739 44.28897847309334 +4267 32017 -78.565 42.815 504919.0953821748 1025637.5837407642 +32017 4326 504919.0953821748 1025637.5837407642 -78.56470801660136 42.81499644761988 +4267 32017 -79.64949999999999 42.072500000000005 210532.62478889717 756864.6237369772 +32017 4326 210532.62478889717 756864.6237369772 -79.64924772541127 42.07250435321437 +4267 32017 -77.4805 43.5575 792342.1719461564 1298188.7056060615 +32017 4326 792342.1719461564 1298188.7056060615 -77.48016740212958 43.557489194410664 +4267 32019 -79.85499999999999 35.21 1744612.055318521 532441.0418790485 +32019 4326 1744612.055318521 532441.0418790485 -79.85477697261392 35.21010383238844 +4267 32019 -83.8825 33.967999999999996 519948.2198293205 115747.8436314722 +32019 4326 519948.2198293205 115747.8436314722 -83.88240157403843 33.968136307505794 +4267 32019 -75.8275 36.452000000000005 2933028.7137133237 998336.7062121461 +32019 4326 2933028.7137133237 998336.7062121461 -75.82714952922558 36.45206961986075 +4267 32020 -100.44999999999999 48.08 2012222.3294780627 393949.869491908 +32020 4326 2012222.3294780627 393949.869491908 -100.45049263906756 48.07996004351021 +4267 32020 -103.708 47.242999999999995 1203325.4483799331 105237.2407977546 +32020 4326 1203325.4483799331 105237.2407977546 -103.70860032840086 47.242942301621156 +4267 32020 -97.192 48.917 2795265.8452188796 716384.3561179865 +32020 4326 2795265.8452188796 716384.3561179865 -97.19237988687033 48.9169761686989 +4267 32021 -100.3 46.879999999999995 2050015.3265964843 442548.337945216 +32021 4326 2050015.3265964843 442548.337945216 -100.30047622530476 46.87995778618816 +4267 32021 -103.675 46.025 1193622.848843599 146978.13450135838 +32021 4326 1193622.848843599 146978.13450135838 -103.67558587373878 46.024941871305835 +4267 32021 -96.925 47.735 2879528.615468742 774357.8553850587 +32021 4326 2879528.615468742 774357.8553850587 -96.9253614663904 47.734971996005186 +4267 32022 -82.66 41.215 1955979.8316594362 564126.482988915 +32022 4326 1955979.8316594362 564126.482988915 -82.6598508635414 41.21501702904569 +4267 32022 -84.595 40.2115 1414884.98335406 205524.29181778806 +32022 4326 1414884.98335406 205524.29181778806 -84.59491651753987 40.21152940107119 +4267 32022 -80.72500000000001 42.2185 2480873.663651233 934650.5780533605 +32022 4326 2480873.663651233 934650.5780533605 -80.72478329755828 42.218505424878295 +4267 32023 -82.765 39.379999999999995 1925092.6828174882 502717.59065397416 +32023 4326 1925092.6828174882 502717.59065397416 -82.76485824169269 39.380037726132535 +4267 32023 -84.62349999999999 38.498 1392253.1009479258 188526.64368850435 +32023 4326 1392253.1009479258 188526.64368850435 -84.62341944489378 38.498051651071435 +4267 32023 -80.90650000000001 40.262 2444741.194048858 827808.4983767316 +32023 4326 2444741.194048858 827808.4983767316 -80.90629559518028 40.262024240301955 +4267 32024 -98.71000000000001 36.14 1790353.5971294064 415735.86661232903 +32024 4326 1790353.5971294064 415735.86661232903 -98.7103553822795 36.140062268652095 +4267 32024 -102.571 35.357 637363.1007289118 162035.9449996157 +32024 4326 637363.1007289118 162035.9449996157 -102.57146666024352 35.357064956907045 +4267 32024 -94.849 36.923 2921011.7473030384 714969.0794389698 +32024 4326 2921011.7473030384 714969.0794389698 -94.84924030817606 36.923057374016096 +4267 32025 -97.21000000000001 34.595 2237753.6607808075 460063.9147428695 +32025 4326 2237753.6607808075 460063.9147428695 -97.2103040523962 34.59509846743288 +4267 32025 -99.721 33.7175 1476633.8222764558 144266.82831056413 +32025 4326 1476633.8222764558 144266.82831056413 -99.7213747202285 33.717611709663935 +4267 32025 -94.699 35.4725 2982839.7755998364 794581.4242187104 +32025 4326 2982839.7755998364 794581.4242187104 -94.6992313645078 35.47258534515953 +4267 32026 -120.32 45.105000000000004 2046474.3407868044 524420.4413979528 +32026 4326 2046474.3407868044 524420.4413979528 -120.32110763120649 45.10481976621942 +4267 32026 -123.785 44.0655 1136671.7836311928 162962.32743899003 +32026 4326 1136671.7836311928 162962.32743899003 -123.78618656808194 44.065291184469075 +4267 32026 -116.855 46.1445 2923692.5622485527 924232.1392545123 +32026 4326 2923692.5622485527 924232.1392545123 -116.85602202424937 46.144348620552165 +4267 32027 -120.75 43.269999999999996 1933418.1296678674 584415.6417076546 +32027 4326 1933418.1296678674 584415.6417076546 -120.75108607511146 43.26982762603916 +4267 32027 -124.21499999999999 42.108999999999995 992153.7900002995 183567.21219381277 +32027 4326 992153.7900002995 183567.21219381277 -124.21616094759682 42.10880511083182 +4267 32027 -117.28500000000001 44.431000000000004 2839791.201966973 1023646.6955746405 +32027 4326 2839791.201966973 1023646.6955746405 -117.28600470925988 44.43085125844271 +4267 32028 -77.61500000000001 41.565 2036944.0152873856 509504.56374735123 +32028 4326 2036944.0152873856 509504.56374735123 -77.61468226553765 41.56500357668058 +4267 32028 -80.2385 40.6965 1310001.463144304 202956.08884614514 +32028 4326 1310001.463144304 202956.08884614514 -80.2382723198364 40.69651824376067 +4267 32028 -74.9915 42.4335 2744683.509897496 837829.6754594165 +32028 4326 2744683.509897496 837829.6754594165 -74.99109036734832 42.43348815014021 +4267 32030 -71.465 41.575 509576.9273737011 179146.82325433445 +32030 4326 509576.9273737011 179146.82325433445 -71.46448109442778 41.57498108900863 +4267 32030 -71.8115 41.1745 414240.88906505686 33370.01841702536 +32030 4326 414240.88906505686 33370.01841702536 -71.81099539321794 41.174486537129305 +4267 32030 -71.1185 41.975500000000004 603741.2196321838 325314.5322851403 +32030 4326 603741.2196321838 325314.5322851403 -71.117966611337 41.97547589531769 +4267 32031 -80.94 34.335 2018113.6687560694 485815.4815269873 +32031 4326 2018113.6687560694 485815.4815269873 -80.939812053631 34.33512456545067 +4267 32031 -83.118 33.5475 1354654.3914245523 205974.94225021417 +32031 4326 1354654.3914245523 205974.94225021417 -83.11787908359985 33.54764542957938 +4267 32031 -78.762 35.1225 2669266.168435819 779792.8120587156 +32031 4326 2669266.168435819 779792.8120587156 -78.76174399722662 35.122603503744294 +4267 32033 -80.49000000000001 33.0 2156360.5356232366 424828.81535162637 +32033 4326 2156360.5356232366 424828.81535162637 -80.48980155579186 33.00015530718346 +4267 32033 -81.876 32.144999999999996 1728839.610089446 114519.83911499278 +32033 4326 1728839.610089446 114519.83911499278 -81.87584428337459 32.145179016023505 +4267 32033 -79.104 33.855000000000004 2575657.307004667 740739.6538428996 +32033 4326 2575657.307004667 740739.6538428996 -79.10375806850486 33.85513231151533 +4267 32034 -100.25999999999999 45.045 1932797.2247287782 441846.1132462031 +32034 4326 1932797.2247287782 441846.1132462031 -100.26045938363234 45.044958993242396 +4267 32034 -103.689 44.2305 1033298.0213694256 166826.06237904867 +32034 4326 1033298.0213694256 166826.06237904867 -103.68956825264262 44.23044624789986 +4267 32034 -96.831 45.859500000000004 2807268.325931732 754524.1696078028 +32034 4326 2807268.325931732 754524.1696078028 -96.831345806089 45.85946989797489 +4267 32035 -100.245 43.635 2023384.8233698376 474427.44253184897 +32035 4326 2023384.8233698376 474427.44253184897 -100.24544800092126 43.634964356666316 +4267 32035 -103.6785 42.595499999999994 1099405.3414363293 113692.64389609508 +32035 4326 1099405.3414363293 113692.64389609508 -103.67905282682302 42.59545588121715 +4267 32035 -96.81150000000001 44.6745 2916005.996737872 872772.1531742834 +32035 4326 2916005.996737872 872772.1531742834 -96.81183801140621 44.67447200617376 +4267 32037 -101.50999999999999 35.4 1997019.8309660032 509536.28290914284 +32037 4326 1997019.8309660032 509536.28290914284 -101.5104355495364 35.400068011517796 +4267 32037 -102.878 34.41 1584324.1051209068 152124.11096758945 +32037 4326 1584324.1051209068 152124.11096758945 -102.8784702738538 34.41008465489224 +4267 32037 -100.142 36.39 2399744.4165567313 872648.5630418909 +32037 4326 2399744.4165567313 872648.5630418909 -100.142399846212 36.39005293730209 +4267 32038 -98.535 33.15 1683240.5211685465 541196.510344037 +32038 4326 1683240.5211685465 541196.510344037 -98.53533774010694 33.150128925289835 +4267 32038 -102.61649999999999 31.863 411905.7623039357 110110.66500685691 +32038 4326 411905.7623039357 110110.66500685691 -102.61694948825678 31.86314970384457 +4267 32038 -94.4535 34.437 2918679.874520432 1021279.9590937601 +32038 4326 2918679.874520432 1021279.9590937601 -94.45372111368656 34.43710790080937 +4267 32039 -100.08 31.025000000000002 2079349.3229783177 494094.2130083614 +32039 4326 2079349.3229783177 494094.2130083614 -100.08037395999028 31.025181282328056 +4267 32039 -106.002 29.904500000000002 204464.5896619115 132259.03508848563 +32039 4326 204464.5896619115 132259.03508848563 -106.002533473193 29.904694016394707 +4267 32039 -94.158 32.145500000000006 3910638.2373687434 954628.6050012732 +32039 4326 3910638.2373687434 954628.6050012732 -94.15820677314026 32.14566405754252 +4267 32040 -99.205 29.225 1934615.423902214 506052.2806329436 +32040 4326 1934615.423902214 506052.2806329436 -99.20534282666321 29.225237657057978 +4267 32040 -104.4205 27.924500000000002 249846.3900490573 73716.292953192 +32040 4326 249846.3900490573 73716.292953192 -104.42098100127704 27.92476431465002 +4267 32040 -93.98949999999999 30.5255 3577351.840124087 1012706.3861068183 +32040 4326 3577351.840124087 1012706.3861068183 -93.98969841840875 30.5257090942363 +4267 32041 -97.78 27.02 2234396.838815423 492568.8100047064 +32041 4326 2234396.838815423 492568.8100047064 -97.78029676749014 27.020313918553626 +4267 32041 -99.958 25.948999999999998 1520857.1737897706 105393.72717768773 +32041 4326 1520857.1737897706 105393.72717768773 -99.95835318239969 25.949347060847284 +4267 32041 -95.602 28.091 2934429.731562592 891966.167535731 +32041 4326 2934429.731562592 891966.167535731 -95.6022389439979 28.091281888385755 +4267 32042 -111.54 41.28 1989005.6928403375 344902.05007470574 +32042 4326 1989005.6928403375 344902.05007470574 -111.54078562817298 41.27992201393563 +4267 32042 -113.79 40.623 1364341.4298439168 113909.88059081766 +32042 4326 1364341.4298439168 113909.88059081766 -113.79084453871499 40.62291381728035 +4267 32042 -109.29 41.937 2601282.2497070525 591941.0553823746 +32042 4326 2601282.2497070525 591941.0553823746 -109.29072440508426 41.93693012830476 +4267 32043 -111.545 39.785 1987354.1583555234 528737.6240493723 +32043 4326 1987354.1583555234 528737.6240493723 -111.54576850690916 39.7849405206138 +4267 32043 -113.7995 38.6195 1342956.4577943392 112681.99772822736 +32043 4326 1342956.4577943392 112681.99772822736 -113.80032080185137 38.61944279753689 +4267 32043 -109.29050000000001 40.9505 2610360.5615361836 960851.6427414921 +32043 4326 2610360.5615361836 960851.6427414921 -109.29121353123554 40.95044031250119 +4267 32044 -111.545 37.785 1986995.1787911875 407199.3038244095 +32044 4326 1986995.1787911875 407199.3038244095 -111.54574733169973 37.78497210205472 +4267 32044 -113.7995 37.069500000000005 1329115.1691293302 154925.05207329945 +32044 4326 1329115.1691293302 154925.05207329945 -113.80030379976522 37.0694705825191 +4267 32044 -109.29050000000001 38.500499999999995 2632321.823485634 675218.541900013 +32044 4326 2632321.823485634 675218.541900013 -109.29118871390274 38.50047381445459 +4267 32045 -72.47 43.875 507910.7947380462 501149.78398461605 +32045 4326 507910.7947380462 501149.78398461605 -72.4694953051441 43.874969272697236 +4267 32045 -73.343 42.835499999999996 273879.08288572583 123399.95640045762 +32045 4326 273879.08288572583 123399.95640045762 -73.34253278608335 42.83547898870323 +4267 32045 -71.597 44.914500000000004 733936.7687356186 881399.364889602 +32045 4326 733936.7687356186 881399.364889602 -71.59645644707686 44.91446131118135 +4267 32046 -78.285 38.615 2061432.549211124 345416.806506069 +32046 4326 2061432.549211124 345416.806506069 -78.2847169286847 38.615040093288215 +4267 32046 -79.88250000000001 37.8545 1600818.5323073315 71407.13952303032 +32046 4326 1600818.5323073315 71407.13952303032 -79.88227011351636 37.854555198894424 +4267 32046 -76.6875 39.3755 2512389.334470524 627383.0835327574 +32046 4326 2512389.334470524 627383.0835327574 -76.68716278058527 39.37552526485316 +4267 32047 -79.495 37.41 1711009.2380672223 393517.5344221255 +32047 4326 1711009.2380672223 393517.5344221255 -79.49475934383202 37.41006170641189 +4267 32047 -83.26150000000001 36.627 603243.9624287277 142150.92060095278 +32047 4326 603243.9624287277 142150.92060095278 -83.26137892577839 36.6270811153222 +4267 32047 -75.7285 38.193 2796492.7102027824 688804.0824503205 +32047 4326 2796492.7102027824 688804.0824503205 -75.72813825156514 38.19303941727188 +4267 32048 -120.905 48.065 1982476.1173877153 388487.1020750288 +32048 4326 1982476.1173877153 388487.1020750288 -120.90618780653902 48.06480881438371 +4267 32048 -124.4015 47.1785 1112839.0357392477 85682.9747494934 +32048 4326 1112839.0357392477 85682.9747494934 -124.40277217475216 47.17827128546782 +4267 32048 -117.4085 48.951499999999996 2822773.311050972 730202.9606545273 +32048 4326 2822773.311050972 730202.9606545273 -117.40959628145258 48.95134561678919 +4267 32049 -120.655 46.75 1961145.2635408945 516645.16802484554 +32049 4326 1961145.2635408945 516645.16802484554 -120.65615103890548 46.74981148563542 +4267 32049 -124.02550000000001 45.661 1098792.6543071785 139633.40274167302 +32049 4326 1098792.6543071785 139633.40274167302 -124.02672666965141 45.66077944472575 +4267 32049 -117.2845 47.839 2789636.591485752 929920.38267076 +32049 4326 2789636.591485752 929920.38267076 -117.2855686613011 47.83884410335262 +4267 32050 -79.74000000000001 39.7 1932469.81659535 437157.4847733441 +32050 4326 1932469.81659535 437157.4847733441 -79.73975950368688 39.700028686236756 +4267 32050 -81.558 38.854 1413920.5391737907 135648.65024664646 +32050 4326 1413920.5391737907 135648.65024664646 -81.55782056055807 38.85404315477896 +4267 32050 -77.922 40.546 2438575.660879058 749102.5581372905 +32050 4326 2438575.660879058 749102.5581372905 -77.92169712914472 40.546014534797216 +4267 32051 -80.85 38.185 2043113.5970229388 431524.4823781891 +32051 4326 2043113.5970229388 431524.4823781891 -80.84979973985105 38.1850518056021 +4267 32051 -82.47 37.298500000000004 1572384.7429075297 112084.64109635363 +32051 4326 1572384.7429075297 112084.64109635363 -82.4698529668769 37.29856846085963 +4267 32051 -79.23 39.0715 2502544.5742530418 759134.0934184206 +32051 4326 2502544.5742530418 759134.0934184206 -79.22974532022236 39.071535815097455 +4267 32052 -90.47 46.34 1881289.013123567 428203.2604074787 +32052 4326 1881289.013123567 428203.2604074787 -90.47012095811564 46.33999564936547 +4267 32052 -92.648 45.467 1320686.112955023 120839.30020006215 +32052 4326 1320686.112955023 120839.30020006215 -92.64819671094546 45.466992871913945 +4267 32052 -88.292 47.213 2424523.594074907 750818.3885445546 +32052 4326 2424523.594074907 750818.3885445546 -88.29204260592167 47.212998480215404 +4267 32053 -89.57 44.89 2111442.615023922 385512.02397195646 +32053 4326 2111442.615023922 385512.02397195646 -89.570086066457 44.88999766004054 +4267 32053 -92.558 44.071 1327748.8108524734 97232.78964396866 +32053 4326 1327748.8108524734 97232.78964396866 -92.55818890195982 44.07099656860653 +4267 32053 -86.582 45.708999999999996 2873016.2553167352 702210.7907260197 +32053 4326 2873016.2553167352 702210.7907260197 -86.58198005048543 45.70899736484843 +4267 32054 -89.19 43.405 2215252.659686536 513118.1058751771 +32054 4326 2215252.659686536 513118.1058751771 -89.19007082870307 43.40500339808473 +4267 32054 -91.206 42.5725 1675130.8531658652 211009.2457707791 +32054 4326 1675130.8531658652 211009.2457707791 -91.2061384321994 42.57250651862886 +4267 32054 -87.174 44.2375 2740578.4548910395 828080.8190731062 +32054 4326 2740578.4548910395 828080.8190731062 -87.17400127419995 44.237500443112076 +4267 32055 -105.19 43.0 493758.0035077193 850211.0027177983 +32055 4326 493758.0035077193 850211.0027177983 -105.19060560972993 42.99994534752648 +4267 32055 -106.216 41.191 211193.60005616225 192765.66172621257 +32055 4326 211193.60005616225 192765.66172621257 -106.2166208490757 41.19095550309869 +4267 32055 -104.164 44.809 760225.8823158107 1511212.5833863672 +32055 4326 760225.8823158107 1511212.5833863672 -104.16458987457936 44.80894169919289 +4267 32056 -107.315 43.0 504904.42581163015 850210.6710248637 +32056 4326 504904.42581163015 850210.6710248637 -107.31567401722621 42.999933467239494 +4267 32056 -108.49849999999999 41.191 179312.40746955498 193171.4644750599 +32056 4326 179312.40746955498 193171.4644750599 -108.49919177610904 41.1909424940981 +4267 32056 -106.1315 44.809 811916.4201716076 1511913.4820623735 +32056 4326 811916.4201716076 1511913.4820623735 -106.13215558462552 44.80893096968287 +4267 32057 -109.28 43.0 358217.36356178706 850657.3711903121 +32057 4326 358217.36356178706 850657.3711903121 -109.28073645108452 42.99992135838405 +4267 32057 -110.882 41.191 -86800.88528147827 198215.6383225941 +32057 4326 -86800.88528147827 198215.6383225941 -110.88276466814821 41.19092742086576 +4267 32057 -107.678 44.809 778220.242094325 1511442.2047367552 +32057 4326 778220.242094325 1511442.2047367552 -107.67870669544052 44.8089217453386 +4267 32058 -110.05000000000001 42.83 508941.68093448697 788256.1729961773 +32058 4326 508941.68093448697 788256.1729961773 -110.05075859608209 42.829917601034346 +4267 32058 -110.959 41.174 258929.82521306415 186043.0324146703 +32058 4326 258929.82521306415 186043.0324146703 -110.95976680324735 41.1739270967222 +4267 32058 -109.141 44.486000000000004 745928.4214308021 1393272.9237386405 +32058 4326 745928.4214308021 1393272.9237386405 -109.14175039738367 44.48591367080006 +4267 32064 -98.935 37.42 1659286.7788566132 13586796.05312704 +32064 4326 1659286.7788566132 13586796.05312704 -98.93536825731076 37.42003823743488 +4267 32064 -101.6935 26.988999999999997 763366.6472703498 9802810.687599646 +32064 4326 763366.6472703498 9802810.687599646 -101.6939035663001 26.98930526815986 +4267 32064 -96.1765 47.851 2333408.6729749623 17399509.125502396 +32064 4326 2333408.6729749623 17399509.125502396 -96.17683485787126 47.85097464241946 +4267 32065 -92.935 37.495000000000005 1659267.944580693 13614094.068134978 +32065 4326 1659267.944580693 13614094.068134978 -92.93518299053281 37.4950635958479 +4267 32065 -95.7025 26.7985 758955.8559211091 9733607.086463828 +32065 4326 758955.8559211091 9733607.086463828 -95.70273995228072 26.798837672671596 +4267 32065 -90.1675 48.191500000000005 2331051.174324301 17523738.021547504 +32065 4326 2331051.174324301 17523738.021547504 -90.16761389247321 48.19150039873837 +4267 32066 -86.96000000000001 36.135 1652224.5083187993 13119140.377629813 +32066 4326 1652224.5083187993 13119140.377629813 -86.95999448325973 36.13509220043547 +4267 32066 -89.705 25.168499999999998 745873.7506593317 9141010.725121608 +32066 4326 745873.7506593317 9141010.725121608 -89.70507118375004 25.168902600833732 +4267 32066 -84.215 47.1015 2333756.626361367 17125913.610384777 +32066 4326 2333756.626361367 17125913.610384777 -84.21489239986653 47.101497075060394 +4267 32067 -81.03999999999999 34.97 1628436.765006431 12695248.37440566 +32067 4326 1628436.765006431 12695248.37440566 -81.03981365906793 34.97011087964842 +4267 32067 -83.785 24.926 717588.9019737849 9053390.618159503 +32067 4326 717588.9019737849 9053390.618159503 -83.78490725067313 24.926411163910043 +4267 32067 -78.295 45.014 2339725.683276681 16364299.44223834 +32067 4326 2339725.683276681 16364299.44223834 -78.29468759514417 45.01398619761803 +4267 32081 -53.519999999999996 48.225 266166.5529196095 5342823.315588692 +32081 4326 266166.5529196095 5342823.315588692 -53.51895777712871 48.22496204372902 +4267 32081 -54.402 46.7265 197634.65145597016 5177061.083726151 +32081 4326 197634.65145597016 5177061.083726151 -54.40101565360793 46.726470849016486 +4267 32081 -52.638 49.7235 330900.620728499 5509385.804560142 +32081 4326 330900.620728499 5509385.804560142 -52.63689593328827 49.72345703892523 +4267 32082 -55.995000000000005 50.760000000000005 305152.77245757374 5624605.380154886 +32082 4326 305152.77245757374 5624605.380154886 -55.99399028233597 50.75999252106262 +4267 32082 -57.3495 47.205000000000005 202563.44038219145 5230179.309776506 +32082 4326 202563.44038219145 5230179.309776506 -57.3486051390037 47.20499436705709 +4267 32082 -54.6405 54.315 393267.51423088566 6021012.318781224 +32082 4326 393267.51423088566 6021012.318781224 -54.63935292703323 54.31501318600588 +4267 32083 -58.489999999999995 49.019999999999996 305531.3752689565 5431089.617376024 +32083 4326 305531.3752689565 5431089.617376024 -58.489113132377696 49.02000516774242 +4267 32083 -59.381 47.652 238618.92378029996 5279363.777699887 +32083 4326 238618.92378029996 5279363.777699887 -59.38016726533395 47.65200958746481 +4267 32083 -57.599 50.388 368871.7162958273 5583616.183147415 +32083 4326 368871.7162958273 5583616.183147415 -57.598055644171374 50.38800384995018 +4267 32084 -61.5 55.46 304800.0 6147611.463061122 +32084 4326 304800.0 6147611.463061122 -61.4990987700461 55.46008712869217 +4267 32084 -62.85 52.346000000000004 212809.3854736999 5801903.1393987 +32084 4326 212809.3854736999 5801903.1393987 -62.84921587788725 52.346057536290274 +4267 32084 -60.15 58.574 383339.70686208946 6495146.799396004 +32084 4326 383339.70686208946 6495146.799396004 -60.14895922441544 58.574132837117375 +4267 32085 -64.5 56.05 304800.0 6213294.765703978 +32085 4326 304800.0 6213294.765703978 -64.49884353563009 56.05001702126343 +4267 32085 -65.85 52.027 212148.65296530374 5766413.501029508 +32085 4326 212148.65296530374 5766413.501029508 -65.84899905934383 52.02697997995453 +4267 32085 -63.15 60.073 379955.9009295918 6662100.682731534 +32085 4326 379955.9009295918 6662100.682731534 -63.148646075354456 60.0730815389274 +4267 32086 -66.905 53.695 344099.4752642332 5951321.299458555 +32086 4326 344099.4752642332 5951321.299458555 -66.90400008646115 53.695005782882134 +4267 32086 -67.7195 52.2145 289798.3295369706 5786436.785985747 +32086 4326 289798.3295369706 5786436.785985747 -67.7185638525248 52.21449698464376 +4267 32086 -66.0905 55.1755 394596.7028119953 6116847.831837838 +32086 4326 394596.7028119953 6116847.831837838 -66.08943119171572 55.175518207646526 +4267 32098 -68.475 53.805 1634.9142691118725 1086417.2019230954 +32098 4326 1634.9142691118725 1086417.2019230954 -68.47441957124278 53.80510634397051 +4267 32098 -78.71249999999999 45.871500000000005 -790466.7867871705 264970.44477040955 +32098 4326 -790466.7867871705 264970.44477040955 -78.71237451769807 45.871591570789 +4267 32098 -58.2375 61.7385 542962.1728097897 2006133.7837669177 +32098 4326 542962.1728097897 2006133.7837669177 -58.236260203515194 61.738689965514276 +4267 32099 -89.955 27.465 2446944.9294382688 656094.6718338549 +32099 4326 2446944.9294382688 656094.6718338549 -89.9550695642731 27.46528106110633 +4267 32099 -93.0375 25.7685 1439077.9929482047 40803.66404288324 +32099 4326 1439077.9929482047 40803.66404288324 -93.03765314704917 25.768837852793784 +4267 32099 -86.8725 29.1615 3424416.096930173 1295768.8035940442 +32099 4326 3424416.096930173 1295768.8035940442 -86.87248323913822 29.161726895649647 +4269 32100 -110.055 46.68 557562.9972258529 270175.48989245493 +32100 4326 557562.9972258529 270175.48989245493 -110.055 46.68000000000001 +4269 32100 -115.46849999999999 44.583 126329.73944052018 55073.69494508364 +32100 4326 126329.73944052018 55073.69494508364 -115.46849999999999 44.583000000000006 +4269 32100 -104.64150000000001 48.777 956814.4198691815 514139.9142825689 +32100 4326 956814.4198691815 514139.9142825689 -104.64150000000001 48.77700000000001 +4269 32104 -99.68 41.5 526709.5912096216 185091.48420576085 +32104 4326 526709.5912096216 185091.48420576085 -99.68 41.5 +4269 32104 -103.622 40.141000000000005 191449.57910202077 40625.804804552994 +32104 4326 191449.57910202077 40625.804804552994 -103.622 40.14100000000001 +4269 32104 -95.738 42.858999999999995 848158.6906421972 344540.8856728787 +32104 4326 848158.6906421972 344540.8856728787 -95.738 42.858999999999995 +4269 32107 -115.52000000000001 38.495000000000005 205524.57150390767 8415545.609160446 +32107 4326 205524.57150390767 8415545.609160446 -115.52000000000001 38.495000000000005 +4269 32107 -116.861 35.3405 83857.77529178846 8066253.469782333 +32107 4326 83857.77529178846 8066253.469782333 -116.86100000000002 35.3405 +4269 32107 -114.179 41.6495 316976.07839282975 8766724.77413967 +32107 4326 316976.07839282975 8766724.77413967 -114.179 41.64950000000001 +4269 32108 -116.59 38.5 506687.1770771124 6416101.467797132 +32108 4326 506687.1770771124 6416101.467797132 -116.59 38.50000000000001 +4269 32108 -118.03 36.25 377474.7274872544 6167270.088553119 +32108 4326 377474.7274872544 6167270.088553119 -118.03000000000002 36.25 +4269 32108 -115.14999999999999 40.75 628075.3950645399 6666992.146123811 +32108 4326 628075.3950645399 6666992.146123811 -115.14999999999999 40.75 +4269 32109 -118.495 39.475 807599.8137248502 4524331.450272928 +32109 4326 807599.8137248502 4524331.450272928 -118.495 39.475 +4269 32109 -119.8495 37.2025 687604.7269785614 4272849.753804591 +32109 4326 687604.7269785614 4272849.753804591 -119.8495 37.20250000000001 +4269 32109 -117.14049999999999 41.7475 920000.7132682288 4777661.662819959 +32109 4326 920000.7132682288 4777661.662819959 -117.14049999999999 41.747499999999995 +4269 32110 -71.595 44.0 305747.9189590065 166643.36188109402 +32110 4326 305747.9189590065 166643.36188109402 -71.595 43.99999999999999 +4269 32110 -72.4635 42.821 234839.1555409388 35965.449943250715 +32110 4326 234839.1555409388 35965.449943250715 -72.4635 42.821 +4269 32110 -70.7265 45.179 373895.511671885 298081.46545753366 +32110 4326 373895.511671885 298081.46545753366 -70.7265 45.17900000000001 +4269 32111 -74.74 40.114999999999995 129541.93200635779 142309.84863179177 +32111 4326 129541.93200635779 142309.84863179177 -74.74 40.115 +4269 32111 -75.514 38.994499999999995 62161.845770000305 18379.059719253342 +32111 4326 62161.845770000305 18379.059719253342 -75.514 38.99449999999999 +4269 32111 -73.966 41.2355 194763.50353423748 266836.18215498014 +32111 4326 194763.50353423748 266836.18215498014 -73.966 41.23550000000001 +4269 32112 -104.35499999999999 34.5 163010.31461583532 388115.5015019724 +32112 4326 163010.31461583532 388115.5015019724 -104.355 34.5 +4269 32112 -105.5835 32.25 47196.13683957874 139273.63539034483 +32112 4326 47196.13683957874 139273.63539034483 -105.5835 32.24999999999999 +4269 32112 -103.1265 36.75 272765.40574489284 638413.9669679842 +32112 4326 272765.40574489284 638413.9669679842 -103.1265 36.75 +4269 32113 -106.285 34.39 496781.7063517648 375911.1857639483 +32113 4326 496781.7063517648 375911.1857639483 -106.28500000000001 34.39000000000001 +4269 32113 -107.58550000000001 32.041000000000004 373868.1798690023 116192.89978335424 +32113 4326 373868.1798690023 116192.89978335424 -107.58550000000002 32.041 +4269 32113 -104.9845 36.739 613019.4483625791 637255.137029333 +32113 4326 613019.4483625791 637255.137029333 -104.9845 36.739000000000004 +4269 32114 -107.69 34.165 843215.0652553578 350969.5921065168 +32114 4326 843215.0652553578 350969.5921065168 -107.69 34.165 +4269 32114 -108.923 31.6135 726610.1809091113 68531.38595231783 +32114 4326 726610.1809091113 68531.38595231783 -108.923 31.6135 +4269 32114 -106.457 36.716499999999996 952956.2155361695 634905.4140072872 +32114 4326 952956.2155361695 634905.4140072872 -106.45700000000001 36.7165 +4269 32115 -74.55000000000001 42.95 145920.05529655414 457119.0986501149 +32115 4326 145920.05529655414 457119.0986501149 -74.55000000000001 42.95000000000001 +4269 32115 -75.738 41.087 45987.00461632412 250946.93264947843 +32115 4326 45987.00461632412 250946.93264947843 -75.738 41.087 +4269 32115 -73.36200000000001 44.813 240010.12987699886 664725.038343087 +32115 4326 240010.12987699886 664725.038343087 -73.36200000000001 44.813 +4269 32116 -76.405 43.2 264493.31539123424 355403.0177038457 +32116 4326 264493.31539123424 355403.0177038457 -76.405 43.2 +4269 32116 -77.6155 42.111000000000004 164637.84596996327 234938.07240019878 +32116 4326 164637.84596996327 234938.07240019878 -77.6155 42.111000000000004 +4269 32116 -75.1945 44.288999999999994 360844.2676941891 477313.98916154087 +32116 4326 360844.2676941891 477313.98916154087 -75.19450000000002 44.288999999999994 +4269 32117 -78.565 42.815 351499.30115682975 312619.6539237273 +32117 4326 351499.30115682975 312619.6539237273 -78.565 42.81499999999999 +4269 32117 -79.64949999999999 42.072500000000005 261772.6033852207 230696.4045349582 +32117 4326 261772.6033852207 230696.4045349582 -79.64949999999999 42.0725 +4269 32117 -77.4805 43.5575 439103.5261805072 395694.3379393331 +32117 4326 439103.5261805072 395694.3379393331 -77.4805 43.557500000000005 +4269 32118 -73.03 40.885 381750.7399232304 80221.1659232867 +32118 4326 381750.7399232304 80221.1659232867 -73.03 40.885000000000005 +4269 32118 -74.137 40.5115 288388.66202628706 38301.14700668562 +32118 4326 288388.66202628706 38301.14700668562 -74.137 40.511500000000005 +4269 32118 -71.923 41.2585 474051.4783082586 123311.4021303021 +32118 4326 474051.4783082586 123311.4021303021 -71.923 41.2585 +4269 32119 -79.85499999999999 35.21 531760.6294742532 162292.8993226596 +32119 4326 531760.6294742532 162292.8993226596 -79.85499999999999 35.21000000000001 +4269 32119 -83.8825 33.967999999999996 158490.68215049576 35280.47102519372 +32119 4326 158490.68215049576 35280.47102519372 -83.8825 33.96799999999999 +4269 32119 -75.8275 36.452000000000005 893982.110337277 304301.6117079829 +32119 4326 893982.110337277 304301.6117079829 -75.8275 36.45200000000001 +4269 32120 -100.44999999999999 48.08 603725.2560461811 120076.52492659682 +32120 4326 603725.2560461811 120076.52492659682 -100.44999999999999 48.080000000000005 +4269 32120 -103.708 47.242999999999995 357180.63227263116 32076.32260325921 +32120 4326 357180.63227263116 32076.32260325921 -103.708 47.242999999999995 +4269 32120 -97.192 48.917 842389.7430150689 218354.69478269183 +32120 4326 842389.7430150689 218354.69478269183 -97.192 48.91700000000001 +4269 32121 -100.3 46.879999999999995 615244.233397394 134889.74595170486 +32121 4326 615244.233397394 134889.74595170486 -100.3 46.88000000000001 +4269 32121 -103.675 46.025 354223.17057553853 44799.12802504772 +32121 4326 354223.17057553853 44799.12802504772 -103.675 46.025000000000006 +4269 32121 -96.925 47.735 868072.4690473499 236025.6304733758 +32121 4326 868072.4690473499 236025.6304733758 -96.925 47.73499999999999 +4269 32122 -82.66 41.215 586582.9888405734 171948.99910634552 +32122 4326 586582.9888405734 171948.99910634552 -82.66 41.21500000000002 +4269 32122 -84.595 40.2115 421661.2948510517 62644.95346345006 +32122 4326 421661.2948510517 62644.95346345006 -84.595 40.211499999999994 +4269 32122 -80.72500000000001 42.2185 746566.5265375796 284886.53487657424 +32122 4326 746566.5265375796 284886.53487657424 -80.72500000000001 42.218499999999985 +4269 32123 -82.765 39.379999999999995 577168.794808187 153231.72928756103 +32123 4326 577168.794808187 153231.72928756103 -82.765 39.379999999999995 +4269 32123 -84.62349999999999 38.498 414763.0625386159 57464.14699698818 +32123 4326 414763.0625386159 57464.14699698818 -84.62349999999999 38.498 +4269 32123 -80.90650000000001 40.262 735553.8037072645 252321.37590929342 +32123 4326 735553.8037072645 252321.37590929342 -80.90650000000001 40.261999999999986 +4269 32124 -98.71000000000001 36.14 536101.1714221651 126719.8429846217 +32124 4326 536101.1714221651 126719.8429846217 -98.71000000000001 36.14000000000001 +4269 32124 -102.571 35.357 184677.13744587317 49389.48967290248 +32124 4326 184677.13744587317 49389.48967290248 -102.571 35.357000000000006 +4269 32124 -94.849 36.923 880718.1162748237 217928.31690933244 +32124 4326 880718.1162748237 217928.31690933244 -94.849 36.92300000000001 +4269 32125 -97.21000000000001 34.595 672465.8025262959 140231.8207822339 +32125 4326 672465.8025262959 140231.8207822339 -97.21000000000001 34.595 +4269 32125 -99.721 33.7175 440481.2346152521 43973.85878489801 +32125 4326 440481.2346152521 43973.85878489801 -99.721 33.717499999999994 +4269 32125 -94.699 35.4725 899563.148845982 242195.49149143902 +32125 4326 899563.148845982 242195.49149143902 -94.699 35.4725 +4269 32126 -120.32 45.105000000000004 2514164.9883181253 159845.1412173828 +32126 4326 2514164.9883181253 159845.1412173828 -120.32 45.105000000000004 +4269 32126 -123.785 44.0655 2236864.6381944637 49671.3154349094 +32126 4326 2236864.6381944637 49671.3154349094 -123.78500000000001 44.06550000000001 +4269 32126 -116.855 46.1445 2781533.537177242 281708.5817855314 +32126 4326 2781533.537177242 281708.5817855314 -116.855 46.1445 +4269 32127 -120.75 43.269999999999996 1479706.3814610983 178132.5497103679 +32127 4326 1479706.3814610983 178132.5497103679 -120.75 43.269999999999996 +4269 32127 -124.21499999999999 42.108999999999995 1192816.3476943534 55951.90213787231 +32127 4326 1192816.3476943534 55951.90213787231 -124.21499999999999 42.10900000000001 +4269 32127 -117.28500000000001 44.431000000000004 1755961.4123870605 312011.62286356714 +32127 4326 1755961.4123870605 312011.62286356714 -117.28500000000001 44.431000000000004 +4269 32128 -77.61500000000001 41.565 611260.251203743 155299.796259382 +32128 4326 611260.251203743 155299.796259382 -77.61500000000001 41.56500000000001 +4269 32128 -80.2385 40.6965 389693.64377760055 61862.05383360608 +32128 4326 389693.64377760055 61862.05383360608 -80.2385 40.6965 +4269 32128 -74.9915 42.4335 826973.6695968212 255374.7277154768 +32128 4326 826973.6695968212 255374.7277154768 -74.9915 42.43350000000001 +4269 32129 -77.625 40.445 610604.1860345764 123441.44619973708 +32129 4326 610604.1860345764 123441.44619973708 -77.625 40.445 +4269 32129 -80.2395 39.783500000000004 386764.6591716248 52990.14482360457 +32129 4326 386764.6591716248 52990.14482360457 -80.2395 39.783500000000025 +4269 32129 -75.0105 41.1065 830085.7794705472 200460.87468017207 +32129 4326 830085.7794705472 200460.87468017207 -75.0105 41.1065 +4269 32130 -71.465 41.575 102918.97351274508 54604.88640063912 +32130 4326 102918.97351274508 54604.88640063912 -71.465 41.57500000000001 +4269 32130 -71.8115 41.1745 73861.27843044055 10171.357680009327 +32130 4326 73861.27843044055 10171.357680009327 -71.8115 41.174499999999995 +4269 32130 -71.1185 41.975500000000004 131619.5144680358 99157.52522727985 +32130 4326 131619.5144680358 99157.52522727985 -71.1185 41.975500000000004 +4269 32133 -80.94 33.629999999999995 615165.8566942128 199244.85107013452 +32133 4326 615165.8566942128 199244.85107013452 -80.94 33.63 +4269 32133 -83.118 32.208 409907.55380288104 43600.556483864246 +32133 4326 409907.55380288104 43600.556483864246 -83.118 32.208000000000006 +4269 32133 -78.762 35.052 813775.0055652673 359176.8093884141 +32133 4326 813775.0055652673 359176.8093884141 -78.762 35.052 +4269 32134 -100.25999999999999 45.045 579517.1582990598 134676.18923571636 +32134 4326 579517.1582990598 134676.18923571636 -100.25999999999999 45.04500000000001 +4269 32134 -103.689 44.2305 305357.195159725 50848.92774272213 +32134 4326 305357.195159725 50848.92774272213 -103.689 44.23049999999999 +4269 32134 -96.831 45.859500000000004 846048.4784497482 229981.1518169474 +32134 4326 846048.4784497482 229981.1518169474 -96.831 45.859500000000004 +4269 32135 -100.245 43.635 607127.5043806202 144607.50092678124 +32135 4326 607127.5043806202 144607.50092678124 -100.245 43.63499999999999 +4269 32135 -103.6785 42.595499999999994 325505.86952982674 34653.81023614486 +32135 4326 325505.86952982674 34653.81023614486 -103.6785 42.595499999999994 +4269 32135 -96.81150000000001 44.6745 879191.0064309249 266024.15353638545 +32135 4326 879191.0064309249 266024.15353638545 -96.81150000000001 44.6745 +4269 32136 -85.98 35.83 601807.0427761595 166053.81335326185 +32136 4326 601807.0427761595 166053.81335326185 -85.98 35.830000000000005 +4269 32136 -89.87700000000001 35.065 246434.998258424 88183.71880346097 +32136 4326 246434.998258424 88183.71880346097 -89.87700000000001 35.06499999999998 +4269 32136 -82.083 36.595 950418.3889824077 257951.77285579516 +32136 4326 950418.3889824077 257951.77285579516 -82.083 36.59500000000002 +4269 32137 -101.50999999999999 35.4 199091.66389534375 1155311.273626022 +32137 4326 199091.66389534375 1155311.273626022 -101.50999999999999 35.4 +4269 32137 -102.878 34.41 73304.61790902242 1046368.8035420016 +32137 4326 73304.61790902242 1046368.8035420016 -102.878 34.40999999999999 +4269 32137 -100.142 36.39 321839.4199389363 1265990.8998509285 +32137 4326 321839.4199389363 1265990.8998509285 -100.142 36.38999999999999 +4269 32138 -98.535 33.15 596735.0946008511 2164487.199652242 +32138 4326 596735.0946008511 2164487.199652242 -98.535 33.149999999999984 +4269 32138 -102.61649999999999 31.863 210507.96670704812 2029404.6807503384 +32138 4326 210507.96670704812 2029404.6807503384 -102.61649999999999 31.863000000000007 +4269 32138 -94.4535 34.437 971879.2800386127 2314398.7444936717 +32138 4326 971879.2800386127 2314398.7444936717 -94.45350000000002 34.437000000000005 +4269 32139 -100.08 31.025000000000002 724185.2196589834 3150605.5484253517 +32139 4326 724185.2196589834 3150605.5484253517 -100.08 31.025000000000023 +4269 32139 -106.002 29.904500000000002 152730.7257107871 3040313.3125654417 +32139 4326 152730.7257107871 3040313.3125654417 -106.002 29.90449999999999 +4269 32139 -94.158 32.145500000000006 1282351.2308671223 3290980.4998480678 +32139 4326 1282351.2308671223 3290980.4998480678 -94.158 32.14550000000003 +4269 32140 -99.38 29.225 563058.7354705705 4154293.521203296 +32140 4326 563058.7354705705 4154293.521203296 -99.38 29.225000000000026 +4269 32140 -104.438 27.924500000000002 64841.30729672348 4022548.884762567 +32140 4326 64841.30729672348 4022548.884762567 -104.43800000000002 27.924499999999995 +4269 32140 -94.322 30.5255 1048881.5104492614 4307362.930045891 +32140 4326 1048881.5104492614 4307362.930045891 -94.322 30.5255 +4269 32141 -98.525 27.02 297519.3277685372 5149938.002474285 +32141 4326 297519.3277685372 5149938.002474285 -98.525 27.01999999999998 +4269 32141 -100.0325 25.948999999999998 146497.67705421898 5032213.809542949 +32141 4326 146497.67705421898 5032213.809542949 -100.0325 25.949000000000012 +4269 32141 -97.0175 28.091 445706.4161671256 5269468.382250638 +32141 4326 445706.4161671256 5269468.382250638 -97.0175 28.091 +4269 32142 -111.54 41.28 496649.01927216403 1105128.0559982655 +32142 4326 496649.01927216403 1105128.0559982655 -111.54 41.28000000000001 +4269 32142 -113.79 40.623 306256.0472603298 1034720.2739077386 +32142 4326 306256.0472603298 1034720.2739077386 -113.79 40.62299999999999 +4269 32142 -109.29 41.937 683266.1531509589 1180426.6984195628 +32142 4326 683266.1531509589 1180426.6984195628 -109.29 41.937000000000005 +4269 32143 -111.545 39.785 496145.6404933844 2161162.6996333404 +32143 4326 496145.6404933844 2161162.6996333404 -111.545 39.785000000000004 +4269 32143 -113.7995 38.6195 299737.81075454154 2034346.1327566376 +32143 4326 299737.81075454154 2034346.1327566376 -113.79950000000001 38.61950000000001 +4269 32143 -109.29050000000001 40.9505 686033.2712166447 2292873.457553067 +32143 4326 686033.2712166447 2292873.457553067 -109.29050000000001 40.950500000000005 +4269 32144 -111.545 37.785 496036.2211105266 3124117.453606073 +32144 4326 496036.2211105266 3124117.453606073 -111.545 37.785000000000004 +4269 32144 -113.7995 37.069500000000005 295518.8854022882 3047222.248513894 +32144 4326 295518.8854022882 3047222.248513894 -113.79950000000001 37.069500000000026 +4269 32144 -109.29050000000001 38.500499999999995 692727.1993279501 3205811.513840428 +32144 4326 692727.1993279501 3205811.513840428 -109.29050000000001 38.5005 +4269 32145 -72.47 43.875 502411.1456667513 152752.52373386305 +32145 4326 502411.1456667513 152752.52373386305 -72.47 43.87500000000001 +4269 32145 -73.343 42.835499999999996 431080.14350458205 37612.8404625171 +32145 4326 431080.14350458205 37612.8404625171 -73.343 42.835499999999996 +4269 32145 -71.597 44.914500000000004 571301.9692705146 268653.87743555265 +32145 4326 571301.9692705146 268653.87743555265 -71.597 44.91450000000001 +4269 32146 -78.285 38.615 3518724.203106891 2105285.4939807146 +32146 4326 3518724.203106891 2105285.4939807146 -78.285 38.614999999999995 +4269 32146 -79.88250000000001 37.8545 3378332.274917174 2021765.3758821723 +32146 4326 3378332.274917174 2021765.3758821723 -79.88250000000001 37.85450000000001 +4269 32146 -76.6875 39.3755 3656172.5417433567 2191230.607915427 +32146 4326 3656172.5417433567 2191230.607915427 -76.6875 39.3755 +4269 32147 -79.495 37.41 3411917.6086140587 1119947.2032969252 +32147 4326 3411917.6086140587 1119947.2032969252 -79.495 37.41000000000001 +4269 32147 -83.26150000000001 36.627 3074278.1812144495 1043328.2241050153 +32147 4326 3074278.1812144495 1043328.2241050153 -83.26150000000001 36.626999999999995 +4269 32147 -75.7285 38.193 3742765.366771591 1209952.5513936281 +32147 4326 3742765.366771591 1209952.5513936281 -75.7285 38.19300000000001 +4269 32148 -120.905 48.065 494658.8782095999 118411.46677224239 +32148 4326 494658.8782095999 118411.46677224239 -120.90500000000002 48.065 +4269 32148 -124.4015 47.1785 229601.1611486969 26116.106779398477 +32148 4326 229601.1611486969 26116.106779398477 -124.4015 47.178500000000014 +4269 32148 -117.4085 48.951499999999996 750773.7610210045 222566.60307881774 +32148 4326 750773.7610210045 222566.60307881774 -117.4085 48.951499999999996 +4269 32149 -120.655 46.575 488119.19059687364 138022.62988068728 +32149 4326 488119.19059687364 138022.62988068728 -120.655 46.575 +4269 32149 -124.02550000000001 45.643499999999996 225232.76586336124 40617.24264164518 +32149 4326 225232.76586336124 40617.24264164518 -124.02550000000001 45.64349999999999 +4269 32149 -117.2845 47.5065 742180.9642298741 246499.98165656062 +32149 4326 742180.9642298741 246499.98165656062 -117.2845 47.5065 +4269 32150 -79.74000000000001 39.7 579417.2957110632 133248.46017368144 +32150 4326 579417.2957110632 133248.46017368144 -79.74000000000001 39.70000000000001 +4269 32150 -81.558 38.854 421367.18441514135 41346.53591224026 +32150 4326 421367.18441514135 41346.53591224026 -81.558 38.854000000000006 +4269 32150 -77.922 40.546 733674.570779342 228331.12197136818 +32150 4326 733674.570779342 228331.12197136818 -77.922 40.546000000000014 +4269 32151 -80.85 38.185 613140.720628098 131531.86202500196 +32151 4326 613140.720628098 131531.86202500196 -80.85 38.18500000000001 +4269 32151 -82.47 37.298500000000004 469665.8086919263 34164.20925580738 +32151 4326 469665.8086919263 34164.20925580738 -82.47 37.298500000000004 +4269 32151 -79.23 39.0715 753171.9602462114 231389.43782599163 +32151 4326 753171.9602462114 231389.43782599163 -79.23 39.07150000000001 +4269 32152 -90.47 46.34 563817.9183929065 130517.46296681912 +32152 4326 563817.9183929065 130517.46296681912 -90.47 46.339999999999996 +4269 32152 -92.648 45.467 392950.88681591925 36832.035967861266 +32152 4326 392950.88681591925 36832.035967861266 -92.648 45.467 +4269 32152 -88.292 47.213 729391.0452604456 228851.15030724084 +32152 4326 729391.0452604456 228851.15030724084 -88.292 47.212999999999994 +4269 32153 -89.57 44.89 633966.7768787964 117505.3837196188 +32153 4326 633966.7768787964 117505.3837196188 -89.57000000000001 44.88999999999999 +4269 32153 -92.558 44.071 395103.35046936397 29636.785858339033 +32153 4326 395103.35046936397 29636.785858339033 -92.558 44.071 +4269 32153 -86.582 45.708999999999996 866087.9109689173 214035.8706913536 +32153 4326 866087.9109689173 214035.8706913536 -86.582 45.708999999999996 +4269 32154 -89.19 43.405 665607.2736576359 156400.65287319338 +32154 4326 665607.2736576359 156400.65287319338 -89.19 43.404999999999994 +4269 32154 -91.206 42.5725 500982.45158363064 64316.57338896093 +32154 4326 500982.45158363064 64316.57338896093 -91.206 42.57250000000001 +4269 32154 -87.174 44.2375 825722.2152976134 252402.33148048513 +32154 4326 825722.2152976134 252402.33148048513 -87.174 44.23750000000001 +4269 32155 -105.19 43.0 198097.49632915662 277654.042474903 +32155 4326 198097.49632915662 277654.042474903 -105.19 43.0 +4269 32155 -106.216 41.191 111974.33693520293 77262.27621200448 +32155 4326 111974.33693520293 77262.27621200448 -106.216 41.19100000000001 +4269 32155 -104.164 44.809 279314.3848616645 479129.0114329825 +32155 4326 279314.3848616645 479129.0114329825 -104.164 44.809 +4269 32156 -107.315 43.0 401494.82431166724 377653.9413779187 +32156 4326 401494.82431166724 377653.9413779187 -107.315 43.0 +4269 32156 -108.49849999999999 41.191 302257.2284902079 177385.96133674524 +32156 4326 302257.2284902079 177385.96133674524 -108.49849999999999 41.191 +4269 32156 -106.1315 44.809 495069.171304143 579342.6387105695 +32156 4326 495069.171304143 579342.6387105695 -106.1315 44.809 +4269 32157 -109.28 43.0 556785.943962579 277790.0915190472 +32157 4326 556785.943962579 277790.0915190472 -109.28000000000002 43.0 +4269 32157 -110.882 41.191 421148.2266105671 78923.38133064652 +32157 4326 421148.2266105671 78923.38133064652 -110.882 41.191 +4269 32157 -107.678 44.809 684798.8953224984 479198.9978457161 +32157 4326 684798.8953224984 479198.9978457161 -107.678 44.809000000000005 +4269 32158 -110.05000000000001 42.83 802725.3431980808 358770.01389699115 +32158 4326 802725.3431980808 358770.01389699115 -110.05000000000001 42.83 +4269 32158 -110.959 41.174 726523.9197376369 175213.1974278146 +32158 4326 726523.9197376369 175213.1974278146 -110.959 41.174 +4269 32158 -109.141 44.486000000000004 874956.6699701425 543180.753202716 +32158 4326 874956.6699701425 543180.753202716 -109.141 44.48600000000001 +4269 32159 -107.555 43.0 495517.9695104008 422058.51273764414 +32159 4326 495517.9695104008 422058.51273764414 -107.55500000000002 43.00000000000001 +4269 32159 -110.7095 41.191 230844.10426883394 226353.44422791727 +32159 4326 230844.10426883394 226353.44422791727 -110.7095 41.191 +4269 32159 -104.4005 44.809 745113.3437662122 627489.6324631852 +32159 4326 745113.3437662122 627489.6324631852 -104.4005 44.80899999999999 +4269 32161 -66.185 18.39 226241.24800736172 261630.59702072313 +4269 32161 -68.25949999999999 15.267 3585.063588968391 -83165.98132082267 +4269 32161 -64.1105 21.512999999999998 441058.152086962 609066.0717281448 +4269 32164 -98.935 37.42 1659286.3141232086 13587466.174662098 +32164 4326 1659286.3141232086 13587466.174662098 -98.935 37.41999999999998 +4269 32164 -101.6935 26.988999999999997 763382.9232475144 9803359.03027318 +32164 4326 763382.9232475144 9803359.03027318 -101.69350000000001 26.98899999999999 +4269 32164 -96.1765 47.851 2333386.929101685 17400226.714606088 +32164 4326 2333386.929101685 17400226.714606088 -96.17650000000002 47.851000000000006 +4269 32165 -92.935 37.495000000000005 1659267.479421868 13614764.806267321 +32165 4326 1659267.479421868 13614764.806267321 -92.935 37.49499999999999 +4269 32165 -95.7025 26.7985 758972.1259009573 9734152.586663194 +32165 4326 758972.1259009573 9734152.586663194 -95.7025 26.798500000000004 +4269 32165 -90.1675 48.191500000000005 2331029.3517941954 17524455.866288807 +32165 4326 2331029.3517941954 17524455.866288807 -90.1675 48.191500000000005 +4269 32166 -86.96000000000001 36.135 1652224.2269926541 13119799.338719474 +32166 4326 1652224.2269926541 13119799.338719474 -86.96000000000001 36.135 +4269 32166 -89.705 25.168499999999998 745889.5167859707 9141531.081201045 +32166 4326 745889.5167859707 9141531.081201045 -89.705 25.1685 +4269 32166 -84.215 47.1015 2333735.2094723587 17126630.35163519 +32166 4326 2333735.2094723587 17126630.35163519 -84.215 47.10150000000001 +4269 32167 -81.03999999999999 34.97 1628437.0418346918 12695896.25263612 +32167 4326 1628437.0418346918 12695896.25263612 -81.03999999999999 34.97 +4269 32167 -83.785 24.926 717605.0562595102 9053907.099083865 +32167 4326 717605.0562595102 9053907.099083865 -83.785 24.92599999999999 +4269 32167 -78.295 45.014 2339705.0335666365 16365011.70492429 +32167 4326 2339705.0335666365 16365011.70492429 -78.295 45.01400000000001 +4269 32181 -53.519999999999996 48.225 266167.7742660153 5343042.305810534 +32181 4326 266167.7742660153 5343042.305810534 -53.519999999999996 48.225 +4269 32181 -54.402 46.7265 197637.93505436246 5177279.522066797 +32181 4326 197637.93505436246 5177279.522066797 -54.402 46.726499999999994 +4269 32181 -52.638 49.7235 330899.77029580396 5509604.837755639 +32181 4326 330899.77029580396 5509604.837755639 -52.638 49.72350000000001 +4269 32182 -55.995000000000005 50.760000000000005 305152.7607283704 5624824.161000914 +32182 4326 305152.7607283704 5624824.161000914 -55.995000000000005 50.76000000000001 +4269 32182 -57.3495 47.205000000000005 202566.6048048936 5230397.971164339 +32182 4326 202566.6048048936 5230397.971164339 -57.349500000000006 47.205 +4269 32182 -54.6405 54.315 393264.3743449206 6021228.46386013 +32182 4326 393264.3743449206 6021228.46386013 -54.6405 54.315 +4269 32183 -58.55 51.44 301323.7648052591 5700467.898256849 +32183 4326 301323.7648052591 5700467.898256849 -58.55 51.44 +4269 32183 -59.855 47.894 203486.75376547498 5306999.7403105125 +32183 4326 203486.75376547498 5306999.7403105125 -59.85499999999999 47.89399999999999 +4269 32183 -57.245000000000005 54.986000000000004 385130.37002827233 6095782.81105129 +32183 4326 385130.37002827233 6095782.81105129 -57.245000000000005 54.986 +4269 32184 -61.5 53.04 304800.0 5878483.934355941 +32184 4326 304800.0 5878483.934355941 -61.50000000000001 53.04 +4269 32184 -62.85 47.748 203577.13712667965 5290761.97413933 +32184 4326 203577.13712667965 5290761.97413933 -62.849999999999994 47.74799999999999 +4269 32184 -60.15 58.332 383877.9519267481 6468407.153778225 +32184 4326 383877.9519267481 6468407.153778225 -60.15 58.33200000000001 +4269 32185 -64.5 54.235 304800.0 6011472.0107470285 +32185 4326 304800.0 6011472.0107470285 -64.5 54.23499999999999 +4269 32185 -65.85 48.578500000000005 205198.3311343926 5383096.143461107 +32185 4326 205198.3311343926 5383096.143461107 -65.85 48.57850000000001 +4269 32185 -63.15 59.8915 380365.5001219588 6642090.663385094 +32185 4326 380365.5001219588 6642090.663385094 -63.150000000000006 59.89150000000001 +4269 32186 -67.5 53.155 304800.0 5891280.786565086 +32186 4326 304800.0 5891280.786565086 -67.5 53.154999999999994 +4269 32186 -68.85 47.8945 203861.5683617214 5307048.778975857 +32186 4326 203861.5683617214 5307048.778975857 -68.85 47.8945 +4269 32186 -66.15 58.4155 383691.36466289486 6477705.645014932 +32186 4326 383691.36466289486 6477705.645014932 -66.15 58.415499999999994 +4269 32187 -70.5 53.405 304800.0 5919100.887776715 +32187 4326 304800.0 5919100.887776715 -70.5 53.40500000000001 +4269 32187 -71.85 45.8495 199952.14056091162 5079736.298233002 +32187 4326 199952.14056091162 5079736.298233002 -71.85 45.849499999999985 +4269 32187 -69.15 60.960499999999996 377924.8926710408 6761169.233849469 +32187 4326 377924.8926710408 6761169.233849469 -69.15 60.96049999999998 +4269 32188 -73.5 53.754999999999995 304800.0 5958050.99760996 +32188 4326 304800.0 5958050.99760996 -73.5 53.75499999999999 +4269 32188 -74.85 45.857499999999995 199967.17535427015 5080625.389969004 +32188 4326 199967.17535427015 5080625.389969004 -74.85 45.857499999999995 +4269 32188 -72.15 61.6525 376331.2017690007 6838262.451047374 +32188 4326 376331.2017690007 6838262.451047374 -72.15 61.65250000000001 +4269 32189 -76.5 53.14 304800.0 5889611.617824443 +32189 4326 304800.0 5889611.617824443 -76.5 53.14 +4269 32189 -77.85 44.581 197594.40722045692 4938774.634699871 +32189 4326 197594.40722045692 4938774.634699871 -77.85 44.581 +4269 32189 -75.15 61.699 376223.72955384525 6843443.115298966 +32189 4326 376223.72955384525 6843443.115298966 -75.15 61.69899999999999 +4269 32190 -79.5 52.355000000000004 304800.0 5802264.371886736 +32190 4326 304800.0 5802264.371886736 -79.5 52.355 +4269 32190 -80.85 43.2695 195212.3497160519 4793065.827792506 +32190 4326 195212.3497160519 4793065.827792506 -80.85 43.269499999999994 +4269 32190 -78.15 61.4405 376820.5765459477 6814643.503802963 +32190 4326 376820.5765459477 6814643.503802963 -78.15 61.4405 +4269 32191 -82.3 43.835 320884.01985230704 4855023.008696913 +32191 4326 320884.01985230704 4855023.008696913 -82.3 43.834999999999994 +4269 32191 -83.47 41.8865 224299.73349253612 4639021.263440096 +32191 4326 224299.73349253612 4639021.263440096 -83.47 41.88649999999999 +4269 32191 -81.13 45.783500000000004 411326.9483581694 5072427.79761347 +32191 4326 411326.9483581694 5072427.79761347 -81.13 45.783500000000004 +4269 32192 -81.0 50.605000000000004 304800.0 5607583.33382646 +32192 4326 304800.0 5607583.33382646 -81.0 50.605000000000004 +4269 32192 -82.35 46.4605 201106.3244268999 5147644.078890337 +32192 4326 201106.3244268999 5147644.078890337 -82.35 46.460499999999996 +4269 32192 -79.65 54.7495 391718.0987432869 6069573.696578401 +32192 4326 391718.0987432869 6069573.696578401 -79.65 54.7495 +4269 32193 -84.0 50.795 304800.0 5628717.3001815565 +32193 4326 304800.0 5628717.3001815565 -84.0 50.795 +4269 32193 -85.35 46.4795 201142.4066109733 5149755.887885232 +32193 4326 201142.4066109733 5149755.887885232 -85.35 46.4795 +4269 32193 -82.65 55.1105 390943.12263336655 6109753.223535529 +32193 4326 390943.12263336655 6109753.223535529 -82.65 55.1105 +4269 32194 -87.0 51.935 304800.0 5755535.600946693 +32194 4326 304800.0 5755535.600946693 -87.0 51.93499999999999 +4269 32194 -88.35 47.6465 203380.4647399176 5279478.173564699 +32194 4326 203380.4647399176 5279478.173564699 -88.35 47.64649999999999 +4269 32194 -85.65 56.2235 388532.1709837478 6233645.02786236 +32194 4326 388532.1709837478 6233645.02786236 -85.65 56.22350000000001 +4269 32195 -90.0 52.435 304800.0 5811165.4693391705 +32195 4326 304800.0 5811165.4693391705 -90.0 52.43499999999999 +4269 32195 -91.35 48.4165 204880.4294295852 5365084.149450656 +32195 4326 204880.4294295852 5365084.149450656 -91.35 48.4165 +4269 32195 -88.65 56.4535 388029.9376740323 6259249.762159773 +32195 4326 388029.9376740323 6259249.762159773 -88.65 56.45349999999999 +4269 32196 -93.0 51.63 304800.0 5721603.713476377 +32196 4326 304800.0 5721603.713476377 -93.0 51.63 +4269 32196 -94.35 48.417 204881.4093704535 5365139.741284148 +32196 4326 204881.4093704535 5365139.741284148 -94.35 48.417 +4269 32196 -91.65 54.843 391517.711160886 6079980.087531899 +32196 4326 391517.711160886 6079980.087531899 -91.65 54.843 +4269 32197 -94.83 50.965 386983.664340777 5648279.054044446 +32197 4326 386983.664340777 5648279.054044446 -94.83 50.965 +4269 32197 -95.127 48.9175 368777.6797239066 5420278.251220682 +32197 4326 368777.6797239066 5420278.251220682 -95.127 48.9175 +4269 32197 -94.533 53.0125 403249.05654057354 5876430.670350219 +32197 4326 403249.05654057354 5876430.670350219 -94.533 53.01249999999999 +4269 32198 -68.475 53.805 1634.8564338814983 1086417.5610523205 +32198 4326 1634.8564338814983 1086417.5610523205 -68.475 53.805 +4269 32198 -78.71249999999999 45.871500000000005 -790443.0169708299 264970.4427806088 +32198 4326 -790443.0169708299 264970.4427806088 -78.71249999999999 45.87149999999998 +4269 32198 -58.2375 61.7385 542940.5911125129 2006118.281500356 +32198 4326 542940.5911125129 2006118.281500356 -58.237500000000004 61.7385 +4269 32199 -89.955 27.465 1136226.526362496 218453.59665624824 +32199 4326 1136226.526362496 218453.59665624824 -89.955 27.46499999999999 +4269 32199 -93.0375 25.7685 829033.6908785433 30904.715813586146 +32199 4326 829033.6908785433 30904.715813586146 -93.03750000000001 25.768500000000003 +4269 32199 -86.8725 29.1615 1434154.351324326 413433.82211615506 +32199 4326 1434154.351324326 413433.82211615506 -86.8725 29.16149999999999 +4322 32201 -177.0 42.0 500000.0 4649774.988009868 +32201 4326 500000.0 4649774.988009868 -176.9998461111111 42.00003190735118 +4322 32201 -179.7 4.2 200248.64837286732 464751.96998849313 +32201 4326 200248.64837286732 464751.96998849313 -179.6998461111111 4.200040849907261 +4322 32201 -174.3 79.8 553358.5727386967 8860497.914284788 +32201 4326 553358.5727386967 8860497.914284788 -174.29984611111112 79.80000776644764 +4322 32202 -171.0 42.0 500000.0 4649774.988009868 +32202 4326 500000.0 4649774.988009868 -170.9998461111111 42.00003190735117 +4322 32202 -173.7 4.2 200248.64837286732 464751.96998849313 +32202 4326 200248.64837286732 464751.96998849313 -173.6998461111111 4.20004084990726 +4322 32202 -168.3 79.8 553358.5727386967 8860497.914284788 +32202 4326 553358.5727386967 8860497.914284788 -168.29984611111112 79.80000776644764 +4322 32203 -165.0 42.0 500000.0 4649774.988009868 +32203 4326 500000.0 4649774.988009868 -164.99984611111108 42.00003190735117 +4322 32203 -167.7 4.2 200248.64837286447 464751.96998849313 +32203 4326 200248.64837286447 464751.96998849313 -167.6998461111111 4.20004084990726 +4322 32203 -162.3 79.8 553358.5727386967 8860497.914284788 +32203 4326 553358.5727386967 8860497.914284788 -162.2998461111111 79.80000776644764 +4322 32204 -159.0 42.0 500000.0 4649774.988009868 +32204 4326 500000.0 4649774.988009868 -158.99984611111114 42.00003190735117 +4322 32204 -161.7 4.2 200248.64837286732 464751.96998849313 +32204 4326 200248.64837286732 464751.96998849313 -161.69984611111113 4.20004084990726 +4322 32204 -156.3 79.8 553358.5727386967 8860497.914284788 +32204 4326 553358.5727386967 8860497.914284788 -156.29984611111112 79.80000776644764 +4322 32205 -153.0 42.0 500000.0 4649774.988009868 +32205 4326 500000.0 4649774.988009868 -152.9998461111111 42.00003190735117 +4322 32205 -155.7 4.2 200248.64837286732 464751.96998849313 +32205 4326 200248.64837286732 464751.96998849313 -155.6998461111111 4.20004084990726 +4322 32205 -150.3 79.8 553358.5727386967 8860497.914284788 +32205 4326 553358.5727386967 8860497.914284788 -150.29984611111112 79.80000776644764 +4322 32206 -147.0 42.0 500000.0000000021 4649774.988009868 +32206 4326 500000.0000000021 4649774.988009868 -146.99984611111108 42.00003190735117 +4322 32206 -149.7 4.2 200248.64837286732 464751.96998849313 +32206 4326 200248.64837286732 464751.96998849313 -149.6998461111111 4.20004084990726 +4322 32206 -144.3 79.8 553358.5727386972 8860497.914284788 +32206 4326 553358.5727386972 8860497.914284788 -144.29984611111112 79.80000776644764 +4322 32207 -141.0 42.0 499999.9999999979 4649774.988009868 +32207 4326 499999.9999999979 4649774.988009868 -140.9998461111111 42.00003190735117 +4322 32207 -143.7 4.2 200248.64837286447 464751.96998849313 +32207 4326 200248.64837286447 464751.96998849313 -143.69984611111113 4.20004084990726 +4322 32207 -138.3 79.8 553358.5727386961 8860497.914284788 +32207 4326 553358.5727386961 8860497.914284788 -138.29984611111112 79.80000776644764 +4322 32208 -135.0 42.0 500000.0 4649774.988009868 +32208 4326 500000.0 4649774.988009868 -134.99984611111114 42.00003190735117 +4322 32208 -137.7 4.2 200248.64837286732 464751.96998849313 +32208 4326 200248.64837286732 464751.96998849313 -137.6998461111111 4.20004084990726 +4322 32208 -132.3 79.8 553358.5727386967 8860497.914284788 +32208 4326 553358.5727386967 8860497.914284788 -132.29984611111112 79.80000776644764 +4322 32209 -129.0 42.0 500000.0 4649774.988009868 +32209 4326 500000.0 4649774.988009868 -128.99984611111108 42.00003190735117 +4322 32209 -131.7 4.2 200248.64837286447 464751.96998849313 +32209 4326 200248.64837286447 464751.96998849313 -131.6998461111111 4.20004084990726 +4322 32209 -126.3 79.8 553358.5727386967 8860497.914284788 +32209 4326 553358.5727386967 8860497.914284788 -126.29984611111111 79.80000776644764 +4322 32210 -123.0 42.0 500000.0 4649774.988009868 +32210 4326 500000.0 4649774.988009868 -122.99984611111111 42.00003190735117 +4322 32210 -125.7 4.2 200248.64837286732 464751.96998849313 +32210 4326 200248.64837286732 464751.96998849313 -125.6998461111111 4.20004084990726 +4322 32210 -120.3 79.8 553358.5727386972 8860497.914284788 +32210 4326 553358.5727386972 8860497.914284788 -120.2998461111111 79.80000776644764 +4322 32211 -117.0 42.0 500000.0 4649774.988009868 +32211 4326 500000.0 4649774.988009868 -116.9998461111111 42.00003190735117 +4322 32211 -119.7 4.2 200248.64837286447 464751.96998849313 +32211 4326 200248.64837286447 464751.96998849313 -119.69984611111111 4.20004084990726 +4322 32211 -114.3 79.8 553358.5727386969 8860497.914284788 +32211 4326 553358.5727386969 8860497.914284788 -114.29984611111112 79.80000776644764 +4322 32212 -111.0 42.0 500000.0 4649774.988009868 +32212 4326 500000.0 4649774.988009868 -110.99984611111113 42.00003190735118 +4322 32212 -113.7 4.2 200248.64837286586 464751.96998849313 +32212 4326 200248.64837286586 464751.96998849313 -113.69984611111111 4.20004084990726 +4322 32212 -108.3 79.8 553358.5727386972 8860497.914284788 +32212 4326 553358.5727386972 8860497.914284788 -108.29984611111111 79.80000776644764 +4322 32213 -105.0 42.0 499999.99999999895 4649774.988009868 +32213 4326 499999.99999999895 4649774.988009868 -104.99984611111113 42.00003190735117 +4322 32213 -107.7 4.2 200248.64837286447 464751.96998849313 +32213 4326 200248.64837286447 464751.96998849313 -107.69984611111111 4.200040849907261 +4322 32213 -102.3 79.8 553358.5727386969 8860497.914284788 +32213 4326 553358.5727386969 8860497.914284788 -102.29984611111111 79.80000776644764 +4322 32214 -99.0 42.0 500000.0 4649774.988009868 +32214 4326 500000.0 4649774.988009868 -98.99984611111111 42.00003190735117 +4322 32214 -101.7 4.2 200248.64837286447 464751.96998849313 +32214 4326 200248.64837286447 464751.96998849313 -101.69984611111113 4.200040849907261 +4322 32214 -96.3 79.8 553358.5727386969 8860497.914284788 +32214 4326 553358.5727386969 8860497.914284788 -96.29984611111112 79.80000776644764 +4322 32215 -93.0 42.0 499999.99999999895 4649774.988009868 +32215 4326 499999.99999999895 4649774.988009868 -92.99984611111113 42.00003190735117 +4322 32215 -95.7 4.2 200248.64837286447 464751.96998849313 +32215 4326 200248.64837286447 464751.96998849313 -95.69984611111111 4.20004084990726 +4322 32215 -90.3 79.8 553358.5727386969 8860497.914284788 +32215 4326 553358.5727386969 8860497.914284788 -90.29984611111111 79.80000776644764 +4322 32216 -87.0 42.0 500000.00000000105 4649774.988009868 +32216 4326 500000.00000000105 4649774.988009868 -86.99984611111111 42.00003190735118 +4322 32216 -89.7 4.2 200248.64837286586 464751.96998849313 +32216 4326 200248.64837286586 464751.96998849313 -89.69984611111111 4.200040849907261 +4322 32216 -84.3 79.8 553358.5727386972 8860497.914284788 +32216 4326 553358.5727386972 8860497.914284788 -84.29984611111111 79.80000776644764 +4322 32217 -81.0 42.0 500000.0 4649774.988009868 +32217 4326 500000.0 4649774.988009868 -80.99984611111111 42.00003190735117 +4322 32217 -83.7 4.2 200248.64837286447 464751.96998849313 +32217 4326 200248.64837286447 464751.96998849313 -83.69984611111113 4.20004084990726 +4322 32217 -78.3 79.8 553358.5727386969 8860497.914284788 +32217 4326 553358.5727386969 8860497.914284788 -78.29984611111112 79.80000776644764 +4322 32218 -75.0 42.0 500000.0 4649774.988009868 +32218 4326 500000.0 4649774.988009868 -74.99984611111113 42.00003190735117 +4322 32218 -77.7 4.2 200248.64837286586 464751.96998849313 +32218 4326 200248.64837286586 464751.96998849313 -77.69984611111111 4.20004084990726 +4322 32218 -72.3 79.8 553358.5727386972 8860497.914284788 +32218 4326 553358.5727386972 8860497.914284788 -72.29984611111111 79.80000776644764 +4322 32219 -69.0 42.0 500000.0 4649774.988009868 +32219 4326 500000.0 4649774.988009868 -68.99984611111111 42.00003190735117 +4322 32219 -71.7 4.2 200248.64837286447 464751.96998849313 +32219 4326 200248.64837286447 464751.96998849313 -71.69984611111111 4.20004084990726 +4322 32219 -66.3 79.8 553358.5727386969 8860497.914284788 +32219 4326 553358.5727386969 8860497.914284788 -66.29984611111111 79.80000776644764 +4322 32220 -63.0 42.0 500000.0 4649774.988009868 +32220 4326 500000.0 4649774.988009868 -62.99984611111111 42.00003190735117 +4322 32220 -65.7 4.2 200248.64837286447 464751.96998849313 +32220 4326 200248.64837286447 464751.96998849313 -65.69984611111113 4.20004084990726 +4322 32220 -60.3 79.8 553358.5727386969 8860497.914284788 +32220 4326 553358.5727386969 8860497.914284788 -60.2998461111111 79.80000776644764 +4322 32221 -57.0 42.0 500000.00000000105 4649774.988009868 +32221 4326 500000.00000000105 4649774.988009868 -56.99984611111111 42.00003190735117 +4322 32221 -59.7 4.2 200248.64837286732 464751.96998849313 +32221 4326 200248.64837286732 464751.96998849313 -59.699846111111114 4.200040849907261 +4322 32221 -54.3 79.8 553358.5727386973 8860497.914284788 +32221 4326 553358.5727386973 8860497.914284788 -54.29984611111111 79.80000776644764 +4322 32222 -51.0 42.0 500000.0000000005 4649774.988009868 +32222 4326 500000.0000000005 4649774.988009868 -50.99984611111111 42.00003190735117 +4322 32222 -53.7 4.2 200248.64837286586 464751.96998849313 +32222 4326 200248.64837286586 464751.96998849313 -53.69984611111112 4.20004084990726 +4322 32222 -48.3 79.8 553358.5727386972 8860497.914284788 +32222 4326 553358.5727386972 8860497.914284788 -48.299846111111115 79.80000776644764 +4322 32223 -45.0 42.0 500000.0 4649774.988009868 +32223 4326 500000.0 4649774.988009868 -44.99984611111111 42.00003190735117 +4322 32223 -47.7 4.2 200248.64837286517 464751.96998849313 +32223 4326 200248.64837286517 464751.96998849313 -47.699846111111114 4.200040849907261 +4322 32223 -42.3 79.8 553358.572738697 8860497.914284788 +32223 4326 553358.572738697 8860497.914284788 -42.29984611111111 79.80000776644764 +4322 32224 -39.0 42.0 499999.9999999995 4649774.988009868 +32224 4326 499999.9999999995 4649774.988009868 -38.99984611111111 42.00003190735117 +4322 32224 -41.7 4.2 200248.64837286447 464751.96998849313 +32224 4326 200248.64837286447 464751.96998849313 -41.69984611111111 4.20004084990726 +4322 32224 -36.3 79.8 553358.5727386968 8860497.914284788 +32224 4326 553358.5727386968 8860497.914284788 -36.299846111111115 79.80000776644764 +4322 32225 -33.0 42.0 500000.0000000005 4649774.988009868 +32225 4326 500000.0000000005 4649774.988009868 -32.99984611111112 42.00003190735117 +4322 32225 -35.7 4.2 200248.64837286586 464751.96998849313 +32225 4326 200248.64837286586 464751.96998849313 -35.699846111111114 4.20004084990726 +4322 32225 -30.3 79.8 553358.5727386972 8860497.914284788 +32225 4326 553358.5727386972 8860497.914284788 -30.299846111111112 79.80000776644764 +4322 32226 -27.0 42.0 500000.0 4649774.988009868 +32226 4326 500000.0 4649774.988009868 -26.99984611111111 42.00003190735117 +4322 32226 -29.7 4.2 200248.64837286586 464751.96998849313 +32226 4326 200248.64837286586 464751.96998849313 -29.699846111111107 4.20004084990726 +4322 32226 -24.3 79.8 553358.5727386969 8860497.914284788 +32226 4326 553358.5727386969 8860497.914284788 -24.299846111111115 79.80000776644764 +4322 32227 -21.0 42.0 499999.9999999992 4649774.988009868 +32227 4326 499999.9999999992 4649774.988009868 -20.99984611111112 42.00003190735118 +4322 32227 -23.7 4.2 200248.64837286482 464751.96998849313 +32227 4326 200248.64837286482 464751.96998849313 -23.699846111111107 4.20004084990726 +4322 32227 -18.3 79.8 553358.5727386968 8860497.914284788 +32227 4326 553358.5727386968 8860497.914284788 -18.29984611111112 79.80000776644764 +4322 32228 -15.0 42.0 499999.99999999866 4649774.988009868 +32228 4326 499999.99999999866 4649774.988009868 -14.999846111111113 42.00003190735117 +4322 32228 -17.7 4.2 200248.64837286377 464751.96998849313 +32228 4326 200248.64837286377 464751.96998849313 -17.699846111111114 4.200040849907261 +4322 32228 -12.3 79.8 553358.5727386966 8860497.914284788 +32228 4326 553358.5727386966 8860497.914284788 -12.299846111111114 79.80000776644764 +4322 32229 -9.0 42.0 500000.0 4649774.988009868 +32229 4326 500000.0 4649774.988009868 -8.999846111111111 42.00003190735117 +4322 32229 -11.7 4.2 200248.6483728657 464751.96998849313 +32229 4326 200248.6483728657 464751.96998849313 -11.699846111111112 4.200040849907261 +4322 32229 -6.300000000000001 79.8 553358.5727386969 8860497.914284788 +32229 4326 553358.5727386969 8860497.914284788 -6.299846111111114 79.80000776644764 +4322 32230 -3.0 42.0 499999.9999999993 4649774.988009868 +32230 4326 499999.9999999993 4649774.988009868 -2.9998461111111108 42.00003190735117 +4322 32230 -5.7 4.2 200248.64837286464 464751.96998849313 +32230 4326 200248.64837286464 464751.96998849313 -5.6998461111111105 4.20004084990726 +4322 32230 -0.3000000000000007 79.8 553358.5727386968 8860497.914284788 +32230 4326 553358.5727386968 8860497.914284788 -0.29984611111111104 79.80000776644764 +4322 32231 3.0 42.0 500000.0000000007 4649774.988009868 +32231 4326 500000.0000000007 4649774.988009868 3.000153888888889 42.00003190735118 +4322 32231 0.30000000000000004 4.2 200248.64837286656 464751.96998849313 +32231 4326 200248.64837286656 464751.96998849313 0.30015388888888894 4.200040849907261 +4322 32231 5.699999999999999 79.8 553358.5727386972 8860497.914284788 +32231 4326 553358.5727386972 8860497.914284788 5.700153888888889 79.80000776644764 +4322 32232 9.0 42.0 500000.0 4649774.988009868 +32232 4326 500000.0 4649774.988009868 9.000153888888887 42.00003190735117 +4322 32232 6.3 4.2 200248.64837286563 464751.96998849313 +32232 4326 200248.64837286563 464751.96998849313 6.3001538888888895 4.20004084990726 +4322 32232 11.7 79.8 553358.5727386969 8860497.914284788 +32232 4326 553358.5727386969 8860497.914284788 11.700153888888888 79.80000776644764 +4322 32233 15.0 42.0 500000.00000000134 4649774.988009868 +32233 4326 500000.00000000134 4649774.988009868 15.000153888888883 42.00003190735117 +4322 32233 12.3 4.2 200248.6483728676 464751.96998849313 +32233 4326 200248.6483728676 464751.96998849313 12.300153888888888 4.20004084990726 +4322 32233 17.7 79.8 553358.5727386973 8860497.914284788 +32233 4326 553358.5727386973 8860497.914284788 17.700153888888888 79.80000776644764 +4322 32234 21.0 42.0 500000.0000000008 4649774.988009868 +32234 4326 500000.0000000008 4649774.988009868 21.000153888888885 42.00003190735117 +4322 32234 18.3 4.2 200248.64837286662 464751.96998849313 +32234 4326 200248.64837286662 464751.96998849313 18.300153888888886 4.20004084990726 +4322 32234 23.7 79.8 553358.572738697 8860497.914284788 +32234 4326 553358.572738697 8860497.914284788 23.70015388888889 79.80000776644764 +4322 32235 27.0 42.0 500000.0 4649774.988009868 +32235 4326 500000.0 4649774.988009868 27.00015388888889 42.00003190735117 +4322 32235 24.3 4.2 200248.64837286586 464751.96998849313 +32235 4326 200248.64837286586 464751.96998849313 24.30015388888889 4.20004084990726 +4322 32235 29.7 79.8 553358.5727386969 8860497.914284788 +32235 4326 553358.5727386969 8860497.914284788 29.700153888888888 79.80000776644764 +4322 32236 33.0 42.0 499999.9999999995 4649774.988009868 +32236 4326 499999.9999999995 4649774.988009868 33.00015388888889 42.00003190735117 +4322 32236 30.3 4.2 200248.64837286447 464751.96998849313 +32236 4326 200248.64837286447 464751.96998849313 30.30015388888889 4.200040849907261 +4322 32236 35.7 79.8 553358.5727386969 8860497.914284788 +32236 4326 553358.5727386969 8860497.914284788 35.7001538888889 79.80000776644764 +4322 32237 39.0 42.0 500000.0000000005 4649774.988009868 +32237 4326 500000.0000000005 4649774.988009868 39.00015388888888 42.00003190735117 +4322 32237 36.3 4.2 200248.64837286662 464751.96998849313 +32237 4326 200248.64837286662 464751.96998849313 36.30015388888889 4.20004084990726 +4322 32237 41.7 79.8 553358.5727386972 8860497.914284788 +32237 4326 553358.5727386972 8860497.914284788 41.70015388888889 79.80000776644764 +4322 32238 45.0 42.0 500000.0 4649774.988009868 +32238 4326 500000.0 4649774.988009868 45.00015388888888 42.00003190735117 +4322 32238 42.3 4.2 200248.64837286517 464751.96998849313 +32238 4326 200248.64837286517 464751.96998849313 42.30015388888888 4.20004084990726 +4322 32238 47.7 79.8 553358.572738697 8860497.914284788 +32238 4326 553358.572738697 8860497.914284788 47.70015388888889 79.80000776644764 +4322 32239 51.0 42.0 500000.0000000016 4649774.988009868 +32239 4326 500000.0000000016 4649774.988009868 51.00015388888889 42.00003190735117 +4322 32239 48.3 4.2 200248.64837286732 464751.96998849313 +32239 4326 200248.64837286732 464751.96998849313 48.30015388888889 4.20004084990726 +4322 32239 53.7 79.8 553358.5727386974 8860497.914284788 +32239 4326 553358.5727386974 8860497.914284788 53.700153888888906 79.80000776644764 +4322 32240 57.0 42.0 499999.99999999895 4649774.988009868 +32240 4326 499999.99999999895 4649774.988009868 57.00015388888889 42.00003190735117 +4322 32240 54.3 4.2 200248.64837286377 464751.96998849313 +32240 4326 200248.64837286377 464751.96998849313 54.300153888888886 4.20004084990726 +4322 32240 59.7 79.8 553358.5727386967 8860497.914284788 +32240 4326 553358.5727386967 8860497.914284788 59.700153888888885 79.80000776644764 +4322 32241 63.0 42.0 500000.0 4649774.988009868 +32241 4326 500000.0 4649774.988009868 63.00015388888888 42.00003190735117 +4322 32241 60.3 4.2 200248.64837286586 464751.96998849313 +32241 4326 200248.64837286586 464751.96998849313 60.300153888888886 4.200040849907261 +4322 32241 65.7 79.8 553358.5727386972 8860497.914284788 +32241 4326 553358.5727386972 8860497.914284788 65.70015388888889 79.80000776644764 +4322 32242 69.0 42.0 499999.99999999686 4649774.988009868 +32242 4326 499999.99999999686 4649774.988009868 69.00015388888887 42.00003190735118 +4322 32242 66.3 4.2 200248.64837286161 464751.9699884932 +32242 4326 200248.64837286161 464751.9699884932 66.3001538888889 4.20004084990726 +4322 32242 71.7 79.8 553358.5727386965 8860497.914284788 +32242 4326 553358.5727386965 8860497.914284788 71.70015388888889 79.80000776644764 +4322 32243 75.0 42.0 499999.99999999895 4649774.988009868 +32243 4326 499999.99999999895 4649774.988009868 75.00015388888889 42.00003190735117 +4322 32243 72.3 4.2 200248.64837286295 464751.9699884932 +32243 4326 200248.64837286295 464751.9699884932 72.30015388888889 4.200040849907261 +4322 32243 77.7 79.8 553358.5727386967 8860497.914284788 +32243 4326 553358.5727386967 8860497.914284788 77.70015388888889 79.80000776644764 +4322 32244 81.0 42.0 500000.0 4649774.988009868 +32244 4326 500000.0 4649774.988009868 81.00015388888889 42.00003190735117 +4322 32244 78.3 4.2 200248.64837286586 464751.96998849313 +32244 4326 200248.64837286586 464751.96998849313 78.30015388888889 4.200040849907261 +4322 32244 83.7 79.8 553358.5727386972 8860497.914284788 +32244 4326 553358.5727386972 8860497.914284788 83.70015388888889 79.80000776644764 +4322 32245 87.0 42.0 499999.99999999686 4649774.988009868 +32245 4326 499999.99999999686 4649774.988009868 87.00015388888887 42.00003190735117 +4322 32245 84.3 4.2 200248.64837286161 464751.9699884932 +32245 4326 200248.64837286161 464751.9699884932 84.30015388888889 4.200040849907261 +4322 32245 89.7 79.8 553358.5727386965 8860497.914284788 +32245 4326 553358.5727386965 8860497.914284788 89.70015388888889 79.80000776644764 +4322 32246 93.0 42.0 499999.99999999895 4649774.988009868 +32246 4326 499999.99999999895 4649774.988009868 93.00015388888889 42.00003190735118 +4322 32246 90.3 4.2 200248.64837286295 464751.9699884932 +32246 4326 200248.64837286295 464751.9699884932 90.30015388888889 4.200040849907261 +4322 32246 95.7 79.8 553358.5727386967 8860497.914284788 +32246 4326 553358.5727386967 8860497.914284788 95.70015388888889 79.80000776644764 +4322 32247 99.0 42.0 500000.0 4649774.988009868 +32247 4326 500000.0 4649774.988009868 99.00015388888889 42.00003190735117 +4322 32247 96.3 4.2 200248.64837286586 464751.96998849313 +32247 4326 200248.64837286586 464751.96998849313 96.30015388888889 4.20004084990726 +4322 32247 101.7 79.8 553358.5727386972 8860497.914284788 +32247 4326 553358.5727386972 8860497.914284788 101.70015388888889 79.80000776644764 +4322 32248 105.0 42.0 499999.9999999979 4649774.988009868 +32248 4326 499999.9999999979 4649774.988009868 105.00015388888889 42.00003190735117 +4322 32248 102.3 4.2 200248.64837286161 464751.9699884932 +32248 4326 200248.64837286161 464751.9699884932 102.30015388888889 4.20004084990726 +4322 32248 107.7 79.8 553358.5727386965 8860497.914284788 +32248 4326 553358.5727386965 8860497.914284788 107.70015388888889 79.80000776644764 +4322 32249 111.0 42.0 500000.00000000314 4649774.988009868 +32249 4326 500000.00000000314 4649774.988009868 111.00015388888889 42.00003190735117 +4322 32249 108.3 4.2 200248.64837286866 464751.96998849313 +32249 4326 200248.64837286866 464751.96998849313 108.30015388888889 4.20004084990726 +4322 32249 113.7 79.8 553358.5727386976 8860497.914284788 +32249 4326 553358.5727386976 8860497.914284788 113.70015388888889 79.80000776644764 +4322 32250 117.0 42.0 500000.0 4649774.988009868 +32250 4326 500000.0 4649774.988009868 117.0001538888889 42.00003190735117 +4322 32250 114.3 4.2 200248.64837286586 464751.96998849313 +32250 4326 200248.64837286586 464751.96998849313 114.30015388888889 4.20004084990726 +4322 32250 119.7 79.8 553358.5727386972 8860497.914284788 +32250 4326 553358.5727386972 8860497.914284788 119.7001538888889 79.80000776644764 +4322 32251 123.0 42.0 500000.0000000021 4649774.988009868 +32251 4326 500000.0000000021 4649774.988009868 123.0001538888889 42.00003190735118 +4322 32251 120.3 4.2 200248.64837286732 464751.96998849313 +32251 4326 200248.64837286732 464751.96998849313 120.3001538888889 4.200040849907261 +4322 32251 125.7 79.8 553358.5727386972 8860497.914284788 +32251 4326 553358.5727386972 8860497.914284788 125.70015388888886 79.80000776644764 +4322 32252 129.0 42.0 500000.0000000021 4649774.988009868 +32252 4326 500000.0000000021 4649774.988009868 129.0001538888889 42.00003190735117 +4322 32252 126.3 4.2 200248.6483728701 464751.96998849313 +32252 4326 200248.6483728701 464751.96998849313 126.3001538888889 4.20004084990726 +4322 32252 131.7 79.8 553358.5727386976 8860497.914284788 +32252 4326 553358.5727386976 8860497.914284788 131.7001538888889 79.80000776644764 +4322 32253 135.0 42.0 500000.0 4649774.988009868 +32253 4326 500000.0 4649774.988009868 135.0001538888889 42.00003190735117 +4322 32253 132.3 4.2 200248.64837286732 464751.96998849313 +32253 4326 200248.64837286732 464751.96998849313 132.3001538888889 4.200040849907261 +4322 32253 137.7 79.8 553358.5727386967 8860497.914284788 +32253 4326 553358.5727386967 8860497.914284788 137.70015388888885 79.80000776644764 +4322 32254 141.0 42.0 500000.0000000021 4649774.988009868 +32254 4326 500000.0000000021 4649774.988009868 141.00015388888892 42.00003190735117 +4322 32254 138.3 4.2 200248.6483728701 464751.96998849313 +32254 4326 200248.6483728701 464751.96998849313 138.30015388888893 4.200040849907261 +4322 32254 143.7 79.8 553358.5727386972 8860497.914284788 +32254 4326 553358.5727386972 8860497.914284788 143.70015388888888 79.80000776644764 +4322 32255 147.0 42.0 500000.0000000021 4649774.988009868 +32255 4326 500000.0000000021 4649774.988009868 147.0001538888889 42.00003190735117 +4322 32255 144.3 4.2 200248.6483728701 464751.96998849313 +32255 4326 200248.6483728701 464751.96998849313 144.3001538888889 4.20004084990726 +4322 32255 149.7 79.8 553358.5727386976 8860497.914284788 +32255 4326 553358.5727386976 8860497.914284788 149.7001538888889 79.80000776644764 +4322 32256 153.0 42.0 500000.0 4649774.988009868 +32256 4326 500000.0 4649774.988009868 153.0001538888889 42.00003190735117 +4322 32256 150.3 4.2 200248.64837286732 464751.96998849313 +32256 4326 200248.64837286732 464751.96998849313 150.3001538888889 4.20004084990726 +4322 32256 155.7 79.8 553358.5727386967 8860497.914284788 +32256 4326 553358.5727386967 8860497.914284788 155.70015388888888 79.80000776644764 +4322 32257 159.0 42.0 500000.0000000021 4649774.988009868 +32257 4326 500000.0000000021 4649774.988009868 159.0001538888889 42.00003190735118 +4322 32257 156.3 4.2 200248.6483728701 464751.96998849313 +32257 4326 200248.6483728701 464751.96998849313 156.30015388888893 4.20004084990726 +4322 32257 161.7 79.8 553358.5727386972 8860497.914284788 +32257 4326 553358.5727386972 8860497.914284788 161.70015388888888 79.80000776644764 +4322 32258 165.0 42.0 499999.9999999979 4649774.988009868 +32258 4326 499999.9999999979 4649774.988009868 165.0001538888889 42.00003190735117 +4322 32258 162.3 4.2 200248.64837286447 464751.96998849313 +32258 4326 200248.64837286447 464751.96998849313 162.3001538888889 4.200040849907261 +4322 32258 167.7 79.8 553358.5727386967 8860497.914284788 +32258 4326 553358.5727386967 8860497.914284788 167.7001538888889 79.80000776644764 +4322 32259 171.0 42.0 500000.0 4649774.988009868 +32259 4326 500000.0 4649774.988009868 171.0001538888889 42.00003190735117 +4322 32259 168.3 4.2 200248.64837286732 464751.96998849313 +32259 4326 200248.64837286732 464751.96998849313 168.3001538888889 4.20004084990726 +4322 32259 173.7 79.8 553358.5727386967 8860497.914284788 +32259 4326 553358.5727386967 8860497.914284788 173.70015388888888 79.80000776644764 +4322 32260 177.0 42.0 500000.0000000021 4649774.988009868 +32260 4326 500000.0000000021 4649774.988009868 177.00015388888892 42.00003190735117 +4322 32260 174.3 4.2 200248.6483728701 464751.96998849313 +32260 4326 200248.6483728701 464751.96998849313 174.30015388888893 4.20004084990726 +4322 32260 179.7 79.8 553358.5727386972 8860497.914284788 +32260 4326 553358.5727386972 8860497.914284788 179.70015388888888 79.80000776644764 +4322 32301 -177.0 -40.0 500000.0 5572243.953413932 +32301 4326 500000.0 5572243.953413932 -176.9998461111111 -39.999970736192495 +4322 32301 -179.7 -76.0 427110.12140598043 1563150.3170419894 +32301 4326 427110.12140598043 1563150.3170419894 -179.6998461111111 -75.99999109568323 +4322 32301 -174.3 -4.0 799825.9746359233 9557379.650145141 +32301 4326 799825.9746359233 9557379.650145141 -174.29984611111112 -3.999959656720183 +4322 32302 -171.0 -40.0 500000.0 5572243.953413932 +32302 4326 500000.0 5572243.953413932 -170.9998461111111 -39.999970736192495 +4322 32302 -173.7 -76.0 427110.12140598043 1563150.3170419894 +32302 4326 427110.12140598043 1563150.3170419894 -173.6998461111111 -75.99999109568321 +4322 32302 -168.3 -4.0 799825.9746359233 9557379.650145141 +32302 4326 799825.9746359233 9557379.650145141 -168.29984611111112 -3.999959656720183 +4322 32303 -165.0 -40.0 500000.0 5572243.953413932 +32303 4326 500000.0 5572243.953413932 -164.99984611111108 -39.999970736192495 +4322 32303 -167.7 -76.0 427110.12140597973 1563150.3170419894 +32303 4326 427110.12140597973 1563150.3170419894 -167.6998461111111 -75.99999109568321 +4322 32303 -162.3 -4.0 799825.9746359233 9557379.650145141 +32303 4326 799825.9746359233 9557379.650145141 -162.2998461111111 -3.999959656720183 +4322 32304 -159.0 -40.0 500000.0 5572243.953413932 +32304 4326 500000.0 5572243.953413932 -158.9998461111111 -39.999970736192495 +4322 32304 -161.7 -76.0 427110.12140598043 1563150.3170419894 +32304 4326 427110.12140598043 1563150.3170419894 -161.6998461111111 -75.99999109568321 +4322 32304 -156.3 -4.0 799825.9746359233 9557379.650145141 +32304 4326 799825.9746359233 9557379.650145141 -156.29984611111112 -3.999959656720183 +4322 32305 -153.0 -40.0 500000.0 5572243.953413932 +32305 4326 500000.0 5572243.953413932 -152.9998461111111 -39.999970736192495 +4322 32305 -155.7 -76.0 427110.12140598043 1563150.3170419894 +32305 4326 427110.12140598043 1563150.3170419894 -155.6998461111111 -75.99999109568323 +4322 32305 -150.3 -4.0 799825.9746359233 9557379.650145141 +32305 4326 799825.9746359233 9557379.650145141 -150.29984611111112 -3.999959656720183 +4322 32306 -147.0 -40.0 500000.00000000215 5572243.953413932 +32306 4326 500000.00000000215 5572243.953413932 -146.9998461111111 -39.999970736192495 +4322 32306 -149.7 -76.0 427110.12140598043 1563150.3170419894 +32306 4326 427110.12140598043 1563150.3170419894 -149.6998461111111 -75.99999109568321 +4322 32306 -144.3 -4.0 799825.9746359261 9557379.650145141 +32306 4326 799825.9746359261 9557379.650145141 -144.2998461111111 -3.999959656720183 +4322 32307 -141.0 -40.0 499999.99999999785 5572243.953413932 +32307 4326 499999.99999999785 5572243.953413932 -140.99984611111114 -39.999970736192495 +4322 32307 -143.7 -76.0 427110.12140597973 1563150.3170419894 +32307 4326 427110.12140597973 1563150.3170419894 -143.6998461111111 -75.99999109568323 +4322 32307 -138.3 -4.0 799825.9746359205 9557379.650145141 +32307 4326 799825.9746359205 9557379.650145141 -138.29984611111112 -3.999959656720183 +4322 32308 -135.0 -40.0 500000.0 5572243.953413932 +32308 4326 500000.0 5572243.953413932 -134.9998461111111 -39.999970736192495 +4322 32308 -137.7 -76.0 427110.12140598043 1563150.3170419894 +32308 4326 427110.12140598043 1563150.3170419894 -137.6998461111111 -75.99999109568321 +4322 32308 -132.3 -4.0 799825.9746359233 9557379.650145141 +32308 4326 799825.9746359233 9557379.650145141 -132.29984611111115 -3.999959656720183 +4322 32309 -129.0 -40.0 500000.0 5572243.953413932 +32309 4326 500000.0 5572243.953413932 -128.99984611111108 -39.999970736192495 +4322 32309 -131.7 -76.0 427110.12140597973 1563150.3170419894 +32309 4326 427110.12140597973 1563150.3170419894 -131.6998461111111 -75.99999109568323 +4322 32309 -126.3 -4.0 799825.9746359233 9557379.650145141 +32309 4326 799825.9746359233 9557379.650145141 -126.29984611111111 -3.999959656720183 +4322 32310 -123.0 -40.0 500000.0 5572243.953413932 +32310 4326 500000.0 5572243.953413932 -122.99984611111111 -39.999970736192495 +4322 32310 -125.7 -76.0 427110.12140598043 1563150.3170419894 +32310 4326 427110.12140598043 1563150.3170419894 -125.6998461111111 -75.99999109568321 +4322 32310 -120.3 -4.0 799825.9746359261 9557379.650145141 +32310 4326 799825.9746359261 9557379.650145141 -120.2998461111111 -3.999959656720183 +4322 32311 -117.0 -40.0 500000.0 5572243.953413932 +32311 4326 500000.0 5572243.953413932 -116.99984611111113 -39.999970736192495 +4322 32311 -119.7 -76.0 427110.12140597973 1563150.3170419894 +32311 4326 427110.12140597973 1563150.3170419894 -119.69984611111111 -75.99999109568321 +4322 32311 -114.3 -4.0 799825.9746359247 9557379.650145141 +32311 4326 799825.9746359247 9557379.650145141 -114.29984611111112 -3.999959656720183 +4322 32312 -111.0 -40.0 500000.0 5572243.953413932 +32312 4326 500000.0 5572243.953413932 -110.99984611111111 -39.999970736192495 +4322 32312 -113.7 -76.0 427110.1214059801 1563150.3170419894 +32312 4326 427110.1214059801 1563150.3170419894 -113.69984611111111 -75.99999109568323 +4322 32312 -108.3 -4.0 799825.9746359261 9557379.650145141 +32312 4326 799825.9746359261 9557379.650145141 -108.29984611111111 -3.999959656720183 +4322 32313 -105.0 -40.0 499999.9999999989 5572243.953413932 +32313 4326 499999.9999999989 5572243.953413932 -104.99984611111113 -39.999970736192495 +4322 32313 -107.7 -76.0 427110.12140597973 1563150.3170419894 +32313 4326 427110.12140597973 1563150.3170419894 -107.69984611111111 -75.99999109568323 +4322 32313 -102.3 -4.0 799825.9746359247 9557379.650145141 +32313 4326 799825.9746359247 9557379.650145141 -102.29984611111111 -3.999959656720183 +4322 32314 -99.0 -40.0 500000.0 5572243.953413932 +32314 4326 500000.0 5572243.953413932 -98.99984611111111 -39.999970736192495 +4322 32314 -101.7 -76.0 427110.12140597973 1563150.3170419894 +32314 4326 427110.12140597973 1563150.3170419894 -101.69984611111113 -75.99999109568321 +4322 32314 -96.3 -4.0 799825.9746359247 9557379.650145141 +32314 4326 799825.9746359247 9557379.650145141 -96.29984611111112 -3.999959656720183 +4322 32315 -93.0 -40.0 499999.9999999989 5572243.953413932 +32315 4326 499999.9999999989 5572243.953413932 -92.99984611111113 -39.999970736192495 +4322 32315 -95.7 -76.0 427110.12140597973 1563150.3170419894 +32315 4326 427110.12140597973 1563150.3170419894 -95.69984611111111 -75.99999109568321 +4322 32315 -90.3 -4.0 799825.9746359247 9557379.650145141 +32315 4326 799825.9746359247 9557379.650145141 -90.29984611111111 -3.999959656720183 +4322 32316 -87.0 -40.0 500000.0000000011 5572243.953413932 +32316 4326 500000.0000000011 5572243.953413932 -86.99984611111111 -39.999970736192495 +4322 32316 -89.7 -76.0 427110.1214059801 1563150.3170419894 +32316 4326 427110.1214059801 1563150.3170419894 -89.69984611111111 -75.99999109568323 +4322 32316 -84.3 -4.0 799825.9746359261 9557379.650145141 +32316 4326 799825.9746359261 9557379.650145141 -84.29984611111111 -3.999959656720183 +4322 32317 -81.0 -40.0 500000.0 5572243.953413932 +32317 4326 500000.0 5572243.953413932 -80.99984611111111 -39.999970736192495 +4322 32317 -83.7 -76.0 427110.12140597973 1563150.3170419894 +32317 4326 427110.12140597973 1563150.3170419894 -83.69984611111113 -75.99999109568321 +4322 32317 -78.3 -4.0 799825.9746359247 9557379.650145141 +32317 4326 799825.9746359247 9557379.650145141 -78.29984611111111 -3.999959656720184 +4322 32318 -75.0 -40.0 500000.0 5572243.953413932 +32318 4326 500000.0 5572243.953413932 -74.99984611111113 -39.999970736192495 +4322 32318 -77.7 -76.0 427110.1214059801 1563150.3170419894 +32318 4326 427110.1214059801 1563150.3170419894 -77.69984611111111 -75.99999109568321 +4322 32318 -72.3 -4.0 799825.9746359261 9557379.650145141 +32318 4326 799825.9746359261 9557379.650145141 -72.29984611111111 -3.999959656720183 +4322 32319 -69.0 -40.0 500000.0 5572243.953413932 +32319 4326 500000.0 5572243.953413932 -68.99984611111111 -39.999970736192495 +4322 32319 -71.7 -76.0 427110.12140597973 1563150.3170419894 +32319 4326 427110.12140597973 1563150.3170419894 -71.69984611111111 -75.99999109568321 +4322 32319 -66.3 -4.0 799825.9746359247 9557379.650145141 +32319 4326 799825.9746359247 9557379.650145141 -66.29984611111111 -3.999959656720183 +4322 32320 -63.0 -40.0 500000.0 5572243.953413932 +32320 4326 500000.0 5572243.953413932 -62.99984611111111 -39.999970736192495 +4322 32320 -65.7 -76.0 427110.12140597973 1563150.3170419894 +32320 4326 427110.12140597973 1563150.3170419894 -65.69984611111111 -75.99999109568321 +4322 32320 -60.3 -4.0 799825.9746359247 9557379.650145141 +32320 4326 799825.9746359247 9557379.650145141 -60.299846111111115 -3.999959656720183 +4322 32321 -57.0 -40.0 500000.0000000011 5572243.953413932 +32321 4326 500000.0000000011 5572243.953413932 -56.99984611111111 -39.999970736192495 +4322 32321 -59.7 -76.0 427110.12140598043 1563150.3170419894 +32321 4326 427110.12140598043 1563150.3170419894 -59.699846111111114 -75.99999109568321 +4322 32321 -54.3 -4.0 799825.9746359268 9557379.650145141 +32321 4326 799825.9746359268 9557379.650145141 -54.299846111111115 -3.999959656720183 +4322 32322 -51.0 -40.0 500000.0000000005 5572243.953413932 +32322 4326 500000.0000000005 5572243.953413932 -50.99984611111112 -39.999970736192495 +4322 32322 -53.7 -76.0 427110.1214059801 1563150.3170419894 +32322 4326 427110.1214059801 1563150.3170419894 -53.69984611111113 -75.99999109568321 +4322 32322 -48.3 -4.0 799825.9746359261 9557379.650145141 +32322 4326 799825.9746359261 9557379.650145141 -48.29984611111111 -3.999959656720183 +4322 32323 -45.0 -40.0 500000.0 5572243.953413932 +32323 4326 500000.0 5572243.953413932 -44.999846111111104 -39.999970736192495 +4322 32323 -47.7 -76.0 427110.1214059799 1563150.3170419894 +32323 4326 427110.1214059799 1563150.3170419894 -47.699846111111114 -75.99999109568323 +4322 32323 -42.3 -4.0 799825.9746359254 9557379.650145141 +32323 4326 799825.9746359254 9557379.650145141 -42.299846111111115 -3.9999596567201827 +4322 32324 -39.0 -40.0 499999.9999999995 5572243.953413932 +32324 4326 499999.9999999995 5572243.953413932 -38.99984611111111 -39.999970736192495 +4322 32324 -41.7 -76.0 427110.12140597973 1563150.3170419894 +32324 4326 427110.12140597973 1563150.3170419894 -41.69984611111111 -75.99999109568321 +4322 32324 -36.3 -4.0 799825.974635924 9557379.650145141 +32324 4326 799825.974635924 9557379.650145141 -36.29984611111111 -3.999959656720183 +4322 32325 -33.0 -40.0 500000.0000000005 5572243.953413932 +32325 4326 500000.0000000005 5572243.953413932 -32.99984611111112 -39.999970736192495 +4322 32325 -35.7 -76.0 427110.1214059801 1563150.3170419894 +32325 4326 427110.1214059801 1563150.3170419894 -35.699846111111114 -75.99999109568321 +4322 32325 -30.3 -4.0 799825.9746359261 9557379.650145141 +32325 4326 799825.9746359261 9557379.650145141 -30.299846111111112 -3.999959656720183 +4322 32326 -27.0 -40.0 500000.0 5572243.953413932 +32326 4326 500000.0 5572243.953413932 -26.99984611111111 -39.999970736192495 +4322 32326 -29.7 -76.0 427110.1214059801 1563150.3170419894 +32326 4326 427110.1214059801 1563150.3170419894 -29.69984611111111 -75.99999109568321 +4322 32326 -24.3 -4.0 799825.9746359247 9557379.650145141 +32326 4326 799825.9746359247 9557379.650145141 -24.299846111111115 -3.999959656720183 +4322 32327 -21.0 -40.0 499999.9999999992 5572243.953413932 +32327 4326 499999.9999999992 5572243.953413932 -20.999846111111108 -39.999970736192495 +4322 32327 -23.7 -76.0 427110.12140597985 1563150.3170419894 +32327 4326 427110.12140597985 1563150.3170419894 -23.69984611111111 -75.99999109568323 +4322 32327 -18.3 -4.0 799825.974635924 9557379.650145141 +32327 4326 799825.974635924 9557379.650145141 -18.29984611111111 -3.999959656720183 +4322 32328 -15.0 -40.0 499999.99999999866 5572243.953413932 +32328 4326 499999.99999999866 5572243.953413932 -14.999846111111108 -39.999970736192495 +4322 32328 -17.7 -76.0 427110.12140597956 1563150.3170419894 +32328 4326 427110.12140597956 1563150.3170419894 -17.699846111111114 -75.99999109568323 +4322 32328 -12.3 -4.0 799825.9746359229 9557379.650145141 +32328 4326 799825.9746359229 9557379.650145141 -12.299846111111114 -3.999959656720183 +4322 32329 -9.0 -40.0 500000.0 5572243.953413932 +32329 4326 500000.0 5572243.953413932 -8.999846111111111 -39.999970736192495 +4322 32329 -11.7 -76.0 427110.12140598 1563150.3170419894 +32329 4326 427110.12140598 1563150.3170419894 -11.699846111111112 -75.99999109568321 +4322 32329 -6.300000000000001 -4.0 799825.9746359249 9557379.650145141 +32329 4326 799825.9746359249 9557379.650145141 -6.299846111111113 -3.999959656720183 +4322 32330 -3.0 -40.0 499999.99999999924 5572243.953413932 +32330 4326 499999.99999999924 5572243.953413932 -2.9998461111111117 -39.999970736192495 +4322 32330 -5.7 -76.0 427110.1214059798 1563150.3170419894 +32330 4326 427110.1214059798 1563150.3170419894 -5.69984611111111 -75.99999109568321 +4322 32330 -0.3000000000000007 -4.0 799825.974635924 9557379.650145141 +32330 4326 799825.974635924 9557379.650145141 -0.2998461111111127 -3.999959656720183 +4322 32331 3.0 -40.0 500000.00000000076 5572243.953413932 +32331 4326 500000.00000000076 5572243.953413932 3.000153888888889 -39.999970736192495 +4322 32331 0.30000000000000004 -76.0 427110.12140598026 1563150.3170419894 +32331 4326 427110.12140598026 1563150.3170419894 0.3001538888888886 -75.99999109568323 +4322 32331 5.699999999999999 -4.0 799825.9746359258 9557379.650145141 +32331 4326 799825.9746359258 9557379.650145141 5.700153888888886 -3.999959656720183 +4322 32332 9.0 -40.0 500000.0 5572243.953413932 +32332 4326 500000.0 5572243.953413932 9.00015388888889 -39.999970736192495 +4322 32332 6.3 -76.0 427110.12140598 1563150.3170419894 +32332 4326 427110.12140598 1563150.3170419894 6.3001538888888895 -75.99999109568321 +4322 32332 11.7 -4.0 799825.9746359249 9557379.650145141 +32332 4326 799825.9746359249 9557379.650145141 11.70015388888889 -3.999959656720184 +4322 32333 15.0 -40.0 500000.00000000134 5572243.953413932 +32333 4326 500000.00000000134 5572243.953413932 15.000153888888887 -39.999970736192495 +4322 32333 12.3 -76.0 427110.1214059805 1563150.3170419894 +32333 4326 427110.1214059805 1563150.3170419894 12.300153888888888 -75.99999109568321 +4322 32333 17.7 -4.0 799825.9746359268 9557379.650145141 +32333 4326 799825.9746359268 9557379.650145141 17.70015388888889 -3.999959656720183 +4322 32334 21.0 -40.0 500000.0000000008 5572243.953413932 +32334 4326 500000.0000000008 5572243.953413932 21.00015388888889 -39.999970736192495 +4322 32334 18.3 -76.0 427110.12140598026 1563150.3170419894 +32334 4326 427110.12140598026 1563150.3170419894 18.30015388888889 -75.99999109568321 +4322 32334 23.7 -4.0 799825.9746359258 9557379.650145141 +32334 4326 799825.9746359258 9557379.650145141 23.700153888888885 -3.9999596567201827 +4322 32335 27.0 -40.0 500000.0 5572243.953413932 +32335 4326 500000.0 5572243.953413932 27.00015388888888 -39.999970736192495 +4322 32335 24.3 -76.0 427110.1214059801 1563150.3170419894 +32335 4326 427110.1214059801 1563150.3170419894 24.30015388888889 -75.99999109568321 +4322 32335 29.7 -4.0 799825.9746359247 9557379.650145141 +32335 4326 799825.9746359247 9557379.650145141 29.700153888888888 -3.999959656720183 +4322 32336 33.0 -40.0 499999.9999999995 5572243.953413932 +32336 4326 499999.9999999995 5572243.953413932 33.00015388888889 -39.999970736192495 +4322 32336 30.3 -76.0 427110.12140597973 1563150.3170419894 +32336 4326 427110.12140597973 1563150.3170419894 30.30015388888889 -75.99999109568321 +4322 32336 35.7 -4.0 799825.9746359247 9557379.650145141 +32336 4326 799825.9746359247 9557379.650145141 35.70015388888889 -3.999959656720183 +4322 32337 39.0 -40.0 500000.0000000005 5572243.953413932 +32337 4326 500000.0000000005 5572243.953413932 39.00015388888888 -39.999970736192495 +4322 32337 36.3 -76.0 427110.12140598026 1563150.3170419894 +32337 4326 427110.12140598026 1563150.3170419894 36.300153888888886 -75.99999109568321 +4322 32337 41.7 -4.0 799825.9746359261 9557379.650145141 +32337 4326 799825.9746359261 9557379.650145141 41.70015388888889 -3.999959656720183 +4322 32338 45.0 -40.0 500000.0 5572243.953413932 +32338 4326 500000.0 5572243.953413932 45.00015388888888 -39.999970736192495 +4322 32338 42.3 -76.0 427110.1214059799 1563150.3170419894 +32338 4326 427110.1214059799 1563150.3170419894 42.300153888888886 -75.99999109568321 +4322 32338 47.7 -4.0 799825.9746359254 9557379.650145141 +32338 4326 799825.9746359254 9557379.650145141 47.700153888888885 -3.9999596567201827 +4322 32339 51.0 -40.0 500000.00000000163 5572243.953413932 +32339 4326 500000.00000000163 5572243.953413932 51.00015388888889 -39.999970736192495 +4322 32339 48.3 -76.0 427110.12140598043 1563150.3170419894 +32339 4326 427110.12140598043 1563150.3170419894 48.30015388888889 -75.99999109568321 +4322 32339 53.7 -4.0 799825.9746359276 9557379.650145141 +32339 4326 799825.9746359276 9557379.650145141 53.700153888888906 -3.999959656720183 +4322 32340 57.0 -40.0 499999.9999999989 5572243.953413932 +32340 4326 499999.9999999989 5572243.953413932 57.00015388888888 -39.999970736192495 +4322 32340 54.3 -76.0 427110.12140597956 1563150.3170419894 +32340 4326 427110.12140597956 1563150.3170419894 54.300153888888886 -75.99999109568321 +4322 32340 59.7 -4.0 799825.9746359233 9557379.650145141 +32340 4326 799825.9746359233 9557379.650145141 59.700153888888885 -3.999959656720183 +4322 32341 63.0 -40.0 500000.0 5572243.953413932 +32341 4326 500000.0 5572243.953413932 63.00015388888888 -39.999970736192495 +4322 32341 60.3 -76.0 427110.1214059801 1563150.3170419894 +32341 4326 427110.1214059801 1563150.3170419894 60.300153888888886 -75.99999109568323 +4322 32341 65.7 -4.0 799825.9746359261 9557379.650145141 +32341 4326 799825.9746359261 9557379.650145141 65.70015388888889 -3.999959656720183 +4322 32342 69.0 -40.0 499999.99999999674 5572243.953413932 +32342 4326 499999.99999999674 5572243.953413932 69.00015388888887 -39.999970736192495 +4322 32342 66.3 -76.0 427110.12140597904 1563150.3170419894 +32342 4326 427110.12140597904 1563150.3170419894 66.30015388888889 -75.99999109568323 +4322 32342 71.7 -4.0 799825.9746359219 9557379.650145141 +32342 4326 799825.9746359219 9557379.650145141 71.70015388888889 -3.999959656720183 +4322 32343 75.0 -40.0 499999.9999999989 5572243.953413932 +32343 4326 499999.9999999989 5572243.953413932 75.00015388888889 -39.999970736192495 +4322 32343 72.3 -76.0 427110.1214059794 1563150.3170419894 +32343 4326 427110.1214059794 1563150.3170419894 72.30015388888889 -75.99999109568323 +4322 32343 77.7 -4.0 799825.9746359233 9557379.650145141 +32343 4326 799825.9746359233 9557379.650145141 77.70015388888889 -3.999959656720183 +4322 32344 81.0 -40.0 500000.0 5572243.953413932 +32344 4326 500000.0 5572243.953413932 81.00015388888889 -39.999970736192495 +4322 32344 78.3 -76.0 427110.1214059801 1563150.3170419894 +32344 4326 427110.1214059801 1563150.3170419894 78.30015388888889 -75.99999109568321 +4322 32344 83.7 -4.0 799825.9746359261 9557379.650145141 +32344 4326 799825.9746359261 9557379.650145141 83.70015388888889 -3.999959656720183 +4322 32345 87.0 -40.0 499999.99999999674 5572243.953413932 +32345 4326 499999.99999999674 5572243.953413932 87.00015388888887 -39.999970736192495 +4322 32345 84.3 -76.0 427110.12140597904 1563150.3170419894 +32345 4326 427110.12140597904 1563150.3170419894 84.30015388888889 -75.99999109568321 +4322 32345 89.7 -4.0 799825.9746359219 9557379.650145141 +32345 4326 799825.9746359219 9557379.650145141 89.70015388888889 -3.999959656720183 +4322 32346 93.0 -40.0 499999.9999999989 5572243.953413932 +32346 4326 499999.9999999989 5572243.953413932 93.00015388888889 -39.999970736192495 +4322 32346 90.3 -76.0 427110.1214059794 1563150.3170419894 +32346 4326 427110.1214059794 1563150.3170419894 90.30015388888889 -75.99999109568323 +4322 32346 95.7 -4.0 799825.9746359233 9557379.650145141 +32346 4326 799825.9746359233 9557379.650145141 95.70015388888889 -3.999959656720183 +4322 32347 99.0 -40.0 500000.0 5572243.953413932 +32347 4326 500000.0 5572243.953413932 99.00015388888889 -39.999970736192495 +4322 32347 96.3 -76.0 427110.1214059801 1563150.3170419894 +32347 4326 427110.1214059801 1563150.3170419894 96.30015388888889 -75.99999109568321 +4322 32347 101.7 -4.0 799825.9746359261 9557379.650145141 +32347 4326 799825.9746359261 9557379.650145141 101.70015388888889 -3.999959656720184 +4322 32348 105.0 -40.0 499999.99999999785 5572243.953413932 +32348 4326 499999.99999999785 5572243.953413932 105.00015388888889 -39.999970736192495 +4322 32348 102.3 -76.0 427110.12140597904 1563150.3170419894 +32348 4326 427110.12140597904 1563150.3170419894 102.30015388888889 -75.99999109568321 +4322 32348 107.7 -4.0 799825.9746359219 9557379.650145141 +32348 4326 799825.9746359219 9557379.650145141 107.70015388888889 -3.999959656720183 +4322 32349 111.0 -40.0 500000.00000000326 5572243.953413932 +32349 4326 500000.00000000326 5572243.953413932 111.00015388888889 -39.999970736192495 +4322 32349 108.3 -76.0 427110.1214059808 1563150.3170419894 +32349 4326 427110.1214059808 1563150.3170419894 108.30015388888889 -75.99999109568321 +4322 32349 113.7 -4.0 799825.974635929 9557379.650145141 +32349 4326 799825.974635929 9557379.650145141 113.70015388888889 -3.9999596567201827 +4322 32350 117.0 -40.0 500000.0 5572243.953413932 +32350 4326 500000.0 5572243.953413932 117.0001538888889 -39.999970736192495 +4322 32350 114.3 -76.0 427110.1214059801 1563150.3170419894 +32350 4326 427110.1214059801 1563150.3170419894 114.30015388888889 -75.99999109568321 +4322 32350 119.7 -4.0 799825.9746359261 9557379.650145141 +32350 4326 799825.9746359261 9557379.650145141 119.70015388888888 -3.999959656720183 +4322 32351 123.0 -40.0 500000.00000000215 5572243.953413932 +32351 4326 500000.00000000215 5572243.953413932 123.0001538888889 -39.999970736192495 +4322 32351 120.3 -76.0 427110.12140598043 1563150.3170419894 +32351 4326 427110.12140598043 1563150.3170419894 120.3001538888889 -75.99999109568321 +4322 32351 125.7 -4.0 799825.9746359261 9557379.650145141 +32351 4326 799825.9746359261 9557379.650145141 125.70015388888889 -3.999959656720183 +4322 32352 129.0 -40.0 500000.00000000215 5572243.953413932 +32352 4326 500000.00000000215 5572243.953413932 129.0001538888889 -39.999970736192495 +4322 32352 126.3 -76.0 427110.12140598113 1563150.3170419894 +32352 4326 427110.12140598113 1563150.3170419894 126.30015388888887 -75.99999109568321 +4322 32352 131.7 -4.0 799825.974635929 9557379.650145141 +32352 4326 799825.974635929 9557379.650145141 131.7001538888889 -3.999959656720184 +4322 32353 135.0 -40.0 500000.0 5572243.953413932 +32353 4326 500000.0 5572243.953413932 135.0001538888889 -39.999970736192495 +4322 32353 132.3 -76.0 427110.12140598043 1563150.3170419894 +32353 4326 427110.12140598043 1563150.3170419894 132.3001538888889 -75.99999109568323 +4322 32353 137.7 -4.0 799825.9746359233 9557379.650145141 +32353 4326 799825.9746359233 9557379.650145141 137.70015388888885 -3.999959656720183 +4322 32354 141.0 -40.0 500000.00000000215 5572243.953413932 +32354 4326 500000.00000000215 5572243.953413932 141.00015388888892 -39.999970736192495 +4322 32354 138.3 -76.0 427110.12140598113 1563150.3170419894 +32354 4326 427110.12140598113 1563150.3170419894 138.30015388888893 -75.99999109568321 +4322 32354 143.7 -4.0 799825.9746359261 9557379.650145141 +32354 4326 799825.9746359261 9557379.650145141 143.70015388888888 -3.999959656720183 +4322 32355 147.0 -40.0 500000.00000000215 5572243.953413932 +32355 4326 500000.00000000215 5572243.953413932 147.0001538888889 -39.999970736192495 +4322 32355 144.3 -76.0 427110.12140598113 1563150.3170419894 +32355 4326 427110.12140598113 1563150.3170419894 144.3001538888889 -75.99999109568321 +4322 32355 149.7 -4.0 799825.974635929 9557379.650145141 +32355 4326 799825.974635929 9557379.650145141 149.7001538888889 -3.999959656720183 +4322 32356 153.0 -40.0 500000.0 5572243.953413932 +32356 4326 500000.0 5572243.953413932 153.00015388888886 -39.999970736192495 +4322 32356 150.3 -76.0 427110.12140598043 1563150.3170419894 +32356 4326 427110.12140598043 1563150.3170419894 150.3001538888889 -75.99999109568321 +4322 32356 155.7 -4.0 799825.9746359233 9557379.650145141 +32356 4326 799825.9746359233 9557379.650145141 155.70015388888888 -3.999959656720183 +4322 32357 159.0 -40.0 500000.00000000215 5572243.953413932 +32357 4326 500000.00000000215 5572243.953413932 159.00015388888892 -39.999970736192495 +4322 32357 156.3 -76.0 427110.12140598113 1563150.3170419894 +32357 4326 427110.12140598113 1563150.3170419894 156.30015388888893 -75.99999109568323 +4322 32357 161.7 -4.0 799825.9746359261 9557379.650145141 +32357 4326 799825.9746359261 9557379.650145141 161.70015388888888 -3.999959656720183 +4322 32358 165.0 -40.0 499999.99999999785 5572243.953413932 +32358 4326 499999.99999999785 5572243.953413932 165.0001538888889 -39.999970736192495 +4322 32358 162.3 -76.0 427110.12140597973 1563150.3170419894 +32358 4326 427110.12140597973 1563150.3170419894 162.3001538888889 -75.99999109568323 +4322 32358 167.7 -4.0 799825.9746359233 9557379.650145141 +32358 4326 799825.9746359233 9557379.650145141 167.7001538888889 -3.999959656720183 +4322 32359 171.0 -40.0 500000.0 5572243.953413932 +32359 4326 500000.0 5572243.953413932 171.0001538888889 -39.999970736192495 +4322 32359 168.3 -76.0 427110.12140598043 1563150.3170419894 +32359 4326 427110.12140598043 1563150.3170419894 168.3001538888889 -75.99999109568321 +4322 32359 173.7 -4.0 799825.9746359233 9557379.650145141 +32359 4326 799825.9746359233 9557379.650145141 173.70015388888888 -3.999959656720183 +4322 32360 177.0 -40.0 500000.00000000215 5572243.953413932 +32360 4326 500000.00000000215 5572243.953413932 177.00015388888892 -39.999970736192495 +4322 32360 174.3 -76.0 427110.12140598113 1563150.3170419894 +32360 4326 427110.12140598113 1563150.3170419894 174.30015388888893 -75.99999109568321 +4322 32360 179.7 -4.0 799825.9746359261 9557379.650145141 +32360 4326 799825.9746359261 9557379.650145141 179.70015388888888 -3.999959656720183 +4324 32401 -177.0 42.0 500000.0 4649774.988009868 +32401 4326 500000.0 4649774.988009868 -176.9997738888889 42.00001462641104 +4324 32401 -179.7 4.2 200248.64837286732 464751.96998849313 +32401 4326 200248.64837286732 464751.96998849313 -179.6997738888889 4.200017417596452 +4324 32401 -174.3 79.8 553358.5727386967 8860497.914284788 +32401 4326 553358.5727386967 8860497.914284788 -174.29977388888892 79.8000036829971 +4324 32402 -171.0 42.0 500000.0 4649774.988009868 +32402 4326 500000.0 4649774.988009868 -170.99977388888888 42.00001462641104 +4324 32402 -173.7 4.2 200248.64837286732 464751.96998849313 +32402 4326 200248.64837286732 464751.96998849313 -173.69977388888887 4.200017417596452 +4324 32402 -168.3 79.8 553358.5727386967 8860497.914284788 +32402 4326 553358.5727386967 8860497.914284788 -168.29977388888892 79.8000036829971 +4324 32403 -165.0 42.0 500000.0 4649774.988009868 +32403 4326 500000.0 4649774.988009868 -164.99977388888888 42.00001462641104 +4324 32403 -167.7 4.2 200248.64837286447 464751.96998849313 +32403 4326 200248.64837286447 464751.96998849313 -167.6997738888889 4.200017417596452 +4324 32403 -162.3 79.8 553358.5727386967 8860497.914284788 +32403 4326 553358.5727386967 8860497.914284788 -162.2997738888889 79.8000036829971 +4324 32404 -159.0 42.0 500000.0 4649774.988009868 +32404 4326 500000.0 4649774.988009868 -158.9997738888889 42.00001462641104 +4324 32404 -161.7 4.2 200248.64837286732 464751.96998849313 +32404 4326 200248.64837286732 464751.96998849313 -161.6997738888889 4.200017417596452 +4324 32404 -156.3 79.8 553358.5727386967 8860497.914284788 +32404 4326 553358.5727386967 8860497.914284788 -156.29977388888892 79.8000036829971 +4324 32405 -153.0 42.0 500000.0 4649774.988009868 +32405 4326 500000.0 4649774.988009868 -152.99977388888888 42.00001462641104 +4324 32405 -155.7 4.2 200248.64837286732 464751.96998849313 +32405 4326 200248.64837286732 464751.96998849313 -155.69977388888887 4.200017417596452 +4324 32405 -150.3 79.8 553358.5727386967 8860497.914284788 +32405 4326 553358.5727386967 8860497.914284788 -150.29977388888892 79.80000368299712 +4324 32406 -147.0 42.0 500000.0000000021 4649774.988009868 +32406 4326 500000.0000000021 4649774.988009868 -146.99977388888888 42.00001462641104 +4324 32406 -149.7 4.2 200248.64837286732 464751.96998849313 +32406 4326 200248.64837286732 464751.96998849313 -149.6997738888889 4.200017417596452 +4324 32406 -144.3 79.8 553358.5727386972 8860497.914284788 +32406 4326 553358.5727386972 8860497.914284788 -144.2997738888889 79.8000036829971 +4324 32407 -141.0 42.0 499999.9999999979 4649774.988009868 +32407 4326 499999.9999999979 4649774.988009868 -140.9997738888889 42.00001462641104 +4324 32407 -143.7 4.2 200248.64837286447 464751.96998849313 +32407 4326 200248.64837286447 464751.96998849313 -143.6997738888889 4.200017417596452 +4324 32407 -138.3 79.8 553358.5727386961 8860497.914284788 +32407 4326 553358.5727386961 8860497.914284788 -138.29977388888892 79.8000036829971 +4324 32408 -135.0 42.0 500000.0 4649774.988009868 +32408 4326 500000.0 4649774.988009868 -134.9997738888889 42.00001462641103 +4324 32408 -137.7 4.2 200248.64837286732 464751.96998849313 +32408 4326 200248.64837286732 464751.96998849313 -137.69977388888887 4.200017417596453 +4324 32408 -132.3 79.8 553358.5727386967 8860497.914284788 +32408 4326 553358.5727386967 8860497.914284788 -132.29977388888892 79.80000368299712 +4324 32409 -129.0 42.0 500000.0 4649774.988009868 +32409 4326 500000.0 4649774.988009868 -128.99977388888888 42.00001462641103 +4324 32409 -131.7 4.2 200248.64837286447 464751.96998849313 +32409 4326 200248.64837286447 464751.96998849313 -131.6997738888889 4.200017417596453 +4324 32409 -126.3 79.8 553358.5727386967 8860497.914284788 +32409 4326 553358.5727386967 8860497.914284788 -126.29977388888891 79.8000036829971 +4324 32410 -123.0 42.0 500000.0 4649774.988009868 +32410 4326 500000.0 4649774.988009868 -122.99977388888891 42.00001462641104 +4324 32410 -125.7 4.2 200248.64837286732 464751.96998849313 +32410 4326 200248.64837286732 464751.96998849313 -125.69977388888888 4.200017417596452 +4324 32410 -120.3 79.8 553358.5727386972 8860497.914284788 +32410 4326 553358.5727386972 8860497.914284788 -120.2997738888889 79.80000368299712 +4324 32411 -117.0 42.0 500000.0 4649774.988009868 +32411 4326 500000.0 4649774.988009868 -116.9997738888889 42.00001462641104 +4324 32411 -119.7 4.2 200248.64837286447 464751.96998849313 +32411 4326 200248.64837286447 464751.96998849313 -119.69977388888891 4.200017417596452 +4324 32411 -114.3 79.8 553358.5727386969 8860497.914284788 +32411 4326 553358.5727386969 8860497.914284788 -114.2997738888889 79.8000036829971 +4324 32412 -111.0 42.0 500000.0 4649774.988009868 +32412 4326 500000.0 4649774.988009868 -110.99977388888888 42.00001462641104 +4324 32412 -113.7 4.2 200248.64837286586 464751.96998849313 +32412 4326 200248.64837286586 464751.96998849313 -113.69977388888888 4.200017417596452 +4324 32412 -108.3 79.8 553358.5727386972 8860497.914284788 +32412 4326 553358.5727386972 8860497.914284788 -108.2997738888889 79.8000036829971 +4324 32413 -105.0 42.0 499999.99999999895 4649774.988009868 +32413 4326 499999.99999999895 4649774.988009868 -104.9997738888889 42.00001462641104 +4324 32413 -107.7 4.2 200248.64837286447 464751.96998849313 +32413 4326 200248.64837286447 464751.96998849313 -107.69977388888888 4.200017417596452 +4324 32413 -102.3 79.8 553358.5727386969 8860497.914284788 +32413 4326 553358.5727386969 8860497.914284788 -102.2997738888889 79.80000368299712 +4324 32414 -99.0 42.0 500000.0 4649774.988009868 +32414 4326 500000.0 4649774.988009868 -98.9997738888889 42.00001462641104 +4324 32414 -101.7 4.2 200248.64837286447 464751.96998849313 +32414 4326 200248.64837286447 464751.96998849313 -101.69977388888891 4.200017417596452 +4324 32414 -96.3 79.8 553358.5727386969 8860497.914284788 +32414 4326 553358.5727386969 8860497.914284788 -96.29977388888888 79.8000036829971 +4324 32415 -93.0 42.0 499999.99999999895 4649774.988009868 +32415 4326 499999.99999999895 4649774.988009868 -92.9997738888889 42.00001462641104 +4324 32415 -95.7 4.2 200248.64837286447 464751.96998849313 +32415 4326 200248.64837286447 464751.96998849313 -95.6997738888889 4.200017417596452 +4324 32415 -90.3 79.8 553358.5727386969 8860497.914284788 +32415 4326 553358.5727386969 8860497.914284788 -90.29977388888888 79.80000368299712 +4324 32416 -87.0 42.0 500000.00000000105 4649774.988009868 +32416 4326 500000.00000000105 4649774.988009868 -86.99977388888888 42.00001462641104 +4324 32416 -89.7 4.2 200248.64837286586 464751.96998849313 +32416 4326 200248.64837286586 464751.96998849313 -89.69977388888888 4.200017417596453 +4324 32416 -84.3 79.8 553358.5727386972 8860497.914284788 +32416 4326 553358.5727386972 8860497.914284788 -84.29977388888888 79.80000368299712 +4324 32417 -81.0 42.0 500000.0 4649774.988009868 +32417 4326 500000.0 4649774.988009868 -80.9997738888889 42.00001462641104 +4324 32417 -83.7 4.2 200248.64837286447 464751.96998849313 +32417 4326 200248.64837286447 464751.96998849313 -83.69977388888891 4.200017417596452 +4324 32417 -78.3 79.8 553358.5727386969 8860497.914284788 +32417 4326 553358.5727386969 8860497.914284788 -78.2997738888889 79.8000036829971 +4324 32418 -75.0 42.0 500000.0 4649774.988009868 +32418 4326 500000.0 4649774.988009868 -74.99977388888888 42.00001462641104 +4324 32418 -77.7 4.2 200248.64837286586 464751.96998849313 +32418 4326 200248.64837286586 464751.96998849313 -77.6997738888889 4.200017417596452 +4324 32418 -72.3 79.8 553358.5727386972 8860497.914284788 +32418 4326 553358.5727386972 8860497.914284788 -72.2997738888889 79.8000036829971 +4324 32419 -69.0 42.0 500000.0 4649774.988009868 +32419 4326 500000.0 4649774.988009868 -68.9997738888889 42.00001462641104 +4324 32419 -71.7 4.2 200248.64837286447 464751.96998849313 +32419 4326 200248.64837286447 464751.96998849313 -71.69977388888888 4.200017417596452 +4324 32419 -66.3 79.8 553358.5727386969 8860497.914284788 +32419 4326 553358.5727386969 8860497.914284788 -66.29977388888888 79.8000036829971 +4324 32420 -63.0 42.0 500000.0 4649774.988009868 +32420 4326 500000.0 4649774.988009868 -62.999773888888896 42.00001462641104 +4324 32420 -65.7 4.2 200248.64837286447 464751.96998849313 +32420 4326 200248.64837286447 464751.96998849313 -65.6997738888889 4.200017417596453 +4324 32420 -60.3 79.8 553358.5727386969 8860497.914284788 +32420 4326 553358.5727386969 8860497.914284788 -60.299773888888886 79.80000368299712 +4324 32421 -57.0 42.0 500000.00000000105 4649774.988009868 +32421 4326 500000.00000000105 4649774.988009868 -56.999773888888896 42.00001462641104 +4324 32421 -59.7 4.2 200248.64837286732 464751.96998849313 +32421 4326 200248.64837286732 464751.96998849313 -59.699773888888885 4.200017417596452 +4324 32421 -54.3 79.8 553358.5727386973 8860497.914284788 +32421 4326 553358.5727386973 8860497.914284788 -54.29977388888889 79.8000036829971 +4324 32422 -51.0 42.0 500000.0000000005 4649774.988009868 +32422 4326 500000.0000000005 4649774.988009868 -50.99977388888889 42.00001462641104 +4324 32422 -53.7 4.2 200248.64837286586 464751.96998849313 +32422 4326 200248.64837286586 464751.96998849313 -53.6997738888889 4.200017417596452 +4324 32422 -48.3 79.8 553358.5727386972 8860497.914284788 +32422 4326 553358.5727386972 8860497.914284788 -48.299773888888886 79.8000036829971 +4324 32423 -45.0 42.0 500000.0 4649774.988009868 +32423 4326 500000.0 4649774.988009868 -44.99977388888888 42.00001462641103 +4324 32423 -47.7 4.2 200248.64837286517 464751.96998849313 +32423 4326 200248.64837286517 464751.96998849313 -47.699773888888885 4.200017417596453 +4324 32423 -42.3 79.8 553358.572738697 8860497.914284788 +32423 4326 553358.572738697 8860497.914284788 -42.299773888888886 79.80000368299712 +4324 32424 -39.0 42.0 499999.9999999995 4649774.988009868 +32424 4326 499999.9999999995 4649774.988009868 -38.99977388888889 42.00001462641104 +4324 32424 -41.7 4.2 200248.64837286447 464751.96998849313 +32424 4326 200248.64837286447 464751.96998849313 -41.69977388888889 4.200017417596452 +4324 32424 -36.3 79.8 553358.5727386968 8860497.914284788 +32424 4326 553358.5727386968 8860497.914284788 -36.29977388888889 79.8000036829971 +4324 32425 -33.0 42.0 500000.0000000005 4649774.988009868 +32425 4326 500000.0000000005 4649774.988009868 -32.99977388888889 42.00001462641104 +4324 32425 -35.7 4.2 200248.64837286586 464751.96998849313 +32425 4326 200248.64837286586 464751.96998849313 -35.6997738888889 4.200017417596452 +4324 32425 -30.3 79.8 553358.5727386972 8860497.914284788 +32425 4326 553358.5727386972 8860497.914284788 -30.299773888888893 79.80000368299712 +4324 32426 -27.0 42.0 500000.0 4649774.988009868 +32426 4326 500000.0 4649774.988009868 -26.99977388888889 42.00001462641104 +4324 32426 -29.7 4.2 200248.64837286586 464751.96998849313 +32426 4326 200248.64837286586 464751.96998849313 -29.69977388888889 4.200017417596452 +4324 32426 -24.3 79.8 553358.5727386969 8860497.914284788 +32426 4326 553358.5727386969 8860497.914284788 -24.299773888888897 79.8000036829971 +4324 32427 -21.0 42.0 499999.9999999992 4649774.988009868 +32427 4326 499999.9999999992 4649774.988009868 -20.99977388888889 42.00001462641104 +4324 32427 -23.7 4.2 200248.64837286482 464751.96998849313 +32427 4326 200248.64837286482 464751.96998849313 -23.69977388888889 4.200017417596452 +4324 32427 -18.3 79.8 553358.5727386968 8860497.914284788 +32427 4326 553358.5727386968 8860497.914284788 -18.299773888888893 79.8000036829971 +4324 32428 -15.0 42.0 499999.99999999866 4649774.988009868 +32428 4326 499999.99999999866 4649774.988009868 -14.999773888888889 42.00001462641104 +4324 32428 -17.7 4.2 200248.64837286377 464751.96998849313 +32428 4326 200248.64837286377 464751.96998849313 -17.69977388888889 4.200017417596452 +4324 32428 -12.3 79.8 553358.5727386966 8860497.914284788 +32428 4326 553358.5727386966 8860497.914284788 -12.299773888888895 79.80000368299712 +4324 32429 -9.0 42.0 500000.0 4649774.988009868 +32429 4326 500000.0 4649774.988009868 -8.99977388888889 42.00001462641104 +4324 32429 -11.7 4.2 200248.6483728657 464751.96998849313 +32429 4326 200248.6483728657 464751.96998849313 -11.699773888888892 4.200017417596453 +4324 32429 -6.300000000000001 79.8 553358.5727386969 8860497.914284788 +32429 4326 553358.5727386969 8860497.914284788 -6.299773888888893 79.8000036829971 +4324 32430 -3.0 42.0 499999.9999999993 4649774.988009868 +32430 4326 499999.9999999993 4649774.988009868 -2.9997738888888903 42.00001462641104 +4324 32430 -5.7 4.2 200248.64837286464 464751.96998849313 +32430 4326 200248.64837286464 464751.96998849313 -5.69977388888889 4.200017417596452 +4324 32430 -0.3000000000000007 79.8 553358.5727386968 8860497.914284788 +32430 4326 553358.5727386968 8860497.914284788 -0.29977388888888973 79.80000368299712 +4324 32431 3.0 42.0 500000.0000000007 4649774.988009868 +32431 4326 500000.0000000007 4649774.988009868 3.0002261111111097 42.00001462641104 +4324 32431 0.30000000000000004 4.2 200248.64837286656 464751.96998849313 +32431 4326 200248.64837286656 464751.96998849313 0.3002261111111103 4.200017417596453 +4324 32431 5.699999999999999 79.8 553358.5727386972 8860497.914284788 +32431 4326 553358.5727386972 8860497.914284788 5.70022611111111 79.8000036829971 +4324 32432 9.0 42.0 500000.0 4649774.988009868 +32432 4326 500000.0 4649774.988009868 9.00022611111111 42.00001462641104 +4324 32432 6.3 4.2 200248.64837286563 464751.96998849313 +32432 4326 200248.64837286563 464751.96998849313 6.300226111111111 4.200017417596452 +4324 32432 11.7 79.8 553358.5727386969 8860497.914284788 +32432 4326 553358.5727386969 8860497.914284788 11.700226111111109 79.8000036829971 +4324 32433 15.0 42.0 500000.00000000134 4649774.988009868 +32433 4326 500000.00000000134 4649774.988009868 15.00022611111111 42.00001462641104 +4324 32433 12.3 4.2 200248.6483728676 464751.96998849313 +32433 4326 200248.6483728676 464751.96998849313 12.30022611111111 4.200017417596452 +4324 32433 17.7 79.8 553358.5727386973 8860497.914284788 +32433 4326 553358.5727386973 8860497.914284788 17.70022611111111 79.8000036829971 +4324 32434 21.0 42.0 500000.0000000008 4649774.988009868 +32434 4326 500000.0000000008 4649774.988009868 21.000226111111115 42.00001462641104 +4324 32434 18.3 4.2 200248.64837286662 464751.96998849313 +32434 4326 200248.64837286662 464751.96998849313 18.300226111111108 4.200017417596452 +4324 32434 23.7 79.8 553358.572738697 8860497.914284788 +32434 4326 553358.572738697 8860497.914284788 23.700226111111107 79.8000036829971 +4324 32435 27.0 42.0 500000.0 4649774.988009868 +32435 4326 500000.0 4649774.988009868 27.00022611111111 42.00001462641104 +4324 32435 24.3 4.2 200248.64837286586 464751.96998849313 +32435 4326 200248.64837286586 464751.96998849313 24.30022611111111 4.200017417596452 +4324 32435 29.7 79.8 553358.5727386969 8860497.914284788 +32435 4326 553358.5727386969 8860497.914284788 29.7002261111111 79.80000368299712 +4324 32436 33.0 42.0 499999.9999999995 4649774.988009868 +32436 4326 499999.9999999995 4649774.988009868 33.00022611111111 42.00001462641104 +4324 32436 30.3 4.2 200248.64837286447 464751.96998849313 +32436 4326 200248.64837286447 464751.96998849313 30.300226111111115 4.200017417596452 +4324 32436 35.7 79.8 553358.5727386969 8860497.914284788 +32436 4326 553358.5727386969 8860497.914284788 35.70022611111111 79.8000036829971 +4324 32437 39.0 42.0 500000.0000000005 4649774.988009868 +32437 4326 500000.0000000005 4649774.988009868 39.00022611111111 42.00001462641104 +4324 32437 36.3 4.2 200248.64837286662 464751.96998849313 +32437 4326 200248.64837286662 464751.96998849313 36.30022611111111 4.200017417596452 +4324 32437 41.7 79.8 553358.5727386972 8860497.914284788 +32437 4326 553358.5727386972 8860497.914284788 41.70022611111112 79.8000036829971 +4324 32438 45.0 42.0 500000.0 4649774.988009868 +32438 4326 500000.0 4649774.988009868 45.00022611111111 42.00001462641104 +4324 32438 42.3 4.2 200248.64837286517 464751.96998849313 +32438 4326 200248.64837286517 464751.96998849313 42.30022611111111 4.200017417596452 +4324 32438 47.7 79.8 553358.572738697 8860497.914284788 +32438 4326 553358.572738697 8860497.914284788 47.700226111111114 79.80000368299712 +4324 32439 51.0 42.0 500000.0000000016 4649774.988009868 +32439 4326 500000.0000000016 4649774.988009868 51.00022611111111 42.00001462641104 +4324 32439 48.3 4.2 200248.64837286732 464751.96998849313 +32439 4326 200248.64837286732 464751.96998849313 48.30022611111111 4.200017417596452 +4324 32439 53.7 79.8 553358.5727386974 8860497.914284788 +32439 4326 553358.5727386974 8860497.914284788 53.70022611111112 79.8000036829971 +4324 32440 57.0 42.0 499999.99999999895 4649774.988009868 +32440 4326 499999.99999999895 4649774.988009868 57.000226111111104 42.00001462641104 +4324 32440 54.3 4.2 200248.64837286377 464751.96998849313 +32440 4326 200248.64837286377 464751.96998849313 54.3002261111111 4.200017417596452 +4324 32440 59.7 79.8 553358.5727386967 8860497.914284788 +32440 4326 553358.5727386967 8860497.914284788 59.70022611111111 79.80000368299712 +4324 32441 63.0 42.0 500000.0 4649774.988009868 +32441 4326 500000.0 4649774.988009868 63.00022611111112 42.00001462641104 +4324 32441 60.3 4.2 200248.64837286586 464751.96998849313 +32441 4326 200248.64837286586 464751.96998849313 60.300226111111115 4.200017417596453 +4324 32441 65.7 79.8 553358.5727386972 8860497.914284788 +32441 4326 553358.5727386972 8860497.914284788 65.70022611111112 79.8000036829971 +4324 32442 69.0 42.0 499999.99999999686 4649774.988009868 +32442 4326 499999.99999999686 4649774.988009868 69.00022611111112 42.00001462641104 +4324 32442 66.3 4.2 200248.64837286161 464751.9699884932 +32442 4326 200248.64837286161 464751.9699884932 66.3002261111111 4.200017417596453 +4324 32442 71.7 79.8 553358.5727386965 8860497.914284788 +32442 4326 553358.5727386965 8860497.914284788 71.7002261111111 79.8000036829971 +4324 32443 75.0 42.0 499999.99999999895 4649774.988009868 +32443 4326 499999.99999999895 4649774.988009868 75.00022611111112 42.00001462641104 +4324 32443 72.3 4.2 200248.64837286295 464751.9699884932 +32443 4326 200248.64837286295 464751.9699884932 72.3002261111111 4.200017417596453 +4324 32443 77.7 79.8 553358.5727386967 8860497.914284788 +32443 4326 553358.5727386967 8860497.914284788 77.7002261111111 79.80000368299712 +4324 32444 81.0 42.0 500000.0 4649774.988009868 +32444 4326 500000.0 4649774.988009868 81.00022611111112 42.00001462641104 +4324 32444 78.3 4.2 200248.64837286586 464751.96998849313 +32444 4326 200248.64837286586 464751.96998849313 78.3002261111111 4.200017417596453 +4324 32444 83.7 79.8 553358.5727386972 8860497.914284788 +32444 4326 553358.5727386972 8860497.914284788 83.7002261111111 79.8000036829971 +4324 32445 87.0 42.0 499999.99999999686 4649774.988009868 +32445 4326 499999.99999999686 4649774.988009868 87.0002261111111 42.00001462641104 +4324 32445 84.3 4.2 200248.64837286161 464751.9699884932 +32445 4326 200248.64837286161 464751.9699884932 84.3002261111111 4.200017417596453 +4324 32445 89.7 79.8 553358.5727386965 8860497.914284788 +32445 4326 553358.5727386965 8860497.914284788 89.70022611111112 79.80000368299712 +4324 32446 93.0 42.0 499999.99999999895 4649774.988009868 +32446 4326 499999.99999999895 4649774.988009868 93.00022611111112 42.00001462641104 +4324 32446 90.3 4.2 200248.64837286295 464751.9699884932 +32446 4326 200248.64837286295 464751.9699884932 90.30022611111112 4.200017417596453 +4324 32446 95.7 79.8 553358.5727386967 8860497.914284788 +32446 4326 553358.5727386967 8860497.914284788 95.70022611111112 79.8000036829971 +4324 32447 99.0 42.0 500000.0 4649774.988009868 +32447 4326 500000.0 4649774.988009868 99.00022611111112 42.00001462641104 +4324 32447 96.3 4.2 200248.64837286586 464751.96998849313 +32447 4326 200248.64837286586 464751.96998849313 96.3002261111111 4.200017417596452 +4324 32447 101.7 79.8 553358.5727386972 8860497.914284788 +32447 4326 553358.5727386972 8860497.914284788 101.70022611111112 79.8000036829971 +4324 32448 105.0 42.0 499999.9999999979 4649774.988009868 +32448 4326 499999.9999999979 4649774.988009868 105.00022611111112 42.00001462641104 +4324 32448 102.3 4.2 200248.64837286161 464751.9699884932 +32448 4326 200248.64837286161 464751.9699884932 102.3002261111111 4.200017417596452 +4324 32448 107.7 79.8 553358.5727386965 8860497.914284788 +32448 4326 553358.5727386965 8860497.914284788 107.70022611111112 79.8000036829971 +4324 32449 111.0 42.0 500000.00000000314 4649774.988009868 +32449 4326 500000.00000000314 4649774.988009868 111.00022611111112 42.00001462641104 +4324 32449 108.3 4.2 200248.64837286866 464751.96998849313 +32449 4326 200248.64837286866 464751.96998849313 108.30022611111112 4.200017417596452 +4324 32449 113.7 79.8 553358.5727386976 8860497.914284788 +32449 4326 553358.5727386976 8860497.914284788 113.7002261111111 79.8000036829971 +4324 32450 117.0 42.0 500000.0 4649774.988009868 +32450 4326 500000.0 4649774.988009868 117.0002261111111 42.00001462641104 +4324 32450 114.3 4.2 200248.64837286586 464751.96998849313 +32450 4326 200248.64837286586 464751.96998849313 114.30022611111112 4.200017417596453 +4324 32450 119.7 79.8 553358.5727386972 8860497.914284788 +32450 4326 553358.5727386972 8860497.914284788 119.7002261111111 79.80000368299712 +4324 32451 123.0 42.0 500000.0000000021 4649774.988009868 +32451 4326 500000.0000000021 4649774.988009868 123.00022611111112 42.00001462641104 +4324 32451 120.3 4.2 200248.64837286732 464751.96998849313 +32451 4326 200248.64837286732 464751.96998849313 120.3002261111111 4.200017417596452 +4324 32451 125.7 79.8 553358.5727386972 8860497.914284788 +32451 4326 553358.5727386972 8860497.914284788 125.70022611111109 79.8000036829971 +4324 32452 129.0 42.0 500000.0000000021 4649774.988009868 +32452 4326 500000.0000000021 4649774.988009868 129.0002261111111 42.00001462641104 +4324 32452 126.3 4.2 200248.6483728701 464751.96998849313 +32452 4326 200248.6483728701 464751.96998849313 126.30022611111112 4.200017417596452 +4324 32452 131.7 79.8 553358.5727386976 8860497.914284788 +32452 4326 553358.5727386976 8860497.914284788 131.7002261111111 79.8000036829971 +4324 32453 135.0 42.0 500000.0 4649774.988009868 +32453 4326 500000.0 4649774.988009868 135.0002261111111 42.00001462641104 +4324 32453 132.3 4.2 200248.64837286732 464751.96998849313 +32453 4326 200248.64837286732 464751.96998849313 132.30022611111113 4.200017417596453 +4324 32453 137.7 79.8 553358.5727386967 8860497.914284788 +32453 4326 553358.5727386967 8860497.914284788 137.70022611111108 79.80000368299712 +4324 32454 141.0 42.0 500000.0000000021 4649774.988009868 +32454 4326 500000.0000000021 4649774.988009868 141.00022611111112 42.00001462641104 +4324 32454 138.3 4.2 200248.6483728701 464751.96998849313 +32454 4326 200248.6483728701 464751.96998849313 138.30022611111113 4.200017417596453 +4324 32454 143.7 79.8 553358.5727386972 8860497.914284788 +32454 4326 553358.5727386972 8860497.914284788 143.7002261111111 79.8000036829971 +4324 32455 147.0 42.0 500000.0000000021 4649774.988009868 +32455 4326 500000.0000000021 4649774.988009868 147.0002261111111 42.00001462641104 +4324 32455 144.3 4.2 200248.6483728701 464751.96998849313 +32455 4326 200248.6483728701 464751.96998849313 144.3002261111111 4.200017417596452 +4324 32455 149.7 79.8 553358.5727386976 8860497.914284788 +32455 4326 553358.5727386976 8860497.914284788 149.7002261111111 79.8000036829971 +4324 32456 153.0 42.0 500000.0 4649774.988009868 +32456 4326 500000.0 4649774.988009868 153.0002261111111 42.00001462641104 +4324 32456 150.3 4.2 200248.64837286732 464751.96998849313 +32456 4326 200248.64837286732 464751.96998849313 150.30022611111113 4.200017417596452 +4324 32456 155.7 79.8 553358.5727386967 8860497.914284788 +32456 4326 553358.5727386967 8860497.914284788 155.70022611111108 79.8000036829971 +4324 32457 159.0 42.0 500000.0000000021 4649774.988009868 +32457 4326 500000.0000000021 4649774.988009868 159.00022611111112 42.00001462641104 +4324 32457 156.3 4.2 200248.6483728701 464751.96998849313 +32457 4326 200248.6483728701 464751.96998849313 156.30022611111113 4.200017417596453 +4324 32457 161.7 79.8 553358.5727386972 8860497.914284788 +32457 4326 553358.5727386972 8860497.914284788 161.7002261111111 79.8000036829971 +4324 32458 165.0 42.0 499999.9999999979 4649774.988009868 +32458 4326 499999.9999999979 4649774.988009868 165.0002261111111 42.00001462641104 +4324 32458 162.3 4.2 200248.64837286447 464751.96998849313 +32458 4326 200248.64837286447 464751.96998849313 162.3002261111111 4.200017417596452 +4324 32458 167.7 79.8 553358.5727386967 8860497.914284788 +32458 4326 553358.5727386967 8860497.914284788 167.7002261111111 79.80000368299712 +4324 32459 171.0 42.0 500000.0 4649774.988009868 +32459 4326 500000.0 4649774.988009868 171.0002261111111 42.000014626411044 +4324 32459 168.3 4.2 200248.64837286732 464751.96998849313 +32459 4326 200248.64837286732 464751.96998849313 168.30022611111113 4.200017417596452 +4324 32459 173.7 79.8 553358.5727386967 8860497.914284788 +32459 4326 553358.5727386967 8860497.914284788 173.70022611111108 79.8000036829971 +4324 32460 177.0 42.0 500000.0000000021 4649774.988009868 +32460 4326 500000.0000000021 4649774.988009868 177.00022611111112 42.00001462641104 +4324 32460 174.3 4.2 200248.6483728701 464751.96998849313 +32460 4326 200248.6483728701 464751.96998849313 174.30022611111113 4.200017417596452 +4324 32460 179.7 79.8 553358.5727386972 8860497.914284788 +32460 4326 553358.5727386972 8860497.914284788 179.7002261111111 79.80000368299712 +4324 32501 -177.0 -40.0 500000.0 5572243.953413932 +32501 4326 500000.0 5572243.953413932 -176.9997738888889 -39.99998878744399 +4324 32501 -179.7 -76.0 427110.12140598043 1563150.3170419894 +32501 4326 427110.12140598043 1563150.3170419894 -179.6997738888889 -75.99999678434553 +4324 32501 -174.3 -4.0 799825.9746359233 9557379.650145141 +32501 4326 799825.9746359233 9557379.650145141 -174.29977388888892 -3.999983127974191 +4324 32502 -171.0 -40.0 500000.0 5572243.953413932 +32502 4326 500000.0 5572243.953413932 -170.99977388888888 -39.99998878744399 +4324 32502 -173.7 -76.0 427110.12140598043 1563150.3170419894 +32502 4326 427110.12140598043 1563150.3170419894 -173.69977388888887 -75.99999678434553 +4324 32502 -168.3 -4.0 799825.9746359233 9557379.650145141 +32502 4326 799825.9746359233 9557379.650145141 -168.29977388888892 -3.99998312797419 +4324 32503 -165.0 -40.0 500000.0 5572243.953413932 +32503 4326 500000.0 5572243.953413932 -164.99977388888888 -39.99998878744399 +4324 32503 -167.7 -76.0 427110.12140597973 1563150.3170419894 +32503 4326 427110.12140597973 1563150.3170419894 -167.6997738888889 -75.99999678434553 +4324 32503 -162.3 -4.0 799825.9746359233 9557379.650145141 +32503 4326 799825.9746359233 9557379.650145141 -162.2997738888889 -3.999983127974191 +4324 32504 -159.0 -40.0 500000.0 5572243.953413932 +32504 4326 500000.0 5572243.953413932 -158.9997738888889 -39.99998878744399 +4324 32504 -161.7 -76.0 427110.12140598043 1563150.3170419894 +32504 4326 427110.12140598043 1563150.3170419894 -161.6997738888889 -75.99999678434553 +4324 32504 -156.3 -4.0 799825.9746359233 9557379.650145141 +32504 4326 799825.9746359233 9557379.650145141 -156.29977388888892 -3.999983127974191 +4324 32505 -153.0 -40.0 500000.0 5572243.953413932 +32505 4326 500000.0 5572243.953413932 -152.99977388888888 -39.99998878744399 +4324 32505 -155.7 -76.0 427110.12140598043 1563150.3170419894 +32505 4326 427110.12140598043 1563150.3170419894 -155.69977388888887 -75.99999678434553 +4324 32505 -150.3 -4.0 799825.9746359233 9557379.650145141 +32505 4326 799825.9746359233 9557379.650145141 -150.29977388888892 -3.9999831279741915 +4324 32506 -147.0 -40.0 500000.00000000215 5572243.953413932 +32506 4326 500000.00000000215 5572243.953413932 -146.99977388888888 -39.99998878744399 +4324 32506 -149.7 -76.0 427110.12140598043 1563150.3170419894 +32506 4326 427110.12140598043 1563150.3170419894 -149.6997738888889 -75.99999678434553 +4324 32506 -144.3 -4.0 799825.9746359261 9557379.650145141 +32506 4326 799825.9746359261 9557379.650145141 -144.2997738888889 -3.999983127974191 +4324 32507 -141.0 -40.0 499999.99999999785 5572243.953413932 +32507 4326 499999.99999999785 5572243.953413932 -140.9997738888889 -39.999988787443996 +4324 32507 -143.7 -76.0 427110.12140597973 1563150.3170419894 +32507 4326 427110.12140597973 1563150.3170419894 -143.69977388888887 -75.99999678434553 +4324 32507 -138.3 -4.0 799825.9746359205 9557379.650145141 +32507 4326 799825.9746359205 9557379.650145141 -138.29977388888892 -3.9999831279741915 +4324 32508 -135.0 -40.0 500000.0 5572243.953413932 +32508 4326 500000.0 5572243.953413932 -134.9997738888889 -39.99998878744399 +4324 32508 -137.7 -76.0 427110.12140598043 1563150.3170419894 +32508 4326 427110.12140598043 1563150.3170419894 -137.69977388888887 -75.99999678434553 +4324 32508 -132.3 -4.0 799825.9746359233 9557379.650145141 +32508 4326 799825.9746359233 9557379.650145141 -132.29977388888892 -3.9999831279741915 +4324 32509 -129.0 -40.0 500000.0 5572243.953413932 +32509 4326 500000.0 5572243.953413932 -128.99977388888888 -39.99998878744399 +4324 32509 -131.7 -76.0 427110.12140597973 1563150.3170419894 +32509 4326 427110.12140597973 1563150.3170419894 -131.6997738888889 -75.99999678434553 +4324 32509 -126.3 -4.0 799825.9746359233 9557379.650145141 +32509 4326 799825.9746359233 9557379.650145141 -126.29977388888891 -3.99998312797419 +4324 32510 -123.0 -40.0 500000.0 5572243.953413932 +32510 4326 500000.0 5572243.953413932 -122.99977388888891 -39.99998878744399 +4324 32510 -125.7 -76.0 427110.12140598043 1563150.3170419894 +32510 4326 427110.12140598043 1563150.3170419894 -125.69977388888888 -75.99999678434553 +4324 32510 -120.3 -4.0 799825.9746359261 9557379.650145141 +32510 4326 799825.9746359261 9557379.650145141 -120.2997738888889 -3.9999831279741915 +4324 32511 -117.0 -40.0 500000.0 5572243.953413932 +32511 4326 500000.0 5572243.953413932 -116.9997738888889 -39.99998878744399 +4324 32511 -119.7 -76.0 427110.12140597973 1563150.3170419894 +32511 4326 427110.12140597973 1563150.3170419894 -119.69977388888891 -75.99999678434553 +4324 32511 -114.3 -4.0 799825.9746359247 9557379.650145141 +32511 4326 799825.9746359247 9557379.650145141 -114.29977388888888 -3.999983127974191 +4324 32512 -111.0 -40.0 500000.0 5572243.953413932 +32512 4326 500000.0 5572243.953413932 -110.99977388888888 -39.99998878744399 +4324 32512 -113.7 -76.0 427110.1214059801 1563150.3170419894 +32512 4326 427110.1214059801 1563150.3170419894 -113.6997738888889 -75.99999678434553 +4324 32512 -108.3 -4.0 799825.9746359261 9557379.650145141 +32512 4326 799825.9746359261 9557379.650145141 -108.29977388888888 -3.999983127974191 +4324 32513 -105.0 -40.0 499999.9999999989 5572243.953413932 +32513 4326 499999.9999999989 5572243.953413932 -104.9997738888889 -39.99998878744399 +4324 32513 -107.7 -76.0 427110.12140597973 1563150.3170419894 +32513 4326 427110.12140597973 1563150.3170419894 -107.6997738888889 -75.99999678434553 +4324 32513 -102.3 -4.0 799825.9746359247 9557379.650145141 +32513 4326 799825.9746359247 9557379.650145141 -102.29977388888888 -3.9999831279741915 +4324 32514 -99.0 -40.0 500000.0 5572243.953413932 +32514 4326 500000.0 5572243.953413932 -98.99977388888888 -39.99998878744399 +4324 32514 -101.7 -76.0 427110.12140597973 1563150.3170419894 +32514 4326 427110.12140597973 1563150.3170419894 -101.6997738888889 -75.99999678434553 +4324 32514 -96.3 -4.0 799825.9746359247 9557379.650145141 +32514 4326 799825.9746359247 9557379.650145141 -96.29977388888888 -3.99998312797419 +4324 32515 -93.0 -40.0 499999.9999999989 5572243.953413932 +32515 4326 499999.9999999989 5572243.953413932 -92.99977388888888 -39.99998878744399 +4324 32515 -95.7 -76.0 427110.12140597973 1563150.3170419894 +32515 4326 427110.12140597973 1563150.3170419894 -95.69977388888888 -75.99999678434553 +4324 32515 -90.3 -4.0 799825.9746359247 9557379.650145141 +32515 4326 799825.9746359247 9557379.650145141 -90.29977388888888 -3.999983127974191 +4324 32516 -87.0 -40.0 500000.0000000011 5572243.953413932 +32516 4326 500000.0000000011 5572243.953413932 -86.99977388888888 -39.99998878744399 +4324 32516 -89.7 -76.0 427110.1214059801 1563150.3170419894 +32516 4326 427110.1214059801 1563150.3170419894 -89.69977388888888 -75.99999678434553 +4324 32516 -84.3 -4.0 799825.9746359261 9557379.650145141 +32516 4326 799825.9746359261 9557379.650145141 -84.2997738888889 -3.9999831279741915 +4324 32517 -81.0 -40.0 500000.0 5572243.953413932 +32517 4326 500000.0 5572243.953413932 -80.99977388888888 -39.99998878744399 +4324 32517 -83.7 -76.0 427110.12140597973 1563150.3170419894 +32517 4326 427110.12140597973 1563150.3170419894 -83.6997738888889 -75.99999678434553 +4324 32517 -78.3 -4.0 799825.9746359247 9557379.650145141 +32517 4326 799825.9746359247 9557379.650145141 -78.2997738888889 -3.999983127974191 +4324 32518 -75.0 -40.0 500000.0 5572243.953413932 +32518 4326 500000.0 5572243.953413932 -74.9997738888889 -39.99998878744399 +4324 32518 -77.7 -76.0 427110.1214059801 1563150.3170419894 +32518 4326 427110.1214059801 1563150.3170419894 -77.6997738888889 -75.99999678434553 +4324 32518 -72.3 -4.0 799825.9746359261 9557379.650145141 +32518 4326 799825.9746359261 9557379.650145141 -72.2997738888889 -3.99998312797419 +4324 32519 -69.0 -40.0 500000.0 5572243.953413932 +32519 4326 500000.0 5572243.953413932 -68.99977388888888 -39.99998878744399 +4324 32519 -71.7 -76.0 427110.12140597973 1563150.3170419894 +32519 4326 427110.12140597973 1563150.3170419894 -71.6997738888889 -75.99999678434553 +4324 32519 -66.3 -4.0 799825.9746359247 9557379.650145141 +32519 4326 799825.9746359247 9557379.650145141 -66.2997738888889 -3.999983127974191 +4324 32520 -63.0 -40.0 500000.0 5572243.953413932 +32520 4326 500000.0 5572243.953413932 -62.99977388888888 -39.99998878744399 +4324 32520 -65.7 -76.0 427110.12140597973 1563150.3170419894 +32520 4326 427110.12140597973 1563150.3170419894 -65.6997738888889 -75.99999678434553 +4324 32520 -60.3 -4.0 799825.9746359247 9557379.650145141 +32520 4326 799825.9746359247 9557379.650145141 -60.299773888888886 -3.999983127974191 +4324 32521 -57.0 -40.0 500000.0000000011 5572243.953413932 +32521 4326 500000.0000000011 5572243.953413932 -56.99977388888889 -39.99998878744399 +4324 32521 -59.7 -76.0 427110.12140598043 1563150.3170419894 +32521 4326 427110.12140598043 1563150.3170419894 -59.6997738888889 -75.99999678434553 +4324 32521 -54.3 -4.0 799825.9746359268 9557379.650145141 +32521 4326 799825.9746359268 9557379.650145141 -54.29977388888889 -3.99998312797419 +4324 32522 -51.0 -40.0 500000.0000000005 5572243.953413932 +32522 4326 500000.0000000005 5572243.953413932 -50.99977388888889 -39.99998878744399 +4324 32522 -53.7 -76.0 427110.1214059801 1563150.3170419894 +32522 4326 427110.1214059801 1563150.3170419894 -53.6997738888889 -75.99999678434553 +4324 32522 -48.3 -4.0 799825.9746359261 9557379.650145141 +32522 4326 799825.9746359261 9557379.650145141 -48.299773888888886 -3.999983127974191 +4324 32523 -45.0 -40.0 500000.0 5572243.953413932 +32523 4326 500000.0 5572243.953413932 -44.999773888888896 -39.99998878744399 +4324 32523 -47.7 -76.0 427110.1214059799 1563150.3170419894 +32523 4326 427110.1214059799 1563150.3170419894 -47.699773888888885 -75.99999678434553 +4324 32523 -42.3 -4.0 799825.9746359254 9557379.650145141 +32523 4326 799825.9746359254 9557379.650145141 -42.299773888888886 -3.999983127974191 +4324 32524 -39.0 -40.0 499999.9999999995 5572243.953413932 +32524 4326 499999.9999999995 5572243.953413932 -38.99977388888888 -39.99998878744399 +4324 32524 -41.7 -76.0 427110.12140597973 1563150.3170419894 +32524 4326 427110.12140597973 1563150.3170419894 -41.69977388888889 -75.99999678434553 +4324 32524 -36.3 -4.0 799825.974635924 9557379.650145141 +32524 4326 799825.974635924 9557379.650145141 -36.2997738888889 -3.99998312797419 +4324 32525 -33.0 -40.0 500000.0000000005 5572243.953413932 +32525 4326 500000.0000000005 5572243.953413932 -32.99977388888889 -39.99998878744399 +4324 32525 -35.7 -76.0 427110.1214059801 1563150.3170419894 +32525 4326 427110.1214059801 1563150.3170419894 -35.6997738888889 -75.99999678434553 +4324 32525 -30.3 -4.0 799825.9746359261 9557379.650145141 +32525 4326 799825.9746359261 9557379.650145141 -30.29977388888889 -3.999983127974191 +4324 32526 -27.0 -40.0 500000.0 5572243.953413932 +32526 4326 500000.0 5572243.953413932 -26.999773888888885 -39.99998878744399 +4324 32526 -29.7 -76.0 427110.1214059801 1563150.3170419894 +32526 4326 427110.1214059801 1563150.3170419894 -29.69977388888888 -75.99999678434553 +4324 32526 -24.3 -4.0 799825.9746359247 9557379.650145141 +32526 4326 799825.9746359247 9557379.650145141 -24.29977388888889 -3.999983127974191 +4324 32527 -21.0 -40.0 499999.9999999992 5572243.953413932 +32527 4326 499999.9999999992 5572243.953413932 -20.99977388888889 -39.99998878744399 +4324 32527 -23.7 -76.0 427110.12140597985 1563150.3170419894 +32527 4326 427110.12140597985 1563150.3170419894 -23.69977388888889 -75.99999678434553 +4324 32527 -18.3 -4.0 799825.974635924 9557379.650145141 +32527 4326 799825.974635924 9557379.650145141 -18.299773888888886 -3.99998312797419 +4324 32528 -15.0 -40.0 499999.99999999866 5572243.953413932 +32528 4326 499999.99999999866 5572243.953413932 -14.999773888888889 -39.99998878744399 +4324 32528 -17.7 -76.0 427110.12140597956 1563150.3170419894 +32528 4326 427110.12140597956 1563150.3170419894 -17.69977388888889 -75.99999678434553 +4324 32528 -12.3 -4.0 799825.9746359229 9557379.650145141 +32528 4326 799825.9746359229 9557379.650145141 -12.299773888888891 -3.9999831279741915 +4324 32529 -9.0 -40.0 500000.0 5572243.953413932 +32529 4326 500000.0 5572243.953413932 -8.999773888888889 -39.99998878744399 +4324 32529 -11.7 -76.0 427110.12140598 1563150.3170419894 +32529 4326 427110.12140598 1563150.3170419894 -11.699773888888892 -75.99999678434553 +4324 32529 -6.300000000000001 -4.0 799825.9746359249 9557379.650145141 +32529 4326 799825.9746359249 9557379.650145141 -6.299773888888891 -3.99998312797419 +4324 32530 -3.0 -40.0 499999.99999999924 5572243.953413932 +32530 4326 499999.99999999924 5572243.953413932 -2.99977388888889 -39.99998878744399 +4324 32530 -5.7 -76.0 427110.1214059798 1563150.3170419894 +32530 4326 427110.1214059798 1563150.3170419894 -5.69977388888889 -75.99999678434553 +4324 32530 -0.3000000000000007 -4.0 799825.974635924 9557379.650145141 +32530 4326 799825.974635924 9557379.650145141 -0.2997738888888913 -3.999983127974191 +4324 32531 3.0 -40.0 500000.00000000076 5572243.953413932 +32531 4326 500000.00000000076 5572243.953413932 3.00022611111111 -39.99998878744399 +4324 32531 0.30000000000000004 -76.0 427110.12140598026 1563150.3170419894 +32531 4326 427110.12140598026 1563150.3170419894 0.3002261111111099 -75.99999678434553 +4324 32531 5.699999999999999 -4.0 799825.9746359258 9557379.650145141 +32531 4326 799825.9746359258 9557379.650145141 5.700226111111108 -3.999983127974191 +4324 32532 9.0 -40.0 500000.0 5572243.953413932 +32532 4326 500000.0 5572243.953413932 9.000226111111111 -39.99998878744399 +4324 32532 6.3 -76.0 427110.12140598 1563150.3170419894 +32532 4326 427110.12140598 1563150.3170419894 6.300226111111112 -75.99999678434553 +4324 32532 11.7 -4.0 799825.9746359249 9557379.650145141 +32532 4326 799825.9746359249 9557379.650145141 11.700226111111109 -3.999983127974191 +4324 32533 15.0 -40.0 500000.00000000134 5572243.953413932 +32533 4326 500000.00000000134 5572243.953413932 15.00022611111111 -39.99998878744399 +4324 32533 12.3 -76.0 427110.1214059805 1563150.3170419894 +32533 4326 427110.1214059805 1563150.3170419894 12.300226111111108 -75.99999678434553 +4324 32533 17.7 -4.0 799825.9746359268 9557379.650145141 +32533 4326 799825.9746359268 9557379.650145141 17.70022611111111 -3.999983127974191 +4324 32534 21.0 -40.0 500000.0000000008 5572243.953413932 +32534 4326 500000.0000000008 5572243.953413932 21.000226111111115 -39.99998878744399 +4324 32534 18.3 -76.0 427110.12140598026 1563150.3170419894 +32534 4326 427110.12140598026 1563150.3170419894 18.300226111111108 -75.99999678434553 +4324 32534 23.7 -4.0 799825.9746359258 9557379.650145141 +32534 4326 799825.9746359258 9557379.650145141 23.700226111111103 -3.99998312797419 +4324 32535 27.0 -40.0 500000.0 5572243.953413932 +32535 4326 500000.0 5572243.953413932 27.000226111111115 -39.999988787443996 +4324 32535 24.3 -76.0 427110.1214059801 1563150.3170419894 +32535 4326 427110.1214059801 1563150.3170419894 24.30022611111111 -75.99999678434553 +4324 32535 29.7 -4.0 799825.9746359247 9557379.650145141 +32535 4326 799825.9746359247 9557379.650145141 29.700226111111107 -3.999983127974191 +4324 32536 33.0 -40.0 499999.9999999995 5572243.953413932 +32536 4326 499999.9999999995 5572243.953413932 33.00022611111112 -39.99998878744399 +4324 32536 30.3 -76.0 427110.12140597973 1563150.3170419894 +32536 4326 427110.12140597973 1563150.3170419894 30.300226111111108 -75.99999678434553 +4324 32536 35.7 -4.0 799825.9746359247 9557379.650145141 +32536 4326 799825.9746359247 9557379.650145141 35.70022611111111 -3.99998312797419 +4324 32537 39.0 -40.0 500000.0000000005 5572243.953413932 +32537 4326 500000.0000000005 5572243.953413932 39.00022611111111 -39.999988787443996 +4324 32537 36.3 -76.0 427110.12140598026 1563150.3170419894 +32537 4326 427110.12140598026 1563150.3170419894 36.300226111111115 -75.99999678434553 +4324 32537 41.7 -4.0 799825.9746359261 9557379.650145141 +32537 4326 799825.9746359261 9557379.650145141 41.70022611111112 -3.999983127974191 +4324 32538 45.0 -40.0 500000.0 5572243.953413932 +32538 4326 500000.0 5572243.953413932 45.00022611111111 -39.99998878744399 +4324 32538 42.3 -76.0 427110.1214059799 1563150.3170419894 +32538 4326 427110.1214059799 1563150.3170419894 42.30022611111111 -75.99999678434553 +4324 32538 47.7 -4.0 799825.9746359254 9557379.650145141 +32538 4326 799825.9746359254 9557379.650145141 47.700226111111114 -3.9999831279741915 +4324 32539 51.0 -40.0 500000.00000000163 5572243.953413932 +32539 4326 500000.00000000163 5572243.953413932 51.00022611111112 -39.99998878744399 +4324 32539 48.3 -76.0 427110.12140598043 1563150.3170419894 +32539 4326 427110.12140598043 1563150.3170419894 48.30022611111111 -75.99999678434553 +4324 32539 53.7 -4.0 799825.9746359276 9557379.650145141 +32539 4326 799825.9746359276 9557379.650145141 53.700226111111114 -3.99998312797419 +4324 32540 57.0 -40.0 499999.9999999989 5572243.953413932 +32540 4326 499999.9999999989 5572243.953413932 57.00022611111111 -39.99998878744399 +4324 32540 54.3 -76.0 427110.12140597956 1563150.3170419894 +32540 4326 427110.12140597956 1563150.3170419894 54.30022611111111 -75.99999678434553 +4324 32540 59.7 -4.0 799825.9746359233 9557379.650145141 +32540 4326 799825.9746359233 9557379.650145141 59.70022611111112 -3.999983127974191 +4324 32541 63.0 -40.0 500000.0 5572243.953413932 +32541 4326 500000.0 5572243.953413932 63.00022611111112 -39.99998878744399 +4324 32541 60.3 -76.0 427110.1214059801 1563150.3170419894 +32541 4326 427110.1214059801 1563150.3170419894 60.300226111111115 -75.99999678434553 +4324 32541 65.7 -4.0 799825.9746359261 9557379.650145141 +32541 4326 799825.9746359261 9557379.650145141 65.70022611111112 -3.999983127974191 +4324 32542 69.0 -40.0 499999.99999999674 5572243.953413932 +32542 4326 499999.99999999674 5572243.953413932 69.00022611111112 -39.99998878744399 +4324 32542 66.3 -76.0 427110.12140597904 1563150.3170419894 +32542 4326 427110.12140597904 1563150.3170419894 66.30022611111112 -75.99999678434553 +4324 32542 71.7 -4.0 799825.9746359219 9557379.650145141 +32542 4326 799825.9746359219 9557379.650145141 71.70022611111112 -3.99998312797419 +4324 32543 75.0 -40.0 499999.9999999989 5572243.953413932 +32543 4326 499999.9999999989 5572243.953413932 75.0002261111111 -39.99998878744399 +4324 32543 72.3 -76.0 427110.1214059794 1563150.3170419894 +32543 4326 427110.1214059794 1563150.3170419894 72.3002261111111 -75.99999678434553 +4324 32543 77.7 -4.0 799825.9746359233 9557379.650145141 +32543 4326 799825.9746359233 9557379.650145141 77.70022611111112 -3.9999831279741915 +4324 32544 81.0 -40.0 500000.0 5572243.953413932 +32544 4326 500000.0 5572243.953413932 81.00022611111112 -39.99998878744399 +4324 32544 78.3 -76.0 427110.1214059801 1563150.3170419894 +32544 4326 427110.1214059801 1563150.3170419894 78.30022611111112 -75.99999678434553 +4324 32544 83.7 -4.0 799825.9746359261 9557379.650145141 +32544 4326 799825.9746359261 9557379.650145141 83.70022611111112 -3.99998312797419 +4324 32545 87.0 -40.0 499999.99999999674 5572243.953413932 +32545 4326 499999.99999999674 5572243.953413932 87.00022611111112 -39.99998878744399 +4324 32545 84.3 -76.0 427110.12140597904 1563150.3170419894 +32545 4326 427110.12140597904 1563150.3170419894 84.30022611111112 -75.99999678434553 +4324 32545 89.7 -4.0 799825.9746359219 9557379.650145141 +32545 4326 799825.9746359219 9557379.650145141 89.70022611111112 -3.999983127974191 +4324 32546 93.0 -40.0 499999.9999999989 5572243.953413932 +32546 4326 499999.9999999989 5572243.953413932 93.00022611111112 -39.99998878744399 +4324 32546 90.3 -76.0 427110.1214059794 1563150.3170419894 +32546 4326 427110.1214059794 1563150.3170419894 90.30022611111112 -75.99999678434553 +4324 32546 95.7 -4.0 799825.9746359233 9557379.650145141 +32546 4326 799825.9746359233 9557379.650145141 95.7002261111111 -3.999983127974191 +4324 32547 99.0 -40.0 500000.0 5572243.953413932 +32547 4326 500000.0 5572243.953413932 99.00022611111112 -39.99998878744399 +4324 32547 96.3 -76.0 427110.1214059801 1563150.3170419894 +32547 4326 427110.1214059801 1563150.3170419894 96.30022611111112 -75.99999678434553 +4324 32547 101.7 -4.0 799825.9746359261 9557379.650145141 +32547 4326 799825.9746359261 9557379.650145141 101.7002261111111 -3.999983127974191 +4324 32548 105.0 -40.0 499999.99999999785 5572243.953413932 +32548 4326 499999.99999999785 5572243.953413932 105.00022611111112 -39.99998878744399 +4324 32548 102.3 -76.0 427110.12140597904 1563150.3170419894 +32548 4326 427110.12140597904 1563150.3170419894 102.30022611111112 -75.99999678434553 +4324 32548 107.7 -4.0 799825.9746359219 9557379.650145141 +32548 4326 799825.9746359219 9557379.650145141 107.7002261111111 -3.99998312797419 +4324 32549 111.0 -40.0 500000.00000000326 5572243.953413932 +32549 4326 500000.00000000326 5572243.953413932 111.00022611111112 -39.99998878744399 +4324 32549 108.3 -76.0 427110.1214059808 1563150.3170419894 +32549 4326 427110.1214059808 1563150.3170419894 108.3002261111111 -75.99999678434553 +4324 32549 113.7 -4.0 799825.974635929 9557379.650145141 +32549 4326 799825.974635929 9557379.650145141 113.7002261111111 -3.99998312797419 +4324 32550 117.0 -40.0 500000.0 5572243.953413932 +32550 4326 500000.0 5572243.953413932 117.00022611111113 -39.999988787443996 +4324 32550 114.3 -76.0 427110.1214059801 1563150.3170419894 +32550 4326 427110.1214059801 1563150.3170419894 114.30022611111112 -75.99999678434553 +4324 32550 119.7 -4.0 799825.9746359261 9557379.650145141 +32550 4326 799825.9746359261 9557379.650145141 119.7002261111111 -3.999983127974191 +4324 32551 123.0 -40.0 500000.00000000215 5572243.953413932 +32551 4326 500000.00000000215 5572243.953413932 123.00022611111112 -39.99998878744399 +4324 32551 120.3 -76.0 427110.12140598043 1563150.3170419894 +32551 4326 427110.12140598043 1563150.3170419894 120.3002261111111 -75.99999678434553 +4324 32551 125.7 -4.0 799825.9746359261 9557379.650145141 +32551 4326 799825.9746359261 9557379.650145141 125.70022611111109 -3.99998312797419 +4324 32552 129.0 -40.0 500000.00000000215 5572243.953413932 +32552 4326 500000.00000000215 5572243.953413932 129.0002261111111 -39.99998878744399 +4324 32552 126.3 -76.0 427110.12140598113 1563150.3170419894 +32552 4326 427110.12140598113 1563150.3170419894 126.30022611111112 -75.99999678434553 +4324 32552 131.7 -4.0 799825.974635929 9557379.650145141 +32552 4326 799825.974635929 9557379.650145141 131.7002261111111 -3.9999831279741915 +4324 32553 135.0 -40.0 500000.0 5572243.953413932 +32553 4326 500000.0 5572243.953413932 135.0002261111111 -39.99998878744399 +4324 32553 132.3 -76.0 427110.12140598043 1563150.3170419894 +32553 4326 427110.12140598043 1563150.3170419894 132.30022611111113 -75.99999678434553 +4324 32553 137.7 -4.0 799825.9746359233 9557379.650145141 +32553 4326 799825.9746359233 9557379.650145141 137.70022611111108 -3.9999831279741915 +4324 32554 141.0 -40.0 500000.00000000215 5572243.953413932 +32554 4326 500000.00000000215 5572243.953413932 141.00022611111112 -39.99998878744399 +4324 32554 138.3 -76.0 427110.12140598113 1563150.3170419894 +32554 4326 427110.12140598113 1563150.3170419894 138.30022611111113 -75.99999678434553 +4324 32554 143.7 -4.0 799825.9746359261 9557379.650145141 +32554 4326 799825.9746359261 9557379.650145141 143.7002261111111 -3.9999831279741915 +4324 32555 147.0 -40.0 500000.00000000215 5572243.953413932 +32555 4326 500000.00000000215 5572243.953413932 147.0002261111111 -39.99998878744399 +4324 32555 144.3 -76.0 427110.12140598113 1563150.3170419894 +32555 4326 427110.12140598113 1563150.3170419894 144.3002261111111 -75.99999678434553 +4324 32555 149.7 -4.0 799825.974635929 9557379.650145141 +32555 4326 799825.974635929 9557379.650145141 149.7002261111111 -3.999983127974191 +4324 32556 153.0 -40.0 500000.0 5572243.953413932 +32556 4326 500000.0 5572243.953413932 153.0002261111111 -39.99998878744398 +4324 32556 150.3 -76.0 427110.12140598043 1563150.3170419894 +32556 4326 427110.12140598043 1563150.3170419894 150.30022611111113 -75.99999678434553 +4324 32556 155.7 -4.0 799825.9746359233 9557379.650145141 +32556 4326 799825.9746359233 9557379.650145141 155.70022611111108 -3.999983127974191 +4324 32557 159.0 -40.0 500000.00000000215 5572243.953413932 +32557 4326 500000.00000000215 5572243.953413932 159.00022611111112 -39.99998878744399 +4324 32557 156.3 -76.0 427110.12140598113 1563150.3170419894 +32557 4326 427110.12140598113 1563150.3170419894 156.30022611111113 -75.99999678434553 +4324 32557 161.7 -4.0 799825.9746359261 9557379.650145141 +32557 4326 799825.9746359261 9557379.650145141 161.7002261111111 -3.99998312797419 +4324 32558 165.0 -40.0 499999.99999999785 5572243.953413932 +32558 4326 499999.99999999785 5572243.953413932 165.0002261111111 -39.99998878744399 +4324 32558 162.3 -76.0 427110.12140597973 1563150.3170419894 +32558 4326 427110.12140597973 1563150.3170419894 162.3002261111111 -75.99999678434553 +4324 32558 167.7 -4.0 799825.9746359233 9557379.650145141 +32558 4326 799825.9746359233 9557379.650145141 167.7002261111111 -3.9999831279741915 +4324 32559 171.0 -40.0 500000.0 5572243.953413932 +32559 4326 500000.0 5572243.953413932 171.0002261111111 -39.99998878744399 +4324 32559 168.3 -76.0 427110.12140598043 1563150.3170419894 +32559 4326 427110.12140598043 1563150.3170419894 168.30022611111113 -75.99999678434553 +4324 32559 173.7 -4.0 799825.9746359233 9557379.650145141 +32559 4326 799825.9746359233 9557379.650145141 173.70022611111108 -3.999983127974191 +4324 32560 177.0 -40.0 500000.00000000215 5572243.953413932 +32560 4326 500000.00000000215 5572243.953413932 177.00022611111112 -39.99998878744399 +4324 32560 174.3 -76.0 427110.12140598113 1563150.3170419894 +32560 4326 427110.12140598113 1563150.3170419894 174.30022611111113 -75.99999678434553 +4324 32560 179.7 -4.0 799825.9746359261 9557379.650145141 +32560 4326 799825.9746359261 9557379.650145141 179.7002261111111 -3.999983127974191 +4326 32601 -177.0 42.0 500000.0 4649776.224819178 +4326 32601 -179.7 4.2 200248.5543225074 464752.08672347787 +4326 32601 -174.3 79.8 553358.5910886277 8860500.502039555 +4326 32602 -171.0 42.0 500000.0 4649776.224819178 +4326 32602 -173.7 4.2 200248.5543225074 464752.08672347787 +4326 32602 -168.3 79.8 553358.5910886277 8860500.502039555 +4326 32603 -165.0 42.0 500000.0 4649776.224819178 +4326 32603 -167.7 4.2 200248.55432250455 464752.08672347787 +4326 32603 -162.3 79.8 553358.5910886277 8860500.502039555 +4326 32604 -159.0 42.0 500000.0 4649776.224819178 +4326 32604 -161.7 4.2 200248.5543225074 464752.08672347787 +4326 32604 -156.3 79.8 553358.5910886277 8860500.502039555 +4326 32605 -153.0 42.0 500000.0 4649776.224819178 +4326 32605 -155.7 4.2 200248.5543225074 464752.08672347787 +4326 32605 -150.3 79.8 553358.5910886277 8860500.502039555 +4326 32606 -147.0 42.0 500000.0000000021 4649776.224819178 +4326 32606 -149.7 4.2 200248.5543225074 464752.08672347787 +4326 32606 -144.3 79.8 553358.5910886282 8860500.502039555 +4326 32607 -141.0 42.0 499999.9999999979 4649776.224819178 +4326 32607 -143.7 4.2 200248.55432250455 464752.08672347787 +4326 32607 -138.3 79.8 553358.5910886271 8860500.502039555 +4326 32608 -135.0 42.0 500000.0 4649776.224819178 +4326 32608 -137.7 4.2 200248.5543225074 464752.08672347787 +4326 32608 -132.3 79.8 553358.5910886277 8860500.502039555 +4326 32609 -129.0 42.0 500000.0 4649776.224819178 +4326 32609 -131.7 4.2 200248.55432250455 464752.08672347787 +4326 32609 -126.3 79.8 553358.5910886277 8860500.502039555 +4326 32610 -123.0 42.0 500000.0 4649776.224819178 +4326 32610 -125.7 4.2 200248.5543225074 464752.08672347787 +4326 32610 -120.3 79.8 553358.5910886282 8860500.502039555 +4326 32611 -117.0 42.0 500000.0 4649776.224819178 +4326 32611 -119.7 4.2 200248.55432250455 464752.08672347787 +4326 32611 -114.3 79.8 553358.5910886279 8860500.502039555 +4326 32612 -111.0 42.0 500000.0 4649776.224819178 +4326 32612 -113.7 4.2 200248.554322506 464752.08672347787 +4326 32612 -108.3 79.8 553358.5910886282 8860500.502039555 +4326 32613 -105.0 42.0 499999.99999999895 4649776.224819178 +4326 32613 -107.7 4.2 200248.55432250455 464752.08672347787 +4326 32613 -102.3 79.8 553358.5910886279 8860500.502039555 +4326 32614 -99.0 42.0 500000.0 4649776.224819178 +4326 32614 -101.7 4.2 200248.55432250455 464752.08672347787 +4326 32614 -96.3 79.8 553358.5910886279 8860500.502039555 +4326 32615 -93.0 42.0 499999.99999999895 4649776.224819178 +4326 32615 -95.7 4.2 200248.55432250455 464752.08672347787 +4326 32615 -90.3 79.8 553358.5910886279 8860500.502039555 +4326 32616 -87.0 42.0 500000.00000000105 4649776.224819178 +4326 32616 -89.7 4.2 200248.554322506 464752.08672347787 +4326 32616 -84.3 79.8 553358.5910886282 8860500.502039555 +4326 32617 -81.0 42.0 500000.0 4649776.224819178 +4326 32617 -83.7 4.2 200248.55432250455 464752.08672347787 +4326 32617 -78.3 79.8 553358.5910886279 8860500.502039555 +4326 32618 -75.0 42.0 500000.0 4649776.224819178 +4326 32618 -77.7 4.2 200248.554322506 464752.08672347787 +4326 32618 -72.3 79.8 553358.5910886282 8860500.502039555 +4326 32619 -69.0 42.0 500000.0 4649776.224819178 +4326 32619 -71.7 4.2 200248.55432250455 464752.08672347787 +4326 32619 -66.3 79.8 553358.5910886279 8860500.502039555 +4326 32620 -63.0 42.0 500000.0 4649776.224819178 +4326 32620 -65.7 4.2 200248.55432250455 464752.08672347787 +4326 32620 -60.3 79.8 553358.5910886279 8860500.502039555 +4326 32621 -57.0 42.0 500000.00000000105 4649776.224819178 +4326 32621 -59.7 4.2 200248.5543225074 464752.08672347787 +4326 32621 -54.3 79.8 553358.5910886283 8860500.502039555 +4326 32622 -51.0 42.0 500000.0000000005 4649776.224819178 +4326 32622 -53.7 4.2 200248.554322506 464752.08672347787 +4326 32622 -48.3 79.8 553358.5910886282 8860500.502039555 +4326 32623 -45.0 42.0 500000.0 4649776.224819178 +4326 32623 -47.7 4.2 200248.5543225053 464752.08672347787 +4326 32623 -42.3 79.8 553358.591088628 8860500.502039555 +4326 32624 -39.0 42.0 499999.9999999995 4649776.224819178 +4326 32624 -41.7 4.2 200248.55432250455 464752.08672347787 +4326 32624 -36.3 79.8 553358.5910886278 8860500.502039555 +4326 32625 -33.0 42.0 500000.0000000005 4649776.224819178 +4326 32625 -35.7 4.2 200248.554322506 464752.08672347787 +4326 32625 -30.3 79.8 553358.5910886282 8860500.502039555 +4326 32626 -27.0 42.0 500000.0 4649776.224819178 +4326 32626 -29.7 4.2 200248.554322506 464752.08672347787 +4326 32626 -24.3 79.8 553358.5910886279 8860500.502039555 +4326 32627 -21.0 42.0 499999.9999999992 4649776.224819178 +4326 32627 -23.7 4.2 200248.55432250496 464752.08672347787 +4326 32627 -18.3 79.8 553358.5910886278 8860500.502039555 +4326 32628 -15.0 42.0 499999.99999999866 4649776.224819178 +4326 32628 -17.7 4.2 200248.55432250386 464752.08672347787 +4326 32628 -12.3 79.8 553358.5910886276 8860500.502039555 +4326 32629 -9.004999999999999 42.005 499585.9443114766 4650331.381423269 +4326 32629 -11.7095 4.200500000000001 199193.28288484237 464811.0681912926 +4326 32629 -6.3005 79.8095 553299.562574325 8861559.273251178 +4326 32630 -3.0 42.0 499999.9999999993 4649776.224819178 +4326 32630 -5.7 4.2 200248.5543225048 464752.08672347787 +4326 32630 -0.3000000000000007 79.8 553358.5910886278 8860500.502039555 +4326 32631 3.0 42.0 500000.0000000007 4649776.224819178 +4326 32631 0.30000000000000004 4.2 200248.55432250665 464752.08672347787 +4326 32631 5.699999999999999 79.8 553358.5910886282 8860500.502039555 +4326 32632 9.0 42.0 500000.0 4649776.224819178 +4326 32632 6.3 4.2 200248.55432250578 464752.08672347787 +4326 32632 11.7 79.8 553358.5910886279 8860500.502039555 +4326 32633 15.0 42.0 500000.00000000134 4649776.224819178 +4326 32633 12.3 4.2 200248.55432250776 464752.08672347787 +4326 32633 17.7 79.8 553358.5910886283 8860500.502039555 +4326 32634 21.0 42.0 500000.0000000008 4649776.224819178 +4326 32634 18.3 4.2 200248.5543225067 464752.08672347787 +4326 32634 23.7 79.8 553358.5910886282 8860500.502039555 +4326 32635 27.0 42.0 500000.0 4649776.224819178 +4326 32635 24.3 4.2 200248.554322506 464752.08672347787 +4326 32635 29.7 79.8 553358.5910886279 8860500.502039555 +4326 32636 33.0 42.0 499999.9999999995 4649776.224819178 +4326 32636 30.3 4.2 200248.55432250455 464752.08672347787 +4326 32636 35.7 79.8 553358.5910886279 8860500.502039555 +4326 32637 39.0 42.0 500000.0000000005 4649776.224819178 +4326 32637 36.3 4.2 200248.5543225067 464752.08672347787 +4326 32637 41.7 79.8 553358.5910886282 8860500.502039555 +4326 32638 45.0 42.0 500000.0 4649776.224819178 +4326 32638 42.3 4.2 200248.5543225053 464752.08672347787 +4326 32638 47.7 79.8 553358.591088628 8860500.502039555 +4326 32639 51.0 42.0 500000.0000000016 4649776.224819178 +4326 32639 48.3 4.2 200248.5543225074 464752.08672347787 +4326 32639 53.7 79.8 553358.5910886284 8860500.502039555 +4326 32640 57.0 42.0 499999.99999999895 4649776.224819178 +4326 32640 54.3 4.2 200248.55432250386 464752.08672347787 +4326 32640 59.7 79.8 553358.5910886277 8860500.502039555 +4326 32641 63.0 42.0 500000.0 4649776.224819178 +4326 32641 60.3 4.2 200248.554322506 464752.08672347787 +4326 32641 65.7 79.8 553358.5910886282 8860500.502039555 +4326 32642 69.0 42.0 499999.99999999686 4649776.224819178 +4326 32642 66.3 4.2 200248.5543225017 464752.086723478 +4326 32642 71.7 79.8 553358.5910886275 8860500.502039555 +4326 32643 75.0 42.0 499999.99999999895 4649776.224819178 +4326 32643 72.3 4.2 200248.55432250316 464752.086723478 +4326 32643 77.7 79.8 553358.5910886277 8860500.502039555 +4326 32644 81.0 42.0 500000.0 4649776.224819178 +4326 32644 78.3 4.2 200248.554322506 464752.08672347787 +4326 32644 83.7 79.8 553358.5910886282 8860500.502039555 +4326 32645 87.0 42.0 499999.99999999686 4649776.224819178 +4326 32645 84.3 4.2 200248.5543225017 464752.086723478 +4326 32645 89.7 79.8 553358.5910886275 8860500.502039555 +4326 32646 93.0 42.0 499999.99999999895 4649776.224819178 +4326 32646 90.3 4.2 200248.55432250316 464752.086723478 +4326 32646 95.7 79.8 553358.5910886277 8860500.502039555 +4326 32647 99.0 42.0 500000.0 4649776.224819178 +4326 32647 96.3 4.2 200248.554322506 464752.08672347787 +4326 32647 101.7 79.8 553358.5910886282 8860500.502039555 +4326 32648 105.0 42.0 499999.9999999979 4649776.224819178 +4326 32648 102.3 4.2 200248.5543225017 464752.086723478 +4326 32648 107.7 79.8 553358.5910886275 8860500.502039555 +4326 32649 111.0 42.0 500000.00000000314 4649776.224819178 +4326 32649 108.3 4.2 200248.55432250886 464752.08672347787 +4326 32649 113.7 79.8 553358.5910886286 8860500.502039555 +4326 32650 117.0 42.0 500000.0 4649776.224819178 +4326 32650 114.3 4.2 200248.554322506 464752.08672347787 +4326 32650 119.7 79.8 553358.5910886282 8860500.502039555 +4326 32651 123.0 42.0 500000.0000000021 4649776.224819178 +4326 32651 120.3 4.2 200248.5543225074 464752.08672347787 +4326 32651 125.7 79.8 553358.5910886282 8860500.502039555 +4326 32652 129.0 42.0 500000.0000000021 4649776.224819178 +4326 32652 126.3 4.2 200248.55432251026 464752.08672347787 +4326 32652 131.7 79.8 553358.5910886286 8860500.502039555 +4326 32653 135.0 42.0 500000.0 4649776.224819178 +4326 32653 132.3 4.2 200248.5543225074 464752.08672347787 +4326 32653 137.7 79.8 553358.5910886277 8860500.502039555 +4326 32654 141.0 42.0 500000.0000000021 4649776.224819178 +4326 32654 138.3 4.2 200248.55432251026 464752.08672347787 +4326 32654 143.7 79.8 553358.5910886282 8860500.502039555 +4326 32655 147.0 42.0 500000.0000000021 4649776.224819178 +4326 32655 144.3 4.2 200248.55432251026 464752.08672347787 +4326 32655 149.7 79.8 553358.5910886286 8860500.502039555 +4326 32656 153.0 42.0 500000.0 4649776.224819178 +4326 32656 150.3 4.2 200248.5543225074 464752.08672347787 +4326 32656 155.7 79.8 553358.5910886277 8860500.502039555 +4326 32657 159.0 42.0 500000.0000000021 4649776.224819178 +4326 32657 156.3 4.2 200248.55432251026 464752.08672347787 +4326 32657 161.7 79.8 553358.5910886282 8860500.502039555 +4326 32658 165.0 42.0 499999.9999999979 4649776.224819178 +4326 32658 162.3 4.2 200248.55432250455 464752.08672347787 +4326 32658 167.7 79.8 553358.5910886277 8860500.502039555 +4326 32659 171.0 42.0 500000.0 4649776.224819178 +4326 32659 168.3 4.2 200248.5543225074 464752.08672347787 +4326 32659 173.7 79.8 553358.5910886277 8860500.502039555 +4326 32660 177.0 42.0 500000.0000000021 4649776.224819178 +4326 32660 174.3 4.2 200248.55432251026 464752.08672347787 +4326 32660 179.7 79.8 553358.5910886282 8860500.502039555 +4326 32661 0.0 75.0 2000000.0 325449.80628614617 +4326 32661 -162.0 61.5 1002357.0911193731 5070429.156641536 +4326 32661 162.0 88.5 2051465.1893966065 2158393.5661435425 +4326 32664 -96.545 27.2 2438264.5532736257 9878490.13010709 +4326 32664 -97.1525 26.093 2246607.8327762005 9472729.017869461 +4326 32664 -95.9375 28.307 2625763.389790679 10285476.754275555 +4326 32665 -92.93 27.67 1663065.6754111007 10041480.618500145 +4326 32665 -95.693 25.816 754625.2835531669 9376857.044188825 +4326 32665 -90.167 29.524 2541296.7418210604 10726322.531327555 +4326 32666 -86.96000000000001 27.1 1653425.473462368 9834336.865575533 +4326 32666 -89.705 24.265 739400.8690705807 8813078.587442571 +4326 32666 -84.215 29.935 2522420.1695377133 10875453.678477429 +4326 32667 -82.63 26.880000000000003 1109233.129271954 9757807.517219938 +4326 32667 -83.944 24.126 658681.6946940902 8764158.231467592 +4326 32667 -81.316 29.634 1540060.035410181 10755465.437210351 +4326 32701 -177.0 -40.0 500000.0 5572242.781261626 +4326 32701 -179.7 -76.0 427110.09640165104 1563147.873624554 +4326 32701 -174.3 -4.0 799826.0687050604 9557379.538975965 +4326 32702 -171.0 -40.0 500000.0 5572242.781261626 +4326 32702 -173.7 -76.0 427110.09640165104 1563147.873624554 +4326 32702 -168.3 -4.0 799826.0687050604 9557379.538975965 +4326 32703 -165.0 -40.0 500000.0 5572242.781261626 +4326 32703 -167.7 -76.0 427110.09640165034 1563147.873624554 +4326 32703 -162.3 -4.0 799826.0687050604 9557379.538975965 +4326 32704 -159.0 -40.0 500000.0 5572242.781261626 +4326 32704 -161.7 -76.0 427110.09640165104 1563147.873624554 +4326 32704 -156.3 -4.0 799826.0687050604 9557379.538975965 +4326 32705 -153.0 -40.0 500000.0 5572242.781261626 +4326 32705 -155.7 -76.0 427110.09640165104 1563147.873624554 +4326 32705 -150.3 -4.0 799826.0687050604 9557379.538975965 +4326 32706 -147.0 -40.0 500000.00000000215 5572242.781261626 +4326 32706 -149.7 -76.0 427110.09640165104 1563147.873624554 +4326 32706 -144.3 -4.0 799826.0687050632 9557379.538975965 +4326 32707 -141.0 -40.0 499999.99999999785 5572242.781261626 +4326 32707 -143.7 -76.0 427110.09640165034 1563147.873624554 +4326 32707 -138.3 -4.0 799826.0687050575 9557379.538975965 +4326 32708 -135.0 -40.0 500000.0 5572242.781261626 +4326 32708 -137.7 -76.0 427110.09640165104 1563147.873624554 +4326 32708 -132.3 -4.0 799826.0687050604 9557379.538975965 +4326 32709 -129.0 -40.0 500000.0 5572242.781261626 +4326 32709 -131.7 -76.0 427110.09640165034 1563147.873624554 +4326 32709 -126.3 -4.0 799826.0687050604 9557379.538975965 +4326 32710 -123.0 -40.0 500000.0 5572242.781261626 +4326 32710 -125.7 -76.0 427110.09640165104 1563147.873624554 +4326 32710 -120.3 -4.0 799826.0687050632 9557379.538975965 +4326 32711 -117.0 -40.0 500000.0 5572242.781261626 +4326 32711 -119.7 -76.0 427110.09640165034 1563147.873624554 +4326 32711 -114.3 -4.0 799826.0687050618 9557379.538975965 +4326 32712 -111.0 -40.0 500000.0 5572242.781261626 +4326 32712 -113.7 -76.0 427110.0964016507 1563147.873624554 +4326 32712 -108.3 -4.0 799826.0687050632 9557379.538975965 +4326 32713 -105.0 -40.0 499999.9999999989 5572242.781261626 +4326 32713 -107.7 -76.0 427110.09640165034 1563147.873624554 +4326 32713 -102.3 -4.0 799826.0687050618 9557379.538975965 +4326 32714 -99.0 -40.0 500000.0 5572242.781261626 +4326 32714 -101.7 -76.0 427110.09640165034 1563147.873624554 +4326 32714 -96.3 -4.0 799826.0687050618 9557379.538975965 +4326 32715 -93.0 -40.0 499999.9999999989 5572242.781261626 +4326 32715 -95.7 -76.0 427110.09640165034 1563147.873624554 +4326 32715 -90.3 -4.0 799826.0687050618 9557379.538975965 +4326 32716 -87.0 -40.0 500000.0000000011 5572242.781261626 +4326 32716 -89.7 -76.0 427110.0964016507 1563147.873624554 +4326 32716 -84.3 -4.0 799826.0687050632 9557379.538975965 +4326 32717 -81.0 -40.0 500000.0 5572242.781261626 +4326 32717 -83.7 -76.0 427110.09640165034 1563147.873624554 +4326 32717 -78.3 -4.0 799826.0687050618 9557379.538975965 +4326 32718 -75.0 -40.0 500000.0 5572242.781261626 +4326 32718 -77.7 -76.0 427110.0964016507 1563147.873624554 +4326 32718 -72.3 -4.0 799826.0687050632 9557379.538975965 +4326 32719 -69.0 -40.0 500000.0 5572242.781261626 +4326 32719 -71.7 -76.0 427110.09640165034 1563147.873624554 +4326 32719 -66.3 -4.0 799826.0687050618 9557379.538975965 +4326 32720 -63.0 -40.0 500000.0 5572242.781261626 +4326 32720 -65.7 -76.0 427110.09640165034 1563147.873624554 +4326 32720 -60.3 -4.0 799826.0687050618 9557379.538975965 +4326 32721 -57.0 -40.0 500000.0000000011 5572242.781261626 +4326 32721 -59.7 -76.0 427110.09640165104 1563147.873624554 +4326 32721 -54.3 -4.0 799826.068705064 9557379.538975965 +4326 32722 -51.0 -40.0 500000.0000000005 5572242.781261626 +4326 32722 -53.7 -76.0 427110.0964016507 1563147.873624554 +4326 32722 -48.3 -4.0 799826.0687050632 9557379.538975965 +4326 32723 -45.0 -40.0 500000.0 5572242.781261626 +4326 32723 -47.7 -76.0 427110.0964016505 1563147.873624554 +4326 32723 -42.3 -4.0 799826.0687050625 9557379.538975965 +4326 32724 -39.0 -40.0 499999.9999999995 5572242.781261626 +4326 32724 -41.7 -76.0 427110.09640165034 1563147.873624554 +4326 32724 -36.3 -4.0 799826.0687050611 9557379.538975965 +4326 32725 -33.0 -40.0 500000.0000000005 5572242.781261626 +4326 32725 -35.7 -76.0 427110.0964016507 1563147.873624554 +4326 32725 -30.3 -4.0 799826.0687050632 9557379.538975965 +4326 32726 -27.0 -40.0 500000.0 5572242.781261626 +4326 32726 -29.7 -76.0 427110.0964016507 1563147.873624554 +4326 32726 -24.3 -4.0 799826.0687050618 9557379.538975965 +4326 32727 -21.0 -40.0 499999.9999999992 5572242.781261626 +4326 32727 -23.7 -76.0 427110.09640165046 1563147.873624554 +4326 32727 -18.3 -4.0 799826.0687050611 9557379.538975965 +4326 32728 -15.0 -40.0 499999.99999999866 5572242.781261626 +4326 32728 -17.7 -76.0 427110.09640165017 1563147.873624554 +4326 32728 -12.3 -4.0 799826.06870506 9557379.538975965 +4326 32729 -9.0 -40.0 500000.0 5572242.781261626 +4326 32729 -11.7 -76.0 427110.09640165063 1563147.873624554 +4326 32729 -6.300000000000001 -4.0 799826.0687050619 9557379.538975965 +4326 32730 -3.0 -40.0 499999.99999999924 5572242.781261626 +4326 32730 -5.7 -76.0 427110.0964016504 1563147.873624554 +4326 32730 -0.3000000000000007 -4.0 799826.068705061 9557379.538975965 +4326 32731 3.0 -40.0 500000.00000000076 5572242.781261626 +4326 32731 0.30000000000000004 -76.0 427110.09640165087 1563147.873624554 +4326 32731 5.699999999999999 -4.0 799826.068705063 9557379.538975965 +4326 32732 9.0 -40.0 500000.0 5572242.781261626 +4326 32732 6.3 -76.0 427110.09640165063 1563147.873624554 +4326 32732 11.7 -4.0 799826.0687050619 9557379.538975965 +4326 32733 15.0 -40.0 500000.00000000134 5572242.781261626 +4326 32733 12.3 -76.0 427110.0964016511 1563147.873624554 +4326 32733 17.7 -4.0 799826.068705064 9557379.538975965 +4326 32734 21.0 -40.0 500000.0000000008 5572242.781261626 +4326 32734 18.3 -76.0 427110.09640165087 1563147.873624554 +4326 32734 23.7 -4.0 799826.0687050628 9557379.538975965 +4326 32735 27.0 -40.0 500000.0 5572242.781261626 +4326 32735 24.3 -76.0 427110.0964016507 1563147.873624554 +4326 32735 29.7 -4.0 799826.0687050618 9557379.538975965 +4326 32736 33.0 -40.0 499999.9999999995 5572242.781261626 +4326 32736 30.3 -76.0 427110.09640165034 1563147.873624554 +4326 32736 35.7 -4.0 799826.0687050618 9557379.538975965 +4326 32737 39.0 -40.0 500000.0000000005 5572242.781261626 +4326 32737 36.3 -76.0 427110.09640165087 1563147.873624554 +4326 32737 41.7 -4.0 799826.0687050632 9557379.538975965 +4326 32738 45.0 -40.0 500000.0 5572242.781261626 +4326 32738 42.3 -76.0 427110.0964016505 1563147.873624554 +4326 32738 47.7 -4.0 799826.0687050625 9557379.538975965 +4326 32739 51.0 -40.0 500000.00000000163 5572242.781261626 +4326 32739 48.3 -76.0 427110.09640165104 1563147.873624554 +4326 32739 53.7 -4.0 799826.0687050647 9557379.538975965 +4326 32740 57.0 -40.0 499999.9999999989 5572242.781261626 +4326 32740 54.3 -76.0 427110.09640165017 1563147.873624554 +4326 32740 59.7 -4.0 799826.0687050604 9557379.538975965 +4326 32741 63.0 -40.0 500000.0 5572242.781261626 +4326 32741 60.3 -76.0 427110.0964016507 1563147.873624554 +4326 32741 65.7 -4.0 799826.0687050632 9557379.538975965 +4326 32742 69.0 -40.0 499999.99999999674 5572242.781261626 +4326 32742 66.3 -76.0 427110.09640164964 1563147.873624554 +4326 32742 71.7 -4.0 799826.0687050589 9557379.538975965 +4326 32743 75.0 -40.0 499999.9999999989 5572242.781261626 +4326 32743 72.3 -76.0 427110.09640165 1563147.873624554 +4326 32743 77.7 -4.0 799826.0687050604 9557379.538975965 +4326 32744 81.0 -40.0 500000.0 5572242.781261626 +4326 32744 78.3 -76.0 427110.0964016507 1563147.873624554 +4326 32744 83.7 -4.0 799826.0687050632 9557379.538975965 +4326 32745 87.0 -40.0 499999.99999999674 5572242.781261626 +4326 32745 84.3 -76.0 427110.09640164964 1563147.873624554 +4326 32745 89.7 -4.0 799826.0687050589 9557379.538975965 +4326 32746 93.0 -40.0 499999.9999999989 5572242.781261626 +4326 32746 90.3 -76.0 427110.09640165 1563147.873624554 +4326 32746 95.7 -4.0 799826.0687050604 9557379.538975965 +4326 32747 99.0 -40.0 500000.0 5572242.781261626 +4326 32747 96.3 -76.0 427110.0964016507 1563147.873624554 +4326 32747 101.7 -4.0 799826.0687050632 9557379.538975965 +4326 32748 105.0 -40.0 499999.99999999785 5572242.781261626 +4326 32748 102.3 -76.0 427110.09640164964 1563147.873624554 +4326 32748 107.7 -4.0 799826.0687050589 9557379.538975965 +4326 32749 111.0 -40.0 500000.00000000326 5572242.781261626 +4326 32749 108.3 -76.0 427110.0964016514 1563147.873624554 +4326 32749 113.7 -4.0 799826.0687050661 9557379.538975965 +4326 32750 117.0 -40.0 500000.0 5572242.781261626 +4326 32750 114.3 -76.0 427110.0964016507 1563147.873624554 +4326 32750 119.7 -4.0 799826.0687050632 9557379.538975965 +4326 32751 123.0 -40.0 500000.00000000215 5572242.781261626 +4326 32751 120.3 -76.0 427110.09640165104 1563147.873624554 +4326 32751 125.7 -4.0 799826.0687050632 9557379.538975965 +4326 32752 129.0 -40.0 500000.00000000215 5572242.781261626 +4326 32752 126.3 -76.0 427110.09640165174 1563147.873624554 +4326 32752 131.7 -4.0 799826.0687050661 9557379.538975965 +4326 32753 135.0 -40.0 500000.0 5572242.781261626 +4326 32753 132.3 -76.0 427110.09640165104 1563147.873624554 +4326 32753 137.7 -4.0 799826.0687050604 9557379.538975965 +4326 32754 141.0 -40.0 500000.00000000215 5572242.781261626 +4326 32754 138.3 -76.0 427110.09640165174 1563147.873624554 +4326 32754 143.7 -4.0 799826.0687050632 9557379.538975965 +4326 32755 147.0 -40.0 500000.00000000215 5572242.781261626 +4326 32755 144.3 -76.0 427110.09640165174 1563147.873624554 +4326 32755 149.7 -4.0 799826.0687050661 9557379.538975965 +4326 32756 153.0 -40.0 500000.0 5572242.781261626 +4326 32756 150.3 -76.0 427110.09640165104 1563147.873624554 +4326 32756 155.7 -4.0 799826.0687050604 9557379.538975965 +4326 32757 159.0 -40.0 500000.00000000215 5572242.781261626 +4326 32757 156.3 -76.0 427110.09640165174 1563147.873624554 +4326 32757 161.7 -4.0 799826.0687050632 9557379.538975965 +4326 32758 165.0 -40.0 499999.99999999785 5572242.781261626 +4326 32758 162.3 -76.0 427110.09640165034 1563147.873624554 +4326 32758 167.7 -4.0 799826.0687050604 9557379.538975965 +4326 32759 171.0 -40.0 500000.0 5572242.781261626 +4326 32759 168.3 -76.0 427110.09640165104 1563147.873624554 +4326 32759 173.7 -4.0 799826.0687050604 9557379.538975965 +4326 32760 177.0 -40.0 500000.00000000215 5572242.781261626 +4326 32760 174.3 -76.0 427110.09640165174 1563147.873624554 +4326 32760 179.7 -4.0 799826.0687050632 9557379.538975965 +4326 32761 0.0 -75.0 2000000.0 3674550.193713854 +4326 32761 -162.0 -88.5 1948534.8106033935 1841606.4338564575 +4326 32761 162.0 -61.5 2997642.908880627 -1070429.1566415364 +4326 32766 37.835 -18.9 693274.9165880769 7909234.505035346 +4326 32766 33.1595 -26.829 217685.21902161092 7029344.237607361 +4326 32766 42.5105 -10.971 1212734.909150376 8779495.876679474 +12287 25314 103.52 2.08 -4568.898086313612 4140.634248648942 +25314 4326 -4568.898086313612 4140.634248648942 103.52 2.0800000000000005 +12287 25314 102.548 1.297 -112745.73236220008 -82417.28707129191 +25314 4326 -112745.73236220008 -82417.28707129191 102.54800000000002 1.2969999999967914 +12287 25314 104.49199999999999 2.863 103502.15807898529 90763.86592157671 +25314 4326 103502.15807898529 90763.86592157671 104.49199999999998 2.862999999996814 +12287 25315 102.205 2.655 29500.815173581646 -6312.899319766399 +25315 4326 29500.815173581646 -6312.899319766399 102.205 2.6550000000000042 +12287 25315 101.7505 2.0925 -21048.453096007204 -68513.90063462772 +25315 4326 -21048.453096007204 -68513.90063462772 101.7505 2.0925000000000007 +12287 25315 102.6595 3.2175 80001.54326746448 55911.81752566326 +25315 4326 80001.54326746448 55911.81752566326 102.65949999999998 3.2174999999995735 +12287 25316 102.5 3.615 7263.986210386777 -10587.286330803265 +25316 4326 7263.986210386777 -10587.286330803265 102.5 3.615000000000001 +12287 25316 101.447 2.5665 -109831.49895536335 -126485.70002746412 +25316 4326 -109831.49895536335 -126485.70002746412 101.44700000000005 2.566499999995457 +12287 25316 103.553 4.6635 124088.32444978986 105454.15835668336 +25316 4326 124088.32444978986 105454.15835668336 103.5529999999998 4.663499999989861 +12287 25317 101.36500000000001 3.205 -15761.187205725179 -52535.0555250635 +25317 4326 -15761.187205725179 -52535.0555250635 101.36500000000001 3.2050000000000014 +12287 25317 100.82050000000001 2.6065 -76320.65895757375 -118695.77556743796 +25317 4326 -76320.65895757375 -118695.77556743796 100.82050000000001 2.6064999999996306 +12287 25317 101.9095 3.8035 44729.67017570579 13655.474107406375 +25317 4326 44729.67017570579 13655.474107406375 101.9095 3.8035000000000645 +12287 25318 103.05 4.8950000000000005 17345.473476169747 -5618.245330170613 +25318 4326 17345.473476169747 -5618.245330170613 103.05 4.895000000000005 +12287 25318 102.447 3.9905 -49597.37620888159 -105627.29236253031 +25318 4326 -49597.37620888159 -105627.29236253031 102.447 3.990500000000106 +12287 25318 103.65299999999999 5.7995 84104.36592326085 94459.23334459461 +25318 4326 84104.36592326085 94459.23334459461 103.65299999999996 5.799500000002001 +12287 25319 100.34 5.355 -516.3853107081675 -7286.24118846622 +25319 4326 -516.3853107081675 -7286.24118846622 100.34 5.355 +12287 25319 100.14200000000001 5.1435 -22469.654898131896 -30671.105925877175 +25319 4326 -22469.654898131896 -30671.105925877175 100.14200000000001 5.143500000000015 +12287 25319 100.538 5.5665 21421.805110012694 16105.849279197515 +25319 4326 21421.805110012694 16105.849279197515 100.538 5.566500000000016 +12287 25320 100.355 5.9 -31157.70002978968 -7143.908870632872 +25320 4326 -31157.70002978968 -7143.908870632872 100.355 5.900000000000085 +12287 25320 99.6665 5.162 -107530.98314675367 -88681.31816222472 +25320 4326 -107530.98314675367 -88681.31816222472 99.66650000000011 5.161999999998598 +12287 25320 101.04350000000001 6.638 45019.140228011 74480.31459178882 +25320 4326 45019.140228011 74480.31459178882 101.04350000000001 6.638000000000494 +12287 25321 101.035 4.79 24359.6058212962 -7633.647652309091 +25321 4326 24359.6058212962 -7633.647652309091 101.035 4.790000000000018 +12287 25321 100.1665 3.773 -72081.03838874512 -120071.04619671512 +25321 4326 -72081.03838874512 -120071.04619671512 100.16650000000001 3.773000000000012 +12287 25321 101.9035 5.807 120507.8714300217 104941.94384334724 +25321 4326 120507.8714300217 104941.94384334724 101.90349999999972 5.806999999997087 +12287 25322 102.0 5.415 -19464.07076443025 -52926.73126310755 +25322 4326 -19464.07076443025 -52926.73126310755 102.0 5.41500000000001 +12287 25322 101.39699999999999 4.6275 -86395.71640323933 -139966.1364055133 +25322 4326 -86395.71640323933 -139966.1364055133 101.39700000000002 4.627500000000167 +12287 25322 102.60300000000001 6.2025 47298.26425345272 34175.725742368755 +25322 4326 47298.26425345272 34175.725742368755 102.60300000000001 6.202500000000471 +4326 990001 -170.0 -60.0 -9451579.418015448 -6671703.118599138 +990001 4326 -9451579.418015448 -6671703.118599138 -170.0 -59.99999999999999 +4326 990001 -120.0 -30.0 -11555728.774429437 -3335851.559299569 +990001 4326 -11555728.774429437 -3335851.559299569 -120.00000000000001 -29.999999999999996 +4326 990001 -60.0 0.5 -6671449.080587624 55597.52598832616 +990001 4326 -6671449.080587624 55597.52598832616 -59.99999999999999 0.5 +4326 990001 0.0 0.0 0.0 0.0 +990001 4326 0.0 0.0 0.0 0.0 +4326 990001 30.0 45.0 2358803.258612444 5003777.338949354 +990001 4326 2358803.258612444 5003777.338949354 29.999999999999996 45.0 +4326 990001 60.0 -15.0 6444370.347588227 -1667925.7796497846 +990001 4326 6444370.347588227 -1667925.7796497846 59.99999999999999 -14.999999999999998 +4326 990001 100.0 70.0 3803094.761415982 7783653.638365662 +990001 4326 3803094.761415982 7783653.638365662 100.00000000000001 70.0 +4326 990001 140.0 20.0 14628483.7734512 2223901.039533046 +990001 4326 14628483.7734512 2223901.039533046 140.0 19.999999999999996 +4326 990001 179.0 -80.0 3456278.447297226 -8895604.158132184 +990001 4326 3456278.447297226 -8895604.158132184 178.99999999999977 -79.99999999999999 +4326 990001 -30.0 85.0 -290738.6203466875 9451579.418015448 +990001 4326 -290738.6203466875 9451579.418015448 -30.000000000000075 85.00000000000001 +4326 990002 140.7 0.0 0.0 0.0 +990002 4326 0.0 0.0 140.7 0.0 +4326 990002 100.0 -30.0 -3387194.248597037 -2967586.9560760143 +990002 4326 -3387194.248597037 -2967586.9560760143 100.00000000000024 -29.999999999999897 +4326 990002 180.0 40.0 2883394.56992346 3776151.72097431 +990002 4326 2883394.56992346 3776151.72097431 180.00000000000014 40.000000000000085 +4326 990002 120.0 60.0 -1032087.2013849812 4990147.993494502 +990002 4326 -1032087.2013849812 4990147.993494502 119.99999999999984 60.00000000000017 +4326 990002 160.0 -60.0 965738.5250308937 -4993764.960130982 +990002 4326 965738.5250308937 -4993764.960130982 160.00000000000003 -60.00000000000002 +4326 990002 90.0 10.0 -4531346.128834804 1022582.1512465203 +990002 4326 -4531346.128834804 1022582.1512465203 90.00000000000013 9.999999999999984 +4326 990002 -160.0 20.0 4691144.124881785 1964818.4349137617 +990002 4326 4691144.124881785 1964818.4349137617 -160.00000000000048 19.99999999999991 +4326 990002 130.0 35.0 -938411.8832896658 3503714.8962962744 +990002 4326 -938411.8832896658 3503714.8962962744 130.0 35.00000000000003 +4326 990002 150.0 -10.0 1009858.5361766182 -1094000.7360021363 +990002 4326 1009858.5361766182 -1094000.7360021363 150.00000000000003 -10.000000000000018 +4326 990002 110.0 -50.0 -1940833.4956464563 -4474463.517605711 +990002 4326 -1940833.4956464563 -4474463.517605711 110.00000000000026 -49.99999999999979 diff --git a/nodedb-crs/tests/fixtures/proj_reference_malaysia.tsv b/nodedb-crs/tests/fixtures/proj_reference_malaysia.tsv new file mode 100644 index 000000000..0281ba1ed --- /dev/null +++ b/nodedb-crs/tests/fixtures/proj_reference_malaysia.tsv @@ -0,0 +1,483 @@ +# EPSG v12.029 PROJ 9.8.1 pyproj 3.8.0 +# Built from official EPSG codes with only_best=True. One op line per operation PROJ used. +# tol_m empty: the epsg.tsv strings reproduce the row within 0.001 m projected or 1e-09 degrees geographic. +# tol_m set: PROJ used another operation than the epsg.tsv Helmert. +# tol_m is that Helmert's accuracy, metres, against max(|dlon|, |dlat|) * 111320. +# op 4742 4326: GDM2000 to WGS 84 (1) (with axis order normalized for visualization) | proj=noop ellps=GRS80 +# op 4245 4326: Kertau 1968 to WGS 84 (1) (with axis order normalized for visualization) | proj=pipeline step proj=unitconvert xy_in=deg xy_out=rad step proj=push v_3 step proj=cart ellps=evrst48 step proj=helmert x=-11 y=851 z=5 step inv proj=cart ellps=WGS84 step proj=pop v_3 step proj=unitconvert xy_in=rad xy_out=deg +# op 4298 4326: Timbalai 1948 to WGS 84 (1) (with axis order normalized for visualization) | proj=pipeline step proj=unitconvert xy_in=deg xy_out=rad step proj=push v_3 step proj=cart ellps=evrstSS step proj=helmert x=-679 y=669 z=-48 step inv proj=cart ellps=WGS84 step proj=pop v_3 step proj=unitconvert xy_in=rad xy_out=deg +# op 4298 4326: Timbalai 1948 to WGS 84 (5) (with axis order normalized for visualization) | proj=pipeline step proj=unitconvert xy_in=deg xy_out=rad step proj=push v_3 step proj=cart ellps=evrstSS step proj=helmert x=-689.5937 y=623.84046 z=-65.93566 rx=0.02331 ry=-1.17094 rz=0.80054 s=5.88536 convention=coordinate_frame step inv proj=cart ellps=WGS84 step proj=pop v_3 step proj=unitconvert xy_in=rad xy_out=deg +# op 4742 3375: Peninsular RSO (with axis order normalized for visualization) | proj=pipeline step proj=unitconvert xy_in=deg xy_out=rad step proj=omerc no_uoff lat_0=4 lonc=102.25 alpha=323.025796466667 gamma=323.130102361111 k=0.99984 x_0=804671 y_0=0 ellps=GRS80 +# op 3375 4326: Inverse of Peninsular RSO + GDM2000 to WGS 84 (1) (with axis order normalized for visualization) | proj=pipeline step inv proj=omerc no_uoff lat_0=4 lonc=102.25 alpha=323.025796466667 gamma=323.130102361111 k=0.99984 x_0=804671 y_0=0 ellps=GRS80 step proj=unitconvert xy_in=rad xy_out=deg +# op 4742 3376: Borneo RSO (with axis order normalized for visualization) | proj=pipeline step proj=unitconvert xy_in=deg xy_out=rad step proj=omerc no_uoff lat_0=4 lonc=115 alpha=53.31580995 gamma=53.1301023611111 k=0.99984 x_0=0 y_0=0 ellps=GRS80 +# op 3376 4326: Inverse of Borneo RSO + GDM2000 to WGS 84 (1) (with axis order normalized for visualization) | proj=pipeline step inv proj=omerc no_uoff lat_0=4 lonc=115 alpha=53.31580995 gamma=53.1301023611111 k=0.99984 x_0=0 y_0=0 ellps=GRS80 step proj=unitconvert xy_in=rad xy_out=deg +# op 4751 3168: Rectified Skew Orthomorphic Malaya Grid (metre) (with axis order normalized for visualization) | proj=pipeline step proj=unitconvert xy_in=deg xy_out=rad step proj=omerc no_uoff lat_0=4 lonc=102.25 alpha=323.0257905 gamma=323.130102361111 k=0.99984 x_0=804670.24 y_0=0 ellps=evrst69 +# op 3168 4326: Inverse of Rectified Skew Orthomorphic Malaya Grid (metre) + Kertau (RSO) to Kertau 1968 (1) + Kertau 1968 to WGS 84 (1) (with axis order normalized for visualization) | proj=pipeline step inv proj=omerc no_uoff lat_0=4 lonc=102.25 alpha=323.0257905 gamma=323.130102361111 k=0.99984 x_0=804670.24 y_0=0 ellps=evrst69 step proj=push v_3 step proj=cart ellps=evrst69 step proj=helmert x=-11 y=851 z=5 step inv proj=cart ellps=WGS84 step proj=pop v_3 step proj=unitconvert xy_in=rad xy_out=deg +# op 4751 3167: Rectified Skew Orthomorphic Malaya Grid (chain) (with axis order normalized for visualization) | proj=pipeline step proj=unitconvert xy_in=deg xy_out=rad step proj=omerc no_uoff lat_0=4 lonc=102.25 alpha=323.0257905 gamma=323.130102361111 k=0.99984 x_0=804670.24 y_0=0 ellps=evrst69 step proj=unitconvert xy_in=m xy_out=20.116756 +# op 3167 4326: Inverse of Rectified Skew Orthomorphic Malaya Grid (chain) + Kertau (RSO) to Kertau 1968 (1) + Kertau 1968 to WGS 84 (1) (with axis order normalized for visualization) | proj=pipeline step proj=unitconvert xy_in=20.116756 xy_out=m step inv proj=omerc no_uoff lat_0=4 lonc=102.25 alpha=323.0257905 gamma=323.130102361111 k=0.99984 x_0=804670.24 y_0=0 ellps=evrst69 step proj=push v_3 step proj=cart ellps=evrst69 step proj=helmert x=-11 y=851 z=5 step inv proj=cart ellps=WGS84 step proj=pop v_3 step proj=unitconvert xy_in=rad xy_out=deg +# op 4742 3380: Selangor Grid (with axis order normalized for visualization) | proj=pipeline step proj=unitconvert xy_in=deg xy_out=rad step proj=cass lat_0=3.68464905 lon_0=101.389107913889 x_0=-34836.161 y_0=56464.049 ellps=GRS80 +# op 3380 4326: Inverse of Selangor Grid + GDM2000 to WGS 84 (1) (with axis order normalized for visualization) | proj=pipeline step inv proj=cass lat_0=3.68464905 lon_0=101.389107913889 x_0=-34836.161 y_0=56464.049 ellps=GRS80 step proj=unitconvert xy_in=rad xy_out=deg +# op 4298 29871: Rectified Skew Orthomorphic Borneo Grid (chain) (with axis order normalized for visualization) | proj=pipeline step proj=unitconvert xy_in=deg xy_out=rad step proj=omerc lat_0=4 lonc=115 alpha=53.3158204722222 gamma=53.1301023611111 k=0.99984 x_0=590476.87146304 y_0=442857.653094361 ellps=evrstSS step proj=unitconvert xy_in=m xy_out=20.1167651215526 +# op 29871 4326: Inverse of Rectified Skew Orthomorphic Borneo Grid (chain) + Timbalai 1948 to WGS 84 (1) (with axis order normalized for visualization) | proj=pipeline step proj=unitconvert xy_in=20.1167651215526 xy_out=m step inv proj=omerc lat_0=4 lonc=115 alpha=53.3158204722222 gamma=53.1301023611111 k=0.99984 x_0=590476.87146304 y_0=442857.653094361 ellps=evrstSS step proj=push v_3 step proj=cart ellps=evrstSS step proj=helmert x=-679 y=669 z=-48 step inv proj=cart ellps=WGS84 step proj=pop v_3 step proj=unitconvert xy_in=rad xy_out=deg +# op 29871 4326: Inverse of Rectified Skew Orthomorphic Borneo Grid (chain) + Timbalai 1948 to WGS 84 (5) (with axis order normalized for visualization) | proj=pipeline step proj=unitconvert xy_in=20.1167651215526 xy_out=m step inv proj=omerc lat_0=4 lonc=115 alpha=53.3158204722222 gamma=53.1301023611111 k=0.99984 x_0=590476.87146304 y_0=442857.653094361 ellps=evrstSS step proj=push v_3 step proj=cart ellps=evrstSS step proj=helmert x=-689.5937 y=623.84046 z=-65.93566 rx=0.02331 ry=-1.17094 rz=0.80054 s=5.88536 convention=coordinate_frame step inv proj=cart ellps=WGS84 step proj=pop v_3 step proj=unitconvert xy_in=rad xy_out=deg +# op 4298 29872: Rectified Skew Orthomorphic Borneo Grid (foot) (with axis order normalized for visualization) | proj=pipeline step proj=unitconvert xy_in=deg xy_out=rad step proj=omerc lat_0=4 lonc=115 alpha=53.3158204722222 gamma=53.1301023611111 k=0.99984 x_0=590476.872743198 y_0=442857.654557398 ellps=evrstSS step proj=unitconvert xy_in=m xy_out=0.304799471538676 +# op 29872 4326: Inverse of Rectified Skew Orthomorphic Borneo Grid (foot) + Timbalai 1948 to WGS 84 (1) (with axis order normalized for visualization) | proj=pipeline step proj=unitconvert xy_in=0.304799471538676 xy_out=m step inv proj=omerc lat_0=4 lonc=115 alpha=53.3158204722222 gamma=53.1301023611111 k=0.99984 x_0=590476.872743198 y_0=442857.654557398 ellps=evrstSS step proj=push v_3 step proj=cart ellps=evrstSS step proj=helmert x=-679 y=669 z=-48 step inv proj=cart ellps=WGS84 step proj=pop v_3 step proj=unitconvert xy_in=rad xy_out=deg +# op 29872 4326: Inverse of Rectified Skew Orthomorphic Borneo Grid (foot) + Timbalai 1948 to WGS 84 (5) (with axis order normalized for visualization) | proj=pipeline step proj=unitconvert xy_in=0.304799471538676 xy_out=m step inv proj=omerc lat_0=4 lonc=115 alpha=53.3158204722222 gamma=53.1301023611111 k=0.99984 x_0=590476.872743198 y_0=442857.654557398 ellps=evrstSS step proj=push v_3 step proj=cart ellps=evrstSS step proj=helmert x=-689.5937 y=623.84046 z=-65.93566 rx=0.02331 ry=-1.17094 rz=0.80054 s=5.88536 convention=coordinate_frame step inv proj=cart ellps=WGS84 step proj=pop v_3 step proj=unitconvert xy_in=rad xy_out=deg +# op 4298 29873: Rectified Skew Orthomorphic Borneo Grid (metre) (with axis order normalized for visualization) | proj=pipeline step proj=unitconvert xy_in=deg xy_out=rad step proj=omerc lat_0=4 lonc=115 alpha=53.3158204722222 gamma=53.1301023611111 k=0.99984 x_0=590476.87 y_0=442857.65 ellps=evrstSS +# op 29873 4326: Inverse of Rectified Skew Orthomorphic Borneo Grid (metre) + Timbalai 1948 to WGS 84 (1) (with axis order normalized for visualization) | proj=pipeline step inv proj=omerc lat_0=4 lonc=115 alpha=53.3158204722222 gamma=53.1301023611111 k=0.99984 x_0=590476.87 y_0=442857.65 ellps=evrstSS step proj=push v_3 step proj=cart ellps=evrstSS step proj=helmert x=-679 y=669 z=-48 step inv proj=cart ellps=WGS84 step proj=pop v_3 step proj=unitconvert xy_in=rad xy_out=deg +# op 29873 4326: Inverse of Rectified Skew Orthomorphic Borneo Grid (metre) + Timbalai 1948 to WGS 84 (5) (with axis order normalized for visualization) | proj=pipeline step inv proj=omerc lat_0=4 lonc=115 alpha=53.3158204722222 gamma=53.1301023611111 k=0.99984 x_0=590476.87 y_0=442857.65 ellps=evrstSS step proj=push v_3 step proj=cart ellps=evrstSS step proj=helmert x=-689.5937 y=623.84046 z=-65.93566 rx=0.02331 ry=-1.17094 rz=0.80054 s=5.88536 convention=coordinate_frame step inv proj=cart ellps=WGS84 step proj=pop v_3 step proj=unitconvert xy_in=rad xy_out=deg +# op 4326 32647: UTM zone 47N (with axis order normalized for visualization) | proj=pipeline step proj=unitconvert xy_in=deg xy_out=rad step proj=utm zone=47 ellps=WGS84 +# op 4326 32648: UTM zone 48N (with axis order normalized for visualization) | proj=pipeline step proj=unitconvert xy_in=deg xy_out=rad step proj=utm zone=48 ellps=WGS84 +# op 4326 32649: UTM zone 49N (with axis order normalized for visualization) | proj=pipeline step proj=unitconvert xy_in=deg xy_out=rad step proj=utm zone=49 ellps=WGS84 +# op 4326 32650: UTM zone 50N (with axis order normalized for visualization) | proj=pipeline step proj=unitconvert xy_in=deg xy_out=rad step proj=utm zone=50 ellps=WGS84 +# op 4742 3377: Johor Grid (with axis order normalized for visualization) | proj=pipeline step proj=unitconvert xy_in=deg xy_out=rad step proj=cass lat_0=2.12167974444444 lon_0=103.427936236111 x_0=-14810.562 y_0=8758.32 ellps=GRS80 +# op 3377 4326: Inverse of Johor Grid + GDM2000 to WGS 84 (1) (with axis order normalized for visualization) | proj=pipeline step inv proj=cass lat_0=2.12167974444444 lon_0=103.427936236111 x_0=-14810.562 y_0=8758.32 ellps=GRS80 step proj=unitconvert xy_in=rad xy_out=deg +# op 4742 3378: Sembilan and Melaka Grid (with axis order normalized for visualization) | proj=pipeline step proj=unitconvert xy_in=deg xy_out=rad step proj=cass lat_0=2.68234763611111 lon_0=101.974905041667 x_0=3673.785 y_0=-4240.573 ellps=GRS80 +# op 3378 4326: Inverse of Sembilan and Melaka Grid + GDM2000 to WGS 84 (1) (with axis order normalized for visualization) | proj=pipeline step inv proj=cass lat_0=2.68234763611111 lon_0=101.974905041667 x_0=3673.785 y_0=-4240.573 ellps=GRS80 step proj=unitconvert xy_in=rad xy_out=deg +# op 4742 3379: Pahang Grid (with axis order normalized for visualization) | proj=pipeline step proj=unitconvert xy_in=deg xy_out=rad step proj=cass lat_0=3.76938808888889 lon_0=102.368298983333 x_0=-7368.228 y_0=6485.858 ellps=GRS80 +# op 3379 4326: Inverse of Pahang Grid + GDM2000 to WGS 84 (1) (with axis order normalized for visualization) | proj=pipeline step inv proj=cass lat_0=3.76938808888889 lon_0=102.368298983333 x_0=-7368.228 y_0=6485.858 ellps=GRS80 step proj=unitconvert xy_in=rad xy_out=deg +# op 4742 3380: Selangor Grid (with axis order normalized for visualization) | proj=pipeline step proj=unitconvert xy_in=deg xy_out=rad step proj=cass lat_0=3.68464905 lon_0=101.389107913889 x_0=-34836.161 y_0=56464.049 ellps=GRS80 +# op 3380 4326: Inverse of Selangor Grid + GDM2000 to WGS 84 (1) (with axis order normalized for visualization) | proj=pipeline step inv proj=cass lat_0=3.68464905 lon_0=101.389107913889 x_0=-34836.161 y_0=56464.049 ellps=GRS80 step proj=unitconvert xy_in=rad xy_out=deg +# op 4742 3381: Terengganu Grid (with axis order normalized for visualization) | proj=pipeline step proj=unitconvert xy_in=deg xy_out=rad step proj=cass lat_0=4.9762852 lon_0=103.070275625 x_0=19594.245 y_0=3371.895 ellps=GRS80 +# op 3381 4326: Inverse of Terengganu Grid + GDM2000 to WGS 84 (1) (with axis order normalized for visualization) | proj=pipeline step inv proj=cass lat_0=4.9762852 lon_0=103.070275625 x_0=19594.245 y_0=3371.895 ellps=GRS80 step proj=unitconvert xy_in=rad xy_out=deg +# op 4742 3382: Pinang Grid (with axis order normalized for visualization) | proj=pipeline step proj=unitconvert xy_in=deg xy_out=rad step proj=cass lat_0=5.42151754166667 lon_0=100.344376963889 x_0=-23.414 y_0=62.283 ellps=GRS80 +# op 3382 4326: Inverse of Pinang Grid + GDM2000 to WGS 84 (1) (with axis order normalized for visualization) | proj=pipeline step inv proj=cass lat_0=5.42151754166667 lon_0=100.344376963889 x_0=-23.414 y_0=62.283 ellps=GRS80 step proj=unitconvert xy_in=rad xy_out=deg +# op 4742 3383: Kedah and Perlis Grid (with axis order normalized for visualization) | proj=pipeline step proj=unitconvert xy_in=deg xy_out=rad step proj=cass lat_0=5.96467271388889 lon_0=100.636371111111 x_0=0 y_0=0 ellps=GRS80 +# op 3383 4326: Inverse of Kedah and Perlis Grid + GDM2000 to WGS 84 (1) (with axis order normalized for visualization) | proj=pipeline step inv proj=cass lat_0=5.96467271388889 lon_0=100.636371111111 x_0=0 y_0=0 ellps=GRS80 step proj=unitconvert xy_in=rad xy_out=deg +# op 4742 3384: Perak Grid (with axis order normalized for visualization) | proj=pipeline step proj=unitconvert xy_in=deg xy_out=rad step proj=cass lat_0=4.85906302222222 lon_0=100.815410586111 x_0=-1.769 y_0=133454.779 ellps=GRS80 +# op 3384 4326: Inverse of Perak Grid + GDM2000 to WGS 84 (1) (with axis order normalized for visualization) | proj=pipeline step inv proj=cass lat_0=4.85906302222222 lon_0=100.815410586111 x_0=-1.769 y_0=133454.779 ellps=GRS80 step proj=unitconvert xy_in=rad xy_out=deg +# op 4742 3385: Kelantan Grid (with axis order normalized for visualization) | proj=pipeline step proj=unitconvert xy_in=deg xy_out=rad step proj=cass lat_0=5.97254365833333 lon_0=102.295241669444 x_0=13227.851 y_0=8739.894 ellps=GRS80 +# op 3385 4326: Inverse of Kelantan Grid + GDM2000 to WGS 84 (1) (with axis order normalized for visualization) | proj=pipeline step inv proj=cass lat_0=5.97254365833333 lon_0=102.295241669444 x_0=13227.851 y_0=8739.894 ellps=GRS80 step proj=unitconvert xy_in=rad xy_out=deg +# op 4326 3857: Popular Visualisation Pseudo-Mercator (with axis order normalized for visualization) | proj=pipeline step proj=unitconvert xy_in=deg xy_out=rad step proj=webmerc lat_0=0 lon_0=0 x_0=0 y_0=0 ellps=WGS84 +# op 4326 6933: US NSIDC EASE-Grid 2.0 Global (with axis order normalized for visualization) | proj=pipeline step proj=unitconvert xy_in=deg xy_out=rad step proj=cea lat_ts=30 lon_0=0 x_0=0 y_0=0 ellps=WGS84 +src dst x_in y_in x_out y_out tol_m +4742 4326 100.39625 3.975 100.39625 3.975 +4742 4326 101.49875 2.101457 101.49875 2.101457 +4742 4326 102.60125 5.132913 102.60125 5.132913 +4742 4326 103.70375 3.25937 103.70375 3.25937 +4742 4326 110.8 2.95 110.8 2.95 +4742 4326 112.6 1.540545 112.6 1.540545 +4742 4326 114.4 3.821091 114.4 3.821091 +4742 4326 116.18 5.7 116.18 5.7 +4742 4326 117.35 4.599938 117.35 4.599938 +4742 4326 118.52 6.379876 118.52 6.379876 +4245 4326 100.0655 3.975 100.06425824060575 3.9747475931519864 +4245 4326 100.5065 2.101457 100.5052023103922 2.1013453723718643 +4245 4326 100.9475 5.132913 100.94613980572743 5.1325754001038035 +4245 4326 101.3885 3.25937 101.38708507689846 3.2591730220785298 +4245 4326 101.8295 6.290827 101.82802071276089 6.290405002367701 +4245 4326 102.2705 4.417284 102.26896733586337 4.417002515446903 +4245 4326 102.7115 2.54374 102.70991269219815 2.543597807638683 +4245 4326 103.1525 5.575197 103.15084896983628 5.574831915540324 +4245 4326 103.5935 3.701654 103.59179577508374 3.701427985528418 +4245 4326 104.0345 1.82811 104.03274109712923 1.828021807729627 +4298 4326 110.44 2.95 110.44362151938917 2.949334550559947 +4298 4326 111.52 1.540545 111.52347106115936 1.5399881773372803 +4298 4326 112.6 3.821091 112.6033289954606 3.820348587055938 +4298 4326 113.68 2.411636 113.6831750885962 2.4110045847991226 +4298 4326 114.76 4.692182 114.76304244482851 4.691355995777432 19 +4298 4326 115.946 5.7 115.94886943189263 5.6990882296488845 +4298 4326 116.648 4.599938 116.65076506504143 4.599115518658288 +4298 4326 117.35 6.379876 117.35267312773075 6.378899777300668 +4298 4326 118.052 5.279814 118.05456757152426 5.2789281820337095 +4298 4326 118.754 7.059752 118.75647507332732 7.058710317535029 +4742 3375 100.0655 3.975 230230.7501121316 440481.36658161355 +3375 4326 230230.7501121316 440481.36658161355 100.0655 3.974999999999985 +4742 3375 100.5065 2.101457 278437.91456122114 232998.53787839765 +3375 4326 278437.91456122114 232998.53787839765 100.50650000000002 2.101457 +4742 3375 100.9475 5.132913 328659.6300119151 568106.3396916882 +3375 4326 328659.6300119151 568106.3396916882 100.9475 5.132912999999996 +4742 3375 101.3885 3.25937 376940.72017708037 360785.08950641454 +3375 4326 376940.72017708037 360785.08950641454 101.38850000000001 3.25936999999999 +4742 3375 101.8295 6.290827 426726.6577501332 695848.190900173 +3375 4326 426726.6577501332 695848.190900173 101.8295 6.2908269999999975 +4742 3375 102.2705 4.417284 475189.1927986404 488586.0773852139 +3375 4326 475189.1927986404 488586.0773852139 102.2705 4.4172840000000075 +4742 3375 102.7115 2.54374 523864.76810046035 281368.92721240415 +3375 4326 523864.76810046035 281368.92721240415 102.7115 2.543739999999998 +4742 3375 103.1525 5.575197 573141.2927076708 616525.1385258684 +3375 4326 573141.2927076708 616525.1385258684 103.1525 5.575196999999996 +4742 3375 103.5935 3.701654 621999.3868562107 409307.44915561675 +3375 4326 621999.3868562107 409307.44915561675 103.5935 3.7016539999999956 +4742 3375 104.0345 1.82811 670953.6140483341 202142.64473772547 +3375 4326 670953.6140483341 202142.64473772547 104.0345 1.8281099999999963 +4742 3376 110.44 2.95 83957.57468756929 326213.98642374465 +3376 4326 83957.57468756929 326213.98642374465 110.44 2.949999999999999 +4742 3376 111.52 1.540545 204101.08963759968 170363.31415304885 +3376 4326 204101.08963759968 170363.31415304885 111.52 1.5405450000000096 +4742 3376 112.6 3.821091 324031.1919567574 422592.91561689647 +3376 4326 324031.1919567574 422592.91561689647 112.6 3.8210909999999947 +4742 3376 113.68 2.411636 444272.90524017345 266913.6850285858 +3376 4326 444272.90524017345 266913.6850285858 113.68000000000002 2.411635999999998 +4742 3376 114.76 4.692182 563649.040369081 519340.0702826514 +3376 4326 563649.040369081 519340.0702826514 114.76 4.692182000000007 +4742 3376 115.946 5.7 694717.4377039279 631256.8181033176 +3376 4326 694717.4377039279 631256.8181033176 115.946 5.700000000000011 +4742 3376 116.648 4.599938 773145.5019824354 510015.2914989058 +3376 4326 773145.5019824354 510015.2914989058 116.648 4.599937999999988 +4742 3376 117.35 6.379876 849714.7164101849 707292.4174103282 +3376 4326 849714.7164101849 707292.4174103282 117.35000000000001 6.379875999999991 +4742 3376 118.052 5.279814 928338.6865779057 586231.6859419111 +3376 4326 928338.6865779057 586231.6859419111 118.052 5.279813999999993 +4742 3376 118.754 7.059752 1004310.7786909621 783710.2924517829 +3376 4326 1004310.7786909621 783710.2924517829 118.754 7.059752000000001 +4751 3168 100.0655 3.975 230287.07313047396 440448.09783019935 +3168 4326 230287.07313047396 440448.09783019935 100.06425823897055 3.9747476276837546 +4751 3168 100.5065 2.101457 278487.8089015883 232980.8927576611 +3168 4326 278487.8089015883 232980.8927576611 100.50520230868335 2.101345390699759 +4751 3168 100.9475 5.132913 328702.9927894074 568063.4432018951 +3168 4326 328702.9927894074 568063.4432018951 100.94613980393628 5.132575444581779 +4751 3168 101.3885 3.25937 376977.64809306595 360757.84373702965 +3168 4326 376977.64809306595 360757.84373702965 101.38708507503523 3.2591730504371172 +4751 3168 101.8295 6.290827 426757.0902677551 695795.6141733644 +3168 4326 426757.0902677551 695795.6141733644 101.82802071081291 6.290405056718831 +4751 3168 102.2705 4.417284 475213.1780635796 488549.19992824434 +3168 4326 475213.1780635796 488549.19992824434 102.26896733384508 4.4170025537900965 +4751 3168 102.7115 2.54374 523882.28848705447 281347.6569001513 +3168 4326 523882.28848705447 281347.6569001513 102.70991269010791 2.543597829805199 +4751 3168 103.1525 5.575197 573152.3567751534 616478.5793039716 +3168 4326 573152.3567751534 616478.5793039716 103.15084896766213 5.574831963805479 +4751 3168 103.5935 3.701654 622003.9724651785 409276.54558023607 +3168 4326 622003.9724651785 409276.54558023607 103.59179577283955 3.7014280177115912 +4751 3168 104.0345 1.82811 670951.7163456794 202127.31989316296 +3168 4326 670951.7163456794 202127.31989316296 104.03274109481303 1.8280218236889194 +4751 3167 100.0655 3.975 11447.525293366087 21894.58866182 +3167 4326 11447.525293366087 21894.58866182 100.06425823897055 3.9747476276837546 +4751 3167 100.5065 2.101457 13843.5744262936 11581.434539329359 +3167 4326 13843.5744262936 11581.434539329359 100.50520230868335 2.101345390699759 +4751 3167 100.9475 5.132913 16339.761380483385 28238.32248111451 +3167 4326 16339.761380483385 28238.32248111451 100.94613980393628 5.132575444581779 +4751 3167 101.3885 3.25937 18739.48503889325 17933.20174172365 +3167 4326 18739.48503889325 17933.20174172365 101.38708507503523 3.2591730504371172 +4751 3167 101.8295 6.290827 21214.011357882708 34587.86367808828 +3167 4326 21214.011357882708 34587.86367808828 101.82802071081291 6.290405056718831 +4751 3167 102.2705 4.417284 23622.753989936533 24285.68502437691 +3167 4326 23622.753989936533 24285.68502437691 102.26896733384508 4.4170025537900965 +4751 3167 102.7115 2.54374 26042.08593508091 13985.736910073936 +3167 4326 26042.08593508091 13985.736910073936 102.70991269010791 2.543597829805199 +4751 3167 103.1525 5.575197 28491.29137795147 30645.029412494325 +3167 4326 28491.29137795147 30645.029412494325 103.15084896766213 5.574831963805479 +4751 3167 103.5935 3.701654 30919.695624144297 20345.056905807083 +3167 4326 30919.695624144297 20345.056905807083 103.59179577283955 3.7014280177115912 +4751 3167 104.0345 1.82811 33352.8783838547 10047.709476277536 +3167 4326 33352.8783838547 10047.709476277536 104.03274109481303 1.8280218236889194 +4742 3380 100.87495 3.205 -91983.03053197026 3439.6127177552335 +3380 4326 -91983.03053197026 3439.6127177552335 100.87495 3.205000000000024 +4742 3380 100.98385 2.747787 -79897.74209382711 -47124.451246859186 +3380 4326 -79897.74209382711 -47124.451246859186 100.98385 2.7477870000000117 +4742 3380 101.09275 3.487573 -67765.8826877381 34676.83185789132 +3380 4326 -67765.8826877381 34676.83185789132 101.09275 3.487573000000017 +4742 3380 101.20165 3.03036 -55674.89524038668 -15884.186449098052 +3380 4326 -55674.89524038668 -15884.186449098052 101.20165 3.030360000000002 +4742 3380 101.31055 3.770147 -43562.38881598821 65918.7174971084 +3380 4326 -43562.38881598821 65918.7174971084 101.31055 3.7701469999999992 +4742 3380 101.41945 3.312933 -31464.102493612583 15360.328918868036 +3380 4326 -31464.102493612583 15360.328918868036 101.41945 3.3129330000000006 +4742 3380 101.52835 2.85572 -19354.923277214053 -35196.25425643279 +3380 4326 -19354.923277214053 -35196.25425643279 101.52835 2.8557200000000007 +4742 3380 101.63725 3.595507 -7267.119602319701 46610.57629156516 +3380 4326 -7267.119602319701 46610.57629156516 101.63725 3.5955070000000102 +4742 3380 101.74615 3.138293 4850.3730667412165 -3944.258585315998 +3380 4326 4850.3730667412165 -3944.258585315998 101.74615 3.1382930000000204 +4742 3380 101.85505 2.68108 16975.876534810748 -54498.480640116715 +3380 4326 16975.876534810748 -54498.480640116715 101.85505 2.6810799999999997 +4298 29871 110.44 2.95 4174.6263457040595 16214.807332387714 +29871 4326 4174.6263457040595 16214.807332387714 110.44362151938917 2.9493345505599495 +4298 29871 111.52 1.540545 10146.149490467267 8468.083900815884 +29871 4326 10146.149490467267 8468.083900815884 111.52347106115936 1.5399881773372897 +4298 29871 112.6 3.821091 16107.06234468698 21005.425544501388 +29871 4326 16107.06234468698 21005.425544501388 112.6033289954606 3.8203485870559346 +4298 29871 113.68 2.411636 22083.467229719594 13267.224468265305 +29871 4326 22083.467229719594 13267.224468265305 113.6831750885962 2.4110045847991217 +4298 29871 114.76 4.692182 28016.844183642694 25814.34554781927 +29871 4326 28016.844183642694 25814.34554781927 114.76304244482851 4.6913559957774345 19 +4298 29871 115.946 5.7 34531.36551661241 31377.283862312666 +29871 4326 34531.36551661241 31377.283862312666 115.94886943189263 5.699088229648898 +4298 29871 116.648 4.599938 38429.49681378292 25350.847048351785 +29871 4326 38429.49681378292 25350.847048351785 116.65076506504143 4.5991155186582695 +4298 29871 117.35 6.379876 42235.22948477033 35156.7104106337 +29871 4326 42235.22948477033 35156.7104106337 117.35267312773075 6.378899777300651 +4298 29871 118.052 5.279814 46143.09826774536 29139.260767176853 +29871 4326 46143.09826774536 29139.260767176853 118.05456757152426 5.278928182033696 +4298 29871 118.754 7.059752 49919.15074868361 38955.136131799525 +29871 4326 49919.15074868361 38955.136131799525 118.75647507332732 7.058710317535034 +4298 29872 110.44 2.95 275525.3430164682 1070177.2887375867 +29872 4326 275525.3430164682 1070177.2887375867 110.44362151938917 2.9493345505599495 +4298 29872 111.52 1.540545 669645.8705708397 558893.5422538463 +29872 4326 669645.8705708397 558893.5422538463 111.52347106115936 1.5399881773372897 +4298 29872 112.6 3.821091 1063066.1189493402 1386358.0907370888 +29872 4326 1063066.1189493402 1386358.0907370888 112.6033289954606 3.8203485870559346 +4298 29872 113.68 2.411636 1457508.8413614924 875636.8197055078 +29872 4326 1457508.8413614924 875636.8197055078 113.6831750885962 2.4110045847991217 +4298 29872 114.76 4.692182 1849111.7203204168 1703746.810956069 +29872 4326 1849111.7203204168 1703746.810956069 114.76304244482851 4.6913559957774345 19 +4298 29872 115.946 5.7 2279070.1282964176 2070900.7397126327 +29872 4326 2279070.1282964176 2070900.7397126327 115.94886943189263 5.699088229648898 +4298 29872 116.648 4.599938 2536346.7939096713 1673155.9099912148 +29872 4326 2536346.7939096713 1673155.9099912148 116.65076506504143 4.5991155186582695 +4298 29872 117.35 6.379876 2787525.1501948405 2320342.8919018204 +29872 4326 2787525.1501948405 2320342.8919018204 117.35267312773075 6.378899777300651 +4298 29872 118.052 5.279814 3045444.489871192 1923191.2154336693 +29872 4326 3045444.489871192 1923191.2154336693 118.05456757152426 5.278928182033696 +4298 29872 118.754 7.059752 3294663.953613116 2571038.989498765 +29872 4326 3294663.953613116 2571038.989498765 118.75647507332732 7.058710317535034 +4298 29873 110.44 2.95 83979.976203734 326189.4675025116 +29873 4326 83979.976203734 326189.4675025116 110.44362151938917 2.9493345505599495 +4298 29873 111.52 1.540545 204107.7047248506 170350.45176795305 +29873 4326 204107.7047248506 170350.45176795305 111.52347106115936 1.5399881773372897 +4298 29873 112.6 3.821091 324021.9885232322 422561.20886263455 +29873 4326 324021.9885232322 422561.20886263455 112.6033289954606 3.8203485870559346 +4298 29873 113.68 2.411636 444247.9218667329 266893.6353486477 +29873 4326 444247.9218667329 266893.6353486477 113.6831750885962 2.4110045847991217 +4298 29873 114.76 4.692182 563608.2724264371 519301.1230577163 +29873 4326 563608.2724264371 519301.1230577163 114.76304244482851 4.6913559957774345 19 +4298 29873 115.946 5.7 694659.3679611327 631209.4465160656 +29873 4326 694659.3679611327 631209.4465160656 115.94886943189263 5.699088229648898 +4298 29873 116.648 4.599938 773077.159679285 509977.03260973684 +29873 4326 773077.159679285 509977.03260973684 116.65076506504143 4.5991155186582695 +4298 29873 117.35 6.379876 849636.1899369578 707239.2826828002 +29873 4326 849636.1899369578 707239.2826828002 117.35267312773075 6.378899777300655 +4298 29873 118.052 5.279814 928249.868369914 586187.6615746083 +29873 4326 928249.868369914 586187.6615746083 118.05456757152426 5.278928182033696 +4298 29873 118.754 7.059752 1004211.8292156048 783651.3207471571 +29873 4326 1004211.8292156048 783651.3207471571 118.75647507332732 7.058710317535034 +4326 32647 99.828 3.975 591918.6955871824 439410.04865611164 +4326 32647 100.044 2.101457 616099.9049176839 232313.97444052142 +4326 32647 100.26 5.132913 639659.0957069639 567493.7318410194 +4326 32647 100.476 3.25937 663996.0837599866 360382.33666203415 +4326 32647 100.692 6.290827 687177.7827230431 695656.2965571918 +4326 32647 100.908 4.417284 711725.1033751175 488523.90191766125 +4326 32647 101.124 2.54374 736170.937218107 281355.90490326635 +4326 32647 101.34 5.575197 759231.0605783813 616760.7748830264 +4326 32647 101.556 3.701654 783923.4308340647 409558.77049893176 +4326 32647 101.772 1.82811 808419.1600435992 202299.91850340605 +4326 32648 102.2375 3.975 193218.92490986176 439876.9843452656 +4326 32648 102.4625 2.101457 217735.7497665371 232504.49629903794 +4326 32648 102.6875 5.132913 243632.7303130351 567819.3804349332 +4326 32648 102.9125 3.25937 268035.4187852198 360502.5519673748 +4326 32648 103.1375 6.290827 293954.3952747155 695720.4474298684 +4326 32648 103.3625 4.417284 318300.2837703946 488452.327326002 +4326 32648 103.5875 2.54374 342962.00389679376 281247.4778254855 +4326 32648 103.8125 5.575197 368472.6021570618 616378.7082181394 +4326 32648 104.0375 3.701654 393114.7793272923 409207.66406425077 +4326 32648 104.2625 1.82811 417973.92847368016 202078.6126753299 +4326 32649 110.018 2.95 390866.4726391021 326114.66229918605 +4326 32649 110.414 1.540545 434815.1406048709 170285.88382103096 +4326 32649 110.81 3.821091 478904.4039686892 422353.9774394202 +4326 32649 111.206 2.411636 522902.52514259046 266561.5508905057 +4326 32649 111.602 4.692182 566765.7385047607 518667.412962508 +4326 32649 111.998 3.282727 610877.0144409596 362899.25116621976 +4326 32649 112.394 1.873273 655050.3218869393 207115.35822036478 +4326 32649 112.79 4.153818 698694.6587573757 459354.6374083769 +4326 32649 113.186 2.744363 743029.0469695157 303558.95653923333 +4326 32649 113.582 1.334909 787332.3185012396 147698.72574919564 +4326 32650 114.224 2.95 191386.37768871605 326451.5164417412 +4326 32650 114.512 1.540545 223159.81395745237 170438.59366008558 +4326 32650 114.8 3.821091 255675.7389542038 422664.35632953414 +4326 32650 115.088 2.411636 287389.8598304349 266709.13322730456 +4326 32650 115.376 4.692182 319867.10814709694 518847.59245471127 +4326 32650 115.946 5.7 383285.74050430703 630148.724709863 +4326 32650 116.648 4.599938 460956.2924267269 508451.93309397344 +4326 32650 117.35 6.379876 538706.8794975952 705210.3201332391 +4326 32650 118.052 5.279814 616574.3776817677 583693.1328034763 +4326 32650 118.754 7.059752 693735.8998680577 780718.8002361313 +4742 3377 102.6357 2.1 -102943.05888222714 6383.394461471187 +3377 4326 -102943.05888222714 6383.394461471187 102.63570000000001 2.099999999999017 +4742 3377 102.8211 1.515592 -82339.78769613935 -58250.61858835896 +3377 4326 -82339.78769613935 -58250.61858835896 102.8211 1.515591999999852 +4742 3377 103.0065 2.461184 -61681.6415265526 46306.76347517308 +3377 4326 -61681.6415265526 46306.76347517308 103.0065 2.4611840000000003 +4742 3377 103.1919 1.876776 -41071.99495664491 -18320.293971550902 +3377 4326 -41071.99495664491 -18320.293971550902 103.1919 1.8767760000000007 +4742 3377 103.3773 2.822368 -20440.57021594833 86237.99595681741 +3377 4326 -20440.57021594833 86237.99595681741 103.3773 2.822368 +4742 3377 103.5627 2.23796 179.90562904658873 21616.800307967278 +3377 4326 179.90562904658873 21616.800307967278 103.5627 2.23796 +4742 3377 103.7481 1.653552 20815.16294433894 -43002.260917463704 +3377 4326 20815.16294433894 -43002.260917463704 103.7481 1.6535519999999992 +4742 3377 103.9335 2.599144 41411.02698069734 61565.73523188323 +3377 4326 41411.02698069734 61565.73523188323 103.9335 2.599143999999979 +4742 3377 104.1189 2.014736 62059.93891773758 -3050.765267065719 +3377 4326 62059.93891773758 -3050.765267065719 104.1189 2.014735999999608 +4742 3377 104.3043 1.430328 82715.61008447011 -67669.5257963416 +3377 4326 82715.61008447011 -67669.5257963416 104.3043 1.4303279999985656 +4742 3378 101.79595 2.655 -16226.1578560327 -7263.14432224173 +3378 4326 -16226.1578560327 -7263.14432224173 101.79595 2.6550000000000016 +4742 3378 101.88685 2.225288 -6121.114720839751 -54780.25196904669 +3378 4326 -6121.114720839751 -54780.25196904669 101.88685 2.2252880000000004 +4742 3378 101.97775 2.920576 3990.0757082156715 22101.98847759753 +3378 4326 3990.0757082156715 22101.98847759753 101.97775 2.9205760000000005 +4742 3378 102.06865 2.490865 14099.631954987166 -25413.689362791007 +3378 4326 14099.631954987166 -25413.689362791007 102.06865 2.4908650000000003 +4742 3378 102.15955 3.186153 24196.80709345181 51470.64773881747 +3378 4326 24196.80709345181 51470.64773881747 102.15955 3.1861530000000022 +4742 3378 102.25045 2.756441 34312.05667331084 3955.975220621651 +3378 4326 34312.05667331084 3955.975220621651 102.25045 2.756441000000006 +4742 3378 102.34135 2.326729 44432.84481248236 -43558.309779265765 +3378 4326 44432.84481248236 -43558.309779265765 102.34135 2.3267290000000025 +4742 3378 102.43225 3.022018 54514.864358714316 33329.864150373876 +3378 4326 54514.864358714316 33329.864150373876 102.43225 3.0220180000000205 +4742 3378 102.52315 2.592306 64642.094887374784 -14183.879960589597 +3378 4326 64642.094887374784 -14183.879960589597 102.52315 2.5923059999999447 +4742 3378 102.61405 2.162594 74772.73808404192 -61698.02471704999 +3378 4326 74772.73808404192 -61698.02471704999 102.61405000000002 2.162593999999767 +4742 3379 101.5523 3.615 -98025.27190672375 -10545.496766705852 +3379 4326 -98025.27190672375 -10545.496766705852 101.55230000000003 3.614999999998964 +4742 3379 101.7629 2.814017 -74680.20953133455 -99139.67073472319 +3379 4326 -74680.20953133455 -99139.67073472319 101.76290000000002 2.8140169999998834 +4742 3379 101.9735 4.110035 -51204.776158689165 44165.2549489294 +3379 4326 -51204.776158689165 44165.2549489294 101.9735 4.110035000000083 +4742 3379 102.1841 3.309052 -27839.205527797953 -44415.522878195254 +3379 4326 -27839.205527797953 -44415.522878195254 102.18410000000002 3.3090520000000025 +4742 3379 102.3947 4.605069 -4438.704632551182 98895.68712431152 +3379 4326 -4438.704632551182 98895.68712431152 102.3947 4.605069 +4742 3379 102.6053 3.804086 18956.863765473965 10326.334701730955 +3379 4326 18956.863765473965 10326.334701730955 102.60530000000001 3.8040860000000114 +4742 3379 102.8159 3.003104 42390.51787398435 -78238.248530162 +3379 4326 42390.51787398435 -78238.248530162 102.8159 3.0031040000000204 +4742 3379 103.0265 4.299121 65697.57664627078 65095.06904027329 +3379 4326 65697.57664627078 65095.06904027329 103.0265 4.299121000000238 +4742 3379 103.2371 3.498138 89167.247796753 -23463.972254624074 +3379 4326 89167.247796753 -23463.972254624074 103.23709999999997 3.4981379999981694 +4742 3379 103.4477 2.697155 112657.90752356392 -112026.15115340968 +3379 4326 112657.90752356392 -112026.15115340968 103.44769999999995 2.6971549999917155 +4742 3380 100.87495 3.205 -91983.03053197026 3439.6127177552335 +3380 4326 -91983.03053197026 3439.6127177552335 100.87495 3.205000000000024 +4742 3380 100.98385 2.747787 -79897.74209382711 -47124.451246859186 +3380 4326 -79897.74209382711 -47124.451246859186 100.98385 2.7477870000000117 +4742 3380 101.09275 3.487573 -67765.8826877381 34676.83185789132 +3380 4326 -67765.8826877381 34676.83185789132 101.09275 3.487573000000017 +4742 3380 101.20165 3.03036 -55674.89524038668 -15884.186449098052 +3380 4326 -55674.89524038668 -15884.186449098052 101.20165 3.030360000000002 +4742 3380 101.31055 3.770147 -43562.38881598821 65918.7174971084 +3380 4326 -43562.38881598821 65918.7174971084 101.31055 3.7701469999999992 +4742 3380 101.41945 3.312933 -31464.102493612583 15360.328918868036 +3380 4326 -31464.102493612583 15360.328918868036 101.41945 3.3129330000000006 +4742 3380 101.52835 2.85572 -19354.923277214053 -35196.25425643279 +3380 4326 -19354.923277214053 -35196.25425643279 101.52835 2.8557200000000007 +4742 3380 101.63725 3.595507 -7267.119602319701 46610.57629156516 +3380 4326 -7267.119602319701 46610.57629156516 101.63725 3.5955070000000102 +4742 3380 101.74615 3.138293 4850.3730667412165 -3944.258585315998 +3380 4326 4850.3730667412165 -3944.258585315998 101.74615 3.1382930000000204 +4742 3380 101.85505 2.68108 16975.876534810748 -54498.480640116715 +3380 4326 16975.876534810748 -54498.480640116715 101.85505 2.6810799999999997 +4742 3381 102.5073 4.895 -42848.855478809375 -5590.647024471553 +3381 4326 -42848.855478809375 -5590.647024471553 102.50730000000001 4.895000000000469 +4742 3381 102.6279 4.204023 -29519.163151723867 -82012.04364454187 +3381 4326 -29519.163151723867 -82012.04364454187 102.6279 4.204023000000128 +4742 3381 102.7485 5.322047 -16072.262520938111 41616.6389929906 +3381 4326 -16072.262520938111 41616.6389929906 102.7485 5.322047000000103 +4742 3381 102.8691 4.63107 -2727.8964115953204 -34799.55097555642 +3381 4326 -2727.8964115953204 -34799.55097555642 102.8691 4.631070000000011 +4742 3381 102.9897 5.749094 10669.424076581558 88832.80550805805 +3381 4326 10669.424076581558 88832.80550805805 102.9897 5.7490939999999995 +4742 3381 103.1103 5.058117 24032.502853297614 12421.218694478192 +3381 4326 24032.502853297614 12421.218694478192 103.1103 5.058116999999999 +4742 3381 103.2309 4.367141 37423.29968927939 -63986.37393456291 +3381 4326 37423.29968927939 -63986.37393456291 103.23090000000002 4.367141000000004 +4742 3381 103.3515 5.485164 50757.60159903571 59652.810504618654 +3381 4326 50757.60159903571 59652.810504618654 103.3515 5.485164000000066 +4742 3381 103.4721 4.794188 64169.671407789356 -16751.773684155236 +3381 4326 64169.671407789356 -16751.773684155236 103.4721 4.7941880000001555 +4742 3381 103.5927 4.103211 77602.18340710505 -93154.82084794727 +3381 4326 77602.18340710505 -93154.82084794727 103.5927 4.103211000000169 +4742 3382 100.1618 5.355 -20259.674006968242 -7290.488274947231 +3382 4326 -20259.674006968242 -7290.488274947231 100.1618 5.355000000000012 +4742 3382 100.2014 5.193428 -15874.632353093692 -25158.93007448112 +3382 4326 -15874.632353093692 -25158.93007448112 100.2014 5.193428000000005 +4742 3382 100.241 5.454857 -11479.516968253312 3750.08442893312 +3382 4326 -11479.516968253312 3750.08442893312 100.241 5.454857000000001 +4742 3382 100.2806 5.293285 -7092.958321792349 -14117.815822422051 +3382 4326 -7092.958321792349 -14117.815822422051 100.2806 5.293285000000001 +4742 3382 100.3202 5.554714 -2702.227246201004 14791.792559548221 +3382 4326 -2702.227246201004 14791.792559548221 100.3202 5.554714000000001 +4742 3382 100.3598 5.393142 1685.9208030487246 -3075.580157805521 +3382 4326 1685.9208030487246 -3075.580157805521 100.35980000000002 5.393142 +4742 3382 100.3994 5.23157 6076.376678864045 -20942.57966029924 +3382 4326 6076.376678864045 -20942.57966029924 100.3994 5.2315700000000005 +4742 3382 100.439 5.492999 10461.925403855397 7967.84021099873 +3382 4326 10461.925403855397 7967.84021099873 100.439 5.492999000000001 +4742 3382 100.4786 5.331427 14854.016562437238 -9898.672531848271 +3382 4326 14854.016562437238 -9898.672531848271 100.47860000000001 5.331427000000004 +4742 3382 100.5182 5.169855 19248.284976287527 -27764.867070105338 +3382 4326 19248.284976287527 -27764.867070105338 100.5182 5.169855000000009 +4742 3383 99.7444 5.89 -98773.01212868199 -8178.859946341446 +3383 4326 -98773.01212868199 -8178.859946341446 99.7444000000001 5.8900000000022645 +4742 3383 99.8812 5.333094 -83703.75954479261 -69791.88674568085 +3383 4326 -83703.75954479261 -69791.88674568085 99.88120000000005 5.333094000001187 +4742 3383 100.018 6.234187 -68432.36596787628 29844.829617079893 +3383 4326 -68432.36596787628 29844.829617079893 100.01800000000001 6.23418700000169 +4742 3383 100.1548 5.677281 -53347.03698208393 -31759.23511921311 +3383 4326 -53347.03698208393 -31759.23511921311 100.15480000000001 5.677281000000529 +4742 3383 100.2916 6.578374 -38128.72727618056 67880.85661414126 +3383 4326 -38128.72727618056 67880.85661414126 100.2916 6.578374000000258 +4742 3383 100.4284 6.021468 -23024.35245941423 6285.168634726624 +3383 4326 -23024.35245941423 6285.168634726624 100.4284 6.021468000000028 +4742 3383 100.5652 5.464561 -7886.964731694595 -55304.53370538347 +3383 4326 -7886.964731694595 -55304.53370538347 100.5652 5.464560999999999 +4742 3383 100.702 6.365655 7261.029922578413 44343.92433595619 +3383 4326 7261.029922578413 44343.92433595619 100.702 6.365655 +4742 3383 100.8388 5.808748 22419.341531902355 -17239.075416026568 +3383 4326 22419.341531902355 -17239.075416026568 100.8388 5.808748000000022 +4742 3383 100.9756 5.251842 37605.311222535165 -78818.11955578055 +3383 4326 37605.311222535165 -78818.11955578055 100.9756 5.251842000000116 +4742 3384 100.25335 4.79 -62352.99254259321 125843.18162589375 +3384 4326 -62352.99254259321 125843.18162589375 100.25335 4.7900000000004255 +4742 3384 100.42705 4.013081 -43128.57623074498 39915.521970973685 +3384 4326 -43128.57623074498 39915.521970973685 100.42705000000001 4.0130810000000725 +4742 3384 100.60075 5.270162 -23797.331701208685 178919.40456525076 +3384 4326 -23797.331701208685 178919.40456525076 100.60075 5.270162000000021 +4742 3384 100.77445 4.493243 -4547.5600608861405 93001.9212001422 +3384 4326 -4547.5600608861405 93001.9212001422 100.77445 4.4932430000000005 +4742 3384 100.94815 5.750325 14700.852864608643 232015.61193105223 +3384 4326 14700.852864608643 232015.61193105223 100.94815000000001 5.750325000000005 +4742 3384 101.12185 4.973406 33983.33153081521 146106.9825049647 +3384 4326 33983.33153081521 146106.9825049647 101.12185 4.973406000000068 +4742 3384 101.29555 4.196487 53304.75992698857 60202.66704221834 +3384 4326 53304.75992698857 60202.66704221834 101.29555 4.196487000000158 +4742 3384 101.46925 5.453568 72456.01966130888 199236.36862978307 +3384 4326 72456.01966130888 199236.36862978307 101.46925 5.453568000001104 +4742 3384 101.64295 4.676649 91814.81943815615 113337.14711688069 +3384 4326 91814.81943815615 113337.14711688069 101.64294999999996 4.676648999999883 +4742 3384 101.81665 3.89973 111199.31950851007 27437.07745501236 +3384 4326 111199.31950851007 27437.07745501236 101.81664999999992 3.899729999994957 +4742 3385 101.4573 5.415 -79637.84977459219 -52852.10182035459 +3385 4326 -79637.84977459219 -52852.10182035459 101.45730000000007 5.415000000001177 +4742 3385 101.5779 4.813404 -66346.49414167031 -119400.74553966592 +3385 4326 -66346.49414167031 -119400.74553966592 101.57790000000003 4.813404000000559 +4742 3385 101.6985 5.786807 -52864.83939036649 -11765.262616118733 +3385 4326 -52864.83939036649 -11765.262616118733 101.69850000000001 5.786807000001127 +4742 3385 101.8191 5.185211 -39560.5309158749 -78307.29703531507 +3385 4326 -39560.5309158749 -78307.29703531507 101.8191 5.185211000000367 +4742 3385 101.9397 6.158614 -26123.959635555548 29329.89332373483 +3385 4326 -26123.959635555548 29329.89332373483 101.9397 6.1586140000002345 +4742 3385 102.0603 5.557018 -12803.63902628199 -37206.04370503098 +3385 4326 -12803.63902628199 -37206.04370503098 102.0603 5.557018000000035 +4742 3385 102.1809 4.955421 546.654242658622 -103736.88390812428 +3385 4326 546.654242658622 -103736.88390812428 102.18090000000001 4.955421000000002 +4742 3385 102.3015 5.928825 13920.823392990731 3905.2171412172493 +3385 4326 13920.823392990731 3905.2171412172493 102.3015 5.928825 +4742 3385 102.4221 5.327228 27289.064911934787 -62620.92504563507 +3385 4326 27289.064911934787 -62620.92504563507 102.4221 5.3272280000000025 +4742 3385 102.5427 4.725632 40681.76727339565 -129143.66208139047 +3385 4326 40681.76727339565 -129143.66208139047 102.5427 4.725632000000026 +4326 3857 100.39625 3.975 11176059.42755419 442850.3690855275 +4326 3857 101.49875 2.101457 11298789.166153776 233985.58968988806 +4326 3857 102.60125 5.132913 11421518.904753359 572159.1030294127 +4326 3857 103.70375 3.25937 11544248.643352944 363027.2603552046 +4326 3857 110.8 2.95 12334199.579894712 328537.68516110635 +4326 3857 112.6 1.540545 12534574.663322603 171513.35189294594 +4326 3857 114.4 3.821091 12734949.746750496 425677.56478547823 +4326 3857 116.18 5.7 12933098.440362522 635570.339317789 +4326 3857 117.35 4.599938 13063342.244590653 512613.7299510645 +4326 3857 118.52 6.379876 13193586.048818784 711676.7257708234 +4326 6933 100.39625 3.975 9686860.71363907 506707.99316558754 +4326 6933 101.49875 2.101457 9793236.837615684 268031.2859341505 +4326 6933 102.60125 5.132913 9899612.961592294 653970.4038583545 +4326 6933 103.70375 3.25937 10005989.085568909 415590.31534376176 +4326 6933 110.8 2.95 10690679.851799333 376179.3942345135 +4326 6933 112.6 1.540545 10864355.156250946 196509.34640428936 +4326 6933 114.4 3.821091 11038030.46070256 487117.5323631399 +4326 6933 116.18 5.7 11209776.039549157 726000.8350074961 +4326 6933 117.35 4.599938 11322664.987442706 586215.8439725466 +4326 6933 118.52 6.379876 11435553.935336255 812265.66756127 diff --git a/nodedb-crs/tests/gdm2020.rs b/nodedb-crs/tests/gdm2020.rs new file mode 100644 index 000000000..f0b251598 --- /dev/null +++ b/nodedb-crs/tests/gdm2020.rs @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! GDM2020 (EPSG:12287) and its state Cassini grids (EPSG:25314 to 25322) +//! against the JUPEM worked example. +//! +//! Source: JUPEM, "Technical Guide to the Coordinate Conversion, Datum +//! Transformation and Map Projection", circular KPUP 3/2021 (Lampiran A), +//! §4.6 test example (i), "GDM2020 to Geocentric Cassini (GDM2020)", +//! Kedah/Perlis. + +use nodedb_crs::{CrsContext, CrsKind, DatumShift, Srid}; + +/// Degrees from degrees, minutes and seconds. +fn dms(d: f64, m: f64, s: f64) -> f64 { + d + m / 60.0 + s / 3600.0 +} + +/// Latitude of the worked-example point, N 6° 08′ 22.98682″. +fn example_lat() -> f64 { + dms(6.0, 8.0, 22.986_82) +} + +/// Longitude of the worked-example point, E 100° 23′ 6.56827″. +fn example_lon() -> f64 { + dms(100.0, 23.0, 6.568_27) +} + +/// Northing the guide prints, metres. +const JUPEM_NORTHING_M: f64 = 19_364.381; + +/// Easting from PROJ 9.8.1 `+proj=cass` with the EPSG 12320 origin on +/// GRS 1980, metres. The guide prints −27805.019 m, exactly 1.000 m off +/// this value: its GDM2000 (Rev 2009) and GDM2000 examples on the same page +/// agree with PROJ to the millimetre. +const PROJ_EASTING_M: f64 = -27_806.019_141_437_544; + +/// The guide prints millimetres. +const HALF_MM: f64 = 5e-4; + +#[test] +fn kedah_perlis_grid_matches_the_jupem_example() { + let ctx = CrsContext::builtin_only(); + let gdm2020 = Srid::new(12287).unwrap(); + let kedah = Srid::new(25320).unwrap(); + let t = ctx.transformer(gdm2020, kedah).unwrap(); + let [e, n, _] = t.apply([example_lon(), example_lat(), 0.0]).unwrap(); + assert!((n - JUPEM_NORTHING_M).abs() < HALF_MM, "northing {n}"); + assert!((e - PROJ_EASTING_M).abs() < 1e-3, "easting {e}"); + let back = ctx.transformer(kedah, gdm2020).unwrap(); + let [lon, lat, _] = back.apply([e, n, 0.0]).unwrap(); + assert!((lon - example_lon()).abs() < 1e-9 && (lat - example_lat()).abs() < 1e-9); +} + +/// Each grid origin maps to (0, 0): JUPEM KPUP 3/2021 Table 7 gives the +/// GDM2020 origins with Cassini northing and easting 0.000. +#[test] +fn every_state_origin_maps_to_zero() { + let ctx = CrsContext::builtin_only(); + let gdm2020 = Srid::new(12287).unwrap(); + // (EPSG code, Table 7 latitude, longitude as degrees, minutes, seconds) + let origins = [ + (25314, (2.0, 2.0, 33.195_81), (103.0, 33.0, 39.851_96)), + (25315, (2.0, 42.0, 43.629_44), (101.0, 56.0, 22.944_46)), + (25316, (3.0, 42.0, 38.687_85), (102.0, 26.0, 4.622_19)), + (25317, (3.0, 40.0, 48.373_10), (101.0, 30.0, 24.499_70)), + (25318, (4.0, 56.0, 44.967_15), (102.0, 53.0, 37.018_99)), + (25319, (5.0, 25.0, 15.199_41), (100.0, 20.0, 40.772_28)), + (25320, (5.0, 57.0, 52.817_46), (100.0, 38.0, 10.949_96)), + (25321, (4.0, 51.0, 32.640_21), (100.0, 48.0, 55.483_63)), + (25322, (5.0, 53.0, 37.075_11), (102.0, 10.0, 32.258_23)), + ]; + for (code, lat, lon) in origins { + let grid = Srid::new(code).unwrap(); + let t = ctx.transformer(gdm2020, grid).unwrap(); + let [e, n, _] = t + .apply([dms(lon.0, lon.1, lon.2), dms(lat.0, lat.1, lat.2), 0.0]) + .unwrap(); + assert!(e.abs() < HALF_MM && n.abs() < HALF_MM, "{code}: ({e}, {n})"); + } +} + +/// EPSG transformation 12291 GDM2020 to WGS 84 (1) is a null geocentric +/// translation with an accuracy of 1 m. +#[test] +fn gdm2020_to_wgs84_is_the_one_metre_null_shift() { + let ctx = CrsContext::builtin_only(); + let def = ctx.resolve(Srid::new(12287).unwrap()).unwrap(); + assert_eq!(def.kind, CrsKind::Geographic); + assert_eq!(def.datum.epsg_code, Some(1471)); + let DatumShift::Helmert(h) = def.datum.to_wgs84 else { + panic!("expected a Helmert shift"); + }; + assert!(h.is_zero()); + assert_eq!(h.accuracy_m, Some(1.0)); +} diff --git a/nodedb-crs/tests/proj_reference.rs b/nodedb-crs/tests/proj_reference.rs new file mode 100644 index 000000000..60ae133a9 --- /dev/null +++ b/nodedb-crs/tests/proj_reference.rs @@ -0,0 +1,154 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! PROJ 9.8.1 reference points, written by `scripts/gen_epsg.py`. +//! +//! `proj_reference.tsv` comes from the PROJ strings of the built-in table. +//! `proj_reference_malaysia.tsv` comes from the official EPSG definitions. +//! A Malaysian row with `tol_m` is one where PROJ picks another Helmert than +//! the built-in table. It is held to that Helmert's accuracy, in metres. + +use nodedb_crs::{CrsContext, CrsKind, Srid}; + +const GENERATED: &str = include_str!("fixtures/proj_reference.tsv"); +const MALAYSIA: &str = include_str!("fixtures/proj_reference_malaysia.tsv"); + +/// Projected tolerance, metres. +const PROJECTED_TOL_M: f64 = 1e-3; + +/// Geographic tolerance, degrees. +const GEOGRAPHIC_TOL_DEG: f64 = 1e-9; + +/// Metres per degree on the equator. The generator converts a degree error +/// to metres with the same factor before it compares it with `tol_m`. +const METRES_PER_DEGREE: f64 = 111_320.0; + +/// Degrees in a full turn of longitude. +const FULL_TURN_DEG: f64 = 360.0; + +/// Number of failing rows the assertion message lists. +const SHOWN: usize = 20; + +/// One fixture row. +struct Row { + line: usize, + src: u32, + dst: u32, + input: [f64; 2], + expected: [f64; 2], + tol_m: Option, +} + +/// The data rows of a fixture. A data row starts with a digit. +fn rows(name: &str, text: &str) -> Vec { + text.lines() + .enumerate() + .filter(|(_, l)| l.as_bytes().first().is_some_and(u8::is_ascii_digit)) + .map(|(i, l)| { + let f: Vec<&str> = l.split('\t').collect(); + let num = |k: usize| -> f64 { + f.get(k) + .and_then(|v| v.parse().ok()) + .unwrap_or_else(|| panic!("{name}:{}: bad column {k}: {l}", i + 1)) + }; + let code = |k: usize| -> u32 { + f.get(k) + .and_then(|v| v.parse().ok()) + .unwrap_or_else(|| panic!("{name}:{}: bad code column {k}: {l}", i + 1)) + }; + Row { + line: i + 1, + src: code(0), + dst: code(1), + input: [num(2), num(3)], + expected: [num(4), num(5)], + tol_m: f.get(6).filter(|v| !v.is_empty()).map(|_| num(6)), + } + }) + .collect() +} + +/// Transform one row and compare it with the PROJ output. +fn check_row(ctx: &CrsContext<'_>, row: &Row) -> Result<(), String> { + let srid = |code: u32| Srid::new(code).map_err(|e| e.to_string()); + let (src, dst) = (srid(row.src)?, srid(row.dst)?); + let transformer = ctx.transformer(src, dst).map_err(|e| e.to_string())?; + let target = ctx.resolve(dst).map_err(|e| e.to_string())?; + let [x, y] = row.input; + let got = transformer.apply([x, y, 0.0]).map_err(|e| e.to_string())?; + let dy = (got[1] - row.expected[1]).abs(); + let (error_m, tol_m) = match target.kind { + CrsKind::Geographic => { + // ±180° name one meridian. + let dx = (got[0] - row.expected[0] + FULL_TURN_DEG / 2.0).rem_euclid(FULL_TURN_DEG) + - FULL_TURN_DEG / 2.0; + let error_deg = dx.abs().max(dy); + match row.tol_m { + Some(tol) => (error_deg * METRES_PER_DEGREE, tol), + None if error_deg <= GEOGRAPHIC_TOL_DEG => return Ok(()), + None => ( + error_deg * METRES_PER_DEGREE, + GEOGRAPHIC_TOL_DEG * METRES_PER_DEGREE, + ), + } + } + CrsKind::Projected => { + let dx = (got[0] - row.expected[0]).abs(); + let error_m = dx.max(dy) * target.unit.metres_per_unit; + (error_m, row.tol_m.unwrap_or(PROJECTED_TOL_M)) + } + }; + if error_m <= tol_m { + Ok(()) + } else { + Err(format!( + "got ({}, {}), off by {error_m:e} m, tolerance {tol_m:e} m", + got[0], got[1] + )) + } +} + +/// Check every row of a fixture and report all failures at once. +fn check_fixture(name: &str, text: &str) { + let ctx = CrsContext::builtin_only(); + let rows = rows(name, text); + assert!(!rows.is_empty(), "{name} has no data rows"); + let failures: Vec = rows + .iter() + .filter_map(|row| { + check_row(&ctx, row).err().map(|why| { + format!( + "{name}:{} {} -> {} in ({}, {}) expected ({}, {}): {why}", + row.line, + row.src, + row.dst, + row.input[0], + row.input[1], + row.expected[0], + row.expected[1] + ) + }) + }) + .collect(); + assert!( + failures.is_empty(), + "{} of {} rows fail, first {SHOWN}:\n{}", + failures.len(), + rows.len(), + failures + .iter() + .take(SHOWN) + .cloned() + .collect::>() + .join("\n") + ); +} + +#[test] +fn built_in_table_matches_proj() { + check_fixture("proj_reference.tsv", GENERATED); +} + +#[test] +fn malaysia_matches_official_epsg() { + check_fixture("proj_reference_malaysia.tsv", MALAYSIA); +} diff --git a/nodedb-graph/src/csr/index/mutation.rs b/nodedb-graph/src/csr/index/mutation.rs index 6a1599036..11d2cc458 100644 --- a/nodedb-graph/src/csr/index/mutation.rs +++ b/nodedb-graph/src/csr/index/mutation.rs @@ -47,6 +47,20 @@ impl CsrIndex { self.add_edge_internal(src, label, dst, collection, weight, weight != 1.0) } + /// Add a stored edge record scoped to `collection`. Its weight is the + /// `weight` key of the msgpack `properties`, `1.0` when absent. + pub fn insert_record_edge( + &mut self, + src: &str, + label: &str, + dst: &str, + collection: &str, + properties: &[u8], + ) -> Result<(), crate::GraphError> { + let weight = crate::csr::weights::extract_weight_from_properties(properties); + self.add_edge_weighted_in_collection(src, label, dst, collection, weight) + } + /// Incrementally add a weighted edge (goes into mutable buffer). /// /// If this is the first weighted edge (weight != 1.0), initializes diff --git a/nodedb-graph/src/csr/index/restore.rs b/nodedb-graph/src/csr/index/restore.rs index 5d9e03878..31d58a88d 100644 --- a/nodedb-graph/src/csr/index/restore.rs +++ b/nodedb-graph/src/csr/index/restore.rs @@ -193,6 +193,12 @@ impl CsrIndex { self.node_label_to_id.contains_key(label) } + /// How many distinct node label names are interned. Each holds one bit + /// of the label bitset until it is withdrawn. + pub fn node_label_name_count(&self) -> usize { + self.node_label_names.len() + } + /// Withdraw `node`, which a rolled-back write interned. /// /// An absent node is a no-op. The node must be the newest one, with no diff --git a/nodedb-physical/Cargo.toml b/nodedb-physical/Cargo.toml index d8c360e7f..988659859 100644 --- a/nodedb-physical/Cargo.toml +++ b/nodedb-physical/Cargo.toml @@ -13,6 +13,7 @@ documentation = "https://docs.rs/nodedb-physical" [dependencies] nodedb-array = { workspace = true } +nodedb-crs = { workspace = true } nodedb-graph = { workspace = true } nodedb-query = { workspace = true } nodedb-sql = { workspace = true } diff --git a/nodedb-physical/src/physical_plan/array.rs b/nodedb-physical/src/physical_plan/array.rs index 194aa325f..786d46673 100644 --- a/nodedb-physical/src/physical_plan/array.rs +++ b/nodedb-physical/src/physical_plan/array.rs @@ -15,26 +15,67 @@ use nodedb_array::types::ArrayId; use nodedb_types::sync::wire::SyncProvenance; use nodedb_types::{ShardBucket, SurrogateBitmap, SystemTimeScope}; -/// Reducer for [`ArrayOp::Aggregate`]. Numeric `c_enum` keeps the -/// wire encoding to a single byte. +/// Reducer for [`ArrayOp::Aggregate`]. Mirrors +/// `nodedb_array::query::aggregate::Reducer`. #[derive( Debug, Clone, Copy, PartialEq, - Eq, serde::Serialize, serde::Deserialize, zerompk::ToMessagePack, zerompk::FromMessagePack, )] -#[msgpack(c_enum)] pub enum ArrayReducer { Sum, Count, Min, Max, Mean, + Median, + /// Exact percentile `p`, `0 <= p <= 1`. + Percentile { + p: f64, + }, + StdDevPop, + StdDevSamp, + VarPop, + VarSamp, + Mode, + /// Fixed-width bin counts over `[lo, hi]`. + Histogram { + bins: u32, + lo: f64, + hi: f64, + }, +} + +impl ArrayReducer { + /// The engine reducer. A histogram's bins and range are checked. + pub fn engine( + self, + ) -> Result + { + use nodedb_array::query::aggregate::Reducer; + Ok(match self { + ArrayReducer::Sum => Reducer::Sum, + ArrayReducer::Count => Reducer::Count, + ArrayReducer::Min => Reducer::Min, + ArrayReducer::Max => Reducer::Max, + ArrayReducer::Mean => Reducer::Mean, + ArrayReducer::Median => Reducer::Median, + ArrayReducer::Percentile { p } => Reducer::Percentile(p), + ArrayReducer::StdDevPop => Reducer::StdDevPop, + ArrayReducer::StdDevSamp => Reducer::StdDevSamp, + ArrayReducer::VarPop => Reducer::VarPop, + ArrayReducer::VarSamp => Reducer::VarSamp, + ArrayReducer::Mode => Reducer::Mode, + ArrayReducer::Histogram { bins, lo, hi } => { + Reducer::Histogram(nodedb_types::array_agg::HistogramSpec::new(bins, lo, hi)?) + } + }) + } } /// Pairwise op for [`ArrayOp::Elementwise`]. @@ -170,6 +211,9 @@ pub enum ArrayOp { /// zerompk-encoded `nodedb_array::query::PolygonZone`. Only cells /// inside it are returned. Empty = no zone. zone_msgpack: Vec, + /// Read attrs physically: a nodata cell as NULL, and a cell of an + /// attr with a scale or offset as `raw * scale + offset`. + physical: bool, }, /// Reduce one attribute column. `group_by_dim < 0` means no @@ -193,10 +237,12 @@ pub enum ArrayOp { /// to this bucket contribute, so vShards that share one Data Plane /// count no cell twice. `None` includes every cell. shard_bucket: Option, - /// Bitemporal system-time cutoff. When `Some(t)`, only tile versions - /// with `system_from_ms <= t` contribute to the aggregate. `None` = - /// live read. - system_as_of: Option, + /// Bitemporal system-time scope. A point-in-time scope reads the + /// tile versions visible at its cutoff; a `Pinned` scope also names + /// the cut marker every serving node waits for. `Current` = live + /// read. `AllVersions` is refused: an aggregate over every version + /// has no meaning. + system_time: SystemTimeScope, /// Bitemporal valid-time point. When `Some(vt)`, only cells whose /// `valid_from_ms <= vt < valid_until_ms` contribute. `None` = no /// valid-time filter. @@ -204,6 +250,9 @@ pub enum ArrayOp { /// zerompk-encoded `nodedb_array::query::ArrayRegion`. Only cells /// inside it contribute. Empty = the whole array. region_msgpack: Vec, + /// Reduce physical values: `raw * scale + offset` for an attr with a + /// scale or offset. A nodata cell never contributes, either way. + physical: bool, }, /// Pairwise op between two coord-aligned arrays. Both must share @@ -222,9 +271,9 @@ pub enum ArrayOp { /// to this bucket take part on either side, so vShards that share /// one Data Plane emit no row twice. `None` includes every cell. shard_bucket: Option, - /// Bitemporal system-time cutoff for both operands. `None` = live - /// read. - system_as_of: Option, + /// Bitemporal system-time scope for both operands, as on + /// `Aggregate`. + system_time: SystemTimeScope, /// Bitemporal valid-time point for both operands. `None` = no /// valid-time filter. valid_at_ms: Option, @@ -316,9 +365,10 @@ mod tests { cell_filter: None, return_partial: false, shard_bucket: None, - system_as_of: None, + system_time: SystemTimeScope::Current, valid_at_ms: None, region_msgpack: vec![1, 2, 3], + physical: true, }; let bytes = zerompk::to_msgpack_vec(&op).unwrap(); let back: ArrayOp = zerompk::from_msgpack(&bytes).unwrap(); @@ -338,6 +388,7 @@ mod tests { system_time: SystemTimeScope::AllVersions, valid_at_ms: None, zone_msgpack: Vec::new(), + physical: false, }; let bytes = zerompk::to_msgpack_vec(&op).unwrap(); let back: ArrayOp = zerompk::from_msgpack(&bytes).unwrap(); @@ -367,6 +418,7 @@ mod tests { system_time: SystemTimeScope::AsOf(1_700_000_000_000), valid_at_ms: Some(1_600_000_000_000), zone_msgpack: vec![4, 5, 6], + physical: true, }; let bytes = zerompk::to_msgpack_vec(&op).unwrap(); let back: ArrayOp = zerompk::from_msgpack(&bytes).unwrap(); @@ -382,21 +434,32 @@ mod tests { attr_idx: 0, cell_filter: None, shard_bucket: None, - system_as_of: None, + system_time: SystemTimeScope::Current, valid_at_ms: None, }; assert_eq!(op.primary_array().name, "L"); } #[test] - fn binary_op_and_reducer_are_c_enum_one_byte() { - // c_enum encodes as a single u8 — confirm round-trip preserves identity. + fn binary_op_and_reducer_round_trip() { for r in [ ArrayReducer::Sum, ArrayReducer::Count, ArrayReducer::Min, ArrayReducer::Max, ArrayReducer::Mean, + ArrayReducer::Median, + ArrayReducer::Percentile { p: 0.9 }, + ArrayReducer::StdDevPop, + ArrayReducer::StdDevSamp, + ArrayReducer::VarPop, + ArrayReducer::VarSamp, + ArrayReducer::Mode, + ArrayReducer::Histogram { + bins: 10, + lo: 0.0, + hi: 1.0, + }, ] { let bytes = zerompk::to_msgpack_vec(&r).unwrap(); let back: ArrayReducer = zerompk::from_msgpack(&bytes).unwrap(); diff --git a/nodedb-physical/src/physical_plan/cluster_array.rs b/nodedb-physical/src/physical_plan/cluster_array.rs index 005f91849..a1fe4b6aa 100644 --- a/nodedb-physical/src/physical_plan/cluster_array.rs +++ b/nodedb-physical/src/physical_plan/cluster_array.rs @@ -62,6 +62,8 @@ pub enum ClusterArrayOp { /// zerompk-encoded `nodedb_array::query::PolygonZone`, forwarded to /// every shard. Empty = no zone. zone_msgpack: Vec, + /// Read attrs physically. See `ArrayOp::Slice::physical`. + physical: bool, }, /// Fan-out an aggregate across all target vShards and reduce partials. @@ -77,13 +79,15 @@ pub enum ClusterArrayOp { slice_tile_keys: Vec, /// Routing granularity. prefix_bits: u8, - /// Bitemporal system-time cutoff. `None` = live read. - system_as_of: Option, + /// Bitemporal system-time scope. See `ArrayOp::Aggregate::system_time`. + system_time: SystemTimeScope, /// Bitemporal valid-time point. `None` = no valid-time filter. valid_at_ms: Option, /// zerompk-encoded `nodedb_array::query::ArrayRegion`, forwarded to /// every shard. Empty = the whole array. region_msgpack: Vec, + /// Reduce physical values. See `ArrayOp::Aggregate::physical`. + physical: bool, }, /// Fan-out an elementwise op to every primary vShard and concatenate the @@ -101,9 +105,9 @@ pub enum ClusterArrayOp { attr_idx: u32, /// Routing granularity, shared by both arrays. prefix_bits: u8, - /// Bitemporal system-time cutoff for both operands. `None` = live - /// read. - system_as_of: Option, + /// Bitemporal system-time scope for both operands. See + /// `ArrayOp::Aggregate::system_time`. + system_time: SystemTimeScope, /// Bitemporal valid-time point for both operands. `None` = no /// valid-time filter. valid_at_ms: Option, diff --git a/nodedb-physical/src/physical_plan/columnar.rs b/nodedb-physical/src/physical_plan/columnar.rs index db7ef7e81..336e9be8b 100644 --- a/nodedb-physical/src/physical_plan/columnar.rs +++ b/nodedb-physical/src/physical_plan/columnar.rs @@ -47,6 +47,12 @@ pub enum ColumnarInsertIntent { /// the row with `RejectedConstraint` (SQLSTATE 23505) instead of the /// `Insert` tombstone-and-append behavior above. InsertUnique, + /// A RESTORE re-issue of backed-up rows. A bitemporal row keeps the + /// `_ts_system` ordinal its row carries, the stamp its original write + /// took, and the insert folds it into the shared clock. A duplicate PK + /// behaves as `Insert`. Only the restore path builds it: a client + /// statement never chooses its own system time. + Restore, } /// Base columnar physical operations. @@ -146,6 +152,15 @@ pub enum ColumnarOp { /// to disk. `None` for live ingest (always accepted). #[serde(default)] wal_lsn: Option, + /// System-time ordinal of the first row of a bitemporal insert. Row + /// `i` of `payload` stamps `system_from + i` in `_ts_system`. The + /// write funnel reserves the run on the node's ordinal clock right + /// before the write's WAL record, so the live apply and every replay + /// of the record stamp the same ordinals. `None` mints at apply: a + /// Calvin stage stamps its transaction's ordinal, any other apply a + /// fresh ordinal per row. + #[serde(default)] + system_from: Option, /// Compiled row-level-security WRITE predicate (`Vec` as /// MessagePack), or the reason no predicate is attached. Carried only /// for the ON CONFLICT DO UPDATE shape, whose merged post-image @@ -180,6 +195,16 @@ pub enum ColumnarOp { /// Field assignments: `(column_name, value)`. An `Expr` value /// evaluates against each matched row's pre-image. updates: Vec<(String, super::document::UpdateValue)>, + /// System-time ordinal every new version this update writes to a + /// bitemporal collection stamps in `_ts_system`, raised one above its + /// key's bound row when that row sits at or above it. The write + /// funnel fixes it right before the write's WAL record: the run the + /// applied entry's leader reserved, else an ordinal of the node's + /// clock. The live apply and every replay of the record stamp alike. + /// `None` stamps at apply: a Calvin stage its transaction's ordinal, + /// any other apply a fresh ordinal. + #[serde(default)] + system_from: Option, /// Compiled row-level-security WRITE predicate, evaluated against each /// row's post-image once the assignments have been applied, or the /// reason no predicate is attached. @@ -193,6 +218,14 @@ pub enum ColumnarOp { collection: QualifiedCollection, /// Serialized `Vec` (MessagePack). filters: Vec, + /// System-time ordinal at which a delete on a bitemporal collection + /// closes each removed key: the stamp of the tombstone version it + /// appends, raised one above the key's bound row when that row sits + /// at or above it. Fixed by the write funnel as for an update, so the + /// live apply and every replay close at the same ordinal. `None` + /// stamps at apply. A collection that is not bitemporal ignores it. + #[serde(default)] + system_from: Option, /// Compiled row-level-security WRITE predicate, evaluated against the /// pre-image of every row this removes, or the reason no predicate is /// attached. @@ -211,6 +244,16 @@ pub enum ColumnarOp { /// (primary key, full post-image) for each row the Control Plane /// resolved and the write policy admitted. rows: Vec<(nodedb_types::Value, Vec)>, + /// System-time ordinal every new version this update writes to a + /// bitemporal collection stamps in `_ts_system`, raised one above its + /// key's bound row when that row sits at or above it. The write + /// funnel fixes it right before the write's WAL record: the run the + /// applied entry's leader reserved, else an ordinal of the node's + /// clock. The live apply and every replay of the record stamp alike. + /// `None` stamps at apply: a Calvin stage its transaction's ordinal, + /// any other apply a fresh ordinal. + #[serde(default)] + system_from: Option, rls_write_check: RlsWriteCheck, }, @@ -225,6 +268,14 @@ pub enum ColumnarOp { collection: QualifiedCollection, /// Primary key of each row the write policy admitted for removal. pks: Vec, + /// System-time ordinal at which a delete on a bitemporal collection + /// closes each removed key: the stamp of the tombstone version it + /// appends, raised one above the key's bound row when that row sits + /// at or above it. Fixed by the write funnel as for an update, so the + /// live apply and every replay close at the same ordinal. `None` + /// stamps at apply. A collection that is not bitemporal ignores it. + #[serde(default)] + system_from: Option, rls_write_check: RlsWriteCheck, }, @@ -259,14 +310,27 @@ pub enum ColumnarOp { }, /// `TRUNCATE` of a columnar or spatial collection: every row, every - /// bitemporal version, every flushed segment, and every R-tree entry - /// the collection's geometry columns produced. Reports the number of - /// live rows that existed. + /// flushed segment, and every R-tree entry the collection's geometry + /// columns produced. A bitemporal collection closes every current key + /// with a tombstone version instead and keeps its history. Reports the + /// number of live rows that existed. Truncate { collection: QualifiedCollection, /// `TRUNCATE ... RESTART IDENTITY`: the Control Plane resets the /// collection's sequences after the Data Plane clears the rows. #[serde(default)] restart_identity: bool, + /// System-time ordinal at which a truncate of a bitemporal + /// collection closes every current key, raised per key above its + /// bound row, as for a delete. Fixed by the write funnel, so the live + /// apply and every replay close at the same ordinal. `None` stamps at + /// apply. A collection that is not bitemporal ignores it. + #[serde(default)] + system_from: Option, + /// Erase every row and version even on a bitemporal collection. A + /// backup restore clears this way before it reinstalls every + /// version under the system time it carries. + #[serde(default)] + erase_history: bool, }, } diff --git a/nodedb-physical/src/physical_plan/document/op.rs b/nodedb-physical/src/physical_plan/document/op.rs index d4c2716ba..12080e281 100644 --- a/nodedb-physical/src/physical_plan/document/op.rs +++ b/nodedb-physical/src/physical_plan/document/op.rs @@ -293,6 +293,13 @@ pub enum DocumentOp { /// identity under this column, else under `id`, and only when the /// row lacks that column. declared_key: Option, + /// The collection's incarnation in the catalog: the stamp of the + /// create a drop and a same-name create replace. The collection's + /// CRDT document mints its Loro operations under a peer id derived + /// from it, so a re-created collection never reuses an operation id + /// of the dropped one. `Hlc::ZERO` when the catalog holds none. + #[serde(default)] + incarnation: nodedb_types::Hlc, }, /// Lookup documents by secondary index value. @@ -321,12 +328,13 @@ pub enum DocumentOp { projection: Vec, limit: usize, offset: usize, - /// `FOR SYSTEM_TIME AS OF ` cutoff on a bitemporal collection. - /// `None` reads the current state. The versioned index answers at the - /// cutoff and each row is read at its version as of the cutoff. The - /// index holds no valid time and no history listing, so a valid-time - /// or all-versions read never plans this variant. - system_as_of_ms: Option, + /// System-time scope on a bitemporal collection: `Current` reads the + /// current state, and a point-in-time scope reads at its cut. The + /// versioned index answers at the cut and each row is read at its + /// version as of the cut. The index holds no valid time and no + /// history listing, so a valid-time or all-versions read never plans + /// this variant, and the handler refuses `AllVersions`. + system_time: nodedb_types::SystemTimeScope, }, /// Drop all secondary index entries for a field. @@ -609,14 +617,16 @@ pub enum DocumentOp { /// `value_bytes` is always MessagePack, transcoded from the source /// encoding. `next_cursor` empty = scan complete. /// - /// Honors `system_as_of_ms` (the clone's `as_of_lsn`) on bitemporal - /// collections; ignored otherwise. + /// Honors `system_time` on bitemporal collections; ignored otherwise. + /// A clone copy reads `AsOf` its `as_of_lsn`, and a paged source read + /// reads at its statement's cut. MaterializeScan { collection: QualifiedCollection, cursor: Vec, count: usize, - system_as_of_ms: Option, - // Point-in-time snapshot: `AllVersions` is rejected upstream. + /// `Current` or a point-in-time cut. The handler refuses + /// `AllVersions`. + system_time: nodedb_types::SystemTimeScope, /// Return each body as stored, transcoded to MessagePack with no /// `id` added. Every clone materializer copy reads these: a /// `HASH_CHAIN` link covers the stored contents, and a strict target diff --git a/nodedb-physical/src/physical_plan/document/row_source.rs b/nodedb-physical/src/physical_plan/document/row_source.rs index 682aa502f..cb8f991c9 100644 --- a/nodedb-physical/src/physical_plan/document/row_source.rs +++ b/nodedb-physical/src/physical_plan/document/row_source.rs @@ -47,9 +47,9 @@ pub struct DocumentPageSource { /// Serialized `Vec`: the WHERE clause, ANDed with the /// collection's read policy by the RLS pass. pub filters: Vec, - /// `FOR SYSTEM_TIME AS OF` cut, in epoch milliseconds. `None` reads the - /// current version. - pub system_as_of_ms: Option, + /// `Current`, or the `AS OF SYSTEM TIME` cut every page reads at. Never + /// `AllVersions`. + pub system_time: nodedb_types::SystemTimeScope, /// Matching rows skipped before the first row is read. pub offset: usize, /// Matching rows read after the offset. `None` reads every matching row. @@ -85,7 +85,7 @@ impl RowSource { RowSource::DocumentPages(DocumentPageSource { collection, filters: Vec::new(), - system_as_of_ms: None, + system_time: nodedb_types::SystemTimeScope::Current, offset: 0, limit: None, column_map: Vec::new(), @@ -109,6 +109,15 @@ impl RowSource { } } + /// The plan a query source runs, for an in-place rewrite. `None` for a + /// page source. + pub fn query_plan_mut(&mut self) -> Option<&mut PhysicalPlan> { + match self { + RowSource::DocumentPages(_) => None, + RowSource::Query(query) => Some(&mut query.plan), + } + } + /// A short label for logs and error messages: the paged collection, or /// the primary collection of the query plan. pub fn label(&self) -> &str { diff --git a/nodedb-physical/src/physical_plan/graph/algo_stage.rs b/nodedb-physical/src/physical_plan/graph/algo_stage.rs index bee765060..8eb03d594 100644 --- a/nodedb-physical/src/physical_plan/graph/algo_stage.rs +++ b/nodedb-physical/src/physical_plan/graph/algo_stage.rs @@ -38,10 +38,9 @@ pub enum AlgoStage { /// Run over this core's own edges. Local, /// Run nothing. Answer this core's edges of the collection, after the - /// label filter, as a msgpack array of [`AlgoEdge`]. `system_as_of_ms` - /// bounds them to the edges live at that system time; `None` answers the - /// current edges. - ExportEdges { system_as_of_ms: Option }, + /// label filter, as a msgpack array of [`AlgoEdge`], read at the `Algo` + /// plan's scope. + ExportEdges, /// Run over these edges, gathered from every core that holds the /// collection. The core reads no edges of its own. Gathered { edges: Vec }, diff --git a/nodedb-physical/src/physical_plan/graph/batch_edge.rs b/nodedb-physical/src/physical_plan/graph/batch_edge.rs index 14f2f007d..81217c6d8 100644 --- a/nodedb-physical/src/physical_plan/graph/batch_edge.rs +++ b/nodedb-physical/src/physical_plan/graph/batch_edge.rs @@ -25,4 +25,10 @@ pub struct BatchEdge { pub dst_id: String, pub src_surrogate: Surrogate, pub dst_surrogate: Surrogate, + /// Start of the edge's valid time, ms since epoch. `None` takes the + /// write's wall time. A delete ignores it. + pub valid_from_ms: Option, + /// Exclusive end of the edge's valid time. `None` leaves it open. A + /// delete ignores it. + pub valid_until_ms: Option, } diff --git a/nodedb-physical/src/physical_plan/graph/bsp.rs b/nodedb-physical/src/physical_plan/graph/bsp.rs index 58e5e4e2d..80cc893e0 100644 --- a/nodedb-physical/src/physical_plan/graph/bsp.rs +++ b/nodedb-physical/src/physical_plan/graph/bsp.rs @@ -87,13 +87,19 @@ pub struct BspSuperstepPlan { /// scheduler the node runs passed it, and reads at the marker's epoch /// instant. Every edge version sequenced before the marker is then /// installed and at or below that cut. Every version sequenced after it - /// is above the cut. The count phase answers the cut in + /// is above the cut. A historical run carries its statement's pinned + /// ordinal in `system_as_of` with the marker: each node waits for the + /// marker, folds the ordinal into its ordinal clock, and reads at the + /// ordinal unchanged. The count phase answers the cut in /// `BspSuperstepResult::system_as_of`, and every later dispatch carries /// it as `system_as_of`. pub read_cut_marker: u64, /// The system-time ordinal every core reads the graph at. The handler /// refuses a plan without one. pub system_as_of: Option, + /// The valid-time instant every edge read must contain (`AS OF VALID + /// TIME`), or `None` for every valid time. + pub valid_at_ms: Option, } /// Result of one [`super::op::GraphOp::BspSuperstep`] on a single shard. diff --git a/nodedb-physical/src/physical_plan/graph/mod.rs b/nodedb-physical/src/physical_plan/graph/mod.rs index 61c3c1a5e..596c368fa 100644 --- a/nodedb-physical/src/physical_plan/graph/mod.rs +++ b/nodedb-physical/src/physical_plan/graph/mod.rs @@ -7,6 +7,7 @@ pub mod batch_edge; pub mod bsp; pub mod op; pub mod rag_stage; +pub mod read_scope; pub mod wcc; pub use algo_stage::{AlgoEdge, AlgoStage}; @@ -14,4 +15,5 @@ pub use batch_edge::BatchEdge; pub use bsp::{BspSuperstepPlan, BspSuperstepResult}; pub use op::GraphOp; pub use rag_stage::{RagBindingRow, RagLegs, RagStage, RagTextHit, RagVectorHit}; +pub use read_scope::{ALL_VERSIONS_REFUSED, GraphReadScope, GraphSystemCut}; pub use wcc::{WccSuperstepPlan, WccSuperstepResult}; diff --git a/nodedb-physical/src/physical_plan/graph/op.rs b/nodedb-physical/src/physical_plan/graph/op.rs index 5501358c4..24d899ce8 100644 --- a/nodedb-physical/src/physical_plan/graph/op.rs +++ b/nodedb-physical/src/physical_plan/graph/op.rs @@ -3,14 +3,13 @@ //! `GraphOp`: graph engine physical operations dispatched to the Data Plane. use nodedb_graph::{AlgoParams, Direction, GraphAlgorithm, GraphTraversalOptions}; -use nodedb_types::{ - QualifiedCollection, RlsWriteCheck, Surrogate, SurrogateBitmap, SystemTimeScope, -}; +use nodedb_types::{QualifiedCollection, RlsWriteCheck, Surrogate, SurrogateBitmap}; use super::algo_stage::AlgoStage; use super::batch_edge::BatchEdge; use super::bsp::BspSuperstepPlan; use super::rag_stage::RagStage; +use super::read_scope::GraphReadScope; use super::wcc::WccSuperstepPlan; /// Graph engine physical operations. @@ -37,6 +36,11 @@ pub enum GraphOp { properties: Vec, src_surrogate: Surrogate, dst_surrogate: Surrogate, + /// Start of the edge's valid time, ms since epoch. `None` takes the + /// write's wall time. + valid_from_ms: Option, + /// Exclusive end of the edge's valid time. `None` leaves it open. + valid_until_ms: Option, }, /// Batched edge insert: many `(collection, src, label, dst)` tuples. @@ -92,6 +96,8 @@ pub enum GraphOp { /// Optional surrogate prefilter restricting which frontier nodes are /// eligible as traversal targets. `None` = no restriction. frontier_bitmap: Option, + /// The bitemporal cut the read sees. `AllVersions` is refused. + scope: GraphReadScope, }, /// Immediate 1-hop neighbors lookup. @@ -104,6 +110,8 @@ pub enum GraphOp { direction: Direction, /// RLS filters applied to neighbor nodes before returning. rls_filters: Vec, + /// The bitemporal cut the read sees. `AllVersions` is refused. + scope: GraphReadScope, }, /// Batched 1-hop neighbors lookup: one RPC per hop of a BFS frontier @@ -130,9 +138,11 @@ pub enum GraphOp { /// counts against `max_results`. Empty admits every edge. Non-empty /// requires `collection`. edge_predicate: Vec, - /// Each row carries the crossed edge's current property object. + /// Each row carries the crossed edge's property object at `scope`. /// Requires `collection`. with_properties: bool, + /// The bitemporal cut the read sees. `AllVersions` is refused. + scope: GraphReadScope, }, /// Shortest path between two nodes. @@ -150,6 +160,8 @@ pub enum GraphOp { /// Optional surrogate prefilter restricting which nodes may appear /// on the path. `None` = no restriction. frontier_bitmap: Option, + /// The bitemporal cut the read sees. `AllVersions` is refused. + scope: GraphReadScope, }, /// Materialize a subgraph as edge tuples. @@ -163,6 +175,8 @@ pub enum GraphOp { options: GraphTraversalOptions, /// RLS filters applied to subgraph nodes/edges before returning. rls_filters: Vec, + /// The bitemporal cut the read sees. `AllVersions` is refused. + scope: GraphReadScope, }, /// GraphRAG fusion: vector search → graph expansion → RRF ranking. @@ -195,6 +209,8 @@ pub enum GraphOp { bm25_field: Option, /// Which part of the fusion the core runs. stage: RagStage, + /// The bitemporal cut the read sees. `AllVersions` is refused. + scope: GraphReadScope, }, /// Graph algorithm execution (PageRank, WCC, SSSP, etc.). @@ -203,6 +219,10 @@ pub enum GraphOp { params: AlgoParams, /// Where the algorithm reads its graph from. stage: AlgoStage, + /// The bitemporal cut of the graph the algorithm runs over. A + /// `Local` or `ExportEdges` stage reads its edges at it. + /// `AllVersions` is refused. + scope: GraphReadScope, }, /// Graph pattern matching (MATCH clause execution). @@ -215,6 +235,8 @@ pub enum GraphOp { /// frontier candidate for Control-Plane filtering (cluster /// orchestration). `false` (default) matches non-cluster MATCH output. cluster_mode: bool, + /// The bitemporal cut the read sees. `AllVersions` is refused. + scope: GraphReadScope, }, /// Cross-shard MATCH continuation: resumes the SAME already-optimized @@ -232,6 +254,8 @@ pub enum GraphOp { source_node: String, /// The binding variable bound to `source_node`. source_binding: String, + /// The cut of the originating `Match`. + scope: GraphReadScope, }, /// Cross-shard MATCH variable-length RESUME: continues a `[*min..max]` @@ -245,6 +269,8 @@ pub enum GraphOp { /// triple index, the source bindings, and the un-expanded frontier / /// resume depth. resume: Vec, + /// The cut of the originating `Match`. + scope: GraphReadScope, }, /// One distributed-PageRank BSP superstep on this shard's local CSR. @@ -272,12 +298,13 @@ pub enum GraphOp { labels: Vec, }, - /// Bitemporal 1-hop neighbors lookup. + /// Bitemporal 1-hop neighbors lookup, read from the versioned edge + /// store rather than a CSR. /// - /// Resolves edges whose latest version with `system_from <= system_as_of_ms` - /// (converted to HLC ordinal) is not a sentinel, optionally also filtering - /// by `valid_from_ms <= valid_at_ms < valid_until_ms`. The handler calls - /// `ceiling_resolve_edge` per candidate base edge. + /// Resolves each base edge to its newest version at or below the scope's + /// system cut that is not a sentinel, and whose valid time contains + /// `scope.valid_at_ms` when one is set. The answer has one row per base + /// edge, so `GraphSystemCut::AllVersions` answers a typed `Unsupported`. TemporalNeighbors { /// Edge store is collection-scoped; current-state `Neighbors` reads /// the tenant-wide CSR, but the versioned key layout is @@ -287,29 +314,10 @@ pub enum GraphOp { node_id: String, edge_label: Option, direction: Direction, - /// System-time selection. `Current` falls back to current-state - /// semantics identical to `Neighbors`; `AsOf(ms)` is point-in-time. - /// `AllVersions` returns a typed NotSupported error on graph. - system_time: SystemTimeScope, - /// Optional valid-time point. Skipped when `None`. - valid_at_ms: Option, + scope: GraphReadScope, rls_filters: Vec, }, - /// Bitemporal graph algorithm execution. - /// - /// Identical to `Algo` but builds its CSR snapshot via - /// `CsrSnapshot::from_edge_store_as_of` at the given system-time cutoff - /// before running the algorithm. - TemporalAlgorithm { - algorithm: GraphAlgorithm, - params: AlgoParams, - /// System-time selection. `Current` means current state (equivalent to - /// plain `Algo`); `AsOf(ms)` builds a snapshot at that cutoff. - /// `AllVersions` returns a typed NotSupported error on graph. - system_time: SystemTimeScope, - }, - /// Read persistent graph-stats counters from the edge store. /// /// `collection = Some` returns stats for one `(tenant, collection)` pair. @@ -380,3 +388,38 @@ pub enum GraphOp { ids: Vec, }, } + +impl GraphOp { + /// The bitemporal scope a graph read op reads at. `None` for a write, a + /// guard, a stats read, or a superstep: a superstep carries its own cut + /// marker in its plan. + pub fn read_scope(&self) -> Option<&GraphReadScope> { + match self { + GraphOp::Hop { scope, .. } + | GraphOp::Neighbors { scope, .. } + | GraphOp::NeighborsMulti { scope, .. } + | GraphOp::Path { scope, .. } + | GraphOp::Subgraph { scope, .. } + | GraphOp::RagFusion { scope, .. } + | GraphOp::Algo { scope, .. } + | GraphOp::Match { scope, .. } + | GraphOp::MatchContinuation { scope, .. } + | GraphOp::MatchVarLenResume { scope, .. } + | GraphOp::TemporalNeighbors { scope, .. } => Some(scope), + GraphOp::EdgePut { .. } + | GraphOp::EdgePutBatch { .. } + | GraphOp::EdgeDelete { .. } + | GraphOp::ResolveEdgeDelete(_) + | GraphOp::EdgeDeleteBatch { .. } + | GraphOp::BspSuperstep(_) + | GraphOp::WccSuperstep(_) + | GraphOp::SetNodeLabels { .. } + | GraphOp::RemoveNodeLabels { .. } + | GraphOp::Stats { .. } + | GraphOp::NodeEdgeGuard { .. } + | GraphOp::NodePresenceGuard { .. } + | GraphOp::TruncateEdges { .. } + | GraphOp::NodePresenceRead { .. } => None, + } + } +} diff --git a/nodedb-physical/src/physical_plan/graph/read_scope.rs b/nodedb-physical/src/physical_plan/graph/read_scope.rs new file mode 100644 index 000000000..ba0041125 --- /dev/null +++ b/nodedb-physical/src/physical_plan/graph/read_scope.rs @@ -0,0 +1,152 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! The bitemporal cut a graph read sees. + +/// The refusal of `AS OF SYSTEM TIME NULL` on a graph walk: a walk reads one +/// graph, and every system-time version of every edge is no graph. +pub const ALL_VERSIONS_REFUSED: &str = + "AS OF SYSTEM TIME NULL is not supported on graph walks; name a system time"; + +/// The system-time cut of a graph read. +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Eq, + Default, + serde::Serialize, + serde::Deserialize, + zerompk::ToMessagePack, + zerompk::FromMessagePack, +)] +pub enum GraphSystemCut { + /// The current edges, through the live CSR. + #[default] + Live, + /// The edges whose newest version at or below this system-time ordinal + /// is not a tombstone. The Control Plane pins one ordinal per statement, + /// so every hop of a walk reads the same graph. + AtOrdinal(i64), + /// Every system-time version (`AS OF SYSTEM TIME NULL`). Only + /// `GraphOp::TemporalNeighbors` accepts it. + AllVersions, +} + +/// The cut of a graph read on both temporal axes. +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Eq, + Default, + serde::Serialize, + serde::Deserialize, + zerompk::ToMessagePack, + zerompk::FromMessagePack, +)] +pub struct GraphReadScope { + pub system: GraphSystemCut, + /// An edge is read only when its valid time `[valid_from_ms, + /// valid_until_ms)` contains this instant. `None` reads every valid time. + pub valid_at_ms: Option, + /// The Calvin cut marker whose cut caps an `AtOrdinal` cut. Every node + /// waits until the marker applied and every Calvin scheduler it runs + /// passed it before it reads at the cut. Every edge version at or below + /// the cut is then installed there. `0` when no marker caps the cut: a + /// live read, or a cut every reader already installed. + pub cut_marker: u64, +} + +impl GraphReadScope { + /// A read at `ordinal`, a cut every reader already installed, so no + /// reader waits for a marker. + pub fn at_installed_cut(ordinal: i64, valid_at_ms: Option) -> Self { + Self { + system: GraphSystemCut::AtOrdinal(ordinal), + valid_at_ms, + cut_marker: 0, + } + } + + /// The current edges at every valid time: the live CSR answers. + pub fn is_live(&self) -> bool { + matches!(self.system, GraphSystemCut::Live) && self.valid_at_ms.is_none() + } + + /// The system-time ordinal a versioned read caps at: the pinned ordinal, + /// or `i64::MAX` for the current versions. `None` for `AllVersions`, + /// which has no single cap. + pub fn cutoff_ordinal(&self) -> Option { + match self.system { + GraphSystemCut::Live => Some(i64::MAX), + GraphSystemCut::AtOrdinal(ordinal) => Some(ordinal), + GraphSystemCut::AllVersions => None, + } + } + + /// The ordinal the Control Plane pinned for a historical read. `None` + /// for a live read and for `AllVersions`. + pub fn pinned_ordinal(&self) -> Option { + match self.system { + GraphSystemCut::AtOrdinal(ordinal) => Some(ordinal), + GraphSystemCut::Live | GraphSystemCut::AllVersions => None, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn only_the_default_scope_is_live() { + assert!(GraphReadScope::default().is_live()); + assert!(!GraphReadScope::at_installed_cut(5, None).is_live()); + assert!( + !GraphReadScope { + system: GraphSystemCut::Live, + valid_at_ms: Some(5), + cut_marker: 0, + } + .is_live() + ); + } + + #[test] + fn cutoff_ordinal_follows_the_system_cut() { + let at = |system| GraphReadScope { + system, + valid_at_ms: None, + cut_marker: 0, + }; + assert_eq!(at(GraphSystemCut::Live).cutoff_ordinal(), Some(i64::MAX)); + assert_eq!(at(GraphSystemCut::AtOrdinal(7)).cutoff_ordinal(), Some(7)); + assert_eq!(at(GraphSystemCut::AllVersions).cutoff_ordinal(), None); + } + + #[test] + fn only_an_at_ordinal_cut_is_pinned() { + let at = |system| GraphReadScope { + system, + valid_at_ms: None, + cut_marker: 0, + }; + assert_eq!(at(GraphSystemCut::Live).pinned_ordinal(), None); + assert_eq!(at(GraphSystemCut::AtOrdinal(7)).pinned_ordinal(), Some(7)); + assert_eq!(at(GraphSystemCut::AllVersions).pinned_ordinal(), None); + } + + #[test] + fn scope_round_trips_through_msgpack() { + let scope = GraphReadScope { + system: GraphSystemCut::AtOrdinal(42), + valid_at_ms: Some(9), + cut_marker: 77, + }; + let bytes = zerompk::to_msgpack_vec(&scope).expect("encode"); + let back: GraphReadScope = zerompk::from_msgpack(&bytes).expect("decode"); + assert_eq!(back, scope); + } +} diff --git a/nodedb-physical/src/physical_plan/graph/wcc.rs b/nodedb-physical/src/physical_plan/graph/wcc.rs index b2ef82a1a..3f875c8b9 100644 --- a/nodedb-physical/src/physical_plan/graph/wcc.rs +++ b/nodedb-physical/src/physical_plan/graph/wcc.rs @@ -31,12 +31,16 @@ pub struct WccSuperstepPlan { pub owned_vshards: Vec, /// The watermark of the Calvin cut marker the read cut comes from, `0` /// when `system_as_of` already holds it. Every node resolves the same - /// cut from it (see `BspSuperstepPlan::read_cut_marker`), so every node + /// cut from it, or waits for it under a historical round's pinned + /// ordinal (see `BspSuperstepPlan::read_cut_marker`), so every node /// reads the same graph. pub read_cut_marker: u64, /// The system-time ordinal every core reads the graph at. The handler /// refuses a plan without one. pub system_as_of: Option, + /// The valid-time instant every edge read must contain (`AS OF VALID + /// TIME`), or `None` for every valid time. + pub valid_at_ms: Option, } /// Result of one [`super::op::GraphOp::WccSuperstep`] on a single shard. diff --git a/nodedb-physical/src/physical_plan/meta.rs b/nodedb-physical/src/physical_plan/meta.rs index f644c69c3..f6b7bf755 100644 --- a/nodedb-physical/src/physical_plan/meta.rs +++ b/nodedb-physical/src/physical_plan/meta.rs @@ -157,6 +157,26 @@ pub enum MetaOp { version_floor: Vec, }, + /// Remove this core's data of vShards this node no longer hosts, once + /// their new owners serve them: the rows of every collection homed on + /// them, the array cells routed to them, the label versions of the nodes + /// they home, and every edge version with an endpoint home among them + /// and none among `resident`. Collection definitions stay. The purge + /// checkpoints every memory-only engine before it answers. + /// + /// Idempotent: a re-run after a partial purge or a restart removes what + /// is left. + PurgeDepartedVShards { + /// The vShards that left this node. + departed: Vec, + /// Every vShard this node still hosts. An edge with an endpoint + /// home among them stays. + resident: Vec, + /// Every collection homed on `departed`. Each core clears every one + /// and reclaims the shared on-disk files of the ones it homes. + collections: Vec, + }, + /// Purge ALL data for a tenant across every engine and cache. /// /// Deletes documents, indexes, vectors, graph edges, timeseries, @@ -482,6 +502,13 @@ pub enum MetaOp { /// Called after the Control Plane has already removed it from the catalog. DeleteSynonymGroup { tenant_id: u64, name: String }, + /// Replace this core's user-defined CRS set. + /// + /// Sent to every core after a committed `CREATE` or `DROP + /// SPATIAL_REF_SYS`, and after a metadata image replaced the catalog. + /// The set is whole, so a core never holds part of a change. + SetCustomCrsSet { set: nodedb_crs::CustomCrsSet }, + /// Execute a point write at STATEMENT time by STAGING it into the /// per-transaction overlay, instead of buffering it for COMMIT. /// diff --git a/nodedb-physical/src/physical_plan/meta_restore.rs b/nodedb-physical/src/physical_plan/meta_restore.rs index c4336c7b7..ac88fa5dd 100644 --- a/nodedb-physical/src/physical_plan/meta_restore.rs +++ b/nodedb-physical/src/physical_plan/meta_restore.rs @@ -7,7 +7,8 @@ //! writes order against every other transaction, a TRUNCATE included. Each //! participant installs the batch at its transaction's turn. A restored row //! installs as the backup holds it. A restored edge version keeps its -//! historical `system_from` and is applied at the transaction's ordinal. +//! historical `system_from` and is applied at the transaction's ordinal. A +//! restored node label version keeps its system time too. /// One restored edge version. #[derive( @@ -33,6 +34,9 @@ pub struct RestoredEdgeVersion { pub system_from: i64, /// The version's properties, `None` for a tombstone. pub properties: Option>, + /// The version's valid time `[from, until)` in ms since epoch, as the + /// backup holds it. `None` for a tombstone. + pub valid_time: Option<(i64, i64)>, } /// One restored document row: the keys its writers lock. @@ -55,6 +59,27 @@ pub struct RestoredRow { pub surrogate: u32, } +/// One restored node's label history and current label set. +#[derive( + Debug, + Clone, + PartialEq, + Eq, + serde::Serialize, + serde::Deserialize, + zerompk::ToMessagePack, + zerompk::FromMessagePack, +)] +#[msgpack(map)] +pub struct RestoredNodeLabels { + pub node_id: String, + /// Every label version the backup holds, as `(system_from, labels)`, in + /// ascending system time. Each keeps its system time. + pub versions: Vec<(i64, Vec)>, + /// The label set the node carries in the backup. + pub current: Vec, +} + /// One `(collection, key) → surrogate` identity every participant binds /// before the batch installs. #[derive( @@ -98,6 +123,10 @@ pub struct RestoredRedo { pub rows: Vec, /// The restored edge versions, in apply order. pub edges: Vec, + /// The restored node labels of nodes homed on `vshard`. + #[serde(default)] + #[msgpack(default)] + pub node_labels: Vec, /// Every collection the batch writes, as the Data Plane stores it. pub collections: Vec, /// Every identity the rows and edge endpoints are stored under. @@ -127,6 +156,12 @@ mod tests { dst_surrogate: 5, system_from: 100, properties: None, + valid_time: None, + }], + node_labels: vec![RestoredNodeLabels { + node_id: "alice".into(), + versions: vec![(90, vec!["Person".into()])], + current: vec!["Person".into()], }], collections: vec!["knows".into(), "people".into()], identities: vec![RestoredIdentity { diff --git a/nodedb-physical/src/physical_plan/meta_snapshot.rs b/nodedb-physical/src/physical_plan/meta_snapshot.rs index d83aedf0f..ec3ce73e9 100644 --- a/nodedb-physical/src/physical_plan/meta_snapshot.rs +++ b/nodedb-physical/src/physical_plan/meta_snapshot.rs @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 -//! Types carried by `MetaOp::RestoreTenantSnapshot` and -//! `MetaOp::CreateTenantSnapshot`. +//! Types carried by `MetaOp::RestoreTenantSnapshot`, +//! `MetaOp::CreateTenantSnapshot` and `MetaOp::PurgeDepartedVShards`. /// One collection a Raft snapshot install clears on a core before it /// installs that core's share of the snapshot. @@ -27,6 +27,26 @@ pub struct SnapshotClearTarget { pub reclaim_l1_files: bool, } +/// One collection a departure purge clears on every core: a collection whose +/// home vShard left this node. +#[derive( + Debug, + Clone, + PartialEq, + Eq, + serde::Serialize, + serde::Deserialize, + zerompk::ToMessagePack, + zerompk::FromMessagePack, +)] +pub struct DepartedCollection { + pub database_id: u64, + pub tenant_id: u64, + /// The name the Data Plane stores the collection under: + /// database-qualified outside the default database. + pub collection: String, +} + /// A database backup's request to capture its tenants at its cut barrier. /// /// The barrier each data group's leader places for the backup carries it. The diff --git a/nodedb-physical/src/physical_plan/mod.rs b/nodedb-physical/src/physical_plan/mod.rs index 8aea5328f..0853ca630 100644 --- a/nodedb-physical/src/physical_plan/mod.rs +++ b/nodedb-physical/src/physical_plan/mod.rs @@ -63,8 +63,9 @@ pub use document::{ pub use exchange::{ExchangeMode, ExchangeOp}; pub use gathered_source::GatheredSource; pub use graph::{ - AlgoEdge, AlgoStage, BatchEdge, BspSuperstepPlan, BspSuperstepResult, GraphOp, RagBindingRow, - RagLegs, RagStage, RagTextHit, RagVectorHit, WccSuperstepPlan, WccSuperstepResult, + ALL_VERSIONS_REFUSED, AlgoEdge, AlgoStage, BatchEdge, BspSuperstepPlan, BspSuperstepResult, + GraphOp, GraphReadScope, GraphSystemCut, RagBindingRow, RagLegs, RagStage, RagTextHit, + RagVectorHit, WccSuperstepPlan, WccSuperstepResult, }; pub use kv::{ KvBatchIntent, KvCounterShape, KvOp, KvResolveOutcome, KvResolvedMutation, SortedIndexRead, @@ -72,8 +73,10 @@ pub use kv::{ }; pub use meta::MetaOp; pub use meta_home::{HomeAnswer, HomeVersion, HomeVersionProbe}; -pub use meta_restore::{RestoredEdgeVersion, RestoredIdentity, RestoredRedo, RestoredRow}; -pub use meta_snapshot::{CutCaptureRequest, SnapshotClearTarget}; +pub use meta_restore::{ + RestoredEdgeVersion, RestoredIdentity, RestoredNodeLabels, RestoredRedo, RestoredRow, +}; +pub use meta_snapshot::{CutCaptureRequest, DepartedCollection, SnapshotClearTarget}; pub use plan::PhysicalPlan; pub use query::{AggregateSpec, GroupKeySpec, JoinProjection, QueryOp}; pub use redo_origin::RedoOrigin; diff --git a/nodedb-physical/src/physical_plan/routing.rs b/nodedb-physical/src/physical_plan/routing.rs index e9e9170fe..bbc1f5051 100644 --- a/nodedb-physical/src/physical_plan/routing.rs +++ b/nodedb-physical/src/physical_plan/routing.rs @@ -91,7 +91,6 @@ pub fn plan_contains_cluster_partitioned_leaf(plan: &PhysicalPlan) -> bool { | PhysicalPlan::Graph(GraphOp::MatchContinuation { .. }) | PhysicalPlan::Graph(GraphOp::MatchVarLenResume { .. }) | PhysicalPlan::Graph(GraphOp::TemporalNeighbors { .. }) - | PhysicalPlan::Graph(GraphOp::TemporalAlgorithm { .. }) | PhysicalPlan::Graph(GraphOp::BspSuperstep(_)) | PhysicalPlan::Graph(GraphOp::WccSuperstep(_)) | PhysicalPlan::Graph(GraphOp::Stats { .. }) => true, @@ -195,7 +194,6 @@ impl PhysicalPlan { | PhysicalPlan::Graph(GraphOp::MatchContinuation { .. }) | PhysicalPlan::Graph(GraphOp::MatchVarLenResume { .. }) | PhysicalPlan::Graph(GraphOp::TemporalNeighbors { .. }) - | PhysicalPlan::Graph(GraphOp::TemporalAlgorithm { .. }) | PhysicalPlan::Graph(GraphOp::BspSuperstep(_)) | PhysicalPlan::Graph(GraphOp::WccSuperstep(_)) | PhysicalPlan::Graph(GraphOp::Stats { .. }) diff --git a/nodedb-physical/src/physical_plan/spatial.rs b/nodedb-physical/src/physical_plan/spatial.rs index 735779650..6309a9da0 100644 --- a/nodedb-physical/src/physical_plan/spatial.rs +++ b/nodedb-physical/src/physical_plan/spatial.rs @@ -20,6 +20,8 @@ use nodedb_types::{QualifiedCollection, Surrogate, SurrogateBitmap, geometry::Sr pub enum SpatialPredicate { /// ST_DWithin: geometry within distance (meters). DWithin, + /// ST_3DDWithin: 3D distance within metres, Z in metres. + DWithin3D, /// ST_Contains: query geometry contains document geometry. Contains, /// ST_Intersects: query geometry intersects document geometry. diff --git a/nodedb-physical/src/physical_plan/truncate_target.rs b/nodedb-physical/src/physical_plan/truncate_target.rs index f0d4a5367..fc4d1f363 100644 --- a/nodedb-physical/src/physical_plan/truncate_target.rs +++ b/nodedb-physical/src/physical_plan/truncate_target.rs @@ -33,6 +33,7 @@ impl PhysicalPlan { | PhysicalPlan::Columnar(ColumnarOp::Truncate { collection, restart_identity, + .. }) | PhysicalPlan::Timeseries(TimeseriesOp::Truncate { collection, @@ -93,6 +94,8 @@ mod tests { let plan = PhysicalPlan::Columnar(ColumnarOp::Truncate { collection: coll(), restart_identity: true, + system_from: None, + erase_history: false, }); let (c, restart) = plan.truncate_target().expect("columnar truncate target"); assert_eq!(c.as_str(), "t"); diff --git a/nodedb-query/Cargo.toml b/nodedb-query/Cargo.toml index 144dec761..33f51e32a 100644 --- a/nodedb-query/Cargo.toml +++ b/nodedb-query/Cargo.toml @@ -15,6 +15,7 @@ categories = ["algorithms", "text-processing"] [dependencies] nodedb-types = { workspace = true } +nodedb-crs = { workspace = true } nodedb-spatial = { workspace = true } thiserror = { workspace = true } nodedb-fts = { workspace = true } diff --git a/nodedb-query/src/expr/eval.rs b/nodedb-query/src/expr/eval.rs index 6d7ebe01d..9d8cfb303 100644 --- a/nodedb-query/src/expr/eval.rs +++ b/nodedb-query/src/expr/eval.rs @@ -7,6 +7,7 @@ //! against the pre-update ("old") document — this is the path used by TRANSITION //! CHECK and similar old/new diff predicates. +use nodedb_crs::{CrsContext, CrsError, UnknownSrid}; use nodedb_types::Value; use crate::value_ops::{coerced_eq, is_truthy, to_value_number, value_to_f64}; @@ -27,9 +28,13 @@ use super::types::{SqlExpr, WHOLE_ROW_COLUMN}; /// dimensions, an argument of the wrong type, a malformed JSONPath. /// SQLSTATE `22000`. A `NULL` argument stays `NULL` instead; /// - an exact integer SUM past the `Decimal` range, SQLSTATE `22000`; -/// - a geometry SRID fault: an unsupported SRID, operands in different -/// SRIDs, or a coordinate outside a projection's domain. SQLSTATE `22000`. -#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] +/// - a geometry SRID fault: an SRID no CRS defines, operands in different +/// SRIDs, or a coordinate outside a projection's domain. SQLSTATE `22000`; +/// - a geometry argument that does not parse (WKT, WKB, GeoJSON), or a +/// geometry operation that cannot run on its input. SQLSTATE `22000`; +/// - an argument whose value is out of range for the function. SQLSTATE +/// `22000`. +#[derive(Debug, Clone, PartialEq, thiserror::Error)] pub enum EvalError { #[error("division by zero")] DivisionByZero, @@ -68,13 +73,13 @@ pub enum EvalError { /// An exact integer aggregate total lies outside the `Decimal` range. #[error("{function}(): integer total out of range")] NumericOverflow { function: &'static str }, - /// An SRID argument, or an SRID a geometry declares, is outside the - /// supported set. SQLSTATE `22000`. - #[error( - "{function}(): unsupported SRID {srid}; supported: {}", - nodedb_types::geometry::SUPPORTED_SRID_CODES - )] - UnsupportedSrid { function: &'static str, srid: i64 }, + /// An SRID argument, or an SRID a geometry declares, names no defined + /// CRS. SQLSTATE `22000`. + #[error("{function}(): {source}")] + Crs { + function: &'static str, + source: UnknownSrid, + }, /// Two geometry operands are in different SRIDs. No implicit transform /// runs. SQLSTATE `22000`. #[error("{function}(): operands have different SRIDs {left} and {right}; use ST_Transform")] @@ -83,10 +88,45 @@ pub enum EvalError { left: u32, right: u32, }, - /// A coordinate lies outside the region the target projection is - /// defined on. SQLSTATE `22000`. - #[error("{function}(): coordinate is outside the domain of SRID {srid}")] - ProjectionDomain { function: &'static str, srid: u32 }, + /// A coordinate transform fails: a coordinate lies outside the region a + /// projection is defined on, a CRS definition does not parse or has no + /// PROJ-string form, or a coordinate epoch is out of range. SQLSTATE + /// `22000`. + #[error("{function}(): {source}")] + Transform { + function: &'static str, + source: CrsError, + }, + /// A geometry argument does not parse, or a geometry operation cannot + /// run on its input. SQLSTATE `22000`. + #[error("{function}(): {source}")] + Geometry { + function: &'static str, + source: nodedb_spatial::SpatialError, + }, + /// An argument holds a value outside the range the function accepts. + /// SQLSTATE `22000`. + #[error("{function}(): argument {position} {reason}")] + ArgumentValue { + function: &'static str, + /// 1-based argument position. + position: usize, + reason: String, + }, +} + +impl EvalError { + /// The fault a CRS operation in `function` raises. An unknown SRID keeps + /// its own variant. + pub fn from_crs(function: &'static str, e: CrsError) -> EvalError { + match e { + CrsError::Unknown(source) => EvalError::Crs { function, source }, + source @ (CrsError::OutOfDomain { .. } + | CrsError::Parse(_) + | CrsError::Epoch(_) + | CrsError::ProjExport(_)) => EvalError::Transform { function, source }, + } + } } /// Row scope for `SqlExpr::eval_scope`: how `Column(..)` and `OldColumn(..)` @@ -94,6 +134,8 @@ pub enum EvalError { /// into this scope for every leaf column reference — both `eval()` and /// `eval_with_old()` delegate here instead of duplicating the walk. struct RowScope<'a> { + /// Resolves the SRIDs geo functions meet. + crs: &'a CrsContext<'a>, new_doc: &'a Value, /// Pre-update row, if this is an old/new evaluation (TRANSITION CHECK). /// `None` means `OldColumn(..)` resolves to `Null`, matching plain `eval`. @@ -146,8 +188,11 @@ impl SqlExpr { /// historically-`NULL`-folding case (bad casts, wrong arg counts, /// unknown-value coercions) is unaffected and still evaluates to /// `Ok(Value::Null)`. - pub fn eval(&self, doc: &Value) -> Result { + /// + /// Geo functions resolve SRIDs through `crs`. + pub fn eval(&self, doc: &Value, crs: &CrsContext<'_>) -> Result { self.eval_scope(&RowScope { + crs, new_doc: doc, old_doc: None, excluded_doc: None, @@ -157,8 +202,14 @@ impl SqlExpr { /// Evaluate with access to both NEW and OLD documents, used by /// TRANSITION CHECK predicates. `Column(name)` resolves against /// `new_doc`; `OldColumn(name)` resolves against `old_doc`. - pub fn eval_with_old(&self, new_doc: &Value, old_doc: &Value) -> Result { + pub fn eval_with_old( + &self, + new_doc: &Value, + old_doc: &Value, + crs: &CrsContext<'_>, + ) -> Result { self.eval_scope(&RowScope { + crs, new_doc, old_doc: Some(old_doc), excluded_doc: None, @@ -169,8 +220,14 @@ impl SqlExpr { /// `INSERT ... ON CONFLICT DO UPDATE`. `Column(name)` resolves /// against the existing (current) row `doc`; `ExcludedColumn(name)` /// resolves against `excluded`. - pub fn eval_with_excluded(&self, doc: &Value, excluded: &Value) -> Result { + pub fn eval_with_excluded( + &self, + doc: &Value, + excluded: &Value, + crs: &CrsContext<'_>, + ) -> Result { self.eval_scope(&RowScope { + crs, new_doc: doc, old_doc: None, excluded_doc: Some(excluded), @@ -215,7 +272,7 @@ impl SqlExpr { .iter() .map(|a| a.eval_scope(scope)) .collect::>()?; - crate::functions::eval_function(name, &evaluated) + crate::functions::eval_function(name, &evaluated, scope.crs) } SqlExpr::Cast { expr, to_type } => { @@ -282,6 +339,10 @@ mod tests { use super::super::types::BinaryOp; use super::*; + fn crs() -> CrsContext<'static> { + CrsContext::builtin_only() + } + fn doc() -> Value { Value::Object( [ @@ -300,22 +361,25 @@ mod tests { #[test] fn column_ref() { let expr = SqlExpr::Column("name".into()); - assert_eq!(expr.eval(&doc()).unwrap(), Value::String("Alice".into())); + assert_eq!( + expr.eval(&doc(), &crs()).unwrap(), + Value::String("Alice".into()) + ); } #[test] fn missing_column() { let expr = SqlExpr::Column("missing".into()); - assert_eq!(expr.eval(&doc()).unwrap(), Value::Null); + assert_eq!(expr.eval(&doc(), &crs()).unwrap(), Value::Null); } #[test] fn whole_row_column_is_the_document() { let expr = SqlExpr::Column(WHOLE_ROW_COLUMN.into()); - assert_eq!(expr.eval(&doc()).unwrap(), doc()); + assert_eq!(expr.eval(&doc(), &crs()).unwrap(), doc()); let old = SqlExpr::OldColumn(WHOLE_ROW_COLUMN.into()); let before = Value::Object(Default::default()); - assert_eq!(old.eval_with_old(&doc(), &before).unwrap(), before); + assert_eq!(old.eval_with_old(&doc(), &before, &crs()).unwrap(), before); } #[test] @@ -324,13 +388,13 @@ mod tests { name: "to_jsonb".into(), args: vec![SqlExpr::Column(WHOLE_ROW_COLUMN.into())], }; - assert_eq!(expr.eval(&doc()).unwrap(), doc()); + assert_eq!(expr.eval(&doc(), &crs()).unwrap(), doc()); } #[test] fn literal() { let expr = SqlExpr::Literal(Value::Integer(42)); - assert_eq!(expr.eval(&doc()).unwrap(), Value::Integer(42)); + assert_eq!(expr.eval(&doc(), &crs()).unwrap(), Value::Integer(42)); } #[test] @@ -340,7 +404,7 @@ mod tests { op: BinaryOp::Add, right: Box::new(SqlExpr::Literal(Value::Float(1.5))), }; - assert_eq!(expr.eval(&doc()).unwrap(), Value::Integer(12)); + assert_eq!(expr.eval(&doc(), &crs()).unwrap(), Value::Integer(12)); } #[test] @@ -350,7 +414,7 @@ mod tests { op: BinaryOp::Mul, right: Box::new(SqlExpr::Column("qty".into())), }; - assert_eq!(expr.eval(&doc()).unwrap(), Value::Integer(42)); + assert_eq!(expr.eval(&doc(), &crs()).unwrap(), Value::Integer(42)); } #[test] @@ -367,7 +431,10 @@ mod tests { )], else_expr: Some(Box::new(SqlExpr::Literal(Value::String("minor".into())))), }; - assert_eq!(expr.eval(&doc()).unwrap(), Value::String("adult".into())); + assert_eq!( + expr.eval(&doc(), &crs()).unwrap(), + Value::String("adult".into()) + ); } #[test] @@ -377,7 +444,7 @@ mod tests { SqlExpr::Literal(Value::String("default@example.com".into())), ]); assert_eq!( - expr.eval(&doc()).unwrap(), + expr.eval(&doc(), &crs()).unwrap(), Value::String("default@example.com".into()) ); } @@ -388,7 +455,7 @@ mod tests { expr: Box::new(SqlExpr::Column("email".into())), negated: false, }; - assert_eq!(expr.eval(&doc()).unwrap(), Value::Bool(true)); + assert_eq!(expr.eval(&doc(), &crs()).unwrap(), Value::Bool(true)); } // ── Division/modulo by zero ────────────────────────────────────────────── @@ -400,7 +467,7 @@ mod tests { op: BinaryOp::Div, right: Box::new(SqlExpr::Literal(Value::Integer(0))), }; - assert_eq!(expr.eval(&doc()), Err(EvalError::DivisionByZero)); + assert_eq!(expr.eval(&doc(), &crs()), Err(EvalError::DivisionByZero)); } #[test] @@ -410,7 +477,7 @@ mod tests { op: BinaryOp::Div, right: Box::new(SqlExpr::Literal(Value::Float(0.0))), }; - assert_eq!(expr.eval(&doc()), Err(EvalError::DivisionByZero)); + assert_eq!(expr.eval(&doc(), &crs()), Err(EvalError::DivisionByZero)); } #[test] @@ -424,7 +491,7 @@ mod tests { rust_decimal::Decimal::ZERO, ))), }; - assert_eq!(expr.eval(&doc()), Err(EvalError::DivisionByZero)); + assert_eq!(expr.eval(&doc(), &crs()), Err(EvalError::DivisionByZero)); } #[test] @@ -434,7 +501,7 @@ mod tests { op: BinaryOp::Mod, right: Box::new(SqlExpr::Literal(Value::Integer(0))), }; - assert_eq!(expr.eval(&doc()), Err(EvalError::DivisionByZero)); + assert_eq!(expr.eval(&doc(), &crs()), Err(EvalError::DivisionByZero)); } #[test] @@ -446,7 +513,7 @@ mod tests { SqlExpr::Literal(Value::Integer(0)), ], }; - assert_eq!(expr.eval(&doc()), Err(EvalError::DivisionByZero)); + assert_eq!(expr.eval(&doc(), &crs()), Err(EvalError::DivisionByZero)); } /// An untaken CASE branch must never be evaluated — `1/0` in the THEN @@ -465,7 +532,7 @@ mod tests { )], else_expr: Some(Box::new(SqlExpr::Literal(Value::Integer(0)))), }; - assert_eq!(expr.eval(&doc()).unwrap(), Value::Integer(0)); + assert_eq!(expr.eval(&doc(), &crs()).unwrap(), Value::Integer(0)); } /// COALESCE stops at the first non-null argument — a later `1/0` @@ -480,7 +547,7 @@ mod tests { right: Box::new(SqlExpr::Literal(Value::Integer(0))), }, ]); - assert_eq!(expr.eval(&doc()).unwrap(), Value::Integer(1)); + assert_eq!(expr.eval(&doc(), &crs()).unwrap(), Value::Integer(1)); } /// Control: `NULL + 1` still folds to `Ok(Null)` — non-division NULL @@ -492,7 +559,7 @@ mod tests { op: BinaryOp::Add, right: Box::new(SqlExpr::Literal(Value::Integer(1))), }; - assert_eq!(expr.eval(&doc()).unwrap(), Value::Null); + assert_eq!(expr.eval(&doc(), &crs()).unwrap(), Value::Null); } /// Control: a valid (non-zero-divisor) division still succeeds. @@ -503,6 +570,6 @@ mod tests { op: BinaryOp::Div, right: Box::new(SqlExpr::Literal(Value::Integer(2))), }; - assert_eq!(expr.eval(&doc()).unwrap(), Value::Integer(5)); + assert_eq!(expr.eval(&doc(), &crs()).unwrap(), Value::Integer(5)); } } diff --git a/nodedb-query/src/expr_parse/parser.rs b/nodedb-query/src/expr_parse/parser.rs index 0521dbbfb..62e4926e3 100644 --- a/nodedb-query/src/expr_parse/parser.rs +++ b/nodedb-query/src/expr_parse/parser.rs @@ -409,6 +409,10 @@ mod tests { use super::*; use nodedb_types::Value; + fn crs() -> nodedb_crs::CrsContext<'static> { + nodedb_crs::CrsContext::builtin_only() + } + fn parse_ok(text: &str) -> (SqlExpr, Vec) { parse_generated_expr(text).unwrap() } @@ -418,7 +422,7 @@ mod tests { let (expr, deps) = parse_ok("price * (1 + tax_rate)"); assert_eq!(deps, vec!["price", "tax_rate"]); let doc = Value::from(serde_json::json!({"price": 100.0, "tax_rate": 0.08})); - let result = expr.eval(&doc).unwrap(); + let result = expr.eval(&doc, &crs()).unwrap(); assert_eq!(result.as_f64(), Some(108.0)); } @@ -427,7 +431,7 @@ mod tests { let (expr, deps) = parse_ok("ROUND(price * (1 + tax_rate), 2)"); assert_eq!(deps, vec!["price", "tax_rate"]); let doc = Value::from(serde_json::json!({"price": 99.99, "tax_rate": 0.08})); - let result = expr.eval(&doc).unwrap(); + let result = expr.eval(&doc, &crs()).unwrap(); assert_eq!(result, Value::Float(107.99)); } @@ -436,14 +440,17 @@ mod tests { let (expr, deps) = parse_ok("CONCAT(name, ' ', brand)"); assert_eq!(deps, vec!["brand", "name"]); let doc = Value::from(serde_json::json!({"name": "Shoe", "brand": "Nike"})); - assert_eq!(expr.eval(&doc).unwrap(), Value::String("Shoe Nike".into())); + assert_eq!( + expr.eval(&doc, &crs()).unwrap(), + Value::String("Shoe Nike".into()) + ); } #[test] fn coalesce() { let (expr, _) = parse_ok("COALESCE(description, '')"); let doc = Value::from(serde_json::json!({"description": null})); - assert_eq!(expr.eval(&doc).unwrap(), Value::String("".into())); + assert_eq!(expr.eval(&doc, &crs()).unwrap(), Value::String("".into())); } #[test] @@ -454,10 +461,10 @@ mod tests { assert!(deps.contains(&"price".to_string())); let doc = Value::from(serde_json::json!({"price": 100.0, "discount": 0.2})); - assert_eq!(expr.eval(&doc).unwrap().as_f64(), Some(80.0)); + assert_eq!(expr.eval(&doc, &crs()).unwrap().as_f64(), Some(80.0)); let doc2 = Value::from(serde_json::json!({"price": 100.0, "discount": 0})); - assert_eq!(expr.eval(&doc2).unwrap().as_f64(), Some(100.0)); + assert_eq!(expr.eval(&doc2, &crs()).unwrap().as_f64(), Some(100.0)); } #[test] @@ -480,7 +487,7 @@ mod tests { let (expr, _) = parse_ok("CONCAT(name, ' - ', 'default')"); let doc = Value::from(serde_json::json!({"name": "Product"})); assert_eq!( - expr.eval(&doc).unwrap(), + expr.eval(&doc, &crs()).unwrap(), Value::String("Product - default".into()) ); } @@ -489,14 +496,14 @@ mod tests { fn null_literal() { let (expr, _) = parse_ok("COALESCE(x, NULL, 0)"); let doc = Value::from(serde_json::json!({"x": null})); - assert_eq!(expr.eval(&doc).unwrap(), Value::Integer(0)); + assert_eq!(expr.eval(&doc, &crs()).unwrap(), Value::Integer(0)); } #[test] fn nested_functions() { let (expr, _) = parse_ok("ROUND(price * (1 - COALESCE(discount, 0)), 2)"); let doc = Value::from(serde_json::json!({"price": 49.99})); - assert_eq!(expr.eval(&doc).unwrap(), Value::Float(49.99)); + assert_eq!(expr.eval(&doc, &crs()).unwrap(), Value::Float(49.99)); } #[test] @@ -514,7 +521,10 @@ mod tests { fn cjk_string_in_concat() { let (expr, _) = parse_ok("CONCAT('你好', name)"); let doc = Value::from(serde_json::json!({"name": "world"})); - assert_eq!(expr.eval(&doc).unwrap(), Value::String("你好world".into())); + assert_eq!( + expr.eval(&doc, &crs()).unwrap(), + Value::String("你好world".into()) + ); } #[test] @@ -522,6 +532,6 @@ mod tests { let (expr, deps) = parse_ok("name != '禁止'"); assert_eq!(deps, vec!["name"]); let doc = Value::from(serde_json::json!({"name": "allowed"})); - assert_eq!(expr.eval(&doc).unwrap(), Value::Bool(true)); + assert_eq!(expr.eval(&doc, &crs()).unwrap(), Value::Bool(true)); } } diff --git a/nodedb-query/src/expr_parse/predicate.rs b/nodedb-query/src/expr_parse/predicate.rs index 562cf8f89..7eb5376f3 100644 --- a/nodedb-query/src/expr_parse/predicate.rs +++ b/nodedb-query/src/expr_parse/predicate.rs @@ -131,7 +131,8 @@ mod tests { fn eval(text: &str, doc: serde_json::Value) -> Value { let (expr, _) = parse_generated_expr(text).expect("parses"); - expr.eval(&Value::from(doc)).expect("evaluates") + expr.eval(&Value::from(doc), &nodedb_crs::CrsContext::builtin_only()) + .expect("evaluates") } #[test] diff --git a/nodedb-query/src/functions/eval.rs b/nodedb-query/src/functions/eval.rs index 601f6af6e..e4634963e 100644 --- a/nodedb-query/src/functions/eval.rs +++ b/nodedb-query/src/functions/eval.rs @@ -2,6 +2,7 @@ //! Scalar function evaluation dispatch across per-family sub-modules. +use nodedb_crs::CrsContext; use nodedb_types::Value; use crate::expr::EvalError; @@ -25,7 +26,9 @@ use super::{ /// The fallible families (`math`, `vector`, `json`, geo) return /// `Option>`. The rest stay `Option`-shaped /// and are wrapped in `Ok` at this dispatch boundary. -pub fn eval_function(name: &str, args: &[Value]) -> Result { +/// +/// Geo functions resolve SRIDs through `crs`. +pub fn eval_function(name: &str, args: &[Value], crs: &CrsContext<'_>) -> Result { if let Some(v) = string::try_eval(name, args) { return Ok(v); } @@ -63,8 +66,8 @@ pub fn eval_function(name: &str, args: &[Value]) -> Result { return Ok(v); } // Geo / Spatial functions — delegated to geo_functions module. A geo - // fault (unsupported or mixed SRID, projection domain) fails the call. - match crate::geo_functions::eval_geo_function(name, args) { + // fault (undefined or mixed SRID, projection domain) fails the call. + match crate::geo_functions::eval_geo_function(name, args, crs) { Some(r) => r, None => Err(EvalError::UnknownFunction { name: name.to_owned(), @@ -77,19 +80,24 @@ mod tests { use super::*; use crate::expr::SqlExpr; + fn crs() -> CrsContext<'static> { + CrsContext::builtin_only() + } + fn eval_fn(name: &str, args: Vec) -> Value { - eval_function(name, &args).unwrap() + eval_function(name, &args, &crs()).unwrap() } #[test] fn mod_by_zero_errors() { - let err = eval_function("mod", &[Value::Integer(5), Value::Integer(0)]).unwrap_err(); + let err = + eval_function("mod", &[Value::Integer(5), Value::Integer(0)], &crs()).unwrap_err(); assert_eq!(err, EvalError::DivisionByZero); } #[test] fn an_unknown_function_is_an_error_not_null() { - let err = eval_function("no_such_function", &[Value::Integer(1)]).unwrap_err(); + let err = eval_function("no_such_function", &[Value::Integer(1)], &crs()).unwrap_err(); assert_eq!( err, EvalError::UnknownFunction { @@ -136,7 +144,7 @@ mod tests { ), ]; for (name, args) in calls { - let value = eval_function(name, &args) + let value = eval_function(name, &args, &crs()) .unwrap_or_else(|e| panic!("{name}() must evaluate, got {e}")); assert_ne!(value, Value::Null, "{name}() gave NULL"); } @@ -214,7 +222,7 @@ mod tests { name: "like".into(), args: vec![SqlExpr::Literal(text("bob")), SqlExpr::Literal(text("a%"))], })); - assert_eq!(expr.eval(&Value::Null).unwrap(), Value::Bool(true)); + assert_eq!(expr.eval(&Value::Null, &crs()).unwrap(), Value::Bool(true)); } #[test] @@ -283,7 +291,10 @@ mod tests { .into_iter() .collect(), ); - assert_eq!(expr.eval(&doc).unwrap(), Value::String("ALICE".into())); + assert_eq!( + expr.eval(&doc, &crs()).unwrap(), + Value::String("ALICE".into()) + ); } #[test] @@ -358,7 +369,8 @@ mod tests { vec![point_value(0.0, 0.0), point_value(0.0, 1.0)], ); let d = result.as_f64().unwrap(); - assert!((d - 111_195.0).abs() < 500.0, "got {d}"); + // WGS 84 geodesic, pyproj Geod(ellps="WGS84").inv(0, 0, 0, 1). + assert!((d - 110_574.388_557_798_78).abs() < 1e-6, "got {d}"); } #[test] @@ -401,7 +413,7 @@ mod tests { )); let result = eval_fn("geo_length", vec![line]); let d = result.as_f64().unwrap(); - assert!((d - 111_195.0).abs() < 500.0, "got {d}"); + assert!((d - 110_574.388_557_798_78).abs() < 1e-6, "got {d}"); } #[test] diff --git a/nodedb-query/src/geo_functions/accessors.rs b/nodedb-query/src/geo_functions/accessors.rs index 7c4f8f07a..eaf90a924 100644 --- a/nodedb-query/src/geo_functions/accessors.rs +++ b/nodedb-query/src/geo_functions/accessors.rs @@ -1,192 +1,214 @@ // SPDX-License-Identifier: Apache-2.0 -//! Readers over a geometry's own contents: ordinates, type name, vertex count, -//! and the WKT / GeoJSON renderings. The SRID reader lives in `crs`. +//! Readers over a geometry's own contents: ordinates, dimension, type name +//! and vertex count. The encodings live in `encode`, the SRID reader in +//! `crs`. +use nodedb_crs::CrsContext; use nodedb_types::Value; use nodedb_types::geometry::{Geometry, SridGeometry}; -use super::helpers::{GeoEval, geom_arg}; +use super::helpers::{GeoEval, geom_arg, unary}; +use crate::expr::EvalError; use crate::value_ops::to_value_number; -pub(super) fn eval(canonical: &'static str, args: &[Value]) -> GeoEval { +pub(super) fn eval(canonical: &'static str, args: &[Value], ctx: &CrsContext<'_>) -> GeoEval { let result = match canonical { - "st_x" => ordinate(args, 0), - "st_y" => ordinate(args, 1), - // WKT stays plain: it has no SRID slot. - "st_astext" => unary(args, |g| { - Value::String(nodedb_spatial::geometry_to_wkt(&g.geometry)) + "st_x" => ordinate(canonical, args, ctx, Ordinate::X), + "st_y" => ordinate(canonical, args, ctx, Ordinate::Y), + "st_z" => ordinate(canonical, args, ctx, Ordinate::Z), + "st_m" => ordinate(canonical, args, ctx, Ordinate::M), + "st_zmin" => unary(canonical, args, ctx, |g| Ok(z_extreme(g, f64::min))), + "st_zmax" => unary(canonical, args, ctx, |g| Ok(z_extreme(g, f64::max))), + "st_coorddim" | "st_ndims" => unary(canonical, args, ctx, |g| { + Ok(Value::Integer(i64::from(g.dimension().coord_dim()))) }), - // GeoJSON carries the `crs` member for any SRID other than 4326. - "st_asgeojson" => unary(args, |g| match sonic_rs::to_string(g) { - Ok(json) => Value::String(json), - Err(_) => Value::Null, + "st_zmflag" => unary(canonical, args, ctx, |g| { + Ok(Value::Integer(i64::from(g.dimension().zm_flag()))) }), - "st_geometrytype" => unary(args, |g| { - Value::String(g.geometry.geometry_type().to_string()) + "st_hasz" => unary(canonical, args, ctx, |g| Ok(Value::Bool(g.has_z()))), + "st_hasm" => unary(canonical, args, ctx, |g| Ok(Value::Bool(g.has_m()))), + "st_isempty" => unary(canonical, args, ctx, |g| Ok(Value::Bool(g.is_empty()))), + "st_geometrytype" => unary(canonical, args, ctx, |g| { + Ok(Value::String(g.geometry().geometry_type().to_string())) + }), + "st_npoints" => unary(canonical, args, ctx, |g| { + Ok(Value::Integer(g.geometry().coord_count() as i64)) }), - "st_npoints" => unary(args, |g| Value::Integer(count_points(&g.geometry) as i64)), _ => return None, }; - Some(Ok(result)) + Some(result) } -fn unary(args: &[Value], f: fn(&SridGeometry) -> Value) -> Value { - match geom_arg(args, 0) { - Some(geom) => f(&geom), - None => Value::Null, - } +#[derive(Clone, Copy)] +enum Ordinate { + X, + Y, + Z, + M, } -/// Read one ordinate of a point. PostGIS defines `ST_X`/`ST_Y` on points only; -/// any other geometry yields NULL rather than an arbitrary vertex. -fn ordinate(args: &[Value], index: usize) -> Value { - match geom_arg(args, 0).map(|g| g.geometry) { - Some(Geometry::Point { coordinates }) => to_value_number(coordinates[index]), - _ => Value::Null, +/// One ordinate of a point. PostGIS defines these on points only: any other +/// geometry, an EMPTY point, and a missing Z or M yield NULL. +fn ordinate( + function: &'static str, + args: &[Value], + ctx: &CrsContext<'_>, + which: Ordinate, +) -> Result { + let Some(g) = geom_arg(args, 0, function, ctx)? else { + return Ok(Value::Null); + }; + if !matches!(g.geometry(), Geometry::Point { .. }) || g.is_empty() { + return Ok(Value::Null); } + let Some(p) = g.positions().next() else { + return Ok(Value::Null); + }; + let value = match which { + Ordinate::X => Some(p.x), + Ordinate::Y => Some(p.y), + Ordinate::Z => p.z, + Ordinate::M => p.m, + }; + Ok(value.map_or(Value::Null, to_value_number)) } -fn count_points(geom: &Geometry) -> usize { - match geom { - Geometry::Point { .. } => 1, - Geometry::LineString { coordinates } | Geometry::MultiPoint { coordinates } => { - coordinates.len() - } - Geometry::Polygon { coordinates } | Geometry::MultiLineString { coordinates } => { - coordinates.iter().map(|ring| ring.len()).sum() - } - Geometry::MultiPolygon { coordinates } => coordinates +/// The least or greatest Z. A geometry without Z has a Z of 0 everywhere, +/// as PostGIS reports it. EMPTY is NULL. +fn z_extreme(g: &SridGeometry, pick: fn(f64, f64) -> f64) -> Value { + if g.is_empty() { + return Value::Null; + } + match g.ordinates().z() { + Some(z) => z .iter() - .flat_map(|poly| poly.iter()) - .map(|ring| ring.len()) - .sum(), - Geometry::GeometryCollection { geometries } => geometries.iter().map(count_points).sum(), - // `Geometry` is `#[non_exhaustive]`; a kind added upstream exposes no - // coordinates this crate can count. - _ => 0, + .copied() + .reduce(pick) + .map_or(Value::Null, to_value_number), + None => to_value_number(0.0), } } #[cfg(test)] mod tests { use super::*; + use nodedb_types::geometry::{Ordinates, Srid}; + + fn ctx() -> CrsContext<'static> { + CrsContext::builtin_only() + } - /// The evaluator with its infallible accessors unwrapped. + /// The evaluator with a fault turned into a test failure. fn eval(canonical: &'static str, args: &[Value]) -> Option { - super::eval(canonical, args).map(|r| r.expect("accessors never fail")) + super::eval(canonical, args, &ctx()).map(|r| r.expect("no fault expected")) } fn geom(v: Geometry) -> Vec { vec![Value::Geometry(SridGeometry::wgs84(v))] } + fn zm_point() -> Vec { + vec![Value::Geometry( + SridGeometry::with_ordinates( + Srid::WGS84, + Geometry::point(1.0, 2.0), + Ordinates::new(Some(vec![3.0]), Some(vec![4.0])), + ) + .unwrap(), + )] + } + #[test] fn ordinates_read_a_point() { - let point = geom(Geometry::point(1.0, 2.0)); + let point = zm_point(); assert_eq!(eval("st_x", &point), Some(to_value_number(1.0))); assert_eq!(eval("st_y", &point), Some(to_value_number(2.0))); + assert_eq!(eval("st_z", &point), Some(to_value_number(3.0))); + assert_eq!(eval("st_m", &point), Some(to_value_number(4.0))); + let xy = geom(Geometry::point(1.0, 2.0)); + assert_eq!(eval("st_z", &xy), Some(Value::Null)); + assert_eq!(eval("st_m", &xy), Some(Value::Null)); } /// A non-point has no single X; returning a vertex would be a guess. #[test] - fn ordinates_of_a_non_point_are_null() { + fn ordinates_of_a_non_point_or_empty_are_null() { let line = geom(Geometry::line_string(vec![[0.0, 0.0], [1.0, 1.0]])); assert_eq!(eval("st_x", &line), Some(Value::Null)); - assert_eq!(eval("st_y", &line), Some(Value::Null)); - } - - #[test] - fn astext_renders_wkt() { - let Some(Value::String(wkt)) = eval("st_astext", &geom(Geometry::point(1.0, 2.0))) else { - panic!("expected WKT text"); - }; - assert!(wkt.to_uppercase().starts_with("POINT"), "got {wkt}"); - assert!(wkt.contains('1') && wkt.contains('2'), "got {wkt}"); - } - - #[test] - fn asgeojson_renders_geojson() { - let Some(Value::String(json)) = eval("st_asgeojson", &geom(Geometry::point(1.0, 2.0))) - else { - panic!("expected GeoJSON text"); - }; - assert!( - json.contains("\"Point\"") && json.contains("coordinates"), - "got {json}" + assert_eq!( + eval("st_x", &geom(Geometry::empty_point())), + Some(Value::Null) ); } - /// The two renderings must describe the same geometry — a reader that - /// round-trips one must get the other's geometry back. #[test] - fn wkt_and_geojson_renderings_agree() { - let original = Geometry::line_string(vec![[0.0, 0.0], [1.0, 1.0]]); - let Some(Value::String(wkt)) = eval("st_astext", &geom(original.clone())) else { - panic!("expected WKT"); - }; - let Some(Value::String(json)) = eval("st_asgeojson", &geom(original.clone())) else { - panic!("expected GeoJSON"); - }; - assert_eq!( - super::super::helpers::geometry_from_text(&wkt), - Some(SridGeometry::wgs84(original.clone())) - ); - assert_eq!( - super::super::helpers::geometry_from_text(&json), - Some(SridGeometry::wgs84(original)) - ); + fn dimension_readers() { + let zm = zm_point(); + assert_eq!(eval("st_coorddim", &zm), Some(Value::Integer(4))); + assert_eq!(eval("st_ndims", &zm), Some(Value::Integer(4))); + assert_eq!(eval("st_zmflag", &zm), Some(Value::Integer(3))); + assert_eq!(eval("st_hasz", &zm), Some(Value::Bool(true))); + assert_eq!(eval("st_hasm", &zm), Some(Value::Bool(true))); + let xy = geom(Geometry::point(0.0, 0.0)); + assert_eq!(eval("st_zmflag", &xy), Some(Value::Integer(0))); + assert_eq!(eval("st_coorddim", &xy), Some(Value::Integer(2))); } - /// GeoJSON keeps a projected SRID in its `crs` member; WKT stays plain. #[test] - fn projected_renderings() { - let g = SridGeometry::new( - nodedb_types::geometry::Srid::Utm48N, - Geometry::point(1.0, 2.0), + fn z_extremes() { + let line = vec![Value::Geometry( + SridGeometry::with_ordinates( + Srid::WGS84, + Geometry::line_string(vec![[0.0, 0.0], [1.0, 1.0], [2.0, 2.0]]), + Ordinates::new(Some(vec![5.0, -1.0, 3.0]), None), + ) + .unwrap(), + )]; + assert_eq!(eval("st_zmin", &line), Some(to_value_number(-1.0))); + assert_eq!(eval("st_zmax", &line), Some(to_value_number(5.0))); + assert_eq!( + eval("st_zmax", &geom(Geometry::point(0.0, 0.0))), + Some(to_value_number(0.0)) ); - let args = vec![Value::Geometry(g.clone())]; - let Some(Value::String(json)) = eval("st_asgeojson", &args) else { - panic!("expected GeoJSON"); - }; - assert!(json.contains("EPSG:32648"), "{json}"); - assert_eq!(super::super::helpers::geometry_from_text(&json), Some(g)); - let Some(Value::String(wkt)) = eval("st_astext", &args) else { - panic!("expected WKT"); - }; - assert!(!wkt.contains("SRID"), "{wkt}"); } #[test] - fn geometrytype_names_the_variant() { + fn type_count_and_empty() { assert_eq!( eval("st_geometrytype", &geom(Geometry::point(0.0, 0.0))), Some(Value::String("Point".into())) ); - } - - #[test] - fn npoints_counts_every_vertex() { let line = geom(Geometry::line_string(vec![ [0.0, 0.0], [1.0, 1.0], [2.0, 2.0], ])); assert_eq!(eval("st_npoints", &line), Some(Value::Integer(3))); - } - - #[test] - fn unreadable_geometry_yields_null() { - let bad = vec![Value::String("not a geometry".into())]; - assert_eq!(eval("st_astext", &bad), Some(Value::Null)); - assert_eq!(eval("st_npoints", &bad), Some(Value::Null)); + assert_eq!( + eval("st_npoints", &geom(Geometry::empty_point())), + Some(Value::Integer(0)) + ); + assert_eq!( + eval("st_isempty", &geom(Geometry::empty_point())), + Some(Value::Bool(true)) + ); } /// A WKT string argument is a geometry, so accessors read it directly. #[test] fn wkt_string_argument_is_accepted() { - let wkt = vec![Value::String("POINT(3 4)".into())]; + let wkt = vec![Value::String("POINT Z (3 4 5)".into())]; assert_eq!(eval("st_x", &wkt), Some(to_value_number(3.0))); - assert_eq!(eval("st_y", &wkt), Some(to_value_number(4.0))); + assert_eq!(eval("st_z", &wkt), Some(to_value_number(5.0))); + } + + #[test] + fn unreadable_text_fails_the_statement() { + let bad = vec![Value::String("not a geometry".into())]; + assert!(matches!( + super::eval("st_npoints", &bad, &ctx()), + Some(Err(EvalError::Geometry { .. })) + )); } } diff --git a/nodedb-query/src/geo_functions/catalog/spec.rs b/nodedb-query/src/geo_functions/catalog/spec.rs index 5378abec4..26298544d 100644 --- a/nodedb-query/src/geo_functions/catalog/spec.rs +++ b/nodedb-query/src/geo_functions/catalog/spec.rs @@ -18,12 +18,12 @@ pub enum GeoArgShape { Geometry2Distance, /// `(geom, srid)` GeometrySrid, + /// `(geom, srid [, epoch])`: the epoch is a decimal year. + GeometrySridEpoch, /// `(geom, distance_m [, segments])` GeometryBuffer, /// `(lng, lat)` LngLat, - /// `(x, y [, z])` - LngLatZ, /// `(lng, lat [, precision])` LngLatPrecision, /// `(lat, lng, resolution)` — note the reversed leading pair relative to @@ -45,6 +45,26 @@ pub enum GeoArgShape { PointsVariadic, /// `(ring, ring, ...)` RingsVariadic, + /// `(x, y [, z [, m]])` + Xyzm, + /// `(x, y, m)` + Xym, + /// `(geom, number)` + GeometryNumber, + /// `(geom [, number])` + GeometryNumberOpt, + /// `(geom [, number [, number]])` + GeometryNumbers2Opt, + /// `(geom, number [, number])` + GeometryNumberNumberOpt, + /// `(geom, number, number [, number])` + GeometryRange, + /// `(geom [, text])` + GeometryTextOpt, + /// `(geom1, geom2 [, text])` + Geometry2TextOpt, + /// `(z, x, y [, bounds])` + TileEnvelope, } impl GeoArgShape { @@ -52,11 +72,19 @@ impl GeoArgShape { pub const fn arity(self) -> (usize, usize) { match self { Self::Geometry1 | Self::Text1 => (1, 1), - Self::TextSrid | Self::BytesSrid => (1, 2), - Self::Geometry2 | Self::LngLat | Self::GeometrySrid => (2, 2), - Self::GeometryBuffer | Self::LngLatZ | Self::LngLatPrecision => (2, 3), - Self::Geometry2Distance | Self::LatLngResolution => (3, 3), - Self::Circle => (3, 4), + Self::TextSrid | Self::BytesSrid | Self::GeometryNumberOpt | Self::GeometryTextOpt => { + (1, 2) + } + Self::GeometryNumbers2Opt => (1, 3), + Self::Geometry2 | Self::LngLat | Self::GeometrySrid | Self::GeometryNumber => (2, 2), + Self::GeometryBuffer + | Self::GeometrySridEpoch + | Self::LngLatPrecision + | Self::GeometryNumberNumberOpt + | Self::Geometry2TextOpt => (2, 3), + Self::Xyzm => (2, 4), + Self::Geometry2Distance | Self::LatLngResolution | Self::Xym => (3, 3), + Self::Circle | Self::GeometryRange | Self::TileEnvelope => (3, 4), Self::Haversine4 | Self::Bbox => (4, 4), Self::PointsVariadic => (2, MAX_VARIADIC), Self::RingsVariadic => (1, MAX_VARIADIC), @@ -64,6 +92,42 @@ impl GeoArgShape { } } +impl GeoArgShape { + /// The zero-based position of the SRID argument, for a shape that has + /// one: `(geom, srid)`, `(geom, srid [, epoch])`, `(wkt [, srid])` and + /// `(wkb [, srid])`. + pub const fn srid_position(self) -> Option { + match self { + Self::GeometrySrid | Self::GeometrySridEpoch | Self::TextSrid | Self::BytesSrid => { + Some(1) + } + Self::Geometry1 + | Self::Geometry2 + | Self::Geometry2Distance + | Self::GeometryBuffer + | Self::LngLat + | Self::LngLatPrecision + | Self::LatLngResolution + | Self::Text1 + | Self::Haversine4 + | Self::Circle + | Self::Bbox + | Self::PointsVariadic + | Self::RingsVariadic + | Self::Xyzm + | Self::Xym + | Self::GeometryNumber + | Self::GeometryNumberOpt + | Self::GeometryNumbers2Opt + | Self::GeometryNumberNumberOpt + | Self::GeometryRange + | Self::GeometryTextOpt + | Self::Geometry2TextOpt + | Self::TileEnvelope => None, + } + } +} + /// Upper bound on a variadic geo constructor's argument count. Matches the /// registry's existing cap for `geo_line` / `geo_polygon`. pub const MAX_VARIADIC: usize = 255; @@ -79,9 +143,13 @@ pub enum GeoReturn { Float, Int, Text, + Bytes, Geometry, Object, Array, + /// The type depends on the argument count (`ST_Relate`: text with two + /// arguments, boolean with a pattern). Typed at runtime. + ByArity, } /// One geospatial SQL function. diff --git a/nodedb-query/src/geo_functions/catalog/table.rs b/nodedb-query/src/geo_functions/catalog/table.rs index 36a5bbcd5..967cb9842 100644 --- a/nodedb-query/src/geo_functions/catalog/table.rs +++ b/nodedb-query/src/geo_functions/catalog/table.rs @@ -27,41 +27,96 @@ pub static GEO_FUNCTIONS: &[GeoFunctionSpec] = &[ f("st_within", &[], Geometry2, Bool), f("st_disjoint", &[], Geometry2, Bool), f("st_dwithin", &[], Geometry2Distance, Bool), + f("st_touches", &[], Geometry2, Bool), + f("st_crosses", &[], Geometry2, Bool), + f("st_overlaps", &[], Geometry2, Bool), + f("st_equals", &[], Geometry2, Bool), + f("st_covers", &[], Geometry2, Bool), + f("st_coveredby", &[], Geometry2, Bool), + f("st_relate", &[], Geometry2TextOpt, ByArity), + f("st_3ddwithin", &[], Geometry2Distance, Bool), + f("st_3dintersects", &[], Geometry2, Bool), f("st_isvalid", &["geo_is_valid"], Geometry1, Bool), + f("st_isvalidreason", &[], Geometry1, Text), + f("st_isempty", &[], Geometry1, Bool), // ── Measures ── f("st_distance", &[], Geometry2, Float), f("st_length", &["geo_length"], Geometry1, Float), f("st_perimeter", &["geo_perimeter"], Geometry1, Float), f("st_area", &[], Geometry1, Float), + f("st_3ddistance", &[], Geometry2, Float), + f("st_3dlength", &[], Geometry1, Float), f("geo_distance", &["haversine_distance"], Haversine4, Float), f("geo_bearing", &["haversine_bearing"], Haversine4, Float), // ── Accessors ── f("st_x", &["geo_x"], Geometry1, Float), f("st_y", &["geo_y"], Geometry1, Float), - f("st_astext", &["geo_as_wkt"], Geometry1, Text), - f("st_asgeojson", &["geo_as_geojson"], Geometry1, Text), + f("st_z", &[], Geometry1, Float), + f("st_m", &[], Geometry1, Float), + f("st_zmin", &[], Geometry1, Float), + f("st_zmax", &[], Geometry1, Float), + f("st_coorddim", &[], Geometry1, Int), + f("st_ndims", &[], Geometry1, Int), + f("st_zmflag", &[], Geometry1, Int), + f("st_hasz", &[], Geometry1, Bool), + f("st_hasm", &[], Geometry1, Bool), f("st_geometrytype", &["geo_type"], Geometry1, Text), f("st_npoints", &["geo_num_points"], Geometry1, Int), + // ── Encodings ── + f("st_astext", &["geo_as_wkt"], Geometry1, Text), + f("st_asewkt", &[], Geometry1, Text), + f("st_asgeojson", &["geo_as_geojson"], Geometry1, Text), + f("st_asbinary", &[], GeometryTextOpt, Bytes), + f("st_asewkb", &[], GeometryTextOpt, Bytes), // ── Spatial reference systems ── f("st_srid", &[], Geometry1, Int), f("st_setsrid", &[], GeometrySrid, Geometry), - f("st_transform", &[], GeometrySrid, Geometry), + f("st_transform", &[], GeometrySridEpoch, Geometry), + f("st_shiftlongitude", &[], Geometry1, Geometry), // ── Constructors ── f("st_point", &["geo_point"], LngLat, Geometry), - f("st_makepoint", &[], LngLatZ, Geometry), - f("st_geomfromtext", &["geo_from_wkt"], TextSrid, Geometry), + f("st_makepoint", &[], Xyzm, Geometry), + f("st_makepointm", &[], Xym, Geometry), + f( + "st_geomfromtext", + &["geo_from_wkt", "st_geomfromewkt"], + TextSrid, + Geometry, + ), f("st_geomfromgeojson", &["geo_from_geojson"], Text1, Geometry), - f("st_geomfromwkb", &[], BytesSrid, Geometry), + f("st_geomfromwkb", &["st_geomfromewkb"], BytesSrid, Geometry), f("st_makeline", &["geo_line"], PointsVariadic, Geometry), f("st_makepolygon", &["geo_polygon"], RingsVariadic, Geometry), f("st_makeenvelope", &["geo_bbox"], Bbox, Geometry), + f("st_tileenvelope", &[], TileEnvelope, Geometry), f("geo_circle", &[], Circle, Geometry), + // ── Dimension ── + f("st_force2d", &[], Geometry1, Geometry), + f("st_force3d", &["st_force3dz"], GeometryNumberOpt, Geometry), + f("st_force3dm", &[], GeometryNumberOpt, Geometry), + f("st_force4d", &[], GeometryNumbers2Opt, Geometry), // ── Geometry-returning operations ── f("st_buffer", &[], GeometryBuffer, Geometry), f("st_envelope", &[], Geometry1, Geometry), f("st_union", &[], Geometry2, Geometry), f("st_intersection", &[], Geometry2, Geometry), + f("st_difference", &[], Geometry2, Geometry), + f("st_symdifference", &[], Geometry2, Geometry), f("st_centroid", &[], Geometry1, Geometry), + f("st_convexhull", &[], Geometry1, Geometry), + f("st_concavehull", &[], GeometryNumber, Geometry), + f("st_simplify", &[], GeometryNumber, Geometry), + f("st_simplifypreservetopology", &[], GeometryNumber, Geometry), + f("st_makevalid", &[], Geometry1, Geometry), + f("st_subdivide", &[], GeometryNumberOpt, Geometry), + f("st_segmentize", &[], GeometryNumber, Geometry), + f("st_closestpoint", &[], Geometry2, Geometry), + f("st_split", &[], Geometry2, Geometry), + // ── Linear referencing ── + f("st_lineinterpolatepoint", &[], GeometryNumber, Geometry), + f("st_locatealong", &[], GeometryNumberNumberOpt, Geometry), + f("st_locatebetween", &[], GeometryRange, Geometry), + f("st_interpolatepoint", &[], Geometry2, Float), // ── Geohash ── f("st_geohash", &["geo_geohash"], LngLatPrecision, Text), f("st_geohashdecode", &["geo_geohash_decode"], Text1, Object), @@ -69,8 +124,8 @@ pub static GEO_FUNCTIONS: &[GeoFunctionSpec] = &[ // ── H3 ── // `geo_h3` and `h3_latlngtocell` are NOT aliases: `geo_h3` takes // (lng, lat, resolution) while the H3 surface name takes (lat, lng, - // resolution). Folding them together would silently transpose - // coordinates, so they stay separate rows with separate shapes. + // resolution). One row for both transposes coordinates, so they stay + // separate rows with separate shapes. f("geo_h3", &[], LngLatPrecision, Text), f("h3_latlngtocell", &[], LatLngResolution, Text), f("h3_celltolatlng", &[], Text1, Object), diff --git a/nodedb-query/src/geo_functions/constructors.rs b/nodedb-query/src/geo_functions/constructors.rs index 7556088a0..dd5a0fbd8 100644 --- a/nodedb-query/src/geo_functions/constructors.rs +++ b/nodedb-query/src/geo_functions/constructors.rs @@ -1,159 +1,124 @@ // SPDX-License-Identifier: Apache-2.0 -//! Geometry constructors and geometry-returning operations. +//! Geometry constructors. //! -//! A constructor from bare numbers builds a WGS 84 geometry. An operation on -//! geometries keeps its input SRID. A binary operation needs both inputs in -//! one SRID. +//! A constructor from bare numbers builds a WGS 84 geometry. A constructor +//! from geometries keeps their SRID and needs them in one SRID. Text or +//! bytes that do not parse fail the statement. +use nodedb_crs::CrsContext; +use nodedb_spatial::SridCheck; use nodedb_types::Value; -use nodedb_types::geometry::{Geometry, Srid, SridGeometry}; +use nodedb_types::geometry::{Dimension, Geometry, Ordinates, Srid, SridGeometry}; use super::helpers::{ - GeoEval, SridArg, geom_arg, num_arg, parse_text_geometry, same_srid, srid_arg, str_arg, - text_fault, + GeoEval, SridArg, finite_arg, geom_arg, geometry_fault, num_arg, same_srid, srid_arg, str_arg, + text_geometry, }; use crate::expr::EvalError; /// Default segment count for the circular approximations `ST_Buffer` and /// `geo_circle` produce when the caller does not specify one. -const DEFAULT_BUFFER_SEGMENTS: usize = 32; +pub(super) const DEFAULT_BUFFER_SEGMENTS: usize = 32; -pub(super) fn eval(canonical: &'static str, args: &[Value]) -> GeoEval { +pub(super) fn eval(canonical: &'static str, args: &[Value], ctx: &CrsContext<'_>) -> GeoEval { let result = match canonical { - "st_point" | "st_makepoint" => Ok(point(args)), - "st_geomfromtext" => from_text(canonical, args, true), - "st_geomfromgeojson" => from_text(canonical, args, false), - "st_geomfromwkb" => from_wkb(canonical, args), - "st_makeline" => make_line(canonical, args), - "st_makepolygon" => Ok(make_polygon(args)), - "st_makeenvelope" => Ok(make_envelope(args)), - "geo_circle" => Ok(circle(args)), - "st_buffer" => Ok(buffer(args)), - "st_envelope" => Ok(unary(args, |g| { - Some(nodedb_spatial::st_envelope_in( - &g.geometry, - g.srid.bbox_space(), - )) - })), - "st_centroid" => Ok(unary(args, |g| { - nodedb_spatial::st_centroid_in(&g.geometry, g.srid.bbox_space()) - })), - "st_union" => binary(canonical, args, nodedb_spatial::st_union), - "st_intersection" => binary(canonical, args, nodedb_spatial::st_intersection), + "st_point" => point(canonical, args, Dimension::Xy), + "st_makepoint" => { + let dimension = match args.len() { + 3 => Dimension::Xyz, + 4 => Dimension::Xyzm, + _ => Dimension::Xy, + }; + point(canonical, args, dimension) + } + "st_makepointm" => point(canonical, args, Dimension::Xym), + "st_geomfromtext" => from_text(canonical, args, ctx, true), + "st_geomfromgeojson" => from_text(canonical, args, ctx, false), + "st_geomfromwkb" => from_wkb(canonical, args, ctx), + "st_makeline" => make_line(canonical, args, ctx), + "st_makepolygon" => make_polygon(canonical, args), + "st_makeenvelope" => make_envelope(canonical, args), + "st_tileenvelope" => tile_envelope(canonical, args, ctx), + "geo_circle" => circle(canonical, args, ctx), _ => return None, }; Some(result) } -/// A unary geometry operation whose result keeps the input SRID. -fn unary(args: &[Value], f: fn(&SridGeometry) -> Option) -> Value { - match geom_arg(args, 0) { - Some(g) => match f(&g) { - Some(out) => Value::Geometry(g.with_geometry(out)), - None => Value::Null, - }, - None => Value::Null, +/// `ST_Point(x, y)`, `ST_MakePoint(x, y [, z [, m]])`, `ST_MakePointM(x, +/// y, m)`. A NULL ordinate makes the point NULL. +fn point(function: &'static str, args: &[Value], dimension: Dimension) -> Result { + let n = usize::from(dimension.coord_dim()); + let mut v = [0.0f64; 4]; + for (i, slot) in v.iter_mut().enumerate().take(n) { + match finite_arg(args, i, function)? { + Some(x) => *slot = x, + None => return Ok(Value::Null), + } } + let z = dimension.has_z().then(|| vec![v[2]]); + let m = dimension + .has_m() + .then(|| vec![v[if dimension.has_z() { 3 } else { 2 }]]); + let g = SridGeometry::with_ordinates( + Srid::WGS84, + Geometry::point(v[0], v[1]), + Ordinates::new(z, m), + ) + .map_err(|e| geometry_fault(function, e.into()))?; + Ok(Value::Geometry(g)) } -/// A binary geometry operation over two inputs in one SRID. -fn binary( +/// `ST_GeomFromText(text [, srid])` and `ST_GeomFromGeoJSON(text)`. GeoJSON +/// and EWKT keep the SRID they name. An explicit SRID argument relabels the +/// result. +fn from_text( function: &'static str, args: &[Value], - f: fn(&Geometry, &Geometry) -> Geometry, + ctx: &CrsContext<'_>, + takes_srid: bool, ) -> Result { - let (Some(a), Some(b)) = (geom_arg(args, 0), geom_arg(args, 1)) else { - return Ok(Value::Null); - }; - let srid = same_srid(function, &a, &b)?; - Ok(Value::Geometry(SridGeometry::new( - srid, - f(&a.geometry, &b.geometry), - ))) -} - -/// `ST_GeomFromText(text [, srid])` and `ST_GeomFromGeoJSON(text)`. A GeoJSON -/// text keeps the SRID its `crs` member names. An explicit SRID argument -/// relabels the result. -fn from_text(function: &'static str, args: &[Value], takes_srid: bool) -> Result { let explicit = if takes_srid { - srid_arg(args, 1, function)? + srid_arg(args, 1, function, ctx)? } else { SridArg::Absent }; let Some(text) = str_arg(args, 0) else { return Ok(Value::Null); }; - let mut geom = match parse_text_geometry(&text) { - Ok(g) => g, - Err(e) => return text_fault(function, e), - }; - match explicit { - SridArg::Absent => {} - SridArg::Null => return Ok(Value::Null), - SridArg::Srid(srid) => geom.srid = srid, - } - Ok(Value::Geometry(geom)) + let geom = text_geometry(function, &text, ctx)?; + Ok(match explicit { + SridArg::Absent => Value::Geometry(geom), + SridArg::Null => Value::Null, + SridArg::Srid(srid) => Value::Geometry(geom.with_srid(srid)), + }) } /// `ST_GeomFromWKB(wkb [, srid])`. An explicit SRID argument wins over the /// SRID an EWKB header carries. ISO WKB with no SRID argument is WGS 84. -fn from_wkb(function: &'static str, args: &[Value]) -> Result { - let explicit = srid_arg(args, 1, function)?; - let Some(bytes) = wkb_bytes(args) else { - return Ok(Value::Null); - }; - let Some((geometry, header_srid)) = nodedb_spatial::geometry_from_wkb(&bytes) else { - return Ok(Value::Null); - }; - let srid = match (explicit, header_srid) { - (SridArg::Null, _) => return Ok(Value::Null), - (SridArg::Srid(srid), _) => srid, - (SridArg::Absent, Some(code)) => { - Srid::from_code(i64::from(code)).map_err(|e| EvalError::UnsupportedSrid { - function, - srid: e.code, - })? - } - (SridArg::Absent, None) => Srid::Wgs84, - }; - Ok(Value::Geometry(SridGeometry::new(srid, geometry))) -} - -/// `ST_Buffer(geom, distance [, segments])`. Distance is metres in either -/// space: geodesic-approximate for 4326, planar for a projected SRID. -fn buffer(args: &[Value]) -> Value { - let (Some(g), Some(distance)) = (geom_arg(args, 0), num_arg(args, 1)) else { - return Value::Null; - }; - let segments = segments_arg(args, 2); - let out = if g.srid.is_geographic() { - nodedb_spatial::st_buffer(&g.geometry, distance, segments) - } else { - nodedb_spatial::planar_buffer(&g.geometry, distance, segments) +fn from_wkb( + function: &'static str, + args: &[Value], + ctx: &CrsContext<'_>, +) -> Result { + let explicit = srid_arg(args, 1, function, ctx)?; + let bytes = match args.first() { + Some(Value::Bytes(bytes)) => bytes.clone(), + Some(Value::String(hex)) => decode_hex(hex.trim()).ok_or(EvalError::ArgumentValue { + function, + position: 1, + reason: "is not hex-encoded WKB".into(), + })?, + _ => return Ok(Value::Null), }; - Value::Geometry(g.with_geometry(out)) -} - -/// Segment count for a circular approximation, floored at 3 — fewer than three -/// segments cannot enclose an area, and a caller asking for 0 would otherwise -/// silently receive a degenerate geometry. -fn segments_arg(args: &[Value], idx: usize) -> usize { - num_arg(args, idx) - .filter(|n| n.is_finite() && *n >= 3.0) - .map_or(DEFAULT_BUFFER_SEGMENTS, |n| n as usize) -} - -/// WKB bytes for `ST_GeomFromWKB`, from either an `X'...'` byte literal or the -/// equivalent hex string. Both spellings reach this function — a client that -/// cannot emit a byte literal passes the same bytes as text. -fn wkb_bytes(args: &[Value]) -> Option> { - match args.first()? { - Value::Bytes(bytes) => Some(bytes.clone()), - Value::String(hex) => decode_hex(hex.trim()), - _ => None, - } + let geom = nodedb_spatial::geometry_from_bytes(&bytes, SridCheck::Resolve(ctx)) + .map_err(|e| geometry_fault(function, e))?; + Ok(match explicit { + SridArg::Absent => Value::Geometry(geom), + SridArg::Null => Value::Null, + SridArg::Srid(srid) => Value::Geometry(geom.with_srid(srid)), + }) } fn decode_hex(hex: &str) -> Option> { @@ -162,114 +127,204 @@ fn decode_hex(hex: &str) -> Option> { } (0..hex.len()) .step_by(2) - .map(|i| u8::from_str_radix(&hex[i..i + 2], 16).ok()) + .map(|i| u8::from_str_radix(hex.get(i..i + 2)?, 16).ok()) .collect() } -fn point(args: &[Value]) -> Value { - let (Some(lng), Some(lat)) = (num_arg(args, 0), num_arg(args, 1)) else { - return Value::Null; +/// `geo_circle(lng, lat, radius_m [, segments])`: a WGS 84 geodesic circle. +fn circle( + function: &'static str, + args: &[Value], + ctx: &CrsContext<'_>, +) -> Result { + let (Some(lng), Some(lat), Some(radius)) = ( + finite_arg(args, 0, function)?, + finite_arg(args, 1, function)?, + finite_arg(args, 2, function)?, + ) else { + return Ok(Value::Null); }; - Value::Geometry(SridGeometry::wgs84(Geometry::point(lng, lat))) + let segments = segments_arg(args, 3); + let def = ctx + .resolve(Srid::WGS84) + .map_err(|source| EvalError::Crs { function, source })?; + let shape = nodedb_spatial::ops::buffer(&Geometry::point(lng, lat), radius, segments, &def) + .map_err(|e| geometry_fault(function, e))?; + Ok(Value::Geometry(SridGeometry::wgs84(shape))) } -fn circle(args: &[Value]) -> Value { - let (Some(lng), Some(lat), Some(radius)) = - (num_arg(args, 0), num_arg(args, 1), num_arg(args, 2)) - else { - return Value::Null; - }; - let segments = segments_arg(args, 3); - Value::Geometry(SridGeometry::wgs84(nodedb_spatial::st_buffer( - &Geometry::point(lng, lat), - radius, - segments, - ))) +/// Segment count for a circular approximation, floored at 3 — fewer than three +/// segments cannot enclose an area, and a caller asking for 0 receives the +/// default rather than a degenerate geometry. +pub(super) fn segments_arg(args: &[Value], idx: usize) -> usize { + num_arg(args, idx) + .filter(|n| n.is_finite() && *n >= 3.0) + .map_or(DEFAULT_BUFFER_SEGMENTS, |n| n as usize) } -/// `ST_MakeLine(point, point, ...)` — every argument must be a readable point; -/// a single unreadable one makes the whole line NULL rather than silently -/// dropping a vertex and shortening the line. Every point must share one -/// SRID, which the line keeps. -fn make_line(function: &'static str, args: &[Value]) -> Result { - let mut coords = Vec::with_capacity(args.len()); - let mut first: Option = None; +/// `ST_MakeLine(point, point, ...)`. Every argument must be a point in one +/// SRID and one dimension, which the line keeps. A NULL argument makes the +/// line NULL. +fn make_line( + function: &'static str, + args: &[Value], + ctx: &CrsContext<'_>, +) -> Result { + let mut points: Vec = Vec::with_capacity(args.len()); for idx in 0..args.len() { - let Some(g) = geom_arg(args, idx) else { - return Ok(Value::Null); - }; - let Geometry::Point { coordinates } = g.geometry else { + let Some(g) = geom_arg(args, idx, function, ctx)? else { return Ok(Value::Null); }; - if let Some(head) = &first { + if !matches!(g.geometry(), Geometry::Point { .. }) || g.is_empty() { + return Err(EvalError::ArgumentValue { + function, + position: idx + 1, + reason: format!( + "must be a non-empty point, got {}", + g.geometry().geometry_type() + ), + }); + } + if let Some(head) = points.first() { same_srid(function, head, &g)?; } - coords.push(coordinates); - first.get_or_insert(g); + points.push(g); } - if coords.len() < 2 { + let Some(first) = points.first() else { return Ok(Value::Null); + }; + if points.len() < 2 { + return Err(EvalError::ArgumentValue { + function, + position: 1, + reason: "a line needs at least two points".into(), + }); + } + let dimension = first.dimension(); + let mut builder = nodedb_types::geometry::OrdinatesBuilder::with_dimension(dimension); + let mut coords = Vec::with_capacity(points.len()); + for p in points.iter().flat_map(|g| g.positions()) { + builder + .push(p.z, p.m) + .map_err(|e| geometry_fault(function, e.into()))?; + coords.push([p.x, p.y]); } - let srid = first.map_or(Srid::Wgs84, |g| g.srid); - Ok(Value::Geometry(SridGeometry::new( - srid, - Geometry::line_string(coords), - ))) + SridGeometry::with_ordinates(first.srid, Geometry::line_string(coords), builder.finish()) + .map(Value::Geometry) + .map_err(|e| geometry_fault(function, e.into())) } /// `ST_MakePolygon(ring, ...)` — each argument is an array of coordinate -/// pairs. As with `ST_MakeLine`, a malformed vertex fails the whole call. -fn make_polygon(args: &[Value]) -> Value { +/// pairs. A malformed vertex fails the whole call. +fn make_polygon(function: &'static str, args: &[Value]) -> Result { let mut rings = Vec::with_capacity(args.len()); - for arg in args { - let Some(points) = arg.as_array() else { - return Value::Null; + for (idx, arg) in args.iter().enumerate() { + if matches!(arg, Value::Null) { + return Ok(Value::Null); + } + let bad = |reason: &str| EvalError::ArgumentValue { + function, + position: idx + 1, + reason: reason.into(), }; + let points = arg + .as_array() + .ok_or_else(|| bad("must be an array of [x, y] pairs"))?; let mut ring = Vec::with_capacity(points.len()); for point in points { - let Some(pair) = point.as_array() else { - return Value::Null; - }; - let (Some(lng), Some(lat)) = ( + let pair = point + .as_array() + .ok_or_else(|| bad("holds a vertex that is not an array"))?; + let (Some(x), Some(y)) = ( pair.first().and_then(Value::as_f64), pair.get(1).and_then(Value::as_f64), ) else { - return Value::Null; + return Err(bad("holds a vertex without two numbers")); }; - ring.push([lng, lat]); + ring.push([x, y]); } - if ring.is_empty() { - return Value::Null; + if ring.len() < 4 { + return Err(bad("a ring needs at least 4 points")); } rings.push(ring); } - if rings.is_empty() { - return Value::Null; + Ok(Value::Geometry(SridGeometry::wgs84(Geometry::polygon( + rings, + )))) +} + +fn make_envelope(function: &'static str, args: &[Value]) -> Result { + let mut v = [0.0f64; 4]; + for (i, slot) in v.iter_mut().enumerate() { + match finite_arg(args, i, function)? { + Some(x) => *slot = x, + None => return Ok(Value::Null), + } } - Value::Geometry(SridGeometry::wgs84(Geometry::polygon(rings))) + let [min_x, min_y, max_x, max_y] = v; + Ok(Value::Geometry(SridGeometry::wgs84(Geometry::polygon( + vec![vec![ + [min_x, min_y], + [max_x, min_y], + [max_x, max_y], + [min_x, max_y], + [min_x, min_y], + ]], + )))) } -fn make_envelope(args: &[Value]) -> Value { - let (Some(min_lng), Some(min_lat), Some(max_lng), Some(max_lat)) = ( - num_arg(args, 0), - num_arg(args, 1), - num_arg(args, 2), - num_arg(args, 3), - ) else { - return Value::Null; +/// `ST_TileEnvelope(z, x, y [, bounds])`: an XYZ tile as an EPSG:3857 +/// polygon, or over the box and SRID of `bounds`. +fn tile_envelope( + function: &'static str, + args: &[Value], + ctx: &CrsContext<'_>, +) -> Result { + let mut zxy = [0i64; 3]; + for (i, slot) in zxy.iter_mut().enumerate() { + match args.get(i) { + None | Some(Value::Null) => return Ok(Value::Null), + Some(Value::Integer(n)) => *slot = *n, + Some(Value::Float(f)) if f.fract() == 0.0 && f.is_finite() => *slot = *f as i64, + Some(other) => { + return Err(EvalError::ArgumentType { + function, + position: i + 1, + expected: "an integer", + got: other.type_name(), + }); + } + } + } + let (bounds, srid) = match geom_arg(args, 3, function, ctx)? { + Some(b) => { + let bb = b + .bbox(ctx) + .map_err(|source| EvalError::Crs { function, source })?; + (bb, b.srid) + } + None => ( + nodedb_spatial::ops::web_mercator_bounds(), + Srid::WEB_MERCATOR, + ), }; - Value::Geometry(SridGeometry::wgs84(Geometry::polygon(vec![vec![ - [min_lng, min_lat], - [max_lng, min_lat], - [max_lng, max_lat], - [min_lng, max_lat], - [min_lng, min_lat], - ]]))) + let shape = nodedb_spatial::ops::tile_envelope(zxy[0], zxy[1], zxy[2], &bounds) + .map_err(|e| geometry_fault(function, e.into()))?; + Ok(Value::Geometry(SridGeometry::new(srid, shape))) } #[cfg(test)] mod tests { use super::*; + use nodedb_crs::UnknownSrid; + + fn eval(canonical: &'static str, args: &[Value]) -> GeoEval { + super::eval(canonical, args, &CrsContext::builtin_only()) + } + + fn srid(code: u32) -> Srid { + Srid::new(code).unwrap() + } /// The evaluator, with a fault turned into a test failure. fn run(canonical: &'static str, args: &[Value]) -> Option { @@ -293,14 +348,59 @@ mod tests { } #[test] - fn geomfromtext_accepts_wkt_and_geojson() { - let wkt = vec![Value::String("POINT(1 2)".into())]; - let json = vec![Value::String( - r#"{"type":"Point","coordinates":[1.0,2.0]}"#.into(), - )]; + fn makepoint_takes_z_and_m() { + let Some(Value::Geometry(z)) = run( + "st_makepoint", + &[Value::Float(1.0), Value::Float(2.0), Value::Float(3.0)], + ) else { + panic!(); + }; + assert_eq!(z.ordinates().z(), Some(&[3.0][..])); + let Some(Value::Geometry(zm)) = run( + "st_makepoint", + &[ + Value::Float(1.0), + Value::Float(2.0), + Value::Float(3.0), + Value::Float(4.0), + ], + ) else { + panic!(); + }; + assert_eq!(zm.dimension(), Dimension::Xyzm); + let Some(Value::Geometry(m)) = run( + "st_makepointm", + &[Value::Float(1.0), Value::Float(2.0), Value::Float(9.0)], + ) else { + panic!(); + }; + assert_eq!(m.ordinates().m(), Some(&[9.0][..])); + assert_eq!(m.ordinates().z(), None); + } + + #[test] + fn geomfromtext_accepts_wkt_ewkt_and_geojson() { let expected = Some(wgs84(Geometry::point(1.0, 2.0))); - assert_eq!(run("st_geomfromtext", &wkt), expected); - assert_eq!(run("st_geomfromgeojson", &json), expected); + assert_eq!( + run("st_geomfromtext", &[Value::String("POINT(1 2)".into())]), + expected + ); + assert_eq!( + run( + "st_geomfromgeojson", + &[Value::String( + r#"{"type":"Point","coordinates":[1.0,2.0]}"#.into() + )] + ), + expected + ); + assert_eq!( + run( + "st_geomfromtext", + &[Value::String("SRID=32648;POINT(1 2)".into())] + ), + Some(projected(srid(32648), Geometry::point(1.0, 2.0))) + ); } #[test] @@ -308,37 +408,39 @@ mod tests { let args = vec![Value::String("POINT(1 2)".into()), Value::Integer(3375)]; assert_eq!( run("st_geomfromtext", &args), - Some(projected(Srid::PeninsularRso, Geometry::point(1.0, 2.0))) + Some(projected(srid(3375), Geometry::point(1.0, 2.0))) ); } #[test] - fn geomfromtext_unsupported_srid_fails() { - let args = vec![Value::String("POINT(1 2)".into()), Value::Integer(2154)]; + fn geomfromtext_undefined_srid_fails() { + let args = vec![Value::String("POINT(1 2)".into()), Value::Integer(999)]; assert_eq!( eval("st_geomfromtext", &args), - Some(Err(EvalError::UnsupportedSrid { + Some(Err(EvalError::Crs { function: "st_geomfromtext", - srid: 2154 + source: UnknownSrid::Undefined { code: 999 } })) ); } - /// A byte literal and its hex-string spelling must decode identically. + /// Malformed text is a statement error, never NULL. #[test] - fn geomfromwkb_accepts_bytes_and_hex_string() { - // WKB for POINT(1 2): little-endian, type 1, x=1.0, y=2.0. - let hex = "0101000000000000000000F03F0000000000000040"; - let bytes = decode_hex(hex).expect("test vector must decode"); - let from_bytes = run("st_geomfromwkb", &[Value::Bytes(bytes)]); - let from_hex = run("st_geomfromwkb", &[Value::String(hex.into())]); - assert_eq!(from_bytes, from_hex); - assert_eq!(from_bytes, Some(wgs84(Geometry::point(1.0, 2.0)))); + fn malformed_text_fails() { + for text in ["POINT(", "POINT(1 2 3 4 5)", "POINT Z (1 2)"] { + assert!( + matches!( + eval("st_geomfromtext", &[Value::String(text.into())]), + Some(Err(EvalError::Geometry { .. })) + ), + "{text}" + ); + } } #[test] fn geomfromwkb_reads_ewkb_srid_and_explicit_srid_wins() { - let g = SridGeometry::new(Srid::Utm48N, Geometry::point(1.0, 2.0)); + let g = SridGeometry::new(srid(32648), Geometry::point(1.0, 2.0)); let ewkb = nodedb_spatial::geometry_to_ewkb(&g).expect("encode"); assert_eq!( run("st_geomfromwkb", &[Value::Bytes(ewkb.clone())]), @@ -349,59 +451,70 @@ mod tests { "st_geomfromwkb", &[Value::Bytes(ewkb), Value::Integer(3376)] ), - Some(projected(Srid::BorneoRso, Geometry::point(1.0, 2.0))) - ); - } - - #[test] - fn malformed_hex_yields_null() { - assert_eq!( - run("st_geomfromwkb", &[Value::String("zz".into())]), - Some(Value::Null) - ); - assert_eq!( - run("st_geomfromwkb", &[Value::String("010".into())]), - Some(Value::Null) + Some(projected(srid(3376), Geometry::point(1.0, 2.0))) ); } + /// A byte literal and its hex-string spelling must decode identically. #[test] - fn malformed_text_yields_null() { - let bad = vec![Value::String("POINT(".into())]; - assert_eq!(run("st_geomfromtext", &bad), Some(Value::Null)); - } - - /// A dropped vertex would silently shorten the line; the call must fail - /// whole instead. - #[test] - fn makeline_rejects_a_non_point_argument() { - let args = vec![ - wgs84(Geometry::point(0.0, 0.0)), - Value::String("not a geometry".into()), - ]; - assert_eq!(run("st_makeline", &args), Some(Value::Null)); + fn geomfromwkb_accepts_bytes_and_hex_string() { + let hex = "0101000000000000000000F03F0000000000000040"; + let bytes = decode_hex(hex).expect("test vector must decode"); + let from_bytes = run("st_geomfromwkb", &[Value::Bytes(bytes)]); + let from_hex = run("st_geomfromwkb", &[Value::String(hex.into())]); + assert_eq!(from_bytes, from_hex); + assert_eq!(from_bytes, Some(wgs84(Geometry::point(1.0, 2.0)))); } #[test] - fn makeline_needs_at_least_two_points() { - let one = vec![wgs84(Geometry::point(0.0, 0.0))]; - assert_eq!(run("st_makeline", &one), Some(Value::Null)); + fn malformed_wkb_fails() { + assert!(matches!( + eval("st_geomfromwkb", &[Value::String("zz".into())]), + Some(Err(EvalError::ArgumentValue { .. })) + )); + assert!(matches!( + eval("st_geomfromwkb", &[Value::String("0101".into())]), + Some(Err(EvalError::Geometry { .. })) + )); } #[test] - fn makeline_rejects_mixed_srids() { - let args = vec![ - wgs84(Geometry::point(0.0, 0.0)), - projected(Srid::Utm48N, Geometry::point(1.0, 1.0)), - ]; - assert_eq!( - eval("st_makeline", &args), - Some(Err(EvalError::MixedSrid { - function: "st_makeline", - left: 4326, - right: 32648 - })) - ); + fn makeline_keeps_z_and_rejects_mixed_input() { + let pz = |x: f64, z: f64| { + Value::Geometry( + SridGeometry::with_ordinates( + Srid::WGS84, + Geometry::point(x, 0.0), + Ordinates::new(Some(vec![z]), None), + ) + .unwrap(), + ) + }; + let Some(Value::Geometry(line)) = run("st_makeline", &[pz(0.0, 1.0), pz(1.0, 2.0)]) else { + panic!(); + }; + assert_eq!(line.ordinates().z(), Some(&[1.0, 2.0][..])); + assert!(matches!( + eval( + "st_makeline", + &[pz(0.0, 1.0), wgs84(Geometry::point(1.0, 1.0))] + ), + Some(Err(EvalError::Geometry { .. })) + )); + assert!(matches!( + eval( + "st_makeline", + &[ + wgs84(Geometry::point(0.0, 0.0)), + projected(srid(32648), Geometry::point(1.0, 1.0)) + ] + ), + Some(Err(EvalError::MixedSrid { .. })) + )); + assert!(matches!( + eval("st_makeline", &[wgs84(Geometry::point(0.0, 0.0))]), + Some(Err(EvalError::ArgumentValue { .. })) + )); } #[test] @@ -412,81 +525,47 @@ mod tests { Value::Float(1.0), Value::Float(1.0), ]; - let Some(Value::Geometry(SridGeometry { - geometry: Geometry::Polygon { coordinates }, - .. - })) = run("st_makeenvelope", &args) - else { + let Some(Value::Geometry(g)) = run("st_makeenvelope", &args) else { + panic!("expected a polygon"); + }; + let Geometry::Polygon { coordinates } = g.geometry() else { panic!("expected a polygon"); }; let ring = &coordinates[0]; assert_eq!(ring.first(), ring.last(), "envelope ring must be closed"); } - /// A caller asking for a degenerate segment count must get the default, - /// not a geometry that encloses nothing. - #[test] - fn buffer_segment_count_is_floored() { - let args = vec![ - wgs84(Geometry::point(0.0, 0.0)), - Value::Float(1000.0), - Value::Integer(0), - ]; - let Some(Value::Geometry(SridGeometry { - geometry: Geometry::Polygon { coordinates }, - .. - })) = run("st_buffer", &args) - else { - panic!("expected a buffered polygon"); - }; - assert!( - coordinates[0].len() > 3, - "a zero segment count must fall back to the default" - ); - } - #[test] - fn projected_buffer_is_planar_and_keeps_srid() { - let args = vec![ - projected(Srid::Utm48N, Geometry::point(500_000.0, 100_000.0)), - Value::Float(100.0), - ]; - let Some(Value::Geometry(g)) = run("st_buffer", &args) else { - panic!("expected a geometry"); - }; - assert_eq!(g.srid, Srid::Utm48N); - let Geometry::Polygon { coordinates } = &g.geometry else { - panic!("expected a polygon"); + fn tile_envelope_is_web_mercator() { + let Some(Value::Geometry(g)) = run( + "st_tileenvelope", + &[Value::Integer(0), Value::Integer(0), Value::Integer(0)], + ) else { + panic!(); }; - let [x, y] = coordinates[0][0]; - assert!(((x - 500_000.0).hypot(y - 100_000.0) - 100.0).abs() < 1e-6); - } - - #[test] - fn union_rejects_mixed_srids() { - let args = vec![ - wgs84(Geometry::point(0.0, 0.0)), - projected(Srid::PeninsularRso, Geometry::point(1.0, 1.0)), - ]; + assert_eq!(g.srid, srid(3857)); assert!(matches!( - eval("st_union", &args), - Some(Err(EvalError::MixedSrid { .. })) + eval( + "st_tileenvelope", + &[Value::Integer(1), Value::Integer(5), Value::Integer(0)] + ), + Some(Err(EvalError::Geometry { .. })) )); } #[test] - fn centroid_keeps_srid() { - let args = vec![projected(Srid::BorneoRso, Geometry::point(5.0, 6.0))]; - assert_eq!( - run("st_centroid", &args), - Some(projected(Srid::BorneoRso, Geometry::point(5.0, 6.0))) - ); - } - - #[test] - fn centroid_of_unreadable_geometry_is_null() { - let bad = vec![Value::String("not a geometry".into())]; - assert_eq!(run("st_centroid", &bad), Some(Value::Null)); + fn circle_is_geodesic() { + let Some(Value::Geometry(g)) = run( + "geo_circle", + &[Value::Float(0.0), Value::Float(0.0), Value::Float(1000.0)], + ) else { + panic!(); + }; + let m = nodedb_spatial::Metric::wgs84(); + for c in g.geometry().coords() { + let d = m.segment_length([0.0, 0.0], c); + assert!((d - 1000.0).abs() < 10.0, "{d}"); + } } #[test] diff --git a/nodedb-query/src/geo_functions/crs.rs b/nodedb-query/src/geo_functions/crs.rs index 825c4417a..cff094a01 100644 --- a/nodedb-query/src/geo_functions/crs.rs +++ b/nodedb-query/src/geo_functions/crs.rs @@ -1,35 +1,43 @@ // SPDX-License-Identifier: Apache-2.0 //! Spatial reference system functions: read, set, and transform a -//! geometry's SRID. +//! geometry's SRID, and shift its longitudes across the antimeridian. -use nodedb_spatial::CrsError; +use nodedb_crs::{CoordinateEpoch, CrsContext}; use nodedb_types::Value; -use super::helpers::{GeoEval, SridArg, geom_arg, srid_arg}; +use super::helpers::{GeoEval, SridArg, finite_arg, geom_arg, srid_arg}; use crate::expr::EvalError; -pub(super) fn eval(canonical: &'static str, args: &[Value]) -> GeoEval { +pub(super) fn eval(canonical: &'static str, args: &[Value], ctx: &CrsContext<'_>) -> GeoEval { let result = match canonical { - "st_srid" => Ok(match geom_arg(args, 0) { + "st_srid" => geom_arg(args, 0, canonical, ctx).map(|g| match g { Some(g) => Value::Integer(i64::from(g.srid.code())), None => Value::Null, }), - "st_setsrid" => set_srid(canonical, args), - "st_transform" => transform(canonical, args), + "st_setsrid" => set_srid(canonical, args, ctx), + "st_transform" => transform(canonical, args, ctx), + "st_shiftlongitude" => geom_arg(args, 0, canonical, ctx).map(|g| match g { + Some(g) => Value::Geometry(nodedb_spatial::ops::shift_longitude(&g)), + None => Value::Null, + }), _ => return None, }; Some(result) } /// `ST_SetSRID(geom, srid)`: relabel the geometry's SRID. Coordinates are -/// kept as they are. -fn set_srid(function: &'static str, args: &[Value]) -> Result { - let srid = match srid_arg(args, 1, function)? { +/// kept as they are. The SRID must be defined. +fn set_srid( + function: &'static str, + args: &[Value], + ctx: &CrsContext<'_>, +) -> Result { + let srid = match srid_arg(args, 1, function, ctx)? { SridArg::Srid(srid) => srid, SridArg::Absent | SridArg::Null => return Ok(Value::Null), }; - Ok(match geom_arg(args, 0) { + Ok(match geom_arg(args, 0, function, ctx)? { Some(mut g) => { g.srid = srid; Value::Geometry(g) @@ -38,118 +46,277 @@ fn set_srid(function: &'static str, args: &[Value]) -> Result }) } -/// `ST_Transform(geom, srid)`: reproject every coordinate into `srid`. -fn transform(function: &'static str, args: &[Value]) -> Result { - let target = match srid_arg(args, 1, function)? { +/// Position of the optional coordinate-epoch argument of `ST_Transform`. +const EPOCH_ARG: usize = 2; + +/// `ST_Transform(geom, srid [, epoch])`: reproject every coordinate into +/// `srid`. Z is ellipsoidal height and goes through the datum shift. M +/// passes through. +/// +/// `epoch` is the coordinate epoch as a decimal year in 1900..=2200. A +/// time-dependent datum shift takes its parameters at that epoch. Without +/// it, each shift runs at its own parameter reference epoch. A NULL epoch +/// gives NULL. +fn transform( + function: &'static str, + args: &[Value], + ctx: &CrsContext<'_>, +) -> Result { + let target = match srid_arg(args, 1, function, ctx)? { SridArg::Srid(srid) => srid, SridArg::Absent | SridArg::Null => return Ok(Value::Null), }; - let Some(g) = geom_arg(args, 0) else { + let epoch = match epoch_arg(args, function)? { + EpochArg::Absent => None, + EpochArg::Null => return Ok(Value::Null), + EpochArg::Epoch(epoch) => Some(epoch), + }; + let Some(g) = geom_arg(args, 0, function, ctx)? else { return Ok(Value::Null); }; - nodedb_spatial::transform_geometry(&g, target) - .map(Value::Geometry) - .map_err(|e| projection_fault(function, e)) + let out = match epoch { + None => nodedb_spatial::transform_geometry(&g, target, ctx), + Some(epoch) => nodedb_spatial::transform_geometry_at(&g, target, epoch, ctx), + }; + out.map(Value::Geometry) + .map_err(|e| EvalError::from_crs(function, e)) +} + +/// The optional coordinate-epoch argument of `ST_Transform`. +enum EpochArg { + Absent, + Null, + Epoch(CoordinateEpoch), } -/// A projection that cannot map a coordinate fails the statement. -pub(super) fn projection_fault(function: &'static str, e: CrsError) -> EvalError { - EvalError::ProjectionDomain { - function, - srid: e.srid().code(), +/// Read the epoch argument. A non-number fails with +/// [`EvalError::ArgumentType`]. A non-finite or out-of-range year fails +/// with [`EvalError::ArgumentValue`]. +fn epoch_arg(args: &[Value], function: &'static str) -> Result { + if args.get(EPOCH_ARG).is_none() { + return Ok(EpochArg::Absent); } + let Some(year) = finite_arg(args, EPOCH_ARG, function)? else { + return Ok(EpochArg::Null); + }; + CoordinateEpoch::new(year) + .map(EpochArg::Epoch) + .map_err(|e| EvalError::ArgumentValue { + function, + position: EPOCH_ARG + 1, + reason: format!("is out of range: {e}"), + }) } #[cfg(test)] mod tests { use super::*; + use nodedb_crs::{CrsError, UnknownSrid}; use nodedb_types::geometry::{Geometry, Srid, SridGeometry}; + fn srid(code: u32) -> Srid { + Srid::new(code).unwrap() + } + fn point(srid: Srid, x: f64, y: f64) -> Value { Value::Geometry(SridGeometry::new(srid, Geometry::point(x, y))) } + fn run(canonical: &'static str, args: &[Value]) -> GeoEval { + eval(canonical, args, &CrsContext::builtin_only()) + } + #[test] fn srid_reads_the_geometry_srid() { assert_eq!( - eval("st_srid", &[point(Srid::Wgs84, 0.0, 0.0)]), + run("st_srid", &[point(Srid::WGS84, 0.0, 0.0)]), Some(Ok(Value::Integer(4326))) ); assert_eq!( - eval("st_srid", &[point(Srid::BorneoRso, 0.0, 0.0)]), + run("st_srid", &[point(srid(3376), 0.0, 0.0)]), Some(Ok(Value::Integer(3376))) ); - assert_eq!( - eval("st_srid", &[Value::String("not a geometry".into())]), - Some(Ok(Value::Null)) - ); + assert!(matches!( + run("st_srid", &[Value::String("not a geometry".into())]), + Some(Err(EvalError::Geometry { .. })) + )); } #[test] fn setsrid_relabels_without_moving() { - let got = eval( + let got = run( "st_setsrid", - &[point(Srid::Wgs84, 1.0, 2.0), Value::Integer(32648)], + &[point(Srid::WGS84, 1.0, 2.0), Value::Integer(4742)], ); - assert_eq!(got, Some(Ok(point(Srid::Utm48N, 1.0, 2.0)))); + assert_eq!(got, Some(Ok(point(srid(4742), 1.0, 2.0)))); } + /// PROJ 9.8.1 reference: EPSG:4326 (99.828, 3.975) to EPSG:32647. + const UTM47_X: f64 = 591_918.695_587_182_4; + const UTM47_Y: f64 = 439_410.048_656_111_64; + #[test] - fn transform_projects_klcc_into_utm47n() { - let Some(Ok(Value::Geometry(g))) = eval( + fn transform_matches_proj_for_utm47n() { + let Some(Ok(Value::Geometry(g))) = run( "st_transform", - &[point(Srid::Wgs84, 101.7117, 3.1579), Value::Integer(32647)], + &[point(Srid::WGS84, 99.828, 3.975), Value::Integer(32647)], ) else { panic!("expected a geometry"); }; - assert_eq!(g.srid, Srid::Utm47N); - let Geometry::Point { coordinates } = g.geometry else { + assert_eq!(g.srid, srid(32647)); + let Geometry::Point { coordinates } = *g.geometry() else { panic!("expected a point"); }; - assert!((coordinates[0] - 801_401.794_9).abs() < 1e-3); - assert!((coordinates[1] - 349_439.510_6).abs() < 1e-3); + assert!((coordinates[0] - UTM47_X).abs() < 1e-3, "{coordinates:?}"); + assert!((coordinates[1] - UTM47_Y).abs() < 1e-3, "{coordinates:?}"); } + fn coords(v: Value) -> [f64; 2] { + let Value::Geometry(g) = v else { + panic!("expected a geometry"); + }; + let Geometry::Point { coordinates } = *g.geometry() else { + panic!("expected a point"); + }; + coordinates + } + + /// An epoch changes nothing on a static datum shift: EPSG:4742 to + /// EPSG:32647 gives the same point with and without it. #[test] - fn transform_to_unsupported_srid_names_every_supported_code() { - let Some(Err(err)) = eval( + fn transform_with_epoch_matches_without_for_static_shifts() { + let input = point(srid(4742), 99.828, 3.975); + let plain = run("st_transform", &[input.clone(), Value::Integer(32647)]); + let at = run( "st_transform", - &[point(Srid::Wgs84, 101.7, 3.1), Value::Integer(2154)], - ) else { + &[input, Value::Integer(32647), Value::Float(2025.5)], + ); + let (Some(Ok(plain)), Some(Ok(at))) = (plain, at) else { + panic!("expected two geometries"); + }; + assert_eq!(coords(plain), coords(at)); + } + + #[test] + fn transform_epoch_out_of_range_is_an_argument_fault() { + for epoch in [ + Value::Float(1899.5), + Value::Integer(2201), + Value::Float(f64::NAN), + ] { + let got = run( + "st_transform", + &[ + point(Srid::WGS84, 99.828, 3.975), + Value::Integer(32647), + epoch, + ], + ); + assert!( + matches!( + got, + Some(Err(EvalError::ArgumentValue { + function: "st_transform", + position: 3, + .. + })) + ), + "{got:?}" + ); + } + let got = run( + "st_transform", + &[ + point(Srid::WGS84, 99.828, 3.975), + Value::Integer(32647), + Value::String("soon".into()), + ], + ); + assert!(matches!( + got, + Some(Err(EvalError::ArgumentType { position: 3, .. })) + )); + } + + #[test] + fn transform_null_epoch_yields_null() { + let got = run( + "st_transform", + &[ + point(Srid::WGS84, 99.828, 3.975), + Value::Integer(32647), + Value::Null, + ], + ); + assert_eq!(got, Some(Ok(Value::Null))); + } + + #[test] + fn transform_keeps_z_and_m() { + use nodedb_types::geometry::Ordinates; + let g = SridGeometry::with_ordinates( + Srid::WGS84, + Geometry::point(99.828, 3.975), + Ordinates::new(Some(vec![50.0]), Some(vec![7.0])), + ) + .unwrap(); + let Some(Ok(Value::Geometry(out))) = + run("st_transform", &[Value::Geometry(g), Value::Integer(32647)]) + else { + panic!("expected a geometry"); + }; + assert_eq!(out.ordinates().z(), Some(&[50.0][..])); + assert_eq!(out.ordinates().m(), Some(&[7.0][..])); + } + + #[test] + fn shift_longitude_moves_negative_longitudes() { + let Some(Ok(Value::Geometry(out))) = + run("st_shiftlongitude", &[point(Srid::WGS84, -170.0, 1.0)]) + else { + panic!(); + }; + assert_eq!(out.geometry(), &Geometry::point(190.0, 1.0)); + } + + #[test] + fn transform_to_undefined_srid_fails() { + let got = run( + "st_transform", + &[point(Srid::WGS84, 101.7, 3.1), Value::Integer(999)], + ); + let Some(Err(err)) = got else { panic!("expected an error"); }; assert_eq!( err, - EvalError::UnsupportedSrid { + EvalError::Crs { function: "st_transform", - srid: 2154 + source: UnknownSrid::Undefined { code: 999 } } ); - let text = err.to_string(); - for code in ["4326", "32647", "32648", "3375", "3376"] { - assert!(text.contains(code), "{text}"); - } + assert!(err.to_string().contains("999"), "{err}"); } #[test] - fn transform_out_of_domain_is_a_projection_fault() { - let got = eval( + fn transform_out_of_domain_is_a_transform_fault() { + let got = run( "st_transform", - &[point(Srid::Wgs84, 179.0, 3.0), Value::Integer(32648)], + &[point(Srid::WGS84, 179.0, 3.0), Value::Integer(32648)], ); - assert_eq!( + assert!(matches!( got, - Some(Err(EvalError::ProjectionDomain { + Some(Err(EvalError::Transform { function: "st_transform", - srid: 32648 + source: CrsError::OutOfDomain { .. } })) - ); + )); } #[test] fn null_srid_yields_null() { assert_eq!( - eval("st_setsrid", &[point(Srid::Wgs84, 1.0, 2.0), Value::Null]), + run("st_setsrid", &[point(Srid::WGS84, 1.0, 2.0), Value::Null]), Some(Ok(Value::Null)) ); } diff --git a/nodedb-query/src/geo_functions/dispatch.rs b/nodedb-query/src/geo_functions/dispatch.rs index a1ee8935c..95f4acb3f 100644 --- a/nodedb-query/src/geo_functions/dispatch.rs +++ b/nodedb-query/src/geo_functions/dispatch.rs @@ -2,10 +2,13 @@ //! Entry point for geospatial function evaluation. +use nodedb_crs::CrsContext; use nodedb_types::Value; use super::catalog; -use super::{accessors, constructors, crs, indexing, measures, predicates}; +use super::{ + accessors, constructors, crs, encode, indexing, linear, measures, overlay, predicates, +}; use crate::expr::EvalError; /// Evaluate a geospatial function. @@ -16,11 +19,15 @@ use crate::expr::EvalError; /// /// Returns `None` only when `name` is not a geospatial function at all, so the /// caller can fall through to its own function table. `Some(Err(_))` is a -/// fault that fails the statement: an unsupported SRID, operands in different -/// SRIDs, or a coordinate outside a projection's domain. -pub fn eval_geo_function(name: &str, args: &[Value]) -> Option> { +/// fault that fails the statement: an SRID `ctx` does not define, operands +/// in different SRIDs, or a coordinate outside a projection's domain. +pub fn eval_geo_function( + name: &str, + args: &[Value], + ctx: &CrsContext<'_>, +) -> Option> { let spec = catalog::lookup(name)?; - eval_canonical(spec.canonical, args) + eval_canonical(spec.canonical, args, ctx) } /// Route a canonical name to its family evaluator. @@ -28,12 +35,19 @@ pub fn eval_geo_function(name: &str, args: &[Value]) -> Option Option> { - predicates::eval(canonical, args) - .or_else(|| measures::eval(canonical, args)) - .or_else(|| accessors::eval(canonical, args)) - .or_else(|| crs::eval(canonical, args)) - .or_else(|| constructors::eval(canonical, args)) +fn eval_canonical( + canonical: &'static str, + args: &[Value], + ctx: &CrsContext<'_>, +) -> Option> { + predicates::eval(canonical, args, ctx) + .or_else(|| measures::eval(canonical, args, ctx)) + .or_else(|| accessors::eval(canonical, args, ctx)) + .or_else(|| encode::eval(canonical, args, ctx)) + .or_else(|| crs::eval(canonical, args, ctx)) + .or_else(|| constructors::eval(canonical, args, ctx)) + .or_else(|| overlay::eval(canonical, args, ctx)) + .or_else(|| linear::eval(canonical, args, ctx)) .or_else(|| indexing::eval(canonical, args)) } @@ -42,6 +56,10 @@ mod tests { use super::*; use nodedb_types::geometry::{Geometry, SridGeometry}; + fn ctx() -> CrsContext<'static> { + CrsContext::builtin_only() + } + /// Arguments that satisfy any catalog shape: enough geometry-shaped and /// number-shaped values that no evaluator indexes past the end. fn probe_args() -> Vec { @@ -61,7 +79,7 @@ mod tests { fn every_catalog_entry_is_evaluable() { for spec in catalog::GEO_FUNCTIONS { assert!( - eval_canonical(spec.canonical, &probe_args()).is_some(), + eval_canonical(spec.canonical, &probe_args(), &ctx()).is_some(), "catalog declares '{}' but no evaluator handles it", spec.canonical ); @@ -73,10 +91,10 @@ mod tests { #[test] fn every_alias_evaluates_identically_to_its_canonical_name() { for spec in catalog::GEO_FUNCTIONS { - let canonical = eval_geo_function(spec.canonical, &probe_args()); + let canonical = eval_geo_function(spec.canonical, &probe_args(), &ctx()); for alias in spec.aliases { assert_eq!( - eval_geo_function(alias, &probe_args()), + eval_geo_function(alias, &probe_args(), &ctx()), canonical, "'{alias}' must behave exactly like '{}'", spec.canonical @@ -90,14 +108,14 @@ mod tests { fn dispatch_is_case_insensitive() { let args = probe_args(); assert_eq!( - eval_geo_function("ST_AsText", &args), - eval_geo_function("st_astext", &args) + eval_geo_function("ST_AsText", &args, &ctx()), + eval_geo_function("st_astext", &args, &ctx()) ); } #[test] fn non_geo_function_falls_through() { - assert_eq!(eval_geo_function("lower", &probe_args()), None); + assert_eq!(eval_geo_function("lower", &probe_args(), &ctx()), None); } /// The reported bug in one line: the standard accessor spelling must read @@ -109,15 +127,15 @@ mod tests { 1.0, 2.0, )))]; assert_eq!( - eval_geo_function("st_x", &point), + eval_geo_function("st_x", &point, &ctx()), Some(Ok(to_value_number(1.0))) ); assert_eq!( - eval_geo_function("st_y", &point), + eval_geo_function("st_y", &point, &ctx()), Some(Ok(to_value_number(2.0))) ); assert!(matches!( - eval_geo_function("st_astext", &point), + eval_geo_function("st_astext", &point, &ctx()), Some(Ok(Value::String(_))) )); } @@ -126,16 +144,17 @@ mod tests { #[test] fn setsrid_then_srid_round_trips() { let Some(Ok(point)) = - eval_geo_function("st_point", &[Value::Float(1.0), Value::Float(2.0)]) + eval_geo_function("st_point", &[Value::Float(1.0), Value::Float(2.0)], &ctx()) else { panic!("expected a point"); }; - let Some(Ok(tagged)) = eval_geo_function("ST_SetSRID", &[point, Value::Integer(32648)]) + let Some(Ok(tagged)) = + eval_geo_function("ST_SetSRID", &[point, Value::Integer(32648)], &ctx()) else { panic!("expected a geometry"); }; assert_eq!( - eval_geo_function("ST_SRID", &[tagged]), + eval_geo_function("ST_SRID", &[tagged], &ctx()), Some(Ok(Value::Integer(32648))) ); } diff --git a/nodedb-query/src/geo_functions/encode.rs b/nodedb-query/src/geo_functions/encode.rs new file mode 100644 index 000000000..2c3fb7536 --- /dev/null +++ b/nodedb-query/src/geo_functions/encode.rs @@ -0,0 +1,153 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Geometry encodings: WKT, EWKT, GeoJSON, WKB and EWKB. + +use nodedb_crs::CrsContext; +use nodedb_spatial::ByteOrder; +use nodedb_types::Value; + +use super::helpers::{GeoEval, geom_arg, geometry_fault, str_arg}; +use crate::expr::EvalError; + +pub(super) fn eval(canonical: &'static str, args: &[Value], ctx: &CrsContext<'_>) -> GeoEval { + let result = + match canonical { + // WKT stays plain: it has no SRID slot. + "st_astext" => text(canonical, args, ctx, nodedb_spatial::geometry_to_wkt), + "st_asewkt" => text(canonical, args, ctx, nodedb_spatial::geometry_to_ewkt), + // GeoJSON carries the `crs` member for any SRID other than 4326, + // Z as a third number, and no M. + "st_asgeojson" => geom_arg(args, 0, canonical, ctx).and_then(|g| match g { + Some(g) => sonic_rs::to_string(&g).map(Value::String).map_err(|e| { + EvalError::ArgumentValue { + function: canonical, + position: 1, + reason: format!("does not encode as GeoJSON: {e}"), + } + }), + None => Ok(Value::Null), + }), + "st_asbinary" => binary( + canonical, + args, + ctx, + nodedb_spatial::geometry_to_wkb_ordered, + ), + "st_asewkb" => binary( + canonical, + args, + ctx, + nodedb_spatial::geometry_to_ewkb_ordered, + ), + _ => return None, + }; + Some(result) +} + +fn text( + function: &'static str, + args: &[Value], + ctx: &CrsContext<'_>, + f: fn(&nodedb_types::geometry::SridGeometry) -> String, +) -> Result { + Ok(match geom_arg(args, 0, function, ctx)? { + Some(g) => Value::String(f(&g)), + None => Value::Null, + }) +} + +type Encoder = fn( + &nodedb_types::geometry::SridGeometry, + ByteOrder, +) -> Result, nodedb_spatial::WkbError>; + +/// `ST_AsBinary(geom [, 'NDR' | 'XDR'])`: little-endian unless `'XDR'`. +fn binary( + function: &'static str, + args: &[Value], + ctx: &CrsContext<'_>, + encode: Encoder, +) -> Result { + let order = match str_arg(args, 1) { + None => ByteOrder::LittleEndian, + Some(s) if s.eq_ignore_ascii_case("NDR") => ByteOrder::LittleEndian, + Some(s) if s.eq_ignore_ascii_case("XDR") => ByteOrder::BigEndian, + Some(_) => { + return Err(EvalError::ArgumentValue { + function, + position: 2, + reason: "must be 'NDR' or 'XDR'".into(), + }); + } + }; + let Some(g) = geom_arg(args, 0, function, ctx)? else { + return Ok(Value::Null); + }; + encode(&g, order) + .map(Value::Bytes) + .map_err(|e| geometry_fault(function, e.into())) +} + +#[cfg(test)] +mod tests { + use super::*; + use nodedb_types::geometry::{Geometry, Ordinates, Srid, SridGeometry}; + + fn ctx() -> CrsContext<'static> { + CrsContext::builtin_only() + } + + fn run(canonical: &'static str, args: &[Value]) -> Value { + eval(canonical, args, &ctx()) + .expect("geo name") + .expect("no fault expected") + } + + fn zm() -> Value { + Value::Geometry( + SridGeometry::with_ordinates( + Srid::new(32648).unwrap(), + Geometry::point(1.0, 2.0), + Ordinates::new(Some(vec![3.0]), Some(vec![4.0])), + ) + .unwrap(), + ) + } + + #[test] + fn text_forms_carry_z_and_m() { + assert_eq!( + run("st_astext", &[zm()]), + Value::String("POINT ZM (1 2 3 4)".into()) + ); + assert_eq!( + run("st_asewkt", &[zm()]), + Value::String("SRID=32648;POINT ZM (1 2 3 4)".into()) + ); + let Value::String(json) = run("st_asgeojson", &[zm()]) else { + panic!(); + }; + assert!(json.contains("[1.0,2.0,3.0]"), "{json}"); + } + + #[test] + fn binary_forms_round_trip() { + let Value::Bytes(wkb) = run("st_asbinary", &[zm()]) else { + panic!(); + }; + assert_eq!(u32::from_le_bytes(wkb[1..5].try_into().unwrap()), 3001); + let Value::Bytes(be) = run("st_asbinary", &[zm(), Value::String("XDR".into())]) else { + panic!(); + }; + assert_eq!(be[0], 0); + let Value::Bytes(ewkb) = run("st_asewkb", &[zm()]) else { + panic!(); + }; + let back = nodedb_spatial::geometry_from_wkb(&ewkb).unwrap(); + assert_eq!(back.srid, Some(32648)); + assert!(matches!( + eval("st_asbinary", &[zm(), Value::String("bad".into())], &ctx()), + Some(Err(EvalError::ArgumentValue { .. })) + )); + } +} diff --git a/nodedb-query/src/geo_functions/helpers.rs b/nodedb-query/src/geo_functions/helpers.rs index 06c7687e1..6f5574ff7 100644 --- a/nodedb-query/src/geo_functions/helpers.rs +++ b/nodedb-query/src/geo_functions/helpers.rs @@ -4,8 +4,10 @@ use crate::expr::EvalError; use crate::value_ops::value_to_f64; -use nodedb_types::Value; -use nodedb_types::geometry::{GeoJsonError, Srid, SridGeometry, parse_geojson}; +use nodedb_crs::CrsContext; +use nodedb_spatial::{Metric, SpatialError, SridCheck}; +use nodedb_types::geometry::{GeoJsonError, Srid, SridGeometry, geojson_object}; +use nodedb_types::{BboxSpace, Value}; /// What a geo family evaluator returns: `None` when the canonical name is /// not its own, else the call's value or the fault that fails the statement. @@ -19,47 +21,137 @@ pub(super) fn num_arg(args: &[Value], idx: usize) -> Option { args.get(idx).and_then(|v| value_to_f64(v, true)) } +/// A numeric argument that must be present and finite. NULL gives +/// `Ok(None)`. A non-number fails with [`EvalError::ArgumentType`]. +pub(super) fn finite_arg( + args: &[Value], + idx: usize, + function: &'static str, +) -> Result, EvalError> { + match args.get(idx) { + None | Some(Value::Null) => Ok(None), + Some(v) => match value_to_f64(v, true) { + Some(n) if n.is_finite() => Ok(Some(n)), + Some(_) => Err(EvalError::ArgumentValue { + function, + position: idx + 1, + reason: "must be a finite number".into(), + }), + None => Err(EvalError::ArgumentType { + function, + position: idx + 1, + expected: "a number", + got: v.type_name(), + }), + }, + } +} + +/// The fault a geometry parse or operation in `function` raises. +pub(super) fn geometry_fault(function: &'static str, source: SpatialError) -> EvalError { + match source { + SpatialError::Srid(source) => EvalError::Crs { function, source }, + SpatialError::GeoJson(GeoJsonError::Srid(source)) => EvalError::Crs { function, source }, + SpatialError::Crs(e) => EvalError::from_crs(function, e), + source => EvalError::Geometry { function, source }, + } +} + /// Extract a geometry argument. /// -/// Accepts every representation geometry takes on its way through the system: -/// a native `Value::Geometry`, a GeoJSON object decoded from a document, a -/// GeoJSON string (the form a constant-folded geometry is stored and passed -/// as), and a WKT string. Accepting WKT here is what makes a bare -/// `'POINT(1 2)'` literal usable wherever a geometry is expected, matching -/// PostGIS's treatment of an unknown-typed literal in geometry position. -/// GeoJSON keeps the SRID its `crs` member names. WKT is WGS 84. -pub(super) fn geom_arg(args: &[Value], idx: usize) -> Option { - match args.get(idx)? { - Value::Geometry(g) => Some(g.clone()), - Value::String(s) => geometry_from_text(s), - obj @ Value::Object(_) => SridGeometry::from_value(obj), - _ => None, +/// Accepts every representation geometry takes on its way through the +/// system: a native `Value::Geometry`, a GeoJSON object decoded from a +/// document, text (GeoJSON, EWKT, WKT or hex EWKB, matching PostGIS's +/// treatment of an unknown-typed literal in geometry position), and WKB / +/// EWKB bytes. GeoJSON and EWKT keep the SRID they name. WKT and ISO WKB +/// are WGS 84. +/// +/// SQL NULL and an object that is not GeoJSON give `Ok(None)`. Text or +/// bytes that do not parse fail with [`EvalError::Geometry`]. An SRID `ctx` +/// does not define fails with [`EvalError::Crs`]. +pub(super) fn geom_arg( + args: &[Value], + idx: usize, + function: &'static str, + ctx: &CrsContext<'_>, +) -> Result, EvalError> { + match args.get(idx) { + Some(Value::Geometry(g)) => Ok(Some(g.clone())), + Some(Value::String(s)) => text_geometry(function, s, ctx).map(Some), + Some(Value::Bytes(b)) => nodedb_spatial::geometry_from_bytes(b, SridCheck::Resolve(ctx)) + .map(Some) + .map_err(|e| geometry_fault(function, e)), + Some(obj @ Value::Object(_)) => match geojson_object(obj, ctx) { + Ok(g) => Ok(Some(g)), + Err(GeoJsonError::NotGeometry) => Ok(None), + Err(GeoJsonError::Srid(source)) => Err(EvalError::Crs { function, source }), + Err(e) => Err(EvalError::Geometry { + function, + source: SpatialError::GeoJson(e), + }), + }, + _ => Ok(None), } } -/// Parse a textual geometry: GeoJSON first, then WKT. -/// -/// The two are unambiguous — GeoJSON always starts with `{`, WKT with a type -/// keyword — so trying them in order cannot misread one as the other. -pub fn geometry_from_text(text: &str) -> Option { - parse_text_geometry(text).ok() -} - -/// [`geometry_from_text`] that reports why a text is not a geometry. A -/// GeoJSON `crs` naming an unsupported SRID gives -/// [`GeoJsonError::UnsupportedSrid`]. Any other unreadable text gives -/// [`GeoJsonError::Malformed`]. -pub(super) fn parse_text_geometry(text: &str) -> Result { - let trimmed = text.trim(); - if trimmed.starts_with('{') { - parse_geojson(trimmed) - } else { - nodedb_spatial::geometry_from_wkt(trimmed) - .map(SridGeometry::wgs84) - .ok_or(GeoJsonError::Malformed) +/// Two geometry arguments at positions 0 and 1. `None` when either is not +/// a geometry. +pub(super) fn geom_pair( + args: &[Value], + function: &'static str, + ctx: &CrsContext<'_>, +) -> Result, EvalError> { + let a = geom_arg(args, 0, function, ctx)?; + let b = geom_arg(args, 1, function, ctx)?; + Ok(a.zip(b)) +} + +/// The value `f` gives for the geometry at position 0. A missing geometry +/// is NULL. +pub(super) fn unary( + function: &'static str, + args: &[Value], + ctx: &CrsContext<'_>, + f: impl FnOnce(&SridGeometry) -> Result, +) -> Result { + match geom_arg(args, 0, function, ctx)? { + Some(g) => f(&g), + None => Ok(Value::Null), } } +/// The value `f` gives for the geometries at positions 0 and 1. A missing +/// geometry is NULL. Two SRIDs fail with `EvalError::MixedSrid`. +pub(super) fn pair( + function: &'static str, + args: &[Value], + ctx: &CrsContext<'_>, + f: impl FnOnce(&SridGeometry, &SridGeometry) -> Result, +) -> Result { + let Some((a, b)) = geom_pair(args, function, ctx)? else { + return Ok(Value::Null); + }; + same_srid(function, &a, &b)?; + f(&a, &b) +} + +/// Parse a textual geometry: GeoJSON, EWKT, WKT or hex EWKB. `None` for +/// text that is not a geometry or names an SRID `ctx` does not define. +pub fn geometry_from_text(text: &str, ctx: &CrsContext<'_>) -> Option { + nodedb_spatial::geometry_from_text(text, SridCheck::Resolve(ctx)).ok() +} + +/// A textual geometry argument. Text that does not parse fails the +/// statement. +pub(super) fn text_geometry( + function: &'static str, + text: &str, + ctx: &CrsContext<'_>, +) -> Result { + nodedb_spatial::geometry_from_text(text, SridCheck::Resolve(ctx)) + .map_err(|e| geometry_fault(function, e)) +} + /// An optional SRID argument. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(super) enum SridArg { @@ -73,13 +165,14 @@ pub(super) enum SridArg { /// Read the SRID argument at `idx`. /// /// An integer, an integral float, or integer text resolves through -/// [`Srid::from_code`]. A code outside the supported set fails with -/// [`EvalError::UnsupportedSrid`]. A value of any other kind fails with +/// [`CrsContext::resolve_code`]. A code no CRS defines fails with +/// [`EvalError::Crs`]. A value of any other kind fails with /// [`EvalError::ArgumentType`]. pub(super) fn srid_arg( args: &[Value], idx: usize, function: &'static str, + ctx: &CrsContext<'_>, ) -> Result { let Some(value) = args.get(idx) else { return Ok(SridArg::Absent); @@ -99,12 +192,9 @@ pub(super) fn srid_arg( got: value.type_name(), }); }; - Srid::from_code(code) + ctx.resolve_code(code) .map(SridArg::Srid) - .map_err(|e| EvalError::UnsupportedSrid { - function, - srid: e.code, - }) + .map_err(|source| EvalError::Crs { function, source }) } /// The shared SRID of two operands, or [`EvalError::MixedSrid`]. @@ -124,27 +214,44 @@ pub(super) fn same_srid( } } -/// Map a GeoJSON parse fault: an unsupported SRID fails the statement, -/// anything else is an unreadable argument and yields NULL. -pub(super) fn text_fault(function: &'static str, e: GeoJsonError) -> Result { - match e { - GeoJsonError::UnsupportedSrid(u) => Err(EvalError::UnsupportedSrid { - function, - srid: u.code, - }), - GeoJsonError::Malformed | GeoJsonError::UnknownCrsName { .. } => Ok(Value::Null), - } +/// The coordinate space of `srid`: geographic degrees or planar units. +/// An SRID `ctx` does not define fails with [`EvalError::Crs`]. +pub(super) fn space_of( + function: &'static str, + srid: Srid, + ctx: &CrsContext<'_>, +) -> Result { + BboxSpace::of(ctx, srid).map_err(|source| EvalError::Crs { function, source }) +} + +/// The measure basis of `srid`: geodesic on its ellipsoid or planar in its +/// unit. An SRID `ctx` does not define fails with [`EvalError::Crs`]. +pub(super) fn metric_of( + function: &'static str, + srid: Srid, + ctx: &CrsContext<'_>, +) -> Result { + Metric::resolve(ctx, srid).map_err(|source| EvalError::Crs { function, source }) } #[cfg(test)] mod tests { use super::*; + use nodedb_crs::UnknownSrid; use nodedb_types::geometry::Geometry; + fn ctx() -> CrsContext<'static> { + CrsContext::builtin_only() + } + + fn srid(code: u32) -> Srid { + Srid::new(code).unwrap() + } + #[test] fn geojson_and_wkt_strings_both_parse() { - let geojson = geometry_from_text(r#"{"type":"Point","coordinates":[1.0,2.0]}"#); - let wkt = geometry_from_text("POINT(1 2)"); + let geojson = geometry_from_text(r#"{"type":"Point","coordinates":[1.0,2.0]}"#, &ctx()); + let wkt = geometry_from_text("POINT(1 2)", &ctx()); assert_eq!(geojson, wkt); assert_eq!( geojson, @@ -156,49 +263,95 @@ mod tests { fn geojson_crs_member_sets_srid() { let g = geometry_from_text( r#"{"type":"Point","coordinates":[1.0,2.0],"crs":{"type":"name","properties":{"name":"EPSG:3375"}}}"#, + &ctx(), + ); + assert_eq!(g.map(|g| g.srid), Some(srid(3375))); + } + + #[test] + fn geojson_crs_member_with_an_undefined_srid_fails() { + let text = r#"{"type":"Point","coordinates":[1.0,2.0],"crs":{"type":"name","properties":{"name":"EPSG:999"}}}"#; + assert_eq!( + geom_arg(&[Value::String(text.into())], 0, "st_x", &ctx()), + Err(EvalError::Crs { + function: "st_x", + source: UnknownSrid::Undefined { code: 999 } + }) ); - assert_eq!(g.map(|g| g.srid), Some(Srid::PeninsularRso)); } #[test] fn surrounding_whitespace_does_not_defeat_parsing() { - assert!(geometry_from_text(" POINT(1 2) ").is_some()); - assert!(geometry_from_text(" {\"type\":\"Point\",\"coordinates\":[1,2]} ").is_some()); + assert!(geometry_from_text(" POINT(1 2) ", &ctx()).is_some()); + assert!( + geometry_from_text(" {\"type\":\"Point\",\"coordinates\":[1,2]} ", &ctx()).is_some() + ); + } + + /// Text in geometry position that does not parse fails the statement. + #[test] + fn unparseable_text_is_a_statement_error() { + assert!(matches!( + geom_arg(&[Value::String("not a geometry".into())], 0, "st_x", &ctx()), + Err(EvalError::Geometry { + function: "st_x", + .. + }) + )); + assert!(matches!( + geom_arg( + &[Value::String("POINT(1 2 3 4 5)".into())], + 0, + "st_x", + &ctx() + ), + Err(EvalError::Geometry { .. }) + )); + assert_eq!(geom_arg(&[Value::Null], 0, "st_x", &ctx()), Ok(None)); } #[test] - fn non_geometry_text_is_rejected() { - assert!(geometry_from_text("not a geometry").is_none()); - assert!(geometry_from_text("").is_none()); + fn wkb_bytes_are_a_geometry() { + let wkb = nodedb_spatial::geometry_to_wkb(&SridGeometry::wgs84(Geometry::point(1.0, 2.0))) + .unwrap(); + assert_eq!( + geom_arg(&[Value::Bytes(wkb)], 0, "st_x", &ctx()), + Ok(Some(SridGeometry::wgs84(Geometry::point(1.0, 2.0)))) + ); } #[test] fn srid_arg_forms() { let f = "st_setsrid"; + let ctx = ctx(); let args = [ Value::Integer(32648), Value::Float(3375.0), - Value::String("3376".into()), + Value::String("4742".into()), Value::Null, ]; - assert_eq!(srid_arg(&args, 0, f), Ok(SridArg::Srid(Srid::Utm48N))); - assert_eq!( - srid_arg(&args, 1, f), - Ok(SridArg::Srid(Srid::PeninsularRso)) - ); - assert_eq!(srid_arg(&args, 2, f), Ok(SridArg::Srid(Srid::BorneoRso))); - assert_eq!(srid_arg(&args, 3, f), Ok(SridArg::Null)); - assert_eq!(srid_arg(&args, 4, f), Ok(SridArg::Absent)); + assert_eq!(srid_arg(&args, 0, f, &ctx), Ok(SridArg::Srid(srid(32648)))); + assert_eq!(srid_arg(&args, 1, f, &ctx), Ok(SridArg::Srid(srid(3375)))); + assert_eq!(srid_arg(&args, 2, f, &ctx), Ok(SridArg::Srid(srid(4742)))); + assert_eq!(srid_arg(&args, 3, f, &ctx), Ok(SridArg::Null)); + assert_eq!(srid_arg(&args, 4, f, &ctx), Ok(SridArg::Absent)); assert_eq!( - srid_arg(&[Value::Integer(2154)], 0, f), - Err(EvalError::UnsupportedSrid { + srid_arg(&[Value::Integer(999)], 0, f, &ctx), + Err(EvalError::Crs { function: f, - srid: 2154 + source: UnknownSrid::Undefined { code: 999 } }) ); assert!(matches!( - srid_arg(&[Value::Bool(true)], 0, f), + srid_arg(&[Value::Bool(true)], 0, f, &ctx), Err(EvalError::ArgumentType { .. }) )); } + + #[test] + fn space_follows_the_crs_kind() { + let ctx = ctx(); + assert_eq!(space_of("f", srid(4742), &ctx), Ok(BboxSpace::Geographic)); + assert_eq!(space_of("f", srid(32650), &ctx), Ok(BboxSpace::Planar)); + } } diff --git a/nodedb-query/src/geo_functions/linear.rs b/nodedb-query/src/geo_functions/linear.rs new file mode 100644 index 000000000..4761a32cf --- /dev/null +++ b/nodedb-query/src/geo_functions/linear.rs @@ -0,0 +1,180 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Linear referencing: `ST_LineInterpolatePoint`, `ST_LocateAlong`, +//! `ST_LocateBetween`, `ST_InterpolatePoint`. + +use nodedb_crs::CrsContext; +use nodedb_types::Value; +use nodedb_types::geometry::Geometry; + +use super::helpers::{ + GeoEval, finite_arg, geom_arg, geom_pair, geometry_fault, metric_of, same_srid, +}; +use crate::expr::EvalError; +use crate::value_ops::to_value_number; + +pub(super) fn eval(canonical: &'static str, args: &[Value], ctx: &CrsContext<'_>) -> GeoEval { + let result = match canonical { + "st_lineinterpolatepoint" => line_interpolate_point(canonical, args, ctx), + "st_locatealong" => locate_along(canonical, args, ctx), + "st_locatebetween" => locate_between(canonical, args, ctx), + "st_interpolatepoint" => interpolate_point(canonical, args, ctx), + _ => return None, + }; + Some(result) +} + +/// `ST_LineInterpolatePoint(line, fraction)`: geodesic along a line in a +/// geographic CRS, straight in a projected one. Z and M interpolate. +fn line_interpolate_point( + function: &'static str, + args: &[Value], + ctx: &CrsContext<'_>, +) -> Result { + let (Some(g), Some(fraction)) = ( + geom_arg(args, 0, function, ctx)?, + finite_arg(args, 1, function)?, + ) else { + return Ok(Value::Null); + }; + let metric = metric_of(function, g.srid, ctx)?; + nodedb_spatial::ops::line_interpolate_point(&g, fraction, &metric) + .map(Value::Geometry) + .map_err(|e| geometry_fault(function, e)) +} + +/// `ST_LocateAlong(geom, m [, offset])`. +fn locate_along( + function: &'static str, + args: &[Value], + ctx: &CrsContext<'_>, +) -> Result { + let (Some(g), Some(m)) = ( + geom_arg(args, 0, function, ctx)?, + finite_arg(args, 1, function)?, + ) else { + return Ok(Value::Null); + }; + let offset = finite_arg(args, 2, function)?.unwrap_or(0.0); + nodedb_spatial::ops::locate_along(&g, m, offset) + .map(Value::Geometry) + .map_err(|e| geometry_fault(function, e)) +} + +/// `ST_LocateBetween(geom, m_start, m_end [, offset])`. +fn locate_between( + function: &'static str, + args: &[Value], + ctx: &CrsContext<'_>, +) -> Result { + let (Some(g), Some(start), Some(end)) = ( + geom_arg(args, 0, function, ctx)?, + finite_arg(args, 1, function)?, + finite_arg(args, 2, function)?, + ) else { + return Ok(Value::Null); + }; + let offset = finite_arg(args, 3, function)?.unwrap_or(0.0); + nodedb_spatial::ops::locate_between(&g, start, end, offset) + .map(Value::Geometry) + .map_err(|e| geometry_fault(function, e)) +} + +/// `ST_InterpolatePoint(line, point)`: the M of `line` nearest `point`. +fn interpolate_point( + function: &'static str, + args: &[Value], + ctx: &CrsContext<'_>, +) -> Result { + let Some((line, point)) = geom_pair(args, function, ctx)? else { + return Ok(Value::Null); + }; + same_srid(function, &line, &point)?; + let Geometry::Point { coordinates } = point.geometry() else { + return Err(EvalError::ArgumentValue { + function, + position: 2, + reason: format!("must be a point, got {}", point.geometry().geometry_type()), + }); + }; + if point.is_empty() { + return Ok(Value::Null); + } + Ok( + match nodedb_spatial::ops::interpolate_point(&line, *coordinates) + .map_err(|e| geometry_fault(function, e))? + { + Some(m) => to_value_number(m), + None => Value::Null, + }, + ) +} + +#[cfg(test)] +mod tests { + use super::*; + use nodedb_types::geometry::{Ordinates, Srid, SridGeometry}; + + fn eval(canonical: &'static str, args: &[Value]) -> GeoEval { + super::eval(canonical, args, &CrsContext::builtin_only()) + } + + fn line_m() -> Value { + Value::Geometry( + SridGeometry::with_ordinates( + Srid::new(32648).unwrap(), + Geometry::line_string(vec![[0.0, 0.0], [10.0, 0.0]]), + Ordinates::new(None, Some(vec![0.0, 100.0])), + ) + .unwrap(), + ) + } + + #[test] + fn interpolate_and_locate() { + let Some(Ok(Value::Geometry(p))) = + eval("st_lineinterpolatepoint", &[line_m(), Value::Float(0.5)]) + else { + panic!(); + }; + assert_eq!(p.geometry(), &Geometry::point(5.0, 0.0)); + assert_eq!(p.ordinates().m(), Some(&[50.0][..])); + let Some(Ok(Value::Geometry(at))) = eval("st_locatealong", &[line_m(), Value::Float(30.0)]) + else { + panic!(); + }; + assert_eq!( + at.geometry(), + &Geometry::MultiPoint { + coordinates: vec![[3.0, 0.0]] + } + ); + let Some(Ok(Value::Geometry(between))) = eval( + "st_locatebetween", + &[line_m(), Value::Float(10.0), Value::Float(20.0)], + ) else { + panic!(); + }; + assert_eq!(between.geometry().coord_count(), 2); + let point = Value::Geometry(SridGeometry::new( + Srid::new(32648).unwrap(), + Geometry::point(7.0, 3.0), + )); + assert_eq!( + eval("st_interpolatepoint", &[line_m(), point]), + Some(Ok(to_value_number(70.0))) + ); + } + + #[test] + fn measures_without_m_fail() { + let xy = Value::Geometry(SridGeometry::new( + Srid::new(32648).unwrap(), + Geometry::line_string(vec![[0.0, 0.0], [1.0, 0.0]]), + )); + assert!(matches!( + eval("st_locatealong", &[xy, Value::Float(1.0)]), + Some(Err(EvalError::Geometry { .. })) + )); + } +} diff --git a/nodedb-query/src/geo_functions/measures.rs b/nodedb-query/src/geo_functions/measures.rs index 148ef8bfc..a209357c1 100644 --- a/nodedb-query/src/geo_functions/measures.rs +++ b/nodedb-query/src/geo_functions/measures.rs @@ -2,57 +2,65 @@ //! Distance, length, perimeter, area, and bearing. //! -//! Every measure is reported in metres (or square metres). A WGS 84 geometry -//! is measured geodesically, the same basis as `nodedb_spatial::st_distance`. -//! A projected geometry is measured in planar grid metres. +//! Every measure is reported in metres (or square metres). A geometry in a +//! geographic SRID is measured geodesically on that CRS's own ellipsoid. A +//! projected geometry is measured in its planar grid, then scaled from the +//! CRS's linear unit to metres. `geo_distance` and `geo_bearing` are the +//! spherical haversine formulas on bare numbers. +use nodedb_crs::CrsContext; use nodedb_types::Value; -use nodedb_types::geometry::{Geometry, SridGeometry}; -use super::helpers::{GeoEval, geom_arg, num_arg, same_srid}; +use super::helpers::{GeoEval, metric_of, num_arg, pair, unary}; +use crate::expr::EvalError; use crate::value_ops::to_value_number; -pub(super) fn eval(canonical: &'static str, args: &[Value]) -> GeoEval { +pub(super) fn eval(canonical: &'static str, args: &[Value], ctx: &CrsContext<'_>) -> GeoEval { let result = match canonical { - "st_distance" => { - let (Some(a), Some(b)) = (geom_arg(args, 0), geom_arg(args, 1)) else { - return Some(Ok(Value::Null)); - }; - let srid = match same_srid(canonical, &a, &b) { - Ok(srid) => srid, - Err(e) => return Some(Err(e)), - }; - let d = if srid.is_geographic() { - nodedb_spatial::st_distance(&a.geometry, &b.geometry) - } else { - nodedb_spatial::planar_distance(&a.geometry, &b.geometry) - }; - to_value_number(d) - } - "st_length" => unary(args, linear_length, nodedb_spatial::planar_length), - "st_perimeter" => unary(args, perimeter, nodedb_spatial::planar_perimeter), - "st_area" => unary(args, nodedb_spatial::st_area, nodedb_spatial::planar_area), - "geo_distance" => haversine(args, nodedb_types::geometry::haversine_distance), - "geo_bearing" => haversine(args, nodedb_types::geometry::haversine_bearing), + "st_distance" => measure_pair(canonical, args, ctx, |m, a, b| { + m.distance(a.geometry(), b.geometry()) + }), + "st_3ddistance" => measure_pair(canonical, args, ctx, |m, a, b| m.distance_3d(a, b)), + "st_length" => measure(canonical, args, ctx, |m, g| m.length(g.geometry())), + "st_3dlength" => measure(canonical, args, ctx, |m, g| m.length_3d(g)), + "st_perimeter" => measure(canonical, args, ctx, |m, g| m.perimeter(g.geometry())), + "st_area" => measure(canonical, args, ctx, |m, g| m.area(g.geometry())), + "geo_distance" => Ok(haversine(args, nodedb_types::geometry::haversine_distance)), + "geo_bearing" => Ok(haversine(args, nodedb_types::geometry::haversine_bearing)), _ => return None, }; - Some(Ok(result)) + Some(result) } -/// A unary measure: `geodesic` for a WGS 84 geometry, `planar` for a -/// projected one. -fn unary(args: &[Value], geodesic: fn(&Geometry) -> f64, planar: fn(&Geometry) -> f64) -> Value { - match geom_arg(args, 0) { - Some(SridGeometry { srid, geometry }) => { - let f = if srid.is_geographic() { - geodesic - } else { - planar - }; - to_value_number(f(&geometry)) +type Geom = nodedb_types::geometry::SridGeometry; + +/// A measure between two operands in one SRID. EMPTY gives NULL. +fn measure_pair( + function: &'static str, + args: &[Value], + ctx: &CrsContext<'_>, + f: fn(&nodedb_spatial::Metric, &Geom, &Geom) -> f64, +) -> Result { + pair(function, args, ctx, |a, b| { + let metric = metric_of(function, a.srid, ctx)?; + if a.is_empty() || b.is_empty() { + return Ok(Value::Null); } - None => Value::Null, - } + Ok(to_value_number(f(&metric, a, b))) + }) +} + +/// A measure of one operand in its own SRID. +fn measure( + function: &'static str, + args: &[Value], + ctx: &CrsContext<'_>, + f: fn(&nodedb_spatial::Metric, &Geom) -> f64, +) -> Result { + unary(function, args, ctx, |g| { + let metric = metric_of(function, g.srid, ctx)?; + Ok(to_value_number(f(&metric, g))) + }) } fn haversine(args: &[Value], f: fn(f64, f64, f64, f64) -> f64) -> Value { @@ -67,69 +75,19 @@ fn haversine(args: &[Value], f: fn(f64, f64, f64, f64) -> f64) -> Value { to_value_number(f(lng1, lat1, lng2, lat2)) } -/// Total geodesic length of every linear component. Areal geometries have no -/// length in PostGIS (their boundary is `ST_Perimeter`), and points have none. -fn linear_length(geom: &Geometry) -> f64 { - match geom { - Geometry::LineString { coordinates } => path_length(coordinates), - Geometry::MultiLineString { coordinates } => { - coordinates.iter().map(|line| path_length(line)).sum() - } - Geometry::GeometryCollection { geometries } => geometries.iter().map(linear_length).sum(), - _ => 0.0, - } -} - -/// Total geodesic length of every areal component's rings, holes included — -/// the boundary length of the geometry. -fn perimeter(geom: &Geometry) -> f64 { - match geom { - Geometry::Polygon { coordinates } => coordinates - .iter() - .map(|ring| closed_ring_length(ring)) - .sum(), - Geometry::MultiPolygon { coordinates } => coordinates - .iter() - .flat_map(|rings| rings.iter()) - .map(|ring| closed_ring_length(ring)) - .sum(), - Geometry::GeometryCollection { geometries } => geometries.iter().map(perimeter).sum(), - _ => 0.0, - } -} - -fn path_length(coords: &[[f64; 2]]) -> f64 { - coords - .windows(2) - .map(|pair| { - nodedb_types::geometry::haversine_distance( - pair[0][0], pair[0][1], pair[1][0], pair[1][1], - ) - }) - .sum() -} - -/// Length of a ring, closing it if the source omitted the repeated last -/// vertex — GeoJSON requires closure but stored data is not always well-formed. -fn closed_ring_length(ring: &[[f64; 2]]) -> f64 { - let mut total = path_length(ring); - if let (Some(first), Some(last)) = (ring.first(), ring.last()) - && first != last - { - total += nodedb_types::geometry::haversine_distance(last[0], last[1], first[0], first[1]); - } - total -} - #[cfg(test)] mod tests { use super::*; use crate::value_ops::value_to_f64; - use nodedb_types::geometry::Srid; + use nodedb_types::geometry::{Geometry, Ordinates, Srid, SridGeometry}; + + fn ctx() -> CrsContext<'static> { + CrsContext::builtin_only() + } /// The evaluator with a fault turned into a test failure. fn eval(canonical: &'static str, args: &[Value]) -> Option { - super::eval(canonical, args).map(|r| r.expect("no fault expected")) + super::eval(canonical, args, &ctx()).map(|r| r.expect("no fault expected")) } fn geom(v: Geometry) -> Vec { @@ -137,7 +95,45 @@ mod tests { } fn utm48(v: Geometry) -> Value { - Value::Geometry(SridGeometry::new(Srid::Utm48N, v)) + Value::Geometry(SridGeometry::new(Srid::new(32648).unwrap(), v)) + } + + fn ft2263(v: Geometry) -> Value { + Value::Geometry(SridGeometry::new(Srid::new(2263).unwrap(), v)) + } + + /// Metres per US survey foot, the unit of EPSG:2263. + const US_FOOT_M: f64 = 1200.0 / 3937.0; + + /// EPSG:2263 coordinates are US survey feet: distance, length and area + /// come back in metres and square metres. + #[test] + fn feet_crs_measures_report_metres() { + let a = ft2263(Geometry::point(1_000_000.0, 200_000.0)); + let b = ft2263(Geometry::point(1_001_000.0, 200_000.0)); + let d = eval("st_distance", &[a, b]).and_then(|v| value_to_f64(&v, false)); + let d = d.expect("a distance"); + assert!((d - 1000.0 * US_FOOT_M).abs() < 1e-6, "{d}"); + + let line = ft2263(Geometry::line_string(vec![ + [1_000_000.0, 200_000.0], + [1_000_000.0, 200_500.0], + ])); + let len = eval("st_length", &[line]).and_then(|v| value_to_f64(&v, false)); + let len = len.expect("a length"); + assert!((len - 500.0 * US_FOOT_M).abs() < 1e-6, "{len}"); + + let square = ft2263(Geometry::polygon(vec![vec![ + [1_000_000.0, 200_000.0], + [1_000_100.0, 200_000.0], + [1_000_100.0, 200_100.0], + [1_000_000.0, 200_100.0], + [1_000_000.0, 200_000.0], + ]])); + let area = eval("st_area", &[square]).and_then(|v| value_to_f64(&v, false)); + let area = area.expect("an area"); + let expected = 10_000.0 * US_FOOT_M * US_FOOT_M; + assert!((area - expected).abs() < 1e-6, "{area}"); } #[test] @@ -171,20 +167,20 @@ mod tests { utm48(Geometry::point(0.0, 0.0)), ]; assert!(matches!( - super::eval("st_distance", &args), + super::eval("st_distance", &args, &ctx()), Some(Err(crate::expr::EvalError::MixedSrid { .. })) )); } + /// pyproj 3.8.0 `Geod(ellps="WGS84").inv(0, 0, 0, 1)`: 110574.38855779878 m. #[test] fn length_measures_linestrings_only() { let line = Geometry::line_string(vec![[0.0, 0.0], [0.0, 1.0]]); let measured = eval("st_length", &geom(line)).and_then(|v| value_to_f64(&v, true)); - // One degree of latitude is ~111.19 km. let Some(m) = measured else { panic!("expected a numeric length"); }; - assert!((m - 111_195.0).abs() < 500.0, "got {m}"); + assert!((m - 110_574.388_557_798_78).abs() < 1e-6, "got {m}"); let point = geom(Geometry::point(1.0, 2.0)); assert_eq!(eval("st_length", &point), Some(to_value_number(0.0))); @@ -215,15 +211,86 @@ mod tests { } #[test] - fn unreadable_geometry_yields_null_not_zero() { + fn unreadable_geometry_fails_the_statement() { let bad = vec![Value::String("not a geometry".into())]; - assert_eq!(eval("st_area", &bad), Some(Value::Null)); - assert_eq!(eval("st_length", &bad), Some(Value::Null)); - assert_eq!(eval("st_distance", &bad), Some(Value::Null)); + for f in ["st_area", "st_length", "st_distance"] { + assert!(matches!( + super::eval(f, &bad, &ctx()), + Some(Err(crate::expr::EvalError::Geometry { .. })) + )); + } + assert_eq!(eval("st_area", &[Value::Null]), Some(Value::Null)); + } + + /// pyproj 3.8.0 `Geod(ellps="WGS84").polygon_area_perimeter` on the + /// 1° square at the equator: 12308778361.469452 m², 443770.917248302 m. + #[test] + fn geographic_area_and_perimeter_are_geodesic() { + let square = geom(Geometry::polygon(vec![vec![ + [0.0, 0.0], + [1.0, 0.0], + [1.0, 1.0], + [0.0, 1.0], + [0.0, 0.0], + ]])); + let area = eval("st_area", &square).and_then(|v| value_to_f64(&v, true)); + assert!((area.unwrap() - 12_308_778_361.469_452).abs() < 1e-3); + let per = eval("st_perimeter", &square).and_then(|v| value_to_f64(&v, true)); + assert!((per.unwrap() - 443_770.917_248_302).abs() < 1e-6); + } + + #[test] + fn distance_to_empty_is_null() { + let args = [ + Value::Geometry(SridGeometry::wgs84(Geometry::empty_point())), + Value::Geometry(SridGeometry::wgs84(Geometry::point(0.0, 0.0))), + ]; + assert_eq!(eval("st_distance", &args), Some(Value::Null)); + } + + #[test] + fn three_d_measures() { + let z = |x: f64, y: f64, h: f64| { + Value::Geometry( + SridGeometry::with_ordinates( + Srid::new(32648).unwrap(), + Geometry::point(x, y), + Ordinates::new(Some(vec![h]), None), + ) + .unwrap(), + ) + }; + let d = eval("st_3ddistance", &[z(0.0, 0.0, 0.0), z(3.0, 0.0, 4.0)]) + .and_then(|v| value_to_f64(&v, true)); + assert_eq!(d, Some(5.0)); + let line = Value::Geometry( + SridGeometry::with_ordinates( + Srid::new(32648).unwrap(), + Geometry::line_string(vec![[0.0, 0.0], [3.0, 0.0]]), + Ordinates::new(Some(vec![0.0, 4.0]), None), + ) + .unwrap(), + ); + let l = eval("st_3dlength", &[line]).and_then(|v| value_to_f64(&v, true)); + assert_eq!(l, Some(5.0)); } #[test] fn unknown_name_falls_through() { assert_eq!(eval("st_contains", &[]), None); } + + /// A geographic SRID other than WGS 84 is measured geodesically. + #[test] + fn geographic_gdm2000_length_is_geodesic() { + let line = Value::Geometry(SridGeometry::new( + Srid::new(4742).unwrap(), + Geometry::line_string(vec![[0.0, 0.0], [0.0, 1.0]]), + )); + let Some(m) = eval("st_length", &[line]).and_then(|v| value_to_f64(&v, true)) else { + panic!("expected a numeric length"); + }; + // GRS80: pyproj Geod(ellps="GRS80").inv(0, 0, 0, 1) = 110574.38855415255. + assert!((m - 110_574.388_554_152_55).abs() < 1e-6, "got {m}"); + } } diff --git a/nodedb-query/src/geo_functions/mod.rs b/nodedb-query/src/geo_functions/mod.rs index cf34c7fab..2d7ad62d0 100644 --- a/nodedb-query/src/geo_functions/mod.rs +++ b/nodedb-query/src/geo_functions/mod.rs @@ -7,11 +7,18 @@ pub mod catalog; pub mod constructors; pub mod crs; pub mod dispatch; +pub mod encode; pub mod helpers; pub mod indexing; +pub mod linear; pub mod measures; +pub mod overlay; pub mod predicates; +pub mod srid_refs; pub use catalog::{GEO_FUNCTIONS, GeoArgShape, GeoFunctionSpec, GeoReturn, returns_geometry}; pub use dispatch::eval_geo_function; pub use helpers::geometry_from_text; +pub use srid_refs::{ + expr_names_srid, literal_geometry_srid, literal_srid_code, srid_arg_position, type_names_srid, +}; diff --git a/nodedb-query/src/geo_functions/overlay.rs b/nodedb-query/src/geo_functions/overlay.rs new file mode 100644 index 000000000..53c108713 --- /dev/null +++ b/nodedb-query/src/geo_functions/overlay.rs @@ -0,0 +1,437 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Geometry-returning operations: buffer, envelope, centroid, boolean +//! overlay, hulls, simplification, repair, subdivision, splitting, closest +//! point and dimension forcing. +//! +//! An operation keeps its input SRID. A binary operation needs both inputs +//! in one SRID. A derived shape has no Z or M of its own. + +use nodedb_crs::CrsContext; +use nodedb_spatial::ops::{Hull, Simplifier}; +use nodedb_types::Value; +use nodedb_types::geometry::{Dimension, Geometry, GeometryKind, SridGeometry}; + +use super::constructors::segments_arg; +use super::helpers::{ + GeoEval, finite_arg, geom_arg, geometry_fault, metric_of, pair, space_of, unary, +}; +use crate::expr::EvalError; + +pub(super) fn eval(canonical: &'static str, args: &[Value], ctx: &CrsContext<'_>) -> GeoEval { + let result = match canonical { + "st_buffer" => buffer(canonical, args, ctx), + "st_envelope" => in_space(canonical, args, ctx, |g, space| { + Ok(nodedb_spatial::st_envelope_in(g, space)) + }), + "st_centroid" => centroid(canonical, args, ctx), + "st_union" => binary(canonical, args, ctx, nodedb_spatial::ops::union), + "st_intersection" => binary(canonical, args, ctx, nodedb_spatial::ops::intersection), + "st_difference" => binary(canonical, args, ctx, nodedb_spatial::ops::difference), + "st_symdifference" => binary(canonical, args, ctx, nodedb_spatial::ops::sym_difference), + "st_convexhull" => in_space(canonical, args, ctx, |g, _| { + Ok(nodedb_spatial::ops::hull(g, Hull::Convex)) + }), + "st_concavehull" => with_number(canonical, args, ctx, |g, pct| { + if !(0.0..=1.0).contains(&pct) { + return Err(out_of_range(canonical, 2, "must be in [0, 1]")); + } + Ok(nodedb_spatial::ops::hull( + g, + Hull::Concave { pct_convex: pct }, + )) + }), + "st_simplify" => simplify(canonical, args, ctx, Simplifier::DouglasPeucker), + "st_simplifypreservetopology" => { + simplify(canonical, args, ctx, Simplifier::PreserveTopology) + } + "st_makevalid" => in_space(canonical, args, ctx, |g, _| { + nodedb_spatial::ops::make_valid(g).map_err(|e| geometry_fault(canonical, e)) + }), + "st_subdivide" => subdivide(canonical, args, ctx), + "st_split" => binary_fallible(canonical, args, ctx, nodedb_spatial::ops::split), + "st_closestpoint" => closest_point(canonical, args, ctx), + "st_segmentize" => segmentize(canonical, args, ctx), + "st_force2d" => force(canonical, args, ctx, Dimension::Xy), + "st_force3d" => force(canonical, args, ctx, Dimension::Xyz), + "st_force3dm" => force(canonical, args, ctx, Dimension::Xym), + "st_force4d" => force(canonical, args, ctx, Dimension::Xyzm), + _ => return None, + }; + Some(result) +} + +fn out_of_range(function: &'static str, position: usize, reason: &str) -> EvalError { + EvalError::ArgumentValue { + function, + position, + reason: reason.into(), + } +} + +/// A unary operation in the input's coordinate space. The result keeps the +/// input SRID. +fn in_space( + function: &'static str, + args: &[Value], + ctx: &CrsContext<'_>, + f: impl FnOnce(&Geometry, nodedb_types::BboxSpace) -> Result, +) -> Result { + unary(function, args, ctx, |g| { + let space = space_of(function, g.srid, ctx)?; + Ok(Value::Geometry(g.with_geometry(f(g.geometry(), space)?))) + }) +} + +/// A unary operation with a required numeric second argument. +fn with_number( + function: &'static str, + args: &[Value], + ctx: &CrsContext<'_>, + f: impl Fn(&Geometry, f64) -> Result, +) -> Result { + let (Some(g), Some(n)) = ( + geom_arg(args, 0, function, ctx)?, + finite_arg(args, 1, function)?, + ) else { + return Ok(Value::Null); + }; + Ok(Value::Geometry(g.with_geometry(f(g.geometry(), n)?))) +} + +/// A binary operation over two inputs in one SRID. +fn binary( + function: &'static str, + args: &[Value], + ctx: &CrsContext<'_>, + f: fn(&Geometry, &Geometry) -> Geometry, +) -> Result { + binary_fallible(function, args, ctx, |a, b| Ok(f(a, b))) +} + +fn binary_fallible( + function: &'static str, + args: &[Value], + ctx: &CrsContext<'_>, + f: impl FnOnce(&Geometry, &Geometry) -> Result, +) -> Result { + pair(function, args, ctx, |a, b| { + let out = f(a.geometry(), b.geometry()).map_err(|e| geometry_fault(function, e))?; + Ok(Value::Geometry(SridGeometry::new(a.srid, out))) + }) +} + +/// `ST_Buffer(geom, distance_m [, segments])`. Distance is metres in either +/// space: geodesic in a local projection on a geographic CRS's ellipsoid, +/// planar in a projected CRS's unit. +fn buffer( + function: &'static str, + args: &[Value], + ctx: &CrsContext<'_>, +) -> Result { + let (Some(g), Some(distance)) = ( + geom_arg(args, 0, function, ctx)?, + finite_arg(args, 1, function)?, + ) else { + return Ok(Value::Null); + }; + let def = ctx + .resolve(g.srid) + .map_err(|source| EvalError::Crs { function, source })?; + let out = nodedb_spatial::ops::buffer(g.geometry(), distance, segments_arg(args, 2), &def) + .map_err(|e| geometry_fault(function, e))?; + Ok(Value::Geometry(g.with_geometry(out))) +} + +/// `ST_Centroid(geom)`: weighted by geodesic area or length on a +/// geographic CRS, planar on a projected one. EMPTY gives `POINT EMPTY`. +fn centroid( + function: &'static str, + args: &[Value], + ctx: &CrsContext<'_>, +) -> Result { + unary(function, args, ctx, |g| { + let metric = metric_of(function, g.srid, ctx)?; + let point = nodedb_spatial::st_centroid(g.geometry(), &metric) + .unwrap_or_else(|| Geometry::empty(GeometryKind::Point)); + Ok(Value::Geometry(g.with_geometry(point))) + }) +} + +/// `ST_Simplify` / `ST_SimplifyPreserveTopology(geom, tolerance)`, the +/// tolerance in coordinate units. A negative tolerance fails. +fn simplify( + function: &'static str, + args: &[Value], + ctx: &CrsContext<'_>, + simplifier: Simplifier, +) -> Result { + with_number(function, args, ctx, |g, tolerance| { + if tolerance < 0.0 { + return Err(out_of_range(function, 2, "must not be negative")); + } + Ok(nodedb_spatial::ops::simplify(g, tolerance, simplifier)) + }) +} + +/// `ST_Subdivide(geom [, max_vertices])`, 256 vertices when absent. The +/// pieces come back as one GEOMETRYCOLLECTION. +fn subdivide( + function: &'static str, + args: &[Value], + ctx: &CrsContext<'_>, +) -> Result { + const DEFAULT_MAX_VERTICES: f64 = 256.0; + let Some(g) = geom_arg(args, 0, function, ctx)? else { + return Ok(Value::Null); + }; + let max = match args.get(1) { + None => DEFAULT_MAX_VERTICES, + Some(_) => match finite_arg(args, 1, function)? { + Some(n) => n, + None => return Ok(Value::Null), + }, + }; + if max < nodedb_spatial::ops::MIN_SUBDIVIDE_VERTICES as f64 { + return Err(out_of_range(function, 2, "must be at least 5")); + } + Ok(Value::Geometry(g.with_geometry( + nodedb_spatial::ops::subdivide(g.geometry(), max as usize), + ))) +} + +/// `ST_ClosestPoint(a, b)`: the point of `a` nearest `b`. EMPTY gives NULL. +fn closest_point( + function: &'static str, + args: &[Value], + ctx: &CrsContext<'_>, +) -> Result { + pair(function, args, ctx, |a, b| { + Ok( + match nodedb_spatial::ops::closest_point(a.geometry(), b.geometry()) { + Some([x, y]) => Value::Geometry(SridGeometry::new(a.srid, Geometry::point(x, y))), + None => Value::Null, + }, + ) + }) +} + +/// `ST_Segmentize(geom, max_length_m)`: geodesic on a geographic CRS, in +/// metres of the grid on a projected one. Z and M interpolate. +fn segmentize( + function: &'static str, + args: &[Value], + ctx: &CrsContext<'_>, +) -> Result { + let (Some(g), Some(max)) = ( + geom_arg(args, 0, function, ctx)?, + finite_arg(args, 1, function)?, + ) else { + return Ok(Value::Null); + }; + if max <= 0.0 { + return Err(out_of_range(function, 2, "must be positive")); + } + let metric = metric_of(function, g.srid, ctx)?; + nodedb_spatial::ops::segmentize(&g, max, &metric) + .map(Value::Geometry) + .map_err(|e| geometry_fault(function, e)) +} + +/// `ST_Force2D/3D/3DM/4D(geom [, z [, m]])`: keep the shape, set the +/// dimension. A missing ordinate fills with the given value, 0 by default. +fn force( + function: &'static str, + args: &[Value], + ctx: &CrsContext<'_>, + dimension: Dimension, +) -> Result { + let Some(g) = geom_arg(args, 0, function, ctx)? else { + return Ok(Value::Null); + }; + let fill = |idx: usize| finite_arg(args, idx, function).map(|v| v.unwrap_or(0.0)); + let (fill_z, fill_m) = match dimension { + Dimension::Xy => (0.0, 0.0), + Dimension::Xyz => (fill(1)?, 0.0), + Dimension::Xym => (0.0, fill(1)?), + Dimension::Xyzm => (fill(1)?, fill(2)?), + }; + g.force_dimension(dimension, fill_z, fill_m) + .map(Value::Geometry) + .map_err(|e| geometry_fault(function, e.into())) +} + +#[cfg(test)] +mod tests { + use super::*; + use nodedb_types::geometry::{Ordinates, Srid}; + + fn eval(canonical: &'static str, args: &[Value]) -> GeoEval { + super::eval(canonical, args, &CrsContext::builtin_only()) + } + + fn run(canonical: &'static str, args: &[Value]) -> SridGeometry { + match eval(canonical, args) { + Some(Ok(Value::Geometry(g))) => g, + other => panic!("{canonical}: {other:?}"), + } + } + + fn utm(g: Geometry) -> Value { + Value::Geometry(SridGeometry::new(Srid::new(32648).unwrap(), g)) + } + + fn square(x0: f64, y0: f64, side: f64) -> Geometry { + Geometry::polygon(vec![vec![ + [x0, y0], + [x0 + side, y0], + [x0 + side, y0 + side], + [x0, y0 + side], + [x0, y0], + ]]) + } + + #[test] + fn overlay_ops_keep_srid() { + let (a, b) = (utm(square(0.0, 0.0, 2.0)), utm(square(1.0, 1.0, 2.0))); + for f in [ + "st_union", + "st_intersection", + "st_difference", + "st_symdifference", + ] { + let g = run(f, &[a.clone(), b.clone()]); + assert_eq!(g.srid.code(), 32648, "{f}"); + } + assert!(matches!( + eval( + "st_union", + &[ + a, + Value::Geometry(SridGeometry::wgs84(Geometry::point(0.0, 0.0))) + ] + ), + Some(Err(EvalError::MixedSrid { .. })) + )); + } + + #[test] + fn projected_buffer_is_planar_and_keeps_srid() { + let g = run( + "st_buffer", + &[ + utm(Geometry::point(500_000.0, 100_000.0)), + Value::Float(100.0), + ], + ); + assert_eq!(g.srid.code(), 32648); + let Geometry::Polygon { coordinates } = g.geometry() else { + panic!("expected a polygon"); + }; + let [x, y] = coordinates[0][0]; + // A 32-gon vertex sits at most 0.5 % off the circle. + assert!(((x - 500_000.0).hypot(y - 100_000.0) - 100.0).abs() < 0.5); + } + + #[test] + fn multi_buffer_is_not_a_bbox() { + let multi = Value::Geometry(SridGeometry::wgs84(Geometry::MultiPoint { + coordinates: vec![[0.0, 0.0], [0.1, 0.0]], + })); + let g = run("st_buffer", &[multi, Value::Float(100.0)]); + assert!( + matches!(g.geometry(), Geometry::MultiPolygon { coordinates } if coordinates.len() == 2), + "{g:?}" + ); + } + + #[test] + fn force_sets_dimension() { + let xy = utm(Geometry::point(1.0, 2.0)); + let z = run("st_force3d", &[xy.clone(), Value::Float(9.0)]); + assert_eq!(z.ordinates().z(), Some(&[9.0][..])); + let zm = run("st_force4d", std::slice::from_ref(&xy)); + assert_eq!(zm.dimension(), Dimension::Xyzm); + let m = run("st_force3dm", &[Value::Geometry(zm.clone())]); + assert_eq!(m.dimension(), Dimension::Xym); + assert_eq!( + run("st_force2d", &[Value::Geometry(zm)]).dimension(), + Dimension::Xy + ); + } + + #[test] + fn hulls_simplify_and_repair() { + let pts = utm(Geometry::MultiPoint { + coordinates: vec![[0.0, 0.0], [4.0, 0.0], [4.0, 4.0], [0.0, 4.0], [2.0, 2.0]], + }); + assert!(matches!( + run("st_convexhull", std::slice::from_ref(&pts)).geometry(), + Geometry::Polygon { .. } + )); + assert!(matches!( + eval("st_concavehull", &[pts, Value::Float(2.0)]), + Some(Err(EvalError::ArgumentValue { .. })) + )); + let line = utm(Geometry::line_string(vec![ + [0.0, 0.0], + [5.0, 0.1], + [10.0, 0.0], + ])); + let simple = run("st_simplify", &[line, Value::Float(1.0)]); + assert_eq!(simple.geometry().coord_count(), 2); + let bowtie = utm(Geometry::polygon(vec![vec![ + [0.0, 0.0], + [0.0, 10.0], + [10.0, 0.0], + [10.0, 10.0], + [0.0, 0.0], + ]])); + assert!(nodedb_spatial::is_valid( + run("st_makevalid", &[bowtie]).geometry() + )); + } + + #[test] + fn centroid_of_empty_is_empty_point() { + let g = run( + "st_centroid", + &[Value::Geometry( + SridGeometry::wgs84(Geometry::empty_point()), + )], + ); + assert!(g.is_empty()); + } + + #[test] + fn segmentize_keeps_z() { + let line = Value::Geometry( + SridGeometry::with_ordinates( + Srid::new(32648).unwrap(), + Geometry::line_string(vec![[0.0, 0.0], [10.0, 0.0]]), + Ordinates::new(Some(vec![0.0, 10.0]), None), + ) + .unwrap(), + ); + let g = run("st_segmentize", &[line, Value::Float(5.0)]); + assert_eq!(g.ordinates().z(), Some(&[0.0, 5.0, 10.0][..])); + } + + #[test] + fn subdivide_and_split_return_collections() { + let sq = utm(square(0.0, 0.0, 4.0)); + let blade = utm(Geometry::line_string(vec![[2.0, -1.0], [2.0, 5.0]])); + let parts = run("st_split", &[sq.clone(), blade]); + assert!( + matches!(parts.geometry(), Geometry::GeometryCollection { geometries } if geometries.len() == 2) + ); + assert!(matches!( + run("st_subdivide", std::slice::from_ref(&sq)).geometry(), + Geometry::GeometryCollection { .. } + )); + assert!(matches!( + eval("st_subdivide", &[sq, Value::Integer(2)]), + Some(Err(EvalError::ArgumentValue { .. })) + )); + } +} diff --git a/nodedb-query/src/geo_functions/predicates.rs b/nodedb-query/src/geo_functions/predicates.rs index 940908b03..1f892de56 100644 --- a/nodedb-query/src/geo_functions/predicates.rs +++ b/nodedb-query/src/geo_functions/predicates.rs @@ -1,66 +1,182 @@ // SPDX-License-Identifier: Apache-2.0 //! Topological predicate evaluation. Every predicate is NULL-propagating: an -//! argument that is not a readable geometry yields NULL rather than `false`, -//! so "no geometry" stays distinguishable from "does not match". Two -//! operands in different SRIDs fail with `EvalError::MixedSrid`. +//! argument that is SQL NULL yields NULL rather than `false`, so "no +//! geometry" stays distinguishable from "does not match". Two operands in +//! different SRIDs fail with `EvalError::MixedSrid`. An EMPTY operand +//! intersects, contains and touches nothing, as in PostGIS. +use nodedb_crs::CrsContext; +use nodedb_spatial::ops::{Relation, relates}; use nodedb_types::Value; use nodedb_types::geometry::Geometry; -use super::helpers::{GeoEval, geom_arg, num_arg, same_srid}; +use super::helpers::{GeoEval, finite_arg, geometry_fault, metric_of, pair, str_arg, unary}; use crate::expr::EvalError; -pub(super) fn eval(canonical: &'static str, args: &[Value]) -> GeoEval { +pub(super) fn eval(canonical: &'static str, args: &[Value], ctx: &CrsContext<'_>) -> GeoEval { let result = match canonical { - "st_contains" => binary(canonical, args, nodedb_spatial::st_contains), - "st_intersects" => binary(canonical, args, nodedb_spatial::st_intersects), - "st_within" => binary(canonical, args, nodedb_spatial::st_within), - "st_disjoint" => binary(canonical, args, nodedb_spatial::st_disjoint), - "st_dwithin" => dwithin(canonical, args), - "st_isvalid" => Ok(match geom_arg(args, 0) { - Some(g) => Value::Bool(nodedb_spatial::is_valid(&g.geometry)), - None => Value::Null, + "st_contains" => predicate(canonical, args, ctx, false, nodedb_spatial::st_contains), + "st_intersects" => predicate(canonical, args, ctx, false, nodedb_spatial::st_intersects), + "st_within" => predicate(canonical, args, ctx, false, nodedb_spatial::st_within), + "st_disjoint" => predicate(canonical, args, ctx, true, nodedb_spatial::st_disjoint), + "st_touches" => named(canonical, args, ctx, Relation::Touches), + "st_crosses" => named(canonical, args, ctx, Relation::Crosses), + "st_overlaps" => named(canonical, args, ctx, Relation::Overlaps), + "st_covers" => named(canonical, args, ctx, Relation::Covers), + "st_coveredby" => named(canonical, args, ctx, Relation::CoveredBy), + "st_equals" => equals(canonical, args, ctx), + "st_relate" => relate(canonical, args, ctx), + "st_dwithin" => dwithin(canonical, args, ctx, false), + "st_3ddwithin" => dwithin(canonical, args, ctx, true), + "st_3dintersects" => intersects_3d(canonical, args, ctx), + "st_isvalid" => unary(canonical, args, ctx, |g| { + Ok(Value::Bool(nodedb_spatial::is_valid(g.geometry()))) + }), + "st_isvalidreason" => unary(canonical, args, ctx, |g| { + Ok(Value::String(nodedb_spatial::validity_reason(g.geometry()))) }), _ => return None, }; Some(result) } -fn binary( +/// A planar predicate. An EMPTY operand gives `on_empty`. +fn predicate( function: &'static str, args: &[Value], + ctx: &CrsContext<'_>, + on_empty: bool, f: fn(&Geometry, &Geometry) -> bool, ) -> Result { - let (Some(a), Some(b)) = (geom_arg(args, 0), geom_arg(args, 1)) else { - return Ok(Value::Null); - }; - same_srid(function, &a, &b)?; - Ok(Value::Bool(f(&a.geometry, &b.geometry))) + pair(function, args, ctx, |a, b| { + if a.is_empty() || b.is_empty() { + return Ok(Value::Bool(on_empty)); + } + Ok(Value::Bool(f(a.geometry(), b.geometry()))) + }) } -/// `ST_DWithin(a, b, distance)`, distance in metres: geodesic-approximate for -/// 4326, planar for a projected SRID. -fn dwithin(function: &'static str, args: &[Value]) -> Result { - let (Some(a), Some(b)) = (geom_arg(args, 0), geom_arg(args, 1)) else { - return Ok(Value::Null); - }; - let Some(distance) = num_arg(args, 2) else { - return Ok(Value::Null); - }; - let srid = same_srid(function, &a, &b)?; - let within = if srid.is_geographic() { - nodedb_spatial::st_dwithin(&a.geometry, &b.geometry, distance) - } else { - nodedb_spatial::planar_dwithin(&a.geometry, &b.geometry, distance) - }; - Ok(Value::Bool(within)) +/// A named DE-9IM predicate. An EMPTY operand gives `false`. +fn named( + function: &'static str, + args: &[Value], + ctx: &CrsContext<'_>, + relation: Relation, +) -> Result { + pair(function, args, ctx, |a, b| { + if a.is_empty() || b.is_empty() { + return Ok(Value::Bool(false)); + } + Ok(Value::Bool(relates(a.geometry(), b.geometry(), relation))) + }) +} + +/// `ST_Equals`: the same point set. Two EMPTY operands are equal. +fn equals( + function: &'static str, + args: &[Value], + ctx: &CrsContext<'_>, +) -> Result { + pair(function, args, ctx, |a, b| { + Ok(Value::Bool(match (a.is_empty(), b.is_empty()) { + (true, true) => true, + (false, false) => relates(a.geometry(), b.geometry(), Relation::Equals), + _ => false, + })) + }) +} + +/// `ST_Relate(a, b)`: the DE-9IM matrix text. `ST_Relate(a, b, pattern)`: +/// whether the matrix matches `pattern`. +fn relate( + function: &'static str, + args: &[Value], + ctx: &CrsContext<'_>, +) -> Result { + pair(function, args, ctx, |a, b| match args.get(2) { + None => Ok(Value::String(nodedb_spatial::ops::relate( + a.geometry(), + b.geometry(), + ))), + Some(Value::Null) => Ok(Value::Null), + Some(_) => { + let pattern = str_arg(args, 2).ok_or(EvalError::ArgumentType { + function, + position: 3, + expected: "a DE-9IM pattern text", + got: args.get(2).map_or("null", Value::type_name), + })?; + nodedb_spatial::ops::relate_pattern(a.geometry(), b.geometry(), &pattern) + .map(Value::Bool) + .map_err(|e| geometry_fault(function, e)) + } + }) +} + +/// `ST_DWithin(a, b, metres)` and `ST_3DDWithin(a, b, metres)`. Metres +/// are geodesic on a geographic CRS's ellipsoid, planar in a projected +/// CRS's unit. The 3D form adds Z: Euclidean in a projected CRS, the ECEF +/// chord in a geographic one. A negative distance fails. +fn dwithin( + function: &'static str, + args: &[Value], + ctx: &CrsContext<'_>, + three_d: bool, +) -> Result { + pair(function, args, ctx, |a, b| { + let Some(distance) = finite_arg(args, 2, function)? else { + return Ok(Value::Null); + }; + if distance < 0.0 { + return Err(EvalError::ArgumentValue { + function, + position: 3, + reason: "must not be negative".into(), + }); + } + let metric = metric_of(function, a.srid, ctx)?; + Ok(Value::Bool(if three_d { + metric.dwithin_3d(a, b, distance) + } else { + metric.dwithin(a.geometry(), b.geometry(), distance) + })) + }) +} + +/// `ST_3DIntersects(a, b)`: the 3D distance is zero. +fn intersects_3d( + function: &'static str, + args: &[Value], + ctx: &CrsContext<'_>, +) -> Result { + pair(function, args, ctx, |a, b| { + let metric = metric_of(function, a.srid, ctx)?; + if a.is_empty() || b.is_empty() { + return Ok(Value::Bool(false)); + } + Ok(Value::Bool( + metric.distance_3d(a, b) <= INTERSECT_TOLERANCE_M, + )) + }) } +/// 3D distance, metres, at or under which two geometries intersect: the +/// float noise of an ECEF chord. +const INTERSECT_TOLERANCE_M: f64 = 1e-6; + #[cfg(test)] mod tests { use super::*; - use nodedb_types::geometry::{Srid, SridGeometry}; + use nodedb_types::geometry::{Ordinates, Srid, SridGeometry}; + + fn eval(canonical: &'static str, args: &[Value]) -> GeoEval { + super::eval(canonical, args, &CrsContext::builtin_only()) + } + + fn utm48n() -> Srid { + Srid::new(32648).unwrap() + } fn geom(srid: Srid, g: Geometry) -> Value { Value::Geometry(SridGeometry::new(srid, g)) @@ -69,8 +185,8 @@ mod tests { #[test] fn mixed_srid_intersects_fails() { let args = vec![ - geom(Srid::Wgs84, Geometry::point(1.0, 2.0)), - geom(Srid::Utm48N, Geometry::point(1.0, 2.0)), + geom(Srid::WGS84, Geometry::point(1.0, 2.0)), + geom(utm48n(), Geometry::point(1.0, 2.0)), ]; assert_eq!( eval("st_intersects", &args), @@ -86,8 +202,8 @@ mod tests { fn projected_dwithin_is_planar_metres() { let args = |d: f64| { vec![ - geom(Srid::Utm48N, Geometry::point(500_000.0, 0.0)), - geom(Srid::Utm48N, Geometry::point(500_300.0, 400.0)), + geom(utm48n(), Geometry::point(500_000.0, 0.0)), + geom(utm48n(), Geometry::point(500_300.0, 400.0)), Value::Float(d), ] }; @@ -111,9 +227,174 @@ mod tests { [0.0, 0.0], ]]); let args = vec![ - geom(Srid::PeninsularRso, square), - geom(Srid::PeninsularRso, Geometry::point(5.0, 5.0)), + geom(Srid::new(3375).unwrap(), square), + geom(Srid::new(3375).unwrap(), Geometry::point(5.0, 5.0)), ]; assert_eq!(eval("st_contains", &args), Some(Ok(Value::Bool(true)))); } + + /// EPSG:2263 is in US survey feet: 300 ft apart is about 91.44 m. + #[test] + fn feet_crs_dwithin_compares_metres() { + let ft = Srid::new(2263).unwrap(); + let args = |d: f64| { + vec![ + geom(ft, Geometry::point(1_000_000.0, 200_000.0)), + geom(ft, Geometry::point(1_000_300.0, 200_000.0)), + Value::Float(d), + ] + }; + assert_eq!(eval("st_dwithin", &args(92.0)), Some(Ok(Value::Bool(true)))); + assert_eq!( + eval("st_dwithin", &args(91.0)), + Some(Ok(Value::Bool(false))) + ); + } + + /// A distance predicate needs the CRS kind, so an undefined SRID fails. + #[test] + fn dwithin_in_an_undefined_srid_fails() { + let undefined = Srid::new(999).unwrap(); + let args = vec![ + geom(undefined, Geometry::point(0.0, 0.0)), + geom(undefined, Geometry::point(1.0, 1.0)), + Value::Float(10.0), + ]; + assert!(matches!( + eval("st_dwithin", &args), + Some(Err(EvalError::Crs { .. })) + )); + } + + fn square(srid: Srid, side: f64) -> Value { + geom( + srid, + Geometry::polygon(vec![vec![ + [0.0, 0.0], + [side, 0.0], + [side, side], + [0.0, side], + [0.0, 0.0], + ]]), + ) + } + + #[test] + fn named_relations_and_relate() { + let s = Srid::new(3375).unwrap(); + let shifted = geom( + s, + Geometry::polygon(vec![vec![ + [10.0, 0.0], + [20.0, 0.0], + [20.0, 10.0], + [10.0, 10.0], + [10.0, 0.0], + ]]), + ); + assert_eq!( + eval("st_touches", &[square(s, 10.0), shifted.clone()]), + Some(Ok(Value::Bool(true))) + ); + assert_eq!( + eval("st_covers", &[square(s, 20.0), square(s, 10.0)]), + Some(Ok(Value::Bool(true))) + ); + assert_eq!( + eval("st_relate", &[square(s, 10.0), shifted.clone()]), + Some(Ok(Value::String("FF2F11212".into()))) + ); + assert_eq!( + eval( + "st_relate", + &[square(s, 10.0), shifted, Value::String("FF*F1****".into())] + ), + Some(Ok(Value::Bool(true))) + ); + } + + #[test] + fn empty_operands_follow_postgis() { + let empty = geom(Srid::WGS84, Geometry::empty_point()); + let pt = geom(Srid::WGS84, Geometry::point(0.0, 0.0)); + assert_eq!( + eval("st_intersects", &[empty.clone(), pt.clone()]), + Some(Ok(Value::Bool(false))) + ); + assert_eq!( + eval("st_disjoint", &[empty.clone(), pt.clone()]), + Some(Ok(Value::Bool(true))) + ); + assert_eq!( + eval("st_equals", &[empty.clone(), empty.clone()]), + Some(Ok(Value::Bool(true))) + ); + assert_eq!( + eval("st_dwithin", &[empty, pt, Value::Float(1e9)]), + Some(Ok(Value::Bool(false))) + ); + } + + #[test] + fn three_d_within_and_intersects() { + let z = |x: f64, h: f64| { + Value::Geometry( + SridGeometry::with_ordinates( + utm48n(), + Geometry::point(x, 0.0), + Ordinates::new(Some(vec![h]), None), + ) + .unwrap(), + ) + }; + let args = |d: f64| vec![z(500_000.0, 0.0), z(500_003.0, 4.0), Value::Float(d)]; + assert_eq!( + eval("st_3ddwithin", &args(5.0)), + Some(Ok(Value::Bool(true))) + ); + assert_eq!( + eval("st_3ddwithin", &args(4.9)), + Some(Ok(Value::Bool(false))) + ); + assert_eq!(eval("st_dwithin", &args(4.9)), Some(Ok(Value::Bool(true)))); + assert_eq!( + eval("st_3dintersects", &[z(500_000.0, 1.0), z(500_000.0, 1.0)]), + Some(Ok(Value::Bool(true))) + ); + assert_eq!( + eval("st_3dintersects", &[z(500_000.0, 1.0), z(500_000.0, 2.0)]), + Some(Ok(Value::Bool(false))) + ); + } + + #[test] + fn negative_dwithin_distance_fails() { + let args = vec![ + geom(utm48n(), Geometry::point(0.0, 0.0)), + geom(utm48n(), Geometry::point(0.0, 0.0)), + Value::Float(-1.0), + ]; + assert!(matches!( + eval("st_dwithin", &args), + Some(Err(EvalError::ArgumentValue { .. })) + )); + } + + #[test] + fn validity_reason_text() { + let bowtie = geom( + Srid::WGS84, + Geometry::polygon(vec![vec![ + [0.0, 0.0], + [1.0, 1.0], + [1.0, 0.0], + [0.0, 1.0], + [0.0, 0.0], + ]]), + ); + let Some(Ok(Value::String(reason))) = eval("st_isvalidreason", &[bowtie]) else { + panic!(); + }; + assert!(reason.contains("crosses itself"), "{reason}"); + } } diff --git a/nodedb-query/src/geo_functions/srid_refs.rs b/nodedb-query/src/geo_functions/srid_refs.rs new file mode 100644 index 000000000..0ae79f7b5 --- /dev/null +++ b/nodedb-query/src/geo_functions/srid_refs.rs @@ -0,0 +1,168 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Where a stored definition names an SRID as an SRID. +//! +//! A code counts only in an SRID role: +//! +//! - an integer literal in the SRID argument of a geo function, at the +//! position its catalog argument shape declares; +//! - a geometry literal that carries the code: an EWKT `SRID=;` +//! prefix, a GeoJSON `crs` member, hex EWKB, or a native geometry; +//! - a `GEOMETRY(, )` or `GEOGRAPHY(, )` type. +//! +//! The same number in any other role, such as `amount < 910001`, does not +//! count. + +use nodedb_spatial::SridCheck; +use nodedb_types::Value; + +use super::catalog::lookup; +use crate::expr::SqlExpr; + +/// The zero-based SRID argument position of geo function `name`, when its +/// argument shape has one. +pub fn srid_arg_position(name: &str) -> Option { + lookup(name).and_then(|spec| spec.args.srid_position()) +} + +/// The code a literal SRID argument holds, coerced as the evaluator +/// coerces it: an integer, an integral float, or integer text. +pub fn literal_srid_code(value: &Value) -> Option { + match value { + Value::Integer(i) => Some(*i), + Value::Float(f) if f.is_finite() && f.fract() == 0.0 => Some(*f as i64), + Value::String(s) => s.trim().parse::().ok(), + _ => None, + } +} + +/// The SRID a geometry literal carries. `None` for a value that is not a +/// geometry. +pub fn literal_geometry_srid(value: &Value) -> Option { + match value { + Value::Geometry(g) => Some(g.srid.code()), + Value::String(s) => nodedb_spatial::geometry_from_text(s, SridCheck::Syntax) + .ok() + .map(|g| g.srid.code()), + Value::Bytes(b) => nodedb_spatial::geometry_from_bytes(b, SridCheck::Syntax) + .ok() + .map(|g| g.srid.code()), + _ => None, + } +} + +/// Whether `expr` names `code` in an SRID role. +pub fn expr_names_srid(expr: &SqlExpr, code: u32) -> bool { + let names = |e: &SqlExpr| expr_names_srid(e, code); + match expr { + SqlExpr::Literal(value) => literal_geometry_srid(value) == Some(code), + SqlExpr::Function { name, args } => { + let at = srid_arg_position(name); + args.iter().enumerate().any(|(i, arg)| { + (Some(i) == at && srid_literal(arg) == Some(i64::from(code))) || names(arg) + }) + } + SqlExpr::BinaryOp { left, right, .. } => names(left) || names(right), + SqlExpr::Negate(inner) | SqlExpr::Cast { expr: inner, .. } => names(inner), + SqlExpr::IsNull { expr: inner, .. } => names(inner), + SqlExpr::Case { + operand, + when_thens, + else_expr, + } => { + operand.as_deref().is_some_and(names) + || when_thens.iter().any(|(w, t)| names(w) || names(t)) + || else_expr.as_deref().is_some_and(names) + } + SqlExpr::Coalesce(items) => items.iter().any(names), + SqlExpr::NullIf(a, b) => names(a) || names(b), + SqlExpr::Column(_) | SqlExpr::OldColumn(_) | SqlExpr::ExcludedColumn(_) => false, + } +} + +/// The code an SRID argument holds when it is a literal, through casts. +fn srid_literal(expr: &SqlExpr) -> Option { + match expr { + SqlExpr::Literal(value) => literal_srid_code(value), + SqlExpr::Cast { expr, .. } => srid_literal(expr), + _ => None, + } +} + +/// Whether type text declares `code`: `GEOMETRY(, )` or +/// `GEOGRAPHY(, )`, alone or as one `|` alternative. +pub fn type_names_srid(type_text: &str, code: u32) -> bool { + type_text + .split('|') + .any(|alternative| type_srid(alternative) == Some(code)) +} + +/// The SRID typmod of one geometry type. +fn type_srid(type_text: &str) -> Option { + let text = type_text.trim(); + let open = text.find('(')?; + let name = text[..open].trim(); + if !(name.eq_ignore_ascii_case("geometry") || name.eq_ignore_ascii_case("geography")) { + return None; + } + let inner = text[open + 1..].strip_suffix(')')?; + inner.rsplit(',').next()?.trim().parse::().ok() +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::expr_parse::parse_generated_expr; + + const CODE: u32 = 910_042; + + fn names(text: &str) -> bool { + let (expr, _) = parse_generated_expr(text).expect("parses"); + expr_names_srid(&expr, CODE) + } + + #[test] + fn srid_arguments_name_the_code() { + assert!(names("ST_Transform(geom, 910042)")); + assert!(names("ST_SetSRID(geom, 910042)")); + assert!(names("st_geomfromtext('POINT(1 2)', 910042)")); + assert!(names("ST_SetSRID(geom, '910042')")); + assert!(names("ST_SRID(ST_Transform(geom, 910042)) = 1")); + } + + #[test] + fn geometry_literals_name_their_srid() { + assert!(names("ST_Distance(geom, 'SRID=910042;POINT(1 2)')")); + assert!(names( + "ST_Distance(geom, '{\"type\":\"Point\",\"coordinates\":[1,2],\ + \"crs\":{\"type\":\"name\",\"properties\":{\"name\":\"NODEDB:910042\"}}}')" + )); + } + + #[test] + fn the_number_in_another_role_does_not_count() { + assert!(!names("amount < 910042")); + assert!(!names("ST_SRID(geom) = 910042")); + assert!(!names("ST_Buffer(geom, 910042)")); + assert!(!names("ST_Transform(geom, 910043)")); + assert!(!names("'910042'")); + } + + #[test] + fn geometry_typmods_name_their_srid() { + assert!(type_names_srid("GEOMETRY(Point, 910042)", CODE)); + assert!(type_names_srid("geography(POLYGON,910042)|NULL", CODE)); + assert!(!type_names_srid("GEOMETRY", CODE)); + assert!(!type_names_srid("DECIMAL(10, 910042)", CODE)); + assert!(!type_names_srid("GEOMETRY(Point, 4326)", CODE)); + } + + #[test] + fn every_srid_shape_has_its_position() { + assert_eq!(srid_arg_position("ST_Transform"), Some(1)); + assert_eq!(srid_arg_position("st_geomfromwkb"), Some(1)); + assert_eq!(srid_arg_position("ST_MakeEnvelope"), None); + assert_eq!(srid_arg_position("ST_Buffer"), None); + assert_eq!(srid_arg_position("upper"), None); + } +} diff --git a/nodedb-query/src/json_expr.rs b/nodedb-query/src/json_expr.rs index ec55cc236..c7c2ee5e8 100644 --- a/nodedb-query/src/json_expr.rs +++ b/nodedb-query/src/json_expr.rs @@ -19,13 +19,14 @@ use crate::json_ops::compare_json_numbers; pub fn eval_expr_on_json( expr: &SqlExpr, doc: &serde_json::Value, + crs: &nodedb_crs::CrsContext<'_>, ) -> Result { match expr { SqlExpr::Column(name) => Ok(doc.get(name).cloned().unwrap_or(serde_json::Value::Null)), SqlExpr::Literal(v) => Ok(serde_json::Value::from(v.clone())), other => { let row = nodedb_types::Value::from(doc.clone()); - Ok(serde_json::Value::from(other.eval(&row)?)) + Ok(serde_json::Value::from(other.eval(&row, crs)?)) } } } diff --git a/nodedb-query/src/msgpack_scan/aggregate_helpers.rs b/nodedb-query/src/msgpack_scan/aggregate_helpers.rs index 904222530..7fd518abd 100644 --- a/nodedb-query/src/msgpack_scan/aggregate_helpers.rs +++ b/nodedb-query/src/msgpack_scan/aggregate_helpers.rs @@ -6,8 +6,10 @@ //! streaming aggregate accumulators in the `nodedb` crate. //! Each function operates on a single raw MessagePack document byte slice and //! returns only the scalar value needed by the calling accumulator — no -//! document bytes are retained after the call returns. +//! document bytes are retained after the call returns. Geo functions in an +//! argument expression resolve SRIDs through the `crs` argument. +use nodedb_crs::CrsContext; use nodedb_types::Value; use crate::expr::{EvalError, SqlExpr}; @@ -29,11 +31,11 @@ use crate::value_ops; /// surfaces as SQLSTATE `22012`, exactly like a WHERE/projection expression, /// rather than silently dropping the row from the aggregate. #[inline] -fn eval_expr(doc: &[u8], expr: &SqlExpr) -> Result, EvalError> { +fn eval_expr(doc: &[u8], expr: &SqlExpr, crs: &CrsContext<'_>) -> Result, EvalError> { let Ok(doc_val) = nodedb_types::json_msgpack::value_from_msgpack(doc) else { return Ok(None); }; - Ok(Some(expr.eval(&doc_val)?)) + Ok(Some(expr.eval(&doc_val, crs)?)) } // ── Public extraction helpers ────────────────────────────────────────────── @@ -46,9 +48,10 @@ pub fn extract_f64( doc: &[u8], field: &str, expr: Option<&SqlExpr>, + crs: &CrsContext<'_>, ) -> Result, EvalError> { if let Some(expr) = expr { - return Ok(eval_expr(doc, expr)?.and_then(|v| value_ops::value_to_f64(&v, false))); + return Ok(eval_expr(doc, expr, crs)?.and_then(|v| value_ops::value_to_f64(&v, false))); } let Some((start, _end)) = extract_field(doc, 0, field) else { return Ok(None); @@ -69,9 +72,10 @@ pub fn extract_sum_value( doc: &[u8], field: &str, expr: Option<&SqlExpr>, + crs: &CrsContext<'_>, ) -> Result, EvalError> { if let Some(expr) = expr { - return Ok(eval_expr(doc, expr)?.and_then(|v| crate::numeric_sum::sum_input(&v))); + return Ok(eval_expr(doc, expr, crs)?.and_then(|v| crate::numeric_sum::sum_input(&v))); } let Some((start, _end)) = extract_field(doc, 0, field) else { return Ok(None); @@ -88,9 +92,10 @@ pub fn extract_str( doc: &[u8], field: &str, expr: Option<&SqlExpr>, + crs: &CrsContext<'_>, ) -> Result, EvalError> { if let Some(expr) = expr { - return Ok(eval_expr(doc, expr)?.map(|v| value_ops::value_to_display_string(&v))); + return Ok(eval_expr(doc, expr, crs)?.map(|v| value_ops::value_to_display_string(&v))); } let Some((start, _end)) = extract_field(doc, 0, field) else { return Ok(None); @@ -104,9 +109,10 @@ pub fn extract_value( doc: &[u8], field: &str, expr: Option<&SqlExpr>, + crs: &CrsContext<'_>, ) -> Result, EvalError> { if let Some(expr) = expr { - return eval_expr(doc, expr); + return eval_expr(doc, expr, crs); } let Some((start, end)) = extract_field(doc, 0, field) else { return Ok(None); @@ -124,9 +130,10 @@ pub fn extract_bytes( doc: &[u8], field: &str, expr: Option<&SqlExpr>, + crs: &CrsContext<'_>, ) -> Result>, EvalError> { if let Some(expr) = expr { - let Some(val) = eval_expr(doc, expr)? else { + let Some(val) = eval_expr(doc, expr, crs)? else { return Ok(None); }; return Ok(nodedb_types::json_msgpack::value_to_msgpack(&val).ok()); @@ -144,8 +151,9 @@ pub fn extract_non_null( doc: &[u8], field: &str, expr: Option<&SqlExpr>, + crs: &CrsContext<'_>, ) -> Result, EvalError> { - let Some(v) = extract_value(doc, field, expr)? else { + let Some(v) = extract_value(doc, field, expr, crs)? else { return Ok(None); }; Ok(if v.is_null() { None } else { Some(()) }) diff --git a/nodedb-query/src/msgpack_scan/filter.rs b/nodedb-query/src/msgpack_scan/filter.rs index 27d3c0380..e5b882961 100644 --- a/nodedb-query/src/msgpack_scan/filter.rs +++ b/nodedb-query/src/msgpack_scan/filter.rs @@ -9,6 +9,8 @@ use std::cmp::Ordering; +use nodedb_crs::CrsContext; + use crate::expr::EvalError; use crate::msgpack_scan::field::extract_field; use crate::msgpack_scan::index::FieldIndex; @@ -29,9 +31,13 @@ impl ScanFilter { /// `filters.iter().all(|f| f.matches_binary(doc))` have a single /// drop-in replacement instead of each hand-rolling the same /// short-circuit loop. - pub fn all_match_binary(group: &[ScanFilter], doc: &[u8]) -> Result { + pub fn all_match_binary( + group: &[ScanFilter], + doc: &[u8], + crs: &CrsContext<'_>, + ) -> Result { for f in group { - if !f.matches_binary(doc)? { + if !f.matches_binary(doc, crs)? { return Ok(false); } } @@ -43,9 +49,10 @@ impl ScanFilter { group: &[ScanFilter], doc: &[u8], idx: &FieldIndex, + crs: &CrsContext<'_>, ) -> Result { for f in group { - if !f.matches_binary_indexed(doc, idx)? { + if !f.matches_binary_indexed(doc, idx, crs)? { return Ok(false); } } @@ -63,12 +70,14 @@ impl ScanFilter { /// takes a modulus by zero — see /// `scan_filter::ScanFilter::matches_value` for the WHERE-clause /// behavior-flip rationale, which applies identically here. - pub fn matches_binary(&self, doc: &[u8]) -> Result { + /// + /// Geo functions in an expression filter resolve SRIDs through `crs`. + pub fn matches_binary(&self, doc: &[u8], crs: &CrsContext<'_>) -> Result { match self.op { FilterOp::MatchAll | FilterOp::Exists | FilterOp::NotExists => return Ok(true), FilterOp::Or => { for clause in &self.clauses { - if Self::all_match_binary(clause, doc)? { + if Self::all_match_binary(clause, doc, crs)? { return Ok(true); } } @@ -76,7 +85,9 @@ impl ScanFilter { } FilterOp::Expr => { return match (self.expr.as_ref(), nodedb_types::value_from_msgpack(doc)) { - (Some(expr), Ok(value)) => Ok(crate::value_ops::is_truthy(&expr.eval(&value)?)), + (Some(expr), Ok(value)) => { + Ok(crate::value_ops::is_truthy(&expr.eval(&value, crs)?)) + } _ => Ok(false), }; } @@ -124,12 +135,17 @@ impl ScanFilter { /// Evaluate using a pre-built `FieldIndex` for O(1) field lookup. /// /// Use when evaluating multiple predicates on the same document. - pub fn matches_binary_indexed(&self, doc: &[u8], idx: &FieldIndex) -> Result { + pub fn matches_binary_indexed( + &self, + doc: &[u8], + idx: &FieldIndex, + crs: &CrsContext<'_>, + ) -> Result { match self.op { FilterOp::MatchAll | FilterOp::Exists | FilterOp::NotExists => return Ok(true), FilterOp::Or => { for clause in &self.clauses { - if Self::all_match_binary_indexed(clause, doc, idx)? { + if Self::all_match_binary_indexed(clause, doc, idx, crs)? { return Ok(true); } } @@ -137,7 +153,9 @@ impl ScanFilter { } FilterOp::Expr => { return match (self.expr.as_ref(), nodedb_types::value_from_msgpack(doc)) { - (Some(expr), Ok(value)) => Ok(crate::value_ops::is_truthy(&expr.eval(&value)?)), + (Some(expr), Ok(value)) => { + Ok(crate::value_ops::is_truthy(&expr.eval(&value, crs)?)) + } _ => Ok(false), }; } @@ -356,6 +374,10 @@ fn array_any(buf: &[u8], start: usize, mut pred: impl FnMut(usize) -> bool) -> b #[cfg(test)] mod tests { use super::*; + + fn crs() -> nodedb_crs::CrsContext<'static> { + nodedb_crs::CrsContext::builtin_only() + } use serde_json::json; fn encode(v: &serde_json::Value) -> Vec { @@ -377,12 +399,12 @@ mod tests { let doc = encode(&json!({"age": 25})); assert!( filter("age", "eq", nodedb_types::Value::Integer(25)) - .matches_binary(&doc) + .matches_binary(&doc, &crs()) .unwrap() ); assert!( !filter("age", "eq", nodedb_types::Value::Integer(30)) - .matches_binary(&doc) + .matches_binary(&doc, &crs()) .unwrap() ); } @@ -401,24 +423,32 @@ mod tests { let later = Value::NaiveDateTime(NdbDateTime::from_micros(MICROS + 1)); assert!( filter("ts", "eq", same.clone()) - .matches_binary(&doc) + .matches_binary(&doc, &crs()) .unwrap() ); assert!( !filter("ts", "eq", later.clone()) - .matches_binary(&doc) + .matches_binary(&doc, &crs()) + .unwrap() + ); + assert!( + filter("ts", "lt", later) + .matches_binary(&doc, &crs()) + .unwrap() + ); + assert!( + filter("ts", "gte", same) + .matches_binary(&doc, &crs()) .unwrap() ); - assert!(filter("ts", "lt", later).matches_binary(&doc).unwrap()); - assert!(filter("ts", "gte", same).matches_binary(&doc).unwrap()); assert!( filter("ts", "eq", Value::String("2020-03-05T10:00:00Z".into())) - .matches_binary(&doc) + .matches_binary(&doc, &crs()) .unwrap() ); assert!( filter("ts", "gt", Value::String("2020-03-05 09:00:00".into())) - .matches_binary(&doc) + .matches_binary(&doc, &crs()) .unwrap() ); let idx = FieldIndex::build(&doc, 0).unwrap_or_else(FieldIndex::empty); @@ -428,7 +458,7 @@ mod tests { "eq", Value::NaiveDateTime(NdbDateTime::from_micros(MICROS)) ) - .matches_binary_indexed(&doc, &idx) + .matches_binary_indexed(&doc, &idx, &crs()) .unwrap() ); } @@ -438,7 +468,7 @@ mod tests { let doc = encode(&json!({"age": 25})); assert!( filter("age", "eq", nodedb_types::Value::String("25".into())) - .matches_binary(&doc) + .matches_binary(&doc, &crs()) .unwrap() ); } @@ -448,7 +478,7 @@ mod tests { let doc = encode(&json!({"score": "90"})); assert!( filter("score", "gt", nodedb_types::Value::Integer(80)) - .matches_binary(&doc) + .matches_binary(&doc, &crs()) .unwrap() ); } @@ -458,7 +488,7 @@ mod tests { let doc = encode(&json!({"name": "alice"})); assert!( filter("name", "eq", nodedb_types::Value::String("alice".into())) - .matches_binary(&doc) + .matches_binary(&doc, &crs()) .unwrap() ); } @@ -468,7 +498,7 @@ mod tests { let doc = encode(&json!({"age": 25})); assert!( filter("age", "eq", nodedb_types::Value::String("25".into())) - .matches_binary(&doc) + .matches_binary(&doc, &crs()) .unwrap() ); } @@ -478,7 +508,7 @@ mod tests { let doc = encode(&json!({"score": "90"})); assert!( filter("score", "eq", nodedb_types::Value::Integer(90)) - .matches_binary(&doc) + .matches_binary(&doc, &crs()) .unwrap() ); } @@ -488,12 +518,12 @@ mod tests { let doc = encode(&json!({"x": 1})); assert!( filter("x", "ne", nodedb_types::Value::Integer(2)) - .matches_binary(&doc) + .matches_binary(&doc, &crs()) .unwrap() ); assert!( !filter("x", "ne", nodedb_types::Value::Integer(1)) - .matches_binary(&doc) + .matches_binary(&doc, &crs()) .unwrap() ); } @@ -503,22 +533,22 @@ mod tests { let doc = encode(&json!({"v": 10})); assert!( filter("v", "gt", nodedb_types::Value::Integer(5)) - .matches_binary(&doc) + .matches_binary(&doc, &crs()) .unwrap() ); assert!( !filter("v", "gt", nodedb_types::Value::Integer(15)) - .matches_binary(&doc) + .matches_binary(&doc, &crs()) .unwrap() ); assert!( filter("v", "lt", nodedb_types::Value::Integer(15)) - .matches_binary(&doc) + .matches_binary(&doc, &crs()) .unwrap() ); assert!( !filter("v", "lt", nodedb_types::Value::Integer(5)) - .matches_binary(&doc) + .matches_binary(&doc, &crs()) .unwrap() ); } @@ -528,22 +558,22 @@ mod tests { let doc = encode(&json!({"v": 10})); assert!( filter("v", "gte", nodedb_types::Value::Integer(10)) - .matches_binary(&doc) + .matches_binary(&doc, &crs()) .unwrap() ); assert!( filter("v", "gte", nodedb_types::Value::Integer(5)) - .matches_binary(&doc) + .matches_binary(&doc, &crs()) .unwrap() ); assert!( !filter("v", "gte", nodedb_types::Value::Integer(15)) - .matches_binary(&doc) + .matches_binary(&doc, &crs()) .unwrap() ); assert!( filter("v", "lte", nodedb_types::Value::Integer(10)) - .matches_binary(&doc) + .matches_binary(&doc, &crs()) .unwrap() ); } @@ -553,17 +583,17 @@ mod tests { let doc = encode(&json!({"a": null, "b": 1})); assert!( filter("a", "is_null", nodedb_types::Value::Null) - .matches_binary(&doc) + .matches_binary(&doc, &crs()) .unwrap() ); assert!( !filter("b", "is_null", nodedb_types::Value::Null) - .matches_binary(&doc) + .matches_binary(&doc, &crs()) .unwrap() ); assert!( filter("b", "is_not_null", nodedb_types::Value::Null) - .matches_binary(&doc) + .matches_binary(&doc, &crs()) .unwrap() ); } @@ -573,7 +603,7 @@ mod tests { let doc = encode(&json!({"x": 1})); assert!( filter("missing", "is_null", nodedb_types::Value::Null) - .matches_binary(&doc) + .matches_binary(&doc, &crs()) .unwrap() ); } @@ -587,7 +617,7 @@ mod tests { "contains", nodedb_types::Value::String("world".into()) ) - .matches_binary(&doc) + .matches_binary(&doc, &crs()) .unwrap() ); } @@ -597,17 +627,17 @@ mod tests { let doc = encode(&json!({"name": "Alice"})); assert!( filter("name", "like", nodedb_types::Value::String("Ali%".into())) - .matches_binary(&doc) + .matches_binary(&doc, &crs()) .unwrap() ); assert!( !filter("name", "like", nodedb_types::Value::String("ali%".into())) - .matches_binary(&doc) + .matches_binary(&doc, &crs()) .unwrap() ); assert!( filter("name", "ilike", nodedb_types::Value::String("ali%".into())) - .matches_binary(&doc) + .matches_binary(&doc, &crs()) .unwrap() ); assert!( @@ -616,7 +646,7 @@ mod tests { "not_like", nodedb_types::Value::String("Bob%".into()) ) - .matches_binary(&doc) + .matches_binary(&doc, &crs()) .unwrap() ); } @@ -636,7 +666,7 @@ mod tests { clauses: vec![], expr: None } - .matches_binary(&doc) + .matches_binary(&doc, &crs()) .unwrap() ); @@ -649,7 +679,7 @@ mod tests { clauses: vec![], expr: None } - .matches_binary(&doc2) + .matches_binary(&doc2, &crs()) .unwrap() ); } @@ -663,7 +693,7 @@ mod tests { "array_contains", nodedb_types::Value::String("rust".into()) ) - .matches_binary(&doc) + .matches_binary(&doc, &crs()) .unwrap() ); assert!( @@ -672,7 +702,7 @@ mod tests { "array_contains", nodedb_types::Value::String("slow".into()) ) - .matches_binary(&doc) + .matches_binary(&doc, &crs()) .unwrap() ); } @@ -692,7 +722,7 @@ mod tests { clauses: vec![], expr: None } - .matches_binary(&doc) + .matches_binary(&doc, &crs()) .unwrap() ); } @@ -712,7 +742,7 @@ mod tests { clauses: vec![], expr: None } - .matches_binary(&doc) + .matches_binary(&doc, &crs()) .unwrap() ); } @@ -730,7 +760,7 @@ mod tests { ], expr: None, }; - assert!(f.matches_binary(&doc).unwrap()); + assert!(f.matches_binary(&doc, &crs()).unwrap()); } #[test] @@ -738,7 +768,7 @@ mod tests { let doc = encode(&json!({"any": "thing"})); assert!( filter("", "match_all", nodedb_types::Value::Null) - .matches_binary(&doc) + .matches_binary(&doc, &crs()) .unwrap() ); } @@ -748,12 +778,12 @@ mod tests { let doc = encode(&json!({"temp": 36.6})); assert!( filter("temp", "gt", nodedb_types::Value::Float(30.0)) - .matches_binary(&doc) + .matches_binary(&doc, &crs()) .unwrap() ); assert!( filter("temp", "lt", nodedb_types::Value::Float(40.0)) - .matches_binary(&doc) + .matches_binary(&doc, &crs()) .unwrap() ); } @@ -763,12 +793,12 @@ mod tests { let doc = encode(&json!({"active": true})); assert!( filter("active", "eq", nodedb_types::Value::Bool(true)) - .matches_binary(&doc) + .matches_binary(&doc, &crs()) .unwrap() ); assert!( !filter("active", "eq", nodedb_types::Value::Bool(false)) - .matches_binary(&doc) + .matches_binary(&doc, &crs()) .unwrap() ); } @@ -778,7 +808,7 @@ mod tests { let doc = encode(&json!({"score": "90"})); assert!( filter("score", "gt", nodedb_types::Value::Integer(80)) - .matches_binary(&doc) + .matches_binary(&doc, &crs()) .unwrap() ); } @@ -792,7 +822,7 @@ mod tests { "lte", nodedb_types::Value::String("2026-07-02 14:00:00".into()) ) - .matches_binary(&doc) + .matches_binary(&doc, &crs()) .unwrap() ); assert!( @@ -801,7 +831,7 @@ mod tests { "lt", nodedb_types::Value::String("2026-07-02 14:00:00".into()) ) - .matches_binary(&doc) + .matches_binary(&doc, &crs()) .unwrap() ); assert!( @@ -810,7 +840,7 @@ mod tests { "gte", nodedb_types::Value::String("2026-07-02 12:00:00".into()) ) - .matches_binary(&doc) + .matches_binary(&doc, &crs()) .unwrap() ); assert!( @@ -819,7 +849,7 @@ mod tests { "eq", nodedb_types::Value::String("2026-07-02 13:00:00".into()) ) - .matches_binary(&doc) + .matches_binary(&doc, &crs()) .unwrap() ); } @@ -841,8 +871,8 @@ mod tests { for f in &filters { assert_eq!( - f.matches_binary(&doc).unwrap(), - f.matches_binary_indexed(&doc, &idx).unwrap(), + f.matches_binary(&doc, &crs()).unwrap(), + f.matches_binary_indexed(&doc, &idx, &crs()).unwrap(), "mismatch for field={} op={:?}", f.field, f.op diff --git a/nodedb-query/src/msgpack_scan/group_key.rs b/nodedb-query/src/msgpack_scan/group_key.rs index 2f1ef33fd..bf64b9195 100644 --- a/nodedb-query/src/msgpack_scan/group_key.rs +++ b/nodedb-query/src/msgpack_scan/group_key.rs @@ -5,6 +5,7 @@ //! Builds a deterministic string key from field values extracted directly //! from msgpack bytes, avoiding full document decode. +use nodedb_crs::CrsContext; use nodedb_types::{NdbDateTime, read_instant}; use crate::expr::{EvalError, GroupKeySpec, SqlExpr}; @@ -31,7 +32,13 @@ use crate::msgpack_scan::reader::{read_f64, read_integer, read_null, read_str}; /// `b = 0` fails the whole statement with SQLSTATE `22012`, exactly like a /// WHERE/projection expression, instead of silently grouping the row under a /// `null` key. -pub fn build_group_key(doc: &[u8], group_keys: &[GroupKeySpec]) -> Result { +/// +/// Geo functions in a computed key resolve SRIDs through `crs`. +pub fn build_group_key( + doc: &[u8], + group_keys: &[GroupKeySpec], + crs: &CrsContext<'_>, +) -> Result { if group_keys.is_empty() { return Ok("__all__".to_string()); } @@ -50,7 +57,7 @@ pub fn build_group_key(doc: &[u8], group_keys: &[GroupKeySpec]) -> Result 0 { key_buf.push(','); } - append_computed_value(&mut key_buf, doc, expr)?; + append_computed_value(&mut key_buf, doc, expr, crs)?; written += 1; } } @@ -65,6 +72,7 @@ pub fn build_group_key_indexed( doc: &[u8], group_keys: &[GroupKeySpec], idx: &FieldIndex, + crs: &CrsContext<'_>, ) -> Result { if group_keys.is_empty() { return Ok("__all__".to_string()); @@ -87,7 +95,7 @@ pub fn build_group_key_indexed( if written > 0 { key_buf.push(','); } - append_computed_value(&mut key_buf, doc, expr)?; + append_computed_value(&mut key_buf, doc, expr, crs)?; written += 1; } } @@ -151,12 +159,17 @@ fn append_field_value_range(buf: &mut String, doc: &[u8], range: Option<(usize, /// returns `Err(EvalError::DivisionByZero)`: a computed GROUP BY /// key gets the same `22012` statement-failure treatment as a WHERE/projection /// expression rather than silently grouping the row under a `null` key. -fn append_computed_value(buf: &mut String, doc: &[u8], expr: &SqlExpr) -> Result<(), EvalError> { +fn append_computed_value( + buf: &mut String, + doc: &[u8], + expr: &SqlExpr, + crs: &CrsContext<'_>, +) -> Result<(), EvalError> { let Ok(doc_val) = nodedb_types::json_msgpack::value_from_msgpack(doc) else { buf.push_str("null"); return Ok(()); }; - let val = expr.eval(&doc_val)?; + let val = expr.eval(&doc_val, crs)?; match nodedb_types::json_msgpack::value_to_msgpack(&val) { Ok(vb) => append_value_at(buf, &vb, 0, vb.len()), Err(_) => buf.push_str("null"), @@ -167,6 +180,10 @@ fn append_computed_value(buf: &mut String, doc: &[u8], expr: &SqlExpr) -> Result #[cfg(test)] mod tests { use super::*; + + fn crs() -> CrsContext<'static> { + CrsContext::builtin_only() + } use serde_json::json; fn encode(v: &serde_json::Value) -> Vec { @@ -180,14 +197,14 @@ mod tests { #[test] fn single_string_field() { let doc = encode(&json!({"name": "alice", "age": 30})); - let key = build_group_key(&doc, &keys(&["name"])).unwrap(); + let key = build_group_key(&doc, &keys(&["name"]), &crs()).unwrap(); assert_eq!(key, r#"["alice"]"#); } #[test] fn single_int_field() { let doc = encode(&json!({"status": 200})); - let key = build_group_key(&doc, &keys(&["status"])).unwrap(); + let key = build_group_key(&doc, &keys(&["status"]), &crs()).unwrap(); assert_eq!(key, "[200]"); } @@ -196,48 +213,48 @@ mod tests { #[test] fn large_integers_key_exactly() { let doc = encode(&json!({"v": u64::MAX})); - let key = build_group_key(&doc, &keys(&["v"])).unwrap(); + let key = build_group_key(&doc, &keys(&["v"]), &crs()).unwrap(); assert_eq!(key, "[18446744073709551615]"); let a = encode(&json!({"v": 9_007_199_254_740_993_i64})); let b = encode(&json!({"v": 9_007_199_254_740_992_i64})); assert_ne!( - build_group_key(&a, &keys(&["v"])).unwrap(), - build_group_key(&b, &keys(&["v"])).unwrap() + build_group_key(&a, &keys(&["v"]), &crs()).unwrap(), + build_group_key(&b, &keys(&["v"]), &crs()).unwrap() ); } #[test] fn multiple_fields() { let doc = encode(&json!({"city": "ny", "year": 2024})); - let key = build_group_key(&doc, &keys(&["city", "year"])).unwrap(); + let key = build_group_key(&doc, &keys(&["city", "year"]), &crs()).unwrap(); assert_eq!(key, r#"["ny",2024]"#); } #[test] fn missing_field_is_null() { let doc = encode(&json!({"x": 1})); - let key = build_group_key(&doc, &keys(&["missing"])).unwrap(); + let key = build_group_key(&doc, &keys(&["missing"]), &crs()).unwrap(); assert_eq!(key, "[null]"); } #[test] fn empty_group_fields() { let doc = encode(&json!({"x": 1})); - let key = build_group_key(&doc, &[]).unwrap(); + let key = build_group_key(&doc, &[], &crs()).unwrap(); assert_eq!(key, "__all__"); } #[test] fn null_field_value() { let doc = encode(&json!({"v": null})); - let key = build_group_key(&doc, &keys(&["v"])).unwrap(); + let key = build_group_key(&doc, &keys(&["v"]), &crs()).unwrap(); assert_eq!(key, "[null]"); } #[test] fn float_field() { let doc = encode(&json!({"temp": 36.6})); - let key = build_group_key(&doc, &keys(&["temp"])).unwrap(); + let key = build_group_key(&doc, &keys(&["temp"]), &crs()).unwrap(); assert_eq!(key, "[36.6]"); } @@ -256,11 +273,11 @@ mod tests { ); crate::msgpack_scan::write_kv_i64(&mut doc, "n", 1); - let key = build_group_key(&doc, &keys(&["ts", "n"])).unwrap(); + let key = build_group_key(&doc, &keys(&["ts", "n"]), &crs()).unwrap(); assert_eq!(key, r#"["2020-03-05T10:00:00.000000Z",1]"#); let idx = FieldIndex::build(&doc, 0).unwrap_or_else(FieldIndex::empty); assert_eq!( - build_group_key_indexed(&doc, &keys(&["ts", "n"]), &idx).unwrap(), + build_group_key_indexed(&doc, &keys(&["ts", "n"]), &idx, &crs()).unwrap(), key ); } @@ -283,11 +300,11 @@ mod tests { let lower = encode(&json!({"label": "alpha", "score": 7})); let upper = encode(&json!({"label": "ALPHA", "score": 3})); assert_eq!( - build_group_key(&lower, &[upper_label_spec()]).unwrap(), + build_group_key(&lower, &[upper_label_spec()], &crs()).unwrap(), r#"["ALPHA"]"# ); assert_eq!( - build_group_key(&upper, &[upper_label_spec()]).unwrap(), + build_group_key(&upper, &[upper_label_spec()], &crs()).unwrap(), r#"["ALPHA"]"# ); } @@ -308,12 +325,12 @@ mod tests { }), }; assert_eq!( - build_group_key(&doc, std::slice::from_ref(&spec)), + build_group_key(&doc, std::slice::from_ref(&spec), &crs()), Err(EvalError::DivisionByZero) ); let idx = FieldIndex::build(&doc, 0).unwrap_or_else(FieldIndex::empty); assert_eq!( - build_group_key_indexed(&doc, std::slice::from_ref(&spec), &idx), + build_group_key_indexed(&doc, std::slice::from_ref(&spec), &idx, &crs()), Err(EvalError::DivisionByZero) ); } @@ -324,8 +341,8 @@ mod tests { let specs = [upper_label_spec()]; let idx = FieldIndex::build(&doc, 0).unwrap_or_else(FieldIndex::empty); assert_eq!( - build_group_key(&doc, &specs).unwrap(), - build_group_key_indexed(&doc, &specs, &idx).unwrap(), + build_group_key(&doc, &specs, &crs()).unwrap(), + build_group_key_indexed(&doc, &specs, &idx, &crs()).unwrap(), ); } @@ -333,6 +350,9 @@ mod tests { fn mixed_column_and_computed_keys() { let doc = encode(&json!({"region": "us", "label": "west"})); let specs = [GroupKeySpec::column("region"), upper_label_spec()]; - assert_eq!(build_group_key(&doc, &specs).unwrap(), r#"["us","WEST"]"#); + assert_eq!( + build_group_key(&doc, &specs, &crs()).unwrap(), + r#"["us","WEST"]"# + ); } } diff --git a/nodedb-query/src/scan_filter/types.rs b/nodedb-query/src/scan_filter/types.rs index 1d94abbf7..93cb7a913 100644 --- a/nodedb-query/src/scan_filter/types.rs +++ b/nodedb-query/src/scan_filter/types.rs @@ -84,9 +84,10 @@ impl ScanFilter { pub fn all_match_value( group: &[ScanFilter], doc: &nodedb_types::Value, + crs: &nodedb_crs::CrsContext<'_>, ) -> Result { for f in group { - if !f.matches_value(doc)? { + if !f.matches_value(doc, crs)? { return Ok(false); } } @@ -105,12 +106,18 @@ impl ScanFilter { /// `10 / denom > 1` must fail the whole query with SQLSTATE `22012`, /// matching Postgres, rather than evaluate to `Value::Null`/`false` /// (silently filtering the row out) when `denom` is `0`. - pub fn matches_value(&self, doc: &nodedb_types::Value) -> Result { + /// + /// Geo functions in an expression filter resolve SRIDs through `crs`. + pub fn matches_value( + &self, + doc: &nodedb_types::Value, + crs: &nodedb_crs::CrsContext<'_>, + ) -> Result { match self.op { FilterOp::MatchAll | FilterOp::Exists | FilterOp::NotExists => return Ok(true), FilterOp::Or => { for clause in &self.clauses { - if Self::all_match_value(clause, doc)? { + if Self::all_match_value(clause, doc, crs)? { return Ok(true); } } @@ -118,7 +125,7 @@ impl ScanFilter { } FilterOp::Expr => { return match &self.expr { - Some(expr) => Ok(crate::value_ops::is_truthy(&expr.eval(doc)?)), + Some(expr) => Ok(crate::value_ops::is_truthy(&expr.eval(doc, crs)?)), None => Ok(false), }; } diff --git a/nodedb-query/src/window/aggregate.rs b/nodedb-query/src/window/aggregate.rs index c091e6a87..75e335dc8 100644 --- a/nodedb-query/src/window/aggregate.rs +++ b/nodedb-query/src/window/aggregate.rs @@ -12,6 +12,7 @@ //! //! A float result keeps NaN and ±Infinity, as in PostgreSQL. +use nodedb_crs::CrsContext; use nodedb_types::Value; use super::arg::{ArgValues, arg_at, eval_arg_values}; @@ -26,11 +27,12 @@ pub(super) fn apply_aggregate_window( rows: &mut [(String, Value)], indices: &[usize], spec: &WindowFuncSpec, + crs: &CrsContext<'_>, ) -> Result<(), crate::expr::EvalError> { // The argument is an expression evaluated per row, not a column name: a // zero divisor inside it fails the statement here rather than folding the // aggregate to a silently-wrong total. - let arg_values = eval_arg_values(rows, indices, spec, 0)?; + let arg_values = eval_arg_values(rows, indices, spec, 0, crs)?; // Fast path: RANGE UNBOUNDED PRECEDING TO CURRENT ROW is the most common // pattern (the PostgreSQL default for ordered windows). Use the running @@ -40,11 +42,11 @@ pub(super) fn apply_aggregate_window( && matches!(spec.frame.end, FrameBound::CurrentRow); if use_running { - running_aggregate(rows, indices, spec, &arg_values)?; + running_aggregate(rows, indices, spec, &arg_values, crs)?; return Ok(()); } - per_row_aggregate(rows, indices, spec, &arg_values) + per_row_aggregate(rows, indices, spec, &arg_values, crs) } /// Per-row frame evaluator. @@ -59,6 +61,7 @@ fn per_row_aggregate( indices: &[usize], spec: &WindowFuncSpec, arg_values: &ArgValues, + crs: &CrsContext<'_>, ) -> Result<(), crate::expr::EvalError> { let len = indices.len(); if len == 0 { @@ -70,7 +73,7 @@ fn per_row_aggregate( let order_values: Vec = indices .iter() .map(|&i| match order_expr { - Some(expr) => expr.eval(&rows[i].1), + Some(expr) => expr.eval(&rows[i].1, crs), None => Ok(Value::Null), }) .collect::, _>>()?; @@ -169,9 +172,14 @@ mod tests { use super::super::spec::{FrameBound, WindowFrame, WindowFuncSpec}; use super::apply_aggregate_window; use crate::expr::SqlExpr; + use nodedb_crs::CrsContext; use nodedb_types::Value; use serde_json::json; + fn crs() -> CrsContext<'static> { + CrsContext::builtin_only() + } + fn v(j: serde_json::Value) -> Value { Value::from(j) } @@ -236,7 +244,7 @@ mod tests { "n", rows_frame(FrameBound::Preceding(1), FrameBound::Following(1)), ); - apply_aggregate_window(&mut rows, &indices, &spec).unwrap(); + apply_aggregate_window(&mut rows, &indices, &spec, &crs()).unwrap(); // row 0 (n=1): sum of [1,2] = 3 // row 1 (n=2): sum of [1,2,3] = 6 // row 2 (n=3): sum of [2,3,4] = 9 @@ -258,7 +266,7 @@ mod tests { "n", rows_frame(FrameBound::UnboundedPreceding, FrameBound::CurrentRow), ); - apply_aggregate_window(&mut rows, &indices, &spec).unwrap(); + apply_aggregate_window(&mut rows, &indices, &spec, &crs()).unwrap(); assert_eq!(res(&rows[0]), Value::Integer(1)); assert_eq!(res(&rows[1]), Value::Integer(3)); assert_eq!(res(&rows[2]), Value::Integer(6)); @@ -275,7 +283,7 @@ mod tests { "n", rows_frame(FrameBound::CurrentRow, FrameBound::UnboundedFollowing), ); - apply_aggregate_window(&mut rows, &indices, &spec).unwrap(); + apply_aggregate_window(&mut rows, &indices, &spec, &crs()).unwrap(); // row 0: sum 1+2+3+4+5=15 // row 1: sum 2+3+4+5=14 // ... @@ -304,7 +312,7 @@ mod tests { "n", range_frame(FrameBound::UnboundedPreceding, FrameBound::CurrentRow), ); - apply_aggregate_window(&mut rows, &indices, &spec).unwrap(); + apply_aggregate_window(&mut rows, &indices, &spec, &crs()).unwrap(); // Row a (n=1, pos=0): CURRENT ROW expands to last peer at pos=1, sum=1+1=2 assert_eq!(res(&rows[0]), Value::Integer(2)); // Row b (n=1, pos=1): same @@ -326,12 +334,14 @@ mod tests { let indices: Vec = (0..2).collect(); let mut rows = keyed(&vals); - apply_aggregate_window(&mut rows, &indices, &make_spec("sum", "v", whole())).unwrap(); + apply_aggregate_window(&mut rows, &indices, &make_spec("sum", "v", whole()), &crs()) + .unwrap(); assert_eq!(res(&rows[0]), Value::Integer(18_014_398_509_481_985)); let running = range_frame(FrameBound::UnboundedPreceding, FrameBound::CurrentRow); let mut rows = keyed(&vals); - apply_aggregate_window(&mut rows, &indices, &make_spec("sum", "v", running)).unwrap(); + apply_aggregate_window(&mut rows, &indices, &make_spec("sum", "v", running), &crs()) + .unwrap(); assert_eq!( results(&rows), vec![ @@ -341,7 +351,8 @@ mod tests { ); let mut rows = keyed(&vals); - apply_aggregate_window(&mut rows, &indices, &make_spec("avg", "v", whole())).unwrap(); + apply_aggregate_window(&mut rows, &indices, &make_spec("avg", "v", whole()), &crs()) + .unwrap(); assert_eq!(res(&rows[0]), Value::Float(9_007_199_254_740_992.0)); } @@ -354,7 +365,8 @@ mod tests { ]; let indices: Vec = (0..3).collect(); let mut rows = keyed(&vals); - apply_aggregate_window(&mut rows, &indices, &make_spec("sum", "v", whole())).unwrap(); + apply_aggregate_window(&mut rows, &indices, &make_spec("sum", "v", whole()), &crs()) + .unwrap(); let want = i128::from(i64::MAX) + 2 * i128::from(u64::MAX); assert_eq!( res(&rows[0]), @@ -367,12 +379,14 @@ mod tests { let vals = [Value::Integer(2), Value::Float(0.5), Value::Null]; let indices: Vec = (0..3).collect(); let mut rows = keyed(&vals); - apply_aggregate_window(&mut rows, &indices, &make_spec("sum", "v", whole())).unwrap(); + apply_aggregate_window(&mut rows, &indices, &make_spec("sum", "v", whole()), &crs()) + .unwrap(); assert_eq!(res(&rows[0]), Value::Float(2.5)); let one_row = rows_frame(FrameBound::CurrentRow, FrameBound::CurrentRow); let mut rows = keyed(&vals); - apply_aggregate_window(&mut rows, &indices, &make_spec("sum", "v", one_row)).unwrap(); + apply_aggregate_window(&mut rows, &indices, &make_spec("sum", "v", one_row), &crs()) + .unwrap(); assert_eq!(res(&rows[2]), Value::Null); } @@ -384,12 +398,14 @@ mod tests { let overflow = [Value::Float(1e308), Value::Float(1e308)]; let mut rows = keyed(&overflow); - apply_aggregate_window(&mut rows, &indices, &make_spec("sum", "v", whole())).unwrap(); + apply_aggregate_window(&mut rows, &indices, &make_spec("sum", "v", whole()), &crs()) + .unwrap(); assert_eq!(res(&rows[0]), Value::Float(f64::INFINITY)); let running = range_frame(FrameBound::UnboundedPreceding, FrameBound::CurrentRow); let mut rows = keyed(&overflow); - apply_aggregate_window(&mut rows, &indices, &make_spec("sum", "v", running)).unwrap(); + apply_aggregate_window(&mut rows, &indices, &make_spec("sum", "v", running), &crs()) + .unwrap(); assert_eq!( results(&rows), vec![Value::Float(1e308), Value::Float(f64::INFINITY)] @@ -397,7 +413,8 @@ mod tests { let cancel = [Value::Float(f64::INFINITY), Value::Float(f64::NEG_INFINITY)]; let mut rows = keyed(&cancel); - apply_aggregate_window(&mut rows, &indices, &make_spec("avg", "v", whole())).unwrap(); + apply_aggregate_window(&mut rows, &indices, &make_spec("avg", "v", whole()), &crs()) + .unwrap(); assert!(matches!(res(&rows[0]), Value::Float(f) if f.is_nan())); } @@ -437,11 +454,13 @@ mod tests { let indices: Vec = (0..2).collect(); let mut rows = keyed(&vals); - apply_aggregate_window(&mut rows, &indices, &make_spec("min", "v", whole())).unwrap(); + apply_aggregate_window(&mut rows, &indices, &make_spec("min", "v", whole()), &crs()) + .unwrap(); assert_eq!(res(&rows[0]), Value::Integer(9_007_199_254_740_992)); let mut rows = keyed(&vals); - apply_aggregate_window(&mut rows, &indices, &make_spec("max", "v", whole())).unwrap(); + apply_aggregate_window(&mut rows, &indices, &make_spec("max", "v", whole()), &crs()) + .unwrap(); assert_eq!(res(&rows[0]), Value::Integer(9_007_199_254_740_993)); } @@ -455,11 +474,13 @@ mod tests { let indices: Vec = (0..3).collect(); let mut rows = keyed(&vals); - apply_aggregate_window(&mut rows, &indices, &make_spec("min", "v", whole())).unwrap(); + apply_aggregate_window(&mut rows, &indices, &make_spec("min", "v", whole()), &crs()) + .unwrap(); assert_eq!(res(&rows[2]), Value::Integer(1_700_000_000_000_000_001)); let mut rows = keyed(&vals); - apply_aggregate_window(&mut rows, &indices, &make_spec("max", "v", whole())).unwrap(); + apply_aggregate_window(&mut rows, &indices, &make_spec("max", "v", whole()), &crs()) + .unwrap(); assert_eq!(res(&rows[2]), Value::Integer(1_700_000_000_000_000_003)); } @@ -474,11 +495,13 @@ mod tests { let indices: Vec = (0..4).collect(); let mut rows = keyed(&vals); - apply_aggregate_window(&mut rows, &indices, &make_spec("min", "v", whole())).unwrap(); + apply_aggregate_window(&mut rows, &indices, &make_spec("min", "v", whole()), &crs()) + .unwrap(); assert_eq!(res(&rows[0]), Value::Float(1.5)); let mut rows = keyed(&vals); - apply_aggregate_window(&mut rows, &indices, &make_spec("max", "v", whole())).unwrap(); + apply_aggregate_window(&mut rows, &indices, &make_spec("max", "v", whole()), &crs()) + .unwrap(); assert_eq!(res(&rows[0]), Value::Integer(9_007_199_254_740_993)); } @@ -490,11 +513,13 @@ mod tests { let indices: Vec = (0..3).collect(); let mut rows = keyed(&vals); - apply_aggregate_window(&mut rows, &indices, &make_spec("max", "v", whole())).unwrap(); + apply_aggregate_window(&mut rows, &indices, &make_spec("max", "v", whole()), &crs()) + .unwrap(); assert!(matches!(res(&rows[0]), Value::Float(f) if f.is_nan())); let mut rows = keyed(&vals); - apply_aggregate_window(&mut rows, &indices, &make_spec("min", "v", whole())).unwrap(); + apply_aggregate_window(&mut rows, &indices, &make_spec("min", "v", whole()), &crs()) + .unwrap(); assert_eq!(res(&rows[0]), Value::Float(1.5)); } @@ -509,7 +534,13 @@ mod tests { let running = || range_frame(FrameBound::UnboundedPreceding, FrameBound::CurrentRow); let mut rows = keyed(&vals); - apply_aggregate_window(&mut rows, &indices, &make_spec("min", "v", running())).unwrap(); + apply_aggregate_window( + &mut rows, + &indices, + &make_spec("min", "v", running()), + &crs(), + ) + .unwrap(); assert_eq!( results(&rows), vec![ @@ -520,7 +551,13 @@ mod tests { ); let mut rows = keyed(&vals); - apply_aggregate_window(&mut rows, &indices, &make_spec("max", "v", running())).unwrap(); + apply_aggregate_window( + &mut rows, + &indices, + &make_spec("max", "v", running()), + &crs(), + ) + .unwrap(); assert_eq!( results(&rows), vec![ @@ -549,7 +586,7 @@ mod tests { "n", groups_frame(FrameBound::Preceding(1), FrameBound::Following(1)), ); - apply_aggregate_window(&mut rows, &indices, &spec).unwrap(); + apply_aggregate_window(&mut rows, &indices, &spec, &crs()).unwrap(); // pos=0 (group 0): frame → groups 0..=1 → rows 0..=2 → sum=1+1+2=4 assert_eq!(res(&rows[0]), Value::Integer(4)); // pos=1 (group 0): same frame diff --git a/nodedb-query/src/window/arg.rs b/nodedb-query/src/window/arg.rs index 4562ee954..7f714687a 100644 --- a/nodedb-query/src/window/arg.rs +++ b/nodedb-query/src/window/arg.rs @@ -10,6 +10,7 @@ //! projection or WHERE clause, and a non-column argument yields its computed //! value rather than a NULL placeholder. +use nodedb_crs::CrsContext; use nodedb_types::Value; use super::spec::WindowFuncSpec; @@ -32,13 +33,14 @@ pub(super) fn eval_arg_values( indices: &[usize], spec: &WindowFuncSpec, idx: usize, + crs: &CrsContext<'_>, ) -> Result { let Some(expr) = spec.args.get(idx) else { return Ok(None); }; let values = indices .iter() - .map(|&i| expr.eval(&rows[i].1)) + .map(|&i| expr.eval(&rows[i].1, crs)) .collect::, _>>()?; Ok(Some(values)) } diff --git a/nodedb-query/src/window/eval.rs b/nodedb-query/src/window/eval.rs index 7a5e23f5c..d1429979b 100644 --- a/nodedb-query/src/window/eval.rs +++ b/nodedb-query/src/window/eval.rs @@ -10,6 +10,7 @@ use super::ranking::{ apply_row_number, }; use super::spec::WindowFuncSpec; +use nodedb_crs::CrsContext; /// Evaluate window functions over sorted, partitioned rows. /// @@ -26,32 +27,35 @@ use super::spec::WindowFuncSpec; /// A division/modulo-by-zero in any PARTITION BY / ORDER BY / argument /// expression propagates as `Err(EvalError::DivisionByZero)` rather than /// folding to NULL. +/// +/// Geo functions in any expression resolve SRIDs through `crs`. pub fn evaluate_window_functions( rows: &mut [(String, nodedb_types::Value)], specs: &[WindowFuncSpec], + crs: &CrsContext<'_>, ) -> Result<(), crate::expr::EvalError> { for spec in specs { - let partitions = build_partitions(rows, &spec.partition_by, &spec.order_by)?; + let partitions = build_partitions(rows, &spec.partition_by, &spec.order_by, crs)?; for partition_indices in &partitions { match spec.func_name.as_str() { "row_number" => apply_row_number(rows, partition_indices, &spec.alias), - "rank" => apply_rank(rows, partition_indices, &spec.alias, &spec.order_by)?, + "rank" => apply_rank(rows, partition_indices, &spec.alias, &spec.order_by, crs)?, "dense_rank" => { - apply_dense_rank(rows, partition_indices, &spec.alias, &spec.order_by)? + apply_dense_rank(rows, partition_indices, &spec.alias, &spec.order_by, crs)? } "ntile" => apply_ntile(rows, partition_indices, spec), "percent_rank" => { - apply_percent_rank(rows, partition_indices, &spec.alias, &spec.order_by)? + apply_percent_rank(rows, partition_indices, &spec.alias, &spec.order_by, crs)? } "cume_dist" => { - apply_cume_dist(rows, partition_indices, &spec.alias, &spec.order_by)? + apply_cume_dist(rows, partition_indices, &spec.alias, &spec.order_by, crs)? } - "lag" => apply_lag(rows, partition_indices, spec)?, - "lead" => apply_lead(rows, partition_indices, spec)?, - "nth_value" => apply_nth_value(rows, partition_indices, spec)?, + "lag" => apply_lag(rows, partition_indices, spec, crs)?, + "lead" => apply_lead(rows, partition_indices, spec, crs)?, + "nth_value" => apply_nth_value(rows, partition_indices, spec, crs)?, "sum" | "count" | "avg" | "min" | "max" | "first_value" | "last_value" => { - apply_aggregate_window(rows, partition_indices, spec)? + apply_aggregate_window(rows, partition_indices, spec, crs)? } other => { unreachable!( @@ -130,7 +134,8 @@ mod tests { order_by: vec![], frame: WindowFrame::default(), }; - evaluate_window_functions(&mut rows, &[spec]).unwrap(); + evaluate_window_functions(&mut rows, &[spec], &nodedb_crs::CrsContext::builtin_only()) + .unwrap(); assert_eq!(col(&rows[0], "rn"), json!(1)); assert_eq!(col(&rows[4], "rn"), json!(5)); } @@ -146,7 +151,8 @@ mod tests { order_by: vec![], frame: WindowFrame::default(), }; - evaluate_window_functions(&mut rows, &[spec]).unwrap(); + evaluate_window_functions(&mut rows, &[spec], &nodedb_crs::CrsContext::builtin_only()) + .unwrap(); assert_eq!(col(&rows[0], "rn"), json!(1)); assert_eq!(col(&rows[2], "rn"), json!(3)); assert_eq!(col(&rows[3], "rn"), json!(1)); @@ -164,7 +170,8 @@ mod tests { order_by: vec![(SqlExpr::Column("salary".into()), true)], frame: WindowFrame::default(), }; - evaluate_window_functions(&mut rows, &[spec]).unwrap(); + evaluate_window_functions(&mut rows, &[spec], &nodedb_crs::CrsContext::builtin_only()) + .unwrap(); // The frame runs in salary order within each dept, not in row // arrival order: eng = Carol(90) → Alice(100) → Bob(120). // Integer salaries total exactly as integers. @@ -202,7 +209,8 @@ mod tests { ]; let mut spec = ordered_by_n("total", "sum"); spec.args = vec![SqlExpr::Column("x".into())]; - evaluate_window_functions(&mut rows, &[spec]).unwrap(); + evaluate_window_functions(&mut rows, &[spec], &nodedb_crs::CrsContext::builtin_only()) + .unwrap(); assert_eq!(rows[0].1.get("total"), Some(&Value::Float(1e308))); assert_eq!(rows[1].1.get("total"), Some(&Value::Float(f64::INFINITY))); } @@ -217,7 +225,12 @@ mod tests { ("c".to_string(), doc(f64::NAN)), ("d".to_string(), doc(f64::INFINITY)), ]; - evaluate_window_functions(&mut rows, &[ordered_by_n("rnk", "rank")]).unwrap(); + evaluate_window_functions( + &mut rows, + &[ordered_by_n("rnk", "rank")], + &nodedb_crs::CrsContext::builtin_only(), + ) + .unwrap(); assert_eq!(col(&rows[1], "rnk"), json!(1)); assert_eq!(col(&rows[3], "rnk"), json!(2)); assert_eq!(col(&rows[0], "rnk"), json!(3)); @@ -227,7 +240,12 @@ mod tests { #[test] fn percent_rank_distinct_keys() { let mut rows = numbered(5); - evaluate_window_functions(&mut rows, &[ordered_by_n("pr", "percent_rank")]).unwrap(); + evaluate_window_functions( + &mut rows, + &[ordered_by_n("pr", "percent_rank")], + &nodedb_crs::CrsContext::builtin_only(), + ) + .unwrap(); assert_eq!(col(&rows[0], "pr"), json!(0.0)); assert_eq!(col(&rows[1], "pr"), json!(0.25)); assert_eq!(col(&rows[2], "pr"), json!(0.5)); @@ -240,7 +258,12 @@ mod tests { // Peers share the leader's rank, so [1, 1, 2, 3] yields ranks // 1, 1, 3, 4 → percent_rank = 0, 0, 2/3, 3/3. let mut rows = peers(); - evaluate_window_functions(&mut rows, &[ordered_by_n("pr", "percent_rank")]).unwrap(); + evaluate_window_functions( + &mut rows, + &[ordered_by_n("pr", "percent_rank")], + &nodedb_crs::CrsContext::builtin_only(), + ) + .unwrap(); assert_eq!(col(&rows[0], "pr"), json!(0.0)); assert_eq!(col(&rows[1], "pr"), json!(0.0)); assert_eq!(col(&rows[2], "pr"), json!(2.0 / 3.0)); @@ -250,7 +273,12 @@ mod tests { #[test] fn cume_dist_distinct_keys() { let mut rows = numbered(5); - evaluate_window_functions(&mut rows, &[ordered_by_n("cd", "cume_dist")]).unwrap(); + evaluate_window_functions( + &mut rows, + &[ordered_by_n("cd", "cume_dist")], + &nodedb_crs::CrsContext::builtin_only(), + ) + .unwrap(); assert_eq!(col(&rows[0], "cd"), json!(0.2)); assert_eq!(col(&rows[1], "cd"), json!(0.4)); assert_eq!(col(&rows[2], "cd"), json!(0.6)); @@ -261,7 +289,12 @@ mod tests { #[test] fn cume_dist_with_peers() { let mut rows = peers(); - evaluate_window_functions(&mut rows, &[ordered_by_n("cd", "cume_dist")]).unwrap(); + evaluate_window_functions( + &mut rows, + &[ordered_by_n("cd", "cume_dist")], + &nodedb_crs::CrsContext::builtin_only(), + ) + .unwrap(); // Peers share value of last peer's position / N. assert_eq!(col(&rows[0], "cd"), json!(0.5)); assert_eq!(col(&rows[1], "cd"), json!(0.5)); @@ -277,7 +310,8 @@ mod tests { SqlExpr::Column("n".into()), SqlExpr::Literal(Value::Integer(2)), ]; - evaluate_window_functions(&mut rows, &[spec]).unwrap(); + evaluate_window_functions(&mut rows, &[spec], &nodedb_crs::CrsContext::builtin_only()) + .unwrap(); assert_eq!(col(&rows[0], "nv"), json!(null)); assert_eq!(col(&rows[1], "nv"), json!(2)); assert_eq!(col(&rows[2], "nv"), json!(2)); @@ -299,7 +333,8 @@ mod tests { order_by: vec![(SqlExpr::Column("salary".into()), false)], frame: WindowFrame::default(), }; - evaluate_window_functions(&mut rows, &[spec]).unwrap(); + evaluate_window_functions(&mut rows, &[spec], &nodedb_crs::CrsContext::builtin_only()) + .unwrap(); assert_eq!(col(&rows[0], "name"), json!("Alice")); assert_eq!(col(&rows[1], "name"), json!("Bob")); assert_eq!(col(&rows[2], "name"), json!("Carol")); @@ -324,6 +359,7 @@ mod tests { order_by: vec![], frame: WindowFrame::default(), }; - evaluate_window_functions(&mut rows, &[spec]).unwrap(); + evaluate_window_functions(&mut rows, &[spec], &nodedb_crs::CrsContext::builtin_only()) + .unwrap(); } } diff --git a/nodedb-query/src/window/helpers.rs b/nodedb-query/src/window/helpers.rs index 3e2559d02..cefbd90cf 100644 --- a/nodedb-query/src/window/helpers.rs +++ b/nodedb-query/src/window/helpers.rs @@ -7,6 +7,7 @@ use std::collections::HashMap; +use nodedb_crs::CrsContext; use nodedb_types::Value; use crate::expr::types::SqlExpr; @@ -25,6 +26,7 @@ pub(super) fn build_partitions( rows: &[(String, Value)], partition_by: &[SqlExpr], order_by: &[(SqlExpr, bool)], + crs: &CrsContext<'_>, ) -> Result>, crate::expr::EvalError> { let mut partitions = if partition_by.is_empty() { vec![(0..rows.len()).collect::>()] @@ -35,7 +37,7 @@ pub(super) fn build_partitions( for (i, (_id, doc)) in rows.iter().enumerate() { let key: String = partition_by .iter() - .map(|expr| expr.eval(doc).map(|v| partition_key_part(&v))) + .map(|expr| expr.eval(doc, crs).map(|v| partition_key_part(&v))) .collect::, _>>()? .join("\x00"); let entry = groups.entry(key.clone()).or_default(); @@ -54,7 +56,7 @@ pub(super) fn build_partitions( keys.push( order_by .iter() - .map(|(expr, _)| expr.eval(doc)) + .map(|(expr, _)| expr.eval(doc, crs)) .collect::, _>>()?, ); } @@ -145,10 +147,11 @@ pub(super) fn order_keys_equal( a: usize, b: usize, order_by: &[(SqlExpr, bool)], + crs: &CrsContext<'_>, ) -> Result { for (expr, _) in order_by { - let va = expr.eval(&rows[a].1)?; - let vb = expr.eval(&rows[b].1)?; + let va = expr.eval(&rows[a].1, crs)?; + let vb = expr.eval(&rows[b].1, crs)?; if !sort_peers(&va, &vb) { return Ok(false); } diff --git a/nodedb-query/src/window/offset.rs b/nodedb-query/src/window/offset.rs index 76c51166b..0d9c150a1 100644 --- a/nodedb-query/src/window/offset.rs +++ b/nodedb-query/src/window/offset.rs @@ -2,6 +2,7 @@ //! Offset window functions: lag, lead, nth_value. +use nodedb_crs::CrsContext; use nodedb_types::Value; use super::arg::{arg_at, const_default_arg, const_usize_arg, eval_arg_values}; @@ -12,8 +13,9 @@ pub(super) fn apply_lag( rows: &mut [(String, Value)], indices: &[usize], spec: &WindowFuncSpec, + crs: &CrsContext<'_>, ) -> Result<(), crate::expr::EvalError> { - let arg_values = eval_arg_values(rows, indices, spec, 0)?; + let arg_values = eval_arg_values(rows, indices, spec, 0, crs)?; let offset = const_usize_arg(spec, 1, 1); let default = const_default_arg(spec, 2); @@ -32,8 +34,9 @@ pub(super) fn apply_lead( rows: &mut [(String, Value)], indices: &[usize], spec: &WindowFuncSpec, + crs: &CrsContext<'_>, ) -> Result<(), crate::expr::EvalError> { - let arg_values = eval_arg_values(rows, indices, spec, 0)?; + let arg_values = eval_arg_values(rows, indices, spec, 0, crs)?; let offset = const_usize_arg(spec, 1, 1); let default = const_default_arg(spec, 2); @@ -57,8 +60,9 @@ pub(super) fn apply_nth_value( rows: &mut [(String, Value)], indices: &[usize], spec: &WindowFuncSpec, + crs: &CrsContext<'_>, ) -> Result<(), crate::expr::EvalError> { - let arg_values = eval_arg_values(rows, indices, spec, 0)?; + let arg_values = eval_arg_values(rows, indices, spec, 0, crs)?; let n = const_usize_arg(spec, 1, 1).max(1); for (pos, &i) in indices.iter().enumerate() { diff --git a/nodedb-query/src/window/ranking.rs b/nodedb-query/src/window/ranking.rs index 3bc053d58..2315ac44e 100644 --- a/nodedb-query/src/window/ranking.rs +++ b/nodedb-query/src/window/ranking.rs @@ -3,6 +3,7 @@ //! Ranking and distribution window functions: row_number, rank, dense_rank, //! ntile, percent_rank, cume_dist. +use nodedb_crs::CrsContext; use nodedb_types::Value; use crate::expr::SqlExpr; @@ -26,6 +27,7 @@ pub(super) fn apply_rank( indices: &[usize], alias: &str, order_by: &[(SqlExpr, bool)], + crs: &CrsContext<'_>, ) -> Result<(), crate::expr::EvalError> { if indices.is_empty() { return Ok(()); @@ -34,7 +36,7 @@ pub(super) fn apply_rank( set_window_col(&mut rows[indices[0]].1, alias, count_value(1)); for pos in 1..indices.len() { - if !order_keys_equal(rows, indices[pos - 1], indices[pos], order_by)? { + if !order_keys_equal(rows, indices[pos - 1], indices[pos], order_by, crs)? { current_rank = pos + 1; } set_window_col(&mut rows[indices[pos]].1, alias, count_value(current_rank)); @@ -47,6 +49,7 @@ pub(super) fn apply_dense_rank( indices: &[usize], alias: &str, order_by: &[(SqlExpr, bool)], + crs: &CrsContext<'_>, ) -> Result<(), crate::expr::EvalError> { if indices.is_empty() { return Ok(()); @@ -55,7 +58,7 @@ pub(super) fn apply_dense_rank( set_window_col(&mut rows[indices[0]].1, alias, count_value(1)); for pos in 1..indices.len() { - if !order_keys_equal(rows, indices[pos - 1], indices[pos], order_by)? { + if !order_keys_equal(rows, indices[pos - 1], indices[pos], order_by, crs)? { current_rank += 1; } set_window_col(&mut rows[indices[pos]].1, alias, count_value(current_rank)); @@ -94,6 +97,7 @@ pub(super) fn apply_percent_rank( indices: &[usize], alias: &str, order_by: &[(SqlExpr, bool)], + crs: &CrsContext<'_>, ) -> Result<(), crate::expr::EvalError> { let total = indices.len(); if total == 0 { @@ -108,7 +112,7 @@ pub(super) fn apply_percent_rank( set_window_col(&mut rows[indices[0]].1, alias, Value::Float(0.0)); for pos in 1..total { - if !order_keys_equal(rows, indices[pos - 1], indices[pos], order_by)? { + if !order_keys_equal(rows, indices[pos - 1], indices[pos], order_by, crs)? { current_rank = pos + 1; } let pr = (current_rank - 1) as f64 / denom; @@ -125,6 +129,7 @@ pub(super) fn apply_cume_dist( indices: &[usize], alias: &str, order_by: &[(SqlExpr, bool)], + crs: &CrsContext<'_>, ) -> Result<(), crate::expr::EvalError> { let total = indices.len(); if total == 0 { @@ -136,7 +141,13 @@ pub(super) fn apply_cume_dist( while group_start < total { let mut group_end = group_start + 1; while group_end < total - && order_keys_equal(rows, indices[group_start], indices[group_end], order_by)? + && order_keys_equal( + rows, + indices[group_start], + indices[group_end], + order_by, + crs, + )? { group_end += 1; } diff --git a/nodedb-query/src/window/running.rs b/nodedb-query/src/window/running.rs index b22dbfad3..00052a413 100644 --- a/nodedb-query/src/window/running.rs +++ b/nodedb-query/src/window/running.rs @@ -11,6 +11,7 @@ //! computed at the *last* peer in the group (i.e., they all include each //! other). This matches PostgreSQL behaviour for RANGE CURRENT ROW. +use nodedb_crs::CrsContext; use nodedb_types::Value; use super::arg::{ArgValues, arg_at}; @@ -29,6 +30,7 @@ pub(super) fn running_aggregate( indices: &[usize], spec: &WindowFuncSpec, arg_values: &ArgValues, + crs: &CrsContext<'_>, ) -> Result<(), crate::expr::EvalError> { let len = indices.len(); if len == 0 { @@ -66,7 +68,7 @@ pub(super) fn running_aggregate( // Check if the *next* row starts a new peer group (or we're at the end). let is_last_in_group = - pos + 1 == len || !order_keys_equal(rows, i, indices[pos + 1], &spec.order_by)?; + pos + 1 == len || !order_keys_equal(rows, i, indices[pos + 1], &spec.order_by, crs)?; if is_last_in_group { // Compute the result at the end of this peer group. diff --git a/nodedb-query/src/window/value_agg.rs b/nodedb-query/src/window/value_agg.rs index b75b010f8..e9d152bf0 100644 --- a/nodedb-query/src/window/value_agg.rs +++ b/nodedb-query/src/window/value_agg.rs @@ -5,6 +5,7 @@ use std::collections::HashMap; +use nodedb_crs::CrsContext; use nodedb_types::Value; use super::extremum::{extremum_direction, value_replaces}; @@ -16,6 +17,7 @@ pub(super) fn apply_v_aggregate( rows: &mut [Vec], indices: &[usize], column_index: &HashMap, + crs: &CrsContext<'_>, spec: &WindowFuncSpec, write_col: usize, ) -> Result<(), WindowError> { @@ -24,9 +26,9 @@ pub(super) fn apply_v_aggregate( && matches!(spec.frame.end, FrameBound::CurrentRow); if use_running { - apply_v_running_aggregate(rows, indices, column_index, spec, write_col) + apply_v_running_aggregate(rows, indices, column_index, crs, spec, write_col) } else { - apply_v_per_row_aggregate(rows, indices, column_index, spec, write_col) + apply_v_per_row_aggregate(rows, indices, column_index, crs, spec, write_col) } } @@ -34,9 +36,10 @@ fn eval_arg( spec: &WindowFuncSpec, row: &[Value], column_index: &HashMap, + crs: &CrsContext<'_>, ) -> Result { match spec.args.first() { - Some(expr) => Ok(eval_arg_for_row(expr, row, column_index)?), + Some(expr) => Ok(eval_arg_for_row(expr, row, column_index, crs)?), None => Ok(Value::Null), } } @@ -45,6 +48,7 @@ fn apply_v_running_aggregate( rows: &mut [Vec], indices: &[usize], column_index: &HashMap, + crs: &CrsContext<'_>, spec: &WindowFuncSpec, write_col: usize, ) -> Result<(), WindowError> { @@ -64,7 +68,7 @@ fn apply_v_running_aggregate( for pos in 0..len { let i = indices[pos]; let val = match rows.get(i) { - Some(row) => eval_arg(spec, row, column_index)?, + Some(row) => eval_arg(spec, row, column_index, crs)?, None => Value::Null, }; @@ -83,15 +87,15 @@ fn apply_v_running_aggregate( } let is_last_in_group = pos + 1 == len - || !order_keys_equal_v(rows, i, indices[pos + 1], column_index, &spec.order_by)?; + || !order_keys_equal_v(rows, i, indices[pos + 1], column_index, crs, &spec.order_by)?; if is_last_in_group { let first_val = match rows.get(indices[0]) { - Some(row) => eval_arg(spec, row, column_index)?, + Some(row) => eval_arg(spec, row, column_index, crs)?, None => Value::Null, }; let last_val = match rows.get(indices[pos]) { - Some(row) => eval_arg(spec, row, column_index)?, + Some(row) => eval_arg(spec, row, column_index, crs)?, None => Value::Null, }; @@ -118,6 +122,7 @@ fn apply_v_per_row_aggregate( rows: &mut [Vec], indices: &[usize], column_index: &HashMap, + crs: &CrsContext<'_>, spec: &WindowFuncSpec, write_col: usize, ) -> Result<(), WindowError> { @@ -130,7 +135,7 @@ fn apply_v_per_row_aggregate( let order_values: Vec = indices .iter() .map(|&i| match (order_expr, rows.get(i)) { - (Some(expr), Some(row)) => Ok(eval_arg_for_row(expr, row, column_index)?), + (Some(expr), Some(row)) => Ok(eval_arg_for_row(expr, row, column_index, crs)?), _ => Ok(Value::Null), }) .collect::, WindowError>>()?; @@ -144,7 +149,7 @@ fn apply_v_per_row_aggregate( let arg_vals: Vec = indices .iter() .map(|&i| match rows.get(i) { - Some(row) => eval_arg(spec, row, column_index), + Some(row) => eval_arg(spec, row, column_index, crs), None => Ok(Value::Null), }) .collect::, WindowError>>()?; @@ -423,7 +428,15 @@ mod tests { row.push(Value::Null); } let indices: Vec = (0..rows.len()).collect(); - apply_v_aggregate(rows, &indices, cols, spec, write_col).unwrap(); + apply_v_aggregate( + rows, + &indices, + cols, + &nodedb_crs::CrsContext::builtin_only(), + spec, + write_col, + ) + .unwrap(); } fn frame(mode: &str, start: FrameBound, end: FrameBound) -> WindowFrame { diff --git a/nodedb-query/src/window/value_eval.rs b/nodedb-query/src/window/value_eval.rs index efbf0ed78..1e207f3db 100644 --- a/nodedb-query/src/window/value_eval.rs +++ b/nodedb-query/src/window/value_eval.rs @@ -8,6 +8,7 @@ use std::collections::HashMap; +use nodedb_crs::CrsContext; use nodedb_types::Value; use super::spec::WindowFuncSpec; @@ -42,12 +43,13 @@ pub enum WindowError { pub fn evaluate_window_functions_value( rows: &mut [Vec], column_index: &HashMap, + crs: &CrsContext<'_>, specs: &[WindowFuncSpec], ) -> Result, WindowError> { let mut new_cols: Vec = Vec::with_capacity(specs.len()); for spec in specs { - let partitions = build_value_partitions(rows, column_index, spec)?; + let partitions = build_value_partitions(rows, column_index, crs, spec)?; let write_col = rows.first().map(|r| r.len()).unwrap_or(0); for row in rows.iter_mut() { @@ -57,24 +59,33 @@ pub fn evaluate_window_functions_value( for partition_indices in &partitions { match spec.func_name.as_str() { "row_number" => apply_v_row_number(rows, partition_indices, write_col), - "rank" => apply_v_rank(rows, partition_indices, column_index, spec, write_col)?, + "rank" => { + apply_v_rank(rows, partition_indices, column_index, crs, spec, write_col)? + } "dense_rank" => { - apply_v_dense_rank(rows, partition_indices, column_index, spec, write_col)? + apply_v_dense_rank(rows, partition_indices, column_index, crs, spec, write_col)? } "ntile" => apply_v_ntile(rows, partition_indices, spec, write_col)?, - "percent_rank" => { - apply_v_percent_rank(rows, partition_indices, column_index, spec, write_col)? - } + "percent_rank" => apply_v_percent_rank( + rows, + partition_indices, + column_index, + crs, + spec, + write_col, + )?, "cume_dist" => { - apply_v_cume_dist(rows, partition_indices, column_index, spec, write_col)? + apply_v_cume_dist(rows, partition_indices, column_index, crs, spec, write_col)? + } + "lag" => apply_v_lag(rows, partition_indices, column_index, crs, spec, write_col)?, + "lead" => { + apply_v_lead(rows, partition_indices, column_index, crs, spec, write_col)? } - "lag" => apply_v_lag(rows, partition_indices, column_index, spec, write_col)?, - "lead" => apply_v_lead(rows, partition_indices, column_index, spec, write_col)?, "nth_value" => { - apply_v_nth_value(rows, partition_indices, column_index, spec, write_col)? + apply_v_nth_value(rows, partition_indices, column_index, crs, spec, write_col)? } "sum" | "count" | "avg" | "min" | "max" | "first_value" | "last_value" => { - apply_v_aggregate(rows, partition_indices, column_index, spec, write_col)? + apply_v_aggregate(rows, partition_indices, column_index, crs, spec, write_col)? } other => { return Err(WindowError::ArgEval { @@ -110,13 +121,14 @@ pub(super) fn order_keys_equal_v( a: usize, b: usize, column_index: &HashMap, + crs: &CrsContext<'_>, order_by: &[(SqlExpr, bool)], ) -> Result { for (expr, _) in order_by { let row_a = rows.get(a).map(|r| r.as_slice()).unwrap_or(&[]); let row_b = rows.get(b).map(|r| r.as_slice()).unwrap_or(&[]); - let va = eval_arg_for_row(expr, row_a, column_index)?; - let vb = eval_arg_for_row(expr, row_b, column_index)?; + let va = eval_arg_for_row(expr, row_a, column_index, crs)?; + let vb = eval_arg_for_row(expr, row_b, column_index, crs)?; if !matches!(cmp_values(&va, &vb), std::cmp::Ordering::Equal) { return Ok(false); } @@ -134,6 +146,7 @@ pub(super) fn eval_arg_for_row( expr: &SqlExpr, row: &[Value], column_index: &HashMap, + crs: &CrsContext<'_>, ) -> Result { match expr { SqlExpr::Column(name) => Ok(column_index @@ -144,7 +157,7 @@ pub(super) fn eval_arg_for_row( SqlExpr::Literal(v) => Ok(v.clone()), other => { let doc = row_to_obj(row, column_index); - Ok(other.eval(&doc)?) + Ok(other.eval(&doc, crs)?) } } } @@ -201,6 +214,7 @@ fn apply_v_rank( rows: &mut [Vec], indices: &[usize], column_index: &HashMap, + crs: &CrsContext<'_>, spec: &WindowFuncSpec, write_col: usize, ) -> Result<(), WindowError> { @@ -215,6 +229,7 @@ fn apply_v_rank( indices[pos - 1], indices[pos], column_index, + crs, &spec.order_by, )? { current_rank = pos + 1; @@ -233,6 +248,7 @@ fn apply_v_dense_rank( rows: &mut [Vec], indices: &[usize], column_index: &HashMap, + crs: &CrsContext<'_>, spec: &WindowFuncSpec, write_col: usize, ) -> Result<(), WindowError> { @@ -247,6 +263,7 @@ fn apply_v_dense_rank( indices[pos - 1], indices[pos], column_index, + crs, &spec.order_by, )? { current_rank += 1; @@ -283,6 +300,7 @@ fn apply_v_percent_rank( rows: &mut [Vec], indices: &[usize], column_index: &HashMap, + crs: &CrsContext<'_>, spec: &WindowFuncSpec, write_col: usize, ) -> Result<(), WindowError> { @@ -303,6 +321,7 @@ fn apply_v_percent_rank( indices[pos - 1], indices[pos], column_index, + crs, &spec.order_by, )? { current_rank = pos + 1; @@ -317,6 +336,7 @@ fn apply_v_cume_dist( rows: &mut [Vec], indices: &[usize], column_index: &HashMap, + crs: &CrsContext<'_>, spec: &WindowFuncSpec, write_col: usize, ) -> Result<(), WindowError> { @@ -334,6 +354,7 @@ fn apply_v_cume_dist( indices[group_start], indices[group_end], column_index, + crs, &spec.order_by, )? { @@ -354,12 +375,13 @@ fn collect_arg_values( rows: &[Vec], indices: &[usize], column_index: &HashMap, + crs: &CrsContext<'_>, spec: &WindowFuncSpec, ) -> Result, WindowError> { indices .iter() .map(|&i| match (rows.get(i), spec.args.first()) { - (Some(row), Some(expr)) => Ok(eval_arg_for_row(expr, row, column_index)?), + (Some(row), Some(expr)) => Ok(eval_arg_for_row(expr, row, column_index, crs)?), _ => Ok(Value::Null), }) .collect() @@ -369,12 +391,13 @@ fn apply_v_lag( rows: &mut [Vec], indices: &[usize], column_index: &HashMap, + crs: &CrsContext<'_>, spec: &WindowFuncSpec, write_col: usize, ) -> Result<(), WindowError> { let offset = usize_arg(spec, 1, 1); let default = default_arg_value(spec, 2); - let values = collect_arg_values(rows, indices, column_index, spec)?; + let values = collect_arg_values(rows, indices, column_index, crs, spec)?; for (pos, &i) in indices.iter().enumerate() { let val = if pos >= offset { values[pos - offset].clone() @@ -390,12 +413,13 @@ fn apply_v_lead( rows: &mut [Vec], indices: &[usize], column_index: &HashMap, + crs: &CrsContext<'_>, spec: &WindowFuncSpec, write_col: usize, ) -> Result<(), WindowError> { let offset = usize_arg(spec, 1, 1); let default = default_arg_value(spec, 2); - let values = collect_arg_values(rows, indices, column_index, spec)?; + let values = collect_arg_values(rows, indices, column_index, crs, spec)?; for (pos, &i) in indices.iter().enumerate() { let val = if pos + offset < indices.len() { values[pos + offset].clone() @@ -411,11 +435,12 @@ fn apply_v_nth_value( rows: &mut [Vec], indices: &[usize], column_index: &HashMap, + crs: &CrsContext<'_>, spec: &WindowFuncSpec, write_col: usize, ) -> Result<(), WindowError> { let n = usize_arg(spec, 1, 1).max(1); - let values = collect_arg_values(rows, indices, column_index, spec)?; + let values = collect_arg_values(rows, indices, column_index, crs, spec)?; for (pos, &i) in indices.iter().enumerate() { let val = if pos + 1 >= n { values[n - 1].clone() @@ -484,7 +509,13 @@ mod tests { let mut rows = rows_v(&[5, 5, 5]); let cols = ci(&["v"]); let s = spec("row_number", vec![], vec![], vec![]); - evaluate_window_functions_value(&mut rows, &cols, &[s]).unwrap(); + evaluate_window_functions_value( + &mut rows, + &cols, + &nodedb_crs::CrsContext::builtin_only(), + &[s], + ) + .unwrap(); assert_eq!(out_int(&rows, 1), vec![1, 2, 3]); } @@ -493,7 +524,13 @@ mod tests { let mut rows = rows_v(&[10, 10, 20]); let cols = ci(&["v"]); let s = spec("rank", vec![], vec![], vec![(col("v"), true)]); - evaluate_window_functions_value(&mut rows, &cols, &[s]).unwrap(); + evaluate_window_functions_value( + &mut rows, + &cols, + &nodedb_crs::CrsContext::builtin_only(), + &[s], + ) + .unwrap(); assert_eq!(out_int(&rows, 1), vec![1, 1, 3]); } @@ -502,7 +539,13 @@ mod tests { let mut rows = rows_v(&[10, 10, 20]); let cols = ci(&["v"]); let s = spec("dense_rank", vec![], vec![], vec![(col("v"), true)]); - evaluate_window_functions_value(&mut rows, &cols, &[s]).unwrap(); + evaluate_window_functions_value( + &mut rows, + &cols, + &nodedb_crs::CrsContext::builtin_only(), + &[s], + ) + .unwrap(); assert_eq!(out_int(&rows, 1), vec![1, 1, 2]); } @@ -516,7 +559,13 @@ mod tests { vec![], vec![(col("v"), true)], ); - evaluate_window_functions_value(&mut rows, &cols, &[s]).unwrap(); + evaluate_window_functions_value( + &mut rows, + &cols, + &nodedb_crs::CrsContext::builtin_only(), + &[s], + ) + .unwrap(); assert_eq!(out_int(&rows, 1), vec![1, 1, 2]); } @@ -525,7 +574,13 @@ mod tests { let mut rows = rows_v(&[1, 2, 3]); let cols = ci(&["v"]); let s = spec("lag", vec![col("v")], vec![], vec![(col("v"), true)]); - evaluate_window_functions_value(&mut rows, &cols, &[s]).unwrap(); + evaluate_window_functions_value( + &mut rows, + &cols, + &nodedb_crs::CrsContext::builtin_only(), + &[s], + ) + .unwrap(); // First row has no predecessor → Null; rest carry the prior value. assert!(matches!(rows[0][1], Value::Null)); assert_eq!(rows[1][1].as_f64().unwrap() as i64, 1); @@ -537,7 +592,13 @@ mod tests { let mut rows = rows_v(&[1, 2, 3]); let cols = ci(&["v"]); let s = spec("lead", vec![col("v")], vec![], vec![(col("v"), true)]); - evaluate_window_functions_value(&mut rows, &cols, &[s]).unwrap(); + evaluate_window_functions_value( + &mut rows, + &cols, + &nodedb_crs::CrsContext::builtin_only(), + &[s], + ) + .unwrap(); assert_eq!(rows[0][1].as_f64().unwrap() as i64, 2); assert_eq!(rows[1][1].as_f64().unwrap() as i64, 3); // Last row has no successor → Null. @@ -550,7 +611,13 @@ mod tests { let mut rows = rows_v(&[10, 10, 20]); let pr = spec("percent_rank", vec![], vec![], vec![(col("v"), true)]); - evaluate_window_functions_value(&mut rows, &cols, &[pr]).unwrap(); + evaluate_window_functions_value( + &mut rows, + &cols, + &nodedb_crs::CrsContext::builtin_only(), + &[pr], + ) + .unwrap(); let pr_out = out_f64(&rows, 1); assert!((pr_out[0] - 0.0).abs() < 1e-9); assert!((pr_out[1] - 0.0).abs() < 1e-9); @@ -558,7 +625,13 @@ mod tests { let mut rows = rows_v(&[10, 10, 20]); let cd = spec("cume_dist", vec![], vec![], vec![(col("v"), true)]); - evaluate_window_functions_value(&mut rows, &cols, &[cd]).unwrap(); + evaluate_window_functions_value( + &mut rows, + &cols, + &nodedb_crs::CrsContext::builtin_only(), + &[cd], + ) + .unwrap(); let cd_out = out_f64(&rows, 1); assert!((cd_out[0] - 2.0 / 3.0).abs() < 1e-9); assert!((cd_out[1] - 2.0 / 3.0).abs() < 1e-9); @@ -574,7 +647,13 @@ mod tests { vec![Value::Integer(2), Value::Integer(102)], ]; let s = spec("row_number", vec![], vec![col("g")], vec![]); - evaluate_window_functions_value(&mut rows, &cols, &[s]).unwrap(); + evaluate_window_functions_value( + &mut rows, + &cols, + &nodedb_crs::CrsContext::builtin_only(), + &[s], + ) + .unwrap(); // Two rows in partition g=1 → 1,2; one row in g=2 → 1. Result at idx 2. assert_eq!(out_int(&rows, 2), vec![1, 2, 1]); } @@ -587,7 +666,13 @@ mod tests { let mut rows = rows_v(&[10, 30, 20]); let cols = ci(&["v"]); let s = spec("rank", vec![], vec![], vec![(col("v"), false)]); - evaluate_window_functions_value(&mut rows, &cols, &[s]).unwrap(); + evaluate_window_functions_value( + &mut rows, + &cols, + &nodedb_crs::CrsContext::builtin_only(), + &[s], + ) + .unwrap(); assert_eq!(out_int(&rows, 0), vec![10, 30, 20]); assert_eq!(out_int(&rows, 1), vec![3, 1, 2]); } @@ -597,7 +682,13 @@ mod tests { let mut rows = rows_v(&[1]); let cols = ci(&["v"]); let s = spec("nonexistent", vec![], vec![], vec![]); - let err = evaluate_window_functions_value(&mut rows, &cols, &[s]).unwrap_err(); + let err = evaluate_window_functions_value( + &mut rows, + &cols, + &nodedb_crs::CrsContext::builtin_only(), + &[s], + ) + .unwrap_err(); assert!(matches!(err, WindowError::ArgEval { .. })); } @@ -608,7 +699,13 @@ mod tests { let cols = ci(&["v"]); let mut rows = rows_v(&[1, 2]); let s = spec("row_number", vec![], vec![col("missing")], vec![]); - evaluate_window_functions_value(&mut rows, &cols, &[s]).unwrap(); + evaluate_window_functions_value( + &mut rows, + &cols, + &nodedb_crs::CrsContext::builtin_only(), + &[s], + ) + .unwrap(); assert_eq!(out_int(&rows, 1), vec![1, 2]); } } diff --git a/nodedb-query/src/window/value_partition.rs b/nodedb-query/src/window/value_partition.rs index 3a7a5d0c3..2793b1b40 100644 --- a/nodedb-query/src/window/value_partition.rs +++ b/nodedb-query/src/window/value_partition.rs @@ -5,6 +5,7 @@ use std::collections::HashMap; +use nodedb_crs::CrsContext; use nodedb_types::Value; use super::helpers::null_order; @@ -20,6 +21,7 @@ use crate::expr::types::SqlExpr; pub(super) fn build_value_partitions( rows: &[Vec], column_index: &HashMap, + crs: &CrsContext<'_>, spec: &WindowFuncSpec, ) -> Result>, WindowError> { let mut partitions = if spec.partition_by.is_empty() { @@ -29,7 +31,7 @@ pub(super) fn build_value_partitions( let mut order: Vec = Vec::new(); for (i, row) in rows.iter().enumerate() { - let key = partition_key(row, column_index, &spec.partition_by)?; + let key = partition_key(row, column_index, crs, &spec.partition_by)?; let entry = groups.entry(key.clone()).or_default(); if entry.is_empty() { order.push(key); @@ -46,7 +48,7 @@ pub(super) fn build_value_partitions( keys.push( spec.order_by .iter() - .map(|(expr, _)| eval_arg_for_row(expr, row, column_index)) + .map(|(expr, _)| eval_arg_for_row(expr, row, column_index, crs)) .collect::, _>>()?, ); } @@ -62,12 +64,13 @@ pub(super) fn build_value_partitions( fn partition_key( row: &[Value], column_index: &HashMap, + crs: &CrsContext<'_>, partition_by: &[SqlExpr], ) -> Result { Ok(partition_by .iter() .map(|expr| { - let v = eval_arg_for_row(expr, row, column_index)?; + let v = eval_arg_for_row(expr, row, column_index, crs)?; Ok(format!("{v:?}")) }) .collect::, WindowError>>()? diff --git a/nodedb-raster-load/Cargo.toml b/nodedb-raster-load/Cargo.toml new file mode 100644 index 000000000..5dba5e904 --- /dev/null +++ b/nodedb-raster-load/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "nodedb-raster-load" +description = "Raster-to-array load planning for NodeDB: band mapping, grid alignment, nodata and pixel rows" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license = "Apache-2.0" +authors = ["Biz Impulse Solutions Sdn. Bhd. and the NodeDB contributors"] +repository.workspace = true +homepage.workspace = true +documentation = "https://docs.rs/nodedb-raster-load" +keywords = ["geotiff", "raster", "array", "geospatial", "nodedb"] +categories = ["science::geo", "database"] + +[dependencies] +nodedb-raster = { workspace = true } +thiserror = { workspace = true } diff --git a/nodedb-raster-load/NOTICE b/nodedb-raster-load/NOTICE new file mode 100644 index 000000000..4bf4bfc89 --- /dev/null +++ b/nodedb-raster-load/NOTICE @@ -0,0 +1,2 @@ +Copyright 2026 Biz Impulse Solutions Sdn. Bhd. and the NodeDB contributors. +This product is licensed under the Apache License, Version 2.0. diff --git a/nodedb-raster-load/src/bands.rs b/nodedb-raster-load/src/bands.rs new file mode 100644 index 000000000..9506abbc3 --- /dev/null +++ b/nodedb-raster-load/src/bands.rs @@ -0,0 +1,276 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Which array attr each band loads into, and the type rule. +//! +//! The bands of a load are every source's bands in source order: source 1's +//! bands, then source 2's, and so on. `BANDS (a, SKIP, b)` maps those bands +//! in order: band 1 to `a`, band 2 nowhere, band 3 to `b`. Without `BANDS`, +//! band `i` loads into attr `i`. A band loads only into an attr that holds +//! every value of its sample type exactly. An attr no band loads into must +//! be nullable, and every cell holds NULL there. + +use nodedb_raster::SampleType; + +use crate::error::RasterLoadError; +use crate::options::BandTarget; +use crate::view::{ArrayView, AttrType}; + +/// Where one attr's value comes from. +#[derive(Debug, Clone, PartialEq)] +pub enum AttrSource { + /// No band loads into the attr. It is nullable. + Null, + /// Band `band` of the load loads into the attr. + Band { + band: usize, + /// Whether the attr holds floats, so integer samples widen to `f64`. + float: bool, + nullable: bool, + name: String, + }, +} + +/// The attr sources in schema order, and the bands that load. +#[derive(Debug)] +pub struct BandMap { + pub attrs: Vec, + pub loaded: Vec, +} + +/// Map the load's bands, one sample type each, onto the attrs of `view`. +pub fn map_bands( + view: &ArrayView, + samples: &[SampleType], + bands: Option<&[BandTarget]>, +) -> Result { + let targets = band_targets(view, samples.len(), bands)?; + let mut attrs = vec![AttrSource::Null; view.attrs.len()]; + let mut loaded = Vec::with_capacity(targets.len()); + for (band, (target, sample)) in targets.into_iter().zip(samples).enumerate() { + let Some(index) = target else { continue }; + let Some(attr) = view.attrs.get(index) else { + continue; + }; + if !widens(*sample, attr.dtype) { + return Err(RasterLoadError::BandType { + band: band + 1, + sample: *sample, + attr: attr.name.clone(), + attr_type: attr.dtype.sql_name(), + }); + } + if let Some(slot) = attrs.get_mut(index) { + *slot = AttrSource::Band { + band, + float: attr.dtype.is_float(), + nullable: attr.nullable, + name: attr.name.clone(), + }; + } + loaded.push(band); + } + if loaded.is_empty() { + return Err(RasterLoadError::NoBandLoads); + } + for (attr, source) in view.attrs.iter().zip(&attrs) { + if *source == AttrSource::Null && !attr.nullable { + return Err(RasterLoadError::UnmappedNotNull { + attr: attr.name.clone(), + }); + } + } + Ok(BandMap { attrs, loaded }) +} + +/// The attr index each band loads into, `None` for a skipped band. +fn band_targets( + view: &ArrayView, + count: usize, + bands: Option<&[BandTarget]>, +) -> Result>, RasterLoadError> { + let Some(bands) = bands else { + if count > view.attrs.len() { + return Err(RasterLoadError::BandCount { + array: view.name.clone(), + bands: count, + attrs: view.attrs.len(), + }); + } + return Ok((0..count).map(Some).collect()); + }; + if bands.len() != count { + return Err(RasterLoadError::BandListLength { + listed: bands.len(), + bands: count, + }); + } + bands + .iter() + .map(|band| match band { + BandTarget::Skip => Ok(None), + BandTarget::Attr(name) => view + .attrs + .iter() + .position(|attr| attr.name.eq_ignore_ascii_case(name)) + .map(Some) + .ok_or_else(|| RasterLoadError::UnknownAttr { + array: view.name.clone(), + name: name.clone(), + }), + }) + .collect() +} + +/// Whether every `sample` value converts to `attr` without loss. +fn widens(sample: SampleType, attr: AttrType) -> bool { + use AttrType as A; + match sample { + SampleType::U8 => matches!( + attr, + A::UInt8 + | A::Int16 + | A::UInt16 + | A::Int32 + | A::UInt32 + | A::Int64 + | A::Float32 + | A::Float64 + ), + SampleType::I8 => matches!( + attr, + A::Int8 | A::Int16 | A::Int32 | A::Int64 | A::Float32 | A::Float64 + ), + SampleType::U16 => matches!( + attr, + A::UInt16 | A::Int32 | A::UInt32 | A::Int64 | A::Float32 | A::Float64 + ), + SampleType::I16 => matches!( + attr, + A::Int16 | A::Int32 | A::Int64 | A::Float32 | A::Float64 + ), + SampleType::U32 => matches!(attr, A::UInt32 | A::Int64 | A::Float64), + SampleType::I32 => matches!(attr, A::Int32 | A::Int64 | A::Float64), + SampleType::F32 => matches!(attr, A::Float32 | A::Float64), + SampleType::F64 => matches!(attr, A::Float64), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::view::ViewAttr; + + fn view(attrs: &[(&str, AttrType, bool)]) -> ArrayView { + ArrayView { + name: "scene".into(), + dims: Vec::new(), + attrs: attrs + .iter() + .map(|(name, dtype, nullable)| ViewAttr { + name: (*name).into(), + dtype: *dtype, + nullable: *nullable, + meta: Default::default(), + }) + .collect(), + georef: None, + } + } + + const U16X2: [SampleType; 2] = [SampleType::U16, SampleType::U16]; + + #[test] + fn widening_is_lossless_only() { + use AttrType as A; + assert!(widens(SampleType::U8, A::UInt8)); + assert!(widens(SampleType::U16, A::Float32)); + assert!(!widens(SampleType::U16, A::Int16)); + assert!(!widens(SampleType::I16, A::UInt16)); + assert!(!widens(SampleType::U32, A::Float32)); + assert!(!widens(SampleType::F32, A::Int64)); + assert!(!widens(SampleType::F64, A::Float32)); + assert!(!widens(SampleType::U8, A::String)); + assert!(widens(SampleType::I8, A::Int8)); + assert!(!widens(SampleType::I8, A::UInt8)); + assert!(!widens(SampleType::U8, A::Int8)); + assert!(widens(SampleType::I32, A::Int32)); + assert!(!widens(SampleType::I32, A::Float32)); + assert!(!widens(SampleType::I32, A::UInt32)); + } + + #[test] + fn bands_load_in_order_without_a_list() { + let v = view(&[ + ("b1", AttrType::UInt16, false), + ("b2", AttrType::Float64, true), + ("extra", AttrType::Int64, true), + ]); + let map = map_bands(&v, &U16X2, None).expect("maps"); + assert_eq!(map.loaded, vec![0, 1]); + assert!(matches!( + map.attrs[1], + AttrSource::Band { + band: 1, + float: true, + .. + } + )); + assert_eq!(map.attrs[2], AttrSource::Null); + } + + #[test] + fn a_band_list_skips_and_reorders() { + let v = view(&[ + ("red", AttrType::UInt16, true), + ("nir", AttrType::UInt16, true), + ]); + let list = [ + BandTarget::Attr("nir".into()), + BandTarget::Skip, + BandTarget::Attr("red".into()), + ]; + let samples = [SampleType::U16; 3]; + let map = map_bands(&v, &samples, Some(&list)).expect("maps"); + assert_eq!(map.loaded, vec![0, 2]); + assert!(matches!(map.attrs[0], AttrSource::Band { band: 2, .. })); + assert!(matches!(map.attrs[1], AttrSource::Band { band: 0, .. })); + } + + #[test] + fn each_band_keeps_its_own_sample_type() { + let v = view(&[ + ("red", AttrType::UInt16, true), + ("qa", AttrType::UInt8, true), + ]); + let mixed = [SampleType::U16, SampleType::U8]; + assert!(map_bands(&v, &mixed, None).is_ok()); + let err = map_bands(&v, &U16X2, None).expect_err("u16 into UINT8"); + assert_eq!(err.sqlstate(), "42804"); + assert!(err.to_string().contains("band 2"), "{err}"); + } + + #[test] + fn refusals_carry_their_sqlstate() { + let narrow = view(&[("b1", AttrType::UInt8, true)]); + let one = [SampleType::U16]; + let err = map_bands(&narrow, &one, None).expect_err("u16 into UINT8"); + assert_eq!(err.sqlstate(), "42804"); + + let strict = view(&[ + ("b1", AttrType::UInt16, true), + ("qa", AttrType::UInt16, false), + ]); + let err = map_bands(&strict, &one, None).expect_err("qa unmapped"); + assert_eq!(err.sqlstate(), "23502"); + + let single = view(&[("b1", AttrType::UInt16, true)]); + let err = map_bands(&single, &U16X2, None).expect_err("2 bands, 1 attr"); + assert_eq!(err.sqlstate(), "22023"); + let short = [BandTarget::Attr("b1".into())]; + let err = map_bands(&single, &U16X2, Some(&short)).expect_err("short list"); + assert_eq!(err.sqlstate(), "22023"); + let unknown = [BandTarget::Attr("b9".into())]; + let err = map_bands(&single, &one, Some(&unknown)).expect_err("no attr b9"); + assert_eq!(err.sqlstate(), "42703"); + } +} diff --git a/nodedb-raster-load/src/coords.rs b/nodedb-raster-load/src/coords.rs new file mode 100644 index 000000000..5ca214fe5 --- /dev/null +++ b/nodedb-raster-load/src/coords.rs @@ -0,0 +1,139 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! The source of each dim's coordinate: the GEOREF axes, then `TIME` for +//! the one `TIMESTAMP` dim. Any other dim has no source in a raster. + +use crate::error::RasterLoadError; +use crate::view::{ArrayView, DimType, DomainBound, ViewDim}; + +/// The source of one dim's coordinate. +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum CoordSource { + /// The GEOREF x axis. + X, + /// The GEOREF y axis. + Y, + /// The `TIME` instant, in ms since the Unix epoch. + Time(i64), +} + +/// The source of every dim of `view`, in schema order. +pub fn plan_coords( + view: &ArrayView, + x_index: usize, + y_index: usize, + time_ms: Option, +) -> Result, RasterLoadError> { + let mut time_used = false; + let mut coords = Vec::with_capacity(view.dims.len()); + for (index, dim) in view.dims.iter().enumerate() { + let source = if index == x_index { + CoordSource::X + } else if index == y_index { + CoordSource::Y + } else if dim.dtype == DimType::TimestampMs && !time_used { + time_used = true; + CoordSource::Time(time_coord(dim, time_ms)?) + } else { + return Err(RasterLoadError::DimWithoutSource { + dim: dim.name.clone(), + }); + }; + coords.push(source); + } + if time_ms.is_some() && !time_used { + return Err(RasterLoadError::TimeWithoutDim { + array: view.name.clone(), + }); + } + Ok(coords) +} + +/// The `TIME` instant for `dim`, checked against its domain. +fn time_coord(dim: &ViewDim, time_ms: Option) -> Result { + let time = time_ms.ok_or_else(|| RasterLoadError::TimeMissing { + dim: dim.name.clone(), + })?; + let inside = match (&dim.lo, &dim.hi) { + ( + DomainBound::TimestampMs(lo) | DomainBound::Int64(lo), + DomainBound::TimestampMs(hi) | DomainBound::Int64(hi), + ) => *lo <= time && time <= *hi, + _ => false, + }; + if inside { + return Ok(time); + } + Err(RasterLoadError::TimeOutside { + time, + dim: dim.name.clone(), + lo: bound_text(&dim.lo), + hi: bound_text(&dim.hi), + }) +} + +/// A domain bound as the DDL writes it. +pub(crate) fn bound_text(bound: &DomainBound) -> String { + match bound { + DomainBound::Int64(v) | DomainBound::TimestampMs(v) => v.to_string(), + DomainBound::Float64(v) => v.to_string(), + DomainBound::String(v) => format!("'{v}'"), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn dim(name: &str, dtype: DimType, lo: DomainBound, hi: DomainBound) -> ViewDim { + ViewDim { + name: name.into(), + dtype, + lo, + hi, + } + } + + fn view(dims: Vec) -> ArrayView { + ArrayView { + name: "scene".into(), + dims, + attrs: Vec::new(), + georef: None, + } + } + + #[test] + fn time_fills_the_timestamp_dim() { + let ts = || { + dim( + "t", + DimType::TimestampMs, + DomainBound::TimestampMs(0), + DomainBound::TimestampMs(1_000), + ) + }; + let axis = |name: &str| { + dim( + name, + DimType::Int64, + DomainBound::Int64(0), + DomainBound::Int64(9), + ) + }; + let v = view(vec![ts(), axis("row"), axis("col")]); + let coords = plan_coords(&v, 2, 1, Some(500)).expect("time given"); + assert_eq!( + coords, + vec![CoordSource::Time(500), CoordSource::Y, CoordSource::X] + ); + let sqlstate = + |r: Result, RasterLoadError>| r.expect_err("refused").sqlstate(); + assert_eq!(sqlstate(plan_coords(&v, 2, 1, None)), "42601"); + assert_eq!(sqlstate(plan_coords(&v, 2, 1, Some(5_000))), "22003"); + let plain = view(vec![axis("row"), axis("col")]); + assert_eq!(sqlstate(plan_coords(&plain, 1, 0, Some(5))), "42601"); + let band = view(vec![axis("band"), axis("row"), axis("col")]); + assert_eq!(sqlstate(plan_coords(&band, 2, 1, None)), "0A000"); + } +} diff --git a/nodedb-raster-load/src/error.rs b/nodedb-raster-load/src/error.rs new file mode 100644 index 000000000..71c6ed8b6 --- /dev/null +++ b/nodedb-raster-load/src/error.rs @@ -0,0 +1,216 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! `RasterLoadError`: why a raster load refuses its input, with the +//! SQLSTATE each refusal reports. + +use nodedb_raster::SampleType; + +/// A refused raster load. Every variant but the per-pixel ones refuses the +/// whole input before any row loads. +#[derive(Debug, Clone, PartialEq, thiserror::Error)] +pub enum RasterLoadError { + #[error( + "the file CRS is SRID {file}, array '{array}' GEOREF is SRID {georef}; \ + reproject outside the engine" + )] + CrsMismatch { + array: String, + file: u32, + georef: u32, + }, + #[error("the file GeoKeys name no CRS, array '{array}' GEOREF is SRID {georef}")] + NoCrs { array: String, georef: u32 }, + #[error("the file has no geotransform, so no pixel has a world position")] + NoTransform, + #[error( + "band {band} holds {sample} samples, which attr '{attr}' ({attr_type}) cannot hold \ + exactly; declare the attr as a type that widens {sample}" + )] + BandType { + band: usize, + sample: SampleType, + attr: String, + attr_type: &'static str, + }, + #[error("no file band loads into an attr")] + NoBandLoads, + #[error( + "attr '{attr}' is NOT NULL, and no file band loads into it; map a band to it with \ + BANDS (...)" + )] + UnmappedNotNull { attr: String }, + #[error( + "the sources have {bands} bands, array '{array}' has {attrs} attrs; name each band's \ + attr or SKIP with BANDS (...)" + )] + BandCount { + array: String, + bands: usize, + attrs: usize, + }, + #[error( + "BANDS lists {listed} entries, the sources have {bands} bands; list one entry per band" + )] + BandListLength { listed: usize, bands: usize }, + #[error("BANDS names '{name}', which array '{array}' has no attr")] + UnknownAttr { array: String, name: String }, + #[error("array '{array}' GEOREF names dim '{dim}', which it lacks")] + GeorefDim { array: String, dim: String }, + #[error( + "dim '{dim}' has no source in a GeoTIFF; a raster fills the GEOREF axes and one \ + TIMESTAMP dim from TIME" + )] + DimWithoutSource { dim: String }, + #[error("TIME is given, and array '{array}' has no TIMESTAMP dim")] + TimeWithoutDim { array: String }, + #[error("dim '{dim}' is a TIMESTAMP; give the scene instant with WITH (TIME '')")] + TimeMissing { dim: String }, + #[error("TIME {time} is outside dim '{dim}' domain [{lo}..{hi}]")] + TimeOutside { + time: i64, + dim: String, + lo: String, + hi: String, + }, + #[error("nodata {value} is not a {sample} value, so no sample can match it")] + Nodata { value: f64, sample: SampleType }, + #[error("raster grid does not align with array '{array}': {why}; resample outside the engine")] + Misaligned { array: String, why: String }, + #[error( + "the raster footprint spans [{lo}..{hi}] on dim '{dim}', outside its domain \ + [{domain_lo}..{domain_hi}]" + )] + FootprintOutside { + dim: String, + lo: String, + hi: String, + domain_lo: String, + domain_hi: String, + }, + #[error("GEOREF axis '{dim}' cannot hold world coordinates")] + AxisNotWorld { dim: String }, + #[error( + "pixel centres on INT64 axis '{dim}' are not whole numbers; declare it FLOAT64 or \ + resample outside the engine" + )] + AxisNotWhole { dim: String }, + #[error( + "source {index} ('{source_name}') {what} differs from source 1 ('{first}'); every \ + source of one COPY shares its grid and block layout" + )] + SourceMismatch { + index: usize, + source_name: String, + first: String, + what: String, + }, + #[error( + "WINDOW ({x0}, {y0}, {x1}, {y1}) lies outside the {width}x{height} image; give a \ + window inside it" + )] + WindowOutside { + x0: u32, + y0: u32, + x1: u32, + y1: u32, + width: u32, + height: u32, + }, + #[error("raster block {index} of source {source_index} {what}")] + BlockShape { + index: u32, + source_index: usize, + what: String, + }, + #[error("band {band} has no sample for this pixel")] + MissingSample { row: usize, band: usize }, + #[error( + "attr '{attr}' is NOT NULL, and band {band} holds no data at (pixel row {pixel_row}, \ + col {pixel_col})" + )] + NodataInNotNull { + row: usize, + attr: String, + band: usize, + pixel_row: u32, + pixel_col: u32, + }, +} + +impl RasterLoadError { + /// The SQLSTATE the refusal reports. + pub fn sqlstate(&self) -> &'static str { + match self { + Self::CrsMismatch { .. } + | Self::NoCrs { .. } + | Self::NoTransform + | Self::NoBandLoads + | Self::BandCount { .. } + | Self::BandListLength { .. } + | Self::Nodata { .. } + | Self::Misaligned { .. } + | Self::AxisNotWorld { .. } + | Self::AxisNotWhole { .. } + | Self::SourceMismatch { .. } + | Self::WindowOutside { .. } => "22023", + Self::BandType { .. } => "42804", + Self::UnmappedNotNull { .. } | Self::NodataInNotNull { .. } => "23502", + Self::UnknownAttr { .. } => "42703", + Self::GeorefDim { .. } => "XX000", + Self::DimWithoutSource { .. } => "0A000", + Self::TimeWithoutDim { .. } | Self::TimeMissing { .. } => "42601", + Self::TimeOutside { .. } | Self::FootprintOutside { .. } => "22003", + Self::BlockShape { .. } | Self::MissingSample { .. } => "22P04", + } + } + + /// The 1-based row the refusal names, or 0 when it refuses the whole + /// input. + pub fn row(&self) -> usize { + match self { + Self::MissingSample { row, .. } | Self::NodataInNotNull { row, .. } => *row, + Self::CrsMismatch { .. } + | Self::NoCrs { .. } + | Self::NoTransform + | Self::BandType { .. } + | Self::NoBandLoads + | Self::UnmappedNotNull { .. } + | Self::BandCount { .. } + | Self::BandListLength { .. } + | Self::UnknownAttr { .. } + | Self::GeorefDim { .. } + | Self::DimWithoutSource { .. } + | Self::TimeWithoutDim { .. } + | Self::TimeMissing { .. } + | Self::TimeOutside { .. } + | Self::Nodata { .. } + | Self::Misaligned { .. } + | Self::FootprintOutside { .. } + | Self::AxisNotWorld { .. } + | Self::AxisNotWhole { .. } + | Self::SourceMismatch { .. } + | Self::WindowOutside { .. } + | Self::BlockShape { .. } => 0, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn per_pixel_refusals_name_their_row() { + let e = RasterLoadError::NodataInNotNull { + row: 7, + attr: "b1".into(), + band: 1, + pixel_row: 1, + pixel_col: 2, + }; + assert_eq!((e.row(), e.sqlstate()), (7, "23502")); + assert!(e.to_string().contains("(pixel row 1, col 2)")); + assert_eq!(RasterLoadError::NoTransform.row(), 0); + assert_eq!(RasterLoadError::NoTransform.sqlstate(), "22023"); + } +} diff --git a/nodedb-raster-load/src/grid.rs b/nodedb-raster-load/src/grid.rs new file mode 100644 index 000000000..63d11ba27 --- /dev/null +++ b/nodedb-raster-load/src/grid.rs @@ -0,0 +1,361 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Where each pixel lands in an array: the cell coordinates of its GEOREF +//! axes, checked against the dim domains once at open. +//! +//! A pixel-mode array stores pixel indexes. The file grid must be the +//! array grid shifted by whole pixels, so file pixel `(col, row)` is cell +//! `(col + col0, row + row0)`. A world-mode array stores pixel-centre +//! world coordinates, so the cell is the file geotransform at +//! `(col + 0.5, row + 0.5)`. +//! +//! The pixel-to-cell map is affine and each coordinate is monotone in the +//! pixel index. So the four corner pixels of the loaded window bound every +//! cell, and a footprint inside the dim domains puts every cell inside them. + +use nodedb_raster::GeoTransform; + +use crate::coords::bound_text; +use crate::error::RasterLoadError; +use crate::view::{CoordLiteral, DimType, DomainBound, ViewDim}; +use crate::window::PixelWindow; + +/// Largest relative difference between the file and array linear terms. +const LINEAR_TOLERANCE: f64 = 1e-9; +/// Largest distance from a whole number, in pixels or INT64 units. +const INTEGRAL_TOLERANCE: f64 = 1e-6; +/// Largest pixel offset kept as `i64`, so `offset + u32` never overflows. +const MAX_OFFSET: f64 = (1u64 << 62) as f64; + +/// The cell coordinates of one pixel on the array's GEOREF axes. +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum CellGrid { + /// File pixel `(col, row)` is cell `(col + col0, row + row0)`. + Pixel { col0: i64, row0: i64 }, + /// The cell is the pixel centre in world coordinates. An `INT64` axis + /// holds the rounded centre, which open checks to be whole. + World { + gt: GeoTransform, + x_int: bool, + y_int: bool, + }, +} + +impl CellGrid { + /// The `(x, y)` cell coordinates of file pixel `(col, row)`. + pub fn cell(&self, col: u32, row: u32) -> (CoordLiteral, CoordLiteral) { + match *self { + CellGrid::Pixel { col0, row0 } => ( + CoordLiteral::Int64(i64::from(col) + col0), + CoordLiteral::Int64(i64::from(row) + row0), + ), + CellGrid::World { gt, x_int, y_int } => { + let (x, y) = gt.pixel_to_world(f64::from(col) + 0.5, f64::from(row) + 0.5); + (world_coord(x, x_int), world_coord(y, y_int)) + } + } + } +} + +fn world_coord(value: f64, int: bool) -> CoordLiteral { + if int { + // Open proved every centre on this axis whole and inside the domain. + CoordLiteral::Int64(value.round() as i64) + } else { + CoordLiteral::Float64(value) + } +} + +/// The file facts the grid checks read. +pub struct GridInput<'a> { + /// The array name, for messages. + pub array: &'a str, + pub x_dim: &'a ViewDim, + pub y_dim: &'a ViewDim, + /// The array GEOREF transform. `None` is a world-mode array. + pub array_gt: Option, + pub file_gt: GeoTransform, + /// The pixels that load. + pub window: PixelWindow, +} + +/// Check the file grid against the array and the loaded footprint against +/// the dim domains. Misalignment is `22023`, a footprint outside a domain +/// `22003`. +pub fn plan_grid(input: &GridInput<'_>) -> Result { + match input.array_gt { + Some(array_gt) => pixel_grid(input, array_gt), + None => world_grid(input), + } +} + +fn pixel_grid(input: &GridInput<'_>, array: GeoTransform) -> Result { + let file = input.file_gt.0; + let array_gt = array.0; + let scale = [1, 2, 4, 5] + .iter() + .map(|&k| array_gt[k].abs()) + .fold(0.0_f64, f64::max); + let linear_equal = [1, 2, 4, 5] + .iter() + .all(|&k| (file[k] - array_gt[k]).abs() <= LINEAR_TOLERANCE * scale); + if !linear_equal { + return Err(misaligned( + input, + format!( + "the file pixel size and rotation {:?} differ from the array TRANSFORM {:?}", + [file[1], file[2], file[4], file[5]], + [array_gt[1], array_gt[2], array_gt[4], array_gt[5]] + ), + )); + } + let Some((u0, v0)) = array.world_to_pixel(file[0], file[3]) else { + return Err(misaligned( + input, + "the array TRANSFORM has no inverse".into(), + )); + }; + let (Some(col0), Some(row0)) = (whole_offset(u0), whole_offset(v0)) else { + return Err(misaligned( + input, + format!("the file origin falls at array pixel ({u0}, {v0}), not on a whole pixel"), + )); + }; + let w = input.window; + check_int_span( + input.x_dim, + col0 + i64::from(w.col0), + col0 + i64::from(w.col1) - 1, + )?; + check_int_span( + input.y_dim, + row0 + i64::from(w.row0), + row0 + i64::from(w.row1) - 1, + )?; + Ok(CellGrid::Pixel { col0, row0 }) +} + +/// `value` rounded to `i64` when it is within the tolerance of a whole +/// number and inside the offset range. +fn whole_offset(value: f64) -> Option { + let rounded = value.round(); + let whole = (value - rounded).abs() <= INTEGRAL_TOLERANCE; + (whole && rounded.abs() <= MAX_OFFSET).then_some(rounded as i64) +} + +fn world_grid(input: &GridInput<'_>) -> Result { + let gt = input.file_gt; + let w = input.window; + let (c0, c1) = (f64::from(w.col0) + 0.5, f64::from(w.col1) - 0.5); + let (r0, r1) = (f64::from(w.row0) + 0.5, f64::from(w.row1) - 0.5); + let corners = [(c0, r0), (c1, r0), (c0, r1), (c1, r1)].map(|(u, v)| gt.pixel_to_world(u, v)); + let xs = corners.map(|(x, _)| x); + let ys = corners.map(|(_, y)| y); + let x_int = int_axis(input.x_dim)?; + let y_int = int_axis(input.y_dim)?; + let file = input.file_gt.0; + if x_int { + check_whole_axis(input.x_dim, &xs, [file[1], file[2]])?; + } + if y_int { + check_whole_axis(input.y_dim, &ys, [file[4], file[5]])?; + } + check_world_span(input.x_dim, &xs)?; + check_world_span(input.y_dim, &ys)?; + Ok(CellGrid::World { gt, x_int, y_int }) +} + +/// Whether a world-mode axis is `INT64`. GEOREF admits `INT64` and +/// `FLOAT64` axes only. +fn int_axis(dim: &ViewDim) -> Result { + match dim.dtype { + DimType::Int64 => Ok(true), + DimType::Float64 => Ok(false), + DimType::TimestampMs | DimType::String => Err(RasterLoadError::AxisNotWorld { + dim: dim.name.clone(), + }), + } +} + +/// Every pixel centre on an `INT64` world axis must be whole: the corner +/// centres and the per-pixel steps. +fn check_whole_axis( + dim: &ViewDim, + corners: &[f64; 4], + steps: [f64; 2], +) -> Result<(), RasterLoadError> { + let whole = |v: &f64| (v - v.round()).abs() <= INTEGRAL_TOLERANCE; + if corners.iter().all(whole) && steps.iter().all(whole) { + return Ok(()); + } + Err(RasterLoadError::AxisNotWhole { + dim: dim.name.clone(), + }) +} + +fn check_world_span(dim: &ViewDim, corners: &[f64; 4]) -> Result<(), RasterLoadError> { + let lo = corners.iter().copied().fold(f64::INFINITY, f64::min); + let hi = corners.iter().copied().fold(f64::NEG_INFINITY, f64::max); + match dim.dtype { + DimType::Int64 => check_int_span(dim, lo.round() as i64, hi.round() as i64), + DimType::Float64 | DimType::TimestampMs | DimType::String => { + let inside = match (&dim.lo, &dim.hi) { + (DomainBound::Float64(d_lo), DomainBound::Float64(d_hi)) => { + *d_lo <= lo && hi <= *d_hi + } + _ => false, + }; + if inside { + Ok(()) + } else { + Err(outside(dim, lo.to_string(), hi.to_string())) + } + } + } +} + +/// Check the integer span `[lo, hi]` of an axis against its domain. +fn check_int_span(dim: &ViewDim, lo: i64, hi: i64) -> Result<(), RasterLoadError> { + let inside = match (&dim.lo, &dim.hi) { + (DomainBound::Int64(d_lo), DomainBound::Int64(d_hi)) => *d_lo <= lo && hi <= *d_hi, + _ => false, + }; + if inside { + return Ok(()); + } + Err(outside(dim, lo.to_string(), hi.to_string())) +} + +fn outside(dim: &ViewDim, lo: String, hi: String) -> RasterLoadError { + RasterLoadError::FootprintOutside { + dim: dim.name.clone(), + lo, + hi, + domain_lo: bound_text(&dim.lo), + domain_hi: bound_text(&dim.hi), + } +} + +fn misaligned(input: &GridInput<'_>, why: String) -> RasterLoadError { + RasterLoadError::Misaligned { + array: input.array.to_string(), + why, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn int_dim(name: &str, lo: i64, hi: i64) -> ViewDim { + ViewDim { + name: name.into(), + dtype: DimType::Int64, + lo: DomainBound::Int64(lo), + hi: DomainBound::Int64(hi), + } + } + + fn float_dim(name: &str, lo: f64, hi: f64) -> ViewDim { + ViewDim { + name: name.into(), + dtype: DimType::Float64, + lo: DomainBound::Float64(lo), + hi: DomainBound::Float64(hi), + } + } + + const GT: [f64; 6] = [600_000.0, 30.0, 0.0, 700_000.0, 0.0, -30.0]; + + fn input<'a>( + x: &'a ViewDim, + y: &'a ViewDim, + array_gt: Option<[f64; 6]>, + file_gt: [f64; 6], + ) -> GridInput<'a> { + GridInput { + array: "scene", + x_dim: x, + y_dim: y, + array_gt: array_gt.map(GeoTransform), + file_gt: GeoTransform(file_gt), + window: PixelWindow::whole(256, 192), + } + } + + #[test] + fn a_shifted_file_lands_at_its_pixel_offset() { + let (x, y) = (int_dim("col", 0, 1000), int_dim("row", 0, 1000)); + let file = [ + 600_000.0 + 30.0 * 10.0, + 30.0, + 0.0, + 700_000.0 - 30.0 * 4.0, + 0.0, + -30.0, + ]; + let grid = plan_grid(&input(&x, &y, Some(GT), file)).expect("aligned"); + let (cx, cy) = grid.cell(0, 0); + assert_eq!(cx, CoordLiteral::Int64(10)); + assert_eq!(cy, CoordLiteral::Int64(4)); + } + + #[test] + fn a_half_pixel_shift_is_misaligned() { + let (x, y) = (int_dim("col", 0, 1000), int_dim("row", 0, 1000)); + let file = [600_015.0, 30.0, 0.0, 700_000.0, 0.0, -30.0]; + let err = plan_grid(&input(&x, &y, Some(GT), file)).expect_err("half pixel"); + assert_eq!(err.sqlstate(), "22023"); + assert!( + err.to_string().contains("resample outside the engine"), + "{err}" + ); + let scaled = [600_000.0, 10.0, 0.0, 700_000.0, 0.0, -10.0]; + let err = plan_grid(&input(&x, &y, Some(GT), scaled)).expect_err("pixel size"); + assert_eq!(err.sqlstate(), "22023"); + } + + #[test] + fn a_footprint_past_the_domain_is_22003() { + let (x, y) = (int_dim("col", 0, 255), int_dim("row", 0, 99)); + let err = plan_grid(&input(&x, &y, Some(GT), GT)).expect_err("rows past 99"); + assert_eq!(err.sqlstate(), "22003"); + } + + #[test] + fn a_window_inside_the_domain_loads_where_the_file_does_not_fit() { + let (x, y) = (int_dim("col", 0, 255), int_dim("row", 0, 99)); + let mut windowed = input(&x, &y, Some(GT), GT); + windowed.window = PixelWindow { + col0: 0, + row0: 0, + col1: 256, + row1: 100, + }; + assert!(plan_grid(&windowed).is_ok()); + } + + #[test] + fn world_cells_are_pixel_centres() { + let x = float_dim("x", 600_000.0, 607_680.0); + let y = float_dim("y", 694_240.0, 700_000.0); + let grid = plan_grid(&input(&x, &y, None, GT)).expect("inside"); + let (cx, cy) = grid.cell(2, 1); + assert_eq!(cx, CoordLiteral::Float64(600_075.0)); + assert_eq!(cy, CoordLiteral::Float64(699_955.0)); + } + + #[test] + fn int_world_axes_need_whole_centres() { + let x = int_dim("x", 0, 10_000_000); + let y = int_dim("y", 0, 10_000_000); + let quarter = [600_000.0, 25.0, 0.0, 700_000.0, 0.0, -25.0]; + let err = plan_grid(&input(&x, &y, None, quarter)).expect_err("centres at 12.5 m"); + assert_eq!(err.sqlstate(), "22023"); + let shifted = [599_985.0, 30.0, 0.0, 700_015.0, 0.0, -30.0]; + let grid = plan_grid(&input(&x, &y, None, shifted)).expect("whole centres"); + let (cx, cy) = grid.cell(1, 1); + assert_eq!(cx, CoordLiteral::Int64(600_030)); + assert_eq!(cy, CoordLiteral::Int64(699_970)); + } +} diff --git a/nodedb-raster-load/src/lib.rs b/nodedb-raster-load/src/lib.rs new file mode 100644 index 000000000..ea16076f9 --- /dev/null +++ b/nodedb-raster-load/src/lib.rs @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Raster-to-array load planning for NodeDB. +//! +//! A load opens one or more raster sources with `nodedb-raster`, describes +//! each with a [`SourceHeader`], and checks that every source shares one +//! grid and block layout ([`check_same_layout`]). [`RasterPlan::new`] runs +//! every open-time check against the array: CRS, band-to-attr types, grid +//! alignment, the dim domains, `TIME`, nodata and the pixel window. For +//! each block index the caller reads that block from every source, and +//! [`BlockRows`] turns the blocks into array rows. [`adopted_metadata`] +//! names the attr metadata `ADOPT_METADATA` takes from the files. +//! +//! The crate does no I/O, so a server and an embedded engine share it. + +pub mod bands; +pub mod coords; +pub mod error; +pub mod grid; +pub mod metadata; +pub mod nodata; +pub mod options; +pub mod plan; +pub mod rows; +pub mod source; +pub mod view; +pub mod window; + +pub use bands::{AttrSource, BandMap, map_bands}; +pub use coords::{CoordSource, plan_coords}; +pub use error::RasterLoadError; +pub use grid::{CellGrid, GridInput, plan_grid}; +pub use metadata::adopted_metadata; +pub use nodata::{Nodata, typed_nodata}; +pub use options::{BandTarget, LoadOptions}; +pub use plan::{PlanBand, RasterPlan}; +pub use rows::BlockRows; +pub use source::{LayoutError, SourceHeader, check_same_layout}; +pub use view::{ + AdoptedMeta, ArrayView, AttrLiteral, AttrMeta, AttrType, CoordLiteral, DimType, DomainBound, + LoadRow, ViewAttr, ViewDim, ViewGeoref, +}; +pub use window::PixelWindow; diff --git a/nodedb-raster-load/src/metadata.rs b/nodedb-raster-load/src/metadata.rs new file mode 100644 index 000000000..b31b3f295 --- /dev/null +++ b/nodedb-raster-load/src/metadata.rs @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! `ADOPT_METADATA true`: an attr with no metadata takes the nodata, +//! scale, offset and units the file declares for the band that loads into +//! it. An attr with any metadata set keeps all of it, so a load never +//! overrides a choice the schema made. + +use crate::bands::AttrSource; +use crate::plan::RasterPlan; +use crate::source::SourceHeader; +use crate::view::{AdoptedMeta, ArrayView, AttrMeta}; + +/// The metadata changes adoption makes, one per attr that changes. +pub fn adopted_metadata( + view: &ArrayView, + plan: &RasterPlan, + sources: &[SourceHeader], +) -> Vec { + let mut changes = Vec::new(); + for (attr, source) in view.attrs.iter().zip(plan.attrs()) { + let AttrSource::Band { band, .. } = source else { + continue; + }; + if attr.meta != AttrMeta::default() { + continue; + } + let Some(info) = plan + .band(*band) + .and_then(|at| sources.get(at.source)?.bands.get(at.band)) + else { + continue; + }; + let meta = AttrMeta { + nodata: info.nodata, + scale: info.scale, + offset: info.offset, + units: info.units.clone(), + }; + if meta != AttrMeta::default() { + changes.push(AdoptedMeta { + attr: attr.name.clone(), + meta, + }); + } + } + changes +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::options::LoadOptions; + use crate::plan::tests::{georef, source, view}; + use nodedb_raster::{BandInfo, SampleType}; + + #[test] + fn only_attrs_without_metadata_adopt_the_band_metadata() { + let mut info = BandInfo::plain(SampleType::U16); + info.nodata = Some(0.0); + info.scale = Some(2.75e-5); + info.offset = Some(-0.2); + info.units = Some("reflectance".into()); + let sources = [source("/B4.TIF", info.clone()), source("/B5.TIF", info)]; + let mut view = view(); + view.attrs[1].meta.scale = Some(1.0); + let plan = + RasterPlan::new(&view, &georef(), &sources, &LoadOptions::default()).expect("plans"); + let changes = adopted_metadata(&view, &plan, &sources); + assert_eq!(changes.len(), 1, "nir has metadata; qa has no band"); + assert_eq!(changes[0].attr, "red"); + let meta = &changes[0].meta; + assert_eq!(meta.scale, Some(2.75e-5)); + assert_eq!(meta.units.as_deref(), Some("reflectance")); + } +} diff --git a/nodedb-raster-load/src/nodata.rs b/nodedb-raster-load/src/nodata.rs new file mode 100644 index 000000000..dbface907 --- /dev/null +++ b/nodedb-raster-load/src/nodata.rs @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! A nodata value typed by its band's sample type. + +use nodedb_raster::{BandSamples, SampleType}; + +use crate::error::RasterLoadError; + +/// The nodata value, typed by the sample type. +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Nodata { + Int(i64), + F32(f32), + F64(f64), +} + +impl Nodata { + /// Whether sample `index` of `band` is nodata. NaN matches NaN. + pub fn matches(self, band: &BandSamples, index: usize) -> bool { + let int = |n: i64, v: Option| v == Some(n); + match (self, band) { + (Nodata::Int(n), BandSamples::U8(v)) => int(n, v.get(index).map(|s| i64::from(*s))), + (Nodata::Int(n), BandSamples::I8(v)) => int(n, v.get(index).map(|s| i64::from(*s))), + (Nodata::Int(n), BandSamples::U16(v)) => int(n, v.get(index).map(|s| i64::from(*s))), + (Nodata::Int(n), BandSamples::I16(v)) => int(n, v.get(index).map(|s| i64::from(*s))), + (Nodata::Int(n), BandSamples::U32(v)) => int(n, v.get(index).map(|s| i64::from(*s))), + (Nodata::Int(n), BandSamples::I32(v)) => int(n, v.get(index).map(|s| i64::from(*s))), + (Nodata::F32(n), BandSamples::F32(v)) => v + .get(index) + .is_some_and(|s| *s == n || (s.is_nan() && n.is_nan())), + (Nodata::F64(n), BandSamples::F64(v)) => v + .get(index) + .is_some_and(|s| *s == n || (s.is_nan() && n.is_nan())), + ( + Nodata::Int(_) | Nodata::F32(_) | Nodata::F64(_), + BandSamples::U8(_) + | BandSamples::I8(_) + | BandSamples::U16(_) + | BandSamples::I16(_) + | BandSamples::U32(_) + | BandSamples::I32(_) + | BandSamples::F32(_) + | BandSamples::F64(_), + ) => false, + } + } +} + +/// `value` as a nodata of `sample`. An integer type needs a whole value in +/// its range. `f32` takes the nearest `f32`, as GDAL compares. +pub fn typed_nodata(value: f64, sample: SampleType) -> Result { + let range = match sample { + SampleType::U8 => (f64::from(u8::MIN), f64::from(u8::MAX)), + SampleType::I8 => (f64::from(i8::MIN), f64::from(i8::MAX)), + SampleType::U16 => (f64::from(u16::MIN), f64::from(u16::MAX)), + SampleType::I16 => (f64::from(i16::MIN), f64::from(i16::MAX)), + SampleType::U32 => (f64::from(u32::MIN), f64::from(u32::MAX)), + SampleType::I32 => (f64::from(i32::MIN), f64::from(i32::MAX)), + SampleType::F32 if value.is_finite() && value.abs() > f64::from(f32::MAX) => { + return Err(RasterLoadError::Nodata { value, sample }); + } + SampleType::F32 => return Ok(Nodata::F32(value as f32)), + SampleType::F64 => return Ok(Nodata::F64(value)), + }; + let (lo, hi) = range; + if value.fract() == 0.0 && lo <= value && value <= hi { + Ok(Nodata::Int(value as i64)) + } else { + Err(RasterLoadError::Nodata { value, sample }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn integer_nodata_must_be_a_sample_value() { + assert_eq!(typed_nodata(0.0, SampleType::U16), Ok(Nodata::Int(0))); + assert_eq!( + typed_nodata(-9999.0, SampleType::I16), + Ok(Nodata::Int(-9999)) + ); + for (value, sample) in [ + (-1.0, SampleType::U16), + (65536.0, SampleType::U16), + (0.5, SampleType::U8), + (f64::NAN, SampleType::U32), + (1e39, SampleType::F32), + ] { + let err = typed_nodata(value, sample).expect_err("unrepresentable"); + assert_eq!(err.sqlstate(), "22023", "{value} as {sample}"); + } + } + + #[test] + fn nan_nodata_matches_nan_samples() { + let band = BandSamples::F32(vec![f32::NAN, 1.0]); + let nodata = typed_nodata(f64::NAN, SampleType::F32).expect("NaN is an f32 value"); + assert!(nodata.matches(&band, 0)); + assert!(!nodata.matches(&band, 1)); + let ints = BandSamples::U16(vec![0, 7]); + assert!(Nodata::Int(0).matches(&ints, 0)); + assert!(!Nodata::Int(0).matches(&ints, 1)); + } +} diff --git a/nodedb-raster-load/src/options.rs b/nodedb-raster-load/src/options.rs new file mode 100644 index 000000000..4a23f1fbd --- /dev/null +++ b/nodedb-raster-load/src/options.rs @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! The options a raster load plans from. The server converts its statement +//! options into this form, so the crate holds no SQL types for them. + +use crate::window::PixelWindow; + +/// Where one band of the load goes. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum BandTarget { + /// The band loads into this array attr. + Attr(String), + /// The band does not load. + Skip, +} + +/// The planning options of one load. +#[derive(Debug, Clone, PartialEq, Default)] +pub struct LoadOptions { + /// Entry `i` names where band `i` of the sources' bands in order loads. + /// `None` loads band `i` into attr `i`. + pub bands: Option>, + /// The nodata of every band, in place of the file's nodata. + pub nodata: Option, + /// The scene instant every loaded cell carries, in milliseconds since + /// the Unix epoch. + pub time_ms: Option, + /// The pixel window to load. `None` loads the whole image. + pub window: Option, +} diff --git a/nodedb-raster-load/src/plan.rs b/nodedb-raster-load/src/plan.rs new file mode 100644 index 000000000..7421a5c05 --- /dev/null +++ b/nodedb-raster-load/src/plan.rs @@ -0,0 +1,360 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! The open-time checks of a raster load. Every check runs before the +//! first block decodes, so a refused input commits no row. +//! +//! - The file CRS of every source must be the array GEOREF SRID (`22023`). +//! The server refuses an array with no GEOREF (`55000`) before planning. +//! - Bands map onto attrs by the lossless type rule (`42804`), and an attr +//! no band fills must be nullable (`23502`). +//! - The file grid must align with the array (`22023`), and the loaded +//! footprint must fit the dim domains (`22003`). +//! - `TIME` fills the one `TIMESTAMP` dim. Any other dim has no source. +//! - The nodata value must be a value of the band's sample type (`22023`). +//! +//! Nodata precedence: the COPY `NODATA` option, then the file's nodata of +//! the band (`GDAL_NODATA`, or a `GDAL_METADATA` band item), then the +//! `NODATA` metadata of the attr the band loads into. The COPY option +//! applies to every band. An attr nodata no sample can equal matches no +//! pixel. + +use nodedb_raster::{BlockWindow, SampleType}; + +use crate::bands::{AttrSource, map_bands}; +use crate::coords::{CoordSource, plan_coords}; +use crate::error::RasterLoadError; +use crate::grid::{CellGrid, GridInput, plan_grid}; +use crate::nodata::{Nodata, typed_nodata}; +use crate::options::LoadOptions; +use crate::source::SourceHeader; +use crate::view::{ArrayView, ViewDim, ViewGeoref}; +use crate::window::PixelWindow; + +/// Where one band of the load comes from. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct PlanBand { + /// The source index, in statement order. + pub source: usize, + /// The band index within the source. + pub band: usize, + pub sample: SampleType, +} + +/// How every pixel of an accepted input becomes a row. +#[derive(Debug)] +pub struct RasterPlan { + /// The image width, for row numbers. + pub(crate) width: u32, + /// The pixels that load. + pub(crate) window: PixelWindow, + /// Every source's bands in source order. + pub(crate) bands: Vec, + /// Bands per source. + pub(crate) source_bands: Vec, + /// One source per dim, in schema order. + pub(crate) coords: Vec, + /// One source per attr, in schema order. + pub(crate) attrs: Vec, + /// The bands that load. A pixel is skipped when none of them holds a + /// value. + pub(crate) loaded: Vec, + pub(crate) grid: CellGrid, + /// The nodata of each band, by band index. `None` matches no sample. + pub(crate) band_nodata: Vec>, +} + +impl RasterPlan { + /// Run every open-time check of `sources` against `view`. `sources` + /// share one grid, which [`crate::source::check_same_layout`] checks. + pub fn new( + view: &ArrayView, + georef: &ViewGeoref, + sources: &[SourceHeader], + options: &LoadOptions, + ) -> Result { + let first = sources.first().ok_or(RasterLoadError::NoBandLoads)?; + for source in sources { + check_srid(view, georef, source)?; + } + let bands: Vec = sources + .iter() + .enumerate() + .flat_map(|(source, header)| { + header + .bands + .iter() + .enumerate() + .map(move |(band, info)| PlanBand { + source, + band, + sample: info.sample_type, + }) + }) + .collect(); + let samples: Vec = bands.iter().map(|band| band.sample).collect(); + let map = map_bands(view, &samples, options.bands.as_deref())?; + let (x_index, x_dim) = georef_axis(view, &georef.x_dim)?; + let (y_index, y_dim) = georef_axis(view, &georef.y_dim)?; + let file_gt = first.transform.ok_or(RasterLoadError::NoTransform)?; + let window = PixelWindow::of(options.window, first.width, first.height)?; + let grid = plan_grid(&GridInput { + array: &view.name, + x_dim, + y_dim, + array_gt: georef.transform, + file_gt, + window, + })?; + let coords = plan_coords(view, x_index, y_index, options.time_ms)?; + let band_nodata = band_nodata(view, sources, &bands, options, &map.attrs)?; + Ok(Self { + width: first.width, + window, + source_bands: sources.iter().map(|s| s.bands.len()).collect(), + bands, + coords, + attrs: map.attrs, + loaded: map.loaded, + grid, + band_nodata, + }) + } + + /// The pixels that load. + pub fn window(&self) -> PixelWindow { + self.window + } + + /// Whether block `window` holds a pixel that loads. A load reads only + /// those blocks. + pub fn wants_block(&self, window: &BlockWindow) -> bool { + self.window.overlaps(window) + } + + /// The sources the plan reads, in statement order. + pub fn source_count(&self) -> usize { + self.source_bands.len() + } + + /// Where band `band` of the load comes from. + pub fn band(&self, band: usize) -> Option { + self.bands.get(band).copied() + } + + /// The attr sources, in schema order. + pub fn attrs(&self) -> &[AttrSource] { + &self.attrs + } +} + +/// The nodata of each band. The COPY option applies to every band. Without +/// it, a band takes the file's nodata of the band, else the `NODATA` of the +/// attr it loads into. The COPY option and the file nodata must be sample +/// values. +fn band_nodata( + view: &ArrayView, + sources: &[SourceHeader], + bands: &[PlanBand], + options: &LoadOptions, + attrs: &[AttrSource], +) -> Result>, RasterLoadError> { + if let Some(value) = options.nodata { + return bands + .iter() + .map(|band| typed_nodata(value, band.sample).map(Some)) + .collect(); + } + let mut out = vec![None; bands.len()]; + for (attr, source) in view.attrs.iter().zip(attrs) { + if let (AttrSource::Band { band, .. }, Some(value)) = (source, attr.meta.nodata) + && let (Some(slot), Some(plan)) = (out.get_mut(*band), bands.get(*band)) + { + *slot = typed_nodata(value, plan.sample).ok(); + } + } + for (slot, plan) in out.iter_mut().zip(bands) { + let file = sources + .get(plan.source) + .and_then(|source| source.bands.get(plan.band)) + .and_then(|info| info.nodata); + if let Some(value) = file { + *slot = Some(typed_nodata(value, plan.sample)?); + } + } + Ok(out) +} + +fn check_srid( + view: &ArrayView, + georef: &ViewGeoref, + source: &SourceHeader, +) -> Result<(), RasterLoadError> { + match source.srid { + Some(code) if code == georef.srid => Ok(()), + Some(code) => Err(RasterLoadError::CrsMismatch { + array: view.name.clone(), + file: code, + georef: georef.srid, + }), + None => Err(RasterLoadError::NoCrs { + array: view.name.clone(), + georef: georef.srid, + }), + } +} + +/// The index and spec of the dim a GEOREF axis names. +fn georef_axis<'v>( + view: &'v ArrayView, + name: &str, +) -> Result<(usize, &'v ViewDim), RasterLoadError> { + view.dims + .iter() + .enumerate() + .find(|(_, dim)| dim.name.eq_ignore_ascii_case(name)) + .ok_or_else(|| RasterLoadError::GeorefDim { + array: view.name.clone(), + dim: name.to_string(), + }) +} + +#[cfg(test)] +pub(crate) mod tests { + use super::*; + use crate::options::BandTarget; + use crate::view::{AttrType, DimType, DomainBound, ViewAttr}; + use nodedb_raster::GeoTransform; + use nodedb_raster::{BandInfo, CrsRef, RasterHeader}; + + pub(crate) const GT: [f64; 6] = [600_000.0, 30.0, 0.0, 700_000.0, 0.0, -30.0]; + + fn dim(name: &str) -> ViewDim { + ViewDim { + name: name.into(), + dtype: DimType::Int64, + lo: DomainBound::Int64(0), + hi: DomainBound::Int64(1_000), + } + } + + pub(crate) fn view() -> ArrayView { + let attr = |name: &str, dtype| ViewAttr { + name: name.into(), + dtype, + nullable: true, + meta: Default::default(), + }; + ArrayView { + name: "scene".into(), + dims: vec![dim("row"), dim("col")], + attrs: vec![ + attr("red", AttrType::UInt16), + attr("nir", AttrType::UInt16), + attr("qa", AttrType::UInt16), + ], + georef: None, + } + } + + pub(crate) fn georef() -> ViewGeoref { + ViewGeoref { + srid: 32_647, + x_dim: "col".into(), + y_dim: "row".into(), + transform: Some(GeoTransform(GT)), + } + } + + pub(crate) fn source(name: &str, info: BandInfo) -> SourceHeader { + let header = RasterHeader { + width: 256, + height: 192, + outer: Vec::new(), + bands: vec![info], + transform: Some(GeoTransform(GT)), + crs: CrsRef::Epsg(32_647), + }; + SourceHeader::new(name, &header, Some(32_647)) + } + + fn u16_band() -> BandInfo { + BandInfo::plain(SampleType::U16) + } + + #[test] + fn three_single_band_sources_fill_three_attrs() { + let sources = [ + source("/B4.TIF", u16_band()), + source("/B5.TIF", u16_band()), + source("/QA.TIF", u16_band()), + ]; + let options = LoadOptions { + bands: Some(vec![ + BandTarget::Attr("red".into()), + BandTarget::Attr("nir".into()), + BandTarget::Attr("qa".into()), + ]), + ..Default::default() + }; + let plan = RasterPlan::new(&view(), &georef(), &sources, &options).expect("plans"); + assert_eq!(plan.source_count(), 3); + assert_eq!(plan.loaded, vec![0, 1, 2]); + assert_eq!( + plan.band(2), + Some(PlanBand { + source: 2, + band: 0, + sample: SampleType::U16 + }) + ); + } + + #[test] + fn a_source_in_another_crs_is_refused() { + let mut other = source("/B5.TIF", u16_band()); + other.srid = Some(4326); + let sources = [source("/B4.TIF", u16_band()), other]; + let err = RasterPlan::new(&view(), &georef(), &sources, &Default::default()) + .expect_err("4326 against 32647"); + assert_eq!(err.sqlstate(), "22023"); + } + + #[test] + fn file_nodata_and_the_window_shape_the_plan() { + let mut info = u16_band(); + info.nodata = Some(0.0); + let sources = [source("/B4.TIF", info)]; + let options = LoadOptions { + window: Some(PixelWindow { + col0: 64, + row0: 0, + col1: 128, + row1: 64, + }), + ..Default::default() + }; + let plan = RasterPlan::new(&view(), &georef(), &sources, &options).expect("plans"); + assert_eq!(plan.band_nodata, vec![Some(Nodata::Int(0))]); + let block = |col_off| BlockWindow { + outer: Vec::new(), + col_off, + row_off: 0, + width: 64, + height: 64, + }; + assert!(!plan.wants_block(&block(0))); + assert!(plan.wants_block(&block(64))); + assert!(!plan.wants_block(&block(128))); + let past = LoadOptions { + window: Some(PixelWindow { + col0: 0, + row0: 0, + col1: 300, + row1: 10, + }), + ..Default::default() + }; + let err = RasterPlan::new(&view(), &georef(), &sources, &past).expect_err("past"); + assert_eq!(err.sqlstate(), "22023"); + } +} diff --git a/nodedb-raster-load/src/rows.rs b/nodedb-raster-load/src/rows.rs new file mode 100644 index 000000000..871c8b92d --- /dev/null +++ b/nodedb-raster-load/src/rows.rs @@ -0,0 +1,430 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! The rows of one block index, read one pixel at a time across every +//! source. +//! +//! A load reads the same block index from every source, so one row carries +//! every band's attr. A row's number is the pixel's row-major ordinal in +//! the image plus one. A pixel outside the loaded window is skipped. A band +//! holds no data at a pixel its mask marks invalid (an internal mask, its +//! LERC mask, or a block the file omits) or where it holds its nodata. A +//! pixel where no loaded band holds data is skipped. A kept pixel writes +//! NULL for a band that holds no data, or is refused with `23502` when the +//! attr is NOT NULL. Open checked the types and the footprint, so no other +//! check runs per pixel. +//! +//! Each kept pixel allocates its coordinate and attr vectors, sized +//! exactly. A skipped pixel allocates nothing. + +use nodedb_raster::{BandSamples, BlockWindow, RasterBlock}; + +use crate::bands::AttrSource; +use crate::coords::CoordSource; +use crate::error::RasterLoadError; +use crate::plan::RasterPlan; +use crate::view::{AttrLiteral, CoordLiteral, LoadRow}; + +/// The rows of one block index in pixel order within the block. +pub struct BlockRows<'a> { + plan: &'a RasterPlan, + /// One block per source, in source order, all of one window. + blocks: &'a [RasterBlock], + window: &'a BlockWindow, + next: usize, + pixels: usize, +} + +impl<'a> BlockRows<'a> { + /// Read `blocks`, one per source, under `plan`. A block with the wrong + /// band count, a short band, or a window unlike the first source's is a + /// malformed input (`22P04`). + pub fn new(plan: &'a RasterPlan, blocks: &'a [RasterBlock]) -> Result { + let shape = |index: u32, source: usize, what: String| RasterLoadError::BlockShape { + index, + source_index: source + 1, + what, + }; + let Some(first) = blocks.first() else { + return Err(shape(0, 0, "is missing".into())); + }; + if blocks.len() != plan.source_count() { + return Err(shape( + first.index, + 0, + format!( + "came from {} sources, the load reads {}", + blocks.len(), + plan.source_count() + ), + )); + } + let pixels = first.window.pixels(); + for (source, block) in blocks.iter().enumerate() { + if block.window != first.window { + return Err(shape( + block.index, + source, + "covers another window than source 1".into(), + )); + } + let declared = plan.source_bands.get(source).copied().unwrap_or(0); + if block.bands.len() != declared { + return Err(shape( + block.index, + source, + format!( + "decoded {} bands, the file declares {declared}", + block.bands.len() + ), + )); + } + if let Some(short) = block.bands.iter().find(|band| band.len() < pixels) { + return Err(shape( + block.index, + source, + format!("decoded {} samples, its window holds {pixels}", short.len()), + )); + } + } + Ok(Self { + plan, + blocks, + window: &first.window, + next: 0, + pixels, + }) + } + + /// The samples of load band `band`, and the block that holds them. + fn band_block(&self, band: usize) -> Option<(&'a RasterBlock, usize)> { + let at = self.plan.band(band)?; + let block = self.blocks.get(at.source)?; + Some((block, at.band)) + } + + /// Whether load band `band` holds data at pixel `index`: its mask + /// marks the pixel valid and the sample is not its nodata. + fn holds_data(&self, band: usize, index: usize) -> bool { + let Some((block, local)) = self.band_block(band) else { + return false; + }; + if !block.band_valid(local, index) { + return false; + } + match ( + self.plan.band_nodata.get(band).copied().flatten(), + block.bands.get(local), + ) { + (Some(nodata), Some(samples)) => !nodata.matches(samples, index), + (None, Some(_)) => true, + (_, None) => false, + } + } + + /// The image pixel `(col, row)` of window pixel `index`. + fn pixel(&self, index: usize) -> (u32, u32) { + let window = self.window; + let local_row = index / window.width as usize; + let local_col = index % window.width as usize; + // The window lies inside the image, so both fit `u32`. + ( + window.col_off + local_col as u32, + window.row_off + local_row as u32, + ) + } + + /// The row of pixel `index` of the block. + fn row(&self, index: usize) -> Result<(usize, LoadRow), RasterLoadError> { + let (col, row) = self.pixel(index); + let row_no = row as usize * self.plan.width as usize + col as usize + 1; + let (x, y) = self.plan.grid.cell(col, row); + let mut coords = Vec::with_capacity(self.plan.coords.len()); + for source in &self.plan.coords { + coords.push(match source { + CoordSource::X => x.clone(), + CoordSource::Y => y.clone(), + CoordSource::Time(ms) => CoordLiteral::Int64(*ms), + }); + } + let mut attrs = Vec::with_capacity(self.plan.attrs.len()); + for source in &self.plan.attrs { + let AttrSource::Band { + band, + float, + nullable, + name, + } = source + else { + attrs.push(AttrLiteral::Null); + continue; + }; + let literal = match (self.holds_data(*band, index), *nullable) { + (false, true) => AttrLiteral::Null, + (false, false) => { + return Err(RasterLoadError::NodataInNotNull { + row: row_no, + attr: name.clone(), + band: band + 1, + pixel_row: row, + pixel_col: col, + }); + } + (true, _) => self + .band_block(*band) + .and_then(|(block, local)| block.bands.get(local)) + .and_then(|samples| sample_literal(samples, index, *float)) + .ok_or(RasterLoadError::MissingSample { + row: row_no, + band: band + 1, + })?, + }; + attrs.push(literal); + } + Ok((row_no, LoadRow { coords, attrs })) + } + + /// Whether pixel `index` loads: inside the window, and some loaded band + /// holds data there. + fn keeps(&self, index: usize) -> bool { + let (col, row) = self.pixel(index); + self.plan.window.contains(col, row) + && self + .plan + .loaded + .iter() + .any(|&band| self.holds_data(band, index)) + } +} + +impl Iterator for BlockRows<'_> { + type Item = Result<(usize, LoadRow), RasterLoadError>; + + fn next(&mut self) -> Option { + while self.next < self.pixels { + let index = self.next; + self.next += 1; + if self.keeps(index) { + return Some(self.row(index)); + } + } + None + } +} + +/// Sample `index` as an attr literal. Open admitted only lossless pairs, +/// so an integer sample widens to `f64` exactly when the attr is a float. +fn sample_literal(samples: &BandSamples, index: usize, float: bool) -> Option { + let int = |n: i64| { + if float { + AttrLiteral::Float64(n as f64) + } else { + AttrLiteral::Int64(n) + } + }; + match samples { + BandSamples::U8(v) => v.get(index).map(|s| int(i64::from(*s))), + BandSamples::I8(v) => v.get(index).map(|s| int(i64::from(*s))), + BandSamples::U16(v) => v.get(index).map(|s| int(i64::from(*s))), + BandSamples::I16(v) => v.get(index).map(|s| int(i64::from(*s))), + BandSamples::U32(v) => v.get(index).map(|s| int(i64::from(*s))), + BandSamples::I32(v) => v.get(index).map(|s| int(i64::from(*s))), + BandSamples::F32(v) => v.get(index).map(|s| AttrLiteral::Float64(f64::from(*s))), + BandSamples::F64(v) => v.get(index).map(|s| AttrLiteral::Float64(*s)), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::grid::CellGrid; + use crate::nodata::Nodata; + use crate::plan::PlanBand; + use crate::window::PixelWindow; + use nodedb_raster::SampleType; + + fn band(source: usize, band: usize) -> PlanBand { + PlanBand { + source, + band, + sample: SampleType::U16, + } + } + + fn plan(nullable: bool) -> RasterPlan { + RasterPlan { + width: 4, + window: PixelWindow::whole(4, 2), + bands: vec![band(0, 0), band(0, 1)], + source_bands: vec![2], + coords: vec![CoordSource::Y, CoordSource::X], + attrs: vec![ + AttrSource::Band { + band: 0, + float: false, + nullable, + name: "b1".into(), + }, + AttrSource::Band { + band: 1, + float: true, + nullable: true, + name: "b2".into(), + }, + AttrSource::Null, + ], + loaded: vec![0, 1], + grid: CellGrid::Pixel { col0: 10, row0: 20 }, + band_nodata: vec![Some(Nodata::Int(0)), Some(Nodata::Int(0))], + } + } + + fn window() -> BlockWindow { + BlockWindow { + outer: Vec::new(), + col_off: 2, + row_off: 0, + width: 2, + height: 2, + } + } + + fn block() -> RasterBlock { + RasterBlock { + index: 1, + window: window(), + bands: vec![ + BandSamples::U16(vec![5, 0, 0, 7]), + BandSamples::U16(vec![6, 0, 9, 8]), + ], + masks: vec![None, None], + } + } + + fn rows(plan: &RasterPlan, blocks: &[RasterBlock]) -> Vec<(usize, LoadRow)> { + BlockRows::new(plan, blocks) + .expect("block fits") + .collect::>() + .expect("all rows load") + } + + /// Attr metadata gives each band its own nodata: a band with none never + /// holds nodata, so no pixel is skipped and only band 1 writes NULL. + #[test] + fn per_band_nodata_applies_to_its_band_only() { + let mut plan = plan(true); + plan.band_nodata = vec![Some(Nodata::Int(0)), None]; + let rows = rows(&plan, &[block()]); + assert_eq!(rows.len(), 4); + let (_, both_zero) = &rows[1]; + assert_eq!(both_zero.attrs[0], AttrLiteral::Null); + assert_eq!(both_zero.attrs[1], AttrLiteral::Float64(0.0)); + } + + #[test] + fn rows_skip_all_nodata_pixels_and_null_partial_ones() { + let rows = rows(&plan(true), &[block()]); + let numbers: Vec = rows.iter().map(|(n, _)| *n).collect(); + // Pixel (0, 3) is nodata in both bands. Row-major ordinals: 2, 6, 7. + assert_eq!(numbers, vec![3, 7, 8]); + let (_, first) = &rows[0]; + assert_eq!( + first.coords, + vec![CoordLiteral::Int64(20), CoordLiteral::Int64(12)] + ); + assert_eq!( + first.attrs, + vec![ + AttrLiteral::Int64(5), + AttrLiteral::Float64(6.0), + AttrLiteral::Null + ] + ); + let (_, partial) = &rows[1]; + assert_eq!(partial.attrs[0], AttrLiteral::Null); + assert_eq!(partial.attrs[1], AttrLiteral::Float64(9.0)); + } + + #[test] + fn a_nodata_band_in_a_not_null_attr_names_the_pixel() { + let (plan, blocks) = (plan(false), [block()]); + let err = BlockRows::new(&plan, &blocks) + .expect("block fits") + .find_map(Result::err) + .expect("pixel (1, 2) is refused"); + assert_eq!(err.sqlstate(), "23502"); + assert_eq!(err.row(), 7); + assert!(err.to_string().contains("(pixel row 1, col 2)"), "{err}"); + } + + #[test] + fn a_band_mask_nulls_its_band_and_an_all_masked_pixel_is_skipped() { + let plan = plan(true); + let mut block = block(); + block.masks = vec![ + Some(vec![false, true, true, false]), + Some(vec![true, true, true, false]), + ]; + let rows = rows(&plan, &[block]); + let numbers: Vec = rows.iter().map(|(n, _)| *n).collect(); + // Pixel 1 is nodata in both bands; pixel 3 is masked in both. + assert_eq!(numbers, vec![3, 7]); + let (_, masked_first) = &rows[0]; + assert_eq!(masked_first.attrs[0], AttrLiteral::Null); + assert_eq!(masked_first.attrs[1], AttrLiteral::Float64(6.0)); + } + + #[test] + fn sources_load_in_lockstep_into_one_row() { + let mut plan = plan(true); + plan.bands = vec![band(0, 0), band(1, 0)]; + plan.source_bands = vec![1, 1]; + let red = RasterBlock { + index: 1, + window: window(), + bands: vec![BandSamples::U16(vec![5, 0, 0, 7])], + masks: vec![None], + }; + let nir = RasterBlock { + index: 1, + window: window(), + bands: vec![BandSamples::U16(vec![6, 0, 9, 8])], + masks: vec![None], + }; + let rows = rows(&plan, &[red.clone(), nir.clone()]); + let (_, first) = &rows[0]; + assert_eq!(first.attrs[0], AttrLiteral::Int64(5)); + assert_eq!(first.attrs[1], AttrLiteral::Float64(6.0)); + assert_eq!(rows.len(), 3); + let mut shifted = nir; + shifted.window.col_off = 0; + let err = BlockRows::new(&plan, &[red, shifted]) + .err() + .expect("windows differ"); + assert_eq!(err.sqlstate(), "22P04"); + } + + #[test] + fn pixels_outside_the_window_are_skipped() { + let mut plan = plan(true); + plan.window = PixelWindow { + col0: 3, + row0: 0, + col1: 4, + row1: 1, + }; + let numbers: Vec = rows(&plan, &[block()]).iter().map(|(n, _)| *n).collect(); + assert_eq!(numbers, Vec::::new()); + plan.window.col0 = 2; + let numbers: Vec = rows(&plan, &[block()]).iter().map(|(n, _)| *n).collect(); + assert_eq!(numbers, vec![3]); + } + + #[test] + fn a_short_block_is_malformed() { + let plan = plan(true); + let mut block = block(); + block.bands[1] = BandSamples::U16(vec![1]); + let err = BlockRows::new(&plan, &[block]).err().expect("short band"); + assert_eq!(err.sqlstate(), "22P04"); + } +} diff --git a/nodedb-raster-load/src/source.rs b/nodedb-raster-load/src/source.rs new file mode 100644 index 000000000..f4f9df55c --- /dev/null +++ b/nodedb-raster-load/src/source.rs @@ -0,0 +1,215 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! What each source's header says, and the check that every source of one +//! load shares a grid and a block layout. +//! +//! A multi-source load reads the same block index from every source in +//! lockstep, so each cell write carries the bands of every source. That +//! needs one image size, one geotransform, one CRS and one block grid. + +use nodedb_raster::{BandInfo, BlockWindow, GeoTransform, RasterError, RasterHeader, RasterReader}; + +use crate::error::RasterLoadError; + +/// The header facts of one source a load plans from. +#[derive(Debug, Clone, PartialEq)] +pub struct SourceHeader { + /// The source as the statement wrote it, for messages. + pub name: String, + pub width: u32, + pub height: u32, + /// One entry per band of the source, in file order. + pub bands: Vec, + /// The GDAL-order pixel-corner geotransform. `None` when the file has + /// none. + pub transform: Option, + /// The SRID the file CRS resolves to. `None` when it names none. + pub srid: Option, +} + +impl SourceHeader { + /// The facts of `header`, with its CRS resolved to `srid`. + pub fn new(name: impl Into, header: &RasterHeader, srid: Option) -> Self { + Self { + name: name.into(), + width: header.width, + height: header.height, + bands: header.bands.clone(), + transform: header.transform, + srid, + } + } +} + +/// Why two sources do not share a layout: a header mismatch, or a block +/// window the reader cannot name. +#[derive(Debug, thiserror::Error)] +pub enum LayoutError { + #[error(transparent)] + Mismatch(#[from] RasterLoadError), + #[error(transparent)] + Reader(#[from] RasterError), +} + +/// Check that every source shares the first source's image size, +/// geotransform, CRS and block grid. `headers[i]` describes `readers[i]`. +pub fn check_same_layout( + headers: &[SourceHeader], + readers: &[R], +) -> Result<(), LayoutError> { + let (Some(first), Some(first_reader)) = (headers.first(), readers.first()) else { + return Ok(()); + }; + for (offset, (header, reader)) in headers.iter().zip(readers).enumerate().skip(1) { + let mismatch = |what: String| RasterLoadError::SourceMismatch { + index: offset + 1, + source_name: header.name.clone(), + first: first.name.clone(), + what, + }; + if (header.width, header.height) != (first.width, first.height) { + return Err(mismatch(format!( + "grid {}x{} (source 1 is {}x{})", + header.width, header.height, first.width, first.height + )) + .into()); + } + let transform = header.transform.map(|gt| gt.0); + let first_transform = first.transform.map(|gt| gt.0); + if transform != first_transform { + return Err(mismatch(format!( + "geotransform {transform:?} (source 1 is {first_transform:?})" + )) + .into()); + } + if header.srid != first.srid { + return Err(mismatch(format!( + "CRS {} (source 1 is {})", + srid_text(header.srid), + srid_text(first.srid) + )) + .into()); + } + let count = reader.block_count(); + if count != first_reader.block_count() { + return Err(mismatch(format!( + "block count {count} (source 1 has {})", + first_reader.block_count() + )) + .into()); + } + for index in 0..count { + let window = reader.block_window(index)?; + let expected = first_reader.block_window(index)?; + if window != expected { + return Err(mismatch(format!( + "block {index} window {} (source 1 is {})", + window_text(&window), + window_text(&expected) + )) + .into()); + } + } + } + Ok(()) +} + +fn srid_text(srid: Option) -> String { + srid.map_or_else(|| "none".to_string(), |code| format!("SRID {code}")) +} + +fn window_text(window: &BlockWindow) -> String { + format!( + "{}x{} at ({}, {})", + window.width, window.height, window.col_off, window.row_off + ) +} + +#[cfg(test)] +mod tests { + use super::*; + use nodedb_raster::{CrsRef, RasterBlock, SampleType}; + + /// A reader of one `width x height` image in `tile`-sized blocks. + struct Grid { + header: RasterHeader, + tile: u32, + } + + impl RasterReader for Grid { + fn header(&self) -> &RasterHeader { + &self.header + } + + fn block_count(&self) -> u32 { + self.header.width.div_ceil(self.tile) * self.header.height.div_ceil(self.tile) + } + + fn block_window(&self, index: u32) -> Result { + let across = self.header.width.div_ceil(self.tile); + let (col_off, row_off) = ((index % across) * self.tile, (index / across) * self.tile); + Ok(BlockWindow { + outer: Vec::new(), + col_off, + row_off, + width: self.tile.min(self.header.width - col_off), + height: self.tile.min(self.header.height - row_off), + }) + } + + fn block_ranges(&self, _: u32) -> Result, RasterError> { + Ok(Vec::new()) + } + + fn decode_block( + &self, + index: u32, + _: &[Option<&[u8]>], + ) -> Result { + Err(RasterError::BlockOutOfRange { index, count: 0 }) + } + } + + fn grid(width: u32, tile: u32) -> Grid { + Grid { + header: RasterHeader { + width, + height: 64, + outer: Vec::new(), + bands: vec![BandInfo::plain(SampleType::U16)], + transform: Some(GeoTransform([0.0, 1.0, 0.0, 0.0, 0.0, -1.0])), + crs: CrsRef::Epsg(4326), + }, + tile, + } + } + + fn headers(readers: &[Grid]) -> Vec { + readers + .iter() + .enumerate() + .map(|(i, r)| SourceHeader::new(format!("/b{i}.tif"), &r.header, Some(4326))) + .collect() + } + + #[test] + fn sources_of_one_grid_and_tiling_pass() { + let readers = [grid(128, 32), grid(128, 32)]; + assert!(check_same_layout(&headers(&readers), &readers).is_ok()); + } + + #[test] + fn a_mismatched_grid_or_tiling_names_the_source() { + for readers in [ + [grid(128, 32), grid(96, 32)], + [grid(128, 32), grid(128, 64)], + ] { + let Err(LayoutError::Mismatch(err)) = check_same_layout(&headers(&readers), &readers) + else { + panic!("refused"); + }; + assert_eq!(err.sqlstate(), "22023"); + assert!(err.to_string().contains("/b1.tif"), "{err}"); + } + } +} diff --git a/nodedb-raster-load/src/view/mod.rs b/nodedb-raster-load/src/view/mod.rs new file mode 100644 index 000000000..51ec7d0a3 --- /dev/null +++ b/nodedb-raster-load/src/view/mod.rs @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! The array a load writes into, and the rows and metadata it produces, as +//! plain types. The server converts its catalog view into these and the +//! results back, so the crate holds no SQL types. + +pub mod row; +pub mod schema; + +pub use row::{AdoptedMeta, AttrLiteral, CoordLiteral, LoadRow}; +pub use schema::{ + ArrayView, AttrMeta, AttrType, DimType, DomainBound, ViewAttr, ViewDim, ViewGeoref, +}; diff --git a/nodedb-raster-load/src/view/row.rs b/nodedb-raster-load/src/view/row.rs new file mode 100644 index 000000000..17026d4e9 --- /dev/null +++ b/nodedb-raster-load/src/view/row.rs @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! What a load produces: one row per kept pixel, and the attr metadata +//! `ADOPT_METADATA` takes from the files. + +use super::schema::AttrMeta; + +/// One coordinate of a row. A raster yields integer and float coordinates +/// only. +#[derive(Debug, Clone, PartialEq)] +pub enum CoordLiteral { + Int64(i64), + Float64(f64), +} + +/// One attr value of a row. +#[derive(Debug, Clone, PartialEq)] +pub enum AttrLiteral { + Int64(i64), + Float64(f64), + Null, +} + +/// One row: a coordinate per dim and a value per attr, in schema order. +#[derive(Debug, Clone, PartialEq)] +pub struct LoadRow { + pub coords: Vec, + pub attrs: Vec, +} + +/// The metadata adoption sets on one attr. +#[derive(Debug, Clone, PartialEq)] +pub struct AdoptedMeta { + pub attr: String, + pub meta: AttrMeta, +} diff --git a/nodedb-raster-load/src/view/schema.rs b/nodedb-raster-load/src/view/schema.rs new file mode 100644 index 000000000..a332b1c0a --- /dev/null +++ b/nodedb-raster-load/src/view/schema.rs @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! The dims, attrs and georef of the array a load writes into. + +use nodedb_raster::GeoTransform; + +/// The array a load plans against. +#[derive(Debug, Clone, PartialEq)] +pub struct ArrayView { + pub name: String, + /// Dims in schema order. + pub dims: Vec, + /// Attrs in schema order. + pub attrs: Vec, + /// The georef, axes named by dim. `None` when the array has none. + pub georef: Option, +} + +/// One dim and its domain. +#[derive(Debug, Clone, PartialEq)] +pub struct ViewDim { + pub name: String, + pub dtype: DimType, + pub lo: DomainBound, + pub hi: DomainBound, +} + +/// The type of a dim. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DimType { + Int64, + Float64, + TimestampMs, + String, +} + +/// A domain bound, tagged with its dim's type. +#[derive(Debug, Clone, PartialEq)] +pub enum DomainBound { + Int64(i64), + Float64(f64), + TimestampMs(i64), + String(String), +} + +/// One attr. +#[derive(Debug, Clone, PartialEq)] +pub struct ViewAttr { + pub name: String, + pub dtype: AttrType, + pub nullable: bool, + pub meta: AttrMeta, +} + +/// The type of an attr. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AttrType { + Int8, + UInt8, + Int16, + UInt16, + Int32, + UInt32, + Int64, + Float32, + Float64, + String, + Bytes, +} + +impl AttrType { + /// Whether the type holds floats. + pub fn is_float(self) -> bool { + match self { + AttrType::Float32 | AttrType::Float64 => true, + AttrType::Int8 + | AttrType::UInt8 + | AttrType::Int16 + | AttrType::UInt16 + | AttrType::Int32 + | AttrType::UInt32 + | AttrType::Int64 + | AttrType::String + | AttrType::Bytes => false, + } + } + + /// The DDL name of the type. + pub fn sql_name(self) -> &'static str { + match self { + AttrType::Int8 => "INT8", + AttrType::UInt8 => "UINT8", + AttrType::Int16 => "INT16", + AttrType::UInt16 => "UINT16", + AttrType::Int32 => "INT32", + AttrType::UInt32 => "UINT32", + AttrType::Int64 => "INT64", + AttrType::Float32 => "FLOAT32", + AttrType::Float64 => "FLOAT64", + AttrType::String => "STRING", + AttrType::Bytes => "BYTES", + } + } +} + +/// The metadata of an attr. +#[derive(Debug, Clone, Default, PartialEq)] +pub struct AttrMeta { + pub nodata: Option, + pub scale: Option, + pub offset: Option, + pub units: Option, +} + +/// The georef of an array: the SRID, the dims of the x and y axes, and the +/// transform of a pixel-mode array. `None` is a world-mode array. +#[derive(Debug, Clone, PartialEq)] +pub struct ViewGeoref { + pub srid: u32, + pub x_dim: String, + pub y_dim: String, + pub transform: Option, +} diff --git a/nodedb-raster-load/src/window.rs b/nodedb-raster-load/src/window.rs new file mode 100644 index 000000000..741c40dda --- /dev/null +++ b/nodedb-raster-load/src/window.rs @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! The pixel window a load reads: the whole image, or `WINDOW (...)`. + +use nodedb_raster::BlockWindow; + +use crate::error::RasterLoadError; + +/// The half-open pixel window `cols x rows` of an image. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct PixelWindow { + pub col0: u32, + pub row0: u32, + /// One past the last column. + pub col1: u32, + /// One past the last row. + pub row1: u32, +} + +impl PixelWindow { + /// The whole `width x height` image. + pub const fn whole(width: u32, height: u32) -> Self { + Self { + col0: 0, + row0: 0, + col1: width, + row1: height, + } + } + + /// The requested `window` of a `width x height` image, or the whole + /// image when no window is given. An empty window or one past the image + /// is refused (`22023`). + pub fn of( + window: Option, + width: u32, + height: u32, + ) -> Result { + let Some(w) = window else { + return Ok(Self::whole(width, height)); + }; + if w.col0 >= w.col1 || w.row0 >= w.row1 || w.col1 > width || w.row1 > height { + return Err(RasterLoadError::WindowOutside { + x0: w.col0, + y0: w.row0, + x1: w.col1, + y1: w.row1, + width, + height, + }); + } + Ok(w) + } + + /// Whether pixel `(col, row)` lies inside. + pub fn contains(&self, col: u32, row: u32) -> bool { + (self.col0..self.col1).contains(&col) && (self.row0..self.row1).contains(&row) + } + + /// Whether `block` holds any pixel of the window. + pub fn overlaps(&self, block: &BlockWindow) -> bool { + let block_col1 = block.col_off.saturating_add(block.width); + let block_row1 = block.row_off.saturating_add(block.height); + block.width > 0 + && block.height > 0 + && block.col_off < self.col1 + && self.col0 < block_col1 + && block.row_off < self.row1 + && self.row0 < block_row1 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn block(col_off: u32, row_off: u32, width: u32, height: u32) -> BlockWindow { + BlockWindow { + outer: Vec::new(), + col_off, + row_off, + width, + height, + } + } + + #[test] + fn a_window_selects_the_blocks_it_touches() { + let w = PixelWindow::of( + Some(PixelWindow { + col0: 300, + row0: 0, + col1: 600, + row1: 256, + }), + 1024, + 1024, + ) + .expect("inside"); + assert!(!w.overlaps(&block(0, 0, 256, 256))); + assert!(w.overlaps(&block(256, 0, 256, 256))); + assert!(w.overlaps(&block(512, 0, 256, 256))); + assert!(!w.overlaps(&block(768, 0, 256, 256))); + assert!(!w.overlaps(&block(256, 256, 256, 256))); + assert!(w.contains(300, 255)); + assert!(!w.contains(600, 0)); + } + + #[test] + fn a_window_past_the_image_is_refused() { + let past = PixelWindow { + col0: 0, + row0: 0, + col1: 1025, + row1: 10, + }; + let err = PixelWindow::of(Some(past), 1024, 1024).expect_err("past the edge"); + assert_eq!(err.sqlstate(), "22023"); + assert_eq!(PixelWindow::of(None, 4, 3), Ok(PixelWindow::whole(4, 3))); + } +} diff --git a/nodedb-raster/Cargo.toml b/nodedb-raster/Cargo.toml index f4d530380..c9dd53868 100644 --- a/nodedb-raster/Cargo.toml +++ b/nodedb-raster/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nodedb-raster" -description = "Bounded GeoTIFF decoding for NodeDB array bulk loads" +description = "Sans-IO raster decoding (GeoTIFF/COG) for NodeDB array bulk loads" version.workspace = true edition.workspace = true rust-version.workspace = true @@ -9,10 +9,19 @@ authors = ["Biz Impulse Solutions Sdn. Bhd. and the NodeDB contributors"] repository.workspace = true homepage.workspace = true documentation = "https://docs.rs/nodedb-raster" -keywords = ["geotiff", "raster", "tiff", "geospatial"] +keywords = ["geotiff", "raster", "tiff", "cog", "geospatial"] categories = ["science::geo", "parser-implementations"] [dependencies] nodedb-types = { workspace = true } +nodedb-crs = { workspace = true } thiserror = { workspace = true } +flate2 = { workspace = true } +weezl = { workspace = true } +ruzstd = { workspace = true } +zune-jpeg = { workspace = true } +lerc-rs = { workspace = true } + +[dev-dependencies] +# Builds in-test TIFF fixtures with an independent encoder. tiff = { workspace = true } diff --git a/nodedb-raster/src/block.rs b/nodedb-raster/src/block.rs index d941f6ace..f2b733d12 100644 --- a/nodedb-raster/src/block.rs +++ b/nodedb-raster/src/block.rs @@ -2,22 +2,18 @@ //! Decoded blocks: one typed, row-major sample vector per band. -use std::mem::size_of; - -use nodedb_types::decode_bounds::checked_decode_capacity; -use tiff::decoder::DecodingResult; - -use crate::error::RasterError; use crate::layout::BlockWindow; -use crate::sample::{SampleFormatCode, SampleType}; +use crate::sample::SampleType; /// One band's samples for one block, row-major, `width * height` long. #[derive(Debug, Clone, PartialEq)] pub enum BandSamples { U8(Vec), + I8(Vec), U16(Vec), I16(Vec), U32(Vec), + I32(Vec), F32(Vec), F64(Vec), } @@ -27,9 +23,11 @@ impl BandSamples { pub fn len(&self) -> usize { match self { BandSamples::U8(v) => v.len(), + BandSamples::I8(v) => v.len(), BandSamples::U16(v) => v.len(), BandSamples::I16(v) => v.len(), BandSamples::U32(v) => v.len(), + BandSamples::I32(v) => v.len(), BandSamples::F32(v) => v.len(), BandSamples::F64(v) => v.len(), } @@ -44,9 +42,11 @@ impl BandSamples { pub fn sample_type(&self) -> SampleType { match self { BandSamples::U8(_) => SampleType::U8, + BandSamples::I8(_) => SampleType::I8, BandSamples::U16(_) => SampleType::U16, BandSamples::I16(_) => SampleType::I16, BandSamples::U32(_) => SampleType::U32, + BandSamples::I32(_) => SampleType::I32, BandSamples::F32(_) => SampleType::F32, BandSamples::F64(_) => SampleType::F64, } @@ -57,135 +57,43 @@ impl BandSamples { pub fn value_f64(&self, index: usize) -> Option { match self { BandSamples::U8(v) => v.get(index).map(|s| f64::from(*s)), + BandSamples::I8(v) => v.get(index).map(|s| f64::from(*s)), BandSamples::U16(v) => v.get(index).map(|s| f64::from(*s)), BandSamples::I16(v) => v.get(index).map(|s| f64::from(*s)), BandSamples::U32(v) => v.get(index).map(|s| f64::from(*s)), + BandSamples::I32(v) => v.get(index).map(|s| f64::from(*s)), BandSamples::F32(v) => v.get(index).map(|s| f64::from(*s)), BandSamples::F64(v) => v.get(index).copied(), } } } -/// One decoded block: its window and one sample vector per band. +/// One decoded block: its window, one sample vector per band, and one +/// validity mask per band. #[derive(Debug, Clone, PartialEq)] pub struct RasterBlock { pub index: u32, pub window: BlockWindow, pub bands: Vec, + /// One mask per band, in band order: one flag per window pixel, + /// row-major. `false` marks a sample that holds no data: masked by an + /// internal mask (every band), invalid in the band's LERC mask, or inside + /// a block the file omits. `None` when every pixel of the band is valid. + pub masks: Vec>>, } -/// What a decoded chunk must hold. -#[derive(Debug, Clone, Copy)] -pub(crate) struct ChunkShape { - pub index: u32, - pub pixels: usize, - pub bands: usize, - pub limit_bytes: usize, -} - -/// Split a chunky chunk, `bands` samples per pixel, into one vector per -/// band. -pub(crate) fn chunky_bands( - result: DecodingResult, - shape: ChunkShape, -) -> Result, RasterError> { - match result { - DecodingResult::U8(v) => deinterleave(v, shape, BandSamples::U8), - DecodingResult::U16(v) => deinterleave(v, shape, BandSamples::U16), - DecodingResult::I16(v) => deinterleave(v, shape, BandSamples::I16), - DecodingResult::U32(v) => deinterleave(v, shape, BandSamples::U32), - DecodingResult::F32(v) => deinterleave(v, shape, BandSamples::F32), - DecodingResult::F64(v) => deinterleave(v, shape, BandSamples::F64), - other => Err(unsupported(&other)), - } -} - -/// Crop a planar chunk to the block's pixels. A plane's rows are packed at -/// the block width, so the first `pixels` samples are the block. -pub(crate) fn planar_band( - result: DecodingResult, - shape: ChunkShape, -) -> Result { - match result { - DecodingResult::U8(v) => crop(v, shape).map(BandSamples::U8), - DecodingResult::U16(v) => crop(v, shape).map(BandSamples::U16), - DecodingResult::I16(v) => crop(v, shape).map(BandSamples::I16), - DecodingResult::U32(v) => crop(v, shape).map(BandSamples::U32), - DecodingResult::F32(v) => crop(v, shape).map(BandSamples::F32), - DecodingResult::F64(v) => crop(v, shape).map(BandSamples::F64), - other => Err(unsupported(&other)), - } -} - -fn crop(mut samples: Vec, shape: ChunkShape) -> Result, RasterError> { - if samples.len() < shape.pixels { - return Err(RasterError::ShortBlock { - index: shape.index, - found: samples.len(), - expected: shape.pixels, - }); - } - samples.truncate(shape.pixels); - Ok(samples) -} - -fn deinterleave( - samples: Vec, - shape: ChunkShape, - wrap: fn(Vec) -> BandSamples, -) -> Result, RasterError> { - let needed = shape.pixels.saturating_mul(shape.bands); - if samples.len() < needed { - return Err(RasterError::ShortBlock { - index: shape.index, - found: samples.len(), - expected: needed, - }); - } - if shape.bands == 1 { - return crop(samples, shape).map(|band| vec![wrap(band)]); +impl RasterBlock { + /// Whether band `band` holds data at pixel `index` of the window. + pub fn band_valid(&self, band: usize, index: usize) -> bool { + match self.masks.get(band) { + Some(Some(mask)) => mask.get(index).copied().unwrap_or(false), + Some(None) | None => index < self.window.pixels(), + } } - let capacity = checked_decode_capacity( - shape.pixels, - size_of::(), - samples.len(), - shape.bands, - usize::MAX, - shape.limit_bytes, - ) - .ok_or(RasterError::BlockTooLarge { - needed_bytes: (needed as u64).saturating_mul(size_of::() as u64), - limit_bytes: shape.limit_bytes, - })?; - let bands = (0..shape.bands) - .map(|band| { - let mut out = Vec::with_capacity(capacity); - let interleaved = samples.get(band..needed).unwrap_or_default(); - out.extend(interleaved.iter().step_by(shape.bands).copied()); - wrap(out) - }) - .collect(); - Ok(bands) -} -/// The typed error for a sample type the open-time check refuses. -fn unsupported(result: &DecodingResult) -> RasterError { - let (format, bits) = match result { - DecodingResult::I8(_) => (2, 8), - DecodingResult::I32(_) => (2, 32), - DecodingResult::I64(_) => (2, 64), - DecodingResult::U64(_) => (1, 64), - DecodingResult::F16(_) => (3, 16), - DecodingResult::U8(_) => (1, 8), - DecodingResult::U16(_) => (1, 16), - DecodingResult::I16(_) => (2, 16), - DecodingResult::U32(_) => (1, 32), - DecodingResult::F32(_) => (3, 32), - DecodingResult::F64(_) => (3, 64), - }; - RasterError::UnsupportedSampleType { - format: SampleFormatCode(format), - bits, + /// Whether any band holds data at pixel `index` of the window. + pub fn any_valid(&self, index: usize) -> bool { + (0..self.bands.len()).any(|band| self.band_valid(band, index)) } } @@ -193,83 +101,6 @@ fn unsupported(result: &DecodingResult) -> RasterError { mod tests { use super::*; - fn shape(pixels: usize, bands: usize) -> ChunkShape { - ChunkShape { - index: 0, - pixels, - bands, - limit_bytes: 1 << 20, - } - } - - #[test] - fn chunky_samples_split_per_band() { - let result = DecodingResult::U16(vec![1, 10, 100, 2, 20, 200]); - let bands = chunky_bands(result, shape(2, 3)).expect("splits"); - assert_eq!( - bands, - vec![ - BandSamples::U16(vec![1, 2]), - BandSamples::U16(vec![10, 20]), - BandSamples::U16(vec![100, 200]), - ] - ); - } - - #[test] - fn single_band_keeps_its_buffer() { - let result = DecodingResult::F32(vec![1.5, 2.5, 9.0]); - let bands = chunky_bands(result, shape(2, 1)).expect("splits"); - assert_eq!(bands, vec![BandSamples::F32(vec![1.5, 2.5])]); - } - - #[test] - fn planar_plane_is_cropped_to_the_block() { - let result = DecodingResult::I16(vec![-1, -2, -3, -4, 0, 0]); - let band = planar_band(result, shape(4, 1)).expect("crops"); - assert_eq!(band, BandSamples::I16(vec![-1, -2, -3, -4])); - } - - #[test] - fn short_chunks_are_typed_errors() { - assert!(matches!( - chunky_bands(DecodingResult::U8(vec![1, 2, 3]), shape(2, 2)), - Err(RasterError::ShortBlock { - found: 3, - expected: 4, - .. - }) - )); - assert!(matches!( - planar_band(DecodingResult::U8(vec![1]), shape(2, 1)), - Err(RasterError::ShortBlock { .. }) - )); - } - - #[test] - fn deinterleave_respects_the_byte_cap() { - let tight = ChunkShape { - limit_bytes: 3, - ..shape(2, 2) - }; - assert!(matches!( - chunky_bands(DecodingResult::U16(vec![1, 2, 3, 4]), tight), - Err(RasterError::BlockTooLarge { .. }) - )); - } - - #[test] - fn unsupported_variants_are_typed_errors() { - assert!(matches!( - chunky_bands(DecodingResult::I8(vec![1]), shape(1, 1)), - Err(RasterError::UnsupportedSampleType { bits: 8, .. }) - )); - assert!(matches!( - planar_band(DecodingResult::U64(vec![1]), shape(1, 1)), - Err(RasterError::UnsupportedSampleType { bits: 64, .. }) - )); - } - #[test] fn values_widen_to_f64() { let band = BandSamples::I16(vec![-7, 3]); @@ -277,5 +108,32 @@ mod tests { assert_eq!(band.value_f64(2), None); assert_eq!(band.sample_type(), SampleType::I16); assert_eq!(band.len(), 2); + let wide = BandSamples::I32(vec![i32::MIN]); + assert_eq!(wide.value_f64(0), Some(f64::from(i32::MIN))); + } + + #[test] + fn masks_mark_validity_per_band() { + let window = BlockWindow { + outer: Vec::new(), + col_off: 0, + row_off: 0, + width: 2, + height: 1, + }; + let mut block = RasterBlock { + index: 0, + window, + bands: vec![BandSamples::U8(vec![1, 2]), BandSamples::U8(vec![3, 4])], + masks: vec![None, None], + }; + assert!(block.band_valid(0, 0) && block.band_valid(1, 1)); + assert!(!block.band_valid(0, 2)); + block.masks = vec![Some(vec![true, false]), Some(vec![false, false])]; + assert!(block.band_valid(0, 0)); + assert!(!block.band_valid(0, 1)); + assert!(!block.band_valid(1, 0)); + assert!(block.any_valid(0)); + assert!(!block.any_valid(1)); } } diff --git a/nodedb-raster/src/codec/compression.rs b/nodedb-raster/src/codec/compression.rs new file mode 100644 index 000000000..6e2621173 --- /dev/null +++ b/nodedb-raster/src/codec/compression.rs @@ -0,0 +1,172 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! The TIFF compression methods the reader decodes. + +use crate::codec::error::CodecError; +use crate::codec::{deflate, lzw, packbits, zstd}; +use crate::error::RasterError; + +/// TIFF `Compression` codes. +pub const CODE_NONE: u16 = 1; +pub const CODE_LZW: u16 = 5; +pub const CODE_JPEG: u16 = 7; +pub const CODE_DEFLATE: u16 = 8; +pub const CODE_PACKBITS: u16 = 32773; +pub const CODE_DEFLATE_PKZIP: u16 = 32946; +pub const CODE_LERC: u16 = 34887; +pub const CODE_ZSTD: u16 = 50000; + +/// A byte-stream codec: the chunk decodes to its raw sample bytes. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum StreamCodec { + None, + Lzw, + Deflate, + PackBits, + Zstd, +} + +impl StreamCodec { + /// Decode `data` into exactly `expected` bytes. Output past + /// `expected` is ignored. Fewer bytes are [`CodecError::Short`]. + pub fn decompress(self, data: &[u8], expected: usize) -> Result, CodecError> { + match self { + StreamCodec::None => { + let raw = data.get(..expected).ok_or(CodecError::Short { + expected, + found: data.len(), + })?; + Ok(raw.to_vec()) + } + StreamCodec::Lzw => lzw::decode(data, expected), + StreamCodec::Deflate => deflate::inflate(data, expected), + StreamCodec::PackBits => packbits::unpack(data, expected), + StreamCodec::Zstd => zstd::decode(data, expected), + } + } + + /// Decode `data`, a stream of unknown decoded size, into at most `max` + /// bytes. More is [`CodecError::Long`]. + pub fn decompress_bounded(self, data: &[u8], max: usize) -> Result, CodecError> { + match self { + StreamCodec::None if data.len() <= max => Ok(data.to_vec()), + StreamCodec::None => Err(CodecError::Long { expected: max }), + StreamCodec::Deflate => deflate::inflate_bounded(data, max), + StreamCodec::Zstd => zstd::decode_bounded(data, max), + StreamCodec::Lzw => lzw::decode_bounded(data, max), + StreamCodec::PackBits => packbits::unpack_bounded(data, max), + } + } +} + +/// The byte-stream wrapper around a LERC blob (`LercParameters` value 2). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum LercWrap { + None, + Deflate, + Zstd, +} + +impl LercWrap { + /// The wrapper of `LercParameters` additional-compression code `code`. + pub fn from_code(code: u64) -> Option { + match code { + 0 => Some(LercWrap::None), + 1 => Some(LercWrap::Deflate), + 2 => Some(LercWrap::Zstd), + _ => None, + } + } + + /// The stream codec that unwraps the blob, if any. + pub const fn stream(self) -> Option { + match self { + LercWrap::None => None, + LercWrap::Deflate => Some(StreamCodec::Deflate), + LercWrap::Zstd => Some(StreamCodec::Zstd), + } + } +} + +/// How a chunk is compressed. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Compression { + Stream(StreamCodec), + Jpeg, + Lerc(LercWrap), +} + +impl Compression { + /// The method of TIFF compression code `code`. `lerc` is the LERC + /// wrapper from `LercParameters`. + pub fn from_code(code: u16, lerc: LercWrap) -> Result { + match code { + CODE_NONE => Ok(Compression::Stream(StreamCodec::None)), + CODE_LZW => Ok(Compression::Stream(StreamCodec::Lzw)), + CODE_JPEG => Ok(Compression::Jpeg), + CODE_DEFLATE | CODE_DEFLATE_PKZIP => Ok(Compression::Stream(StreamCodec::Deflate)), + CODE_PACKBITS => Ok(Compression::Stream(StreamCodec::PackBits)), + CODE_LERC => Ok(Compression::Lerc(lerc)), + CODE_ZSTD => Ok(Compression::Stream(StreamCodec::Zstd)), + code => Err(RasterError::UnsupportedCompression { code }), + } + } + + /// Whether libtiff applies the `Predictor` tag under this method: LZW, + /// deflate and ZSTD only. + pub const fn takes_predictor(self) -> bool { + matches!( + self, + Compression::Stream(StreamCodec::Lzw | StreamCodec::Deflate | StreamCodec::Zstd) + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn every_supported_code_maps() { + for code in [1, 5, 7, 8, 32773, 32946, 34887, 50000] { + assert!( + Compression::from_code(code, LercWrap::None).is_ok(), + "{code}" + ); + } + for code in [2, 3, 4, 6, 34925, 50001, 34712] { + assert!(matches!( + Compression::from_code(code, LercWrap::None), + Err(RasterError::UnsupportedCompression { code: c }) if c == code + )); + } + } + + #[test] + fn predictor_applies_to_lzw_deflate_and_zstd_only() { + let takes = |code| { + Compression::from_code(code, LercWrap::None) + .map(Compression::takes_predictor) + .ok() + }; + assert_eq!(takes(5), Some(true)); + assert_eq!(takes(8), Some(true)); + assert_eq!(takes(50000), Some(true)); + assert_eq!(takes(1), Some(false)); + assert_eq!(takes(32773), Some(false)); + assert_eq!(takes(34887), Some(false)); + } + + #[test] + fn raw_chunks_need_the_whole_chunk() { + let codec = StreamCodec::None; + assert_eq!(codec.decompress(&[1, 2, 3], 2).ok(), Some(vec![1, 2])); + assert!(matches!( + codec.decompress(&[1], 2), + Err(CodecError::Short { + expected: 2, + found: 1 + }) + )); + } +} diff --git a/nodedb-raster/src/codec/deflate.rs b/nodedb-raster/src/codec/deflate.rs new file mode 100644 index 000000000..14c428a4c --- /dev/null +++ b/nodedb-raster/src/codec/deflate.rs @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Deflate (codes 8 and 32946): a zlib stream. + +use flate2::{Decompress, FlushDecompress, Status}; + +use crate::codec::error::CodecError; + +/// Inflate the zlib stream `data` into exactly `expected` bytes. +pub fn inflate(data: &[u8], expected: usize) -> Result, CodecError> { + let mut out = vec![0u8; expected]; + let mut stream = Decompress::new(true); + let mut filled = 0usize; + loop { + let consumed = usize::try_from(stream.total_in()).unwrap_or(data.len()); + let input = data.get(consumed..).unwrap_or_default(); + let output = out.get_mut(filled..).unwrap_or_default(); + let (in_before, out_before) = (stream.total_in(), stream.total_out()); + let status = stream + .decompress(input, output, FlushDecompress::Finish) + .map_err(CodecError::Deflate)?; + filled = usize::try_from(stream.total_out()).unwrap_or(expected); + let stalled = stream.total_in() == in_before && stream.total_out() == out_before; + match status { + Status::StreamEnd => break, + Status::Ok | Status::BufError if filled >= expected || stalled => break, + Status::Ok | Status::BufError => {} + } + } + if filled < expected { + return Err(CodecError::Short { + expected, + found: filled, + }); + } + Ok(out) +} + +/// Bytes the bounded inflate grows its output by per step. +const GROW: usize = 64 * 1024; + +/// Inflate the zlib stream `data`, of unknown decoded size, into at most +/// `max` bytes. +pub fn inflate_bounded(data: &[u8], max: usize) -> Result, CodecError> { + let mut out: Vec = Vec::new(); + let mut stream = Decompress::new(true); + loop { + // One byte past `max` stays reservable, so a stream that ends at + // exactly `max` can still report its end. + if out.len() == out.capacity() { + if out.len() > max { + return Err(CodecError::Long { expected: max }); + } + out.reserve_exact(GROW.min(max + 1 - out.len())); + } + let consumed = usize::try_from(stream.total_in()).unwrap_or(data.len()); + let input = data.get(consumed..).unwrap_or_default(); + let (in_before, out_before) = (stream.total_in(), stream.total_out()); + let status = stream + .decompress_vec(input, &mut out, FlushDecompress::None) + .map_err(CodecError::Deflate)?; + if status == Status::StreamEnd { + if out.len() > max { + return Err(CodecError::Long { expected: max }); + } + return Ok(out); + } + let stalled = stream.total_in() == in_before && stream.total_out() == out_before; + if stalled && out.len() < out.capacity() { + return Err(CodecError::Truncated { found: out.len() }); + } + } +} + +#[cfg(test)] +mod tests { + use std::io::Write; + + use flate2::write::ZlibEncoder; + + use super::*; + + fn zlib(raw: &[u8]) -> Vec { + let mut encoder = ZlibEncoder::new(Vec::new(), flate2::Compression::default()); + encoder.write_all(raw).expect("writes"); + encoder.finish().expect("finishes") + } + + #[test] + fn a_zlib_stream_inflates_to_the_chunk() { + let raw: Vec = (0..5000u32).map(|i| (i % 251) as u8).collect(); + assert_eq!(inflate(&zlib(&raw), raw.len()).ok(), Some(raw.clone())); + assert_eq!(inflate(&zlib(&raw), 10).ok(), Some(raw[..10].to_vec())); + } + + #[test] + fn bounded_inflate_stops_at_the_cap() { + let raw: Vec = (0..200_000u32).map(|i| (i % 13) as u8).collect(); + let stream = zlib(&raw); + assert_eq!(inflate_bounded(&stream, raw.len()).ok(), Some(raw.clone())); + assert!(matches!( + inflate_bounded(&stream, 1000), + Err(CodecError::Long { expected: 1000 }) + )); + assert!(matches!( + inflate_bounded(&stream[..stream.len() / 2], raw.len()), + Err(CodecError::Truncated { .. } | CodecError::Deflate(_)) + )); + } + + #[test] + fn short_and_corrupt_streams_are_typed_errors() { + let raw = vec![9u8; 100]; + assert!(matches!( + inflate(&zlib(&raw), 200), + Err(CodecError::Short { + expected: 200, + found: 100 + }) + )); + let stream = zlib(&raw); + assert!(inflate(&stream[..stream.len() / 2], 100).is_err()); + assert!(matches!( + inflate(b"not zlib at all", 10), + Err(CodecError::Deflate(_)) + )); + } +} diff --git a/nodedb-raster/src/codec/error.rs b/nodedb-raster/src/codec/error.rs new file mode 100644 index 000000000..f6e379ea0 --- /dev/null +++ b/nodedb-raster/src/codec/error.rs @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Typed errors of the chunk codecs. + +use crate::limits::CapacityExceeded; + +/// Every way a compressed chunk can fail to decode. +#[derive(Debug, thiserror::Error)] +pub enum CodecError { + #[error("LZW: {0}")] + Lzw(#[source] weezl::LzwError), + + #[error("deflate: {0}")] + Deflate(#[source] flate2::DecompressError), + + #[error("ZSTD: {0}")] + Zstd(#[source] ruzstd::decoding::errors::FrameDecoderError), + + #[error("JPEG: {0}")] + Jpeg(#[source] zune_jpeg::errors::DecodeErrors), + + #[error("LERC: {0}")] + Lerc(#[source] lerc::LercError), + + /// A Lerc2 blob header or checksum is invalid. + #[error("LERC blob at byte {offset}: {reason}")] + LercBlob { offset: usize, reason: &'static str }, + + /// A PackBits run reads or writes past the end of its chunk. + #[error("PackBits run at byte {offset} overruns the chunk")] + PackBitsOverrun { offset: usize }, + + /// The chunk asks for a decode allocation over the ceiling. + #[error(transparent)] + Capacity(#[from] CapacityExceeded), + + /// The stream ended before the chunk was whole. + #[error("the stream decodes to {found} bytes, expected {expected}")] + Short { expected: usize, found: usize }, + + /// The stream holds more data than the chunk. + #[error("the stream decodes to more than the {expected}-byte chunk")] + Long { expected: usize }, + + /// The stream ends before its end marker. + #[error("the stream ends before its end marker, after {found} decoded bytes")] + Truncated { found: usize }, + + /// The JPEG frame does not match the chunk. + #[error( + "JPEG frame is {width}x{height} with {components} components; the chunk is \ + {expected_width}x{expected_height} with {expected_components}" + )] + JpegShape { + width: u32, + height: u32, + components: usize, + expected_width: u32, + expected_height: u32, + expected_components: usize, + }, + + /// The LERC blob does not match the chunk. + #[error( + "LERC blob is {width}x{height}, depth {depth}, {bands} bands of {data_type}; the chunk \ + is {expected_width}x{expected_height}, depth {expected_depth} of {expected_type}" + )] + LercShape { + width: u32, + height: u32, + depth: u32, + bands: u32, + data_type: &'static str, + expected_width: u32, + expected_height: u32, + expected_depth: u32, + expected_type: &'static str, + }, +} diff --git a/nodedb-raster/src/codec/jpeg.rs b/nodedb-raster/src/codec/jpeg.rs new file mode 100644 index 000000000..6996ca35c --- /dev/null +++ b/nodedb-raster/src/codec/jpeg.rs @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! JPEG (code 7): one baseline or progressive JPEG stream per chunk. +//! +//! A TIFF can keep the quantisation and Huffman tables of every chunk in +//! one `JPEGTables` stream. Its tables are spliced in front of the chunk's +//! own markers. A three-component YCbCr stream decodes to RGB. + +use zune_jpeg::JpegDecoder; +use zune_jpeg::zune_core::bytestream::ZCursor; +use zune_jpeg::zune_core::colorspace::ColorSpace; +use zune_jpeg::zune_core::options::DecoderOptions; + +use crate::codec::error::CodecError; + +/// The `SOI` marker that opens a JPEG stream. +const SOI: [u8; 2] = [0xFF, 0xD8]; +/// The `EOI` marker that closes a JPEG stream. +const EOI: [u8; 2] = [0xFF, 0xD9]; + +/// One JPEG chunk and the frame it must decode to. +#[derive(Debug, Clone, Copy)] +pub struct JpegChunk<'a> { + pub data: &'a [u8], + /// The `JPEGTables` stream, when the file has one. + pub tables: Option<&'a [u8]>, + pub width: u32, + pub height: u32, + /// 1 (gray) or 3 (RGB, or YCbCr decoded to RGB). + pub components: usize, +} + +/// Decode `chunk` to pixel-interleaved 8-bit samples. +pub fn decode(chunk: JpegChunk<'_>) -> Result, CodecError> { + let shape_error = |width, height, components| CodecError::JpegShape { + width, + height, + components, + expected_width: chunk.width, + expected_height: chunk.height, + expected_components: chunk.components, + }; + let out_space = match chunk.components { + 1 => ColorSpace::Luma, + 3 => ColorSpace::RGB, + _ => return Err(shape_error(0, 0, 0)), + }; + let stream = splice_tables(chunk.tables, chunk.data); + let options = DecoderOptions::default() + .set_max_width(chunk.width as usize) + .set_max_height(chunk.height as usize) + .jpeg_set_out_colorspace(out_space); + let mut decoder = JpegDecoder::new_with_options(ZCursor::new(stream.as_slice()), options); + decoder.decode_headers().map_err(CodecError::Jpeg)?; + let Some(info) = decoder.info() else { + return Err(shape_error(0, 0, 0)); + }; + let (width, height) = (u32::from(info.width), u32::from(info.height)); + let components = usize::from(info.components); + if width != chunk.width || height != chunk.height || components != chunk.components { + return Err(shape_error(width, height, components)); + } + let len = (width as usize) + .checked_mul(height as usize) + .and_then(|pixels| pixels.checked_mul(components)) + .ok_or(shape_error(width, height, components))?; + let mut out = vec![0u8; len]; + decoder.decode_into(&mut out).map_err(CodecError::Jpeg)?; + Ok(out) +} + +/// The chunk stream with the tables spliced in: the tables without their +/// `EOI`, then the chunk without its `SOI`. +fn splice_tables(tables: Option<&[u8]>, data: &[u8]) -> Vec { + let Some(tables) = tables else { + return data.to_vec(); + }; + let (Some(table_body), Some(chunk_body)) = (tables.strip_suffix(&EOI), data.strip_prefix(&SOI)) + else { + return data.to_vec(); + }; + if !table_body.starts_with(&SOI) { + return data.to_vec(); + } + let mut stream = Vec::with_capacity(table_body.len() + chunk_body.len()); + stream.extend_from_slice(table_body); + stream.extend_from_slice(chunk_body); + stream +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn tables_are_spliced_between_the_markers() { + let tables = [0xFF, 0xD8, 0xFF, 0xDB, 1, 0xFF, 0xD9]; + let data = [0xFF, 0xD8, 0xFF, 0xDA, 2, 0xFF, 0xD9]; + assert_eq!( + splice_tables(Some(&tables), &data), + vec![0xFF, 0xD8, 0xFF, 0xDB, 1, 0xFF, 0xDA, 2, 0xFF, 0xD9] + ); + assert_eq!(splice_tables(None, &data), data.to_vec()); + assert_eq!(splice_tables(Some(&[1, 2]), &data), data.to_vec()); + } + + #[test] + fn garbage_and_bad_shapes_are_typed_errors() { + let chunk = JpegChunk { + data: b"not a jpeg", + tables: None, + width: 8, + height: 8, + components: 1, + }; + assert!(matches!(decode(chunk), Err(CodecError::Jpeg(_)))); + let four = JpegChunk { + components: 4, + ..chunk + }; + assert!(matches!(decode(four), Err(CodecError::JpegShape { .. }))); + } +} diff --git a/nodedb-raster/src/codec/lerc.rs b/nodedb-raster/src/codec/lerc.rs new file mode 100644 index 000000000..8d211d582 --- /dev/null +++ b/nodedb-raster/src/codec/lerc.rs @@ -0,0 +1,259 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! LERC (code 34887): an Esri LERC blob per chunk, optionally inside a +//! deflate or ZSTD stream (`LercParameters`). +//! +//! A chunky chunk is one blob of depth `samples per pixel`, or one band +//! per sample. A planar chunk is one band of depth 1. Each chunk sample +//! takes the validity mask of its blob band: a blob of depth `samples` has +//! one mask for every sample, and a multi-band blob has one per band. + +use lerc::{DataType, SampleData}; + +use crate::codec::error::CodecError; +use crate::codec::lerc_blob::check_blobs; +use crate::sample::SampleType; + +/// The chunk a LERC blob must decode to. +#[derive(Debug, Clone, Copy)] +pub struct LercChunk { + pub width: u32, + pub height: u32, + /// Samples per pixel in the chunk. + pub samples: u32, + pub sample_type: SampleType, +} + +/// A decoded LERC chunk. +#[derive(Debug)] +pub struct LercDecoded { + /// Pixel-interleaved samples, little-endian. + pub bytes: Vec, + /// One mask per chunk sample: one flag per pixel, `None` when every + /// pixel of that sample is valid. + pub masks: Vec>>, +} + +/// Decode the LERC blob `blob` into `chunk`. +pub fn decode(blob: &[u8], chunk: LercChunk) -> Result { + check_blobs(blob)?; + let info = lerc::decode_info(blob).map_err(CodecError::Lerc)?; + let shape_error = || CodecError::LercShape { + width: info.width, + height: info.height, + depth: info.depth, + bands: info.bands, + data_type: data_type_name(info.data_type), + expected_width: chunk.width, + expected_height: chunk.height, + expected_depth: chunk.samples, + expected_type: chunk.sample_type.name(), + }; + let layout_ok = (info.depth == chunk.samples && info.bands == 1) + || (info.depth == 1 && info.bands == chunk.samples); + if info.width != chunk.width + || info.height != chunk.height + || !layout_ok + || sample_type_of(info.data_type) != chunk.sample_type + { + return Err(shape_error()); + } + let image = lerc::decode(blob).map_err(CodecError::Lerc)?; + let pixels = (chunk.width as usize).saturating_mul(chunk.height as usize); + let samples = chunk.samples as usize; + let band_major = info.bands > 1; + let bytes = interleaved_bytes(&image.data, pixels, samples, band_major); + let expected = pixels + .saturating_mul(samples) + .saturating_mul(chunk.sample_type.byte_len()); + if bytes.len() != expected { + return Err(CodecError::Short { + expected, + found: bytes.len(), + }); + } + let masks = (0..samples) + .map(|sample| { + let mask = if band_major { + image + .valid_masks + .get(sample) + .or_else(|| image.valid_masks.first()) + } else { + image.valid_masks.first() + }; + mask.filter(|mask| !mask.is_all_valid()).map(|mask| { + (0..pixels) + .map(|pixel| pixel < mask.num_pixels() && mask.is_valid(pixel)) + .collect() + }) + }) + .collect(); + Ok(LercDecoded { bytes, masks }) +} + +/// The sample type of a LERC data type. +fn sample_type_of(data_type: DataType) -> SampleType { + match data_type { + DataType::Char => SampleType::I8, + DataType::Byte => SampleType::U8, + DataType::Short => SampleType::I16, + DataType::UShort => SampleType::U16, + DataType::Int => SampleType::I32, + DataType::UInt => SampleType::U32, + DataType::Float => SampleType::F32, + DataType::Double => SampleType::F64, + } +} + +fn data_type_name(data_type: DataType) -> &'static str { + sample_type_of(data_type).name() +} + +/// Little-endian bytes of `data` in pixel-interleaved order. Band-major +/// data (one band per sample) is interleaved. +fn interleaved_bytes( + data: &SampleData, + pixels: usize, + samples: usize, + band_major: bool, +) -> Vec { + match data { + SampleData::I8(v) => to_bytes(v, pixels, samples, band_major, |s| s.to_le_bytes()), + SampleData::U8(v) => to_bytes(v, pixels, samples, band_major, |s| s.to_le_bytes()), + SampleData::I16(v) => to_bytes(v, pixels, samples, band_major, |s| s.to_le_bytes()), + SampleData::U16(v) => to_bytes(v, pixels, samples, band_major, |s| s.to_le_bytes()), + SampleData::I32(v) => to_bytes(v, pixels, samples, band_major, |s| s.to_le_bytes()), + SampleData::U32(v) => to_bytes(v, pixels, samples, band_major, |s| s.to_le_bytes()), + SampleData::F32(v) => to_bytes(v, pixels, samples, band_major, |s| s.to_le_bytes()), + SampleData::F64(v) => to_bytes(v, pixels, samples, band_major, |s| s.to_le_bytes()), + } +} + +fn to_bytes( + values: &[T], + pixels: usize, + samples: usize, + band_major: bool, + le: impl Fn(T) -> [u8; N], +) -> Vec { + let mut out = Vec::with_capacity(values.len().saturating_mul(N)); + if band_major && samples > 1 { + for pixel in 0..pixels { + for band in 0..samples { + if let Some(value) = values.get(band * pixels + pixel) { + out.extend_from_slice(&le(*value)); + } + } + } + } else { + for value in values { + out.extend_from_slice(&le(*value)); + } + } + out +} + +#[cfg(test)] +mod tests { + use lerc::bitmask::BitMask; + use lerc::{Image, Precision}; + + use super::*; + + fn blob(width: u32, height: u32, data: Vec, mask: BitMask) -> Vec { + let image = Image { + width, + height, + depth: 1, + bands: 1, + data_type: DataType::UShort, + valid_masks: vec![mask], + data: SampleData::U16(data), + no_data_value: None, + }; + lerc::encode(&image, Precision::Lossless).expect("encodes") + } + + #[test] + fn a_blob_decodes_with_its_mask() { + let mut mask = BitMask::all_valid(4); + mask.set_invalid(2); + let encoded = blob(2, 2, vec![10, 20, 0, 40], mask); + let chunk = LercChunk { + width: 2, + height: 2, + samples: 1, + sample_type: SampleType::U16, + }; + let decoded = decode(&encoded, chunk).expect("decodes"); + let values: Vec = decoded + .bytes + .as_chunks::<2>() + .0 + .iter() + .map(|c| u16::from_le_bytes(*c)) + .collect(); + assert_eq!((values[0], values[1], values[3]), (10, 20, 40)); + assert_eq!(decoded.masks, vec![Some(vec![true, true, false, true])]); + } + + #[test] + fn each_band_of_a_multi_band_blob_keeps_its_own_mask() { + let mut first = BitMask::all_valid(2); + first.set_invalid(0); + let mut second = BitMask::all_valid(2); + second.set_invalid(1); + let image = Image { + width: 2, + height: 1, + depth: 1, + bands: 2, + data_type: DataType::UShort, + valid_masks: vec![first, second], + data: SampleData::U16(vec![0, 2, 3, 0]), + no_data_value: None, + }; + let encoded = lerc::encode(&image, Precision::Lossless).expect("encodes"); + let chunk = LercChunk { + width: 2, + height: 1, + samples: 2, + sample_type: SampleType::U16, + }; + let decoded = decode(&encoded, chunk).expect("decodes"); + assert_eq!( + decoded.masks, + vec![Some(vec![false, true]), Some(vec![true, false])] + ); + } + + #[test] + fn a_blob_of_another_shape_is_refused() { + let encoded = blob(2, 2, vec![1, 2, 3, 4], BitMask::all_valid(4)); + let chunk = LercChunk { + width: 4, + height: 1, + samples: 1, + sample_type: SampleType::U16, + }; + assert!(matches!( + decode(&encoded, chunk), + Err(CodecError::LercShape { .. }) + )); + let wrong_type = LercChunk { + width: 2, + height: 2, + samples: 1, + sample_type: SampleType::F32, + }; + assert!(matches!( + decode(&encoded, wrong_type), + Err(CodecError::LercShape { .. }) + )); + assert!(matches!( + decode(b"junk", wrong_type), + Err(CodecError::LercBlob { .. }) + )); + } +} diff --git a/nodedb-raster/src/codec/lerc_blob.rs b/nodedb-raster/src/codec/lerc_blob.rs new file mode 100644 index 000000000..96abd1a0f --- /dev/null +++ b/nodedb-raster/src/codec/lerc_blob.rs @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! A bounded pre-walk of the Lerc2 blobs in a chunk, before the LERC +//! decoder sees them. +//! +//! Each blob must open with the `Lerc2 ` magic, declare a size that covers +//! its header and fits the chunk, and (version 3 on) match its Fletcher-32 +//! checksum. The blobs must fill the chunk exactly. The decoder walks the +//! same blob chain without these checks. + +use crate::codec::error::CodecError; + +const MAGIC: &[u8; 6] = b"Lerc2 "; +/// Lerc2 versions the decoder reads. +const VERSIONS: std::ops::RangeInclusive = 2..=6; +/// Bytes before the checksum field, which the checksum excludes. +const CHECKSUM_START: usize = 14; +/// Most blobs one chunk may hold: one per band. +const MAX_BLOBS: usize = u16::MAX as usize; + +/// The number of blobs in `chunk`, after checking every one. +pub fn check_blobs(chunk: &[u8]) -> Result { + let mut offset = 0usize; + let mut blobs = 0usize; + while offset < chunk.len() { + if blobs == MAX_BLOBS { + return Err(blob_error(offset, "more blobs than bands")); + } + let blob = chunk.get(offset..).unwrap_or_default(); + let size = check_blob(blob).map_err(|reason| blob_error(offset, reason))?; + offset += size; + blobs += 1; + } + if blobs == 0 { + return Err(blob_error(0, "the chunk is empty")); + } + Ok(blobs) +} + +/// The size of the blob at the start of `blob`. +fn check_blob(blob: &[u8]) -> Result { + if blob.get(..MAGIC.len()) != Some(MAGIC.as_slice()) { + return Err("no Lerc2 magic"); + } + let version = read_i32(blob, 6).ok_or("truncated header")?; + if !VERSIONS.contains(&version) { + return Err("unsupported Lerc2 version"); + } + // magic, version, [checksum], rows, cols, [depth], valid pixels, + // micro block size, then the blob size. + let checksum_len = if version >= 3 { 4 } else { 0 }; + let depth_len = if version >= 4 { 4 } else { 0 }; + let size_at = 10 + checksum_len + 4 + 4 + depth_len + 4 + 4; + let size = read_i32(blob, size_at).ok_or("truncated header")?; + let size = usize::try_from(size).map_err(|_| "negative blob size")?; + if size < size_at + 4 { + return Err("blob size smaller than its header"); + } + let body = blob.get(..size).ok_or("blob size past the chunk")?; + if version >= 3 { + let stored = read_i32(blob, 10).ok_or("truncated header")? as u32; + let computed = lerc::checksum::fletcher32(body.get(CHECKSUM_START..).unwrap_or_default()); + if stored != computed { + return Err("checksum mismatch"); + } + } + Ok(size) +} + +fn read_i32(bytes: &[u8], at: usize) -> Option { + let raw: [u8; 4] = bytes.get(at..at + 4)?.try_into().ok()?; + Some(i32::from_le_bytes(raw)) +} + +fn blob_error(offset: usize, reason: &'static str) -> CodecError { + CodecError::LercBlob { offset, reason } +} + +#[cfg(test)] +mod tests { + use lerc::bitmask::BitMask; + use lerc::{DataType, Image, Precision, SampleData}; + + use super::*; + + fn blob() -> Vec { + let image = Image { + width: 3, + height: 2, + depth: 1, + bands: 1, + data_type: DataType::Byte, + valid_masks: vec![BitMask::all_valid(6)], + data: SampleData::U8(vec![1, 2, 3, 4, 5, 250]), + no_data_value: None, + }; + lerc::encode(&image, Precision::Lossless).expect("encodes") + } + + #[test] + fn whole_blobs_pass_and_count() { + let one = blob(); + assert_eq!(check_blobs(&one).ok(), Some(1)); + let mut two = one.clone(); + two.extend_from_slice(&one); + assert_eq!(check_blobs(&two).ok(), Some(2)); + } + + #[test] + fn corrupt_sizes_and_bodies_are_refused() { + let good = blob(); + let mut zero_size = good.clone(); + zero_size[34..38].copy_from_slice(&0i32.to_le_bytes()); + assert!(matches!( + check_blobs(&zero_size), + Err(CodecError::LercBlob { .. }) + )); + let mut flipped = good.clone(); + let last = flipped.len() - 1; + flipped[last] ^= 0x10; + assert!(matches!( + check_blobs(&flipped), + Err(CodecError::LercBlob { + reason: "checksum mismatch", + .. + }) + )); + assert!(check_blobs(&good[..good.len() - 1]).is_err()); + let mut trailing = good.clone(); + trailing.push(0); + assert!(check_blobs(&trailing).is_err()); + assert!(check_blobs(b"").is_err()); + assert!(check_blobs(b"CntZImage ").is_err()); + } +} diff --git a/nodedb-raster/src/codec/lzw.rs b/nodedb-raster/src/codec/lzw.rs new file mode 100644 index 000000000..f1ab8c23a --- /dev/null +++ b/nodedb-raster/src/codec/lzw.rs @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! LZW (code 5): MSB-first codes with the TIFF early size switch, or the +//! LSB-first codes of pre-5.0 libtiff. + +use weezl::decode::Decoder; +use weezl::{BitOrder, LzwStatus}; + +use crate::codec::error::CodecError; +use crate::limits::checked_raster_capacity; + +/// Decode the LZW stream `data`, of unknown decoded size, into at most +/// `max` bytes. +pub fn decode_bounded(data: &[u8], max: usize) -> Result, CodecError> { + let max = checked_raster_capacity(max)?; + let mut decoder = decoder_for(data); + let mut out = vec![0u8; max]; + let mut filled = 0usize; + let mut input = data; + loop { + let output = out.get_mut(filled..).unwrap_or_default(); + let result = decoder.decode_bytes(input, output); + input = input.get(result.consumed_in..).unwrap_or_default(); + filled += result.consumed_out; + match result.status { + Ok(LzwStatus::Done) => break, + Ok(LzwStatus::NoProgress) if filled >= max => { + return Err(CodecError::Long { expected: max }); + } + Ok(LzwStatus::NoProgress) => break, + Ok(LzwStatus::Ok) if result.consumed_in == 0 && result.consumed_out == 0 => { + if filled >= max { + return Err(CodecError::Long { expected: max }); + } + break; + } + Ok(LzwStatus::Ok) => {} + Err(error) => return Err(CodecError::Lzw(error)), + } + } + out.truncate(filled); + Ok(out) +} + +/// The decoder for `data`: libtiff reads a stream that opens with 0x00 +/// and an odd second byte as the old LSB-first code order. +fn decoder_for(data: &[u8]) -> Decoder { + if matches!(data, [0, second, ..] if second & 1 == 1) { + Decoder::new(BitOrder::Lsb, 8) + } else { + Decoder::with_tiff_size_switch(BitOrder::Msb, 8) + } +} + +/// Decode the LZW stream `data` into exactly `expected` bytes. +pub fn decode(data: &[u8], expected: usize) -> Result, CodecError> { + let expected = checked_raster_capacity(expected)?; + let mut decoder = decoder_for(data); + let mut out = vec![0u8; expected]; + let mut filled = 0usize; + let mut input = data; + while filled < expected { + let output = out.get_mut(filled..).unwrap_or_default(); + let result = decoder.decode_bytes(input, output); + input = input.get(result.consumed_in..).unwrap_or_default(); + filled += result.consumed_out; + match result.status { + Ok(LzwStatus::Done | LzwStatus::NoProgress) => break, + Ok(LzwStatus::Ok) if result.consumed_in == 0 && result.consumed_out == 0 => break, + Ok(LzwStatus::Ok) => {} + // Trailing garbage after a whole chunk is harmless. + Err(_) if filled >= expected => break, + Err(error) => return Err(CodecError::Lzw(error)), + } + } + if filled < expected { + return Err(CodecError::Short { + expected, + found: filled, + }); + } + Ok(out) +} + +#[cfg(test)] +mod tests { + use weezl::encode::Encoder; + + use super::*; + + #[test] + fn a_tiff_lzw_stream_decodes() { + let raw: Vec = (0..4000u32).map(|i| (i * 7 % 256) as u8).collect(); + let encoded = Encoder::with_tiff_size_switch(BitOrder::Msb, 8) + .encode(&raw) + .expect("encodes"); + assert_eq!(decode(&encoded, raw.len()).ok(), Some(raw)); + } + + #[test] + fn short_and_corrupt_streams_are_typed_errors() { + let raw = vec![3u8; 64]; + let encoded = Encoder::with_tiff_size_switch(BitOrder::Msb, 8) + .encode(&raw) + .expect("encodes"); + assert!(matches!( + decode(&encoded, 128), + Err(CodecError::Short { expected: 128, .. }) + )); + assert!(decode(&[0xFF; 16], 64).is_err()); + assert!(decode(&[], 4).is_err()); + } +} diff --git a/nodedb-raster/src/codec/mod.rs b/nodedb-raster/src/codec/mod.rs new file mode 100644 index 000000000..6370ecfd4 --- /dev/null +++ b/nodedb-raster/src/codec/mod.rs @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! TIFF chunk codecs and predictors, all pure Rust. + +pub mod compression; +pub mod deflate; +pub mod error; +pub mod jpeg; +pub mod lerc; +pub mod lerc_blob; +pub mod lzw; +pub mod packbits; +pub mod predictor; +pub mod zstd; + +pub use compression::{Compression, LercWrap}; +pub use error::CodecError; +pub use predictor::Predictor; diff --git a/nodedb-raster/src/codec/packbits.rs b/nodedb-raster/src/codec/packbits.rs new file mode 100644 index 000000000..f6c1248e4 --- /dev/null +++ b/nodedb-raster/src/codec/packbits.rs @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! PackBits (code 32773): byte runs and literals. +//! +//! A header byte `n` in `0..=127` copies the next `n + 1` bytes. `n` in +//! `-127..=-1` repeats the next byte `1 - n` times. `-128` is a no-op. + +use crate::codec::error::CodecError; + +/// Unpack `data` into exactly `expected` bytes. A run past `expected` is +/// cut at `expected`. +pub fn unpack(data: &[u8], expected: usize) -> Result, CodecError> { + let mut out = Vec::with_capacity(expected); + let mut at = 0usize; + while out.len() < expected { + let Some(&header) = data.get(at) else { + break; + }; + at += 1; + let room = expected - out.len(); + let n = i8::from_ne_bytes([header]); + if n >= 0 { + let count = usize::from(n.unsigned_abs()) + 1; + let literal = data + .get(at..at + count) + .ok_or(CodecError::PackBitsOverrun { offset: at - 1 })?; + out.extend_from_slice(literal.get(..count.min(room)).unwrap_or_default()); + at += count; + } else if n != i8::MIN { + let count = usize::from(n.unsigned_abs()) + 1; + let &byte = data + .get(at) + .ok_or(CodecError::PackBitsOverrun { offset: at - 1 })?; + at += 1; + out.resize(out.len() + count.min(room), byte); + } + } + if out.len() < expected { + return Err(CodecError::Short { + expected, + found: out.len(), + }); + } + Ok(out) +} + +/// Unpack `data`, of unknown decoded size, into at most `max` bytes. +pub fn unpack_bounded(data: &[u8], max: usize) -> Result, CodecError> { + let mut out = Vec::new(); + let mut at = 0usize; + while let Some(&header) = data.get(at) { + at += 1; + let n = i8::from_ne_bytes([header]); + let count = usize::from(n.unsigned_abs()) + 1; + if n >= 0 { + let literal = data + .get(at..at + count) + .ok_or(CodecError::PackBitsOverrun { offset: at - 1 })?; + if out.len() + count > max { + return Err(CodecError::Long { expected: max }); + } + out.extend_from_slice(literal); + at += count; + } else if n != i8::MIN { + let &byte = data + .get(at) + .ok_or(CodecError::PackBitsOverrun { offset: at - 1 })?; + at += 1; + if out.len() + count > max { + return Err(CodecError::Long { expected: max }); + } + out.resize(out.len() + count, byte); + } + } + Ok(out) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// The worked example of the TIFF 6.0 specification, section 9. + #[test] + fn the_specification_example_unpacks() { + let packed = [ + 0xFE, 0xAA, 0x02, 0x80, 0x00, 0x2A, 0xFD, 0xAA, 0x03, 0x80, 0x00, 0x2A, 0x22, 0xF7, + 0xAA, + ]; + let want = [ + 0xAA, 0xAA, 0xAA, 0x80, 0x00, 0x2A, 0xAA, 0xAA, 0xAA, 0xAA, 0x80, 0x00, 0x2A, 0x22, + 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, + ]; + assert_eq!(unpack(&packed, want.len()).ok(), Some(want.to_vec())); + } + + #[test] + fn bounded_unpack_refuses_more_than_the_cap() { + assert_eq!( + unpack_bounded(&[0xFD, 7, 0x00, 1], 8).ok(), + Some(vec![7, 7, 7, 7, 1]) + ); + assert!(matches!( + unpack_bounded(&[0x81, 7], 8), + Err(CodecError::Long { expected: 8 }) + )); + } + + #[test] + fn noops_are_skipped_and_runs_are_cut_at_the_chunk() { + assert_eq!(unpack(&[0x80, 0xFD, 7], 2).ok(), Some(vec![7, 7])); + } + + #[test] + fn overruns_and_short_streams_are_typed_errors() { + assert!(matches!( + unpack(&[0x05, 1, 2], 6), + Err(CodecError::PackBitsOverrun { offset: 0 }) + )); + assert!(matches!( + unpack(&[0xFF], 2), + Err(CodecError::PackBitsOverrun { offset: 0 }) + )); + assert!(matches!( + unpack(&[0x00, 9], 3), + Err(CodecError::Short { + expected: 3, + found: 1 + }) + )); + } +} diff --git a/nodedb-raster/src/codec/predictor.rs b/nodedb-raster/src/codec/predictor.rs new file mode 100644 index 000000000..e5bcdc74a --- /dev/null +++ b/nodedb-raster/src/codec/predictor.rs @@ -0,0 +1,193 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! TIFF predictors: horizontal differencing (2) and the floating-point +//! byte-plane predictor of TIFF Technical Note 3 (3). + +use crate::error::RasterError; +use crate::sample::SampleType; +use crate::tiff::byte_order::ByteOrder; +use crate::tiff::error::UnsupportedTiff; + +/// The `Predictor` tag. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Predictor { + None, + Horizontal, + FloatingPoint, +} + +/// The shape of one decoded chunk a predictor runs over. +#[derive(Debug, Clone, Copy)] +pub struct PredictorShape { + /// Samples per row: chunk width times samples per pixel. + pub row_samples: usize, + /// Samples per pixel in the chunk: the differencing stride. + pub stride: usize, + pub sample_type: SampleType, + pub order: ByteOrder, +} + +impl Predictor { + /// The predictor of tag value `code` for `sample_type` samples. + pub fn from_code(code: u16, sample_type: SampleType) -> Result { + match code { + 1 => Ok(Predictor::None), + 2 => Ok(Predictor::Horizontal), + 3 if sample_type.is_float() => Ok(Predictor::FloatingPoint), + 3 => Err(UnsupportedTiff::FloatPredictorOnIntegers { sample_type }.into()), + code => Err(UnsupportedTiff::Predictor { code }.into()), + } + } + + /// Undo the predictor over `data`, rows of `shape.row_samples` samples + /// stored in `shape.order`. The floating-point predictor leaves the + /// samples in `shape.order` as well. A trailing partial row is left + /// as is. + pub fn undo(self, data: &mut [u8], shape: PredictorShape) { + let width = shape.sample_type.byte_len(); + let row_bytes = shape.row_samples.saturating_mul(width); + if row_bytes == 0 { + return; + } + match self { + Predictor::None => {} + Predictor::Horizontal => { + for row in data.chunks_exact_mut(row_bytes) { + undo_horizontal(row, shape.stride, width, shape.order); + } + } + Predictor::FloatingPoint => { + let mut scratch = vec![0u8; row_bytes]; + for row in data.chunks_exact_mut(row_bytes) { + undo_floating(row, &mut scratch, shape.stride, width, shape.order); + } + } + } + } +} + +/// `sample[i] += sample[i - stride]`, wrapping at the sample width. +fn undo_horizontal(row: &mut [u8], stride: usize, width: usize, order: ByteOrder) { + let samples = row.len() / width; + let mask = if width >= 8 { + u64::MAX + } else { + (1u64 << (width * 8)) - 1 + }; + for i in stride..samples { + let (Some(left), Some(here)) = ( + order.uint_at(row, (i - stride) * width, width), + order.uint_at(row, i * width, width), + ) else { + return; + }; + order.put_uint(row, i * width, width, here.wrapping_add(left) & mask); + } +} + +/// Undo byte differencing at the pixel stride, then gather the byte +/// planes (most significant byte first) back into samples. +fn undo_floating( + row: &mut [u8], + scratch: &mut [u8], + stride: usize, + width: usize, + order: ByteOrder, +) { + for i in stride..row.len() { + row[i] = row[i].wrapping_add(row[i - stride]); + } + let samples = row.len() / width; + for i in 0..samples { + for byte in 0..width { + let plane = row[byte * samples + i]; + let at = match order { + ByteOrder::Big => i * width + byte, + ByteOrder::Little => i * width + (width - 1 - byte), + }; + scratch[at] = plane; + } + } + row.copy_from_slice(scratch); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn horizontal_differences_add_back_per_band() { + // Two pixels of two u16 bands, little-endian: (1, 100), (+2, -1). + let mut data = Vec::new(); + for v in [1u16, 100, 2, u16::MAX] { + data.extend(v.to_le_bytes()); + } + let shape = PredictorShape { + row_samples: 4, + stride: 2, + sample_type: SampleType::U16, + order: ByteOrder::Little, + }; + Predictor::Horizontal.undo(&mut data, shape); + let samples: Vec = data + .as_chunks::<2>() + .0 + .iter() + .map(|c| u16::from_le_bytes(*c)) + .collect(); + assert_eq!(samples, vec![1, 100, 3, 99]); + } + + #[test] + fn floating_point_planes_reassemble() { + let values = [1.5f32, -2.25, 1000.0]; + // Encode: big-endian byte planes, then byte differencing at stride 1. + let be: Vec<[u8; 4]> = values.iter().map(|v| v.to_be_bytes()).collect(); + let mut encoded: Vec = (0..4) + .flat_map(|byte| be.iter().map(move |s| s[byte])) + .collect(); + for i in (1..encoded.len()).rev() { + encoded[i] = encoded[i].wrapping_sub(encoded[i - 1]); + } + for order in [ByteOrder::Little, ByteOrder::Big] { + let mut data = encoded.clone(); + let shape = PredictorShape { + row_samples: 3, + stride: 1, + sample_type: SampleType::F32, + order, + }; + Predictor::FloatingPoint.undo(&mut data, shape); + let decoded: Vec = data + .as_chunks::<4>() + .0 + .iter() + .map(|raw| match order { + ByteOrder::Little => f32::from_le_bytes(*raw), + ByteOrder::Big => f32::from_be_bytes(*raw), + }) + .collect(); + assert_eq!(decoded, values); + } + } + + #[test] + fn predictor_codes_are_checked() { + assert_eq!( + Predictor::from_code(3, SampleType::F64).ok(), + Some(Predictor::FloatingPoint) + ); + assert!(matches!( + Predictor::from_code(3, SampleType::U16), + Err(RasterError::UnsupportedTiff( + UnsupportedTiff::FloatPredictorOnIntegers { .. } + )) + )); + assert!(matches!( + Predictor::from_code(34892, SampleType::U8), + Err(RasterError::UnsupportedTiff(UnsupportedTiff::Predictor { + code: 34892 + })) + )); + } +} diff --git a/nodedb-raster/src/codec/zstd.rs b/nodedb-raster/src/codec/zstd.rs new file mode 100644 index 000000000..d666e7b3c --- /dev/null +++ b/nodedb-raster/src/codec/zstd.rs @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! ZSTD (code 50000), through the pure-Rust `ruzstd` decoder. + +use ruzstd::decoding::errors::FrameDecoderError; +use ruzstd::decoding::{DEFAULT_MAX_WINDOW_SIZE, FrameDecoder}; + +use crate::codec::error::CodecError; +use crate::limits::checked_raster_capacity; + +/// Smallest window the decoder accepts, whatever the chunk size. +const MIN_WINDOW: u64 = 1 << 20; + +/// A frame decoder whose window is bounded by `content` decoded bytes. A +/// frame needs no window larger than its content. Twice the content, +/// rounded up, bounds the decode buffer for any sane encoder. +fn decoder_for(content: usize) -> FrameDecoder { + let mut decoder = FrameDecoder::new(); + let window = (content as u64) + .max(1) + .checked_next_power_of_two() + .map_or(DEFAULT_MAX_WINDOW_SIZE, |w| w.saturating_mul(2)) + .clamp(MIN_WINDOW, DEFAULT_MAX_WINDOW_SIZE); + decoder.set_max_window_size(window); + decoder +} + +/// Decode the ZSTD frames `data`, of unknown decoded size, into at most +/// `max` bytes. +pub fn decode_bounded(data: &[u8], max: usize) -> Result, CodecError> { + let max = checked_raster_capacity(max)?; + let mut decoder = decoder_for(max); + let mut out = Vec::with_capacity(max); + decoder + .decode_all_to_vec(data, &mut out) + .map_err(|error| match error { + FrameDecoderError::TargetTooSmall => CodecError::Long { expected: max }, + other => CodecError::Zstd(other), + })?; + Ok(out) +} + +/// Decode the ZSTD frames `data` into exactly `expected` bytes. +pub fn decode(data: &[u8], expected: usize) -> Result, CodecError> { + let expected = checked_raster_capacity(expected)?; + let mut decoder = decoder_for(expected); + let mut out = vec![0u8; expected]; + let written = decoder + .decode_all(data, &mut out) + .map_err(|error| match error { + FrameDecoderError::TargetTooSmall => CodecError::Long { expected }, + other => CodecError::Zstd(other), + })?; + if written < expected { + return Err(CodecError::Short { + expected, + found: written, + }); + } + Ok(out) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// One raw ZSTD block frame of `raw`: magic, a single-segment frame + /// header with a one-byte content size, and a last raw block. + fn raw_frame(raw: &[u8]) -> Vec { + let len = u8::try_from(raw.len()).expect("short"); + let mut frame = vec![0x28, 0xB5, 0x2F, 0xFD, 0x20, len]; + let block_header = (u32::from(len) << 3) | 1; + frame.extend_from_slice(&block_header.to_le_bytes()[..3]); + frame.extend_from_slice(raw); + frame + } + + #[test] + fn a_frame_decodes_to_the_chunk() { + let raw = [5u8, 6, 7, 8]; + assert_eq!(decode(&raw_frame(&raw), 4).ok(), Some(raw.to_vec())); + } + + #[test] + fn short_long_and_corrupt_frames_are_typed_errors() { + let frame = raw_frame(&[1, 2, 3, 4]); + assert!(matches!( + decode(&frame, 8), + Err(CodecError::Short { + expected: 8, + found: 4 + }) + )); + assert!(matches!( + decode(&frame, 2), + Err(CodecError::Long { expected: 2 }) + )); + assert!(matches!(decode(b"garbage!", 4), Err(CodecError::Zstd(_)))); + } + + #[test] + fn bounded_decode_returns_the_content() { + let frame = raw_frame(&[1, 2, 3, 4]); + assert_eq!(decode_bounded(&frame, 16).ok(), Some(vec![1, 2, 3, 4])); + assert!(matches!( + decode_bounded(&frame, 2), + Err(CodecError::Long { expected: 2 }) + )); + } +} diff --git a/nodedb-raster/src/crs.rs b/nodedb-raster/src/crs.rs deleted file mode 100644 index d9218bb67..000000000 --- a/nodedb-raster/src/crs.rs +++ /dev/null @@ -1,119 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -//! The raster CRS, from the GeoKey directory. -//! -//! A projected model (1) reads `ProjectedCRSGeoKey` (3072), a geographic -//! model (2) reads `GeodeticCRSGeoKey` (2048). With no model key, 3072 wins -//! over 2048. The EPSG code then maps through [`Srid::from_code`]. - -use nodedb_types::geometry::Srid; - -use crate::error::RasterError; -use crate::geokeys::{GEOGRAPHIC_TYPE, GT_MODEL_TYPE, GeoKeyDirectory, PROJECTED_CS_TYPE}; - -const MODEL_PROJECTED: u16 = 1; -const MODEL_GEOGRAPHIC: u16 = 2; -/// GeoKey value of a CRS defined by parameters, not by an EPSG code. -const USER_DEFINED: u16 = 32767; -/// GeoKey value of an undefined CRS. -const UNDEFINED: u16 = 0; - -/// The SRID the GeoKeys name, or `None` when they name no CRS. -pub fn srid_from_geokeys(keys: Option<&GeoKeyDirectory>) -> Result, RasterError> { - let Some(keys) = keys else { - return Ok(None); - }; - let key = match keys.short(GT_MODEL_TYPE) { - Some(MODEL_PROJECTED) => PROJECTED_CS_TYPE, - Some(MODEL_GEOGRAPHIC) => GEOGRAPHIC_TYPE, - Some(model) => return Err(RasterError::UnsupportedModelType { model }), - None if keys.short(PROJECTED_CS_TYPE).is_some() => PROJECTED_CS_TYPE, - None if keys.short(GEOGRAPHIC_TYPE).is_some() => GEOGRAPHIC_TYPE, - None => return Ok(None), - }; - let model = keys.short(GT_MODEL_TYPE).unwrap_or(UNDEFINED); - match keys.short(key) { - None | Some(UNDEFINED) => Err(RasterError::MissingCrsKey { model, key }), - Some(USER_DEFINED) => Err(RasterError::UserDefinedCrs { key }), - Some(code) => Ok(Some(Srid::from_code(i64::from(code))?)), - } -} - -#[cfg(test)] -mod tests { - use super::*; - - fn keys(entries: &[(u16, u16)]) -> GeoKeyDirectory { - let count = u16::try_from(entries.len()).expect("few keys"); - let mut dir = vec![1, 1, 0, count]; - for (key, value) in entries { - dir.extend([*key, 0, 1, *value]); - } - GeoKeyDirectory::parse(&dir, None, None).expect("valid directory") - } - - #[test] - fn no_directory_is_no_crs() { - assert_eq!(srid_from_geokeys(None).ok(), Some(None)); - } - - #[test] - fn projected_model_reads_3072() { - let dir = keys(&[(GT_MODEL_TYPE, 1), (PROJECTED_CS_TYPE, 32647)]); - assert_eq!(srid_from_geokeys(Some(&dir)).ok(), Some(Some(Srid::Utm47N))); - } - - #[test] - fn geographic_model_reads_2048() { - let dir = keys(&[(GT_MODEL_TYPE, 2), (GEOGRAPHIC_TYPE, 4326)]); - assert_eq!(srid_from_geokeys(Some(&dir)).ok(), Some(Some(Srid::Wgs84))); - } - - #[test] - fn model_key_picks_the_crs_key() { - let dir = keys(&[ - (GT_MODEL_TYPE, 2), - (GEOGRAPHIC_TYPE, 4326), - (PROJECTED_CS_TYPE, 32648), - ]); - assert_eq!(srid_from_geokeys(Some(&dir)).ok(), Some(Some(Srid::Wgs84))); - } - - #[test] - fn without_a_model_key_3072_wins() { - let dir = keys(&[(GEOGRAPHIC_TYPE, 4326), (PROJECTED_CS_TYPE, 3375)]); - assert_eq!( - srid_from_geokeys(Some(&dir)).ok(), - Some(Some(Srid::PeninsularRso)) - ); - let none = keys(&[(1025, 1)]); - assert_eq!(srid_from_geokeys(Some(&none)).ok(), Some(None)); - } - - #[test] - fn unmappable_crs_are_typed_errors() { - let geocentric = keys(&[(GT_MODEL_TYPE, 3)]); - assert!(matches!( - srid_from_geokeys(Some(&geocentric)), - Err(RasterError::UnsupportedModelType { model: 3 }) - )); - let missing = keys(&[(GT_MODEL_TYPE, 1)]); - assert!(matches!( - srid_from_geokeys(Some(&missing)), - Err(RasterError::MissingCrsKey { - model: 1, - key: 3072 - }) - )); - let user = keys(&[(GT_MODEL_TYPE, 1), (PROJECTED_CS_TYPE, 32767)]); - assert!(matches!( - srid_from_geokeys(Some(&user)), - Err(RasterError::UserDefinedCrs { key: 3072 }) - )); - let other = keys(&[(GT_MODEL_TYPE, 1), (PROJECTED_CS_TYPE, 32633)]); - assert!(matches!( - srid_from_geokeys(Some(&other)), - Err(RasterError::UnsupportedCrs(_)) - )); - } -} diff --git a/nodedb-raster/src/crs/mod.rs b/nodedb-raster/src/crs/mod.rs new file mode 100644 index 000000000..a41fe0f03 --- /dev/null +++ b/nodedb-raster/src/crs/mod.rs @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! The raster CRS, from the GeoKey directory. + +pub mod projection_keys; +pub mod registry_match; +pub mod srid; +pub mod units; +pub mod user_defined; + +pub use registry_match::match_registered; +pub use srid::{crs_ref_from_geokeys, resolve_crs, srid_from_geokeys}; +pub use units::USER_DEFINED; +pub use user_defined::decode_user_crs; diff --git a/nodedb-raster/src/crs/projection_keys.rs b/nodedb-raster/src/crs/projection_keys.rs new file mode 100644 index 000000000..8c0830c37 --- /dev/null +++ b/nodedb-raster/src/crs/projection_keys.rs @@ -0,0 +1,312 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! The projection of a user-defined GeoTIFF CRS, as PROJ parameters. Keys +//! map the way GDAL maps them, with the same fallbacks between natural +//! origin, false origin and centre keys. + +use super::units::{AngleUnit, epsg_code, push, unsupported}; +use crate::error::RasterError; +use crate::tiff::geokeys::{ + GEOG_ANGULAR_UNIT_SIZE, GEOG_AZIMUTH_UNITS, GeoKeyDirectory, PROJ_AZIMUTH_ANGLE, + PROJ_CENTER_EASTING, PROJ_CENTER_LAT, PROJ_CENTER_LONG, PROJ_CENTER_NORTHING, + PROJ_FALSE_EASTING, PROJ_FALSE_NORTHING, PROJ_FALSE_ORIGIN_EASTING, PROJ_FALSE_ORIGIN_LAT, + PROJ_FALSE_ORIGIN_LONG, PROJ_FALSE_ORIGIN_NORTHING, PROJ_METHOD, PROJ_NAT_ORIGIN_LAT, + PROJ_NAT_ORIGIN_LONG, PROJ_RECTIFIED_GRID_ANGLE, PROJ_SCALE_AT_CENTER, + PROJ_SCALE_AT_NAT_ORIGIN, PROJ_STD_PARALLEL_1, PROJ_STD_PARALLEL_2, + PROJ_STRAIGHT_VERT_POLE_LONG, PROJECTION, +}; + +/// EPSG conversion codes of UTM zones: north `16000 + zone`, south +/// `16100 + zone`. +const UTM_NORTH_BASE: u16 = 16_000; +const UTM_SOUTH_BASE: u16 = 16_100; +const UTM_ZONES: u16 = 60; + +/// GeoTIFF projection method codes (`ProjMethodGeoKey`). +mod method { + pub const TRANSVERSE_MERCATOR: u16 = 1; + pub const OBLIQUE_MERCATOR: u16 = 3; + pub const MERCATOR: u16 = 7; + pub const LCC_2SP: u16 = 8; + pub const LCC_1SP: u16 = 9; + pub const LAEA: u16 = 10; + pub const ALBERS: u16 = 11; + pub const AZIMUTHAL_EQUIDISTANT: u16 = 12; + pub const STEREOGRAPHIC: u16 = 14; + pub const POLAR_STEREOGRAPHIC: u16 = 15; + pub const OBLIQUE_STEREOGRAPHIC: u16 = 16; + pub const EQUIRECTANGULAR: u16 = 17; + pub const CASSINI_SOLDNER: u16 = 18; + pub const POLYCONIC: u16 = 22; + pub const SINUSOIDAL: u16 = 24; + pub const NEW_ZEALAND_MAP_GRID: u16 = 26; + pub const TM_SOUTH_ORIENTATED: u16 = 27; + pub const CYLINDRICAL_EQUAL_AREA: u16 = 28; +} + +/// Reads projection parameters in their units. +struct Params<'a> { + keys: &'a GeoKeyDirectory, + angle: AngleUnit, + azimuth: AngleUnit, + metres: f64, +} + +impl Params<'_> { + /// The first of `keys` present, as degrees. + fn angle(&self, keys: &[u16]) -> Option { + self.first(keys).map(|v| self.angle.degrees(v)) + } + + /// The first of `keys` present, as metres. + fn length(&self, keys: &[u16]) -> f64 { + self.first(keys).map_or(0.0, |v| v * self.metres) + } + + fn first(&self, keys: &[u16]) -> Option { + keys.iter().find_map(|k| self.keys.double(*k)) + } + + fn scale(&self) -> f64 { + self.first(&[PROJ_SCALE_AT_NAT_ORIGIN, PROJ_SCALE_AT_CENTER]) + .unwrap_or(1.0) + } + + fn azimuth(&self, key: u16) -> Option { + self.keys.double(key).map(|v| self.azimuth.degrees(v)) + } +} + +const NAT_LAT: [u16; 3] = [PROJ_NAT_ORIGIN_LAT, PROJ_FALSE_ORIGIN_LAT, PROJ_CENTER_LAT]; +const NAT_LONG: [u16; 3] = [ + PROJ_NAT_ORIGIN_LONG, + PROJ_FALSE_ORIGIN_LONG, + PROJ_CENTER_LONG, +]; +const CENTER_LAT: [u16; 3] = [PROJ_CENTER_LAT, PROJ_NAT_ORIGIN_LAT, PROJ_FALSE_ORIGIN_LAT]; +const CENTER_LONG: [u16; 3] = [ + PROJ_CENTER_LONG, + PROJ_NAT_ORIGIN_LONG, + PROJ_FALSE_ORIGIN_LONG, +]; +const ORIGIN_LAT: [u16; 3] = [PROJ_FALSE_ORIGIN_LAT, PROJ_NAT_ORIGIN_LAT, PROJ_CENTER_LAT]; +const ORIGIN_LONG: [u16; 3] = [ + PROJ_FALSE_ORIGIN_LONG, + PROJ_NAT_ORIGIN_LONG, + PROJ_CENTER_LONG, +]; +const EASTING: [u16; 3] = [ + PROJ_FALSE_EASTING, + PROJ_FALSE_ORIGIN_EASTING, + PROJ_CENTER_EASTING, +]; +const NORTHING: [u16; 3] = [ + PROJ_FALSE_NORTHING, + PROJ_FALSE_ORIGIN_NORTHING, + PROJ_CENTER_NORTHING, +]; + +/// Write the PROJ projection parameters of `keys` into `out`. False +/// eastings and northings convert from the projected unit to metres. +pub(super) fn projection( + keys: &GeoKeyDirectory, + angle: AngleUnit, + metres: f64, + out: &mut String, +) -> Result<(), RasterError> { + if let Some(code) = epsg_code(keys.short(PROJECTION)) { + return utm_conversion(code, out); + } + let code = keys.short(PROJ_METHOD).ok_or_else(|| { + unsupported(format!( + "GeoKey {PROJECTION} is user-defined but GeoKey {PROJ_METHOD} is absent" + )) + })?; + let p = Params { + keys, + angle, + azimuth: AngleUnit::of(keys, GEOG_AZIMUTH_UNITS, GEOG_ANGULAR_UNIT_SIZE)?, + metres, + }; + let deg = |v: Option| v.unwrap_or(0.0); + let x0 = p.length(&EASTING); + let y0 = p.length(&NORTHING); + let k = p.scale(); + match code { + method::TRANSVERSE_MERCATOR | method::TM_SOUTH_ORIENTATED => { + push( + out, + format_args!( + "+proj=tmerc +lat_0={} +lon_0={} +k={k}", + deg(p.angle(&NAT_LAT)), + deg(p.angle(&NAT_LONG)) + ), + ); + if code == method::TM_SOUTH_ORIENTATED { + out.push_str(" +axis=wsu"); + } + } + method::OBLIQUE_MERCATOR => { + let alpha = deg(p.azimuth(PROJ_AZIMUTH_ANGLE)); + let gamma = p.azimuth(PROJ_RECTIFIED_GRID_ANGLE).unwrap_or(alpha); + push( + out, + format_args!( + "+proj=omerc +no_uoff +lat_0={} +lonc={} +alpha={alpha} +gamma={gamma} +k={k}", + deg(p.angle(&CENTER_LAT)), + deg(p.angle(&CENTER_LONG)) + ), + ); + } + method::MERCATOR => { + push( + out, + format_args!("+proj=merc +lon_0={}", deg(p.angle(&NAT_LONG))), + ); + match p.angle(&[PROJ_STD_PARALLEL_1]).filter(|v| *v != 0.0) { + Some(lat_ts) => push(out, format_args!(" +lat_ts={lat_ts}")), + None => push(out, format_args!(" +k={k}")), + } + } + method::LCC_2SP => push( + out, + format_args!( + "+proj=lcc +lat_0={} +lon_0={} +lat_1={} +lat_2={}", + deg(p.angle(&ORIGIN_LAT)), + deg(p.angle(&ORIGIN_LONG)), + deg(p.angle(&[PROJ_STD_PARALLEL_1])), + deg(p.angle(&[PROJ_STD_PARALLEL_2])) + ), + ), + method::LCC_1SP => { + let lat = deg(p.angle(&NAT_LAT)); + push( + out, + format_args!( + "+proj=lcc +lat_1={lat} +lat_0={lat} +lon_0={} +k_0={k}", + deg(p.angle(&NAT_LONG)) + ), + ); + } + method::LAEA | method::AZIMUTHAL_EQUIDISTANT => { + let name = if code == method::LAEA { "laea" } else { "aeqd" }; + push( + out, + format_args!( + "+proj={name} +lat_0={} +lon_0={}", + deg(p.angle(&CENTER_LAT)), + deg(p.angle(&CENTER_LONG)) + ), + ); + } + method::ALBERS => push( + out, + format_args!( + "+proj=aea +lat_1={} +lat_2={} +lat_0={} +lon_0={}", + deg(p.angle(&[PROJ_STD_PARALLEL_1])), + deg(p.angle(&[PROJ_STD_PARALLEL_2])), + deg(p.angle(&ORIGIN_LAT)), + deg(p.angle(&ORIGIN_LONG)) + ), + ), + method::STEREOGRAPHIC | method::OBLIQUE_STEREOGRAPHIC => { + let name = if code == method::STEREOGRAPHIC { + "stere" + } else { + "sterea" + }; + push( + out, + format_args!( + "+proj={name} +lat_0={} +lon_0={} +k={k}", + deg(p.angle(&CENTER_LAT)), + deg(p.angle(&CENTER_LONG)) + ), + ); + } + method::POLAR_STEREOGRAPHIC => { + let lat_ts = deg(p.angle(&NAT_LAT)); + let pole = if lat_ts < 0.0 { -90.0 } else { 90.0 }; + let lon = p + .angle(&[PROJ_STRAIGHT_VERT_POLE_LONG]) + .or_else(|| p.angle(&NAT_LONG)); + push( + out, + format_args!( + "+proj=stere +lat_0={pole} +lat_ts={lat_ts} +lon_0={} +k={k}", + deg(lon) + ), + ); + } + method::EQUIRECTANGULAR => { + let lat_ts = p + .angle(&[PROJ_STD_PARALLEL_1]) + .or_else(|| p.angle(&[PROJ_CENTER_LAT])); + push( + out, + format_args!( + "+proj=eqc +lat_ts={} +lon_0={}", + deg(lat_ts), + deg(p.angle(&CENTER_LONG)) + ), + ); + } + method::CASSINI_SOLDNER | method::POLYCONIC | method::NEW_ZEALAND_MAP_GRID => { + let name = match code { + method::CASSINI_SOLDNER => "cass", + method::POLYCONIC => "poly", + _ => "nzmg", + }; + push( + out, + format_args!( + "+proj={name} +lat_0={} +lon_0={}", + deg(p.angle(&NAT_LAT)), + deg(p.angle(&NAT_LONG)) + ), + ); + } + method::SINUSOIDAL => push( + out, + format_args!("+proj=sinu +lon_0={}", deg(p.angle(&CENTER_LONG))), + ), + method::CYLINDRICAL_EQUAL_AREA => push( + out, + format_args!( + "+proj=cea +lat_ts={} +lon_0={}", + deg(p.angle(&[PROJ_STD_PARALLEL_1])), + deg(p.angle(&NAT_LONG)) + ), + ), + other => { + return Err(unsupported(format!( + "projection method {other} in GeoKey {PROJ_METHOD} is not supported" + ))); + } + } + push(out, format_args!(" +x_0={x0} +y_0={y0}")); + Ok(()) +} + +/// The UTM zone of EPSG conversion `code`, as PROJ parameters. +fn utm_conversion(code: u16, out: &mut String) -> Result<(), RasterError> { + let (zone, south) = match code { + c if (UTM_NORTH_BASE + 1..=UTM_NORTH_BASE + UTM_ZONES).contains(&c) => { + (c - UTM_NORTH_BASE, false) + } + c if (UTM_SOUTH_BASE + 1..=UTM_SOUTH_BASE + UTM_ZONES).contains(&c) => { + (c - UTM_SOUTH_BASE, true) + } + other => { + return Err(unsupported(format!( + "conversion {other} in GeoKey {PROJECTION} is not supported; \ + supported: UTM zones (16001..16060, 16101..16160) and user-defined (32767)" + ))); + } + }; + push(out, format_args!("+proj=utm +zone={zone}")); + if south { + out.push_str(" +south"); + } + Ok(()) +} diff --git a/nodedb-raster/src/crs/registry_match.rs b/nodedb-raster/src/crs/registry_match.rs new file mode 100644 index 000000000..0f9bf8cee --- /dev/null +++ b/nodedb-raster/src/crs/registry_match.rs @@ -0,0 +1,200 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Match a decoded CRS definition to the registered CRS it equals: a live +//! user-defined CRS of the context, or a built-in CRS. +//! +//! Two definitions have one shape when their projections have the same +//! method and parameters, their ellipsoids the same axes, their prime +//! meridians and axis orders agree, and their linear units are equal. +//! +//! The datum decides among candidates of one shape: +//! - A decoded datum with an EPSG code accepts only candidates on that +//! datum code. +//! - A decoded datum without one accepts candidates where +//! [`Datum::same_datum`] holds: same ellipsoid and a shift with the same +//! effect. A GeoTIFF that names only an ellipsoid carries no shift, so it +//! accepts every candidate with a null shift on that ellipsoid. +//! - When the accepted candidates sit on more than one EPSG datum, the file +//! does not say which one it means. The match is refused as ambiguous, so +//! the stored SRID never names a datum the file does not. +//! +//! Among accepted candidates on one datum the lowest code wins. + +use nodedb_crs::registry::builtin_codes; +use nodedb_crs::{ + CrsContext, CrsDef, CrsKind, CustomCrsState, Datum, Ellipsoid, LinearUnit, ProjectionDef, +}; +use nodedb_types::geometry::Srid; + +use crate::error::RasterError; + +/// Relative tolerance of a projection parameter, an axis or a unit. +const RELATIVE_TOLERANCE: f64 = 1e-9; + +/// The registered SRID `def` equals. +/// +/// Fails with [`RasterError::UnmatchedUserCrs`] when no candidate has the +/// shape and datum of `def`, and with [`RasterError::AmbiguousUserCrs`] +/// when the accepted candidates sit on two EPSG datums. Both name the PROJ +/// text of `def`. +pub fn match_registered(def: &CrsDef, ctx: &CrsContext<'_>) -> Result { + let custom: Vec = ctx + .custom_set() + .entries() + .iter() + .filter_map(|entry| match &entry.state { + CustomCrsState::Live(live) if live.kind == def.kind => Some(entry.srid), + CustomCrsState::Live(_) | CustomCrsState::Dropped => None, + }) + .collect(); + let mut chosen: Option<(Srid, Option)> = None; + for srid in builtin_codes(def.kind).chain(custom) { + // A fork per candidate keeps the memo from growing over the whole + // table. + let Ok(candidate) = ctx.fork().resolve(srid) else { + continue; + }; + if !same_shape(def, &candidate) || !datum_accepts(&def.datum, &candidate.datum) { + continue; + } + let datum_code = candidate.datum.epsg_code; + chosen = match chosen { + None => Some((srid, datum_code)), + Some((first, first_code)) if first_code != datum_code => { + return Err(RasterError::AmbiguousUserCrs { + proj: def.to_proj_string().unwrap_or_else(|_| def.to_wkt2()), + first: first.min(srid), + second: first.max(srid), + }); + } + Some((first, first_code)) => Some((first.min(srid), first_code)), + }; + } + chosen + .map(|(srid, _)| srid) + .ok_or_else(|| RasterError::UnmatchedUserCrs { + proj: def.to_proj_string().unwrap_or_else(|_| def.to_wkt2()), + }) +} + +/// Whether a candidate datum is one the decoded datum accepts. +fn datum_accepts(decoded: &Datum, candidate: &Datum) -> bool { + match decoded.epsg_code { + Some(code) => candidate.epsg_code == Some(code), + None => decoded.same_datum(candidate), + } +} + +/// Whether `a` and `b` have equal projections, ellipsoids, prime +/// meridians, axis orders and units. +fn same_shape(a: &CrsDef, b: &CrsDef) -> bool { + a.kind == b.kind + && a.axis == b.axis + && close(a.datum.prime_meridian_deg, b.datum.prime_meridian_deg) + && same_ellipsoid(&a.datum.ellipsoid, &b.datum.ellipsoid) + && same_unit(&a.unit, &b.unit, a.kind) + && same_projection(a.projection.as_ref(), b.projection.as_ref()) +} + +fn close(a: f64, b: f64) -> bool { + (a - b).abs() <= RELATIVE_TOLERANCE * a.abs().max(b.abs()).max(1.0) +} + +fn same_ellipsoid(a: &Ellipsoid, b: &Ellipsoid) -> bool { + close(a.a, b.a) && (a.is_sphere() == b.is_sphere()) && close(a.inv_f, b.inv_f) +} + +/// Geographic coordinates are degrees whatever the unit field holds. +fn same_unit(a: &LinearUnit, b: &LinearUnit, kind: CrsKind) -> bool { + match kind { + CrsKind::Geographic => true, + CrsKind::Projected => close(a.metres_per_unit, b.metres_per_unit), + } +} + +/// Same method and every parameter within tolerance. The parameters are +/// compared through their PROJ text, so every method compares one way. +fn same_projection(a: Option<&ProjectionDef>, b: Option<&ProjectionDef>) -> bool { + match (a, b) { + (None, None) => true, + (Some(a), Some(b)) => { + if core::mem::discriminant(a) != core::mem::discriminant(b) { + return false; + } + let (mut ta, mut tb) = (String::new(), String::new()); + a.write_proj(&mut ta); + b.write_proj(&mut tb); + same_tokens(&ta, &tb) + } + (Some(_), None) | (None, Some(_)) => false, + } +} + +/// Whether two PROJ parameter lists hold the same keys in the same order, +/// with numeric values within tolerance and other values equal. +fn same_tokens(a: &str, b: &str) -> bool { + let mut ia = a.split_whitespace(); + let mut ib = b.split_whitespace(); + loop { + match (ia.next(), ib.next()) { + (None, None) => return true, + (Some(x), Some(y)) => { + let (kx, vx) = x.split_once('=').unwrap_or((x, "")); + let (ky, vy) = y.split_once('=').unwrap_or((y, "")); + if kx != ky { + return false; + } + let equal = match (vx.parse::(), vy.parse::()) { + (Ok(nx), Ok(ny)) => close(nx, ny), + _ => vx == vy, + }; + if !equal { + return false; + } + } + (Some(_), None) | (None, Some(_)) => return false, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use nodedb_crs::{CustomCrsSet, parse_proj_string}; + + #[test] + fn a_builtin_matches_itself() { + let ctx = CrsContext::builtin_only(); + let srid = Srid::new(32650).unwrap(); + let def = ctx.resolve(srid).unwrap(); + assert_eq!(match_registered(&def, &ctx).ok(), Some(srid)); + } + + /// A definition with no built-in twin matches the user-defined CRS of + /// the context that equals it. + #[test] + fn a_custom_definition_matches_its_code() { + let custom = Srid::new(910_042).unwrap(); + let proj = "+proj=tmerc +lat_0=0 +lon_0=101.25 +k=0.99991 +x_0=123456 +y_0=0 \ + +ellps=GRS80 +units=m"; + let mut set = CustomCrsSet::new(); + set.define(parse_proj_string(custom, "custom tm", proj).unwrap()) + .unwrap(); + let ctx = CrsContext::new(&set); + let decoded = parse_proj_string(Srid::new(910_000).unwrap(), "decoded", proj).unwrap(); + assert_eq!(match_registered(&decoded, &ctx).ok(), Some(custom)); + let builtin_only = CrsContext::builtin_only(); + assert!(matches!( + match_registered(&decoded, &builtin_only), + Err(RasterError::UnmatchedUserCrs { .. }) + )); + } + + #[test] + fn tokens_compare_numbers_with_tolerance() { + assert!(same_tokens("+proj=sinu +lon_0=0", "+proj=sinu +lon_0=0.0")); + assert!(same_tokens("+k=0.9996", "+k=0.99960000000001")); + assert!(!same_tokens("+k=0.9996", "+k=0.9997")); + assert!(!same_tokens("+proj=sinu", "+proj=tmerc")); + } +} diff --git a/nodedb-raster/src/crs/srid.rs b/nodedb-raster/src/crs/srid.rs new file mode 100644 index 000000000..64d8a4399 --- /dev/null +++ b/nodedb-raster/src/crs/srid.rs @@ -0,0 +1,403 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! The CRS the GeoKey directory names, and its SRID. +//! +//! A projected model (1) reads `ProjectedCRSGeoKey` (3072), a geographic +//! model (2) reads `GeodeticCRSGeoKey` (2048). With no model key, 3072 wins +//! over 2048. An EPSG code resolves through the [`CrsContext`]. A +//! user-defined CRS (32767) decodes from its parameter keys and maps to the +//! registered CRS with the same projection, ellipsoid, unit and datum: a +//! live user-defined CRS of the context, or a built-in CRS. + +use nodedb_crs::CrsContext; +use nodedb_types::geometry::Srid; + +use super::registry_match::match_registered; +use super::units::{UNDEFINED, USER_DEFINED}; +use super::user_defined::decode_user_crs; +use crate::error::RasterError; +use crate::header::{CrsRef, UserDefinedCrs}; +use crate::tiff::geokeys::{GEOGRAPHIC_TYPE, GT_MODEL_TYPE, GeoKeyDirectory, PROJECTED_CS_TYPE}; + +const MODEL_PROJECTED: u16 = 1; +const MODEL_GEOGRAPHIC: u16 = 2; + +/// The CRS the GeoKeys name. A model type or CRS key the directory cannot +/// name a CRS with is an error here; codes resolve later. +pub fn crs_ref_from_geokeys(keys: Option<&GeoKeyDirectory>) -> Result { + let Some(keys) = keys else { + return Ok(CrsRef::None); + }; + let key = match keys.short(GT_MODEL_TYPE) { + Some(MODEL_PROJECTED) => PROJECTED_CS_TYPE, + Some(MODEL_GEOGRAPHIC) => GEOGRAPHIC_TYPE, + Some(model) => return Err(RasterError::UnsupportedModelType { model }), + None if keys.short(PROJECTED_CS_TYPE).is_some() => PROJECTED_CS_TYPE, + None if keys.short(GEOGRAPHIC_TYPE).is_some() => GEOGRAPHIC_TYPE, + None => return Ok(CrsRef::None), + }; + let model = keys.short(GT_MODEL_TYPE).unwrap_or(UNDEFINED); + match keys.short(key) { + None | Some(UNDEFINED) => Err(RasterError::MissingCrsKey { model, key }), + Some(USER_DEFINED) => Ok(CrsRef::UserDefined(Box::new(UserDefinedCrs { + keys: keys.clone(), + projected: key == PROJECTED_CS_TYPE, + }))), + Some(code) => Ok(CrsRef::Epsg(u32::from(code))), + } +} + +/// The SRID `crs` resolves to in `ctx`, or `None` when it names no CRS. +pub fn resolve_crs(crs: &CrsRef, ctx: &CrsContext<'_>) -> Result, RasterError> { + match crs { + CrsRef::None => Ok(None), + CrsRef::Epsg(code) => Ok(Some(ctx.resolve_u32(*code)?)), + CrsRef::UserDefined(user) => { + let def = decode_user_crs(&user.keys, user.projected, ctx)?; + match_registered(&def, ctx).map(Some) + } + } +} + +/// The SRID the GeoKeys name, or `None` when they name no CRS. +pub fn srid_from_geokeys( + keys: Option<&GeoKeyDirectory>, + ctx: &CrsContext<'_>, +) -> Result, RasterError> { + resolve_crs(&crs_ref_from_geokeys(keys)?, ctx) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::tiff::geokeys::{ + ELLIPSOID, GEODETIC_DATUM, INV_FLATTENING, PROJ_CENTER_LONG, PROJ_FALSE_EASTING, + PROJ_FALSE_NORTHING, PROJ_LINEAR_UNITS, PROJ_METHOD, PROJ_NAT_ORIGIN_LAT, + PROJ_NAT_ORIGIN_LONG, PROJ_SCALE_AT_NAT_ORIGIN, PROJECTION, SEMI_MAJOR_AXIS, + SEMI_MINOR_AXIS, + }; + use nodedb_crs::UnknownSrid; + + const DOUBLE_PARAMS_TAG: u16 = 34736; + + fn srid(code: u32) -> Srid { + Srid::new(code).unwrap() + } + + fn ctx() -> CrsContext<'static> { + CrsContext::builtin_only() + } + + fn keys(entries: &[(u16, u16)]) -> GeoKeyDirectory { + with_doubles(entries, &[]) + } + + /// A directory of inline short keys and double keys, sorted by key. + fn with_doubles(shorts: &[(u16, u16)], doubles: &[(u16, f64)]) -> GeoKeyDirectory { + let mut all: Vec<(u16, u16, u16)> = shorts.iter().map(|(k, v)| (*k, 0, *v)).collect(); + let mut values = Vec::new(); + for (key, value) in doubles { + let offset = u16::try_from(values.len()).expect("few doubles"); + values.push(*value); + all.push((*key, DOUBLE_PARAMS_TAG, offset)); + } + all.sort_by_key(|(k, _, _)| *k); + let count = u16::try_from(all.len()).expect("few keys"); + let mut dir = vec![1, 1, 0, count]; + for (key, location, value) in all { + dir.extend([key, location, 1, value]); + } + GeoKeyDirectory::parse(&dir, Some(&values), None).expect("valid directory") + } + + #[test] + fn no_directory_is_no_crs() { + assert_eq!(srid_from_geokeys(None, &ctx()).ok(), Some(None)); + } + + #[test] + fn projected_model_reads_3072() { + let dir = keys(&[(GT_MODEL_TYPE, 1), (PROJECTED_CS_TYPE, 32650)]); + assert_eq!( + srid_from_geokeys(Some(&dir), &ctx()).ok(), + Some(Some(srid(32650))) + ); + } + + #[test] + fn geographic_model_reads_2048() { + let dir = keys(&[(GT_MODEL_TYPE, 2), (GEOGRAPHIC_TYPE, 4742)]); + assert_eq!( + srid_from_geokeys(Some(&dir), &ctx()).ok(), + Some(Some(srid(4742))) + ); + } + + #[test] + fn model_key_picks_the_crs_key() { + let dir = keys(&[ + (GT_MODEL_TYPE, 2), + (GEOGRAPHIC_TYPE, 4326), + (PROJECTED_CS_TYPE, 32648), + ]); + assert_eq!( + srid_from_geokeys(Some(&dir), &ctx()).ok(), + Some(Some(Srid::WGS84)) + ); + } + + #[test] + fn without_a_model_key_3072_wins() { + let dir = keys(&[(GEOGRAPHIC_TYPE, 4326), (PROJECTED_CS_TYPE, 3375)]); + assert_eq!( + srid_from_geokeys(Some(&dir), &ctx()).ok(), + Some(Some(srid(3375))) + ); + let none = keys(&[(1025, 1)]); + assert_eq!(srid_from_geokeys(Some(&none), &ctx()).ok(), Some(None)); + } + + #[test] + fn unmappable_crs_are_typed_errors() { + let geocentric = keys(&[(GT_MODEL_TYPE, 3)]); + assert!(matches!( + srid_from_geokeys(Some(&geocentric), &ctx()), + Err(RasterError::UnsupportedModelType { model: 3 }) + )); + let missing = keys(&[(GT_MODEL_TYPE, 1)]); + assert!(matches!( + srid_from_geokeys(Some(&missing), &ctx()), + Err(RasterError::MissingCrsKey { + model: 1, + key: 3072 + }) + )); + let undefined = keys(&[(GT_MODEL_TYPE, 1), (PROJECTED_CS_TYPE, 999)]); + assert!(matches!( + srid_from_geokeys(Some(&undefined), &ctx()), + Err(RasterError::UnknownCrs(UnknownSrid::Undefined { + code: 999 + })) + )); + } + + /// The MODIS sinusoidal grid as GDAL writes it: user-defined projection, + /// datum and ellipsoid on the 6371007.181 m sphere. + #[test] + fn modis_sinusoidal_maps_to_its_nodedb_code() { + let dir = with_doubles( + &[ + (GT_MODEL_TYPE, 1), + (GEOGRAPHIC_TYPE, USER_DEFINED), + (GEODETIC_DATUM, USER_DEFINED), + (ELLIPSOID, USER_DEFINED), + (PROJECTED_CS_TYPE, USER_DEFINED), + (PROJECTION, USER_DEFINED), + (PROJ_METHOD, 24), + (PROJ_LINEAR_UNITS, 9001), + ], + &[ + (SEMI_MAJOR_AXIS, 6_371_007.181), + (SEMI_MINOR_AXIS, 6_371_007.181), + (PROJ_FALSE_EASTING, 0.0), + (PROJ_FALSE_NORTHING, 0.0), + (PROJ_CENTER_LONG, 0.0), + ], + ); + assert_eq!( + srid_from_geokeys(Some(&dir), &ctx()).ok(), + Some(Some(Srid::MODIS_SINUSOIDAL)) + ); + } + + /// UTM zone 49N on WGS 84, spelled out as Transverse Mercator + /// parameters over the EPSG:4326 geographic CRS. + #[test] + fn user_defined_utm_49n_maps_to_32649() { + let dir = with_doubles( + &[ + (GT_MODEL_TYPE, 1), + (GEOGRAPHIC_TYPE, 4326), + (PROJECTED_CS_TYPE, USER_DEFINED), + (PROJECTION, USER_DEFINED), + (PROJ_METHOD, 1), + (PROJ_LINEAR_UNITS, 9001), + ], + &[ + (PROJ_NAT_ORIGIN_LONG, 111.0), + (PROJ_NAT_ORIGIN_LAT, 0.0), + (PROJ_FALSE_EASTING, 500_000.0), + (PROJ_FALSE_NORTHING, 0.0), + (PROJ_SCALE_AT_NAT_ORIGIN, 0.9996), + ], + ); + assert_eq!( + srid_from_geokeys(Some(&dir), &ctx()).ok(), + Some(Some(srid(32649))) + ); + } + + /// UTM zone 49N parameters as GeoKeys, over `datum_keys`. + fn utm_49n_over(datum_keys: &[(u16, u16)]) -> GeoKeyDirectory { + let mut shorts = vec![ + (GT_MODEL_TYPE, 1), + (GEOGRAPHIC_TYPE, USER_DEFINED), + (PROJECTED_CS_TYPE, USER_DEFINED), + (PROJECTION, USER_DEFINED), + (PROJ_METHOD, 1), + (PROJ_LINEAR_UNITS, 9001), + ]; + shorts.extend_from_slice(datum_keys); + with_doubles( + &shorts, + &[ + (PROJ_NAT_ORIGIN_LONG, 111.0), + (PROJ_NAT_ORIGIN_LAT, 0.0), + (PROJ_FALSE_EASTING, 500_000.0), + (PROJ_FALSE_NORTHING, 0.0), + (PROJ_SCALE_AT_NAT_ORIGIN, 0.9996), + ], + ) + } + + /// The WGS 84 datum code picks WGS 84 / UTM zone 49N, never VN-2000 / + /// UTM zone 49N (3406) on the same ellipsoid. + #[test] + fn utm_49n_on_the_wgs84_datum_maps_to_32649() { + let dir = utm_49n_over(&[(GEODETIC_DATUM, 6326)]); + assert_eq!( + srid_from_geokeys(Some(&dir), &ctx()).ok(), + Some(Some(srid(32649))) + ); + } + + /// Only the WGS 84 ellipsoid (7030) is known. SRGI2013 (9479), DGN95 + /// (23869) and WGS 84 (32649) all put UTM zone 49N on that ellipsoid + /// with a null shift, so the file names no single datum and the match + /// is refused. VN-2000 (3406) carries a shift and is never accepted. + #[test] + fn utm_49n_on_the_wgs84_ellipsoid_alone_is_ambiguous() { + let dir = utm_49n_over(&[(GEODETIC_DATUM, USER_DEFINED), (ELLIPSOID, 7030)]); + let Err(RasterError::AmbiguousUserCrs { + proj, + first, + second, + }) = srid_from_geokeys(Some(&dir), &ctx()) + else { + panic!("expected an ambiguous user CRS"); + }; + assert!(proj.contains("+proj=tmerc"), "{proj}"); + for code in [first.code(), second.code()] { + assert_ne!(code, 3406); + assert!([9479, 23869, 32649].contains(&code), "{code}"); + } + } + + /// A geographic CRS on the WGS 84 ellipsoid alone is refused, never + /// mapped to Moznet (4130): Moznet, Hartebeesthoek94 (4148), WGS 84 + /// (4326) and others share that ellipsoid with a null shift. + #[test] + fn geographic_wgs84_ellipsoid_alone_is_ambiguous() { + let dir = keys(&[ + (GT_MODEL_TYPE, 2), + (GEOGRAPHIC_TYPE, USER_DEFINED), + (GEODETIC_DATUM, USER_DEFINED), + (ELLIPSOID, 7030), + ]); + match srid_from_geokeys(Some(&dir), &ctx()) { + Err(RasterError::AmbiguousUserCrs { first, second, .. }) => { + assert_eq!((first.code(), second.code()), (4130, 4148)); + } + other => panic!("expected an ambiguous user CRS, got {other:?}"), + } + let on_datum = keys(&[ + (GT_MODEL_TYPE, 2), + (GEOGRAPHIC_TYPE, USER_DEFINED), + (GEODETIC_DATUM, 6326), + ]); + assert_eq!( + srid_from_geokeys(Some(&on_datum), &ctx()).ok(), + Some(Some(Srid::WGS84)) + ); + } + + /// A user-defined GeoKey CRS that equals a live user-defined CRS of the + /// context maps to that code. + #[test] + fn user_defined_crs_maps_to_a_custom_definition() { + let custom = srid(910_777); + let mut set = nodedb_crs::CustomCrsSet::new(); + set.define( + nodedb_crs::parse_proj_string( + custom, + "custom sinusoidal", + "+proj=sinu +lon_0=17 +R=6371000 +units=m", + ) + .unwrap(), + ) + .unwrap(); + let dir = with_doubles( + &[ + (GT_MODEL_TYPE, 1), + (GEOGRAPHIC_TYPE, USER_DEFINED), + (GEODETIC_DATUM, USER_DEFINED), + (ELLIPSOID, USER_DEFINED), + (PROJECTED_CS_TYPE, USER_DEFINED), + (PROJECTION, USER_DEFINED), + (PROJ_METHOD, 24), + ], + &[ + (SEMI_MAJOR_AXIS, 6_371_000.0), + (INV_FLATTENING, 0.0), + (PROJ_CENTER_LONG, 17.0), + ], + ); + assert_eq!( + srid_from_geokeys(Some(&dir), &CrsContext::new(&set)).ok(), + Some(Some(custom)) + ); + } + + /// The same zone through the EPSG conversion code 16049. + #[test] + fn utm_conversion_code_maps_to_32649() { + let dir = keys(&[ + (GT_MODEL_TYPE, 1), + (GEOGRAPHIC_TYPE, 4326), + (PROJECTED_CS_TYPE, USER_DEFINED), + (PROJECTION, 16049), + ]); + assert_eq!( + srid_from_geokeys(Some(&dir), &ctx()).ok(), + Some(Some(srid(32649))) + ); + } + + /// A user-defined CRS with no built-in twin names its PROJ text. + #[test] + fn unmatched_user_crs_names_its_proj_text() { + let dir = with_doubles( + &[ + (GT_MODEL_TYPE, 1), + (GEOGRAPHIC_TYPE, USER_DEFINED), + (GEODETIC_DATUM, USER_DEFINED), + (ELLIPSOID, USER_DEFINED), + (PROJECTED_CS_TYPE, USER_DEFINED), + (PROJECTION, USER_DEFINED), + (PROJ_METHOD, 24), + ], + &[ + (SEMI_MAJOR_AXIS, 6_371_000.0), + (INV_FLATTENING, 0.0), + (PROJ_CENTER_LONG, 17.0), + ], + ); + let Err(RasterError::UnmatchedUserCrs { proj }) = srid_from_geokeys(Some(&dir), &ctx()) + else { + panic!("expected an unmatched user CRS"); + }; + assert!(proj.contains("+proj=sinu"), "{proj}"); + assert!(proj.contains("+lon_0=17"), "{proj}"); + assert!(proj.contains("+R=6371000"), "{proj}"); + } +} diff --git a/nodedb-raster/src/crs/units.rs b/nodedb-raster/src/crs/units.rs new file mode 100644 index 000000000..14933a1d3 --- /dev/null +++ b/nodedb-raster/src/crs/units.rs @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! GeoKey codes and units shared by the user-defined CRS decoder. + +use core::fmt::Write; + +use nodedb_crs::LinearUnit; + +use crate::error::RasterError; +use crate::tiff::geokeys::{GeoKeyDirectory, PROJ_LINEAR_UNIT_SIZE, PROJ_LINEAR_UNITS}; + +/// GeoKey value of a CRS, datum, ellipsoid or unit defined by parameters. +pub const USER_DEFINED: u16 = 32767; + +/// GeoKey value of an undefined code. +pub const UNDEFINED: u16 = 0; + +/// EPSG angle unit codes. +const RADIAN: u16 = 9101; +const DEGREE: u16 = 9102; +const GRAD: u16 = 9105; +const DEGREE_SUPPLIER: u16 = 9122; + +/// A key value that names an EPSG code: neither absent, undefined, nor +/// user-defined. +pub(super) fn epsg_code(value: Option) -> Option { + value.filter(|v| *v != UNDEFINED && *v != USER_DEFINED) +} + +/// Append formatted text. Writing to a `String` cannot fail. +pub(super) fn push(out: &mut String, args: core::fmt::Arguments<'_>) { + let _ = out.write_fmt(args); +} + +pub(super) fn unsupported(detail: String) -> RasterError { + RasterError::UnsupportedUserCrs { detail } +} + +/// An angle unit as degrees per unit. +#[derive(Debug, Clone, Copy)] +pub(super) struct AngleUnit(f64); + +impl AngleUnit { + /// The unit of `code_key`, or of `size_key` (radians per unit) when the + /// code is user-defined. Degrees when absent. + pub(super) fn of( + keys: &GeoKeyDirectory, + code_key: u16, + size_key: u16, + ) -> Result { + match keys.short(code_key) { + None | Some(DEGREE) | Some(DEGREE_SUPPLIER) => Ok(AngleUnit(1.0)), + Some(RADIAN) => Ok(AngleUnit(1.0_f64.to_degrees())), + Some(GRAD) => Ok(AngleUnit(0.9)), + Some(USER_DEFINED) => { + let radians = keys.double(size_key).ok_or_else(|| { + unsupported(format!( + "GeoKey {code_key} is 32767 but GeoKey {size_key} is absent" + )) + })?; + Ok(AngleUnit(radians.to_degrees())) + } + Some(code) => Err(unsupported(format!( + "angle unit {code} in GeoKey {code_key} is not supported; \ + supported: degree (9102, 9122), radian (9101), grad (9105)" + ))), + } + } + + pub(super) fn degrees(self, value: f64) -> f64 { + value * self.0 + } +} + +/// Metres per projected unit, from `ProjLinearUnitsGeoKey`. Metres when +/// absent. +pub(super) fn linear_unit(keys: &GeoKeyDirectory) -> Result { + match keys.short(PROJ_LINEAR_UNITS) { + None => Ok(1.0), + Some(USER_DEFINED) => keys.double(PROJ_LINEAR_UNIT_SIZE).ok_or_else(|| { + unsupported(format!( + "GeoKey {PROJ_LINEAR_UNITS} is 32767 but GeoKey {PROJ_LINEAR_UNIT_SIZE} is absent" + )) + }), + Some(code) => LinearUnit::by_epsg_code(code) + .map(|unit| unit.metres_per_unit) + .ok_or_else(|| { + unsupported(format!( + "length unit {code} is not supported; supported: metre (9001), \ + foot (9002), US survey foot (9003), kilometre (9036)" + )) + }), + } +} diff --git a/nodedb-raster/src/crs/user_defined.rs b/nodedb-raster/src/crs/user_defined.rs new file mode 100644 index 000000000..c73ba7944 --- /dev/null +++ b/nodedb-raster/src/crs/user_defined.rs @@ -0,0 +1,224 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! A user-defined GeoTIFF CRS (GeoKey value 32767), decoded into a +//! [`CrsDef`] from its parameter keys (OGC GeoTIFF 1.1, Requirements +//! Classes GeodeticCRS and ProjectedCRS). +//! +//! The projection keys map to PROJ parameters, then the PROJ-string parser +//! builds the definition. The datum comes from the geodetic CRS code, the +//! datum code, or the ellipsoid keys. + +use nodedb_crs::registry::geographic_crs_on_datum; +use nodedb_crs::{CrsContext, CrsDef, Datum, DatumShift, Ellipsoid, parse_proj_string}; +use nodedb_types::geometry::Srid; + +use super::projection_keys::projection; +use super::units::{AngleUnit, USER_DEFINED, epsg_code, linear_unit, push, unsupported}; +use crate::error::RasterError; +use crate::tiff::geokeys::{ + ELLIPSOID, GEODETIC_DATUM, GEOG_ANGULAR_UNIT_SIZE, GEOG_ANGULAR_UNITS, GEOGRAPHIC_TYPE, + GeoKeyDirectory, INV_FLATTENING, PRIME_MERIDIAN, PRIME_MERIDIAN_LONGITUDE, SEMI_MAJOR_AXIS, + SEMI_MINOR_AXIS, +}; + +/// The SRID a decoded definition holds until it matches a built-in CRS: +/// the first code of the user range. The definition is never stored under +/// it. +const UNASSIGNED: Srid = Srid::known(Srid::USER_FIRST); + +/// Name of every decoded definition. +const NAME: &str = "GeoTIFF user-defined CRS"; + +/// EPSG prime meridian code of Greenwich. +const GREENWICH: u16 = 8901; + +/// Decode the user-defined CRS of `keys`: projected when `projected`, else +/// geographic. An EPSG geodetic CRS code resolves through `ctx`. +pub fn decode_user_crs( + keys: &GeoKeyDirectory, + projected: bool, + ctx: &CrsContext<'_>, +) -> Result { + let angle = AngleUnit::of(keys, GEOG_ANGULAR_UNITS, GEOG_ANGULAR_UNIT_SIZE)?; + let datum = geodetic_datum(keys, angle, ctx)?; + let mut proj = String::new(); + if projected { + let unit = linear_unit(keys)?; + projection(keys, angle, unit, &mut proj)?; + push(&mut proj, format_args!(" +to_meter={unit}")); + } else { + proj.push_str("+proj=longlat"); + } + write_ellipsoid(&mut proj, &datum.ellipsoid); + let mut def = parse_proj_string(UNASSIGNED, NAME, &proj)?; + def.datum = datum; + Ok(def) +} + +fn write_ellipsoid(out: &mut String, e: &Ellipsoid) { + if e.is_sphere() { + push(out, format_args!(" +R={}", e.a)); + } else { + push(out, format_args!(" +a={} +rf={}", e.a, e.inv_f)); + } +} + +/// The datum: from an EPSG geodetic CRS code, else an EPSG datum code, +/// else the ellipsoid and prime meridian keys with no known shift. +fn geodetic_datum( + keys: &GeoKeyDirectory, + angle: AngleUnit, + ctx: &CrsContext<'_>, +) -> Result { + if let Some(code) = epsg_code(keys.short(GEOGRAPHIC_TYPE)) { + let srid = ctx.resolve_u32(u32::from(code))?; + return Ok(ctx.resolve(srid)?.datum); + } + if let Some(code) = epsg_code(keys.short(GEODETIC_DATUM)) { + return datum_by_code(code, ctx); + } + Ok(Datum { + ellipsoid: ellipsoid(keys)?, + prime_meridian_deg: prime_meridian(keys, angle)?, + to_wgs84: DatumShift::Identity, + epsg_code: None, + }) +} + +/// The datum of the lowest-coded built-in geographic CRS on EPSG datum +/// `code`. +fn datum_by_code(code: u16, ctx: &CrsContext<'_>) -> Result { + let srid = geographic_crs_on_datum(u32::from(code)).ok_or_else(|| { + unsupported(format!( + "datum {code} in GeoKey {GEODETIC_DATUM} has no built-in geographic CRS" + )) + })?; + Ok(ctx.resolve(srid)?.datum) +} + +/// The ellipsoid: an EPSG code from the table, else the axis keys. +fn ellipsoid(keys: &GeoKeyDirectory) -> Result { + if let Some(code) = epsg_code(keys.short(ELLIPSOID)) { + return Ellipsoid::by_epsg_code(code).ok_or_else(|| { + unsupported(format!( + "ellipsoid {code} in GeoKey {ELLIPSOID} is not supported" + )) + }); + } + let a = keys + .double(SEMI_MAJOR_AXIS) + .filter(|a| a.is_finite() && *a > 0.0) + .ok_or_else(|| { + unsupported(format!( + "GeoKey {SEMI_MAJOR_AXIS} is absent or not positive" + )) + })?; + if let Some(rf) = keys.double(INV_FLATTENING) { + return Ok(if rf == 0.0 { + Ellipsoid::sphere(a) + } else { + Ellipsoid { a, inv_f: rf } + }); + } + match keys.double(SEMI_MINOR_AXIS) { + Some(b) if b == a => Ok(Ellipsoid::sphere(a)), + Some(b) if b.is_finite() && b > 0.0 && b < a => Ok(Ellipsoid { + a, + inv_f: a / (a - b), + }), + _ => Err(unsupported(format!( + "GeoKey {SEMI_MAJOR_AXIS} needs GeoKey {INV_FLATTENING} or a smaller GeoKey {SEMI_MINOR_AXIS}" + ))), + } +} + +/// The prime meridian, degrees east of Greenwich. +fn prime_meridian(keys: &GeoKeyDirectory, angle: AngleUnit) -> Result { + match keys.short(PRIME_MERIDIAN) { + None | Some(GREENWICH) => Ok(0.0), + Some(USER_DEFINED) => Ok(keys + .double(PRIME_MERIDIAN_LONGITUDE) + .map_or(0.0, |v| angle.degrees(v))), + Some(code) => Err(unsupported(format!( + "prime meridian {code} in GeoKey {PRIME_MERIDIAN} is not supported; \ + supported: Greenwich (8901), user-defined (32767)" + ))), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::tiff::geokeys::{ + PROJ_FALSE_EASTING, PROJ_LINEAR_UNITS, PROJ_METHOD, PROJ_NAT_ORIGIN_LONG, PROJECTION, + }; + + const DOUBLE_PARAMS_TAG: u16 = 34736; + + fn directory(shorts: &[(u16, u16)], doubles: &[(u16, f64)]) -> GeoKeyDirectory { + let mut all: Vec<(u16, u16, u16)> = shorts.iter().map(|(k, v)| (*k, 0, *v)).collect(); + let mut values = Vec::new(); + for (key, value) in doubles { + let offset = u16::try_from(values.len()).expect("few doubles"); + values.push(*value); + all.push((*key, DOUBLE_PARAMS_TAG, offset)); + } + all.sort_by_key(|(k, _, _)| *k); + let count = u16::try_from(all.len()).expect("few keys"); + let mut dir = vec![1, 1, 0, count]; + for (key, location, value) in all { + dir.extend([key, location, 1, value]); + } + GeoKeyDirectory::parse(&dir, Some(&values), None).expect("valid directory") + } + + #[test] + fn feet_convert_false_easting_to_metres() { + let dir = directory( + &[ + (GEOGRAPHIC_TYPE, 4326), + (PROJECTION, USER_DEFINED), + (PROJ_METHOD, 1), + (PROJ_LINEAR_UNITS, 9002), + ], + &[(PROJ_FALSE_EASTING, 1000.0), (PROJ_NAT_ORIGIN_LONG, 111.0)], + ); + let def = decode_user_crs(&dir, true, &CrsContext::builtin_only()).unwrap(); + let text = def.to_proj_string().unwrap(); + assert!(text.contains("+x_0=304.8"), "{text}"); + assert!(text.contains("+units=ft"), "{text}"); + } + + #[test] + fn ellipsoid_from_axes() { + let dir = directory( + &[ + (GEOGRAPHIC_TYPE, USER_DEFINED), + (GEODETIC_DATUM, USER_DEFINED), + ], + &[ + (SEMI_MAJOR_AXIS, 6_378_137.0), + (INV_FLATTENING, 298.257_222_101), + ], + ); + let def = decode_user_crs(&dir, false, &CrsContext::builtin_only()).unwrap(); + assert!(def.is_geographic()); + assert_eq!(def.datum.ellipsoid, Ellipsoid::GRS80); + } + + #[test] + fn unknown_method_is_refused() { + let dir = directory( + &[ + (GEOGRAPHIC_TYPE, 4326), + (PROJECTION, USER_DEFINED), + (PROJ_METHOD, 19), + ], + &[], + ); + assert!(matches!( + decode_user_crs(&dir, true, &CrsContext::builtin_only()), + Err(RasterError::UnsupportedUserCrs { .. }) + )); + } +} diff --git a/nodedb-raster/src/error.rs b/nodedb-raster/src/error.rs index db090436d..f8087178d 100644 --- a/nodedb-raster/src/error.rs +++ b/nodedb-raster/src/error.rs @@ -1,34 +1,65 @@ // SPDX-License-Identifier: Apache-2.0 -//! Typed errors of the GeoTIFF reader. +//! Typed errors of the raster readers. use std::io; -use nodedb_types::geometry::UnsupportedSrid; -use tiff::TiffError; +use nodedb_crs::{CrsParseError, UnknownSrid}; -use crate::geokeys::GeoKeyError; +use crate::codec::error::CodecError; use crate::sample::SampleFormatCode; +use crate::source::range::ByteRange; +use crate::tiff::error::{TiffFormatError, UnsupportedTiff}; +use crate::tiff::geokeys::GeoKeyError; -/// Every way opening or decoding a GeoTIFF can fail. +/// Every way opening or decoding a raster can fail. #[derive(Debug, thiserror::Error)] pub enum RasterError { - /// The underlying reader failed for a reason outside the file's bytes. - #[error("GeoTIFF read failed: {0}")] - Io(#[source] io::Error), + /// The byte source failed for a reason outside the file's bytes. + #[error("raster read failed: {0}")] + Io(#[from] io::Error), /// The bytes do not form a valid TIFF: truncated, corrupt, or /// self-contradictory. #[error("malformed TIFF: {0}")] - Malformed(#[source] TiffError), + Malformed(#[from] TiffFormatError), + + /// A compressed chunk does not decode. + #[error("corrupt compressed block: {0}")] + Codec(#[from] CodecError), /// The TIFF is valid, but it uses a feature the decoder does not read. #[error("unsupported TIFF feature: {0}")] - UnsupportedTiff(#[source] TiffError), + UnsupportedTiff(#[from] UnsupportedTiff), + + /// The byte source has no object with this id. + #[error("raster source object {object} does not exist")] + SourceMissing { object: u32 }, + + /// A source returned more bytes than the range asked for. + #[error("raster source returned {got} bytes for the {len}-byte range {range:?}")] + RangeOverrun { + range: ByteRange, + len: u64, + got: usize, + }, - /// One block decodes to more bytes than the caller allows. + /// The header ranges an open needs exceed the header byte cap. #[error( - "GeoTIFF block needs {needed_bytes} decoded bytes, over the {limit_bytes}-byte limit; \ + "raster header data exceeds the {limit_bytes}-byte header cap; \ + the file has too many blocks or too large tag values" + )] + HeaderTooLarge { limit_bytes: usize }, + + /// An open asked for ranges it already holds, or kept asking past the + /// round cap. + #[error("raster open made no progress after {rounds} fetch rounds")] + OpenStalled { rounds: usize }, + + /// One block decodes to more bytes than the caller allows, or one + /// compressed chunk is larger than that cap. + #[error( + "raster block needs {needed_bytes} bytes, over the {limit_bytes}-byte limit; \ rewrite the file with smaller strips or tiles" )] BlockTooLarge { @@ -36,54 +67,47 @@ pub enum RasterError { limit_bytes: usize, }, - /// An IFD 0 tag value ends past the end of the file or exceeds the - /// tag value cap. - #[error( - "GeoTIFF tag {tag} declares a {bytes}-byte value, past the {file_len}-byte file \ - or over the tag value cap" - )] - TagValueOutOfBounds { tag: u16, bytes: u64, file_len: u64 }, - - /// The tiff decoder hit its own buffer limit. - #[error("GeoTIFF decode exceeds the {limit_bytes}-byte buffer limit")] - DecodeLimitExceeded { limit_bytes: usize }, + /// A tag value exceeds the tag value cap. + #[error("TIFF tag {tag} declares a {bytes}-byte value, over the {limit_bytes}-byte cap")] + TagValueTooLarge { + tag: u16, + bytes: u64, + limit_bytes: usize, + }, - /// The compression method is not deflate, LZW, PackBits, or none. + /// The compression method has no decoder. #[error( - "GeoTIFF compression code {code} is not supported; \ - supported: none (1), LZW (5), deflate (8, 32946), PackBits (32773)" + "TIFF compression code {code} is not supported; supported: none (1), LZW (5), \ + JPEG (7), deflate (8, 32946), PackBits (32773), LERC (34887), ZSTD (50000)" )] UnsupportedCompression { code: u16 }, - /// The sample format and width are outside u8, u16, i16, u32, f32, f64. + /// The sample format and width are outside the supported types. #[error( - "GeoTIFF samples of format {format} with {bits} bits are not supported; \ - supported: u8, u16, i16, u32, f32, f64" + "TIFF samples of format {format} with {bits} bits are not supported; \ + supported: u8, i8, u16, i16, u32, i32, f32, f64" )] - UnsupportedSampleType { format: SampleFormatCode, bits: u8 }, + UnsupportedSampleType { format: SampleFormatCode, bits: u16 }, /// The photometric interpretation transforms samples on decode, or /// stores palette indexes. #[error( - "GeoTIFF photometric interpretation {code} is not supported; \ - supported: BlackIsZero (1), RGB (2), CMYK (5), YCbCr (6)" + "TIFF photometric interpretation {code} is not supported; supported: \ + BlackIsZero (1), RGB (2), CMYK (5), YCbCr (6) with JPEG or without subsampling" )] UnsupportedPhotometric { code: u16 }, - /// A chunky pixel decodes to fewer samples than the file stores. - #[error( - "GeoTIFF stores {samples} samples per pixel, but its color type decodes {decoded}; \ - write the extra bands as BlackIsZero multiband" - )] - UnsupportedBandLayout { samples: u16, decoded: u16 }, - - /// IFD 0 is an overview, so the full-resolution image is elsewhere. + /// The first image of the IFD chain is an overview. #[error( - "GeoTIFF IFD 0 is a reduced-resolution image; \ + "TIFF first image is a reduced-resolution image; \ the full-resolution image must be the first IFD" )] ReducedResolutionFirstImage, + /// The caller selected an image the file does not hold. + #[error("TIFF image {index} is out of range; the file has {count} images")] + ImageOutOfRange { index: u32, count: u32 }, + /// The GeoKey directory does not parse. #[error("malformed GeoKey directory: {0}")] GeoKeys(#[from] GeoKeyError), @@ -129,55 +153,67 @@ pub enum RasterError { #[error("GeoTIFF model type {model} needs GeoKey {key}, which is absent")] MissingCrsKey { model: u16, key: u16 }, - /// The CRS is user-defined, so no EPSG code names it. - #[error("GeoTIFF CRS is user-defined (GeoKey {key} = 32767); only EPSG codes are supported")] - UserDefinedCrs { key: u16 }, - - /// The CRS EPSG code is outside the supported set. + /// The CRS code names no defined CRS. #[error("GeoTIFF CRS: {0}")] - UnsupportedCrs(#[from] UnsupportedSrid), + UnknownCrs(#[from] UnknownSrid), + + /// A user-defined CRS (GeoKey value 32767) uses a GeoKey value the + /// decoder does not read. + #[error("GeoTIFF user-defined CRS: {detail}")] + UnsupportedUserCrs { detail: String }, + + /// A user-defined CRS decodes to PROJ text the CRS parser refuses. + #[error("GeoTIFF user-defined CRS: {0}")] + UserCrsParse(#[from] CrsParseError), - /// GDAL_NODATA is not a number. - #[error("GeoTIFF GDAL_NODATA '{text}' is not a number")] + /// A user-defined CRS matches no registered CRS by projection, + /// ellipsoid, unit and datum. + #[error( + "GeoTIFF user-defined CRS matches no registered CRS: {proj}; \ + define it with CREATE SPATIAL_REF_SYS and load again" + )] + UnmatchedUserCrs { proj: String }, + + /// A user-defined CRS names an ellipsoid but no datum, and registered + /// CRSs of its shape sit on more than one datum. + #[error( + "GeoTIFF user-defined CRS {proj} names no datum and matches CRSs on different \ + datums, SRID {first} and SRID {second}; write the datum code into the file, \ + or define the CRS with CREATE SPATIAL_REF_SYS and load again" + )] + AmbiguousUserCrs { + proj: String, + first: nodedb_crs::Srid, + second: nodedb_crs::Srid, + }, + + /// A nodata value (`GDAL_NODATA` or a `GDAL_METADATA` item) is not a + /// number. + #[error("raster nodata '{text}' is not a number")] InvalidNodata { text: String }, + /// A `GDAL_METADATA` band number or numeric item does not parse. + #[error("GDAL_METADATA item '{name}' holds '{text}', which is not a valid value")] + InvalidMetadata { name: String, text: String }, + /// The caller asked for a block the image does not have. - #[error("GeoTIFF block {index} is out of range; the image has {count} blocks")] + #[error("raster block {index} is out of range; the image has {count} blocks")] BlockOutOfRange { index: u32, count: u32 }, - /// The decoder returned fewer samples than the block holds. - #[error("GeoTIFF block {index} decoded {found} samples, expected at least {expected}")] - ShortBlock { + /// The caller passed a different number of byte buffers than + /// `block_ranges` returned. + #[error("raster block {index} got {found} byte buffers, expected {expected}")] + BlockBuffers { index: u32, found: usize, expected: usize, }, -} - -impl RasterError { - /// Classify a tiff decoder error. `limit_bytes` is the buffer cap the - /// decoder ran under. - pub(crate) fn from_tiff(error: TiffError, limit_bytes: usize) -> Self { - match error { - TiffError::IoError(io) => RasterError::from(io), - TiffError::UnsupportedError(_) => RasterError::UnsupportedTiff(error), - TiffError::LimitsExceeded => RasterError::DecodeLimitExceeded { limit_bytes }, - TiffError::FormatError(_) | TiffError::IntSizeError | TiffError::UsageError(_) => { - RasterError::Malformed(error) - } - } - } -} -impl From for RasterError { - /// A short or undecodable stream is a malformed file. Every other kind - /// is a reader error. - fn from(error: io::Error) -> Self { - match error.kind() { - io::ErrorKind::UnexpectedEof - | io::ErrorKind::InvalidData - | io::ErrorKind::InvalidInput => RasterError::Malformed(TiffError::IoError(error)), - _ => RasterError::Io(error), - } - } + /// A block decoded to fewer samples than it holds. + #[error("raster block {index} decoded {found} bytes, expected at least {expected}")] + ShortBlock { + index: u32, + found: usize, + expected: usize, + }, } diff --git a/nodedb-raster/src/header.rs b/nodedb-raster/src/header.rs new file mode 100644 index 000000000..269afa844 --- /dev/null +++ b/nodedb-raster/src/header.rs @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! What a raster's header says: grid size, outer dims, bands, georeference. + +use nodedb_crs::{CrsContext, GeoTransform}; +use nodedb_types::geometry::Srid; + +use crate::crs::srid::resolve_crs; +use crate::error::RasterError; +use crate::sample::SampleType; +use crate::tiff::geokeys::GeoKeyDirectory; + +/// A dimension outside the 2D grid, such as time or a band axis of a +/// multi-dimensional format. A TIFF has none. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct OuterDim { + pub name: String, + pub len: u64, +} + +/// One band: its sample type and the value metadata the file declares. +#[derive(Debug, Clone, PartialEq)] +pub struct BandInfo { + /// The band name or description, when the file gives one. + pub name: Option, + pub sample_type: SampleType, + /// The sample value that means "no data". + pub nodata: Option, + /// Physical value = `sample * scale + offset`. + pub scale: Option, + pub offset: Option, + /// Inclusive valid sample range `(min, max)`. + pub valid_range: Option<(f64, f64)>, + /// The unit of the physical value. + pub units: Option, +} + +impl BandInfo { + /// A band of `sample_type` with no metadata. + pub const fn plain(sample_type: SampleType) -> Self { + Self { + name: None, + sample_type, + nodata: None, + scale: None, + offset: None, + valid_range: None, + units: None, + } + } +} + +/// A user-defined GeoTIFF CRS (GeoKey value 32767): the directory its +/// parameters decode from. +#[derive(Debug, Clone, PartialEq)] +pub struct UserDefinedCrs { + pub keys: GeoKeyDirectory, + /// True for a projected CRS, false for a geographic one. + pub projected: bool, +} + +/// The CRS a raster names, before it resolves against a CRS context. +#[derive(Debug, Clone, PartialEq)] +pub enum CrsRef { + /// The file names no CRS. + None, + /// An EPSG code. + Epsg(u32), + /// A CRS defined by parameters. + UserDefined(Box), +} + +impl CrsRef { + /// The SRID this CRS resolves to in `ctx`, or `None` when the file + /// names no CRS. + pub fn resolve(&self, ctx: &CrsContext<'_>) -> Result, RasterError> { + resolve_crs(self, ctx) + } +} + +/// The header of one raster image. +#[derive(Debug, Clone, PartialEq)] +pub struct RasterHeader { + pub width: u32, + pub height: u32, + /// Dims outside the 2D grid, outermost first. + pub outer: Vec, + pub bands: Vec, + /// The GDAL-order pixel-corner geotransform. `None` when the file has + /// none. + pub transform: Option, + pub crs: CrsRef, +} + +impl RasterHeader { + /// The band count. + pub fn band_count(&self) -> usize { + self.bands.len() + } +} diff --git a/nodedb-raster/src/layout.rs b/nodedb-raster/src/layout.rs index 7db3bf692..3848b279b 100644 --- a/nodedb-raster/src/layout.rs +++ b/nodedb-raster/src/layout.rs @@ -1,22 +1,11 @@ // SPDX-License-Identifier: Apache-2.0 -//! The block grid of IFD 0: image size, band layout, and strip or tile -//! geometry, read once at open. - -use std::io::{Read, Seek}; - -use tiff::decoder::{ChunkType, Decoder}; -use tiff::tags::{SampleFormat, Tag}; +//! The block grid of one image: size, band layout, and strip or tile +//! geometry. use crate::error::RasterError; use crate::sample::SampleType; -/// `PlanarConfiguration` value of band-separate storage. -const PLANAR_SEPARATE: u16 = 2; -/// Photometric interpretations whose samples decode unchanged: -/// BlackIsZero, RGB, CMYK, YCbCr. -const RAW_PHOTOMETRICS: [u16; 4] = [1, 2, 5, 6]; - /// How a raster stores its blocks. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum BlockKind { @@ -37,8 +26,11 @@ pub enum Interleave { /// The pixel rectangle one block covers. Edge blocks are cropped to the /// image. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct BlockWindow { + /// The block's index along each outer dim, outermost first. Empty for + /// a 2D raster. + pub outer: Vec, pub col_off: u32, pub row_off: u32, pub width: u32, @@ -70,80 +62,18 @@ pub struct RasterLayout { } impl RasterLayout { - /// Read the layout of the decoder's current image. - pub(crate) fn read( - decoder: &mut Decoder, - limit_bytes: usize, - ) -> Result { - let tiff = |e| RasterError::from_tiff(e, limit_bytes); - let photometric = decoder - .find_tag_unsigned::(Tag::PhotometricInterpretation) - .map_err(tiff)? - .unwrap_or(1); - if !RAW_PHOTOMETRICS.contains(&photometric) { - return Err(RasterError::UnsupportedPhotometric { code: photometric }); - } - let (width, height) = decoder.dimensions().map_err(tiff)?; - let color = decoder.colortype().map_err(tiff)?; - let bands = decoder - .find_tag_unsigned::(Tag::SamplesPerPixel) - .map_err(tiff)? - .unwrap_or(1); - let format = decoder - .find_tag_unsigned_vec::(Tag::SampleFormat) - .map_err(tiff)? - .and_then(|formats| formats.first().copied()) - .map_or(SampleFormat::Uint, SampleFormat::from_u16_exhaustive); - let sample_type = SampleType::from_tiff(format, color.bit_depth())?; - let interleave = match decoder - .find_tag_unsigned::(Tag::PlanarConfiguration) - .map_err(tiff)? - { - Some(PLANAR_SEPARATE) => Interleave::Planar, - _ => Interleave::Chunky, - }; - // A chunky pixel decodes to the color type's samples. Fewer than the - // file stores drops bands. - if interleave == Interleave::Chunky && color.num_samples() != bands { - return Err(RasterError::UnsupportedBandLayout { - samples: bands, - decoded: color.num_samples(), - }); - } - // The tiff decoder refuses a zero image size, tile size, and - // RowsPerStrip, so every block dimension here is at least 1. - let (stored_width, stored_height) = decoder.chunk_dimensions(); - let (kind, block_width, block_height) = match decoder.get_chunk_type() { - ChunkType::Strip => (BlockKind::Strips, width, stored_height.min(height)), - ChunkType::Tile => (BlockKind::Tiles, stored_width, stored_height), - }; - Ok(Self { - width, - height, - bands, - sample_type, - kind, - interleave, - block_width, - block_height, - blocks_across: width.div_ceil(block_width), - blocks_down: height.div_ceil(block_height), - }) - } - /// Blocks per band plane. pub fn block_count(&self) -> u32 { - // The tiff decoder checked that the chunk offsets, a u32-countable - // list, hold this many entries per plane. + // The TIFF layer checks at open that this product fits a u32. self.blocks_across.saturating_mul(self.blocks_down) } /// Decoded bytes of the largest block, across all bands. pub fn block_bytes(&self) -> u64 { u64::from(self.block_width) - * u64::from(self.block_height) - * u64::from(self.bands) - * self.sample_type.byte_len() as u64 + .saturating_mul(u64::from(self.block_height)) + .saturating_mul(u64::from(self.bands)) + .saturating_mul(self.sample_type.byte_len() as u64) } /// The window of block `index`, cropped to the image. @@ -155,6 +85,7 @@ impl RasterLayout { let col_off = (index % self.blocks_across) * self.block_width; let row_off = (index / self.blocks_across) * self.block_height; Ok(BlockWindow { + outer: Vec::new(), col_off, row_off, width: self.block_width.min(self.width - col_off), @@ -190,6 +121,7 @@ mod tests { assert_eq!( last, BlockWindow { + outer: Vec::new(), col_off: 32, row_off: 16, width: 8, diff --git a/nodedb-raster/src/lib.rs b/nodedb-raster/src/lib.rs index 04a966306..66275d128 100644 --- a/nodedb-raster/src/lib.rs +++ b/nodedb-raster/src/lib.rs @@ -1,31 +1,47 @@ // SPDX-License-Identifier: Apache-2.0 -//! Bounded GeoTIFF decoding for NodeDB array bulk loads. +//! Sans-IO raster decoding for NodeDB array bulk loads. //! -//! [`GeoTiffReader`] opens IFD 0 of a GeoTIFF and decodes it one strip or -//! tile at a time into typed per-band samples, under a caller-given byte -//! cap. It reads the georeference (geotransform, CRS, `GDAL_NODATA`) from -//! the GeoTIFF tags. +//! A [`RasterOpener`] reads a raster header from a [`RangeCache`] and +//! names the byte ranges it needs next, so any transport (a local file, +//! HTTP range requests, an object store) can feed it. The resulting +//! [`RasterReader`] names each block's ranges and decodes the block from +//! the fetched bytes, under the caller's [`RasterLimits`]. The synchronous +//! driver in [`reader::driver`] answers both from a blocking source. +//! +//! [`TiffOpener`] reads classic TIFF and BigTIFF, cloud-optimised or not: +//! strips or tiles, chunky or planar bands, overviews, internal masks, +//! GeoKeys, `GDAL_NODATA` and `GDAL_METADATA`. Its codecs are pure Rust: +//! none, LZW, deflate, PackBits, ZSTD, JPEG and LERC, with the horizontal +//! and floating-point predictors. pub mod block; +pub mod codec; pub mod crs; pub mod error; -pub mod geokeys; +pub mod header; pub mod layout; pub mod limits; pub mod nodata; -mod preflight; pub mod reader; pub mod sample; -pub mod transform; +pub mod source; +pub mod tiff; pub use block::{BandSamples, RasterBlock}; pub use crs::srid_from_geokeys; pub use error::RasterError; -pub use geokeys::{GeoKeyDirectory, GeoKeyError, GeoKeyValue}; +pub use header::{BandInfo, CrsRef, OuterDim, RasterHeader, UserDefinedCrs}; pub use layout::{BlockKind, BlockWindow, Interleave, RasterLayout}; -pub use limits::RasterLimits; +pub use limits::{CapacityExceeded, DEFAULT_HEADER_BYTES, MAX_DECODE_CAPACITY, RasterLimits}; pub use nodata::parse_gdal_nodata; -pub use reader::GeoTiffReader; +pub use nodedb_crs::GeoTransform; +pub use reader::{ + Opened, RangeRead, RasterOpener, RasterReader, SliceSource, open_all, open_with, + read_block_all, read_block_with, +}; pub use sample::{SampleFormatCode, SampleType}; -pub use transform::{GeoTransform, ModelTags}; +pub use source::{ByteRange, COALESCE_GAP, Lookup, ObjectId, RangeCache, coalesce}; +pub use tiff::geokeys::{GeoKeyDirectory, GeoKeyError, GeoKeyValue}; +pub use tiff::transform::{ModelTags, transform_from_tags}; +pub use tiff::{TiffOpener, TiffReader}; diff --git a/nodedb-raster/src/limits.rs b/nodedb-raster/src/limits.rs index 0d5c5856d..282b87906 100644 --- a/nodedb-raster/src/limits.rs +++ b/nodedb-raster/src/limits.rs @@ -1,35 +1,65 @@ // SPDX-License-Identifier: Apache-2.0 -//! Memory caps the reader decodes under. +//! Memory caps the readers decode under. -use tiff::decoder::Limits; - -/// Largest out-of-line IFD 0 tag value: strip offsets, GeoKeys, GDAL_NODATA. +/// Largest single tag value other than the block index: GeoKeys, +/// `GDAL_METADATA`, `JPEGTables`, per-sample arrays. pub(crate) const MAX_TAG_VALUE_BYTES: usize = 1024 * 1024; -/// Caps on what one decode allocates. +/// Default cap on the header bytes an open caches: IFDs, tag values and +/// the block offset and byte count arrays. +pub const DEFAULT_HEADER_BYTES: usize = 32 * 1024 * 1024; + +/// Most elements one decode allocation reserves, whatever limits the +/// caller passes. A larger request comes from a hostile header or cap. +pub const MAX_DECODE_CAPACITY: usize = 1 << 30; + +/// A decode allocation over [`MAX_DECODE_CAPACITY`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] +#[error("a decode allocation of {requested} elements is over the {limit}-element cap")] +pub struct CapacityExceeded { + pub requested: usize, + pub limit: usize, +} + +/// `len` as the capacity of one decode allocation, refused over +/// [`MAX_DECODE_CAPACITY`]. +pub(crate) fn checked_raster_capacity(len: usize) -> Result { + if len > MAX_DECODE_CAPACITY { + return Err(CapacityExceeded { + requested: len, + limit: MAX_DECODE_CAPACITY, + }); + } + Ok(len) +} + +/// Caps on what one open or one block decode allocates. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct RasterLimits { /// Most bytes one block decodes to, across all its bands. The same cap - /// bounds one compressed chunk read from the file. + /// bounds one compressed chunk read from the source. pub block_bytes: usize, + /// Most bytes an open caches from the source. + pub header_bytes: usize, } impl RasterLimits { - /// Limits with `block_bytes` as the per-block decoded byte cap. + /// Limits with `block_bytes` as the per-block cap and the default + /// header cap. pub const fn new(block_bytes: usize) -> Self { - Self { block_bytes } + Self { + block_bytes, + header_bytes: DEFAULT_HEADER_BYTES, + } } - /// The tiff decoder limits these caps imply. - pub(crate) fn tiff_limits(self) -> Limits { - // `Limits` is non-exhaustive: start from a constructor, then set - // every field this tiff version has. - let mut limits = Limits::unlimited(); - limits.decoding_buffer_size = self.block_bytes; - limits.intermediate_buffer_size = self.block_bytes; - limits.ifd_value_size = MAX_TAG_VALUE_BYTES; - limits + /// These limits with `header_bytes` as the header cap. + pub const fn with_header_bytes(self, header_bytes: usize) -> Self { + Self { + block_bytes: self.block_bytes, + header_bytes, + } } } @@ -38,10 +68,25 @@ mod tests { use super::*; #[test] - fn block_cap_bounds_decode_and_compressed_buffers() { - let limits = RasterLimits::new(4096).tiff_limits(); - assert_eq!(limits.decoding_buffer_size, 4096); - assert_eq!(limits.intermediate_buffer_size, 4096); - assert_eq!(limits.ifd_value_size, MAX_TAG_VALUE_BYTES); + fn new_keeps_the_block_cap_and_the_default_header_cap() { + let limits = RasterLimits::new(4096); + assert_eq!(limits.block_bytes, 4096); + assert_eq!(limits.header_bytes, DEFAULT_HEADER_BYTES); + assert_eq!(limits.with_header_bytes(10).header_bytes, 10); + } + + #[test] + fn capacity_over_the_ceiling_is_refused() { + assert_eq!( + checked_raster_capacity(MAX_DECODE_CAPACITY), + Ok(MAX_DECODE_CAPACITY) + ); + assert_eq!( + checked_raster_capacity(MAX_DECODE_CAPACITY + 1), + Err(CapacityExceeded { + requested: MAX_DECODE_CAPACITY + 1, + limit: MAX_DECODE_CAPACITY, + }) + ); } } diff --git a/nodedb-raster/src/preflight.rs b/nodedb-raster/src/preflight.rs deleted file mode 100644 index dbdedd83f..000000000 --- a/nodedb-raster/src/preflight.rs +++ /dev/null @@ -1,288 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -//! A bounds pass over the IFD 0 entry table before the tiff decoder reads -//! it. -//! -//! The tiff decoder reads IFD 0 under its default limits, before a caller -//! can set its own. It reserves a tag's value list from the entry's count. -//! This pass refuses any out-of-line value that is larger than -//! [`MAX_TAG_VALUE_BYTES`] or ends past the end of the file. So no tag -//! value allocation exceeds the file's own size. -//! -//! A header or entry table the pass cannot read is left to the tiff -//! decoder, which refuses it without a large allocation. - -use std::io::{self, Read, Seek, SeekFrom}; - -use crate::error::RasterError; -use crate::limits::MAX_TAG_VALUE_BYTES; - -const CLASSIC_MAGIC: u16 = 42; -const BIG_MAGIC: u16 = 43; - -#[derive(Clone, Copy)] -enum Order { - Little, - Big, -} - -/// The fixed-width entry layout of classic TIFF or BigTIFF. -#[derive(Clone, Copy)] -struct Format { - order: Order, - big: bool, -} - -impl Format { - fn entry_len(self) -> u64 { - if self.big { 20 } else { 12 } - } - - fn inline_len(self) -> u64 { - if self.big { 8 } else { 4 } - } -} - -/// Check every out-of-line IFD 0 value of `reader`, then rewind it. -pub(crate) fn check_ifd0(reader: &mut R) -> Result<(), RasterError> { - let file_len = reader.seek(SeekFrom::End(0))?; - reader.seek(SeekFrom::Start(0))?; - let checked = check_entries(reader, file_len); - reader.seek(SeekFrom::Start(0))?; - match checked { - // A short read means the tiff decoder refuses the same bytes. - Err(Short::Eof) | Ok(()) => Ok(()), - Err(Short::Refused(error)) => Err(error), - Err(Short::Io(error)) => Err(RasterError::from(error)), - } -} - -/// Why the entry walk stopped early. -enum Short { - Eof, - Io(io::Error), - Refused(RasterError), -} - -impl From for Short { - fn from(error: io::Error) -> Self { - if error.kind() == io::ErrorKind::UnexpectedEof { - Short::Eof - } else { - Short::Io(error) - } - } -} - -fn check_entries(reader: &mut R, file_len: u64) -> Result<(), Short> { - let mut header = [0u8; 8]; - reader.read_exact(&mut header)?; - let order = match &header[..2] { - b"II" => Order::Little, - b"MM" => Order::Big, - _ => return Ok(()), - }; - let magic = u16_at(order, [header[2], header[3]]); - let (format, ifd_offset) = match magic { - CLASSIC_MAGIC => { - let offset = u32_at(order, [header[4], header[5], header[6], header[7]]); - (Format { order, big: false }, u64::from(offset)) - } - BIG_MAGIC => { - let mut offset = [0u8; 8]; - reader.read_exact(&mut offset)?; - (Format { order, big: true }, u64_at(order, offset)) - } - _ => return Ok(()), - }; - if ifd_offset >= file_len { - return Ok(()); - } - reader.seek(SeekFrom::Start(ifd_offset))?; - let count = if format.big { - let mut raw = [0u8; 8]; - reader.read_exact(&mut raw)?; - u64_at(order, raw) - } else { - let mut raw = [0u8; 2]; - reader.read_exact(&mut raw)?; - u64::from(u16_at(order, raw)) - }; - // Entries past the end of the file are a short read for the decoder. - let room = file_len.saturating_sub(ifd_offset) / format.entry_len(); - for _ in 0..count.min(room) { - check_entry(reader, format, file_len)?; - } - Ok(()) -} - -fn check_entry(reader: &mut R, format: Format, file_len: u64) -> Result<(), Short> { - let order = format.order; - let mut head = [0u8; 4]; - reader.read_exact(&mut head)?; - let tag = u16_at(order, [head[0], head[1]]); - let kind = u16_at(order, [head[2], head[3]]); - let (count, offset) = if format.big { - let mut rest = [0u8; 16]; - reader.read_exact(&mut rest)?; - let mut count = [0u8; 8]; - let mut value = [0u8; 8]; - count.copy_from_slice(&rest[..8]); - value.copy_from_slice(&rest[8..]); - (u64_at(order, count), u64_at(order, value)) - } else { - let mut rest = [0u8; 8]; - reader.read_exact(&mut rest)?; - let count = u32_at(order, [rest[0], rest[1], rest[2], rest[3]]); - let value = u32_at(order, [rest[4], rest[5], rest[6], rest[7]]); - (u64::from(count), u64::from(value)) - }; - // The decoder skips an entry of unknown type, so this pass does too. - let Some(width) = type_width(kind) else { - return Ok(()); - }; - let refuse = |bytes| { - Short::Refused(RasterError::TagValueOutOfBounds { - tag, - bytes, - file_len, - }) - }; - let bytes = count.checked_mul(width).ok_or(refuse(u64::MAX))?; - if bytes <= format.inline_len() { - return Ok(()); - } - let end = offset.checked_add(bytes).ok_or(refuse(bytes))?; - if bytes > MAX_TAG_VALUE_BYTES as u64 || end > file_len { - return Err(refuse(bytes)); - } - Ok(()) -} - -/// Bytes per value of TIFF field type `kind`, or `None` for an unknown -/// type. -fn type_width(kind: u16) -> Option { - match kind { - // BYTE, ASCII, SBYTE, UNDEFINED. - 1 | 2 | 6 | 7 => Some(1), - // SHORT, SSHORT. - 3 | 8 => Some(2), - // LONG, SLONG, FLOAT, IFD. - 4 | 9 | 11 | 13 => Some(4), - // RATIONAL, SRATIONAL, DOUBLE, LONG8, SLONG8, IFD8. - 5 | 10 | 12 | 16 | 17 | 18 => Some(8), - _ => None, - } -} - -fn u16_at(order: Order, raw: [u8; 2]) -> u16 { - match order { - Order::Little => u16::from_le_bytes(raw), - Order::Big => u16::from_be_bytes(raw), - } -} - -fn u32_at(order: Order, raw: [u8; 4]) -> u32 { - match order { - Order::Little => u32::from_le_bytes(raw), - Order::Big => u32::from_be_bytes(raw), - } -} - -fn u64_at(order: Order, raw: [u8; 8]) -> u64 { - match order { - Order::Little => u64::from_le_bytes(raw), - Order::Big => u64::from_be_bytes(raw), - } -} - -#[cfg(test)] -mod tests { - use std::io::Cursor; - - use super::*; - - /// A little-endian classic TIFF: header, then one IFD of `entries`. - fn classic(entries: &[(u16, u16, u32, u32)], padding: usize) -> Vec { - let mut out = b"II".to_vec(); - out.extend(42u16.to_le_bytes()); - out.extend(8u32.to_le_bytes()); - out.extend(u16::try_from(entries.len()).expect("few").to_le_bytes()); - for (tag, kind, count, value) in entries { - out.extend(tag.to_le_bytes()); - out.extend(kind.to_le_bytes()); - out.extend(count.to_le_bytes()); - out.extend(value.to_le_bytes()); - } - out.extend(0u32.to_le_bytes()); - out.resize(out.len() + padding, 0); - out - } - - fn check(bytes: Vec) -> Result<(), RasterError> { - let mut cursor = Cursor::new(bytes); - let result = check_ifd0(&mut cursor); - assert_eq!(cursor.position(), 0, "the pass rewinds"); - result - } - - #[test] - fn values_inside_the_file_pass() { - // ImageWidth inline, StripOffsets: 4 LONGs at offset 26. - let bytes = classic(&[(256, 3, 1, 64), (273, 4, 4, 26)], 16); - assert!(check(bytes).is_ok()); - } - - #[test] - fn a_value_past_the_end_is_refused() { - let bytes = classic(&[(273, 4, 1_000_000, 26)], 16); - assert!(matches!( - check(bytes), - Err(RasterError::TagValueOutOfBounds { - tag: 273, - bytes: 4_000_000, - .. - }) - )); - } - - #[test] - fn a_value_over_the_cap_is_refused_even_inside_the_file() { - let count = u32::try_from(MAX_TAG_VALUE_BYTES + 1).expect("fits"); - let bytes = classic(&[(270, 2, count, 26)], MAX_TAG_VALUE_BYTES + 64); - assert!(matches!( - check(bytes), - Err(RasterError::TagValueOutOfBounds { tag: 270, .. }) - )); - } - - #[test] - fn unknown_types_and_short_tables_are_left_to_the_decoder() { - let unknown = classic(&[(273, 99, u32::MAX, 0)], 0); - assert!(check(unknown).is_ok()); - let mut short = classic(&[(273, 4, 4, 26)], 0); - short.truncate(12); - assert!(check(short).is_ok()); - assert!(check(b"not a tiff".to_vec()).is_ok()); - assert!(check(Vec::new()).is_ok()); - } - - #[test] - fn bigtiff_entries_are_checked() { - let mut out = b"II".to_vec(); - out.extend(43u16.to_le_bytes()); - out.extend(8u16.to_le_bytes()); - out.extend(0u16.to_le_bytes()); - out.extend(16u64.to_le_bytes()); - out.extend(1u64.to_le_bytes()); - out.extend(273u16.to_le_bytes()); - out.extend(16u16.to_le_bytes()); - out.extend(u64::MAX.to_le_bytes()); - out.extend(0u64.to_le_bytes()); - out.extend(0u64.to_le_bytes()); - assert!(matches!( - check(out), - Err(RasterError::TagValueOutOfBounds { tag: 273, .. }) - )); - } -} diff --git a/nodedb-raster/src/reader.rs b/nodedb-raster/src/reader.rs deleted file mode 100644 index 93ba1877c..000000000 --- a/nodedb-raster/src/reader.rs +++ /dev/null @@ -1,267 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -//! `GeoTiffReader`: IFD 0 of a GeoTIFF, decoded one block at a time. -//! -//! Open bounds every IFD 0 tag value by the file size first. It then reads -//! the tags once and refuses what the reader cannot decode exactly: an -//! overview as IFD 0, an unsupported codec, sample type, or photometric -//! interpretation, and a block larger than the caller's cap. A block read -//! then allocates at most that cap. - -use std::io::{Read, Seek}; -use std::mem::size_of; - -use nodedb_types::decode_bounds::checked_decode_capacity; -use nodedb_types::geometry::Srid; -use tiff::decoder::{Decoder, DecodingResult}; -use tiff::tags::Tag; - -use crate::block::{BandSamples, ChunkShape, RasterBlock, chunky_bands, planar_band}; -use crate::crs::srid_from_geokeys; -use crate::error::RasterError; -use crate::geokeys::{GT_RASTER_TYPE, GeoKeyDirectory}; -use crate::layout::{Interleave, RasterLayout}; -use crate::limits::RasterLimits; -use crate::nodata::parse_gdal_nodata; -use crate::preflight::check_ifd0; -use crate::sample::SampleType; -use crate::transform::{GeoTransform, ModelTags}; - -/// Compression codes the decoder reads: none, LZW, deflate (both codes), -/// PackBits. -const SUPPORTED_COMPRESSION: [u16; 5] = [1, 5, 8, 32946, 32773]; -/// `NewSubfileType` bit of a reduced-resolution image. -const REDUCED_RESOLUTION_BIT: u32 = 1; -/// `SubfileType` value of a reduced-resolution image. -const SUBFILE_REDUCED_RESOLUTION: u32 = 2; - -/// A GeoTIFF opened at IFD 0. -pub struct GeoTiffReader { - decoder: Decoder, - layout: RasterLayout, - limits: RasterLimits, - transform: Option, - geokeys: Option, - nodata: Option, -} - -impl GeoTiffReader { - /// Open `reader` and read the tags of IFD 0. - pub fn open(mut reader: R, limits: RasterLimits) -> Result { - let cap = limits.block_bytes; - let tiff = |e| RasterError::from_tiff(e, cap); - check_ifd0(&mut reader)?; - let mut decoder = Decoder::new(reader) - .map_err(tiff)? - .with_limits(limits.tiff_limits()); - refuse_reduced_resolution(&mut decoder, cap)?; - let compression = decoder - .find_tag_unsigned::(Tag::Compression) - .map_err(tiff)? - .unwrap_or(1); - if !SUPPORTED_COMPRESSION.contains(&compression) { - return Err(RasterError::UnsupportedCompression { code: compression }); - } - let layout = RasterLayout::read(&mut decoder, cap)?; - let needed_bytes = layout.block_bytes(); - if needed_bytes > cap as u64 { - return Err(RasterError::BlockTooLarge { - needed_bytes, - limit_bytes: cap, - }); - } - let geokeys = read_geokeys(&mut decoder, cap)?; - let pixel_scale = f64_tag(&mut decoder, Tag::ModelPixelScaleTag, cap)?; - let tiepoints = f64_tag(&mut decoder, Tag::ModelTiepointTag, cap)?; - let transformation = f64_tag(&mut decoder, Tag::ModelTransformationTag, cap)?; - let transform = GeoTransform::from_tags(ModelTags { - pixel_scale: pixel_scale.as_deref(), - tiepoints: tiepoints.as_deref(), - transformation: transformation.as_deref(), - raster_type: geokeys.as_ref().and_then(|keys| keys.short(GT_RASTER_TYPE)), - })?; - let nodata = decoder - .find_tag(Tag::GdalNodata) - .map_err(tiff)? - .map(|value| value.into_string().map_err(tiff)) - .transpose()? - .map(|text| parse_gdal_nodata(&text)) - .transpose()?; - Ok(Self { - decoder, - layout, - limits, - transform, - geokeys, - nodata, - }) - } - - /// The block grid and band layout. - pub fn layout(&self) -> &RasterLayout { - &self.layout - } - - /// Image width in pixels. - pub fn width(&self) -> u32 { - self.layout.width - } - - /// Image height in pixels. - pub fn height(&self) -> u32 { - self.layout.height - } - - /// Number of bands. - pub fn band_count(&self) -> u16 { - self.layout.bands - } - - /// The sample type every band shares. - pub fn sample_type(&self) -> SampleType { - self.layout.sample_type - } - - /// Blocks per band plane. Block indexes run `0..block_count()`. - pub fn block_count(&self) -> u32 { - self.layout.block_count() - } - - /// The pixel-corner geotransform, PixelIsPoint already shifted. - pub fn transform(&self) -> Option { - self.transform - } - - /// The GeoKey directory, when the file has one. - pub fn geokeys(&self) -> Option<&GeoKeyDirectory> { - self.geokeys.as_ref() - } - - /// The SRID the GeoKeys name, or `None` when they name no CRS. - pub fn srid(&self) -> Result, RasterError> { - srid_from_geokeys(self.geokeys.as_ref()) - } - - /// The `GDAL_NODATA` value, shared by every band. - pub fn nodata(&self) -> Option { - self.nodata - } - - /// Decode block `index`: one sample vector per band, cropped to the - /// image. - pub fn read_block(&mut self, index: u32) -> Result { - let window = self.layout.window(index)?; - let shape = ChunkShape { - index, - pixels: window.pixels(), - bands: usize::from(self.layout.bands), - limit_bytes: self.limits.block_bytes, - }; - let bands = match self.layout.interleave { - Interleave::Chunky => chunky_bands(self.read_chunk(index)?, shape)?, - Interleave::Planar => self.read_planes(index, shape)?, - }; - Ok(RasterBlock { - index, - window, - bands, - }) - } - - /// Read block `index` of every band plane. Plane `b` holds its blocks - /// at chunk indexes `b * block_count() ..`. - fn read_planes( - &mut self, - index: u32, - shape: ChunkShape, - ) -> Result, RasterError> { - let count = self.layout.block_count(); - let capacity = checked_decode_capacity( - shape.bands, - size_of::(), - shape.bands, - 1, - usize::from(u16::MAX), - shape.limit_bytes, - ) - .ok_or(RasterError::BlockTooLarge { - needed_bytes: self.layout.block_bytes(), - limit_bytes: shape.limit_bytes, - })?; - let mut bands = Vec::with_capacity(capacity); - for band in 0..u32::from(self.layout.bands) { - let chunk = band - .checked_mul(count) - .and_then(|start| start.checked_add(index)) - .ok_or(RasterError::BlockOutOfRange { index, count })?; - bands.push(planar_band(self.read_chunk(chunk)?, shape)?); - } - Ok(bands) - } - - fn read_chunk(&mut self, chunk: u32) -> Result { - let cap = self.limits.block_bytes; - self.decoder - .read_chunk(chunk) - .map_err(|e| RasterError::from_tiff(e, cap)) - } -} - -/// Refuse an IFD 0 flagged as an overview by `NewSubfileType` or the -/// older `SubfileType`. -fn refuse_reduced_resolution( - decoder: &mut Decoder, - cap: usize, -) -> Result<(), RasterError> { - let tiff = |e| RasterError::from_tiff(e, cap); - let new_subfile = decoder - .find_tag_unsigned::(Tag::NewSubfileType) - .map_err(tiff)? - .unwrap_or(0); - let subfile = decoder - .find_tag_unsigned::(Tag::SubfileType) - .map_err(tiff)? - .unwrap_or(0); - if new_subfile & REDUCED_RESOLUTION_BIT != 0 || subfile == SUBFILE_REDUCED_RESOLUTION { - return Err(RasterError::ReducedResolutionFirstImage); - } - Ok(()) -} - -/// The GeoKey directory with its parameter arrays, when the file has one. -fn read_geokeys( - decoder: &mut Decoder, - cap: usize, -) -> Result, RasterError> { - let tiff = |e| RasterError::from_tiff(e, cap); - let Some(directory) = decoder - .find_tag(Tag::GeoKeyDirectoryTag) - .map_err(tiff)? - .map(|value| value.into_u16_vec().map_err(tiff)) - .transpose()? - else { - return Ok(None); - }; - let doubles = f64_tag(decoder, Tag::GeoDoubleParamsTag, cap)?; - let ascii = decoder - .find_tag(Tag::GeoAsciiParamsTag) - .map_err(tiff)? - .map(|value| value.into_string().map_err(tiff)) - .transpose()?; - let keys = GeoKeyDirectory::parse(&directory, doubles.as_deref(), ascii.as_deref())?; - Ok(Some(keys)) -} - -/// The doubles of `tag`, when present. -fn f64_tag( - decoder: &mut Decoder, - tag: Tag, - cap: usize, -) -> Result>, RasterError> { - let tiff = |e| RasterError::from_tiff(e, cap); - decoder - .find_tag(tag) - .map_err(tiff)? - .map(|value| value.into_f64_vec().map_err(tiff)) - .transpose() -} diff --git a/nodedb-raster/src/reader/driver.rs b/nodedb-raster/src/reader/driver.rs new file mode 100644 index 000000000..890f29c48 --- /dev/null +++ b/nodedb-raster/src/reader/driver.rs @@ -0,0 +1,149 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! A synchronous driver: answer an opener's rounds and a reader's block +//! ranges from a blocking range source. + +use crate::block::RasterBlock; +use crate::error::RasterError; +use crate::limits::RasterLimits; +use crate::reader::traits::{Opened, RasterOpener, RasterReader}; +use crate::source::cache::{Lookup, RangeCache}; +use crate::source::range::{ByteRange, ObjectId}; + +/// Open rounds before the driver gives up. A COG opens in one or two. +const MAX_OPEN_ROUNDS: usize = 64; + +/// A blocking range source. +pub trait RangeRead { + /// The bytes of `range`, fewer where the object ends, or `None` when + /// the object does not exist. The readers bound every range they name, + /// so an implementation can allocate `range.len()` bytes. + fn read_range(&mut self, range: ByteRange) -> Result>, RasterError>; +} + +/// An in-memory source: one object, the whole slice. +#[derive(Debug, Clone, Copy)] +pub struct SliceSource<'a> { + object: ObjectId, + bytes: &'a [u8], +} + +impl<'a> SliceSource<'a> { + /// `bytes` as `object`. + pub const fn new(object: ObjectId, bytes: &'a [u8]) -> Self { + Self { object, bytes } + } +} + +impl RangeRead for SliceSource<'_> { + fn read_range(&mut self, range: ByteRange) -> Result>, RasterError> { + if range.object != self.object { + return Ok(None); + } + let len = self.bytes.len(); + let start = usize::try_from(range.start).map_or(len, |s| s.min(len)); + let end = usize::try_from(range.end).map_or(len, |e| e.min(len)); + Ok(Some( + self.bytes + .get(start..end.max(start)) + .unwrap_or_default() + .to_vec(), + )) + } +} + +/// Run `opener` to completion, fetching each round's ranges from `source`. +pub fn open_with( + opener: &O, + source: &mut S, + limits: RasterLimits, +) -> Result +where + O: RasterOpener, + S: RangeRead + ?Sized, +{ + let mut cache = RangeCache::new(limits.header_bytes); + for _ in 0..MAX_OPEN_ROUNDS { + match opener.open(&cache, limits)? { + Opened::Ready(reader) => return Ok(reader), + Opened::Need(ranges) => { + let mut fetched = false; + for range in ranges { + if matches!(cache.get(range), Lookup::Missing) { + let bytes = source.read_range(range)?; + cache.insert(range, bytes)?; + fetched = true; + } + } + if !fetched { + return Err(RasterError::OpenStalled { + rounds: MAX_OPEN_ROUNDS, + }); + } + } + } + } + Err(RasterError::OpenStalled { + rounds: MAX_OPEN_ROUNDS, + }) +} + +/// Fetch the ranges of block `index` from `source` and decode it. +pub fn read_block_with( + reader: &R, + index: u32, + source: &mut S, +) -> Result +where + R: RasterReader + ?Sized, + S: RangeRead + ?Sized, +{ + let ranges = reader.block_ranges(index)?; + let mut owned: Vec>> = Vec::with_capacity(ranges.len()); + for range in &ranges { + owned.push(if range.is_empty() { + Some(Vec::new()) + } else { + source.read_range(*range)? + }); + } + let borrowed: Vec> = owned.iter().map(Option::as_deref).collect(); + reader.decode_block(index, &borrowed) +} + +/// Open `bytes`, a whole single-object source, with `opener`. +pub fn open_all( + opener: &O, + bytes: &[u8], + limits: RasterLimits, +) -> Result { + open_with(opener, &mut SliceSource::new(ObjectId(0), bytes), limits) +} + +/// Decode block `index` of a reader opened over `bytes`. +pub fn read_block_all( + reader: &R, + index: u32, + bytes: &[u8], +) -> Result { + read_block_with(reader, index, &mut SliceSource::new(ObjectId(0), bytes)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn a_slice_source_clips_at_the_end_and_knows_one_object() { + let mut source = SliceSource::new(ObjectId(0), &[1, 2, 3, 4]); + let read = |source: &mut SliceSource<'_>, object, start, end| { + source + .read_range(ByteRange::new(ObjectId(object), start, end)) + .expect("reads") + }; + assert_eq!(read(&mut source, 0, 1, 3), Some(vec![2, 3])); + assert_eq!(read(&mut source, 0, 2, 100), Some(vec![3, 4])); + assert_eq!(read(&mut source, 0, 9, 12), Some(vec![])); + assert_eq!(read(&mut source, 1, 0, 2), None); + } +} diff --git a/nodedb-raster/src/reader/mod.rs b/nodedb-raster/src/reader/mod.rs new file mode 100644 index 000000000..5d03806e7 --- /dev/null +++ b/nodedb-raster/src/reader/mod.rs @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! The sans-IO reader contract and a synchronous driver over it. + +pub mod driver; +pub mod traits; + +pub use driver::{RangeRead, SliceSource, open_all, open_with, read_block_all, read_block_with}; +pub use traits::{Opened, RasterOpener, RasterReader}; diff --git a/nodedb-raster/src/reader/traits.rs b/nodedb-raster/src/reader/traits.rs new file mode 100644 index 000000000..5e2bad4f1 --- /dev/null +++ b/nodedb-raster/src/reader/traits.rs @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! The sans-IO reader contract. +//! +//! An opener never performs I/O. Each call reads what the cache holds and +//! either returns the reader or names the ranges it needs next. The caller +//! fetches them over any transport, inserts them, and calls again. A +//! reader names each block's ranges, and decodes a block from the bytes +//! the caller fetched. + +use crate::block::RasterBlock; +use crate::error::RasterError; +use crate::header::RasterHeader; +use crate::layout::BlockWindow; +use crate::limits::RasterLimits; +use crate::source::cache::RangeCache; +use crate::source::range::ByteRange; + +/// The result of one open round. +#[derive(Debug)] +pub enum Opened { + /// The header is read. + Ready(R), + /// Fetch these ranges into the cache, then open again. + Need(Vec), +} + +/// Opens a raster from cached header ranges. +pub trait RasterOpener { + type Reader: RasterReader; + + /// One open round over `cache`. + fn open( + &self, + cache: &RangeCache, + limits: RasterLimits, + ) -> Result, RasterError>; +} + +/// Decodes the blocks of an opened raster. +pub trait RasterReader: Send + Sync { + /// The header the open read. + fn header(&self) -> &RasterHeader; + + /// Blocks per band. Block indexes run `0..block_count()`. + fn block_count(&self) -> u32; + + /// The pixel window of block `index`, cropped to the image. + fn block_window(&self, index: u32) -> Result; + + /// The ranges block `index` decodes from. An empty range stands for a + /// block the file omits and needs no fetch. + fn block_ranges(&self, index: u32) -> Result, RasterError>; + + /// Decode block `index` from the bytes of its ranges, in + /// [`RasterReader::block_ranges`] order. `None` marks a missing + /// object. A buffer shorter than its range means the object ends there. + fn decode_block(&self, index: u32, bytes: &[Option<&[u8]>]) + -> Result; +} diff --git a/nodedb-raster/src/sample.rs b/nodedb-raster/src/sample.rs index 5d82b4a68..f88f547d0 100644 --- a/nodedb-raster/src/sample.rs +++ b/nodedb-raster/src/sample.rs @@ -1,13 +1,18 @@ // SPDX-License-Identifier: Apache-2.0 -//! The closed set of sample types the reader decodes. +//! The closed set of sample types the readers decode. use std::fmt; -use tiff::tags::SampleFormat; - use crate::error::RasterError; +/// TIFF `SampleFormat` 1: unsigned integer. +pub const FORMAT_UINT: u16 = 1; +/// TIFF `SampleFormat` 2: two's complement signed integer. +pub const FORMAT_INT: u16 = 2; +/// TIFF `SampleFormat` 3: IEEE floating point. +pub const FORMAT_IEEE_FP: u16 = 3; + /// A TIFF `SampleFormat` tag value, kept for error messages. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct SampleFormatCode(pub u16); @@ -24,30 +29,34 @@ impl fmt::Display for SampleFormatCode { } } -/// One band sample type. Every band of a raster shares it. +/// One band sample type. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum SampleType { U8, + I8, U16, I16, U32, + I32, F32, F64, } impl SampleType { - /// The sample type for a TIFF sample format and bit width. Any other - /// pair fails with [`RasterError::UnsupportedSampleType`]. - pub fn from_tiff(format: SampleFormat, bits: u8) -> Result { + /// The sample type for a TIFF sample format code and bit width. Any + /// other pair fails with [`RasterError::UnsupportedSampleType`]. + pub fn from_tiff(format: u16, bits: u16) -> Result { match (format, bits) { - (SampleFormat::Uint, 8) => Ok(SampleType::U8), - (SampleFormat::Uint, 16) => Ok(SampleType::U16), - (SampleFormat::Int, 16) => Ok(SampleType::I16), - (SampleFormat::Uint, 32) => Ok(SampleType::U32), - (SampleFormat::IEEEFP, 32) => Ok(SampleType::F32), - (SampleFormat::IEEEFP, 64) => Ok(SampleType::F64), + (FORMAT_UINT, 8) => Ok(SampleType::U8), + (FORMAT_INT, 8) => Ok(SampleType::I8), + (FORMAT_UINT, 16) => Ok(SampleType::U16), + (FORMAT_INT, 16) => Ok(SampleType::I16), + (FORMAT_UINT, 32) => Ok(SampleType::U32), + (FORMAT_INT, 32) => Ok(SampleType::I32), + (FORMAT_IEEE_FP, 32) => Ok(SampleType::F32), + (FORMAT_IEEE_FP, 64) => Ok(SampleType::F64), _ => Err(RasterError::UnsupportedSampleType { - format: SampleFormatCode(format.to_u16()), + format: SampleFormatCode(format), bits, }), } @@ -56,20 +65,27 @@ impl SampleType { /// Bytes per sample. pub const fn byte_len(self) -> usize { match self { - SampleType::U8 => 1, + SampleType::U8 | SampleType::I8 => 1, SampleType::U16 | SampleType::I16 => 2, - SampleType::U32 | SampleType::F32 => 4, + SampleType::U32 | SampleType::I32 | SampleType::F32 => 4, SampleType::F64 => 8, } } + /// True for `f32` and `f64`. + pub const fn is_float(self) -> bool { + matches!(self, SampleType::F32 | SampleType::F64) + } + /// The lower-case Rust name, such as `u16`. pub const fn name(self) -> &'static str { match self { SampleType::U8 => "u8", + SampleType::I8 => "i8", SampleType::U16 => "u16", SampleType::I16 => "i16", SampleType::U32 => "u32", + SampleType::I32 => "i32", SampleType::F32 => "f32", SampleType::F64 => "f64", } @@ -88,29 +104,27 @@ mod tests { #[test] fn supported_pairs_map_to_their_type() { - assert_eq!( - SampleType::from_tiff(SampleFormat::Uint, 8).ok(), - Some(SampleType::U8) - ); - assert_eq!( - SampleType::from_tiff(SampleFormat::Int, 16).ok(), - Some(SampleType::I16) - ); - assert_eq!( - SampleType::from_tiff(SampleFormat::IEEEFP, 64).ok(), - Some(SampleType::F64) - ); + let cases = [ + (FORMAT_UINT, 8, SampleType::U8), + (FORMAT_INT, 8, SampleType::I8), + (FORMAT_INT, 16, SampleType::I16), + (FORMAT_INT, 32, SampleType::I32), + (FORMAT_UINT, 32, SampleType::U32), + (FORMAT_IEEE_FP, 64, SampleType::F64), + ]; + for (format, bits, want) in cases { + assert_eq!(SampleType::from_tiff(format, bits).ok(), Some(want)); + } } #[test] fn unsupported_pairs_are_typed_errors() { for (format, bits) in [ - (SampleFormat::Int, 8), - (SampleFormat::Uint, 64), - (SampleFormat::Int, 32), - (SampleFormat::IEEEFP, 16), - (SampleFormat::Uint, 1), - (SampleFormat::Void, 8), + (FORMAT_UINT, 64), + (FORMAT_INT, 64), + (FORMAT_IEEE_FP, 16), + (FORMAT_UINT, 1), + (4, 8), ] { assert!(matches!( SampleType::from_tiff(format, bits), diff --git a/nodedb-raster/src/source/cache.rs b/nodedb-raster/src/source/cache.rs new file mode 100644 index 000000000..6f65e62c1 --- /dev/null +++ b/nodedb-raster/src/source/cache.rs @@ -0,0 +1,171 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! `RangeCache`: the fetched ranges an open reads from, under a byte cap. +//! +//! An entry shorter than its range marks the end of its object: a lookup +//! past that end answers with the bytes up to the end, possibly none. An +//! entry without bytes marks a missing object. + +use crate::error::RasterError; +use crate::source::range::ByteRange; + +/// The answer of a cache lookup. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Lookup<'a> { + /// The bytes of the range. Shorter than the range only where the + /// object ends. + Bytes(&'a [u8]), + /// The object does not exist. + Absent, + /// The cache does not hold the range yet. + Missing, +} + +#[derive(Debug)] +struct Entry { + range: ByteRange, + bytes: Option>, +} + +/// Fetched ranges, bounded by a byte cap. +#[derive(Debug)] +pub struct RangeCache { + entries: Vec, + held_bytes: usize, + limit_bytes: usize, +} + +impl RangeCache { + /// An empty cache that holds at most `limit_bytes` bytes. + pub fn new(limit_bytes: usize) -> Self { + Self { + entries: Vec::new(), + held_bytes: 0, + limit_bytes, + } + } + + /// Store the answer for `range`: its bytes, or `None` for a missing + /// object. Fewer bytes than the range marks the end of the object. + pub fn insert(&mut self, range: ByteRange, bytes: Option>) -> Result<(), RasterError> { + if let Some(data) = &bytes { + if data.len() as u64 > range.len() { + return Err(RasterError::RangeOverrun { + range, + len: range.len(), + got: data.len(), + }); + } + let held = self + .held_bytes + .checked_add(data.len()) + .filter(|held| *held <= self.limit_bytes) + .ok_or(RasterError::HeaderTooLarge { + limit_bytes: self.limit_bytes, + })?; + self.held_bytes = held; + } + self.entries.push(Entry { range, bytes }); + Ok(()) + } + + /// The cached bytes of `range`. + pub fn get(&self, range: ByteRange) -> Lookup<'_> { + for entry in &self.entries { + if entry.range.object != range.object { + continue; + } + let Some(data) = &entry.bytes else { + return Lookup::Absent; + }; + if entry.range.start > range.start { + continue; + } + let held = data.len() as u64; + let at_end = held < entry.range.len(); + let available_end = entry.range.start.saturating_add(held); + let skip = range.start - entry.range.start; + if range.end <= available_end { + let (Ok(from), Ok(to)) = ( + usize::try_from(skip), + usize::try_from(range.end - entry.range.start), + ) else { + continue; + }; + if let Some(bytes) = data.get(from..to.max(from)) { + return Lookup::Bytes(bytes); + } + } else if at_end { + let from = usize::try_from(skip.min(held)).unwrap_or(data.len()); + return Lookup::Bytes(data.get(from..).unwrap_or_default()); + } + } + Lookup::Missing + } + + /// Bytes the cache holds. + pub fn held_bytes(&self) -> usize { + self.held_bytes + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::source::range::ObjectId; + + fn r(start: u64, end: u64) -> ByteRange { + ByteRange::new(ObjectId(0), start, end) + } + + #[test] + fn covered_ranges_hit_and_others_miss() { + let mut cache = RangeCache::new(1024); + cache + .insert(r(10, 20), Some((10..20).collect())) + .expect("fits"); + assert_eq!(cache.get(r(12, 15)), Lookup::Bytes(&[12, 13, 14])); + assert_eq!( + cache.get(r(10, 20)), + Lookup::Bytes(&[10, 11, 12, 13, 14, 15, 16, 17, 18, 19]) + ); + assert_eq!(cache.get(r(5, 12)), Lookup::Missing); + assert_eq!(cache.get(r(15, 25)), Lookup::Missing); + assert_eq!( + cache.get(ByteRange::new(ObjectId(1), 10, 12)), + Lookup::Missing + ); + } + + #[test] + fn a_short_entry_marks_the_object_end() { + let mut cache = RangeCache::new(1024); + cache.insert(r(0, 100), Some(vec![7; 8])).expect("fits"); + assert_eq!(cache.get(r(4, 50)), Lookup::Bytes(&[7; 4])); + assert_eq!(cache.get(r(60, 70)), Lookup::Bytes(&[])); + } + + #[test] + fn a_missing_object_is_absent() { + let mut cache = RangeCache::new(1024); + cache + .insert(ByteRange::new(ObjectId(3), 0, 10), None) + .expect("fits"); + assert_eq!(cache.get(ByteRange::new(ObjectId(3), 4, 8)), Lookup::Absent); + } + + #[test] + fn the_byte_cap_and_overruns_are_typed_errors() { + let mut cache = RangeCache::new(10); + cache.insert(r(0, 8), Some(vec![0; 8])).expect("fits"); + assert!(matches!( + cache.insert(r(8, 16), Some(vec![0; 8])), + Err(RasterError::HeaderTooLarge { limit_bytes: 10 }) + )); + assert!(matches!( + cache.insert(r(0, 2), Some(vec![0; 3])), + Err(RasterError::RangeOverrun { got: 3, .. }) + )); + assert_eq!(cache.held_bytes(), 8); + } +} diff --git a/nodedb-raster/src/source/mod.rs b/nodedb-raster/src/source/mod.rs new file mode 100644 index 000000000..c8bb7fa2b --- /dev/null +++ b/nodedb-raster/src/source/mod.rs @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Byte ranges of source objects and the bounded cache an open reads from. + +pub mod cache; +pub mod range; + +pub use cache::{Lookup, RangeCache}; +pub use range::{ByteRange, COALESCE_GAP, ObjectId, coalesce}; diff --git a/nodedb-raster/src/source/range.rs b/nodedb-raster/src/source/range.rs new file mode 100644 index 000000000..f115695cf --- /dev/null +++ b/nodedb-raster/src/source/range.rs @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Byte ranges of source objects. +//! +//! A source holds one or more objects: one file, or the files of a +//! multi-file format. A reader names the bytes it needs as half-open +//! ranges of an object, and the caller fetches them over any transport. + +/// One object of a source. A single-file source has object 0. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct ObjectId(pub u32); + +/// The half-open byte range `start..end` of `object`. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct ByteRange { + pub object: ObjectId, + pub start: u64, + pub end: u64, +} + +/// Ranges of one object closer than this many bytes merge into one fetch. +pub const COALESCE_GAP: u64 = 64 * 1024; + +impl ByteRange { + /// The range `start..end` of `object`. + pub const fn new(object: ObjectId, start: u64, end: u64) -> Self { + Self { object, start, end } + } + + /// Bytes in the range. An inverted range is empty. + pub const fn len(&self) -> u64 { + self.end.saturating_sub(self.start) + } + + /// True when the range holds no byte. + pub const fn is_empty(&self) -> bool { + self.len() == 0 + } +} + +/// Sort `ranges` and merge those of one object that overlap or lie within +/// `gap` bytes of each other. Empty ranges are dropped. +pub fn coalesce(mut ranges: Vec, gap: u64) -> Vec { + ranges.retain(|range| !range.is_empty()); + ranges.sort_by_key(|range| (range.object, range.start, range.end)); + let mut merged: Vec = Vec::with_capacity(ranges.len()); + for range in ranges { + match merged.last_mut() { + Some(last) + if last.object == range.object && range.start <= last.end.saturating_add(gap) => + { + last.end = last.end.max(range.end); + } + Some(_) | None => merged.push(range), + } + } + merged +} + +#[cfg(test)] +mod tests { + use super::*; + + fn r(object: u32, start: u64, end: u64) -> ByteRange { + ByteRange::new(ObjectId(object), start, end) + } + + #[test] + fn close_ranges_of_one_object_merge() { + let merged = coalesce( + vec![r(0, 200_000, 200_100), r(0, 0, 10), r(0, 100, 50_000)], + COALESCE_GAP, + ); + assert_eq!(merged, vec![r(0, 0, 50_000), r(0, 200_000, 200_100)]); + } + + #[test] + fn objects_never_merge_and_empty_ranges_drop() { + let merged = coalesce(vec![r(1, 0, 4), r(0, 0, 4), r(0, 9, 9)], COALESCE_GAP); + assert_eq!(merged, vec![r(0, 0, 4), r(1, 0, 4)]); + } + + #[test] + fn overlaps_merge_to_the_union() { + let merged = coalesce(vec![r(0, 0, 100), r(0, 50, 80), r(0, 90, 150)], 0); + assert_eq!(merged, vec![r(0, 0, 150)]); + assert_eq!(r(0, 9, 3).len(), 0); + } +} diff --git a/nodedb-raster/src/tiff/byte_order.rs b/nodedb-raster/src/tiff/byte_order.rs new file mode 100644 index 000000000..2ea028d25 --- /dev/null +++ b/nodedb-raster/src/tiff/byte_order.rs @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! TIFF byte order and fixed-width reads. + +/// The byte order a TIFF stores its numbers in. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ByteOrder { + /// `II`. + Little, + /// `MM`. + Big, +} + +impl ByteOrder { + pub const fn u16(self, raw: [u8; 2]) -> u16 { + match self { + ByteOrder::Little => u16::from_le_bytes(raw), + ByteOrder::Big => u16::from_be_bytes(raw), + } + } + + pub const fn u32(self, raw: [u8; 4]) -> u32 { + match self { + ByteOrder::Little => u32::from_le_bytes(raw), + ByteOrder::Big => u32::from_be_bytes(raw), + } + } + + pub const fn u64(self, raw: [u8; 8]) -> u64 { + match self { + ByteOrder::Little => u64::from_le_bytes(raw), + ByteOrder::Big => u64::from_be_bytes(raw), + } + } + + /// `N` bytes of `bytes` at `at`, read as an `N`-byte unsigned integer. + pub fn uint_at(self, bytes: &[u8], at: usize, width: usize) -> Option { + match width { + 1 => bytes.get(at).map(|b| u64::from(*b)), + 2 => array::<2>(bytes, at).map(|raw| u64::from(self.u16(raw))), + 4 => array::<4>(bytes, at).map(|raw| u64::from(self.u32(raw))), + 8 => array::<8>(bytes, at).map(|raw| self.u64(raw)), + _ => None, + } + } + + /// Write the low `width` bytes of `value` at `at` in this order. + pub fn put_uint(self, bytes: &mut [u8], at: usize, width: usize, value: u64) { + let raw = match self { + ByteOrder::Little => value.to_le_bytes(), + ByteOrder::Big => value.to_be_bytes(), + }; + let field = match self { + ByteOrder::Little => raw.get(..width), + ByteOrder::Big => raw.get(8usize.saturating_sub(width)..), + }; + if let (Some(field), Some(out)) = (field, bytes.get_mut(at..at.saturating_add(width))) { + out.copy_from_slice(field); + } + } +} + +/// The `N` bytes of `bytes` at `at`, or `None` past the end. +pub fn array(bytes: &[u8], at: usize) -> Option<[u8; N]> { + bytes.get(at..at.checked_add(N)?)?.try_into().ok() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn both_orders_read_and_write() { + let bytes = [0x12, 0x34, 0x56, 0x78]; + assert_eq!(ByteOrder::Little.uint_at(&bytes, 0, 2), Some(0x3412)); + assert_eq!(ByteOrder::Big.uint_at(&bytes, 0, 4), Some(0x1234_5678)); + assert_eq!(ByteOrder::Big.uint_at(&bytes, 2, 4), None); + let mut out = [0u8; 4]; + ByteOrder::Big.put_uint(&mut out, 1, 2, 0xABCD); + assert_eq!(out, [0, 0xAB, 0xCD, 0]); + ByteOrder::Little.put_uint(&mut out, 0, 2, 0xABCD); + assert_eq!(out, [0xCD, 0xAB, 0xCD, 0]); + } +} diff --git a/nodedb-raster/src/tiff/chain.rs b/nodedb-raster/src/tiff/chain.rs new file mode 100644 index 000000000..326d3bc53 --- /dev/null +++ b/nodedb-raster/src/tiff/chain.rs @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! The IFD chain: every IFD's offset, size and subfile kind. + +use crate::error::RasterError; +use crate::tiff::error::TiffFormatError; +use crate::tiff::fetch::Fetcher; +use crate::tiff::file_header::Format; +use crate::tiff::ifd::{Ifd, entry_count, parse_ifd}; +use crate::tiff::tags::{ + IMAGE_LENGTH, IMAGE_WIDTH, NEW_SUBFILE_TYPE, OLD_SUBFILE_REDUCED, SUBFILE_MASK, + SUBFILE_REDUCED, SUBFILE_TYPE, +}; +use crate::tiff::values::{TagValues, prefetch}; + +/// Most IFDs a chain may hold. A COG holds two per overview level. +pub const MAX_IFDS: usize = 256; + +/// The tags the chain walk reads from every IFD. +const SUMMARY_TAGS: [u16; 4] = [NEW_SUBFILE_TYPE, SUBFILE_TYPE, IMAGE_WIDTH, IMAGE_LENGTH]; + +/// One IFD of the chain. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct IfdSummary { + pub offset: u64, + pub width: u64, + pub height: u64, + /// A reduced-resolution image: an overview. + pub reduced: bool, + /// A transparency mask. + pub mask: bool, +} + +/// The full IFD at `offset`, or `None` when this round lacks its bytes. +pub fn read_ifd( + fetcher: &mut Fetcher<'_>, + format: Format, + offset: u64, +) -> Result, RasterError> { + let Some(head) = fetcher.bytes(offset, format.count_len())? else { + return Ok(None); + }; + let count = entry_count(format, offset, head)?; + let len = format + .ifd_len(count) + .ok_or(TiffFormatError::OffsetOverflow { + offset, + len: u64::MAX, + })?; + let Some(body) = fetcher.bytes(offset, len)? else { + return Ok(None); + }; + Ok(Some(parse_ifd(format, offset, body)?)) +} + +/// Walk the chain from `first`. `None` when this round lacks the bytes of +/// an IFD or of a summary tag value. +pub fn walk( + fetcher: &mut Fetcher<'_>, + format: Format, + first: u64, + header_bytes: usize, +) -> Result>, RasterError> { + let mut chain: Vec = Vec::new(); + let mut offset = first; + while offset != 0 { + if chain.iter().any(|seen| seen.offset == offset) { + return Err(TiffFormatError::IfdLoop { offset }.into()); + } + if chain.len() >= MAX_IFDS { + return Err(TiffFormatError::TooManyIfds { limit: MAX_IFDS }.into()); + } + let Some(ifd) = read_ifd(fetcher, format, offset)? else { + return Ok(None); + }; + prefetch(&ifd, &SUMMARY_TAGS, fetcher, header_bytes)?; + if fetcher.pending() { + return Ok(None); + } + let values = TagValues { + ifd: &ifd, + cache: fetcher.cache(), + object: fetcher.object(), + order: format.order, + header_bytes, + }; + let subfile = values.uint(NEW_SUBFILE_TYPE)?.unwrap_or(0); + let old_subfile = values.uint(SUBFILE_TYPE)?.unwrap_or(0); + chain.push(IfdSummary { + offset, + width: values.uint(IMAGE_WIDTH)?.unwrap_or(0), + height: values.uint(IMAGE_LENGTH)?.unwrap_or(0), + reduced: subfile & SUBFILE_REDUCED != 0 || old_subfile == OLD_SUBFILE_REDUCED, + mask: subfile & SUBFILE_MASK != 0, + }); + offset = ifd.next; + } + Ok(Some(chain)) +} diff --git a/nodedb-raster/src/tiff/decode.rs b/nodedb-raster/src/tiff/decode.rs new file mode 100644 index 000000000..7e960b6d5 --- /dev/null +++ b/nodedb-raster/src/tiff/decode.rs @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! One compressed chunk to raw sample bytes: decompress, then undo the +//! predictor. + +use crate::codec::compression::Compression; +use crate::codec::error::CodecError; +use crate::codec::jpeg::{self, JpegChunk}; +use crate::codec::lerc::{self, LercChunk}; +use crate::codec::predictor::PredictorShape; +use crate::error::RasterError; +use crate::limits::checked_raster_capacity; +use crate::tiff::byte_order::ByteOrder; +use crate::tiff::image::TiffImage; + +/// Slack a LERC blob may need over its raw samples: header, mask and +/// incompressible blocks. +const LERC_SLACK: usize = 64 * 1024; + +/// A decoded chunk. +#[derive(Debug)] +pub struct DecodedChunk { + /// `width x rows` pixels of interleaved samples. + pub bytes: Vec, + pub order: ByteOrder, + /// One mask per chunk sample from LERC validity masks: one flag per + /// chunk pixel, `None` when every pixel of the sample is valid. + pub masks: Vec>>, +} + +/// Decode chunk `data` of `image`, `rows` rows tall. +pub fn decode_chunk( + image: &TiffImage, + data: &[u8], + rows: u32, +) -> Result { + let samples = checked_raster_capacity(image.chunk_samples()).map_err(CodecError::from)?; + let width = image.layout.block_width; + let sample_type = image.layout.sample_type; + // The open-time block cap bounds this product. + let expected = (width as usize) + .saturating_mul(rows as usize) + .saturating_mul(samples) + .saturating_mul(sample_type.byte_len()); + match image.compression { + Compression::Stream(codec) => { + let mut bytes = codec.decompress(data, expected)?; + image.predictor.undo( + &mut bytes, + PredictorShape { + row_samples: width as usize * samples, + stride: samples, + sample_type, + order: image.order, + }, + ); + Ok(DecodedChunk { + bytes, + order: image.order, + masks: vec![None; samples], + }) + } + Compression::Jpeg => { + let bytes = jpeg::decode(JpegChunk { + data, + tables: image.jpeg_tables.as_deref(), + width, + height: rows, + components: samples, + })?; + Ok(DecodedChunk { + bytes, + order: image.order, + masks: vec![None; samples], + }) + } + Compression::Lerc(wrap) => { + let unwrapped; + let blob = match wrap.stream() { + None => data, + Some(codec) => { + let max = expected.saturating_mul(2).saturating_add(LERC_SLACK); + unwrapped = codec.decompress_bounded(data, max)?; + unwrapped.as_slice() + } + }; + let decoded = lerc::decode( + blob, + LercChunk { + width, + height: rows, + samples: u32::try_from(samples).unwrap_or(u32::MAX), + sample_type, + }, + )?; + Ok(DecodedChunk { + bytes: decoded.bytes, + order: ByteOrder::Little, + masks: decoded.masks, + }) + } + } +} diff --git a/nodedb-raster/src/tiff/error.rs b/nodedb-raster/src/tiff/error.rs new file mode 100644 index 000000000..f63ca7c5a --- /dev/null +++ b/nodedb-raster/src/tiff/error.rs @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Typed errors of the TIFF container layer. + +use crate::limits::CapacityExceeded; +use crate::sample::SampleType; + +/// Every way TIFF structure can be malformed. +#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] +pub enum TiffFormatError { + #[error("not a TIFF: the file opens with neither 'II*' nor 'MM*' nor BigTIFF 'II+'/'MM+'")] + NotTiff, + + #[error("BigTIFF header declares {offset_size}-byte offsets; BigTIFF offsets are 8 bytes")] + BigTiffOffsetSize { offset_size: u16 }, + + #[error("the file ends at byte {end}, inside a {len}-byte structure at byte {offset}")] + Truncated { offset: u64, len: u64, end: u64 }, + + #[error("the IFD chain loops back to byte {offset}")] + IfdLoop { offset: u64 }, + + #[error("the IFD chain holds more than {limit} IFDs")] + TooManyIfds { limit: usize }, + + #[error("the IFD at byte {offset} declares {count} entries, over the {limit}-entry cap")] + TooManyEntries { offset: u64, count: u64, limit: u64 }, + + #[error(transparent)] + Capacity(#[from] CapacityExceeded), + + #[error("the IFD chain holds no full-resolution image")] + NoImage, + + #[error("tag {tag} has field type {kind}, which does not fit its value")] + FieldType { tag: u16, kind: u16 }, + + #[error("required tag {tag} ({name}) is absent")] + MissingTag { tag: u16, name: &'static str }, + + #[error("tag {tag} holds {found} values, expected {expected}")] + TagCount { tag: u16, found: u64, expected: u64 }, + + #[error("tag {tag} declares a value whose offset and length overflow")] + ValueOverflow { tag: u16 }, + + #[error("tag {tag} holds {value}, which is out of range")] + TagValue { tag: u16, value: u64 }, + + #[error("tag {tag} holds ASCII text that is not UTF-8")] + AsciiEncoding { tag: u16 }, + + #[error("a {len}-byte range at byte {offset} overflows the file offsets")] + OffsetOverflow { offset: u64, len: u64 }, + + #[error("chunk {chunk} at byte {offset} with {len} bytes overflows the file offsets")] + ChunkOverflow { chunk: u64, offset: u64, len: u64 }, + + #[error( + "the image is {width}x{height} in {block_width}x{block_height} blocks: too many blocks" + )] + TooManyBlocks { + width: u32, + height: u32, + block_width: u32, + block_height: u32, + }, +} + +/// A valid TIFF feature the reader does not decode. +#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] +pub enum UnsupportedTiff { + #[error("bands of different bit widths ({first} and {other} bits)")] + MixedBitsPerSample { first: u16, other: u16 }, + + #[error("bands of different sample formats ({first} and {other})")] + MixedSampleFormats { first: u16, other: u16 }, + + #[error("predictor {code}; supported: none (1), horizontal (2), floating point (3)")] + Predictor { code: u16 }, + + #[error("the floating-point predictor (3) over {sample_type} samples")] + FloatPredictorOnIntegers { sample_type: SampleType }, + + #[error("JPEG over {sample_type} samples; JPEG decodes 8-bit samples only")] + JpegSampleType { sample_type: SampleType }, + + #[error("JPEG chunks of {samples} samples per pixel; supported: 1 and 3")] + JpegSamples { samples: u16 }, + + #[error("JPEG chunks of {width}x{height} pixels; JPEG frames are at most 65535 on a side")] + JpegFrameSize { width: u32, height: u32 }, + + #[error("YCbCr subsampling {horizontal}x{vertical} without JPEG compression")] + YCbCrSubsampling { horizontal: u16, vertical: u16 }, + + #[error("LERC additional compression {code}; supported: none (0), deflate (1), ZSTD (2)")] + LercCompression { code: u64 }, + + #[error("mask IFD of {bits}-bit samples; supported: 1 and 8")] + MaskBits { bits: u16 }, +} diff --git a/nodedb-raster/src/tiff/fetch.rs b/nodedb-raster/src/tiff/fetch.rs new file mode 100644 index 000000000..2578cd493 --- /dev/null +++ b/nodedb-raster/src/tiff/fetch.rs @@ -0,0 +1,126 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! `Fetcher`: reads an open round's bytes from the cache and records the +//! ranges the cache lacks. + +use crate::error::RasterError; +use crate::source::cache::{Lookup, RangeCache}; +use crate::source::range::{ByteRange, COALESCE_GAP, ObjectId, coalesce}; +use crate::tiff::error::TiffFormatError; + +/// Smallest range a miss asks for. It covers the header and IFD 0 of a +/// cloud-optimised GeoTIFF, GDAL's ghost area included. +pub const MIN_FETCH: u64 = 64 * 1024; + +/// Cache reads of one open round. +#[derive(Debug)] +pub struct Fetcher<'c> { + cache: &'c RangeCache, + object: ObjectId, + need: Vec, +} + +impl<'c> Fetcher<'c> { + pub fn new(cache: &'c RangeCache, object: ObjectId) -> Self { + Self { + cache, + object, + need: Vec::new(), + } + } + + /// The cache this round reads. + pub fn cache(&self) -> &'c RangeCache { + self.cache + } + + /// The object this round reads. + pub fn object(&self) -> ObjectId { + self.object + } + + /// The `len` bytes at `offset`. `Ok(None)` when the cache lacks them: + /// the range, widened to [`MIN_FETCH`], is recorded as needed. + pub fn bytes(&mut self, offset: u64, len: u64) -> Result, RasterError> { + match self.lookup(offset, len)? { + Some(bytes) if bytes.len() as u64 == len => Ok(Some(bytes)), + Some(bytes) => Err(TiffFormatError::Truncated { + offset, + len, + end: offset.saturating_add(bytes.len() as u64), + } + .into()), + None => Ok(None), + } + } + + /// Up to `len` bytes at `offset`: fewer only where the object ends. + pub fn prefix(&mut self, offset: u64, len: u64) -> Result, RasterError> { + self.lookup(offset, len) + } + + /// True when this round recorded a needed range. + pub fn pending(&self) -> bool { + !self.need.is_empty() + } + + /// The needed ranges, coalesced. + pub fn into_need(self) -> Vec { + coalesce(self.need, COALESCE_GAP) + } + + fn lookup(&mut self, offset: u64, len: u64) -> Result, RasterError> { + let end = offset + .checked_add(len) + .ok_or(TiffFormatError::OffsetOverflow { offset, len })?; + match self.cache.get(ByteRange::new(self.object, offset, end)) { + Lookup::Bytes(bytes) => Ok(Some(bytes)), + Lookup::Absent => Err(RasterError::SourceMissing { + object: self.object.0, + }), + Lookup::Missing => { + let fetch_end = offset.saturating_add(len.max(MIN_FETCH)); + self.need + .push(ByteRange::new(self.object, offset, fetch_end)); + Ok(None) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn misses_ask_for_at_least_the_minimum_fetch() { + let cache = RangeCache::new(1 << 20); + let mut fetcher = Fetcher::new(&cache, ObjectId(0)); + assert_eq!(fetcher.bytes(0, 16).ok(), Some(None)); + assert_eq!(fetcher.bytes(100, 8).ok(), Some(None)); + assert!(fetcher.pending()); + assert_eq!( + fetcher.into_need(), + vec![ByteRange::new(ObjectId(0), 0, 100 + MIN_FETCH)] + ); + } + + #[test] + fn hits_and_the_object_end_answer_from_the_cache() { + let mut cache = RangeCache::new(1 << 20); + cache + .insert(ByteRange::new(ObjectId(0), 0, MIN_FETCH), Some(vec![1; 10])) + .expect("fits"); + let mut fetcher = Fetcher::new(&cache, ObjectId(0)); + assert_eq!(fetcher.bytes(2, 4).ok(), Some(Some(&[1u8; 4][..]))); + assert_eq!(fetcher.prefix(0, 16).ok(), Some(Some(&[1u8; 10][..]))); + assert!(matches!( + fetcher.bytes(8, 4), + Err(RasterError::Malformed(TiffFormatError::Truncated { + end: 10, + .. + })) + )); + assert!(!fetcher.pending()); + } +} diff --git a/nodedb-raster/src/tiff/file_header.rs b/nodedb-raster/src/tiff/file_header.rs new file mode 100644 index 000000000..a756f18f5 --- /dev/null +++ b/nodedb-raster/src/tiff/file_header.rs @@ -0,0 +1,126 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! The TIFF file header: byte order, classic or BigTIFF, first IFD. + +use crate::tiff::byte_order::{ByteOrder, array}; +use crate::tiff::error::TiffFormatError; + +const CLASSIC_MAGIC: u16 = 42; +const BIG_MAGIC: u16 = 43; +/// Bytes a BigTIFF header occupies; a classic header is 8. +pub const MAX_HEADER_LEN: u64 = 16; + +/// Classic TIFF or BigTIFF, in one byte order. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Format { + pub order: ByteOrder, + pub big: bool, +} + +impl Format { + /// Bytes of one IFD entry. + pub const fn entry_len(self) -> u64 { + if self.big { 20 } else { 12 } + } + + /// Bytes of the entry count that opens an IFD. + pub const fn count_len(self) -> u64 { + if self.big { 8 } else { 2 } + } + + /// Bytes of an offset: the next-IFD link and an out-of-line value. + pub const fn offset_len(self) -> u64 { + if self.big { 8 } else { 4 } + } + + /// Bytes of an IFD with `count` entries, or `None` on overflow. + pub const fn ifd_len(self, count: u64) -> Option { + match count.checked_mul(self.entry_len()) { + Some(entries) => entries.checked_add(self.count_len() + self.offset_len()), + None => None, + } + } +} + +/// The parsed file header. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct FileHeader { + pub format: Format, + pub first_ifd: u64, +} + +/// Parse the header at the start of `bytes`. `bytes` holds the first +/// [`MAX_HEADER_LEN`] bytes of the file, fewer for a shorter file. +pub fn parse_header(bytes: &[u8]) -> Result { + let truncated = |len| TiffFormatError::Truncated { + offset: 0, + len, + end: bytes.len() as u64, + }; + let order = match bytes.get(..2) { + Some(b"II") => ByteOrder::Little, + Some(b"MM") => ByteOrder::Big, + Some(_) => return Err(TiffFormatError::NotTiff), + None => return Err(truncated(8)), + }; + let magic = array::<2>(bytes, 2) + .map(|raw| order.u16(raw)) + .ok_or(truncated(8))?; + match magic { + CLASSIC_MAGIC => { + let first = array::<4>(bytes, 4).ok_or(truncated(8))?; + Ok(FileHeader { + format: Format { order, big: false }, + first_ifd: u64::from(order.u32(first)), + }) + } + BIG_MAGIC => { + let raw = array::<2>(bytes, 4).ok_or(truncated(MAX_HEADER_LEN))?; + let offset_size = order.u16(raw); + if offset_size != 8 { + return Err(TiffFormatError::BigTiffOffsetSize { offset_size }); + } + let first = array::<8>(bytes, 8).ok_or(truncated(MAX_HEADER_LEN))?; + Ok(FileHeader { + format: Format { order, big: true }, + first_ifd: order.u64(first), + }) + } + _ => Err(TiffFormatError::NotTiff), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn classic_and_big_headers_parse_in_both_orders() { + let classic = parse_header(b"II*\0\x08\0\0\0").expect("classic"); + assert_eq!(classic.first_ifd, 8); + assert!(!classic.format.big); + let big = parse_header(b"MM\0+\0\x08\0\0\0\0\0\0\0\0\0\x10").expect("big"); + assert_eq!(big.first_ifd, 16); + assert_eq!(big.format.order, ByteOrder::Big); + assert!(big.format.big); + assert_eq!(big.format.ifd_len(2), Some(8 + 40 + 8)); + } + + #[test] + fn garbage_and_short_headers_are_typed_errors() { + assert_eq!(parse_header(b"hello"), Err(TiffFormatError::NotTiff)); + assert!(matches!( + parse_header(b"II*"), + Err(TiffFormatError::Truncated { .. }) + )); + assert!(matches!( + parse_header(b""), + Err(TiffFormatError::Truncated { .. }) + )); + assert_eq!( + parse_header(b"II+\0\x04\0\0\0"), + Err(TiffFormatError::BigTiffOffsetSize { offset_size: 4 }) + ); + assert_eq!(parse_header(b"II\x2b\x01"), Err(TiffFormatError::NotTiff)); + } +} diff --git a/nodedb-raster/src/tiff/gdal_metadata.rs b/nodedb-raster/src/tiff/gdal_metadata.rs new file mode 100644 index 000000000..288cb8da5 --- /dev/null +++ b/nodedb-raster/src/tiff/gdal_metadata.rs @@ -0,0 +1,222 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! The `GDAL_METADATA` tag (42112): GDAL's XML band metadata. +//! +//! ```xml +//! +//! 0.0000275 +//! -0.2 +//! red +//! +//! ``` +//! +//! Only per-band items (those with a `sample` attribute) are read: name +//! (`description`), `scale`, `offset`, `unittype`, a nodata item +//! (`NODATA` or `_FillValue`), and `valid_range` / `valid_min` / +//! `valid_max`. Other items are ignored. + +use std::collections::BTreeMap; + +use crate::error::RasterError; +use crate::nodata::parse_gdal_nodata; + +/// The metadata of one band. +#[derive(Debug, Clone, Default, PartialEq)] +pub struct BandMetadata { + pub name: Option, + pub scale: Option, + pub offset: Option, + pub units: Option, + pub nodata: Option, + pub valid_min: Option, + pub valid_max: Option, +} + +impl BandMetadata { + /// The valid range, when both ends are known. + pub fn valid_range(&self) -> Option<(f64, f64)> { + self.valid_min.zip(self.valid_max) + } +} + +/// One parsed ``. +#[derive(Debug, Default)] +struct Item { + name: String, + sample: Option, + role: Option, + text: String, +} + +/// Per-band metadata by zero-based band index. +pub fn parse_gdal_metadata(xml: &str) -> Result, RasterError> { + let mut bands: BTreeMap = BTreeMap::new(); + let mut rest = xml; + while let Some(start) = rest.find("() + .map_err(|_| RasterError::InvalidMetadata { + name: "sample".to_owned(), + text: sample.clone(), + })?; + apply(bands.entry(band).or_default(), &item)?; + } + Ok(bands) +} + +/// Fold one item into its band. +fn apply(band: &mut BandMetadata, item: &Item) -> Result<(), RasterError> { + let role = item + .role + .as_deref() + .unwrap_or_default() + .to_ascii_lowercase(); + let name = item.name.to_ascii_lowercase(); + let text = item.text.trim(); + let number = || { + text.parse::() + .map_err(|_| RasterError::InvalidMetadata { + name: item.name.clone(), + text: text.to_owned(), + }) + }; + match (role.as_str(), name.as_str()) { + ("description", _) | (_, "description") => band.name = Some(text.to_owned()), + ("scale", _) | ("", "scale") => band.scale = Some(number()?), + ("offset", _) | ("", "offset") => band.offset = Some(number()?), + ("unittype", _) | ("", "units") => band.units = Some(text.to_owned()), + (_, "nodata" | "_fillvalue") => band.nodata = Some(parse_gdal_nodata(text)?), + (_, "valid_min") => band.valid_min = Some(number()?), + (_, "valid_max") => band.valid_max = Some(number()?), + (_, "valid_range") => { + let (min, max) = parse_range(text).ok_or(RasterError::InvalidMetadata { + name: item.name.clone(), + text: text.to_owned(), + })?; + band.valid_min = Some(min); + band.valid_max = Some(max); + } + _ => {} + } + Ok(()) +} + +/// Two numbers, comma or space separated, optionally in braces. +fn parse_range(text: &str) -> Option<(f64, f64)> { + let inner = text + .trim() + .trim_start_matches(['{', '[', '(']) + .trim_end_matches(['}', ']', ')']); + let mut numbers = inner + .split(|c: char| c == ',' || c.is_whitespace()) + .filter(|part| !part.is_empty()) + .map(str::parse::); + let min = numbers.next()?.ok()?; + let max = numbers.next()?.ok()?; + numbers.next().is_none().then_some((min, max)) +} + +/// Parse the attributes and text of an item whose ` Option<(Item, &str)> { + let close = input.find('>')?; + let (attrs, after) = (input.get(..close)?, input.get(close + 1..)?); + let mut item = Item::default(); + let self_closing = attrs.trim_end().ends_with('/'); + let mut rest = attrs.trim_end_matches('/'); + while let Some(eq) = rest.find('=') { + let key = rest.get(..eq)?.trim(); + let value_part = rest.get(eq + 1..)?.trim_start(); + let quote = value_part.chars().next()?; + if quote != '"' && quote != '\'' { + return None; + } + let body = value_part.get(1..)?; + let end = body.find(quote)?; + let value = unescape(body.get(..end)?); + match key { + "name" => item.name = value, + "sample" => item.sample = Some(value), + "role" => item.role = Some(value), + _ => {} + } + rest = body.get(end + 1..)?; + } + if self_closing { + return Some((item, after)); + } + let end = after.find("")?; + item.text = unescape(after.get(..end)?); + Some((item, after.get(end + "".len()..)?)) +} + +/// Replace the five predefined XML entities. +fn unescape(text: &str) -> String { + text.replace("<", "<") + .replace(">", ">") + .replace(""", "\"") + .replace("'", "'") + .replace("&", "&") +} + +#[cfg(test)] +mod tests { + use super::*; + + const XML: &str = r#" + Area + 2.75e-05 + -0.2 + red & nir + K + -9999 + {1, 65455} + +"#; + + #[test] + fn per_band_items_parse() { + let bands = parse_gdal_metadata(XML).expect("parses"); + let red = &bands[&0]; + assert_eq!(red.scale, Some(2.75e-05)); + assert_eq!(red.offset, Some(-0.2)); + assert_eq!(red.name.as_deref(), Some("red & nir")); + let second = &bands[&1]; + assert_eq!(second.units.as_deref(), Some("K")); + assert_eq!(second.nodata, Some(-9999.0)); + assert_eq!(second.valid_range(), Some((1.0, 65455.0))); + assert!(bands.contains_key(&2)); + assert_eq!(bands.len(), 3, "dataset items are ignored"); + } + + #[test] + fn bad_numbers_are_typed_errors() { + let bad = r#"wide"#; + assert!(matches!( + parse_gdal_metadata(bad), + Err(RasterError::InvalidMetadata { .. }) + )); + let bad_band = r#"1"#; + assert!(matches!( + parse_gdal_metadata(bad_band), + Err(RasterError::InvalidMetadata { .. }) + )); + } + + #[test] + fn truncated_xml_stops_without_error() { + assert!(parse_gdal_metadata(r#", + pub geokeys: Option, + pub crs: CrsRef, + /// `GDAL_NODATA`, shared by every band. + pub nodata: Option, + pub metadata: BTreeMap, +} + +impl GeoInfo { + /// Read the georeference tags of `values`, the first image's IFD. + pub fn read(values: &TagValues<'_>) -> Result { + let geokeys = read_geokeys(values)?; + let pixel_scale = values.f64s(MODEL_PIXEL_SCALE)?; + let tiepoints = values.f64s(MODEL_TIEPOINT)?; + let transformation = values.f64s(MODEL_TRANSFORMATION)?; + let transform = transform_from_tags(ModelTags { + pixel_scale: pixel_scale.as_deref(), + tiepoints: tiepoints.as_deref(), + transformation: transformation.as_deref(), + raster_type: geokeys.as_ref().and_then(|keys| keys.short(GT_RASTER_TYPE)), + })?; + let crs = crs_ref_from_geokeys(geokeys.as_ref())?; + let nodata = values + .ascii(GDAL_NODATA)? + .map(|text| parse_gdal_nodata(&text)) + .transpose()?; + let metadata = match values.ascii(GDAL_METADATA)? { + Some(xml) => parse_gdal_metadata(&xml)?, + None => BTreeMap::new(), + }; + Ok(Self { + transform, + geokeys, + crs, + nodata, + metadata, + }) + } + + /// The band list of an image with `bands` bands of `sample_type`. + pub fn bands(&self, bands: u16, sample_type: SampleType) -> Vec { + (0..u32::from(bands)) + .map(|band| { + let meta = self.metadata.get(&band); + BandInfo { + name: meta.and_then(|m| m.name.clone()), + sample_type, + nodata: meta.and_then(|m| m.nodata).or(self.nodata), + scale: meta.and_then(|m| m.scale), + offset: meta.and_then(|m| m.offset), + valid_range: meta.and_then(BandMetadata::valid_range), + units: meta.and_then(|m| m.units.clone()), + } + }) + .collect() + } +} + +/// The GeoKey directory with its parameter arrays, when the file has one. +fn read_geokeys(values: &TagValues<'_>) -> Result, RasterError> { + let Some(directory) = values.u16s(GEO_KEY_DIRECTORY)? else { + return Ok(None); + }; + let doubles = values.f64s(GEO_DOUBLE_PARAMS)?; + let ascii = values.ascii(GEO_ASCII_PARAMS)?; + let keys = GeoKeyDirectory::parse(&directory, doubles.as_deref(), ascii.as_deref())?; + Ok(Some(keys)) +} diff --git a/nodedb-raster/src/geokeys.rs b/nodedb-raster/src/tiff/geokeys.rs similarity index 78% rename from nodedb-raster/src/geokeys.rs rename to nodedb-raster/src/tiff/geokeys.rs index 7d85a8f91..5638ba28f 100644 --- a/nodedb-raster/src/geokeys.rs +++ b/nodedb-raster/src/tiff/geokeys.rs @@ -20,6 +20,72 @@ pub const GT_RASTER_TYPE: u16 = 1025; pub const GEOGRAPHIC_TYPE: u16 = 2048; /// `ProjectedCRSGeoKey` (`ProjectedCSTypeGeoKey` in GeoTIFF 1.0). pub const PROJECTED_CS_TYPE: u16 = 3072; +/// `GeodeticDatumGeoKey`: EPSG datum code, or 32767. +pub const GEODETIC_DATUM: u16 = 2050; +/// `PrimeMeridianGeoKey`: EPSG prime meridian code, or 32767. +pub const PRIME_MERIDIAN: u16 = 2051; +/// `GeogAngularUnitsGeoKey`: EPSG angle unit code, or 32767. +pub const GEOG_ANGULAR_UNITS: u16 = 2054; +/// `GeogAngularUnitSizeGeoKey`: radians per user-defined angle unit. +pub const GEOG_ANGULAR_UNIT_SIZE: u16 = 2055; +/// `EllipsoidGeoKey`: EPSG ellipsoid code, or 32767. +pub const ELLIPSOID: u16 = 2056; +/// `EllipsoidSemiMajorAxisGeoKey`, in the geodetic linear unit. +pub const SEMI_MAJOR_AXIS: u16 = 2057; +/// `EllipsoidSemiMinorAxisGeoKey`. +pub const SEMI_MINOR_AXIS: u16 = 2058; +/// `EllipsoidInvFlatteningGeoKey`. +pub const INV_FLATTENING: u16 = 2059; +/// `GeogAzimuthUnitsGeoKey`: EPSG angle unit of azimuths. +pub const GEOG_AZIMUTH_UNITS: u16 = 2060; +/// `PrimeMeridianLongitudeGeoKey`, in the angular unit. +pub const PRIME_MERIDIAN_LONGITUDE: u16 = 2061; +/// `ProjectionGeoKey`: EPSG conversion code, or 32767. +pub const PROJECTION: u16 = 3074; +/// `ProjMethodGeoKey` (`ProjCoordTransGeoKey` in GeoTIFF 1.0). +pub const PROJ_METHOD: u16 = 3075; +/// `ProjLinearUnitsGeoKey`: EPSG length unit code, or 32767. +pub const PROJ_LINEAR_UNITS: u16 = 3076; +/// `ProjLinearUnitSizeGeoKey`: metres per user-defined length unit. +pub const PROJ_LINEAR_UNIT_SIZE: u16 = 3077; +/// `ProjStdParallel1GeoKey`. +pub const PROJ_STD_PARALLEL_1: u16 = 3078; +/// `ProjStdParallel2GeoKey`. +pub const PROJ_STD_PARALLEL_2: u16 = 3079; +/// `ProjNatOriginLongGeoKey`. +pub const PROJ_NAT_ORIGIN_LONG: u16 = 3080; +/// `ProjNatOriginLatGeoKey`. +pub const PROJ_NAT_ORIGIN_LAT: u16 = 3081; +/// `ProjFalseEastingGeoKey`. +pub const PROJ_FALSE_EASTING: u16 = 3082; +/// `ProjFalseNorthingGeoKey`. +pub const PROJ_FALSE_NORTHING: u16 = 3083; +/// `ProjFalseOriginLongGeoKey`. +pub const PROJ_FALSE_ORIGIN_LONG: u16 = 3084; +/// `ProjFalseOriginLatGeoKey`. +pub const PROJ_FALSE_ORIGIN_LAT: u16 = 3085; +/// `ProjFalseOriginEastingGeoKey`. +pub const PROJ_FALSE_ORIGIN_EASTING: u16 = 3086; +/// `ProjFalseOriginNorthingGeoKey`. +pub const PROJ_FALSE_ORIGIN_NORTHING: u16 = 3087; +/// `ProjCenterLongGeoKey`. +pub const PROJ_CENTER_LONG: u16 = 3088; +/// `ProjCenterLatGeoKey`. +pub const PROJ_CENTER_LAT: u16 = 3089; +/// `ProjCenterEastingGeoKey`. +pub const PROJ_CENTER_EASTING: u16 = 3090; +/// `ProjCenterNorthingGeoKey`. +pub const PROJ_CENTER_NORTHING: u16 = 3091; +/// `ProjScaleAtNatOriginGeoKey`. +pub const PROJ_SCALE_AT_NAT_ORIGIN: u16 = 3092; +/// `ProjScaleAtCenterGeoKey`. +pub const PROJ_SCALE_AT_CENTER: u16 = 3093; +/// `ProjAzimuthAngleGeoKey`, in the azimuth unit. +pub const PROJ_AZIMUTH_ANGLE: u16 = 3094; +/// `ProjStraightVertPoleLongGeoKey`. +pub const PROJ_STRAIGHT_VERT_POLE_LONG: u16 = 3095; +/// `ProjRectifiedGridAngleGeoKey`, in the azimuth unit. +pub const PROJ_RECTIFIED_GRID_ANGLE: u16 = 3096; const DIRECTORY_TAG: u16 = 34735; const DOUBLE_PARAMS_TAG: u16 = 34736; @@ -180,6 +246,14 @@ impl GeoKeyDirectory { } } + /// The first double of `key`, when `key` holds doubles. + pub fn double(&self, key: u16) -> Option { + match self.get(key)? { + GeoKeyValue::Double(values) => values.first().copied(), + GeoKeyValue::Short(_) | GeoKeyValue::Ascii(_) => None, + } + } + /// The number of keys. pub fn len(&self) -> usize { self.keys.len() diff --git a/nodedb-raster/src/tiff/ifd.rs b/nodedb-raster/src/tiff/ifd.rs new file mode 100644 index 000000000..f05722c00 --- /dev/null +++ b/nodedb-raster/src/tiff/ifd.rs @@ -0,0 +1,243 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! One IFD: its entry table and the link to the next IFD. + +use crate::limits::checked_raster_capacity; +use crate::tiff::byte_order::array; +use crate::tiff::error::TiffFormatError; +use crate::tiff::file_header::Format; +use crate::tiff::tags::FieldType; + +/// Most entries one IFD may declare. Real images use a few dozen. +pub const MAX_IFD_ENTRIES: u64 = 4096; + +/// Where an entry's value bytes are. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ValueLocation { + /// In the entry itself: the first `len` bytes of `raw`. + Inline { raw: [u8; 8], len: usize }, + /// `len` bytes at file offset `offset`. + At { offset: u64, len: u64 }, +} + +/// One IFD entry of a known field type. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Entry { + pub tag: u16, + pub kind: FieldType, + pub count: u64, + pub location: ValueLocation, +} + +impl Entry { + /// The value's byte length. + pub fn byte_len(&self) -> u64 { + match self.location { + ValueLocation::Inline { len, .. } => len as u64, + ValueLocation::At { len, .. } => len, + } + } +} + +/// A parsed IFD. Entries of unknown field type are skipped. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Ifd { + pub offset: u64, + pub entries: Vec, + pub next: u64, +} + +impl Ifd { + /// The entry of `tag`, when present. + pub fn entry(&self, tag: u16) -> Option<&Entry> { + self.entries.iter().find(|entry| entry.tag == tag) + } +} + +/// The entry count of the IFD whose first bytes are `head`. +pub fn entry_count(format: Format, offset: u64, head: &[u8]) -> Result { + let truncated = TiffFormatError::Truncated { + offset, + len: format.count_len(), + end: offset.saturating_add(head.len() as u64), + }; + let count = if format.big { + format.order.u64(array::<8>(head, 0).ok_or(truncated)?) + } else { + u64::from(format.order.u16(array::<2>(head, 0).ok_or(truncated)?)) + }; + if count > MAX_IFD_ENTRIES { + return Err(TiffFormatError::TooManyEntries { + offset, + count, + limit: MAX_IFD_ENTRIES, + }); + } + Ok(count) +} + +/// Parse the IFD at file offset `offset` from `bytes`, which start at that +/// offset and hold the whole IFD. +pub fn parse_ifd(format: Format, offset: u64, bytes: &[u8]) -> Result { + let count = entry_count(format, offset, bytes)?; + let total = format + .ifd_len(count) + .ok_or(TiffFormatError::TooManyEntries { + offset, + count, + limit: MAX_IFD_ENTRIES, + })?; + if (bytes.len() as u64) < total { + return Err(TiffFormatError::Truncated { + offset, + len: total, + end: offset.saturating_add(bytes.len() as u64), + }); + } + // `count` is at most MAX_IFD_ENTRIES, so these fit any usize. + let entry_len = format.entry_len() as usize; + let first = format.count_len() as usize; + let capacity = checked_raster_capacity(count as usize)?; + let mut entries = Vec::with_capacity(capacity); + for index in 0..count as usize { + let at = first + index * entry_len; + if let Some(entry) = parse_entry(format, bytes, at)? { + entries.push(entry); + } + } + let next_at = first + count as usize * entry_len; + let next = if format.big { + array::<8>(bytes, next_at).map(|raw| format.order.u64(raw)) + } else { + array::<4>(bytes, next_at).map(|raw| u64::from(format.order.u32(raw))) + } + .unwrap_or(0); + Ok(Ifd { + offset, + entries, + next, + }) +} + +/// The entry at `at`, or `None` for an unknown field type. +fn parse_entry(format: Format, bytes: &[u8], at: usize) -> Result, TiffFormatError> { + let order = format.order; + let short = |off: usize| array::<2>(bytes, at + off).map(|raw| order.u16(raw)); + let (Some(tag), Some(code)) = (short(0), short(2)) else { + return Ok(None); + }; + let (count, value_at) = if format.big { + let count = array::<8>(bytes, at + 4).map(|raw| order.u64(raw)); + (count, at + 12) + } else { + let count = array::<4>(bytes, at + 4).map(|raw| u64::from(order.u32(raw))); + (count, at + 8) + }; + let inline_len = format.offset_len() as usize; + let (Some(count), Some(field)) = (count, bytes.get(value_at..value_at + inline_len)) else { + return Ok(None); + }; + let Some(kind) = FieldType::from_code(code) else { + return Ok(None); + }; + let len = count + .checked_mul(kind.width()) + .ok_or(TiffFormatError::ValueOverflow { tag })?; + let location = if len <= inline_len as u64 { + let mut raw = [0u8; 8]; + raw[..inline_len].copy_from_slice(field); + ValueLocation::Inline { + raw, + len: len as usize, + } + } else { + let offset = if format.big { + array::<8>(field, 0).map(|raw| order.u64(raw)) + } else { + array::<4>(field, 0).map(|raw| u64::from(order.u32(raw))) + } + .unwrap_or(0); + offset + .checked_add(len) + .ok_or(TiffFormatError::ValueOverflow { tag })?; + ValueLocation::At { offset, len } + }; + Ok(Some(Entry { + tag, + kind, + count, + location, + })) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::tiff::byte_order::ByteOrder; + + const CLASSIC_LE: Format = Format { + order: ByteOrder::Little, + big: false, + }; + + fn entry(tag: u16, kind: u16, count: u32, value: u32) -> Vec { + let mut out = tag.to_le_bytes().to_vec(); + out.extend(kind.to_le_bytes()); + out.extend(count.to_le_bytes()); + out.extend(value.to_le_bytes()); + out + } + + #[test] + fn entries_parse_inline_and_out_of_line() { + let mut bytes = 3u16.to_le_bytes().to_vec(); + bytes.extend(entry(256, 3, 1, 640)); + bytes.extend(entry(273, 4, 10, 1000)); + bytes.extend(entry(999, 77, 1, 0)); + bytes.extend(500u32.to_le_bytes()); + let ifd = parse_ifd(CLASSIC_LE, 8, &bytes).expect("parses"); + assert_eq!(ifd.next, 500); + assert_eq!(ifd.entries.len(), 2, "the unknown type is skipped"); + let width = ifd.entry(256).expect("width"); + assert!(matches!( + width.location, + ValueLocation::Inline { len: 2, .. } + )); + let offsets = ifd.entry(273).expect("offsets"); + assert_eq!( + offsets.location, + ValueLocation::At { + offset: 1000, + len: 40 + } + ); + } + + #[test] + fn truncated_and_oversized_tables_are_typed_errors() { + let mut bytes = 2u16.to_le_bytes().to_vec(); + bytes.extend(entry(256, 3, 1, 640)); + assert!(matches!( + parse_ifd(CLASSIC_LE, 8, &bytes), + Err(TiffFormatError::Truncated { .. }) + )); + assert!(matches!( + entry_count(CLASSIC_LE, 8, &u16::MAX.to_le_bytes()), + Err(TiffFormatError::TooManyEntries { .. }) + )); + let big = Format { + order: ByteOrder::Little, + big: true, + }; + let mut huge = 1u64.to_le_bytes().to_vec(); + huge.extend(273u16.to_le_bytes()); + huge.extend(16u16.to_le_bytes()); + huge.extend(u64::MAX.to_le_bytes()); + huge.extend(0u64.to_le_bytes()); + huge.extend(0u64.to_le_bytes()); + assert_eq!( + parse_ifd(big, 16, &huge), + Err(TiffFormatError::ValueOverflow { tag: 273 }) + ); + } +} diff --git a/nodedb-raster/src/tiff/image.rs b/nodedb-raster/src/tiff/image.rs new file mode 100644 index 000000000..b1aabc6d8 --- /dev/null +++ b/nodedb-raster/src/tiff/image.rs @@ -0,0 +1,299 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! One image IFD read into what its blocks decode with: the block grid, +//! the codec, the predictor and the block index. + +use crate::codec::compression::{CODE_NONE, Compression, LercWrap}; +use crate::codec::predictor::Predictor; +use crate::error::RasterError; +use crate::layout::{BlockKind, Interleave, RasterLayout}; +use crate::limits::RasterLimits; +use crate::sample::{FORMAT_UINT, SampleType}; +use crate::tiff::byte_order::ByteOrder; +use crate::tiff::error::{TiffFormatError, UnsupportedTiff}; +use crate::tiff::tags::{ + BITS_PER_SAMPLE, COMPRESSION, IMAGE_LENGTH, IMAGE_WIDTH, JPEG_TABLES, LERC_PARAMETERS, + PHOTOMETRIC, PLANAR_CONFIG, PREDICTOR, ROWS_PER_STRIP, SAMPLE_FORMAT, SAMPLES_PER_PIXEL, + STRIP_BYTE_COUNTS, STRIP_OFFSETS, TILE_BYTE_COUNTS, TILE_LENGTH, TILE_OFFSETS, TILE_WIDTH, + YCBCR_SUBSAMPLING, +}; +use crate::tiff::tile_index::ChunkIndex; +use crate::tiff::values::TagValues; + +/// Photometric interpretations. +const PHOTOMETRIC_BLACK_IS_ZERO: u16 = 1; +const PHOTOMETRIC_RGB: u16 = 2; +const PHOTOMETRIC_SEPARATED: u16 = 5; +const PHOTOMETRIC_YCBCR: u16 = 6; +/// Largest side of a JPEG frame. +const MAX_JPEG_SIDE: u32 = 65535; + +/// A decodable image IFD. +#[derive(Debug, Clone, PartialEq)] +pub struct TiffImage { + pub layout: RasterLayout, + pub order: ByteOrder, + pub compression: Compression, + pub predictor: Predictor, + /// `JPEGTables`, for JPEG chunks. + pub jpeg_tables: Option>, + pub index: ChunkIndex, +} + +impl TiffImage { + /// Read the image IFD of `values`. + pub fn read(values: &TagValues<'_>, limits: RasterLimits) -> Result { + let width = required_u32(values, IMAGE_WIDTH, "ImageWidth")?; + let height = required_u32(values, IMAGE_LENGTH, "ImageLength")?; + let samples = values.u16_value(SAMPLES_PER_PIXEL)?.unwrap_or(1); + if samples == 0 { + return Err(TiffFormatError::TagValue { + tag: SAMPLES_PER_PIXEL, + value: 0, + } + .into()); + } + let bits = per_sample(values, BITS_PER_SAMPLE, 1)?; + let format = per_sample(values, SAMPLE_FORMAT, FORMAT_UINT)?; + let sample_type = SampleType::from_tiff(format, bits)?; + let interleave = match values.uint(PLANAR_CONFIG)?.unwrap_or(1) { + 1 => Interleave::Chunky, + 2 => Interleave::Planar, + value => { + return Err(TiffFormatError::TagValue { + tag: PLANAR_CONFIG, + value, + } + .into()); + } + }; + let compression = Compression::from_code( + values.u16_value(COMPRESSION)?.unwrap_or(CODE_NONE), + lerc_wrap(values)?, + )?; + let chunk_samples = match interleave { + Interleave::Chunky => samples, + Interleave::Planar => 1, + }; + check_photometric(values, samples, compression)?; + let (kind, block_width, block_height) = block_shape(values, width, height)?; + if compression == Compression::Jpeg { + check_jpeg(sample_type, chunk_samples, block_width, block_height)?; + } + let predictor = if compression.takes_predictor() { + Predictor::from_code(values.u16_value(PREDICTOR)?.unwrap_or(1), sample_type)? + } else { + Predictor::None + }; + let too_many = TiffFormatError::TooManyBlocks { + width, + height, + block_width, + block_height, + }; + let blocks_across = width.div_ceil(block_width); + let blocks_down = height.div_ceil(block_height); + let per_plane = blocks_across + .checked_mul(blocks_down) + .ok_or(too_many.clone())?; + let planes = match interleave { + Interleave::Chunky => 1, + Interleave::Planar => u64::from(samples), + }; + let chunks = u64::from(per_plane) * planes; + let layout = RasterLayout { + width, + height, + bands: samples, + sample_type, + kind, + interleave, + block_width, + block_height, + blocks_across, + blocks_down, + }; + let needed_bytes = layout.block_bytes(); + if needed_bytes > limits.block_bytes as u64 { + return Err(RasterError::BlockTooLarge { + needed_bytes, + limit_bytes: limits.block_bytes, + }); + } + let index = match kind { + BlockKind::Tiles => ChunkIndex::read( + values, + (TILE_OFFSETS, "TileOffsets"), + (TILE_BYTE_COUNTS, "TileByteCounts"), + chunks, + )?, + BlockKind::Strips => ChunkIndex::read( + values, + (STRIP_OFFSETS, "StripOffsets"), + (STRIP_BYTE_COUNTS, "StripByteCounts"), + chunks, + )?, + }; + let jpeg_tables = match compression { + Compression::Jpeg => values + .raw(JPEG_TABLES)? + .map(|(_, bytes)| bytes.into_owned()), + Compression::Stream(_) | Compression::Lerc(_) => None, + }; + Ok(Self { + layout, + order: values.order, + compression, + predictor, + jpeg_tables, + index, + }) + } + + /// Samples per pixel in one chunk. + pub fn chunk_samples(&self) -> usize { + match self.layout.interleave { + Interleave::Chunky => usize::from(self.layout.bands), + Interleave::Planar => 1, + } + } + + /// Rows of the chunk whose first row is `row_off`: a full tile, or a + /// strip cropped to the image. + pub fn chunk_rows(&self, row_off: u32) -> u32 { + match self.layout.kind { + BlockKind::Tiles => self.layout.block_height, + BlockKind::Strips => self.layout.block_height.min(self.layout.height - row_off), + } + } +} + +/// A required, non-zero u32 tag. +pub fn required_u32( + values: &TagValues<'_>, + tag: u16, + name: &'static str, +) -> Result { + let value = values + .uint(tag)? + .ok_or(TiffFormatError::MissingTag { tag, name })?; + u32::try_from(value) + .ok() + .filter(|v| *v > 0) + .ok_or(TiffFormatError::TagValue { tag, value }.into()) +} + +/// The one value a per-sample tag holds for every sample. Every sample +/// must share it. +fn per_sample(values: &TagValues<'_>, tag: u16, default: u16) -> Result { + let Some(all) = values.uints(tag)? else { + return Ok(default); + }; + let first = all.first().copied().ok_or(TiffFormatError::TagCount { + tag, + found: 0, + expected: 1, + })?; + let first = + u16::try_from(first).map_err(|_| TiffFormatError::TagValue { tag, value: first })?; + if let Some(&other) = all.iter().find(|v| **v != u64::from(first)) { + let other = u16::try_from(other).unwrap_or(u16::MAX); + return Err(match tag { + BITS_PER_SAMPLE => UnsupportedTiff::MixedBitsPerSample { first, other }, + _ => UnsupportedTiff::MixedSampleFormats { first, other }, + } + .into()); + } + Ok(first) +} + +/// The LERC wrapper from `LercParameters` `[version, compression]`. +fn lerc_wrap(values: &TagValues<'_>) -> Result { + let code = values + .uints(LERC_PARAMETERS)? + .and_then(|params| params.get(1).copied()) + .unwrap_or(0); + LercWrap::from_code(code).ok_or(UnsupportedTiff::LercCompression { code }.into()) +} + +/// Refuse a photometric interpretation whose samples need a transform the +/// reader does not apply. +fn check_photometric( + values: &TagValues<'_>, + samples: u16, + compression: Compression, +) -> Result<(), RasterError> { + let default = if samples >= 3 { + PHOTOMETRIC_RGB + } else { + PHOTOMETRIC_BLACK_IS_ZERO + }; + match values.u16_value(PHOTOMETRIC)?.unwrap_or(default) { + PHOTOMETRIC_BLACK_IS_ZERO | PHOTOMETRIC_RGB | PHOTOMETRIC_SEPARATED => Ok(()), + PHOTOMETRIC_YCBCR if compression == Compression::Jpeg => Ok(()), + PHOTOMETRIC_YCBCR => { + let sub = values + .uints(YCBCR_SUBSAMPLING)? + .unwrap_or_else(|| vec![2, 2]); + let horizontal = sub.first().copied().unwrap_or(2); + let vertical = sub.get(1).copied().unwrap_or(2); + if horizontal == 1 && vertical == 1 { + Ok(()) + } else { + Err(UnsupportedTiff::YCbCrSubsampling { + horizontal: u16::try_from(horizontal).unwrap_or(u16::MAX), + vertical: u16::try_from(vertical).unwrap_or(u16::MAX), + } + .into()) + } + } + code => Err(RasterError::UnsupportedPhotometric { code }), + } +} + +/// Strips or tiles, and the stored block size. +fn block_shape( + values: &TagValues<'_>, + width: u32, + height: u32, +) -> Result<(BlockKind, u32, u32), RasterError> { + if values.ifd.entry(TILE_WIDTH).is_some() { + let tile_width = required_u32(values, TILE_WIDTH, "TileWidth")?; + let tile_height = required_u32(values, TILE_LENGTH, "TileLength")?; + return Ok((BlockKind::Tiles, tile_width, tile_height)); + } + let rows = match values.uint(ROWS_PER_STRIP)? { + None => height, + Some(0) => { + return Err(TiffFormatError::TagValue { + tag: ROWS_PER_STRIP, + value: 0, + } + .into()); + } + Some(rows) => u32::try_from(rows).unwrap_or(u32::MAX).min(height), + }; + Ok((BlockKind::Strips, width, rows)) +} + +/// Refuse a JPEG image the decoder cannot produce. +fn check_jpeg( + sample_type: SampleType, + chunk_samples: u16, + width: u32, + height: u32, +) -> Result<(), RasterError> { + if sample_type != SampleType::U8 { + return Err(UnsupportedTiff::JpegSampleType { sample_type }.into()); + } + if chunk_samples != 1 && chunk_samples != 3 { + return Err(UnsupportedTiff::JpegSamples { + samples: chunk_samples, + } + .into()); + } + if width > MAX_JPEG_SIDE || height > MAX_JPEG_SIDE { + return Err(UnsupportedTiff::JpegFrameSize { width, height }.into()); + } + Ok(()) +} diff --git a/nodedb-raster/src/tiff/mask.rs b/nodedb-raster/src/tiff/mask.rs new file mode 100644 index 000000000..89b158b22 --- /dev/null +++ b/nodedb-raster/src/tiff/mask.rs @@ -0,0 +1,225 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! The internal mask of an image: a 1- or 8-bit IFD flagged +//! `NewSubfileType` bit 4. A zero sample masks its pixel out. +//! +//! The mask has its own block grid. The reader names every mask chunk a +//! data block overlaps and fills the block's mask from them. + +use crate::codec::compression::{CODE_NONE, Compression, LercWrap, StreamCodec}; +use crate::codec::predictor::{Predictor, PredictorShape}; +use crate::error::RasterError; +use crate::layout::{BlockKind, BlockWindow}; +use crate::limits::RasterLimits; +use crate::sample::SampleType; +use crate::tiff::byte_order::ByteOrder; +use crate::tiff::error::UnsupportedTiff; +use crate::tiff::image::required_u32; +use crate::tiff::tags::{ + BITS_PER_SAMPLE, COMPRESSION, IMAGE_LENGTH, IMAGE_WIDTH, PREDICTOR, ROWS_PER_STRIP, + STRIP_BYTE_COUNTS, STRIP_OFFSETS, TILE_BYTE_COUNTS, TILE_LENGTH, TILE_OFFSETS, TILE_WIDTH, +}; +use crate::tiff::tile_index::{ChunkIndex, ChunkSpan}; +use crate::tiff::values::TagValues; + +/// A decodable mask IFD. +#[derive(Debug, Clone, PartialEq)] +pub struct MaskImage { + pub width: u32, + pub height: u32, + pub kind: BlockKind, + pub block_width: u32, + pub block_height: u32, + pub blocks_across: u32, + /// 1 or 8. + pub bits: u16, + pub order: ByteOrder, + pub codec: StreamCodec, + pub predictor: Predictor, + pub index: ChunkIndex, +} + +impl MaskImage { + /// Read the mask IFD of `values`. + pub fn read(values: &TagValues<'_>, limits: RasterLimits) -> Result { + let width = required_u32(values, IMAGE_WIDTH, "ImageWidth")?; + let height = required_u32(values, IMAGE_LENGTH, "ImageLength")?; + let bits = values.u16_value(BITS_PER_SAMPLE)?.unwrap_or(1); + if bits != 1 && bits != 8 { + return Err(UnsupportedTiff::MaskBits { bits }.into()); + } + let code = values.u16_value(COMPRESSION)?.unwrap_or(CODE_NONE); + let codec = match Compression::from_code(code, LercWrap::None)? { + Compression::Stream(codec) => codec, + Compression::Jpeg | Compression::Lerc(_) => { + return Err(RasterError::UnsupportedCompression { code }); + } + }; + let compression = Compression::Stream(codec); + let predictor = if bits == 8 && compression.takes_predictor() { + Predictor::from_code(values.u16_value(PREDICTOR)?.unwrap_or(1), SampleType::U8)? + } else { + Predictor::None + }; + let tiled = values.ifd.entry(TILE_WIDTH).is_some(); + let (kind, block_width, block_height) = if tiled { + ( + BlockKind::Tiles, + required_u32(values, TILE_WIDTH, "TileWidth")?, + required_u32(values, TILE_LENGTH, "TileLength")?, + ) + } else { + let rows = values + .uint(ROWS_PER_STRIP)? + .map_or(height, |rows| { + u32::try_from(rows).unwrap_or(u32::MAX).min(height) + }) + .max(1); + (BlockKind::Strips, width, rows) + }; + let blocks_across = width.div_ceil(block_width); + let blocks_down = height.div_ceil(block_height); + let chunks = u64::from(blocks_across) * u64::from(blocks_down); + let mask = Self { + width, + height, + kind, + block_width, + block_height, + blocks_across, + bits, + order: values.order, + codec, + predictor, + index: match kind { + BlockKind::Tiles => ChunkIndex::read( + values, + (TILE_OFFSETS, "TileOffsets"), + (TILE_BYTE_COUNTS, "TileByteCounts"), + chunks, + )?, + BlockKind::Strips => ChunkIndex::read( + values, + (STRIP_OFFSETS, "StripOffsets"), + (STRIP_BYTE_COUNTS, "StripByteCounts"), + chunks, + )?, + }, + }; + let needed_bytes = mask.chunk_bytes(0); + if needed_bytes > limits.block_bytes as u64 { + return Err(RasterError::BlockTooLarge { + needed_bytes, + limit_bytes: limits.block_bytes, + }); + } + Ok(mask) + } + + /// Bytes per stored row: rows pad to whole bytes. + fn row_bytes(&self) -> u64 { + (u64::from(self.block_width) * u64::from(self.bits)).div_ceil(8) + } + + /// Rows of the chunk whose first row is `row_off`. + fn chunk_rows(&self, row_off: u32) -> u32 { + match self.kind { + BlockKind::Tiles => self.block_height, + BlockKind::Strips => self.block_height.min(self.height - row_off), + } + } + + /// Decoded bytes of the chunk whose first row is `row_off`. + fn chunk_bytes(&self, row_off: u32) -> u64 { + self.row_bytes() * u64::from(self.chunk_rows(row_off)) + } + + /// The mask chunks `window` overlaps, as `(chunk index, column offset, + /// row offset)`. + pub fn chunks_for(&self, window: &BlockWindow) -> Vec<(u64, u32, u32)> { + if window.width == 0 + || window.height == 0 + || window.col_off >= self.width + || window.row_off >= self.height + { + return Vec::new(); + } + let last_col = (window.col_off + window.width - 1).min(self.width - 1); + let last_row = (window.row_off + window.height - 1).min(self.height - 1); + let (first_x, last_x) = ( + window.col_off / self.block_width, + last_col / self.block_width, + ); + let (first_y, last_y) = ( + window.row_off / self.block_height, + last_row / self.block_height, + ); + let mut chunks = Vec::new(); + for y in first_y..=last_y { + for x in first_x..=last_x { + let index = u64::from(y) * u64::from(self.blocks_across) + u64::from(x); + chunks.push((index, x * self.block_width, y * self.block_height)); + } + } + chunks + } + + /// The file bytes of chunk `index`. + pub fn span(&self, index: u64) -> Result { + self.index.span(index) + } + + /// Mark the window pixels the chunk at `(col, row)` covers, from its + /// compressed `data`. A sparse chunk masks its pixels out. + pub fn apply_chunk( + &self, + data: Option<&[u8]>, + (col, row): (u32, u32), + window: &BlockWindow, + mask: &mut [bool], + ) -> Result<(), RasterError> { + let rows = self.chunk_rows(row); + let row_bytes = usize::try_from(self.row_bytes()).unwrap_or(usize::MAX); + let raw = match data { + Some(data) => { + let expected = row_bytes.saturating_mul(rows as usize); + let mut raw = self.codec.decompress(data, expected)?; + self.predictor.undo( + &mut raw, + PredictorShape { + row_samples: self.block_width as usize, + stride: 1, + sample_type: SampleType::U8, + order: self.order, + }, + ); + Some(raw) + } + None => None, + }; + let row_end = row.saturating_add(rows).min(window.row_off + window.height); + let col_end = col + .saturating_add(self.block_width) + .min(window.col_off + window.width); + for y in row.max(window.row_off)..row_end { + for x in col.max(window.col_off)..col_end { + let valid = raw.as_deref().is_some_and(|raw| { + let (cx, cy) = ((x - col) as usize, (y - row) as usize); + let line = cy * row_bytes; + if self.bits == 1 { + raw.get(line + cx / 8) + .is_some_and(|byte| byte & (0x80 >> (cx % 8)) != 0) + } else { + raw.get(line + cx).is_some_and(|byte| *byte != 0) + } + }); + let at = (y - window.row_off) as usize * window.width as usize + + (x - window.col_off) as usize; + if let Some(slot) = mask.get_mut(at) { + *slot = valid; + } + } + } + Ok(()) + } +} diff --git a/nodedb-raster/src/tiff/mod.rs b/nodedb-raster/src/tiff/mod.rs new file mode 100644 index 000000000..66368b265 --- /dev/null +++ b/nodedb-raster/src/tiff/mod.rs @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! The TIFF and BigTIFF container: header, IFD chain, tags, block index, +//! internal masks, and the GeoTIFF and GDAL tags. + +pub mod byte_order; +pub mod chain; +pub mod decode; +pub mod error; +pub mod fetch; +pub mod file_header; +pub mod gdal_metadata; +pub mod geo; +pub mod geokeys; +pub mod ifd; +pub mod image; +pub mod mask; +pub mod opener; +pub mod reader; +pub mod samples; +pub mod tags; +pub mod tile_index; +pub mod transform; +pub mod values; + +pub use opener::TiffOpener; +pub use reader::TiffReader; diff --git a/nodedb-raster/src/tiff/opener.rs b/nodedb-raster/src/tiff/opener.rs new file mode 100644 index 000000000..864b16095 --- /dev/null +++ b/nodedb-raster/src/tiff/opener.rs @@ -0,0 +1,207 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! `TiffOpener`: the sans-IO open of a TIFF or BigTIFF image. +//! +//! The first round asks for the first 64 KiB: the header and, in a +//! cloud-optimised GeoTIFF, every IFD. Later rounds ask for IFDs and tag +//! values that lie further. The opener walks the whole IFD chain, picks +//! the full-resolution image or an overview, and the mask IFD of the same +//! size and subfile kind. Georeference and band metadata come from the +//! first image; an overview's geotransform is scaled to its size. + +use nodedb_crs::GeoTransform; + +use crate::error::RasterError; +use crate::header::RasterHeader; +use crate::limits::RasterLimits; +use crate::reader::traits::{Opened, RasterOpener}; +use crate::source::cache::RangeCache; +use crate::source::range::ObjectId; +use crate::tiff::byte_order::ByteOrder; +use crate::tiff::chain::{IfdSummary, read_ifd, walk}; +use crate::tiff::error::TiffFormatError; +use crate::tiff::fetch::Fetcher; +use crate::tiff::file_header::{MAX_HEADER_LEN, parse_header}; +use crate::tiff::geo::GeoInfo; +use crate::tiff::ifd::Ifd; +use crate::tiff::image::TiffImage; +use crate::tiff::mask::MaskImage; +use crate::tiff::reader::TiffReader; +use crate::tiff::tags::{GEO_TAGS, IMAGE_TAGS}; +use crate::tiff::values::{TagValues, prefetch}; + +/// Opens one image of a TIFF object. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct TiffOpener { + object: ObjectId, + image: u32, +} + +impl Default for TiffOpener { + fn default() -> Self { + Self::new() + } +} + +impl TiffOpener { + /// The full-resolution image of object 0. + pub const fn new() -> Self { + Self { + object: ObjectId(0), + image: 0, + } + } + + /// Read `object` instead of object 0. + pub const fn with_object(self, object: ObjectId) -> Self { + Self { + object, + image: self.image, + } + } + + /// Open image `image`: 0 is full resolution, `n` the n-th overview. + pub const fn with_image(self, image: u32) -> Self { + Self { + object: self.object, + image, + } + } +} + +impl RasterOpener for TiffOpener { + type Reader = TiffReader; + + fn open( + &self, + cache: &RangeCache, + limits: RasterLimits, + ) -> Result, RasterError> { + let mut fetcher = Fetcher::new(cache, self.object); + let Some(head) = fetcher.prefix(0, MAX_HEADER_LEN)? else { + return Ok(Opened::Need(fetcher.into_need())); + }; + let file = parse_header(head)?; + let format = file.format; + let Some(chain) = walk(&mut fetcher, format, file.first_ifd, limits.header_bytes)? else { + return Ok(Opened::Need(fetcher.into_need())); + }; + let (first, selected, mask) = pick(&chain, self.image)?; + let first_ifd = read_ifd(&mut fetcher, format, first.offset)? + .ok_or(RasterError::OpenStalled { rounds: 0 })?; + let selected_ifd = read_ifd(&mut fetcher, format, selected.offset)? + .ok_or(RasterError::OpenStalled { rounds: 0 })?; + let mask_ifd = match mask { + Some(summary) => Some( + read_ifd(&mut fetcher, format, summary.offset)? + .ok_or(RasterError::OpenStalled { rounds: 0 })?, + ), + None => None, + }; + prefetch( + &selected_ifd, + &IMAGE_TAGS, + &mut fetcher, + limits.header_bytes, + )?; + prefetch(&first_ifd, &GEO_TAGS, &mut fetcher, limits.header_bytes)?; + if let Some(ifd) = &mask_ifd { + prefetch(ifd, &IMAGE_TAGS, &mut fetcher, limits.header_bytes)?; + } + if fetcher.pending() { + return Ok(Opened::Need(fetcher.into_need())); + } + let read = Reading { + cache, + object: self.object, + order: format.order, + header_bytes: limits.header_bytes, + }; + let image = TiffImage::read(&read.values(&selected_ifd), limits)?; + let geo = GeoInfo::read(&read.values(&first_ifd))?; + let mask = mask_ifd + .as_ref() + .map(|ifd| MaskImage::read(&read.values(ifd), limits)) + .transpose()?; + let transform = geo.transform.map(|gt| { + let mut terms = gt.0; + let sx = first.width as f64 / f64::from(image.layout.width); + let sy = first.height as f64 / f64::from(image.layout.height); + terms[1] *= sx; + terms[2] *= sy; + terms[4] *= sx; + terms[5] *= sy; + GeoTransform(terms) + }); + let header = RasterHeader { + width: image.layout.width, + height: image.layout.height, + outer: Vec::new(), + bands: geo.bands(image.layout.bands, image.layout.sample_type), + transform, + crs: geo.crs.clone(), + }; + Ok(Opened::Ready(TiffReader { + object: self.object, + header, + image, + mask, + geokeys: geo.geokeys, + limits, + })) + } +} + +/// What every tag read of one open round shares. +struct Reading<'a> { + cache: &'a RangeCache, + object: ObjectId, + order: ByteOrder, + header_bytes: usize, +} + +impl<'a> Reading<'a> { + fn values<'b>(&self, ifd: &'b Ifd) -> TagValues<'b> + where + 'a: 'b, + { + TagValues { + ifd, + cache: self.cache, + object: self.object, + order: self.order, + header_bytes: self.header_bytes, + } + } +} + +/// The first image, the selected image, and the selected image's mask. +fn pick( + chain: &[IfdSummary], + image: u32, +) -> Result<(IfdSummary, IfdSummary, Option), RasterError> { + let mut images = chain.iter().filter(|ifd| !ifd.mask); + let first = *images.next().ok_or(TiffFormatError::NoImage)?; + if first.reduced { + return Err(RasterError::ReducedResolutionFirstImage); + } + let count = u32::try_from(chain.iter().filter(|ifd| !ifd.mask).count()).unwrap_or(u32::MAX); + let selected = *chain + .iter() + .filter(|ifd| !ifd.mask) + .nth(image as usize) + .ok_or(RasterError::ImageOutOfRange { + index: image, + count, + })?; + let mask = chain + .iter() + .find(|ifd| { + ifd.mask + && ifd.width == selected.width + && ifd.height == selected.height + && ifd.reduced == selected.reduced + }) + .copied(); + Ok((first, selected, mask)) +} diff --git a/nodedb-raster/src/tiff/reader.rs b/nodedb-raster/src/tiff/reader.rs new file mode 100644 index 000000000..20c479b5f --- /dev/null +++ b/nodedb-raster/src/tiff/reader.rs @@ -0,0 +1,234 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! `TiffReader`: one opened TIFF image, decoded block by block. +//! +//! A block's ranges are its data chunks (one, or one per band when +//! planar) followed by the internal mask chunks it overlaps. The internal +//! mask applies to every band. A LERC mask applies to its own band. + +use crate::block::{BandSamples, RasterBlock}; +use crate::error::RasterError; +use crate::header::RasterHeader; +use crate::layout::{BlockWindow, Interleave, RasterLayout}; +use crate::limits::RasterLimits; +use crate::reader::traits::RasterReader; +use crate::source::range::{ByteRange, ObjectId}; +use crate::tiff::decode::decode_chunk; +use crate::tiff::error::TiffFormatError; +use crate::tiff::geokeys::GeoKeyDirectory; +use crate::tiff::image::TiffImage; +use crate::tiff::mask::MaskImage; +use crate::tiff::samples::{RawChunk, crop_mask, split_bands, zeroed}; +use crate::tiff::tile_index::ChunkSpan; + +/// An opened TIFF image. +#[derive(Debug, Clone)] +pub struct TiffReader { + pub(crate) object: ObjectId, + pub(crate) header: RasterHeader, + pub(crate) image: TiffImage, + pub(crate) mask: Option, + pub(crate) geokeys: Option, + pub(crate) limits: RasterLimits, +} + +/// One validity mask per band, `None` where every pixel of the band is valid. +type BandMasks = Vec>>; + +impl TiffReader { + /// The block grid and band layout. + pub fn layout(&self) -> &RasterLayout { + &self.image.layout + } + + /// The GeoKey directory of the first image, when the file has one. + pub fn geokeys(&self) -> Option<&GeoKeyDirectory> { + self.geokeys.as_ref() + } + + /// True when the image has an internal mask. + pub fn has_mask(&self) -> bool { + self.mask.is_some() + } + + /// The data chunks of block `index`: one, or one per band when planar. + fn data_chunks(&self, index: u32) -> Vec { + let per_plane = u64::from(self.image.layout.block_count()); + match self.image.layout.interleave { + Interleave::Chunky => vec![u64::from(index)], + Interleave::Planar => (0..u64::from(self.image.layout.bands)) + .map(|band| band * per_plane + u64::from(index)) + .collect(), + } + } + + /// The range of `span`, refusing a chunk over the block cap. + fn range_of(&self, span: ChunkSpan) -> Result { + if span.is_sparse() { + return Ok(ByteRange::new(self.object, span.offset, span.offset)); + } + if span.len > self.limits.block_bytes as u64 { + return Err(RasterError::BlockTooLarge { + needed_bytes: span.len, + limit_bytes: self.limits.block_bytes, + }); + } + Ok(ByteRange::new( + self.object, + span.offset, + span.offset + span.len, + )) + } + + /// Decode the data chunks of `window` into bands and their LERC masks, + /// one mask per band. + fn decode_bands( + &self, + index: u32, + window: &BlockWindow, + spans: &[ChunkSpan], + bytes: &[Option<&[u8]>], + ) -> Result<(Vec, BandMasks), RasterError> { + let layout = &self.image.layout; + let (cols, rows) = (window.width as usize, window.height as usize); + let chunk_rows = self.image.chunk_rows(window.row_off); + let mut bands = Vec::with_capacity(usize::from(layout.bands)); + let mut masks = Vec::with_capacity(usize::from(layout.bands)); + for (span, data) in spans.iter().zip(bytes) { + if span.is_sparse() { + for _ in 0..self.image.chunk_samples() { + bands.push(zeroed(layout.sample_type, cols * rows)); + masks.push(Some(vec![false; cols * rows])); + } + continue; + } + let data = present(*data, *span, self.object)?; + let decoded = decode_chunk(&self.image, data, chunk_rows)?; + let chunk = RawChunk { + bytes: &decoded.bytes, + order: decoded.order, + width: layout.block_width as usize, + samples: self.image.chunk_samples(), + sample_type: layout.sample_type, + }; + let needed = chunk.needed(cols, rows); + if decoded.bytes.len() < needed { + return Err(RasterError::ShortBlock { + index, + found: decoded.bytes.len(), + expected: needed, + }); + } + bands.extend(split_bands(&chunk, cols, rows)); + for sample in 0..self.image.chunk_samples() { + masks.push(decoded.masks.get(sample).and_then(|mask| { + mask.as_ref() + .map(|mask| crop_mask(mask, layout.block_width as usize, cols, rows)) + })); + } + } + Ok((bands, masks)) + } +} + +/// The bytes of a fetched chunk: the whole span, or a typed error. +fn present(data: Option<&[u8]>, span: ChunkSpan, object: ObjectId) -> Result<&[u8], RasterError> { + let data = data.ok_or(RasterError::SourceMissing { object: object.0 })?; + if (data.len() as u64) < span.len { + return Err(TiffFormatError::Truncated { + offset: span.offset, + len: span.len, + end: span.offset.saturating_add(data.len() as u64), + } + .into()); + } + Ok(data) +} + +impl RasterReader for TiffReader { + fn header(&self) -> &RasterHeader { + &self.header + } + + fn block_count(&self) -> u32 { + self.image.layout.block_count() + } + + fn block_window(&self, index: u32) -> Result { + self.image.layout.window(index) + } + + fn block_ranges(&self, index: u32) -> Result, RasterError> { + let window = self.image.layout.window(index)?; + let mut ranges = Vec::new(); + for chunk in self.data_chunks(index) { + ranges.push(self.range_of(self.image.index.span(chunk)?)?); + } + if let Some(mask) = &self.mask { + for (chunk, _, _) in mask.chunks_for(&window) { + ranges.push(self.range_of(mask.span(chunk)?)?); + } + } + Ok(ranges) + } + + fn decode_block( + &self, + index: u32, + bytes: &[Option<&[u8]>], + ) -> Result { + let window = self.image.layout.window(index)?; + let data_chunks = self.data_chunks(index); + let mask_chunks = self + .mask + .as_ref() + .map(|mask| mask.chunks_for(&window)) + .unwrap_or_default(); + let expected = data_chunks.len() + mask_chunks.len(); + if bytes.len() != expected { + return Err(RasterError::BlockBuffers { + index, + found: bytes.len(), + expected, + }); + } + let (data_bytes, mask_bytes) = bytes.split_at(data_chunks.len()); + let spans = data_chunks + .iter() + .map(|chunk| self.image.index.span(*chunk)) + .collect::, _>>()?; + let (bands, mut masks) = self.decode_bands(index, &window, &spans, data_bytes)?; + if let Some(mask_image) = &self.mask { + let mut internal = vec![true; window.pixels()]; + for ((chunk, col, row), data) in mask_chunks.iter().zip(mask_bytes) { + let span = mask_image.span(*chunk)?; + let data = if span.is_sparse() { + None + } else { + Some(present(*data, span, self.object)?) + }; + mask_image.apply_chunk(data, (*col, *row), &window, &mut internal)?; + } + for mask in &mut masks { + *mask = Some(match mask.take() { + None => internal.clone(), + Some(lerc) => lerc.iter().zip(&internal).map(|(a, b)| *a && *b).collect(), + }); + } + } + for mask in &mut masks { + if mask + .as_ref() + .is_some_and(|flags| flags.iter().all(|valid| *valid)) + { + *mask = None; + } + } + Ok(RasterBlock { + index, + window, + bands, + masks, + }) + } +} diff --git a/nodedb-raster/src/tiff/samples.rs b/nodedb-raster/src/tiff/samples.rs new file mode 100644 index 000000000..6d4130c6c --- /dev/null +++ b/nodedb-raster/src/tiff/samples.rs @@ -0,0 +1,171 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Raw chunk bytes to typed per-band samples, cropped to the block window. + +use crate::block::BandSamples; +use crate::sample::SampleType; +use crate::tiff::byte_order::ByteOrder; + +/// One decoded chunk: `samples` interleaved samples per pixel, `width` +/// pixels per row, stored in `order`. +#[derive(Debug, Clone, Copy)] +pub struct RawChunk<'a> { + pub bytes: &'a [u8], + pub order: ByteOrder, + pub width: usize, + pub samples: usize, + pub sample_type: SampleType, +} + +impl RawChunk<'_> { + /// Bytes the top-left `cols x rows` window reads. + pub fn needed(&self, cols: usize, rows: usize) -> usize { + if cols == 0 || rows == 0 { + return 0; + } + ((rows - 1) * self.width + cols) * self.samples * self.sample_type.byte_len() + } +} + +/// A fixed-width sample read from bytes in a byte order. +trait Sample: Copy { + const WIDTH: usize; + fn read(raw: &[u8], order: ByteOrder) -> Self; +} + +macro_rules! sample { + ($ty:ty, $width:expr) => { + impl Sample for $ty { + const WIDTH: usize = $width; + fn read(raw: &[u8], order: ByteOrder) -> Self { + let bytes: [u8; $width] = raw.try_into().unwrap_or_default(); + match order { + ByteOrder::Little => <$ty>::from_le_bytes(bytes), + ByteOrder::Big => <$ty>::from_be_bytes(bytes), + } + } + } + }; +} + +sample!(u8, 1); +sample!(i8, 1); +sample!(u16, 2); +sample!(i16, 2); +sample!(u32, 4); +sample!(i32, 4); +sample!(f32, 4); +sample!(f64, 8); + +/// One vector per chunk sample over the top-left `cols x rows` window. +/// The caller checks `chunk.needed(cols, rows) <= chunk.bytes.len()`. +fn split(chunk: &RawChunk<'_>, cols: usize, rows: usize) -> Vec> { + let pixels = cols * rows; + let mut bands: Vec> = (0..chunk.samples) + .map(|_| Vec::with_capacity(pixels)) + .collect(); + let pixel_bytes = chunk.samples * T::WIDTH; + for row in 0..rows { + let start = row * chunk.width * pixel_bytes; + let line = chunk + .bytes + .get(start..start + cols * pixel_bytes) + .unwrap_or_default(); + for pixel in line.chunks_exact(pixel_bytes) { + for (band, raw) in bands.iter_mut().zip(pixel.chunks_exact(T::WIDTH)) { + band.push(T::read(raw, chunk.order)); + } + } + } + bands +} + +/// The window's samples, one [`BandSamples`] per chunk sample. +pub fn split_bands(chunk: &RawChunk<'_>, cols: usize, rows: usize) -> Vec { + match chunk.sample_type { + SampleType::U8 => wrap(split::(chunk, cols, rows), BandSamples::U8), + SampleType::I8 => wrap(split::(chunk, cols, rows), BandSamples::I8), + SampleType::U16 => wrap(split::(chunk, cols, rows), BandSamples::U16), + SampleType::I16 => wrap(split::(chunk, cols, rows), BandSamples::I16), + SampleType::U32 => wrap(split::(chunk, cols, rows), BandSamples::U32), + SampleType::I32 => wrap(split::(chunk, cols, rows), BandSamples::I32), + SampleType::F32 => wrap(split::(chunk, cols, rows), BandSamples::F32), + SampleType::F64 => wrap(split::(chunk, cols, rows), BandSamples::F64), + } +} + +fn wrap(bands: Vec>, variant: fn(Vec) -> BandSamples) -> Vec { + bands.into_iter().map(variant).collect() +} + +/// `pixels` zero samples of `sample_type`. +pub fn zeroed(sample_type: SampleType, pixels: usize) -> BandSamples { + match sample_type { + SampleType::U8 => BandSamples::U8(vec![0; pixels]), + SampleType::I8 => BandSamples::I8(vec![0; pixels]), + SampleType::U16 => BandSamples::U16(vec![0; pixels]), + SampleType::I16 => BandSamples::I16(vec![0; pixels]), + SampleType::U32 => BandSamples::U32(vec![0; pixels]), + SampleType::I32 => BandSamples::I32(vec![0; pixels]), + SampleType::F32 => BandSamples::F32(vec![0.0; pixels]), + SampleType::F64 => BandSamples::F64(vec![0.0; pixels]), + } +} + +/// The top-left `cols x rows` window of a chunk mask `width` pixels wide. +pub fn crop_mask(mask: &[bool], width: usize, cols: usize, rows: usize) -> Vec { + (0..rows) + .flat_map(|row| (0..cols).map(move |col| row * width + col)) + .map(|at| mask.get(at).copied().unwrap_or(false)) + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn chunky_samples_split_and_crop() { + // A 3-pixel-wide chunk of two big-endian u16 bands, 2 rows. + let mut bytes = Vec::new(); + for v in [1u16, 10, 2, 20, 3, 30, 4, 40, 5, 50, 6, 60] { + bytes.extend(v.to_be_bytes()); + } + let chunk = RawChunk { + bytes: &bytes, + order: ByteOrder::Big, + width: 3, + samples: 2, + sample_type: SampleType::U16, + }; + assert_eq!(chunk.needed(2, 2), (3 + 2) * 2 * 2); + assert_eq!( + split_bands(&chunk, 2, 2), + vec![ + BandSamples::U16(vec![1, 2, 4, 5]), + BandSamples::U16(vec![10, 20, 40, 50]) + ] + ); + } + + #[test] + fn floats_and_masks_crop() { + let bytes: Vec = [1.5f64, -2.0] + .iter() + .flat_map(|v| v.to_le_bytes()) + .collect(); + let chunk = RawChunk { + bytes: &bytes, + order: ByteOrder::Little, + width: 2, + samples: 1, + sample_type: SampleType::F64, + }; + assert_eq!(split_bands(&chunk, 1, 1), vec![BandSamples::F64(vec![1.5])]); + assert_eq!( + crop_mask(&[true, false, false, true], 2, 1, 2), + vec![true, false] + ); + assert_eq!(zeroed(SampleType::I8, 2), BandSamples::I8(vec![0, 0])); + } +} diff --git a/nodedb-raster/src/tiff/tags.rs b/nodedb-raster/src/tiff/tags.rs new file mode 100644 index 000000000..fb52ffd99 --- /dev/null +++ b/nodedb-raster/src/tiff/tags.rs @@ -0,0 +1,158 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! TIFF tag codes and field types the reader uses. + +pub const NEW_SUBFILE_TYPE: u16 = 254; +pub const SUBFILE_TYPE: u16 = 255; +pub const IMAGE_WIDTH: u16 = 256; +pub const IMAGE_LENGTH: u16 = 257; +pub const BITS_PER_SAMPLE: u16 = 258; +pub const COMPRESSION: u16 = 259; +pub const PHOTOMETRIC: u16 = 262; +pub const STRIP_OFFSETS: u16 = 273; +pub const SAMPLES_PER_PIXEL: u16 = 277; +pub const ROWS_PER_STRIP: u16 = 278; +pub const STRIP_BYTE_COUNTS: u16 = 279; +pub const PLANAR_CONFIG: u16 = 284; +pub const PREDICTOR: u16 = 317; +pub const TILE_WIDTH: u16 = 322; +pub const TILE_LENGTH: u16 = 323; +pub const TILE_OFFSETS: u16 = 324; +pub const TILE_BYTE_COUNTS: u16 = 325; +pub const SAMPLE_FORMAT: u16 = 339; +pub const JPEG_TABLES: u16 = 347; +pub const YCBCR_SUBSAMPLING: u16 = 530; +pub const MODEL_PIXEL_SCALE: u16 = 33550; +pub const MODEL_TIEPOINT: u16 = 33922; +pub const MODEL_TRANSFORMATION: u16 = 34264; +pub const GEO_KEY_DIRECTORY: u16 = 34735; +pub const GEO_DOUBLE_PARAMS: u16 = 34736; +pub const GEO_ASCII_PARAMS: u16 = 34737; +pub const GDAL_METADATA: u16 = 42112; +pub const GDAL_NODATA: u16 = 42113; +pub const LERC_PARAMETERS: u16 = 50674; + +/// `NewSubfileType` bit of a reduced-resolution image. +pub const SUBFILE_REDUCED: u64 = 1; +/// `NewSubfileType` bit of a transparency mask. +pub const SUBFILE_MASK: u64 = 4; +/// `SubfileType` value of a reduced-resolution image. +pub const OLD_SUBFILE_REDUCED: u64 = 2; + +/// Tags whose out-of-line values the image and mask decode reads. +pub const IMAGE_TAGS: [u16; 9] = [ + BITS_PER_SAMPLE, + SAMPLE_FORMAT, + STRIP_OFFSETS, + STRIP_BYTE_COUNTS, + TILE_OFFSETS, + TILE_BYTE_COUNTS, + JPEG_TABLES, + YCBCR_SUBSAMPLING, + LERC_PARAMETERS, +]; + +/// Tags whose out-of-line values the georeference read takes from the +/// first image. +pub const GEO_TAGS: [u16; 8] = [ + MODEL_PIXEL_SCALE, + MODEL_TIEPOINT, + MODEL_TRANSFORMATION, + GEO_KEY_DIRECTORY, + GEO_DOUBLE_PARAMS, + GEO_ASCII_PARAMS, + GDAL_METADATA, + GDAL_NODATA, +]; + +/// The block index tags, bounded by the header cap instead of the tag +/// value cap. +pub const fn is_block_index(tag: u16) -> bool { + matches!( + tag, + STRIP_OFFSETS | STRIP_BYTE_COUNTS | TILE_OFFSETS | TILE_BYTE_COUNTS + ) +} + +/// A TIFF field type. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FieldType { + Byte, + Ascii, + Short, + Long, + Rational, + SByte, + Undefined, + SShort, + SLong, + SRational, + Float, + Double, + Ifd, + Long8, + SLong8, + Ifd8, +} + +impl FieldType { + /// The field type of code `code`, or `None` for an unknown code. + pub const fn from_code(code: u16) -> Option { + match code { + 1 => Some(FieldType::Byte), + 2 => Some(FieldType::Ascii), + 3 => Some(FieldType::Short), + 4 => Some(FieldType::Long), + 5 => Some(FieldType::Rational), + 6 => Some(FieldType::SByte), + 7 => Some(FieldType::Undefined), + 8 => Some(FieldType::SShort), + 9 => Some(FieldType::SLong), + 10 => Some(FieldType::SRational), + 11 => Some(FieldType::Float), + 12 => Some(FieldType::Double), + 13 => Some(FieldType::Ifd), + 16 => Some(FieldType::Long8), + 17 => Some(FieldType::SLong8), + 18 => Some(FieldType::Ifd8), + _ => None, + } + } + + /// The type code. + pub const fn code(self) -> u16 { + match self { + FieldType::Byte => 1, + FieldType::Ascii => 2, + FieldType::Short => 3, + FieldType::Long => 4, + FieldType::Rational => 5, + FieldType::SByte => 6, + FieldType::Undefined => 7, + FieldType::SShort => 8, + FieldType::SLong => 9, + FieldType::SRational => 10, + FieldType::Float => 11, + FieldType::Double => 12, + FieldType::Ifd => 13, + FieldType::Long8 => 16, + FieldType::SLong8 => 17, + FieldType::Ifd8 => 18, + } + } + + /// Bytes per value. + pub const fn width(self) -> u64 { + match self { + FieldType::Byte | FieldType::Ascii | FieldType::SByte | FieldType::Undefined => 1, + FieldType::Short | FieldType::SShort => 2, + FieldType::Long | FieldType::SLong | FieldType::Float | FieldType::Ifd => 4, + FieldType::Rational + | FieldType::SRational + | FieldType::Double + | FieldType::Long8 + | FieldType::SLong8 + | FieldType::Ifd8 => 8, + } + } +} diff --git a/nodedb-raster/src/tiff/tile_index.rs b/nodedb-raster/src/tiff/tile_index.rs new file mode 100644 index 000000000..c37c16e79 --- /dev/null +++ b/nodedb-raster/src/tiff/tile_index.rs @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! The block index: `StripOffsets`/`TileOffsets` and their byte counts, +//! kept as the file stores them and decoded per lookup. + +use crate::error::RasterError; +use crate::tiff::byte_order::ByteOrder; +use crate::tiff::error::TiffFormatError; +use crate::tiff::tags::FieldType; +use crate::tiff::values::TagValues; + +/// One offset or byte count array. +#[derive(Debug, Clone, PartialEq, Eq)] +struct ChunkTable { + bytes: Vec, + width: usize, + order: ByteOrder, +} + +impl ChunkTable { + fn read( + values: &TagValues<'_>, + tag: u16, + name: &'static str, + chunks: u64, + ) -> Result { + let (entry, bytes) = values + .raw(tag)? + .ok_or(TiffFormatError::MissingTag { tag, name })?; + let width = match entry.kind { + FieldType::Short => 2, + FieldType::Long | FieldType::Ifd => 4, + FieldType::Long8 | FieldType::Ifd8 => 8, + FieldType::Byte + | FieldType::Ascii + | FieldType::Rational + | FieldType::SByte + | FieldType::Undefined + | FieldType::SShort + | FieldType::SLong + | FieldType::SRational + | FieldType::Float + | FieldType::Double + | FieldType::SLong8 => { + return Err(TiffFormatError::FieldType { + tag, + kind: entry.kind.code(), + } + .into()); + } + }; + if entry.count < chunks { + return Err(TiffFormatError::TagCount { + tag, + found: entry.count, + expected: chunks, + } + .into()); + } + Ok(Self { + bytes: bytes.into_owned(), + width, + order: values.order, + }) + } + + fn get(&self, index: u64) -> Option { + let at = usize::try_from(index).ok()?.checked_mul(self.width)?; + self.order.uint_at(&self.bytes, at, self.width) + } +} + +/// Where each chunk lies in the file. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ChunkIndex { + offsets: ChunkTable, + counts: ChunkTable, + chunks: u64, +} + +/// One chunk's file bytes. A zero offset or length marks a chunk the file +/// omits. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ChunkSpan { + pub offset: u64, + pub len: u64, +} + +impl ChunkSpan { + /// True for a chunk the file omits. + pub const fn is_sparse(&self) -> bool { + self.offset == 0 || self.len == 0 + } +} + +impl ChunkIndex { + /// Read the index of `chunks` chunks from the offset and byte count + /// tags. + pub fn read( + values: &TagValues<'_>, + (offsets_tag, offsets_name): (u16, &'static str), + (counts_tag, counts_name): (u16, &'static str), + chunks: u64, + ) -> Result { + Ok(Self { + offsets: ChunkTable::read(values, offsets_tag, offsets_name, chunks)?, + counts: ChunkTable::read(values, counts_tag, counts_name, chunks)?, + chunks, + }) + } + + /// The file bytes of chunk `chunk`. + pub fn span(&self, chunk: u64) -> Result { + let lookup = |table: &ChunkTable| { + table.get(chunk).ok_or(TiffFormatError::TagCount { + tag: 0, + found: chunk, + expected: self.chunks, + }) + }; + let offset = lookup(&self.offsets)?; + let len = lookup(&self.counts)?; + if offset.checked_add(len).is_none() { + return Err(TiffFormatError::ChunkOverflow { chunk, offset, len }.into()); + } + Ok(ChunkSpan { offset, len }) + } +} diff --git a/nodedb-raster/src/transform.rs b/nodedb-raster/src/tiff/transform.rs similarity index 69% rename from nodedb-raster/src/transform.rs rename to nodedb-raster/src/tiff/transform.rs index 4e97b0790..0f2db2832 100644 --- a/nodedb-raster/src/transform.rs +++ b/nodedb-raster/src/tiff/transform.rs @@ -1,21 +1,18 @@ // SPDX-License-Identifier: Apache-2.0 -//! The pixel-to-world affine transform, from the GeoTIFF model tags. +//! The pixel-to-world [`GeoTransform`], from the GeoTIFF model tags. //! -//! The transform uses GDAL order: `x = gt[0] + col·gt[1] + row·gt[2]` and -//! `y = gt[3] + col·gt[4] + row·gt[5]`, with `(col, row)` the pixel corner. +//! The transform maps the pixel corner `(col, row)` to world coordinates. //! Tag precedence follows GDAL: a tiepoint with a pixel scale wins over //! `ModelTransformationTag`. +use nodedb_crs::GeoTransform; + use crate::error::RasterError; /// `GTRasterTypeGeoKey` value of a raster whose tags name pixel centres. const PIXEL_IS_POINT: u16 = 2; -/// A GDAL-order affine geotransform from pixel corner to world coordinates. -#[derive(Debug, Clone, Copy, PartialEq)] -pub struct GeoTransform(pub [f64; 6]); - /// The georeferencing tags of IFD 0, as read from the file. #[derive(Debug, Clone, Copy, Default)] pub struct ModelTags<'a> { @@ -29,51 +26,36 @@ pub struct ModelTags<'a> { pub raster_type: Option, } -impl GeoTransform { - /// The transform the tags define, or `None` when the file carries no - /// model tags. - pub fn from_tags(tags: ModelTags<'_>) -> Result, RasterError> { - let base = match (tags.pixel_scale, tags.tiepoints, tags.transformation) { - (Some(scale), Some(tiepoints), _) => from_tiepoint(scale, tiepoints)?, - (Some(_), None, None) => return Err(RasterError::PixelScaleWithoutTiepoint), - (_, _, Some(matrix)) => from_matrix(matrix)?, - (None, Some(_), None) => return Err(RasterError::GroundControlPoints), - (None, None, None) => return Ok(None), - }; - let transform = if tags.raster_type == Some(PIXEL_IS_POINT) { - base.shifted_to_corner() - } else { - base - }; - transform.check()?; - Ok(Some(transform)) - } - - /// The world coordinate of the pixel corner `(col, row)`. - pub fn apply(&self, col: f64, row: f64) -> (f64, f64) { - let [x0, xc, xr, y0, yc, yr] = self.0; - (x0 + col * xc + row * xr, y0 + col * yc + row * yr) - } - - /// Move a PixelIsPoint origin from the first pixel's centre to its - /// corner, half a pixel up and left. - fn shifted_to_corner(self) -> Self { - let mut gt = self.0; - gt[0] -= 0.5 * gt[1] + 0.5 * gt[2]; - gt[3] -= 0.5 * gt[4] + 0.5 * gt[5]; - Self(gt) +/// The transform the tags define, or `None` when the file carries no model +/// tags. +pub fn transform_from_tags(tags: ModelTags<'_>) -> Result, RasterError> { + let base = match (tags.pixel_scale, tags.tiepoints, tags.transformation) { + (Some(scale), Some(tiepoints), _) => from_tiepoint(scale, tiepoints)?, + (Some(_), None, None) => return Err(RasterError::PixelScaleWithoutTiepoint), + (_, _, Some(matrix)) => from_matrix(matrix)?, + (None, Some(_), None) => return Err(RasterError::GroundControlPoints), + (None, None, None) => return Ok(None), + }; + let transform = if tags.raster_type == Some(PIXEL_IS_POINT) { + shifted_to_corner(base) + } else { + base + }; + if !transform.is_invertible() { + return Err(RasterError::DegenerateTransform { + transform: transform.0, + }); } + Ok(Some(transform)) +} - /// Refuse a non-finite term or a transform with a zero determinant. - fn check(&self) -> Result<(), RasterError> { - let gt = self.0; - let determinant = gt[1] * gt[5] - gt[2] * gt[4]; - if gt.iter().all(|term| term.is_finite()) && determinant != 0.0 && determinant.is_finite() { - Ok(()) - } else { - Err(RasterError::DegenerateTransform { transform: gt }) - } - } +/// Move a PixelIsPoint origin from the first pixel's centre to its corner, +/// half a pixel up and left. +fn shifted_to_corner(gt: GeoTransform) -> GeoTransform { + let mut terms = gt.0; + terms[0] -= 0.5 * terms[1] + 0.5 * terms[2]; + terms[3] -= 0.5 * terms[4] + 0.5 * terms[5]; + GeoTransform(terms) } /// `GT = [X − I·Sx, Sx, 0, Y + J·Sy, 0, −Sy]` from the first tiepoint. @@ -129,14 +111,14 @@ mod tests { #[test] fn no_model_tags_is_no_transform() { - assert_eq!(GeoTransform::from_tags(tags()).ok(), Some(None)); + assert_eq!(transform_from_tags(tags()).ok(), Some(None)); } #[test] fn tiepoint_and_scale() { let scale = [30.0, 30.0, 0.0]; let tie = [0.0, 0.0, 0.0, 600_000.0, 450_000.0, 0.0]; - let gt = GeoTransform::from_tags(ModelTags { + let gt = transform_from_tags(ModelTags { pixel_scale: Some(&scale), tiepoints: Some(&tie), ..tags() @@ -150,7 +132,7 @@ mod tests { fn tiepoint_at_a_nonzero_pixel_moves_the_origin() { let scale = [10.0, 5.0, 0.0]; let tie = [2.0, 3.0, 0.0, 1000.0, 2000.0, 0.0]; - let gt = GeoTransform::from_tags(ModelTags { + let gt = transform_from_tags(ModelTags { pixel_scale: Some(&scale), tiepoints: Some(&tie), ..tags() @@ -158,14 +140,14 @@ mod tests { .expect("valid") .expect("present"); assert_eq!(gt.0, [980.0, 10.0, 0.0, 2015.0, 0.0, -5.0]); - assert_eq!(gt.apply(2.0, 3.0), (1000.0, 2000.0)); + assert_eq!(gt.pixel_to_world(2.0, 3.0), (1000.0, 2000.0)); } #[test] fn pixel_is_point_shifts_half_a_pixel() { let scale = [10.0, 10.0, 0.0]; let tie = [0.0, 0.0, 0.0, 100.0, 200.0, 0.0]; - let gt = GeoTransform::from_tags(ModelTags { + let gt = transform_from_tags(ModelTags { pixel_scale: Some(&scale), tiepoints: Some(&tie), raster_type: Some(PIXEL_IS_POINT), @@ -184,7 +166,7 @@ mod tests { 0.0, 0.0, 0.0, 0.0, // 0.0, 0.0, 0.0, 1.0, ]; - let gt = GeoTransform::from_tags(ModelTags { + let gt = transform_from_tags(ModelTags { transformation: Some(&m), ..tags() }) @@ -201,7 +183,7 @@ mod tests { 0.0, 0.0, 0.0, 0.0, // 0.0, 0.0, 0.0, 1.0, ]; - let gt = GeoTransform::from_tags(ModelTags { + let gt = transform_from_tags(ModelTags { transformation: Some(&m), raster_type: Some(PIXEL_IS_POINT), ..tags() @@ -219,7 +201,7 @@ mod tests { m[2] = 4.0; m[15] = 1.0; assert!(matches!( - GeoTransform::from_tags(ModelTags { + transform_from_tags(ModelTags { transformation: Some(&m), ..tags() }), @@ -228,7 +210,7 @@ mod tests { m[2] = 0.0; m[15] = 2.0; assert!(matches!( - GeoTransform::from_tags(ModelTags { + transform_from_tags(ModelTags { transformation: Some(&m), ..tags() }), @@ -240,7 +222,7 @@ mod tests { fn tiepoints_without_scale_are_gcps() { let tie = [0.0, 0.0, 0.0, 1.0, 2.0, 0.0, 10.0, 10.0, 0.0, 5.0, 6.0, 0.0]; assert!(matches!( - GeoTransform::from_tags(ModelTags { + transform_from_tags(ModelTags { tiepoints: Some(&tie), ..tags() }), @@ -252,7 +234,7 @@ mod tests { fn scale_without_tiepoint_is_refused() { let scale = [1.0, 1.0, 0.0]; assert!(matches!( - GeoTransform::from_tags(ModelTags { + transform_from_tags(ModelTags { pixel_scale: Some(&scale), ..tags() }), @@ -265,7 +247,7 @@ mod tests { let scale = [1.0]; let tie = [0.0; 6]; assert!(matches!( - GeoTransform::from_tags(ModelTags { + transform_from_tags(ModelTags { pixel_scale: Some(&scale), tiepoints: Some(&tie), ..tags() @@ -274,7 +256,7 @@ mod tests { )); let zero_scale = [0.0, 1.0, 0.0]; assert!(matches!( - GeoTransform::from_tags(ModelTags { + transform_from_tags(ModelTags { pixel_scale: Some(&zero_scale), tiepoints: Some(&tie), ..tags() @@ -283,7 +265,7 @@ mod tests { )); let nan_scale = [f64::NAN, 1.0, 0.0]; assert!(matches!( - GeoTransform::from_tags(ModelTags { + transform_from_tags(ModelTags { pixel_scale: Some(&nan_scale), tiepoints: Some(&tie), ..tags() @@ -292,7 +274,7 @@ mod tests { )); let short_matrix = [1.0; 9]; assert!(matches!( - GeoTransform::from_tags(ModelTags { + transform_from_tags(ModelTags { transformation: Some(&short_matrix), ..tags() }), diff --git a/nodedb-raster/src/tiff/values.rs b/nodedb-raster/src/tiff/values.rs new file mode 100644 index 000000000..117c2073b --- /dev/null +++ b/nodedb-raster/src/tiff/values.rs @@ -0,0 +1,248 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Typed tag values of one IFD, read from the open cache. +//! +//! An open round first calls [`prefetch`] for the tags it reads, so every +//! out-of-line value is cached before [`TagValues`] decodes it. Each value +//! is bounded: the block index by the header cap, any other tag by the tag +//! value cap. + +use std::borrow::Cow; + +use crate::error::RasterError; +use crate::limits::MAX_TAG_VALUE_BYTES; +use crate::source::cache::{Lookup, RangeCache}; +use crate::source::range::{ByteRange, ObjectId}; +use crate::tiff::byte_order::ByteOrder; +use crate::tiff::error::TiffFormatError; +use crate::tiff::fetch::Fetcher; +use crate::tiff::ifd::{Entry, Ifd, ValueLocation}; +use crate::tiff::tags::{FieldType, is_block_index}; + +/// The byte cap of `tag`'s value. +fn value_cap(tag: u16, header_bytes: usize) -> usize { + if is_block_index(tag) { + header_bytes + } else { + MAX_TAG_VALUE_BYTES + } +} + +/// Refuse a value over its cap. +fn check_cap(entry: &Entry, header_bytes: usize) -> Result<(), RasterError> { + let limit_bytes = value_cap(entry.tag, header_bytes); + if entry.byte_len() > limit_bytes as u64 { + return Err(RasterError::TagValueTooLarge { + tag: entry.tag, + bytes: entry.byte_len(), + limit_bytes, + }); + } + Ok(()) +} + +/// Record every out-of-line value of `tags` in `ifd` the cache lacks. +pub fn prefetch( + ifd: &Ifd, + tags: &[u16], + fetcher: &mut Fetcher<'_>, + header_bytes: usize, +) -> Result<(), RasterError> { + for &tag in tags { + let Some(entry) = ifd.entry(tag) else { + continue; + }; + check_cap(entry, header_bytes)?; + if let ValueLocation::At { offset, len } = entry.location { + fetcher.bytes(offset, len)?; + } + } + Ok(()) +} + +/// An entry and its value bytes. +pub type RawValue<'a> = (Entry, Cow<'a, [u8]>); + +/// Typed access to the cached tag values of one IFD. +#[derive(Debug, Clone, Copy)] +pub struct TagValues<'a> { + pub ifd: &'a Ifd, + pub cache: &'a RangeCache, + pub object: ObjectId, + pub order: ByteOrder, + pub header_bytes: usize, +} + +impl<'a> TagValues<'a> { + /// The value bytes of `tag`, when present. + pub fn raw(&self, tag: u16) -> Result>, RasterError> { + let Some(entry) = self.ifd.entry(tag).copied() else { + return Ok(None); + }; + check_cap(&entry, self.header_bytes)?; + let bytes = match entry.location { + ValueLocation::Inline { raw, len } => { + Cow::Owned(raw.get(..len).unwrap_or_default().to_vec()) + } + ValueLocation::At { offset, len } => { + let range = ByteRange::new(self.object, offset, offset.saturating_add(len)); + match self.cache.get(range) { + Lookup::Bytes(bytes) if bytes.len() as u64 == len => Cow::Borrowed(bytes), + Lookup::Bytes(bytes) => { + return Err(TiffFormatError::Truncated { + offset, + len, + end: offset.saturating_add(bytes.len() as u64), + } + .into()); + } + Lookup::Absent => { + return Err(RasterError::SourceMissing { + object: self.object.0, + }); + } + Lookup::Missing => return Err(RasterError::OpenStalled { rounds: 0 }), + } + } + }; + Ok(Some((entry, bytes))) + } + + /// The unsigned integers of `tag` (BYTE, SHORT, LONG, LONG8, IFD, + /// IFD8), when present. + pub fn uints(&self, tag: u16) -> Result>, RasterError> { + let Some((entry, bytes)) = self.raw(tag)? else { + return Ok(None); + }; + let width = match entry.kind { + FieldType::Byte | FieldType::Undefined => 1, + FieldType::Short => 2, + FieldType::Long | FieldType::Ifd => 4, + FieldType::Long8 | FieldType::Ifd8 => 8, + FieldType::Ascii + | FieldType::Rational + | FieldType::SByte + | FieldType::SShort + | FieldType::SLong + | FieldType::SRational + | FieldType::Float + | FieldType::Double + | FieldType::SLong8 => return Err(field_type(&entry)), + }; + let values = bytes + .chunks_exact(width) + .filter_map(|raw| self.order.uint_at(raw, 0, width)) + .collect(); + Ok(Some(values)) + } + + /// The first unsigned integer of `tag`, when present. + pub fn uint(&self, tag: u16) -> Result, RasterError> { + match self.uints(tag)? { + None => Ok(None), + Some(values) => values.first().copied().map(Some).ok_or( + TiffFormatError::TagCount { + tag, + found: 0, + expected: 1, + } + .into(), + ), + } + } + + /// The first value of `tag` as a u16, refusing larger values. + pub fn u16_value(&self, tag: u16) -> Result, RasterError> { + self.uint(tag)? + .map(|value| { + u16::try_from(value).map_err(|_| TiffFormatError::TagValue { tag, value }.into()) + }) + .transpose() + } + + /// The SHORT values of `tag`, when present. + pub fn u16s(&self, tag: u16) -> Result>, RasterError> { + let Some((entry, bytes)) = self.raw(tag)? else { + return Ok(None); + }; + if entry.kind != FieldType::Short { + return Err(field_type(&entry)); + } + let values = bytes + .as_chunks::<2>() + .0 + .iter() + .map(|raw| self.order.u16(*raw)) + .collect(); + Ok(Some(values)) + } + + /// The numbers of `tag` as doubles (DOUBLE, FLOAT, SHORT, LONG), when + /// present. + pub fn f64s(&self, tag: u16) -> Result>, RasterError> { + let Some((entry, bytes)) = self.raw(tag)? else { + return Ok(None); + }; + let order = self.order; + let values = match entry.kind { + FieldType::Double => bytes + .as_chunks::<8>() + .0 + .iter() + .map(|raw| f64::from_bits(order.u64(*raw))) + .collect(), + FieldType::Float => bytes + .as_chunks::<4>() + .0 + .iter() + .map(|raw| f64::from(f32::from_bits(order.u32(*raw)))) + .collect(), + FieldType::Short | FieldType::Long => { + let width = entry.kind.width() as usize; + bytes + .chunks_exact(width) + .filter_map(|raw| order.uint_at(raw, 0, width)) + .map(|value| value as f64) + .collect() + } + FieldType::Byte + | FieldType::Ascii + | FieldType::Rational + | FieldType::SByte + | FieldType::Undefined + | FieldType::SShort + | FieldType::SLong + | FieldType::SRational + | FieldType::Ifd + | FieldType::Long8 + | FieldType::SLong8 + | FieldType::Ifd8 => return Err(field_type(&entry)), + }; + Ok(Some(values)) + } + + /// The ASCII text of `tag` without its NUL terminators, when present. + pub fn ascii(&self, tag: u16) -> Result, RasterError> { + let Some((entry, bytes)) = self.raw(tag)? else { + return Ok(None); + }; + if !matches!( + entry.kind, + FieldType::Ascii | FieldType::Byte | FieldType::Undefined + ) { + return Err(field_type(&entry)); + } + let text = std::str::from_utf8(&bytes) + .map_err(|_| TiffFormatError::AsciiEncoding { tag })? + .trim_end_matches('\0'); + Ok(Some(text.to_owned())) + } +} + +fn field_type(entry: &Entry) -> RasterError { + TiffFormatError::FieldType { + tag: entry.tag, + kind: entry.kind.code(), + } + .into() +} diff --git a/nodedb-raster/tests/fixtures/cog-mask-0.mask.z b/nodedb-raster/tests/fixtures/cog-mask-0.mask.z new file mode 100644 index 0000000000000000000000000000000000000000..f7dcd68344313b3bff512c20f931789f9c232d57 GIT binary patch literal 35 tcmV+;0Nnq0+U?6R4FCWL12E>_GzPc{A?@D;uBw6WZ+!sxsR6003popf4`*qEI&NX4cc$hq9f zt9(m9V>DT_v_Q+XPFuA{hjmgHbY1uKRB!ZIKNW~VA~KMNQq-UsA^DRCjn_2I(PFLA zM(xmk4eE?8>y{qsh2HC%{%9Ctk%E6HL6hF*+d3X90pd@a*jZP9KW(g~f{HQm({ zz1AoFP(dUTkd9oGpc+l+!~n)Hixq6+2$y(3yY}g*PV16x>Vcl?oxbX~1`vZ}WT61% zs6#7yFpNnoU>$on#SNbE30RaR+N$p8v0mw;hV)M(5RWwEpcqwXLWB4$VL$=(SUaJVHDF?!X^%Ijyt^K8v%>4WXrMwE4MmpwH_O`Nn5aW+p|-< zv1W8(5aXD`Dt2&;D?B2EKSWr(rCE*@Ta`6hhxJ>~W^CEE?9eXk-rnrT!Whd%Y+xT} zxWx;;K#R5{%d~tevs!DhZX2=*o3}OFwG+FxC;KpABomm<|Gp>!}6@uYOLA1Y|zGS&Q@*5 zj_t}GEo6Td!FZ-IhsCU7BRkm7AZNJDEgtfM_10#+Heyq@XdAX~XLf5Z_GM%=lbFeT zma&#C?B)e6r8-1-{(h*t2~a7{x?pFps6IVKcip$Z^hbl{-A<6(1SmA0vFcPxCpx z*jM>R-{Jdx(8n@`|5(Th*0YVh9N`ofxxsy&@s=;LrV?fA!x! z5SZiw*SW`2-td{9gpcxxKEvnvQeWeneU~5f<9^Ps`W=7lul%DA`9B{Kh!3O%astJH I|2{W~dE}H?ZvX%Q literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/cog-mask-2.mask.z b/nodedb-raster/tests/fixtures/cog-mask-2.mask.z new file mode 100644 index 0000000000000000000000000000000000000000..8002112eeb79f35772bc55640072bbbe358ef4b6 GIT binary patch literal 18 Zcmb=pm7Jo&Z_vOXXSbJu!Nr7OI{-Kj1w{Y= literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/cog-mask-2.raw.z b/nodedb-raster/tests/fixtures/cog-mask-2.raw.z new file mode 100644 index 000000000..ae1f95aa8 --- /dev/null +++ b/nodedb-raster/tests/fixtures/cog-mask-2.raw.z @@ -0,0 +1,3 @@ +xÚÁt`àÿÞ÷îNAƒ`Á` ‚ AAAAƒ Á`Á A ‚ :g߇� ¥P �0˰ §ðQE#,г°ûp <áuô0Å'~ð1Ç{H£€ +Zâß8à†ŸùÆ&œ³‹ VØá 0ËkìpÌ%·<ñÁŒ]‘dže6ØçŒkîyažìÕêֳ©µ9â‚ygÚ +V±– íþí`7Kùe9{±wëÚÄV¶³³Á³^òšw|ìMØܾì׮–ô¼—½á}ŸùÚ÷~ñ„Nö°Œ½êmùÂ7~ô»§UPE- U÷žOýÓüϥœ^ô®®&Zi§³~ó”žõ¦¦šëK¿º*ó±±kêh¬¥¶:é¡L,ÆjlÇQ\ÄM<Æ{üÎíaÿ \ No newline at end of file diff --git a/nodedb-raster/tests/fixtures/cog-mask.tif b/nodedb-raster/tests/fixtures/cog-mask.tif new file mode 100644 index 0000000000000000000000000000000000000000..3214fe3c0e18aeaf6c819ef3beec143598eb28e7 GIT binary patch literal 3324 zcmcgteNYo;9)33j2m!)J>eorl8bHpXk}5qoCCM&iOIHGsBuJ^FD+to!0dtjVsT|n> z6}@&!IxWrFYny)2R=pXuJ)-)G;CXPin)hvc1|EW)x}m@Nts}?TR6MXZmSl`*|j!%bk@qTWSWQx ztV5{cE1db!C8wR`t*~Zg9Zn%%D6VmG4#Cc?<5(wWQI}QNsw)J(qQq9dj&<_IR!-n7 zypy+87%OaQx}0^C!K#z9!xPqX7NMGVJ|ghtY$<22G1hPnHA+W_ia`{-BLkqK+yGn= z<*C49qFe<$F3QQ7In#K?G;WM?1?(w~a-vf*jaw)sN`kqrC%L<4UFeym4<76Jb)aPb`l+6H_G4*v=6i%8+h8`^5?Yujo~ zYML(M+ZvkbmfGe9qu#cjv#;m5YKYUpmant&6{UK*zO}jbi58*KCRkZ0@vc~LO7#<0y!qtdJNEzV}Q+Q6HJk~RptgDXo z#G|<=4vImc=QBxg6x!!fhR3Cu)uj%)FrUt&tDNQZGp#KRbbet)M`X4GX;27+As!+R z2fw@aWZz7+Cui?fdvf+(wHK$={^(t`7oqUC-4eAaw=|(q=ktMo-<>Lg=2WK=PCAu5 z{{R$)i}J7GetM^_fJkO`OZtAwE$Q+9m)o=9Y$MKJxy$YOJKV;?>bDm4g4BpOmy4%^Z{h{5qOzVWqyWGMpIu386NkFIxa0-yAMyL}o1}I2G z=q11;U@bI6Kfs7g@HKzy`ybG8!ug|sWH{Rd_&q=mXFCC7fSY$$p@6LH}CkTj_m8;{{ziTU{Ugbq0(X=J5@eT)&oG4=uxH;e~Vk z0iP}8o8O0V^E*2Z$+{Qu@Tp6f54cDivwhwW;>XgLj}wp-pMLcw_ENEO&gYFeL)a#S zemp<+?alW+*HvjyKN64RJ#&HtdvY}xCP!PTz zzhd8`Lj+5ssbK7i%ev|X<-N}w3LLyL$ez;OiYAWyqw9EdF@Z6BU`zex(}{Cb(wNrW zn@J00Os9ql{$1&BO><+O+0>Qh8~7;|G$;c7O80W?y$7CJL@ z@e17pfhjzfON4V$-GYq?Wl+HrN`L$Of1TN;bzj%hUY;537Ud@ufzv9NKlKvp3s0|aI(HwdboO{HM6e>8P^xM+hE}nV4dx39YHPv3A2pm-U zH>jk%CA4cFh<7d1*a&9IJ0e|HUkAPys-)o@+Se<}8x;YUN-A6RwdaJM)+LDO213%_SAiv*UB@dF0lhz@Cl{=M7rBo%!F1^?TgpT0rXyD z$`*o%@egrp%HEF7w_0WUa!m`t?13lN*Ur=^HPGa< zG|Jvcqwph|rwC@3UL&0Z*Cv|AUU48Jx3hhNBJiNne>g!J*3-ThMLAg!7*CL%JvQMf z$fkA0%;2v?ISXp(`zmD@!R*qj>Knjyj7qw!bx&r}zL!OLfg*4uLD~*tqgnJM%a~ph eI~Gzw_`&z4x?NW0_5JtziqOY}nr?!br2hwj1U_Z} literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/gdal-metadata.tif b/nodedb-raster/tests/fixtures/gdal-metadata.tif new file mode 100644 index 0000000000000000000000000000000000000000..586dcef0f7db06d22551975f7e24aec65fc822d6 GIT binary patch literal 1369 zcmaiyZ%9*77{=dSV`NY)qy!1MUJxXj+g)W1w&8Gd3~kyrAwjUqcGZQObGxe~=pPjn zeGvK(^dSfeB7&koC_F{9$?iRz+K2peYc8Lc>rZ)nE_{6HtJ;TKw9UsW;Y^H>9CZ-BD_&bE>Zx3CU3?qCzWxCep-5f0+)9^z~-adv>R_yiZ7!B@EHJieosuHq*`bQ8Z3 zAw2R!Wp)%ArFny~F0wQ~u=_ll|4Y^r3it<)>4uin z49&MgmOWuZPsUotiI-2l{Hc`8O6swIjiS1qma|ZwIV|Sf81X!qbfc-|7-DXAz3XqZwq<0piQTYIdPrS z>|Js^r)4JfD%!>RbwG7GonlXYX09@}oS9wyhqErEim?*XVKMRVj4IY@MrpBE*r|$r TOQ)vv)Z~;=F5_`*J-GA-R41r- literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/gen_fixtures.py b/nodedb-raster/tests/fixtures/gen_fixtures.py new file mode 100644 index 000000000..cca38fbca --- /dev/null +++ b/nodedb-raster/tests/fixtures/gen_fixtures.py @@ -0,0 +1,287 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 +"""Generate the TIFF fixtures of the nodedb-raster file tests. + +Run from any directory: + + python -I gen_fixtures.py [output-dir] + +The output directory defaults to the directory of this script. It needs +numpy, tifffile, imagecodecs and rasterio (GDAL 3.8 or later). The script +writes every fixture, checks each one by reading it back with GDAL or +tifffile, and writes manifest.tsv. + +Pattern fixtures hold value(band, y, x) of the counter +band * 97 + y * 31 + x * 7 + 1, cast with wrapping to the sample type +after the per-type offset below (floats: counter * 0.25 - 40). The Rust +tests compute the same pattern. Other fixtures store the expected samples +GDAL decodes as .raw.z (band-sequential, little-endian, zlib) and the +expected mask as .mask.z (one byte per pixel, 1 for valid, zlib). +""" + +import os +import sys +import warnings +import zlib + +import numpy as np +import rasterio +import rasterio.shutil +import tifffile +from rasterio.transform import from_origin + +warnings.filterwarnings("ignore", category=rasterio.errors.NotGeoreferencedWarning) + +OUT = os.path.abspath(sys.argv[1] if len(sys.argv) > 1 else os.path.dirname(__file__)) +OFFSETS = {"uint8": 0, "int8": 60, "uint16": 0, "int16": 300, "uint32": 0, "int32": 70000} +ROWS = [] + + +def pattern(bands, height, width, dtype): + b, y, x = np.meshgrid(np.arange(bands), np.arange(height), np.arange(width), indexing="ij") + counter = (b * 97 + y * 31 + x * 7 + 1).astype(np.int64) + if dtype in ("float32", "float64"): + return (counter * 0.25 - 40.0).astype(dtype) + bits = np.dtype(dtype).itemsize * 8 + wrapped = (counter - OFFSETS[dtype]) % (1 << bits) + unsigned = wrapped.astype(np.uint64).astype(f"uint{bits}") + return unsigned.view(dtype) + + +def path(name): + return os.path.join(OUT, name) + + +def row(rid, file, image, dtype, width, height, bands, expect="pattern", mask="-", tol=0): + ROWS.append((rid, file, str(image), dtype, str(width), str(height), str(bands), expect, mask, str(tol))) + + +def write_z(name, raw): + with open(path(name), "wb") as out: + out.write(zlib.compress(raw, 9)) + return name + + +def write_raw(rid, data): + raw = np.ascontiguousarray(data.astype(data.dtype.newbyteorder("<"))).tobytes() + return write_z(f"{rid}.raw.z", raw) + + +def write_mask(rid, mask): + return write_z(f"{rid}.mask.z", (np.asarray(mask) != 0).astype(np.uint8).tobytes()) + + +def matrix(): + """Every stream codec x predictor x strips/tiles x classic/BigTIFF x + chunky/planar, alternating the byte order.""" + width, height, bands = 20, 18, 2 + codecs = { + "none": None, + "lzw": "lzw", + "deflate": "adobe_deflate", + "packbits": "packbits", + "zstd": "zstd", + } + flip = False + for codec, tf_codec in codecs.items(): + predictors = [("p1", None, "uint16")] + if codec in ("lzw", "deflate", "zstd"): + predictors += [("p2", "horizontal", "uint16"), ("p3", "floatingpoint", "float32")] + for pname, predictor, dtype in predictors: + for layout in ("strip", "tile"): + for big in (False, True): + for planar in ("contig", "separate"): + flip = not flip + order = ">" if flip else "<" + rid = f"m-{codec}-{pname}-{layout}-{'big' if big else 'classic'}-{planar}-{'be' if flip else 'le'}" + data = pattern(bands, height, width, dtype) + array = data if planar == "separate" else np.moveaxis(data, 0, -1) + kwargs = dict( + bigtiff=big, + byteorder=order, + photometric="minisblack", + planarconfig=planar, + compression=tf_codec, + predictor=predictor, + metadata=None, + ) + if layout == "tile": + kwargs["tile"] = (16, 16) + else: + kwargs["rowsperstrip"] = 5 + tifffile.imwrite(path(rid + ".tif"), array, **kwargs) + back = tifffile.imread(path(rid + ".tif")) + back = back if planar == "separate" else np.moveaxis(back, -1, 0) + assert np.array_equal(back, data), rid + row(rid, rid + ".tif", 0, dtype, width, height, bands) + + +def sample_types(): + """Each sample type once, deflate tiles.""" + width, height = 20, 18 + for dtype in ("uint8", "int8", "int16", "uint32", "int32", "float64"): + rid = f"t-{dtype}" + data = pattern(1, height, width, dtype) + tifffile.imwrite( + path(rid + ".tif"), data[0], tile=(16, 16), compression="adobe_deflate", + photometric="minisblack", metadata=None, + ) + assert np.array_equal(tifffile.imread(path(rid + ".tif")), data[0]), rid + row(rid, rid + ".tif", 0, dtype, width, height, 1) + + +def gdal_write(name, data, **profile): + bands, height, width = data.shape + profile = dict(driver="GTiff", width=width, height=height, count=bands, dtype=data.dtype.name, **profile) + with rasterio.open(path(name), "w", **profile) as dst: + dst.write(data) + + +def lerc(): + """LERC, LERC_DEFLATE and LERC_ZSTD, chunky and planar, strips and + tiles; a float band with NaN gives a LERC mask.""" + width, height, bands = 20, 18, 2 + for comp in ("LERC", "LERC_DEFLATE", "LERC_ZSTD"): + for interleave in ("pixel", "band"): + for tiled in (True, False): + rid = f"l-{comp.lower()}-{interleave}-{'tile' if tiled else 'strip'}" + data = pattern(bands, height, width, "uint16") + extra = dict(tiled=True, blockxsize=16, blockysize=16) if tiled else dict(blockysize=5) + gdal_write(rid + ".tif", data, compress=comp, max_z_error=0, interleave=interleave, **extra) + with rasterio.open(path(rid + ".tif")) as src: + assert np.array_equal(src.read(), data), rid + row(rid, rid + ".tif", 0, "uint16", width, height, bands) + rid = f"l-{comp.lower()}-mask" + data = pattern(1, height, width, "float32") + data[0, 3:6, 4:9] = np.nan + gdal_write(rid + ".tif", data, compress=comp, max_z_error=0, tiled=True, blockxsize=16, blockysize=16) + valid = ~np.isnan(data[0]) + filled = np.where(valid, data[0], 0).astype("float32") + row(rid, rid + ".tif", 0, "float32", width, height, 1, write_raw(rid, filled[None]), write_mask(rid, valid)) + rid = "l-lerc-bigtiff" + data = pattern(1, 18, 20, "int16") + gdal_write(rid + ".tif", data, compress="LERC", max_z_error=0, tiled=True, blockxsize=16, blockysize=16, BIGTIFF="YES") + row(rid, rid + ".tif", 0, "int16", 20, 18, 1) + + +def smooth(bands, height, width): + b, y, x = np.meshgrid(np.arange(bands), np.arange(height), np.arange(width), indexing="ij") + return (60 + 40 * np.sin(x / 5.0 + b) + 30 * np.cos(y / 4.0 - b)).astype("uint8") + + +def jpeg(): + """JPEG YCbCr tiles with JPEGTables, gray strips, and RGB tiles. The + expected samples are GDAL's (libjpeg) decode.""" + cases = [ + ("j-ycbcr-tile", 3, dict(tiled=True, blockxsize=16, blockysize=16, photometric="YCBCR")), + ("j-gray-strip", 1, dict(blockysize=8)), + ("j-rgb-tile", 3, dict(tiled=True, blockxsize=16, blockysize=16, photometric="RGB")), + ] + for rid, bands, extra in cases: + data = smooth(bands, 24, 40) + gdal_write(rid + ".tif", data, compress="JPEG", jpeg_quality=90, interleave="pixel", **extra) + with rasterio.open(path(rid + ".tif")) as src: + decoded = src.read() + row(rid, rid + ".tif", 0, "uint8", 40, 24, bands, write_raw(rid, decoded), tol=3) + + +def cog(): + """A COG with two overview levels and an internal mask.""" + width, height = 64, 48 + data = pattern(1, height, width, "uint16") + mask = np.full((height, width), 255, dtype=np.uint8) + mask[10:20, 30:50] = 0 + src_name = path("cog-src.tif") + profile = dict( + driver="GTiff", width=width, height=height, count=1, dtype="uint16", + crs="EPSG:32648", transform=from_origin(600000, 450000, 30, 30), + tiled=True, blockxsize=16, blockysize=16, + ) + with rasterio.Env(GDAL_TIFF_INTERNAL_MASK=True): + with rasterio.open(src_name, "w", **profile) as dst: + dst.write(data) + dst.write_mask(mask) + rasterio.shutil.copy( + src_name, path("cog-mask.tif"), driver="COG", COMPRESS="DEFLATE", PREDICTOR="2", + BLOCKSIZE="16", OVERVIEWS="AUTO", RESAMPLING="NEAREST", OVERVIEW_COUNT="2", + ) + os.remove(src_name) + for level in (None, 0, 1): + image = 0 if level is None else level + 1 + with rasterio.open(path("cog-mask.tif"), overview_level=level) as src: + values = src.read() + valid = src.read_masks(1) + h, w = src.height, src.width + rid = f"cog-mask-{image}" + expect = "pattern" if image == 0 else write_raw(rid, values) + row(rid, "cog-mask.tif", image, "uint16", w, h, 1, expect, write_mask(rid, valid)) + + +def landsat(): + """A Landsat Collection 2 Level-2 shaped band: uint16 COG, 256x256 + deflate tiles with predictor 2, overviews, nodata 0, UTM, 30 m.""" + width, height = 300, 260 + y, x = np.mgrid[0:height, 0:width] + data = (7273 + 3 * x + 2 * y).astype("uint16")[None] + data[0, :, :12] = 0 + src_name = path("landsat-src.tif") + profile = dict( + driver="GTiff", width=width, height=height, count=1, dtype="uint16", nodata=0, + crs="EPSG:32648", transform=from_origin(399960, 5000040, 30, 30), + ) + with rasterio.open(src_name, "w", **profile) as dst: + dst.write(data) + dst.scales = (2.75e-05,) + dst.offsets = (-0.2,) + dst.set_band_description(1, "SR_B4") + rasterio.shutil.copy( + src_name, path("landsat-c2.tif"), driver="COG", COMPRESS="DEFLATE", PREDICTOR="2", + BLOCKSIZE="256", OVERVIEWS="AUTO", RESAMPLING="NEAREST", + ) + os.remove(src_name) + with rasterio.open(path("landsat-c2.tif")) as src: + assert src.overviews(1), "landsat fixture has overviews" + assert np.array_equal(src.read(), data) + row("landsat-c2", "landsat-c2.tif", 0, "uint16", width, height, 1, write_raw("landsat-c2", data)) + with rasterio.open(path("landsat-c2.tif"), overview_level=0) as src: + row("landsat-c2-1", "landsat-c2.tif", 1, "uint16", src.width, src.height, 1, write_raw("landsat-c2-1", src.read())) + + +def metadata(): + """Per-band GDAL_METADATA: description, scale, offset, units, a band + nodata item and a valid range, over a shared GDAL_NODATA.""" + data = pattern(3, 6, 8, "int16") + name = path("gdal-metadata.tif") + profile = dict(driver="GTiff", width=8, height=6, count=3, dtype="int16", nodata=-1) + with rasterio.open(name, "w", **profile) as dst: + dst.write(data) + dst.scales = (0.01, 1.0, 0.5) + dst.offsets = (273.15, 0.0, -10.0) + dst.units = ("K", "", "m") + for band, desc in enumerate(("temperature", "quality", "height"), start=1): + dst.set_band_description(band, desc) + dst.update_tags(2, NODATA="-9999") + dst.update_tags(3, valid_range="0,1000") + tags = tifffile.TiffFile(name).pages[0].tags + assert 42112 in tags and 42113 in tags + row("gdal-metadata", "gdal-metadata.tif", 0, "int16", 8, 6, 3) + + +def main(): + os.makedirs(OUT, exist_ok=True) + matrix() + sample_types() + lerc() + jpeg() + cog() + landsat() + metadata() + with open(path("manifest.tsv"), "w") as out: + out.write("# id\tfile\timage\tdtype\twidth\theight\tbands\texpect\tmask\ttol\n") + for fields in ROWS: + out.write("\t".join(fields) + "\n") + print(f"{len(ROWS)} fixtures in {OUT}") + + +if __name__ == "__main__": + main() diff --git a/nodedb-raster/tests/fixtures/j-gray-strip.raw.z b/nodedb-raster/tests/fixtures/j-gray-strip.raw.z new file mode 100644 index 0000000000000000000000000000000000000000..802366556aad586159ae78c4a2d4578ca767f7ad GIT binary patch literal 869 zcmV-r1DgDJ+C7s8R~mT~#b*;yfkiL|1Qf_pq!(xC!%(Ktr8yu~LBL3fNQt0g@GIx6 zyT_o1{Tt8O_W|yG@4nwH);ojA-H*lNg+C4brK6t@Uj@oi84X zXO~x#+lR$sF`te5t}_dgc-YP&z2-pwE8ci*qO=au6m ziMbJ*)nY~57?H@88^G^&{*Wm)zV=5q)7k9%b+273q#^;2-D)%#j26W04+Ft=Z!nq8 z61mFx*Z%crJi6{()Qi~|?nNy61C3gvH^ZJ#G<|Z`?hZy{D#=vp7u{aJ-)WtdvT?$P zn6*lUT&7TJO)!ebGNrTTWv`Es$>U9=TMN+xS0DD8xOrcV5 zww!)CnLVjgYqfGd1JvwhwOqU-;BN_b#4?o$LGgHoDOGDWSCCF*n0z6hOT|c^!*n2( zh*@k7o4YO)inS&?N<@!x`GWq?6Qts)bS9mQl76Q}rxdd{IUgbD1DC@Q|D`rNFd~vn zXH*6Rz z{_|`3?-JVF`Ycr&fiFRa-*f}y|99wBd)wSKX!ZKM`nbl~mVVK_-4Tw(byl~Zj3!g* zbTUF<4vR)EI_iv^fALQ*`p}#wVO2iA^~@EV`Gym5bmjs zh$j?DW*Nqb(TQxKT&Y$|`AiJ=Atu%S?kD~hZ|l?UzVi2jQ%tc^1svo62j{KUI|p9G zpq5KSLZL_^SLtC7{?0*@N-*VmyVL7+FPo=DpoqdItx_(N$=|sG0M?q9-CjIfu3z*9 z!{P9%3)+#1gxs)Mr&g&nIy3AJMbd@SR_AIs1iW7KhLfAw^qu=78gs#By-o*kc42ff vUu|^(_tQ82hc|cg`TTY==rqc?1Oa+vwV16i_fS3`=mH@JuplpyTW+5lq0rb2EBcmY0|04|IApe1_g%M0F9PB^|X#oaC7Le27fQ^BX ziCIL+@c%6a4o09JdxkSkXEctrojr1~uvKEy%^k~=mAQ=d_{_% zyXWN8o}Bi!*UYKY%gQ8jvu4bv>5rF6T4-H+98^$l!n1elA*H{&7cCG_^1CI<858g= z-ikQSFhT-RN`Qfh85CAn}?^Tr~B*kPNP=LV1Ad?Y%-ZwoW4l9P^ov$dUuacg-Wy2?cLr#+}~cFwsy;F zsj$b68Vz~_YWIYatD8Ge@8Q22;->IzU;vpBRk%@Wp95#n1lB&(l zK#VAsx9bh77fa>V%av+%vp_&Li%uru&N8Q`n6q4=RExTT2_nBysTys5EJ@@G#lmVP z9(J#26}$!RIE_x5oM5xL%UX*o6icrbiYk-CA4z1gL^c%*d#!qC?58^ zR&%V@(USb zQFu_#;SEMGp5WE!X&!f-OLFZ^c?m6Xq#97FoQUizbBbQW(>NzQ@C z6H0^K8;+$i*=#Br@~r6PBK9a`MNbeXlw5|L0S8tMuJ zSgjwl+KJre_Fns>+wC5=z#T+1U^nZOGO1Lq)S2ypXtr2u93FSOpx3>2_u}fhH`IL^ z^VrNfl~M_Ewt28rq0(r9?t7&EyH~gO_xCp!Cx<(mtH}`f$e`C5%<%4s+y?mZ{QB;$ z1KRJNwDz_(h*;2t>QoAuj4bK&$B6YB*%3JB=wP>dd41ixIBi33Y20r|^;)$`twZhL z-D0KQJ_W1wuFj7RdbbY)>mJsjWCTi@^m-`i3CF>@2d9@{-TS-V+0hL!;qktA-fmX% zY24>rF&m90i^Gd0^ObrFQ1I~dNVb0a@cgf*hwF>ZUbT>k0x~Q^WZ)@4OZ)s9(mXvv zz5gHQ@ADMI1X zOCS$9FYNCO_SYQj&xz`Ei>f8L+-P$6qC~LqNPM6Ae$DL!Q*fROu0y7a)si=dS$2HZ0v4r#HeZtynCqWfEa8m|Q&1qEQ))(eckS zLLdeNW&kb4d@hM6(gcxB$6zCRg_z5v(?^g`qcj$aBarJMkVH#1lfVLzXd)SphI}@Y za&cjXK92O?zxNS@&YA~HTD>8#WFiJ=4@B^R#VvZdkTXpgd4K=;_VZ<&GQ}1tbQYIC zjNy1B=(d5^2gopKB)r+rW5`?o+t=402xW4HFIAiDo*)n<p`p@gH z@9!fc{~H$w<2d$FcZ%|<|MvCgdw*mU5G2Gb?@JWclyHy77mL@+jyW8dUY&_sF zYosEcV0MnjmdMm5ColuhvI{b)mDX0X@eq79bDv9n_Hz^f+U7QA_kf*$+ZBzc(zn+9JD&^!@Zr&Jjte`cC)q)G;^R@ zKD}+ASu7!H5D{602pn3P`j(09r^yrSQ9(^G>r?NMnA-3QD2?(xvj%Ivf}GkIzpJ zx0lBUI~zm-SU|>Q@PN~5A+HvvAC8AwqjN?gf~T|lSq8zBjX17oPL09jm&rV zSHAf5)#>m4Y^aI6T0Z(a9-!~GN2V#JqZi%IU|yeNIIrFeXaSV|HSab#?{Q}j7EOM! zbgJdS)q*M#9Hn zaYohR!Yq@5ynrVWWQxgMG^=gy5FSg!qgVhSE9bMr0hdLm5OO9*Mqc|re*DuP8)5Jl z77Z3SR)9QM&|@{Kmbk2-&uP}m1anL((*O4I^6xifVtR_d1lI^$EkFvtdqux2<}d?Z zK#+_-J4zXSeSi7Ve;q|AJibI_vcc6tqUCp7Xjqp$O9S%#{O8LnGDVx^3w7l214m6H o1nsMrxHI_gIarjjk^ajov`3rfiM4v0=L0R^2pNCwzsAWlvqA7?uK)l5 literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/j-rgb-tile.tif b/nodedb-raster/tests/fixtures/j-rgb-tile.tif new file mode 100644 index 0000000000000000000000000000000000000000..e202b1809817bd27c040403f0181ea5c7f281929 GIT binary patch literal 1656 zcmb7E3rtg27(Vy*zMv0!+g|AK1w|AgBebBX6O~8hC770nb>JosBRY(zV}cto2;(72 zAmE4_j}FFy;No0~h@iY<3ls2B24(7IKm;phXvmz8?o_+XW^sxCz5hL5&bjxT|NH*` zzoDV-fC2z;0YF#)fyfOyV!>jFQHWV^Y=q9T%##ok7K}p7Sg`rMg%)hyE5L&J5C^`2 zgAfc5aIPJ?9L}5ZkI)0$$jHGb5uoTk82n2s) z5hbJuJY6Iq1wv<;e}KsVI4mUy1_1zU0IJ#%fwZt$yfga7^&Kx&E*09$tg>kW_xMU6 zjc67x%@kBuH12Asd6bfwnK#0t-P%sXO4psryA)>pbG-@R3oolej5R4pR-VHtTs;Nm)U*JjOGH*n@2{Jq)oc(S68UBPe$MeBc(nl5_a`02<0>7|xGb26Rf-5IM%otvU6|@+qSQ$Bd;}Jls|iWtf$@iMO4+(L&Z0kmaN<)$Eb%! zT9ueADitqLM}PEexvoyxLZ#*g)TXcF?hO?iZdH~&A>Ync41WgJZAu~pg+jU><~C&! z_`iLMpe!7AE(z-!@}-6T3LC~*D)YAH+P(557O$8)^aoZMeE8(e%Fxm1TVMY?Hj%>_ zSL>cNbw|2;$`8BKNeRQEez)5(66#|`+;{Eq-A-v;LrQLQ)fv69yr;B!_xUTQb<0{7 zw>tENEo;>tyA;$(ecrnwB14TY{-SdYN5074LA3eO^NlO3l^O?Ozt{JJzpp%MSC%E8 z{Yk$dL#=y|lYwYVU?${Vv0j*QKr__k{In)$LGatP`0pPJCrC@tB8XMu0fJ&U!6ISV z`Y87cp$y*s3TqvX_X~upV$=TeuG|!pan6%5ChqAf@JpLs+c1!J>N;}qtYdWLWAbbx ZQ!YD`8hv+->uBwW-LaBnP*!Fd`x|aEWXz?Q>3y(f(=551*|F-uwd#$5fZA9kQnq{``}U3mXqSmU7Y_t{_c1F|KE4= zl~#9hdA(dN=Hp?fUdcs#P8Vd->I{(Eg&x+5_0!?>YOyFDxBHXpND8%Y(bjo#>Td2>^!wtM5N#qI5KJ{@)%WfBj-Zim%uw%Hw!2ai-5-O=p& zW_eg_b_N&otLv-TdGDlJBoLp+VKwRuU`Lb9Pvonu-gtI7k7r7?PJcAH7!SH9)qIA+ zJdjZ>ktmcZjZ*ImrH+7bG#XQ}OtE&->Gsb~8kNIj1oJt}YS9jVUo4kPwJ;G)6)LT> zeisQwQu#`)(WqDQ**JyUOnSA5&*AWPC33085g@2!u6*36JADM5JS>z;g`;$gL}9&J zalmIYAF;W+dqRl`f-x$d$rlS2$dA*BbS9fghC_(QuF;CN@2xPt_V59RwX!8r+1)4^ zOJy=TvkM5Lu|zx^L}9yGCuZ|FOoiH{T)EK>c8tVh3Z2#E50Z3*0y4;`Rq}Zo zTkwy6_3lr7vduN#zSivY;$)bTsm+iVBSN-SdYRNwvmmoGkk+o|3?*iq>r zHxeX6QkBULqj)eFM7>U{PPxZoZo@zP^Yj0F^mA-)i@mPY+FSuVNQe~%t0#aGI3932 z%o_Pi3TD6CI|`A(Riwmk75X9H$v)-RW8I}J-oll zTIY#mb{86oCDUoUhls|~ncPt^880NF!JuCwQAHSR<_c?rFV@>!{!k=+c$kOLXeO60 zRgNq9X6-0JGP*fB!FaT}&f)G!b#^!qjHdIIY89n3rAn>d>NU!vB5;C-bMFCTMSK8E zpf*|^ZZeuJ)LJKPDp9DnI%oa!c5Bhd=i)`krV~q4R=HGVusOYc;N$vfZ!m}--r4!$ z5~O1?sAQw32%)s`(#v~Hmmt0g?g<+SwvMI&yjK>>bpr4e|8jOUM!>%;M6eBSRg$J5LC<#g0* zRRPla*V;j|eII-}i#l8Jn? zKbc=GZkE^c$zZ;?eR8wBoOD~|Of=|oyCJi|XmNTmI$dg?UoNk2pWH64E{5Rw%9ER` zNxxmm#X4Q!Du`h_S=nCr`zkE zf`dp=sN14Z90=B>60T@hta1>k(^9A1?RHO3YUOgR(X7`>hY1>WSu~P;K8v@`T!qap za^K+#rpw3mMx$0fy7OMCl+Pw;%x%_6ce$L6wXZXL!UKULA@_!|*`s2qn9rqv_vF|e;F&L`-ZHYRvCh`W8scbHjN`ymz9r0L%#JrF}zQ<)T`6B)%0}?*w zN!?t9i=ZO0L@Yw#2oBIflbFwGR*QGmnGfK1KYafuaV2|I5$9+e7#gC(bcjG;EJ#sQ z2=PKDsai76>_Q{JE|Nb41!Q9vpYphNFcAZou-efYO zzj^Py541avwst`-ZJq#V$X5uZBe7UC8YcV(xm~!q#?&&|LIHQ}AxOE}=t4o;lJOWF zMB>RzHl0e)h(+gD?F+cuj7ML8$b7^VfbHyl3M}U`X@Gbz63WFwE*bJWJvNz;E4jC^ zvccZwOEngEfJ^}HfaRl9tOCxteq72$Xs;etE5vNpCU1RH1pd^G(eYfdTC3Nq<)d^m z84n>2y=%9^=7kLJvuzRtKA4( z2(0XPm^4a>_+WQOC{o*fI2Fy5+x@}dtW&Ol+HtR~ebzqCCxTvwS+7>fB?oea!4V+i zx!TESe!W;Mo6SZk9mYI^A(-OX2nqwV=ru~28H^ne$?9o;wzz%uwby&yPQ3_(lZ#Qe zQApg4N~^`FQ+qugKN&AJd*|cX?E3cA(Rr_Zd=w{VmluPRN+yi^JdhpS3$VZ)3d*g) zO2oS)SJ^XBu}dFMDA2?k&X1fby~FaphF>u0^?^73}Em|l##%~Co7bi>n1 zCIZmna9B+Sug5`=fmp0i>rWSNOqUl^P**Wck0&FbPX-b2A!Yy#yUFV^JExP^zEvKL zM#JIx-EAco3l4|9Ry7yKUv23*@2v02 zlW+a(op)e|LTt=4)J7;k5TQs6MZ7k>T*w2r3D~3rW)^#1`SoXCeE!lP>`yV6yGkQ` z7xA#)=eDRsf(^e9vYJ)A4Zb4s?O*=m+0%{&TudJDcNIn_5~6}Zj~g;7_IMn^Z_?^j zJkF*h`qpnhdH(G6@c!!dz5t9kFHTVe4s?2%aGQmC4Qj1Iu*u%@ee;8VJ^yDH;=e4G zOC)9w9*)oeJTU03h`&w*YE&<>yw}8cVF!YMm$@i$@4N5CF}S3by|T HwPrQC5Stsa literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/j-ycbcr-tile.tif b/nodedb-raster/tests/fixtures/j-ycbcr-tile.tif new file mode 100644 index 0000000000000000000000000000000000000000..e852b77485dd7122421823aff0f33b5592235f7e GIT binary patch literal 1736 zcmb7EYiv|S6h3oj?!CL+2Yc_{-4?8Db}q@(Y#`Mgc z-0#kvIrE+Kof*IX3D5z+JODz0AXGEFreVF`t(j$J|$D z99s})O~cuQ0bh3+dk&6s9L8RNnESYey@<&0F}_{sNEIjuhy`q#Ajan3!EbK5SZjty z$#*aYe?cQO;)+t7z8hV67{DVUKniX_VvVoi+@kUKp8xrd4n?m*7EnRp4;sTq)QKWv zVv;ONI%AU2Xh<>`OlH+$GAm|-!D6>4R?TL!8RsRZ*tHZ@vuWH3p}2;LdQsGCCWA?v zvqguIO$ZbnSinMw3j~Ewg+zY@J8H(GnlhP6IKB{5FVTcw0+n2D!ZeQ;|J!CR0}yd| zfdUxplruz(idM}Xy#hu`a9^rG2K;;=_wBqx#?Rz|PxZ=`6OL~#d3LoG2AdasUm#u2 zdDc{#F+1aAUQP)+h?15|==H~*o;DJQ;20V0SL75~qO zfXo%u4U*I9@&?{bjNqr`qdyc6wimv+>B!iGexSB$@{e63%g>I!b>4qz9ehxI!?Lus z|BF&r%gBrR$lj3A$_<62e-}$phJ-A~?`nG1+ zCl{V+yKAZtUpqG!%Kyr_gj1%>aGx-qJ}M|GD(aLqwe$3vMjUcwHnMZ%_) z15W`^#VNT}Zy=HWh4Yb3<+=S$TKjL6XCoU=uDZ8zknQ;L*yCzATz;{yzO};~?hkf$ zjxF1>-XH8v+j3-JJmZs2==AlN&EARY*T#>j)$gw@D=(gVpjfwr5;>vtD3d^x71im| z*)y{P7>$;27I{(_jye^*oG6_pP^{)bajMq*l9@%@^ZU8Wqc``Zt?15bd3|N!i3Mr{ zTU}7MeQ9HJ(KcUi-DdwUtGk}x-<1TLx30aGnp?ZG>h@P@&s6WYqHF zbV8g54%Jh>jt#d?&{{akC BT-g8s literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/l-lerc-band-strip.tif b/nodedb-raster/tests/fixtures/l-lerc-band-strip.tif new file mode 100644 index 0000000000000000000000000000000000000000..ac26641c8d8e1e1c13e759e1497035de39dca188 GIT binary patch literal 1769 zcmebD)MDUZU|`^5U|?isU<9&6fS3`=76P)FfNUlpEdYefP;vJT1_m}L8>B~&kp*nZ zF(6wMDh|>k24%AX+2TMp$fj*TEQ6#b2+9TuGXw+KAbpHL*sz3&p$N!+2gIL_vVg^( z0ofq1n_&;bK8AiEy%$U;039I>)Oa0;r!q4zsIV|F+y~-0K#QZ;7#M&|FtBlVarE&` zElEsCEJ?KEQc$q*EJ@8($V<#kwN-NV_YDYg4G#A7_lpm54RZ|&_H^>}@eGMnQb@_q zP0Y-*Rr2(8ba#yp4haf%4hao%RkHK(4-WPL>ao!W8Dz(0qmOC>gHLKvvXKJFe%6gz zYe3-u4qqUh0>q&3*bT%$CquyldkEtLgjOhU0Pz`tvB2QS*rCC|&L=9PtYs*#rfXvD zJuE5n2{fwl3i5U(B40G8bef$UKl)Aaf8-Kb^cz z1)J0N0G)>6^oC@xlNl~lg6TO7bAe8uL%efbuMn@6lZTOz+Yh~WsmJE_ec0SyPz-VV zfkrTG&S=5Jz-UfFxU=SypqO(xLD%oyytEG#!mvbR17w2|O+OH0guFtn15lWOVMPm= zzQi~c==w{H?Gg>miw{YzOp*`o-X^TJaGz4~D&EESS-F^S$Z$F_wetY=d@|gc0aVO| zNC-1nxCF5UCe}3E5CL`ugFzXXPGU#~CWIv7BiD5n@oGIU5lRP@^I4j)IejlMz%W8` zK|aLk59-16Uxt4`r~f71Ijp9nB!gUn$-uT&P!5~pu_odMNXBD00Z9fk7-s^L!3+`{ w#Cm}Q#he^Wgo56FfmJBciFigMBB}R$_A;E zL1H^W*#bauXC$@@BMaEpOF(f~pg70{hk!T;ss_kp2nMo2<}m_c!xAQjA|U%05Pv$# z0v7)SWP?Br<6DOF47CikP&x_~4MBQ0I`)AXg}q*Ad1csam!(amr^5}?b~Pzh zr>&LVS$gxp+~V;5`wp$Hmee!L@hA1nVl||qClO))k$qnrwy=K-3|q{w2ZcBg>}YZT zQ49>>z_4fNbSV7r#}J&5Cg@da0TUPl!}^@BO2AYGjN4U((!iv~!0`6PTVYV7GVrKp z@o<6|+NsZ2xIhfA#Yum`3^qrGE{Dv6rPJd)qavzxGq{jJlDwxHF@a*1z=1OdyJr7if)3Ya<9l+)=RDwLa z%0cks!|(GOd7V5Swn|rNBz5YXTai4$ktfJ6icRy{oo0wyT%;?=0L?=ci~C)bPCl6& z9JMladdfP@^m)V@z=Y%-x9_D}v3aK!Xgf~ttY~q7IBqJ$JDiD&elDBaAonU`siHFj zLuxxNrvuA4WXqTIzTAV=a%`ElAp&CQ1Bm5G40C{%Cs8kRxXz-US&UIs%xJ8WsAw1> zAuCK>cMMzL)w9qNkO)49r@*N=kbjn1z(grMX)9uz415uD#Ia05*r=7Btre zrExx+nRhHweO}E!%M}ZIgLbW4L2RZ2=h`l}u9wS~ExV<)X3M=@tJfbjvR=E5SOZWp j&W2t=Y~I0}Yd5q*z4Hy4YY(Y$zuo$9(eHJ8iO978PlXt? literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/l-lerc-bigtiff.tif b/nodedb-raster/tests/fixtures/l-lerc-bigtiff.tif new file mode 100644 index 0000000000000000000000000000000000000000..ce21cf2c4771a88f6521a69479f6137c53624e3a GIT binary patch literal 1388 zcmebD)MnsdU|C}cwtgPALe zMN9^Zm=ls1%noNPVlIpV3@lK0SwJmu1*(II)iT~=IL%PQ5R9ak397zf2@^vRlpO)p z`{^hP156%f22=^dDX1EEW{8L?RKW)*ZR76Z=;NDOl9-ZMl4!@JpkU)!lA5cKmzbMs ztK{tO8xZ6g9PH`u7a!yr<{A|2>E!9-84{_akdmL9n3-p*xHe<*MO(O3WRGcqt<{nzQhz-O`IKw?XmguKs|1sk0`gf#5B3}!A;<(Xw3 zdt%MR$?O*t-D}u1yINjOUKuvqWoc9C>2SleT}_JBX=|l-mfk!tw>Z52zC$Z3?|W|A@|1H*eg#wY+sI|dm5 literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/l-lerc-mask.raw.z b/nodedb-raster/tests/fixtures/l-lerc-mask.raw.z new file mode 100644 index 0000000000000000000000000000000000000000..dbc1ecafa2103b468f17d724aaefb3b292ff4fc9 GIT binary patch literal 662 zcmV;H0%`qt+HKQqXq9CE$KhL)617k=8?rH zZaI5`$nfNzrVy%TyUuG3R8v72B@~ci0)x5gOcnX0osBu}NQF?$Vg6?LoDkM~ZxQ)S zqrb0Ia^7~9Gv%}qoE=9tFH_70hPk8HW(|eu{_pE`ewg=8B9B7WvYB1faEw->?Ue0I zXG&B3-&d4;4z^Q89j9pLDt~f^0lB_+$6(JBNKrruWmHg211)sY!%glp$oZj+WDJ?C z@bh()Q%Nn2w9!Q$w+Y_+AP@5h<9M7WnMyXZsrTMA9dvV@dkpm25Jr%}c%I-XrZJ0L zo+qD$yv8kO|KkDA4>5|dJjP_Qm_ZKDF^`v6%+loZ@fK@%pV2(ZM5gdGGnvC&UgQ;) zu#7iZ#k<7Z5I@pSv`n`$ooC781?KZAg)COofX9=79pwH-I9?YLcS$DL|BR_Q+7uLra) z`F-k9{XtK$(X;JXuD0VBYCBe{?f8w_j?CKm3kMtdL}6Qvd(} literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/l-lerc-mask.tif b/nodedb-raster/tests/fixtures/l-lerc-mask.tif new file mode 100644 index 0000000000000000000000000000000000000000..56fffeff545e418c8e590b226725b7287f6f23fe GIT binary patch literal 2097 zcmajge@v8h9Ki9)31W56|=PJ)R#N zZuc^Y6_G_E7OPm85@CtV3CuB(*vm|Oxj+&j3Ii6#(qULOiO>AsC z3K!e;+zVgi7%Q{th>>bKirJGkiJ`;LuT`1dAa#sm(S*;P{=MbJN4IeC?rZ`>42yqC+jds5*8y4R-Ee> zIg2wGMi4#dKnogBgWDk-KTD@Dib3?D8*OO9E|kNA6-dNPkV}gJ=0oU2D;iOYBA7@+ zEXFvt9r*|z<2E{k14QcCw-xzV2~+P|%loje34@SKkz`!R{>h+77fLW`vW@LmBO4{y zfjv;HW3rCHF%8(twt6(99S3j(r!dC;SegbCMW{t1TG5F^7{CZ7FpW9boLn2a3r%Q4 zH~KJ$QA}YL!m$gm2uqNRRP?Yfh+&+;S)7BFeQ{WfBrL_1xDG3^22RXy>|d}mUxY+l zh7??b6}TA=xZr^gcOZaEa5=8PwYUklAQS75hYi?>Qfxsr>Y%U+8OX-1@S+eUxEqzY z4?CQq9-@!X2D*nnMGH`bP1uZkQHz~;m}7R+M*0NZOP`^w^f~$>YVZJ9Th&w6R*zHG zR?W19?xQczcG^K-rEky!c#N5~RTE`x^( zUZ$+AUZ;BPx9Pj|Fg-$#(c|=voI z(@Z0V%>2)n{@6(G7d7r=IR-Ic!$^?Zhe=FvH(B zZ8R4$*d)YjD#X(1#DV~{Dn`HB9D8Rp;VpWy@k2Ev!ey#N3J literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/l-lerc-pixel-strip.tif b/nodedb-raster/tests/fixtures/l-lerc-pixel-strip.tif new file mode 100644 index 0000000000000000000000000000000000000000..081e4573237550eb638d0774ef07d2264f38bda2 GIT binary patch literal 1461 zcmebD)MDUZU|`^5U|?isU<9&6fS3`=76P)FfNUlpEdYefP;vJT1_m}L8>B~&k%fT; zNbdt;QK&dbj~JBA3S^4|*&sFBfLI1eZxECX6lMqpvO)S7fv{l-6GIV@eGP~|9c2NF zp98W%K%dcw(UkEzg9p&^a-fEl%nS_ofQEz22LT&*7e^o8)RM%M#F9iiE(HY}&yv(! zg}lVvR9huyf8T&0*Wh4Jf4}%3*D%+hU{5DcAJ33TC54py+{Da0TP06lM|aow;E%k4NdaO|2?)>w1NFwXCKj#2F_KBldvd z%*7#r2(hrNrS{lD%oJOQ6%<24>_iKgHfNl{Xu&j-iGk6a1lIyJkq{2~q$s{Xf@03) zIKqaLg9&?RA_DFA@wZz*xet~rZGdc0uDlM!7`alR)&VHYz_6hmOkZL&W}3?M4H#&b z7~3Tpnin6ET$v;v+`Ua$ZQ(wp;#Ith?*mofkYVM*DU*#$hSQ0uod;;oC&R57Knu7y H7o+k4=7wx` literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/l-lerc-pixel-tile.tif b/nodedb-raster/tests/fixtures/l-lerc-pixel-tile.tif new file mode 100644 index 0000000000000000000000000000000000000000..90be602f48ad1b6d2de00c80109548650f9c3307 GIT binary patch literal 1955 zcmebD)MDUZU|`^9U|?isU<9&6fS3`=76P)FfNUlpEdYefP;vJT1_m}L8>B}R$_A;E zK@xX@vIT(R&PZ$*MivGZpx7xOb_I%q*oS~P2&x9iWC#YbLFO?6VZ#z8h9V&Q5fFbm z$^sU@1!RN3CZ@@Z;f$LZLVylo0~$1wm4V>{8w1!(ki3n%i=&TkYDr>BVo9PMmx6+g zXGv+p1$RIl|8+}w87<^KTl8qEVe)^%_KLzA>5C-`L6u_X+ z-UQ?@gZNfY1+G96)?Vh9m}Sra4Ruj*OiS415+V4kWg8Nyz(bS+LRBLrBA} z%V6drRi0Vqu_x9{oXmbf(Y=OEv#aIhn?QCQ;8US4%3IHH&&?IsT-cS&UIs4188YY8p0)iiROl?16hSE3l>5$-tD0nPMU7 zmf=7vIK_f^;Y>S#DVCwrq42{WLr@lAn4njw1o_F4w(l_r^kc13>~&1j6Mq)ddrM@ddxgX4OGxK~p4a`d2WVw7EnAM`EmAMSeTD(e1dmWg?#)gQJX*I{2 zu|-KZz9_lS<^VPbNK`W20!7Ix2f>dIzt3;vb@F)FDqW?K)TwiBMe+nko*=&{HqC2y znqlg((_jOF(m0>Z%sUpTKCkAV<%)&9LAzG2Al3j^kP9>qRV?mzRXX`(a&Xkj)afbf zG}GsiWI&f&*URP0mfg}?v*q5d)$5NMS+CtjtN~0+$ie!1n)6g_!MYjff1D|OL%Rbk zc&0Lag9IyQ;-a6+<~GQ^%2=xC%)pQeiF24T6dDwWoQKr7-)?=l==Zw4gp{+fft(Bg D0%G+6 literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/l-lerc_deflate-band-strip.tif b/nodedb-raster/tests/fixtures/l-lerc_deflate-band-strip.tif new file mode 100644 index 0000000000000000000000000000000000000000..db0d3b9d3dae151b3253d1df0eb8f4bc2241952f GIT binary patch literal 1305 zcmebD)MDUZU|`^5U|?isU<9&6fS3`=76P)FfNUlpEdYefP;vJT1_m}L8>B~&kp*nZ zF(6wMDh|>k24%AX+2TMp$fj*TEQ6#b2+9TuGXw+KAbpHL*sz3&p$N!+2gIL_vVg^( z0ofo>#Zbdg%aFxT4W=^~5`d1l1;k-M&6k1LmYIQJ2N0XEFfeQcVju$h&Boou(Z@Hn zBrzqiB+-sbLBYneBsEtdFEKaOR>|4lHz3G0IM~zQFFwdM%rz+3)5+7vGbB<;AtgUI zF*DCr$~m&>Ux<4B_t*! zB-~xGwN2Eco54_7U^#;!yTE(4q%<~(iUj_J_6`}XOzZ+jPaHUM;=zd<3lAPRr>?bl zO{&KFW!Xt@pGYP=eJZeF+-e)*1R;jurd*|EBv;jbfaC^7|c2eAh+ zPg^ovHwX^Y~Q1WgU=g*IvCuvW9(!6^+=W5+M%R}~bm4B=a(^Hc%n&ap5*2ZC` ze_OQiH%-xEMh2!Csuz%LnsM-u>ItqTt_?E;JAxZ#Fn5$Q8yYk7fNbIwof)kP^I89KR&;*Ki2KDp$naFl;SSvaune~#A!x@evnhs|ekNj?!F_V!^?nd*< zHZOLA+C{8B=M(0fJaWLId82c4<6Qp`x5(20alY4&+&XD|;M`Sp3oS!sdo$JDD>g0M zyB-*ak9vNdzP2e&!O>8iJ-D62!_Y&~L(s#~xA?jXgVaXdFciz<6GTt(K*Bs16z0Dh zB~)XkF;@Za#DgxTGsT8jJO()qSQ$>1>emQx3>PC(iQ$uZ{PV@ cz20rbhQZeS0na8syw+3ebu@+{J4lri081OtSO5S3 literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/l-lerc_deflate-band-tile.tif b/nodedb-raster/tests/fixtures/l-lerc_deflate-band-tile.tif new file mode 100644 index 0000000000000000000000000000000000000000..c40a47921951005c2991f42b465f0b134bd9a8ec GIT binary patch literal 1401 zcmebD)MDUZU|`^9U|?isU<9&6fS3`=76P)FfNUlpEdYefP;vJT1_m}L8>B}R$_A;E zL1H^W*#bauXC$@@BMaEpOF(f~pg70{hk!T;ss_kp2nMo2<}m_c!xAQjA|U%05Pv$# z0v7)SWP`vYhBXZ34E_vL8P+mX0O=f{gZ=^WDxhXXW(I}=AZBG@VAur2TC5BVKnmFSp%sg8qPhUrO*ZAO&pit+K&>&YOJ0JhxU>~3!8-0*Lc3d|4s5UTE%=zr8 z>tz;{keHBQF+I)No57>mK$*Ga0Yi|f!>pN%Y;rf8U+@e3a$C&oveHzzSvh$*GxPkT zUCf=ju|l8hqQ!Dbz6o5~@Y-m`PLHYS(<W!S~OMRCv zKg@f%%v3XM`P!-XBC9?hxbx`4zu$ao*G;=(eEOsM*E30`OBSg!e!tW+1=&SEPVCe7 zL%1lvaR%5$#tO}{zI+017C`qnFL!>}Zr`SGSVHYZXx7FiiPy#Zv^F+MJP#A~+T1LW zAHPTR8G~WPmCY3vjy!L#r0c{ev|WF+WPbwNY-Q)^&d&~BdigQovUSd7rOOA`JheW& zxj>VHY4^B*++Wg>n`~BzUpR6iva;C1k zg5j*QX!q~fseX1dWn{J*l~$dyI{T&ah}3~2 zYz%5QRWBi1Tktz>S12Ngwj!+U5@Y8`)&d66VJ-G^M_lcvpUs_6J#}Nq#hHzYTjw&W zCnP5^GX%UAC_^?b;?nP;eufEbiEeBh2N{=SHJTVR^Vn=)59Sr*6UgJ9;g>Yy@R0{I zuDm>OAbsN_ha~UmEql&SW0jt3+hX!&7f+gA(9@Zza@9#^kDPoXE^;^fhRN=zIKMba zv)iee!7pDH9NrS^+jTWFdujQ!do~8!E%yB1=e+v0&a0V6C60R79*$5CylB9%+`caw z*--&AcDY^GO}YQJj;42%y3EJ%n1fXi-s$e3TbH#jqKlePCRg$&D>Jh4M_(H0aDL5&#XMI zboKewKbvDVZZOZdeoC~@#<4CiXV&$1CdLm~*C{aXy0a@zFMivbY1d}yelJ@aSAXS1 z)myV9-UMLGy?v@a71`d1O$);VuqK4IGIox~($Iw9oOLIEyIste>br5S>1=IR%Gl;K N^mg(xcrOtY2LKPA4!HmT literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/l-lerc_deflate-mask.mask.z b/nodedb-raster/tests/fixtures/l-lerc_deflate-mask.mask.z new file mode 100644 index 0000000000000000000000000000000000000000..d5b48a0153d95a4917beb7fd5a278e6fa0ab539d GIT binary patch literal 19 acmb=pm7KDK$Dn~>W|A@|1H*eg#wY+sI|dm5 literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/l-lerc_deflate-mask.raw.z b/nodedb-raster/tests/fixtures/l-lerc_deflate-mask.raw.z new file mode 100644 index 0000000000000000000000000000000000000000..dbc1ecafa2103b468f17d724aaefb3b292ff4fc9 GIT binary patch literal 662 zcmV;H0%`qt+HKQqXq9CE$KhL)617k=8?rH zZaI5`$nfNzrVy%TyUuG3R8v72B@~ci0)x5gOcnX0osBu}NQF?$Vg6?LoDkM~ZxQ)S zqrb0Ia^7~9Gv%}qoE=9tFH_70hPk8HW(|eu{_pE`ewg=8B9B7WvYB1faEw->?Ue0I zXG&B3-&d4;4z^Q89j9pLDt~f^0lB_+$6(JBNKrruWmHg211)sY!%glp$oZj+WDJ?C z@bh()Q%Nn2w9!Q$w+Y_+AP@5h<9M7WnMyXZsrTMA9dvV@dkpm25Jr%}c%I-XrZJ0L zo+qD$yv8kO|KkDA4>5|dJjP_Qm_ZKDF^`v6%+loZ@fK@%pV2(ZM5gdGGnvC&UgQ;) zu#7iZ#k<7Z5I@pSv`n`$ooC781?KZAg)COofX9=79pwH-I9?YLcS$DL|BR_Q+7uLra) z`F-k9{XtK$(X;JXuD0VBYCBe{?f8w_j?CKm3kMtdL}6Qvd(} literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/l-lerc_deflate-mask.tif b/nodedb-raster/tests/fixtures/l-lerc_deflate-mask.tif new file mode 100644 index 0000000000000000000000000000000000000000..7062c1bcbb0e648bbc3bd1e8f8de131b4d107f5c GIT binary patch literal 1300 zcmebD)MDUZU|`^5U|?isU<9&6fS3`=76P)Fplk&on;FV>?_gkHgR((-M3LAsNNgu4 zTL7rW8Hw$}$ilz^6x#>Hu0U}Rdm9i3L)9a=OH6Z?Ul!bu_NS^~@5MW}u z!!Vs8oWTQVxff8e8qk2fKnyYm05f@j-z~0G@6BdZL zwlyYBoWj!CIPpP>SX!4_QderwvRIchJuWI|6D;@mT<7aS);qI184yvvx zlMmTlGkD8hcJcikPvfmqCi$}oZkv2!#a*{cFJAL1mfhK{tSB=3>4akkf7pJ!$@$)J zbxOkokwb3NX7}&@uRF76wr+W?_{o$Y2T8;8ofYTAtW}@v2=fu`-E#4;!s2J0yXJC8 zJ_+{HJzH|=ynmhXQ5o+=|D<&nO=63BnbshvHbe17>?yBnnIRO5Zq@_pNZo<2g#L#W9b#Uf(#v_pP8oS*37Rh+e_W z)pt~;?>=I2#Bh@3;mAuTpIB|*;p}K~piAF6ojash1C2;l@SNja<{<){O2Ce=&^ICTEzDwr6{GzwU z+P@Tkdu{y{OZ)3?wQF^M#XUa%Z^itr-ql;e_rKT`4#Qrc7Go5Gqwo0@@OuJoyl|dqK)o?6MDdKT5xRLWwqRehu>YYlTA1?@1kXF z!qK?Pymxo0*Kr5y08{t5gItV>B5e=zvnFkt8FMhEu~mCUT5fixs%U-_to&)#q-`bz6RY-S2fvvew@IZg5_4{;4mECUHGBWq!Rt zG~__&sXxIdPw+U;R4IA0NbtCXy2VjTy~5t;JC6m=R~)sr`{-=^jdfi?v$f3wPW_6* z?E5};+xHl3<4-wy%#y2O&Sy_uFSDS8#Ds(me7q9{7%nNC;W#1_P^K!d*>C}4qXfHw ztibyNtV?D{+8NYT{F&dluyNtTMq%f}`s!{5%?~*ze3tRp&-RGl;aj@GU)ePb3{Pbx FLICee3xfav literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/l-lerc_deflate-pixel-strip.tif b/nodedb-raster/tests/fixtures/l-lerc_deflate-pixel-strip.tif new file mode 100644 index 0000000000000000000000000000000000000000..4dfca484a1b07d6bbd01c4a6c716083040073ba0 GIT binary patch literal 866 zcmebD)MDUZU|`^5U|?isU<9&6fS3`=76P)FfNUlpEdYefP;vJT1_m}L8>B~&k%fT; zNbdt;QK&dbj~JBA3S^4|*&sFBfLI1eZxECX6lMqpvO)S7fv{l-6GIV@eGP~|9c2NF zp98W%pp&5&2+J8ffR_~4MBQ0I`) zAXgn2@kx*_f^8ujtt{ZQ-6->pHhiPH&jI z+P@$q$*nHOt1K|pxjNpr`1%pa&(qhI`Y6S%U9HNQl`gpc+U5<;EK9_e@GW6e;h*$* zPfM%}!>iAM>yaIDqeRPYo~TDRgQ2j%at1?o0ck@c0|Vm&2bil3u1B*U+ii`{*i$k4}9m8=tD(gIQ zsQAJ|WrmhU8N+4ur~_LpAM?0zFPrJZ)AOuHR*L6M%_5^yQEi{HcBx(So5xtT&hB7f zr{vF#nm1ji^1iJ#JsT^WekOjK*ow}IqicRH(b(tElyvTCkF1iyQH53oR|QrDPd`4j zH$n_59|AukJLX62pSNOZ3c5^3d>nF^kH|RG$aXkB68LaH&PVf#Rl`f(w+X+578_m; zS(XoU%XEp`s#3GW=dx>j VK3R~>7SPdMY^WvA5c9(MC;-U98ms^S literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/l-lerc_deflate-pixel-tile.tif b/nodedb-raster/tests/fixtures/l-lerc_deflate-pixel-tile.tif new file mode 100644 index 0000000000000000000000000000000000000000..7fa1c42f325e741fbf3f5147142f6a0a270bee4f GIT binary patch literal 1080 zcmebD)MDUZU|`^9U|?isU<9&6fS3`=76P)FfNUlpEdYefP;vJT1_m}L8>B}R$_A;E zK@xX@vIT(R&PZ$*MivGZpx7xOb_I%q*oS~P2&x9iWC#YbLFO?6VZ#z8h9V&Q5fFbm z$^sU@1!RN3Dh3tC4Gd`vAwY-x17am+28K&O3^EhsHXC;rM<3tRlEjq6l0-W$1qB<= zlGI#Rch~sf zkf2cKkkBAkB|9Jg;9wu19vgj-L3Ug=`lvQARLuG8sq1AHl#rN^@MGaUdshaJW&>dX zV*?|D0)~~o4t&PUJT@EJFUTu=X%=JexTM0Cz0A^wr>|!&A5V>#HlND263-LAb0Rlf zyy@(5c-`rQXG)>-BA;zkzveUNOr7J!y=}gmS3iCFWNPB(*lA`{=kb~Q+`hUbV(zxE zs`Qs1^*DC){qKPME#)9yb zN4b*Lod3D%2wT#e=unNbd`Ww1KZeaYh}rfYF*ES4 zoZ3<(KjZ0=V?Rr@X0RH4o|66hZjr(Jrw2d%{F)%v9_yB5J!9!UZKs{`XNrtJ*I4nM z@w%I`vqo;l)kVvv?&LWWwa3e=T6V_KV{8m9d;KRPhtZ6%;ysZlv00#S_6!3v{~O5( z2V2e9B{olbDJI;kd{~S9U_bx<^Z8FoKULh&id{PK%F5zA*H0|&^Az@Ln4QfopC$Q$ zX#=BU=A83s&t$tp=7)biUv%`)y#=w_;R4wW+9w#!I*WGyj-BdfH&aGtt5IpyDXX(z zDpe#eJiYd{=;x-??OU$p?A)IH_19vzYuVccvl)birKK6P6x=zH-TTA#Y~?DHIGx=% zV__?T+t4j|I7=v$b6*W1GXU=FbGF@3+_T Xi~o(=K7B%wnVFiJnHj@|`%;Gh9%zAd literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/l-lerc_zstd-band-strip.tif b/nodedb-raster/tests/fixtures/l-lerc_zstd-band-strip.tif new file mode 100644 index 0000000000000000000000000000000000000000..dbe3b4d31795929b10e97b638b9fafaa1f932b1e GIT binary patch literal 1461 zcmb7?Z%7ky7{{Nx-RU-#&DyGgjGkR{FYpoS+$HVW-vwQCM;rV`fJ`>~s zkOV-;fIy|eA|$;JHHn(UQ;m;I>RY-15+p6oBokEVE6-7DrM@_GK+-hogQ&%uX0TEs z-mys<+aRD8=LmfK_ep3%{TXX(nF{saqZWTX(2vhG=nI`!p%x)NVco#ty)uAqEFA?f ziB*7?;}M!SSW1emrS2BH%idx)DjCMWx41n_gWcmU)SJsn%Wa$>@MY&}Y}|RyCh#WS z%2!nD8JE{%ztm8u=Szz%T#Zm+t29?s+Bm(@S|$iqoH1mHA2KQpS-;-^8Pi!i%QR{rnaGeLLLTM zKp;&7(9)NZwpc(55>R6^3ybaf#3>Sp{5Tdwdlln<5>bQxl|`+Nxw*2;u)5HO`UYf> z7Gc-?v$0+bRM;*9YSFGf35aQBVWM-57(k#kB5=sxF}kFC=tvcA&FoLVx1evH+%vjC zD@lck45;wWt(@sP>?&#aSiRA$Qk&m7)FY~&Ny(#NJL2jfrA&^si`}I7t60ds;s7U_ zB}R$_A;E zL1H^W*#bauXC$@@BMaEpOF(f~pg70{hk!T;ss_kp2nMo2<}m_c!xAQjA|U%05Pv$# z0v7)SWP`v0hOG=u4ABgW8Fql_9H4`EfEo`1u`@FR!#p5%WMN?V3B=W`3=BXDtl!4n z#nHz%wInemu_V!sOF_ZLvm`ZFAulmE)mF*b-!~x0H8|MQ-!DGMHOw_A*we|=$1@~S zNg*XaH!(BMR>{-X(cLvZI3y_4IV3d5RmslBKRDP2sK-VhWRM+~jXtUk3>sVY|0dKh zY-M5C!tRq=lx(B`a?80i+gYISVFZUl4bb(>3=ix-+-P)QWUyvpaAfRsVBoV@aUij! zOG4gf%Yu#09zq&+T?R83sq)M+k3F$w;$-#a?}eJ42f}$D+4sdEoc9*!P>}N&b~HJJ10BcE=}`FLk0HYZy-F?C^*LXa*v_pglxBbX z;;k?Tk9rmlr*`Ty7A~*FNq^ZK8M+)Y50*}k@32jaOkch;OgjIVsr%isryI@Z*Rkkn zB(^E9l2Gn*QsaC&6=(}Efr)51Fh8;qsOZ@!^JPoatX&@M$|tS%hIw!1Hu?1CqU8*Z zhP&AkU`L-n%gms{hH&)WfaXesqtC+}eWKZ+66EMr4uT&aexKjS>*VpURk}(esZ;0N zisT87JVAa@Y?{~ZG&8tr9;#T}@2Yh2$>iXum8sKH)@i2CV`AfAa55CUR(+>d=LSQ~ z0}dz2HwRi7!7iws%EZ9Jf^dP`_tLEh7u3RBu%g8Qs1W1=&csDOm(6XEdzGDiH(in;k5)wnFoCCR;Lo!^?1N3rk3F>3&Rtn6tkrFqZnByaWZgd*lApt`Hu6%nhxfNA$f-<_8}#jT2_V>q(q}Ib=@(93*m|8 zK&!(JNTRv4snMGKW|A@|1H*eg#wY+sI|dm5 literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/l-lerc_zstd-mask.raw.z b/nodedb-raster/tests/fixtures/l-lerc_zstd-mask.raw.z new file mode 100644 index 0000000000000000000000000000000000000000..dbc1ecafa2103b468f17d724aaefb3b292ff4fc9 GIT binary patch literal 662 zcmV;H0%`qt+HKQqXq9CE$KhL)617k=8?rH zZaI5`$nfNzrVy%TyUuG3R8v72B@~ci0)x5gOcnX0osBu}NQF?$Vg6?LoDkM~ZxQ)S zqrb0Ia^7~9Gv%}qoE=9tFH_70hPk8HW(|eu{_pE`ewg=8B9B7WvYB1faEw->?Ue0I zXG&B3-&d4;4z^Q89j9pLDt~f^0lB_+$6(JBNKrruWmHg211)sY!%glp$oZj+WDJ?C z@bh()Q%Nn2w9!Q$w+Y_+AP@5h<9M7WnMyXZsrTMA9dvV@dkpm25Jr%}c%I-XrZJ0L zo+qD$yv8kO|KkDA4>5|dJjP_Qm_ZKDF^`v6%+loZ@fK@%pV2(ZM5gdGGnvC&UgQ;) zu#7iZ#k<7Z5I@pSv`n`$ooC781?KZAg)COofX9=79pwH-I9?YLcS$DL|BR_Q+7uLra) z`F-k9{XtK$(X;JXuD0VBYCBe{?f8w_j?CKm3kMtdL}6Qvd(} literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/l-lerc_zstd-mask.tif b/nodedb-raster/tests/fixtures/l-lerc_zstd-mask.tif new file mode 100644 index 0000000000000000000000000000000000000000..70fa08f5ad675f52e308083f74ae88cd8527f0a6 GIT binary patch literal 1440 zcmY+Cdo6SZE==uIy+j0LFh6z zO|F&77^*|)H;zk4ing{~nn_`*9jd{`XwRPZr{jqbap23GMIVjn?-R%t zrU+sMDFVotOs26?go)%NL86dO@e5{$a2Q-JD>x{c!w6$=xGY~*AS-k?g&dolC`d@6 zQ&?;tCL@{~%HjEi@;D3%6d26q1|mH)x5Yt_G0pA&8vwPz?ccqtF&c9a_qlM(qtZ3C zDAR3KvWa4=UA)M!ZR5t7FFjt@*LE_1`O8jNFN8Yy*ms4@=t`ya9Y3V0Q!{!t}SRjL|Q4^$H>PL(!~ld|(rRLixd>81_M zp6WK$kZMY$S7Frzwe`hca(}Wp9Md9fE^2OXu1NYdDKjZMDL)A3<@wU`IM)R2wf&%R z$J7&U@gg(CsvSVEVE?P(*=<_W$IGd?EYjQ+3^j<5ANU z`e%k?<2s?{~ArJDpw~(5=-Qi&%9`H^%5OhgL$_$$<{T zS1~#Vy@3eD&*x|JGx@*rufEArLZfzbe~Z$G?#wrF zO=$mv(9K~ZsKIvWEzFl+b_6gC!Db83E$+#uJq)t(>!f#N*PW-j)$=aF&e|Tsr(^3c z5^qaC#F7iX&pIq!^Wk9iyF;pTW+@Oe!}NWFr895@6cDcsl&tRkV=68pM4XjnOM|sSTx!uczkQMmD38Rhu7}n@3K1aMx>fVst z3WA^1#meqme2tC@<5SJ;GFuD>+M{%qFs81}Mu&#)SHXpbGo^#m=;wbXypVX4h+W%u z#g8#kDOz8cm}+O2b-Ti~voj>Vf-vjqX#lR8W@t*9-@6QoC-?RZqZVIG9?8do$GK!j z@qmZHrjM>+E2QF8ZzFnI7G$v~a#_i0CI}sF+Y>E5h{F#Ap|o%9yBtj_9<#m-ru;ke!|2*%SEOl_-EN$Qc&dZ1( z!?49ww3ioR6bTVz>$Es-VO#lLQ!Kc3FQVCYPgGPt;mc*|@6dpQvv4R}mtf*TDw@1s z^cjii^NbjFXN{xO=-tQ2(G5`J2q#zT{()0ms^Mv=7Z|ChBjX(tv?AbXZaV%=n!oNf b(dUMjZHT)UndpxLHy3v(I*Ub(vQ_^9W_U%3 literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/l-lerc_zstd-pixel-strip.tif b/nodedb-raster/tests/fixtures/l-lerc_zstd-pixel-strip.tif new file mode 100644 index 0000000000000000000000000000000000000000..359c36b81c1013ad4f1224ffa15ef8e9aa840a7a GIT binary patch literal 934 zcmebD)MDUZU|`^5U|?isU<9&6fS3`=76P)FfNUlpEdYefP;vJT1_m}L8>B~&k%fT; zNbdt;QK&dbj~JBA3S^4|*&sFBfLI1eZxECX6lMqpvO)S7fv{l-6GIV@eGP~|9c2NF zp98W%Uw1ONl#!E%u|tG2mseqd(ro@U0uL0#C$L^?TFvLI%yB=xVYhp>Mx)~$i0e$zT^E+M z)E=AbOrfqTD0W~t(PGXxgVBO%CKCgrISB#BID<9+0_Sp&<2aaj6&@(fW}m|+;K1d; ze1n0VYx2BQ8|1C;+{ gxHSVP!Ns|lL%~4VwoS&Nk#+a#{1gdi@#Yt)0A{+{dH?_b literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/l-lerc_zstd-pixel-tile.tif b/nodedb-raster/tests/fixtures/l-lerc_zstd-pixel-tile.tif new file mode 100644 index 0000000000000000000000000000000000000000..20c0deeaa7cbde7b776a2da996fa7c98f4a46a54 GIT binary patch literal 1104 zcmebD)MDUZU|`^9U|?isU<9&6fS3`=76P)FfNUlpEdYefP;vJT1_m}L8>B}R$_A;E zK@xX@vIT(R&PZ$*MivGZpx7xOb_I%q*oS~P2&x9iWC#YbLFO?6VZ#z8h9V&Q5fFbm z$^sU@1!RN34hBiaoj}FSp%sg8qPhUrO*ZAO& zpit+K&>&YOJ0JhxU>~3!8-0*Lc3d|4s5UTYY}Nmpu!*shm0=5qPij%JkpjpqKh*oD zfWiUfCQv|wLVXjE!_4r&{=t z!>-F<<|0*|S>~}P)=ZqtenHW_hE21p<>lm+VY6M9HkF#3b z!~5?$v^plaTF!F($r#0IIEjaWV}*xCKu-hrQT70v1|ya&vENyG5_k`krzTi%@y(G1 zyL&Q2EeFFFUWB_3+>=>>?(WGz_k-NcaG=#8oM{Kp%?zCmg&+PHGEC5`)M8zq^HquM z+^Rxp_O~zI3UlzNXYp`qr#@rh@>-nq_tK_DYxa*H{u*-#=vV1*>gRq_;qqHuB+H%q z@|`G8k9syA@9Na&YyLNolVG z1wI2Co9c?dmaahk%^PeQ4oAodbgY}kocrRLsdescuH?xaKN)y9pS~8km|L#Oyiz@g z85}U-47IEbE}V#fnO1YW89iXaVF7cY&7qR%7ARm=IS77y_{IC!b6Xj#`;I zJ!PF{`n)c;u9wS~ExV<)X3M=@tJfbjvR=E5iHVJkOW{LPuB?`%bo6?CAC-m$(QitN z+EZjxtU(S}*zC*95WW#NC{Ui+(Pf+aUKUW2vGu r14C*%!yz^9w_6`B`n_&18ym=Cb_RyUAxsk6+qfSxlozDPT37=B>Qp%K literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/landsat-c2-1.raw.z b/nodedb-raster/tests/fixtures/landsat-c2-1.raw.z new file mode 100644 index 0000000000000000000000000000000000000000..7b470581d77ec87d25590c951cc715e9497c92ce GIT binary patch literal 1730 zcmZ{jeN<9s9LKlHiKr+hVZM;&Yg0@`lQN~PEH%`NDQ%jHk}gweD@&TBsHrQp%=xm) z#Ix1vwA8lKjCA>iz@}!RcI+hPrIwDfAeWVqXgmC_f9#)~!#VeGKF{a*-sksyF8`56 znYe3Hymg7H+9kZ&rCIm0;qkw}mtp^b8p+~NvCd-ptx4dx8i#*Zz~znH6Cc0*CwsDV z;#i)x+o6M&tL7>1<{fU;N=|ez$atcq`BxuQ;WkteKCd!0Ld?<-lK{jbav1M0Y<|KA z=j=n+<6~-i(=79*Nsy&*JH7CTxF~@y{DNN`{kD)}CJC$P;kz(p80IO;pOAw@OWalLZO-$O8FBz0?wa)#LP)RWECp z88zXXYobGABS^84N5{gbV^N7?yI1-%x&Gla|LEPfBJ6HO=H+aY=j^P{**^bj$NS6p z3tx`%hN$hN+uAl|%S7%j?O38XGk1nES)P}htQ{}eM&I#?my57$e^@c|_d7vm|{~L{$1hm&x+z63bP*;ucVjK#3${rHEHo7O&RTK#p?x>Am{8^?B#!-+w2e!C^W+1Mf{~iWc?LnOB&0Yg!tC7Tpu980W8m>NZ@OQW92YCQsG4%_G?zSX%Hbn zeR4>Bnx`&oR7c+OD`@Z9`_8g?P^5ivsdIVdi7GLIvQ{?UR98Ao-vv1 zQ+A3y9X(I(Cw^b68(l0pQga|}DW~9^y>q+)`^zyrU;nO4zb7Lhf)Q~xVW&!?%z8A3l5Qfj10`^2x0iPDf0=Du7+*;uY_$Txr;Ok;GVA~c9 zpRog+iKYX-AjY!S@Gx8%1o&^12e@5~WmB{mK5Gov4ov}E%Ez*a6u^fn+yP%iGXVd{ z$FkQpV)zUhZ~~eJxQ>rylaer883=d+%?8}Y$Fdy=-xx-GTW$<9)XbfO5tj+p8-_UL zD}tHk_LBi$nyUp|FMtN70;VuLU;%h??pwfT^_WfT0zBWp4)EQ%dcYz*W(U5<@PJd2 zKKVT3LGn$D)zHz{E_f-j_6N8h<*$6f4_%h6;uL_Sk)IC=fda^tg7&`G)&&?5wQKu_z80Ue)=>9wt(x5~yr z-#fEh`RqN=19WWA;{_d{XLn-hgegp~e}U=pdC5;+;Zy+&Y%$%PeH1D ri3w-kQ{V)*;7*qne6J%@&`Yl71?U~Gmup{e2c5E5{@!ArGNtNYekGn? literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/landsat-c2.raw.z b/nodedb-raster/tests/fixtures/landsat-c2.raw.z new file mode 100644 index 0000000000000000000000000000000000000000..b064ad67f8980998f83ef07f35eb6c4436b84aa4 GIT binary patch literal 4121 zcmX}tc_7s39|!PXD-_ytMJQ3@4k@=rE7y?wzOz(h8%2^_QB&JNnsM`E$1zNYX(f$D zB;!g;j!s$E%;adNF)GIn^ZUc|`OTm6c|Gsv^Lf6{eCNW)f8MZ_whhbJhv#sA4)1^U zcjl!{o3utTn5;{eGONxdCTfkMPuAcW{P?T#<<$o<{{0K~zw?C3lUErY`1>W}8jNT> zrDTf3u)|Y>CVUwQ_{x#VMn(vpW;y;dBZ6CHIn~E-=H6vY;2FtWa`I#u!;fpd*q3@P z?314yp~6e=hVy2v{g_HQTb8zbHTkQS$m-z$ZDn?qRc^V2hlh@9b*)rhjgvPat+Lqr z=B#__%FSZ;_)~VLsUis!{4i<7JzdFG_}Wkj7ZGLUQb9d<`NWV_prdn?`Gbnb4H+gw zCLeLhQPw*vD$ixQ4w*l7^qaOAtzdk+v}@?VMVxRjXytIZwz>l+N`*9aCdHMBTaG!e zC)kqSJF}GB#|&7~>}zW>S5;uC3l0>p!sse~tgt4fcjMMeZgpK-n)aJc<$t<560ga9 zd5CVhIF%tV-O84>D(;%MEqgJPw7s7#+FV#L?-bfTmn0j@-q~ETIR6XwMc1-iIa@NN zh&J!i(!RJXZOv8?6p!(e*O=0KiMDx)GC`9#8=QI%ruyMV+`eecv&h_6{L09JV4|Od zW4N{krm7(~Pr@_cu2aHt$SLQL=-pXW)P|h5gRT}%3N!l8s%TOP*R`}$U9zvVkYlxXrn(hrNv_(OYnieuGrg~*QHb9p^D1wA z)AgxT`X;`L`(CSNOgS-3{OF!MYa2op|4vCen6pM9AI!8LGAnlUin92tqN(B1_93G{ zTujuV(-jXIE}tL5S~xmPoBg-q(YXx$A>*gG#A)oIikfekp+m|S-O9Coa+s`Dxf~I1 z<=TxiGp~u{Jg1uPtNDDZv?!hRrS#@P+y?zu9rTR_ociDl-FTn6zSj8Db-i2Y(x&X~ zpNdD$I&fc%HYqH!H&_*3n5s~zh8zyn1|GOi3`9G?h zh!kQ!FR6v;w=6csNfzXk^WxJwg!7hb-C;|**VP9w`OLhHM1LYqLT#StG}xU;{3R{t zKG7x4X;+T{p{iD{z|+(Ir89=A-imXfViKxa1Q?g9rUqi;;4c==rZd{9RjpFQ^vJ?n zx%-VmHF~^@)E*r#)62!w2mAH}e%M`dyyQ#nq580&dpyk5YS^X0xw>&7_@0oY-M8P+ z?CAse(voQkeFmRjGPj-HfiDW0c}xF}CymUWr;p;xEvK{TQ`}t3IemIBx0o@*rT@#l zlRO(rf5**A{)%ndBzL{SQ|gAxyp|e<%*rJe{!~qA$SpJSJh5`8-b??bvuLfMe5H(c z@k>w9yGMPf7OSq0srtr$*bnChy2qSy*h!U4s0ke|5PN-0$)-J8E~|R;!6M4Q(c&Wu z(bD^>hP2nUHGbb0yq?BNuJ@XXIBMzE1B+CDY6# z96tN26v&?W)wNT*@O`|FYx~$)v9*jxaVC8|NoLjxlNG_G&S!V%_vg_igq_As+uu#B z2oy{wIG=8e#EElXjnXw2C)TWf7fys*5xaO7%9vhBV*Q+?=9~gvTxds3l1MD)bTes@ zmxg6JEN?95#HZY$@e*1(5|=kvb506~LiQ6uhIPE{b-jVT6y}{Jq9gI;yGdJ?^0Mz> z&ms|5-|a>`66cK7*0N)r)YfiwG^c8{IvG>d(!0w&-ISanqBZtaXEhYK4*FbljGfUG zVaM*!*%R4ti*(({j?$}Fq$?TuryeP<-u`W`#s?kC$R}S&m*Z@+d$rAV6?xX zZttF?yJ?~#;eWh&uE`Hxl@NPz_+Caot9nIV>Pm*MNLY8wcU{FzA`iq`h{9povE5P) zN=vk6pVrI_!`G+Am6K>_aU$OXEmmbcLW`|&8)>QQ*#`VP^yQwF?pZ&>ZDN62Zm}xw zhP545j50R7ak~!XL}4iiMl8Jt$ese2eH%8sfF&a+yTnBBD2oK}x*H@$VhISg{%}P- zY9G3{zH?5q-})$jEXAVn6jM&2ktzIW82N!)4Qj9f`wZe1c0l=Ae-$da|M8EVcrPZ3Hs6WdK~B4 z^{F1x4XRN;#5Dz!lmFv5_gkUfAIvM?wBtc z`*cb&f{hD10k$Oq)Ex6cFeD`r!Sf4;0JbjzG#G;uPfbZj@W+A(f@aURA~?QVcpHr% z{BZ=z%&q7k#|^DL(S~~jQ79*y9|5rQHb66LIB{7)B!aGdFMu6x01sMwp~M4%-w^EM z#{=x@2WZvmj-aIAR|NI>&Ir<(1^|s(p+{W--0?bo48TqrK#NxBQCV;TCF1!(06Ss< znzTZXmI6G2Tz)dZuCD;G>CmINARNJ6dZy5WHdkC7Vl~9bz_odg|*T)|JN^V4}V+aQ80}Lxe4Wz5b^;^2Lkkog7YyU!{5{hAsfLj+bjTjO+%tS z8BUx*C<7>c5uo2RBx1>M;{Osd5LDSFhTs|ZB?mM@`ZY3?Wv)WmUy+IqQaZc=jo?7| z6QFD-z~DkS@y+3I;+h01f~7JPfPsaOxFZ}6_avbSpezSqs6Ql%hX0ASRv=U(7%Y}Il2)Z0hH|n7)*ym$q6_|1HwH7n`P<%2GSu>c>>Nc hmcRfg%LN#E0TRV0;2c#6H3+82lwA{UXuGsD^FOP3emnpG literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/landsat-c2.tif b/nodedb-raster/tests/fixtures/landsat-c2.tif new file mode 100644 index 0000000000000000000000000000000000000000..ec6101accbd8dff6512899b246bef14f4eb75811 GIT binary patch literal 5491 zcmc(jdstIvp2x#Q@Q$r|=two$kLac^kPTqIFFT?EP?9<(6|Jj{9 zImwUnem=k7dp)38SxKsMDwQfTLzitf81rVP8)xPr%n^;c44qMDHe?luY2(I?duseR z&Ab)Mik9}z*1e#gX-vzSnqe?c5vS_&L^C7%Ps!G&PdDrHGQ_;JJbk`7M>kEM*Lycx z)Un$vnOSEr&&QsYhy;7(PfJNxp75) za;~TYXZ&lbXH)-n+Ax_tugSmr+wZ^p^xu8XUG$iawOx7C(DTjWh@M>+t74mg#n=Kp z-;vQrD>>HQ_h@CTic`g4(?zH@jaCh0-~TK{_k52Xou6sY{ARo+RWs9=qgh#2TBI5O zo7~lZxw~2wry@vxqvLbCLDN_D$qnxQLXP9sFFaEF!t>AfU9b6{-{1Lr zh?j2uhvLQG~IlR*&C45M)f98q0)Df^HJQ?(~tAq7!sZMfVw$Lb2Zf*(3 z$<8$?LE&3<@SvN%Be|XCs7pILZL3m)f=?Zs<(Do9{u6GxLvl}VrhRDBEeoAa%Jn{y zue6V94L8WnwCa$j({?&F=?e|;4v9hycYbdVEc6Dq}eN){y+s`u0YNm0dyw*nq*;!H@vUS=_sX?_*-8joH zTo6i6xFrl^S~HENO+E|FBIVP4#Gtfut>OK$Q(GNs>$KIT2J3w4iTQrPDU_acOBhIY zGmWH8-&?3i$_+j;U1@)!HC!V*bE-oPowkJ3pc%p}zXKJHyCrnvpPFe5ZL0H;TBUt$ zb*QY%7MkOCTog*1+)}0Fp3zKGXw!KMO(W%=mT(YDpbZM&sDtmj>06Q;Q#_iK*R_Og zXi$DT<61x36tqwSDKGPpVx@gfYq(8zo~aI*u}&#LhgTgm`lU|<|4}#HF1gd2=^)w^ zvQQH#ukeuyrM>TuHp+H}T3b)@{XkEAQ@ zue5|KG5Iwi>6W??Wj09eh0Rn?$`gEKw9@`{YdBMO)^7>fx@;zGPz@;0FPs-jo88i8 z$(`3shtZ}_EOZemAK)WTEA2O1!^N`m;+9Zbm#tPCth=S2Xz)AE38kmpl3j8eo9Uyp z>0d3hh?EcYkz}R)LTfl*c5d7fYUr{hXoF@5d49)v!C%}=pP)@@A2BKIXKO-bcWntN zLC+2KM5Eu)DwKZcHgEoA*8ko6LF}+cj{U$ccJ|p>zY#MeMkU_JS%}x><6HmN#lP6~ zjgtN2j=N{h`ABp}xcHo2rhD(-b3~qS(BTK7!VSM0`J>e5_6(XlqcUWEZPFLC|5u-r z{N0;?EdL0|P#`-QVg$0EAzuRdcOaiK&*k%QYZ=hOoZG0!d@Yb3klBR|dnu)b^rQo@j>Kukccuq-EmoMZ^=%T6F245{Y4J`0u+U6p|1{4|LMuxX= z_s#oQ}AL`AnyRVz>p(Ax`5;`WFC;Wfox}4s)6idNIj76fm~$B zLLkQ&!m{iDa+4tqKx80Vh7-kgplS`f?9Q z2}9-ssReR{W!VbkBtuwVI)HRAq!`F)hOjKVNPc&m`{`FEv5Wob!X9CUlbRS+^yf9% z7fzO3efZsy^k8q>!9!!*H_M~?ByO^#t{b>`Y{2%qL#!9cBb|*0>RrO)ggX&0SDyFm z*~HTs-KMWz@Tw!Fs#8{F!;P}%D(un_MSFW}fW}ExE2|XxiD%DQo~m#!H9(h6TsLq; zVt~Hk5I2ZK=xluHfQu#&Zcw}|A)%e8`fk(s0xyk_sxDX+&yBLrU$xT&MY}yQAVGRt zB&m=Nx#%wlmlH1wkhbwO6OzeGRZ`VWt8xd@YC9dNXbTbp4oFoZnF#5ii;f_iHeNPE z+QQT6-KOIOUI~@HwkmCqR@&(=6z!G7fB>mhB)@`m*hL>BTuHpFhP0EX>5xob36<_! zl^~?Gb~;MYPD%`TAXSRwna;-V4!9(YX=J=y2I*~{=5(8u6?h$}^r=<33~7a(j!?9x z5(8#PH6lrdbjT%POta(VR7kZv6(O0t4pj16m0OV3*y-bnc4cCq4w6kIPeVHBk}#&U zcv%mrhNoHGrk@JD0xDg%Do#kN?DR23`yer(hEyk#(U1 z#HxfK30>pKn$E_=w_J{R!u8?g;5pBp-8?PrHVu2x>**s^O|dG^V6k7aORSg`LYnRML`zlktV#l;C3Y!F(OQ!O4UoD-vK-R8E(b@r zF`RrE(tA8z+-<6U(Q8JfJgbrnX|Y}EqiClk2T~z@FOniidtHuUge&6Y8b}}TbRMMH zUNb6LtjYjLrFJPo(Y7Q9+91gyS%D3_+a+L3!#VjDq&+-c(rq%n=&eJgX;$TFNQ>-J zjG|3S4(K8IM6w9d9+!YI&E(|$kPh*50i@a9I#gP0Rfa+;u}hJP_E2)b2I(ID;Dq$9 zOTd^WaIzE9KAygWqw=CxjY2T1dM4pCs#r`!qd5s zW_#7Bw9=~3k3D;?^Hl0;Jka1`>-7NV%iT}2A;zjmShu-$8mDMSBn9Z34)J4=n6ZK1 zbJ3xM+sMfh5+3Dg1&+!bFO89^p0FxrNDJ(A0M1uZK!Wt8Nc52Qx#%N=yU57`q-LJ3 zf@Jneu~HRhRmvdEv(rI1Ur7N6q*jsSLwesuA0=EpCz~Oi;OSBvl{sDsmHyGH=pik% z(|$N#NdW=URgq*u+V7%62-m^MYDi~zx(1Tj>p-Q!R;2<`p`8xK`AQ0SAUQ>1!UlfN zC1FfEIk^ndF`h2RQJLd)pi+ue$%j;Ar~Prhk^*K(mqjui(mt1jFn1*k{U>|-ZI^=TTIU&Z&6Cu6B)2Wb5UJrJ|w^ro` zNUQAW_C16Z5@GS zZ`zi;UO)HZa}OK~LY_U|Zd?yMJmA0EYn?Y@Q2XGK>|+0kz&d@jpXwimsB||HUd{M7 ztv=(&KNo!P?fMPdw1v02r)7k>N1xZxS~~W}-SiS2mirm?NPoA^qwu<~M&*r;4YTV% E00n^l2LJ#7 literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/m-deflate-p1-strip-big-contig-be.tif b/nodedb-raster/tests/fixtures/m-deflate-p1-strip-big-contig-be.tif new file mode 100644 index 0000000000000000000000000000000000000000..ddf75f83ce18390c9218774ca155ddf586329da3 GIT binary patch literal 1575 zcmebEWzc5eV1NMuD1)Dofq@0e1~M5$Kx{@v6fq&N7!w0Chz0^C1_2-h0!C)I7$eMV zMm8idkcl95f)MpUCe$uQA4X9mbxdGo42)thc?OV9R*)#8I9weIqdO3~FBg~E+yCE_A*s_f|Ic8?FbIoo8De2k0x&Lhsa+;^?(mh>!Q||Ofr*+#u)l6C+ z(>R|iR4Zt4#9kp^udP~_ANKv1d-3_mgz3lj8b4kT-dFK{#(BRV&!)tmTUF$^)>|s( z;+tJ&+e>yWKd#I?Px9lfCH&87Ha^*HxnRjL% zYtFQpcIHY<61+l|$h-?F=%tsbwKXOXAiC)Igb?eU!FZhH-%F0PE%{hb)=+Vbi| zh{5i;wBC(|X2+IkxB^4cYiS55IH&Y(>TuOb+Pv|mgmiK;C@gEf+f4Nl3Z)oWOwCGe_M1nZo6#3eL;Vj z??$_xxXj-p5h9@6l@#OYqupN_x^anFnCX#a-8-`SXMfxMdO~|&)w7oCewFWg&Y%1F zZqIzfl_qJSN19$mgo$TMC7)K6Tz9g>ds_7-+i9;`Yw|^Zt+)}M8Wr+u=`s1Q|1vMl zJhpe^!={3xdFF45k2~@|sn9yF&3aGslV@lCslNeFw@1~R{+NI4Zswi&$DA{5W*rmG zw0XS2y|41!5_7-b57y{=nXbODDr&aXTdC3|h5l0fOFkAz^)LMxB6WW0$0VusOFr%r zyKSA=6MF$1j9L=FbQxG4d~J&9*;#41zF-&_9j2}As=25mXv!jyKo?N5)L7J^)WspF z1ZIH3)kW~=?6^f_f~9jVonT#i=fX+my=+Dq28-K_QY}8$ zrQFnC^LuV?{l@q5UN;v`V$QNyHqUt0jvA@^mr@yO*$h)S9=91Lvvj*{$`YCFwmDm; z{om6g|L4p;GIiyhnV#I&OA7q^&r5t@Ro<6$Y5SR0HZ@I;NX zj?oz-5Gp|f z&k7WPK)JSzjj4#rwZa;_t4syLW!jk&Ihwe51dH?^6#g@kb_XAg~ literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/m-deflate-p1-strip-big-separate-le.tif b/nodedb-raster/tests/fixtures/m-deflate-p1-strip-big-separate-le.tif new file mode 100644 index 0000000000000000000000000000000000000000..de901ad8a4db566a7cb16490e5310221a538a8c6 GIT binary patch literal 1791 zcmebD)MnsdU|Gt#mEY`7bFD2(pbb~u-Ku2 zBnCB(!H|)Ofd^z65X^xJ1|g|~$RNlKP&KJghbu5cQzK)t^^2u4c)qQhze|&JXUEWc!P&2a9H8oUS?{4L%XQ$^+Skba&PSd2WS-aYn zZFAr*b26L(a-sTb7> zT^hH!?C!7Fr)K<>=c4z0I&JTI>T1T7Rl(~Wm5=$({5tW+Vuwzz(pg(xUP^~}KVnmo z@60UTsFD|k2@vl`ZOgjb<{Mr1hvB5Ob~?oAEDRzH0q>XdoLazhDsA)3898RNjWn8p z2;zQF*bBP0aDXF0aOsNGyLatc4I;kJ+LCc~S?uz>y}x)RO%rd;QO(}=#aNYrdHZHB!=+HM zMtB-^xcGlDIA}yJDq6X;zYw^)tMqlq^1QpdT!9?fMb3z1d+A2bwwpPdZ{EnsG26Cx z`!TlJIUCRF{gx>@BRTy}!Fl2Qnvd=s7LsK*9yciaSv>EGmaBN!EYIZPF=??&uUPE7 zqJyU8HlI$Z?`P?pxXhuIP4?T3_Y)TT{WxQN|BtYm<20qvStYk#HyrM>e7R-wx!oC4 zL@G~RxtRA`rsPQB@;Qa)Sf|I79Syyn%lx0uVm90Z32)zrDA){C-={EQ8#0lT4$($?Pt8 zeQ~w_ex^f7lVI7deBPd)pVT#!^i-9XFL-idr!;?D&5w`zDq4!V+Ur-WS+r`|Le3Mr zjI!X4cDVRY-z(5-X$ZJ9ScEGAZX~`jD3bt{54{@=&7^1d?)`pH{QjR0r|lgNxp+=c zbDzk?yWu#a``l0c42nueJSHjncS`B*dQ*I&Fg>Q=C~tYsWF_xLqf)Ck+wKdh&n-S_ zy8X`Q3&Q93Zt|M!S1ns|Ci8i0+1=dlwx6zWuiyLip8I^+vO|&Lv89(|`E9Qm9FS4~R4^!n(oc`Flp$7WAW2so9V)yaJ` TH|DPG-rB!^Se*ndZI#^te*usu literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/m-deflate-p1-strip-classic-contig-be.tif b/nodedb-raster/tests/fixtures/m-deflate-p1-strip-classic-contig-be.tif new file mode 100644 index 0000000000000000000000000000000000000000..354cf644067425c6942bf77473d6127bd48ea692 GIT binary patch literal 1447 zcmYk44K&kh9LN8EqtlB*d6~b|Jyso|*s@urdr#Ty*g_LR^kOWVEkZADo89`|mUsIUfLwkvRqWE&9mMw@=r`fC2!hEaVFP zAO#toKp$f2HkM`ri^*iNIFZ|96Tg+|*K-A{pQSQQkD-BwTF>A%J4?Aa4J)9Fl=hiX zBuu_^E0r$rb13%{DDBCEK?E39p(SvIoWmgyBwWHvB-ltYxeI%KU*Q(fxUDmJa6_kN z-|hO@q^oyAQ;SyyIQW$YHLvxi{^rm(mbI|qpZ9G!d2sU%_02d;k3Or8 z+gnS6LVKqh3H1q$mF*Z*k+o*1%w#f2KK=mT%AH@gyO2KC#J2k5zN|N38*8`-4r2{V z;EOExT)2{#L;d<}htM zDmygnN~PQEYR^=;y{j$RQmUegReWU!U%_4D#KwJxaVAJm9z^X_DqM46We_1ZQ;sFT z74~F=B(>aMhf|!X;FF!g>*RxI8wNv%5LrdtFcZN@j^~hTL{ZnQ-zcOWXFhafg7a9J z3vAvlR$M=5Eo6t2lV#BYZr+=?kt6lCv7=_b>p$Td$4SLaXn#|TAUxD-zhib{fV_+l zh;OS9Hn6iLA0NEVNzeLu^3s4$;*6;0+5Fq4V(cwfWGl0xkEv zVo&bt%Ska7pQ}`dB18N?m%|(X{mN3_fScuGX|az_x=qKo?==}e=g{M5^yk)DuZ)j(XCpra#stCa62 zmYUN^x@T!G<=Y9Co7w4wjw{LRu{Moj%x&u>yC-kLkyffBTs+nY$LpwO60w|Sk=`G- z5B175p1k{P!m2Mno>1lOpW_9OUV}FUcU|dRYb2w0Sx!vTyUk7)C_9X5ho3(|H7~v7k;L~GA_kph+CpxMd}bqcu;eO=O{^%|k>8L6jVdF7>)8L^HmpVyZ6`&V^kNmBgw-F z*M-B#aLSNUj>`~}!A&t_q@M`VQN&UVj{(4eQ6Ue3q$kzD7dG%|rx1j8i|}^o03v(5 zC_b-0C!bw?%T>&)n)#Au^|n8qlJU;T`bhbs2MKdmSG0Z}M~hF8N>*%UiY134y=nti zEAlxkc};ifn)b%R%f5wU6yLc%=YgE0Qo())e9faYcIT9gb+=^jUf}s5+o?F|z;C?7?SCO?yU5)|&{ZVRMbj&=D`-lP=G34m zi$s=c9K9hSot&K0V|z?4xqsvF8~OYHe!Cg#u$yo0t}ic)6Hct)y!=cjYHQ~8g~r!+ zeSKw~abt$3@iX1%?YZ|ia&MR0`|C^gksX?w#n#-|@c6(=?eFvU{{E(~qo$dBa+Yp& z-<;ha9~^C$cN8qtjI4A`4OQ2>TlwkP>G=~@v}~EvG^uOWuC`^{9JtGz3}=8GZK$EZ zz`%Op|EqPocXcZ{wR9+TX)Nl1I5ahY4TLt|^zkv9?JGIO5r`muQR>>WeeT^isrKxa zr##OUO?-XlsiA)5`SQBo>mypD+AYszo-3X?`_6OC^*?#)MYTeg#%(UU`z!XT8Gq%u z=zX6~+q<5+nsH@S@Onq(W4<%LPW-Xhq0_5$)|Qu-(jnfD*p%cuGmAH>ojs(G_ zD^~B`wQDtq_&#e(#?@u9%k%dB;*~T_yfsHPd)ph!vW3pmVF=HHk`>$yk&= zJvZ4ro~>8HJoVb5(Aj0OmgSERcJtf+Tbd$zCVE@m?R~=2^K7fWz6^JY@(ZjCaVx!Y z3>azeY^%S&^$&83iwtoIz2Xvk#ogJ&t#}HIJq5yB;d)Y@4%Po$p=6&oim*4l-^VA9Lm020DC3hXOo%iwG<^Foj zNY+$SU)fq^VPkIX_&U`kPenqnY%SsK@ja`%HaF+(o4pK|Ld6>4Y1HB3|Ha^-5xJ;n z<0bt_Zl1_{N}20#rWqZZtHLp543m`$6&he?FYHcRb|cIYG^RA{Xz5;5t=??AFQ`7Z_@wFfJD)EIpWnO5YqDRp zY{{9-=dopXbHCeuy28DF@7H_o^JU8pMT*CkUXJCr`F6%Tf6wQG>hU#S&z9TgGHXxL zu>^TwBO9m)4Y;Sj6$o6NO>ZmuH6h?sdR8a*&D@x~wtH*;{$X_zw6s-r F2LPOUk|6*9 literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/m-deflate-p1-tile-big-contig-be.tif b/nodedb-raster/tests/fixtures/m-deflate-p1-tile-big-contig-be.tif new file mode 100644 index 0000000000000000000000000000000000000000..ec82ad7dfc2d5a44b9724493fc50765c15a3f9b5 GIT binary patch literal 1659 zcmebEWzc5eV1NMuC_{jefq@0e1~M5$Kx{@v6fq&N7!w0Chz0^C1_2-h0!C)I7$eMV zMm8idkcl95qDW#)P%$Y6RtSp$qza4~rLl;~AnApfr-4Px5a=!l;DJz#35-rC{(!jI z8H<<;#6LiN(6C_4VRVJt!NM%V$i*;`!GtjgE(Uc8)J7mxl9`s4mYI{PS5OHRWSjtD zu&ja5tlJ&cEn8SOHF_@!F=J><+&(*^(uk(wJ%`M$}_3E_M(P8SVCtp2TbgO8V z%;aQ?V|~ewZxrRo%$7Ajc6J(A!pJvl^3{}?+kAvwMFf@9i(JqBuVn4NkW-o2ShLlI zF>0Z}+OU>u*)97ndE{Qss9nDC*5Z%1mT&($`QFv_RsQ>5*Zy6}|7H83En$JTRz&0m zHkD~a?w+zt$@fB??0A}Rd}u2qboj(Zg~W%zkH<@yf-Dg;pKsxJrNE1 z-W&<6ifh!GzGf+UzFo4ao9F9FLq8lDM(H;rxC*LUb zDt$HPHRSex_V{qppGZH0%9hQ28D@gV!-Q^na%^iS8Lc}jZuP#pv*KXytUD{N_F73C-QJUE{`kge7e0fT&Uqm( z-AwaB-n;eYU3ue{n|I}<+ugh?uicnMBzztk7+ai@>DxRZSm$Yv^wu4LyQNEa2KGyT z-FZ==^_gR5WpG+uNQA&uv17;QJPp?SWEuW*QgqE(Tk}`x+dKC@JIVO@;e?;TnFp^# zSbP=pU3~OnPUn_LuKOPv-dCRCH*1m4m9W2Ez!$CHt~=$i*h$UqhdoCxCwXp(){33x z*q?ea-I6CtZ~u+Yf70|nweLL>|4r-vt?W5l17o%r#%wLTv#ImW=96D;n@m1y{!^&!se0KQgTeRLncilp;0JIBR#fpMN)TES>}@oVSb_=dh1?1o#rSf`82d~ z#wYc(zf)cRr(CTU?eKNq_%`C(+!GsvMLW3D3*W}Pi#@T~Tl9o{%**rZzLj79y?wUm z3Bh*-TXxI7uT!s%Xq@wqGrjuO&w1q=r;EOjt&MazT`}kFLBsYYU}SN&VJUZroTdCw|7^MRK9ohde}D*`_+d+B_9`aF5&dN(Dt zq%lgNKT(NS;&LvB?Y@M@q(cVqXs}{FS-u5|IL!@ek z^{tOG_bTQjd!<+%nR2jXlIHxEOu>?uOWA}NgPAWoFDqLn?o;dYe&#Zx+GGjV#R(!j Mk_-$J`nhfb08E&9_y7O^ literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/m-deflate-p1-tile-big-separate-le.tif b/nodedb-raster/tests/fixtures/m-deflate-p1-tile-big-separate-le.tif new file mode 100644 index 0000000000000000000000000000000000000000..2aa10e68141f0d9e0434c9681d6614310c178b6b GIT binary patch literal 1809 zcmZuxdpMJO9DkR#xg{|r+bEamY?N&hx-41cGL&JQJPvK6MJ3}jbW&1TE}2)yFkOy& zxpi1Vr_xl6MY$A7mX1RQU8$@%TekD~qwn*4pU?aK{Jx*x@BO~-=Y3sWaez7i0J`#2 zgbD%3VgNEv0SF~R7I1n>ZV^NcSfsqxRjSqGoJynaRc%QR+8Hm%%Pgs`3D>sqWk^XD6|nu?RNPYs+r1_ocDrr z6}<~1niU!v%8FpFjf@5EDEs%S&3AlI;?~9Ub+8h(VDt4KN_?lPAw0kXSOr{#WCLDu zR0Kx=ZPM763<0)EKOG|@cK3Go`t|zN8XUN64KhvjbIj-j%meaB3qQ3L&vPJP=us3# zdTJ{lrCP!eJTNaTpO0==my+=rP1^Lu1uHukp*Ckuk@cKW8s@ya{un}9Ksz1Z79fIM z&!k{kg`m@NEq^h#_ZoL%d<58O?~3VrmEmp-jR>)n**D`o*%xpFLul zfiI2fZgkLt-kv^}F(O9Z}M5}HGAp#I5Szg zqtAtCUHf0xwbO5n);CyjmRNnv*suDn)k zM7z0F$c9W>og_uCK^dEojzzh~ob6KQ{qZ8rHb z@Z{E~6jDU?rSMMTC^8A^b#!bG>5Jr-Ss}G^^C&tAw#+G77ugKwHYMCg9lyvC#I#=4 zk-z%IosD@rOdnCqc>9WZF17(m>~b;BWtrY=dw9C{49#(q_wW5!Ir> zS&bC~k#%!-;iTkA=Tw{fH@+c=?f65tn>EABpB<`hwQHP)qoUf&zCp7C+2M0{%sOuN zC%kRlaC=%!6I)(7V43CfbASwL;Oo~jYvSa)%8?_GwR}GLj%0?^Uk9p#wxAw}0U@9R zhz4=sQmPFV0Ro^shyt;o4u}U0Kr;{ylBkAME2<{dj=GF$p)Xov_!t24&u9#C1nvxR zus94$-+KpQ#P83U;TONmGJ!fH0U)7AZS1>0De^Dol=Krx?nb%&OxX-dI(OcNR$x!H z(Vn!5-z)s`{E%1Eda83NAburC|88AXv#apdx_~k_T5(iXm6~H=fnz1rvBbTh@>$%a zO$Jx|i?Fn!hogy=uZt&N^Dc}D2mejM`V{T&DfOFbsIrVJtD7l3G*dK3P%EXLH0mh~ z6~3-HHd+FNjwPEbgI6XJcF#s zVq{3ROtUl0e0FVLzrNesYk56l7g>WT7_$ARej9p;?PD^r0ySc2>Cm>l@^DK0rV*%h z?ITZk{!rV%kj~OrF>df3_s!SKc>vt8rAcRsJ^1!S)z?=hQKY5Q>SyfRrRkA{1>f@4 z&TuZBiOYHrn^_W{UVs|*f8a34HJ-#@7%ICk78~@688k8F8JS5HjA)0&UthuD(&8#M z#HG~5mK@pGR=+lq=N>o`6xwziqA==eHY5_bFP(d{q)}*nfEC`jUXME>cjm&xgv;G3 zckGrXS$7z6T59OoRDU|2b9%!-!h6ZOgz182*UUdJT6pm?zhPr?YsZg_5gCjGvA_Z+ z3%47$e@bFY?~7&|w|p!zsh^|Ce-Yy)=XZNTWlUcms_57r^ptHish3Q&9={FTKC&v4 zBhcD=mf;rc7AB@+nO8#ghrZsM+hTi?wWW(tETrt=|4K9<_}cUkh4?9gM@X)uHkoD7 z1uLGQ$oT1m{E&wB=oD@ULYoBuQTh7vDK|DpXO1oEiow88L#w;Aia$|aM2gQPUS0zD IWy9Y62UDPz6#xJL literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/m-deflate-p1-tile-classic-contig-be.tif b/nodedb-raster/tests/fixtures/m-deflate-p1-tile-classic-contig-be.tif new file mode 100644 index 0000000000000000000000000000000000000000..c19da0fcbda82a3b95f7551a7da6e2ff0718cdad GIT binary patch literal 1531 zcmYk6eLT~79Ke4+Ym%&xi8Bv*dD*BG3F+xPY|X>C8O!4((>#sjMhM~L*~~*8r>kLx zxED5ZojS==MYOG$amndW@;H}E=SI%;{eC~6&-?p%`hLIvd~rBH9{>OuKpq5u9TJil zKnVo5SrL>1AQGPxATP;*Y%(ax2SDj9mQ;Z7EtZn-Hvn|2Pd})(%`ZX3SNW)~*ih0N z0OTYx23l^PD-T+I#nzj=alaW5y;-}#e}hh2eI%Q$v2p)xp$3r2V=hZuScm;=Z1%iTF47tc|%rKX_)*7DnasT2Hj%{5Ce;i{fnyObjO)W?2 z`>0q3%L&S>5F_;_zYh+Jg7FBOMvJD@QF&N7#D~58X5dy1@m5+GIB+~ zp?jf9ZBXX<2huL7DYo-O)wmihtRShHhB_`^tEsGEFL*Mqf2P>4T_)sjqzi4LDNH;1E9Nli3=6NPS~ z;;@Y@w1Qx-Tj@*qfN=?Nw$nPe>k=%7qLtkDxT#EM(Wa6vXWPTfMCgmy!mJq)yFIH! z#1>?kt8}(xUNoH;dTPDrXt7m@^EBpoi1QpKD}?_Z6CA>y#*BvW-(et1D)!??kC{C| z(3Q;Y&CJ~pw^AI7HLrga-6fPyg<3Dn=wf+SUholHP<7i@3iTR zUrSos+g@;KoW4p3Xy?0{iIs8Xor9D#j+^Z7^YL?0&-R)mX$GsD9SPmzp>^(X{)BR` zUdDK4C(p-*ttyTQYur8V-A#gw*Z7gqGwg+UB1-jieK$-N?KOcOQr zE{_=J^%*&qUOv-2v=-b7_0U)!e#dAry+^qmHahl(*46DW8c|MC;k& zsW}Er6k0D`2cOSWLg%7v@#)MAs=`J{Cyl98$&Tyz>r@TzRtdGjq^z1yC@ZKs37fVd zMe}qt;eoWv?r#cTeyoCN!vj;y)ae!bUQiuamZS8lo$1S#sw~TdkCw2r{;&5QUaRs- zNWGJ?LmGQAiP{BEMUYY635rCNpH~9gC&N7=;14>{0iJ45KQ&okp`kkIl2EjWB!9k% zUiTGTho|DsHO#uMl=Uo1-S z&Eayn;v+)gj2|av4!XbiwZ5h(|5R-xeu*`0)m?Z?6LKA+t?NTpRCXdZn#e|cFK7!| z<%{d)UJX2KU{~L3t*GR18FkFI#vg0Slk=LF2q>lKqLab}#8@PQWb14GIKQ2mtM}8i o6hW02F0BX>AUrEp1Z%&2to>Xu%P5wl0xkDel2Zi$dbaHFzZLR)asU7T literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/m-deflate-p1-tile-classic-separate-le.tif b/nodedb-raster/tests/fixtures/m-deflate-p1-tile-classic-separate-le.tif new file mode 100644 index 0000000000000000000000000000000000000000..67029d098d8a0426f56fabfe1deb19e0816a943a GIT binary patch literal 1649 zcmYk63pA8z7{}j9lJcw1YA;?v)8cE`v0#n`6_=(4fMw4ce3xlS{_Ox(%@| zQMt58XhU={43%3XnY0#*E*r&!{Yd9{&-tJC_nzl@zUO(*_y0aSJ1h_f0LTD<*bERb zpZPAlGubU{+e^ewfi4a`tSf#UVBU zDL6g?%W}OhJPokgz$BPgdrrZ!hHF<@5^j8S{Wm79N-Alz%4`uaEABqi^WB@NSusEfJ1^W=Se7+{@#A~ ze)k&rQw4euU0p6qgNnyIA&o!HOl-$-Oz{|MD4CX=*q(|05yIddF>kbAPgsi!NVt?1 zDeC4zg>IUke%xh}tQA^9lk?CMgHQ76q~bcfT1D@slCg{;$YRG9Pad}a4tr*L92_>W z!wkMlaZo`9`DzPI+Hg+HJe}duy$h(LB%D@SNKCc&TgAG2-Bh0sSFWWUbwZ}Qy0j{* zd`k4VKSp@7z$Z3%I_ib@_8gK^{mOG*&VM~kj~Cbs+7k3?Uf77`&iJYd6&Q&jm`vK$ znd2T&Lj(H4{mmBAhJ&s>WaCy{t0es)&oRfaA}V*(mB2T8;{Bg)aD1!s-zM6ouLrx% zvUT`%&(st2?sWdt6;_epJsvR7IH){Z+Z(ELyN0Leaj!q=G&4hZa5|)$^*kW3ym~}5 zXL!uwS|pF@Gv846+bBip)ZIt!uckl5c>FbaP<3H=;K7#!Vo*kY;4{L6QY_N>+~hIh z0{@rgTFSG76Uwoo+bzOml-k7DEzytB7p}9o;q3*|@XOu&tTD$%{W)2KbFzeEYv8rM z_tgX>lviV@>ytrGPg-&;Zv zI&o*3+9U&SzdBRZZqzt0h7Rp4TS8`dGXqzeHM;K)MSo~FZkiX7#NI9))=qQ%*-MC& zcXRJsR<&?bGh=auPrqLJNYqf0+Xsn529PX-fe?@>qzvgm+bH@J6a*j>2n}H&X$S|& zLmH46M5OGZ=u#vpMwIOoExFb`yIug08z(Ws;yTcH$9eu(>WOBAV&+iRVrI$PG*zUP z5&+SCB9m+WB*WjsWx*FB>_*sr>P#9rnf={{x=>%0;<2Ri-^<-|+?8%6^;Kn)A)Eq4 zU9;|wWV_l2`@G8RJxW5;Dn-nS3e74gW;YxfDqcnAACSN4S&a24?wW|Hm@1i_;^a-% zj{KW|buA9)D|Mf1sML-ut6MBRvsk=>7b&G&QtT`8tDUMoKT#Rkn_(eEH5kjXOc?5| zkPeqzB*bzO>FPb1kIwC=jya8#v6X3w#b+sW(sw&}Ju)-8ldc>qV`H+NB+C!&ue!+B zgzU|--laIBaBZ@O53qy$cSgN*P`Qkv*qf$nrjR5Vv=qMiJTpbZ^@z2hVee7r9rdUq zBndiq)Zm|bOXaNwFGz%P^!P4q(+=y3kc6lM<4C=l=T2g|qaDMe(%T|$pb33U-S&hGE1-y6(v@E-T^>$ree zHR^6AA{aatti0daSgUu65!h%b%N~cFxlx!v*sZW*&0B=M{z~(LT~00aSE5)~jEAE? z@%Kf~7q;1@{&`)?nUlJN4bQHbK08UE(V}@=EgfN?(X`1+B2)0Vb-B^}bFpguiU<6Q zn7)zj;)E2^-CQZH=Z`7R8B`JbNd&#=CeU0_K_2g>-$$s$&EXw=v-vgg46Po~k{PlvGZ~-j+t3-7!1hH+F#rh7 mm4k1&zb!0va#K$@Mif1|yJt(u7xJ56-nE#5t-w7)^yB}#ewXk7 literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/m-deflate-p2-strip-big-contig-be.tif b/nodedb-raster/tests/fixtures/m-deflate-p2-strip-big-contig-be.tif new file mode 100644 index 0000000000000000000000000000000000000000..31308994a7f04017b7d53ee87c1e9bd67a97376b GIT binary patch literal 571 zcmebEWzc5eV1NMuC_{jefq@0e1~M5$Kx{@v6fq&N7!w0Chz0^C1_2-h0!C)I7$eMV zMm8idkcl95f)MpUCe$v*1V&LLbxdGo42)thc?OV9R*)#8I9weIgD!&}g8_pAqZC9A zBn`ui(pbb~;CdOsDnW`hu!tD~{R9C#5Q?#Z(H2P^%zZ&fVo(jJ6ypS_{=HE8HH2o8 zF3C(wOUukj)hnn33suZXPDt@cN?5KGl8~|F22V=H`3((8#uE}{4%c;^b!gkl^IgM? znUjgZe->vSQ1OhzAEr0X?J6)|%yTqN;c#@%HIdnkJoQFyfjMkKDN_>GEV(1X!oo07 zg=-;DwMqJ%g9rRfwF}EmKV8X^JAEllyQ&4y;a%}yS*-Z^SvuCu@MN$WIUl5WQL^Xmu0M&O3N`HgWf>6!sP}&hn$3W>yC@sYx z!ywHd#vlbGWq~nKl9`s4mYI{PS5OJIq+(8TN`g9>W<%%UD-e_Th4mcJ U328|{C$IsXFac=yVm7880MANgiU0rr literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/m-deflate-p2-strip-classic-contig-be.tif b/nodedb-raster/tests/fixtures/m-deflate-p2-strip-classic-contig-be.tif new file mode 100644 index 0000000000000000000000000000000000000000..6d088561d3695791b26f54bba31a3941f479b753 GIT binary patch literal 443 zcmebEWzb?^VBla7U}Rum0kVJ;g9sxdoGrx2#J~&`V`2~hs$mAR8G-5<7}=m~pbkbs zuzHaGbBv-;aVDU+7~C9IMsct>NXvSQ1OhzAEr0X z?J6)|%yTqN;c#@%HIdnkJoQFyfjMkKDN_>GEV(1X!oo07g=-;DwMqJ%g9rRfw zF}EmKV8X^JAEllyQ&4y;a%}yS*-Z^SvuCz8V1gZy# zodaS~s5nTE7}OkAAX^+L4pQ?Dh@}`=!RGt}vZbMHHlRT=NP0Dp%r|6YV&DO)BLuy zIzqyMRxxPsu^a`u;2_ATJAO-cWdohywZuvgY~m6Z)^k87q$L5Jzy@@}1fboE*_e6& Do=;|l literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/m-deflate-p2-tile-big-contig-be.tif b/nodedb-raster/tests/fixtures/m-deflate-p2-tile-big-contig-be.tif new file mode 100644 index 0000000000000000000000000000000000000000..4e1568891577d5338f1bb1c1b3130fd1a1160783 GIT binary patch literal 780 zcmebEWzc5eV1NMuC_|8ufq@0e1~M5$Kx{@v6fq&N7!w0Chz0^C1_2-h0!C)I7$eMV zMm8idkcl95qDW#)P%$Y6RtSp$qza4~rLl;~AnApfr-4Px5a=!l;DJz#C5*O6>R|Rd zq4*8r4`(c5E)YKg&4PvxV*{fr+&mVBWQM~Gats=bL2xms>!7XxQYD#bX=#}`sd@#K zP(j885C#)8ESZi$`A;h5BqyYJBqc0o&@wOyT5^A0BBSfC#}eOym+YVRP|o$&bAvxi zJ^tspNp^j?pR{F}$AAB08eU(Fy$r&Disuxvx&AsVu^p&*%10^JU&{^ZfQr-91iQY( zCmAhsv1j=7Pqw6D&eYQj`5F^N+CTocG-h!WWib|JF;|!|bLIm!mV;+-30(aqx9I(m z&()ur&CYTB`CYb8Frw#USiP&=hKS&Zzmhu-JaWnW>YmEnEk1Sc{tll62gR(F|FvQz z0=nE@-S-k@3t2Sv*YZiNjHWMF{F`?q!NIv|)qXV|=BR>zzm?_9jBhPh{%g5?u;E=y z!2bJZ6A##}tFHfEz${TP=l3i79c(-n$Bn=0#|pC9a~*fQ4-BP0Cq#}M@Uu%1O3+&3 zVt-gef^~7i0R{&Bm&^q~S+5kUBM15#>UaOkGMd0JLu!WHq8I1?ojz*7<#td*0+^Dg IK4bq20B~ltK>z>% literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/m-deflate-p2-tile-big-separate-le.tif b/nodedb-raster/tests/fixtures/m-deflate-p2-tile-big-separate-le.tif new file mode 100644 index 0000000000000000000000000000000000000000..3edecca3ab766781e0822c147eebdb52c37284d5 GIT binary patch literal 916 zcmebD)MnsdU|)iGc^?03axV3fdy6gW2nZDvIC?R8I)wErKM%&r0Nw^GF*VF=Y=|?4oXjf z(wm_42PmD$3^Az`O4~9NGKewAG1viV5e9jNiaE(C3CFobIZoUbp3twX5by0!$H(~d zF3XdA!3qC89pah<_wTV}{As}SX(`K-v^WyH#*o9Zo@FA zUExE;oZlx-oNM6cGI%&$p4C~Jfnnk*#_JVxyi=0W6M*o=uhR{pCJs+G7oO;ETFA?= zB%Ap^P&NtZLut+v`U(pB=QsSZW%^XldE&Ieqv_k#6y#kU_DyQ|!^8CHHs^_WWrhD! z8}@Jr*HtDnePU2o;diZ=v-`*azP3sKB{wwn2mFlxzu`2ufzy-x#}lr%pL%~#@jd(0 z`Q6TT;#%?T&imN4@*;OqQu+aQZcd07xH}nVRm`zIa^&0r cAYAZsI-_<4$JFPC6z#d%%orF}RWttw0C}_E6aWAK literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/m-deflate-p2-tile-classic-contig-be.tif b/nodedb-raster/tests/fixtures/m-deflate-p2-tile-classic-contig-be.tif new file mode 100644 index 0000000000000000000000000000000000000000..5c5c89bfe75f836e00c0d36d069ef78dcdbd5344 GIT binary patch literal 636 zcmebEWzb?^VBla7WMp7q0kVJ;g9sxdoGrx2#J~&`V`30s5MX2mvl)Tv85r51Y@iNC zQ7D@U$d+PYg_?7VQ5w#E#3+NLUIU432sE34fd`0RG1?-T;{={1&lP#&3GxhXB zzQ#n6_K*K9jaeK;S&W5Q%oS$LocVx_<=`1y0$0DuEqZ_CbM>cYvvVAOewXbNjOh6o zR_|)JAtE^9ujI}Hk6bdpx~DRCi%;FVzr!cNK{0FPf2~-FfG)RJ_q{~fLKaQ^wR}=5 zqv^{P|K=S@aB!|#wO@^gIjSJwZ)JHi<6Fy>|5|PzYbzq!~=Hgs_VZOFiRB7 z`Tfd%2OE#YapSN0v4U*&T*n>n14HT036UcQ{OnSM610}M*dNxAU|pPWfPq2(C368# s)+@#8$br6w`rZGsj3zM5keVU4=*9Ver;i$NxgFGy5Mp4M`i%WA00r~5kN^Mx literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/m-deflate-p2-tile-classic-separate-le.tif b/nodedb-raster/tests/fixtures/m-deflate-p2-tile-classic-separate-le.tif new file mode 100644 index 0000000000000000000000000000000000000000..8fb272f1ed8495c78195b6a807a51f9304b2389a GIT binary patch literal 756 zcmebD)MDUZU|ExeFPWWTvI1W#**n6;v`903~vP_#O~{0b(Jb!vlc$AP`>#Vq1nn z1~CRX20I`v!XOVd5kytYNlr;P&MnGu;j)DL>y#UQDegQ^J_q*w9KdH z3qEbKcyizA>Hoin_0AvWn7F^+IZj%uo}qn@cu1Wh-%pG*RM1NkAV;bDq#w zP}o1e;g2oTr+Us4rwtxW->#-0@9MB`Qo|n}rcbvyPsA%L{GZyeheNooGMVWUgSra8 zYsH-1M-K3{P5Ljnp{YOMXZ-&Sr@0NBp5#BCaJ~K1`-6(_*{9C$cD57Oif?z`$F3#c z?_6gZa*wh7kLHR!N_;=32UM!be3J1I_<6j3FJ zV-b^q>tzJ11S!_QB4!Bm69n);D8?y_wn*xrK4%O<5`$`B42Fw=LI@T{P+1VgxB$Xo zS_Gk)eWCQ>lFYQUw9K4Ty@E=HiaBd1Ec9b`6mh+uWHzDTvVxS$(NkSqERBLHIn6KQ zH_YGQm?ZI!A@Zd98YZ5U`-#V%hhGj7c))8Trev2dbt zt5bdTM`cID%*R{rOcU?BwR`jK??)m-f7j>sPE>GhDnz#i>yfKNJD`BVJFEk}SHLjSkV$SXNHx3*)!NArNAjO@KvSGEw zf$t6a&F7oWH~BZlvhk?i_<2D=|M&k&o)A{PzKS_V8z#)!cD5m!?UkM)PkG@+XU5yj zY-~vm%*=(aBxL3q9Nl0U@phR=+v*=T9yUy!J9BO;5I;>|WUjlw(CWgJ^8F#FM_ZFT zM|h#~9oAA&CVj6XtM)Q-t8iIZJuDaJStI;B`ME=j827T%3Og(XJcF6#JX9DM7VcLE zI<>ecNr5ZIG&wyzJu&@Q>&(cA13Ng9;+XX~n0a`4To~1r?tVWiJbQJ-!gWp$p7{QV zX=RbIvc<6J$MP6t3026V-9q z(brnZ?adt6aN`3r!?*9T5L*xMODKj^NE>r=8_S(Ja)#y5s!-k^2N^X|6!c}ndWd83^15RW_1JctJ}}{6W1~jHGdq)TMTeU~BH@OMcX2sxPJLV~Dx_k> Kz<4{~gAo7{_$ZqI literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/m-deflate-p3-strip-big-separate-le.tif b/nodedb-raster/tests/fixtures/m-deflate-p3-strip-big-separate-le.tif new file mode 100644 index 0000000000000000000000000000000000000000..a8344bf5dba7e9037c428a88f1cfdee898df8c26 GIT binary patch literal 1206 zcmebD)MnsdU|X;!?3_(a@P$7n3pcu^k%nUG<5ZMb*v(lgn&O&Jw zs4_n&eE~}AvOx6ZLFs7>=?vivS`1ALu?&t39ti|nanVoB$b=wNOSIKx!<&s^pY3-kE~x4V{wNr&25IeCsWHWys0S6G$K zWF#T&=(d;H^7WxkURHJ~E*TvYI|Bjs1J4}i9OW?K(s}lWDdS4BuzH$?MUKaY4HFod zHfpgwI^Z~Av&`FvX^jV7YO9x-XxLYyrs@H97Be1Tu+;>GFjmaDegD9T0|yum z6&%PYf8f~G+S=69)59V!^5I{)0-uG!qJo2aGTDqA6j+SAyJb1#fY!=9)M+`G6;xwz zfRCA7vV&RRbAW6lgIU*XSxKEFpq@oDS(#q{u()y2(U`fAfg$?=KhT=uqysGBHv<`s zxsAOAXU$?$NI1YWXG2G=qr}luhj~%?WOei3TC-7Ba3cj5uqY zl%AaC*sQn0ZQ>KVa=inwWW6`NX7*91esZagecK zFk06rm?k2TV_%rm)^xayg`0aqU{Z}j!XXEh4eqAgPfpbGOWEj@IHYlYaAbU*&-SQ+ zxhqoOoEhtaKp}(06*nCp@G%!25IpB3k)s9Wc@1Y5W^4t^OF*E${H3J*qz}n+*=h&ehrkM>3 zRxygoFfg&{9N;{wBQT>aMdE^no4~)G6tTmM90dn*<&-zw2Z7W@Df~Rx^|_#DXM{aY<%cT3TjK zs$M}QP`F~w+6fE&m>or2?T`bflGH-90j~joY|BcW%;~o=AQ3IPR^D1cx7w#6R&N5G=AMb`SauV zJ5RH{PB&S4XFc;-Y4_{%ywlu#dMX-|H)J6y{&91WG0&)yr>)w6DXL}0PYSIdlBv&wY1o@gwbsNCvQU;R&U9T zjNB?*7FG|-#d+2UKTm${&?3gY?6krTO99VdW;qWP28M%39{RQhd`JV~-xz(eTX9BwW$qW{^m@;o@Cfj+;{-7mEt17%?#3&i7yh0Dk)^<^TWy literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/m-deflate-p3-strip-classic-separate-le.tif b/nodedb-raster/tests/fixtures/m-deflate-p3-strip-classic-separate-le.tif new file mode 100644 index 0000000000000000000000000000000000000000..743595d357eb952eb7abf3ae7f470c218af997e7 GIT binary patch literal 1046 zcmebD)MDUZU|o?g1)?(y5&PS`B-_F-D%ftT9q zWhNRn86P%WV4SIXfStvR=LjQ1O;8A9#hlys51cq~fZGcnb8y6jonF|>hvLEmRttn1Az!H8lkkOdi*jsScEH;IN159%^bksUZ z96fc2r|~4mOE!xGi^OMF9unA`;MSOE5VCF|(1|~N)k${8jOm8$221T-Mh-sqfpaiOzuAtQ6b#n&yaXM`_XO!MV+k!-)Z$HV9GjH%Kc#sae| zB_r;b&t&=@nouauJ@Fkg!~b40umKLNJq~w{9qM74*|1<0qo@o66PwNf&a*lKGul!l tE@-$3{Od^(JIu&Ya3EJsdDD%cj=(bkiw{^xU)3Y%P? zbVBhPI2;(Av52`az`_U`K8#ZsUE$`jFrH)7We8{RXAFXiL0t#*OfXyw6l$<=13RlE zGc7GGGbdHApc3j_#sd%rb036e*$tst!Ql>JGcZ)lSvqmQ_Y((^sfSV?7~h{>#jA8EKxYGA{Gzoo2hM!RR(SQZ>F1hLT4$1{ z*vqEb|Ka8OJ$-@eUGAga99v!rhwWs2AXB*d{I;1#dqd5R=cL^iZ3?^l=gY=D98n*B zC`ZOwcJvxwdr*1$Ox+uPrT4Du`$N|(Xcu`XpAlHR#GGa4vE~P-Up(Bax~A7ozV?nA zw`|^gXX&@^S%vmAh?#~jE1m54WYYWRM^EqRKW?+KmG_@caejSyW5+91?duDU&eXnK zci~vinyNdyFZgtarMy*^2>-~krQnWFt_$BbzW?7(Z237QJjA^6HlyyHpx<%7oa)!r zZ;zaKq~z|pMzPHLYYJNWomg8t(coce9$Wdere1i>8!(vhPig-0gGOAZqWMRLkIoS$_ZIFK2GvqGoIVVP)hC z)6e{R0-v*Io4!1`S10=yQ5aS8ORjLKQPGJk(_6?Z)dCRz4xu+aX-$k_nF9WK%$UeWo>h!Nzc8ne(XA5gtN^x zZ0aW8eKE1_%5vTf0{*|77fiqW{hPuy-@Q96ez9|VzS?&FeoSY`l51D4nn$eobU!~k zb@^8=u{rh*?>^ScM*iL;W#7=c<<0k>)AgpNCMFngH{UGR5B;|`TXkx{ri|ueQ-UXJ z{eL)jtM+FPf2@0*TISs)ks z@6F|}^VeK1fA{aX{Pq3&xul-`dvyJ)xk~N)-Sy|+-ub-0&Y{If~sbJwVYjZI@{uC12b(8MmI z<8#JGr)Sapji=2`Ok^@FE)+BzY;4qd;Bw#}KNCpmj8D#b$qmfx9e;Uwc@`&#z)VyC KW>YgZAw~efnabJ# literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/m-deflate-p3-tile-big-separate-le.tif b/nodedb-raster/tests/fixtures/m-deflate-p3-tile-big-separate-le.tif new file mode 100644 index 0000000000000000000000000000000000000000..13d5fd57abdae9ee57c001a9b2a3a0feb16abc13 GIT binary patch literal 1642 zcmZuveLT~79RL3I+eX_kHjH@+=P@#;OQJ2Y=58&OLwA*t9G6N8OGm?KE-I`hl zuP*E2j`LzmDrIUa%&ANfJ$WqYJl)3D%OAa8ug~}M`My6--_Q5;@$+*8ng9U2LX^;3 zPyj$k0I6)EN<)h{bsPeM0Yc@s02cEWtl-qS%Gyex7jbHz{vu9Y!x%z+&Q-=Kh5QB1 zN#Pc?!4eV7!?r_5f#15Ipz=;QPm)-LS!nfc+NarRoWS~a35A_H}t~Y z8fpUefC8q$4d4sDSs+y>ry%v!g^ z^tOR^*_8`_ydK^9WUNfS{V%%xMeHTBBbB!U1KW*=ce1`Z{jfi{x_>l^y(jHh+=_V0 z)%+<`f}rHThJW-rb=%1`iZL*AaKagg=9Hy64#}4&5iS~`hX?S0%z>kg(EdAL}6O(fJa^w5T7+d!URFrKGgPYp5o>fq+x_4d11E!y6MY#N;f{EB*)6WQyKCv#-}|p$;CA6AQwYX$ zHWl$59_VSXeNKnBBf=oU;N>s80GNcAF4w+xHr)vGBBv^>?RZH~mg!Y}9DH&GAyi~r z5q}oe;#BV(JmJ%RKU&7^0S9M{A}5wxx6NYQrqUTzBGcAc8^i<*2sgu(n{i6D8HGy- z5=&tm2ic2oLZ`_!6$Wy*JHlYCLS#HEwl2@g;)aEQl^g%DwrF}~4-=CAO1@d%4HX02+;%s@iY3IxtMrV-Yvp!5mkL&0U zyN1ah@= z#ZQk5$~fyAC+=1ZV8r8<8!&z)jzQfM#i#!~rt@{LzH((E6f4uh1tv^pR*_<5G_*}( j*kY}Mw)a>UifcXn`DzpQnBtUBVpi%V7ZL;{H5llBo(~+2 literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/m-deflate-p3-tile-classic-contig-be.tif b/nodedb-raster/tests/fixtures/m-deflate-p3-tile-classic-contig-be.tif new file mode 100644 index 0000000000000000000000000000000000000000..7308fd24534b2205b8fb8f9b3bf0fd443ca82c19 GIT binary patch literal 1217 zcmebEWzb?^VBla7Vq{=o0kVJ;g9sxdoGrx2#J~&`V`5NXP+(*Rvl)Tv85r51Y@iNC zQ7D@U$d+PYg_`q>Q5w#E$0&oOUIU432sE34fd`1cG1@}Kfi^Na!QCLh=#0d60keT_ zWn^M>1*-?Kg&2dNdO?;4gV`XLgWL`RAb-JeNoHDFT4qkFUO^>DSOMt0cR=q;191`K zIYwQEa0Y*%1Q1lrSvqmQ_Y((^sfSV? z7~h{>#jA8EKxYGA{Gzoo2hM!RR(SQZ>F1hLT4$1{*vqEb|Ka8OJ$-@eUGAga99v!r zhwWs2AXB*d{I;1#dqd5R=cL^iZ3?^l=gY=D98n*BC`ZOwcJvxwdr*1$Ox+uPrT4Du z`$N|(Xcu`XpAlHR#GGa4vE~P-Up(Bax~A7ozV?nAw`|^gXX&@^S%vmAh?#~jE1m54 zWYYWRM^EqRKW?+KmG_@caejSyW5+91?duDU&eXnKci~vinyNdyFZgtarMy*^2>-~k zrQnWFt_$BbzW?7(Z237QJjA^6HlyyHpx<%7oa)!rZ;zaKq~z|pMzPHLYYJNWomg8t(coce9$Wdere1i>8!( zvhPig-0gGOAZqWMRLkIoS$_ZIFK2GvqGoIVVP)hC)6e{R0-v*Io4!1`S10=yQ5aS8ORjLKQPGJk(_6?Z)dCR zz4xu+aX-$k_nF9WK%$UeWo>h!Nzc8ne(XA5gtN^xZ0aW8eKE1_%5vTf0{*|77fiqW z{hPuy-@Q96ez9|VzS?&FeoSY`l51D4nn$eobU!~kb@^8=u{rh*?>^ScM*iL;W#7=c z<<0k>)AgpNCMFngH{UGR5B;|`TXkx{ri|ueQ-UXJ{eL)jtM+FPf2@0*TISs)ks@6F|}^VeK1fA{aX{Pq3&xul-` zdvyJ)xk~N)-Sy|+-ub-0&Y{If~sbJwVYjZI@{uC12b(8MmI<8#JGr)Sapji=2`Ok^@FE)+Bz rY;4qd;Bw#}KNCpmj8D#b$qmfx9e;Uwc@`&#z)VySVqh?16Ji7ajF8Ly literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/m-deflate-p3-tile-classic-separate-le.tif b/nodedb-raster/tests/fixtures/m-deflate-p3-tile-classic-separate-le.tif new file mode 100644 index 0000000000000000000000000000000000000000..892b12bf292e2075feca9b2ce859a0fbd64c0a60 GIT binary patch literal 1466 zcmYk43pCVO9LMi}{xd@}-ZL1+t6{y0jIB)?%_KB-C#fvjtqe_;O(lg%OM@XcDyi&< zJd#?c&9t%0I+&zVMvV$%RYnoL#;Yc+*WT&OIr-i5{r^Ar-tX^!@Bf^yzrQok1^^fV zfM5Xvi428k7_I^gf&vVD+rWWBR(sSPcnz!P#u`@p%n{sY&r8Tu4J#pAEburk=%phV zpab_+P+ppK6tcJG4hE3dEnpwW>hm%1U|;B0GY#@)O)s2+Es)iBLE%uB`i={AFgh|a zGCF3LYwRJwf*U+&c7)bVP-##pSOAzf0EAF>NGosxbT9*M0AKLU=kI-a@QZRE(!XnU zBTL3Ze#~_wclz3`b8yhwbTX#rwW7myZ~3_wi(HLZ^4>0mitXA)=cDr}h?tY!aUGggz{&+pU^~pqqV*6iyP8W%n ztd3OQ4i4@zBj3sS>h!~*(AuH#Xzrf$WAQ5z=vND;afza`|C;_W>M`u1)=>{l8hULQ z_J3+w*Dje$J;y)0yaAKgw{zMwVt6%pG1n)1<(Sh#n#_8`NQvedcfDzAad|dD<=N+$ z?|vZ5emUw_RvV`rbZOo<8Jph_Hl{_tos95{&g3rEYhfAqGDZG=2EpY&`)d+pU2t#q zTGN$q2V7F(P{VSo9&tV*OSbU6iM|!i_O2?B1mNHAz1M$V*IX__WyxVaYs4fK$ z;cWqJvm`*i&)4o~vG{?AU;X6p==f1F)9{g>bqzy%IxRcoUV}0$JGxBYQ(3@aY+C;&Lc8+|r% zx$-*#pdb3%R-e2nU06DN0jY3KU%FHp(?-xXb0hfOaHO~a5W>)g4XfKE63JLe>c9~% z5{ja>>7Vi)HlE++aji_jT9=xhktsFC&nN)|vzMUhouij@kvN%iuz3VSpK zbe|Z^B#3t;c*t)FtUcVzC;r}l{eqxZXDXFsK5t)@(Cvwz0o&*Gg*zfmB28ZYA`C() z2C`*(*Un~`5nklgM06f6>(8;gYOI5vTtNzNcBo1?i|X=gcaEI!>AD{y7xaUJvu07M zWp!FS^!Pi0AUMO`R_(OR|1{ zoY6W;L2F?|ti&iCYo-f?hyZ)AeHw$I945?%BuAOcu za5KPlHd+hmdBU{I5@b!2t1}@oj-=(J+|yW0mOMQ!s^G70QQfT?CP*f$HxT^GmW1@F zN>BfJO#kZvV|8RAVPx6`z=FfcDS?sE(zA%=N_2}l-xJ;F?u~xWS6g_*mZpZ2bJ8}s LQ4k=lBOw0+SWO)0 literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/m-lzw-p1-strip-big-contig-be.tif b/nodedb-raster/tests/fixtures/m-lzw-p1-strip-big-contig-be.tif new file mode 100644 index 0000000000000000000000000000000000000000..1e8411b43910952e18fad5154c148ff26299a52c GIT binary patch literal 1871 zcmZvb4NM%@5y$`YZujnS5WL)(JD>y}+(FL4p}0v??AX?V38k2iGj<4dZM==J8#|cL zIB9Y0Mw9XpOzeopI04eMTgy_3QpcAnbzRF%pHxLc&4-W`Rc@uKD>WrFiYhCzBGOh- z7lSQF%B*&0_V@ntM!RpOwG~y6xM9{xu!;apN2Xrp}xRCJ@-b={Jd z=qq2sif`unNh>u{U&7Y;-?VHwE2>`VxJ9?J_(ey&{2nM>_C{ve2iqUtzi;2Z{k?mu z`wuT8|H*&~Ed958n7jaEE$-uh8@P%l)U)Xr4tnq{4k=^Km!?^Irj4KCK@PiS1{cuH zIZjaFeeC626&%M^^zte%1j@l7Heg7!p#i2g25qZd=s!ZZYobLFmjl{yPD?L(58sWGl)JYh%+!9J5VN{11Dvj)OXI z4-Vn$2)Tj28GOGJxziqb8H6s*qV#QBh_fsUE14ZZ{o~{ zHR9CEZn*nNteN^0&ptOU*13ui<g$E{QZYam9c#elIcgU5@tH;?Nk{w{bXFOASp(eWuL2QuyUw z&QEIb>NGmHb9jD$9F(faoB^s~+l$&pF@1yU-RN)9;zz$i`jie=YdpVIiI%f|JDo?x z1`ns+cE!$f$Qr=8k)qEr)lwiH&Ujgwx43dqwM7jgRROZg*o>O-YaOHYe_wNjl8iRea901$c+2Y;%_g zai^;?=77lMW}Sb*xJY}@!m*U`XWEB0J2q*2LI;sXT$;J`IJW8dfO#h!@q0hMU^WVY z2W-`4J|wd6Ak?h+l(5jH%WBPO5%!UGR7kHaoUM1b2UA|9V$LyiO+_> zmbOZi2;GmmE-lv=ll%Z>JI0N56L=<~ti*-&mJI3+%u?`K+qnHKTSFI6-@HKKY;9DXW9yQ}mMTfN9k-43ud{Ux zFE~~TFWT0O*A=zwAUx4x?%66UuI#GirY8T5}$@92ue9 z_iBvIzf*Z~9u4358AaO0jh3Z(*=d(en!gseV;9P$`JPB(w=N$rKN3~g!{rMyD87cL z?L?O>7wa+Tw}jj*Yz*mSt=uWTi4jiD$RCI{jM^0i@_F$9#!xXTUlY5~t;)yEt70wh z@e+_Z;%nq~f8R)(T=jqIl&t>% literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/m-lzw-p1-strip-big-separate-le.tif b/nodedb-raster/tests/fixtures/m-lzw-p1-strip-big-separate-le.tif new file mode 100644 index 0000000000000000000000000000000000000000..49c9858ba0519a6e1f16c0e4cc0dbfb70542a6c1 GIT binary patch literal 2075 zcmZuxeQcXY8UNk$UXu62sqb~NF6~n4)NvfoaqL1`RV>{uP12+xtGuZa(>B0}Qc?)Q z5)tcAkq(-5p$(ns(y3?}m^Fj0ADh<>=-O(Wpujc?GEyZfsK7wR3It(^3PmU&&cucM zv3qs*y!zd9&&TiXd3Nkbzy=`9td8Ka02l|W{WsSnTY-gY7%>Y}`xiAUmceeHt*hg ztC_<_MhH|Lm@!)eA6%igyz~FHSksxQ1^$?|y!QxSmT%YOo3$j))-U5;Q}_9YvpOyi3iNNx(YjvPh%Y#ie%%E=l`~1L zm$sxX97Tt8L9=)XN$F`TfVa>i8--AqQ9IFefiJND`pbTrKg`x*C>s*|s9}4g9IEiQ*k)OVNNYjhdMRJ7#e=~I|h zvAkjMNt{sejGLnqIHeN8Ez|e$is~wP8TvJzlQHcE=n&>)T)F${TR1Hf(p{h*;G9gt zdz5~I*JQW$uF^lvt;dV*6uoF3QX=Or(vNT+$+R~{zr#gzSG?==FMJWQD2DkO)}<2# zkJB4CuajACg645ir^4PmfiW%1Vwd5{*4ql=3g5z{R6Q!0_Y}Q@ODdJ|Dr7NIeL`gU zdgiEYB~jw{FrfzWzT_Wdy)vb|B{F?N_DM0$H!xTBL)_r^GG7j8f7Ec>BDX95GXEUo z=*x+HJjy)ur^QX)$3hsW_z&?9u{GFU^xxo*voMBpp)LGaHIf!ppJy76VZE|5l*ZHO zP_8f(zl@~vN|wM+(W4skmcly-OYDnANd@%V`QHbUf=$&>T+b zMA)4tjb~JcFwNpKcuvJihU3GSQ}Mj3=-W805*fEbKg2ne6sC{-7GIa0a9imJUXXF^ zPSYuzkqPB4(T{OnnjSGuzsE(Hf_H=dUCprr@rrw#&f+W*MbD-;@hXxzZy&vlOGu@? zo5UDFR>VVm9ZTv&*=we&cvUA0-Xz_`C7sH8w}>;+ePOYihnb_dmBlsQ%QmTG$;;DU za9O4DHh2TGR9{Bqc!Zhq7oyDXW4;nKm{j2=*tPXueF~#G|w1}Ko68{s~l4P5Q8``42?h#zg=C literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/m-lzw-p1-strip-classic-contig-be.tif b/nodedb-raster/tests/fixtures/m-lzw-p1-strip-classic-contig-be.tif new file mode 100644 index 0000000000000000000000000000000000000000..779cdad8d04f01a57812025ffd12ee959f15a453 GIT binary patch literal 1743 zcmYjQ4NO(%8GhdPp2PjPP(0i#_X4AE;9ltUrwrGwL#yV1lraRnqNU6!o;B#IMapWs zVRg;h@l&MQu!=3vzcXVpn$Aj>&N<_{FI$?B^=HI1i)*s%Wa}t1&0-qU(6woHaK`jY z-tT+9=Y7BT`+MHjR;&b|u#5mBK*50JL^pAi7@>e;Sg#qd-v|$*R!X(@6eL(Miame1vkT;LVifF8h1;`3tdGM z>cw;o7v1=dgl%IEf{ZBNbnpW1lZa=|;5@n{*Uhr=KK4kSoi&E5=#gc97?g{HV!%*n zLjz20^kY9ZBP_-OhS82}$Lz!@d>1(|XYnRnaLb~mcS~@0H^QgK z@cB5LcCD&hQmrUIq>T;hanvz={sMcjMzVD1P8`HH5%xm8)A(Tr@}_+9XAr(Hi?X*J z<};!S&%>Ttz`$nB)>%;*jq<|^!|ym;`nsqQEc56hETKOk-=)ilIy8@%P1i0VUDO8e zUwu6EtQRTmLV9u-r{*(!e9De&?7@nnH*xx-8b0}=7wLKoD<(h1Q%{fa8qXHQB)mY4 zfd~s6tCQ}hY$RaK(&J>IQ0LduRG_P3$)BckREiQOT1c0u5~YZa(0^z(%5-$mh|nFV zlh^}B6|KW2CwAGWr#d8D%cnugmm)hqLmm}Mv7fK$B9%#r7pO zey1?`eTj8Dd~g(7wn`+cmKvIn{>(OSPvaMRq#&jF)hTprlgNBOxhS)va|WnJ96#n5 zCG;(+_hP@!@K1h;%t;-o)_7*K%`ImFIh{l0Iv*$B_ISrxWcTCjaPgOzY$@aiX8i1# zx1?&3bCI0oWs(BNkgDalr24oRsaG3u;&?3&9JM3yG}8UA;naZ()qhJKxU$ht^%l4N zv$D6jYrd-YvEF}|%_>2ozg!zfXp1y?1vSP5bx5;Y;2CewE@`pjmN7@&((1>>_?RA) zEnfVpspw(Z>Ly$>MTcaYotQHnhHUq3$K1(b>G13sv!C;%Sr?o)F3@hYNIY%)h4!M& ziBA}x&;ewSP-Y%If~`8yZ?2@nf%X#%W+OB1b?i>_e$K{yuxHK3*+Qo-uQjJQ5tK#w zw0Vw8(dNZxj792|jGM5{<@Bhu+lg_DKO2Nt+A6gO+)oA2ZgBx^~!pR>lIGU`}{n9kCnES%dZg2Dp-xvoZErA|N5$B26g*qDeEc6xb=)! zgXdA-yg-p0ZLB&g)so0%=U8`P!nPVP39B8GzU5v*Q_%&o zKeCOX7u{&-Av8^XLOHE-M$KR4k}0{sx|KlAWr8*J2=$gxbgs26d`d?ci@?SW?sK zN8E-Hr?OBz!}nqol_Tmk-ia=|V$8hCt7W&Jgv#YR{Ld% z+$6((N~J8o{$LuOg&2hIJ_p3yG%n!;>ksMLgygsO| E0c7m3U;qFB literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/m-lzw-p1-strip-classic-separate-le.tif b/nodedb-raster/tests/fixtures/m-lzw-p1-strip-classic-separate-le.tif new file mode 100644 index 0000000000000000000000000000000000000000..8c5f2157e77c6c83fd2e7ad6bd491ef0f19fdabd GIT binary patch literal 1915 zcmYjR4QyLi75?sdH_7{P>i3+iOS_ahO&rH{9J`QK6-&39CTY?TDi2j+f&oU9l0p!c zh@eA7I?${OZ75dPPDRVWTeH!%-R89eN>h!iQDBUMj8thARA5G61%faTp=b(->nccl zSD)T_=bd}bJ>R+K?B2Z@Hh?f7B!rQg4aBM(F)|4YWZu46mXT{Vbq4jTa_u`X_BAiB z8r-$2ukBg8>ZfLZx6#*Z-ZWS*>hAdYr;+bjmESgU>pS*#y~F<&K?s@uWP*KQRc}7; zJigH_8N6Wv&Rf8c1QI4-V?FQ>g9ZG-;HW`u9laB`cIN+M^1%N6`wvWfvM2wr(K3`l zAJ(Elk-`PM8*7vyrWJc|m#WiZ2A@W&YGAQ~BPJip<1)4y`ea6Q;Q_O+t}0IBv)F)! z6503~I^g8#Af7>3%be)NUaZyiQ89-+Ht2>5HDd-H+9}c$&LXM394+A*?&kWeDB<&X z7u!BX@eRb-DN_OG(am1L0GXHQZGj_UTPyfS4uxly-QoMsxsNgXRXQ#6hzbUf!Q z8J3^ciKx4eig-?MuDCboDo$~%>=fuTnB{oEF!(6Wa3bsG=?ugp&oPLih=&5=)=|A`iRLLKa>qNIs7ClL?;gU|}yh&QX6`hWH3uO9HtBSwZaAoVA zMgN-IE|lV4P8B?+w=mD?tXCyV5cm6jPHq$q?=1Ued9Uz!un;KuLD8qu?5&XLTGg-o z3AstQY5@K%d7lW>pbo|iw=HTH2Uq16ghYSdKPY3u!$8KrE&D|TgVo?c`4O=WyGp@p z@?jChNItS%o~oUsh3gAKh; zAH%n!6viXmR<;Y2#0<9=9MCWx<5<>o?cQ7lyLLh}ij> z=ONmUCsbTHi}Y1IicJ~&EN#JYv{#)LdOuDfR&u840A>--JInM4&LEL-$LT2?*Bw!( zgR(fKV-@ElO<-2Xi>{<;oY9G#J4tgmr;|~4fixcHPTvfR&*4dqmkr0qFvp34%k&+b z=VaEc(vNVFQ@)uazr(jw7u;4lj%QRtyEAkS7gUnn75WJ-DKjG`=uKQvX?VBjzqK4Y zk*K<->3KYlWXZGXXLuQ@ymyfPhv&nGiry66#=K7F zyd{!?bbr*}C!@mAJ1hPT*(bJgs_Yf$uei$Tf(_XqEbhAp{S}kBW6_mkzGWFNr$zm;7m&5N1T={be~M znlO|J#^uM&tk_izZptr5)og zubdTKdbAk1CNGO-9x4PZKPB$r;cQTqhs7q|?MHI*+akfEJVx(z44Q14#v$3YbMB|Va4e4a;c4`DWrsK3Wu}P-WC7@$wJUct* z%x~_w=iGbtoGmWKR4_acY21(qAdK6liLgid_(Jch+S;`?B~l*h0PGp{jmj7LyO_7Bc6mUGTyn0M?v zrr-EokXn-aFoqyVQ3R3!zjb}0*(Nvpjs;a7LYS*R*wvR*7GGU*HOxw?o zY7WSj(R-Z9iDBVL#awRDEO$|WY;IKr6_r@P?XoanF7!I+9{TbT zPQF2y${4~G7J&%a%q9|S`saL#vkBd|s_DsBc~IvDKBfH<-WV>RZ*?N*!Y}w#=XZGN zpF{L_tty*^D23(0DYK=B2CG1{!2(|+)%g+L9aiawooaC0i%9sKqRA)InYzLLW`(OS z8ixirZF)^%TM;X>dQ1X4j7bL55T~==1>1UZ{(a8E_xHf+I5usIQniY)RbX z>Ba{$dYoAw7&^`3@_nix-a~VEd^OESyhaN@QN-E#$VfSi4->d0qS0_XQxf?fSYif$ zb*&`sTB-Hv_`Qbc!9HD_kNuihgaceGL!&Cztj6@W8d8V;^ySN*&;FctVpEyw_;mfp z*9ZT;@oIzFppCk?5ie=t3uxkEBbrsQ4F?tRR2`pLd^_*2Va@+7G{UaherafuSP!K< zf>3`zaPY+Yf&^DLT@RN*H*$9reGYCP-NN1b>Go&z2|G%9w|lG=;aYf08J4ZtN-SKg zCxR*-CRRn%h)ovXCU%H@#BPWw=+wka9Ot5!Kov^~QACx{IX-J$q#k~=f-irsjjYaV zv+j&hq^;!)@_2teMA{8?0lPzSm5 zOFXe)GH2<(*9B)B=!8uYu_qtIp35I0o802@A@?91+@Z3<^Qw@{7)OX~Yr$RA@*f#? z5C@*;35#cSElZ1R%Bwg4%O$j8K19Rv9Zq7`uS?HX_9nf9dIYO%CC+0XcPQ+YRZnTT zPCaxRdBdEgF+6mH{E;O1)|C!d3ms-l7Hak(;fy)ni?+;lS5gOFQ{p}10F6eco*)GU zU9M~7xoVgyRFVZeUk)1(IPu-cP#~tz-{Pn;RTfox7Oi@!W0gT`aGED0*h}lsi)15s z=iEAT*rc}vWzoAh$)^Q^Hq-xo`k68%Aj$M?V$rAiBtJFFw=Gi%juMjbzLISO`-GpM zSI@b6tcZR=LV3C#I+Om}{9{bhLN{GD>xw*|lqzIGX%S{~rC*pg{~t^U znu7-Ju4YxV5c?GO5Zg@`;{Xy1SSR(HeUf;FoujMGHc3ih*J+j6B}r}UUvvXJ{;Z_P z3HvTp{M_GH_2i^O{guzH$a;NwTI-dn=U4RIs+)Iwpn7xJyrkCRQ-kX^2PBRrIZ0vz zw30U)Nu1rG>mT_zR5!$nX73+^J0RYmg}n2V-&FRln$}iSwPbeJ^$kyUEP8gy*_&Uz Y_>|_WUOFc+qB*~-v}WnKwbSYU0VI6_Q2+n{ literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/m-lzw-p1-tile-big-separate-le.tif b/nodedb-raster/tests/fixtures/m-lzw-p1-tile-big-separate-le.tif new file mode 100644 index 0000000000000000000000000000000000000000..15a819099362f3c63132384022afe79170647906 GIT binary patch literal 2360 zcma)6eN2_t8ULO0zQ8&6@^LOj$`tDh+{;C@c(usTC^;&kSn80WC9ZB)i_Wi_o6Rx1 zIdi5I5ifX)h$x_MHrL#=6sdELS<{_3*AbhVyQCjxam~BTb*-@(Yq!K~X4^}>bo^nm zo#edFo8NPubDodi^VHVP0Rtf1J+USwhzarlLQYbWDOh$Aqr?DB{)?ow;bN1vDM?-G z6l_vAZ3;Gd$8?hNh3&+-i9O>XSpGw7OQv9xZ_FnY=}8~LNpSg;I40v=@er(!y;^=mp_*irxc{Smmn8ul1dC=0Wzgqx_qp~G?}7F6gypqX#}HK zh%A^PQi65v@sx4W=iV|CX(N>32(q*pCgo^Eu1>YcIYcm1r#UnSQTR2tX)EG*RHfFF z5qukuDJH0eV+bgopfS9Hxyl@^=zCLsNfjxD2L_apv@}L8fJ}XvseU&9TvqA zo5gW7%&y@OSQT0|+lV&BTF@?8H#)$Eaa6PG=v3@O9Fv@)&0+h|ZnHjggxN(zEtWv1 z!|vjk%^6+{b3cw-T%tp>%ZMuW9=as^6fy8jyr_98jw@b+6OwPiN$>;cve`Yv!aN7v z77yXL!|QOu=Gz@rENHVTR&iSQZic z%mDFXMM!*PE+g5f9``kwo5+_?GvXUFpC>43TpY;lTy?HtN8{zK6@y=iH||`&_$M1) zeR^oauJxDy)Y4C&@??$jT`^k-3MP^?(<>2k{1HBfAAa-Mb)&aimd=W;Zf;oQ?|mxR zvu10C$-#dJ&W!Nr!L& zWilN`K5k&4%+N*@cTfUP#2pPk!#t#ij1v47704Jj`tS*sB5MT9&DVLI9yZGH4k~n} zWt_t}mg+1AIW$?NR$79etIT?11fQWwK`@JE;RRKYU@qpPz~Mb~HwJXj_LwwDguS!@g$Wu`MUsA56^+m;<+tcc<&V)i6#)IJ3Pk8e6>#Wg zRW#;KK7Xa_oj*?3B9KSdBhXHtnQQSVPT4$;{xHAhrk7WtNAr#7RlEg#l6T`Y_^?*N z654`bAN|36<=cakns?xo;xD0J@~apG{{Uy+W|TrUnH+M=6fQsL9u>6ccT_M&_p8#6 zG6~V5lo(3XkQ6DlkTi$`O2w6JI;+!8kNIN}PSN>mV2 ziUyJjv4^B-(LuNpFA=X4S4jrM2P9LAe~{F$*h`otVuU+lh|g(JL1ohwTH%g1e{D|p>b34* zavQwsYh}@X8I=4kJ=lMvU`%`J=xM~amzQiAq8BhM=Xy3R$hnS-(xvHvS2kd=oCohM z`mgiv{t(tammZmFNc^FaJwvCf!zbqqgrc-qb*RGMfBH|)0@bOC`q>Uk?x$-s7VR_)tsyK(m3zhY0d~T7445l^_8{eN9x< zB=xG`2LYvO0wjn#79&mbX(*!>osBgeCK`vdXh^=8?%E~Ym0E0vT!6b7+Bq5J^5}~k|YZQ`cV|l~`R@2Zn!KMd^nqX>j-1yf= zajud0$9#h%JYk>y1ZSJE0f#wmlDxEz4#uRASw1(G6`E@H_YQ~!BQ@2)*yOO!9!K+ zEeU^Ugwv;63T4z6AnbuoK2N~BYS??)@ZV!l{5o@^64P~?&k~Dz%v9_iiw`@IGsf*v zGVZ*MM6+r)d$DUdW6v8V`Kn%NYI1;8Nos%s=^?-e+lW%#n8S9 z(xx}Gki<%)ckaW<+dkU*rW~@M9zS(nM9-*)zJ8RGhlo@eLzKdz5iOh9#DLBCnoo7M zp!aqyJ@qCJ>E6hvc3#Fiqs8>SZiHNXnNM^6h}ZrxLVwq;ve}4LSRtG;TZTBWD#RHq z_yaPWpW(w%m44c-h9)Xv;j9R!$p?0{o z(-7|Jdl!e#4AHzrx)_rU_pz%uI}oe)w0{%M-8rt*4|&(8N+4 z*6N7rirhhg^SH- zRmBb*QpD2@d{)_=!oNng{-?x6miO=QSgWG6sJ03$U%QQ1xY$SpRXjqhil`BrEWS_d5C@3e5Yy4E zi8(mIMK6IW`Uz1)mC(69YeS44b*qZ6_^5-dDeSQBidUp<=#r&I97l9`0}m{@OV*wq zvzAAzQaaidDF+?0-W0gOau+vED?L!A?&^K z8S=<&o)mr`(xIIyE4iQwX^e4%%eFS$Lp}eAVFz*GC7!%wcF*$6n3lq-gRopiI~G7R zEI;BDcK_OcuDU<<12iI3Wvg%j`?*75Z>)YsD{$%&+sQj-M~vqYW8{w{*|*+5S}Sy! zk}T2eBf?p;>6UJt=dPv>yrm?0#334oZarBF3AxhH%nQ^=Rj4Kld667BByi%pnXN!f zr@zH9WtuFi^c>pt499AN*5M3KL#UTFpdV>Q=&pGUX0yp^3(2MTaEeb4hHRn#`|>Mg zYEY8t`^2J8^GN|}mT!BG5*jO{;bSGw2=xg+L%*JX?RY8uhD7izJz^I9m)Ym1YZ05T zm~};=Ps$K-ptK2dxH2frU+^sxBDhx;qTme(vE17xB*1%hl7>7U}XRpw5 zen3m{unxMMH>)Wc>!Yjr9=Hy$ESihGy6XWeq4{Xy?pjtuOR!&YkFY&-2@WEqn03>D zsgsnm>^xm#$|NV%6Kj}tz0=cO%Vf!yv{UR_>^YrAygVist%zb-BX8YBe zmsSqkZkT^!sCG-m{M7cc)5Ghx1SO6ZIaOjqw3@dXshr)V8=klvsvF`ZQ~Sr^4vIHv z3Ge>mH`V>Cr+1XrES=MHW8>3Zi(goJ?$-COKBM_+m(5LyZY}EZ*DX80ZU+57I3xqi literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/m-lzw-p1-tile-classic-separate-le.tif b/nodedb-raster/tests/fixtures/m-lzw-p1-tile-classic-separate-le.tif new file mode 100644 index 0000000000000000000000000000000000000000..33613fb64d63dfff1b3e7a4c48465de608720a93 GIT binary patch literal 2200 zcma)6Yiv}<75--KwLP=z$E;%<7fiUecfH0Cvk>DN1l9n?JnB&68iHtO7GhjpL93R! zq#|evjxlC!Hav{MHe97NS~afiP)c2b(h;SpMHIA^QWCWk-KsQA5K&#yN~lFu$JDL( z*Iw!9exo^a=9_cAb5^Y?2Lm7gVuKizXN*kZT!SgGKp%MsSO!n{q!~+}#1rc&lX${! zDoK0fbHm_illZ2=g^&4^J$803p~y7*w~Z~Gyw|Y7vd3_(!4oX+8e4A8PGFz8xN$O$ zSw8wExIAuqwzssjY~QuDBK9&Iv+;@r%(sEpjs4bGbp~+X*b64$COV3ju)zE;Kl=YW zqJo6NBIF}OQi(y#L$)-DD#RL0k!hO5vBS`zF^pn9a$rYD8P*#2v~kjF;!H=z2xT~o z9BoHQC7O`0(;adiF-+GP9?e4>LCsy-h9sU;=?!EA-^5dj3F_b|LW-wo0&ilr@+S=W zUt)%2lFr64lu93HKZfwM^lK{<*HJ06l(i2ZBMe`JCh-<3;2*aN@V=pzHDYz(Q}dFX znDxLEa7br5RuC!7*IAx*0{5^;=eV#9*@L<$%J$G8E~#u`x%e{{tDF=B^`lJ{C77aN z98o0=Y@8OU@EGnYj}_v8%c{{9Wld;zSPX|e7RO;V!{o`+)&kKnk+>v7WM+dNe&XtOF+bf+pE zr>hbCh^|FwCEb9~D18B?WwZsQeOJ-%@Dv6-eh=qd-hxw-AHZqwvpA#qW%Mh46K5NI zHi0J!33Wv^nJW1O^n>5PfaZ5`PO)@Cq$oC86*a`xVk1dcqLny~DwG+DJVZN|Zf+QD zuF6j!q`60Xrau|S6fbRrd~$Pcg^h2L24;zj%wH5yLlh1yJ$&mxz#@2raZ zbcw_6P3!*>T7^E&W7Ju~WSefw*esmndz2lUF1KJ3-` z+tQNcrCU3?8~h{mbG$R$FDILJtXuf~^*?-WaQ)78m;cz>N1zI1oeEsBTM3FL zk~G~fk??{sK8qiE_r=f65Yg4~{f)UW6WmrBBj9TtJ1)gjI+em@l)m6*n`d48E8d4nM{m zWJatq{0ddb8n=4!85SdF1kB`XSZ7A9N_>DSo$XlXF^%VMy8T7fQEqIsrK=m}}2!9F9)Lnw~0 zB%OJNKnn^FO!3oVbqDS#o^h$mVXTXQF3YXDVgnQ{Lb5&qBPHEnW(~7@=KFP0Q z0Q@%2zRxIy++cIaFo0pJWrFMLDsQs3U1oY#|vC`^i%-e*hH~ zd4xJ5LM%_zlQdUsBNnx3x9C5IfBjuJdyPD@)u8x&HG7^;RfkT^ z>W{=}sp?e4zq|hr-#pc&N_yGf+JCNu3T`kOsJ|1r8_7-DZ;^^;p8JTYJheO#Q1;i! zG_|53Fm4|t#cGwvjoRnPOtm_do3h^~g>r=oJha2zb7j4L gqTc?^|JS?`RiAi5+8_S6d1Gn-;l2E`|Ixhv0<}6TOaK4? literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/m-lzw-p2-strip-big-contig-be.tif b/nodedb-raster/tests/fixtures/m-lzw-p2-strip-big-contig-be.tif new file mode 100644 index 0000000000000000000000000000000000000000..4b2d57503230721eb94da646363fa3e9b752713c GIT binary patch literal 655 zcmebEWzc5eV1NMuC_{jefq@0e1~M5$Kx{@v6fq&N7!w0Chz0^C1_2-h0!C)I7$XBK zNCYUxh9m|u5hNxEQ4eH7?P5$|6h%_U1XjktCf0Oc!Y#>nRiGh$nV z@(zd*_Z5V$fQ)cIpxD6qEZ%&^xx@_u4GeA%L=Iq@p}`c!u=df$9dpVo<@;Ux7Km+W z@>QICD(R#H_q9;{DLyF-qBA%&4d(bP54ge6z!2r2I0Ks@9~ng!K@D+gaAXiX=BPg5 Jlxn~V4gjvVYVrU8 literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/m-lzw-p2-strip-big-separate-le.tif b/nodedb-raster/tests/fixtures/m-lzw-p2-strip-big-separate-le.tif new file mode 100644 index 0000000000000000000000000000000000000000..a08af1f72e3a0b6e5cebb73e4eaba45903b98325 GIT binary patch literal 781 zcmebD)MnsdU|R|2*LK1_hMvw=f`d&e4QK%+oD4hzW`=Rt+DE$UXYcXgu z=rJfUXaPxm21SOF%(S$$%$!ucf=aL@4Gazp6*6L+?9EPz0y{!P9C_PZk_0Uj{%MML zx;e1EI8tWHVRw8<;0lfgbp~XWqU8{k%2{BQ{4+o*c^HyEzIemZzE|cW1CCjMvVmwNX!?|1E6AhxB+S8?*Gq>~QZ*FyED_@pq1&fw5A onB%iN;08wnLzIK!3~YvcWE5EhHN>gGkwNg7qxytXssSrF0MA`+X8-^I literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/m-lzw-p2-strip-classic-separate-le.tif b/nodedb-raster/tests/fixtures/m-lzw-p2-strip-classic-separate-le.tif new file mode 100644 index 0000000000000000000000000000000000000000..0cf015a9751afc3e241d105d4f819a208c95c894 GIT binary patch literal 621 zcmebD)MDUZU|Egmlz_AzgFb^I*bP8wI9-yN zmX?;8ld4xx2{o~S!GWPdMvRla*(p(AM~H|cZ<|Y!poPLeP4P}Q2i6xy%1k-zjxPyZ z!O@`3fUHuq9HLS=3#^iV21q3jL-NNLZ&(_54=5C{2=TJEI6e%Rk)p>D)_6E6;KiJf z*MePcNgJ0eNivu3b-Y&ilBda@k)gq)0af)!sA>f!km?mGEDaJ3C{{B=tX66QTh09g zWOcqf*pc?A-mze8b4lE&v4Ejmpz~PL#-J-n=28=!_zhq3Ow$J1tj2(9^LB{Mp=-hJ hT)qbE&O;5FJWakV4Gj%E2OLnm6$EwYW3ciS8~}Z{fu#Td literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/m-lzw-p2-tile-big-contig-be.tif b/nodedb-raster/tests/fixtures/m-lzw-p2-tile-big-contig-be.tif new file mode 100644 index 0000000000000000000000000000000000000000..360ade30d3d52d097b2bc520cfc8a6b466f044d1 GIT binary patch literal 942 zcmebEWzc5eV1NMuC_|8ufq@0e1~M5$Kx{@v6fq&N7!w0Chz0^C1_2-h0!C)I7$XBK zNCYUxh9m|u5hNyxB*p|4lY;4B0I33FMrkZ!GDv!1=4oINGX%N|0(hV{lrY*Nse{?; zgyJ`dKb*0Mxj_5~Gz%I&j17#gaPwFg`WWsogfIj!2EoOku7kP)NR?!!rKM%&r0Nw^ zLIoKYKp0GV5Sp0_O2;)YFeoq!Fz~arI6e%Rk)kKd+u{5u;2e@uJQgU@sCW$!x&~c=&bhs$?{L&_Dcs52iD7_hWi`B zS9!4dF|ovY^k0_~s$)!MX!#zf{k5}iK10j`CDSWBe}kT>IVdP_oX)JNiA4PY`fmDeO3wRJh|xnH<9ej})Na zrunK*@k$NanX)Zbe}+$5u;r3#x#n~H(t~$C`BrPcARr^eN>!}aeSw2R14EC38Z`F& fuLOSyf`^wFqXL7Ugio1Rc3sn=Pehria*qiB*Ky$V literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/m-lzw-p2-tile-big-separate-le.tif b/nodedb-raster/tests/fixtures/m-lzw-p2-tile-big-separate-le.tif new file mode 100644 index 0000000000000000000000000000000000000000..1b80ef0d4a4ede563f346674aa64d8d7f568d3da GIT binary patch literal 1140 zcmebD)MnsdU|}b9qN$vQ2Hg5 zR$zwctA^6dED*jeln!U;V6b8^XNZQ<4Gazp6*6L+?9EPz0vi~bMEToX5|w|ngsJlA zyDknaVEkq--=?l2WTEuVQ#sf3@r3p>tL!8gPCY$b*|W>j8MvY_f}=qOWUR=S{|ruv0XtGm7$!6wRA}AdVx`L)ek5V>i8XOH4C!tQ8gIO* z(|J3QLH_(plL`sOPRHm^Cvzgg4SEgKCP!py6&ePddp=>a&p)oFg#iyWx=aL)doI;{ zxqwO4sqNUx&aBJFb$xiFXEibeODNC%?bXKdrf`|-yo+3lQO~qym+oO?WN5H)P$*y# z;$>}dd>Akz!GV>#!&%9ofF(>_JWLZN_JW{RtOQKd@FR-Gwl5|+cR4tCHo|LW;$|zj@+UdZpk2^KKTN(R)gZg^RXrjqG~xDjBXG5%-;%0sjoOGF*Rz7 zPH$xI%Iv8(Jf|IGb&hz`;cX*uK5N~Iw+onD53L2dL!K7r0B`=b~rx@ILN@rEZF1nIB>=jn>vO7_mse$Dm@Hw4Ej$lMW(Ei zsAVu;dgHRU4+lDBTb& zn)O73DU4z5>#Y|!uP|NPc;OYN1NXJdVGL^yIC|HvQewysc$71uaM4)^5sA0PkRt~$dfN#_B_zEG}a z`@`#>J~>sZQr6&kxx)Kb&)oT!k`@Rgu-tZOwV(a|@mH<2f(%^;G)}ch{y9)D{_1N1 zrv}sNb&dB=iGO4|9>y@sL1(r1PnLfIw_iGlIIvzWHQe72zRH8ukBKGLqyM^`P#t43 zL(BI-?XR7E^BH0mD4Aa2`5W|1%|Std<8)@-&igBJMUJp2Gb)<$w7;AmZNMnd;AQKy z^q0r&lL-n93~>rdP`@EUdV+uhOJT>cq{1Cn%H$X(c%%URHqBRkidSmT&XjGj`ZIje zf-RR^%Qc_lmma+H$+uej1pyf$R;psX?h70o8W?&M)S$8FePql!h7!q#76;7%F7MIN6(>5(PFeG>P)Jxg;w8XbDs0(RW=OSitzrT)s_R zMaV+wou_iH=i>?OXI9xsFr0dNxUy%LMcAw}%L9VfJj>?Q>XKGB7^+-mnIbw zjGd0rpHAjPgd6l4s7;Q@)G9O#IQM+QW}kmtO$!4aYIKPw`3(+L^K~R)2lUdsLA8j83?pcq5&y9nb^SLsl@>b&-kOj@MNw6hNpdskcC14v&`s`zd`tJ^Kmn#twhG;omlSS!GcSk?XxgJ^zbcaX-C}f~vns|(zr}apZzyV^y)BvoLLlYjR1qU8% JGzcLOrT{td5%2&2 literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/m-lzw-p3-strip-big-contig-be.tif b/nodedb-raster/tests/fixtures/m-lzw-p3-strip-big-contig-be.tif new file mode 100644 index 0000000000000000000000000000000000000000..efe727b6a61fff079baef2c98ac0c41ec5535bd4 GIT binary patch literal 1206 zcmebEWzc5eV1NMuC_|8ufq@0e1~M5$Kx{@v6fq&N7!w0Chz0^C1_dAl0!C)I7$XBK zNCYUxh9m|u5hNza0J9Hj7h?&dD3UrRs5&ted%-3$io?~h0QDSY*vQb$CYzSn3_=ovYG4e8i-AH27DiB65XHCv z!eQD9p_!{7G>djgW?EWWW=^VJK_x?j!3+fs6)}$g^#>aSCjVeyaw0kDBAL1dD{C&U5L(JD+^3wM;ZnG+5!q zz$nxlq$Facz|?av`uHo)_6bd)ij@+ola8Al+iQ@=I+NkGl!MF%b`8d3j7$FA&EFCI zAc66(oT%5bg9|6Tn8Cu-7;iI2sC=SQ#5*ycXSocmy$O64%rh9CusO&$u=6n3A3 z*_ZOQ$#I!0hqQjaxvvRha`>HV=m^ys4&h6nHCx8Jad`&<`qp~KRk ze?kM8J{j3t7!odYq?pO_HS_oLFcde)WU%uoD?DV7v6P#@#_-^!%rsB+$&3dVl>e%d z*Pr2&7Hqj>PlIEtZ(6~Qs>mDz*5faN7^W->bf47J@MTx34$p%ozf}P@RNa<`GYEKu zRzCIcxyc*2CZsEO3Ku(LRj||ohDOUZzUji5chiK*f{rT}?r0V3cUc;wbMb@Wy7}rWu4SRm?4q4YT@J)0q)A&tR-p@$)pA(A1Cp(HacEiE%ARj;6up~0X*z<~K5Cwq&d z_xl;iPhWlNa7p4jt`d6UjD%Bi;EpGSt!#Y<6Z*d}v#F{VyUy-ub8>vJz`!Fd*g~1j zQlWnSfdB_CHkEhXmQp;ejRzYVECkq@+FLHom7VSJB!hv6Z#BE(E~RsvNeK=P5fbdo z4Ns?7h}j0e4VcFBpvivq)yF>D7AqDs2v`WPG#=P=MuvlVgTuq8>v@?Q8XEL3FbJ@S zak92JB^-Xn+;G7HC_B;MfR(OzH*@@l6;H}+J7h>7w8UzMr1)e;r=CO{QN%QN@CcfqI zO$Qtr1c0tk5#wdeYJ3>5BgKS`rG3tVj}}r)It^WJ2@^D~1is{LYfKWdsNgv+)qN`I zX$6^!)8|{vn1nz%TX$q6kNK_tqjckBUWp#d~wB=(4~57 zeUWX^Za6SuMT(v*ZyRTTpv96fU9sk42_H1BlrW^VpGXoCSrg|g-tWG6(hSR6?)noM zmwdKZvdNec7+(&c_+tBS->{%TLWL*qNe6PsH#{k^;Inj239?j~=5wIQ{(`^(mQ7yU zQ;dX}n3xV6u#jeENT5fX1h& zTQ?l#ctV~ZxL`Z&)SWd^87h*dc~WMa5z7~>HobOg@7B1K0$_Ln-NL{IOgj$e8D69q z@bd%Hj^+!WFkl2tNVwRs>V~+`1P_K298W?SRVE$s2%6!Y=B&unaA2}>%LYdE$!8Y| z?YzSKQNR7nieSr`ZH5+0{M0vCKKY_A#B@=k)IiZq&~r`D$^;G$c+^NU9%!(LF_YwP zcY5H!!UD9Y$92KS9X5WJ@+wDwQPXwGSiR@?QwNSO>v$q3`J^~mx?D5W>pQ3LtW)Hh z$K=_*>A|yP1U?!qan3N&(&1x*8qCAkqzFv8AcNZ)7C9(iVgefM79qHeMaovb@xT%R z3zte=fhkUp5+tL~8S68+tGRH>TytfebT(weiwHhdo%yE@xb18?)hpxR3k(+k_cT`I literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/m-lzw-p3-strip-classic-contig-be.tif b/nodedb-raster/tests/fixtures/m-lzw-p3-strip-classic-contig-be.tif new file mode 100644 index 0000000000000000000000000000000000000000..f80a2f06f7785e12c41351d1223e4bea97ce45e6 GIT binary patch literal 1062 zcmebEWzb?^VBla7WMp7q0kVJ;g9sxdoGrx2#J~&`V`5NXP+(*Rvl$s!feP86Y@iNC zL9lv|{#%TqP;n-pxES0VRz`8KI7rPaMk$a9V7opsO2gT|7-f*m)Ieez0_|sD-~no3 zVzh;d16{=!1Z9H^3kI`6t_8UlBq9KGwki<6VqjpN4RqI0hK&r}APHn#l9`s4mYI{P zS5OHQZZMdkz@Z|>@xT6HgTUk;46IyD3`qh7j5T>bi`%2=gvNv@kztP zJstdthc^T~;qy^*JeFXQP}F%YJ$UDnZ?%?*28sqNycigTnuC-?tQ44f4n`k;<=H-= zDO9miLUqz{lVf`g@>pjwyq0p1`M|Ehc#Lt$zq|Q6q8}tM{*@E;T6S>Zgcmbdm>T14 z<_MKfREl^f=JPC2fr6y$SCmI_$RQCNcX`zBV~7bLBAiH4*z;nA$t5 zQCsS<=1z7C>nFWGHi#a5)WY!Kef;(tHgTWp;w^Mo8uU+S084e9zHjD1J{Ifm1A`R56Y4eCB-ySX3I6E27wL-V9fHew>TvRxUIOs&(FkoAfcki zPg|tZP3hu{6+9d)jK`O-Ysg44Fibv`Dzr0XTSNoU6GA&;uH|wz`aTa{zEI87zWYLw zL&Tr9Zl%Q@%EwHnY?`Y#?aZoB%PH^Hn$EfUIv`)=3lb*&PU-*mXFTX9KZi5h;@N5@AN~y zhQ&KLkQ|(_K*BMWhnb0yp^#%rkh);^(PY7l1srTlOzsU%!IzkP)u(u+2JN(FWYn9X zpa=~0D~wFDeG?~7Hi|N}@41jXiBmv;S$@JL2F>s}o91dUc)tpkTENg~xyCnLIP-3r nP+8D%<-#4UV*M^lgLE!_FkClZeZ^I+a_cF_QseeGE>Hjf)Td2C literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/m-lzw-p3-strip-classic-separate-le.tif b/nodedb-raster/tests/fixtures/m-lzw-p3-strip-classic-separate-le.tif new file mode 100644 index 0000000000000000000000000000000000000000..ab7fe3a6a19ee07f00d147f134cdbaa6c9d16f5b GIT binary patch literal 1156 zcmebD)MDUZU|PrRbAVD;fmn})fnhd7K0_LV149o( zCPO4c7}z~P<#4(rGc7GGGbdHApb~5zglI5m5HMi=$I0H}=>2|1^3zwJI$VZvs9>`e;~kti%sQSx1|(M zYvaL&1`7dpruLRgb7g0HJjr0-;aknFxJ&6AXHtTLLxco7bHmdq7Gk!+Zv&?BJZQ3C zef6==w#AAC4FVPdER6>?osr>S-r(@?>3Uw~hK2_H3k(7*Vw|ikP6>yfF*jVW0Lo4@ zIAEnK-pw5UVa1a&Tltm=3ZEEQdKjijxI9YKIFca3K7;X&kmr(X0*?JB7djM3OQorhbFO=z(1lpGxdwrOS%D{ys(GwqXVUz-vx#qceA5Ak1_7WeRK$2$vl<@; z>_{l(C3LCYT3=*av>OggSdpS9%iG2o zAZW29OjoS=Si%R5D{J;gtJQaKNKRn(;t`MU0swf4kEI2No8fMLn(yKJKvb zvy@jk0*sojQ^x8&$DcZId|Ag6Imsu*$IzJ8dXyj;ea={) l!ClRTQ|6j0>!hb--g007j4R^R{t literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/m-lzw-p3-tile-big-contig-be.tif b/nodedb-raster/tests/fixtures/m-lzw-p3-tile-big-contig-be.tif new file mode 100644 index 0000000000000000000000000000000000000000..38a3e3011895ad856870ca89c85b5dd5bd547e63 GIT binary patch literal 1627 zcmZ{j2~bm46o&83&Jqk^6@tD9Mp_*Sv|s{4Ujj&26A7D&@~EsDBp@y*Y6vQ*h%5pQ zqF~Eb6`K)iqktxgS`Zkhpw$YKY8>WPk#<}iInJ+qp_a0X^K za|7s4z2DIOBaAWU?o2=Dk1$uAuIVr}{GkAP&pBsM5BVRw?Y$4s_an>?%y?w*2Ofw1 zv;7D-XP5vgKsF4YWd?P+$B1W{-nALu-pk5LO-f2il_V~gW*g)})pUU$o6f`#Lw-D+ zr@u1x354QgO?kIZEu)pa0Yqatzz+P<_O#2okh`FnT*lNYvKb3P$fY>`D`aK9ncBCQ z)3nKtCFa)MOY?|lGucf+0MheBi!2lnraRQJ7VwSy_Gq#4plWs3wg}E66F1nImoU;1 zrwGwt@ZYyq{L^dW6@SvFw3^cPL-a=J#pb9K!qQ-np<<}W2voss`PBTAY=QOWP*dpB zDjXxYx76}>m~rq{XMe;P2$=@&-vFG922wtF5pc_K{kL+q;Bx^7GRdG( zRHZHB4OiXil$v{86?LwfJVXGatNLy)0f`|1=CoaB(g>kUV{8bxQE4ACb7s}a) z6w^(M`hbAhM5LpPak4YK1?$eHt*8zvpmPuc?pjx(6v}fm>0fQ2FSE;-6m0FsT&>R9M)dJC&Sn#A|(X04uUh{xaFf zW1;ZtyRL<;9oLd~rEBCn8-8Wg)HLBgDR?$qvnBe`F&~q*s*~0M7Cp~^9AW*{QS{_( zG3rt(!A=WXzOn^!#^3LfS%Cv2;OnAV5=0E*JgJmBi9#dNMyMd*?e8O_uv_B=rM_auo5Ec;AM zPV$PnV|pfC&5Le78R)hpt+eC@IlcNO(PLd$BeKNZvp%Xh`h3*p$SY5`>t-=V66LkH zqBps9ZCX@Y;-%qqW$cz{uljqx?`YqU9^LkIa%!Inlg3c=sxXR^RZfTedb~&(ETa)1 zP@yO#a@4fYmu7wmhx1ecnxmUeRWR;=T02lF<1e;61W#ouyKvqsifUP9m$j37zNi(Y eM0hT?J{nLLo*Ho_(c_e`DPmLP|EXG|LH+~Qy6pD= literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/m-lzw-p3-tile-big-separate-le.tif b/nodedb-raster/tests/fixtures/m-lzw-p3-tile-big-separate-le.tif new file mode 100644 index 0000000000000000000000000000000000000000..918fbd8cfe6bb2d1581b4773ba355a67d31a1364 GIT binary patch literal 1798 zcma)4X;4#F6u$4hkbsHg0RfVbk;o>p1`8O$z!O4Q3bMsO3fjmLK@lNEz-@SmsO*hM zh1x2M(NWPUww9$*jkvMuRM8GDBO+8#sumFhMVm;{;RiGAnYnY$d}q7on=d%n5$FQ| zQ1#Q8@vH*?A^=Dm)3hF%gX!vE2m!D*&j##I5u(O)y?WXlaSo=-na;s!#Oz0K|UCea>r6aN@6P;?2eIp1Mr`)iw8ygO`UZjtS0{NO`vjmE7Wrp|#Z&hBi7_me zPD1b}<-1~J4arxGW@ykl`LcT@d)ffjj~SC-)zCRE>Lqk|K5b)s$;`5WMJ%e4KcJJu z8P=OB-pX=H@Tkk=j0{_OJ0c73b{gMZm~XIK#KJ9kN79FqMY%p!h8gYh*VCzn(-muHraPK~H;LKyBNy5@xsXKj2h8V=vmHTw06z4VKL zzqM6wwe@FnEq>`62b~cyt=+IC(nvm~Ec#5fk+YkR~f8YY%EoGS*zRXnE|A_k?LGhNC#jwK1beVDOmJEbuftifIl}GM$OC zfPtqeS=kJO1pzjZvAqYwmQ;i!N3|q1u63;nl|-*l-rc_ss z@uM$ZgQMVK2UY6MAS1gWkij<8gRUu!d6tTxB#OTE3`7AT)?L~Di#cqz69ti}q-iWN zQd+)raKgv8B1Gvp2?GV!EbOujZ1p_kQY}d0FQ66C8_}@Kv}gTXCTXYObmRbK?3AZL zOscAe*Lbh*?1Qw1mXU$;>8){5bX z(1rKB?j`2%5<3L)Nrg*d*mtn`lc~QXd8-mFpz(J+j8co-eX+IAg0Up^V%OECxcaia zb)tK(K0_8+Qol-Ym$o=8N`G3MxKg@9gjdF#DL7j@9p`SoLzJmu6+=DVYgjE;AC}o^ zMAaycPBXRxk;qa1pw?_2T7<{^o`Fn1oUK5*q z_w}eustP@GkJLnovm9)&J~xDIJ-pys2Wn_>ErxRzx48;Vt*JFFI3#RFIM>rm<50N6 zI7+D6!<>=dCpKLX9^6g~`6H+4i-b{cMd$PNs&xqqy}4y~vd<6HzHn58Q_~s;YpIT0 z6U%}|MgQ&LW^(9saf3%F1;FsE+Gqfh+v4J&cuxie;$yB#g3#F<19YRG2uz-yY*nq% z8z&zoQpm?J)Tq;qQQ9qWt9n15+5(kPse)gVJr9SU5NurIbxe3BMyb)X3iTM(P$dT# nn|wnpK#mn(f2YuyDqCgsij&VFJTJxdjqG69|Nm&(8^`|vmk}Qp literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/m-lzw-p3-tile-classic-contig-be.tif b/nodedb-raster/tests/fixtures/m-lzw-p3-tile-classic-contig-be.tif new file mode 100644 index 0000000000000000000000000000000000000000..036af2365327f7da21c90ea7f1d7c75c72323131 GIT binary patch literal 1467 zcmZ{jc~DbV6o>E4`dES?tU}Ni!APwmffh_a@FjqRHIcBXD38jjK?35A8iEQcB1-`W zQLtsJg3SoEQ9u(#EeH%$&}xNAYb;t@1>2&X7VQheF#OXqbLM_?&bjye&dfdG;b1uc z00Arr0EHmp0M-!BF$vor7f0v+f06=ct= zcb&)X#M=Ov6FvofXX8XiD4Ns2g#Pn5V1@~;kOTu~>W$2Tk+XM=&1ExeRBqI6G~--z zo|B%El9Db@Ua8n?45I*$#l-k608m9FSC|AUfiXdtl6M+KTx^jKhjdD2`8z;0RRR3S zPaV&CY>NboTIuB+gL*G}Q5e09Bzl9cF0jz~m++gn25@D9`Ue?ai98OkIfPJ`FI{4# zhA`8qp1Vk75^%tPSA^8)dUi(fpO|^T?);>&t^{?M9*6(Fw-KG&kf{EXHEqzBbsl3i zDK59hq)|Q&1zB3QmX1O#+*!aVIKva$ZVNYue(jRy6wkIg;cg2)+2+FcI2)xi;lo>i zpVde!0I!1XIB)t|#S?!j=7apCJ3>rgFIyoWHi@aSXTKBZx?R%qZfavL*3d^NV0`Vs zt=6LnCE1pYG4$XbZB{?Km7jlpe1rz{uM@s~^qb?#YahHPLH_99-DUR@m2T(8RgxUa zTP-HFIB1nwZ(WTnv9cKB2;DC;FhvV81t$7D8KCj`G_@Ova;Uy(qS5DyCYW}N+e$u- ztSrq*w6nXle{*e+d2UW^fTd&h%L;AB50pUlcV-K3PZpIg3|^P#=hisX*WlD!Y7lXR zL-2y5f26#N8^((7#!Qi@rcZj?!dj`JmL|_#daR0WxPv+rfyy226C?{RWn|4=3Y97x zERTP*LWiGj;|<6<{|J&q<-YYP;+%v|gYErhP61YOY$3x^$p{|)ydqpYLMrtSlSOB2 z5(c1DUuxmg+nkNV-vnj_+hY|82_wS>yY)KJ)djMLJ4beiX-PJDsrNmPlaxF$ilFd4 zK;86eO_G=GY8L^yl#BE6?1Hsh)@cm8d>5;Dr~)Vcy6%XW(@bSy>Y{XV|7V(byi)8+0oU6iHd21X*$(tWO_$<)plOqNrz|f;>$l2*rU<^ol2@{J96kDT znyB8DjnLuv>1Cz|BSV|+$=d8s^y$kg22}_O2VL`!e;aJ&nI|Yipay z9~HhBt=%5`MDNvz65hCljG+kZC^NmQ;$Pw9qN624kGUz zJMVx38U#w~2v%*#UL_Vdd188ja~hGlIMGU$hefS2zPhmc_&XVShVgkyU=0ALu? z!V9juUcwT%RE94rRWi*@;#&ZAxGzOoXgy$Nc1Bp-8`n4GZdrWy=}@mdZMC%^#O2Lb z$zB^Hn$TsQ-VHIWu`My%qOU#MMeJgnJjQ2fWq)e>hK!hwFhzM3MC!U z8HTEqqNUcy;Mr_V56O2`aUHk(s$uHDnmTD(l=o8GlR@>7=~35`z0OLSqqau>pQ`nG F^gkF4?Hd39 literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/m-lzw-p3-tile-classic-separate-le.tif b/nodedb-raster/tests/fixtures/m-lzw-p3-tile-classic-separate-le.tif new file mode 100644 index 0000000000000000000000000000000000000000..36b00fa2e379944b65987012ba4c70feaecb7bd8 GIT binary patch literal 1622 zcma))c~DbF9LM*)7ZNa$JRm>_8HrpXa)cHzf+0@`LnCVs8l0foH{Dn;xR=81*K{cK~S`tmOtoBXWHHQ&HL=`clZ5%zxjrQ zI0HiffGGe71t5@EFpyb>y8uNnfWj{iP7JbMV*rCX%lftXEbDa~#NaQ@W60LC{2VfO zj>c)uT@M5W40t{PLpZySLH3``0f!tghsBWf8KdyWfl#k!TgcI~ci|GmK-TAi!Kokm z9Op)MnnIySOHXmj+yV%Apnzg`C{@F_0pnK+00&`_7!?3Vqyxl&2tY$F0|lgi4w6se zeC)juO>J4tZ|b@|#4HSWP#!3@b+s(@XGf-X8#9Q)_JnI8c3GO>^3J4_?Cz}h`3SGh zZNz?0O?w=dZD=H!aCcD$@s0t8adAL)cmiXWznmcA*fflMRPkl3sv+f)$qWlwqyFe_ z>CQGl^b@8uR5x@+fcr?DpH17FUa+uf;8KgL)c4smd8XZleQ#8`rDXI)dS<4Bx*bzR zbi0i2C@L`8A>)!Z!b2HDDY862TjR`j^{eUDvA2PXyGH7s_GjA3nyxQ;{l`u7)%DH& z5lgemMyEz}*AasBlU?y4hqKqelMP2~>KgrG*)I0^pr6~SH#-FI1lB+Hjf2iezYk4E z9@g)f5&0$2A55OiJK~Tpn!IUs?nT>nk|g;ApiQAPS^;sjJjjl})~HaN7$w+?8j`9& z95#=rS-e za)ONy@d$aO<<^6bo%Wt1yOmrE;}=;Pr|(JsLzqDUNVY9;2{E&(y!x;%om+LwRn@Ji z+sF;B6dgOOBS@UY1@;yUXqbomW{J1OVM1Ssmg7oQ1r9t(&Canhnipsv71z5ze9^wp zl<1b^#?|gsVTzb#+B+MUez$U6Ov~WJm!g`;REdv+MVScp20rOEZakl+Y6c{qCpBRb z;z<0BR=OBf2i9ydlr!dYRF~?MM)v>xJt2SXss5!3jUnzEJIwNfc-V7U36F zV6*oDw`xhUcpj^m-H3-@WIgQ{aA?~lC!+=!V<)_gV$*ar!p6IOr|+jXw2Ta#&1j9U zjJ39+)-R7oUkH4a8B>W1k2IDP3gGt6plZFsVbLp47M{g@1!u zKAQSnk-s9z8X12pBp9{Wo#&hTtgRNMo$tEb6klJyt4?x5={fT_td@clxeYz{0XRO_+Mev00{$APPGi5Zsz0#EZ$; z8@ZHJ)3;xBCJj1ymOC=B?)Zx1+Lbb_QoQpP?lZALw7W-VR#oJkcc>;>p6zT;^m$-B zyWx3XJ2S&dY6+6Ru+3d^VpXkq;Q?tg#=n|w9*?6PrqNQ}PR@+_9<}L`bpKXX=r6fV zpC*p_YC4~-)vZaK?<*+3opW}e_W2S`1T(#Hu$H+*U}jU;sOi5o+)NLfE@|)zV*nKW z+ifg>s2xaoaDulL0}&JMT8h-wk^pRzA1EB5foesq$!ixs4pt;42;8L8!>X)X;ZgPO z-`p0YjZTyNl;V9b;+SOpDxV|LQ?XjTr**j3sJ>NdK=A0-paJ=|V#DoHSEg!(?Mr?E UpYp7XG&FHU(SZNsX%t`@DPad+F(_ru;gY3g=dmkP>t2QB~2bI{Yt| zoE@_gvdTzS;mDClI2A)#qqAapPZKMW(?6PXpG7N*{imO^hl=7S+q3VMC;NUSPW4M< zSXS14N-Mi9>v1O!%A6*aa%$|qzAC4QrNuktq=GpGJ!$u!ez}eRFBp__osc`1iJ3Xq zd296Pk&%(nqi6TjUj1`IS!Z-}%zqChsh5hYugdCSRn+6LD$lB>UQ|83uErXpEA@`9 z)_b}}vvs`|>K1*ZJM^jU)feigul0z&)f4(ra2OXK}(dOk9C(mQy+b)2lb5})pvSYKWUf_=vDopxAeE(j#X=XqLKY zL5P=ycvXnkg?Lklw}<$L5dRY5-$MMS#$dg}8G4VxnoS8UaG>)sWs}Y z4H}>=A>J9{Jt5v7;v*qGq4DI=WD05qXKP-p>=Mq^$5hd0T%<3#MBi|kzT*lF)HV&# zE)CUQjnE;D)G=koPMt`8O(jXQV)rbdtd>zxtEi@R)YK;GX*-Se16S%7uGVi{qd&PG zbBjjlxW6 zYCKC#UZftc)0i>ZOnvR3iFVUM`)H%XbkcFUkw|w6#QsdCH>DUrc?MINr%2@mYV!&W zc$21#rzMkV%M3a*FT_i9l+( z9hep31tDG*;#DDD7vfDJ-cCvSQ;tDY;z_FWJhgb4`i!Cp<7mMo+Ay6?%nkA45U&XF znhi9j4HpnIWDZ;-w*88RE4e-WcL-A>I|@ zy&*mn;$vhQCOSV;oy05`XMxMokvVi_5x26OJ6X+rtPk<#5bp@_?hx+_@!=33XPgt6 zD;{DikMTXv@H4|X7~-RhwwU02OmQJ*x)}4F z!ctdYrK_^mX>4>|wz(0z+?>5m=a4&a%w2ei-+6<-80!Sy^_fg_5$3ogi(HQ7uEc6r zXT58&+4b4sChT?#_PGs*-HGGw=0taQ0rzyWd%Khe@V<-kfy?lr&*Kwcz~`>PSFXcW zH{^Ra<7c+}I7k(Vyu znIJi^4iB6X6z@IgeV`$a@X``~FFT>VdY~{vqPhp?MkLBj5y;9IUzc$2gfTYIn}rnr z%T2JeKEliPh>QY>ya#tKf|dGSf)p1p>m;fRY(10}ftp@{i_8ld4C zt4W%nx%x;S>r;KMFV$Spid9$j(O`|zcumzTEznY}(t2&tSNcZZ=?DF!{u-t+nyBfT zqeWV-HQK0c+O7RMqLcbXzw0mkBNM0jTB4O&r_I`-y*i}hI-?7^tn0d?6r|=!vhWle zv{k#bPlt6vXLV6mbxROR7!hPA4}~a7Davz9r*&SJbWOJvOe)fmnOx*2lH!!1BGsr( zBU*4Xp6ryQAp_aSO+kuKl5$k02K8t{E85YCo(v!pImt(1icy*hL{W=|G$)!^y3&Wi zjAA@fnMDc8QiyydNYWTjAIIM%x4KJ zS;uB}u$My|r#t-^%4jAqjoB<@8LQdAR(7$E!<^tO7rDwUCNq2b(g!%oDbDc+fAcRv ze%xuD$vK_Rgj>&9;Bw(jU24tGZ9 za9%&_7yOc6aaF(O25#myZtpJc?Li*tah~F&sry<7bX2EwE*a{6)fL^)U4@V|b&nBF zMskpsq^T>wbG*pQyh_s4RiP&JX-aF7rY?pKbfYgrlA&%mW0}MZ<|aek0+zCh^=wIo zx^3)cKSwy340UI?z-6v;CmHH+sKXrL>`t1xT+Z)E7k3#aOiT<_$9STrCqrGF=X;4)dY$X|b-&>^{kFTipND$1CwQ7?d!d(kwKsUHcX^)= jyR&L6@r{N87o)htO6=P zuJbY3y}0OtxZwHz+3#mj`2W14Pm!E)61n_*KD*B*u25pM*>i2Dcuu^B5*LhF;et3l z=9ltCJwTiP^KtWE$sWcdgP$TL&S7NsrK6F|evni9^vKN2?9sDZTCaYQ&MD~b`1&47 zQZE(J<0`2^Dy!#INiV62UQ;c-t@@g%TlBtKXtr+EN9wFkbdNsM{rXamXp^4McX~=c zYN&oyik?<^J+CyqqUw4>bu>;5HAT%dL#;JW9kp0@XoWKMxgOM4>Z5P;q`ucP`bjV7 zfJW+fy{W(TuKv+`vBvc@L5(#{8JeYbTA)r^8se29UK`?#A>J0^T_OG@#D_!tXNXU0 zV(i=dT&&p?&_}WMpKzr<<7#!)a&^;c_0)Rx))w{Gju7t&@%|7W4e^N(pVJf)HG_Pb z$7Na^`?Z3r^f?vu71!z;uGjb6sGqo51GQ5_v{%D*NTYO2V{}Rr;`~e_k7kji1#t$J zQc^1^tF=_pMyhBVwX}=+`h{C`m=^kzTXmYwn0qu=Skxgs`CbQ7)L{<(2NJ? zlb?qv%wv>bAY~XrD#J-<6g3$`JtokYX=E^qb}R_-(h#o<@!Ak?4Dq%Q@1iLED8*pP zF^tNLpc=1Jo3S)tGEJFIE9TOHMIl}u;?*HuAL1<`-Vx$GA>JS2qai*K;&asCE$T9! zMogtSGil5G5HAUFR*2VxcteP{hInU)_lEdTh>wN%6cY^7oQGLXVu6dW)Fo-pLb|Yw zds)Q;tm9EOhj@F4cZYaih>wK$c!yrhrWEv z0DjkraCt>ouBzG%o3L%%Vk*OR5m!Bt**&V*JH06bI2JSb30DC z6QlWycR0f&=i&oj!W zJj|6n!qxn`YkRC4c(R*%x?6d!J9rVlyA6N)HvaLQzR36aQa|KE?(5G z^Q*4mw_Mlb-N;kj+%w(Q^FzEO#91L;6XFda-s%E=#FzUCU+JfOwTJo|zwD|W?Hl}# nZ}KEJ@ds|{Id1QTAzl{ZRUuv%;>{u69^&00-WTE{AwK>uE+crw literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/m-none-p1-strip-classic-separate-le.tif b/nodedb-raster/tests/fixtures/m-none-p1-strip-classic-separate-le.tif new file mode 100644 index 0000000000000000000000000000000000000000..01403904792c075ab7091f8bbe6457e0463d0022 GIT binary patch literal 1712 zcmaizd6bA%7{;I9GlMB&h_NqajZ6_CLWIUacCu!f$z){g`@V+Anl)?ILx>O|B1GBB zR<;t_Y-KC-&73)%`D^C8=Q;QJ-uvG7e&@ZvFDk0I!X#yq2o-_L8i*vHCGaLh4(9M+ zgakezCqZeG`OHCnLX};CvL(e6_CzGrNfY?&K|CSnVxU~4Nvv@*@VS%x{lMo*wzpWa z`lYZk1nUkCZkQAgUK5}2Aa*j)g5bbTA=3Fk%R{C3;L!4ErG1H`@BeecwkYRSjANouG z1bv#T#agL#+N>Sgt3x`jGePIC>bmaeq0*3nC&|TAYz^*gzYgn!&gznW*Dc)-Za<7n zM39$4l%O;fsLE+w&=pB&M)3Q&|t%2J8y)TRN=Y0JH!x#`GAHu6xA;*_F1 zRj5f_8q<>Y#1c;*29tyQ6rm(#s7N)Us82K65KSCC=+7|5GKm??qa2l~K^+>=f_8MG zE4>-SNX9dTSu9`~YuLy(n$nt%bf!D~7|IwXGM%|BW+m&`%ntT)h~u207Xul=I3_cb z`7C8M8`#P&_H&pMoaGX~bBp^-V-Aa0!CE%4ojn}n7^k_w6|Qlc2ZTDkvpA73EooW})S+@)OJM>)lLF7qdU^DiNO+?k!j`CY^%UB(q%%~7uJ zX1>8)9+B2hIIDB}X+PuVUCxzV!*$%qE!@tX+||7u?(ELz!hY5-_$9yM*ZjJhy0tsH zv%9;WhkA@Bdb(4l?rR;@DV^8lRH*w^H*{Bzf`2|`>K-GU?Bt^`DN|RB=XjBqd6kr@ zt3^GU(25SEOkE6J=*a+vr$XH*CNP!REKG&EC9GmSTiBTjb-Ovh5l(V073wZ>mFwK$ zVJg(&FlTav^Ezef3b?2vUDlPHGIdp4({Px# literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/m-none-p1-tile-big-contig-be.tif b/nodedb-raster/tests/fixtures/m-none-p1-tile-big-contig-be.tif new file mode 100644 index 0000000000000000000000000000000000000000..9aa6ad01f04ae0c8971096e0eb68bfdc149a1c0b GIT binary patch literal 4496 zcmeH}ZIF*;6vnULWknI&hPuMK6?IRZ6dVRSFTsP(+9j zQA80%6j6#&lp=~c+x{~(<3rC(Gkw|n`Ebshxv&5Id(LzJuQ@q6s;%PsYo(`>bfmOY z9;wRY=kSkO#-BB-NPdiErN0#B^lWL;p)Ka6=R^!w`lhjqyb%Ew6 zSMTU5y{|s{NH^n7sYx8F=~UDl zj?gbQt|8j3;c@fEP+Suzttph#Oe$+0)wPJ)T1H*1qLJ3oTwigT zhH8&SlBThg&_pscjS8AgRV|>VmXM_t)YlrCXag;^nYP+S2koS*_HqG5$<+)pHJ55y zNG&ZTTPta(wKUU4T5AhA+D<3!rWIy(q_xROVKybGKI0SnK14ZK92~(q230 zqCIpcjUJSU`;$R$D$tLr+(AtSlEuT+=SiCI94#58U7Q<#nu{pL<&@$Y%5npl^rspF zsKp?%8B9Zl(2U`~8D45bAlX~S6BGm$P# z3!>RUv>=F<1ks8hS`$PYXu#7nWf-j(O*_WZk;y?cBZ%e((ZV2F8bm9DXl)Q}45BST zv^|J+Gu+UbshrO&E@eJfvY6{w9z?5yXnhcE3Zkt+v?GZ2Fw$v^bqOXqgK4h7Y**!a zKIRra<1W7Be!k^Vehi{r46_*RVvKhwCc7*%oXK2QW1(xY)Y+_bL)N+(kMlFn@*6L> z2rv5}UiBeNb$MpF3iDlq#jeA0*JHICv)(P(@Bd)%GZT!uGXiMMvsI;j_mTemM?NKU+z-A#$|njGu@vLeFC5OBtG{keC^Zu-e>Z& zJM){*cM)IegM6hA@pUfmzOLfiUBmafjvsP8KjFq6s%!P3Zqg^ZO`q!?eVsCme;*#v z4|+l~&xIb3gZR8qO#=Qw@DiTae2^#!LU)_=zt`iZmkE9YS@ zNvfwLS8*tPsL0J6!JUcqwRwQLJVqm)p*bTG>)SGp4ospe)5B5bB-ZD#C>&*3ILfMU zly!;q1KAvovMn5CXE@5<#QHHV&IFfcipw$6l@se1xF$=S#R}JFjhiIaZ**(6IEU@- z#BO&>s&|@uxP*H;!@XU>{Sxb|d4Owqkh4A54Lu~WzJ*7+jmNsZC%TKLCDrqT&*B%K z!|%S(2lz4{?5kbI*SnH$@sYmE$M}99??>IhPrIpyxs^w|oyWVQCx;`=aJsMcVZO%A~&>6UF+NC^p4Ju{9=&9ZB_t`wKfz*n$7A1AhRp9dyM2 literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/m-none-p1-tile-big-separate-le.tif b/nodedb-raster/tests/fixtures/m-none-p1-tile-big-separate-le.tif new file mode 100644 index 0000000000000000000000000000000000000000..8f2079575287f46cfe99b0858bdc6e905497f975 GIT binary patch literal 4544 zcmeH}c~q2T7{%|sEMlk-xr@mP7fch= z#56TbNz>A_a4B3$#WJxhGz*t<8RGj+cxK*n{F6VJIWy0k`|6B7=!%|*!axkg7)-)jXp9iFK^Vf(2hoVd2#muN%))#u z#cF(j2t*n2tGEgcVqejo6A^NX3_kM*?PI9+qGg)?+iaV>kBWFpl9A&f^y> zMmd+=Zi_TdoHkdAY>gsZrLJ0QL2!;&n|Dm;S>WFiaM zxC?iB(~o6Xi2)2^FdMQNZ#za%h{aff{;bRzti^h4%n-I=7{l3zrCE{HSd(?wfK3_7 zcI?6kMlyzR9LA zkx5MEPVVI}j^$*|;9M@|O0MH3Zet4f@etFP&U0MAWn9Az+`=8)!vj3R6FkEVW-^P} zyvvXIDL>~|{FdMINB+#;_$O}@Dx_j6A%7j`X43@=LtR8@yxW?(&wO z%GmbiG@#0=p<1eE8YYBu5x-xPwP3=RU@@fYjspN`KpvEsH&dP z^LkM)>lMAK&g!9l8lWK>WjwCaLJNd^If35luR$7ayp9tzO|!Mo$n)i<3KsRq6L6i{ zwyS?_NNwBIe>J4G?dpZ;I<;+AuTIygZM%9)x=wA|)uRllZM*suLu%WuzSfZ1wyW|El8+u5JISm+LrA;{q-l@0aVi zh5PU{=94c=u>z|a`Lle1FR{6iw`C`GXI~>9$e|p=Nk%@4^SP9(jeH}wau@A?&d%FB z%ws&o^Tz%wyvCcnXXIY;QAw3I`eT5C6s(3u9-=l1Q@D{wDn@Y{ZR81>sd-vrXpau)h)(E? k@wm>DE>QC01or8W(v)tzjv2~Sma>gJKklhuO^-f-2N<_`R{#J2 literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/m-none-p1-tile-classic-contig-be.tif b/nodedb-raster/tests/fixtures/m-none-p1-tile-classic-contig-be.tif new file mode 100644 index 0000000000000000000000000000000000000000..77658b23ba15d46c6805a887081eff06dedf1414 GIT binary patch literal 4368 zcmeH}dyt4#7{;I9V?`0$h6qtat!%9bk!uuViwLE3V{1!AbfKh+q;%C)DMS?G8X-bN z5poMrL=>ecMHG717-RaU`DU8wpFQ84`QGo$`<(AL?>Y0#o0X+>Nd=XPloC<^~!a{ckFb zkBKin9Ov@=K$FAT+;~4%9N8y6j*MLXqq9d&c6N47m(Ceod*&{uO8H|RAM?c2l-M%& z`Nu#vMm2I&R9#g<_o}QOR7E|eDtb!Q^nz;ZH8s#MU7~k%na1l%y{~KZp>ER0x>cX4 zydG9%^;d?TRSmtYx_U#6HA2lbMy)hS?KNH3X^yh>iEh_Y_0Si(Utj4FeXA$*gX(Lj znrf7?G)`?aMIAIVh~@>+q99rxL~Da+V-S58L_Y=5Z$b2@hQ&F(!}02<+3KtX>Y^p; zu2t%#^+B{bh_(mOt{~bQLH37T)LR?WS6ekeJ2X(cHAwq4 zH15MF3ThlhHH8wINm(qKQZ$-Enn;?aQA)EZuLV@r z5;C-k8d^_XZI1KZPIK*|mG;tJ2f2>?WNSv;letvTLejO2Os%0-+>83!N>lA1OS@?k z=iULhk%Ht({KZ?&$Gbj(2|kIbF3BvHW4a24eHn|bo+>D)W$sV`kfIIS@i}8U=^O4WsQ=iS}K9AL|$=9yO7B}HXx8PT|W}mON ze1i-478miIF7A6=#=TkL^ZC*j@{Jqvy)WfwU(WAt%U{0M`P|7T_%@&9yIj&eUCs}= zk{@?fKjZ3t$#pzf-LyjY=}Yy|H|nSF|KSe*{`s7K(JT5xZ|acV4v$Pa#)nJ2&nfzl z)ATWCCY5U`=jsbC&{tffZ@D- z3)JQ{8Za!ezByxP#U$D@Jsf3DVto#a!cmroqpS@_*_c@0pY7o&yTVcShNB!ztRLlq zjB`<@xCAp@HnDzzE3?EIta1(3yKZ9rRySpbv)Ju6?01KxdZ)Ov3%QHa+})+zE3v+U z`#RkNoauqCUtjHCLZY)9_!Yg9F8=@sqW@ee4kHqAD`)dKF3e{e81=m{kj|aEnn() zeYq#Nt*3?~%?d}FAC9y*9BE}Z(z>L)dHH!2$g9AASApm#=0-=cFgl85(NU~Px^r|C co1&xG79GXT=qUCi)#v5sRUoed|6c|E1|U~2j5-qz3)BG`Oba6GwglC!>hpyfDb@- z&}79;Femplx#$LE_wa$6$+P-oMS)zoUrwIYuQWZf`4N+s$;nTeylk#MRdd}NOreP3 zXHA6U{I016p}F!VCeNz!nu(_7-mJWkDWYx8IOc2i<<#bU4vC45j*f|q3K}pHfu=Fg z&4h;uPZI@A6gE-Rgtxg}|9j<(=ml^1BLEdq9W_x8jnNFP&;ec13-2HhRS<;QXn;^O zM;mlTcSNHfhF~-%;XO1$7+Rt|BG3~t7=YmzhpCu_d02v#_ym!NLM#Sj6eeN@=3pU~ zVGTB7D|TWpzQI^bK^)?-7%Q+2o3S0cunz}u6sK?wzajyvu>pxl!X6yJVVuAjT*Ot} z#2q|@2a}P4RHPx@%^>97NZ}_vI46yn048RVQk6vj9^ceWO-I*4c20PhA^D1*@=;iVk`%96eqF{8?q@| zupPUw2m7!;hjA>YFplwD%oXg&ZtTsz9K?~F!0DXL1x(;-ZeSvlxQ9bIhLbsybGeAi zxt5!_jmb=5D$|(Gvz*VRT*dX=!X4br{XE3u%-{uH;SFZ;0YBpx{EFZ5d!FP^{Dr^q zPu?Tssbca|SsmkPp66x$!N2)0-Bd_VsiexQvTCT7>T4iIU_7Qh-d--=Kfs6h2p_v3 zkDETl=lBv|Tlo?EfFJR*mH&>v@Q*ozwhl%3G)watE3eL)tjESy-kfdNncb~Ch66a9 zILcA zwH;NjO4qLKsCqNHc5O%1qb;uOsQOfkYdfmG#^TzJs_(P7wxjA7Ew1gTI_2869aRsM zYu9#Ey^&nIwxjBi7S(o6^;nB)JEuB9$8Z|waoM_OZs9H-ng7DJ4n8cwa;#+KFYsl) z$|hFciXGUMy{x<+hj26}S@|r^;}Wj4@{Qceo!o2X2YHmIc+SeN@+R-_p_O~dTmA~L z>SGlJskRzed6-(Ny&|kUO0gQOQC2=haf;VsD_^Y*N>q}Sx7C|^OKP;)=bUS zA}!ZiZPGR+YoJDGyr$`W&DT<`(t2&t4(--{9nx`SSjV;Pm;Cm;H3KP1RhrVR>v%y| SbVHd|p0}PRznUkVf&Ty=fp#_k literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/m-packbits-p1-strip-big-contig-be.tif b/nodedb-raster/tests/fixtures/m-packbits-p1-strip-big-contig-be.tif new file mode 100644 index 0000000000000000000000000000000000000000..0f1fd3e18416a7af0bda82691baa3c576e40c47b GIT binary patch literal 1842 zcmZwH3zSY(7{>AE|ID})873n#qKG0>BSK_E6w7poP)b)*YAQt+N~v_!Rad1DQDad= zh!9ak5k(YHic*v!ihAdgYOVHJ>)ZQ0=RIftzP0x`xw&efviiGKEE%LBrIw6isLF+7 z_)nHzlvyc-eKf0L9MMQQm@%YOVG>#Ce=-$5u`sJtTpz=dx#&d3zv^66CaU~>z39%O zCvwEWx>TMjJhY1=RWe&qUEwTH{eQB|gZFaKa@_x9+3}x>Tlo@6KIiJBAdW zr(_iOsRhM->6`(5^YZfg=J(0!H?TO0-y{H+#sc(Jn)<1VZdMK5t~$CWmgONe(i3W~ z=hRwbbf#X>*?LXq>P=msw{@A`)m8dH*Xv{5qR(`PzSO6zsd~C!S$b4W^t4*) z1+~>Ub<{+4(Ny)+EM27e%F}zgMjvW`KGCiETzBa!J)rOOn10eT`b{tDPrXd(L^&F* z_L`tvO;$HeS1-*8^TIGM4fDz{uMP9YFn<%~AHw`gn19z8tXDWhuTfEN#)7@gQF@nS z6ZO_S_0eMGYq|PswFYT@m^X)cTbOr-c~6)RXdI<9k@A|#;hGhzJD;QV9>?iJPShuy zqR%-^UvWmF!P=mq+M;3FuHo9H5!$EGv0W!nR+CB7^w>gksG)__(NY>{C5^O}=GsVW zeZ!ghfwT1s=jwMZz+9GSr1onpsVbnHrjV|gRMR|aX)*P*oGh)TiPqCnn`x_Ubkt6| zXb(MgfQu+a9_6`)!x_Ml+{)41m1s7ZT0m_rp}tm-tu-{$26D88_S#NvY~F6#M=#(~ z%F>rK`cZ|Osln~k;T{_B5RG_(<~&Dh#w1!t3vHr}w$ed6=&arJ(0+Q8N*~IRPdfdn z#vp2OC-u0WEFPr^Pt%eYXv;V{GLbG!r6;q(JU`K1&X2d@63TET6}XNJZX%Px)MhC4 z8Adk4X~qb07)^U7kjrGcF+I$4!n`odOT)Y}%xlBEF`+60smT!Pavu$OgvRlfwqPV} z7)u8V=*$#)Ff+{a!n`=l%fq}n%vcrZ}CMuEspqVzKM7+*z!46V|(B!i{{&ZG6Gqe9eP= z&*S_Y=Dm!v81FJnas{S2gW1kxforqG^;zL;*0>oPoWmBkXS;LR$1!ZS>?v8a|<@P4O`uT9q!C-_h7$!JJo$$&iPJve^>LM zgg0E7w_Kffd<^gVcs}w;eCnor;Z}U@c6{$n{Oqpm_4$@BaT#Cf3ck)6zR8&$?Ajjc z`X1(N4|g+M&*-!dZKkw81 llF#z1PW-yNdq$Y&hIvt#mxXy%nAe4QQ<%4gc}JLc{{4W-oc7&mTgW$IlOXMO%@^(nYL?_!yGb)9JVZz!{#u|`IId(hm6T# zB6ATUG@*$iY7RA~oJywDNX?EF5ow8jrSeMZ`j_hK z;VOp!N1pTFPD``iR-wm(zD~)xt3E=j_J|(!N@-R(1XC#IY~Zj=OYB@K&lP!iZnAPx ztdB>yxncqCE}w4szSa;8Jze@O%O8Prjk|w3eNuc}TwHuYY|yae2TcqeV|&9NfvAlJ zXpH7)jrQn(uIPmUcniZZ1`{y@rX~!65Q3IyiwJZ=5A;PGh9JpNc?#aadw3ro;6s>D zgdqyih(UkEV;Dwb0;VGk^RWag@ew}8r}!LS!t_Bb5|D_on2cGNi$z$5)mV=$*onRP z3g6;;{D_}nQZNmvNJj=%U@bOc8+KtI4&f+H;w*l}ANU*pLNuAkLN+#I2ln8AW9}0; zjSIMptGI!?@L)MU$trw`CI`7Vh&<%u3@+jduH!aHFM89Tfvn92Y|Q3tO;dpLC`1v8 zaR=@!OJ7!IO&SIY5$wbs>`QaY!ABXEryr}a7V9&Z%^1osMlqT(?9X@(<7iHx zsl*zr%SLR%7Hq?Cc4T+CSIoEIl zw=##hJjgue^9(QY3a|4v%>pjvDz4)uZs%_9=V2aa0nf9LMJ(nWx~r^wRaxc}e#S5O zHNWEz{E5HtcmBm&gvzMA{8UY~RA0erCUcCZc#fC&C;#DpbkpOiph~Ktx@x2*YN0j? zS4VZ1xyGBkM^8PWimIxo^^BfVQ?*h%MXHN>s-FgFs7A^7C_vR!M-BD7UewEaRj;X= zdTXEtYlMVM#-768a1A$c51usk6h1~D2CzEo(AZO0kI(W2 zzQk8(>?v%|4(!Ta96)1FVH}4riQ_nh#-76IOyhhm;Yu2N3RiPIw{R!-(%4hDkB4}a zCwZ2}p27>f%&WY?yEOI`QeN_wzXE0KDXb}@Acd%+3R9G#6(eI$VIRdRL5UhG zV^3j8#iFhThcM8maM`s@a;S#aga4+Munj;lcDRj48r>wom$ftUaQ literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/m-packbits-p1-strip-classic-contig-be.tif b/nodedb-raster/tests/fixtures/m-packbits-p1-strip-classic-contig-be.tif new file mode 100644 index 0000000000000000000000000000000000000000..7d58e534ec5b4fec5fa7ffa152be9228be2dbd07 GIT binary patch literal 1714 zcmYk*3zP_D7{>AE|JX&5WkZB0qE@!nB}9lQj%|ogN>^J-rRYK_N>REhl~RZ(4n>5> zYDE#{8kUGslyWPgsCQS#Y39t#JMVm#-*=hpY-LC)t~8{Sh~lBLa3W5}DiS%Xv`SN? zFlJC{6fAl?E~r77!tw?6^C()lGI#A7MwZ|$$}3ju7168a&mh0?wQ%A|M6;t_aqA3 z6vOYOH1$z=J)kOjL^bq;YU>%*)k|uu*VRJfbeZ1O6?$J+=_6gMPjs_B)9w0F_h_{q z)J8q3@ARa8N>ojQRZGK^spr)|uc)csR4YwTJ55nX%}_VZ)%9Aa9DS}k^_BW*ogUI= zJ*MyVlz!F=+NW3br{2;(dWWKknrp1uXp*uuO`SDM-8DbVi^9At%&Wq@AE7iey-#U<6x06+XXrD|PSiuW>Zv8_trhC4wHlyJVcs6*U18oE=0jmV zrU|5K3MDmzQ#Ci%Y$0drbI#FMoUe6UsLfog@3}P5KyA?w?a*-T)`*z;Q97crv56*8 zT+>L?tk?+isiH;H&@yUk6?L_N#@b2?{m5ndl`Hf+SLrXV#oU}|w2o>#DVj_PO($J* zsHj}3X$iHof=sQYfi}@p+i9g;w9{TX>JZ&@jO$4yhmzdMsr2J?9^yb(9{Y(32AMCY`=iWB}C|Of80y$@4Ve6`JxUt(ZVNrqGcYbYpIq z7bZH)HHaH2#%+}1Zpv{#l^IBNhLFK=Cxto6z1(=-WBG( zVLlY*V+jp;jb@CYH4|yiR5~#;%=5yW7v`m5UK!@~Vcrtv9bw)b<^y3q!dSy37iXH& znC0@!ca?-L%;q{4a0`pMgXP@InlR^wd0UuwhIvnz4~F?D2l0-W%68|rOsfbvsmv& zY;kjTxDC6V%>j4jh`T4e%pbhL-@NT2yyuhoz-5^23M_C{7P}_PU57QUPrjS5%`MsK zw(M~S4!SEx-NPyF=@RbkboX^d4@mgXW%<~Z_|#|dh0o<{U%)qR$hU6B4{psbZqI&q z;;^r=e4~r`Hkb0uE$1S#CN`gpL{vLIpGg?;cs8( zBEH2Z`wo}!y{_Q?uIfRq>7lOU=Um?--Nd8a(&OFMlik77-PLmv_W4Tw^ws?18(h@4 z`V`;gvcAui{IJjR<386<`vSk{hJMY>JjSg((d|9eojfzl^TM1L=A~g?8RqqgOZy(5 z<_CR-ANARO(&zbE*YnH1$Zz-(zwOKYo)drIE}k9c1z}zs=H+2t6XyIdZwvFzFz*TT G!G8gXD1Qe4 literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/m-packbits-p1-strip-classic-separate-le.tif b/nodedb-raster/tests/fixtures/m-packbits-p1-strip-classic-separate-le.tif new file mode 100644 index 0000000000000000000000000000000000000000..dd0c0bccae48cfc46ca62648dfa4bee8c96214d1 GIT binary patch literal 1748 zcmaKsdw9rI7{|Zg?>1Isvuth~E0?3mY9d64O!JuBZ)0-VvP>?+Y%}IATVyU7lgmWr zArqO%G*J`fR&KeLOsO$7tkBt>J)Zsb`<>@~&iB0MJ@0#-_xE{DNJs$O0eAp(1+A)P zjb+-)YS9I@%6bjiA^jW5ZPYt6gNUvkE$ERUPj)vdoI=Bl+?bSuqM zXm#x}U2L^)xwDPRsjWv%hJ!&`Yu?@v;|{bfI@3Iu0V<{I-mk@v~(mq zo=fgq-X=svMn*PUKZYO@qcH&qn1T230Y1b>_!y=gf)R?|2*WT$VJu=X6|<3o#aMy0_ynKg zb9{-fVZsrCXv82M(=iVVu?(xQ9-FZpyRjeN;CuXtpYaP!B4!~OsYt^bY{XXV#9kc0 zVI0R9T*7bo6My4h+bolg3}hk;*~q~m+ksO!Z~K1(w{RB^;l|2*nzi^0%^v&f@^B2N zaS_*W6L(N-A#kS$edx#fY|N%?#r8Dka0U4&KoK6mm6hnln)Ih(Ae*xdJF*LVus?^; z+_zn>$SU-vFY7UYL2Su(3}z^MGmOI+#j%X#RGJ#B%Z6;iW^Bz43}JWnV>lxi%^1dW zI_GgAmvI$M3$|q^c4aRP}7*1j$XEB+nOye4Ed8ww%XZ(U+@mv1DpLl`4@(&jBKB0=LB5(Pso&pr4 zmNF-JmX~>*fAJswM;AS*s;Z&7YN#e^rq=495Or5SncKX_N35WyR86(@te(@0YN593 zq^|0vff}lj8ly=vo~o`oYM|%!l3vkkdP8rjuLf&`Mrpj_l%%;@prta-6n=*jIE%}; z4&zMW@3@V7cx2xJ<4oZb^kj9`VFMax3LEhSzRXwoI*l`ho!O0jIEce(oGFatXii`P zXV5rPIGZV4%oSWq<4oatZsvCG=6)Jy3J>rwkMj&K(Ku6hl{a{cclnUUnL^549`cc& zj5CG)G7404wUKeAu$_Vxs@@8dai%a_5sFrf;$@sEOw=qTD^+PS&J?CALz&7_woFeA z&|7+2?`oo^Xr|_Ck(O(XpeH0r(-%TGftB=O>^|VmT0BcX_K~Tm-gwPj_9P$ L>5B4Ipd$Sb%g%(6 literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/m-packbits-p1-tile-big-contig-be.tif b/nodedb-raster/tests/fixtures/m-packbits-p1-tile-big-contig-be.tif new file mode 100644 index 0000000000000000000000000000000000000000..5f5a1067aeb7dbb3ada78f22e95d4895037517a3 GIT binary patch literal 1968 zcmaKseV9*W9>(v_ZM(iDx0e%?e{d3>bmSc=a1*PKG%89`8>b-{+;}MHC6@vFtSP) zSx6bB>sYEvx`v;z?C+UXCY@tgS#ian%rVA@vMC8x?nkEdi7Bi46XsG@Q>8n;zbm~H zY8{8={ABMB&pYEdEH}JLMy1j>TtVK?&Qapr@i^?g-wA$%; zb<|{a)pYgHZ1vRwU9P1n&>Om5Z)%9%)*X6J_v%CaQU6ZDVS+kqs`52cy);+-v?!cb zgwvXEDh{Vj;j}HBJ`ShP!s*L!`bHD6UgQ^v`fG^>Xq5(Pod#=zhG|PU?Fgsc;j}ND z4u;cFO{T1-Q%SQqNeif{rJSlaI3v+;ZPiHa)M)L|812_s9oB@X-&87SCYhQ`H7%mH zR#0DSXslwIYZGm>jSl*l^Ae5I5ltdP)2OIfWNSV(w1m1^MMJHlsW#A3TWF^pbkuIT zY9BpxkiI&~<&>o$(L!>xoO)VKBNdUWjkMNQ678h3_K>gr^wMGa0asChLNXadHEyFe zcTu1F6K$rgwnx);(M5adt^@SZ5&DzC04he0vKdSbhEbP$Xvip<@+d8Nnsz)-MV=D>)3O9wTYQXmS}tYsQi=fzC`NpPBSxZa6Iprxgj+8A=^S(0~VN z!XvccDcUlQJSNeFX>?~6eV89kOTuYYIIRn(4dJvUoOUF%;u+dAo=!}m8#Cz1oN!tg zPRqk-bvPA;)5dVx8csXIX-_!q52wRSaESG09+$D0Ygoy3tmS6bhtuY8+8$23!f9_f z9SEl*OmYU(T#;GMX1;5%#B~#H;T`VeeeUBU9^z9T=ZkPU#CVG-F3${CW{#_}&^au3 zJyyFBMb2fTTeH?CKusIv(K$e$Y+)h+Ft6 zxAnM8-K4j4yWZ8^`aloplj8U=Jf_d}XMG*tp6~Qm%BARKvh*q^>orc%>ztmVH~F>R z<~Mqe-|9ooP0?pupf9;t-*74B>OfTDdQM^pHMxURxi>|Pd6?!rK^vZ>0~1r!mFe_g zHhozT|B$6ADr8M`zBoF+DLTI`MWfgqE#4O`J{T=Nnxd($z)WW{*VS0$+9_J)hOBc_ zHn=5Q+%82s-I+bkXTN)K*!==I!vkE=1D)-`uHj)Rs^^hzSe7aBXC` sJ~G_AO#l1JjD{?ahOCZ;6h%Were_%8?UDJe$b4^PexUgO>|dt;0V!Z literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/m-packbits-p1-tile-big-separate-le.tif b/nodedb-raster/tests/fixtures/m-packbits-p1-tile-big-separate-le.tif new file mode 100644 index 0000000000000000000000000000000000000000..08bbda5c990b01396a18ed7d915260ad7bb82b31 GIT binary patch literal 2139 zcmaKrd7MsF7{`CV-)xF_lihHXolFrSGO{*4$WE5A&SWyO@B1!9)~s10`w}5SMA>RA zg;5wKqZCrMvfSgnx8{$_eLweozVCC+^PYRp^F1*!#gPs`z`5<^MKBZ$rjW&rOq0v( z=EM-l<$zlq;^e&Au%W@h@H9DZENhya_aC_!_Ft`?XYbrTxk#VdB588o8YL-Y2tESZ zxXCJM-oty}sy?}D3_RQ$Tb}6FR(IC0`(>|HZmvO^b%P`SfA>s?kB?7CjEm~p54+s? zNg=`UfuX^e?8XdX!QYV)!FbS(McoAhT7_7YLO^RDMmx*Ud7MnhhCC>M;?7`sR7OqI zMPsx?d%O*k4}}qhGN_2EsD=7yhSrEjXY|AX49B}L(WrqsXoMDMhXiy*9}LDQOu$sk z#zK4y6N@+`q6hk87{+1}W?&wcU?nzSD|X>4m_Znc@tA^HSb$|%gH70uJve|PIEl0P z1!gW5V+Gb@3wB~34&yjZ<2)|mDsJK)!f3W(H})e5CvXNAa2eNe2V^>CVm9Vs0h(l_ zAQe||19uV1^vukh%*VovVi{IsRhrw5D-q1dtc+xS7Gp`4V>D~94jZur+tK8pVIh`a zX;xqr#;_inGL~^nWDoY|Fq*Qg#OkcghHTEZ?8q+c%|RT=@tne0TtL%=t=NH`*q!}2 zlw&xN(>a%mxq|Dtg*$0_aUe%<94B)o=W{7nb0fELH}^A%CwPWt8s~5kmvbFAa|ieG z5RWmLDNN-R-r!xDPxu+X;Me??-}6WQ%-{GYZxf18MrBo`%u$}=IbP%+{G0zWL{I8z z<&aS!l~8F_PZQx4`=F_ly~i&Ye(dTQ#RxyD<(&u~4ZEXu9tR8%jjtSYIxYOA4|%LJ5Nc~wx) z>m|LS*YvvHP!qLM2X#_+^;3#t#iKqEZPig-)Z4B(RAV$z(;a-&Y@0X!{=2(}gE0R7 zyPJw%Vf_8~_WB7LfB(G>6ru6=-|I_F8h`)2ZpG91`|tH|6pg?CUT2rk`1|kmdk>Aj z|DFZsY5e{7+@OrV|DG}VWc>a2Jc^d__usQCR>t2c&x%1Z{!V$W%vJbNoWeO=bWY1P z+`@f?TdM=i&b%yW@ddujS6R(sT{dP*wzt@sJvo5GEl%K6&gMdkE4hJNxy#}K9^pxz zwRnkFd6V}nrc)+mQy!~pVMVEoDq5_eI%=dA7UPtt9_nv#q{eHCW?5XU6Z>6dt@kudbF@gywN9J0Lwj{d$CUi|ve+7%v-ZrU?-;MbyY-#3I&DBZ|w^h_ETWnr$|1ie4zCQrWb%>19(| z#8O-oONhKi%VJH0D54alC`A)kh10X<0C>3Z~Lv+8j*VgX!~N`Z}1t52l|q zKB9V!UnT0RMe3&&>aR5#s0|vTt--W2nDzwI{$M&3Ovf~l@|sE&&Ez!Ai`*7-mfqvs zL_@Vr!?jBzwO6BbK%;d;vJwmG)6}? zfecNdvSyI2xzy4kaT&_Y{jtDTYZ9=d2h-E@fFI>vRBr#R94sLN97YbCi_ zN4_@ED(aDF7oD`1LLHz-#NP+FkxG=1$pC6_4|REf20WT*3vIL``m~$Q+DDNN(o09_ zO9uU@Onm3st$D>fB8ohEkv5?AKH~|#;u*dRro)W2nBb~1`JT&2bU&x2Ph);YmU-&Y<@fG~w zp8V`A7}A!YBlCbn(-{Hd5QLnPpvLYr5iKp&Aj-BEKaQwRz>Gaqw|}i^V?Hv z1bd>z`=iB&qQ%EjYqBdb-I>gG4Hmd=YOQc1);Nz1ZoyW!O|4z-#9kM2z&$wPK4Ce- z{ao4oo$Z0H{RmHg=6@r$o_1^?c^ z_8(o%ce%Fj_t}2L=lKaY^)qhi7v0Wd-O-cW)zjSFvm(R!iL-o%Pxrk((+~O_Kj!oO zXJ6>&e34)F#eUV7`E_66H{H{7BEyA|;qu6Eb!50cGTgF6|NF{}hAfSStc-@Ni-v4U Xzr!%^h|G6K=KCV^gQfpx{Sy5LLDHf} literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/m-packbits-p1-tile-classic-separate-le.tif b/nodedb-raster/tests/fixtures/m-packbits-p1-tile-classic-separate-le.tif new file mode 100644 index 0000000000000000000000000000000000000000..8bdd181e851147be12ffa716c3ee8330199dd38d GIT binary patch literal 1979 zcmaKtd6bT27{;&bnlUpWCfT=V${LvvSwk2a2ieIovdm;MvhVx8jL4d0WQ~wDLWGDS zWXm2#$tZ=Ctt`(oeeXH*$IJJf`@FyRd+ztWpYzH`Ar&9I~7zz$Yx{O_NhJ&9kK_WVGw&ZB!4`wYx!$ zG+fiN+vRUIqU^Jd)7!$00`IZ6sUNqOw9zX*E-o%UAvUsGe?-`Yl0m>Z8x@0r1RH}w zfa0ORcpKsR3^6EeuWM4T|G&}D7R!w6$cKU`ZkH>d8fv3HnxZv2;60cK6hS1)qB3fr z4jQ5b+9D2J&CK2 ziFsIpl~|80*okjp24fTwF%`410L!o%8?g<$u^&fp3K#GzOcEAj1=e9Rc3>|K;yBLY z60YI~?%*NP(QL&o?89N4w8wlI*KiB>ZIi>8nc10-1!>M887a7q+jxLrW?&ZPVg!pY zl4V($HE8bHlMQ7iW@BC!U@?|rc~)gD)?*X4VtbmLG%U;#EW=8y&S*AZbH*^13GB%M z98Ob?RT#y(Y|NHy$4>0ZJ{-(ZOypF~<^q~#Y{QQ1%pUB|VI0dzoWUe6<_fOkX6~Tr z%|RT=37o=NoX@3P#SPrbUEIgRJjwGk(>a%mxSVUbiQBn{2Y8HUn9LMj=WRZq`J7+! zYktQcc!EFi7yiz_c#lx1GAWz#${gisUgQ=2$$$7CgOpy+DW{AItAxs^l0v$pABJK~ z3PL`_NB9_@SXzTG@D;wXbO=A<9DYtg=pXnS|JvcXiqMRFp1Jvg!{RK>3asX^4jZxs z+d7PAH}>TahvPVzGda)UGOp%EZgaSo2YH-l9bV=&-r{|S!OEa4$|Z{hR7|B*-eGk` ztAUzZG&gydj~Jq7l~sB4qFz!-l~WZ(sjeEUrA)YTD8CBnWxcA3dP8sOEj3dcbyR2d zP=6&`E1vWXZKqD^sy?pfFpbqD&9L~SZ1=nI_djrY_#VdJ|G=Tz={NrVduRO&jlch1 z2a3}8`|tIoHjTgkUbo_C{QdWOIGV=af3LGkX#D;6`n{XR-+#}7OEmuedu~w1-+#}T z2pNC>J&&r&`1|kK6(i&ClxM|Y8GoldSCSNR6sK_!SM0Rh#9chHe<{|baOPls7IOFs zU*qen>99VVvNbz6?806g$Po@FavJAwp~ID2&n?{Pa6gam6fZcu${W1HhYrJ(S=p7( z=~_gQDyzy4YpI@^sFlN5C8(zcI2@%!P1S6Ni?u@QwArF*q__2s-qri+tw9>837Vo= zny;l=r48CD(_Q^ERAcmkrfaSiX}Q*FleTM*4(OQ9Jbj#YX`c@3q^p^%6kXSCi%%P; E$FG*scmMzZ literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/m-zstd-p1-strip-big-contig-be.tif b/nodedb-raster/tests/fixtures/m-zstd-p1-strip-big-contig-be.tif new file mode 100644 index 0000000000000000000000000000000000000000..b6f4d9a0c115d6f4ff32e29af0675322d89ea2f8 GIT binary patch literal 1582 zcmebEWzc5eV1NMuD1)Dofq@0e1~M5$Kx{@v6fq&N7!w0Chz0^C1_2-h0!C)I7~|mp zkO)wW4I#z=G7%&u2vHAYLhWMoVH8DD#{^c!z$gZj2kT@7>lKHqV`20H!eEBuj8Y6> zIgrsX%qWdTOa`u(5$+ZZEMkU0KS2NwgksEL3_?-|l|-c&8z6k9QxKX39Ci#0tm!40 zX=!PhIjMRDm2fQ@TlN1YOkiN;VqlZ>cX;q6>{@}2+n$5f75x8K?)Lb+Z@J$8h&|_v zFC|r)>=VyEnp|jgM@QTKOM=as$MI38mj9TxEbL3yA_ZUoX(|}8S~A!$I53#2uHk&> z!=7yP!^lx7o@bJ)eBhIfF0!sJt2SwPE!=LOTY2r-lC75B!C`4SJ`vN`>&8ZITbJTn z$s7=Cm3wl^(o0j_gN5E2ylAsCzEi2_$Hjc7*ZG;$4fl?PIIAy-PW}Gi&mzG(F21p6i}Y z432!_$F);jJ7=PksH%;ilaMUSt_A)qm0~M`9p+rmDKeUR$6Mw2r4vhKiWn`ftqFYA zqo17eGRaFUKX{F4VcC%gfp}}10%s;;o)3qz6(^tmxUAPCvqy8Ww?w#JYeny_ zi_vq>zWeavOT`pE->jLV7AaeGwLe<8VvgYKD@=9_jzDNStH4d@n8qP- z#~VrPf;LB(m;(>kuoUp!ZF5^>vBY1Y`rC=%<8#elEiqi)W0AP$~$W&M`voD z`on3wD$-@|qD)cG`5-0wXSWmIDxH>al*^l;W)JfO?Bqn+{E z!;|JYO1v{Q4)aT&e5h#3qtaHrWkcV_{2JfI*^<$RPux1Has1ogUE8zotu5btZLO7g z)}3YflefjD}rys?GvbF=I-_76`U-+jKG-Tp`A`#pCWx2A14Z~JCu zM5OgDhcgk=!pfJwaywhwb^A-$%C{G9E!yIo-gxR;&sEOq&r_GJJ`*-WAl58pWx&&e zlUb%ObIH1-;kihuwdv2ziOY}8k;sjj<7v3;$aMZ_qnMcjOAk(Bc4y<6`PDAYwl73~ zh5LNz^0XtB8`F;7?c60;Ec-}rk84KshA;))1CeXgqrWCCmD9OWU0SuyXT9l5Rt5kA Cd^q<2 literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/m-zstd-p1-strip-big-separate-le.tif b/nodedb-raster/tests/fixtures/m-zstd-p1-strip-big-separate-le.tif new file mode 100644 index 0000000000000000000000000000000000000000..fdc021cbac549bcce63ee566fa818ab88b58c49a GIT binary patch literal 1728 zcmebD)MnsdU|Pe+JXhb{{W@uLOm101`%J! zu$^HeLlMIchP6Oe4ns+1T3T9WPO4r(CEOB?t@?izPUx~SH2G*48EiFUR#-f3>a@s@ zO1rh+s6E`<6u0xvN$>4<%Wmgx@BRMSc|-5^i_8)S7}%ItIwxLA;fl`;TH@X-B+7N= zx!)Yq)O|6PMNiy&MZ5WUe{EiVZnjDCHlIkN6!#usc|9A83F&^bjT1MycnUpn>lTz& z(~+29e!cA4Qm@HMwrNW^CMr5LF_af(rzXZmhFZo=c@ePCv2pLN?OQgkU%Ptcisj4V z86d7L1-iCJOW&Y8mG{y~?>k?#!cIPrch~u;vQJs<&w0k&7pan)Zh9Ge`I;@>bvYtK zq1Qlcmcq%*Q$3uYQ%_IT^q<7@vQXKrOF&XdCgb&_rCw9i+?(F4xVXrror{-kNo(rf z=BS`2?tPNlZaFh1JXpJC^{XZ4UQXkC+2!}*XeYn8oJfT9?O%)5y`1vyMTg#tx^>UX z^wJ$0Hm_ejd)M;hT}zC2Eo8Qu|JZWw;hnQL@0>N86Y6YNptE^2^%W{pdDqCs7UbO9 zQ~CMXjx7;|&w3<}X=l5OcUZT%wFU`rHuK3^eevAC{o*9OOm^{t?VDrYW~f`=*x9#r z)s^Kwlf`&%8LOY2K0EeJ-1VxdSs}~4`}zLv{JiYUG)?ceHyNc`SC)D8a(!L7S?$bJ zb@#eE9UB)NY4_-4dfHjM%zLVuYt@yCiAgT)Jfcb_8OCbfQ&b#_&SVrS9ckkhmecVl zSm4kAad3q%GeeWRq*8>edzPFS z9~&JW?CXv|)`e)d4u0_6`S+Al(VPAuDgbJ^{ADn82t_iTN+ zjeprn_e{oy3rBW-KBF6~UMTRrfbV(%_w)kx@B*fC>sv?9m4>ehUh3M|_fUlIAy3}} zrgLSRn`N~_SNKfi`?~V-GN0+v77eTCzKPpz_4Q@YV#mryOE@Q|dQDb#T(>pr$}-Q+ ze|IX2pPinn;obG;Mq%=?Zui!2XCjT$yr-&rwtYDfsO;7)s_ECm0d==3C~7qf9IoHh ztM=Y(>$aKmo{!)H2PRIH2?w~sHYn>(a5!`;qxv#)?XL5_Qy0yv4gVp$?h@Z*In$^q zD*_hSRX*EdDV%nCimG$j5sSi%3-fKuAFa{+{PfghW&6A>5*rf_H8GanTOxTGnpb`l zJUrOAf6vZsn{@&wv7Gz}PP$xQ?jBt`wY4-g^lF5ll5-0Sv~ z@aUB6!*R7y?SCd7m&u(Y=q4T+B6w8TSEo<~ni^$+sqwhDQp4@EyP@nAjqY4ue$-z- z^Ba;7ITl@Pn>ux>?+K9}nf%=B%=F~==10s4Mq86#6f(ynI>fWnjJlz>Ae5%tF3OqX4mz zTz}ZyY4HpOBR=W4HIwxvZR5nX?EQ4?XMj`%TXqCFzg(e8nU1lw0m0uN-sYPaX~LreQde?0#>z2w8{?6VzzH}5dviW& z++QUYzlL`)J*r;hu`^`quDhX!d*eNv@69%HGD|vB+v4a)p)xG}X{C3pqUfznaRDrN za1=4Qx43So#EYV}uXlS3N_2D5FcNL6$YIobg2?V7(@#Hg2>ee;^171pg(QZp{$h4I(ZXYiXqoF< z9wr-qP%YyL2pw%Sqx`oGLFo>3#6W9MBfj!T#b}aL0nl7Z!rvgQ!xxX2waqA z_Y2cg>j_STbs3FO!8mop;31t(2qyYPwkOA-Hpn<*RYDP0+0WP7@bVm@JLWh6I*Co* zl%De4eIHS_jdb^ioOo>?9&IYfBzA35jg=H#ut$KotbJtSQpwh6v;Ct#sqUJXH|DO6 z&lz5OGG1tBmXhrv`ojzRM&fEf(KXtY(&fS!6|24Vjy5oloXk2A$IL^0KiFx7ZqI?F z{Q)T?nX2+!4_wk@eujtDl5ziGSTrzc0N_v`>(8 zPqAC0;*w3`KRZ--jX8>1ZA3n43Jf-~8?IK^Ne)|_-^R0EoWqo&7Ym^cNsSfnkN|eo zdY>^X8Xa0xQuPWdfub5)_5UiI&}C(4^3gCd*lNbCuz1?kX^|h5c5A;;d$_kLZs(no z-rMh%-Ok@a=>q$$!rl`3$y;*T_kxM%lFWZvV)VdgW=wdn zcFpQnOU}KV#`m(z@5RwhesMXG28E;`cg(aH3* zvv`^JR5jPCD-{!yT-td=l}s{>)x4*uI2N7BC{#Mq#w{$T<594{p#kFH3SVZ1CU;4t z2J1773&iIIc^N4RE4B#qUd~Xz^Z0+P@%#VfXZQTS&l&bCIWayqIy~6l+tuFESXW(9 zT9ltNGBnWF)7jSCP+L`AQka*WIsbdn`!bO&!Jj7?T`CmZ7$ESWg#UZa`fnNe-%=Q% zK~xJ2qBGE#y?FiXfvyG0A4;@eg!-IVw*BU^+w)X>mIvh5Q)1@sMR?mGCx83UN%b>-Mm5-KiPEPfjtn9dMYu1%zo}K^hR2DxwJypZI>(7nC z4Z>8mDG5w#8C7?er8? z=dvRfg&7y-+m=6Cqxt#ismaRrd0QklCLU^HEWNix@-j59{3v*MuyOyMo!d6+1WsZ( z`4OCSxxU;zx^`-7X=>=z2tg(178b^DukKwswr|_IWsBy`^74QMG9>#LI9xr;tRBE! z$T8XW(9DF_whGZ+K~<~eHtIYxX1U<$r&0f+Ojcp#uCtS3**#YEs%)LCQ*=V1{?5l^ zlJP!;NBH!63=T4UK4yD+>v5Uvo{dL^o$qWo#K!$5di&cOYXT>$2)*c1t()<%g`00j zdETw{p|j;Qm-JgdidcMxo&Qhu_uQLngQx2loscWtGx3;2d`{ufDcOhPYNOi!Ogt`= zJ4et>JTgS^sIae2p$arL$^uj4adD-F+i7=0*((~|xxW0UzkcR7Bq4Gvy4W^#>Qvto zB0Vzsx!IZN$??(Qfr#`7E>rvw>5-qCojKUo-O<`qUsG9DT#%QOm2v+)J#hvACYV{E literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/m-zstd-p1-tile-big-contig-be.tif b/nodedb-raster/tests/fixtures/m-zstd-p1-tile-big-contig-be.tif new file mode 100644 index 0000000000000000000000000000000000000000..64fbb7f6056575da8240b56913bb267770036c50 GIT binary patch literal 1641 zcmZvcc{J2(7{`CV-;m|vqH8T$u4Twl*RB~_#7)CUDpSgC7SbffR#K9Sk|oIu(>b|j zklme0wj3^nY8nPn7(@0ZGPv6A9nI9Ob9&xCzTfwA-sgRu_dVx*U0i@|fHd$W6vajn z1ON%K7h2$#H4lQ{G3Cz;0$^(@3uMBK;TO&fY?tccX?YPENzG0AVV?Ldzz}fX*BTPA zZNNW3JYWgBe`VrM#1(}Sjt>e7!iNN!5TnHqOcxo6dXY-jh;*IVn!*{TAAJCryaph< zizsL1rcF_Z-0rhg4;w_=7^zpHpdt0qcCh13ZNA0BjmSJ=cknR3$(LPGkpCg=1?KEy zcB{<2AbpKmpah@;ax{0zy#|+ZIn7Sa+%57%KUi<&Ia|-2l5>=06_-UrTEvqOFVoq+ z&ho?6I2B^L-3r@&h>d7t+b|AV4h829tDq+93WB`xt(&eWuZJ43 zQ{^?Q@{L2T3~*WkZGE>V&fm0s?p#0%>B(>zta3CX;o3g=e!Ni`rRiz2G)tbzt|7^z z(H%L|>zj}p^kYo41r801Eh|P*`Tvs4MzHO%XY1-;`WaP3b;9Gql5>oBx0BxI5)o1+ z*a!WkR+FS)6RU~S$Db~b_px!6?lbR zq-RJW66Lt7{<{`ESvLHbu<|9&hnZxX-kz|c$#E3=Q3iFtNsKaPN@mxBORDAU;r2bETsS|QbYSymgj={mQQ}ICs5X@l1G(%;LBCX(I`KyRB5Np z%&H{(V_%J6^&LIIIrmPk&ScTGb6sN_1no}V2*KZifzf=mrd4jP)I?X~D7QuGPV$f(7yg~OdoBoZ6AF{JX=6Wcc{ZI7J&!Al;ulrV`f5meuKxl z-8-FI_SD%{|5jp1-I}S%CruEBPxc*o>e}wuWXH5DH=*f~wbN9uD95Ru%aib)XjTIO zut_By64ws|0UZePg<2pS)B&A_o<|oc?!k~0t5{Z zAq8Ybh^*pdL>xp=0E(XRqHNSMx037HDs*C2aVtK#RoqGqB}D9Nt{_e@mA}IgzLUFQ z6}QsE7=ZvQ`aXt;B(K`X%6=)|;kF|9|IS8`9xBSEF82_emtb5Kaqg@73O(P}=pZJO z85Fv2Q$zx25ydl6LO&WU#GN8c5}`gu$ajiRMqK#LL5hI~&;nIR5pY~a!Prn|xWoX+ zYb?O1F^MUmgV?`msjvTcW6;`WbmWq;vm zUGND$St=dnMNz2vd`x5UBE}&1b+X?-sW)QQx4cs+@5$YK0Jcwe_F+SVD}+ntK9kPeRL;WOt0hNVY8<`A!&6->{48KWV@1( zrTFpov3`Mcpg99@|hR^jB&%j=qcTN zuTl2;#DSM5^6-FR%rFM81nW1W7+{`=rw{~&_7!Q!1p-<01Tv3&K`;;pWuY=uhq}-R zTEL$n6*@x?xC`Q;JXD2Ra3eHD>0N}N}v!Ms?$P!{goyn){MB~x{a`nMpET?$v(E7$vL+(do)`O zhsgTbWF23wrYTt+?Jr-)rs}(kRi@qK1)N@rIm>#_S8>A4^lr0gHXZO=7EvF4B)8pY zw{fqdPTF)2b&A+we!lv&hwmspYMyJi&Sv_+k8{k4leG7HaXX1zoZaV?GSw+|hjqW0 znVwtfLglAsq>HHGB@Z| zJeif36wC8Z{V>xX(Ku{A7*dw@N?qE!(^q)Q#B4&mJCr=D!e?lQ=(K~icK)?gx9qK3 zvMartgLjlE^(iL1$(gyW{qP7|>MG&qT27uR-xl`HofJ(mpp#WhNm8;c$Nur)=$Vz< z+XTBQPTPuQOEo-9;FuIXMX!js2o1tZjR~bhAXGEgEw3{f=Qa{FS_C46N{Z4@7I4) z=9FQ3_+!(Zr20wpB)?s%EmcaIn6hQ|wnn-|KT%4kVWE1ztK_d57K1>h4X1GNHFGLR zAzFTz8rS0&EpId7Q&CaD!OGmRZK=A|*uA^5ckD*JvugdWt2uhdt@0dyt+ppfBL9;v U7k0r%@F@f@;TZe`XTM4JUjbfXfB*mh literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/m-zstd-p1-tile-classic-contig-be.tif b/nodedb-raster/tests/fixtures/m-zstd-p1-tile-classic-contig-be.tif new file mode 100644 index 0000000000000000000000000000000000000000..38acdf22c872792830ebf90088e99fbd6daa2564 GIT binary patch literal 1513 zcmYk6c{tR07{I^3-z3Lk(K>5%tYgSg>pq4Ku?ZuoOewcHNRt>>NlCM)DI_sW&yzKS z+;%3p@~{-D34wm zUjA<5kFQy^hFOe{1t^NEjh7Fpv-&U_gpmbIXZVv;siP zNFI@nb)cbUl=H`5+zQtaA(a|LNI8a-)3KRk+kY>1v9n`s&$!&0O#PPW^L;uQ2Fzt%R7a$Th&%B*g7CqAof;(lX(Mm7Fk1uC6^x|@YcqfVs6PTUE> z`#vRll*pPsienMIm|4Bdvjs_xlZlo(iMoV8ln9i~gd3~*34)31eAM;+{CI&$VZ^y9 z&orr%x$N+KVjYxt<(zqMJWi&n`!XhHxhvcdx!e)5rH=d>AHkkMBWqlm$j3@KVbd=K z$i)F?PLANml$dS<%s=k-^(V(t;1Gt3*bWV!MSla$%8 zrt_;NS)uw2EUss&-Q`RlQ?o|scRNI{BUwhRJV!tHMjy8^z|v<=!rVQ}H;%cC;I0(s z{&IUGGQRn%&zHMpk!l_mi_=+C>6K&|45l@MetSE5t9G=3y3nq6p>bI+GUs2i(GadB z=3;ftJ72xh$Tn1LXkvyQ{{iJwCdpdd0Qa=F$ZVV(WMDRS{`9M*QNqCxa_W93e_4cT z6S|~~s(Ev)w%Idc?1Gno{66Rib!jBP%w~!2^Q$0y`1DWI#i8R1`X+-eUB$-li4L*s z5q~2CHs5>upvmwl@mSW7`?L&ez^VO~@a>^W{jmOl85@}dPBD|WWqM_~DEcEhj~xU>)IK!bSKOc?aqZ+tk6G3 z3f)D&oQ3`*&5y##a?JfA3!gNmy-=Wg#m;d<<8kTA*16fdzEK25BNuID7$b<9!H$

CK#|r(RNaz`Qm6;-LogmFy@!PlH!LblThn6gIKTjoEba#d3 zkB>TGo~O_c8$>H$CnQI0V&379Ln|#8O3JfvPGc%pR+=HO{#C-oLCb5i-E&ow5?H`< zSKTk)M*Cg0W8Zw`3AICZc5#84_I3@cAy+dTumGD<^IQX=4|GIFha>NAMy_qS$?=$1 z+45P+kve-#^Z`4wGVw96FGFif52fLg?c;TZOq_VdwDC6)cyLeZ~|dx6rYe=vI&_A)BG(3Pg<&VUmpOZ^K5Q7qb@ zp%kIMoVy&*Je;a=cn1Mm-n<z*P7HoDMIEZYwsPs0knW4}oBF`v3p{ literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/m-zstd-p1-tile-classic-separate-le.tif b/nodedb-raster/tests/fixtures/m-zstd-p1-tile-classic-separate-le.tif new file mode 100644 index 0000000000000000000000000000000000000000..1ea3915d86a5d8b41fdbec44c6676dfbddf46b5b GIT binary patch literal 1687 zcmZvb3pCVO9LMkf&KQ=NNF0wDR^E@q*#0*qyPYN03cX^ zprn8b5wkqX5WxWWB|{EiP+rb)?*srR=H<1bn3wZZ5s5E3cTgsZc?4zBH#z#>oFyX= z;L-husA%GQ(QW|!8@5Avxyu)*80gt$mPRl36xTtQU&eAT-zp)3&1OeL?^qw33N+A- zZnVKr3;>r=jibtv06-rHKpZL_QVzU;4roBifD<~e4E+|9rC@+O#RHrMo01VdhHrNK zR8XdXP4XOCGlyZdCCco{@W1>3In*mi@bu+6xwe%RIdyZmi&mj6%(Z+eHDR`k!B(`g z7bqql3+U2RthmypCEb|0BgcSU7`(^ehOTZd*LM6dA#9qj@Uu|ov|r?rYT0-nx^mOI zgIb@?;EW5O7HoA>sm?Q8w_R!@mnv=lj)P}C%5%|84b7DdOHUMdF<;Me2riGnd}~d4 z>QuzSkc(b=!l0i6^&|Os@`&TJ(m1~HHM+d~iL|RC(&Vq*8@m7CmF#S)<*C0^W?EM6 zJ{B}ysaSP|T^wE>q3heTbZKno#q*bP58MtR4K3aKhn)6JSd98bWw#tqDhw|6Z`= zh2&h+M}sBlPq#$cvd_b2wLWf~<#@J`(1-j`)o z50lzA_J`ukPB;9pL(izSH+j@`VLYz${8K}EQJ;hQ%<;r%kun0rf2kS^;xdWavBJ3F zn6(2$RjHZ7xcyhpax`{@pApX*y+qY9rmjl9ll1cg9*osC`R-I?DdlB%dX+J8KC2~i zSnpmXbsr_FJFU?_HLh#7=*D-Qha@Fagrb~{4}3(&$Zhwhjv5sEOmWxlx&QcZksJsn z2a~~*v0Gf2yU^%r;N3V{OlJ+g>=MsSb-6(A96LIY?Dt>BN40bQUM+y>>K z5>$sea2+&*n;;cBz%9@l2Eb4l1>;~UMD2|hu9io$E^8~XeK3yqKzzO45J4(5U!CRg_DKu$tw6-l&c$+<;Rg><7ep7tAsZ6b$w18`GcGBZmkanlZIL#=ZrsvPsHm7M~ zLzG&$3?om8`s_=>(5$7z6P#BfHD|)ykegM9`TYRYL;}`iVYg`@dB{mGdv=iVfzo4n zsxiyUe@ZTXk#E1oW_H*23+(qtn6I`IexmRR_U|%k)IV^0tVbj)3_UVG*1zpQI>M}P z3Hu|~n-#XERW~F*OqmR@D!IFkZ4%Xb(<7$oj8YVyHtRCEp4vWZRZP5Y69JuMN>1U$|zhr_?H*Y(4yACCelM?)>a~4rtNMj z&#ZJ5`;UpsuzTs-wD*`e*m!(msroU3B$J!q2}PF&eIMK5o13&DHB^tuxpe zQMXK2ri?5lWApqKtsJWnip+|KSKRx*PX5=!5)jCD5tPq7Wq*iJPEeX)Bo788DA~OC ztE;Qy;pMN}UTe7A-XB;$G<~tvMZI;~xdOvOn~I!%X>=e-Bmc23ANIikI10gII1S&z I`LEXf8%pP8SpWb4 literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/m-zstd-p2-strip-big-contig-be.tif b/nodedb-raster/tests/fixtures/m-zstd-p2-strip-big-contig-be.tif new file mode 100644 index 0000000000000000000000000000000000000000..5b61fb5d93460664d94b663e343226b58bb02b01 GIT binary patch literal 601 zcmZvXy-Pw-7{;IH_;G|pZekhb;7}w4g?$ha5)F-l2t@>i1PMk%MGg+aZS4ajA{s*c z1Kc7C8WQRch=zuSXl|;(!9nQU&lG$w=e`fW^E~IgC!NLw0&q&Rg+{<@Yb6meG}*<% zCKK=&U6p{QK2`L%jL#W^DCYlXFitdBzvWlbzKb~ybohj60|%XbGwCz4QP=0iEatF) zaSp@WGmdr^M|!ZBo25HTq6bT=H(3?58g-^Re9nB?4rT|s8!LAH#qKSui|M06KA$h_ z?M&{MOq95rYUe6gB_7mh+mLY_FQF!}g(rzJKBXUl%(=dMCPMW_RSyM=D|Tj+Xy68I z$VbLLxG2%$hU5@SP$AhnR3^SS&ud7o@t#G#D|N}H`rnF(8GJp|uTDj{UaRUCeb#O9 epC+L5P0T3YugD5b96)*z^^LACr#KexyL}ohF@ecfjN;lh>fo>ACeP(XNZhZC{AT}BzO~Pcwg$kIs^C4a4d1`aM)0Z=GOdmzL#-m^*din)&Md^ z@cR#rAC4iW>sb7+t_-LXV!E((b}d64RyDOck~};)wm0u=p97PxV*ag27Ci3fL+`N* zxszlCe7D5$z)|}ziIbtw0QZO^Q=%6|Yof*fi%X|g|E1%g9r_uF6~yfJt?rOyVW(De ZiIHTjo#qG_i%J}ctVLwahrVL7)?YV8XjcFL literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/m-zstd-p2-strip-classic-contig-be.tif b/nodedb-raster/tests/fixtures/m-zstd-p2-strip-classic-contig-be.tif new file mode 100644 index 0000000000000000000000000000000000000000..e0226b969b4cdac4bda6d9573c7852d51bea305c GIT binary patch literal 473 zcmZurze_@46g}tD=O-lc63ZwDhazbxq9P(Bf*J)8iU^7b5{!n592|t#+7F0`XbABS za4MppA)!5@p`k&V8XBSog9f4dp7Jp0!ujsu-gD0Vl1U5z@WUkF6($W~(&Zs4cx0wv zipJw`-ls(9YjE4ac1LgfAK1~HE17q?IR+!n+}2dt4MVzp>y14w|6sHwGv307WFg&Is~?3yfk66X=(neapp8Hj~L@H`WF>MfRuSS3~ucBg_#uB434$ zVRs58jb+?v6j0SZ_)~}0`hg0TD@7|9$jvrAOGE|ds6*c}`oc+#A1-Qk$ydar|2Gwg zSB~=xn)AG7j?cnQSd;fsg&pJLwspF%LgiA?I&N!Ri+$Dri%)7qbYCj1Otl5=Ma1Y` Lm`<=S^1k;A453IF literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/m-zstd-p2-strip-classic-separate-le.tif b/nodedb-raster/tests/fixtures/m-zstd-p2-strip-classic-separate-le.tif new file mode 100644 index 0000000000000000000000000000000000000000..aba993efd4e7d81677b118b8e452d7dd1b749d68 GIT binary patch literal 567 zcmZ{g&nts*9LL|^XM6VDK^kpno7!q+X8F+$l;pW>&rPd2I5;RnDN~dk)NrB@F;foC zt`a37DZ3~Rio^Z{a@awcyr0L@tmIp-zVGMzd4J!}yRWWC-~`|TgdKv)O@yTzsTE;^ zVme%~Q5&A28^B>{rS~B&{sehA;x~aqmD7Ub6#7)qQR%olZ|pk9TIW zR$}<1s}i^?1Cs(WFeou9u_)odRV+6VD$C`JJ6k>r{@5x&<^b~iMdCxEMHoJz|Mgt} zS}&)=^E<~Xw01446{D%OtqrG{v#<;FJ#zl;Qf#qgSGMh;efMg$^evDmaCZ7sb}K4>rxkO_XsT3gvkr8}6p7@lja~DR Iw?y3j4fz{sQUCw| literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/m-zstd-p2-tile-big-contig-be.tif b/nodedb-raster/tests/fixtures/m-zstd-p2-tile-big-contig-be.tif new file mode 100644 index 0000000000000000000000000000000000000000..1e81d7210dcf8a23c70fe1e3f1863ac78ea44882 GIT binary patch literal 853 zcmebEWzc5eV1NMuC_|8ufq@0e1~M5$Kx{@v6fq&N7!w0Chz0^C1_2-h0!C)I7~|mp zkO)wW4I#z=G7%&uiX_Ga6_a9Mg|NV?Kq?s-rLl;~AnApfr-4Px5a=!l;DJz#C5*O6 z>R|Rdq4*8r4`(c5E)YKg&4PvxV*{fr+&mVBR))I_$_xgKL2xms>!7XxQYD#bX=#}` zsd@#KP(j885C)Sfgl2}iidj`-tNz~v2IjlW3_+~IiVMsSGcNe?T)A_9r_-CcY;Zi0<+G8_2(K@ z!dak>n9Ihn=(MVKf&WrwyN(Ju$yHn;k}H^=3a-!!NCDvmTvOZ^tnv^LahytCZrS1c{=P+k~TWeWZ40!Wd~*%se0^SX_&`qZ^*HX zWwH{~b7vVCA{ak11u?KQ2{07`Be&X-GsAJe)k*7EL8uZ_Mur7UiA;wW*#CC@)dVK^ d2BwF9v;T6jF*Gb>YG7c|`O&4osFS;n0{~HSyc_@k literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/m-zstd-p2-tile-big-separate-le.tif b/nodedb-raster/tests/fixtures/m-zstd-p2-tile-big-separate-le.tif new file mode 100644 index 0000000000000000000000000000000000000000..929bb713f5b492cf2dc26055cae0ca66cb984d21 GIT binary patch literal 1103 zcmebD)MnsdU|E)xtZ=;`bs#K_MN9^ZUJWEMm>q_UObk3w_mw~eZIRT$>~%u&8!RlG zv52`a3V_24WMl)>JXfH3pfCi115mLbBs(B72(lzIEiElGCsnVYlHmeWy&2RY7ohYz zC@shg(N_hf1z8|`9Vnf~FpEKsL75?wVLF2}kk;6$|2Khw@hKBS2s0xCyMjGCLmWFp zEjz;;c81;T47b=BezG%2FtRh4192D-mjdw=Al?kbmw;H1p+ONDK9GaMc?;e#NZTL2 z{k9EiODhY*CU$Yf2JcG}yssM>?}&< zlwsMyG~FWnq0lq0m_;!?-V?ctRu=l|X|6l1dU)$jl^w5a+?y}U$et_OVsY|fvMi&5 z!iQEiP@|SdmUH<@-?P2p4!;Q0afysiJ^r#iGjUw z0XxHfc7}WG41d`fWPm1G1921(R|4@2Al?qd*MRsd5Q_lKGzMbyFoc9BI2pj3dWk|f(-2GmKLWEM)I92 zDwx*1S!Rxu!zG4EY!gfxmN2UDPB3XwVAgrC{#>I}FBjjj5B6BrnuGBJcOGcvF%*t0Xlu`|@NGt6OU*v-yxi=E*o zJA(uxJA*k8hXHXZ5KjT(%|Lt!hy@uM6p`TrIXIlR;2nds{o&hh+n~0zvM_977gucX zz9hl>x{>jY6601^rmP8pQxrBg+=6gYH?E&3Qm@6X_%DS|>Ca0UmK{vfEy5oPJ@blL z6w~89k-KPRp|76iy3?wMx9(Kg@yf=%`Lc}cxuPu=Cod+;GAby1Xf<_-aB5M75|cQ* zJ2teyh!jrMLknCarf?h%7DB zz!Z|m#Lm$5mw}-no0Va&18YFO#&pJ08~Zq*O8zo2v@jrrMx3cW)>DNSJLJ>)_SJpc=0>2&}A literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/m-zstd-p3-strip-big-contig-be.tif b/nodedb-raster/tests/fixtures/m-zstd-p3-strip-big-contig-be.tif new file mode 100644 index 0000000000000000000000000000000000000000..35c0783d37fc9c44a780411101ee4bb81495aca1 GIT binary patch literal 1134 zcmebEWzc5eV1NMuC_|8ufq@0e1~M5$Kx{@v6fq&N7!w0Chz0^C1_dAl0!C)I7~|mp zkO)wW4I#z=G7%&u$N;ksY8PV(qbQO(Ca5|w6nk00dd1=DSQrHujxsbclru^(faO3& z!!V;X7BLyPUPicEG_Z&n0{sL5JP?X;3ZpHOI;hVXgOJ3a8W@A&VxSO$g%MO1L@_Ra zaF{kgXy#HV{k};iaz3Yq zg^6?Agwtwio#~G_q`4k81Yh&ce$zHV^U#xbQu~bE9@HIqcr7p`r^~>|m<1XhcUc*x zuMwa4z(;fzU8-2&b?`3j8IP;Wo9_S_B4ujR=Hk52uZ4F`_iZFyw4VS?fAxfwMgs`HdBrn5SzRb20D zyz{vY?1Bdjxlk7{GK9AQZD(d;W>T$cIKY@TjfrvUX9flbVEC3TWoBk#%WgOTbQ*&U z1Eb;xrWXtrEG#KL`xJ{6Ch(X@&g5&DKPmP_M4{udN2ih%I{H>}#57bZZ)P$(aJY3& Qb)nG#tFna*jNJ(w00Y|{asU7T literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/m-zstd-p3-strip-big-separate-le.tif b/nodedb-raster/tests/fixtures/m-zstd-p3-strip-big-separate-le.tif new file mode 100644 index 0000000000000000000000000000000000000000..21b986cc9565c32ef253825f017af083355d1cef GIT binary patch literal 1397 zcmZuvZ){Ul6hH6X_os!ev}@@Gbo$0*twGtC1Itp!YXx))v$E-ik=VH~ix|_u#DWs< zHmewCY=TUqV;R$teL&ObhKok*8;e#UgcvZ>Y{?^0vkySZ5S=lkp10o4e&Cn9+;e~D z-19qsUVZ&)FadxSKLfVx0l*2sG26jTE{`i>9M2KpnXP~_ZxQ5jZ-ZyDPAQMe`D~nZ zUfYQ1VC2%gxLjUmp01cp2gMj?)NNpdyUf$Sj+KHU4 z82Lb69Lc(1dmhIqXhbeoKY?6i)_Z~Fim?Emxu0j}FgrI3+4!&6S;(3ng+maAIyeR| zK_glnyW88_cfYV}P2>Q0Mk{ZH;;>VMP4c+$JOi0${z2TFz49C4TosIGs5bit+Eb}i zJUtN7G8pxmQ4803?LsOMNz#bw^`rqbER=h221E5!>H|ehguGrC!g&xh|HiWyQ)5)o z>BHW8tQLgNkShWDqy6XWOZ$TtOWxkMM@c+!Y5IVDE3~ST`gGgb!9+!nyBFT?Q;9v< z{q72%rTRQq&Wb)pV1j&0jp=Z8Nj>cKDkxTBDa?S{TV)6rEs_zsA1anxG>9xVZi5)^ z|IA{NyHQ;0=8Ep1?vA?@!ErpiY}ZdGXiT@g%>7l{sZ18cDZuvr=y#&j;C(SI#qVtL zMm96?n>;k}0k|X+fe3qnA|L>N$|7D#8?`r9%hINY@G?>YDpzH9#O>!IT|R4E*!PXER>UfkTL);*hc`Ob5&fMhpO1>U5ee{#sA0#r*WA+Jeq*u!FU}a0 zWiKRgkY6sW&1tEDji(ouW&RgasHf)VTPRO~AJ!Kw`UaMPR=$S6cBo64z5*tfPwI>c zt6s9ybJW2; zzyGwNePkwIkQwWwz1Ha~?8D!K+L-7z9wq~@UKR)rlg+gL^8oB1<>4by!2#;s$_9AL zzjBMj>&G2FvV)e6@ikOzn0aMXGtUlhc&zz8IDEcrg0y5;bc@F}0@wq<8v@}G2PsV= z0=;N+Hvml)oq-|3?@3}qkhD#LkQ{K7^`-t^duDE(I}Q>bS6q&st7Su>0Ddagdr^3=BL# zEliBIP;sEE7=xf}kYT}KHpsOg_ku(OfX-F`;-?G@%;`YhQHCala*zZvF3C(wOUukj z)hnn33TtfD|C^w|WXi$7bVl2-!TS>9fx;zxtrq^5j@Fe%o=#TYE)iz%v#H;i@1V7K zLZv;=%wv1vpRd_?WU`0qt!a#6oOyE?I@vNlT_yvwk&MJ$WhM0ndsqt8Z`Ly}x|-;ZuEiQ4XeNzgVe@>~pP_yLqcAFfenq z9I9ku(J|GlyfI}K`*&gayyFmaBXa9S;`GyM^VG}pt1 z;A`I5Z`vkk9(wXlYM-&&gSsORuLY*$bQu^Kvp~b+E-S+n*8PqRz@P)B4ZDg1d=L0C z_$+`>fn7#|osXCO|2zjrVE9ejX231OF2u*q#m@GifuVtc!O@9P?WPnTJ3A{oGds({ z0>*+L-0bYkixL?b)`|16bFelC>MIB^Pg3~AXmfzQ(LtrCz$9Xq>x(o2D}xxfbqiRW zm^eQKGfh7267k~N%9o!swmVA{EKFuCZty>zt-H8Wx>)X{?!6s0KR6n_UnxJ{FpuQ| zhhQ+*p_V1bxBM2$xi?LW5$cJf%nV1EoSYc0nz6I9v#>KWv#=C3fZh1TlQnr$gK#7Z zbFi{e20IIz%VhV8EiB3k0?l2IncIV$SPCQ_mw7z-;K$g|dPuQZ?CZje2NxJpPC2IX zwLJOY(W#%a;lRd4%)sCP4BxV)%*;$|*$oGPPGgW^U{w6T^n$^Hg(byjpJK7X z1RfK~nS2fNC&j*qD0E!*=v1;oN8d`0n1*WQ%}izo4!6#!E;Kq|Rko0Uu{(hS06dl+ Ay#N3J literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/m-zstd-p3-strip-classic-separate-le.tif b/nodedb-raster/tests/fixtures/m-zstd-p3-strip-classic-separate-le.tif new file mode 100644 index 0000000000000000000000000000000000000000..be6130ab91c7fcd24575faee885f0677b81bc9c7 GIT binary patch literal 1237 zcmY*WZERCz6n^e~??*efvaO{XqtiPk+x%i<4lGL@w*}ED%<85aMqiZ)V-sZ>9m|-O@B_`F43`+OcPv_g5M#hhvLz)EvmZdp5SD0P+AqfE}_tS>-I*0xNEwaa(m{ z;sq2pbN(hP668#sEIPc%;`ziqx16oGrT#KOM0VyJ@PHL(oXkey7+H}7yg)XJ-RM9a_TeQo5~I}rS?8Aa_Vz8$Z(bVSnW=aERWVw5EfmE@ z8Ee#WGn7X9*}n)^XD|FpI9Y}9G&g2{BR-W#L{mc{BTZSanO76KF8!Do3CDR@_g16; zgEWQJGnAT4BtF#ik&xHxA-H$J@UJ>?Ix)tzB!Ad@Pp*UDGvx}wk2`<9ykIbRy72Ao zTeXoV&rI)hu0yM?=+CsB7#=AL3islJ0i8MHJ?}2^IjT6x%PJ~-2Br4 z-kZ#OS@@&2OPkD&a$3n?9AP}TX}1^v3QDHuw& zMmw|ut!;itFm<%lX95le(9EPIRG$RUP9^v5U`jTc<`Ajq{l4+lvfj%0x*^{8(Us2S zEcwoq&&TB(Ba-l`Y*5Fv*BeX2{pMo-Tbwy4$2P=ikYC+go7+)?RYz|w%l;3Q(2r#1 zo1;#_j}`e@-(Vq((xu|1-QC*s1=u`3r7I#Wd0k{s?R&aM%Eh4jPLC0_=BWw?4e}S+9BX*4m7_p@+)N>OkVGA~OOH&) zp7x$Aw)y0<{JF8kk6t`q+|Yv5`pdnpI&Am*k7~xpcJ?Kk$(?l8x_vnV5bAiICAwb3 zWB@BviP12vnbv<1zy?;@@m@r7LBCU5iMRZV*D7Lizstuq@ZvGChRY2zuZ$Y@*@G(| zYrYSAPL)ismg_6JCQ=&#>H&B|Vj^J|=W#-ypSW-ZM0NTZh%s?%oEkzrZxT{`$W=0s o_;cB@x#i_iC}LFexcV-Z#6sD|VAXKUlMtI#;mse4*D5Xl0)G=lY%P?bVBhPI2;(Av52`az`_U`K8#ZsUE$`jFflQzF~l(VGX}xMpsoXYBp5CR3N=`` zft^*7nU$qWT_7}s)Yz*3H-UluC_lruiLQ1EPc38> z!rwjUX%U=rd`@DyZQGF_2bG6Hq>yI-%*ZU1X}V|%Qr z3Ihkvvy+TGhg7GlcCVFotlj(a!S3SlzyF%t^wLBR+E^Dh=F9)H{_(mnHzi|P*~;Ah zvtGS3j`m(!-^ji5;1!j86^5rZRy^8*^Wv5(S+N${*Vc4MRqD)O|9YWUxT^N$x5qng zFaJ{)rzDm(Pj32B^KvQw6YXa zC*-ib;P+dWdV1rci#?b8-pu~Y0)ijACJ1*9OjFKF}mI^NkQgqQjKFMN^DD z99x)#J10tToznQCe1)yxb6{ygd!vnj_$@!v1NM(57y6x0sMgzdFh9+vqL!sLPVd1N zhkrA?JFMbw)k+?SI*{P~Eq}pJ_M7Xc#oqZdgL$z+U8kv9vC&+G^tK080u1asIb}rh zQ(nrvc8Fed-*FnJY%>39Ypz1)`&RN_uBGi;yJOz^JoDRckF0zWA+!FbqOD*2@)c#_ z#SZ5WWpAD0p{1|gJcIS_o$1y$|HmACaC6=4hsSU0Oyz4*oBi4CuIw|O)l!q>zU(Tj zhNgh091Jd82OUAF;y;7Keg+$lCo)fD&hQx&F&uDgU^uv+fuUj11ilY^3-}UD7#JKF z8Taoic;LWi!KcCb;kToc1-}ft47(V6)ty;-eC#%R85#E7@a1LyKaYXIiGe$Zo$Wyb zLj!|WD?1B2Gds({0?(=J%!?8;mNW-8Z((6(W?^A2assNDe3+Rf*=W&a7Up2(O^;lJ z4}LOXao&58ftiVgiJ3|D*MSC01&3zOOSvt~O%J73PByf2dfSR7cWpU^R5rooKwd@;&0 zLK%sxj19y)7}$?1Z5Ck{WZ-Q(-r{$%A-hG>Q)R+Yqo3!i8KI#xm5ITG$;k;=wxmr1 zxf<*$7RJTDfKERE43x5^%*;$8MGe9XLWkMaUz|4nmB5(7z{lF-Ab0rfVaEx~UD7k; z0vZf#&P6gWUl1!+kjTmdGh3I5A%w}vnPJ~vh6fxV-?A`@er9NB02%y2fSHNyH85@* o8V)ox@G$&;Rwq!vvWBIDrRQklL1qgDjYF2x8qPP%>q;;I085#b@c;k- literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/m-zstd-p3-tile-big-separate-le.tif b/nodedb-raster/tests/fixtures/m-zstd-p3-tile-big-separate-le.tif new file mode 100644 index 0000000000000000000000000000000000000000..b8ff2a77465fcacdf9f8ffd1e836060580c6f24c GIT binary patch literal 1679 zcmZuvYfw{16h3=zZf*#03E^Q3QG%t2BiNu63ZfM@Oi%ik3Fj_^7dtBi2!>&`olq`Qe$_J$t_M*xhr! ztgNvh0sskKZjpKw01^P=&*8jAewZ(gKqL^nUkOUjiNu&MH|Mn1DfGj9z6d|eS3{1d z7rE{@_lkN2rg%keydUPfBMA`@2&^X&+Nbk46P$J+{d=F%Wg4pEI`K)i{FPJ?G6SVcKc4H#-dlN6;5sH`5yZ*R_S0f z)d>nLk`ic78uWVQu=pBtw3%gxsbd8R|&hw0+6U_lVH56UBM?LGghLMw*Hz zuU~w%*Sa@)=k@+$GFw8<;+8B!>4k%`262h}-i=)$UtgL8Ph0CRJ8GQc9#nNHm}O^{ zUi{hKrEmLlo%yz;=t!gD;>GTyw1|ojSCjm-3$1xPTl=Uh{IlM{UC;NFqqThVHc>D> zBkJ|)u_M~8=_jew=)JC5SKjpDb>^1o$KSn{qPjwqg;@tnQo5 z^6uIT*&_LvyX}nwEykgM`k|Z!Hd6KD@u^|?Uq}PeYkD4ctyH#UB+a>Y&a`k_)A`HU zf%5aauP$;C3d|ld_5=M=DDPD0mV->3DM6&f#l|RqgKfgxvf3Dfo-xNmTf=@s(6CWxk*Ehr~@bQ9Kr_#6JPSk zs8Po&dVuqA>F7%qVWLDbg#fAT9vVrQahNm+j(8XpOg{lFq)5edFQ8I}wu1IZApI5o zM=wpSg(vt)0$mcTWISYCiGH3qf6EpV3>Em z>K`rKXM-R*v9$RwrS$GkXZF8UTz1sV72WtQs<;*rl{eNKvTduv9TUu&b5~LxJ30(8 zJBJN^YPZF#A1xSG(zMWF5%gbgNj$;PbD-+W0h3BE9X*`y!fXommdi$Arc6mK{%^(K zg}Gb%b{P(Yo#<_iU83LJT+%)Bk4Hu4R*hNNOy$m6J?fybFQlXGK+eY>8k=kGE|QxQ ztE{zCDpsEDqXy}lsrds%j%U%;+jT@MBV<*r;@2W)1Quk*5hH;O)hDZxxgHN0G`|Sr zg!=dkF&l;K9##;lg2SDPjmD&kH#wW;1z$2?W|=HxKZ62LLez`FMGz26=PP`W z5NH5To8vf*4rAYv7$AyS2!<`%81v~v6fCu*i}E@3jt>15Q>E~Xv$7W=`R}hm;7G%T jPJ#%h^#kiJ0}@=prNKv*hBaDB(@Y=$H49Rx0}8e-MKBpLg(8K3)w)0*Bmye5 zP_`hJ(x_k*%V-OhIzR$aYSk(uT@-F`G}r!Wk8!FJd{FVMrg)qhq3WUm#>l&^c6_aJN;Z_5)y ziPHK+-wElyHHBB6>)genM7^7qJ*mDxqz)<*8sGOE^k_b&jhetU7j4a^45L+lI6FQ~ zWCTJWgh0}WUb62->)FiFs0mfAZw{>+6W;lk$v|)W*GXC(>-yv-w{SE0+T26MPWxA>$iHrt+8oH8nEAbZr0G>&vYWlE-lgXBil3sL zG)3%V8!SEJ6P7phSBMo+jY+m!+obPQrwz*r3cC}0K3cx3Y&YYfRb1n)YZui0B?EDf z>Rvs)D5N~E?>U3)H@BXTKaOh)IJJ*BAQe=8uC5G@7Ai5DBjt`3zj#l(>ls#|C>t`|>UXVjA z)k$|=sqmA$yd$ArIYD3XJ}fjGU<28{gln6bGQph`B&tV+)k~J`v^p1IAs3H^yG{?v zI?w-5-}3QWBag3W#hL*31+tGAp@0J|F&F=Kp%4i?|l?s*F-8(fALv!1E4MX47 z-Rm1!RmCdss(Ti7ciA}eoYR*s(?7}&nqI&IbBMIi35{=J4g_7mj~?TWaj&xivOz1P zK-UF8smoYXtOk}O1VDgLS7(+%zzSn|FPZvBC9WeXPGi+#r7S(0N)e5~OTrVrD8<1Z;z)dr~kW^H%pa#E#dg999j zf$Vep&X%->rGtOf^X^_< MP2E(u`Z{9&0y|HZ6#xJL literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/m-zstd-p3-tile-classic-separate-le.tif b/nodedb-raster/tests/fixtures/m-zstd-p3-tile-classic-separate-le.tif new file mode 100644 index 0000000000000000000000000000000000000000..73a1787c42f5ef2fd694bc81975386dd89b473ff GIT binary patch literal 1503 zcmYjP4NwzT5Z-r}%Y^_(2tUS#z6)kP5(OP2}M=YaMp&RB#*_*xlcHg(}_PzbG zvR(xl04M>71PG#VbRuHR0!3(`@TOA5Nn2U z;)m5H5(g5ho-=5}vmSvSHHDelK3}&96T* z1HC5zzLU54(}S?+3EIC8?|W^4nL#i-yyNwHe{*NHVgQ5ds0}?Bd)wU(tH1-a4)WLm zfwa4r^tjzSwFZY-uh)2usTVulCwQ%g&(tSq&=keck}7zX>-AcUR9rIlUaM1b94CKj zgg7l&dI(u{lY(DB(#KH!-p-Iz)EvX85p&_O;4O{J8DedpJ>SoR?yqb zqe4ANo3<~R`7RZDK1Dt+FDO26!6;kN%3JUEUB;L+d#R!Uj;v{zoC1J`6p zEWei|=;A4{<~9qRvQ{h-HH3gb$_pJ?4f=RZCkR7YI{rdMD3nO903fwxNJa{7Jc@?E zu^|NovyZ_D$x1of1*C*yonSl`$bO0E=wg|*@EBW?(1rX6W-$-RY)%%2!aH>0^12e` z<_%M|y0Te!$kw}D=#_wapQ=8<@>WmfKSt=`gCKfbNyDF7<-H%z9eA^-?6_Shy!CBV zQ4OKC+*)tRcC8NgOtc#=Tubirv|3_zju`mZ?TT4HMlz!K$U=`p(tERU+$n*b1C?L& z+jM5-n33Ww%w}L;xoR{jWlCz%e=Ghj$lcn#%W^R6R992%67%kc;*OcWKPtSidhE&u zCU@4F(TA+vA+5~^b3T0E+E9IOk=j14(pfX5V&(a6W`Mn&n%7_Gc^X}{-9$BUQeM|2 ze>rkSV18yCH46ApbCNDe=o}J*;b&=_)Es{)W}}p^<0YXwINGM!XicnmLvR_M>m>tb zmZ?Gxa3p{vL_Hr|1O>5_35NwxOG_wGcVo;2L(G{~^57kK|Rpuin`2828drAEfkuJ9?Upny1p Hy|VuS|JTh{ literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/manifest.tsv b/nodedb-raster/tests/fixtures/manifest.tsv new file mode 100644 index 000000000..e7c8a2746 --- /dev/null +++ b/nodedb-raster/tests/fixtures/manifest.tsv @@ -0,0 +1,120 @@ +# id file image dtype width height bands expect mask tol +m-none-p1-strip-classic-contig-be m-none-p1-strip-classic-contig-be.tif 0 uint16 20 18 2 pattern - 0 +m-none-p1-strip-classic-separate-le m-none-p1-strip-classic-separate-le.tif 0 uint16 20 18 2 pattern - 0 +m-none-p1-strip-big-contig-be m-none-p1-strip-big-contig-be.tif 0 uint16 20 18 2 pattern - 0 +m-none-p1-strip-big-separate-le m-none-p1-strip-big-separate-le.tif 0 uint16 20 18 2 pattern - 0 +m-none-p1-tile-classic-contig-be m-none-p1-tile-classic-contig-be.tif 0 uint16 20 18 2 pattern - 0 +m-none-p1-tile-classic-separate-le m-none-p1-tile-classic-separate-le.tif 0 uint16 20 18 2 pattern - 0 +m-none-p1-tile-big-contig-be m-none-p1-tile-big-contig-be.tif 0 uint16 20 18 2 pattern - 0 +m-none-p1-tile-big-separate-le m-none-p1-tile-big-separate-le.tif 0 uint16 20 18 2 pattern - 0 +m-lzw-p1-strip-classic-contig-be m-lzw-p1-strip-classic-contig-be.tif 0 uint16 20 18 2 pattern - 0 +m-lzw-p1-strip-classic-separate-le m-lzw-p1-strip-classic-separate-le.tif 0 uint16 20 18 2 pattern - 0 +m-lzw-p1-strip-big-contig-be m-lzw-p1-strip-big-contig-be.tif 0 uint16 20 18 2 pattern - 0 +m-lzw-p1-strip-big-separate-le m-lzw-p1-strip-big-separate-le.tif 0 uint16 20 18 2 pattern - 0 +m-lzw-p1-tile-classic-contig-be m-lzw-p1-tile-classic-contig-be.tif 0 uint16 20 18 2 pattern - 0 +m-lzw-p1-tile-classic-separate-le m-lzw-p1-tile-classic-separate-le.tif 0 uint16 20 18 2 pattern - 0 +m-lzw-p1-tile-big-contig-be m-lzw-p1-tile-big-contig-be.tif 0 uint16 20 18 2 pattern - 0 +m-lzw-p1-tile-big-separate-le m-lzw-p1-tile-big-separate-le.tif 0 uint16 20 18 2 pattern - 0 +m-lzw-p2-strip-classic-contig-be m-lzw-p2-strip-classic-contig-be.tif 0 uint16 20 18 2 pattern - 0 +m-lzw-p2-strip-classic-separate-le m-lzw-p2-strip-classic-separate-le.tif 0 uint16 20 18 2 pattern - 0 +m-lzw-p2-strip-big-contig-be m-lzw-p2-strip-big-contig-be.tif 0 uint16 20 18 2 pattern - 0 +m-lzw-p2-strip-big-separate-le m-lzw-p2-strip-big-separate-le.tif 0 uint16 20 18 2 pattern - 0 +m-lzw-p2-tile-classic-contig-be m-lzw-p2-tile-classic-contig-be.tif 0 uint16 20 18 2 pattern - 0 +m-lzw-p2-tile-classic-separate-le m-lzw-p2-tile-classic-separate-le.tif 0 uint16 20 18 2 pattern - 0 +m-lzw-p2-tile-big-contig-be m-lzw-p2-tile-big-contig-be.tif 0 uint16 20 18 2 pattern - 0 +m-lzw-p2-tile-big-separate-le m-lzw-p2-tile-big-separate-le.tif 0 uint16 20 18 2 pattern - 0 +m-lzw-p3-strip-classic-contig-be m-lzw-p3-strip-classic-contig-be.tif 0 float32 20 18 2 pattern - 0 +m-lzw-p3-strip-classic-separate-le m-lzw-p3-strip-classic-separate-le.tif 0 float32 20 18 2 pattern - 0 +m-lzw-p3-strip-big-contig-be m-lzw-p3-strip-big-contig-be.tif 0 float32 20 18 2 pattern - 0 +m-lzw-p3-strip-big-separate-le m-lzw-p3-strip-big-separate-le.tif 0 float32 20 18 2 pattern - 0 +m-lzw-p3-tile-classic-contig-be m-lzw-p3-tile-classic-contig-be.tif 0 float32 20 18 2 pattern - 0 +m-lzw-p3-tile-classic-separate-le m-lzw-p3-tile-classic-separate-le.tif 0 float32 20 18 2 pattern - 0 +m-lzw-p3-tile-big-contig-be m-lzw-p3-tile-big-contig-be.tif 0 float32 20 18 2 pattern - 0 +m-lzw-p3-tile-big-separate-le m-lzw-p3-tile-big-separate-le.tif 0 float32 20 18 2 pattern - 0 +m-deflate-p1-strip-classic-contig-be m-deflate-p1-strip-classic-contig-be.tif 0 uint16 20 18 2 pattern - 0 +m-deflate-p1-strip-classic-separate-le m-deflate-p1-strip-classic-separate-le.tif 0 uint16 20 18 2 pattern - 0 +m-deflate-p1-strip-big-contig-be m-deflate-p1-strip-big-contig-be.tif 0 uint16 20 18 2 pattern - 0 +m-deflate-p1-strip-big-separate-le m-deflate-p1-strip-big-separate-le.tif 0 uint16 20 18 2 pattern - 0 +m-deflate-p1-tile-classic-contig-be m-deflate-p1-tile-classic-contig-be.tif 0 uint16 20 18 2 pattern - 0 +m-deflate-p1-tile-classic-separate-le m-deflate-p1-tile-classic-separate-le.tif 0 uint16 20 18 2 pattern - 0 +m-deflate-p1-tile-big-contig-be m-deflate-p1-tile-big-contig-be.tif 0 uint16 20 18 2 pattern - 0 +m-deflate-p1-tile-big-separate-le m-deflate-p1-tile-big-separate-le.tif 0 uint16 20 18 2 pattern - 0 +m-deflate-p2-strip-classic-contig-be m-deflate-p2-strip-classic-contig-be.tif 0 uint16 20 18 2 pattern - 0 +m-deflate-p2-strip-classic-separate-le m-deflate-p2-strip-classic-separate-le.tif 0 uint16 20 18 2 pattern - 0 +m-deflate-p2-strip-big-contig-be m-deflate-p2-strip-big-contig-be.tif 0 uint16 20 18 2 pattern - 0 +m-deflate-p2-strip-big-separate-le m-deflate-p2-strip-big-separate-le.tif 0 uint16 20 18 2 pattern - 0 +m-deflate-p2-tile-classic-contig-be m-deflate-p2-tile-classic-contig-be.tif 0 uint16 20 18 2 pattern - 0 +m-deflate-p2-tile-classic-separate-le m-deflate-p2-tile-classic-separate-le.tif 0 uint16 20 18 2 pattern - 0 +m-deflate-p2-tile-big-contig-be m-deflate-p2-tile-big-contig-be.tif 0 uint16 20 18 2 pattern - 0 +m-deflate-p2-tile-big-separate-le m-deflate-p2-tile-big-separate-le.tif 0 uint16 20 18 2 pattern - 0 +m-deflate-p3-strip-classic-contig-be m-deflate-p3-strip-classic-contig-be.tif 0 float32 20 18 2 pattern - 0 +m-deflate-p3-strip-classic-separate-le m-deflate-p3-strip-classic-separate-le.tif 0 float32 20 18 2 pattern - 0 +m-deflate-p3-strip-big-contig-be m-deflate-p3-strip-big-contig-be.tif 0 float32 20 18 2 pattern - 0 +m-deflate-p3-strip-big-separate-le m-deflate-p3-strip-big-separate-le.tif 0 float32 20 18 2 pattern - 0 +m-deflate-p3-tile-classic-contig-be m-deflate-p3-tile-classic-contig-be.tif 0 float32 20 18 2 pattern - 0 +m-deflate-p3-tile-classic-separate-le m-deflate-p3-tile-classic-separate-le.tif 0 float32 20 18 2 pattern - 0 +m-deflate-p3-tile-big-contig-be m-deflate-p3-tile-big-contig-be.tif 0 float32 20 18 2 pattern - 0 +m-deflate-p3-tile-big-separate-le m-deflate-p3-tile-big-separate-le.tif 0 float32 20 18 2 pattern - 0 +m-packbits-p1-strip-classic-contig-be m-packbits-p1-strip-classic-contig-be.tif 0 uint16 20 18 2 pattern - 0 +m-packbits-p1-strip-classic-separate-le m-packbits-p1-strip-classic-separate-le.tif 0 uint16 20 18 2 pattern - 0 +m-packbits-p1-strip-big-contig-be m-packbits-p1-strip-big-contig-be.tif 0 uint16 20 18 2 pattern - 0 +m-packbits-p1-strip-big-separate-le m-packbits-p1-strip-big-separate-le.tif 0 uint16 20 18 2 pattern - 0 +m-packbits-p1-tile-classic-contig-be m-packbits-p1-tile-classic-contig-be.tif 0 uint16 20 18 2 pattern - 0 +m-packbits-p1-tile-classic-separate-le m-packbits-p1-tile-classic-separate-le.tif 0 uint16 20 18 2 pattern - 0 +m-packbits-p1-tile-big-contig-be m-packbits-p1-tile-big-contig-be.tif 0 uint16 20 18 2 pattern - 0 +m-packbits-p1-tile-big-separate-le m-packbits-p1-tile-big-separate-le.tif 0 uint16 20 18 2 pattern - 0 +m-zstd-p1-strip-classic-contig-be m-zstd-p1-strip-classic-contig-be.tif 0 uint16 20 18 2 pattern - 0 +m-zstd-p1-strip-classic-separate-le m-zstd-p1-strip-classic-separate-le.tif 0 uint16 20 18 2 pattern - 0 +m-zstd-p1-strip-big-contig-be m-zstd-p1-strip-big-contig-be.tif 0 uint16 20 18 2 pattern - 0 +m-zstd-p1-strip-big-separate-le m-zstd-p1-strip-big-separate-le.tif 0 uint16 20 18 2 pattern - 0 +m-zstd-p1-tile-classic-contig-be m-zstd-p1-tile-classic-contig-be.tif 0 uint16 20 18 2 pattern - 0 +m-zstd-p1-tile-classic-separate-le m-zstd-p1-tile-classic-separate-le.tif 0 uint16 20 18 2 pattern - 0 +m-zstd-p1-tile-big-contig-be m-zstd-p1-tile-big-contig-be.tif 0 uint16 20 18 2 pattern - 0 +m-zstd-p1-tile-big-separate-le m-zstd-p1-tile-big-separate-le.tif 0 uint16 20 18 2 pattern - 0 +m-zstd-p2-strip-classic-contig-be m-zstd-p2-strip-classic-contig-be.tif 0 uint16 20 18 2 pattern - 0 +m-zstd-p2-strip-classic-separate-le m-zstd-p2-strip-classic-separate-le.tif 0 uint16 20 18 2 pattern - 0 +m-zstd-p2-strip-big-contig-be m-zstd-p2-strip-big-contig-be.tif 0 uint16 20 18 2 pattern - 0 +m-zstd-p2-strip-big-separate-le m-zstd-p2-strip-big-separate-le.tif 0 uint16 20 18 2 pattern - 0 +m-zstd-p2-tile-classic-contig-be m-zstd-p2-tile-classic-contig-be.tif 0 uint16 20 18 2 pattern - 0 +m-zstd-p2-tile-classic-separate-le m-zstd-p2-tile-classic-separate-le.tif 0 uint16 20 18 2 pattern - 0 +m-zstd-p2-tile-big-contig-be m-zstd-p2-tile-big-contig-be.tif 0 uint16 20 18 2 pattern - 0 +m-zstd-p2-tile-big-separate-le m-zstd-p2-tile-big-separate-le.tif 0 uint16 20 18 2 pattern - 0 +m-zstd-p3-strip-classic-contig-be m-zstd-p3-strip-classic-contig-be.tif 0 float32 20 18 2 pattern - 0 +m-zstd-p3-strip-classic-separate-le m-zstd-p3-strip-classic-separate-le.tif 0 float32 20 18 2 pattern - 0 +m-zstd-p3-strip-big-contig-be m-zstd-p3-strip-big-contig-be.tif 0 float32 20 18 2 pattern - 0 +m-zstd-p3-strip-big-separate-le m-zstd-p3-strip-big-separate-le.tif 0 float32 20 18 2 pattern - 0 +m-zstd-p3-tile-classic-contig-be m-zstd-p3-tile-classic-contig-be.tif 0 float32 20 18 2 pattern - 0 +m-zstd-p3-tile-classic-separate-le m-zstd-p3-tile-classic-separate-le.tif 0 float32 20 18 2 pattern - 0 +m-zstd-p3-tile-big-contig-be m-zstd-p3-tile-big-contig-be.tif 0 float32 20 18 2 pattern - 0 +m-zstd-p3-tile-big-separate-le m-zstd-p3-tile-big-separate-le.tif 0 float32 20 18 2 pattern - 0 +t-uint8 t-uint8.tif 0 uint8 20 18 1 pattern - 0 +t-int8 t-int8.tif 0 int8 20 18 1 pattern - 0 +t-int16 t-int16.tif 0 int16 20 18 1 pattern - 0 +t-uint32 t-uint32.tif 0 uint32 20 18 1 pattern - 0 +t-int32 t-int32.tif 0 int32 20 18 1 pattern - 0 +t-float64 t-float64.tif 0 float64 20 18 1 pattern - 0 +l-lerc-pixel-tile l-lerc-pixel-tile.tif 0 uint16 20 18 2 pattern - 0 +l-lerc-pixel-strip l-lerc-pixel-strip.tif 0 uint16 20 18 2 pattern - 0 +l-lerc-band-tile l-lerc-band-tile.tif 0 uint16 20 18 2 pattern - 0 +l-lerc-band-strip l-lerc-band-strip.tif 0 uint16 20 18 2 pattern - 0 +l-lerc-mask l-lerc-mask.tif 0 float32 20 18 1 l-lerc-mask.raw.z l-lerc-mask.mask.z 0 +l-lerc_deflate-pixel-tile l-lerc_deflate-pixel-tile.tif 0 uint16 20 18 2 pattern - 0 +l-lerc_deflate-pixel-strip l-lerc_deflate-pixel-strip.tif 0 uint16 20 18 2 pattern - 0 +l-lerc_deflate-band-tile l-lerc_deflate-band-tile.tif 0 uint16 20 18 2 pattern - 0 +l-lerc_deflate-band-strip l-lerc_deflate-band-strip.tif 0 uint16 20 18 2 pattern - 0 +l-lerc_deflate-mask l-lerc_deflate-mask.tif 0 float32 20 18 1 l-lerc_deflate-mask.raw.z l-lerc_deflate-mask.mask.z 0 +l-lerc_zstd-pixel-tile l-lerc_zstd-pixel-tile.tif 0 uint16 20 18 2 pattern - 0 +l-lerc_zstd-pixel-strip l-lerc_zstd-pixel-strip.tif 0 uint16 20 18 2 pattern - 0 +l-lerc_zstd-band-tile l-lerc_zstd-band-tile.tif 0 uint16 20 18 2 pattern - 0 +l-lerc_zstd-band-strip l-lerc_zstd-band-strip.tif 0 uint16 20 18 2 pattern - 0 +l-lerc_zstd-mask l-lerc_zstd-mask.tif 0 float32 20 18 1 l-lerc_zstd-mask.raw.z l-lerc_zstd-mask.mask.z 0 +l-lerc-bigtiff l-lerc-bigtiff.tif 0 int16 20 18 1 pattern - 0 +j-ycbcr-tile j-ycbcr-tile.tif 0 uint8 40 24 3 j-ycbcr-tile.raw.z - 3 +j-gray-strip j-gray-strip.tif 0 uint8 40 24 1 j-gray-strip.raw.z - 3 +j-rgb-tile j-rgb-tile.tif 0 uint8 40 24 3 j-rgb-tile.raw.z - 3 +cog-mask-0 cog-mask.tif 0 uint16 64 48 1 pattern cog-mask-0.mask.z 0 +cog-mask-1 cog-mask.tif 1 uint16 32 24 1 cog-mask-1.raw.z cog-mask-1.mask.z 0 +cog-mask-2 cog-mask.tif 2 uint16 16 12 1 cog-mask-2.raw.z cog-mask-2.mask.z 0 +landsat-c2 landsat-c2.tif 0 uint16 300 260 1 landsat-c2.raw.z - 0 +landsat-c2-1 landsat-c2.tif 1 uint16 150 130 1 landsat-c2-1.raw.z - 0 +gdal-metadata gdal-metadata.tif 0 int16 8 6 3 pattern - 0 diff --git a/nodedb-raster/tests/fixtures/t-float64.tif b/nodedb-raster/tests/fixtures/t-float64.tif new file mode 100644 index 0000000000000000000000000000000000000000..bf05aab5688b269b49e063123c14915ecbb184ca GIT binary patch literal 1183 zcmebD)MDUZU|`^9U|?inU<9&6fS3`=76P)F7@5K14nQ_Dlnv6s24#cvh$6A27+JyU z4*|ucq3lyYwg!?KLq;YB9-!DIAa;V-eQBe7k8Y!;x{Js@@k>IJdi0dX+Y9A+RJ z1VHv9qu!Iay;SV5A0~#R1bcZ32!4j+wLR8E#y|latC#&jOg;oWlJ@?PB*4Q84p0c+k@=)v{orTj6@a^%Qp<3c$v5>KG>EG9W`>bkz z{Xe^s^Z&eGO6~vo#h&v%jy!Dis`}o16CZcp&y98Q`&Zw;mjB9pPrcfDrFxsI^1H3C zd_Hdbx3A9oy!`G@v)<1;;r&VZC*!^1^zu#Cw`G4kZrk&2ciHz%do=8iMDFR{{_*_# zw#9K7^KRU^J@*Zt_NPgE8a`Vz2U|2=|C9VLdgDXcJ^iisV-J*lm+^b7a__^o^Su96 z?wg$3YWH*F9v%Cs*9#B-HvDh;zU6zcMf2wy@8TwfKiT^2e~yIt*W0|G`|AAb7tD9v zeReS7ZD$#*ZlODKQlfBaXlZGP|1 zHHoF~es{M$EiNx(-)&sK$@-S;8@)>9{DaaJ*=PP-<=!{_UHs1ax98s7asJn3|8e@B ziQ6CW?lDaRa`qPfzxLPq`}?~e(_vB><)5$3_^JKxNPJzv|BHXszt@)k5v_lu4>s`D zU9i;sr|UO-j_glf^Fz4)q;w_sy#vw7C#?^iC~rMFz0H{?UYc*iu_N)H#A=**YM9q- zJU@Hm>F~to()N8D|KypM-Tu#LUBh`i;!~f&r`W`U=0`pp&Retb|K&WLA8isp*dmhW zn{VI!Q|rwKRhi_Ur}KKIm&G63|2+H0_lh}tFP!E)-CytP<-w_WP)O$>pAYA>2}(K+U*?Gw{XeU?IzR7BexRz^|EWtSKHcqbIgU@= zdGhoxmyNYfTV>7@%RDJLHS|d5stVK0H!pvz>$`j)_tU&-efhU`$lh6Z#Y)!xMfYR2 z9nNBF>L|5>@_TD`59 z^Y)xKZ-aq=>&1x;EGh~Mg>HOkUm~QnP-sE`r^kvGFCbD#T*v(^@7%kmogE(LJI{CU z{#_$h()}x8|0?rS%h=XfyvsWFFhH2Ano+y&O`6ON?;9J%H!e)yzQFot@86^OTXaru zS~LGEQ?};rDf)%>J?OiC6)jcS}-#(v;pxF#wiSb4Dw)o5TarZ>p{+pK1POyhu_0p zIs_D*Qu=yM&X5tFshn)2cHHIJi5Wd74JTR}sU;Wrz@$!?%-nWCY^!UoZ*FdC?$^EZ zmK&|$%WD3$Kr@4v$Li)?JHC<`i)G%Yq~8$Q!Y=zJ$^WmHAiZa;Eipc0a{KhX59|1AUuoa}d2;pY`P)jt^0lV{y&f7qjR=1$G-5iKecAVDXG^vU*Q2nqiY&HAlZH= zU*eu|P4bV^2M(P$t8ql*>WS)U>k;eu z)|1z>uZL%Gf(J8$b{d~a#T?_*Er}&*Uy@P|Fgi&rmYghMTzIO#X(2DesSKt@0HjA( A{{R30 literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/t-int32.tif b/nodedb-raster/tests/fixtures/t-int32.tif new file mode 100644 index 0000000000000000000000000000000000000000..dfa0e91d64312d9c0c9d462ff09599633da7b2e6 GIT binary patch literal 1049 zcmebD)MDUZU|`^9U|?inU<9&6fS3`=76P)F7@5K13P3h9lnv6s24#cvh$6A27+JyU z4*|ucq3lyYwg!?KLq;YB9-!DIAa;V-eQBe7k8Y!;x{Js@@k>IJdi0dX+Y93~(e z1VHv9qu*5c?!g6K?hUY-MjqxBu0fP}(AB3owBYH72gM&eWEy1$* z*%?X8VxMDAW}J|boNQ>RX5@Y>#iy^+CuNpTbVm2BMcbBb+qP-jqO}>^xr(Pvy<16Y3p7U5K zoI|`Adkq4#BHcgFv^H|`xrLU0Yl8KIDJ9E#x1+M+D(1{Rt!S*kAmVoM?v~jZZpssJ;`^&l?LFrI!Oy&;3L+%bwp6S250{aRHo2);VA5?xYs<`@N z`@!E2l0U5c7~lN<;Q0ip|F55m{L_|MasU6rLlX~eJk)t?zTO-HoQ(j()o#Xq{|-L*DF=+6|uQ1nr#Ww;Pzt4(Z+C-JQHXyn6So(+LI@ zv)J{gl{f?KJH3#RL4k+aQADc!@W%gHz0OZ(GWz(QpMPkMLCZ^{w-+bOH1bZX{}bdj KkK3z3>KFiO6pB>< literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/t-int8.tif b/nodedb-raster/tests/fixtures/t-int8.tif new file mode 100644 index 0000000000000000000000000000000000000000..3a1dea0af2539f22a6c97ec0ea292c8f56bd4463 GIT binary patch literal 665 zcmebD)MDUZU|`^9U|?inU<9&6fS3`=76P)F7@5K1AXUsrY&Iwxq(>BqEyc(RR(}X6 zE)8X$04MP@#E`u0YAB3owV_o&^)7p0Zd-cChAJSD> zxnSM*CtsJ(=Xq1J$mmp5o7lI!eP?2tgg#~MQoH6ikMUvQN=vUC{x zRrhY6+`q()nm1ji^1iJNey$TYb;7*xsLOXu3y=Lt&EEdTu4L1`wdJSYxN4i{+%Ff& zxx8m-w%L2DibsF8?iQH+P0q6R<+JAJ@Av&{yixM}qFq~di$5=7Pcx&OeBnKwNi=H!2{CTwM7+=c2B{R9F zDStM9ccY$XX8prodzYX1^Zow6O$wiXx};8-_!H#o6E*W6tZ+%GD+^3zHrk*4sORU2 zKU1Z*y?1^#-=hAlp3(oGyVuuG_?fdu>C%ZBhAjsafi~UV_58!iZu58c)xV#b3omcj zacAo7{-eQBe7k8Y!;x{B_MVM>IJbM0Wk=G>_o;T znQ3WhnK`L?1(ggRfO0FC7#M<>85nkeFyktQGzNW;HXx{&BYH72;{>Aw+lT)$g03Qg z(b1977sIx+Zu6Pe>66meb8?1^@J!`oBeml$&rZzfIcYf2(#ZYTlNl#ufTBe{DN4dK z|9pFRJ^KCUZ*`SUO$skO!mEVbRQ^gFNN6)^GW1E%GM?$qa`A|eztYK`B$>%G&*g}1 zXqmbuRoHXM1TU$VDMr&>b<#~=-#o)J1mh>GbO!FqY zGq#EZ`e?da9<5wg(7fB`iRAj94xFh|f*n;u?>w^euQ(ZP_pvwr&*EASff%#ZD;G|) zdR2K|yzWE0z;@Y}UjBzVp0uuMbeom2thH>@xsv7Pf-b_|Hk~S;tut!nSY%%~@jJbA zj{jxcp%Ubw+FN4u($ymHh=4oOVTqFwY7-sBZa$gdZO1HmsKCv5p5~bZKJG|2IaZM% z7sI7KDN>tfoKFhd(8imSDy}*u(2sR$$i^w5Ym$v`-#pCNS1_q~t!J?4dF%Q$^^kiBP9n zHiS-->U1&gby+miO)1dk@ZRO~b>AfL|Gp*rkR{#X`a3T39f$Xp`Bxm2zPI)FA^v{> z7jravS~eQo`?&m&&kXL*vh|v4TDESu_tE@N+6}(lvi}ui+8!6&`lx>>bqC*e+5d_* zZO=d4`e=VB{RiKE+4=<%I&Iw-FXb$np|h;}X5an*9kJ6#vyT5~Z+fg?RWax7!G&2) zj0_GJKj%eA>+p!NaW}UfJaj-of~7e@fTziE!_6Es``c-k&u`dMypDN$D#?DxoM-s~|j;k)d_@YzJI*Rii(3aDR wVhjQdtOskWgMBZj_+GwqT~g!Jd^^__PQtrlqz{Qq+aO=(@~4>L|1Rd20EoVHI{*Lx literal 0 HcmV?d00001 diff --git a/nodedb-raster/tests/fixtures/t-uint8.tif b/nodedb-raster/tests/fixtures/t-uint8.tif new file mode 100644 index 0000000000000000000000000000000000000000..62190ada15881f9e854645953f94d4d1e7ed7309 GIT binary patch literal 648 zcmebD)MDUZU|`^5U|?inU<9&6fS3`=76P)F7@5K1AXUsrY&Iwxq(>BqEyc(RR=*7> zE)8Yx1F|)c)EF`{G4KGzjsdX~)Gh%a+Zl=N0%WrQ#V!G{D^M?p{RoIb0AwdJF3C(w zOUukj)hnoE_yCl<3zT$cVqho&;)S1M$hB7tJ=66ny9I^*k38 zaNT%o;Ww_H4*%1-EAw7$DgVsd>32#;V}179nKs!EyLMce^JvwvzJHm;)7RYF^7+8W zThrco8jI`1Z!bOnFjzeQPQ6+B$5p-7-{h^UpMG0BU4GyHd(9`jD;)=f;=n%+~W)^lX~8koAZfXQgSZaJKsShJp^?XJzZ`4hD8g{!A5^ zeJCk-Q@57%t>4R>a~&_n$^$6@BaPd zt9pm;^gO$YfA3z)r|CI%_%c`?X<)3FqrGNDezF6T@UrvKU1#F==e`$A`L}a*xt#Ln z`*lChww!q`I4#KKS+;bfmBQz}Ov;ZY{+!ABwDiOukZ&jcJbgX)Zu1%YvTvtyQ~q5x zpI@)|IdVs<%EX_iqT0kx)PS6wV#mN2rvS9+_m9W+((606Oj_2sbM9*Yf+-C<=B(=5 tICXJ*eMF8|S)l4+19k?{{hSLc=6Ihup$~Kv@7vm7NjG4$Z)j$|2>|+W { + bytes: &'a [u8], + reads: Vec, +} + +impl RangeRead for Counting<'_> { + fn read_range(&mut self, range: ByteRange) -> Result>, RasterError> { + self.reads.push(range); + let len = self.bytes.len() as u64; + let (start, end) = (range.start.min(len) as usize, range.end.min(len) as usize); + Ok(Some(self.bytes[start..end.max(start)].to_vec())) + } +} + +#[test] +fn a_cog_opens_in_one_round_from_its_first_64_kib() { + for file in ["cog-mask.tif", "landsat-c2.tif"] { + let bytes = fixture_bytes(file); + let mut source = Counting { + bytes: &bytes, + reads: Vec::new(), + }; + for image in [0, 1] { + source.reads.clear(); + let opener = TiffOpener::new().with_image(image); + open_with(&opener, &mut source, LIMITS).expect("opens"); + assert_eq!( + source.reads, + vec![ByteRange::new(ObjectId(0), 0, 64 * 1024)], + "{file} image {image}" + ); + } + } +} + +#[test] +fn overviews_scale_the_geotransform_and_keep_the_crs() { + let full = open_fixture(&fixture("cog-mask-0")).0; + let half = open_fixture(&fixture("cog-mask-1")).0; + let quarter = open_fixture(&fixture("cog-mask-2")).0; + assert_eq!( + full.header().transform.map(|gt| gt.0), + Some([600_000.0, 30.0, 0.0, 450_000.0, 0.0, -30.0]) + ); + assert_eq!( + half.header().transform.map(|gt| gt.0), + Some([600_000.0, 60.0, 0.0, 450_000.0, 0.0, -60.0]) + ); + assert_eq!( + quarter.header().transform.map(|gt| gt.0), + Some([600_000.0, 120.0, 0.0, 450_000.0, 0.0, -120.0]) + ); + for reader in [&full, &half, &quarter] { + assert_eq!(reader.header().crs, CrsRef::Epsg(32648)); + assert!(reader.has_mask()); + } +} + +#[test] +fn an_image_past_the_last_overview_is_a_typed_error() { + let bytes = fixture_bytes("cog-mask.tif"); + let opener = TiffOpener::new().with_image(3); + assert!(matches!( + nodedb_raster::open_all(&opener, &bytes, LIMITS), + Err(RasterError::ImageOutOfRange { index: 3, count: 3 }) + )); +} + +#[test] +fn mask_ranges_follow_the_data_ranges() { + let (reader, _) = open_fixture(&fixture("cog-mask-0")); + let ranges = reader.block_ranges(0).expect("ranges"); + // One data tile and the one mask tile of the same 16x16 block. + assert_eq!(ranges.len(), 2); + assert!( + ranges + .iter() + .all(|r| r.object == ObjectId(0) && !r.is_empty()) + ); + assert_ne!(ranges[0], ranges[1]); +} + +#[test] +fn the_landsat_band_reads_its_georeference_and_metadata() { + let (reader, _) = open_fixture(&fixture("landsat-c2")); + let header = reader.header(); + assert_eq!( + header.transform.map(|gt| gt.0), + Some([399_960.0, 30.0, 0.0, 5_000_040.0, 0.0, -30.0]) + ); + assert_eq!( + header + .crs + .resolve(&CrsContext::builtin_only()) + .expect("CRS maps"), + Some(nodedb_types::geometry::Srid::new(32648).expect("srid")) + ); + let band = &header.bands[0]; + assert_eq!(band.nodata, Some(0.0)); + assert_eq!(band.scale, Some(2.75e-05)); + assert_eq!(band.offset, Some(-0.2)); + assert_eq!(band.name.as_deref(), Some("SR_B4")); + assert_eq!(reader.layout().block_width, 256); + assert_eq!(reader.block_count(), 4); + assert!(!reader.has_mask()); +} + +#[test] +fn per_band_gdal_metadata_reaches_the_band_list() { + let (reader, _) = open_fixture(&fixture("gdal-metadata")); + let bands = &reader.header().bands; + assert_eq!(bands.len(), 3); + assert_eq!(bands[0].name.as_deref(), Some("temperature")); + assert_eq!(bands[0].scale, Some(0.01)); + assert_eq!(bands[0].offset, Some(273.15)); + assert_eq!(bands[0].units.as_deref(), Some("K")); + assert_eq!( + bands[0].nodata, + Some(-1.0), + "GDAL_NODATA applies to every band" + ); + assert_eq!(bands[1].nodata, Some(-9999.0), "a band item overrides it"); + assert_eq!(bands[1].scale, None); + assert_eq!(bands[2].valid_range, Some((0.0, 1000.0))); + assert_eq!(bands[2].offset, Some(-10.0)); + assert_eq!(bands[2].units.as_deref(), Some("m")); +} diff --git a/nodedb-raster/tests/geotiff/files.rs b/nodedb-raster/tests/geotiff/files.rs new file mode 100644 index 000000000..0e9e33787 --- /dev/null +++ b/nodedb-raster/tests/geotiff/files.rs @@ -0,0 +1,222 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Every generated fixture of `tests/fixtures/manifest.tsv`: codec x +//! predictor x strips/tiles x classic/BigTIFF x chunky/planar, sample +//! types, LERC with masks, JPEG, COG overviews and masks, a Landsat-shaped +//! COG. Expected samples are the shared pattern, or GDAL's decode. + +use std::io::Read; +use std::path::PathBuf; + +use flate2::read::ZlibDecoder; +use nodedb_raster::{ + RasterBlock, RasterReader, SampleType, TiffOpener, TiffReader, open_all, read_block_all, +}; + +use crate::fixtures::{LIMITS, expected, planes}; + +/// One manifest row. +pub struct Fixture { + pub id: String, + pub file: String, + pub image: u32, + pub dtype: String, + pub width: u32, + pub height: u32, + pub bands: u16, + pub expect: String, + pub mask: String, + pub tol: f64, +} + +pub fn fixture_path(name: &str) -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("tests") + .join("fixtures") + .join(name) +} + +pub fn fixture_bytes(name: &str) -> Vec { + std::fs::read(fixture_path(name)).unwrap_or_else(|e| panic!("fixture {name}: {e}")) +} + +pub fn manifest() -> Vec { + let text = std::fs::read_to_string(fixture_path("manifest.tsv")).expect("manifest"); + text.lines() + .filter(|line| !line.starts_with('#') && !line.is_empty()) + .map(|line| { + let f: Vec<&str> = line.split('\t').collect(); + assert_eq!(f.len(), 10, "manifest row {line}"); + Fixture { + id: f[0].to_owned(), + file: f[1].to_owned(), + image: f[2].parse().expect("image"), + dtype: f[3].to_owned(), + width: f[4].parse().expect("width"), + height: f[5].parse().expect("height"), + bands: f[6].parse().expect("bands"), + expect: f[7].to_owned(), + mask: f[8].to_owned(), + tol: f[9].parse().expect("tol"), + } + }) + .collect() +} + +pub fn fixture(id: &str) -> Fixture { + manifest() + .into_iter() + .find(|f| f.id == id) + .unwrap_or_else(|| panic!("no fixture {id}")) +} + +fn inflate(name: &str) -> Vec { + let mut out = Vec::new(); + ZlibDecoder::new(fixture_bytes(name).as_slice()) + .read_to_end(&mut out) + .expect("inflates"); + out +} + +fn sample_type(dtype: &str) -> SampleType { + match dtype { + "uint8" => SampleType::U8, + "int8" => SampleType::I8, + "uint16" => SampleType::U16, + "int16" => SampleType::I16, + "uint32" => SampleType::U32, + "int32" => SampleType::I32, + "float32" => SampleType::F32, + "float64" => SampleType::F64, + other => panic!("dtype {other}"), + } +} + +/// The expected samples, one row-major `f64` plane per band. +fn expected_planes(f: &Fixture) -> Vec> { + let (w, h, b) = (f.width, f.height, u32::from(f.bands)); + if f.expect == "pattern" { + return match sample_type(&f.dtype) { + SampleType::U8 => expected(&planes::(w, h, b)), + SampleType::I8 => expected(&planes::(w, h, b)), + SampleType::U16 => expected(&planes::(w, h, b)), + SampleType::I16 => expected(&planes::(w, h, b)), + SampleType::U32 => expected(&planes::(w, h, b)), + SampleType::I32 => expected(&planes::(w, h, b)), + SampleType::F32 => expected(&planes::(w, h, b)), + SampleType::F64 => expected(&planes::(w, h, b)), + }; + } + let raw = inflate(&f.expect); + let st = sample_type(&f.dtype); + let values: Vec = raw + .chunks_exact(st.byte_len()) + .map(|c| match st { + SampleType::U8 => f64::from(c[0]), + SampleType::I8 => f64::from(c[0] as i8), + SampleType::U16 => f64::from(u16::from_le_bytes([c[0], c[1]])), + SampleType::I16 => f64::from(i16::from_le_bytes([c[0], c[1]])), + SampleType::U32 => f64::from(u32::from_le_bytes([c[0], c[1], c[2], c[3]])), + SampleType::I32 => f64::from(i32::from_le_bytes([c[0], c[1], c[2], c[3]])), + SampleType::F32 => f64::from(f32::from_le_bytes([c[0], c[1], c[2], c[3]])), + SampleType::F64 => f64::from_le_bytes(c.try_into().expect("8 bytes")), + }) + .collect(); + let pixels = (w * h) as usize; + assert_eq!(values.len(), pixels * usize::from(f.bands), "{}", f.id); + values.chunks(pixels).map(<[f64]>::to_vec).collect() +} + +pub fn open_fixture(f: &Fixture) -> (TiffReader, Vec) { + let bytes = fixture_bytes(&f.file); + let opener = TiffOpener::new().with_image(f.image); + let reader = open_all(&opener, &bytes, LIMITS).unwrap_or_else(|e| panic!("{}: {e}", f.id)); + (reader, bytes) +} + +/// Every pixel of every band, and the mask (all valid when absent). +pub fn decode_image(reader: &TiffReader, bytes: &[u8]) -> (Vec>, Vec) { + let header = reader.header(); + let width = header.width as usize; + let pixels = width * header.height as usize; + let mut planes = vec![vec![f64::NAN; pixels]; header.band_count()]; + let mut mask = vec![true; pixels]; + for index in 0..reader.block_count() { + let block: RasterBlock = read_block_all(reader, index, bytes).expect("block decodes"); + let w = &block.window; + for r in 0..w.height as usize { + for c in 0..w.width as usize { + let at = (w.row_off as usize + r) * width + w.col_off as usize + c; + let local = r * w.width as usize + c; + mask[at] = block.any_valid(local); + for (band, samples) in block.bands.iter().enumerate() { + planes[band][at] = samples.value_f64(local).expect("in window"); + } + } + } + } + (planes, mask) +} + +fn check(f: &Fixture) { + let (reader, bytes) = open_fixture(f); + let header = reader.header(); + assert_eq!( + (header.width, header.height), + (f.width, f.height), + "{}", + f.id + ); + assert_eq!(header.band_count(), usize::from(f.bands), "{}", f.id); + let st = sample_type(&f.dtype); + assert!(header.bands.iter().all(|b| b.sample_type == st), "{}", f.id); + let (got, mask) = decode_image(&reader, &bytes); + let want_mask: Vec = if f.mask == "-" { + vec![true; mask.len()] + } else { + inflate(&f.mask).iter().map(|v| *v != 0).collect() + }; + assert_eq!(mask, want_mask, "{}: mask", f.id); + let want = expected_planes(f); + for (band, (g, w)) in got.iter().zip(&want).enumerate() { + for (i, (a, b)) in g.iter().zip(w).enumerate() { + if !want_mask[i] { + continue; + } + let close = (a - b).abs() <= f.tol || (a.is_nan() && b.is_nan()); + assert!(close, "{}: band {band} pixel {i}: got {a}, want {b}", f.id); + } + } +} + +#[test] +fn every_manifest_fixture_decodes_to_its_expected_samples() { + let fixtures = manifest(); + assert!(fixtures.len() > 100, "the manifest lists the codec matrix"); + for f in &fixtures { + check(f); + } +} + +#[test] +fn the_matrix_covers_both_byte_orders_and_bigtiff() { + let ids: Vec = manifest().into_iter().map(|f| f.id).collect(); + for codec in ["none", "lzw", "deflate", "packbits", "zstd"] { + for part in [ + "-be", + "-le", + "-big-", + "-classic-", + "-separate-", + "-contig-", + "-tile-", + "-strip-", + ] { + assert!( + ids.iter() + .any(|id| id.starts_with(&format!("m-{codec}-")) && id.contains(part)), + "{codec} lacks {part}" + ); + } + } +} diff --git a/nodedb-raster/tests/geotiff/fixtures.rs b/nodedb-raster/tests/geotiff/fixtures.rs index 236d37d92..25097a8df 100644 --- a/nodedb-raster/tests/geotiff/fixtures.rs +++ b/nodedb-raster/tests/geotiff/fixtures.rs @@ -6,9 +6,9 @@ //! floating-point-predictor fixtures write each chunk by hand through the //! `DirectoryEncoder`, since `ImageEncoder` writes chunky strips only. -use std::io::{Cursor, Read, Seek, Write}; +use std::io::{Cursor, Seek, Write}; -use nodedb_raster::{GeoTiffReader, RasterError, RasterLimits}; +use nodedb_raster::RasterLimits; use tiff::encoder::colortype::ColorType; use tiff::encoder::compression::{CompressionAlgorithm, Deflate, Lzw}; use tiff::encoder::{Compression, DirectoryEncoder, Predictor, TiffEncoder, TiffKind, TiffValue}; @@ -112,6 +112,8 @@ macro_rules! float_sample { } int_sample!(u8, 0); +int_sample!(i8, 60); +int_sample!(i32, 70_000); int_sample!(u16, 0); int_sample!(i16, 300); int_sample!(u32, 0); @@ -423,44 +425,3 @@ pub fn encode_chunks( .map(|chunk| chunk_bytes(chunk, chunk_w as usize * spp, spp, predictor, compression)) .collect() } - -/// Open an owned copy of `bytes` under [`LIMITS`], so callers can pass a temporary. -pub fn open(bytes: &[u8]) -> Result>>, RasterError> { - GeoTiffReader::open(Cursor::new(bytes.to_vec()), LIMITS) -} - -/// Decode every block and reassemble one row-major `f64` plane per band. -pub fn read_all(reader: &mut GeoTiffReader) -> Vec> { - let width = reader.width() as usize; - let pixels = width * reader.height() as usize; - let mut out = vec![vec![f64::NAN; pixels]; usize::from(reader.band_count())]; - for index in 0..reader.block_count() { - let block = reader.read_block(index).expect("block decodes"); - let window = block.window; - assert_eq!(block.bands.len(), out.len(), "band count of block {index}"); - for (band, samples) in block.bands.iter().enumerate() { - assert_eq!(samples.len(), window.pixels(), "samples in block {index}"); - assert_eq!(samples.sample_type(), reader.sample_type()); - for r in 0..window.height as usize { - for c in 0..window.width as usize { - let row = window.row_off as usize + r; - let col = window.col_off as usize + c; - out[band][row * width + col] = samples - .value_f64(r * window.width as usize + c) - .expect("sample in window"); - } - } - } - } - out -} - -/// Open `bytes` with `limits` and decode every block; the first error. -pub fn decode_everything(bytes: &[u8], limits: RasterLimits) -> Result<(), RasterError> { - let mut reader = GeoTiffReader::open(Cursor::new(bytes), limits)?; - reader.srid()?; - for index in 0..reader.block_count() { - reader.read_block(index)?; - } - Ok(()) -} diff --git a/nodedb-raster/tests/geotiff/georef.rs b/nodedb-raster/tests/geotiff/georef.rs index e7af7e33c..ee69b4efa 100644 --- a/nodedb-raster/tests/geotiff/georef.rs +++ b/nodedb-raster/tests/geotiff/georef.rs @@ -2,13 +2,17 @@ //! Georeference from the GeoTIFF tags: geotransform, CRS, and nodata. +use nodedb_crs::CrsContext; use nodedb_raster::RasterError; -use nodedb_raster::geokeys::{GEOGRAPHIC_TYPE, GT_MODEL_TYPE, GT_RASTER_TYPE, PROJECTED_CS_TYPE}; +use nodedb_raster::tiff::geokeys::{ + GEOGRAPHIC_TYPE, GT_MODEL_TYPE, GT_RASTER_TYPE, PROJECTED_CS_TYPE, +}; use nodedb_types::geometry::Srid; use tiff::encoder::colortype::Gray16; use tiff::tags::Tag; -use crate::fixtures::{Extra, StripSpec, encode_strips, geokeys, open, planes, strip_spec}; +use crate::fixtures::{Extra, StripSpec, encode_strips, geokeys, planes, strip_spec}; +use crate::image::open; const W: u32 = 6; const H: u32 = 4; @@ -45,7 +49,10 @@ fn tiepoint_and_scale_give_a_north_up_transform() { let reader = open(&with_tags(&utm_tiepoint(1))).expect("opens"); let gt = reader.transform().expect("georeferenced"); assert_eq!(gt.0, [600_000.0, 30.0, 0.0, 450_000.0, 0.0, -30.0]); - assert_eq!(reader.srid().expect("CRS maps"), Some(Srid::Utm47N)); + assert_eq!( + reader.srid(&CrsContext::builtin_only()).expect("CRS maps"), + Some(Srid::new(32647).unwrap()) + ); } #[test] @@ -73,7 +80,10 @@ fn model_transformation_gives_a_rotated_transform() { let reader = open(&with_tags(&extras)).expect("opens"); let gt = reader.transform().expect("georeferenced"); assert_eq!(gt.0, [101.0, 0.5, 0.1, 3.5, -0.1, -0.5]); - assert_eq!(reader.srid().expect("CRS maps"), Some(Srid::Wgs84)); + assert_eq!( + reader.srid(&CrsContext::builtin_only()).expect("CRS maps"), + Some(Srid::WGS84) + ); } #[test] @@ -103,7 +113,10 @@ fn gdal_nodata_is_read() { fn a_plain_tiff_has_no_georeference() { let reader = open(&with_tags(&[])).expect("opens"); assert_eq!(reader.transform(), None); - assert_eq!(reader.srid().expect("no CRS"), None); + assert_eq!( + reader.srid(&CrsContext::builtin_only()).expect("no CRS"), + None + ); assert_eq!(reader.nodata(), None); assert!(reader.geokeys().is_none()); } @@ -124,13 +137,30 @@ fn ground_control_points_are_refused() { } #[test] -fn an_unsupported_crs_is_a_typed_error() { +fn an_undefined_crs_is_a_typed_error() { + let extras = [Extra::Shorts( + Tag::GeoKeyDirectoryTag, + geokeys(&[(GT_MODEL_TYPE, 1), (PROJECTED_CS_TYPE, 999)]), + )]; + let reader = open(&with_tags(&extras)).expect("opens"); + assert!(matches!( + reader.srid(&CrsContext::builtin_only()), + Err(RasterError::UnknownCrs(_)) + )); +} + +/// Any EPSG projected CRS maps, such as UTM 33N. +#[test] +fn an_epsg_projected_crs_maps() { let extras = [Extra::Shorts( Tag::GeoKeyDirectoryTag, geokeys(&[(GT_MODEL_TYPE, 1), (PROJECTED_CS_TYPE, 32633)]), )]; let reader = open(&with_tags(&extras)).expect("opens"); - assert!(matches!(reader.srid(), Err(RasterError::UnsupportedCrs(_)))); + assert_eq!( + reader.srid(&CrsContext::builtin_only()).expect("CRS maps"), + Some(Srid::new(32633).unwrap()) + ); } #[test] diff --git a/nodedb-raster/tests/geotiff/image.rs b/nodedb-raster/tests/geotiff/image.rs new file mode 100644 index 000000000..3d41ea1da --- /dev/null +++ b/nodedb-raster/tests/geotiff/image.rs @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! [`Image`]: the sans-IO reader driven over in-memory bytes, and +//! whole-image readback. + +use nodedb_crs::CrsContext; +use nodedb_raster::{ + GeoKeyDirectory, GeoTransform, RasterBlock, RasterError, RasterLayout, RasterLimits, + RasterReader, SampleType, TiffOpener, TiffReader, open_all, read_block_all, +}; +use nodedb_types::geometry::Srid; + +use crate::fixtures::LIMITS; + +/// An opened image and the bytes it reads its blocks from. +pub struct Image { + pub reader: TiffReader, + pub bytes: Vec, +} + +impl Image { + pub fn layout(&self) -> &RasterLayout { + self.reader.layout() + } + + pub fn width(&self) -> u32 { + self.reader.header().width + } + + pub fn height(&self) -> u32 { + self.reader.header().height + } + + pub fn band_count(&self) -> u16 { + self.layout().bands + } + + pub fn sample_type(&self) -> SampleType { + self.layout().sample_type + } + + pub fn block_count(&self) -> u32 { + self.reader.block_count() + } + + pub fn read_block(&self, index: u32) -> Result { + read_block_all(&self.reader, index, &self.bytes) + } + + pub fn transform(&self) -> Option { + self.reader.header().transform + } + + pub fn srid(&self, ctx: &CrsContext<'_>) -> Result, RasterError> { + self.reader.header().crs.resolve(ctx) + } + + /// The nodata of band 0: `GDAL_NODATA` in these fixtures. + pub fn nodata(&self) -> Option { + self.reader + .header() + .bands + .first() + .and_then(|band| band.nodata) + } + + pub fn geokeys(&self) -> Option<&GeoKeyDirectory> { + self.reader.geokeys() + } +} + +/// Open an owned copy of `bytes` under [`LIMITS`], so callers can pass a temporary. +pub fn open(bytes: &[u8]) -> Result { + open_with_limits(bytes, LIMITS) +} + +/// Open an owned copy of `bytes` under `limits`. +pub fn open_with_limits(bytes: &[u8], limits: RasterLimits) -> Result { + let reader = open_all(&TiffOpener::new(), bytes, limits)?; + Ok(Image { + reader, + bytes: bytes.to_vec(), + }) +} + +/// Decode every block and reassemble one row-major `f64` plane per band. +pub fn read_all(image: &Image) -> Vec> { + let width = image.width() as usize; + let pixels = width * image.height() as usize; + let mut out = vec![vec![f64::NAN; pixels]; usize::from(image.band_count())]; + for index in 0..image.block_count() { + let block = image.read_block(index).expect("block decodes"); + let window = &block.window; + assert_eq!(block.bands.len(), out.len(), "band count of block {index}"); + for (band, samples) in block.bands.iter().enumerate() { + assert_eq!(samples.len(), window.pixels(), "samples in block {index}"); + assert_eq!(samples.sample_type(), image.sample_type()); + for r in 0..window.height as usize { + for c in 0..window.width as usize { + let row = window.row_off as usize + r; + let col = window.col_off as usize + c; + out[band][row * width + col] = samples + .value_f64(r * window.width as usize + c) + .expect("sample in window"); + } + } + } + } + out +} + +/// Open `bytes` with `limits` and decode every block; the first error. +pub fn decode_everything(bytes: &[u8], limits: RasterLimits) -> Result<(), RasterError> { + let image = open_with_limits(bytes, limits)?; + image.srid(&CrsContext::builtin_only())?; + for index in 0..image.block_count() { + image.read_block(index)?; + } + Ok(()) +} diff --git a/nodedb-raster/tests/geotiff/main.rs b/nodedb-raster/tests/geotiff/main.rs index 84ead462c..8230ea421 100644 --- a/nodedb-raster/tests/geotiff/main.rs +++ b/nodedb-raster/tests/geotiff/main.rs @@ -1,9 +1,14 @@ // SPDX-License-Identifier: Apache-2.0 -//! `GeoTiffReader` over GeoTIFFs the tiff encoder builds in-test. +//! The TIFF reader over GeoTIFFs the tiff encoder builds in-test, and over +//! the generated files under `tests/fixtures`. +mod cog; +mod files; mod fixtures; mod georef; +mod image; +mod malformed; mod robustness; mod strips; mod tiles; diff --git a/nodedb-raster/tests/geotiff/malformed.rs b/nodedb-raster/tests/geotiff/malformed.rs new file mode 100644 index 000000000..b4b64ff41 --- /dev/null +++ b/nodedb-raster/tests/geotiff/malformed.rs @@ -0,0 +1,195 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Hostile files over the generated fixtures: truncation, corruption, +//! absurd counts and offsets, IFD loops, a missing object, and the header +//! cap. Every failure is a typed error, never a panic and never an +//! allocation sized from the file. + +use nodedb_raster::tiff::error::TiffFormatError; +use nodedb_raster::{ + ByteRange, RangeRead, RasterError, RasterLimits, TiffOpener, open_all, open_with, +}; + +use crate::files::fixture_bytes; +use crate::fixtures::LIMITS; +use crate::image::decode_everything; + +/// One fixture per codec and container feature. +const CODEC_FILES: [&str; 12] = [ + "m-none-p1-strip-classic-contig-be.tif", + "m-lzw-p2-tile-big-separate-le.tif", + "m-deflate-p3-tile-classic-contig-be.tif", + "m-packbits-p1-tile-classic-separate-le.tif", + "m-zstd-p2-strip-big-contig-be.tif", + "l-lerc-pixel-tile.tif", + "l-lerc_deflate-band-strip.tif", + "l-lerc_zstd-mask.tif", + "j-ycbcr-tile.tif", + "j-gray-strip.tif", + "cog-mask.tif", + "gdal-metadata.tif", +]; + +/// Decode cap for hostile input: small, so no header can make one block +/// a large allocation. +const HOSTILE: RasterLimits = RasterLimits::new(1 << 20); + +#[test] +fn every_truncation_of_every_codec_is_a_typed_error_or_a_whole_image() { + for file in CODEC_FILES { + let bytes = fixture_bytes(file); + for len in 0..bytes.len() { + let result = decode_everything(&bytes[..len], HOSTILE); + if len < 64 { + assert!(result.is_err(), "{file}: a {len}-byte prefix decoded"); + } + } + } +} + +#[test] +fn corrupt_bytes_of_every_codec_never_panic() { + for file in CODEC_FILES { + let bytes = fixture_bytes(file); + for position in 0..bytes.len() { + for flip in [0xFF_u8, 0x10] { + let mut corrupt = bytes.clone(); + corrupt[position] ^= flip; + // Ok or a typed error are both fine. A panic fails the test. + let _ = decode_everything(&corrupt, HOSTILE); + } + } + } +} + +/// The byte offset of `tag`'s entry in IFD 0 of a little-endian classic +/// TIFF. +fn entry_at(bytes: &[u8], tag: u16) -> usize { + let ifd = u32::from_le_bytes(bytes[4..8].try_into().expect("4 bytes")) as usize; + let count = u16::from_le_bytes([bytes[ifd], bytes[ifd + 1]]) as usize; + (0..count) + .map(|i| ifd + 2 + i * 12) + .find(|at| u16::from_le_bytes([bytes[*at], bytes[*at + 1]]) == tag) + .unwrap_or_else(|| panic!("tag {tag}")) +} + +/// Write `value` as the inline LONG value of `tag`. +fn patch_long(bytes: &mut [u8], tag: u16, value: u32) { + let at = entry_at(bytes, tag); + bytes[at + 2..at + 4].copy_from_slice(&4u16.to_le_bytes()); + bytes[at + 4..at + 8].copy_from_slice(&1u32.to_le_bytes()); + bytes[at + 8..at + 12].copy_from_slice(&value.to_le_bytes()); +} + +/// A one-band little-endian classic tiled file: one tile per block, so +/// the block index of a 16x16 single-tile image is inline. +fn single_tile() -> Vec { + fixture_bytes("t-uint8.tif") +} + +#[test] +fn an_absurd_byte_count_is_refused_before_any_fetch() { + let mut bytes = single_tile(); + // Rewrite the image as one 16x16 tile with a 4 GiB byte count. + patch_long(&mut bytes, 256, 16); + patch_long(&mut bytes, 257, 16); + patch_long(&mut bytes, 324, 8); + patch_long(&mut bytes, 325, u32::MAX); + let reader = open_all(&TiffOpener::new(), &bytes, HOSTILE).expect("header is valid"); + assert!(matches!( + nodedb_raster::RasterReader::block_ranges(&reader, 0), + Err(RasterError::BlockTooLarge { + needed_bytes, + .. + }) if needed_bytes == u64::from(u32::MAX) + )); +} + +#[test] +fn an_offset_past_the_end_is_a_truncation() { + let mut bytes = single_tile(); + patch_long(&mut bytes, 256, 16); + patch_long(&mut bytes, 257, 16); + patch_long(&mut bytes, 324, 0x7FFF_0000); + patch_long(&mut bytes, 325, 100); + assert!(matches!( + decode_everything(&bytes, HOSTILE), + Err(RasterError::Malformed(TiffFormatError::Truncated { .. })) + )); +} + +#[test] +fn a_huge_image_without_its_block_index_is_refused() { + let mut bytes = single_tile(); + patch_long(&mut bytes, 256, u32::MAX); + assert!(matches!( + open_all(&TiffOpener::new(), &bytes, HOSTILE), + Err(RasterError::Malformed(TiffFormatError::TagCount { .. })) + )); +} + +#[test] +fn an_absurd_entry_count_is_refused() { + let mut bytes = single_tile(); + let ifd = u32::from_le_bytes(bytes[4..8].try_into().expect("4 bytes")) as usize; + bytes[ifd..ifd + 2].copy_from_slice(&u16::MAX.to_le_bytes()); + assert!(matches!( + open_all(&TiffOpener::new(), &bytes, HOSTILE), + Err(RasterError::Malformed( + TiffFormatError::TooManyEntries { .. } + )) + )); +} + +#[test] +fn an_ifd_loop_is_refused() { + let mut bytes = single_tile(); + let ifd = u32::from_le_bytes(bytes[4..8].try_into().expect("4 bytes")) as usize; + let count = u16::from_le_bytes([bytes[ifd], bytes[ifd + 1]]) as usize; + let next = ifd + 2 + count * 12; + let link = u32::try_from(ifd).expect("small file").to_le_bytes(); + bytes[next..next + 4].copy_from_slice(&link); + assert!(matches!( + open_all(&TiffOpener::new(), &bytes, HOSTILE), + Err(RasterError::Malformed(TiffFormatError::IfdLoop { .. })) + )); +} + +#[test] +fn a_tag_value_over_the_cap_is_refused() { + let mut bytes = single_tile(); + // A TileOffsets count of 64 Mi LONGs: 256 MiB, over the header cap. + let at = entry_at(&bytes, 324); + bytes[at + 4..at + 8].copy_from_slice(&(64 * 1024 * 1024u32).to_le_bytes()); + assert!(matches!( + open_all(&TiffOpener::new(), &bytes, HOSTILE), + Err(RasterError::TagValueTooLarge { tag: 324, .. }) + )); +} + +#[test] +fn the_header_cap_bounds_what_an_open_caches() { + let bytes = fixture_bytes("landsat-c2.tif"); + let tight = LIMITS.with_header_bytes(1000); + assert!(matches!( + open_all(&TiffOpener::new(), &bytes, tight), + Err(RasterError::HeaderTooLarge { limit_bytes: 1000 }) + )); +} + +/// A source whose object does not exist. +struct Missing; + +impl RangeRead for Missing { + fn read_range(&mut self, _range: ByteRange) -> Result>, RasterError> { + Ok(None) + } +} + +#[test] +fn a_missing_object_is_a_typed_error() { + assert!(matches!( + open_with(&TiffOpener::new(), &mut Missing, LIMITS), + Err(RasterError::SourceMissing { object: 0 }) + )); +} diff --git a/nodedb-raster/tests/geotiff/robustness.rs b/nodedb-raster/tests/geotiff/robustness.rs index c22e35ba4..0fe603c2a 100644 --- a/nodedb-raster/tests/geotiff/robustness.rs +++ b/nodedb-raster/tests/geotiff/robustness.rs @@ -3,16 +3,17 @@ //! Refusals and hostile input: every failure is a typed error, never a //! panic and never an allocation past the cap. -use nodedb_raster::geokeys::{GT_MODEL_TYPE, PROJECTED_CS_TYPE}; +use nodedb_raster::tiff::geokeys::{GT_MODEL_TYPE, PROJECTED_CS_TYPE}; use nodedb_raster::{RasterError, RasterLimits}; -use tiff::encoder::colortype::{Gray16, Gray64, GrayI8}; +use tiff::encoder::colortype::{Gray16, Gray64, GrayI64}; use tiff::encoder::{Compression, DeflateLevel}; use tiff::tags::Tag; use crate::fixtures::{ - DEFLATE, Extra, LIMITS, Pred, RawImage, RawLayout, StripSpec, decode_everything, encode_chunks, - encode_raw, encode_strips, geokeys, open, planes, strip_spec, + DEFLATE, Extra, LIMITS, Pred, RawImage, RawLayout, StripSpec, encode_chunks, encode_raw, + encode_strips, geokeys, planes, strip_spec, }; +use crate::image::{decode_everything, open}; /// A small planar tiled GeoTIFF with GeoKeys: every code path the reader /// has, in a few hundred bytes. @@ -149,10 +150,10 @@ fn unsupported_sample_types_are_refused_at_open() { compression: Compression::Deflate(DeflateLevel::Fast), ..strip_spec(4, 4) }; - let signed_bytes = vec![-3_i8; 16]; + let wide_signed = vec![-3_i64; 16]; assert!(matches!( - open(&encode_strips::(&spec, &signed_bytes)), - Err(RasterError::UnsupportedSampleType { bits: 8, .. }) + open(&encode_strips::(&spec, &wide_signed)), + Err(RasterError::UnsupportedSampleType { bits: 64, .. }) )); let wide = vec![7_u64; 16]; assert!(matches!( @@ -182,8 +183,8 @@ fn raw_gray16(compression: u16, photometric: u16) -> Vec { #[test] fn unsupported_compression_is_refused_at_open() { - // 50000 is ZSTD, 34887 LERC, 7 JPEG. - for code in [50000, 34887, 7] { + // 2 is CCITT RLE, 34925 LZMA, 50001 WebP. + for code in [2, 34925, 50001] { assert!(matches!( open(&raw_gray16(code, 1)), Err(RasterError::UnsupportedCompression { code: c }) if c == code @@ -204,7 +205,7 @@ fn transforming_photometrics_are_refused_at_open() { #[test] fn an_out_of_range_block_is_a_typed_error() { - let mut reader = open(&small_fixture()).expect("opens"); + let reader = open(&small_fixture()).expect("opens"); let count = reader.block_count(); assert!(matches!( reader.read_block(count), diff --git a/nodedb-raster/tests/geotiff/strips.rs b/nodedb-raster/tests/geotiff/strips.rs index 43dadb965..5d7cb1a72 100644 --- a/nodedb-raster/tests/geotiff/strips.rs +++ b/nodedb-raster/tests/geotiff/strips.rs @@ -5,14 +5,16 @@ use nodedb_raster::{BlockKind, Interleave, SampleType}; use tiff::encoder::colortype::{ - ColorType, Gray8, Gray16, Gray32, Gray32Float, Gray64Float, GrayI16, RGB8, RGB16, + ColorType, Gray8, Gray16, Gray32, Gray32Float, Gray64Float, GrayI8, GrayI16, GrayI32, RGB8, + RGB16, }; use tiff::encoder::{Compression, DeflateLevel, Predictor, TiffValue}; use crate::fixtures::{ - DEFLATE, NONE, Pred, RawImage, RawLayout, Sample, StripSpec, encode_chunks, encode_raw, - encode_strips, expected, interleave, open, planes, read_all, strip_spec, + DEFLATE, LZW, Pred, RawImage, RawLayout, Sample, StripSpec, encode_chunks, encode_raw, + encode_strips, expected, interleave, planes, strip_spec, }; +use crate::image::{open, read_all}; const W: u32 = 10; const H: u32 = 7; @@ -35,10 +37,10 @@ fn every_strip_codec_round_trips() { ..strip_spec(W, H) }; let bytes = encode_strips::(&spec, &planes[0]); - let mut reader = open(&bytes).expect("opens"); + let reader = open(&bytes).expect("opens"); assert_eq!(reader.layout().kind, BlockKind::Strips); assert_eq!(reader.block_count(), blocks); - assert_eq!(read_all(&mut reader), expected(&planes)); + assert_eq!(read_all(&reader), expected(&planes)); } } @@ -50,7 +52,7 @@ fn last_strip_is_cropped_to_the_image() { ..strip_spec(W, H) }; let bytes = encode_strips::(&spec, &planes[0]); - let mut reader = open(&bytes).expect("opens"); + let reader = open(&bytes).expect("opens"); let last = reader.read_block(2).expect("last strip"); assert_eq!( (last.window.row_off, last.window.width, last.window.height), @@ -68,8 +70,8 @@ fn horizontal_predictor_round_trips() { rows_per_strip: Some(2), ..strip_spec(W, H) }; - let mut reader = open(&encode_strips::(&spec, &gray[0])).expect("opens"); - assert_eq!(read_all(&mut reader), expected(&gray)); + let reader = open(&encode_strips::(&spec, &gray[0])).expect("opens"); + assert_eq!(read_all(&reader), expected(&gray)); let signed = planes::(W, H, 1); let spec = StripSpec { @@ -77,8 +79,8 @@ fn horizontal_predictor_round_trips() { predictor: Predictor::Horizontal, ..strip_spec(W, H) }; - let mut reader = open(&encode_strips::(&spec, &signed[0])).expect("opens"); - assert_eq!(read_all(&mut reader), expected(&signed)); + let reader = open(&encode_strips::(&spec, &signed[0])).expect("opens"); + assert_eq!(read_all(&reader), expected(&signed)); let rgb = planes::(W, H, 3); let spec = StripSpec { @@ -87,15 +89,16 @@ fn horizontal_predictor_round_trips() { rows_per_strip: Some(4), ..strip_spec(W, H) }; - let mut reader = open(&encode_strips::(&spec, &interleave(&rgb))).expect("opens"); + let reader = open(&encode_strips::(&spec, &interleave(&rgb))).expect("opens"); assert_eq!(reader.band_count(), 3); - assert_eq!(read_all(&mut reader), expected(&rgb)); + assert_eq!(read_all(&reader), expected(&rgb)); } #[test] fn floating_point_predictor_round_trips() { let layout = RawLayout::Strips { rows_per_strip: 3 }; - for compression in [NONE, DEFLATE] { + // libtiff applies a predictor under LZW, deflate and ZSTD only. + for compression in [LZW, DEFLATE] { let two_band = planes::(W, H, 2); let chunks = encode_chunks( &two_band, @@ -119,9 +122,9 @@ fn floating_point_predictor_round_trips() { chunks, extras: &[], }); - let mut reader = open(&bytes).expect("opens"); + let reader = open(&bytes).expect("opens"); assert_eq!(reader.sample_type(), SampleType::F32); - assert_eq!(read_all(&mut reader), expected(&two_band)); + assert_eq!(read_all(&reader), expected(&two_band)); } let single = planes::(W, H, 1); @@ -140,9 +143,9 @@ fn floating_point_predictor_round_trips() { chunks, extras: &[], }); - let mut reader = open(&bytes).expect("opens"); + let reader = open(&bytes).expect("opens"); assert_eq!(reader.sample_type(), SampleType::F64); - assert_eq!(read_all(&mut reader), expected(&single)); + assert_eq!(read_all(&reader), expected(&single)); } fn round_trip(sample_type: SampleType) @@ -156,14 +159,16 @@ where rows_per_strip: Some(2), ..strip_spec(W, H) }; - let mut reader = open(&encode_strips::(&spec, &planes[0])).expect("opens"); + let reader = open(&encode_strips::(&spec, &planes[0])).expect("opens"); assert_eq!(reader.sample_type(), sample_type); - assert_eq!(read_all(&mut reader), expected(&planes)); + assert_eq!(read_all(&reader), expected(&planes)); } #[test] fn every_supported_sample_type_round_trips() { round_trip::(SampleType::U8); + round_trip::(SampleType::I8); + round_trip::(SampleType::I32); round_trip::(SampleType::U16); round_trip::(SampleType::I16); round_trip::(SampleType::U32); @@ -180,16 +185,16 @@ fn multi_band_chunky_pixels_split_per_band() { extra_samples: 3, ..strip_spec(W, H) }; - let mut reader = open(&encode_strips::(&spec, &interleave(&multiband))).expect("opens"); + let reader = open(&encode_strips::(&spec, &interleave(&multiband))).expect("opens"); assert_eq!(reader.band_count(), 4); assert_eq!(reader.layout().interleave, Interleave::Chunky); - assert_eq!(read_all(&mut reader), expected(&multiband)); + assert_eq!(read_all(&reader), expected(&multiband)); let rgb = planes::(W, H, 3); let spec = StripSpec { compression: Compression::Packbits, ..strip_spec(W, H) }; - let mut reader = open(&encode_strips::(&spec, &interleave(&rgb))).expect("opens"); - assert_eq!(read_all(&mut reader), expected(&rgb)); + let reader = open(&encode_strips::(&spec, &interleave(&rgb))).expect("opens"); + assert_eq!(read_all(&reader), expected(&rgb)); } diff --git a/nodedb-raster/tests/geotiff/tiles.rs b/nodedb-raster/tests/geotiff/tiles.rs index b143316a8..87d25413f 100644 --- a/nodedb-raster/tests/geotiff/tiles.rs +++ b/nodedb-raster/tests/geotiff/tiles.rs @@ -6,8 +6,9 @@ use nodedb_raster::{BlockKind, BlockWindow, Interleave, SampleType}; use crate::fixtures::{ DEFLATE, LZW, NONE, Pred, RawImage, RawLayout, Sample, encode_chunks, encode_raw, expected, - open, planes, read_all, + planes, }; +use crate::image::{open, read_all}; /// 40 x 24 pixels in 16 x 16 tiles: the right column is 8 wide and the /// bottom row 8 tall. @@ -63,7 +64,7 @@ fn chunky_tiles_crop_edge_tiles() { compression: DEFLATE, }; let (bytes, want) = encode::(&case, 16, 1); - let mut reader = open(&bytes).expect("opens"); + let reader = open(&bytes).expect("opens"); assert_eq!(reader.layout().kind, BlockKind::Tiles); assert_eq!(reader.layout().interleave, Interleave::Chunky); assert_eq!(reader.block_count(), 6); @@ -71,6 +72,7 @@ fn chunky_tiles_crop_edge_tiles() { assert_eq!( corner.window, BlockWindow { + outer: Vec::new(), col_off: 32, row_off: 16, width: 8, @@ -78,7 +80,7 @@ fn chunky_tiles_crop_edge_tiles() { } ); assert!(corner.bands.iter().all(|band| band.len() == 64)); - assert_eq!(read_all(&mut reader), want); + assert_eq!(read_all(&reader), want); } #[test] @@ -91,13 +93,13 @@ fn planar_tiles_crop_edge_tiles_in_every_plane() { compression: LZW, }; let (bytes, want) = encode::(&case, 16, 2); - let mut reader = open(&bytes).expect("opens"); + let reader = open(&bytes).expect("opens"); assert_eq!(reader.layout().interleave, Interleave::Planar); assert_eq!(reader.sample_type(), SampleType::I16); let bottom = reader.read_block(4).expect("bottom tile"); assert_eq!((bottom.window.width, bottom.window.height), (16, 8)); assert!(bottom.bands.iter().all(|band| band.len() == 16 * 8)); - assert_eq!(read_all(&mut reader), want); + assert_eq!(read_all(&reader), want); } #[test] @@ -110,10 +112,10 @@ fn planar_strips_round_trip() { compression: NONE, }; let (bytes, want) = encode::(&case, 8, 1); - let mut reader = open(&bytes).expect("opens"); + let reader = open(&bytes).expect("opens"); assert_eq!(reader.layout().kind, BlockKind::Strips); assert_eq!(reader.block_count(), H.div_ceil(5)); - assert_eq!(read_all(&mut reader), want); + assert_eq!(read_all(&reader), want); } #[test] @@ -126,8 +128,8 @@ fn tiles_with_horizontal_predictor_round_trip() { compression: DEFLATE, }; let (bytes, want) = encode::(&case, 16, 1); - let mut reader = open(&bytes).expect("opens"); - assert_eq!(read_all(&mut reader), want); + let reader = open(&bytes).expect("opens"); + assert_eq!(read_all(&reader), want); } #[test] @@ -140,9 +142,9 @@ fn planar_float_tiles_with_floating_point_predictor_round_trip() { compression: DEFLATE, }; let (bytes, want) = encode::(&case, 32, 3); - let mut reader = open(&bytes).expect("opens"); + let reader = open(&bytes).expect("opens"); assert_eq!(reader.sample_type(), SampleType::F32); - assert_eq!(read_all(&mut reader), want); + assert_eq!(read_all(&reader), want); } #[test] @@ -155,12 +157,12 @@ fn u32_and_f64_tiles_round_trip() { compression: LZW, }; let (bytes, want) = encode::(&case, 32, 1); - let mut reader = open(&bytes).expect("opens"); + let reader = open(&bytes).expect("opens"); assert_eq!(reader.sample_type(), SampleType::U32); - assert_eq!(read_all(&mut reader), want); + assert_eq!(read_all(&reader), want); let (bytes, want) = encode::(&case, 64, 3); - let mut reader = open(&bytes).expect("opens"); + let reader = open(&bytes).expect("opens"); assert_eq!(reader.sample_type(), SampleType::F64); - assert_eq!(read_all(&mut reader), want); + assert_eq!(read_all(&reader), want); } diff --git a/nodedb-spatial/Cargo.toml b/nodedb-spatial/Cargo.toml index 3336ad7a7..1fe42691e 100644 --- a/nodedb-spatial/Cargo.toml +++ b/nodedb-spatial/Cargo.toml @@ -18,6 +18,7 @@ default = [] [dependencies] nodedb-types = { workspace = true } +nodedb-crs = { workspace = true } nodedb-wal = { workspace = true } nodedb-mem = { workspace = true } thiserror = { workspace = true } @@ -27,6 +28,7 @@ sonic-rs = { workspace = true } zerompk = { workspace = true } rkyv = { workspace = true } h3o = { workspace = true } +geo = { workspace = true } [dev-dependencies] rand = { workspace = true } diff --git a/nodedb-spatial/src/crs/ellipsoid.rs b/nodedb-spatial/src/crs/ellipsoid.rs deleted file mode 100644 index 240623529..000000000 --- a/nodedb-spatial/src/crs/ellipsoid.rs +++ /dev/null @@ -1,69 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -//! Reference ellipsoids. - -/// An ellipsoid of revolution, defined by its semi-major axis and inverse -/// flattening. -#[derive(Debug, Clone, Copy, PartialEq)] -pub struct Ellipsoid { - /// Semi-major axis in metres. - pub a: f64, - /// Inverse flattening, 1/f. - pub inv_f: f64, -} - -/// GRS 1980 (EPSG:7019), the ellipsoid of GDM2000. -pub const GRS80: Ellipsoid = Ellipsoid { - a: 6_378_137.0, - inv_f: 298.257_222_101, -}; - -/// WGS 84 (EPSG:7030). -pub const WGS84: Ellipsoid = Ellipsoid { - a: 6_378_137.0, - inv_f: 298.257_223_563, -}; - -impl Ellipsoid { - /// Flattening f. - pub fn f(&self) -> f64 { - 1.0 / self.inv_f - } - - /// First eccentricity squared, e² = f(2 − f). - pub fn e2(&self) -> f64 { - let f = self.f(); - f * (2.0 - f) - } - - /// First eccentricity e. - pub fn e(&self) -> f64 { - self.e2().sqrt() - } - - /// Third flattening n = f / (2 − f). - pub fn n(&self) -> f64 { - let f = self.f(); - f / (2.0 - f) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn grs80_and_wgs84_differ_only_in_flattening() { - assert_eq!(GRS80.a, WGS84.a); - assert!((GRS80.e2() - 0.006_694_380_022_90).abs() < 1e-14); - assert!((WGS84.e2() - 0.006_694_379_990_14).abs() < 1e-14); - } - - #[test] - fn third_flattening_matches_definition() { - let n = WGS84.n(); - let f = WGS84.f(); - assert!((n - f / (2.0 - f)).abs() < 1e-18); - assert!((WGS84.e() * WGS84.e() - WGS84.e2()).abs() < 1e-18); - } -} diff --git a/nodedb-spatial/src/crs/error.rs b/nodedb-spatial/src/crs/error.rs deleted file mode 100644 index 4d55bd78d..000000000 --- a/nodedb-spatial/src/crs/error.rs +++ /dev/null @@ -1,130 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -//! Coordinate transformation errors. - -use nodedb_types::geometry::Srid; - -/// A coordinate a projection cannot map. -#[derive(Debug, Clone, Copy, PartialEq, thiserror::Error)] -pub enum CrsError { - /// The coordinate lies outside the region where the projection is - /// defined or accurate. - #[error("coordinate ({x}, {y}) is outside the domain of SRID {srid}: {reason}")] - OutOfDomain { - srid: Srid, - x: f64, - y: f64, - reason: DomainReason, - }, -} - -/// Why a coordinate is outside a projection's domain. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum DomainReason { - /// An ordinate is NaN or infinite. - NonFinite, - /// Latitude is beyond ±90°. - LatitudeRange, - /// Longitude is too far from a Transverse Mercator central meridian for - /// the Krüger series to hold. - CentralMeridianOffset, - /// The Hotine Oblique Mercator U term reaches ±1, where the projection - /// diverges. - ObliqueSingularity, -} - -impl std::fmt::Display for DomainReason { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(match self { - DomainReason::NonFinite => "non-finite ordinate", - DomainReason::LatitudeRange => "latitude beyond ±90°", - DomainReason::CentralMeridianOffset => "too far from the central meridian", - DomainReason::ObliqueSingularity => "on the oblique projection's singular line", - }) - } -} - -/// Largest absolute latitude, in degrees. -const MAX_LATITUDE_DEG: f64 = 90.0; - -/// Fail unless `[lon, lat]` is a finite geographic coordinate with latitude -/// inside ±90°. -pub(crate) fn check_lon_lat(srid: Srid, coord: [f64; 2]) -> Result<(), CrsError> { - let [lon, lat] = coord; - if !lon.is_finite() || !lat.is_finite() { - return Err(CrsError::out_of_domain( - srid, - coord, - DomainReason::NonFinite, - )); - } - if lat.abs() > MAX_LATITUDE_DEG { - return Err(CrsError::out_of_domain( - srid, - coord, - DomainReason::LatitudeRange, - )); - } - Ok(()) -} - -/// Fail unless both ordinates are finite. -pub(crate) fn check_finite(srid: Srid, coord: [f64; 2]) -> Result<(), CrsError> { - if coord[0].is_finite() && coord[1].is_finite() { - Ok(()) - } else { - Err(CrsError::out_of_domain( - srid, - coord, - DomainReason::NonFinite, - )) - } -} - -impl CrsError { - pub(crate) fn out_of_domain(srid: Srid, [x, y]: [f64; 2], reason: DomainReason) -> Self { - CrsError::OutOfDomain { srid, x, y, reason } - } - - /// The SRID whose projection rejected the coordinate. - pub fn srid(&self) -> Srid { - match self { - CrsError::OutOfDomain { srid, .. } => *srid, - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn lon_lat_checks() { - assert!(check_lon_lat(Srid::Utm48N, [105.0, 90.0]).is_ok()); - assert_eq!( - check_lon_lat(Srid::Utm48N, [105.0, 90.5]), - Err(CrsError::out_of_domain( - Srid::Utm48N, - [105.0, 90.5], - DomainReason::LatitudeRange - )) - ); - assert!(check_lon_lat(Srid::Utm48N, [f64::NAN, 0.0]).is_err()); - assert!(check_finite(Srid::BorneoRso, [0.0, f64::INFINITY]).is_err()); - } - - #[test] - fn message_names_srid_and_reason() { - let e = CrsError::out_of_domain( - Srid::Utm47N, - [150.0, 3.0], - DomainReason::CentralMeridianOffset, - ); - assert_eq!(e.srid(), Srid::Utm47N); - let text = e.to_string(); - assert!( - text.contains("32647") && text.contains("central meridian"), - "{text}" - ); - } -} diff --git a/nodedb-spatial/src/crs/mod.rs b/nodedb-spatial/src/crs/mod.rs index 5734313fc..bf745a7ca 100644 --- a/nodedb-spatial/src/crs/mod.rs +++ b/nodedb-spatial/src/crs/mod.rs @@ -1,18 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 -//! Coordinate reference system transforms for the supported SRIDs: -//! WGS 84, UTM 47N / 48N, and the GDM2000 Malaysian RSO grids. +//! Geometry transforms between coordinate reference systems. The CRS +//! registry and the coordinate math live in `nodedb-crs`. -pub mod ellipsoid; -pub mod error; -pub mod hom; -pub mod params; -pub mod tm; pub mod transform; -pub use error::{CrsError, DomainReason}; -pub use hom::{HomParams, HomVariant, HotineObliqueMercator}; -pub use tm::{ - TAU_NEWTON_MAX_ITERS, TAU_TOLERANCE, TM_MAX_LON_OFFSET_DEG, TmParams, TransverseMercator, -}; -pub use transform::{Projection, Transformer, transform_geometry}; +pub use transform::{transform_geometry, transform_geometry_at}; diff --git a/nodedb-spatial/src/crs/params.rs b/nodedb-spatial/src/crs/params.rs deleted file mode 100644 index ff75d39cb..000000000 --- a/nodedb-spatial/src/crs/params.rs +++ /dev/null @@ -1,103 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -//! Defining parameters of every supported projected SRID. -//! -//! Values are the EPSG definitions as PROJ publishes them. -//! -//! Datum bound: GDM2000 (EPSG:3375, 3376) is ITRF2000 at epoch 2000.0. Its -//! published `towgs84` is all zeros, so WGS 84 → GDM2000 is taken as the -//! identity. GDM2000 is fixed to the plate while WGS 84 tracks ITRF, so the -//! identity drifts with Sundaland plate motion: about 1 m by 2026. Results in -//! 3375 / 3376 are exact to the projection, not to that datum offset. - -use super::ellipsoid::{GRS80, WGS84}; -use super::hom::{HomParams, HomVariant}; -use super::tm::TmParams; - -/// UTM scale factor on the central meridian. -const UTM_K0: f64 = 0.9996; - -/// UTM false easting, metres. -const UTM_FALSE_EASTING: f64 = 500_000.0; - -/// UTM false northing in the northern hemisphere, metres. -const UTM_FALSE_NORTHING_NORTH: f64 = 0.0; - -/// RSO scale factor on the initial line, both Malaysian RSO grids. -const RSO_K: f64 = 0.99984; - -/// EPSG:32647, UTM zone 47N on WGS 84. -pub const UTM_47N: TmParams = TmParams { - lon0_deg: 99.0, - k0: UTM_K0, - false_easting: UTM_FALSE_EASTING, - false_northing: UTM_FALSE_NORTHING_NORTH, - ellipsoid: WGS84, -}; - -/// EPSG:32648, UTM zone 48N on WGS 84. -pub const UTM_48N: TmParams = TmParams { - lon0_deg: 105.0, - k0: UTM_K0, - false_easting: UTM_FALSE_EASTING, - false_northing: UTM_FALSE_NORTHING_NORTH, - ellipsoid: WGS84, -}; - -/// EPSG:3375, GDM2000 Peninsula RSO. -/// `+proj=omerc +no_uoff +lat_0=4 +lonc=102.25 +alpha=323.025796466667 -/// +gamma=323.130102361111 +k=0.99984 +x_0=804671 +y_0=0 +ellps=GRS80`. -pub const PENINSULAR_RSO: HomParams = HomParams { - variant: HomVariant::A, - lat_c_deg: 4.0, - lon_c_deg: 102.25, - alpha_c_deg: 323.025_796_466_667, - gamma_c_deg: 323.130_102_361_111, - k_c: RSO_K, - false_easting: 804_671.0, - false_northing: 0.0, - ellipsoid: GRS80, -}; - -/// EPSG:3376, GDM2000 East Malaysia (Borneo) RSO. -/// `+proj=omerc +no_uoff +lat_0=4 +lonc=115 +alpha=53.31580995 -/// +gamma=53.1301023611111 +k=0.99984 +x_0=0 +y_0=0 +ellps=GRS80`. -pub const BORNEO_RSO: HomParams = HomParams { - variant: HomVariant::A, - lat_c_deg: 4.0, - lon_c_deg: 115.0, - alpha_c_deg: 53.315_809_95, - gamma_c_deg: 53.130_102_361_111_1, - k_c: RSO_K, - false_easting: 0.0, - false_northing: 0.0, - ellipsoid: GRS80, -}; - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn utm_zones_differ_only_in_central_meridian() { - assert_eq!(UTM_47N.lon0_deg, 99.0); - assert_eq!(UTM_48N.lon0_deg, 105.0); - assert_eq!( - TmParams { - lon0_deg: UTM_48N.lon0_deg, - ..UTM_47N - }, - UTM_48N - ); - } - - #[test] - fn rso_grids_are_variant_a_on_grs80() { - for p in [PENINSULAR_RSO, BORNEO_RSO] { - assert_eq!(p.variant, HomVariant::A); - assert_eq!(p.ellipsoid, GRS80); - assert_eq!(p.k_c, RSO_K); - assert_eq!(p.lat_c_deg, 4.0); - } - } -} diff --git a/nodedb-spatial/src/crs/transform.rs b/nodedb-spatial/src/crs/transform.rs index d328727f3..470e37d98 100644 --- a/nodedb-spatial/src/crs/transform.rs +++ b/nodedb-spatial/src/crs/transform.rs @@ -1,122 +1,71 @@ // SPDX-License-Identifier: Apache-2.0 -//! Coordinate transformation between supported SRIDs. -//! -//! Every path runs through WGS 84 geographic coordinates: the inverse of the -//! source projection (none for 4326), then the forward of the target. The -//! WGS 84 → GDM2000 datum step is the identity (see `params`). - -use nodedb_types::geometry::{Srid, SridGeometry}; - -use super::error::CrsError; -use super::hom::HotineObliqueMercator; -use super::params::{BORNEO_RSO, PENINSULAR_RSO, UTM_47N, UTM_48N}; -use super::tm::TransverseMercator; - -/// The projection behind one projected SRID. -#[derive(Debug, Clone)] -pub enum Projection { - TransverseMercator(TransverseMercator), - HotineObliqueMercator(HotineObliqueMercator), -} - -impl Projection { - /// The projection of `srid`, or `None` for the geographic WGS 84. - pub fn for_srid(srid: Srid) -> Option { - match srid { - Srid::Wgs84 => None, - Srid::Utm47N => Some(Projection::TransverseMercator(TransverseMercator::new( - srid, UTM_47N, - ))), - Srid::Utm48N => Some(Projection::TransverseMercator(TransverseMercator::new( - srid, UTM_48N, - ))), - Srid::PeninsularRso => Some(Projection::HotineObliqueMercator( - HotineObliqueMercator::new(srid, PENINSULAR_RSO), - )), - Srid::BorneoRso => Some(Projection::HotineObliqueMercator( - HotineObliqueMercator::new(srid, BORNEO_RSO), - )), - } - } +//! Coordinate transformation of whole geometries between SRIDs. - /// `[lon, lat]` degrees to `[easting, northing]` metres. - pub fn forward(&self, coord: [f64; 2]) -> Result<[f64; 2], CrsError> { - match self { - Projection::TransverseMercator(p) => p.forward(coord), - Projection::HotineObliqueMercator(p) => p.forward(coord), - } - } +use nodedb_crs::{CoordinateEpoch, CrsContext, CrsError}; +use nodedb_types::geometry::{Position, Srid, SridGeometry}; - /// `[easting, northing]` metres to `[lon, lat]` degrees. - pub fn inverse(&self, coord: [f64; 2]) -> Result<[f64; 2], CrsError> { - match self { - Projection::TransverseMercator(p) => p.inverse(coord), - Projection::HotineObliqueMercator(p) => p.inverse(coord), - } - } -} - -/// A prepared `from → to` coordinate transform. -#[derive(Debug, Clone)] -pub struct Transformer { - from: Srid, - source: Option, - target: Option, +/// Transform every coordinate of `geom` into `target`, resolving both SRIDs +/// through `ctx`. +/// +/// A geometry already in `target` comes back unchanged once `ctx` confirms +/// the SRID is defined. Z is ellipsoidal height in metres and goes through +/// the datum shift with x and y. M passes through. Any coordinate the +/// target projection cannot map fails the whole call with +/// [`CrsError::OutOfDomain`]. +/// +/// A time-dependent datum shift runs at its parameter reference epoch. Use +/// [`transform_geometry_at`] for coordinates at another epoch. +pub fn transform_geometry( + geom: &SridGeometry, + target: Srid, + ctx: &CrsContext<'_>, +) -> Result { + transform(geom, target, None, ctx) } -impl Transformer { - pub fn new(from: Srid, to: Srid) -> Self { - // A same-SRID transform maps nothing. - if from == to { - return Self { - from, - source: None, - target: None, - }; - } - Self { - from, - source: Projection::for_srid(from), - target: Projection::for_srid(to), - } - } - - /// Map one coordinate. - pub fn apply(&self, coord: [f64; 2]) -> Result<[f64; 2], CrsError> { - let geographic = match &self.source { - Some(p) => p.inverse(coord)?, - None => { - super::error::check_finite(self.from, coord)?; - coord - } - }; - match &self.target { - Some(p) => p.forward(geographic), - None => Ok(geographic), - } - } +/// [`transform_geometry`] for coordinates that refer to coordinate epoch +/// `epoch`: every time-dependent datum shift takes its parameters at that +/// epoch. Static shifts give the same result as [`transform_geometry`]. +pub fn transform_geometry_at( + geom: &SridGeometry, + target: Srid, + epoch: CoordinateEpoch, + ctx: &CrsContext<'_>, +) -> Result { + transform(geom, target, Some(epoch), ctx) } -/// Transform every coordinate of `geom` into `target`. -/// -/// A geometry already in `target` comes back unchanged. Any coordinate a -/// projection cannot map fails the whole call with -/// [`CrsError::OutOfDomain`]. -pub fn transform_geometry(geom: &SridGeometry, target: Srid) -> Result { +fn transform( + geom: &SridGeometry, + target: Srid, + epoch: Option, + ctx: &CrsContext<'_>, +) -> Result { + let transformer = ctx.transformer(geom.srid, target)?; if geom.srid == target { return Ok(geom.clone()); } - let transformer = Transformer::new(geom.srid, target); - let geometry = geom - .geometry - .try_map_coords(&mut |c| transformer.apply(c))?; - Ok(SridGeometry::new(target, geometry)) + let mapped = geom.try_map_positions(|p| { + let xyz = [p.x, p.y, p.z.unwrap_or(0.0)]; + let [x, y, z] = match epoch { + None => transformer.apply(xyz)?, + Some(epoch) => transformer.apply_at(xyz, epoch)?, + }; + Ok::<_, CrsError>(Position { + x, + y, + z: p.z.map(|_| z), + m: p.m, + }) + })?; + Ok(mapped.with_srid(target)) } #[cfg(test)] mod tests { use super::*; + use nodedb_crs::UnknownSrid; use nodedb_types::geometry::Geometry; /// Largest round-trip error accepted, metres. @@ -126,80 +75,145 @@ mod tests { /// to a conservative degree tolerance. const METRES_PER_DEGREE: f64 = 111_320.0; + fn srid(code: u32) -> Srid { + Srid::new(code).unwrap() + } + fn point(srid: Srid, x: f64, y: f64) -> SridGeometry { SridGeometry::new(srid, Geometry::point(x, y)) } fn coords(g: &SridGeometry) -> [f64; 2] { - match g.geometry { - Geometry::Point { coordinates } => coordinates, + match g.geometry() { + Geometry::Point { coordinates } => *coordinates, _ => panic!("expected a point"), } } #[test] - fn wgs84_round_trip_through_every_srid() { - let inputs = [[101.7117, 3.1579], [116.0735, 5.9804], [103.5, 1.5]]; - for srid in &Srid::SUPPORTED[1..] { + fn wgs84_round_trip_through_projected_srids() { + let ctx = CrsContext::builtin_only(); + let inputs = [[101.7117, 3.1579], [103.5, 1.5]]; + for code in [32647, 32648, 3375, 3857, 6933] { for input in inputs { - let src = point(Srid::Wgs84, input[0], input[1]); - let projected = transform_geometry(&src, *srid).unwrap(); - assert_eq!(projected.srid, *srid); - let back = transform_geometry(&projected, Srid::Wgs84).unwrap(); + let src = point(Srid::WGS84, input[0], input[1]); + let projected = transform_geometry(&src, srid(code), &ctx).unwrap(); + assert_eq!(projected.srid, srid(code)); + let back = transform_geometry(&projected, Srid::WGS84, &ctx).unwrap(); let [lon, lat] = coords(&back); let tol = ROUND_TRIP_TOLERANCE_M / METRES_PER_DEGREE; assert!( (lon - input[0]).abs() < tol && (lat - input[1]).abs() < tol, - "{srid:?}: {input:?} -> {:?}", + "{code}: {input:?} -> {:?}", [lon, lat] ); } } } + /// A forward then inverse UTM transform returns the input within 1e-8°. #[test] - fn peninsular_rso_through_utm47n_round_trips() { - let src = point(Srid::PeninsularRso, 412_834.468_1, 349_472.917_7); - let utm = transform_geometry(&src, Srid::Utm47N).unwrap(); - let back = transform_geometry(&utm, Srid::PeninsularRso).unwrap(); - let [x, y] = coords(&back); + fn utm47n_round_trip_is_tight() { + let ctx = CrsContext::builtin_only(); + let src = point(Srid::WGS84, 101.7117, 3.1579); + let utm = transform_geometry(&src, srid(32647), &ctx).unwrap(); + let back = transform_geometry(&utm, Srid::WGS84, &ctx).unwrap(); + let [lon, lat] = coords(&back); + assert!((lon - 101.7117).abs() < 1e-8 && (lat - 3.1579).abs() < 1e-8); + } + + /// PROJ 9.8.1 reference: EPSG:4326 (102.2375, 3.975) to EPSG:32648. + #[test] + fn utm48n_matches_proj() { + let ctx = CrsContext::builtin_only(); + let src = point(Srid::WGS84, 102.2375, 3.975); + let [x, y] = coords(&transform_geometry(&src, srid(32648), &ctx).unwrap()); + assert!( + (x - 193_218.924_909_861_76).abs() < ROUND_TRIP_TOLERANCE_M, + "{x}" + ); assert!( - (x - 412_834.468_1).abs() < ROUND_TRIP_TOLERANCE_M - && (y - 349_472.917_7).abs() < ROUND_TRIP_TOLERANCE_M, - "{x} {y}" + (y - 439_876.984_345_265_6).abs() < ROUND_TRIP_TOLERANCE_M, + "{y}" ); - let [ux, uy] = coords(&utm); - assert!((ux - 801_401.794_9).abs() < ROUND_TRIP_TOLERANCE_M, "{ux}"); - assert!((uy - 349_439.510_6).abs() < ROUND_TRIP_TOLERANCE_M, "{uy}"); } #[test] fn same_srid_is_identity() { - let src = point(Srid::Utm48N, 1.0, 2.0); - assert_eq!(transform_geometry(&src, Srid::Utm48N), Ok(src)); + let ctx = CrsContext::builtin_only(); + let src = point(srid(32648), 1.0, 2.0); + assert_eq!(transform_geometry(&src, srid(32648), &ctx), Ok(src)); + } + + #[test] + fn undefined_srid_fails() { + let ctx = CrsContext::builtin_only(); + let src = point(Srid::WGS84, 101.0, 3.0); + assert_eq!( + transform_geometry(&src, srid(999), &ctx), + Err(CrsError::Unknown(UnknownSrid::Undefined { code: 999 })) + ); } #[test] fn every_coordinate_is_mapped() { + let ctx = CrsContext::builtin_only(); let src = SridGeometry::wgs84(Geometry::polygon(vec![vec![ [101.0, 3.0], [102.0, 3.0], [102.0, 4.0], [101.0, 3.0], ]])); - let out = transform_geometry(&src, Srid::Utm47N).unwrap(); - let Geometry::Polygon { coordinates } = out.geometry else { + let out = transform_geometry(&src, srid(32647), &ctx).unwrap(); + let Geometry::Polygon { coordinates } = out.geometry() else { panic!("shape changed"); }; assert_eq!(coordinates[0].len(), 4); assert!(coordinates[0].iter().all(|c| c[0] > 100_000.0)); } + /// Z is ellipsoidal height through the datum shift. M passes through. + #[test] + fn z_goes_through_the_transform_and_m_passes_through() { + use nodedb_types::geometry::Ordinates; + let ctx = CrsContext::builtin_only(); + let src = SridGeometry::with_ordinates( + Srid::WGS84, + Geometry::point(101.7, 3.1), + Ordinates::new(Some(vec![100.0]), Some(vec![7.0])), + ) + .unwrap(); + let utm = transform_geometry(&src, srid(32647), &ctx).unwrap(); + assert_eq!(utm.ordinates().z(), Some(&[100.0][..])); + assert_eq!(utm.ordinates().m(), Some(&[7.0][..])); + // GDM2000 is GRS80 with a zero shift: height moves by under 1 mm. + let gdm = transform_geometry(&src, srid(4742), &ctx).unwrap(); + let z = gdm.ordinates().z().unwrap()[0]; + assert!((z - 100.0).abs() < 1e-3, "{z}"); + } + + /// A static shift gives the same result at any epoch. + #[test] + fn epoch_leaves_static_shifts_unchanged() { + let ctx = CrsContext::builtin_only(); + let src = point(srid(4742), 101.7117, 3.1579); + let epoch = CoordinateEpoch::new(2024.5).unwrap(); + assert_eq!( + transform_geometry_at(&src, srid(32647), epoch, &ctx), + transform_geometry(&src, srid(32647), &ctx) + ); + assert_eq!( + transform_geometry_at(&src, srid(4742), epoch, &ctx), + Ok(src) + ); + } + #[test] fn out_of_domain_fails_the_call() { - let src = point(Srid::Wgs84, 179.0, 3.0); + let ctx = CrsContext::builtin_only(); + let src = point(Srid::WGS84, 179.0, 3.0); assert!(matches!( - transform_geometry(&src, Srid::Utm48N), + transform_geometry(&src, srid(32648), &ctx), Err(CrsError::OutOfDomain { .. }) )); } diff --git a/nodedb-spatial/src/distance3d.rs b/nodedb-spatial/src/distance3d.rs new file mode 100644 index 000000000..4616c986d --- /dev/null +++ b/nodedb-spatial/src/distance3d.rs @@ -0,0 +1,363 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! 3D distance between geometries in Cartesian metres. +//! +//! Every position maps to Cartesian metres first: a projected CRS scales x +//! and y by its unit and keeps Z, a geographic CRS goes to Earth-centred +//! (ECEF) coordinates on its ellipsoid with Z as ellipsoidal height. A +//! missing Z is 0. A polygon is a planar surface through its first three +//! non-collinear vertices, as PostGIS `ST_3DDistance` treats it. + +use nodedb_types::geometry::{Geometry, SridGeometry, point_in_polygon}; + +use crate::predicates::edge::point_on_ring_boundary; + +/// Cross-product length under which three vertices count as collinear. +const COLLINEAR_EPS: f64 = 1e-12; + +/// Maps one position (x, y, z) to Cartesian metres. +pub(crate) type ToCartesian<'a> = &'a dyn Fn([f64; 2], f64) -> [f64; 3]; + +/// One polygon: its 2D rings for containment and its 3D plane. +struct Surface { + rings: Vec>, + /// A point on the plane and its unit normal. `None` for a degenerate + /// polygon, which then measures by its boundary alone. + plane: Option<([f64; 3], [f64; 3])>, +} + +/// A geometry as 3D edges, 3D vertices with their 2D position, and +/// surfaces. +#[derive(Default)] +struct Parts3 { + segments: Vec<([f64; 3], [f64; 3])>, + vertices: Vec<([f64; 2], [f64; 3])>, + surfaces: Vec, +} + +struct Builder<'a> { + g: &'a SridGeometry, + to_xyz: ToCartesian<'a>, + index: usize, + parts: Parts3, +} + +impl Builder<'_> { + fn next(&mut self, c: [f64; 2]) -> [f64; 3] { + let (z, _) = self.g.ordinates().at(self.index); + self.index += 1; + let p = (self.to_xyz)(c, z.unwrap_or(0.0)); + self.parts.vertices.push((c, p)); + p + } + + fn path(&mut self, coords: &[[f64; 2]], close: bool) -> Vec<[f64; 3]> { + let points: Vec<[f64; 3]> = coords.iter().map(|c| self.next(*c)).collect(); + match points.as_slice() { + [] => {} + [only] => self.parts.segments.push((*only, *only)), + _ => { + self.parts + .segments + .extend(points.windows(2).map(|w| (w[0], w[1]))); + if close && let (Some(first), Some(last)) = (points.first(), points.last()) { + self.parts.segments.push((*last, *first)); + } + } + } + points + } + + fn polygon(&mut self, rings: &[Vec<[f64; 2]>]) { + let mut exterior = Vec::new(); + for (i, ring) in rings.iter().enumerate() { + let points = self.path(ring, true); + if i == 0 { + exterior = points; + } + } + self.parts.surfaces.push(Surface { + rings: rings.to_vec(), + plane: plane_of(&exterior), + }); + } + + fn geometry(&mut self, geom: &Geometry) { + match geom { + Geometry::Point { .. } if geom.is_empty() => {} + Geometry::Point { coordinates } => { + let p = self.next(*coordinates); + self.parts.segments.push((p, p)); + } + Geometry::MultiPoint { coordinates } => { + for c in coordinates { + let p = self.next(*c); + self.parts.segments.push((p, p)); + } + } + Geometry::LineString { coordinates } => { + self.path(coordinates, false); + } + Geometry::MultiLineString { coordinates } => { + for line in coordinates { + self.path(line, false); + } + } + Geometry::Polygon { coordinates } => self.polygon(coordinates), + Geometry::MultiPolygon { coordinates } => { + for rings in coordinates { + self.polygon(rings); + } + } + Geometry::GeometryCollection { geometries } => { + for member in geometries { + self.geometry(member); + } + } + } + } +} + +fn parts_of(g: &SridGeometry, to_xyz: ToCartesian<'_>) -> Parts3 { + let mut builder = Builder { + g, + to_xyz, + index: 0, + parts: Parts3::default(), + }; + builder.geometry(g.geometry()); + builder.parts +} + +/// Minimum 3D distance between two geometries, metres. Infinite when +/// either is EMPTY. +pub(crate) fn distance_3d(a: &SridGeometry, b: &SridGeometry, to_xyz: ToCartesian<'_>) -> f64 { + let pa = parts_of(a, to_xyz); + let pb = parts_of(b, to_xyz); + if pa.segments.is_empty() || pb.segments.is_empty() { + return f64::INFINITY; + } + let mut best = f64::INFINITY; + for &(a1, a2) in &pa.segments { + for &(b1, b2) in &pb.segments { + best = best.min(segment_segment(a1, a2, b1, b2)); + } + } + best.min(surface_distance(&pa, &pb)) + .min(surface_distance(&pb, &pa)) +} + +/// Least distance from a vertex of `points` lying over a surface of +/// `surfaces` to that surface's plane. +fn surface_distance(surfaces: &Parts3, points: &Parts3) -> f64 { + let mut best = f64::INFINITY; + for surface in &surfaces.surfaces { + let Some((origin, normal)) = surface.plane else { + continue; + }; + for (xy, p) in &points.vertices { + if covers(&surface.rings, *xy) { + best = best.min(dot(sub(*p, origin), normal).abs()); + } + } + } + best +} + +fn covers(rings: &[Vec<[f64; 2]>], pt: [f64; 2]) -> bool { + let Some(exterior) = rings.first() else { + return false; + }; + if !point_in_polygon(pt[0], pt[1], exterior) && !point_on_ring_boundary(pt, exterior) { + return false; + } + !rings[1..] + .iter() + .any(|hole| point_in_polygon(pt[0], pt[1], hole) && !point_on_ring_boundary(pt, hole)) +} + +/// The plane through the first three non-collinear points. +fn plane_of(points: &[[f64; 3]]) -> Option<([f64; 3], [f64; 3])> { + let origin = *points.first()?; + for (i, b) in points.iter().enumerate().skip(1) { + for c in points.iter().skip(i + 1) { + let n = cross(sub(*b, origin), sub(*c, origin)); + let len = norm(n); + if len > COLLINEAR_EPS * norm(sub(*b, origin)).max(1.0) { + return Some((origin, [n[0] / len, n[1] / len, n[2] / len])); + } + } + } + None +} + +/// Total 3D length of every linear component, metres. +pub(crate) fn length_3d(g: &SridGeometry, to_xyz: ToCartesian<'_>) -> f64 { + let mut index = 0usize; + let mut point = |c: [f64; 2]| { + let (z, _) = g.ordinates().at(index); + index += 1; + to_xyz(c, z.unwrap_or(0.0)) + }; + let mut total = 0.0; + walk_lines(g.geometry(), &mut |coords, linear| { + let points: Vec<[f64; 3]> = coords.iter().map(|c| point(*c)).collect(); + if linear { + total += points + .windows(2) + .map(|w| norm(sub(w[1], w[0]))) + .sum::(); + } + }); + total +} + +/// Visit every coordinate path in traversal order, flagging the linear +/// ones. Points and rings are visited so the Z index stays aligned. +fn walk_lines(geom: &Geometry, f: &mut dyn FnMut(&[[f64; 2]], bool)) { + match geom { + Geometry::Point { .. } if geom.is_empty() => {} + Geometry::Point { coordinates } => f(std::slice::from_ref(coordinates), false), + Geometry::MultiPoint { coordinates } => f(coordinates, false), + Geometry::LineString { coordinates } => f(coordinates, true), + Geometry::MultiLineString { coordinates } => { + coordinates.iter().for_each(|line| f(line, true)); + } + Geometry::Polygon { coordinates } => coordinates.iter().for_each(|ring| f(ring, false)), + Geometry::MultiPolygon { coordinates } => { + coordinates.iter().flatten().for_each(|ring| f(ring, false)) + } + Geometry::GeometryCollection { geometries } => { + geometries.iter().for_each(|member| walk_lines(member, f)); + } + } +} + +/// Distance between segments p1–q1 and p2–q2 (Ericson, Real-Time Collision +/// Detection, 5.1.9). +fn segment_segment(p1: [f64; 3], q1: [f64; 3], p2: [f64; 3], q2: [f64; 3]) -> f64 { + let d1 = sub(q1, p1); + let d2 = sub(q2, p2); + let r = sub(p1, p2); + let a = dot(d1, d1); + let e = dot(d2, d2); + let f = dot(d2, r); + let tiny = f64::EPSILON; + let (s, t) = if a <= tiny && e <= tiny { + (0.0, 0.0) + } else if a <= tiny { + (0.0, (f / e).clamp(0.0, 1.0)) + } else { + let c = dot(d1, r); + if e <= tiny { + ((-c / a).clamp(0.0, 1.0), 0.0) + } else { + let b = dot(d1, d2); + let denom = a * e - b * b; + let mut s = if denom > tiny { + ((b * f - c * e) / denom).clamp(0.0, 1.0) + } else { + 0.0 + }; + let mut t = (b * s + f) / e; + if t < 0.0 { + t = 0.0; + s = (-c / a).clamp(0.0, 1.0); + } else if t > 1.0 { + t = 1.0; + s = ((b - c) / a).clamp(0.0, 1.0); + } + (s, t) + } + }; + let c1 = add(p1, scale(d1, s)); + let c2 = add(p2, scale(d2, t)); + norm(sub(c1, c2)) +} + +fn sub(a: [f64; 3], b: [f64; 3]) -> [f64; 3] { + [a[0] - b[0], a[1] - b[1], a[2] - b[2]] +} + +fn add(a: [f64; 3], b: [f64; 3]) -> [f64; 3] { + [a[0] + b[0], a[1] + b[1], a[2] + b[2]] +} + +fn scale(a: [f64; 3], k: f64) -> [f64; 3] { + [a[0] * k, a[1] * k, a[2] * k] +} + +fn dot(a: [f64; 3], b: [f64; 3]) -> f64 { + a[0] * b[0] + a[1] * b[1] + a[2] * b[2] +} + +fn cross(a: [f64; 3], b: [f64; 3]) -> [f64; 3] { + [ + a[1] * b[2] - a[2] * b[1], + a[2] * b[0] - a[0] * b[2], + a[0] * b[1] - a[1] * b[0], + ] +} + +fn norm(a: [f64; 3]) -> f64 { + dot(a, a).sqrt() +} + +#[cfg(test)] +mod tests { + use super::*; + use nodedb_types::geometry::{Ordinates, Srid}; + + fn planar(c: [f64; 2], z: f64) -> [f64; 3] { + [c[0], c[1], z] + } + + fn with_z(g: Geometry, z: Vec) -> SridGeometry { + SridGeometry::with_ordinates(Srid::WGS84, g, Ordinates::new(Some(z), None)).unwrap() + } + + #[test] + fn points_measure_the_3d_chord() { + let a = with_z(Geometry::point(0.0, 0.0), vec![0.0]); + let b = with_z(Geometry::point(3.0, 0.0), vec![4.0]); + assert!((distance_3d(&a, &b, &planar) - 5.0).abs() < 1e-12); + } + + #[test] + fn point_above_a_polygon_measures_to_its_plane() { + let square = with_z( + Geometry::polygon(vec![vec![ + [0.0, 0.0], + [10.0, 0.0], + [10.0, 10.0], + [0.0, 10.0], + [0.0, 0.0], + ]]), + vec![2.0; 5], + ); + let above = with_z(Geometry::point(5.0, 5.0), vec![9.0]); + assert!((distance_3d(&square, &above, &planar) - 7.0).abs() < 1e-12); + } + + #[test] + fn crossing_segments_at_different_heights() { + let a = with_z( + Geometry::line_string(vec![[-1.0, 0.0], [1.0, 0.0]]), + vec![0.0, 0.0], + ); + let b = with_z( + Geometry::line_string(vec![[0.0, -1.0], [0.0, 1.0]]), + vec![3.0, 3.0], + ); + assert!((distance_3d(&a, &b, &planar) - 3.0).abs() < 1e-12); + } + + #[test] + fn length_counts_z() { + let line = with_z( + Geometry::line_string(vec![[0.0, 0.0], [3.0, 0.0], [3.0, 0.0]]), + vec![0.0, 4.0, 10.0], + ); + assert!((length_3d(&line, &planar) - 11.0).abs() < 1e-12); + } +} diff --git a/nodedb-spatial/src/error.rs b/nodedb-spatial/src/error.rs new file mode 100644 index 000000000..51789bf20 --- /dev/null +++ b/nodedb-spatial/src/error.rs @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! The error every fallible spatial operation returns. + +use nodedb_crs::{CrsError, UnknownSrid}; +use nodedb_types::geometry::{GeoJsonError, OrdinateError}; + +use crate::ops::linear::LinearError; +use crate::ops::tile::TileError; +use crate::wkb::WkbError; +use crate::wkt::WktError; + +/// Why a geometry does not parse, or an operation on it fails. +#[derive(Debug, Clone, PartialEq, thiserror::Error)] +pub enum SpatialError { + #[error(transparent)] + Wkt(#[from] WktError), + #[error(transparent)] + Wkb(#[from] WkbError), + #[error(transparent)] + GeoJson(#[from] GeoJsonError), + #[error("invalid ordinates: {0}")] + Ordinates(#[from] OrdinateError), + /// An SRID code no CRS defines, or outside the code range. + #[error(transparent)] + Srid(#[from] UnknownSrid), + /// A local projection or a transform fails. + #[error(transparent)] + Crs(#[from] CrsError), + /// `ST_MakeValid` cannot repair the input. + #[error("cannot repair geometry: {reason}")] + Repair { reason: Box }, + /// A DE-9IM pattern is not 9 characters of `0 1 2 T F *`. + #[error("invalid DE-9IM pattern '{pattern}': {reason}")] + Pattern { pattern: Box, reason: Box }, + #[error(transparent)] + Linear(#[from] LinearError), + #[error(transparent)] + Tile(#[from] TileError), + /// A value of a type no geometry form takes. + #[error("a {found} value is not a geometry")] + NotGeometryValue { found: &'static str }, + /// The operation has no meaning for these geometry kinds. + #[error("{operation} does not take {kinds}")] + Unsupported { + operation: &'static str, + kinds: Box, + }, +} diff --git a/nodedb-spatial/src/geo_meta.rs b/nodedb-spatial/src/geo_meta.rs index 037416c2e..6a129c187 100644 --- a/nodedb-spatial/src/geo_meta.rs +++ b/nodedb-spatial/src/geo_meta.rs @@ -13,8 +13,77 @@ //! - GeoParquet spec: https://geoparquet.org/releases/v1.1.0/ //! - GeoArrow spec: https://geoarrow.org/extension-types +use nodedb_crs::CrsDef; +use nodedb_types::geometry::{Dimension, GeometryKind, SridGeometry}; use serde::{Deserialize, Serialize}; -use std::collections::HashMap; +use std::collections::{BTreeSet, HashMap}; + +/// Why geometry column metadata cannot be built. +#[derive(Debug, Clone, PartialEq, thiserror::Error)] +pub enum GeoMetaError { + /// The CRS writer produced text that is not JSON. + #[error("PROJJSON for SRID {code} does not parse: {reason}")] + Projjson { code: u32, reason: Box }, +} + +/// The CRS of `def` as a PROJJSON value. +pub fn projjson(def: &CrsDef) -> Result { + sonic_rs::from_str(&def.to_projjson()).map_err(|e| GeoMetaError::Projjson { + code: def.srid.code(), + reason: e.to_string().into(), + }) +} + +/// A GeoParquet geometry type name: the OGC name with its dimension +/// suffix, `"Point"`, `"Point Z"`, `"Point M"` or `"Point ZM"`. +pub fn geometry_type_name(kind: GeometryKind, dimension: Dimension) -> String { + let suffix = dimension.wkt_tag(); + if suffix.is_empty() { + kind.name().to_string() + } else { + format!("{} {suffix}", kind.name()) + } +} + +/// The distinct geometry type names of a column, sorted. +pub fn column_geometry_types<'a>(geoms: impl IntoIterator) -> Vec { + geoms + .into_iter() + .map(|g| geometry_type_name(g.geometry().kind(), g.dimension())) + .collect::>() + .into_iter() + .collect() +} + +/// The bounding box of a column: `[xmin, ymin, xmax, ymax]`, or +/// `[xmin, ymin, zmin, xmax, ymax, zmax]` when every geometry has Z. EMPTY +/// geometries add nothing. `None` when nothing has a coordinate. +pub fn column_bbox<'a>(geoms: impl IntoIterator) -> Option> { + let mut lo = [f64::INFINITY; 3]; + let mut hi = [f64::NEG_INFINITY; 3]; + let mut all_z = true; + let mut any = false; + for g in geoms { + all_z &= g.has_z() || g.is_empty(); + for p in g.positions() { + any = true; + for (i, v) in [p.x, p.y, p.z.unwrap_or(f64::NAN)].into_iter().enumerate() { + if !v.is_nan() { + lo[i] = lo[i].min(v); + hi[i] = hi[i].max(v); + } + } + } + } + if !any { + return None; + } + Some(if all_z { + vec![lo[0], lo[1], lo[2], hi[0], hi[1], hi[2]] + } else { + vec![lo[0], lo[1], hi[0], hi[1]] + }) +} /// GeoParquet metadata for a Parquet file containing geometry columns. /// @@ -32,43 +101,39 @@ pub struct GeoParquetMetadata { /// Metadata for a single geometry column in GeoParquet. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GeoParquetColumnMeta { - /// Encoding: "WKB" (default). + /// Encoding: "WKB" (ISO WKB, Z and M in the type code). pub encoding: String, - /// Geometry types present in this column. + /// Geometry types present, dimension suffix included. pub geometry_types: Vec, - /// Coordinate Reference System. "EPSG:4326" for WGS-84. + /// The column CRS as PROJJSON. pub crs: serde_json::Value, - /// Bounding box of all geometries: [min_lng, min_lat, max_lng, max_lat]. + /// Bounding box: 4 values, or 6 when every geometry has Z. #[serde(skip_serializing_if = "Option::is_none")] - pub bbox: Option<[f64; 4]>, + pub bbox: Option>, } impl GeoParquetMetadata { - /// Create metadata for a single geometry column. - pub fn single_column( + /// Metadata for one geometry column of `geoms` in `crs`. + pub fn single_column<'a>( column_name: &str, - geometry_types: Vec, - bbox: Option<[f64; 4]>, - ) -> Self { + geoms: impl IntoIterator + Clone, + crs: &CrsDef, + ) -> Result { let mut columns = HashMap::new(); columns.insert( column_name.to_string(), GeoParquetColumnMeta { encoding: "WKB".to_string(), - geometry_types, - crs: serde_json::json!({ - "type": "GeographicCRS", - "name": "WGS 84", - "id": { "authority": "EPSG", "code": 4326 } - }), - bbox, + geometry_types: column_geometry_types(geoms.clone()), + crs: projjson(crs)?, + bbox: column_bbox(geoms), }, ); - Self { + Ok(Self { version: "1.1.0".to_string(), primary_column: column_name.to_string(), columns, - } + }) } /// Serialize to JSON string for Parquet file metadata. @@ -80,35 +145,27 @@ impl GeoParquetMetadata { pub const PARQUET_KEY: &'static str = "geo"; } -/// GeoArrow extension type name for WKB-encoded geometry columns. +/// GeoArrow extension type name for WKB-encoded geometry columns. Z and M +/// travel in the WKB type code. /// /// Register this on Arrow `DataType::Binary` columns so Arrow-native tools /// (DuckDB, GeoPolars) recognize them as geometry. pub const GEOARROW_EXTENSION_NAME: &str = "geoarrow.wkb"; -/// GeoArrow extension metadata (JSON). +/// GeoArrow extension metadata (JSON): `{"crs": }`. /// /// Stored in Arrow schema's field metadata under the key /// `ARROW:extension:metadata`. -pub fn geoarrow_extension_metadata(crs_epsg: u32) -> String { - serde_json::json!({ - "crs": { - "type": "GeographicCRS", - "name": "WGS 84", - "id": { "authority": "EPSG", "code": crs_epsg } - } - }) - .to_string() +pub fn geoarrow_extension_metadata(crs: &CrsDef) -> Result { + Ok(serde_json::json!({ "crs": projjson(crs)? }).to_string()) } /// Arrow field metadata keys for extension types. pub const ARROW_EXTENSION_NAME_KEY: &str = "ARROW:extension:name"; pub const ARROW_EXTENSION_METADATA_KEY: &str = "ARROW:extension:metadata"; -/// Build Arrow field metadata for a WKB geometry column. -/// -/// Returns a HashMap to set on the Arrow Field's metadata. -pub fn geoarrow_field_metadata() -> HashMap { +/// Arrow field metadata for a WKB geometry column in `crs`. +pub fn geoarrow_field_metadata(crs: &CrsDef) -> Result, GeoMetaError> { let mut meta = HashMap::new(); meta.insert( ARROW_EXTENSION_NAME_KEY.to_string(), @@ -116,52 +173,88 @@ pub fn geoarrow_field_metadata() -> HashMap { ); meta.insert( ARROW_EXTENSION_METADATA_KEY.to_string(), - geoarrow_extension_metadata(4326), + geoarrow_extension_metadata(crs)?, ); - meta + Ok(meta) } #[cfg(test)] mod tests { use super::*; + use nodedb_crs::CrsContext; + use nodedb_types::geometry::{Geometry, Ordinates, Srid}; + + fn def(code: u32) -> std::sync::Arc { + CrsContext::builtin_only() + .resolve(Srid::new(code).unwrap()) + .unwrap() + } + + fn point_z(x: f64, y: f64, z: f64) -> SridGeometry { + SridGeometry::with_ordinates( + Srid::WGS84, + Geometry::point(x, y), + Ordinates::new(Some(vec![z]), None), + ) + .unwrap() + } + + #[test] + fn type_names_carry_the_dimension_suffix() { + assert_eq!( + geometry_type_name(GeometryKind::Point, Dimension::Xy), + "Point" + ); + assert_eq!( + geometry_type_name(GeometryKind::Point, Dimension::Xyz), + "Point Z" + ); + assert_eq!( + geometry_type_name(GeometryKind::Polygon, Dimension::Xym), + "Polygon M" + ); + assert_eq!( + geometry_type_name(GeometryKind::LineString, Dimension::Xyzm), + "LineString ZM" + ); + } #[test] fn geoparquet_metadata_json() { - let meta = GeoParquetMetadata::single_column( - "geom", - vec!["Point".to_string(), "Polygon".to_string()], - Some([-180.0, -90.0, 180.0, 90.0]), + let geoms = [point_z(1.0, 2.0, 3.0), point_z(4.0, 5.0, 6.0)]; + let meta = GeoParquetMetadata::single_column("geom", geoms.iter(), &def(4326)).unwrap(); + let col = &meta.columns["geom"]; + assert_eq!(col.geometry_types, vec!["Point Z".to_string()]); + assert_eq!(col.bbox, Some(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0])); + assert!( + col.crs.get("id").is_some() || col.crs.get("name").is_some(), + "{}", + col.crs ); let json = meta.to_json().unwrap(); assert!(json.contains("\"version\":\"1.1.0\"")); - assert!(json.contains("\"primary_column\":\"geom\"")); assert!(json.contains("\"encoding\":\"WKB\"")); - assert!(json.contains("EPSG")); + let parsed: GeoParquetMetadata = sonic_rs::from_str(&json).unwrap(); + assert_eq!(parsed.primary_column, "geom"); } #[test] - fn geoparquet_key() { - assert_eq!(GeoParquetMetadata::PARQUET_KEY, "geo"); + fn mixed_dimensions_give_a_2d_bbox() { + let geoms = [ + point_z(1.0, 2.0, 3.0), + SridGeometry::wgs84(Geometry::point(-1.0, 0.0)), + ]; + assert_eq!(column_bbox(geoms.iter()), Some(vec![-1.0, 0.0, 1.0, 2.0])); + assert_eq!( + column_geometry_types(geoms.iter()), + vec!["Point".to_string(), "Point Z".to_string()] + ); } #[test] - fn geoarrow_field_meta() { - let meta = geoarrow_field_metadata(); + fn geoarrow_field_meta_holds_projjson() { + let meta = geoarrow_field_metadata(&def(32648)).unwrap(); assert_eq!(meta[ARROW_EXTENSION_NAME_KEY], "geoarrow.wkb"); - assert!(meta[ARROW_EXTENSION_METADATA_KEY].contains("EPSG")); - } - - #[test] - fn geoarrow_extension_name() { - assert_eq!(GEOARROW_EXTENSION_NAME, "geoarrow.wkb"); - } - - #[test] - fn roundtrip_parquet_metadata() { - let meta = GeoParquetMetadata::single_column("location", vec!["Point".into()], None); - let json = meta.to_json().unwrap(); - let parsed: GeoParquetMetadata = sonic_rs::from_str(&json).unwrap(); - assert_eq!(parsed.primary_column, "location"); - assert_eq!(parsed.columns["location"].encoding, "WKB"); + assert!(meta[ARROW_EXTENSION_METADATA_KEY].contains("32648")); } } diff --git a/nodedb-spatial/src/geodesic/measure.rs b/nodedb-spatial/src/geodesic/measure.rs new file mode 100644 index 000000000..eab1cdb26 --- /dev/null +++ b/nodedb-spatial/src/geodesic/measure.rs @@ -0,0 +1,268 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Geodesic length, perimeter, area and distance on a CRS's own ellipsoid. +//! +//! Coordinates are x = longitude, y = latitude, in degrees. Every edge is +//! a geodesic. Results are in metres (the units of the semi-major axis) +//! and square metres. + +use nodedb_crs::Ellipsoid; +use nodedb_crs::method::geodesic::Geodesic; +use nodedb_types::geometry::Geometry; + +use super::segment::{point_distance, point_segment_distance}; +use crate::parts::Parts; +use crate::predicates::edge::{ring_edges, segment_to_segment_dist_sq}; + +/// Squared degree distance under which two edges count as touching. +const TOUCH_DEG_SQ: f64 = 1e-20; + +/// Edge pairs whose approximate distance is within this factor of the +/// smallest approximate distance go through the exact geodesic refine. +const REFINE_FACTOR: f64 = 2.0; + +/// Edge pairs closer than this many metres (approximate) always refine. +const REFINE_FLOOR_M: f64 = 1_000.0; + +/// Upper bound on metres per degree, used to scale the planar degree +/// distance that ranks edge pairs. +const METRES_PER_DEGREE_MAX: f64 = 111_700.0; + +/// Floor on cos(latitude) so ranking near a pole stays finite. +const MIN_COS_LAT: f64 = 0.01; + +/// Geodesic measures on one ellipsoid. +#[derive(Debug, Clone, PartialEq)] +pub struct GeodesicMeasure { + geodesic: Geodesic, + ellipsoid: Ellipsoid, +} + +impl GeodesicMeasure { + pub fn new(ellipsoid: Ellipsoid) -> Self { + Self { + geodesic: Geodesic::new(ellipsoid.a, ellipsoid.f()), + ellipsoid, + } + } + + pub fn ellipsoid(&self) -> &Ellipsoid { + &self.ellipsoid + } + + pub fn geodesic(&self) -> &Geodesic { + &self.geodesic + } + + /// Distance between two points. + pub fn point_distance(&self, a: [f64; 2], b: [f64; 2]) -> f64 { + point_distance(&self.geodesic, a, b) + } + + /// Total length of every linear component. Areal geometries and points + /// have no length, matching PostGIS. + pub fn length(&self, geom: &Geometry) -> f64 { + match geom { + Geometry::LineString { coordinates } => self.path_length(coordinates), + Geometry::MultiLineString { coordinates } => { + coordinates.iter().map(|l| self.path_length(l)).sum() + } + Geometry::GeometryCollection { geometries } => { + geometries.iter().map(|g| self.length(g)).sum() + } + _ => 0.0, + } + } + + /// Total length of every areal component's rings, holes included. An + /// unclosed ring is closed implicitly. + pub fn perimeter(&self, geom: &Geometry) -> f64 { + let ring = |r: &Vec<[f64; 2]>| -> f64 { + ring_edges(r) + .iter() + .map(|(a, b)| self.point_distance(*a, *b)) + .sum() + }; + match geom { + Geometry::Polygon { coordinates } => coordinates.iter().map(ring).sum(), + Geometry::MultiPolygon { coordinates } => coordinates.iter().flatten().map(ring).sum(), + Geometry::GeometryCollection { geometries } => { + geometries.iter().map(|g| self.perimeter(g)).sum() + } + _ => 0.0, + } + } + + /// Area of every areal component: exterior ring less its holes, never + /// negative. Points and lines have zero area. + pub fn area(&self, geom: &Geometry) -> f64 { + match geom { + Geometry::Polygon { coordinates } => self.polygon_area(coordinates), + Geometry::MultiPolygon { coordinates } => { + coordinates.iter().map(|p| self.polygon_area(p)).sum() + } + Geometry::GeometryCollection { geometries } => { + geometries.iter().map(|g| self.area(g)).sum() + } + _ => 0.0, + } + } + + /// Unsigned area enclosed by one ring. + pub fn ring_area(&self, ring: &[[f64; 2]]) -> f64 { + if ring.len() < 3 { + return 0.0; + } + let vertices: Vec<(f64, f64)> = ring.iter().map(|c| (c[1], c[0])).collect(); + self.geodesic.polygon_measure(&vertices).area.abs() + } + + fn polygon_area(&self, rings: &[Vec<[f64; 2]>]) -> f64 { + let mut iter = rings.iter(); + let Some(exterior) = iter.next() else { + return 0.0; + }; + let holes: f64 = iter.map(|r| self.ring_area(r)).sum(); + (self.ring_area(exterior) - holes).max(0.0) + } + + fn path_length(&self, path: &[[f64; 2]]) -> f64 { + path.windows(2) + .map(|w| self.point_distance(w[0], w[1])) + .sum() + } + + /// Minimum geodesic distance between two geometries. Zero when one + /// covers a vertex of the other or their edges cross. Infinite when + /// either is EMPTY. + /// + /// Edge pairs are ranked by a planar degree distance. The pairs near + /// the best rank go through the exact search: the distance from each + /// endpoint to the other geodesic edge. + pub fn distance(&self, a: &Geometry, b: &Geometry) -> f64 { + let pa = Parts::of(a); + let pb = Parts::of(b); + if pa.segments.is_empty() || pb.segments.is_empty() { + return f64::INFINITY; + } + if pa.covers_any_vertex_of(&pb) || pb.covers_any_vertex_of(&pa) { + return 0.0; + } + let mut ranked = Vec::with_capacity(pa.segments.len() * pb.segments.len()); + for &(a1, a2) in &pa.segments { + for &(b1, b2) in &pb.segments { + if segment_to_segment_dist_sq(a1, a2, b1, b2) < TOUCH_DEG_SQ { + return 0.0; + } + ranked.push((local_distance(a1, a2, b1, b2), (a1, a2), (b1, b2))); + } + } + let best = ranked + .iter() + .map(|(approx, _, _)| *approx) + .fold(f64::INFINITY, f64::min); + let limit = best * REFINE_FACTOR + REFINE_FLOOR_M; + ranked + .iter() + .filter(|(approx, _, _)| *approx <= limit) + .map(|&(_, (a1, a2), (b1, b2))| self.segment_segment(a1, a2, b1, b2)) + .fold(f64::INFINITY, f64::min) + } + + /// Distance between two non-crossing geodesic edges: the least of each + /// endpoint's distance to the other edge. + fn segment_segment(&self, a1: [f64; 2], a2: [f64; 2], b1: [f64; 2], b2: [f64; 2]) -> f64 { + let g = &self.geodesic; + point_segment_distance(g, a1, b1, b2) + .min(point_segment_distance(g, a2, b1, b2)) + .min(point_segment_distance(g, b1, a1, a2)) + .min(point_segment_distance(g, b2, a1, a2)) + } +} + +/// Approximate metres between two edges: the planar distance after +/// longitudes shrink by cos of the edges' mean latitude. +fn local_distance(a1: [f64; 2], a2: [f64; 2], b1: [f64; 2], b2: [f64; 2]) -> f64 { + let mean_lat = (a1[1] + a2[1] + b1[1] + b2[1]) / 4.0; + let k = mean_lat.to_radians().cos().max(MIN_COS_LAT); + let scale = |c: [f64; 2]| [c[0] * k, c[1]]; + segment_to_segment_dist_sq(scale(a1), scale(a2), scale(b1), scale(b2)).sqrt() + * METRES_PER_DEGREE_MAX +} + +#[cfg(test)] +mod tests { + use super::*; + + fn wgs84() -> GeodesicMeasure { + GeodesicMeasure::new(Ellipsoid::WGS84) + } + + fn square() -> Geometry { + Geometry::polygon(vec![vec![ + [0.0, 0.0], + [1.0, 0.0], + [1.0, 1.0], + [0.0, 1.0], + [0.0, 0.0], + ]]) + } + + /// pyproj 3.8.0 (PROJ 9.8.1) `Geod(ellps="WGS84")` reference values. + #[test] + fn values_match_pyproj() { + let m = wgs84(); + assert!((m.area(&square()) - 12_308_778_361.469_452).abs() < 1e-3); + assert!((m.perimeter(&square()) - 443_770.917_248_302).abs() < 1e-6); + let meridian = Geometry::line_string(vec![[0.0, 0.0], [0.0, 1.0]]); + assert!((m.length(&meridian) - 110_574.388_557_798_78).abs() < 1e-6); + let d = m.distance( + &Geometry::point(-74.006, 40.7128), + &Geometry::point(-0.1278, 51.5074), + ); + assert!((d - 5_585_233.578_931_3).abs() < 1e-3, "{d}"); + } + + #[test] + fn hole_is_subtracted() { + let m = wgs84(); + let with_hole = Geometry::polygon(vec![ + vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0], [0.0, 0.0]], + vec![ + [0.25, 0.25], + [0.75, 0.25], + [0.75, 0.75], + [0.25, 0.75], + [0.25, 0.25], + ], + ]); + assert!(m.area(&with_hole) < m.area(&square())); + } + + #[test] + fn point_inside_and_crossing_lines_are_zero() { + let m = wgs84(); + assert_eq!(m.distance(&square(), &Geometry::point(0.5, 0.5)), 0.0); + let line = Geometry::line_string(vec![[-1.0, 0.5], [2.0, 0.5]]); + assert_eq!(m.distance(&square(), &line), 0.0); + } + + #[test] + fn point_to_polygon_edge_is_geodesic() { + let m = wgs84(); + // Due east of the square's east edge, on the equator band. + let d = m.distance(&square(), &Geometry::point(1.1, 0.5)); + let direct = m.point_distance([1.1, 0.5], [1.0, 0.5]); + assert!((d - direct).abs() < 0.5, "{d} {direct}"); + } + + #[test] + fn empty_is_infinitely_far() { + let m = wgs84(); + assert!( + m.distance(&Geometry::empty_point(), &square()) + .is_infinite() + ); + assert_eq!(m.length(&Geometry::empty_point()), 0.0); + } +} diff --git a/nodedb-spatial/src/geodesic/mod.rs b/nodedb-spatial/src/geodesic/mod.rs new file mode 100644 index 000000000..a2f968095 --- /dev/null +++ b/nodedb-spatial/src/geodesic/mod.rs @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Geodesic measures on the ellipsoid of a geographic CRS. + +pub mod measure; +pub(crate) mod segment; + +pub use measure::GeodesicMeasure; diff --git a/nodedb-spatial/src/geodesic/segment.rs b/nodedb-spatial/src/geodesic/segment.rs new file mode 100644 index 000000000..3694297c5 --- /dev/null +++ b/nodedb-spatial/src/geodesic/segment.rs @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Distance from a point to a geodesic segment on an ellipsoid. + +use nodedb_crs::method::geodesic::{Geodesic, GeodesicLine}; + +/// Golden-section iterations. Each shrinks the bracket by 0.618, so 80 +/// reach below 1e-16 of the segment length. +const GOLDEN_ITERATIONS: usize = 80; + +/// Bracket width, metres, below which the search stops. +const GOLDEN_TOLERANCE_M: f64 = 1e-6; + +/// 1 / golden ratio. +const INV_PHI: f64 = 0.618_033_988_749_894_9; + +/// Geodesic distance between two (x = longitude, y = latitude) points, +/// in the units of the ellipsoid's semi-major axis. +pub(crate) fn point_distance(g: &Geodesic, a: [f64; 2], b: [f64; 2]) -> f64 { + g.inverse(a[1], a[0], b[1], b[0]).0 +} + +/// Shortest geodesic distance from `p` to the geodesic segment `a`–`b`. +/// +/// The distance from a point to the positions along a geodesic is +/// unimodal for segments shorter than half a meridian, so a golden-section +/// search over the arc length finds the foot of the perpendicular. +pub(crate) fn point_segment_distance(g: &Geodesic, p: [f64; 2], a: [f64; 2], b: [f64; 2]) -> f64 { + let (s12, azi1, _) = g.inverse(a[1], a[0], b[1], b[0]); + let to_a = point_distance(g, p, a); + if s12 <= GOLDEN_TOLERANCE_M { + return to_a; + } + let to_b = point_distance(g, p, b); + let line = GeodesicLine::new(g, a[1], a[0], azi1); + let at = |s: f64| { + let pos = line.position(s); + g.inverse(p[1], p[0], pos.lat2, pos.lon2).0 + }; + let (mut lo, mut hi) = (0.0, s12); + let mut x1 = hi - INV_PHI * (hi - lo); + let mut x2 = lo + INV_PHI * (hi - lo); + let mut f1 = at(x1); + let mut f2 = at(x2); + for _ in 0..GOLDEN_ITERATIONS { + if hi - lo <= GOLDEN_TOLERANCE_M { + break; + } + if f1 <= f2 { + hi = x2; + x2 = x1; + f2 = f1; + x1 = hi - INV_PHI * (hi - lo); + f1 = at(x1); + } else { + lo = x1; + x1 = x2; + f1 = f2; + x2 = lo + INV_PHI * (hi - lo); + f2 = at(x2); + } + } + to_a.min(to_b).min(f1).min(f2) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn wgs84() -> Geodesic { + let e = nodedb_crs::Ellipsoid::WGS84; + Geodesic::new(e.a, e.f()) + } + + /// A point north of the middle of an equatorial segment is one + /// meridian arc away: 0.1° of latitude at the equator is 11057.43 m. + #[test] + fn perpendicular_foot_on_the_equator() { + let g = wgs84(); + let d = point_segment_distance(&g, [0.5, 0.1], [0.0, 0.0], [1.0, 0.0]); + assert!((d - 11_057.43).abs() < 0.1, "{d}"); + } + + #[test] + fn beyond_an_end_measures_to_the_end() { + let g = wgs84(); + let d = point_segment_distance(&g, [2.0, 0.0], [0.0, 0.0], [1.0, 0.0]); + let e = point_distance(&g, [2.0, 0.0], [1.0, 0.0]); + assert!((d - e).abs() < 1e-6, "{d} {e}"); + } +} diff --git a/nodedb-spatial/src/hybrid.rs b/nodedb-spatial/src/hybrid.rs index d63f1932d..37f1c1d6b 100644 --- a/nodedb-spatial/src/hybrid.rs +++ b/nodedb-spatial/src/hybrid.rs @@ -23,8 +23,9 @@ use nodedb_mem::ScopedMemory; #[cfg(test)] use nodedb_types::BoundingBox; use nodedb_types::geometry::Geometry; -use nodedb_types::{BboxSpace, geometry_bbox_in}; +use nodedb_types::geometry_bbox_in; +use crate::metric::Metric; use crate::predicates; use crate::rtree::RTree; @@ -43,13 +44,16 @@ pub struct SpatialPreFilterResult { /// Execute the spatial pre-filter phase. /// -/// 1. Compute search bounding box from query geometry + optional distance expansion +/// 1. Compute search bounding box from query geometry, widened by +/// `distance_meters` under `metric` /// 2. R-tree range search → rough candidates /// 3. For each candidate, load geometry and apply exact predicate /// 4. Return filtered entry IDs /// /// The caller (vector engine) uses the candidate IDs to build a RoaringBitmap -/// that restricts HNSW graph traversal. +/// that restricts HNSW graph traversal. `metric` is the tree SRID's +/// metric: geodesic metres in a geographic CRS, grid metres in a projected +/// one. /// /// The `candidate_ids` allocation is budgeted via [`ScopedMemory::reserve`] /// before the `Vec::with_capacity`. Budget pressure is a backpressure @@ -58,15 +62,15 @@ pub fn spatial_prefilter( rtree: &RTree, query_geometry: &Geometry, distance_meters: Option, + metric: &Metric, exact_geometries: &dyn Fn(u64) -> Option, memory: &ScopedMemory, ) -> SpatialPreFilterResult { // Compute the search bbox in the tree's space. The query geometry is in // the tree's SRID: the caller rejects a mismatch before this point. - let space = rtree.space(); - let query_bbox = geometry_bbox_in(query_geometry, space); + let query_bbox = geometry_bbox_in(query_geometry, rtree.space()); let search_bbox = match distance_meters { - Some(dist) => query_bbox.expand_meters_in(dist, space), + Some(dist) => metric.widen(&query_bbox, dist), None => query_bbox, }; @@ -80,14 +84,9 @@ pub fn spatial_prefilter( let mut candidate_ids = Vec::with_capacity(rtree_candidates); for entry in &rtree_results { if let Some(doc_geom) = exact_geometries(entry.id) { - let passes = match (distance_meters, space) { - (Some(dist), BboxSpace::Geographic) => { - predicates::st_dwithin(&doc_geom, query_geometry, dist) - } - (Some(dist), BboxSpace::Planar) => { - crate::planar::planar_dwithin(&doc_geom, query_geometry, dist) - } - (None, _) => predicates::st_intersects(&doc_geom, query_geometry), + let passes = match distance_meters { + Some(dist) => metric.dwithin(&doc_geom, query_geometry, dist), + None => predicates::st_intersects(&doc_geom, query_geometry), }; if passes { candidate_ids.push(entry.id); @@ -141,7 +140,7 @@ mod tests { use crate::test_support::test_memory; fn make_tree() -> RTree { - let mut tree = RTree::new(test_memory(), nodedb_types::geometry::Srid::Wgs84); + let mut tree = RTree::new(test_memory(), crate::rtree::TreeSrid::WGS84); // 10 points in a grid from (0,0) to (9,9). for i in 0..10 { tree.insert(RTreeEntry { @@ -167,7 +166,14 @@ mod tests { }; // 200km radius should capture several nearby points. - let result = spatial_prefilter(&tree, &query, Some(200_000.0), &get_geom, &test_memory()); + let result = spatial_prefilter( + &tree, + &query, + Some(200_000.0), + &Metric::wgs84(), + &get_geom, + &test_memory(), + ); assert!(!result.candidate_ids.is_empty()); // Point (5,5) should definitely be in results (distance = 0). assert!(result.candidate_ids.contains(&5)); @@ -193,7 +199,14 @@ mod tests { } }; - let result = spatial_prefilter(&tree, &query, None, &get_geom, &test_memory()); + let result = spatial_prefilter( + &tree, + &query, + None, + &Metric::wgs84(), + &get_geom, + &test_memory(), + ); // Points 4, 5, 6 should be inside (3 is on edge → not contained by intersects returns true). assert!(result.candidate_ids.contains(&4)); assert!(result.candidate_ids.contains(&5)); @@ -207,7 +220,7 @@ mod tests { /// threshold in Euclidean metres. #[test] fn projected_prefilter_uses_planar_metres() { - let mut tree = RTree::new(test_memory(), nodedb_types::geometry::Srid::Utm48N); + let mut tree = RTree::new(test_memory(), crate::test_support::utm48n()); let points = [[500_000.0, 0.0], [500_300.0, 400.0], [510_000.0, 0.0]]; for (id, p) in points.iter().enumerate() { tree.insert(RTreeEntry { @@ -217,7 +230,16 @@ mod tests { } let get_geom = |id: u64| points.get(id as usize).map(|p| Geometry::point(p[0], p[1])); let query = Geometry::point(500_000.0, 0.0); - let result = spatial_prefilter(&tree, &query, Some(500.0), &get_geom, &test_memory()); + let result = spatial_prefilter( + &tree, + &query, + Some(500.0), + &Metric::Planar { + metres_per_unit: 1.0, + }, + &get_geom, + &test_memory(), + ); let mut ids = result.candidate_ids.clone(); ids.sort_unstable(); assert_eq!(ids, vec![0, 1]); diff --git a/nodedb-spatial/src/lib.rs b/nodedb-spatial/src/lib.rs index a68490a25..170327753 100644 --- a/nodedb-spatial/src/lib.rs +++ b/nodedb-spatial/src/lib.rs @@ -2,23 +2,29 @@ //! Spatial engine primitives shared by Origin, Lite, and WASM: R*-tree with //! bulk load and nearest-neighbor / range queries, geohash and H3 hex -//! indexes, OGC predicates (ST_Contains, ST_Intersects, ST_Within, -//! ST_DWithin, ST_Distance, etc.), WKB / WKT / GeoJSON interchange, and -//! hybrid spatial-vector composition. +//! indexes, OGC predicates and DE-9IM, geodesic measures on a CRS's own +//! ellipsoid, boolean overlay and buffer, WKB / WKT / GeoJSON interchange +//! with Z / M / ZM and EMPTY, and hybrid spatial-vector composition. //! //! Spatial collections are picked per-collection via `WITH (engine='spatial')` //! and use the columnar storage core; this crate provides the index -//! structures and predicate evaluators only. +//! structures and geometry algorithms only. pub mod crs; +pub mod distance3d; +pub mod error; pub mod geo_meta; +pub mod geodesic; pub mod geohash; pub mod geohash_index; pub mod h3; pub mod hybrid; pub mod measures; +pub mod metric; pub mod operations; +pub mod ops; pub mod orient; +pub(crate) mod parts; pub mod persist; pub mod planar; pub mod predicates; @@ -26,11 +32,15 @@ pub mod rtree; pub mod spatial_join; #[cfg(test)] mod test_support; +pub mod text; pub mod validate; +pub mod value; pub mod wkb; pub mod wkt; -pub use crs::{CrsError, transform_geometry}; +pub use crs::{transform_geometry, transform_geometry_at}; +pub use error::SpatialError; +pub use geodesic::GeodesicMeasure; pub use geohash::{geohash_decode, geohash_encode, geohash_neighbors}; pub use geohash_index::GeohashIndex; pub use h3::{ @@ -38,20 +48,29 @@ pub use h3::{ h3_to_boundary, h3_to_center, }; pub use hybrid::{SpatialPreFilterResult, bitmap_contains, ids_to_bitmap, spatial_prefilter}; -pub use measures::{st_area, st_centroid, st_centroid_in}; -pub use operations::{st_buffer, st_buffer_in, st_envelope, st_envelope_in, st_union}; +pub use measures::st_centroid; +pub use metric::Metric; +pub use operations::{st_envelope, st_envelope_in}; +pub use ops::{ + difference as st_difference, intersection as st_intersection, + sym_difference as st_symdifference, union as st_union, +}; pub use orient::orient_rings; pub use persist::{ RTreeCheckpointError, SpatialIndexMeta, SpatialIndexType, deserialize_meta, meta_storage_key, rtree_storage_key, serialize_meta, }; -pub use planar::{ - planar_area, planar_buffer, planar_distance, planar_dwithin, planar_length, planar_perimeter, +pub use planar::{planar_area, planar_distance, planar_dwithin, planar_length, planar_perimeter}; +pub use predicates::{st_contains, st_disjoint, st_distance, st_dwithin, st_intersects, st_within}; +pub use rtree::{RTree, RTreeEntry, SridMismatch, TreeSrid}; +pub use text::{SridCheck, geometry_from_bytes, geometry_from_text}; +pub use validate::{ValidityReason, is_valid, validate_geometry, validity_reason}; +pub use value::{ewkb_of_value, geometry_from_value, value_of_ewkb}; +pub use wkb::{ + ByteOrder, Wkb, WkbError, geometry_from_wkb, geometry_to_ewkb, geometry_to_ewkb_ordered, + geometry_to_wkb, geometry_to_wkb_ordered, }; -pub use predicates::{ - st_contains, st_disjoint, st_distance, st_dwithin, st_intersection, st_intersects, st_within, +pub use wkt::{ + Ewkt, WktError, geometry_from_ewkt, geometry_from_wkt, geometry_to_ewkt, geometry_to_wkt, + shape_to_wkt, }; -pub use rtree::{RTree, RTreeEntry, SridMismatch}; -pub use validate::{is_valid, validate_geometry}; -pub use wkb::{geometry_from_wkb, geometry_to_ewkb, geometry_to_wkb}; -pub use wkt::{Ewkt, geometry_from_ewkt, geometry_from_wkt, geometry_to_wkt}; diff --git a/nodedb-spatial/src/measures.rs b/nodedb-spatial/src/measures.rs index cb94c2ff8..c0d9709ff 100644 --- a/nodedb-spatial/src/measures.rs +++ b/nodedb-spatial/src/measures.rs @@ -1,115 +1,36 @@ // SPDX-License-Identifier: Apache-2.0 -//! Geodesic measures over geometry: area and centroid. +//! Centroid of a geometry. //! -//! Area uses the spherical-excess formula rather than a projected shoelace, -//! so it stays correct for polygons spanning arbitrary extents — the same -//! basis as [`crate::st_distance`], which is haversine. Centroid follows OGC -//! semantics: the result is the centroid of the highest-dimension components -//! present, so a `GeometryCollection` of a polygon and a stray point returns -//! the polygon's centroid, not a blend of the two. +//! The result is the centroid of the highest-dimension components present, +//! so a `GeometryCollection` of a polygon and a stray point returns the +//! polygon's centroid, not a blend of the two. Components weigh by their +//! area or length in the CRS's [`Metric`]: geodesic on a geographic CRS, +//! planar on a projected one. -use nodedb_types::BboxSpace; use nodedb_types::geometry::Geometry; -/// Mean Earth radius in meters, matching `nodedb_types::geometry`'s haversine. -const EARTH_RADIUS_M: f64 = 6_371_000.0; - -/// Geodesic area of a geometry in square meters. -/// -/// Areal geometries (`Polygon`, `MultiPolygon`) measure their exterior ring -/// minus their holes. Point and linear geometries have zero area, matching -/// PostGIS. A `GeometryCollection` sums the areas of its members. -pub fn st_area(geom: &Geometry) -> f64 { - match geom { - Geometry::Polygon { coordinates } => polygon_area(coordinates), - Geometry::MultiPolygon { coordinates } => { - coordinates.iter().map(|rings| polygon_area(rings)).sum() - } - Geometry::GeometryCollection { geometries } => geometries.iter().map(st_area).sum(), - Geometry::Point { .. } - | Geometry::LineString { .. } - | Geometry::MultiPoint { .. } - | Geometry::MultiLineString { .. } => 0.0, - // `Geometry` is `#[non_exhaustive]`. A geometry kind added upstream is - // not known to be areal, and reporting a fabricated area for it would - // be worse than reporting none. - _ => 0.0, - } -} - -/// Area of one polygon: exterior ring less every hole. -fn polygon_area(rings: &[Vec<[f64; 2]>]) -> f64 { - let mut iter = rings.iter(); - let Some(exterior) = iter.next() else { - return 0.0; - }; - let holes: f64 = iter.map(|ring| ring_area(ring)).sum(); - (ring_area(exterior) - holes).max(0.0) -} - -/// Unsigned area of a single closed ring via spherical excess. -/// -/// For a ring of vertices `(λᵢ, φᵢ)` in radians the enclosed area is -/// `R²/2 · |Σ (λᵢ₊₁ − λᵢ)(2 + sin φᵢ + sin φᵢ₊₁)|`. Longitude deltas are -/// normalized into `[-π, π]` so a ring crossing the antimeridian measures its -/// true extent instead of wrapping the long way around the globe. -fn ring_area(ring: &[[f64; 2]]) -> f64 { - if ring.len() < 3 { - return 0.0; - } - let mut total = 0.0; - for i in 0..ring.len() { - let a = ring[i]; - let b = ring[(i + 1) % ring.len()]; - let mut dlng = (b[0] - a[0]).to_radians(); - if dlng > std::f64::consts::PI { - dlng -= std::f64::consts::TAU; - } else if dlng < -std::f64::consts::PI { - dlng += std::f64::consts::TAU; - } - total += dlng * (2.0 + a[1].to_radians().sin() + b[1].to_radians().sin()); - } - (total * EARTH_RADIUS_M * EARTH_RADIUS_M / 2.0).abs() -} +use crate::metric::Metric; /// Centroid of a geometry, as a `Point`. /// /// Follows OGC dimension precedence: areal components win over linear, which /// win over punctual. Returns `None` only when the geometry carries no -/// vertices at all, or when every component of its winning dimension is -/// degenerate (zero area / zero length) — those fall back to the next -/// dimension down rather than returning nothing. -/// -/// Weights are geodesic (spherical area, haversine length). Same as -/// `st_centroid_in(geom, BboxSpace::Geographic)`. -pub fn st_centroid(geom: &Geometry) -> Option { - st_centroid_in(geom, BboxSpace::Geographic) +/// vertices at all. A winning dimension whose every component is degenerate +/// (zero area or zero length) falls back to the next dimension down. +pub fn st_centroid(geom: &Geometry, metric: &Metric) -> Option { + let [x, y] = centroid_coords(geom, metric)?; + Some(Geometry::point(x, y)) } -/// Centroid of a geometry in `space`, as a `Point`. -/// -/// Planar space weights components by planar area and Euclidean length, so -/// a projected geometry's centroid is in the same metres as its input. -pub fn st_centroid_in(geom: &Geometry, space: BboxSpace) -> Option { - let [lng, lat] = centroid_coords(geom, space)?; - Some(Geometry::point(lng, lat)) +/// Unsigned area of a ring, square metres. +fn ring_weight(ring: &[[f64; 2]], metric: &Metric) -> f64 { + metric.ring_area(ring) } -/// Unsigned area of a ring in `space`: square metres in either space. -fn ring_weight(ring: &[[f64; 2]], space: BboxSpace) -> f64 { - match space { - BboxSpace::Geographic => ring_area(ring), - BboxSpace::Planar => crate::planar::area::planar_ring_area(ring), - } -} - -/// Length of one segment in `space`, metres. -fn segment_weight(a: [f64; 2], b: [f64; 2], space: BboxSpace) -> f64 { - match space { - BboxSpace::Geographic => nodedb_types::geometry::haversine_distance(a[0], a[1], b[0], b[1]), - BboxSpace::Planar => (b[0] - a[0]).hypot(b[1] - a[1]), - } +/// Length of one segment, metres. +fn segment_weight(a: [f64; 2], b: [f64; 2], metric: &Metric) -> f64 { + metric.segment_length(a, b) } /// A weighted point accumulator: `(Σ w·lng, Σ w·lat, Σ w)`. @@ -142,55 +63,49 @@ impl Weighted { } } -fn centroid_coords(geom: &Geometry, space: BboxSpace) -> Option<[f64; 2]> { - areal_centroid(geom, space) +fn centroid_coords(geom: &Geometry, metric: &Metric) -> Option<[f64; 2]> { + areal_centroid(geom, metric) .and_then(Weighted::resolve) - .or_else(|| linear_centroid(geom, space).and_then(Weighted::resolve)) + .or_else(|| linear_centroid(geom, metric).and_then(Weighted::resolve)) .or_else(|| punctual_centroid(geom).and_then(Weighted::resolve)) } /// Area-weighted centroid of every areal component, or `None` if there are no /// areal components at all. -fn areal_centroid(geom: &Geometry, space: BboxSpace) -> Option { +fn areal_centroid(geom: &Geometry, metric: &Metric) -> Option { match geom { - Geometry::Polygon { coordinates } => Some(polygon_centroid(coordinates, space)), + Geometry::Polygon { coordinates } => Some(polygon_centroid(coordinates, metric)), Geometry::MultiPolygon { coordinates } => { let mut acc = Weighted::default(); for rings in coordinates { - acc.merge(polygon_centroid(rings, space)); + acc.merge(polygon_centroid(rings, metric)); } Some(acc) } - Geometry::GeometryCollection { geometries } => collect(geometries, space, areal_centroid), + Geometry::GeometryCollection { geometries } => collect(geometries, metric, areal_centroid), Geometry::Point { .. } | Geometry::LineString { .. } | Geometry::MultiPoint { .. } | Geometry::MultiLineString { .. } => None, - // `Geometry` is `#[non_exhaustive]`; an unknown kind is not areal, so - // it falls through to the linear and punctual dimensions below. - _ => None, } } /// Length-weighted centroid of every linear component. -fn linear_centroid(geom: &Geometry, space: BboxSpace) -> Option { +fn linear_centroid(geom: &Geometry, metric: &Metric) -> Option { match geom { - Geometry::LineString { coordinates } => Some(line_centroid(coordinates, space)), + Geometry::LineString { coordinates } => Some(line_centroid(coordinates, metric)), Geometry::MultiLineString { coordinates } => { let mut acc = Weighted::default(); for line in coordinates { - acc.merge(line_centroid(line, space)); + acc.merge(line_centroid(line, metric)); } Some(acc) } - Geometry::GeometryCollection { geometries } => collect(geometries, space, linear_centroid), + Geometry::GeometryCollection { geometries } => collect(geometries, metric, linear_centroid), Geometry::Point { .. } | Geometry::MultiPoint { .. } | Geometry::Polygon { .. } | Geometry::MultiPolygon { .. } => None, - // `Geometry` is `#[non_exhaustive]`; an unknown kind is not linear, so - // it falls through to the punctual dimension. - _ => None, } } @@ -208,13 +123,13 @@ fn punctual_centroid(geom: &Geometry) -> Option { /// when no member contributed at that dimension. fn collect( geometries: &[Geometry], - space: BboxSpace, - f: fn(&Geometry, BboxSpace) -> Option, + metric: &Metric, + f: fn(&Geometry, &Metric) -> Option, ) -> Option { let mut acc = Weighted::default(); let mut any = false; for member in geometries { - if let Some(part) = f(member, space) { + if let Some(part) = f(member, metric) { acc.merge(part); any = true; } @@ -224,23 +139,23 @@ fn collect( /// Centroid of one polygon, weighting the exterior ring positively and each /// hole negatively so the hole's mass is removed from the result. -fn polygon_centroid(rings: &[Vec<[f64; 2]>], space: BboxSpace) -> Weighted { +fn polygon_centroid(rings: &[Vec<[f64; 2]>], metric: &Metric) -> Weighted { let mut acc = Weighted::default(); let mut iter = rings.iter(); let Some(exterior) = iter.next() else { return acc; }; if let Some(point) = ring_centroid(exterior) { - acc.add(point, ring_weight(exterior, space)); + acc.add(point, ring_weight(exterior, metric)); } for hole in iter { if let Some(point) = ring_centroid(hole) { - acc.add(point, -ring_weight(hole, space)); + acc.add(point, -ring_weight(hole, metric)); } } - // Holes can only ever cancel the exterior down to nothing; a negative - // total weight would mean malformed input (holes larger than the shell), - // which must not flip the centroid to the far side of the polygon. + // Holes cancel at most the whole exterior. A negative total weight + // means malformed input (holes larger than the shell), and it must not + // flip the centroid to the far side of the polygon. if acc.weight <= 0.0 { return Weighted::default(); } @@ -275,108 +190,28 @@ fn ring_centroid(ring: &[[f64; 2]]) -> Option<[f64; 2]> { } /// Length-weighted centroid of a polyline: each segment contributes its -/// midpoint weighted by its length in `space`. -fn line_centroid(coords: &[[f64; 2]], space: BboxSpace) -> Weighted { +/// midpoint weighted by its length in `metric`. +fn line_centroid(coords: &[[f64; 2]], metric: &Metric) -> Weighted { let mut acc = Weighted::default(); for pair in coords.windows(2) { let (a, b) = (pair[0], pair[1]); - let length = segment_weight(a, b, space); + let length = segment_weight(a, b, metric); acc.add([(a[0] + b[0]) / 2.0, (a[1] + b[1]) / 2.0], length); } acc } /// Every vertex of a geometry, in document order. -fn vertices(geom: &Geometry) -> Vec<[f64; 2]> { - match geom { - Geometry::Point { coordinates } => vec![*coordinates], - Geometry::LineString { coordinates } | Geometry::MultiPoint { coordinates } => { - coordinates.clone() - } - Geometry::Polygon { coordinates } | Geometry::MultiLineString { coordinates } => { - coordinates.iter().flatten().copied().collect() - } - Geometry::MultiPolygon { coordinates } => { - coordinates.iter().flatten().flatten().copied().collect() - } - Geometry::GeometryCollection { geometries } => { - geometries.iter().flat_map(vertices).collect() - } - // `Geometry` is `#[non_exhaustive]`; an unknown kind exposes no - // coordinates this crate can read, so it contributes no vertices and - // the centroid of a geometry made only of such parts is `None`. - _ => Vec::new(), - } +fn vertices(geom: &Geometry) -> impl Iterator + '_ { + geom.coords() } #[cfg(test)] mod tests { use super::*; - /// One degree of latitude at the equator is ~111.19 km; a 1°×1° cell is - /// therefore ~1.236e10 m². Spherical excess must land within a fraction of - /// a percent of that. - #[test] - fn area_of_unit_degree_cell_at_equator() { - let poly = Geometry::polygon(vec![vec![ - [0.0, 0.0], - [1.0, 0.0], - [1.0, 1.0], - [0.0, 1.0], - [0.0, 0.0], - ]]); - let area = st_area(&poly); - let expected = 1.236e10; - assert!( - (area - expected).abs() / expected < 0.01, - "expected ~{expected} m², got {area}" - ); - } - - /// Winding order must not change the magnitude of the result. - #[test] - fn area_is_orientation_independent() { - let ccw = Geometry::polygon(vec![vec![ - [0.0, 0.0], - [1.0, 0.0], - [1.0, 1.0], - [0.0, 1.0], - [0.0, 0.0], - ]]); - let cw = Geometry::polygon(vec![vec![ - [0.0, 0.0], - [0.0, 1.0], - [1.0, 1.0], - [1.0, 0.0], - [0.0, 0.0], - ]]); - assert!((st_area(&ccw) - st_area(&cw)).abs() < 1.0); - } - - #[test] - fn area_subtracts_holes() { - let solid = Geometry::polygon(vec![vec![ - [0.0, 0.0], - [4.0, 0.0], - [4.0, 4.0], - [0.0, 4.0], - [0.0, 0.0], - ]]); - let with_hole = Geometry::polygon(vec![ - vec![[0.0, 0.0], [4.0, 0.0], [4.0, 4.0], [0.0, 4.0], [0.0, 0.0]], - vec![[1.0, 1.0], [2.0, 1.0], [2.0, 2.0], [1.0, 2.0], [1.0, 1.0]], - ]); - assert!( - st_area(&with_hole) < st_area(&solid), - "hole must reduce the measured area" - ); - } - - #[test] - fn area_of_non_areal_geometry_is_zero() { - let line = Geometry::line_string(vec![[0.0, 0.0], [1.0, 1.0]]); - assert_eq!(st_area(&line), 0.0); - assert_eq!(st_area(&Geometry::point(1.0, 2.0)), 0.0); + fn st_centroid(geom: &Geometry) -> Option { + super::st_centroid(geom, &Metric::wgs84()) } #[test] @@ -401,8 +236,8 @@ mod tests { assert!((coordinates[1] - 1.0).abs() < 1e-9); } - /// An L-shape's centroid must sit at the area-weighted center, which the - /// vertex mean would get wrong. + /// An L-shape's centroid sits at the area-weighted center, which the + /// vertex mean misses. #[test] fn centroid_is_area_weighted_not_vertex_mean() { let l_shape = Geometry::polygon(vec![vec![ @@ -419,7 +254,7 @@ mod tests { }; // A 4x1 arm (area 4, centroid (2, 0.5)) plus a 1x3 arm (area 3, // centroid (0.5, 2.5)) gives 9.5/7 ≈ 1.357 on both axes. The vertex - // mean would be 10/6 ≈ 1.667, so this discriminates the two. + // mean is 10/6 ≈ 1.667, so this discriminates the two. let expected = 9.5 / 7.0; assert!( (coordinates[0] - expected).abs() < 0.01 && (coordinates[1] - expected).abs() < 0.01, diff --git a/nodedb-spatial/src/metric.rs b/nodedb-spatial/src/metric.rs new file mode 100644 index 000000000..030235ef6 --- /dev/null +++ b/nodedb-spatial/src/metric.rs @@ -0,0 +1,320 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! The measure basis of a CRS: geodesic on its ellipsoid, or planar in its +//! linear unit. Every result is metres or square metres. + +use nodedb_crs::datum::geodetic_to_ecef; +use nodedb_crs::{CrsContext, CrsDef, CrsKind, Ellipsoid, UnknownSrid}; +use nodedb_types::geometry::{Geometry, Srid, SridGeometry}; +use nodedb_types::{BboxSpace, BoundingBox, geometry_bbox_in}; + +use crate::distance3d::{distance_3d, length_3d}; +use crate::geodesic::GeodesicMeasure; +use crate::planar; + +/// Smallest radius of curvature of any Earth ellipsoid in use, metres: the +/// WGS 84 meridional radius at the equator, a(1 − e²), rounded down. A box +/// widened with it covers every point within the distance. +const MIN_RADIUS_OF_CURVATURE_M: f64 = 6_335_000.0; + +/// Smallest distance from the Earth's centre to its surface, metres: the +/// WGS 84 polar radius 6356752 m, less 11 km for the deepest trench. +const MIN_SURFACE_RADIUS_M: f64 = 6_345_000.0; + +/// How a CRS measures. +#[derive(Debug, Clone, PartialEq)] +pub enum Metric { + /// A geographic CRS: geodesics on its ellipsoid. + Geodesic(Box), + /// A projected CRS: Euclidean in the grid, scaled to metres. + Planar { metres_per_unit: f64 }, +} + +impl Metric { + /// The metric of `def`. + pub fn of(def: &CrsDef) -> Self { + match def.kind { + CrsKind::Geographic => { + Metric::Geodesic(Box::new(GeodesicMeasure::new(def.datum.ellipsoid))) + } + CrsKind::Projected => Metric::Planar { + metres_per_unit: def.unit.metres_per_unit, + }, + } + } + + /// The metric of `srid` in `ctx`. + pub fn resolve(ctx: &CrsContext<'_>, srid: Srid) -> Result { + ctx.resolve(srid).map(|def| Metric::of(&def)) + } + + /// Geodesics on WGS 84. + pub fn wgs84() -> Self { + Metric::Geodesic(Box::new(GeodesicMeasure::new(Ellipsoid::WGS84))) + } + + pub fn space(&self) -> BboxSpace { + match self { + Metric::Geodesic(_) => BboxSpace::Geographic, + Metric::Planar { .. } => BboxSpace::Planar, + } + } + + /// Metres in one coordinate unit of a projected CRS. `None` for a + /// geographic CRS, whose degrees have no fixed length. + pub fn metres_per_unit(&self) -> Option { + match self { + Metric::Geodesic(_) => None, + Metric::Planar { metres_per_unit } => Some(*metres_per_unit), + } + } + + /// Minimum distance in metres. Infinite when either is EMPTY. + pub fn distance(&self, a: &Geometry, b: &Geometry) -> f64 { + if a.is_empty() || b.is_empty() { + return f64::INFINITY; + } + match self { + Metric::Geodesic(m) => m.distance(a, b), + Metric::Planar { metres_per_unit } => planar::planar_distance(a, b) * metres_per_unit, + } + } + + /// Whether `a` and `b` lie within `metres`. Never true for an EMPTY + /// operand. A widened bbox rejects distant pairs before the exact + /// distance runs. + pub fn dwithin(&self, a: &Geometry, b: &Geometry, metres: f64) -> bool { + if a.is_empty() || b.is_empty() { + return false; + } + let a_box = self.widen(&geometry_bbox_in(a, self.space()), metres); + if !a_box.intersects(&geometry_bbox_in(b, self.space())) { + return false; + } + self.distance(a, b) <= metres + } + + /// Widen `bbox` so it covers every point within `metres` of it. + /// + /// A geographic box grows by the spherical cap on the smallest Earth + /// radius of curvature, measured at the box's poleward edge. A box + /// that reaches a pole spans every longitude. + pub fn widen(&self, bbox: &BoundingBox, metres: f64) -> BoundingBox { + match self { + Metric::Planar { metres_per_unit } => { + let units = metres / metres_per_unit; + BoundingBox::new( + bbox.min_lng - units, + bbox.min_lat - units, + bbox.max_lng + units, + bbox.max_lat + units, + ) + } + Metric::Geodesic(_) => widen_geographic(bbox, metres), + } + } + + /// Total length of every linear component, metres. + pub fn length(&self, geom: &Geometry) -> f64 { + match self { + Metric::Geodesic(m) => m.length(geom), + Metric::Planar { metres_per_unit } => planar::planar_length(geom) * metres_per_unit, + } + } + + /// Total boundary length of every areal component, metres. + pub fn perimeter(&self, geom: &Geometry) -> f64 { + match self { + Metric::Geodesic(m) => m.perimeter(geom), + Metric::Planar { metres_per_unit } => planar::planar_perimeter(geom) * metres_per_unit, + } + } + + /// Area, square metres. + pub fn area(&self, geom: &Geometry) -> f64 { + match self { + Metric::Geodesic(m) => m.area(geom), + Metric::Planar { metres_per_unit } => { + planar::planar_area(geom) * metres_per_unit * metres_per_unit + } + } + } + + /// Unsigned area enclosed by one ring, square metres. + pub fn ring_area(&self, ring: &[[f64; 2]]) -> f64 { + match self { + Metric::Geodesic(m) => m.ring_area(ring), + Metric::Planar { metres_per_unit } => { + planar::planar_ring_area(ring) * metres_per_unit * metres_per_unit + } + } + } + + /// Length of one edge, metres. + pub fn segment_length(&self, a: [f64; 2], b: [f64; 2]) -> f64 { + match self { + Metric::Geodesic(m) => m.point_distance(a, b), + Metric::Planar { metres_per_unit } => { + (b[0] - a[0]).hypot(b[1] - a[1]) * metres_per_unit + } + } + } + + /// One position in Cartesian metres: grid x, y scaled to metres and Z + /// for a projected CRS, ECEF on the ellipsoid for a geographic one. + pub fn cartesian(&self, c: [f64; 2], z: f64) -> [f64; 3] { + match self { + Metric::Geodesic(m) => { + geodetic_to_ecef(c[0].to_radians(), c[1].to_radians(), z, m.ellipsoid()) + } + Metric::Planar { metres_per_unit } => { + [c[0] * metres_per_unit, c[1] * metres_per_unit, z] + } + } + } + + /// Minimum 3D distance, metres: Euclidean in grid metres for a + /// projected CRS, the ECEF chord for a geographic one. Infinite when + /// either is EMPTY. + pub fn distance_3d(&self, a: &SridGeometry, b: &SridGeometry) -> f64 { + distance_3d(a, b, &|c, z| self.cartesian(c, z)) + } + + /// Whether `a` and `b` lie within `metres` in 3D. Never true for an + /// EMPTY operand. A widened bbox rejects distant pairs first: by + /// `metres` in a projected CRS, by the longest surface arc a chord of + /// `metres` spans in a geographic one. + pub fn dwithin_3d(&self, a: &SridGeometry, b: &SridGeometry, metres: f64) -> bool { + if a.is_empty() || b.is_empty() { + return false; + } + let reach = self.reach_3d(metres); + let a_box = self.widen(&geometry_bbox_in(a.geometry(), self.space()), reach); + if !a_box.intersects(&geometry_bbox_in(b.geometry(), self.space())) { + return false; + } + self.distance_3d(a, b) <= metres + } + + /// The horizontal distance a 3D distance of `metres` can span: itself + /// in a projected CRS, the arc of a chord of `metres` on the smallest + /// sphere a surface point lies on in a geographic one. + pub fn reach_3d(&self, metres: f64) -> f64 { + match self { + Metric::Planar { .. } => metres, + Metric::Geodesic(_) => { + let half = metres / (2.0 * MIN_SURFACE_RADIUS_M); + if half >= 1.0 { + std::f64::consts::PI * MIN_SURFACE_RADIUS_M + } else { + 2.0 * MIN_SURFACE_RADIUS_M * half.asin() + } + } + } + } + + /// Total 3D length of every linear component, metres. A geographic + /// edge is the chord between its ECEF endpoints. + pub fn length_3d(&self, g: &SridGeometry) -> f64 { + length_3d(g, &|c, z| self.cartesian(c, z)) + } +} + +fn widen_geographic(bbox: &BoundingBox, metres: f64) -> BoundingBox { + let angle = metres / MIN_RADIUS_OF_CURVATURE_M; + let lat_delta = angle.to_degrees(); + let min_lat = (bbox.min_lat - lat_delta).max(-90.0); + let max_lat = (bbox.max_lat + lat_delta).min(90.0); + let poleward = min_lat.abs().max(max_lat.abs()); + let cos_lat = poleward.to_radians().cos(); + let sin_angle = angle.min(std::f64::consts::FRAC_PI_2).sin(); + if poleward >= 90.0 || sin_angle >= cos_lat { + return BoundingBox::new(-180.0, min_lat, 180.0, max_lat); + } + let lng_delta = (sin_angle / cos_lat).asin().to_degrees(); + BoundingBox::new( + bbox.min_lng - lng_delta, + min_lat, + bbox.max_lng + lng_delta, + max_lat, + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn widen_covers_the_true_distance() { + let m = Metric::wgs84(); + let point = BoundingBox::new(10.0, 0.0, 10.0, 0.0); + let wide = m.widen(&point, 111_000.0); + // One degree of latitude at the equator is 110 574 m. + assert!(wide.max_lat >= 1.0, "{wide:?}"); + let north = BoundingBox::new(10.0, 70.0, 10.0, 70.0); + let wide = m.widen(&north, 100_000.0); + let east = m.segment_length([10.0, 70.0], [wide.max_lng, 70.0]); + assert!(east >= 100_000.0, "{east}"); + } + + #[test] + fn widen_reaching_a_pole_spans_every_longitude() { + let m = Metric::wgs84(); + let wide = m.widen(&BoundingBox::new(0.0, 89.5, 0.0, 89.5), 100_000.0); + assert_eq!((wide.min_lng, wide.max_lng), (-180.0, 180.0)); + } + + #[test] + fn planar_scales_by_the_unit() { + let feet = Metric::Planar { + metres_per_unit: 0.3048, + }; + let d = feet.distance(&Geometry::point(0.0, 0.0), &Geometry::point(1000.0, 0.0)); + assert!((d - 304.8).abs() < 1e-9); + assert!(feet.dwithin( + &Geometry::point(0.0, 0.0), + &Geometry::point(1000.0, 0.0), + 305.0 + )); + } + + #[test] + fn empty_operands_are_never_within() { + let m = Metric::wgs84(); + assert!(!m.dwithin(&Geometry::empty_point(), &Geometry::point(0.0, 0.0), 1e9)); + assert!( + m.distance(&Geometry::empty_point(), &Geometry::point(0.0, 0.0)) + .is_infinite() + ); + } + + /// A chord shorter than the arc it spans still passes the prefilter. + #[test] + fn dwithin_3d_reaches_past_the_chord() { + let m = Metric::wgs84(); + let a = SridGeometry::wgs84(Geometry::point(0.0, 0.0)); + let b = SridGeometry::wgs84(Geometry::point(30.0, 0.0)); + let chord = m.distance_3d(&a, &b); + let arc = m.distance(a.geometry(), b.geometry()); + assert!(chord < arc); + assert!(m.dwithin_3d(&a, &b, chord + 1.0)); + assert!(!m.dwithin_3d(&a, &b, chord - 1.0)); + } + + /// ECEF chord between two points 1 km apart in height on one spot. + #[test] + fn geographic_3d_distance_is_the_ecef_chord() { + use nodedb_types::geometry::Ordinates; + let m = Metric::wgs84(); + let at = |h: f64| { + SridGeometry::with_ordinates( + Srid::WGS84, + Geometry::point(101.7, 3.1), + Ordinates::new(Some(vec![h]), None), + ) + .unwrap() + }; + let d = m.distance_3d(&at(0.0), &at(1000.0)); + assert!((d - 1000.0).abs() < 1e-6, "{d}"); + } +} diff --git a/nodedb-spatial/src/operations.rs b/nodedb-spatial/src/operations.rs index d910c8cf2..b0de0ee38 100644 --- a/nodedb-spatial/src/operations.rs +++ b/nodedb-spatial/src/operations.rs @@ -1,490 +1,61 @@ // SPDX-License-Identifier: Apache-2.0 -//! Geometry construction and transformation operations. -//! -//! ST_Buffer, ST_Envelope, ST_Union — produce new geometries from existing ones. +//! `ST_Envelope`: the bounding box of a geometry as a geometry. use nodedb_types::geometry::Geometry; -use nodedb_types::{BboxSpace, BoundingBox, geometry_bbox_in}; +use nodedb_types::{BboxSpace, geometry_bbox_in}; -/// Metres per degree of latitude (WGS 84 meridian average). -const METRES_PER_DEGREE_LAT: f64 = 110_540.0; - -/// Metres per degree of longitude at the equator (WGS 84). -const METRES_PER_DEGREE_LNG_EQUATOR: f64 = 111_320.0; - -/// Floor on cos(latitude) so a buffer near a pole stays finite. -const MIN_COS_LAT: f64 = 0.001; - -/// Floor on a vector length before normalising it, so a degenerate edge -/// does not divide by zero. -const MIN_VECTOR_LEN: f64 = 1e-12; - -/// ST_Buffer(geom, distance_meters) → Polygon, in geographic space. -/// -/// Same as `st_buffer_in(geom, distance_meters, segments, BboxSpace::Geographic)`. -pub fn st_buffer(geom: &Geometry, distance_meters: f64, segments: usize) -> Geometry { - st_buffer_in(geom, distance_meters, segments, BboxSpace::Geographic) -} - -/// ST_Buffer(geom, distance_meters) → Polygon, in `space`. -/// -/// - Point → `segments`-sided circle polygon -/// - LineString → buffered corridor (parallel offset + end caps) -/// - Polygon → expanded polygon (vertex offset outward) -/// -/// Geographic space converts metres to degrees by equirectangular -/// approximation: `Δlng = meters / (111320 * cos(lat))`, -/// `Δlat = meters / 110540`, accurate to <0.5% within ±70° latitude. -/// Planar space adds metres directly. -pub fn st_buffer_in( - geom: &Geometry, - distance_meters: f64, - segments: usize, - space: BboxSpace, -) -> Geometry { - let segments = segments.max(4); - - match geom { - Geometry::Point { coordinates } => buffer_point( - coordinates[0], - coordinates[1], - distance_meters, - segments, - space, - ), - Geometry::LineString { coordinates } => { - buffer_linestring(coordinates, distance_meters, segments, space) - } - Geometry::Polygon { coordinates } => buffer_polygon(coordinates, distance_meters, space), - // Multi* types: buffer each component and collect. - Geometry::MultiPoint { coordinates } => { - let polys: Vec>> = coordinates - .iter() - .map(|pt| { - if let Geometry::Polygon { coordinates: rings } = - buffer_point(pt[0], pt[1], distance_meters, segments, space) - { - rings - } else { - vec![] - } - }) - .collect(); - Geometry::MultiPolygon { coordinates: polys } - } - _ => { - // For unsupported types, fall back to buffering the bbox. - let bb = geometry_bbox_in(geom, space).expand_meters_in(distance_meters, space); - bbox_to_polygon(&bb) - } +/// `ST_Envelope(geom)`: the bounding box in `space` as a polygon. A box of +/// zero width and height is a point, a box of zero width or height a line, +/// as PostGIS returns them. EMPTY stays EMPTY. +pub fn st_envelope_in(geom: &Geometry, space: BboxSpace) -> Geometry { + if geom.is_empty() { + return geom.clone(); } -} - -/// Coordinate deltas `(dlng, dlat)` that `meters` spans at latitude `lat`. -fn metre_deltas(meters: f64, lat: f64, space: BboxSpace) -> (f64, f64) { - match space { - BboxSpace::Geographic => ( - meters / (METRES_PER_DEGREE_LNG_EQUATOR * lat.to_radians().cos().max(MIN_COS_LAT)), - meters / METRES_PER_DEGREE_LAT, - ), - BboxSpace::Planar => (meters, meters), + let bb = geometry_bbox_in(geom, space); + let (lo, hi) = ([bb.min_lng, bb.min_lat], [bb.max_lng, bb.max_lat]); + match (lo[0] == hi[0], lo[1] == hi[1]) { + (true, true) => Geometry::point(lo[0], lo[1]), + (true, false) | (false, true) => Geometry::line_string(vec![lo, hi]), + (false, false) => Geometry::polygon(vec![vec![lo, [hi[0], lo[1]], hi, [lo[0], hi[1]], lo]]), } } -/// ST_Envelope(geom) → Polygon — bounding box as a polygon, in geographic -/// space. +/// [`st_envelope_in`] in geographic space. pub fn st_envelope(geom: &Geometry) -> Geometry { st_envelope_in(geom, BboxSpace::Geographic) } -/// ST_Envelope(geom) → Polygon — bounding box as a polygon, in `space`. -pub fn st_envelope_in(geom: &Geometry, space: BboxSpace) -> Geometry { - let bb = geometry_bbox_in(geom, space); - bbox_to_polygon(&bb) -} - -/// ST_Union(a, b) → Geometry — merge two geometries. -/// -/// For this version, collects into appropriate Multi* type or -/// GeometryCollection. True polygon clipping (Weiler-Atherton) is -/// deferred — this handles the common cases correctly. -pub fn st_union(a: &Geometry, b: &Geometry) -> Geometry { - match (a, b) { - // Point + Point → MultiPoint - (Geometry::Point { coordinates: ca }, Geometry::Point { coordinates: cb }) => { - Geometry::MultiPoint { - coordinates: vec![*ca, *cb], - } - } - // LineString + LineString → MultiLineString - (Geometry::LineString { coordinates: la }, Geometry::LineString { coordinates: lb }) => { - Geometry::MultiLineString { - coordinates: vec![la.clone(), lb.clone()], - } - } - // Polygon + Polygon → MultiPolygon - (Geometry::Polygon { coordinates: ra }, Geometry::Polygon { coordinates: rb }) => { - Geometry::MultiPolygon { - coordinates: vec![ra.clone(), rb.clone()], - } - } - // Same-type Multi* + element → extend - (Geometry::MultiPoint { coordinates: pts }, Geometry::Point { coordinates: pt }) => { - let mut coords = pts.clone(); - coords.push(*pt); - Geometry::MultiPoint { - coordinates: coords, - } - } - (Geometry::Point { coordinates: pt }, Geometry::MultiPoint { coordinates: pts }) => { - let mut coords = vec![*pt]; - coords.extend_from_slice(pts); - Geometry::MultiPoint { - coordinates: coords, - } - } - // Everything else → GeometryCollection - _ => Geometry::GeometryCollection { - geometries: vec![a.clone(), b.clone()], - }, - } -} - -// ── Buffer helpers ── - -/// Buffer a point into an approximate circle polygon. -fn buffer_point(lng: f64, lat: f64, meters: f64, segments: usize, space: BboxSpace) -> Geometry { - let (dlng, dlat) = metre_deltas(meters, lat, space); - - let mut ring = Vec::with_capacity(segments + 1); - for i in 0..segments { - let angle = 2.0 * std::f64::consts::PI * (i as f64) / (segments as f64); - ring.push([lng + dlng * angle.cos(), lat + dlat * angle.sin()]); - } - // Close the ring. - ring.push(ring[0]); - - Geometry::Polygon { - coordinates: vec![ring], - } -} - -/// Buffer a linestring by creating a corridor (parallel offset on both sides + end caps). -fn buffer_linestring( - line: &[[f64; 2]], - meters: f64, - segments: usize, - space: BboxSpace, -) -> Geometry { - if line.is_empty() { - return Geometry::Polygon { - coordinates: vec![vec![]], - }; - } - if line.len() == 1 { - return buffer_point(line[0][0], line[0][1], meters, segments, space); - } - - // Build the offset polygon: left side forward, right side backward, with - // semicircular end caps. - let mut left_side = Vec::new(); - let mut right_side = Vec::new(); - - for i in 0..line.len() - 1 { - let (nl, nr) = offset_segment(line[i], line[i + 1], meters, space); - left_side.push(nl.0); - left_side.push(nl.1); - right_side.push(nr.0); - right_side.push(nr.1); - } - - // Build polygon: left side forward → end cap → right side backward → start cap. - let mut ring = Vec::new(); - ring.extend_from_slice(&left_side); - - // End cap (semicircle around last point). - let last = line[line.len() - 1]; - let cap_segments = segments / 2; - let end_dir = direction(line[line.len() - 2], last); - for i in 0..=cap_segments { - let angle = end_dir - std::f64::consts::FRAC_PI_2 - + std::f64::consts::PI * (i as f64) / (cap_segments as f64); - let (dlng, dlat) = metre_deltas(meters, last[1], space); - ring.push([last[0] + dlng * angle.cos(), last[1] + dlat * angle.sin()]); - } - - // Right side backward. - for pt in right_side.iter().rev() { - ring.push(*pt); - } - - // Start cap (semicircle around first point). - let first = line[0]; - let start_dir = direction(first, line[1]); - for i in 0..=cap_segments { - let angle = start_dir - + std::f64::consts::FRAC_PI_2 - + std::f64::consts::PI * (i as f64) / (cap_segments as f64); - let (dlng, dlat) = metre_deltas(meters, first[1], space); - ring.push([first[0] + dlng * angle.cos(), first[1] + dlat * angle.sin()]); - } - - // Close. - if let Some(&first_pt) = ring.first() { - ring.push(first_pt); - } - - Geometry::Polygon { - coordinates: vec![ring], - } -} - -/// Buffer a polygon by offsetting each vertex outward. -fn buffer_polygon(rings: &[Vec<[f64; 2]>], meters: f64, space: BboxSpace) -> Geometry { - let mut new_rings = Vec::with_capacity(rings.len()); - - for (ring_idx, ring) in rings.iter().enumerate() { - if ring.len() < 3 { - new_rings.push(ring.clone()); - continue; - } - let is_exterior = ring_idx == 0; - // For exterior: expand outward. For holes: shrink inward. - let sign = if is_exterior { 1.0 } else { -1.0 }; - let offset_m = meters * sign; - - let mut new_ring = Vec::with_capacity(ring.len()); - let n = if ring.first() == ring.last() { - ring.len() - 1 - } else { - ring.len() - }; - - for i in 0..n { - let prev = ring[(i + n - 1) % n]; - let curr = ring[i]; - let next = ring[(i + 1) % n]; - - // Bisector direction (average of the two edge normals). - let n1 = edge_outward_normal(prev, curr); - let n2 = edge_outward_normal(curr, next); - let bisect = [(n1[0] + n2[0]) / 2.0, (n1[1] + n2[1]) / 2.0]; - let len = (bisect[0] * bisect[0] + bisect[1] * bisect[1]) - .sqrt() - .max(MIN_VECTOR_LEN); - let unit = [bisect[0] / len, bisect[1] / len]; - - let (dlng, dlat) = metre_deltas(offset_m, curr[1], space); - - new_ring.push([curr[0] + unit[0] * dlng, curr[1] + unit[1] * dlat]); - } - - // Close. - if let Some(&first_pt) = new_ring.first() { - new_ring.push(first_pt); - } - new_rings.push(new_ring); - } - - Geometry::Polygon { - coordinates: new_rings, - } -} - -/// Compute offset segments (left and right parallel lines at `meters` distance). -/// A pair of offset segments: (left_segment, right_segment). -type OffsetPair = (([f64; 2], [f64; 2]), ([f64; 2], [f64; 2])); - -fn offset_segment(a: [f64; 2], b: [f64; 2], meters: f64, space: BboxSpace) -> OffsetPair { - let normal = edge_outward_normal(a, b); - let (dlng, dlat) = metre_deltas(meters, (a[1] + b[1]) / 2.0, space); - - let left = ( - [a[0] + normal[0] * dlng, a[1] + normal[1] * dlat], - [b[0] + normal[0] * dlng, b[1] + normal[1] * dlat], - ); - let right = ( - [a[0] - normal[0] * dlng, a[1] - normal[1] * dlat], - [b[0] - normal[0] * dlng, b[1] - normal[1] * dlat], - ); - (left, right) -} - -/// Outward-pointing unit normal for an edge (a→b). -/// -/// For CCW rings (standard exterior), the outward direction is the right -/// normal (dy, -dx). This is correct because CCW traversal has the -/// interior on the left. -fn edge_outward_normal(a: [f64; 2], b: [f64; 2]) -> [f64; 2] { - let dx = b[0] - a[0]; - let dy = b[1] - a[1]; - let len = (dx * dx + dy * dy).sqrt().max(MIN_VECTOR_LEN); - // Right normal: (dy, -dx) normalized — outward for CCW rings. - [dy / len, -dx / len] -} - -/// Direction angle (radians) from a to b. -fn direction(a: [f64; 2], b: [f64; 2]) -> f64 { - (b[1] - a[1]).atan2(b[0] - a[0]) -} - -/// Convert a BoundingBox to a Polygon. -fn bbox_to_polygon(bb: &BoundingBox) -> Geometry { - Geometry::Polygon { - coordinates: vec![vec![ - [bb.min_lng, bb.min_lat], - [bb.max_lng, bb.min_lat], - [bb.max_lng, bb.max_lat], - [bb.min_lng, bb.max_lat], - [bb.min_lng, bb.min_lat], - ]], - } -} - #[cfg(test)] mod tests { use super::*; - use nodedb_types::geometry_bbox; #[test] - fn planar_buffer_point_is_a_metre_circle() { - let circle = st_buffer_in( - &Geometry::point(500_000.0, 100_000.0), - 250.0, - 16, - BboxSpace::Planar, + fn polygon_envelope_is_its_box() { + let poly = Geometry::polygon(vec![vec![[1.0, 2.0], [5.0, 3.0], [3.0, 8.0], [1.0, 2.0]]]); + assert_eq!( + st_envelope(&poly), + Geometry::polygon(vec![vec![ + [1.0, 2.0], + [5.0, 2.0], + [5.0, 8.0], + [1.0, 8.0], + [1.0, 2.0] + ]]) ); - let Geometry::Polygon { coordinates } = &circle else { - panic!("expected Polygon"); - }; - for pt in &coordinates[0][..16] { - let d = (pt[0] - 500_000.0).hypot(pt[1] - 100_000.0); - assert!((d - 250.0).abs() < 1e-6, "d={d}"); - } - } - - #[test] - fn buffer_point_circle() { - let circle = st_buffer(&Geometry::point(0.0, 0.0), 1000.0, 32); - if let Geometry::Polygon { coordinates } = &circle { - assert_eq!(coordinates.len(), 1); - // 32 segments + 1 closing point. - assert_eq!(coordinates[0].len(), 33); - // All points should be roughly 1000m from center. - for pt in &coordinates[0][..32] { - let d = nodedb_types::geometry::haversine_distance(0.0, 0.0, pt[0], pt[1]); - assert!((d - 1000.0).abs() < 50.0, "d={d}"); - } - } else { - panic!("expected Polygon"); - } } #[test] - fn buffer_point_4_segments() { - let circle = st_buffer(&Geometry::point(10.0, 50.0), 500.0, 4); - if let Geometry::Polygon { coordinates } = &circle { - assert_eq!(coordinates[0].len(), 5); // 4 + close - } else { - panic!("expected Polygon"); - } - } - - #[test] - fn envelope_polygon() { - let poly = Geometry::polygon(vec![vec![[1.0, 2.0], [5.0, 2.0], [3.0, 8.0], [1.0, 2.0]]]); - let env = st_envelope(&poly); - if let Geometry::Polygon { coordinates } = &env { - let ring = &coordinates[0]; - assert_eq!(ring[0], [1.0, 2.0]); // min_lng, min_lat - assert_eq!(ring[2], [5.0, 8.0]); // max_lng, max_lat - } else { - panic!("expected Polygon"); - } - } - - #[test] - fn envelope_point() { - let env = st_envelope(&Geometry::point(5.0, 10.0)); - if let Geometry::Polygon { coordinates } = &env { - // Zero-area bbox → degenerate polygon. - assert_eq!(coordinates[0][0], [5.0, 10.0]); - assert_eq!(coordinates[0][2], [5.0, 10.0]); - } else { - panic!("expected Polygon"); - } - } - - #[test] - fn union_points() { - let a = Geometry::point(1.0, 2.0); - let b = Geometry::point(3.0, 4.0); - let u = st_union(&a, &b); - if let Geometry::MultiPoint { coordinates } = &u { - assert_eq!(coordinates.len(), 2); - } else { - panic!("expected MultiPoint, got {:?}", u.geometry_type()); - } - } - - #[test] - fn union_polygons() { - let a = Geometry::polygon(vec![vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 0.0]]]); - let b = Geometry::polygon(vec![vec![[2.0, 2.0], [3.0, 2.0], [3.0, 3.0], [2.0, 2.0]]]); - let u = st_union(&a, &b); - if let Geometry::MultiPolygon { coordinates } = &u { - assert_eq!(coordinates.len(), 2); - } else { - panic!("expected MultiPolygon"); - } - } - - #[test] - fn union_mixed_types() { - let a = Geometry::point(1.0, 2.0); - let b = Geometry::polygon(vec![vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 0.0]]]); - let u = st_union(&a, &b); - if let Geometry::GeometryCollection { geometries } = &u { - assert_eq!(geometries.len(), 2); - } else { - panic!("expected GeometryCollection"); - } - } - - #[test] - fn buffer_linestring_corridor() { - let line = Geometry::line_string(vec![[0.0, 0.0], [0.1, 0.0]]); - let buf = st_buffer(&line, 1000.0, 16); - if let Geometry::Polygon { coordinates } = &buf { - assert!(!coordinates[0].is_empty()); - // Should have more points than a simple rectangle due to end caps. - assert!(coordinates[0].len() > 10); - } else { - panic!("expected Polygon"); - } - } - - #[test] - fn buffer_polygon_expands() { - let poly = Geometry::polygon(vec![vec![ - [0.0, 0.0], - [1.0, 0.0], - [1.0, 1.0], - [0.0, 1.0], - [0.0, 0.0], - ]]); - let buf = st_buffer(&poly, 1000.0, 32); - if let Geometry::Polygon { .. } = &buf { - let orig_bb = geometry_bbox(&poly); - let buf_bb = geometry_bbox(&buf); - // Buffered should be larger. - assert!(buf_bb.min_lng < orig_bb.min_lng); - assert!(buf_bb.max_lng > orig_bb.max_lng); - assert!(buf_bb.min_lat < orig_bb.min_lat); - assert!(buf_bb.max_lat > orig_bb.max_lat); - } else { - panic!("expected Polygon"); - } + fn degenerate_envelopes_drop_dimension() { + assert_eq!( + st_envelope(&Geometry::point(5.0, 10.0)), + Geometry::point(5.0, 10.0) + ); + let flat = Geometry::line_string(vec![[0.0, 1.0], [4.0, 1.0]]); + assert_eq!( + st_envelope(&flat), + Geometry::line_string(vec![[0.0, 1.0], [4.0, 1.0]]) + ); + assert!(st_envelope(&Geometry::empty_point()).is_empty()); } } diff --git a/nodedb-spatial/src/ops/boolean.rs b/nodedb-spatial/src/ops/boolean.rs new file mode 100644 index 000000000..cdd13e6cc --- /dev/null +++ b/nodedb-spatial/src/ops/boolean.rs @@ -0,0 +1,436 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! `ST_Union`, `ST_Intersection`, `ST_Difference`, `ST_SymDifference`. +//! +//! Each operand splits into its areal, linear and punctual parts. Areal +//! parts go through `geo::BooleanOps` (exact for concave and multi +//! polygons, holes kept, overlapping members dissolved). Lines are clipped +//! by the other side's polygons. Points are kept or dropped by containment. +//! Rings are oriented by the right-hand rule and filled non-zero, so a +//! member drawn clockwise and two overlapping members both behave. +//! +//! Contacts between polygons of lower dimension than the operation (two +//! polygons sharing only an edge or a corner under `ST_Intersection`) +//! produce no output part. + +use geo::algorithm::bool_ops::FillRule; +use geo::{BooleanOps, LineString, MultiLineString, MultiPolygon}; +use nodedb_types::geometry::{Geometry, GeometryKind}; + +use super::convert::{multipolygon, polygon, simplest}; +use super::noding::{Meet, meet}; +use crate::orient::orient_rings; +use crate::parts::polygon_covers; +use crate::predicates::edge::point_on_segment; + +/// One operand split by dimension. +#[derive(Default)] +struct Split { + polygons: Vec>>, + lines: Vec>, + points: Vec<[f64; 2]>, +} + +impl Split { + fn of(geom: &Geometry) -> Self { + let mut oriented = geom.clone(); + orient_rings(&mut oriented); + let mut split = Split::default(); + split.add(&oriented); + split + } + + fn add(&mut self, geom: &Geometry) { + match geom { + Geometry::Point { .. } if geom.is_empty() => {} + Geometry::Point { coordinates } => self.points.push(*coordinates), + Geometry::MultiPoint { coordinates } => self.points.extend_from_slice(coordinates), + Geometry::LineString { coordinates } if coordinates.len() >= 2 => { + self.lines.push(coordinates.clone()); + } + Geometry::LineString { .. } => {} + Geometry::MultiLineString { coordinates } => self + .lines + .extend(coordinates.iter().filter(|l| l.len() >= 2).cloned()), + Geometry::Polygon { coordinates } if !coordinates.is_empty() => { + self.polygons.push(coordinates.clone()); + } + Geometry::Polygon { .. } => {} + Geometry::MultiPolygon { coordinates } => self + .polygons + .extend(coordinates.iter().filter(|p| !p.is_empty()).cloned()), + Geometry::GeometryCollection { geometries } => { + geometries.iter().for_each(|g| self.add(g)); + } + } + } + + fn areal(&self) -> MultiPolygon { + MultiPolygon::new(self.polygons.iter().map(|p| polygon(p)).collect()) + } + + fn linear(&self) -> MultiLineString { + MultiLineString::new( + self.lines + .iter() + .map(|l| { + LineString::new(l.iter().map(|c| geo::Coord { x: c[0], y: c[1] }).collect()) + }) + .collect(), + ) + } + + /// Whether `pt` lies in or on any part. + fn covers(&self, pt: [f64; 2]) -> bool { + self.polygons.iter().any(|p| polygon_covers(p, pt)) + || self + .lines + .iter() + .any(|l| l.windows(2).any(|w| point_on_segment(pt, w[0], w[1]))) + || self.points.contains(&pt) + } + + /// The lowest dimension present, for the kind of an EMPTY result. + fn kind(&self) -> GeometryKind { + if !self.points.is_empty() { + GeometryKind::Point + } else if !self.lines.is_empty() { + GeometryKind::LineString + } else if !self.polygons.is_empty() { + GeometryKind::Polygon + } else { + GeometryKind::GeometryCollection + } + } +} + +fn op(a: &MultiPolygon, b: &MultiPolygon, kind: geo::OpType) -> MultiPolygon { + a.boolean_op_with_fill_rule(b, kind, FillRule::NonZero) +} + +/// Lines of `lines` inside (`invert` false) or outside (`invert` true) the +/// polygons of `by`. No polygons: everything is outside. +fn clip( + lines: &MultiLineString, + by: &MultiPolygon, + invert: bool, +) -> MultiLineString { + if lines.0.is_empty() { + return MultiLineString::new(Vec::new()); + } + if by.0.is_empty() { + return if invert { + lines.clone() + } else { + MultiLineString::new(Vec::new()) + }; + } + by.clip_with_fill_rule(lines, invert, FillRule::NonZero) +} + +fn from_lines(lines: &MultiLineString) -> Vec> { + lines + .0 + .iter() + .map(|l| l.0.iter().map(|c| [c.x, c.y]).collect::>()) + .filter(|l: &Vec<[f64; 2]>| l.len() >= 2) + .collect() +} + +/// Build the result from its parts. No parts gives EMPTY of `empty`. +fn assemble( + polygons: &MultiPolygon, + lines: Vec>, + mut points: Vec<[f64; 2]>, + empty: GeometryKind, +) -> Geometry { + let mut parts = Vec::new(); + let areal = multipolygon(polygons); + if !areal.is_empty() { + parts.push(simplest(areal)); + } + if !lines.is_empty() { + parts.push(simplest(Geometry::MultiLineString { coordinates: lines })); + } + points.sort_by(|a, b| a[0].total_cmp(&b[0]).then(a[1].total_cmp(&b[1]))); + points.dedup(); + if !points.is_empty() { + parts.push(simplest(Geometry::MultiPoint { + coordinates: points, + })); + } + match parts.len() { + 0 => Geometry::empty(empty), + 1 => parts.remove(0), + _ => Geometry::GeometryCollection { geometries: parts }, + } +} + +/// `ST_Union(a, b)`: the point set of both, polygons dissolved. +pub fn union(a: &Geometry, b: &Geometry) -> Geometry { + let (sa, sb) = (Split::of(a), Split::of(b)); + let polygons = op(&sa.areal(), &sb.areal(), geo::OpType::Union); + let mut all_lines = sa.linear(); + all_lines.0.extend(sb.linear().0); + let lines = from_lines(&clip(&all_lines, &polygons, true)); + let line_parts = Split { + lines: lines.clone(), + ..Split::default() + }; + let areal_parts = Split { + polygons: multipolygon_rings(&polygons), + ..Split::default() + }; + let points = sa + .points + .iter() + .chain(&sb.points) + .copied() + .filter(|p| !areal_parts.covers(*p) && !line_parts.covers(*p)) + .collect(); + assemble(&polygons, lines, points, GeometryKind::GeometryCollection) +} + +/// `ST_Intersection(a, b)`: the point set the two share. +pub fn intersection(a: &Geometry, b: &Geometry) -> Geometry { + let (sa, sb) = (Split::of(a), Split::of(b)); + let (pa, pb) = (sa.areal(), sb.areal()); + let polygons = if pa.0.is_empty() || pb.0.is_empty() { + MultiPolygon::new(Vec::new()) + } else { + op(&pa, &pb, geo::OpType::Intersection) + }; + let mut lines = from_lines(&clip(&sa.linear(), &pb, false)); + lines.extend(from_lines(&clip(&sb.linear(), &pa, false))); + let mut points: Vec<[f64; 2]> = sa + .points + .iter() + .copied() + .filter(|p| sb.covers(*p)) + .collect(); + points.extend(sb.points.iter().copied().filter(|p| sa.covers(*p))); + for la in &sa.lines { + for lb in &sb.lines { + line_contacts(la, lb, &mut lines, &mut points); + } + } + let empty = match (sa.kind(), sb.kind()) { + (GeometryKind::GeometryCollection, _) | (_, GeometryKind::GeometryCollection) => { + GeometryKind::GeometryCollection + } + (x, y) => lower(x, y), + }; + assemble(&polygons, lines, points, empty) +} + +/// `ST_Difference(a, b)`: the part of `a` outside `b`. Lower-dimensional +/// parts of `b` remove nothing from the polygons of `a`. +pub fn difference(a: &Geometry, b: &Geometry) -> Geometry { + let (sa, sb) = (Split::of(a), Split::of(b)); + let pb = sb.areal(); + let polygons = op(&sa.areal(), &pb, geo::OpType::Difference); + let lines = from_lines(&clip(&sa.linear(), &pb, true)); + let points = sa + .points + .iter() + .copied() + .filter(|p| !sb.covers(*p)) + .collect(); + assemble(&polygons, lines, points, a.kind()) +} + +/// `ST_SymDifference(a, b)`: the parts of each outside the other. +pub fn sym_difference(a: &Geometry, b: &Geometry) -> Geometry { + let (sa, sb) = (Split::of(a), Split::of(b)); + let (pa, pb) = (sa.areal(), sb.areal()); + let polygons = op(&pa, &pb, geo::OpType::Xor); + let mut lines = from_lines(&clip(&sa.linear(), &pb, true)); + lines.extend(from_lines(&clip(&sb.linear(), &pa, true))); + let mut points: Vec<[f64; 2]> = sa + .points + .iter() + .copied() + .filter(|p| !sb.covers(*p)) + .collect(); + points.extend(sb.points.iter().copied().filter(|p| !sa.covers(*p))); + assemble(&polygons, lines, points, GeometryKind::GeometryCollection) +} + +fn lower(a: GeometryKind, b: GeometryKind) -> GeometryKind { + let rank = |k: GeometryKind| match k { + GeometryKind::Point | GeometryKind::MultiPoint => 0, + GeometryKind::LineString | GeometryKind::MultiLineString => 1, + GeometryKind::Polygon | GeometryKind::MultiPolygon => 2, + GeometryKind::GeometryCollection => 3, + }; + if rank(a) <= rank(b) { a } else { b } +} + +fn multipolygon_rings(mp: &MultiPolygon) -> Vec>> { + mp.0.iter() + .map(super::convert::rings) + .filter(|r| !r.is_empty()) + .collect() +} + +/// Crossing points and shared stretches of two lines. +fn line_contacts( + a: &[[f64; 2]], + b: &[[f64; 2]], + lines: &mut Vec>, + points: &mut Vec<[f64; 2]>, +) { + for wa in a.windows(2) { + for wb in b.windows(2) { + match meet(wa[0], wa[1], wb[0], wb[1]) { + Meet::Disjoint => {} + Meet::Point(p) => points.push(p), + Meet::Overlap(s, e) => lines.push(vec![s, e]), + } + } + } + // A crossing point on a shared stretch is already in the stretch. + points.retain(|p| { + !lines + .iter() + .any(|l| l.windows(2).any(|w| point_on_segment(*p, w[0], w[1]))) + }); +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::planar::planar_area; + + fn square(x0: f64, y0: f64, side: f64) -> Geometry { + Geometry::polygon(vec![vec![ + [x0, y0], + [x0 + side, y0], + [x0 + side, y0 + side], + [x0, y0 + side], + [x0, y0], + ]]) + } + + /// A "U": the 3×3 square minus the 1×2 notch at the top middle. + fn u_shape() -> Geometry { + Geometry::polygon(vec![vec![ + [0.0, 0.0], + [3.0, 0.0], + [3.0, 3.0], + [2.0, 3.0], + [2.0, 1.0], + [1.0, 1.0], + [1.0, 3.0], + [0.0, 3.0], + [0.0, 0.0], + ]]) + } + + /// Areas computed with shapely 2.2.0 (GEOS) on the same inputs. + #[test] + fn concave_clip_matches_shapely() { + // shapely: box(0.5,1.5,2.5,2.5).intersection(U).area == 1.0 + let bar = Geometry::polygon(vec![vec![ + [0.5, 1.5], + [2.5, 1.5], + [2.5, 2.5], + [0.5, 2.5], + [0.5, 1.5], + ]]); + let i = intersection(&u_shape(), &bar); + assert!((planar_area(&i) - 1.0).abs() < 1e-9, "{i:?}"); + assert!(matches!(i, Geometry::MultiPolygon { ref coordinates } if coordinates.len() == 2)); + // Sutherland–Hodgman with the U as clip polygon gets this wrong. + let reversed = intersection(&bar, &u_shape()); + assert!((planar_area(&reversed) - 1.0).abs() < 1e-9); + } + + #[test] + fn union_dissolves_overlap() { + // shapely: box(0,0,2,2).union(box(1,1,3,3)).area == 7.0 + let u = union(&square(0.0, 0.0, 2.0), &square(1.0, 1.0, 2.0)); + assert!(matches!(u, Geometry::Polygon { .. }), "{u:?}"); + assert!((planar_area(&u) - 7.0).abs() < 1e-9); + } + + #[test] + fn difference_and_sym_difference() { + let (a, b) = (square(0.0, 0.0, 2.0), square(1.0, 1.0, 2.0)); + // shapely: a.difference(b).area == 3.0, a.symmetric_difference(b).area == 6.0 + assert!((planar_area(&difference(&a, &b)) - 3.0).abs() < 1e-9); + assert!((planar_area(&sym_difference(&a, &b)) - 6.0).abs() < 1e-9); + } + + #[test] + fn multipolygon_with_hole_intersection() { + let holed = Geometry::polygon(vec![ + vec![[0.0, 0.0], [4.0, 0.0], [4.0, 4.0], [0.0, 4.0], [0.0, 0.0]], + vec![[1.0, 1.0], [1.0, 3.0], [3.0, 3.0], [3.0, 1.0], [1.0, 1.0]], + ]); + let multi = Geometry::MultiPolygon { + coordinates: vec![ + vec![vec![ + [-1.0, -1.0], + [2.0, -1.0], + [2.0, 2.0], + [-1.0, 2.0], + [-1.0, -1.0], + ]], + vec![vec![ + [3.5, 3.5], + [5.0, 3.5], + [5.0, 5.0], + [3.5, 5.0], + [3.5, 3.5], + ]], + ], + }; + // shapely: holed.intersection(multi).area == 3.25 + let i = intersection(&holed, &multi); + assert!((planar_area(&i) - 3.25).abs() < 1e-9, "{i:?}"); + } + + #[test] + fn disjoint_polygons_intersect_to_empty_polygon() { + let i = intersection(&square(0.0, 0.0, 1.0), &square(5.0, 5.0, 1.0)); + assert!(i.is_empty()); + assert_eq!(i.kind(), GeometryKind::Polygon); + } + + #[test] + fn line_is_clipped_by_polygon() { + let line = Geometry::line_string(vec![[-1.0, 0.5], [3.0, 0.5]]); + let i = intersection(&line, &square(0.0, 0.0, 2.0)); + let Geometry::LineString { coordinates } = &i else { + panic!("{i:?}"); + }; + assert_eq!(coordinates.len(), 2); + let d = difference(&line, &square(0.0, 0.0, 2.0)); + assert!( + matches!(d, Geometry::MultiLineString { ref coordinates } if coordinates.len() == 2) + ); + } + + #[test] + fn crossing_lines_intersect_at_a_point() { + let a = Geometry::line_string(vec![[0.0, 0.0], [2.0, 2.0]]); + let b = Geometry::line_string(vec![[0.0, 2.0], [2.0, 0.0]]); + assert_eq!(intersection(&a, &b), Geometry::point(1.0, 1.0)); + } + + #[test] + fn points_inside_survive_intersection() { + let pts = Geometry::MultiPoint { + coordinates: vec![[0.5, 0.5], [5.0, 5.0]], + }; + assert_eq!( + intersection(&pts, &square(0.0, 0.0, 1.0)), + Geometry::point(0.5, 0.5) + ); + assert_eq!( + difference(&pts, &square(0.0, 0.0, 1.0)), + Geometry::point(5.0, 5.0) + ); + } +} diff --git a/nodedb-spatial/src/ops/buffer.rs b/nodedb-spatial/src/ops/buffer.rs new file mode 100644 index 000000000..c325a14df --- /dev/null +++ b/nodedb-spatial/src/ops/buffer.rs @@ -0,0 +1,365 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! `ST_Buffer` for every geometry kind, by a distance in metres. +//! +//! A projected geometry buffers in its grid, the distance converted to the +//! CRS unit. A geographic geometry buffers in a local projection on its own +//! ellipsoid, then maps back to degrees: +//! +//! - an extent of at most 12° of longitude, away from the poles: Transverse +//! Mercator centred on the extent (scale 1 on the central meridian, error +//! under 0.6 % at its edge); +//! - an extent reaching past 80° latitude: polar Lambert azimuthal +//! equal-area; +//! - any other extent: each component buffers in its own local projection +//! and the pieces union in degrees. +//! +//! Curves use round joins and caps with `segments` sides per full circle. + +use geo::algorithm::buffer::{BufferStyle, LineCap, LineJoin}; +use geo::{Buffer, MultiPolygon}; +use nodedb_crs::method::{LaeaParams, ProjectionDef, Projector, TmParams}; +use nodedb_crs::{CrsDef, CrsError, CrsKind, DomainReason, Srid}; +use nodedb_types::geometry::{Geometry, GeometryKind}; + +use super::convert::{from_geo, multipolygon, simplest, to_geo}; +use crate::error::SpatialError; + +/// Widest longitude extent, degrees, buffered in one Transverse Mercator. +const MAX_TM_SPAN_DEG: f64 = 12.0; + +/// Latitude, degrees, past which the polar projection is used. +const POLAR_LAT_DEG: f64 = 80.0; + +/// Metres per degree of latitude, rounded down, to widen an extent by the +/// buffer distance. +const METRES_PER_DEGREE_MIN: f64 = 110_000.0; + +/// Buffer `geom` (coordinates in `def`) by `metres`. A negative distance +/// shrinks polygons and erases points and lines. An EMPTY input or result +/// is `POLYGON EMPTY`. +pub fn buffer( + geom: &Geometry, + metres: f64, + segments: usize, + def: &CrsDef, +) -> Result { + if geom.is_empty() { + return Ok(Geometry::empty(GeometryKind::Polygon)); + } + let style = || { + let angle = std::f64::consts::TAU / segments.max(3) as f64; + move |distance: f64| { + BufferStyle::new(distance) + .line_join(LineJoin::Round(angle)) + .line_cap(LineCap::Round(angle)) + } + }; + let out = match def.kind { + CrsKind::Projected => { + let units = metres / def.unit.metres_per_unit; + to_geo(geom).buffer_with_style(style()(units)) + } + CrsKind::Geographic => geographic(geom, metres, def, &style())?, + }; + Ok(finish(&out)) +} + +fn finish(out: &MultiPolygon) -> Geometry { + let g = multipolygon(out); + if g.is_empty() { + Geometry::empty(GeometryKind::Polygon) + } else { + simplest(g) + } +} + +fn geographic( + geom: &Geometry, + metres: f64, + def: &CrsDef, + style: &dyn Fn(f64) -> BufferStyle, +) -> Result, SpatialError> { + let extent = Extent::of(geom, metres); + if let Some(local) = Local::choose(&extent, def) { + return local.buffer(geom, metres, style); + } + let mut pieces: Vec> = Vec::new(); + for component in components(geom) { + let extent = Extent::of(&component, metres); + let local = Local::choose(&extent, def).unwrap_or_else(|| Local::laea(&extent, def)); + pieces.extend(local.buffer(&component, metres, style)?.0); + } + Ok(geo::unary_union(&pieces)) +} + +/// Every point, line and polygon of a geometry. +fn components(geom: &Geometry) -> Vec { + match geom { + Geometry::MultiPoint { coordinates } => coordinates + .iter() + .map(|c| Geometry::point(c[0], c[1])) + .collect(), + Geometry::MultiLineString { coordinates } => coordinates + .iter() + .map(|l| Geometry::line_string(l.clone())) + .collect(), + Geometry::MultiPolygon { coordinates } => coordinates + .iter() + .map(|p| Geometry::polygon(p.clone())) + .collect(), + Geometry::GeometryCollection { geometries } => { + geometries.iter().flat_map(components).collect() + } + other if other.is_empty() => Vec::new(), + other => vec![other.clone()], + } +} + +/// Longitude extent unwrapped around the first vertex, and latitude +/// extent, both widened by the buffer distance. +struct Extent { + min_lon: f64, + max_lon: f64, + min_lat: f64, + max_lat: f64, +} + +impl Extent { + fn of(geom: &Geometry, metres: f64) -> Self { + let mut coords = geom.coords(); + let first = coords.next().unwrap_or([0.0, 0.0]); + let mut e = Extent { + min_lon: first[0], + max_lon: first[0], + min_lat: first[1], + max_lat: first[1], + }; + for [lon, lat] in coords { + let lon = unwrap(lon, first[0]); + e.min_lon = e.min_lon.min(lon); + e.max_lon = e.max_lon.max(lon); + e.min_lat = e.min_lat.min(lat); + e.max_lat = e.max_lat.max(lat); + } + let pad = metres.abs() / METRES_PER_DEGREE_MIN; + e.min_lat = (e.min_lat - pad).max(-90.0); + e.max_lat = (e.max_lat + pad).min(90.0); + let poleward = e.min_lat.abs().max(e.max_lat.abs()).min(89.0); + let lon_pad = pad / poleward.to_radians().cos(); + e.min_lon -= lon_pad; + e.max_lon += lon_pad; + e + } + + fn centre(&self) -> [f64; 2] { + [ + (self.min_lon + self.max_lon) / 2.0, + (self.min_lat + self.max_lat) / 2.0, + ] + } +} + +/// `lon` moved by whole turns into `reference ± 180°`. +fn unwrap(lon: f64, reference: f64) -> f64 { + lon - 360.0 * ((lon - reference) / 360.0).round() +} + +/// A local projection on the CRS ellipsoid. +struct Local { + projector: Projector, + /// Longitude output is unwrapped around this, so a buffer across the + /// antimeridian stays one continuous ring. + centre_lon: f64, + srid: Srid, +} + +impl Local { + /// The projection for `extent`, or `None` when it is too wide for one. + fn choose(extent: &Extent, def: &CrsDef) -> Option { + if extent.max_lat > POLAR_LAT_DEG || extent.min_lat < -POLAR_LAT_DEG { + let pole = if extent.max_lat > POLAR_LAT_DEG { + 90.0 + } else { + -90.0 + }; + let both = extent.max_lat > POLAR_LAT_DEG && extent.min_lat < -POLAR_LAT_DEG; + return (!both).then(|| Self::polar(pole, extent, def)); + } + if extent.max_lon - extent.min_lon > MAX_TM_SPAN_DEG { + return None; + } + let [lon0, _] = extent.centre(); + let params = TmParams { + lat0_deg: 0.0, + lon0_deg: normalize(lon0), + k0: 1.0, + false_easting: 0.0, + false_northing: 0.0, + }; + Some(Self::new( + ProjectionDef::TransverseMercator(params), + lon0, + def, + )) + } + + fn polar(lat0: f64, extent: &Extent, def: &CrsDef) -> Self { + let [lon0, _] = extent.centre(); + let params = LaeaParams { + lat0_deg: lat0, + lon0_deg: normalize(lon0), + false_easting: 0.0, + false_northing: 0.0, + }; + Self::new(ProjectionDef::LambertAzimuthalEqualArea(params), lon0, def) + } + + /// Lambert azimuthal equal-area centred on `extent`, for a component + /// too wide for Transverse Mercator. + fn laea(extent: &Extent, def: &CrsDef) -> Self { + let [lon0, lat0] = extent.centre(); + let params = LaeaParams { + lat0_deg: lat0, + lon0_deg: normalize(lon0), + false_easting: 0.0, + false_northing: 0.0, + }; + Self::new(ProjectionDef::LambertAzimuthalEqualArea(params), lon0, def) + } + + fn new(projection: ProjectionDef, centre_lon: f64, def: &CrsDef) -> Self { + Self { + projector: projection.prepare(&def.datum.ellipsoid), + centre_lon, + srid: def.srid, + } + } + + fn buffer( + &self, + geom: &Geometry, + metres: f64, + style: &dyn Fn(f64) -> BufferStyle, + ) -> Result, SpatialError> { + let projected = geom.try_map_coords(&mut |[x, y]| { + self.projector + .forward(x.to_radians(), y.to_radians()) + .map(|(e, n)| [e, n]) + .map_err(|reason| self.fault(x, y, reason)) + })?; + let buffered = from_geo(&geo::Geometry::MultiPolygon( + to_geo(&projected).buffer_with_style(style(metres)), + )); + let back = buffered.try_map_coords(&mut |[e, n]| { + self.projector + .inverse(e, n) + .map(|(lam, phi)| [unwrap(lam.to_degrees(), self.centre_lon), phi.to_degrees()]) + .map_err(|reason| self.fault(e, n, reason)) + })?; + let Geometry::MultiPolygon { coordinates } = back else { + return Ok(MultiPolygon::new(Vec::new())); + }; + Ok(MultiPolygon::new( + coordinates + .iter() + .map(|p| super::convert::polygon(p)) + .collect(), + )) + } + + fn fault(&self, x: f64, y: f64, reason: DomainReason) -> SpatialError { + SpatialError::Crs(CrsError::OutOfDomain { + srid: self.srid, + x, + y, + reason, + }) + } +} + +/// Longitude in [−180°, 180°). +fn normalize(lon: f64) -> f64 { + (lon + 180.0).rem_euclid(360.0) - 180.0 +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::metric::Metric; + use nodedb_crs::CrsContext; + + fn def(code: u32) -> std::sync::Arc { + CrsContext::builtin_only() + .resolve(Srid::new(code).unwrap()) + .unwrap() + } + + /// A geographic point buffer is a geodesic circle: every vertex lies + /// the buffer distance from the centre, measured on the ellipsoid. A + /// 64-gon vertex sits at most 0.13 % off the circle. + #[test] + fn geographic_point_buffer_is_a_geodesic_circle() { + let wgs84 = def(4326); + let out = buffer(&Geometry::point(101.7, 3.1), 1_000.0, 64, &wgs84).unwrap(); + let Geometry::Polygon { coordinates } = &out else { + panic!("{out:?}"); + }; + let m = Metric::wgs84(); + for c in &coordinates[0] { + let d = m.segment_length([101.7, 3.1], *c); + assert!((d - 1_000.0).abs() < 2.0, "{d}"); + } + } + + #[test] + fn multi_geometries_buffer_every_member() { + let wgs84 = def(4326); + let far = Geometry::MultiPoint { + coordinates: vec![[0.0, 0.0], [100.0, 10.0]], + }; + let out = buffer(&far, 500.0, 32, &wgs84).unwrap(); + assert!( + matches!(&out, Geometry::MultiPolygon { coordinates } if coordinates.len() == 2), + "{out:?}" + ); + let collection = Geometry::GeometryCollection { + geometries: vec![ + Geometry::point(0.0, 0.0), + Geometry::line_string(vec![[0.01, 0.0], [0.02, 0.0]]), + ], + }; + assert!(!buffer(&collection, 100.0, 32, &wgs84).unwrap().is_empty()); + } + + #[test] + fn polar_point_buffers_in_a_polar_projection() { + let wgs84 = def(4326); + let out = buffer(&Geometry::point(0.0, 89.9), 1_000.0, 32, &wgs84).unwrap(); + let m = Metric::wgs84(); + let Geometry::Polygon { coordinates } = &out else { + panic!("{out:?}"); + }; + for c in &coordinates[0] { + let d = m.segment_length([0.0, 89.9], *c); + assert!((d - 1_000.0).abs() < 10.0, "{d}"); + } + } + + #[test] + fn projected_buffer_uses_the_crs_unit() { + let feet = def(2263); + let out = buffer(&Geometry::point(1_000_000.0, 200_000.0), 100.0, 64, &feet).unwrap(); + let Geometry::Polygon { coordinates } = &out else { + panic!("{out:?}"); + }; + let r = (coordinates[0][0][0] - 1_000_000.0).hypot(coordinates[0][0][1] - 200_000.0); + assert!((r - 100.0 / (1200.0 / 3937.0)).abs() < 1.0, "{r}"); + } + + #[test] + fn negative_distance_erases_a_point() { + let out = buffer(&Geometry::point(0.0, 0.0), -10.0, 32, &def(4326)).unwrap(); + assert!(out.is_empty()); + } +} diff --git a/nodedb-spatial/src/ops/closest.rs b/nodedb-spatial/src/ops/closest.rs new file mode 100644 index 000000000..77c935aa4 --- /dev/null +++ b/nodedb-spatial/src/ops/closest.rs @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! `ST_ClosestPoint(a, b)`: the point of `a` nearest to `b`, planar in the +//! coordinates. Where the two meet, a shared point. + +use nodedb_types::geometry::Geometry; + +use super::noding::{Meet, fraction_along, meet}; +use crate::parts::{Parts, polygon_covers}; +use crate::predicates::edge::point_to_segment_dist_sq; + +/// The point of `a` closest to `b`. `None` when either is EMPTY. +pub fn closest_point(a: &Geometry, b: &Geometry) -> Option<[f64; 2]> { + let pa = Parts::of(a); + let pb = Parts::of(b); + if pa.segments.is_empty() || pb.segments.is_empty() { + return None; + } + if let Some(v) = pb + .vertices + .iter() + .find(|v| pa.polygons.iter().any(|rings| polygon_covers(rings, **v))) + { + return Some(*v); + } + if let Some(v) = pa + .vertices + .iter() + .find(|v| pb.polygons.iter().any(|rings| polygon_covers(rings, **v))) + { + return Some(*v); + } + let mut best: Option<(f64, [f64; 2])> = None; + let mut consider = |d: f64, p: [f64; 2]| { + if best.is_none_or(|(bd, _)| d < bd) { + best = Some((d, p)); + } + }; + for &(a1, a2) in &pa.segments { + for &(b1, b2) in &pb.segments { + match meet(a1, a2, b1, b2) { + Meet::Point(p) | Meet::Overlap(p, _) => return Some(p), + Meet::Disjoint => {} + } + for v in [b1, b2] { + let t = fraction_along(a1, a2, v); + let foot = [a1[0] + (a2[0] - a1[0]) * t, a1[1] + (a2[1] - a1[1]) * t]; + consider(point_to_segment_dist_sq(v, a1, a2), foot); + } + for u in [a1, a2] { + consider(point_to_segment_dist_sq(u, b1, b2), u); + } + } + } + best.map(|(_, p)| p) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn foot_of_the_perpendicular_on_a_line() { + let line = Geometry::line_string(vec![[0.0, 0.0], [10.0, 0.0]]); + assert_eq!( + closest_point(&line, &Geometry::point(3.0, 5.0)), + Some([3.0, 0.0]) + ); + assert_eq!( + closest_point(&Geometry::point(3.0, 5.0), &line), + Some([3.0, 5.0]) + ); + } + + #[test] + fn point_inside_a_polygon_is_its_own_closest() { + let square = Geometry::polygon(vec![vec![ + [0.0, 0.0], + [4.0, 0.0], + [4.0, 4.0], + [0.0, 4.0], + [0.0, 0.0], + ]]); + assert_eq!( + closest_point(&square, &Geometry::point(1.0, 1.0)), + Some([1.0, 1.0]) + ); + assert_eq!( + closest_point(&square, &Geometry::point(6.0, 2.0)), + Some([4.0, 2.0]) + ); + } + + #[test] + fn empty_has_no_closest_point() { + assert_eq!( + closest_point(&Geometry::empty_point(), &Geometry::point(0.0, 0.0)), + None + ); + } +} diff --git a/nodedb-spatial/src/ops/convert.rs b/nodedb-spatial/src/ops/convert.rs new file mode 100644 index 000000000..db01e635f --- /dev/null +++ b/nodedb-spatial/src/ops/convert.rs @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Conversion between NodeDB shapes and `geo` types. +//! +//! `geo` has no EMPTY point, so an EMPTY point converts to an empty +//! `GeometryCollection`. `geo` closes rings on construction. + +use geo::{ + Coord, GeometryCollection, LineString, MultiLineString, MultiPoint, MultiPolygon, Point, + Polygon, +}; +use nodedb_types::geometry::Geometry; + +fn coord(c: [f64; 2]) -> Coord { + Coord { x: c[0], y: c[1] } +} + +fn line(coords: &[[f64; 2]]) -> LineString { + LineString::new(coords.iter().map(|c| coord(*c)).collect()) +} + +pub(crate) fn polygon(rings: &[Vec<[f64; 2]>]) -> Polygon { + let mut iter = rings.iter(); + let exterior = iter + .next() + .map_or_else(|| LineString::new(Vec::new()), |r| line(r)); + Polygon::new(exterior, iter.map(|r| line(r)).collect()) +} + +/// A NodeDB shape as a `geo` geometry. +pub(crate) fn to_geo(geom: &Geometry) -> geo::Geometry { + match geom { + Geometry::Point { .. } if geom.is_empty() => { + geo::Geometry::GeometryCollection(GeometryCollection::new_from(Vec::new())) + } + Geometry::Point { coordinates } => geo::Geometry::Point(Point(coord(*coordinates))), + Geometry::LineString { coordinates } => geo::Geometry::LineString(line(coordinates)), + Geometry::Polygon { coordinates } => geo::Geometry::Polygon(polygon(coordinates)), + Geometry::MultiPoint { coordinates } => geo::Geometry::MultiPoint(MultiPoint::new( + coordinates.iter().map(|c| Point(coord(*c))).collect(), + )), + Geometry::MultiLineString { coordinates } => geo::Geometry::MultiLineString( + MultiLineString::new(coordinates.iter().map(|l| line(l)).collect()), + ), + Geometry::MultiPolygon { coordinates } => geo::Geometry::MultiPolygon(MultiPolygon::new( + coordinates.iter().map(|p| polygon(p)).collect(), + )), + Geometry::GeometryCollection { geometries } => geo::Geometry::GeometryCollection( + GeometryCollection::new_from(geometries.iter().map(to_geo).collect()), + ), + } +} + +fn path(l: &LineString) -> Vec<[f64; 2]> { + l.0.iter().map(|c| [c.x, c.y]).collect() +} + +/// A `geo` polygon as rings. A polygon with an empty exterior is EMPTY. +pub(crate) fn rings(p: &Polygon) -> Vec> { + if p.exterior().0.is_empty() { + return Vec::new(); + } + std::iter::once(path(p.exterior())) + .chain(p.interiors().iter().map(path)) + .collect() +} + +/// A `geo` geometry as a NodeDB shape. +pub(crate) fn from_geo(geom: &geo::Geometry) -> Geometry { + match geom { + geo::Geometry::Point(p) => Geometry::point(p.x(), p.y()), + geo::Geometry::Line(l) => { + Geometry::line_string(vec![[l.start.x, l.start.y], [l.end.x, l.end.y]]) + } + geo::Geometry::LineString(l) => Geometry::line_string(path(l)), + geo::Geometry::Polygon(p) => Geometry::polygon(rings(p)), + geo::Geometry::MultiPoint(mp) => Geometry::MultiPoint { + coordinates: mp.0.iter().map(|p| [p.x(), p.y()]).collect(), + }, + geo::Geometry::MultiLineString(ml) => Geometry::MultiLineString { + coordinates: ml.0.iter().map(path).collect(), + }, + geo::Geometry::MultiPolygon(mp) => multipolygon(mp), + geo::Geometry::GeometryCollection(gc) => Geometry::GeometryCollection { + geometries: gc.0.iter().map(from_geo).collect(), + }, + geo::Geometry::Rect(r) => Geometry::polygon(rings(&r.to_polygon())), + geo::Geometry::Triangle(t) => Geometry::polygon(rings(&t.to_polygon())), + } +} + +/// A `geo` multipolygon as a MultiPolygon shape, empty members dropped. +pub(crate) fn multipolygon(mp: &MultiPolygon) -> Geometry { + Geometry::MultiPolygon { + coordinates: mp.0.iter().map(rings).filter(|r| !r.is_empty()).collect(), + } +} + +/// Collapse a multi with one member to that member, as PostGIS returns a +/// `POLYGON` rather than a one-member `MULTIPOLYGON`. +pub(crate) fn simplest(geom: Geometry) -> Geometry { + match geom { + Geometry::MultiPolygon { mut coordinates } if coordinates.len() == 1 => Geometry::Polygon { + coordinates: coordinates.remove(0), + }, + Geometry::MultiLineString { mut coordinates } if coordinates.len() == 1 => { + Geometry::LineString { + coordinates: coordinates.remove(0), + } + } + Geometry::MultiPoint { coordinates } if coordinates.len() == 1 => Geometry::Point { + coordinates: coordinates[0], + }, + other => other, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn round_trip_keeps_shapes() { + for g in [ + Geometry::point(1.0, 2.0), + Geometry::line_string(vec![[0.0, 0.0], [1.0, 1.0]]), + Geometry::polygon(vec![ + vec![[0.0, 0.0], [4.0, 0.0], [4.0, 4.0], [0.0, 4.0], [0.0, 0.0]], + vec![[1.0, 1.0], [1.0, 2.0], [2.0, 2.0], [2.0, 1.0], [1.0, 1.0]], + ]), + Geometry::MultiPoint { + coordinates: vec![[1.0, 2.0], [3.0, 4.0]], + }, + ] { + assert_eq!(from_geo(&to_geo(&g)), g); + } + } + + #[test] + fn simplest_unwraps_single_members() { + let mp = Geometry::MultiPolygon { + coordinates: vec![vec![vec![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [0.0, 0.0]]]], + }; + assert!(matches!(simplest(mp), Geometry::Polygon { .. })); + } +} diff --git a/nodedb-spatial/src/ops/hull.rs b/nodedb-spatial/src/ops/hull.rs new file mode 100644 index 000000000..86c4c6f27 --- /dev/null +++ b/nodedb-spatial/src/ops/hull.rs @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! `ST_ConvexHull` and `ST_ConcaveHull` over every vertex of a geometry. +//! +//! One distinct vertex gives a point, collinear vertices give a line +//! between the extremes, as PostGIS returns them. + +use geo::concave_hull::ConcaveHullOptions; +use geo::{ConcaveHull, ConvexHull, Coord, MultiPoint, Point}; +use nodedb_types::geometry::{Geometry, GeometryKind}; + +use super::convert::rings; +use crate::predicates::edge::{Orientation, orientation}; + +/// The hull algorithm and its parameter. +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Hull { + Convex, + /// `pct_convex` in [0, 1]: 1 is the convex hull, smaller values drill + /// the hull inward. Maps to concaveman concavity `2·p / (1 − p)`. + Concave { + pct_convex: f64, + }, +} + +/// The hull of every vertex of `geom`. EMPTY input gives +/// `GEOMETRYCOLLECTION EMPTY`. +pub fn hull(geom: &Geometry, kind: Hull) -> Geometry { + let mut coords: Vec<[f64; 2]> = geom.coords().collect(); + coords.sort_by(|a, b| a[0].total_cmp(&b[0]).then(a[1].total_cmp(&b[1]))); + coords.dedup(); + match coords.as_slice() { + [] => return Geometry::empty(GeometryKind::GeometryCollection), + [only] => return Geometry::point(only[0], only[1]), + [first, .., last] if all_collinear(&coords) => { + return Geometry::line_string(vec![*first, *last]); + } + _ => {} + } + let points = MultiPoint::new( + coords + .iter() + .map(|c| Point(Coord { x: c[0], y: c[1] })) + .collect(), + ); + let polygon = match kind { + Hull::Convex => points.convex_hull(), + Hull::Concave { pct_convex } if pct_convex >= 1.0 => points.convex_hull(), + Hull::Concave { pct_convex } => { + let p = pct_convex.max(0.0); + points.concave_hull_with_options( + ConcaveHullOptions::default().concavity(2.0 * p / (1.0 - p)), + ) + } + }; + Geometry::polygon(rings(&polygon)) +} + +/// Whether every point lies on the line through the first and last of a +/// sorted, de-duplicated list. +fn all_collinear(sorted: &[[f64; 2]]) -> bool { + let (Some(first), Some(last)) = (sorted.first(), sorted.last()) else { + return true; + }; + sorted + .iter() + .all(|p| orientation(*first, *last, *p) == Orientation::Collinear) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::planar::planar_area; + + fn u_points() -> Geometry { + Geometry::MultiPoint { + coordinates: vec![ + [0.0, 0.0], + [3.0, 0.0], + [3.0, 3.0], + [2.0, 3.0], + [2.0, 1.0], + [1.0, 1.0], + [1.0, 3.0], + [0.0, 3.0], + ], + } + } + + #[test] + fn convex_hull_area() { + assert!((planar_area(&hull(&u_points(), Hull::Convex)) - 9.0).abs() < 1e-9); + } + + #[test] + fn concave_hull_is_inside_the_convex_hull() { + let concave = planar_area(&hull(&u_points(), Hull::Concave { pct_convex: 0.0 })); + assert!(concave <= 9.0 + 1e-9 && concave > 0.0, "{concave}"); + let convex = planar_area(&hull(&u_points(), Hull::Concave { pct_convex: 1.0 })); + assert!((convex - 9.0).abs() < 1e-9); + } + + #[test] + fn degenerate_inputs() { + assert_eq!( + hull(&Geometry::point(1.0, 2.0), Hull::Convex), + Geometry::point(1.0, 2.0) + ); + let line = Geometry::line_string(vec![[0.0, 0.0], [1.0, 1.0], [2.0, 2.0]]); + assert_eq!( + hull(&line, Hull::Convex), + Geometry::line_string(vec![[0.0, 0.0], [2.0, 2.0]]) + ); + assert!(hull(&Geometry::empty_point(), Hull::Convex).is_empty()); + } +} diff --git a/nodedb-spatial/src/ops/linear.rs b/nodedb-spatial/src/ops/linear.rs new file mode 100644 index 000000000..e90fa21f2 --- /dev/null +++ b/nodedb-spatial/src/ops/linear.rs @@ -0,0 +1,496 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Linear referencing and densification. +//! +//! - `ST_LineInterpolatePoint`: the point at a fraction of a line's length, +//! geodesic on a geographic CRS. +//! - `ST_LocateAlong`, `ST_LocateBetween`: the parts of an M geometry at or +//! between measures, optionally offset sideways. +//! - `ST_InterpolatePoint`: the M of a line at the point nearest a point. +//! - `ST_Segmentize`: no edge longer than a length, geodesic on a geographic +//! CRS. +//! +//! Z and M interpolate linearly along each edge. + +use nodedb_crs::method::geodesic::GeodesicLine; +use nodedb_types::geometry::{Geometry, Position, SridGeometry}; + +use super::noding::fraction_along; +use super::pieces::{Assembler, Piece, lerp, multiline, multipoint, pieces}; +use crate::error::SpatialError; +use crate::metric::Metric; +use crate::predicates::edge::point_to_segment_dist_sq; + +/// Why a linear-referencing call has no answer. +#[derive(Debug, Clone, PartialEq, thiserror::Error)] +pub enum LinearError { + #[error("{function} takes a LineString, got {kind}")] + NotALine { + function: &'static str, + kind: &'static str, + }, + #[error("{function} takes a geometry with M ordinates")] + NoMeasure { function: &'static str }, + #[error("{function}: fraction {fraction} is outside [0, 1]")] + Fraction { + function: &'static str, + fraction: f64, + }, +} + +/// The positions of a single LineString. +fn line_positions(g: &SridGeometry, function: &'static str) -> Result, LinearError> { + match (g.geometry(), pieces(g).as_slice()) { + (Geometry::LineString { .. }, [Piece::Line(ps)]) => Ok(ps.clone()), + (other, _) => Err(LinearError::NotALine { + function, + kind: other.geometry_type(), + }), + } +} + +/// `ST_LineInterpolatePoint(line, fraction)`: the point `fraction` of the +/// way along `line`, measured in `metric`. +pub fn line_interpolate_point( + g: &SridGeometry, + fraction: f64, + metric: &Metric, +) -> Result { + const F: &str = "ST_LineInterpolatePoint"; + if !(0.0..=1.0).contains(&fraction) { + return Err(LinearError::Fraction { + function: F, + fraction, + } + .into()); + } + let ps = line_positions(g, F)?; + let mut asm = Assembler::new(g.dimension()); + let Some(first) = ps.first() else { + return Ok(asm.finish(g.srid, Geometry::empty_point())?); + }; + let lengths: Vec = ps + .windows(2) + .map(|w| metric.segment_length([w[0].x, w[0].y], [w[1].x, w[1].y])) + .collect(); + let total: f64 = lengths.iter().sum(); + let target = total * fraction; + let mut walked = 0.0; + let mut at = *ps.last().unwrap_or(first); + for (i, len) in lengths.iter().enumerate() { + if walked + len >= target && *len > 0.0 { + let t = (target - walked) / len; + at = along(&ps[i], &ps[i + 1], t, metric); + break; + } + walked += len; + } + let c = asm.coord(&at)?; + Ok(asm.finish(g.srid, Geometry::point(c[0], c[1]))?) +} + +/// The position fraction `t` along edge a–b: a geodesic position on a +/// geographic CRS, a straight one on a projected CRS. +fn along(a: &Position, b: &Position, t: f64, metric: &Metric) -> Position { + let straight = lerp(a, b, t); + match metric { + Metric::Planar { .. } => straight, + Metric::Geodesic(m) => { + let g = m.geodesic(); + let (s12, azi1, _) = g.inverse(a.y, a.x, b.y, b.x); + let pos = GeodesicLine::new(g, a.y, a.x, azi1).position(s12 * t); + Position { + x: pos.lon2, + y: pos.lat2, + ..straight + } + } + } +} + +/// `ST_Segmentize(geom, max_length_m)`: insert vertices so no edge is +/// longer than `max_metres`, measured in `metric`. +pub fn segmentize( + g: &SridGeometry, + max_metres: f64, + metric: &Metric, +) -> Result { + let mut asm = Assembler::new(g.dimension()); + let dense = |ps: &[Position], asm: &mut Assembler| -> Result, SpatialError> { + let mut out = Vec::with_capacity(ps.len()); + for (i, p) in ps.iter().enumerate() { + if i > 0 { + let prev = &ps[i - 1]; + let len = metric.segment_length([prev.x, prev.y], [p.x, p.y]); + let parts = (len / max_metres).ceil().clamp(1.0, MAX_SEGMENTS_PER_EDGE) as usize; + for k in 1..parts { + out.push(asm.coord(&along(prev, p, k as f64 / parts as f64, metric))?); + } + } + out.push(asm.coord(p)?); + } + Ok(out) + }; + let shape = rebuild(g.geometry(), &pieces(g), &mut asm, &dense)?; + Ok(asm.finish(g.srid, shape)?) +} + +/// Cap on inserted vertices per edge, so a tiny length cannot allocate +/// without bound. +const MAX_SEGMENTS_PER_EDGE: f64 = 1_000_000.0; + +type Densify<'a> = &'a dyn Fn(&[Position], &mut Assembler) -> Result, SpatialError>; + +/// Rebuild `geom`'s shape from its pieces, every path through `f`. +fn rebuild( + geom: &Geometry, + all: &[Piece], + asm: &mut Assembler, + f: Densify<'_>, +) -> Result { + let mut iter = all.iter(); + rebuild_inner(geom, &mut iter, asm, f) +} + +fn rebuild_inner<'p>( + geom: &Geometry, + iter: &mut impl Iterator, + asm: &mut Assembler, + f: Densify<'_>, +) -> Result { + let next_points = |n: usize, iter: &mut dyn Iterator, asm: &mut Assembler| { + let mut out = Vec::with_capacity(n); + for _ in 0..n { + if let Some(Piece::Point(p)) = iter.next() { + out.push(asm.coord(p)?); + } + } + Ok::<_, SpatialError>(out) + }; + Ok(match geom { + g if g.is_empty() && !matches!(g, Geometry::GeometryCollection { .. }) => g.clone(), + Geometry::Point { .. } => { + let c = next_points(1, iter, asm)?; + c.first() + .map_or_else(Geometry::empty_point, |c| Geometry::point(c[0], c[1])) + } + Geometry::MultiPoint { coordinates } => Geometry::MultiPoint { + coordinates: next_points(coordinates.len(), iter, asm)?, + }, + Geometry::LineString { .. } => match iter.next() { + Some(Piece::Line(ps)) => Geometry::line_string(f(ps, asm)?), + _ => geom.clone(), + }, + Geometry::MultiLineString { coordinates } => { + let mut lines = Vec::with_capacity(coordinates.len()); + for _ in coordinates { + if let Some(Piece::Line(ps)) = iter.next() { + lines.push(f(ps, asm)?); + } + } + Geometry::MultiLineString { coordinates: lines } + } + Geometry::Polygon { .. } => match iter.next() { + Some(Piece::Polygon(rings)) => { + Geometry::polygon(rings.iter().map(|r| f(r, asm)).collect::>()?) + } + _ => geom.clone(), + }, + Geometry::MultiPolygon { coordinates } => { + let mut polygons = Vec::with_capacity(coordinates.len()); + for _ in coordinates { + if let Some(Piece::Polygon(rings)) = iter.next() { + polygons.push(rings.iter().map(|r| f(r, asm)).collect::>()?); + } + } + Geometry::MultiPolygon { + coordinates: polygons, + } + } + Geometry::GeometryCollection { geometries } => Geometry::GeometryCollection { + geometries: geometries + .iter() + .map(|member| rebuild_inner(member, iter, asm, f)) + .collect::>()?, + }, + }) +} + +fn require_m(g: &SridGeometry, function: &'static str) -> Result<(), LinearError> { + if g.has_m() { + Ok(()) + } else { + Err(LinearError::NoMeasure { function }) + } +} + +/// `p` moved `offset` to the left of the direction a → b. +fn offset_left(p: Position, a: &Position, b: &Position, offset: f64) -> Position { + if offset == 0.0 { + return p; + } + let (dx, dy) = (b.x - a.x, b.y - a.y); + let len = dx.hypot(dy); + if len == 0.0 { + return p; + } + Position { + x: p.x - dy / len * offset, + y: p.y + dx / len * offset, + ..p + } +} + +/// `ST_LocateAlong(geom, m [, offset])`: every point where the measure +/// equals `m`, moved `offset` coordinate units to the left of the line. +/// The result is a MULTIPOINT in the input's dimension. +pub fn locate_along(g: &SridGeometry, m: f64, offset: f64) -> Result { + require_m(g, "ST_LocateAlong")?; + let mut found = Vec::new(); + for piece in pieces(g) { + match piece { + Piece::Point(p) if p.m == Some(m) => found.push(p), + Piece::Point(_) | Piece::Polygon(_) => {} + Piece::Line(ps) => { + for w in ps.windows(2) { + let (Some(m0), Some(m1)) = (w[0].m, w[1].m) else { + continue; + }; + let (lo, hi) = (m0.min(m1), m0.max(m1)); + if !(lo..=hi).contains(&m) { + continue; + } + let t = if m1 == m0 { 0.0 } else { (m - m0) / (m1 - m0) }; + let p = Position { + m: Some(m), + ..lerp(&w[0], &w[1], t) + }; + found.push(offset_left(p, &w[0], &w[1], offset)); + } + } + } + } + found.dedup(); + Ok(multipoint(g.srid, g.dimension(), &found)?) +} + +/// `ST_LocateBetween(geom, m_start, m_end [, offset])`: the parts whose +/// measures lie in [m_start, m_end]. Points come back as a MULTIPOINT, +/// line pieces as a MULTILINESTRING, both when both occur in a +/// GEOMETRYCOLLECTION. Each line piece moves `offset` to its left. +pub fn locate_between( + g: &SridGeometry, + m_start: f64, + m_end: f64, + offset: f64, +) -> Result { + require_m(g, "ST_LocateBetween")?; + let (lo, hi) = (m_start.min(m_end), m_start.max(m_end)); + let inside = |m: Option| m.is_some_and(|m| (lo..=hi).contains(&m)); + let mut points = Vec::new(); + let mut lines: Vec> = Vec::new(); + for piece in pieces(g) { + match piece { + Piece::Point(p) if inside(p.m) => points.push(p), + Piece::Point(_) | Piece::Polygon(_) => {} + Piece::Line(ps) => { + let mut current: Vec = Vec::new(); + for w in ps.windows(2) { + let (a, b) = (&w[0], &w[1]); + let (Some(ma), Some(mb)) = (a.m, b.m) else { + continue; + }; + let clamp = |m: f64| { + if mb == ma { + 0.0 + } else { + ((m - ma) / (mb - ma)).clamp(0.0, 1.0) + } + }; + let (t0, t1) = { + let (u, v) = (clamp(lo), clamp(hi)); + (u.min(v), u.max(v)) + }; + let overlaps = ma.min(mb) <= hi && ma.max(mb) >= lo; + if !overlaps { + flush(&mut current, &mut lines, offset); + continue; + } + let (s, e) = (lerp(a, b, t0), lerp(a, b, t1)); + if current.last() != Some(&s) { + flush(&mut current, &mut lines, offset); + current.push(s); + } + current.push(e); + if t1 < 1.0 { + flush(&mut current, &mut lines, offset); + } + } + flush(&mut current, &mut lines, offset); + } + } + } + let dimension = g.dimension(); + Ok(match (points.is_empty(), lines.is_empty()) { + (_, true) => multipoint(g.srid, dimension, &points)?, + (true, false) => multiline(g.srid, dimension, &lines)?, + (false, false) => { + let mut asm = Assembler::new(dimension); + let pts = asm.path(&points)?; + let ls = lines + .iter() + .map(|l| asm.path(l)) + .collect::>()?; + asm.finish( + g.srid, + Geometry::GeometryCollection { + geometries: vec![ + Geometry::MultiPoint { coordinates: pts }, + Geometry::MultiLineString { coordinates: ls }, + ], + }, + )? + } + }) +} + +/// Close the running piece: a single position becomes nothing, two or +/// more become a line moved `offset` to the left. +fn flush(current: &mut Vec, lines: &mut Vec>, offset: f64) { + let piece = std::mem::take(current); + if piece.len() < 2 { + return; + } + let moved = piece + .iter() + .enumerate() + .map(|(i, p)| { + let (a, b) = if i + 1 < piece.len() { + (&piece[i], &piece[i + 1]) + } else { + (&piece[i - 1], &piece[i]) + }; + offset_left(*p, a, b, offset) + }) + .collect(); + lines.push(moved); +} + +/// `ST_InterpolatePoint(line, point)`: the M of `line` at the position +/// nearest `point`. +pub fn interpolate_point( + line: &SridGeometry, + point: [f64; 2], +) -> Result, SpatialError> { + const F: &str = "ST_InterpolatePoint"; + require_m(line, F)?; + let ps = line_positions(line, F)?; + let mut best: Option<(f64, f64)> = None; + for w in ps.windows(2) { + let (a, b) = ([w[0].x, w[0].y], [w[1].x, w[1].y]); + let d = point_to_segment_dist_sq(point, a, b); + if best.is_none_or(|(bd, _)| d < bd) { + let t = fraction_along(a, b, point); + best = lerp(&w[0], &w[1], t).m.map(|m| (d, m)); + } + } + Ok(best + .map(|(_, m)| m) + .or_else(|| ps.first().and_then(|p| p.m))) +} + +#[cfg(test)] +mod tests { + use super::*; + use nodedb_types::geometry::{Ordinates, Srid}; + + fn line_m(coords: Vec<[f64; 2]>, m: Vec) -> SridGeometry { + SridGeometry::with_ordinates( + Srid::new(32648).unwrap(), + Geometry::line_string(coords), + Ordinates::new(None, Some(m)), + ) + .unwrap() + } + + fn planar() -> Metric { + Metric::Planar { + metres_per_unit: 1.0, + } + } + + #[test] + fn interpolate_along_a_planar_line() { + let line = line_m( + vec![[0.0, 0.0], [10.0, 0.0], [10.0, 10.0]], + vec![0.0, 10.0, 20.0], + ); + let p = line_interpolate_point(&line, 0.75, &planar()).unwrap(); + assert_eq!(p.geometry(), &Geometry::point(10.0, 5.0)); + assert_eq!(p.ordinates().m(), Some(&[15.0][..])); + assert!(line_interpolate_point(&line, 1.5, &planar()).is_err()); + } + + #[test] + fn geodesic_interpolation_lies_on_the_geodesic() { + let m = Metric::wgs84(); + let line = SridGeometry::wgs84(Geometry::line_string(vec![[0.0, 0.0], [0.0, 10.0]])); + let mid = line_interpolate_point(&line, 0.5, &m).unwrap(); + let Geometry::Point { coordinates } = mid.geometry() else { + panic!(); + }; + let d0 = m.segment_length([0.0, 0.0], *coordinates); + let d1 = m.segment_length(*coordinates, [0.0, 10.0]); + assert!((d0 - d1).abs() < 1e-6, "{d0} {d1}"); + } + + #[test] + fn locate_along_and_between() { + let line = line_m(vec![[0.0, 0.0], [10.0, 0.0]], vec![0.0, 100.0]); + let at = locate_along(&line, 25.0, 0.0).unwrap(); + assert_eq!( + at.geometry(), + &Geometry::MultiPoint { + coordinates: vec![[2.5, 0.0]] + } + ); + let offset = locate_along(&line, 25.0, 1.0).unwrap(); + assert_eq!( + offset.geometry(), + &Geometry::MultiPoint { + coordinates: vec![[2.5, 1.0]] + } + ); + let between = locate_between(&line, 20.0, 40.0, 0.0).unwrap(); + assert_eq!( + between.geometry(), + &Geometry::MultiLineString { + coordinates: vec![vec![[2.0, 0.0], [4.0, 0.0]]] + } + ); + assert_eq!(between.ordinates().m(), Some(&[20.0, 40.0][..])); + } + + #[test] + fn interpolate_point_reads_m() { + let line = line_m(vec![[0.0, 0.0], [10.0, 0.0]], vec![0.0, 100.0]); + assert_eq!(interpolate_point(&line, [3.0, 4.0]).unwrap(), Some(30.0)); + let xy = SridGeometry::wgs84(Geometry::line_string(vec![[0.0, 0.0], [1.0, 0.0]])); + assert!(interpolate_point(&xy, [0.0, 0.0]).is_err()); + } + + #[test] + fn segmentize_bounds_edge_length_and_keeps_m() { + let line = line_m(vec![[0.0, 0.0], [10.0, 0.0]], vec![0.0, 10.0]); + let dense = segmentize(&line, 3.0, &planar()).unwrap(); + assert_eq!(dense.geometry().coord_count(), 5); + assert_eq!(dense.ordinates().m(), Some(&[0.0, 2.5, 5.0, 7.5, 10.0][..])); + let m = Metric::wgs84(); + let geo = SridGeometry::wgs84(Geometry::line_string(vec![[0.0, 0.0], [1.0, 1.0]])); + let dense = segmentize(&geo, 10_000.0, &m).unwrap(); + let coords: Vec<_> = dense.geometry().coords().collect(); + for w in coords.windows(2) { + assert!(m.segment_length(w[0], w[1]) <= 10_000.0 + 1e-6); + } + } +} diff --git a/nodedb-spatial/src/ops/mod.rs b/nodedb-spatial/src/ops/mod.rs new file mode 100644 index 000000000..a5f1a1981 --- /dev/null +++ b/nodedb-spatial/src/ops/mod.rs @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Geometry operations: boolean overlay, buffer, DE-9IM, simplification, +//! hulls, repair, linear referencing, subdivision, splitting and tiles. + +pub mod boolean; +pub mod buffer; +pub mod closest; +pub(crate) mod convert; +pub mod hull; +pub mod linear; +pub(crate) mod noding; +pub(crate) mod pieces; +pub mod relate; +pub mod repair; +pub mod simplify; +pub mod split; +pub mod subdivide; +pub mod tile; + +pub use boolean::{difference, intersection, sym_difference, union}; +pub use buffer::buffer; +pub use closest::closest_point; +pub use hull::{Hull, hull}; +pub use linear::{ + LinearError, interpolate_point, line_interpolate_point, locate_along, locate_between, + segmentize, +}; +pub use relate::{Relation, relate, relate_pattern, relates}; +pub use repair::make_valid; +pub use simplify::{Simplifier, simplify}; +pub use split::split; +pub use subdivide::{MIN_SUBDIVIDE_VERTICES, subdivide}; +pub use tile::{ + MAX_TILE_ZOOM, TileError, WEB_MERCATOR_HALF_SIDE, shift_longitude, tile_envelope, + web_mercator_bounds, +}; diff --git a/nodedb-spatial/src/ops/noding.rs b/nodedb-spatial/src/ops/noding.rs new file mode 100644 index 000000000..5ac1d7a27 --- /dev/null +++ b/nodedb-spatial/src/ops/noding.rs @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Exact-as-floats intersection of two segments. + +/// Cross-product magnitude under which segments count as parallel. +const PARALLEL_EPS: f64 = 1e-12; + +/// Where two segments meet. +#[derive(Debug, Clone, Copy, PartialEq)] +pub(crate) enum Meet { + Disjoint, + Point([f64; 2]), + /// Collinear segments sharing a stretch, start and end in the first + /// segment's direction. + Overlap([f64; 2], [f64; 2]), +} + +fn sub(a: [f64; 2], b: [f64; 2]) -> [f64; 2] { + [a[0] - b[0], a[1] - b[1]] +} + +fn cross(a: [f64; 2], b: [f64; 2]) -> f64 { + a[0] * b[1] - a[1] * b[0] +} + +fn dot(a: [f64; 2], b: [f64; 2]) -> f64 { + a[0] * b[0] + a[1] * b[1] +} + +fn lerp(a: [f64; 2], b: [f64; 2], t: f64) -> [f64; 2] { + [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t] +} + +/// Whether `x` lies on segment p–q, within the parallel tolerance. +fn on(p: [f64; 2], q: [f64; 2], x: [f64; 2]) -> bool { + let d = sub(q, p); + let len = dot(d, d).sqrt(); + if len == 0.0 { + return p == x; + } + let px = sub(x, p); + cross(d, px).abs() <= PARALLEL_EPS * len * len.max(px[0].hypot(px[1])) + && (0.0..=len * len).contains(&dot(px, d)) +} + +/// Where segment p1–q1 meets segment p2–q2. +pub(crate) fn meet(p1: [f64; 2], q1: [f64; 2], p2: [f64; 2], q2: [f64; 2]) -> Meet { + let r = sub(q1, p1); + let s = sub(q2, p2); + let r_len = dot(r, r).sqrt(); + let s_len = dot(s, s).sqrt(); + if r_len == 0.0 { + return if on(p2, q2, p1) { + Meet::Point(p1) + } else { + Meet::Disjoint + }; + } + if s_len == 0.0 { + return if on(p1, q1, p2) { + Meet::Point(p2) + } else { + Meet::Disjoint + }; + } + let qp = sub(p2, p1); + let denom = cross(r, s); + if denom.abs() <= PARALLEL_EPS * r_len * s_len { + let qp_len = qp[0].hypot(qp[1]); + if cross(qp, r).abs() > PARALLEL_EPS * r_len * r_len.max(qp_len) { + return Meet::Disjoint; + } + let rr = r_len * r_len; + let t0 = dot(qp, r) / rr; + let t1 = t0 + dot(s, r) / rr; + let (lo, hi) = (t0.min(t1).max(0.0), t0.max(t1).min(1.0)); + if lo > hi { + return Meet::Disjoint; + } + let (a, b) = (lerp(p1, q1, lo), lerp(p1, q1, hi)); + return if a == b { + Meet::Point(a) + } else { + Meet::Overlap(a, b) + }; + } + let t = cross(qp, s) / denom; + let u = cross(qp, r) / denom; + if (0.0..=1.0).contains(&t) && (0.0..=1.0).contains(&u) { + Meet::Point(lerp(p1, q1, t)) + } else { + Meet::Disjoint + } +} + +/// The fraction along p–q of the point on it nearest to `x`, in [0, 1]. +pub(crate) fn fraction_along(p: [f64; 2], q: [f64; 2], x: [f64; 2]) -> f64 { + let d = sub(q, p); + let len_sq = dot(d, d); + if len_sq == 0.0 { + return 0.0; + } + (dot(sub(x, p), d) / len_sq).clamp(0.0, 1.0) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn crossing_segments_meet_at_a_point() { + assert_eq!( + meet([0.0, 0.0], [2.0, 2.0], [0.0, 2.0], [2.0, 0.0]), + Meet::Point([1.0, 1.0]) + ); + } + + #[test] + fn collinear_segments_overlap() { + assert_eq!( + meet([0.0, 0.0], [4.0, 0.0], [2.0, 0.0], [6.0, 0.0]), + Meet::Overlap([2.0, 0.0], [4.0, 0.0]) + ); + assert_eq!( + meet([0.0, 0.0], [1.0, 0.0], [2.0, 0.0], [3.0, 0.0]), + Meet::Disjoint + ); + } + + #[test] + fn parallel_offset_segments_are_disjoint() { + assert_eq!( + meet([0.0, 0.0], [4.0, 0.0], [0.0, 1.0], [4.0, 1.0]), + Meet::Disjoint + ); + } +} diff --git a/nodedb-spatial/src/ops/pieces.rs b/nodedb-spatial/src/ops/pieces.rs new file mode 100644 index 000000000..225f6ebf8 --- /dev/null +++ b/nodedb-spatial/src/ops/pieces.rs @@ -0,0 +1,158 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! A geometry split into points, lines and polygons whose coordinates +//! carry their Z and M, and the way back. + +use nodedb_types::geometry::{ + Dimension, Geometry, OrdinateError, OrdinatesBuilder, Position, Srid, SridGeometry, +}; + +/// One component with its positions. +#[derive(Debug, Clone, PartialEq)] +pub(crate) enum Piece { + Point(Position), + Line(Vec), + Polygon(Vec>), +} + +/// Every component of `g` in traversal order. EMPTY components are skipped. +pub(crate) fn pieces(g: &SridGeometry) -> Vec { + let positions: Vec = g.positions().collect(); + let mut cursor = 0usize; + let mut out = Vec::new(); + walk(g.geometry(), &positions, &mut cursor, &mut out); + out +} + +fn take(positions: &[Position], cursor: &mut usize, n: usize) -> Vec { + let end = (*cursor + n).min(positions.len()); + let slice = positions[*cursor..end].to_vec(); + *cursor = end; + slice +} + +fn walk(geom: &Geometry, positions: &[Position], cursor: &mut usize, out: &mut Vec) { + match geom { + Geometry::Point { .. } if geom.is_empty() => {} + Geometry::Point { .. } => { + out.extend(take(positions, cursor, 1).into_iter().map(Piece::Point)) + } + Geometry::MultiPoint { coordinates } => out.extend( + take(positions, cursor, coordinates.len()) + .into_iter() + .map(Piece::Point), + ), + Geometry::LineString { coordinates } => { + out.push(Piece::Line(take(positions, cursor, coordinates.len()))); + } + Geometry::MultiLineString { coordinates } => { + for line in coordinates { + out.push(Piece::Line(take(positions, cursor, line.len()))); + } + } + Geometry::Polygon { coordinates } => out.push(Piece::Polygon( + coordinates + .iter() + .map(|ring| take(positions, cursor, ring.len())) + .collect(), + )), + Geometry::MultiPolygon { coordinates } => { + for polygon in coordinates { + out.push(Piece::Polygon( + polygon + .iter() + .map(|ring| take(positions, cursor, ring.len())) + .collect(), + )); + } + } + Geometry::GeometryCollection { geometries } => { + for member in geometries { + walk(member, positions, cursor, out); + } + } + } +} + +/// Builds a geometry of one dimension from positions. +pub(crate) struct Assembler { + dimension: Dimension, + ordinates: OrdinatesBuilder, +} + +impl Assembler { + pub(crate) fn new(dimension: Dimension) -> Self { + Self { + dimension, + ordinates: OrdinatesBuilder::with_dimension(dimension), + } + } + + /// The XY of `p`, recording its Z and M under the fixed dimension. + pub(crate) fn coord(&mut self, p: &Position) -> Result<[f64; 2], OrdinateError> { + let z = self.dimension.has_z().then(|| p.z.unwrap_or(0.0)); + let m = self.dimension.has_m().then(|| p.m.unwrap_or(0.0)); + self.ordinates.push(z, m)?; + Ok([p.x, p.y]) + } + + pub(crate) fn path(&mut self, ps: &[Position]) -> Result, OrdinateError> { + ps.iter().map(|p| self.coord(p)).collect() + } + + pub(crate) fn finish( + self, + srid: Srid, + geometry: Geometry, + ) -> Result { + SridGeometry::with_ordinates(srid, geometry, self.ordinates.finish()) + } +} + +/// Points as a single point, a multipoint, or `MULTIPOINT EMPTY`, in +/// `dimension`. +pub(crate) fn multipoint( + srid: Srid, + dimension: Dimension, + points: &[Position], +) -> Result { + let mut asm = Assembler::new(dimension); + let coords = asm.path(points)?; + asm.finish( + srid, + Geometry::MultiPoint { + coordinates: coords, + }, + ) +} + +/// Lines as a multilinestring (possibly EMPTY) in `dimension`. +pub(crate) fn multiline( + srid: Srid, + dimension: Dimension, + lines: &[Vec], +) -> Result { + let mut asm = Assembler::new(dimension); + let coords = lines + .iter() + .map(|l| asm.path(l)) + .collect::>()?; + asm.finish( + srid, + Geometry::MultiLineString { + coordinates: coords, + }, + ) +} + +/// Linear interpolation between two positions at fraction `t`. Z and M +/// interpolate where both ends carry them. +pub(crate) fn lerp(a: &Position, b: &Position, t: f64) -> Position { + let mix = |u: Option, v: Option| u.zip(v).map(|(u, v)| u + (v - u) * t); + Position { + x: a.x + (b.x - a.x) * t, + y: a.y + (b.y - a.y) * t, + z: mix(a.z, b.z), + m: mix(a.m, b.m), + } +} diff --git a/nodedb-spatial/src/ops/relate.rs b/nodedb-spatial/src/ops/relate.rs new file mode 100644 index 000000000..a0f10783b --- /dev/null +++ b/nodedb-spatial/src/ops/relate.rs @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! DE-9IM: `ST_Relate` and the named predicates it defines (`ST_Touches`, +//! `ST_Crosses`, `ST_Overlaps`, `ST_Equals`, `ST_Covers`, `ST_CoveredBy`), +//! through `geo::Relate`. Planar in the coordinates. + +use geo::Relate; +use geo::coordinate_position::CoordPos; +use geo::dimensions::Dimensions; +use geo::relate::IntersectionMatrix; +use nodedb_types::geometry::Geometry; + +use super::convert::to_geo; +use crate::error::SpatialError; + +fn matrix(a: &Geometry, b: &Geometry) -> IntersectionMatrix { + to_geo(a).relate(&to_geo(b)) +} + +/// The DE-9IM matrix of `a` and `b`, as the 9-character string PostGIS +/// returns: rows interior, boundary, exterior of `a`, columns the same of +/// `b`. +pub fn relate(a: &Geometry, b: &Geometry) -> String { + let m = matrix(a, b); + let order = [CoordPos::Inside, CoordPos::OnBoundary, CoordPos::Outside]; + let mut out = String::with_capacity(9); + for lhs in order { + for rhs in order { + out.push(match m.get(lhs, rhs) { + Dimensions::Empty => 'F', + Dimensions::ZeroDimensional => '0', + Dimensions::OneDimensional => '1', + Dimensions::TwoDimensional => '2', + }); + } + } + out +} + +/// Whether the DE-9IM matrix of `a` and `b` matches `pattern` +/// (`ST_Relate(a, b, pattern)`). Characters: `0 1 2 T F *`. +pub fn relate_pattern(a: &Geometry, b: &Geometry, pattern: &str) -> Result { + matrix(a, b) + .matches(pattern) + .map_err(|e| SpatialError::Pattern { + pattern: pattern.into(), + reason: e.to_string().into(), + }) +} + +/// The named DE-9IM predicates. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Relation { + Touches, + Crosses, + Overlaps, + Equals, + Covers, + CoveredBy, +} + +/// Whether `a` and `b` stand in `relation`. +pub fn relates(a: &Geometry, b: &Geometry, relation: Relation) -> bool { + let m = matrix(a, b); + match relation { + Relation::Touches => m.is_touches(), + Relation::Crosses => m.is_crosses(), + Relation::Overlaps => m.is_overlaps(), + Relation::Equals => m.is_equal_topo(), + Relation::Covers => m.is_covers(), + Relation::CoveredBy => m.is_coveredby(), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn square(x0: f64, y0: f64, side: f64) -> Geometry { + Geometry::polygon(vec![vec![ + [x0, y0], + [x0 + side, y0], + [x0 + side, y0 + side], + [x0, y0 + side], + [x0, y0], + ]]) + } + + /// shapely 2.2.0: box(0,0,2,2).relate(box(1,1,3,3)) == '212101212'. + #[test] + fn overlapping_squares_matrix() { + assert_eq!( + relate(&square(0.0, 0.0, 2.0), &square(1.0, 1.0, 2.0)), + "212101212" + ); + assert!(relates( + &square(0.0, 0.0, 2.0), + &square(1.0, 1.0, 2.0), + Relation::Overlaps + )); + } + + #[test] + fn named_predicates() { + let (a, b) = (square(0.0, 0.0, 1.0), square(1.0, 0.0, 1.0)); + assert!(relates(&a, &b, Relation::Touches)); + let line = Geometry::line_string(vec![[-1.0, 0.5], [0.5, 0.5]]); + assert!(relates(&line, &a, Relation::Crosses)); + assert!(relates(&a, &a.clone(), Relation::Equals)); + assert!(relates(&square(0.0, 0.0, 4.0), &a, Relation::Covers)); + assert!(relates(&a, &square(0.0, 0.0, 4.0), Relation::CoveredBy)); + assert!(relate_pattern(&a, &b, "FF*F0****").is_ok()); + assert!(relate_pattern(&a, &b, "bad").is_err()); + } +} diff --git a/nodedb-spatial/src/ops/repair.rs b/nodedb-spatial/src/ops/repair.rs new file mode 100644 index 000000000..51d25c0ac --- /dev/null +++ b/nodedb-spatial/src/ops/repair.rs @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! `ST_MakeValid`. +//! +//! Polygons repair through `geo::MakeValid` (constrained Delaunay +//! triangulation, odd-even fill, Ledoux et al. 2014): a self-intersecting +//! ring splits into its valid pieces, a dangling spike drops, an unclosed +//! ring closes. A line with fewer than two distinct points becomes a point. +//! Repeated consecutive vertices drop. + +use geo::MakeValid; +use nodedb_types::geometry::Geometry; + +use super::convert::{multipolygon, polygon, simplest}; +use crate::error::SpatialError; + +/// A valid geometry with the same point set where one exists. +pub fn make_valid(geom: &Geometry) -> Result { + Ok(match geom { + g if g.is_empty() => g.clone(), + Geometry::Point { .. } | Geometry::MultiPoint { .. } => geom.clone(), + Geometry::LineString { coordinates } => line(coordinates), + Geometry::MultiLineString { coordinates } => { + let parts: Vec = coordinates.iter().map(|l| line(l)).collect(); + if parts + .iter() + .all(|p| matches!(p, Geometry::LineString { .. })) + { + Geometry::MultiLineString { + coordinates: parts + .into_iter() + .filter_map(|p| match p { + Geometry::LineString { coordinates } => Some(coordinates), + _ => None, + }) + .collect(), + } + } else { + Geometry::GeometryCollection { geometries: parts } + } + } + Geometry::Polygon { coordinates } => repaired(polygon(coordinates).make_valid())?, + Geometry::MultiPolygon { coordinates } => repaired( + geo::MultiPolygon::new(coordinates.iter().map(|p| polygon(p)).collect()).make_valid(), + )?, + Geometry::GeometryCollection { geometries } => Geometry::GeometryCollection { + geometries: geometries + .iter() + .map(make_valid) + .collect::>()?, + }, + }) +} + +fn repaired( + result: Result, E>, +) -> Result { + let mp = result.map_err(|e| SpatialError::Repair { + reason: e.to_string().into(), + })?; + let g = multipolygon(&mp); + Ok(if g.is_empty() { + Geometry::empty(nodedb_types::geometry::GeometryKind::Polygon) + } else { + simplest(g) + }) +} + +/// A line without repeated consecutive vertices, or a point when one +/// distinct vertex remains. +fn line(coords: &[[f64; 2]]) -> Geometry { + let mut out: Vec<[f64; 2]> = Vec::with_capacity(coords.len()); + for c in coords { + if out.last() != Some(c) { + out.push(*c); + } + } + match out.as_slice() { + [only] => Geometry::point(only[0], only[1]), + _ => Geometry::line_string(out), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::planar::planar_area; + use crate::validate::is_valid; + + /// shapely 2.2.0: make_valid(bowtie) is a MultiPolygon of area 50. + #[test] + fn bowtie_splits_into_two_triangles() { + let bowtie = Geometry::polygon(vec![vec![ + [0.0, 0.0], + [0.0, 10.0], + [10.0, 0.0], + [10.0, 10.0], + [0.0, 0.0], + ]]); + assert!(!is_valid(&bowtie)); + let fixed = make_valid(&bowtie).unwrap(); + assert!(is_valid(&fixed), "{fixed:?}"); + assert!((planar_area(&fixed) - 50.0).abs() < 1e-9); + assert!( + matches!(fixed, Geometry::MultiPolygon { ref coordinates } if coordinates.len() == 2) + ); + } + + #[test] + fn collapsed_line_becomes_a_point() { + let line = Geometry::line_string(vec![[1.0, 1.0], [1.0, 1.0]]); + assert_eq!(make_valid(&line).unwrap(), Geometry::point(1.0, 1.0)); + } +} diff --git a/nodedb-spatial/src/ops/simplify.rs b/nodedb-spatial/src/ops/simplify.rs new file mode 100644 index 000000000..fd93e63ef --- /dev/null +++ b/nodedb-spatial/src/ops/simplify.rs @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! `ST_Simplify` and `ST_SimplifyPreserveTopology`, planar in the +//! coordinates. +//! +//! `ST_Simplify` is Ramer–Douglas–Peucker: a vertex goes when it lies +//! within `tolerance` of the simplified line. `ST_SimplifyPreserveTopology` +//! is Visvalingam–Whyatt with topology preservation: a vertex goes when its +//! triangle with its neighbours has an area under `tolerance² / 2` and +//! removing it creates no self-intersection. Points pass through. + +use geo::{Simplify, SimplifyVwPreserve}; +use nodedb_types::geometry::Geometry; + +use super::convert::{from_geo, to_geo}; + +/// The simplification algorithm. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Simplifier { + DouglasPeucker, + PreserveTopology, +} + +/// Simplify every linear and areal component of `geom`. +pub fn simplify(geom: &Geometry, tolerance: f64, simplifier: Simplifier) -> Geometry { + match geom { + Geometry::GeometryCollection { geometries } => Geometry::GeometryCollection { + geometries: geometries + .iter() + .map(|g| simplify(g, tolerance, simplifier)) + .collect(), + }, + g if g.is_empty() => g.clone(), + g => { + let area = tolerance * tolerance / 2.0; + let out = match (to_geo(g), simplifier) { + (geo::Geometry::LineString(l), Simplifier::DouglasPeucker) => { + geo::Geometry::LineString(l.simplify(tolerance)) + } + (geo::Geometry::LineString(l), Simplifier::PreserveTopology) => { + geo::Geometry::LineString(l.simplify_vw_preserve(area)) + } + (geo::Geometry::MultiLineString(l), Simplifier::DouglasPeucker) => { + geo::Geometry::MultiLineString(l.simplify(tolerance)) + } + (geo::Geometry::MultiLineString(l), Simplifier::PreserveTopology) => { + geo::Geometry::MultiLineString(l.simplify_vw_preserve(area)) + } + (geo::Geometry::Polygon(p), Simplifier::DouglasPeucker) => { + geo::Geometry::Polygon(p.simplify(tolerance)) + } + (geo::Geometry::Polygon(p), Simplifier::PreserveTopology) => { + geo::Geometry::Polygon(p.simplify_vw_preserve(area)) + } + (geo::Geometry::MultiPolygon(p), Simplifier::DouglasPeucker) => { + geo::Geometry::MultiPolygon(p.simplify(tolerance)) + } + (geo::Geometry::MultiPolygon(p), Simplifier::PreserveTopology) => { + geo::Geometry::MultiPolygon(p.simplify_vw_preserve(area)) + } + (other, _) => other, + }; + from_geo(&out) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn douglas_peucker_drops_near_collinear_vertices() { + let line = Geometry::line_string(vec![[0.0, 0.0], [5.0, 0.1], [10.0, 0.0]]); + let out = simplify(&line, 0.5, Simplifier::DouglasPeucker); + assert_eq!(out, Geometry::line_string(vec![[0.0, 0.0], [10.0, 0.0]])); + let kept = simplify(&line, 0.01, Simplifier::DouglasPeucker); + assert_eq!(kept, line); + } + + #[test] + fn preserve_topology_keeps_a_valid_polygon() { + let poly = Geometry::polygon(vec![vec![ + [0.0, 0.0], + [5.0, 0.01], + [10.0, 0.0], + [10.0, 10.0], + [0.0, 10.0], + [0.0, 0.0], + ]]); + let out = simplify(&poly, 1.0, Simplifier::PreserveTopology); + let Geometry::Polygon { coordinates } = &out else { + panic!("{out:?}"); + }; + assert_eq!(coordinates[0].len(), 5); + } +} diff --git a/nodedb-spatial/src/ops/split.rs b/nodedb-spatial/src/ops/split.rs new file mode 100644 index 000000000..cfe3c6246 --- /dev/null +++ b/nodedb-spatial/src/ops/split.rs @@ -0,0 +1,451 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! `ST_Split(input, blade)`: cut a line by points, lines or polygon +//! boundaries, or a polygon by lines or polygon boundaries. The pieces +//! return as a GEOMETRYCOLLECTION, as PostGIS returns them. Planar in the +//! coordinates. +//! +//! A polygon splits by polygonizing: its rings and the blade's parts inside +//! it are noded at every crossing, the faces of the planar graph are +//! traced, and every face inside the input becomes a piece. A blade stretch +//! that does not reach across a face leaves that face whole. + +use std::collections::{HashMap, HashSet}; + +use geo::{BooleanOps, InteriorPoint}; +use nodedb_types::geometry::{Geometry, point_in_polygon}; + +use super::convert::{polygon, rings as geo_rings}; +use super::noding::{Meet, fraction_along, meet}; +use crate::error::SpatialError; +use crate::parts::polygon_covers; +use crate::planar::area::signed_area; +use crate::predicates::edge::{point_on_ring_boundary, point_on_segment, ring_edges}; + +/// Vertices closer than this fraction of the extent merge when noding. +const SNAP_FRACTION: f64 = 1e-9; + +/// A kept polygon face: outer ring, signed area, and the holes it owns. +type Face = (Vec<[f64; 2]>, f64, Vec>); + +/// Blade parts by dimension. +struct Blade { + points: Vec<[f64; 2]>, + lines: Vec>, +} + +impl Blade { + fn of(geom: &Geometry) -> Result { + let mut blade = Blade { + points: Vec::new(), + lines: Vec::new(), + }; + blade.add(geom)?; + Ok(blade) + } + + fn add(&mut self, geom: &Geometry) -> Result<(), SpatialError> { + match geom { + g if g.is_empty() => {} + Geometry::Point { coordinates } => self.points.push(*coordinates), + Geometry::MultiPoint { coordinates } => self.points.extend_from_slice(coordinates), + Geometry::LineString { coordinates } => self.lines.push(coordinates.clone()), + Geometry::MultiLineString { coordinates } => { + self.lines.extend(coordinates.iter().cloned()) + } + Geometry::Polygon { coordinates } => self.lines.extend(closed(coordinates)), + Geometry::MultiPolygon { coordinates } => { + self.lines + .extend(coordinates.iter().flat_map(|p| closed(p))); + } + Geometry::GeometryCollection { geometries } => { + for member in geometries { + self.add(member)?; + } + } + } + Ok(()) + } +} + +/// Rings as closed paths. +fn closed(rings: &[Vec<[f64; 2]>]) -> Vec> { + rings + .iter() + .map(|r| { + let mut r = r.clone(); + if let (Some(first), Some(last)) = (r.first().copied(), r.last()) + && first != *last + { + r.push(first); + } + r + }) + .collect() +} + +fn unsupported(geom: &Geometry) -> SpatialError { + SpatialError::Unsupported { + operation: "ST_Split", + kinds: format!("a {} input", geom.geometry_type()).into(), + } +} + +/// Split `input` by `blade`. +pub fn split(input: &Geometry, blade: &Geometry) -> Result { + let blade = Blade::of(blade)?; + let geometries = match input { + g if g.is_empty() => Vec::new(), + Geometry::LineString { coordinates } => split_line(coordinates, &blade) + .into_iter() + .map(Geometry::line_string) + .collect(), + Geometry::MultiLineString { coordinates } => coordinates + .iter() + .flat_map(|l| split_line(l, &blade)) + .map(Geometry::line_string) + .collect(), + Geometry::Polygon { coordinates } => split_polygon(coordinates, &blade.lines) + .into_iter() + .map(Geometry::polygon) + .collect(), + Geometry::MultiPolygon { coordinates } => coordinates + .iter() + .flat_map(|p| split_polygon(p, &blade.lines)) + .map(Geometry::polygon) + .collect(), + other => return Err(unsupported(other)), + }; + Ok(Geometry::GeometryCollection { geometries }) +} + +/// Cut one line at every blade point on it and every blade crossing. +fn split_line(line: &[[f64; 2]], blade: &Blade) -> Vec> { + let mut cuts: Vec<(usize, f64)> = Vec::new(); + for (i, w) in line.windows(2).enumerate() { + for p in &blade.points { + if point_on_segment(*p, w[0], w[1]) { + cuts.push((i, fraction_along(w[0], w[1], *p))); + } + } + for bl in &blade.lines { + for bw in bl.windows(2) { + match meet(w[0], w[1], bw[0], bw[1]) { + Meet::Disjoint => {} + Meet::Point(p) => cuts.push((i, fraction_along(w[0], w[1], p))), + Meet::Overlap(s, e) => { + cuts.push((i, fraction_along(w[0], w[1], s))); + cuts.push((i, fraction_along(w[0], w[1], e))); + } + } + } + } + } + cuts.sort_by(|a, b| a.0.cmp(&b.0).then(a.1.total_cmp(&b.1))); + cuts.dedup(); + let mut pieces = Vec::new(); + let mut current = vec![line[0]]; + let mut cut_iter = cuts.into_iter().peekable(); + for (i, w) in line.windows(2).enumerate() { + while let Some(&(ci, t)) = cut_iter.peek() { + if ci != i { + break; + } + cut_iter.next(); + let p = [ + w[0][0] + (w[1][0] - w[0][0]) * t, + w[0][1] + (w[1][1] - w[0][1]) * t, + ]; + if current.last() != Some(&p) { + current.push(p); + } + if current.len() >= 2 { + pieces.push(std::mem::replace(&mut current, vec![p])); + } + } + if current.last() != Some(&w[1]) { + current.push(w[1]); + } + } + if current.len() >= 2 { + pieces.push(current); + } + pieces +} + +/// Split one polygon by blade lines. No blade inside it: the polygon +/// comes back whole. +fn split_polygon(rings: &[Vec<[f64; 2]>], blade: &[Vec<[f64; 2]>]) -> Vec>> { + if rings.is_empty() { + return Vec::new(); + } + let shape = polygon(rings); + let blade_lines = geo::MultiLineString::new( + blade + .iter() + .map(|l| { + geo::LineString::new(l.iter().map(|c| geo::Coord { x: c[0], y: c[1] }).collect()) + }) + .collect(), + ); + let inside = shape.clip(&blade_lines, false); + let mut segments: Vec<([f64; 2], [f64; 2])> = + rings.iter().flat_map(|r| ring_edges(r)).collect(); + let ring_count = segments.len(); + for l in &inside.0 { + segments.extend(l.0.windows(2).map(|w| ([w[0].x, w[0].y], [w[1].x, w[1].y]))); + } + if segments.len() == ring_count { + return vec![rings.to_vec()]; + } + let graph = Graph::build(&noded(&segments)); + let faces = graph.faces(); + let mut kept: Vec = Vec::new(); + let mut holes = Vec::new(); + for face in faces { + let area = signed_area(&face); + if area > 0.0 { + let candidate = polygon(std::slice::from_ref(&face)); + let interior = candidate.interior_point(); + if interior.is_some_and(|p| polygon_covers(rings, [p.x(), p.y()])) { + kept.push((face, area, Vec::new())); + } + } else if area < 0.0 { + holes.push(face); + } + } + for hole in holes { + let Some(probe) = hole.first().copied() else { + continue; + }; + let owner = kept + .iter_mut() + .filter(|(outer, _, _)| { + point_in_polygon(probe[0], probe[1], outer) && !point_on_ring_boundary(probe, outer) + }) + .min_by(|a, b| a.1.total_cmp(&b.1)); + if let Some((_, _, owned)) = owner { + let mut ring = hole; + ring.reverse(); + owned.push(ring); + } + } + kept.into_iter() + .map(|(outer, _, owned)| { + let mut out = vec![outer]; + out.extend(owned); + geo_rings(&polygon(&out)) + }) + .collect() +} + +/// Every segment cut at every point another segment meets it. +fn noded(segments: &[([f64; 2], [f64; 2])]) -> Vec<([f64; 2], [f64; 2])> { + let mut out = Vec::new(); + for (i, &(a, b)) in segments.iter().enumerate() { + let mut ts = vec![0.0, 1.0]; + for (j, &(c, d)) in segments.iter().enumerate() { + if i == j { + continue; + } + match meet(a, b, c, d) { + Meet::Disjoint => {} + Meet::Point(p) => ts.push(fraction_along(a, b, p)), + Meet::Overlap(s, e) => { + ts.push(fraction_along(a, b, s)); + ts.push(fraction_along(a, b, e)); + } + } + } + ts.sort_by(f64::total_cmp); + ts.dedup(); + let at = |t: f64| [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t]; + out.extend(ts.windows(2).map(|w| (at(w[0]), at(w[1])))); + } + out +} + +/// A planar graph of noded segments. +struct Graph { + vertices: Vec<[f64; 2]>, + /// Neighbours of each vertex, counter-clockwise by angle. + around: Vec>, +} + +impl Graph { + fn build(segments: &[([f64; 2], [f64; 2])]) -> Self { + let extent = segments + .iter() + .flat_map(|(a, b)| [a[0].abs(), a[1].abs(), b[0].abs(), b[1].abs()]) + .fold(1.0f64, f64::max); + let eps = extent * SNAP_FRACTION; + let mut ids: HashMap<(i64, i64), usize> = HashMap::new(); + let mut vertices = Vec::new(); + let mut id = |p: [f64; 2]| { + let key = ((p[0] / eps).round() as i64, (p[1] / eps).round() as i64); + *ids.entry(key).or_insert_with(|| { + vertices.push(p); + vertices.len() - 1 + }) + }; + let mut edges: HashSet<(usize, usize)> = HashSet::new(); + for &(a, b) in segments { + let (u, v) = (id(a), id(b)); + if u != v { + edges.insert((u.min(v), u.max(v))); + } + } + let mut around = vec![Vec::new(); vertices.len()]; + for &(u, v) in &edges { + around[u].push(v); + around[v].push(u); + } + for (u, list) in around.iter_mut().enumerate() { + let o = vertices[u]; + list.sort_by(|&a, &b| { + let angle = |p: [f64; 2]| (p[1] - o[1]).atan2(p[0] - o[0]); + angle(vertices[a]).total_cmp(&angle(vertices[b])) + }); + } + Graph { vertices, around } + } + + /// Every face cycle, as a closed ring with spikes removed. Bounded + /// faces run counter-clockwise, the outer boundary of each connected + /// part clockwise. + fn faces(&self) -> Vec> { + let mut seen: HashSet<(usize, usize)> = HashSet::new(); + let mut faces = Vec::new(); + for (u, list) in self.around.iter().enumerate() { + for &v in list { + if seen.contains(&(u, v)) { + continue; + } + let mut cycle = vec![u]; + let (mut a, mut b) = (u, v); + loop { + seen.insert((a, b)); + let next_list = &self.around[b]; + let Some(idx) = next_list.iter().position(|&x| x == a) else { + break; + }; + let c = next_list[(idx + next_list.len() - 1) % next_list.len()]; + (a, b) = (b, c); + if (a, b) == (u, v) { + break; + } + cycle.push(a); + } + let mut ring: Vec<[f64; 2]> = despike(cycle) + .into_iter() + .map(|i| self.vertices[i]) + .collect(); + if ring.len() >= 3 { + ring.push(ring[0]); + faces.push(ring); + } + } + } + faces + } +} + +/// Remove back-and-forth visits (`a, b, a`) a dangling edge leaves in a +/// cycle. +fn despike(mut cycle: Vec) -> Vec { + let mut changed = true; + while changed && cycle.len() >= 3 { + changed = false; + let n = cycle.len(); + for i in 0..n { + let prev = cycle[(i + n - 1) % n]; + let next = cycle[(i + 1) % n]; + if prev == next { + let (lo, hi) = (i, (i + 1) % n); + if hi > lo { + cycle.drain(lo..=hi); + } else { + cycle.remove(lo); + cycle.remove(0); + } + changed = true; + break; + } + } + } + cycle +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::planar::planar_area; + + fn square() -> Geometry { + Geometry::polygon(vec![vec![ + [0.0, 0.0], + [4.0, 0.0], + [4.0, 4.0], + [0.0, 4.0], + [0.0, 0.0], + ]]) + } + + fn members(g: Geometry) -> Vec { + match g { + Geometry::GeometryCollection { geometries } => geometries, + other => panic!("{other:?}"), + } + } + + #[test] + fn line_splits_a_square_in_two() { + let blade = Geometry::line_string(vec![[2.0, -1.0], [2.0, 5.0]]); + let parts = members(split(&square(), &blade).unwrap()); + assert_eq!(parts.len(), 2, "{parts:?}"); + for p in &parts { + assert!((planar_area(p) - 8.0).abs() < 1e-9, "{p:?}"); + } + } + + #[test] + fn blade_through_a_hole_keeps_the_hole_halves() { + let holed = Geometry::polygon(vec![ + vec![[0.0, 0.0], [4.0, 0.0], [4.0, 4.0], [0.0, 4.0], [0.0, 0.0]], + vec![[1.0, 1.0], [1.0, 3.0], [3.0, 3.0], [3.0, 1.0], [1.0, 1.0]], + ]); + let blade = Geometry::line_string(vec![[2.0, -1.0], [2.0, 5.0]]); + let parts = members(split(&holed, &blade).unwrap()); + assert_eq!(parts.len(), 2); + let total: f64 = parts.iter().map(planar_area).sum(); + assert!((total - 12.0).abs() < 1e-9); + } + + #[test] + fn blade_missing_the_polygon_returns_it_whole() { + let blade = Geometry::line_string(vec![[10.0, 10.0], [11.0, 11.0]]); + let parts = members(split(&square(), &blade).unwrap()); + assert_eq!(parts.len(), 1); + assert!((planar_area(&parts[0]) - 16.0).abs() < 1e-9); + } + + #[test] + fn line_splits_at_points_and_crossings() { + let line = Geometry::line_string(vec![[0.0, 0.0], [10.0, 0.0]]); + let by_point = members(split(&line, &Geometry::point(4.0, 0.0)).unwrap()); + assert_eq!( + by_point, + vec![ + Geometry::line_string(vec![[0.0, 0.0], [4.0, 0.0]]), + Geometry::line_string(vec![[4.0, 0.0], [10.0, 0.0]]), + ] + ); + let cross = Geometry::line_string(vec![[5.0, -1.0], [5.0, 1.0]]); + assert_eq!(members(split(&line, &cross).unwrap()).len(), 2); + } + + #[test] + fn point_input_is_refused() { + assert!(split(&Geometry::point(0.0, 0.0), &square()).is_err()); + } +} diff --git a/nodedb-spatial/src/ops/subdivide.rs b/nodedb-spatial/src/ops/subdivide.rs new file mode 100644 index 000000000..dee04f813 --- /dev/null +++ b/nodedb-spatial/src/ops/subdivide.rs @@ -0,0 +1,157 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! `ST_Subdivide(geom, max_vertices)`: cut a geometry along box halves until +//! every piece has at most `max_vertices` vertices. The pieces return as +//! one GEOMETRYCOLLECTION (a scalar function returns one value), each +//! piece a polygon, line or point. Planar in the coordinates. + +use geo::{BooleanOps, LineString, MultiLineString, MultiPolygon, Rect}; +use nodedb_types::BoundingBox; +use nodedb_types::geometry::Geometry; + +use super::convert::{from_geo, rings, to_geo}; + +/// Smallest vertex budget, as PostGIS requires. +pub const MIN_SUBDIVIDE_VERTICES: usize = 5; + +/// Recursion limit: 50 halvings shrink a box below any f64 coordinate +/// step. +const MAX_DEPTH: usize = 50; + +/// Cut `geom` into pieces of at most `max_vertices` vertices. +/// `max_vertices` below [`MIN_SUBDIVIDE_VERTICES`] rises to it. +pub fn subdivide(geom: &Geometry, max_vertices: usize) -> Geometry { + let budget = max_vertices.max(MIN_SUBDIVIDE_VERTICES); + let mut pieces = Vec::new(); + for part in parts(geom) { + split(part, budget, MAX_DEPTH, &mut pieces); + } + Geometry::GeometryCollection { geometries: pieces } +} + +/// Polygons, lines and points of `geom`, one per component. +fn parts(geom: &Geometry) -> Vec { + match geom { + g if g.is_empty() => Vec::new(), + Geometry::MultiPoint { coordinates } => coordinates + .iter() + .map(|c| Geometry::point(c[0], c[1])) + .collect(), + Geometry::MultiLineString { coordinates } => coordinates + .iter() + .map(|l| Geometry::line_string(l.clone())) + .collect(), + Geometry::MultiPolygon { coordinates } => coordinates + .iter() + .map(|p| Geometry::polygon(p.clone())) + .collect(), + Geometry::GeometryCollection { geometries } => geometries.iter().flat_map(parts).collect(), + other => vec![other.clone()], + } +} + +fn split(geom: Geometry, budget: usize, depth: usize, out: &mut Vec) { + if geom.is_empty() { + return; + } + if geom.coord_count() <= budget || depth == 0 { + out.push(geom); + return; + } + let bb = nodedb_types::geometry_bbox_in(&geom, nodedb_types::BboxSpace::Planar); + let (a, b) = halves(&bb); + for half in [a, b] { + for piece in clip(&geom, &half) { + split(piece, budget, depth - 1, out); + } + } +} + +/// The two halves of `bb` across its longer side. +fn halves(bb: &BoundingBox) -> (BoundingBox, BoundingBox) { + if bb.max_lng - bb.min_lng >= bb.max_lat - bb.min_lat { + let mid = (bb.min_lng + bb.max_lng) / 2.0; + ( + BoundingBox::new(bb.min_lng, bb.min_lat, mid, bb.max_lat), + BoundingBox::new(mid, bb.min_lat, bb.max_lng, bb.max_lat), + ) + } else { + let mid = (bb.min_lat + bb.max_lat) / 2.0; + ( + BoundingBox::new(bb.min_lng, bb.min_lat, bb.max_lng, mid), + BoundingBox::new(bb.min_lng, mid, bb.max_lng, bb.max_lat), + ) + } +} + +/// The parts of one component inside `bb`, each a single geometry. +fn clip(geom: &Geometry, bb: &BoundingBox) -> Vec { + let rect = Rect::new( + geo::Coord { + x: bb.min_lng, + y: bb.min_lat, + }, + geo::Coord { + x: bb.max_lng, + y: bb.max_lat, + }, + ) + .to_polygon(); + match to_geo(geom) { + geo::Geometry::Polygon(p) => MultiPolygon::new(vec![p]) + .intersection(&rect) + .0 + .iter() + .map(|p| Geometry::polygon(rings(p))) + .collect(), + geo::Geometry::LineString(l) => { + let lines: MultiLineString = MultiLineString::new(vec![l]); + rect.clip(&lines, false) + .0 + .iter() + .map(|l: &LineString| from_geo(&geo::Geometry::LineString(l.clone()))) + .collect() + } + // A point never exceeds the budget, so it is never cut. + _ => vec![geom.clone()], + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::planar::planar_area; + + fn circle(n: usize) -> Geometry { + let ring: Vec<[f64; 2]> = (0..=n) + .map(|i| { + let a = std::f64::consts::TAU * (i % n) as f64 / n as f64; + [a.cos() * 10.0, a.sin() * 10.0] + }) + .collect(); + Geometry::polygon(vec![ring]) + } + + #[test] + fn pieces_respect_the_budget_and_keep_the_area() { + let c = circle(200); + let out = subdivide(&c, 32); + let Geometry::GeometryCollection { geometries } = &out else { + panic!(); + }; + assert!(geometries.len() > 1); + assert!(geometries.iter().all(|g| g.coord_count() <= 32)); + assert!((planar_area(&out) - planar_area(&c)).abs() < 1e-6); + } + + #[test] + fn small_geometry_is_one_piece() { + let out = subdivide(&Geometry::point(1.0, 2.0), 256); + assert_eq!( + out, + Geometry::GeometryCollection { + geometries: vec![Geometry::point(1.0, 2.0)] + } + ); + } +} diff --git a/nodedb-spatial/src/ops/tile.rs b/nodedb-spatial/src/ops/tile.rs new file mode 100644 index 000000000..fbe46c214 --- /dev/null +++ b/nodedb-spatial/src/ops/tile.rs @@ -0,0 +1,153 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! `ST_TileEnvelope` and `ST_ShiftLongitude`. + +use nodedb_types::BoundingBox; +use nodedb_types::geometry::{Geometry, SridGeometry}; + +/// Half the side of the EPSG:3857 square, metres: π × 6378137. +pub const WEB_MERCATOR_HALF_SIDE: f64 = 20_037_508.342_789_244; + +/// Largest zoom `ST_TileEnvelope` accepts, as PostGIS limits it. +pub const MAX_TILE_ZOOM: u32 = 31; + +/// Why a tile address is out of range. +#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] +pub enum TileError { + #[error("tile zoom {zoom} is outside 0..={max}", max = MAX_TILE_ZOOM)] + Zoom { zoom: i64 }, + #[error("tile column {x} is outside 0..{count} at zoom {zoom}")] + Column { x: i64, zoom: u32, count: u64 }, + #[error("tile row {y} is outside 0..{count} at zoom {zoom}")] + Row { y: i64, zoom: u32, count: u64 }, + #[error("tile bounds have zero or negative extent")] + Bounds, +} + +/// The square EPSG:3857 bounds `ST_TileEnvelope` divides by default. +pub fn web_mercator_bounds() -> BoundingBox { + BoundingBox::new( + -WEB_MERCATOR_HALF_SIDE, + -WEB_MERCATOR_HALF_SIDE, + WEB_MERCATOR_HALF_SIDE, + WEB_MERCATOR_HALF_SIDE, + ) +} + +/// `ST_TileEnvelope(z, x, y [, bounds])`: the polygon of tile (`x`, `y`) +/// at zoom `z` of the XYZ scheme over `bounds`. Row 0 is the top (north) +/// row. +pub fn tile_envelope(z: i64, x: i64, y: i64, bounds: &BoundingBox) -> Result { + let zoom = u32::try_from(z) + .ok() + .filter(|z| *z <= MAX_TILE_ZOOM) + .ok_or(TileError::Zoom { zoom: z })?; + let count = 1u64 << zoom; + let in_range = |v: i64| u64::try_from(v).is_ok_and(|v| v < count); + if !in_range(x) { + return Err(TileError::Column { x, zoom, count }); + } + if !in_range(y) { + return Err(TileError::Row { y, zoom, count }); + } + let width = bounds.max_lng - bounds.min_lng; + let height = bounds.max_lat - bounds.min_lat; + if !(width > 0.0 && height > 0.0) { + return Err(TileError::Bounds); + } + let n = count as f64; + let (tw, th) = (width / n, height / n); + let min_x = bounds.min_lng + x as f64 * tw; + let max_y = bounds.max_lat - y as f64 * th; + let (max_x, min_y) = (min_x + tw, max_y - th); + Ok(Geometry::polygon(vec![vec![ + [min_x, min_y], + [min_x, max_y], + [max_x, max_y], + [max_x, min_y], + [min_x, min_y], + ]])) +} + +/// `ST_ShiftLongitude(geom)`: longitudes below 0 gain 360°, longitudes +/// above 180° lose 360°, so a geometry across the antimeridian reads in +/// 0..360 and back. Z and M are kept. +pub fn shift_longitude(g: &SridGeometry) -> SridGeometry { + let shifted: Result = g.try_map_positions(|mut p| { + if p.x < 0.0 { + p.x += 360.0; + } else if p.x > 180.0 { + p.x -= 360.0; + } + Ok(p) + }); + match shifted { + Ok(g) => g, + Err(never) => match never {}, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use nodedb_types::geometry::Srid; + + #[test] + fn zoom_zero_is_the_whole_square() { + let g = tile_envelope(0, 0, 0, &web_mercator_bounds()).unwrap(); + let Geometry::Polygon { coordinates } = g else { + panic!(); + }; + assert_eq!( + coordinates[0][0], + [-WEB_MERCATOR_HALF_SIDE, -WEB_MERCATOR_HALF_SIDE] + ); + assert_eq!( + coordinates[0][2], + [WEB_MERCATOR_HALF_SIDE, WEB_MERCATOR_HALF_SIDE] + ); + } + + /// PostGIS: ST_TileEnvelope(2, 1, 1) is + /// POLYGON((-10018754.17 0,-10018754.17 10018754.17,0 10018754.17,0 0,-10018754.17 0)). + #[test] + fn zoom_two_tile_matches_postgis() { + let g = tile_envelope(2, 1, 1, &web_mercator_bounds()).unwrap(); + let Geometry::Polygon { coordinates } = g else { + panic!(); + }; + let half = WEB_MERCATOR_HALF_SIDE / 2.0; + assert_eq!(coordinates[0][0], [-half, 0.0]); + assert_eq!(coordinates[0][2], [0.0, half]); + } + + #[test] + fn out_of_range_addresses_fail() { + let b = web_mercator_bounds(); + assert!(matches!( + tile_envelope(32, 0, 0, &b), + Err(TileError::Zoom { .. }) + )); + assert!(matches!( + tile_envelope(1, 2, 0, &b), + Err(TileError::Column { .. }) + )); + assert!(matches!( + tile_envelope(1, 0, -1, &b), + Err(TileError::Row { .. }) + )); + } + + #[test] + fn shift_longitude_wraps_both_ways() { + let g = SridGeometry::new( + Srid::WGS84, + Geometry::line_string(vec![[-179.0, 0.0], [179.0, 0.0], [181.0, 1.0]]), + ); + let s = shift_longitude(&g); + assert_eq!( + s.geometry(), + &Geometry::line_string(vec![[181.0, 0.0], [179.0, 0.0], [-179.0, 1.0]]) + ); + } +} diff --git a/nodedb-spatial/src/orient.rs b/nodedb-spatial/src/orient.rs index 6e284a95e..84c8699ac 100644 --- a/nodedb-spatial/src/orient.rs +++ b/nodedb-spatial/src/orient.rs @@ -9,7 +9,7 @@ use nodedb_types::geometry::Geometry; -use crate::validate::signed_area; +use crate::planar::area::signed_area; /// Reverse every ring of `geom` that breaks the right-hand rule. Polygons /// nested in a `MultiPolygon` or `GeometryCollection` are oriented too. @@ -40,7 +40,21 @@ fn orient_polygon(rings: &mut [Vec<[f64; 2]>]) { #[cfg(test)] mod tests { use super::*; - use crate::validate::validate_geometry; + + /// Whether every shell runs counter-clockwise and every hole clockwise. + fn right_handed(geom: &Geometry) -> bool { + let polygon_ok = |rings: &Vec>| { + rings.iter().enumerate().all(|(i, r)| { + let a = signed_area(r); + if i == 0 { a > 0.0 } else { a < 0.0 } + }) + }; + match geom { + Geometry::Polygon { coordinates } => polygon_ok(coordinates), + Geometry::MultiPolygon { coordinates } => coordinates.iter().all(polygon_ok), + _ => true, + } + } fn square(clockwise: bool, lo: f64, hi: f64) -> Vec<[f64; 2]> { let mut ring = vec![[lo, lo], [hi, lo], [hi, hi], [lo, hi], [lo, lo]]; @@ -51,11 +65,11 @@ mod tests { } #[test] - fn clockwise_exterior_and_ccw_hole_become_valid() { + fn clockwise_exterior_and_ccw_hole_are_reversed() { let mut geom = Geometry::polygon(vec![square(true, 0.0, 10.0), square(false, 2.0, 4.0)]); - assert!(!validate_geometry(&geom).is_empty()); + assert!(!right_handed(&geom)); orient_rings(&mut geom); - assert!(validate_geometry(&geom).is_empty(), "{geom:?}"); + assert!(right_handed(&geom), "{geom:?}"); } #[test] @@ -72,6 +86,6 @@ mod tests { coordinates: vec![vec![square(true, 0.0, 1.0)], vec![square(false, 5.0, 6.0)]], }; orient_rings(&mut geom); - assert!(validate_geometry(&geom).is_empty(), "{geom:?}"); + assert!(right_handed(&geom), "{geom:?}"); } } diff --git a/nodedb-spatial/src/parts.rs b/nodedb-spatial/src/parts.rs new file mode 100644 index 000000000..fb2f45f57 --- /dev/null +++ b/nodedb-spatial/src/parts.rs @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! A geometry broken into the pieces distance algorithms measure between: +//! edges, vertices and polygons. An EMPTY geometry has no pieces. + +use nodedb_types::geometry::{Geometry, point_in_polygon}; + +use crate::predicates::edge::{point_on_ring_boundary, ring_edges}; + +/// The pieces of a geometry a distance is measured between. +#[derive(Default)] +pub(crate) struct Parts<'a> { + /// Every edge, with a lone point as a zero-length edge. + pub(crate) segments: Vec<([f64; 2], [f64; 2])>, + /// Every vertex. + pub(crate) vertices: Vec<[f64; 2]>, + /// Every polygon, as its rings. + pub(crate) polygons: Vec<&'a [Vec<[f64; 2]>]>, +} + +impl<'a> Parts<'a> { + pub(crate) fn of(geom: &'a Geometry) -> Self { + let mut parts = Parts::default(); + parts.add(geom); + parts + } + + fn add(&mut self, geom: &'a Geometry) { + match geom { + Geometry::Point { .. } if geom.is_empty() => {} + Geometry::Point { coordinates } => self.add_point(*coordinates), + Geometry::MultiPoint { coordinates } => { + coordinates.iter().for_each(|c| self.add_point(*c)); + } + Geometry::LineString { coordinates } => self.add_path(coordinates), + Geometry::MultiLineString { coordinates } => { + coordinates.iter().for_each(|line| self.add_path(line)); + } + Geometry::Polygon { coordinates } => self.add_polygon(coordinates), + Geometry::MultiPolygon { coordinates } => { + coordinates.iter().for_each(|rings| self.add_polygon(rings)); + } + Geometry::GeometryCollection { geometries } => { + geometries.iter().for_each(|g| self.add(g)); + } + } + } + + fn add_point(&mut self, c: [f64; 2]) { + self.segments.push((c, c)); + self.vertices.push(c); + } + + fn add_path(&mut self, path: &[[f64; 2]]) { + match path { + [] => {} + [only] => self.add_point(*only), + _ => { + self.segments.extend(path.windows(2).map(|w| (w[0], w[1]))); + self.vertices.extend_from_slice(path); + } + } + } + + fn add_polygon(&mut self, rings: &'a [Vec<[f64; 2]>]) { + for ring in rings { + self.segments.extend(ring_edges(ring)); + self.vertices.extend_from_slice(ring); + } + self.polygons.push(rings); + } + + /// Whether any polygon of `self` covers any vertex of `other`. + pub(crate) fn covers_any_vertex_of(&self, other: &Parts<'_>) -> bool { + self.polygons + .iter() + .any(|rings| other.vertices.iter().any(|v| polygon_covers(rings, *v))) + } +} + +/// Whether `pt` lies inside or on a polygon, outside every hole interior. +pub(crate) fn polygon_covers(rings: &[Vec<[f64; 2]>], pt: [f64; 2]) -> bool { + let Some(exterior) = rings.first() else { + return false; + }; + if !point_in_polygon(pt[0], pt[1], exterior) && !point_on_ring_boundary(pt, exterior) { + return false; + } + !rings[1..] + .iter() + .any(|hole| point_in_polygon(pt[0], pt[1], hole) && !point_on_ring_boundary(pt, hole)) +} diff --git a/nodedb-spatial/src/persist.rs b/nodedb-spatial/src/persist.rs index af7cfcb82..75c42adba 100644 --- a/nodedb-spatial/src/persist.rs +++ b/nodedb-spatial/src/persist.rs @@ -22,13 +22,14 @@ //! - `{collection}\x00{field}\x00rtree` → serialized R-tree entries //! - `{collection}\x00{field}\x00meta` → SpatialIndexMeta +use nodedb_crs::InvalidSridCode; use nodedb_mem::ScopedMemory; -use nodedb_types::BoundingBox; -use nodedb_types::geometry::{Srid, UnsupportedSrid}; +use nodedb_types::geometry::Srid; +use nodedb_types::{BboxSpace, BoundingBox}; use serde::{Deserialize, Serialize}; use zerompk::{FromMessagePack, ToMessagePack}; -use crate::rtree::{RTree, RTreeEntry}; +use crate::rtree::{RTree, RTreeEntry, TreeSrid}; // ── SEGV framing constants ───────────────────────────────────────────────── // @@ -44,9 +45,9 @@ const SEGV_MAGIC: [u8; 4] = *b"SEGV"; /// Magic header for rkyv-serialized R-tree snapshots (6 bytes). const RTREE_RKYV_MAGIC: &[u8; 6] = b"RKSPT\0"; -/// Current format version for rkyv-serialized R-tree snapshots. Version 2 -/// carries the tree's SRID. -pub const RTREE_FORMAT_VERSION: u8 = 2; +/// Current format version for rkyv-serialized R-tree snapshots. Version 3 +/// carries the tree's SRID and bbox space. +pub const RTREE_FORMAT_VERSION: u8 = 3; // ── SEGV framing helpers ─────────────────────────────────────────────────── @@ -133,8 +134,11 @@ impl std::fmt::Display for SpatialIndexType { /// rkyv-serialized R-tree snapshot. #[derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)] struct RTreeSnapshotRkyv { - /// EPSG code of the tree's SRID. + /// Code of the tree's SRID. srid: u32, + /// Whether the tree's bbox space is planar. A restore reads the space + /// from here, so it never resolves the SRID against a CRS registry. + planar: bool, entries: Vec, } @@ -152,6 +156,7 @@ impl RTree { ) -> Result, RTreeCheckpointError> { let snapshot = RTreeSnapshotRkyv { srid: self.srid().code(), + planar: self.space() == BboxSpace::Planar, entries: self.entries().into_iter().cloned().collect(), }; let rkyv_bytes = rkyv::to_bytes::(&snapshot) @@ -226,7 +231,15 @@ impl RTree { let snapshot: RTreeSnapshotRkyv = rkyv::from_bytes::(&aligned) .map_err(|e| RTreeCheckpointError::RkyvDeserialize(e.to_string()))?; - let srid = Srid::try_from(snapshot.srid)?; + // The SRID is checked for syntax only. + let srid = TreeSrid { + srid: Srid::new(snapshot.srid)?, + space: if snapshot.planar { + BboxSpace::Planar + } else { + BboxSpace::Geographic + }, + }; Ok(RTree::bulk_load(snapshot.entries, memory, srid)) } } @@ -305,9 +318,9 @@ pub enum RTreeCheckpointError { /// AES-256-GCM decryption failed. #[error("spatial checkpoint decryption failed: {0}")] DecryptionFailed(String), - /// The checkpoint names an SRID outside the supported set. + /// The checkpoint names an SRID code outside the valid range. #[error("R-tree checkpoint: {0}")] - UnsupportedSrid(#[from] UnsupportedSrid), + InvalidSrid(#[from] InvalidSridCode), } // ── Tests ────────────────────────────────────────────────────────────────── @@ -315,7 +328,7 @@ pub enum RTreeCheckpointError { #[cfg(test)] mod tests { use super::*; - use crate::test_support::test_memory; + use crate::test_support::{planar_srid, test_memory}; fn make_entry(id: u64, lng: f64, lat: f64) -> RTreeEntry { RTreeEntry { @@ -326,7 +339,7 @@ mod tests { #[test] fn checkpoint_roundtrip_empty() { - let tree = RTree::new(test_memory(), Srid::Wgs84); + let tree = RTree::new(test_memory(), TreeSrid::WGS84); let bytes = tree.checkpoint_to_bytes(None).unwrap(); let restored = RTree::from_checkpoint(&bytes, None, test_memory()).unwrap(); assert_eq!(restored.len(), 0); @@ -334,7 +347,7 @@ mod tests { #[test] fn checkpoint_roundtrip_entries() { - let mut tree = RTree::new(test_memory(), Srid::Wgs84); + let mut tree = RTree::new(test_memory(), TreeSrid::WGS84); for i in 0..100 { tree.insert(make_entry(i, (i as f64) * 0.5, (i as f64) * 0.3)); } @@ -351,7 +364,7 @@ mod tests { #[test] fn checkpoint_preserves_ids() { - let mut tree = RTree::new(test_memory(), Srid::Wgs84); + let mut tree = RTree::new(test_memory(), TreeSrid::WGS84); tree.insert(make_entry(42, 10.0, 20.0)); tree.insert(make_entry(99, 30.0, 40.0)); @@ -379,23 +392,23 @@ mod tests { index_type: SpatialIndexType::RTree, entry_count: 1000, extent: Some(BoundingBox::new(-180.0, -90.0, 180.0, 90.0)), - srid: Srid::PeninsularRso, + srid: Srid::new(3375).unwrap(), }; let bytes = serialize_meta(&meta).unwrap(); let restored = deserialize_meta(&bytes).unwrap(); assert_eq!(restored.collection, "buildings"); assert_eq!(restored.entry_count, 1000); assert_eq!(restored.index_type, SpatialIndexType::RTree); - assert_eq!(restored.srid, Srid::PeninsularRso); + assert_eq!(restored.srid, Srid::new(3375).unwrap()); } #[test] fn checkpoint_preserves_srid() { - let mut tree = RTree::new(test_memory(), Srid::BorneoRso); + let mut tree = RTree::new(test_memory(), planar_srid(3376)); tree.insert(make_entry(7, 708_697.0, 662_326.0)); let bytes = tree.checkpoint_to_bytes(None).unwrap(); let restored = RTree::from_checkpoint(&bytes, None, test_memory()).unwrap(); - assert_eq!(restored.srid(), Srid::BorneoRso); + assert_eq!(restored.tree_srid(), planar_srid(3376)); assert_eq!(restored.len(), 1); } @@ -410,7 +423,7 @@ mod tests { #[test] fn checkpoint_size_reasonable() { - let mut tree = RTree::new(test_memory(), Srid::Wgs84); + let mut tree = RTree::new(test_memory(), TreeSrid::WGS84); for i in 0..1000 { tree.insert(make_entry(i, (i as f64) * 0.01, (i as f64) * 0.01)); } @@ -431,7 +444,7 @@ mod tests { #[test] fn golden_header_layout() { - let mut tree = RTree::new(test_memory(), Srid::Wgs84); + let mut tree = RTree::new(test_memory(), TreeSrid::WGS84); tree.insert(make_entry(1, 10.0, 20.0)); let bytes = tree.checkpoint_to_bytes(None).unwrap(); // Magic at bytes[0..6]. @@ -444,7 +457,7 @@ mod tests { #[test] fn version_mismatch_returns_error() { - let mut tree = RTree::new(test_memory(), Srid::Wgs84); + let mut tree = RTree::new(test_memory(), TreeSrid::WGS84); tree.insert(make_entry(1, 10.0, 20.0)); let mut bytes = tree.checkpoint_to_bytes(None).unwrap(); // Corrupt the version byte to an unsupported value. @@ -466,7 +479,7 @@ mod tests { #[test] fn spatial_rtree_checkpoint_encrypted_at_rest() { let kek = make_test_kek(); - let mut tree = RTree::new(test_memory(), Srid::Wgs84); + let mut tree = RTree::new(test_memory(), TreeSrid::WGS84); for i in 0..50 { tree.insert(make_entry(i, i as f64, i as f64 * 0.5)); } @@ -486,7 +499,7 @@ mod tests { #[test] fn spatial_rtree_refuses_plaintext_when_kek_required() { let kek = make_test_kek(); - let mut tree = RTree::new(test_memory(), Srid::Wgs84); + let mut tree = RTree::new(test_memory(), TreeSrid::WGS84); tree.insert(make_entry(1, 10.0, 20.0)); // Write plaintext checkpoint. @@ -502,7 +515,7 @@ mod tests { #[test] fn spatial_rtree_refuses_encrypted_without_kek() { let kek = make_test_kek(); - let mut tree = RTree::new(test_memory(), Srid::Wgs84); + let mut tree = RTree::new(test_memory(), TreeSrid::WGS84); tree.insert(make_entry(1, 10.0, 20.0)); let enc_bytes = tree.checkpoint_to_bytes(Some(&kek)).unwrap(); @@ -517,7 +530,7 @@ mod tests { #[test] fn spatial_rtree_tampered_ciphertext_rejected() { let kek = make_test_kek(); - let mut tree = RTree::new(test_memory(), Srid::Wgs84); + let mut tree = RTree::new(test_memory(), TreeSrid::WGS84); tree.insert(make_entry(1, 10.0, 20.0)); let mut enc_bytes = tree.checkpoint_to_bytes(Some(&kek)).unwrap(); diff --git a/nodedb-spatial/src/planar/area.rs b/nodedb-spatial/src/planar/area.rs index 286c8b8f4..21b793fc9 100644 --- a/nodedb-spatial/src/planar/area.rs +++ b/nodedb-spatial/src/planar/area.rs @@ -4,13 +4,14 @@ use nodedb_types::geometry::Geometry; -/// Unsigned area of one ring by the shoelace formula. A ring with fewer than -/// three vertices encloses nothing. An unclosed ring is closed implicitly. +/// Signed area of one ring by the shoelace formula, positive +/// counter-clockwise. A ring with fewer than three vertices encloses +/// nothing. An unclosed ring is closed implicitly. /// /// Coordinates are taken relative to the first vertex. Projected eastings and /// northings run to hundreds of kilometres, and summing their raw cross /// products cancels away the digits a small ring's area lives in. -pub fn planar_ring_area(ring: &[[f64; 2]]) -> f64 { +pub fn signed_area(ring: &[[f64; 2]]) -> f64 { if ring.len() < 3 { return 0.0; } @@ -22,7 +23,12 @@ pub fn planar_ring_area(ring: &[[f64; 2]]) -> f64 { let b = rel(ring[(i + 1) % ring.len()]); twice += a[0] * b[1] - b[0] * a[1]; } - (twice / 2.0).abs() + twice / 2.0 +} + +/// Unsigned area of one ring by the shoelace formula. +pub fn planar_ring_area(ring: &[[f64; 2]]) -> f64 { + signed_area(ring).abs() } /// Area of one polygon: exterior ring less every hole, never negative. @@ -51,9 +57,6 @@ pub fn planar_area(geom: &Geometry) -> f64 { | Geometry::LineString { .. } | Geometry::MultiPoint { .. } | Geometry::MultiLineString { .. } => 0.0, - // `Geometry` is `#[non_exhaustive]`. A kind added upstream is not - // known to be areal. - _ => 0.0, } } diff --git a/nodedb-spatial/src/planar/buffer.rs b/nodedb-spatial/src/planar/buffer.rs deleted file mode 100644 index d3f2e2474..000000000 --- a/nodedb-spatial/src/planar/buffer.rs +++ /dev/null @@ -1,37 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -//! Planar buffer of projected geometries, by a distance in metres. - -use nodedb_types::BboxSpace; -use nodedb_types::geometry::Geometry; - -use crate::operations::st_buffer_in; - -/// Buffer `geom` by `distance` coordinate units (metres for a projected -/// SRID), approximating curves with `segments` sides. -/// -/// Uses the same construction as the geographic `st_buffer`, with metres -/// added to coordinates directly instead of converted to degrees. -pub fn planar_buffer(geom: &Geometry, distance: f64, segments: usize) -> Geometry { - st_buffer_in(geom, distance, segments, BboxSpace::Planar) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::planar::area::planar_area; - - #[test] - fn buffered_point_area_approaches_circle() { - let segments = 64; - let circle = planar_buffer(&Geometry::point(500_000.0, 200_000.0), 100.0, segments); - let area = planar_area(&circle); - // A regular n-gon inscribed in radius r has area n r² sin(2π/n) / 2. - let n = segments as f64; - let expected = n * 100.0 * 100.0 * (std::f64::consts::TAU / n).sin() / 2.0; - assert!( - (area - expected).abs() < 1e-6, - "area={area} expected={expected}" - ); - } -} diff --git a/nodedb-spatial/src/planar/distance.rs b/nodedb-spatial/src/planar/distance.rs index e3ea7cfda..72e9ba76d 100644 --- a/nodedb-spatial/src/planar/distance.rs +++ b/nodedb-spatial/src/planar/distance.rs @@ -5,96 +5,11 @@ //! Every measure is Euclidean in coordinate units, which for a projected SRID //! are metres on the grid. -use nodedb_types::geometry::{Geometry, point_in_polygon}; -use nodedb_types::{BboxSpace, geometry_bbox_in}; +use nodedb_types::geometry::Geometry; +use nodedb_types::{BboxSpace, BoundingBox, geometry_bbox_in}; -use crate::predicates::edge::{point_on_ring_boundary, ring_edges, segment_to_segment_dist_sq}; - -/// The pieces of a geometry a distance is measured between. -#[derive(Default)] -struct Parts<'a> { - /// Every edge, with a lone point as a zero-length edge. - segments: Vec<([f64; 2], [f64; 2])>, - /// Every vertex. - vertices: Vec<[f64; 2]>, - /// Every polygon, as its rings. - polygons: Vec<&'a [Vec<[f64; 2]>]>, -} - -impl<'a> Parts<'a> { - fn of(geom: &'a Geometry) -> Self { - let mut parts = Parts::default(); - parts.add(geom); - parts - } - - fn add(&mut self, geom: &'a Geometry) { - match geom { - Geometry::Point { coordinates } => self.add_point(*coordinates), - Geometry::MultiPoint { coordinates } => { - coordinates.iter().for_each(|c| self.add_point(*c)); - } - Geometry::LineString { coordinates } => self.add_path(coordinates), - Geometry::MultiLineString { coordinates } => { - coordinates.iter().for_each(|line| self.add_path(line)); - } - Geometry::Polygon { coordinates } => self.add_polygon(coordinates), - Geometry::MultiPolygon { coordinates } => { - coordinates.iter().for_each(|rings| self.add_polygon(rings)); - } - Geometry::GeometryCollection { geometries } => { - geometries.iter().for_each(|g| self.add(g)); - } - // `Geometry` is `#[non_exhaustive]`; a kind added upstream exposes - // no coordinates this crate can read. - _ => {} - } - } - - fn add_point(&mut self, c: [f64; 2]) { - self.segments.push((c, c)); - self.vertices.push(c); - } - - fn add_path(&mut self, path: &[[f64; 2]]) { - match path { - [] => {} - [only] => self.add_point(*only), - _ => { - self.segments.extend(path.windows(2).map(|w| (w[0], w[1]))); - self.vertices.extend_from_slice(path); - } - } - } - - fn add_polygon(&mut self, rings: &'a [Vec<[f64; 2]>]) { - for ring in rings { - self.segments.extend(ring_edges(ring)); - self.vertices.extend_from_slice(ring); - } - self.polygons.push(rings); - } - - /// Whether any polygon of `self` covers any vertex of `other`. - fn covers_any_vertex_of(&self, other: &Parts<'_>) -> bool { - self.polygons - .iter() - .any(|rings| other.vertices.iter().any(|v| polygon_covers(rings, *v))) - } -} - -/// Whether `pt` lies inside or on a polygon, outside every hole interior. -fn polygon_covers(rings: &[Vec<[f64; 2]>], pt: [f64; 2]) -> bool { - let Some(exterior) = rings.first() else { - return false; - }; - if !point_in_polygon(pt[0], pt[1], exterior) && !point_on_ring_boundary(pt, exterior) { - return false; - } - !rings[1..] - .iter() - .any(|hole| point_in_polygon(pt[0], pt[1], hole) && !point_on_ring_boundary(pt, hole)) -} +use crate::parts::Parts; +use crate::predicates::edge::{ring_edges, segment_to_segment_dist_sq}; /// Minimum Euclidean distance between two geometries, in coordinate units. /// @@ -121,7 +36,13 @@ pub fn planar_distance(a: &Geometry, b: &Geometry) -> f64 { /// A planar bbox pre-filter rejects clearly distant pairs before the exact /// distance runs. pub fn planar_dwithin(a: &Geometry, b: &Geometry, distance: f64) -> bool { - let a_bb = geometry_bbox_in(a, BboxSpace::Planar).expand_meters_in(distance, BboxSpace::Planar); + let bb = geometry_bbox_in(a, BboxSpace::Planar); + let a_bb = BoundingBox::new( + bb.min_lng - distance, + bb.min_lat - distance, + bb.max_lng + distance, + bb.max_lat + distance, + ); let b_bb = geometry_bbox_in(b, BboxSpace::Planar); if !a_bb.intersects(&b_bb) { return false; @@ -155,9 +76,6 @@ pub fn planar_length(geom: &Geometry) -> f64 { | Geometry::MultiPoint { .. } | Geometry::Polygon { .. } | Geometry::MultiPolygon { .. } => 0.0, - // `Geometry` is `#[non_exhaustive]`; a kind added upstream is not - // known to be linear. - _ => 0.0, } } @@ -181,9 +99,6 @@ pub fn planar_perimeter(geom: &Geometry) -> f64 { | Geometry::MultiPoint { .. } | Geometry::LineString { .. } | Geometry::MultiLineString { .. } => 0.0, - // `Geometry` is `#[non_exhaustive]`; a kind added upstream is not - // known to be areal. - _ => 0.0, } } diff --git a/nodedb-spatial/src/planar/mod.rs b/nodedb-spatial/src/planar/mod.rs index 13d0d1848..45d3cc469 100644 --- a/nodedb-spatial/src/planar/mod.rs +++ b/nodedb-spatial/src/planar/mod.rs @@ -1,11 +1,10 @@ // SPDX-License-Identifier: Apache-2.0 -//! Euclidean measures for geometries in a projected SRID, in metres. +//! Euclidean measures for geometries in a projected SRID, in coordinate +//! units. pub mod area; -pub mod buffer; pub mod distance; pub use area::{planar_area, planar_ring_area}; -pub use buffer::planar_buffer; pub use distance::{planar_distance, planar_dwithin, planar_length, planar_perimeter}; diff --git a/nodedb-spatial/src/predicates/contains.rs b/nodedb-spatial/src/predicates/contains.rs index 61ab25407..c1d387dbc 100644 --- a/nodedb-spatial/src/predicates/contains.rs +++ b/nodedb-spatial/src/predicates/contains.rs @@ -16,8 +16,12 @@ use super::edge::{is_collection, point_on_ring_boundary, ring_edges}; /// ST_Contains(a, b) — does geometry A fully contain geometry B? /// /// Returns false if any part of B lies outside A, or if B only touches -/// A's boundary without entering its interior. +/// A's boundary without entering its interior. EMPTY contains nothing and +/// is contained by nothing. pub fn st_contains(a: &Geometry, b: &Geometry) -> bool { + if a.is_empty() || b.is_empty() { + return false; + } // Fast path: bbox pre-filter. A GeometryCollection skips it: its bbox is // a union whose wrap rule depends on the SRID, and the arms below test // each member on its own. diff --git a/nodedb-spatial/src/predicates/distance.rs b/nodedb-spatial/src/predicates/distance.rs index 2fd827b7e..aef1830aa 100644 --- a/nodedb-spatial/src/predicates/distance.rs +++ b/nodedb-spatial/src/predicates/distance.rs @@ -1,282 +1,23 @@ // SPDX-License-Identifier: Apache-2.0 -//! ST_Distance and ST_DWithin — minimum distance between geometries. -//! -//! Point-to-point uses haversine (great-circle distance in meters). -//! All other combinations use planar geometry to find the nearest -//! segment pair, then convert the coordinate-space result to meters -//! using equirectangular approximation. -//! -//! ST_DWithin uses bbox expansion as a fast pre-filter: expand query -//! geometry's bbox by the distance threshold, check intersection first. +//! `ST_Distance` and `ST_DWithin` on WGS 84 longitude / latitude, geodesic +//! on the WGS 84 ellipsoid. Callers that know the CRS use +//! [`crate::metric::Metric`] for its own ellipsoid or unit. -use nodedb_types::geometry::{Geometry, haversine_distance, point_in_polygon}; -use nodedb_types::geometry_bbox; +use nodedb_types::geometry::Geometry; -use super::edge::{point_on_ring_boundary, ring_edges, segment_to_segment_dist_sq}; +use crate::metric::Metric; -/// ST_Distance(a, b) — minimum distance in meters between two geometries. -/// -/// Point-to-point: haversine (exact great-circle). -/// All others: find minimum coordinate-space distance, convert to meters. +/// Minimum geodesic distance in metres between two WGS 84 geometries. +/// Infinite when either is EMPTY. pub fn st_distance(a: &Geometry, b: &Geometry) -> f64 { - match (a, b) { - // Point–Point: haversine (exact). - (Geometry::Point { coordinates: ca }, Geometry::Point { coordinates: cb }) => { - haversine_distance(ca[0], ca[1], cb[0], cb[1]) - } - - // Point–LineString or reverse. - (Geometry::Point { coordinates: pt }, Geometry::LineString { coordinates: line }) - | (Geometry::LineString { coordinates: line }, Geometry::Point { coordinates: pt }) => { - point_to_linestring_distance(*pt, line) - } - - // Point–Polygon or reverse. - (Geometry::Point { coordinates: pt }, Geometry::Polygon { coordinates: rings }) - | (Geometry::Polygon { coordinates: rings }, Geometry::Point { coordinates: pt }) => { - point_to_polygon_distance(*pt, rings) - } - - // LineString–LineString. - (Geometry::LineString { coordinates: la }, Geometry::LineString { coordinates: lb }) => { - linestring_to_linestring_distance(la, lb) - } - - // LineString–Polygon or reverse. - (Geometry::LineString { coordinates: line }, Geometry::Polygon { coordinates: rings }) - | (Geometry::Polygon { coordinates: rings }, Geometry::LineString { coordinates: line }) => { - linestring_to_polygon_distance(line, rings) - } - - // Polygon–Polygon. - (Geometry::Polygon { coordinates: ra }, Geometry::Polygon { coordinates: rb }) => { - polygon_to_polygon_distance(ra, rb) - } - - // Multi* types: minimum distance among all component pairs. - (Geometry::MultiPoint { coordinates }, other) - | (other, Geometry::MultiPoint { coordinates }) => coordinates - .iter() - .map(|pt| st_distance(&Geometry::Point { coordinates: *pt }, other)) - .fold(f64::INFINITY, f64::min), - - (Geometry::MultiLineString { coordinates }, other) - | (other, Geometry::MultiLineString { coordinates }) => coordinates - .iter() - .map(|ls| { - st_distance( - &Geometry::LineString { - coordinates: ls.clone(), - }, - other, - ) - }) - .fold(f64::INFINITY, f64::min), - - (Geometry::MultiPolygon { coordinates }, other) - | (other, Geometry::MultiPolygon { coordinates }) => coordinates - .iter() - .map(|poly| { - st_distance( - &Geometry::Polygon { - coordinates: poly.clone(), - }, - other, - ) - }) - .fold(f64::INFINITY, f64::min), - - (Geometry::GeometryCollection { geometries }, other) - | (other, Geometry::GeometryCollection { geometries }) => geometries - .iter() - .map(|g| st_distance(g, other)) - .fold(f64::INFINITY, f64::min), - - // Unknown future geometry types — treat as infinite distance. - (&_, &_) => f64::INFINITY, - } + Metric::wgs84().distance(a, b) } -/// ST_DWithin(a, b, distance_meters) — are A and B within the given distance? -/// -/// Optimized: uses bbox expansion pre-filter to avoid expensive exact -/// distance computation when geometries are clearly far apart. +/// Whether two WGS 84 geometries lie within `distance_meters` of each +/// other, geodesic. pub fn st_dwithin(a: &Geometry, b: &Geometry, distance_meters: f64) -> bool { - // Fast path for points: just haversine. - if let (Geometry::Point { coordinates: ca }, Geometry::Point { coordinates: cb }) = (a, b) { - return haversine_distance(ca[0], ca[1], cb[0], cb[1]) <= distance_meters; - } - - // Bbox pre-filter: expand A's bbox by distance, check if B's bbox intersects. - let a_bb = geometry_bbox(a).expand_meters(distance_meters); - let b_bb = geometry_bbox(b); - if !a_bb.intersects(&b_bb) { - return false; - } - - // Exact check. - st_distance(a, b) <= distance_meters -} - -// ── Distance helpers ── - -/// Point to linestring: minimum haversine distance to any segment. -fn point_to_linestring_distance(pt: [f64; 2], line: &[[f64; 2]]) -> f64 { - if line.is_empty() { - return f64::INFINITY; - } - if line.len() == 1 { - return haversine_distance(pt[0], pt[1], line[0][0], line[0][1]); - } - - let mut min_dist = f64::INFINITY; - for i in 0..line.len() - 1 { - let d = point_to_segment_meters(pt, line[i], line[i + 1]); - min_dist = min_dist.min(d); - } - min_dist -} - -/// Point to polygon: 0 if inside, otherwise min distance to any edge. -fn point_to_polygon_distance(pt: [f64; 2], rings: &[Vec<[f64; 2]>]) -> f64 { - let Some(exterior) = rings.first() else { - return f64::INFINITY; - }; - - // Inside exterior (and not in a hole) → distance 0. - if point_in_polygon(pt[0], pt[1], exterior) || point_on_ring_boundary(pt, exterior) { - let in_hole = rings[1..] - .iter() - .any(|hole| point_in_polygon(pt[0], pt[1], hole) && !point_on_ring_boundary(pt, hole)); - if !in_hole { - return 0.0; - } - } - - // Distance to nearest edge of all rings. - let mut min_dist = f64::INFINITY; - for ring in rings { - for i in 0..ring.len().saturating_sub(1) { - let d = point_to_segment_meters(pt, ring[i], ring[i + 1]); - min_dist = min_dist.min(d); - } - } - min_dist -} - -fn linestring_to_linestring_distance(la: &[[f64; 2]], lb: &[[f64; 2]]) -> f64 { - let mut min_dist = f64::INFINITY; - for i in 0..la.len().saturating_sub(1) { - for j in 0..lb.len().saturating_sub(1) { - let d_sq = segment_to_segment_dist_sq(la[i], la[i + 1], lb[j], lb[j + 1]); - if d_sq < 1e-20 { - return 0.0; - } - let d = coord_dist_to_meters(d_sq.sqrt(), la[i], lb[j]); - min_dist = min_dist.min(d); - } - } - min_dist -} - -fn linestring_to_polygon_distance(line: &[[f64; 2]], rings: &[Vec<[f64; 2]>]) -> f64 { - // If any line point is inside the polygon, distance is 0. - let Some(exterior) = rings.first() else { - return f64::INFINITY; - }; - for pt in line { - if point_in_polygon(pt[0], pt[1], exterior) || point_on_ring_boundary(*pt, exterior) { - return 0.0; - } - } - - // Minimum distance between line segments and ring edges. - let mut min_dist = f64::INFINITY; - for ring in rings { - let edges = ring_edges(ring); - for i in 0..line.len().saturating_sub(1) { - for &(re_a, re_b) in &edges { - let d_sq = segment_to_segment_dist_sq(line[i], line[i + 1], re_a, re_b); - if d_sq < 1e-20 { - return 0.0; - } - let d = coord_dist_to_meters(d_sq.sqrt(), line[i], re_a); - min_dist = min_dist.min(d); - } - } - } - min_dist -} - -fn polygon_to_polygon_distance(ra: &[Vec<[f64; 2]>], rb: &[Vec<[f64; 2]>]) -> f64 { - let Some(ext_a) = ra.first() else { - return f64::INFINITY; - }; - let Some(ext_b) = rb.first() else { - return f64::INFINITY; - }; - - // If any vertex of B is inside A (or vice versa), distance is 0. - for pt in ext_b { - if point_in_polygon(pt[0], pt[1], ext_a) || point_on_ring_boundary(*pt, ext_a) { - return 0.0; - } - } - for pt in ext_a { - if point_in_polygon(pt[0], pt[1], ext_b) || point_on_ring_boundary(*pt, ext_b) { - return 0.0; - } - } - - // Min edge-to-edge distance. - let mut min_dist = f64::INFINITY; - let a_edges = ring_edges(ext_a); - let b_edges = ring_edges(ext_b); - for &(a1, a2) in &a_edges { - for &(b1, b2) in &b_edges { - let d_sq = segment_to_segment_dist_sq(a1, a2, b1, b2); - if d_sq < 1e-20 { - return 0.0; - } - let d = coord_dist_to_meters(d_sq.sqrt(), a1, b1); - min_dist = min_dist.min(d); - } - } - min_dist -} - -/// Point to segment distance in meters. -/// -/// Projects onto segment in coordinate space, then uses haversine for the -/// final distance to the nearest point on the segment. -fn point_to_segment_meters(pt: [f64; 2], seg_a: [f64; 2], seg_b: [f64; 2]) -> f64 { - let dx = seg_b[0] - seg_a[0]; - let dy = seg_b[1] - seg_a[1]; - let len_sq = dx * dx + dy * dy; - - let nearest = if len_sq < 1e-20 { - seg_a - } else { - let t = ((pt[0] - seg_a[0]) * dx + (pt[1] - seg_a[1]) * dy) / len_sq; - let t = t.clamp(0.0, 1.0); - [seg_a[0] + t * dx, seg_a[1] + t * dy] - }; - - haversine_distance(pt[0], pt[1], nearest[0], nearest[1]) -} - -/// Convert coordinate-space distance to approximate meters. -/// -/// Uses equirectangular approximation at the midpoint latitude. -fn coord_dist_to_meters(coord_dist: f64, a: [f64; 2], b: [f64; 2]) -> f64 { - let mid_lat = ((a[1] + b[1]) / 2.0).to_radians(); - let meters_per_deg_lat = 110_540.0; - let meters_per_deg_lng = 111_320.0 * mid_lat.cos(); - // Approximate: treat coord_dist as an isotropic distance in degrees, - // scale by average meters per degree. - let avg_scale = (meters_per_deg_lat + meters_per_deg_lng) / 2.0; - coord_dist * avg_scale + Metric::wgs84().dwithin(a, b, distance_meters) } #[cfg(test)] @@ -294,10 +35,10 @@ mod tests { } #[test] - fn point_to_point_haversine() { + fn point_to_point_is_geodesic() { let d = st_distance(&Geometry::point(0.0, 0.0), &Geometry::point(0.0, 1.0)); - // ~111 km for 1 degree latitude. - assert!((d - 111_195.0).abs() < 500.0, "got {d}"); + // pyproj Geod(ellps="WGS84").inv(0, 0, 0, 1). + assert!((d - 110_574.388_557_798_78).abs() < 1e-6, "got {d}"); } #[test] @@ -328,9 +69,9 @@ mod tests { #[test] fn point_to_linestring() { let line = Geometry::line_string(vec![[0.0, 0.0], [10.0, 0.0]]); - // Point 1 degree north of the line. + // Point 1 degree north of an equatorial line: one meridian degree. let d = st_distance(&Geometry::point(5.0, 1.0), &line); - assert!((d - 111_195.0).abs() < 500.0, "got {d}"); + assert!((d - 110_574.388_557_798_78).abs() < 1e-3, "got {d}"); } #[test] diff --git a/nodedb-spatial/src/predicates/edge.rs b/nodedb-spatial/src/predicates/edge.rs index 763360af6..fd43e3bd1 100644 --- a/nodedb-spatial/src/predicates/edge.rs +++ b/nodedb-spatial/src/predicates/edge.rs @@ -2,10 +2,9 @@ //! Low-level segment and edge geometry primitives. //! -//! Used by ST_Contains, ST_Intersects, and ST_Distance. All operations -//! are planar (coordinate-space), not spherical. This is correct for the -//! small-area geometries typical of spatial predicates (city blocks, not -//! hemispheres). For large-area work, haversine handles the globe. +//! Used by ST_Contains, ST_Intersects and the distance algorithms. Every +//! operation is planar in the coordinates. Geodesic measures live in +//! `crate::geodesic`. /// Orientation of three points (collinear, clockwise, counter-clockwise). /// Uses the cross product of vectors (p→q) and (p→r). diff --git a/nodedb-spatial/src/predicates/intersection.rs b/nodedb-spatial/src/predicates/intersection.rs deleted file mode 100644 index 9ca6fcef3..000000000 --- a/nodedb-spatial/src/predicates/intersection.rs +++ /dev/null @@ -1,336 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -//! ST_Intersection — compute the area shared by two geometries. -//! -//! Uses Sutherland-Hodgman polygon clipping for polygon-polygon intersection. -//! For other geometry type combinations, delegates to simpler cases or -//! returns empty geometry. -//! -//! Sutherland-Hodgman works correctly for convex clipping polygons. For -//! concave clip polygons, the result may include extra edges — this is a -//! known limitation shared by most lightweight GIS implementations. Full -//! Weiler-Atherton is needed for perfect concave-concave intersection but -//! adds significant complexity. Sutherland-Hodgman covers the vast majority -//! of practical use cases (convex query regions, rectangular bboxes, etc.). -//! -//! Reference: Sutherland & Hodgman, "Reentrant Polygon Clipping" (1974) - -use nodedb_types::geometry::Geometry; - -/// ST_Intersection(a, b) — return the geometry representing the shared area. -/// -/// Returns GeometryCollection with an empty geometries vec if there is no -/// intersection. -pub fn st_intersection(a: &Geometry, b: &Geometry) -> Geometry { - match (a, b) { - // Polygon–Polygon: Sutherland-Hodgman clipping. - ( - Geometry::Polygon { - coordinates: rings_a, - }, - Geometry::Polygon { - coordinates: rings_b, - }, - ) => { - let Some(ext_a) = rings_a.first() else { - return empty_geometry(); - }; - let Some(ext_b) = rings_b.first() else { - return empty_geometry(); - }; - let clipped = sutherland_hodgman(ext_a, ext_b); - if clipped.len() < 3 { - return empty_geometry(); - } - // Close the ring. - let mut ring = clipped; - if ring.first() != ring.last() - && let Some(&first) = ring.first() - { - ring.push(first); - } - Geometry::Polygon { - coordinates: vec![ring], - } - } - - // Point–Polygon or reverse: if point is inside polygon, return point. - (Geometry::Point { coordinates: pt }, Geometry::Polygon { coordinates: rings }) - | (Geometry::Polygon { coordinates: rings }, Geometry::Point { coordinates: pt }) => { - if let Some(ext) = rings.first() - && (nodedb_types::geometry::point_in_polygon(pt[0], pt[1], ext) - || crate::predicates::edge::point_on_ring_boundary(*pt, ext)) - { - return Geometry::Point { coordinates: *pt }; - } - empty_geometry() - } - - // Point–Point: if identical, return point. - (Geometry::Point { coordinates: ca }, Geometry::Point { coordinates: cb }) => { - if (ca[0] - cb[0]).abs() < 1e-12 && (ca[1] - cb[1]).abs() < 1e-12 { - Geometry::Point { coordinates: *ca } - } else { - empty_geometry() - } - } - - // LineString–Polygon or reverse: clip line to polygon boundary. - (Geometry::LineString { coordinates: line }, Geometry::Polygon { coordinates: rings }) - | (Geometry::Polygon { coordinates: rings }, Geometry::LineString { coordinates: line }) => { - clip_linestring_to_polygon(line, rings) - } - - // All other combinations: fall back to checking intersection, - // return one of the inputs if they intersect. - _ => { - if crate::predicates::st_intersects(a, b) { - // Return the smaller geometry as the "intersection". - // This is an approximation for complex type combinations. - Geometry::GeometryCollection { - geometries: vec![a.clone(), b.clone()], - } - } else { - empty_geometry() - } - } - } -} - -/// Empty geometry (no intersection). -fn empty_geometry() -> Geometry { - Geometry::GeometryCollection { - geometries: Vec::new(), - } -} - -/// Sutherland-Hodgman polygon clipping. -/// -/// Clips `subject` polygon against `clip` polygon. Both are coordinate rings -/// (closed or unclosed). Returns the clipped polygon vertices (unclosed). -/// -/// The clip polygon's edges define half-planes. For each edge, vertices of -/// the subject that are "inside" (left of the edge) are kept. Vertices -/// that cross from inside to outside (or vice versa) generate intersection -/// points on the clip edge. -fn sutherland_hodgman(subject: &[[f64; 2]], clip: &[[f64; 2]]) -> Vec<[f64; 2]> { - if subject.is_empty() || clip.is_empty() { - return Vec::new(); - } - - let mut output = strip_closing(subject); - let clip_edges = strip_closing(clip); - - let n = clip_edges.len(); - for i in 0..n { - if output.is_empty() { - return Vec::new(); - } - - let edge_start = clip_edges[i]; - let edge_end = clip_edges[(i + 1) % n]; - - let input = output; - output = Vec::with_capacity(input.len()); - - let m = input.len(); - for j in 0..m { - let current = input[j]; - let previous = input[(j + m - 1) % m]; - - let curr_inside = is_inside(current, edge_start, edge_end); - let prev_inside = is_inside(previous, edge_start, edge_end); - - if curr_inside { - if !prev_inside { - // Entering: add intersection point. - if let Some(pt) = line_intersection(previous, current, edge_start, edge_end) { - output.push(pt); - } - } - output.push(current); - } else if prev_inside { - // Leaving: add intersection point. - if let Some(pt) = line_intersection(previous, current, edge_start, edge_end) { - output.push(pt); - } - } - } - } - - output -} - -/// Check if a point is on the "inside" (left side) of a directed edge. -fn is_inside(point: [f64; 2], edge_start: [f64; 2], edge_end: [f64; 2]) -> bool { - // Cross product: (edge_end - edge_start) × (point - edge_start) - // Positive = left side = inside for CCW winding. - let cross = (edge_end[0] - edge_start[0]) * (point[1] - edge_start[1]) - - (edge_end[1] - edge_start[1]) * (point[0] - edge_start[0]); - cross >= 0.0 -} - -/// Compute the intersection point of two line segments (as infinite lines). -fn line_intersection(a1: [f64; 2], a2: [f64; 2], b1: [f64; 2], b2: [f64; 2]) -> Option<[f64; 2]> { - let dx_a = a2[0] - a1[0]; - let dy_a = a2[1] - a1[1]; - let dx_b = b2[0] - b1[0]; - let dy_b = b2[1] - b1[1]; - - let denom = dx_a * dy_b - dy_a * dx_b; - if denom.abs() < 1e-15 { - return None; // Parallel lines. - } - - let t = ((b1[0] - a1[0]) * dy_b - (b1[1] - a1[1]) * dx_b) / denom; - Some([a1[0] + t * dx_a, a1[1] + t * dy_a]) -} - -/// Strip the closing vertex from a ring if present. -fn strip_closing(ring: &[[f64; 2]]) -> Vec<[f64; 2]> { - if ring.len() >= 2 && ring.first() == ring.last() { - ring[..ring.len() - 1].to_vec() - } else { - ring.to_vec() - } -} - -/// Clip a linestring to a polygon: keep only the parts inside. -fn clip_linestring_to_polygon(line: &[[f64; 2]], rings: &[Vec<[f64; 2]>]) -> Geometry { - let Some(exterior) = rings.first() else { - return empty_geometry(); - }; - - // Collect points that are inside the polygon. - let mut inside_points: Vec<[f64; 2]> = Vec::new(); - for pt in line { - if nodedb_types::geometry::point_in_polygon(pt[0], pt[1], exterior) - || crate::predicates::edge::point_on_ring_boundary(*pt, exterior) - { - inside_points.push(*pt); - } - } - - if inside_points.len() < 2 { - return empty_geometry(); - } - - Geometry::LineString { - coordinates: inside_points, - } -} - -#[cfg(test)] -mod tests { - use super::*; - - fn square(min: f64, max: f64) -> Geometry { - Geometry::polygon(vec![vec![ - [min, min], - [max, min], - [max, max], - [min, max], - [min, min], - ]]) - } - - #[test] - fn overlapping_squares() { - let a = square(0.0, 10.0); - let b = square(5.0, 15.0); - let result = st_intersection(&a, &b); - if let Geometry::Polygon { coordinates } = &result { - assert!(!coordinates[0].is_empty()); - // Intersection should be approximately [5,5] to [10,10]. - let xs: Vec = coordinates[0].iter().map(|c| c[0]).collect(); - let ys: Vec = coordinates[0].iter().map(|c| c[1]).collect(); - let min_x = xs.iter().cloned().fold(f64::INFINITY, f64::min); - let max_x = xs.iter().cloned().fold(f64::NEG_INFINITY, f64::max); - let min_y = ys.iter().cloned().fold(f64::INFINITY, f64::min); - let max_y = ys.iter().cloned().fold(f64::NEG_INFINITY, f64::max); - assert!((min_x - 5.0).abs() < 0.01, "min_x={min_x}"); - assert!((max_x - 10.0).abs() < 0.01, "max_x={max_x}"); - assert!((min_y - 5.0).abs() < 0.01, "min_y={min_y}"); - assert!((max_y - 10.0).abs() < 0.01, "max_y={max_y}"); - } else { - panic!("expected Polygon, got {:?}", result.geometry_type()); - } - } - - #[test] - fn contained_square() { - let a = square(0.0, 10.0); - let b = square(2.0, 8.0); - let result = st_intersection(&a, &b); - if let Geometry::Polygon { coordinates } = &result { - // Result should be approximately b itself. - let xs: Vec = coordinates[0].iter().map(|c| c[0]).collect(); - let min_x = xs.iter().cloned().fold(f64::INFINITY, f64::min); - let max_x = xs.iter().cloned().fold(f64::NEG_INFINITY, f64::max); - assert!((min_x - 2.0).abs() < 0.01); - assert!((max_x - 8.0).abs() < 0.01); - } else { - panic!("expected Polygon"); - } - } - - #[test] - fn disjoint_returns_empty() { - let a = square(0.0, 5.0); - let b = square(10.0, 15.0); - let result = st_intersection(&a, &b); - if let Geometry::GeometryCollection { geometries } = &result { - assert!(geometries.is_empty()); - } else { - panic!("expected empty GeometryCollection"); - } - } - - #[test] - fn point_inside_polygon() { - let poly = square(0.0, 10.0); - let pt = Geometry::point(5.0, 5.0); - let result = st_intersection(&poly, &pt); - assert_eq!(result.geometry_type(), "Point"); - } - - #[test] - fn point_outside_polygon() { - let poly = square(0.0, 10.0); - let pt = Geometry::point(20.0, 20.0); - let result = st_intersection(&poly, &pt); - if let Geometry::GeometryCollection { geometries } = &result { - assert!(geometries.is_empty()); - } - } - - #[test] - fn identical_points() { - let a = Geometry::point(5.0, 5.0); - let b = Geometry::point(5.0, 5.0); - let result = st_intersection(&a, &b); - assert_eq!(result.geometry_type(), "Point"); - } - - #[test] - fn different_points() { - let a = Geometry::point(0.0, 0.0); - let b = Geometry::point(1.0, 1.0); - let result = st_intersection(&a, &b); - if let Geometry::GeometryCollection { geometries } = &result { - assert!(geometries.is_empty()); - } - } - - #[test] - fn linestring_clipped_to_polygon() { - let poly = square(0.0, 10.0); - // Line fully inside the polygon. - let line = Geometry::line_string(vec![[2.0, 5.0], [5.0, 5.0], [8.0, 5.0]]); - let result = st_intersection(&poly, &line); - assert_eq!(result.geometry_type(), "LineString"); - if let Geometry::LineString { coordinates } = &result { - assert_eq!(coordinates.len(), 3); - } - } -} diff --git a/nodedb-spatial/src/predicates/intersects.rs b/nodedb-spatial/src/predicates/intersects.rs index 9bae4d4e9..7fea716e6 100644 --- a/nodedb-spatial/src/predicates/intersects.rs +++ b/nodedb-spatial/src/predicates/intersects.rs @@ -13,8 +13,12 @@ use nodedb_types::geometry_bbox; use super::edge::{is_collection, point_on_ring_boundary, ring_edges, segments_intersect}; -/// ST_Intersects(a, b) — do geometries A and B share any space? +/// ST_Intersects(a, b) — do geometries A and B share any space? EMPTY +/// intersects nothing. pub fn st_intersects(a: &Geometry, b: &Geometry) -> bool { + if a.is_empty() || b.is_empty() { + return false; + } // Bbox pre-filter. A GeometryCollection skips it: its bbox is a union // whose wrap rule depends on the SRID, and the recursion below filters // each member on its own. @@ -90,9 +94,6 @@ pub fn st_intersects(a: &Geometry, b: &Geometry) -> bool { | (other, Geometry::GeometryCollection { geometries }) => { geometries.iter().any(|g| st_intersects(g, other)) } - - // Unknown future geometry types — conservatively non-intersecting. - (&_, &_) => false, } } diff --git a/nodedb-spatial/src/predicates/mod.rs b/nodedb-spatial/src/predicates/mod.rs index f4772df56..accc78102 100644 --- a/nodedb-spatial/src/predicates/mod.rs +++ b/nodedb-spatial/src/predicates/mod.rs @@ -3,12 +3,10 @@ pub mod contains; pub mod distance; pub mod edge; -pub mod intersection; pub mod intersects; mod relations; pub use contains::st_contains; pub use distance::{st_distance, st_dwithin}; -pub use intersection::st_intersection; pub use intersects::st_intersects; pub use relations::{st_disjoint, st_within}; diff --git a/nodedb-spatial/src/rtree/bulk_load.rs b/nodedb-spatial/src/rtree/bulk_load.rs index 1d669546f..b250d0637 100644 --- a/nodedb-spatial/src/rtree/bulk_load.rs +++ b/nodedb-spatial/src/rtree/bulk_load.rs @@ -4,10 +4,10 @@ use nodedb_mem::ScopedMemory; use nodedb_types::BboxSpace; -use nodedb_types::geometry::Srid; use super::node::{ChildRef, INTERNAL_CAPACITY, LEAF_CAPACITY, Node, NodeKind, RTreeEntry}; use super::tree::RTree; +use super::tree_srid::TreeSrid; impl RTree { /// Bulk load entries in `srid` using Sort-Tile-Recursive packing, tracked @@ -15,11 +15,11 @@ impl RTree { /// /// More efficient than repeated single inserts for large datasets. /// Produces better packing (less overlap between nodes). - pub fn bulk_load(entries: Vec, memory: ScopedMemory, srid: Srid) -> Self { + pub fn bulk_load(entries: Vec, memory: ScopedMemory, srid: TreeSrid) -> Self { if entries.is_empty() { return Self::new(memory, srid); } - let space = srid.bbox_space(); + let space = srid.space; let len = entries.len(); // Reserve budget for nodes vec (approximately len / LEAF_CAPACITY nodes, @@ -34,7 +34,7 @@ impl RTree { nodes: Vec::new(), root: 0, len, - srid, + srid: srid.srid, space, memory, }; @@ -133,7 +133,7 @@ fn pack_internal(nodes: &mut Vec, child_indices: Vec, space: BboxSp #[cfg(test)] mod tests { use super::*; - use crate::test_support::test_memory; + use crate::test_support::{planar_srid, test_memory, utm48n}; use nodedb_types::BoundingBox; fn make_entry(id: u64, lng: f64, lat: f64) -> RTreeEntry { @@ -148,7 +148,7 @@ mod tests { let entries: Vec = (0..500) .map(|i| make_entry(i, (i as f64) * 0.1 - 25.0, (i as f64) * 0.06 - 15.0)) .collect(); - let tree = RTree::bulk_load(entries, test_memory(), Srid::Wgs84); + let tree = RTree::bulk_load(entries, test_memory(), TreeSrid::WGS84); assert_eq!(tree.len(), 500); let all = tree.search(&BoundingBox::new(-180.0, -90.0, 180.0, 90.0)); @@ -161,22 +161,22 @@ mod tests { #[test] fn bulk_load_small() { let entries = vec![make_entry(1, 0.0, 0.0), make_entry(2, 1.0, 1.0)]; - let tree = RTree::bulk_load(entries, test_memory(), Srid::Wgs84); + let tree = RTree::bulk_load(entries, test_memory(), TreeSrid::WGS84); assert_eq!(tree.len(), 2); } #[test] fn bulk_load_empty() { - let tree = RTree::bulk_load(Vec::new(), test_memory(), Srid::Utm47N); + let tree = RTree::bulk_load(Vec::new(), test_memory(), planar_srid(32647)); assert!(tree.is_empty()); - assert_eq!(tree.srid(), Srid::Utm47N); + assert_eq!(tree.tree_srid(), planar_srid(32647)); } #[test] fn bulk_load_keeps_srid_and_planar_space() { let entries = vec![make_entry(1, 1e5, 0.0), make_entry(2, 8e5, 0.0)]; - let tree = RTree::bulk_load(entries, test_memory(), Srid::Utm48N); - assert_eq!(tree.srid(), Srid::Utm48N); + let tree = RTree::bulk_load(entries, test_memory(), utm48n()); + assert_eq!(tree.tree_srid(), utm48n()); assert_eq!(tree.search(&BoundingBox::new(7e5, -1.0, 9e5, 1.0)).len(), 1); } } diff --git a/nodedb-spatial/src/rtree/mod.rs b/nodedb-spatial/src/rtree/mod.rs index 774159861..a94a6e211 100644 --- a/nodedb-spatial/src/rtree/mod.rs +++ b/nodedb-spatial/src/rtree/mod.rs @@ -7,7 +7,9 @@ pub mod node; pub mod search; pub mod split; pub mod tree; +pub mod tree_srid; pub use node::{EntryId, RTreeEntry}; pub use search::NnResult; pub use tree::{RTree, SridMismatch}; +pub use tree_srid::TreeSrid; diff --git a/nodedb-spatial/src/rtree/tree.rs b/nodedb-spatial/src/rtree/tree.rs index f607c46c0..1aadbf98a 100644 --- a/nodedb-spatial/src/rtree/tree.rs +++ b/nodedb-spatial/src/rtree/tree.rs @@ -7,6 +7,7 @@ use nodedb_types::{BboxSpace, BoundingBox}; use super::node::{Node, NodeKind, RTreeEntry}; use super::search::NnResult; +use super::tree_srid::TreeSrid; use nodedb_mem::ScopedMemory; /// R*-tree spatial index. @@ -44,13 +45,13 @@ pub struct SridMismatch { impl RTree { /// An empty tree for entries in `srid`. - pub fn new(memory: ScopedMemory, srid: Srid) -> Self { + pub fn new(memory: ScopedMemory, srid: TreeSrid) -> Self { Self { nodes: vec![Node::new_leaf()], root: 0, len: 0, - srid, - space: srid.bbox_space(), + srid: srid.srid, + space: srid.space, memory, } } @@ -80,22 +81,30 @@ impl RTree { /// /// An empty tree adopts `srid`. A non-empty tree in another SRID refuses /// with [`SridMismatch`] and is left unchanged. - pub fn bind_srid(&mut self, srid: Srid) -> Result<(), SridMismatch> { + pub fn bind_srid(&mut self, srid: TreeSrid) -> Result<(), SridMismatch> { if self.is_empty() { - self.srid = srid; - self.space = srid.bbox_space(); + self.srid = srid.srid; + self.space = srid.space; return Ok(()); } - if self.srid == srid { + if self.srid == srid.srid { Ok(()) } else { Err(SridMismatch { index: self.srid, - entry: srid, + entry: srid.srid, }) } } + /// The SRID and bbox space of every entry. + pub fn tree_srid(&self) -> TreeSrid { + TreeSrid { + srid: self.srid, + space: self.space, + } + } + pub fn len(&self) -> usize { self.len } @@ -194,7 +203,7 @@ pub(crate) fn collect_entries_owned(nodes: &[Node], node_idx: usize, result: &mu mod tests { use super::super::node::LEAF_CAPACITY; use super::*; - use crate::test_support::test_memory; + use crate::test_support::{test_memory, utm48n}; fn make_entry(id: u64, lng: f64, lat: f64) -> RTreeEntry { RTreeEntry { @@ -212,7 +221,7 @@ mod tests { #[test] fn empty_tree() { - let tree = RTree::new(test_memory(), Srid::Wgs84); + let tree = RTree::new(test_memory(), TreeSrid::WGS84); assert!(tree.is_empty()); assert!( tree.search(&BoundingBox::new(-180.0, -90.0, 180.0, 90.0)) @@ -222,7 +231,7 @@ mod tests { #[test] fn insert_and_search_single() { - let mut tree = RTree::new(test_memory(), Srid::Wgs84); + let mut tree = RTree::new(test_memory(), TreeSrid::WGS84); tree.insert(make_entry(1, 10.0, 20.0)); assert_eq!(tree.len(), 1); assert_eq!( @@ -237,7 +246,7 @@ mod tests { #[test] fn insert_many_and_search() { - let mut tree = RTree::new(test_memory(), Srid::Wgs84); + let mut tree = RTree::new(test_memory(), TreeSrid::WGS84); for i in 0..200 { tree.insert(make_entry( i, @@ -252,7 +261,7 @@ mod tests { #[test] fn delete_entry() { - let mut tree = RTree::new(test_memory(), Srid::Wgs84); + let mut tree = RTree::new(test_memory(), TreeSrid::WGS84); for i in 0..50 { tree.insert(make_entry(i, i as f64, i as f64)); } @@ -265,7 +274,7 @@ mod tests { #[test] fn nearest_neighbor() { - let mut tree = RTree::new(test_memory(), Srid::Wgs84); + let mut tree = RTree::new(test_memory(), TreeSrid::WGS84); tree.insert(make_entry(1, 0.0, 0.0)); tree.insert(make_entry(2, 10.0, 10.0)); tree.insert(make_entry(3, 5.0, 5.0)); @@ -278,7 +287,7 @@ mod tests { #[test] fn nearest_empty() { assert!( - RTree::new(test_memory(), Srid::Wgs84) + RTree::new(test_memory(), TreeSrid::WGS84) .nearest(0.0, 0.0, 5) .is_empty() ); @@ -286,7 +295,7 @@ mod tests { #[test] fn rect_overlap_search() { - let mut tree = RTree::new(test_memory(), Srid::Wgs84); + let mut tree = RTree::new(test_memory(), TreeSrid::WGS84); tree.insert(make_rect(1, 0.0, 0.0, 10.0, 10.0)); tree.insert(make_rect(2, 5.0, 5.0, 15.0, 15.0)); tree.insert(make_rect(3, 20.0, 20.0, 30.0, 30.0)); @@ -296,7 +305,7 @@ mod tests { #[test] fn stress_insert_delete() { - let mut tree = RTree::new(test_memory(), Srid::Wgs84); + let mut tree = RTree::new(test_memory(), TreeSrid::WGS84); for i in 0..100_u64 { tree.insert(make_entry(i, (i as f64) * 0.5, (i as f64) * 0.3)); } @@ -310,7 +319,7 @@ mod tests { #[test] fn triggers_node_split() { - let mut tree = RTree::new(test_memory(), Srid::Wgs84); + let mut tree = RTree::new(test_memory(), TreeSrid::WGS84); let count = LEAF_CAPACITY * 3; for i in 0..count as u64 { tree.insert(make_entry(i, (i as f64) * 0.1, (i as f64) * 0.1)); @@ -324,7 +333,7 @@ mod tests { /// spans them directly instead of wrapping as if x were longitude. #[test] fn projected_tree_unions_planar() { - let mut tree = RTree::new(test_memory(), Srid::Utm48N); + let mut tree = RTree::new(test_memory(), utm48n()); tree.insert(make_rect(1, 1e5, 0.0, 2e5, 10.0)); tree.insert(make_rect(2, 7e5, 0.0, 8e5, 10.0)); let root = tree.nodes[tree.root].bbox; @@ -343,7 +352,7 @@ mod tests { #[test] fn projected_nearest_is_euclidean_metres() { - let mut tree = RTree::new(test_memory(), Srid::Utm48N); + let mut tree = RTree::new(test_memory(), utm48n()); tree.insert(make_entry(1, 500_000.0, 0.0)); tree.insert(make_entry(2, 500_300.0, 400.0)); let nn = tree.nearest(500_000.0, 0.0, 2); @@ -353,18 +362,18 @@ mod tests { #[test] fn empty_tree_adopts_first_srid_then_refuses_another() { - let mut tree = RTree::new(test_memory(), Srid::Wgs84); - assert!(tree.accepts_srid(Srid::Utm48N)); - assert_eq!(tree.bind_srid(Srid::Utm48N), Ok(())); + let mut tree = RTree::new(test_memory(), TreeSrid::WGS84); + assert!(tree.accepts_srid(utm48n().srid)); + assert_eq!(tree.bind_srid(utm48n()), Ok(())); tree.insert(make_entry(1, 500_000.0, 0.0)); - assert_eq!(tree.srid(), Srid::Utm48N); + assert_eq!(tree.srid(), utm48n().srid); assert_eq!(tree.space(), BboxSpace::Planar); - assert!(!tree.accepts_srid(Srid::Wgs84)); + assert!(!tree.accepts_srid(Srid::WGS84)); assert_eq!( - tree.bind_srid(Srid::Wgs84), + tree.bind_srid(TreeSrid::WGS84), Err(SridMismatch { - index: Srid::Utm48N, - entry: Srid::Wgs84 + index: utm48n().srid, + entry: Srid::WGS84 }) ); assert_eq!(tree.len(), 1); @@ -372,7 +381,7 @@ mod tests { #[test] fn entries_enumeration() { - let mut tree = RTree::new(test_memory(), Srid::Wgs84); + let mut tree = RTree::new(test_memory(), TreeSrid::WGS84); for i in 0..10 { tree.insert(make_entry(i, i as f64, i as f64)); } diff --git a/nodedb-spatial/src/rtree/tree_srid.rs b/nodedb-spatial/src/rtree/tree_srid.rs new file mode 100644 index 000000000..8255efc4c --- /dev/null +++ b/nodedb-spatial/src/rtree/tree_srid.rs @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! The SRID of an R-tree with the bbox space it fixes. + +use nodedb_crs::{CrsContext, UnknownSrid}; +use nodedb_types::BboxSpace; +use nodedb_types::geometry::Srid; + +/// An SRID and its bbox space, resolved once so the tree's hot insert path +/// never consults a CRS registry. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct TreeSrid { + pub srid: Srid, + pub space: BboxSpace, +} + +impl TreeSrid { + /// WGS 84, geographic. + pub const WGS84: TreeSrid = TreeSrid { + srid: Srid::WGS84, + space: BboxSpace::Geographic, + }; + + /// `srid` with the space its kind in `ctx` gives. + pub fn resolve(srid: Srid, ctx: &CrsContext<'_>) -> Result { + Ok(TreeSrid { + srid, + space: BboxSpace::of(ctx, srid)?, + }) + } +} + +impl Default for TreeSrid { + fn default() -> Self { + TreeSrid::WGS84 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn resolve_follows_the_crs_kind() { + let ctx = CrsContext::builtin_only(); + let utm = Srid::new(32650).unwrap(); + assert_eq!( + TreeSrid::resolve(utm, &ctx), + Ok(TreeSrid { + srid: utm, + space: BboxSpace::Planar + }) + ); + let gdm2000 = Srid::new(4742).unwrap(); + assert_eq!( + TreeSrid::resolve(gdm2000, &ctx).map(|t| t.space), + Ok(BboxSpace::Geographic) + ); + assert_eq!( + TreeSrid::resolve(Srid::new(999).unwrap(), &ctx), + Err(UnknownSrid::Undefined { code: 999 }) + ); + } +} diff --git a/nodedb-spatial/src/spatial_join.rs b/nodedb-spatial/src/spatial_join.rs index cd999c9ed..6d0fb0561 100644 --- a/nodedb-spatial/src/spatial_join.rs +++ b/nodedb-spatial/src/spatial_join.rs @@ -14,11 +14,12 @@ //! to index based on collection cardinality. use nodedb_mem::ScopedMemory; -use nodedb_types::geometry::{Geometry, Srid}; -use nodedb_types::{BboxSpace, BoundingBox, geometry_bbox_in}; +use nodedb_types::geometry::Geometry; +use nodedb_types::{BoundingBox, geometry_bbox_in}; +use crate::metric::Metric; use crate::predicates; -use crate::rtree::{RTree, RTreeEntry}; +use crate::rtree::{RTree, RTreeEntry, TreeSrid}; /// Result of a spatial join between two collections. pub struct SpatialJoinResult { @@ -37,12 +38,15 @@ pub struct SpatialJoinResult { /// `get_geometry`: callback to retrieve the full geometry for an entry ID /// (needed for exact predicate evaluation after R-tree bbox filter). /// `predicate`: which spatial predicate to apply (intersects, contains, etc.). +/// `metric`: the shared SRID's metric. `DWithin` widens each probe box by +/// its distance under it and measures under it. pub fn spatial_join( indexed_side: &RTree, probe_entries: &[(u64, BoundingBox)], get_indexed_geom: &dyn Fn(u64) -> Option, get_probe_geom: &dyn Fn(u64) -> Option, predicate: SpatialJoinPredicate, + metric: &Metric, ) -> SpatialJoinResult { let mut pairs = Vec::new(); let mut probes = 0; @@ -50,7 +54,13 @@ pub fn spatial_join( for &(probe_id, ref probe_bbox) in probe_entries { // R-tree range search: find indexed entries whose bbox intersects probe bbox. - let candidates = indexed_side.search(probe_bbox); + let search_bbox = match predicate { + SpatialJoinPredicate::DWithin(metres) => metric.widen(probe_bbox, metres), + SpatialJoinPredicate::Intersects + | SpatialJoinPredicate::Contains + | SpatialJoinPredicate::Within => *probe_bbox, + }; + let candidates = indexed_side.search(&search_bbox); probes += 1; for candidate in &candidates { @@ -71,14 +81,9 @@ pub fn spatial_join( predicates::st_contains(&probe_geom, &indexed_geom) } SpatialJoinPredicate::Within => predicates::st_within(&probe_geom, &indexed_geom), - SpatialJoinPredicate::DWithin(dist) => match indexed_side.space() { - BboxSpace::Geographic => { - predicates::st_dwithin(&probe_geom, &indexed_geom, dist) - } - BboxSpace::Planar => { - crate::planar::planar_dwithin(&probe_geom, &indexed_geom, dist) - } - }, + SpatialJoinPredicate::DWithin(metres) => { + metric.dwithin(&probe_geom, &indexed_geom, metres) + } }; if matches { @@ -96,8 +101,12 @@ pub fn spatial_join( /// Build an R-tree from a list of (entry_id, geometry) pairs, all in /// `srid`, for join. -pub fn build_join_index(entries: &[(u64, Geometry)], memory: ScopedMemory, srid: Srid) -> RTree { - let space = srid.bbox_space(); +pub fn build_join_index( + entries: &[(u64, Geometry)], + memory: ScopedMemory, + srid: TreeSrid, +) -> RTree { + let space = srid.space; let rtree_entries: Vec = entries .iter() .map(|(id, geom)| RTreeEntry { @@ -115,6 +124,7 @@ pub enum SpatialJoinPredicate { Intersects, Contains, Within, + /// Within a distance in metres under the join's metric. DWithin(f64), } @@ -161,7 +171,7 @@ mod tests { .collect(); // Build index on left side. - let index = build_join_index(&left, test_memory(), Srid::Wgs84); + let index = build_join_index(&left, test_memory(), TreeSrid::WGS84); // Probe with right side. let probe_entries: Vec<(u64, BoundingBox)> = right @@ -178,6 +188,7 @@ mod tests { &|id| left_map.get(&id).cloned(), &|id| right_map.get(&id).cloned(), SpatialJoinPredicate::Intersects, + &Metric::wgs84(), ); // Adjacent overlapping squares should produce matches. @@ -208,7 +219,7 @@ mod tests { ]]), )]; - let index = build_join_index(&left, test_memory(), Srid::Wgs84); + let index = build_join_index(&left, test_memory(), TreeSrid::WGS84); let probes: Vec<(u64, BoundingBox)> = right .iter() .map(|(id, g)| (*id, geometry_bbox(g))) @@ -223,6 +234,7 @@ mod tests { &|id| left_map.get(&id).cloned(), &|id| right_map.get(&id).cloned(), SpatialJoinPredicate::Intersects, + &Metric::wgs84(), ); assert!(result.pairs.is_empty()); @@ -233,13 +245,10 @@ mod tests { let left = vec![(1u64, Geometry::point(0.0, 0.0))]; let right = vec![(100u64, Geometry::point(0.001, 0.0))]; // ~111m away - let index = build_join_index(&left, test_memory(), Srid::Wgs84); + let index = build_join_index(&left, test_memory(), TreeSrid::WGS84); let probes: Vec<(u64, BoundingBox)> = right .iter() - .map(|(id, g)| { - // Expand bbox by distance for R-tree search. - (*id, geometry_bbox(g).expand_meters(500.0)) - }) + .map(|(id, g)| (*id, geometry_bbox(g))) .collect(); let left_map: std::collections::HashMap = left.into_iter().collect(); @@ -250,7 +259,8 @@ mod tests { &probes, &|id| left_map.get(&id).cloned(), &|id| right_map.get(&id).cloned(), - SpatialJoinPredicate::DWithin(500.0), // 500m + SpatialJoinPredicate::DWithin(500.0), + &Metric::wgs84(), ); assert_eq!(result.pairs.len(), 1); diff --git a/nodedb-spatial/src/test_support.rs b/nodedb-spatial/src/test_support.rs index 70eb7b483..4879b3887 100644 --- a/nodedb-spatial/src/test_support.rs +++ b/nodedb-spatial/src/test_support.rs @@ -26,3 +26,16 @@ pub(crate) fn test_memory() -> ScopedMemory { EngineId::Spatial, ) } + +/// A projected tree SRID with code `code`, planar space. +pub(crate) fn planar_srid(code: u32) -> crate::rtree::TreeSrid { + crate::rtree::TreeSrid { + srid: nodedb_types::geometry::Srid::new(code).expect("test SRID code in range"), + space: nodedb_types::BboxSpace::Planar, + } +} + +/// EPSG:32648, UTM zone 48N. +pub(crate) fn utm48n() -> crate::rtree::TreeSrid { + planar_srid(32648) +} diff --git a/nodedb-spatial/src/text.rs b/nodedb-spatial/src/text.rs new file mode 100644 index 000000000..75adcad72 --- /dev/null +++ b/nodedb-spatial/src/text.rs @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! One entry point for every textual and binary geometry form. +//! +//! Text starting with `{` is GeoJSON. Text of hex digits only is (E)WKB in +//! hex, as PostGIS prints it. Any other text is EWKT or WKT. Bytes are WKB +//! or EWKB. +//! +//! An input parse resolves the SRID through a resolver, so an undefined +//! SRID is refused. A stored parse checks the SRID's syntax only. + +use nodedb_crs::{SridResolver, UnknownSrid}; +use nodedb_types::geometry::{Srid, SridGeometry, decode_stored_geojson, parse_geojson}; + +use crate::error::SpatialError; +use crate::wkb::geometry_from_wkb; +use crate::wkt::geometry_from_ewkt; + +/// How a parsed SRID code becomes an [`Srid`]. +#[derive(Clone, Copy)] +pub enum SridCheck<'a> { + /// Resolve through a resolver: an undefined code fails. + Resolve(&'a dyn SridResolver), + /// Check the code range only, for stored data. + Syntax, +} + +impl SridCheck<'_> { + fn code(&self, code: i64) -> Result { + let in_range = u32::try_from(code) + .ok() + .and_then(|c| Srid::new(c).ok()) + .ok_or(UnknownSrid::OutOfRange { code })?; + match self { + SridCheck::Resolve(ctx) => ctx.resolve_u32(in_range.code()), + SridCheck::Syntax => Ok(in_range), + } + } +} + +/// Parse a geometry text in any accepted form. +pub fn geometry_from_text(text: &str, check: SridCheck<'_>) -> Result { + let trimmed = text.trim(); + if trimmed.starts_with('{') { + return Ok(match check { + SridCheck::Resolve(ctx) => parse_geojson(trimmed, ctx)?, + SridCheck::Syntax => decode_stored_geojson(trimmed.as_bytes())?, + }); + } + if is_hex(trimmed) { + let bytes = decode_hex(trimmed); + return geometry_from_bytes(&bytes, check); + } + let ewkt = geometry_from_ewkt(trimmed)?; + Ok(match ewkt.srid { + Some(code) => ewkt.geometry.with_srid(check.code(code)?), + None => ewkt.geometry, + }) +} + +/// Parse WKB or EWKB bytes. ISO WKB is WGS 84. +pub fn geometry_from_bytes( + bytes: &[u8], + check: SridCheck<'_>, +) -> Result { + let wkb = geometry_from_wkb(bytes)?; + Ok(match wkb.srid { + Some(code) => wkb.geometry.with_srid(check.code(i64::from(code))?), + None => wkb.geometry, + }) +} + +/// Whether `text` is an even-length run of hex digits long enough to hold +/// a WKB header. +fn is_hex(text: &str) -> bool { + text.len() >= 10 && text.len().is_multiple_of(2) && text.bytes().all(|b| b.is_ascii_hexdigit()) +} + +fn decode_hex(text: &str) -> Vec { + text.as_bytes() + .chunks(2) + .map(|pair| (hex_value(pair[0]) << 4) | hex_value(pair[1])) + .collect() +} + +/// Value of one ASCII hex digit. [`is_hex`] admits only hex digits. +fn hex_value(b: u8) -> u8 { + match b { + b'0'..=b'9' => b - b'0', + b'a'..=b'f' => b - b'a' + 10, + _ => b - b'A' + 10, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use nodedb_types::geometry::Geometry; + + fn ctx() -> nodedb_crs::CrsContext<'static> { + nodedb_crs::CrsContext::builtin_only() + } + + #[test] + fn every_text_form_parses() { + let ctx = ctx(); + let check = SridCheck::Resolve(&ctx); + let want = SridGeometry::wgs84(Geometry::point(1.0, 2.0)); + assert_eq!(geometry_from_text("POINT(1 2)", check), Ok(want.clone())); + assert_eq!( + geometry_from_text(r#"{"type":"Point","coordinates":[1,2]}"#, check), + Ok(want.clone()) + ); + assert_eq!( + geometry_from_text("0101000000000000000000F03F0000000000000040", check), + Ok(want) + ); + let ewkt = geometry_from_text("SRID=32648;POINT(1 2)", check).unwrap(); + assert_eq!(ewkt.srid.code(), 32648); + } + + #[test] + fn undefined_srid_fails_only_on_input() { + let ctx = ctx(); + assert!(matches!( + geometry_from_text("SRID=999;POINT(1 2)", SridCheck::Resolve(&ctx)), + Err(SpatialError::Srid(UnknownSrid::Undefined { code: 999 })) + )); + assert_eq!( + geometry_from_text("SRID=999;POINT(1 2)", SridCheck::Syntax).map(|g| g.srid.code()), + Ok(999) + ); + } + + #[test] + fn malformed_text_is_an_error() { + assert!(geometry_from_text("POINT(", SridCheck::Syntax).is_err()); + assert!(geometry_from_text("not a geometry", SridCheck::Syntax).is_err()); + } +} diff --git a/nodedb-spatial/src/validate.rs b/nodedb-spatial/src/validate.rs index 2c6816eeb..ccd5f55ce 100644 --- a/nodedb-spatial/src/validate.rs +++ b/nodedb-spatial/src/validate.rs @@ -1,293 +1,439 @@ // SPDX-License-Identifier: Apache-2.0 -//! Geometry validation (geo_is_valid). +//! Geometry validity per OGC Simple Features, as `ST_IsValid` defines it. //! -//! Checks structural validity per OGC Simple Features: -//! - Polygon rings must be closed (first == last coordinate) -//! - Polygon rings must have at least 4 points (triangle + close) -//! - Polygon exterior ring should be counter-clockwise (right-hand rule) -//! - Polygon holes should be clockwise -//! - No self-intersection of ring edges -//! - LineStrings must have at least 2 points +//! - every coordinate finite; +//! - a LineString has at least 2 points; +//! - a ring is closed and has at least 4 points; +//! - a ring does not cross itself; +//! - a hole lies inside its shell, and rings meet at points only; +//! - MultiPolygon members neither overlap nor share an edge. +//! +//! Ring direction does not affect validity. EMPTY is valid. +use geo::Validation; +use geo::algorithm::validation::{InvalidMultiPolygon, InvalidPolygon, RingRole}; use nodedb_types::geometry::Geometry; -use crate::predicates::edge::segments_intersect; - -/// Validate a geometry. Returns a list of issues (empty = valid). -pub fn validate_geometry(geom: &Geometry) -> Vec { - let mut issues = Vec::new(); - validate_recursive(geom, &mut issues); - issues +use crate::ops::convert::polygon; + +/// Where in a geometry a problem lies. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Location { + /// Index path of collection members, outermost first. + pub members: Vec, + /// Polygon index within a MultiPolygon. + pub polygon: Option, + /// Ring index within a polygon, 0 the shell. + pub ring: Option, } -/// Whether a geometry is valid (no issues). -pub fn is_valid(geom: &Geometry) -> bool { - validate_geometry(geom).is_empty() -} +impl Location { + fn root() -> Self { + Self { + members: Vec::new(), + polygon: None, + ring: None, + } + } -fn validate_recursive(geom: &Geometry, issues: &mut Vec) { - match geom { - Geometry::Point { coordinates } - if coordinates[0].is_nan() - || coordinates[1].is_nan() - || coordinates[0].is_infinite() - || coordinates[1].is_infinite() => - { - issues.push("Point has NaN or Infinite coordinate".to_string()); + fn member(&self, index: usize) -> Self { + let mut members = self.members.clone(); + members.push(index); + Self { + members, + polygon: None, + ring: None, } + } - Geometry::LineString { coordinates } if coordinates.len() < 2 => { - issues.push(format!( - "LineString has {} points, minimum is 2", - coordinates.len() - )); + fn polygon(&self, index: usize) -> Self { + Self { + polygon: Some(index), + ..self.clone() } + } - Geometry::Polygon { coordinates } => { - if coordinates.is_empty() { - issues.push("Polygon has no rings".to_string()); - return; - } + fn ring(&self, index: usize) -> Self { + Self { + ring: Some(index), + ..self.clone() + } + } +} - for (ring_idx, ring) in coordinates.iter().enumerate() { - let label = if ring_idx == 0 { - "Exterior ring".to_string() - } else { - format!("Hole ring {ring_idx}") - }; +impl std::fmt::Display for Location { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut parts = Vec::new(); + for m in &self.members { + parts.push(format!("member {m}")); + } + if let Some(p) = self.polygon { + parts.push(format!("polygon {p}")); + } + match self.ring { + Some(0) => parts.push("shell".to_string()), + Some(r) => parts.push(format!("hole {r}")), + None => {} + } + if parts.is_empty() { + f.write_str("geometry") + } else { + f.write_str(&parts.join(" ")) + } + } +} - if ring.len() < 4 { - issues.push(format!( - "{label} has {} points, minimum is 4 (triangle + close)", - ring.len() - )); - continue; - } +/// Why a geometry is invalid. +#[derive(Debug, Clone, PartialEq, thiserror::Error)] +pub enum ValidityReason { + #[error("{location}: coordinate {index} is not finite")] + NonFinite { location: Location, index: usize }, + #[error("{location}: {found} points, a LineString needs at least 2")] + LineTooShort { location: Location, found: usize }, + #[error("{location}: {found} points, a ring needs at least 4")] + RingTooShort { location: Location, found: usize }, + #[error("{location}: ring is not closed")] + RingNotClosed { location: Location }, + #[error("{location}: ring crosses itself")] + SelfIntersection { location: Location }, + #[error("{location}: hole lies outside the shell")] + HoleOutsideShell { location: Location }, + #[error("{location}: rings {first} and {second} share a line")] + RingsTouchOnLine { + location: Location, + first: usize, + second: usize, + }, + #[error("{location}: rings {first} and {second} overlap")] + RingsOverlap { + location: Location, + first: usize, + second: usize, + }, + #[error("{location}: polygons {first} and {second} overlap")] + PolygonsOverlap { + location: Location, + first: usize, + second: usize, + }, + #[error("{location}: polygons {first} and {second} share a line")] + PolygonsTouchOnLine { + location: Location, + first: usize, + second: usize, + }, +} - // Check closed. - if let (Some(first), Some(last)) = (ring.first(), ring.last()) - && ((first[0] - last[0]).abs() > 1e-10 || (first[1] - last[1]).abs() > 1e-10) - { - issues.push(format!("{label} is not closed (first != last)")); - } +/// Every reason `geom` is invalid. Empty when valid. +pub fn validate_geometry(geom: &Geometry) -> Vec { + let mut reasons = Vec::new(); + check(geom, &Location::root(), &mut reasons); + reasons +} - // Check winding order. - let area = signed_area(ring); - if ring_idx == 0 && area < 0.0 { - issues.push( - "Exterior ring has clockwise winding (should be counter-clockwise)" - .to_string(), - ); - } else if ring_idx > 0 && area > 0.0 { - issues.push(format!( - "{label} has counter-clockwise winding (holes should be clockwise)" - )); - } +/// Whether `geom` is valid. +pub fn is_valid(geom: &Geometry) -> bool { + validate_geometry(geom).is_empty() +} - // Check self-intersection. - check_ring_self_intersection(ring, &label, issues); +fn check(geom: &Geometry, at: &Location, out: &mut Vec) { + match geom { + g if g.is_empty() => {} + Geometry::Point { coordinates } => finite(&[*coordinates], at, out), + Geometry::MultiPoint { coordinates } => finite(coordinates, at, out), + Geometry::LineString { coordinates } => line(coordinates, at, out), + Geometry::MultiLineString { coordinates } => { + for (i, l) in coordinates.iter().enumerate() { + line(l, &at.polygon(i), out); } } - - Geometry::MultiPoint { coordinates } => { - for (i, c) in coordinates.iter().enumerate() { - if c[0].is_nan() || c[1].is_nan() || c[0].is_infinite() || c[1].is_infinite() { - issues.push(format!("MultiPoint[{i}] has NaN or Infinite coordinate")); - } + Geometry::Polygon { coordinates } => { + if rings_ok(coordinates, at, out) { + polygon_topology(coordinates, at, out); } } - - Geometry::MultiLineString { coordinates } => { - for (i, ls) in coordinates.iter().enumerate() { - if ls.len() < 2 { - issues.push(format!( - "MultiLineString[{i}] has {} points, minimum is 2", - ls.len() - )); + Geometry::MultiPolygon { coordinates } => { + let mut all_ok = true; + for (i, rings) in coordinates.iter().enumerate() { + let here = at.polygon(i); + if rings_ok(rings, &here, out) { + polygon_topology(rings, &here, out); + } else { + all_ok = false; } } - } - - Geometry::MultiPolygon { coordinates } => { - for (i, poly) in coordinates.iter().enumerate() { - validate_recursive( - &Geometry::Polygon { - coordinates: poly.clone(), - }, - issues, - ); - // Prefix existing issues from this sub-polygon. - // (Already added by recursive call.) - let _ = i; // used for context only + if all_ok { + multipolygon_topology(coordinates, at, out); } } - Geometry::GeometryCollection { geometries } => { - for geom in geometries { - validate_recursive(geom, issues); + for (i, member) in geometries.iter().enumerate() { + check(member, &at.member(i), out); } } + } +} - // Unknown future geometry type — no validation rules yet. - _ => {} +fn finite(coords: &[[f64; 2]], at: &Location, out: &mut Vec) { + if let Some(index) = coords + .iter() + .position(|c| !(c[0].is_finite() && c[1].is_finite())) + { + out.push(ValidityReason::NonFinite { + location: at.clone(), + index, + }); } } -/// Signed area of a ring (Shoelace formula). Positive = CCW, Negative = CW. -pub(crate) fn signed_area(ring: &[[f64; 2]]) -> f64 { - let n = ring.len(); - if n < 3 { - return 0.0; +fn line(coords: &[[f64; 2]], at: &Location, out: &mut Vec) { + finite(coords, at, out); + if coords.len() < 2 { + out.push(ValidityReason::LineTooShort { + location: at.clone(), + found: coords.len(), + }); } - let mut sum = 0.0; - for i in 0..n { - let j = (i + 1) % n; - sum += ring[i][0] * ring[j][1]; - sum -= ring[j][0] * ring[i][1]; +} + +/// The structural checks `geo` cannot see once it closes rings. `true` +/// when every ring passes them. +fn rings_ok(rings: &[Vec<[f64; 2]>], at: &Location, out: &mut Vec) -> bool { + let before = out.len(); + for (r, ring) in rings.iter().enumerate() { + let here = at.ring(r); + finite(ring, &here, out); + if ring.len() < 4 { + out.push(ValidityReason::RingTooShort { + location: here, + found: ring.len(), + }); + continue; + } + if ring.first() != ring.last() { + out.push(ValidityReason::RingNotClosed { location: here }); + } } - sum / 2.0 + out.len() == before } -/// Check if any non-adjacent edges of a ring intersect. -fn check_ring_self_intersection(ring: &[[f64; 2]], label: &str, issues: &mut Vec) { - let n = if ring.first() == ring.last() && ring.len() > 1 { - ring.len() - 1 - } else { - ring.len() - }; +fn role_index(role: &RingRole) -> usize { + match role { + RingRole::Exterior => 0, + RingRole::Interior(i) => i + 1, + } +} - if n < 4 { - return; // Triangle can't self-intersect. +fn polygon_reason(e: &InvalidPolygon, at: &Location) -> ValidityReason { + match e { + InvalidPolygon::TooFewPointsInRing(role) => ValidityReason::RingTooShort { + location: at.ring(role_index(role)), + found: 3, + }, + InvalidPolygon::SelfIntersection(role) => ValidityReason::SelfIntersection { + location: at.ring(role_index(role)), + }, + InvalidPolygon::NonFiniteCoord(role, index) => ValidityReason::NonFinite { + location: at.ring(role_index(role)), + index: index.0, + }, + InvalidPolygon::InteriorRingNotContainedInExteriorRing(role) => { + ValidityReason::HoleOutsideShell { + location: at.ring(role_index(role)), + } + } + InvalidPolygon::IntersectingRingsOnALine(a, b) => ValidityReason::RingsTouchOnLine { + location: at.clone(), + first: role_index(a), + second: role_index(b), + }, + InvalidPolygon::IntersectingRingsOnAnArea(a, b) => ValidityReason::RingsOverlap { + location: at.clone(), + first: role_index(a), + second: role_index(b), + }, } +} + +fn polygon_topology(rings: &[Vec<[f64; 2]>], at: &Location, out: &mut Vec) { + out.extend( + polygon(rings) + .validation_errors() + .iter() + .map(|e| polygon_reason(e, at)), + ); +} - for i in 0..n { - let i_next = (i + 1) % n; - // Only check non-adjacent edges (skip i-1, i, i+1). - for j in (i + 2)..n { - let j_next = (j + 1) % n; - // Skip if edges share a vertex. - if j_next == i { - continue; +fn multipolygon_topology( + polygons: &[Vec>], + at: &Location, + out: &mut Vec, +) { + let mp = geo::MultiPolygon::new(polygons.iter().map(|p| polygon(p)).collect()); + for e in mp.validation_errors() { + match e { + // Each member is checked on its own above. + InvalidMultiPolygon::InvalidPolygon(..) => {} + InvalidMultiPolygon::ElementsOverlaps(a, b) => { + out.push(ValidityReason::PolygonsOverlap { + location: at.clone(), + first: a.0, + second: b.0, + }); } - if segments_intersect(ring[i], ring[i_next], ring[j], ring[j_next]) { - // Check if it's just a shared endpoint (adjacent-ish for closed rings). - let shared_endpoint = ring[i] == ring[j] - || ring[i] == ring[j_next] - || ring[i_next] == ring[j] - || ring[i_next] == ring[j_next]; - if !shared_endpoint { - issues.push(format!( - "{label} has self-intersection at edges {i}-{i_next} and {j}-{j_next}" - )); - return; // One self-intersection is enough to report. - } + InvalidMultiPolygon::ElementsTouchOnALine(a, b) => { + out.push(ValidityReason::PolygonsTouchOnLine { + location: at.clone(), + first: a.0, + second: b.0, + }); } } } } +/// `ST_IsValidReason`: `"Valid Geometry"` or the first reason, as PostGIS +/// words the valid case. +pub fn validity_reason(geom: &Geometry) -> String { + validate_geometry(geom) + .first() + .map_or_else(|| "Valid Geometry".to_string(), ToString::to_string) +} + #[cfg(test)] mod tests { use super::*; - #[test] - fn valid_polygon() { - let geom = Geometry::polygon(vec![vec![ + fn square(clockwise: bool) -> Vec<[f64; 2]> { + let mut ring = vec![ [0.0, 0.0], [10.0, 0.0], [10.0, 10.0], [0.0, 10.0], [0.0, 0.0], - ]]); - assert!(is_valid(&geom)); + ]; + if clockwise { + ring.reverse(); + } + ring } #[test] - fn unclosed_ring() { - let geom = Geometry::polygon(vec![vec![ + fn valid_polygon_either_direction() { + assert!(is_valid(&Geometry::polygon(vec![square(false)]))); + assert!(is_valid(&Geometry::polygon(vec![square(true)]))); + } + + #[test] + fn unclosed_and_short_rings() { + let unclosed = Geometry::polygon(vec![vec![ [0.0, 0.0], [10.0, 0.0], [10.0, 10.0], [0.0, 10.0], ]]); - let issues = validate_geometry(&geom); - assert!(issues.iter().any(|i| i.contains("not closed"))); - } - - #[test] - fn too_few_points() { - let geom = Geometry::polygon(vec![vec![[0.0, 0.0], [10.0, 0.0], [10.0, 10.0]]]); - let issues = validate_geometry(&geom); - assert!(issues.iter().any(|i| i.contains("minimum is 4"))); + assert!(matches!( + validate_geometry(&unclosed).as_slice(), + [ValidityReason::RingNotClosed { .. }] + )); + let short = Geometry::polygon(vec![vec![[0.0, 0.0], [10.0, 0.0], [10.0, 10.0]]]); + assert!(matches!( + validate_geometry(&short).as_slice(), + [ValidityReason::RingTooShort { found: 3, .. }] + )); } #[test] - fn self_intersecting_ring() { - // Bowtie: edges cross. - let geom = Geometry::polygon(vec![vec![ + fn bowtie_self_intersects() { + let bowtie = Geometry::polygon(vec![vec![ [0.0, 0.0], [10.0, 10.0], [10.0, 0.0], [0.0, 10.0], [0.0, 0.0], ]]); - let issues = validate_geometry(&geom); - assert!(issues.iter().any(|i| i.contains("self-intersection"))); - } - - #[test] - fn valid_linestring() { - let geom = Geometry::line_string(vec![[0.0, 0.0], [1.0, 1.0]]); - assert!(is_valid(&geom)); - } - - #[test] - fn linestring_too_short() { - let geom = Geometry::LineString { - coordinates: vec![[0.0, 0.0]], - }; - assert!(!is_valid(&geom)); + let reasons = validate_geometry(&bowtie); + assert!( + reasons + .iter() + .any(|r| matches!(r, ValidityReason::SelfIntersection { .. })), + "{reasons:?}" + ); + assert!(validity_reason(&bowtie).contains("crosses itself")); } #[test] - fn valid_point() { - assert!(is_valid(&Geometry::point(0.0, 0.0))); + fn hole_outside_shell() { + let g = Geometry::polygon(vec![ + square(false), + vec![ + [20.0, 20.0], + [21.0, 20.0], + [21.0, 21.0], + [20.0, 21.0], + [20.0, 20.0], + ], + ]); + assert!( + validate_geometry(&g) + .iter() + .any(|r| matches!(r, ValidityReason::HoleOutsideShell { .. })) + ); } #[test] - fn nan_point_invalid() { - let geom = Geometry::Point { - coordinates: [f64::NAN, 0.0], + fn overlapping_multipolygon_members() { + let shifted: Vec<[f64; 2]> = square(false).iter().map(|c| [c[0] + 5.0, c[1]]).collect(); + let g = Geometry::MultiPolygon { + coordinates: vec![vec![square(false)], vec![shifted]], }; - assert!(!is_valid(&geom)); + assert!( + validate_geometry(&g) + .iter() + .any(|r| matches!(r, ValidityReason::PolygonsOverlap { .. })) + ); } #[test] - fn clockwise_exterior_warned() { - // Clockwise exterior ring. - let geom = Geometry::polygon(vec![vec![ - [0.0, 0.0], - [0.0, 10.0], - [10.0, 10.0], - [10.0, 0.0], + fn lines_points_and_empty() { + assert!(is_valid(&Geometry::line_string(vec![ [0.0, 0.0], - ]]); - let issues = validate_geometry(&geom); - assert!(issues.iter().any(|i| i.contains("clockwise"))); + [1.0, 1.0] + ]))); + assert!(!is_valid(&Geometry::LineString { + coordinates: vec![[0.0, 0.0]] + })); + assert!(is_valid(&Geometry::point(0.0, 0.0))); + assert!(!is_valid(&Geometry::Point { + coordinates: [f64::INFINITY, 0.0] + })); + assert!(is_valid(&Geometry::empty_point())); + assert!(is_valid(&Geometry::empty( + nodedb_types::geometry::GeometryKind::Polygon + ))); + assert_eq!( + validity_reason(&Geometry::point(0.0, 0.0)), + "Valid Geometry" + ); } #[test] - fn geometry_collection_validates_all() { + fn collection_reports_the_member() { let gc = Geometry::GeometryCollection { geometries: vec![ Geometry::point(0.0, 0.0), Geometry::LineString { coordinates: vec![[0.0, 0.0]], - }, // invalid + }, ], }; - let issues = validate_geometry(&gc); - assert!(!issues.is_empty()); + let reasons = validate_geometry(&gc); + assert_eq!(reasons.len(), 1); + assert!( + reasons[0].to_string().starts_with("member 1"), + "{}", + reasons[0] + ); } } diff --git a/nodedb-spatial/src/value.rs b/nodedb-spatial/src/value.rs new file mode 100644 index 000000000..dc17c075c --- /dev/null +++ b/nodedb-spatial/src/value.rs @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! The geometry a `Value` holds, and its EWKB. +//! +//! A geometry arrives as the native geometry, its text (GeoJSON, EWKT, WKT, +//! hex EWKB), WKB / EWKB bytes, or a GeoJSON object. Every storage codec +//! reads a geometry cell through these functions. + +use nodedb_types::Value; +use nodedb_types::geometry::{SridGeometry, geojson_object, stored_geojson_object}; + +use crate::error::SpatialError; +use crate::text::{SridCheck, geometry_from_bytes, geometry_from_text}; +use crate::wkb::geometry_to_ewkb; + +/// The geometry `value` holds. `check` resolves or range-checks its SRID. +/// A value of any other type is [`SpatialError::NotGeometryValue`]. +pub fn geometry_from_value( + value: &Value, + check: SridCheck<'_>, +) -> Result { + match value { + Value::Geometry(g) => { + if let SridCheck::Resolve(ctx) = check { + ctx.resolve_u32(g.srid.code())?; + } + Ok(g.clone()) + } + Value::String(s) => geometry_from_text(s, check), + Value::Bytes(b) => geometry_from_bytes(b, check), + Value::Object(_) => Ok(match check { + SridCheck::Resolve(ctx) => geojson_object(value, ctx)?, + SridCheck::Syntax => stored_geojson_object(value)?, + }), + other => Err(SpatialError::NotGeometryValue { + found: other.type_name(), + }), + } +} + +/// The little-endian EWKB of the geometry `value` holds. +pub fn ewkb_of_value(value: &Value, check: SridCheck<'_>) -> Result, SpatialError> { + Ok(geometry_to_ewkb(&geometry_from_value(value, check)?)?) +} + +/// The geometry stored EWKB bytes hold, as a `Value`. The stored SRID is +/// checked for range only. +pub fn value_of_ewkb(bytes: &[u8]) -> Result { + geometry_from_bytes(bytes, SridCheck::Syntax).map(Value::Geometry) +} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + use super::*; + use nodedb_types::geometry::{Geometry, Ordinates, Srid}; + + fn point_object(crs: Option<&str>) -> Value { + let mut map = HashMap::new(); + map.insert("type".to_string(), Value::String("Point".into())); + map.insert( + "coordinates".to_string(), + Value::Array(vec![Value::Float(1.0), Value::Float(2.0)]), + ); + if let Some(name) = crs { + let mut props = HashMap::new(); + props.insert("name".to_string(), Value::String(name.into())); + let mut member = HashMap::new(); + member.insert("type".to_string(), Value::String("name".into())); + member.insert("properties".to_string(), Value::Object(props)); + map.insert("crs".to_string(), Value::Object(member)); + } + Value::Object(map) + } + + #[test] + fn every_input_form_round_trips_through_ewkb() { + let zm = SridGeometry::with_ordinates( + Srid::new(32648).unwrap(), + Geometry::point(1.0, 2.0), + Ordinates::new(Some(vec![3.0]), Some(vec![4.0])), + ) + .unwrap(); + let native = ewkb_of_value(&Value::Geometry(zm.clone()), SridCheck::Syntax).unwrap(); + assert_eq!(value_of_ewkb(&native), Ok(Value::Geometry(zm))); + let text = Value::String("POINT M (1 2 9)".into()); + let Ok(Value::Geometry(g)) = + value_of_ewkb(&ewkb_of_value(&text, SridCheck::Syntax).unwrap()) + else { + panic!("text form decodes"); + }; + assert_eq!(g.ordinates().m(), Some(&[9.0][..])); + let wkb = Value::Bytes(crate::geometry_to_wkb(&g.force_2d()).unwrap()); + assert!(ewkb_of_value(&wkb, SridCheck::Syntax).is_ok()); + } + + #[test] + fn a_native_geojson_object_decodes_with_its_crs() { + let wgs84 = geometry_from_value(&point_object(None), SridCheck::Syntax).unwrap(); + assert_eq!(wgs84, SridGeometry::wgs84(Geometry::point(1.0, 2.0))); + let stored = + geometry_from_value(&point_object(Some("EPSG:999")), SridCheck::Syntax).unwrap(); + assert_eq!(stored.srid.code(), 999); + let ctx = nodedb_crs::CrsContext::builtin_only(); + assert!(matches!( + geometry_from_value(&point_object(Some("EPSG:999")), SridCheck::Resolve(&ctx)), + Err(SpatialError::GeoJson(_)) + )); + } + + #[test] + fn other_value_types_are_not_geometry() { + assert_eq!( + geometry_from_value(&Value::Integer(1), SridCheck::Syntax), + Err(SpatialError::NotGeometryValue { found: "int" }) + ); + assert!(value_of_ewkb(b"{\"type\":\"Point\"}").is_err()); + } +} diff --git a/nodedb-spatial/src/wkb/error.rs b/nodedb-spatial/src/wkb/error.rs new file mode 100644 index 000000000..817bb3115 --- /dev/null +++ b/nodedb-spatial/src/wkb/error.rs @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! WKB decode and encode errors. + +use nodedb_types::geometry::{Dimension, OrdinateError}; + +/// Why WKB bytes do not decode, or a geometry does not encode. +#[derive(Debug, Clone, PartialEq, thiserror::Error)] +pub enum WkbError { + #[error("WKB truncated at byte {offset}")] + Truncated { offset: usize }, + #[error("WKB byte order marker {byte} at byte {offset} is neither 0 nor 1")] + ByteOrder { byte: u8, offset: usize }, + #[error("WKB type word {word:#010x} at byte {offset} names no geometry type")] + UnknownType { word: u32, offset: usize }, + #[error( + "WKB type word {word:#010x} at byte {offset} has EWKB flags and ISO code for different dimensions" + )] + ConflictingDimension { word: u32, offset: usize }, + #[error("WKB {parent} member at byte {offset} is {found}")] + MemberKind { + parent: &'static str, + found: &'static str, + offset: usize, + }, + #[error("WKB collection member at byte {offset} is {member}, the collection is {parent}")] + MemberDimension { + parent: Dimension, + member: Dimension, + offset: usize, + }, + #[error("WKB coordinate at byte {offset} is not finite")] + NonFinite { offset: usize }, + #[error("WKB count {count} exceeds {max} elements")] + TooManyElements { count: usize, max: usize }, + #[error("WKB exceeds {max} bytes")] + TooLarge { max: usize }, + #[error("WKB collection nesting exceeds {max} levels")] + TooDeep { max: usize }, + #[error("{count} bytes follow the WKB geometry")] + Trailing { count: usize }, + #[error("WKB ordinates: {0}")] + Ordinates(#[from] OrdinateError), +} diff --git a/nodedb-spatial/src/wkb/ewkb.rs b/nodedb-spatial/src/wkb/ewkb.rs index 92b652af9..1f1aecd49 100644 --- a/nodedb-spatial/src/wkb/ewkb.rs +++ b/nodedb-spatial/src/wkb/ewkb.rs @@ -1,43 +1,40 @@ // SPDX-License-Identifier: Apache-2.0 -//! PostGIS EWKB writer: ISO WKB with the SRID in the top-level header. +//! PostGIS EWKB writer. use nodedb_types::geometry::SridGeometry; -use super::format::{EWKB_SRID_FLAG, MAX_WKB_BYTES}; -use super::write::{geometry_to_wkb, write_u32}; - -/// Length of the WKB byte-order marker plus type word. -const HEADER_LEN: usize = 5; - -/// Serialize a geometry to little-endian EWKB with its SRID. -/// -/// The top-level type word gains [`EWKB_SRID_FLAG`] and the SRID code -/// follows it. Nested geometries stay plain WKB, as PostGIS writes them. -/// Returns `None` under the same limits as `geometry_to_wkb`. -pub fn geometry_to_ewkb(geom: &SridGeometry) -> Option> { - let iso = geometry_to_wkb(&geom.geometry)?; - if iso.len() + 4 > MAX_WKB_BYTES || iso.len() < HEADER_LEN { - return None; - } - let type_word = u32::from_le_bytes(iso[1..HEADER_LEN].try_into().ok()?); - let mut out = Vec::with_capacity(iso.len() + 4); - out.push(iso[0]); - write_u32(&mut out, type_word | EWKB_SRID_FLAG); - write_u32(&mut out, geom.srid.code()); - out.extend_from_slice(&iso[HEADER_LEN..]); - Some(out) +use super::error::WkbError; +use super::write::{ByteOrder, Convention, encode}; + +/// Serialize to little-endian EWKB: Z / M flags on every type word, the +/// SRID in the top-level header (`ST_AsEWKB`). +pub fn geometry_to_ewkb(g: &SridGeometry) -> Result, WkbError> { + geometry_to_ewkb_ordered(g, ByteOrder::LittleEndian) +} + +/// [`geometry_to_ewkb`] in `order`. +pub fn geometry_to_ewkb_ordered(g: &SridGeometry, order: ByteOrder) -> Result, WkbError> { + encode( + g, + Convention::Ewkb { + srid: g.srid.code(), + }, + order, + ) } #[cfg(test)] mod tests { use super::super::read::geometry_from_wkb; + use super::super::write::geometry_to_wkb; use super::*; use nodedb_types::geometry::{Geometry, Srid}; #[test] fn ewkb_round_trip_keeps_srid() { - for srid in Srid::SUPPORTED { + for code in [4326, 32650, 990_001] { + let srid = Srid::new(code).unwrap(); let g = SridGeometry::new( srid, Geometry::polygon(vec![vec![ @@ -47,16 +44,15 @@ mod tests { [0.0, 0.0], ]]), ); - let bytes = geometry_to_ewkb(&g).unwrap(); - let (geometry, code) = geometry_from_wkb(&bytes).unwrap(); - assert_eq!(geometry, g.geometry); - assert_eq!(code, Some(srid.code())); + let back = geometry_from_wkb(&geometry_to_ewkb(&g).unwrap()).unwrap(); + assert_eq!(back.geometry.geometry(), g.geometry()); + assert_eq!(back.srid, Some(srid.code())); } } #[test] fn iso_wkb_carries_no_srid() { - let iso = geometry_to_wkb(&Geometry::point(1.0, 2.0)).unwrap(); - assert_eq!(geometry_from_wkb(&iso).map(|(_, s)| s), Some(None)); + let iso = geometry_to_wkb(&SridGeometry::wgs84(Geometry::point(1.0, 2.0))).unwrap(); + assert_eq!(geometry_from_wkb(&iso).map(|w| w.srid), Ok(None)); } } diff --git a/nodedb-spatial/src/wkb/format.rs b/nodedb-spatial/src/wkb/format.rs index 9d3d9f75a..fe7fa7a72 100644 --- a/nodedb-spatial/src/wkb/format.rs +++ b/nodedb-spatial/src/wkb/format.rs @@ -1,21 +1,26 @@ // SPDX-License-Identifier: Apache-2.0 -//! WKB type codes and decoder limits shared by the reader and writers. +//! WKB type words, flags and decoder limits shared by the reader and +//! writers. -// WKB geometry type codes (ISO 13249 / OGC SFA). -pub(super) const WKB_POINT: u32 = 1; -pub(super) const WKB_LINESTRING: u32 = 2; -pub(super) const WKB_POLYGON: u32 = 3; -pub(super) const WKB_MULTIPOINT: u32 = 4; -pub(super) const WKB_MULTILINESTRING: u32 = 5; -pub(super) const WKB_MULTIPOLYGON: u32 = 6; -pub(super) const WKB_GEOMETRYCOLLECTION: u32 = 7; +use nodedb_types::geometry::{Dimension, GeometryKind}; +pub(super) const BYTE_ORDER_BE: u8 = 0; pub(super) const BYTE_ORDER_LE: u8 = 1; +/// PostGIS EWKB flag: Z ordinates follow X and Y. +pub const EWKB_Z_FLAG: u32 = 0x8000_0000; +/// PostGIS EWKB flag: M ordinates follow X, Y and Z. +pub const EWKB_M_FLAG: u32 = 0x4000_0000; /// PostGIS EWKB flag: a 4-byte SRID follows the type word. pub const EWKB_SRID_FLAG: u32 = 0x2000_0000; +/// The bits of a type word the EWKB flags do not use. +pub(super) const TYPE_CODE_MASK: u32 = 0x0FFF_FFFF; + +/// ISO 13249 adds 1000 for Z, 2000 for M, 3000 for ZM to the base code. +pub(super) const ISO_DIMENSION_STEP: u32 = 1000; + // WKB count fields are attacker-controlled. The bound caps allocation and // parsing work even when the encoded input itself is very large. pub(super) const MAX_WKB_ELEMENTS: usize = 1_000_000; @@ -23,3 +28,103 @@ pub(super) const MAX_WKB_BYTES: usize = 64 * 1024 * 1024; // Nested geometry collections are recursive on the stack. Keep the budget // explicit so hostile nesting cannot exhaust the decoder thread's stack. pub(super) const MAX_WKB_DEPTH: usize = 64; + +/// The ISO type word of `kind` in `dimension`. +pub(super) fn iso_type_word(kind: GeometryKind, dimension: Dimension) -> u32 { + let step = match dimension { + Dimension::Xy => 0, + Dimension::Xyz => 1, + Dimension::Xym => 2, + Dimension::Xyzm => 3, + }; + kind.wkb_code() + step * ISO_DIMENSION_STEP +} + +/// The EWKB type word of `kind` in `dimension`, SRID flag excluded. +pub(super) fn ewkb_type_word(kind: GeometryKind, dimension: Dimension) -> u32 { + let mut word = kind.wkb_code(); + if dimension.has_z() { + word |= EWKB_Z_FLAG; + } + if dimension.has_m() { + word |= EWKB_M_FLAG; + } + word +} + +/// A decoded type word. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) struct TypeWord { + pub kind: GeometryKind, + pub dimension: Dimension, + pub has_srid: bool, +} + +/// Why a type word does not decode. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) enum TypeWordError { + UnknownCode, + /// EWKB flags and the ISO thousands name different dimensions. + ConflictingDimension, +} + +/// Decode a type word in either the ISO or the EWKB convention. +pub(super) fn decode_type_word(raw: u32) -> Result { + let code = raw & TYPE_CODE_MASK; + let iso_step = code / ISO_DIMENSION_STEP; + let kind = + GeometryKind::from_wkb_code(code % ISO_DIMENSION_STEP).ok_or(TypeWordError::UnknownCode)?; + let iso = match iso_step { + 0 => None, + 1 => Some(Dimension::Xyz), + 2 => Some(Dimension::Xym), + 3 => Some(Dimension::Xyzm), + _ => return Err(TypeWordError::UnknownCode), + }; + let flags = Dimension::from_flags(raw & EWKB_Z_FLAG != 0, raw & EWKB_M_FLAG != 0); + let dimension = match (iso, flags) { + (None, flags) => flags, + (Some(iso), Dimension::Xy) => iso, + (Some(iso), flags) if iso == flags => iso, + (Some(_), _) => return Err(TypeWordError::ConflictingDimension), + }; + Ok(TypeWord { + kind, + dimension, + has_srid: raw & EWKB_SRID_FLAG != 0, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn iso_and_ewkb_words_decode_alike() { + for dimension in [ + Dimension::Xy, + Dimension::Xyz, + Dimension::Xym, + Dimension::Xyzm, + ] { + for kind in [GeometryKind::Point, GeometryKind::GeometryCollection] { + let iso = decode_type_word(iso_type_word(kind, dimension)).unwrap(); + let ewkb = decode_type_word(ewkb_type_word(kind, dimension)).unwrap(); + assert_eq!(iso, ewkb); + assert_eq!(iso.dimension, dimension); + assert_eq!(iso.kind, kind); + } + } + assert_eq!(iso_type_word(GeometryKind::Point, Dimension::Xyzm), 3001); + } + + #[test] + fn conflicting_and_unknown_words_fail() { + assert_eq!( + decode_type_word(1001 | EWKB_M_FLAG), + Err(TypeWordError::ConflictingDimension) + ); + assert_eq!(decode_type_word(8), Err(TypeWordError::UnknownCode)); + assert_eq!(decode_type_word(4001), Err(TypeWordError::UnknownCode)); + } +} diff --git a/nodedb-spatial/src/wkb/mod.rs b/nodedb-spatial/src/wkb/mod.rs index 678c08189..ee7ee1bb6 100644 --- a/nodedb-spatial/src/wkb/mod.rs +++ b/nodedb-spatial/src/wkb/mod.rs @@ -1,21 +1,20 @@ // SPDX-License-Identifier: Apache-2.0 -//! Well-Known Binary (WKB) serialization for Geometry types. +//! Well-Known Binary (ISO 13249) and PostGIS EWKB, with Z / M / ZM and +//! EMPTY. //! -//! WKB is the standard binary format for geometry interchange (ISO 13249). -//! Used as the Arrow `DataType::Binary` backing for spatial columns, which -//! avoids JSON parse overhead during query execution. -//! -//! Format: `[byte_order: u8] [type: u32] [coordinates...]`. Byte order 1 is -//! little-endian (NDR), 0 is big-endian (XDR). Writers always emit -//! little-endian. The reader accepts both, plus PostGIS EWKB. +//! Format: `[byte_order: u8] [type: u32] [srid: u32 if EWKB] [body]`. Byte +//! order 1 is little-endian (NDR), 0 is big-endian (XDR). Writers emit +//! little-endian. The reader accepts both. +pub mod error; pub mod ewkb; pub mod format; pub mod read; pub mod write; -pub use ewkb::geometry_to_ewkb; -pub use format::EWKB_SRID_FLAG; -pub use read::{geometry_from_wkb, wkb_bbox}; -pub use write::geometry_to_wkb; +pub use error::WkbError; +pub use ewkb::{geometry_to_ewkb, geometry_to_ewkb_ordered}; +pub use format::{EWKB_M_FLAG, EWKB_SRID_FLAG, EWKB_Z_FLAG}; +pub use read::{Wkb, geometry_from_wkb, wkb_bbox}; +pub use write::{ByteOrder, geometry_to_wkb, geometry_to_wkb_ordered}; diff --git a/nodedb-spatial/src/wkb/read.rs b/nodedb-spatial/src/wkb/read.rs index cd236f4a4..c4f698f5a 100644 --- a/nodedb-spatial/src/wkb/read.rs +++ b/nodedb-spatial/src/wkb/read.rs @@ -2,386 +2,627 @@ //! WKB / EWKB reader. //! -//! Accepts ISO WKB and PostGIS EWKB in either byte order. An EWKB type word -//! with [`EWKB_SRID_FLAG`] set carries a 4-byte SRID after it. The reader -//! masks the flag, and reports the top-level SRID to the caller. +//! Accepts ISO WKB (Z / M / ZM as +1000 / +2000 / +3000) and PostGIS EWKB +//! (Z / M / SRID flags) in either byte order. An EMPTY point is all-NaN +//! ordinates. Every member of a multi or collection has the parent's +//! dimension. Only the top-level SRID is reported. +//! +//! Every declared element consumes input bytes. The reader keeps one +//! budget of input bytes promised to declared but unread elements across +//! all nesting levels. A count fits only into the unpromised remainder, so +//! the total reserved capacity stays a small multiple of the input size. -use nodedb_types::geometry::Geometry; +use nodedb_types::geometry::{ + Dimension, Geometry, GeometryKind, OrdinatesBuilder, Srid, SridGeometry, +}; +use super::error::WkbError; use super::format::{ - EWKB_SRID_FLAG, MAX_WKB_BYTES, MAX_WKB_DEPTH, MAX_WKB_ELEMENTS, WKB_GEOMETRYCOLLECTION, - WKB_LINESTRING, WKB_MULTILINESTRING, WKB_MULTIPOINT, WKB_MULTIPOLYGON, WKB_POINT, WKB_POLYGON, + BYTE_ORDER_BE, BYTE_ORDER_LE, MAX_WKB_BYTES, MAX_WKB_DEPTH, MAX_WKB_ELEMENTS, TypeWordError, + decode_type_word, }; -/// Deserialize a Geometry from WKB or EWKB bytes. -/// -/// Returns the geometry and the SRID an EWKB header declares, `None` for -/// ISO WKB. The SRID is the raw code: the caller decides whether it is -/// supported. Returns `None` overall if the bytes are malformed or truncated. -pub fn geometry_from_wkb(data: &[u8]) -> Option<(Geometry, Option)> { +/// Bytes of a header: byte order marker and type word. +const HEADER_BYTES: usize = 5; +/// Bytes of an element count. +const COUNT_BYTES: usize = 4; +/// Bytes of a member header followed by its count. +const NESTED_MEMBER_BYTES: usize = HEADER_BYTES + COUNT_BYTES; + +/// A decoded WKB value. +#[derive(Debug, Clone, PartialEq)] +pub struct Wkb { + /// The SRID an EWKB header declares, unresolved. `None` for ISO WKB. + pub srid: Option, + /// The geometry, tagged WGS 84. The caller retags it once `srid` + /// resolves. + pub geometry: SridGeometry, +} + +/// Decode WKB or EWKB bytes. +pub fn geometry_from_wkb(data: &[u8]) -> Result { if data.len() > MAX_WKB_BYTES { - return None; + return Err(WkbError::TooLarge { max: MAX_WKB_BYTES }); + } + let mut reader = Reader { + data, + cursor: 0, + promised: 0, + ordinates: OrdinatesBuilder::default(), + }; + let header = reader.header()?; + reader.ordinates = OrdinatesBuilder::with_dimension(header.dimension); + let geometry = reader.body(header, MAX_WKB_DEPTH)?; + if reader.cursor != data.len() { + return Err(WkbError::Trailing { + count: data.len() - reader.cursor, + }); } - let mut cursor = 0; - let (geometry, srid) = read_geometry(data, &mut cursor, MAX_WKB_DEPTH)?; - Some((geometry, srid)) + let ordinates = reader.ordinates.finish(); + Ok(Wkb { + srid: header.srid, + geometry: SridGeometry::with_ordinates(Srid::WGS84, geometry, ordinates)?, + }) } -/// Extract bounding box from WKB, in geographic space. -pub fn wkb_bbox(data: &[u8]) -> Option { - let (geom, _) = geometry_from_wkb(data)?; - Some(nodedb_types::geometry_bbox(&geom)) +/// Bounding box of WKB bytes, in geographic space. +pub fn wkb_bbox(data: &[u8]) -> Result { + let wkb = geometry_from_wkb(data)?; + Ok(nodedb_types::geometry_bbox(wkb.geometry.geometry())) } -/// One geometry and the SRID its own header declared. -fn read_geometry( - data: &[u8], - cursor: &mut usize, - depth_remaining: usize, -) -> Option<(Geometry, Option)> { - if depth_remaining == 0 { - return None; - } - let byte_order = read_u8(data, cursor)?; - let is_le = byte_order == 1; - let raw_type = read_u32(data, cursor, is_le)?; - let srid = if raw_type & EWKB_SRID_FLAG != 0 { - Some(read_u32(data, cursor, is_le)?) - } else { - None - }; - let wkb_type = raw_type & !EWKB_SRID_FLAG; - let child = |data: &[u8], cursor: &mut usize| { - read_geometry(data, cursor, depth_remaining - 1).map(|(g, _)| g) - }; +/// One header: byte order, type word and the optional SRID. +#[derive(Debug, Clone, Copy)] +struct Header { + little_endian: bool, + kind: GeometryKind, + dimension: Dimension, + srid: Option, + /// Offset of the byte order marker. + offset: usize, +} - let geometry = match wkb_type { - WKB_POINT => { - let x = read_f64(data, cursor, is_le)?; - let y = read_f64(data, cursor, is_le)?; - Geometry::Point { - coordinates: [x, y], - } +struct Reader<'a> { + data: &'a [u8], + cursor: usize, + /// Input bytes promised to declared elements not yet started. + promised: usize, + ordinates: OrdinatesBuilder, +} + +impl Reader<'_> { + fn header(&mut self) -> Result { + let offset = self.cursor; + let byte = self.u8()?; + let little_endian = match byte { + BYTE_ORDER_LE => true, + BYTE_ORDER_BE => false, + byte => return Err(WkbError::ByteOrder { byte, offset }), + }; + let word = self.u32(little_endian)?; + let decoded = decode_type_word(word).map_err(|e| match e { + TypeWordError::UnknownCode => WkbError::UnknownType { + word, + offset: offset + 1, + }, + TypeWordError::ConflictingDimension => WkbError::ConflictingDimension { + word, + offset: offset + 1, + }, + })?; + let srid = if decoded.has_srid { + Some(self.u32(little_endian)?) + } else { + None + }; + Ok(Header { + little_endian, + kind: decoded.kind, + dimension: decoded.dimension, + srid, + offset, + }) + } + + /// A member header: its dimension must equal the parent's. + fn member(&mut self, parent: Dimension) -> Result { + let header = self.header()?; + if header.dimension != parent { + return Err(WkbError::MemberDimension { + parent, + member: header.dimension, + offset: header.offset, + }); } - WKB_LINESTRING => { - let count = read_u32(data, cursor, is_le)? as usize; - Geometry::LineString { - coordinates: read_coords(data, cursor, count, is_le)?, - } + Ok(header) + } + + /// A member of `parent_kind` that must be a `want`. + fn typed_member(&mut self, parent: Header, want: GeometryKind) -> Result { + let header = self.member(parent.dimension)?; + if header.kind != want { + return Err(WkbError::MemberKind { + parent: parent.kind.name(), + found: header.kind.name(), + offset: header.offset, + }); } - WKB_POLYGON => { - let count = read_u32(data, cursor, is_le)? as usize; - let capacity = checked_wkb_capacity(data, cursor, count, 4)?; - let mut rings = Vec::with_capacity(capacity); - for _ in 0..capacity { - let ring_count = read_u32(data, cursor, is_le)? as usize; - rings.push(read_coords(data, cursor, ring_count, is_le)?); - } - Geometry::Polygon { coordinates: rings } + Ok(header) + } + + fn body(&mut self, h: Header, depth: usize) -> Result { + if depth == 0 { + return Err(WkbError::TooDeep { max: MAX_WKB_DEPTH }); } - WKB_MULTIPOINT => { - let count = read_u32(data, cursor, is_le)? as usize; - let capacity = checked_wkb_capacity(data, cursor, count, 21)?; - let mut coords = Vec::with_capacity(capacity); - for _ in 0..capacity { - let Geometry::Point { coordinates } = child(data, cursor)? else { - return None; - }; - coords.push(coordinates); + let stride = 8 * usize::from(h.dimension.coord_dim()); + let point_member = HEADER_BYTES + stride; + Ok(match h.kind { + GeometryKind::Point => match self.point(h)? { + Some(coordinates) => Geometry::Point { coordinates }, + None => Geometry::empty_point(), + }, + GeometryKind::LineString => Geometry::LineString { + coordinates: self.coords(h)?, + }, + GeometryKind::Polygon => Geometry::Polygon { + coordinates: self.rings(h)?, + }, + GeometryKind::MultiPoint => { + let count = self.checked_wkb_capacity(h.little_endian, point_member)?; + let mut points = Vec::with_capacity(count); + for _ in 0..count { + self.start_element(point_member); + let member = self.typed_member(h, GeometryKind::Point)?; + let offset = self.cursor; + let coordinates = self.point(member)?.ok_or(WkbError::MemberKind { + parent: h.kind.name(), + found: "an EMPTY point", + offset, + })?; + points.push(coordinates); + } + Geometry::MultiPoint { + coordinates: points, + } } - Geometry::MultiPoint { - coordinates: coords, + GeometryKind::MultiLineString => { + let count = self.checked_wkb_capacity(h.little_endian, NESTED_MEMBER_BYTES)?; + let mut lines = Vec::with_capacity(count); + for _ in 0..count { + self.start_element(NESTED_MEMBER_BYTES); + let member = self.typed_member(h, GeometryKind::LineString)?; + lines.push(self.coords(member)?); + } + Geometry::MultiLineString { coordinates: lines } } - } - WKB_MULTILINESTRING => { - let count = read_u32(data, cursor, is_le)? as usize; - let capacity = checked_wkb_capacity(data, cursor, count, 5)?; - let mut lines = Vec::with_capacity(capacity); - for _ in 0..capacity { - let Geometry::LineString { coordinates } = child(data, cursor)? else { - return None; - }; - lines.push(coordinates); + GeometryKind::MultiPolygon => { + let count = self.checked_wkb_capacity(h.little_endian, NESTED_MEMBER_BYTES)?; + let mut polygons = Vec::with_capacity(count); + for _ in 0..count { + self.start_element(NESTED_MEMBER_BYTES); + let member = self.typed_member(h, GeometryKind::Polygon)?; + polygons.push(self.rings(member)?); + } + Geometry::MultiPolygon { + coordinates: polygons, + } } - Geometry::MultiLineString { coordinates: lines } - } - WKB_MULTIPOLYGON => { - let count = read_u32(data, cursor, is_le)? as usize; - let capacity = checked_wkb_capacity(data, cursor, count, 5)?; - let mut polys = Vec::with_capacity(capacity); - for _ in 0..capacity { - let Geometry::Polygon { coordinates } = child(data, cursor)? else { - return None; - }; - polys.push(coordinates); + GeometryKind::GeometryCollection => { + let count = self.checked_wkb_capacity(h.little_endian, HEADER_BYTES)?; + let mut geometries = Vec::with_capacity(count); + for _ in 0..count { + self.start_element(HEADER_BYTES); + let member = self.member(h.dimension)?; + geometries.push(self.body(member, depth - 1)?); + } + Geometry::GeometryCollection { geometries } } - Geometry::MultiPolygon { coordinates: polys } + }) + } + + /// A point body. All-NaN ordinates are the EMPTY point (`None`) and + /// record no coordinate. + fn point(&mut self, h: Header) -> Result, WkbError> { + let offset = self.cursor; + let values = self.ordinate_values(h)?; + let used = usize::from(h.dimension.coord_dim()); + if values[..used].iter().all(|v| v.is_nan()) { + return Ok(None); } - WKB_GEOMETRYCOLLECTION => { - let count = read_u32(data, cursor, is_le)? as usize; - let capacity = checked_wkb_capacity(data, cursor, count, 5)?; - let mut geoms = Vec::with_capacity(capacity); - for _ in 0..capacity { - geoms.push(child(data, cursor)?); - } - Geometry::GeometryCollection { geometries: geoms } + self.record(values, h.dimension, offset).map(Some) + } + + /// A polygon body: a ring count, then each ring. + fn rings(&mut self, h: Header) -> Result>, WkbError> { + let count = self.checked_wkb_capacity(h.little_endian, COUNT_BYTES)?; + let mut rings = Vec::with_capacity(count); + for _ in 0..count { + self.start_element(COUNT_BYTES); + rings.push(self.coords(h)?); } - _ => return None, - }; - Some((geometry, srid)) -} + Ok(rings) + } -/// Validate an untrusted count before using it as a vector capacity. -/// -/// Every encoded element must occupy at least `min_encoded_bytes` after the -/// current cursor. This makes a truncated count fail before allocation, while -/// the independent element cap bounds work on large valid inputs. -fn checked_wkb_capacity( - data: &[u8], - cursor: &usize, - count: usize, - min_encoded_bytes: usize, -) -> Option { - if count > MAX_WKB_ELEMENTS { - return None; - } - let required = count.checked_mul(min_encoded_bytes)?; - let remaining = data.len().checked_sub(*cursor)?; - (required <= remaining).then_some(count) -} + fn coords(&mut self, h: Header) -> Result, WkbError> { + let stride = 8 * usize::from(h.dimension.coord_dim()); + let count = self.checked_wkb_capacity(h.little_endian, stride)?; + let mut coords = Vec::with_capacity(count); + for _ in 0..count { + self.start_element(stride); + let offset = self.cursor; + let values = self.ordinate_values(h)?; + coords.push(self.record(values, h.dimension, offset)?); + } + Ok(coords) + } -fn read_u8(data: &[u8], cursor: &mut usize) -> Option { - let val = *data.get(*cursor)?; - *cursor += 1; - Some(val) -} + /// The 2 to 4 ordinates of one coordinate. + fn ordinate_values(&mut self, h: Header) -> Result<[f64; 4], WkbError> { + let mut values = [0.0; 4]; + for slot in values.iter_mut().take(usize::from(h.dimension.coord_dim())) { + *slot = self.f64(h.little_endian)?; + } + Ok(values) + } -fn read_u32(data: &[u8], cursor: &mut usize, is_le: bool) -> Option { - let bytes: [u8; 4] = data.get(*cursor..*cursor + 4)?.try_into().ok()?; - *cursor += 4; - Some(if is_le { - u32::from_le_bytes(bytes) - } else { - u32::from_be_bytes(bytes) - }) -} + /// Check one coordinate's ordinates and record its Z and M. + fn record( + &mut self, + values: [f64; 4], + dimension: Dimension, + offset: usize, + ) -> Result<[f64; 2], WkbError> { + let used = usize::from(dimension.coord_dim()); + if values[..used].iter().any(|v| !v.is_finite()) { + return Err(WkbError::NonFinite { offset }); + } + let z = dimension.has_z().then_some(values[2]); + let m = dimension + .has_m() + .then_some(values[if dimension.has_z() { 3 } else { 2 }]); + self.ordinates.push(z, m)?; + Ok([values[0], values[1]]) + } -fn read_f64(data: &[u8], cursor: &mut usize, is_le: bool) -> Option { - let bytes: [u8; 8] = data.get(*cursor..*cursor + 8)?.try_into().ok()?; - *cursor += 8; - Some(if is_le { - f64::from_le_bytes(bytes) - } else { - f64::from_be_bytes(bytes) - }) -} + /// An element count whose elements occupy at least `min_bytes` each. + /// The elements claim their bytes from the unpromised input. A count + /// that does not fit fails before any allocation. + fn checked_wkb_capacity( + &mut self, + little_endian: bool, + min_bytes: usize, + ) -> Result { + let offset = self.cursor; + let count = self.u32(little_endian)? as usize; + if count > MAX_WKB_ELEMENTS { + return Err(WkbError::TooManyElements { + count, + max: MAX_WKB_ELEMENTS, + }); + } + let required = count.saturating_mul(min_bytes); + let unpromised = self + .data + .len() + .saturating_sub(self.cursor) + .saturating_sub(self.promised); + if required > unpromised { + return Err(WkbError::Truncated { offset }); + } + self.promised += required; + Ok(count) + } + + /// Start reading one declared element: release its promised bytes. + /// Its fixed part consumes at least these bytes before any nested + /// count claims more. + fn start_element(&mut self, min_bytes: usize) { + self.promised = self.promised.saturating_sub(min_bytes); + } + + fn take(&mut self) -> Result<[u8; N], WkbError> { + let end = self.cursor.checked_add(N).ok_or(WkbError::Truncated { + offset: self.cursor, + })?; + let bytes: [u8; N] = self + .data + .get(self.cursor..end) + .and_then(|s| s.try_into().ok()) + .ok_or(WkbError::Truncated { + offset: self.cursor, + })?; + self.cursor = end; + Ok(bytes) + } + + fn u8(&mut self) -> Result { + self.take::<1>().map(|b| b[0]) + } -fn read_coords( - data: &[u8], - cursor: &mut usize, - count: usize, - is_le: bool, -) -> Option> { - let capacity = checked_wkb_capacity(data, cursor, count, 16)?; - let mut coords = Vec::with_capacity(capacity); - for _ in 0..capacity { - let x = read_f64(data, cursor, is_le)?; - let y = read_f64(data, cursor, is_le)?; - coords.push([x, y]); - } - Some(coords) + fn u32(&mut self, little_endian: bool) -> Result { + let bytes = self.take::<4>()?; + Ok(if little_endian { + u32::from_le_bytes(bytes) + } else { + u32::from_be_bytes(bytes) + }) + } + + fn f64(&mut self, little_endian: bool) -> Result { + let bytes = self.take::<8>()?; + Ok(if little_endian { + f64::from_le_bytes(bytes) + } else { + f64::from_be_bytes(bytes) + }) + } } #[cfg(test)] mod tests { - use super::super::write::{geometry_to_wkb as encode, write_header, write_u32}; + use super::super::ewkb::geometry_to_ewkb; + use super::super::format::{EWKB_SRID_FLAG, EWKB_Z_FLAG}; + use super::super::write::geometry_to_wkb; use super::*; + use nodedb_types::geometry::Ordinates; - fn geometry_to_wkb(geometry: &Geometry) -> Vec { - encode(geometry).expect("test geometry must fit WKB limits") + fn xy(g: Geometry) -> SridGeometry { + SridGeometry::wgs84(g) } - fn decode(wkb: &[u8]) -> Option { - geometry_from_wkb(wkb).map(|(g, _)| g) + fn decode(bytes: &[u8]) -> Result { + geometry_from_wkb(bytes).map(|w| w.geometry) } - #[test] - fn point_roundtrip() { - let geom = Geometry::point(-73.9857, 40.7484); - let wkb = geometry_to_wkb(&geom); - assert_eq!(geometry_from_wkb(&wkb), Some((geom, None))); + fn header(buf: &mut Vec, word: u32) { + buf.push(1); + buf.extend_from_slice(&word.to_le_bytes()); + } + + fn shapes() -> Vec { + vec![ + Geometry::point(-73.9857, 40.7484), + Geometry::line_string(vec![[0.0, 0.0], [1.0, 1.0], [2.0, 0.0]]), + Geometry::polygon(vec![ + vec![ + [0.0, 0.0], + [10.0, 0.0], + [10.0, 10.0], + [0.0, 10.0], + [0.0, 0.0], + ], + vec![[2.0, 2.0], [3.0, 2.0], [3.0, 3.0], [2.0, 3.0], [2.0, 2.0]], + ]), + Geometry::MultiPoint { + coordinates: vec![[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]], + }, + Geometry::MultiLineString { + coordinates: vec![vec![[0.0, 0.0], [1.0, 1.0]], vec![[2.0, 2.0], [4.0, 2.0]]], + }, + Geometry::MultiPolygon { + coordinates: vec![ + vec![vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 0.0]]], + vec![vec![[5.0, 5.0], [6.0, 5.0], [6.0, 6.0], [5.0, 5.0]]], + ], + }, + Geometry::GeometryCollection { + geometries: vec![ + Geometry::point(1.0, 2.0), + Geometry::line_string(vec![[0.0, 0.0], [1.0, 1.0]]), + ], + }, + ] } #[test] - fn linestring_roundtrip() { - let geom = Geometry::line_string(vec![[0.0, 0.0], [1.0, 1.0], [2.0, 0.0]]); - assert_eq!(decode(&geometry_to_wkb(&geom)), Some(geom)); + fn xy_shapes_round_trip() { + for g in shapes() { + let g = xy(g); + let wkb = geometry_to_wkb(&g).unwrap(); + assert_eq!(decode(&wkb), Ok(g)); + } } #[test] - fn polygon_roundtrip() { - let geom = Geometry::polygon(vec![ - vec![ - [0.0, 0.0], - [10.0, 0.0], - [10.0, 10.0], - [0.0, 10.0], - [0.0, 0.0], - ], - vec![[2.0, 2.0], [3.0, 2.0], [3.0, 3.0], [2.0, 3.0], [2.0, 2.0]], // hole - ]); - assert_eq!(decode(&geometry_to_wkb(&geom)), Some(geom)); + fn z_m_and_zm_round_trip_in_iso_and_ewkb() { + for g in shapes() { + let n = g.coord_count(); + let seq = |k: f64| (0..n).map(|i| k + i as f64).collect::>(); + for ordinates in [ + Ordinates::new(Some(seq(100.0)), None), + Ordinates::new(None, Some(seq(200.0))), + Ordinates::new(Some(seq(100.0)), Some(seq(200.0))), + ] { + let g = SridGeometry::with_ordinates(Srid::WGS84, g.clone(), ordinates).unwrap(); + let iso = geometry_to_wkb(&g).unwrap(); + assert_eq!(decode(&iso), Ok(g.clone())); + let ewkb = geometry_to_ewkb(&g).unwrap(); + let back = geometry_from_wkb(&ewkb).unwrap(); + assert_eq!(back.srid, Some(4326)); + assert_eq!(back.geometry, g); + } + } } #[test] - fn multipoint_roundtrip() { - let geom = Geometry::MultiPoint { - coordinates: vec![[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]], - }; - assert_eq!(decode(&geometry_to_wkb(&geom)), Some(geom)); + fn iso_point_zm_layout() { + let g = SridGeometry::with_ordinates( + Srid::WGS84, + Geometry::point(1.0, 2.0), + Ordinates::new(Some(vec![3.0]), Some(vec![4.0])), + ) + .unwrap(); + let wkb = geometry_to_wkb(&g).unwrap(); + assert_eq!(wkb.len(), 5 + 32); + assert_eq!(u32::from_le_bytes(wkb[1..5].try_into().unwrap()), 3001); } #[test] - fn multilinestring_roundtrip() { - let geom = Geometry::MultiLineString { - coordinates: vec![ - vec![[0.0, 0.0], [1.0, 1.0]], - vec![[2.0, 2.0], [3.0, 3.0], [4.0, 2.0]], - ], - }; - assert_eq!(decode(&geometry_to_wkb(&geom)), Some(geom)); + fn empty_geometries_round_trip() { + for kind in [ + GeometryKind::Point, + GeometryKind::LineString, + GeometryKind::Polygon, + GeometryKind::MultiPoint, + GeometryKind::MultiLineString, + GeometryKind::MultiPolygon, + GeometryKind::GeometryCollection, + ] { + let g = xy(Geometry::empty(kind)); + let back = decode(&geometry_to_wkb(&g).unwrap()).unwrap(); + assert!(back.is_empty(), "{kind:?}"); + assert_eq!(back.geometry().kind(), kind); + } } #[test] - fn multipolygon_roundtrip() { - let geom = Geometry::MultiPolygon { - coordinates: vec![ - vec![vec![ - [0.0, 0.0], - [1.0, 0.0], - [1.0, 1.0], - [0.0, 1.0], - [0.0, 0.0], - ]], - vec![vec![ - [5.0, 5.0], - [6.0, 5.0], - [6.0, 6.0], - [5.0, 6.0], - [5.0, 5.0], - ]], - ], - }; - assert_eq!(decode(&geometry_to_wkb(&geom)), Some(geom)); + fn member_dimension_must_match() { + let mut wkb = Vec::new(); + header(&mut wkb, 1007); // GEOMETRYCOLLECTION Z + wkb.extend_from_slice(&1u32.to_le_bytes()); + header(&mut wkb, 1); // POINT (XY) + wkb.extend_from_slice(&1.0f64.to_le_bytes()); + wkb.extend_from_slice(&2.0f64.to_le_bytes()); + assert!(matches!( + decode(&wkb), + Err(WkbError::MemberDimension { .. }) + )); } #[test] - fn geometry_collection_roundtrip() { - let geom = Geometry::GeometryCollection { - geometries: vec![ - Geometry::point(1.0, 2.0), - Geometry::line_string(vec![[0.0, 0.0], [1.0, 1.0]]), - ], - }; - assert_eq!(decode(&geometry_to_wkb(&geom)), Some(geom)); + fn ewkb_flags_and_srid_are_read() { + let mut wkb = Vec::new(); + header(&mut wkb, 1 | EWKB_Z_FLAG | EWKB_SRID_FLAG); + wkb.extend_from_slice(&32648u32.to_le_bytes()); + for v in [1.0f64, 2.0, 3.0] { + wkb.extend_from_slice(&v.to_le_bytes()); + } + let w = geometry_from_wkb(&wkb).unwrap(); + assert_eq!(w.srid, Some(32648)); + assert_eq!(w.geometry.ordinates().z(), Some(&[3.0][..])); } #[test] - fn parsed_max_counts_are_rejected_before_allocation() { - for geometry_type in [ - WKB_LINESTRING, - WKB_POLYGON, - WKB_MULTIPOINT, - WKB_MULTILINESTRING, - WKB_MULTIPOLYGON, - WKB_GEOMETRYCOLLECTION, - ] { - let mut wkb = Vec::new(); - write_header(&mut wkb, geometry_type); - write_u32(&mut wkb, u32::MAX); - assert!(decode(&wkb).is_none(), "type={geometry_type}"); + fn big_endian_is_read() { + let mut wkb = vec![0u8]; + wkb.extend_from_slice(&1001u32.to_be_bytes()); + for v in [1.0f64, 2.0, 3.0] { + wkb.extend_from_slice(&v.to_be_bytes()); } + assert_eq!(decode(&wkb).unwrap().ordinates().z(), Some(&[3.0][..])); } #[test] - fn truncated_count_bodies_are_rejected_before_allocation() { - for geometry_type in [ - WKB_LINESTRING, - WKB_POLYGON, - WKB_MULTIPOINT, - WKB_MULTILINESTRING, - WKB_MULTIPOLYGON, - WKB_GEOMETRYCOLLECTION, - ] { - let mut wkb = Vec::new(); - write_header(&mut wkb, geometry_type); - write_u32(&mut wkb, 1); - assert!(decode(&wkb).is_none(), "type={geometry_type}"); + fn malformed_bytes_are_errors() { + let wkb = geometry_to_wkb(&xy(Geometry::point(1.0, 2.0))).unwrap(); + assert!(matches!(decode(&wkb[..3]), Err(WkbError::Truncated { .. }))); + assert!(matches!(decode(&[]), Err(WkbError::Truncated { .. }))); + let mut bad = wkb.clone(); + bad[1] = 99; + assert!(matches!(decode(&bad), Err(WkbError::UnknownType { .. }))); + let mut order = wkb.clone(); + order[0] = 7; + assert!(matches!(decode(&order), Err(WkbError::ByteOrder { .. }))); + let mut trailing = wkb; + trailing.push(0); + assert_eq!(decode(&trailing), Err(WkbError::Trailing { count: 1 })); + } + + #[test] + fn oversized_and_truncated_counts_fail_before_allocation() { + for code in [2u32, 3, 4, 5, 6, 7] { + for count in [u32::MAX, 1] { + let mut wkb = Vec::new(); + header(&mut wkb, code); + wkb.extend_from_slice(&count.to_le_bytes()); + assert!(decode(&wkb).is_err(), "type={code} count={count}"); + } } } #[test] - fn geometry_collection_depth_is_bounded() { - let mut valid = Geometry::point(1.0, 2.0); - for _ in 0..MAX_WKB_DEPTH - 1 { - valid = Geometry::GeometryCollection { - geometries: vec![valid], - }; + fn nested_counts_share_one_budget() { + // The outer collection promises every remaining byte to its members, + // so the first member cannot claim the same bytes again. + let padding = 100usize; + let mut wkb = Vec::new(); + header(&mut wkb, 7); + let outer = (HEADER_BYTES + COUNT_BYTES + padding) / HEADER_BYTES; + wkb.extend_from_slice(&(outer as u32).to_le_bytes()); + header(&mut wkb, 7); + let inner_count_offset = wkb.len(); + wkb.extend_from_slice(&2u32.to_le_bytes()); + wkb.extend(std::iter::repeat_n(0u8, padding)); + assert_eq!( + decode(&wkb), + Err(WkbError::Truncated { + offset: inner_count_offset + }) + ); + } + + #[test] + fn deep_nesting_reserves_capacity_once() { + // Every level declares the largest count the input admits. Only the + // outermost claim fits, so the reader rejects the second level. + let mut wkb = Vec::new(); + let levels = 32usize; + for _ in 0..levels { + header(&mut wkb, 7); + wkb.extend_from_slice(&0u32.to_le_bytes()); } - let valid_wkb = geometry_to_wkb(&valid); - assert_eq!(decode(&valid_wkb), Some(valid)); + let total = wkb.len() + 4096; + let mut cursor = 0usize; + for _ in 0..levels { + let count_at = cursor + HEADER_BYTES; + let after = count_at + COUNT_BYTES; + let count = (total - after) / HEADER_BYTES; + wkb[count_at..after].copy_from_slice(&(count as u32).to_le_bytes()); + cursor = after; + } + wkb.resize(total, 0); + assert_eq!( + decode(&wkb), + Err(WkbError::Truncated { + offset: 2 * HEADER_BYTES + COUNT_BYTES + }) + ); + } + #[test] + fn collection_depth_is_bounded() { let mut too_deep = Geometry::point(1.0, 2.0); for _ in 0..MAX_WKB_DEPTH { too_deep = Geometry::GeometryCollection { geometries: vec![too_deep], }; } - assert!(encode(&too_deep).is_none()); - } - - #[test] - fn truncated_data_returns_none() { - let wkb = geometry_to_wkb(&Geometry::point(1.0, 2.0)); - assert!(decode(&wkb[..3]).is_none()); - assert!(decode(&[]).is_none()); - } - - #[test] - fn invalid_type_returns_none() { - let mut wkb = geometry_to_wkb(&Geometry::point(1.0, 2.0)); - wkb[1] = 99; // invalid WKB type - assert!(decode(&wkb).is_none()); + assert!(geometry_to_wkb(&xy(too_deep)).is_err()); } #[test] - fn ewkb_srid_flag_is_masked_and_reported() { - // EWKB POINT(1 2) SRID=32648, little-endian. + fn nan_inside_a_line_is_refused() { let mut wkb = Vec::new(); - write_header(&mut wkb, WKB_POINT | EWKB_SRID_FLAG); - write_u32(&mut wkb, 32648); - wkb.extend_from_slice(&1.0f64.to_le_bytes()); - wkb.extend_from_slice(&2.0f64.to_le_bytes()); - assert_eq!( - geometry_from_wkb(&wkb), - Some((Geometry::point(1.0, 2.0), Some(32648))) - ); + header(&mut wkb, 2); + wkb.extend_from_slice(&1u32.to_le_bytes()); + wkb.extend_from_slice(&f64::NAN.to_le_bytes()); + wkb.extend_from_slice(&0.0f64.to_le_bytes()); + assert!(matches!(decode(&wkb), Err(WkbError::NonFinite { .. }))); } #[test] - fn wkb_bbox_extraction() { - let geom = Geometry::polygon(vec![vec![ + fn bbox_of_wkb() { + let g = xy(Geometry::polygon(vec![vec![ [-10.0, -5.0], [10.0, -5.0], [10.0, 5.0], [-10.0, 5.0], [-10.0, -5.0], - ]]); - let wkb = geometry_to_wkb(&geom); - let bb = wkb_bbox(&wkb).unwrap(); - assert_eq!(bb.min_lng, -10.0); - assert_eq!(bb.max_lng, 10.0); - assert_eq!(bb.min_lat, -5.0); - assert_eq!(bb.max_lat, 5.0); + ]])); + let bb = wkb_bbox(&geometry_to_wkb(&g).unwrap()).unwrap(); + assert_eq!( + (bb.min_lng, bb.max_lng, bb.min_lat, bb.max_lat), + (-10.0, 10.0, -5.0, 5.0) + ); } } diff --git a/nodedb-spatial/src/wkb/write.rs b/nodedb-spatial/src/wkb/write.rs index 4c188102f..10fe46123 100644 --- a/nodedb-spatial/src/wkb/write.rs +++ b/nodedb-spatial/src/wkb/write.rs @@ -1,226 +1,323 @@ // SPDX-License-Identifier: Apache-2.0 -//! ISO WKB writer (little-endian, no SRID). +//! WKB writers: ISO WKB and PostGIS EWKB, little-endian. +//! +//! Both writers carry Z and M. ISO WKB adds 1000 / 2000 / 3000 to the type +//! code. EWKB sets the Z / M flags on every type word and puts the SRID in +//! the top-level header only, as PostGIS writes it. An EMPTY point writes +//! NaN for every ordinate. Output is little-endian unless a caller asks +//! for big-endian. -use nodedb_types::geometry::Geometry; +use nodedb_types::geometry::{Dimension, Geometry, GeometryKind, Ordinates, SridGeometry}; +use super::error::WkbError; use super::format::{ - BYTE_ORDER_LE, MAX_WKB_BYTES, MAX_WKB_DEPTH, MAX_WKB_ELEMENTS, WKB_GEOMETRYCOLLECTION, - WKB_LINESTRING, WKB_MULTILINESTRING, WKB_MULTIPOINT, WKB_MULTIPOLYGON, WKB_POINT, WKB_POLYGON, + BYTE_ORDER_BE, BYTE_ORDER_LE, EWKB_SRID_FLAG, MAX_WKB_BYTES, MAX_WKB_DEPTH, MAX_WKB_ELEMENTS, + ewkb_type_word, iso_type_word, }; -/// Serialize a Geometry to ISO WKB (little-endian). Carries no SRID. +/// WKB byte order. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum ByteOrder { + /// NDR, marker 1. + #[default] + LittleEndian, + /// XDR, marker 0. + BigEndian, +} + +/// Serialize to little-endian ISO WKB. Carries Z and M, no SRID. /// -/// Returns `None` when the geometry exceeds the decoder's element or nesting -/// limits, ensuring every emitted WKB value remains decodable. -pub fn geometry_to_wkb(geom: &Geometry) -> Option> { - let encoded_size = checked_encoded_size(geom, MAX_WKB_DEPTH)?; - let mut buf = Vec::with_capacity(encoded_size); - write_geometry(&mut buf, geom, MAX_WKB_DEPTH)?; - debug_assert_eq!(buf.len(), encoded_size); - Some(buf) +/// Fails when the geometry exceeds the decoder's element, size or nesting +/// limits, so every emitted value stays decodable. +pub fn geometry_to_wkb(g: &SridGeometry) -> Result, WkbError> { + encode(g, Convention::Iso, ByteOrder::LittleEndian) +} + +/// [`geometry_to_wkb`] in `order`. +pub fn geometry_to_wkb_ordered(g: &SridGeometry, order: ByteOrder) -> Result, WkbError> { + encode(g, Convention::Iso, order) +} + +/// The type-word convention of a writer. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) enum Convention { + Iso, + /// EWKB with the SRID in the top-level header. + Ewkb { + srid: u32, + }, +} + +pub(super) fn encode( + g: &SridGeometry, + convention: Convention, + order: ByteOrder, +) -> Result, WkbError> { + let dimension = g.dimension(); + let stride = 8 * usize::from(dimension.coord_dim()); + let srid_bytes = match convention { + Convention::Iso => 0, + Convention::Ewkb { .. } => 4, + }; + let size = encoded_size(g.geometry(), stride, MAX_WKB_DEPTH)? + .checked_add(srid_bytes) + .filter(|s| *s <= MAX_WKB_BYTES) + .ok_or(WkbError::TooLarge { max: MAX_WKB_BYTES })?; + let mut writer = Writer { + buf: Vec::with_capacity(size), + ordinates: g.ordinates(), + dimension, + convention, + order, + index: 0, + }; + writer.geometry(g.geometry(), true); + debug_assert_eq!(writer.buf.len(), size); + Ok(writer.buf) } -pub(super) fn checked_encoded_size(geom: &Geometry, depth_remaining: usize) -> Option { - if depth_remaining == 0 { - return None; +/// Encoded size of `geom` whose coordinates take `stride` bytes each. +fn encoded_size(geom: &Geometry, stride: usize, depth: usize) -> Result { + if depth == 0 { + return Err(WkbError::TooDeep { max: MAX_WKB_DEPTH }); } + let path = |len: usize| -> Result { + checked_count(len)?; + add(4, mul(len, stride)?) + }; let size = match geom { - Geometry::Point { .. } => 21, - Geometry::LineString { coordinates } => { - checked_count(coordinates.len())?; - bounded_add(9, coordinates.len().checked_mul(16)?)? - } + Geometry::Point { .. } => 5 + stride, + Geometry::LineString { coordinates } => add(5, path(coordinates.len())?)?, Geometry::Polygon { coordinates } => { checked_count(coordinates.len())?; - let mut size = 9usize; - for ring in coordinates { - checked_count(ring.len())?; - size = bounded_add(size, bounded_add(4, ring.len().checked_mul(16)?)?)?; - } - size + coordinates + .iter() + .try_fold(9usize, |acc, ring| add(acc, path(ring.len())?))? } Geometry::MultiPoint { coordinates } => { checked_count(coordinates.len())?; - bounded_add(9, coordinates.len().checked_mul(21)?)? + add(9, mul(coordinates.len(), 5 + stride)?)? } Geometry::MultiLineString { coordinates } => { - if depth_remaining == 1 { - return None; + if depth == 1 { + return Err(WkbError::TooDeep { max: MAX_WKB_DEPTH }); } checked_count(coordinates.len())?; - let mut size = 9usize; - for line in coordinates { - checked_count(line.len())?; - size = bounded_add(size, bounded_add(9, line.len().checked_mul(16)?)?)?; - } - size + coordinates + .iter() + .try_fold(9usize, |acc, line| add(acc, add(5, path(line.len())?)?))? } Geometry::MultiPolygon { coordinates } => { - if depth_remaining == 1 { - return None; + if depth == 1 { + return Err(WkbError::TooDeep { max: MAX_WKB_DEPTH }); } checked_count(coordinates.len())?; - let mut size = 9usize; - for polygon in coordinates { + coordinates.iter().try_fold(9usize, |acc, polygon| { checked_count(polygon.len())?; - let mut polygon_size = 9usize; - for ring in polygon { - checked_count(ring.len())?; - polygon_size = - bounded_add(polygon_size, bounded_add(4, ring.len().checked_mul(16)?)?)?; - } - size = bounded_add(size, polygon_size)?; - } - size + let rings = polygon + .iter() + .try_fold(9usize, |acc, ring| add(acc, path(ring.len())?))?; + add(acc, rings) + })? } Geometry::GeometryCollection { geometries } => { checked_count(geometries.len())?; - let mut size = 9usize; - for geometry in geometries { - size = bounded_add(size, checked_encoded_size(geometry, depth_remaining - 1)?)?; - } - size + geometries.iter().try_fold(9usize, |acc, member| { + add(acc, encoded_size(member, stride, depth - 1)?) + })? } - _ => 9, }; - (size <= MAX_WKB_BYTES).then_some(size) + if size > MAX_WKB_BYTES { + return Err(WkbError::TooLarge { max: MAX_WKB_BYTES }); + } + Ok(size) } -fn checked_count(count: usize) -> Option<()> { - (count <= MAX_WKB_ELEMENTS && u32::try_from(count).is_ok()).then_some(()) +fn checked_count(count: usize) -> Result<(), WkbError> { + if count > MAX_WKB_ELEMENTS || u32::try_from(count).is_err() { + return Err(WkbError::TooManyElements { + count, + max: MAX_WKB_ELEMENTS, + }); + } + Ok(()) } -fn bounded_add(left: usize, right: usize) -> Option { +fn add(left: usize, right: usize) -> Result { left.checked_add(right) - .filter(|&size| size <= MAX_WKB_BYTES) + .filter(|size| *size <= MAX_WKB_BYTES) + .ok_or(WkbError::TooLarge { max: MAX_WKB_BYTES }) } -fn write_geometry(buf: &mut Vec, geom: &Geometry, depth_remaining: usize) -> Option<()> { - if depth_remaining == 0 { - return None; - } - match geom { - Geometry::Point { coordinates } => { - write_header(buf, WKB_POINT); - write_f64(buf, coordinates[0]); - write_f64(buf, coordinates[1]); - } - Geometry::LineString { coordinates } => { - write_header(buf, WKB_LINESTRING); - write_count(buf, coordinates.len())?; - for c in coordinates { - write_f64(buf, c[0]); - write_f64(buf, c[1]); +fn mul(left: usize, right: usize) -> Result { + left.checked_mul(right) + .filter(|size| *size <= MAX_WKB_BYTES) + .ok_or(WkbError::TooLarge { max: MAX_WKB_BYTES }) +} + +struct Writer<'a> { + buf: Vec, + ordinates: &'a Ordinates, + dimension: Dimension, + convention: Convention, + order: ByteOrder, + /// Index of the next coordinate in traversal order. + index: usize, +} + +impl Writer<'_> { + fn header(&mut self, kind: GeometryKind, top: bool) { + self.buf.push(match self.order { + ByteOrder::LittleEndian => BYTE_ORDER_LE, + ByteOrder::BigEndian => BYTE_ORDER_BE, + }); + match self.convention { + Convention::Iso => self.u32(iso_type_word(kind, self.dimension)), + Convention::Ewkb { srid } if top => { + self.u32(ewkb_type_word(kind, self.dimension) | EWKB_SRID_FLAG); + self.u32(srid); } + Convention::Ewkb { .. } => self.u32(ewkb_type_word(kind, self.dimension)), } - Geometry::Polygon { coordinates } => { - write_header(buf, WKB_POLYGON); - write_count(buf, coordinates.len())?; - for ring in coordinates { - write_count(buf, ring.len())?; - for c in ring { - write_f64(buf, c[0]); - write_f64(buf, c[1]); + } + + /// `encoded_size` runs first and bounds every count to + /// `MAX_WKB_ELEMENTS`, so the count fits a `u32`. + fn count(&mut self, count: usize) { + self.u32(count as u32); + } + + fn geometry(&mut self, geom: &Geometry, top: bool) { + self.header(geom.kind(), top); + match geom { + Geometry::Point { .. } if geom.is_empty() => { + for _ in 0..self.dimension.coord_dim() { + self.f64(f64::NAN); } } - } - Geometry::MultiPoint { coordinates } => { - write_header(buf, WKB_MULTIPOINT); - write_count(buf, coordinates.len())?; - for c in coordinates { - write_header(buf, WKB_POINT); - write_f64(buf, c[0]); - write_f64(buf, c[1]); + Geometry::Point { coordinates } => self.coord(*coordinates), + Geometry::LineString { coordinates } => self.path(coordinates), + Geometry::Polygon { coordinates } => self.rings(coordinates), + Geometry::MultiPoint { coordinates } => { + self.count(coordinates.len()); + for c in coordinates { + self.header(GeometryKind::Point, false); + self.coord(*c); + } } - } - Geometry::MultiLineString { coordinates } => { - write_header(buf, WKB_MULTILINESTRING); - write_count(buf, coordinates.len())?; - for ls in coordinates { - write_geometry( - buf, - &Geometry::LineString { - coordinates: ls.clone(), - }, - depth_remaining - 1, - )?; + Geometry::MultiLineString { coordinates } => { + self.count(coordinates.len()); + for line in coordinates { + self.header(GeometryKind::LineString, false); + self.path(line); + } } - } - Geometry::MultiPolygon { coordinates } => { - write_header(buf, WKB_MULTIPOLYGON); - write_count(buf, coordinates.len())?; - for poly in coordinates { - write_geometry( - buf, - &Geometry::Polygon { - coordinates: poly.clone(), - }, - depth_remaining - 1, - )?; + Geometry::MultiPolygon { coordinates } => { + self.count(coordinates.len()); + for polygon in coordinates { + self.header(GeometryKind::Polygon, false); + self.rings(polygon); + } } - } - Geometry::GeometryCollection { geometries } => { - write_header(buf, WKB_GEOMETRYCOLLECTION); - write_count(buf, geometries.len())?; - for geometry in geometries { - write_geometry(buf, geometry, depth_remaining - 1)?; + Geometry::GeometryCollection { geometries } => { + self.count(geometries.len()); + for member in geometries { + self.geometry(member, false); + } } } - _ => { - write_header(buf, WKB_GEOMETRYCOLLECTION); - write_u32(buf, 0); + } + + fn rings(&mut self, rings: &[Vec<[f64; 2]>]) { + self.count(rings.len()); + for ring in rings { + self.path(ring); } } - Some(()) -} -fn write_count(buf: &mut Vec, count: usize) -> Option<()> { - if count > MAX_WKB_ELEMENTS { - return None; + fn path(&mut self, coords: &[[f64; 2]]) { + self.count(coords.len()); + for c in coords { + self.coord(*c); + } } - write_u32(buf, u32::try_from(count).ok()?); - Some(()) -} -pub(super) fn write_header(buf: &mut Vec, wkb_type: u32) { - buf.push(BYTE_ORDER_LE); - write_u32(buf, wkb_type); -} + fn coord(&mut self, c: [f64; 2]) { + let (z, m) = self.ordinates.at(self.index); + self.index += 1; + self.f64(c[0]); + self.f64(c[1]); + if let Some(z) = z { + self.f64(z); + } + if let Some(m) = m { + self.f64(m); + } + } -pub(super) fn write_u32(buf: &mut Vec, val: u32) { - buf.extend_from_slice(&val.to_le_bytes()); -} + fn u32(&mut self, v: u32) { + match self.order { + ByteOrder::LittleEndian => self.buf.extend_from_slice(&v.to_le_bytes()), + ByteOrder::BigEndian => self.buf.extend_from_slice(&v.to_be_bytes()), + } + } -fn write_f64(buf: &mut Vec, val: f64) { - buf.extend_from_slice(&val.to_le_bytes()); + fn f64(&mut self, v: f64) { + match self.order { + ByteOrder::LittleEndian => self.buf.extend_from_slice(&v.to_le_bytes()), + ByteOrder::BigEndian => self.buf.extend_from_slice(&v.to_be_bytes()), + } + } } #[cfg(test)] mod tests { use super::*; + use nodedb_types::geometry::Srid; + + fn xy(g: Geometry) -> SridGeometry { + SridGeometry::new(Srid::WGS84, g) + } #[test] - fn encoder_rejects_total_output_above_byte_limit_before_writing() { - let geometry = Geometry::MultiLineString { + fn encoder_rejects_total_output_above_byte_limit() { + let g = xy(Geometry::MultiLineString { coordinates: (0..5).map(|_| vec![[0.0, 0.0]; 840_000]).collect(), - }; - assert!(checked_encoded_size(&geometry, MAX_WKB_DEPTH).is_none()); - assert!(geometry_to_wkb(&geometry).is_none()); + }); + assert!(matches!( + geometry_to_wkb(&g), + Err(WkbError::TooLarge { .. }) + )); } #[test] fn encoder_rejects_element_count_above_decoder_limit() { - let geometry = Geometry::LineString { + let g = xy(Geometry::LineString { coordinates: vec![[0.0, 0.0]; MAX_WKB_ELEMENTS + 1], - }; - assert!(geometry_to_wkb(&geometry).is_none()); + }); + assert!(geometry_to_wkb(&g).is_err()); + } + + #[test] + fn big_endian_round_trips() { + use super::super::read::geometry_from_wkb; + let g = xy(Geometry::line_string(vec![[1.0, 2.0], [3.0, 4.0]])); + let be = geometry_to_wkb_ordered(&g, ByteOrder::BigEndian).unwrap(); + assert_eq!(be[0], BYTE_ORDER_BE); + assert_eq!(geometry_from_wkb(&be).map(|w| w.geometry), Ok(g)); } #[test] - fn point_wkb_size() { - let wkb = geometry_to_wkb(&Geometry::point(0.0, 0.0)).unwrap(); - // 1 (byte order) + 4 (type) + 8 (x) + 8 (y) = 21 bytes - assert_eq!(wkb.len(), 21); + fn point_sizes() { + assert_eq!( + geometry_to_wkb(&xy(Geometry::point(0.0, 0.0))) + .unwrap() + .len(), + 21 + ); + let empty = geometry_to_wkb(&xy(Geometry::empty_point())).unwrap(); + assert_eq!(empty.len(), 21); + assert!(f64::from_le_bytes(empty[5..13].try_into().unwrap()).is_nan()); } } diff --git a/nodedb-spatial/src/wkt.rs b/nodedb-spatial/src/wkt.rs deleted file mode 100644 index 4cd943847..000000000 --- a/nodedb-spatial/src/wkt.rs +++ /dev/null @@ -1,451 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -//! Well-Known Text (WKT) serialization and parsing for Geometry types. -//! -//! Standard interchange format used by PostGIS, ArcadeDB (Spatial4J), -//! and most GIS tools. Format examples: -//! - `POINT(lng lat)` -//! - `LINESTRING(lng1 lat1, lng2 lat2)` -//! - `POLYGON((lng1 lat1, lng2 lat2, ...), (hole1...))` -//! - `MULTIPOINT((lng1 lat1), (lng2 lat2))` -//! - `GEOMETRYCOLLECTION(POINT(...), LINESTRING(...))` - -use nodedb_types::geometry::Geometry; - -/// Serialize a Geometry to WKT string. -pub fn geometry_to_wkt(geom: &Geometry) -> String { - match geom { - Geometry::Point { coordinates } => { - format!("POINT({} {})", coordinates[0], coordinates[1]) - } - Geometry::LineString { coordinates } => { - format!("LINESTRING({})", coords_to_wkt(coordinates)) - } - Geometry::Polygon { coordinates } => { - let rings: Vec = coordinates - .iter() - .map(|ring| format!("({})", coords_to_wkt(ring))) - .collect(); - format!("POLYGON({})", rings.join(", ")) - } - Geometry::MultiPoint { coordinates } => { - let pts: Vec = coordinates - .iter() - .map(|c| format!("({} {})", c[0], c[1])) - .collect(); - format!("MULTIPOINT({})", pts.join(", ")) - } - Geometry::MultiLineString { coordinates } => { - let lines: Vec = coordinates - .iter() - .map(|ls| format!("({})", coords_to_wkt(ls))) - .collect(); - format!("MULTILINESTRING({})", lines.join(", ")) - } - Geometry::MultiPolygon { coordinates } => { - let polys: Vec = coordinates - .iter() - .map(|poly| { - let rings: Vec = poly - .iter() - .map(|ring| format!("({})", coords_to_wkt(ring))) - .collect(); - format!("({})", rings.join(", ")) - }) - .collect(); - format!("MULTIPOLYGON({})", polys.join(", ")) - } - Geometry::GeometryCollection { geometries } => { - let geoms: Vec = geometries.iter().map(geometry_to_wkt).collect(); - format!("GEOMETRYCOLLECTION({})", geoms.join(", ")) - } - - // Unknown future geometry type — emit empty geometry collection. - _ => "GEOMETRYCOLLECTION EMPTY".to_string(), - } -} - -/// Parse a WKT string into a Geometry. -/// -/// Returns `None` if the input is malformed. -pub fn geometry_from_wkt(input: &str) -> Option { - let s = input.trim(); - if let Some(rest) = strip_prefix_ci(s, "GEOMETRYCOLLECTION") { - parse_geometry_collection(rest.trim()) - } else if let Some(rest) = strip_prefix_ci(s, "MULTIPOLYGON") { - parse_multipolygon(rest.trim()) - } else if let Some(rest) = strip_prefix_ci(s, "MULTILINESTRING") { - parse_multilinestring(rest.trim()) - } else if let Some(rest) = strip_prefix_ci(s, "MULTIPOINT") { - parse_multipoint(rest.trim()) - } else if let Some(rest) = strip_prefix_ci(s, "POLYGON") { - parse_polygon(rest.trim()) - } else if let Some(rest) = strip_prefix_ci(s, "LINESTRING") { - parse_linestring(rest.trim()) - } else if let Some(rest) = strip_prefix_ci(s, "POINT") { - parse_point(rest.trim()) - } else { - None - } -} - -/// EWKT text split into its optional SRID code and its geometry. -#[derive(Debug, Clone, PartialEq)] -pub struct Ewkt { - /// The code after `SRID=`, unresolved. `None` when the text has no prefix. - pub srid: Option, - pub geometry: Geometry, -} - -/// Parse EWKT: an optional `SRID=;` prefix, then WKT. -/// -/// The prefix matches case-insensitively. The code is ASCII digits only. -/// Returns `None` for a malformed prefix or malformed WKT. -pub fn geometry_from_ewkt(input: &str) -> Option { - let s = input.trim_start(); - let Some(rest) = strip_prefix_ci(s, "SRID=") else { - return geometry_from_wkt(s).map(|geometry| Ewkt { - srid: None, - geometry, - }); - }; - let (code, wkt) = rest.split_once(';')?; - let code = code.trim(); - if code.is_empty() || !code.bytes().all(|b| b.is_ascii_digit()) { - return None; - } - let srid = code.parse::().ok()?; - let geometry = geometry_from_wkt(wkt)?; - Some(Ewkt { - srid: Some(srid), - geometry, - }) -} - -// ── Serialization helpers ── - -fn coords_to_wkt(coords: &[[f64; 2]]) -> String { - coords - .iter() - .map(|c| format!("{} {}", c[0], c[1])) - .collect::>() - .join(", ") -} - -// ── Parsing helpers ── - -/// Case-insensitive prefix strip of an ASCII `prefix`. -/// -/// Compares bytes, so a multi-byte character at the cut point never panics. -fn strip_prefix_ci<'a>(s: &'a str, prefix: &str) -> Option<&'a str> { - let head = s.as_bytes().get(..prefix.len())?; - if head.eq_ignore_ascii_case(prefix.as_bytes()) { - s.get(prefix.len()..) - } else { - None - } -} - -/// Strip outer parentheses: "(content)" → "content" -fn strip_parens(s: &str) -> Option<&str> { - let s = s.trim(); - if s.starts_with('(') && s.ends_with(')') { - Some(&s[1..s.len() - 1]) - } else { - None - } -} - -/// Parse "lng lat" pair. -fn parse_coord(s: &str) -> Option<[f64; 2]> { - let s = s.trim(); - let mut parts = s.split_whitespace(); - let lng: f64 = parts.next()?.parse().ok()?; - let lat: f64 = parts.next()?.parse().ok()?; - Some([lng, lat]) -} - -/// Parse "lng1 lat1, lng2 lat2, ..." into coordinate vec. -fn parse_coord_list(s: &str) -> Option> { - s.split(',').map(parse_coord).collect() -} - -fn parse_point(s: &str) -> Option { - let inner = strip_parens(s)?; - let coord = parse_coord(inner)?; - Some(Geometry::Point { coordinates: coord }) -} - -fn parse_linestring(s: &str) -> Option { - let inner = strip_parens(s)?; - let coords = parse_coord_list(inner)?; - Some(Geometry::LineString { - coordinates: coords, - }) -} - -fn parse_polygon(s: &str) -> Option { - let inner = strip_parens(s)?; - let rings = split_top_level_parens(inner)?; - let ring_coords: Option>> = rings - .iter() - .map(|r| parse_coord_list(strip_parens(r.trim())?)) - .collect(); - Some(Geometry::Polygon { - coordinates: ring_coords?, - }) -} - -fn parse_multipoint(s: &str) -> Option { - let inner = strip_parens(s)?; - // MULTIPOINT((x y), (x y)) or MULTIPOINT(x y, x y) - let coords = if inner.contains('(') { - let parts = split_top_level_parens(inner)?; - parts - .iter() - .map(|p| parse_coord(strip_parens(p.trim())?)) - .collect::>>()? - } else { - parse_coord_list(inner)? - }; - Some(Geometry::MultiPoint { - coordinates: coords, - }) -} - -fn parse_multilinestring(s: &str) -> Option { - let inner = strip_parens(s)?; - let parts = split_top_level_parens(inner)?; - let lines: Option>> = parts - .iter() - .map(|p| parse_coord_list(strip_parens(p.trim())?)) - .collect(); - Some(Geometry::MultiLineString { - coordinates: lines?, - }) -} - -fn parse_multipolygon(s: &str) -> Option { - let inner = strip_parens(s)?; - let poly_parts = split_top_level_parens(inner)?; - let polys: Option>>> = poly_parts - .iter() - .map(|p| { - let rings_str = strip_parens(p.trim())?; - let ring_parts = split_top_level_parens(rings_str)?; - ring_parts - .iter() - .map(|r| parse_coord_list(strip_parens(r.trim())?)) - .collect::>>() - }) - .collect(); - Some(Geometry::MultiPolygon { - coordinates: polys?, - }) -} - -fn parse_geometry_collection(s: &str) -> Option { - let inner = strip_parens(s)?; - // Split by top-level commas (not inside parentheses). - let parts = split_top_level_items(inner); - let geoms: Option> = parts.iter().map(|p| geometry_from_wkt(p.trim())).collect(); - Some(Geometry::GeometryCollection { geometries: geoms? }) -} - -/// Split by commas at the top level of parentheses nesting. -/// "(...), (...)" → ["(...)", "(...)"] -fn split_top_level_parens(s: &str) -> Option> { - let mut parts = Vec::new(); - let mut depth = 0; - let mut start = 0; - - for (i, ch) in s.char_indices() { - match ch { - '(' => depth += 1, - ')' => depth -= 1, - ',' if depth == 0 => { - parts.push(s[start..i].to_string()); - start = i + 1; - } - _ => {} - } - } - if start < s.len() { - parts.push(s[start..].to_string()); - } - if parts.is_empty() { None } else { Some(parts) } -} - -/// Split geometry collection items by top-level commas, handling nested parens. -fn split_top_level_items(s: &str) -> Vec { - let mut parts = Vec::new(); - let mut depth = 0; - let mut start = 0; - - for (i, ch) in s.char_indices() { - match ch { - '(' => depth += 1, - ')' => depth -= 1, - ',' if depth == 0 => { - parts.push(s[start..i].to_string()); - start = i + 1; - } - _ => {} - } - } - if start < s.len() { - parts.push(s[start..].to_string()); - } - parts -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn point_roundtrip() { - let geom = Geometry::point(-73.9857, 40.7484); - let wkt = geometry_to_wkt(&geom); - assert!(wkt.starts_with("POINT(")); - let parsed = geometry_from_wkt(&wkt).unwrap(); - assert_eq!(geom, parsed); - } - - #[test] - fn linestring_roundtrip() { - let geom = Geometry::line_string(vec![[0.0, 0.0], [1.0, 1.0], [2.0, 0.0]]); - let wkt = geometry_to_wkt(&geom); - let parsed = geometry_from_wkt(&wkt).unwrap(); - assert_eq!(geom, parsed); - } - - #[test] - fn polygon_roundtrip() { - let geom = Geometry::polygon(vec![vec![ - [0.0, 0.0], - [10.0, 0.0], - [10.0, 10.0], - [0.0, 10.0], - [0.0, 0.0], - ]]); - let wkt = geometry_to_wkt(&geom); - assert!(wkt.starts_with("POLYGON(")); - let parsed = geometry_from_wkt(&wkt).unwrap(); - assert_eq!(geom, parsed); - } - - #[test] - fn polygon_with_hole_roundtrip() { - let geom = Geometry::polygon(vec![ - vec![ - [0.0, 0.0], - [10.0, 0.0], - [10.0, 10.0], - [0.0, 10.0], - [0.0, 0.0], - ], - vec![[2.0, 2.0], [8.0, 2.0], [8.0, 8.0], [2.0, 8.0], [2.0, 2.0]], - ]); - let wkt = geometry_to_wkt(&geom); - let parsed = geometry_from_wkt(&wkt).unwrap(); - assert_eq!(geom, parsed); - } - - #[test] - fn multipoint_roundtrip() { - let geom = Geometry::MultiPoint { - coordinates: vec![[1.0, 2.0], [3.0, 4.0]], - }; - let wkt = geometry_to_wkt(&geom); - let parsed = geometry_from_wkt(&wkt).unwrap(); - assert_eq!(geom, parsed); - } - - #[test] - fn multilinestring_roundtrip() { - let geom = Geometry::MultiLineString { - coordinates: vec![vec![[0.0, 0.0], [1.0, 1.0]], vec![[2.0, 2.0], [3.0, 3.0]]], - }; - let wkt = geometry_to_wkt(&geom); - let parsed = geometry_from_wkt(&wkt).unwrap(); - assert_eq!(geom, parsed); - } - - #[test] - fn multipolygon_roundtrip() { - let geom = Geometry::MultiPolygon { - coordinates: vec![ - vec![vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 0.0]]], - vec![vec![[5.0, 5.0], [6.0, 5.0], [6.0, 6.0], [5.0, 5.0]]], - ], - }; - let wkt = geometry_to_wkt(&geom); - let parsed = geometry_from_wkt(&wkt).unwrap(); - assert_eq!(geom, parsed); - } - - #[test] - fn geometry_collection_roundtrip() { - let geom = Geometry::GeometryCollection { - geometries: vec![ - Geometry::point(1.0, 2.0), - Geometry::line_string(vec![[0.0, 0.0], [1.0, 1.0]]), - ], - }; - let wkt = geometry_to_wkt(&geom); - assert!(wkt.starts_with("GEOMETRYCOLLECTION(")); - let parsed = geometry_from_wkt(&wkt).unwrap(); - assert_eq!(geom, parsed); - } - - #[test] - fn case_insensitive_parse() { - let parsed = geometry_from_wkt("point(5 10)").unwrap(); - assert_eq!(parsed, Geometry::point(5.0, 10.0)); - } - - #[test] - fn ewkt_prefix_is_case_insensitive() { - let square = Geometry::polygon(vec![vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 0.0]]]); - for text in [ - "SRID=4326;POLYGON((0 0, 1 0, 1 1, 0 0))", - "srid=4326;polygon((0 0, 1 0, 1 1, 0 0))", - " SrId= 4326 ;POLYGON((0 0, 1 0, 1 1, 0 0))", - ] { - let parsed = geometry_from_ewkt(text).unwrap(); - assert_eq!(parsed.srid, Some(4326), "{text}"); - assert_eq!(parsed.geometry, square, "{text}"); - } - } - - #[test] - fn ewkt_without_prefix_has_no_srid() { - let parsed = geometry_from_ewkt("POINT(5 10)").unwrap(); - assert_eq!(parsed.srid, None); - assert_eq!(parsed.geometry, Geometry::point(5.0, 10.0)); - } - - #[test] - fn malformed_ewkt_returns_none() { - assert!(geometry_from_ewkt("SRID=;POINT(1 2)").is_none()); - assert!(geometry_from_ewkt("SRID=43x6;POINT(1 2)").is_none()); - assert!(geometry_from_ewkt("SRID=-4326;POINT(1 2)").is_none()); - assert!(geometry_from_ewkt("SRID=4326 POINT(1 2)").is_none()); - assert!(geometry_from_ewkt("SRID=4326;GARBAGE").is_none()); - } - - #[test] - fn a_multibyte_character_at_the_prefix_cut_does_not_panic() { - assert!(geometry_from_ewkt("SRÍD=4326;POINT(1 2)").is_none()); - assert!(geometry_from_wkt("POIÑT(1 2)").is_none()); - assert!(geometry_from_wkt("é").is_none()); - } - - #[test] - fn invalid_wkt_returns_none() { - assert!(geometry_from_wkt("").is_none()); - assert!(geometry_from_wkt("GARBAGE(1 2)").is_none()); - assert!(geometry_from_wkt("POINT(abc def)").is_none()); - } -} diff --git a/nodedb-spatial/src/wkt/error.rs b/nodedb-spatial/src/wkt/error.rs new file mode 100644 index 000000000..2f4a143c8 --- /dev/null +++ b/nodedb-spatial/src/wkt/error.rs @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! WKT parse errors. + +use nodedb_types::geometry::{Dimension, OrdinateError}; + +/// Why a WKT or EWKT text does not parse. +#[derive(Debug, Clone, PartialEq, thiserror::Error)] +#[error("WKT parse error at offset {offset}: {kind}")] +pub struct WktError { + /// Byte offset into the input where the error was found. + pub offset: usize, + pub kind: WktErrorKind, +} + +/// The cause of a [`WktError`]. +#[derive(Debug, Clone, PartialEq, thiserror::Error)] +pub enum WktErrorKind { + #[error("unexpected end of input, expected {expected}")] + UnexpectedEnd { expected: &'static str }, + #[error("found '{found}', expected {expected}")] + UnexpectedToken { + found: Box, + expected: &'static str, + }, + #[error("unknown geometry type '{name}'")] + UnknownType { name: Box }, + #[error("'{text}' is not a finite number")] + BadNumber { text: Box }, + #[error("coordinate has {found} numbers, {dimension} takes {expected}")] + CoordinateCount { + found: usize, + expected: u8, + dimension: Dimension, + }, + #[error("coordinate has {found} numbers; WKT takes 2, 3 or 4")] + UntaggedCount { found: usize }, + #[error("collection member is {member}, the collection is {parent}")] + MemberDimension { + parent: Dimension, + member: Dimension, + }, + #[error("{0}")] + Ordinates(#[from] OrdinateError), + #[error("EMPTY members inside {kind} are not supported")] + EmptyMember { kind: &'static str }, + #[error("GEOMETRYCOLLECTION nesting exceeds {max} levels")] + TooDeep { max: usize }, + #[error("SRID prefix '{text}' is not 'SRID=;'")] + BadSridPrefix { text: Box }, + #[error("trailing input '{text}'")] + Trailing { text: Box }, +} diff --git a/nodedb-spatial/src/wkt/mod.rs b/nodedb-spatial/src/wkt/mod.rs new file mode 100644 index 000000000..354f86107 --- /dev/null +++ b/nodedb-spatial/src/wkt/mod.rs @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Well-Known Text (WKT) and PostGIS EWKT, with Z / M / ZM and EMPTY. +//! +//! - `POINT(x y)`, `POINT Z (x y z)`, `POINTM(x y m)`, `POINT ZM (x y z m)` +//! - `LINESTRING(x1 y1, x2 y2)`, `POLYGON((...), (hole...))` +//! - `MULTIPOINT((x y), (x y))` or `MULTIPOINT(x y, x y)` +//! - `GEOMETRYCOLLECTION(POINT(...), LINESTRING(...))` +//! - `POINT EMPTY`, `GEOMETRYCOLLECTION EMPTY` + +pub mod error; +pub mod read; +pub mod write; + +pub use error::{WktError, WktErrorKind}; +pub use read::{Ewkt, geometry_from_ewkt, geometry_from_wkt}; +pub use write::{geometry_to_ewkt, geometry_to_wkt, shape_to_wkt}; diff --git a/nodedb-spatial/src/wkt/read.rs b/nodedb-spatial/src/wkt/read.rs new file mode 100644 index 000000000..d8fe1ffb8 --- /dev/null +++ b/nodedb-spatial/src/wkt/read.rs @@ -0,0 +1,724 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! WKT and EWKT reader. +//! +//! A type keyword takes an optional dimension tag, spaced (`POINT Z`) or +//! attached (`POINTZ`): `Z`, `M` or `ZM`. With a tag every coordinate holds +//! exactly the numbers the tag names. Without one, 2 numbers are XY, 3 are +//! XYZ and 4 are XYZM, and one geometry never mixes counts. `EMPTY` replaces +//! the parenthesised body of any type. + +use nodedb_types::geometry::{ + Dimension, Geometry, GeometryKind, OrdinatesBuilder, Srid, SridGeometry, +}; + +use super::error::{WktError, WktErrorKind}; + +/// Nesting limit for `GEOMETRYCOLLECTION`, the same bound the WKB reader +/// keeps. +const MAX_WKT_DEPTH: usize = 64; + +/// EWKT text split into its optional SRID code and its geometry. +#[derive(Debug, Clone, PartialEq)] +pub struct Ewkt { + /// The code after `SRID=`, unresolved. `None` when the text has no + /// prefix. + pub srid: Option, + /// The geometry, tagged WGS 84. The caller retags it once `srid` + /// resolves. + pub geometry: SridGeometry, +} + +/// Parse WKT. WKT carries no SRID, so the result is tagged WGS 84. +pub fn geometry_from_wkt(input: &str) -> Result { + let mut parser = Parser::new(input, 0); + let geometry = parser.document()?; + Ok(geometry) +} + +/// Parse EWKT: an optional `SRID=;` prefix, then WKT. +/// +/// The prefix matches case-insensitively. The code is ASCII digits only. +pub fn geometry_from_ewkt(input: &str) -> Result { + let leading = input.len() - input.trim_start().len(); + let s = &input[leading..]; + let Some(rest) = strip_prefix_ci(s, "SRID=") else { + return geometry_from_wkt(input).map(|geometry| Ewkt { + srid: None, + geometry, + }); + }; + let bad_prefix = || WktError { + offset: leading, + kind: WktErrorKind::BadSridPrefix { + text: s.chars().take(24).collect::().into(), + }, + }; + let (code, wkt) = rest.split_once(';').ok_or_else(bad_prefix)?; + let digits = code.trim(); + if digits.is_empty() || !digits.bytes().all(|b| b.is_ascii_digit()) { + return Err(bad_prefix()); + } + let srid = digits.parse::().map_err(|_| bad_prefix())?; + let wkt_offset = input.len() - wkt.len(); + let geometry = Parser::new(wkt, wkt_offset).document()?; + Ok(Ewkt { + srid: Some(srid), + geometry, + }) +} + +/// Case-insensitive prefix strip of an ASCII `prefix`. Compares bytes, so a +/// multi-byte character at the cut point never panics. +fn strip_prefix_ci<'a>(s: &'a str, prefix: &str) -> Option<&'a str> { + let head = s.as_bytes().get(..prefix.len())?; + if head.eq_ignore_ascii_case(prefix.as_bytes()) { + s.get(prefix.len()..) + } else { + None + } +} + +#[derive(Debug, Clone, Copy, PartialEq)] +enum Token<'a> { + Word(&'a str), + Number(&'a str), + Open, + Close, + Comma, +} + +impl Token<'_> { + fn text(&self) -> Box { + match self { + Token::Word(s) | Token::Number(s) => (*s).into(), + Token::Open => "(".into(), + Token::Close => ")".into(), + Token::Comma => ",".into(), + } + } +} + +struct Parser<'a> { + input: &'a str, + pos: usize, + /// Offset of `input` within the caller's text, for error reports. + base: usize, + ordinates: OrdinatesBuilder, +} + +impl<'a> Parser<'a> { + fn new(input: &'a str, base: usize) -> Self { + Self { + input, + pos: 0, + base, + ordinates: OrdinatesBuilder::default(), + } + } + + fn error(&self, offset: usize, kind: WktErrorKind) -> WktError { + WktError { + offset: self.base + offset, + kind, + } + } + + /// The whole input as one geometry. + fn document(&mut self) -> Result { + let geometry = self.geometry(None, MAX_WKT_DEPTH)?; + self.skip_space(); + if self.pos < self.input.len() { + let text = self.input[self.pos..].chars().take(24).collect::(); + return Err(self.error(self.pos, WktErrorKind::Trailing { text: text.into() })); + } + let ordinates = std::mem::take(&mut self.ordinates).finish(); + SridGeometry::with_ordinates(Srid::WGS84, geometry, ordinates) + .map_err(|e| self.error(0, e.into())) + } + + fn skip_space(&mut self) { + let rest = &self.input[self.pos..]; + self.pos += rest.len() - rest.trim_start().len(); + } + + /// The next token and its start offset, without consuming it. + fn peek(&mut self) -> Option<(Token<'a>, usize, usize)> { + self.skip_space(); + let start = self.pos; + let rest = &self.input[start..]; + let first = rest.chars().next()?; + let (token, len) = match first { + '(' => (Token::Open, 1), + ')' => (Token::Close, 1), + ',' => (Token::Comma, 1), + c if c.is_ascii_alphabetic() => { + let len = rest + .find(|c: char| !c.is_ascii_alphanumeric()) + .unwrap_or(rest.len()); + (Token::Word(&rest[..len]), len) + } + _ => { + let len = rest + .find(|c: char| c.is_whitespace() || matches!(c, '(' | ')' | ',')) + .unwrap_or(rest.len()); + (Token::Number(&rest[..len]), len.max(first.len_utf8())) + } + }; + Some((token, start, len)) + } + + fn next(&mut self, expected: &'static str) -> Result<(Token<'a>, usize), WktError> { + match self.peek() { + Some((token, start, len)) => { + self.pos = start + len; + Ok((token, start)) + } + None => Err(self.error(self.pos, WktErrorKind::UnexpectedEnd { expected })), + } + } + + fn expect(&mut self, want: Token<'static>, expected: &'static str) -> Result<(), WktError> { + let (token, start) = self.next(expected)?; + if token == want { + Ok(()) + } else { + Err(self.error( + start, + WktErrorKind::UnexpectedToken { + found: token.text(), + expected, + }, + )) + } + } + + /// `KIND [Z|M|ZM] (EMPTY | body)`. `inherited` is the enclosing + /// collection's declared dimension. + fn geometry( + &mut self, + inherited: Option, + depth: usize, + ) -> Result { + if depth == 0 { + return Err(self.error(self.pos, WktErrorKind::TooDeep { max: MAX_WKT_DEPTH })); + } + let (token, start) = self.next("a geometry type")?; + let Token::Word(word) = token else { + return Err(self.error( + start, + WktErrorKind::UnexpectedToken { + found: token.text(), + expected: "a geometry type", + }, + )); + }; + let (kind, mut tag) = + split_keyword(word).ok_or_else(|| self.error(start, unknown_type(word)))?; + if tag.is_none() + && let Some((Token::Word(next), at, len)) = self.peek() + && let Some(dimension) = dimension_tag(next) + { + self.pos = at + len; + tag = Some(dimension); + } + let declared = match (inherited, tag) { + (Some(parent), Some(member)) if parent != member => { + return Err(self.error(start, WktErrorKind::MemberDimension { parent, member })); + } + (parent, member) => member.or(parent), + }; + if let Some(dimension) = declared { + self.declare(dimension, start)?; + } + if let Some((Token::Word(w), at, len)) = self.peek() + && w.eq_ignore_ascii_case("EMPTY") + { + self.pos = at + len; + return Ok(Geometry::empty(kind)); + } + self.body(kind, declared, depth) + } + + /// Fix the dimension of the whole geometry. A declared tag that differs + /// from coordinates already read is a mixed-dimension error. + fn declare(&mut self, dimension: Dimension, at: usize) -> Result<(), WktError> { + match self.ordinates.dimension() { + None => { + self.ordinates = OrdinatesBuilder::with_dimension(dimension); + Ok(()) + } + Some(existing) if existing == dimension => Ok(()), + Some(existing) => Err(self.error( + at, + WktErrorKind::MemberDimension { + parent: existing, + member: dimension, + }, + )), + } + } + + fn body( + &mut self, + kind: GeometryKind, + declared: Option, + depth: usize, + ) -> Result { + Ok(match kind { + GeometryKind::Point => { + self.expect(Token::Open, "'('")?; + let c = self.coord(declared)?; + self.expect(Token::Close, "')'")?; + Geometry::Point { coordinates: c } + } + GeometryKind::LineString => Geometry::LineString { + coordinates: self.coord_list(declared)?, + }, + GeometryKind::Polygon => Geometry::Polygon { + coordinates: self.ring_list(declared)?, + }, + GeometryKind::MultiPoint => Geometry::MultiPoint { + coordinates: self.multipoint(declared)?, + }, + GeometryKind::MultiLineString => Geometry::MultiLineString { + coordinates: self.ring_list(declared)?, + }, + GeometryKind::MultiPolygon => { + self.expect(Token::Open, "'('")?; + let mut polygons = vec![self.member_rings(declared, "MULTIPOLYGON")?]; + while self.comma_or_close()? { + polygons.push(self.member_rings(declared, "MULTIPOLYGON")?); + } + Geometry::MultiPolygon { + coordinates: polygons, + } + } + GeometryKind::GeometryCollection => { + self.expect(Token::Open, "'('")?; + let mut geometries = vec![self.geometry(declared, depth - 1)?]; + while self.comma_or_close()? { + geometries.push(self.geometry(declared, depth - 1)?); + } + Geometry::GeometryCollection { geometries } + } + }) + } + + /// After a list item: `true` on `,`, `false` on `)`. + fn comma_or_close(&mut self) -> Result { + let (token, start) = self.next("',' or ')'")?; + match token { + Token::Comma => Ok(true), + Token::Close => Ok(false), + other => Err(self.error( + start, + WktErrorKind::UnexpectedToken { + found: other.text(), + expected: "',' or ')'", + }, + )), + } + } + + /// `(c, c, ...)`. + fn coord_list(&mut self, declared: Option) -> Result, WktError> { + self.expect(Token::Open, "'('")?; + let mut coords = vec![self.coord(declared)?]; + while self.comma_or_close()? { + coords.push(self.coord(declared)?); + } + Ok(coords) + } + + /// `((c, ...), (c, ...))`. + fn ring_list(&mut self, declared: Option) -> Result>, WktError> { + self.expect(Token::Open, "'('")?; + let mut rings = vec![self.coord_list(declared)?]; + while self.comma_or_close()? { + rings.push(self.coord_list(declared)?); + } + Ok(rings) + } + + /// One polygon of a MULTIPOLYGON. `EMPTY` here is refused. + fn member_rings( + &mut self, + declared: Option, + kind: &'static str, + ) -> Result>, WktError> { + self.refuse_empty_member(kind)?; + self.ring_list(declared) + } + + fn refuse_empty_member(&mut self, kind: &'static str) -> Result<(), WktError> { + if let Some((Token::Word(w), start, _)) = self.peek() + && w.eq_ignore_ascii_case("EMPTY") + { + return Err(self.error(start, WktErrorKind::EmptyMember { kind })); + } + Ok(()) + } + + /// `(c, c)` or `((c), (c))`. + fn multipoint(&mut self, declared: Option) -> Result, WktError> { + self.expect(Token::Open, "'('")?; + let mut points = Vec::new(); + loop { + self.refuse_empty_member("MULTIPOINT")?; + let wrapped = matches!(self.peek(), Some((Token::Open, _, _))); + if wrapped { + self.expect(Token::Open, "'('")?; + } + points.push(self.coord(declared)?); + if wrapped { + self.expect(Token::Close, "')'")?; + } + if !self.comma_or_close()? { + return Ok(points); + } + } + } + + /// One coordinate: 2 to 4 numbers. + fn coord(&mut self, declared: Option) -> Result<[f64; 2], WktError> { + let start = self.peek().map_or(self.pos, |(_, s, _)| s); + let mut numbers = Vec::with_capacity(4); + while let Some((Token::Number(text), at, len)) = self.peek() { + let value = text + .parse::() + .ok() + .filter(|v| v.is_finite()) + .ok_or_else(|| self.error(at, WktErrorKind::BadNumber { text: text.into() }))?; + numbers.push(value); + self.pos = at + len; + if numbers.len() > 4 { + break; + } + } + let dimension = match declared { + Some(d) => { + if numbers.len() != usize::from(d.coord_dim()) { + return Err(self.error( + start, + WktErrorKind::CoordinateCount { + found: numbers.len(), + expected: d.coord_dim(), + dimension: d, + }, + )); + } + d + } + None => match numbers.len() { + 2 => Dimension::Xy, + 3 => Dimension::Xyz, + 4 => Dimension::Xyzm, + found => { + return Err(self.error(start, WktErrorKind::UntaggedCount { found })); + } + }, + }; + let z = dimension.has_z().then(|| numbers[2]); + let m = dimension + .has_m() + .then(|| numbers[if dimension.has_z() { 3 } else { 2 }]); + self.ordinates + .push(z, m) + .map_err(|e| self.error(start, e.into()))?; + Ok([numbers[0], numbers[1]]) + } +} + +fn unknown_type(word: &str) -> WktErrorKind { + WktErrorKind::UnknownType { name: word.into() } +} + +/// Type keywords, longest first so `MULTIPOINT` never reads as `POINT`. +const KEYWORDS: [GeometryKind; 7] = [ + GeometryKind::GeometryCollection, + GeometryKind::MultiLineString, + GeometryKind::MultiPolygon, + GeometryKind::LineString, + GeometryKind::MultiPoint, + GeometryKind::Polygon, + GeometryKind::Point, +]; + +/// A type keyword with an optional attached dimension tag (`POINTZ`). +fn split_keyword(word: &str) -> Option<(GeometryKind, Option)> { + KEYWORDS.iter().find_map(|kind| { + let rest = strip_prefix_ci(word, kind.wkt_keyword())?; + if rest.is_empty() { + Some((*kind, None)) + } else { + dimension_tag(rest).map(|d| (*kind, Some(d))) + } + }) +} + +fn dimension_tag(word: &str) -> Option { + if word.eq_ignore_ascii_case("Z") { + Some(Dimension::Xyz) + } else if word.eq_ignore_ascii_case("M") { + Some(Dimension::Xym) + } else if word.eq_ignore_ascii_case("ZM") { + Some(Dimension::Xyzm) + } else { + None + } +} + +#[cfg(test)] +mod tests { + use super::super::write::{geometry_to_ewkt, geometry_to_wkt}; + use super::*; + use nodedb_types::geometry::{OrdinateError, Ordinates}; + + fn shape(text: &str) -> Geometry { + geometry_from_wkt(text).unwrap().geometry().clone() + } + + fn round_trip(g: SridGeometry) { + let text = geometry_to_wkt(&g); + assert_eq!(geometry_from_wkt(&text), Ok(g), "{text}"); + } + + #[test] + fn xy_shapes_round_trip() { + for g in [ + Geometry::point(-73.9857, 40.7484), + Geometry::line_string(vec![[0.0, 0.0], [1.0, 1.0], [2.0, 0.0]]), + Geometry::polygon(vec![ + vec![ + [0.0, 0.0], + [10.0, 0.0], + [10.0, 10.0], + [0.0, 10.0], + [0.0, 0.0], + ], + vec![[2.0, 2.0], [8.0, 2.0], [8.0, 8.0], [2.0, 8.0], [2.0, 2.0]], + ]), + Geometry::MultiPoint { + coordinates: vec![[1.0, 2.0], [3.0, 4.0]], + }, + Geometry::MultiLineString { + coordinates: vec![vec![[0.0, 0.0], [1.0, 1.0]], vec![[2.0, 2.0], [3.0, 3.0]]], + }, + Geometry::MultiPolygon { + coordinates: vec![ + vec![vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 0.0]]], + vec![vec![[5.0, 5.0], [6.0, 5.0], [6.0, 6.0], [5.0, 5.0]]], + ], + }, + Geometry::GeometryCollection { + geometries: vec![ + Geometry::point(1.0, 2.0), + Geometry::line_string(vec![[0.0, 0.0], [1.0, 1.0]]), + ], + }, + ] { + round_trip(SridGeometry::wgs84(g)); + } + } + + #[test] + fn three_numbers_are_z_not_dropped() { + let g = geometry_from_wkt("POINT(1 2 3)").unwrap(); + assert_eq!(g.dimension(), Dimension::Xyz); + assert_eq!(g.ordinates().z(), Some(&[3.0][..])); + let zm = geometry_from_wkt("POINT(1 2 3 4)").unwrap(); + assert_eq!(zm.dimension(), Dimension::Xyzm); + assert_eq!(zm.ordinates().m(), Some(&[4.0][..])); + } + + #[test] + fn every_tag_spelling_parses() { + for (text, dim) in [ + ("POINT Z (1 2 3)", Dimension::Xyz), + ("POINTZ(1 2 3)", Dimension::Xyz), + ("point z(1 2 3)", Dimension::Xyz), + ("POINT M (1 2 3)", Dimension::Xym), + ("POINTM(1 2 3)", Dimension::Xym), + ("POINT ZM (1 2 3 4)", Dimension::Xyzm), + ("POINTZM(1 2 3 4)", Dimension::Xyzm), + ("LINESTRING Z (0 0 1, 1 1 2)", Dimension::Xyz), + ("MULTIPOINT M ((0 0 1), (1 1 2))", Dimension::Xym), + ( + "POLYGON ZM ((0 0 0 0, 1 0 0 0, 1 1 0 0, 0 0 0 0))", + Dimension::Xyzm, + ), + ] { + let g = geometry_from_wkt(text).unwrap_or_else(|e| panic!("{text}: {e}")); + assert_eq!(g.dimension(), dim, "{text}"); + } + let m = geometry_from_wkt("POINT M (1 2 9)").unwrap(); + assert_eq!(m.ordinates().m(), Some(&[9.0][..])); + assert_eq!(m.ordinates().z(), None); + } + + #[test] + fn mismatched_counts_fail() { + for text in [ + "POINT Z (1 2)", + "POINT M (1 2 3 4)", + "POINT ZM (1 2 3)", + "POINT(1)", + "POINT(1 2 3 4 5)", + ] { + assert!(geometry_from_wkt(text).is_err(), "{text}"); + } + let mixed = geometry_from_wkt("LINESTRING(0 0, 1 1 1)").unwrap_err(); + assert!( + matches!( + mixed.kind, + WktErrorKind::Ordinates(OrdinateError::MixedDimension { .. }) + ), + "{mixed}" + ); + } + + #[test] + fn collection_members_share_the_dimension() { + let g = geometry_from_wkt("GEOMETRYCOLLECTION Z (POINT Z (1 2 3), POINT(4 5 6))").unwrap(); + assert_eq!(g.ordinates().z(), Some(&[3.0, 6.0][..])); + assert!(geometry_from_wkt("GEOMETRYCOLLECTION Z (POINT M (1 2 3))").is_err()); + assert!(geometry_from_wkt("GEOMETRYCOLLECTION(POINT Z (1 2 3), POINT(1 2))").is_err()); + let m = geometry_from_wkt("GEOMETRYCOLLECTION M (POINT(1 2 3))").unwrap(); + assert_eq!(m.ordinates().m(), Some(&[3.0][..])); + } + + #[test] + fn z_m_and_zm_round_trip() { + let line = Geometry::line_string(vec![[0.0, 0.0], [1.5, -2.25]]); + for ordinates in [ + Ordinates::new(Some(vec![1.0, 2.0]), None), + Ordinates::new(None, Some(vec![3.0, 4.0])), + Ordinates::new(Some(vec![1.0, 2.0]), Some(vec![3.0, 4.0])), + ] { + round_trip(SridGeometry::with_ordinates(Srid::WGS84, line.clone(), ordinates).unwrap()); + } + let gc = SridGeometry::with_ordinates( + Srid::WGS84, + Geometry::GeometryCollection { + geometries: vec![Geometry::point(1.0, 1.0), Geometry::empty_point()], + }, + Ordinates::new(Some(vec![7.0]), None), + ) + .unwrap(); + let text = geometry_to_wkt(&gc); + assert_eq!(text, "GEOMETRYCOLLECTION Z (POINT Z (1 1 7),POINT Z EMPTY)"); + let back = geometry_from_wkt(&text).unwrap(); + assert_eq!(back.ordinates().z(), Some(&[7.0][..])); + } + + #[test] + fn empty_of_every_kind() { + for (text, kind) in [ + ("POINT EMPTY", GeometryKind::Point), + ("LINESTRING EMPTY", GeometryKind::LineString), + ("POLYGON EMPTY", GeometryKind::Polygon), + ("MULTIPOINT EMPTY", GeometryKind::MultiPoint), + ("MULTILINESTRING EMPTY", GeometryKind::MultiLineString), + ("MULTIPOLYGON EMPTY", GeometryKind::MultiPolygon), + ("GEOMETRYCOLLECTION EMPTY", GeometryKind::GeometryCollection), + ] { + let g = geometry_from_wkt(text).unwrap(); + assert!(g.is_empty(), "{text}"); + assert_eq!(g.geometry().kind(), kind); + assert_eq!(geometry_to_wkt(&g), text); + } + let z = geometry_from_wkt("POINT Z EMPTY").unwrap(); + assert_eq!(z.dimension(), Dimension::Xyz); + assert_eq!(geometry_to_wkt(&z), "POINT Z EMPTY"); + } + + #[test] + fn postgis_text_form() { + let g = geometry_from_wkt("LINESTRING (0 0, 1 1)").unwrap(); + assert_eq!(geometry_to_wkt(&g), "LINESTRING(0 0,1 1)"); + let z = geometry_from_wkt("POINT(1 2 3)").unwrap(); + assert_eq!(geometry_to_wkt(&z), "POINT Z (1 2 3)"); + } + + #[test] + fn case_insensitive_and_multipoint_forms() { + assert_eq!(shape("point(5 10)"), Geometry::point(5.0, 10.0)); + assert_eq!( + shape("MULTIPOINT(1 2, 3 4)"), + shape("MULTIPOINT((1 2), (3 4))") + ); + } + + #[test] + fn ewkt_prefix_is_case_insensitive() { + let square = Geometry::polygon(vec![vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 0.0]]]); + for text in [ + "SRID=4326;POLYGON((0 0, 1 0, 1 1, 0 0))", + "srid=4326;polygon((0 0, 1 0, 1 1, 0 0))", + " SrId= 4326 ;POLYGON((0 0, 1 0, 1 1, 0 0))", + ] { + let parsed = geometry_from_ewkt(text).unwrap(); + assert_eq!(parsed.srid, Some(4326), "{text}"); + assert_eq!(parsed.geometry.geometry(), &square, "{text}"); + } + let g = SridGeometry::new(Srid::new(32648).unwrap(), Geometry::point(1.0, 2.0)); + assert_eq!(geometry_to_ewkt(&g), "SRID=32648;POINT(1 2)"); + } + + #[test] + fn ewkt_without_prefix_has_no_srid() { + let parsed = geometry_from_ewkt("POINT(5 10)").unwrap(); + assert_eq!(parsed.srid, None); + assert_eq!(parsed.geometry.geometry(), &Geometry::point(5.0, 10.0)); + } + + #[test] + fn malformed_text_is_an_error() { + for text in [ + "SRID=;POINT(1 2)", + "SRID=43x6;POINT(1 2)", + "SRID=-4326;POINT(1 2)", + "SRID=4326 POINT(1 2)", + "SRID=4326;GARBAGE", + "SRÍD=4326;POINT(1 2)", + ] { + assert!(geometry_from_ewkt(text).is_err(), "{text}"); + } + for text in [ + "", + "GARBAGE(1 2)", + "POINT(abc def)", + "POIÑT(1 2)", + "é", + "POINT(1 2", + "POINT(1 2) trailing", + "POINT(nan 2)", + "MULTIPOINT(EMPTY, (1 2))", + ] { + assert!(geometry_from_wkt(text).is_err(), "{text}"); + } + } + + #[test] + fn error_reports_the_offset() { + let e = geometry_from_wkt("POINT(1 x)").unwrap_err(); + assert_eq!(e.offset, 6); + } + + #[test] + fn collection_depth_is_bounded() { + let mut text = String::new(); + for _ in 0..70 { + text.push_str("GEOMETRYCOLLECTION("); + } + text.push_str("POINT(1 2)"); + for _ in 0..70 { + text.push(')'); + } + assert!(matches!( + geometry_from_wkt(&text).map_err(|e| e.kind), + Err(WktErrorKind::TooDeep { .. }) + )); + } +} diff --git a/nodedb-spatial/src/wkt/write.rs b/nodedb-spatial/src/wkt/write.rs new file mode 100644 index 000000000..7485a89cb --- /dev/null +++ b/nodedb-spatial/src/wkt/write.rs @@ -0,0 +1,152 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! WKT and EWKT writer. +//! +//! A geometry with Z or M writes its dimension tag after every type keyword +//! (`POINT Z (1 2 3)`), as PostGIS `ST_AsText` does. An EMPTY geometry +//! writes `KIND [tag] EMPTY`. + +use std::fmt::Write as _; + +use nodedb_types::geometry::{Dimension, Geometry, Ordinates, Srid, SridGeometry}; + +/// WKT of `g`, Z and M included. WKT has no SRID slot. +pub fn geometry_to_wkt(g: &SridGeometry) -> String { + let mut out = String::new(); + let mut writer = Writer { + out: &mut out, + ordinates: g.ordinates(), + dimension: g.dimension(), + index: 0, + }; + writer.geometry(g.geometry()); + out +} + +/// EWKT of `g`: `SRID=;` then WKT, as PostGIS `ST_AsEWKT` writes it. +pub fn geometry_to_ewkt(g: &SridGeometry) -> String { + format!("SRID={};{}", g.srid.code(), geometry_to_wkt(g)) +} + +/// WKT of an XY shape. +pub fn shape_to_wkt(geometry: &Geometry) -> String { + geometry_to_wkt(&SridGeometry::new(Srid::WGS84, geometry.clone())) +} + +struct Writer<'a> { + out: &'a mut String, + ordinates: &'a Ordinates, + dimension: Dimension, + /// Index of the next coordinate in traversal order. + index: usize, +} + +impl Writer<'_> { + fn keyword(&mut self, geometry: &Geometry) { + self.out.push_str(geometry.kind().wkt_keyword()); + let tag = self.dimension.wkt_tag(); + if !tag.is_empty() { + self.out.push(' '); + self.out.push_str(tag); + self.out.push(' '); + } + } + + fn empty(&mut self) { + if !self.out.ends_with(' ') { + self.out.push(' '); + } + self.out.push_str("EMPTY"); + } + + fn geometry(&mut self, geometry: &Geometry) { + self.keyword(geometry); + if geometry.is_empty() && !matches!(geometry, Geometry::GeometryCollection { .. }) { + self.empty(); + // An EMPTY polygon or multi may still hold empty rings. + self.index += geometry.coord_count(); + return; + } + match geometry { + Geometry::Point { coordinates } => { + self.out.push('('); + self.coord(*coordinates); + self.out.push(')'); + } + Geometry::LineString { coordinates } => self.path(coordinates), + Geometry::Polygon { coordinates } | Geometry::MultiLineString { coordinates } => { + self.paths(coordinates) + } + Geometry::MultiPoint { coordinates } => { + self.out.push('('); + for (i, c) in coordinates.iter().enumerate() { + if i > 0 { + self.out.push(','); + } + self.out.push('('); + self.coord(*c); + self.out.push(')'); + } + self.out.push(')'); + } + Geometry::MultiPolygon { coordinates } => { + self.out.push('('); + for (i, polygon) in coordinates.iter().enumerate() { + if i > 0 { + self.out.push(','); + } + self.paths(polygon); + } + self.out.push(')'); + } + Geometry::GeometryCollection { geometries } => { + if geometries.is_empty() { + self.empty(); + return; + } + self.out.push('('); + for (i, member) in geometries.iter().enumerate() { + if i > 0 { + self.out.push(','); + } + self.geometry(member); + } + self.out.push(')'); + } + } + } + + fn paths(&mut self, rings: &[Vec<[f64; 2]>]) { + self.out.push('('); + for (i, ring) in rings.iter().enumerate() { + if i > 0 { + self.out.push(','); + } + self.path(ring); + } + self.out.push(')'); + } + + fn path(&mut self, coords: &[[f64; 2]]) { + self.out.push('('); + for (i, c) in coords.iter().enumerate() { + if i > 0 { + self.out.push(','); + } + self.coord(*c); + } + self.out.push(')'); + } + + fn coord(&mut self, c: [f64; 2]) { + let (z, m) = self.ordinates.at(self.index); + self.index += 1; + let _ = write!(self.out, "{} {}", c[0], c[1]); + if let Some(z) = z { + let _ = write!(self.out, " {z}"); + } + if let Some(m) = m { + let _ = write!(self.out, " {m}"); + } + } +} diff --git a/nodedb-sql/Cargo.toml b/nodedb-sql/Cargo.toml index f6daf7936..02f192479 100644 --- a/nodedb-sql/Cargo.toml +++ b/nodedb-sql/Cargo.toml @@ -15,6 +15,7 @@ categories = ["database", "parser-implementations"] [dependencies] nodedb-types = { workspace = true } +nodedb-crs = { workspace = true } nodedb-query = { workspace = true } nodedb-spatial = { workspace = true } sqlparser = { workspace = true } diff --git a/nodedb-sql/src/catalog.rs b/nodedb-sql/src/catalog.rs index 3fbcb23ff..5dc36578f 100644 --- a/nodedb-sql/src/catalog.rs +++ b/nodedb-sql/src/catalog.rs @@ -2,6 +2,7 @@ //! `SqlCatalog` trait + descriptor-resolution error type. +use nodedb_crs::CrsContext; use nodedb_types::DatabaseId; use thiserror::Error; @@ -254,6 +255,14 @@ pub trait SqlCatalog { ) -> Result { Err(sequence_access_unavailable()) } + + /// The CRS context SRIDs resolve against while planning: the built-in + /// registry plus the catalog's user-defined CRSs. The default holds the + /// built-in registry only. Origin overrides it, and a wrapping catalog + /// forwards it to the catalog it wraps. + fn crs_context(&self) -> CrsContext<'_> { + CrsContext::builtin_only() + } } /// The error a catalog with no sequence state returns for every accessor. diff --git a/nodedb-sql/src/ddl_ast/graph_parse/cursor.rs b/nodedb-sql/src/ddl_ast/graph_parse/cursor.rs index 40ad8c061..a9fe76f46 100644 --- a/nodedb-sql/src/ddl_ast/graph_parse/cursor.rs +++ b/nodedb-sql/src/ddl_ast/graph_parse/cursor.rs @@ -121,6 +121,26 @@ impl<'t, 'a> Cursor<'t, 'a> { .map_err(|_| parse_error(format!("{clause} must be a number — found '{text}'"))) } + /// A timestamp in ms since epoch, in the forms `AS OF VALID TIME` + /// accepts: an integer, `NOW()`, or an ISO-8601 string. + pub(super) fn timestamp(&mut self, clause: &str) -> Result { + let expression = match self.peek() { + Some(Tok::Quoted(text)) => format!("'{text}'"), + // The tokenizer drops `(` and `)`, so `NOW()` arrives as `NOW`. + Some(Tok::Word(word)) if word.eq_ignore_ascii_case("NOW") => "NOW()".to_owned(), + Some(Tok::Word(word)) => (*word).to_owned(), + other => { + return Err(parse_error(format!( + "{clause} needs a timestamp — found {}", + describe_opt(other) + ))); + } + }; + self.pos += 1; + crate::parser::preprocess::temporal::parse_temporal_expr(&expression) + .map_err(|error| parse_error(format!("{clause}: {}", error.0))) + } + /// Consume the next word when it parses as a number. pub(super) fn number_word(&mut self) -> Option { let Some(Tok::Word(word)) = self.peek() else { diff --git a/nodedb-sql/src/ddl_ast/graph_parse/entry.rs b/nodedb-sql/src/ddl_ast/graph_parse/entry.rs index 151946d4e..1cfc25a1a 100644 --- a/nodedb-sql/src/ddl_ast/graph_parse/entry.rs +++ b/nodedb-sql/src/ddl_ast/graph_parse/entry.rs @@ -5,6 +5,8 @@ use super::super::statement::{GraphStmt, NodedbStatement}; use super::{edge_predicate, tokenizer, variants}; use crate::error::SqlError; +use crate::parser::preprocess::temporal::{ExprBoundary, extract_bounded}; +use crate::temporal::{TemporalScope, ValidTime}; /// Parse a graph DSL statement. /// @@ -19,9 +21,7 @@ pub fn try_parse(sql: &str) -> Option> { let upper = trimmed.to_ascii_uppercase(); if upper.starts_with("MATCH ") || upper.starts_with("OPTIONAL MATCH ") { - return Some(Ok(NodedbStatement::Graph(GraphStmt::MatchQuery { - body: trimmed.to_string(), - }))); + return Some(parse_match(trimmed)); } if !upper.starts_with("GRAPH ") { @@ -31,14 +31,54 @@ pub fn try_parse(sql: &str) -> Option> { Some(parse_graph(trimmed)) } +/// A MATCH statement: its temporal clauses split off, the rest kept for the +/// pattern compiler. Comments are not blanked: `--` is not a comment inside +/// a pattern. +fn parse_match(sql: &str) -> Result { + let (body, temporal) = split_temporal(sql)?; + Ok(NodedbStatement::Graph(GraphStmt::MatchQuery { + body: body.trim().to_string(), + temporal, + })) +} + +/// Remove the `AS OF SYSTEM TIME` / `AS OF VALID TIME` clauses of a graph +/// read. A valid-time range is refused: a graph read selects the edges valid +/// at one instant. +fn split_temporal(sql: &str) -> Result<(String, TemporalScope), SqlError> { + let extracted = extract_bounded(sql, ExprBoundary::Token) + .map_err(|error| SqlError::Parse { detail: error.0 })?; + let Some(extracted) = extracted else { + return Ok((sql.to_string(), TemporalScope::default())); + }; + if let ValidTime::Range(lo, hi) = extracted.temporal.valid_time { + return Err(SqlError::Parse { + detail: format!( + "FOR VALID_TIME FROM {lo} TO {hi} is not supported on graph reads; use \ + AS OF VALID TIME " + ), + }); + } + Ok((extracted.sql, extracted.temporal)) +} + /// Parse a statement known to start with `GRAPH `. Comments are blanked -/// first. A trailing `EDGE WHERE` predicate splits off next, so the clause -/// text before it tokenizes alone. +/// first, then the temporal clauses split off. A trailing `EDGE WHERE` +/// predicate splits off next, so the clause text before it tokenizes alone. fn parse_graph(sql: &str) -> Result { let blanked = tokenizer::blank_comments(sql); - let trimmed = blanked.trim(); + let (text, temporal) = split_temporal(blanked.trim())?; + let trimmed = text.trim(); let upper = trimmed.to_ascii_uppercase(); let upper = upper.as_str(); + if temporal.is_temporal() && is_graph_write(upper) { + return Err(SqlError::Parse { + detail: format!( + "{} does not accept AS OF: a write applies at the current system time", + graph_command(upper) + ), + }); + } let (head, predicate_text) = edge_predicate::split_edge_where(trimmed)?; let predicate = match predicate_text { Some(text) => edge_predicate::parse_edge_predicate(text)?, @@ -47,10 +87,10 @@ fn parse_graph(sql: &str) -> Result { let toks = tokenizer::tokenize(head); if upper.starts_with("GRAPH TRAVERSE ") { - return variants::parse_traverse(&toks, predicate); + return variants::parse_traverse(&toks, predicate, temporal); } if upper.starts_with("GRAPH PATH ") { - return variants::parse_path(&toks, predicate); + return variants::parse_path(&toks, predicate, temporal); } if predicate_text.is_some() { return Err(SqlError::Parse { @@ -71,11 +111,11 @@ fn parse_graph(sql: &str) -> Result { } else if upper.starts_with("GRAPH UNLABEL ") { variants::parse_set_labels(&toks, true) } else if upper.starts_with("GRAPH NEIGHBORS ") { - variants::parse_neighbors(&toks) + variants::parse_neighbors(&toks, temporal) } else if upper.starts_with("GRAPH ALGO ") { - variants::parse_algo(&toks) + variants::parse_algo(&toks, temporal) } else if upper.starts_with("GRAPH RAG FUSION ") { - variants::parse_rag_fusion(&toks) + variants::parse_rag_fusion(&toks, temporal) } else { // Starts with `GRAPH ` but names no known command. Still graph DSL, // so report it here rather than letting the SQL parser guess. @@ -85,6 +125,18 @@ fn parse_graph(sql: &str) -> Result { } } +/// A graph statement that writes: edges or node labels. +fn is_graph_write(upper: &str) -> bool { + [ + "GRAPH INSERT EDGE ", + "GRAPH DELETE EDGE ", + "GRAPH LABEL ", + "GRAPH UNLABEL ", + ] + .iter() + .any(|command| upper.starts_with(command)) +} + /// The command words of a `GRAPH` statement, for an error message: /// `GRAPH NEIGHBORS`, `GRAPH INSERT EDGE`, `GRAPH RAG FUSION`. fn graph_command(upper: &str) -> String { @@ -121,7 +173,11 @@ mod tests { dst, label, properties, + valid_from_ms, + valid_until_ms, }) => { + assert_eq!(valid_from_ms, None); + assert_eq!(valid_until_ms, None); assert_eq!(collection, "myedges"); assert_eq!(src, "TO"); assert_eq!(dst, "FROM"); @@ -216,7 +272,9 @@ mod tests { max_depth, edge_labels, edge_predicate, + temporal, }) => { + assert!(!temporal.is_temporal()); assert!(edge_predicate.is_empty()); assert_eq!(collection, "docs"); assert_eq!(src, "a"); @@ -385,7 +443,7 @@ mod tests { fn parse_match_query_captures_raw() { let stmt = parsed("MATCH (x)-[:l]->(y) RETURN x, y"); match stmt { - NodedbStatement::Graph(GraphStmt::MatchQuery { body }) => { + NodedbStatement::Graph(GraphStmt::MatchQuery { body, .. }) => { assert!(body.starts_with("MATCH")); } other => panic!("expected MatchQuery, got {other:?}"), @@ -412,7 +470,9 @@ mod tests { RRF_K (60.0, 35.0)", ); match stmt { - NodedbStatement::Graph(GraphStmt::GraphRagFusion { collection, params }) => { + NodedbStatement::Graph(GraphStmt::GraphRagFusion { + collection, params, .. + }) => { assert_eq!(collection, "entities"); let v = params.query_vector.expect("QUERY ARRAY parsed"); assert_eq!(v.len(), 3); @@ -433,7 +493,9 @@ mod tests { fn parse_rag_fusion_minimal_defaults_to_none() { let stmt = parsed("GRAPH RAG FUSION ON mycol QUERY ARRAY[1.0, 0.0]"); match stmt { - NodedbStatement::Graph(GraphStmt::GraphRagFusion { collection, params }) => { + NodedbStatement::Graph(GraphStmt::GraphRagFusion { + collection, params, .. + }) => { assert_eq!(collection, "mycol"); assert!(params.query_vector.is_some()); assert_eq!(params.vector_top_k, None); @@ -659,7 +721,9 @@ mod tests { #[test] fn rag_fusion_bm25_field_is_the_on_after_bm25() { let params_of = |sql: &str| match parsed(sql) { - NodedbStatement::Graph(GraphStmt::GraphRagFusion { collection, params }) => { + NodedbStatement::Graph(GraphStmt::GraphRagFusion { + collection, params, .. + }) => { assert_eq!(collection, "entities"); params } @@ -675,4 +739,128 @@ mod tests { let params = params_of("GRAPH RAG FUSION ON entities QUERY ARRAY[0.1]"); assert_eq!(params.bm25_field, None); } + + // ── Temporal clauses ───────────────────────────────────────────────── + + fn temporal_of(sql: &str) -> TemporalScope { + match parsed(sql) { + NodedbStatement::Graph( + GraphStmt::GraphTraverse { temporal, .. } + | GraphStmt::GraphNeighbors { temporal, .. } + | GraphStmt::GraphPath { temporal, .. } + | GraphStmt::GraphAlgo { temporal, .. } + | GraphStmt::GraphRagFusion { temporal, .. } + | GraphStmt::MatchQuery { temporal, .. }, + ) => temporal, + other => panic!("expected a graph read, got {other:?}"), + } + } + + #[test] + fn every_graph_read_accepts_as_of() { + use nodedb_types::SystemTimeScope; + for sql in [ + "GRAPH TRAVERSE FROM 'x' IN coll DEPTH 2 AS OF SYSTEM TIME 1700", + "GRAPH TRAVERSE IN coll FROM 'x' AS OF SYSTEM TIME 1700 DEPTH 2", + "GRAPH NEIGHBORS IN coll OF 'x' AS OF SYSTEM TIME 1700", + "GRAPH PATH IN coll FROM 'a' TO 'b' AS OF SYSTEM TIME 1700 MAX_DEPTH 3", + "GRAPH ALGO PAGERANK ON coll AS OF SYSTEM TIME 1700 ITERATIONS 5", + "GRAPH RAG FUSION ON coll QUERY ARRAY[0.1] AS OF SYSTEM TIME 1700", + "MATCH (a)-[:l]->(b) IN coll AS OF SYSTEM TIME 1700 RETURN a, b", + ] { + assert_eq!( + temporal_of(sql).system_time, + SystemTimeScope::AsOf(1700), + "{sql}" + ); + } + } + + #[test] + fn traverse_with_as_of_keeps_its_other_clauses() { + match parsed( + "GRAPH TRAVERSE IN 'g' FROM 'a' AS OF SYSTEM TIME 5 AS OF VALID TIME 9 DEPTH 3 \ + EDGE WHERE w > 1", + ) { + NodedbStatement::Graph(GraphStmt::GraphTraverse { + depth, + edge_predicate, + temporal, + .. + }) => { + assert_eq!(depth, 3); + assert_eq!(edge_predicate.len(), 1); + assert_eq!(temporal.valid_time, ValidTime::At(9)); + } + other => panic!("expected GraphTraverse, got {other:?}"), + } + } + + #[test] + fn match_body_loses_its_as_of_text() { + match parsed("MATCH (a)-[:l]->(b) AS OF SYSTEM TIME 5 WHERE a.x = 1 RETURN a AS n") { + NodedbStatement::Graph(GraphStmt::MatchQuery { body, temporal }) => { + assert_eq!(body, "MATCH (a)-[:l]->(b) WHERE a.x = 1 RETURN a AS n"); + assert!(temporal.is_temporal()); + } + other => panic!("expected MatchQuery, got {other:?}"), + } + } + + #[test] + fn as_of_on_a_graph_write_is_refused() { + for (sql, command) in [ + ( + "GRAPH INSERT EDGE IN 'e' FROM 'a' TO 'b' TYPE 'l' AS OF SYSTEM TIME 5", + "GRAPH INSERT EDGE", + ), + ( + "GRAPH DELETE EDGE IN 'e' FROM 'a' TO 'b' TYPE 'l' AS OF SYSTEM TIME 5", + "GRAPH DELETE EDGE", + ), + ("GRAPH LABEL 'a' AS 'P' AS OF VALID TIME 5", "GRAPH LABEL"), + ] { + let error = parse_error(sql); + assert!( + error.contains(command) && error.contains("AS OF"), + "{sql}: {error}" + ); + } + } + + #[test] + fn valid_time_range_is_refused_on_graph_reads() { + let error = parse_error("GRAPH NEIGHBORS IN 'g' OF 'a' FOR VALID_TIME FROM 1 TO 5"); + assert!(error.contains("AS OF VALID TIME"), "{error}"); + } + + #[test] + fn insert_edge_valid_time_interval_parses() { + match parsed( + "GRAPH INSERT EDGE IN 'e' FROM 'a' TO 'b' TYPE 'l' \ + VALID_TIME '2024-01-01T00:00:00Z' UNTIL 1704153600000", + ) { + NodedbStatement::Graph(GraphStmt::GraphInsertEdge { + valid_from_ms, + valid_until_ms, + .. + }) => { + assert_eq!(valid_from_ms, Some(1_704_067_200_000)); + assert_eq!(valid_until_ms, Some(1_704_153_600_000)); + } + other => panic!("expected GraphInsertEdge, got {other:?}"), + } + } + + #[test] + fn insert_edge_empty_or_unanchored_interval_is_refused() { + let error = + parse_error("GRAPH INSERT EDGE IN 'e' FROM 'a' TO 'b' TYPE 'l' VALID_TIME 5 UNTIL 5"); + assert!(error.contains("UNTIL must be after VALID_TIME"), "{error}"); + let error = parse_error("GRAPH INSERT EDGE IN 'e' FROM 'a' TO 'b' TYPE 'l' UNTIL 5"); + assert!(error.contains("UNTIL needs VALID_TIME"), "{error}"); + let error = + parse_error("GRAPH INSERT EDGE IN 'e' FROM 'a' TO 'b' TYPE 'l' VALID_TIME 'soon'"); + assert!(error.contains("VALID_TIME"), "{error}"); + } } diff --git a/nodedb-sql/src/ddl_ast/graph_parse/variants.rs b/nodedb-sql/src/ddl_ast/graph_parse/variants.rs index 58697687f..8513947ba 100644 --- a/nodedb-sql/src/ddl_ast/graph_parse/variants.rs +++ b/nodedb-sql/src/ddl_ast/graph_parse/variants.rs @@ -15,14 +15,17 @@ use super::{ tokenizer::Tok, }; use crate::error::SqlError; +use crate::temporal::TemporalScope; use nodedb_types::filter::MetadataFilter; -/// Parse `GRAPH INSERT EDGE IN FROM TO TYPE [PROPERTIES ...]`. +/// Parse `GRAPH INSERT EDGE IN FROM TO TYPE [PROPERTIES ...] +/// [VALID_TIME [UNTIL ]]`. pub(super) fn parse_insert_edge(toks: &[Tok<'_>]) -> Result { const STMT: &str = "GRAPH INSERT EDGE"; let mut c = Cursor::new(toks, STMT, 3); let (mut collection, mut src, mut dst, mut label, mut properties) = (None, None, None, None, None); + let (mut valid_from_ms, mut valid_until_ms) = (None, None); while let Some(clause) = c.next_clause()? { match clause.as_str() { "IN" => set_once(&mut collection, c.value("IN")?, "IN", STMT)?, @@ -30,18 +33,45 @@ pub(super) fn parse_insert_edge(toks: &[Tok<'_>]) -> Result set_once(&mut dst, c.value("TO")?, "TO", STMT)?, "TYPE" => set_once(&mut label, c.value("TYPE")?, "TYPE", STMT)?, "PROPERTIES" => set_once(&mut properties, c.properties()?, "PROPERTIES", STMT)?, + "VALID_TIME" => set_once( + &mut valid_from_ms, + c.timestamp("VALID_TIME")?, + "VALID_TIME", + STMT, + )?, + "UNTIL" => set_once(&mut valid_until_ms, c.timestamp("UNTIL")?, "UNTIL", STMT)?, _ => return Err(c.unknown_clause()), } } + check_valid_interval(valid_from_ms, valid_until_ms)?; Ok(NodedbStatement::Graph(GraphStmt::GraphInsertEdge { collection: collection.ok_or_else(|| missing_clause(STMT, "IN "))?, src: src.ok_or_else(|| missing_clause(STMT, "FROM "))?, dst: dst.ok_or_else(|| missing_clause(STMT, "TO "))?, label: label.ok_or_else(|| missing_clause(STMT, "TYPE TO TYPE `. pub(super) fn parse_delete_edge(toks: &[Tok<'_>]) -> Result { const STMT: &str = "GRAPH DELETE EDGE"; @@ -96,6 +126,7 @@ pub(super) fn parse_set_labels( pub(super) fn parse_traverse( toks: &[Tok<'_>], edge_predicate: Vec, + temporal: TemporalScope, ) -> Result { const STMT: &str = "GRAPH TRAVERSE"; let mut c = Cursor::new(toks, STMT, 2); @@ -118,11 +149,15 @@ pub(super) fn parse_traverse( edge_labels: labels.unwrap_or_default(), direction: direction.unwrap_or(GraphDirection::Out), edge_predicate, + temporal, })) } /// Parse `GRAPH NEIGHBORS IN OF [LABEL ...] [DIRECTION d]`. -pub(super) fn parse_neighbors(toks: &[Tok<'_>]) -> Result { +pub(super) fn parse_neighbors( + toks: &[Tok<'_>], + temporal: TemporalScope, +) -> Result { const STMT: &str = "GRAPH NEIGHBORS"; let mut c = Cursor::new(toks, STMT, 2); let (mut collection, mut node, mut labels, mut direction) = (None, None, None, None); @@ -140,6 +175,7 @@ pub(super) fn parse_neighbors(toks: &[Tok<'_>]) -> Result"))?, edge_labels: labels.unwrap_or_default(), direction: direction.unwrap_or(GraphDirection::Out), + temporal, })) } @@ -147,6 +183,7 @@ pub(super) fn parse_neighbors(toks: &[Tok<'_>]) -> Result], edge_predicate: Vec, + temporal: TemporalScope, ) -> Result { const STMT: &str = "GRAPH PATH"; let mut c = Cursor::new(toks, STMT, 2); @@ -169,6 +206,7 @@ pub(super) fn parse_path( max_depth: max_depth.unwrap_or(10), edge_labels: labels.unwrap_or_default(), edge_predicate, + temporal, })) } @@ -189,7 +227,10 @@ struct AlgoClauses { } /// Parse `GRAPH ALGO ON [options...]`. -pub(super) fn parse_algo(toks: &[Tok<'_>]) -> Result { +pub(super) fn parse_algo( + toks: &[Tok<'_>], + temporal: TemporalScope, +) -> Result { const STMT: &str = "GRAPH ALGO"; let mut c = Cursor::new(toks, STMT, 2); let Some(algorithm) = c.next_clause()? else { @@ -232,6 +273,7 @@ pub(super) fn parse_algo(toks: &[Tok<'_>]) -> Result direction: a.direction, mode: a.mode, personalization: a.personalization, + temporal, })) } @@ -255,7 +297,10 @@ fn refuse_subquery(collection: &str) -> Result<(), SqlError> { /// /// The fusion parameters go through [`FusionParams::parse`], which every /// fusion SQL surface shares. -pub(super) fn parse_rag_fusion(toks: &[Tok<'_>]) -> Result { +pub(super) fn parse_rag_fusion( + toks: &[Tok<'_>], + temporal: TemporalScope, +) -> Result { const STMT: &str = "GRAPH RAG FUSION"; let mut c = Cursor::new(toks, STMT, 3); if !c.eat_keyword("ON") { @@ -266,5 +311,6 @@ pub(super) fn parse_rag_fusion(toks: &[Tok<'_>]) -> Result'`. +//! - `ALTER USER ... SET PASSWORD ''`. +//! +//! A secret-bearing statement that does not parse keeps only its leading +//! keywords, so a malformed one never carries its secret either. + +use super::{credential, user_auth}; +use crate::ddl_ast::alter_ops::AlterUserOp; +use crate::ddl_ast::statement::{AuthStmt, NodedbStatement, TenantSelector}; + +const REDACTED: &str = "''"; + +/// The redacted audit text of `sql`, or `None` when `sql` carries no secret +/// and its own text is the audit text. +pub fn audit_text_for(sql: &str) -> Option { + let trimmed = sql.trim(); + let upper = trimmed.to_uppercase(); + if let Some(text) = credential::audit_text(trimmed, &upper) { + return Some(text); + } + let create = upper.starts_with("CREATE USER "); + if !create && !upper.starts_with("ALTER USER ") { + return None; + } + let parsed = user_auth::try_parse(&upper, &[], trimmed)?; + match parsed { + Ok(NodedbStatement::Auth(AuthStmt::CreateUser { + username, + password, + role, + tenant, + if_not_exists, + })) => { + let mut text = String::from("CREATE USER "); + if if_not_exists { + text.push_str("IF NOT EXISTS "); + } + text.push_str(&username); + if !password.is_empty() { + text.push_str(" WITH PASSWORD "); + text.push_str(REDACTED); + } + if let Some(role) = role { + text.push_str(" ROLE "); + text.push_str(&role); + } + match tenant { + Some(TenantSelector::Id(id)) => text.push_str(&format!(" TENANT {id}")), + Some(TenantSelector::Name(name)) => text.push_str(&format!(" TENANT '{name}'")), + None => {} + } + Some(text) + } + Ok(NodedbStatement::Auth(AuthStmt::AlterUser { + username, + op: AlterUserOp::SetPassword { .. }, + })) => Some(format!("ALTER USER {username} SET PASSWORD {REDACTED}")), + // Every other form carries no secret. + Ok(_) => None, + Err(_) if create => Some("CREATE USER ".to_string()), + Err(_) => Some("ALTER USER ".to_string()), + } +} + +#[cfg(test)] +mod tests { + use super::audit_text_for; + + #[test] + fn a_credential_keeps_its_name_and_type_only() { + assert_eq!( + audit_text_for(" create credential earthdata (TYPE 'bearer', TOKEN 'tok-secret') ") + .as_deref(), + Some("CREATE CREDENTIAL earthdata (TYPE 'bearer', )") + ); + let s3 = audit_text_for( + "CREATE CREDENTIAL l (TYPE 's3', ACCESS_KEY_ID 'tok-a', SECRET_ACCESS_KEY 'tok-b', \ + REGION 'r', SESSION_TOKEN 'tok-c')", + ); + assert_eq!( + s3.as_deref(), + Some("CREATE CREDENTIAL l (TYPE 's3', )") + ); + assert_eq!( + audit_text_for("CREATE CREDENTIAL x (TYPE 'bearer', TOKEN 'tok-secret', 'tok-2')") + .as_deref(), + Some("CREATE CREDENTIAL ") + ); + } + + #[test] + fn a_user_password_is_removed() { + assert_eq!( + audit_text_for("CREATE USER alice WITH PASSWORD 'pw-secret' ROLE readwrite TENANT 7") + .as_deref(), + Some("CREATE USER alice WITH PASSWORD '' ROLE readwrite TENANT 7") + ); + assert_eq!( + audit_text_for("create user if not exists bob password 'pw-secret' tenant 'acme'") + .as_deref(), + Some("CREATE USER IF NOT EXISTS bob WITH PASSWORD '' TENANT 'acme'") + ); + assert_eq!( + audit_text_for("ALTER USER alice SET PASSWORD 'pw-secret'").as_deref(), + Some("ALTER USER alice SET PASSWORD ''") + ); + // A malformed statement keeps only its leading keywords. + assert_eq!( + audit_text_for("CREATE USER bob WITH PASSWORD 'pw-secret' BOGUS").as_deref(), + Some("CREATE USER ") + ); + assert_eq!( + audit_text_for("ALTER USER bob SET PASSWORD 'pw-secret' extra").as_deref(), + Some("ALTER USER ") + ); + } + + #[test] + fn a_statement_without_a_secret_keeps_its_text() { + for sql in [ + "ALTER USER alice SET ROLE readonly", + "ALTER USER alice PASSWORD NEVER EXPIRES", + "DROP CREDENTIAL earthdata", + "DROP USER alice", + "CREATE COLLECTION tok", + ] { + assert_eq!(audit_text_for(sql), None, "{sql}"); + } + } +} diff --git a/nodedb-sql/src/ddl_ast/parse/copy_from.rs b/nodedb-sql/src/ddl_ast/parse/copy_from.rs index 55a5127ea..f72a31208 100644 --- a/nodedb-sql/src/ddl_ast/parse/copy_from.rs +++ b/nodedb-sql/src/ddl_ast/parse/copy_from.rs @@ -1,8 +1,12 @@ // SPDX-License-Identifier: Apache-2.0 -//! Parse `COPY FROM '' [WITH (...)]`. +//! Parse `COPY FROM '' [WITH (...)]` and the raster +//! list form `COPY FROM ('', ...) WITH (FORMAT geotiff, ...)`. //! -//! The server-side file-path form parses here. Its options parse in +//! A source is a server path, an `http(s)://` URL or an `s3://bucket/key` +//! object. Text formats read one server file. A raster reads one source per +//! band group, and every remote source needs `FORMAT geotiff` or a `.tif` +//! path. The file-path form parses here. Its options parse in //! `copy_options`. The STDIN streaming shape //! (`COPY FROM STDIN`) parses in `copy_from_stdin`. The restore shape //! `COPY tenant_restore() FROM STDIN` returns `None`, so dispatch falls @@ -14,7 +18,7 @@ use super::clause::{ClauseCursor, Tok, Token, tokenize}; use super::copy_options::{detect_format_from_path, parse_copy_options}; -use crate::ddl_ast::statement::{MiscStmt, NodedbStatement}; +use crate::ddl_ast::statement::{CopyFormat, CopySource, MiscStmt, NodedbStatement}; use crate::error::SqlError; /// Which way a `COPY` statement moves rows. @@ -103,24 +107,31 @@ pub(super) fn try_parse( } fn parse_copy_from(sql: &str) -> Result { - // Grammar: COPY FROM '' [WITH (...)] + // Grammar: COPY FROM '' | ('', ...) [WITH (...)] let toks = tokenize(sql); let mut c = ClauseCursor::new(sql, &toks, "COPY"); c.expect_keyword("COPY")?; let collection = copy_collection_name(&mut c)?; c.expect_keyword("FROM")?; - let path = c - .string("FROM") - .map_err(|_| c.error("expected single-quoted path after FROM"))?; + let listed = c.peek_punct('('); + let sources = if listed { + c.paren_list("FROM sources", copy_source)? + } else { + vec![copy_source(&mut c)?] + }; let options = parse_copy_options(&mut c)?; let format = match options.format.clone() { Some(f) => Some(f), - None => detect_format_from_path(&path)?, + None => match sources.first() { + Some(first) => detect_format_from_path(first.format_hint())?, + None => None, + }, }; options.check_for(format.as_ref())?; + check_sources(&c, &sources, listed, format.as_ref())?; Ok(NodedbStatement::Misc(MiscStmt::CopyFromFile { collection, - path, + sources, format, delimiter: options.delimiter, header: options.header.unwrap_or(true), @@ -128,6 +139,38 @@ fn parse_copy_from(sql: &str) -> Result { })) } +/// One quoted source. +fn copy_source(c: &mut ClauseCursor<'_, '_>) -> Result { + let text = c + .string("FROM") + .map_err(|_| c.error("expected a single-quoted path or URL after FROM"))?; + CopySource::parse(&text).map_err(|e| c.error(format!("FROM: {e}"))) +} + +/// A text format reads one server file. A list of sources, and every +/// remote source, need `FORMAT geotiff`. +fn check_sources( + c: &ClauseCursor<'_, '_>, + sources: &[CopySource], + listed: bool, + format: Option<&CopyFormat>, +) -> Result<(), SqlError> { + if format == Some(&CopyFormat::GeoTiff) { + return Ok(()); + } + if listed { + return Err( + c.error("a list of sources loads one raster per band group and needs FORMAT geotiff") + ); + } + match sources { + [CopySource::File(_)] => Ok(()), + _ => Err( + c.error("text formats read one server file; HTTP and S3 sources need FORMAT geotiff") + ), + } +} + /// The copied collection: one bare or quoted name, quotes removed and /// lower-cased. pub(super) fn copy_collection_name(c: &mut ClauseCursor<'_, '_>) -> Result { @@ -143,7 +186,6 @@ pub(super) fn copy_collection_name(c: &mut ClauseCursor<'_, '_>) -> Result NodedbStatement { parse(sql).expect("expected Some").expect("expected Ok") @@ -160,10 +202,12 @@ mod tests { let stmt = ok("COPY usersßffİ FROM '/tmp/users.ndjson'"); match stmt { NodedbStatement::Misc(MiscStmt::CopyFromFile { - collection, path, .. + collection, + sources, + .. }) => { assert_eq!(collection, "usersßffi\u{307}"); - assert_eq!(path, "/tmp/users.ndjson"); + assert_eq!(sources, vec![CopySource::File("/tmp/users.ndjson".into())]); } other => panic!("unexpected: {other:?}"), } @@ -175,14 +219,14 @@ mod tests { match stmt { NodedbStatement::Misc(MiscStmt::CopyFromFile { collection, - path, + sources, format, delimiter, header, raster, }) => { assert_eq!(collection, "users"); - assert_eq!(path, "/tmp/users.ndjson"); + assert_eq!(sources, vec![CopySource::File("/tmp/users.ndjson".into())]); assert_eq!(format, Some(CopyFormat::Ndjson)); assert_eq!(delimiter, None); assert!(header); @@ -304,6 +348,55 @@ mod tests { assert!(parse("COPY tenant_restore(1) FROM STDIN").is_none()); } + #[test] + fn a_source_list_loads_one_raster_per_band_group() { + let stmt = ok("COPY scene FROM ('/B4.TIF', 'https://h.example/B5.TIF', \ + 's3://bucket/QA_PIXEL.TIF') WITH (FORMAT geotiff, BANDS (red, nir, qa))"); + match stmt { + NodedbStatement::Misc(MiscStmt::CopyFromFile { + sources, format, .. + }) => { + assert_eq!(format, Some(CopyFormat::GeoTiff)); + assert_eq!( + sources, + vec![ + CopySource::File("/B4.TIF".into()), + CopySource::Http("https://h.example/B5.TIF".into()), + CopySource::S3 { + bucket: "bucket".into(), + key: "QA_PIXEL.TIF".into() + }, + ] + ); + } + other => panic!("unexpected: {other:?}"), + } + } + + #[test] + fn remote_and_listed_sources_need_geotiff() { + let e = err("COPY users FROM 'https://h.example/u.csv'"); + assert!(e.to_string().contains("FORMAT geotiff"), "{e}"); + let e = err("COPY users FROM ('/a.csv', '/b.csv') WITH (FORMAT csv)"); + assert!(e.to_string().contains("FORMAT geotiff"), "{e}"); + let e = err("COPY scene FROM 'ftp://h/a.tif'"); + assert!(e.to_string().contains("scheme"), "{e}"); + let e = err("COPY scene FROM ()"); + assert!(matches!(e, SqlError::Parse { .. })); + } + + #[test] + fn an_s3_url_detects_geotiff_from_its_key() { + let stmt = ok("COPY scene FROM 's3://landsat/a/B4.TIF'"); + assert!(matches!( + stmt, + NodedbStatement::Misc(MiscStmt::CopyFromFile { + format: Some(CopyFormat::GeoTiff), + .. + }) + )); + } + #[test] fn unknown_with_option_is_err() { let e = err("COPY users FROM '/tmp/data.csv' WITH (FORMAT csv, BOGUS opt)"); diff --git a/nodedb-sql/src/ddl_ast/parse/copy_options.rs b/nodedb-sql/src/ddl_ast/parse/copy_options.rs index ea4571314..980e2b2f6 100644 --- a/nodedb-sql/src/ddl_ast/parse/copy_options.rs +++ b/nodedb-sql/src/ddl_ast/parse/copy_options.rs @@ -4,14 +4,15 @@ //! from the file extension. //! //! Text formats take `FORMAT`, `DELIMITER` and `HEADER`. GeoTIFF takes -//! `FORMAT geotiff`, `BANDS (name | SKIP, ...)`, `NODATA ` and -//! `TIME ''`. An option of the other family is refused once the -//! format is known. +//! `FORMAT geotiff`, `BANDS (name | SKIP, ...)`, `NODATA `, +//! `TIME ''`, `CREDENTIAL `, `ADOPT_METADATA `, +//! `OVERVIEW `, `WINDOW (x0, y0, x1, y1)` and `REQUESTER_PAYS `. +//! An option of the other family is refused once the format is known. use nodedb_types::NdbDateTime; use super::clause::{ClauseCursor, Tok, Token, parse_err}; -use crate::ddl_ast::statement::{CopyBand, CopyFormat, CopyRasterOptions}; +use crate::ddl_ast::statement::{CopyBand, CopyFormat, CopyRasterOptions, CopyWindow}; use crate::error::SqlError; /// The options of one `COPY ... WITH (...)` clause, before the format is @@ -36,7 +37,8 @@ impl CopyOptions { } if !geotiff && !self.raster.is_empty() { return Err(parse_err( - "COPY: BANDS, NODATA and TIME need FORMAT geotiff (or a .tif / .tiff path)", + "COPY: BANDS, NODATA, TIME, CREDENTIAL, ADOPT_METADATA, OVERVIEW, WINDOW and \ + REQUESTER_PAYS need FORMAT geotiff (or a .tif / .tiff path)", )); } Ok(()) @@ -49,6 +51,11 @@ enum OptionValue { Bands(Vec), Nodata(f64), TimeMs(i64), + Credential(String), + AdoptMetadata(bool), + Overview(u32), + Window(CopyWindow), + RequesterPays(bool), } /// Read an optional `WITH (key [=] value, ...)` and require the end of the @@ -63,6 +70,11 @@ pub(super) fn parse_copy_options(c: &mut ClauseCursor<'_, '_>) -> Result OptionValue::Bands(bands_value(c)?), "NODATA" => OptionValue::Nodata(nodata_value(c)?), "TIME" => OptionValue::TimeMs(time_value(c)?), + "CREDENTIAL" => OptionValue::Credential(c.name("CREDENTIAL")?), + "ADOPT_METADATA" => OptionValue::AdoptMetadata(c.bool_literal("ADOPT_METADATA")?), + "OVERVIEW" => OptionValue::Overview(overview_value(c)?), + "WINDOW" => OptionValue::Window(window_value(c)?), + "REQUESTER_PAYS" => OptionValue::RequesterPays(c.bool_literal("REQUESTER_PAYS")?), _ => OptionValue::Text(match c.string(&key) { Ok(value) => value, Err(_) => c.word(&key)?.to_string(), @@ -104,6 +116,11 @@ fn apply( (_, OptionValue::Bands(bands)) => options.raster.bands = Some(bands), (_, OptionValue::Nodata(nodata)) => options.raster.nodata = Some(nodata), (_, OptionValue::TimeMs(time_ms)) => options.raster.time_ms = Some(time_ms), + (_, OptionValue::Credential(name)) => options.raster.credential = Some(name), + (_, OptionValue::AdoptMetadata(adopt)) => options.raster.adopt_metadata = adopt, + (_, OptionValue::Overview(level)) => options.raster.overview = Some(level), + (_, OptionValue::Window(window)) => options.raster.window = Some(window), + (_, OptionValue::RequesterPays(pays)) => options.raster.requester_pays = pays, (other, OptionValue::Text(_)) => { return Err(c.error(format!("WITH: unknown option '{other}'"))); } @@ -188,6 +205,32 @@ fn nodata_value(c: &mut ClauseCursor<'_, '_>) -> Result { Ok(value) } +/// An image index: 0 is full resolution, `n` the n-th overview. +fn overview_value(c: &mut ClauseCursor<'_, '_>) -> Result { + let level = c.u64("OVERVIEW")?; + u32::try_from(level).map_err(|_| c.error(format!("OVERVIEW {level} is out of range"))) +} + +/// `(x0, y0, x1, y1)`: a non-empty half-open pixel window. +fn window_value(c: &mut ClauseCursor<'_, '_>) -> Result { + let bounds = c.paren_list("WINDOW", |c| { + let value = c.u64("WINDOW bound")?; + u32::try_from(value).map_err(|_| c.error(format!("WINDOW bound {value} is out of range"))) + })?; + let &[x0, y0, x1, y1] = bounds.as_slice() else { + return Err(c.error(format!( + "WINDOW takes four bounds (x0, y0, x1, y1) — found {}", + bounds.len() + ))); + }; + if x1 <= x0 || y1 <= y0 { + return Err(c.error(format!( + "WINDOW ({x0}, {y0}, {x1}, {y1}) is empty; x1 must exceed x0 and y1 must exceed y0" + ))); + } + Ok(CopyWindow { x0, y0, x1, y1 }) +} + /// A quoted ISO 8601 instant, in milliseconds since the Unix epoch. fn time_value(c: &mut ClauseCursor<'_, '_>) -> Result { let text = c.string("TIME")?; @@ -392,6 +435,37 @@ mod tests { assert!(message.contains("twice"), "{message}"); } + #[test] + fn remote_load_options_parse() { + let (_, raster) = copy_from( + "COPY s FROM 's3://b/k.tif' WITH (CREDENTIAL earthdata, ADOPT_METADATA true, \ + OVERVIEW 2, WINDOW (10, 20, 266, 276), REQUESTER_PAYS true)", + ); + assert_eq!(raster.credential.as_deref(), Some("earthdata")); + assert!(raster.adopt_metadata); + assert!(raster.requester_pays); + assert_eq!(raster.overview, Some(2)); + assert_eq!( + raster.window, + Some(CopyWindow { + x0: 10, + y0: 20, + x1: 266, + y1: 276 + }) + ); + } + + #[test] + fn a_window_needs_four_ordered_bounds() { + assert!(refusal("COPY s FROM '/d/s.tif' WITH (WINDOW (1, 2, 3))").contains("four")); + assert!(refusal("COPY s FROM '/d/s.tif' WITH (WINDOW (5, 0, 5, 9))").contains("empty")); + assert!(refusal("COPY s FROM '/d/s.tif' WITH (OVERVIEW -1)").contains("OVERVIEW")); + assert!( + refusal("COPY s FROM '/d/s.csv' WITH (ADOPT_METADATA true)").contains("FORMAT geotiff") + ); + } + #[test] fn unknown_format_lists_geotiff() { let message = refusal("COPY s FROM '/d/s.csv' WITH (FORMAT parquet)"); diff --git a/nodedb-sql/src/ddl_ast/parse/credential.rs b/nodedb-sql/src/ddl_ast/parse/credential.rs new file mode 100644 index 000000000..3a64b95ed --- /dev/null +++ b/nodedb-sql/src/ddl_ast/parse/credential.rs @@ -0,0 +1,273 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Parse `CREATE CREDENTIAL`, `DROP CREDENTIAL` and `SHOW CREDENTIALS`. +//! +//! Syntax: +//! - `CREATE CREDENTIAL (TYPE 's3', ACCESS_KEY_ID '..', SECRET_ACCESS_KEY '..', +//! REGION '..' [, SESSION_TOKEN '..'] [, ENDPOINT '..'])` +//! - `CREATE CREDENTIAL (TYPE 'bearer', TOKEN '..')` +//! - `CREATE CREDENTIAL (TYPE 'sas', TOKEN '..')` +//! - `DROP CREDENTIAL [IF EXISTS] ` +//! - `SHOW CREDENTIALS` +//! +//! Options come in any order, each once. No error message quotes an option +//! value, so a secret never reaches an error or a log line. + +use super::clause::{ClauseCursor, tokenize}; +use crate::ddl_ast::statement::{CredentialSpec, MiscStmt, NodedbStatement, SecretText}; +use crate::error::SqlError; + +/// The option keys `CREATE CREDENTIAL` takes. +const KEYS: &[&str] = &[ + "TYPE", + "ACCESS_KEY_ID", + "SECRET_ACCESS_KEY", + "REGION", + "SESSION_TOKEN", + "ENDPOINT", + "TOKEN", +]; + +/// Try to parse a credential statement. +pub(super) fn try_parse(upper: &str, trimmed: &str) -> Option> { + if upper.starts_with("CREATE CREDENTIAL ") { + return Some(parse_create(trimmed)); + } + if upper.starts_with("DROP CREDENTIAL ") { + return Some(parse_drop(trimmed)); + } + let bare = upper.trim_end_matches(';').trim_end(); + if bare == "SHOW CREDENTIALS" { + return Some(Ok(NodedbStatement::Misc(MiscStmt::ShowCredentials))); + } + None +} + +/// The audit text of a `CREATE CREDENTIAL` statement: its name and type, +/// with every secret removed. `None` for any other statement. A statement +/// that does not parse keeps only its leading keywords. +pub(super) fn audit_text(trimmed: &str, upper: &str) -> Option { + if !upper.starts_with("CREATE CREDENTIAL ") { + return None; + } + Some(match parse_create(trimmed) { + Ok(NodedbStatement::Misc(MiscStmt::CreateCredential { name, spec })) => format!( + "CREATE CREDENTIAL {name} (TYPE '{}', )", + spec.type_name() + ), + _ => "CREATE CREDENTIAL ".to_string(), + }) +} + +fn parse_create(sql: &str) -> Result { + let toks = tokenize(sql); + let mut c = ClauseCursor::new(sql, &toks, "CREATE CREDENTIAL"); + c.expect_keywords(&["CREATE", "CREDENTIAL"])?; + let name = c.name("credential name")?; + let shape = || { + format!( + "expected ( KEY 'value', ... ) with keys {}; values are quoted strings", + KEYS.join(", ") + ) + }; + let mut entries: Vec<(&'static str, String)> = Vec::new(); + c.expect_punct('(').map_err(|_| c.error(shape()))?; + loop { + let key = c.one_of("option", KEYS).map_err(|_| c.error(shape()))?; + let value = c.string(key).map_err(|_| c.error(shape()))?; + if entries.iter().any(|(seen, _)| *seen == key) { + return Err(c.error(format!("{key} appears twice"))); + } + entries.push((key, value)); + if !c.eat_punct(',') { + break; + } + } + c.expect_punct(')').map_err(|_| c.error(shape()))?; + if !c.at_end() { + return Err(c.error("unexpected content after the option list")); + } + let spec = build_spec(&c, entries)?; + Ok(NodedbStatement::Misc(MiscStmt::CreateCredential { + name, + spec, + })) +} + +/// The credential the options describe, checked against its `TYPE`. +fn build_spec( + c: &ClauseCursor<'_, '_>, + entries: Vec<(&'static str, String)>, +) -> Result { + let mut fields = Fields(entries); + let kind = fields + .take("TYPE") + .ok_or_else(|| c.error("TYPE is required: 's3', 'bearer' or 'sas'"))?; + let spec = match kind.to_ascii_lowercase().as_str() { + "s3" => CredentialSpec::S3 { + access_key_id: fields.secret(c, "ACCESS_KEY_ID", "s3")?, + secret_access_key: fields.secret(c, "SECRET_ACCESS_KEY", "s3")?, + region: fields.required(c, "REGION", "s3")?, + session_token: fields.take("SESSION_TOKEN").map(SecretText::new), + endpoint: fields.take("ENDPOINT"), + }, + "bearer" => CredentialSpec::Bearer { + token: fields.secret(c, "TOKEN", "bearer")?, + }, + "sas" => CredentialSpec::Sas { + token: fields.secret(c, "TOKEN", "sas")?, + }, + other => { + return Err(c.error(format!( + "TYPE '{other}' is not a credential type; use 's3', 'bearer' or 'sas'" + ))); + } + }; + if let Some((key, _)) = fields.0.first() { + return Err(c.error(format!( + "{key} does not apply to TYPE '{}'", + spec.type_name() + ))); + } + Ok(spec) +} + +/// The options not yet read. +struct Fields(Vec<(&'static str, String)>); + +impl Fields { + fn take(&mut self, key: &str) -> Option { + let at = self.0.iter().position(|(k, _)| *k == key)?; + Some(self.0.remove(at).1) + } + + fn required( + &mut self, + c: &ClauseCursor<'_, '_>, + key: &str, + kind: &str, + ) -> Result { + let value = self + .take(key) + .ok_or_else(|| c.error(format!("TYPE '{kind}' needs {key}")))?; + if value.is_empty() { + return Err(c.error(format!("{key} must not be empty"))); + } + Ok(value) + } + + fn secret( + &mut self, + c: &ClauseCursor<'_, '_>, + key: &str, + kind: &str, + ) -> Result { + self.required(c, key, kind).map(SecretText::new) + } +} + +fn parse_drop(sql: &str) -> Result { + let toks = tokenize(sql); + let mut c = ClauseCursor::new(sql, &toks, "DROP CREDENTIAL"); + c.expect_keywords(&["DROP", "CREDENTIAL"])?; + let if_exists = c.eat_keywords(&["IF", "EXISTS"]); + let name = c.name("credential name")?; + c.finish()?; + Ok(NodedbStatement::Misc(MiscStmt::DropCredential { + name, + if_exists, + })) +} + +#[cfg(test)] +mod tests { + use crate::ddl_ast::parse::dispatch::parse; + use crate::ddl_ast::statement::{CredentialSpec, MiscStmt, NodedbStatement}; + + fn ok(sql: &str) -> NodedbStatement { + parse(sql).expect("expected Some").expect("expected Ok") + } + + fn refusal(sql: &str) -> String { + parse(sql) + .expect("expected Some") + .expect_err("expected Err") + .to_string() + } + + #[test] + fn each_type_parses() { + let stmt = ok( + "CREATE CREDENTIAL landsat (TYPE 's3', ACCESS_KEY_ID 'AKIA1', \ + SECRET_ACCESS_KEY 'sek', REGION 'us-west-2', SESSION_TOKEN 'st')", + ); + match stmt { + NodedbStatement::Misc(MiscStmt::CreateCredential { name, spec }) => { + assert_eq!(name, "landsat"); + match spec { + CredentialSpec::S3 { + access_key_id, + secret_access_key, + region, + session_token, + endpoint, + } => { + assert_eq!(access_key_id.expose(), "AKIA1"); + assert_eq!(secret_access_key.expose(), "sek"); + assert_eq!(region, "us-west-2"); + assert_eq!( + session_token.map(|t| t.expose().to_string()), + Some("st".into()) + ); + assert_eq!(endpoint, None); + } + other => panic!("unexpected: {other:?}"), + } + } + other => panic!("unexpected: {other:?}"), + } + assert!(matches!( + ok("CREATE CREDENTIAL earthdata (TOKEN 'eyJ', TYPE 'bearer')"), + NodedbStatement::Misc(MiscStmt::CreateCredential { + spec: CredentialSpec::Bearer { .. }, + .. + }) + )); + assert!(matches!( + ok("CREATE CREDENTIAL blob (TYPE 'SAS', TOKEN 'sv=1')"), + NodedbStatement::Misc(MiscStmt::CreateCredential { + spec: CredentialSpec::Sas { .. }, + .. + }) + )); + } + + #[test] + fn drop_and_show_parse() { + assert_eq!( + ok("DROP CREDENTIAL IF EXISTS earthdata"), + NodedbStatement::Misc(MiscStmt::DropCredential { + name: "earthdata".into(), + if_exists: true + }) + ); + assert_eq!( + ok("SHOW CREDENTIALS"), + NodedbStatement::Misc(MiscStmt::ShowCredentials) + ); + } + + #[test] + fn refusals_never_quote_a_value() { + let message = refusal("CREATE CREDENTIAL x (TYPE 'bearer', TOKEN 'tok-secret', 'tok-2')"); + assert!(!message.contains("tok-"), "{message}"); + let message = refusal("CREATE CREDENTIAL x (TYPE 'bearer')"); + assert!(message.contains("TOKEN"), "{message}"); + let message = refusal("CREATE CREDENTIAL x (TYPE 'bearer', TOKEN 'a', REGION 'r')"); + assert!(message.contains("REGION"), "{message}"); + let message = refusal("CREATE CREDENTIAL x (TYPE 'ftp', TOKEN 'tok-secret')"); + assert!(!message.contains("tok-"), "{message}"); + let message = refusal("CREATE CREDENTIAL x (TYPE 's3', TOKEN 'a', TOKEN 'b')"); + assert!(message.contains("twice"), "{message}"); + } +} diff --git a/nodedb-sql/src/ddl_ast/parse/dispatch.rs b/nodedb-sql/src/ddl_ast/parse/dispatch.rs index c6f6f03fc..b3695f315 100644 --- a/nodedb-sql/src/ddl_ast/parse/dispatch.rs +++ b/nodedb-sql/src/ddl_ast/parse/dispatch.rs @@ -4,9 +4,9 @@ use super::{ alert, backup, change_stream, cluster_admin, collection, conflict_policy, copy_from, copy_to, - custom_type, database, grant, graph_stats, index, maintenance, materialized_view, - oidc_provider, redaction, retention, rls, schedule, sequence, synonym_group, tenant, trigger, - user_auth, + credential, custom_type, database, grant, graph_stats, index, maintenance, materialized_view, + oidc_provider, redaction, retention, rls, schedule, sequence, spatial_ref_sys, synonym_group, + tenant, trigger, user_auth, }; use crate::ddl_ast::graph_parse; use crate::ddl_ast::statement::NodedbStatement; @@ -66,6 +66,7 @@ pub fn parse(sql: &str) -> Option> { // `SHOW GRAPH STATS` must be checked before the generic collection parser // so its `SHOW` prefix is not consumed by `SHOW COLLECTIONS`/etc. try_family!(graph_stats::try_parse(&upper, &parts, trimmed)); + try_family!(credential::try_parse(&upper, trimmed)); // Conflict policy must be checked before the generic collection parser // so "SET ON CONFLICT" does not fall through to the raw-SQL path. try_family!(conflict_policy::try_parse(&upper, &parts, trimmed)); @@ -91,6 +92,7 @@ pub fn parse(sql: &str) -> Option> { try_family!(redaction::try_parse(&upper, &parts, trimmed)); try_family!(materialized_view::try_parse(&upper, &parts, trimmed)); try_family!(synonym_group::try_parse(&upper, &parts, trimmed)); + try_family!(spatial_ref_sys::try_parse(&upper, trimmed)); try_family!(custom_type::try_parse(&upper, &parts, trimmed)); try_family!(database::try_parse(&upper, &parts, trimmed)); try_family!(tenant::try_parse(&upper, &parts, trimmed)); @@ -469,7 +471,7 @@ mod tests { fn match_query_uses_body_field() { let stmt = ok("MATCH (x)-[:l]->(y) RETURN x, y"); match stmt { - NodedbStatement::Graph(GraphStmt::MatchQuery { body }) => { + NodedbStatement::Graph(GraphStmt::MatchQuery { body, .. }) => { assert!(body.starts_with("MATCH"), "body must hold the original SQL"); } other => panic!("expected MatchQuery, got {other:?}"), diff --git a/nodedb-sql/src/ddl_ast/parse/mod.rs b/nodedb-sql/src/ddl_ast/parse/mod.rs index 670365f94..254ccf3d9 100644 --- a/nodedb-sql/src/ddl_ast/parse/mod.rs +++ b/nodedb-sql/src/ddl_ast/parse/mod.rs @@ -1,6 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 mod alert; +mod audit_text; mod backup; mod change_stream; mod clause; @@ -11,6 +12,7 @@ mod copy_from; mod copy_from_stdin; mod copy_options; mod copy_to; +mod credential; mod custom_type; pub mod database; mod dispatch; @@ -27,10 +29,12 @@ mod retention; mod rls; mod schedule; mod sequence; +mod spatial_ref_sys; mod synonym_group; mod tenant; mod trigger; mod user_auth; pub mod vector_primary; +pub use audit_text::audit_text_for; pub use dispatch::parse; diff --git a/nodedb-sql/src/ddl_ast/parse/spatial_ref_sys.rs b/nodedb-sql/src/ddl_ast/parse/spatial_ref_sys.rs new file mode 100644 index 000000000..5334c57e2 --- /dev/null +++ b/nodedb-sql/src/ddl_ast/parse/spatial_ref_sys.rs @@ -0,0 +1,154 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Parse `CREATE SPATIAL_REF_SYS` and `DROP SPATIAL_REF_SYS`. +//! +//! Syntax: +//! - `CREATE SPATIAL_REF_SYS (NAME '', PROJ '')` +//! - `CREATE SPATIAL_REF_SYS (NAME '', PROJJSON '')` +//! - `CREATE SPATIAL_REF_SYS (NAME '', WKT2 '')` +//! - `DROP SPATIAL_REF_SYS ` +//! +//! The two options come in either order, each once. Whether the code and +//! the definition are valid is the handler's question. + +use super::clause::{ClauseCursor, tokenize}; +use crate::ddl_ast::statement::{CrsTextFormat, MiscStmt, NodedbStatement}; +use crate::error::SqlError; + +/// Try to parse a user-defined CRS statement. +pub(super) fn try_parse(upper: &str, trimmed: &str) -> Option> { + if upper.starts_with("CREATE SPATIAL_REF_SYS") { + return Some(parse_create(trimmed)); + } + if upper.starts_with("DROP SPATIAL_REF_SYS") { + return Some(parse_drop(trimmed)); + } + None +} + +fn parse_create(sql: &str) -> Result { + let toks = tokenize(sql); + let mut c = ClauseCursor::new(sql, &toks, "CREATE SPATIAL_REF_SYS"); + c.expect_keywords(&["CREATE", "SPATIAL_REF_SYS"])?; + let srid = c.u64("SRID code")?; + c.expect_punct('(')?; + let mut name: Option = None; + let mut definition: Option<(CrsTextFormat, String)> = None; + loop { + let option = c.one_of("option", &["NAME", "PROJJSON", "PROJ", "WKT2"])?; + let value = c.string(option)?; + match option { + "NAME" if name.is_none() => name = Some(value), + "NAME" => return Err(c.error("NAME appears twice")), + other if definition.is_some() => { + return Err(c.error(format!( + "{other} conflicts with an earlier definition; give one of PROJ, PROJJSON, \ + WKT2" + ))); + } + "PROJ" => definition = Some((CrsTextFormat::Proj, value)), + "PROJJSON" => definition = Some((CrsTextFormat::ProjJson, value)), + _ => definition = Some((CrsTextFormat::Wkt2, value)), + } + if !c.eat_punct(',') { + break; + } + } + c.expect_punct(')')?; + c.finish()?; + let name = name.ok_or_else(|| c.error("NAME is required"))?; + if name.trim().is_empty() { + return Err(c.error("NAME must not be empty")); + } + let (format, definition) = + definition.ok_or_else(|| c.error("a definition is required: PROJ, PROJJSON or WKT2"))?; + Ok(NodedbStatement::Misc(MiscStmt::CreateSpatialRefSys { + srid, + name, + format, + definition, + })) +} + +fn parse_drop(sql: &str) -> Result { + let toks = tokenize(sql); + let mut c = ClauseCursor::new(sql, &toks, "DROP SPATIAL_REF_SYS"); + c.expect_keywords(&["DROP", "SPATIAL_REF_SYS"])?; + let srid = c.u64("SRID code")?; + c.finish()?; + Ok(NodedbStatement::Misc(MiscStmt::DropSpatialRefSys { srid })) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ddl_ast::parse::dispatch::parse; + + fn ok(sql: &str) -> NodedbStatement { + parse(sql).expect("expected Some").expect("expected Ok") + } + + fn err(sql: &str) -> SqlError { + parse(sql) + .expect("expected Some") + .expect_err("expected Err") + } + + #[test] + fn create_each_format() { + let stmt = + ok("CREATE SPATIAL_REF_SYS 910001 (NAME 'Johor Grid', PROJ '+proj=tmerc +lon_0=103')"); + assert_eq!( + stmt, + NodedbStatement::Misc(MiscStmt::CreateSpatialRefSys { + srid: 910_001, + name: "Johor Grid".into(), + format: CrsTextFormat::Proj, + definition: "+proj=tmerc +lon_0=103".into(), + }) + ); + let stmt = ok(r#"create spatial_ref_sys 910002 (WKT2 'GEOGCRS["x"]', name 'n')"#); + let NodedbStatement::Misc(MiscStmt::CreateSpatialRefSys { + format, definition, .. + }) = stmt + else { + panic!("expected CreateSpatialRefSys"); + }; + assert_eq!(format, CrsTextFormat::Wkt2); + assert_eq!(definition, r#"GEOGCRS["x"]"#); + let stmt = + ok(r#"CREATE SPATIAL_REF_SYS 910003 (NAME 'j', PROJJSON '{"type": "GeographicCRS"}')"#); + assert!(matches!( + stmt, + NodedbStatement::Misc(MiscStmt::CreateSpatialRefSys { + format: CrsTextFormat::ProjJson, + .. + }) + )); + } + + #[test] + fn drop_reads_the_code() { + assert_eq!( + ok("DROP SPATIAL_REF_SYS 910001"), + NodedbStatement::Misc(MiscStmt::DropSpatialRefSys { srid: 910_001 }) + ); + } + + #[test] + fn malformed_statements_are_refused() { + for sql in [ + "CREATE SPATIAL_REF_SYS 910001 (PROJ '+proj=longlat')", + "CREATE SPATIAL_REF_SYS 910001 (NAME 'a')", + "CREATE SPATIAL_REF_SYS 910001 (NAME 'a', NAME 'b', PROJ 'x')", + "CREATE SPATIAL_REF_SYS 910001 (NAME 'a', PROJ 'x', WKT2 'y')", + "CREATE SPATIAL_REF_SYS 910001 (NAME '', PROJ 'x')", + "CREATE SPATIAL_REF_SYS abc (NAME 'a', PROJ 'x')", + "CREATE SPATIAL_REF_SYS 910001 (NAME 'a', PROJ 'x') extra", + "DROP SPATIAL_REF_SYS", + "DROP SPATIAL_REF_SYS 910001 CASCADE", + ] { + let _ = err(sql); + } + } +} diff --git a/nodedb-sql/src/ddl_ast/statement/copy_source.rs b/nodedb-sql/src/ddl_ast/statement/copy_source.rs new file mode 100644 index 000000000..424788f23 --- /dev/null +++ b/nodedb-sql/src/ddl_ast/statement/copy_source.rs @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! The sources of `COPY ... FROM`: a server file, an HTTP(S) URL, or an S3 +//! object. + +/// One source a `COPY ... FROM` reads. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CopySource { + /// A server file path. + File(String), + /// An `http://` or `https://` URL, read with byte-range requests. + Http(String), + /// An `s3://bucket/key` object. + S3 { bucket: String, key: String }, +} + +/// Why a source string names no source. +#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] +pub enum CopySourceError { + #[error("source must not be empty")] + Empty, + #[error("'{0}' names no bucket and key; write s3:///")] + S3Shape(String), + #[error("'{0}' uses an unsupported scheme; use a server path, https://, http:// or s3://")] + Scheme(String), +} + +impl CopySource { + /// The source `text` names: `s3://bucket/key`, an `http(s)://` URL, or + /// a server path. + pub fn parse(text: &str) -> Result { + if text.is_empty() { + return Err(CopySourceError::Empty); + } + let lower = text.to_ascii_lowercase(); + if lower.starts_with("https://") || lower.starts_with("http://") { + return Ok(Self::Http(text.to_string())); + } + if lower.starts_with("s3://") { + let rest = &text["s3://".len()..]; + return match rest.split_once('/') { + Some((bucket, key)) if !bucket.is_empty() && !key.is_empty() => Ok(Self::S3 { + bucket: bucket.to_string(), + key: key.to_string(), + }), + _ => Err(CopySourceError::S3Shape(text.to_string())), + }; + } + if let Some((scheme, _)) = text.split_once("://") + && !scheme.is_empty() + && scheme + .chars() + .all(|c| c.is_ascii_alphanumeric() || c == '+') + { + return Err(CopySourceError::Scheme(text.to_string())); + } + Ok(Self::File(text.to_string())) + } + + /// The source as the statement wrote it, for messages. + pub fn display(&self) -> String { + match self { + Self::File(path) => path.clone(), + Self::Http(url) => url.clone(), + Self::S3 { bucket, key } => format!("s3://{bucket}/{key}"), + } + } + + /// The path part a format is detected from: the file path, the URL path + /// without its query, or the object key. + pub fn format_hint(&self) -> &str { + match self { + Self::File(path) => path, + Self::Http(url) => { + let end = url.find(['?', '#']).unwrap_or(url.len()); + &url[..end] + } + Self::S3 { key, .. } => key, + } + } +} + +/// `WINDOW (x0, y0, x1, y1)`: the pixel columns `x0..x1` and rows `y0..y1` +/// of the image, half-open. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct CopyWindow { + pub x0: u32, + pub y0: u32, + pub x1: u32, + pub y1: u32, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn each_scheme_names_its_source() { + assert_eq!( + CopySource::parse("/data/a.tif"), + Ok(CopySource::File("/data/a.tif".into())) + ); + assert_eq!( + CopySource::parse("HTTPS://h.example/a.tif?x=1"), + Ok(CopySource::Http("HTTPS://h.example/a.tif?x=1".into())) + ); + assert_eq!( + CopySource::parse("s3://bucket/dir/a.tif"), + Ok(CopySource::S3 { + bucket: "bucket".into(), + key: "dir/a.tif".into() + }) + ); + assert!(matches!( + CopySource::parse("s3://bucket"), + Err(CopySourceError::S3Shape(_)) + )); + assert!(matches!( + CopySource::parse("ftp://h/a.tif"), + Err(CopySourceError::Scheme(_)) + )); + assert_eq!(CopySource::parse(""), Err(CopySourceError::Empty)); + } + + #[test] + fn the_format_hint_drops_the_query() { + let url = CopySource::Http("https://h.example/a.TIF?sig=1".into()); + assert_eq!(url.format_hint(), "https://h.example/a.TIF"); + let s3 = CopySource::S3 { + bucket: "b".into(), + key: "k.tif".into(), + }; + assert_eq!(s3.display(), "s3://b/k.tif"); + assert_eq!(s3.format_hint(), "k.tif"); + } +} diff --git a/nodedb-sql/src/ddl_ast/statement/credential.rs b/nodedb-sql/src/ddl_ast/statement/credential.rs new file mode 100644 index 000000000..90962e307 --- /dev/null +++ b/nodedb-sql/src/ddl_ast/statement/credential.rs @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! `CREATE CREDENTIAL`: the secrets a remote `COPY` source authenticates +//! with. A secret never prints: its `Debug` form is redacted. + +/// Secret text. `Debug` prints ``. +#[derive(Clone, PartialEq, Eq)] +pub struct SecretText(String); + +impl SecretText { + /// Wrap `text`. + pub fn new(text: String) -> Self { + Self(text) + } + + /// The secret. + pub fn expose(&self) -> &str { + &self.0 + } +} + +impl std::fmt::Debug for SecretText { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("") + } +} + +/// What a credential holds, by `TYPE`. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CredentialSpec { + /// `TYPE 's3'`: AWS keys for `s3://` sources. + S3 { + access_key_id: SecretText, + secret_access_key: SecretText, + region: String, + session_token: Option, + /// An S3-compatible endpoint URL in place of AWS. + endpoint: Option, + }, + /// `TYPE 'bearer'`: an `Authorization: Bearer` token, such as an + /// Earthdata Login token. + Bearer { token: SecretText }, + /// `TYPE 'sas'`: a shared access signature appended to the URL query. + Sas { token: SecretText }, +} + +impl CredentialSpec { + /// The `TYPE` keyword. + pub fn type_name(&self) -> &'static str { + match self { + Self::S3 { .. } => "s3", + Self::Bearer { .. } => "bearer", + Self::Sas { .. } => "sas", + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn debug_never_prints_the_secret() { + let spec = CredentialSpec::Bearer { + token: SecretText::new("tok-123".into()), + }; + let text = format!("{spec:?}"); + assert!(!text.contains("tok-123"), "{text}"); + assert!(text.contains(""), "{text}"); + assert_eq!(spec.type_name(), "bearer"); + } +} diff --git a/nodedb-sql/src/ddl_ast/statement/maintenance.rs b/nodedb-sql/src/ddl_ast/statement/maintenance.rs index 4e61ccd72..5ada4718c 100644 --- a/nodedb-sql/src/ddl_ast/statement/maintenance.rs +++ b/nodedb-sql/src/ddl_ast/statement/maintenance.rs @@ -1,5 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 +use super::copy_source::CopyWindow; + /// Source for `COPY ... TO`. #[derive(Debug, Clone, PartialEq, Eq)] pub enum CopyToSource { @@ -29,8 +31,8 @@ pub enum CopyFormat { JsonArray, /// CSV with an optional header row (`.csv`). Csv, - /// A GeoTIFF raster (`.tif` / `.tiff`), read from a server path into an - /// array. + /// A GeoTIFF raster (`.tif` / `.tiff`), read from a server path, an + /// HTTP(S) URL or an S3 object into an array. GeoTiff, } @@ -43,12 +45,12 @@ pub enum CopyBand { Skip, } -/// The GeoTIFF options of `COPY FROM '' WITH (FORMAT geotiff, ...)`. -/// Every field is `None` for the other formats. +/// The GeoTIFF options of `COPY FROM WITH (FORMAT geotiff, ...)`. +/// Every field is unset for the other formats. #[derive(Debug, Clone, PartialEq, Default)] pub struct CopyRasterOptions { - /// `BANDS (a, SKIP, b)`: entry `i` names where band `i` loads. `None` - /// loads band `i` into attribute `i`. + /// `BANDS (a, SKIP, b)`: entry `i` names where band `i` of the sources' + /// bands in order loads. `None` loads band `i` into attribute `i`. pub bands: Option>, /// `NODATA `: the nodata value, in place of the file's /// `GDAL_NODATA`. @@ -56,11 +58,30 @@ pub struct CopyRasterOptions { /// `TIME ''`: the scene instant every loaded cell carries, in /// milliseconds since the Unix epoch. pub time_ms: Option, + /// `CREDENTIAL `: the credential remote sources authenticate with. + pub credential: Option, + /// `ADOPT_METADATA true`: an attr with no metadata takes the band's + /// nodata, scale, offset and units from the file. + pub adopt_metadata: bool, + /// `OVERVIEW `: read overview `n` (1 is the first) in place of the + /// full-resolution image. + pub overview: Option, + /// `WINDOW (x0, y0, x1, y1)`: load this pixel window only. + pub window: Option, + /// `REQUESTER_PAYS true`: an `s3://` read bills the requester. + pub requester_pays: bool, } impl CopyRasterOptions { /// True when no GeoTIFF option is given. pub fn is_empty(&self) -> bool { - self.bands.is_none() && self.nodata.is_none() && self.time_ms.is_none() + self.bands.is_none() + && self.nodata.is_none() + && self.time_ms.is_none() + && self.credential.is_none() + && !self.adopt_metadata + && self.overview.is_none() + && self.window.is_none() + && !self.requester_pays } } diff --git a/nodedb-sql/src/ddl_ast/statement/mod.rs b/nodedb-sql/src/ddl_ast/statement/mod.rs index 2aa165bf1..82d8552b2 100644 --- a/nodedb-sql/src/ddl_ast/statement/mod.rs +++ b/nodedb-sql/src/ddl_ast/statement/mod.rs @@ -4,12 +4,16 @@ pub mod auth; pub mod collection; +pub mod copy_source; +pub mod credential; pub mod maintenance; pub mod types; pub mod wrapper; pub use auth::*; pub use collection::*; +pub use copy_source::*; +pub use credential::*; pub use maintenance::*; pub use types::*; pub use wrapper::*; diff --git a/nodedb-sql/src/ddl_ast/statement/types/graph.rs b/nodedb-sql/src/ddl_ast/statement/types/graph.rs index a84aa8fda..3679c8ec5 100644 --- a/nodedb-sql/src/ddl_ast/statement/types/graph.rs +++ b/nodedb-sql/src/ddl_ast/statement/types/graph.rs @@ -3,6 +3,7 @@ //! Graph DDL/DML statements. use crate::ddl_ast::graph_types::{GraphDirection, GraphProperties}; +use crate::temporal::TemporalScope; #[derive(Debug, Clone, PartialEq)] pub enum GraphStmt { @@ -13,6 +14,12 @@ pub enum GraphStmt { dst: String, label: String, properties: GraphProperties, + /// `VALID_TIME `: start of the edge's valid time, ms since + /// epoch. `None` takes the write's wall time. + valid_from_ms: Option, + /// `UNTIL `: exclusive end of the edge's valid time. `None` + /// leaves it open. + valid_until_ms: Option, }, GraphDeleteEdge { collection: String, @@ -37,6 +44,8 @@ pub enum GraphStmt { direction: GraphDirection, /// `EDGE WHERE` filters, AND-ed. Empty admits every edge. edge_predicate: Vec, + /// `AS OF SYSTEM TIME` / `AS OF VALID TIME` of the read. + temporal: TemporalScope, }, GraphNeighbors { /// Collection whose edges the traversal is scoped to. Required: a @@ -47,6 +56,8 @@ pub enum GraphStmt { /// Empty keeps every edge. Otherwise an edge with any listed label. edge_labels: Vec, direction: GraphDirection, + /// `AS OF SYSTEM TIME` / `AS OF VALID TIME` of the read. + temporal: TemporalScope, }, GraphPath { /// Collection whose edges the traversal is scoped to. Required: a @@ -60,6 +71,8 @@ pub enum GraphStmt { edge_labels: Vec, /// `EDGE WHERE` filters, AND-ed. Empty admits every edge. edge_predicate: Vec, + /// `AS OF SYSTEM TIME` / `AS OF VALID TIME` of the read. + temporal: TemporalScope, }, GraphAlgo { algorithm: String, @@ -77,13 +90,22 @@ pub enum GraphStmt { /// e.g. `PERSONALIZATION {"alice": 1.0, "bob": 0.5}`. Parsed into a /// `node_id → weight` map by the handler. `None` = standard PageRank. personalization: Option, + /// `AS OF SYSTEM TIME` / `AS OF VALID TIME` of the read. + temporal: TemporalScope, + }, + /// `MATCH (x)-[:l]->(y) RETURN x, y`. `body` is the statement with its + /// temporal clauses removed, for the graph pattern compiler. + MatchQuery { + body: String, + /// `AS OF SYSTEM TIME` / `AS OF VALID TIME` of the match. + temporal: TemporalScope, }, - /// `MATCH (x)-[:l]->(y) RETURN x, y` — body forwarded verbatim to the graph pattern compiler. - MatchQuery { body: String }, /// `GRAPH RAG FUSION ON QUERY ARRAY[…] [options…]` GraphRagFusion { collection: String, params: crate::ddl_ast::graph_parse::FusionParams, + /// `AS OF SYSTEM TIME` / `AS OF VALID TIME` of the read. + temporal: TemporalScope, }, /// `SHOW GRAPH STATS [''] [VERBOSE] [AS OF SYSTEM TIME ]` /// diff --git a/nodedb-sql/src/ddl_ast/statement/types/misc.rs b/nodedb-sql/src/ddl_ast/statement/types/misc.rs index 09713d77f..b019d1b8c 100644 --- a/nodedb-sql/src/ddl_ast/statement/types/misc.rs +++ b/nodedb-sql/src/ddl_ast/statement/types/misc.rs @@ -2,10 +2,23 @@ //! Miscellaneous DDL/DML statements. +use crate::ddl_ast::statement::copy_source::CopySource; +use crate::ddl_ast::statement::credential::CredentialSpec; use crate::ddl_ast::statement::maintenance::{ CopyFormat, CopyRasterOptions, CopyStdinFormat, CopyToSource, }; +/// The text form of a `CREATE SPATIAL_REF_SYS` definition. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CrsTextFormat { + /// A PROJ string: `PROJ '+proj=...'`. + Proj, + /// PROJJSON: `PROJJSON '{...}'`. + ProjJson, + /// WKT2: `WKT2 'PROJCRS[...]'`. + Wkt2, +} + #[derive(Debug, Clone, PartialEq)] pub enum MiscStmt { // ── Miscellaneous ──────────────────────────────────────────── @@ -17,15 +30,44 @@ pub enum MiscStmt { collection: String, }, + // ── User-defined CRSs ──────────────────────────────────────── + /// `CREATE SPATIAL_REF_SYS (NAME '', PROJ|PROJJSON|WKT2 '')` + CreateSpatialRefSys { + srid: u64, + name: String, + format: CrsTextFormat, + definition: String, + }, + /// `DROP SPATIAL_REF_SYS ` + DropSpatialRefSys { + srid: u64, + }, + + // ── Credentials ────────────────────────────────────────────── + /// `CREATE CREDENTIAL (TYPE 's3' | 'bearer' | 'sas', ...)` + CreateCredential { + name: String, + spec: CredentialSpec, + }, + /// `DROP CREDENTIAL [IF EXISTS] ` + DropCredential { + name: String, + if_exists: bool, + }, + /// `SHOW CREDENTIALS`: names and types, never a secret. + ShowCredentials, + // ── Bulk import ────────────────────────────────────────────── /// `COPY FROM '' [WITH (FORMAT ..., DELIMITER ..., HEADER ...)]` - /// `COPY FROM '' WITH (FORMAT geotiff [, BANDS (...)] [, NODATA n] [, TIME '..'])` + /// `COPY FROM '' | ('', ...) WITH (FORMAT geotiff, ...)` /// - /// Server-side file-path bulk import. `CopyFromStdin` is the streaming - /// form, and `CopyToFile` the export. + /// Bulk import from a server file, or for a raster from HTTP(S) or S3 + /// sources too. `CopyFromStdin` is the streaming form, and `CopyToFile` + /// the export. CopyFromFile { collection: String, - path: String, + /// One source, or one per band group of a multi-source raster. + sources: Vec, format: Option, delimiter: Option, header: bool, diff --git a/nodedb-sql/src/functions/builtins/scalars/spatial/arg_shape.rs b/nodedb-sql/src/functions/builtins/scalars/spatial/arg_shape.rs index ac8b8e9c8..154b19501 100644 --- a/nodedb-sql/src/functions/builtins/scalars/spatial/arg_shape.rs +++ b/nodedb-sql/src/functions/builtins/scalars/spatial/arg_shape.rs @@ -44,13 +44,50 @@ static GEOMETRY_2_DISTANCE: &[ArgTypeSpec] = &[ arg("distance_m", FLOAT), ]; static GEOMETRY_SRID: &[ArgTypeSpec] = &[arg("geom", GEOMETRY), arg("srid", INT)]; +static GEOMETRY_SRID_EPOCH: &[ArgTypeSpec] = + &[arg("geom", GEOMETRY), arg("srid", INT), arg("epoch", FLOAT)]; static GEOMETRY_BUFFER: &[ArgTypeSpec] = &[ arg("geom", GEOMETRY), arg("distance_m", FLOAT), arg("segments", INT), ]; static LNG_LAT: &[ArgTypeSpec] = &[arg("lng", FLOAT), arg("lat", FLOAT)]; -static LNG_LAT_Z: &[ArgTypeSpec] = &[arg("x", FLOAT), arg("y", FLOAT), arg("z", FLOAT)]; +static XYZM: &[ArgTypeSpec] = &[ + arg("x", FLOAT), + arg("y", FLOAT), + arg("z", FLOAT), + arg("m", FLOAT), +]; +static XYM: &[ArgTypeSpec] = &[arg("x", FLOAT), arg("y", FLOAT), arg("m", FLOAT)]; +static GEOMETRY_NUMBER: &[ArgTypeSpec] = &[arg("geom", GEOMETRY), arg("value", FLOAT)]; +static GEOMETRY_NUMBERS_2_OPT: &[ArgTypeSpec] = &[ + arg("geom", GEOMETRY), + arg("value1", FLOAT), + arg("value2", FLOAT), +]; +static GEOMETRY_NUMBER_NUMBER_OPT: &[ArgTypeSpec] = &[ + arg("geom", GEOMETRY), + arg("measure", FLOAT), + arg("offset", FLOAT), +]; +static GEOMETRY_RANGE: &[ArgTypeSpec] = &[ + arg("geom", GEOMETRY), + arg("measure_start", FLOAT), + arg("measure_end", FLOAT), + arg("offset", FLOAT), +]; +static GEOMETRY_TEXT_OPT: &[ArgTypeSpec] = &[arg("geom", GEOMETRY), arg("option", TEXT)]; +static GEOMETRY_2_TEXT_OPT: &[ArgTypeSpec] = &[ + arg("geom1", GEOMETRY), + arg("geom2", GEOMETRY), + arg("pattern", TEXT), +]; +static TILE_ENVELOPE: &[ArgTypeSpec] = &[ + arg("zoom", INT), + arg("x", INT), + arg("y", INT), + arg("bounds", GEOMETRY), +]; static LNG_LAT_PRECISION: &[ArgTypeSpec] = &[arg("lng", FLOAT), arg("lat", FLOAT), arg("precision", INT)]; static LAT_LNG_RESOLUTION: &[ArgTypeSpec] = @@ -86,9 +123,9 @@ pub(super) fn arg_types(shape: GeoArgShape) -> &'static [ArgTypeSpec] { GeoArgShape::Geometry2 => GEOMETRY_2, GeoArgShape::Geometry2Distance => GEOMETRY_2_DISTANCE, GeoArgShape::GeometrySrid => GEOMETRY_SRID, + GeoArgShape::GeometrySridEpoch => GEOMETRY_SRID_EPOCH, GeoArgShape::GeometryBuffer => GEOMETRY_BUFFER, GeoArgShape::LngLat => LNG_LAT, - GeoArgShape::LngLatZ => LNG_LAT_Z, GeoArgShape::LngLatPrecision => LNG_LAT_PRECISION, GeoArgShape::LatLngResolution => LAT_LNG_RESOLUTION, GeoArgShape::Text1 => TEXT_1, @@ -99,23 +136,33 @@ pub(super) fn arg_types(shape: GeoArgShape) -> &'static [ArgTypeSpec] { GeoArgShape::Bbox => BBOX, GeoArgShape::PointsVariadic => POINTS_VARIADIC, GeoArgShape::RingsVariadic => RINGS_VARIADIC, + GeoArgShape::Xyzm => XYZM, + GeoArgShape::Xym => XYM, + GeoArgShape::GeometryNumber | GeoArgShape::GeometryNumberOpt => GEOMETRY_NUMBER, + GeoArgShape::GeometryNumbers2Opt => GEOMETRY_NUMBERS_2_OPT, + GeoArgShape::GeometryNumberNumberOpt => GEOMETRY_NUMBER_NUMBER_OPT, + GeoArgShape::GeometryRange => GEOMETRY_RANGE, + GeoArgShape::GeometryTextOpt => GEOMETRY_TEXT_OPT, + GeoArgShape::Geometry2TextOpt => GEOMETRY_2_TEXT_OPT, + GeoArgShape::TileEnvelope => TILE_ENVELOPE, } } /// Plan-time return type for a catalog return kind. /// -/// `Object` and `Array` results (decoded geohash bounds, H3 cell centres, -/// neighbour lists) have no scalar column type; `None` marks them as resolved -/// at runtime, the same convention the rest of the registry uses. +/// `Object` results (decoded geohash bounds, H3 cell centres) and `ByArity` +/// results (`ST_Relate`) have no single column type. `None` marks them as +/// resolved at runtime, the same convention the rest of the registry uses. pub(super) fn return_type(returns: GeoReturn) -> Option { match returns { GeoReturn::Bool => Some(ColumnType::Bool), GeoReturn::Float => Some(ColumnType::Float64), GeoReturn::Int => Some(ColumnType::Int64), GeoReturn::Text => Some(ColumnType::String), + GeoReturn::Bytes => Some(ColumnType::Bytes), GeoReturn::Geometry => Some(ColumnType::Geometry), GeoReturn::Array => Some(ColumnType::Array), - GeoReturn::Object => None, + GeoReturn::Object | GeoReturn::ByArity => None, } } diff --git a/nodedb-sql/src/functions/builtins/scalars/spatial/triggers.rs b/nodedb-sql/src/functions/builtins/scalars/spatial/triggers.rs index c6a40ac88..6b456b452 100644 --- a/nodedb-sql/src/functions/builtins/scalars/spatial/triggers.rs +++ b/nodedb-sql/src/functions/builtins/scalars/spatial/triggers.rs @@ -4,7 +4,7 @@ //! //! Search routing is a planner concern, not a property of the function's //! semantics, so it lives here rather than in the shared catalog. Only the -//! four predicates the spatial engine can answer from its index are routed; +//! five predicates the spatial engine can answer from its index are routed; //! every other geo function evaluates per-row. use crate::functions::registry::SearchTrigger; @@ -12,6 +12,7 @@ use crate::functions::registry::SearchTrigger; pub(super) fn search_trigger(canonical: &str) -> SearchTrigger { match canonical { "st_dwithin" => SearchTrigger::SpatialDWithin, + "st_3ddwithin" => SearchTrigger::SpatialDWithin3D, "st_contains" => SearchTrigger::SpatialContains, "st_intersects" => SearchTrigger::SpatialIntersects, "st_within" => SearchTrigger::SpatialWithin, @@ -28,7 +29,13 @@ mod tests { /// name the catalog does not declare would never fire. #[test] fn routed_predicates_are_declared_in_the_catalog() { - for name in ["st_dwithin", "st_contains", "st_intersects", "st_within"] { + for name in [ + "st_dwithin", + "st_3ddwithin", + "st_contains", + "st_intersects", + "st_within", + ] { assert!( catalog::lookup(name).is_some(), "'{name}' is routed to the spatial index but is not in the catalog" diff --git a/nodedb-sql/src/functions/registry.rs b/nodedb-sql/src/functions/registry.rs index 9b681a511..9c17f4ace 100644 --- a/nodedb-sql/src/functions/registry.rs +++ b/nodedb-sql/src/functions/registry.rs @@ -62,6 +62,7 @@ pub enum SearchTrigger { HybridSearch, TextMatch, SpatialDWithin, + SpatialDWithin3D, SpatialContains, SpatialIntersects, SpatialWithin, diff --git a/nodedb-sql/src/lib.rs b/nodedb-sql/src/lib.rs index 2e28aad35..d667a053e 100644 --- a/nodedb-sql/src/lib.rs +++ b/nodedb-sql/src/lib.rs @@ -191,7 +191,7 @@ fn plan_statements( /// Plan one parsed array statement. fn plan_array_statement(stmt: ArrayStatement, catalog: &dyn SqlCatalog) -> Result> { match stmt { - ArrayStatement::Create(c) => Ok(vec![planner::array_ddl::plan_create_array(&c)?]), + ArrayStatement::Create(c) => Ok(vec![planner::array_ddl::plan_create_array(&c, catalog)?]), ArrayStatement::Drop(d) => Ok(vec![planner::array_ddl::plan_drop_array(&d)?]), ArrayStatement::Insert(i) => planner::array_dml::plan_insert_array(&i, catalog), ArrayStatement::Delete(d) => planner::array_dml::plan_delete_array(&d, catalog), diff --git a/nodedb-sql/src/parser/array_stmt/ast.rs b/nodedb-sql/src/parser/array_stmt/ast.rs index 142b44f81..a8fbb32f6 100644 --- a/nodedb-sql/src/parser/array_stmt/ast.rs +++ b/nodedb-sql/src/parser/array_stmt/ast.rs @@ -3,8 +3,8 @@ //! Typed AST returned by the array-statement parser. use crate::types_array::{ - ArrayAttrAst, ArrayCellOrderAst, ArrayCoordLiteral, ArrayDimAst, ArrayGeorefAlter, - ArrayGeorefAst, ArrayInsertRow, ArrayTileOrderAst, + ArrayAttrAst, ArrayAttrMetaAlter, ArrayCellOrderAst, ArrayCoordLiteral, ArrayDimAst, + ArrayGeorefAlter, ArrayGeorefAst, ArrayInsertRow, ArrayTileOrderAst, }; /// Top-level array-statement AST. One variant per surface command. @@ -18,7 +18,9 @@ pub enum ArrayStatement { } /// `ALTER ARRAY SET ( key = value [, key = value]* )`, -/// `ALTER ARRAY SET GEOREF (...)`, or `ALTER ARRAY DROP GEOREF`. +/// `ALTER ARRAY SET GEOREF (...)`, `ALTER ARRAY DROP GEOREF`, +/// `ALTER ARRAY ALTER ATTR SET (NODATA n, ...)`, or +/// `ALTER ARRAY ALTER ATTR DROP NODATA [, ...]`. /// /// Supported SET keys: /// - `audit_retain_ms` — `NULL` or non-negative integer. @@ -37,6 +39,8 @@ pub enum AlterArrayAction { SetOptions(Vec<(String, Option)>), /// `SET GEOREF (...)` or `DROP GEOREF`. Georef(ArrayGeorefAlter), + /// `ALTER ATTR SET (...)` or `ALTER ATTR DROP ...`. + AttrMeta(ArrayAttrMetaAlter), } #[derive(Debug, Clone, PartialEq)] diff --git a/nodedb-sql/src/parser/array_stmt/parse_impl/alter.rs b/nodedb-sql/src/parser/array_stmt/parse_impl/alter.rs index 71ae0442b..7cc6d2c14 100644 --- a/nodedb-sql/src/parser/array_stmt/parse_impl/alter.rs +++ b/nodedb-sql/src/parser/array_stmt/parse_impl/alter.rs @@ -5,14 +5,24 @@ use crate::error::Result; use crate::parser::array_stmt::ast::{AlterArrayAction, AlterArrayAst}; use crate::parser::array_stmt::lexer::Tok; -use crate::types_array::ArrayGeorefAlter; +use crate::types_array::{ArrayAttrMetaAlter, ArrayAttrMetaChange, ArrayGeorefAlter}; use super::core::Parser; impl<'a> Parser<'a> { pub(in crate::parser::array_stmt::parse) fn parse_alter(&mut self) -> Result { let name = self.expect_ident()?; - let action = if self.match_kw("DROP") { + let action = if self.match_kw("ALTER") { + self.expect_kw("ATTR")?; + let attr = self.expect_ident()?; + let change = if self.match_kw("DROP") { + ArrayAttrMetaChange::Drop(self.parse_meta_drop()?) + } else { + self.expect_kw("SET")?; + ArrayAttrMetaChange::Set(self.parse_meta_set()?) + }; + AlterArrayAction::AttrMeta(ArrayAttrMetaAlter { attr, change }) + } else if self.match_kw("DROP") { self.expect_kw("GEOREF")?; AlterArrayAction::Georef(ArrayGeorefAlter::Drop) } else { diff --git a/nodedb-sql/src/parser/array_stmt/parse_impl/attr_meta.rs b/nodedb-sql/src/parser/array_stmt/parse_impl/attr_meta.rs new file mode 100644 index 000000000..b61ac445e --- /dev/null +++ b/nodedb-sql/src/parser/array_stmt/parse_impl/attr_meta.rs @@ -0,0 +1,221 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Attr metadata clauses: `NODATA n`, `SCALE n`, `OFFSET n`, `UNITS '..'`. +//! +//! `CREATE ARRAY` writes them after the attr type, space-separated: +//! `red UINT16 NODATA 0 SCALE 0.0000275 OFFSET -0.2 UNITS 'reflectance'`. +//! `ALTER ARRAY a ALTER ATTR red SET (NODATA 0, SCALE 0.5)` sets them, and +//! `ALTER ARRAY a ALTER ATTR red DROP NODATA, UNITS` removes them. +//! +//! A number is an integer or float literal, `NAN` or `INF`, or one of +//! `'nan'`, `'inf'`, `'-inf'` quoted. The lexer reads a leading `-` only +//! before a digit, so negative infinity takes the quoted form. The engine +//! checks that each value fits the attr type. + +use crate::error::Result; +use crate::parser::array_stmt::lexer::Tok; +use crate::types_array::{ArrayAttrMetaAst, ArrayAttrMetaKey}; + +use super::core::Parser; + +impl<'a> Parser<'a> { + /// The metadata key the next token names, without consuming it. + pub(super) fn peek_meta_key(&self) -> Option { + match self.peek() { + Some(Tok::Ident(s)) => meta_key(s), + _ => None, + } + } + + /// Parse one `KEY value` pair into `meta`. A key given twice is refused. + pub(super) fn parse_meta_pair(&mut self, meta: &mut ArrayAttrMetaAst) -> Result<()> { + let key = self.expect_meta_key()?; + if key.is_set(meta) { + return Err(self.err(format!("{} is given twice", key.sql_name()))); + } + match key { + ArrayAttrMetaKey::Nodata => meta.nodata = Some(self.parse_meta_number(key)?), + ArrayAttrMetaKey::Scale => meta.scale = Some(self.parse_meta_number(key)?), + ArrayAttrMetaKey::Offset => meta.offset = Some(self.parse_meta_number(key)?), + ArrayAttrMetaKey::Units => match self.bump().map(|t| &t.tok) { + Some(Tok::Str(s)) => meta.units = Some(s.clone()), + other => { + return Err(self.err(format!("UNITS expects a quoted string, got {other:?}"))); + } + }, + } + Ok(()) + } + + /// Parse `( KEY value [, KEY value]* )` after `SET`. + pub(super) fn parse_meta_set(&mut self) -> Result { + self.expect(&Tok::LParen)?; + let mut meta = ArrayAttrMetaAst::default(); + loop { + self.parse_meta_pair(&mut meta)?; + if !self.match_token(&Tok::Comma) { + break; + } + } + self.expect(&Tok::RParen)?; + Ok(meta) + } + + /// Parse `KEY [, KEY]*` after `DROP`. A key named twice is refused. + pub(super) fn parse_meta_drop(&mut self) -> Result> { + let mut keys: Vec = Vec::new(); + loop { + let key = self.expect_meta_key()?; + if keys.contains(&key) { + return Err(self.err(format!("DROP {} is given twice", key.sql_name()))); + } + keys.push(key); + if !self.match_token(&Tok::Comma) { + break; + } + } + Ok(keys) + } + + fn expect_meta_key(&mut self) -> Result { + match self.bump().map(|t| &t.tok) { + Some(Tok::Ident(s)) => meta_key(s).ok_or_else(|| { + self.err(format!( + "unknown attr metadata `{s}`; expected NODATA, SCALE, OFFSET or UNITS" + )) + }), + other => Err(self.err(format!( + "expected NODATA, SCALE, OFFSET or UNITS, got {other:?}" + ))), + } + } + + fn parse_meta_number(&mut self, key: ArrayAttrMetaKey) -> Result { + let named = |s: &str| match s.to_ascii_lowercase().as_str() { + "nan" => Some(f64::NAN), + "inf" | "infinity" | "+inf" | "+infinity" => Some(f64::INFINITY), + "-inf" | "-infinity" => Some(f64::NEG_INFINITY), + _ => None, + }; + let value = match self.bump().map(|t| &t.tok) { + Some(Tok::Int(n)) => Some(*n as f64), + Some(Tok::Float(f)) => Some(*f), + Some(Tok::Ident(s)) | Some(Tok::Str(s)) => named(s), + _ => None, + }; + value.ok_or_else(|| { + self.err(format!( + "{} expects a number, NAN, INF or '-inf'", + key.sql_name() + )) + }) + } +} + +fn meta_key(s: &str) -> Option { + match s.to_ascii_uppercase().as_str() { + "NODATA" => Some(ArrayAttrMetaKey::Nodata), + "SCALE" => Some(ArrayAttrMetaKey::Scale), + "OFFSET" => Some(ArrayAttrMetaKey::Offset), + "UNITS" => Some(ArrayAttrMetaKey::Units), + _ => None, + } +} + +#[cfg(test)] +mod tests { + use crate::parser::array_stmt::ast::{AlterArrayAction, ArrayStatement}; + use crate::parser::array_stmt::try_parse_array_statement; + use crate::types_array::{ + ArrayAttrMetaAlter, ArrayAttrMetaAst, ArrayAttrMetaChange, ArrayAttrMetaKey, ArrayAttrType, + }; + + fn create(attrs: &str) -> crate::parser::array_stmt::ast::CreateArrayAst { + let sql = format!( + "CREATE ARRAY scene DIMS (row INT64 [0..9], col INT64 [0..9]) ATTRS ({attrs}) \ + TILE_EXTENTS (10, 10)" + ); + match try_parse_array_statement(&sql).expect("parses") { + Some(ArrayStatement::Create(c)) => c, + other => panic!("expected CREATE, got {other:?}"), + } + } + + #[test] + fn create_reads_every_metadata_clause() { + let c = create( + "red UINT16 NODATA 0 SCALE 0.0000275 OFFSET -0.2 UNITS 'reflectance', \ + qa INT8 NOT NULL, sst FLOAT32 NODATA NAN", + ); + assert_eq!( + c.attrs[0].meta, + ArrayAttrMetaAst { + nodata: Some(0.0), + scale: Some(0.0000275), + offset: Some(-0.2), + units: Some("reflectance".into()), + } + ); + assert_eq!(c.attrs[1].dtype, ArrayAttrType::Int8); + assert!(!c.attrs[1].nullable); + assert_eq!(c.attrs[1].meta, ArrayAttrMetaAst::default()); + assert!(c.attrs[2].meta.nodata.is_some_and(f64::is_nan)); + } + + #[test] + fn not_null_and_metadata_mix_in_any_order() { + let c = create("b1 UINT16 NODATA 0 NOT NULL UNITS 'dn'"); + assert!(!c.attrs[0].nullable); + assert_eq!(c.attrs[0].meta.nodata, Some(0.0)); + assert_eq!(c.attrs[0].meta.units.as_deref(), Some("dn")); + } + + #[test] + fn a_repeated_or_malformed_clause_is_refused() { + for attrs in [ + "b1 UINT16 NODATA 0 NODATA 1", + "b1 UINT16 SCALE 'x'", + "b1 UINT16 UNITS 3", + ] { + let sql = + format!("CREATE ARRAY s DIMS (r INT64 [0..9]) ATTRS ({attrs}) TILE_EXTENTS (10)"); + assert!(try_parse_array_statement(&sql).is_err(), "{attrs}"); + } + } + + fn alter(sql: &str) -> AlterArrayAction { + match try_parse_array_statement(sql).expect("parses") { + Some(ArrayStatement::Alter(a)) => a.action, + other => panic!("expected ALTER, got {other:?}"), + } + } + + #[test] + fn alter_attr_sets_and_drops() { + assert_eq!( + alter("ALTER ARRAY scene ALTER ATTR red SET (NODATA 0, SCALE 0.5)"), + AlterArrayAction::AttrMeta(ArrayAttrMetaAlter { + attr: "red".into(), + change: ArrayAttrMetaChange::Set(ArrayAttrMetaAst { + nodata: Some(0.0), + scale: Some(0.5), + ..ArrayAttrMetaAst::default() + }), + }) + ); + assert_eq!( + alter("ALTER ARRAY scene ALTER ATTR red DROP NODATA, UNITS"), + AlterArrayAction::AttrMeta(ArrayAttrMetaAlter { + attr: "red".into(), + change: ArrayAttrMetaChange::Drop(vec![ + ArrayAttrMetaKey::Nodata, + ArrayAttrMetaKey::Units + ]), + }) + ); + assert!( + try_parse_array_statement("ALTER ARRAY s ALTER ATTR red DROP NODATA, NODATA").is_err() + ); + assert!(try_parse_array_statement("ALTER ARRAY s ALTER ATTR red SET (COLOR 1)").is_err()); + } +} diff --git a/nodedb-sql/src/parser/array_stmt/parse_impl/create.rs b/nodedb-sql/src/parser/array_stmt/parse_impl/create.rs index e5f48c94e..50a01e3fd 100644 --- a/nodedb-sql/src/parser/array_stmt/parse_impl/create.rs +++ b/nodedb-sql/src/parser/array_stmt/parse_impl/create.rs @@ -6,8 +6,8 @@ use crate::error::Result; use crate::parser::array_stmt::ast::CreateArrayAst; use crate::parser::array_stmt::lexer::Tok; use crate::types_array::{ - ArrayAttrAst, ArrayAttrType, ArrayCellOrderAst, ArrayDimAst, ArrayDimType, ArrayDomainBound, - ArrayTileOrderAst, + ArrayAttrAst, ArrayAttrMetaAst, ArrayAttrType, ArrayCellOrderAst, ArrayDimAst, ArrayDimType, + ArrayDomainBound, ArrayTileOrderAst, }; use super::core::Parser; @@ -176,18 +176,26 @@ impl<'a> Parser<'a> { let type_name = self.expect_ident()?; let dtype = parse_attr_type(&type_name) .ok_or_else(|| self.err(format!("unknown attr type `{type_name}`")))?; - let nullable = if self.match_kw("NOT") { - // The lexer reads `NULL` as its own token, never as an identifier. - self.expect(&Tok::Null)?; - false - } else { - // Default: nullable. - true - }; + // Default: nullable. `NOT NULL` and the metadata clauses follow the + // type in any order. + let mut nullable = true; + let mut meta = ArrayAttrMetaAst::default(); + loop { + if self.match_kw("NOT") { + // The lexer reads `NULL` as its own token, never as an identifier. + self.expect(&Tok::Null)?; + nullable = false; + } else if self.peek_meta_key().is_some() { + self.parse_meta_pair(&mut meta)?; + } else { + break; + } + } Ok(ArrayAttrAst { name, dtype, nullable, + meta, }) } @@ -245,10 +253,14 @@ fn parse_dim_type(s: &str) -> Option { } } -/// An attr type name. `INT` is the 4-byte `INT32`, as in PostgreSQL. A -/// bare `FLOAT` is the 8-byte `FLOAT64`. +/// An attr type name. The canonical names give the width in bits, so +/// `INT8` is the 1-byte signed type, like `UINT8`. The PostgreSQL aliases +/// give it in bytes: `INT2` is `INT16`, `INT4` is `INT32`, and an 8-byte +/// integer is `INT64` or `BIGINT`. `INT` is the 4-byte `INT32`, as in +/// PostgreSQL. A bare `FLOAT` is the 8-byte `FLOAT64`. fn parse_attr_type(s: &str) -> Option { match s.to_ascii_uppercase().as_str() { + "INT8" | "TINYINT" => Some(ArrayAttrType::Int8), "INT64" | "BIGINT" => Some(ArrayAttrType::Int64), "FLOAT64" | "DOUBLE" | "FLOAT" => Some(ArrayAttrType::Float64), "STRING" | "TEXT" | "VARCHAR" => Some(ArrayAttrType::String), diff --git a/nodedb-sql/src/parser/array_stmt/parse_impl/georef.rs b/nodedb-sql/src/parser/array_stmt/parse_impl/georef.rs index efdef8fe5..7cdfbdecc 100644 --- a/nodedb-sql/src/parser/array_stmt/parse_impl/georef.rs +++ b/nodedb-sql/src/parser/array_stmt/parse_impl/georef.rs @@ -2,9 +2,10 @@ //! `GEOREF (SRID n, X dim, Y dim [, TRANSFORM (gt0, .., gt5)])` clause parsing. //! -//! The parser checks the SRID against the supported set and the coefficient -//! count. The planner checks the dims and the coefficient values. +//! The parser checks the SRID code range and the coefficient count. The +//! planner checks the SRID is defined, the dims and the coefficient values. +use nodedb_crs::UnknownSrid; use nodedb_types::geometry::Srid; use crate::error::{Result, SqlError}; @@ -22,9 +23,11 @@ impl<'a> Parser<'a> { self.expect(&Tok::LParen)?; self.expect_kw("SRID")?; let code = self.expect_int()?; - let srid = Srid::from_code(code) - .map_err(|e| SqlError::DataException { - detail: format!("GEOREF: {e}"), + let srid = u32::try_from(code) + .ok() + .and_then(|c| Srid::new(c).ok()) + .ok_or_else(|| SqlError::DataException { + detail: format!("GEOREF: {}", UnknownSrid::OutOfRange { code }), })? .code(); self.expect(&Tok::Comma)?; @@ -137,15 +140,22 @@ mod tests { } #[test] - fn unsupported_srid_is_data_exception_naming_supported_codes() { - let err = create_georef("GEOREF (SRID 2154, X col, Y row)").unwrap_err(); + fn out_of_range_srid_is_data_exception() { + let err = create_georef("GEOREF (SRID 0, X col, Y row)").unwrap_err(); let SqlError::DataException { detail } = err else { panic!("expected DataException, got {err:?}"); }; - assert!(detail.contains("2154"), "{detail}"); - for code in ["4326", "32647", "32648", "3375", "3376"] { - assert!(detail.contains(code), "{detail}"); - } + assert!(detail.contains("SRID 0"), "{detail}"); + } + + /// The parser checks syntax only: an undefined code in range parses and + /// the planner refuses it. + #[test] + fn undefined_srid_in_range_parses() { + let g = create_georef("GEOREF (SRID 999, X col, Y row)") + .unwrap() + .unwrap(); + assert_eq!(g.srid, 999); } #[test] @@ -218,7 +228,7 @@ mod tests { #[test] fn alter_georef_errors() { - let err = alter("ALTER ARRAY scene SET GEOREF (SRID 27700, X col, Y row)").unwrap_err(); + let err = alter("ALTER ARRAY scene SET GEOREF (SRID 0, X col, Y row)").unwrap_err(); assert!(matches!(err, SqlError::DataException { .. }), "{err:?}"); for sql in [ "ALTER ARRAY scene DROP GEOREF extra", diff --git a/nodedb-sql/src/parser/array_stmt/parse_impl/mod.rs b/nodedb-sql/src/parser/array_stmt/parse_impl/mod.rs index 7769d845f..b67da94b3 100644 --- a/nodedb-sql/src/parser/array_stmt/parse_impl/mod.rs +++ b/nodedb-sql/src/parser/array_stmt/parse_impl/mod.rs @@ -3,6 +3,7 @@ //! `Parser` struct and recursive-descent parse methods for array statements. mod alter; +mod attr_meta; mod core; mod create; mod delete_stmt; diff --git a/nodedb-sql/src/parser/normalize.rs b/nodedb-sql/src/parser/normalize.rs index 66f0f6087..fd2d2c03a 100644 --- a/nodedb-sql/src/parser/normalize.rs +++ b/nodedb-sql/src/parser/normalize.rs @@ -81,13 +81,14 @@ pub fn normalize_insert_column(name: &sqlparser::ast::ObjectName) -> Result Result { @@ -106,6 +107,7 @@ fn normalize_table_name(name: &sqlparser::ast::ObjectName) -> Result { match parts[0].as_str() { "pg_catalog" => return Ok(parts[1].clone()), "_system" => return Ok(format!("_system.{}", parts[1])), + "nodedb" => return Ok(format!("nodedb.{}", parts[1])), _ => {} } } @@ -120,6 +122,7 @@ fn normalize_table_name(name: &sqlparser::ast::ObjectName) -> Result { /// /// - `pg_catalog.pg_class` → `pg_class`. /// - `_system.audit_log` → `_system.audit_log`. +/// - `nodedb.spatial_ref_sys` → `nodedb.spatial_ref_sys`. /// - Any other qualifier is rejected as schema-qualified. /// - An unqualified token decodes through [`check_identifier`]. /// @@ -139,6 +142,7 @@ pub fn check_relation_token(raw_name: &str) -> Result { match parts[0].as_str() { "pg_catalog" => return Ok(parts[1].clone()), "_system" => return Ok(format!("_system.{}", parts[1])), + "nodedb" => return Ok(format!("nodedb.{}", parts[1])), _ => {} } } @@ -298,7 +302,7 @@ mod tests { } #[test] - fn relation_tokens_accept_the_two_system_qualifiers() { + fn relation_tokens_accept_the_system_qualifiers() { assert_eq!( check_relation_token("_system.audit_log").expect("system relation"), "_system.audit_log" @@ -307,6 +311,10 @@ mod tests { check_relation_token("pg_catalog.pg_class").expect("catalog relation"), "pg_class" ); + assert_eq!( + check_relation_token("nodedb.spatial_ref_sys").expect("nodedb relation"), + "nodedb.spatial_ref_sys" + ); assert_eq!(check_relation_token("MiXeD").expect("bare"), "mixed"); assert_eq!(check_relation_token("\"MiXeD\"").expect("quoted"), "MiXeD"); } diff --git a/nodedb-sql/src/parser/preprocess/temporal.rs b/nodedb-sql/src/parser/preprocess/temporal.rs index 7645da315..ca71ded41 100644 --- a/nodedb-sql/src/parser/preprocess/temporal.rs +++ b/nodedb-sql/src/parser/preprocess/temporal.rs @@ -60,6 +60,24 @@ pub struct TemporalParseError(pub String); /// extracted `TemporalScope`. Returns `Ok(None)` when no temporal clause is /// present so the caller can short-circuit to the existing pipeline. pub fn extract(sql: &str) -> Result, TemporalParseError> { + extract_bounded(sql, ExprBoundary::SqlClause) +} + +/// Where an unquoted `AS OF` expression ends. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ExprBoundary { + /// At the next SQL clause keyword (`WHERE`, `LIMIT`, `ORDER`, ...). + SqlClause, + /// At the next whitespace. The graph DSL and MATCH use it: their clause + /// keywords (`DEPTH`, `LABEL`, `RETURN`, ...) are not SQL keywords. + Token, +} + +/// [`extract`] with the `AS OF` expression ending at `boundary`. +pub fn extract_bounded( + sql: &str, + boundary: ExprBoundary, +) -> Result, TemporalParseError> { let mut scope = TemporalScope::default(); let mut working = sql.to_string(); let mut any = false; @@ -82,7 +100,7 @@ pub fn extract(sql: &str) -> Result, TemporalParseError> { any = true; } // AS OF SYSTEM TIME (array read style, CockroachDB-inspired) - if let Some((rewritten, sys_scope)) = strip_as_of_system_time(&working)? { + if let Some((rewritten, sys_scope)) = strip_as_of_system_time(&working, boundary)? { if scope.system_time.is_temporal() { return Err(TemporalParseError( "multiple system-time AS OF clauses in one statement".into(), @@ -91,7 +109,7 @@ pub fn extract(sql: &str) -> Result, TemporalParseError> { working = rewritten; scope.system_time = sys_scope; any = true; - if strip_as_of_system_time(&working)?.is_some() { + if strip_as_of_system_time(&working, boundary)?.is_some() { return Err(TemporalParseError( "multiple system-time AS OF clauses in one statement".into(), )); @@ -104,7 +122,7 @@ pub fn extract(sql: &str) -> Result, TemporalParseError> { any = true; } // AS OF VALID TIME (array read style) - if let Some((rewritten, ms)) = strip_as_of_valid_time(&working)? { + if let Some((rewritten, ms)) = strip_as_of_valid_time(&working, boundary)? { if !matches!(scope.valid_time, ValidTime::Any) { return Err(TemporalParseError( "multiple valid-time AS OF clauses in one statement".into(), @@ -241,7 +259,7 @@ fn strip_valid_time(sql: &str) -> Result, TemporalPa /// /// Any other expression form is rejected with a descriptive error rather than /// silently defaulting, so callers always get exactly the version they asked for. -fn parse_temporal_expr(token: &str) -> Result { +pub(crate) fn parse_temporal_expr(token: &str) -> Result { let t = token.trim(); // Integer literal @@ -296,13 +314,14 @@ fn parse_temporal_expr(token: &str) -> Result { /// preprocessing in the same pipeline. fn strip_as_of_system_time( sql: &str, + boundary: ExprBoundary, ) -> Result, TemporalParseError> { let keyword = "AS OF SYSTEM TIME"; let Some(start) = keyword_position_outside_literals(sql, keyword) else { return Ok(None); }; let after_kw = start + keyword.len(); - let (scope, end_abs) = parse_as_of_expr(sql, after_kw)?; + let (scope, end_abs) = parse_as_of_expr(sql, after_kw, boundary)?; let mut out = String::with_capacity(sql.len()); out.push_str(sql[..start].trim_end()); out.push(' '); @@ -313,13 +332,16 @@ fn strip_as_of_system_time( /// Match `AS OF VALID TIME ` (case-insensitive) anywhere in `sql` and /// strip it. Same expression forms as `strip_as_of_system_time`. Returns `None` /// if the clause is absent. -fn strip_as_of_valid_time(sql: &str) -> Result, TemporalParseError> { +fn strip_as_of_valid_time( + sql: &str, + boundary: ExprBoundary, +) -> Result, TemporalParseError> { let keyword = "AS OF VALID TIME"; let Some(start) = keyword_position_outside_literals(sql, keyword) else { return Ok(None); }; let after_kw = start + keyword.len(); - let (scope, end_abs) = parse_as_of_expr(sql, after_kw)?; + let (scope, end_abs) = parse_as_of_expr(sql, after_kw, boundary)?; let ms = match scope { SystemTimeScope::AsOf(ms) => ms, SystemTimeScope::AllVersions => { @@ -334,6 +356,11 @@ fn strip_as_of_valid_time(sql: &str) -> Result, TemporalPa "AS OF VALID TIME requires an expression".into(), )); } + SystemTimeScope::Pinned { .. } => { + return Err(TemporalParseError( + "AS OF VALID TIME takes a time, not a pinned system-time ordinal".into(), + )); + } }; let mut out = String::with_capacity(sql.len()); out.push_str(sql[..start].trim_end()); @@ -351,6 +378,7 @@ fn strip_as_of_valid_time(sql: &str) -> Result, TemporalPa fn parse_as_of_expr( sql: &str, offset: usize, + boundary: ExprBoundary, ) -> Result<(SystemTimeScope, usize), TemporalParseError> { let rest = &sql[offset..]; let trimmed = rest.trim_start(); @@ -394,7 +422,12 @@ fn parse_as_of_expr( "OFFSET", ";", ]; - let mut end_rel = trimmed.len(); + let mut end_rel = match boundary { + ExprBoundary::SqlClause => trimmed.len(), + ExprBoundary::Token => trimmed + .find(|c: char| c.is_ascii_whitespace()) + .unwrap_or(trimmed.len()), + }; for stop in &stop_tokens { // Only match at a word boundary (preceded by whitespace or start). let mut search_from = 0; @@ -661,4 +694,37 @@ mod tests { "AS OF VALID TIME NULL must be rejected" ); } + + #[test] + fn token_boundary_ends_the_expression_at_whitespace() { + let ex = extract_bounded( + "GRAPH TRAVERSE IN 'g' FROM 'a' AS OF SYSTEM TIME 1700 DEPTH 2 \ + AS OF VALID TIME '2024-01-15T00:00:00Z' LABEL 'l'", + ExprBoundary::Token, + ) + .unwrap() + .unwrap(); + assert_eq!(ex.temporal.system_time, SystemTimeScope::AsOf(1700)); + assert_eq!(ex.temporal.valid_time, ValidTime::At(1_705_276_800_000)); + assert_eq!(ex.sql, "GRAPH TRAVERSE IN 'g' FROM 'a' DEPTH 2 LABEL 'l'"); + } + + #[test] + fn token_boundary_reads_null_and_now() { + let ex = extract_bounded( + "MATCH (a)-[:l]->(b) AS OF SYSTEM TIME NULL RETURN a", + ExprBoundary::Token, + ) + .unwrap() + .unwrap(); + assert_eq!(ex.temporal.system_time, SystemTimeScope::AllVersions); + assert_eq!(ex.sql, "MATCH (a)-[:l]->(b) RETURN a"); + let ex = extract_bounded( + "MATCH (a)-[:l]->(b) AS OF SYSTEM TIME NOW() RETURN a", + ExprBoundary::Token, + ) + .unwrap() + .unwrap(); + assert!(matches!(ex.temporal.system_time, SystemTimeScope::AsOf(_))); + } } diff --git a/nodedb-sql/src/planner/array_ddl.rs b/nodedb-sql/src/planner/array_ddl.rs index fff2d5cfd..5740e4462 100644 --- a/nodedb-sql/src/planner/array_ddl.rs +++ b/nodedb-sql/src/planner/array_ddl.rs @@ -16,9 +16,11 @@ use crate::error::{Result, SqlError}; use crate::parser::array_stmt::{AlterArrayAction, AlterArrayAst, CreateArrayAst, DropArrayAst}; use crate::planner::array_georef::check_georef; use crate::types::SqlPlan; -use crate::types_array::{ArrayDimType, ArrayDomainBound, ArrayGeorefAlter}; +use crate::types_array::{ + ArrayAttrMetaAlter, ArrayAttrMetaChange, ArrayDimType, ArrayDomainBound, ArrayGeorefAlter, +}; -pub fn plan_create_array(ast: &CreateArrayAst) -> Result { +pub fn plan_create_array(ast: &CreateArrayAst, catalog: &dyn SqlCatalog) -> Result { if ast.name.is_empty() { return Err(SqlError::Parse { detail: "CREATE ARRAY: array name must not be empty".into(), @@ -106,7 +108,7 @@ pub fn plan_create_array(ast: &CreateArrayAst) -> Result { }); } if let Some(georef) = &ast.georef { - check_georef(&ast.name, georef, &ast.dims)?; + check_georef(&ast.name, georef, &ast.dims, &catalog.crs_context())?; } Ok(SqlPlan::CreateArray(CreateArrayPlan { name: ast.name.clone(), @@ -131,9 +133,53 @@ pub fn plan_alter_array(ast: &AlterArrayAst, catalog: &dyn SqlCatalog) -> Result match &ast.action { AlterArrayAction::SetOptions(set) => plan_alter_options(&ast.name, set), AlterArrayAction::Georef(change) => plan_alter_georef(&ast.name, change, catalog), + AlterArrayAction::AttrMeta(change) => plan_alter_attr_meta(&ast.name, change, catalog), } } +/// `ALTER ARRAY ALTER ATTR SET (...)` or `DROP ...`, checked +/// against the array's attrs. The engine checks each value against the +/// attr type when it applies the change. +fn plan_alter_attr_meta( + name: &str, + change: &ArrayAttrMetaAlter, + catalog: &dyn SqlCatalog, +) -> Result { + let view = catalog + .lookup_array(name) + .ok_or_else(|| SqlError::UnknownTable { + name: name.to_string(), + })?; + let attr = view + .attrs + .iter() + .find(|a| a.name == change.attr) + .ok_or_else(|| SqlError::UnknownColumn { + table: name.to_string(), + column: change.attr.clone(), + })?; + if let ArrayAttrMetaChange::Drop(keys) = &change.change + && let Some(unset) = keys.iter().find(|k| !k.is_set(&attr.meta)) + { + return Err(SqlError::ObjectNotInPrerequisiteState { + object: format!("attr '{}' of array '{name}'", change.attr), + detail: format!( + "ALTER ARRAY {name} ALTER ATTR {} DROP {}: the attr has no {}", + change.attr, + unset.sql_name(), + unset.sql_name() + ), + }); + } + Ok(SqlPlan::AlterArray(AlterArrayPlan { + name: name.to_string(), + audit_retain_ms: None, + minimum_audit_retain_ms: None, + georef: None, + attr_meta: Some(change.clone()), + })) +} + /// `ALTER ARRAY SET GEOREF (...)` or `DROP GEOREF`, checked against /// the array's current dims and georef. fn plan_alter_georef( @@ -147,7 +193,9 @@ fn plan_alter_georef( name: name.to_string(), })?; match change { - ArrayGeorefAlter::Set(georef) => check_georef(name, georef, &view.dims)?, + ArrayGeorefAlter::Set(georef) => { + check_georef(name, georef, &view.dims, &catalog.crs_context())? + } ArrayGeorefAlter::Drop => { if view.georef.is_none() { return Err(SqlError::ObjectNotInPrerequisiteState { @@ -162,6 +210,7 @@ fn plan_alter_georef( audit_retain_ms: None, minimum_audit_retain_ms: None, georef: Some(change.clone()), + attr_meta: None, })) } @@ -208,6 +257,7 @@ fn plan_alter_options(name: &str, set: &[(String, Option)]) -> Result <0..1>`. +//! - `'histogram'` takes `bins => <1..65536>, lo => , hi => `, with +//! `lo < hi`. +//! - Every reducer takes `physical => true | false`. +//! +//! A parameter given to a reducer that takes none is refused, so a typo +//! never changes the answer silently. + +use sqlparser::ast; + +use super::helpers::{expect_bool, expect_f64, expect_u32}; +use crate::error::{Result, SqlError}; +use crate::types_array::{ArrayReducerAst, ArrayReducerName}; + +/// The named arguments `ARRAY_AGG` accepts. +pub(super) const AGG_NAMED_ARGS: &[&str] = &["physical", "p", "bins", "lo", "hi"]; + +/// The named arguments `ARRAY_SLICE` accepts. +pub(super) const SLICE_NAMED_ARGS: &[&str] = &["physical"]; + +/// The most bins a histogram takes. Mirrors +/// `nodedb_types::array_agg::MAX_HISTOGRAM_BINS`. +const MAX_HISTOGRAM_BINS: u32 = 65_536; + +/// The reducer and read mode of one `ARRAY_AGG` call. +#[derive(Debug, Clone, Copy, PartialEq)] +pub(super) struct AggOptions { + pub reducer: ArrayReducerAst, + pub physical: bool, +} + +/// The value of named argument `name`, if given. +fn named<'a>(args: &'a [(String, ast::Expr)], name: &str) -> Option<&'a ast::Expr> { + args.iter().find(|(n, _)| n == name).map(|(_, e)| e) +} + +/// `physical => `, false when absent. +pub(super) fn parse_physical(args: &[(String, ast::Expr)], fn_name: &str) -> Result { + named(args, "physical") + .map(|e| expect_bool(e, &format!("{fn_name} physical"))) + .transpose() + .map(|v| v.unwrap_or(false)) +} + +/// The reducer `reducer_str` names, with its named parameters. +pub(super) fn parse_agg_options( + reducer_str: &str, + args: &[(String, ast::Expr)], +) -> Result { + let name = ArrayReducerName::parse(reducer_str).ok_or_else(|| SqlError::Unsupported { + detail: format!( + "ARRAY_AGG: unknown reducer '{reducer_str}' (want {})", + ArrayReducerName::ACCEPTED + ), + })?; + let takes: &[&str] = match name { + ArrayReducerName::Percentile => &["p"], + ArrayReducerName::Histogram => &["bins", "lo", "hi"], + ArrayReducerName::Sum + | ArrayReducerName::Count + | ArrayReducerName::Min + | ArrayReducerName::Max + | ArrayReducerName::Mean + | ArrayReducerName::Median + | ArrayReducerName::StdDevPop + | ArrayReducerName::StdDevSamp + | ArrayReducerName::VarPop + | ArrayReducerName::VarSamp + | ArrayReducerName::Mode => &[], + }; + for param in ["p", "bins", "lo", "hi"] { + let given = named(args, param).is_some(); + let wanted = takes.contains(¶m); + if given && !wanted { + return Err(SqlError::Unsupported { + detail: format!("ARRAY_AGG: reducer '{reducer_str}' takes no '{param}' argument"), + }); + } + if wanted && !given { + return Err(SqlError::Unsupported { + detail: format!( + "ARRAY_AGG: reducer '{reducer_str}' needs {}", + takes + .iter() + .map(|t| format!("{t} => ")) + .collect::>() + .join(", ") + ), + }); + } + } + let number = |param: &str| -> Result { + let expr = named(args, param).ok_or_else(|| SqlError::Unsupported { + detail: format!("ARRAY_AGG: missing '{param}' argument"), + })?; + expect_f64(expr, &format!("ARRAY_AGG {param}")) + }; + let reducer = match name { + ArrayReducerName::Sum => ArrayReducerAst::Sum, + ArrayReducerName::Count => ArrayReducerAst::Count, + ArrayReducerName::Min => ArrayReducerAst::Min, + ArrayReducerName::Max => ArrayReducerAst::Max, + ArrayReducerName::Mean => ArrayReducerAst::Mean, + ArrayReducerName::Median => ArrayReducerAst::Median, + ArrayReducerName::StdDevPop => ArrayReducerAst::StdDevPop, + ArrayReducerName::StdDevSamp => ArrayReducerAst::StdDevSamp, + ArrayReducerName::VarPop => ArrayReducerAst::VarPop, + ArrayReducerName::VarSamp => ArrayReducerAst::VarSamp, + ArrayReducerName::Mode => ArrayReducerAst::Mode, + ArrayReducerName::Percentile => { + let p = number("p")?; + if !(0.0..=1.0).contains(&p) { + return Err(SqlError::DataException { + detail: format!("ARRAY_AGG: percentile p {p} must be between 0 and 1"), + }); + } + ArrayReducerAst::Percentile(p) + } + ArrayReducerName::Histogram => { + let bins_expr = named(args, "bins").ok_or_else(|| SqlError::Unsupported { + detail: "ARRAY_AGG: missing 'bins' argument".into(), + })?; + let bins = expect_u32(bins_expr, "ARRAY_AGG bins")?; + if bins == 0 || bins > MAX_HISTOGRAM_BINS { + return Err(SqlError::DataException { + detail: format!( + "ARRAY_AGG: histogram bins {bins} must be between 1 and \ + {MAX_HISTOGRAM_BINS}" + ), + }); + } + let (lo, hi) = (number("lo")?, number("hi")?); + if !lo.is_finite() || !hi.is_finite() || lo >= hi { + return Err(SqlError::DataException { + detail: format!( + "ARRAY_AGG: histogram range [{lo}, {hi}] must be finite with lo < hi" + ), + }); + } + ArrayReducerAst::Histogram { bins, lo, hi } + } + }; + Ok(AggOptions { + reducer, + physical: parse_physical(args, "ARRAY_AGG")?, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn num(v: &str) -> ast::Expr { + ast::Expr::Value(ast::Value::Number(v.into(), false).into()) + } + + fn args(pairs: &[(&str, ast::Expr)]) -> Vec<(String, ast::Expr)> { + pairs + .iter() + .map(|(n, e)| ((*n).to_string(), e.clone())) + .collect() + } + + #[test] + fn reducer_names_parse_with_their_parameters() { + let ok = |s: &str, a: &[(&str, ast::Expr)]| parse_agg_options(s, &args(a)).expect(s); + assert_eq!(ok("median", &[]).reducer, ArrayReducerAst::Median); + assert_eq!(ok("stddev", &[]).reducer, ArrayReducerAst::StdDevSamp); + assert_eq!(ok("variance", &[]).reducer, ArrayReducerAst::VarSamp); + assert_eq!(ok("VAR_POP", &[]).reducer, ArrayReducerAst::VarPop); + assert_eq!( + ok("percentile", &[("p", num("0.9"))]).reducer, + ArrayReducerAst::Percentile(0.9) + ); + let neg = ast::Expr::UnaryOp { + op: ast::UnaryOperator::Minus, + expr: Box::new(num("1")), + }; + assert_eq!( + ok( + "histogram", + &[("bins", num("4")), ("lo", neg), ("hi", num("1"))] + ) + .reducer, + ArrayReducerAst::Histogram { + bins: 4, + lo: -1.0, + hi: 1.0 + } + ); + let physical = ast::Expr::Value(ast::Value::Boolean(true).into()); + assert!(ok("sum", &[("physical", physical)]).physical); + } + + #[test] + fn wrong_or_missing_parameters_are_refused() { + let err = |s: &str, a: &[(&str, ast::Expr)]| parse_agg_options(s, &args(a)).is_err(); + assert!(err("percentile", &[])); + assert!(err("percentile", &[("p", num("1.5"))])); + assert!(err("median", &[("p", num("0.5"))])); + assert!(err("histogram", &[("bins", num("4")), ("lo", num("0"))])); + assert!(err( + "histogram", + &[("bins", num("0")), ("lo", num("0")), ("hi", num("1"))] + )); + assert!(err( + "histogram", + &[("bins", num("2")), ("lo", num("1")), ("hi", num("1"))] + )); + assert!(err("geomean", &[])); + assert!(err("sum", &[("physical", num("1"))])); + } +} diff --git a/nodedb-sql/src/planner/array_fn/helpers.rs b/nodedb-sql/src/planner/array_fn/helpers.rs index d4a2a9874..7ff99d523 100644 --- a/nodedb-sql/src/planner/array_fn/helpers.rs +++ b/nodedb-sql/src/planner/array_fn/helpers.rs @@ -14,13 +14,131 @@ use crate::parser::object_literal::parse_object_literal_complete; use crate::types::SqlCatalog; use crate::types_array::{ArrayCoordLiteral, ArraySliceAst, NamedDimRange}; -pub(super) fn collect_args(args: &[ast::FunctionArg]) -> Vec { - args.iter() - .filter_map(|a| match a { - ast::FunctionArg::Unnamed(ast::FunctionArgExpr::Expr(e)) => Some(e.clone()), - _ => None, - }) - .collect() +/// The positional arguments of a call to `fn_name`, which takes no named +/// argument. A named argument, or a wildcard, is refused. +pub(super) fn collect_args(args: &[ast::FunctionArg], fn_name: &str) -> Result> { + let (positional, named) = split_args(args, fn_name)?; + if let Some((name, _)) = named.first() { + return Err(SqlError::Unsupported { + detail: format!("{fn_name}: takes no named argument, got '{name}'"), + }); + } + Ok(positional) +} + +/// `name => value` arguments in call order, names lower-cased. +pub(super) type NamedArgs = Vec<(String, ast::Expr)>; + +/// The positional and the `name => value` arguments of a call to +/// `fn_name`, each in call order. Names are lower-cased. A wildcard +/// argument, a name that is not an identifier, or `=` in place of `=>` is +/// refused. +pub(super) fn split_args( + args: &[ast::FunctionArg], + fn_name: &str, +) -> Result<(Vec, NamedArgs)> { + let mut positional = Vec::new(); + let mut named: NamedArgs = Vec::new(); + for arg in args { + let (name, value, operator) = match arg { + ast::FunctionArg::Unnamed(ast::FunctionArgExpr::Expr(e)) => { + positional.push(e.clone()); + continue; + } + ast::FunctionArg::Unnamed(other) => { + return Err(SqlError::Unsupported { + detail: format!("{fn_name}: argument {other} is not a value"), + }); + } + ast::FunctionArg::Named { + name, + arg, + operator, + } => (name.value.to_ascii_lowercase(), arg, operator), + ast::FunctionArg::ExprNamed { + name: ast::Expr::Identifier(ident), + arg, + operator, + } => (ident.value.to_ascii_lowercase(), arg, operator), + ast::FunctionArg::ExprNamed { name, .. } => { + return Err(SqlError::Unsupported { + detail: format!("{fn_name}: argument name {name} is not an identifier"), + }); + } + }; + if *operator != ast::FunctionArgOperator::RightArrow { + return Err(SqlError::Unsupported { + detail: format!("{fn_name}: use '=>' not '{operator}' for named argument '{name}'"), + }); + } + let ast::FunctionArgExpr::Expr(value) = value else { + return Err(SqlError::Unsupported { + detail: format!("{fn_name}: named argument '{name}' expects a value"), + }); + }; + if named.iter().any(|(n, _)| *n == name) { + return Err(SqlError::Unsupported { + detail: format!("{fn_name}: named argument '{name}' is given twice"), + }); + } + named.push((name, value.clone())); + } + Ok((positional, named)) +} + +/// The named arguments of a call to `fn_name`, each name checked against +/// the closed set `allowed`. An unknown name is refused, naming the set. +pub(super) fn collect_named_args( + named: NamedArgs, + fn_name: &str, + allowed: &[&str], +) -> Result { + if let Some((name, _)) = named.iter().find(|(n, _)| !allowed.contains(&n.as_str())) { + let detail = if allowed.is_empty() { + format!("{fn_name}: takes no named argument, got '{name}'") + } else { + format!( + "{fn_name}: unknown named argument '{name}'; valid: {}", + allowed.join(", ") + ) + }; + return Err(SqlError::Unsupported { detail }); + } + Ok(named) +} + +/// A boolean literal. +pub(super) fn expect_bool(expr: &ast::Expr, ctx: &str) -> Result { + match expr { + ast::Expr::Value(v) => match &v.value { + ast::Value::Boolean(b) => Ok(*b), + other => Err(SqlError::TypeMismatch { + detail: format!("{ctx}: expected true or false, got {other}"), + }), + }, + _ => Err(SqlError::TypeMismatch { + detail: format!("{ctx}: expected true or false, got {expr}"), + }), + } +} + +/// A numeric literal, with an optional leading minus. +pub(super) fn expect_f64(expr: &ast::Expr, ctx: &str) -> Result { + let mismatch = || SqlError::TypeMismatch { + detail: format!("{ctx}: expected a number, got {expr}"), + }; + match expr { + ast::Expr::Value(v) => match &v.value { + ast::Value::Number(n, _) => n.parse::().map_err(|_| mismatch()), + _ => Err(mismatch()), + }, + ast::Expr::UnaryOp { + op: ast::UnaryOperator::Minus, + expr: inner, + } => expect_f64(inner, ctx).map(|v| -v), + ast::Expr::Nested(inner) => expect_f64(inner, ctx), + _ => Err(mismatch()), + } } pub(super) fn require_array_name( @@ -99,7 +217,7 @@ pub(super) fn expect_string_array(expr: &ast::Expr, ctx: &str) -> Result { match &f.args { - ast::FunctionArguments::List(list) => collect_args(&list.args), + ast::FunctionArguments::List(list) => collect_args(&list.args, ctx)?, _ => Vec::new(), } } diff --git a/nodedb-sql/src/planner/array_fn/maint_fn.rs b/nodedb-sql/src/planner/array_fn/maint_fn.rs index 195292ac5..495f9a087 100644 --- a/nodedb-sql/src/planner/array_fn/maint_fn.rs +++ b/nodedb-sql/src/planner/array_fn/maint_fn.rs @@ -28,21 +28,20 @@ pub fn try_plan_array_maint_fn( _ => return Ok(None), }; let fn_name = crate::parser::normalize::normalize_object_name_checked(&func.name)?; + let sql_name = match fn_name.as_str() { + "array_flush" => "ARRAY_FLUSH", + "array_compact" => "ARRAY_COMPACT", + _ => return Ok(None), + }; let arg_exprs = match &func.args { - ast::FunctionArguments::List(list) => collect_args(&list.args), + ast::FunctionArguments::List(list) => collect_args(&list.args, sql_name)?, _ => Vec::new(), }; - match fn_name.as_str() { - "array_flush" => { - let name = require_array_name(&arg_exprs, 0, "ARRAY_FLUSH", catalog)?; - Ok(Some(SqlPlan::ArrayFlush { name })) - } - "array_compact" => { - let name = require_array_name(&arg_exprs, 0, "ARRAY_COMPACT", catalog)?; - Ok(Some(SqlPlan::ArrayCompact { name })) - } - _ => Ok(None), - } + let name = require_array_name(&arg_exprs, 0, sql_name, catalog)?; + Ok(Some(match sql_name { + "ARRAY_FLUSH" => SqlPlan::ArrayFlush { name }, + _ => SqlPlan::ArrayCompact { name }, + })) } #[cfg(test)] @@ -101,11 +100,13 @@ mod tests { name: "variant".into(), dtype: ArrayAttrType::String, nullable: true, + meta: Default::default(), }, ArrayAttrAst { name: "qual".into(), dtype: ArrayAttrType::Float64, nullable: true, + meta: Default::default(), }, ], tile_extents: vec![1, 1_000_000], diff --git a/nodedb-sql/src/planner/array_fn/mod.rs b/nodedb-sql/src/planner/array_fn/mod.rs index d8cb5b395..29037b3d0 100644 --- a/nodedb-sql/src/planner/array_fn/mod.rs +++ b/nodedb-sql/src/planner/array_fn/mod.rs @@ -14,6 +14,7 @@ //! [`try_plan_array_maint_fn`] intercepts these from the constant- //! query path. +mod agg_args; mod helpers; mod maint_fn; mod pushdown; diff --git a/nodedb-sql/src/planner/array_fn/pushdown.rs b/nodedb-sql/src/planner/array_fn/pushdown.rs index b4432f1cd..42fcbaad5 100644 --- a/nodedb-sql/src/planner/array_fn/pushdown.rs +++ b/nodedb-sql/src/planner/array_fn/pushdown.rs @@ -340,6 +340,7 @@ mod tests { name: "nir".into(), dtype: ArrayAttrType::Float64, nullable: true, + meta: Default::default(), }], tile_extents: vec![2, 2], georef: None, @@ -354,6 +355,7 @@ mod tests { limit, zone: None, temporal: TemporalScope::default(), + physical: false, } } diff --git a/nodedb-sql/src/planner/array_fn/table_fn.rs b/nodedb-sql/src/planner/array_fn/table_fn.rs index 940179be2..eea7dd975 100644 --- a/nodedb-sql/src/planner/array_fn/table_fn.rs +++ b/nodedb-sql/src/planner/array_fn/table_fn.rs @@ -6,16 +6,17 @@ use crate::types::{ArrayAggPlan, ArrayElementwisePlan, ArraySlicePlan}; use sqlparser::ast; +use super::agg_args::{AGG_NAMED_ARGS, SLICE_NAMED_ARGS, parse_agg_options, parse_physical}; use super::helpers::{ - collect_args, expect_string_array, expect_string_literal, expect_u32, is_null_literal, - parse_slice_arg, require_array_name, + collect_named_args, expect_string_array, expect_string_literal, expect_u32, is_null_literal, + parse_slice_arg, require_array_name, split_args, }; use super::zone::parse_optional_zone; use crate::error::{Result, SqlError}; use crate::resolver::array_tvf::check_agg_reducer; use crate::temporal::TemporalScope; use crate::types::{SqlCatalog, SqlPlan}; -use crate::types_array::{ArrayBinaryOpAst, ArrayReducerAst, ArraySliceAst}; +use crate::types_array::{ArrayBinaryOpAst, ArraySliceAst}; /// One planned array table-valued function call. /// @@ -106,26 +107,34 @@ pub fn try_plan_array_table_fn( _ => return Ok(None), }; let fn_name = crate::parser::normalize::normalize_object_name_checked(name)?; - let arg_exprs = collect_args(&args.args); + let (sql_name, allowed): (&str, &[&str]) = match fn_name.as_str() { + "array_slice" => ("ARRAY_SLICE", SLICE_NAMED_ARGS), + "array_project" => ("ARRAY_PROJECT", &[]), + "array_agg" => ("ARRAY_AGG", AGG_NAMED_ARGS), + "array_elementwise" => ("ARRAY_ELEMENTWISE", &[]), + _ => return Ok(None), + }; + let (arg_exprs, named) = split_args(&args.args, sql_name)?; + let named = collect_named_args(named, sql_name, allowed)?; match fn_name.as_str() { "array_slice" => Ok(Some(ArrayTableFn::Slice(plan_slice( - &arg_exprs, catalog, temporal, + &arg_exprs, &named, catalog, temporal, )?))), "array_project" => Ok(Some(ArrayTableFn::Project(plan_project( &arg_exprs, catalog, temporal, )?))), "array_agg" => Ok(Some(ArrayTableFn::Agg(plan_agg( - &arg_exprs, catalog, temporal, + &arg_exprs, &named, catalog, temporal, )?))), - "array_elementwise" => Ok(Some(ArrayTableFn::Elementwise(plan_elementwise( + _ => Ok(Some(ArrayTableFn::Elementwise(plan_elementwise( &arg_exprs, catalog, temporal, )?))), - _ => Ok(None), } } fn plan_slice( args: &[ast::Expr], + named: &[(String, ast::Expr)], catalog: &dyn SqlCatalog, temporal: TemporalScope, ) -> Result { @@ -173,7 +182,7 @@ fn plan_slice( Some(arg) if !is_null_literal(arg) => expect_u32(arg, "ARRAY_SLICE limit")?, _ => 0, }; - let zone = parse_optional_zone("ARRAY_SLICE", args, 4, &name, &view)?; + let zone = parse_optional_zone("ARRAY_SLICE", args, 4, &name, &view, &catalog.crs_context())?; Ok(ArraySlicePlan { name, @@ -182,6 +191,7 @@ fn plan_slice( limit, zone, temporal, + physical: parse_physical(named, "ARRAY_SLICE")?, }) } @@ -224,11 +234,13 @@ fn plan_project( limit: 0, zone: None, temporal, + physical: false, }) } fn plan_agg( args: &[ast::Expr], + named: &[(String, ast::Expr)], catalog: &dyn SqlCatalog, temporal: TemporalScope, ) -> Result { @@ -264,9 +276,8 @@ fn plan_agg( })?; let reducer_str = expect_string_literal(&args[2], "ARRAY_AGG reducer")?; - let reducer = ArrayReducerAst::parse(&reducer_str).ok_or_else(|| SqlError::Unsupported { - detail: format!("ARRAY_AGG: unknown reducer '{reducer_str}' (want sum/count/min/max/mean)"), - })?; + let options = parse_agg_options(&reducer_str, named)?; + let reducer = options.reducer; check_agg_reducer(reducer, &attr, attr_type)?; let group_by_dim = match args.get(3) { @@ -287,7 +298,7 @@ fn plan_agg( _ => ArraySliceAst::default(), }; - let zone = parse_optional_zone("ARRAY_AGG", args, 5, &name, &view)?; + let zone = parse_optional_zone("ARRAY_AGG", args, 5, &name, &view, &catalog.crs_context())?; Ok(ArrayAggPlan { name, @@ -297,6 +308,7 @@ fn plan_agg( slice, zone, temporal, + physical: options.physical, }) } @@ -419,11 +431,13 @@ mod tests { name: "variant".into(), dtype: ArrayAttrType::String, nullable: true, + meta: Default::default(), }, ArrayAttrAst { name: "qual".into(), dtype: ArrayAttrType::Float64, nullable: true, + meta: Default::default(), }, ], tile_extents: vec![1, 1_000_000], @@ -567,6 +581,37 @@ mod tests { } } + #[test] + fn agg_named_args_parse_and_unknown_ones_are_refused() { + let p = plan_one( + "SELECT * FROM ARRAY_AGG('g', 'qual', 'percentile', 'chrom', p => 0.9, physical => true)", + ) + .unwrap(); + match relation_body(p) { + SqlPlan::ArrayAgg(ArrayAggPlan { + reducer, physical, .. + }) => { + assert_eq!(reducer, ArrayReducerAst::Percentile(0.9)); + assert!(physical); + } + other => panic!("expected ArrayAgg, got {other:?}"), + } + for sql in [ + "SELECT * FROM ARRAY_AGG('g', 'qual', 'sum', colour => 1)", + "SELECT * FROM ARRAY_AGG('g', 'qual', 'sum', p => 0.5)", + "SELECT * FROM ARRAY_SLICE('g', '{chrom: [1, 1]}', p => 0.5)", + "SELECT * FROM ARRAY_PROJECT('g', ARRAY['qual'], physical => true)", + ] { + assert!(plan_one(sql).is_err(), "{sql}"); + } + let p = plan_one("SELECT * FROM ARRAY_SLICE('g', '{chrom: [1, 1]}', physical => true)") + .unwrap(); + match relation_body(p) { + SqlPlan::ArraySlice(ArraySlicePlan { physical, .. }) => assert!(physical), + other => panic!("expected ArraySlice, got {other:?}"), + } + } + #[test] fn agg_grouped() { let p = plan_one("SELECT * FROM ARRAY_AGG('g', 'qual', 'mean', 'chrom')").unwrap(); diff --git a/nodedb-sql/src/planner/array_fn/zone.rs b/nodedb-sql/src/planner/array_fn/zone.rs index 54157b174..4ec3f3745 100644 --- a/nodedb-sql/src/planner/array_fn/zone.rs +++ b/nodedb-sql/src/planner/array_fn/zone.rs @@ -12,7 +12,8 @@ use sqlparser::ast; -use nodedb_types::geometry::{Geometry, Srid}; +use nodedb_crs::CrsContext; +use nodedb_types::geometry::Geometry; use super::helpers::{expect_string_array, expect_string_literal, is_null_literal}; use crate::catalog::ArrayCatalogView; @@ -27,10 +28,11 @@ pub(super) fn parse_optional_zone( at: usize, name: &str, view: &ArrayCatalogView, + crs: &CrsContext<'_>, ) -> Result> { match (args.get(at), args.get(at + 1)) { (Some(wkt), Some(dims)) if !is_null_literal(wkt) => { - parse_zone_args(func, wkt, dims, name, view).map(Some) + parse_zone_args(func, wkt, dims, name, view, crs).map(Some) } (Some(_), Some(dims)) if !is_null_literal(dims) => Err(SqlError::Unsupported { detail: format!("{func}: zone_dims needs a zone_wkt; pass both, or neither"), @@ -46,22 +48,26 @@ fn parse_zone_args( dims_expr: &ast::Expr, name: &str, view: &ArrayCatalogView, + crs: &CrsContext<'_>, ) -> Result { let wkt = expect_string_literal(wkt_expr, &format!("{func} zone"))?; - let mut ewkt = - nodedb_spatial::geometry_from_ewkt(&wkt).ok_or_else(|| SqlError::InvalidFunction { - detail: format!("{func}: zone is not valid WKT or EWKT: {wkt}"), - })?; + let ewkt = nodedb_spatial::geometry_from_ewkt(&wkt).map_err(|e| SqlError::InvalidFunction { + detail: format!("{func}: zone is not valid WKT or EWKT: {e}"), + })?; + let ewkt_srid = ewkt.srid; + // A zone is a 2D region: Z and M do not take part in membership. + let (_, mut zone, _) = ewkt.geometry.into_parts(); // Membership is an even-odd test, so ring direction never changes it. // Orienting first accepts a zone drawn clockwise. - nodedb_spatial::orient_rings(&mut ewkt.geometry); - let issues = nodedb_spatial::validate::validate_geometry(&ewkt.geometry); + nodedb_spatial::orient_rings(&mut zone); + let issues = nodedb_spatial::validate::validate_geometry(&zone); if !issues.is_empty() { + let reasons: Vec = issues.iter().map(ToString::to_string).collect(); return Err(SqlError::InvalidFunction { - detail: format!("{func}: invalid zone: {}", issues.join("; ")), + detail: format!("{func}: invalid zone: {}", reasons.join("; ")), }); } - let polygons = match ewkt.geometry { + let polygons = match zone { Geometry::Polygon { coordinates } => vec![coordinates], Geometry::MultiPolygon { coordinates } => coordinates, other => { @@ -81,7 +87,7 @@ fn parse_zone_args( &format!("{func} zone dims"), )?) }; - let (x_dim, y_dim, srid) = match ewkt.srid { + let (x_dim, y_dim, srid) = match ewkt_srid { None => { let dims = dims.ok_or_else(|| SqlError::Unsupported { detail: format!( @@ -93,9 +99,11 @@ fn parse_zone_args( (x_dim, y_dim, None) } Some(code) => { - let srid = Srid::from_code(code).map_err(|e| SqlError::DataException { - detail: format!("{func}: zone SRID: {e}"), - })?; + let srid = crs + .resolve_code(code) + .map_err(|e| SqlError::DataException { + detail: format!("{func}: zone SRID: {e}"), + })?; let (x_dim, y_dim) = georef_axes(func, name, view, dims)?; (x_dim, y_dim, Some(srid.code())) } @@ -227,6 +235,7 @@ mod tests { name: "b1".into(), dtype: ArrayAttrType::Float64, nullable: true, + meta: Default::default(), }], tile_extents: vec![64, 64], georef, @@ -343,10 +352,10 @@ mod tests { } #[test] - fn an_unsupported_zone_srid_is_a_data_exception() { + fn an_undefined_zone_srid_is_a_data_exception() { let err = plan( "SELECT * FROM ARRAY_AGG('img', 'b1', 'sum', NULL, NULL, \ - 'SRID=2154;POLYGON((0 0, 1 0, 1 1, 0 0))', NULL)", + 'SRID=999;POLYGON((0 0, 1 0, 1 1, 0 0))', NULL)", ) .expect_err("refused"); assert!(matches!(err, SqlError::DataException { .. }), "{err:?}"); diff --git a/nodedb-sql/src/planner/array_georef.rs b/nodedb-sql/src/planner/array_georef.rs index a7f3429a9..ed8e9dc09 100644 --- a/nodedb-sql/src/planner/array_georef.rs +++ b/nodedb-sql/src/planner/array_georef.rs @@ -5,21 +5,28 @@ //! The same rules run again in the engine schema builder. This copy //! reports each refusal with its own SQLSTATE class. //! -//! - SRID outside the supported set: `22000`. +//! - SRID no CRS defines: `22000`. //! - X or Y names no dim: `42703`. //! - X equals Y, wrong dim type, or bad coefficients: `22000`. -use nodedb_types::geometry::Srid; +use nodedb_crs::CrsContext; use crate::error::{Result, SqlError}; use crate::types_array::{ArrayDimAst, ArrayDimType, ArrayGeorefAst}; -/// Check `georef` against the dims of array `array`. -pub fn check_georef(array: &str, georef: &ArrayGeorefAst, dims: &[ArrayDimAst]) -> Result<()> { +/// Check `georef` against the dims of array `array`. The SRID must resolve +/// in `crs`. +pub fn check_georef( + array: &str, + georef: &ArrayGeorefAst, + dims: &[ArrayDimAst], + crs: &CrsContext<'_>, +) -> Result<()> { let data = |detail: String| SqlError::DataException { detail: format!("array '{array}' GEOREF: {detail}"), }; - Srid::try_from(georef.srid).map_err(|e| data(e.to_string()))?; + crs.resolve_u32(georef.srid) + .map_err(|e| data(e.to_string()))?; let dim = |name: &str| { dims.iter() .find(|d| d.name == name) @@ -84,6 +91,10 @@ fn dim_type_name(dtype: ArrayDimType) -> &'static str { #[cfg(test)] mod tests { use super::*; + + fn crs() -> CrsContext<'static> { + CrsContext::builtin_only() + } use crate::types_array::ArrayDomainBound; fn dim(name: &str, dtype: ArrayDimType) -> ArrayDimAst { @@ -130,33 +141,38 @@ mod tests { #[test] fn accepts_pixel_and_world() { - assert!(check_georef("a", &georef(Some(GT)), &ints()).is_ok()); + assert!(check_georef("a", &georef(Some(GT)), &ints(), &crs()).is_ok()); let world = vec![ dim("row", ArrayDimType::Float64), dim("col", ArrayDimType::Int64), ]; - assert!(check_georef("a", &georef(None), &world).is_ok()); + assert!(check_georef("a", &georef(None), &world, &crs()).is_ok()); } #[test] - fn unsupported_srid_is_22000() { + fn undefined_srid_is_22000() { let mut g = georef(None); - g.srid = 2154; - let err = check_georef("a", &g, &ints()).unwrap_err(); + g.srid = 999; + let err = check_georef("a", &g, &ints(), &crs()).unwrap_err(); let SqlError::DataException { detail } = err else { panic!("expected DataException, got {err:?}"); }; - assert!( - detail.contains("4326, 32647, 32648, 3375, 3376"), - "{detail}" - ); + assert!(detail.contains("SRID 999 is not defined"), "{detail}"); + } + + /// Any EPSG projected CRS is accepted, such as UTM 50N for Sabah. + #[test] + fn epsg_projected_srid_is_accepted() { + let mut g = georef(Some(GT)); + g.srid = 32650; + assert!(check_georef("a", &g, &ints(), &crs()).is_ok()); } #[test] fn unknown_dim_is_42703() { let mut g = georef(None); g.y_dim = "lat".into(); - let err = check_georef("a", &g, &ints()).unwrap_err(); + let err = check_georef("a", &g, &ints(), &crs()).unwrap_err(); assert_eq!( err, SqlError::UnknownColumn { @@ -170,7 +186,7 @@ mod tests { fn same_dim_is_22000() { let mut g = georef(None); g.y_dim = "col".into(); - let err = check_georef("a", &g, &ints()).unwrap_err(); + let err = check_georef("a", &g, &ints(), &crs()).unwrap_err(); assert!(matches!(err, SqlError::DataException { .. }), "{err:?}"); } @@ -180,19 +196,19 @@ mod tests { dim("row", ArrayDimType::String), dim("col", ArrayDimType::Int64), ]; - let err = check_georef("a", &georef(None), &strings).unwrap_err(); + let err = check_georef("a", &georef(None), &strings, &crs()).unwrap_err(); assert!(matches!(err, SqlError::DataException { .. }), "{err:?}"); let floats = vec![ dim("row", ArrayDimType::Float64), dim("col", ArrayDimType::Float64), ]; - let err = check_georef("a", &georef(Some(GT)), &floats).unwrap_err(); + let err = check_georef("a", &georef(Some(GT)), &floats, &crs()).unwrap_err(); assert!(matches!(err, SqlError::DataException { .. }), "{err:?}"); let times = vec![ dim("row", ArrayDimType::TimestampMs), dim("col", ArrayDimType::Int64), ]; - let err = check_georef("a", &georef(None), ×).unwrap_err(); + let err = check_georef("a", &georef(None), ×, &crs()).unwrap_err(); assert!(matches!(err, SqlError::DataException { .. }), "{err:?}"); } @@ -205,7 +221,7 @@ mod tests { [0.0, 1.0, 2.0, 0.0, 2.0, 4.0], [0.0, 1e300, 0.0, 0.0, 0.0, 1e300], ] { - let err = check_georef("a", &georef(Some(gt)), &ints()).unwrap_err(); + let err = check_georef("a", &georef(Some(gt)), &ints(), &crs()).unwrap_err(); assert!(matches!(err, SqlError::DataException { .. }), "{err:?}"); } } diff --git a/nodedb-sql/src/planner/bitmap_emit/predicate.rs b/nodedb-sql/src/planner/bitmap_emit/predicate.rs index 89a2e3c7d..0fb787a44 100644 --- a/nodedb-sql/src/planner/bitmap_emit/predicate.rs +++ b/nodedb-sql/src/planner/bitmap_emit/predicate.rs @@ -36,22 +36,23 @@ pub struct BitmapHint { pub primary_value: SqlValue, /// Additional values for IN-list expansion (empty for equality). pub extra_values: Vec, - /// The side's system-time cutoff, `None` for the current state. The - /// bitmap reads the versioned index at this cutoff, so it holds the rows + /// The side's system-time scope: `Current` or a point-in-time cut. The + /// bitmap reads the versioned index at this scope, so it holds the rows /// the side reads. - pub system_as_of_ms: Option, + pub system_time: SystemTimeScope, } -/// The system-time cutoff a side's bitmap reads at. `None` inside the outer -/// `Option` when the index cannot answer the side's scope: it holds no valid -/// time and lists no history, so such a side takes no pushdown. -fn bitmap_cutoff(temporal: &TemporalScope) -> Option> { +/// The system-time scope a side's bitmap reads at. `None` when the index +/// cannot answer the side's scope: it holds no valid time and lists no +/// history, so such a side takes no pushdown. +fn bitmap_cutoff(temporal: &TemporalScope) -> Option { if !matches!(temporal.valid_time, ValidTime::Any) { return None; } match temporal.system_time { - SystemTimeScope::Current => Some(None), - SystemTimeScope::AsOf(ms) => Some(Some(ms)), + scope @ (SystemTimeScope::Current + | SystemTimeScope::AsOf(_) + | SystemTimeScope::Pinned { .. }) => Some(scope), SystemTimeScope::AllVersions => None, } } @@ -75,7 +76,7 @@ pub fn analyze(plan: &SqlPlan) -> Option { field: field.clone(), primary_value: value.clone(), extra_values: Vec::new(), - system_as_of_ms: bitmap_cutoff(temporal)?, + system_time: bitmap_cutoff(temporal)?, }), // Plain scan: inspect WHERE filters for indexed-column equality / IN-list. @@ -85,7 +86,7 @@ pub fn analyze(plan: &SqlPlan) -> Option { temporal, .. } => { - let system_as_of_ms = bitmap_cutoff(temporal)?; + let system_time = bitmap_cutoff(temporal)?; // The `SqlPlan::Scan` does not directly carry `IndexSpec` list — that // lives on `TableInfo` which the engine-rules layer consumed when it // chose *not* to rewrite to `DocumentIndexLookup`. However, when the @@ -100,7 +101,7 @@ pub fn analyze(plan: &SqlPlan) -> Option { // index-backed) is emitted unconditionally. // // Future: if `Scan` carries index metadata, extend this arm. - analyze_scan_filters(collection, filters, system_as_of_ms) + analyze_scan_filters(collection, filters, system_time) } _ => None, @@ -118,7 +119,7 @@ pub fn analyze(plan: &SqlPlan) -> Option { fn analyze_scan_filters( collection: &str, filters: &[crate::types::Filter], - system_as_of_ms: Option, + system_time: SystemTimeScope, ) -> Option { // Conservative: only emit when there is exactly one filter and it is an // equality or a small IN-list. The caller (engine rules) would have promoted @@ -137,7 +138,7 @@ fn analyze_scan_filters( field: field.clone(), primary_value: value.clone(), extra_values: Vec::new(), - system_as_of_ms, + system_time, }), FilterExpr::InList { field, values } if !values.is_empty() && values.len() <= MAX_IN_LIST => @@ -149,7 +150,7 @@ fn analyze_scan_filters( field: field.clone(), primary_value: primary, extra_values: iter.collect(), - system_as_of_ms, + system_time, }) } _ => None, diff --git a/nodedb-sql/src/planner/catalog_expr_fold.rs b/nodedb-sql/src/planner/catalog_expr_fold.rs index cf2d75ed8..469cbf810 100644 --- a/nodedb-sql/src/planner/catalog_expr_fold.rs +++ b/nodedb-sql/src/planner/catalog_expr_fold.rs @@ -36,10 +36,13 @@ pub(super) fn eval_catalog_constant( // A from-less SELECT's plan is marked volatile when it holds a volatile // call, so it is never cached. Evaluating the call here therefore serves // this execution only, and the next execution re-plans and re-evaluates. - Ok( - crate::planner::const_fold::fold_constant_scoped(expr, functions, FoldScope::Once)? - .unwrap_or(SqlValue::Null), - ) + Ok(crate::planner::const_fold::fold_constant_scoped( + expr, + functions, + FoldScope::Once, + &catalog.crs_context(), + )? + .unwrap_or(SqlValue::Null)) } /// Route `nextval` / `currval` / `setval` to the catalog's sequence state. diff --git a/nodedb-sql/src/planner/const_fold.rs b/nodedb-sql/src/planner/const_fold.rs index 511037175..4c031ab39 100644 --- a/nodedb-sql/src/planner/const_fold.rs +++ b/nodedb-sql/src/planner/const_fold.rs @@ -21,6 +21,7 @@ use std::sync::LazyLock; +use nodedb_crs::CrsContext; use nodedb_types::Value; use sonic_rs; @@ -47,8 +48,8 @@ pub fn default_registry() -> &'static FunctionRegistry { } /// Convenience wrapper around [`fold_constant`] using the default registry. -pub fn fold_constant_default(expr: &SqlExpr) -> FoldResult { - fold_constant(expr, default_registry()) +pub fn fold_constant_default(expr: &SqlExpr, crs: &CrsContext<'_>) -> FoldResult { + fold_constant(expr, default_registry(), crs) } /// Whether a fold is allowed to evaluate `Volatile` functions. @@ -66,9 +67,14 @@ pub enum FoldScope { } /// Fold a `SqlExpr` to a literal `SqlValue` at plan time. See [`FoldResult`] -/// for what each outcome means. Volatile calls are never folded. -pub fn fold_constant(expr: &SqlExpr, registry: &FunctionRegistry) -> FoldResult { - fold_constant_scoped(expr, registry, FoldScope::Reuse) +/// for what each outcome means. Volatile calls are never folded. Geo +/// functions resolve SRIDs through `crs`. +pub fn fold_constant( + expr: &SqlExpr, + registry: &FunctionRegistry, + crs: &CrsContext<'_>, +) -> FoldResult { + fold_constant_scoped(expr, registry, FoldScope::Reuse, crs) } /// Fold a `SqlExpr` under an explicit [`FoldScope`]. @@ -76,13 +82,14 @@ pub fn fold_constant_scoped( expr: &SqlExpr, registry: &FunctionRegistry, scope: FoldScope, + crs: &CrsContext<'_>, ) -> FoldResult { match expr { SqlExpr::Literal(v) => Ok(Some(v.clone())), SqlExpr::ArrayLiteral(items) => { let mut folded = Vec::with_capacity(items.len()); for item in items { - match fold_constant_scoped(item, registry, scope)? { + match fold_constant_scoped(item, registry, scope, crs)? { Some(v) => folded.push(v), None => return Ok(None), } @@ -92,7 +99,7 @@ pub fn fold_constant_scoped( SqlExpr::UnaryOp { op: UnaryOp::Neg, expr, - } => Ok(match fold_constant_scoped(expr, registry, scope)? { + } => Ok(match fold_constant_scoped(expr, registry, scope, crs)? { // `checked_neg` so negating `i64::MIN` declines to fold rather // than wrapping (release) or panicking (debug). Some(SqlValue::Int(i)) => i.checked_neg().map(SqlValue::Int), @@ -110,17 +117,17 @@ pub fn fold_constant_scoped( }), SqlExpr::BinaryOp { left, op, right } => { let (Some(l), Some(r)) = ( - fold_constant_scoped(left, registry, scope)?, - fold_constant_scoped(right, registry, scope)?, + fold_constant_scoped(left, registry, scope, crs)?, + fold_constant_scoped(right, registry, scope, crs)?, ) else { return Ok(None); }; fold_binary(l, *op, r) } SqlExpr::Function { name, args, .. } => { - fold_function_call_scoped(name, args, registry, scope) + fold_function_call_scoped(name, args, registry, scope, crs) } - SqlExpr::Cast { expr, to_type } => Ok(fold_constant_scoped(expr, registry, scope)? + SqlExpr::Cast { expr, to_type } => Ok(fold_constant_scoped(expr, registry, scope, crs)? .and_then(|inner| fold_cast(inner, to_type))), _ => Ok(None), } @@ -343,8 +350,13 @@ fn fold_binary(l: SqlValue, op: BinaryOp, r: SqlValue) -> FoldResult { /// `SqlValue`. Only folds functions that are present in `registry`, so /// callers can distinguish "unknown function" from "known function, all /// args folded". A `Volatile` function is never folded. -pub fn fold_function_call(name: &str, args: &[SqlExpr], registry: &FunctionRegistry) -> FoldResult { - fold_function_call_scoped(name, args, registry, FoldScope::Reuse) +pub fn fold_function_call( + name: &str, + args: &[SqlExpr], + registry: &FunctionRegistry, + crs: &CrsContext<'_>, +) -> FoldResult { + fold_function_call_scoped(name, args, registry, FoldScope::Reuse, crs) } /// Fold a function call under an explicit [`FoldScope`]. @@ -353,6 +365,7 @@ pub fn fold_function_call_scoped( args: &[SqlExpr], registry: &FunctionRegistry, scope: FoldScope, + crs: &CrsContext<'_>, ) -> FoldResult { // Gate on registry so unknown-function paths keep their existing // fallbacks instead of collapsing to SqlValue::Null. Aggregates and @@ -382,7 +395,7 @@ pub fn fold_function_call_scoped( let mut folded_args = Vec::with_capacity(args.len()); for arg in args { - match fold_constant_scoped(arg, registry, scope)? { + match fold_constant_scoped(arg, registry, scope, crs)? { Some(v) => folded_args.push(sql_to_ndb_value(v)), None => return Ok(None), } @@ -394,7 +407,7 @@ pub fn fold_function_call_scoped( // clause; `SELECT mod(5, 0)` has no row scope and became NULL. // Exhaustive on purpose: a new `EvalError` variant must be classified // here rather than defaulting to "defer to a runtime that may not exist". - match nodedb_query::functions::eval_function(&name_lower, &folded_args) { + match nodedb_query::functions::eval_function(&name_lower, &folded_args, crs) { Ok(result) => Ok(Some(ndb_to_sql_value(result))), Err(nodedb_query::EvalError::DivisionByZero) => Err(SqlError::DivisionByZero), Err( @@ -402,9 +415,11 @@ pub fn fold_function_call_scoped( | nodedb_query::EvalError::ArgumentType { .. } | nodedb_query::EvalError::InvalidJsonPath { .. } | nodedb_query::EvalError::NumericOverflow { .. } - | nodedb_query::EvalError::UnsupportedSrid { .. } + | nodedb_query::EvalError::Crs { .. } | nodedb_query::EvalError::MixedSrid { .. } - | nodedb_query::EvalError::ProjectionDomain { .. }), + | nodedb_query::EvalError::Transform { .. } + | nodedb_query::EvalError::Geometry { .. } + | nodedb_query::EvalError::ArgumentValue { .. }), ) => Err(SqlError::DataException { detail: e.to_string(), }), @@ -427,6 +442,7 @@ fn sql_to_ndb_value(v: SqlValue) -> Value { SqlValue::Array(a) => Value::Array(a.into_iter().map(sql_to_ndb_value).collect()), SqlValue::Timestamp(dt) => Value::NaiveDateTime(dt), SqlValue::Timestamptz(dt) => Value::DateTime(dt), + SqlValue::Geometry(g) => Value::Geometry(g), } } @@ -445,15 +461,10 @@ fn ndb_to_sql_value(v: Value) -> SqlValue { Value::Uuid(s) | Value::Ulid(s) | Value::Regex(s) => SqlValue::String(s), Value::Duration(d) => SqlValue::String(d.to_human()), Value::Decimal(d) => SqlValue::Decimal(d), - // Geometry and Object values are serialized to JSON strings so that - // nested function calls like ST_Distance(ST_Point(...), ST_Point(...)) - // survive the SqlValue round-trip. The geo evaluator's geom_arg helper - // recovers Geometry from a GeoJSON string, SRID included through the - // `crs` member; Object results (e.g. from - // ST_GeoHashDecode) reach the client as a JSON-encoded string column. - Value::Geometry(g) => sonic_rs::to_string(&g) - .map(SqlValue::String) - .unwrap_or(SqlValue::Null), + // A geometry keeps its SRID, Z and M through the fold. + Value::Geometry(g) => SqlValue::Geometry(g), + // Object results (ST_GeoHashDecode) reach the client as a + // JSON-encoded string column. Value::Object(map) => sonic_rs::to_string(&map) .map(SqlValue::String) .unwrap_or(SqlValue::Null), @@ -472,6 +483,10 @@ fn ndb_to_sql_value(v: Value) -> SqlValue { mod tests { use super::*; + fn crs() -> CrsContext<'static> { + CrsContext::builtin_only() + } + #[test] fn fold_now_produces_timestamptz_once_only() { let registry = FunctionRegistry::new(); @@ -482,10 +497,10 @@ mod tests { }; // `now` is volatile, so a reusable plan must never carry its value. assert!( - matches!(fold_constant(&expr, ®istry), Ok(None)), + matches!(fold_constant(&expr, ®istry, &crs()), Ok(None)), "now() must not fold into a reusable plan" ); - let val = fold_constant_scoped(&expr, ®istry, FoldScope::Once) + let val = fold_constant_scoped(&expr, ®istry, FoldScope::Once, &crs()) .expect("fold must not error") .expect("now() must fold for a single execution"); match val { @@ -509,11 +524,11 @@ mod tests { ))), }; assert_eq!( - fold_constant(&neg("9223372036854775808"), ®istry).unwrap(), + fold_constant(&neg("9223372036854775808"), ®istry, &crs()).unwrap(), Some(SqlValue::Int(i64::MIN)) ); assert_eq!( - fold_constant(&neg("18446744073709551615"), ®istry).unwrap(), + fold_constant(&neg("18446744073709551615"), ®istry, &crs()).unwrap(), Some(SqlValue::Decimal(-rust_decimal::Decimal::from(u64::MAX))) ); } @@ -531,7 +546,7 @@ mod tests { ], distinct: false, }; - let err = fold_constant(&expr, ®istry).expect_err("malformed path must fail"); + let err = fold_constant(&expr, ®istry, &crs()).expect_err("malformed path must fail"); assert!( matches!(&err, SqlError::DataException { detail } if detail.contains("invalid JSONPath")), "{err:?}" @@ -547,11 +562,11 @@ mod tests { distinct: false, }; assert!( - matches!(fold_constant(&expr, ®istry), Ok(None)), + matches!(fold_constant(&expr, ®istry, &crs()), Ok(None)), "current_timestamp must not fold into a reusable plan" ); assert!(matches!( - fold_constant_scoped(&expr, ®istry, FoldScope::Once), + fold_constant_scoped(&expr, ®istry, FoldScope::Once, &crs()), Ok(Some(SqlValue::Timestamptz(_))) )); } @@ -565,7 +580,7 @@ mod tests { distinct: false, }; assert!( - fold_constant(&expr, ®istry) + fold_constant(&expr, ®istry, &crs()) .expect("fold must not error") .is_none() ); @@ -579,7 +594,7 @@ mod tests { SqlExpr::Literal(SqlValue::Int(42)), ]); assert_eq!( - fold_constant(&expr, ®istry), + fold_constant(&expr, ®istry, &crs()), Ok(Some(SqlValue::Array(vec![ SqlValue::String("public".into()), SqlValue::Int(42), @@ -595,7 +610,10 @@ mod tests { op: BinaryOp::Add, right: Box::new(SqlExpr::Literal(SqlValue::Int(3))), }; - assert_eq!(fold_constant(&expr, ®istry), Ok(Some(SqlValue::Int(5)))); + assert_eq!( + fold_constant(&expr, ®istry, &crs()), + Ok(Some(SqlValue::Int(5))) + ); } #[test] @@ -606,7 +624,7 @@ mod tests { name: "name".into(), }; assert!( - fold_constant(&expr, ®istry) + fold_constant(&expr, ®istry, &crs()) .expect("fold must not error") .is_none() ); @@ -618,7 +636,7 @@ mod tests { let d = rust_decimal::Decimal::new(12345, 2); // 123.45 let expr = SqlExpr::Literal(SqlValue::Decimal(d)); assert_eq!( - fold_constant(&expr, ®istry), + fold_constant(&expr, ®istry, &crs()), Ok(Some(SqlValue::Decimal(d))) ); } @@ -636,7 +654,7 @@ mod tests { }; let expected = Decimal::new(58023, 2); // 580.23 assert_eq!( - fold_constant(&expr, ®istry), + fold_constant(&expr, ®istry, &crs()), Ok(Some(SqlValue::Decimal(expected))) ); } @@ -651,7 +669,7 @@ mod tests { expr: Box::new(SqlExpr::Literal(SqlValue::Decimal(d))), }; assert_eq!( - fold_constant(&expr, ®istry), + fold_constant(&expr, ®istry, &crs()), Ok(Some(SqlValue::Decimal(-d))) ); } @@ -671,7 +689,7 @@ mod tests { ], distinct: false, }; - let v = fold_constant(&expr, ®istry).expect("fold must not error"); + let v = fold_constant(&expr, ®istry, &crs()).expect("fold must not error"); match v { Some(SqlValue::String(ref s)) if !s.is_empty() => {} other => panic!("expected non-empty SqlValue::String, got {other:?}"), @@ -694,7 +712,7 @@ mod tests { args: vec![make_point(-122.4, 37.8), make_point(-87.6, 41.8)], distinct: false, }; - let v = fold_constant(&expr, ®istry).expect("fold must not error"); + let v = fold_constant(&expr, ®istry, &crs()).expect("fold must not error"); match v { Some(SqlValue::Float(d)) => { assert!(d > 0.0, "distance should be positive, got {d}"); @@ -715,7 +733,7 @@ mod tests { }; let expected = Decimal::new(150, 0); assert_eq!( - fold_constant(&expr, ®istry), + fold_constant(&expr, ®istry, &crs()), Ok(Some(SqlValue::Decimal(expected))) ); } diff --git a/nodedb-sql/src/planner/declared_type_coerce.rs b/nodedb-sql/src/planner/declared_type_coerce.rs index 226a68cad..68c2e614a 100644 --- a/nodedb-sql/src/planner/declared_type_coerce.rs +++ b/nodedb-sql/src/planner/declared_type_coerce.rs @@ -363,7 +363,7 @@ fn coerce_to_instant(column: &str, value: SqlValue, declared: &SqlDataType) -> R .and_then(|millis| NdbDateTime::from_millis(millis).ok()) .map(tag) .ok_or_else(|| not_representable(column, &d.to_string(), declared_name)), - SqlValue::Bool(_) | SqlValue::Bytes(_) | SqlValue::Array(_) => { + SqlValue::Bool(_) | SqlValue::Bytes(_) | SqlValue::Array(_) | SqlValue::Geometry(_) => { Err(SqlError::TypeMismatch { detail: format!( "column '{column}': {} is not representable as {declared_name}", @@ -406,6 +406,7 @@ fn literal_kind(value: &SqlValue) -> &'static str { SqlValue::Array(_) => "an array", SqlValue::Timestamp(_) => "a timestamp", SqlValue::Timestamptz(_) => "a timestamptz", + SqlValue::Geometry(_) => "a geometry", } } diff --git a/nodedb-sql/src/planner/declared_vector_coerce.rs b/nodedb-sql/src/planner/declared_vector_coerce.rs index 8d91d6b3a..7f63b2db1 100644 --- a/nodedb-sql/src/planner/declared_vector_coerce.rs +++ b/nodedb-sql/src/planner/declared_vector_coerce.rs @@ -100,6 +100,7 @@ fn element_description(element: &SqlValue) -> String { SqlValue::Array(_) => "a nested array".to_string(), SqlValue::Timestamp(_) => "a timestamp".to_string(), SqlValue::Timestamptz(_) => "a timestamptz".to_string(), + SqlValue::Geometry(_) => "a geometry".to_string(), SqlValue::Int(_) => "an integer".to_string(), SqlValue::Float(_) => "a float".to_string(), SqlValue::Decimal(_) => "a decimal".to_string(), diff --git a/nodedb-sql/src/planner/defaults/compiled.rs b/nodedb-sql/src/planner/defaults/compiled.rs index 0f2286ffa..146652b4e 100644 --- a/nodedb-sql/src/planner/defaults/compiled.rs +++ b/nodedb-sql/src/planner/defaults/compiled.rs @@ -124,6 +124,7 @@ impl CompiledDefault { parsed, crate::planner::const_fold::default_registry(), crate::planner::const_fold::FoldScope::Once, + &catalog.crs_context(), ) .map_err(|_| unevaluable(&self.column, &self.text))? .ok_or_else(|| unevaluable(&self.column, &self.text))?; diff --git a/nodedb-sql/src/planner/defaults/convert.rs b/nodedb-sql/src/planner/defaults/convert.rs index 6817a5cca..016494013 100644 --- a/nodedb-sql/src/planner/defaults/convert.rs +++ b/nodedb-sql/src/planner/defaults/convert.rs @@ -20,6 +20,7 @@ pub(super) fn sql_value_to_ndb(v: SqlValue) -> nodedb_types::Value { } SqlValue::Timestamp(dt) => nodedb_types::Value::NaiveDateTime(dt), SqlValue::Timestamptz(dt) => nodedb_types::Value::DateTime(dt), + SqlValue::Geometry(g) => nodedb_types::Value::Geometry(g), } } @@ -42,6 +43,7 @@ pub fn default_value_to_sql(column: &str, value: nodedb_types::Value) -> crate:: nodedb_types::Value::Bytes(b) => SqlValue::Bytes(b), nodedb_types::Value::NaiveDateTime(dt) => SqlValue::Timestamp(dt), nodedb_types::Value::DateTime(dt) => SqlValue::Timestamptz(dt), + nodedb_types::Value::Geometry(g) => SqlValue::Geometry(g), nodedb_types::Value::Array(items) => SqlValue::Array( items .into_iter() diff --git a/nodedb-sql/src/planner/dml/target.rs b/nodedb-sql/src/planner/dml/target.rs index 0f1a46b56..33e597f5d 100644 --- a/nodedb-sql/src/planner/dml/target.rs +++ b/nodedb-sql/src/planner/dml/target.rs @@ -166,7 +166,7 @@ pub(super) fn typed_rows( rows_ast: &[ast::Parens>], catalog: &dyn SqlCatalog, ) -> Result { - let mut rows = convert_value_rows(columns, rows_ast)?; + let mut rows = convert_value_rows(columns, rows_ast, &catalog.crs_context())?; let volatile_defaults = materialize_defaults_in_rows(&info.columns, &mut rows, catalog)?; coerce_and_check_rows(info, &mut rows)?; Ok(TypedRows { diff --git a/nodedb-sql/src/planner/dml/update_delete.rs b/nodedb-sql/src/planner/dml/update_delete.rs index 1d5874857..89cb50c93 100644 --- a/nodedb-sql/src/planner/dml/update_delete.rs +++ b/nodedb-sql/src/planner/dml/update_delete.rs @@ -72,7 +72,7 @@ pub fn plan_update(stmt: &ast::Statement, catalog: &dyn SqlCatalog) -> Result Vec::new(), }; - let target_keys = extract_point_keys(update.selection.as_ref(), &info); + let target_keys = extract_point_keys(update.selection.as_ref(), &info, &catalog.crs_context()); // Vector-primary collection: the row lives in the HNSW index and its // payload sidecar, never in the document store the engine rules target. @@ -179,7 +179,7 @@ pub fn plan_delete(stmt: &ast::Statement, catalog: &dyn SqlCatalog) -> Result Vec::new(), }; - let target_keys = extract_point_keys(delete.selection.as_ref(), &info); + let target_keys = extract_point_keys(delete.selection.as_ref(), &info, &catalog.crs_context()); // Vector-primary collection: see `plan_update`. if is_vector_primary(&info) diff --git a/nodedb-sql/src/planner/dml_helpers/ast_extract.rs b/nodedb-sql/src/planner/dml_helpers/ast_extract.rs index 4bdcc5d3f..7f3bed215 100644 --- a/nodedb-sql/src/planner/dml_helpers/ast_extract.rs +++ b/nodedb-sql/src/planner/dml_helpers/ast_extract.rs @@ -23,7 +23,12 @@ pub(crate) fn extract_table_name_from_table_with_joins( } /// Extract point-operation keys from WHERE clause (WHERE pk = literal OR pk IN (...)). -pub fn extract_point_keys(selection: Option<&ast::Expr>, info: &CollectionInfo) -> Vec { +/// Geo functions in a key value resolve SRIDs through `crs`. +pub fn extract_point_keys( + selection: Option<&ast::Expr>, + info: &CollectionInfo, + crs: &nodedb_crs::CrsContext<'_>, +) -> Vec { let pk = match &info.primary_key { Some(pk) => pk.clone(), None => return Vec::new(), @@ -38,7 +43,7 @@ pub fn extract_point_keys(selection: Option<&ast::Expr>, info: &CollectionInfo) // The keys stand for the WHERE only when they cover every row it matches. // A disjunct that is no key equality (`id = 'd' OR v < 2`) matches rows no // key names, so the statement takes the predicate path. - if collect_pk_equalities(expr, &pk, &mut keys) { + if collect_pk_equalities(expr, &pk, &mut keys, crs) { keys } else { Vec::new() @@ -48,7 +53,12 @@ pub fn extract_point_keys(selection: Option<&ast::Expr>, info: &CollectionInfo) /// Push the keys `expr` names into `keys`. Returns `false` when `expr` matches /// a row no pushed key names: a disjunct that is no key equality, or a key /// value that is no literal. -fn collect_pk_equalities(expr: &ast::Expr, pk: &str, keys: &mut Vec) -> bool { +fn collect_pk_equalities( + expr: &ast::Expr, + pk: &str, + keys: &mut Vec, + crs: &nodedb_crs::CrsContext<'_>, +) -> bool { match expr { ast::Expr::BinaryOp { left, @@ -62,7 +72,7 @@ fn collect_pk_equalities(expr: &ast::Expr, pk: &str, keys: &mut Vec) - } else { return false; }; - match expr_to_sql_value(value) { + match expr_to_sql_value(value, crs) { Ok(v) => { keys.push(v); true @@ -74,12 +84,15 @@ fn collect_pk_equalities(expr: &ast::Expr, pk: &str, keys: &mut Vec) - left, op: ast::BinaryOperator::Or, right, - } => collect_pk_equalities(left, pk, keys) && collect_pk_equalities(right, pk, keys), + } => { + collect_pk_equalities(left, pk, keys, crs) + && collect_pk_equalities(right, pk, keys, crs) + } ast::Expr::InList { expr: inner, list, negated: false, - } if is_column(inner, pk) => list.iter().all(|item| match expr_to_sql_value(item) { + } if is_column(inner, pk) => list.iter().all(|item| match expr_to_sql_value(item, crs) { Ok(v) => { keys.push(v); true diff --git a/nodedb-sql/src/planner/dml_helpers/kv_insert.rs b/nodedb-sql/src/planner/dml_helpers/kv_insert.rs index e3276e3af..dc03412a2 100644 --- a/nodedb-sql/src/planner/dml_helpers/kv_insert.rs +++ b/nodedb-sql/src/planner/dml_helpers/kv_insert.rs @@ -77,11 +77,12 @@ pub(crate) fn build_kv_insert_plan(params: KvInsertParams<'_>) -> Result> = Vec::with_capacity(rows_ast.len()); let mut volatile_defaults = false; + let crs = catalog.crs_context(); for row_exprs in rows_ast { let mut row: Vec<(String, SqlValue)> = Vec::with_capacity(columns.len()); for (i, col) in columns.iter().enumerate() { let Some(expr) = row_exprs.get(i) else { break }; - row.push((col.clone(), expr_to_sql_value(expr)?)); + row.push((col.clone(), expr_to_sql_value(expr, &crs)?)); } volatile_defaults |= compiled_defaults.materialize_row(&mut row, catalog)?; // The key column is exempt — see `coerce_rows_to_declared_types`. diff --git a/nodedb-sql/src/planner/dml_helpers/value_convert.rs b/nodedb-sql/src/planner/dml_helpers/value_convert.rs index 9c55b9514..b7a088ffa 100644 --- a/nodedb-sql/src/planner/dml_helpers/value_convert.rs +++ b/nodedb-sql/src/planner/dml_helpers/value_convert.rs @@ -7,6 +7,7 @@ //! concrete value, so constant folding and spatial-constructor handling never //! drift between them. +use nodedb_crs::CrsContext; use sqlparser::ast; use crate::error::{Result, SqlError}; @@ -16,6 +17,7 @@ use crate::types::*; pub(crate) fn convert_value_rows( columns: &[String], rows: &[ast::Parens>], + crs: &CrsContext<'_>, ) -> Result>> { rows.iter() .map(|row| { @@ -23,7 +25,7 @@ pub(crate) fn convert_value_rows( .enumerate() .map(|(i, expr)| { let col = columns.get(i).cloned().unwrap_or_else(|| format!("col{i}")); - let val = expr_to_sql_value(expr)?; + let val = expr_to_sql_value(expr, crs)?; Ok((col, val)) }) .collect::>>() @@ -31,13 +33,17 @@ pub(crate) fn convert_value_rows( .collect() } -pub(crate) fn expr_to_sql_value(expr: &ast::Expr) -> Result { +/// Geo functions resolve SRIDs through `crs`. +pub(crate) fn expr_to_sql_value(expr: &ast::Expr, crs: &CrsContext<'_>) -> Result { match expr { ast::Expr::Value(v) => convert_value(&v.value), // Array literals lower element-wise into `SqlValue::Array`; there is // no array-literal `SqlValue` the constant folder could produce. ast::Expr::Array(ast::Array { elem, .. }) => { - let vals = elem.iter().map(expr_to_sql_value).collect::>()?; + let vals = elem + .iter() + .map(|e| expr_to_sql_value(e, crs)) + .collect::>()?; Ok(SqlValue::Array(vals)) } // A geometry-producing call resolves through the geometry-expression @@ -45,11 +51,11 @@ pub(crate) fn expr_to_sql_value(expr: &ast::Expr) -> Result { // than the NULL the generic folder would produce — storing NULL for // `ST_GeomFromText('POINT(')` would lose the row's geometry silently. ast::Expr::Function(func) => { - match crate::planner::geometry_expr::fold_geometry_function(func) { + match crate::planner::geometry_expr::fold_geometry_function(func, crs) { Some(result) => result, // Non-geometry functions (`now()`, `date_add(...)`, registered // scalars) fold through the shared pipeline below. - None => fold_constant_value(expr), + None => fold_constant_value(expr, crs), } } // Everything else — `::TYPE` / `CAST(... AS TYPE)` casts, arithmetic, @@ -58,14 +64,14 @@ pub(crate) fn expr_to_sql_value(expr: &ast::Expr) -> Result { // uses, so the two surfaces never drift. Only genuinely row- or // runtime-dependent expressions (column refs, subqueries, unknown // functions) fail here. - _ => fold_constant_value(expr), + _ => fold_constant_value(expr, crs), } } -fn fold_constant_value(expr: &ast::Expr) -> Result { +fn fold_constant_value(expr: &ast::Expr, crs: &CrsContext<'_>) -> Result { let sql_expr = crate::resolver::expr::convert_expr(expr, &crate::resolver::ColumnScope::Unchecked)?; - crate::planner::const_fold::fold_constant_default(&sql_expr)?.ok_or_else(|| { + crate::planner::const_fold::fold_constant_default(&sql_expr, crs)?.ok_or_else(|| { SqlError::Unsupported { detail: format!("value expression: {expr}"), } diff --git a/nodedb-sql/src/planner/geometry_expr/resolve.rs b/nodedb-sql/src/planner/geometry_expr/resolve.rs index 62f75387a..6f8c21511 100644 --- a/nodedb-sql/src/planner/geometry_expr/resolve.rs +++ b/nodedb-sql/src/planner/geometry_expr/resolve.rs @@ -11,6 +11,7 @@ use sqlparser::ast; +use nodedb_crs::CrsContext; use nodedb_query::geo_functions; use nodedb_types::geometry::SridGeometry; @@ -23,20 +24,21 @@ use crate::types::*; /// Returns `None` when `func` is not a geometry-producing geospatial call, so /// the caller falls through to the shared constant-folding pipeline. /// -/// The stored form is a GeoJSON string, which is what the spatial read path -/// parses back. A geometry in any SRID other than 4326 carries the `crs` -/// member, so the SRID survives storage. A call whose arguments do not -/// describe a valid geometry is an -/// error rather than a NULL: silently storing NULL for a malformed -/// `ST_GeomFromText('POINT(')` would lose the row's geometry with no signal. -pub(crate) fn fold_geometry_function(func: &ast::Function) -> Option> { +/// The folded form is [`SqlValue::Geometry`], which keeps the SRID, Z and +/// M into storage. A call whose arguments do not describe a valid geometry +/// is an error rather than a NULL: storing NULL for a malformed +/// `ST_GeomFromText('POINT(')` loses the row's geometry with no signal. +pub(crate) fn fold_geometry_function( + func: &ast::Function, + crs: &CrsContext<'_>, +) -> Option> { let name = function_name(func); if !geo_functions::returns_geometry(&name) { return None; } let expr = ast::Expr::Function(func.clone()); - Some(match resolve(&expr) { - Ok(Some(geom)) => serialize(&geom, &name), + Some(match resolve(&expr, crs) { + Ok(Some(geom)) => Ok(SqlValue::Geometry(geom)), Ok(None) => Err(invalid_geometry(&name)), Err(e) => Err(e), }) @@ -49,8 +51,11 @@ pub(crate) fn fold_geometry_function(func: &ast::Function) -> Option Result { - match resolve(expr)? { +pub(crate) fn resolve_geometry_expr( + expr: &ast::Expr, + crs: &CrsContext<'_>, +) -> Result { + match resolve(expr, crs)? { Some(geom) => Ok(geom), None => Err(SqlError::InvalidFunction { detail: format!( @@ -64,34 +69,25 @@ pub(crate) fn resolve_geometry_expr(expr: &ast::Expr) -> Result { /// /// `Ok(None)` means the expression folded but is not a geometry, or could not /// be folded at plan time at all. -fn resolve(expr: &ast::Expr) -> Result> { +fn resolve(expr: &ast::Expr, crs: &CrsContext<'_>) -> Result> { let sql_expr = crate::resolver::expr::convert_expr(expr, &crate::resolver::ColumnScope::Unchecked)?; - let Some(value) = crate::planner::const_fold::fold_constant_default(&sql_expr)? else { + let Some(value) = crate::planner::const_fold::fold_constant_default(&sql_expr, crs)? else { return Ok(None); }; - Ok(geometry_from_value(&value)) + Ok(geometry_from_value(&value, crs)) } -/// Geometry travels through `SqlValue` as its GeoJSON string form, which is -/// also how it is stored; WKT literals are accepted here for the same reason -/// they are accepted by the evaluator. -fn geometry_from_value(value: &SqlValue) -> Option { +/// A folded geometry, or a geometry literal: GeoJSON, EWKT, WKT or hex +/// EWKB text, as the evaluator accepts them. +fn geometry_from_value(value: &SqlValue, crs: &CrsContext<'_>) -> Option { match value { - SqlValue::String(text) => geo_functions::geometry_from_text(text), + SqlValue::Geometry(g) => Some(g.clone()), + SqlValue::String(text) => geo_functions::geometry_from_text(text, crs), _ => None, } } -/// GeoJSON text of `geom`, with the `crs` member for a non-4326 SRID. -fn serialize(geom: &SridGeometry, name: &str) -> Result { - sonic_rs::to_string(geom) - .map(SqlValue::String) - .map_err(|e| SqlError::InvalidFunction { - detail: format!("{name}: failed to serialize geometry: {e}"), - }) -} - fn invalid_geometry(name: &str) -> SqlError { SqlError::InvalidFunction { detail: format!("{name}: arguments do not describe a valid geometry"), @@ -119,6 +115,16 @@ mod tests { use super::{fold_geometry_function, resolve_geometry_expr}; use crate::types::SqlValue; + fn crs() -> nodedb_crs::CrsContext<'static> { + nodedb_crs::CrsContext::builtin_only() + } + + /// An SRID no CRS defines fails the fold. + #[test] + fn undefined_srid_is_an_error() { + assert!(resolve_geometry_expr(&expr("ST_SetSRID(ST_Point(1, 2), 999)"), &crs()).is_err()); + } + /// Parse a scalar SQL expression out of `SELECT `. fn expr(sql: &str) -> sqlparser::ast::Expr { use sqlparser::ast::{SetExpr, Statement}; @@ -139,8 +145,8 @@ mod tests { } fn resolved(sql: &str) -> Geometry { - resolve_geometry_expr(&expr(sql)) - .map(|g| g.geometry) + resolve_geometry_expr(&expr(sql), &crs()) + .map(|g| g.geometry().clone()) .unwrap_or_else(|e| panic!("`{sql}` must resolve to a geometry: {e}")) } @@ -197,7 +203,7 @@ mod tests { #[test] fn non_geometry_argument_reports_a_geometry_error() { for sql in ["42", "'not a geometry'", "TRUE"] { - let err = resolve_geometry_expr(&expr(sql)) + let err = resolve_geometry_expr(&expr(sql), &crs()) .expect_err("`{sql}` must not resolve to a geometry") .to_string(); assert!( @@ -214,50 +220,40 @@ mod tests { // ── Inserted-value position ───────────────────────────────────────────────── #[test] - fn geometry_functions_fold_to_their_stored_geojson_form() { + fn geometry_functions_fold_to_a_geometry_value() { let sqlparser::ast::Expr::Function(func) = expr("ST_GeomFromText('POINT(1 2)')") else { panic!("expected a function"); }; - let Some(Ok(SqlValue::String(geojson))) = fold_geometry_function(&func) else { - panic!("a geometry constructor must fold to its stored form"); + let Some(Ok(SqlValue::Geometry(g))) = fold_geometry_function(&func, &crs()) else { + panic!("a geometry constructor must fold to a geometry"); }; - assert_eq!( - nodedb_types::geometry::from_geojson_str(&geojson), - Some(SridGeometry::wgs84(Geometry::point(1.0, 2.0))), - "stored form must parse back through the spatial read path" - ); - assert!( - !geojson.contains("crs"), - "4326 writes no crs member: {geojson}" - ); + assert_eq!(g, SridGeometry::wgs84(Geometry::point(1.0, 2.0))); } - /// A projected geometry folds with its `crs` member, so the stored form - /// keeps the SRID. + /// Z, M and a projected SRID survive the fold. #[test] - fn projected_geometry_folds_with_crs_member() { - let sqlparser::ast::Expr::Function(func) = expr("ST_GeomFromText('POINT(1 2)', 3375)") + fn fold_keeps_srid_z_and_m() { + let sqlparser::ast::Expr::Function(func) = + expr("ST_GeomFromText('POINT ZM (1 2 3 4)', 3375)") else { panic!("expected a function"); }; - let Some(Ok(SqlValue::String(geojson))) = fold_geometry_function(&func) else { - panic!("a geometry constructor must fold to its stored form"); + let Some(Ok(SqlValue::Geometry(g))) = fold_geometry_function(&func, &crs()) else { + panic!("a geometry constructor must fold to a geometry"); }; - assert!(geojson.contains("EPSG:3375"), "{geojson}"); - assert_eq!( - nodedb_types::geometry::from_geojson_str(&geojson).map(|g| g.srid), - Some(Srid::PeninsularRso) - ); - let tagged = resolve_geometry_expr(&expr("ST_SetSRID(ST_Point(1, 2), 32648)")) + assert_eq!(g.srid, Srid::new(3375).unwrap()); + assert_eq!(g.ordinates().z(), Some(&[3.0][..])); + assert_eq!(g.ordinates().m(), Some(&[4.0][..])); + let tagged = resolve_geometry_expr(&expr("ST_SetSRID(ST_Point(1, 2), 32648)"), &crs()) .expect("must resolve"); - assert_eq!(tagged.srid, Srid::Utm48N); + assert_eq!(tagged.srid, Srid::new(32648).unwrap()); } /// A mixed-SRID operation in geometry position is an error, not a fold. #[test] fn mixed_srid_operation_is_an_error() { let sql = "ST_Union(ST_Point(1, 2), ST_SetSRID(ST_Point(1, 2), 32648))"; - assert!(resolve_geometry_expr(&expr(sql)).is_err()); + assert!(resolve_geometry_expr(&expr(sql), &crs()).is_err()); } /// A non-geometry call falls through so the generic constant folder handles @@ -267,7 +263,7 @@ mod tests { let sqlparser::ast::Expr::Function(func) = expr("ST_Area(ST_Point(1, 2))") else { panic!("expected a function"); }; - assert!(fold_geometry_function(&func).is_none()); + assert!(fold_geometry_function(&func, &crs()).is_none()); } /// Storing NULL for a malformed geometry would drop the row's location with @@ -278,7 +274,7 @@ mod tests { let sqlparser::ast::Expr::Function(func) = expr(sql) else { panic!("expected a function"); }; - let Some(result) = fold_geometry_function(&func) else { + let Some(result) = fold_geometry_function(&func, &crs()) else { panic!("`{sql}` is a geometry constructor and must be claimed"); }; assert!( diff --git a/nodedb-sql/src/planner/search_scope/lookup.rs b/nodedb-sql/src/planner/search_scope/lookup.rs index f69929cb5..fbc2c06db 100644 --- a/nodedb-sql/src/planner/search_scope/lookup.rs +++ b/nodedb-sql/src/planner/search_scope/lookup.rs @@ -23,6 +23,7 @@ pub(super) fn is_index_owned(trigger: SearchTrigger) -> bool { SearchTrigger::None | SearchTrigger::VectorSearch | SearchTrigger::SpatialDWithin + | SearchTrigger::SpatialDWithin3D | SearchTrigger::SpatialContains | SearchTrigger::SpatialIntersects | SearchTrigger::SpatialWithin diff --git a/nodedb-sql/src/planner/select/array_cell_geom.rs b/nodedb-sql/src/planner/select/array_cell_geom.rs index 02acc723f..a4b58e021 100644 --- a/nodedb-sql/src/planner/select/array_cell_geom.rs +++ b/nodedb-sql/src/planner/select/array_cell_geom.rs @@ -300,6 +300,7 @@ mod tests { name: "b1".into(), dtype: ArrayAttrType::Float64, nullable: true, + meta: Default::default(), }], tile_extents: vec![4, 4], georef, diff --git a/nodedb-sql/src/planner/select/array_from.rs b/nodedb-sql/src/planner/select/array_from.rs index 80748ff00..d63eecae0 100644 --- a/nodedb-sql/src/planner/select/array_from.rs +++ b/nodedb-sql/src/planner/select/array_from.rs @@ -366,6 +366,7 @@ mod tests { name: name.into(), dtype: ArrayAttrType::Float64, nullable: true, + meta: Default::default(), }; (name == "img").then(|| ArrayCatalogView { name: "img".into(), diff --git a/nodedb-sql/src/planner/select/cte_catalog.rs b/nodedb-sql/src/planner/select/cte_catalog.rs index 3cc9df08e..8cc285d46 100644 --- a/nodedb-sql/src/planner/select/cte_catalog.rs +++ b/nodedb-sql/src/planner/select/cte_catalog.rs @@ -77,6 +77,10 @@ impl SqlCatalog for CteCatalog<'_> { } } + fn crs_context(&self) -> nodedb_crs::CrsContext<'_> { + self.inner.crs_context() + } + fn lookup_array(&self, name: &str) -> Option { self.inner.lookup_array(name) } diff --git a/nodedb-sql/src/planner/select/entry/fixtures.rs b/nodedb-sql/src/planner/select/entry/fixtures.rs index 49625db6a..c23839350 100644 --- a/nodedb-sql/src/planner/select/entry/fixtures.rs +++ b/nodedb-sql/src/planner/select/entry/fixtures.rs @@ -153,6 +153,7 @@ impl SqlCatalog for TestCatalog { name: "qual".into(), dtype: crate::types_array::ArrayAttrType::Float64, nullable: true, + meta: Default::default(), }], tile_extents: vec![1, 1_000_000], georef: None, diff --git a/nodedb-sql/src/planner/select/select_stmt.rs b/nodedb-sql/src/planner/select/select_stmt.rs index 7a2f684c8..e57cddfb4 100644 --- a/nodedb-sql/src/planner/select/select_stmt.rs +++ b/nodedb-sql/src/planner/select/select_stmt.rs @@ -181,8 +181,13 @@ pub(super) fn plan_select( // SELECT target list is threaded through so the search plan // self-describes its output columns. let where_projection = convert_projection(&select.projection, &scope)?; - if let Some(plan) = try_extract_where_search(expr, table, functions, &where_projection)? - { + if let Some(plan) = try_extract_where_search( + expr, + table, + functions, + &where_projection, + &catalog.crs_context(), + )? { refuse_dropped_clauses( &plan, SearchBodyClauses { diff --git a/nodedb-sql/src/planner/select/where_search.rs b/nodedb-sql/src/planner/select/where_search.rs index ad748e012..7b6dee36d 100644 --- a/nodedb-sql/src/planner/select/where_search.rs +++ b/nodedb-sql/src/planner/select/where_search.rs @@ -43,8 +43,9 @@ pub(super) fn try_extract_where_search( table: &crate::resolver::columns::ResolvedTable, functions: &FunctionRegistry, projection: &[Projection], + crs: &nodedb_crs::CrsContext<'_>, ) -> Result> { - try_extract_with_extra_filters(expr, table, functions, &[], projection) + try_extract_with_extra_filters(expr, table, functions, &[], projection, crs) } /// Internal entry that threads the sibling-AND predicates through to the @@ -58,11 +59,20 @@ fn try_extract_with_extra_filters( functions: &FunctionRegistry, extra_filters: &[&ast::Expr], projection: &[Projection], + crs: &nodedb_crs::CrsContext<'_>, ) -> Result> { match expr { ast::Expr::Function(func) => { let name = function_name(func); - dispatch_trigger(&name, func, table, functions, extra_filters, projection) + dispatch_trigger( + &name, + func, + table, + functions, + extra_filters, + projection, + crs, + ) } // AND: recurse on each side, carrying the other side as a scan filter. ast::Expr::BinaryOp { @@ -73,24 +83,29 @@ fn try_extract_with_extra_filters( // Try left as the trigger, with right as a carried filter. let mut with_right = extra_filters.to_vec(); with_right.push(right.as_ref()); - if let Some(plan) = - try_extract_with_extra_filters(left, table, functions, &with_right, projection)? - { + if let Some(plan) = try_extract_with_extra_filters( + left, + table, + functions, + &with_right, + projection, + crs, + )? { return Ok(Some(plan)); } // Try right as the trigger, with left as a carried filter. let mut with_left = extra_filters.to_vec(); with_left.push(left.as_ref()); - if let Some(plan) = - try_extract_with_extra_filters(right, table, functions, &with_left, projection)? - { + if let Some(plan) = try_extract_with_extra_filters( + right, table, functions, &with_left, projection, crs, + )? { return Ok(Some(plan)); } Ok(None) } // A parenthesised conjunction is the same conjunction. ast::Expr::Nested(inner) => { - try_extract_with_extra_filters(inner, table, functions, extra_filters, projection) + try_extract_with_extra_filters(inner, table, functions, extra_filters, projection, crs) } _ => Ok(None), } @@ -152,6 +167,7 @@ fn dispatch_trigger( functions: &FunctionRegistry, extra_filters: &[&ast::Expr], projection: &[Projection], + crs: &nodedb_crs::CrsContext<'_>, ) -> Result> { // Exhaustive match on `SearchTrigger`: when a new trigger is added, // this fails to compile until a WHERE-clause routing decision is made @@ -162,10 +178,11 @@ fn dispatch_trigger( plan_text_from_where(name, func, table, extra_filters, projection) } SearchTrigger::SpatialDWithin + | SearchTrigger::SpatialDWithin3D | SearchTrigger::SpatialContains | SearchTrigger::SpatialIntersects | SearchTrigger::SpatialWithin => { - plan_spatial_from_where(name, func, table, extra_filters, projection) + plan_spatial_from_where(name, func, table, extra_filters, projection, crs) } SearchTrigger::VectorSearch => { plan_vector_from_where(name, func, table, extra_filters, projection) @@ -366,9 +383,11 @@ fn plan_spatial_from_where( table: &crate::resolver::columns::ResolvedTable, extra_filters: &[&ast::Expr], projection: &[Projection], + crs: &nodedb_crs::CrsContext<'_>, ) -> Result> { let predicate = match name { "st_dwithin" => SpatialPredicate::DWithin, + "st_3ddwithin" => SpatialPredicate::DWithin3D, "st_contains" => SpatialPredicate::Contains, "st_intersects" => SpatialPredicate::Intersects, "st_within" => SpatialPredicate::Within, @@ -386,15 +405,17 @@ fn plan_spatial_from_where( field: "query geometry".into(), context: name.into(), })?; - let geometry = crate::planner::geometry_expr::resolve_geometry_expr(geom_arg).map_err(|e| { - SqlError::InvalidFunction { - detail: format!("invalid geometry in {name}: {e}"), - } - })?; - let issues = nodedb_spatial::validate::validate_geometry(&geometry.geometry); + let geometry = + crate::planner::geometry_expr::resolve_geometry_expr(geom_arg, crs).map_err(|e| { + SqlError::InvalidFunction { + detail: format!("invalid geometry in {name}: {e}"), + } + })?; + let issues = nodedb_spatial::validate::validate_geometry(geometry.geometry()); if !issues.is_empty() { + let reasons: Vec = issues.iter().map(ToString::to_string).collect(); return Err(SqlError::InvalidFunction { - detail: format!("invalid geometry in {name}: {}", issues.join("; ")), + detail: format!("invalid geometry in {name}: {}", reasons.join("; ")), }); } let distance = if args.len() >= 3 { diff --git a/nodedb-sql/src/resolver/array_tvf.rs b/nodedb-sql/src/resolver/array_tvf.rs index f62baaee8..568ffd7fb 100644 --- a/nodedb-sql/src/resolver/array_tvf.rs +++ b/nodedb-sql/src/resolver/array_tvf.rs @@ -15,7 +15,7 @@ use crate::types::{ ArrayAggPlan, ArrayCatalogView, ArraySlicePlan, CollectionInfo, ColumnInfo, EngineType, SqlCatalog, SqlDataType, }; -use crate::types_array::{ArrayAttrType, ArrayDimType, ArrayReducerAst}; +use crate::types_array::{ArrayAttrAst, ArrayAttrType, ArrayDimType, ArrayReducerAst}; use nodedb_types::columnar::{FloatWidth, IntWidth}; /// The column an audit-log slice (`AS OF SYSTEM TIME NULL`) stamps on each @@ -101,29 +101,50 @@ pub(super) fn resolve_array_tvf( /// The columns of the relation an array cell read exposes: every dim in /// schema order, then each attr `attrs` names in that order, or every attr /// in schema order when `attrs` is empty. -pub fn array_relation_columns(view: &ArrayCatalogView, attrs: &[String]) -> Vec { +/// +/// A `physical` read types an attr with a scale or offset as `float8`, and +/// an attr with a nodata as nullable, since a nodata cell reads as NULL. +pub fn array_relation_columns( + view: &ArrayCatalogView, + attrs: &[String], + physical: bool, +) -> Vec { let mut cols = Vec::with_capacity(view.dims.len() + view.attrs.len()); for d in &view.dims { cols.push(column(&d.name, dim_type_to_sql(d.dtype), false)); } + let attr_col = |a: &ArrayAttrAst| { + let nullable = a.nullable || (physical && a.meta.nodata.is_some()); + attr_column(&a.name, read_type(a, physical), nullable) + }; if attrs.is_empty() { for a in &view.attrs { - cols.push(attr_column(&a.name, a.dtype, a.nullable)); + cols.push(attr_col(a)); } } else { for name in attrs { if let Some(a) = view.attrs.iter().find(|a| &a.name == name) { - cols.push(attr_column(&a.name, a.dtype, a.nullable)); + cols.push(attr_col(a)); } } } cols } +/// The type a read of `attr` carries: `FLOAT64` for a physical read of an +/// attr with a scale or offset, the attr's own type otherwise. +fn read_type(attr: &ArrayAttrAst, physical: bool) -> ArrayAttrType { + if physical && attr.meta.has_transform() { + ArrayAttrType::Float64 + } else { + attr.dtype + } +} + /// The columns a slice's rows carry: its dims and attrs, plus /// [`ARRAY_SYSTEM_TIME_COLUMN`] when it reads every system-time version. pub fn array_slice_columns(view: &ArrayCatalogView, slice: &ArraySlicePlan) -> Vec { - let mut cols = array_relation_columns(view, &slice.attr_projection); + let mut cols = array_relation_columns(view, &slice.attr_projection, slice.physical); if matches!( slice.temporal.system_time, nodedb_types::SystemTimeScope::AllVersions @@ -134,11 +155,12 @@ pub fn array_slice_columns(view: &ArrayCatalogView, slice: &ArraySlicePlan) -> V } /// The columns an aggregate's rows carry: `group`, typed as the group dim, -/// when it groups, then `result`, typed by the reducer. +/// when it groups, then `result`, typed by the reducer. A histogram carries +/// `bin_lo`, `bin_hi` and `count` in place of `result`, one row per bin. /// /// Refuses an attr the array does not hold, or a reducer the attr type cannot answer. pub fn array_agg_columns(view: &ArrayCatalogView, agg: &ArrayAggPlan) -> Result> { - let mut cols = Vec::with_capacity(2); + let mut cols = Vec::with_capacity(4); if let Some(dim) = &agg.group_by_dim && let Some(d) = view.dims.iter().find(|d| &d.name == dim) { @@ -152,7 +174,18 @@ pub fn array_agg_columns(view: &ArrayCatalogView, agg: &ArrayAggPlan) -> Result< table: view.name.clone(), column: agg.attr.clone(), })?; - cols.push(agg_result_column(agg.reducer, &attr.name, attr.dtype)?); + check_agg_reducer(agg.reducer, &attr.name, attr.dtype)?; + if let ArrayReducerAst::Histogram { .. } = agg.reducer { + cols.push(column("bin_lo", SqlDataType::Float64, false)); + cols.push(column("bin_hi", SqlDataType::Float64, false)); + cols.push(column("count", SqlDataType::Int64, false)); + return Ok(cols); + } + cols.push(agg_result_column( + agg.reducer, + &attr.name, + read_type(attr, agg.physical), + )?); Ok(cols) } @@ -178,10 +211,10 @@ fn horizon_column() -> ColumnInfo { /// Refuse a reducer that an attr of type `attr` cannot answer. /// -/// The engine folds only numeric cells and skips every other cell. -/// `SUM`, `MEAN`, `MIN`, and `MAX` over a `STRING` or `BYTES` attr see no cell. -/// Their rows can only carry NULL, so the planner refuses them. -/// `COUNT` counts the non-null cells of every attr type. +/// The engine folds only numeric cells and skips every other cell. Every +/// reducer but `COUNT` over a `STRING` or `BYTES` attr sees no cell, so its +/// rows can only carry NULL, and the planner refuses it. `COUNT` counts the +/// non-null cells of every attr type. pub fn check_agg_reducer( reducer: ArrayReducerAst, attr_name: &str, @@ -192,7 +225,15 @@ pub fn check_agg_reducer( ArrayReducerAst::Sum | ArrayReducerAst::Min | ArrayReducerAst::Max - | ArrayReducerAst::Mean => attr.is_integer() || attr.is_float(), + | ArrayReducerAst::Mean + | ArrayReducerAst::Median + | ArrayReducerAst::Percentile(_) + | ArrayReducerAst::StdDevPop + | ArrayReducerAst::StdDevSamp + | ArrayReducerAst::VarPop + | ArrayReducerAst::VarSamp + | ArrayReducerAst::Mode + | ArrayReducerAst::Histogram { .. } => attr.is_integer() || attr.is_float(), }; if answers { Ok(()) @@ -267,8 +308,8 @@ fn dim_type_to_sql(t: ArrayDimType) -> SqlDataType { } } -/// The column of attr `name`, typed to its wire type: `UINT8` and `INT16` -/// are `int2`, `UINT16` and `INT32` are `int4`, `UINT32` and `INT64` are +/// The column of attr `name`, typed to its wire type: `INT8`, `UINT8` and +/// `INT16` are `int2`, `UINT16` and `INT32` are `int4`, `UINT32` and `INT64` are /// `int8`, `FLOAT32` is `float4`, and `FLOAT64` is `float8`. An unsigned /// type takes the next signed width, so every stored value fits. A width of /// `None` keeps the base `int8` / `float8`. @@ -287,7 +328,7 @@ fn attr_wire_type(t: ArrayAttrType) -> (SqlDataType, Option, Option { (SqlDataType::Int64, Some(IntWidth::I32), None) } - ArrayAttrType::Int16 | ArrayAttrType::UInt8 => { + ArrayAttrType::Int16 | ArrayAttrType::UInt8 | ArrayAttrType::Int8 => { (SqlDataType::Int64, Some(IntWidth::I16), None) } ArrayAttrType::Float64 => (SqlDataType::Float64, None, None), @@ -298,7 +339,8 @@ fn attr_wire_type(t: ArrayAttrType) -> (SqlDataType, Option, Option int8(), - ArrayReducerAst::Mean => float8(), + ArrayReducerAst::Mean + | ArrayReducerAst::Median + | ArrayReducerAst::Percentile(_) + | ArrayReducerAst::StdDevPop + | ArrayReducerAst::StdDevSamp + | ArrayReducerAst::VarPop + | ArrayReducerAst::VarSamp + | ArrayReducerAst::Histogram { .. } => float8(), ArrayReducerAst::Sum if attr.is_integer() => int8(), ArrayReducerAst::Sum => float8(), - ArrayReducerAst::Min | ArrayReducerAst::Max => attr_column("result", attr, true), + ArrayReducerAst::Min | ArrayReducerAst::Max | ArrayReducerAst::Mode => { + attr_column("result", attr, true) + } }) } @@ -395,6 +446,7 @@ mod tests { name: "nir".into(), dtype: ArrayAttrType::Float64, nullable: true, + meta: Default::default(), }], tile_extents: vec![2], georef: None, @@ -407,6 +459,7 @@ mod tests { slice: ArraySliceAst::default(), zone: None, temporal, + physical: false, }; let names = |plan: &ArrayAggPlan| -> Vec { array_agg_row_columns(&view, plan) @@ -426,6 +479,72 @@ mod tests { ); } + #[test] + fn histograms_and_physical_reads_announce_their_columns() { + use crate::types_array::{ArrayAttrMetaAst, ArrayDimAst, ArrayDomainBound, ArraySliceAst}; + let view = ArrayCatalogView { + name: "img".into(), + dims: vec![ArrayDimAst { + name: "x".into(), + dtype: ArrayDimType::Int64, + lo: ArrayDomainBound::Int64(0), + hi: ArrayDomainBound::Int64(3), + }], + attrs: vec![ArrayAttrAst { + name: "red".into(), + dtype: ArrayAttrType::UInt16, + nullable: false, + meta: ArrayAttrMetaAst { + nodata: Some(0.0), + scale: Some(0.5), + ..ArrayAttrMetaAst::default() + }, + }], + tile_extents: vec![2], + georef: None, + }; + let agg = |reducer, physical| ArrayAggPlan { + name: "img".into(), + attr: "red".into(), + reducer, + group_by_dim: Some("x".into()), + slice: ArraySliceAst::default(), + zone: None, + temporal: TemporalScope::default(), + physical, + }; + let hist = array_agg_columns( + &view, + &agg( + ArrayReducerAst::Histogram { + bins: 4, + lo: 0.0, + hi: 1.0, + }, + false, + ), + ) + .expect("answerable"); + let names: Vec<&str> = hist.iter().map(|c| c.name.as_str()).collect(); + assert_eq!(names, ["group", "bin_lo", "bin_hi", "count"]); + let raw_max = array_agg_columns(&view, &agg(ArrayReducerAst::Max, false)).expect("max"); + assert_eq!(raw_max[1].int_width, Some(IntWidth::I32)); + let physical_max = + array_agg_columns(&view, &agg(ArrayReducerAst::Max, true)).expect("physical max"); + assert_eq!(physical_max[1].data_type, SqlDataType::Float64); + let mode = array_agg_columns(&view, &agg(ArrayReducerAst::Mode, false)).expect("mode"); + assert_eq!(mode[1].int_width, Some(IntWidth::I32)); + let median = array_agg_columns(&view, &agg(ArrayReducerAst::Median, false)).expect("med"); + assert_eq!(median[1].data_type, SqlDataType::Float64); + + let physical = array_relation_columns(&view, &[], true); + assert_eq!(physical[1].data_type, SqlDataType::Float64); + assert!(physical[1].nullable, "a nodata cell reads as NULL"); + let raw = array_relation_columns(&view, &[], false); + assert_eq!(raw[1].int_width, Some(IntWidth::I32)); + assert!(!raw[1].nullable); + } + #[test] fn non_numeric_attrs_refuse_numeric_reducers() { for attr in [ArrayAttrType::String, ArrayAttrType::Bytes] { @@ -434,6 +553,9 @@ mod tests { ArrayReducerAst::Mean, ArrayReducerAst::Min, ArrayReducerAst::Max, + ArrayReducerAst::Median, + ArrayReducerAst::Mode, + ArrayReducerAst::VarPop, ] { match agg_result_column(reducer, "label", attr) { Err(SqlError::TypeMismatch { detail }) => { diff --git a/nodedb-sql/src/temporal.rs b/nodedb-sql/src/temporal.rs index dcd0ff331..00ebfbac5 100644 --- a/nodedb-sql/src/temporal.rs +++ b/nodedb-sql/src/temporal.rs @@ -9,8 +9,9 @@ //! statement, threaded through `plan_sql` into each `SqlPlan::Scan`, and //! finally honored by the engine's Data Plane handler. //! -//! All timestamps are **milliseconds since Unix epoch**. The edge store -//! converts to HLC ordinal via `nodedb_types::ms_to_ordinal_upper`. A +//! All timestamps are **milliseconds since Unix epoch**. The edge store and +//! the document versioned store key versions by system-time ordinal, so a +//! system-time cut converts via `nodedb_types::ms_to_ordinal_upper`. A //! `TemporalScope::default()` value is equivalent to "current state" //! (no temporal qualifier) — every scan site must construct one even when //! the query is not temporal. diff --git a/nodedb-sql/src/types/collection.rs b/nodedb-sql/src/types/collection.rs index ffa202ce2..1f3261ba9 100644 --- a/nodedb-sql/src/types/collection.rs +++ b/nodedb-sql/src/types/collection.rs @@ -19,7 +19,7 @@ pub struct CollectionInfo { /// choosing an index lookup — only `Ready` indexes back query rewrites. pub indexes: Vec, /// When `true`, this collection stores every write as an immutable - /// version keyed by `system_from_ms`. Enables `FOR SYSTEM_TIME AS OF` + /// version keyed by its system time. Enables `FOR SYSTEM_TIME AS OF` /// and `FOR VALID_TIME` queries. Only meaningful for document engines /// today; other engines ignore this flag. pub bitemporal: bool, diff --git a/nodedb-sql/src/types/plan/variants/array.rs b/nodedb-sql/src/types/plan/variants/array.rs index ad68f5573..3ff0a1127 100644 --- a/nodedb-sql/src/types/plan/variants/array.rs +++ b/nodedb-sql/src/types/plan/variants/array.rs @@ -35,6 +35,8 @@ pub struct AlterArrayPlan { pub minimum_audit_retain_ms: Option, /// Georef change. `None` = georef unchanged. pub georef: Option, + /// Attr metadata change. `None` = every attr's metadata unchanged. + pub attr_meta: Option, } /// Payload of [`SqlPlan::InsertArray`](crate::types::SqlPlan::InsertArray). @@ -66,6 +68,9 @@ pub struct ArraySlicePlan { /// Plane returns the live (current) state — the default fast path. /// Populated from `AS OF SYSTEM TIME` / `AS OF VALID TIME` clauses. pub temporal: TemporalScope, + /// `physical => true`: a nodata cell reads as NULL, and a numeric cell + /// of an attr with a scale or offset as `raw * scale + offset`. + pub physical: bool, } /// Payload of [`SqlPlan::ArrayAgg`](crate::types::SqlPlan::ArrayAgg). @@ -84,6 +89,9 @@ pub struct ArrayAggPlan { /// Plane aggregates against the live (current) state — the default /// fast path. Populated from `AS OF SYSTEM TIME` / `AS OF VALID TIME`. pub temporal: TemporalScope, + /// `physical => true`: reduce `raw * scale + offset` for an attr with a + /// scale or offset. A nodata cell never contributes, either way. + pub physical: bool, } /// Payload of [`SqlPlan::ArrayElementwise`](crate::types::SqlPlan::ArrayElementwise). diff --git a/nodedb-sql/src/types/query.rs b/nodedb-sql/src/types/query.rs index f6a3aaf28..7b1c88a11 100644 --- a/nodedb-sql/src/types/query.rs +++ b/nodedb-sql/src/types/query.rs @@ -53,6 +53,7 @@ impl JoinType { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SpatialPredicate { DWithin, + DWithin3D, Contains, Intersects, Within, diff --git a/nodedb-sql/src/types_array.rs b/nodedb-sql/src/types_array.rs deleted file mode 100644 index 2ef706ab7..000000000 --- a/nodedb-sql/src/types_array.rs +++ /dev/null @@ -1,273 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -//! Engine-agnostic AST shapes for array DDL/DML carried on `SqlPlan`. -//! -//! Kept free of any dependency on `nodedb-array` so the SQL crate stays -//! lean. The Origin-side SqlPlan→PhysicalPlan converter is responsible -//! for translating these into typed `nodedb_array::ArraySchema` and -//! engine-typed coord/cell values before crossing the bridge. - -/// Dimension type tag (mirrors `nodedb_array::schema::DimType`). -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ArrayDimType { - Int64, - Float64, - TimestampMs, - String, -} - -/// Attribute type tag (mirrors `nodedb_array::schema::AttrType`). -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ArrayAttrType { - Int64, - Float64, - String, - Bytes, - UInt8, - Int16, - UInt16, - Int32, - UInt32, - Float32, -} - -impl ArrayAttrType { - /// Whether the type holds integers. - pub fn is_integer(self) -> bool { - match self { - ArrayAttrType::Int64 - | ArrayAttrType::UInt8 - | ArrayAttrType::Int16 - | ArrayAttrType::UInt16 - | ArrayAttrType::Int32 - | ArrayAttrType::UInt32 => true, - ArrayAttrType::Float64 - | ArrayAttrType::Float32 - | ArrayAttrType::String - | ArrayAttrType::Bytes => false, - } - } - - /// Whether the type holds floats. - pub fn is_float(self) -> bool { - match self { - ArrayAttrType::Float64 | ArrayAttrType::Float32 => true, - ArrayAttrType::Int64 - | ArrayAttrType::UInt8 - | ArrayAttrType::Int16 - | ArrayAttrType::UInt16 - | ArrayAttrType::Int32 - | ArrayAttrType::UInt32 - | ArrayAttrType::String - | ArrayAttrType::Bytes => false, - } - } - - /// The canonical DDL name of the type. - pub fn sql_name(self) -> &'static str { - match self { - ArrayAttrType::Int64 => "INT64", - ArrayAttrType::Float64 => "FLOAT64", - ArrayAttrType::String => "STRING", - ArrayAttrType::Bytes => "BYTES", - ArrayAttrType::UInt8 => "UINT8", - ArrayAttrType::Int16 => "INT16", - ArrayAttrType::UInt16 => "UINT16", - ArrayAttrType::Int32 => "INT32", - ArrayAttrType::UInt32 => "UINT32", - ArrayAttrType::Float32 => "FLOAT32", - } - } -} - -/// Cell-order strategy AST mirror. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -pub enum ArrayCellOrderAst { - RowMajor, - ColMajor, - #[default] - Hilbert, - ZOrder, -} - -/// Tile-order strategy AST mirror. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -pub enum ArrayTileOrderAst { - RowMajor, - ColMajor, - #[default] - Hilbert, - ZOrder, -} - -/// Domain bound carrying its dim's type tag. -#[derive(Debug, Clone, PartialEq)] -pub enum ArrayDomainBound { - Int64(i64), - Float64(f64), - TimestampMs(i64), - String(String), -} - -/// One dim spec in `CREATE ARRAY ... DIMS (...)`. -#[derive(Debug, Clone, PartialEq)] -pub struct ArrayDimAst { - pub name: String, - pub dtype: ArrayDimType, - pub lo: ArrayDomainBound, - pub hi: ArrayDomainBound, -} - -/// One attribute spec in `CREATE ARRAY ... ATTRS (...)`. -#[derive(Debug, Clone, PartialEq)] -pub struct ArrayAttrAst { - pub name: String, - pub dtype: ArrayAttrType, - pub nullable: bool, -} - -/// One coord scalar in `INSERT INTO ARRAY ... COORDS (...)` — a literal -/// value typed loosely; the converter coerces against the schema's -/// declared dim type. -#[derive(Debug, Clone, PartialEq)] -pub enum ArrayCoordLiteral { - Int64(i64), - Float64(f64), - String(String), -} - -/// One attribute literal in `... VALUES (...)`. `Null` permitted only -/// for nullable attrs (validated in the converter). -#[derive(Debug, Clone, PartialEq)] -pub enum ArrayAttrLiteral { - Int64(i64), - Float64(f64), - String(String), - Bytes(Vec), - Null, -} - -/// One row of an `INSERT INTO ARRAY ...`: `COORDS (...) VALUES (...)`. -#[derive(Debug, Clone, PartialEq)] -pub struct ArrayInsertRow { - pub coords: Vec, - pub attrs: Vec, -} - -/// One named `dim: [lo, hi]` entry in a slice predicate. -/// -/// The literal type is loose at planning time; the converter coerces -/// against the array's declared dim dtype before encoding to wire. -#[derive(Debug, Clone, PartialEq)] -pub struct NamedDimRange { - /// Schema dim name. Resolved to a positional index by the converter. - pub dim: String, - pub lo: ArrayCoordLiteral, - pub hi: ArrayCoordLiteral, -} - -/// `ARRAY_SLICE(...)` slice predicate: an unordered set of named -/// dim ranges, expanded to per-position `Option` by the -/// converter (unconstrained dims become `None`). -#[derive(Debug, Clone, Default, PartialEq)] -pub struct ArraySliceAst { - pub dim_ranges: Vec, -} - -/// `ARRAY_AGG(...)` / `ARRAY_SLICE(...)` polygon zone over two numeric dims. -/// -/// `polygons` holds one entry per member polygon: its exterior ring, then -/// its holes. Each ring is closed and lists `[x, y]` positions, `x` on -/// `x_dim` and `y` on `y_dim`. -/// -/// `srid: None` means the positions are dim coordinates. `Some` means they -/// are in that SRID, and `x_dim` / `y_dim` are the array's GEOREF axes. -#[derive(Debug, Clone, PartialEq)] -pub struct ArrayZoneAst { - pub polygons: Vec>>, - pub x_dim: String, - pub y_dim: String, - pub srid: Option, -} - -/// `GEOREF (SRID n, X dim, Y dim [, TRANSFORM (gt0, .., gt5)])` clause. -/// -/// `transform: None` means the dims hold pixel-centre world coordinates. -/// `Some` means the dims hold `INT64` pixel indices and the six -/// coefficients map them to world coordinates in GDAL order. -#[derive(Debug, Clone, PartialEq)] -pub struct ArrayGeorefAst { - pub srid: u32, - pub x_dim: String, - pub y_dim: String, - pub transform: Option<[f64; 6]>, -} - -/// Georef change carried by `ALTER ARRAY`. -#[derive(Debug, Clone, PartialEq)] -pub enum ArrayGeorefAlter { - /// `SET GEOREF (...)`: install or replace the georef. - Set(ArrayGeorefAst), - /// `DROP GEOREF`: remove the georef. - Drop, -} - -/// Reducer carried on `SqlPlan::ArrayAgg`. Mirrors -/// `crate::bridge::physical_plan::ArrayReducer` without the bridge dep. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ArrayReducerAst { - Sum, - Count, - Min, - Max, - Mean, -} - -impl ArrayReducerAst { - /// Parse the SQL-surface string ('sum', 'count', ...). Returns `None` - /// for any other value so the planner can surface an `Unsupported` - /// error with the offending token. - pub fn parse(s: &str) -> Option { - match s.to_ascii_lowercase().as_str() { - "sum" => Some(ArrayReducerAst::Sum), - "count" => Some(ArrayReducerAst::Count), - "min" => Some(ArrayReducerAst::Min), - "max" => Some(ArrayReducerAst::Max), - "mean" | "avg" => Some(ArrayReducerAst::Mean), - _ => None, - } - } - - /// The uppercase SQL name of the reducer. - pub fn sql_name(self) -> &'static str { - match self { - ArrayReducerAst::Sum => "SUM", - ArrayReducerAst::Count => "COUNT", - ArrayReducerAst::Min => "MIN", - ArrayReducerAst::Max => "MAX", - ArrayReducerAst::Mean => "MEAN", - } - } -} - -/// Pairwise op carried on `SqlPlan::ArrayElementwise`. Mirrors -/// `crate::bridge::physical_plan::ArrayBinaryOp`. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ArrayBinaryOpAst { - Add, - Sub, - Mul, - Div, -} - -impl ArrayBinaryOpAst { - pub fn parse(s: &str) -> Option { - match s.to_ascii_lowercase().as_str() { - "add" | "+" => Some(ArrayBinaryOpAst::Add), - "sub" | "-" => Some(ArrayBinaryOpAst::Sub), - "mul" | "*" => Some(ArrayBinaryOpAst::Mul), - "div" | "/" => Some(ArrayBinaryOpAst::Div), - _ => None, - } - } -} diff --git a/nodedb-sql/src/types_array/attr.rs b/nodedb-sql/src/types_array/attr.rs new file mode 100644 index 000000000..efd33d27f --- /dev/null +++ b/nodedb-sql/src/types_array/attr.rs @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Attr specs and their reading metadata. + +use super::schema::ArrayAttrType; + +/// One attribute spec in `CREATE ARRAY ... ATTRS (...)`. +#[derive(Debug, Clone, PartialEq)] +pub struct ArrayAttrAst { + pub name: String, + pub dtype: ArrayAttrType, + pub nullable: bool, + /// `NODATA`, `SCALE`, `OFFSET` and `UNITS`. + pub meta: ArrayAttrMetaAst, +} + +/// Reading metadata of one attr (mirrors `nodedb_array::schema::AttrMeta`). +/// +/// A stored cell holds the raw value. A physical read gives +/// `raw * scale + offset`, and a cell equal to `nodata` holds no data. +#[derive(Debug, Clone, Default, PartialEq)] +pub struct ArrayAttrMetaAst { + pub nodata: Option, + pub scale: Option, + pub offset: Option, + pub units: Option, +} + +impl ArrayAttrMetaAst { + /// Whether a physical read changes a numeric cell's value. + pub fn has_transform(&self) -> bool { + self.scale.is_some() || self.offset.is_some() + } +} + +/// One metadata field of an attr, as `DROP` names it. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ArrayAttrMetaKey { + Nodata, + Scale, + Offset, + Units, +} + +impl ArrayAttrMetaKey { + /// The DDL keyword. + pub fn sql_name(self) -> &'static str { + match self { + ArrayAttrMetaKey::Nodata => "NODATA", + ArrayAttrMetaKey::Scale => "SCALE", + ArrayAttrMetaKey::Offset => "OFFSET", + ArrayAttrMetaKey::Units => "UNITS", + } + } + + /// Whether `meta` holds this field. + pub fn is_set(self, meta: &ArrayAttrMetaAst) -> bool { + match self { + ArrayAttrMetaKey::Nodata => meta.nodata.is_some(), + ArrayAttrMetaKey::Scale => meta.scale.is_some(), + ArrayAttrMetaKey::Offset => meta.offset.is_some(), + ArrayAttrMetaKey::Units => meta.units.is_some(), + } + } + + /// Clear this field of `meta`. + pub fn clear(self, meta: &mut ArrayAttrMetaAst) { + match self { + ArrayAttrMetaKey::Nodata => meta.nodata = None, + ArrayAttrMetaKey::Scale => meta.scale = None, + ArrayAttrMetaKey::Offset => meta.offset = None, + ArrayAttrMetaKey::Units => meta.units = None, + } + } +} + +/// `ALTER ARRAY a ALTER ATTR SET (...)` or `... DROP , ...`. +#[derive(Debug, Clone, PartialEq)] +pub struct ArrayAttrMetaAlter { + pub attr: String, + pub change: ArrayAttrMetaChange, +} + +/// The change an attr-metadata `ALTER` makes. +#[derive(Debug, Clone, PartialEq)] +pub enum ArrayAttrMetaChange { + /// Set each given field. A field the clause omits keeps its value. + Set(ArrayAttrMetaAst), + /// Remove each named field. Each must be set. + Drop(Vec), +} + +impl ArrayAttrMetaChange { + /// `current` with this change applied. + pub fn apply(&self, current: &ArrayAttrMetaAst) -> ArrayAttrMetaAst { + let mut out = current.clone(); + match self { + ArrayAttrMetaChange::Set(set) => { + if set.nodata.is_some() { + out.nodata = set.nodata; + } + if set.scale.is_some() { + out.scale = set.scale; + } + if set.offset.is_some() { + out.offset = set.offset; + } + if set.units.is_some() { + out.units = set.units.clone(); + } + } + ArrayAttrMetaChange::Drop(keys) => { + for key in keys { + key.clear(&mut out); + } + } + } + out + } +} diff --git a/nodedb-sql/src/types_array/mod.rs b/nodedb-sql/src/types_array/mod.rs new file mode 100644 index 000000000..f20c6c3ea --- /dev/null +++ b/nodedb-sql/src/types_array/mod.rs @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Engine-agnostic AST shapes for array DDL/DML carried on `SqlPlan`. +//! +//! Kept free of any dependency on `nodedb-array` so the SQL crate stays +//! lean. The Origin-side SqlPlan→PhysicalPlan converter is responsible +//! for translating these into typed `nodedb_array::ArraySchema` and +//! engine-typed coord/cell values before crossing the bridge. + +pub mod attr; +pub mod ops; +pub mod query; +pub mod schema; + +pub use attr::{ + ArrayAttrAst, ArrayAttrMetaAlter, ArrayAttrMetaAst, ArrayAttrMetaChange, ArrayAttrMetaKey, +}; +pub use ops::{ArrayBinaryOpAst, ArrayReducerAst, ArrayReducerName}; +pub use query::{ + ArrayAttrLiteral, ArrayCoordLiteral, ArrayGeorefAlter, ArrayGeorefAst, ArrayInsertRow, + ArraySliceAst, ArrayZoneAst, NamedDimRange, +}; +pub use schema::{ + ArrayAttrType, ArrayCellOrderAst, ArrayDimAst, ArrayDimType, ArrayDomainBound, + ArrayTileOrderAst, +}; diff --git a/nodedb-sql/src/types_array/ops.rs b/nodedb-sql/src/types_array/ops.rs new file mode 100644 index 000000000..db0d4a4d3 --- /dev/null +++ b/nodedb-sql/src/types_array/ops.rs @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Reducers and pairwise ops carried on array plans. + +/// Reducer carried on `SqlPlan::ArrayAgg`. Mirrors +/// `nodedb_physical::physical_plan::ArrayReducer` without the dependency. +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum ArrayReducerAst { + Sum, + Count, + Min, + Max, + Mean, + Median, + /// Exact percentile, `0 <= p <= 1`. + Percentile(f64), + StdDevPop, + StdDevSamp, + VarPop, + VarSamp, + Mode, + /// Fixed-width bins over `[lo, hi]`. + Histogram { + bins: u32, + lo: f64, + hi: f64, + }, +} + +/// A reducer name of the SQL surface, before its named arguments. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ArrayReducerName { + Sum, + Count, + Min, + Max, + Mean, + Median, + Percentile, + StdDevPop, + StdDevSamp, + VarPop, + VarSamp, + Mode, + Histogram, +} + +impl ArrayReducerName { + /// Every reducer name the SQL surface accepts, for error messages. + pub const ACCEPTED: &'static str = "sum, count, min, max, mean (avg), median, percentile, \ + stddev (stddev_samp), stddev_pop, variance (var_samp), var_pop, mode, histogram"; + + /// Parse the reducer string (`'sum'`, `'stddev_pop'`, ...). `stddev` + /// and `variance` are the sample forms, as in PostgreSQL. `None` for + /// any other value. + pub fn parse(s: &str) -> Option { + Some(match s.to_ascii_lowercase().as_str() { + "sum" => ArrayReducerName::Sum, + "count" => ArrayReducerName::Count, + "min" => ArrayReducerName::Min, + "max" => ArrayReducerName::Max, + "mean" | "avg" => ArrayReducerName::Mean, + "median" => ArrayReducerName::Median, + "percentile" | "percentile_cont" => ArrayReducerName::Percentile, + "stddev" | "stddev_samp" => ArrayReducerName::StdDevSamp, + "stddev_pop" => ArrayReducerName::StdDevPop, + "variance" | "var_samp" => ArrayReducerName::VarSamp, + "var_pop" => ArrayReducerName::VarPop, + "mode" => ArrayReducerName::Mode, + "histogram" => ArrayReducerName::Histogram, + _ => return None, + }) + } +} + +impl ArrayReducerAst { + /// The uppercase SQL name of the reducer. + pub fn sql_name(self) -> &'static str { + match self { + ArrayReducerAst::Sum => "SUM", + ArrayReducerAst::Count => "COUNT", + ArrayReducerAst::Min => "MIN", + ArrayReducerAst::Max => "MAX", + ArrayReducerAst::Mean => "MEAN", + ArrayReducerAst::Median => "MEDIAN", + ArrayReducerAst::Percentile(_) => "PERCENTILE", + ArrayReducerAst::StdDevPop => "STDDEV_POP", + ArrayReducerAst::StdDevSamp => "STDDEV_SAMP", + ArrayReducerAst::VarPop => "VAR_POP", + ArrayReducerAst::VarSamp => "VAR_SAMP", + ArrayReducerAst::Mode => "MODE", + ArrayReducerAst::Histogram { .. } => "HISTOGRAM", + } + } +} + +/// Pairwise op carried on `SqlPlan::ArrayElementwise`. Mirrors +/// `crate::bridge::physical_plan::ArrayBinaryOp`. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ArrayBinaryOpAst { + Add, + Sub, + Mul, + Div, +} + +impl ArrayBinaryOpAst { + pub fn parse(s: &str) -> Option { + match s.to_ascii_lowercase().as_str() { + "add" | "+" => Some(ArrayBinaryOpAst::Add), + "sub" | "-" => Some(ArrayBinaryOpAst::Sub), + "mul" | "*" => Some(ArrayBinaryOpAst::Mul), + "div" | "/" => Some(ArrayBinaryOpAst::Div), + _ => None, + } + } +} diff --git a/nodedb-sql/src/types_array/query.rs b/nodedb-sql/src/types_array/query.rs new file mode 100644 index 000000000..32d9958f9 --- /dev/null +++ b/nodedb-sql/src/types_array/query.rs @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Literals, slice boxes, zones and georefs carried on array plans. + +/// One coord scalar in `INSERT INTO ARRAY ... COORDS (...)` — a literal +/// value typed loosely; the converter coerces against the schema's +/// declared dim type. +#[derive(Debug, Clone, PartialEq)] +pub enum ArrayCoordLiteral { + Int64(i64), + Float64(f64), + String(String), +} + +/// One attribute literal in `... VALUES (...)`. `Null` permitted only +/// for nullable attrs (validated in the converter). +#[derive(Debug, Clone, PartialEq)] +pub enum ArrayAttrLiteral { + Int64(i64), + Float64(f64), + String(String), + Bytes(Vec), + Null, +} + +/// One row of an `INSERT INTO ARRAY ...`: `COORDS (...) VALUES (...)`. +#[derive(Debug, Clone, PartialEq)] +pub struct ArrayInsertRow { + pub coords: Vec, + pub attrs: Vec, +} + +/// One named `dim: [lo, hi]` entry in a slice predicate. +/// +/// The literal type is loose at planning time; the converter coerces +/// against the array's declared dim dtype before encoding to wire. +#[derive(Debug, Clone, PartialEq)] +pub struct NamedDimRange { + /// Schema dim name. Resolved to a positional index by the converter. + pub dim: String, + pub lo: ArrayCoordLiteral, + pub hi: ArrayCoordLiteral, +} + +/// `ARRAY_SLICE(...)` slice predicate: an unordered set of named +/// dim ranges, expanded to per-position `Option` by the +/// converter (unconstrained dims become `None`). +#[derive(Debug, Clone, Default, PartialEq)] +pub struct ArraySliceAst { + pub dim_ranges: Vec, +} + +/// `ARRAY_AGG(...)` / `ARRAY_SLICE(...)` polygon zone over two numeric dims. +/// +/// `polygons` holds one entry per member polygon: its exterior ring, then +/// its holes. Each ring is closed and lists `[x, y]` positions, `x` on +/// `x_dim` and `y` on `y_dim`. +/// +/// `srid: None` means the positions are dim coordinates. `Some` means they +/// are in that SRID, and `x_dim` / `y_dim` are the array's GEOREF axes. +#[derive(Debug, Clone, PartialEq)] +pub struct ArrayZoneAst { + pub polygons: Vec>>, + pub x_dim: String, + pub y_dim: String, + pub srid: Option, +} + +/// `GEOREF (SRID n, X dim, Y dim [, TRANSFORM (gt0, .., gt5)])` clause. +/// +/// `transform: None` means the dims hold pixel-centre world coordinates. +/// `Some` means the dims hold `INT64` pixel indices and the six +/// coefficients map them to world coordinates in GDAL order. +#[derive(Debug, Clone, PartialEq)] +pub struct ArrayGeorefAst { + pub srid: u32, + pub x_dim: String, + pub y_dim: String, + pub transform: Option<[f64; 6]>, +} + +/// Georef change carried by `ALTER ARRAY`. +#[derive(Debug, Clone, PartialEq)] +pub enum ArrayGeorefAlter { + /// `SET GEOREF (...)`: install or replace the georef. + Set(ArrayGeorefAst), + /// `DROP GEOREF`: remove the georef. + Drop, +} diff --git a/nodedb-sql/src/types_array/schema.rs b/nodedb-sql/src/types_array/schema.rs new file mode 100644 index 000000000..690e88dd2 --- /dev/null +++ b/nodedb-sql/src/types_array/schema.rs @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Dim, attr type, order and bound shapes of `CREATE ARRAY`. + +/// Dimension type tag (mirrors `nodedb_array::schema::DimType`). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ArrayDimType { + Int64, + Float64, + TimestampMs, + String, +} + +/// Attribute type tag (mirrors `nodedb_array::schema::AttrType`). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ArrayAttrType { + Int64, + Float64, + String, + Bytes, + UInt8, + Int16, + UInt16, + Int32, + UInt32, + Float32, + /// 1-byte signed integer. The array DDL names integer widths in bits, + /// so `INT8` here is one byte, not PostgreSQL's 8-byte `int8`. + Int8, +} + +impl ArrayAttrType { + /// Whether the type holds integers. + pub fn is_integer(self) -> bool { + match self { + ArrayAttrType::Int64 + | ArrayAttrType::Int8 + | ArrayAttrType::UInt8 + | ArrayAttrType::Int16 + | ArrayAttrType::UInt16 + | ArrayAttrType::Int32 + | ArrayAttrType::UInt32 => true, + ArrayAttrType::Float64 + | ArrayAttrType::Float32 + | ArrayAttrType::String + | ArrayAttrType::Bytes => false, + } + } + + /// Whether the type holds floats. + pub fn is_float(self) -> bool { + match self { + ArrayAttrType::Float64 | ArrayAttrType::Float32 => true, + ArrayAttrType::Int64 + | ArrayAttrType::Int8 + | ArrayAttrType::UInt8 + | ArrayAttrType::Int16 + | ArrayAttrType::UInt16 + | ArrayAttrType::Int32 + | ArrayAttrType::UInt32 + | ArrayAttrType::String + | ArrayAttrType::Bytes => false, + } + } + + /// The canonical DDL name of the type. + pub fn sql_name(self) -> &'static str { + match self { + ArrayAttrType::Int64 => "INT64", + ArrayAttrType::Float64 => "FLOAT64", + ArrayAttrType::String => "STRING", + ArrayAttrType::Bytes => "BYTES", + ArrayAttrType::UInt8 => "UINT8", + ArrayAttrType::Int16 => "INT16", + ArrayAttrType::UInt16 => "UINT16", + ArrayAttrType::Int32 => "INT32", + ArrayAttrType::UInt32 => "UINT32", + ArrayAttrType::Float32 => "FLOAT32", + ArrayAttrType::Int8 => "INT8", + } + } +} + +/// Cell-order strategy AST mirror. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum ArrayCellOrderAst { + RowMajor, + ColMajor, + #[default] + Hilbert, + ZOrder, +} + +/// Tile-order strategy AST mirror. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum ArrayTileOrderAst { + RowMajor, + ColMajor, + #[default] + Hilbert, + ZOrder, +} + +/// Domain bound carrying its dim's type tag. +#[derive(Debug, Clone, PartialEq)] +pub enum ArrayDomainBound { + Int64(i64), + Float64(f64), + TimestampMs(i64), + String(String), +} + +/// One dim spec in `CREATE ARRAY ... DIMS (...)`. +#[derive(Debug, Clone, PartialEq)] +pub struct ArrayDimAst { + pub name: String, + pub dtype: ArrayDimType, + pub lo: ArrayDomainBound, + pub hi: ArrayDomainBound, +} diff --git a/nodedb-sql/src/types_expr.rs b/nodedb-sql/src/types_expr.rs index 9e5c65004..60cdfdc6b 100644 --- a/nodedb-sql/src/types_expr.rs +++ b/nodedb-sql/src/types_expr.rs @@ -26,6 +26,9 @@ pub enum SqlValue { Timestamp(NdbDateTime), /// Typed timezone-aware timestamp literal: `TIMESTAMPTZ '...'` or `'...'::TIMESTAMPTZ`. Timestamptz(NdbDateTime), + /// A geometry: what a geometry-returning call folds to. Carries its + /// SRID, Z and M. + Geometry(nodedb_types::geometry::SridGeometry), } /// SQL-side payload-bitmap predicate atom. Mirrors `nodedb_types::PayloadAtom` diff --git a/nodedb-sql/src/visitor/plan_visitor/args.rs b/nodedb-sql/src/visitor/plan_visitor/args.rs index 249176b1f..f57e80078 100644 --- a/nodedb-sql/src/visitor/plan_visitor/args.rs +++ b/nodedb-sql/src/visitor/plan_visitor/args.rs @@ -17,8 +17,8 @@ use crate::types::query::{ AggregateExpr, EngineType, JoinType, Projection, SortKey, SpatialPredicate, WindowSpec, }; use crate::types_array::{ - ArrayAttrAst, ArrayCellOrderAst, ArrayDimAst, ArrayGeorefAlter, ArrayGeorefAst, - ArrayReducerAst, ArraySliceAst, ArrayTileOrderAst, ArrayZoneAst, + ArrayAttrAst, ArrayAttrMetaAlter, ArrayCellOrderAst, ArrayDimAst, ArrayGeorefAlter, + ArrayGeorefAst, ArrayReducerAst, ArraySliceAst, ArrayTileOrderAst, ArrayZoneAst, }; use crate::types_expr::{SqlExpr, SqlPayloadAtom, SqlValue}; use nodedb_types::vector_distance::DistanceMetric; @@ -273,6 +273,8 @@ pub struct AlterArrayVisitArgs<'a> { pub minimum_audit_retain_ms: Option, /// Georef change. `None` = georef unchanged. pub georef: Option<&'a ArrayGeorefAlter>, + /// Attr metadata change. `None` = unchanged. + pub attr_meta: Option<&'a ArrayAttrMetaAlter>, } /// Parameters for [`super::trait_def::PlanVisitor::array_slice`]. @@ -287,6 +289,8 @@ pub struct ArraySliceVisitArgs<'a> { /// Polygon zone inside the box. `None` = the whole box. pub zone: Option<&'a ArrayZoneAst>, pub temporal: &'a TemporalScope, + /// Read attrs physically. + pub physical: bool, } /// Parameters for [`super::trait_def::PlanVisitor::array_agg`]. @@ -300,6 +304,8 @@ pub struct ArrayAggVisitArgs<'a> { /// Polygon zone inside the box. `None` = the whole box. pub zone: Option<&'a ArrayZoneAst>, pub temporal: &'a TemporalScope, + /// Reduce physical values. + pub physical: bool, } /// Parameters for [`super::trait_def::PlanVisitor::merge`]. diff --git a/nodedb-sql/src/visitor/plan_visitor/dispatch_rest.rs b/nodedb-sql/src/visitor/plan_visitor/dispatch_rest.rs index feb5be807..ac8df1616 100644 --- a/nodedb-sql/src/visitor/plan_visitor/dispatch_rest.rs +++ b/nodedb-sql/src/visitor/plan_visitor/dispatch_rest.rs @@ -78,11 +78,13 @@ pub(super) fn dispatch_rest( audit_retain_ms, minimum_audit_retain_ms, georef, + attr_meta, }) => visitor.alter_array(AlterArrayVisitArgs { name, audit_retain_ms: *audit_retain_ms, minimum_audit_retain_ms: *minimum_audit_retain_ms, georef: georef.as_ref(), + attr_meta: attr_meta.as_ref(), }), SqlPlan::InsertArray(InsertArrayPlan { name, rows }) => visitor.insert_array(name, rows), SqlPlan::DeleteArray(DeleteArrayPlan { name, coords }) => { @@ -95,6 +97,7 @@ pub(super) fn dispatch_rest( limit, zone, temporal, + physical, }) => visitor.array_slice(ArraySliceVisitArgs { name, slice, @@ -102,6 +105,7 @@ pub(super) fn dispatch_rest( limit: *limit, zone: zone.as_ref(), temporal, + physical: *physical, }), SqlPlan::ArrayAgg(ArrayAggPlan { name, @@ -111,6 +115,7 @@ pub(super) fn dispatch_rest( slice, zone, temporal, + physical, }) => visitor.array_agg(ArrayAggVisitArgs { name, attr, @@ -119,6 +124,7 @@ pub(super) fn dispatch_rest( slice, zone: zone.as_ref(), temporal, + physical: *physical, }), SqlPlan::ArrayElementwise(ArrayElementwisePlan { left, diff --git a/nodedb-srid/Cargo.toml b/nodedb-srid/Cargo.toml new file mode 100644 index 000000000..87e985bb7 --- /dev/null +++ b/nodedb-srid/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "nodedb-srid" +description = "Spatial reference system identifiers for NodeDB: SRID codes, CRS names and the resolver trait" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license = "Apache-2.0" +authors = ["Biz Impulse Solutions Sdn. Bhd. and the NodeDB contributors"] +repository.workspace = true +homepage.workspace = true +documentation = "https://docs.rs/nodedb-srid" +keywords = ["crs", "srid", "epsg", "geospatial"] +categories = ["science::geo"] + +[features] +default = [] + +[dependencies] +thiserror = { workspace = true } +serde = { workspace = true } +zerompk = { workspace = true } diff --git a/nodedb-srid/NOTICE b/nodedb-srid/NOTICE new file mode 100644 index 000000000..4bf4bfc89 --- /dev/null +++ b/nodedb-srid/NOTICE @@ -0,0 +1,2 @@ +Copyright 2026 Biz Impulse Solutions Sdn. Bhd. and the NodeDB contributors. +This product is licensed under the Apache License, Version 2.0. diff --git a/nodedb-srid/src/error.rs b/nodedb-srid/src/error.rs new file mode 100644 index 000000000..06d918859 --- /dev/null +++ b/nodedb-srid/src/error.rs @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Errors for SRID resolution and CRS definition parsing. + +use crate::srid::Srid; + +/// An SRID that does not resolve to a usable CRS. +#[derive(Debug, Clone, PartialEq, thiserror::Error)] +pub enum UnknownSrid { + /// The code is outside `1..=Srid::MAX_CODE`. + #[error("SRID {code} is outside 1..={max}", max = Srid::MAX_CODE)] + OutOfRange { code: i64 }, + /// No built-in or user-defined CRS has this code. + #[error("SRID {code} is not defined")] + Undefined { code: u32 }, + /// A user-defined CRS with this code was dropped. + #[error("SRID {code} was dropped")] + Dropped { code: u32 }, + /// The CRS name matches no supported naming form. + #[error("unknown CRS name '{name}'")] + UnknownName { name: Box }, + /// A GeoJSON `crs` member that is neither the named form nor the + /// legacy `EPSG` form. + #[error( + "invalid GeoJSON crs member: {reason}; supported: \ + {{\"type\":\"name\",\"properties\":{{\"name\":\"EPSG:\"}}}} or \ + {{\"type\":\"EPSG\",\"properties\":{{\"code\":}}}}" + )] + InvalidCrsMember { reason: Box }, + /// The CRS exists but its definition uses something this build does + /// not implement. + #[error("SRID {code} is not supported: {cause}")] + Unsupported { code: u32, cause: CrsParseError }, +} + +/// A CRS definition text that does not parse. +#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] +pub enum CrsParseError { + #[error("unknown parameter '+{key}'")] + UnknownParameter { key: Box }, + #[error("projection method '{method}' is not implemented; implemented: {implemented}")] + UnsupportedMethod { + method: Box, + implemented: &'static str, + }, + #[error("invalid value '{value}' for '+{key}'")] + InvalidValue { key: Box, value: Box }, + #[error("method '{method}' needs '+{key}'")] + Missing { + key: &'static str, + method: &'static str, + }, + #[error("malformed {format} text at byte {offset}")] + Malformed { format: &'static str, offset: usize }, + /// Well-formed text whose content is not a CRS this build reads. + #[error("invalid {format} definition: {detail}")] + Invalid { + format: &'static str, + detail: Box, + }, +} + +impl CrsParseError { + /// An [`CrsParseError::Invalid`] in `format` explaining `detail`. + pub fn content(format: &'static str, detail: impl Into>) -> Self { + CrsParseError::Invalid { + format, + detail: detail.into(), + } + } + + /// An [`CrsParseError::InvalidValue`] naming `key` and `value`. + pub fn invalid(key: &str, value: &str) -> Self { + CrsParseError::InvalidValue { + key: key.into(), + value: value.into(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn messages_name_the_input() { + let err = UnknownSrid::Undefined { code: 2154 }; + assert!(err.to_string().contains("2154")); + let err = CrsParseError::invalid("zone", "99"); + assert_eq!(err.to_string(), "invalid value '99' for '+zone'"); + let err = UnknownSrid::InvalidCrsMember { + reason: "type 'link'".into(), + }; + assert!(err.to_string().contains("type 'link'"), "{err}"); + } +} diff --git a/nodedb-srid/src/kind.rs b/nodedb-srid/src/kind.rs new file mode 100644 index 000000000..85620e5ba --- /dev/null +++ b/nodedb-srid/src/kind.rs @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Whether a CRS is geographic or projected. + +use serde::{Deserialize, Serialize}; + +/// Whether coordinates are angles or lengths. +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Eq, + Hash, + Serialize, + Deserialize, + zerompk::ToMessagePack, + zerompk::FromMessagePack, +)] +pub enum CrsKind { + /// x is longitude and y is latitude, degrees. + Geographic, + /// x is easting and y is northing, in the CRS's linear unit. + Projected, +} + +impl CrsKind { + /// The lower-case name: `geographic` or `projected`. + pub const fn as_str(self) -> &'static str { + match self { + CrsKind::Geographic => "geographic", + CrsKind::Projected => "projected", + } + } + + /// The kind `name` spells, as [`CrsKind::as_str`] writes it. + pub fn from_name(name: &str) -> Option { + match name { + "geographic" => Some(CrsKind::Geographic), + "projected" => Some(CrsKind::Projected), + _ => None, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn names_round_trip() { + for kind in [CrsKind::Geographic, CrsKind::Projected] { + assert_eq!(CrsKind::from_name(kind.as_str()), Some(kind)); + } + assert_eq!(CrsKind::from_name("Geographic"), None); + } +} diff --git a/nodedb-srid/src/lib.rs b/nodedb-srid/src/lib.rs new file mode 100644 index 000000000..2a10c88ab --- /dev/null +++ b/nodedb-srid/src/lib.rs @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Spatial reference system identifiers for NodeDB. +//! +//! An [`Srid`] names a CRS by code. A [`SridResolver`] answers whether a +//! code or a CRS name is defined, and whether the CRS is geographic or +//! projected. The registry that defines CRSs, its EPSG table and the +//! coordinate transforms live in `nodedb-crs`, which implements the +//! resolver. Crates that only carry SRIDs depend on this crate alone and +//! never link the table. + +pub mod error; +pub mod kind; +pub mod name; +pub mod resolver; +pub mod srid; + +pub use error::{CrsParseError, UnknownSrid}; +pub use kind::CrsKind; +pub use name::{NODEDB_AUTHORITY, code_from_name}; +pub use resolver::SridResolver; +pub use srid::{InvalidSridCode, NotUserSrid, Srid, SridAuthority}; diff --git a/nodedb-srid/src/name.rs b/nodedb-srid/src/name.rs new file mode 100644 index 000000000..130af3f50 --- /dev/null +++ b/nodedb-srid/src/name.rs @@ -0,0 +1,154 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! CRS names: authority codes, OGC URNs and OGC URLs. + +/// NodeDB code of the MODIS sinusoidal grid, SR-ORG:6842. +const MODIS_SINUSOIDAL_CODE: i64 = 990_001; + +/// SR-ORG code of the MODIS sinusoidal grid. +const SR_ORG_MODIS: &str = "6842"; + +/// EPSG code of WGS 84, which OGC CRS84 maps to. CRS84 has longitude +/// first, the axis order NodeDB uses for every geographic CRS. +const WGS84_CODE: i64 = 4326; + +/// Authority of NodeDB's own and user-defined codes. +pub const NODEDB_AUTHORITY: &str = "NODEDB"; + +/// OGC URN prefix, compared without case. +const URN_PREFIX: &str = "urn:ogc:def:crs:"; + +/// OGC URL prefixes, compared without case. +const URL_PREFIXES: [&str; 2] = [ + "http://www.opengis.net/def/crs/", + "https://www.opengis.net/def/crs/", +]; + +/// The numeric code `name` refers to, before any registry check. `None` +/// when the name matches no supported form. +pub fn code_from_name(name: &str) -> Option { + let name = name.trim(); + if let Some(rest) = strip_prefix_ci(name, URN_PREFIX) { + // AUTHORITY:[version]:code + let mut parts = rest.split(':'); + let authority = parts.next()?; + let _version = parts.next()?; + let code = parts.next()?; + if parts.next().is_some() { + return None; + } + return authority_code(authority, code); + } + if let Some(rest) = URL_PREFIXES.iter().find_map(|p| strip_prefix_ci(name, p)) { + // AUTHORITY/version/code + let mut parts = rest.split('/'); + let authority = parts.next()?; + let _version = parts.next()?; + let code = parts.next()?; + if parts.next().is_some_and(|tail| !tail.is_empty()) { + return None; + } + return authority_code(authority, code); + } + if name.eq_ignore_ascii_case("CRS84") { + return Some(WGS84_CODE); + } + let (authority, code) = name.split_once(':')?; + authority_code(authority, code) +} + +/// Map an authority and its code to a NodeDB code. +fn authority_code(authority: &str, code: &str) -> Option { + if authority.eq_ignore_ascii_case("EPSG") || authority.eq_ignore_ascii_case(NODEDB_AUTHORITY) { + decimal(code) + } else if authority.eq_ignore_ascii_case("OGC") { + code.eq_ignore_ascii_case("CRS84").then_some(WGS84_CODE) + } else if authority.eq_ignore_ascii_case("SR-ORG") { + (code == SR_ORG_MODIS).then_some(MODIS_SINUSOIDAL_CODE) + } else { + None + } +} + +/// A non-empty run of ASCII digits as an integer. +fn decimal(text: &str) -> Option { + if text.is_empty() || !text.bytes().all(|b| b.is_ascii_digit()) { + return None; + } + text.parse().ok() +} + +/// `text` without `prefix`, compared without ASCII case. +fn strip_prefix_ci<'a>(text: &'a str, prefix: &str) -> Option<&'a str> { + let head = text.get(..prefix.len())?; + head.eq_ignore_ascii_case(prefix) + .then(|| &text[prefix.len()..]) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn epsg_forms() { + assert_eq!(code_from_name("EPSG:32648"), Some(32648)); + assert_eq!(code_from_name("epsg:4326"), Some(4326)); + assert_eq!(code_from_name("urn:ogc:def:crs:EPSG::3375"), Some(3375)); + assert_eq!( + code_from_name("urn:ogc:def:crs:EPSG:9.8.1:3376"), + Some(3376) + ); + assert_eq!( + code_from_name("http://www.opengis.net/def/crs/EPSG/0/4742"), + Some(4742) + ); + assert_eq!( + code_from_name("https://www.opengis.net/def/crs/EPSG/0/4742"), + Some(4742) + ); + } + + #[test] + fn nodedb_forms() { + assert_eq!(code_from_name("NODEDB:910001"), Some(910_001)); + assert_eq!(code_from_name("nodedb:990001"), Some(990_001)); + assert_eq!( + code_from_name("urn:ogc:def:crs:NODEDB::910002"), + Some(910_002) + ); + } + + #[test] + fn crs84_forms() { + for name in [ + "urn:ogc:def:crs:OGC:1.3:CRS84", + "urn:ogc:def:crs:OGC::CRS84", + "http://www.opengis.net/def/crs/OGC/1.3/CRS84", + "OGC:CRS84", + "CRS84", + ] { + assert_eq!(code_from_name(name), Some(4326), "{name}"); + } + } + + #[test] + fn sr_org_modis() { + assert_eq!(code_from_name("SR-ORG:6842"), Some(990_001)); + assert_eq!(code_from_name("SR-ORG:1"), None); + } + + #[test] + fn refuses_other_forms() { + for name in [ + "", + "4326", + "EPSG:", + "EPSG:-1", + "EPSG:4326x", + "ESRI:102100", + "urn:ogc:def:crs:EPSG:4326", + ] { + assert_eq!(code_from_name(name), None, "{name}"); + } + } +} diff --git a/nodedb-srid/src/resolver.rs b/nodedb-srid/src/resolver.rs new file mode 100644 index 000000000..eaa73fd81 --- /dev/null +++ b/nodedb-srid/src/resolver.rs @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! The resolver a crate that carries SRIDs checks them against. + +use crate::error::UnknownSrid; +use crate::kind::CrsKind; +use crate::name::code_from_name; +use crate::srid::Srid; + +/// Answers whether an SRID code or a CRS name is defined, and the kind of +/// a defined CRS. +/// +/// `nodedb-crs` implements it for its per-statement CRS context, so a crate +/// that depends on this crate alone checks SRIDs without linking the EPSG +/// table. +pub trait SridResolver { + /// The SRID of `code`, when a CRS with that code is defined. + fn resolve_u32(&self, code: u32) -> Result; + + /// Whether `srid` is geographic or projected. + fn kind(&self, srid: Srid) -> Result; + + /// The SRID a CRS name refers to: every form [`code_from_name`] + /// accepts, checked with [`SridResolver::resolve_u32`]. + fn resolve_name(&self, name: &str) -> Result { + let code = + code_from_name(name).ok_or_else(|| UnknownSrid::UnknownName { name: name.into() })?; + let code = u32::try_from(code).map_err(|_| UnknownSrid::OutOfRange { code })?; + self.resolve_u32(code) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Defines EPSG:4326 only. + struct OnlyWgs84; + + impl SridResolver for OnlyWgs84 { + fn resolve_u32(&self, code: u32) -> Result { + let srid = Srid::new(code).map_err(|_| UnknownSrid::OutOfRange { + code: i64::from(code), + })?; + if srid == Srid::WGS84 { + Ok(srid) + } else { + Err(UnknownSrid::Undefined { code }) + } + } + + fn kind(&self, _srid: Srid) -> Result { + Ok(CrsKind::Geographic) + } + } + + #[test] + fn names_resolve_through_the_code_check() { + assert_eq!(OnlyWgs84.resolve_name("EPSG:4326"), Ok(Srid::WGS84)); + assert_eq!(OnlyWgs84.resolve_name("CRS84"), Ok(Srid::WGS84)); + assert_eq!( + OnlyWgs84.resolve_name("EPSG:3857"), + Err(UnknownSrid::Undefined { code: 3857 }) + ); + assert!(matches!( + OnlyWgs84.resolve_name("ESRI:102100"), + Err(UnknownSrid::UnknownName { .. }) + )); + assert_eq!( + OnlyWgs84.resolve_name("EPSG:99999999999"), + Err(UnknownSrid::OutOfRange { + code: 99_999_999_999 + }) + ); + } +} diff --git a/nodedb-srid/src/srid.rs b/nodedb-srid/src/srid.rs new file mode 100644 index 000000000..7f0be5c77 --- /dev/null +++ b/nodedb-srid/src/srid.rs @@ -0,0 +1,255 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Spatial reference system identifiers. + +use core::num::NonZeroU32; + +use serde::{Deserialize, Serialize}; + +/// First code of the NodeDB built-in range. +const NODEDB_FIRST: u32 = 990_000; + +/// Last code of the NodeDB built-in range. +const NODEDB_LAST: u32 = 990_999; + +/// First code above the EPSG range. +const EPSG_END: u32 = 900_000; + +/// A spatial reference system identifier. +/// +/// The code is checked for syntax only: `1..=MAX_CODE`. Whether a CRS with +/// that code exists is a registry question, answered by a +/// [`SridResolver`](crate::SridResolver). Serialized as its numeric code. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] +#[serde(into = "u32", try_from = "u32")] +pub struct Srid(NonZeroU32); + +/// Who assigns an SRID code. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum SridAuthority { + /// EPSG, codes below 900 000. + Epsg, + /// NodeDB built-ins, codes 990 000 to 990 999. + NodeDb, + /// User-defined CRSs, codes 910 000 to 989 999. + User, + /// Codes no authority assigns: 900 000 to 909 999 and 991 000 to + /// `Srid::MAX_CODE`. + Reserved, +} + +/// A code outside `1..=Srid::MAX_CODE`. +#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] +#[error("SRID code {code} is outside 1..={max}", max = Srid::MAX_CODE)] +pub struct InvalidSridCode { + pub code: u32, +} + +/// A code outside the user-defined range. +#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] +#[error( + "SRID {code} is outside the user range {first}..={last}", + first = Srid::USER_FIRST, + last = Srid::USER_LAST +)] +pub struct NotUserSrid { + pub code: u64, +} + +impl Srid { + /// Largest accepted code. + pub const MAX_CODE: u32 = 998_999; + + /// First code of the user-defined range. + pub const USER_FIRST: u32 = 910_000; + + /// Last code of the user-defined range. + pub const USER_LAST: u32 = 989_999; + + /// EPSG:4326, WGS 84 geographic. + pub const WGS84: Srid = Srid::known(4326); + + /// EPSG:3857, WGS 84 / Pseudo-Mercator, the web map tile grid. + pub const WEB_MERCATOR: Srid = Srid::known(3857); + + /// NodeDB 990001, the MODIS sinusoidal grid (SR-ORG:6842). + pub const MODIS_SINUSOIDAL: Srid = Srid::known(990_001); + + /// NodeDB 990002, the Himawari geostationary view. + pub const HIMAWARI: Srid = Srid::known(990_002); + + /// Check `code` is in `1..=MAX_CODE`. + pub const fn new(code: u32) -> Result { + if code > Self::MAX_CODE { + return Err(InvalidSridCode { code }); + } + match NonZeroU32::new(code) { + Some(n) => Ok(Srid(n)), + None => Err(InvalidSridCode { code }), + } + } + + /// Build an SRID from a code known valid at compile time. Use it in a + /// `const` item only: an invalid code fails the build there. + pub const fn known(code: u32) -> Srid { + match Srid::new(code) { + Ok(srid) => srid, + Err(_) => panic!("SRID constant out of range"), + } + } + + /// Check `code` is in the user-defined range. It takes `u64` so a SQL + /// literal checks without a narrowing step. + pub const fn new_user(code: u64) -> Result { + if code < Self::USER_FIRST as u64 || code > Self::USER_LAST as u64 { + return Err(NotUserSrid { code }); + } + match Srid::new(code as u32) { + Ok(srid) => Ok(srid), + Err(_) => Err(NotUserSrid { code }), + } + } + + /// The numeric code. + pub const fn code(self) -> u32 { + self.0.get() + } + + /// The CRS name of this code: `EPSG:` for an EPSG code, + /// `NODEDB:` for every other. + pub fn crs_name(self) -> String { + match self.authority() { + SridAuthority::Epsg => format!("EPSG:{}", self.code()), + SridAuthority::NodeDb | SridAuthority::User | SridAuthority::Reserved => { + format!("{}:{}", crate::name::NODEDB_AUTHORITY, self.code()) + } + } + } + + /// The authority that assigns this code. + pub const fn authority(self) -> SridAuthority { + match self.code() { + 0..EPSG_END => SridAuthority::Epsg, + Self::USER_FIRST..=Self::USER_LAST => SridAuthority::User, + NODEDB_FIRST..=NODEDB_LAST => SridAuthority::NodeDb, + _ => SridAuthority::Reserved, + } + } +} + +impl Default for Srid { + fn default() -> Self { + Srid::WGS84 + } +} + +impl From for u32 { + fn from(srid: Srid) -> u32 { + srid.code() + } +} + +impl TryFrom for Srid { + type Error = InvalidSridCode; + + fn try_from(code: u32) -> Result { + Srid::new(code) + } +} + +impl core::fmt::Display for Srid { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{}", self.code()) + } +} + +impl zerompk::ToMessagePack for Srid { + fn write(&self, writer: &mut W) -> zerompk::Result<()> { + writer.write_u32(self.code()) + } +} + +impl<'a> zerompk::FromMessagePack<'a> for Srid { + fn read>(reader: &mut R) -> zerompk::Result { + let code = reader.read_u32()?; + Srid::new(code).map_err(|e| zerompk::Error::UnknownKey(e.to_string())) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn range_is_one_to_max() { + assert!(Srid::new(0).is_err()); + assert_eq!(Srid::new(1).map(Srid::code), Ok(1)); + assert_eq!( + Srid::new(Srid::MAX_CODE).map(Srid::code), + Ok(Srid::MAX_CODE) + ); + assert_eq!( + Srid::new(Srid::MAX_CODE + 1), + Err(InvalidSridCode { + code: Srid::MAX_CODE + 1 + }) + ); + } + + #[test] + fn user_codes_are_the_user_range() { + assert_eq!( + Srid::new_user(u64::from(Srid::USER_FIRST)).map(Srid::code), + Ok(Srid::USER_FIRST) + ); + assert_eq!( + Srid::new_user(u64::from(Srid::USER_LAST)).map(Srid::code), + Ok(Srid::USER_LAST) + ); + for code in [0, 4326, 909_999, 990_001, 999_999, u64::from(u32::MAX) + 1] { + assert_eq!(Srid::new_user(code), Err(NotUserSrid { code })); + } + assert_eq!( + NotUserSrid { code: 4326 }.to_string(), + "SRID 4326 is outside the user range 910000..=989999" + ); + } + + #[test] + fn authority_by_range() { + let auth = |c: u32| Srid::new(c).map(Srid::authority); + assert_eq!(auth(4326), Ok(SridAuthority::Epsg)); + assert_eq!(auth(899_999), Ok(SridAuthority::Epsg)); + assert_eq!(auth(900_000), Ok(SridAuthority::Reserved)); + assert_eq!(auth(909_999), Ok(SridAuthority::Reserved)); + assert_eq!(auth(910_000), Ok(SridAuthority::User)); + assert_eq!(auth(989_999), Ok(SridAuthority::User)); + assert_eq!(auth(990_000), Ok(SridAuthority::NodeDb)); + assert_eq!(auth(990_999), Ok(SridAuthority::NodeDb)); + assert_eq!(auth(991_000), Ok(SridAuthority::Reserved)); + assert_eq!(auth(Srid::MAX_CODE), Ok(SridAuthority::Reserved)); + } + + #[test] + fn crs_names_follow_the_authority() { + assert_eq!(Srid::WGS84.crs_name(), "EPSG:4326"); + assert_eq!(Srid::MODIS_SINUSOIDAL.crs_name(), "NODEDB:990001"); + assert_eq!(Srid::new(910_001).unwrap().crs_name(), "NODEDB:910001"); + } + + #[test] + fn constants() { + assert_eq!(Srid::WGS84.code(), 4326); + assert_eq!(Srid::default(), Srid::WGS84); + assert_eq!(Srid::MODIS_SINUSOIDAL.authority(), SridAuthority::NodeDb); + } + + #[test] + fn msgpack_round_trip_and_rejects_zero() { + let srid = Srid::new(32648).unwrap(); + let bytes = zerompk::to_msgpack_vec(&srid).unwrap(); + assert_eq!(zerompk::from_msgpack::(&bytes).unwrap(), srid); + let zero = zerompk::to_msgpack_vec(&0u32).unwrap(); + assert!(zerompk::from_msgpack::(&zero).is_err()); + } +} diff --git a/nodedb-strict/Cargo.toml b/nodedb-strict/Cargo.toml index 7bfa0cbc9..4133bbc49 100644 --- a/nodedb-strict/Cargo.toml +++ b/nodedb-strict/Cargo.toml @@ -15,6 +15,7 @@ categories = ["encoding", "data-structures"] [dependencies] nodedb-types = { workspace = true } +nodedb-spatial = { workspace = true } arrow = { workspace = true, features = ["ffi"] } thiserror = { workspace = true } rust_decimal = { workspace = true } diff --git a/nodedb-strict/src/decode.rs b/nodedb-strict/src/decode.rs index 8d26cd20a..2eda3973e 100644 --- a/nodedb-strict/src/decode.rs +++ b/nodedb-strict/src/decode.rs @@ -269,7 +269,7 @@ impl TupleDecoder { column: col.name.clone(), expected: col.column_type, })?; - Ok(decode_variable_value(&col.column_type, raw)) + decode_variable_value(&col.column_type, raw) } } @@ -337,7 +337,7 @@ impl TupleDecoder { } /// Extract the three bitemporal timestamps from a tuple: - /// `(system_from_ms, valid_from_ms, valid_until_ms)`. Only valid for + /// `(system_from, valid_from_ms, valid_until_ms)`. Only valid for /// schemas constructed with `StrictSchema::new_bitemporal`. pub fn extract_bitemporal_timestamps( &self, diff --git a/nodedb-strict/src/decode/value.rs b/nodedb-strict/src/decode/value.rs index 5f62f03fd..c2e6fc1bc 100644 --- a/nodedb-strict/src/decode/value.rs +++ b/nodedb-strict/src/decode/value.rs @@ -7,6 +7,8 @@ use nodedb_types::datetime::NdbDateTime; use nodedb_types::decode_bounds::checked_decode_capacity; use nodedb_types::value::Value; +use crate::error::StrictError; + /// Decode a fixed-size raw byte slice into a value. pub(super) fn decode_fixed_value(col_type: &ColumnType, raw: &[u8]) -> Value { match col_type { @@ -80,19 +82,24 @@ pub(super) fn decode_fixed_value(col_type: &ColumnType, raw: &[u8]) -> Value { } } -/// Decode a variable-length raw byte slice into a value. -pub(super) fn decode_variable_value(col_type: &ColumnType, raw: &[u8]) -> Value { - match col_type { +/// Decode a variable-length raw byte slice into a value. A geometry cell +/// that is not EWKB is [`StrictError::CorruptCell`]. +pub(super) fn decode_variable_value( + col_type: &ColumnType, + raw: &[u8], +) -> Result { + Ok(match col_type { ColumnType::String | ColumnType::SparseVector => { Value::String(std::str::from_utf8(raw).unwrap_or_default().to_string()) } ColumnType::Bytes => Value::Bytes(raw.to_vec()), - // GeoJSON with an optional `crs` member, which carries the SRID. - // Any other text (WKT) stays a string. - ColumnType::Geometry => match nodedb_types::geometry::parse_geojson_slice(raw) { - Ok(geometry) => Value::Geometry(geometry), - Err(_) => Value::String(std::str::from_utf8(raw).unwrap_or_default().to_string()), - }, + // EWKB: SRID, Z and M. A stored SRID is checked for syntax only. + ColumnType::Geometry => { + nodedb_spatial::value_of_ewkb(raw).map_err(|e| StrictError::CorruptCell { + column_type: *col_type, + detail: e.to_string(), + })? + } ColumnType::Json => match nodedb_types::value_from_msgpack(raw) { Ok(value) => value, Err(error) => { @@ -110,7 +117,7 @@ pub(super) fn decode_variable_value(col_type: &ColumnType, raw: &[u8]) -> Value decode_typed_msgpack(raw, |value| matches!(value, Value::Record { .. })) } _ => Value::Null, - } + }) } /// Decode MessagePack only when it carries the exact Value variant required @@ -128,19 +135,23 @@ mod tests { use super::*; #[test] - fn projected_geometry_decodes_with_its_srid() { - use nodedb_types::geometry::{Geometry, Srid, SridGeometry}; - let g = SridGeometry::new(Srid::Utm48N, Geometry::point(134_445.5, 349_624.7)); - let raw = sonic_rs::to_vec(&g).unwrap(); + fn geometry_cell_decodes_srid_z_and_m() { + use nodedb_types::geometry::{Geometry, Ordinates, Srid, SridGeometry}; + let g = SridGeometry::with_ordinates( + Srid::new(32648).unwrap(), + Geometry::point(134_445.5, 349_624.7), + Ordinates::new(Some(vec![12.0]), Some(vec![3.0])), + ) + .unwrap(); + let raw = nodedb_spatial::geometry_to_ewkb(&g).unwrap(); assert_eq!( decode_variable_value(&ColumnType::Geometry, &raw), - Value::Geometry(g) - ); - let plain = sonic_rs::to_vec(&SridGeometry::wgs84(Geometry::point(1.0, 2.0))).unwrap(); - assert!( - !String::from_utf8(plain).unwrap().contains("crs"), - "4326 writes no crs member" + Ok(Value::Geometry(g)) ); + assert!(matches!( + decode_variable_value(&ColumnType::Geometry, b"POINT(1 2)"), + Err(StrictError::CorruptCell { .. }) + )); } #[test] diff --git a/nodedb-strict/src/encode.rs b/nodedb-strict/src/encode.rs index 4cb249a85..6a42c919e 100644 --- a/nodedb-strict/src/encode.rs +++ b/nodedb-strict/src/encode.rs @@ -164,14 +164,16 @@ impl TupleEncoder { } /// Encode a row for a bitemporal strict schema. The three reserved - /// slots (0/1/2) are populated from the provided timestamps; the - /// remaining slots are filled from `user_values` in schema order. + /// slots (0/1/2) are populated from the provided system-time key and + /// valid-time bounds; the remaining slots are filled from `user_values` + /// in schema order. The system-time key is opaque here: the host engine + /// decides its domain (Origin stores the system-time ordinal). /// /// Errors if the schema is not bitemporal or if `user_values.len() != /// schema.len() - 3`. pub fn encode_bitemporal( &self, - system_from_ms: i64, + system_from: i64, valid_from_ms: i64, valid_until_ms: i64, user_values: &[Value], @@ -190,7 +192,7 @@ impl TupleEncoder { }); } let mut all = Vec::with_capacity(self.schema.columns.len()); - all.push(Value::Integer(system_from_ms)); + all.push(Value::Integer(system_from)); all.push(Value::Integer(valid_from_ms)); all.push(Value::Integer(valid_until_ms)); all.extend_from_slice(user_values); diff --git a/nodedb-strict/src/encode/value.rs b/nodedb-strict/src/encode/value.rs index 4aa19b338..074359b9e 100644 --- a/nodedb-strict/src/encode/value.rs +++ b/nodedb-strict/src/encode/value.rs @@ -130,23 +130,17 @@ pub(super) fn encode_variable( value: &Value, ) -> Result<(), StrictError> { match (&col.column_type, value) { - (ColumnType::String, Value::String(s)) - | (ColumnType::Geometry, Value::String(s)) - | (ColumnType::SparseVector, Value::String(s)) => { - // Geometry text is WKT or GeoJSON. Sparse vector text is a - // `'{id: weight}'` literal parsed at index-build time. + (ColumnType::String, Value::String(s)) | (ColumnType::SparseVector, Value::String(s)) => { + // Sparse vector text is a `'{id: weight}'` literal parsed at + // index-build time. var_data.extend_from_slice(s.as_bytes()); } (ColumnType::Bytes | ColumnType::SparseVector, Value::Bytes(b)) => { var_data.extend_from_slice(b); } - // GeoJSON, with a `crs` member for any SRID other than 4326. - (ColumnType::Geometry, Value::Geometry(g)) => { - let json = sonic_rs::to_vec(g).map_err(|e| { - invalid_value(col, format!("geometry does not serialize to GeoJSON: {e}")) - })?; - var_data.extend_from_slice(&json); - } + // EWKB, so the cell keeps its SRID, Z and M. A stored SRID is + // checked for syntax only. + (ColumnType::Geometry, value) => append_geometry(var_data, col, value)?, (ColumnType::Json, Value::String(s)) => { // A JSON text stores as the value it spells. Any other text // stores as a JSON string. @@ -226,6 +220,22 @@ fn check_vector_len(col: &ColumnDef, dim: u32, len: usize) -> Result<(), StrictE } } +/// Append the EWKB of the geometry `value` holds, or refuse the value. +fn append_geometry( + var_data: &mut Vec, + col: &ColumnDef, + value: &Value, +) -> Result<(), StrictError> { + let ewkb = nodedb_spatial::ewkb_of_value(value, nodedb_spatial::SridCheck::Syntax).map_err( + |e| match e { + nodedb_spatial::SpatialError::NotGeometryValue { .. } => type_mismatch(col), + e => invalid_value(col, e.to_string()), + }, + )?; + var_data.extend_from_slice(&ewkb); + Ok(()) +} + fn type_mismatch(col: &ColumnDef) -> StrictError { StrictError::TypeMismatch { column: col.name.clone(), diff --git a/nodedb-strict/src/error.rs b/nodedb-strict/src/error.rs index 11b8ea798..684c5171d 100644 --- a/nodedb-strict/src/error.rs +++ b/nodedb-strict/src/error.rs @@ -28,6 +28,13 @@ pub enum StrictError { detail: String, }, + /// A stored variable-length cell whose bytes do not hold its type. + #[error("column of type {column_type}: corrupt cell: {detail}")] + CorruptCell { + column_type: ColumnType, + detail: String, + }, + /// A non-nullable column received a null value with no default. #[error("column '{0}' is NOT NULL and has no default")] NullViolation(String), diff --git a/nodedb-test-support/Cargo.toml b/nodedb-test-support/Cargo.toml index 9db699c65..c0145113d 100644 --- a/nodedb-test-support/Cargo.toml +++ b/nodedb-test-support/Cargo.toml @@ -25,6 +25,7 @@ nodedb-bridge = { workspace = true } nodedb-client = { workspace = true, features = ["native"] } nodedb-cluster = { workspace = true } nodedb-crdt = { workspace = true } +nodedb-crs = { workspace = true } nodedb-mem = { workspace = true } nodedb-types = { workspace = true } nodedb-wal = { workspace = true } diff --git a/nodedb-test-support/src/booted_state.rs b/nodedb-test-support/src/booted_state.rs index 6cbb67fa8..5bf3f3011 100644 --- a/nodedb-test-support/src/booted_state.rs +++ b/nodedb-test-support/src/booted_state.rs @@ -201,6 +201,7 @@ async fn boot_node(options: BootOptions) -> Result { doc_config_seed: nodedb::bootstrap::data_plane::load_doc_config_registry_from( state.credentials.catalog(), ), + crs_set_seed: state.credentials.catalog().custom_crs_set(), event_interest: crate::core_loop_runner::event_interest_for(&state), stop_rx: core_stop_rx, }); diff --git a/nodedb-test-support/src/cluster_harness/node/inspect/core_placement.rs b/nodedb-test-support/src/cluster_harness/node/inspect/core_placement.rs index 24cd79998..f0df6aa5b 100644 --- a/nodedb-test-support/src/cluster_harness/node/inspect/core_placement.rs +++ b/nodedb-test-support/src/cluster_harness/node/inspect/core_placement.rs @@ -58,6 +58,26 @@ impl TestClusterNode { &self, core_id: usize, tenant: TenantId, + ) -> TenantDataSnapshot { + self.snapshot_on_core(core_id, tenant, false).await + } + + /// The tenant snapshot of `tenant` that each core of this node holds, + /// array cells included, in core order. The reads never leave this node, + /// so they show this replica's own stored data in every engine. + pub async fn stored_tenant_snapshots(&self, tenant: TenantId) -> Vec { + let mut snapshots = Vec::with_capacity(self.num_cores()); + for core_id in 0..self.num_cores() { + snapshots.push(self.snapshot_on_core(core_id, tenant, true).await); + } + snapshots + } + + async fn snapshot_on_core( + &self, + core_id: usize, + tenant: TenantId, + arrays: bool, ) -> TenantDataSnapshot { let request_id = RequestId::new(PLACEMENT_REQUEST_ID.fetch_add(1, Ordering::Relaxed)); let request = Request { @@ -69,7 +89,7 @@ impl TestClusterNode { tenant_id: tenant.as_u64(), cut_watermark: None, cut_capture: None, - arrays: false, + arrays, }), deadline: std::time::Instant::now() + std::time::Duration::from_secs(10), priority: Priority::Normal, diff --git a/nodedb-test-support/src/cluster_harness/node/lifecycle/spawn_full.rs b/nodedb-test-support/src/cluster_harness/node/lifecycle/spawn_full.rs index 6e70a4f11..5bb2144a0 100644 --- a/nodedb-test-support/src/cluster_harness/node/lifecycle/spawn_full.rs +++ b/nodedb-test-support/src/cluster_harness/node/lifecycle/spawn_full.rs @@ -224,6 +224,16 @@ impl TestClusterNode { // Fixed test KEK so backup tests produce encrypted envelopes. state.backup_kek = Some(Arc::new([0x42u8; 32])); state.backup_storage = config.backup_storage.clone().map(Arc::new); + // Webhook sinks in tests listen on loopback. The `[egress]` policy + // admits `http://localhost` there, as an operator allow-lists a + // private sink. + state.remote_sources = nodedb::control::security::egress::RemoteSources::new( + Arc::new(nodedb::control::security::egress::EgressPolicy::from_parts( + true, + &["localhost".to_string()], + &["127.0.0.0/8".to_string(), "::1/128".to_string()], + )?), + )?; super::wire_state::open_producer_registry(state)?; // Drains, pending DDL records, and the DDL preparation owner load // from their rows (mirrors production `SharedState::open`). @@ -274,6 +284,7 @@ impl TestClusterNode { doc_config_seed: nodedb::bootstrap::data_plane::load_doc_config_registry_from( shared.credentials.catalog(), ), + crs_set_seed: shared.credentials.catalog().custom_crs_set(), event_interest: crate::core_loop_runner::event_interest_for(&shared), stop_rx: core_stop_rx, }); diff --git a/nodedb-test-support/src/cluster_harness/pitr.rs b/nodedb-test-support/src/cluster_harness/pitr.rs index 1dbb51ce9..a8b98ef37 100644 --- a/nodedb-test-support/src/cluster_harness/pitr.rs +++ b/nodedb-test-support/src/cluster_harness/pitr.rs @@ -87,9 +87,7 @@ impl PitrStorage { ) -> HarnessResult { let mut config = ServerConfig::default(); config.server.data_dir = data_dir.to_path_buf(); - config.encryption = Some(EncryptionSettings { - key_path: self.key_path(), - }); + config.encryption = Some(EncryptionSettings::file(self.key_path())); config.cold_storage = Some(self.cold_settings()?); config.snapshot_storage = Some(self.snapshot_settings()?); config.pitr.enabled = true; diff --git a/nodedb-test-support/src/cluster_harness/retriable.rs b/nodedb-test-support/src/cluster_harness/retriable.rs index 8a9deafc0..0f4be04c3 100644 --- a/nodedb-test-support/src/cluster_harness/retriable.rs +++ b/nodedb-test-support/src/cluster_harness/retriable.rs @@ -49,7 +49,10 @@ where } tokio::time::sleep(step).await; } - Err(err) => panic!("{desc}: {err}"), + Err(err) => match err.as_db_error() { + Some(db) => panic!("{desc}: {} {}", db.code().code(), db.message()), + None => panic!("{desc}: {err}"), + }, } } } diff --git a/nodedb-test-support/src/core_loop_runner.rs b/nodedb-test-support/src/core_loop_runner.rs index 1d20df3a5..624f8f1f2 100644 --- a/nodedb-test-support/src/core_loop_runner.rs +++ b/nodedb-test-support/src/core_loop_runner.rs @@ -111,6 +111,10 @@ pub struct CoreLoopSpawn { /// `timestamp`. That made every restart test here quietly weaker than it /// looked — none of them exercised the seeded path production always takes. pub doc_config_seed: Vec, + /// The catalog's user-defined CRS set, installed BEFORE WAL replay exactly + /// as production's `seed_catalog_state` does, so replay and every later + /// request resolve user-defined SRIDs. + pub crs_set_seed: Arc, /// The collections some Event Plane consumer reads, as production boot /// wires them. Build it with [`event_interest_for`]. pub event_interest: Arc, @@ -147,6 +151,7 @@ pub fn spawn_core_loop(spawn: CoreLoopSpawn) -> tokio::task::JoinHandle<()> { query_tuning, timeseries_tuning, doc_config_seed, + crs_set_seed, event_interest, stop_rx, } = spawn; @@ -180,6 +185,7 @@ pub fn spawn_core_loop(spawn: CoreLoopSpawn) -> tokio::task::JoinHandle<()> { // `doc_configs`, and production seeds it in the same order // (`seed_catalog_state` -> `replay_wal_and_rebuild_indexes`). core.seed_doc_configs(&doc_config_seed); + core.set_crs_set(crs_set_seed); if let Some(WalReplay { records, tombstones, diff --git a/nodedb-test-support/src/jwks_fixture.rs b/nodedb-test-support/src/jwks_fixture.rs index 14f1bd433..052260ca4 100644 --- a/nodedb-test-support/src/jwks_fixture.rs +++ b/nodedb-test-support/src/jwks_fixture.rs @@ -116,15 +116,10 @@ impl JwksFixture { } /// An `[auth.jwt]` section with one static provider pointing at this - /// fixture, bound to `tenant_id`. - /// - /// Includes the loopback allowances the JWKS fetcher needs to accept a - /// plaintext `http://localhost` keyset — production forbids both. + /// fixture, bound to `tenant_id`. Its registry fetches through + /// [`loopback_egress`]. pub fn auth_config(&self, issuer: &str, audience: &str, tenant_id: u64) -> JwtAuthConfig { JwtAuthConfig { - allow_http_jwks: true, - allow_jwks_hosts: vec!["localhost".into()], - allow_jwks_cidrs: vec!["127.0.0.0/8".into(), "::1/128".into()], providers: vec![JwtProviderConfig { name: "test-provider".into(), jwks_url: self.jwks_url.clone(), @@ -137,6 +132,26 @@ impl JwksFixture { } } +/// The `[egress]` policy a JWKS fetch of a fixture needs: plaintext +/// `http://localhost` resolving to loopback. The strict default refuses both. +pub fn loopback_egress() -> std::sync::Arc { + std::sync::Arc::new( + nodedb::control::security::egress::EgressPolicy::from_parts( + true, + &["localhost".to_string()], + &["127.0.0.0/8".to_string(), "::1/128".to_string()], + ) + .expect("the loopback egress policy is valid"), + ) +} + +/// Remote sources under [`loopback_egress`], so `CREATE OIDC PROVIDER` admits +/// a fixture's `http://localhost` JWKS URI. +pub fn loopback_sources() -> nodedb::control::security::egress::RemoteSources { + nodedb::control::security::egress::RemoteSources::new(loopback_egress()) + .expect("the loopback remote sources build") +} + /// Seconds since the UNIX epoch, for `iat` / `exp` claims. pub fn now_secs() -> u64 { std::time::SystemTime::now() diff --git a/nodedb-test-support/src/pgwire_harness/multicore.rs b/nodedb-test-support/src/pgwire_harness/multicore.rs index 85e1add27..fde0184fd 100644 --- a/nodedb-test-support/src/pgwire_harness/multicore.rs +++ b/nodedb-test-support/src/pgwire_harness/multicore.rs @@ -87,6 +87,7 @@ impl TestServer { doc_config_seed: nodedb::bootstrap::data_plane::load_doc_config_registry_from( shared.credentials.catalog(), ), + crs_set_seed: shared.credentials.catalog().custom_crs_set(), event_interest: crate::core_loop_runner::event_interest_for(&shared), stop_rx: core_stop_rx, }); diff --git a/nodedb-test-support/src/pgwire_harness/restart.rs b/nodedb-test-support/src/pgwire_harness/restart.rs index 9d270e9fa..5d4dd1dfc 100644 --- a/nodedb-test-support/src/pgwire_harness/restart.rs +++ b/nodedb-test-support/src/pgwire_harness/restart.rs @@ -273,6 +273,7 @@ impl TestServer { doc_config_seed: nodedb::bootstrap::data_plane::load_doc_config_registry_from( shared.credentials.catalog(), ), + crs_set_seed: shared.credentials.catalog().custom_crs_set(), event_interest: crate::core_loop_runner::event_interest_for(&shared), stop_rx: core_stop_rx, }); diff --git a/nodedb-test-support/src/pgwire_harness/start.rs b/nodedb-test-support/src/pgwire_harness/start.rs index 40edc2d92..caf657c7b 100644 --- a/nodedb-test-support/src/pgwire_harness/start.rs +++ b/nodedb-test-support/src/pgwire_harness/start.rs @@ -317,6 +317,7 @@ impl TestServer { doc_config_seed: nodedb::bootstrap::data_plane::load_doc_config_registry_from( shared.credentials.catalog(), ), + crs_set_seed: shared.credentials.catalog().custom_crs_set(), event_interest: crate::core_loop_runner::event_interest_for(&shared), stop_rx: core_stop_rx, }); diff --git a/nodedb-test-support/src/sync_client.rs b/nodedb-test-support/src/sync_client.rs index 9c8294b26..8c2005031 100644 --- a/nodedb-test-support/src/sync_client.rs +++ b/nodedb-test-support/src/sync_client.rs @@ -4,7 +4,9 @@ //! //! Test-only infra for driving the Lite↔Origin sync WebSocket end-to-end: //! performs a Trust-mode handshake, then pushes CRDT deltas and reports -//! back whether the server acked or rejected each one. Not a library-grade +//! back whether the server acked or rejected each one. Each delta states its +//! collection's incarnation, learned from the server's `CollectionSchema` +//! announce as a Lite client learns it. Not a library-grade //! client — errors are returned as `String` since this crate is test //! infrastructure, not production code. @@ -39,6 +41,8 @@ pub struct SyncTestClient { producer_id: u64, epoch: u64, next_seq: u64, + /// The `creation_hlc` of every collection the server announced. + incarnations: HashMap, } impl SyncTestClient { @@ -85,7 +89,7 @@ impl SyncTestClient { client_version: "test".to_string(), lite_id, epoch, - wire_version: nodedb_types::wire_version::WIRE_FORMAT_VERSION, + wire_version: nodedb_types::sync::wire::SYNC_WIRE_VERSION, }; let frame = SyncFrame::try_encode(SyncMessageType::Handshake, &handshake) .ok_or_else(|| "failed to encode HandshakeMsg".to_string())?; @@ -109,6 +113,7 @@ impl SyncTestClient { producer_id: ack.producer_id, epoch: ack.accepted_epoch, next_seq: 1, + incarnations: HashMap::new(), }) } @@ -146,6 +151,7 @@ impl SyncTestClient { seq: u64, delta: Vec, ) -> Result { + let collection_incarnation = self.incarnation(collection).await?; let push = DeltaPushMsg { collection: collection.to_string(), document_id: document_id.to_string(), @@ -159,6 +165,7 @@ impl SyncTestClient { seq, device_id: 0, delta_signature: [0; 32], + collection_incarnation, }; let frame = SyncFrame::try_encode(SyncMessageType::DeltaPush, &push) .ok_or_else(|| "failed to encode DeltaPushMsg".to_string())?; @@ -168,7 +175,7 @@ impl SyncTestClient { .map_err(|e| format!("failed to send DeltaPush frame: {e}"))?; loop { - let frame = recv_any_frame(&mut self.ws).await?; + let frame = self.recv_next_frame().await?; match frame.msg_type { SyncMessageType::DeltaAck => { let ack: DeltaAckMsg = frame @@ -246,7 +253,34 @@ impl SyncTestClient { /// `RECV_TIMEOUT`, so callers do not need to wrap this in a timeout to /// avoid hanging on a silent server. pub async fn recv_next_frame(&mut self) -> Result { - recv_any_frame(&mut self.ws).await + let frame = recv_any_frame(&mut self.ws).await?; + if frame.msg_type == SyncMessageType::CollectionSchema + && let Some(announce) = frame.decode_body::() + { + self.incarnations + .insert(announce.descriptor.name, announce.creation_hlc); + } + Ok(frame) + } + + /// The incarnation of `collection` the server announced. When it has not + /// announced it on this session yet, subscribe to the collection: the + /// server sends the announce before the snapshot. The server binds the + /// subscription to the session's tenant, not to the shape's. + pub async fn incarnation(&mut self, collection: &str) -> Result { + if let Some(hlc) = self.incarnations.get(collection) { + return Ok(*hlc); + } + self.subscribe_document_shape(&format!("{collection}_incarnation"), collection, 0) + .await?; + loop { + self.recv_next_frame().await.map_err(|e| { + format!("no CollectionSchema announce for collection {collection}: {e}") + })?; + if let Some(hlc) = self.incarnations.get(collection) { + return Ok(*hlc); + } + } } } diff --git a/nodedb-test-support/src/tx_batch_helpers.rs b/nodedb-test-support/src/tx_batch_helpers.rs index 77b5a4edc..a6dcd4289 100644 --- a/nodedb-test-support/src/tx_batch_helpers.rs +++ b/nodedb-test-support/src/tx_batch_helpers.rs @@ -159,6 +159,8 @@ pub fn edge_put(collection: &str, src: &str, dst: &str) -> PhysicalPlan { properties: Vec::new(), src_surrogate: crate::kv_rows::kv_row_surrogate(src.as_bytes()), dst_surrogate: crate::kv_rows::kv_row_surrogate(dst.as_bytes()), + valid_from_ms: None, + valid_until_ms: None, }) } @@ -169,6 +171,7 @@ pub fn neighbors(_collection: &str, src: &str) -> PhysicalPlan { edge_labels: vec!["REL".into()], direction: nodedb::engine::graph::edge_store::Direction::Out, rls_filters: Vec::new(), + scope: nodedb_physical::physical_plan::GraphReadScope::default(), }) } @@ -209,6 +212,7 @@ pub fn columnar_insert(collection: &str, id: &str, val: i64) -> PhysicalPlan { schema_bytes: Vec::new(), provenance: None, wal_lsn: None, + system_from: None, // These helpers build plans for transaction-batch tests on // collections that carry no RLS policy. rls_write_check: nodedb_types::RlsWriteCheck::NoPolicyApplies, diff --git a/nodedb-types/Cargo.toml b/nodedb-types/Cargo.toml index db64dda1a..155c0acda 100644 --- a/nodedb-types/Cargo.toml +++ b/nodedb-types/Cargo.toml @@ -33,6 +33,7 @@ nanoid = { workspace = true } rand = { workspace = true } rust_decimal = { workspace = true } nodedb-codec = { workspace = true } +nodedb-srid = { workspace = true } crc32c = { workspace = true } roaring = { workspace = true } bytemuck = { workspace = true } @@ -42,4 +43,5 @@ sha2 = { workspace = true } hex = { workspace = true } [dev-dependencies] +nodedb-crs = { workspace = true } toml = { workspace = true } diff --git a/nodedb-types/src/array_agg/dist.rs b/nodedb-types/src/array_agg/dist.rs new file mode 100644 index 000000000..cd65dbdc1 --- /dev/null +++ b/nodedb-types/src/array_agg/dist.rs @@ -0,0 +1,436 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! An exact value distribution: every distinct value with its count. +//! +//! Values are kept in ascending order. Floats order with NaN above every +//! other value, as PostgreSQL sorts them, and `-0.0` equals `0.0`. Two +//! distributions merge exactly by summing the counts of equal values. +//! +//! A distribution past [`MAX_DIST_ENTRIES`] distinct values drops its +//! entries and becomes [`ValueDist::OverBudget`]. Finalizing one is a +//! typed error, never an approximation. + +use std::cmp::Ordering; +use std::fmt; + +use serde::{Deserialize, Serialize}; + +/// The most distinct values one distribution holds. At 16 bytes a run, +/// this bounds a partial near 64 MiB. +pub const MAX_DIST_ENTRIES: usize = 1 << 22; + +/// One distinct integer value and how many cells hold it. +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Eq, + Serialize, + Deserialize, + zerompk::ToMessagePack, + zerompk::FromMessagePack, +)] +pub struct IntRun { + pub value: i64, + pub count: u64, +} + +/// One distinct float value and how many cells hold it. +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Serialize, + Deserialize, + zerompk::ToMessagePack, + zerompk::FromMessagePack, +)] +pub struct FloatRun { + pub value: f64, + pub count: u64, +} + +/// One value of a distribution, typed as it was stored. +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum DistValue { + Int(i64), + Float(f64), +} + +/// A distribution's distinct values passed [`MAX_DIST_ENTRIES`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct DistBudgetExceeded { + /// Distinct values seen when the budget ran out. A lower bound. + pub distinct: u64, +} + +impl fmt::Display for DistBudgetExceeded { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "an exact median, percentile or mode holds at most {MAX_DIST_ENTRIES} distinct \ + values, and the input holds at least {}; narrow the slice or zone, or group by \ + a dim", + self.distinct + ) + } +} + +/// An exact value distribution. +#[derive( + Debug, + Clone, + PartialEq, + Serialize, + Deserialize, + zerompk::ToMessagePack, + zerompk::FromMessagePack, +)] +pub enum ValueDist { + /// No value. + Empty, + /// Distinct integers, strictly ascending. + Ints(Vec), + /// Distinct floats, strictly ascending, NaN last. + Floats(Vec), + /// More than [`MAX_DIST_ENTRIES`] distinct values. The entries are gone. + OverBudget { distinct: u64 }, +} + +/// The float total order a distribution keeps: NaN above every other +/// value, `-0.0` equal to `0.0`. +pub(super) fn cmp_float(a: f64, b: f64) -> Ordering { + match (a.is_nan(), b.is_nan()) { + (true, true) => Ordering::Equal, + (true, false) => Ordering::Greater, + (false, true) => Ordering::Less, + (false, false) => a.partial_cmp(&b).unwrap_or(Ordering::Equal), + } +} + +impl ValueDist { + /// The number of values: the sum of every run's count. + pub fn count(&self) -> u64 { + match self { + ValueDist::Empty | ValueDist::OverBudget { .. } => 0, + ValueDist::Ints(runs) => runs.iter().map(|r| r.count).sum(), + ValueDist::Floats(runs) => runs.iter().map(|r| r.count).sum(), + } + } + + /// The number of distinct values. + pub fn distinct(&self) -> u64 { + match self { + ValueDist::Empty => 0, + ValueDist::Ints(runs) => runs.len() as u64, + ValueDist::Floats(runs) => runs.len() as u64, + ValueDist::OverBudget { distinct } => *distinct, + } + } + + /// The exact merge of two distributions. + /// + /// An integer and a float distribution merge as floats. An integer past + /// 2^53 then rounds, which happens only when one attr mixes kinds. + pub fn merge(self, other: ValueDist) -> ValueDist { + match (self, other) { + (ValueDist::Empty, x) | (x, ValueDist::Empty) => x, + (ValueDist::OverBudget { distinct: a }, x) + | (x, ValueDist::OverBudget { distinct: a }) => ValueDist::OverBudget { + distinct: a.max(x.distinct()), + }, + (ValueDist::Ints(a), ValueDist::Ints(b)) => budgeted( + merge_runs(a, b, |x, y| x.value.cmp(&y.value), add_int), + ValueDist::Ints, + ), + (ValueDist::Floats(a), ValueDist::Floats(b)) => budgeted( + merge_runs(a, b, |x, y| cmp_float(x.value, y.value), add_float), + ValueDist::Floats, + ), + (ValueDist::Ints(a), ValueDist::Floats(b)) + | (ValueDist::Floats(b), ValueDist::Ints(a)) => { + ValueDist::Floats(ints_as_floats(a)).merge(ValueDist::Floats(b)) + } + } + } + + /// The exact `p` percentile, `0 <= p <= 1`, by linear interpolation + /// between the closest ranks. + /// + /// With `N` values sorted ascending and numbered from 0, the result is + /// at rank `h = p * (N - 1)`: `v[floor(h)] + (h - floor(h)) * + /// (v[ceil(h)] - v[floor(h)])`. This is PostgreSQL's + /// `percentile_cont`. `None` when the distribution holds no value. + pub fn percentile(&self, p: f64) -> Result, DistBudgetExceeded> { + let total = match self { + ValueDist::OverBudget { distinct } => { + return Err(DistBudgetExceeded { + distinct: *distinct, + }); + } + other => other.count(), + }; + if total == 0 { + return Ok(None); + } + let h = p.clamp(0.0, 1.0) * (total - 1) as f64; + let lo_rank = h.floor() as u64; + let hi_rank = h.ceil() as u64; + let frac = h - h.floor(); + let (Some(lo), Some(hi)) = (self.value_at(lo_rank), self.value_at(hi_rank)) else { + return Ok(None); + }; + Ok(Some(interpolate(lo, hi, frac))) + } + + /// The value held by the most cells. Among equal counts, the smallest + /// value. `None` when the distribution holds no value. + pub fn mode(&self) -> Result, DistBudgetExceeded> { + Ok(match self { + ValueDist::Empty => None, + ValueDist::OverBudget { distinct } => { + return Err(DistBudgetExceeded { + distinct: *distinct, + }); + } + // Runs ascend, so the first run of the highest count is the + // smallest value among the ties. + ValueDist::Ints(runs) => runs + .iter() + .fold(None::, |best, r| match best { + Some(b) if b.count >= r.count => Some(b), + _ => Some(*r), + }) + .map(|r| DistValue::Int(r.value)), + ValueDist::Floats(runs) => runs + .iter() + .fold(None::, |best, r| match best { + Some(b) if b.count >= r.count => Some(b), + _ => Some(*r), + }) + .map(|r| DistValue::Float(r.value)), + }) + } + + /// The value at 0-based `rank` of the ascending values. + fn value_at(&self, rank: u64) -> Option { + let mut seen = 0u64; + match self { + ValueDist::Empty | ValueDist::OverBudget { .. } => None, + ValueDist::Ints(runs) => runs.iter().find_map(|r| { + seen += r.count; + (rank < seen).then_some(DistValue::Int(r.value)) + }), + ValueDist::Floats(runs) => runs.iter().find_map(|r| { + seen += r.count; + (rank < seen).then_some(DistValue::Float(r.value)) + }), + } + } +} + +/// `lo + frac * (hi - lo)`. Two integers subtract exactly in `i128`. +fn interpolate(lo: DistValue, hi: DistValue, frac: f64) -> f64 { + match (lo, hi) { + (DistValue::Int(a), DistValue::Int(b)) => { + if frac == 0.0 { + return a as f64; + } + a as f64 + frac * ((i128::from(b) - i128::from(a)) as f64) + } + (a, b) => { + let (a, b) = (as_f64(a), as_f64(b)); + if frac == 0.0 { a } else { a + frac * (b - a) } + } + } +} + +fn as_f64(v: DistValue) -> f64 { + match v { + DistValue::Int(i) => i as f64, + DistValue::Float(f) => f, + } +} + +fn add_int(a: IntRun, b: IntRun) -> IntRun { + IntRun { + value: a.value, + count: a.count.saturating_add(b.count), + } +} + +fn add_float(a: FloatRun, b: FloatRun) -> FloatRun { + FloatRun { + value: a.value, + count: a.count.saturating_add(b.count), + } +} + +fn ints_as_floats(runs: Vec) -> Vec { + let mut out: Vec = Vec::with_capacity(runs.len()); + for r in runs { + let value = r.value as f64; + // Two integers past 2^53 can round to one float: fold them. + match out.last_mut() { + Some(last) if last.value == value => last.count = last.count.saturating_add(r.count), + _ => out.push(FloatRun { + value, + count: r.count, + }), + } + } + out +} + +/// Merge two ascending run lists, summing the counts of equal values. +fn merge_runs( + a: Vec, + b: Vec, + cmp: impl Fn(&T, &T) -> Ordering, + add: impl Fn(T, T) -> T, +) -> Vec { + let mut out = Vec::with_capacity(a.len() + b.len()); + let (mut i, mut j) = (0, 0); + while i < a.len() && j < b.len() { + match cmp(&a[i], &b[j]) { + Ordering::Less => { + out.push(a[i]); + i += 1; + } + Ordering::Greater => { + out.push(b[j]); + j += 1; + } + Ordering::Equal => { + out.push(add(a[i], b[j])); + i += 1; + j += 1; + } + } + } + out.extend_from_slice(&a[i..]); + out.extend_from_slice(&b[j..]); + out +} + +/// `runs` wrapped by `wrap`, or [`ValueDist::OverBudget`] past the budget. +fn budgeted(runs: Vec, wrap: fn(Vec) -> ValueDist) -> ValueDist { + if runs.len() > MAX_DIST_ENTRIES { + ValueDist::OverBudget { + distinct: runs.len() as u64, + } + } else if runs.is_empty() { + ValueDist::Empty + } else { + wrap(runs) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn ints(pairs: &[(i64, u64)]) -> ValueDist { + ValueDist::Ints( + pairs + .iter() + .map(|&(value, count)| IntRun { value, count }) + .collect(), + ) + } + + fn floats(pairs: &[(f64, u64)]) -> ValueDist { + ValueDist::Floats( + pairs + .iter() + .map(|&(value, count)| FloatRun { value, count }) + .collect(), + ) + } + + #[test] + fn merge_sums_equal_values_and_keeps_order() { + let a = ints(&[(1, 2), (5, 1)]); + let b = ints(&[(0, 1), (5, 3), (9, 1)]); + assert_eq!(a.merge(b), ints(&[(0, 1), (1, 2), (5, 4), (9, 1)])); + assert_eq!(ValueDist::Empty.merge(ints(&[(3, 1)])), ints(&[(3, 1)])); + } + + #[test] + fn percentile_interpolates_between_closest_ranks() { + // Values 1, 2, 3, 4. + let d = ints(&[(1, 1), (2, 1), (3, 1), (4, 1)]); + assert_eq!(d.percentile(0.5), Ok(Some(2.5))); + assert_eq!(d.percentile(0.0), Ok(Some(1.0))); + assert_eq!(d.percentile(1.0), Ok(Some(4.0))); + assert_eq!(d.percentile(0.25), Ok(Some(1.75))); + // Values 7, 7, 7, 10: rank 1.5 sits between two sevens. + let runs = ints(&[(7, 3), (10, 1)]); + assert_eq!(runs.percentile(0.5), Ok(Some(7.0))); + assert_eq!(ValueDist::Empty.percentile(0.5), Ok(None)); + } + + #[test] + fn integer_interpolation_is_exact_past_2_pow_53() { + let big = 1i64 << 60; + let d = ints(&[(big, 1), (big + 2, 1)]); + assert_eq!(d.percentile(0.5), Ok(Some((big + 1) as f64))); + } + + #[test] + fn mode_breaks_ties_by_the_smallest_value() { + let d = ints(&[(-3, 2), (4, 2), (9, 1)]); + assert_eq!(d.mode(), Ok(Some(DistValue::Int(-3)))); + let f = floats(&[(0.5, 1), (2.5, 3)]); + assert_eq!(f.mode(), Ok(Some(DistValue::Float(2.5)))); + assert_eq!(ValueDist::Empty.mode(), Ok(None)); + } + + #[test] + fn nan_sorts_last_and_zeroes_are_equal() { + let a = floats(&[(-0.0, 1), (f64::NAN, 1)]); + let b = floats(&[(0.0, 2)]); + let merged = a.merge(b); + let ValueDist::Floats(runs) = &merged else { + panic!("floats stay floats"); + }; + assert_eq!(runs.len(), 2); + assert_eq!(runs[0].count, 3); + assert!(runs[1].value.is_nan()); + assert!(merged.percentile(1.0).unwrap().unwrap().is_nan()); + } + + #[test] + fn over_budget_is_a_typed_error() { + let over = ValueDist::OverBudget { distinct: 9 }; + let merged = over.merge(ints(&[(1, 1)])); + assert!(matches!(merged, ValueDist::OverBudget { distinct: 9 })); + assert_eq!( + merged.percentile(0.5), + Err(DistBudgetExceeded { distinct: 9 }) + ); + assert_eq!(merged.mode(), Err(DistBudgetExceeded { distinct: 9 })); + } + + #[test] + fn mixed_kinds_merge_as_floats() { + let merged = ints(&[(1, 1), (2, 1)]).merge(floats(&[(1.0, 1), (1.5, 1)])); + assert_eq!(merged, floats(&[(1.0, 2), (1.5, 1), (2.0, 1)])); + } + + #[test] + fn wire_round_trip() { + for d in [ + ValueDist::Empty, + ints(&[(-1, 4), (8, 1)]), + floats(&[(0.25, 2)]), + ValueDist::OverBudget { distinct: 5 }, + ] { + let bytes = zerompk::to_msgpack_vec(&d).expect("encode"); + let back: ValueDist = zerompk::from_msgpack(&bytes).expect("decode"); + assert_eq!(back, d); + } + } +} diff --git a/nodedb-types/src/array_agg/dist_acc.rs b/nodedb-types/src/array_agg/dist_acc.rs new file mode 100644 index 000000000..02910a6ee --- /dev/null +++ b/nodedb-types/src/array_agg/dist_acc.rs @@ -0,0 +1,246 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! The running form of a [`ValueDist`]: a count per distinct value. +//! +//! A core folds cells into a hash map, one insert per cell, and sorts the +//! distinct values once when it hands the partial on. Memory follows the +//! distinct values, not the cells, so a `UINT16` band of any size holds at +//! most 65 536 entries. + +use std::collections::HashMap; + +use super::dist::{DistValue, FloatRun, IntRun, MAX_DIST_ENTRIES, ValueDist, cmp_float}; + +/// A running value distribution. +#[derive(Debug, Clone, Default, PartialEq)] +pub enum DistAcc { + #[default] + Empty, + Ints(HashMap), + /// Keyed by the canonical bits of each float: `-0.0` folds into `0.0` + /// and every NaN into one NaN. + Floats(HashMap), + /// More than [`MAX_DIST_ENTRIES`] distinct values. The counts are gone. + OverBudget { + distinct: u64, + }, +} + +fn float_key(v: f64) -> u64 { + if v.is_nan() { + f64::NAN.to_bits() + } else if v == 0.0 { + 0.0f64.to_bits() + } else { + v.to_bits() + } +} + +/// Integer counts re-keyed as floats. Two integers past 2^53 that round to +/// one float fold together. +fn ints_as_float_counts(map: HashMap) -> HashMap { + let mut floats: HashMap = HashMap::with_capacity(map.len()); + for (v, n) in map { + *floats.entry(float_key(v as f64)).or_insert(0) += n; + } + floats +} + +impl DistAcc { + /// Fold `n` cells holding `v`. + pub fn insert(&mut self, v: DistValue, n: u64) { + match v { + DistValue::Int(i) => match self { + DistAcc::OverBudget { .. } => return, + DistAcc::Empty => *self = DistAcc::Ints(HashMap::from([(i, n)])), + DistAcc::Ints(map) => *map.entry(i).or_insert(0) += n, + DistAcc::Floats(map) => *map.entry(float_key(i as f64)).or_insert(0) += n, + }, + DistValue::Float(f) => { + let key = float_key(f); + match self { + DistAcc::OverBudget { .. } => return, + DistAcc::Empty => *self = DistAcc::Floats(HashMap::from([(key, n)])), + DistAcc::Floats(map) => *map.entry(key).or_insert(0) += n, + DistAcc::Ints(map) => { + let mut floats = ints_as_float_counts(std::mem::take(map)); + *floats.entry(key).or_insert(0) += n; + *self = DistAcc::Floats(floats); + } + } + } + } + self.check_budget(); + } + + /// Fold every value of `other` into this distribution. + pub fn merge(&mut self, other: DistAcc) { + match other { + DistAcc::Empty => {} + DistAcc::OverBudget { distinct } => { + *self = DistAcc::OverBudget { + distinct: distinct.max(self.distinct()), + } + } + DistAcc::Ints(map) => { + for (v, n) in map { + self.insert(DistValue::Int(v), n); + } + } + DistAcc::Floats(map) => { + for (bits, n) in map { + self.insert(DistValue::Float(f64::from_bits(bits)), n); + } + } + } + } + + /// The number of distinct values. + pub fn distinct(&self) -> u64 { + match self { + DistAcc::Empty => 0, + DistAcc::Ints(map) => map.len() as u64, + DistAcc::Floats(map) => map.len() as u64, + DistAcc::OverBudget { distinct } => *distinct, + } + } + + /// The running form of a received distribution. + pub fn from_dist(dist: ValueDist) -> Self { + match dist { + ValueDist::Empty => DistAcc::Empty, + ValueDist::OverBudget { distinct } => DistAcc::OverBudget { distinct }, + ValueDist::Ints(runs) => { + DistAcc::Ints(runs.into_iter().map(|r| (r.value, r.count)).collect()) + } + ValueDist::Floats(runs) => DistAcc::Floats( + runs.into_iter() + .map(|r| (float_key(r.value), r.count)) + .collect(), + ), + } + } + + /// The sorted wire form. + pub fn into_dist(self) -> ValueDist { + match self { + DistAcc::Empty => ValueDist::Empty, + DistAcc::OverBudget { distinct } => ValueDist::OverBudget { distinct }, + DistAcc::Ints(map) => { + let mut runs: Vec = map + .into_iter() + .map(|(value, count)| IntRun { value, count }) + .collect(); + runs.sort_unstable_by_key(|r| r.value); + ValueDist::Ints(runs) + } + DistAcc::Floats(map) => { + let mut runs: Vec = map + .into_iter() + .map(|(bits, count)| FloatRun { + value: f64::from_bits(bits), + count, + }) + .collect(); + runs.sort_unstable_by(|a, b| cmp_float(a.value, b.value)); + ValueDist::Floats(runs) + } + } + } + + fn check_budget(&mut self) { + let distinct = self.distinct(); + if distinct > MAX_DIST_ENTRIES as u64 && !matches!(self, DistAcc::OverBudget { .. }) { + *self = DistAcc::OverBudget { distinct }; + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn folds_counts_and_sorts_once() { + let mut acc = DistAcc::default(); + for v in [5, -2, 5, 9, -2, 5] { + acc.insert(DistValue::Int(v), 1); + } + assert_eq!( + acc.into_dist(), + ValueDist::Ints(vec![ + IntRun { + value: -2, + count: 2 + }, + IntRun { value: 5, count: 3 }, + IntRun { value: 9, count: 1 }, + ]) + ); + } + + #[test] + fn merged_accumulators_equal_one_pass() { + let values = [3.5, -1.0, 3.5, f64::NAN, 0.0, -0.0, 7.25]; + let mut one = DistAcc::default(); + for v in values { + one.insert(DistValue::Float(v), 1); + } + let (left, right) = values.split_at(3); + let mut a = DistAcc::default(); + for v in left { + a.insert(DistValue::Float(*v), 1); + } + let mut b = DistAcc::default(); + for v in right { + b.insert(DistValue::Float(*v), 1); + } + a.merge(b); + let (x, y) = (a.into_dist(), one.into_dist()); + assert_eq!(x.count(), y.count()); + assert_eq!(x.distinct(), y.distinct()); + assert_eq!(x.percentile(0.5), y.percentile(0.5)); + // -0.0 and 0.0 count as one value. + assert_eq!(y.distinct(), 5); + } + + #[test] + fn an_int_then_a_float_widens() { + let mut acc = DistAcc::default(); + acc.insert(DistValue::Int(2), 1); + acc.insert(DistValue::Float(2.5), 1); + acc.insert(DistValue::Int(2), 1); + assert_eq!( + acc.into_dist(), + ValueDist::Floats(vec![ + FloatRun { + value: 2.0, + count: 2 + }, + FloatRun { + value: 2.5, + count: 1 + }, + ]) + ); + } + + #[test] + fn the_wire_form_round_trips() { + let mut acc = DistAcc::default(); + for v in [2.5, -1.0, 2.5] { + acc.insert(DistValue::Float(v), 1); + } + let dist = acc.clone().into_dist(); + assert_eq!(DistAcc::from_dist(dist.clone()).into_dist(), dist); + assert_eq!(DistAcc::from_dist(dist), acc); + } + + #[test] + fn over_budget_absorbs_merges() { + let mut acc = DistAcc::OverBudget { distinct: 10 }; + acc.insert(DistValue::Int(1), 1); + acc.merge(DistAcc::Ints(HashMap::from([(2, 1)]))); + assert_eq!(acc.into_dist(), ValueDist::OverBudget { distinct: 10 }); + } +} diff --git a/nodedb-types/src/array_agg/histogram.rs b/nodedb-types/src/array_agg/histogram.rs new file mode 100644 index 000000000..609cb2d72 --- /dev/null +++ b/nodedb-types/src/array_agg/histogram.rs @@ -0,0 +1,185 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Fixed-width histogram bins. +//! +//! `HISTOGRAM(bins, lo, hi)` splits `[lo, hi]` into `bins` bins of width +//! `w = (hi - lo) / bins`. Bin `i` holds the values in +//! `[lo + i*w, lo + (i+1)*w)`, and the last bin also holds `hi`. A value +//! below `lo`, above `hi`, or NaN falls in no bin and is not counted. +//! Bin counts merge by addition. + +use std::fmt; + +use serde::{Deserialize, Serialize}; + +/// The most bins one histogram holds. +pub const MAX_HISTOGRAM_BINS: u32 = 65_536; + +/// The bins of a histogram. +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Serialize, + Deserialize, + zerompk::ToMessagePack, + zerompk::FromMessagePack, +)] +pub struct HistogramSpec { + pub bins: u32, + pub lo: f64, + pub hi: f64, +} + +/// Why a histogram spec is refused. +#[derive(Debug, Clone, PartialEq)] +pub enum HistogramSpecError { + /// `bins` is 0 or above [`MAX_HISTOGRAM_BINS`]. + Bins(u32), + /// `lo` or `hi` is not finite, or `lo >= hi`. + Range { lo: f64, hi: f64 }, +} + +impl fmt::Display for HistogramSpecError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + HistogramSpecError::Bins(bins) => write!( + f, + "HISTOGRAM bins {bins} must be between 1 and {MAX_HISTOGRAM_BINS}" + ), + HistogramSpecError::Range { lo, hi } => write!( + f, + "HISTOGRAM range [{lo}, {hi}] must be finite with lo < hi" + ), + } + } +} + +/// One bin of a finalized histogram. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct HistogramBin { + pub lo: f64, + pub hi: f64, + pub count: u64, +} + +impl HistogramSpec { + /// A checked spec. + pub fn new(bins: u32, lo: f64, hi: f64) -> Result { + if bins == 0 || bins > MAX_HISTOGRAM_BINS { + return Err(HistogramSpecError::Bins(bins)); + } + if !lo.is_finite() || !hi.is_finite() || lo >= hi { + return Err(HistogramSpecError::Range { lo, hi }); + } + Ok(Self { bins, lo, hi }) + } + + /// The bin `v` falls in, or `None` outside `[lo, hi]` or for NaN. + pub fn bin_of(&self, v: f64) -> Option { + if v.is_nan() || v < self.lo || v > self.hi { + return None; + } + let last = self.bins as usize - 1; + let raw = ((v - self.lo) / (self.hi - self.lo) * f64::from(self.bins)).floor(); + // `v == hi` lands on `bins`, and rounding can push a value just + // below a boundary up one bin past the last. Both go to the last bin. + Some((raw as usize).min(last)) + } + + /// The lower bound of bin `i`. Bin `bins` gives `hi`. + pub fn edge(&self, i: usize) -> f64 { + if i >= self.bins as usize { + return self.hi; + } + self.lo + (self.hi - self.lo) * (i as f64) / f64::from(self.bins) + } + + /// A zero count per bin. + pub fn empty_counts(&self) -> Vec { + vec![0; self.bins as usize] + } + + /// `counts` as bins with their bounds. A missing count reads as 0. + pub fn finish(&self, counts: &[u64]) -> Vec { + (0..self.bins as usize) + .map(|i| HistogramBin { + lo: self.edge(i), + hi: self.edge(i + 1), + count: counts.get(i).copied().unwrap_or(0), + }) + .collect() + } + + /// Add `other` into `into`, bin by bin. A shorter vector grows to the + /// spec's bin count. + pub fn merge_counts(&self, into: &mut Vec, other: &[u64]) { + let bins = self.bins as usize; + if into.len() < bins { + into.resize(bins, 0); + } + for (slot, n) in into.iter_mut().zip(other) { + *slot = slot.saturating_add(*n); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn values_fall_in_half_open_bins_and_hi_in_the_last() { + let s = HistogramSpec::new(4, 0.0, 1.0).expect("valid"); + assert_eq!(s.bin_of(0.0), Some(0)); + assert_eq!(s.bin_of(0.2499), Some(0)); + assert_eq!(s.bin_of(0.25), Some(1)); + assert_eq!(s.bin_of(0.99), Some(3)); + assert_eq!(s.bin_of(1.0), Some(3)); + assert_eq!(s.bin_of(-0.01), None); + assert_eq!(s.bin_of(1.01), None); + assert_eq!(s.bin_of(f64::NAN), None); + } + + #[test] + fn bins_carry_their_bounds() { + let s = HistogramSpec::new(2, -1.0, 1.0).expect("valid"); + let bins = s.finish(&[3, 4]); + assert_eq!( + bins, + vec![ + HistogramBin { + lo: -1.0, + hi: 0.0, + count: 3 + }, + HistogramBin { + lo: 0.0, + hi: 1.0, + count: 4 + }, + ] + ); + assert_eq!(s.finish(&[]).iter().map(|b| b.count).sum::(), 0); + } + + #[test] + fn counts_merge_by_addition() { + let s = HistogramSpec::new(3, 0.0, 3.0).expect("valid"); + let mut a = vec![1, 0, 2]; + s.merge_counts(&mut a, &[0, 5, 1]); + assert_eq!(a, vec![1, 5, 3]); + let mut empty = Vec::new(); + s.merge_counts(&mut empty, &[1, 1, 1]); + assert_eq!(empty, vec![1, 1, 1]); + } + + #[test] + fn bad_specs_are_refused() { + assert!(HistogramSpec::new(0, 0.0, 1.0).is_err()); + assert!(HistogramSpec::new(MAX_HISTOGRAM_BINS + 1, 0.0, 1.0).is_err()); + assert!(HistogramSpec::new(4, 1.0, 1.0).is_err()); + assert!(HistogramSpec::new(4, 0.0, f64::INFINITY).is_err()); + } +} diff --git a/nodedb-types/src/array_agg/mod.rs b/nodedb-types/src/array_agg/mod.rs new file mode 100644 index 000000000..8af9c918e --- /dev/null +++ b/nodedb-types/src/array_agg/mod.rs @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Exact, mergeable partial states of the array reducers that need more +//! than a running number: median, percentile and mode (a value +//! distribution), variance and standard deviation (moments), and +//! histogram (fixed-width bin counts). +//! +//! A Data Plane core builds a partial, a shard ships it, and the +//! coordinator merges and finalizes it. Every merge is exact. + +pub mod dist; +pub mod dist_acc; +pub mod histogram; +pub mod moments; + +pub use dist::{DistBudgetExceeded, DistValue, FloatRun, IntRun, MAX_DIST_ENTRIES, ValueDist}; +pub use dist_acc::DistAcc; +pub use histogram::{HistogramBin, HistogramSpec, HistogramSpecError, MAX_HISTOGRAM_BINS}; +pub use moments::Moments; diff --git a/nodedb-types/src/array_agg/moments.rs b/nodedb-types/src/array_agg/moments.rs new file mode 100644 index 000000000..bee20e2b4 --- /dev/null +++ b/nodedb-types/src/array_agg/moments.rs @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Running count, mean and sum of squared deviations (Welford), merged by +//! Chan's parallel formula. Variance and standard deviation finish from it. + +/// The moments of a set of values. +#[derive( + Debug, Clone, Copy, PartialEq, Default, zerompk::ToMessagePack, zerompk::FromMessagePack, +)] +pub struct Moments { + pub count: u64, + pub mean: f64, + /// Sum of squared deviations from the mean. + pub m2: f64, +} + +impl Moments { + /// The moments of one value. + pub fn one(v: f64) -> Self { + Self { + count: 1, + mean: v, + m2: 0.0, + } + } + + /// Add one value (Welford's update). + pub fn push(&mut self, v: f64) { + self.count += 1; + let delta = v - self.mean; + self.mean += delta / self.count as f64; + self.m2 += delta * (v - self.mean); + } + + /// The moments of both sets. + pub fn merge(self, other: Moments) -> Moments { + if other.count == 0 { + return self; + } + if self.count == 0 { + return other; + } + let count = self.count + other.count; + let (na, nb, n) = (self.count as f64, other.count as f64, count as f64); + let delta = other.mean - self.mean; + Moments { + count, + mean: self.mean + delta * nb / n, + m2: self.m2 + other.m2 + delta * delta * na * nb / n, + } + } + + /// The population variance `m2 / n`, or `None` with no value. + pub fn var_pop(&self) -> Option { + (self.count > 0).then(|| self.m2 / self.count as f64) + } + + /// The sample variance `m2 / (n - 1)`, or `None` with fewer than two + /// values, as PostgreSQL's `var_samp`. + pub fn var_samp(&self) -> Option { + (self.count > 1).then(|| self.m2 / (self.count - 1) as f64) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn of(values: &[f64]) -> Moments { + values + .iter() + .fold(Moments::default(), |m, v| m.merge(Moments::one(*v))) + } + + #[test] + fn variance_matches_the_definition() { + let m = of(&[2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0]); + assert!((m.var_pop().unwrap() - 4.0).abs() < 1e-12); + assert!((m.var_samp().unwrap() - 32.0 / 7.0).abs() < 1e-12); + } + + #[test] + fn split_merge_equals_one_pass() { + let all = [1.5, -3.0, 8.25, 0.0, 2.0]; + let (a, b) = all.split_at(2); + let merged = of(a).merge(of(b)); + let one = of(&all); + assert_eq!(merged.count, one.count); + assert!((merged.mean - one.mean).abs() < 1e-12); + assert!((merged.m2 - one.m2).abs() < 1e-9); + } + + #[test] + fn push_equals_merge_of_ones() { + let all = [1.5, -3.0, 8.25, 0.0, 2.0]; + let mut pushed = Moments::default(); + for v in all { + pushed.push(v); + } + let merged = of(&all); + assert_eq!(pushed.count, merged.count); + assert!((pushed.mean - merged.mean).abs() < 1e-12); + assert!((pushed.m2 - merged.m2).abs() < 1e-9); + } + + #[test] + fn too_few_values_have_no_variance() { + assert_eq!(Moments::default().var_pop(), None); + assert_eq!(Moments::one(3.0).var_samp(), None); + assert_eq!(Moments::one(3.0).var_pop(), Some(0.0)); + } +} diff --git a/nodedb-types/src/bbox.rs b/nodedb-types/src/bbox.rs index f0075f6f3..029076f9a 100644 --- a/nodedb-types/src/bbox.rs +++ b/nodedb-types/src/bbox.rs @@ -9,22 +9,15 @@ //! metres, nothing wraps, and `min_lng <= max_lng` always holds for a //! non-empty box. -use crate::geometry::Geometry; +use crate::geometry::{Geometry, Srid}; +use nodedb_srid::{CrsKind, SridResolver, UnknownSrid}; use serde::{Deserialize, Serialize}; use zerompk::{FromMessagePack, ToMessagePack}; -/// Metres per degree of latitude, the WGS 84 meridian average used by the -/// equirectangular expansion. -const METRES_PER_DEGREE_LAT: f64 = 110_540.0; - -/// Metres per degree of longitude at the equator (WGS 84 semi-major axis -/// times π / 180, rounded). -const METRES_PER_DEGREE_LNG_EQUATOR: f64 = 111_320.0; - /// The coordinate space a bounding box lives in. /// -/// Derived from the SRID of the geometries it bounds -/// (`Srid::bbox_space`). Union and enlargement depend on it: only +/// Derived from the kind of the SRID of the geometries it bounds +/// ([`BboxSpace::of`]). Union and enlargement depend on it: only /// geographic longitude wraps at ±180°. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] pub enum BboxSpace { @@ -35,6 +28,22 @@ pub enum BboxSpace { Planar, } +impl From for BboxSpace { + fn from(kind: CrsKind) -> Self { + match kind { + CrsKind::Geographic => BboxSpace::Geographic, + CrsKind::Projected => BboxSpace::Planar, + } + } +} + +impl BboxSpace { + /// The space of `srid`, from its kind in `ctx`. + pub fn of(ctx: &dyn SridResolver, srid: Srid) -> Result { + ctx.kind(srid).map(BboxSpace::from) + } +} + /// An axis-aligned bounding box. The `lng` fields hold x and the `lat` fields /// hold y: degrees for a geographic SRID, metres for a projected one. /// @@ -194,36 +203,6 @@ impl BoundingBox { } } - /// Expand by meters in geographic space. Same as - /// `expand_meters_in(meters, BboxSpace::Geographic)`. - pub fn expand_meters(&self, meters: f64) -> BoundingBox { - self.expand_meters_in(meters, BboxSpace::Geographic) - } - - /// Expand by `meters` on every side, in `space`. - /// - /// Geographic space converts metres to degrees with an equirectangular - /// approximation (<0.5% error within ±70° latitude). Planar space adds - /// the metres directly. - pub fn expand_meters_in(&self, meters: f64, space: BboxSpace) -> BoundingBox { - let (dlng, dlat) = match space { - BboxSpace::Geographic => { - let center_lat = (self.min_lat + self.max_lat) / 2.0; - ( - meters / (METRES_PER_DEGREE_LNG_EQUATOR * center_lat.to_radians().cos()), - meters / METRES_PER_DEGREE_LAT, - ) - } - BboxSpace::Planar => (meters, meters), - }; - BoundingBox { - min_lng: self.min_lng - dlng, - min_lat: self.min_lat - dlat, - max_lng: self.max_lng + dlng, - max_lat: self.max_lat + dlat, - } - } - pub fn overlap_area(&self, other: &BoundingBox) -> f64 { if !self.intersects(other) { return 0.0; @@ -455,14 +434,6 @@ mod tests { assert!((a.enlargement_in(&b, BboxSpace::Planar) - (7e5 - 1e5) * 10.0).abs() < 1e-6); } - #[test] - fn planar_expand_adds_metres_directly() { - let bb = BoundingBox::from_point(500_000.0, 100.0); - let e = bb.expand_meters_in(250.0, BboxSpace::Planar); - assert_eq!((e.min_lng, e.max_lng), (499_750.0, 500_250.0)); - assert_eq!((e.min_lat, e.max_lat), (-150.0, 350.0)); - } - #[test] fn overlap_area_partial() { let a = BoundingBox::new(0.0, 0.0, 10.0, 10.0); @@ -470,13 +441,6 @@ mod tests { assert!((a.overlap_area(&b) - 25.0).abs() < 1e-10); } - #[test] - fn expand_meters_basic() { - let bb = BoundingBox::from_point(0.0, 0.0); - let expanded = bb.expand_meters(1000.0); - assert!((expanded.min_lat - (-0.00905)).abs() < 0.001); - } - #[test] fn geometry_collection_bbox() { let gc = Geometry::GeometryCollection { diff --git a/nodedb-types/src/columnar/dml_wal_record.rs b/nodedb-types/src/columnar/dml_wal_record.rs index 02c3731fa..f551a6656 100644 --- a/nodedb-types/src/columnar/dml_wal_record.rs +++ b/nodedb-types/src/columnar/dml_wal_record.rs @@ -46,6 +46,14 @@ pub struct ColumnarDmlWalRecord { /// value is a MessagePack-encoded `UpdateValue`, a literal or an /// expression over the row. Always empty for `Delete`. pub updates: Vec<(String, Vec)>, + /// System-time ordinal every new version an update writes stamps, or at + /// which a delete closes each removed key on a bitemporal collection, + /// raised above the key's bound row at apply. Replay stamps what the + /// live write stamped. `None` for a writer that leaves the stamp to the + /// apply. + #[serde(default)] + #[msgpack(default)] + pub system_from: Option, } #[cfg(test)] @@ -61,6 +69,7 @@ mod tests { is_update: false, filters: vec![1, 2, 3], updates: Vec::new(), + system_from: None, }; let bytes = zerompk::to_msgpack_vec(&rec).expect("encode"); let decoded: ColumnarDmlWalRecord = zerompk::from_msgpack(&bytes).expect("decode"); @@ -75,6 +84,7 @@ mod tests { is_update: true, filters: vec![4, 5], updates: vec![("v".to_string(), vec![9, 9])], + system_from: Some(1_700_000_000_000_000_011), }; let bytes = zerompk::to_msgpack_vec(&rec).expect("encode"); let decoded: ColumnarDmlWalRecord = zerompk::from_msgpack(&bytes).expect("decode"); @@ -93,6 +103,7 @@ mod tests { is_update: false, filters: vec![1], updates: Vec::new(), + system_from: None, }; let bytes = zerompk::to_msgpack_vec(&rec).expect("encode"); let as_row_record: Result = zerompk::from_msgpack(&bytes); @@ -111,6 +122,7 @@ mod tests { provenance: None, surrogates: Vec::new(), conflict_policy: Vec::new(), + system_from: None, }; let bytes = zerompk::to_msgpack_vec(&rec).expect("encode"); let as_dml_record: Result = zerompk::from_msgpack(&bytes); diff --git a/nodedb-types/src/columnar/image_wal_record.rs b/nodedb-types/src/columnar/image_wal_record.rs index f67ef95a9..62e5f5f08 100644 --- a/nodedb-types/src/columnar/image_wal_record.rs +++ b/nodedb-types/src/columnar/image_wal_record.rs @@ -40,6 +40,14 @@ pub struct ColumnarImageWalRow { /// MessagePack-encoded post-image (`Value::Object`, column name to value, /// bitemporal columns included). Empty for a delete. pub image_msgpack: Vec, + /// The system-time ordinal the transaction's resolve fixed for this row + /// in a bitemporal collection: the post-image's `_ts_system`, and the + /// stamp of the tombstone that closes the base key when the write removes + /// it (a delete, or an update that changes the key). `None` for a + /// collection that is not bitemporal. + #[msgpack(default)] + #[serde(default)] + pub system_from: Option, } /// Map-encoded columnar row-image WAL record. @@ -81,11 +89,13 @@ mod tests { surrogate: 7, prior_pk_msgpack: vec![1], image_msgpack: vec![2, 3], + system_from: Some(11), }, ColumnarImageWalRow { surrogate: 8, prior_pk_msgpack: vec![4], image_msgpack: Vec::new(), + system_from: None, }, ], } diff --git a/nodedb-types/src/columnar/resolved_dml_wal_record.rs b/nodedb-types/src/columnar/resolved_dml_wal_record.rs index 2b5f16c38..83825df0a 100644 --- a/nodedb-types/src/columnar/resolved_dml_wal_record.rs +++ b/nodedb-types/src/columnar/resolved_dml_wal_record.rs @@ -57,6 +57,14 @@ pub struct ColumnarResolvedDmlWalRecord { pub is_update: bool, /// Every row the Control Plane resolved and the write policy admitted. pub rows: Vec, + /// System-time ordinal every new version an update writes stamps, or at + /// which a delete closes each removed key on a bitemporal collection, + /// raised above the key's bound row at apply. Replay stamps what the + /// live write stamped. `None` for a writer that leaves the stamp to the + /// apply. + #[serde(default)] + #[msgpack(default)] + pub system_from: Option, } #[cfg(test)] @@ -74,6 +82,7 @@ mod tests { pk_msgpack: vec![1, 2, 3], new_row_msgpack: vec![4, 5, 6], }], + system_from: None, }; let bytes = zerompk::to_msgpack_vec(&rec).expect("encode"); let decoded: ColumnarResolvedDmlWalRecord = zerompk::from_msgpack(&bytes).expect("decode"); @@ -90,6 +99,7 @@ mod tests { pk_msgpack: vec![7], new_row_msgpack: Vec::new(), }], + system_from: None, }; let bytes = zerompk::to_msgpack_vec(&rec).expect("encode"); let decoded: ColumnarResolvedDmlWalRecord = zerompk::from_msgpack(&bytes).expect("decode"); @@ -106,6 +116,7 @@ mod tests { pk_msgpack: vec![1], new_row_msgpack: Vec::new(), }], + system_from: None, }; let bytes = zerompk::to_msgpack_vec(&rec).expect("encode"); assert!(zerompk::from_msgpack::(&bytes).is_err()); diff --git a/nodedb-types/src/columnar/schema.rs b/nodedb-types/src/columnar/schema.rs index 975bbb922..8c8d785e3 100644 --- a/nodedb-types/src/columnar/schema.rs +++ b/nodedb-types/src/columnar/schema.rs @@ -100,7 +100,8 @@ pub struct ColumnarSchema { /// Reserved strict-tuple column names for bitemporal collections. Stored /// in fixed Int64 slots 0/1/2 so the decoder can extract them via a -/// constant-offset jump. +/// constant-offset jump. Slot 0 holds the host engine's system-time key: +/// Origin stores the system-time ordinal there, not milliseconds. pub const BITEMPORAL_SYSTEM_FROM: &str = "__system_from_ms"; pub const BITEMPORAL_VALID_FROM: &str = "__valid_from_ms"; pub const BITEMPORAL_VALID_UNTIL: &str = "__valid_until_ms"; @@ -341,12 +342,28 @@ impl ColumnarSchema { /// by `prepend_bitemporal_columns` on the write path, so its /// presence is a reliable bitemporal signal. pub fn is_bitemporal(&self) -> bool { - self.columns.iter().any(|c| c.name == "_ts_system") + self.ts_system_idx().is_some() } /// Position of the `_ts_system` column, or `None` for non-bitemporal. pub fn ts_system_idx(&self) -> Option { - self.columns.iter().position(|c| c.name == "_ts_system") + self.column_idx(TS_SYSTEM) + } + + /// Position of the `_ts_valid_from` column, or `None` for + /// non-bitemporal. + pub fn ts_valid_from_idx(&self) -> Option { + self.column_idx(TS_VALID_FROM) + } + + /// Position of the `_ts_valid_until` column, or `None` for + /// non-bitemporal. + pub fn ts_valid_until_idx(&self) -> Option { + self.column_idx(TS_VALID_UNTIL) + } + + fn column_idx(&self, name: &str) -> Option { + self.columns.iter().position(|c| c.name == name) } } @@ -432,4 +449,25 @@ mod tests { Err(SchemaError::ZeroVectorDim(_)) )); } + + #[test] + fn bitemporal_columns_resolve_by_reserved_name() { + let schema = ColumnarSchema::new(vec![ + ColumnDef::required(TS_SYSTEM, ColumnType::Int64), + ColumnDef::required(TS_VALID_FROM, ColumnType::Int64), + ColumnDef::required(TS_VALID_UNTIL, ColumnType::Int64), + ColumnDef::required("v", ColumnType::Int64), + ]) + .unwrap(); + assert!(schema.is_bitemporal()); + assert_eq!(schema.ts_system_idx(), Some(0)); + assert_eq!(schema.ts_valid_from_idx(), Some(1)); + assert_eq!(schema.ts_valid_until_idx(), Some(2)); + + let plain = ColumnarSchema::new(vec![ColumnDef::required("v", ColumnType::Int64)]).unwrap(); + assert!(!plain.is_bitemporal()); + assert_eq!(plain.ts_system_idx(), None); + assert_eq!(plain.ts_valid_from_idx(), None); + assert_eq!(plain.ts_valid_until_idx(), None); + } } diff --git a/nodedb-types/src/columnar/truncate_wal_record.rs b/nodedb-types/src/columnar/truncate_wal_record.rs index 508a62914..c1c36c796 100644 --- a/nodedb-types/src/columnar/truncate_wal_record.rs +++ b/nodedb-types/src/columnar/truncate_wal_record.rs @@ -4,9 +4,10 @@ //! //! Rides `RecordType::ColumnarTruncate` (columnar and spatial collections) //! and `RecordType::TimeseriesTruncate` (timeseries collections). The record -//! type says which engine clears the collection, so the payload carries the -//! collection name only; `restart_identity` is a Control-Plane sequence -//! concern applied after dispatch and never enters the Data-Plane record. +//! type says which engine clears the collection. The payload carries the +//! collection name and, for a bitemporal columnar collection, the ordinal its +//! tombstones stamp. `restart_identity` is a Control-Plane sequence concern +//! applied after dispatch and never enters the Data-Plane record. use serde::{Deserialize, Serialize}; @@ -25,6 +26,17 @@ use serde::{Deserialize, Serialize}; pub struct ColumnarTruncateWalRecord { /// Target collection name. pub collection: String, + /// The system-time ordinal at which a truncate of a bitemporal columnar + /// collection closes every current key, raised per key above its bound + /// row. `None` for any other truncate. + #[msgpack(default)] + #[serde(default)] + pub system_from: Option, + /// Erase every row and version even on a bitemporal collection: the + /// clear a backup restore runs before it reinstalls every version. + #[msgpack(default)] + #[serde(default)] + pub erase_history: bool, } #[cfg(test)] @@ -35,6 +47,8 @@ mod tests { fn round_trips() { let rec = ColumnarTruncateWalRecord { collection: "metrics".to_string(), + system_from: Some(42), + erase_history: true, }; let bytes = zerompk::to_msgpack_vec(&rec).expect("encode"); let back: ColumnarTruncateWalRecord = zerompk::from_msgpack(&bytes).expect("decode"); diff --git a/nodedb-types/src/columnar/wal_record.rs b/nodedb-types/src/columnar/wal_record.rs index 856342d20..a36fa2778 100644 --- a/nodedb-types/src/columnar/wal_record.rs +++ b/nodedb-types/src/columnar/wal_record.rs @@ -67,6 +67,13 @@ pub struct ColumnarWalRecord { #[serde(default)] #[msgpack(default)] pub conflict_policy: Vec, + /// System-time ordinal of the first row of a bitemporal insert. Row `i` + /// of `payload` stamps `system_from + i`. Replay stamps the ordinals the + /// live insert stamped. `None` for a collection that is not bitemporal, + /// and for a writer that leaves the stamp to the apply. + #[serde(default)] + #[msgpack(default)] + pub system_from: Option, } #[cfg(test)] @@ -88,6 +95,7 @@ mod tests { provenance: Some(prov.clone()), surrogates: vec![Surrogate::new(10), Surrogate::new(11), Surrogate::new(12)], conflict_policy: Vec::new(), + system_from: Some(1_700_000_000_000_000_007), }; let bytes = zerompk::to_msgpack_vec(&rec).expect("encode ColumnarWalRecord"); @@ -102,6 +110,7 @@ mod tests { decoded.surrogates, vec![Surrogate::new(10), Surrogate::new(11), Surrogate::new(12)] ); + assert_eq!(decoded.system_from, Some(1_700_000_000_000_000_007)); } #[test] @@ -113,6 +122,7 @@ mod tests { provenance: None, surrogates: Vec::new(), conflict_policy: Vec::new(), + system_from: None, }; let bytes = zerompk::to_msgpack_vec(&rec).expect("encode"); let decoded: ColumnarWalRecord = zerompk::from_msgpack(&bytes).expect("decode"); diff --git a/nodedb-types/src/config/tuning/engines.rs b/nodedb-types/src/config/tuning/engines.rs index cc604ba12..a3be769c3 100644 --- a/nodedb-types/src/config/tuning/engines.rs +++ b/nodedb-types/src/config/tuning/engines.rs @@ -110,6 +110,10 @@ pub const DEFAULT_VARLEN_MAX_FRONTIER: usize = 100_000; /// typical keys, so the default bounds one run near 1 GB before its CSR. pub const DEFAULT_MAX_GATHERED_ALGO_EDGES: usize = 10_000_000; +/// Default number of historical CSRs one Data Plane core keeps for +/// `AS OF` graph reads. +pub const DEFAULT_MAX_HISTORICAL_CSR_CACHE_ENTRIES: usize = 8; + /// Graph engine tuning (traversal limits, LCC algorithm). #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GraphTuning { @@ -140,6 +144,13 @@ pub struct GraphTuning { /// rather than answered from part of the graph. #[serde(default = "default_max_gathered_algo_edges")] pub max_gathered_algo_edges: usize, + /// Number of historical CSRs one Data Plane core keeps. An `AS OF` graph + /// read builds the CSR of one collection at one cut from the versioned + /// edge store. Later reads at the same cut reuse it until it is the + /// least recently used past this count. `0` keeps none: every historical + /// read builds its CSR again. + #[serde(default = "default_max_historical_csr_cache_entries")] + pub max_historical_csr_cache_entries: usize, } impl Default for GraphTuning { @@ -152,6 +163,7 @@ impl Default for GraphTuning { varlen_max_results: default_varlen_max_results(), varlen_max_frontier: default_varlen_max_frontier(), max_gathered_algo_edges: default_max_gathered_algo_edges(), + max_historical_csr_cache_entries: default_max_historical_csr_cache_entries(), } } } @@ -177,6 +189,9 @@ fn default_varlen_max_frontier() -> usize { fn default_max_gathered_algo_edges() -> usize { DEFAULT_MAX_GATHERED_ALGO_EDGES } +fn default_max_historical_csr_cache_entries() -> usize { + DEFAULT_MAX_HISTORICAL_CSR_CACHE_ENTRIES +} /// Timeseries engine tuning (memtable budgets, block sizes). #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/nodedb-types/src/geometry/math.rs b/nodedb-types/src/geometry/math.rs index 0aeb5c52a..1bb5ae0cc 100644 --- a/nodedb-types/src/geometry/math.rs +++ b/nodedb-types/src/geometry/math.rs @@ -2,9 +2,9 @@ //! Spherical geo math on (longitude, latitude) degrees. -/// Mean Earth radius in metres (IUGG R1), the sphere every haversine -/// measure in this crate uses. -const EARTH_RADIUS_M: f64 = 6_371_000.0; +/// Mean Earth radius in metres (IUGG R1), the sphere every haversine and +/// spherical-excess measure uses. +pub const EARTH_RADIUS_M: f64 = 6_371_000.0; /// Haversine distance between two points in meters. /// diff --git a/nodedb-types/src/geometry/mod.rs b/nodedb-types/src/geometry/mod.rs index 376bb2474..f025fb5fe 100644 --- a/nodedb-types/src/geometry/mod.rs +++ b/nodedb-types/src/geometry/mod.rs @@ -1,17 +1,20 @@ // SPDX-License-Identifier: Apache-2.0 -//! GeoJSON-compatible geometry types, spatial reference systems, and -//! spherical geo math. +//! GeoJSON-compatible geometry types, Z / M ordinates, spatial reference +//! systems, and spherical geo math. pub mod math; +pub mod ordinates; pub mod shape; pub mod srid; pub mod tagged; -pub use math::{haversine_bearing, haversine_distance, point_in_polygon}; -pub use shape::{Coord, Geometry}; -pub use srid::{SUPPORTED_SRID_CODES, Srid, UnsupportedSrid}; +pub use math::{EARTH_RADIUS_M, haversine_bearing, haversine_distance, point_in_polygon}; +pub use ordinates::{Dimension, Ordinate, OrdinateError, Ordinates, OrdinatesBuilder, Position}; +pub use shape::{Coord, Geometry, GeometryKind, is_empty_coord}; +pub use srid::{Srid, SridResolver}; pub use tagged::{ - CrsMember, CrsProperties, GeoJsonError, SridGeometry, from_geojson_str, parse_geojson, - parse_geojson_slice, srid_from_crs, + CrsCode, CrsMember, CrsProperties, GeoJsonError, GeoJsonParts, SridGeometry, + decode_stored_geojson, geojson_object, geojson_tree, parse_geojson, parse_geojson_slice, + read_geojson_value, srid_from_crs, stored_geojson_object, }; diff --git a/nodedb-types/src/geometry/ordinates.rs b/nodedb-types/src/geometry/ordinates.rs new file mode 100644 index 000000000..8f5bb3da6 --- /dev/null +++ b/nodedb-types/src/geometry/ordinates.rs @@ -0,0 +1,323 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Z and M ordinates carried beside a 2D [`Geometry`](super::Geometry). +//! +//! A shape holds XY only. Z (height) and M (measure) live in flat arrays, +//! one value per XY coordinate in the traversal order of +//! [`Geometry::coords`](super::Geometry::coords). + +/// Which ordinates every coordinate of a geometry carries. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Dimension { + Xy, + Xyz, + Xym, + Xyzm, +} + +impl Dimension { + /// The dimension with Z when `z`, M when `m`. + pub fn from_flags(z: bool, m: bool) -> Self { + match (z, m) { + (false, false) => Dimension::Xy, + (true, false) => Dimension::Xyz, + (false, true) => Dimension::Xym, + (true, true) => Dimension::Xyzm, + } + } + + pub fn has_z(self) -> bool { + matches!(self, Dimension::Xyz | Dimension::Xyzm) + } + + pub fn has_m(self) -> bool { + matches!(self, Dimension::Xym | Dimension::Xyzm) + } + + /// Number of ordinates per coordinate: 2, 3 or 4 (`ST_CoordDim`). + pub fn coord_dim(self) -> u8 { + 2 + u8::from(self.has_z()) + u8::from(self.has_m()) + } + + /// The PostGIS `ST_Zmflag` code: 0 XY, 1 XYM, 2 XYZ, 3 XYZM. + pub fn zm_flag(self) -> u8 { + u8::from(self.has_m()) + 2 * u8::from(self.has_z()) + } + + /// The WKT dimension tag: empty, `Z`, `M` or `ZM`. + pub fn wkt_tag(self) -> &'static str { + match self { + Dimension::Xy => "", + Dimension::Xyz => "Z", + Dimension::Xym => "M", + Dimension::Xyzm => "ZM", + } + } +} + +impl std::fmt::Display for Dimension { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + Dimension::Xy => "XY", + Dimension::Xyz => "XYZ", + Dimension::Xym => "XYM", + Dimension::Xyzm => "XYZM", + }) + } +} + +/// One coordinate with its optional Z and M. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Position { + pub x: f64, + pub y: f64, + pub z: Option, + pub m: Option, +} + +impl Position { + /// An XY position. + pub fn xy(x: f64, y: f64) -> Self { + Self { + x, + y, + z: None, + m: None, + } + } + + pub fn dimension(&self) -> Dimension { + Dimension::from_flags(self.z.is_some(), self.m.is_some()) + } +} + +/// The Z or M ordinate, for error reports. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Ordinate { + Z, + M, +} + +impl std::fmt::Display for Ordinate { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + Ordinate::Z => "Z", + Ordinate::M => "M", + }) + } +} + +/// Why a set of ordinates does not fit a geometry. +#[derive(Debug, Clone, PartialEq, thiserror::Error)] +pub enum OrdinateError { + #[error("{ordinate} has {actual} values for {expected} coordinates")] + LengthMismatch { + ordinate: Ordinate, + expected: usize, + actual: usize, + }, + #[error("{ordinate} value of coordinate {index} is not finite")] + NonFinite { ordinate: Ordinate, index: usize }, + #[error("coordinate {index} is {found}, earlier coordinates are {expected}")] + MixedDimension { + index: usize, + expected: Dimension, + found: Dimension, + }, +} + +/// Z and M values, one per XY coordinate, or absent. +#[derive(Debug, Clone, PartialEq, Default)] +pub struct Ordinates { + z: Option>, + m: Option>, +} + +impl Ordinates { + /// No Z, no M. + pub fn xy() -> Self { + Self::default() + } + + /// Z and M arrays as given. [`super::SridGeometry::with_ordinates`] + /// checks them against the shape. + pub fn new(z: Option>, m: Option>) -> Self { + Self { + z: z.map(Vec::into_boxed_slice), + m: m.map(Vec::into_boxed_slice), + } + } + + pub fn z(&self) -> Option<&[f64]> { + self.z.as_deref() + } + + pub fn m(&self) -> Option<&[f64]> { + self.m.as_deref() + } + + pub fn dimension(&self) -> Dimension { + Dimension::from_flags(self.z.is_some(), self.m.is_some()) + } + + pub fn is_xy(&self) -> bool { + self.z.is_none() && self.m.is_none() + } + + /// Z and M as owned vectors. + pub fn into_parts(self) -> (Option>, Option>) { + (self.z.map(Vec::from), self.m.map(Vec::from)) + } + + /// Check one value per coordinate and finite values. + pub fn check(&self, coord_count: usize) -> Result<(), OrdinateError> { + check_one(self.z(), Ordinate::Z, coord_count)?; + check_one(self.m(), Ordinate::M, coord_count) + } + + /// The Z and M of coordinate `index`. + pub fn at(&self, index: usize) -> (Option, Option) { + ( + self.z().and_then(|z| z.get(index).copied()), + self.m().and_then(|m| m.get(index).copied()), + ) + } +} + +fn check_one( + values: Option<&[f64]>, + ordinate: Ordinate, + expected: usize, +) -> Result<(), OrdinateError> { + let Some(values) = values else { + return Ok(()); + }; + if values.len() != expected { + return Err(OrdinateError::LengthMismatch { + ordinate, + expected, + actual: values.len(), + }); + } + match values.iter().position(|v| !v.is_finite()) { + Some(index) => Err(OrdinateError::NonFinite { ordinate, index }), + None => Ok(()), + } +} + +/// Collects Z and M while a parser walks coordinates in traversal order. +/// +/// The first coordinate fixes the dimension. A later coordinate with a +/// different one fails with [`OrdinateError::MixedDimension`]. +#[derive(Debug, Clone, Default)] +pub struct OrdinatesBuilder { + dimension: Option, + count: usize, + z: Vec, + m: Vec, +} + +impl OrdinatesBuilder { + /// A builder whose dimension is fixed up front, as a WKT or WKB type + /// tag declares it. + pub fn with_dimension(dimension: Dimension) -> Self { + Self { + dimension: Some(dimension), + ..Self::default() + } + } + + /// The dimension fixed so far. + pub fn dimension(&self) -> Option { + self.dimension + } + + /// Record the Z and M of the next coordinate. + pub fn push(&mut self, z: Option, m: Option) -> Result<(), OrdinateError> { + let found = Dimension::from_flags(z.is_some(), m.is_some()); + let expected = *self.dimension.get_or_insert(found); + if expected != found { + return Err(OrdinateError::MixedDimension { + index: self.count, + expected, + found, + }); + } + if let Some(z) = z { + self.z.push(z); + } + if let Some(m) = m { + self.m.push(m); + } + self.count += 1; + Ok(()) + } + + /// The collected ordinates. A builder with no coordinates and no fixed + /// dimension gives XY. + pub fn finish(self) -> Ordinates { + let dimension = self.dimension.unwrap_or(Dimension::Xy); + Ordinates::new( + dimension.has_z().then_some(self.z), + dimension.has_m().then_some(self.m), + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn dimension_codes_match_postgis() { + assert_eq!(Dimension::Xy.zm_flag(), 0); + assert_eq!(Dimension::Xym.zm_flag(), 1); + assert_eq!(Dimension::Xyz.zm_flag(), 2); + assert_eq!(Dimension::Xyzm.zm_flag(), 3); + assert_eq!(Dimension::Xyzm.coord_dim(), 4); + assert_eq!(Dimension::Xym.coord_dim(), 3); + } + + #[test] + fn check_rejects_wrong_length_and_non_finite() { + let o = Ordinates::new(Some(vec![1.0, 2.0]), None); + assert!(o.check(2).is_ok()); + assert_eq!( + o.check(3), + Err(OrdinateError::LengthMismatch { + ordinate: Ordinate::Z, + expected: 3, + actual: 2 + }) + ); + let nan = Ordinates::new(None, Some(vec![f64::NAN])); + assert_eq!( + nan.check(1), + Err(OrdinateError::NonFinite { + ordinate: Ordinate::M, + index: 0 + }) + ); + } + + #[test] + fn builder_refuses_mixed_dimensions() { + let mut b = OrdinatesBuilder::default(); + b.push(Some(1.0), None).unwrap(); + assert_eq!( + b.push(None, None), + Err(OrdinateError::MixedDimension { + index: 1, + expected: Dimension::Xyz, + found: Dimension::Xy + }) + ); + } + + #[test] + fn builder_with_fixed_dimension_and_no_coords_keeps_it() { + let o = OrdinatesBuilder::with_dimension(Dimension::Xyzm).finish(); + assert_eq!(o.dimension(), Dimension::Xyzm); + assert_eq!(o.z(), Some(&[][..])); + } +} diff --git a/nodedb-types/src/geometry/shape.rs b/nodedb-types/src/geometry/shape.rs index 1ed8fda4c..def50f80b 100644 --- a/nodedb-types/src/geometry/shape.rs +++ b/nodedb-types/src/geometry/shape.rs @@ -34,6 +34,92 @@ impl Coord { } } +/// The kind of a geometry, without its coordinates. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum GeometryKind { + Point, + LineString, + Polygon, + MultiPoint, + MultiLineString, + MultiPolygon, + GeometryCollection, +} + +impl GeometryKind { + /// The OGC type name, as `ST_GeometryType` and GeoJSON spell it. + pub fn name(self) -> &'static str { + match self { + GeometryKind::Point => "Point", + GeometryKind::LineString => "LineString", + GeometryKind::Polygon => "Polygon", + GeometryKind::MultiPoint => "MultiPoint", + GeometryKind::MultiLineString => "MultiLineString", + GeometryKind::MultiPolygon => "MultiPolygon", + GeometryKind::GeometryCollection => "GeometryCollection", + } + } + + /// The upper-case WKT keyword. + pub fn wkt_keyword(self) -> &'static str { + match self { + GeometryKind::Point => "POINT", + GeometryKind::LineString => "LINESTRING", + GeometryKind::Polygon => "POLYGON", + GeometryKind::MultiPoint => "MULTIPOINT", + GeometryKind::MultiLineString => "MULTILINESTRING", + GeometryKind::MultiPolygon => "MULTIPOLYGON", + GeometryKind::GeometryCollection => "GEOMETRYCOLLECTION", + } + } + + /// The ISO WKB base type code, 1 to 7. + pub fn wkb_code(self) -> u32 { + match self { + GeometryKind::Point => 1, + GeometryKind::LineString => 2, + GeometryKind::Polygon => 3, + GeometryKind::MultiPoint => 4, + GeometryKind::MultiLineString => 5, + GeometryKind::MultiPolygon => 6, + GeometryKind::GeometryCollection => 7, + } + } + + /// The kind with WKB base type code `code`. + pub fn from_wkb_code(code: u32) -> Option { + Some(match code { + 1 => GeometryKind::Point, + 2 => GeometryKind::LineString, + 3 => GeometryKind::Polygon, + 4 => GeometryKind::MultiPoint, + 5 => GeometryKind::MultiLineString, + 6 => GeometryKind::MultiPolygon, + 7 => GeometryKind::GeometryCollection, + _ => return None, + }) + } + + /// The kind GeoJSON names `name`, case-sensitive as RFC 7946 requires. + pub fn from_geojson_name(name: &str) -> Option { + Some(match name { + "Point" => GeometryKind::Point, + "LineString" => GeometryKind::LineString, + "Polygon" => GeometryKind::Polygon, + "MultiPoint" => GeometryKind::MultiPoint, + "MultiLineString" => GeometryKind::MultiLineString, + "MultiPolygon" => GeometryKind::MultiPolygon, + "GeometryCollection" => GeometryKind::GeometryCollection, + _ => return None, + }) + } +} + +/// Whether a coordinate is the NaN pair of an EMPTY point. +pub fn is_empty_coord(c: [f64; 2]) -> bool { + c[0].is_nan() && c[1].is_nan() +} + /// GeoJSON-compatible geometry types. #[derive( Debug, @@ -45,7 +131,6 @@ impl Coord { zerompk::FromMessagePack, )] #[serde(tag = "type")] -#[non_exhaustive] pub enum Geometry { Point { coordinates: [f64; 2], @@ -95,18 +180,121 @@ impl Geometry { /// Get the type name of this geometry. pub fn geometry_type(&self) -> &'static str { + self.kind().name() + } + + /// The EMPTY point. Its two ordinates are NaN, the encoding WKB uses. + pub fn empty_point() -> Self { + Geometry::Point { + coordinates: [f64::NAN, f64::NAN], + } + } + + /// An EMPTY geometry of `kind`. + pub fn empty(kind: GeometryKind) -> Self { + match kind { + GeometryKind::Point => Self::empty_point(), + GeometryKind::LineString => Geometry::LineString { + coordinates: Vec::new(), + }, + GeometryKind::Polygon => Geometry::Polygon { + coordinates: Vec::new(), + }, + GeometryKind::MultiPoint => Geometry::MultiPoint { + coordinates: Vec::new(), + }, + GeometryKind::MultiLineString => Geometry::MultiLineString { + coordinates: Vec::new(), + }, + GeometryKind::MultiPolygon => Geometry::MultiPolygon { + coordinates: Vec::new(), + }, + GeometryKind::GeometryCollection => Geometry::GeometryCollection { + geometries: Vec::new(), + }, + } + } + + /// The kind of this geometry. + pub fn kind(&self) -> GeometryKind { + match self { + Geometry::Point { .. } => GeometryKind::Point, + Geometry::LineString { .. } => GeometryKind::LineString, + Geometry::Polygon { .. } => GeometryKind::Polygon, + Geometry::MultiPoint { .. } => GeometryKind::MultiPoint, + Geometry::MultiLineString { .. } => GeometryKind::MultiLineString, + Geometry::MultiPolygon { .. } => GeometryKind::MultiPolygon, + Geometry::GeometryCollection { .. } => GeometryKind::GeometryCollection, + } + } + + /// Whether the geometry has no coordinates (OGC EMPTY). A point is + /// EMPTY when both its ordinates are NaN. + pub fn is_empty(&self) -> bool { + match self { + Geometry::Point { coordinates } => is_empty_coord(*coordinates), + Geometry::LineString { coordinates } | Geometry::MultiPoint { coordinates } => { + coordinates.is_empty() + } + Geometry::Polygon { coordinates } | Geometry::MultiLineString { coordinates } => { + coordinates.iter().all(Vec::is_empty) + } + Geometry::MultiPolygon { coordinates } => coordinates + .iter() + .all(|polygon| polygon.iter().all(Vec::is_empty)), + Geometry::GeometryCollection { geometries } => geometries.iter().all(Self::is_empty), + } + } + + /// Number of XY coordinates, in the order [`Geometry::coords`] yields + /// them. An EMPTY point has none. + pub fn coord_count(&self) -> usize { match self { - Geometry::Point { .. } => "Point", - Geometry::LineString { .. } => "LineString", - Geometry::Polygon { .. } => "Polygon", - Geometry::MultiPoint { .. } => "MultiPoint", - Geometry::MultiLineString { .. } => "MultiLineString", - Geometry::MultiPolygon { .. } => "MultiPolygon", - Geometry::GeometryCollection { .. } => "GeometryCollection", + Geometry::Point { coordinates } => usize::from(!is_empty_coord(*coordinates)), + Geometry::LineString { coordinates } | Geometry::MultiPoint { coordinates } => { + coordinates.len() + } + Geometry::Polygon { coordinates } | Geometry::MultiLineString { coordinates } => { + coordinates.iter().map(Vec::len).sum() + } + Geometry::MultiPolygon { coordinates } => coordinates + .iter() + .flat_map(|polygon| polygon.iter()) + .map(Vec::len) + .sum(), + Geometry::GeometryCollection { geometries } => { + geometries.iter().map(Self::coord_count).sum() + } } } - /// Map every coordinate through `f`, keeping the shape. + /// Every XY coordinate in traversal order: rings in order, polygons in + /// order, collection members in order. Z and M arrays index the same + /// sequence. + pub fn coords(&self) -> Box + '_> { + match self { + Geometry::Point { coordinates } => Box::new( + (!is_empty_coord(*coordinates)) + .then_some(*coordinates) + .into_iter(), + ), + Geometry::LineString { coordinates } | Geometry::MultiPoint { coordinates } => { + Box::new(coordinates.iter().copied()) + } + Geometry::Polygon { coordinates } | Geometry::MultiLineString { coordinates } => { + Box::new(coordinates.iter().flatten().copied()) + } + Geometry::MultiPolygon { coordinates } => { + Box::new(coordinates.iter().flatten().flatten().copied()) + } + Geometry::GeometryCollection { geometries } => { + Box::new(geometries.iter().flat_map(Self::coords)) + } + } + } + + /// Map every coordinate through `f`, keeping the shape. An EMPTY point + /// stays EMPTY and `f` never sees it. /// /// Stops at the first coordinate `f` rejects and returns its error. pub fn try_map_coords( @@ -114,6 +302,7 @@ impl Geometry { f: &mut impl FnMut([f64; 2]) -> Result<[f64; 2], E>, ) -> Result { Ok(match self { + Geometry::Point { coordinates } if is_empty_coord(*coordinates) => self.clone(), Geometry::Point { coordinates } => Geometry::Point { coordinates: f(*coordinates)?, }, @@ -212,6 +401,40 @@ mod tests { assert_eq!(geometries[0], Geometry::point(11.0, 2.0)); } + #[test] + fn coords_follow_traversal_order_and_skip_an_empty_point() { + let gc = Geometry::GeometryCollection { + geometries: vec![ + Geometry::empty_point(), + Geometry::polygon(vec![vec![[0.0, 0.0], [1.0, 0.0], [0.0, 0.0]]]), + Geometry::point(5.0, 6.0), + ], + }; + let coords: Vec<_> = gc.coords().collect(); + assert_eq!(coords.len(), gc.coord_count()); + assert_eq!(coords, vec![[0.0, 0.0], [1.0, 0.0], [0.0, 0.0], [5.0, 6.0]]); + } + + #[test] + fn every_kind_has_an_empty_form() { + for kind in [ + GeometryKind::Point, + GeometryKind::LineString, + GeometryKind::Polygon, + GeometryKind::MultiPoint, + GeometryKind::MultiLineString, + GeometryKind::MultiPolygon, + GeometryKind::GeometryCollection, + ] { + let g = Geometry::empty(kind); + assert!(g.is_empty(), "{kind:?}"); + assert_eq!(g.kind(), kind); + assert_eq!(g.coord_count(), 0); + assert_eq!(GeometryKind::from_wkb_code(kind.wkb_code()), Some(kind)); + } + assert!(!Geometry::point(0.0, 0.0).is_empty()); + } + #[test] fn try_map_coords_stops_at_first_error() { let line = Geometry::line_string(vec![[0.0, 0.0], [1.0, 1.0]]); diff --git a/nodedb-types/src/geometry/srid.rs b/nodedb-types/src/geometry/srid.rs index 4bb85739d..c017fbd27 100644 --- a/nodedb-types/src/geometry/srid.rs +++ b/nodedb-types/src/geometry/srid.rs @@ -1,169 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 -//! The closed set of coordinate reference systems NodeDB supports. +//! Spatial reference system identifiers. The registry that defines them +//! lives in `nodedb-crs`, which `nodedb-types` never links. -use serde::{Deserialize, Serialize}; - -use crate::bbox::BboxSpace; - -/// Every supported SRID code, comma-separated, for error messages. -pub const SUPPORTED_SRID_CODES: &str = "4326, 32647, 32648, 3375, 3376"; - -/// A spatial reference system identifier, restricted to the systems NodeDB -/// can transform between. -/// -/// The set is closed: an EPSG code outside it is rejected with -/// [`UnsupportedSrid`], never stored. Serialized as its numeric EPSG code. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)] -#[serde(into = "u32", try_from = "u32")] -pub enum Srid { - /// EPSG:4326, WGS 84 geographic. x is longitude, y is latitude, degrees. - #[default] - Wgs84, - /// EPSG:32647, UTM zone 47N on WGS 84. Metres. - Utm47N, - /// EPSG:32648, UTM zone 48N on WGS 84. Metres. - Utm48N, - /// EPSG:3375, GDM2000 Peninsula RSO. Metres. - PeninsularRso, - /// EPSG:3376, GDM2000 East Malaysia (Borneo) RSO. Metres. - BorneoRso, -} - -/// An SRID code outside the supported set. -#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] -#[error("unsupported SRID {code}; supported: {SUPPORTED_SRID_CODES}")] -pub struct UnsupportedSrid { - pub code: i64, -} - -impl Srid { - /// Every supported SRID, WGS 84 first. - pub const SUPPORTED: [Srid; 5] = [ - Srid::Wgs84, - Srid::Utm47N, - Srid::Utm48N, - Srid::PeninsularRso, - Srid::BorneoRso, - ]; - - /// The EPSG code. - pub const fn code(self) -> u32 { - match self { - Srid::Wgs84 => 4326, - Srid::Utm47N => 32647, - Srid::Utm48N => 32648, - Srid::PeninsularRso => 3375, - Srid::BorneoRso => 3376, - } - } - - /// Resolve an EPSG code. Any code outside [`Srid::SUPPORTED`] fails. - pub fn from_code(code: i64) -> Result { - Srid::SUPPORTED - .into_iter() - .find(|srid| i64::from(srid.code()) == code) - .ok_or(UnsupportedSrid { code }) - } - - /// Whether coordinates are longitude / latitude degrees. A `false` SRID - /// is projected, with coordinates in metres. - pub const fn is_geographic(self) -> bool { - match self { - Srid::Wgs84 => true, - Srid::Utm47N | Srid::Utm48N | Srid::PeninsularRso | Srid::BorneoRso => false, - } - } - - /// The coordinate space bounding boxes in this SRID live in. - pub const fn bbox_space(self) -> BboxSpace { - if self.is_geographic() { - BboxSpace::Geographic - } else { - BboxSpace::Planar - } - } -} - -impl From for u32 { - fn from(srid: Srid) -> u32 { - srid.code() - } -} - -impl TryFrom for Srid { - type Error = UnsupportedSrid; - - fn try_from(code: u32) -> Result { - Srid::from_code(i64::from(code)) - } -} - -impl std::fmt::Display for Srid { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.code()) - } -} - -impl zerompk::ToMessagePack for Srid { - fn write(&self, writer: &mut W) -> zerompk::Result<()> { - writer.write_u32(self.code()) - } -} - -impl<'a> zerompk::FromMessagePack<'a> for Srid { - fn read>(reader: &mut R) -> zerompk::Result { - let code = reader.read_u32()?; - Srid::try_from(code).map_err(|e| zerompk::Error::UnknownKey(e.to_string())) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn every_supported_code_round_trips() { - for srid in Srid::SUPPORTED { - assert_eq!(Srid::from_code(i64::from(srid.code())), Ok(srid)); - } - } - - #[test] - fn unsupported_code_names_every_supported_code() { - let err = Srid::from_code(2154).unwrap_err(); - let text = err.to_string(); - for srid in Srid::SUPPORTED { - assert!(text.contains(&srid.code().to_string()), "{text}"); - } - assert!(text.contains("2154"), "{text}"); - } - - #[test] - fn only_wgs84_is_geographic() { - assert!(Srid::Wgs84.is_geographic()); - assert_eq!(Srid::Wgs84.bbox_space(), BboxSpace::Geographic); - for srid in &Srid::SUPPORTED[1..] { - assert!(!srid.is_geographic()); - assert_eq!(srid.bbox_space(), BboxSpace::Planar); - } - } - - #[test] - fn default_is_wgs84() { - assert_eq!(Srid::default(), Srid::Wgs84); - } - - #[test] - fn msgpack_round_trip() { - let bytes = zerompk::to_msgpack_vec(&Srid::BorneoRso).unwrap(); - let back: Srid = zerompk::from_msgpack(&bytes).unwrap(); - assert_eq!(back, Srid::BorneoRso); - } - - #[test] - fn msgpack_rejects_unknown_code() { - let bytes = zerompk::to_msgpack_vec(&2154u32).unwrap(); - assert!(zerompk::from_msgpack::(&bytes).is_err()); - } -} +pub use nodedb_srid::{Srid, SridResolver}; diff --git a/nodedb-types/src/geometry/tagged.rs b/nodedb-types/src/geometry/tagged.rs deleted file mode 100644 index be786e583..000000000 --- a/nodedb-types/src/geometry/tagged.rs +++ /dev/null @@ -1,368 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -//! A geometry tagged with its spatial reference system, and its GeoJSON form. -//! -//! GeoJSON (RFC 7946) fixes the reference system to WGS 84. A geometry in any -//! other SRID carries the 2008 GeoJSON `crs` member: -//! `"crs":{"type":"name","properties":{"name":"EPSG:"}}`. A WGS 84 -//! geometry writes no `crs` member, so its text stays plain RFC 7946. - -use serde::de::Error as _; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; - -use super::shape::Geometry; -use super::srid::{Srid, UnsupportedSrid}; -use crate::bbox::{BoundingBox, geometry_bbox_in}; - -/// A geometry and the SRID its coordinates are in. -#[derive(Debug, Clone, PartialEq)] -pub struct SridGeometry { - pub srid: Srid, - pub geometry: Geometry, -} - -impl SridGeometry { - pub fn new(srid: Srid, geometry: Geometry) -> Self { - Self { srid, geometry } - } - - /// A geometry in WGS 84, the SRID of untagged GeoJSON. - pub fn wgs84(geometry: Geometry) -> Self { - Self::new(Srid::Wgs84, geometry) - } - - /// The same SRID around a different geometry. - pub fn with_geometry(&self, geometry: Geometry) -> Self { - Self::new(self.srid, geometry) - } - - /// Bounding box in the coordinate space of this SRID. - pub fn bbox(&self) -> BoundingBox { - geometry_bbox_in(&self.geometry, self.srid.bbox_space()) - } - - /// Read a geometry out of a document field value. - /// - /// Accepts the three stored forms: a native `Value::Geometry`, a GeoJSON - /// string, and a GeoJSON object decoded from a document. Any other value, - /// or GeoJSON that does not parse, gives `None`. - pub fn from_value(value: &crate::Value) -> Option { - match value { - crate::Value::Geometry(g) => Some(g.clone()), - crate::Value::String(s) => from_geojson_str(s), - crate::Value::Object(_) => { - let json = serde_json::Value::from(value.clone()); - serde_json::from_value(json).ok() - } - _ => None, - } - } -} - -impl From for SridGeometry { - fn from(geometry: Geometry) -> Self { - Self::wgs84(geometry) - } -} - -/// A total, deterministic text form. A WGS 84 geometry renders as its shape. -/// Any other SRID gets an EWKT-style `SRID=;` prefix. -impl std::fmt::Display for SridGeometry { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self.srid { - Srid::Wgs84 => write!(f, "{:?}", self.geometry), - Srid::Utm47N | Srid::Utm48N | Srid::PeninsularRso | Srid::BorneoRso => { - write!(f, "SRID={};{:?}", self.srid.code(), self.geometry) - } - } - } -} - -/// Why a GeoJSON text does not yield a [`SridGeometry`]. -#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] -pub enum GeoJsonError { - #[error("malformed GeoJSON geometry")] - Malformed, - #[error("unrecognized GeoJSON crs name {name:?}")] - UnknownCrsName { name: String }, - #[error(transparent)] - UnsupportedSrid(#[from] UnsupportedSrid), -} - -/// The GeoJSON 2008 named `crs` member. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct CrsMember { - #[serde(rename = "type")] - pub kind: String, - pub properties: CrsProperties, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct CrsProperties { - pub name: String, -} - -/// The `type` value of a named `crs` member. -const CRS_KIND_NAME: &str = "name"; - -/// OGC URN prefix that ends in a version segment and then an EPSG code. -const OGC_EPSG_URN_PREFIX: &str = "urn:ogc:def:crs:epsg:"; - -/// The OGC URNs for WGS 84 in longitude / latitude order. -const OGC_CRS84_URNS: [&str; 2] = [ - "urn:ogc:def:crs:ogc:1.3:crs84", - "urn:ogc:def:crs:ogc::crs84", -]; - -impl CrsMember { - fn for_srid(srid: Srid) -> Self { - Self { - kind: CRS_KIND_NAME.to_string(), - properties: CrsProperties { - name: format!("EPSG:{}", srid.code()), - }, - } - } -} - -/// Resolve an optional `crs` member to an SRID. No member means WGS 84. -/// -/// Accepts `EPSG:`, `urn:ogc:def:crs:EPSG:[version]:` and the OGC -/// CRS84 URNs. Names compare without regard to ASCII case. -pub fn srid_from_crs(crs: Option<&CrsMember>) -> Result { - let Some(member) = crs else { - return Ok(Srid::Wgs84); - }; - let name = member.properties.name.trim(); - let unknown = || GeoJsonError::UnknownCrsName { - name: name.to_string(), - }; - if !member.kind.eq_ignore_ascii_case(CRS_KIND_NAME) { - return Err(unknown()); - } - let lower = name.to_ascii_lowercase(); - if OGC_CRS84_URNS.contains(&lower.as_str()) { - return Ok(Srid::Wgs84); - } - let code_text = if let Some(code) = lower.strip_prefix("epsg:") { - code - } else if let Some(rest) = lower.strip_prefix(OGC_EPSG_URN_PREFIX) { - // `rest` is `:`. The version can be empty. - rest.rsplit_once(':') - .map(|(_, code)| code) - .ok_or_else(unknown)? - } else { - return Err(unknown()); - }; - let code: i64 = code_text.parse().map_err(|_| unknown())?; - Ok(Srid::from_code(code)?) -} - -/// Only the `crs` member of a GeoJSON object. Every other key is skipped -/// without being built, so probing costs one shallow pass. -#[derive(Deserialize)] -struct CrsProbe { - #[serde(default)] - crs: Option, -} - -/// Parse GeoJSON text into a [`SridGeometry`]. -/// -/// The geometry parses through its own derive. The `crs` member is then read -/// by a separate shallow probe, so the common untagged case never buffers -/// the document. Uses `sonic_rs` per workspace policy. -pub fn parse_geojson(text: &str) -> Result { - let geometry: Geometry = sonic_rs::from_str(text).map_err(|_| GeoJsonError::Malformed)?; - let probe: CrsProbe = sonic_rs::from_str(text).map_err(|_| GeoJsonError::Malformed)?; - let srid = srid_from_crs(probe.crs.as_ref())?; - Ok(SridGeometry::new(srid, geometry)) -} - -/// [`parse_geojson`] over raw UTF-8 bytes. -pub fn parse_geojson_slice(bytes: &[u8]) -> Result { - let text = std::str::from_utf8(bytes).map_err(|_| GeoJsonError::Malformed)?; - parse_geojson(text) -} - -/// Parse a GeoJSON string into a [`SridGeometry`]. -/// -/// Shared by every storage/read path that meets geometry stored as a JSON -/// string rather than a native value: SQL `ST_Point(...)` inserts serialize -/// to a GeoJSON string, while schemaless document writes and -/// `Value::Geometry` keep the native form. The document index build, the -/// spatial read path and the columnar geometry index all read through here. -/// -/// Returns `None` on malformed JSON, non-geometry JSON, or a `crs` member -/// naming an unsupported SRID. -pub fn from_geojson_str(s: &str) -> Option { - parse_geojson(s).ok() -} - -#[derive(Serialize)] -struct SridGeometryOut<'a> { - #[serde(flatten)] - geometry: &'a Geometry, - #[serde(skip_serializing_if = "Option::is_none")] - crs: Option, -} - -#[derive(Deserialize)] -struct SridGeometryIn { - #[serde(flatten)] - geometry: Geometry, - #[serde(default)] - crs: Option, -} - -impl Serialize for SridGeometry { - fn serialize(&self, serializer: S) -> Result { - let crs = match self.srid { - Srid::Wgs84 => None, - Srid::Utm47N | Srid::Utm48N | Srid::PeninsularRso | Srid::BorneoRso => { - Some(CrsMember::for_srid(self.srid)) - } - }; - SridGeometryOut { - geometry: &self.geometry, - crs, - } - .serialize(serializer) - } -} - -impl<'de> Deserialize<'de> for SridGeometry { - fn deserialize>(deserializer: D) -> Result { - let raw = SridGeometryIn::deserialize(deserializer)?; - let srid = srid_from_crs(raw.crs.as_ref()).map_err(D::Error::custom)?; - Ok(SridGeometry::new(srid, raw.geometry)) - } -} - -/// MessagePack form: `[srid_code_u32, geometry]`. -impl zerompk::ToMessagePack for SridGeometry { - fn write(&self, writer: &mut W) -> zerompk::Result<()> { - writer.write_array_len(2)?; - self.srid.write(writer)?; - self.geometry.write(writer) - } -} - -impl<'a> zerompk::FromMessagePack<'a> for SridGeometry { - fn read>(reader: &mut R) -> zerompk::Result { - let len = reader.read_array_len()?; - if len != 2 { - return Err(zerompk::Error::ArrayLengthMismatch { - expected: 2, - actual: len, - }); - } - let srid = Srid::read(reader)?; - let geometry = Geometry::read(reader)?; - Ok(SridGeometry::new(srid, geometry)) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - fn crs(name: &str) -> CrsMember { - CrsMember { - kind: "name".into(), - properties: CrsProperties { name: name.into() }, - } - } - - #[test] - fn wgs84_writes_no_crs_member() { - let g = SridGeometry::wgs84(Geometry::point(1.0, 2.0)); - let json = sonic_rs::to_string(&g).unwrap(); - assert!(!json.contains("crs"), "{json}"); - assert!(json.contains("\"type\":\"Point\""), "{json}"); - } - - #[test] - fn projected_geojson_round_trip_keeps_srid() { - let g = SridGeometry::new(Srid::Utm48N, Geometry::point(134445.5, 349624.7)); - let json = sonic_rs::to_string(&g).unwrap(); - assert!(json.contains("\"name\":\"EPSG:32648\""), "{json}"); - assert_eq!(parse_geojson(&json), Ok(g.clone())); - let derived: SridGeometry = sonic_rs::from_str(&json).unwrap(); - assert_eq!(derived, g); - } - - #[test] - fn missing_crs_member_is_wgs84() { - let g = parse_geojson(r#"{"type":"Point","coordinates":[1.0,2.0]}"#).unwrap(); - assert_eq!(g.srid, Srid::Wgs84); - } - - #[test] - fn every_accepted_crs_spelling_resolves() { - assert_eq!( - srid_from_crs(Some(&crs("EPSG:3375"))), - Ok(Srid::PeninsularRso) - ); - assert_eq!( - srid_from_crs(Some(&crs("urn:ogc:def:crs:EPSG::3376"))), - Ok(Srid::BorneoRso) - ); - assert_eq!( - srid_from_crs(Some(&crs("urn:ogc:def:crs:OGC:1.3:CRS84"))), - Ok(Srid::Wgs84) - ); - assert_eq!(srid_from_crs(Some(&crs("epsg:32647"))), Ok(Srid::Utm47N)); - } - - #[test] - fn unsupported_and_unknown_crs_names_fail() { - assert_eq!( - srid_from_crs(Some(&crs("EPSG:2154"))), - Err(GeoJsonError::UnsupportedSrid(UnsupportedSrid { - code: 2154 - })) - ); - assert!(matches!( - srid_from_crs(Some(&crs("urn:x:y"))), - Err(GeoJsonError::UnknownCrsName { .. }) - )); - } - - #[test] - fn from_value_reads_every_stored_form() { - let g = SridGeometry::new(Srid::Utm47N, Geometry::point(1.0, 2.0)); - let text = sonic_rs::to_string(&g).unwrap(); - assert_eq!( - SridGeometry::from_value(&crate::Value::Geometry(g.clone())), - Some(g.clone()) - ); - assert_eq!( - SridGeometry::from_value(&crate::Value::String(text.clone())), - Some(g.clone()) - ); - let json: serde_json::Value = sonic_rs::from_str(&text).unwrap(); - let obj = crate::Value::from(json); - assert_eq!(SridGeometry::from_value(&obj), Some(g)); - } - - #[test] - fn msgpack_round_trip_keeps_srid() { - let g = SridGeometry::new(Srid::BorneoRso, Geometry::point(1.0, 2.0)); - let bytes = zerompk::to_msgpack_vec(&g).unwrap(); - let back: SridGeometry = zerompk::from_msgpack(&bytes).unwrap(); - assert_eq!(back, g); - } - - #[test] - fn display_prefixes_non_wgs84() { - let g = SridGeometry::new(Srid::Utm48N, Geometry::point(1.0, 2.0)); - assert_eq!( - g.to_string(), - "SRID=32648;Point { coordinates: [1.0, 2.0] }" - ); - assert_eq!( - SridGeometry::wgs84(Geometry::point(1.0, 2.0)).to_string(), - "Point { coordinates: [1.0, 2.0] }" - ); - } -} diff --git a/nodedb-types/src/geometry/tagged/crs_member.rs b/nodedb-types/src/geometry/tagged/crs_member.rs new file mode 100644 index 000000000..ac5e1d8b3 --- /dev/null +++ b/nodedb-types/src/geometry/tagged/crs_member.rs @@ -0,0 +1,215 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! The GeoJSON 2008 `crs` member. +//! +//! GeoJSON (RFC 7946) fixes the reference system to WGS 84. A geometry in any +//! other SRID carries the 2008 GeoJSON `crs` member: +//! `"crs":{"type":"name","properties":{"name":"EPSG:"}}`, or +//! `NODEDB:` for a NodeDB or user-defined code. A WGS 84 geometry +//! writes no `crs` member, so its text stays plain RFC 7946. +//! Input also accepts the legacy linked form GDAL and old PostGIS write: +//! `"crs":{"type":"EPSG","properties":{"code":}}`. +//! +//! Input paths resolve a `crs` member through a [`SridResolver`], so an +//! undefined SRID is refused. Stored geometry decodes with a syntax check +//! only, so a row written under a CRS that is later dropped still decodes. + +use nodedb_srid::{SridResolver, UnknownSrid, code_from_name}; +use serde::{Deserialize, Serialize}; + +use super::super::srid::Srid; +use super::geojson_read::GeoJsonError; + +/// A GeoJSON 2008 `crs` member: the named form, or the legacy `EPSG` form. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct CrsMember { + #[serde(rename = "type")] + pub kind: String, + pub properties: CrsProperties, +} + +/// The `properties` of a `crs` member. The named form sets `name`, the +/// legacy `EPSG` form sets `code`. +#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)] +pub struct CrsProperties { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub name: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub code: Option, +} + +/// The `code` of a legacy `EPSG` member: a number, or a decimal string. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(untagged)] +pub enum CrsCode { + Number(i64), + Text(String), +} + +/// The `type` value of a named `crs` member. +const CRS_KIND_NAME: &str = "name"; + +/// The `type` value of a legacy `EPSG` `crs` member. +const CRS_KIND_EPSG: &str = "EPSG"; + +/// What a `crs` member refers to. +enum CrsRef<'a> { + Name(&'a str), + Code(i64), +} + +impl CrsMember { + pub(super) fn for_srid(srid: Srid) -> Self { + Self { + kind: CRS_KIND_NAME.to_string(), + properties: CrsProperties { + name: Some(srid.crs_name()), + code: None, + }, + } + } + + /// The name or code the member refers to. + fn reference(&self) -> Result, UnknownSrid> { + let invalid = |reason: &str| UnknownSrid::InvalidCrsMember { + reason: reason.into(), + }; + if self.kind.eq_ignore_ascii_case(CRS_KIND_NAME) { + let name = self + .properties + .name + .as_deref() + .ok_or_else(|| invalid("type 'name' has no properties.name"))?; + return Ok(CrsRef::Name(name.trim())); + } + if self.kind.eq_ignore_ascii_case(CRS_KIND_EPSG) { + let code = match &self.properties.code { + Some(CrsCode::Number(n)) => *n, + Some(CrsCode::Text(t)) => t + .trim() + .parse::() + .map_err(|_| invalid("type 'EPSG' has a non-integer properties.code"))?, + None => return Err(invalid("type 'EPSG' has no properties.code")), + }; + return Ok(CrsRef::Code(code)); + } + Err(UnknownSrid::InvalidCrsMember { + reason: format!("type '{}'", self.kind).into(), + }) + } +} + +/// `code` as an SRID, checking the range only. +fn srid_in_range(code: i64) -> Result { + u32::try_from(code) + .ok() + .and_then(|c| Srid::new(c).ok()) + .ok_or(UnknownSrid::OutOfRange { code }) +} + +/// Resolve an optional `crs` member to an SRID `ctx` defines. No member +/// means WGS 84. +/// +/// Names take every form [`SridResolver::resolve_name`] accepts: `EPSG:`, +/// OGC URNs and URLs, the OGC CRS84 names and `SR-ORG:6842`. A legacy +/// `EPSG` member resolves its code. +pub fn srid_from_crs( + crs: Option<&CrsMember>, + ctx: &dyn SridResolver, +) -> Result { + let Some(member) = crs else { + return Ok(Srid::WGS84); + }; + let srid = match member.reference()? { + CrsRef::Name(name) => ctx.resolve_name(name)?, + CrsRef::Code(code) => { + let code = u32::try_from(code).map_err(|_| UnknownSrid::OutOfRange { code })?; + ctx.resolve_u32(code)? + } + }; + Ok(srid) +} + +/// The SRID an optional `crs` member names, checking the member form and +/// the code range only. +pub(super) fn stored_srid_from_crs(crs: Option<&CrsMember>) -> Result { + let Some(member) = crs else { + return Ok(Srid::WGS84); + }; + let code = match member.reference()? { + CrsRef::Name(name) => { + code_from_name(name).ok_or_else(|| UnknownSrid::UnknownName { name: name.into() })? + } + CrsRef::Code(code) => code, + }; + Ok(srid_in_range(code)?) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn srid(code: u32) -> Srid { + Srid::new(code).unwrap() + } + + fn crs(name: &str) -> CrsMember { + CrsMember { + kind: "name".into(), + properties: CrsProperties { + name: Some(name.into()), + code: None, + }, + } + } + + fn ctx() -> nodedb_crs::CrsContext<'static> { + nodedb_crs::CrsContext::builtin_only() + } + + #[test] + fn every_accepted_crs_spelling_resolves() { + let ctx = ctx(); + assert_eq!(srid_from_crs(Some(&crs("EPSG:3375")), &ctx), Ok(srid(3375))); + assert_eq!( + srid_from_crs(Some(&crs("urn:ogc:def:crs:EPSG::3376")), &ctx), + Ok(srid(3376)) + ); + assert_eq!( + srid_from_crs(Some(&crs("urn:ogc:def:crs:OGC:1.3:CRS84")), &ctx), + Ok(Srid::WGS84) + ); + assert_eq!( + srid_from_crs(Some(&crs("epsg:32647")), &ctx), + Ok(srid(32647)) + ); + assert_eq!( + srid_from_crs(Some(&crs("SR-ORG:6842")), &ctx), + Ok(Srid::MODIS_SINUSOIDAL) + ); + } + + #[test] + fn undefined_and_unknown_crs_names_fail() { + let ctx = ctx(); + assert_eq!( + srid_from_crs(Some(&crs("EPSG:999")), &ctx), + Err(GeoJsonError::Srid(UnknownSrid::Undefined { code: 999 })) + ); + assert!(matches!( + srid_from_crs(Some(&crs("urn:x:y")), &ctx), + Err(GeoJsonError::Srid(UnknownSrid::UnknownName { .. })) + )); + let linked = CrsMember { + kind: "link".into(), + properties: CrsProperties { + name: Some("x".into()), + code: None, + }, + }; + assert!(matches!( + srid_from_crs(Some(&linked), &ctx), + Err(GeoJsonError::Srid(UnknownSrid::InvalidCrsMember { .. })) + )); + } +} diff --git a/nodedb-types/src/geometry/tagged/geojson_read.rs b/nodedb-types/src/geometry/tagged/geojson_read.rs new file mode 100644 index 000000000..ebca6e36a --- /dev/null +++ b/nodedb-types/src/geometry/tagged/geojson_read.rs @@ -0,0 +1,388 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! GeoJSON geometry reader. +//! +//! A position holds 2 or 3 numbers: x, y and an optional Z. GeoJSON has no +//! M. More than 3 numbers, or 2 and 3 mixed in one geometry, is an error. +//! An empty `coordinates` array is the EMPTY form of its kind. + +use nodedb_srid::{SridResolver, UnknownSrid}; +use serde_json::{Map, Value as Json}; + +use super::super::ordinates::{OrdinateError, Ordinates, OrdinatesBuilder}; +use super::super::shape::{Geometry, GeometryKind}; +use super::crs_member::{CrsMember, srid_from_crs, stored_srid_from_crs}; +use super::srid_geometry::SridGeometry; + +/// Nesting limit for `GeometryCollection` members, the same bound the WKB +/// reader keeps. +const MAX_GEOJSON_DEPTH: usize = 64; + +/// Why a GeoJSON text does not yield a [`SridGeometry`]. +#[derive(Debug, Clone, PartialEq, thiserror::Error)] +pub enum GeoJsonError { + /// The text is not JSON, or the object has no geometry `type`. + #[error("not a GeoJSON geometry")] + NotGeometry, + /// The object names a geometry type but its members do not fit it. + #[error("malformed GeoJSON geometry: {reason}")] + Malformed { reason: Box }, + /// Positions mix 2 and 3 numbers. + #[error("malformed GeoJSON geometry: {0}")] + Ordinates(#[from] OrdinateError), + /// The `crs` member is not a supported form, or does not resolve to a + /// defined SRID. + #[error(transparent)] + Srid(#[from] UnknownSrid), +} + +fn malformed(reason: impl Into>) -> GeoJsonError { + GeoJsonError::Malformed { + reason: reason.into(), + } +} + +/// The parts of a GeoJSON geometry object before its `crs` resolves. +#[derive(Debug, Clone, PartialEq)] +pub struct GeoJsonParts { + pub geometry: Geometry, + pub ordinates: Ordinates, + pub crs: Option, +} + +/// Read a GeoJSON geometry object. +pub fn read_geojson_value(value: &Json) -> Result { + let object = value.as_object().ok_or(GeoJsonError::NotGeometry)?; + let kind = geometry_kind(object)?; + let crs = match object.get("crs") { + None | Some(Json::Null) => None, + Some(member) => Some(serde_json::from_value::(member.clone()).map_err( + |_| UnknownSrid::InvalidCrsMember { + reason: "not an object with type and properties".into(), + }, + )?), + }; + let mut ordinates = OrdinatesBuilder::default(); + let geometry = read_geometry(kind, object, &mut ordinates, MAX_GEOJSON_DEPTH)?; + Ok(GeoJsonParts { + geometry, + ordinates: ordinates.finish(), + crs, + }) +} + +fn geometry_kind(object: &Map) -> Result { + let name = object + .get("type") + .and_then(Json::as_str) + .ok_or(GeoJsonError::NotGeometry)?; + GeometryKind::from_geojson_name(name).ok_or(GeoJsonError::NotGeometry) +} + +fn read_geometry( + kind: GeometryKind, + object: &Map, + ordinates: &mut OrdinatesBuilder, + depth: usize, +) -> Result { + if depth == 0 { + return Err(malformed("GeometryCollection nesting exceeds 64 levels")); + } + if kind == GeometryKind::GeometryCollection { + let members = object + .get("geometries") + .and_then(Json::as_array) + .ok_or_else(|| malformed("GeometryCollection has no geometries array"))?; + let geometries = members + .iter() + .map(|member| { + let member = member + .as_object() + .ok_or_else(|| malformed("GeometryCollection member is not an object"))?; + let member_kind = geometry_kind(member) + .map_err(|_| malformed("GeometryCollection member has no geometry type"))?; + read_geometry(member_kind, member, ordinates, depth - 1) + }) + .collect::>()?; + return Ok(Geometry::GeometryCollection { geometries }); + } + let coords = object + .get("coordinates") + .ok_or_else(|| malformed(format!("{} has no coordinates", kind.name())))?; + Ok(match kind { + GeometryKind::Point => { + let items = array(coords, "Point coordinates")?; + if items.is_empty() { + Geometry::empty_point() + } else { + Geometry::Point { + coordinates: position(coords, ordinates)?, + } + } + } + GeometryKind::LineString => Geometry::LineString { + coordinates: path(coords, ordinates)?, + }, + GeometryKind::MultiPoint => Geometry::MultiPoint { + coordinates: path(coords, ordinates)?, + }, + GeometryKind::Polygon => Geometry::Polygon { + coordinates: paths(coords, ordinates)?, + }, + GeometryKind::MultiLineString => Geometry::MultiLineString { + coordinates: paths(coords, ordinates)?, + }, + GeometryKind::MultiPolygon => Geometry::MultiPolygon { + coordinates: array(coords, "MultiPolygon coordinates")? + .iter() + .map(|polygon| paths(polygon, ordinates)) + .collect::>()?, + }, + GeometryKind::GeometryCollection => { + return Err(malformed("GeometryCollection takes geometries")); + } + }) +} + +fn array<'a>(value: &'a Json, what: &str) -> Result<&'a Vec, GeoJsonError> { + value + .as_array() + .ok_or_else(|| malformed(format!("{what} is not an array"))) +} + +fn position(value: &Json, ordinates: &mut OrdinatesBuilder) -> Result<[f64; 2], GeoJsonError> { + let items = array(value, "position")?; + if !(2..=3).contains(&items.len()) { + return Err(malformed(format!( + "position has {} numbers; GeoJSON takes 2 or 3", + items.len() + ))); + } + let mut numbers = [0.0f64; 3]; + for (slot, item) in numbers.iter_mut().zip(items) { + *slot = item + .as_f64() + .filter(|n| n.is_finite()) + .ok_or_else(|| malformed("position holds a value that is not a finite number"))?; + } + let z = (items.len() == 3).then_some(numbers[2]); + ordinates.push(z, None)?; + Ok([numbers[0], numbers[1]]) +} + +fn path(value: &Json, ordinates: &mut OrdinatesBuilder) -> Result, GeoJsonError> { + array(value, "coordinates")? + .iter() + .map(|p| position(p, ordinates)) + .collect() +} + +fn paths( + value: &Json, + ordinates: &mut OrdinatesBuilder, +) -> Result>, GeoJsonError> { + array(value, "coordinates")? + .iter() + .map(|p| path(p, ordinates)) + .collect() +} + +/// GeoJSON text as a JSON tree. Uses `sonic_rs` per workspace policy. +fn parse_tree(text: &str) -> Result { + sonic_rs::from_str::(text).map_err(|_| GeoJsonError::NotGeometry) +} + +/// Parse GeoJSON input text into a [`SridGeometry`]. The `crs` member must +/// resolve in `ctx`. +pub fn parse_geojson(text: &str, ctx: &dyn SridResolver) -> Result { + let parts = read_geojson_value(&parse_tree(text)?)?; + let srid = srid_from_crs(parts.crs.as_ref(), ctx)?; + Ok(SridGeometry::with_ordinates( + srid, + parts.geometry, + parts.ordinates, + )?) +} + +/// [`parse_geojson`] over raw UTF-8 bytes. +pub fn parse_geojson_slice( + bytes: &[u8], + ctx: &dyn SridResolver, +) -> Result { + let text = std::str::from_utf8(bytes).map_err(|_| GeoJsonError::NotGeometry)?; + parse_geojson(text, ctx) +} + +/// Decode stored GeoJSON bytes. The `crs` name is checked for form and +/// code range only: whether the SRID is defined is a question for the +/// reader that uses the coordinates. +pub fn decode_stored_geojson(bytes: &[u8]) -> Result { + let text = std::str::from_utf8(bytes).map_err(|_| GeoJsonError::NotGeometry)?; + decode_stored_tree(&parse_tree(text)?) +} + +/// A stored GeoJSON tree as a [`SridGeometry`], `crs` checked for syntax +/// only. +pub(super) fn decode_stored_tree(tree: &Json) -> Result { + let parts = read_geojson_value(tree)?; + let srid = stored_srid_from_crs(parts.crs.as_ref())?; + Ok(SridGeometry::with_ordinates( + srid, + parts.geometry, + parts.ordinates, + )?) +} + +#[cfg(test)] +mod tests { + use super::super::super::ordinates::Dimension; + use super::super::super::srid::Srid; + use super::*; + + fn ctx() -> nodedb_crs::CrsContext<'static> { + nodedb_crs::CrsContext::builtin_only() + } + + fn srid(code: u32) -> Srid { + Srid::new(code).unwrap() + } + + #[test] + fn missing_crs_member_is_wgs84() { + let g = parse_geojson(r#"{"type":"Point","coordinates":[1.0,2.0]}"#, &ctx()).unwrap(); + assert_eq!(g.srid, Srid::WGS84); + assert_eq!(g.dimension(), Dimension::Xy); + } + + #[test] + fn three_numbers_are_z() { + let g = parse_geojson( + r#"{"type":"LineString","coordinates":[[1,2,3],[4,5,6]]}"#, + &ctx(), + ) + .unwrap(); + assert_eq!(g.ordinates().z(), Some(&[3.0, 6.0][..])); + assert_eq!(g.ordinates().m(), None); + } + + #[test] + fn four_numbers_and_mixed_arity_fail() { + assert!(matches!( + parse_geojson(r#"{"type":"Point","coordinates":[1,2,3,4]}"#, &ctx()), + Err(GeoJsonError::Malformed { .. }) + )); + assert!(matches!( + parse_geojson( + r#"{"type":"LineString","coordinates":[[1,2,3],[4,5]]}"#, + &ctx() + ), + Err(GeoJsonError::Ordinates( + OrdinateError::MixedDimension { .. } + )) + )); + assert!(matches!( + parse_geojson(r#"{"type":"Point","coordinates":[1]}"#, &ctx()), + Err(GeoJsonError::Malformed { .. }) + )); + } + + #[test] + fn collection_members_share_one_dimension() { + let mixed = r#"{"type":"GeometryCollection","geometries":[ + {"type":"Point","coordinates":[1,2,3]}, + {"type":"Point","coordinates":[1,2]}]}"#; + assert!(matches!( + parse_geojson(mixed, &ctx()), + Err(GeoJsonError::Ordinates(_)) + )); + } + + #[test] + fn empty_coordinates_are_empty_geometries() { + for (text, kind) in [ + (r#"{"type":"Point","coordinates":[]}"#, GeometryKind::Point), + ( + r#"{"type":"LineString","coordinates":[]}"#, + GeometryKind::LineString, + ), + ( + r#"{"type":"MultiPolygon","coordinates":[]}"#, + GeometryKind::MultiPolygon, + ), + ( + r#"{"type":"GeometryCollection","geometries":[]}"#, + GeometryKind::GeometryCollection, + ), + ] { + let g = parse_geojson(text, &ctx()).unwrap(); + assert!(g.is_empty(), "{text}"); + assert_eq!(g.geometry().kind(), kind); + } + } + + #[test] + fn non_geometry_json_is_not_geometry() { + assert_eq!( + parse_geojson(r#"{"a":1}"#, &ctx()), + Err(GeoJsonError::NotGeometry) + ); + assert_eq!( + parse_geojson(r#"{"type":"Feature"}"#, &ctx()), + Err(GeoJsonError::NotGeometry) + ); + assert_eq!( + parse_geojson("nope", &ctx()), + Err(GeoJsonError::NotGeometry) + ); + } + + #[test] + fn non_finite_and_non_numeric_positions_fail() { + assert!(matches!( + parse_geojson(r#"{"type":"Point","coordinates":["a",2]}"#, &ctx()), + Err(GeoJsonError::Malformed { .. }) + )); + } + + /// The legacy `EPSG` member GDAL and old PostGIS write resolves its + /// code, as a number or a decimal string. + #[test] + fn legacy_epsg_member_resolves() { + let ctx = ctx(); + let numeric = r#"{"type":"Point","coordinates":[1.0,2.0],"crs":{"type":"EPSG","properties":{"code":32648}}}"#; + assert_eq!( + parse_geojson(numeric, &ctx).map(|g| g.srid), + Ok(srid(32648)) + ); + let text = r#"{"type":"Point","coordinates":[1.0,2.0],"crs":{"type":"EPSG","properties":{"code":"3375"}}}"#; + assert_eq!(parse_geojson(text, &ctx).map(|g| g.srid), Ok(srid(3375))); + assert_eq!( + decode_stored_geojson(numeric.as_bytes()).map(|g| g.srid), + Ok(srid(32648)) + ); + let undefined = r#"{"type":"Point","coordinates":[1.0,2.0],"crs":{"type":"EPSG","properties":{"code":999}}}"#; + assert_eq!( + parse_geojson(undefined, &ctx), + Err(GeoJsonError::Srid(UnknownSrid::Undefined { code: 999 })) + ); + let no_code = + r#"{"type":"Point","coordinates":[1.0,2.0],"crs":{"type":"EPSG","properties":{}}}"#; + assert!(matches!( + parse_geojson(no_code, &ctx), + Err(GeoJsonError::Srid(UnknownSrid::InvalidCrsMember { .. })) + )); + } + + /// A stored decode checks syntax only, so an undefined code decodes. + #[test] + fn stored_decode_checks_syntax_only() { + let text = r#"{"type":"Point","coordinates":[1.0,2.0],"crs":{"type":"name","properties":{"name":"EPSG:999"}}}"#; + let g = decode_stored_geojson(text.as_bytes()).unwrap(); + assert_eq!(g.srid, srid(999)); + let zero = r#"{"type":"Point","coordinates":[1.0,2.0],"crs":{"type":"name","properties":{"name":"EPSG:0"}}}"#; + assert_eq!( + decode_stored_geojson(zero.as_bytes()), + Err(GeoJsonError::Srid(UnknownSrid::OutOfRange { code: 0 })) + ); + } +} diff --git a/nodedb-types/src/geometry/tagged/geojson_write.rs b/nodedb-types/src/geometry/tagged/geojson_write.rs new file mode 100644 index 000000000..954d969b7 --- /dev/null +++ b/nodedb-types/src/geometry/tagged/geojson_write.rs @@ -0,0 +1,164 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! GeoJSON writer: serde `Serialize` / `Deserialize` for [`SridGeometry`]. +//! +//! The writer emits Z as the third number of each position and drops M, +//! as PostGIS `ST_AsGeoJSON` does. An EMPTY geometry writes an empty +//! `coordinates` (or `geometries`) array. + +use serde::de::Error as _; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use serde_json::{Map, Value as Json}; + +use super::super::shape::Geometry; +use super::super::srid::Srid; +use super::crs_member::CrsMember; +use super::geojson_read::decode_stored_tree; +use super::srid_geometry::SridGeometry; + +/// The GeoJSON object of `g`, `crs` member included for a non-WGS 84 SRID. +pub fn geojson_tree(g: &SridGeometry) -> Json { + let mut z = g.ordinates().z().map(|z| z.iter().copied()); + let mut object = geometry_object(g.geometry(), &mut z); + if g.srid != Srid::WGS84 + && let Ok(member) = serde_json::to_value(CrsMember::for_srid(g.srid)) + { + object.insert("crs".into(), member); + } + Json::Object(object) +} + +/// Z values in traversal order, consumed as positions are written. +type ZIter<'a> = Option>>; + +fn geometry_object(geometry: &Geometry, z: &mut ZIter<'_>) -> Map { + let mut object = Map::new(); + object.insert("type".into(), Json::from(geometry.geometry_type())); + let coordinates = match geometry { + Geometry::Point { .. } if geometry.is_empty() => Json::Array(Vec::new()), + Geometry::Point { coordinates } => position(*coordinates, z), + Geometry::LineString { coordinates } | Geometry::MultiPoint { coordinates } => { + path(coordinates, z) + } + Geometry::Polygon { coordinates } | Geometry::MultiLineString { coordinates } => { + paths(coordinates, z) + } + Geometry::MultiPolygon { coordinates } => { + Json::Array(coordinates.iter().map(|p| paths(p, z)).collect()) + } + Geometry::GeometryCollection { geometries } => { + let members = geometries + .iter() + .map(|member| Json::Object(geometry_object(member, z))) + .collect(); + object.insert("geometries".into(), Json::Array(members)); + return object; + } + }; + object.insert("coordinates".into(), coordinates); + object +} + +fn position(c: [f64; 2], z: &mut ZIter<'_>) -> Json { + let mut numbers = vec![Json::from(c[0]), Json::from(c[1])]; + if let Some(value) = z.as_mut().and_then(Iterator::next) { + numbers.push(Json::from(value)); + } + Json::Array(numbers) +} + +fn path(coords: &[[f64; 2]], z: &mut ZIter<'_>) -> Json { + Json::Array(coords.iter().map(|c| position(*c, z)).collect()) +} + +fn paths(rings: &[Vec<[f64; 2]>], z: &mut ZIter<'_>) -> Json { + Json::Array(rings.iter().map(|ring| path(ring, z)).collect()) +} + +impl Serialize for SridGeometry { + fn serialize(&self, serializer: S) -> Result { + geojson_tree(self).serialize(serializer) + } +} + +/// Deserialization is a stored-data decode: the `crs` name is checked for +/// form and code range only. +impl<'de> Deserialize<'de> for SridGeometry { + fn deserialize>(deserializer: D) -> Result { + let tree = Json::deserialize(deserializer)?; + decode_stored_tree(&tree).map_err(D::Error::custom) + } +} + +#[cfg(test)] +mod tests { + use super::super::super::ordinates::Ordinates; + use super::super::geojson_read::parse_geojson; + use super::*; + + fn ctx() -> nodedb_crs::CrsContext<'static> { + nodedb_crs::CrsContext::builtin_only() + } + + fn srid(code: u32) -> Srid { + Srid::new(code).unwrap() + } + + #[test] + fn wgs84_writes_no_crs_member() { + let g = SridGeometry::wgs84(Geometry::point(1.0, 2.0)); + let json = sonic_rs::to_string(&g).unwrap(); + assert!(!json.contains("crs"), "{json}"); + assert!(json.contains("\"type\":\"Point\""), "{json}"); + } + + #[test] + fn projected_round_trip_keeps_srid() { + let g = SridGeometry::new(srid(32648), Geometry::point(134445.5, 349624.7)); + let json = sonic_rs::to_string(&g).unwrap(); + assert!(json.contains("\"name\":\"EPSG:32648\""), "{json}"); + assert_eq!(parse_geojson(&json, &ctx()), Ok(g.clone())); + let derived: SridGeometry = sonic_rs::from_str(&json).unwrap(); + assert_eq!(derived, g); + } + + #[test] + fn writer_emits_z_and_drops_m() { + let zm = SridGeometry::with_ordinates( + Srid::WGS84, + Geometry::line_string(vec![[0.0, 0.0], [1.0, 1.0]]), + Ordinates::new(Some(vec![5.0, 6.0]), Some(vec![7.0, 8.0])), + ) + .unwrap(); + let json = sonic_rs::to_string(&zm).unwrap(); + assert!(json.contains("[1.0,1.0,6.0]"), "{json}"); + let back = parse_geojson(&json, &ctx()).unwrap(); + assert_eq!(back.ordinates().z(), Some(&[5.0, 6.0][..])); + assert_eq!(back.ordinates().m(), None); + } + + #[test] + fn empty_point_round_trips() { + let g = SridGeometry::wgs84(Geometry::empty_point()); + let json = sonic_rs::to_string(&g).unwrap(); + assert!(json.contains("\"coordinates\":[]"), "{json}"); + assert!(parse_geojson(&json, &ctx()).unwrap().is_empty()); + } + + #[test] + fn collection_z_follows_traversal_order() { + let g = SridGeometry::with_ordinates( + Srid::WGS84, + Geometry::GeometryCollection { + geometries: vec![ + Geometry::point(1.0, 1.0), + Geometry::line_string(vec![[2.0, 2.0], [3.0, 3.0]]), + ], + }, + Ordinates::new(Some(vec![10.0, 20.0, 30.0]), None), + ) + .unwrap(); + let json = sonic_rs::to_string(&g).unwrap(); + assert_eq!(parse_geojson(&json, &ctx()), Ok(g)); + } +} diff --git a/nodedb-types/src/geometry/tagged/mod.rs b/nodedb-types/src/geometry/tagged/mod.rs new file mode 100644 index 000000000..d11e2e00b --- /dev/null +++ b/nodedb-types/src/geometry/tagged/mod.rs @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: Apache-2.0 + +pub mod crs_member; +pub mod geojson_read; +pub mod geojson_write; +pub mod msgpack; +pub mod srid_geometry; + +pub use crs_member::{CrsCode, CrsMember, CrsProperties, srid_from_crs}; +pub use geojson_read::{ + GeoJsonError, GeoJsonParts, decode_stored_geojson, parse_geojson, parse_geojson_slice, + read_geojson_value, +}; +pub use geojson_write::geojson_tree; +pub use srid_geometry::{SridGeometry, geojson_object, stored_geojson_object}; diff --git a/nodedb-types/src/geometry/tagged/msgpack.rs b/nodedb-types/src/geometry/tagged/msgpack.rs new file mode 100644 index 000000000..b820619c0 --- /dev/null +++ b/nodedb-types/src/geometry/tagged/msgpack.rs @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! MessagePack form of [`SridGeometry`]: `[srid, geometry, z | nil, m | nil]`. +//! +//! `z` and `m` are arrays of one float per XY coordinate in traversal +//! order, or nil when the geometry lacks that ordinate. The SRID is +//! checked for syntax only. + +use super::super::ordinates::Ordinates; +use super::super::shape::Geometry; +use super::super::srid::Srid; +use super::srid_geometry::SridGeometry; +use zerompk::{FromMessagePack, ToMessagePack}; + +/// Number of fields after the array header. +pub const SRID_GEOMETRY_FIELDS: usize = 4; + +/// Write the four fields, without the array header. `Value` writes them +/// after its own type tag. +pub fn write_fields(g: &SridGeometry, writer: &mut W) -> zerompk::Result<()> { + g.srid.write(writer)?; + g.geometry().write(writer)?; + write_ordinate(g.ordinates().z(), writer)?; + write_ordinate(g.ordinates().m(), writer) +} + +fn write_ordinate( + values: Option<&[f64]>, + writer: &mut W, +) -> zerompk::Result<()> { + match values { + Some(values) => { + writer.write_array_len(values.len())?; + values.iter().try_for_each(|v| writer.write_f64(*v)) + } + None => writer.write_nil(), + } +} + +/// Read the four fields, without the array header. Z or M arrays that do +/// not hold one finite value per coordinate fail the decode. +pub fn read_fields<'a, R: zerompk::Read<'a>>(reader: &mut R) -> zerompk::Result { + let srid = Srid::read(reader)?; + let geometry = Geometry::read(reader)?; + let z = Option::>::read(reader)?; + let m = Option::>::read(reader)?; + let expected = geometry.coord_count(); + let actual = z + .as_ref() + .map(Vec::len) + .filter(|len| *len != expected) + .or_else(|| m.as_ref().map(Vec::len).filter(|len| *len != expected)); + SridGeometry::with_ordinates(srid, geometry, Ordinates::new(z, m)).map_err(|_| { + zerompk::Error::ArrayLengthMismatch { + expected, + actual: actual.unwrap_or(expected), + } + }) +} + +impl zerompk::ToMessagePack for SridGeometry { + fn write(&self, writer: &mut W) -> zerompk::Result<()> { + writer.write_array_len(SRID_GEOMETRY_FIELDS)?; + write_fields(self, writer) + } +} + +impl<'a> zerompk::FromMessagePack<'a> for SridGeometry { + fn read>(reader: &mut R) -> zerompk::Result { + let len = reader.read_array_len()?; + if len != SRID_GEOMETRY_FIELDS { + return Err(zerompk::Error::ArrayLengthMismatch { + expected: SRID_GEOMETRY_FIELDS, + actual: len, + }); + } + read_fields(reader) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn round_trip_keeps_srid_z_and_m() { + let g = SridGeometry::with_ordinates( + Srid::new(3376).unwrap(), + Geometry::line_string(vec![[1.0, 2.0], [3.0, 4.0]]), + Ordinates::new(Some(vec![5.0, 6.0]), Some(vec![7.0, 8.0])), + ) + .unwrap(); + let bytes = zerompk::to_msgpack_vec(&g).unwrap(); + let back: SridGeometry = zerompk::from_msgpack(&bytes).unwrap(); + assert_eq!(back, g); + let xy = SridGeometry::new(Srid::WGS84, Geometry::point(1.0, 2.0)); + let bytes = zerompk::to_msgpack_vec(&xy).unwrap(); + assert_eq!(zerompk::from_msgpack::(&bytes).unwrap(), xy); + } + + #[test] + fn empty_point_round_trips() { + let g = SridGeometry::wgs84(Geometry::empty_point()); + let bytes = zerompk::to_msgpack_vec(&g).unwrap(); + let back: SridGeometry = zerompk::from_msgpack(&bytes).unwrap(); + assert!(back.is_empty()); + } + + #[test] + fn wrong_z_length_fails_decode() { + let g = SridGeometry::new(Srid::WGS84, Geometry::point(1.0, 2.0)); + let mut encoded = zerompk::to_msgpack_vec(&g).unwrap(); + // Replace the trailing `nil, nil` with `[1.0, 2.0], nil`. + encoded.truncate(encoded.len() - 2); + encoded.push(0x92); + encoded.push(0xcb); + encoded.extend_from_slice(&1.0f64.to_be_bytes()); + encoded.push(0xcb); + encoded.extend_from_slice(&2.0f64.to_be_bytes()); + encoded.push(0xc0); + assert!(zerompk::from_msgpack::(&encoded).is_err()); + } +} diff --git a/nodedb-types/src/geometry/tagged/srid_geometry.rs b/nodedb-types/src/geometry/tagged/srid_geometry.rs new file mode 100644 index 000000000..9a265b2a8 --- /dev/null +++ b/nodedb-types/src/geometry/tagged/srid_geometry.rs @@ -0,0 +1,408 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! A geometry tagged with its spatial reference system and its Z / M +//! ordinates. + +use nodedb_srid::{SridResolver, UnknownSrid}; + +use super::super::ordinates::{Dimension, OrdinateError, Ordinates, Position}; +use super::super::shape::Geometry; +use super::super::srid::Srid; +use super::crs_member::srid_from_crs; +use super::geojson_read::{GeoJsonError, decode_stored_tree, parse_geojson, read_geojson_value}; +use crate::bbox::{BboxSpace, BoundingBox, geometry_bbox_in}; + +/// A geometry, the SRID its coordinates are in, and its Z / M ordinates. +/// +/// The shape and the ordinates are private so they always agree: Z and M +/// hold one finite value per XY coordinate, in traversal order. +#[derive(Debug, Clone, PartialEq)] +pub struct SridGeometry { + pub srid: Srid, + geometry: Geometry, + ordinates: Ordinates, +} + +impl SridGeometry { + /// An XY geometry. + pub fn new(srid: Srid, geometry: Geometry) -> Self { + Self { + srid, + geometry, + ordinates: Ordinates::xy(), + } + } + + /// A geometry with Z and / or M. Fails when an array does not hold one + /// finite value per coordinate. + pub fn with_ordinates( + srid: Srid, + geometry: Geometry, + ordinates: Ordinates, + ) -> Result { + ordinates.check(geometry.coord_count())?; + Ok(Self { + srid, + geometry, + ordinates, + }) + } + + /// An XY geometry in WGS 84, the SRID of untagged GeoJSON. + pub fn wgs84(geometry: Geometry) -> Self { + Self::new(Srid::WGS84, geometry) + } + + pub fn geometry(&self) -> &Geometry { + &self.geometry + } + + pub fn ordinates(&self) -> &Ordinates { + &self.ordinates + } + + pub fn dimension(&self) -> Dimension { + self.ordinates.dimension() + } + + pub fn has_z(&self) -> bool { + self.dimension().has_z() + } + + pub fn has_m(&self) -> bool { + self.dimension().has_m() + } + + /// Whether the geometry has no coordinates (OGC EMPTY). + pub fn is_empty(&self) -> bool { + self.geometry.is_empty() + } + + pub fn into_parts(self) -> (Srid, Geometry, Ordinates) { + (self.srid, self.geometry, self.ordinates) + } + + /// The same geometry under another SRID label. + pub fn with_srid(mut self, srid: Srid) -> Self { + self.srid = srid; + self + } + + /// The same SRID around a derived XY shape. A derived shape (buffer, + /// hull, envelope) has no Z or M of its own. + pub fn with_geometry(&self, geometry: Geometry) -> Self { + Self::new(self.srid, geometry) + } + + /// The same shape with Z and M dropped (`ST_Force2D`). + pub fn force_2d(&self) -> Self { + Self::new(self.srid, self.geometry.clone()) + } + + /// The same shape in `dimension`. An ordinate the geometry lacks is + /// filled with `fill_z` or `fill_m`. An ordinate `dimension` lacks is + /// dropped. Fails when a fill value is not finite. + pub fn force_dimension( + &self, + dimension: Dimension, + fill_z: f64, + fill_m: f64, + ) -> Result { + let n = self.geometry.coord_count(); + let pick = |want: bool, have: Option<&[f64]>, fill: f64| { + want.then(|| have.map_or_else(|| vec![fill; n], <[f64]>::to_vec)) + }; + let ordinates = Ordinates::new( + pick(dimension.has_z(), self.ordinates.z(), fill_z), + pick(dimension.has_m(), self.ordinates.m(), fill_m), + ); + Self::with_ordinates(self.srid, self.geometry.clone(), ordinates) + } + + /// Every coordinate with its Z and M, in traversal order. + pub fn positions(&self) -> impl Iterator + '_ { + self.geometry.coords().enumerate().map(|(index, [x, y])| { + let (z, m) = self.ordinates.at(index); + Position { x, y, z, m } + }) + } + + /// Map every position through `f`, keeping the shape, the SRID and the + /// dimension. A Z or M `f` returns as `None` keeps the input value. + /// + /// Stops at the first position `f` rejects and returns its error. + pub fn try_map_positions( + &self, + mut f: impl FnMut(Position) -> Result, + ) -> Result { + let dimension = self.dimension(); + let n = self.geometry.coord_count(); + let mut z_out = dimension.has_z().then(|| Vec::with_capacity(n)); + let mut m_out = dimension.has_m().then(|| Vec::with_capacity(n)); + let mut index = 0usize; + let ordinates = &self.ordinates; + let geometry = self.geometry.try_map_coords(&mut |[x, y]| { + let (z, m) = ordinates.at(index); + index += 1; + let p = f(Position { x, y, z, m })?; + if let (Some(out), Some(value)) = (z_out.as_mut(), p.z.or(z)) { + out.push(value); + } + if let (Some(out), Some(value)) = (m_out.as_mut(), p.m.or(m)) { + out.push(value); + } + Ok([p.x, p.y]) + })?; + Ok(Self { + srid: self.srid, + geometry, + ordinates: Ordinates::new(z_out, m_out), + }) + } + + /// Bounding box in the coordinate space of this SRID. Fails when `ctx` + /// does not define the SRID. + pub fn bbox(&self, ctx: &dyn SridResolver) -> Result { + let space = BboxSpace::of(ctx, self.srid)?; + Ok(geometry_bbox_in(&self.geometry, space)) + } + + /// Read a geometry out of a stored document field value. + /// + /// Accepts the three stored forms: a native `Value::Geometry`, a GeoJSON + /// string, and a GeoJSON object decoded from a document. A value that is + /// not a well-formed geometry gives `Ok(None)`. A geometry whose `crs` + /// member is not a supported form, or names an SRID `ctx` does not + /// define, gives `Err`. + pub fn from_value( + value: &crate::Value, + ctx: &dyn SridResolver, + ) -> Result, UnknownSrid> { + let parsed = match value { + crate::Value::Geometry(g) => { + ctx.resolve_u32(g.srid.code())?; + return Ok(Some(g.clone())); + } + crate::Value::String(s) => parse_geojson(s, ctx), + crate::Value::Object(_) => geojson_object(value, ctx), + _ => return Ok(None), + }; + match parsed { + Ok(g) => Ok(Some(g)), + Err(GeoJsonError::Srid(e)) => Err(e), + Err( + GeoJsonError::NotGeometry + | GeoJsonError::Malformed { .. } + | GeoJsonError::Ordinates(_), + ) => Ok(None), + } + } +} + +/// Parse a GeoJSON object held in a document `Value`. The `crs` member +/// must resolve in `ctx`. +pub fn geojson_object( + value: &crate::Value, + ctx: &dyn SridResolver, +) -> Result { + let json = serde_json::Value::from(value.clone()); + let parts = read_geojson_value(&json)?; + let srid = srid_from_crs(parts.crs.as_ref(), ctx)?; + Ok(SridGeometry::with_ordinates( + srid, + parts.geometry, + parts.ordinates, + )?) +} + +/// Decode a stored GeoJSON object held in a document `Value`. The `crs` +/// name is checked for form and code range only. +pub fn stored_geojson_object(value: &crate::Value) -> Result { + decode_stored_tree(&serde_json::Value::from(value.clone())) +} + +impl From for SridGeometry { + fn from(geometry: Geometry) -> Self { + Self::wgs84(geometry) + } +} + +/// A total, deterministic text form. A WGS 84 geometry renders as its +/// shape. Any other SRID gets an EWKT-style `SRID=;` prefix. Z and M +/// follow the shape when present. +impl std::fmt::Display for SridGeometry { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.srid != Srid::WGS84 { + write!(f, "SRID={};", self.srid.code())?; + } + write!(f, "{:?}", self.geometry)?; + if let Some(z) = self.ordinates.z() { + write!(f, " Z={z:?}")?; + } + if let Some(m) = self.ordinates.m() { + write!(f, " M={m:?}")?; + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::super::super::ordinates::Ordinate; + use super::*; + + fn srid(code: u32) -> Srid { + Srid::new(code).unwrap() + } + + fn ctx() -> nodedb_crs::CrsContext<'static> { + nodedb_crs::CrsContext::builtin_only() + } + + fn line_z() -> SridGeometry { + SridGeometry::with_ordinates( + Srid::WGS84, + Geometry::line_string(vec![[0.0, 0.0], [1.0, 1.0]]), + Ordinates::new(Some(vec![10.0, 20.0]), None), + ) + .unwrap() + } + + #[test] + fn with_ordinates_checks_lengths() { + let err = SridGeometry::with_ordinates( + Srid::WGS84, + Geometry::point(1.0, 2.0), + Ordinates::new(Some(vec![1.0, 2.0]), None), + ); + assert_eq!( + err, + Err(OrdinateError::LengthMismatch { + ordinate: Ordinate::Z, + expected: 1, + actual: 2 + }) + ); + } + + #[test] + fn positions_pair_xy_with_z_and_m() { + let g = line_z(); + let p: Vec = g.positions().collect(); + assert_eq!(p[1].z, Some(20.0)); + assert_eq!(p[1].m, None); + assert_eq!(g.dimension(), Dimension::Xyz); + } + + #[test] + fn derived_shape_drops_z() { + let g = line_z(); + assert_eq!( + g.with_geometry(Geometry::point(0.0, 0.0)).dimension(), + Dimension::Xy + ); + assert_eq!(g.force_2d().dimension(), Dimension::Xy); + } + + #[test] + fn force_dimension_fills_and_drops() { + let g = line_z(); + let zm = g.force_dimension(Dimension::Xyzm, 0.0, 5.0).unwrap(); + assert_eq!(zm.ordinates().z(), Some(&[10.0, 20.0][..])); + assert_eq!(zm.ordinates().m(), Some(&[5.0, 5.0][..])); + let m = g.force_dimension(Dimension::Xym, 0.0, 0.0).unwrap(); + assert_eq!(m.ordinates().z(), None); + } + + #[test] + fn try_map_positions_keeps_dimension() { + let g = line_z(); + let moved = g + .try_map_positions(|p| { + Ok::<_, ()>(Position { + x: p.x + 1.0, + z: p.z.map(|z| z * 2.0), + ..p + }) + }) + .unwrap(); + assert_eq!(moved.ordinates().z(), Some(&[20.0, 40.0][..])); + assert_eq!( + moved.geometry(), + &Geometry::line_string(vec![[1.0, 0.0], [2.0, 1.0]]) + ); + } + + #[test] + fn from_value_reads_every_stored_form() { + let ctx = ctx(); + let g = SridGeometry::new(srid(32647), Geometry::point(1.0, 2.0)); + let text = sonic_rs::to_string(&g).unwrap(); + assert_eq!( + SridGeometry::from_value(&crate::Value::Geometry(g.clone()), &ctx), + Ok(Some(g.clone())) + ); + assert_eq!( + SridGeometry::from_value(&crate::Value::String(text.clone()), &ctx), + Ok(Some(g.clone())) + ); + let json: serde_json::Value = sonic_rs::from_str(&text).unwrap(); + let obj = crate::Value::from(json); + assert_eq!(SridGeometry::from_value(&obj, &ctx), Ok(Some(g))); + assert_eq!( + SridGeometry::from_value(&crate::Value::Integer(1), &ctx), + Ok(None) + ); + } + + #[test] + fn from_value_refuses_an_undefined_srid() { + let g = SridGeometry::new(srid(999), Geometry::point(1.0, 2.0)); + assert_eq!( + SridGeometry::from_value(&crate::Value::Geometry(g), &ctx()), + Err(UnknownSrid::Undefined { code: 999 }) + ); + } + + /// A geometry with an unsupported `crs` member is an error, never + /// "not a geometry". + #[test] + fn from_value_refuses_an_unsupported_crs_member() { + let ctx = ctx(); + let linked = r#"{"type":"Point","coordinates":[1.0,2.0],"crs":{"type":"link","properties":{"href":"x"}}}"#; + assert!(matches!( + SridGeometry::from_value(&crate::Value::String(linked.into()), &ctx), + Err(UnknownSrid::InvalidCrsMember { .. }) + )); + assert_eq!( + SridGeometry::from_value(&crate::Value::String("not json".into()), &ctx), + Ok(None) + ); + } + + #[test] + fn bbox_space_follows_the_crs_kind() { + let ctx = ctx(); + let projected = SridGeometry::new(srid(32650), Geometry::point(5e5, 6e5)); + assert!(projected.bbox(&ctx).is_ok()); + let undefined = SridGeometry::new(srid(999), Geometry::point(1.0, 2.0)); + assert_eq!( + undefined.bbox(&ctx), + Err(UnknownSrid::Undefined { code: 999 }) + ); + } + + #[test] + fn display_prefixes_non_wgs84_and_appends_z() { + let g = SridGeometry::new(srid(32648), Geometry::point(1.0, 2.0)); + assert_eq!( + g.to_string(), + "SRID=32648;Point { coordinates: [1.0, 2.0] }" + ); + assert_eq!( + SridGeometry::wgs84(Geometry::point(1.0, 2.0)).to_string(), + "Point { coordinates: [1.0, 2.0] }" + ); + assert!(line_z().to_string().ends_with(" Z=[10.0, 20.0]")); + } +} diff --git a/nodedb-types/src/hlc.rs b/nodedb-types/src/hlc.rs index e500bcdc2..65f090688 100644 --- a/nodedb-types/src/hlc.rs +++ b/nodedb-types/src/hlc.rs @@ -10,9 +10,9 @@ //! Metadata DDL is not a hot path (hundreds per second at most), so the //! clock is backed by a short-lived mutex rather than atomics. +use crate::wall_clock::wall_now_ns; use std::cmp::Ordering; use std::sync::Mutex; -use std::time::{SystemTime, UNIX_EPOCH}; use serde::{Deserialize, Serialize}; @@ -188,24 +188,6 @@ impl HlcClock { } } -fn wall_now_ns() -> u64 { - SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|d| d.as_nanos() as u64) - .unwrap_or_else(|_| { - use std::sync::atomic::{AtomicBool, Ordering}; - static LOGGED: AtomicBool = AtomicBool::new(false); - if !LOGGED.swap(true, Ordering::Relaxed) { - tracing::error!( - module = module_path!(), - "system clock is before UNIX_EPOCH; using 0 (epoch) \ - — check NTP/RTC configuration" - ); - } - 0 - }) -} - #[cfg(test)] mod tests { diff --git a/nodedb-types/src/json_msgpack/geometry_ext.rs b/nodedb-types/src/json_msgpack/geometry_ext.rs new file mode 100644 index 000000000..995bbcb7d --- /dev/null +++ b/nodedb-types/src/json_msgpack/geometry_ext.rs @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Msgpack ext encoding for geometry in standard-msgpack documents. +//! +//! Ext type `3` carries the MessagePack form of a [`SridGeometry`] +//! (`[srid, geometry, z | nil, m | nil]`) as its payload, so a document +//! field keeps its SRID, Z and M. JSON renderings write it as GeoJSON. + +use crate::geometry::SridGeometry; + +/// Ext type byte of a geometry. +pub const EXT_GEOMETRY: i8 = 3; + +/// Write `g` as a geometry ext value. +pub fn write_geometry_ext( + writer: &mut W, + g: &SridGeometry, +) -> zerompk::Result<()> { + let payload = zerompk::to_msgpack_vec(g)?; + writer.write_ext(EXT_GEOMETRY, &payload) +} + +/// The geometry an ext value carries. `None` when `ext_type` is not +/// [`EXT_GEOMETRY`] or the payload does not decode. +pub fn geometry_from_ext(ext_type: i8, payload: &[u8]) -> Option { + if ext_type != EXT_GEOMETRY { + return None; + } + zerompk::from_msgpack(payload).ok() +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::geometry::{Geometry, Ordinates, Srid}; + + #[test] + fn round_trip_keeps_z_m_and_srid() { + let g = SridGeometry::with_ordinates( + Srid::new(32648).unwrap(), + Geometry::point(1.0, 2.0), + Ordinates::new(Some(vec![3.0]), Some(vec![4.0])), + ) + .unwrap(); + let payload = zerompk::to_msgpack_vec(&g).unwrap(); + assert_eq!(geometry_from_ext(EXT_GEOMETRY, &payload), Some(g)); + assert_eq!(geometry_from_ext(9, &payload), None); + } +} diff --git a/nodedb-types/src/json_msgpack/mod.rs b/nodedb-types/src/json_msgpack/mod.rs index 09d0cad79..c91af4b23 100644 --- a/nodedb-types/src/json_msgpack/mod.rs +++ b/nodedb-types/src/json_msgpack/mod.rs @@ -1,6 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 pub mod error; +pub mod geometry_ext; pub mod instant_ext; pub mod json_value; pub mod native_cell; @@ -9,6 +10,7 @@ pub mod transcoder; pub mod writer; pub use error::{MsgpackError, MsgpackResult}; +pub use geometry_ext::{EXT_GEOMETRY, geometry_from_ext, write_geometry_ext}; pub use instant_ext::{ EXT_INSTANT_NAIVE, EXT_INSTANT_UTC, INSTANT_EXT_LEN, InstantKind, instant_from_ext, read_instant, write_instant, diff --git a/nodedb-types/src/json_msgpack/reader/json.rs b/nodedb-types/src/json_msgpack/reader/json.rs index 4aea713fc..72d8e0b09 100644 --- a/nodedb-types/src/json_msgpack/reader/json.rs +++ b/nodedb-types/src/json_msgpack/reader/json.rs @@ -2,13 +2,16 @@ //! Msgpack → `serde_json::Value` reader. //! -//! Instant ext values (fixext8 type 1 / 2) become ISO 8601 strings. Every -//! other ext type becomes `null`. +//! Instant ext values (fixext8 type 1 / 2) become ISO 8601 strings. A +//! geometry ext (type 3) becomes its GeoJSON object. Every other ext type +//! becomes `null`. use super::super::error::MsgpackResult; +use super::super::geometry_ext::geometry_from_ext; use super::super::instant_ext::instant_from_ext; use super::cursor::Cursor; use crate::datetime::NdbDateTime; +use crate::geometry::geojson_tree; use crate::value::float_text::float_to_json; /// Deserialize a `serde_json::Value` from MessagePack bytes. @@ -151,30 +154,32 @@ fn read_json_value(c: &mut Cursor<'_>) -> zerompk::Result { c.take_n(5)?; Ok(serde_json::Value::Null) } - 0xD8 => { - c.take_n(17)?; - Ok(serde_json::Value::Null) - } + 0xD8 => read_json_ext(c, 16), 0xC7 => { let l = c.take()? as usize; - c.take_n(1 + l)?; - Ok(serde_json::Value::Null) + read_json_ext(c, l) } 0xC8 => { let l = c.read_u16_be()? as usize; - c.take_n(1 + l)?; - Ok(serde_json::Value::Null) + read_json_ext(c, l) } 0xC9 => { let l = c.read_u32_be()? as usize; - c.take_n(1 + l)?; - Ok(serde_json::Value::Null) + read_json_ext(c, l) } _ => Err(zerompk::Error::InvalidMarker(marker)), } } +/// An ext value of `len` payload bytes: a geometry becomes its GeoJSON +/// object, any other type `null`. +fn read_json_ext(c: &mut Cursor<'_>, len: usize) -> zerompk::Result { + let ext_type = c.take()? as i8; + let payload = c.take_n(len)?; + Ok(geometry_from_ext(ext_type, payload).map_or(serde_json::Value::Null, |g| geojson_tree(&g))) +} + fn read_json_str(c: &mut Cursor<'_>, len: usize) -> zerompk::Result { let bytes = c.take_n(len)?; let s = String::from_utf8(bytes.to_vec()).map_err(|_| zerompk::Error::InvalidMarker(0))?; diff --git a/nodedb-types/src/json_msgpack/reader/native.rs b/nodedb-types/src/json_msgpack/reader/native.rs index 70a900d21..b7cfb6777 100644 --- a/nodedb-types/src/json_msgpack/reader/native.rs +++ b/nodedb-types/src/json_msgpack/reader/native.rs @@ -7,13 +7,14 @@ //! entry point) and `NativeCell::read` (inside a zerompk derive) share it. //! //! Instant ext values (`fixext8` type 1 / 2) become `Value::DateTime` / -//! `Value::NaiveDateTime`. Every other ext marker, and a `fixext8` of any -//! other type, becomes `Value::Null`. Map keys that are not strings are +//! `Value::NaiveDateTime`. A geometry ext (type 3) becomes +//! `Value::Geometry`. Every other ext becomes `Value::Null`. Map keys that are not strings are //! rendered with `Debug`. use zerompk::{Read, SliceReader}; use super::super::error::{MsgpackError, MsgpackResult}; +use super::super::geometry_ext::geometry_from_ext; use super::super::instant_ext::instant_from_ext; use crate::Value; @@ -99,7 +100,10 @@ pub(crate) fn read_native_value<'de, R: Read<'de>>(reader: &mut R) -> zerompk::R }; Ok(match instant { Some((kind, micros)) => kind.from_micros(micros), - None => Value::Null, + None => match geometry_from_ext(ext_type, &payload) { + Some(g) => Value::Geometry(g), + None => Value::Null, + }, }) } other => Err(zerompk::Error::InvalidMarker(other)), diff --git a/nodedb-types/src/json_msgpack/transcoder.rs b/nodedb-types/src/json_msgpack/transcoder.rs index 693e65c55..32554037a 100644 --- a/nodedb-types/src/json_msgpack/transcoder.rs +++ b/nodedb-types/src/json_msgpack/transcoder.rs @@ -6,15 +6,18 @@ //! No intermediate `serde_json::Value` or `nodedb_types::Value`. //! Used ONLY at the outermost pgwire/HTTP layer for client compatibility. //! -//! Instant ext values (fixext8 type 1 / 2) render as ISO 8601 strings. -//! Every other ext type renders as `null`. +//! Instant ext values (fixext8 type 1 / 2) render as ISO 8601 strings. A +//! geometry ext (type 3) renders as GeoJSON. Every other ext type renders as +//! `null`. use std::fmt::Write as _; use super::error::MsgpackResult; +use super::geometry_ext::geometry_from_ext; use super::instant_ext::instant_from_ext; use super::reader::{Cursor, base64_encode}; use crate::datetime::NdbDateTime; +use crate::geometry::geojson_tree; use crate::value::float_text::non_finite_float_text; /// Transcode raw msgpack bytes to a JSON string without intermediate types. @@ -171,24 +174,18 @@ fn transcode_value(c: &mut Cursor<'_>, out: &mut String) -> zerompk::Result<()> c.take_n(5)?; out.push_str("null"); } - 0xD8 => { - c.take_n(17)?; - out.push_str("null"); - } + 0xD8 => transcode_ext(c, out, 16)?, 0xC7 => { let l = c.take()? as usize; - c.take_n(1 + l)?; - out.push_str("null"); + transcode_ext(c, out, l)?; } 0xC8 => { let l = c.read_u16_be()? as usize; - c.take_n(1 + l)?; - out.push_str("null"); + transcode_ext(c, out, l)?; } 0xC9 => { let l = c.read_u32_be()? as usize; - c.take_n(1 + l)?; - out.push_str("null"); + transcode_ext(c, out, l)?; } _ => return Err(zerompk::Error::InvalidMarker(marker)), @@ -196,6 +193,20 @@ fn transcode_value(c: &mut Cursor<'_>, out: &mut String) -> zerompk::Result<()> Ok(()) } +/// An ext value of `len` payload bytes: a geometry renders as GeoJSON, +/// any other type as `null`. +fn transcode_ext(c: &mut Cursor<'_>, out: &mut String, len: usize) -> zerompk::Result<()> { + let ext_type = c.take()? as i8; + let payload = c.take_n(len)?; + match geometry_from_ext(ext_type, payload) + .and_then(|g| sonic_rs::to_string(&geojson_tree(&g)).ok()) + { + Some(json) => out.push_str(&json), + None => out.push_str("null"), + } + Ok(()) +} + fn write_int(out: &mut String, v: i64) { let _ = write!(out, "{v}"); } diff --git a/nodedb-types/src/json_msgpack/writer.rs b/nodedb-types/src/json_msgpack/writer.rs index dd979abd6..4a8f71556 100644 --- a/nodedb-types/src/json_msgpack/writer.rs +++ b/nodedb-types/src/json_msgpack/writer.rs @@ -73,9 +73,9 @@ impl zerompk::ToMessagePack for OrderedMapRef<'_> { /// Write a `nodedb_types::Value` as standard msgpack. /// -/// `Duration`, `Decimal`, and `Geometry` are strings, except a `Decimal` -/// that holds a `u64` above `i64::MAX` (see [`crate::Value::decimal_as_wide_u64`]), -/// which is a `uint64`. `Vector` is a float64 array, `ArrayCell` a map, and +/// `Duration` and `Decimal` are strings, except a `Decimal` that holds a +/// `u64` above `i64::MAX` (see [`crate::Value::decimal_as_wide_u64`]), +/// which is a `uint64`. `Geometry` is the geometry ext (type 3). `Vector` is a float64 array, `ArrayCell` a map, and /// `Range` / `Record` are `nil`. pub(crate) fn write_native_value( writer: &mut W, @@ -113,10 +113,7 @@ pub(crate) fn write_native_value( Some(u) => writer.write_u64(u), None => writer.write_string(&d.to_string()), }, - crate::Value::Geometry(g) => match sonic_rs::to_string(g) { - Ok(s) => writer.write_string(&s), - Err(_) => writer.write_nil(), - }, + crate::Value::Geometry(g) => super::geometry_ext::write_geometry_ext(writer, g), crate::Value::Range { .. } | crate::Value::Record { .. } => writer.write_nil(), crate::Value::Vector(v) => { // A standard msgpack array of float64 values, so pgwire clients diff --git a/nodedb-types/src/lib.rs b/nodedb-types/src/lib.rs index d7a23ed5b..ba33c78b3 100644 --- a/nodedb-types/src/lib.rs +++ b/nodedb-types/src/lib.rs @@ -14,6 +14,7 @@ pub mod config; pub mod quota; pub mod approx; +pub mod array_agg; pub mod array_cell; pub mod array_route; pub mod ascii; @@ -74,6 +75,7 @@ pub mod vector_index_params; pub mod vector_index_stats; pub mod vector_model; pub mod volatility; +pub mod wall_clock; pub mod wire_version; pub mod write_version; @@ -154,4 +156,5 @@ pub use vector_index_stats::{ }; pub use vector_model::{VectorModelEntry, VectorModelMetadata}; pub use volatility::Volatility; +pub use wall_clock::{wall_now_ns, wall_now_ns_i64}; pub use write_version::{ShardVersion, WriteVersion}; diff --git a/nodedb-types/src/sync/wire/delta.rs b/nodedb-types/src/sync/wire/delta.rs index 25acc7ab8..98a12877e 100644 --- a/nodedb-types/src/sync/wire/delta.rs +++ b/nodedb-types/src/sync/wire/delta.rs @@ -56,6 +56,13 @@ pub struct DeltaPushMsg { /// All zeros denotes an unsigned legacy delta. #[serde(default)] pub delta_signature: [u8; 32], + /// The incarnation of the collection the client built this delta + /// against: the `creation_hlc` Origin announced with the collection's + /// descriptor. Origin refuses a delta for an incarnation the collection + /// no longer holds, because a drop and a same-name create replaced it. + /// Every client the handshake admits states one: Origin refuses + /// `Hlc::ZERO` the same way. + pub collection_incarnation: crate::hlc::Hlc, } /// Delta acknowledgment (server → client, 0x11). diff --git a/nodedb-types/src/sync/wire/mod.rs b/nodedb-types/src/sync/wire/mod.rs index 3e222e155..4a86a55af 100644 --- a/nodedb-types/src/sync/wire/mod.rs +++ b/nodedb-types/src/sync/wire/mod.rs @@ -96,7 +96,8 @@ pub use presence::{PeerPresence, PresenceBroadcastMsg, PresenceLeaveMsg, Presenc pub use provenance::SyncProvenance; pub use resync::{ResyncReason, ResyncRequestMsg, ThrottleMsg}; pub use session::{ - HandshakeAckMsg, HandshakeMsg, PingPongMsg, TokenRefreshAckMsg, TokenRefreshMsg, + HandshakeAckMsg, HandshakeMsg, PingPongMsg, SYNC_WIRE_VERSION, SyncWireVersionMismatch, + TokenRefreshAckMsg, TokenRefreshMsg, }; pub use shape::{ ShapeDeltaMsg, ShapeSnapshotMsg, ShapeSubscribeMsg, ShapeUnsubscribeMsg, VectorClockSyncMsg, diff --git a/nodedb-types/src/sync/wire/session.rs b/nodedb-types/src/sync/wire/session.rs index 72e576a46..8b0f172eb 100644 --- a/nodedb-types/src/sync/wire/session.rs +++ b/nodedb-types/src/sync/wire/session.rs @@ -6,6 +6,29 @@ use std::collections::HashMap; use serde::{Deserialize, Serialize}; +/// The sync protocol version this build speaks. A client sends it in +/// [`HandshakeMsg::wire_version`], and a server answers its own in +/// [`HandshakeAckMsg::server_wire_version`]. A server refuses a handshake +/// whose version differs from its own: the two sides frame messages +/// differently, and a frame of the other layout does not decode. +/// +/// It rises whenever the layout of a sync message changes. At version 2, +/// `DeltaPushMsg` carries `collection_incarnation`. +pub const SYNC_WIRE_VERSION: u16 = 2; + +/// A handshake whose sync protocol version differs from the server's. +#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] +#[error( + "sync wire version {client} does not match the server's {server}: rebuild the client \ + against a nodedb-types release with sync wire version {server}" +)] +pub struct SyncWireVersionMismatch { + /// The version the client's handshake carries. + pub client: u16, + /// The version the server speaks. + pub server: u16, +} + /// Handshake message (client → server, 0x01). #[derive( Debug, Clone, Serialize, Deserialize, zerompk::ToMessagePack, zerompk::FromMessagePack, @@ -25,12 +48,27 @@ pub struct HandshakeMsg { /// Monotonic epoch counter (incremented on every open). Default 0 for non-Lite peers. #[serde(default)] pub epoch: u64, - /// Wire format version. Server rejects connections with incompatible versions. - /// Missing field deserializes to 0 and is rejected by the server explicitly. + /// Sync protocol version: [`SYNC_WIRE_VERSION`] of the client's build. + /// A server refuses any other version. A missing field reads as 0 and is + /// refused. #[serde(default)] pub wire_version: u16, } +impl HandshakeMsg { + /// `Err` when this handshake's sync protocol version is not this build's + /// [`SYNC_WIRE_VERSION`]. + pub fn check_wire_version(&self) -> Result<(), SyncWireVersionMismatch> { + if self.wire_version == SYNC_WIRE_VERSION { + return Ok(()); + } + Err(SyncWireVersionMismatch { + client: self.wire_version, + server: SYNC_WIRE_VERSION, + }) + } +} + /// Handshake acknowledgment (server → client, 0x02). #[derive( Debug, Clone, Serialize, Deserialize, zerompk::ToMessagePack, zerompk::FromMessagePack, @@ -47,7 +85,7 @@ pub struct HandshakeAckMsg { /// Fork detection: if true, client must regenerate LiteId and reconnect. #[serde(default)] pub fork_detected: bool, - /// Server's wire format version (for client-side compatibility check). + /// Server's sync protocol version ([`SYNC_WIRE_VERSION`]). #[serde(default)] pub server_wire_version: u16, /// Server-assigned producer ID for this session. 0 if not yet assigned. @@ -63,6 +101,20 @@ pub struct HandshakeAckMsg { pub delta_signing_key: [u8; 32], } +impl HandshakeAckMsg { + /// The version refusal this ack carries: `Some` when the server refused + /// the handshake and speaks a sync protocol version other than this + /// build's [`SYNC_WIRE_VERSION`]. + pub fn wire_version_mismatch(&self) -> Option { + (!self.success && self.server_wire_version != SYNC_WIRE_VERSION).then_some( + SyncWireVersionMismatch { + client: SYNC_WIRE_VERSION, + server: self.server_wire_version, + }, + ) + } +} + /// Token refresh request (client → server, 0x60). /// /// Sent by Lite before the current JWT expires. The client provides @@ -116,7 +168,7 @@ mod tests { client_version: "0.1.0".into(), lite_id: String::new(), epoch: 0, - wire_version: 1, + wire_version: SYNC_WIRE_VERSION, }; let frame = SyncFrame::new_msgpack(SyncMessageType::Handshake, &msg).unwrap(); let bytes = frame.to_bytes(); @@ -124,4 +176,48 @@ mod tests { assert_eq!(bytes[0], SyncFrame::FORMAT_VERSION); assert_eq!(bytes[1], 0x01); } + + /// A handshake at another sync protocol version is refused with the + /// typed mismatch, and an ack refusing at another version names it. + #[test] + fn a_handshake_at_another_wire_version_is_a_typed_mismatch() { + let handshake = |wire_version| HandshakeMsg { + jwt_token: String::new(), + vector_clock: HashMap::new(), + subscribed_shapes: Vec::new(), + client_version: "0.1.0".into(), + lite_id: String::new(), + epoch: 0, + wire_version, + }; + assert_eq!(handshake(SYNC_WIRE_VERSION).check_wire_version(), Ok(())); + assert_eq!( + handshake(1).check_wire_version(), + Err(SyncWireVersionMismatch { + client: 1, + server: SYNC_WIRE_VERSION + }) + ); + + let ack = |success, server_wire_version| HandshakeAckMsg { + success, + session_id: String::new(), + server_clock: HashMap::new(), + error: None, + fork_detected: false, + server_wire_version, + producer_id: 0, + accepted_epoch: 0, + delta_signing_key: [0; 32], + }; + assert_eq!( + ack(false, SYNC_WIRE_VERSION + 1).wire_version_mismatch(), + Some(SyncWireVersionMismatch { + client: SYNC_WIRE_VERSION, + server: SYNC_WIRE_VERSION + 1 + }) + ); + assert_eq!(ack(false, SYNC_WIRE_VERSION).wire_version_mismatch(), None); + assert_eq!(ack(true, SYNC_WIRE_VERSION).wire_version_mismatch(), None); + } } diff --git a/nodedb-types/src/sync/wire/spatial.rs b/nodedb-types/src/sync/wire/spatial.rs index 351160d6a..677f29da9 100644 --- a/nodedb-types/src/sync/wire/spatial.rs +++ b/nodedb-types/src/sync/wire/spatial.rs @@ -25,7 +25,7 @@ use crate::sync::wire::ack_status::AckStatus; /// /// `geometry_bytes` is a MessagePack-serialised /// `nodedb_types::geometry::SridGeometry` produced via -/// `zerompk::to_msgpack_vec(&geometry)`: `[srid_code_u32, geometry]`. +/// `zerompk::to_msgpack_vec(&geometry)`: `[srid_code_u32, geometry, z | nil, m | nil]`. #[derive( Debug, Clone, Serialize, Deserialize, zerompk::ToMessagePack, zerompk::FromMessagePack, )] diff --git a/nodedb-types/src/temporal/ordinal.rs b/nodedb-types/src/temporal/ordinal.rs index 11786081f..1fcbc3b46 100644 --- a/nodedb-types/src/temporal/ordinal.rs +++ b/nodedb-types/src/temporal/ordinal.rs @@ -22,8 +22,8 @@ //! handled by the closed-timestamp wiring; `update_from_remote` is //! exposed here so that wiring has a single canonical API to call. +use crate::wall_clock::wall_now_ns_i64; use std::sync::Mutex; -use std::time::{SystemTime, UNIX_EPOCH}; /// Nanoseconds in one millisecond. pub const NANOS_PER_MS: i64 = 1_000_000; @@ -48,13 +48,26 @@ impl OrdinalClock { /// Return a strictly-greater ordinal than any previously returned value, /// seeded by the current wall-clock nanosecond. pub fn next_ordinal(&self) -> i64 { - let wall = wall_now_ns(); + let wall = wall_now_ns_i64(); let mut st = self.last.lock().unwrap_or_else(|p| p.into_inner()); let next = wall.max(st.saturating_add(1)); *st = next; next } + /// Reserve `count` consecutive ordinals and return the first. Every one + /// is strictly greater than any ordinal handed out or folded in before, + /// and the next [`next_ordinal`](Self::next_ordinal) is greater than the + /// last of them. `count` 0 reserves one. + pub fn reserve(&self, count: usize) -> i64 { + let span = i64::try_from(count.max(1)).unwrap_or(i64::MAX); + let wall = wall_now_ns_i64(); + let mut st = self.last.lock().unwrap_or_else(|p| p.into_inner()); + let first = wall.max(st.saturating_add(1)); + *st = first.saturating_add(span - 1); + first + } + /// Fold a remote peer's observed ordinal into the local clock so the /// next local [`next_ordinal`](Self::next_ordinal) call is strictly /// greater. No-op if the remote value is already behind us. @@ -106,31 +119,6 @@ pub fn calvin_txn_ordinal(epoch_system_ms: i64, position: u32) -> i64 { .saturating_add(i64::from(position)) } -fn wall_now_ns() -> i64 { - SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|d| { - let ns = d.as_nanos(); - if ns > i64::MAX as u128 { - i64::MAX - } else { - ns as i64 - } - }) - .unwrap_or_else(|_| { - use std::sync::atomic::{AtomicBool, Ordering}; - static LOGGED: AtomicBool = AtomicBool::new(false); - if !LOGGED.swap(true, Ordering::Relaxed) { - tracing::error!( - module = module_path!(), - "system clock is before UNIX_EPOCH; using 0 (epoch) \ - — check NTP/RTC configuration" - ); - } - 0 - }) -} - #[cfg(test)] mod tests { use super::*; @@ -150,7 +138,7 @@ mod tests { fn next_ordinal_tracks_wall_clock_roughly() { let clock = OrdinalClock::new(); let ord = clock.next_ordinal(); - let wall = wall_now_ns(); + let wall = wall_now_ns_i64(); // The ordinal should be within one second of wall clock on any // reasonable machine — proves we're not emitting a pure counter. assert!((ord - wall).abs() < 1_000_000_000, "ord={ord} wall={wall}"); @@ -160,12 +148,28 @@ mod tests { fn update_from_remote_advances_on_greater() { let clock = OrdinalClock::new(); let _ = clock.next_ordinal(); - let remote = wall_now_ns() + 10_000_000_000; // 10s in future + let remote = wall_now_ns_i64() + 10_000_000_000; // 10s in future clock.update_from_remote(remote); let next = clock.next_ordinal(); assert!(next > remote); } + #[test] + fn a_reserved_run_sits_between_its_neighbours() { + let clock = OrdinalClock::new(); + let before = clock.next_ordinal(); + let first = clock.reserve(5); + assert!(first > before); + assert_eq!( + clock.peek(), + first + 4, + "the clock holds the run's last ordinal" + ); + assert!(clock.next_ordinal() > first + 4); + let single = clock.reserve(0); + assert_eq!(clock.peek(), single, "an empty run reserves one ordinal"); + } + #[test] fn update_from_remote_noop_when_behind() { let clock = OrdinalClock::new(); diff --git a/nodedb-types/src/temporal/system_time.rs b/nodedb-types/src/temporal/system_time.rs index 88bc593d3..cae2a484c 100644 --- a/nodedb-types/src/temporal/system_time.rs +++ b/nodedb-types/src/temporal/system_time.rs @@ -2,16 +2,17 @@ //! System-time scope for bitemporal scans. //! -//! Replaces the former `Option` system-AS-OF value with an explicit -//! three-state enum so that the "all versions" (audit-log) mode is -//! representable instead of being an out-of-band sentinel. +//! An explicit enum makes the "all versions" (audit-log) mode and the +//! statement-pinned cut representable, with no out-of-band sentinel. use serde::{Deserialize, Serialize}; /// System-time selection for a temporal scan. /// -/// All timestamps are **milliseconds since Unix epoch**. Valid time is a -/// separate axis and is unaffected by this enum. +/// `AsOf` carries the statement's time in milliseconds since Unix epoch. +/// `Pinned` carries the system-time ordinal the Control Plane pinned for +/// one statement. Valid time is a separate axis and is unaffected by this +/// enum. #[derive( Debug, Clone, @@ -28,8 +29,21 @@ pub enum SystemTimeScope { /// Latest committed version (no `AS OF SYSTEM TIME` clause). #[default] Current, - /// Point-in-time snapshot: the version visible AS OF `ms`. + /// Point-in-time snapshot: the version visible AS OF `ms`. A read at + /// this scope reads at the last ordinal of `ms`, with no cap. AsOf(i64), + /// Point-in-time snapshot at one pinned system-time ordinal: the + /// statement's `AS OF SYSTEM TIME`, capped at the statement's read cut. + /// Every read of the statement reads at `ordinal`, so the statement + /// reads one cut. + Pinned { + /// The ordinal every versioned read of the statement reads at. + ordinal: i64, + /// The Calvin cut marker whose cut caps `ordinal`, `0` when no + /// marker caps it. A serving node waits for the marker before it + /// reads. + cut_marker: u64, + }, /// `AS OF SYSTEM TIME NULL` — every system-time version of each matching /// row, ordered ascending by system time, with the system-time column /// projected into the output (audit-log semantics). @@ -42,20 +56,61 @@ impl SystemTimeScope { !matches!(self, Self::Current) } - /// The point-in-time cutoff, if this is an `AsOf` scope. - pub const fn as_of_ms(&self) -> Option { + /// The point-in-time cutoff in milliseconds, if this is a point-in-time + /// scope. An engine that stamps system time in milliseconds reads at + /// this value. A pinned scope answers the newest millisecond whose every + /// ordinal is at or below its ordinal. A write in the cut's own + /// millisecond can land after the cut, so a cut inside a millisecond + /// reads the millisecond before it, and every read of the statement + /// reads the same rows. + pub fn as_of_ms(&self) -> Option { match self { Self::AsOf(ms) => Some(*ms), + Self::Pinned { ordinal, .. } => Some(whole_ms_at_or_below(*ordinal)), Self::Current | Self::AllVersions => None, } } + /// The point-in-time cutoff as a system-time ordinal. Version keys + /// compare against this value. `AsOf` answers the newest ordinal a + /// write in the cutoff millisecond can carry. `Pinned` answers its + /// ordinal. + pub fn as_of_ordinal(&self) -> Option { + match self { + Self::AsOf(ms) => Some(super::ordinal::ms_to_ordinal_upper(*ms)), + Self::Pinned { ordinal, .. } => Some(*ordinal), + Self::Current | Self::AllVersions => None, + } + } + + /// The pinned ordinal and the cut marker that caps it, when a marker + /// caps a pinned scope. + pub const fn marked_cut(&self) -> Option<(i64, u64)> { + match self { + Self::Pinned { + ordinal, + cut_marker, + } if *cut_marker != 0 => Some((*ordinal, *cut_marker)), + Self::Pinned { .. } | Self::Current | Self::AsOf(_) | Self::AllVersions => None, + } + } + /// True when this scope requests every system-time version. pub const fn is_all_versions(&self) -> bool { matches!(self, Self::AllVersions) } } +/// The newest millisecond whose every ordinal is at or below `ordinal`. +fn whole_ms_at_or_below(ordinal: i64) -> i64 { + let ms = super::ordinal::ordinal_to_ms(ordinal); + if super::ordinal::ms_to_ordinal_upper(ms) <= ordinal { + ms + } else { + ms - 1 + } +} + #[cfg(test)] mod tests { use super::*; @@ -73,6 +128,47 @@ mod tests { assert_eq!(SystemTimeScope::AllVersions.as_of_ms(), None); } + #[test] + fn as_of_ordinal_is_the_last_ordinal_of_the_millisecond() { + assert_eq!( + SystemTimeScope::AsOf(42).as_of_ordinal(), + Some(42 * 1_000_000 + 999_999) + ); + assert_eq!(SystemTimeScope::Current.as_of_ordinal(), None); + assert_eq!(SystemTimeScope::AllVersions.as_of_ordinal(), None); + } + + #[test] + fn a_pinned_scope_reads_at_its_ordinal() { + let pinned = SystemTimeScope::Pinned { + ordinal: 42 * 1_000_000 + 7, + cut_marker: 0, + }; + assert_eq!(pinned.as_of_ordinal(), Some(42 * 1_000_000 + 7)); + assert_eq!( + pinned.as_of_ms(), + Some(41), + "a cut inside 42 reads up to 41" + ); + let whole = SystemTimeScope::Pinned { + ordinal: 42 * 1_000_000 + 999_999, + cut_marker: 0, + }; + assert_eq!( + whole.as_of_ms(), + Some(42), + "a cut at the end of 42 reads 42" + ); + assert!(pinned.is_temporal()); + assert_eq!(pinned.marked_cut(), None); + let marked = SystemTimeScope::Pinned { + ordinal: 9, + cut_marker: 4, + }; + assert_eq!(marked.marked_cut(), Some((9, 4))); + assert_eq!(SystemTimeScope::AsOf(9).marked_cut(), None); + } + #[test] fn all_versions_is_temporal() { assert!(SystemTimeScope::AllVersions.is_temporal()); diff --git a/nodedb-types/src/value/msgpack.rs b/nodedb-types/src/value/msgpack.rs index cb59e8934..e9319c21b 100644 --- a/nodedb-types/src/value/msgpack.rs +++ b/nodedb-types/src/value/msgpack.rs @@ -11,7 +11,7 @@ use std::sync::Arc; use super::core::Value; use crate::array_cell::ArrayCell; use crate::datetime::{NdbDateTime, NdbDuration}; -use crate::geometry::{Geometry, Srid, SridGeometry}; +use crate::geometry::tagged::msgpack::{SRID_GEOMETRY_FIELDS, read_fields, write_fields}; impl zerompk::ToMessagePack for Value { fn write(&self, writer: &mut W) -> zerompk::Result<()> { @@ -80,12 +80,11 @@ impl zerompk::ToMessagePack for Value { writer.write_u8(12)?; writer.write_binary(&d.serialize()) } - // `[13, srid_code_u32, geometry]`. + // `[13, srid_code_u32, geometry, z | nil, m | nil]`. Value::Geometry(g) => { - writer.write_array_len(3)?; + writer.write_array_len(1 + SRID_GEOMETRY_FIELDS)?; writer.write_u8(13)?; - g.srid.write(writer)?; - g.geometry.write(writer) + write_fields(g, writer) } Value::Set(s) => { writer.write_array_len(2)?; @@ -166,15 +165,14 @@ impl<'a> zerompk::FromMessagePack<'a> for Value { Ok(Value::Decimal(rust_decimal::Decimal::deserialize(buf))) } 13 => { - if len != 3 { + if len != 1 + SRID_GEOMETRY_FIELDS { return Err(zerompk::Error::ArrayLengthMismatch { - expected: 3, + expected: 1 + SRID_GEOMETRY_FIELDS, actual: len, }); } - let srid = Srid::read(reader)?; - let geometry = Geometry::read(reader)?; - Ok(Value::Geometry(SridGeometry::new(srid, geometry))) + // A stored SRID is checked for syntax only. + Ok(Value::Geometry(read_fields(reader)?)) } 14 => Ok(Value::Set(Vec::::read(reader)?)), 15 => Ok(Value::Regex(reader.read_string()?.into_owned())), @@ -223,10 +221,26 @@ impl<'a> zerompk::FromMessagePack<'a> for Value { #[cfg(test)] mod tests { use super::*; + use crate::geometry::{Geometry, Ordinates, Srid, SridGeometry}; + + #[test] + fn geometry_round_trip_keeps_z_and_m() { + let g = SridGeometry::with_ordinates( + Srid::WGS84, + Geometry::point(1.0, 2.0), + Ordinates::new(Some(vec![3.0]), Some(vec![4.0])), + ) + .unwrap(); + let value = Value::Geometry(g); + let bytes = zerompk::to_msgpack_vec(&value).unwrap(); + let back: Value = zerompk::from_msgpack(&bytes).unwrap(); + assert_eq!(back, value); + } #[test] fn geometry_round_trip_keeps_srid() { - for srid in Srid::SUPPORTED { + for code in [4326, 32650, 4742, 990_001, 999] { + let srid = Srid::new(code).unwrap(); let value = Value::Geometry(SridGeometry::new(srid, Geometry::point(1.0, 2.0))); let bytes = zerompk::to_msgpack_vec(&value).unwrap(); let back: Value = zerompk::from_msgpack(&bytes).unwrap(); diff --git a/nodedb-types/src/value/sql_literal.rs b/nodedb-types/src/value/sql_literal.rs index 286300150..5995461c9 100644 --- a/nodedb-types/src/value/sql_literal.rs +++ b/nodedb-types/src/value/sql_literal.rs @@ -216,8 +216,11 @@ mod tests { quote_literal("Point { coordinates: [1.0, 2.0] }") ); assert_eq!( - Value::Geometry(SridGeometry::new(Srid::Utm48N, Geometry::point(1.0, 2.0))) - .to_sql_literal(), + Value::Geometry(SridGeometry::new( + Srid::new(32648).unwrap(), + Geometry::point(1.0, 2.0) + )) + .to_sql_literal(), quote_literal("SRID=32648;Point { coordinates: [1.0, 2.0] }") ); assert_eq!( diff --git a/nodedb-types/src/wall_clock.rs b/nodedb-types/src/wall_clock.rs new file mode 100644 index 000000000..e8f3e254b --- /dev/null +++ b/nodedb-types/src/wall_clock.rs @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! The wall clock in nanoseconds since the Unix epoch. Every clock, lease, +//! and cut wait reads wall time here. + +use std::sync::atomic::{AtomicBool, Ordering}; +use std::time::{SystemTime, UNIX_EPOCH}; + +/// Wall-clock nanoseconds since the Unix epoch. A clock before the epoch +/// reads `0` and logs once. A clock past `u64::MAX` saturates. +pub fn wall_now_ns() -> u64 { + u64::try_from(wall_now_nanos()).unwrap_or(u64::MAX) +} + +/// [`wall_now_ns`] as a signed ordinal-space value. A clock past +/// `i64::MAX` (the year 2262) saturates. +pub fn wall_now_ns_i64() -> i64 { + i64::try_from(wall_now_nanos()).unwrap_or(i64::MAX) +} + +fn wall_now_nanos() -> u128 { + match SystemTime::now().duration_since(UNIX_EPOCH) { + Ok(since) => since.as_nanos(), + Err(_) => { + static LOGGED: AtomicBool = AtomicBool::new(false); + if !LOGGED.swap(true, Ordering::Relaxed) { + tracing::error!( + module = module_path!(), + "system clock is before UNIX_EPOCH; using 0 (epoch) \ + — check NTP/RTC configuration" + ); + } + 0 + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn both_accessors_read_one_clock() { + let unsigned = wall_now_ns(); + let signed = wall_now_ns_i64(); + let later = wall_now_ns(); + assert!(unsigned > 0, "the wall clock is past the epoch"); + assert!(u64::try_from(signed).is_ok_and(|s| s >= unsigned && s <= later)); + } +} diff --git a/nodedb/Cargo.toml b/nodedb/Cargo.toml index 06109907b..c1aaf9416 100644 --- a/nodedb/Cargo.toml +++ b/nodedb/Cargo.toml @@ -24,6 +24,7 @@ path = "src/lib.rs" [dependencies] # Internal foundation crates nodedb-types = { workspace = true } +nodedb-crs = { workspace = true } nodedb-columnar = { workspace = true, features = ["diagnostics"] } nodedb-bridge = { workspace = true } nodedb-physical = { workspace = true } @@ -42,6 +43,7 @@ nodedb-graph = { workspace = true } nodedb-vector = { workspace = true } nodedb-array = { workspace = true } nodedb-raster = { workspace = true } +nodedb-raster-load = { workspace = true } nodedb-sql = { workspace = true } arrow = { workspace = true } diff --git a/nodedb/src/bootstrap/catalog_open.rs b/nodedb/src/bootstrap/catalog_open.rs index bf0e03fc0..7ee13b991 100644 --- a/nodedb/src/bootstrap/catalog_open.rs +++ b/nodedb/src/bootstrap/catalog_open.rs @@ -101,6 +101,13 @@ impl CatalogForRead { Self::Repaired(c) => c.list_all_vector_index_params(), } } + + pub(crate) fn load_custom_crs_set(&self) -> crate::Result { + match self { + Self::ReadOnly(c) => c.load_custom_crs_set(), + Self::Repaired(c) => Ok(nodedb_crs::CustomCrsSet::clone(&c.custom_crs_set())), + } + } } impl CollectionSource for CatalogForRead { diff --git a/nodedb/src/bootstrap/data_plane.rs b/nodedb/src/bootstrap/data_plane.rs index 0f578619d..4a68c12fc 100644 --- a/nodedb/src/bootstrap/data_plane.rs +++ b/nodedb/src/bootstrap/data_plane.rs @@ -264,6 +264,39 @@ pub fn load_columnar_schema_seed( .collect() } +/// Load the user-defined CRS set from the durable catalog so each Data Plane +/// core resolves user-defined SRIDs from its first request, WAL replay +/// included. A committed `CREATE` or `DROP SPATIAL_REF_SYS` reaches a running +/// core through `MetaOp::SetCustomCrsSet`; this seed covers the set as of +/// boot. Mirrors [`load_doc_config_registry`]'s fail-open pattern: a catalog +/// open/load failure logs an error and yields an empty set, so a geometry in +/// a user-defined SRID fails to resolve instead of booting being refused. +pub fn load_crs_set_seed(config: &ServerConfig) -> Arc { + let catalog = match CatalogForRead::open(&config.catalog_path()) { + Ok(Some(catalog)) => catalog, + // No catalog yet: a genuine fresh start, nothing to seed. + Ok(None) => return Arc::new(nodedb_crs::CustomCrsSet::new()), + Err(error) => { + tracing::error!( + error = %error, + "catalog exists but could not be opened; cores will boot with an \ + EMPTY user-defined CRS set" + ); + return Arc::new(nodedb_crs::CustomCrsSet::new()); + } + }; + match catalog.load_custom_crs_set() { + Ok(set) => Arc::new(set), + Err(error) => { + tracing::error!( + error = %error, + "failed to load the user-defined CRS set; cores will boot with an EMPTY set" + ); + Arc::new(nodedb_crs::CustomCrsSet::new()) + } + } +} + /// Result of [`spawn_data_plane_cores`]: the core thread handles plus each /// core's WAL-replay-completion signal. pub struct SpawnedDataPlaneCores { @@ -295,6 +328,7 @@ pub struct CoreSharedResources { nodedb_types::columnar::ColumnarSchema, )>, >, + pub crs_set_seed: Arc, } /// Spawn all Data Plane cores, wire dispatcher notifiers, and return core @@ -325,6 +359,7 @@ pub fn spawn_data_plane_cores( doc_config_seed, vector_index_param_seed, columnar_schema_seed, + crs_set_seed, } = resources; let num_cores = config.server.data_plane_cores; let node_id = crate::control::cluster::configured_node_id(config); @@ -368,6 +403,7 @@ pub fn spawn_data_plane_cores( doc_config_seed: Arc::clone(&doc_config_seed), vector_index_param_seed: Arc::clone(&vector_index_param_seed), columnar_schema_seed: Arc::clone(&columnar_schema_seed), + crs_set_seed: Arc::clone(&crs_set_seed), replay_done: replay_done_tx, })?; core_handles.push(handle); diff --git a/nodedb/src/bootstrap/jwks_registry.rs b/nodedb/src/bootstrap/jwks_registry.rs new file mode 100644 index 000000000..19a34b585 --- /dev/null +++ b/nodedb/src/bootstrap/jwks_registry.rs @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! Wire the JWKS registry into `SharedState`. +//! +//! The registry is built on every server: static `[auth.jwt]` providers and +//! catalog OIDC providers both verify through it, and a catalog provider can +//! be created at runtime on a server that has no static provider. Every +//! catalog provider the catalog holds at boot is registered here; one created, +//! altered or dropped later registers through its catalog entry's post-apply. + +use std::sync::Arc; + +use tracing::info; + +use crate::ServerConfig; +use crate::control::security::jwks::registry::JwksRegistry; +use crate::control::state::SharedState; + +/// Build the registry, install it, and register the catalog's providers. +pub async fn wire_jwks_registry( + shared: &mut Arc, + config: &ServerConfig, +) -> crate::Result<()> { + let jwt_config = config.auth.jwt.clone().unwrap_or_default(); + let static_count = jwt_config.providers.len(); + let state = Arc::get_mut(shared).ok_or_else(|| crate::Error::Internal { + detail: "JWKS wiring: shared state is already shared before the registry was installed" + .into(), + })?; + let registry = + Arc::new(JwksRegistry::init(jwt_config, Arc::clone(state.remote_sources.egress())).await?); + let catalog_providers = state.credentials.catalog().list_oidc_providers()?; + // The key fetches run detached, so an unreachable provider never holds + // up boot. A token that arrives first fetches its keys itself. + for provider in &catalog_providers { + let target = + registry.register_catalog_provider(&provider.provider_name, &provider.jwks_uri); + let registry = Arc::clone(®istry); + tokio::spawn(async move { + registry.prefetch(&target).await; + }); + } + state.jwks_registry = Some(registry); + info!( + static_providers = static_count, + catalog_providers = catalog_providers.len(), + "JWKS registry initialised" + ); + Ok(()) +} diff --git a/nodedb/src/bootstrap/mod.rs b/nodedb/src/bootstrap/mod.rs index 735077884..8cf48a9ad 100644 --- a/nodedb/src/bootstrap/mod.rs +++ b/nodedb/src/bootstrap/mod.rs @@ -11,10 +11,12 @@ pub mod data_group_recovery; pub mod data_plane; pub mod diagnostics; pub mod index_registry_seed; +pub mod jwks_registry; pub mod listeners; pub mod panic_hook; pub mod permission_tree_load; pub mod quota_replay; +pub mod remote_sources; pub mod schema_rehydrate; pub mod signal; pub mod stall_clock; diff --git a/nodedb/src/bootstrap/remote_sources.rs b/nodedb/src/bootstrap/remote_sources.rs new file mode 100644 index 000000000..5d6699dac --- /dev/null +++ b/nodedb/src/bootstrap/remote_sources.rs @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! Wire `[credential_encryption]` into `SharedState`. `SharedState::open` +//! installs the `[egress]` policy and its outbound client. + +use std::sync::Arc; + +use tracing::info; + +use crate::ServerConfig; +use crate::control::security::source_credential::CredentialKek; +use crate::control::state::SharedState; + +/// Install the credential key. A key file that fails its checks stops +/// startup. +pub fn wire_remote_sources( + shared: &mut Arc, + config: &ServerConfig, +) -> crate::Result<()> { + let credential_kek = match &config.credential_encryption { + Some(settings) => { + let reused = config + .encryption + .as_ref() + .is_some_and(|wal| wal.key_file() == Some(settings.key_path.as_path())) + || config + .backup_encryption + .as_ref() + .is_some_and(|backup| backup.key_path == settings.key_path); + if reused { + return Err(crate::Error::Config { + detail: "credential_encryption.key_path names the WAL or backup key file; \ + give credentials a key of their own" + .into(), + }); + } + let kek = CredentialKek::read(&settings.key_path)?; + info!(key_path = %settings.key_path.display(), "credential encryption enabled"); + Some(Arc::new(kek)) + } + None => None, + }; + let state = Arc::get_mut(shared).ok_or_else(|| crate::Error::Internal { + detail: "remote source settings: shared state is already shared before wiring".into(), + })?; + state.remote_sources.credential_kek = credential_kek; + Ok(()) +} diff --git a/nodedb/src/bootstrap/state_wiring.rs b/nodedb/src/bootstrap/state_wiring.rs index e14d0b31e..dae33f3cc 100644 --- a/nodedb/src/bootstrap/state_wiring.rs +++ b/nodedb/src/bootstrap/state_wiring.rs @@ -77,20 +77,8 @@ pub async fn wire_state( root_span.record("node_id", cluster_handle.node_id); } - // Initialise JWKS registry. - if let Some(ref jwt_config) = config.auth.jwt - && !jwt_config.providers.is_empty() - && let Some(state) = Arc::get_mut(shared) - { - let registry = - crate::control::security::jwks::registry::JwksRegistry::init(jwt_config.clone()) - .await?; - state.jwks_registry = Some(Arc::new(registry)); - info!( - "JWKS registry initialised with {} providers", - jwt_config.providers.len() - ); - } + // The JWKS registry verifies static and catalog OIDC providers alike. + super::jwks_registry::wire_jwks_registry(shared, config).await?; // Initialise cold storage (L2 tiering and the WAL archive). if let Some(ref cold_settings) = config.cold_storage { @@ -212,7 +200,7 @@ pub async fn wire_state( state.backup_kek = Some(Arc::new(key_bytes)); } if let Some(ref enc) = config.encryption - && enc.key_path == benc.key_path + && enc.key_file() == Some(benc.key_path.as_path()) { tracing::warn!( path = %benc.key_path.display(), @@ -241,6 +229,8 @@ pub async fn wire_state( } } + super::remote_sources::wire_remote_sources(shared, config)?; + // Wire OTLP trace exporter and misc config fields. if let Some(state) = Arc::get_mut(shared) { let otlp = &config.observability.otlp.export; @@ -248,6 +238,7 @@ pub async fn wire_state( crate::control::trace_export::TraceExporter::new( otlp.endpoint.clone(), std::time::Duration::from_secs(5), + Arc::clone(state.remote_sources.egress()), ) .map_err(|e| crate::Error::Config { detail: format!("OTLP trace exporter: {e}"), @@ -259,6 +250,27 @@ pub async fn wire_state( state.scheduler_config = config.scheduler.clone(); } + // OTLP metrics push. It holds no clone of the shared state, so the + // `Arc::get_mut` installs below still succeed. + let otlp = &config.observability.otlp.export; + let metrics_exporter = crate::control::otel::exporter::ExporterConfig { + endpoint: if otlp.enabled { + otlp.endpoint.clone() + } else { + String::new() + }, + metrics_interval: std::time::Duration::from_secs(otlp.metrics_interval_secs), + export_traces: otlp.enabled, + export_metrics: otlp.enabled, + }; + if crate::control::otel::exporter::spawn_metrics_exporter(shared, metrics_exporter).map_err( + |e| crate::Error::Config { + detail: format!("OTLP metrics exporter: the endpoint fails the [egress] policy: {e}"), + }, + )? { + info!(endpoint = %otlp.endpoint, "OTLP metrics export running"); + } + // The gateway's weak back-reference outlives this call, so every // `Arc::get_mut` install above must run BEFORE it: one placed after // it no-ops. diff --git a/nodedb/src/bootstrap/wal_init.rs b/nodedb/src/bootstrap/wal_init.rs index 0a17aa35b..430d8eef1 100644 --- a/nodedb/src/bootstrap/wal_init.rs +++ b/nodedb/src/bootstrap/wal_init.rs @@ -11,10 +11,15 @@ use crate::wal::WalManager; /// Open, validate, and replay the WAL. Load the tombstone set from redb + WAL. /// +/// `wal_key` is the key `[encryption]` names, loaded by +/// [`crate::control::security::keystore::load_wal_key`]; `None` when the +/// section is absent. +/// /// Returns `(wal, wal_records, tombstones)`. Exits the process on unrecoverable /// corruption — a dirty WAL cannot be safely replayed. pub fn init_wal( config: &ServerConfig, + wal_key: Option, ) -> anyhow::Result<( Arc, Arc<[nodedb_wal::WalRecord]>, @@ -27,11 +32,9 @@ pub fn init_wal( let mut mgr = WalManager::open_with_tuning(&wal_dir, wal_segment_target, &config.tuning.wal) .map_err(|error| wal_open_error(&wal_dir, error))?; - if let Some(ref enc) = config.encryption { - let key = nodedb_wal::crypto::WalEncryptionKey::from_file(&enc.key_path) - .map_err(crate::Error::Wal)?; + if let Some(key) = wal_key { mgr.set_encryption_ring(nodedb_wal::crypto::KeyRing::new(key))?; - info!(key_path = %enc.key_path.display(), "WAL encryption enabled"); + info!("WAL encryption enabled"); } Arc::new(mgr) }; diff --git a/nodedb/src/bridge/envelope/applied_entry.rs b/nodedb/src/bridge/envelope/applied_entry.rs new file mode 100644 index 000000000..8c91eed6a --- /dev/null +++ b/nodedb/src/bridge/envelope/applied_entry.rs @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! The replicated data-group entry a write applies. + +use nodedb_types::WriteVersion; + +/// What every replica that applies one data-group entry shares. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct AppliedEntry { + /// The entry's log position. The Data Plane records it as the version of + /// every row the write touches. + pub version: WriteVersion, + /// The system-time ordinal the entry's leader assigned: the stamp of + /// every bitemporal version the write lands, or the first ordinal of the + /// run a columnar insert stamps. `None` for an entry no leader stamped. + pub system_ordinal: Option, +} + +impl AppliedEntry { + /// The entry at `log_index` of the group the vShard moved to at `epoch`, + /// stamped with `system_ordinal`. + pub const fn logged(epoch: u64, log_index: u64, system_ordinal: Option) -> Self { + Self { + version: WriteVersion::logged(epoch, log_index), + system_ordinal, + } + } + + /// An entry at `version` that carries no system-time ordinal. + pub const fn unstamped(version: WriteVersion) -> Self { + Self { + version, + system_ordinal: None, + } + } +} diff --git a/nodedb/src/bridge/envelope/error_code.rs b/nodedb/src/bridge/envelope/error_code.rs index d21d282b6..3ed47e48b 100644 --- a/nodedb/src/bridge/envelope/error_code.rs +++ b/nodedb/src/bridge/envelope/error_code.rs @@ -211,6 +211,10 @@ pub enum ErrorCode { label: String, limit: usize, }, + /// The statement passed a fixed program limit, such as the distinct + /// values an exact median holds. `detail` names the limit and the + /// next action. SQLSTATE `54000` (program_limit_exceeded). + ProgramLimitExceeded { detail: String }, /// Text does not parse as the type of the column it is written to. /// `detail` names the column, the text and the type. SQLSTATE `22P02` /// (invalid_text_representation). diff --git a/nodedb/src/bridge/envelope/error_code_from.rs b/nodedb/src/bridge/envelope/error_code_from.rs index 855c68fe9..49bdff38b 100644 --- a/nodedb/src/bridge/envelope/error_code_from.rs +++ b/nodedb/src/bridge/envelope/error_code_from.rs @@ -16,9 +16,11 @@ impl From for ErrorCode { e @ (nodedb_query::EvalError::VectorDimensionMismatch { .. } | nodedb_query::EvalError::ArgumentType { .. } | nodedb_query::EvalError::InvalidJsonPath { .. } - | nodedb_query::EvalError::UnsupportedSrid { .. } + | nodedb_query::EvalError::Crs { .. } | nodedb_query::EvalError::MixedSrid { .. } - | nodedb_query::EvalError::ProjectionDomain { .. }) => Self::DataException { + | nodedb_query::EvalError::Transform { .. } + | nodedb_query::EvalError::Geometry { .. } + | nodedb_query::EvalError::ArgumentValue { .. }) => Self::DataException { detail: e.to_string(), }, e @ nodedb_query::EvalError::NumericOverflow { .. } => Self::NumericValueOutOfRange { @@ -164,7 +166,8 @@ impl From for ErrorCode { e @ (crate::Error::OffsetRegression { .. } | crate::Error::BackupTenantMismatch { .. } | crate::Error::InvalidLimitValue { .. } - | crate::Error::MixedSrid { .. }) => Self::DataException { + | crate::Error::MixedSrid { .. } + | crate::Error::UnknownSrid { .. }) => Self::DataException { detail: e.to_string(), }, // `22003`, as the Control Plane gives it. diff --git a/nodedb/src/bridge/envelope/mod.rs b/nodedb/src/bridge/envelope/mod.rs index 337b116e2..50841c736 100644 --- a/nodedb/src/bridge/envelope/mod.rs +++ b/nodedb/src/bridge/envelope/mod.rs @@ -2,6 +2,7 @@ //! Request/response envelopes exchanged over the SPSC bridge. +pub mod applied_entry; pub mod crdt_error_code; pub mod error_code; pub mod error_code_from; @@ -12,6 +13,7 @@ pub mod stage_vote; pub mod status; pub mod sync_hold; +pub use applied_entry::AppliedEntry; pub use error_code::ErrorCode; pub use nodedb_physical::kv_atomic::CounterFault; pub use nodedb_physical::physical_plan::PhysicalPlan; diff --git a/nodedb/src/bridge/envelope/request.rs b/nodedb/src/bridge/envelope/request.rs index b1e074483..d34f34ec2 100644 --- a/nodedb/src/bridge/envelope/request.rs +++ b/nodedb/src/bridge/envelope/request.rs @@ -110,12 +110,14 @@ pub struct Request { /// of the WAL record they reproduce. pub commit_hlc: Option, - /// The log position of the data-group Raft entry this write applies. The - /// Data Plane records it as the version of every row the write touches, - /// so every replica that applies the entry records the same version. + /// The data-group Raft entry this write applies: its log position, which + /// the Data Plane records as the version of every row the write touches, + /// and the system-time ordinal its leader assigned, which every + /// bitemporal version the write lands stamps. Every replica that applies + /// the entry records the same version and stamps the same ordinal. /// `None` for reads, control ops, and writes that apply no entry: those - /// version by their WAL LSN on the core. - pub entry_version: Option, + /// version by their WAL LSN on the core and stamp from the local clock. + pub entry_version: Option, /// Write-admission decision for this request. /// diff --git a/nodedb/src/bridge/envelope/response.rs b/nodedb/src/bridge/envelope/response.rs index ea1a53c39..5a7a834c9 100644 --- a/nodedb/src/bridge/envelope/response.rs +++ b/nodedb/src/bridge/envelope/response.rs @@ -53,9 +53,9 @@ pub enum RowEffect { }, /// The row is gone. Delete { - /// The system time of the tombstone of a row on a `bitemporal=true` - /// collection. - system_from_ms: Option, + /// The system-time ordinal of the tombstone of a row on a + /// `bitemporal=true` collection. + system_from: Option, }, /// Replay must not apply the write's own pre-dispatch record: the apply /// wrote nothing, or another entry of this write set carries the row's @@ -82,16 +82,17 @@ pub enum EdgeImage { /// at exactly this key. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct RowVersion { - pub sys_from_ms: i64, + /// The version's system-time ordinal. + pub sys_from: i64, pub valid_from_ms: i64, pub valid_until_ms: i64, } impl RowVersion { - /// The key of a version valid for all time, written at `sys_from_ms`. - pub fn open(sys_from_ms: i64) -> Self { + /// The key of a version valid for all time, written at `sys_from`. + pub fn open(sys_from: i64) -> Self { Self { - sys_from_ms, + sys_from, valid_from_ms: i64::MIN, valid_until_ms: i64::MAX, } @@ -117,9 +118,7 @@ impl WriteSetEntry { Self { surrogate, identity, - effect: RowEffect::Delete { - system_from_ms: None, - }, + effect: RowEffect::Delete { system_from: None }, collection: None, } } @@ -155,8 +154,8 @@ impl WriteSetEntry { pub fn versioned(mut self, version: Option) -> Self { match &mut self.effect { RowEffect::Put { version: slot, .. } => *slot = version, - RowEffect::Delete { system_from_ms } => { - *system_from_ms = version.map(|v| v.sys_from_ms); + RowEffect::Delete { system_from } => { + *system_from = version.map(|v| v.sys_from); } RowEffect::CancelForward | RowEffect::Edge(_) => {} } diff --git a/nodedb/src/config/auth/jwt.rs b/nodedb/src/config/auth/jwt.rs index dcca68461..76b11784e 100644 --- a/nodedb/src/config/auth/jwt.rs +++ b/nodedb/src/config/auth/jwt.rs @@ -22,7 +22,11 @@ use serde::{Deserialize, Serialize}; /// audience = "nodedb" /// tenant_id = 42 /// ``` +/// +/// JWKS fetches go through the `[egress]` policy. A provider on a private +/// network needs its host on that allow-list. #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub struct JwtAuthConfig { /// JWKS refresh interval in seconds (default: 3600 = 1 hour). #[serde(default = "default_jwks_refresh")] @@ -106,23 +110,6 @@ pub struct JwtAuthConfig { /// Enforced by `control::security::jwt_policy::enforce_declared_scopes`. #[serde(default)] pub enforce_scopes: bool, - - /// SSRF relaxation: allow `http://` scheme for JWKS URLs whose host - /// is in [`Self::allow_jwks_hosts`]. Off by default. - #[serde(default)] - pub allow_http_jwks: bool, - - /// SSRF relaxation: hostnames that may resolve to addresses inside - /// [`Self::allow_jwks_cidrs`]. Exact-match, lowercase. IP literals - /// remain forbidden regardless of this list. - #[serde(default)] - pub allow_jwks_hosts: Vec, - - /// SSRF relaxation: CIDR ranges that [`Self::allow_jwks_hosts`] are - /// permitted to resolve into, in addition to global unicast. - /// Example: `["10.42.0.0/16"]` for an in-cluster Keycloak. - #[serde(default)] - pub allow_jwks_cidrs: Vec, } /// Configuration for a single JWT identity provider. @@ -131,7 +118,8 @@ pub struct JwtProviderConfig { /// Provider name (for logging and diagnostics). pub name: String, - /// JWKS endpoint URL. Must be HTTPS in production. + /// JWKS endpoint URL. It must pass the `[egress]` policy: `https://`, + /// or `http://` for a host on its allow-list. pub jwks_url: String, /// Expected `iss` claim. Empty = don't validate issuer for this provider. @@ -150,11 +138,11 @@ pub struct JwtProviderConfig { } impl JwtProviderConfig { - /// Validate provider config against a [`JwksPolicy`]. Fail-closed: + /// Validate provider config against the `[egress]` policy. Fail-closed: /// empty `issuer` is rejected; `jwks_url` must pass the policy. pub fn validate( &self, - policy: &crate::control::security::jwks::url::JwksPolicy, + policy: &crate::control::security::egress::EgressPolicy, ) -> crate::Result<()> { if self.name.trim().is_empty() { return Err(crate::Error::Config { @@ -181,35 +169,27 @@ impl JwtProviderConfig { } impl JwtAuthConfig { - /// Build the effective [`JwksPolicy`] from the allow-list fields. - pub fn jwks_policy( + /// Validate all providers against the `[egress]` policy. Called by the + /// server-config loader and JWKS registry construction so + /// misconfiguration fails before authentication. + pub fn validate( &self, - ) -> Result< - crate::control::security::jwks::url::JwksPolicy, - crate::control::security::jwks::url::UrlValidationError, - > { - crate::control::security::jwks::url::JwksPolicy::from_parts( - self.allow_http_jwks, - &self.allow_jwks_hosts, - &self.allow_jwks_cidrs, - ) - } - - /// Validate all providers. Called by the server-config loader and JWKS - /// registry construction so misconfiguration fails before authentication. - pub fn validate(&self) -> crate::Result<()> { + policy: &crate::control::security::egress::EgressPolicy, + ) -> crate::Result<()> { + if self.jwks_refresh_secs == 0 { + return Err(crate::Error::Config { + detail: "auth.jwt jwks_refresh_secs must be greater than zero".into(), + }); + } if self.max_token_lifetime_secs == 0 { return Err(crate::Error::Config { detail: "auth.jwt max_token_lifetime_secs must be greater than zero".into(), }); } crate::control::security::jwt_policy::validate_claim_remap(&self.claims)?; - let policy = self.jwks_policy().map_err(|e| crate::Error::Config { - detail: format!("auth.jwt allow-list is invalid: {e}"), - })?; let mut provider_names = HashSet::with_capacity(self.providers.len()); for provider in &self.providers { - provider.validate(&policy)?; + provider.validate(policy)?; if !provider_names.insert(provider.name.as_str()) { return Err(crate::Error::Config { detail: format!( @@ -285,9 +265,6 @@ impl Default for JwtAuthConfig { status_claim: None, blocked_statuses: Vec::new(), enforce_scopes: false, - allow_http_jwks: false, - allow_jwks_hosts: Vec::new(), - allow_jwks_cidrs: Vec::new(), } } } @@ -317,7 +294,9 @@ mod tests { upn = "email" "#, ); - config.validate().expect("section must be valid"); + config + .validate(&crate::control::security::egress::EgressPolicy::strict()) + .expect("section must be valid"); assert!(config.jit_provisioning); assert!(!config.jit_sync_claims); @@ -336,7 +315,7 @@ mod tests { "#, ); let err = config - .validate() + .validate(&crate::control::security::egress::EgressPolicy::strict()) .expect_err("a mapping no reader consults must fail startup"); assert!(err.to_string().contains("unknown field")); } diff --git a/nodedb/src/config/server/config.rs b/nodedb/src/config/server/config.rs index 21e916b5f..631b59eb2 100644 --- a/nodedb/src/config/server/config.rs +++ b/nodedb/src/config/server/config.rs @@ -13,6 +13,7 @@ use super::backup_storage::BackupStorageSettings; use super::checkpoint::CheckpointSettings; use super::cluster::ClusterSettings; use super::cold_storage::ColdStorageSettings; +use super::egress::{CredentialEncryptionSettings, EgressSettings}; use super::observability::ObservabilityConfig; use super::pitr::PitrSettings; use super::retention::RetentionSettings; @@ -74,6 +75,16 @@ pub struct ServerConfig { #[serde(default)] pub backup_encryption: Option, + /// Hosts a remote `COPY` source may reach. Absent = strict: `https://`, + /// DNS names, global unicast addresses only. + #[serde(default)] + pub egress: EgressSettings, + + /// The key `CREATE CREDENTIAL` seals secrets under. Absent = `CREATE + /// CREDENTIAL` is refused. + #[serde(default)] + pub credential_encryption: Option, + /// Object-store access for `BACKUP DATABASE` and `RESTORE DATABASE`. /// Absent = every `file://` URI is refused, and `s3://` URIs use IAM /// credentials against AWS. @@ -185,8 +196,12 @@ impl ServerConfig { /// Validate cross-field invariants that serde cannot express. Called /// from [`Self::from_file`] so misconfiguration fails startup. pub fn validate(&self) -> crate::Result<()> { + let egress = self.egress.checked_policy()?; + if let Some(ref encryption) = self.encryption { + encryption.source()?; + } if let Some(ref jwt) = self.auth.jwt { - jwt.validate()?; + jwt.validate(&egress)?; } super::domain::validate_domain(self) } diff --git a/nodedb/src/config/server/domain.rs b/nodedb/src/config/server/domain.rs index 188584526..9c0f1275f 100644 --- a/nodedb/src/config/server/domain.rs +++ b/nodedb/src/config/server/domain.rs @@ -115,6 +115,7 @@ pub(super) fn validate_domain(config: &ServerConfig) -> crate::Result<()> { )?; super::pitr::validate_pitr(config)?; super::backup::validate_backup(config)?; + super::egress::validate_egress(&config.egress)?; let s = &config.tuning.startup; startup_bound( diff --git a/nodedb/src/config/server/egress.rs b/nodedb/src/config/server/egress.rs new file mode 100644 index 000000000..4a52872cc --- /dev/null +++ b/nodedb/src/config/server/egress.rs @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! `[egress]` and `[credential_encryption]`: what an outbound request of the +//! server may reach, and the key `COPY` source credentials are sealed under. +//! +//! One `[egress]` policy governs every outbound request: remote `COPY` +//! sources, JWKS fetches, change-stream and alert webhooks, SIEM export, +//! OTLP trace and metrics export, and the Vault key provider. A URL the +//! operator configures (SIEM, OTLP, Vault) needs its host on `allow_hosts` +//! when it is plain `http://` or resolves to a private address. +//! +//! ```toml +//! [egress] +//! # Every relaxation names hosts. http:// is allowed only for these hosts, +//! # and only these hosts may resolve into allow_cidrs. +//! allow_http = false +//! allow_hosts = ["mirror.internal", "keycloak.internal", "otel-collector"] +//! allow_cidrs = ["10.42.0.0/16"] +//! +//! [credential_encryption] +//! # 32 bytes, mode 0600, owned by the server's user. Every node of a +//! # cluster needs the same file. Generate with: +//! # head -c 32 /dev/urandom > /etc/nodedb/keys/credential.key +//! key_path = "/etc/nodedb/keys/credential.key" +//! ``` +//! +//! With no `[egress]` section the policy is strict: `https://` only, DNS +//! names only, and every resolved address must be global unicast. With no +//! `[credential_encryption]` section, `CREATE CREDENTIAL` is refused. + +use std::path::PathBuf; + +use serde::{Deserialize, Serialize}; + +use crate::control::security::egress::{EgressError, EgressPolicy}; + +/// The `[egress]` allow-list of every outbound request. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct EgressSettings { + /// Allow `http://` for the hosts in `allow_hosts`. + #[serde(default)] + pub allow_http: bool, + /// Hosts that may use `http://` when `allow_http` is set, and may + /// resolve into `allow_cidrs`. + #[serde(default)] + pub allow_hosts: Vec, + /// Address blocks `allow_hosts` may resolve into beyond global unicast, + /// such as `"10.42.0.0/16"` or `"127.0.0.0/8"` for a local mirror. + #[serde(default)] + pub allow_cidrs: Vec, +} + +impl EgressSettings { + /// The policy these settings describe. + pub fn policy(&self) -> Result { + EgressPolicy::from_parts(self.allow_http, &self.allow_hosts, &self.allow_cidrs) + } + + /// The policy, or a config error that names the section. + pub fn checked_policy(&self) -> crate::Result { + self.policy().map_err(|e| crate::Error::Config { + detail: format!("[egress] is invalid: {e}"), + }) + } +} + +/// The `[credential_encryption]` key file. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct CredentialEncryptionSettings { + /// Path to the 32-byte key that seals credential secrets. + pub key_path: PathBuf, +} + +/// Check the `[egress]` allow-list at startup. +pub(super) fn validate_egress(settings: &EgressSettings) -> crate::Result<()> { + settings.checked_policy().map(|_| ()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn an_absent_section_is_strict_and_a_bad_cidr_is_refused() { + assert_eq!( + EgressSettings::default().policy(), + Ok(EgressPolicy::strict()) + ); + let bad = EgressSettings { + allow_cidrs: vec!["10.0.0.0/99".into()], + ..Default::default() + }; + assert!(validate_egress(&bad).is_err()); + let toml = + "allow_http = true\nallow_hosts = [\"localhost\"]\nallow_cidrs = [\"127.0.0.0/8\"]\n"; + let parsed: EgressSettings = toml::from_str(toml).expect("parses"); + assert!(parsed.policy().expect("valid").allow_http); + } +} diff --git a/nodedb/src/config/server/mod.rs b/nodedb/src/config/server/mod.rs index 6b0fe00a6..bdeb321c3 100644 --- a/nodedb/src/config/server/mod.rs +++ b/nodedb/src/config/server/mod.rs @@ -7,6 +7,7 @@ mod cluster; mod cold_storage; mod config; mod domain; +mod egress; mod env; mod env_expand; mod log_format; @@ -28,6 +29,7 @@ pub use checkpoint::CheckpointSettings; pub use cluster::{ClusterSettings, TlsPaths}; pub use cold_storage::ColdStorageSettings; pub use config::ServerConfig; +pub use egress::{CredentialEncryptionSettings, EgressSettings}; pub use env::{apply_env_overrides, parse_memory_size, parse_seed_nodes}; pub use log_format::LogFormat; pub use observability::{ diff --git a/nodedb/src/config/server/tls.rs b/nodedb/src/config/server/tls.rs index 6089b7a07..4ee4d1bd0 100644 --- a/nodedb/src/config/server/tls.rs +++ b/nodedb/src/config/server/tls.rs @@ -1,15 +1,79 @@ // SPDX-License-Identifier: BUSL-1.1 -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use serde::{Deserialize, Serialize}; -/// Encryption at rest settings (WAL). +use crate::control::security::keystore::KeyDerivation; + +/// Encryption at rest settings: the WAL key, which also derives the segment +/// and checkpoint keys. +/// +/// The key comes from exactly one source: +/// +/// ```toml +/// [encryption] +/// # A 32-byte key file, the default source. +/// key_path = "/etc/nodedb/keys/wal.key" +/// ``` +/// +/// ```toml +/// [encryption.key_derivation] +/// # A key-management service: type = "vault", "aws_kms" or "file". +/// type = "vault" +/// addr = "https://vault.internal:8200" +/// token_path = "/etc/nodedb/keys/vault.token" +/// key_name = "nodedb-wal" +/// mount = "transit" +/// ciphertext_blob_path = "/var/lib/nodedb/.wal-key.wrapped" +/// ``` #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub struct EncryptionSettings { /// Path to the 32-byte AES-256-GCM key file used to encrypt WAL records. /// Generate with: `head -c 32 /dev/urandom > /etc/nodedb/keys/wal.key` - pub key_path: PathBuf, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub key_path: Option, + /// A key-management service that supplies the key in place of + /// `key_path`. A Vault address goes through the `[egress]` policy. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub key_derivation: Option, +} + +impl EncryptionSettings { + /// Settings that read the key from the file at `key_path`. + pub fn file(key_path: PathBuf) -> Self { + Self { + key_path: Some(key_path), + key_derivation: None, + } + } + + /// The key source these settings name. Exactly one of `key_path` and + /// `key_derivation` must be set. + pub fn source(&self) -> crate::Result { + match (&self.key_path, &self.key_derivation) { + (Some(key_path), None) => Ok(KeyDerivation::File { + key_path: key_path.clone(), + }), + (None, Some(derivation)) => Ok(derivation.clone()), + (Some(_), Some(_)) => Err(crate::Error::Config { + detail: "[encryption] sets both key_path and key_derivation; keep one".into(), + }), + (None, None) => Err(crate::Error::Config { + detail: "[encryption] needs key_path or a [encryption.key_derivation] table".into(), + }), + } + } + + /// The key file, when the key comes from a local file. + pub fn key_file(&self) -> Option<&Path> { + match (&self.key_path, &self.key_derivation) { + (Some(key_path), _) => Some(key_path), + (None, Some(KeyDerivation::File { key_path })) => Some(key_path), + (None, _) => None, + } + } } /// Per-backup encryption settings. @@ -67,3 +131,76 @@ pub struct TlsSettings { fn default_true() -> bool { true } + +#[cfg(test)] +mod tests { + use super::*; + + fn parse(body: &str) -> EncryptionSettings { + toml::from_str(body).expect("the [encryption] section must deserialize") + } + + #[test] + fn key_path_selects_the_file_source() { + let settings = parse(r#"key_path = "/k/wal.key""#); + assert!(matches!( + settings.source(), + Ok(KeyDerivation::File { ref key_path }) if key_path == Path::new("/k/wal.key") + )); + assert_eq!(settings.key_file(), Some(Path::new("/k/wal.key"))); + } + + #[test] + fn key_derivation_selects_vault_kms_or_file() { + let vault = parse( + r#" + [key_derivation] + type = "vault" + addr = "https://vault.internal:8200" + token_path = "/k/token" + key_name = "wal" + mount = "transit" + ciphertext_blob_path = "/k/blob" + "#, + ); + assert!(matches!(vault.source(), Ok(KeyDerivation::Vault { .. }))); + assert_eq!(vault.key_file(), None); + + let kms = parse( + r#" + [key_derivation] + type = "aws_kms" + key_id = "arn:aws:kms:us-east-1:1:key/k" + region = "us-east-1" + ciphertext_blob_path = "/k/blob" + "#, + ); + assert!(matches!(kms.source(), Ok(KeyDerivation::AwsKms { .. }))); + assert_eq!(kms.key_file(), None); + + let file = parse( + r#" + [key_derivation] + type = "file" + key_path = "/k/wal.key" + "#, + ); + assert!(matches!(file.source(), Ok(KeyDerivation::File { .. }))); + assert_eq!(file.key_file(), Some(Path::new("/k/wal.key"))); + } + + #[test] + fn exactly_one_source_is_required() { + let both = parse( + r#" + key_path = "/k/wal.key" + [key_derivation] + type = "file" + key_path = "/k/other.key" + "#, + ); + assert!(both.source().is_err()); + assert!(parse("").source().is_err()); + assert!(toml::from_str::(r#"keypath = "/k""#).is_err()); + } +} diff --git a/nodedb/src/control/array_catalog/attr_meta.rs b/nodedb/src/control/array_catalog/attr_meta.rs new file mode 100644 index 000000000..110a4d943 --- /dev/null +++ b/nodedb/src/control/array_catalog/attr_meta.rs @@ -0,0 +1,173 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! Conversions between the engine attr metadata and its SQL form, and the +//! schema re-encode an `ALTER ARRAY ... ALTER ATTR` makes. +//! +//! Attr metadata is not part of `content_msgpack`, so a change keeps +//! `schema_hash` and every segment valid, as a georef change does. A value +//! that does not fit the attr type is SQLSTATE `22000`. + +use nodedb_array::schema::AttrMeta; +use nodedb_sql::types_array::{ArrayAttrMetaAlter, ArrayAttrMetaAst}; + +use super::schema_recode::{decode_schema, reencode_checked, schema_error}; + +/// The SQL form of `meta`. +pub(crate) fn meta_ast(meta: &AttrMeta) -> ArrayAttrMetaAst { + ArrayAttrMetaAst { + nodata: meta.nodata, + scale: meta.scale, + offset: meta.offset, + units: meta.units.clone(), + } +} + +/// The engine form of `ast`. +pub(crate) fn meta_from_ast(ast: &ArrayAttrMetaAst) -> AttrMeta { + AttrMeta { + nodata: ast.nodata, + scale: ast.scale, + offset: ast.offset, + units: ast.units.clone(), + } +} + +/// The stored schema `stored_msgpack` with `change` applied, re-encoded. +/// +/// The content bytes stay equal, so `schema_hash` and every segment stay +/// valid. +pub(crate) fn remeta_schema( + statement: &str, + stored_msgpack: &[u8], + change: &ArrayAttrMetaAlter, +) -> crate::Result> { + let schema = decode_schema(statement, stored_msgpack)?; + let current = schema + .attr(&change.attr) + .map(|a| meta_ast(&a.meta)) + .ok_or_else(|| crate::Error::UndefinedColumn { + column: change.attr.clone(), + })?; + let next = meta_from_ast(&change.change.apply(¤t)); + let altered = schema + .with_attr_meta(&change.attr, next) + .map_err(|e| schema_error(statement, e))?; + reencode_checked(statement, stored_msgpack, &altered) +} + +/// The metadata clauses DESCRIBE shows after an attr type, in the DDL form +/// that recreates them: ` NODATA 0.0 SCALE 2.75e-5 UNITS 'reflectance'`. +/// Empty when the attr has none. +pub(crate) fn meta_sql(meta: &ArrayAttrMetaAst) -> String { + let number = |v: f64| { + if v.is_nan() { + "NAN".to_string() + } else if v.is_infinite() { + // The lexer reads `-` only before a digit, so negative + // infinity takes the quoted form. + if v > 0.0 { "INF" } else { "'-INF'" }.to_string() + } else { + format!("{v:?}") + } + }; + let mut out = String::new(); + if let Some(v) = meta.nodata { + out.push_str(&format!(" NODATA {}", number(v))); + } + if let Some(v) = meta.scale { + out.push_str(&format!(" SCALE {}", number(v))); + } + if let Some(v) = meta.offset { + out.push_str(&format!(" OFFSET {}", number(v))); + } + if let Some(units) = &meta.units { + out.push_str(&format!(" UNITS '{}'", units.replace('\'', "''"))); + } + out +} + +#[cfg(test)] +mod tests { + use super::*; + use nodedb_array::schema::{ + ArraySchema, ArraySchemaBuilder, AttrSpec, AttrType, DimSpec, DimType, + }; + use nodedb_array::types::domain::{Domain, DomainBound}; + use nodedb_sql::types_array::{ArrayAttrMetaChange, ArrayAttrMetaKey}; + + fn stored() -> ArraySchema { + ArraySchemaBuilder::new("scene") + .dim(DimSpec::new( + "row", + DimType::Int64, + Domain::new(DomainBound::Int64(0), DomainBound::Int64(1023)), + )) + .attr(AttrSpec::new("red", AttrType::UInt16, true)) + .tile_extents(vec![256]) + .build() + .unwrap() + } + + fn set(meta: ArrayAttrMetaAst) -> ArrayAttrMetaAlter { + ArrayAttrMetaAlter { + attr: "red".into(), + change: ArrayAttrMetaChange::Set(meta), + } + } + + #[test] + fn set_then_drop_keeps_content() { + let plain = zerompk::to_msgpack_vec(&stored()).unwrap(); + let landsat = ArrayAttrMetaAst { + nodata: Some(0.0), + scale: Some(0.0000275), + offset: Some(-0.2), + units: Some("reflectance".into()), + }; + let with = remeta_schema("ALTER ARRAY scene", &plain, &set(landsat.clone())).unwrap(); + let schema: ArraySchema = zerompk::from_msgpack(&with).unwrap(); + assert_eq!(meta_ast(&schema.attrs[0].meta), landsat); + assert_eq!( + schema.content_msgpack().unwrap(), + stored().content_msgpack().unwrap() + ); + let drop_all = ArrayAttrMetaAlter { + attr: "red".into(), + change: ArrayAttrMetaChange::Drop(vec![ + ArrayAttrMetaKey::Nodata, + ArrayAttrMetaKey::Scale, + ArrayAttrMetaKey::Offset, + ArrayAttrMetaKey::Units, + ]), + }; + let dropped = remeta_schema("ALTER ARRAY scene", &with, &drop_all).unwrap(); + assert_eq!(dropped, plain); + } + + #[test] + fn an_unrepresentable_nodata_is_data_exception() { + let plain = zerompk::to_msgpack_vec(&stored()).unwrap(); + let err = remeta_schema( + "ALTER ARRAY scene", + &plain, + &set(ArrayAttrMetaAst { + nodata: Some(-1.0), + ..ArrayAttrMetaAst::default() + }), + ) + .unwrap_err(); + assert!(matches!(err, crate::Error::DataException { .. }), "{err:?}"); + } + + #[test] + fn sql_form_names_every_clause() { + let text = meta_sql(&ArrayAttrMetaAst { + nodata: Some(f64::NAN), + scale: Some(0.5), + offset: None, + units: Some("it's".into()), + }); + assert_eq!(text, " NODATA NAN SCALE 0.5 UNITS 'it''s'"); + assert_eq!(meta_sql(&ArrayAttrMetaAst::default()), ""); + } +} diff --git a/nodedb/src/control/array_catalog/ddl.rs b/nodedb/src/control/array_catalog/ddl.rs index 64ed21fb8..21ac2ae8d 100644 --- a/nodedb/src/control/array_catalog/ddl.rs +++ b/nodedb/src/control/array_catalog/ddl.rs @@ -17,8 +17,8 @@ use nodedb_types::Hlc; use crate::bridge::envelope::{Payload, PhysicalPlan, Response, Status}; use crate::control::array_catalog::ArrayCatalogEntry; -use crate::control::array_catalog::georef::check_same_content; use crate::control::array_catalog::retention::check_retention_floor; +use crate::control::array_catalog::schema_recode::check_same_content; use crate::control::catalog_entry::CatalogEntry; use crate::control::metadata_proposer::propose_catalog_batch_async; use crate::control::security::catalog::SystemCatalog; @@ -172,7 +172,8 @@ fn entry_for( *audit_retain_ms, *minimum_audit_retain_ms, )?; - // A georef change keeps every field `schema_hash` covers. Checked + // A georef or attr metadata change keeps every field + // `schema_hash` covers. Checked // against the committed schema, so the kept hash stays true. let schema_msgpack = match schema_msgpack { Some(altered) => { @@ -350,7 +351,7 @@ mod tests { let catalog = store.catalog(); let created = put_raster(catalog); let georef = nodedb_array::schema::ArrayGeoref { - srid: nodedb_types::geometry::Srid::Utm47N, + srid: nodedb_types::geometry::Srid::new(32647).expect("srid"), x_dim: 1, y_dim: 0, transform: Some(nodedb_array::schema::GeoTransform([ diff --git a/nodedb/src/control/array_catalog/georef.rs b/nodedb/src/control/array_catalog/georef.rs index c817e4d3b..59370939e 100644 --- a/nodedb/src/control/array_catalog/georef.rs +++ b/nodedb/src/control/array_catalog/georef.rs @@ -6,10 +6,10 @@ //! name. Every refusal carries its own SQLSTATE class: `42703` for an //! unknown dim, `22000` for every other georef error. -use nodedb_array::ArrayError; use nodedb_array::schema::{ArrayGeoref, ArraySchema, DimSpec, GeoTransform}; use nodedb_sql::types_array::{ArrayGeorefAlter, ArrayGeorefAst}; -use nodedb_types::geometry::Srid; + +use super::schema_recode::{decode_schema, reencode_checked, schema_error}; /// The SQL form of `schema`'s georef, axes named by dim. /// @@ -36,10 +36,13 @@ pub(crate) fn georef_from_ast( array: &str, ast: &ArrayGeorefAst, dims: &[DimSpec], + crs: &nodedb_crs::CrsContext<'_>, ) -> crate::Result { - let srid = Srid::try_from(ast.srid).map_err(|e| crate::Error::DataException { - detail: format!("array '{array}' GEOREF: {e}"), - })?; + let srid = crs + .resolve_u32(ast.srid) + .map_err(|e| crate::Error::DataException { + detail: format!("array '{array}' GEOREF: {e}"), + })?; let index = |name: &str| { dims.iter() .position(|d| d.name == name) @@ -63,63 +66,17 @@ pub(crate) fn regeoref_schema( statement: &str, stored_msgpack: &[u8], change: &ArrayGeorefAlter, + crs: &nodedb_crs::CrsContext<'_>, ) -> crate::Result> { let schema = decode_schema(statement, stored_msgpack)?; let georef = match change { - ArrayGeorefAlter::Set(ast) => Some(georef_from_ast(&schema.name, ast, &schema.dims)?), + ArrayGeorefAlter::Set(ast) => Some(georef_from_ast(&schema.name, ast, &schema.dims, crs)?), ArrayGeorefAlter::Drop => None, }; let altered = schema .with_georef(georef) .map_err(|e| schema_error(statement, e))?; - let altered_msgpack = - zerompk::to_msgpack_vec(&altered).map_err(|e| crate::Error::Serialization { - format: "msgpack".into(), - detail: format!("{statement}: schema encode: {e}"), - })?; - check_same_content(statement, stored_msgpack, &altered_msgpack)?; - Ok(altered_msgpack) -} - -/// Check that `altered_msgpack` keeps the content of `stored_msgpack`: every -/// field `schema_hash` covers. Only the georef may differ. -pub(crate) fn check_same_content( - statement: &str, - stored_msgpack: &[u8], - altered_msgpack: &[u8], -) -> crate::Result<()> { - let stored = decode_schema(statement, stored_msgpack)?; - let altered = decode_schema(statement, altered_msgpack)?; - if stored.name != altered.name || stored.content_msgpack() != altered.content_msgpack() { - return Err(crate::Error::Internal { - detail: format!( - "{statement}: the altered schema changes more than the georef; \ - schema_hash would no longer match" - ), - }); - } - Ok(()) -} - -fn decode_schema(statement: &str, msgpack: &[u8]) -> crate::Result { - zerompk::from_msgpack(msgpack).map_err(|e| crate::Error::Serialization { - format: "msgpack".into(), - detail: format!("{statement}: array schema decode: {e}"), - }) -} - -/// The error a schema build or georef change reports. -/// -/// A georef refusal is `22000`. Every other schema refusal is a plan error. -pub(crate) fn schema_error(statement: &str, error: ArrayError) -> crate::Error { - match error { - ArrayError::InvalidGeoref { .. } => crate::Error::DataException { - detail: format!("{statement}: {error}"), - }, - other => crate::Error::PlanError { - detail: format!("{statement}: {other}"), - }, - } + reencode_checked(statement, stored_msgpack, &altered) } /// The clause body DESCRIBE shows, in the DDL form that recreates it. @@ -134,7 +91,9 @@ pub(crate) fn georef_sql(ast: &ArrayGeorefAst) -> String { #[cfg(test)] mod tests { + use super::super::schema_recode::check_same_content; use super::*; + use nodedb_array::ArrayError; use nodedb_array::schema::{ArraySchemaBuilder, AttrSpec, AttrType, DimType}; use nodedb_array::types::domain::{Domain, DomainBound}; @@ -169,7 +128,13 @@ mod tests { #[test] fn ast_round_trips_through_the_engine_georef() { - let georef = georef_from_ast("scene", &ast(), &dims()).unwrap(); + let georef = georef_from_ast( + "scene", + &ast(), + &dims(), + &nodedb_crs::CrsContext::builtin_only(), + ) + .unwrap(); assert_eq!(georef.x_dim, 1); assert_eq!(georef.y_dim, 0); let mut builder = ArraySchemaBuilder::new("scene"); @@ -189,7 +154,13 @@ mod tests { fn unknown_dim_is_undefined_column() { let mut bad = ast(); bad.x_dim = "lon".into(); - let err = georef_from_ast("scene", &bad, &dims()).unwrap_err(); + let err = georef_from_ast( + "scene", + &bad, + &dims(), + &nodedb_crs::CrsContext::builtin_only(), + ) + .unwrap_err(); assert!( matches!(err, crate::Error::UndefinedColumn { .. }), "{err:?}" @@ -199,8 +170,14 @@ mod tests { #[test] fn unsupported_srid_is_data_exception() { let mut bad = ast(); - bad.srid = 2154; - let err = georef_from_ast("scene", &bad, &dims()).unwrap_err(); + bad.srid = 999; + let err = georef_from_ast( + "scene", + &bad, + &dims(), + &nodedb_crs::CrsContext::builtin_only(), + ) + .unwrap_err(); assert!(matches!(err, crate::Error::DataException { .. }), "{err:?}"); } @@ -231,12 +208,26 @@ mod tests { #[test] fn regeoref_set_then_drop_keeps_content() { let plain = zerompk::to_msgpack_vec(&stored()).unwrap(); - let set = - regeoref_schema("ALTER ARRAY scene", &plain, &ArrayGeorefAlter::Set(ast())).unwrap(); + let set = regeoref_schema( + "ALTER ARRAY scene", + &plain, + &ArrayGeorefAlter::Set(ast()), + &nodedb_crs::CrsContext::builtin_only(), + ) + .unwrap(); let with: ArraySchema = zerompk::from_msgpack(&set).unwrap(); assert_eq!(georef_ast(&with), Some(ast())); - assert_eq!(with.content_msgpack(), stored().content_msgpack()); - let dropped = regeoref_schema("ALTER ARRAY scene", &set, &ArrayGeorefAlter::Drop).unwrap(); + assert_eq!( + with.content_msgpack().unwrap(), + stored().content_msgpack().unwrap() + ); + let dropped = regeoref_schema( + "ALTER ARRAY scene", + &set, + &ArrayGeorefAlter::Drop, + &nodedb_crs::CrsContext::builtin_only(), + ) + .unwrap(); assert_eq!(dropped, plain); } @@ -245,8 +236,13 @@ mod tests { let plain = zerompk::to_msgpack_vec(&stored()).unwrap(); let mut unknown = ast(); unknown.y_dim = "lat".into(); - let err = regeoref_schema("ALTER ARRAY scene", &plain, &ArrayGeorefAlter::Set(unknown)) - .unwrap_err(); + let err = regeoref_schema( + "ALTER ARRAY scene", + &plain, + &ArrayGeorefAlter::Set(unknown), + &nodedb_crs::CrsContext::builtin_only(), + ) + .unwrap_err(); assert!( matches!(err, crate::Error::UndefinedColumn { .. }), "{err:?}" @@ -257,6 +253,7 @@ mod tests { "ALTER ARRAY scene", &plain, &ArrayGeorefAlter::Set(singular), + &nodedb_crs::CrsContext::builtin_only(), ) .unwrap_err(); assert!(matches!(err, crate::Error::DataException { .. }), "{err:?}"); diff --git a/nodedb/src/control/array_catalog/mod.rs b/nodedb/src/control/array_catalog/mod.rs index 2d1802b20..c4a18b4cd 100644 --- a/nodedb/src/control/array_catalog/mod.rs +++ b/nodedb/src/control/array_catalog/mod.rs @@ -7,6 +7,7 @@ //! The Data Plane never touches this directly — dispatch handlers //! consult the in-memory registry via the shared [`ArrayCatalogHandle`]. +pub(crate) mod attr_meta; pub(crate) mod cell_route; pub(crate) mod ddl; pub mod entry; @@ -14,6 +15,7 @@ pub(crate) mod georef; pub mod persist; pub mod registry; pub(crate) mod retention; +pub(crate) mod schema_recode; pub use entry::{ArrayCatalogEntry, ArrayMove}; pub use registry::{ArrayCatalog, ArrayCatalogHandle}; diff --git a/nodedb/src/control/array_catalog/schema_recode.rs b/nodedb/src/control/array_catalog/schema_recode.rs new file mode 100644 index 000000000..2858a4572 --- /dev/null +++ b/nodedb/src/control/array_catalog/schema_recode.rs @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! The decode, check and re-encode every array schema change shares. +//! +//! A georef or attr metadata change leaves `content_msgpack` as it is, so +//! `schema_hash` and every segment stay valid. Each change re-encodes the +//! schema and checks that the content bytes did not move. + +use nodedb_array::ArrayError; +use nodedb_array::schema::ArraySchema; + +/// The stored schema `msgpack` holds. +pub(crate) fn decode_schema(statement: &str, msgpack: &[u8]) -> crate::Result { + zerompk::from_msgpack(msgpack).map_err(|e| crate::Error::Serialization { + format: "msgpack".into(), + detail: format!("{statement}: array schema decode: {e}"), + }) +} + +/// The error a schema build, georef change or attr metadata change reports. +/// +/// A georef or attr metadata refusal is `22000`. Every other schema +/// refusal is a plan error. +pub(crate) fn schema_error(statement: &str, error: ArrayError) -> crate::Error { + match error { + ArrayError::InvalidGeoref { .. } | ArrayError::InvalidAttrMeta { .. } => { + crate::Error::DataException { + detail: format!("{statement}: {error}"), + } + } + other => crate::Error::PlanError { + detail: format!("{statement}: {other}"), + }, + } +} + +/// Check that `altered_msgpack` keeps the content of `stored_msgpack`: every +/// field `schema_hash` covers. Only the georef and attr metadata can differ. +pub(crate) fn check_same_content( + statement: &str, + stored_msgpack: &[u8], + altered_msgpack: &[u8], +) -> crate::Result<()> { + let stored = decode_schema(statement, stored_msgpack)?; + let altered = decode_schema(statement, altered_msgpack)?; + let content = |schema: &ArraySchema| { + schema + .content_msgpack() + .map_err(|e| schema_error(statement, e)) + }; + if stored.name != altered.name || content(&stored)? != content(&altered)? { + return Err(crate::Error::Internal { + detail: format!( + "{statement}: the altered schema changes more than the georef and attr \ + metadata, so schema_hash no longer matches" + ), + }); + } + Ok(()) +} + +/// `altered` encoded, checked to keep the content of `stored_msgpack`. +pub(crate) fn reencode_checked( + statement: &str, + stored_msgpack: &[u8], + altered: &ArraySchema, +) -> crate::Result> { + let altered_msgpack = + zerompk::to_msgpack_vec(altered).map_err(|e| crate::Error::Serialization { + format: "msgpack".into(), + detail: format!("{statement}: schema encode: {e}"), + })?; + check_same_content(statement, stored_msgpack, &altered_msgpack)?; + Ok(altered_msgpack) +} diff --git a/nodedb/src/control/array_sync/raft_apply/common.rs b/nodedb/src/control/array_sync/raft_apply/common.rs index 4843d16cc..11b65bc21 100644 --- a/nodedb/src/control/array_sync/raft_apply/common.rs +++ b/nodedb/src/control/array_sync/raft_apply/common.rs @@ -140,6 +140,8 @@ pub(super) async fn submit_array_write( group_id, log_index, )), + // An array cell version takes no system-time ordinal. + system_ordinal: None, }, // Raft committed this entry at a fixed log index and every replica // applies it in that order; re-entering the write-admission gate diff --git a/nodedb/src/control/backup/restore/columnar_reissue.rs b/nodedb/src/control/backup/restore/columnar_reissue.rs index f4fa03c80..f374ac3eb 100644 --- a/nodedb/src/control/backup/restore/columnar_reissue.rs +++ b/nodedb/src/control/backup/restore/columnar_reissue.rs @@ -62,22 +62,10 @@ pub fn decode_snapshot_live_rows( let mut rows: Vec = Vec::new(); let mut surrogates: Vec = Vec::new(); - // Memtable: non-deleted rows in schema column order → keyed object. - // A memtable row that does not read refuses the restore: dropping it - // would lose the row from the restored collection. - for scanned in engine.scan_memtable_rows_with_surrogates() { - let (surrogate, values) = scanned?; - let surrogate = surrogate.ok_or_else(|| Error::Storage { - engine: "columnar".into(), - detail: format!( - "restore reissue: memtable row for '{collection}' has no surrogate; \ - cannot preserve cross-engine identity" - ), - })?; - rows.push(row_values_to_object(&column_names, values, collection)?); - surrogates.push(surrogate); - } - + // Rows re-issue in the order the engine appended them: flushed segments + // in flush order, then the memtable. A bitemporal collection keeps every + // version of a key, and the primary-key index binds the version appended + // last, so this order binds each key to its newest version again. // Flushed segments: blob index i corresponds to segment_id i + 1 (the // export/flush convention). The per-segment delete bitmap lives on the // rebuilt engine under that segment_id; the surrogate sidecar is parallel to @@ -112,6 +100,22 @@ pub fn decode_snapshot_live_rows( } } + // Memtable: non-deleted rows in schema column order → keyed object. + // A memtable row that does not read refuses the restore: dropping it + // would lose the row from the restored collection. + for scanned in engine.scan_memtable_rows_with_surrogates() { + let (surrogate, values) = scanned?; + let surrogate = surrogate.ok_or_else(|| Error::Storage { + engine: "columnar".into(), + detail: format!( + "restore reissue: memtable row for '{collection}' has no surrogate; \ + cannot preserve cross-engine identity" + ), + })?; + rows.push(row_values_to_object(&column_names, values, collection)?); + surrogates.push(surrogate); + } + Ok(DecodedColumnarRows { rows, surrogates, @@ -123,7 +127,9 @@ pub fn decode_snapshot_live_rows( /// /// The payload is the msgpack encoding of `Value::Array(rows)` (array of /// per-row field-keyed objects) — the exact shape the columnar insert handler -/// expects. +/// expects. The plan's intent is `Restore`: a bitemporal row keeps the +/// `_ts_system` ordinal it carries, so every restored version keeps the +/// system time its write stamped and `AS OF` answers as before the backup. pub fn build_columnar_insert_plan( collection: &str, decoded: DecodedColumnarRows, @@ -142,12 +148,13 @@ pub fn build_columnar_insert_plan( collection: nodedb_types::QualifiedCollection::from_stored(collection.to_string()), payload, format: "msgpack".into(), - intent: ColumnarInsertIntent::Insert, + intent: ColumnarInsertIntent::Restore, on_conflict_updates: Vec::new(), surrogates: decoded.surrogates, schema_bytes: decoded.schema_bytes, provenance: None, wal_lsn: None, + system_from: None, // No predicate here: a restore re-issues rows that were already // admitted before the backup was taken. The identity that admitted // them is not available during restore. diff --git a/nodedb/src/control/backup/restore/orchestrate/database.rs b/nodedb/src/control/backup/restore/orchestrate/database.rs index f8d4615a0..10bfb6310 100644 --- a/nodedb/src/control/backup/restore/orchestrate/database.rs +++ b/nodedb/src/control/backup/restore/orchestrate/database.rs @@ -128,6 +128,8 @@ pub(super) async fn reissue_database( super::super::redo_reissue::RestoredRows { documents, edges: std::mem::take(&mut snap.edges), + node_labels: std::mem::take(&mut snap.node_labels), + node_label_versions: std::mem::take(&mut snap.node_label_versions), }, ) .await?; diff --git a/nodedb/src/control/backup/restore/orchestrate/reissue.rs b/nodedb/src/control/backup/restore/orchestrate/reissue.rs index b6502334b..30d1534e5 100644 --- a/nodedb/src/control/backup/restore/orchestrate/reissue.rs +++ b/nodedb/src/control/backup/restore/orchestrate/reissue.rs @@ -181,6 +181,10 @@ pub(super) async fn reissue_columnar_snapshots( PhysicalPlan::Columnar(ColumnarOp::Truncate { collection: name.stored.clone(), restart_identity: false, + system_from: None, + // The restore reinstalls every version under the system time + // it carries, so the old history goes, bitemporal or not. + erase_history: true, }), ) .await?; diff --git a/nodedb/src/control/backup/restore/redo_reissue/commit.rs b/nodedb/src/control/backup/restore/redo_reissue/commit.rs index c1baba404..4aa2d5edc 100644 --- a/nodedb/src/control/backup/restore/redo_reissue/commit.rs +++ b/nodedb/src/control/backup/restore/redo_reissue/commit.rs @@ -8,7 +8,9 @@ //! writes, as their live writers lock them. Its resolve appends them to the //! transaction's redo record, each edge version applied at the transaction's //! ordinal, so a TRUNCATE sequenced before the RESTORE leaves them visible -//! and one sequenced after it hides them. Every replica binds the batch's +//! and one sequenced after it hides them. Restored node labels go to each +//! node's home, every label version at its system time. Every replica binds +//! the batch's //! identities, installs the record as a RESTORE, and raises the tenant's //! restore mark under the restore's id. The call returns once the //! transaction committed. @@ -26,6 +28,7 @@ use crate::event::EventSource; use crate::types::{DatabaseId, TenantId, VShardId}; use crate::wal::RedoRecord; +use super::labels::{HomedLabels, label_weight}; use super::units::{CollectionEdges, CollectionUnits, EdgeUnit, RowUnit}; /// Most sub-records or edge versions one restore batch carries. @@ -115,6 +118,7 @@ fn row_batch( rows_redo, rows, edges: Vec::new(), + node_labels: Vec::new(), collections: vec![stored.to_string()], identities, }) @@ -133,6 +137,7 @@ fn edge_batch(stored: &str, batch: Vec) -> Vec { rows_redo: Vec::new(), rows: Vec::new(), edges: Vec::new(), + node_labels: Vec::new(), collections: vec![stored.to_string()], identities: Vec::new(), }, @@ -264,6 +269,46 @@ pub(super) async fn commit_edges( Ok(transactions) } +/// Commit every restored node of `homed` in `database_id`, marked under +/// `restore_id`: one batch of a home's nodes per transaction. Returns the +/// transactions committed. +pub(super) async fn commit_labels( + state: &SharedState, + tenant_id: TenantId, + database_id: DatabaseId, + restore_id: u64, + homed: HomedLabels, +) -> crate::Result { + let mut transactions = 0usize; + for (vshard_id, nodes) in homed { + for batch in batch_units(nodes, label_weight) { + super::super::durable::log_reissue_step( + state, + "node labels", + "", + vshard_id, + batch.len(), + ); + let plan = RestoredRedo { + vshard: vshard_id.as_u32(), + rows_redo: Vec::new(), + rows: Vec::new(), + edges: Vec::new(), + node_labels: batch, + collections: Vec::new(), + identities: Vec::new(), + }; + commit_batch(state, tenant_id, database_id, restore_id, vec![plan]) + .await + .map_err(|e| { + in_context(e, &format!("node labels to vShard {}", vshard_id.as_u32())) + })?; + transactions += 1; + } + } + Ok(transactions) +} + #[cfg(test)] mod tests { use nodedb_physical::physical_plan::RestoredEdgeVersion; @@ -302,6 +347,7 @@ mod tests { dst_surrogate: 2, system_from, properties: Some(Vec::new()), + valid_time: Some((0, i64::MAX)), }, identities: vec![ CarriedIdentity { diff --git a/nodedb/src/control/backup/restore/redo_reissue/documents.rs b/nodedb/src/control/backup/restore/redo_reissue/documents.rs index c1252002e..1eeda0aa1 100644 --- a/nodedb/src/control/backup/restore/redo_reissue/documents.rs +++ b/nodedb/src/control/backup/restore/redo_reissue/documents.rs @@ -38,7 +38,7 @@ use super::sub_record::VersionStamp; enum StoredRow { /// The one current body of a row with no history. Current(Vec), - /// Every `(sys_from_ms, versioned value)` of a bitemporal row. + /// Every `(sys_from, versioned value)` of a bitemporal row. Versions(Vec<(i64, Vec)>), } @@ -115,13 +115,13 @@ fn group_rows( let (db, collection, rest) = split_key(&key, tenant_id)?; let (hex, sys) = rest.split_once('\x00').ok_or_else(|| malformed(&key))?; let storage_key = StorageKey::parse(hex).ok_or_else(|| malformed(&key))?; - let sys_from_ms = sys.parse::().map_err(|_| malformed(&key))?; + let sys_from = sys.parse::().map_err(|_| malformed(&key))?; let rows = grouped.entry((db, collection.to_string())).or_default(); match rows .entry(storage_key) .or_insert_with(|| StoredRow::Versions(Vec::new())) { - StoredRow::Versions(versions) => versions.push((sys_from_ms, value)), + StoredRow::Versions(versions) => versions.push((sys_from, value)), StoredRow::Current(_) => { return Err(crate::Error::Serialization { format: "backup".into(), @@ -266,7 +266,7 @@ impl RowBuilder<'_> { fn versions(&self, key: StorageKey, versions: &[(i64, Vec)]) -> crate::Result { // Decode every version first: the identity comes from a live body. let mut decoded = Vec::with_capacity(versions.len()); - for (sys_from_ms, raw) in versions { + for (sys_from, raw) in versions { let version = decode_value(raw)?; let body = match version.tag { TAG_LIVE => Some(body_msgpack( @@ -280,7 +280,7 @@ impl RowBuilder<'_> { return Err(crate::Error::Serialization { format: "versioned-doc".into(), detail: format!( - "restore: version {sys_from_ms} of row {key} of '{}' carries tag \ + "restore: version {sys_from} of row {key} of '{}' carries tag \ {tag:#04x}, which no write path records", self.collection ), @@ -288,7 +288,7 @@ impl RowBuilder<'_> { } }; let stamp = VersionStamp { - sys_from_ms: *sys_from_ms, + sys_from: *sys_from, valid_from_ms: version.valid_from_ms, valid_until_ms: version.valid_until_ms, }; diff --git a/nodedb/src/control/backup/restore/redo_reissue/edges.rs b/nodedb/src/control/backup/restore/redo_reissue/edges.rs index 3049a0687..7b060dbce 100644 --- a/nodedb/src/control/backup/restore/redo_reissue/edges.rs +++ b/nodedb/src/control/backup/restore/redo_reissue/edges.rs @@ -119,10 +119,14 @@ fn edge_unit( ), }); } - let properties = if is_tombstone(value) { - None + let (properties, valid_time) = if is_tombstone(value) { + (None, None) } else { - Some(EdgeValuePayload::decode(value)?.properties) + let payload = EdgeValuePayload::decode(value)?; + ( + Some(payload.properties), + Some((payload.valid_from_ms, payload.valid_until_ms)), + ) }; let src = node_identity(resolved, collection, src_id)?; let dst = node_identity(resolved, collection, dst_id)?; @@ -136,6 +140,7 @@ fn edge_unit( dst_surrogate: dst.surrogate.as_u32(), system_from, properties, + valid_time, }, homes: RecordHomes::edge(src_id, dst_id), identities: vec![src, dst], diff --git a/nodedb/src/control/backup/restore/redo_reissue/labels.rs b/nodedb/src/control/backup/restore/redo_reissue/labels.rs new file mode 100644 index 000000000..cd9296871 --- /dev/null +++ b/nodedb/src/control/backup/restore/redo_reissue/labels.rs @@ -0,0 +1,132 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! Restored node labels as label units. +//! +//! A backup carries every node's current label set and every label version. +//! Each node re-issues to the owner home of its labels +//! ([`HomedRecord::NodeLabels`]), where every label write of the node +//! routes. Its versions re-issue in ascending system time, each at its +//! original system time, so the restored node keeps its label history. +//! Labels belong to no collection: a node keeps its id across the move into +//! the destination database. + +use std::collections::BTreeMap; + +use nodedb_physical::physical_plan::RestoredNodeLabels; + +use crate::types::{HomedRecord, NodeLabelVersionEntry, NodeLabelsEntry, RecordHomes, VShardId}; + +/// The restored nodes of each home vShard, in node order. +pub(super) type HomedLabels = BTreeMap>; + +fn foreign_tenant(found: u64, tenant_id: u64) -> crate::Error { + crate::Error::Serialization { + format: "backup".into(), + detail: format!( + "restore: a node label section of tenant {found} is in the backup of tenant \ + {tenant_id}" + ), + } +} + +/// Every node `current` or `versions` names, with its versions and its +/// current label set, grouped by home vShard. Every entry must belong to +/// `tenant_id`. +pub(super) fn label_units( + tenant_id: u64, + current: Vec, + versions: Vec, +) -> crate::Result { + let mut nodes: BTreeMap = BTreeMap::new(); + let mut by_ordinal: BTreeMap>> = BTreeMap::new(); + for (_, tid, node, ordinal, labels) in versions { + if tid != tenant_id { + return Err(foreign_tenant(tid, tenant_id)); + } + by_ordinal.entry(node).or_default().insert(ordinal, labels); + } + for (_, tid, node, labels) in current { + if tid != tenant_id { + return Err(foreign_tenant(tid, tenant_id)); + } + nodes + .entry(node.clone()) + .or_insert_with(|| empty(node)) + .current = labels; + } + for (node, ordinals) in by_ordinal { + nodes + .entry(node.clone()) + .or_insert_with(|| empty(node)) + .versions = ordinals.into_iter().collect(); + } + let mut homed = HomedLabels::new(); + for (node, restored) in nodes { + let home = RecordHomes::of(HomedRecord::NodeLabels { node: &node }).owner(); + homed.entry(home).or_default().push(restored); + } + Ok(homed) +} + +fn empty(node_id: String) -> RestoredNodeLabels { + RestoredNodeLabels { + node_id, + versions: Vec::new(), + current: Vec::new(), + } +} + +/// The encoded weight of one restored node, for sizing batches: its +/// versions plus its current set as units, and their bytes. +pub(super) fn label_weight(node: &RestoredNodeLabels) -> (usize, usize) { + let labels_len = |labels: &[String]| labels.iter().map(String::len).sum::(); + let bytes = node.node_id.len() + + labels_len(&node.current) + + node + .versions + .iter() + .map(|(_, labels)| 8 + labels_len(labels)) + .sum::(); + (node.versions.len() + 1, bytes) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn labels(names: &[&str]) -> Vec { + names.iter().map(|n| n.to_string()).collect() + } + + #[test] + fn each_node_restores_on_its_home_with_its_history() { + let homed = label_units( + 7, + vec![(1, 7, "alice".into(), labels(&["Admin"]))], + vec![ + (1, 7, "alice".into(), 200, labels(&["Admin"])), + (1, 7, "alice".into(), 100, labels(&["Person"])), + (1, 7, "bob".into(), 150, labels(&[])), + ], + ) + .expect("units"); + let alice = &homed[&VShardId::from_key(b"alice")] + .iter() + .find(|n| n.node_id == "alice") + .expect("alice restores on her home"); + assert_eq!( + alice.versions, + vec![(100, labels(&["Person"])), (200, labels(&["Admin"]))] + ); + assert_eq!(alice.current, labels(&["Admin"])); + let bob = &homed[&VShardId::from_key(b"bob")] + .iter() + .find(|n| n.node_id == "bob") + .expect("bob restores on his home"); + assert!(bob.current.is_empty()); + assert!( + label_units(7, vec![(1, 8, "x".into(), labels(&["L"]))], Vec::new()).is_err(), + "another tenant's labels are refused" + ); + } +} diff --git a/nodedb/src/control/backup/restore/redo_reissue/mod.rs b/nodedb/src/control/backup/restore/redo_reissue/mod.rs index 4516ddf58..16bc41e69 100644 --- a/nodedb/src/control/backup/restore/redo_reissue/mod.rs +++ b/nodedb/src/control/backup/restore/redo_reissue/mod.rs @@ -1,11 +1,12 @@ // SPDX-License-Identifier: BUSL-1.1 -//! Durable, replicated RESTORE of document rows, their index entries, and -//! graph edges, as Calvin transactions. +//! Durable, replicated RESTORE of document rows, their index entries, graph +//! edges and node labels, as Calvin transactions. mod commit; mod documents; mod edges; +mod labels; mod prepared; mod reissue; mod sub_record; diff --git a/nodedb/src/control/backup/restore/redo_reissue/prepared.rs b/nodedb/src/control/backup/restore/redo_reissue/prepared.rs index 7451b755f..f2bbbb8a2 100644 --- a/nodedb/src/control/backup/restore/redo_reissue/prepared.rs +++ b/nodedb/src/control/backup/restore/redo_reissue/prepared.rs @@ -106,7 +106,7 @@ pub(super) fn document_units( for (stamp, body) in versions { ops.push(match body { Some(value) => document_put(stored, pk, value, raw, Some(stamp))?, - None => document_tombstone(stored, pk, raw, stamp.sys_from_ms)?, + None => document_tombstone(stored, pk, raw, stamp.sys_from)?, }); } (ops, ends_live) diff --git a/nodedb/src/control/backup/restore/redo_reissue/reissue.rs b/nodedb/src/control/backup/restore/redo_reissue/reissue.rs index eab133411..a4f7b3793 100644 --- a/nodedb/src/control/backup/restore/redo_reissue/reissue.rs +++ b/nodedb/src/control/backup/restore/redo_reissue/reissue.rs @@ -1,11 +1,11 @@ // SPDX-License-Identifier: BUSL-1.1 -//! Re-issue every restored document row and graph edge as Calvin +//! Re-issue every restored document row, graph edge and node label as Calvin //! transactions. //! //! Rows go first, then edges, so an edge's endpoints are in place when it -//! installs. Each row goes to its collection's home, each edge to both of its -//! endpoint homes. Every one of them runs in the Calvin sequence, so one +//! installs, then node labels. Each row goes to its collection's home, each +//! edge to both of its endpoint homes, each node's labels to its home. Every one of them runs in the Calvin sequence, so one //! RESTORE has one ordering domain. A backup's index entries are not //! re-issued: every replica derives a row's secondary index entries as it //! installs the row, exactly as for a committed transaction's row. @@ -14,8 +14,9 @@ use crate::control::state::SharedState; use crate::types::TenantId; use super::super::target::DatabaseTarget; -use super::commit::{commit_collection, commit_edges}; +use super::commit::{commit_collection, commit_edges, commit_labels}; use super::edges::edge_units; +use super::labels::label_units; use super::prepared::{PreparedRows, document_units}; /// The backup sections this re-issue consumes. @@ -23,6 +24,10 @@ pub(in crate::control::backup::restore) struct RestoredRows { /// The document rows, prepared and checked against their homes. pub documents: Vec, pub edges: Vec<(String, Vec)>, + /// Each node's current label set. + pub node_labels: Vec, + /// Every node label version. + pub node_label_versions: Vec, } /// What the re-issue committed. @@ -70,5 +75,7 @@ pub(in crate::control::backup::restore) async fn reissue_rows_and_edges( for collection in edges { stats.records += commit_edges(state, tenant, target.restore_id, collection).await?; } + let labels = label_units(tenant_id, rows.node_labels, rows.node_label_versions)?; + stats.records += commit_labels(state, tenant, target.dest, target.restore_id, labels).await?; Ok(stats) } diff --git a/nodedb/src/control/backup/restore/redo_reissue/sub_record.rs b/nodedb/src/control/backup/restore/redo_reissue/sub_record.rs index 15df26f95..c3e8ce2a1 100644 --- a/nodedb/src/control/backup/restore/redo_reissue/sub_record.rs +++ b/nodedb/src/control/backup/restore/redo_reissue/sub_record.rs @@ -7,9 +7,9 @@ //! arm a committed transaction's row does: //! //! * document put: `(collection, document_id, value, prov, surrogate)`, plus -//! `(sys_from_ms, valid_from_ms, valid_until_ms)` for a version of a +//! `(sys_from, valid_from_ms, valid_until_ms)` for a version of a //! `bitemporal=true` collection; -//! * document delete: `(collection, document_id, prov, surrogate, sys_from_ms)`, +//! * document delete: `(collection, document_id, prov, surrogate, sys_from)`, //! a tombstone version of a `bitemporal=true` collection. //! //! A restored edge version travels typed, and its transaction's resolve @@ -20,10 +20,10 @@ use nodedb_wal::record::RecordType; use crate::wal::RedoSubRecord; -/// The system and valid time a document version was stored at. +/// The system-time ordinal and valid time a document version was stored at. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(super) struct VersionStamp { - pub sys_from_ms: i64, + pub sys_from: i64, pub valid_from_ms: i64, pub valid_until_ms: i64, } @@ -53,7 +53,7 @@ pub(super) fn document_put( value, prov, surrogate, - s.sys_from_ms, + s.sys_from, s.valid_from_ms, s.valid_until_ms, )), @@ -66,15 +66,15 @@ pub(super) fn document_put( }) } -/// The tombstone version of a `bitemporal=true` document at `sys_from_ms`. +/// The tombstone version of a `bitemporal=true` document at `sys_from`. pub(super) fn document_tombstone( collection: &str, document_id: &str, surrogate: u32, - sys_from_ms: i64, + sys_from: i64, ) -> crate::Result { let prov: Option = None; - let payload = zerompk::to_msgpack_vec(&(collection, document_id, prov, surrogate, sys_from_ms)) + let payload = zerompk::to_msgpack_vec(&(collection, document_id, prov, surrogate, sys_from)) .map_err(|e| encode_error("document tombstone", e))?; Ok(RedoSubRecord { record_type: RecordType::Delete as u32, @@ -116,7 +116,7 @@ mod tests { #[test] fn a_version_keeps_its_stamp() { let stamp = VersionStamp { - sys_from_ms: 1_000, + sys_from: 1_000, valid_from_ms: 10, valid_until_ms: 20, }; diff --git a/nodedb/src/control/backup/restore/sections.rs b/nodedb/src/control/backup/restore/sections.rs index 995c09e1c..bd4119b31 100644 --- a/nodedb/src/control/backup/restore/sections.rs +++ b/nodedb/src/control/backup/restore/sections.rs @@ -92,6 +92,8 @@ pub(in crate::control::backup) fn append_snapshot( index_configs, surrogate_pk, tenant_edges, + node_labels, + node_label_versions, group_write_marks, group_proposal_keys, group_proposal_keys_complete_from, @@ -141,6 +143,9 @@ pub(in crate::control::backup) fn append_snapshot( into.surrogate_pk.extend(surrogate_pk); into.arrays.extend(arrays); into.tenant_edges.extend(tenant_edges); + // Each node's labels come from the source of its home vShard. + into.node_labels.extend(node_labels); + into.node_label_versions.extend(node_label_versions); // A backup carries no marks: the guard reads the destination's marks. into.group_write_marks.extend(group_write_marks); // A backup carries no proposal keys: they serve Raft installs only. diff --git a/nodedb/src/control/backup/snapshot_keys.rs b/nodedb/src/control/backup/snapshot_keys.rs index 9e7b6da19..dcfb47f2d 100644 --- a/nodedb/src/control/backup/snapshot_keys.rs +++ b/nodedb/src/control/backup/snapshot_keys.rs @@ -208,6 +208,14 @@ pub fn retain_tenant_data_for_vshards( snap.edge_hidden.clear(); snap.edge_cuts.clear(); snap.edge_applied.clear(); + // A node's labels home by the node, whatever collection its edges live + // in. The source of that home keeps them. + let owned_node = + |node: &str| RecordHomes::of(HomedRecord::NodeLabels { node }).owned_by(source_vshards); + snap.node_labels + .retain(|(_, _, node, _)| owned_node(node.as_str())); + snap.node_label_versions + .retain(|(_, _, node, _, _)| owned_node(node.as_str())); // CRDT (`crdt_state`): each entry carries its single collection. Keep it iff // that collection's vshard is in this source set — exactly one source node diff --git a/nodedb/src/control/catalog_entry/apply/credential.rs b/nodedb/src/control/catalog_entry/apply/credential.rs new file mode 100644 index 000000000..0311aa6ff --- /dev/null +++ b/nodedb/src/control/catalog_entry/apply/credential.rs @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! Apply credential catalog entries to `SystemCatalog` redb. +//! +//! A put leaves a live row of the same name unchanged, so two racing +//! `CREATE CREDENTIAL` statements leave the one the log orders first. The +//! statement that proposed the entry reads the row back to report the +//! outcome. + +use crate::control::security::catalog::{SystemCatalog, catalog_err}; +use crate::control::security::source_credential::StoredCredential; + +pub fn put(row: &StoredCredential, catalog: &SystemCatalog) -> crate::Result<()> { + let existing = catalog + .get_credential(row.tenant_id, &row.name) + .map_err(|e| catalog_err(&format!("read credential {}", row.name), e))?; + if existing.is_some() { + return Ok(()); + } + catalog + .put_credential(row) + .map_err(|e| catalog_err(&format!("create credential {}", row.name), e)) +} + +pub fn remove(tenant_id: u64, name: &str, catalog: &SystemCatalog) -> crate::Result<()> { + catalog + .delete_credential(tenant_id, name) + .map(|_| ()) + .map_err(|e| catalog_err(&format!("drop credential {name}"), e)) +} diff --git a/nodedb/src/control/catalog_entry/apply/dispatch.rs b/nodedb/src/control/catalog_entry/apply/dispatch.rs index 0f0a41711..18561ae4f 100644 --- a/nodedb/src/control/catalog_entry/apply/dispatch.rs +++ b/nodedb/src/control/catalog_entry/apply/dispatch.rs @@ -12,11 +12,11 @@ use super::outcome::ApplyOutcome; use super::{ alert_rule, api_key, array, auth_user, backup_schedule, change_stream, checkpoint, clone_cow, - collection, column_stats, consumer_group, continuous_aggregate, custom_type, database, - function, index_registry, materialized_view, oidc_provider, owner, permission, procedure, - quota, redaction, retention_policy, rls, role, schedule, scope_grant, scope_quota, sequence, - streaming_materialized_view, synonym_group, tenant, topic, trigger, user, vector, - wal_tombstone, + collection, column_stats, consumer_group, continuous_aggregate, credential, custom_type, + database, function, index_registry, materialized_view, oidc_provider, owner, permission, + procedure, quota, redaction, retention_policy, rls, role, schedule, scope_grant, scope_quota, + sequence, spatial_ref_sys, streaming_materialized_view, synonym_group, tenant, topic, trigger, + user, vector, wal_tombstone, }; /// Apply `entry` to `catalog`. @@ -423,6 +423,12 @@ fn apply_to_inner(entry: &CatalogEntry, catalog: &SystemCatalog) -> crate::Resul // The post-apply writes them. CatalogEntry::CommitConsumerOffsets(_) => Ok(()), CatalogEntry::PutBackupScheduleMark(mark) => backup_schedule::raise_mark(mark, catalog), + CatalogEntry::PutSpatialRefSys(def) => spatial_ref_sys::define(def, catalog), + CatalogEntry::DropSpatialRefSys { srid } => spatial_ref_sys::retire(*srid, catalog), + CatalogEntry::PutCredential(row) => credential::put(row, catalog), + CatalogEntry::DropCredential { tenant_id, name } => { + credential::remove(*tenant_id, name, catalog) + } } } diff --git a/nodedb/src/control/catalog_entry/apply/mod.rs b/nodedb/src/control/catalog_entry/apply/mod.rs index 0055e017a..45557436a 100644 --- a/nodedb/src/control/catalog_entry/apply/mod.rs +++ b/nodedb/src/control/catalog_entry/apply/mod.rs @@ -18,6 +18,7 @@ pub mod collection; pub mod column_stats; pub mod consumer_group; pub mod continuous_aggregate; +pub mod credential; pub mod custom_type; pub mod database; mod dispatch; @@ -38,6 +39,7 @@ pub mod schedule; pub mod scope_grant; pub mod scope_quota; pub mod sequence; +pub mod spatial_ref_sys; pub mod streaming_materialized_view; pub mod synonym_group; pub mod tenant; diff --git a/nodedb/src/control/catalog_entry/apply/spatial_ref_sys.rs b/nodedb/src/control/catalog_entry/apply/spatial_ref_sys.rs new file mode 100644 index 000000000..9fe0f70c1 --- /dev/null +++ b/nodedb/src/control/catalog_entry/apply/spatial_ref_sys.rs @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! Apply user-defined CRS catalog entries to `SystemCatalog` redb. +//! +//! A define of a code that is live or dropped leaves the set unchanged. A +//! drop of a code that is not live, or that a catalog definition names, +//! leaves it unchanged too. Every node applies the same log over the same +//! catalog, so every node leaves the set alike, and the statement that +//! proposed the entry reads the set back to report the outcome. + +use nodedb_crs::{CrsDef, Srid}; + +use crate::control::security::catalog::{SystemCatalog, catalog_err}; + +pub fn define(def: &CrsDef, catalog: &SystemCatalog) -> crate::Result<()> { + catalog + .define_custom_crs(def) + .map(|_| ()) + .map_err(|e| catalog_err(&format!("define spatial_ref_sys {}", def.srid), e)) +} + +/// Retire `srid` unless a definition names it. The DDL checks the names +/// before it proposes. This check covers a definition that committed +/// between that check and the drop. +pub fn retire(srid: Srid, catalog: &SystemCatalog) -> crate::Result<()> { + if !catalog.srid_referrers(srid)?.is_empty() { + return Ok(()); + } + catalog + .drop_custom_crs(srid) + .map(|_| ()) + .map_err(|e| catalog_err(&format!("drop spatial_ref_sys {srid}"), e)) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::control::security::catalog::StoredCollection; + use crate::control::security::catalog::collection_constraints::CheckConstraintDef; + use nodedb_crs::{CustomCrsState, parse_proj_string}; + use nodedb_types::DatabaseId; + + /// A collection that names the code committed after the DDL checked: + /// the apply leaves the code live. With the definition gone, the drop + /// applies. + #[test] + fn a_named_code_stays_live_at_apply() { + let tmp = tempfile::tempdir().expect("tmpdir"); + let catalog = SystemCatalog::open(&tmp.path().join("system.redb")).expect("open"); + let srid = Srid::new(910_077).expect("srid"); + let def = + parse_proj_string(srid, "tm", "+proj=tmerc +lon_0=101 +ellps=GRS80").expect("parses"); + define(&def, &catalog).expect("define"); + + let mut coll = StoredCollection::stamped_for_test(1, "parcels", "admin"); + coll.check_constraints = vec![CheckConstraintDef { + name: "in_tm".into(), + check_sql: "ST_X(ST_Transform(NEW.loc, 910077)) > 0".into(), + has_subquery: false, + }]; + catalog + .put_collection(DatabaseId::DEFAULT, &coll) + .expect("collection"); + retire(srid, &catalog).expect("retire"); + assert!(matches!( + catalog.custom_crs_set().get(srid), + Some(CustomCrsState::Live(_)) + )); + assert_eq!( + catalog.srid_referrers(srid).expect("scan"), + vec!["collection 'parcels' (tenant 1, database 0): CHECK constraint 'in_tm'"] + ); + + coll.check_constraints.clear(); + catalog + .put_collection(DatabaseId::DEFAULT, &coll) + .expect("collection"); + retire(srid, &catalog).expect("retire"); + assert_eq!( + catalog.custom_crs_set().get(srid), + Some(&CustomCrsState::Dropped) + ); + } +} diff --git a/nodedb/src/control/catalog_entry/authorization.rs b/nodedb/src/control/catalog_entry/authorization.rs index 087d08fe4..680f33741 100644 --- a/nodedb/src/control/catalog_entry/authorization.rs +++ b/nodedb/src/control/catalog_entry/authorization.rs @@ -105,7 +105,11 @@ impl CatalogEntry { | Self::DeleteArray { .. } | Self::DeleteVectorIndexParams { .. } | Self::CommitConsumerOffsets(_) - | Self::PutBackupScheduleMark(_) => false, + | Self::PutBackupScheduleMark(_) + | Self::PutSpatialRefSys(_) + | Self::DropSpatialRefSys { .. } + | Self::PutCredential(_) + | Self::DropCredential { .. } => false, } } } diff --git a/nodedb/src/control/catalog_entry/descriptor_stamp.rs b/nodedb/src/control/catalog_entry/descriptor_stamp.rs index 4d5a367c1..03e5e1d74 100644 --- a/nodedb/src/control/catalog_entry/descriptor_stamp.rs +++ b/nodedb/src/control/catalog_entry/descriptor_stamp.rs @@ -293,7 +293,11 @@ pub fn stamp( | CatalogEntry::DeleteCloneSourceDrain { .. } | CatalogEntry::MoveTenantCutover { .. } | CatalogEntry::CommitConsumerOffsets(_) - | CatalogEntry::PutBackupScheduleMark(_)) => entry, + | CatalogEntry::PutBackupScheduleMark(_) + | CatalogEntry::PutSpatialRefSys(_) + | CatalogEntry::DropSpatialRefSys { .. } + | CatalogEntry::PutCredential(_) + | CatalogEntry::DropCredential { .. }) => entry, }) } diff --git a/nodedb/src/control/catalog_entry/entry.rs b/nodedb/src/control/catalog_entry/entry.rs index 18673fb7b..0cd863057 100644 --- a/nodedb/src/control/catalog_entry/entry.rs +++ b/nodedb/src/control/catalog_entry/entry.rs @@ -673,4 +673,27 @@ pub enum CatalogEntry { /// minute nor repeats a finished one. The mark is keyed by the schedule's /// config incarnation, and apply only raises it, so a replay is a no-op. PutBackupScheduleMark(Box), + + // ── User-defined CRSs ────────────────────────────────────────── + /// `CREATE SPATIAL_REF_SYS`: one user-defined CRS. Apply defines it in + /// the catalog's set, and leaves the set unchanged when the code is live + /// or dropped. Post-apply sends the set to every core of every node. + PutSpatialRefSys(Box), + /// `DROP SPATIAL_REF_SYS`: retire a user-defined CRS. The code stays a + /// tombstone and is never reused, so stored data that names it fails to + /// resolve instead of changing meaning. + DropSpatialRefSys { + srid: nodedb_crs::Srid, + }, + + // ── Credentials ──────────────────────────────────────────────── + /// `CREATE CREDENTIAL`: one credential, its secret sealed before the + /// proposal so the log holds ciphertext only. Apply writes the row when + /// the name is free and leaves a live row unchanged. + PutCredential(Box), + /// `DROP CREDENTIAL`: remove one credential. + DropCredential { + tenant_id: u64, + name: String, + }, } diff --git a/nodedb/src/control/catalog_entry/kind.rs b/nodedb/src/control/catalog_entry/kind.rs index 5c36f74a3..7c1cc91a2 100644 --- a/nodedb/src/control/catalog_entry/kind.rs +++ b/nodedb/src/control/catalog_entry/kind.rs @@ -103,6 +103,10 @@ impl CatalogEntry { Self::DeleteVectorIndexParams { .. } => "delete_vector_index_params", Self::CommitConsumerOffsets(_) => "commit_consumer_offsets", Self::PutBackupScheduleMark(_) => "put_backup_schedule_mark", + Self::PutSpatialRefSys(_) => "put_spatial_ref_sys", + Self::DropSpatialRefSys { .. } => "drop_spatial_ref_sys", + Self::PutCredential(_) => "put_credential", + Self::DropCredential { .. } => "drop_credential", } } } diff --git a/nodedb/src/control/catalog_entry/post_apply/async_dispatch/dispatcher.rs b/nodedb/src/control/catalog_entry/post_apply/async_dispatch/dispatcher.rs index 4bae70d60..ecbdb489e 100644 --- a/nodedb/src/control/catalog_entry/post_apply/async_dispatch/dispatcher.rs +++ b/nodedb/src/control/catalog_entry/post_apply/async_dispatch/dispatcher.rs @@ -276,6 +276,19 @@ pub async fn run_post_apply_async_side_effects( } => { super::synonym_group::delete_async(database_id, tenant_id, name, &shared).await; } + // AWAITED: every core must hold the new set before the applied-index + // watcher bumps. A statement that runs first refuses the CRS it just + // defined, or still resolves the one it just dropped. A core that + // misses the set stops the batch, and the re-delivered entry resends + // it. + CatalogEntry::PutSpatialRefSys(_) => { + super::spatial_ref_sys::install_on_every_core(&shared, "define").await?; + } + CatalogEntry::DropSpatialRefSys { .. } => { + super::spatial_ref_sys::install_on_every_core(&shared, "drop").await?; + } + // No core holds a credential: a COPY reads the catalog row. + CatalogEntry::PutCredential(_) | CatalogEntry::DropCredential { .. } => {} // AWAITED: the moved rows must leave the source key before the // applied-index watcher bumps. Otherwise a same-name CREATE in the // source database observes them. diff --git a/nodedb/src/control/catalog_entry/post_apply/async_dispatch/mod.rs b/nodedb/src/control/catalog_entry/post_apply/async_dispatch/mod.rs index 2207389b5..d8b176603 100644 --- a/nodedb/src/control/catalog_entry/post_apply/async_dispatch/mod.rs +++ b/nodedb/src/control/catalog_entry/post_apply/async_dispatch/mod.rs @@ -27,6 +27,7 @@ pub mod crdt_compact; mod dispatcher; pub mod materialized_view; pub mod move_tenant; +pub mod spatial_ref_sys; pub mod synonym_group; pub mod vector; diff --git a/nodedb/src/control/catalog_entry/post_apply/async_dispatch/spatial_ref_sys.rs b/nodedb/src/control/catalog_entry/post_apply/async_dispatch/spatial_ref_sys.rs new file mode 100644 index 000000000..fe7d96a62 --- /dev/null +++ b/nodedb/src/control/catalog_entry/post_apply/async_dispatch/spatial_ref_sys.rs @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! Async post-apply for user-defined CRS catalog entries. +//! +//! `PutSpatialRefSys` and `DropSpatialRefSys` change the catalog's +//! user-defined CRS set. Each Data Plane core resolves SRIDs against its own +//! copy, so this sends the whole set to every core on this node. Both run on +//! every node, so every core in the cluster resolves a user-defined SRID the +//! way the statement's node does. +//! +//! The metadata applier awaits the install before the entry counts as +//! applied. A core that does not apply the set fails the install: the +//! applier stops the batch at the entry, and the re-delivered entry sends +//! the set again. So no later metadata entry, and no replicated write held +//! behind this entry's metadata index, reaches a core that lacks the set. + +use nodedb_physical::physical_plan::MetaOp; + +use crate::bridge::envelope::PhysicalPlan; +use crate::control::state::SharedState; + +use super::core_fanout::{CoreFanout, dispatch_to_every_core}; + +/// Name the fan-out reports a CRS set dispatch under. The set belongs to no +/// collection, and the fan-out addresses each core directly. +const SPATIAL_REF_SYS_SENTINEL_COLLECTION: &str = "_spatial_ref_sys"; + +/// Send the catalog's current user-defined CRS set to every core on this +/// node. `cause` names the change for the report: `define`, `drop` or +/// `image`. `Err` when a core did not apply the set. +pub async fn install_on_every_core(shared: &SharedState, cause: &'static str) -> crate::Result<()> { + let set = shared.credentials.catalog().custom_crs_set(); + let entries = set.len(); + let plan = PhysicalPlan::Meta(MetaOp::SetCustomCrsSet { + set: nodedb_crs::CustomCrsSet::clone(&set), + }); + let fanout = CoreFanout { + database_id: crate::types::DatabaseId::DEFAULT.as_u64(), + tenant_id: 0, + collection: SPATIAL_REF_SYS_SENTINEL_COLLECTION, + what: "user-defined CRS set", + detail: cause, + }; + dispatch_to_every_core(shared, &fanout, &plan) + .await + .inspect_err(|error| crate::diag::custom_crs_set_not_applied(error, cause, entries)) +} diff --git a/nodedb/src/control/catalog_entry/post_apply/gateway_invalidation.rs b/nodedb/src/control/catalog_entry/post_apply/gateway_invalidation.rs index bc0c396c3..a595c4b96 100644 --- a/nodedb/src/control/catalog_entry/post_apply/gateway_invalidation.rs +++ b/nodedb/src/control/catalog_entry/post_apply/gateway_invalidation.rs @@ -48,6 +48,9 @@ use crate::control::state::SharedState; /// | PutAlertRule / DeleteAlertRule | ❌ no | alert rules drive their own eval loop and never enter a PhysicalPlan | /// | Topic / consumer-group variants | ❌ no | Event Plane delivery identities that never enter a PhysicalPlan | /// | PutArray / DeleteArray | ✅ yes | array schema and routing prefix are baked into the plan | +/// | PutSpatialRefSys | ❌ no | a new code is one no cached plan resolved: planning it failed | +/// | DropSpatialRefSys | ✅ yes, all | any cached plan can carry the dropped definition, and no version set names a CRS | +/// | PutCredential / DropCredential | ❌ no | a COPY reads the credential row when it runs; no plan carries it | pub(crate) fn invalidate_gateway_cache_for_entry(entry: &CatalogEntry, shared: &Arc) { let Some(inv) = shared.gateway_invalidator.get() else { return; @@ -343,6 +346,17 @@ pub(crate) fn invalidate_gateway_cache_for_entry(entry: &CatalogEntry, shared: & CatalogEntry::PutBackupScheduleMark(_) => { // no-op: a backup schedule mark never enters a query plan. } + CatalogEntry::PutSpatialRefSys(_) => { + // no-op: no cached plan resolved a code before it was defined. + } + CatalogEntry::PutCredential(_) | CatalogEntry::DropCredential { .. } => { + // no-op: a COPY reads the credential row when it runs. + } + CatalogEntry::DropSpatialRefSys { .. } => { + // A plan resolves a CRS to its definition at planning time, so + // a cached plan can still transform with the dropped one. + inv.invalidate_all(); + } CatalogEntry::PutCheckpoint(_) | CatalogEntry::DeleteCheckpoint { .. } | CatalogEntry::CompactHistory { .. } => { diff --git a/nodedb/src/control/catalog_entry/post_apply/mod.rs b/nodedb/src/control/catalog_entry/post_apply/mod.rs index e4d7190a5..1b00509f2 100644 --- a/nodedb/src/control/catalog_entry/post_apply/mod.rs +++ b/nodedb/src/control/catalog_entry/post_apply/mod.rs @@ -29,6 +29,7 @@ pub mod custom_type; pub mod database; pub mod function; pub mod materialized_view; +pub mod oidc_provider; pub mod owner; pub mod permission; pub mod procedure; @@ -70,6 +71,7 @@ pub(crate) use async_dispatch::continuous_aggregate::{ }; pub(crate) use async_dispatch::crdt_compact::{compact_async, drain_pending_compactions}; pub use async_dispatch::run_post_apply_async_side_effects; +pub(crate) use async_dispatch::spatial_ref_sys::install_on_every_core as install_custom_crs_set; pub(crate) use async_dispatch::synonym_group::delete_async as remove_synonym_group; pub(crate) use async_dispatch::synonym_group::put_async as install_synonym_group; pub(crate) use async_dispatch::vector::delete_async as remove_vector_index_params; diff --git a/nodedb/src/control/catalog_entry/post_apply/oidc_provider.rs b/nodedb/src/control/catalog_entry/post_apply/oidc_provider.rs new file mode 100644 index 000000000..95e814ff0 --- /dev/null +++ b/nodedb/src/control/catalog_entry/post_apply/oidc_provider.rs @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! Post-apply for `PutOidcProvider` / `DeleteOidcProvider`: register the +//! provider with this node's JWKS registry, or unregister it. +//! +//! Registration is synchronous, so a token verified after the applied index +//! advances finds the provider registered. The key fetch runs detached: it +//! is network I/O, and the first token with an unknown `kid` fetches the +//! keys itself when the detached fetch has not landed. + +use std::sync::Arc; + +use crate::control::security::catalog::StoredOidcProvider; +use crate::control::state::SharedState; + +/// Post-apply for `PutOidcProvider`. +pub fn put(provider: &StoredOidcProvider, shared: &Arc) { + let Some(registry) = shared.jwks_registry.as_ref() else { + return; + }; + let target = registry.register_catalog_provider(&provider.provider_name, &provider.jwks_uri); + let registry = Arc::clone(registry); + tokio::spawn(async move { + registry.prefetch(&target).await; + }); +} + +/// Post-apply for `DeleteOidcProvider`. +pub fn delete(name: &str, shared: &Arc) { + if let Some(registry) = shared.jwks_registry.as_ref() { + registry.unregister_catalog_provider(name); + } +} diff --git a/nodedb/src/control/catalog_entry/post_apply/sync.rs b/nodedb/src/control/catalog_entry/post_apply/sync.rs index d7d65020d..43b05c4ad 100644 --- a/nodedb/src/control/catalog_entry/post_apply/sync.rs +++ b/nodedb/src/control/catalog_entry/post_apply/sync.rs @@ -18,9 +18,10 @@ use std::sync::Arc; use super::gateway_invalidation::invalidate_gateway_cache_for_entry; use super::{ alert_rule, api_key, array, auth_user, change_stream, collection, consumer_group, - continuous_aggregate, custom_type, database, function, materialized_view, owner, permission, - procedure, quota, redaction, retention_policy, rls, role, schedule, scope_grant, scope_quota, - sequence, streaming_materialized_view, synonym_group, tenant, topic, trigger, user, + continuous_aggregate, custom_type, database, function, materialized_view, oidc_provider, owner, + permission, procedure, quota, redaction, retention_policy, rls, role, schedule, scope_grant, + scope_quota, sequence, streaming_materialized_view, synonym_group, tenant, topic, trigger, + user, }; use crate::control::catalog_entry::entry::CatalogEntry; use crate::control::state::SharedState; @@ -348,10 +349,8 @@ pub fn apply_post_apply_side_effects_sync(entry: &CatalogEntry, shared: &Arc { database::delete_grant(*db_id, *user_id, privilege.clone(), Arc::clone(shared)); } - CatalogEntry::PutOidcProvider(_) | CatalogEntry::DeleteOidcProvider { .. } => { - // No in-memory cache to update yet; the OIDC verify path reads from - // catalog on each request. A runtime cache can be added when needed. - } + CatalogEntry::PutOidcProvider(provider) => oidc_provider::put(provider, shared), + CatalogEntry::DeleteOidcProvider { name } => oidc_provider::delete(name, shared), CatalogEntry::CloneDatabase { target_descriptor, .. } => { @@ -489,5 +488,10 @@ pub fn apply_post_apply_side_effects_sync(entry: &CatalogEntry, shared: &Arc {} + // Apply swapped the catalog's in-memory set. The cores receive it in + // the async post-apply. + CatalogEntry::PutSpatialRefSys(_) | CatalogEntry::DropSpatialRefSys { .. } => {} + // A credential is read from the catalog row on each use. + CatalogEntry::PutCredential(_) | CatalogEntry::DropCredential { .. } => {} } } diff --git a/nodedb/src/control/clone/resolver/resolve.rs b/nodedb/src/control/clone/resolver/resolve.rs index 0cf369073..235834baf 100644 --- a/nodedb/src/control/clone/resolver/resolve.rs +++ b/nodedb/src/control/clone/resolver/resolve.rs @@ -15,11 +15,12 @@ use super::rewrite::rewrite_plan_for_source; /// Parameters for the clone read resolver. pub struct CloneReadParams { - /// The LSN at which the query runs (T_lsn). - pub query_lsn: Lsn, - /// Wall-clock milliseconds corresponding to `query_lsn` (for engine - /// `system_as_of_ms` fields that work in millisecond space). - pub query_ms: Option, + /// The read's `AS OF SYSTEM TIME` milliseconds, `None` for a current read. + /// Only a read of a live clone maps it to an LSN (T_lsn), through this + /// node's WAL time anchors. A read of any other collection never consults + /// the anchors, so a historical read of versions that predate them (a + /// restored backup's) is answered from the versions themselves. + pub as_of_ms: Option, } /// Outcome of attempting to resolve a clone read for one task. @@ -77,10 +78,17 @@ pub async fn resolve_read( }); } + // T_lsn: the highest LSN committed by the `AS OF` time, and a time before + // the oldest retained anchor is refused. Without one, the WAL frontier. + let query_lsn = match params.as_of_ms { + Some(as_of_ms) => state.ms_to_lsn(as_of_ms)?, + None => state.wal.next_lsn(), + }; + // Bitemporal correctness: check if T_lsn < clone_created_at. - if params.query_lsn < live.origin.clone_created_at { + if query_lsn < live.origin.clone_created_at { return Ok(Some(ResolveOutcome::PreDatesClone( - ClonePredicatesNote::new(params.query_lsn, live.origin.clone_created_at), + ClonePredicatesNote::new(query_lsn, live.origin.clone_created_at), ))); } @@ -89,7 +97,7 @@ pub async fn resolve_read( coll_name: coll_name.to_string(), live, }; - let source_tasks = walk_clone_chain(state, &task, tenant_id, params.query_lsn, first).await?; + let source_tasks = walk_clone_chain(state, &task, tenant_id, query_lsn, first).await?; let target_collection_key = crate::control::planner::sql_plan_convert::convert::db_qualified(db_id, coll_name); diff --git a/nodedb/src/control/clone/resolver/rewrite.rs b/nodedb/src/control/clone/resolver/rewrite.rs index fd102c785..753c8f5bd 100644 --- a/nodedb/src/control/clone/resolver/rewrite.rs +++ b/nodedb/src/control/clone/resolver/rewrite.rs @@ -16,9 +16,10 @@ use super::rewrite_engine::{rewrite_columnar, rewrite_document, rewrite_kv, rewr /// Compute the source-side system-time selection for a clone scan rewrite. /// -/// Snapshot clones read the source at a fixed point-in-time (`effective_ms`), -/// so they always collapse to an `AsOf` (or the plan's own selection when the -/// clone carries no ceiling). `AllVersions` (audit log) does not compose with a +/// Snapshot clones read the source at a fixed point-in-time (`effective_ms`): +/// the plan's own point-in-time cut when it is at or before `effective_ms`, +/// else an `AsOf` at `effective_ms`. A clone with no ceiling keeps the plan's +/// selection. `AllVersions` (audit log) does not compose with a /// snapshot clone — the request is rejected with a typed error rather than /// silently picking an arbitrary snapshot. fn rewrite_system_time( @@ -32,9 +33,21 @@ fn rewrite_system_time( .into(), }); } - match effective_ms { - Some(ms) => Ok(SystemTimeScope::AsOf(ms)), - None => Ok(plan_scope), + let Some(ms) = effective_ms else { + return Ok(plan_scope); + }; + // A plan cut at or before the clone's source time is the stricter cut, + // and a pinned cut keeps the statement's one read cut. + match plan_scope { + SystemTimeScope::AsOf(_) | SystemTimeScope::Pinned { .. } + if plan_scope.as_of_ms().is_some_and(|plan_ms| plan_ms <= ms) => + { + Ok(plan_scope) + } + SystemTimeScope::Current + | SystemTimeScope::AsOf(_) + | SystemTimeScope::Pinned { .. } + | SystemTimeScope::AllVersions => Ok(SystemTimeScope::AsOf(ms)), } } @@ -282,6 +295,7 @@ pub(super) fn strip_db_prefix(db_id: DatabaseId, qualified: &str) -> &str { #[cfg(test)] mod tests { + use super::rewrite_system_time; use crate::control::server::shared::plan_util::extract_collection; use nodedb_graph::{Direction, GraphTraversalOptions}; use nodedb_physical::physical_plan::{ @@ -468,6 +482,7 @@ mod tests { bm25_query: None, bm25_field: None, stage: nodedb_physical::physical_plan::RagStage::Local, + scope: nodedb_physical::physical_plan::GraphReadScope::default(), }), ), ] @@ -541,4 +556,29 @@ mod tests { ); } } + + /// A plan cut at or before the clone's source time keeps its cut, pinned + /// or not. A later cut, or a live read, reads at the source time. + #[test] + fn a_stricter_plan_cut_survives_the_clone_ceiling() { + let pinned = SystemTimeScope::Pinned { + ordinal: nodedb_types::ms_to_ordinal_upper(100), + cut_marker: 2, + }; + assert_eq!(rewrite_system_time(Some(200), pinned).unwrap(), pinned); + assert_eq!( + rewrite_system_time(Some(200), SystemTimeScope::AsOf(150)).unwrap(), + SystemTimeScope::AsOf(150) + ); + assert_eq!( + rewrite_system_time(Some(50), pinned).unwrap(), + SystemTimeScope::AsOf(50) + ); + assert_eq!( + rewrite_system_time(Some(50), SystemTimeScope::Current).unwrap(), + SystemTimeScope::AsOf(50) + ); + assert_eq!(rewrite_system_time(None, pinned).unwrap(), pinned); + assert!(rewrite_system_time(Some(50), SystemTimeScope::AllVersions).is_err()); + } } diff --git a/nodedb/src/control/clone/resolver/rewrite_engine.rs b/nodedb/src/control/clone/resolver/rewrite_engine.rs index 7a800be2f..3a89a5bc5 100644 --- a/nodedb/src/control/clone/resolver/rewrite_engine.rs +++ b/nodedb/src/control/clone/resolver/rewrite_engine.rs @@ -97,7 +97,7 @@ pub(super) async fn rewrite_document( projection, limit, offset, - system_as_of_ms, + system_time, } if ctx.is_target(collection) => Ok(SourceRewrite::task(PhysicalPlan::Document( DocumentOp::IndexedFetch { collection: ctx.source_qualified.clone(), @@ -107,7 +107,7 @@ pub(super) async fn rewrite_document( projection: projection.clone(), limit: *limit, offset: *offset, - system_as_of_ms: *system_as_of_ms, + system_time: ctx.system_time(*system_time)?, }, ))), diff --git a/nodedb/src/control/cluster/array_cluster_exec/elementwise.rs b/nodedb/src/control/cluster/array_cluster_exec/elementwise.rs index 39405ff38..2ff1dfa27 100644 --- a/nodedb/src/control/cluster/array_cluster_exec/elementwise.rs +++ b/nodedb/src/control/cluster/array_cluster_exec/elementwise.rs @@ -25,7 +25,7 @@ pub(super) struct ElementwiseArgs<'a> { pub op: ArrayBinaryOp, pub attr_idx: u32, pub prefix_bits: u8, - pub system_as_of: Option, + pub system_time: nodedb_types::SystemTimeScope, pub valid_at_ms: Option, pub txn_id: Option, } @@ -41,7 +41,7 @@ impl ClusterArrayExecutor { op, attr_idx, prefix_bits, - system_as_of, + system_time, valid_at_ms, txn_id, } = args; @@ -64,7 +64,7 @@ impl ClusterArrayExecutor { attr_idx, // coord_elementwise stamps each shard's bucket. shard_bucket: None, - system_as_of, + system_time, valid_at_ms, txn_id: txn_id.map(TxnId::as_u64), }; @@ -77,7 +77,7 @@ impl ClusterArrayExecutor { note_array_reads(right, &result.read_versions); // A temporal read ends with one summary row, as a local elementwise // op answers. - if system_as_of.is_some() || valid_at_ms.is_some() { + if system_time.is_temporal() || valid_at_ms.is_some() { let summary = nodedb_types::json_to_msgpack(&serde_json::json!({ "truncated_before_horizon": result.truncated_before_horizon })) diff --git a/nodedb/src/control/cluster/array_cluster_exec/executor.rs b/nodedb/src/control/cluster/array_cluster_exec/executor.rs index 7caa85ca5..3ccd62683 100644 --- a/nodedb/src/control/cluster/array_cluster_exec/executor.rs +++ b/nodedb/src/control/cluster/array_cluster_exec/executor.rs @@ -55,6 +55,7 @@ struct SliceArgs<'a> { system_time: nodedb_types::SystemTimeScope, valid_at_ms: Option, zone_msgpack: &'a [u8], + physical: bool, txn_id: Option, } @@ -65,9 +66,10 @@ struct AggArgs<'a> { group_by_dim: i32, slice_tile_keys: &'a [u64], prefix_bits: u8, - system_as_of: Option, + system_time: nodedb_types::SystemTimeScope, valid_at_ms: Option, region_msgpack: &'a [u8], + physical: bool, txn_id: Option, } @@ -123,6 +125,7 @@ impl ClusterArrayExecutor { system_time, valid_at_ms, zone_msgpack, + physical, } => { self.execute_slice(SliceArgs { array_id, @@ -134,6 +137,7 @@ impl ClusterArrayExecutor { system_time: *system_time, valid_at_ms: *valid_at_ms, zone_msgpack, + physical: *physical, txn_id, }) .await @@ -146,9 +150,10 @@ impl ClusterArrayExecutor { group_by_dim, slice_tile_keys, prefix_bits, - system_as_of, + system_time, valid_at_ms, region_msgpack, + physical, } => { self.execute_agg(AggArgs { array_id, @@ -157,9 +162,10 @@ impl ClusterArrayExecutor { group_by_dim: *group_by_dim, slice_tile_keys, prefix_bits: *prefix_bits, - system_as_of: *system_as_of, + system_time: *system_time, valid_at_ms: *valid_at_ms, region_msgpack, + physical: *physical, txn_id, }) .await @@ -171,7 +177,7 @@ impl ClusterArrayExecutor { op, attr_idx, prefix_bits, - system_as_of, + system_time, valid_at_ms, } => { self.execute_elementwise(super::elementwise::ElementwiseArgs { @@ -180,7 +186,7 @@ impl ClusterArrayExecutor { op: *op, attr_idx: *attr_idx, prefix_bits: *prefix_bits, - system_as_of: *system_as_of, + system_time: *system_time, valid_at_ms: *valid_at_ms, txn_id, }) @@ -241,6 +247,7 @@ impl ClusterArrayExecutor { system_time, valid_at_ms, zone_msgpack, + physical, txn_id, } = args; let coordinator = ArrayCoordinator::for_slice( @@ -270,6 +277,7 @@ impl ClusterArrayExecutor { valid_at_ms, txn_id: txn_id.map(TxnId::as_u64), zone_msgpack: zone_msgpack.to_vec(), + physical, }; let result = coordinator .coord_slice(req, limit, system_time) @@ -301,9 +309,10 @@ impl ClusterArrayExecutor { group_by_dim, slice_tile_keys, prefix_bits, - system_as_of, + system_time, valid_at_ms, region_msgpack, + physical, txn_id, } = args; let coordinator = ArrayCoordinator::for_slice( @@ -325,10 +334,11 @@ impl ClusterArrayExecutor { group_by_dim, cell_filter_msgpack: vec![], shard_bucket: None, - system_as_of, + system_time, valid_at_ms, txn_id: txn_id.map(TxnId::as_u64), region_msgpack: region_msgpack.to_vec(), + physical, }; let agg = coordinator.coord_agg(req).await.map_err(coord_err)?; note_array_reads(array_id, &agg.read_versions); @@ -346,7 +356,7 @@ impl ClusterArrayExecutor { // only. This ensures payload_to_response → decode_payload_to_json emits // the same JSON structure for both single-node and cluster agg queries — // including the below-horizon signal, which must not be silently dropped. - let emit_horizon = system_as_of.is_some() || valid_at_ms.is_some(); + let emit_horizon = system_time.is_temporal() || valid_at_ms.is_some(); let rows = finalize_agg_partials( &agg.partials, &reducer, diff --git a/nodedb/src/control/cluster/array_cluster_helpers.rs b/nodedb/src/control/cluster/array_cluster_helpers.rs index 3c1b3ecdb..b6a8b977a 100644 --- a/nodedb/src/control/cluster/array_cluster_helpers.rs +++ b/nodedb/src/control/cluster/array_cluster_helpers.rs @@ -4,11 +4,14 @@ //! and response-opcode → `VShardMessageType` lookup for the local-dispatch //! fast path. +use nodedb_array::query::agg_number::{AggNumber, AggScalar}; +use nodedb_array::query::aggregate::{AggOutput, AggregateResult, Reducer}; use nodedb_cluster::distributed_array::merge::ArrayAggPartial; use nodedb_cluster::distributed_array::scatter::{ArrayCoordError, ShardFailure}; use nodedb_cluster::distributed_array::{ArrayAggGroupKey, ArrayAggNumber}; use nodedb_cluster::error::ClusterError; use nodedb_cluster::wire::VShardMessageType; +use nodedb_types::array_agg::{DistAcc, Moments}; use crate::Error; @@ -17,6 +20,7 @@ use crate::Error; /// Writes as an untagged msgpack scalar — same wire shape as `AggCell` in /// `data::executor::dispatch::array::aggregate` — so that /// `decode_payload_to_json` produces clean JSON numbers and nulls. +#[derive(Clone)] pub(super) enum AggValue { Float(f64), Int(i64), @@ -65,16 +69,20 @@ fn group_cell(key: &ArrayAggGroupKey) -> Result { /// Finalize merged `ArrayAggPartial`s into the same msgpack-map structure /// the local `ArrayOp::Aggregate` path produces via `encode_agg_rows`. /// -/// For scalar (no group-by), returns a single `[{"result": }]`. -/// For group-by, returns one `{"group": , "result": }` per -/// partial, the dim value typed as its dim. `COUNT` and an integer attr's -/// `SUM` / `MIN` / `MAX` are integers, and a `SUM` past `i64` refuses with -/// SQLSTATE `22003`, as on a single node. `MEAN` is a float. +/// Each partial turns back into the engine partial of `reducer` and +/// finalizes through `AggregateResult::finalize`, the one finalizer a +/// single-node read uses, so both topologies answer the same values: +/// +/// - A scalar reducer gives one `{"result": v}` row, after `"group"` when +/// it groups. `COUNT` and an integer attr's `SUM` / `MIN` / `MAX` / `MODE` +/// are integers, and a `SUM` past `i64` refuses with SQLSTATE `22003`. +/// - A histogram gives one `{"bin_lo", "bin_hi", "count"}` row per bin. +/// - A median, percentile or mode past its distinct-value budget refuses +/// with SQLSTATE `54000`. +/// /// When `emit_horizon` is set (temporal queries only), a trailing -/// `{"truncated_before_horizon": bool}` summary row is appended. This mirrors -/// the single-node path (`dispatch::array::aggregate`) exactly — including the -/// rule that the below-horizon signal is surfaced only for temporal reads — so -/// `decode_payload_to_json` produces byte-identical JSON on both topologies. +/// `{"truncated_before_horizon": bool}` summary row is appended, as on the +/// single-node path. pub(super) fn finalize_agg_partials( partials: &[ArrayAggPartial], reducer: &nodedb_physical::physical_plan::ArrayReducer, @@ -82,35 +90,30 @@ pub(super) fn finalize_agg_partials( truncated_before_horizon: bool, emit_horizon: bool, ) -> Result>, Error> { - use nodedb_physical::physical_plan::ArrayReducer; - - let finalize = |p: &ArrayAggPartial| -> Result { - if p.count == 0 { - return Ok(AggValue::Null); - } - let n = match reducer { - ArrayReducer::Sum => p.sum, - ArrayReducer::Count => return count_value(p.count), - ArrayReducer::Min => p.min, - ArrayReducer::Max => p.max, - ArrayReducer::Mean => return Ok(AggValue::Float(p.welford_mean)), - }; - number_value(n) - }; - + let engine = reducer.engine().map_err(|e| Error::BadRequest { + detail: format!("array aggregate: {e}"), + })?; let is_grouped = group_by_dim >= 0; - let mut rows: Vec> = partials - .iter() - .map(|p| { - let mut row = std::collections::BTreeMap::new(); - if is_grouped { - row.insert("group".to_string(), group_cell(&p.group_key)?); - } - row.insert("result".to_string(), finalize(p)?); - Ok(row) - }) - .collect::>()?; + let mut rows: Vec> = Vec::new(); + for p in partials { + let group = if is_grouped { + Some(group_cell(&p.group_key)?) + } else { + None + }; + let output = partial_result(p, engine) + .finalize() + .map_err(finalize_error)?; + rows.extend(output_rows(group, output)?); + } + // A scalar read over no shard partial still answers its one row. + if partials.is_empty() && !is_grouped { + let output = AggregateResult::Empty(engine) + .finalize() + .map_err(finalize_error)?; + rows.extend(output_rows(None, output)?); + } // Trailing summary row carrying the below-horizon signal — only for // temporal queries, matching the single-node path so both topologies emit @@ -127,20 +130,114 @@ pub(super) fn finalize_agg_partials( Ok(rows) } -fn out_of_range(detail: String) -> Error { - Error::NumericValueOutOfRange { - detail: format!("array aggregate: {detail}"), +/// The engine partial of `reducer` that partial `p` carries. +fn partial_result(p: &ArrayAggPartial, reducer: Reducer) -> AggregateResult { + let number = |n: ArrayAggNumber| match n.as_i128() { + Some(v) => AggNumber::Int(v), + None => AggNumber::Float(n.as_f64()), + }; + if p.count == 0 { + // A histogram keeps the bins of a group whose values all fell + // outside its range. + return match reducer { + Reducer::Histogram(spec) if !p.bins.is_empty() => AggregateResult::Histogram { + spec, + counts: p.bins.clone(), + }, + _ => AggregateResult::Empty(reducer), + }; + } + match reducer { + Reducer::Sum => AggregateResult::Sum { + value: number(p.sum), + count: p.count, + }, + Reducer::Count => AggregateResult::Count { count: p.count }, + Reducer::Min => AggregateResult::Min { + value: number(p.min), + count: p.count, + }, + Reducer::Max => AggregateResult::Max { + value: number(p.max), + count: p.count, + }, + Reducer::Mean => AggregateResult::Mean { + sum: number(p.sum), + count: p.count, + }, + Reducer::Median | Reducer::Percentile(_) | Reducer::Mode => AggregateResult::Dist { + reducer, + acc: DistAcc::from_dist(p.dist.clone()), + }, + Reducer::StdDevPop | Reducer::StdDevSamp | Reducer::VarPop | Reducer::VarSamp => { + AggregateResult::Moments { + reducer, + moments: Moments { + count: p.count, + mean: p.welford_mean, + m2: p.welford_m2, + }, + } + } + Reducer::Histogram(spec) => AggregateResult::Histogram { + spec, + counts: p.bins.clone(), + }, + } +} + +/// The rows of one finalized partial, after `group` when it groups. +fn output_rows( + group: Option, + output: AggOutput, +) -> Result>, Error> { + let with_group = |mut row: std::collections::BTreeMap| { + if let Some(g) = &group { + row.insert("group".to_string(), g.clone()); + } + row + }; + Ok(match output { + AggOutput::Scalar(v) => { + let value = match v { + Some(AggScalar::Int(i)) => AggValue::Int(i), + Some(AggScalar::Float(f)) => AggValue::Float(f), + None => AggValue::Null, + }; + vec![with_group(std::collections::BTreeMap::from([( + "result".to_string(), + value, + )]))] + } + AggOutput::Bins(bins) => bins + .into_iter() + .map(|bin| { + Ok(with_group(std::collections::BTreeMap::from([ + ("bin_lo".to_string(), AggValue::Float(bin.lo)), + ("bin_hi".to_string(), AggValue::Float(bin.hi)), + ("count".to_string(), count_value(bin.count)?), + ]))) + }) + .collect::>()?, + }) +} + +/// The statement error of a finalize refusal: `54000` past a distribution +/// budget, `22003` for a value out of range. +fn finalize_error(e: nodedb_array::ArrayError) -> Error { + match e { + nodedb_array::ArrayError::AggregateBudgetExceeded { .. } => { + Error::DataPlane(crate::bridge::envelope::ErrorCode::ProgramLimitExceeded { + detail: format!("array aggregate: {e}"), + }) + } + other => out_of_range(other.to_string()), } } -/// The final value of a partial's number: an exact integer, refused past -/// `i64`, or a float. -fn number_value(n: ArrayAggNumber) -> Result { - match n.as_i128() { - Some(v) => i64::try_from(v) - .map(AggValue::Int) - .map_err(|_| out_of_range(format!("value {v} is out of range for bigint"))), - None => Ok(AggValue::Float(n.as_f64())), +fn out_of_range(detail: String) -> Error { + Error::NumericValueOutOfRange { + detail: format!("array aggregate: {detail}"), } } @@ -374,6 +471,85 @@ mod tests { )); } + /// Exact median, mode, variance and histogram finalize from merged shard + /// partials, and a distribution past its budget refuses with `54000`. + #[test] + fn new_reducers_finalize_from_merged_partials() { + use nodedb_physical::physical_plan::ArrayReducer; + use nodedb_types::array_agg::{IntRun, ValueDist}; + let dist = |runs: &[(i64, u64)]| ArrayAggPartial { + count: runs.iter().map(|r| r.1).sum(), + dist: ValueDist::Ints( + runs.iter() + .map(|&(value, count)| IntRun { value, count }) + .collect(), + ), + ..ArrayAggPartial::empty(ArrayAggGroupKey::Scalar) + }; + let mut merged = dist(&[(1, 1), (4, 2)]); + merged.merge(&dist(&[(2, 1), (4, 1)])); + let result = |reducer: ArrayReducer, p: &ArrayAggPartial| { + let rows = finalize_agg_partials(std::slice::from_ref(p), &reducer, -1, false, false) + .expect("finalize"); + match rows[0].get("result") { + Some(AggValue::Float(v)) => *v, + Some(AggValue::Int(v)) => *v as f64, + _ => f64::NAN, + } + }; + // Values 1, 2, 4, 4, 4. + assert_eq!(result(ArrayReducer::Median, &merged), 4.0); + assert_eq!(result(ArrayReducer::Mode, &merged), 4.0); + assert_eq!(result(ArrayReducer::Percentile { p: 0.25 }, &merged), 2.0); + + let mut a = ArrayAggPartial { + count: 2, + welford_mean: 3.0, + welford_m2: 2.0, + ..ArrayAggPartial::empty(ArrayAggGroupKey::Scalar) + }; + a.merge(&ArrayAggPartial { + count: 2, + welford_mean: 7.0, + welford_m2: 2.0, + ..ArrayAggPartial::empty(ArrayAggGroupKey::Scalar) + }); + // Values 2, 4, 6, 8: population variance 5. + assert!((result(ArrayReducer::VarPop, &a) - 5.0).abs() < 1e-9); + + let hist = ArrayAggPartial { + count: 3, + bins: vec![1, 2], + ..ArrayAggPartial::empty(ArrayAggGroupKey::Int64(7)) + }; + let rows = finalize_agg_partials( + &[hist], + &ArrayReducer::Histogram { + bins: 2, + lo: 0.0, + hi: 1.0, + }, + 0, + false, + false, + ) + .expect("bins"); + assert_eq!(rows.len(), 2); + assert!(matches!(rows[1].get("count"), Some(AggValue::Int(2)))); + assert!(matches!(rows[1].get("bin_lo"), Some(AggValue::Float(v)) if *v == 0.5)); + assert!(matches!(rows[1].get("group"), Some(AggValue::Int(7)))); + + let over = ArrayAggPartial { + count: 1, + dist: ValueDist::OverBudget { distinct: 9 }, + ..ArrayAggPartial::empty(ArrayAggGroupKey::Scalar) + }; + assert!(matches!( + finalize_agg_partials(&[over], &ArrayReducer::Median, -1, false, false), + Err(Error::DataPlane(ErrorCode::ProgramLimitExceeded { .. })) + )); + } + /// A shard request that never went out answers `NoLeader` for that /// shard: nothing ran, so the refusal is definite. #[test] diff --git a/nodedb/src/control/cluster/array_executor/elementwise.rs b/nodedb/src/control/cluster/array_executor/elementwise.rs index 9cb22f8fd..6c6537514 100644 --- a/nodedb/src/control/cluster/array_executor/elementwise.rs +++ b/nodedb/src/control/cluster/array_executor/elementwise.rs @@ -40,7 +40,7 @@ impl DataPlaneArrayExecutor { attr_idx: req.attr_idx, cell_filter: None, shard_bucket: req.shard_bucket, - system_as_of: req.system_as_of, + system_time: req.system_time, valid_at_ms: req.valid_at_ms, }); let resp = self @@ -58,7 +58,8 @@ impl DataPlaneArrayExecutor { // A temporal read ends with the `{"truncated_before_horizon": bool}` // summary row. The coordinator ORs the flag and appends one summary // row for the whole fan-out. - let truncated_before_horizon = if req.system_as_of.is_some() || req.valid_at_ms.is_some() { + let truncated_before_horizon = if req.system_time.is_temporal() || req.valid_at_ms.is_some() + { let summary = rows.pop().ok_or_else(|| ClusterError::Codec { detail: "exec_elementwise: temporal answer has no summary row".into(), })?; diff --git a/nodedb/src/control/cluster/array_executor/executor.rs b/nodedb/src/control/cluster/array_executor/executor.rs index 20d4f8b48..5bf5df7ba 100644 --- a/nodedb/src/control/cluster/array_executor/executor.rs +++ b/nodedb/src/control/cluster/array_executor/executor.rs @@ -55,17 +55,19 @@ impl DataPlaneArrayExecutor { txn_id: Option, ) -> Result { // Array reads have no weaker consistency: a shard read confirms its - // group on this node before it reads. + // group on this node before it reads. A read pinned to a marked cut + // then waits until this node installed the cut + // (`read_cut::wait`), so every shard reads one cut. if !plan_is_write(&plan) { + let deadline = StatementDeadline::of_statement(&self.state); let groups = groups_of_vshards(&self.state, [local_vshard_id.as_u32()]) .map_err(|e| execution_error("array read confirmation", e))?; - confirm_linearizable_read( - &self.state, - &groups, - StatementDeadline::of_statement(&self.state), - ) - .await - .map_err(|e| execution_error("array read confirmation", e))?; + confirm_linearizable_read(&self.state, &groups, deadline) + .await + .map_err(|e| execution_error("array read confirmation", e))?; + crate::control::read_cut::await_read_cut(&self.state, &plan, deadline) + .await + .map_err(|e| execution_error("array read cut", e))?; } let request_id = self.state.next_request_id(); let request = local_request(request_id, array_id, local_vshard_id, plan, txn_id); diff --git a/nodedb/src/control/cluster/array_executor/read.rs b/nodedb/src/control/cluster/array_executor/read.rs index 91fc1c75e..b30b71a13 100644 --- a/nodedb/src/control/cluster/array_executor/read.rs +++ b/nodedb/src/control/cluster/array_executor/read.rs @@ -52,6 +52,7 @@ impl DataPlaneArrayExecutor { system_time: req.system_time, valid_at_ms: req.valid_at_ms, zone_msgpack: req.zone_msgpack.clone(), + physical: req.physical, }); let resp = self @@ -121,9 +122,10 @@ impl DataPlaneArrayExecutor { cell_filter, return_partial: true, shard_bucket: req.shard_bucket, - system_as_of: req.system_as_of, + system_time: req.system_time, valid_at_ms: req.valid_at_ms, region_msgpack: req.region_msgpack.clone(), + physical: req.physical, }); let resp = self diff --git a/nodedb/src/control/cluster/array_executor/refusal.rs b/nodedb/src/control/cluster/array_executor/refusal.rs index edd3a536a..b211bb343 100644 --- a/nodedb/src/control/cluster/array_executor/refusal.rs +++ b/nodedb/src/control/cluster/array_executor/refusal.rs @@ -112,6 +112,7 @@ pub(super) fn execution_error(context: &str, error: crate::Error) -> ClusterErro | crate::Error::DivisionByZero | crate::Error::DataException { .. } | crate::Error::MixedSrid { .. } + | crate::Error::UnknownSrid { .. } | crate::Error::NumericValueOutOfRange { .. } | crate::Error::InvalidTextRepresentation { .. } | crate::Error::DatatypeMismatch { .. } diff --git a/nodedb/src/control/cluster/calvin/scheduler/driver/core/routing.rs b/nodedb/src/control/cluster/calvin/scheduler/driver/core/routing.rs index 82305dd4a..63dae8533 100644 --- a/nodedb/src/control/cluster/calvin/scheduler/driver/core/routing.rs +++ b/nodedb/src/control/cluster/calvin/scheduler/driver/core/routing.rs @@ -317,7 +317,6 @@ fn graph_routing(op: &GraphOp) -> PlanRouting { | GraphOp::BspSuperstep(_) | GraphOp::WccSuperstep(_) | GraphOp::TemporalNeighbors { .. } - | GraphOp::TemporalAlgorithm { .. } | GraphOp::Stats { .. } | GraphOp::NodePresenceRead { .. } => PlanRouting::NotAWrite, } @@ -450,6 +449,8 @@ mod tests { properties: Vec::new(), src_surrogate: Surrogate::new(1), dst_surrogate: Surrogate::new(2), + valid_from_ms: None, + valid_until_ms: None, }); let mut got = vshards_of(&plan); @@ -471,6 +472,8 @@ mod tests { dst_id: dst.to_owned(), src_surrogate: Surrogate::new(1), dst_surrogate: Surrogate::new(2), + valid_from_ms: None, + valid_until_ms: None, }; // Two edges over the same endpoint pair: the union must dedupe. @@ -520,6 +523,8 @@ mod tests { properties: Vec::new(), src_surrogate: Surrogate::new(1), dst_surrogate: Surrogate::new(1), + valid_from_ms: None, + valid_until_ms: None, }); assert_eq!(vshards_of(&plan), vec![v]); } @@ -615,6 +620,7 @@ mod tests { collection: QualifiedCollection::new(DatabaseId::DEFAULT, "metrics"), filters: Vec::new(), updates: Vec::new(), + system_from: None, rls_write_check: nodedb_types::RlsWriteCheck::pending_injection(), }); assert_eq!(vshards_of(&update), vec![want]); @@ -622,6 +628,7 @@ mod tests { let delete = PhysicalPlan::Columnar(ColumnarOp::Delete { collection: QualifiedCollection::new(DatabaseId::DEFAULT, "metrics"), filters: Vec::new(), + system_from: None, rls_write_check: nodedb_types::RlsWriteCheck::pending_injection(), }); assert_eq!(vshards_of(&delete), vec![want]); diff --git a/nodedb/src/control/cluster/data_plane_error_wire.rs b/nodedb/src/control/cluster/data_plane_error_wire.rs index 52b3c9395..17439eee1 100644 --- a/nodedb/src/control/cluster/data_plane_error_wire.rs +++ b/nodedb/src/control/cluster/data_plane_error_wire.rs @@ -156,6 +156,7 @@ impl From for DataPlaneErrorCode { label, limit: to_wire_count(limit), }, + ErrorCode::ProgramLimitExceeded { detail } => Self::ProgramLimitExceeded { detail }, ErrorCode::InvalidTextRepresentation { detail } => { Self::InvalidTextRepresentation { detail } } @@ -327,6 +328,9 @@ impl From for ErrorCode { label, limit: from_wire_count(limit), }, + DataPlaneErrorCode::ProgramLimitExceeded { detail } => { + Self::ProgramLimitExceeded { detail } + } DataPlaneErrorCode::InvalidTextRepresentation { detail } => { Self::InvalidTextRepresentation { detail } } diff --git a/nodedb/src/control/cluster/execution_error_wire.rs b/nodedb/src/control/cluster/execution_error_wire.rs index e6e53b353..d561e1a41 100644 --- a/nodedb/src/control/cluster/execution_error_wire.rs +++ b/nodedb/src/control/cluster/execution_error_wire.rs @@ -127,6 +127,7 @@ pub(crate) fn execution_error_to_typed(err: crate::Error) -> TypedClusterError { | crate::Error::DivisionByZero | crate::Error::DataException { .. } | crate::Error::MixedSrid { .. } + | crate::Error::UnknownSrid { .. } | crate::Error::NumericValueOutOfRange { .. } | crate::Error::InvalidLimitValue { .. } | crate::Error::RetryableLeaderChange { .. } diff --git a/nodedb/src/control/cluster/metadata_applier/audit_describe.rs b/nodedb/src/control/cluster/metadata_applier/audit_describe.rs index f2967c69f..15cf25c2b 100644 --- a/nodedb/src/control/cluster/metadata_applier/audit_describe.rs +++ b/nodedb/src/control/cluster/metadata_applier/audit_describe.rs @@ -204,6 +204,16 @@ pub(super) fn describe_entry(e: &catalog_entry::CatalogEntry) -> (String, u64, S 0, String::new(), ), + E::PutSpatialRefSys(def) => (format!("spatial_ref_sys:{}", def.srid), 0, String::new()), + E::DropSpatialRefSys { srid } => (format!("spatial_ref_sys:{srid}"), 0, String::new()), + E::PutCredential(row) => ( + format!("credential:{}:{}", row.tenant_id, row.name), + 0, + row.kind.type_name().to_string(), + ), + E::DropCredential { tenant_id, name } => { + (format!("credential:{tenant_id}:{name}"), 0, String::new()) + } E::CommitConsumerOffsets(c) => ( format!( "consumer_group:{}:{}:{}:{}", diff --git a/nodedb/src/control/cluster/metadata_applier/pending_ddl.rs b/nodedb/src/control/cluster/metadata_applier/pending_ddl.rs index acc8563f9..f850f4fe8 100644 --- a/nodedb/src/control/cluster/metadata_applier/pending_ddl.rs +++ b/nodedb/src/control/cluster/metadata_applier/pending_ddl.rs @@ -154,7 +154,7 @@ impl MetadataCommitApplier { catalog, teardown, shared.wal.next_lsn().as_u64(), - crate::control::lease::wall_now_ns(), + nodedb_types::wall_now_ns(), )?; // A same-name CREATE waits on the pending-reclaim path's hold until // the teardown finishes. A re-delivered cancel finds it already held. diff --git a/nodedb/src/control/cluster/metadata_applier/wedge.rs b/nodedb/src/control/cluster/metadata_applier/wedge.rs index 6b7a6c1e5..9621a9c27 100644 --- a/nodedb/src/control/cluster/metadata_applier/wedge.rs +++ b/nodedb/src/control/cluster/metadata_applier/wedge.rs @@ -121,6 +121,7 @@ pub fn classify(error: &crate::Error) -> ApplyFailureClass { | crate::Error::DivisionByZero | crate::Error::DataException { .. } | crate::Error::MixedSrid { .. } + | crate::Error::UnknownSrid { .. } | crate::Error::NumericValueOutOfRange { .. } | crate::Error::InvalidTextRepresentation { .. } | crate::Error::DatatypeMismatch { .. } diff --git a/nodedb/src/control/cluster/metadata_image/reconcile.rs b/nodedb/src/control/cluster/metadata_image/reconcile.rs index d1c6794f7..6087708c8 100644 --- a/nodedb/src/control/cluster/metadata_image/reconcile.rs +++ b/nodedb/src/control/cluster/metadata_image/reconcile.rs @@ -19,6 +19,8 @@ //! aggregates are registered, removed ones unregistered. //! - `PutSynonymGroup` / `DeleteSynonymGroup`: every group is installed, //! removed ones deleted. +//! - `PutSpatialRefSys` / `DropSpatialRefSys`: the catalog's user-defined +//! CRS set is sent to every core. //! - `DeleteMaterializedView`: a view target is a collection, reclaimed by //! the collection rule. //! - `DeleteTopicWithConsumerGroups`: a removed topic's node-local messages @@ -34,8 +36,8 @@ use std::sync::Arc; use crate::control::array_catalog::ArrayCatalogEntry; use crate::control::catalog_entry::post_apply::{ ContinuousAggregateRegisterFailure, array, clear_before_recreate, compact_async, - drop_array_on_every_core, install_synonym_group, install_vector_index_params, - open_array_on_every_core, reclaim_collection_storage, + drop_array_on_every_core, install_custom_crs_set, install_synonym_group, + install_vector_index_params, open_array_on_every_core, reclaim_collection_storage, register_continuous_aggregate_on_every_core, remove_synonym_group, remove_vector_index_params, unregister_continuous_aggregate, }; @@ -57,6 +59,8 @@ pub(super) async fn reconcile_data_plane( reconcile_vector_params(shared, before, after).await?; reconcile_continuous_aggregates(shared, before, after).await?; reconcile_synonym_groups(shared, before, after).await?; + // The set is whole and small, so every install sends it. + install_custom_crs_set(shared, "image").await?; reconcile_event_buffers(shared, before, after)?; invalidate_plans(shared, before, after); Ok(()) diff --git a/nodedb/src/control/cluster/metadata_image/reload.rs b/nodedb/src/control/cluster/metadata_image/reload.rs index a5a435969..7ba3a4910 100644 --- a/nodedb/src/control/cluster/metadata_image/reload.rs +++ b/nodedb/src/control/cluster/metadata_image/reload.rs @@ -67,6 +67,7 @@ pub(super) async fn reload_registries( })?; shared.synonym_registry.reload_from_catalog(catalog)?; shared.custom_type_registry.reload_from_catalog(catalog)?; + catalog.reload_custom_crs_set()?; shared.block_cache.clear(); // Event Plane definitions. diff --git a/nodedb/src/control/cluster/mod.rs b/nodedb/src/control/cluster/mod.rs index 5dcf47912..58b512b32 100644 --- a/nodedb/src/control/cluster/mod.rs +++ b/nodedb/src/control/cluster/mod.rs @@ -46,6 +46,7 @@ pub mod start_raft_helpers; #[cfg(test)] pub(crate) mod test_one_node; pub mod tls; +pub mod vshard_departure; pub mod vshard_envelope_handler; pub mod warm_peers; diff --git a/nodedb/src/control/cluster/snapshot_applier.rs b/nodedb/src/control/cluster/snapshot_applier.rs index d3df6a916..dcffaf91b 100644 --- a/nodedb/src/control/cluster/snapshot_applier.rs +++ b/nodedb/src/control/cluster/snapshot_applier.rs @@ -17,7 +17,10 @@ //! runs before the snapshot's rows of that incarnation land, never after. //! The wait is bounded: a group 0 snapshot this node still needs can queue //! behind this install, so a lagging node refuses the install and the -//! leader resends it; +//! leader resends it. It then holds the departure fence on the group's +//! vShards until it settles: a running departure purge of any of them +//! ends first, and no purge of them starts meanwhile +//! ([`crate::control::cluster::vshard_departure::fence`]); //! 1. splits the snapshot into one share per local core, routed the way live //! writes route ([`crate::control::cluster::snapshot_install::split`]); //! 2. appends and fsyncs the WAL install barrier, so restart replay never @@ -149,6 +152,13 @@ impl DataPlaneSnapshotApplier { .await?; let group_vshards = self.group_vshards(group_id)?; + // Held to the end: a departure purge removes no row this install + // writes. + let _fenced = self + .shared + .departure_fence + .begin_install(&group_vshards) + .await; let cores = { let dispatcher = self .shared diff --git a/nodedb/src/control/cluster/snapshot_builder.rs b/nodedb/src/control/cluster/snapshot_builder.rs index 1f4771afd..94efee172 100644 --- a/nodedb/src/control/cluster/snapshot_builder.rs +++ b/nodedb/src/control/cluster/snapshot_builder.rs @@ -35,7 +35,9 @@ use crate::control::backup::snapshot_keys::{ homes_of_stored, }; use crate::control::cluster::calvin_snapshot::{await_calvin_cut, capture_calvin_cut}; -use crate::control::cluster::snapshot_builder_filter::{bind_in_group, push_group_edges}; +use crate::control::cluster::snapshot_builder_filter::{ + bind_in_group, push_group_edges, push_group_node_labels, +}; use crate::control::cluster::snapshot_cut::settled_cut; use crate::control::security::catalog::SystemCatalog; use crate::control::state::SharedState; @@ -266,6 +268,12 @@ impl DataPlaneSnapshotBuilder { .into_iter() .map(|(collection, cut)| (database_id.as_u64(), tenant_id, collection, cut)), ); + push_group_node_labels( + snap.node_labels, + snap.node_label_versions, + group_vshards, + merged, + ); // CRDT: one Loro doc per (tenant, collection). Each entry carries its // single collection; include it iff that collection's vshard belongs to diff --git a/nodedb/src/control/cluster/snapshot_builder_filter.rs b/nodedb/src/control/cluster/snapshot_builder_filter.rs index 03226352d..5a885f848 100644 --- a/nodedb/src/control/cluster/snapshot_builder_filter.rs +++ b/nodedb/src/control/cluster/snapshot_builder_filter.rs @@ -1,7 +1,8 @@ // SPDX-License-Identifier: BUSL-1.1 -//! The record filters a data-group snapshot build applies: which edges and -//! which PK→surrogate binds have a home in the target group. +//! The record filters a data-group snapshot build applies: which edges, +//! which node labels and which PK→surrogate binds have a home in the target +//! group. use std::collections::HashSet; @@ -9,7 +10,9 @@ use nodedb_types::id::DatabaseId; use crate::Error; use crate::control::backup::snapshot_keys::{StoredRecord, homes_of_stored}; -use crate::types::{HomedRecord, RecordHomes}; +use crate::types::{ + HomedRecord, NodeLabelVersionEntry, NodeLabelsEntry, RecordHomes, TenantDataSnapshot, +}; /// The number of edge-key characters an unparseable-key error carries. pub(crate) const EDGE_KEY_PREFIX_CHARS: usize = 32; @@ -29,6 +32,28 @@ pub(crate) fn bind_in_group( .intersects(group_vshards) } +/// Push the current labels and the label versions of every node whose +/// labels home on a vShard of `group_vshards` onto `merged`. +pub(crate) fn push_group_node_labels( + labels: Vec, + versions: Vec, + group_vshards: &HashSet, + merged: &mut TenantDataSnapshot, +) { + let in_group = + |node: &str| RecordHomes::of(HomedRecord::NodeLabels { node }).owned_by(group_vshards); + merged.node_labels.extend( + labels + .into_iter() + .filter(|(_, _, node, _)| in_group(node.as_str())), + ); + merged.node_label_versions.extend( + versions + .into_iter() + .filter(|(_, _, node, _, _)| in_group(node.as_str())), + ); +} + /// Push every entry of `edges`, keyed by an edge version's key, with an /// endpoint home in `group_vshards` onto `out`, tagged with its database and /// tenant. diff --git a/nodedb/src/control/cluster/snapshot_install/split.rs b/nodedb/src/control/cluster/snapshot_install/split.rs index 8a0488705..16ae1335d 100644 --- a/nodedb/src/control/cluster/snapshot_install/split.rs +++ b/nodedb/src/control/cluster/snapshot_install/split.rs @@ -11,7 +11,9 @@ //! timeseries, CRDT): the vShard of the collection's `(database, bare name)` //! key, the same key [`vshard_of_stored`] resolves; //! - graph edges: dual-homed on `VShardId::from_key(src)` and -//! `VShardId::from_key(dst)`, the homes an `EdgePut` is written to. +//! `VShardId::from_key(dst)`, the homes an `EdgePut` is written to; +//! - node labels: homed on `VShardId::from_key(node)`, the home a label +//! write is written to. //! //! The vShard → core step is the dispatcher's own [`VShardRouter`]. @@ -152,6 +154,12 @@ impl Splitter<'_> { Ok(()) } + /// The core a node's labels are stored on: its home vShard's. + fn node_core(&self, node: &str) -> Result { + self.cores + .core_of(self.group_id, VShardId::from_key(node.as_bytes()).as_u32()) + } + /// The cores an edge is stored on: the homes of its two endpoints. fn edge_cores( &self, @@ -201,6 +209,8 @@ pub fn split_by_core( edge_applied, tenant_edge_cuts, tenant_edge_applied, + node_labels, + node_label_versions, group_write_marks, group_proposal_keys, group_proposal_keys_complete_from, @@ -332,6 +342,16 @@ pub fn split_by_core( .tenant_edge_applied .push((db, tid, key, applied)); } + // A node's labels go to the core of its home vShard, where every label + // write of the node routes. + for entry in node_labels { + let core = s.node_core(&entry.2)?; + s.per_core[core].node_labels.push(entry); + } + for entry in node_label_versions { + let core = s.node_core(&entry.2)?; + s.per_core[core].node_label_versions.push(entry); + } // A cut covers every edge of its collection, on whichever core it lives. for share in &mut s.per_core { share.edge_cuts.extend(edge_cuts.iter().cloned()); @@ -476,6 +496,22 @@ mod tests { } } + #[test] + fn a_nodes_labels_land_on_its_home_core() { + let node = "a7"; + let snap = TenantDataSnapshot { + node_labels: vec![(0, 1, node.into(), vec!["L".into()])], + node_label_versions: vec![(0, 1, node.into(), 5, vec!["L".into()])], + ..Default::default() + }; + let shares = split_by_core(GROUP, snap, &cores()).unwrap(); + for (core, share) in shares.per_core.iter().enumerate() { + let expected = usize::from(core == endpoint_core(node)); + assert_eq!(share.node_labels.len(), expected, "core {core}"); + assert_eq!(share.node_label_versions.len(), expected, "core {core}"); + } + } + #[test] fn control_plane_sections_stay_off_the_cores() { let snap = TenantDataSnapshot { diff --git a/nodedb/src/control/cluster/start_raft_helpers.rs b/nodedb/src/control/cluster/start_raft_helpers.rs index a4888f1e8..f5ce5c6ac 100644 --- a/nodedb/src/control/cluster/start_raft_helpers.rs +++ b/nodedb/src/control/cluster/start_raft_helpers.rs @@ -404,6 +404,14 @@ pub(super) fn spawn_vshard_schedulers( scheduler_config, })?; + // The data of vShards that left this node goes once their new owners + // serve them. + crate::control::cluster::vshard_departure::spawn_departure_purge( + shared, + node_id, + Arc::clone(&raft_loop_handle), + ); + // Background reconcile: pick up vShards whose membership lands after startup // (joiner admission) or shifts later (rebalancing). The routing table has no // change-notification, so a short fixed-interval reconcile is the simplest diff --git a/nodedb/src/control/cluster/vshard_departure/fence.rs b/nodedb/src/control/cluster/vshard_departure/fence.rs new file mode 100644 index 000000000..29d6f2fbe --- /dev/null +++ b/nodedb/src/control/cluster/vshard_departure/fence.rs @@ -0,0 +1,253 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! The per-vShard fence between a departure purge and a data-group install. +//! +//! A purge removes this node's data of every vShard outside the residency it +//! read: rows homed on a departed vShard, and edges with no endpoint home in +//! that residency. An install writes the data of its group's vShards. On a +//! vShard both touch, the install lands after the purge: +//! +//! - A purge starts only while no install of a vShard outside its residency +//! runs. It holds the fence until every core answered. +//! - An install of a vShard outside the running purge's residency waits for +//! the purge to end. An install of a resident vShard never waits: the +//! purge keeps every record a resident vShard homes. +//! +//! The purge takes the fence under the `MultiRaft` lock, at the residency +//! read. A group installs here only once it mounts, so a group that mounts +//! after that read installs after the purge. +//! +//! The fence lives in memory only. A crash ends the purge and the install +//! together. Boot recovery completes an interrupted install before the first +//! departure pass runs, and that pass hosts the vShards of every mounted +//! group again, so it purges none of them. + +use std::collections::{BTreeSet, HashMap, HashSet}; +use std::sync::{Mutex, MutexGuard}; + +use tokio::sync::Notify; + +/// The fence one node's purges and installs share. +#[derive(Debug, Default)] +pub struct DepartureFence { + state: Mutex, + /// Woken when a purge ends. + purge_ended: Notify, +} + +#[derive(Debug, Default)] +struct FenceState { + /// The residency the running purge read. `None` while no purge runs. + purge: Option>, + /// The installs running for each vShard. + installing: HashMap, +} + +impl FenceState { + /// Whether an install of `vshards` conflicts with the running purge. + fn purge_blocks(&self, vshards: &HashSet) -> bool { + self.purge + .as_ref() + .is_some_and(|resident| vshards.iter().any(|v| !resident.contains(v))) + } +} + +/// The running purge's hold on the fence. Dropping it ends the purge. +#[derive(Debug)] +#[must_use = "the purge holds the fence only while its guard lives"] +pub struct PurgeGuard<'a> { + fence: &'a DepartureFence, +} + +/// A running install's hold on the fence. Dropping it ends the install. +#[derive(Debug)] +#[must_use = "the install holds the fence only while its guard lives"] +pub struct InstallGuard<'a> { + fence: &'a DepartureFence, + vshards: Vec, +} + +impl DepartureFence { + fn state(&self) -> MutexGuard<'_, FenceState> { + self.state.lock().unwrap_or_else(|p| p.into_inner()) + } + + /// Start a purge of every vShard outside `resident`. `None` while an + /// install of such a vShard runs, or while another purge runs. + pub fn try_purge(&self, resident: &BTreeSet) -> Option> { + let mut state = self.state(); + let installing_outside = state + .installing + .keys() + .any(|vshard_id| !resident.contains(vshard_id)); + if state.purge.is_some() || installing_outside { + return None; + } + state.purge = Some(resident.clone()); + Some(PurgeGuard { fence: self }) + } + + /// Start an install of `vshards`, once no running purge removes data of + /// any of them. + pub async fn begin_install(&self, vshards: &HashSet) -> InstallGuard<'_> { + loop { + let ended = self.purge_ended.notified(); + tokio::pin!(ended); + // Registered before the check, so a purge that ends after it + // wakes this wait. + ended.as_mut().enable(); + { + let mut state = self.state(); + if !state.purge_blocks(vshards) { + for &vshard_id in vshards { + *state.installing.entry(vshard_id).or_default() += 1; + } + return InstallGuard { + fence: self, + vshards: vshards.iter().copied().collect(), + }; + } + } + ended.await; + } + } +} + +impl Drop for PurgeGuard<'_> { + fn drop(&mut self) { + self.fence.state().purge = None; + self.fence.purge_ended.notify_waiters(); + } +} + +impl Drop for InstallGuard<'_> { + fn drop(&mut self) { + let mut state = self.fence.state(); + for vshard_id in &self.vshards { + if let Some(count) = state.installing.get_mut(vshard_id) { + *count -= 1; + if *count == 0 { + state.installing.remove(vshard_id); + } + } + } + } +} + +#[cfg(test)] +mod tests { + use std::sync::{Arc, RwLock}; + use std::time::Duration; + + use nodedb_cluster::RoutingTable; + + use super::*; + use crate::bridge::dispatch::Dispatcher; + use crate::control::cluster::snapshot_applier::DataPlaneSnapshotApplier; + use crate::control::state::SharedState; + use crate::types::TenantDataSnapshot; + use crate::wal::WalManager; + + #[test] + fn a_purge_and_an_install_of_a_vshard_outside_its_residency_exclude_each_other() { + let fence = DepartureFence::default(); + let resident = BTreeSet::from([1, 2]); + let install = futures::executor::block_on(fence.begin_install(&HashSet::from([3]))); + assert!( + fence.try_purge(&resident).is_none(), + "no purge starts while an install of a vShard it purges runs" + ); + drop(install); + let purge = fence.try_purge(&resident).expect("the install ended"); + assert!(fence.try_purge(&resident).is_none(), "one purge at a time"); + let resident_install = + futures::executor::block_on(fence.begin_install(&HashSet::from([1, 2]))); + drop(resident_install); + drop(purge); + assert!(fence.state().installing.is_empty()); + assert!(fence.state().purge.is_none()); + } + + /// Wait up to five seconds for core 0 to receive a request. + async fn core_receives(sides: &mut [crate::bridge::dispatch::CoreChannelDataSide]) -> bool { + let deadline = std::time::Instant::now() + Duration::from_secs(5); + loop { + if sides[0].request_rx.try_pop().is_ok() { + return true; + } + if std::time::Instant::now() > deadline { + return false; + } + tokio::time::sleep(Duration::from_millis(20)).await; + } + } + + /// A group mounts here again while a purge of its vShards runs. Its + /// snapshot install reaches no core until the purge ends, and a purge + /// that finds the install running does not start. + #[tokio::test(flavor = "multi_thread")] + async fn a_rehost_install_waits_for_an_in_flight_purge() { + let dir = tempfile::tempdir().expect("tempdir"); + let wal = Arc::new( + WalManager::open_for_testing(&dir.path().join("fence.wal")).expect("test WAL"), + ); + let (dispatcher, mut sides) = Dispatcher::new(1, 64); + let mut shared = SharedState::new(dispatcher, wal).expect("shared state"); + let routing = RoutingTable::uniform(2, &[1], 1); + // `group_ids` lists metadata group 0 too, which homes no vShard. + let groups: Vec = routing + .group_ids() + .into_iter() + .filter(|&group_id| group_id != nodedb_cluster::METADATA_GROUP_ID) + .collect(); + let (rehosted, kept) = (groups[0], groups[1]); + let resident: BTreeSet = routing.vshards_for_group(kept).into_iter().collect(); + assert!( + !routing.vshards_for_group(rehosted).is_empty(), + "the re-hosted group homes vShards the purge covers" + ); + Arc::get_mut(&mut shared) + .expect("unshared state") + .cluster_routing = Some(Arc::new(RwLock::new(routing))); + + let snapshot = TenantDataSnapshot { + group_calvin: Some(crate::types::GroupCalvinCut::default()), + ..Default::default() + }; + let bytes = zerompk::to_msgpack_vec(&snapshot).expect("encode snapshot"); + let applier = Arc::new(DataPlaneSnapshotApplier::new(Arc::clone(&shared))); + + // The departure pass read a residency without the group, and purges. + let purge = shared + .departure_fence + .try_purge(&resident) + .expect("no install runs"); + let installing = { + let applier = Arc::clone(&applier); + let bytes = bytes.clone(); + tokio::spawn(async move { applier.install(rehosted, &bytes).await }) + }; + tokio::time::sleep(Duration::from_millis(300)).await; + assert!( + sides[0].request_rx.try_pop().is_err(), + "no core receives the install while the purge runs" + ); + + drop(purge); + assert!( + core_receives(&mut sides).await, + "the install reaches the core once the purge ended" + ); + assert!( + shared.departure_fence.try_purge(&resident).is_none(), + "no purge starts while the install of a vShard it purges runs" + ); + + installing.abort(); + let _ = installing.await; + assert!( + shared.departure_fence.try_purge(&resident).is_some(), + "a purge starts once the install ended" + ); + } +} diff --git a/nodedb/src/control/cluster/vshard_departure/ledger.rs b/nodedb/src/control/cluster/vshard_departure/ledger.rs new file mode 100644 index 000000000..1492fcbbb --- /dev/null +++ b/nodedb/src/control/cluster/vshard_departure/ledger.rs @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! Bring the residency rows in line with the vShards resident here. +//! +//! - A resident vShard with no row gets a hosted row. +//! - A resident vShard with a departed row is hosted again: its pending +//! purge is cancelled. Its group owes a snapshot install here since it +//! left, so the data it lost comes back from that install. +//! - A hosted row whose vShard is not resident departs: its purge is owed. + +use std::collections::BTreeMap; + +use crate::control::security::catalog::vshard_residency::{ResidencyChange, VShardResidency}; + +use super::resident::ResidencyView; + +/// The row changes one reconcile makes. +#[derive(Debug, Default, Clone, PartialEq, Eq)] +pub struct LedgerPlan { + pub changes: Vec, + /// vShards whose hosted row departs. + pub departed: Vec, + /// vShards whose departed row is hosted again. + pub rehosted: Vec, +} + +/// The changes that bring `rows` in line with `residency`. +pub fn reconcile(rows: &BTreeMap, residency: &ResidencyView) -> LedgerPlan { + let mut plan = LedgerPlan::default(); + for &vshard_id in &residency.resident { + match rows.get(&vshard_id) { + None => plan + .changes + .push((vshard_id, Some(VShardResidency::HOSTED))), + Some(row) if row.departed => { + plan.changes + .push((vshard_id, Some(VShardResidency::HOSTED))); + plan.rehosted.push(vshard_id); + } + Some(_) => {} + } + } + for (&vshard_id, row) in rows { + if !row.departed && !residency.holds(vshard_id) { + plan.changes + .push((vshard_id, Some(VShardResidency::DEPARTED))); + plan.departed.push(vshard_id); + } + } + plan +} + +#[cfg(test)] +mod tests { + use std::collections::BTreeSet; + + use super::*; + + fn residency(resident: &[u32]) -> ResidencyView { + ResidencyView { + resident: resident.iter().copied().collect::>(), + group_of: BTreeMap::new(), + } + } + + #[test] + fn a_hosted_vshard_that_left_departs_and_a_returning_one_is_hosted() { + let purged = VShardResidency { + departed: true, + barrier_lsn: 7, + purged: true, + }; + let rows = BTreeMap::from([ + (1, VShardResidency::HOSTED), + (2, VShardResidency::HOSTED), + (3, purged), + ]); + let plan = reconcile(&rows, &residency(&[1, 3, 4])); + assert_eq!(plan.departed, vec![2]); + assert_eq!(plan.rehosted, vec![3]); + let mut changes = plan.changes.clone(); + changes.sort_by_key(|(vshard_id, _)| *vshard_id); + assert_eq!( + changes, + vec![ + (2, Some(VShardResidency::DEPARTED)), + (3, Some(VShardResidency::HOSTED)), + (4, Some(VShardResidency::HOSTED)), + ] + ); + } + + #[test] + fn a_settled_ledger_changes_nothing() { + let rows = BTreeMap::from([(1, VShardResidency::HOSTED), (2, VShardResidency::DEPARTED)]); + assert_eq!(reconcile(&rows, &residency(&[1])), LedgerPlan::default()); + } +} diff --git a/nodedb/src/control/cluster/vshard_departure/mod.rs b/nodedb/src/control/cluster/vshard_departure/mod.rs new file mode 100644 index 000000000..7043cd9f8 --- /dev/null +++ b/nodedb/src/control/cluster/vshard_departure/mod.rs @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! Purge the data of vShards that left this node, once their new owners +//! serve them. + +pub mod fence; +pub mod ledger; +pub mod pass; +pub mod purge; +pub mod resident; +pub mod task; + +pub use fence::{DepartureFence, InstallGuard, PurgeGuard}; +pub use pass::DeparturePass; +pub use resident::ResidencyView; +pub use task::spawn_departure_purge; diff --git a/nodedb/src/control/cluster/vshard_departure/pass.rs b/nodedb/src/control/cluster/vshard_departure/pass.rs new file mode 100644 index 000000000..c05a2da15 --- /dev/null +++ b/nodedb/src/control/cluster/vshard_departure/pass.rs @@ -0,0 +1,297 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! One departure pass: record vShards that left, purge their data here, and +//! drop the rows of purges no replay can undo. +//! +//! 1. Ledger, under the `MultiRaft` lock. A vShard that left gets a +//! departed row, and its data group owes a snapshot install here, both +//! durable before any purge. A remount of the group resumes its old log, +//! whose applied index the purged data no longer matches: the debt makes +//! the remount catch up by snapshot instead. A group that mounts +//! meanwhile keeps its vShards resident, so none of them departs. +//! 2. After a boot, every purged row whose barrier the WAL still holds +//! records below is owed again: replay re-applied edge, label and array +//! records of the purged vShards. +//! 3. Purge, under the departure fence ([`super::fence`]). The fence, and +//! the snapshot debt of the purged vShards' groups, are taken under the +//! `MultiRaft` lock at the residency read. A group that mounts later +//! owes a snapshot, and its install waits for the purge to end, so the +//! purge removes no row an install wrote. A pass that finds such an +//! install running purges nothing. Then the barrier is fsynced and every +//! core purges. A vShard that became resident meanwhile is hosted again. +//! 4. Settle. A purged row goes once the WAL holds no record below its +//! barrier. +//! +//! A step that fails records a faultbox capture and leaves its rows as they +//! were. The next pass, or the first pass after a restart, retries it. No +//! pass runs before boot recovery sets `SharedState::data_groups_replayed`: +//! until every mounted group applied its committed log, the routing table +//! can omit a group this node replicates. + +use std::collections::{BTreeMap, BTreeSet, HashSet}; +use std::sync::Mutex; + +use nodedb_cluster::multi_raft::MultiRaft; + +use crate::control::security::catalog::vshard_residency::{ResidencyChange, VShardResidency}; +use crate::control::state::SharedState; + +use super::ledger; +use super::purge::{ + append_purge_barrier, departed_collections, purge_on_every_core, wal_oldest_lsn, +}; +use super::resident::ResidencyView; + +/// The departure pass state of this process. +#[derive(Debug, Default)] +pub struct DeparturePass { + /// Whether this process ran step 2 since it booted. + boot_checked: bool, +} + +impl DeparturePass { + /// Run one pass on node `node_id`, whose groups `multi_raft` mounts. + pub async fn run(&mut self, shared: &SharedState, node_id: u64, multi_raft: &Mutex) { + if !shared + .data_groups_replayed + .load(std::sync::atomic::Ordering::Acquire) + { + return; + } + if let Err((step, vshards, error)) = record_departures(shared, node_id, multi_raft) { + report(step, &vshards, &error); + return; + } + if !self.boot_checked { + match reopen_replayed(shared) { + Ok(()) => self.boot_checked = true, + Err(error) => { + report("settle", &[], &error); + return; + } + } + } + if let Err((step, vshards, error)) = purge_pending(shared, node_id, multi_raft).await { + report(step, &vshards, &error); + return; + } + if let Err(error) = settle(shared) { + report("settle", &[], &error); + } + } +} + +/// A failed step: its name, the vShards it ran for, and its error. +type StepError = (&'static str, Vec, Box); + +fn report(step: &'static str, vshards: &[u32], error: &crate::Error) { + tracing::warn!( + step, + ?vshards, + %error, + "vshard departure: a purge step failed; the next pass retries it" + ); + crate::diag::vshard_departure_purge_failed(step, vshards, error); +} + +/// The residency of this node now. +fn residency_now(node_id: u64, multi_raft: &MultiRaft) -> ResidencyView { + let mounted: HashSet = multi_raft.group_ids().into_iter().collect(); + let routing = multi_raft.routing(); + let routing = routing.read().unwrap_or_else(|p| p.into_inner()); + ResidencyView::of(node_id, &mounted, &routing) +} + +fn load_rows(shared: &SharedState) -> crate::Result> { + Ok(shared + .credentials + .catalog() + .load_vshard_residency()? + .into_iter() + .collect()) +} + +/// Step 1. The `MultiRaft` lock is held throughout, so no group mounts +/// between the residency read and the debt write. +fn record_departures( + shared: &SharedState, + node_id: u64, + multi_raft: &Mutex, +) -> Result<(), StepError> { + let catalog = shared.credentials.catalog(); + let multi_raft = multi_raft.lock().unwrap_or_else(|p| p.into_inner()); + let residency = residency_now(node_id, &multi_raft); + let rows = load_rows(shared).map_err(|e| ("ledger", Vec::new(), Box::new(e)))?; + let plan = ledger::reconcile(&rows, &residency); + if !plan.departed.is_empty() { + shared + .redo_chunks + .record_owed(catalog, &residency.groups_of(&plan.departed)) + .map_err(|e| ("snapshot_debt", plan.departed.clone(), Box::new(e)))?; + } + catalog + .apply_vshard_residency(&plan.changes) + .map_err(|e| ("ledger", plan.departed.clone(), Box::new(e)))?; + drop(multi_raft); + if !plan.departed.is_empty() { + tracing::info!( + node_id, + departed = ?plan.departed, + "vshard departure: vShards left this node; their data here is purged" + ); + } + if !plan.rehosted.is_empty() { + tracing::info!( + node_id, + rehosted = ?plan.rehosted, + "vshard departure: vShards are hosted here again; their pending purge is cancelled" + ); + } + Ok(()) +} + +/// Step 2: owe again every purge whose rows a boot replay re-applied. +fn reopen_replayed(shared: &SharedState) -> crate::Result<()> { + let oldest = wal_oldest_lsn(&shared.wal)?; + let changes: Vec = load_rows(shared)? + .into_iter() + .filter(|(_, row)| row.departed && row.purged && row.barrier_lsn > oldest) + .map(|(vshard_id, row)| { + ( + vshard_id, + Some(VShardResidency { + purged: false, + ..row + }), + ) + }) + .collect(); + shared + .credentials + .catalog() + .apply_vshard_residency(&changes) +} + +/// Step 3. +async fn purge_pending( + shared: &SharedState, + node_id: u64, + multi_raft: &Mutex, +) -> Result<(), StepError> { + let catalog = shared.credentials.catalog(); + let rows = load_rows(shared).map_err(|e| ("ledger", Vec::new(), Box::new(e)))?; + let pending: BTreeMap = rows + .into_iter() + .filter(|(_, row)| row.departed && !row.purged) + .collect(); + if pending.is_empty() { + return Ok(()); + } + let departed: BTreeSet = pending.keys().copied().collect(); + let vshards: Vec = departed.iter().copied().collect(); + let (resident, fence) = { + let multi_raft = multi_raft.lock().unwrap_or_else(|p| p.into_inner()); + let residency = residency_now(node_id, &multi_raft); + // Step 1 hosted every resident vShard again, so none is pending + // here. One that became resident since stays out of the purge. + if departed.iter().any(|vshard_id| residency.holds(*vshard_id)) { + return Ok(()); + } + // An install of a vShard this purge removes runs: the next pass + // retries. + let Some(fence) = shared.departure_fence.try_purge(&residency.resident) else { + return Ok(()); + }; + // The purge leaves the groups' data here behind their logs. An + // install can settle the debt step 1 recorded, and the group can + // leave again, before the purge runs, so the debt is recorded again. + shared + .redo_chunks + .record_owed(catalog, &residency.groups_of(&vshards)) + .map_err(|e| ("snapshot_debt", vshards.clone(), Box::new(e)))?; + (residency.resident, fence) + }; + + let collections = departed_collections(catalog, &departed) + .map_err(|e| ("barrier", vshards.clone(), Box::new(e)))?; + let barriered: BTreeMap = + if pending.values().any(|row| row.barrier_lsn == 0) { + let barrier = append_purge_barrier(&shared.wal, &collections) + .map_err(|e| ("barrier", vshards.clone(), Box::new(e)))?; + let rows: BTreeMap = pending + .iter() + .map(|(&vshard_id, row)| { + let barrier_lsn = if row.barrier_lsn == 0 { + barrier + } else { + row.barrier_lsn + }; + ( + vshard_id, + VShardResidency { + barrier_lsn, + ..*row + }, + ) + }) + .collect(); + let changes: Vec = + rows.iter().map(|(&v, row)| (v, Some(*row))).collect(); + catalog + .apply_vshard_residency(&changes) + .map_err(|e| ("barrier", vshards.clone(), Box::new(e)))?; + rows + } else { + pending + }; + + let purged = purge_on_every_core(shared, &departed, &resident, &collections) + .await + .map_err(|e| ("purge", vshards.clone(), Box::new(e)))?; + // Every core answered: an install waiting on the fence can start. + drop(fence); + + // A vShard resident again is hosted. Its group owes the snapshot debt + // recorded above, which only an install after the purge settles. + let multi_raft = multi_raft.lock().unwrap_or_else(|p| p.into_inner()); + let after = residency_now(node_id, &multi_raft); + let changes: Vec = barriered + .iter() + .map(|(&vshard_id, row)| { + let row = if after.holds(vshard_id) { + VShardResidency::HOSTED + } else { + VShardResidency { + purged: true, + ..*row + } + }; + (vshard_id, Some(row)) + }) + .collect(); + catalog + .apply_vshard_residency(&changes) + .map_err(|e| ("settle", vshards.clone(), Box::new(e)))?; + drop(multi_raft); + tracing::info!( + node_id, + departed = ?vshards, + purged, + "vshard departure: every core purged the data of vShards this node left" + ); + Ok(()) +} + +/// Step 4. +fn settle(shared: &SharedState) -> crate::Result<()> { + let oldest = wal_oldest_lsn(&shared.wal)?; + let changes: Vec = load_rows(shared)? + .into_iter() + .filter(|(_, row)| row.departed && row.purged && row.barrier_lsn <= oldest) + .map(|(vshard_id, _)| (vshard_id, None)) + .collect(); + shared + .credentials + .catalog() + .apply_vshard_residency(&changes) +} diff --git a/nodedb/src/control/cluster/vshard_departure/purge.rs b/nodedb/src/control/cluster/vshard_departure/purge.rs new file mode 100644 index 000000000..5cdc718ec --- /dev/null +++ b/nodedb/src/control/cluster/vshard_departure/purge.rs @@ -0,0 +1,206 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! One departure purge: the WAL barrier, then every core's purge. +//! +//! The purge removes rows the WAL can still hold records of. Replay +//! re-applies the redb-backed engines' records with no floor, so a pre-purge +//! record brings a purged row back at the next boot. The barrier is one +//! `CollectionTombstoned` record per purged collection, all naming the WAL +//! position before the purge, fsynced before any core purges: replay then +//! skips every record of those collections below it. Edge, label and array +//! records of other collections stay replayable, so a boot purges again +//! while the WAL still holds a record below the barrier (see +//! [`super::pass`]). + +use std::collections::BTreeSet; +use std::time::{Duration, Instant}; + +use nodedb_physical::physical_plan::{DepartedCollection, MetaOp, PhysicalPlan}; +use nodedb_types::id::{CollectionKey, QualifiedCollection}; + +use crate::control::security::catalog::SystemCatalog; +use crate::control::state::SharedState; +use crate::types::{DatabaseId, TenantId, TraceId}; +use crate::wal::WalManager; +use crate::wal::manager::NO_APPLY_KEY; + +/// How long every core gets to answer one purge. +const PURGE_DEADLINE: Duration = Duration::from_secs(120); + +/// The key of the removed-record count every core answers. +const PURGED_COUNT: &str = "purged"; + +/// One collection homed on a departed vShard. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct HomedCollection { + pub database_id: u64, + pub tenant_id: u64, + /// Bare catalog name. + pub name: String, +} + +/// Every active collection of the local catalog homed on `departed`. +pub fn departed_collections( + catalog: &SystemCatalog, + departed: &BTreeSet, +) -> crate::Result> { + if departed.is_empty() { + return Ok(Vec::new()); + } + Ok(catalog + .load_all_collections_across_databases()? + .into_iter() + .filter(|c| { + c.is_active + && departed.contains(&nodedb_cluster::routing::vshard_for_collection( + CollectionKey::from_bare(c.database_id, &c.name), + )) + }) + .map(|c| HomedCollection { + database_id: c.database_id.as_u64(), + tenant_id: c.tenant_id, + name: c.name, + }) + .collect()) +} + +/// The oldest WAL position this node's WAL still holds a record at or +/// above. +pub fn wal_oldest_lsn(wal: &WalManager) -> crate::Result { + let (segments, active_first) = wal.segments_with_active()?; + Ok(segments + .iter() + .map(|segment| segment.first_lsn) + .chain(std::iter::once(active_first)) + .min() + .unwrap_or(active_first)) +} + +/// Append and fsync the purge barrier for `collections`. Returns the WAL +/// position the barrier names: the next position at the time of the call, +/// above every record the purge removes the rows of. +pub fn append_purge_barrier( + wal: &WalManager, + collections: &[HomedCollection], +) -> crate::Result { + let barrier = wal.next_lsn().as_u64(); + let appender = wal.appender(NO_APPLY_KEY); + for collection in collections { + appender.append_collection_tombstone( + TenantId::new(collection.tenant_id), + DatabaseId::new(collection.database_id), + &collection.name, + barrier, + )?; + } + wal.sync()?; + Ok(barrier) +} + +/// Purge `departed` on every Data-Plane core of this node. Edges with an +/// endpoint home in `resident` stay. Returns the documents, edge versions +/// and label versions removed, summed over the cores. Every core answers +/// before it returns, and any core's error fails it. +pub async fn purge_on_every_core( + shared: &SharedState, + departed: &BTreeSet, + resident: &BTreeSet, + collections: &[HomedCollection], +) -> crate::Result { + injected_fault(shared.node_id)?; + let plan = PhysicalPlan::Meta(MetaOp::PurgeDepartedVShards { + departed: departed.iter().copied().collect(), + resident: resident.iter().copied().collect(), + collections: collections + .iter() + .map(|c| DepartedCollection { + database_id: c.database_id, + tenant_id: c.tenant_id, + collection: QualifiedCollection::new(DatabaseId::new(c.database_id), &c.name) + .as_str() + .to_string(), + }) + .collect(), + }); + let response = crate::control::server::broadcast::broadcast_count_to_all_cores_until( + shared, + TenantId::new(0), + DatabaseId::DEFAULT, + plan, + TraceId::generate(), + PURGED_COUNT, + Instant::now() + PURGE_DEADLINE, + ) + .await?; + let counts: std::collections::BTreeMap = + zerompk::from_msgpack(response.payload.as_bytes()).map_err(|e| { + crate::Error::Serialization { + format: "msgpack".into(), + detail: format!("departure purge count: {e}"), + } + })?; + counts + .get(PURGED_COUNT) + .copied() + .ok_or(crate::Error::Serialization { + format: "msgpack".into(), + detail: format!("departure purge count: the response carries no `{PURGED_COUNT}` key"), + }) +} + +/// Fail point `vshard_departure::purge`, scoped to node `node_id`: the purge +/// fails before any core runs, as an interrupted purge leaves it. +fn injected_fault(node_id: u64) -> crate::Result<()> { + #[cfg(feature = "failpoints")] + if let Some(detail) = crate::fail_point::eval_fail( + crate::fail_point::FailScope::Node(node_id), + "vshard_departure::purge", + ) { + return Err(crate::Error::Internal { detail }); + } + #[cfg(not(feature = "failpoints"))] + let _ = node_id; + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn only_collections_homed_on_a_departed_vshard_are_named() { + let dir = tempfile::tempdir().expect("tempdir"); + let catalog = SystemCatalog::open(&dir.path().join("system.redb")).expect("catalog"); + let names = ["dp_a", "dp_b", "dp_c", "dp_d", "dp_e", "dp_f"]; + for name in names { + let stored = + crate::control::security::catalog::StoredCollection::stamped_for_test(1, name, "o"); + catalog + .put_collection(DatabaseId::DEFAULT, &stored) + .expect("put"); + } + let home = |name: &str| { + nodedb_cluster::routing::vshard_for_collection(CollectionKey::from_bare( + DatabaseId::DEFAULT, + name, + )) + }; + let departed = BTreeSet::from([home("dp_a")]); + let named: Vec = departed_collections(&catalog, &departed) + .expect("collections") + .into_iter() + .map(|c| c.name) + .collect(); + let expected: Vec = names + .iter() + .filter(|name| home(name) == home("dp_a")) + .map(|name| name.to_string()) + .collect(); + assert_eq!(named, expected); + assert!( + departed_collections(&catalog, &BTreeSet::new()) + .expect("none") + .is_empty() + ); + } +} diff --git a/nodedb/src/control/cluster/vshard_departure/resident.rs b/nodedb/src/control/cluster/vshard_departure/resident.rs new file mode 100644 index 000000000..0c63096f4 --- /dev/null +++ b/nodedb/src/control/cluster/vshard_departure/resident.rs @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! The vShards whose data this node holds as a replica. +//! +//! A vShard is resident here while its data group: +//! - is mounted here: its replica applies the group's log; +//! - lists this node as a voter or a learner; +//! - names this node in its placement: a replica of it is entering here. +//! +//! A vShard outside all three left this node. Its new owners serve it, so +//! the copy here is stale. + +use std::collections::{BTreeMap, BTreeSet, HashSet}; + +use nodedb_cluster::RoutingTable; + +/// The vShards resident on `node_id`, and the data group of every vShard. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ResidencyView { + pub resident: BTreeSet, + /// The data group each vShard routes to. + pub group_of: BTreeMap, +} + +impl ResidencyView { + /// The residency of `node_id`, which mounts the groups `mounted`, under + /// `routing`. + pub fn of(node_id: u64, mounted: &HashSet, routing: &RoutingTable) -> Self { + let mut groups: BTreeSet = mounted.iter().copied().collect(); + for (group_id, info) in routing.group_members() { + let placed = info + .placement + .as_ref() + .is_some_and(|placement| placement.contains(&node_id)); + if placed || info.members.contains(&node_id) || info.learners.contains(&node_id) { + groups.insert(*group_id); + } + } + let resident = groups + .iter() + .flat_map(|group_id| routing.vshards_for_group(*group_id)) + .collect(); + let group_of = routing + .vshard_to_group() + .iter() + .enumerate() + .filter_map(|(vshard, group)| u32::try_from(vshard).ok().map(|v| (v, *group))) + .collect(); + Self { resident, group_of } + } + + /// Whether `vshard_id` is resident here. + pub fn holds(&self, vshard_id: u32) -> bool { + self.resident.contains(&vshard_id) + } + + /// The data groups of `vshards`. + pub fn groups_of(&self, vshards: &[u32]) -> BTreeSet { + vshards + .iter() + .filter_map(|vshard| self.group_of.get(vshard).copied()) + .collect() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Data groups 1 and 2 over nodes 1..=3, every node a voter of both. + fn table() -> RoutingTable { + RoutingTable::uniform(2, &[1, 2, 3], 3) + } + + fn vshards(routing: &RoutingTable, group_id: u64) -> BTreeSet { + routing.vshards_for_group(group_id).into_iter().collect() + } + + #[test] + fn a_voter_holds_every_vshard_of_its_groups() { + let rt = table(); + let residency = ResidencyView::of(1, &HashSet::new(), &rt); + let all: BTreeSet = vshards(&rt, 1).union(&vshards(&rt, 2)).copied().collect(); + assert_eq!(residency.resident, all); + } + + #[test] + fn a_left_group_leaves_its_vshards() { + let mut rt = table(); + rt.set_placement(1, vec![2, 3]); + rt.set_group_members(1, vec![2, 3]); + let residency = ResidencyView::of(1, &HashSet::new(), &rt); + assert_eq!(residency.resident, vshards(&rt, 2)); + let left: Vec = vshards(&rt, 1).into_iter().collect(); + assert_eq!(residency.groups_of(&left), BTreeSet::from([1])); + } + + #[test] + fn a_mount_a_learner_or_a_placement_keeps_the_vshards() { + let mut rt = table(); + rt.set_group_members(1, vec![2, 3]); + // Still mounted: the replica applies the group's log. + let mounted = HashSet::from([1]); + assert!( + ResidencyView::of(1, &mounted, &rt) + .resident + .is_superset(&vshards(&rt, 1)) + ); + // A learner replicates the group. + rt.set_group_learners(1, vec![1]); + assert!( + ResidencyView::of(1, &HashSet::new(), &rt) + .resident + .is_superset(&vshards(&rt, 1)) + ); + // A placement naming the node: a replica is entering. + rt.set_group_learners(1, Vec::new()); + rt.set_placement(1, vec![1, 2, 3]); + assert!( + ResidencyView::of(1, &HashSet::new(), &rt) + .resident + .is_superset(&vshards(&rt, 1)) + ); + } +} diff --git a/nodedb/src/control/cluster/vshard_departure/task.rs b/nodedb/src/control/cluster/vshard_departure/task.rs new file mode 100644 index 000000000..291245921 --- /dev/null +++ b/nodedb/src/control/cluster/vshard_departure/task.rs @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! The background loop that runs the departure pass. + +use std::sync::{Arc, Mutex}; +use std::time::Duration; + +use nodedb_cluster::multi_raft::MultiRaft; + +use crate::control::shutdown::{ShutdownPhase, spawn_loop}; +use crate::control::state::SharedState; + +use super::pass::DeparturePass; + +/// How often the departure pass runs. The pass reads the routing table and +/// the residency rows, and purges only when a vShard left. +const PASS_PERIOD: Duration = Duration::from_secs(1); + +/// Spawn the departure pass of node `node_id`, whose data groups +/// `multi_raft` mounts. It runs until shutdown. +pub fn spawn_departure_purge( + shared: &Arc, + node_id: u64, + multi_raft: Arc>, +) { + let state = Arc::clone(shared); + spawn_loop( + &shared.loop_registry, + &shared.shutdown, + "vshard_departure_purge", + ShutdownPhase::DrainingControlPlane, + move |mut shutdown| async move { + let mut pass = DeparturePass::default(); + let mut tick = tokio::time::interval(PASS_PERIOD); + tick.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); + loop { + tokio::select! { + biased; + _ = shutdown.wait_cancelled() => break, + _ = tick.tick() => pass.run(&state, node_id, &multi_raft).await, + } + } + }, + ); +} diff --git a/nodedb/src/control/crdt_post_image_policy.rs b/nodedb/src/control/crdt_post_image_policy.rs index a15b57fe2..5e1e5f947 100644 --- a/nodedb/src/control/crdt_post_image_policy.rs +++ b/nodedb/src/control/crdt_post_image_policy.rs @@ -11,6 +11,7 @@ use crate::control::security::audit::{AuditEmitContext, AuditEmitter, AuditEvent use crate::control::security::auth_context::AuthContext; use crate::control::security::catalog::SystemCatalog; use crate::control::security::rls::RlsPolicyStore; +use crate::control::state::SharedState; use crate::types::TenantId; use nodedb_types::{CrdtPreviewResult, Value}; @@ -19,18 +20,30 @@ pub struct ExternalCrdtPostImagePolicy<'a> { tenant_id: TenantId, collection: &'a str, auth: AuthContext, - rls: &'a RlsPolicyStore, - audit: &'a dyn AuditEmitter, + services: PolicyServices<'a>, /// Where the collection's typeguards and transition rules are read from. /// `None` judges no rule. guards: Option>, } +/// The shared services a CRDT post-image policy evaluates against: the RLS +/// store, the audit sink, and the user-defined CRSs write predicates resolve +/// SRIDs against. +#[derive(Clone, Copy)] +pub struct PolicyServices<'a> { + pub rls: &'a RlsPolicyStore, + pub audit: &'a dyn AuditEmitter, + pub crs_set: &'a nodedb_crs::CustomCrsSet, +} + /// The catalog row a CRDT admission reads the collection's typeguards and /// transition rules from. struct GuardSource<'a> { catalog: &'a SystemCatalog, database_id: nodedb_types::DatabaseId, + /// Resolves the CRS context CHECK and transition predicates evaluate + /// under. + state: &'a SharedState, } impl<'a> ExternalCrdtPostImagePolicy<'a> { @@ -38,15 +51,13 @@ impl<'a> ExternalCrdtPostImagePolicy<'a> { tenant_id: TenantId, collection: &'a str, auth: AuthContext, - rls: &'a RlsPolicyStore, - audit: &'a dyn AuditEmitter, + services: PolicyServices<'a>, ) -> Self { Self { tenant_id, collection, auth, - rls, - audit, + services, guards: None, } } @@ -61,12 +72,13 @@ impl<'a> ExternalCrdtPostImagePolicy<'a> { /// change, with the writer's roles. pub fn with_type_guards_from( mut self, - catalog: &'a SystemCatalog, + state: &'a SharedState, database_id: nodedb_types::DatabaseId, ) -> Self { self.guards = Some(GuardSource { - catalog, + catalog: state.credentials.catalog(), database_id, + state, }); self } @@ -97,6 +109,7 @@ impl<'a> ExternalCrdtPostImagePolicy<'a> { else { return Ok(()); }; + let crs = crate::control::crs_context::crs_context_for(source.state); if !coll.type_guards.is_empty() { let compiled = crate::data::executor::enforcement::typeguard::CompiledTypeGuard::compile_all( @@ -104,7 +117,7 @@ impl<'a> ExternalCrdtPostImagePolicy<'a> { &coll.type_guards, ); crate::data::executor::enforcement::typeguard::check_type_guards( - &compiled, document, None, + &compiled, document, None, &crs, ) .map_err(crate::Error::DataPlane)?; } @@ -124,6 +137,7 @@ impl<'a> ExternalCrdtPostImagePolicy<'a> { &serde_json::Value::from(previous.clone()), &serde_json::Value::from(document.clone()), &self.auth.roles, + &crs, ) .map_err(crate::Error::DataPlane)?; } @@ -139,16 +153,15 @@ impl<'a> ExternalCrdtPostImagePolicy<'a> { collection: &'a str, identity: &crate::control::security::identity::AuthenticatedIdentity, session_id: String, - rls: &'a RlsPolicyStore, - audit: &'a dyn AuditEmitter, + services: PolicyServices<'a>, ) -> Self { let mut auth = AuthContext::from_identity(identity, session_id); auth.database_id = Some(database_id); - Self::with_auth(tenant_id, collection, auth, rls, audit) + Self::with_auth(tenant_id, collection, auth, services) } fn deny(&self) -> crate::Result<()> { - self.audit.emit( + self.services.audit.emit( AuditEvent::RlsRejected, &self.auth.username, "CRDT post-merge write policy rejected", @@ -180,6 +193,7 @@ impl CrdtPostImagePolicy for ExternalCrdtPostImagePolicy<'_> { return Ok(()); } let policies = self + .services .rls .write_policies(self.tenant_id.as_u64(), self.collection); if policies.is_empty() { @@ -205,12 +219,13 @@ impl CrdtPostImagePolicy for ExternalCrdtPostImagePolicy<'_> { }; }; let document = serde_json::Value::from(value); - self.rls.check_write_with_auth( + self.services.rls.check_write_with_auth( self.tenant_id.as_u64(), self.collection, &document, &self.auth, - self.audit, + self.services.audit, + &nodedb_crs::CrsContext::new(self.services.crs_set), ) } } @@ -288,8 +303,11 @@ mod tests { principal.tenant_id, "orders", AuthContext::from_identity(&principal, "test".into()), - &store, - &audit, + PolicyServices { + rls: &store, + audit: &audit, + crs_set: nodedb_crs::CustomCrsSet::empty(), + }, ); assert!( policy @@ -315,8 +333,11 @@ mod tests { principal.tenant_id, "orders", AuthContext::from_identity(&principal, "test".into()), - &store, - &audit, + PolicyServices { + rls: &store, + audit: &audit, + crs_set: nodedb_crs::CustomCrsSet::empty(), + }, ); let malformed = CrdtPreviewResult { post_image_msgpack: vec![0x8f], @@ -348,8 +369,11 @@ mod tests { principal.tenant_id, "orders", AuthContext::from_identity(&principal, "test".into()), - &constrained, - &audit, + PolicyServices { + rls: &constrained, + audit: &audit, + crs_set: nodedb_crs::CustomCrsSet::empty(), + }, ); assert!(matches!( constrained_policy.evaluate(&preview(None)), @@ -361,8 +385,11 @@ mod tests { principal.tenant_id, "orders", AuthContext::from_identity(&principal, "test".into()), - &vacuous, - &audit, + PolicyServices { + rls: &vacuous, + audit: &audit, + crs_set: nodedb_crs::CustomCrsSet::empty(), + }, ); assert!(vacuous_policy.evaluate(&preview(None)).is_ok()); } @@ -380,8 +407,11 @@ mod tests { principal.tenant_id, "orders", AuthContext::from_identity(&principal, "test".into()), - &store, - &audit, + PolicyServices { + rls: &store, + audit: &audit, + crs_set: nodedb_crs::CustomCrsSet::empty(), + }, ); assert!( policy @@ -402,8 +432,11 @@ mod tests { root.tenant_id, "orders", AuthContext::from_identity(&root, "test".into()), - &store, - &audit, + PolicyServices { + rls: &store, + audit: &audit, + crs_set: nodedb_crs::CustomCrsSet::empty(), + }, ); assert!( root_policy @@ -433,8 +466,11 @@ mod tests { "orders", &principal, "test".into(), - &store, - &audit, + PolicyServices { + rls: &store, + audit: &audit, + crs_set: nodedb_crs::CustomCrsSet::empty(), + }, ); assert!( diff --git a/nodedb/src/control/crs_context.rs b/nodedb/src/control/crs_context.rs new file mode 100644 index 000000000..631cd5b53 --- /dev/null +++ b/nodedb/src/control/crs_context.rs @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! The CRS context the Control Plane resolves SRIDs against. + +use std::sync::Arc; + +use nodedb_crs::{CrsContext, CustomCrsSet}; + +use crate::control::security::catalog::SystemCatalog; +use crate::control::state::SharedState; + +/// The CRS context for one statement planned or evaluated against +/// `catalog`: its user-defined CRSs first, then the built-in registry. The +/// context owns its share of the set, so it borrows nothing. +pub fn catalog_crs_context(catalog: &SystemCatalog) -> CrsContext<'static> { + CrsContext::shared(catalog.custom_crs_set()) +} + +/// The user-defined CRS set SRIDs resolve against under `state`: a share +/// of the set the catalog holds, as of this call. +pub fn crs_set_for(state: &SharedState) -> Arc { + state.credentials.catalog().custom_crs_set() +} + +/// The CRS context for one statement planned or evaluated against `state`. +pub fn crs_context_for(state: &SharedState) -> CrsContext<'static> { + catalog_crs_context(state.credentials.catalog()) +} diff --git a/nodedb/src/control/distributed_applier/apply_loop/start.rs b/nodedb/src/control/distributed_applier/apply_loop/start.rs index 3c726f71d..1ba41b81c 100644 --- a/nodedb/src/control/distributed_applier/apply_loop/start.rs +++ b/nodedb/src/control/distributed_applier/apply_loop/start.rs @@ -149,6 +149,9 @@ fn entry_scope(decoded: &mut Option) -> EntryScope { .as_mut() .map(|e| std::mem::take(&mut e.user_roles)) .unwrap_or_default(), + system_ordinal: decoded + .as_ref() + .and_then(ReplicatedEntry::carried_system_ordinal), } } diff --git a/nodedb/src/control/distributed_applier/apply_loop/write_dispatch.rs b/nodedb/src/control/distributed_applier/apply_loop/write_dispatch.rs index 09c6f93b7..5f1f158e8 100644 --- a/nodedb/src/control/distributed_applier/apply_loop/write_dispatch.rs +++ b/nodedb/src/control/distributed_applier/apply_loop/write_dispatch.rs @@ -45,6 +45,9 @@ pub(super) struct EntryScope { /// The roles of the identity that wrote the write, which every replica /// judges a role-guarded state transition against. pub user_roles: Vec, + /// The system-time ordinal the entry's leader assigned, which every + /// replica stamps the write's bitemporal versions from. + pub system_ordinal: Option, } /// Prepare a generic entry. `exclusive` marks an entry whose apply awaits its @@ -89,6 +92,7 @@ async fn enqueue_generic_entry<'a>( event_source, incarnations, user_roles, + system_ordinal, } = scope; let ApplyContext { state, tracker, .. } = ctx; let AppliedPosition { @@ -253,12 +257,15 @@ async fn enqueue_generic_entry<'a>( // resolved, so this replica's redo record and its live apply // install the byte-identical value every other replica does. // `change_position` gives the write's change events the entry's - // log position, which every replica shares. + // log position, which every replica shares. `system_ordinal` is + // the stamp the entry's leader assigned, which every replica's + // bitemporal versions take. durability: WalDurability::AppendHere { now_override: resolved_now_ms, apply_key: applied_key, commit_hlc: pos.carried_commit_hlc(), change_position: Some(pos.change_position(state, vshard_id.as_u32())), + system_ordinal, }, // Raft committed this entry at a fixed log index; every // replica applies it in that order. Re-entering the diff --git a/nodedb/src/control/exec_receiver/executor.rs b/nodedb/src/control/exec_receiver/executor.rs index 4bc66676d..884b556a3 100644 --- a/nodedb/src/control/exec_receiver/executor.rs +++ b/nodedb/src/control/exec_receiver/executor.rs @@ -271,15 +271,32 @@ impl LocalPlanExecutor { } } - if let Err(error) = - confirm_read_leg(&self.state, database_id, &plan, &req.read_groups, deadline).await + // The read confirm, the cut wait, and the read share one instant: the + // coordinator's remaining budget. + let hop_deadline = + crate::control::server::shared::session::StatementDeadline::after(deadline); + if let Err(error) = confirm_read_leg( + &self.state, + database_id, + &plan, + &req.read_groups, + hop_deadline.remaining(), + ) + .await { return ExecuteResponse::err(error); } + // A historical read runs only once this node installed every + // version at or below its cut. + if let Err(error) = + crate::control::read_cut::await_read_cut(&self.state, &plan, hop_deadline).await + { + return ExecuteResponse::err(execution_error_to_typed(error)); + } // A vShard-scoped plan runs on its one owning core. Every other plan // fans across all local cores. - match tokio::time::timeout( - deadline, + match tokio::time::timeout_at( + hop_deadline.tokio_instant(), execute_received_plan( &self.state, tenant_id, diff --git a/nodedb/src/control/gateway/dispatcher.rs b/nodedb/src/control/gateway/dispatcher.rs index 68050b747..3f63f4054 100644 --- a/nodedb/src/control/gateway/dispatcher.rs +++ b/nodedb/src/control/gateway/dispatcher.rs @@ -85,6 +85,16 @@ pub(crate) async fn dispatch_route( match route.decision { RouteDecision::Local => { confirm_local_read(shared, database_id, &route.plan, &read_groups, deadline_ms).await?; + // A historical read runs only once this node installed its cut + // (`read_cut::wait`). + crate::control::read_cut::await_read_cut( + shared, + &route.plan, + crate::control::server::shared::session::StatementDeadline::after( + std::time::Duration::from_millis(deadline_ms), + ), + ) + .await?; dispatch_local( route, LocalContext { diff --git a/nodedb/src/control/gateway/error_map/class_parity/code_samples.rs b/nodedb/src/control/gateway/error_map/class_parity/code_samples.rs index aa3a526bb..b424ceefb 100644 --- a/nodedb/src/control/gateway/error_map/class_parity/code_samples.rs +++ b/nodedb/src/control/gateway/error_map/class_parity/code_samples.rs @@ -8,7 +8,7 @@ use nodedb_types::sync::wire::SyncProvenance; use crate::bridge::envelope::{CounterFault, ErrorCode, SyncHold}; /// The number of `ErrorCode` variants [`variant_index`] numbers. -pub(super) const VARIANT_COUNT: usize = 53; +pub(super) const VARIANT_COUNT: usize = 54; /// A dense index per variant. Exhaustive, so a new variant fails to compile /// here until it gets an index, and [`every_variant_has_a_sample`] then fails @@ -61,6 +61,7 @@ pub(super) fn variant_index(code: &ErrorCode) -> usize { ErrorCode::TextColumn { .. } => 43, ErrorCode::NumericValueOutOfRange { .. } => 44, ErrorCode::NodeLabelLimit { .. } => 45, + ErrorCode::ProgramLimitExceeded { .. } => 53, ErrorCode::InvalidTextRepresentation { .. } => 46, ErrorCode::DatatypeMismatch { .. } => 47, ErrorCode::InvalidDatetimeFormat { .. } => 48, @@ -221,6 +222,7 @@ pub(super) fn samples() -> Vec { label: "Person".into(), limit: 64, }, + ErrorCode::ProgramLimitExceeded { detail: text() }, ]; for constraint in [ "not_null", diff --git a/nodedb/src/control/gateway/error_map/class_parity/error_index.rs b/nodedb/src/control/gateway/error_map/class_parity/error_index.rs index 6e7541dc1..1c3cbd0b5 100644 --- a/nodedb/src/control/gateway/error_map/class_parity/error_index.rs +++ b/nodedb/src/control/gateway/error_map/class_parity/error_index.rs @@ -3,7 +3,7 @@ //! A dense index per `crate::Error` variant. /// The number of `crate::Error` variants [`error_variant_index`] numbers. -pub(super) const ERROR_VARIANT_COUNT: usize = 126; +pub(super) const ERROR_VARIANT_COUNT: usize = 127; /// A dense index per `crate::Error` variant. Exhaustive, so a new variant /// fails to compile here until it gets an index, and @@ -138,5 +138,6 @@ pub(crate) fn error_variant_index(err: &crate::Error) -> usize { E::WrongObjectType { .. } => 123, E::MixedSrid { .. } => 124, E::UngroupedColumn { .. } => 125, + E::UnknownSrid { .. } => 126, } } diff --git a/nodedb/src/control/gateway/error_map/class_parity/error_samples.rs b/nodedb/src/control/gateway/error_map/class_parity/error_samples.rs index 0016f90ce..d984ebe6b 100644 --- a/nodedb/src/control/gateway/error_map/class_parity/error_samples.rs +++ b/nodedb/src/control/gateway/error_map/class_parity/error_samples.rs @@ -221,6 +221,9 @@ pub(crate) fn error_samples() -> Vec { index_srid: 4326, row_srid: 32648, }, + E::UnknownSrid { + source: nodedb_crs::UnknownSrid::Undefined { code: 999 }, + }, E::NumericValueOutOfRange { detail: text() }, E::InvalidTextRepresentation { detail: text() }, E::DatatypeMismatch { detail: text() }, diff --git a/nodedb/src/control/gateway/error_map/resp.rs b/nodedb/src/control/gateway/error_map/resp.rs index 2ee02b72e..b1f7c050a 100644 --- a/nodedb/src/control/gateway/error_map/resp.rs +++ b/nodedb/src/control/gateway/error_map/resp.rs @@ -97,6 +97,7 @@ impl GatewayErrorMap { | Error::DivisionByZero | Error::DataException { .. } | Error::MixedSrid { .. } + | Error::UnknownSrid { .. } | Error::NumericValueOutOfRange { .. } | Error::InvalidTextRepresentation { .. } | Error::DatatypeMismatch { .. } diff --git a/nodedb/src/control/gateway/invalidation.rs b/nodedb/src/control/gateway/invalidation.rs index ba95351be..83b3c83c2 100644 --- a/nodedb/src/control/gateway/invalidation.rs +++ b/nodedb/src/control/gateway/invalidation.rs @@ -49,6 +49,16 @@ impl PlanCacheInvalidator { cache.invalidate_descriptor(name, new_version); } } + + /// Evict every cache entry. + /// + /// No-op if the plan cache has been dropped. + pub fn invalidate_all(&self) { + if let Some(cache) = self.cache.upgrade() { + debug!("gateway plan cache: invalidating every entry"); + cache.clear(); + } + } } #[cfg(test)] diff --git a/nodedb/src/control/gateway/plan_cache.rs b/nodedb/src/control/gateway/plan_cache.rs index e2a0f7076..b7d963b4f 100644 --- a/nodedb/src/control/gateway/plan_cache.rs +++ b/nodedb/src/control/gateway/plan_cache.rs @@ -194,6 +194,14 @@ impl PlanCache { }); } + /// Evict every entry. A change that no version set names, such as a + /// dropped user-defined CRS a plan resolved, makes any plan stale. + pub fn clear(&self) { + let mut inner = self.inner.lock().unwrap_or_else(|p| p.into_inner()); + inner.version_set_index.clear(); + inner.entries.clear(); + } + /// Look up the most recently stored `GatewayVersionSet` for a SQL key. /// /// Used by `execute_sql` for the two-phase cache lookup: check the side diff --git a/nodedb/src/control/gateway/version_set/plan_keys.rs b/nodedb/src/control/gateway/version_set/plan_keys.rs index d09592bb9..638cf9a5c 100644 --- a/nodedb/src/control/gateway/version_set/plan_keys.rs +++ b/nodedb/src/control/gateway/version_set/plan_keys.rs @@ -238,7 +238,6 @@ pub fn touched_collections(plan: &PhysicalPlan) -> Vec { | Match { .. } | MatchContinuation { .. } | MatchVarLenResume { .. } - | TemporalAlgorithm { .. } | Stats { collection: None, .. } diff --git a/nodedb/src/control/lease/drain_propose.rs b/nodedb/src/control/lease/drain_propose.rs index 808169f1e..2c6379208 100644 --- a/nodedb/src/control/lease/drain_propose.rs +++ b/nodedb/src/control/lease/drain_propose.rs @@ -122,7 +122,7 @@ fn count_matching_leases( ) -> usize { let instant = Instant::now(); let now = nodedb_cluster::LeaseNow { - wall_ns: super::wall_now_ns(), + wall_ns: nodedb_types::wall_now_ns(), instant, // Reading the leader term takes the raft coordinator lock, so skip // it unless some holder can qualify for early release. @@ -288,7 +288,7 @@ mod tests { /// nothing. fn unexpired() -> nodedb_types::Hlc { nodedb_types::Hlc::new( - super::super::wall_now_ns().saturating_add(60_000_000_000), + nodedb_types::wall_now_ns().saturating_add(60_000_000_000), 0, ) } @@ -296,7 +296,7 @@ mod tests { /// A lease expiry a minute in the past, in REAL wall time. fn expired() -> nodedb_types::Hlc { nodedb_types::Hlc::new( - super::super::wall_now_ns().saturating_sub(60_000_000_000), + nodedb_types::wall_now_ns().saturating_sub(60_000_000_000), 0, ) } @@ -395,11 +395,11 @@ mod tests { // An HLC an hour ahead of real time, folded into the local clock. let skewed = nodedb_types::Hlc::new( - super::super::wall_now_ns().saturating_add(3_600_000_000_000), + nodedb_types::wall_now_ns().saturating_add(3_600_000_000_000), 0, ); state.hlc_clock.update(skewed); - assert!(state.hlc_clock.peek().wall_ns > super::super::wall_now_ns()); + assert!(state.hlc_clock.peek().wall_ns > nodedb_types::wall_now_ns()); insert_lease(&state, &descriptor, 1, 1, unexpired()); assert_eq!( @@ -523,7 +523,7 @@ mod tests { let (state, remote, _directory) = state_with_remote_member(); let descriptor = orders(); let just_expired = - nodedb_types::Hlc::new(super::super::wall_now_ns().saturating_sub(1_000_000_000), 0); + nodedb_types::Hlc::new(nodedb_types::wall_now_ns().saturating_sub(1_000_000_000), 0); insert_lease(&state, &descriptor, remote, 1, just_expired); assert_eq!( diff --git a/nodedb/src/control/lease/mod.rs b/nodedb/src/control/lease/mod.rs index e248e1a86..91ffcef99 100644 --- a/nodedb/src/control/lease/mod.rs +++ b/nodedb/src/control/lease/mod.rs @@ -38,10 +38,8 @@ pub mod renewal; pub mod runtime; mod self_fence; pub mod shutdown_release; -mod wall_time; pub(crate) use self_fence::lease_use_is_fenced; -pub(super) use wall_time::wall_now_ns; pub use descriptor_lookup::{ clear_implicit_drains, descriptor_id_and_prior_version, descriptor_id_for_implicit_clear, diff --git a/nodedb/src/control/lease/renewal.rs b/nodedb/src/control/lease/renewal.rs index 07fecec82..e5555c7d4 100644 --- a/nodedb/src/control/lease/renewal.rs +++ b/nodedb/src/control/lease/renewal.rs @@ -180,7 +180,7 @@ impl LeaseRenewalLoop { let Some(shared) = self.shared.upgrade() else { return; }; - let now_wall_ns = super::wall_now_ns(); + let now_wall_ns = nodedb_types::wall_now_ns(); let near_expiry = collect_near_expiry(&shared, now_wall_ns, &self.config); if near_expiry.is_empty() { return; diff --git a/nodedb/src/control/lease/wall_time.rs b/nodedb/src/control/lease/wall_time.rs deleted file mode 100644 index 518401e29..000000000 --- a/nodedb/src/control/lease/wall_time.rs +++ /dev/null @@ -1,15 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 - -//! Wall-clock helper shared by every lease code path that needs a -//! "real time now" reference — the renewal loop, the drain tracker -//! check, and the drain TTL computation. See `lease::renewal::tick` -//! for the rationale on why these paths use wall clock directly -//! instead of `HlcClock::peek`. - -/// Wall-clock nanoseconds since Unix epoch. -pub(crate) fn wall_now_ns() -> u64 { - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|d| d.as_nanos() as u64) - .unwrap_or(0) -} diff --git a/nodedb/src/control/maintenance/clone_materializer/chained.rs b/nodedb/src/control/maintenance/clone_materializer/chained.rs index 4e359510a..d1701f168 100644 --- a/nodedb/src/control/maintenance/clone_materializer/chained.rs +++ b/nodedb/src/control/maintenance/clone_materializer/chained.rs @@ -86,7 +86,7 @@ pub(super) async fn materialize_chained_collection( source_db_id: origin.source_database, source_qualified: &source_qualified, cursor: &cursor, - system_as_of_ms, + system_time: super::document::clone_scope(system_as_of_ms), raw_bodies: true, }; let (entries, next_cursor) = scan_page(state, page, None).await?; diff --git a/nodedb/src/control/maintenance/clone_materializer/columnar.rs b/nodedb/src/control/maintenance/clone_materializer/columnar.rs index 901d130c9..a35439c3e 100644 --- a/nodedb/src/control/maintenance/clone_materializer/columnar.rs +++ b/nodedb/src/control/maintenance/clone_materializer/columnar.rs @@ -248,6 +248,7 @@ impl ColumnarCopy<'_> { schema_bytes: Vec::new(), provenance: None, wal_lsn: None, + system_from: None, // `materialize_one` (walker.rs) already refused this // materialization if either side carried an RLS policy, // so no policy applies to source or target here — this diff --git a/nodedb/src/control/maintenance/clone_materializer/document.rs b/nodedb/src/control/maintenance/clone_materializer/document.rs index 6c9a5debf..c63fb638f 100644 --- a/nodedb/src/control/maintenance/clone_materializer/document.rs +++ b/nodedb/src/control/maintenance/clone_materializer/document.rs @@ -11,7 +11,7 @@ //! filters CoW-copied rows, and `if_absent` skips already-written rows — the //! per-page checkpoint is best-effort but the per-key probes cover it. -use nodedb_types::{DatabaseId, TenantId}; +use nodedb_types::{DatabaseId, SystemTimeScope, TenantId}; use crate::types::TxnId; @@ -105,7 +105,7 @@ pub(crate) async fn scan_source_page( source_db_id: DatabaseId, source_qualified: &str, cursor: &[u8], - system_as_of_ms: Option, + system_time: SystemTimeScope, txn_id: Option, ) -> crate::Result { let page = SourcePage { @@ -113,19 +113,26 @@ pub(crate) async fn scan_source_page( source_db_id, source_qualified, cursor, - system_as_of_ms, + system_time, raw_bodies: false, }; scan_page(state, page, txn_id).await } +/// The scope a clone copy reads its source at: `AsOf` the clone's source +/// time, or `Current` when the clone carries none. +pub(super) fn clone_scope(system_as_of_ms: Option) -> SystemTimeScope { + system_as_of_ms.map_or(SystemTimeScope::Current, SystemTimeScope::AsOf) +} + /// One source-side scan page request. pub(super) struct SourcePage<'a> { pub tenant_id: TenantId, pub source_db_id: DatabaseId, pub source_qualified: &'a str, pub cursor: &'a [u8], - pub system_as_of_ms: Option, + /// `Current`, or the point-in-time cut every page reads at. + pub system_time: SystemTimeScope, /// Bodies as stored, with no `id` added. pub raw_bodies: bool, } @@ -141,14 +148,14 @@ pub(super) async fn scan_page( source_db_id, source_qualified, cursor, - system_as_of_ms, + system_time, raw_bodies, } = page; let plan = PhysicalPlan::Document(DocumentOp::MaterializeScan { collection: nodedb_types::QualifiedCollection::from_stored(source_qualified.to_string()), cursor: cursor.to_vec(), count: SCAN_PAGE, - system_as_of_ms, + system_time, raw_bodies, }); // A transaction's staging overlay lives on the source shard's leader, so diff --git a/nodedb/src/control/maintenance/clone_materializer/document_copy.rs b/nodedb/src/control/maintenance/clone_materializer/document_copy.rs index 57bcde928..d9e1f6d10 100644 --- a/nodedb/src/control/maintenance/clone_materializer/document_copy.rs +++ b/nodedb/src/control/maintenance/clone_materializer/document_copy.rs @@ -56,7 +56,7 @@ impl RowCopy<'_> { source_db_id: self.source_db_id, source_qualified: &self.source_qualified, cursor: &cursor, - system_as_of_ms: self.system_as_of_ms, + system_time: super::document::clone_scope(self.system_as_of_ms), raw_bodies: true, }; let (entries, next_cursor) = scan_page(self.state, page, None).await?; diff --git a/nodedb/src/control/merge_orchestrator/expand_staged_merge.rs b/nodedb/src/control/merge_orchestrator/expand_staged_merge.rs index dce850b18..837677507 100644 --- a/nodedb/src/control/merge_orchestrator/expand_staged_merge.rs +++ b/nodedb/src/control/merge_orchestrator/expand_staged_merge.rs @@ -80,6 +80,7 @@ pub(crate) async fn resolve_and_emit_merge_ops( body, tenant_id.as_u64(), target_collection.as_str(), + &crate::control::crs_context::crs_context_for(state), )?; } } diff --git a/nodedb/src/control/merge_orchestrator/orchestrator.rs b/nodedb/src/control/merge_orchestrator/orchestrator.rs index 8254f0001..d96d44c02 100644 --- a/nodedb/src/control/merge_orchestrator/orchestrator.rs +++ b/nodedb/src/control/merge_orchestrator/orchestrator.rs @@ -225,6 +225,7 @@ pub(crate) async fn run_merge(state: &SharedState, args: MergeArgs<'_>) -> crate .chain(arms.inserts.iter().map(|(_, body)| body.as_slice())), args.tenant_id, args.target_collection, + &crate::control::crs_context::crs_context_for(state), )?; let insert_rows = arms.inserts; diff --git a/nodedb/src/control/metadata_proposer/ddl_prepare.rs b/nodedb/src/control/metadata_proposer/ddl_prepare.rs index a9eb2bffe..172e74fa3 100644 --- a/nodedb/src/control/metadata_proposer/ddl_prepare.rs +++ b/nodedb/src/control/metadata_proposer/ddl_prepare.rs @@ -4,7 +4,7 @@ //! serialize descriptor preparation across the cluster. use std::sync::atomic::Ordering; -use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; +use std::time::{Duration, Instant}; use nodedb_cluster::{MetadataEntry, encode_entry}; @@ -19,14 +19,6 @@ use super::wait::await_metadata_apply; const DDL_PREPARE_WAIT: Duration = super::ddl_owner::DDL_PREPARE_LEASE.saturating_add(Duration::from_secs(10)); -fn wall_now_ns() -> u64 { - SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_default() - .as_nanos() - .min(u64::MAX as u128) as u64 -} - /// Poll interval while another token holds the preparation lease. const DDL_PREPARE_POLL: Duration = Duration::from_millis(10); @@ -36,7 +28,9 @@ fn next_token(shared: &SharedState) -> u64 { .metadata_ddl .token_seq .fetch_add(1, Ordering::Relaxed); - shared.node_id.wrapping_mul(0x9e37_79b9_7f4a_7c15) ^ wall_now_ns().rotate_left(17) ^ sequence + shared.node_id.wrapping_mul(0x9e37_79b9_7f4a_7c15) + ^ nodedb_types::wall_now_ns().rotate_left(17) + ^ sequence } fn encode_metadata(entry: &MetadataEntry) -> Result, Error> { diff --git a/nodedb/src/control/mod.rs b/nodedb/src/control/mod.rs index 496097847..15bbdbb3c 100644 --- a/nodedb/src/control/mod.rs +++ b/nodedb/src/control/mod.rs @@ -17,6 +17,7 @@ pub mod cold_tier; pub mod constraint_install; pub mod crdt_admission; pub mod crdt_post_image_policy; +pub mod crs_context; pub mod custom_type; pub mod database; pub mod distributed_applier; @@ -43,6 +44,7 @@ pub mod pitr; pub mod planner; pub mod promql; pub mod propose_outcome; +pub mod read_cut; pub mod request_tracker; pub mod rolling_upgrade; pub mod router; diff --git a/nodedb/src/control/orchestrated_write.rs b/nodedb/src/control/orchestrated_write.rs index 1e52e789a..b684b3729 100644 --- a/nodedb/src/control/orchestrated_write.rs +++ b/nodedb/src/control/orchestrated_write.rs @@ -104,6 +104,7 @@ pub(crate) fn decide_write_policy_over_images<'a>( images: impl IntoIterator, tenant_id: TenantId, collection: &str, + crs: &nodedb_crs::CrsContext<'_>, ) -> crate::Result<()> { match rls_write_check.decision() { nodedb_types::WriteGateDecision::AdmitAll => Ok(()), @@ -114,6 +115,7 @@ pub(crate) fn decide_write_policy_over_images<'a>( image, tenant_id.as_u64(), collection, + crs, )?; } Ok(()) diff --git a/nodedb/src/control/otel/exporter.rs b/nodedb/src/control/otel/exporter.rs index a45cd3ed8..2a7ab1f05 100644 --- a/nodedb/src/control/otel/exporter.rs +++ b/nodedb/src/control/otel/exporter.rs @@ -10,7 +10,6 @@ use std::sync::Arc; use std::time::Duration; -use tokio::sync::watch; use tracing::{debug, warn}; use super::proto::{ @@ -19,6 +18,9 @@ use super::proto::{ SpanKind, SpanStatus, StatusCode as OtelStatusCode, }; use crate::control::metrics::SystemMetrics; +use crate::control::security::egress::EgressError; +use crate::control::shutdown::{ShutdownPhase, spawn_loop}; +use crate::control::state::SharedState; use nodedb_types::TraceId; /// Configuration for the OTLP exporter. @@ -51,61 +53,72 @@ impl ExporterConfig { } } -/// Spawn the background OTLP metrics exporter. +/// Cap on one metrics POST. +const METRICS_POST_TIMEOUT: Duration = Duration::from_secs(10); + +/// Spawn the background OTLP metrics exporter of `shared`. /// -/// Periodically reads SystemMetrics and pushes to the configured OTLP endpoint. -/// Returns a shutdown signal sender. +/// Every `config.metrics_interval` it reads the node's `SystemMetrics` and +/// posts them to `/v1/metrics` through the outbound egress client. +/// The loop joins at the Control Plane drain. The endpoint is operator +/// config: one the `[egress]` policy refuses is an error, and nothing is +/// spawned. Returns whether the exporter started: it does not when metrics +/// export is off or the state carries no metrics registry. pub fn spawn_metrics_exporter( + shared: &Arc, config: ExporterConfig, - metrics: Arc, - node_id: u64, -) -> watch::Sender { - let (shutdown_tx, mut shutdown_rx) = watch::channel(false); - +) -> Result { if !config.is_enabled() || !config.export_metrics { - return shutdown_tx; + return Ok(false); } - + let Some(metrics) = shared.system_metrics.clone() else { + return Ok(false); + }; let endpoint = format!("{}/v1/metrics", config.endpoint.trim_end_matches('/')); + shared.remote_sources.egress().check_url(&endpoint)?; + let client = Arc::clone(shared.remote_sources.http()); let interval = config.metrics_interval; - - tokio::spawn(async move { - let client = reqwest::Client::new(); - let mut ticker = tokio::time::interval(interval); - - loop { - tokio::select! { - _ = ticker.tick() => {} - _ = shutdown_rx.changed() => break, - } - - let request = build_metrics_request(&metrics, node_id); - let mut buf = Vec::new(); - if prost::Message::encode(&request, &mut buf).is_err() { - continue; - } - - match client - .post(&endpoint) - .header("content-type", "application/x-protobuf") - .body(buf) - .send() - .await - { - Ok(resp) if resp.status().is_success() => { - debug!("OTLP metrics export: OK"); + let node_id = shared.node_id; + + spawn_loop( + &shared.loop_registry, + &shared.shutdown, + "otlp_metrics_export", + ShutdownPhase::DrainingControlPlane, + move |mut shutdown| async move { + let mut ticker = tokio::time::interval(interval); + loop { + tokio::select! { + _ = shutdown.wait_cancelled() => break, + _ = ticker.tick() => {} } - Ok(resp) => { - warn!("OTLP metrics export: status {}", resp.status()); + let request = build_metrics_request(&metrics, node_id); + let mut buf = Vec::new(); + if prost::Message::encode(&request, &mut buf).is_err() { + continue; } - Err(e) => { - warn!("OTLP metrics export: {e}"); + match client + .post(&endpoint) + .header("content-type", "application/x-protobuf") + .timeout(METRICS_POST_TIMEOUT) + .body(buf) + .send() + .await + { + Ok(resp) if resp.status().is_success() => { + debug!("OTLP metrics export: OK"); + } + Ok(resp) => { + warn!("OTLP metrics export: status {}", resp.status()); + } + Err(e) => { + warn!("OTLP metrics export: {}", e.without_url()); + } } } - } - }); - - shutdown_tx + }, + ); + Ok(true) } /// Build an OTLP ExportMetricsServiceRequest from SystemMetrics. @@ -351,3 +364,52 @@ fn rand_span_id() -> [u8; 8] { id.copy_from_slice(&ts.to_le_bytes()); id } + +#[cfg(test)] +mod tests { + use super::*; + + fn test_state() -> (Arc, tempfile::TempDir) { + let dir = tempfile::tempdir().expect("tempdir"); + let wal = Arc::new( + crate::wal::WalManager::open_for_testing(&dir.path().join("otlp.wal")).expect("wal"), + ); + let (dispatcher, _sides) = crate::bridge::dispatch::Dispatcher::new(1, 64); + let state = SharedState::new(dispatcher, wal).expect("shared state"); + (state, dir) + } + + fn metrics_config(endpoint: &str) -> ExporterConfig { + ExporterConfig { + endpoint: endpoint.into(), + export_metrics: true, + ..ExporterConfig::default() + } + } + + #[tokio::test] + async fn a_metrics_endpoint_outside_the_egress_policy_is_refused() { + let (state, _dir) = test_state(); + for (endpoint, expected) in [ + ("http://127.0.0.1:4318", EgressError::IpLiteral), + ("http://otel-collector:4318", EgressError::NonHttps), + ] { + let refused = spawn_metrics_exporter(&state, metrics_config(endpoint)); + assert_eq!(refused.err(), Some(expected), "{endpoint}"); + } + } + + #[tokio::test] + async fn an_unconfigured_exporter_never_starts() { + let (state, _dir) = test_state(); + let off = ExporterConfig { + endpoint: "https://otel.example".into(), + ..ExporterConfig::default() + }; + assert_eq!(spawn_metrics_exporter(&state, off), Ok(false)); + assert_eq!( + spawn_metrics_exporter(&state, ExporterConfig::default()), + Ok(false) + ); + } +} diff --git a/nodedb/src/control/planner/calvin/dependent_recon_node_edges.rs b/nodedb/src/control/planner/calvin/dependent_recon_node_edges.rs index dbe90cc81..d8bb72f58 100644 --- a/nodedb/src/control/planner/calvin/dependent_recon_node_edges.rs +++ b/nodedb/src/control/planner/calvin/dependent_recon_node_edges.rs @@ -23,7 +23,7 @@ use std::collections::{BTreeSet, HashMap}; use nodedb_physical::physical_plan::{BatchEdge, GraphOp, PhysicalPlan}; use nodedb_physical::physical_task::{PhysicalTask, PostSetOp}; use nodedb_types::graph::Direction; -use nodedb_types::{QualifiedCollection, Surrogate, SystemTimeScope, Value}; +use nodedb_types::{QualifiedCollection, Surrogate, Value}; use super::dependent_recon_predicate::EdgeLifecycle; use super::preexec::{PreexecRequest, PreexecScan, RowIdentityRead, run_preexec_scan}; @@ -115,8 +115,7 @@ pub(super) async fn read_node_incident_edges( node_id: node.clone(), edge_label: None, direction, - system_time: SystemTimeScope::Current, - valid_at_ms: None, + scope: nodedb_physical::physical_plan::GraphReadScope::default(), rls_filters: Vec::new(), }); let payload = @@ -293,6 +292,8 @@ fn build_node_delete_tasks( dst_id: dst.clone(), src_surrogate: surrogate_of(src)?, dst_surrogate: surrogate_of(dst)?, + valid_from_ms: None, + valid_until_ms: None, }); } guards.push(PhysicalTask { diff --git a/nodedb/src/control/planner/calvin/dispatch.rs b/nodedb/src/control/planner/calvin/dispatch.rs index fc8583c3e..1645bb70b 100644 --- a/nodedb/src/control/planner/calvin/dispatch.rs +++ b/nodedb/src/control/planner/calvin/dispatch.rs @@ -362,11 +362,13 @@ mod tests { collection: QualifiedCollection::new(DatabaseId::DEFAULT, "metrics"), filters: vec![], updates: vec![], + system_from: None, rls_write_check: nodedb_types::RlsWriteCheck::pending_injection(), }); let delete = PhysicalPlan::Columnar(ColumnarOp::Delete { collection: QualifiedCollection::new(DatabaseId::DEFAULT, "metrics"), filters: vec![], + system_from: None, rls_write_check: nodedb_types::RlsWriteCheck::pending_injection(), }); assert!( diff --git a/nodedb/src/control/planner/calvin/dispatch_multi.rs b/nodedb/src/control/planner/calvin/dispatch_multi.rs index dd06df0ae..634b865c2 100644 --- a/nodedb/src/control/planner/calvin/dispatch_multi.rs +++ b/nodedb/src/control/planner/calvin/dispatch_multi.rs @@ -272,6 +272,8 @@ mod tests { properties: Vec::new(), src_surrogate: Surrogate::new(1), dst_surrogate: Surrogate::new(2), + valid_from_ms: None, + valid_until_ms: None, }), post_set_op: PostSetOp::None, txn_id: None, diff --git a/nodedb/src/control/planner/calvin/edge_truncate.rs b/nodedb/src/control/planner/calvin/edge_truncate.rs index 232f6bbad..bdadea183 100644 --- a/nodedb/src/control/planner/calvin/edge_truncate.rs +++ b/nodedb/src/control/planner/calvin/edge_truncate.rs @@ -12,7 +12,7 @@ //! transaction's ordinal. The cut writes no edge version and reads no //! stored edge: every read hides the collection's versions applied below //! it. Every edge write runs as a Calvin transaction -//! ([`super::edge_sequencing`]), so every edge version is applied at a +//! ([`super::graph_sequencing`]), so every edge version is applied at a //! Calvin ordinal. An edge sequenced before the TRUNCATE is hidden, and //! one sequenced after it stays, on every replica, under any clock skew //! and in any order a core applies the transactions in. diff --git a/nodedb/src/control/planner/calvin/edge_sequencing.rs b/nodedb/src/control/planner/calvin/graph_sequencing.rs similarity index 60% rename from nodedb/src/control/planner/calvin/edge_sequencing.rs rename to nodedb/src/control/planner/calvin/graph_sequencing.rs index a2fb73234..6bada05a8 100644 --- a/nodedb/src/control/planner/calvin/edge_sequencing.rs +++ b/nodedb/src/control/planner/calvin/graph_sequencing.rs @@ -1,6 +1,6 @@ // SPDX-License-Identifier: BUSL-1.1 -//! Every edge write runs as a Calvin transaction. +//! Every edge write and every node-label write runs as a Calvin transaction. //! //! The edge versions of a collection live in one ordering domain: the //! Calvin sequence. An edge version is applied at its transaction's ordinal. @@ -8,14 +8,20 @@ //! version applied below it. So on every replica, and under any clock skew, //! a TRUNCATE hides exactly the versions sequenced before it. //! +//! A node-label version takes its transaction's ordinal too, carried in the +//! redo record every replica installs. Every replica then writes the same +//! label version at the same ordinal, and a historical read at a pinned cut +//! reads the same labels on every node. +//! //! A plain data-group Raft entry cannot give that. It carries no Calvin -//! ordinal, so its edge versions order against no TRUNCATE cut. It also -//! takes no Calvin lock, so a node delete's guard does not order against it. +//! ordinal, so its versions order against no TRUNCATE cut and each replica +//! stamps them from its own clock. It also takes no Calvin lock, so a node +//! delete's guard does not order against it. //! //! The Raft propose seams (`propose_replicated_entry`, `propose_sync_write`) -//! hand every edge write here. A session COMMIT that buffered an edge write -//! commits through Calvin. A RESTORE re-issues its edge versions as Calvin -//! transactions of its own, which carry the restore's mark (see +//! hand every edge and label write here. A session COMMIT that buffered one +//! commits through Calvin. A RESTORE re-issues its edge and label versions +//! as Calvin transactions of its own, which carry the restore's mark (see //! `backup::restore::redo_reissue`). use std::future::Future; @@ -44,7 +50,8 @@ pub fn is_edge_write(plan: &PhysicalPlan) -> bool { | GraphOp::EdgePutBatch { .. } | GraphOp::EdgeDeleteBatch { .. } => true, // Guards and a TRUNCATE's edge share run only inside a Calvin - // transaction already. Label writes version no edge. + // transaction already. A label write versions no edge + // (`is_label_write` names it). GraphOp::NodeEdgeGuard { .. } | GraphOp::NodePresenceGuard { .. } | GraphOp::TruncateEdges { .. } @@ -64,19 +71,61 @@ pub fn is_edge_write(plan: &PhysicalPlan) -> bool { | GraphOp::BspSuperstep(_) | GraphOp::WccSuperstep(_) | GraphOp::TemporalNeighbors { .. } - | GraphOp::TemporalAlgorithm { .. } | GraphOp::Stats { .. } | GraphOp::NodePresenceRead { .. } => false, } } -/// Whether any task among `tasks` writes an edge. -pub fn writes_edges(tasks: &[PhysicalTask]) -> bool { - tasks.iter().any(|task| is_edge_write(&task.plan)) +/// Whether `plan` writes node labels. Exhaustive over `GraphOp`: a new graph +/// op is a compile error here. +pub fn is_label_write(plan: &PhysicalPlan) -> bool { + let PhysicalPlan::Graph(op) = plan else { + return false; + }; + match op { + GraphOp::SetNodeLabels { .. } | GraphOp::RemoveNodeLabels { .. } => true, + GraphOp::EdgePut { .. } + | GraphOp::EdgeDelete { .. } + | GraphOp::EdgePutBatch { .. } + | GraphOp::EdgeDeleteBatch { .. } + | GraphOp::NodeEdgeGuard { .. } + | GraphOp::NodePresenceGuard { .. } + | GraphOp::TruncateEdges { .. } + | GraphOp::ResolveEdgeDelete(_) + | GraphOp::Hop { .. } + | GraphOp::Neighbors { .. } + | GraphOp::NeighborsMulti { .. } + | GraphOp::Path { .. } + | GraphOp::Subgraph { .. } + | GraphOp::RagFusion { .. } + | GraphOp::Algo { .. } + | GraphOp::Match { .. } + | GraphOp::MatchContinuation { .. } + | GraphOp::MatchVarLenResume { .. } + | GraphOp::BspSuperstep(_) + | GraphOp::WccSuperstep(_) + | GraphOp::TemporalNeighbors { .. } + | GraphOp::Stats { .. } + | GraphOp::NodePresenceRead { .. } => false, + } +} + +/// Whether `plan` is a graph write that runs only as a Calvin transaction: +/// an edge write or a node-label write. +pub fn is_sequenced_graph_write(plan: &PhysicalPlan) -> bool { + is_edge_write(plan) || is_label_write(plan) +} + +/// Whether any task among `tasks` is a graph write that runs only as a +/// Calvin transaction. +pub fn writes_sequenced_graph(tasks: &[PhysicalTask]) -> bool { + tasks + .iter() + .any(|task| is_sequenced_graph_write(&task.plan)) } -/// One edge write and the scope it runs in. -pub struct EdgeWrite { +/// One edge or label write and the scope it runs in. +pub struct GraphWrite { pub tenant_id: TenantId, pub database_id: DatabaseId, /// The vShard the write was addressed to. The transaction's participants @@ -93,12 +142,12 @@ pub struct EdgeWrite { /// /// Returns a boxed future: the Calvin submit can reach a Raft propose seam /// that calls back here, and the box gives that cycle a finite size. -pub fn sequence_edge_write<'a>( +pub fn sequence_graph_write<'a>( state: &'a SharedState, - write: EdgeWrite, + write: GraphWrite, ) -> Pin> + Send + 'a>> { Box::pin(async move { - let EdgeWrite { + let GraphWrite { tenant_id, database_id, vshard_id, @@ -124,18 +173,20 @@ pub fn sequence_edge_write<'a>( }) } -/// Run the edge write `entry` carries as a Calvin transaction. `None` when -/// `entry` writes no edge: it keeps its Raft entry. -pub async fn sequence_replicated_edge_write( +/// Run the edge or label write `entry` carries as a Calvin transaction. +/// `None` when `entry` writes neither: it keeps its Raft entry. +pub async fn sequence_replicated_graph_write( state: &SharedState, entry: &ReplicatedEntry, ) -> crate::Result> { - let Some(plan) = crate::control::wal_replication::decode::edge_write_plan(&entry.write)? else { + let Some(plan) = + crate::control::wal_replication::decode::sequenced_graph_write_plan(&entry.write)? + else { return Ok(None); }; - let response = sequence_edge_write( + let response = sequence_graph_write( state, - EdgeWrite { + GraphWrite { tenant_id: TenantId::new(entry.tenant_id), database_id: DatabaseId::new(entry.database_id), vshard_id: VShardId::new(entry.vshard_id), @@ -161,14 +212,16 @@ mod tests { dst_id: "b".into(), src_surrogate: Surrogate::new(1), dst_surrogate: Surrogate::new(2), + valid_from_ms: None, + valid_until_ms: None, } } - /// Single and batched edge writes take the Calvin route. A guard, a - /// TRUNCATE share and a label write do not: they version no edge - /// outside a Calvin transaction. + /// Single and batched edge writes and label writes take the Calvin + /// route. A guard and a TRUNCATE share do not: they run only inside a + /// Calvin transaction already. #[test] - fn every_edge_write_and_only_an_edge_write_is_sequenced() { + fn every_edge_and_label_write_is_sequenced() { let put = PhysicalPlan::Graph(GraphOp::EdgePutBatch { edges: vec![batch_edge()], }); @@ -187,5 +240,13 @@ mod tests { }); assert!(!is_edge_write(&share)); assert!(!is_edge_write(&labels)); + assert!(is_sequenced_graph_write(&put)); + assert!(is_sequenced_graph_write(&labels)); + let unlabel = PhysicalPlan::Graph(GraphOp::RemoveNodeLabels { + node_id: "a".into(), + labels: vec!["L".into()], + }); + assert!(is_sequenced_graph_write(&unlabel)); + assert!(!is_sequenced_graph_write(&share)); } } diff --git a/nodedb/src/control/planner/calvin/mod.rs b/nodedb/src/control/planner/calvin/mod.rs index efd331f29..8a5043155 100644 --- a/nodedb/src/control/planner/calvin/mod.rs +++ b/nodedb/src/control/planner/calvin/mod.rs @@ -10,9 +10,9 @@ pub mod dependent_recon_plan; mod dependent_recon_predicate; pub mod dispatch; pub mod dispatch_multi; -pub mod edge_sequencing; pub mod edge_truncate; pub mod explain; +pub mod graph_sequencing; pub mod node_delete_txn; pub mod predicate; pub mod preexec; @@ -38,10 +38,11 @@ pub use dispatch::{ }; pub use dispatch_multi::dispatch_authorized_tasks_to_calvin; pub(crate) use dispatch_multi::{TxnProvenance, dispatch_strict_atomic_tasks_to_calvin}; -pub use edge_sequencing::{ - EdgeWrite, is_edge_write, sequence_edge_write, sequence_replicated_edge_write, writes_edges, -}; pub use explain::calvin_explain_preamble; +pub use graph_sequencing::{ + GraphWrite, is_edge_write, is_label_write, is_sequenced_graph_write, sequence_graph_write, + sequence_replicated_graph_write, writes_sequenced_graph, +}; pub use predicate::predicate_class_for_filters; pub use retry_loop::{DependentOutcome, DependentRetryArgs, run_dependent_with_retry}; pub use submit::{ diff --git a/nodedb/src/control/planner/calvin/submit/edge_slices.rs b/nodedb/src/control/planner/calvin/submit/edge_slices.rs index 23658f2bd..393dc60e7 100644 --- a/nodedb/src/control/planner/calvin/submit/edge_slices.rs +++ b/nodedb/src/control/planner/calvin/submit/edge_slices.rs @@ -140,6 +140,8 @@ mod tests { dst_id: dst, src_surrogate: Surrogate::new(1), dst_surrogate: Surrogate::new(2), + valid_from_ms: None, + valid_until_ms: None, } } diff --git a/nodedb/src/control/planner/calvin/tx_class/shared.rs b/nodedb/src/control/planner/calvin/tx_class/shared.rs index 5cdd4efa4..8bf9c0483 100644 --- a/nodedb/src/control/planner/calvin/tx_class/shared.rs +++ b/nodedb/src/control/planner/calvin/tx_class/shared.rs @@ -396,6 +396,8 @@ mod lockstep_tests { properties: Vec::new(), src_surrogate: Surrogate::new(1), dst_surrogate: Surrogate::new(2), + valid_from_ms: None, + valid_until_ms: None, }); let (_, gate_keys) = plan_lock_keys(&plan).expect("a single-home edge is fast-path"); for node in [src, dst.as_str()] { diff --git a/nodedb/src/control/planner/calvin/tx_class/write_keys/graph.rs b/nodedb/src/control/planner/calvin/tx_class/write_keys/graph.rs index 58c935872..79f578a25 100644 --- a/nodedb/src/control/planner/calvin/tx_class/write_keys/graph.rs +++ b/nodedb/src/control/planner/calvin/tx_class/write_keys/graph.rs @@ -94,7 +94,6 @@ pub(super) fn add_keys(keys: &mut WriteKeys, op: &GraphOp) -> crate::Result<()> | GraphOp::BspSuperstep(_) | GraphOp::WccSuperstep(_) | GraphOp::TemporalNeighbors { .. } - | GraphOp::TemporalAlgorithm { .. } | GraphOp::Stats { .. } | GraphOp::NodePresenceRead { .. } => { return Err(not_a_write("a graph read")); diff --git a/nodedb/src/control/planner/calvin/tx_class/write_keys/plan.rs b/nodedb/src/control/planner/calvin/tx_class/write_keys/plan.rs index f56673983..3d76f76e4 100644 --- a/nodedb/src/control/planner/calvin/tx_class/write_keys/plan.rs +++ b/nodedb/src/control/planner/calvin/tx_class/write_keys/plan.rs @@ -188,6 +188,8 @@ mod tests { dst_id: dst.to_owned(), src_surrogate: Surrogate::new(1), dst_surrogate: Surrogate::new(2), + valid_from_ms: None, + valid_until_ms: None, } } @@ -442,6 +444,8 @@ mod tests { properties: Vec::new(), src_surrogate: edge.src_surrogate, dst_surrogate: edge.dst_surrogate, + valid_from_ms: None, + valid_until_ms: None, }) }) .collect(), @@ -482,6 +486,7 @@ mod tests { PhysicalPlan::Columnar(ColumnarOp::Delete { collection: qualified("col"), filters: Vec::new(), + system_from: None, rls_write_check: nodedb_types::RlsWriteCheck::pending_injection(), }), PhysicalPlan::Vector(VectorOp::SparseDelete { diff --git a/nodedb/src/control/planner/calvin/tx_class/write_keys/restore.rs b/nodedb/src/control/planner/calvin/tx_class/write_keys/restore.rs index f5ddf966b..7a9829544 100644 --- a/nodedb/src/control/planner/calvin/tx_class/write_keys/restore.rs +++ b/nodedb/src/control/planner/calvin/tx_class/write_keys/restore.rs @@ -5,18 +5,22 @@ //! A restored row locks what every point writer of the row locks: its //! surrogate and its row id. A restored edge version locks what every edge //! write locks: its surrogate pair and both endpoints' node lock pairs, on -//! both endpoint homes. So a restored write orders against every other -//! writer of the same row or edge, and against a node delete's guard. +//! both endpoint homes. Restored node labels lock what every label write of +//! the node locks. So a restored write orders against every other writer of +//! the same row, edge or node labels, and against a node delete's guard. use nodedb_physical::physical_plan::RestoredRedo; +use super::graph::NODE_LABEL_LOCKS; use super::plan::unsequenced; use super::set::WriteKeys; /// Add the write keys of the RESTORE batch `batch` to `keys`. pub(super) fn add_keys(keys: &mut WriteKeys, batch: &RestoredRedo) -> crate::Result<()> { - if batch.rows.is_empty() && batch.edges.is_empty() { - return Err(unsequenced("a RESTORE batch carries no row and no edge")); + if batch.rows.is_empty() && batch.edges.is_empty() && batch.node_labels.is_empty() { + return Err(unsequenced( + "a RESTORE batch carries no row, no edge and no node label", + )); } for row in &batch.rows { keys.rows(&row.collection, [row.surrogate]); @@ -30,5 +34,8 @@ pub(super) fn add_keys(keys: &mut WriteKeys, batch: &RestoredRedo) -> crate::Res (edge.src_surrogate, edge.dst_surrogate), ); } + for node in &batch.node_labels { + keys.node(NODE_LABEL_LOCKS, &node.node_id); + } Ok(()) } diff --git a/nodedb/src/control/planner/calvin/write_class.rs b/nodedb/src/control/planner/calvin/write_class.rs index 8f1471ec1..1d5be1a1f 100644 --- a/nodedb/src/control/planner/calvin/write_class.rs +++ b/nodedb/src/control/planner/calvin/write_class.rs @@ -234,7 +234,6 @@ fn graph_is_write(op: &GraphOp) -> bool { | GraphOp::BspSuperstep(_) | GraphOp::WccSuperstep(_) | GraphOp::TemporalNeighbors { .. } - | GraphOp::TemporalAlgorithm { .. } | GraphOp::Stats { .. } | GraphOp::NodePresenceRead { .. } => false, } @@ -608,6 +607,8 @@ mod tests { dst_id: "b".to_owned(), src_surrogate: Surrogate::new(1), dst_surrogate: Surrogate::new(2), + valid_from_ms: None, + valid_until_ms: None, }; let plan = PhysicalPlan::Graph(GraphOp::EdgePutBatch { edges: vec![edge] }); assert!( @@ -625,6 +626,8 @@ mod tests { dst_id: "b".to_owned(), src_surrogate: Surrogate::new(1), dst_surrogate: Surrogate::new(2), + valid_from_ms: None, + valid_until_ms: None, }; let plan = PhysicalPlan::Graph(GraphOp::EdgeDeleteBatch { edges: vec![edge] }); assert!( diff --git a/nodedb/src/control/planner/catalog_adapter/sql_catalog_impl.rs b/nodedb/src/control/planner/catalog_adapter/sql_catalog_impl.rs index 8fcf6a2cf..4a170dc9e 100644 --- a/nodedb/src/control/planner/catalog_adapter/sql_catalog_impl.rs +++ b/nodedb/src/control/planner/catalog_adapter/sql_catalog_impl.rs @@ -12,6 +12,11 @@ use super::adapter::OriginCatalog; use super::type_convert::convert_collection_type; impl SqlCatalog for OriginCatalog { + /// The built-in registry plus the user-defined CRSs of this catalog. + fn crs_context(&self) -> nodedb_crs::CrsContext<'_> { + crate::control::crs_context::catalog_crs_context(self.credentials.catalog()) + } + /// Resolve any relation name to planner metadata. Catalog tables /// (pg_class, information_schema.*, _system.*, etc.) are returned /// directly from the static schema registry without a catalog round-trip. @@ -355,6 +360,7 @@ impl SqlCatalog for OriginCatalog { a.dtype, ), nullable: a.nullable, + meta: crate::control::array_catalog::attr_meta::meta_ast(&a.meta), }) .collect(); diff --git a/nodedb/src/control/planner/context/query/context.rs b/nodedb/src/control/planner/context/query/context.rs index fb52dabb3..03c28b854 100644 --- a/nodedb/src/control/planner/context/query/context.rs +++ b/nodedb/src/control/planner/context/query/context.rs @@ -33,6 +33,11 @@ pub struct QueryContext { pub(super) array_catalog: Option, /// WAL allocator — required by array DML for `wal_lsn` allocation. pub(super) wal: Option>, + /// The node a statement's `AS OF SYSTEM TIME` cut is pinned against + /// (`statement_cut`). `None` before the node installs its gateway: a + /// cut then stays unpinned, and the Data Plane reads it at the last + /// ordinal of its millisecond. + pub(super) cut_state: Option>, /// The node's surrogate assigner. Every write the planner lowers binds /// its rows through it. pub(super) surrogate_assigner: Arc, @@ -130,6 +135,7 @@ impl QueryContext { // context can plan array DML. array_catalog: Some(state.array_catalog.clone()), wal: None, + cut_state: state.self_arc().ok().map(|shared| Arc::downgrade(&shared)), surrogate_assigner: Arc::clone(&state.surrogate_assigner), cluster_enabled: state.cluster_topology.is_some(), bitemporal_retention_registry: Some(Arc::clone(&state.bitemporal_retention_registry)), @@ -171,6 +177,7 @@ impl QueryContext { retention_registry: retention, array_catalog: Some(state.array_catalog.clone()), wal: Some(Arc::clone(&state.wal)), + cut_state: Some(Arc::downgrade(state)), surrogate_assigner: Arc::clone(&state.surrogate_assigner), cluster_enabled: state.cluster_topology.is_some(), bitemporal_retention_registry: Some(Arc::clone(&state.bitemporal_retention_registry)), diff --git a/nodedb/src/control/planner/context/query/mod.rs b/nodedb/src/control/planner/context/query/mod.rs index b0beaac2a..82b531985 100644 --- a/nodedb/src/control/planner/context/query/mod.rs +++ b/nodedb/src/control/planner/context/query/mod.rs @@ -5,6 +5,7 @@ mod convert_plans; mod functions; mod planning; mod planning_built; +mod statement_cut; mod tuning; pub use context::QueryContext; diff --git a/nodedb/src/control/planner/context/query/planning.rs b/nodedb/src/control/planner/context/query/planning.rs index f07c70203..ef1ea582d 100644 --- a/nodedb/src/control/planner/context/query/planning.rs +++ b/nodedb/src/control/planner/context/query/planning.rs @@ -271,6 +271,9 @@ impl QueryContext { .await?; self.apply_task_policy(&mut tasks, sec, tenant_id, &mut version_set) .await?; + let cache_eligibility = self + .pin_statement_cut(&mut tasks, purpose, cache_eligibility) + .await?; Ok((tasks, output_schema, version_set, cache_eligibility)) } @@ -361,6 +364,14 @@ impl QueryContext { let mut version_set = catalog.take_recorded_versions(); self.apply_task_policy(&mut tasks, sec, tenant_id, &mut version_set) .await?; + // A parameterized plan never enters a plan cache, so its verdict + // is not kept. + self.pin_statement_cut( + &mut tasks, + PlanningPurpose::Execute, + nodedb_sql::types::PlanCacheEligibility::DataDependent, + ) + .await?; Ok((tasks, output_schema, version_set)) } } diff --git a/nodedb/src/control/planner/context/query/planning_built.rs b/nodedb/src/control/planner/context/query/planning_built.rs index a12aff226..07c55bb3f 100644 --- a/nodedb/src/control/planner/context/query/planning_built.rs +++ b/nodedb/src/control/planner/context/query/planning_built.rs @@ -85,6 +85,13 @@ impl QueryContext { let mut version_set = catalog.take_recorded_versions(); self.apply_task_policy(&mut tasks, sec, tenant_id, &mut version_set) .await?; + // A built plan never enters a plan cache, so its verdict is not kept. + self.pin_statement_cut( + &mut tasks, + PlanningPurpose::Execute, + nodedb_sql::types::PlanCacheEligibility::DataDependent, + ) + .await?; Ok((tasks, output_schema, version_set)) } diff --git a/nodedb/src/control/planner/context/query/statement_cut.rs b/nodedb/src/control/planner/context/query/statement_cut.rs new file mode 100644 index 000000000..609a1c30a --- /dev/null +++ b/nodedb/src/control/planner/context/query/statement_cut.rs @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! The per-statement `AS OF SYSTEM TIME` pin of [`QueryContext`]. + +use nodedb_physical::physical_task::PhysicalTask; +use nodedb_sql::types::PlanCacheEligibility; + +use super::QueryContext; +use crate::control::planner::sql_plan_convert::PlanningPurpose; +use crate::control::planner::system_cut::pin_statement_cuts; + +impl QueryContext { + /// Pin every `AS OF SYSTEM TIME` cut of one statement's `tasks` under + /// one read cut, and return the statement's cache verdict: a capped cut + /// depends on when the statement ran, so the statement is + /// `DataDependent`. + /// + /// Metadata tasks are never dispatched and take no cut. A context with + /// no booted node leaves the cuts unpinned. The Data Plane then reads + /// each at the last ordinal of its millisecond. + pub(super) async fn pin_statement_cut( + &self, + tasks: &mut [PhysicalTask], + purpose: PlanningPurpose, + eligibility: PlanCacheEligibility, + ) -> crate::Result { + if purpose == PlanningPurpose::Metadata { + return Ok(eligibility); + } + let Some(state) = self.cut_state.as_ref().and_then(|weak| weak.upgrade()) else { + return Ok(eligibility); + }; + let pinning = pin_statement_cuts(&state, tasks).await?; + Ok(if pinning.capped { + PlanCacheEligibility::DataDependent + } else { + eligibility + }) + } +} diff --git a/nodedb/src/control/planner/implicit_edges/insert.rs b/nodedb/src/control/planner/implicit_edges/insert.rs index ff2d9a4c2..fb96ac3d4 100644 --- a/nodedb/src/control/planner/implicit_edges/insert.rs +++ b/nodedb/src/control/planner/implicit_edges/insert.rs @@ -162,6 +162,9 @@ pub async fn append_implicit_edge_tasks( properties, src_surrogate, dst_surrogate, + // An implicit edge takes its write's own valid time. + valid_from_ms: None, + valid_until_ms: None, }), post_set_op: PostSetOp::None, txn_id: None, diff --git a/nodedb/src/control/planner/implicit_edges/routed.rs b/nodedb/src/control/planner/implicit_edges/routed.rs index 7f04b3025..0a0c8b801 100644 --- a/nodedb/src/control/planner/implicit_edges/routed.rs +++ b/nodedb/src/control/planner/implicit_edges/routed.rs @@ -140,6 +140,9 @@ pub(super) async fn push_edge_put( properties, src_surrogate, dst_surrogate, + // An implicit edge takes its write's own valid time. + valid_from_ms: None, + valid_until_ms: None, }), post_set_op: PostSetOp::None, txn_id: None, diff --git a/nodedb/src/control/planner/materialized_sum/cross_shard.rs b/nodedb/src/control/planner/materialized_sum/cross_shard.rs index 99d71b357..f6328ff8f 100644 --- a/nodedb/src/control/planner/materialized_sum/cross_shard.rs +++ b/nodedb/src/control/planner/materialized_sum/cross_shard.rs @@ -48,6 +48,7 @@ pub fn append_cross_shard_balance_tasks( ) -> crate::Result<()> { let schema_version = state.schema_version.current(); let catalog = state.credentials.catalog(); + let crs = crate::control::crs_context::crs_context_for(state); // Collected first so the immutable walk of `tasks` does not borrow-conflict // with the `&mut Vec` pushed into below — the same two-phase shape @@ -82,7 +83,7 @@ pub fn append_cross_shard_balance_tasks( if sum_target_is_co_resident(source, &binding.target_collection) { continue; } - for (join_value, delta) in crate::query::binding_insert_deltas(binding, &docs)? { + for (join_value, delta) in crate::query::binding_insert_deltas(binding, &docs, &crs)? { // A zero net delta leaves the stored total unchanged, so the // read-modify-write on the target would rewrite the row // byte-for-byte. Shipping a task for it would also make an diff --git a/nodedb/src/control/planner/materialized_sum/predicate.rs b/nodedb/src/control/planner/materialized_sum/predicate.rs index 033b34ec3..52cfd42e6 100644 --- a/nodedb/src/control/planner/materialized_sum/predicate.rs +++ b/nodedb/src/control/planner/materialized_sum/predicate.rs @@ -106,7 +106,8 @@ pub(super) async fn resolve_predicate_sum_targets( // Folded from the SAME scan the resolution came from: a second scan will // see a different snapshot, and two snapshots is two totals. - let images = predicate_images(&scope, &read.rows)?; + let crs = crate::control::crs_context::crs_context_for(state); + let images = predicate_images(&scope, &read.rows, &crs)?; let input = SettleInput { source_collection: &scope.collection, images: &images, @@ -115,6 +116,7 @@ pub(super) async fn resolve_predicate_sum_targets( // that JOINS the match set after the scan has to invalidate it too. source_row: None, read_version: read.read_version, + crs: &crs, }; let settlement = settle_cross_shard_images(&bindings, &input, &resolved, txn_id, tenant_id, database_id)?; @@ -131,6 +133,7 @@ pub(super) async fn resolve_predicate_sum_targets( fn predicate_images( scope: &PredicateScope, rows: &[serde_json::Value], + crs: &nodedb_crs::CrsContext<'_>, ) -> crate::Result, Option)>> { let mut images = Vec::with_capacity(rows.len()); for row in rows { @@ -138,7 +141,11 @@ fn predicate_images( PredicateEffect::Remove => (Some(row.clone()), None), PredicateEffect::Assign => ( Some(row.clone()), - Some(crate::query::apply_update_assignments(row, &scope.updates)?), + Some(crate::query::apply_update_assignments( + row, + &scope.updates, + crs, + )?), ), }); } @@ -163,7 +170,13 @@ async fn resolve_scanned_rows( ) -> crate::Result> { let mut resolved = ResolvedTargets::new(); for binding in bindings.iter() { - for join_value in crate::query::binding_join_keys(binding, updates, rows)? { + let join_values = crate::query::binding_join_keys( + binding, + updates, + rows, + &crate::control::crs_context::crs_context_for(state), + )?; + for join_value in join_values { resolved .resolve(&binding.target_collection, join_value, async |v| { lookup_join_value(state, binding, v, tenant_id, database_id, trace_id) diff --git a/nodedb/src/control/planner/materialized_sum/resolve.rs b/nodedb/src/control/planner/materialized_sum/resolve.rs index 956e9a66d..d1c1c0972 100644 --- a/nodedb/src/control/planner/materialized_sum/resolve.rs +++ b/nodedb/src/control/planner/materialized_sum/resolve.rs @@ -144,6 +144,7 @@ pub async fn resolve_materialized_sum_targets( ); } let mut resolved = resolved.into_vec(); + let crs = crate::control::crs_context::crs_context_for(state); // Every row settles against the whole resolution, before any // shipped value leaves it. let mut settlements = Vec::with_capacity(stored.len()); @@ -155,6 +156,7 @@ pub async fn resolve_materialized_sum_targets( images: &images.images, source_row: Some(scope.surrogate), read_version: images.read_version, + crs: &crs, }; let mut settlement = settle_cross_shard_images( &bindings, diff --git a/nodedb/src/control/planner/materialized_sum/settle.rs b/nodedb/src/control/planner/materialized_sum/settle.rs index 6ecb7d014..d364b177b 100644 --- a/nodedb/src/control/planner/materialized_sum/settle.rs +++ b/nodedb/src/control/planner/materialized_sum/settle.rs @@ -93,6 +93,8 @@ pub(super) struct SettleInput<'a> { pub source_row: Option, /// Version the images were read at, on the source collection's vShard. pub read_version: nodedb_types::WriteVersion, + /// Resolves SRIDs in the binding's value expression. + pub crs: &'a nodedb_crs::CrsContext<'a>, } /// What settling produced. @@ -158,6 +160,7 @@ pub(super) fn settle_cross_shard_images( binding, old.as_ref(), new.as_ref(), + input.crs, )?); } for (join_value, delta) in crate::query::coalesce_binding_deltas(folded) { @@ -327,7 +330,9 @@ pub(super) fn co_resident_target_keys( continue; } for (old, new) in input.images { - for entry in crate::query::binding_image_deltas(binding, old.as_ref(), new.as_ref())? { + for entry in + crate::query::binding_image_deltas(binding, old.as_ref(), new.as_ref(), input.crs)? + { let key = SumTargetKey::new(&binding.target_collection, entry.join_value); if !keep.contains(&key) { keep.push(key); @@ -403,6 +408,7 @@ mod tests { images: &images, source_row: Some(Surrogate::new(11)), read_version: nodedb_types::WriteVersion::logged(3, 42), + crs: &nodedb_crs::CrsContext::builtin_only(), }; let settlement = settle_cross_shard_images( &[binding(&target)], @@ -438,6 +444,7 @@ mod tests { images: &images, source_row: Some(Surrogate::new(11)), read_version: nodedb_types::WriteVersion::logged(3, 42), + crs: &nodedb_crs::CrsContext::builtin_only(), }; let settlement = settle_cross_shard_images( &[binding(&target)], @@ -478,6 +485,7 @@ mod tests { images: &images, source_row: Some(Surrogate::new(11)), read_version: nodedb_types::WriteVersion::logged(3, 42), + crs: &nodedb_crs::CrsContext::builtin_only(), }; let settlement = settle_cross_shard_images( &[binding(&target)], @@ -502,6 +510,7 @@ mod tests { images: &images, source_row: Some(Surrogate::new(11)), read_version: nodedb_types::WriteVersion::logged(3, 42), + crs: &nodedb_crs::CrsContext::builtin_only(), }; let settlement = settle_cross_shard_images( &[binding(&target)], @@ -530,6 +539,7 @@ mod tests { images: &images, source_row: Some(Surrogate::new(11)), read_version: nodedb_types::WriteVersion::logged(3, 42), + crs: &nodedb_crs::CrsContext::builtin_only(), }; // A binding whose target IS the source collection is co-resident by // construction, whatever the hash function does. @@ -559,6 +569,7 @@ mod tests { images: &images, source_row: Some(Surrogate::new(11)), read_version: nodedb_types::WriteVersion::logged(3, 42), + crs: &nodedb_crs::CrsContext::builtin_only(), }; let settlement = settle_cross_shard_images( &[binding(&target)], @@ -596,6 +607,7 @@ mod tests { images: &images, source_row: None, read_version: nodedb_types::WriteVersion::logged(3, 42), + crs: &nodedb_crs::CrsContext::builtin_only(), }; let settlement = settle_cross_shard_images( &[binding(&target)], diff --git a/nodedb/src/control/planner/materialized_sum/stored.rs b/nodedb/src/control/planner/materialized_sum/stored.rs index 1d490dbe4..69ca97796 100644 --- a/nodedb/src/control/planner/materialized_sum/stored.rs +++ b/nodedb/src/control/planner/materialized_sum/stored.rs @@ -251,7 +251,11 @@ pub(super) async fn extend_with_stored_row( .await?; let outcome = StoredImages { - images: images_of(scope, read.rows.as_ref())?, + images: images_of( + scope, + read.rows.as_ref(), + &crate::control::crs_context::crs_context_for(state), + )?, read_version: read.read_version, }; @@ -280,7 +284,13 @@ pub(super) async fn extend_with_stored_row( // column and name different targets each keep their own entry. // `ResolvedTargets` enforces the dedupe. for binding in bindings.iter() { - for join_value in crate::query::binding_join_keys(binding, &[], &rows)? { + let join_values = crate::query::binding_join_keys( + binding, + &[], + &rows, + &crate::control::crs_context::crs_context_for(state), + )?; + for join_value in join_values { resolved .resolve(&binding.target_collection, join_value, async |v| { lookup_join_value(state, binding, v, tenant_id, database_id, trace_id) @@ -300,9 +310,10 @@ pub(super) async fn extend_with_stored_row( fn images_of( scope: &StoredRowScope<'_>, stored: Option<&serde_json::Value>, + crs: &nodedb_crs::CrsContext<'_>, ) -> crate::Result, Option)>> { let assigned = - |old: &serde_json::Value| crate::query::apply_update_assignments(old, scope.updates); + |old: &serde_json::Value| crate::query::apply_update_assignments(old, scope.updates, crs); Ok(match (&scope.post_image, stored) { (PostImage::Removed, Some(old)) => vec![(Some(old.clone()), None)], (PostImage::Assigned, Some(old)) => vec![(Some(old.clone()), Some(assigned(old)?))], @@ -326,6 +337,7 @@ fn images_of( old, &submitted, scope.updates, + crs, )?), )] } @@ -530,7 +542,12 @@ mod tests { ); let scope = stored_row_scope(&op, None).expect("an upsert names its stored row"); let stored = serde_json::json!({"account_id": "acc-1", "amount": 25}); - let images = images_of(&scope, Some(&stored)).expect("images"); + let images = images_of( + &scope, + Some(&stored), + &nodedb_crs::CrsContext::builtin_only(), + ) + .expect("images"); assert_eq!(images.len(), 1); assert_eq!(images[0].0.as_ref(), Some(&stored)); @@ -545,7 +562,12 @@ mod tests { let op = upsert_with(body("acc-1", 60), Vec::new()); let scope = stored_row_scope(&op, None).expect("an upsert names its stored row"); let stored = serde_json::json!({"account_id": "acc-1", "amount": 25, "memo": "kept"}); - let images = images_of(&scope, Some(&stored)).expect("images"); + let images = images_of( + &scope, + Some(&stored), + &nodedb_crs::CrsContext::builtin_only(), + ) + .expect("images"); assert_eq!(amount_of(images[0].1.as_ref()), Some(60)); assert_eq!( @@ -573,7 +595,12 @@ mod tests { ); let scope = stored_row_scope(&op, None).expect("an upsert names its stored row"); let stored = serde_json::json!({"account_id": "acc-1", "amount": 25}); - let images = images_of(&scope, Some(&stored)).expect("images"); + let images = images_of( + &scope, + Some(&stored), + &nodedb_crs::CrsContext::builtin_only(), + ) + .expect("images"); assert_eq!( images[0] @@ -595,7 +622,8 @@ mod tests { vec![("amount".to_string(), excluded("amount"))], ); let scope = stored_row_scope(&op, None).expect("an upsert names its stored row"); - let images = images_of(&scope, None).expect("images"); + let images = + images_of(&scope, None, &nodedb_crs::CrsContext::builtin_only()).expect("images"); assert_eq!(images.len(), 1); assert!(images[0].0.is_none(), "an insert has no pre-image"); @@ -619,7 +647,12 @@ mod tests { }; let scope = stored_row_scope(&op, None).expect("a put names its stored row"); let stored = serde_json::json!({"account_id": "acc-1", "amount": 25}); - let images = images_of(&scope, Some(&stored)).expect("images"); + let images = images_of( + &scope, + Some(&stored), + &nodedb_crs::CrsContext::builtin_only(), + ) + .expect("images"); assert_eq!(images[0].0.as_ref(), Some(&stored)); assert_eq!(amount_of(images[0].1.as_ref()), Some(60)); diff --git a/nodedb/src/control/planner/mod.rs b/nodedb/src/control/planner/mod.rs index a37045775..f2bc3d35f 100644 --- a/nodedb/src/control/planner/mod.rs +++ b/nodedb/src/control/planner/mod.rs @@ -14,4 +14,5 @@ pub mod procedural; pub mod redaction_refusal; pub mod rls_injection; pub mod sql_plan_convert; +pub mod system_cut; pub mod wasm; diff --git a/nodedb/src/control/planner/period_lock/point_update.rs b/nodedb/src/control/planner/period_lock/point_update.rs index 7cbb8245c..9144dd7ca 100644 --- a/nodedb/src/control/planner/period_lock/point_update.rs +++ b/nodedb/src/control/planner/period_lock/point_update.rs @@ -38,7 +38,11 @@ pub(super) async fn resolve_update_period_values( let Some(old_row) = read.rows else { return Ok(Vec::new()); }; - let new_row = crate::query::apply_update_assignments(&old_row, updates)?; + let new_row = crate::query::apply_update_assignments( + &old_row, + updates, + &crate::control::crs_context::crs_context_for(scope.state), + )?; // `ResolvedTargets` dedupes on `(target, value)` itself, so the old and // new images resolve through the same call whether or not they carry the diff --git a/nodedb/src/control/planner/period_lock/predicate.rs b/nodedb/src/control/planner/period_lock/predicate.rs index 2855b9bdf..1e9a64fcd 100644 --- a/nodedb/src/control/planner/period_lock/predicate.rs +++ b/nodedb/src/control/planner/period_lock/predicate.rs @@ -53,7 +53,11 @@ pub(super) async fn resolve_predicate_period_values( for row in &read.rows { resolve_period_value(scope, def, row, &mut resolved).await?; if matches!(effect, PeriodLockEffect::Assign) { - let new_row = crate::query::apply_update_assignments(row, updates)?; + let new_row = crate::query::apply_update_assignments( + row, + updates, + &crate::control::crs_context::crs_context_for(scope.state), + )?; resolve_period_value(scope, def, &new_row, &mut resolved).await?; } } diff --git a/nodedb/src/control/planner/plan_children/document.rs b/nodedb/src/control/planner/plan_children/document.rs index d97f92dbf..d465d84e0 100644 --- a/nodedb/src/control/planner/plan_children/document.rs +++ b/nodedb/src/control/planner/plan_children/document.rs @@ -35,3 +35,35 @@ pub(super) fn child(op: &DocumentOp) -> Option<&PhysicalPlan> { | DocumentOp::ResolvedWrite { .. } => None, } } + +/// The child plan `op` reads its rows from, if any, mutable. +pub(super) fn child_mut(op: &mut DocumentOp) -> Option<&mut PhysicalPlan> { + match op { + DocumentOp::InsertSelect { source, .. } + | DocumentOp::Merge { source, .. } + | DocumentOp::UpdateFromJoin { source, .. } => source.query_plan_mut(), + DocumentOp::ResolveWrite(inner) => child_mut(inner), + DocumentOp::PointGet { .. } + | DocumentOp::PointPut { .. } + | DocumentOp::PointInsert { .. } + | DocumentOp::PointDelete { .. } + | DocumentOp::PointUpdate { .. } + | DocumentOp::Scan { .. } + | DocumentOp::BatchInsert { .. } + | DocumentOp::RangeScan { .. } + | DocumentOp::Register { .. } + | DocumentOp::IndexLookup { .. } + | DocumentOp::IndexedFetch { .. } + | DocumentOp::DropIndex { .. } + | DocumentOp::BackfillIndex { .. } + | DocumentOp::Truncate { .. } + | DocumentOp::EstimateCount { .. } + | DocumentOp::Upsert { .. } + | DocumentOp::BatchUpsert { .. } + | DocumentOp::BulkUpdate { .. } + | DocumentOp::BulkDelete { .. } + | DocumentOp::MaterializeScan { .. } + | DocumentOp::ApplyBalanceDelta { .. } + | DocumentOp::ResolvedWrite { .. } => None, + } +} diff --git a/nodedb/src/control/planner/plan_children/mod.rs b/nodedb/src/control/planner/plan_children/mod.rs index 962edf6d9..bb9701069 100644 --- a/nodedb/src/control/planner/plan_children/mod.rs +++ b/nodedb/src/control/planner/plan_children/mod.rs @@ -6,5 +6,7 @@ mod document; mod query; mod vector; mod walk; +mod walk_mut; pub use walk::children; +pub use walk_mut::children_mut; diff --git a/nodedb/src/control/planner/plan_children/query.rs b/nodedb/src/control/planner/plan_children/query.rs index bbce07040..67e738a75 100644 --- a/nodedb/src/control/planner/plan_children/query.rs +++ b/nodedb/src/control/planner/plan_children/query.rs @@ -50,3 +50,50 @@ pub(super) fn children(op: &QueryOp) -> Vec<&PhysicalPlan> { | QueryOp::RecursiveValue { .. } => Vec::new(), } } + +/// The child plans `op` reads its rows from, mutable. +pub(super) fn children_mut(op: &mut QueryOp) -> Vec<&mut PhysicalPlan> { + match op { + QueryOp::Exchange(exchange) => vec![&mut *exchange.child], + QueryOp::PostProcess { input, .. } => vec![&mut **input], + QueryOp::SetOp { inputs, .. } => inputs.iter_mut().collect(), + QueryOp::Aggregate { input, .. } | QueryOp::PartialAggregateState { input, .. } => { + input.iter_mut().map(|child| &mut **child).collect() + } + QueryOp::HashJoin { + left_input, + right_input, + left_bitmap, + right_bitmap, + .. + } => [left_input, right_input, left_bitmap, right_bitmap] + .into_iter() + .flatten() + .map(|child| &mut **child) + .collect(), + QueryOp::NestedLoopJoin { + left_input, + right_input, + .. + } + | QueryOp::SortMergeJoin { + left_input, + right_input, + .. + } => [left_input, right_input] + .into_iter() + .flatten() + .map(|child| &mut **child) + .collect(), + QueryOp::LateralTopK { outer_plan, .. } | QueryOp::LateralLoop { outer_plan, .. } => { + vec![&mut **outer_plan] + } + QueryOp::ProviderScan { .. } + | QueryOp::PartialAggregate { .. } + | QueryOp::ShuffleJoinConsume { .. } + | QueryOp::ShuffleAggregateConsume { .. } + | QueryOp::FacetCounts { .. } + | QueryOp::RecursiveScan { .. } + | QueryOp::RecursiveValue { .. } => Vec::new(), + } +} diff --git a/nodedb/src/control/planner/plan_children/vector.rs b/nodedb/src/control/planner/plan_children/vector.rs index f2f8a6bf3..84fbfa9cd 100644 --- a/nodedb/src/control/planner/plan_children/vector.rs +++ b/nodedb/src/control/planner/plan_children/vector.rs @@ -40,3 +40,41 @@ pub(super) fn vector_child(op: &VectorOp) -> Option<&PhysicalPlan> { | VectorOp::ResolvedDirectWrite { .. } => None, } } + +/// The resolved sub-plan a vector search takes its prefilter from, if any, +/// mutable. +pub(super) fn vector_child_mut(op: &mut VectorOp) -> Option<&mut PhysicalPlan> { + match op { + VectorOp::Search { + inline_prefilter_plan: Some(child), + .. + } => Some(child), + VectorOp::ResolveDirectWrite(inner) => vector_child_mut(inner), + VectorOp::Search { .. } + | VectorOp::Insert { .. } + | VectorOp::BatchInsert { .. } + | VectorOp::MultiSearch { .. } + | VectorOp::Delete { .. } + | VectorOp::DeleteBySurrogate { .. } + | VectorOp::SetParams { .. } + | VectorOp::DropIndex { .. } + | VectorOp::QueryStats { .. } + | VectorOp::Seal { .. } + | VectorOp::CompactIndex { .. } + | VectorOp::Rebuild { .. } + | VectorOp::SparseInsert { .. } + | VectorOp::SparseSearch { .. } + | VectorOp::SparseDelete { .. } + | VectorOp::MultiVectorInsert { .. } + | VectorOp::MultiVectorDelete { .. } + | VectorOp::MultiVectorScoreSearch { .. } + | VectorOp::DirectUpsert { .. } + | VectorOp::DirectInsert { .. } + | VectorOp::DirectInsertIfAbsent { .. } + | VectorOp::DirectBatchWrite { .. } + | VectorOp::DirectDelete { .. } + | VectorOp::DirectTruncate { .. } + | VectorOp::DirectUpdate { .. } + | VectorOp::ResolvedDirectWrite { .. } => None, + } +} diff --git a/nodedb/src/control/planner/plan_children/walk_mut.rs b/nodedb/src/control/planner/plan_children/walk_mut.rs new file mode 100644 index 000000000..4e1c2036c --- /dev/null +++ b/nodedb/src/control/planner/plan_children/walk_mut.rs @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! The plans a plan reads its rows from, one level down, for an in-place +//! rewrite. + +use nodedb_physical::physical_plan::PhysicalPlan; + +use super::{document, query, vector}; + +/// The plans whose rows `plan` reads, mutable. Exhaustive over +/// [`PhysicalPlan`], and the same plans [`super::children`] names. +pub fn children_mut(plan: &mut PhysicalPlan) -> Vec<&mut PhysicalPlan> { + match plan { + PhysicalPlan::Query(op) => query::children_mut(op), + PhysicalPlan::Document(op) => document::child_mut(op).into_iter().collect(), + PhysicalPlan::Vector(op) => vector::vector_child_mut(op).into_iter().collect(), + PhysicalPlan::Graph(_) + | PhysicalPlan::Kv(_) + | PhysicalPlan::Text(_) + | PhysicalPlan::Columnar(_) + | PhysicalPlan::Timeseries(_) + | PhysicalPlan::Spatial(_) + | PhysicalPlan::Crdt(_) + | PhysicalPlan::Meta(_) + | PhysicalPlan::Array(_) + | PhysicalPlan::ClusterArray(_) + | PhysicalPlan::ClusterEvent(_) => Vec::new(), + } +} diff --git a/nodedb/src/control/planner/redaction_refusal/graph.rs b/nodedb/src/control/planner/redaction_refusal/graph.rs index b41005164..43f4b6305 100644 --- a/nodedb/src/control/planner/redaction_refusal/graph.rs +++ b/nodedb/src/control/planner/redaction_refusal/graph.rs @@ -44,7 +44,6 @@ pub(super) fn refuse_graph_op(op: &GraphOp, ctx: &RefusalCtx<'_>) -> crate::Resu | GraphOp::ResolveEdgeDelete(_) | GraphOp::RagFusion { .. } | GraphOp::Algo { .. } - | GraphOp::TemporalAlgorithm { .. } | GraphOp::BspSuperstep(_) | GraphOp::WccSuperstep(_) | GraphOp::SetNodeLabels { .. } diff --git a/nodedb/src/control/planner/redaction_refusal/plan.rs b/nodedb/src/control/planner/redaction_refusal/plan.rs index a6372fbb2..4413ed4b0 100644 --- a/nodedb/src/control/planner/redaction_refusal/plan.rs +++ b/nodedb/src/control/planner/redaction_refusal/plan.rs @@ -581,6 +581,7 @@ mod tests { edge_labels: Vec::new(), direction: nodedb_types::graph::Direction::Out, rls_filters: Vec::new(), + scope: nodedb_physical::physical_plan::GraphReadScope::default(), }); assert_refused(check(&plan, &store, "support")); assert!(check(&plan, &store, "analyst").is_ok()); @@ -606,6 +607,7 @@ mod tests { query: zerompk::to_msgpack_vec(&query).expect("encode match query"), frontier_bitmap: None, cluster_mode: false, + scope: nodedb_physical::physical_plan::GraphReadScope::default(), }); assert_refused(check(&refused, &store, "support")); @@ -614,6 +616,7 @@ mod tests { query: zerompk::to_msgpack_vec(&query).expect("encode match query"), frontier_bitmap: None, cluster_mode: false, + scope: nodedb_physical::physical_plan::GraphReadScope::default(), }); assert!(check(&allowed, &store, "support").is_ok()); } @@ -639,6 +642,7 @@ mod tests { query: zerompk::to_msgpack_vec(&query).expect("encode match query"), frontier_bitmap: None, cluster_mode: false, + scope: nodedb_physical::physical_plan::GraphReadScope::default(), }); assert_refused(check(&plan, &store, "support")); assert!(check(&plan, &store, "analyst").is_ok()); diff --git a/nodedb/src/control/planner/rls_injection/columnar.rs b/nodedb/src/control/planner/rls_injection/columnar.rs index 64fce4f61..7017d982a 100644 --- a/nodedb/src/control/planner/rls_injection/columnar.rs +++ b/nodedb/src/control/planner/rls_injection/columnar.rs @@ -186,6 +186,7 @@ mod tests { schema_bytes: Vec::new(), provenance: None, wal_lsn: None, + system_from: None, rls_write_check: nodedb_types::RlsWriteCheck::pending_injection(), returning: None, rls_filters: Vec::new(), @@ -200,6 +201,7 @@ mod tests { ), filters: Vec::new(), updates: Vec::new(), + system_from: None, rls_write_check: nodedb_types::RlsWriteCheck::pending_injection(), }) } @@ -211,6 +213,8 @@ mod tests { collection, ), restart_identity: false, + system_from: None, + erase_history: false, }) } @@ -349,6 +353,7 @@ mod tests { schema_bytes: Vec::new(), provenance: None, wal_lsn: None, + system_from: None, rls_write_check: nodedb_types::RlsWriteCheck::pending_injection(), returning: None, rls_filters: Vec::new(), @@ -378,6 +383,7 @@ mod tests { schema_bytes: Vec::new(), provenance: None, wal_lsn: None, + system_from: None, rls_write_check: nodedb_types::RlsWriteCheck::pending_injection(), returning: None, rls_filters: Vec::new(), @@ -405,6 +411,7 @@ mod tests { "events", ), filters: Vec::new(), + system_from: None, rls_write_check: nodedb_types::RlsWriteCheck::pending_injection(), }); assert!(inject(&mut delete, &store).is_ok()); diff --git a/nodedb/src/control/planner/rls_injection/context.rs b/nodedb/src/control/planner/rls_injection/context.rs index 55f3cdace..67362f224 100644 --- a/nodedb/src/control/planner/rls_injection/context.rs +++ b/nodedb/src/control/planner/rls_injection/context.rs @@ -29,6 +29,11 @@ pub(super) struct RlsCtx<'a> { } impl RlsCtx<'_> { + /// The CRS context write predicates resolve SRIDs against. + fn crs(&self) -> nodedb_crs::CrsContext<'_> { + crate::control::crs_context::catalog_crs_context(self.catalog) + } + /// The concrete read filters for `collection`; empty when no policy /// restricts this identity. pub(super) fn read_filters( @@ -112,6 +117,7 @@ impl RlsCtx<'_> { image, self.tenant_id, collection.as_str(), + &self.crs(), ) } @@ -147,6 +153,7 @@ impl RlsCtx<'_> { &with_id, self.tenant_id, collection.as_str(), + &self.crs(), ) } @@ -196,6 +203,7 @@ impl RlsCtx<'_> { image, self.tenant_id, collection.as_str(), + &self.crs(), ) } @@ -238,6 +246,7 @@ impl RlsCtx<'_> { &bytes, self.tenant_id, collection.as_str(), + &self.crs(), ) } @@ -278,6 +287,7 @@ impl RlsCtx<'_> { &image, self.tenant_id, collection.as_str(), + &self.crs(), )?; } // Decided here, with a live identity, against the exact images carried. diff --git a/nodedb/src/control/planner/rls_injection/document.rs b/nodedb/src/control/planner/rls_injection/document.rs index 9e2d2e8c0..359565847 100644 --- a/nodedb/src/control/planner/rls_injection/document.rs +++ b/nodedb/src/control/planner/rls_injection/document.rs @@ -253,7 +253,7 @@ mod tests { collection, ), filters: Vec::new(), - system_as_of_ms: None, + system_time: nodedb_types::SystemTimeScope::Current, offset: 0, limit: None, column_map: Vec::new(), @@ -651,7 +651,7 @@ mod tests { projection: Vec::new(), limit: 0, offset: 0, - system_as_of_ms: None, + system_time: nodedb_types::SystemTimeScope::Current, }) } diff --git a/nodedb/src/control/planner/rls_injection/graph.rs b/nodedb/src/control/planner/rls_injection/graph.rs index 210d12d12..3bcc7c7d0 100644 --- a/nodedb/src/control/planner/rls_injection/graph.rs +++ b/nodedb/src/control/planner/rls_injection/graph.rs @@ -51,11 +51,10 @@ pub(super) fn inject_graph(ctx: &RlsCtx<'_>, op: &mut GraphOp) -> crate::Result< // Refuse: runs over the whole CSR, returns scalars derived from // every row (including hidden ones), no row to filter. `params.collection` // is bare (from `nodedb-graph`) — qualify before the lookup. - GraphOp::Algo { params, .. } | GraphOp::TemporalAlgorithm { params, .. } => ctx - .refuse_if_policy( - &nodedb_types::QualifiedCollection::new(ctx.database_id, ¶ms.collection), - ALGORITHM_REASON, - ), + GraphOp::Algo { params, .. } => ctx.refuse_if_policy( + &nodedb_types::QualifiedCollection::new(ctx.database_id, ¶ms.collection), + ALGORITHM_REASON, + ), // Refuse: same algorithm, one round at a time. GraphOp::BspSuperstep(plan) => ctx.refuse_if_policy( @@ -182,6 +181,7 @@ mod tests { personalization_vector: None, }, stage: nodedb_physical::physical_plan::AlgoStage::Local, + scope: nodedb_physical::physical_plan::GraphReadScope::default(), }) } @@ -199,6 +199,7 @@ mod tests { query: zerompk::to_msgpack_vec(&query).expect("encode match query"), frontier_bitmap: None, cluster_mode: false, + scope: nodedb_physical::physical_plan::GraphReadScope::default(), }) } @@ -262,6 +263,8 @@ mod tests { properties, src_surrogate: nodedb_types::Surrogate::new(1), dst_surrogate: nodedb_types::Surrogate::new(2), + valid_from_ms: None, + valid_until_ms: None, }) } diff --git a/nodedb/src/control/planner/rls_injection/meta.rs b/nodedb/src/control/planner/rls_injection/meta.rs index 8155bdac1..f8457ef37 100644 --- a/nodedb/src/control/planner/rls_injection/meta.rs +++ b/nodedb/src/control/planner/rls_injection/meta.rs @@ -90,6 +90,7 @@ pub(super) fn inject_meta(ctx: &RlsCtx<'_>, op: &mut MetaOp) -> crate::Result<() | MetaOp::AwaitContinuousAggregateRefresh { .. } | MetaOp::ConvertCollection { .. } | MetaOp::RestoreTenantSnapshot { .. } + | MetaOp::PurgeDepartedVShards { .. } | MetaOp::PurgeTenant { .. } | MetaOp::UnregisterCollection { .. } | MetaOp::UnregisterMaterializedView { .. } @@ -105,6 +106,7 @@ pub(super) fn inject_meta(ctx: &RlsCtx<'_>, op: &mut MetaOp) -> crate::Result<() | MetaOp::RebuildIndex { .. } | MetaOp::PutSynonymGroup { .. } | MetaOp::DeleteSynonymGroup { .. } + | MetaOp::SetCustomCrsSet { .. } | MetaOp::DropTxnOverlay { .. } | MetaOp::MarkSavepoint { .. } | MetaOp::RollbackToSavepoint { .. } diff --git a/nodedb/src/control/planner/rls_injection/permission_tree/graph.rs b/nodedb/src/control/planner/rls_injection/permission_tree/graph.rs index 42153272b..f6d8a49e6 100644 --- a/nodedb/src/control/planner/rls_injection/permission_tree/graph.rs +++ b/nodedb/src/control/planner/rls_injection/permission_tree/graph.rs @@ -52,11 +52,10 @@ pub(super) fn apply_graph(ctx: &PermCtx<'_>, op: &GraphOp) -> crate::Result<()> // every row (including ones outside the subtree), no column to // filter. `AlgoParams` comes from `nodedb-graph` and carries a bare // name — qualify it against this task's database before the lookup. - GraphOp::Algo { params, .. } | GraphOp::TemporalAlgorithm { params, .. } => ctx - .refuse_if_tree( - &nodedb_types::QualifiedCollection::new(ctx.database_id, ¶ms.collection), - ALGORITHM_REASON, - ), + GraphOp::Algo { params, .. } => ctx.refuse_if_tree( + &nodedb_types::QualifiedCollection::new(ctx.database_id, ¶ms.collection), + ALGORITHM_REASON, + ), // Refuse: the distributed supersteps are the same algorithms one round // at a time, carrying the target collection in their params. @@ -159,6 +158,7 @@ mod tests { edge_labels: Vec::new(), direction: nodedb_types::graph::Direction::Out, rls_filters: Vec::new(), + scope: nodedb_physical::physical_plan::GraphReadScope::default(), }) } @@ -210,6 +210,7 @@ mod tests { personalization_vector: None, }, stage: nodedb_physical::physical_plan::AlgoStage::Local, + scope: nodedb_physical::physical_plan::GraphReadScope::default(), }); assert_refused(apply(&mut plan, &cache), "docs"); } diff --git a/nodedb/src/control/planner/rls_injection/permission_tree/meta.rs b/nodedb/src/control/planner/rls_injection/permission_tree/meta.rs index 45317e9f0..d04e61cca 100644 --- a/nodedb/src/control/planner/rls_injection/permission_tree/meta.rs +++ b/nodedb/src/control/planner/rls_injection/permission_tree/meta.rs @@ -89,6 +89,7 @@ pub(super) fn apply_meta(ctx: &PermCtx<'_>, op: &mut MetaOp) -> crate::Result<() | MetaOp::AwaitContinuousAggregateRefresh { .. } | MetaOp::ConvertCollection { .. } | MetaOp::RestoreTenantSnapshot { .. } + | MetaOp::PurgeDepartedVShards { .. } | MetaOp::PurgeTenant { .. } | MetaOp::UnregisterCollection { .. } | MetaOp::UnregisterMaterializedView { .. } @@ -104,6 +105,7 @@ pub(super) fn apply_meta(ctx: &PermCtx<'_>, op: &mut MetaOp) -> crate::Result<() | MetaOp::RebuildIndex { .. } | MetaOp::PutSynonymGroup { .. } | MetaOp::DeleteSynonymGroup { .. } + | MetaOp::SetCustomCrsSet { .. } | MetaOp::DropTxnOverlay { .. } | MetaOp::MarkSavepoint { .. } | MetaOp::RollbackToSavepoint { .. } diff --git a/nodedb/src/control/planner/rls_injection/plan.rs b/nodedb/src/control/planner/rls_injection/plan.rs index 12395ca1d..10f9460c4 100644 --- a/nodedb/src/control/planner/rls_injection/plan.rs +++ b/nodedb/src/control/planner/rls_injection/plan.rs @@ -341,6 +341,7 @@ mod tests { bm25_query: None, bm25_field: None, stage: nodedb_physical::physical_plan::RagStage::Local, + scope: nodedb_physical::physical_plan::GraphReadScope::default(), }) } diff --git a/nodedb/src/control/planner/sql_plan_convert/array_alter_convert.rs b/nodedb/src/control/planner/sql_plan_convert/array_alter_convert.rs index e8e141f60..84409af01 100644 --- a/nodedb/src/control/planner/sql_plan_convert/array_alter_convert.rs +++ b/nodedb/src/control/planner/sql_plan_convert/array_alter_convert.rs @@ -10,6 +10,7 @@ use nodedb_sql::AlterArrayVisitArgs; use crate::bridge::envelope::PhysicalPlan; use crate::control::array_catalog::ArrayCatalogEntry; +use crate::control::array_catalog::attr_meta::remeta_schema; use crate::control::array_catalog::georef::regeoref_schema; use crate::control::array_catalog::retention::check_retention_floor; use crate::types::TenantId; @@ -25,8 +26,9 @@ use nodedb_physical::physical_task::{PhysicalTask, PostSetOp}; /// - `Some(None)` = key present with value `NULL` → unregister. /// - `Some(Some(v))` = key present with value `v` → update. /// -/// A georef change re-encodes the schema. Its content bytes stay equal, so -/// `schema_hash`, segments, and the Data Plane open check stay valid. +/// A georef or attr metadata change re-encodes the schema. Its content bytes +/// stay equal, so `schema_hash`, segments, and the Data Plane open check stay +/// valid. pub(super) fn convert_alter_array( args: AlterArrayVisitArgs<'_>, tenant_id: TenantId, @@ -37,6 +39,7 @@ pub(super) fn convert_alter_array( audit_retain_ms, minimum_audit_retain_ms, georef, + attr_meta, } = args; ctx.require_execute("ALTER ARRAY")?; let array_catalog = ctx @@ -65,11 +68,19 @@ pub(super) fn convert_alter_array( minimum_audit_retain_ms.map(Some), )?; - // 3. Re-encode the schema for a georef change. + // 3. Re-encode the schema for a georef or attr metadata change. One + // statement makes one change, so at most one applies. let statement = format!("ALTER ARRAY {name}"); - let schema_msgpack = georef - .map(|change| regeoref_schema(&statement, ¤t.schema_msgpack, change)) - .transpose()?; + let schema_msgpack = match (georef, attr_meta) { + (Some(change), _) => Some(regeoref_schema( + &statement, + ¤t.schema_msgpack, + change, + &ctx.crs_context(), + )?), + (None, Some(change)) => Some(remeta_schema(&statement, ¤t.schema_msgpack, change)?), + (None, None) => None, + }; let vshard = ctx.collection_key(name).vshard(); Ok(vec![PhysicalTask { diff --git a/nodedb/src/control/planner/sql_plan_convert/array_convert/attr_type.rs b/nodedb/src/control/planner/sql_plan_convert/array_convert/attr_type.rs index f6438f376..7dbf19c87 100644 --- a/nodedb/src/control/planner/sql_plan_convert/array_convert/attr_type.rs +++ b/nodedb/src/control/planner/sql_plan_convert/array_convert/attr_type.rs @@ -18,6 +18,7 @@ pub(crate) fn engine_attr_type(t: ArrayAttrType) -> AttrType { ArrayAttrType::Int32 => AttrType::Int32, ArrayAttrType::UInt32 => AttrType::UInt32, ArrayAttrType::Float32 => AttrType::Float32, + ArrayAttrType::Int8 => AttrType::Int8, } } @@ -34,6 +35,7 @@ pub(crate) fn sql_attr_type(t: AttrType) -> ArrayAttrType { AttrType::Int32 => ArrayAttrType::Int32, AttrType::UInt32 => ArrayAttrType::UInt32, AttrType::Float32 => ArrayAttrType::Float32, + AttrType::Int8 => ArrayAttrType::Int8, } } @@ -54,6 +56,7 @@ mod tests { ArrayAttrType::Int32, ArrayAttrType::UInt32, ArrayAttrType::Float32, + ArrayAttrType::Int8, ]; for t in all { assert_eq!(sql_attr_type(engine_attr_type(t)), t); diff --git a/nodedb/src/control/planner/sql_plan_convert/array_convert/ddl.rs b/nodedb/src/control/planner/sql_plan_convert/array_convert/ddl.rs index 9acd32f4d..8fe502142 100644 --- a/nodedb/src/control/planner/sql_plan_convert/array_convert/ddl.rs +++ b/nodedb/src/control/planner/sql_plan_convert/array_convert/ddl.rs @@ -19,7 +19,9 @@ use nodedb_sql::types_array::{ use crate::bridge::envelope::PhysicalPlan; use crate::control::array_catalog::ArrayCatalogEntry; -use crate::control::array_catalog::georef::{georef_from_ast, schema_error}; +use crate::control::array_catalog::attr_meta::meta_from_ast; +use crate::control::array_catalog::georef::georef_from_ast; +use crate::control::array_catalog::schema_recode::schema_error; use crate::types::TenantId; use nodedb_physical::physical_plan::ArrayOp; @@ -88,6 +90,7 @@ pub(in super::super) fn convert_create_array( cell_order, tile_order, georef, + crs: &ctx.crs_context(), })?; // 2. Encode + hash. @@ -96,7 +99,10 @@ pub(in super::super) fn convert_create_array( format: "msgpack".into(), detail: format!("array schema encode: {e}"), })?; - let schema_hash = stable_schema_hash(&schema.content_msgpack()); + let content = schema + .content_msgpack() + .map_err(|e| schema_error(&format!("CREATE ARRAY {name}"), e))?; + let schema_hash = stable_schema_hash(&content); // Retention is carried as `u64` from the parser and stored signed, because // downstream it is subtracted from a wall-clock millisecond @@ -212,6 +218,7 @@ pub(super) struct SchemaParts<'a> { pub cell_order: ArrayCellOrderAst, pub tile_order: ArrayTileOrderAst, pub georef: Option<&'a ArrayGeorefAst>, + pub crs: &'a nodedb_crs::CrsContext<'a>, } pub(super) fn build_schema(parts: SchemaParts<'_>) -> crate::Result> { @@ -223,6 +230,7 @@ pub(super) fn build_schema(parts: SchemaParts<'_>) -> crate::Result = dims .iter() @@ -240,14 +248,16 @@ pub(super) fn build_schema(parts: SchemaParts<'_>) -> crate::Result = tile_extents.iter().map(|n| *n as u64).collect(); builder = builder diff --git a/nodedb/src/control/planner/sql_plan_convert/array_fn_convert/aggregate.rs b/nodedb/src/control/planner/sql_plan_convert/array_fn_convert/aggregate.rs index 78a31c260..7a952086e 100644 --- a/nodedb/src/control/planner/sql_plan_convert/array_fn_convert/aggregate.rs +++ b/nodedb/src/control/planner/sql_plan_convert/array_fn_convert/aggregate.rs @@ -28,6 +28,7 @@ pub(crate) fn convert_agg( slice, zone, temporal, + physical, } = args; let entry = load_entry(name, tenant_id, ctx)?; let schema = decode_schema(&entry)?; @@ -51,9 +52,9 @@ pub(crate) fn convert_agg( })? as i32, }; - let (system_as_of, valid_at_ms) = + let (system_time, valid_at_ms) = super::helpers::resolve_array_temporal(*temporal, "ARRAY_AGG")?; - let region = resolve_region(name, slice, zone, &schema)?; + let region = resolve_region(name, slice, zone, &schema, &ctx.crs_context())?; let region_msgpack = match ®ion { Some(region) => { zerompk::to_msgpack_vec(region).map_err(|e| crate::Error::Serialization { @@ -64,6 +65,10 @@ pub(crate) fn convert_agg( None => Vec::new(), }; let mapped = map_reducer(reducer); + // A malformed histogram spec is refused here, before any shard runs it. + mapped.engine().map_err(|e| crate::Error::PlanError { + detail: format!("ARRAY_AGG: {e}"), + })?; let aid = ArrayId::in_database(tenant_id, ctx.database_id, name); let vshard = ctx.collection_key(name).vshard(); @@ -87,9 +92,10 @@ pub(crate) fn convert_agg( group_by_dim: group_by_dim_idx, slice_tile_keys: compute_slice_tile_keys(&schema, box_ranges, entry.prefix_bits), prefix_bits: entry.prefix_bits, - system_as_of, + system_time, valid_at_ms, region_msgpack, + physical, }) } else { PhysicalPlan::Array(ArrayOp::Aggregate { @@ -100,9 +106,10 @@ pub(crate) fn convert_agg( cell_filter: None, return_partial: false, shard_bucket: None, - system_as_of, + system_time, valid_at_ms, region_msgpack, + physical, }) }; @@ -141,6 +148,7 @@ mod tests { slice, zone: None, temporal, + physical: false, } } use nodedb_array::schema::{ArraySchemaBuilder, AttrSpec, AttrType, DimSpec, DimType}; diff --git a/nodedb/src/control/planner/sql_plan_convert/array_fn_convert/elementwise.rs b/nodedb/src/control/planner/sql_plan_convert/array_fn_convert/elementwise.rs index 280c5f3cb..f7fe1e069 100644 --- a/nodedb/src/control/planner/sql_plan_convert/array_fn_convert/elementwise.rs +++ b/nodedb/src/control/planner/sql_plan_convert/array_fn_convert/elementwise.rs @@ -43,7 +43,7 @@ pub(crate) fn convert_elementwise( attr, temporal, } = call; - let (system_as_of, valid_at_ms) = + let (system_time, valid_at_ms) = super::helpers::resolve_array_temporal(temporal, "ARRAY_ELEMENTWISE")?; let lentry = load_entry(left_name, tenant_id, ctx)?; let rentry = load_entry(right_name, tenant_id, ctx)?; @@ -90,7 +90,7 @@ pub(crate) fn convert_elementwise( op, attr_idx, prefix_bits: lentry.prefix_bits, - system_as_of, + system_time, valid_at_ms, }) } else { @@ -101,7 +101,7 @@ pub(crate) fn convert_elementwise( attr_idx, cell_filter: None, shard_bucket: None, - system_as_of, + system_time, valid_at_ms, }) }; @@ -213,7 +213,7 @@ mod tests { assert!(matches!( &tasks[0].plan, PhysicalPlan::ClusterArray(ClusterArrayOp::Elementwise { - system_as_of: Some(1_000), + system_time: nodedb_types::SystemTimeScope::AsOf(1_000), valid_at_ms: Some(2_000), .. }) @@ -227,7 +227,7 @@ mod tests { assert!(matches!( &tasks[0].plan, PhysicalPlan::Array(ArrayOp::Elementwise { - system_as_of: Some(1_000), + system_time: nodedb_types::SystemTimeScope::AsOf(1_000), valid_at_ms: Some(2_000), .. }) diff --git a/nodedb/src/control/planner/sql_plan_convert/array_fn_convert/helpers.rs b/nodedb/src/control/planner/sql_plan_convert/array_fn_convert/helpers.rs index 73ab4d6cc..4da4d7136 100644 --- a/nodedb/src/control/planner/sql_plan_convert/array_fn_convert/helpers.rs +++ b/nodedb/src/control/planner/sql_plan_convert/array_fn_convert/helpers.rs @@ -5,7 +5,7 @@ use nodedb_array::query::slice::{DimRange, Slice}; use nodedb_array::schema::{ArraySchema, DimType as EngineDimType}; use nodedb_array::types::domain::DomainBound; -use nodedb_sql::temporal::{TemporalScope, ValidTime}; +use nodedb_sql::temporal::TemporalScope; use nodedb_sql::types_array::{ ArrayBinaryOpAst, ArrayCoordLiteral, ArrayReducerAst, ArraySliceAst, }; @@ -122,6 +122,14 @@ pub(super) fn map_reducer(r: ArrayReducerAst) -> ArrayReducer { ArrayReducerAst::Min => ArrayReducer::Min, ArrayReducerAst::Max => ArrayReducer::Max, ArrayReducerAst::Mean => ArrayReducer::Mean, + ArrayReducerAst::Median => ArrayReducer::Median, + ArrayReducerAst::Percentile(p) => ArrayReducer::Percentile { p }, + ArrayReducerAst::StdDevPop => ArrayReducer::StdDevPop, + ArrayReducerAst::StdDevSamp => ArrayReducer::StdDevSamp, + ArrayReducerAst::VarPop => ArrayReducer::VarPop, + ArrayReducerAst::VarSamp => ArrayReducer::VarSamp, + ArrayReducerAst::Mode => ArrayReducer::Mode, + ArrayReducerAst::Histogram { bins, lo, hi } => ArrayReducer::Histogram { bins, lo, hi }, } } @@ -134,18 +142,17 @@ pub(super) fn map_binary_op(o: ArrayBinaryOpAst) -> ArrayBinaryOp { } } -/// Resolve a [`TemporalScope`] into the `(system_as_of, valid_at_ms)` pair -/// expected by `ArrayOp::Aggregate` (and `ClusterArrayOp::Agg`). +/// Resolve a [`TemporalScope`] into the `(system_time, valid_at_ms)` pair +/// expected by `ArrayOp::Aggregate` / `ArrayOp::Elementwise` (and their +/// `ClusterArrayOp` fan-outs). /// -/// Default semantics when neither clause was given: both `None` — the Data -/// Plane handler treats this as the live-state fast path (equivalent to -/// `system_as_of = i64::MAX`, no valid-time filter). This avoids allocating -/// any bitemporal bookkeeping for the overwhelmingly common non-temporal case. +/// With neither clause: `SystemTimeScope::Current` and no valid-time +/// filter, the Data Plane's live-state path. /// -/// When `AS OF SYSTEM TIME` is given: `system_as_of = Some(t)`, the DP -/// handler applies the Ceiling resolver to reconstruct the array's state at -/// the named system timestamp. `valid_at_ms` remains `None` unless -/// `AS OF VALID TIME` is also present. +/// `AS OF SYSTEM TIME` passes its scope through. The statement's cut pin +/// (`system_cut::pin_statement_cuts`) later turns `AsOf` into `Pinned`, and +/// every serving node waits for the pinned cut's marker before it reads. +/// `valid_at_ms` stays `None` unless `AS OF VALID TIME` is also present. /// /// When `AS OF VALID TIME` is given: `valid_at_ms = Some(v)`. Only the /// `ValidTime::At(v)` (point-in-time) form is supported by the array engine; @@ -158,10 +165,11 @@ pub(super) fn map_binary_op(o: ArrayBinaryOpAst) -> ArrayBinaryOp { pub(super) fn resolve_array_temporal( temporal: TemporalScope, context: &str, -) -> crate::Result<(Option, Option)> { - let system_as_of = match temporal.system_time { - nodedb_types::SystemTimeScope::Current => None, - nodedb_types::SystemTimeScope::AsOf(ms) => Some(ms), +) -> crate::Result<(nodedb_types::SystemTimeScope, Option)> { + let system_time = match temporal.system_time { + scope @ (nodedb_types::SystemTimeScope::Current + | nodedb_types::SystemTimeScope::AsOf(_) + | nodedb_types::SystemTimeScope::Pinned { .. }) => scope, nodedb_types::SystemTimeScope::AllVersions => { return Err(crate::Error::PlanError { detail: format!( @@ -171,19 +179,9 @@ pub(super) fn resolve_array_temporal( }); } }; - let valid_at_ms = match temporal.valid_time { - ValidTime::Any => None, - ValidTime::At(ms) => Some(ms), - ValidTime::Range(lo, _hi) => { - return Err(crate::Error::PlanError { - detail: format!( - "{context}: AS OF VALID TIME range predicates are not supported on array reads; \ - use AS OF VALID TIME (point-in-time). Got FOR VALID_TIME FROM {lo} ..." - ), - }); - } - }; - Ok((system_as_of, valid_at_ms)) + let valid_at_ms = + crate::control::planner::sql_plan_convert::valid_time_point(temporal.valid_time, "array")?; + Ok((system_time, valid_at_ms)) } /// Resolve a [`TemporalScope`] into the `(system_time, valid_at_ms)` pair @@ -198,20 +196,9 @@ pub(super) fn resolve_array_temporal( /// valid-time point, not an interval, so range predicates are nonsensical. pub(super) fn resolve_array_temporal_scope( temporal: TemporalScope, - context: &str, ) -> crate::Result<(nodedb_types::SystemTimeScope, Option)> { let system_time = temporal.system_time; // all three variants pass through - let valid_at_ms = match temporal.valid_time { - ValidTime::Any => None, - ValidTime::At(ms) => Some(ms), - ValidTime::Range(lo, _hi) => { - return Err(crate::Error::PlanError { - detail: format!( - "{context}: AS OF VALID TIME range predicates are not supported on array reads; \ - use AS OF VALID TIME (point-in-time). Got FOR VALID_TIME FROM {lo} ..." - ), - }); - } - }; + let valid_at_ms = + crate::control::planner::sql_plan_convert::valid_time_point(temporal.valid_time, "array")?; Ok((system_time, valid_at_ms)) } diff --git a/nodedb/src/control/planner/sql_plan_convert/array_fn_convert/region.rs b/nodedb/src/control/planner/sql_plan_convert/array_fn_convert/region.rs index b9d204944..bc266ba14 100644 --- a/nodedb/src/control/planner/sql_plan_convert/array_fn_convert/region.rs +++ b/nodedb/src/control/planner/sql_plan_convert/array_fn_convert/region.rs @@ -19,7 +19,6 @@ use nodedb_array::schema::{ArraySchema, DimType}; use nodedb_array::types::domain::DomainBound; use nodedb_sql::types_array::{ArraySliceAst, ArrayZoneAst}; use nodedb_types::NodeDbError; -use nodedb_types::geometry::Srid; use super::helpers::resolve_slice; @@ -30,13 +29,14 @@ pub(super) fn resolve_region( slice: &ArraySliceAst, zone: Option<&ArrayZoneAst>, schema: &ArraySchema, + crs: &nodedb_crs::CrsContext<'_>, ) -> crate::Result> { if slice.dim_ranges.is_empty() && zone.is_none() { return Ok(None); } let mut engine_slice = resolve_slice("ARRAY_AGG", name, slice, schema)?; let zone = zone - .map(|z| lower_zone("ARRAY_AGG", name, z, schema, &mut engine_slice)) + .map(|z| lower_zone("ARRAY_AGG", name, z, schema, &mut engine_slice, crs)) .transpose()?; Ok(Some(ArrayRegion::new(engine_slice, zone))) } @@ -48,6 +48,7 @@ pub(super) fn lower_zone( zone: &ArrayZoneAst, schema: &ArraySchema, slice: &mut Slice, + crs: &nodedb_crs::CrsContext<'_>, ) -> crate::Result { let x = zone_axis(func, name, &zone.x_dim, schema)?; let y = zone_axis(func, name, &zone.y_dim, schema)?; @@ -55,11 +56,13 @@ pub(super) fn lower_zone( let frame = match zone.srid { None => None, Some(code) => { - let zone_srid = Srid::try_from(code).map_err(|e| crate::Error::DataException { - detail: format!("{func}: zone SRID: {e}"), - })?; + let zone_srid = crs + .resolve_u32(code) + .map_err(|e| crate::Error::DataException { + detail: format!("{func}: zone SRID: {e}"), + })?; check_georef_axes(func, name, schema, x.dim, y.dim)?; - Some(build_zone_frame(schema, zone_srid, &polygons).map_err(frame_error)?) + Some(build_zone_frame(schema, zone_srid, &polygons, crs).map_err(frame_error)?) } }; let extent = match &frame { @@ -176,11 +179,11 @@ fn frame_error(e: ZoneFrameError) -> crate::Error { ZoneFrameError::VertexBudget { .. } => { crate::Error::from(NodeDbError::program_limit_exceeded(e.to_string())) } - ZoneFrameError::OutOfDomain { .. } | ZoneFrameError::EmptyZone { .. } => { - crate::Error::DataException { - detail: e.to_string(), - } - } + ZoneFrameError::OutOfDomain { .. } + | ZoneFrameError::EmptyZone { .. } + | ZoneFrameError::Crs { .. } => crate::Error::DataException { + detail: e.to_string(), + }, } } @@ -229,6 +232,7 @@ mod tests { }; use nodedb_array::types::domain::Domain; use nodedb_sql::types_array::{ArrayCoordLiteral, NamedDimRange}; + use nodedb_types::geometry::Srid; fn schema() -> ArraySchema { ArraySchemaBuilder::new("grid") @@ -270,7 +274,7 @@ mod tests { .build() .expect("schema build") .with_georef(Some(ArrayGeoref { - srid: Srid::Wgs84, + srid: Srid::WGS84, x_dim: 1, y_dim: 0, transform: Some(GeoTransform([100.0, 1.0, 0.0, 8.0, 0.0, -1.0])), @@ -310,7 +314,14 @@ mod tests { #[test] fn the_whole_array_has_no_region() { - let region = resolve_region("grid", &ArraySliceAst::default(), None, &schema()).unwrap(); + let region = resolve_region( + "grid", + &ArraySliceAst::default(), + None, + &schema(), + &nodedb_crs::CrsContext::builtin_only(), + ) + .unwrap(); assert!(region.is_none()); } @@ -321,6 +332,7 @@ mod tests { &ArraySliceAst::default(), Some(&zone("lon", "row")), &schema(), + &nodedb_crs::CrsContext::builtin_only(), ) .unwrap() .expect("region"); @@ -349,9 +361,15 @@ mod tests { hi: ArrayCoordLiteral::Int64(50), }], }; - let region = resolve_region("grid", &slice, Some(&zone("lon", "row")), &schema()) - .unwrap() - .expect("region"); + let region = resolve_region( + "grid", + &slice, + Some(&zone("lon", "row")), + &schema(), + &nodedb_crs::CrsContext::builtin_only(), + ) + .unwrap() + .expect("region"); assert_eq!( range(®ion, 0), Some((DomainBound::Float64(5.0), DomainBound::Float64(7.25))) @@ -365,6 +383,7 @@ mod tests { &ArraySliceAst::default(), Some(&zone("lon", "ts")), &schema(), + &nodedb_crs::CrsContext::builtin_only(), ) .expect_err("refused"); assert!( @@ -380,6 +399,7 @@ mod tests { &ArraySliceAst::default(), Some(&zone("lon", "lat")), &schema(), + &nodedb_crs::CrsContext::builtin_only(), ) .expect_err("refused"); assert!( @@ -395,14 +415,15 @@ mod tests { &ArraySliceAst::default(), Some(&lon_lat_zone(101.0, 5.0, 103.0, 7.0)), &pixel_schema(), + &nodedb_crs::CrsContext::builtin_only(), ) .unwrap() .expect("region"); let zone = region.zone.as_ref().expect("zone"); let frame = zone.frame.as_ref().expect("frame"); assert_eq!( - (frame.array_srid, frame.zone_srid), - (Srid::Wgs84, Srid::Wgs84) + (frame.array_crs.srid, frame.zone_srid()), + (Srid::WGS84, Srid::WGS84) ); // Cells 1..=2 on both axes hold the zone. The box pads by half a // pixel and rounds outward: 0..=3. @@ -427,6 +448,7 @@ mod tests { &lon_lat_zone(130.0, 5.0, 131.0, 6.0), &pixel_schema(), &mut engine, + &nodedb_crs::CrsContext::builtin_only(), ) .expect("lowered"); let col = engine.dim_ranges[1].as_ref().expect("col range"); @@ -442,6 +464,7 @@ mod tests { &ArraySliceAst::default(), Some(&lon_lat_zone(101.0, 5.0, 103.0, 7.0)), &no_georef, + &nodedb_crs::CrsContext::builtin_only(), ) .expect_err("refused"); assert!( @@ -459,6 +482,7 @@ mod tests { &ArraySliceAst::default(), Some(&swapped), &pixel_schema(), + &nodedb_crs::CrsContext::builtin_only(), ) .expect_err("refused"); assert!( @@ -481,7 +505,7 @@ mod tests { array: "img".into(), x: 150.0, y: 6.0, - zone_srid: Srid::Wgs84, + zone_srid: Srid::WGS84, detail: "too far from the central meridian".into(), }); assert!( diff --git a/nodedb/src/control/planner/sql_plan_convert/array_fn_convert/slice.rs b/nodedb/src/control/planner/sql_plan_convert/array_fn_convert/slice.rs index c95f31683..c3c80474a 100644 --- a/nodedb/src/control/planner/sql_plan_convert/array_fn_convert/slice.rs +++ b/nodedb/src/control/planner/sql_plan_convert/array_fn_convert/slice.rs @@ -27,6 +27,7 @@ pub(crate) fn convert_slice( limit, zone, temporal, + physical, } = args; let entry = load_entry(name, tenant_id, ctx)?; let schema = decode_schema(&entry)?; @@ -37,7 +38,14 @@ pub(crate) fn convert_slice( let zone_msgpack = match zone { None => Vec::new(), Some(zone) => { - let lowered = lower_zone("ARRAY_SLICE", name, zone, &schema, &mut slice)?; + let lowered = lower_zone( + "ARRAY_SLICE", + name, + zone, + &schema, + &mut slice, + &ctx.crs_context(), + )?; zerompk::to_msgpack_vec(&lowered).map_err(|e| crate::Error::Serialization { format: "msgpack".into(), detail: format!("array slice zone encode: {e}"), @@ -50,8 +58,7 @@ pub(crate) fn convert_slice( detail: format!("array slice encode: {e}"), })?; - let (system_time, valid_at_ms) = - super::helpers::resolve_array_temporal_scope(*temporal, "ARRAY_SLICE")?; + let (system_time, valid_at_ms) = super::helpers::resolve_array_temporal_scope(*temporal)?; let attr_indices = resolve_attr_indices(name, attr_projection, &schema)?; let aid = ArrayId::in_database(tenant_id, ctx.database_id, name); let vshard = ctx.collection_key(name).vshard(); @@ -75,6 +82,7 @@ pub(crate) fn convert_slice( system_time, valid_at_ms, zone_msgpack, + physical, }) } else { PhysicalPlan::Array(ArrayOp::Slice { @@ -87,6 +95,7 @@ pub(crate) fn convert_slice( system_time, valid_at_ms, zone_msgpack, + physical, }) }; @@ -185,6 +194,7 @@ mod tests { limit: 100, zone: None, temporal: &TemporalScope::default(), + physical: false, }, TenantId::new(1), ctx, diff --git a/nodedb/src/control/planner/sql_plan_convert/convert.rs b/nodedb/src/control/planner/sql_plan_convert/convert.rs index a0c4b3954..50dc5865b 100644 --- a/nodedb/src/control/planner/sql_plan_convert/convert.rs +++ b/nodedb/src/control/planner/sql_plan_convert/convert.rs @@ -139,6 +139,18 @@ impl ConvertContext { self.purpose == PlanningPurpose::Metadata } + /// The CRS context SRIDs resolve against. A context with no catalog + /// reaches no user-defined CRS, so it resolves through the built-in + /// registry only. + pub fn crs_context(&self) -> nodedb_crs::CrsContext<'_> { + match &self.credentials { + Some(credentials) => { + crate::control::crs_context::catalog_crs_context(credentials.catalog()) + } + None => nodedb_crs::CrsContext::builtin_only(), + } + } + /// The canonical key of `bare`, a catalog collection name in this /// context's database. Placement and surrogate identity use this key. pub fn collection_key<'a>(&self, bare: &'a str) -> nodedb_types::CollectionKey<'a> { diff --git a/nodedb/src/control/planner/sql_plan_convert/dml/insert/convert.rs b/nodedb/src/control/planner/sql_plan_convert/dml/insert/convert.rs index ab099d4dc..223574226 100644 --- a/nodedb/src/control/planner/sql_plan_convert/dml/insert/convert.rs +++ b/nodedb/src/control/planner/sql_plan_convert/dml/insert/convert.rs @@ -228,6 +228,7 @@ pub(in super::super::super) fn convert_insert( schema_bytes, provenance: None, wal_lsn: None, + system_from: None, rls_write_check: nodedb_types::RlsWriteCheck::pending_injection(), // Both slots are filled by later passes over the built plan — // `inject_returning_spec` from the statement's RETURNING list, diff --git a/nodedb/src/control/planner/sql_plan_convert/dml/kv_insert.rs b/nodedb/src/control/planner/sql_plan_convert/dml/kv_insert.rs index 7cf010066..c9e74cccf 100644 --- a/nodedb/src/control/planner/sql_plan_convert/dml/kv_insert.rs +++ b/nodedb/src/control/planner/sql_plan_convert/dml/kv_insert.rs @@ -212,7 +212,7 @@ fn kv_entry_value(value_cols: &[(String, SqlValue)]) -> crate::Result> { write_msgpack_map_header(&mut buf, value_cols.len()); for (col, val) in value_cols { write_msgpack_str(&mut buf, col); - write_msgpack_value(&mut buf, val); + write_msgpack_value(&mut buf, val)?; } Ok(buf) } diff --git a/nodedb/src/control/planner/sql_plan_convert/dml/merge.rs b/nodedb/src/control/planner/sql_plan_convert/dml/merge.rs index 31c7965b0..0526e6053 100644 --- a/nodedb/src/control/planner/sql_plan_convert/dml/merge.rs +++ b/nodedb/src/control/planner/sql_plan_convert/dml/merge.rs @@ -140,11 +140,13 @@ fn convert_action(action: &MergePlanAction) -> crate::Result { // are evaluated against the qualified source document at apply time. let encoded: Vec = values .iter() - .map(|expr| match expr { - SqlExpr::Literal(v) => UpdateValue::Literal(sql_value_to_msgpack(v)), - other => UpdateValue::Expr(sql_expr_to_bridge_expr_qualified(other)), + .map(|expr| { + Ok(match expr { + SqlExpr::Literal(v) => UpdateValue::Literal(sql_value_to_msgpack(v)?), + other => UpdateValue::Expr(sql_expr_to_bridge_expr_qualified(other)), + }) }) - .collect(); + .collect::>()?; Ok(MergeActionOp::Insert { columns: columns.clone(), values: encoded, diff --git a/nodedb/src/control/planner/sql_plan_convert/dml/update_delete/delete.rs b/nodedb/src/control/planner/sql_plan_convert/dml/update_delete/delete.rs index c20ec0ba4..b2365d0bc 100644 --- a/nodedb/src/control/planner/sql_plan_convert/dml/update_delete/delete.rs +++ b/nodedb/src/control/planner/sql_plan_convert/dml/update_delete/delete.rs @@ -106,6 +106,7 @@ pub(in crate::control::planner::sql_plan_convert) fn convert_delete( plan: PhysicalPlan::Columnar(ColumnarOp::Delete { collection: qualified_collection.clone(), filters: effective_filter, + system_from: None, rls_write_check: nodedb_types::RlsWriteCheck::pending_injection(), }), post_set_op: PostSetOp::None, diff --git a/nodedb/src/control/planner/sql_plan_convert/dml/update_delete/update.rs b/nodedb/src/control/planner/sql_plan_convert/dml/update_delete/update.rs index ef6da81f9..6849796a4 100644 --- a/nodedb/src/control/planner/sql_plan_convert/dml/update_delete/update.rs +++ b/nodedb/src/control/planner/sql_plan_convert/dml/update_delete/update.rs @@ -89,10 +89,12 @@ pub(in crate::control::planner::sql_plan_convert) fn convert_update( let literal_updates: Vec<(String, Vec)> = assignments .iter() .filter_map(|(field, expr)| match expr { - SqlExpr::Literal(val) => Some((field.clone(), sql_value_to_msgpack(val))), + SqlExpr::Literal(val) => { + Some(sql_value_to_msgpack(val).map(|bytes| (field.clone(), bytes))) + } _ => None, }) - .collect(); + .collect::>()?; return Ok(vec![PhysicalTask { tenant_id, vshard_id: vshard, @@ -116,12 +118,12 @@ pub(in crate::control::planner::sql_plan_convert) fn convert_update( .iter() .filter_map(|(field, expr)| { if let SqlExpr::Literal(val) = expr { - Some((field.clone(), sql_value_to_msgpack(val))) + Some(sql_value_to_msgpack(val).map(|bytes| (field.clone(), bytes))) } else { None } }) - .collect(); + .collect::>()?; let key_bytes = sql_value_to_bytes(key)?; // Content-addressed identity: keeps the surrogate the original insert assigned. let surrogate = ctx.surrogate_for_pk(collection_key, &key_bytes)?; @@ -173,6 +175,7 @@ pub(in crate::control::planner::sql_plan_convert) fn convert_update( collection: qualified_collection.clone(), filters: effective_filter, updates, + system_from: None, rls_write_check: nodedb_types::RlsWriteCheck::pending_injection(), }), post_set_op: PostSetOp::None, diff --git a/nodedb/src/control/planner/sql_plan_convert/dml/upsert.rs b/nodedb/src/control/planner/sql_plan_convert/dml/upsert.rs index 3b1cf1dde..e87610b22 100644 --- a/nodedb/src/control/planner/sql_plan_convert/dml/upsert.rs +++ b/nodedb/src/control/planner/sql_plan_convert/dml/upsert.rs @@ -232,6 +232,7 @@ pub(in super::super) fn convert_upsert( schema_bytes, provenance: None, wal_lsn: None, + system_from: None, rls_write_check: nodedb_types::RlsWriteCheck::pending_injection(), // Filled by the later `inject_returning_spec` / row-level-security // passes — see the plain-insert site above. diff --git a/nodedb/src/control/planner/sql_plan_convert/expr/bridge_expr.rs b/nodedb/src/control/planner/sql_plan_convert/expr/bridge_expr.rs index 83e47f5a4..c7c521223 100644 --- a/nodedb/src/control/planner/sql_plan_convert/expr/bridge_expr.rs +++ b/nodedb/src/control/planner/sql_plan_convert/expr/bridge_expr.rs @@ -297,7 +297,9 @@ mod tests { let expr = SqlExpr::ArrayLiteral(vec![col("n"), SqlExpr::Literal(SqlValue::Int(1))]); let lowered = sql_expr_to_bridge_expr(&expr); assert_eq!( - lowered.eval(&row()).expect("eval"), + lowered + .eval(&row(), &nodedb_crs::CrsContext::builtin_only()) + .expect("eval"), Value::Array(vec![Value::Integer(7), Value::Integer(1)]) ); } @@ -325,7 +327,11 @@ mod tests { negated, case_insensitive, }; - let eval = |e: SqlExpr| sql_expr_to_bridge_expr(&e).eval(&row()).expect("eval"); + let eval = |e: SqlExpr| { + sql_expr_to_bridge_expr(&e) + .eval(&row(), &nodedb_crs::CrsContext::builtin_only()) + .expect("eval") + }; assert_eq!(eval(like(false, false, "Al%")), Value::Bool(true)); assert_eq!(eval(like(true, false, "Al%")), Value::Bool(false)); assert_eq!(eval(like(false, true, "al%")), Value::Bool(true)); diff --git a/nodedb/src/control/planner/sql_plan_convert/kv_counter_shape.rs b/nodedb/src/control/planner/sql_plan_convert/kv_counter_shape.rs index 8325bb1d0..d69109d16 100644 --- a/nodedb/src/control/planner/sql_plan_convert/kv_counter_shape.rs +++ b/nodedb/src/control/planner/sql_plan_convert/kv_counter_shape.rs @@ -53,7 +53,7 @@ pub(crate) fn kv_counter_shape( write_msgpack_map_header(&mut template, cells.len()); for (name, value) in &cells { write_msgpack_str(&mut template, name); - write_msgpack_value(&mut template, value); + write_msgpack_value(&mut template, value)?; } KvCounterShape::Typed { column, template } } diff --git a/nodedb/src/control/planner/sql_plan_convert/mod.rs b/nodedb/src/control/planner/sql_plan_convert/mod.rs index 202a4fc0e..322ec78d6 100644 --- a/nodedb/src/control/planner/sql_plan_convert/mod.rs +++ b/nodedb/src/control/planner/sql_plan_convert/mod.rs @@ -24,9 +24,11 @@ pub mod set_ops; pub mod surrogate_prefetch; #[cfg(test)] pub(crate) mod test_support; +pub mod valid_time; pub mod value; pub mod visitor; pub use cache_verdict::batch_cache_eligibility; pub use convert::{ConvertContext, PlanningPurpose, convert}; pub use surrogate_prefetch::{PrefetchedSurrogates, convert_bound}; +pub use valid_time::valid_time_point; diff --git a/nodedb/src/control/planner/sql_plan_convert/output_schema/build/constant.rs b/nodedb/src/control/planner/sql_plan_convert/output_schema/build/constant.rs index 0728c019c..4784e6ddc 100644 --- a/nodedb/src/control/planner/sql_plan_convert/output_schema/build/constant.rs +++ b/nodedb/src/control/planner/sql_plan_convert/output_schema/build/constant.rs @@ -52,7 +52,8 @@ fn constant_cell_type(value: Option<&nodedb_sql::types_expr::SqlValue>) -> DdlCo | SqlValue::Bytes(_) | SqlValue::Array(_) | SqlValue::Timestamp(_) - | SqlValue::Timestamptz(_), + | SqlValue::Timestamptz(_) + | SqlValue::Geometry(_), ) | None => DdlColType::Text, } diff --git a/nodedb/src/control/planner/sql_plan_convert/output_schema/cte_types.rs b/nodedb/src/control/planner/sql_plan_convert/output_schema/cte_types.rs index ede48c606..c95b4a40d 100644 --- a/nodedb/src/control/planner/sql_plan_convert/output_schema/cte_types.rs +++ b/nodedb/src/control/planner/sql_plan_convert/output_schema/cte_types.rs @@ -49,6 +49,10 @@ impl SqlCatalog for CatalogRef<'_, C> { fn lookup_array(&self, name: &str) -> Option { self.0.lookup_array(name) } + + fn crs_context(&self) -> nodedb_crs::CrsContext<'_> { + self.0.crs_context() + } } /// A catalog that answers for each CTE definition name first. @@ -136,6 +140,10 @@ impl SqlCatalog for CteTypesCatalog<'_> { fn lookup_array(&self, name: &str) -> Option { self.inner.lookup_array(name) } + + fn crs_context(&self) -> nodedb_crs::CrsContext<'_> { + self.inner.crs_context() + } } /// The relation one definition body exposes: one column per announced output diff --git a/nodedb/src/control/planner/sql_plan_convert/row_source.rs b/nodedb/src/control/planner/sql_plan_convert/row_source.rs index 1788164b6..9f54f08c5 100644 --- a/nodedb/src/control/planner/sql_plan_convert/row_source.rs +++ b/nodedb/src/control/planner/sql_plan_convert/row_source.rs @@ -72,16 +72,17 @@ pub(super) fn lower_row_source( && window_functions.is_empty() && matches!(temporal.valid_time, nodedb_sql::ValidTime::Any) { - let system_as_of_ms = match temporal.system_time { - SystemTimeScope::Current => Some(None), - SystemTimeScope::AsOf(ms) => Some(Some(ms)), + let system_time = match temporal.system_time { + scope @ (SystemTimeScope::Current + | SystemTimeScope::AsOf(_) + | SystemTimeScope::Pinned { .. }) => Some(scope), SystemTimeScope::AllVersions => None, }; - if let Some(system_as_of_ms) = system_as_of_ms { + if let Some(system_time) = system_time { return Ok(RowSource::DocumentPages(DocumentPageSource { collection: nodedb_types::QualifiedCollection::new(ctx.database_id, collection), filters: super::filter::serialize_filters(filters)?, - system_as_of_ms, + system_time, offset: *offset, limit: *limit, column_map: super::aggregate::serialize_column_map(maps.stored)?, @@ -380,6 +381,6 @@ mod tests { let RowSource::DocumentPages(page) = source_of(&tasks) else { panic!("expected a page source"); }; - assert_eq!(page.system_as_of_ms, Some(1_700_000_000_000)); + assert_eq!(page.system_time, SystemTimeScope::AsOf(1_700_000_000_000)); } } diff --git a/nodedb/src/control/planner/sql_plan_convert/scan/core.rs b/nodedb/src/control/planner/sql_plan_convert/scan/core.rs index 259345c01..08ec7c598 100644 --- a/nodedb/src/control/planner/sql_plan_convert/scan/core.rs +++ b/nodedb/src/control/planner/sql_plan_convert/scan/core.rs @@ -16,7 +16,7 @@ use super::super::expr::convert_sort_keys; use super::super::filter::serialize_filters; use super::super::scan_params::ScanParams; use super::super::value::{sql_value_to_bytes, sql_value_to_nodedb_value, sql_value_to_string}; -use super::helpers::valid_at_from_scope; +use crate::control::planner::sql_plan_convert::valid_time_point; use nodedb_physical::physical_task::{PhysicalTask, PostSetOp}; pub(in crate::control::planner::sql_plan_convert) fn convert_scan( @@ -103,7 +103,7 @@ pub(in crate::control::planner::sql_plan_convert) fn convert_scan( computed_columns: computed_bytes, rls_filters: Vec::new(), system_time: temporal.system_time, - valid_at_ms: valid_at_from_scope(temporal), + valid_at_ms: valid_time_point(temporal.valid_time, "timeseries")?, having: Vec::new(), tiering: None, output_columns: Vec::new(), @@ -117,7 +117,7 @@ pub(in crate::control::planner::sql_plan_convert) fn convert_scan( rls_filters: Vec::new(), sort_keys: sort.clone(), system_time: temporal.system_time, - valid_at_ms: valid_at_from_scope(temporal), + valid_at_ms: valid_time_point(temporal.valid_time, "columnar")?, prefilter: None, computed_columns: computed_bytes.clone(), }), @@ -133,7 +133,7 @@ pub(in crate::control::planner::sql_plan_convert) fn convert_scan( rls_filters: Vec::new(), sort_keys: sort.clone(), system_time: temporal.system_time, - valid_at_ms: valid_at_from_scope(temporal), + valid_at_ms: valid_time_point(temporal.valid_time, "spatial")?, prefilter: None, computed_columns: computed_bytes.clone(), }), @@ -171,7 +171,7 @@ pub(in crate::control::planner::sql_plan_convert) fn convert_scan( computed_columns: computed_bytes, window_functions: window_bytes, system_time: temporal.system_time, - valid_at_ms: valid_at_from_scope(temporal), + valid_at_ms: valid_time_point(temporal.valid_time, "document")?, prefilter: None, }) } @@ -256,9 +256,10 @@ pub(in crate::control::planner::sql_plan_convert) fn convert_document_index_look detail: format!("index lookup on '{collection}' cannot answer a valid-time read"), }); } - let system_as_of_ms = match temporal.system_time { - SystemTimeScope::Current => None, - SystemTimeScope::AsOf(ms) => Some(ms), + let system_time = match temporal.system_time { + scope @ (SystemTimeScope::Current + | SystemTimeScope::AsOf(_) + | SystemTimeScope::Pinned { .. }) => scope, SystemTimeScope::AllVersions => { return Err(crate::Error::PlanError { detail: format!("index lookup on '{collection}' cannot list every version"), @@ -279,7 +280,7 @@ pub(in crate::control::planner::sql_plan_convert) fn convert_document_index_look // No LIMIT fetches every matching row under the byte budget. limit: limit.unwrap_or(usize::MAX), offset, - system_as_of_ms, + system_time, }); Ok(vec![PhysicalTask { tenant_id, diff --git a/nodedb/src/control/planner/sql_plan_convert/scan/helpers.rs b/nodedb/src/control/planner/sql_plan_convert/scan/helpers.rs deleted file mode 100644 index 28804bbea..000000000 --- a/nodedb/src/control/planner/sql_plan_convert/scan/helpers.rs +++ /dev/null @@ -1,16 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 - -//! Tiny helpers shared across the scan/search converters. - -use nodedb_sql::TemporalScope; - -/// Project `TemporalScope::valid_time` into the wire field `valid_at_ms`. -/// -/// `ValidTime::Range` is currently not threaded on the wire — a range form -/// requires a wire-type widening, tracked as a separate batch. -pub(super) fn valid_at_from_scope(t: &TemporalScope) -> Option { - match t.valid_time { - nodedb_sql::ValidTime::Any | nodedb_sql::ValidTime::Range(..) => None, - nodedb_sql::ValidTime::At(ms) => Some(ms), - } -} diff --git a/nodedb/src/control/planner/sql_plan_convert/scan/join.rs b/nodedb/src/control/planner/sql_plan_convert/scan/join.rs index e37fe71ad..38c141c6a 100644 --- a/nodedb/src/control/planner/sql_plan_convert/scan/join.rs +++ b/nodedb/src/control/planner/sql_plan_convert/scan/join.rs @@ -104,7 +104,7 @@ fn bitmap_hint_to_plan(hint: &BitmapHint, database_id: DatabaseId) -> Option SpatialPredicate::DWithin, + nodedb_sql::types::SpatialPredicate::DWithin3D => SpatialPredicate::DWithin3D, nodedb_sql::types::SpatialPredicate::Contains => SpatialPredicate::Contains, nodedb_sql::types::SpatialPredicate::Intersects => SpatialPredicate::Intersects, nodedb_sql::types::SpatialPredicate::Within => SpatialPredicate::Within, diff --git a/nodedb/src/control/planner/sql_plan_convert/scan/timeseries.rs b/nodedb/src/control/planner/sql_plan_convert/scan/timeseries.rs index 9e933c4d2..5de086792 100644 --- a/nodedb/src/control/planner/sql_plan_convert/scan/timeseries.rs +++ b/nodedb/src/control/planner/sql_plan_convert/scan/timeseries.rs @@ -15,7 +15,7 @@ use super::super::expr::convert_sort_keys; use super::super::filter::serialize_filters; use super::super::scan_params::TimeseriesScanParams; use super::super::value::{InstantForm, row_to_msgpack_with, write_msgpack_array_header}; -use super::helpers::valid_at_from_scope; +use crate::control::planner::sql_plan_convert::valid_time_point; use nodedb_physical::physical_task::{PhysicalTask, PostSetOp}; pub(in crate::control::planner::sql_plan_convert) fn convert_timeseries_scan( @@ -98,7 +98,7 @@ pub(in crate::control::planner::sql_plan_convert) fn convert_timeseries_scan( computed_columns: computed_bytes, rls_filters: Vec::new(), system_time: temporal.system_time, - valid_at_ms: valid_at_from_scope(temporal), + valid_at_ms: valid_time_point(temporal.valid_time, "timeseries")?, having: having_bytes, tiering, output_columns, diff --git a/nodedb/src/control/planner/sql_plan_convert/set_ops.rs b/nodedb/src/control/planner/sql_plan_convert/set_ops.rs index 411681518..b9f3e4138 100644 --- a/nodedb/src/control/planner/sql_plan_convert/set_ops.rs +++ b/nodedb/src/control/planner/sql_plan_convert/set_ops.rs @@ -33,7 +33,8 @@ pub(super) fn convert_constant_result( | SqlValue::Float(_) | SqlValue::Bool(_) | SqlValue::Null - | SqlValue::String(_) => sql_value_to_nodedb_value(val), + | SqlValue::String(_) + | SqlValue::Geometry(_) => sql_value_to_nodedb_value(val), // The shaper has no typed renderer that reproduces PostgreSQL's // text form for these — `\x..` for bytes, `{1,2}` for arrays, the // ISO string for timestamps — from a typed value, so they keep @@ -113,6 +114,8 @@ pub(super) fn convert_truncate( PhysicalPlan::Columnar(ColumnarOp::Truncate { collection: qualified_collection, restart_identity, + system_from: None, + erase_history: false, }) } EngineType::Timeseries => PhysicalPlan::Timeseries(TimeseriesOp::Truncate { @@ -500,6 +503,7 @@ mod tests { PhysicalPlan::Columnar(ColumnarOp::Truncate { collection, restart_identity, + .. }) => { assert_eq!(collection.as_str(), "c"); assert!(*restart_identity, "{engine:?} must carry restart_identity"); diff --git a/nodedb/src/control/planner/sql_plan_convert/valid_time.rs b/nodedb/src/control/planner/sql_plan_convert/valid_time.rs new file mode 100644 index 000000000..a162d6982 --- /dev/null +++ b/nodedb/src/control/planner/sql_plan_convert/valid_time.rs @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! The valid-time instant a point read carries. + +use nodedb_sql::temporal::ValidTime; + +/// The valid-time instant of `valid_time` for a read on `engine`: `None` +/// reads every valid time, `Some(ms)` reads the versions whose valid time +/// contains `ms`. +/// +/// A `FOR VALID_TIME FROM lo TO hi` range answers `FeatureNotSupported`: +/// the read carries one instant only, so a range is refused, never widened +/// to every valid time. +pub fn valid_time_point(valid_time: ValidTime, engine: &'static str) -> crate::Result> { + match valid_time { + ValidTime::Any => Ok(None), + ValidTime::At(ms) => Ok(Some(ms)), + ValidTime::Range(lo, hi) => Err(crate::Error::FeatureNotSupported { + detail: format!( + "FOR VALID_TIME FROM {lo} TO {hi} is not supported on {engine} reads; use \ + AS OF VALID TIME or FOR VALID_TIME CONTAINS " + ), + }), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn a_point_passes_and_a_range_is_refused() { + assert_eq!(valid_time_point(ValidTime::Any, "graph").ok(), Some(None)); + assert_eq!( + valid_time_point(ValidTime::At(5), "graph").ok(), + Some(Some(5)) + ); + assert!(matches!( + valid_time_point(ValidTime::Range(1, 2), "document"), + Err(crate::Error::FeatureNotSupported { .. }) + )); + } +} diff --git a/nodedb/src/control/planner/sql_plan_convert/value/assignments.rs b/nodedb/src/control/planner/sql_plan_convert/value/assignments.rs index a91b0c570..d130c6065 100644 --- a/nodedb/src/control/planner/sql_plan_convert/value/assignments.rs +++ b/nodedb/src/control/planner/sql_plan_convert/value/assignments.rs @@ -21,7 +21,7 @@ pub(crate) fn assignments_to_update_values( let mut result = Vec::with_capacity(assignments.len()); for (field, expr) in assignments { let value = match expr { - SqlExpr::Literal(v) => UpdateValue::Literal(sql_value_to_msgpack(v)), + SqlExpr::Literal(v) => UpdateValue::Literal(sql_value_to_msgpack(v)?), _ => UpdateValue::Expr(sql_expr_to_bridge_expr(expr)), }; result.push((field.clone(), value)); @@ -38,7 +38,7 @@ pub(crate) fn assignments_to_update_values_qualified( let mut result = Vec::with_capacity(assignments.len()); for (field, expr) in assignments { let value = match expr { - SqlExpr::Literal(v) => UpdateValue::Literal(sql_value_to_msgpack(v)), + SqlExpr::Literal(v) => UpdateValue::Literal(sql_value_to_msgpack(v)?), _ => UpdateValue::Expr(sql_expr_to_bridge_expr_qualified(expr)), }; result.push((field.clone(), value)); diff --git a/nodedb/src/control/planner/sql_plan_convert/value/convert.rs b/nodedb/src/control/planner/sql_plan_convert/value/convert.rs index b803f5726..5c535067e 100644 --- a/nodedb/src/control/planner/sql_plan_convert/value/convert.rs +++ b/nodedb/src/control/planner/sql_plan_convert/value/convert.rs @@ -20,6 +20,7 @@ pub(crate) fn sql_value_to_nodedb_value(v: &SqlValue) -> nodedb_types::Value { SqlValue::Bytes(b) => nodedb_types::Value::Bytes(b.clone()), SqlValue::Timestamp(dt) => nodedb_types::Value::NaiveDateTime(*dt), SqlValue::Timestamptz(dt) => nodedb_types::Value::DateTime(*dt), + SqlValue::Geometry(g) => nodedb_types::Value::Geometry(g.clone()), } } @@ -34,6 +35,8 @@ pub(crate) fn sql_value_to_string(v: &SqlValue) -> String { SqlValue::Bytes(b) => format!("\\x{}", hex::encode(b)), SqlValue::Array(arr) => format_pg_array(arr), SqlValue::Null => String::new(), + // GeoJSON text, `crs` member included for a non-WGS 84 SRID. + SqlValue::Geometry(g) => nodedb_types::geometry::geojson_tree(g).to_string(), } } @@ -76,17 +79,18 @@ pub(crate) fn sql_value_to_bytes(v: &SqlValue) -> crate::Result> { | SqlValue::Null | SqlValue::Bytes(_) | SqlValue::Timestamp(_) - | SqlValue::Timestamptz(_) => Ok(nodedb_types::scalar_to_raw_bytes( + | SqlValue::Timestamptz(_) + | SqlValue::Geometry(_) => Ok(nodedb_types::scalar_to_raw_bytes( &sql_value_to_nodedb_value(v), )?), } } /// Encode a SQL value as standard msgpack for field-level updates. -pub(crate) fn sql_value_to_msgpack(v: &SqlValue) -> Vec { +pub(crate) fn sql_value_to_msgpack(v: &SqlValue) -> crate::Result> { let mut buf = Vec::with_capacity(16); - write_msgpack_value(&mut buf, v); - buf + write_msgpack_value(&mut buf, v)?; + Ok(buf) } #[cfg(test)] diff --git a/nodedb/src/control/planner/sql_plan_convert/value/msgpack_write.rs b/nodedb/src/control/planner/sql_plan_convert/value/msgpack_write.rs index 257123a6f..2cc8cd41b 100644 --- a/nodedb/src/control/planner/sql_plan_convert/value/msgpack_write.rs +++ b/nodedb/src/control/planner/sql_plan_convert/value/msgpack_write.rs @@ -37,7 +37,7 @@ pub(crate) fn row_to_msgpack_with( write_msgpack_map_header(&mut buf, row.len()); for (key, val) in row { write_msgpack_str(&mut buf, key); - write_msgpack_value_with(&mut buf, val, instants); + write_msgpack_value_with(&mut buf, val, instants)?; } Ok(buf) } @@ -85,12 +85,17 @@ pub(crate) fn write_msgpack_str(buf: &mut Vec, s: &str) { } /// Write one value with instants as ISO 8601 text. -pub(crate) fn write_msgpack_value(buf: &mut Vec, val: &SqlValue) { +pub(crate) fn write_msgpack_value(buf: &mut Vec, val: &SqlValue) -> crate::Result<()> { write_msgpack_value_with(buf, val, InstantForm::Iso8601) } -/// Write one value with instants in the given form. -pub(crate) fn write_msgpack_value_with(buf: &mut Vec, val: &SqlValue, instants: InstantForm) { +/// Write one value with instants in the given form. A geometry is the +/// geometry ext, which keeps its SRID, Z and M. +pub(crate) fn write_msgpack_value_with( + buf: &mut Vec, + val: &SqlValue, + instants: InstantForm, +) -> crate::Result<()> { match val { SqlValue::Null => buf.push(0xC0), SqlValue::Bool(true) => buf.push(0xC3), @@ -118,7 +123,7 @@ pub(crate) fn write_msgpack_value_with(buf: &mut Vec, val: &SqlValue, instan SqlValue::Array(arr) => { write_msgpack_array_header(buf, arr.len()); for item in arr { - write_msgpack_value_with(buf, item, instants); + write_msgpack_value_with(buf, item, instants)?; } } SqlValue::Bytes(b) => write_msgpack_bin(buf, b), @@ -130,7 +135,15 @@ pub(crate) fn write_msgpack_value_with(buf: &mut Vec, val: &SqlValue, instan InstantForm::Iso8601 => write_msgpack_str(buf, &dt.to_iso8601()), InstantForm::Ext => write_instant(buf, InstantKind::Utc, dt.micros), }, + SqlValue::Geometry(g) => { + let ext = nodedb_types::value_to_msgpack(&nodedb_types::Value::Geometry(g.clone())) + .map_err(|e| crate::Error::Internal { + detail: format!("geometry does not encode as MessagePack: {e}"), + })?; + buf.extend_from_slice(&ext); + } } + Ok(()) } fn write_msgpack_int(buf: &mut Vec, i: i64) { @@ -193,7 +206,7 @@ mod tests { #[test] fn write_msgpack_value_int() { let mut buf = Vec::new(); - write_msgpack_value(&mut buf, &SqlValue::Int(42)); + write_msgpack_value(&mut buf, &SqlValue::Int(42)).unwrap(); assert_eq!(buf, vec![42]); } @@ -203,7 +216,7 @@ mod tests { fn wide_integer_decimal_is_uint64() { let wide = rust_decimal::Decimal::from(u64::MAX); let mut buf = Vec::new(); - write_msgpack_value(&mut buf, &SqlValue::Decimal(wide)); + write_msgpack_value(&mut buf, &SqlValue::Decimal(wide)).unwrap(); assert_eq!(buf, [0xCF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]); assert_eq!( nodedb_types::value_from_msgpack(&buf).unwrap(), @@ -212,7 +225,7 @@ mod tests { let mut buf = Vec::new(); let fractional = rust_decimal::Decimal::from_str_exact("1.5").unwrap(); - write_msgpack_value(&mut buf, &SqlValue::Decimal(fractional)); + write_msgpack_value(&mut buf, &SqlValue::Decimal(fractional)).unwrap(); let mut expected = Vec::new(); write_msgpack_str(&mut expected, "1.5"); assert_eq!(buf, expected); @@ -221,7 +234,7 @@ mod tests { #[test] fn write_msgpack_value_string() { let mut buf = Vec::new(); - write_msgpack_value(&mut buf, &SqlValue::String("hi".into())); + write_msgpack_value(&mut buf, &SqlValue::String("hi".into())).unwrap(); assert_eq!(buf, vec![0xA2, b'h', b'i']); } @@ -234,7 +247,7 @@ mod tests { let dt = nodedb_types::NdbDateTime::from_micros(EARLY_MICROS); for val in [SqlValue::Timestamp(dt), SqlValue::Timestamptz(dt)] { let mut buf = Vec::new(); - write_msgpack_value(&mut buf, &val); + write_msgpack_value(&mut buf, &val).unwrap(); let mut expected = Vec::new(); write_msgpack_str(&mut expected, "2020-03-05T10:00:00.000000Z"); assert_eq!(buf, expected); @@ -251,7 +264,7 @@ mod tests { ]; for (val, kind) in cases { let mut buf = Vec::new(); - write_msgpack_value_with(&mut buf, &val, InstantForm::Ext); + write_msgpack_value_with(&mut buf, &val, InstantForm::Ext).unwrap(); assert_eq!( nodedb_types::read_instant(&buf, 0), Some((kind, EARLY_MICROS)), diff --git a/nodedb/src/control/planner/system_cut/mod.rs b/nodedb/src/control/planner/system_cut/mod.rs new file mode 100644 index 000000000..fd6243d80 --- /dev/null +++ b/nodedb/src/control/planner/system_cut/mod.rs @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! The per-statement system-time cut: every `AS OF SYSTEM TIME` of a +//! statement pinned to one ordinal under one read cut. + +pub mod pin; +pub mod sites; + +pub use pin::{CutPinning, pin_statement_cuts}; +pub use sites::{for_each_cut_site, for_each_cut_site_ref, plan_scopes}; diff --git a/nodedb/src/control/planner/system_cut/pin.rs b/nodedb/src/control/planner/system_cut/pin.rs new file mode 100644 index 000000000..eb64a9b6c --- /dev/null +++ b/nodedb/src/control/planner/system_cut/pin.rs @@ -0,0 +1,182 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! Pin every `AS OF SYSTEM TIME` of one statement under one read cut. +//! +//! The planner lowers `AS OF SYSTEM TIME ` to `SystemTimeScope::AsOf`. +//! Before the statement's tasks leave the Control Plane, this pass takes the +//! statement's read cut once (`read_cut::statement_cap`), sized by the +//! latest cut of the statement, and rewrites every `AsOf` to +//! `SystemTimeScope::Pinned`: the last ordinal of its millisecond, capped at +//! the cut. Every Data Plane read of the statement then reads one cut, on +//! every core and every node. A future time reads the state at the +//! statement and never a later write. + +use nodedb_physical::physical_task::PhysicalTask; +use nodedb_types::SystemTimeScope; + +use crate::control::read_cut::{StatementCap, statement_cap}; +use crate::control::state::SharedState; + +use super::sites::{for_each_cut_site, for_each_cut_site_ref}; + +/// What pinning a statement's cuts did. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct CutPinning { + /// True when the read cut capped at least one cut. A capped cut depends + /// on when the statement ran, so its plan never enters a plan cache. An + /// uncapped cut reads the same rows whenever it runs. + pub capped: bool, +} + +/// Pin every unpinned `AS OF SYSTEM TIME` cut in `tasks` under one read cut +/// of `state`. A task set with no unpinned cut takes no read cut. The read +/// cut is cheaper when every cut is in the past (`statement_cap`). +pub async fn pin_statement_cuts( + state: &SharedState, + tasks: &mut [PhysicalTask], +) -> crate::Result { + let latest = tasks + .iter() + .filter_map(|task| latest_unpinned_ms(&task.plan)) + .max(); + let Some(latest_as_of_ms) = latest else { + return Ok(CutPinning::default()); + }; + let cap = statement_cap(state, latest_as_of_ms).await?; + let mut pinning = CutPinning::default(); + for task in tasks.iter_mut() { + for_each_cut_site(&mut task.plan, &mut |site| { + pinning.capped |= pin_site(site, cap); + }); + } + Ok(pinning) +} + +/// The latest millisecond of a cut in `plan` no statement pinned yet, or +/// `None` when `plan` holds no such cut. +fn latest_unpinned_ms(plan: &nodedb_physical::physical_plan::PhysicalPlan) -> Option { + let mut latest: Option = None; + for_each_cut_site_ref(plan, &mut |scope| { + let ms = match *scope { + SystemTimeScope::AsOf(ms) => Some(ms), + SystemTimeScope::Current + | SystemTimeScope::Pinned { .. } + | SystemTimeScope::AllVersions => None, + }; + latest = latest.max(ms); + }); + latest +} + +/// Pin one cut under `cap`. Returns true when the cap lowered it. +fn pin_site(scope: &mut SystemTimeScope, cap: StatementCap) -> bool { + match *scope { + SystemTimeScope::AsOf(ms) => { + let (pinned, capped) = pin_ms(ms, cap); + *scope = pinned; + capped + } + SystemTimeScope::Current + | SystemTimeScope::Pinned { .. } + | SystemTimeScope::AllVersions => false, + } +} + +/// The pinned scope of a cut at `ms` under `cap`, and whether the cap +/// lowered it. +fn pin_ms(ms: i64, cap: StatementCap) -> (SystemTimeScope, bool) { + let cut = cap.pin(ms); + let pinned = SystemTimeScope::Pinned { + ordinal: cut.ordinal, + cut_marker: cut.marker, + }; + let capped = cut.ordinal < nodedb_types::ms_to_ordinal_upper(ms).max(0); + (pinned, capped) +} + +#[cfg(test)] +mod tests { + use super::*; + + const CAP: StatementCap = StatementCap { + cap: 1_700_000_000_500 * nodedb_types::NANOS_PER_MS + 7, + marker: 3, + }; + + #[test] + fn a_past_cut_keeps_its_millisecond_and_takes_the_marker() { + let mut scope = SystemTimeScope::AsOf(1_700_000_000_000); + assert!(!pin_site(&mut scope, CAP)); + assert_eq!( + scope, + SystemTimeScope::Pinned { + ordinal: nodedb_types::ms_to_ordinal_upper(1_700_000_000_000), + cut_marker: 3, + } + ); + } + + #[test] + fn a_future_cut_reads_at_the_cap() { + let mut scope = SystemTimeScope::AsOf(1_800_000_000_000); + assert!(pin_site(&mut scope, CAP)); + assert_eq!( + scope, + SystemTimeScope::Pinned { + ordinal: CAP.cap, + cut_marker: 3, + } + ); + } + + /// An array aggregate's cut pins like any scan's: it takes the cap's + /// ordinal and the marker, so every serving node waits for the cut. + #[test] + fn an_array_aggregate_cut_pins_with_the_marker() { + use nodedb_physical::physical_plan::{ArrayOp, ArrayReducer, PhysicalPlan}; + let mut plan = PhysicalPlan::Array(ArrayOp::Aggregate { + array_id: nodedb_array::types::ArrayId::new(nodedb_types::TenantId::new(1), "a"), + attr_idx: 0, + reducer: ArrayReducer::Sum, + group_by_dim: -1, + cell_filter: None, + return_partial: false, + shard_bucket: None, + system_time: SystemTimeScope::AsOf(1_800_000_000_000), + valid_at_ms: None, + region_msgpack: Vec::new(), + physical: false, + }); + assert_eq!(latest_unpinned_ms(&plan), Some(1_800_000_000_000)); + let mut capped = false; + for_each_cut_site(&mut plan, &mut |scope| capped |= pin_site(scope, CAP)); + assert!(capped); + let PhysicalPlan::Array(ArrayOp::Aggregate { system_time, .. }) = plan else { + panic!("the plan stays an array aggregate"); + }; + assert_eq!( + system_time, + SystemTimeScope::Pinned { + ordinal: CAP.cap, + cut_marker: 3, + } + ); + assert_eq!(system_time.marked_cut(), Some((CAP.cap, 3))); + } + + #[test] + fn a_live_pinned_or_audit_scope_stays() { + for scope in [ + SystemTimeScope::Current, + SystemTimeScope::AllVersions, + SystemTimeScope::Pinned { + ordinal: 9, + cut_marker: 0, + }, + ] { + let mut site = scope; + assert!(!pin_site(&mut site, CAP)); + assert_eq!(site, scope); + } + } +} diff --git a/nodedb/src/control/planner/system_cut/sites.rs b/nodedb/src/control/planner/system_cut/sites.rs new file mode 100644 index 000000000..d14f7185a --- /dev/null +++ b/nodedb/src/control/planner/system_cut/sites.rs @@ -0,0 +1,186 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! Every system-time scope a plan reads at, the plan's own and its child +//! plans'. +//! +//! One macro writes the exhaustive match once. It expands to a shared walk +//! for reads and to a mutable walk for the in-place rewrite. + +use nodedb_physical::physical_plan::{ + ArrayOp, ClusterArrayOp, ColumnarOp, DocumentOp, PhysicalPlan, RowSource, TimeseriesOp, +}; +use nodedb_types::SystemTimeScope; + +use crate::control::planner::plan_children::{children, children_mut}; + +/// Every system-time scope `plan` and its child plans read at, in walk +/// order. +pub fn plan_scopes(plan: &PhysicalPlan) -> Vec { + let mut scopes = Vec::new(); + for_each_cut_site_ref(plan, &mut |scope| scopes.push(*scope)); + scopes +} + +/// Expands the cut-site walk for one reference kind: `$($m)?` is empty for +/// a shared walk and `mut` for a mutable one. Every match is exhaustive. +macro_rules! cut_site_walk { + ($walk:ident, $own:ident, $document:ident, $children:ident $(, $m:tt)?) => { + /// Call `visit` on every system-time scope `plan` and its child plans + /// read at, in walk order. + pub fn $walk( + plan: &$($m)? PhysicalPlan, + visit: &mut dyn FnMut(&$($m)? SystemTimeScope), + ) { + $own(plan, visit); + for child in $children(plan) { + $walk(child, visit); + } + } + + /// The cuts `plan` itself reads at. Exhaustive over [`PhysicalPlan`]. + fn $own( + plan: &$($m)? PhysicalPlan, + visit: &mut dyn FnMut(&$($m)? SystemTimeScope), + ) { + match plan { + PhysicalPlan::Document(op) => $document(op, visit), + PhysicalPlan::Columnar(op) => match op { + ColumnarOp::Scan { system_time, .. } => visit(system_time), + ColumnarOp::Insert { .. } + | ColumnarOp::Update { .. } + | ColumnarOp::Delete { .. } + | ColumnarOp::ResolvedUpdate { .. } + | ColumnarOp::ResolvedDelete { .. } + | ColumnarOp::ResolveDml { .. } + | ColumnarOp::MaterializeScan { .. } + | ColumnarOp::Truncate { .. } => {} + }, + PhysicalPlan::Timeseries(op) => match op { + TimeseriesOp::Scan { system_time, .. } => visit(system_time), + TimeseriesOp::Ingest { .. } + | TimeseriesOp::ResolveIngest(_) + | TimeseriesOp::Truncate { .. } + | TimeseriesOp::RefreshAggregate { .. } + | TimeseriesOp::Partitions { .. } + | TimeseriesOp::ReadAggregate { .. } => {} + }, + PhysicalPlan::Array(op) => match op { + ArrayOp::Slice { system_time, .. } + | ArrayOp::Aggregate { system_time, .. } + | ArrayOp::Elementwise { system_time, .. } => visit(system_time), + ArrayOp::OpenArray { .. } + | ArrayOp::Put { .. } + | ArrayOp::Delete { .. } + | ArrayOp::Flush { .. } + | ArrayOp::Compact { .. } + | ArrayOp::SurrogateBitmapScan { .. } + | ArrayOp::DropArray { .. } + | ArrayOp::RekeyArray { .. } + | ArrayOp::PurgeArrayDrop { .. } => {} + }, + PhysicalPlan::ClusterArray(op) => match op { + ClusterArrayOp::Slice { system_time, .. } + | ClusterArrayOp::Agg { system_time, .. } + | ClusterArrayOp::Elementwise { system_time, .. } => visit(system_time), + ClusterArrayOp::Put { .. } + | ClusterArrayOp::Delete { .. } + | ClusterArrayOp::Flush { .. } + | ClusterArrayOp::Compact { .. } => {} + }, + PhysicalPlan::Query(_) + | PhysicalPlan::Vector(_) + | PhysicalPlan::Graph(_) + | PhysicalPlan::Kv(_) + | PhysicalPlan::Text(_) + | PhysicalPlan::Spatial(_) + | PhysicalPlan::Crdt(_) + | PhysicalPlan::Meta(_) + | PhysicalPlan::ClusterEvent(_) => {} + } + } + + /// The cuts a document op itself reads at. A query source's plan is + /// a child plan, so the walk reaches it through the child walk. + fn $document( + op: &$($m)? DocumentOp, + visit: &mut dyn FnMut(&$($m)? SystemTimeScope), + ) { + match op { + DocumentOp::PointGet { system_time, .. } + | DocumentOp::Scan { system_time, .. } + | DocumentOp::IndexedFetch { system_time, .. } + | DocumentOp::MaterializeScan { system_time, .. } => visit(system_time), + DocumentOp::InsertSelect { source, .. } + | DocumentOp::Merge { source, .. } + | DocumentOp::UpdateFromJoin { source, .. } => match source { + RowSource::DocumentPages(page) => visit(&$($m)? page.system_time), + RowSource::Query(_) => {} + }, + DocumentOp::ResolveWrite(inner) => $document(inner, visit), + DocumentOp::PointPut { .. } + | DocumentOp::PointInsert { .. } + | DocumentOp::PointDelete { .. } + | DocumentOp::PointUpdate { .. } + | DocumentOp::BatchInsert { .. } + | DocumentOp::RangeScan { .. } + | DocumentOp::Register { .. } + | DocumentOp::IndexLookup { .. } + | DocumentOp::DropIndex { .. } + | DocumentOp::BackfillIndex { .. } + | DocumentOp::Truncate { .. } + | DocumentOp::EstimateCount { .. } + | DocumentOp::Upsert { .. } + | DocumentOp::BatchUpsert { .. } + | DocumentOp::BulkUpdate { .. } + | DocumentOp::BulkDelete { .. } + | DocumentOp::ApplyBalanceDelta { .. } + | DocumentOp::ResolvedWrite { .. } => {} + } + } + }; +} + +cut_site_walk!( + for_each_cut_site_ref, + own_sites_ref, + document_sites_ref, + children +); +cut_site_walk!( + for_each_cut_site, + own_sites_mut, + document_sites_mut, + children_mut, + mut +); + +#[cfg(test)] +mod tests { + use super::*; + + fn point_get(system_time: SystemTimeScope) -> DocumentOp { + DocumentOp::PointGet { + collection: nodedb_types::QualifiedCollection::new( + nodedb_types::DatabaseId::DEFAULT, + "docs", + ), + document_id: "a".into(), + surrogate: None, + pk_bytes: Vec::new(), + rls_filters: Vec::new(), + system_time, + valid_at_ms: None, + } + } + + /// The shared and the mutable walk reach the same sites in one order. + #[test] + fn both_walks_reach_the_same_sites() { + let mut plan = PhysicalPlan::Document(DocumentOp::ResolveWrite(Box::new(point_get( + SystemTimeScope::AsOf(7), + )))); + assert_eq!(plan_scopes(&plan), vec![SystemTimeScope::AsOf(7)]); + for_each_cut_site(&mut plan, &mut |scope| *scope = SystemTimeScope::Current); + assert_eq!(plan_scopes(&plan), vec![SystemTimeScope::Current]); + } +} diff --git a/nodedb/src/control/read_cut/mod.rs b/nodedb/src/control/read_cut/mod.rs new file mode 100644 index 000000000..ec95d6f97 --- /dev/null +++ b/nodedb/src/control/read_cut/mod.rs @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! The read cut of a historical statement or a distributed run: the cap it +//! pins, the cut marker that fixes it, and the wait every serving node does +//! before it reads. + +pub mod run_marker; +pub mod statement_cap; +pub mod wait; + +pub(crate) use run_marker::{agreed_cut, new_cut_marker}; +pub(crate) use statement_cap::{StatementCap, pin_historical_cut, statement_cap}; +pub(crate) use wait::await_read_cut; diff --git a/nodedb/src/control/server/graph_dispatch/run_cut.rs b/nodedb/src/control/read_cut/run_marker.rs similarity index 90% rename from nodedb/src/control/server/graph_dispatch/run_cut.rs rename to nodedb/src/control/read_cut/run_marker.rs index 76a4595bf..8e4706621 100644 --- a/nodedb/src/control/server/graph_dispatch/run_cut.rs +++ b/nodedb/src/control/read_cut/run_marker.rs @@ -1,6 +1,6 @@ // SPDX-License-Identifier: BUSL-1.1 -//! The read cut a distributed graph run (BSP PageRank, WCC) reads at. +//! The cut marker one distributed run (BSP PageRank, WCC) reads at. //! //! The coordinator names a Calvin cut marker by a fresh watermark and sends //! it in the run's first dispatch. Every owner node proposes and waits for @@ -12,13 +12,13 @@ use crate::control::state::SharedState; /// A fresh cut marker watermark for one run, from this node's HLC. -pub(super) fn new_cut_marker(state: &SharedState) -> u64 { +pub(crate) fn new_cut_marker(state: &SharedState) -> u64 { state.hlc_clock.now().wall_ns.max(1) } /// The one read cut every owner node answered. A node that answered no cut, -/// or a different one, is an error: the run will read two graphs. -pub(super) fn agreed_cut( +/// or a different one, is an error: the run reads two graphs. +pub(crate) fn agreed_cut( answers: impl IntoIterator)>, ) -> crate::Result { let mut agreed: Option<(u64, i64)> = None; diff --git a/nodedb/src/control/read_cut/statement_cap.rs b/nodedb/src/control/read_cut/statement_cap.rs new file mode 100644 index 000000000..0c58c52b7 --- /dev/null +++ b/nodedb/src/control/read_cut/statement_cap.rs @@ -0,0 +1,353 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! The read cut one historical statement pins, shared by every engine. +//! +//! The statement takes its cap once ([`statement_cap`]). Every graph walk +//! and every versioned document, index, columnar, timeseries, and array +//! read of the statement pins its `AS OF SYSTEM TIME` under that cap +//! ([`StatementCap::pin`]), and every serving node waits for the cap's cut +//! marker before it reads (`super::wait`). + +use crate::control::state::SharedState; + +/// The cut a historical statement pins: the ordinal every read of the +/// statement reads at, and the Calvin cut marker that caps it. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct HistoricalCut { + /// `AS OF SYSTEM TIME as_of_ms`, capped at the statement's read cut. + pub ordinal: i64, + /// The cut marker whose cut is the cap, `0` on a node without cluster + /// routing. Every owner node waits for it before it reads at `ordinal` + /// (`super::wait`). + pub marker: u64, +} + +/// The read cut of one historical statement: the cap every `AS OF SYSTEM +/// TIME` of the statement is pinned under, and the cut marker that fixes it. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct StatementCap { + /// The newest ordinal any read of the statement reads at. + pub cap: i64, + /// The cut marker whose cut is `cap`, `0` on a node without cluster + /// routing. + pub marker: u64, +} + +impl StatementCap { + /// The cut a read `AS OF SYSTEM TIME as_of_ms` pins: the last ordinal of + /// `as_of_ms`, capped at the statement's read cut. A time before the + /// epoch reads before every write. A time past the cap reads as of the + /// cap. + pub(crate) fn pin(self, as_of_ms: i64) -> HistoricalCut { + HistoricalCut { + ordinal: nodedb_types::ms_to_ordinal_upper(as_of_ms) + .max(0) + .min(self.cap), + marker: self.marker, + } + } +} + +/// The read cut of one historical statement, taken once per statement. +/// Every graph walk and every versioned document, index, columnar, +/// timeseries, and array read of the statement pins its `AS OF SYSTEM +/// TIME` under it ([`StatementCap::pin`]), so the statement reads one cut. +/// +/// `latest_as_of_ms` is the latest `AS OF SYSTEM TIME` the statement pins. +/// +/// Versions carry ordinals from two sources. A write outside the Calvin +/// sequencer stamps a fresh ordinal of the node's ordinal clock, the clock +/// every Data Plane core stamps from. Applying a replicated version folds +/// its stamp into that clock. A Calvin transaction stamps the ordinal of +/// its epoch instant and position. Epoch instants rise strictly in log +/// order, and the sequencer mints each at or after its wall clock. +/// +/// A past cut (`latest_as_of_ms` ends below a fresh clock ordinal) needs no +/// cap: the clock stamps every later write above it, and a Calvin epoch +/// minted after the statement starts carries a later instant. An epoch +/// minted before the statement starts can still be applying, so in a +/// cluster the statement places one cut marker, and every serving node +/// waits for it. +/// +/// A cut at or past now takes a cap in a cluster in three steps: +/// +/// 1. A first marker. Once it resolves, every Calvin version sequenced +/// before it is installed here and folded into the clock. +/// 2. The clock part ([`settled_clock_cap`]), read after the first marker: +/// at or above every version applied here, including one stamped by a +/// node whose clock leads. The wall clock then leaves its millisecond. +/// 3. A second marker, proposed after the clock part settles. A Calvin +/// epoch minted after it carries a later instant, so its versions are +/// above the cap. Its cut covers every Calvin epoch minted before it. +/// +/// The cap is the higher of the clock part and the second marker's cut +/// ([`cluster_cap`]), and the plan carries the second marker. On a node +/// without cluster routing the cap is the clock part alone. +pub(crate) async fn statement_cap( + state: &SharedState, + latest_as_of_ms: i64, +) -> crate::Result { + let clock = state.wal.ordinal_clock(); + let probe = clock_cap(&clock); + let clustered = state.cluster_routing.is_some(); + if is_past_cut(latest_as_of_ms, probe) { + let marker = if clustered { + let marker = super::run_marker::new_cut_marker(state); + resolve_read_cut(state, marker).await?; + marker + } else { + 0 + }; + return Ok(StatementCap { cap: probe, marker }); + } + if !clustered { + return Ok(StatementCap { + cap: settled_clock_cap(&clock).await, + marker: 0, + }); + } + resolve_read_cut(state, super::run_marker::new_cut_marker(state)).await?; + let local = settled_clock_cap(&clock).await; + let marker = super::run_marker::new_cut_marker(state); + let calvin_cut = resolve_read_cut(state, marker).await?; + let cap = cluster_cap(local, calvin_cut); + // A Calvin cut above the clock part moves the clock past the cap too. + clock.update_from_remote(cap); + Ok(StatementCap { cap, marker }) +} + +/// True when a cut at `as_of_ms` ends below `clock_now`, a fresh ordinal of +/// the clock: no later clock stamp can fall at or below it. +fn is_past_cut(as_of_ms: i64, clock_now: i64) -> bool { + nodedb_types::ms_to_ordinal_upper(as_of_ms) < clock_now +} + +async fn resolve_read_cut(state: &SharedState, marker: u64) -> crate::Result { + crate::control::server::exchange::all_cores::read_cut::resolve_read_cut(state, marker).await +} + +/// The cap of a cluster statement: the clock ordinal `local`, raised to the +/// Calvin marker's cut when the sequencer's epoch instants run ahead of the +/// clock. The marker's cut alone misses every clock-stamped version newer +/// than the last Calvin epoch, and is `0` when no epoch applied. +fn cluster_cap(local: i64, calvin_cut: i64) -> i64 { + local.max(calvin_cut) +} + +/// The longest the clock ordinal can lead the wall clock before +/// [`await_wall_past`] stops waiting for it. A clock folded far ahead +/// carries a replayed stamp from a node whose clock ran ahead. Waiting out +/// that lead stalls every historical statement for the whole lead, so the +/// wait stops at this bound. +const MAX_CLOCK_LEAD: std::time::Duration = std::time::Duration::from_millis(2); + +/// The clock part of a statement's cap: a fresh ordinal of `clock`, raised +/// to the last ordinal of its millisecond once the wall clock has left that +/// millisecond. A later write then stamps above the cap, and an engine that +/// stamps milliseconds reads the cap's whole millisecond. A clock that leads +/// the wall by more than [`MAX_CLOCK_LEAD`] keeps the fresh ordinal: later +/// writes still stamp from the lead, inside its millisecond. +async fn settled_clock_cap(clock: &nodedb_types::OrdinalClock) -> i64 { + let local = clock_cap(clock); + if await_wall_past(local).await { + nodedb_types::ms_to_ordinal_upper(nodedb_types::ordinal_to_ms(local)) + } else { + local + } +} + +/// Wait until the wall clock is past the millisecond of `ordinal`. Returns +/// false at once when `ordinal` leads the wall clock by more than +/// [`MAX_CLOCK_LEAD`]. +async fn await_wall_past(ordinal: i64) -> bool { + let target_ns = nodedb_types::ms_to_ordinal_upper(nodedb_types::ordinal_to_ms(ordinal)); + loop { + let now_ns = nodedb_types::wall_now_ns_i64(); + if now_ns > target_ns { + return true; + } + let wait_ns = u64::try_from(target_ns - now_ns + 1).unwrap_or(0); + let wait = std::time::Duration::from_nanos(wait_ns); + if wait > MAX_CLOCK_LEAD { + return false; + } + tokio::time::sleep(wait).await; + } +} + +/// The one system-time ordinal every read of a historical statement reads +/// at: `AS OF SYSTEM TIME as_of_ms`, pinned under the statement's +/// read cut ([`statement_cap`]). Every hop of a walk carries it, so the walk +/// reads one graph. +pub(crate) async fn pin_historical_cut( + state: &SharedState, + as_of_ms: i64, +) -> crate::Result { + Ok(statement_cap(state, as_of_ms).await?.pin(as_of_ms)) +} + +/// The clock part of a statement's cap: a fresh ordinal of `clock`. Every +/// version stamped from `clock` before the read is at or below it, and +/// every later one is above it. +fn clock_cap(clock: &nodedb_types::OrdinalClock) -> i64 { + clock.next_ordinal() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn a_single_node_cut_closes_between_two_writes() { + let clock = nodedb_types::OrdinalClock::new(); + let before = clock.next_ordinal(); + let cap = StatementCap { + cap: clock_cap(&clock), + marker: 0, + }; + let after = clock.next_ordinal(); + let cut = cap.pin(i64::MAX / nodedb_types::NANOS_PER_MS).ordinal; + assert!(before <= cut, "a write before the read is under the cut"); + assert!(after > cut, "a write after the read is above the cut"); + assert_eq!( + cap.pin(5).ordinal, + nodedb_types::ms_to_ordinal_upper(5), + "an earlier time is not capped" + ); + assert_eq!( + cap.pin(-5).ordinal, + 0, + "a time before the epoch reads nothing" + ); + } + + /// A document version stamped from the Data Plane clock, with no Calvin + /// epoch applied (the marker's cut is `0`), sits under the cluster cap + /// and under a pin at its own millisecond. The marker's cut alone read + /// every such version as absent. + #[test] + fn a_clock_stamped_version_is_under_the_cluster_cut() { + let clock = nodedb_types::OrdinalClock::new(); + let version = clock.next_ordinal(); + let no_epoch = 0; + assert!( + no_epoch < version, + "the marker's cut alone hides the version" + ); + let cap = StatementCap { + cap: cluster_cap(clock_cap(&clock), no_epoch), + marker: 9, + }; + assert!(cap.cap >= version); + let at_its_ms = cap.pin(nodedb_types::ordinal_to_ms(version)); + assert!( + at_its_ms.ordinal >= version, + "a cut at the version's ms reads it" + ); + let future = cap.pin(nodedb_types::ordinal_to_ms(version) + 86_400_000); + assert!(future.ordinal >= version, "a future cut reads it"); + let pinned = nodedb_types::SystemTimeScope::Pinned { + ordinal: future.ordinal, + cut_marker: future.marker, + }; + assert!( + pinned.as_of_ordinal() >= Some(version), + "a pinned scope reads the version" + ); + } + + /// A version stamped by a node whose clock leads, folded into this + /// node's clock when it applies, sits under a clock part read after + /// the fold. + #[tokio::test] + async fn a_folded_version_from_a_leading_clock_is_under_the_cap() { + let clock = nodedb_types::OrdinalClock::new(); + let ahead = clock.next_ordinal() + 1_500_000; + clock.update_from_remote(ahead); + let cap = cluster_cap(settled_clock_cap(&clock).await, 0); + assert!(cap >= ahead, "a folded version is under the cap"); + assert!(clock.next_ordinal() > cap, "a later write is above the cap"); + } + + /// A cut that ends below a fresh clock ordinal is past: no later stamp + /// falls at or below it. A cut in the clock's millisecond or later is + /// not. + #[test] + fn only_a_cut_below_the_clock_is_past() { + let clock = nodedb_types::OrdinalClock::new(); + let now = clock.next_ordinal(); + let now_ms = nodedb_types::ordinal_to_ms(now); + assert!(is_past_cut(now_ms - 1, now)); + assert!( + !is_past_cut(now_ms, now), + "the clock's own millisecond is not past" + ); + assert!(!is_past_cut(now_ms + 1, now)); + let cap = StatementCap { + cap: now, + marker: 0, + }; + assert_eq!( + cap.pin(now_ms - 1).ordinal, + nodedb_types::ms_to_ordinal_upper(now_ms - 1), + "a past cut is never capped" + ); + } + + /// Calvin epoch instants that ran ahead of the clock raise the cap. + #[test] + fn a_calvin_cut_ahead_of_the_clock_raises_the_cap() { + let clock = nodedb_types::OrdinalClock::new(); + let local = clock_cap(&clock); + let ahead = nodedb_types::ms_to_ordinal_upper(nodedb_types::ordinal_to_ms(local) + 5); + assert_eq!(cluster_cap(local, ahead), ahead); + assert_eq!(cluster_cap(local, 0), local); + } + + /// The settled clock cap covers every version stamped before it, the + /// whole of its millisecond, and no version stamped after it. + #[tokio::test] + async fn a_settled_clock_cap_closes_its_millisecond() { + let clock = nodedb_types::OrdinalClock::new(); + let before = clock.next_ordinal(); + let cap = settled_clock_cap(&clock).await; + let after = clock.next_ordinal(); + assert!( + before <= cap, + "a write before the statement is under the cap" + ); + assert!(after > cap, "a write after the statement is above the cap"); + assert_eq!( + cap, + nodedb_types::ms_to_ordinal_upper(nodedb_types::ordinal_to_ms(cap)), + "the cap is the last ordinal of its millisecond" + ); + } + + #[tokio::test] + async fn a_clock_far_ahead_of_the_wall_is_not_waited_out() { + let clock = nodedb_types::OrdinalClock::new(); + let lead = nodedb_types::wall_now_ns_i64() + 10_000_000_000; + clock.update_from_remote(lead); + let started = std::time::Instant::now(); + let cap = settled_clock_cap(&clock).await; + assert!(started.elapsed() < std::time::Duration::from_secs(1)); + assert_eq!(cap, lead + 1, "a leading clock keeps its fresh ordinal"); + } + + #[test] + fn every_pin_of_one_statement_shares_its_marker() { + let cap = StatementCap { + cap: 10_000_000_000, + marker: 7, + }; + let early = cap.pin(3); + let late = cap.pin(1_000_000); + assert_eq!(early.marker, 7); + assert_eq!(late.marker, 7); + assert_eq!( + late.ordinal, 10_000_000_000, + "a future time reads at the cap" + ); + } +} diff --git a/nodedb/src/control/read_cut/wait.rs b/nodedb/src/control/read_cut/wait.rs new file mode 100644 index 000000000..c5e69b6d3 --- /dev/null +++ b/nodedb/src/control/read_cut/wait.rs @@ -0,0 +1,184 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! The wait every node does before it serves a historical read. +//! +//! A historical statement pins one cut: `AS OF SYSTEM TIME`, capped at the +//! statement's read cut (`super::statement_cap`). Every plan of the +//! statement carries the ordinal and the Calvin cut marker: a graph read in +//! its `GraphReadScope`, and a document, columnar, timeseries, or array read +//! in its `SystemTimeScope::Pinned`. A node reads at the ordinal only once +//! the marker applied there and every Calvin scheduler it runs passed it, so +//! every Calvin version sequenced before the marker is installed on that +//! node. It then folds the ordinal into its ordinal clock, so no write it +//! stamps later lands at or below the cut. A node that has not installed +//! them answers with versions missing, so the wait is never skipped: it runs +//! on the coordinator's local cores and on every remote owner, and it ends +//! at the statement deadline with `Error::DeadlineExceeded`, never with a +//! partial answer. + +use nodedb_physical::physical_plan::{GraphReadScope, GraphSystemCut, PhysicalPlan}; + +use crate::control::server::exchange::all_cores::read_cut::resolve_read_cut_by; +use crate::control::server::shared::session::StatementDeadline; +use crate::control::state::SharedState; + +/// Wait until this node installed every version at or below each +/// historical cut `plan` reads at, or until `deadline`. A live plan and a +/// cut no marker caps return at once. +/// +/// The marker wait installs every Calvin version sequenced before the +/// marker. The cut can sit above the node's Calvin cut: the statement's cap +/// holds a clock part for writes outside Calvin. The wait then folds the +/// cut's ordinal into this node's ordinal clock, so no write this node +/// stamps after the fold lands at or below the cut. +pub(crate) async fn await_read_cut( + state: &SharedState, + plan: &PhysicalPlan, + deadline: StatementDeadline, +) -> crate::Result<()> { + for (ordinal, marker) in plan_marked_cuts(plan) { + resolve_read_cut_by(state, marker, deadline).await?; + state.wal.ordinal_clock().update_from_remote(ordinal); + } + Ok(()) +} + +/// Each marker that caps a cut of `plan`, with the newest ordinal the plan +/// reads at under it. Empty when no marker caps a cut of the plan. +fn plan_marked_cuts(plan: &PhysicalPlan) -> Vec<(i64, u64)> { + let graph_cut = if let PhysicalPlan::Graph(op) = plan { + op.read_scope().and_then(marked_cut) + } else { + None + }; + let scope_cuts = crate::control::planner::system_cut::plan_scopes(plan) + .into_iter() + .filter_map(|scope| scope.marked_cut()); + let mut cuts: Vec<(i64, u64)> = Vec::new(); + for (ordinal, marker) in graph_cut.into_iter().chain(scope_cuts) { + match cuts.iter_mut().find(|(_, seen)| *seen == marker) { + Some((newest, _)) => *newest = (*newest).max(ordinal), + None => cuts.push((ordinal, marker)), + } + } + cuts +} + +/// The ordinal and the marker of `scope`, when a marker caps an `AtOrdinal` +/// cut. +fn marked_cut(scope: &GraphReadScope) -> Option<(i64, u64)> { + match scope.system { + GraphSystemCut::AtOrdinal(ordinal) if scope.cut_marker != 0 => { + Some((ordinal, scope.cut_marker)) + } + GraphSystemCut::AtOrdinal(_) | GraphSystemCut::Live | GraphSystemCut::AllVersions => None, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use nodedb_physical::physical_plan::GraphOp; + + fn scope(system: GraphSystemCut, cut_marker: u64) -> GraphReadScope { + GraphReadScope { + system, + valid_at_ms: None, + cut_marker, + } + } + + #[test] + fn only_a_marked_historical_cut_waits() { + assert_eq!( + marked_cut(&scope(GraphSystemCut::AtOrdinal(9), 4)), + Some((9, 4)) + ); + assert_eq!(marked_cut(&scope(GraphSystemCut::AtOrdinal(9), 0)), None); + assert_eq!(marked_cut(&scope(GraphSystemCut::Live, 4)), None); + assert_eq!(marked_cut(&scope(GraphSystemCut::AllVersions, 4)), None); + } + + #[test] + fn a_marked_neighbors_read_names_its_cut() { + let plan = PhysicalPlan::Graph(GraphOp::Neighbors { + collection: None, + node_id: "a".into(), + edge_labels: Vec::new(), + direction: crate::engine::graph::edge_store::Direction::Out, + rls_filters: Vec::new(), + scope: scope(GraphSystemCut::AtOrdinal(11), 3), + }); + assert_eq!(plan_marked_cuts(&plan), vec![(11, 3)]); + } + + #[test] + fn a_pinned_document_read_names_its_cut() { + let plan = PhysicalPlan::Document(nodedb_physical::physical_plan::DocumentOp::PointGet { + collection: nodedb_types::QualifiedCollection::new( + nodedb_types::DatabaseId::DEFAULT, + "docs", + ), + document_id: "a".into(), + surrogate: None, + pk_bytes: Vec::new(), + rls_filters: Vec::new(), + system_time: nodedb_types::SystemTimeScope::Pinned { + ordinal: 21, + cut_marker: 5, + }, + valid_at_ms: None, + }); + assert_eq!(plan_marked_cuts(&plan), vec![(21, 5)]); + let unmarked = + PhysicalPlan::Document(nodedb_physical::physical_plan::DocumentOp::PointGet { + collection: nodedb_types::QualifiedCollection::new( + nodedb_types::DatabaseId::DEFAULT, + "docs", + ), + document_id: "a".into(), + surrogate: None, + pk_bytes: Vec::new(), + rls_filters: Vec::new(), + system_time: nodedb_types::SystemTimeScope::AsOf(21), + valid_at_ms: None, + }); + assert!(plan_marked_cuts(&unmarked).is_empty()); + } + + /// A pinned array aggregate or elementwise fan-out names its cut, so the + /// coordinator and every shard wait for the marker before they read. + #[test] + fn a_pinned_array_fan_out_names_its_cut() { + use nodedb_physical::physical_plan::{ArrayBinaryOp, ClusterArrayOp}; + let pinned = nodedb_types::SystemTimeScope::Pinned { + ordinal: 33, + cut_marker: 6, + }; + let array = + |name: &str| nodedb_array::types::ArrayId::new(nodedb_types::TenantId::new(1), name); + let agg = PhysicalPlan::ClusterArray(ClusterArrayOp::Agg { + array_id: array("a"), + attr_idx: 0, + reducer_msgpack: Vec::new(), + group_by_dim: -1, + slice_tile_keys: Vec::new(), + prefix_bits: 0, + system_time: pinned, + valid_at_ms: None, + region_msgpack: Vec::new(), + physical: false, + }); + assert_eq!(plan_marked_cuts(&agg), vec![(33, 6)]); + let elementwise = PhysicalPlan::ClusterArray(ClusterArrayOp::Elementwise { + left: array("l"), + right: array("r"), + op: ArrayBinaryOp::Add, + attr_idx: 0, + prefix_bits: 0, + system_time: pinned, + valid_at_ms: None, + }); + assert_eq!(plan_marked_cuts(&elementwise), vec![(33, 6)]); + } +} diff --git a/nodedb/src/control/security/audit/export_sink.rs b/nodedb/src/control/security/audit/export_sink.rs index 93af1d054..132cd9e85 100644 --- a/nodedb/src/control/security/audit/export_sink.rs +++ b/nodedb/src/control/security/audit/export_sink.rs @@ -43,7 +43,7 @@ mod tests { use crate::control::security::siem::SiemConfig; fn configured_siem() -> Arc { - Arc::new(SiemExporter::new(SiemConfig { + Arc::new(SiemExporter::strict_for_test(SiemConfig { destinations: vec!["splunk".into()], ..SiemConfig::default() })) @@ -79,7 +79,7 @@ mod tests { #[test] fn dormant_siem_takes_no_copy() { - let siem = Arc::new(SiemExporter::new(SiemConfig::default())); + let siem = Arc::new(SiemExporter::strict_for_test(SiemConfig::default())); let audit = audit_with(&siem); lock_audit_log(&audit).record(AuditEvent::AdminAction, None, "src", "dormant"); assert!(siem.drain_audit().is_empty()); diff --git a/nodedb/src/control/security/blacklist/ip.rs b/nodedb/src/control/security/blacklist/ip.rs index 2aef94e77..16536815e 100644 --- a/nodedb/src/control/security/blacklist/ip.rs +++ b/nodedb/src/control/security/blacklist/ip.rs @@ -7,64 +7,7 @@ use std::net::{IpAddr, SocketAddr}; -/// A parsed CIDR range for IP blacklist matching. -#[derive(Debug, Clone)] -pub struct CidrRange { - /// Network address. - network: IpAddr, - /// Prefix length (0-32 for IPv4, 0-128 for IPv6). - prefix_len: u8, - /// Pre-computed network mask. - mask: u128, -} - -impl CidrRange { - /// Parse a CIDR string like `"10.0.0.0/8"` or `"192.168.1.100"` (single host). - pub fn parse(s: &str) -> Option { - if let Some((addr_str, prefix_str)) = s.split_once('/') { - let addr: IpAddr = addr_str.parse().ok()?; - let prefix_len: u8 = prefix_str.parse().ok()?; - let max_prefix = if addr.is_ipv4() { 32 } else { 128 }; - if prefix_len > max_prefix { - return None; - } - let total_bits: u32 = if addr.is_ipv4() { 32 } else { 128 }; - let mask = if prefix_len == 0 { - 0 - } else if prefix_len as u32 == total_bits { - u128::MAX - } else { - u128::MAX << (total_bits - prefix_len as u32) - }; - Some(Self { - network: addr, - prefix_len, - mask, - }) - } else { - // Single IP — treat as /32 (IPv4) or /128 (IPv6). - let addr: IpAddr = s.parse().ok()?; - let prefix_len = if addr.is_ipv4() { 32 } else { 128 }; - Some(Self { - network: addr, - prefix_len, - mask: u128::MAX, - }) - } - } - - /// The prefix length (e.g., 24 for /24). - pub fn prefix_len(&self) -> u8 { - self.prefix_len - } - - /// Check if an IP address falls within this CIDR range. - pub fn contains(&self, ip: &IpAddr) -> bool { - let ip_bits = ip_to_u128(ip); - let net_bits = ip_to_u128(&self.network); - (ip_bits & self.mask) == (net_bits & self.mask) - } -} +use crate::control::security::net::Cidr; /// Check if an IP address matches any CIDR entry in a blacklist. /// @@ -73,7 +16,7 @@ impl CidrRange { pub fn check_ip_against_cidrs<'a>(ip_str: &str, entries: &'a [String]) -> Option<&'a str> { let ip: IpAddr = ip_str.parse().ok()?; for entry in entries { - if let Some(cidr) = CidrRange::parse(entry) + if let Some(cidr) = Cidr::parse(entry) && cidr.contains(&ip) { return Some(entry); @@ -107,7 +50,7 @@ pub fn normalize_peer_ip(s: &str) -> Option { /// genuinely ranges pay for iteration. #[derive(Debug)] pub struct CidrSet { - entries: Vec<(String, CidrRange, T)>, + entries: Vec<(String, Cidr, T)>, } impl CidrSet { @@ -119,7 +62,7 @@ impl CidrSet { } /// Insert a new entry, or replace the existing one with the same key. - pub fn upsert(&mut self, key: String, range: CidrRange, value: T) { + pub fn upsert(&mut self, key: String, range: Cidr, value: T) { if let Some(slot) = self.entries.iter_mut().find(|(k, _, _)| *k == key) { slot.1 = range; slot.2 = value; @@ -187,44 +130,10 @@ impl Default for CidrSet { } } -/// Convert an IP address to a u128 for bitwise comparison. -/// -/// IPv4 addresses are placed in the lower 32 bits (not IPv4-mapped IPv6) -/// so that IPv4 prefix masks (shifted by 32 - prefix_len) work correctly. -fn ip_to_u128(ip: &IpAddr) -> u128 { - match ip { - IpAddr::V4(v4) => u128::from(u32::from_be_bytes(v4.octets())), - IpAddr::V6(v6) => u128::from_be_bytes(v6.octets()), - } -} - #[cfg(test)] mod tests { use super::*; - #[test] - fn single_ip_match() { - let cidr = CidrRange::parse("192.168.1.100").unwrap(); - assert!(cidr.contains(&"192.168.1.100".parse().unwrap())); - assert!(!cidr.contains(&"192.168.1.101".parse().unwrap())); - } - - #[test] - fn cidr_range_match() { - let cidr = CidrRange::parse("10.0.0.0/8").unwrap(); - assert!(cidr.contains(&"10.0.0.1".parse().unwrap())); - assert!(cidr.contains(&"10.255.255.255".parse().unwrap())); - assert!(!cidr.contains(&"11.0.0.1".parse().unwrap())); - } - - #[test] - fn cidr_24_match() { - let cidr = CidrRange::parse("192.168.1.0/24").unwrap(); - assert!(cidr.contains(&"192.168.1.1".parse().unwrap())); - assert!(cidr.contains(&"192.168.1.254".parse().unwrap())); - assert!(!cidr.contains(&"192.168.2.1".parse().unwrap())); - } - #[test] fn check_against_list() { let entries = vec!["10.0.0.0/8".into(), "192.168.1.100".into()]; @@ -239,20 +148,6 @@ mod tests { assert_eq!(check_ip_against_cidrs("172.16.0.1", &entries), None); } - #[test] - fn invalid_cidr_returns_none() { - assert!(CidrRange::parse("not-an-ip").is_none()); - assert!(CidrRange::parse("10.0.0.0/33").is_none()); - } - - #[test] - fn ipv6_cidr_range_match() { - let cidr = CidrRange::parse("2001:db8::/32").unwrap(); - assert!(cidr.contains(&"2001:db8::1".parse().unwrap())); - assert!(cidr.contains(&"2001:db8:ffff:ffff::1".parse().unwrap())); - assert!(!cidr.contains(&"2001:db9::1".parse().unwrap())); - } - #[test] fn normalize_peer_ip_strips_port() { assert_eq!( @@ -284,12 +179,12 @@ mod tests { let mut set: CidrSet = CidrSet::new(); set.upsert( "ip:10.0.0.0/8".into(), - CidrRange::parse("10.0.0.0/8").unwrap(), + Cidr::parse("10.0.0.0/8").unwrap(), false, // not expired ); set.upsert( "ip:192.168.0.0/16".into(), - CidrRange::parse("192.168.0.0/16").unwrap(), + Cidr::parse("192.168.0.0/16").unwrap(), true, // expired ); @@ -314,12 +209,12 @@ mod tests { let mut set: CidrSet = CidrSet::new(); set.upsert( "ip:10.0.0.0/8".into(), - CidrRange::parse("10.0.0.0/8").unwrap(), + Cidr::parse("10.0.0.0/8").unwrap(), 1, ); set.upsert( "ip:10.0.0.0/8".into(), - CidrRange::parse("10.0.0.0/8").unwrap(), + Cidr::parse("10.0.0.0/8").unwrap(), 2, ); assert_eq!(set.len(), 1); diff --git a/nodedb/src/control/security/blacklist/store.rs b/nodedb/src/control/security/blacklist/store.rs index 70caa77e3..610fb8448 100644 --- a/nodedb/src/control/security/blacklist/store.rs +++ b/nodedb/src/control/security/blacklist/store.rs @@ -15,8 +15,9 @@ use std::time::{SystemTime, UNIX_EPOCH}; use tracing::{info, warn}; -use super::ip::{CidrRange, CidrSet, normalize_peer_ip}; +use super::ip::{CidrSet, normalize_peer_ip}; use crate::control::security::catalog::{StoredBlacklistEntry, SystemCatalog}; +use crate::control::security::net::Cidr; /// A cached blacklist entry with expiry tracking. #[derive(Debug, Clone)] @@ -102,7 +103,7 @@ impl BlacklistStore { let stored = catalog.load_all_blacklist_entries()?; let mut expired_keys = Vec::new(); let mut loaded_exact = Vec::new(); - let mut loaded_cidr: Vec<(String, CidrRange, BlacklistEntry)> = Vec::new(); + let mut loaded_cidr: Vec<(String, Cidr, BlacklistEntry)> = Vec::new(); for s in &stored { let entry = BlacklistEntry::from_stored(s); @@ -111,7 +112,7 @@ impl BlacklistStore { continue; } match cidr_addr_part(&entry) { - Some(addr) => match CidrRange::parse(addr) { + Some(addr) => match Cidr::parse(addr) { Some(range) => loaded_cidr.push((entry.key.clone(), range, entry)), None => { warn!(key = %entry.key, "skipping unparsable CIDR blacklist entry on load"); @@ -218,7 +219,7 @@ impl BlacklistStore { /// CIDR range matched by [`check_ip`](Self::check_ip) against every /// address in the range. A bare address is validated and stored under /// its canonical `IpAddr` string form so lookups stay exact-match O(1). - /// Both IPv4 and IPv6 are supported by [`CidrRange`]. Returns a + /// Both IPv4 and IPv6 are supported by [`Cidr`]. Returns a /// [`crate::Error::BadRequest`] if `addr` is neither a valid IP address /// nor a valid CIDR range — an entry that can never match must not be /// stored silently. @@ -230,7 +231,7 @@ impl BlacklistStore { expires_at: u64, ) -> crate::Result<()> { if addr.contains('/') { - let range = CidrRange::parse(addr).ok_or_else(|| crate::Error::BadRequest { + let range = Cidr::parse(addr).ok_or_else(|| crate::Error::BadRequest { detail: format!( "invalid CIDR range '{addr}': expected e.g. '10.0.0.0/8' or a valid IPv6 prefix" ), @@ -282,7 +283,7 @@ impl BlacklistStore { fn add_cidr_entry( &self, key: String, - range: CidrRange, + range: Cidr, reason: &str, created_by: &str, expires_at: u64, @@ -419,7 +420,7 @@ impl BlacklistStore { } match cidr_addr_part(&entry) { Some(addr) => { - if let Some(range) = CidrRange::parse(addr) { + if let Some(range) = Cidr::parse(addr) { cidrs.upsert(entry.key.clone(), range, entry); } else { warn!(key = %entry.key, "skipping unparsable CIDR blacklist entry on reload"); @@ -454,7 +455,7 @@ impl Default for BlacklistStore { } /// If `entry` is a CIDR-range IP entry (key `"ip:/"`), return -/// the `/` part to be parsed by [`CidrRange::parse`]. +/// the `/` part to be parsed by [`Cidr::parse`]. /// Returns `None` for exact-match entries (user or single-host IP). fn cidr_addr_part(entry: &BlacklistEntry) -> Option<&str> { if entry.kind != "ip" { diff --git a/nodedb/src/control/security/catalog/audit_dml_index.rs b/nodedb/src/control/security/catalog/audit_dml_index.rs index 2d45835c8..e9e0a2ba8 100644 --- a/nodedb/src/control/security/catalog/audit_dml_index.rs +++ b/nodedb/src/control/security/catalog/audit_dml_index.rs @@ -115,7 +115,8 @@ impl SystemCatalog { continue; }; #[cfg(test)] - self.dml_audit_rows_read + self.faults + .dml_audit_rows_read .fetch_add(1, std::sync::atomic::Ordering::Relaxed); entries.push(decode_entry(value.value())?); } @@ -125,7 +126,8 @@ impl SystemCatalog { /// DML audit rows the position read decoded since the catalog opened. #[cfg(test)] pub(crate) fn dml_audit_rows_read_for_test(&self) -> u64 { - self.dml_audit_rows_read + self.faults + .dml_audit_rows_read .load(std::sync::atomic::Ordering::Relaxed) } } diff --git a/nodedb/src/control/security/catalog/bootstrap_tables.rs b/nodedb/src/control/security/catalog/bootstrap_tables.rs index 72ba44fed..7ec640319 100644 --- a/nodedb/src/control/security/catalog/bootstrap_tables.rs +++ b/nodedb/src/control/security/catalog/bootstrap_tables.rs @@ -82,6 +82,7 @@ pub(super) const BOOTSTRAP_TABLES: &[BootstrapTable] = bootstrap_tables![ "calvin_base" => super::calvin_base::CALVIN_BASE, "calvin_sequencer_install" => super::calvin_base::CALVIN_SEQUENCER_INSTALL, "redo_snapshot_owed" => super::redo_snapshot_owed::REDO_SNAPSHOT_OWED, + "vshard_residency" => super::vshard_residency::VSHARD_RESIDENCY, "l2_cleanup_queue" => L2_CLEANUP_QUEUE, "pending_reclaim" => PENDING_RECLAIM, // ── Metadata-group host state ── @@ -125,6 +126,8 @@ pub(super) const BOOTSTRAP_TABLES: &[BootstrapTable] = bootstrap_tables![ "synonym_groups" => SYNONYM_GROUPS, "custom_types" => CUSTOM_TYPES, "custom_type_oid_hwm" => super::custom_type_oid_hwm::CUSTOM_TYPE_OID_HWM, + "spatial_ref_sys" => SPATIAL_REF_SYS, + "credentials" => super::credentials::CREDENTIALS, "wasm_modules" => WASM_MODULES, "rls_policies" => super::rls::RLS_POLICIES, "redaction_policies" => super::redaction::REDACTION_POLICIES, diff --git a/nodedb/src/control/security/catalog/calvin_barrier_logs.rs b/nodedb/src/control/security/catalog/calvin_barrier_logs.rs index 59b343901..759fdfb4c 100644 --- a/nodedb/src/control/security/catalog/calvin_barrier_logs.rs +++ b/nodedb/src/control/security/catalog/calvin_barrier_logs.rs @@ -117,6 +117,7 @@ impl SystemCatalog { pub fn persist_calvin_barrier_logs(&self) -> crate::Result<()> { #[cfg(test)] if self + .faults .fail_next_calvin_barrier_persist .swap(false, Ordering::SeqCst) { @@ -257,6 +258,7 @@ impl SystemCatalog { } #[cfg(test)] if self + .faults .fail_next_calvin_barrier_replace .swap(false, Ordering::SeqCst) { diff --git a/nodedb/src/control/security/catalog/collection.rs b/nodedb/src/control/security/catalog/collection.rs index 30c5edbc1..982fa29ea 100644 --- a/nodedb/src/control/security/catalog/collection.rs +++ b/nodedb/src/control/security/catalog/collection.rs @@ -151,7 +151,7 @@ pub struct StoredCollection { #[msgpack(default)] pub lvc_enabled: bool, /// Bitemporal storage: every write is appended as an immutable version - /// keyed by `system_from_ms`, enabling `FOR SYSTEM_TIME AS OF` / + /// keyed by its system-time ordinal, enabling `FOR SYSTEM_TIME AS OF` / /// `FOR VALID_TIME` queries. Only honored for document engines today; /// other engines ignore it. #[msgpack(default)] diff --git a/nodedb/src/control/security/catalog/collections.rs b/nodedb/src/control/security/catalog/collections.rs index 76b86f11d..4c6285fb3 100644 --- a/nodedb/src/control/security/catalog/collections.rs +++ b/nodedb/src/control/security/catalog/collections.rs @@ -106,6 +106,7 @@ impl SystemCatalog { ) -> crate::Result<()> { #[cfg(test)] if self + .faults .fail_next_collection_write .swap(false, std::sync::atomic::Ordering::SeqCst) { diff --git a/nodedb/src/control/security/catalog/credentials.rs b/nodedb/src/control/security/catalog/credentials.rs new file mode 100644 index 000000000..4cb699810 --- /dev/null +++ b/nodedb/src/control/security/catalog/credentials.rs @@ -0,0 +1,156 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! `CREATE CREDENTIAL` rows in the system catalog. +//! +//! `_system.credentials` keys on `"{tenant_id}:{name}"` and holds one +//! MessagePack `StoredCredential` per credential. The secret inside is +//! sealed, so the table never holds plaintext. + +use redb::{ReadableDatabase, ReadableTable, TableDefinition}; + +use super::types::{SystemCatalog, catalog_err}; +use crate::control::security::source_credential::StoredCredential; + +/// Table: `"{tenant_id}:{name}"` -> MessagePack `StoredCredential`. +pub(super) const CREDENTIALS: TableDefinition<&str, &[u8]> = + TableDefinition::new("_system.credentials"); + +/// The row key of credential `name` of `tenant_id`. +fn credential_key(tenant_id: u64, name: &str) -> String { + format!("{tenant_id}:{name}") +} + +impl SystemCatalog { + /// Store `credential`, replacing a row of the same name. + pub fn put_credential(&self, credential: &StoredCredential) -> crate::Result<()> { + let key = credential_key(credential.tenant_id, &credential.name); + let bytes = zerompk::to_msgpack_vec(credential) + .map_err(|e| catalog_err("serialize credential", e))?; + let txn = self + .db + .begin_write() + .map_err(|e| catalog_err("credential write txn", e))?; + { + let mut table = txn + .open_table(CREDENTIALS) + .map_err(|e| catalog_err("open credentials", e))?; + table + .insert(key.as_str(), bytes.as_slice()) + .map_err(|e| catalog_err("insert credential", e))?; + } + txn.commit() + .map_err(|e| catalog_err("credential commit", e)) + } + + /// Remove credential `name` of `tenant_id`. Returns whether it existed. + pub fn delete_credential(&self, tenant_id: u64, name: &str) -> crate::Result { + let key = credential_key(tenant_id, name); + let txn = self + .db + .begin_write() + .map_err(|e| catalog_err("credential write txn", e))?; + let removed = { + let mut table = txn + .open_table(CREDENTIALS) + .map_err(|e| catalog_err("open credentials", e))?; + table + .remove(key.as_str()) + .map_err(|e| catalog_err("remove credential", e))? + .is_some() + }; + txn.commit() + .map_err(|e| catalog_err("credential commit", e))?; + Ok(removed) + } + + /// Credential `name` of `tenant_id`. + pub fn get_credential( + &self, + tenant_id: u64, + name: &str, + ) -> crate::Result> { + let key = credential_key(tenant_id, name); + let txn = self + .db + .begin_read() + .map_err(|e| catalog_err("credential read txn", e))?; + let table = txn + .open_table(CREDENTIALS) + .map_err(|e| catalog_err("open credentials", e))?; + let Some(row) = table + .get(key.as_str()) + .map_err(|e| catalog_err("read credential", e))? + else { + return Ok(None); + }; + zerompk::from_msgpack(row.value()) + .map(Some) + .map_err(|e| catalog_err("decode credential", e)) + } + + /// Every credential of `tenant_id`, in name order. + pub fn list_credentials(&self, tenant_id: u64) -> crate::Result> { + let prefix = format!("{tenant_id}:"); + let txn = self + .db + .begin_read() + .map_err(|e| catalog_err("credential read txn", e))?; + let table = txn + .open_table(CREDENTIALS) + .map_err(|e| catalog_err("open credentials", e))?; + let mut out = Vec::new(); + for row in table + .range(prefix.as_str()..) + .map_err(|e| catalog_err("range credentials", e))? + { + let (key, value) = row.map_err(|e| catalog_err("read credential", e))?; + if !key.value().starts_with(&prefix) { + break; + } + let credential: StoredCredential = zerompk::from_msgpack(value.value()) + .map_err(|e| catalog_err("decode credential", e))?; + out.push(credential); + } + Ok(out) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::control::security::source_credential::CredentialKind; + + fn row(tenant_id: u64, name: &str) -> StoredCredential { + StoredCredential { + tenant_id, + name: name.into(), + kind: CredentialKind::Bearer, + sealed: vec![1, 2, 3], + owner: "nodedb".into(), + created_at_ms: 1, + } + } + + #[test] + fn rows_round_trip_per_tenant() { + let dir = tempfile::tempdir().expect("tempdir"); + let catalog = SystemCatalog::open(&dir.path().join("system.redb")).expect("catalog"); + catalog.put_credential(&row(1, "b")).expect("put"); + catalog.put_credential(&row(1, "a")).expect("put"); + catalog.put_credential(&row(12, "c")).expect("put"); + let names: Vec = catalog + .list_credentials(1) + .expect("list") + .into_iter() + .map(|c| c.name) + .collect(); + assert_eq!(names, vec!["a".to_string(), "b".to_string()]); + assert_eq!( + catalog.get_credential(1, "a").expect("get"), + Some(row(1, "a")) + ); + assert!(catalog.delete_credential(1, "a").expect("delete")); + assert!(!catalog.delete_credential(1, "a").expect("delete")); + assert_eq!(catalog.get_credential(1, "a").expect("get"), None); + } +} diff --git a/nodedb/src/control/security/catalog/functions.rs b/nodedb/src/control/security/catalog/functions.rs index 7f1e4bd0f..f73b35d3b 100644 --- a/nodedb/src/control/security/catalog/functions.rs +++ b/nodedb/src/control/security/catalog/functions.rs @@ -123,6 +123,7 @@ impl SystemCatalog { #[cfg(test)] if self + .faults .fail_next_function_wasm_write .swap(false, std::sync::atomic::Ordering::SeqCst) { diff --git a/nodedb/src/control/security/catalog/mod.rs b/nodedb/src/control/security/catalog/mod.rs index b6b6f5b46..5e598951e 100644 --- a/nodedb/src/control/security/catalog/mod.rs +++ b/nodedb/src/control/security/catalog/mod.rs @@ -29,6 +29,7 @@ pub mod consumer_groups; pub mod continuous_aggregate; pub mod continuous_aggregates; pub mod crdt_compaction_points; +pub mod credentials; pub mod custom_type_oid_hwm; pub mod custom_types; pub mod cut_floors; @@ -74,6 +75,8 @@ pub mod scopes; pub mod security; pub mod sequence_types; pub mod sequences; +pub mod spatial_ref_sys; +pub mod srid_refs; pub mod streaming_mvs; pub mod surrogate_hwm; pub mod surrogate_pk; @@ -87,6 +90,8 @@ pub mod tables; pub mod tenant_group_marks; pub mod tenant_id_hwm; pub mod tenant_quotas; +#[cfg(test)] +mod test_faults; pub mod topic_lookup; pub mod topic_messages; pub mod topic_publish_marks; @@ -97,7 +102,9 @@ pub mod types; pub mod users; pub mod vector_index_params; pub mod vector_model; +pub mod vshard_residency; pub mod wal_tombstones; +pub mod wasm_raw; pub use auth_types::{ StoredApiKey, StoredAuthUser, StoredBlacklistEntry, StoredOwner, StoredPermission, StoredRole, @@ -134,6 +141,7 @@ pub use redaction::StoredRedactionPolicy; pub use rls::StoredRlsPolicy; pub use scopes::{StoredScope, StoredScopeGrant}; pub use sequence_types::{SequenceState, StoredSequence}; +pub use spatial_ref_sys::CustomCrsWrite; pub use synonym_groups::StoredSynonymGroup; pub use system_catalog::SystemCatalog; pub use trigger_types::StoredTrigger; diff --git a/nodedb/src/control/security/catalog/read_only.rs b/nodedb/src/control/security/catalog/read_only.rs index a9fe166b4..a18b1d252 100644 --- a/nodedb/src/control/security/catalog/read_only.rs +++ b/nodedb/src/control/security/catalog/read_only.rs @@ -75,6 +75,13 @@ impl ReadOnlySystemCatalog { super::vector_index_params::list_all_vector_index_params_in(&self.read_txn("read txn")?) } + /// The user-defined CRS set the catalog holds. + pub fn load_custom_crs_set(&self) -> crate::Result { + super::spatial_ref_sys::load_custom_crs_set_in( + &self.read_txn("load_custom_crs_set read txn")?, + ) + } + /// See [`SystemCatalog::list_databases`]. pub fn list_databases(&self) -> crate::Result> { super::database::list_databases_in(&self.read_txn("list_databases read txn")?) diff --git a/nodedb/src/control/security/catalog/redo_snapshot_owed.rs b/nodedb/src/control/security/catalog/redo_snapshot_owed.rs index 46a537e0f..622451184 100644 --- a/nodedb/src/control/security/catalog/redo_snapshot_owed.rs +++ b/nodedb/src/control/security/catalog/redo_snapshot_owed.rs @@ -60,6 +60,7 @@ impl SystemCatalog { } #[cfg(test)] if self + .faults .fail_next_redo_snapshot_owed_write .swap(false, std::sync::atomic::Ordering::SeqCst) { diff --git a/nodedb/src/control/security/catalog/replicated_image.rs b/nodedb/src/control/security/catalog/replicated_image.rs index 39311f061..3607f16bf 100644 --- a/nodedb/src/control/security/catalog/replicated_image.rs +++ b/nodedb/src/control/security/catalog/replicated_image.rs @@ -28,6 +28,8 @@ //! carried by data-group snapshots. //! - `redo_snapshot_owed`: the data-group snapshot installs this node's //! replicas owe. +//! - `vshard_residency`: the vShards this node hosts, and the departure +//! purges it owes. //! - `l2_cleanup_queue`, `pending_reclaim`, `pending_history_compaction`: //! work this node still owes its own storage. //! - `surrogate_pk_v3`, `surrogate_pk_rev_v3`: PK bindings of stored rows, @@ -195,6 +197,8 @@ pub(super) const REPLICATED_TABLES: &[ReplicatedTable] = replicated_tables![ "synonym_groups" => SYNONYM_GROUPS, "custom_types" => CUSTOM_TYPES, "custom_type_oid_hwm" => super::custom_type_oid_hwm::CUSTOM_TYPE_OID_HWM, + "spatial_ref_sys" => SPATIAL_REF_SYS, + "credentials" => super::credentials::CREDENTIALS, "wasm_modules" => WASM_MODULES, "rls_policies" => super::rls::RLS_POLICIES, "redaction_policies" => super::redaction::REDACTION_POLICIES, diff --git a/nodedb/src/control/security/catalog/spatial_ref_sys.rs b/nodedb/src/control/security/catalog/spatial_ref_sys.rs new file mode 100644 index 000000000..ff4a690cb --- /dev/null +++ b/nodedb/src/control/security/catalog/spatial_ref_sys.rs @@ -0,0 +1,191 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! User-defined CRSs (`CREATE SPATIAL_REF_SYS`) in the system catalog. +//! +//! `_system.spatial_ref_sys` keys on the SRID code and holds one +//! MessagePack `CustomCrs` per code: a live definition, or the tombstone a +//! `DROP SPATIAL_REF_SYS` leaves. A dropped code is never reused, so stored +//! data that names it fails to resolve instead of changing meaning. +//! +//! The catalog keeps the whole set in memory as an `Arc`. +//! Readers take a share of the current set. A write builds the next set, +//! commits its row, then swaps the share, all under the set's write lock. + +use std::sync::Arc; + +use nodedb_crs::{CrsDef, CustomCrs, CustomCrsSet, CustomCrsState, Srid}; +use redb::{ReadableDatabase, ReadableTable}; + +use super::tables::SPATIAL_REF_SYS; +use super::types::{SystemCatalog, catalog_err}; + +/// What a write did to the user-defined CRS set. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CustomCrsWrite { + /// The row changed. + Written, + /// The set already holds the change, or refuses it. Every node applies + /// the same log, so every node leaves the set alike. The DDL that + /// proposed the entry reads the set back to tell which. + Unchanged, +} + +impl SystemCatalog { + /// A share of the current user-defined CRS set. + pub fn custom_crs_set(&self) -> Arc { + Arc::clone(&self.crs_set.read().unwrap_or_else(|p| p.into_inner())) + } + + /// Define `def` under its SRID. A code that is live or dropped leaves + /// the set unchanged. + pub fn define_custom_crs(&self, def: &CrsDef) -> crate::Result { + let mut current = self.crs_set.write().unwrap_or_else(|p| p.into_inner()); + let mut next = CustomCrsSet::clone(¤t); + if next.define(def.clone()).is_err() { + return Ok(CustomCrsWrite::Unchanged); + } + let row = CustomCrs { + srid: def.srid, + state: CustomCrsState::Live(Box::new(def.clone())), + }; + self.write_custom_crs_row(&row)?; + *current = Arc::new(next); + Ok(CustomCrsWrite::Written) + } + + /// Retire the live CRS `srid`. A code that is not live leaves the set + /// unchanged. + pub fn drop_custom_crs(&self, srid: Srid) -> crate::Result { + let mut current = self.crs_set.write().unwrap_or_else(|p| p.into_inner()); + let mut next = CustomCrsSet::clone(¤t); + if next.drop_crs(srid).is_err() { + return Ok(CustomCrsWrite::Unchanged); + } + let row = CustomCrs { + srid, + state: CustomCrsState::Dropped, + }; + self.write_custom_crs_row(&row)?; + *current = Arc::new(next); + Ok(CustomCrsWrite::Written) + } + + /// Reload the in-memory set from the table, after open or after a + /// metadata image replaced the table. + pub fn reload_custom_crs_set(&self) -> crate::Result<()> { + let read_txn = self + .db + .begin_read() + .map_err(|e| catalog_err("spatial_ref_sys read txn", e))?; + let set = load_custom_crs_set_in(&read_txn)?; + *self.crs_set.write().unwrap_or_else(|p| p.into_inner()) = Arc::new(set); + Ok(()) + } + + fn write_custom_crs_row(&self, row: &CustomCrs) -> crate::Result<()> { + let bytes = zerompk::to_msgpack_vec(row) + .map_err(|e| catalog_err("serialize spatial_ref_sys row", e))?; + let write_txn = self + .db + .begin_write() + .map_err(|e| catalog_err("spatial_ref_sys write txn", e))?; + { + let mut table = write_txn + .open_table(SPATIAL_REF_SYS) + .map_err(|e| catalog_err("open spatial_ref_sys", e))?; + table + .insert(row.srid.code(), bytes.as_slice()) + .map_err(|e| catalog_err("insert spatial_ref_sys row", e))?; + } + write_txn + .commit() + .map_err(|e| catalog_err("commit spatial_ref_sys", e)) + } +} + +/// The user-defined CRS set the table holds in `read_txn`. +pub(super) fn load_custom_crs_set_in( + read_txn: &redb::ReadTransaction, +) -> crate::Result { + let table = read_txn + .open_table(SPATIAL_REF_SYS) + .map_err(|e| catalog_err("open spatial_ref_sys", e))?; + let mut entries = Vec::new(); + for row in table + .iter() + .map_err(|e| catalog_err("iter spatial_ref_sys", e))? + { + let (key, value) = row.map_err(|e| catalog_err("read spatial_ref_sys row", e))?; + let entry: CustomCrs = zerompk::from_msgpack(value.value()) + .map_err(|e| catalog_err("decode spatial_ref_sys row", e))?; + if entry.srid.code() != key.value() { + return Err(catalog_err( + "spatial_ref_sys row", + format!( + "row under key {} holds SRID {}", + key.value(), + entry.srid.code() + ), + )); + } + entries.push(entry); + } + CustomCrsSet::from_entries(entries).map_err(|e| catalog_err("spatial_ref_sys rows", e)) +} + +#[cfg(test)] +mod tests { + use super::*; + use nodedb_crs::parse_proj_string; + + fn def(code: u32) -> CrsDef { + let srid = Srid::new(code).unwrap(); + parse_proj_string(srid, "user tm", "+proj=tmerc +lon_0=101 +ellps=GRS80").unwrap() + } + + #[test] + fn define_drop_and_reload() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("system.redb"); + { + let catalog = SystemCatalog::open(&path).unwrap(); + assert_eq!( + catalog.define_custom_crs(&def(910_010)).unwrap(), + CustomCrsWrite::Written + ); + assert_eq!( + catalog.define_custom_crs(&def(910_010)).unwrap(), + CustomCrsWrite::Unchanged + ); + let srid = Srid::new(910_010).unwrap(); + assert_eq!( + catalog.drop_custom_crs(srid).unwrap(), + CustomCrsWrite::Written + ); + assert_eq!( + catalog.drop_custom_crs(srid).unwrap(), + CustomCrsWrite::Unchanged + ); + assert_eq!( + catalog.define_custom_crs(&def(910_010)).unwrap(), + CustomCrsWrite::Unchanged + ); + catalog.define_custom_crs(&def(910_011)).unwrap(); + assert_eq!( + catalog.define_custom_crs(&def(4326)).unwrap(), + CustomCrsWrite::Unchanged + ); + } + let reopened = SystemCatalog::open(&path).unwrap(); + let set = reopened.custom_crs_set(); + assert_eq!(set.len(), 2); + assert_eq!( + set.get(Srid::new(910_010).unwrap()), + Some(&CustomCrsState::Dropped) + ); + assert!(matches!( + set.get(Srid::new(910_011).unwrap()), + Some(CustomCrsState::Live(_)) + )); + } +} diff --git a/nodedb/src/control/security/catalog/srid_refs/mod.rs b/nodedb/src/control/security/catalog/srid_refs/mod.rs new file mode 100644 index 000000000..0b4599c12 --- /dev/null +++ b/nodedb/src/control/security/catalog/srid_refs/mod.rs @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! The catalog objects whose definitions name a user-defined SRID. + +pub mod scan; +pub mod text; + +pub use text::text_names_srid; diff --git a/nodedb/src/control/security/catalog/srid_refs/scan.rs b/nodedb/src/control/security/catalog/srid_refs/scan.rs new file mode 100644 index 000000000..1480a3d52 --- /dev/null +++ b/nodedb/src/control/security/catalog/srid_refs/scan.rs @@ -0,0 +1,287 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! The catalog scan `DROP SPATIAL_REF_SYS` runs: every catalog object whose +//! definition names a user-defined SRID in an SRID role. +//! +//! Every definition the write path evaluates counts: field and type-guard +//! types, DEFAULT / VALUE / ASSERT expressions, generated columns, type +//! guards, CHECK constraints, transition checks, materialized sums, partial +//! index predicates, events, and an array GEOREF. A number counts only in +//! an SRID role, as `nodedb_query::geo_functions::srid_refs` defines it, so +//! `CHECK (amount < 910001)` does not hold SRID 910001. +//! +//! Stored rows are data, not definitions. A geometry a row stores keeps its +//! code after the drop, and a read of it reports the code as dropped, as +//! PostGIS does for a row whose `spatial_ref_sys` entry is gone. + +use std::borrow::Cow; + +use nodedb_array::schema::ArraySchema; +use nodedb_crs::Srid; +use nodedb_query::expr::SqlExpr; +use nodedb_query::geo_functions::{expr_names_srid, type_names_srid}; +use nodedb_types::columnar::ColumnDef; +use nodedb_types::{CollectionType, DocumentMode}; + +use super::text::text_names_srid; +use crate::control::security::catalog::types::{StoredCollection, SystemCatalog, catalog_err}; + +impl SystemCatalog { + /// Every catalog object whose definition names `srid`, one line each, + /// sorted: `collection '' (tenant , database ): ` or + /// `array '' (tenant , database ): GEOREF`. + pub fn srid_referrers(&self, srid: Srid) -> crate::Result> { + let code = srid.code(); + let mut referrers = Vec::new(); + for coll in self.load_all_collections_across_databases()? { + for part in collection_parts_naming(&coll, code) { + referrers.push(format!( + "collection '{}' (tenant {}, database {}): {part}", + coll.name, + coll.tenant_id, + coll.database_id.as_u64() + )); + } + } + for array in self.load_all_arrays()? { + let schema: ArraySchema = zerompk::from_msgpack(&array.schema_msgpack) + .map_err(|e| catalog_err("decode array schema", e))?; + if schema.georef.as_ref().is_some_and(|g| g.srid == srid) { + referrers.push(format!( + "array '{}' (tenant {}, database {}): GEOREF", + array.name, + array.array_id.tenant_id.as_u64(), + array.array_id.database_id.as_u64() + )); + } + } + referrers.sort(); + referrers.dedup(); + Ok(referrers) + } +} + +/// What one definition part holds. +enum Part<'a> { + /// A type declaration. + Type(&'a str), + /// Expression or statement text. + Text(&'a str), + /// A stored expression tree. + Expr(Cow<'a, SqlExpr>), +} + +impl Part<'_> { + fn names(&self, code: u32) -> bool { + match self { + Part::Type(text) => type_names_srid(text, code), + Part::Text(text) => text_names_srid(text, code), + Part::Expr(expr) => expr_names_srid(expr, code), + } + } +} + +/// The parts of `coll`'s definition that name `code`. +fn collection_parts_naming(coll: &StoredCollection, code: u32) -> Vec { + definition_parts(coll) + .into_iter() + .filter(|(_, part)| part.names(code)) + .map(|(label, _)| label) + .collect() +} + +/// Every evaluated part of `coll`'s definition, labelled for the error. +fn definition_parts(coll: &StoredCollection) -> Vec<(String, Part<'_>)> { + let mut parts = Vec::new(); + for (name, field_type) in &coll.fields { + parts.push((format!("field '{name}' type"), Part::Type(field_type))); + } + for def in &coll.field_defs { + let name = &def.name; + parts.push((format!("field '{name}' type"), Part::Type(&def.field_type))); + parts.push(( + format!("field '{name}' DEFAULT"), + Part::Text(&def.default_expr), + )); + // A generated field stores a serialized tree. Other value text + // parses as an expression. + let value = if def.is_generated { + "generated column" + } else { + "VALUE" + }; + parts.push(( + format!("field '{name}' {value}"), + stored_or_text(&def.value_expr), + )); + parts.push(( + format!("field '{name}' ASSERT"), + Part::Text(&def.assert_expr), + )); + } + for column in strict_columns(&coll.collection_type) { + let name = &column.name; + if let Some(default) = &column.default { + parts.push((format!("column '{name}' DEFAULT"), Part::Text(default))); + } + if let Some(generated) = &column.generated_expr { + parts.push(( + format!("column '{name}' generated column"), + stored_or_text(generated), + )); + } + } + for guard in &coll.type_guards { + let field = &guard.field; + parts.push(( + format!("type guard '{field}' type"), + Part::Type(&guard.type_expr), + )); + for (clause, text) in [ + ("CHECK", &guard.check_expr), + ("DEFAULT", &guard.default_expr), + ("VALUE", &guard.value_expr), + ] { + if let Some(text) = text { + parts.push((format!("type guard '{field}' {clause}"), Part::Text(text))); + } + } + } + for constraint in &coll.check_constraints { + parts.push(( + format!("CHECK constraint '{}'", constraint.name), + Part::Text(&constraint.check_sql), + )); + } + for transition in &coll.transition_checks { + parts.push(( + format!("transition check '{}'", transition.name), + Part::Expr(Cow::Borrowed(&transition.predicate)), + )); + } + for sum in &coll.materialized_sums { + parts.push(( + format!( + "materialized sum into '{}.{}'", + sum.target_collection, sum.target_column + ), + Part::Expr(Cow::Borrowed(&sum.value_expr)), + )); + } + for index in &coll.indexes { + if let Some(predicate) = &index.predicate { + parts.push(( + format!("index '{}' predicate", index.name), + Part::Text(predicate), + )); + } + } + for event in &coll.event_defs { + parts.push(( + format!("event '{}' WHEN", event.name), + Part::Text(&event.when_condition), + )); + parts.push(( + format!("event '{}' THEN", event.name), + Part::Text(&event.then_action), + )); + } + parts +} + +/// A part that holds a serialized expression tree, else expression text. +fn stored_or_text(text: &str) -> Part<'_> { + match sonic_rs::from_str::(text) { + Ok(tree) => Part::Expr(Cow::Owned(tree)), + Err(_) => Part::Text(text), + } +} + +/// The columns of a strict document or key-value schema. +fn strict_columns(collection_type: &CollectionType) -> &[ColumnDef] { + match collection_type { + CollectionType::Document(DocumentMode::Strict(schema)) => &schema.columns, + CollectionType::KeyValue(config) => &config.schema.columns, + CollectionType::Document(DocumentMode::Schemaless) | CollectionType::Columnar(_) => &[], + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::control::security::catalog::collection_constraints::{ + CheckConstraintDef, FieldDefinition, + }; + + const CODE: u32 = 910_042; + + fn field(name: &str, value_expr: &str, generated: bool) -> FieldDefinition { + FieldDefinition { + name: name.into(), + field_type: String::new(), + default_expr: String::new(), + value_expr: value_expr.into(), + assert_expr: String::new(), + readonly: false, + sequence_name: None, + is_generated: generated, + generated_deps: Vec::new(), + } + } + + fn check(name: &str, sql: &str) -> CheckConstraintDef { + CheckConstraintDef { + name: name.into(), + check_sql: sql.into(), + has_subquery: false, + } + } + + #[test] + fn every_srid_role_is_reported() { + let (tree, _) = + nodedb_query::expr_parse::parse_generated_expr("ST_Transform(geom, 910042)") + .expect("parses"); + let mut coll = StoredCollection::new(1, "parcels", "admin"); + coll.fields = vec![("site".into(), "GEOMETRY(Point, 910042)".into())]; + coll.field_defs = vec![ + field( + "geom_local", + &sonic_rs::to_string(&tree).expect("encodes"), + true, + ), + field("label", "'parcel'", false), + ]; + coll.check_constraints = vec![check("in_zone", "ST_SRID(ST_SetSRID(NEW.g, 910042)) > 0")]; + coll.type_guards = vec![nodedb_types::TypeGuardFieldDef { + field: "origin".into(), + type_expr: "GEOMETRY".into(), + required: false, + check_expr: None, + default_expr: Some("'SRID=910042;POINT(0 0)'".into()), + value_expr: None, + }]; + assert_eq!( + collection_parts_naming(&coll, CODE), + vec![ + "field 'site' type".to_string(), + "field 'geom_local' generated column".to_string(), + "type guard 'origin' DEFAULT".to_string(), + "CHECK constraint 'in_zone'".to_string(), + ] + ); + assert!(collection_parts_naming(&coll, 910_043).is_empty()); + } + + /// The code as a plain number holds nothing: the drop proceeds. + #[test] + fn the_same_number_in_another_role_is_not_a_reference() { + let mut coll = StoredCollection::new(1, "ledger", "admin"); + coll.check_constraints = vec![ + check("small", "NEW.amount < 910042"), + check("srid_is", "ST_SRID(NEW.g) = 910042"), + ]; + coll.field_defs = vec![field("limit", "910042", false)]; + assert!(collection_parts_naming(&coll, CODE).is_empty()); + } +} diff --git a/nodedb/src/control/security/catalog/srid_refs/text.rs b/nodedb/src/control/security/catalog/srid_refs/text.rs new file mode 100644 index 000000000..5e42c8795 --- /dev/null +++ b/nodedb/src/control/security/catalog/srid_refs/text.rs @@ -0,0 +1,151 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! Whether definition text names an SRID in an SRID role. +//! +//! The text parses with the expression parser the write path evaluates +//! definitions with. Text that parser does not take, such as a CHECK with +//! a subquery or an event action, parses as SQL statements instead. Both +//! apply the same rules from `nodedb_query::geo_functions::srid_refs`. +//! Text that parses neither way never evaluates, so it names nothing. + +use std::ops::ControlFlow; + +use nodedb_query::geo_functions::{ + expr_names_srid, literal_geometry_srid, literal_srid_code, srid_arg_position, +}; +use nodedb_types::Value; +use sqlparser::ast::{ + Expr, FunctionArg, FunctionArgExpr, FunctionArguments, ObjectNamePart, Statement, + Value as SqlValue, visit_expressions, +}; +use sqlparser::dialect::GenericDialect; +use sqlparser::parser::Parser; + +/// Whether `text` names `code` in an SRID role. +pub fn text_names_srid(text: &str, code: u32) -> bool { + if text.trim().is_empty() { + return false; + } + if let Ok((expr, _)) = nodedb_query::expr_parse::parse_generated_expr(text) { + return expr_names_srid(&expr, code); + } + match statements(text) { + Some(parsed) => visit_expressions(&parsed, |expr| { + if sql_expr_names_srid(expr, code) { + ControlFlow::Break(()) + } else { + ControlFlow::Continue(()) + } + }) + .is_break(), + None => false, + } +} + +/// `text` as statements, else as the expression of a `SELECT`. +fn statements(text: &str) -> Option> { + let dialect = GenericDialect {}; + // reconstructed-sql: parser-only scans stored SQL text for SRID literals without dispatch + if let Ok(statements) = Parser::parse_sql(&dialect, text) { + return Some(statements); + } + let select = format!("SELECT ({text})"); + // reconstructed-sql: parser-only scans a stored SQL expression for SRID literals without dispatch + Parser::parse_sql(&dialect, &select).ok() +} + +/// Whether one SQL expression node names `code`: a geometry literal, or a +/// literal in a geo function's SRID argument. The visitor reaches every +/// nested node. +fn sql_expr_names_srid(expr: &Expr, code: u32) -> bool { + match expr { + Expr::Value(v) => { + literal_value(&v.value).is_some_and(|value| literal_geometry_srid(&value) == Some(code)) + } + Expr::Function(function) => { + let Some(ObjectNamePart::Identifier(name)) = function.name.0.last() else { + return false; + }; + let (Some(at), FunctionArguments::List(list)) = + (srid_arg_position(&name.value), &function.args) + else { + return false; + }; + match list.args.get(at) { + Some(FunctionArg::Unnamed(FunctionArgExpr::Expr(arg))) => { + srid_literal(arg) == Some(i64::from(code)) + } + _ => false, + } + } + _ => false, + } +} + +/// The code an SRID argument holds when it is a literal, through casts. +fn srid_literal(expr: &Expr) -> Option { + match expr { + Expr::Value(v) => literal_value(&v.value).and_then(|value| literal_srid_code(&value)), + Expr::Cast { expr, .. } | Expr::Nested(expr) => srid_literal(expr), + _ => None, + } +} + +/// A number or string literal as a `Value`. +fn literal_value(value: &SqlValue) -> Option { + match value { + SqlValue::Number(n, _) => n + .parse::() + .map(Value::Integer) + .ok() + .or_else(|| n.parse::().ok().map(Value::Float)), + SqlValue::SingleQuotedString(s) | SqlValue::EscapedStringLiteral(s) => { + Some(Value::String(s.clone())) + } + _ => None, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const CODE: u32 = 910_042; + + #[test] + fn expression_text_names_only_srid_roles() { + assert!(text_names_srid("ST_Transform(geom, 910042)", CODE)); + assert!(!text_names_srid("amount < 910042", CODE)); + assert!(!text_names_srid("", CODE)); + } + + #[test] + fn check_text_with_new_and_subqueries_parses_as_sql() { + assert!(text_names_srid( + "ST_SRID(ST_Transform(NEW.loc, 910042)) = 910042", + CODE + )); + assert!(!text_names_srid("NEW.amount < 910042", CODE)); + assert!(text_names_srid( + "EXISTS (SELECT 1 FROM zones z WHERE ST_Intersects(z.g, \ + ST_SetSRID(NEW.loc, 910042)))", + CODE + )); + assert!(!text_names_srid( + "EXISTS (SELECT 1 FROM zones z WHERE z.code = 910042)", + CODE + )); + } + + #[test] + fn statement_text_names_geometry_literals() { + assert!(text_names_srid( + "INSERT INTO log (g) VALUES ('SRID=910042;POINT(1 2)')", + CODE + )); + assert!(!text_names_srid( + "INSERT INTO log (n) VALUES (910042)", + CODE + )); + } +} diff --git a/nodedb/src/control/security/catalog/surrogate_hwm.rs b/nodedb/src/control/security/catalog/surrogate_hwm.rs index a4bb832c0..d2019f521 100644 --- a/nodedb/src/control/security/catalog/surrogate_hwm.rs +++ b/nodedb/src/control/security/catalog/surrogate_hwm.rs @@ -31,6 +31,7 @@ impl SystemCatalog { pub fn put_surrogate_hwm(&self, hwm: u32) -> crate::Result<()> { #[cfg(test)] if self + .faults .fail_next_surrogate_write .swap(false, std::sync::atomic::Ordering::SeqCst) { @@ -64,6 +65,7 @@ impl SystemCatalog { pub fn put_surrogate_reserve_state(&self, hwm: u32, reserve_index: u64) -> crate::Result<()> { #[cfg(test)] if self + .faults .fail_next_surrogate_write .swap(false, std::sync::atomic::Ordering::SeqCst) { diff --git a/nodedb/src/control/security/catalog/system_catalog.rs b/nodedb/src/control/security/catalog/system_catalog.rs index 76ac11a4b..b862e9c3f 100644 --- a/nodedb/src/control/security/catalog/system_catalog.rs +++ b/nodedb/src/control/security/catalog/system_catalog.rs @@ -2,13 +2,12 @@ //! SystemCatalog: redb-backed persistent catalog database. //! -//! Opens or creates the system.redb file, initializes all tables, -//! and provides raw WASM module storage methods. +//! Opens or creates the system.redb file and initializes all tables. use std::path::Path; use std::sync::Arc; -use redb::{Database, ReadableDatabase, ReadableTable}; +use redb::{Database, ReadableDatabase}; use tracing::info; use super::types::*; @@ -24,26 +23,14 @@ pub struct SystemCatalog { /// Committed DEFINE EVENT definitions, for readers that must not read /// redb. pub(super) event_defs: Arc, + /// The user-defined CRS set, loaded at open and swapped on each write. + pub(super) crs_set: Arc>>, /// Whether a barrier log row committed without persisting since the /// last `persist_calvin_barrier_logs`. pub(super) calvin_barrier_logs_pending: Arc, + /// Test-only fault switches and read counters. #[cfg(test)] - pub(super) fail_next_calvin_barrier_persist: Arc, - #[cfg(test)] - pub(super) fail_next_calvin_barrier_replace: Arc, - #[cfg(test)] - pub(super) fail_next_user_counter_write: Arc, - #[cfg(test)] - pub(super) fail_next_function_wasm_write: Arc, - #[cfg(test)] - pub(super) fail_next_collection_write: Arc, - #[cfg(test)] - pub(super) fail_next_surrogate_write: Arc, - #[cfg(test)] - pub(super) fail_next_redo_snapshot_owed_write: Arc, - /// Audit rows the DML position read decoded, so a test can tell a range read from a full scan. - #[cfg(test)] - pub(super) dml_audit_rows_read: Arc, + pub(super) faults: super::test_faults::CatalogTestFaults, } impl crate::storage::RedbBacked for SystemCatalog { @@ -67,31 +54,7 @@ impl SystemCatalog { info!(path = %path.display(), "system catalog opened"); } - let catalog = Self { - db: Arc::new(db), - crdt_signing_root: Arc::new(std::sync::RwLock::new(None)), - event_defs: Arc::new(super::event_defs_index::EventDefsIndex::new()), - calvin_barrier_logs_pending: Arc::new(std::sync::atomic::AtomicBool::new(false)), - #[cfg(test)] - fail_next_calvin_barrier_persist: Arc::new(std::sync::atomic::AtomicBool::new(false)), - #[cfg(test)] - fail_next_calvin_barrier_replace: Arc::new(std::sync::atomic::AtomicBool::new(false)), - #[cfg(test)] - fail_next_user_counter_write: Arc::new(std::sync::atomic::AtomicBool::new(false)), - #[cfg(test)] - fail_next_function_wasm_write: Arc::new(std::sync::atomic::AtomicBool::new(false)), - #[cfg(test)] - fail_next_collection_write: Arc::new(std::sync::atomic::AtomicBool::new(false)), - #[cfg(test)] - fail_next_surrogate_write: Arc::new(std::sync::atomic::AtomicBool::new(false)), - #[cfg(test)] - fail_next_redo_snapshot_owed_write: Arc::new(std::sync::atomic::AtomicBool::new(false)), - #[cfg(test)] - dml_audit_rows_read: Arc::new(std::sync::atomic::AtomicU64::new(0)), - }; - catalog.bootstrap_default_database()?; - catalog.reload_event_definitions()?; - Ok(catalog) + Self::from_bootstrapped(db) } /// Open a non-persistent system catalog backed by an in-memory redb @@ -102,83 +65,29 @@ impl SystemCatalog { .create_with_backend(redb::backends::InMemoryBackend::new()) .map_err(|e| catalog_err("open in-memory", e))?; Self::ensure_bootstrapped(&db)?; + Self::from_bootstrapped(db) + } + + /// The catalog over `db`, whose tables exist: seeds the default database + /// and loads the event definitions and the custom CRS set. + fn from_bootstrapped(db: Database) -> crate::Result { let catalog = Self { db: Arc::new(db), crdt_signing_root: Arc::new(std::sync::RwLock::new(None)), event_defs: Arc::new(super::event_defs_index::EventDefsIndex::new()), + crs_set: Arc::new(std::sync::RwLock::new(Arc::new( + nodedb_crs::CustomCrsSet::new(), + ))), calvin_barrier_logs_pending: Arc::new(std::sync::atomic::AtomicBool::new(false)), #[cfg(test)] - fail_next_calvin_barrier_persist: Arc::new(std::sync::atomic::AtomicBool::new(false)), - #[cfg(test)] - fail_next_calvin_barrier_replace: Arc::new(std::sync::atomic::AtomicBool::new(false)), - #[cfg(test)] - fail_next_user_counter_write: Arc::new(std::sync::atomic::AtomicBool::new(false)), - #[cfg(test)] - fail_next_function_wasm_write: Arc::new(std::sync::atomic::AtomicBool::new(false)), - #[cfg(test)] - fail_next_collection_write: Arc::new(std::sync::atomic::AtomicBool::new(false)), - #[cfg(test)] - fail_next_surrogate_write: Arc::new(std::sync::atomic::AtomicBool::new(false)), - #[cfg(test)] - fail_next_redo_snapshot_owed_write: Arc::new(std::sync::atomic::AtomicBool::new(false)), - #[cfg(test)] - dml_audit_rows_read: Arc::new(std::sync::atomic::AtomicU64::new(0)), + faults: super::test_faults::CatalogTestFaults::default(), }; catalog.bootstrap_default_database()?; catalog.reload_event_definitions()?; + catalog.reload_custom_crs_set()?; Ok(catalog) } - #[cfg(test)] - pub(crate) fn fail_next_user_counter_write_for_test(&self) { - self.fail_next_user_counter_write - .store(true, std::sync::atomic::Ordering::SeqCst); - } - - #[cfg(test)] - pub(crate) fn fail_next_function_wasm_write_for_test(&self) { - self.fail_next_function_wasm_write - .store(true, std::sync::atomic::Ordering::SeqCst); - } - - /// Make the next `put_collection` raise, so an applier's error path runs. - #[cfg(test)] - pub(crate) fn fail_next_collection_write_for_test(&self) { - self.fail_next_collection_write - .store(true, std::sync::atomic::Ordering::SeqCst); - } - - /// Make the next surrogate watermark write raise, so an applier's error - /// path runs. - #[cfg(test)] - pub(crate) fn fail_next_surrogate_write_for_test(&self) { - self.fail_next_surrogate_write - .store(true, std::sync::atomic::Ordering::SeqCst); - } - - /// Make the next `save_redo_snapshot_owed` raise, so a held group's debt - /// write fails once. - #[cfg(test)] - pub(crate) fn fail_next_redo_snapshot_owed_write_for_test(&self) { - self.fail_next_redo_snapshot_owed_write - .store(true, std::sync::atomic::Ordering::SeqCst); - } - - /// Make the next `persist_calvin_barrier_logs` raise. The pending rows - /// stay pending. - #[cfg(test)] - pub(crate) fn fail_next_calvin_barrier_persist_for_test(&self) { - self.fail_next_calvin_barrier_persist - .store(true, std::sync::atomic::Ordering::SeqCst); - } - - /// Make the next `replace_calvin_barrier_logs` raise before it commits. - #[cfg(test)] - pub(crate) fn fail_next_calvin_barrier_replace_for_test(&self) { - self.fail_next_calvin_barrier_replace - .store(true, std::sync::atomic::Ordering::SeqCst); - } - /// Bootstrap every `_system.*` table from the canonical registry — /// but only if at least one is actually missing. Probing read-only /// first keeps `open` byte-idempotent on an already-bootstrapped @@ -212,67 +121,6 @@ impl SystemCatalog { } Ok(needs_bootstrap) } - - /// Execute a write transaction on the WASM_MODULES table. - fn wasm_write(&self, op: &str, f: F) -> crate::Result - where - F: FnOnce(&mut redb::Table<&str, &[u8]>) -> crate::Result, - { - let txn = self - .db - .begin_write() - .map_err(|e| catalog_err(&format!("{op} txn"), e))?; - let result = { - let mut table = txn - .open_table(WASM_MODULES) - .map_err(|e| catalog_err(&format!("{op} open"), e))?; - f(&mut table)? - }; - txn.commit() - .map_err(|e| catalog_err(&format!("{op} commit"), e))?; - Ok(result) - } - - /// Store raw bytes under a string key in the WASM_MODULES table. - pub fn put_raw(&self, key: &[u8], value: &[u8]) -> crate::Result<()> { - let key_str = std::str::from_utf8(key).map_err(|e| catalog_err("put_raw key", e))?; - self.wasm_write("put_raw", |table| { - table - .insert(key_str, value) - .map_err(|e| catalog_err("put_raw insert", e))?; - Ok(()) - }) - } - - /// Load raw bytes by string key from the WASM_MODULES table. - pub fn get_raw(&self, key: &[u8]) -> crate::Result>> { - let key_str = std::str::from_utf8(key).map_err(|e| catalog_err("get_raw key", e))?; - let txn = self - .db - .begin_read() - .map_err(|e| catalog_err("get_raw txn", e))?; - let table = txn - .open_table(WASM_MODULES) - .map_err(|e| catalog_err("get_raw open", e))?; - match table - .get(key_str) - .map_err(|e| catalog_err("get_raw get", e))? - { - Some(v) => Ok(Some(v.value().to_vec())), - None => Ok(None), - } - } - - /// Delete raw bytes by string key from the WASM_MODULES table. - pub fn delete_raw(&self, key: &[u8]) -> crate::Result<()> { - let key_str = std::str::from_utf8(key).map_err(|e| catalog_err("delete_raw key", e))?; - self.wasm_write("delete_raw", |table| { - table - .remove(key_str) - .map_err(|e| catalog_err("delete_raw remove", e))?; - Ok(()) - }) - } } #[cfg(test)] diff --git a/nodedb/src/control/security/catalog/tables.rs b/nodedb/src/control/security/catalog/tables.rs index 7721d3c0c..b7d1e97da 100644 --- a/nodedb/src/control/security/catalog/tables.rs +++ b/nodedb/src/control/security/catalog/tables.rs @@ -240,6 +240,11 @@ pub(super) const SYNONYM_GROUPS: TableDefinition<&str, &[u8]> = pub(super) const CUSTOM_TYPES: TableDefinition<&str, &[u8]> = TableDefinition::new("_system.custom_types"); +/// Table: SRID code -> MessagePack-serialized `CustomCrs`: a user-defined +/// CRS or the tombstone of a dropped one. +pub(super) const SPATIAL_REF_SYS: TableDefinition = + TableDefinition::new("_system.spatial_ref_sys"); + /// Table: "wasm_module:{sha256_hex}" -> raw WASM binary bytes. pub(super) const WASM_MODULES: TableDefinition<&str, &[u8]> = TableDefinition::new("_system.wasm_modules"); diff --git a/nodedb/src/control/security/catalog/test_faults.rs b/nodedb/src/control/security/catalog/test_faults.rs new file mode 100644 index 000000000..ba91e22d9 --- /dev/null +++ b/nodedb/src/control/security/catalog/test_faults.rs @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! Test-only fault switches of the system catalog: each makes the next write +//! of one kind raise, so a caller's error path runs. + +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; + +use super::system_catalog::SystemCatalog; + +/// The fault switches and read counters a test arms on a catalog. Clones of +/// one catalog share them. +#[derive(Clone, Default)] +pub(super) struct CatalogTestFaults { + pub(super) fail_next_calvin_barrier_persist: Arc, + pub(super) fail_next_calvin_barrier_replace: Arc, + pub(super) fail_next_user_counter_write: Arc, + pub(super) fail_next_function_wasm_write: Arc, + pub(super) fail_next_collection_write: Arc, + pub(super) fail_next_surrogate_write: Arc, + pub(super) fail_next_redo_snapshot_owed_write: Arc, + /// Audit rows the DML position read decoded, so a test can tell a range + /// read from a full scan. + pub(super) dml_audit_rows_read: Arc, +} + +impl SystemCatalog { + pub(crate) fn fail_next_user_counter_write_for_test(&self) { + self.faults + .fail_next_user_counter_write + .store(true, Ordering::SeqCst); + } + + pub(crate) fn fail_next_function_wasm_write_for_test(&self) { + self.faults + .fail_next_function_wasm_write + .store(true, Ordering::SeqCst); + } + + /// Make the next `put_collection` raise, so an applier's error path runs. + pub(crate) fn fail_next_collection_write_for_test(&self) { + self.faults + .fail_next_collection_write + .store(true, Ordering::SeqCst); + } + + /// Make the next surrogate watermark write raise, so an applier's error + /// path runs. + pub(crate) fn fail_next_surrogate_write_for_test(&self) { + self.faults + .fail_next_surrogate_write + .store(true, Ordering::SeqCst); + } + + /// Make the next `save_redo_snapshot_owed` raise, so a held group's debt + /// write fails once. + pub(crate) fn fail_next_redo_snapshot_owed_write_for_test(&self) { + self.faults + .fail_next_redo_snapshot_owed_write + .store(true, Ordering::SeqCst); + } + + /// Make the next `persist_calvin_barrier_logs` raise. The pending rows + /// stay pending. + pub(crate) fn fail_next_calvin_barrier_persist_for_test(&self) { + self.faults + .fail_next_calvin_barrier_persist + .store(true, Ordering::SeqCst); + } + + /// Make the next `replace_calvin_barrier_logs` raise before it commits. + pub(crate) fn fail_next_calvin_barrier_replace_for_test(&self) { + self.faults + .fail_next_calvin_barrier_replace + .store(true, Ordering::SeqCst); + } +} diff --git a/nodedb/src/control/security/catalog/users.rs b/nodedb/src/control/security/catalog/users.rs index 2e29db9b7..96ec361c5 100644 --- a/nodedb/src/control/security/catalog/users.rs +++ b/nodedb/src/control/security/catalog/users.rs @@ -99,6 +99,7 @@ impl SystemCatalog { } #[cfg(test)] if self + .faults .fail_next_user_counter_write .swap(false, std::sync::atomic::Ordering::SeqCst) { diff --git a/nodedb/src/control/security/catalog/vshard_residency.rs b/nodedb/src/control/security/catalog/vshard_residency.rs new file mode 100644 index 000000000..182e8d2c9 --- /dev/null +++ b/nodedb/src/control/security/catalog/vshard_residency.rs @@ -0,0 +1,165 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! The vShards this node hosts, and the ones it left whose data it still +//! owes a purge, backing `_system.vshard_residency`. +//! +//! A row is written when a vShard starts being hosted here and changes when +//! it leaves, so a departure that happens while this node is down is found +//! at the next boot. A departed row records the WAL barrier its purge wrote +//! and whether the purge finished. It goes once the WAL no longer holds a +//! record below the barrier: no replay can bring a purged row back then. +//! +//! Table: `vshard_id` -> MessagePack [`VShardResidency`]. + +use redb::{ReadableDatabase, ReadableTable, TableDefinition}; + +use super::types::{SystemCatalog, catalog_err}; + +pub(super) const VSHARD_RESIDENCY: TableDefinition = + TableDefinition::new("_system.vshard_residency"); + +/// Where one vShard stands on this node. +#[derive(Debug, Clone, Copy, PartialEq, Eq, zerompk::ToMessagePack, zerompk::FromMessagePack)] +#[msgpack(map)] +pub struct VShardResidency { + /// Whether the vShard left this node. + pub departed: bool, + /// The WAL position the purge's barrier names: replay skips every record + /// of a purged collection below it. `0` until the barrier is written. + pub barrier_lsn: u64, + /// Whether every core purged the vShard's data. + pub purged: bool, +} + +impl VShardResidency { + /// A vShard this node hosts. + pub const HOSTED: Self = Self { + departed: false, + barrier_lsn: 0, + purged: false, + }; + + /// A vShard that left, with its purge not yet run. + pub const DEPARTED: Self = Self { + departed: true, + barrier_lsn: 0, + purged: false, + }; +} + +/// One change of a row: `Some` writes it, `None` removes it. +pub type ResidencyChange = (u32, Option); + +impl SystemCatalog { + /// Every residency row, in vShard order. + pub fn load_vshard_residency(&self) -> crate::Result> { + let txn = self + .db + .begin_read() + .map_err(|e| catalog_err("vshard_residency read txn", e))?; + let table = txn + .open_table(VSHARD_RESIDENCY) + .map_err(|e| catalog_err("open vshard_residency", e))?; + let mut out = Vec::new(); + for row in table + .iter() + .map_err(|e| catalog_err("iterate vshard_residency", e))? + { + let (vshard_id, value) = row.map_err(|e| catalog_err("read vshard_residency", e))?; + let residency: VShardResidency = + zerompk::from_msgpack(value.value()).map_err(|e| crate::Error::Serialization { + format: "msgpack".into(), + detail: format!("vshard_residency row of vShard {}: {e}", vshard_id.value()), + })?; + out.push((vshard_id.value(), residency)); + } + Ok(out) + } + + /// Apply every change of `changes` in one transaction. + pub fn apply_vshard_residency(&self, changes: &[ResidencyChange]) -> crate::Result<()> { + if changes.is_empty() { + return Ok(()); + } + let txn = self + .db + .begin_write() + .map_err(|e| catalog_err("vshard_residency write txn", e))?; + { + let mut table = txn + .open_table(VSHARD_RESIDENCY) + .map_err(|e| catalog_err("open vshard_residency", e))?; + for (vshard_id, change) in changes { + match change { + Some(residency) => { + let bytes = zerompk::to_msgpack_vec(residency).map_err(|e| { + crate::Error::Serialization { + format: "msgpack".into(), + detail: format!("vshard_residency row of vShard {vshard_id}: {e}"), + } + })?; + table + .insert(*vshard_id, bytes.as_slice()) + .map_err(|e| catalog_err("insert vshard_residency", e))?; + } + None => { + table + .remove(*vshard_id) + .map_err(|e| catalog_err("remove vshard_residency", e))?; + } + } + } + } + txn.commit() + .map_err(|e| catalog_err("vshard_residency commit", e)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn rows_change_and_survive_a_reopen() { + let dir = tempfile::tempdir().expect("tempdir"); + let path = dir.path().join("system.redb"); + { + let catalog = SystemCatalog::open(&path).expect("open"); + catalog + .apply_vshard_residency(&[ + (3, Some(VShardResidency::HOSTED)), + (4, Some(VShardResidency::HOSTED)), + (5, Some(VShardResidency::DEPARTED)), + ]) + .expect("write"); + catalog + .apply_vshard_residency(&[ + (4, None), + ( + 5, + Some(VShardResidency { + departed: true, + barrier_lsn: 9, + purged: true, + }), + ), + ]) + .expect("change"); + } + let catalog = SystemCatalog::open(&path).expect("reopen"); + assert_eq!( + catalog.load_vshard_residency().expect("load"), + vec![ + (3, VShardResidency::HOSTED), + ( + 5, + VShardResidency { + departed: true, + barrier_lsn: 9, + purged: true, + } + ), + ] + ); + } +} diff --git a/nodedb/src/control/security/catalog/wasm_raw.rs b/nodedb/src/control/security/catalog/wasm_raw.rs new file mode 100644 index 000000000..987ee66fa --- /dev/null +++ b/nodedb/src/control/security/catalog/wasm_raw.rs @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! Raw byte storage in the `WASM_MODULES` table, keyed by string. + +use redb::{ReadableDatabase, ReadableTable}; + +use super::types::{SystemCatalog, WASM_MODULES, catalog_err}; + +impl SystemCatalog { + /// Execute a write transaction on the WASM_MODULES table. + fn wasm_write(&self, op: &str, f: F) -> crate::Result + where + F: FnOnce(&mut redb::Table<&str, &[u8]>) -> crate::Result, + { + let txn = self + .db + .begin_write() + .map_err(|e| catalog_err(&format!("{op} txn"), e))?; + let result = { + let mut table = txn + .open_table(WASM_MODULES) + .map_err(|e| catalog_err(&format!("{op} open"), e))?; + f(&mut table)? + }; + txn.commit() + .map_err(|e| catalog_err(&format!("{op} commit"), e))?; + Ok(result) + } + + /// Store raw bytes under a string key in the WASM_MODULES table. + pub fn put_raw(&self, key: &[u8], value: &[u8]) -> crate::Result<()> { + let key_str = std::str::from_utf8(key).map_err(|e| catalog_err("put_raw key", e))?; + self.wasm_write("put_raw", |table| { + table + .insert(key_str, value) + .map_err(|e| catalog_err("put_raw insert", e))?; + Ok(()) + }) + } + + /// Load raw bytes by string key from the WASM_MODULES table. + pub fn get_raw(&self, key: &[u8]) -> crate::Result>> { + let key_str = std::str::from_utf8(key).map_err(|e| catalog_err("get_raw key", e))?; + let txn = self + .db + .begin_read() + .map_err(|e| catalog_err("get_raw txn", e))?; + let table = txn + .open_table(WASM_MODULES) + .map_err(|e| catalog_err("get_raw open", e))?; + match table + .get(key_str) + .map_err(|e| catalog_err("get_raw get", e))? + { + Some(v) => Ok(Some(v.value().to_vec())), + None => Ok(None), + } + } + + /// Delete raw bytes by string key from the WASM_MODULES table. + pub fn delete_raw(&self, key: &[u8]) -> crate::Result<()> { + let key_str = std::str::from_utf8(key).map_err(|e| catalog_err("delete_raw key", e))?; + self.wasm_write("delete_raw", |table| { + table + .remove(key_str) + .map_err(|e| catalog_err("delete_raw remove", e))?; + Ok(()) + }) + } +} diff --git a/nodedb/src/control/security/conditional/parse.rs b/nodedb/src/control/security/conditional/parse.rs index 405340874..b7e37b321 100644 --- a/nodedb/src/control/security/conditional/parse.rs +++ b/nodedb/src/control/security/conditional/parse.rs @@ -25,7 +25,7 @@ //! a condition that failed to parse would leave a grant looking conditional //! while applying unconditionally. -use crate::control::security::blacklist::ip::CidrRange; +use crate::control::security::net::Cidr; use super::condition::{DEFAULT_STEP_UP_SECS, GrantCondition, WEEKDAYS, WEEKENDS}; @@ -208,7 +208,7 @@ fn parse_require_ip(tokens: &[&str], start: usize) -> crate::Result<(GrantCondit if cidr.is_empty() { continue; } - if CidrRange::parse(cidr).is_none() { + if Cidr::parse(cidr).is_none() { return Err(condition_error(format!( "REQUIRE IP IN: '{cidr}' is not a valid IP address or CIDR range" ))); diff --git a/nodedb/src/control/security/egress/client.rs b/nodedb/src/control/security/egress/client.rs new file mode 100644 index 000000000..93bcaa3bd --- /dev/null +++ b/nodedb/src/control/security/egress/client.rs @@ -0,0 +1,195 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! The HTTP client every egress request goes through: the policy-checking +//! resolver, a redirect policy that checks every hop, no system proxy, and +//! bounded timeouts. + +use std::sync::Arc; +use std::time::Duration; + +use super::policy::{EgressError, EgressPolicy}; +use super::resolver::{EgressResolver, HostLookup, SystemLookup}; + +/// Timeouts and the redirect cap of one client. +#[derive(Debug, Clone, Copy)] +pub struct EgressClientOptions { + /// Cap on a whole request. `None` bounds only the connect and each read. + pub timeout: Option, + pub connect_timeout: Duration, + /// Cap on the time between two reads of a response. + pub read_timeout: Duration, + pub max_redirects: usize, +} + +/// Why a redirect was not followed. +#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] +pub enum RedirectError { + #[error("the request exceeded {0} redirects")] + TooMany(usize), + #[error("a redirect hop was refused: {0}")] + Refused(EgressError), +} + +/// A client that reaches only what `policy` allows. Each redirect hop's URL +/// is checked, and each host it resolves passes through the resolver. +pub fn egress_client( + policy: Arc, + lookup: Arc, + options: EgressClientOptions, +) -> Result { + let hop_policy = Arc::clone(&policy); + let max = options.max_redirects; + let mut builder = reqwest::Client::builder() + .no_proxy() + .connect_timeout(options.connect_timeout) + .read_timeout(options.read_timeout) + .dns_resolver(EgressResolver::new(policy, lookup)) + .redirect(reqwest::redirect::Policy::custom(move |attempt| { + if attempt.previous().len() >= max { + return attempt.error(RedirectError::TooMany(max)); + } + match hop_policy.check_parsed(attempt.url()) { + Ok(()) => attempt.follow(), + Err(e) => attempt.error(RedirectError::Refused(e)), + } + })); + if let Some(timeout) = options.timeout { + builder = builder.timeout(timeout); + } + builder.build() +} + +/// The options of the shared outbound client: each request sets its own +/// total timeout, and no redirect is followed. +pub const OUTBOUND_OPTIONS: EgressClientOptions = EgressClientOptions { + timeout: None, + connect_timeout: Duration::from_secs(10), + read_timeout: Duration::from_secs(30), + max_redirects: 0, +}; + +/// The client the server's own outbound requests share: alert webhooks, +/// SIEM export, OTLP export and the key-management client. It resolves +/// through the system resolver under `policy`. The caller checks each URL +/// with [`EgressPolicy::check_url`] before it sends: an IP-literal host is +/// never resolved, so only that check refuses one. +pub fn outbound_client(policy: Arc) -> Result { + egress_client(policy, Arc::new(SystemLookup), OUTBOUND_OPTIONS) +} + +/// What an egress request failed on. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum EgressFault { + /// The policy refused a redirect hop or a resolved address. + Refused(EgressError), + /// The request exceeded its redirect cap. + TooManyRedirects(usize), + /// Any other error: connect, TLS, timeout, status or body read. + Other, +} + +/// The egress fault behind a failed send or body read of an egress client. +pub fn classify(error: &reqwest::Error) -> EgressFault { + match find_cause::(error) { + Some(RedirectError::TooMany(max)) => EgressFault::TooManyRedirects(max), + Some(RedirectError::Refused(refused)) => EgressFault::Refused(refused), + None => match find_cause::(error) { + Some(refused) => EgressFault::Refused(refused), + None => EgressFault::Other, + }, + } +} + +/// The first `T` in the source chain of `error`. A boxed error in the +/// chain exposes its inner error as its source, so the walk reaches it. +fn find_cause(error: &reqwest::Error) -> Option { + let mut source: Option<&(dyn std::error::Error + 'static)> = std::error::Error::source(error); + while let Some(e) = source { + if let Some(found) = e.downcast_ref::() { + return Some(found.clone()); + } + source = e.source(); + } + None +} + +#[cfg(test)] +mod tests { + use tokio::io::{AsyncReadExt, AsyncWriteExt}; + use tokio::net::TcpListener; + + use super::*; + use crate::control::security::egress::resolver::tests::table; + + fn client(policy: EgressPolicy, max_redirects: usize) -> reqwest::Client { + egress_client( + Arc::new(policy), + table(&[("mirror.test", "127.0.0.1"), ("rebind.test", "127.0.0.1")]), + EgressClientOptions { + timeout: Some(Duration::from_secs(5)), + connect_timeout: Duration::from_secs(5), + read_timeout: Duration::from_secs(5), + max_redirects, + }, + ) + .expect("client") + } + + fn mirror_policy() -> EgressPolicy { + EgressPolicy::from_parts(true, &["mirror.test".into()], &["127.0.0.0/8".into()]) + .expect("valid policy") + } + + /// A loopback server that answers every request with a redirect to + /// itself. Returns its port. + async fn redirect_loop() -> u16 { + let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind"); + let port = listener.local_addr().expect("addr").port(); + tokio::spawn(async move { + while let Ok((mut socket, _)) = listener.accept().await { + tokio::spawn(async move { + let mut request = [0u8; 2048]; + let _ = socket.read(&mut request).await; + let answer = format!( + "HTTP/1.1 302 Found\r\nLocation: http://mirror.test:{port}/next\r\n\ + Content-Length: 0\r\nConnection: close\r\n\r\n" + ); + let _ = socket.write_all(answer.as_bytes()).await; + }); + } + }); + port + } + + #[tokio::test] + async fn each_fault_is_classified() { + let port = redirect_loop().await; + let err = client(mirror_policy(), 0) + .get(format!("http://mirror.test:{port}/a")) + .send() + .await + .expect_err("redirect cap"); + assert_eq!(classify(&err), EgressFault::TooManyRedirects(0)); + + let err = client(mirror_policy(), 3) + .get(format!("http://rebind.test:{port}/a")) + .send() + .await + .expect_err("loopback refused"); + assert_eq!( + classify(&err), + EgressFault::Refused(EgressError::PrivateAddress) + ); + + // An allowed host with nothing listening fails at connect. + let closed = TcpListener::bind("127.0.0.1:0").await.expect("bind"); + let closed_port = closed.local_addr().expect("addr").port(); + drop(closed); + let err = client(mirror_policy(), 3) + .get(format!("http://mirror.test:{closed_port}/a")) + .send() + .await + .expect_err("connection refused"); + assert_eq!(classify(&err), EgressFault::Other); + } +} diff --git a/nodedb/src/control/security/egress/global_unicast.rs b/nodedb/src/control/security/egress/global_unicast.rs new file mode 100644 index 000000000..6fdcd0a17 --- /dev/null +++ b/nodedb/src/control/security/egress/global_unicast.rs @@ -0,0 +1,169 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! The global-unicast test an egress check runs on every resolved address. + +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + +/// Whether `ip` is a routable global unicast address: not loopback, +/// private, link-local, shared (CGNAT), documentation, benchmarking, +/// multicast, broadcast, unspecified or reserved. An IPv6 address that +/// embeds an IPv4 address passes only when that IPv4 address passes. +pub fn is_global_unicast(ip: &IpAddr) -> bool { + match ip { + IpAddr::V4(v4) => is_global_v4(v4), + IpAddr::V6(v6) => is_global_v6(v6), + } +} + +fn is_global_v4(v4: &Ipv4Addr) -> bool { + let o = v4.octets(); + !(v4.is_loopback() + || v4.is_private() + || v4.is_link_local() + || v4.is_broadcast() + || v4.is_multicast() + || v4.is_unspecified() + || v4.is_documentation() + || o[0] == 0 + || (o[0] == 100 && (o[1] & 0xC0) == 64) + || (o[0] == 192 && o[1] == 0 && o[2] == 0) + || (o[0] == 198 && (o[1] & 0xFE) == 18) + || o[0] >= 240) +} + +fn is_global_v6(v6: &Ipv6Addr) -> bool { + if let Some(v4) = embedded_v4(v6) { + return is_global_v4(&v4); + } + let seg = v6.segments(); + !(v6.is_multicast() + // Link-local fe80::/10 and site-local fec0::/10. + || (seg[0] & 0xffc0) == 0xfe80 + || (seg[0] & 0xffc0) == 0xfec0 + // Unique local fc00::/7. + || (seg[0] & 0xfe00) == 0xfc00 + // NAT64 local-use 64:ff9b:1::/48: the IPv4 position depends on a + // site-chosen prefix length, and the block is not globally reachable. + || (seg[0] == 0x0064 && seg[1] == 0xff9b && seg[2] == 0x0001) + // Discard-only 100::/64. + || (seg[0] == 0x0100 && seg[1] == 0 && seg[2] == 0 && seg[3] == 0) + // Teredo 2001::/32 tunnels to an obfuscated IPv4 endpoint. + || (seg[0] == 0x2001 && seg[1] == 0) + // Benchmarking 2001:2::/48. + || (seg[0] == 0x2001 && seg[1] == 0x0002 && seg[2] == 0) + // Documentation 2001:db8::/32 and 3fff::/20. + || (seg[0] == 0x2001 && seg[1] == 0x0db8) + || (seg[0] == 0x3fff && (seg[1] & 0xf000) == 0)) +} + +/// The IPv4 address an IPv6 address carries in a form that reaches it: +/// IPv4-mapped `::ffff:0:0/96`, IPv4-compatible `::/96` (the loopback and +/// unspecified addresses among them), NAT64 `64:ff9b::/96` and 6to4 +/// `2002::/16`. +fn embedded_v4(v6: &Ipv6Addr) -> Option { + let seg = v6.segments(); + let low = |hi: u16, lo: u16| Ipv4Addr::from((u32::from(hi) << 16) | u32::from(lo)); + if seg[..5] == [0; 5] && (seg[5] == 0 || seg[5] == 0xffff) { + return Some(low(seg[6], seg[7])); + } + if seg[..6] == [0x0064, 0xff9b, 0, 0, 0, 0] { + return Some(low(seg[6], seg[7])); + } + if seg[0] == 0x2002 { + return Some(low(seg[1], seg[2])); + } + None +} + +#[cfg(test)] +mod tests { + use super::*; + + fn global(s: &str) -> bool { + is_global_unicast(&s.parse().expect("ip")) + } + + #[test] + fn private_loopback_and_link_local_are_not_global() { + for ip in [ + "127.0.0.1", + "10.1.2.3", + "172.16.0.1", + "192.168.1.1", + "169.254.169.254", + "100.64.0.1", + "::1", + "::", + "fe80::1", + "fd00::1", + "ff02::1", + "2001:db8::1", + ] { + assert!(!global(ip), "{ip}"); + } + assert!(global("93.184.216.34")); + assert!(global("2606:2800:220:1:248:1893:25c8:1946")); + } + + #[test] + fn ipv4_mapped_takes_the_ipv4_verdict() { + assert!(!global("::ffff:127.0.0.1")); + assert!(!global("::ffff:169.254.169.254")); + assert!(global("::ffff:93.184.216.34")); + } + + #[test] + fn ipv4_compatible_takes_the_ipv4_verdict() { + assert!(!global("::127.0.0.1")); + assert!(!global("::10.0.0.1")); + assert!(global("::93.184.216.34")); + } + + #[test] + fn nat64_well_known_takes_the_ipv4_verdict() { + assert!(!global("64:ff9b::127.0.0.1")); + assert!(!global("64:ff9b::a9fe:a9fe")); + assert!(global("64:ff9b::93.184.216.34")); + } + + #[test] + fn nat64_local_use_is_not_global() { + assert!(!global("64:ff9b:1::5db8:d822")); + assert!(!global("64:ff9b:1:ffff::1")); + } + + #[test] + fn six_to_four_takes_the_ipv4_verdict() { + // 2002:WWXX:YYZZ:: embeds WW.XX.YY.ZZ. + assert!(!global("2002:7f00:1::1")); + assert!(!global("2002:c0a8:101::")); + assert!(global("2002:5db8:d822::1")); + } + + #[test] + fn teredo_is_not_global() { + assert!(!global("2001:0:4136:e378:8000:63bf:3fff:fdd2")); + assert!(!global("2001::1")); + } + + #[test] + fn site_local_is_not_global() { + assert!(!global("fec0::1")); + assert!(!global("feff::1")); + } + + #[test] + fn discard_only_is_not_global() { + assert!(!global("100::1")); + assert!(!global("100::ffff:ffff:ffff:ffff")); + assert!(global("100:0:0:1::1")); + } + + #[test] + fn benchmarking_and_documentation_are_not_global() { + assert!(!global("2001:2::1")); + assert!(!global("3fff::1")); + assert!(!global("3fff:fff::1")); + assert!(global("2001:4860::8888")); + } +} diff --git a/nodedb/src/control/security/egress/mod.rs b/nodedb/src/control/security/egress/mod.rs new file mode 100644 index 000000000..4c4654235 --- /dev/null +++ b/nodedb/src/control/security/egress/mod.rs @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pub mod client; +pub mod global_unicast; +pub mod policy; +pub mod remote; +pub mod resolver; + +pub use client::{ + EgressClientOptions, EgressFault, OUTBOUND_OPTIONS, RedirectError, classify, egress_client, + outbound_client, +}; +pub use global_unicast::is_global_unicast; +pub use policy::{EgressError, EgressPolicy}; +pub use remote::RemoteSources; +pub use resolver::{EgressResolver, HostLookup, SystemLookup}; diff --git a/nodedb/src/control/security/egress/policy.rs b/nodedb/src/control/security/egress/policy.rs new file mode 100644 index 000000000..4e7792df1 --- /dev/null +++ b/nodedb/src/control/security/egress/policy.rs @@ -0,0 +1,211 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! `EgressPolicy`: which URLs an outbound request of the server may reach. +//! Remote `COPY` sources, JWKS fetches, change-stream and alert webhooks, SIEM +//! export, OTLP export and the Vault key provider share it. +//! +//! The strict policy allows `https://` only, refuses IP-literal hosts, and +//! refuses any resolved address outside global unicast. A relaxed policy +//! names hosts, and every relaxation is tied to a named host: +//! +//! - `allow_http` admits `http://` only for a host in `hosts`. +//! - A resolved non-global address passes only when the host is in `hosts` +//! AND the address lies in one of `cidrs`. +//! - An IP-literal host is never admitted. +//! +//! So a typo or a forgotten flag never opens the whole private address +//! space. Error messages never echo the URL: external input flowing into a +//! log line is its own exfiltration path. + +use std::collections::HashSet; +use std::net::IpAddr; + +use reqwest::Url; + +use super::global_unicast::is_global_unicast; +use crate::control::security::net::Cidr; + +/// Why a URL or an address is refused. +#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] +pub enum EgressError { + #[error("URL is malformed")] + Malformed, + #[error("URL must use the https scheme; http needs the host on the egress allow-list")] + NonHttps, + #[error("URL has no host component")] + NoHost, + #[error("URL must use a DNS name, not an IP literal")] + IpLiteral, + #[error("host resolves to a non-global address the egress allow-list does not cover")] + PrivateAddress, + #[error("invalid CIDR in the egress allow-list: {0}")] + BadCidr(String), +} + +/// The effective egress allow-list. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct EgressPolicy { + /// Allow `http://` for hosts in `hosts`. + pub allow_http: bool, + /// Hostnames (exact, lowercase) that may use `http://` when + /// `allow_http` is set, and may resolve into `cidrs`. + pub hosts: HashSet, + /// Blocks `hosts` may resolve into, beyond global unicast. + pub cidrs: Vec, +} + +impl EgressPolicy { + /// No relaxation: https, DNS names, global unicast addresses. + pub fn strict() -> Self { + Self::default() + } + + /// A policy from configuration strings. A CIDR that does not parse is + /// an error. + pub fn from_parts( + allow_http: bool, + hosts: &[String], + cidrs: &[String], + ) -> Result { + let hosts = hosts.iter().map(|h| h.to_ascii_lowercase()).collect(); + let cidrs = cidrs + .iter() + .map(|s| Cidr::parse(s).ok_or_else(|| EgressError::BadCidr(s.clone()))) + .collect::, _>>()?; + Ok(Self { + allow_http, + hosts, + cidrs, + }) + } + + /// Check a URL string: the first request and every redirect hop. + pub fn check_url(&self, url: &str) -> Result<(), EgressError> { + let parsed = Url::parse(url).map_err(|_| EgressError::Malformed)?; + self.check_parsed(&parsed) + } + + /// Check a parsed URL's scheme and host. + pub fn check_parsed(&self, parsed: &Url) -> Result<(), EgressError> { + let host = match parsed.domain() { + Some(d) if !d.is_empty() => d.to_ascii_lowercase(), + Some(_) => return Err(EgressError::NoHost), + None if parsed.host_str().is_some() => return Err(EgressError::IpLiteral), + None => return Err(EgressError::NoHost), + }; + match parsed.scheme() { + "https" => Ok(()), + "http" if self.allow_http && self.hosts.contains(&host) => Ok(()), + _ => Err(EgressError::NonHttps), + } + } + + /// Check every address `host` resolved to. All must pass, so a name + /// with one public and one private address is refused. + pub fn check_resolved(&self, host: &str, addrs: &[IpAddr]) -> Result<(), EgressError> { + if addrs.is_empty() { + return Err(EgressError::PrivateAddress); + } + let allowlisted = self.hosts.contains(&host.to_ascii_lowercase()); + for ip in addrs { + if is_global_unicast(ip) { + continue; + } + if allowlisted && self.cidrs.iter().any(|c| c.contains(ip)) { + continue; + } + return Err(EgressError::PrivateAddress); + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::net::{Ipv4Addr, Ipv6Addr}; + + fn relaxed(hosts: &[&str], cidrs: &[&str], http: bool) -> EgressPolicy { + let hosts: Vec = hosts.iter().map(|h| (*h).to_string()).collect(); + let cidrs: Vec = cidrs.iter().map(|c| (*c).to_string()).collect(); + EgressPolicy::from_parts(http, &hosts, &cidrs).expect("valid allow-list") + } + + #[test] + fn https_public_dns_passes_strict() { + assert!( + EgressPolicy::strict() + .check_url("https://auth.example.com/.well-known/jwks.json") + .is_ok() + ); + } + + #[test] + fn http_is_refused_unless_the_host_is_allowlisted() { + assert_eq!( + EgressPolicy::strict().check_url("http://example.com/"), + Err(EgressError::NonHttps) + ); + let p = relaxed(&["mirror.internal"], &[], true); + assert!(p.check_url("http://mirror.internal/a.tif").is_ok()); + assert_eq!( + p.check_url("http://evil.example.com/"), + Err(EgressError::NonHttps) + ); + assert_eq!( + p.check_url("ftp://mirror.internal/"), + Err(EgressError::NonHttps) + ); + } + + #[test] + fn ip_literals_are_always_refused() { + let p = relaxed(&["ignored"], &["0.0.0.0/0", "::/0"], true); + for u in [ + "https://127.0.0.1/", + "https://169.254.169.254/latest/meta-data/", + "https://[::1]/", + "http://10.0.0.5/", + ] { + assert_eq!(p.check_url(u), Err(EgressError::IpLiteral), "{u}"); + } + } + + #[test] + fn private_addresses_need_an_allowlisted_host_and_cidr() { + let p = relaxed(&["mirror.internal"], &["10.42.0.0/16"], false); + let ip = |a, b, c, d| IpAddr::V4(Ipv4Addr::new(a, b, c, d)); + assert!( + p.check_resolved("mirror.internal", &[ip(10, 42, 1, 5)]) + .is_ok() + ); + assert!( + p.check_resolved("mirror.internal", &[ip(10, 43, 0, 1)]) + .is_err() + ); + assert!( + p.check_resolved("evil.example.com", &[ip(10, 42, 0, 1)]) + .is_err() + ); + assert!( + p.check_resolved("evil.example.com", &[ip(127, 0, 0, 1)]) + .is_err() + ); + assert!(p.check_resolved("evil.example.com", &[]).is_err()); + let mixed = [ip(93, 184, 216, 34), ip(169, 254, 169, 254)]; + assert!(p.check_resolved("cdn.example.com", &mixed).is_err()); + let v6 = relaxed(&["v6.internal"], &["fd00::/8"], false); + let ula = IpAddr::V6(Ipv6Addr::new(0xfd12, 0, 0, 0, 0, 0, 0, 1)); + let link = IpAddr::V6(Ipv6Addr::new(0xfe80, 0, 0, 0, 0, 0, 0, 1)); + assert!(v6.check_resolved("v6.internal", &[ula]).is_ok()); + assert!(v6.check_resolved("v6.internal", &[link]).is_err()); + } + + #[test] + fn a_bad_cidr_is_an_error_at_build_time() { + assert_eq!( + EgressPolicy::from_parts(false, &[], &["not-a-cidr".into()]), + Err(EgressError::BadCidr("not-a-cidr".into())) + ); + } +} diff --git a/nodedb/src/control/security/egress/remote.rs b/nodedb/src/control/security/egress/remote.rs new file mode 100644 index 000000000..7c91f8c70 --- /dev/null +++ b/nodedb/src/control/security/egress/remote.rs @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! The server's `[egress]` policy, the outbound client built on it, and the +//! key remote `COPY` source credentials are sealed under. + +use std::sync::Arc; + +use super::client::outbound_client; +use super::policy::EgressPolicy; +use crate::control::security::source_credential::CredentialKek; + +/// The `[egress]` policy with the shared outbound client built on it, and +/// the `[credential_encryption]` key. The client and the policy are paired +/// at construction, so no request goes out under another policy. +#[derive(Debug, Clone)] +pub struct RemoteSources { + egress: Arc, + http: Arc, + /// The key `CREATE CREDENTIAL` seals secrets under. `None` refuses + /// `CREATE CREDENTIAL` and every `COPY ... CREDENTIAL`. + pub credential_kek: Option>, +} + +impl RemoteSources { + /// `egress` with its outbound client, and no credential key. + pub fn new(egress: Arc) -> Result { + let http = Arc::new(outbound_client(Arc::clone(&egress))?); + Ok(Self { + egress, + http, + credential_kek: None, + }) + } + + /// What an outbound request may reach. Strict by default. + pub fn egress(&self) -> &Arc { + &self.egress + } + + /// The outbound client built on [`Self::egress`]. Alert webhooks, SIEM + /// export and OTLP export share it. Each sender checks its URL with + /// [`EgressPolicy::check_url`] first. + pub fn http(&self) -> &Arc { + &self.http + } +} diff --git a/nodedb/src/control/security/egress/resolver.rs b/nodedb/src/control/security/egress/resolver.rs new file mode 100644 index 000000000..65218af0a --- /dev/null +++ b/nodedb/src/control/security/egress/resolver.rs @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! `EgressResolver`: the DNS resolver every egress HTTP client connects +//! through. It checks each resolved address against the policy and hands +//! the client exactly the addresses it checked, so the connection goes to +//! a checked address. A name that resolves to a private address between a +//! first check and the connect (DNS rebinding) is refused at the connect, +//! and so is every redirect hop's host. + +use std::future::Future; +use std::net::{IpAddr, SocketAddr}; +use std::pin::Pin; +use std::sync::Arc; + +use reqwest::dns::{Addrs, Name, Resolve, Resolving}; + +use super::policy::EgressPolicy; + +/// The future a host lookup returns. +pub type LookupFuture = Pin>> + Send>>; + +/// Resolves a host name to addresses. Tests replace the system resolver +/// with a fixed table. +pub trait HostLookup: Send + Sync + 'static { + /// The addresses `host` resolves to. + fn lookup(&self, host: &str) -> LookupFuture; +} + +/// The system resolver (`getaddrinfo` on Tokio's blocking pool). +#[derive(Debug, Clone, Copy, Default)] +pub struct SystemLookup; + +impl HostLookup for SystemLookup { + fn lookup(&self, host: &str) -> LookupFuture { + let host = host.to_string(); + Box::pin(async move { + let addrs = tokio::net::lookup_host((host.as_str(), 0)).await?; + Ok(addrs.map(|addr| addr.ip()).collect()) + }) + } +} + +/// A DNS resolver that refuses addresses the policy refuses. +#[derive(Clone)] +pub struct EgressResolver { + policy: Arc, + lookup: Arc, +} + +impl EgressResolver { + /// A resolver that checks `lookup`'s answers against `policy`. + pub fn new(policy: Arc, lookup: Arc) -> Self { + Self { policy, lookup } + } +} + +impl Resolve for EgressResolver { + fn resolve(&self, name: Name) -> Resolving { + let host = name.as_str().to_string(); + let policy = Arc::clone(&self.policy); + let lookup = Arc::clone(&self.lookup); + Box::pin(async move { + let ips = lookup.lookup(&host).await?; + policy.check_resolved(&host, &ips)?; + let addrs: Addrs = Box::new(ips.into_iter().map(|ip| SocketAddr::new(ip, 0))); + Ok(addrs) + }) + } +} + +#[cfg(test)] +pub(crate) mod tests { + use super::*; + use std::collections::HashMap; + + /// A fixed host table in place of DNS. + pub(crate) struct TableLookup(pub HashMap>); + + impl HostLookup for TableLookup { + fn lookup(&self, host: &str) -> LookupFuture { + let answer = self.0.get(host).cloned(); + Box::pin(async move { + answer.ok_or_else(|| { + std::io::Error::new(std::io::ErrorKind::NotFound, "no such host") + }) + }) + } + } + + pub(crate) fn table(entries: &[(&str, &str)]) -> Arc { + let mut map: HashMap> = HashMap::new(); + for (host, ip) in entries { + let ip: IpAddr = ip.parse().expect("ip"); + map.entry((*host).to_string()).or_default().push(ip); + } + Arc::new(TableLookup(map)) + } + + #[tokio::test] + async fn a_name_rebound_to_a_private_address_is_refused_at_resolve() { + let lookup = table(&[ + ("cdn.example.com", "93.184.216.34"), + ("rebind.example.com", "169.254.169.254"), + ]); + let resolver = EgressResolver::new(Arc::new(EgressPolicy::strict()), lookup); + let name: Name = "cdn.example.com".parse().expect("name"); + let addrs: Vec = resolver.resolve(name).await.expect("public").collect(); + assert_eq!(addrs.len(), 1); + let name: Name = "rebind.example.com".parse().expect("name"); + assert!(resolver.resolve(name).await.is_err()); + } +} diff --git a/nodedb/src/control/security/identity/plan_permission.rs b/nodedb/src/control/security/identity/plan_permission.rs index 673de65c3..28ecf64b4 100644 --- a/nodedb/src/control/security/identity/plan_permission.rs +++ b/nodedb/src/control/security/identity/plan_permission.rs @@ -64,7 +64,6 @@ pub fn required_permission(plan: &crate::bridge::envelope::PhysicalPlan) -> Perm | GraphOp::MatchContinuation { .. } | GraphOp::MatchVarLenResume { .. } | GraphOp::TemporalNeighbors { .. } - | GraphOp::TemporalAlgorithm { .. } | GraphOp::BspSuperstep(_) | GraphOp::WccSuperstep(_) | GraphOp::Stats { .. } @@ -281,6 +280,7 @@ pub fn required_permission(plan: &crate::bridge::envelope::PhysicalPlan) -> Perm | MetaOp::Checkpoint | MetaOp::CreateTenantSnapshot { .. } | MetaOp::RestoreTenantSnapshot { .. } + | MetaOp::PurgeDepartedVShards { .. } | MetaOp::UnregisterCollection { .. } | MetaOp::UnregisterMaterializedView { .. } | MetaOp::QueryCollectionSize { .. } @@ -425,6 +425,9 @@ pub fn required_permission(plan: &crate::bridge::envelope::PhysicalPlan) -> Perm PhysicalPlan::Meta(MetaOp::PutSynonymGroup { .. } | MetaOp::DeleteSynonymGroup { .. }) => { Permission::Alter } + + // User-defined CRS set install: Alter permission, as the DDL it carries. + PhysicalPlan::Meta(MetaOp::SetCustomCrsSet { .. }) => Permission::Alter, } } diff --git a/nodedb/src/control/security/jwks/cache.rs b/nodedb/src/control/security/jwks/cache.rs index 1d454d0b4..a8e77204b 100644 --- a/nodedb/src/control/security/jwks/cache.rs +++ b/nodedb/src/control/security/jwks/cache.rs @@ -77,6 +77,17 @@ impl JwksCache { } } + /// Drop every cached key of `provider`: a catalog provider that is + /// dropped, or whose JWKS endpoint changed, keeps no key. + pub fn remove_provider(&self, provider: &str) { + let mut providers = self.providers.write(); + if providers.remove(provider).is_some() + && let Some(ref path) = self.disk_path + { + let _ = self.persist_to_disk(path, &providers); + } + } + /// Check if enough time has passed since last re-fetch attempt. /// Used to rate-limit on-demand re-fetches for unknown `kid`. pub fn can_refetch(&self, provider: &str, min_interval_secs: u64) -> bool { diff --git a/nodedb/src/control/security/jwks/fetch.rs b/nodedb/src/control/security/jwks/fetch.rs index 2db613cf5..f07d672c8 100644 --- a/nodedb/src/control/security/jwks/fetch.rs +++ b/nodedb/src/control/security/jwks/fetch.rs @@ -5,22 +5,31 @@ //! Handles HTTP fetch, JSON deserialization, key extraction, and error //! reporting. Used by the registry for initial load and periodic refresh. //! -//! SSRF defense: every URL (initial + each redirect hop) is re-validated -//! against [`super::url::validate_jwks_url`], and the hostname is resolved -//! up-front with every returned address checked against -//! [`super::url::validate_resolved_addrs`]. A maximum of 3 redirect hops -//! is allowed. +//! SSRF defense: the request goes through the egress client. The URL and +//! every redirect hop pass the [`EgressPolicy`], and every address a host +//! resolves to is checked at connect time, so a rebinding DNS answer never +//! reaches a private address. A maximum of 3 redirect hops is allowed. -use std::net::{SocketAddr, ToSocketAddrs}; +use std::sync::Arc; +use std::time::Duration; use tracing::{debug, info, warn}; use super::cache::JwksCache; use super::key::{JwksResponse, VerificationKey, parse_jwk}; -use super::url::{JwksPolicy, UrlValidationError}; +use crate::control::security::egress::{ + EgressClientOptions, EgressError, EgressFault, EgressPolicy, SystemLookup, classify, + egress_client, +}; const MAX_REDIRECTS: usize = 3; +/// Cap on a JWKS response body. 1 MiB is far beyond any real JWKS document. +const MAX_BODY: usize = 1 << 20; + +/// Cap on one JWKS request, connect and read included. +const FETCH_TIMEOUT: Duration = Duration::from_secs(10); + /// Fetch JWKS from a provider's endpoint and update the cache. /// /// `cache_identity` is an internal generated cache key; `provider_name` is @@ -33,7 +42,7 @@ pub async fn fetch_and_cache( provider_name: &str, jwks_url: &str, cache: &JwksCache, - policy: &JwksPolicy, + policy: &EgressPolicy, ) -> usize { match fetch_jwks(jwks_url, policy).await { Ok(keys) => { @@ -73,81 +82,43 @@ pub async fn fetch_and_cache( /// Fetch and parse JWKS from a URL, with full SSRF defense. async fn fetch_jwks( url: &str, - policy: &JwksPolicy, + policy: &EgressPolicy, ) -> Result, JwksFetchError> { debug!(url = %url, "fetching JWKS"); - // 1. Syntactic validation (scheme + not-IP-literal, respecting policy). + // Scheme and host before any connection, respecting the policy. policy .check_url(url) .map_err(JwksFetchError::UrlValidation)?; - // 2. Resolve host and validate every address against the policy. - let parsed = reqwest::Url::parse(url) - .map_err(|_| JwksFetchError::UrlValidation(UrlValidationError::Malformed))?; - let host = parsed - .host_str() - .ok_or(JwksFetchError::UrlValidation(UrlValidationError::NoHost))? - .to_owned(); - let port = parsed.port_or_known_default().unwrap_or(443); - let addrs = resolve_host(&host, port).await?; - let ips: Vec<_> = addrs.iter().map(|a| a.ip()).collect(); - policy - .check_resolved(&host, &ips) - .map_err(JwksFetchError::UrlValidation)?; - - // Pin the first resolved SocketAddr so reqwest uses the IP we - // validated instead of re-resolving. This closes the DNS-rebinding - // window between our validation and reqwest's own resolution. - let pinned = addrs[0]; - - // 3. Build a reqwest client that enforces per-hop URL check, caps - // redirect depth, and uses the pinned IP for the target host. - let policy_for_redirect = policy.clone(); - let client = reqwest::Client::builder() - .timeout(std::time::Duration::from_secs(10)) - .resolve(&host, pinned) - .redirect(reqwest::redirect::Policy::custom(move |attempt| { - if attempt.previous().len() >= MAX_REDIRECTS { - return attempt.error(redirect_limit_err()); - } - if policy_for_redirect.check_parsed(attempt.url()).is_err() { - return attempt.stop(); - } - attempt.follow() - })) - .build() - .map_err(|_| JwksFetchError::HttpClient)?; + let client = egress_client( + Arc::new(policy.clone()), + Arc::new(SystemLookup), + EgressClientOptions { + timeout: Some(FETCH_TIMEOUT), + connect_timeout: FETCH_TIMEOUT, + read_timeout: FETCH_TIMEOUT, + max_redirects: MAX_REDIRECTS, + }, + ) + .map_err(|_| JwksFetchError::HttpClient)?; let response = client .get(url) .header("accept", "application/json") .send() .await - .map_err(|e| { - // Distinguish our redirect-limit sentinel from generic errors. - if format!("{e}").contains(REDIRECT_LIMIT_SENTINEL) { - JwksFetchError::RedirectLimit - } else { - JwksFetchError::HttpRequest - } + .map_err(|e| match classify(&e) { + EgressFault::TooManyRedirects(_) => JwksFetchError::RedirectLimit, + EgressFault::Refused(refused) => JwksFetchError::UrlValidation(refused), + EgressFault::Other => JwksFetchError::HttpRequest, })?; let status = response.status(); if !status.is_success() { return Err(JwksFetchError::HttpStatus(status.as_u16())); } - - // Cap body size to prevent a hostile endpoint from forcing unbounded - // allocation. 1 MiB is well beyond any real JWKS document. - const MAX_BODY: usize = 1 << 20; - let body = response - .bytes() - .await - .map_err(|_| JwksFetchError::HttpBody)?; - if body.len() > MAX_BODY { - return Err(JwksFetchError::BodyTooLarge); - } + let body = read_bounded(response).await?; let jwks: JwksResponse = crate::util::bounded_json::from_slice(&body).map_err(|_| JwksFetchError::JsonParse)?; @@ -157,21 +128,27 @@ async fn fetch_jwks( Ok(keys) } -/// Resolve a hostname to socket addresses using the blocking resolver on a -/// worker thread (tokio's async resolver isn't available without -/// additional deps and std's resolver is well-exercised for this purpose). -async fn resolve_host(host: &str, port: u16) -> Result, JwksFetchError> { - let h = host.to_owned(); - tokio::task::spawn_blocking(move || (h.as_str(), port).to_socket_addrs().map(|i| i.collect())) +/// The response body, read chunk by chunk. A hostile endpoint that sends +/// more than [`MAX_BODY`] bytes fails before the buffer grows past the cap. +async fn read_bounded(mut response: reqwest::Response) -> Result, JwksFetchError> { + if response + .content_length() + .is_some_and(|len| len > MAX_BODY as u64) + { + return Err(JwksFetchError::BodyTooLarge); + } + let mut body = Vec::new(); + while let Some(chunk) = response + .chunk() .await - .map_err(|_| JwksFetchError::DnsResolution)? - .map_err(|_| JwksFetchError::DnsResolution) -} - -const REDIRECT_LIMIT_SENTINEL: &str = "nodedb-jwks-redirect-limit"; - -fn redirect_limit_err() -> Box { - Box::::from(REDIRECT_LIMIT_SENTINEL) + .map_err(|_| JwksFetchError::HttpBody)? + { + if body.len() + chunk.len() > MAX_BODY { + return Err(JwksFetchError::BodyTooLarge); + } + body.extend_from_slice(&chunk); + } + Ok(body) } /// JWKS fetch errors. Variants carry no external data so they cannot @@ -179,9 +156,7 @@ fn redirect_limit_err() -> Box { #[derive(Debug, thiserror::Error)] pub enum JwksFetchError { #[error("JWKS URL failed validation: {0}")] - UrlValidation(#[from] UrlValidationError), - #[error("DNS resolution failed")] - DnsResolution, + UrlValidation(#[from] EgressError), #[error("HTTP client construction failed")] HttpClient, #[error("HTTP request failed")] @@ -203,7 +178,6 @@ impl JwksFetchError { pub fn category(&self) -> &'static str { match self { Self::UrlValidation(_) => "url_validation", - Self::DnsResolution => "dns_resolution", Self::HttpClient => "http_client", Self::HttpRequest => "http_request", Self::HttpStatus(_) => "http_status", @@ -217,13 +191,15 @@ impl JwksFetchError { /// Start the periodic JWKS refresh task. /// -/// Spawns a Tokio task that refreshes JWKS keys for all providers -/// at the configured interval. Runs on the Control Plane. +/// Spawns a Tokio task that refreshes JWKS keys for every target at the +/// configured interval. Each tick reads `targets` afresh, so a catalog +/// provider registered after the start is refreshed too. Runs on the +/// Control Plane. pub fn spawn_refresh_task( - providers: Vec<(String, String, String)>, // (cache identity, name, jwks_url) triples + targets: Arc>, cache: std::sync::Arc, refresh_interval_secs: u64, - policy: std::sync::Arc, + policy: std::sync::Arc, ) -> tokio::task::JoinHandle<()> { tokio::spawn(async move { let mut interval = @@ -233,9 +209,121 @@ pub fn spawn_refresh_task( loop { interval.tick().await; - for (cache_identity, name, url) in &providers { - fetch_and_cache(cache_identity, name, url, &cache, &policy).await; + let snapshot = targets.read().snapshot(); + for target in &snapshot { + fetch_and_cache( + &target.cache_identity, + &target.provider_name, + &target.jwks_url, + &cache, + &policy, + ) + .await; } } }) } + +#[cfg(test)] +mod tests { + use tokio::io::{AsyncReadExt, AsyncWriteExt}; + use tokio::net::TcpListener; + + use super::*; + + /// Serve `head` and then `body_chunks` as a chunked body on every + /// connection. Returns the port. + async fn serve(head: String, body_chunks: usize, chunk: usize) -> u16 { + let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind"); + let port = listener.local_addr().expect("addr").port(); + tokio::spawn(async move { + while let Ok((mut socket, _)) = listener.accept().await { + let head = head.clone(); + tokio::spawn(async move { + let mut request = [0u8; 4096]; + let _ = socket.read(&mut request).await; + if socket.write_all(head.as_bytes()).await.is_err() { + return; + } + let data = vec![b' '; chunk]; + for _ in 0..body_chunks { + let frame = format!("{chunk:x}\r\n"); + if socket.write_all(frame.as_bytes()).await.is_err() + || socket.write_all(&data).await.is_err() + || socket.write_all(b"\r\n").await.is_err() + { + return; + } + } + let _ = socket.write_all(b"0\r\n\r\n").await; + }); + } + }); + port + } + + fn local_policy() -> EgressPolicy { + EgressPolicy::from_parts( + true, + &["localhost".to_string()], + &["127.0.0.0/8".to_string(), "::1/128".to_string()], + ) + .expect("valid policy") + } + + #[tokio::test] + async fn a_chunked_body_over_the_cap_is_refused() { + let head = "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n\ + Transfer-Encoding: chunked\r\nConnection: close\r\n\r\n" + .to_string(); + // 2 MiB in 64 KiB chunks, with no Content-Length to check up front. + let port = serve(head, 32, 64 * 1024).await; + let url = format!("http://localhost:{port}/jwks.json"); + let err = fetch_jwks(&url, &local_policy()) + .await + .expect_err("over the cap"); + assert!(matches!(err, JwksFetchError::BodyTooLarge), "{err}"); + } + + #[tokio::test] + async fn a_redirect_loop_hits_the_redirect_limit() { + let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind"); + let port = listener.local_addr().expect("addr").port(); + let head = format!( + "HTTP/1.1 302 Found\r\nLocation: http://localhost:{port}/again\r\n\ + Content-Length: 0\r\nConnection: close\r\n\r\n" + ); + tokio::spawn(async move { + while let Ok((mut socket, _)) = listener.accept().await { + let head = head.clone(); + tokio::spawn(async move { + let mut request = [0u8; 4096]; + let _ = socket.read(&mut request).await; + let _ = socket.write_all(head.as_bytes()).await; + }); + } + }); + let url = format!("http://localhost:{port}/jwks.json"); + let err = fetch_jwks(&url, &local_policy()) + .await + .expect_err("redirect loop"); + assert!(matches!(err, JwksFetchError::RedirectLimit), "{err}"); + } + + #[tokio::test] + async fn a_host_outside_the_allow_list_is_refused_at_resolve() { + // https passes the URL check, and the strict policy refuses the + // loopback address localhost resolves to before any connect. + let url = "https://localhost:9/jwks.json"; + let err = fetch_jwks(url, &EgressPolicy::strict()) + .await + .expect_err("loopback"); + assert!( + matches!( + err, + JwksFetchError::UrlValidation(EgressError::PrivateAddress) + ), + "{err}" + ); + } +} diff --git a/nodedb/src/control/security/jwks/mod.rs b/nodedb/src/control/security/jwks/mod.rs index 64e8bbe1c..8aa446f2c 100644 --- a/nodedb/src/control/security/jwks/mod.rs +++ b/nodedb/src/control/security/jwks/mod.rs @@ -4,4 +4,3 @@ pub mod cache; pub mod fetch; pub mod key; pub mod registry; -pub mod url; diff --git a/nodedb/src/control/security/jwks/registry/catalog.rs b/nodedb/src/control/security/jwks/registry/catalog.rs new file mode 100644 index 000000000..5b9b115dd --- /dev/null +++ b/nodedb/src/control/security/jwks/registry/catalog.rs @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! Register and unregister catalog OIDC providers. +//! +//! Every node applies a `PutOidcProvider` / `DeleteOidcProvider` entry, and +//! boot registers every provider the catalog holds. A registered provider +//! joins the periodic refresh, and its keys are fetched once at +//! registration so its first token needs no on-demand fetch. A provider +//! that is dropped, or whose JWKS endpoint changed, loses its cached keys. + +use super::cache_identity::catalog_cache_identity; +use super::state::JwksRegistry; +use super::targets::RefreshTarget; +use crate::control::security::jwks::fetch; + +impl JwksRegistry { + /// Register catalog provider `name` with its JWKS endpoint, replacing a + /// previous registration of the same name. Returns the target whose + /// keys the caller fetches with [`Self::prefetch`]. + pub fn register_catalog_provider(&self, name: &str, jwks_uri: &str) -> RefreshTarget { + let target = RefreshTarget { + cache_identity: catalog_cache_identity(name, jwks_uri), + provider_name: name.to_string(), + jwks_url: jwks_uri.to_string(), + }; + let replaced = self.targets.write().put_catalog(target.clone()); + if let Some(old) = replaced + && old.cache_identity != target.cache_identity + { + self.cache.remove_provider(&old.cache_identity); + } + target + } + + /// Unregister catalog provider `name` and drop its cached keys. + pub fn unregister_catalog_provider(&self, name: &str) { + let removed = self.targets.write().remove_catalog(name); + if let Some(old) = removed { + self.cache.remove_provider(&old.cache_identity); + } + } + + /// Fetch the keys of `target` into the cache. A failed fetch leaves the + /// cache unchanged; the first token with an unknown `kid` fetches again. + pub async fn prefetch(&self, target: &RefreshTarget) -> usize { + fetch::fetch_and_cache( + &target.cache_identity, + &target.provider_name, + &target.jwks_url, + &self.cache, + &self.policy, + ) + .await + } + + /// Whether catalog provider `name` is registered on this node. + pub fn has_catalog_provider(&self, name: &str) -> bool { + self.targets.read().has_catalog(name) + } +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use super::*; + use crate::config::auth::JwtAuthConfig; + use crate::control::security::egress::EgressPolicy; + + #[tokio::test] + async fn a_moved_or_dropped_provider_loses_its_cached_keys() { + let registry = + JwksRegistry::init(JwtAuthConfig::default(), Arc::new(EgressPolicy::strict())) + .await + .expect("a registry with no static provider"); + let first = registry.register_catalog_provider("idp", "https://a.example/jwks"); + registry + .cache + .update_provider(&first.cache_identity, Vec::new()); + assert!(registry.has_catalog_provider("idp")); + + // ALTER-style re-registration under a new endpoint. + let moved = registry.register_catalog_provider("idp", "https://b.example/jwks"); + assert_ne!(moved.cache_identity, first.cache_identity); + assert!(!registry.cache.has_provider(&first.cache_identity)); + registry + .cache + .update_provider(&moved.cache_identity, Vec::new()); + + // The same endpoint keeps its keys. + let same = registry.register_catalog_provider("idp", "https://b.example/jwks"); + assert!(registry.cache.has_provider(&same.cache_identity)); + + registry.unregister_catalog_provider("idp"); + assert!(!registry.has_catalog_provider("idp")); + assert!(!registry.cache.has_provider(&moved.cache_identity)); + } +} diff --git a/nodedb/src/control/security/jwks/registry/init.rs b/nodedb/src/control/security/jwks/registry/init.rs index 4c02423b8..47e29cca4 100644 --- a/nodedb/src/control/security/jwks/registry/init.rs +++ b/nodedb/src/control/security/jwks/registry/init.rs @@ -7,26 +7,26 @@ use std::sync::Arc; use crate::config::auth::JwtAuthConfig; +use crate::control::security::egress::EgressPolicy; use crate::control::security::jwks::cache::JwksCache; use crate::control::security::jwks::fetch; use super::cache_identity::static_cache_identity; use super::state::JwksRegistry; +use super::targets::{RefreshTarget, RefreshTargets}; impl JwksRegistry { /// Create and initialize the registry. /// /// Fetches JWKS from all providers on startup, loads disk cache as fallback, - /// and spawns the periodic refresh task. - pub async fn init(config: JwtAuthConfig) -> crate::Result { + /// and spawns the periodic refresh task. Every fetch goes through + /// `policy`, the server's `[egress]` policy. + pub async fn init(config: JwtAuthConfig, policy: Arc) -> crate::Result { // Registry construction is also a public entry point, so it must not // rely on the server-config loader to reject unsafe static providers. // Validate before creating cache state, fetching remote keys, or // spawning a refresh task. - config.validate()?; - let policy = Arc::new(config.jwks_policy().map_err(|e| crate::Error::Config { - detail: format!("auth.jwt allow-list is invalid: {e}"), - })?); + config.validate(&policy)?; let cache = Arc::new(JwksCache::new(config.jwks_cache_path.clone())); // Load disk cache first (offline fallback). @@ -45,35 +45,34 @@ impl JwksRegistry { .await; } - // Spawn periodic refresh. - let refresh_handle = if !config.providers.is_empty() { - let pairs: Vec<(String, String, String)> = config - .providers - .iter() - .map(|p| { - ( - static_cache_identity(&p.name), - p.name.clone(), - p.jwks_url.clone(), - ) - }) - .collect(); - Some(fetch::spawn_refresh_task( - pairs, - cache.clone(), - config.jwks_refresh_secs, - policy.clone(), - )) - } else { - None - }; + // Spawn periodic refresh. It runs with no static provider too: a + // catalog provider registered later joins its targets. + let statics = config + .providers + .iter() + .map(|p| RefreshTarget { + cache_identity: static_cache_identity(&p.name), + provider_name: p.name.clone(), + jwks_url: p.jwks_url.clone(), + }) + .collect(); + let targets = Arc::new(parking_lot::RwLock::new(RefreshTargets::with_statics( + statics, + ))); + let refresh_handle = fetch::spawn_refresh_task( + Arc::clone(&targets), + cache.clone(), + config.jwks_refresh_secs, + policy.clone(), + ); Ok(Self { providers: config.providers.clone(), cache, config, policy, - _refresh_handle: refresh_handle, + targets, + _refresh_handle: Some(refresh_handle), }) } } diff --git a/nodedb/src/control/security/jwks/registry/mod.rs b/nodedb/src/control/security/jwks/registry/mod.rs index d827f2a38..e7388ff6c 100644 --- a/nodedb/src/control/security/jwks/registry/mod.rs +++ b/nodedb/src/control/security/jwks/registry/mod.rs @@ -21,10 +21,13 @@ //! - `validate` — the three public entry points + the claim-policy gate. //! - `claims` — post-verification claim checks + identity construction. //! - `cache_identity` — collision-free cache keys for static/catalog providers. +//! - `catalog` — register and unregister catalog OIDC providers. +//! - `targets` — the endpoints the periodic refresh task fetches. //! - `header` — JWT header (first token segment) parsing. //! - `verified` — `VerifiedJwtClaims`, the opaque post-verification proof. pub mod cache_identity; +pub mod catalog; pub mod claims; pub mod header; pub mod init; @@ -32,6 +35,7 @@ pub mod pipeline; pub mod refetch; pub mod routing; pub mod state; +pub mod targets; pub mod validate; pub mod verified; diff --git a/nodedb/src/control/security/jwks/registry/state.rs b/nodedb/src/control/security/jwks/registry/state.rs index 39e841e5f..809a7d52a 100644 --- a/nodedb/src/control/security/jwks/registry/state.rs +++ b/nodedb/src/control/security/jwks/registry/state.rs @@ -6,11 +6,12 @@ use std::sync::Arc; use crate::config::auth::{JwtAuthConfig, JwtProviderConfig}; +use crate::control::security::egress::EgressPolicy; use crate::control::security::jwks::cache::JwksCache; -use crate::control::security::jwks::url::JwksPolicy; use crate::control::security::jwt::JwtClaims; use super::header::JwtHeader; +use super::targets::RefreshTargets; /// Multi-provider JWKS registry. /// @@ -20,7 +21,10 @@ pub struct JwksRegistry { pub(super) providers: Vec, pub(super) cache: Arc, pub(super) config: JwtAuthConfig, - pub(super) policy: Arc, + pub(super) policy: Arc, + /// The endpoints the refresh task fetches: static providers and every + /// catalog provider registered on this node. + pub(super) targets: Arc>, /// Background refresh task handle. pub(super) _refresh_handle: Option>, } diff --git a/nodedb/src/control/security/jwks/registry/targets.rs b/nodedb/src/control/security/jwks/registry/targets.rs new file mode 100644 index 000000000..c250e0367 --- /dev/null +++ b/nodedb/src/control/security/jwks/registry/targets.rs @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! The JWKS endpoints the periodic refresh task fetches: every static +//! provider of `[auth.jwt]`, and every catalog OIDC provider registered on +//! this node. + +use std::collections::HashMap; + +/// One JWKS endpoint and the cache identity its keys live under. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RefreshTarget { + pub cache_identity: String, + pub provider_name: String, + pub jwks_url: String, +} + +/// The endpoints the refresh task fetches each tick. +#[derive(Debug, Default)] +pub struct RefreshTargets { + statics: Vec, + /// Catalog providers by name. + catalog: HashMap, +} + +impl RefreshTargets { + /// The static providers of `[auth.jwt]`. + pub fn with_statics(statics: Vec) -> Self { + Self { + statics, + catalog: HashMap::new(), + } + } + + /// Register catalog provider `target`. Returns the target it replaced, + /// whose cached keys the caller drops when its identity differs. + pub fn put_catalog(&mut self, target: RefreshTarget) -> Option { + self.catalog.insert(target.provider_name.clone(), target) + } + + /// Unregister catalog provider `name`. Returns its target. + pub fn remove_catalog(&mut self, name: &str) -> Option { + self.catalog.remove(name) + } + + /// Whether catalog provider `name` is registered. + pub fn has_catalog(&self, name: &str) -> bool { + self.catalog.contains_key(name) + } + + /// Every endpoint to fetch, static providers first. + pub fn snapshot(&self) -> Vec { + self.statics + .iter() + .chain(self.catalog.values()) + .cloned() + .collect() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn target(identity: &str, name: &str) -> RefreshTarget { + RefreshTarget { + cache_identity: identity.into(), + provider_name: name.into(), + jwks_url: format!("https://{name}.example/jwks.json"), + } + } + + #[test] + fn catalog_providers_register_replace_and_unregister() { + let mut targets = RefreshTargets::with_statics(vec![target("static:a", "a")]); + assert_eq!(targets.put_catalog(target("catalog:b1", "b")), None); + let replaced = targets.put_catalog(target("catalog:b2", "b")); + assert_eq!( + replaced.map(|t| t.cache_identity), + Some("catalog:b1".into()) + ); + let names: Vec = targets + .snapshot() + .into_iter() + .map(|t| t.cache_identity) + .collect(); + assert_eq!( + names, + vec!["static:a".to_string(), "catalog:b2".to_string()] + ); + assert!(targets.remove_catalog("b").is_some()); + assert_eq!(targets.snapshot().len(), 1); + } +} diff --git a/nodedb/src/control/security/jwks/registry/validate.rs b/nodedb/src/control/security/jwks/registry/validate.rs index e5f1988d5..dbd1c06b3 100644 --- a/nodedb/src/control/security/jwks/registry/validate.rs +++ b/nodedb/src/control/security/jwks/registry/validate.rs @@ -155,6 +155,7 @@ impl JwksRegistry { mod tests { use super::*; use crate::config::auth::JwtAuthConfig; + use crate::control::security::egress::EgressPolicy; fn claims(iat: u64, exp: u64) -> JwtClaims { JwtClaims { @@ -186,7 +187,7 @@ mod tests { /// A session whose stored claims have since expired must be rejected. #[tokio::test] async fn check_not_expired_rejects_claims_past_exp() { - let registry = JwksRegistry::init(JwtAuthConfig::default()) + let registry = JwksRegistry::init(JwtAuthConfig::default(), strict()) .await .expect("registry with no configured providers must still initialize"); let now = now_secs(); @@ -200,7 +201,7 @@ mod tests { /// path this check now gates. #[tokio::test] async fn check_not_expired_accepts_claims_before_exp() { - let registry = JwksRegistry::init(JwtAuthConfig::default()) + let registry = JwksRegistry::init(JwtAuthConfig::default(), strict()) .await .expect("registry with no configured providers must still initialize"); let now = now_secs(); @@ -215,8 +216,14 @@ mod tests { fn config_from_toml(body: &str) -> JwtAuthConfig { let parsed: JwtAuthConfig = toml::from_str(body).expect("the [auth.jwt] section must deserialize"); - parsed.validate().expect("the section must be valid"); parsed + .validate(&EgressPolicy::strict()) + .expect("the section must be valid"); + parsed + } + + fn strict() -> std::sync::Arc { + std::sync::Arc::new(EgressPolicy::strict()) } fn claims_with_extra(pairs: &[(&str, serde_json::Value)]) -> JwtClaims { @@ -232,12 +239,15 @@ mod tests { /// must actually reject a token carrying a blocked value. #[tokio::test] async fn blocked_status_from_server_config_rejects_the_token() { - let registry = JwksRegistry::init(config_from_toml( - r#" + let registry = JwksRegistry::init( + config_from_toml( + r#" status_claim = "account_status" blocked_statuses = ["suspended", "banned"] "#, - )) + ), + strict(), + ) .await .expect("registry with no configured providers must still initialize"); @@ -259,12 +269,15 @@ mod tests { /// provider-named claim onto the field NodeDB reads. #[tokio::test] async fn claim_remap_from_server_config_reaches_the_verification_pipeline() { - let registry = JwksRegistry::init(config_from_toml( - r#" + let registry = JwksRegistry::init( + config_from_toml( + r#" [claims] upn = "email" "#, - )) + ), + strict(), + ) .await .expect("registry with no configured providers must still initialize"); @@ -280,15 +293,18 @@ mod tests { /// provider claim after renaming it onto `status`. #[tokio::test] async fn remap_feeds_the_status_check() { - let registry = JwksRegistry::init(config_from_toml( - r#" + let registry = JwksRegistry::init( + config_from_toml( + r#" status_claim = "status" blocked_statuses = ["deactivated"] [claims] acct_state = "status" "#, - )) + ), + strict(), + ) .await .expect("registry with no configured providers must still initialize"); @@ -304,10 +320,13 @@ mod tests { /// authentication — not a hand-rolled or zero tolerance. #[tokio::test] async fn check_not_expired_honors_configured_clock_skew() { - let registry = JwksRegistry::init(JwtAuthConfig { - clock_skew_secs: 120, - ..JwtAuthConfig::default() - }) + let registry = JwksRegistry::init( + JwtAuthConfig { + clock_skew_secs: 120, + ..JwtAuthConfig::default() + }, + strict(), + ) .await .expect("registry with no configured providers must still initialize"); let now = now_secs(); diff --git a/nodedb/src/control/security/jwks/url.rs b/nodedb/src/control/security/jwks/url.rs deleted file mode 100644 index 66b886321..000000000 --- a/nodedb/src/control/security/jwks/url.rs +++ /dev/null @@ -1,376 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 - -//! JWKS URL validation: SSRF defense for the JWKS fetch path. -//! -//! A [`JwksPolicy`] captures the effective allow-list for a running -//! process. The default (`strict`) policy forbids `http://`, IP-literal -//! hosts, and any resolved address outside global unicast — the correct -//! default for public-IdP deployments. -//! -//! For self-hosted IdPs (in-cluster Keycloak, internal Auth0 on a VPC), -//! a relaxed policy can be built from config: -//! -//! ```toml -//! [auth.jwt] -//! allow_http_jwks = true -//! allow_jwks_hosts = ["keycloak.internal"] -//! allow_jwks_cidrs = ["10.42.0.0/16"] -//! ``` -//! -//! Relaxations are **scoped**: -//! -//! - `allow_http_jwks` only applies to URLs whose host is in `hosts`. -//! - A resolved private IP is only accepted if the host is in `hosts` -//! AND the IP falls in one of the `cidrs`. -//! - IP-literal URLs are never permitted. -//! -//! This keeps the invariant — every relaxation is tied to a named host — -//! so a typo or forgotten flag can't open the whole private IP space. -//! -//! Error messages do not echo the URL back; external input flowing into -//! log lines is its own exfil surface. - -use std::collections::HashSet; -use std::net::IpAddr; - -use reqwest::Url; - -/// URL validation errors for JWKS endpoints. -#[derive(Debug, thiserror::Error)] -pub enum UrlValidationError { - #[error("JWKS URL is malformed")] - Malformed, - #[error("JWKS URL must use https scheme")] - NonHttps, - #[error("JWKS URL has no host component")] - NoHost, - #[error("JWKS URL must use a DNS name, not an IP literal")] - IpLiteral, - #[error("JWKS URL resolves to a non-global address not covered by allow_jwks_cidrs")] - PrivateAddress, - #[error("invalid CIDR in allow_jwks_cidrs: {0}")] - BadCidr(String), -} - -/// Effective JWKS allow-list policy. -#[derive(Debug, Clone, Default)] -pub struct JwksPolicy { - /// Allow `http://` scheme for hosts in `hosts`. - pub allow_http: bool, - /// Hostnames (exact, lowercase) that may resolve to private addresses - /// covered by `cidrs` and, if `allow_http`, may use http scheme. - pub hosts: HashSet, - /// CIDR blocks that `hosts` are allowed to resolve into, in addition - /// to global unicast. - pub cidrs: Vec, -} - -impl JwksPolicy { - /// Strict default: no relaxations. Correct for public-IdP deployments. - pub fn strict() -> Self { - Self::default() - } - - /// Build a policy from parsed strings. Returns an error if any CIDR - /// fails to parse. - pub fn from_parts( - allow_http: bool, - hosts: &[String], - cidrs: &[String], - ) -> Result { - let hosts = hosts.iter().map(|h| h.to_ascii_lowercase()).collect(); - let cidrs = cidrs - .iter() - .map(|s| Cidr::parse(s).ok_or_else(|| UrlValidationError::BadCidr(s.clone()))) - .collect::, _>>()?; - Ok(Self { - allow_http, - hosts, - cidrs, - }) - } - - /// Validate a URL string (used initially and for each redirect hop). - pub fn check_url(&self, url: &str) -> Result<(), UrlValidationError> { - let parsed = Url::parse(url).map_err(|_| UrlValidationError::Malformed)?; - self.check_parsed(&parsed) - } - - /// Validate an already-parsed URL. - pub fn check_parsed(&self, parsed: &Url) -> Result<(), UrlValidationError> { - let host = match parsed.domain() { - Some(d) if !d.is_empty() => d.to_ascii_lowercase(), - Some(_) => return Err(UrlValidationError::NoHost), - None if parsed.host_str().is_some() => return Err(UrlValidationError::IpLiteral), - None => return Err(UrlValidationError::NoHost), - }; - match parsed.scheme() { - "https" => Ok(()), - "http" if self.allow_http && self.hosts.contains(&host) => Ok(()), - _ => Err(UrlValidationError::NonHttps), - } - } - - /// Validate every resolved IP for a hostname. ALL addresses must pass - /// (AAAA/A split defense). - pub fn check_resolved(&self, host: &str, addrs: &[IpAddr]) -> Result<(), UrlValidationError> { - if addrs.is_empty() { - return Err(UrlValidationError::PrivateAddress); - } - let host_lc = host.to_ascii_lowercase(); - let host_allowlisted = self.hosts.contains(&host_lc); - for ip in addrs { - if is_global_unicast(ip) { - continue; - } - if host_allowlisted && self.cidrs.iter().any(|c| c.contains(ip)) { - continue; - } - return Err(UrlValidationError::PrivateAddress); - } - Ok(()) - } -} - -/// Back-compat helper: strict URL validation with no allow-list. -pub fn validate_jwks_url(url: &str) -> Result<(), UrlValidationError> { - JwksPolicy::strict().check_url(url) -} - -/// Back-compat helper: strict resolved-addr validation. -pub fn validate_resolved_addrs(addrs: &[IpAddr]) -> Result<(), UrlValidationError> { - JwksPolicy::strict().check_resolved("", addrs) -} - -/// Back-compat helper: strict parsed-URL validation (used by the redirect -/// policy when no policy-aware closure is in scope). -pub fn validate_parsed_url(parsed: &Url) -> Result<(), UrlValidationError> { - JwksPolicy::strict().check_parsed(parsed) -} - -/// Is this IP a routable, global unicast address? -fn is_global_unicast(ip: &IpAddr) -> bool { - match ip { - IpAddr::V4(v4) => { - !(v4.is_loopback() - || v4.is_private() - || v4.is_link_local() - || v4.is_broadcast() - || v4.is_multicast() - || v4.is_unspecified() - || v4.is_documentation() - || v4.octets()[0] == 0 - || (v4.octets()[0] == 100 && (v4.octets()[1] & 0xC0) == 64) - || (v4.octets()[0] == 169 && v4.octets()[1] == 254) - || (v4.octets()[0] == 192 && v4.octets()[1] == 0 && v4.octets()[2] == 0) - || (v4.octets()[0] == 198 && (v4.octets()[1] & 0xFE) == 18) - || v4.octets()[0] >= 240) - } - IpAddr::V6(v6) => { - let seg = v6.segments(); - !(v6.is_loopback() - || v6.is_multicast() - || v6.is_unspecified() - || (seg[0] & 0xffc0) == 0xfe80 - || (seg[0] & 0xfe00) == 0xfc00 - || v6 - .to_ipv4_mapped() - .is_some_and(|v4| !is_global_unicast(&IpAddr::V4(v4))) - || (seg[0] == 0x2001 && seg[1] == 0x0db8)) - } - } -} - -// ── Minimal CIDR type (v4 + v6) ───────────────────────────────────────── - -/// A small CIDR block with `contains` support for IPv4 and IPv6. -#[derive(Debug, Clone, Copy)] -pub struct Cidr { - base: u128, - mask: u128, - is_v4: bool, -} - -impl Cidr { - /// Parse a CIDR like `"10.42.0.0/16"` or `"fd00::/8"`. Returns `None` - /// if the input doesn't match `IP/prefix` with a valid prefix width. - pub fn parse(s: &str) -> Option { - let (ip_str, prefix_str) = s.split_once('/')?; - let prefix: u8 = prefix_str.parse().ok()?; - let ip: IpAddr = ip_str.parse().ok()?; - let (bits, is_v4) = match ip { - IpAddr::V4(v4) => (u32::from(v4) as u128, true), - IpAddr::V6(v6) => (u128::from(v6), false), - }; - let width: u8 = if is_v4 { 32 } else { 128 }; - if prefix > width { - return None; - } - let mask = build_mask(prefix, is_v4); - Some(Self { - base: bits & mask, - mask, - is_v4, - }) - } - - /// Does this CIDR contain `ip`? - pub fn contains(&self, ip: &IpAddr) -> bool { - match (self.is_v4, ip) { - (true, IpAddr::V4(v4)) => (u32::from(*v4) as u128) & self.mask == self.base, - (false, IpAddr::V6(v6)) => u128::from(*v6) & self.mask == self.base, - _ => false, - } - } -} - -fn build_mask(prefix: u8, is_v4: bool) -> u128 { - let width: u8 = if is_v4 { 32 } else { 128 }; - let prefix = prefix.min(width); - if prefix == 0 { - 0 - } else { - let shift = width - prefix; - let full: u128 = if is_v4 { u32::MAX as u128 } else { u128::MAX }; - (full >> shift) << shift & full - } -} - -#[cfg(test)] -mod tests { - use super::*; - use std::net::{Ipv4Addr, Ipv6Addr}; - - #[test] - fn https_public_dns_passes_strict() { - validate_jwks_url("https://auth.example.com/.well-known/jwks.json").unwrap(); - } - - #[test] - fn http_scheme_rejected_strict() { - assert!(matches!( - validate_jwks_url("http://example.com/"), - Err(UrlValidationError::NonHttps) - )); - } - - #[test] - fn ip_literal_always_rejected() { - let relaxed = JwksPolicy::from_parts( - true, - &["ignored".into()], - &["0.0.0.0/0".into(), "::/0".into()], - ) - .unwrap(); - for u in [ - "https://127.0.0.1/", - "https://169.254.169.254/", - "https://[::1]/", - ] { - assert!(matches!( - relaxed.check_url(u), - Err(UrlValidationError::IpLiteral) - )); - } - } - - #[test] - fn http_allowed_only_for_allowlisted_host() { - let p = JwksPolicy::from_parts(true, &["keycloak.internal".into()], &[]).unwrap(); - p.check_url("http://keycloak.internal/jwks.json").unwrap(); - assert!(p.check_url("http://evil.example.com/").is_err()); - } - - #[test] - fn private_addrs_rejected_unless_host_and_cidr_match() { - let p = JwksPolicy::from_parts( - false, - &["keycloak.internal".into()], - &["10.42.0.0/16".into()], - ) - .unwrap(); - // Allowed host + allowed CIDR. - p.check_resolved( - "keycloak.internal", - &[IpAddr::V4(Ipv4Addr::new(10, 42, 1, 5))], - ) - .unwrap(); - // Allowed host but CIDR outside list. - assert!( - p.check_resolved( - "keycloak.internal", - &[IpAddr::V4(Ipv4Addr::new(10, 43, 0, 1))] - ) - .is_err() - ); - // Non-allowlisted host — private IP refused. - assert!( - p.check_resolved( - "evil.example.com", - &[IpAddr::V4(Ipv4Addr::new(10, 42, 0, 1))] - ) - .is_err() - ); - } - - #[test] - fn mixed_addrs_fail_if_any_outside_allowed_set() { - let p = - JwksPolicy::from_parts(false, &["h.internal".into()], &["10.0.0.0/8".into()]).unwrap(); - // One public + one private-but-allowed: OK. - p.check_resolved( - "h.internal", - &[ - IpAddr::V4(Ipv4Addr::new(93, 184, 216, 34)), - IpAddr::V4(Ipv4Addr::new(10, 1, 2, 3)), - ], - ) - .unwrap(); - // One public + one loopback (not in cidr): fails. - assert!( - p.check_resolved( - "h.internal", - &[ - IpAddr::V4(Ipv4Addr::new(93, 184, 216, 34)), - IpAddr::V4(Ipv4Addr::LOCALHOST), - ], - ) - .is_err() - ); - } - - #[test] - fn v6_unique_local_with_cidr() { - let p = - JwksPolicy::from_parts(false, &["v6.internal".into()], &["fd00::/8".into()]).unwrap(); - p.check_resolved( - "v6.internal", - &[IpAddr::V6(Ipv6Addr::new(0xfd12, 0, 0, 0, 0, 0, 0, 1))], - ) - .unwrap(); - // Link-local outside the CIDR stays rejected. - assert!( - p.check_resolved( - "v6.internal", - &[IpAddr::V6(Ipv6Addr::new(0xfe80, 0, 0, 0, 0, 0, 0, 1))] - ) - .is_err() - ); - } - - #[test] - fn bad_cidr_string_errors_at_build_time() { - assert!(JwksPolicy::from_parts(false, &[], &["not-a-cidr".into()]).is_err()); - assert!(JwksPolicy::from_parts(false, &[], &["10.0.0.0/40".into()]).is_err()); - } - - #[test] - fn cidr_contains_basic() { - let c4 = Cidr::parse("10.0.0.0/8").unwrap(); - assert!(c4.contains(&IpAddr::V4(Ipv4Addr::new(10, 99, 0, 1)))); - assert!(!c4.contains(&IpAddr::V4(Ipv4Addr::new(11, 0, 0, 1)))); - let c6 = Cidr::parse("fd00::/8").unwrap(); - assert!(c6.contains(&IpAddr::V6(Ipv6Addr::new(0xfd12, 0, 0, 0, 0, 0, 0, 1)))); - assert!(!c6.contains(&IpAddr::V6(Ipv6Addr::new(0xfe80, 0, 0, 0, 0, 0, 0, 1)))); - } -} diff --git a/nodedb/src/control/security/keystore/derivation.rs b/nodedb/src/control/security/keystore/derivation.rs index e904d5055..8bfa6b7b6 100644 --- a/nodedb/src/control/security/keystore/derivation.rs +++ b/nodedb/src/control/security/keystore/derivation.rs @@ -9,6 +9,7 @@ use super::file::FileKeyProvider; use super::provider::KeyProvider; use super::vault::VaultKeyProvider; use crate::Result; +use crate::control::security::egress::RemoteSources; /// How the server obtains its 32-byte AES-256 encryption key. /// @@ -79,11 +80,24 @@ pub enum KeyDerivation { } impl KeyDerivation { + /// The `type` name of this source, for log lines and errors. + pub fn kind(&self) -> &'static str { + match self { + KeyDerivation::File { .. } => "file", + KeyDerivation::Vault { .. } => "vault", + KeyDerivation::AwsKms { .. } => "aws_kms", + } + } + /// Build a boxed `KeyProvider` for this derivation config. /// /// This is called once at startup. The returned provider's `unwrap_key()` - /// is then called to obtain the 32-byte plaintext key. - pub async fn into_provider(self) -> Result> { + /// is then called to obtain the 32-byte plaintext key. A Vault provider + /// reaches Vault through the outbound client of `sources`. + pub async fn into_provider( + self, + sources: &RemoteSources, + ) -> Result> { match self { KeyDerivation::File { key_path } => Ok(Box::new(FileKeyProvider { key_path })), KeyDerivation::Vault { @@ -98,7 +112,8 @@ impl KeyDerivation { key_name, mount, ciphertext_blob_path, - ))), + sources, + )?)), KeyDerivation::AwsKms { key_id, region, diff --git a/nodedb/src/control/security/keystore/mod.rs b/nodedb/src/control/security/keystore/mod.rs index 4e7799a70..c2bb4d865 100644 --- a/nodedb/src/control/security/keystore/mod.rs +++ b/nodedb/src/control/security/keystore/mod.rs @@ -6,9 +6,11 @@ pub mod file; pub mod key_file_security; pub mod provider; pub mod vault; +pub mod wal_key; pub use aws_kms::AwsKmsProvider; pub use derivation::KeyDerivation; pub use file::FileKeyProvider; pub use provider::KeyProvider; pub use vault::VaultKeyProvider; +pub use wal_key::load_wal_key; diff --git a/nodedb/src/control/security/keystore/vault.rs b/nodedb/src/control/security/keystore/vault.rs index 27f60f62d..edcf75ba9 100644 --- a/nodedb/src/control/security/keystore/vault.rs +++ b/nodedb/src/control/security/keystore/vault.rs @@ -34,6 +34,10 @@ use crate::Result; use super::KeyProvider; use super::key_file_security::check_key_file; +use crate::control::security::egress::RemoteSources; + +/// Cap on one Vault request, connect and read included. +const VAULT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30); /// Vault transit-engine key provider. pub struct VaultKeyProvider { @@ -42,25 +46,39 @@ pub struct VaultKeyProvider { pub(super) key_name: String, pub(super) mount: String, pub(super) ciphertext_blob_path: PathBuf, - client: reqwest::Client, + /// The outbound egress client of the server's `[egress]` policy. + client: std::sync::Arc, } impl VaultKeyProvider { + /// A provider for the Vault at `addr`, reached through the outbound + /// client of `sources`. The address is operator config: one the + /// `[egress]` policy refuses is an error, so its host must be on the + /// allow-list. pub(super) fn new( addr: String, token_path: PathBuf, key_name: String, mount: String, ciphertext_blob_path: PathBuf, - ) -> Self { - Self { + sources: &RemoteSources, + ) -> Result { + sources + .egress() + .check_url(&addr) + .map_err(|e| crate::Error::Config { + detail: format!( + "Vault addr fails the [egress] policy: {e}; put its host on allow_hosts" + ), + })?; + Ok(Self { addr, token_path, key_name, mount, ciphertext_blob_path, - client: reqwest::Client::new(), - } + client: std::sync::Arc::clone(sources.http()), + }) } fn read_token(&self) -> Result { @@ -111,6 +129,7 @@ impl VaultKeyProvider { .client .post(&url) .header("X-Vault-Token", &token) + .timeout(VAULT_TIMEOUT) .json(&serde_json::json!({ "ciphertext": ciphertext })) .send() .await @@ -153,6 +172,7 @@ impl VaultKeyProvider { .client .post(&url) .header("X-Vault-Token", &token) + .timeout(VAULT_TIMEOUT) .json(&serde_json::json!({ "ciphertext": old_ciphertext })) .send() .await @@ -286,6 +306,46 @@ mod tests { }) } + /// Remote sources whose `[egress]` policy admits plaintext + /// `http://localhost` resolving to loopback, as an operator allow-lists a + /// private Vault. + fn loopback() -> RemoteSources { + RemoteSources::new(std::sync::Arc::new( + crate::control::security::egress::EgressPolicy::from_parts( + true, + &["localhost".to_string()], + &["127.0.0.0/8".to_string(), "::1/128".to_string()], + ) + .expect("valid policy"), + )) + .expect("remote sources") + } + + /// A Vault address outside the `[egress]` policy is refused before any + /// request: an IP literal always, and a host off the allow-list. + #[test] + fn a_vault_addr_outside_the_egress_policy_is_refused() { + let strict = RemoteSources::new(std::sync::Arc::new( + crate::control::security::egress::EgressPolicy::strict(), + )) + .expect("remote sources"); + for (addr, sources) in [ + ("http://127.0.0.1:8200", loopback()), + ("http://vault.internal:8200", strict), + ] { + let refused = VaultKeyProvider::new( + addr.into(), + "token".into(), + "mykey".into(), + "transit".into(), + "blob".into(), + &sources, + ); + let detail = refused.err().map(|e| e.to_string()).unwrap_or_default(); + assert!(detail.contains("[egress]"), "{addr}: {detail}"); + } + } + /// Write a file with secure Unix permissions (0o600) for use in tests. #[cfg(unix)] fn write_secure(path: &std::path::Path, content: &[u8]) { @@ -310,12 +370,14 @@ mod tests { write_secure(&blob_path, b"vault:v1:someciphertext=="); let provider = VaultKeyProvider::new( - format!("http://127.0.0.1:{port}"), + format!("http://localhost:{port}"), token_path, "mykey".into(), "transit".into(), blob_path, - ); + &loopback(), + ) + .expect("an allow-listed Vault"); let key = provider.unwrap_key().await.unwrap(); assert_eq!(*key, [0x42u8; 32]); @@ -335,12 +397,14 @@ mod tests { write_secure(&blob_path, b"vault:v1:cipher=="); let provider = VaultKeyProvider::new( - format!("http://127.0.0.1:{port}"), + format!("http://localhost:{port}"), token_path, "badkey".into(), "transit".into(), blob_path, - ); + &loopback(), + ) + .expect("an allow-listed Vault"); let err = provider.unwrap_key().await.unwrap_err(); let detail = format!("{err:?}"); @@ -364,12 +428,14 @@ mod tests { write_secure(&blob_path, b"vault:v1:original=="); let provider = VaultKeyProvider::new( - format!("http://127.0.0.1:{port}"), + format!("http://localhost:{port}"), token_path, "mykey".into(), "transit".into(), blob_path.clone(), - ); + &loopback(), + ) + .expect("an allow-listed Vault"); let key = provider.rotate().await.unwrap(); assert_eq!(*key, [0x42u8; 32]); @@ -395,12 +461,14 @@ mod tests { write_secure(&blob_path, b"vault:v1:cipher=="); let provider = VaultKeyProvider::new( - "http://127.0.0.1:1".into(), // unreachable — error before HTTP + "http://localhost:1".into(), // unreachable — error before HTTP token_path, "mykey".into(), "transit".into(), blob_path, - ); + &loopback(), + ) + .expect("an allow-listed Vault"); let err = provider.unwrap_key().await.unwrap_err(); let detail = format!("{err:?}"); diff --git a/nodedb/src/control/security/keystore/wal_key.rs b/nodedb/src/control/security/keystore/wal_key.rs new file mode 100644 index 000000000..571dc18bc --- /dev/null +++ b/nodedb/src/control/security/keystore/wal_key.rs @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! Load the WAL key from the source `[encryption]` names: a key file, Vault, +//! or AWS KMS. The WAL key also derives the segment and checkpoint keys. + +use std::sync::Arc; + +use crate::config::server::EncryptionSettings; +use crate::control::security::egress::{EgressPolicy, RemoteSources}; + +/// The WAL key `settings` names, read from its file or unwrapped by its +/// key-management service. A Vault request goes through `egress`, the +/// server's `[egress]` policy. +pub async fn load_wal_key( + settings: &EncryptionSettings, + egress: Arc, +) -> crate::Result { + let source = settings.source()?; + let kind = source.kind(); + let sources = RemoteSources::new(egress).map_err(|e| crate::Error::Config { + detail: format!("[encryption] {kind} key source: outbound HTTP client: {e}"), + })?; + let provider = source.into_provider(&sources).await?; + let key = provider.unwrap_key().await?; + nodedb_wal::crypto::WalEncryptionKey::from_bytes(&key).map_err(crate::Error::Wal) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[cfg(unix)] + #[tokio::test] + async fn a_key_file_is_the_default_source() { + use std::os::unix::fs::PermissionsExt as _; + let dir = tempfile::tempdir().expect("tempdir"); + let path = dir.path().join("wal.key"); + std::fs::write(&path, [0x24u8; 32]).expect("write key"); + std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600)) + .expect("restrict key"); + let key = load_wal_key( + &EncryptionSettings::file(path), + Arc::new(EgressPolicy::strict()), + ) + .await + .expect("the file key loads"); + let expected = nodedb_wal::crypto::WalEncryptionKey::with_epoch(&[0x24u8; 32], [0u8; 4]); + let sealed = expected + .encrypt_aad(1, b"header", b"payload") + .expect("encrypt with the expected key"); + let opened = key + .decrypt_aad(&[0u8; 4], 1, b"header", &sealed) + .expect("the loaded key opens what the file key sealed"); + assert_eq!(opened, b"payload"); + } + + #[tokio::test] + async fn a_settings_block_with_no_source_is_refused() { + let empty = EncryptionSettings { + key_path: None, + key_derivation: None, + }; + let refused = load_wal_key(&empty, Arc::new(EgressPolicy::strict())).await; + assert!( + refused + .err() + .is_some_and(|e| e.to_string().contains("[encryption]")) + ); + } +} diff --git a/nodedb/src/control/security/mod.rs b/nodedb/src/control/security/mod.rs index ca20d0c6a..23c9d7157 100644 --- a/nodedb/src/control/security/mod.rs +++ b/nodedb/src/control/security/mod.rs @@ -15,6 +15,7 @@ pub mod conditional; pub mod credential; pub mod crl; pub mod deny; +pub mod egress; pub mod emergency; pub mod encryption; pub mod escalation; @@ -28,6 +29,7 @@ pub mod jwt_policy; pub mod keystore; pub mod metering; pub mod mtls; +pub mod net; pub mod observability; pub mod oidc; pub mod org; @@ -50,6 +52,7 @@ pub mod session_handle; pub mod session_registry; pub mod sessions; pub mod siem; +pub mod source_credential; pub mod tenant; pub mod time; pub mod tls_policy; diff --git a/nodedb/src/control/security/net/cidr.rs b/nodedb/src/control/security/net/cidr.rs new file mode 100644 index 000000000..93cd042b8 --- /dev/null +++ b/nodedb/src/control/security/net/cidr.rs @@ -0,0 +1,144 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! A CIDR block: the one address-range type the IP blacklist, the +//! conditional-grant IP rule and the egress allow-list share. + +use std::net::IpAddr; + +/// An IPv4 or IPv6 address block. A block matches only addresses of its +/// own family. An IPv4-mapped IPv6 address matches as its IPv4 form. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Cidr { + base: u128, + mask: u128, + prefix_len: u8, + is_v4: bool, +} + +impl Cidr { + /// Parse `"10.42.0.0/16"`, `"fd00::/8"`, or a single host such as + /// `"192.168.1.100"` (a `/32` or `/128`). Returns `None` when the input + /// is not an address with an optional prefix no wider than the family. + pub fn parse(s: &str) -> Option { + let (ip_str, prefix_str) = match s.split_once('/') { + Some((ip, prefix)) => (ip, Some(prefix)), + None => (s, None), + }; + let ip: IpAddr = ip_str.parse().ok()?; + let width = family_width(&ip); + let prefix_len = match prefix_str { + Some(p) => p.parse::().ok()?, + None => width, + }; + if prefix_len > width { + return None; + } + let mask = mask_of(prefix_len, width); + Some(Self { + base: bits_of(&ip) & mask, + mask, + prefix_len, + is_v4: ip.is_ipv4(), + }) + } + + /// The prefix length: 24 for a `/24`. + pub fn prefix_len(&self) -> u8 { + self.prefix_len + } + + /// Whether `ip` lies inside the block. + pub fn contains(&self, ip: &IpAddr) -> bool { + let ip = ip.to_canonical(); + ip.is_ipv4() == self.is_v4 && bits_of(&ip) & self.mask == self.base + } +} + +/// The address width of `ip`'s family in bits. +fn family_width(ip: &IpAddr) -> u8 { + if ip.is_ipv4() { 32 } else { 128 } +} + +/// The address as an integer. An IPv4 address fills the low 32 bits. +fn bits_of(ip: &IpAddr) -> u128 { + match ip { + IpAddr::V4(v4) => u128::from(u32::from(*v4)), + IpAddr::V6(v6) => u128::from(*v6), + } +} + +/// The mask of the top `prefix_len` bits of a `width`-bit address. +fn mask_of(prefix_len: u8, width: u8) -> u128 { + if prefix_len == 0 { + return 0; + } + let full = u128::MAX >> (128 - u32::from(width)); + (full >> (width - prefix_len)) << (width - prefix_len) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn ip(s: &str) -> IpAddr { + s.parse().expect("ip") + } + + fn cidr(s: &str) -> Cidr { + Cidr::parse(s).expect("cidr") + } + + #[test] + fn ranges_contain_their_addresses() { + assert!(cidr("10.0.0.0/8").contains(&ip("10.255.255.255"))); + assert!(!cidr("10.0.0.0/8").contains(&ip("11.0.0.1"))); + assert!(cidr("192.168.1.0/24").contains(&ip("192.168.1.254"))); + assert!(!cidr("192.168.1.0/24").contains(&ip("192.168.2.1"))); + assert!(cidr("2001:db8::/32").contains(&ip("2001:db8:ffff::1"))); + assert!(!cidr("2001:db8::/32").contains(&ip("2001:db9::1"))); + assert!(cidr("fd00::/8").contains(&ip("fd12::1"))); + assert!(!cidr("fd00::/8").contains(&ip("fe80::1"))); + assert_eq!(cidr("10.0.0.0/8").prefix_len(), 8); + } + + #[test] + fn a_bare_address_is_a_single_host() { + let host = cidr("192.168.1.100"); + assert_eq!(host.prefix_len(), 32); + assert!(host.contains(&ip("192.168.1.100"))); + assert!(!host.contains(&ip("192.168.1.101"))); + let host6 = cidr("2001:db8::1"); + assert_eq!(host6.prefix_len(), 128); + assert!(host6.contains(&ip("2001:db8::1"))); + assert!(!host6.contains(&ip("2001:db8::2"))); + } + + #[test] + fn a_block_matches_only_its_own_family() { + // `::a00:1` has the same low 32 bits as `10.0.0.1`. + assert!(!cidr("10.0.0.0/8").contains(&ip("::a00:1"))); + assert!(!cidr("0.0.0.0/0").contains(&ip("2001:db8::1"))); + assert!(!cidr("::/0").contains(&ip("10.0.0.1"))); + assert!(cidr("0.0.0.0/0").contains(&ip("10.0.0.1"))); + assert!(cidr("::/0").contains(&ip("2001:db8::1"))); + } + + #[test] + fn a_mapped_address_matches_as_ipv4() { + assert!(cidr("10.0.0.0/8").contains(&ip("::ffff:10.0.0.5"))); + assert!(!cidr("::ffff:0:0/96").contains(&ip("::ffff:10.0.0.5"))); + } + + #[test] + fn malformed_input_is_refused() { + for bad in [ + "not-an-ip", + "10.0.0.0/33", + "::/129", + "10.0.0.0/", + "10.0.0.0/x", + ] { + assert!(Cidr::parse(bad).is_none(), "{bad}"); + } + } +} diff --git a/nodedb/src/control/security/net/mod.rs b/nodedb/src/control/security/net/mod.rs new file mode 100644 index 000000000..a57d7ee96 --- /dev/null +++ b/nodedb/src/control/security/net/mod.rs @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pub mod cidr; + +pub use cidr::Cidr; diff --git a/nodedb/src/control/security/predicate_typing.rs b/nodedb/src/control/security/predicate_typing.rs index 165e36ae1..b9d2eebb0 100644 --- a/nodedb/src/control/security/predicate_typing.rs +++ b/nodedb/src/control/security/predicate_typing.rs @@ -124,7 +124,8 @@ fn sql_literal_to_predicate_value( | SqlValue::Decimal(_) | SqlValue::String(_) | SqlValue::Bytes(_) - | SqlValue::Array(_) => PredicateValue::Literal(original), + | SqlValue::Array(_) + | SqlValue::Geometry(_) => PredicateValue::Literal(original), } } diff --git a/nodedb/src/control/security/rls/eval.rs b/nodedb/src/control/security/rls/eval.rs index 3d536f07c..f2fd22a03 100644 --- a/nodedb/src/control/security/rls/eval.rs +++ b/nodedb/src/control/security/rls/eval.rs @@ -95,6 +95,7 @@ impl RlsPolicyStore { document: &serde_json::Value, auth: &AuthContext, emitter: &dyn AuditEmitter, + crs: &nodedb_crs::CrsContext<'_>, ) -> crate::Result<()> { if auth.is_superuser() { return Ok(()); @@ -107,9 +108,16 @@ impl RlsPolicyStore { for policy in &policies { if let Some(ref compiled) = policy.compiled_predicate { - check_compiled_write( - policy, compiled, document, auth, tenant_id, collection, emitter, - )?; + check_compiled_write(CompiledWriteCheck { + policy, + compiled, + document, + auth, + tenant_id, + collection, + emitter, + crs, + })?; } } @@ -172,6 +180,7 @@ pub fn admit_compiled_write_image( image: &[u8], tenant_id: u64, collection: &str, + crs: &nodedb_crs::CrsContext<'_>, ) -> crate::Result<()> { if compiled.is_empty() { return Ok(()); @@ -184,22 +193,36 @@ pub fn admit_compiled_write_image( tenant_id: crate::types::TenantId::new(tenant_id), resource: format!("RLS write policy on '{collection}' {detail}"), }; - match crate::bridge::scan_filter::ScanFilter::all_match_binary(&filters, image) { + match crate::bridge::scan_filter::ScanFilter::all_match_binary(&filters, image, crs) { Ok(true) => Ok(()), Ok(false) => Err(deny("rejected the row".into())), Err(error) => Err(deny(format!("could not be evaluated: {error}"))), } } -fn check_compiled_write( - policy: &RlsPolicy, - compiled: &RlsPredicate, - document: &serde_json::Value, - auth: &AuthContext, +/// One compiled write policy judged against one document. +struct CompiledWriteCheck<'a> { + policy: &'a RlsPolicy, + compiled: &'a RlsPredicate, + document: &'a serde_json::Value, + auth: &'a AuthContext, tenant_id: u64, - collection: &str, - emitter: &dyn AuditEmitter, -) -> crate::Result<()> { + collection: &'a str, + emitter: &'a dyn AuditEmitter, + crs: &'a nodedb_crs::CrsContext<'a>, +} + +fn check_compiled_write(check: CompiledWriteCheck<'_>) -> crate::Result<()> { + let CompiledWriteCheck { + policy, + compiled, + document, + auth, + tenant_id, + collection, + emitter, + crs, + } = check; let filters = match substitute_to_scan_filters(compiled, auth) { Some(f) => f, None => { @@ -233,7 +256,7 @@ fn check_compiled_write( }; let doc_mp = nodedb_types::json_to_msgpack_or_empty(document); - if !crate::bridge::scan_filter::ScanFilter::all_match_binary(&filters, &doc_mp)? { + if !crate::bridge::scan_filter::ScanFilter::all_match_binary(&filters, &doc_mp, crs)? { info!( policy = %policy.name, username = %auth.username, @@ -330,7 +353,14 @@ mod tests { let doc_ok = serde_json::json!({"owner_id": "42", "amount": 100}); assert!( store - .check_write_with_auth(1, "orders", &doc_ok, &auth, NOOP) + .check_write_with_auth( + 1, + "orders", + &doc_ok, + &auth, + NOOP, + &nodedb_crs::CrsContext::builtin_only() + ) .is_ok() ); @@ -338,7 +368,14 @@ mod tests { let doc_bad = serde_json::json!({"owner_id": "99", "amount": 100}); assert!( store - .check_write_with_auth(1, "orders", &doc_bad, &auth, NOOP) + .check_write_with_auth( + 1, + "orders", + &doc_bad, + &auth, + NOOP, + &nodedb_crs::CrsContext::builtin_only() + ) .is_err() ); } @@ -350,7 +387,14 @@ mod tests { let doc = serde_json::json!({"anything": "goes"}); assert!( store - .check_write_with_auth(1, "whatever", &doc, &auth, NOOP) + .check_write_with_auth( + 1, + "whatever", + &doc, + &auth, + NOOP, + &nodedb_crs::CrsContext::builtin_only() + ) .is_ok() ); } @@ -371,7 +415,14 @@ mod tests { let auth = nonsuper_auth(); let doc = serde_json::json!({"org_id": "some_org"}); - let result = store.check_write_with_auth(1, "orders", &doc, &auth, NOOP); + let result = store.check_write_with_auth( + 1, + "orders", + &doc, + &auth, + NOOP, + &nodedb_crs::CrsContext::builtin_only(), + ); assert!( result.is_err(), "RLS must fail-closed on unresolvable $auth ref; got {result:?}" @@ -394,7 +445,14 @@ mod tests { let doc = serde_json::json!({"secret_scope": "admin"}); let auth = nonsuper_auth(); - let result = store.check_write_with_auth(1, "orders", &doc, &auth, NOOP); + let result = store.check_write_with_auth( + 1, + "orders", + &doc, + &auth, + NOOP, + &nodedb_crs::CrsContext::builtin_only(), + ); assert!( result.is_err(), "write path must fail-closed on unresolvable auth ref; got {result:?}" @@ -419,7 +477,14 @@ mod tests { // alice's user_id is 42; owner_id is 99 → denied. let doc_bad = serde_json::json!({"owner_id": "99", "qty": 1}); let emitter = CapturingEmitter::new(); - let result = store.check_write_with_auth(1, "items", &doc_bad, &auth, &emitter); + let result = store.check_write_with_auth( + 1, + "items", + &doc_bad, + &auth, + &emitter, + &nodedb_crs::CrsContext::builtin_only(), + ); assert!(result.is_err()); let recorded = emitter.recorded(); @@ -444,7 +509,14 @@ mod tests { let auth = nonsuper_auth(); let doc_ok = serde_json::json!({"owner_id": "42", "qty": 1}); let emitter = CapturingEmitter::new(); - let result = store.check_write_with_auth(1, "items", &doc_ok, &auth, &emitter); + let result = store.check_write_with_auth( + 1, + "items", + &doc_ok, + &auth, + &emitter, + &nodedb_crs::CrsContext::builtin_only(), + ); assert!(result.is_ok()); assert!(emitter.recorded().is_empty()); } @@ -540,7 +612,13 @@ mod tests { }]; let compiled = encode_scan_filters(&filters).expect("encodes"); let image = nodedb_types::json_to_msgpack_or_empty(&serde_json::json!({"qty": 1})); - let result = admit_compiled_write_image(&compiled, &image, 1, "items"); + let result = admit_compiled_write_image( + &compiled, + &image, + 1, + "items", + &nodedb_crs::CrsContext::builtin_only(), + ); match result { Err(crate::Error::RejectedAuthz { resource, .. }) => { assert!( diff --git a/nodedb/src/control/security/siem/delivery.rs b/nodedb/src/control/security/siem/delivery.rs index 38955cb4d..f5b10dff5 100644 --- a/nodedb/src/control/security/siem/delivery.rs +++ b/nodedb/src/control/security/siem/delivery.rs @@ -45,6 +45,12 @@ impl SiemExporter { } let count = batch.len(); + // An IP-literal host is never resolved, so only this check refuses it. + if let Err(e) = self.policy.check_url(&self.config.webhook_url) { + warn!(error = %e, events = count, "SIEM webhook URL fails the [egress] policy"); + return self.fail(batch, audit_len); + } + let (body, signature) = match self.build_webhook_payload(&batch) { Ok(payload) => payload, Err(e) => { @@ -100,6 +106,10 @@ mod tests { use crate::control::security::audit::AuditEvent; use crate::control::security::siem::config::SiemConfig; + fn exporter(config: SiemConfig) -> SiemExporter { + SiemExporter::strict_for_test(config) + } + fn entry(seq: u64) -> AuditEntry { AuditEntry { seq, @@ -118,7 +128,7 @@ mod tests { #[tokio::test] async fn no_webhook_url_is_idle_and_keeps_the_buffer() { - let exporter = SiemExporter::new(SiemConfig { + let exporter = exporter(SiemConfig { destinations: vec!["splunk".into()], ..Default::default() }); @@ -129,7 +139,7 @@ mod tests { #[tokio::test] async fn empty_buffers_are_idle() { - let exporter = SiemExporter::new(SiemConfig { + let exporter = exporter(SiemConfig { destinations: vec!["webhook".into()], webhook_url: "http://127.0.0.1:1/ingest".into(), ..Default::default() @@ -139,13 +149,13 @@ mod tests { /// No local HTTP sink is available in unit tests, so the success path is /// covered by payload-construction tests; here the reachable behaviour is - /// the failure path — a refused connection must requeue every event and - /// count the failure rather than silently discarding the batch. + /// the failure path. The strict egress policy refuses the IP-literal URL, + /// and the refusal must requeue every event and count the failure rather + /// than silently discarding the batch. #[tokio::test] async fn failed_delivery_requeues_both_buffers() { - let exporter = SiemExporter::new(SiemConfig { + let exporter = exporter(SiemConfig { destinations: vec!["webhook".into()], - // Port 1 is reserved and refuses connections immediately. webhook_url: "http://127.0.0.1:1/ingest".into(), webhook_timeout_secs: 2, ..Default::default() diff --git a/nodedb/src/control/security/siem/exporter.rs b/nodedb/src/control/security/siem/exporter.rs index 10a83c82d..7dab2c60e 100644 --- a/nodedb/src/control/security/siem/exporter.rs +++ b/nodedb/src/control/security/siem/exporter.rs @@ -19,14 +19,17 @@ use std::sync::{Arc, RwLock}; use super::config::SiemConfig; use crate::control::security::audit::AuditEntry; +use crate::control::security::egress::{EgressPolicy, outbound_client}; /// SIEM export adapter: buffers events for CDC streaming and webhook delivery. pub struct SiemExporter { pub(super) config: SiemConfig, /// Shared HTTP client — constructed once at startup and reused across /// every webhook flush so we don't rebuild the connection pool and - /// TLS session cache per call. + /// TLS session cache per call. It is the egress client built on `policy`. pub(super) client: Arc, + /// The `[egress]` policy the webhook URL passes before every flush. + pub(super) policy: Arc, /// Buffered audit events for CDC consumers. audit_buffer: RwLock>, /// Buffered auth events for CDC consumers. @@ -42,16 +45,24 @@ pub struct SiemExporter { } impl SiemExporter { - pub fn new(config: SiemConfig) -> Self { - Self::with_client(config, Arc::new(reqwest::Client::new())) - } - - /// Construct with an existing shared HTTP client. - pub fn with_client(config: SiemConfig, client: Arc) -> Self { + /// An exporter with its own egress client built on `policy`. + pub fn new(config: SiemConfig, policy: Arc) -> Result { + let client = Arc::new(outbound_client(Arc::clone(&policy))?); + Ok(Self::with_client(config, client, policy)) + } + + /// Construct with an existing shared HTTP client, the egress client + /// built on `policy`. + pub fn with_client( + config: SiemConfig, + client: Arc, + policy: Arc, + ) -> Self { let cap = config.buffer_size; Self { config, client, + policy, audit_buffer: RwLock::new(VecDeque::with_capacity(cap.min(10_000))), auth_buffer: RwLock::new(VecDeque::with_capacity(cap.min(10_000))), dropped_audit: AtomicU64::new(0), @@ -61,6 +72,12 @@ impl SiemExporter { } } + /// An exporter under the strict egress policy, for tests. + #[cfg(test)] + pub(crate) fn strict_for_test(config: SiemConfig) -> Self { + Self::new(config, Arc::new(EgressPolicy::strict())).expect("exporter") + } + /// Push an audit event to the export buffer. pub fn push_audit(&self, entry: AuditEntry) { self.push_bounded(&self.audit_buffer, &self.dropped_audit, entry); @@ -240,12 +257,6 @@ impl SiemExporter { } } -impl Default for SiemExporter { - fn default() -> Self { - Self::new(SiemConfig::default()) - } -} - /// Compute HMAC-SHA256 signature for webhook payload. pub(super) fn compute_hmac(secret: &str, message: &str) -> String { use hmac::{Hmac, Mac}; @@ -292,7 +303,7 @@ mod tests { #[test] fn buffer_and_drain() { - let exporter = SiemExporter::default(); + let exporter = SiemExporter::strict_for_test(SiemConfig::default()); exporter.push_audit(test_entry(1)); exporter.push_audit(test_entry(2)); exporter.push_auth(test_entry(3)); @@ -306,7 +317,7 @@ mod tests { #[test] fn push_past_buffer_size_is_bounded_and_counted() { - let exporter = SiemExporter::new(webhook_config(3)); + let exporter = SiemExporter::strict_for_test(webhook_config(3)); for seq in 0..10 { exporter.push_audit(test_entry(seq)); } @@ -325,7 +336,7 @@ mod tests { #[test] fn zero_capacity_buffers_nothing_and_counts_every_event() { - let exporter = SiemExporter::new(webhook_config(0)); + let exporter = SiemExporter::strict_for_test(webhook_config(0)); exporter.push_audit(test_entry(1)); exporter.push_auth(test_entry(2)); assert_eq!(exporter.buffered_count(), 0); @@ -335,7 +346,7 @@ mod tests { #[test] fn requeue_restores_order_and_respects_the_ceiling() { - let exporter = SiemExporter::new(webhook_config(3)); + let exporter = SiemExporter::strict_for_test(webhook_config(3)); exporter.push_audit(test_entry(10)); // Two in-flight events come back ahead of the one that arrived after. exporter.requeue_audit(vec![test_entry(1), test_entry(2)]); @@ -358,7 +369,7 @@ mod tests { webhook_hmac_secret: "test_secret".into(), ..Default::default() }; - let exporter = SiemExporter::new(config); + let exporter = SiemExporter::strict_for_test(config); let (body, signature) = exporter .build_webhook_payload(&[test_entry(1)]) @@ -371,7 +382,7 @@ mod tests { #[test] fn webhook_payload_carries_the_whole_drained_batch() { - let exporter = SiemExporter::new(webhook_config(10)); + let exporter = SiemExporter::strict_for_test(webhook_config(10)); let batch = vec![test_entry(1), test_entry(2), test_entry(3)]; let (body, signature) = exporter .build_webhook_payload(&batch) @@ -392,7 +403,7 @@ mod tests { #[test] fn default_exporter_is_not_configured() { - assert!(!SiemExporter::default().is_configured()); - assert!(SiemExporter::new(webhook_config(1)).is_configured()); + assert!(!SiemExporter::strict_for_test(SiemConfig::default()).is_configured()); + assert!(SiemExporter::strict_for_test(webhook_config(1)).is_configured()); } } diff --git a/nodedb/src/control/security/siem/metrics.rs b/nodedb/src/control/security/siem/metrics.rs index 02fa07aee..53e1b7949 100644 --- a/nodedb/src/control/security/siem/metrics.rs +++ b/nodedb/src/control/security/siem/metrics.rs @@ -94,13 +94,16 @@ mod tests { #[test] fn unconfigured_exporter_renders_nothing() { let mut out = String::new(); - render_prometheus(&SiemExporter::default(), &mut out); + render_prometheus( + &SiemExporter::strict_for_test(SiemConfig::default()), + &mut out, + ); assert!(out.is_empty()); } #[test] fn surfaces_buffer_overflow_drops() { - let exporter = SiemExporter::new(SiemConfig { + let exporter = SiemExporter::strict_for_test(SiemConfig { destinations: vec!["webhook".into()], buffer_size: 1, ..Default::default() diff --git a/nodedb/src/control/security/source_credential/kek.rs b/nodedb/src/control/security/source_credential/kek.rs new file mode 100644 index 000000000..2df60c6c5 --- /dev/null +++ b/nodedb/src/control/security/source_credential/kek.rs @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! The key that seals `CREATE CREDENTIAL` secrets at rest, read from the +//! `[credential_encryption] key_path` file. +//! +//! Every node of a cluster reads the same key file, so a secret one node +//! seals opens on every node. The key file passes the same checks as the +//! WAL key: no symlink, a regular file, mode `0400` or `0600`, owned by the +//! server's user. + +use std::path::Path; + +use zeroize::Zeroizing; + +use crate::control::security::keystore::key_file_security::check_key_file; + +/// Bytes in the key. +pub const KEK_LEN: usize = 32; + +/// The credential key. `Debug` never prints it. +#[derive(Clone)] +pub struct CredentialKek(Zeroizing<[u8; KEK_LEN]>); + +impl CredentialKek { + /// The key `bytes`. + pub fn from_bytes(bytes: [u8; KEK_LEN]) -> Self { + Self(Zeroizing::new(bytes)) + } + + /// Read the key file at `path`: exactly 32 bytes. + pub fn read(path: &Path) -> crate::Result { + check_key_file(path)?; + let raw = Zeroizing::new(std::fs::read(path).map_err(|e| crate::Error::Encryption { + detail: format!("cannot read credential key file {}: {e}", path.display()), + })?); + let bytes: [u8; KEK_LEN] = + raw.as_slice() + .try_into() + .map_err(|_| crate::Error::Encryption { + detail: format!( + "credential key file {} holds {} bytes, expected {KEK_LEN}; generate one \ + with `head -c 32 /dev/urandom`", + path.display(), + raw.len() + ), + })?; + Ok(Self::from_bytes(bytes)) + } + + /// The key bytes. + pub(super) fn bytes(&self) -> &[u8; KEK_LEN] { + &self.0 + } +} + +impl std::fmt::Debug for CredentialKek { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("CredentialKek()") + } +} diff --git a/nodedb/src/control/security/source_credential/mod.rs b/nodedb/src/control/security/source_credential/mod.rs new file mode 100644 index 000000000..635269611 --- /dev/null +++ b/nodedb/src/control/security/source_credential/mod.rs @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pub mod kek; +pub mod seal; +pub mod secret; +pub mod stored; + +pub use kek::CredentialKek; +pub use seal::{SealError, open_secret, seal_secret}; +pub use secret::CredentialSecret; +pub use stored::{CredentialKind, StoredCredential}; diff --git a/nodedb/src/control/security/source_credential/seal.rs b/nodedb/src/control/security/source_credential/seal.rs new file mode 100644 index 000000000..4ab946624 --- /dev/null +++ b/nodedb/src/control/security/source_credential/seal.rs @@ -0,0 +1,162 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! Seal and open credential secrets: AES-256-GCM under a key derived from +//! the credential key with HKDF-SHA256. +//! +//! A sealed secret is `nonce (12 bytes) || ciphertext || tag`. The +//! additional data binds it to its tenant, name and type, so a sealed blob +//! copied onto another credential row fails to open. + +use aes_gcm::aead::{Aead as _, KeyInit as _, Payload}; +use aes_gcm::{Aes256Gcm, Nonce}; +use hkdf::Hkdf; +use sha2::Sha256; +use zeroize::Zeroizing; + +use super::kek::CredentialKek; +use super::secret::CredentialSecret; + +/// HKDF salt: the sealing scheme and its version. +const HKDF_SALT: &[u8] = b"nodedb-credential-v1"; +/// HKDF info: what the derived key encrypts. +const HKDF_INFO: &[u8] = b"credential-secret"; +/// Bytes of an AES-GCM nonce. +const NONCE_LEN: usize = 12; + +/// Why a secret did not seal or open. +#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] +pub enum SealError { + #[error("credential key derivation failed")] + Derive, + #[error("the OS random source failed")] + Random, + #[error("credential secret encoding failed")] + Encode, + #[error("credential secret encryption failed")] + Encrypt, + #[error( + "the sealed credential does not open with this node's credential key; every node needs \ + the same [credential_encryption] key file, or the credential must be re-created" + )] + Open, +} + +/// The additional data of one credential row. +fn aad(tenant_id: u64, name: &str, kind: &str) -> Vec { + let mut out = Vec::with_capacity(16 + name.len() + kind.len()); + out.extend_from_slice(&tenant_id.to_be_bytes()); + out.extend_from_slice(&(name.len() as u64).to_be_bytes()); + out.extend_from_slice(name.as_bytes()); + out.extend_from_slice(kind.as_bytes()); + out +} + +fn cipher(kek: &CredentialKek) -> Result { + let mut key = Zeroizing::new([0u8; 32]); + Hkdf::::new(Some(HKDF_SALT), kek.bytes()) + .expand(HKDF_INFO, &mut *key) + .map_err(|_| SealError::Derive)?; + Aes256Gcm::new_from_slice(&*key).map_err(|_| SealError::Derive) +} + +/// Seal `secret` for credential `name` of `tenant_id`. +pub fn seal_secret( + kek: &CredentialKek, + tenant_id: u64, + name: &str, + secret: &CredentialSecret, +) -> Result, SealError> { + let plaintext = Zeroizing::new(zerompk::to_msgpack_vec(secret).map_err(|_| SealError::Encode)?); + let mut nonce = [0u8; NONCE_LEN]; + getrandom::fill(&mut nonce).map_err(|_| SealError::Random)?; + let ciphertext = cipher(kek)? + .encrypt( + &Nonce::from(nonce), + Payload { + msg: &plaintext, + aad: &aad(tenant_id, name, secret.type_name()), + }, + ) + .map_err(|_| SealError::Encrypt)?; + let mut sealed = Vec::with_capacity(NONCE_LEN + ciphertext.len()); + sealed.extend_from_slice(&nonce); + sealed.extend_from_slice(&ciphertext); + Ok(sealed) +} + +/// Open the sealed secret of credential `name` of `tenant_id`, whose type +/// is `kind`. +pub fn open_secret( + kek: &CredentialKek, + tenant_id: u64, + name: &str, + kind: &str, + sealed: &[u8], +) -> Result { + let (nonce, ciphertext) = sealed.split_at_checked(NONCE_LEN).ok_or(SealError::Open)?; + let nonce: [u8; NONCE_LEN] = nonce.try_into().map_err(|_| SealError::Open)?; + let plaintext = Zeroizing::new( + cipher(kek)? + .decrypt( + &Nonce::from(nonce), + Payload { + msg: ciphertext, + aad: &aad(tenant_id, name, kind), + }, + ) + .map_err(|_| SealError::Open)?, + ); + let secret: CredentialSecret = + zerompk::from_msgpack(&plaintext).map_err(|_| SealError::Open)?; + if secret.type_name() != kind { + return Err(SealError::Open); + } + Ok(secret) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn bearer() -> CredentialSecret { + CredentialSecret::Bearer { + token: "eyJ-token".into(), + } + } + + #[test] + fn a_sealed_secret_opens_only_on_its_own_row_and_key() { + let kek = CredentialKek::from_bytes([7; 32]); + let sealed = seal_secret(&kek, 1, "earthdata", &bearer()).expect("seals"); + assert!(!sealed.windows(9).any(|w| w == b"eyJ-token")); + let opened = open_secret(&kek, 1, "earthdata", "bearer", &sealed).expect("opens"); + assert_eq!(opened, bearer()); + let other_key = CredentialKek::from_bytes([8; 32]); + assert_eq!( + open_secret(&other_key, 1, "earthdata", "bearer", &sealed), + Err(SealError::Open) + ); + assert_eq!( + open_secret(&kek, 2, "earthdata", "bearer", &sealed), + Err(SealError::Open) + ); + assert_eq!( + open_secret(&kek, 1, "other", "bearer", &sealed), + Err(SealError::Open) + ); + assert_eq!( + open_secret(&kek, 1, "earthdata", "sas", &sealed), + Err(SealError::Open) + ); + assert_eq!( + open_secret(&kek, 1, "earthdata", "bearer", &sealed[..5]), + Err(SealError::Open) + ); + } + + #[test] + fn debug_never_prints_a_secret() { + let text = format!("{:?} {:?}", bearer(), CredentialKek::from_bytes([7; 32])); + assert!(!text.contains("eyJ"), "{text}"); + } +} diff --git a/nodedb/src/control/security/source_credential/secret.rs b/nodedb/src/control/security/source_credential/secret.rs new file mode 100644 index 000000000..98798ba6f --- /dev/null +++ b/nodedb/src/control/security/source_credential/secret.rs @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! The plaintext of a credential: what a sealed credential opens to. It +//! never leaves the server: no SHOW, describe or error prints it, and its +//! `Debug` form is redacted. + +use zeroize::Zeroize; + +/// The secret fields of one credential, by type. +#[derive(Clone, PartialEq, Eq, zerompk::ToMessagePack, zerompk::FromMessagePack)] +pub enum CredentialSecret { + /// AWS keys for `s3://` sources. + S3 { + access_key_id: String, + secret_access_key: String, + region: String, + session_token: Option, + /// An S3-compatible endpoint in place of AWS. + endpoint: Option, + }, + /// An `Authorization: Bearer` token. + Bearer { token: String }, + /// A shared access signature appended to the URL query. + Sas { token: String }, +} + +impl CredentialSecret { + /// The `TYPE` keyword. + pub fn type_name(&self) -> &'static str { + match self { + Self::S3 { .. } => "s3", + Self::Bearer { .. } => "bearer", + Self::Sas { .. } => "sas", + } + } +} + +impl std::fmt::Debug for CredentialSecret { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "CredentialSecret::{}()", self.type_name()) + } +} + +impl Drop for CredentialSecret { + fn drop(&mut self) { + match self { + Self::S3 { + access_key_id, + secret_access_key, + session_token, + .. + } => { + access_key_id.zeroize(); + secret_access_key.zeroize(); + if let Some(token) = session_token { + token.zeroize(); + } + } + Self::Bearer { token } | Self::Sas { token } => token.zeroize(), + } + } +} diff --git a/nodedb/src/control/security/source_credential/stored.rs b/nodedb/src/control/security/source_credential/stored.rs new file mode 100644 index 000000000..bf80057df --- /dev/null +++ b/nodedb/src/control/security/source_credential/stored.rs @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! The catalog row of one credential. The secret is sealed before the row +//! is proposed, so the metadata log, the catalog file and every snapshot +//! hold ciphertext only. + +use serde::{Deserialize, Serialize}; + +/// The credential type, readable without opening the secret. +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Eq, + Serialize, + Deserialize, + zerompk::ToMessagePack, + zerompk::FromMessagePack, +)] +pub enum CredentialKind { + S3, + Bearer, + Sas, +} + +impl CredentialKind { + /// The `TYPE` keyword. + pub fn type_name(self) -> &'static str { + match self { + Self::S3 => "s3", + Self::Bearer => "bearer", + Self::Sas => "sas", + } + } +} + +/// One `CREATE CREDENTIAL` row. +#[derive( + Debug, + Clone, + PartialEq, + Eq, + Serialize, + Deserialize, + zerompk::ToMessagePack, + zerompk::FromMessagePack, +)] +pub struct StoredCredential { + pub tenant_id: u64, + pub name: String, + pub kind: CredentialKind, + /// The sealed `CredentialSecret`. + pub sealed: Vec, + /// The user that created it. + pub owner: String, + pub created_at_ms: u64, +} diff --git a/nodedb/src/control/server/dispatch_utils/change_events/extract.rs b/nodedb/src/control/server/dispatch_utils/change_events/extract.rs index 976600ac9..cc80642bc 100644 --- a/nodedb/src/control/server/dispatch_utils/change_events/extract.rs +++ b/nodedb/src/control/server/dispatch_utils/change_events/extract.rs @@ -485,6 +485,7 @@ mod tests { schema_bytes: Vec::new(), provenance: None, wal_lsn: None, + system_from: None, rls_write_check: nodedb_types::RlsWriteCheck::pending_injection(), returning: None, rls_filters: Vec::new(), @@ -501,6 +502,7 @@ mod tests { let plan = PhysicalPlan::Columnar(ColumnarOp::Delete { collection: QualifiedCollection::new(DatabaseId::DEFAULT, "metrics"), filters: Vec::new(), + system_from: None, rls_write_check: nodedb_types::RlsWriteCheck::pending_injection(), }); let meta = extract_write_metadata(&plan, TenantId::new(1)); @@ -586,6 +588,7 @@ mod tests { system_time: nodedb_types::SystemTimeScope::Current, valid_at_ms: None, zone_msgpack: Vec::new(), + physical: false, }); assert!(extract_write_metadata(&slice, TenantId::new(1)).is_empty()); @@ -596,9 +599,10 @@ mod tests { group_by_dim: -1, slice_tile_keys: Vec::new(), prefix_bits: 8, - system_as_of: None, + system_time: nodedb_types::SystemTimeScope::Current, valid_at_ms: None, region_msgpack: Vec::new(), + physical: false, }); assert!(extract_write_metadata(&agg, TenantId::new(1)).is_empty()); } @@ -615,6 +619,8 @@ mod tests { properties: Vec::new(), src_surrogate: Surrogate::new(1), dst_surrogate: Surrogate::new(2), + valid_from_ms: None, + valid_until_ms: None, }); let meta = extract_write_metadata(&plan, TenantId::new(1)); assert!(meta.is_empty()); diff --git a/nodedb/src/control/server/dispatch_utils/dispatch.rs b/nodedb/src/control/server/dispatch_utils/dispatch.rs index 95e4f8a9c..5338892ed 100644 --- a/nodedb/src/control/server/dispatch_utils/dispatch.rs +++ b/nodedb/src/control/server/dispatch_utils/dispatch.rs @@ -80,6 +80,7 @@ pub async fn dispatch_authorized_autocommit_write( apply_key: 0, commit_hlc: None, change_position: None, + system_ordinal: None, }, change_feed: ChangeFeedOwner::LocalApply, read_route: ReadRoute::Owned, @@ -121,6 +122,7 @@ pub(crate) async fn dispatch_authorized_autocommit_write_with_source( apply_key: 0, commit_hlc: None, change_position: None, + system_ordinal: None, }, change_feed: ChangeFeedOwner::LocalApply, read_route: ReadRoute::Owned, @@ -333,6 +335,7 @@ pub(crate) async fn dispatch_autocommit_write( apply_key: 0, commit_hlc: None, change_position: None, + system_ordinal: None, }, change_feed: ChangeFeedOwner::LocalApply, read_route: ReadRoute::Owned, @@ -1054,6 +1057,7 @@ mod tests { apply_key, commit_hlc: None, change_position: None, + system_ordinal: None, }, ordering: super::WriteOrdering::AlreadyOrdered, change_feed: super::ChangeFeedOwner::Unowned, diff --git a/nodedb/src/control/server/dispatch_utils/durable_write.rs b/nodedb/src/control/server/dispatch_utils/durable_write.rs index 8b952913b..001dfb619 100644 --- a/nodedb/src/control/server/dispatch_utils/durable_write.rs +++ b/nodedb/src/control/server/dispatch_utils/durable_write.rs @@ -6,9 +6,9 @@ //! //! - Replicable write: the write is proposed through Raft. Every replica //! applies the committed entry through the funnel, which appends the redo -//! record. The proposing node publishes the change event. An edge write -//! runs as a Calvin transaction at that seam instead -//! (`planner::calvin::edge_sequencing`). +//! record. The proposing node publishes the change event. An edge or label +//! write runs as a Calvin transaction at that seam instead +//! (`planner::calvin::graph_sequencing`). //! - A write inside a transaction block, or a plan with no replicated form: //! the write enters the funnel with `WalDurability::AppendHere`. The funnel //! appends the redo record under the write-admission guard, inside the diff --git a/nodedb/src/control/server/dispatch_utils/submit_write/funnel/dispatch.rs b/nodedb/src/control/server/dispatch_utils/submit_write/funnel/dispatch.rs index 8613f744a..8aac52cbe 100644 --- a/nodedb/src/control/server/dispatch_utils/submit_write/funnel/dispatch.rs +++ b/nodedb/src/control/server/dispatch_utils/submit_write/funnel/dispatch.rs @@ -49,9 +49,10 @@ pub(super) struct DispatchTarget { pub resolved_now_ms: Option, /// The write's commit HLC, which dates every event it emits. pub commit_hlc: u64, - /// The log position of the data-group entry the write applies, its - /// version on every replica. `None` for a write that applies no entry. - pub entry_version: Option, + /// The data-group entry the write applies: its version and the + /// system-time ordinal its leader assigned, alike on every replica. + /// `None` for a write that applies no entry. + pub entry_version: Option, pub admission: Admission, /// The record group the write journals its write set into. pub journal: Option, diff --git a/nodedb/src/control/server/dispatch_utils/submit_write/funnel/driver.rs b/nodedb/src/control/server/dispatch_utils/submit_write/funnel/driver.rs index 9e0ad428f..d89e69ef0 100644 --- a/nodedb/src/control/server/dispatch_utils/submit_write/funnel/driver.rs +++ b/nodedb/src/control/server/dispatch_utils/submit_write/funnel/driver.rs @@ -7,6 +7,7 @@ use std::sync::Arc; use crate::bridge::dispatch::JournalGroup; +use crate::bridge::envelope::AppliedEntry; use crate::control::server::dispatch_utils::change_events::{ PendingChanges, extract_write_change_set, }; @@ -397,10 +398,9 @@ pub(crate) async fn enqueue_write( wal_lsn, resolved_now_ms, commit_hlc, - // A write that applies a data-group entry versions its rows by - // the entry's position, which every replica shares. - entry_version: wal_append_outcome.change_position.map(|position| { - nodedb_types::WriteVersion::logged(position.epoch, position.log_index) + // An entry's position versions its rows, its leader's ordinal stamps them. + entry_version: wal_append_outcome.change_position.map(|at| { + AppliedEntry::logged(at.epoch, at.log_index, wal_append_outcome.system_ordinal) }), admission, journal, diff --git a/nodedb/src/control/server/dispatch_utils/submit_write/funnel/pending.rs b/nodedb/src/control/server/dispatch_utils/submit_write/funnel/pending.rs index 8361a1dea..cddad0858 100644 --- a/nodedb/src/control/server/dispatch_utils/submit_write/funnel/pending.rs +++ b/nodedb/src/control/server/dispatch_utils/submit_write/funnel/pending.rs @@ -93,6 +93,7 @@ fn timeseries_retry(params: &SubmitWrite) -> Option { apply_key: 0, commit_hlc, change_position: None, + system_ordinal: None, } = ¶ms.durability else { return None; @@ -123,6 +124,7 @@ fn timeseries_retry(params: &SubmitWrite) -> Option { apply_key: 0, commit_hlc: *commit_hlc, change_position: None, + system_ordinal: None, }, ordering: WriteOrdering::Gate, change_feed, diff --git a/nodedb/src/control/server/dispatch_utils/submit_write/funnel/wal_append.rs b/nodedb/src/control/server/dispatch_utils/submit_write/funnel/wal_append.rs index b2afe6892..da2a219ee 100644 --- a/nodedb/src/control/server/dispatch_utils/submit_write/funnel/wal_append.rs +++ b/nodedb/src/control/server/dispatch_utils/submit_write/funnel/wal_append.rs @@ -26,6 +26,8 @@ pub(super) struct WalAppendOutcome { pub group_origin: Option, /// The replicated position whose marker precedes the write's records. pub change_position: Option, + /// The system-time ordinal the leader of the applied entry assigned. + pub system_ordinal: Option, } /// Where and how [`authorize_and_append`] journals one write. @@ -88,11 +90,13 @@ pub(super) fn authorize_and_append( }); } - let marked_position = match &durability { + let (marked_position, system_ordinal) = match &durability { WalDurability::AppendHere { - change_position, .. - } => *change_position, - WalDurability::CallerSupplied { .. } => None, + change_position, + system_ordinal, + .. + } => (*change_position, *system_ordinal), + WalDurability::CallerSupplied { .. } => (None, None), }; let (wal_lsn, resolved_now_ms, group_origin) = match durability { WalDurability::AppendHere { @@ -103,6 +107,14 @@ pub(super) fn authorize_and_append( } => { // The marker precedes the redo in the WAL, so the fsync that makes // the redo durable makes the marker durable too. + // The rows' system-time ordinals are fixed here, under the guard + // and before the record, so the record carries them: the run the + // applied entry's leader assigned, else a run of this node's clock. + wal_dispatch::stamp_system_ordinals( + &mut plan, + &shared.wal.ordinal_clock(), + system_ordinal, + ); if let Some(position) = change_position { let marker = ChangePositionMarker { apply_key, @@ -166,6 +178,7 @@ pub(super) fn authorize_and_append( resolved_now_ms, group_origin, change_position: marked_position, + system_ordinal, }) } diff --git a/nodedb/src/control/server/dispatch_utils/submit_write/params.rs b/nodedb/src/control/server/dispatch_utils/submit_write/params.rs index 791fd34f9..489c2d1b7 100644 --- a/nodedb/src/control/server/dispatch_utils/submit_write/params.rs +++ b/nodedb/src/control/server/dispatch_utils/submit_write/params.rs @@ -36,11 +36,17 @@ pub(crate) enum WalDurability { /// write applies, `None` for a write no entry carries. The funnel makes it /// durable ahead of the redo and records it for the CDC router, so every /// replica positions the write's change events alike. + /// + /// `system_ordinal` is the system-time ordinal the leader of that entry + /// assigned, `None` for a write no entry carries. Every replica stamps + /// the write's bitemporal versions from it, so every replica writes the + /// same version keys. AppendHere { now_override: Option, apply_key: u64, commit_hlc: Option, change_position: Option, + system_ordinal: Option, }, /// The caller already recorded this write's durability elsewhere — COMMIT's /// single `Transaction` record, the procedural batch flush, a trigger / diff --git a/nodedb/src/control/server/dispatch_utils/write_abort.rs b/nodedb/src/control/server/dispatch_utils/write_abort.rs index 088202080..0fb27dfa4 100644 --- a/nodedb/src/control/server/dispatch_utils/write_abort.rs +++ b/nodedb/src/control/server/dispatch_utils/write_abort.rs @@ -61,6 +61,7 @@ fn is_transient_verdict(code: &ErrorCode) -> bool { | ErrorCode::ActiveSqlTransaction { .. } | ErrorCode::DependentObjectsExist { .. } | ErrorCode::NodeLabelLimit { .. } + | ErrorCode::ProgramLimitExceeded { .. } | ErrorCode::RejectedConstraint { .. } | ErrorCode::RejectedPrevalidation { .. } | ErrorCode::SyncRejected { .. } @@ -166,6 +167,8 @@ pub(crate) fn write_definitely_not_applied(code: &ErrorCode) -> bool { // A label write past the node-label cap is refused before any label of // the statement stays set. | ErrorCode::NodeLabelLimit { .. } + // A statement past a program limit is refused before it applies. + | ErrorCode::ProgramLimitExceeded { .. } // Expression evaluation failed before producing a value to write. | ErrorCode::DivisionByZero | ErrorCode::UndefinedFunction { .. } diff --git a/nodedb/src/control/server/exchange/all_cores/dispatch.rs b/nodedb/src/control/server/exchange/all_cores/dispatch.rs index 44addceb4..b1c697053 100644 --- a/nodedb/src/control/server/exchange/all_cores/dispatch.rs +++ b/nodedb/src/control/server/exchange/all_cores/dispatch.rs @@ -43,9 +43,10 @@ pub(crate) async fn execute_plan_all_local_cores( match &plan { PhysicalPlan::Graph(g) => match g { // ── MATCH / MatchContinuation ───────────────────────────────────── - GraphOp::Match { .. } - | GraphOp::MatchContinuation { .. } - | GraphOp::MatchVarLenResume { .. } => { + GraphOp::Match { scope, .. } + | GraphOp::MatchContinuation { scope, .. } + | GraphOp::MatchVarLenResume { scope, .. } => { + let scope = *scope; use crate::control::server::graph_dispatch::match_broadcast::broadcast_match_to_all_cores; use crate::data::executor::handlers::graph_match::encode_match_envelope_raw; @@ -63,6 +64,7 @@ pub(crate) async fn execute_plan_all_local_cores( crate::control::server::graph_dispatch::GraphRead { txn_id, linearizable: false, + scope, }, ) .await?; @@ -111,7 +113,6 @@ pub(crate) async fn execute_plan_all_local_cores( | GraphOp::SetNodeLabels { .. } | GraphOp::RemoveNodeLabels { .. } | GraphOp::TemporalNeighbors { .. } - | GraphOp::TemporalAlgorithm { .. } | GraphOp::Stats { .. } | GraphOp::NodeEdgeGuard { .. } | GraphOp::NodePresenceGuard { .. } @@ -150,6 +151,11 @@ pub(crate) async fn execute_plan_all_local_cores( MetaOp::TransactionBatch { .. } => Err(fanned_scoped_plan("TransactionBatch")), MetaOp::ApplyTransactionRedo { .. } => Err(fanned_scoped_plan("ApplyTransactionRedo")), MetaOp::RestoreRedo(_) => Err(fanned_scoped_plan("RestoreRedo")), + MetaOp::PurgeDepartedVShards { .. } => Err(crate::Error::Internal { + detail: "PurgeDepartedVShards reached the all-core fan; the departure purge \ + sends it to every core itself" + .into(), + }), // One image per core, kept apart by core id. A merged payload // loses which core each image belongs to. // Each core answers the probes of the vShards it owns. @@ -216,6 +222,7 @@ pub(crate) async fn execute_plan_all_local_cores( | MetaOp::RebuildIndex { .. } | MetaOp::PutSynonymGroup { .. } | MetaOp::DeleteSynonymGroup { .. } + | MetaOp::SetCustomCrsSet { .. } | MetaOp::CalvinDrop { .. } => { generic_gather(state, tenant_id, database_id, plan, trace_id, txn_id).await } diff --git a/nodedb/src/control/server/exchange/all_cores/fanout.rs b/nodedb/src/control/server/exchange/all_cores/fanout.rs index 089a123c1..e8e80cd44 100644 --- a/nodedb/src/control/server/exchange/all_cores/fanout.rs +++ b/nodedb/src/control/server/exchange/all_cores/fanout.rs @@ -102,7 +102,6 @@ async fn dispatch_all_cores( | GraphOp::SetNodeLabels { .. } | GraphOp::RemoveNodeLabels { .. } | GraphOp::TemporalNeighbors { .. } - | GraphOp::TemporalAlgorithm { .. } | GraphOp::Stats { .. } | GraphOp::NodeEdgeGuard { .. } | GraphOp::NodePresenceGuard { .. } diff --git a/nodedb/src/control/server/exchange/all_cores/mod.rs b/nodedb/src/control/server/exchange/all_cores/mod.rs index 0fd000c60..f2823bfe1 100644 --- a/nodedb/src/control/server/exchange/all_cores/mod.rs +++ b/nodedb/src/control/server/exchange/all_cores/mod.rs @@ -33,7 +33,7 @@ mod bsp; mod dispatch; mod fanout; mod home_versions; -mod read_cut; +pub(crate) mod read_cut; mod snapshot; mod wcc; diff --git a/nodedb/src/control/server/exchange/all_cores/read_cut.rs b/nodedb/src/control/server/exchange/all_cores/read_cut.rs index 879d20548..624a22075 100644 --- a/nodedb/src/control/server/exchange/all_cores/read_cut.rs +++ b/nodedb/src/control/server/exchange/all_cores/read_cut.rs @@ -29,9 +29,19 @@ use crate::control::state::SharedState; /// leader change can drop a proposed marker. const MARKER_RETRY: Duration = Duration::from_secs(1); -/// Resolve the read cut of a BSP or WCC `plan` on this node. A plan that -/// carries a cut marker gets its cut in `system_as_of`. Returns the cut the -/// plan's cores read at. A plan with neither a marker nor a cut is an error. +/// Resolve the read cut of a BSP or WCC `plan` on this node. Returns the cut +/// the plan's cores read at. A plan with neither a marker nor a cut is an +/// error. +/// +/// - A live run carries a marker and no cut. Its cut is the marker's cut, a +/// pure Calvin cut, so every node resolves the same one. +/// - A historical run (`AS OF SYSTEM TIME`) carries its statement's pinned +/// ordinal and a marker. The ordinal can sit above the marker's cut: the +/// statement's cap holds a clock part for writes outside Calvin. The node +/// waits for the marker, so every Calvin version sequenced before it is +/// installed, folds the ordinal into its ordinal clock, so no write it +/// stamps later lands at or below the cut, and reads at the ordinal +/// unchanged. pub(super) async fn resolve_plan_cut( state: &SharedState, plan: &mut PhysicalPlan, @@ -50,7 +60,11 @@ pub(super) async fn resolve_plan_cut( } }; if *marker != 0 { - *system_as_of = Some(resolve_read_cut(state, *marker).await?); + let cut = resolve_read_cut(state, *marker).await?; + match *system_as_of { + Some(pinned) => state.wal.ordinal_clock().update_from_remote(pinned), + None => *system_as_of = Some(cut), + } *marker = 0; } system_as_of.ok_or_else(|| crate::Error::Internal { @@ -61,10 +75,20 @@ pub(super) async fn resolve_plan_cut( } /// Propose the cut marker `marker` and wait until it applied here and every -/// local Calvin scheduler passed it. Returns the cut as a system-time -/// ordinal: `0` when no epoch applied before the marker, so no Calvin edge -/// version is visible. -async fn resolve_read_cut(state: &SharedState, marker: u64) -> crate::Result { +/// local Calvin scheduler passed it, within the running statement's +/// deadline. Returns the cut as a system-time ordinal: `0` when no epoch +/// applied before the marker, so no Calvin edge version is visible. +pub(crate) async fn resolve_read_cut(state: &SharedState, marker: u64) -> crate::Result { + resolve_read_cut_by(state, marker, StatementDeadline::of_statement(state)).await +} + +/// [`resolve_read_cut`] bounded by `statement`. A marker that did not apply +/// and pass every local scheduler by then is `Error::DeadlineExceeded`. +pub(crate) async fn resolve_read_cut_by( + state: &SharedState, + marker: u64, + statement: StatementDeadline, +) -> crate::Result { let proposer = state .calvin .sequencer_proposer @@ -82,7 +106,6 @@ async fn resolve_read_cut(state: &SharedState, marker: u64) -> crate::Result crate::Result= deadline { - return Err(crate::Error::Internal { - detail: format!( - "graph read cut: cut marker {marker} did not apply and pass every Calvin \ - scheduler of this node within the statement deadline ({} ms) (vShards \ - still before it: {:?}, last marker refusal: {}). Retry the query", - budget_ms, - state.calvin.cuts.lagging(marker), - last_refusal.as_deref().unwrap_or("none"), - ), + tracing::warn!( + marker, + budget_ms, + lagging = ?state.calvin.cuts.lagging(marker), + last_refusal = last_refusal.as_deref().unwrap_or("none"), + "graph read cut: the cut marker did not apply and pass every Calvin scheduler \ + of this node within the statement deadline" + ); + return Err(crate::Error::DeadlineExceeded { + request_id: crate::types::RequestId::new(0), }); } } diff --git a/nodedb/src/control/server/exchange/all_cores/snapshot.rs b/nodedb/src/control/server/exchange/all_cores/snapshot.rs index 5eef3a328..a1aa55d92 100644 --- a/nodedb/src/control/server/exchange/all_cores/snapshot.rs +++ b/nodedb/src/control/server/exchange/all_cores/snapshot.rs @@ -122,6 +122,8 @@ pub(super) fn merge_core_snapshots( edge_applied, tenant_edge_cuts, tenant_edge_applied, + node_labels, + node_label_versions, group_write_marks, group_proposal_keys, group_proposal_keys_complete_from, @@ -145,6 +147,8 @@ pub(super) fn merge_core_snapshots( merged.edge_applied.extend(edge_applied); merged.tenant_edge_cuts.extend(tenant_edge_cuts); merged.tenant_edge_applied.extend(tenant_edge_applied); + merged.node_labels.extend(node_labels); + merged.node_label_versions.extend(node_label_versions); merged.documents.extend(documents); merged.vector_multi_documents.extend(vector_multi_documents); merged.indexes.extend(indexes); @@ -181,6 +185,15 @@ pub(super) fn merge_core_snapshots( dedup_edges(&mut merged.tenant_edge_applied, |(db, tid, key, _)| { (*db, *tid, key.clone()) }); + // A node's labels live on its home core alone. A version both report + // is kept once all the same. + dedup_edges(&mut merged.node_labels, |(db, tid, node, _)| { + (*db, *tid, node.clone()) + }); + dedup_edges( + &mut merged.node_label_versions, + |(db, tid, node, ordinal, _)| (*db, *tid, node.clone(), *ordinal), + ); // Every core that holds a vShard of the collection records the cut. dedup_edges(&mut merged.edge_cuts, Clone::clone); dedup_edges(&mut merged.tenant_edge_cuts, Clone::clone); diff --git a/nodedb/src/control/server/exchange/cluster_leaf.rs b/nodedb/src/control/server/exchange/cluster_leaf.rs index 0135c2838..6cb07cefa 100644 --- a/nodedb/src/control/server/exchange/cluster_leaf.rs +++ b/nodedb/src/control/server/exchange/cluster_leaf.rs @@ -246,6 +246,7 @@ mod tests { system_time: nodedb_types::SystemTimeScope::Current, valid_at_ms: None, zone_msgpack: Vec::new(), + physical: false, } } @@ -257,9 +258,10 @@ mod tests { group_by_dim: -1, slice_tile_keys: Vec::new(), prefix_bits: 0, - system_as_of: None, + system_time: nodedb_types::SystemTimeScope::Current, valid_at_ms: None, region_msgpack: Vec::new(), + physical: false, } } @@ -329,7 +331,7 @@ mod tests { op: nodedb_physical::physical_plan::ArrayBinaryOp::Add, attr_idx: 0, prefix_bits: 0, - system_as_of: None, + system_time: nodedb_types::SystemTimeScope::Current, valid_at_ms: None, }; let rows = vec![row(1), row(2)]; diff --git a/nodedb/src/control/server/exchange/resolve/exchange/post_process_arm.rs b/nodedb/src/control/server/exchange/resolve/exchange/post_process_arm.rs index 7ab00b1a9..117773a45 100644 --- a/nodedb/src/control/server/exchange/resolve/exchange/post_process_arm.rs +++ b/nodedb/src/control/server/exchange/resolve/exchange/post_process_arm.rs @@ -287,7 +287,12 @@ pub(super) async fn materialize_child_rows( HitShape::Vector => { let merged = outcome.merged_rows(as_aggregate)?; let merged = match &hit_cut { - Some((rls_filters, top_k)) => filter_vector_hits(&merged, rls_filters, *top_k)?, + Some((rls_filters, top_k)) => filter_vector_hits( + &merged, + rls_filters, + *top_k, + &crate::control::crs_context::crs_context_for(state), + )?, None => merged, }; // Every surrogate the flatten can ask for resolves in one batch. diff --git a/nodedb/src/control/server/graph_dispatch/bfs.rs b/nodedb/src/control/server/graph_dispatch/bfs.rs index 77104e945..2441cfb05 100644 --- a/nodedb/src/control/server/graph_dispatch/bfs.rs +++ b/nodedb/src/control/server/graph_dispatch/bfs.rs @@ -13,6 +13,8 @@ use std::collections::HashSet; +use nodedb_physical::physical_plan::GraphReadScope; + use crate::bridge::envelope::Response; use crate::control::state::SharedState; use crate::engine::graph::traversal_options::GraphTraversalOptions; @@ -40,6 +42,8 @@ pub struct CrossCoreBfsParams<'a> { pub linearizable: bool, /// The session's transaction. The walk sees its staged edge writes. pub txn_id: Option, + /// The bitemporal cut every hop of the walk reads. + pub read_scope: GraphReadScope, } /// Cross-core BFS with explicit traversal options. @@ -66,6 +70,7 @@ pub async fn cross_core_bfs_with_options( options, linearizable, txn_id, + read_scope, } = params; let cap = walk_visit_cap(shared, options); let whole = whole_hop(); @@ -85,6 +90,8 @@ pub async fn cross_core_bfs_with_options( options: &whole, linearizable, txn_id, + collection, + read_scope, }, &frontier, &mut presence_reads, @@ -115,6 +122,7 @@ pub async fn cross_core_bfs_with_options( edge_predicate: &[], with_properties: false, txn_id, + read_scope, }, ) .await?; diff --git a/nodedb/src/control/server/graph_dispatch/bsp_pagerank/coord.rs b/nodedb/src/control/server/graph_dispatch/bsp_pagerank/coord.rs index 6ad7529de..5998230bf 100644 --- a/nodedb/src/control/server/graph_dispatch/bsp_pagerank/coord.rs +++ b/nodedb/src/control/server/graph_dispatch/bsp_pagerank/coord.rs @@ -22,7 +22,7 @@ //! graph node is homed on exactly one owner node. The count phase carries //! a fresh cut marker. Every node resolves the run's read cut from it and //! answers it, and every later superstep reads at that one cut (see -//! `graph_dispatch::run_cut`). A write during the run is above the cut, so +//! `read_cut::run_marker`). A write during the run is above the cut, so //! the rank set stays the nodes that existed at the cut. //! 2. **Initial scatter.** Superstep 0 sets every node's initial rank and //! returns the contributions it scatters. It changes no rank, so it is not @@ -53,7 +53,7 @@ use crate::types::{DatabaseId, TenantId}; use super::enumerate::enumerate_shards; use super::scatter::{ScatterSuperstepParams, ShardDispatch, ShardResult, scatter_superstep}; -use crate::control::server::graph_dispatch::run_cut::{agreed_cut, new_cut_marker}; +use crate::control::read_cut::{agreed_cut, new_cut_marker}; use crate::control::server::graph_dispatch::shard_reads::{ShardReadLog, qualified}; /// Default max supersteps when the query carries no explicit `ITERATIONS`. @@ -85,6 +85,8 @@ struct Carry { /// /// Caller guarantees cluster mode (`cluster_routing.is_some()`) and /// `algorithm == PageRank`; single-node / other algorithms never enter here. +/// `scope` is the run's bitemporal cut: a historical run reads every +/// superstep at the scope's pinned ordinal and at its valid time. pub async fn run_bsp_pagerank( state: &SharedState, tenant_id: TenantId, @@ -92,6 +94,7 @@ pub async fn run_bsp_pagerank( params: AlgoParams, deadline_ms: u64, linearizable: bool, + scope: nodedb_physical::physical_plan::GraphReadScope, ) -> crate::Result { let algorithm = GraphAlgorithm::PageRank; @@ -145,7 +148,8 @@ pub async fn run_bsp_pagerank( global_dangling: 0.0, personalization_sum: 0.0, read_cut_marker, - system_as_of: None, + // A historical run reads at its statement's pinned ordinal. + system_as_of: scope.pinned_ordinal(), }) .collect(); let counts = scatter_superstep( @@ -160,6 +164,7 @@ pub async fn run_bsp_pagerank( dispatches: count_dispatches, deadline_ms, linearizable, + valid_at_ms: scope.valid_at_ms, }, ) .await?; @@ -248,6 +253,7 @@ pub async fn run_bsp_pagerank( global_n, dispatches, deadline_ms, + valid_at_ms: scope.valid_at_ms, linearizable, }, ) diff --git a/nodedb/src/control/server/graph_dispatch/bsp_pagerank/scatter.rs b/nodedb/src/control/server/graph_dispatch/bsp_pagerank/scatter.rs index 9ecb5432e..8974dc3b0 100644 --- a/nodedb/src/control/server/graph_dispatch/bsp_pagerank/scatter.rs +++ b/nodedb/src/control/server/graph_dispatch/bsp_pagerank/scatter.rs @@ -74,6 +74,8 @@ pub(super) struct ScatterSuperstepParams<'a> { pub(super) dispatches: Vec, pub(super) deadline_ms: u64, pub(super) linearizable: bool, + /// The valid-time instant every superstep reads at, or `None`. + pub(super) valid_at_ms: Option, } /// Dispatch one `BspSuperstep` to every owner node concurrently and decode each @@ -93,6 +95,7 @@ pub(super) async fn scatter_superstep( dispatches, deadline_ms, linearizable, + valid_at_ms, } = args; let shared_arc = gateway_shared(state)?; let version_set = GatewayVersionSet::from_pairs(Vec::new()); @@ -112,6 +115,7 @@ pub(super) async fn scatter_superstep( personalization_sum: d.personalization_sum, read_cut_marker: d.read_cut_marker, system_as_of: d.system_as_of, + valid_at_ms, }))); let version_set = version_set.clone(); let node_id = d.node_id; diff --git a/nodedb/src/control/server/graph_dispatch/bsp_wcc/coord.rs b/nodedb/src/control/server/graph_dispatch/bsp_wcc/coord.rs index 3a1c80607..ba56c6186 100644 --- a/nodedb/src/control/server/graph_dispatch/bsp_wcc/coord.rs +++ b/nodedb/src/control/server/graph_dispatch/bsp_wcc/coord.rs @@ -9,7 +9,7 @@ //! set. Reuses `bsp_pagerank::enumerate::enumerate_shards`. //! 2. **Contract.** Dispatch ONE `GraphOp::WccSuperstep` per owner node, with //! one fresh cut marker. Every node resolves the same read cut from it and -//! reads the graph at it (see `graph_dispatch::run_cut`). Each +//! reads the graph at it (see `read_cut::run_marker`). Each //! shard contracts its OWNED nodes into local components and returns //! `node_labels` (`(name, local_component_root_name)`) plus `boundary_edges` //! (`(owned_name, ghost_name)` for every owned→ghost out-edge). @@ -31,7 +31,7 @@ use crate::engine::graph::algo::result::AlgoResultBatch; use crate::types::{DatabaseId, TenantId}; use super::scatter::{WccRound, scatter_wcc_round}; -use crate::control::server::graph_dispatch::run_cut::{agreed_cut, new_cut_marker}; +use crate::control::read_cut::{agreed_cut, new_cut_marker}; use crate::control::server::graph_dispatch::shard_reads::{ShardReadLog, qualified}; /// Run distributed WCC and return the bare `AlgoResultBatch` payload (the exact @@ -40,6 +40,9 @@ use crate::control::server::graph_dispatch::shard_reads::{ShardReadLog, qualifie /// /// Caller guarantees cluster mode (`cluster_routing.is_some()`) and /// `algorithm == Wcc`; single-node / other algorithms never enter here. +/// +/// `scope` is the round's bitemporal cut: a historical round reads at +/// the scope's pinned ordinal and at its valid time. pub async fn run_bsp_wcc( state: &SharedState, tenant_id: TenantId, @@ -47,6 +50,7 @@ pub async fn run_bsp_wcc( params: AlgoParams, deadline_ms: u64, linearizable: bool, + scope: nodedb_physical::physical_plan::GraphReadScope, ) -> crate::Result { // ── Enumerate shards (one per distinct owner node, local + remote). ── let enumeration = enumerate_shards(state)?; @@ -66,6 +70,8 @@ pub async fn run_bsp_wcc( params: ¶ms, targets: &targets, read_cut_marker: new_cut_marker(state), + system_as_of_cap: scope.pinned_ordinal(), + valid_at_ms: scope.valid_at_ms, deadline_ms, linearizable, }, diff --git a/nodedb/src/control/server/graph_dispatch/bsp_wcc/scatter.rs b/nodedb/src/control/server/graph_dispatch/bsp_wcc/scatter.rs index 0231c040e..37fcb32ec 100644 --- a/nodedb/src/control/server/graph_dispatch/bsp_wcc/scatter.rs +++ b/nodedb/src/control/server/graph_dispatch/bsp_wcc/scatter.rs @@ -40,6 +40,11 @@ pub(super) struct WccRound<'a> { pub(super) targets: &'a [ShardTarget], /// The cut marker every node resolves the round's read cut from. pub(super) read_cut_marker: u64, + /// A historical round's pinned ordinal: every node waits for the marker + /// and reads at it unchanged. + pub(super) system_as_of_cap: Option, + /// The valid-time instant the round reads at, or `None`. + pub(super) valid_at_ms: Option, pub(super) deadline_ms: u64, pub(super) linearizable: bool, } @@ -57,6 +62,8 @@ pub(super) async fn scatter_wcc_round( params, targets, read_cut_marker, + system_as_of_cap, + valid_at_ms, deadline_ms, linearizable, } = round; @@ -72,7 +79,8 @@ pub(super) async fn scatter_wcc_round( owned_vshards: t.owned_vshards.clone(), // Every node resolves the same read cut from the marker. read_cut_marker, - system_as_of: None, + system_as_of: system_as_of_cap, + valid_at_ms, }))); let version_set = version_set.clone(); let node_id = t.node_id; diff --git a/nodedb/src/control/server/graph_dispatch/cluster_resolve.rs b/nodedb/src/control/server/graph_dispatch/cluster_resolve.rs index 47a4fe7d1..f6b3d9350 100644 --- a/nodedb/src/control/server/graph_dispatch/cluster_resolve.rs +++ b/nodedb/src/control/server/graph_dispatch/cluster_resolve.rs @@ -69,6 +69,15 @@ pub(in crate::control::server::graph_dispatch) async fn dispatch_superstep_to_no if linearizable { super::read_groups::confirm_graph_read(shared_arc, database_id, &plan).await?; } + // A historical read runs only once this node installed its cut. + crate::control::read_cut::await_read_cut( + shared_arc, + &plan, + crate::control::server::shared::session::StatementDeadline::after( + std::time::Duration::from_millis(deadline_ms), + ), + ) + .await?; let node_result = execute_plan_all_local_cores( shared_arc.as_ref(), tenant_id, diff --git a/nodedb/src/control/server/graph_dispatch/hop.rs b/nodedb/src/control/server/graph_dispatch/hop.rs index 099839ef3..d64e3a90b 100644 --- a/nodedb/src/control/server/graph_dispatch/hop.rs +++ b/nodedb/src/control/server/graph_dispatch/hop.rs @@ -64,7 +64,7 @@ use crate::control::state::SharedState; use crate::engine::graph::edge_store::Direction; use crate::engine::graph::traversal_options::GraphTraversalOptions; use crate::types::{DatabaseId, TenantId, TraceId, TxnId, VShardId}; -use nodedb_physical::physical_plan::GraphOp; +use nodedb_physical::physical_plan::{GraphOp, GraphReadScope}; use nodedb_types::filter::MetadataFilter; use super::neighbor_rows::{NeighborRow, decode_neighbor_rows}; @@ -107,6 +107,9 @@ pub(super) struct NeighborHopParams<'a> { pub with_properties: bool, /// The session's transaction. Its staged edge writes merge into the hop. pub txn_id: Option, + /// The bitemporal cut the hop reads. Every hop of a walk carries the + /// same one. + pub read_scope: GraphReadScope, } /// Execute one hop of BFS from `params.frontier`. @@ -127,6 +130,7 @@ pub(super) async fn execute_neighbor_hop( edge_predicate, with_properties, txn_id, + read_scope, } = params; // Cap this hop's handler-side allocation to the remaining budget under @@ -151,6 +155,7 @@ pub(super) async fn execute_neighbor_hop( edge_predicate, with_properties, txn_id, + read_scope, }; // Single-node mode: no routing table — every frontier node is local. @@ -287,6 +292,8 @@ struct ExpandScope<'a> { with_properties: bool, /// The session's transaction, whose staged edge writes merge in. txn_id: Option, + /// The bitemporal cut the hop reads. + read_scope: GraphReadScope, } impl ExpandScope<'_> { @@ -303,6 +310,7 @@ impl ExpandScope<'_> { rls_filters: Vec::new(), edge_predicate: self.edge_predicate.to_vec(), with_properties: self.with_properties, + scope: self.read_scope, }) } } @@ -319,6 +327,13 @@ async fn expand_local( if scope.linearizable { super::read_groups::confirm_graph_read(shared, scope.database_id, &plan).await?; } + // A historical hop reads only once this node installed its cut. + crate::control::read_cut::await_read_cut( + shared, + &plan, + crate::control::server::shared::session::StatementDeadline::of_statement(shared), + ) + .await?; let resp = crate::control::server::broadcast::broadcast_to_all_cores_txn( shared, scope.tenant_id, diff --git a/nodedb/src/control/server/graph_dispatch/keyed_read.rs b/nodedb/src/control/server/graph_dispatch/keyed_read.rs index 19430b1fc..874bddcff 100644 --- a/nodedb/src/control/server/graph_dispatch/keyed_read.rs +++ b/nodedb/src/control/server/graph_dispatch/keyed_read.rs @@ -76,6 +76,12 @@ pub async fn read_on_vshard( if linearizable { super::read_groups::confirm_graph_read(state, database_id, &plan).await?; } + crate::control::read_cut::await_read_cut( + state, + &plan, + crate::control::server::shared::session::StatementDeadline::of_statement(state), + ) + .await?; let response = crate::control::server::broadcast::broadcast_to_all_cores_txn( state, tenant_id, diff --git a/nodedb/src/control/server/graph_dispatch/match_broadcast.rs b/nodedb/src/control/server/graph_dispatch/match_broadcast.rs index 8875bef47..e32e21d60 100644 --- a/nodedb/src/control/server/graph_dispatch/match_broadcast.rs +++ b/nodedb/src/control/server/graph_dispatch/match_broadcast.rs @@ -53,6 +53,8 @@ pub struct GraphRead { /// The read observes every write committed before it began: each node /// that serves part of it confirms its groups first. pub linearizable: bool, + /// The bitemporal cut every plan of the read carries. + pub scope: nodedb_physical::physical_plan::GraphReadScope, } /// Result of a MATCH cross-core broadcast after envelope unwrapping. @@ -219,6 +221,7 @@ pub async fn broadcast_match_to_all_cores( let GraphRead { txn_id, linearizable, + .. } = read; // A linearizable read confirms the groups the plan reads first. A MATCH // walks the local CSR, so that is every group this node replicates. A @@ -226,6 +229,13 @@ pub async fn broadcast_match_to_all_cores( if linearizable { super::read_groups::confirm_graph_read(state, database_id, &plan).await?; } + // A historical MATCH reads only once this node installed its cut. + crate::control::read_cut::await_read_cut( + state, + &plan, + crate::control::server::shared::session::StatementDeadline::of_statement(state), + ) + .await?; // Shared broadcast call counter (parity with the generic gather path). crate::control::server::broadcast::broadcast_call_count_increment(); diff --git a/nodedb/src/control/server/graph_dispatch/match_scatter/round_loop.rs b/nodedb/src/control/server/graph_dispatch/match_scatter/round_loop.rs index 8f1661faf..667663d89 100644 --- a/nodedb/src/control/server/graph_dispatch/match_scatter/round_loop.rs +++ b/nodedb/src/control/server/graph_dispatch/match_scatter/round_loop.rs @@ -184,6 +184,7 @@ pub(super) async fn dispatch_continuations( partial_row, source_node: cont.start_node.clone(), source_binding: cont.start_binding.clone(), + scope: read.scope, }); push_dispatch_fut(&mut futs, &ctx, plan, remote_coords); } @@ -239,6 +240,7 @@ pub(super) async fn dispatch_resumes( let plan = PhysicalPlan::Graph(GraphOp::MatchVarLenResume { query: query_bytes.to_vec(), resume: resume_bytes, + scope: read.scope, }); push_dispatch_fut(&mut futs, &ctx, plan, remote_coords); } diff --git a/nodedb/src/control/server/graph_dispatch/match_scatter/round_zero.rs b/nodedb/src/control/server/graph_dispatch/match_scatter/round_zero.rs index 288545410..81421aa61 100644 --- a/nodedb/src/control/server/graph_dispatch/match_scatter/round_zero.rs +++ b/nodedb/src/control/server/graph_dispatch/match_scatter/round_zero.rs @@ -58,6 +58,7 @@ pub(super) async fn scatter_round_zero( let GraphRead { txn_id, linearizable, + scope, } = read; // Local cores: fan to all and unwrap each `{rows, frontier}` envelope. The // active `txn_id` is threaded onto this LOCAL leg so each core merges the @@ -71,6 +72,7 @@ pub(super) async fn scatter_round_zero( query: query_bytes.to_vec(), frontier_bitmap: None, cluster_mode: true, + scope, }); let local_fut = broadcast_match_to_all_cores( state, @@ -93,6 +95,7 @@ pub(super) async fn scatter_round_zero( query: query_bytes.to_vec(), frontier_bitmap: None, cluster_mode: true, + scope, }); let route = TaskRoute { plan, diff --git a/nodedb/src/control/server/graph_dispatch/mod.rs b/nodedb/src/control/server/graph_dispatch/mod.rs index fd3ceaf72..6fab44ccc 100644 --- a/nodedb/src/control/server/graph_dispatch/mod.rs +++ b/nodedb/src/control/server/graph_dispatch/mod.rs @@ -29,7 +29,7 @@ pub(crate) mod neighbor_rows; pub(crate) mod presence; pub mod rag_fusion; pub mod read_groups; -pub(crate) mod run_cut; +pub mod read_scope; pub(crate) mod shard_reads; pub mod shortest_path; pub(crate) mod subgraph_edges; @@ -47,6 +47,7 @@ pub use match_broadcast::{ pub use match_scatter::{MatchScatterOutcome, scatter_match}; pub use rag_fusion::serve_rag_plan; pub use read_groups::{confirm_graph_read, graph_read_groups}; +pub use read_scope::{check_walk_scope, graph_read_scope}; pub use shortest_path::{CrossCoreShortestPathParams, cross_core_shortest_path}; pub use traverse_subgraph::{CrossCoreTraverseSubgraphParams, cross_core_traverse_subgraph}; pub use walk_reads::serve_walk_plan; diff --git a/nodedb/src/control/server/graph_dispatch/presence.rs b/nodedb/src/control/server/graph_dispatch/presence.rs index dc955554a..5c2169b3a 100644 --- a/nodedb/src/control/server/graph_dispatch/presence.rs +++ b/nodedb/src/control/server/graph_dispatch/presence.rs @@ -6,9 +6,14 @@ //! a node that graph does not hold finds nothing, so `GRAPH PATH`, //! `GRAPH TRAVERSE` and the cross-core hop walk check their endpoints against //! this node set first. +//! +//! A historical walk reads a CSR of its own collection at its cut, so its +//! node set is the nodes an edge of that collection names at the cut. use std::collections::HashSet; +use nodedb_physical::physical_plan::GraphReadScope; + use crate::control::state::SharedState; use crate::engine::graph::edge_store::Direction; use crate::engine::graph::traversal_options::GraphTraversalOptions; @@ -26,6 +31,11 @@ pub(crate) struct PresenceScope<'a> { /// The session's transaction. Presence counts its staged edge puts and /// tombstones. pub txn_id: Option, + /// The walk's database-qualified collection. A historical presence + /// check reads it. + pub collection: Option<&'a str>, + /// The walk's bitemporal cut. + pub read_scope: GraphReadScope, } /// The nodes of `nodes` that exist in the graph: those with an edge of any @@ -43,7 +53,13 @@ pub(crate) async fn graph_nodes_present( scope.tenant_id, scope.database_id, NeighborHopParams { - collection: None, + // The live graph holds every collection's edges in one node + // space. A historical CSR holds the walk's collection alone. + collection: if scope.read_scope.is_live() { + None + } else { + scope.collection + }, frontier: nodes, edge_labels: &[], direction: Direction::Both, @@ -54,6 +70,7 @@ pub(crate) async fn graph_nodes_present( edge_predicate: &[], with_properties: false, txn_id: scope.txn_id, + read_scope: scope.read_scope, }, ) .await?; diff --git a/nodedb/src/control/server/graph_dispatch/rag_fusion/coord.rs b/nodedb/src/control/server/graph_dispatch/rag_fusion/coord.rs index bc1ba1349..12e181c7c 100644 --- a/nodedb/src/control/server/graph_dispatch/rag_fusion/coord.rs +++ b/nodedb/src/control/server/graph_dispatch/rag_fusion/coord.rs @@ -60,11 +60,15 @@ pub async fn serve_rag_plan( state.cluster_routing.as_ref()?; let PhysicalPlan::Graph(GraphOp::RagFusion { stage: RagStage::Local, + scope: read_scope, .. }) = plan else { return None; }; + if let Err(error) = super::super::read_scope::check_walk_scope(read_scope) { + return Some(Err(error)); + } Some( run( state, @@ -72,6 +76,7 @@ pub async fn serve_rag_plan( tenant_id, database_id, linearizable, + read_scope: *read_scope, }, plan, ) @@ -84,6 +89,8 @@ struct RagScope { tenant_id: TenantId, database_id: DatabaseId, linearizable: bool, + /// The bitemporal cut every hop of the expansion reads. + read_scope: nodedb_physical::physical_plan::GraphReadScope, } async fn run(state: &SharedState, scope: RagScope, plan: &PhysicalPlan) -> crate::Result { @@ -109,6 +116,7 @@ async fn run(state: &SharedState, scope: RagScope, plan: &PhysicalPlan) -> crate tenant_id, database_id, linearizable, + .. } = scope; let qualified = collection.as_str().to_owned(); let vshard = nodedb_types::CollectionKey::from_qualified(database_id, collection)? @@ -361,6 +369,7 @@ async fn walk_from_seeds( with_properties: false, // A RAG fusion plan carries no session transaction. txn_id: None, + read_scope: scope.read_scope, }, ) .await?; diff --git a/nodedb/src/control/server/graph_dispatch/read_groups.rs b/nodedb/src/control/server/graph_dispatch/read_groups.rs index 40fbc05f6..c21966102 100644 --- a/nodedb/src/control/server/graph_dispatch/read_groups.rs +++ b/nodedb/src/control/server/graph_dispatch/read_groups.rs @@ -174,6 +174,7 @@ mod tests { rls_filters: Vec::new(), edge_predicate: Vec::new(), with_properties: false, + scope: nodedb_physical::physical_plan::GraphReadScope::default(), }) } @@ -204,6 +205,7 @@ mod tests { query: Vec::new(), frontier_bitmap: None, cluster_mode: true, + scope: nodedb_physical::physical_plan::GraphReadScope::default(), }); let mut groups = graph_read_groups(&state, DatabaseId::DEFAULT, &plan).expect("read groups"); diff --git a/nodedb/src/control/server/graph_dispatch/read_scope.rs b/nodedb/src/control/server/graph_dispatch/read_scope.rs new file mode 100644 index 000000000..0b259e343 --- /dev/null +++ b/nodedb/src/control/server/graph_dispatch/read_scope.rs @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! The bitemporal cut of a graph read statement. + +use nodedb_physical::physical_plan::{ALL_VERSIONS_REFUSED, GraphReadScope, GraphSystemCut}; +use nodedb_sql::temporal::TemporalScope; +use nodedb_types::SystemTimeScope; + +use crate::control::planner::sql_plan_convert::valid_time_point; +use crate::control::state::SharedState; + +use crate::control::read_cut::pin_historical_cut; + +/// The `GraphReadScope` a statement's `AS OF` clauses select. A system time +/// pins one ordinal for the whole statement ([`pin_historical_cut`]), and +/// the scope carries the cut marker every serving node waits for +/// (`read_cut::wait`). +/// +/// `AS OF SYSTEM TIME NULL` is refused: a graph walk reads one graph, and +/// every system-time version of every edge is no graph. +pub async fn graph_read_scope( + state: &SharedState, + temporal: &TemporalScope, +) -> crate::Result { + let valid_at_ms = valid_time_point(temporal.valid_time, "graph")?; + let (system, cut_marker) = match temporal.system_time { + SystemTimeScope::Current => (GraphSystemCut::Live, 0), + SystemTimeScope::AsOf(ms) => { + let cut = pin_historical_cut(state, ms).await?; + (GraphSystemCut::AtOrdinal(cut.ordinal), cut.marker) + } + // A cut the statement already pinned reads as pinned. + SystemTimeScope::Pinned { + ordinal, + cut_marker, + } => (GraphSystemCut::AtOrdinal(ordinal), cut_marker), + SystemTimeScope::AllVersions => return Err(all_versions_refused()), + }; + Ok(GraphReadScope { + system, + valid_at_ms, + cut_marker, + }) +} + +/// Refuse a plan scope a graph read cannot run at. +pub fn check_walk_scope(scope: &GraphReadScope) -> crate::Result<()> { + match scope.system { + GraphSystemCut::AllVersions => Err(all_versions_refused()), + GraphSystemCut::Live | GraphSystemCut::AtOrdinal(_) => Ok(()), + } +} + +fn all_versions_refused() -> crate::Error { + crate::Error::BadRequest { + detail: ALL_VERSIONS_REFUSED.into(), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn all_versions_is_refused_on_a_walk() { + let scope = GraphReadScope { + system: GraphSystemCut::AllVersions, + valid_at_ms: None, + cut_marker: 0, + }; + assert!(matches!( + check_walk_scope(&scope), + Err(crate::Error::BadRequest { .. }) + )); + assert!(check_walk_scope(&GraphReadScope::default()).is_ok()); + } +} diff --git a/nodedb/src/control/server/graph_dispatch/shortest_path.rs b/nodedb/src/control/server/graph_dispatch/shortest_path.rs index 16b89fe5a..0944a00f9 100644 --- a/nodedb/src/control/server/graph_dispatch/shortest_path.rs +++ b/nodedb/src/control/server/graph_dispatch/shortest_path.rs @@ -36,6 +36,7 @@ use super::hop::{NeighborHopParams, execute_neighbor_hop}; use super::neighbor_rows::NeighborRow; use super::presence::{PresenceScope, graph_nodes_present}; use super::shard_reads::ShardReadLog; +use nodedb_physical::physical_plan::GraphReadScope; use nodedb_types::filter::MetadataFilter; /// Parameters for [`cross_core_shortest_path`]. @@ -63,6 +64,8 @@ pub struct CrossCoreShortestPathParams { pub edge_predicate: Vec, /// The session's transaction. The path sees its staged edge writes. pub txn_id: Option, + /// The bitemporal cut every hop of the search reads. + pub read_scope: GraphReadScope, } /// Node → the node it was reached from. An endpoint maps to itself. @@ -90,6 +93,7 @@ pub async fn cross_core_shortest_path( linearizable, edge_predicate, txn_id, + read_scope, } = params; let whole = whole_hop(); // One core finds no path when either endpoint is absent from its graph, @@ -104,6 +108,8 @@ pub async fn cross_core_shortest_path( options: &whole, linearizable, txn_id, + collection: collection.as_deref(), + read_scope, }, &[src.clone(), dst.clone()], &mut presence_reads, @@ -153,6 +159,7 @@ pub async fn cross_core_shortest_path( edge_predicate: &edge_predicate, with_properties: false, txn_id, + read_scope, }, ) .await?; diff --git a/nodedb/src/control/server/graph_dispatch/traverse_subgraph.rs b/nodedb/src/control/server/graph_dispatch/traverse_subgraph.rs index 56ed05afe..e05b7ea3c 100644 --- a/nodedb/src/control/server/graph_dispatch/traverse_subgraph.rs +++ b/nodedb/src/control/server/graph_dispatch/traverse_subgraph.rs @@ -14,6 +14,7 @@ use std::collections::HashSet; +use nodedb_physical::physical_plan::GraphReadScope; use nodedb_types::filter::MetadataFilter; use sonic_rs; @@ -38,8 +39,8 @@ struct WireNode<'a> { depth: u8, } -/// Wire-shape JSON edge entry. `properties` is the edge's current property -/// object, `{}` for an edge without properties. +/// Wire-shape JSON edge entry. `properties` is the edge's property object +/// at the walk's cut, `{}` for an edge without properties. #[derive(serde::Serialize)] struct WireEdge<'a> { from: &'a str, @@ -79,6 +80,8 @@ pub struct CrossCoreTraverseSubgraphParams<'a> { /// The session's transaction. The walk, its predicate and the returned /// properties see its staged edge writes. pub txn_id: Option, + /// The bitemporal cut every hop of the walk reads. + pub read_scope: GraphReadScope, } /// BFS that returns a `{nodes,edges}` JSON subgraph for `GRAPH TRAVERSE`. @@ -168,6 +171,7 @@ pub(crate) async fn walk_subgraph( edge_predicate, with_properties, txn_id, + read_scope, } = params; // Per-node depth: the start node is at depth 0. Each later node carries // the hop index that first surfaced it. @@ -189,6 +193,7 @@ pub(crate) async fn walk_subgraph( edge_predicate, with_properties, txn_id, + read_scope, }; // One core's subgraph is empty for a start its graph does not hold, as is @@ -205,6 +210,8 @@ pub(crate) async fn walk_subgraph( options: &whole, linearizable, txn_id, + collection: collection.as_deref(), + read_scope, }, std::slice::from_ref(&start), &mut presence_reads, @@ -278,6 +285,7 @@ struct WalkHop<'a> { edge_predicate: &'a [MetadataFilter], with_properties: bool, txn_id: Option, + read_scope: GraphReadScope, } impl WalkHop<'_> { @@ -309,6 +317,7 @@ impl WalkHop<'_> { edge_predicate: self.edge_predicate, with_properties: self.with_properties, txn_id: self.txn_id, + read_scope: self.read_scope, }, ) .await?; diff --git a/nodedb/src/control/server/graph_dispatch/walk_reads.rs b/nodedb/src/control/server/graph_dispatch/walk_reads.rs index 0c76b19de..dec473ebb 100644 --- a/nodedb/src/control/server/graph_dispatch/walk_reads.rs +++ b/nodedb/src/control/server/graph_dispatch/walk_reads.rs @@ -15,7 +15,7 @@ //! - `Path`: a msgpack array `[src, …, dst]`, or a `NotFound` refusal when //! `dst` is unreachable. -use nodedb_physical::physical_plan::GraphOp; +use nodedb_physical::physical_plan::{GraphOp, GraphReadScope}; use crate::bridge::envelope::{Payload, PhysicalPlan, Response}; use crate::control::server::dispatch_utils::{not_found_response, ok_payload_response}; @@ -24,6 +24,7 @@ use crate::engine::graph::edge_store::Direction; use crate::types::{DatabaseId, TenantId, TxnId}; use super::bfs::{CrossCoreBfsParams, cross_core_bfs_with_options}; +use super::read_scope::check_walk_scope; use super::shortest_path::{CrossCoreShortestPathParams, cross_core_shortest_path}; use super::traverse_subgraph::{CrossCoreTraverseSubgraphParams, SubgraphWalk, walk_subgraph}; @@ -37,8 +38,9 @@ struct SubgraphEdgeWire<'a> { } /// Run `plan` through the walk coordinators when it is a `Hop`, `Path` or -/// `Subgraph`. Returns `None` for every other plan. The walk sees the staged -/// edge writes of `txn_id`, the session's transaction. +/// `Subgraph`. Returns `None` for every other plan. A live walk sees the +/// staged edge writes of `txn_id`, the session's transaction. Every hop +/// carries the plan's bitemporal scope. pub async fn serve_walk_plan( state: &SharedState, tenant_id: TenantId, @@ -58,48 +60,58 @@ pub async fn serve_walk_plan( direction, depth, options, + scope, .. - } => cross_core_bfs_with_options( - state, - CrossCoreBfsParams { - tenant_id, - database_id, - collection: collection.as_ref().map(|c| c.as_str()), - start_nodes: start_nodes.clone(), - edge_labels, - direction: *direction, - max_depth: *depth, - options, - linearizable, - txn_id, - }, - ) - .await - .and_then(|response| rewrap_as_msgpack(&response.payload)), - GraphOp::Subgraph { - collection, - start_nodes, - edge_labels, - depth, - options, - .. - } => { - subgraph( + } => match check_walk_scope(scope) { + Err(error) => Err(error), + Ok(()) => cross_core_bfs_with_options( state, - tenant_id, - database_id, - SubgraphRequest { - collection: collection.as_ref().map(|c| c.as_str().to_owned()), - start_nodes, + CrossCoreBfsParams { + tenant_id, + database_id, + collection: collection.as_ref().map(|c| c.as_str()), + start_nodes: start_nodes.clone(), edge_labels, - depth: *depth, + direction: *direction, + max_depth: *depth, options, linearizable, txn_id, + read_scope: *scope, }, ) .await - } + .and_then(|response| rewrap_as_msgpack(&response.payload)), + }, + GraphOp::Subgraph { + collection, + start_nodes, + edge_labels, + depth, + options, + scope, + .. + } => match check_walk_scope(scope) { + Err(error) => Err(error), + Ok(()) => { + subgraph( + state, + tenant_id, + database_id, + SubgraphRequest { + collection: collection.as_ref().map(|c| c.as_str().to_owned()), + start_nodes, + edge_labels, + depth: *depth, + options, + linearizable, + txn_id, + read_scope: *scope, + }, + ) + .await + } + }, GraphOp::Path { collection, src, @@ -107,26 +119,52 @@ pub async fn serve_walk_plan( edge_labels, max_depth, options, + scope, .. - } => { - path( - state, - tenant_id, - database_id, - PathRequest { - collection: collection.as_ref(), - src, - dst, - edge_labels, - max_depth: *max_depth, - options, - linearizable, - txn_id, - }, - ) - .await - } - _ => return None, + } => match check_walk_scope(scope) { + Err(error) => Err(error), + Ok(()) => { + path( + state, + tenant_id, + database_id, + PathRequest { + collection: collection.as_ref(), + src, + dst, + edge_labels, + max_depth: *max_depth, + options, + linearizable, + txn_id, + read_scope: *scope, + }, + ) + .await + } + }, + GraphOp::EdgePut { .. } + | GraphOp::EdgePutBatch { .. } + | GraphOp::EdgeDelete { .. } + | GraphOp::ResolveEdgeDelete(_) + | GraphOp::EdgeDeleteBatch { .. } + | GraphOp::Neighbors { .. } + | GraphOp::NeighborsMulti { .. } + | GraphOp::RagFusion { .. } + | GraphOp::Algo { .. } + | GraphOp::Match { .. } + | GraphOp::MatchContinuation { .. } + | GraphOp::MatchVarLenResume { .. } + | GraphOp::BspSuperstep(_) + | GraphOp::WccSuperstep(_) + | GraphOp::SetNodeLabels { .. } + | GraphOp::RemoveNodeLabels { .. } + | GraphOp::TemporalNeighbors { .. } + | GraphOp::Stats { .. } + | GraphOp::NodeEdgeGuard { .. } + | GraphOp::NodePresenceGuard { .. } + | GraphOp::TruncateEdges { .. } + | GraphOp::NodePresenceRead { .. } => return None, }) } @@ -139,6 +177,7 @@ struct SubgraphRequest<'a> { options: &'a crate::engine::graph::traversal_options::GraphTraversalOptions, linearizable: bool, txn_id: Option, + read_scope: GraphReadScope, } async fn subgraph( @@ -171,6 +210,7 @@ async fn subgraph( edge_predicate: &[], with_properties: false, txn_id: request.txn_id, + read_scope: request.read_scope, }, ) .await?; @@ -198,6 +238,7 @@ struct PathRequest<'a> { options: &'a crate::engine::graph::traversal_options::GraphTraversalOptions, linearizable: bool, txn_id: Option, + read_scope: GraphReadScope, } async fn path( @@ -222,6 +263,7 @@ async fn path( linearizable: request.linearizable, edge_predicate: Vec::new(), txn_id: request.txn_id, + read_scope: request.read_scope, }, ) .await?; diff --git a/nodedb/src/control/server/graph_dispatch/whole_graph.rs b/nodedb/src/control/server/graph_dispatch/whole_graph.rs index 0052aad94..157d7632f 100644 --- a/nodedb/src/control/server/graph_dispatch/whole_graph.rs +++ b/nodedb/src/control/server/graph_dispatch/whole_graph.rs @@ -25,8 +25,10 @@ //! are sorted, so the CSR, and the answer, match a single node's exactly. //! - On a single node every algorithm gathers, so an algorithm sees the edges //! of every core, not one core's share. -//! - A historical run (`AS OF SYSTEM TIME`) gathers for every algorithm: each -//! owner exports the edges live at that time. +//! - A historical run (`AS OF SYSTEM TIME` / `AS OF VALID TIME`) runs the +//! same way at its pinned cut: PageRank and WCC as BSP supersteps that +//! every owner reads at that cut, every other algorithm over the edges each +//! owner exports at it. //! - A gathered run needs the whole graph in one CSR on one core, as a single //! node's run does. It is bounded by `GraphTuning::max_gathered_algo_edges`: //! past the cap it is refused with the edge count, never answered from part @@ -43,7 +45,7 @@ use crate::control::server::shared::session::StatementDeadline; use crate::control::state::SharedState; use crate::types::{DatabaseId, TenantId, TraceId, VShardId}; use nodedb_graph::{AlgoParams, GraphAlgorithm}; -use nodedb_physical::physical_plan::{AlgoEdge, AlgoStage, GraphOp}; +use nodedb_physical::physical_plan::{AlgoEdge, AlgoStage, GraphOp, GraphReadScope}; use super::bsp_pagerank::enumerate::enumerate_shards; use super::cluster_resolve::{DispatchSuperstepParams, dispatch_superstep_to_node, gateway_shared}; @@ -112,21 +114,22 @@ pub async fn scatter_to_graph_owners( } /// Run `algorithm` over the whole graph of `params.collection` and return its -/// `AlgoResultBatch` payload. `system_as_of_ms` runs it over the edges live at -/// that system time; `None` runs it over the current edges. +/// `AlgoResultBatch` payload, read at `scope`: the current edges, or the +/// edges at the statement's pinned bitemporal cut. pub async fn run_graph_algo( state: &SharedState, tenant_id: TenantId, database_id: DatabaseId, algorithm: GraphAlgorithm, params: AlgoParams, - system_as_of_ms: Option, + scope: GraphReadScope, linearizable: bool, ) -> crate::Result { + super::read_scope::check_walk_scope(&scope)?; let deadline_ms = StatementDeadline::of_statement(state).hop_budget_ms(); - // A current-state BSP run pins its own read cut from a Calvin cut marker - // (`run_cut`). A historical run gathers the edges live at its system time. - if state.cluster_routing.is_some() && system_as_of_ms.is_none() { + // A BSP run pins its read cut from a Calvin cut marker (`read_cut::run_marker`), + // capped at a historical run's own ordinal. + if state.cluster_routing.is_some() { match algorithm { GraphAlgorithm::PageRank => { return super::run_bsp_pagerank( @@ -136,6 +139,7 @@ pub async fn run_graph_algo( params, deadline_ms, linearizable, + scope, ) .await; } @@ -147,10 +151,21 @@ pub async fn run_graph_algo( params, deadline_ms, linearizable, + scope, ) .await; } - _ => {} + GraphAlgorithm::LabelPropagation + | GraphAlgorithm::Lcc + | GraphAlgorithm::Sssp + | GraphAlgorithm::Betweenness + | GraphAlgorithm::Closeness + | GraphAlgorithm::Harmonic + | GraphAlgorithm::Degree + | GraphAlgorithm::Louvain + | GraphAlgorithm::Triangles + | GraphAlgorithm::Diameter + | GraphAlgorithm::KCore => {} } } let edges = gather_algo_edges( @@ -159,7 +174,7 @@ pub async fn run_graph_algo( database_id, algorithm, ¶ms, - system_as_of_ms, + scope, linearizable, ) .await?; @@ -167,6 +182,8 @@ pub async fn run_graph_algo( algorithm, params, stage: AlgoStage::Gathered { edges }, + // The gathered edges already are the graph at the cut. + scope, }); let response = crate::control::server::dispatch_utils::dispatch_to_data_plane( state, @@ -190,13 +207,14 @@ async fn gather_algo_edges( database_id: DatabaseId, algorithm: GraphAlgorithm, params: &AlgoParams, - system_as_of_ms: Option, + scope: GraphReadScope, linearizable: bool, ) -> crate::Result> { let plan = PhysicalPlan::Graph(GraphOp::Algo { algorithm, params: params.clone(), - stage: AlgoStage::ExportEdges { system_as_of_ms }, + stage: AlgoStage::ExportEdges, + scope, }); let payloads = scatter_to_graph_owners( state, diff --git a/nodedb/src/control/server/http/routes/crdt.rs b/nodedb/src/control/server/http/routes/crdt.rs index 301f13bf8..fb916a5f7 100644 --- a/nodedb/src/control/server/http/routes/crdt.rs +++ b/nodedb/src/control/server/http/routes/crdt.rs @@ -151,6 +151,7 @@ pub async fn crdt_apply( // collection)`. let qualified_collection = crate::control::planner::sql_plan_convert::convert::db_qualified(database_id, &collection); + let crs_set = crate::control::crs_context::crs_set_for(&state.shared); let policy = crate::control::crdt_post_image_policy::ExternalCrdtPostImagePolicy::from_identity( identity.tenant_id, @@ -158,10 +159,13 @@ pub async fn crdt_apply( &qualified_collection, &identity, "http".into(), - &state.shared.rls, - &audit, + crate::control::crdt_post_image_policy::PolicyServices { + rls: &state.shared.rls, + audit: &audit, + crs_set: &crs_set, + }, ) - .with_type_guards_from(state.shared.credentials.catalog(), database_id); + .with_type_guards_from(&state.shared, database_id); let result = crate::control::crdt_admission::dispatch_authorized_crdt_apply_admitted( &state.shared, crate::control::crdt_admission::AuthorizedCrdtApplyAdmissionRequest { diff --git a/nodedb/src/control/server/http/routes/query_stream.rs b/nodedb/src/control/server/http/routes/query_stream.rs index 958febb1a..da2f2942a 100644 --- a/nodedb/src/control/server/http/routes/query_stream.rs +++ b/nodedb/src/control/server/http/routes/query_stream.rs @@ -204,6 +204,7 @@ pub(super) fn ndjson_body_stream( projection.as_ref(), redaction.as_ref().map(|r| r.ctx(&state.redaction)), None, + &crate::control::crs_context::crs_set_for(&state), ) { Ok(s) => s, Err(e) => { diff --git a/nodedb/src/control/server/native/dispatch/ctx.rs b/nodedb/src/control/server/native/dispatch/ctx.rs index 04aadc7cf..3e78260d8 100644 --- a/nodedb/src/control/server/native/dispatch/ctx.rs +++ b/nodedb/src/control/server/native/dispatch/ctx.rs @@ -99,7 +99,6 @@ impl DispatchCtx<'_> { | GraphOp::SetNodeLabels { .. } | GraphOp::RemoveNodeLabels { .. } | GraphOp::TemporalNeighbors { .. } - | GraphOp::TemporalAlgorithm { .. } | GraphOp::Stats { .. } | GraphOp::NodePresenceGuard { .. } | GraphOp::NodePresenceRead { .. } diff --git a/nodedb/src/control/server/native/dispatch/graph_match.rs b/nodedb/src/control/server/native/dispatch/graph_match.rs index f7dda5697..70808562a 100644 --- a/nodedb/src/control/server/native/dispatch/graph_match.rs +++ b/nodedb/src/control/server/native/dispatch/graph_match.rs @@ -173,7 +173,7 @@ async fn match_across_shards( txn_id: Option, ) -> NativeResponse { let crate::bridge::envelope::PhysicalPlan::Graph( - nodedb_physical::physical_plan::GraphOp::Match { query, .. }, + nodedb_physical::physical_plan::GraphOp::Match { query, scope, .. }, ) = plan else { return error_to_native( @@ -205,6 +205,7 @@ async fn match_across_shards( txn_id, // The native protocol has no read-consistency setting. linearizable: true, + scope: *scope, }, ) .await; diff --git a/nodedb/src/control/server/native/dispatch/graph_owner.rs b/nodedb/src/control/server/native/dispatch/graph_owner.rs index e20509a74..587bb8941 100644 --- a/nodedb/src/control/server/native/dispatch/graph_owner.rs +++ b/nodedb/src/control/server/native/dispatch/graph_owner.rs @@ -29,12 +29,12 @@ use super::{DispatchCtx, error_to_native}; /// A graph read this module serves. enum OwnerRead { - /// An algorithm, over the edges live at `Option` system time when set. - /// The parameters are boxed: they dwarf the other variants. + /// An algorithm, over the edges at the plan's bitemporal cut. The + /// parameters are boxed: they dwarf the other variants. Algo( nodedb_graph::GraphAlgorithm, Box, - Option, + nodedb_physical::physical_plan::GraphReadScope, ), Neighbors(String), /// A `Hop`, `Path` or `Subgraph` in a cluster. @@ -51,41 +51,45 @@ pub(super) async fn dispatch_graph_owner_read( plan: &PhysicalPlan, vshard_id: VShardId, ) -> Option { - let read = match plan { - PhysicalPlan::Graph(GraphOp::Algo { - algorithm, params, .. - }) => OwnerRead::Algo(*algorithm, Box::new(params.clone()), None), - PhysicalPlan::Graph(GraphOp::TemporalAlgorithm { + let PhysicalPlan::Graph(op) = plan else { + return None; + }; + let clustered = ctx.state.cluster_routing.is_some(); + let read = match op { + GraphOp::Algo { algorithm, params, - system_time, - }) => match system_time { - nodedb_types::SystemTimeScope::Current => { - OwnerRead::Algo(*algorithm, Box::new(params.clone()), None) - } - nodedb_types::SystemTimeScope::AsOf(ms) => { - OwnerRead::Algo(*algorithm, Box::new(params.clone()), Some(*ms)) - } - nodedb_types::SystemTimeScope::AllVersions => { - return Some(error_to_native( - seq, - &crate::Error::BadRequest { - detail: "a graph algorithm runs over one system time; all-versions is not supported" - .into(), - }, - )); - } - }, - PhysicalPlan::Graph(GraphOp::Neighbors { node_id, .. }) => { - OwnerRead::Neighbors(node_id.clone()) - } - PhysicalPlan::Graph( - GraphOp::Hop { .. } | GraphOp::Path { .. } | GraphOp::Subgraph { .. }, - ) if ctx.state.cluster_routing.is_some() => OwnerRead::Walk, - PhysicalPlan::Graph(GraphOp::RagFusion { .. }) if ctx.state.cluster_routing.is_some() => { - OwnerRead::Rag + scope, + .. + } => OwnerRead::Algo(*algorithm, Box::new(params.clone()), *scope), + GraphOp::Neighbors { node_id, .. } => OwnerRead::Neighbors(node_id.clone()), + GraphOp::Hop { .. } | GraphOp::Path { .. } | GraphOp::Subgraph { .. } if clustered => { + OwnerRead::Walk } - _ => return None, + GraphOp::RagFusion { .. } if clustered => OwnerRead::Rag, + GraphOp::Hop { .. } + | GraphOp::Path { .. } + | GraphOp::Subgraph { .. } + | GraphOp::RagFusion { .. } + | GraphOp::EdgePut { .. } + | GraphOp::EdgePutBatch { .. } + | GraphOp::EdgeDelete { .. } + | GraphOp::ResolveEdgeDelete(_) + | GraphOp::EdgeDeleteBatch { .. } + | GraphOp::NeighborsMulti { .. } + | GraphOp::Match { .. } + | GraphOp::MatchContinuation { .. } + | GraphOp::MatchVarLenResume { .. } + | GraphOp::BspSuperstep(_) + | GraphOp::WccSuperstep(_) + | GraphOp::SetNodeLabels { .. } + | GraphOp::RemoveNodeLabels { .. } + | GraphOp::TemporalNeighbors { .. } + | GraphOp::Stats { .. } + | GraphOp::NodeEdgeGuard { .. } + | GraphOp::NodePresenceGuard { .. } + | GraphOp::TruncateEdges { .. } + | GraphOp::NodePresenceRead { .. } => return None, }; let tenant_id = ctx.tenant_id(); let database_id = ctx.database_id(); @@ -103,14 +107,14 @@ pub(super) async fn dispatch_graph_owner_read( } let _request = ctx.state.tenant_request_guard(tenant_id); let served = match read { - OwnerRead::Algo(algorithm, params, system_as_of_ms) => { + OwnerRead::Algo(algorithm, params, scope) => { crate::control::server::graph_dispatch::run_graph_algo( ctx.state, tenant_id, database_id, algorithm, *params, - system_as_of_ms, + scope, true, ) .await diff --git a/nodedb/src/control/server/native/dispatch/plan_builder/columnar.rs b/nodedb/src/control/server/native/dispatch/plan_builder/columnar.rs index 68a489495..4fba583c7 100644 --- a/nodedb/src/control/server/native/dispatch/plan_builder/columnar.rs +++ b/nodedb/src/control/server/native/dispatch/plan_builder/columnar.rs @@ -72,6 +72,7 @@ pub(crate) async fn build_insert( schema_bytes: Vec::new(), provenance: None, wal_lsn: None, + system_from: None, rls_write_check: nodedb_types::RlsWriteCheck::pending_injection(), // The native insert API takes rows, not a projection; a caller wanting // rows back issues SQL, where `inject_returning_spec` fills this slot. diff --git a/nodedb/src/control/server/native/dispatch/plan_builder/dispatch.rs b/nodedb/src/control/server/native/dispatch/plan_builder/dispatch.rs index a2625fd37..3a1c440c4 100644 --- a/nodedb/src/control/server/native/dispatch/plan_builder/dispatch.rs +++ b/nodedb/src/control/server/native/dispatch/plan_builder/dispatch.rs @@ -79,7 +79,7 @@ pub(crate) async fn build_plan( OpCode::ColumnarInsert => columnar::build_insert(ctx, fields, collection).await, // Graph DDL. OpCode::GraphAlgo => graph::build_algo(fields, collection).await, - OpCode::GraphMatch => graph::build_match(fields, collection).await, + OpCode::GraphMatch => graph::build_match(ctx, fields).await, // Document DDL. OpCode::DocumentTruncate => document_bulk::build_truncate(ctx, collection).await, OpCode::DocumentEstimateCount => { diff --git a/nodedb/src/control/server/native/dispatch/plan_builder/document_bulk.rs b/nodedb/src/control/server/native/dispatch/plan_builder/document_bulk.rs index ec0fae7f1..59e977550 100644 --- a/nodedb/src/control/server/native/dispatch/plan_builder/document_bulk.rs +++ b/nodedb/src/control/server/native/dispatch/plan_builder/document_bulk.rs @@ -160,7 +160,7 @@ pub(crate) async fn build_insert_select( source: RowSource::DocumentPages(DocumentPageSource { collection: QualifiedCollection::new(ctx.database_id(), &source), filters: fields.filters.clone().unwrap_or_default(), - system_as_of_ms: None, + system_time: nodedb_types::SystemTimeScope::Current, offset: 0, limit, column_map: Vec::new(), diff --git a/nodedb/src/control/server/native/dispatch/plan_builder/graph.rs b/nodedb/src/control/server/native/dispatch/plan_builder/graph.rs index f8c2edd79..059722ba4 100644 --- a/nodedb/src/control/server/native/dispatch/plan_builder/graph.rs +++ b/nodedb/src/control/server/native/dispatch/plan_builder/graph.rs @@ -57,6 +57,9 @@ pub(crate) async fn build_rag_fusion( bm25_query: None, bm25_field: None, stage: nodedb_physical::physical_plan::RagStage::Local, + // The native graph ops carry no temporal field: they read the + // current graph. `AS OF` reads go through the SQL surface. + scope: nodedb_physical::physical_plan::GraphReadScope::default(), })) } @@ -99,6 +102,9 @@ pub(crate) async fn build_hop( options: Default::default(), rls_filters: Vec::new(), frontier_bitmap: None, + // The native graph ops carry no temporal field: they read the + // current graph. `AS OF` reads go through the SQL surface. + scope: nodedb_physical::physical_plan::GraphReadScope::default(), })) } @@ -121,6 +127,9 @@ pub(crate) async fn build_neighbors( edge_labels: fields.edge_labels.clone().unwrap_or_default(), direction: parse_direction(fields.direction.as_deref())?, rls_filters: Vec::new(), + // The native graph ops carry no temporal field: they read the + // current graph. `AS OF` reads go through the SQL surface. + scope: nodedb_physical::physical_plan::GraphReadScope::default(), })) } @@ -152,6 +161,9 @@ pub(crate) async fn build_path( options: Default::default(), rls_filters: Vec::new(), frontier_bitmap: None, + // The native graph ops carry no temporal field: they read the + // current graph. `AS OF` reads go through the SQL surface. + scope: nodedb_physical::physical_plan::GraphReadScope::default(), })) } @@ -175,6 +187,9 @@ pub(crate) async fn build_subgraph( edge_labels: fields.edge_labels.clone().unwrap_or_default(), options: Default::default(), rls_filters: Vec::new(), + // The native graph ops carry no temporal field: they read the + // current graph. `AS OF` reads go through the SQL surface. + scope: nodedb_physical::physical_plan::GraphReadScope::default(), })) } @@ -218,6 +233,9 @@ pub(crate) async fn build_edge_put( properties, src_surrogate, dst_surrogate, + // A native edge put takes the write's own valid time. + valid_from_ms: None, + valid_until_ms: None, })) } @@ -339,6 +357,9 @@ pub(crate) async fn build_algo( algorithm, params, stage: nodedb_physical::physical_plan::AlgoStage::Local, + // The native graph ops carry no temporal field: they read the + // current graph. `AS OF` reads go through the SQL surface. + scope: nodedb_physical::physical_plan::GraphReadScope::default(), })) } @@ -374,9 +395,11 @@ fn parse_algo_personalization( Ok(Some(map)) } +/// A native MATCH: the query text, its `AS OF` clauses split off and its +/// pattern compiled, as the `MatchQuery` the Data Plane executes. pub(crate) async fn build_match( + ctx: &DispatchCtx<'_>, fields: &TextFields, - _collection: &str, ) -> crate::Result { let query_str = fields .match_query @@ -385,18 +408,34 @@ pub(crate) async fn build_match( .ok_or_else(|| crate::Error::BadRequest { detail: "missing 'match_query'".to_string(), })?; - - // Serialize the MATCH query string as MessagePack for the Data Plane. - let query = zerompk::to_msgpack_vec(query_str).map_err(|e| crate::Error::Serialization { + let (body, temporal) = match nodedb_sql::ddl_ast::graph_parse::try_parse(query_str) { + Some(Ok(nodedb_sql::ddl_ast::NodedbStatement::Graph( + nodedb_sql::ddl_ast::GraphStmt::MatchQuery { body, temporal }, + ))) => (body, temporal), + Some(Err(e)) => { + return Err(crate::Error::BadRequest { + detail: format!("MATCH parse error: {e}"), + }); + } + Some(Ok(_)) | None => { + return Err(crate::Error::BadRequest { + detail: "'match_query' must be a MATCH statement".to_string(), + }); + } + }; + let compiled = crate::engine::graph::pattern::compiler::parse(&body)?; + let query = zerompk::to_msgpack_vec(&compiled).map_err(|e| crate::Error::Serialization { format: "msgpack".into(), detail: format!("match query serialization: {e}"), })?; + let scope = + crate::control::server::graph_dispatch::graph_read_scope(ctx.state, &temporal).await?; Ok(PhysicalPlan::Graph(GraphOp::Match { query, frontier_bitmap: None, - // B1: native MATCH stays single-node; B2 wires cluster orchestration. cluster_mode: false, + scope, })) } diff --git a/nodedb/src/control/server/native/dispatch/plan_builder/spatial.rs b/nodedb/src/control/server/native/dispatch/plan_builder/spatial.rs index 062c3592a..7b8713c10 100644 --- a/nodedb/src/control/server/native/dispatch/plan_builder/spatial.rs +++ b/nodedb/src/control/server/native/dispatch/plan_builder/spatial.rs @@ -27,16 +27,18 @@ pub(crate) async fn build_scan( detail: format!("invalid query geometry: {e}"), })?; - let issues = nodedb_spatial::validate::validate_geometry(&geometry.geometry); + let issues = nodedb_spatial::validate::validate_geometry(geometry.geometry()); if !issues.is_empty() { + let reasons: Vec = issues.iter().map(ToString::to_string).collect(); return Err(crate::Error::BadRequest { - detail: format!("invalid query geometry: {}", issues.join("; ")), + detail: format!("invalid query geometry: {}", reasons.join("; ")), }); } let predicate_str = fields.spatial_predicate.as_deref().unwrap_or("dwithin"); let predicate = match predicate_str.to_lowercase().as_str() { "dwithin" => SpatialPredicate::DWithin, + "3ddwithin" => SpatialPredicate::DWithin3D, "contains" => SpatialPredicate::Contains, "intersects" => SpatialPredicate::Intersects, "within" => SpatialPredicate::Within, diff --git a/nodedb/src/control/server/native/dispatch/raw_dispatch.rs b/nodedb/src/control/server/native/dispatch/raw_dispatch.rs index 7472944c3..f8236f005 100644 --- a/nodedb/src/control/server/native/dispatch/raw_dispatch.rs +++ b/nodedb/src/control/server/native/dispatch/raw_dispatch.rs @@ -150,6 +150,7 @@ async fn dispatch_external_crdt_apply( // before this dispatch runs), so no re-qualification happens — doing so // will double-prefix the name. let qualified_collection = collection.as_str(); + let crs_set = crate::control::crs_context::crs_set_for(ctx.state); let policy = crate::control::crdt_post_image_policy::ExternalCrdtPostImagePolicy::from_identity( tenant_id, @@ -157,10 +158,13 @@ async fn dispatch_external_crdt_apply( qualified_collection, ctx.identity, "native".into(), - &ctx.state.rls, - &audit, + crate::control::crdt_post_image_policy::PolicyServices { + rls: &ctx.state.rls, + audit: &audit, + crs_set: &crs_set, + }, ) - .with_type_guards_from(ctx.state.credentials.catalog(), ctx.database_id()); + .with_type_guards_from(ctx.state, ctx.database_id()); let outcome = crate::control::crdt_admission::dispatch_authorized_crdt_apply_admitted_outcome( ctx.state, crate::control::crdt_admission::AuthorizedCrdtApplyAdmissionRequest { diff --git a/nodedb/src/control/server/native/session/session_stream.rs b/nodedb/src/control/server/native/session/session_stream.rs index d3844f4ea..79266ddf8 100644 --- a/nodedb/src/control/server/native/session/session_stream.rs +++ b/nodedb/src/control/server/native/session/session_stream.rs @@ -40,13 +40,14 @@ fn decode_batch_to_columns_rows( payload: &[u8], projection: Option<&OutputSchema>, redaction: Option>, + crs_set: &nodedb_crs::CustomCrsSet, ) -> crate::Result<(Vec, Vec>)> { match decode_payload_value(payload) { Ok(decoded) => { // A streamed plan never carries Control-Plane computed columns: // `try_open_sql_stream` declines those, so no session access // is needed per batch. - let shaped = shape_decoded_rows(decoded, projection, redaction, None)?; + let shaped = shape_decoded_rows(decoded, projection, redaction, None, crs_set)?; Ok(to_native_columns_rows(&shaped)) } Err(_) => Ok(( @@ -120,6 +121,7 @@ pub(super) async fn emit_sql_stream( &batch.payload, projection.as_ref(), redaction.as_ref().map(|r| r.ctx(&state.redaction)), + &crate::control::crs_context::crs_set_for(state), )?; if batch_rows.is_empty() { continue; diff --git a/nodedb/src/control/server/pgwire/catalog/mod.rs b/nodedb/src/control/server/pgwire/catalog/mod.rs index 01d9ffbc8..3091aa48d 100644 --- a/nodedb/src/control/server/pgwire/catalog/mod.rs +++ b/nodedb/src/control/server/pgwire/catalog/mod.rs @@ -9,6 +9,7 @@ pub mod l2_cleanup_queue; pub mod oid; pub mod provider; pub mod schema; +pub mod spatial_ref_sys; pub mod tables; pub use provider::{CatalogProvider, catalog_rows}; diff --git a/nodedb/src/control/server/pgwire/catalog/provider.rs b/nodedb/src/control/server/pgwire/catalog/provider.rs index c31927d64..e89433da8 100644 --- a/nodedb/src/control/server/pgwire/catalog/provider.rs +++ b/nodedb/src/control/server/pgwire/catalog/provider.rs @@ -11,7 +11,7 @@ use crate::control::security::identity::AuthenticatedIdentity; use crate::control::state::SharedState; use super::schema::{catalog_collection_info, known_table}; -use super::{audit_log, dropped_collections, l2_cleanup_queue, tables}; +use super::{audit_log, dropped_collections, l2_cleanup_queue, spatial_ref_sys, tables}; /// Produce the rows of a catalog relation, identity-scoped, per request. /// @@ -47,6 +47,7 @@ pub async fn catalog_rows( dropped_collections::dropped_collections(state, identity, database_id).await } Some("_system.l2_cleanup_queue") => l2_cleanup_queue::l2_cleanup_queue(state, identity), + Some("nodedb.spatial_ref_sys") => spatial_ref_sys::spatial_ref_sys(state), _ => Ok(Vec::new()), } } diff --git a/nodedb/src/control/server/pgwire/catalog/schema.rs b/nodedb/src/control/server/pgwire/catalog/schema.rs index 50c9f8126..3bc40878b 100644 --- a/nodedb/src/control/server/pgwire/catalog/schema.rs +++ b/nodedb/src/control/server/pgwire/catalog/schema.rs @@ -21,6 +21,8 @@ pub enum CatalogColType { Int4, /// 8-byte integer. Int8, + /// 8-byte float. + Float8, Text, } @@ -29,6 +31,7 @@ impl CatalogColType { match self { CatalogColType::Bool => SqlDataType::Bool, CatalogColType::Int4 | CatalogColType::Int8 => SqlDataType::Int64, + CatalogColType::Float8 => SqlDataType::Float64, CatalogColType::Text => SqlDataType::String, } } @@ -59,6 +62,7 @@ pub const KNOWN_TABLES: &[&str] = &[ "_system.audit_log", "_system.dropped_collections", "_system.l2_cleanup_queue", + "nodedb.spatial_ref_sys", ]; /// Map a relation name to its canonical static name (case-insensitive). @@ -72,7 +76,7 @@ pub fn known_table(name: &str) -> Option<&'static str> { /// Static column spec for a known catalog relation, in output order. Returns /// `None` for any name that is not a catalog relation. pub fn catalog_columns(table: &str) -> Option> { - use CatalogColType::{Bool, Int4, Int8, Text}; + use CatalogColType::{Bool, Float8, Int4, Int8, Text}; Some(match table { "pg_database" => vec![ col("oid", Int8), @@ -197,6 +201,16 @@ pub fn catalog_columns(table: &str) -> Option> { col("last_error", Text), col("attempts", Int4), ], + "nodedb.spatial_ref_sys" => vec![ + col("srid", Int4), + col("auth_name", Text), + col("auth_srid", Int4), + col("name", Text), + col("kind", Text), + col("proj4text", Text), + col("srtext", Text), + col("datum_shift_accuracy_m", Float8), + ], _ => return None, }) } diff --git a/nodedb/src/control/server/pgwire/catalog/spatial_ref_sys.rs b/nodedb/src/control/server/pgwire/catalog/spatial_ref_sys.rs new file mode 100644 index 000000000..88902c9fc --- /dev/null +++ b/nodedb/src/control/server/pgwire/catalog/spatial_ref_sys.rs @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! `nodedb.spatial_ref_sys` row producer: every built-in CRS, then every +//! live user-defined CRS. +//! +//! Columns follow PostGIS `spatial_ref_sys`, plus `name`, `kind` and the +//! accuracy of the datum shift to WGS 84. `srtext` is WKT2:2019 and +//! `proj4text` a PROJ string. `proj4text` is NULL for a CRS no PROJ string +//! expresses, such as one with a time-dependent datum shift, as PostGIS +//! leaves it empty for such a CRS. A dropped user-defined code is not +//! listed: it resolves to nothing. + +use std::collections::HashMap; + +use nodedb_crs::{CrsDef, CustomCrsState, DatumShift, NODEDB_AUTHORITY, Srid, SridAuthority}; +use nodedb_types::Value; + +use crate::control::state::SharedState; + +use super::tables::collections::encode_row; + +pub fn spatial_ref_sys(state: &SharedState) -> crate::Result>> { + let mut rows = Vec::new(); + for entry in nodedb_crs::builtin_entries() { + let srid = Srid::new(entry.code).map_err(|e| crate::Error::Internal { + detail: format!("built-in CRS table: {e}"), + })?; + let def = entry.definition(srid).map_err(|e| crate::Error::Internal { + detail: format!("built-in CRS {srid}: {e}"), + })?; + rows.push(encode_row(row(&def, Some(entry.proj.to_string())))?); + } + let custom = state.credentials.catalog().custom_crs_set(); + for entry in custom.entries() { + match &entry.state { + CustomCrsState::Live(def) => { + // The only export error is a time-dependent shift, which no + // PROJ string carries: the column is NULL, as in PostGIS. + let proj4text = def.to_proj_string().ok(); + rows.push(encode_row(row(def, proj4text))?); + } + CustomCrsState::Dropped => {} + } + } + Ok(rows) +} + +/// One row: `def` with `proj4text` as the PROJ text to show, NULL when +/// `None`. +fn row(def: &CrsDef, proj4text: Option) -> HashMap { + let srid = def.srid; + let auth_name = match srid.authority() { + SridAuthority::Epsg => "EPSG", + SridAuthority::NodeDb | SridAuthority::User | SridAuthority::Reserved => NODEDB_AUTHORITY, + }; + let accuracy = match &def.datum.to_wgs84 { + DatumShift::Helmert(h) => h.accuracy_m.map_or(Value::Null, Value::Float), + DatumShift::Identity => Value::Null, + }; + let mut r = HashMap::with_capacity(8); + r.insert("srid".into(), Value::Integer(i64::from(srid.code()))); + r.insert("auth_name".into(), Value::String(auth_name.into())); + r.insert("auth_srid".into(), Value::Integer(i64::from(srid.code()))); + r.insert("name".into(), Value::String(def.name.to_string())); + r.insert("kind".into(), Value::String(def.kind.as_str().into())); + r.insert( + "proj4text".into(), + proj4text.map_or(Value::Null, Value::String), + ); + r.insert("srtext".into(), Value::String(def.to_wkt2())); + r.insert("datum_shift_accuracy_m".into(), accuracy); + r +} diff --git a/nodedb/src/control/server/pgwire/handler/core/simple_query.rs b/nodedb/src/control/server/pgwire/handler/core/simple_query.rs index 25645ecf9..90a948f84 100644 --- a/nodedb/src/control/server/pgwire/handler/core/simple_query.rs +++ b/nodedb/src/control/server/pgwire/handler/core/simple_query.rs @@ -92,19 +92,8 @@ impl SimpleQueryHandler for NodeDbPgHandler { .await; } - // J.4: install the DDL audit context for this statement. Any - // `propose_catalog_entry` call reached from `execute_sql` - // picks up the identity + raw SQL so the applier can emit a - // full audit record on every replica. The guard auto-clears - // on scope exit. - let _audit_scope = crate::control::server::shared::session::audit_context::AuditScope::new( - crate::control::server::shared::session::audit_context::AuditCtx { - auth_user_id: identity.user_id.to_string(), - auth_user_name: identity.username.clone(), - sql_text: query.to_string(), - }, - ); - + // `execute_sql` installs the DDL audit context of each statement the + // message splits into. let result = self .execute_sql(&identity, session_id, query, &TEXT_RESULTS) .await; diff --git a/nodedb/src/control/server/pgwire/handler/dispatch/local.rs b/nodedb/src/control/server/pgwire/handler/dispatch/local.rs index a06d6462a..394bd180b 100644 --- a/nodedb/src/control/server/pgwire/handler/dispatch/local.rs +++ b/nodedb/src/control/server/pgwire/handler/dispatch/local.rs @@ -31,6 +31,7 @@ impl NodeDbPgHandler { apply_key: 0, commit_hlc: None, change_position: None, + system_ordinal: None, }, ) .await diff --git a/nodedb/src/control/server/pgwire/handler/prepared/execute.rs b/nodedb/src/control/server/pgwire/handler/prepared/execute.rs index 1078cd399..06eb66470 100644 --- a/nodedb/src/control/server/pgwire/handler/prepared/execute.rs +++ b/nodedb/src/control/server/pgwire/handler/prepared/execute.rs @@ -64,18 +64,14 @@ impl NodeDbPgHandler { self.authorize_session_database(&identity, session_id)?; let tenant_id = identity.tenant_id; - // J.4: mirror `do_query`'s audit scope. The extended-query - // path also triggers DDL (a prepared `CREATE COLLECTION` - // binds parameters then dispatches), so audit context must - // be installed here too or followers receive a plain - // `CatalogDdl` with no SQL trail. - let _audit_scope = crate::control::server::shared::session::audit_context::AuditScope::new( - crate::control::server::shared::session::audit_context::AuditCtx { - auth_user_id: identity.user_id.to_string(), - auth_user_name: identity.username.clone(), - sql_text: stmt.sql.clone(), - }, - ); + // The extended-query path also runs DDL (a prepared `CREATE + // COLLECTION` binds parameters, then dispatches), so it installs the + // statement's audit context too. A credential statement installs its + // redacted text. + let _audit_scope = + crate::control::server::shared::session::audit_context::AuditScope::statement( + &identity, &stmt.sql, + ); // Wire-streaming COPY shapes for backup/restore. Recognised before // sqlparser-based execution because the shapes aren't standard COPY diff --git a/nodedb/src/control/server/pgwire/handler/routing/dispatch_loop/finish.rs b/nodedb/src/control/server/pgwire/handler/routing/dispatch_loop/finish.rs index 08e1eefd6..501cce8d2 100644 --- a/nodedb/src/control/server/pgwire/handler/routing/dispatch_loop/finish.rs +++ b/nodedb/src/control/server/pgwire/handler/routing/dispatch_loop/finish.rs @@ -97,6 +97,7 @@ impl NodeDbPgHandler { .as_ref() .map(|r| r.ctx(&self.state.redaction)), sequences.as_ref().map(|s| s as &dyn SequenceAccess), + &crate::control::crs_context::crs_set_for(&self.state), )?; if let Some(n) = notice { self.sessions.push_notice(session_id, n); diff --git a/nodedb/src/control/server/pgwire/handler/routing/execute_dml_hooks.rs b/nodedb/src/control/server/pgwire/handler/routing/execute_dml_hooks.rs index af8f88b52..5cd8e09a9 100644 --- a/nodedb/src/control/server/pgwire/handler/routing/execute_dml_hooks.rs +++ b/nodedb/src/control/server/pgwire/handler/routing/execute_dml_hooks.rs @@ -204,6 +204,7 @@ impl NodeDbPgHandler { projection, Some(redaction.ctx(&self.state.redaction)), None, + &crate::control::crs_context::crs_set_for(&self.state), ) .map_err(|e| shape_error_to_pg(&e))? { diff --git a/nodedb/src/control/server/pgwire/handler/routing/set_ops.rs b/nodedb/src/control/server/pgwire/handler/routing/set_ops.rs index b841f95e5..dd9756968 100644 --- a/nodedb/src/control/server/pgwire/handler/routing/set_ops.rs +++ b/nodedb/src/control/server/pgwire/handler/routing/set_ops.rs @@ -33,6 +33,7 @@ pub(super) fn apply_set_ops( result_formats: &Format, redaction: Option>, sequences: Option<&dyn SequenceAccess>, + crs_set: &nodedb_crs::CustomCrsSet, ) -> PgWireResult<(Response, Option)> { let merged = match dedup_set_op { PostSetOp::Intersect | PostSetOp::IntersectAll => { @@ -51,6 +52,7 @@ pub(super) fn apply_set_ops( projection, redaction, sequences, + crs_set, ) .map_err(|e| shape_error_to_pg(&e))? { diff --git a/nodedb/src/control/server/pgwire/handler/routing/streaming.rs b/nodedb/src/control/server/pgwire/handler/routing/streaming.rs index 6ddc207a9..c1ad7d428 100644 --- a/nodedb/src/control/server/pgwire/handler/routing/streaming.rs +++ b/nodedb/src/control/server/pgwire/handler/routing/streaming.rs @@ -154,6 +154,7 @@ impl NodeDbPgHandler { shaping.projection, Some(redaction.ctx(&state.redaction)), None, + &crate::control::crs_context::crs_set_for(&state), ) .map_err(|e| shape_error_to_pg(&e))? { diff --git a/nodedb/src/control/server/pgwire/handler/sql_exec.rs b/nodedb/src/control/server/pgwire/handler/sql_exec.rs index 1740c25a7..1d83cfc58 100644 --- a/nodedb/src/control/server/pgwire/handler/sql_exec.rs +++ b/nodedb/src/control/server/pgwire/handler/sql_exec.rs @@ -22,6 +22,7 @@ use super::super::types::text_field; use super::connection_admin; use super::core::NodeDbPgHandler; use super::sql_split::split_sql_statements; +use crate::control::server::shared::session::audit_context::AuditScope; use crate::control::server::shared::session::{SessionId, TransactionState}; use crate::control::server::shared::txn_control::{TxnControl, classify as classify_txn_control}; @@ -43,23 +44,19 @@ impl NodeDbPgHandler { requested: &Format, ) -> PgWireResult> { let statements = split_sql_statements(sql); - match statements.len() { - 0 => Ok(vec![Response::EmptyQuery]), - 1 => { - self.execute_single_sql(identity, session_id, &statements[0], requested) - .await - } - _ => { - let mut all = Vec::new(); - for stmt in statements { - let mut resp = self - .execute_single_sql(identity, session_id, &stmt, requested) - .await?; - all.append(&mut resp); - } - Ok(all) - } + if statements.is_empty() { + return Ok(vec![Response::EmptyQuery]); + } + let mut all = Vec::new(); + for stmt in statements { + // Each statement's DDL proposals carry that statement's own text. + let _audit = AuditScope::statement(identity, &stmt); + let mut resp = self + .execute_single_sql(identity, session_id, &stmt, requested) + .await?; + all.append(&mut resp); } + Ok(all) } /// Execute a single (already-split) SQL statement. diff --git a/nodedb/src/control/server/pgwire/handler/stream_response.rs b/nodedb/src/control/server/pgwire/handler/stream_response.rs index e5c5a2bb8..ca8a94d3d 100644 --- a/nodedb/src/control/server/pgwire/handler/stream_response.rs +++ b/nodedb/src/control/server/pgwire/handler/stream_response.rs @@ -233,6 +233,7 @@ pub(crate) fn streaming_shaped_response( Some(&schema_out), redaction.as_ref().map(|r| r.ctx(&state.redaction)), None, + &crate::control::crs_context::crs_set_for(&state), ) .map_err(|e| error_to_pg_in_context("failed to shape streamed batch", &e))?; for row in &shaped.rows { @@ -345,6 +346,7 @@ pub(crate) async fn streaming_star_response( Some(schema), redaction.as_ref().map(|r| r.ctx(&state.redaction)), None, + &crate::control::crs_context::crs_set_for(state), ) { Ok(s) => s, Err(e) => { diff --git a/nodedb/src/control/server/pgwire/types/error_map.rs b/nodedb/src/control/server/pgwire/types/error_map.rs index efee02dfd..f8e0bf7e3 100644 --- a/nodedb/src/control/server/pgwire/types/error_map.rs +++ b/nodedb/src/control/server/pgwire/types/error_map.rs @@ -129,6 +129,7 @@ pub fn error_to_sqlstate(err: &crate::Error) -> (&'static str, &'static str, Str ("ERROR", sqlstate::DATA_EXCEPTION, detail.clone()) } crate::Error::MixedSrid { .. } => ("ERROR", sqlstate::DATA_EXCEPTION, err.to_string()), + crate::Error::UnknownSrid { .. } => ("ERROR", sqlstate::DATA_EXCEPTION, err.to_string()), crate::Error::NumericValueOutOfRange { detail } => ( "ERROR", sqlstate::NUMERIC_VALUE_OUT_OF_RANGE, diff --git a/nodedb/src/control/server/response_shape/compose/array_slice.rs b/nodedb/src/control/server/response_shape/compose/array_slice.rs index d31ead499..09bcfd779 100644 --- a/nodedb/src/control/server/response_shape/compose/array_slice.rs +++ b/nodedb/src/control/server/response_shape/compose/array_slice.rs @@ -30,6 +30,7 @@ pub(super) fn shape_array_slice( projection: Option<&OutputSchema>, redaction: Option>, sequences: Option<&dyn SequenceAccess>, + crs_set: &nodedb_crs::CustomCrsSet, ) -> crate::Result { if payload.is_empty() { return Ok(empty_shaped()); @@ -45,7 +46,7 @@ pub(super) fn shape_array_slice( let notice = truncated.then(|| TRUNCATED_BEFORE_HORIZON_NOTICE.to_string()); let mut shaped = match rows { - Ok(value) => shape_decoded_rows(value, projection, redaction, sequences)?, + Ok(value) => shape_decoded_rows(value, projection, redaction, sequences, crs_set)?, Err(_) => empty_shaped(), }; shaped.notice = notice; diff --git a/nodedb/src/control/server/response_shape/compose/kernel.rs b/nodedb/src/control/server/response_shape/compose/kernel.rs index 450c24a40..5ab1d9b26 100644 --- a/nodedb/src/control/server/response_shape/compose/kernel.rs +++ b/nodedb/src/control/server/response_shape/compose/kernel.rs @@ -39,6 +39,7 @@ pub fn shape_decoded_rows( projection: Option<&OutputSchema>, redaction: Option>, sequences: Option<&dyn SequenceAccess>, + crs_set: &nodedb_crs::CustomCrsSet, ) -> crate::Result { let mut rows = Vec::new(); push_flat_rows( @@ -61,7 +62,12 @@ pub fn shape_decoded_rows( // redaction (an expression reads the values the policy lets through) // and BEFORE projection (the projection drops the pass-through base // columns the expression reads and keeps only the alias). - stamp_computed_columns(projection, &mut rows, sequences)?; + stamp_computed_columns( + projection, + &mut rows, + sequences, + &nodedb_crs::CrsContext::new(crs_set), + )?; match projection { Some(s) if !s.is_star && !s.columns.is_empty() => { @@ -112,6 +118,7 @@ pub(in crate::control::server::response_shape) fn stamp_computed_columns( projection: Option<&OutputSchema>, rows: &mut [ShapedRow], sequences: Option<&dyn SequenceAccess>, + crs: &nodedb_crs::CrsContext<'_>, ) -> crate::Result<()> { let Some(schema) = projection.filter(|s| !s.cp_computed.is_empty()) else { return Ok(()); @@ -120,7 +127,7 @@ pub(in crate::control::server::response_shape) fn stamp_computed_columns( Some(access) => access, None => &NoSequenceAccess, }; - stamp_rows(rows, &schema.cp_computed, access) + stamp_rows(rows, &schema.cp_computed, access, crs) } /// Apply the statement's column-level redaction policy to every flat row. @@ -348,8 +355,14 @@ mod tests { let sources = vec![(String::new(), "users".to_string())]; let decoded = one_row(&[("email", text("a@b.c")), ("name", text("Alice"))]); - let shaped = shape_decoded_rows(decoded, None, Some(ctx(&store, &roles, &sources)), None) - .expect("shape rows"); + let shaped = shape_decoded_rows( + decoded, + None, + Some(ctx(&store, &roles, &sources)), + None, + nodedb_crs::CustomCrsSet::empty(), + ) + .expect("shape rows"); assert_eq!(shaped.rows[0]["email"], text("***")); assert_eq!(shaped.rows[0]["name"], text("Alice")); } @@ -368,9 +381,22 @@ mod tests { let sources = vec![(String::new(), "users".to_string())]; let decoded = one_row(&[("email", text("a@b.c")), ("name", text("Alice"))]); - let baseline = shape_decoded_rows(decoded.clone(), None, None, None).expect("shape rows"); - let shaped = shape_decoded_rows(decoded, None, Some(ctx(&store, &roles, &sources)), None) - .expect("shape rows"); + let baseline = shape_decoded_rows( + decoded.clone(), + None, + None, + None, + nodedb_crs::CustomCrsSet::empty(), + ) + .expect("shape rows"); + let shaped = shape_decoded_rows( + decoded, + None, + Some(ctx(&store, &roles, &sources)), + None, + nodedb_crs::CustomCrsSet::empty(), + ) + .expect("shape rows"); assert_eq!(shaped.rows, baseline.rows); assert_eq!(shaped.columns, baseline.columns); } @@ -395,6 +421,7 @@ mod tests { Some(&projection), Some(ctx(&store, &roles, &sources)), None, + nodedb_crs::CustomCrsSet::empty(), ) .expect("shape rows"); assert_eq!(shaped.columns, vec!["contact".to_string()]); @@ -424,6 +451,7 @@ mod tests { Some(&projection), Some(ctx(&store, &roles, &sources)), None, + nodedb_crs::CustomCrsSet::empty(), ) .expect("shape rows"); // `cell_keys` suffixes the duplicate display name. @@ -445,8 +473,14 @@ mod tests { let sources = vec![(String::new(), "users".to_string())]; let decoded = one_row(&[("id", text("u1")), ("email", text("a@b.c"))]); - let shaped = shape_decoded_rows(decoded, None, Some(ctx(&store, &roles, &sources)), None) - .expect("shape rows"); + let shaped = shape_decoded_rows( + decoded, + None, + Some(ctx(&store, &roles, &sources)), + None, + nodedb_crs::CustomCrsSet::empty(), + ) + .expect("shape rows"); assert!( shaped.columns.contains(&"email".to_string()), "redacted column must stay in the derived SELECT * schema" @@ -477,8 +511,14 @@ mod tests { let decoded = one_row(&[("at", Value::NaiveDateTime(at)), ("id", text("r1"))]); let projection = named_projection(&[("at", "at")]); - let shaped = - shape_decoded_rows(decoded, Some(&projection), None, None).expect("shape rows"); + let shaped = shape_decoded_rows( + decoded, + Some(&projection), + None, + None, + nodedb_crs::CustomCrsSet::empty(), + ) + .expect("shape rows"); assert_eq!(shaped.rows[0]["at"], Value::NaiveDateTime(at)); assert_eq!( crate::control::server::response_shape::cell::value_to_wire_json(&shaped.rows[0]["at"]), @@ -573,8 +613,14 @@ mod tests { let projection = cp_computed_projection(); let access = FixedAccess(41); - let shaped = shape_decoded_rows(decoded, Some(&projection), None, Some(&access)) - .expect("shape rows"); + let shaped = shape_decoded_rows( + decoded, + Some(&projection), + None, + Some(&access), + nodedb_crs::CustomCrsSet::empty(), + ) + .expect("shape rows"); assert_eq!(shaped.columns, vec!["id".to_string(), "n".to_string()]); assert_eq!(shaped.rows[0]["n"], Value::Integer(41)); } @@ -584,8 +630,14 @@ mod tests { let decoded = one_row(&[("id", text("r1"))]); let projection = cp_computed_projection(); - let err = - shape_decoded_rows(decoded, Some(&projection), None, None).expect_err("must refuse"); + let err = shape_decoded_rows( + decoded, + Some(&projection), + None, + None, + nodedb_crs::CustomCrsSet::empty(), + ) + .expect_err("must refuse"); assert!(matches!(err, crate::Error::FeatureNotSupported { .. })); } @@ -608,8 +660,14 @@ mod tests { }, ]; - let shaped = shape_decoded_rows(decoded, Some(&projection), None, None) - .expect("an accessor-free column evaluates without sequence access"); + let shaped = shape_decoded_rows( + decoded, + Some(&projection), + None, + None, + nodedb_crs::CustomCrsSet::empty(), + ) + .expect("an accessor-free column evaluates without sequence access"); assert_eq!(shaped.columns, vec!["document".to_string()]); let Value::Object(document) = &shaped.rows[0]["document"] else { panic!("the whole row is an object: {:?}", shaped.rows[0]); diff --git a/nodedb/src/control/server/response_shape/compose/materialized.rs b/nodedb/src/control/server/response_shape/compose/materialized.rs index 9bf118d15..7eb0b71bb 100644 --- a/nodedb/src/control/server/response_shape/compose/materialized.rs +++ b/nodedb/src/control/server/response_shape/compose/materialized.rs @@ -92,6 +92,7 @@ pub async fn shape_response_materialized( projection, redaction, sequences.map(|s| s as &dyn SequenceAccess), + &crate::control::crs_context::crs_set_for(state), ) } @@ -106,6 +107,7 @@ pub fn shape_returning_materialized( plan, plan_kind, projection, + state, redaction, sequences, .. @@ -125,6 +127,7 @@ pub fn shape_returning_materialized( projection, redaction, sequences.map(|s| s as &dyn SequenceAccess), + &crate::control::crs_context::crs_set_for(state), ) } @@ -135,16 +138,19 @@ fn shape_translated( projection: Option<&OutputSchema>, redaction: Option>, sequences: Option<&dyn SequenceAccess>, + crs_set: &nodedb_crs::CustomCrsSet, ) -> Result { let shaped = match plan_kind { - PlanKind::ArraySlice => shape_array_slice(translated, projection, redaction, sequences)?, + PlanKind::ArraySlice => { + shape_array_slice(translated, projection, redaction, sequences, crs_set)? + } // `RETURNING` rows are held to the columns already announced to the // client, when any were — see `super::returning`. PlanKind::ReturningRows => { - shape_returning_rows(translated, projection, redaction, sequences)? + shape_returning_rows(translated, projection, redaction, sequences, crs_set)? } PlanKind::SingleDocument | PlanKind::MultiRow => { - shape_generic_rows(translated, projection, redaction, sequences)? + shape_generic_rows(translated, projection, redaction, sequences, crs_set)? } // The callers return early for these; kept exhaustive (no catch-all, // no panic) so a future PlanKind desync degrades to passthrough @@ -175,19 +181,20 @@ pub fn shape_payload_no_plan( projection: Option<&OutputSchema>, redaction: Option>, sequences: Option<&dyn SequenceAccess>, + crs_set: &nodedb_crs::CustomCrsSet, ) -> Result { Ok(match plan_kind { PlanKind::Execution | PlanKind::DmlResult(_) | PlanKind::DmlResultByOp => { ShapeOutcome::Passthrough } PlanKind::ArraySlice => ShapeOutcome::Rows(shape_array_slice( - payload, projection, redaction, sequences, + payload, projection, redaction, sequences, crs_set, )?), PlanKind::ReturningRows => ShapeOutcome::Rows(shape_returning_rows( - payload, projection, redaction, sequences, + payload, projection, redaction, sequences, crs_set, )?), PlanKind::SingleDocument | PlanKind::MultiRow => ShapeOutcome::Rows(shape_generic_rows( - payload, projection, redaction, sequences, + payload, projection, redaction, sequences, crs_set, )?), }) } @@ -203,12 +210,13 @@ fn shape_generic_rows( projection: Option<&OutputSchema>, redaction: Option>, sequences: Option<&dyn SequenceAccess>, + crs_set: &nodedb_crs::CustomCrsSet, ) -> crate::Result { if payload.is_empty() { return Ok(empty_shaped()); } match decode_payload_value(payload) { - Ok(value) => shape_decoded_rows(value, projection, redaction, sequences), + Ok(value) => shape_decoded_rows(value, projection, redaction, sequences, crs_set), Err(_) => Ok(single_result_row( String::from_utf8_lossy(payload).into_owned(), )), @@ -295,7 +303,14 @@ mod tests { expected ); - let no_plan = shape_payload_no_plan(&payload, kind, None, None, None); + let no_plan = shape_payload_no_plan( + &payload, + kind, + None, + None, + None, + nodedb_crs::CustomCrsSet::empty(), + ); assert!(matches!(no_plan, Ok(ShapeOutcome::Passthrough))); assert_eq!( payload, original_payload, @@ -322,9 +337,15 @@ mod tests { ])) .expect("encode"); - let ShapeOutcome::Rows(shaped) = - shape_payload_no_plan(&payload, PlanKind::MultiRow, None, None, None).expect("shape") - else { + let ShapeOutcome::Rows(shaped) = shape_payload_no_plan( + &payload, + PlanKind::MultiRow, + None, + None, + None, + nodedb_crs::CustomCrsSet::empty(), + ) + .expect("shape") else { panic!("multi-row plan must yield rows"); }; assert_eq!( diff --git a/nodedb/src/control/server/response_shape/returning.rs b/nodedb/src/control/server/response_shape/returning.rs index f53b55d58..203cfdb53 100644 --- a/nodedb/src/control/server/response_shape/returning.rs +++ b/nodedb/src/control/server/response_shape/returning.rs @@ -61,6 +61,7 @@ pub fn shape_returning_rows( projection: Option<&OutputSchema>, redaction: Option>, sequences: Option<&dyn SequenceAccess>, + crs_set: &nodedb_crs::CustomCrsSet, ) -> Result { let announced = announced_columns(projection); @@ -119,7 +120,12 @@ pub fn shape_returning_rows( redact_rows(redaction.as_ref(), &mut rows); // Stamped after redaction and before the announced projection drops the // pass-through columns an expression reads. - stamp_computed_columns(projection, &mut rows, sequences)?; + stamp_computed_columns( + projection, + &mut rows, + sequences, + &nodedb_crs::CrsContext::new(crs_set), + )?; let Some(schema) = announced else { let column_types = ShapedRows::text_types(columns.len()); @@ -295,7 +301,14 @@ mod tests { }, }) .expect("encode rejecting payload"); - let shaped = shape_returning_rows(&rejecting, None, None, None).expect("shape"); + let shaped = shape_returning_rows( + &rejecting, + None, + None, + None, + nodedb_crs::CustomCrsSet::empty(), + ) + .expect("shape"); assert_eq!(shaped.rows.len(), 1); let notices = statement_notice::take(); assert_eq!(notices.len(), 1); @@ -305,7 +318,8 @@ mod tests { ); let plain = payload(&["id"], &[&[Some("a")]]); - shape_returning_rows(&plain, None, None, None).expect("shape"); + shape_returning_rows(&plain, None, None, None, nodedb_crs::CustomCrsSet::empty()) + .expect("shape"); assert!(statement_notice::take().is_empty()); }) .await; @@ -365,7 +379,9 @@ mod tests { &["id", "name", "score"], &[&[Some("a"), Some("x"), Some("1")]], ); - let shaped = shape_returning_rows(&bytes, None, None, None).expect("shape"); + let shaped = + shape_returning_rows(&bytes, None, None, None, nodedb_crs::CustomCrsSet::empty()) + .expect("shape"); assert_eq!(shaped.columns, ["id", "name", "score"]); assert_eq!(shaped.rows.len(), 1); } @@ -379,7 +395,14 @@ mod tests { &[&[Some("a"), Some("x"), Some("surprise")]], ); let schema = announced(&[("id", DdlColType::Text), ("name", DdlColType::Text)]); - let shaped = shape_returning_rows(&bytes, Some(&schema), None, None).expect("shape"); + let shaped = shape_returning_rows( + &bytes, + Some(&schema), + None, + None, + nodedb_crs::CustomCrsSet::empty(), + ) + .expect("shape"); assert_eq!(shaped.columns, ["id", "name"]); assert_eq!(shaped.column_types.len(), shaped.columns.len()); let keys = shaped.cell_keys(); @@ -398,7 +421,14 @@ mod tests { ("name", DdlColType::Text), ("score", DdlColType::Int8), ]); - let shaped = shape_returning_rows(&bytes, Some(&schema), None, None).expect("shape"); + let shaped = shape_returning_rows( + &bytes, + Some(&schema), + None, + None, + nodedb_crs::CustomCrsSet::empty(), + ) + .expect("shape"); assert_eq!(shaped.columns, ["id", "name", "score"]); assert_eq!(shaped.rows[0]["id"], text("a")); assert_eq!(shaped.rows[0]["name"], Value::Null); @@ -419,7 +449,14 @@ mod tests { ("b", DdlColType::Bool), ("s", DdlColType::Text), ]); - let shaped = shape_returning_rows(&bytes, Some(&schema), None, None).expect("shape"); + let shaped = shape_returning_rows( + &bytes, + Some(&schema), + None, + None, + nodedb_crs::CustomCrsSet::empty(), + ) + .expect("shape"); assert_eq!(shaped.rows[0]["i"], Value::Integer(42)); assert_eq!(shaped.rows[0]["f"], Value::Float(1.5)); assert_eq!(shaped.rows[0]["b"], Value::Bool(true)); @@ -446,7 +483,14 @@ mod tests { ("tstz", DdlColType::Timestamptz), ("digits", DdlColType::Timestamp), ]); - let shaped = shape_returning_rows(&bytes, Some(&schema), None, None).expect("shape"); + let shaped = shape_returning_rows( + &bytes, + Some(&schema), + None, + None, + nodedb_crs::CustomCrsSet::empty(), + ) + .expect("shape"); assert_eq!(shaped.rows[0]["ts"], Value::NaiveDateTime(at)); assert_eq!(shaped.rows[0]["tstz"], Value::DateTime(at)); assert_eq!(shaped.rows[0]["digits"], text("1583402400000000")); @@ -461,7 +505,14 @@ mod tests { ("v", DdlColType::Float4Array), ("bad", DdlColType::Float8Array), ]); - let shaped = shape_returning_rows(&bytes, Some(&schema), None, None).expect("shape"); + let shaped = shape_returning_rows( + &bytes, + Some(&schema), + None, + None, + nodedb_crs::CustomCrsSet::empty(), + ) + .expect("shape"); assert_eq!( shaped.rows[0]["v"], Value::Array(vec![ @@ -495,7 +546,14 @@ mod tests { ("f", DdlColType::Float8), ("gone", DdlColType::Text), ]); - let shaped = shape_returning_rows(&bytes, Some(&schema), None, None).expect("shape"); + let shaped = shape_returning_rows( + &bytes, + Some(&schema), + None, + None, + nodedb_crs::CustomCrsSet::empty(), + ) + .expect("shape"); assert_eq!(shaped.rows[0]["n"], Value::Integer(7)); assert_eq!(shaped.rows[0]["at"], Value::NaiveDateTime(at)); assert_eq!( @@ -513,7 +571,9 @@ mod tests { fn an_instant_cell_renders_iso8601_without_a_projection() { let at = NdbDateTime::from_micros(1_583_402_400_000_000); let bytes = typed_payload(&["at"], vec![vec![Value::DateTime(at)]]); - let shaped = shape_returning_rows(&bytes, None, None, None).expect("shape"); + let shaped = + shape_returning_rows(&bytes, None, None, None, nodedb_crs::CustomCrsSet::empty()) + .expect("shape"); assert_eq!(shaped.rows[0]["at"], Value::DateTime(at)); assert_eq!( value_to_wire_json(&shaped.rows[0]["at"]), @@ -527,7 +587,14 @@ mod tests { fn an_unparseable_cell_stays_text() { let bytes = payload(&["i"], &[&[Some("not a number")]]); let schema = announced(&[("i", DdlColType::Int8)]); - let shaped = shape_returning_rows(&bytes, Some(&schema), None, None).expect("shape"); + let shaped = shape_returning_rows( + &bytes, + Some(&schema), + None, + None, + nodedb_crs::CustomCrsSet::empty(), + ) + .expect("shape"); assert_eq!(shaped.rows[0]["i"], text("not a number")); } @@ -536,7 +603,14 @@ mod tests { #[test] fn an_empty_payload_keeps_the_announced_columns() { let schema = announced(&[("id", DdlColType::Text), ("score", DdlColType::Int8)]); - let shaped = shape_returning_rows(&[], Some(&schema), None, None).expect("shape"); + let shaped = shape_returning_rows( + &[], + Some(&schema), + None, + None, + nodedb_crs::CustomCrsSet::empty(), + ) + .expect("shape"); assert_eq!(shaped.columns, ["id", "score"]); assert!(shaped.rows.is_empty()); } @@ -547,7 +621,14 @@ mod tests { fn a_rowless_payload_keeps_the_announced_columns() { let bytes = payload(&[], &[]); let schema = announced(&[("id", DdlColType::Text)]); - let shaped = shape_returning_rows(&bytes, Some(&schema), None, None).expect("shape"); + let shaped = shape_returning_rows( + &bytes, + Some(&schema), + None, + None, + nodedb_crs::CustomCrsSet::empty(), + ) + .expect("shape"); assert_eq!(shaped.columns, ["id"]); assert!(shaped.rows.is_empty()); } @@ -558,14 +639,30 @@ mod tests { #[test] fn a_malformed_payload_fails_when_columns_were_announced() { let schema = announced(&[("id", DdlColType::Text)]); - assert!(shape_returning_rows(&[0xFF, 0xFE], Some(&schema), None, None).is_err()); + assert!( + shape_returning_rows( + &[0xFF, 0xFE], + Some(&schema), + None, + None, + nodedb_crs::CustomCrsSet::empty() + ) + .is_err() + ); } /// With nothing announced there is no contract to violate, so the legacy /// single-column fallback still applies. #[test] fn a_malformed_payload_falls_back_when_nothing_was_announced() { - let shaped = shape_returning_rows(&[0xFF, 0xFE], None, None, None).expect("fallback"); + let shaped = shape_returning_rows( + &[0xFF, 0xFE], + None, + None, + None, + nodedb_crs::CustomCrsSet::empty(), + ) + .expect("fallback"); assert_eq!(shaped.columns, ["result"]); } } diff --git a/nodedb/src/control/server/response_shape/stamp/evaluate.rs b/nodedb/src/control/server/response_shape/stamp/evaluate.rs index 403aa28d0..951427d1d 100644 --- a/nodedb/src/control/server/response_shape/stamp/evaluate.rs +++ b/nodedb/src/control/server/response_shape/stamp/evaluate.rs @@ -27,6 +27,7 @@ pub fn stamp_rows( rows: &mut [ShapedRow], columns: &[CpComputedColumn], access: &dyn SequenceAccess, + crs: &nodedb_crs::CrsContext<'_>, ) -> crate::Result<()> { if columns.is_empty() { return Ok(()); @@ -34,8 +35,8 @@ pub fn stamp_rows( for row in rows.iter_mut() { let mut doc = row_document(row); for column in columns { - let resolved = resolve_accessors(&column.expr, &doc, access)?; - let value = resolved.eval(&doc)?; + let resolved = resolve_accessors(&column.expr, &doc, access, crs)?; + let value = resolved.eval(&doc, crs)?; if let Some(map) = doc.as_object_mut() { map.insert(column.alias.clone(), value.clone()); } @@ -126,7 +127,13 @@ mod tests { fn each_row_gets_its_own_nextval_in_row_order() { let access = FakeAccess::new(); let mut rows = rows(3); - stamp_rows(&mut rows, &[column("n", accessor("nextval"))], &access).expect("stamp"); + stamp_rows( + &mut rows, + &[column("n", accessor("nextval"))], + &access, + &nodedb_crs::CrsContext::builtin_only(), + ) + .expect("stamp"); let stamped: Vec<&Value> = rows.iter().map(|r| &r["n"]).collect(); assert_eq!( stamped, @@ -145,6 +152,7 @@ mod tests { column("c", accessor("currval")), ], &access, + &nodedb_crs::CrsContext::builtin_only(), ) .expect("stamp"); assert_eq!(rows[0]["n"], rows[0]["c"]); @@ -170,6 +178,7 @@ mod tests { ), ], &access, + &nodedb_crs::CrsContext::builtin_only(), ) .expect("stamp"); assert_eq!(rows[0]["doubled"], Value::Integer(2)); @@ -190,6 +199,7 @@ mod tests { }, )], &access, + &nodedb_crs::CrsContext::builtin_only(), ) .expect("stamp"); assert_eq!(rows[0]["tagged"], Value::Integer(1)); @@ -200,8 +210,13 @@ mod tests { fn currval_before_nextval_fails_with_its_own_class() { let access = FakeAccess::new(); let mut rows = rows(1); - let err = stamp_rows(&mut rows, &[column("c", accessor("currval"))], &access) - .expect_err("must fail"); + let err = stamp_rows( + &mut rows, + &[column("c", accessor("currval"))], + &access, + &nodedb_crs::CrsContext::builtin_only(), + ) + .expect_err("must fail"); assert!(matches!( err, crate::Error::ObjectNotInPrerequisiteState { .. } @@ -223,6 +238,7 @@ mod tests { }, )], &access, + &nodedb_crs::CrsContext::builtin_only(), ) .expect_err("must fail"); assert!(matches!(err, crate::Error::DivisionByZero)); @@ -232,7 +248,13 @@ mod tests { fn no_columns_leaves_rows_untouched() { let access = FakeAccess::new(); let mut rows = rows(2); - stamp_rows(&mut rows, &[], &access).expect("stamp"); + stamp_rows( + &mut rows, + &[], + &access, + &nodedb_crs::CrsContext::builtin_only(), + ) + .expect("stamp"); assert!(rows.iter().all(|r| r.len() == 1)); } } diff --git a/nodedb/src/control/server/response_shape/stamp/substitute.rs b/nodedb/src/control/server/response_shape/stamp/substitute.rs index 5d2a0f75e..2bfc4334b 100644 --- a/nodedb/src/control/server/response_shape/stamp/substitute.rs +++ b/nodedb/src/control/server/response_shape/stamp/substitute.rs @@ -19,6 +19,7 @@ pub(super) fn resolve_accessors( expr: &SqlExpr, row: &Value, access: &dyn SequenceAccess, + crs: &nodedb_crs::CrsContext<'_>, ) -> crate::Result { match expr { SqlExpr::Column(_) @@ -26,15 +27,15 @@ pub(super) fn resolve_accessors( | SqlExpr::OldColumn(_) | SqlExpr::ExcludedColumn(_) => Ok(expr.clone()), SqlExpr::BinaryOp { left, op, right } => Ok(SqlExpr::BinaryOp { - left: Box::new(resolve_accessors(left, row, access)?), + left: Box::new(resolve_accessors(left, row, access, crs)?), op: *op, - right: Box::new(resolve_accessors(right, row, access)?), + right: Box::new(resolve_accessors(right, row, access, crs)?), }), SqlExpr::Negate(inner) => Ok(SqlExpr::Negate(Box::new(resolve_accessors( - inner, row, access, + inner, row, access, crs, )?))), SqlExpr::Cast { expr, to_type } => Ok(SqlExpr::Cast { - expr: Box::new(resolve_accessors(expr, row, access)?), + expr: Box::new(resolve_accessors(expr, row, access, crs)?), to_type: to_type.clone(), }), SqlExpr::Case { @@ -42,41 +43,41 @@ pub(super) fn resolve_accessors( when_thens, else_expr, } => Ok(SqlExpr::Case { - operand: resolve_boxed(operand.as_deref(), row, access)?, + operand: resolve_boxed(operand.as_deref(), row, access, crs)?, when_thens: when_thens .iter() .map(|(when, then)| { Ok(( - resolve_accessors(when, row, access)?, - resolve_accessors(then, row, access)?, + resolve_accessors(when, row, access, crs)?, + resolve_accessors(then, row, access, crs)?, )) }) .collect::>>()?, - else_expr: resolve_boxed(else_expr.as_deref(), row, access)?, + else_expr: resolve_boxed(else_expr.as_deref(), row, access, crs)?, }), - SqlExpr::Coalesce(items) => Ok(SqlExpr::Coalesce(resolve_list(items, row, access)?)), + SqlExpr::Coalesce(items) => Ok(SqlExpr::Coalesce(resolve_list(items, row, access, crs)?)), SqlExpr::NullIf(left, right) => Ok(SqlExpr::NullIf( - Box::new(resolve_accessors(left, row, access)?), - Box::new(resolve_accessors(right, row, access)?), + Box::new(resolve_accessors(left, row, access, crs)?), + Box::new(resolve_accessors(right, row, access, crs)?), )), SqlExpr::IsNull { expr, negated } => Ok(SqlExpr::IsNull { - expr: Box::new(resolve_accessors(expr, row, access)?), + expr: Box::new(resolve_accessors(expr, row, access, crs)?), negated: *negated, }), SqlExpr::Function { name, args } => { - let args = resolve_list(args, row, access)?; + let args = resolve_list(args, row, access, crs)?; let lowered = name.to_ascii_lowercase(); match lowered.as_str() { "nextval" => { - let sequence = sequence_name(&lowered, &args, row)?; + let sequence = sequence_name(&lowered, &args, row, crs)?; Ok(SqlExpr::Literal(Value::Integer(access.nextval(&sequence)?))) } "currval" => { - let sequence = sequence_name(&lowered, &args, row)?; + let sequence = sequence_name(&lowered, &args, row, crs)?; Ok(SqlExpr::Literal(Value::Integer(access.currval(&sequence)?))) } "setval" => { - let (sequence, value) = setval_args(&args, row)?; + let (sequence, value) = setval_args(&args, row, crs)?; Ok(SqlExpr::Literal(Value::Integer( access.setval(&sequence, value)?, ))) @@ -94,8 +95,9 @@ fn resolve_boxed( expr: Option<&SqlExpr>, row: &Value, access: &dyn SequenceAccess, + crs: &nodedb_crs::CrsContext<'_>, ) -> crate::Result>> { - expr.map(|e| resolve_accessors(e, row, access).map(Box::new)) + expr.map(|e| resolve_accessors(e, row, access, crs).map(Box::new)) .transpose() } @@ -103,16 +105,22 @@ fn resolve_list( items: &[SqlExpr], row: &Value, access: &dyn SequenceAccess, + crs: &nodedb_crs::CrsContext<'_>, ) -> crate::Result> { items .iter() - .map(|item| resolve_accessors(item, row, access)) + .map(|item| resolve_accessors(item, row, access, crs)) .collect() } /// The single sequence-name argument of `nextval` / `currval`, evaluated /// against the row. -fn sequence_name(function: &str, args: &[SqlExpr], row: &Value) -> crate::Result { +fn sequence_name( + function: &str, + args: &[SqlExpr], + row: &Value, + crs: &nodedb_crs::CrsContext<'_>, +) -> crate::Result { let [arg] = args else { return Err(crate::Error::PlanError { detail: format!( @@ -121,7 +129,7 @@ fn sequence_name(function: &str, args: &[SqlExpr], row: &Value) -> crate::Result ), }); }; - match arg.eval(row)? { + match arg.eval(row, crs)? { Value::String(name) => Ok(name), other => Err(crate::Error::PlanError { detail: format!( @@ -132,7 +140,11 @@ fn sequence_name(function: &str, args: &[SqlExpr], row: &Value) -> crate::Result } /// The `(name, value)` arguments of `setval`, both evaluated against the row. -fn setval_args(args: &[SqlExpr], row: &Value) -> crate::Result<(String, i64)> { +fn setval_args( + args: &[SqlExpr], + row: &Value, + crs: &nodedb_crs::CrsContext<'_>, +) -> crate::Result<(String, i64)> { let [name_arg, value_arg] = args else { return Err(crate::Error::PlanError { detail: format!( @@ -141,7 +153,7 @@ fn setval_args(args: &[SqlExpr], row: &Value) -> crate::Result<(String, i64)> { ), }); }; - let name = match name_arg.eval(row)? { + let name = match name_arg.eval(row, crs)? { Value::String(name) => name, other => { return Err(crate::Error::PlanError { @@ -151,7 +163,7 @@ fn setval_args(args: &[SqlExpr], row: &Value) -> crate::Result<(String, i64)> { }); } }; - let value = coerce_i64(&value_arg.eval(row)?).ok_or_else(|| crate::Error::PlanError { + let value = coerce_i64(&value_arg.eval(row, crs)?).ok_or_else(|| crate::Error::PlanError { detail: "setval() second argument must evaluate to a bigint".to_string(), })?; Ok((name, value)) @@ -237,8 +249,13 @@ mod tests { #[test] fn nextval_becomes_an_integer_literal() { let access = FakeAccess::new(); - let out = - resolve_accessors(&call("NEXTVAL", vec![text("s")]), &row(), &access).expect("resolve"); + let out = resolve_accessors( + &call("NEXTVAL", vec![text("s")]), + &row(), + &access, + &nodedb_crs::CrsContext::builtin_only(), + ) + .expect("resolve"); assert_eq!(out, SqlExpr::Literal(Value::Integer(1))); assert_eq!(access.calls.borrow().as_slice(), ["nextval:s"]); } @@ -262,12 +279,22 @@ mod tests { ], )), }; - let out = resolve_accessors(&expr, &row(), &access).expect("resolve"); + let out = resolve_accessors( + &expr, + &row(), + &access, + &nodedb_crs::CrsContext::builtin_only(), + ) + .expect("resolve"); assert_eq!( access.calls.borrow().as_slice(), ["nextval:s", "nextval:s", "setval:s=12"] ); - assert_eq!(out.eval(&row()).expect("eval"), Value::Integer(13)); + assert_eq!( + out.eval(&row(), &nodedb_crs::CrsContext::builtin_only()) + .expect("eval"), + Value::Integer(13) + ); } #[test] @@ -280,6 +307,7 @@ mod tests { &call("nextval", vec![SqlExpr::Column("seq".to_string())]), &row, &access, + &nodedb_crs::CrsContext::builtin_only(), ) .expect("resolve"); assert_eq!(access.calls.borrow().as_slice(), ["nextval:orders"]); @@ -292,6 +320,7 @@ mod tests { &call("currval", vec![SqlExpr::Literal(Value::Integer(3))]), &row(), &access, + &nodedb_crs::CrsContext::builtin_only(), ) .expect_err("must fail"); assert!( @@ -304,11 +333,21 @@ mod tests { fn wrong_arity_is_a_plan_error() { let access = FakeAccess::new(); assert!(matches!( - resolve_accessors(&call("nextval", vec![]), &row(), &access), + resolve_accessors( + &call("nextval", vec![]), + &row(), + &access, + &nodedb_crs::CrsContext::builtin_only() + ), Err(crate::Error::PlanError { .. }) )); assert!(matches!( - resolve_accessors(&call("setval", vec![text("s")]), &row(), &access), + resolve_accessors( + &call("setval", vec![text("s")]), + &row(), + &access, + &nodedb_crs::CrsContext::builtin_only() + ), Err(crate::Error::PlanError { .. }) )); } @@ -320,6 +359,7 @@ mod tests { &call("setval", vec![text("s"), text(" 7 ")]), &row(), &access, + &nodedb_crs::CrsContext::builtin_only(), ) .expect("text"); resolve_accessors( @@ -329,6 +369,7 @@ mod tests { ), &row(), &access, + &nodedb_crs::CrsContext::builtin_only(), ) .expect("float"); assert!(matches!( @@ -339,6 +380,7 @@ mod tests { ), &row(), &access, + &nodedb_crs::CrsContext::builtin_only() ), Err(crate::Error::PlanError { .. }) )); @@ -355,7 +397,13 @@ mod tests { SqlExpr::Column("x".to_string()), call("abs", vec![call("nextval", vec![text("s")])]), ]); - let out = resolve_accessors(&expr, &row(), &access).expect("resolve"); + let out = resolve_accessors( + &expr, + &row(), + &access, + &nodedb_crs::CrsContext::builtin_only(), + ) + .expect("resolve"); assert_eq!( out, SqlExpr::Coalesce(vec![ diff --git a/nodedb/src/control/server/response_shape/types/plan_kind/graph.rs b/nodedb/src/control/server/response_shape/types/plan_kind/graph.rs index 0795dc56d..8bcd5a232 100644 --- a/nodedb/src/control/server/response_shape/types/plan_kind/graph.rs +++ b/nodedb/src/control/server/response_shape/types/plan_kind/graph.rs @@ -23,7 +23,6 @@ pub(super) fn describe_graph(op: &GraphOp) -> PlanKind { | GraphOp::BspSuperstep(_) | GraphOp::WccSuperstep(_) | GraphOp::TemporalNeighbors { .. } - | GraphOp::TemporalAlgorithm { .. } | GraphOp::Stats { .. } | GraphOp::NodePresenceRead { .. } => PlanKind::MultiRow, diff --git a/nodedb/src/control/server/response_shape/walk.rs b/nodedb/src/control/server/response_shape/walk.rs index edb8cfb7a..b922f5ed6 100644 --- a/nodedb/src/control/server/response_shape/walk.rs +++ b/nodedb/src/control/server/response_shape/walk.rs @@ -73,6 +73,7 @@ mod tests { options: GraphTraversalOptions::default(), rls_filters: Vec::new(), frontier_bitmap: None, + scope: nodedb_physical::physical_plan::GraphReadScope::default(), }) } @@ -83,9 +84,14 @@ mod tests { let names = vec!["a".to_string(), "c".to_string(), "b".to_string()]; let payload = zerompk::to_msgpack_vec(&names).expect("names encode"); let wrapped = apply_walk_wrap(&hop(), &payload).expect("walk wraps"); - let Ok(ShapeOutcome::Rows(shaped)) = - shape_payload_no_plan(&wrapped, PlanKind::MultiRow, None, None, None) - else { + let Ok(ShapeOutcome::Rows(shaped)) = shape_payload_no_plan( + &wrapped, + PlanKind::MultiRow, + None, + None, + None, + nodedb_crs::CustomCrsSet::empty(), + ) else { panic!("a walk shapes into rows"); }; let got: Vec = shaped diff --git a/nodedb/src/control/server/response_translate/dispatch.rs b/nodedb/src/control/server/response_translate/dispatch.rs index 51e2c341e..6eb9bda6d 100644 --- a/nodedb/src/control/server/response_translate/dispatch.rs +++ b/nodedb/src/control/server/response_translate/dispatch.rs @@ -240,7 +240,6 @@ async fn translate_graph_op( | GraphOp::BspSuperstep(_) | GraphOp::WccSuperstep(_) | GraphOp::TemporalNeighbors { .. } - | GraphOp::TemporalAlgorithm { .. } | GraphOp::Stats { .. } | GraphOp::NodeEdgeGuard { .. } | GraphOp::NodePresenceGuard { .. } diff --git a/nodedb/src/control/server/response_translate/vector.rs b/nodedb/src/control/server/response_translate/vector.rs index e4009f8a0..8abe20826 100644 --- a/nodedb/src/control/server/response_translate/vector.rs +++ b/nodedb/src/control/server/response_translate/vector.rs @@ -57,13 +57,19 @@ pub struct VectorHitCut<'a> { /// Apply the residual filter, then truncate to `top_k`. With /// `strip_bodies`, drop the body bytes the Data Plane attached only for the /// predicate. A no-op when `rls_filters` is empty. -fn apply_rls_filter(hits: &mut Vec, rls_filters: &[u8], top_k: usize, strip_bodies: bool) { +fn apply_rls_filter( + hits: &mut Vec, + rls_filters: &[u8], + top_k: usize, + strip_bodies: bool, + crs: &nodedb_crs::CrsContext<'_>, +) { // When filters are empty the Data Plane ranks exactly `top_k` and // attaches a body only for projection, so there is nothing to do. if rls_filters.is_empty() { return; } - admit_hits(hits, rls_filters, top_k); + admit_hits(hits, rls_filters, top_k, crs); if strip_bodies { for h in hits.iter_mut() { h.body = None; @@ -77,7 +83,12 @@ fn apply_rls_filter(hits: &mut Vec, rls_filters: &[u8], top_k: usize, strip /// The Data Plane ranks a wider window when a residual filter applies: the /// statement's `WHERE` conjuncts and any read policy. The filter and the cut /// run here, on the Control Plane. -fn admit_hits(hits: &mut Vec, rls_filters: &[u8], top_k: usize) { +fn admit_hits( + hits: &mut Vec, + rls_filters: &[u8], + top_k: usize, + crs: &nodedb_crs::CrsContext<'_>, +) { let filters: Vec = match zerompk::from_msgpack(rls_filters) { Ok(f) => f, Err(_) => { @@ -96,7 +107,7 @@ fn admit_hits(hits: &mut Vec, rls_filters: &[u8], top_k: usize) { // request without larger dispatch-chain changes, and dropping hits is // the strictly safer outcome for an RLS filter regardless. hits.retain(|h| match h.body.as_deref() { - Some(body) => ScanFilter::all_match_binary(&filters, body).unwrap_or(false), + Some(body) => ScanFilter::all_match_binary(&filters, body, crs).unwrap_or(false), None => false, }); if hits.len() > top_k { @@ -115,6 +126,7 @@ pub(crate) fn filter_vector_hits( payload: &[u8], rls_filters: &[u8], top_k: usize, + crs: &nodedb_crs::CrsContext<'_>, ) -> crate::Result> { if rls_filters.is_empty() { return Ok(payload.to_vec()); @@ -124,7 +136,7 @@ pub(crate) fn filter_vector_hits( format: "msgpack".into(), detail: format!("vector search hits do not decode: {e}"), })?; - admit_hits(&mut hits, rls_filters, top_k); + admit_hits(&mut hits, rls_filters, top_k, crs); zerompk::to_msgpack_vec(&hits).map_err(|e| crate::Error::Serialization { format: "msgpack".into(), detail: format!("vector search hits do not encode: {e}"), @@ -159,7 +171,13 @@ pub async fn translate_vector_search_payload( detail: format!("vector search hits do not decode: {e}"), })?; - apply_rls_filter(&mut hits, cut.rls_filters, cut.top_k, cut.strip_bodies); + apply_rls_filter( + &mut hits, + cut.rls_filters, + cut.top_k, + cut.strip_bodies, + &crate::control::crs_context::crs_context_for(state), + ); let FlattenedHits { mut rows, unnamed } = flatten_hits(&hits)?; let keys = resolve_surrogate_pks( diff --git a/nodedb/src/control/server/shared/authorization/requirements.rs b/nodedb/src/control/server/shared/authorization/requirements.rs index f95b2f066..1f5e864dc 100644 --- a/nodedb/src/control/server/shared/authorization/requirements.rs +++ b/nodedb/src/control/server/shared/authorization/requirements.rs @@ -58,7 +58,7 @@ mod tests { nodedb_physical::physical_plan::DocumentPageSource { collection: QualifiedCollection::new(DatabaseId::DEFAULT, "source"), filters: Vec::new(), - system_as_of_ms: None, + system_time: nodedb_types::SystemTimeScope::Current, offset: 0, limit: None, column_map: Vec::new(), diff --git a/nodedb/src/control/server/shared/check_constraint/enforce.rs b/nodedb/src/control/server/shared/check_constraint/enforce.rs index ae2f2bff1..b83323af9 100644 --- a/nodedb/src/control/server/shared/check_constraint/enforce.rs +++ b/nodedb/src/control/server/shared/check_constraint/enforce.rs @@ -34,7 +34,11 @@ pub async fn enforce_check_constraints( if constraint.has_subquery { enforce_subquery_check(state, identity, database_id, constraint, fields).await?; } else { - enforce_simple_check(constraint, fields)?; + enforce_simple_check( + constraint, + fields, + &crate::control::crs_context::crs_context_for(state), + )?; } } diff --git a/nodedb/src/control/server/shared/check_constraint/simple.rs b/nodedb/src/control/server/shared/check_constraint/simple.rs index 8c169bbe7..027b1160d 100644 --- a/nodedb/src/control/server/shared/check_constraint/simple.rs +++ b/nodedb/src/control/server/shared/check_constraint/simple.rs @@ -16,6 +16,7 @@ use crate::control::server::shared::ddl::result::DdlError; pub(super) fn enforce_simple_check( constraint: &CheckConstraintDef, fields: &HashMap, + crs: &nodedb_crs::CrsContext<'_>, ) -> Result<(), DdlError> { // Strip NEW. prefixes to get bare column references. let bare_expr = strip_new_prefix(&constraint.check_sql); @@ -35,13 +36,12 @@ pub(super) fn enforce_simple_check( // Build a Value::Object from the fields for evaluation. let doc = nodedb_types::Value::Object(fields.clone()); - // Evaluate the expression against the document. A division/modulo-by-zero - // is neither a PASS nor an ordinary FAIL — it's an evaluation error, - // reported under SQLSTATE 22012 rather than folded - // into the 23514 (check_violation) the ordinary-failure arm below uses. - let result = expr.eval(&doc).map_err(|e| { + // Evaluate the expression against the document. An evaluation error is + // neither a PASS nor an ordinary FAIL. It keeps its own SQLSTATE rather + // than folding into the 23514 (check_violation) of the failure arm below. + let result = expr.eval(&doc, crs).map_err(|e| { ddl_err( - nodedb_types::error::sqlstate::DIVISION_BY_ZERO, + crate::control::server::shared::ddl::sqlstate::eval_error_sqlstate(&e), &format!( "CHECK constraint '{}' failed to evaluate: {}: {e}", constraint.name, constraint.check_sql diff --git a/nodedb/src/control/server/shared/clone_read/entry.rs b/nodedb/src/control/server/shared/clone_read/entry.rs index 22f3549c3..3c7c37e73 100644 --- a/nodedb/src/control/server/shared/clone_read/entry.rs +++ b/nodedb/src/control/server/shared/clone_read/entry.rs @@ -59,20 +59,9 @@ pub(in crate::control::server) async fn maybe_intercept_clone_read( roles, emitter, } = params; - // With `system_as_of_ms`, query_lsn is the highest LSN committed by that - // time, and a time before the oldest retained anchor is refused. Without - // it, query_lsn is the current WAL LSN. - let (query_lsn, query_ms) = if let Some(as_of_ms) = extract_system_as_of_ms(Some(&task.plan)) { - let lsn = state.ms_to_lsn(as_of_ms)?; - (lsn, Some(as_of_ms)) - } else { - let lsn = state.wal.next_lsn(); - let ms = state.ms_to_lsn_inverse(lsn); - (lsn, ms) - }; + // The resolver maps the `AS OF` time to an LSN only for a live clone. let resolve_params = CloneReadParams { - query_lsn, - query_ms, + as_of_ms: extract_system_as_of_ms(Some(&task.plan)), }; let Some(outcome) = resolve_read(state, task.clone(), tenant_id, &resolve_params).await? else { diff --git a/nodedb/src/control/server/shared/cluster_array_dispatch.rs b/nodedb/src/control/server/shared/cluster_array_dispatch.rs index 50da36f14..f96f98b5a 100644 --- a/nodedb/src/control/server/shared/cluster_array_dispatch.rs +++ b/nodedb/src/control/server/shared/cluster_array_dispatch.rs @@ -146,6 +146,7 @@ pub(crate) async fn execute_cluster_array( projection, Some(redaction.ctx(&state.redaction)), None, + &crate::control::crs_context::crs_set_for(state), )? { ShapeOutcome::Rows(shaped) => Ok(ClusterArrayShaped::Rows(shaped)), ShapeOutcome::Passthrough => match cluster_plan_kind { diff --git a/nodedb/src/control/server/shared/ddl/neutral/alert/create.rs b/nodedb/src/control/server/shared/ddl/neutral/alert/create.rs index a2e63bf55..311313662 100644 --- a/nodedb/src/control/server/shared/ddl/neutral/alert/create.rs +++ b/nodedb/src/control/server/shared/ddl/neutral/alert/create.rs @@ -101,6 +101,18 @@ pub async fn create_alert( // Parse NOTIFY targets. let notify_targets = parse_notify_targets_raw(notify_targets_raw)?; + // A webhook URL is user-supplied: it passes the `[egress]` policy here, and + // again before every send. + for target in ¬ify_targets { + if let NotifyTarget::Webhook { url } = target { + state.remote_sources.egress().check_url(url).map_err(|e| { + err( + "42501", + format!("NOTIFY WEBHOOK fails the [egress] policy: {e}"), + ) + })?; + } + } let now = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) diff --git a/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/decode.rs b/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/decode.rs index 13026540d..a28426725 100644 --- a/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/decode.rs +++ b/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/decode.rs @@ -230,6 +230,7 @@ fn parse_coord(text: &str, dtype: ArrayDimType) -> Option { fn parse_attr(text: &str, dtype: ArrayAttrType) -> Option { match dtype { ArrayAttrType::Int64 + | ArrayAttrType::Int8 | ArrayAttrType::UInt8 | ArrayAttrType::Int16 | ArrayAttrType::UInt16 @@ -396,11 +397,13 @@ mod tests { name: "b1".into(), dtype: ArrayAttrType::Float64, nullable: false, + meta: Default::default(), }, ArrayAttrAst { name: "label".into(), dtype: ArrayAttrType::String, nullable: true, + meta: Default::default(), }, ], tile_extents: vec![64], @@ -465,6 +468,7 @@ mod tests { name: name.into(), dtype, nullable: true, + meta: Default::default(), }; ArrayCatalogView { name: "scene".into(), diff --git a/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/adopt.rs b/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/adopt.rs new file mode 100644 index 000000000..3c453906e --- /dev/null +++ b/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/adopt.rs @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! `ADOPT_METADATA true`: write the attr metadata the files declare into +//! the array schema before the first row loads. +//! +//! One catalog entry carries every attr's change, built from the committed +//! schema under the DDL preparation lease, as `ALTER ARRAY ... ALTER ATTR` +//! builds its own. Metadata sits outside the schema content, so +//! `schema_hash` and every segment stay valid. A value the attr type cannot +//! hold is refused (`22000`) before anything loads. The change is DDL, so a +//! COPY inside a transaction block cannot make it (`25001`). + +use nodedb_sql::types_array::ArrayAttrMetaAlter; + +use crate::control::array_catalog::ArrayCatalogEntry; +use crate::control::array_catalog::attr_meta::remeta_schema; +use crate::control::array_catalog::ddl::propose_array_entries; +use crate::control::array_catalog::schema_recode::check_same_content; +use crate::control::catalog_entry::CatalogEntry; +use crate::control::server::shared::ddl::result::DdlError; +use crate::control::trigger::statement_txn::in_block; + +use super::super::dispatch::ArrayLoadCtx; + +/// Apply `changes` to `array`'s attr metadata in one catalog entry. +pub(super) async fn adopt_attr_metadata( + ctx: &ArrayLoadCtx<'_>, + array: &str, + changes: &[ArrayAttrMetaAlter], +) -> Result<(), DdlError> { + if changes.is_empty() { + return Ok(()); + } + if in_block(ctx.txn_ctx) { + return Err(DdlError::new( + "25001", + format!( + "COPY {array}: ADOPT_METADATA changes the array schema; run the COPY outside a \ + transaction block" + ), + )); + } + let tenant_id = ctx.identity.tenant_id; + let database_id = ctx.database_id; + let statement = format!("COPY {array} WITH (ADOPT_METADATA true)"); + propose_array_entries(ctx.state, |catalog| { + let current = catalog + .get_array_in_database(tenant_id, database_id, array)? + .ok_or_else(|| crate::Error::PlanError { + detail: format!("{statement}: array not found"), + })?; + let mut schema = current.schema_msgpack.clone(); + for change in changes { + schema = remeta_schema(&statement, &schema, change)?; + } + check_same_content(&statement, ¤t.schema_msgpack, &schema)?; + Ok(vec![CatalogEntry::PutArray(Box::new(ArrayCatalogEntry { + schema_msgpack: schema, + ..current + }))]) + }) + .await + .map_err(DdlError::from) +} diff --git a/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/bands.rs b/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/bands.rs deleted file mode 100644 index 4d71dfe85..000000000 --- a/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/bands.rs +++ /dev/null @@ -1,280 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 - -//! Which array attr each GeoTIFF band loads into, and the type rule. -//! -//! `BANDS (a, SKIP, b)` maps file bands in order: band 0 to `a`, band 1 -//! nowhere, band 2 to `b`. Without `BANDS`, band `i` loads into attr `i`. -//! A band loads only into an attr that holds every value of its sample type -//! exactly. An attr no band loads into must be nullable, and every cell -//! holds NULL there. - -use nodedb_raster::SampleType; -use nodedb_sql::catalog::ArrayCatalogView; -use nodedb_sql::ddl_ast::statement::CopyBand; -use nodedb_sql::types_array::ArrayAttrType; - -use super::super::decode::RowError; - -/// Where one attr's value comes from. -#[derive(Debug, Clone, PartialEq)] -pub(super) enum AttrSource { - /// No band loads into the attr. It is nullable. - Null, - /// File band `band` loads into the attr. - Band { - band: usize, - /// Whether the attr holds floats, so integer samples widen to `f64`. - float: bool, - nullable: bool, - name: String, - }, -} - -/// The attr sources in schema order, and the file bands that load. -#[derive(Debug)] -pub(super) struct BandMap { - pub attrs: Vec, - pub loaded: Vec, -} - -/// Map `band_count` file bands of `sample` onto the attrs of `view`. -pub(super) fn map_bands( - view: &ArrayCatalogView, - band_count: u16, - sample: SampleType, - bands: Option<&[CopyBand]>, -) -> Result { - let targets = band_targets(view, band_count, bands)?; - let mut attrs = vec![AttrSource::Null; view.attrs.len()]; - let mut loaded = Vec::with_capacity(targets.len()); - for (band, target) in targets.into_iter().enumerate() { - let Some(index) = target else { continue }; - let Some(attr) = view.attrs.get(index) else { - continue; - }; - if !widens(sample, attr.dtype) { - return Err(RowError::new( - 0, - "42804", - format!( - "band {} holds {sample} samples, which attr '{}' ({}) cannot hold exactly; \ - declare the attr as a type that widens {sample}", - band + 1, - attr.name, - attr.dtype.sql_name() - ), - )); - } - if let Some(slot) = attrs.get_mut(index) { - *slot = AttrSource::Band { - band, - float: attr.dtype.is_float(), - nullable: attr.nullable, - name: attr.name.clone(), - }; - } - loaded.push(band); - } - if loaded.is_empty() { - return Err(RowError::new( - 0, - "22023", - "no file band loads into an attr".to_string(), - )); - } - for (attr, source) in view.attrs.iter().zip(&attrs) { - if *source == AttrSource::Null && !attr.nullable { - return Err(RowError::new( - 0, - "23502", - format!( - "attr '{}' is NOT NULL, and no file band loads into it; map a band to it \ - with BANDS (...)", - attr.name - ), - )); - } - } - Ok(BandMap { attrs, loaded }) -} - -/// The attr index each file band loads into, `None` for a skipped band. -fn band_targets( - view: &ArrayCatalogView, - band_count: u16, - bands: Option<&[CopyBand]>, -) -> Result>, RowError> { - let count = usize::from(band_count); - let Some(bands) = bands else { - if count > view.attrs.len() { - return Err(RowError::new( - 0, - "22023", - format!( - "the file has {count} bands, array '{}' has {} attrs; \ - name each band's attr or SKIP with BANDS (...)", - view.name, - view.attrs.len() - ), - )); - } - return Ok((0..count).map(Some).collect()); - }; - if bands.len() != count { - return Err(RowError::new( - 0, - "22023", - format!( - "BANDS lists {} entries, the file has {count} bands; list one entry per band", - bands.len() - ), - )); - } - bands - .iter() - .map(|band| match band { - CopyBand::Skip => Ok(None), - CopyBand::Attr(name) => view - .attrs - .iter() - .position(|attr| attr.name.eq_ignore_ascii_case(name)) - .map(Some) - .ok_or_else(|| { - RowError::new( - 0, - "42703", - format!( - "BANDS names '{name}', which array '{}' has no attr", - view.name - ), - ) - }), - }) - .collect() -} - -/// Whether every `sample` value converts to `attr` without loss. -fn widens(sample: SampleType, attr: ArrayAttrType) -> bool { - use ArrayAttrType as A; - match sample { - SampleType::U8 => matches!( - attr, - A::UInt8 - | A::Int16 - | A::UInt16 - | A::Int32 - | A::UInt32 - | A::Int64 - | A::Float32 - | A::Float64 - ), - SampleType::U16 => matches!( - attr, - A::UInt16 | A::Int32 | A::UInt32 | A::Int64 | A::Float32 | A::Float64 - ), - SampleType::I16 => matches!( - attr, - A::Int16 | A::Int32 | A::Int64 | A::Float32 | A::Float64 - ), - SampleType::U32 => matches!(attr, A::UInt32 | A::Int64 | A::Float64), - SampleType::F32 => matches!(attr, A::Float32 | A::Float64), - SampleType::F64 => matches!(attr, A::Float64), - } -} - -#[cfg(test)] -mod tests { - use super::*; - use nodedb_sql::types_array::ArrayAttrAst; - - fn view(attrs: &[(&str, ArrayAttrType, bool)]) -> ArrayCatalogView { - ArrayCatalogView { - name: "scene".into(), - dims: Vec::new(), - attrs: attrs - .iter() - .map(|(name, dtype, nullable)| ArrayAttrAst { - name: (*name).into(), - dtype: *dtype, - nullable: *nullable, - }) - .collect(), - tile_extents: Vec::new(), - georef: None, - } - } - - #[test] - fn widening_is_lossless_only() { - use ArrayAttrType as A; - assert!(widens(SampleType::U8, A::UInt8)); - assert!(widens(SampleType::U16, A::Float32)); - assert!(!widens(SampleType::U16, A::Int16)); - assert!(!widens(SampleType::I16, A::UInt16)); - assert!(!widens(SampleType::U32, A::Float32)); - assert!(!widens(SampleType::F32, A::Int64)); - assert!(!widens(SampleType::F64, A::Float32)); - assert!(!widens(SampleType::U8, A::String)); - } - - #[test] - fn bands_load_in_order_without_a_list() { - let v = view(&[ - ("b1", ArrayAttrType::UInt16, false), - ("b2", ArrayAttrType::Float64, true), - ("extra", ArrayAttrType::Int64, true), - ]); - let map = map_bands(&v, 2, SampleType::U16, None).expect("maps"); - assert_eq!(map.loaded, vec![0, 1]); - assert!(matches!( - map.attrs[1], - AttrSource::Band { - band: 1, - float: true, - .. - } - )); - assert_eq!(map.attrs[2], AttrSource::Null); - } - - #[test] - fn a_band_list_skips_and_reorders() { - let v = view(&[ - ("red", ArrayAttrType::UInt16, true), - ("nir", ArrayAttrType::UInt16, true), - ]); - let list = [ - CopyBand::Attr("nir".into()), - CopyBand::Skip, - CopyBand::Attr("red".into()), - ]; - let map = map_bands(&v, 3, SampleType::U16, Some(&list)).expect("maps"); - assert_eq!(map.loaded, vec![0, 2]); - assert!(matches!(map.attrs[0], AttrSource::Band { band: 2, .. })); - assert!(matches!(map.attrs[1], AttrSource::Band { band: 0, .. })); - } - - #[test] - fn refusals_carry_their_sqlstate() { - let narrow = view(&[("b1", ArrayAttrType::UInt8, true)]); - let err = map_bands(&narrow, 1, SampleType::U16, None).expect_err("u16 into UINT8"); - assert_eq!(err.sqlstate, "42804"); - - let strict = view(&[ - ("b1", ArrayAttrType::UInt16, true), - ("qa", ArrayAttrType::UInt16, false), - ]); - let err = map_bands(&strict, 1, SampleType::U16, None).expect_err("qa unmapped"); - assert_eq!(err.sqlstate, "23502"); - - let one = view(&[("b1", ArrayAttrType::UInt16, true)]); - let err = map_bands(&one, 2, SampleType::U16, None).expect_err("2 bands, 1 attr"); - assert_eq!(err.sqlstate, "22023"); - let short = [CopyBand::Attr("b1".into())]; - let err = map_bands(&one, 2, SampleType::U16, Some(&short)).expect_err("short list"); - assert_eq!(err.sqlstate, "22023"); - let unknown = [CopyBand::Attr("b9".into())]; - let err = map_bands(&one, 1, SampleType::U16, Some(&unknown)).expect_err("no attr b9"); - assert_eq!(err.sqlstate, "42703"); - } -} diff --git a/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/driver/array_view.rs b/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/driver/array_view.rs new file mode 100644 index 000000000..82adc5c31 --- /dev/null +++ b/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/driver/array_view.rs @@ -0,0 +1,199 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! The catalog's array view in the plain form the load planner reads, and +//! the planner's rows and adopted metadata back in the catalog's form. + +use nodedb_raster::GeoTransform; +use nodedb_raster_load::{ + AdoptedMeta, ArrayView, AttrLiteral, AttrMeta, AttrType, CoordLiteral, DimType, DomainBound, + LoadRow, ViewAttr, ViewDim, ViewGeoref, +}; +use nodedb_sql::catalog::ArrayCatalogView; +use nodedb_sql::types_array::{ + ArrayAttrAst, ArrayAttrLiteral, ArrayAttrMetaAlter, ArrayAttrMetaAst, ArrayAttrMetaChange, + ArrayAttrType, ArrayCoordLiteral, ArrayDimAst, ArrayDimType, ArrayDomainBound, ArrayGeorefAst, + ArrayInsertRow, +}; + +/// The planner's view of `view`. +pub(crate) fn array_view(view: &ArrayCatalogView) -> ArrayView { + ArrayView { + name: view.name.clone(), + dims: view.dims.iter().map(view_dim).collect(), + attrs: view.attrs.iter().map(view_attr).collect(), + georef: view.georef.as_ref().map(view_georef), + } +} + +/// The planner's form of a georef. +pub(crate) fn view_georef(georef: &ArrayGeorefAst) -> ViewGeoref { + ViewGeoref { + srid: georef.srid, + x_dim: georef.x_dim.clone(), + y_dim: georef.y_dim.clone(), + transform: georef.transform.map(GeoTransform), + } +} + +/// A planner row as an array insert row. +pub(crate) fn insert_row(row: LoadRow) -> ArrayInsertRow { + ArrayInsertRow { + coords: row + .coords + .into_iter() + .map(|coord| match coord { + CoordLiteral::Int64(v) => ArrayCoordLiteral::Int64(v), + CoordLiteral::Float64(v) => ArrayCoordLiteral::Float64(v), + }) + .collect(), + attrs: row + .attrs + .into_iter() + .map(|attr| match attr { + AttrLiteral::Int64(v) => ArrayAttrLiteral::Int64(v), + AttrLiteral::Float64(v) => ArrayAttrLiteral::Float64(v), + AttrLiteral::Null => ArrayAttrLiteral::Null, + }) + .collect(), + } +} + +/// Adopted metadata as the `ALTER ATTR ... SET` it makes. +pub(crate) fn meta_alter(adopted: &AdoptedMeta) -> ArrayAttrMetaAlter { + let meta = &adopted.meta; + ArrayAttrMetaAlter { + attr: adopted.attr.clone(), + change: ArrayAttrMetaChange::Set(ArrayAttrMetaAst { + nodata: meta.nodata, + scale: meta.scale, + offset: meta.offset, + units: meta.units.clone(), + }), + } +} + +fn view_dim(dim: &ArrayDimAst) -> ViewDim { + ViewDim { + name: dim.name.clone(), + dtype: match dim.dtype { + ArrayDimType::Int64 => DimType::Int64, + ArrayDimType::Float64 => DimType::Float64, + ArrayDimType::TimestampMs => DimType::TimestampMs, + ArrayDimType::String => DimType::String, + }, + lo: bound(&dim.lo), + hi: bound(&dim.hi), + } +} + +fn bound(bound: &ArrayDomainBound) -> DomainBound { + match bound { + ArrayDomainBound::Int64(v) => DomainBound::Int64(*v), + ArrayDomainBound::Float64(v) => DomainBound::Float64(*v), + ArrayDomainBound::TimestampMs(v) => DomainBound::TimestampMs(*v), + ArrayDomainBound::String(v) => DomainBound::String(v.clone()), + } +} + +fn view_attr(attr: &ArrayAttrAst) -> ViewAttr { + ViewAttr { + name: attr.name.clone(), + dtype: attr_type(attr.dtype), + nullable: attr.nullable, + meta: AttrMeta { + nodata: attr.meta.nodata, + scale: attr.meta.scale, + offset: attr.meta.offset, + units: attr.meta.units.clone(), + }, + } +} + +fn attr_type(dtype: ArrayAttrType) -> AttrType { + match dtype { + ArrayAttrType::Int8 => AttrType::Int8, + ArrayAttrType::UInt8 => AttrType::UInt8, + ArrayAttrType::Int16 => AttrType::Int16, + ArrayAttrType::UInt16 => AttrType::UInt16, + ArrayAttrType::Int32 => AttrType::Int32, + ArrayAttrType::UInt32 => AttrType::UInt32, + ArrayAttrType::Int64 => AttrType::Int64, + ArrayAttrType::Float32 => AttrType::Float32, + ArrayAttrType::Float64 => AttrType::Float64, + ArrayAttrType::String => AttrType::String, + ArrayAttrType::Bytes => AttrType::Bytes, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn the_view_and_rows_carry_over() { + let catalog = ArrayCatalogView { + name: "scene".into(), + dims: vec![ArrayDimAst { + name: "t".into(), + dtype: ArrayDimType::TimestampMs, + lo: ArrayDomainBound::TimestampMs(0), + hi: ArrayDomainBound::TimestampMs(9), + }], + attrs: vec![ArrayAttrAst { + name: "red".into(), + dtype: ArrayAttrType::UInt16, + nullable: true, + meta: ArrayAttrMetaAst { + nodata: Some(0.0), + ..Default::default() + }, + }], + tile_extents: vec![8], + georef: Some(ArrayGeorefAst { + srid: 32_647, + x_dim: "col".into(), + y_dim: "row".into(), + transform: Some([0.0, 1.0, 0.0, 0.0, 0.0, -1.0]), + }), + }; + let view = array_view(&catalog); + assert_eq!(view.dims[0].dtype, DimType::TimestampMs); + assert_eq!(view.dims[0].hi, DomainBound::TimestampMs(9)); + assert_eq!(view.attrs[0].dtype, AttrType::UInt16); + assert_eq!(view.attrs[0].meta.nodata, Some(0.0)); + let georef = view.georef.expect("georef"); + assert_eq!( + georef.transform, + Some(GeoTransform([0.0, 1.0, 0.0, 0.0, 0.0, -1.0])) + ); + + let row = insert_row(LoadRow { + coords: vec![CoordLiteral::Int64(3), CoordLiteral::Float64(1.5)], + attrs: vec![AttrLiteral::Int64(7), AttrLiteral::Null], + }); + assert_eq!( + row.coords, + vec![ArrayCoordLiteral::Int64(3), ArrayCoordLiteral::Float64(1.5)] + ); + assert_eq!( + row.attrs, + vec![ArrayAttrLiteral::Int64(7), ArrayAttrLiteral::Null] + ); + + let alter = meta_alter(&AdoptedMeta { + attr: "red".into(), + meta: AttrMeta { + scale: Some(2.0), + ..Default::default() + }, + }); + assert_eq!(alter.attr, "red"); + assert!(matches!( + alter.change, + ArrayAttrMetaChange::Set(ArrayAttrMetaAst { + scale: Some(2.0), + .. + }) + )); + } +} diff --git a/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/driver/blocks.rs b/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/driver/blocks.rs new file mode 100644 index 000000000..8f6a153b1 --- /dev/null +++ b/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/driver/blocks.rs @@ -0,0 +1,236 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! Read one block index from every source: fetch the ranges on the Control +//! Plane, decode on Tokio's blocking pool. +//! +//! The block ranges of every source merge where they lie close together, +//! so a block costs one request per source when its data and mask chunks +//! sit near each other. The producer reads block indexes in order into a +//! queue of [`BLOCK_QUEUE`], so a load holds at most that many decoded +//! block sets, one in decode and one in the load. + +use std::sync::Arc; + +use nodedb_raster::{ByteRange, COALESCE_GAP, RasterBlock, RasterReader, TiffReader, coalesce}; +use tokio::sync::mpsc; + +use super::super::fetch::{FetchError, RangeFetch}; +use super::error::DriveError; + +/// Decoded block sets queued between the producer and the load. +pub const BLOCK_QUEUE: usize = 2; + +/// One block index, read from every source in source order. +pub type BlockSet = Vec; + +/// Fetch `ranges`, merging close ones, and answer each range in order. An +/// empty range answers with no bytes and no fetch. +pub async fn fetch_ranges( + fetch: &F, + ranges: &[ByteRange], +) -> Result>>, FetchError> { + let merged = coalesce(ranges.to_vec(), COALESCE_GAP); + let answers = if merged.is_empty() { + Vec::new() + } else { + fetch.fetch(&merged).await? + }; + Ok(ranges + .iter() + .map(|range| { + if range.is_empty() { + return Some(Vec::new()); + } + let (outer, answer) = merged.iter().zip(&answers).find(|(outer, _)| { + outer.object == range.object && outer.start <= range.start && range.end <= outer.end + })?; + let bytes = answer.as_ref()?; + let from = usize::try_from(range.start - outer.start).unwrap_or(usize::MAX); + let to = usize::try_from(range.end - outer.start).unwrap_or(usize::MAX); + let (from, to) = (from.min(bytes.len()), to.min(bytes.len())); + Some(bytes.get(from..to.max(from)).unwrap_or_default().to_vec()) + }) + .collect()) +} + +/// Read block `index` from every reader: fetch, then decode on the blocking +/// pool. +pub async fn read_index( + readers: &Arc>, + fetch: &F, + index: u32, +) -> Result { + let mut counts = Vec::with_capacity(readers.len()); + let mut ranges = Vec::new(); + for (source, reader) in readers.iter().enumerate() { + let block = reader + .block_ranges(index) + .map_err(|error| DriveError::Raster { + index: source, + error, + })?; + counts.push(block.len()); + ranges.extend(block); + } + let bytes = fetch_ranges(fetch, &ranges).await?; + let readers = Arc::clone(readers); + tokio::task::spawn_blocking(move || decode_set(&readers, index, &counts, &bytes)) + .await + .map_err(|e| DriveError::Task(e.to_string()))? +} + +/// Decode block `index` of each reader from its share of `bytes`. +fn decode_set( + readers: &[TiffReader], + index: u32, + counts: &[usize], + bytes: &[Option>], +) -> Result { + let mut set = Vec::with_capacity(readers.len()); + let mut rest = bytes; + for (source, (reader, count)) in readers.iter().zip(counts).enumerate() { + let (mine, after) = rest.split_at((*count).min(rest.len())); + rest = after; + let borrowed: Vec> = mine.iter().map(Option::as_deref).collect(); + let block = reader + .decode_block(index, &borrowed) + .map_err(|error| DriveError::Raster { + index: source, + error, + })?; + set.push(block); + } + Ok(set) +} + +/// Read `indexes` in order into `tx`. Ends at the first error, after +/// sending it, or when the load drops its side. +pub async fn produce_blocks( + readers: Arc>, + fetch: Arc, + indexes: Vec, + tx: mpsc::Sender>, +) { + for index in indexes { + let read = read_index(&readers, &*fetch, index).await; + let failed = read.is_err(); + if tx.send(read).await.is_err() || failed { + return; + } + } +} + +#[cfg(test)] +mod tests { + use nodedb_raster::{RasterLimits, TiffOpener}; + use nodedb_raster_load::PixelWindow; + + use super::super::super::fetch::memory::MemoryFetch; + use super::super::open::open_sources; + use super::*; + + /// A 64x48 cloud-optimised GeoTIFF in 16x16 tiles with two overviews + /// and an internal mask. + const COG: &[u8] = include_bytes!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../nodedb-raster/tests/fixtures/cog-mask.tif" + )); + + const LIMITS: RasterLimits = RasterLimits::new(1 << 20); + + fn opener(object: u32) -> TiffOpener { + TiffOpener::new().with_object(nodedb_raster::ObjectId(object)) + } + + #[tokio::test] + async fn a_cog_opens_in_one_round() { + let fetch = MemoryFetch::new(vec![COG.to_vec(), COG.to_vec()]); + let readers = open_sources(&fetch, &[opener(0), opener(1)], LIMITS) + .await + .expect("opens"); + assert_eq!(readers.len(), 2); + let calls = fetch.calls(); + assert_eq!(calls.len(), 1, "one round"); + assert_eq!(calls[0].len(), 2, "one request per source"); + assert_eq!(readers[0].header().width, 64); + } + + #[tokio::test] + async fn a_window_fetches_only_its_tiles() { + let fetch = MemoryFetch::new(vec![COG.to_vec()]); + let readers = open_sources(&fetch, &[opener(0)], LIMITS) + .await + .expect("opens"); + let opened = fetch.calls().len(); + let window = PixelWindow { + col0: 16, + row0: 16, + col1: 32, + row1: 32, + }; + let reader = &readers[0]; + let wanted: Vec = (0..reader.block_count()) + .filter(|index| window.overlaps(&reader.block_window(*index).expect("window"))) + .collect(); + assert_eq!(wanted, vec![5], "one 16x16 tile of twelve"); + let expected = coalesce(reader.block_ranges(5).expect("ranges"), COALESCE_GAP); + let readers = Arc::new(readers); + let set = read_index(&readers, &fetch, 5).await.expect("reads"); + let calls = fetch.calls(); + assert_eq!(calls.len(), opened + 1); + assert_eq!(calls[opened], expected); + assert_eq!((set[0].window.col_off, set[0].window.row_off), (16, 16)); + } + + #[tokio::test] + async fn every_source_reads_the_same_block_in_lockstep() { + let fetch = MemoryFetch::new(vec![COG.to_vec(), COG.to_vec()]); + let readers = Arc::new( + open_sources(&fetch, &[opener(0), opener(1)], LIMITS) + .await + .expect("opens"), + ); + let set = read_index(&readers, &fetch, 3).await.expect("reads"); + assert_eq!(set.len(), 2); + assert_eq!(set[0].window, set[1].window); + assert_eq!(set[0].bands, set[1].bands); + assert_eq!(set[0].masks, set[1].masks); + let objects: Vec = fetch + .calls() + .last() + .expect("a block fetch") + .iter() + .map(|r| r.object.0) + .collect(); + assert!(objects.contains(&0) && objects.contains(&1), "{objects:?}"); + } + + #[test] + fn close_ranges_merge_and_slice_back() { + let r = |object, start, end| ByteRange::new(nodedb_raster::ObjectId(object), start, end); + let fetch = MemoryFetch::new(vec![(0u8..=255).collect()]); + let answers = futures::executor::block_on(fetch_ranges( + &fetch, + &[ + r(0, 10, 12), + r(0, 4, 4), + r(0, 2, 5), + r(0, 250, 300), + r(1, 0, 3), + ], + )) + .expect("reads"); + assert_eq!( + answers, + vec![ + Some(vec![10, 11]), + Some(vec![]), + Some(vec![2, 3, 4]), + Some(vec![250, 251, 252, 253, 254, 255]), + None + ] + ); + assert_eq!(fetch.calls().len(), 1); + assert_eq!(fetch.calls()[0].len(), 2, "object 0 merges into one range"); + } +} diff --git a/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/driver/drive.rs b/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/driver/drive.rs new file mode 100644 index 000000000..931a7c519 --- /dev/null +++ b/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/driver/drive.rs @@ -0,0 +1,180 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! `drive_raster`: the one async driver of a raster load, whatever the +//! transport. +//! +//! 1. Open every source in shared rounds ([`open_sources`]). +//! 2. Check that the sources share a grid and block layout, and run every +//! open-time check of the plan. A refused input commits no row. +//! 3. With `ADOPT_METADATA true`, write the files' attr metadata. +//! 4. A producer task reads the blocks the plan wants, one block index from +//! every source at a time, into a queue of two. The load turns each set +//! into rows, every source's bands in one row, and feeds the loader. +//! +//! Network reads run on the Control Plane's Tokio runtime and decoding on +//! its blocking pool. The Data Plane receives only the array writes. + +use std::sync::Arc; + +use nodedb_raster::{ObjectId, RasterLimits, RasterReader, TiffOpener}; +use nodedb_raster_load::{ + BlockRows, LayoutError, RasterPlan, SourceHeader, adopted_metadata, check_same_layout, +}; +use nodedb_sql::ddl_ast::statement::CopyRasterOptions; +use tokio::sync::mpsc; +use tokio::task::JoinHandle; + +use crate::control::server::shared::ddl::result::DdlError; + +use super::super::super::decode::RowError; +use super::super::super::dispatch::{ArrayLoadCtx, input_cap}; +use super::super::super::loader::{ArrayLoader, LoadInput}; +use super::super::adopt::adopt_attr_metadata; +use super::super::fetch::RangeFetch; +use super::super::refusal::{drive_refusal, raster_refusal}; +use super::array_view::{array_view, insert_row, meta_alter, view_georef}; +use super::blocks::{BLOCK_QUEUE, produce_blocks}; +use super::error::DriveError; +use super::load_options::load_options; +use super::open::open_sources; + +/// The most bytes one block decodes to, before the memory headroom cap. +const MAX_BLOCK_BYTES: usize = 64 * 1024 * 1024; + +/// Aborts the producer task when the load ends early. +struct AbortOnDrop(Option>); + +impl AbortOnDrop { + /// The task, which the guard no longer aborts. + fn take(&mut self) -> Option> { + self.0.take() + } +} + +impl Drop for AbortOnDrop { + fn drop(&mut self) { + if let Some(handle) = &self.0 { + handle.abort(); + } + } +} + +/// Load the sources `names` into `array`, reading object `i` as source `i` +/// through `fetch`. `opener` picks the image (full resolution or an +/// overview). Returns the number of rows loaded. +pub(crate) async fn drive_raster( + ctx: &ArrayLoadCtx<'_>, + array: &str, + names: &[String], + opener: TiffOpener, + fetch: Arc, + options: &CopyRasterOptions, +) -> Result { + let mut loader = ArrayLoader::open(ctx, array, LoadInput::Raster)?; + let georef = loader.view().georef.clone().ok_or_else(|| { + loader.refuse(RowError::new( + 0, + "55000", + format!( + "array '{array}' has no GEOREF, so no pixel has a cell; \ + add one with ALTER ARRAY {array} SET GEOREF (...)" + ), + )) + })?; + let cap = MAX_BLOCK_BYTES.min(input_cap(ctx.state)); + let limits = RasterLimits::new(cap); + let refuse = |loader: &ArrayLoader, e: DriveError| loader.refuse(drive_refusal(e, names, cap)); + + let openers: Vec = (0..names.len()) + .map(|i| opener.with_object(ObjectId(u32::try_from(i).unwrap_or(u32::MAX)))) + .collect(); + let readers = open_sources(&*fetch, &openers, limits) + .await + .map_err(|e| refuse(&loader, e))?; + let crs = crate::control::crs_context::crs_context_for(ctx.state); + let mut headers = Vec::with_capacity(readers.len()); + for (index, (reader, name)) in readers.iter().zip(names).enumerate() { + let srid = reader + .header() + .crs + .resolve(&crs) + .map_err(|error| refuse(&loader, DriveError::Raster { index, error }))?; + headers.push(SourceHeader::new( + name.clone(), + reader.header(), + srid.map(|s| s.code()), + )); + } + check_same_layout(&headers, &readers).map_err(|e| match e { + LayoutError::Mismatch(mismatch) => loader.refuse(mismatch.into()), + LayoutError::Reader(error) => loader.refuse(raster_refusal( + &error, + names.first().map_or("?", String::as_str), + cap, + )), + })?; + let view = array_view(loader.view()); + let plan = RasterPlan::new( + &view, + &view_georef(&georef), + &headers, + &load_options(options), + ) + .map_err(|e| loader.refuse(e.into()))?; + if options.adopt_metadata { + let changes: Vec<_> = adopted_metadata(&view, &plan, &headers) + .iter() + .map(meta_alter) + .collect(); + adopt_attr_metadata(ctx, array, &changes).await?; + } + + let indexes = wanted_blocks(&readers, &plan) + .map_err(|error| refuse(&loader, DriveError::Raster { index: 0, error }))?; + let readers = Arc::new(readers); + let (tx, mut rx) = mpsc::channel(BLOCK_QUEUE); + let mut producer = AbortOnDrop(Some(tokio::spawn(produce_blocks( + Arc::clone(&readers), + Arc::clone(&fetch), + indexes, + tx, + )))); + while let Some(next) = rx.recv().await { + let blocks = next.map_err(|e| refuse(&loader, e))?; + let rows = BlockRows::new(&plan, &blocks).map_err(|e| loader.refuse(e.into()))?; + loader + .feed_rows( + ctx, + rows.map(|row| { + row.map(|(row_no, row)| (row_no, insert_row(row))) + .map_err(RowError::from) + }), + ) + .await?; + } + // The channel closes once the producer returns. A panic closes it + // early, so the join result tells a whole input from a cut-short one. + if let Some(handle) = producer.take() { + handle + .await + .map_err(|e| DdlError::internal(format!("COPY: raster block reader failed: {e}")))?; + } + loader.finish(ctx).await +} + +/// The block indexes that hold a pixel the plan loads, in order. +fn wanted_blocks( + readers: &[R], + plan: &RasterPlan, +) -> Result, nodedb_raster::RasterError> { + let Some(first) = readers.first() else { + return Ok(Vec::new()); + }; + let mut indexes = Vec::new(); + for index in 0..first.block_count() { + if plan.wants_block(&first.block_window(index)?) { + indexes.push(index); + } + } + Ok(indexes) +} diff --git a/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/driver/error.rs b/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/driver/error.rs new file mode 100644 index 000000000..fc4af1941 --- /dev/null +++ b/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/driver/error.rs @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! `DriveError`: why reading the sources of a raster load failed. + +use nodedb_raster::RasterError; + +use super::super::fetch::FetchError; + +/// A failed open or block read of a raster load. +#[derive(Debug, thiserror::Error)] +pub enum DriveError { + /// Source `index` (0-based, statement order) is malformed or uses a + /// feature the reader lacks. + #[error("source {}: {error}", index + 1)] + Raster { index: usize, error: RasterError }, + #[error(transparent)] + Fetch(#[from] FetchError), + #[error("raster decode task failed: {0}")] + Task(String), +} diff --git a/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/driver/load_options.rs b/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/driver/load_options.rs new file mode 100644 index 000000000..77ff64cd4 --- /dev/null +++ b/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/driver/load_options.rs @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! The statement's GeoTIFF options in the form the load planner reads. + +use nodedb_raster_load::{BandTarget, LoadOptions, PixelWindow}; +use nodedb_sql::ddl_ast::statement::{CopyBand, CopyRasterOptions}; + +/// The planning options `options` carries. The transport options +/// (`CREDENTIAL`, `REQUESTER_PAYS`, `OVERVIEW`) and `ADOPT_METADATA` stay +/// with the server. +pub(crate) fn load_options(options: &CopyRasterOptions) -> LoadOptions { + LoadOptions { + bands: options.bands.as_ref().map(|bands| { + bands + .iter() + .map(|band| match band { + CopyBand::Attr(name) => BandTarget::Attr(name.clone()), + CopyBand::Skip => BandTarget::Skip, + }) + .collect() + }), + nodata: options.nodata, + time_ms: options.time_ms, + window: options.window.map(|w| PixelWindow { + col0: w.x0, + row0: w.y0, + col1: w.x1, + row1: w.y1, + }), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use nodedb_sql::ddl_ast::statement::CopyWindow; + + #[test] + fn planning_options_carry_over() { + let options = CopyRasterOptions { + bands: Some(vec![CopyBand::Attr("red".into()), CopyBand::Skip]), + nodata: Some(-9999.0), + time_ms: Some(1_700_000_000_000), + window: Some(CopyWindow { + x0: 1, + y0: 2, + x1: 3, + y1: 4, + }), + ..Default::default() + }; + assert_eq!( + load_options(&options), + LoadOptions { + bands: Some(vec![BandTarget::Attr("red".into()), BandTarget::Skip]), + nodata: Some(-9999.0), + time_ms: Some(1_700_000_000_000), + window: Some(PixelWindow { + col0: 1, + row0: 2, + col1: 3, + row1: 4, + }), + } + ); + assert_eq!( + load_options(&CopyRasterOptions::default()), + LoadOptions::default() + ); + } +} diff --git a/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/driver/mod.rs b/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/driver/mod.rs new file mode 100644 index 000000000..e8a3f25eb --- /dev/null +++ b/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/driver/mod.rs @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pub mod array_view; +pub mod blocks; +pub mod drive; +pub mod error; +pub mod load_options; +pub mod open; + +pub(crate) use drive::drive_raster; +pub use error::DriveError; diff --git a/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/driver/open.rs b/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/driver/open.rs new file mode 100644 index 000000000..91808dd8f --- /dev/null +++ b/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/driver/open.rs @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! Open every source of a load in shared rounds. +//! +//! Each round asks every source still opening for the ranges it needs, +//! merges the ranges that lie close together, and fetches them in one +//! batch. A cloud-optimised GeoTIFF names every IFD in its first 64 KiB, so +//! a load of COGs opens in one round: one request per source. + +use nodedb_raster::{ + COALESCE_GAP, Lookup, Opened, RangeCache, RasterError, RasterLimits, RasterOpener, TiffOpener, + TiffReader, coalesce, +}; + +use super::super::fetch::RangeFetch; +use super::error::DriveError; + +/// Rounds before an open gives up. +const MAX_OPEN_ROUNDS: usize = 64; + +/// Open the sources `openers` name, one per object, under `limits`. +pub async fn open_sources( + fetch: &F, + openers: &[TiffOpener], + limits: RasterLimits, +) -> Result, DriveError> { + let mut caches: Vec = openers + .iter() + .map(|_| RangeCache::new(limits.header_bytes)) + .collect(); + let mut readers: Vec> = vec![None; openers.len()]; + for _ in 0..MAX_OPEN_ROUNDS { + let mut need = Vec::new(); + let slots = openers.iter().zip(&caches).zip(readers.iter_mut()); + for (index, ((opener, cache), slot)) in slots.enumerate() { + if slot.is_some() { + continue; + } + let opened = opener + .open(cache, limits) + .map_err(|error| DriveError::Raster { index, error })?; + match opened { + Opened::Ready(reader) => *slot = Some(reader), + Opened::Need(ranges) => need.extend( + ranges + .into_iter() + .filter(|range| matches!(cache.get(*range), Lookup::Missing)), + ), + } + } + if readers.iter().all(Option::is_some) { + return Ok(readers.into_iter().flatten().collect()); + } + if need.is_empty() { + let index = readers.iter().position(Option::is_none).unwrap_or(0); + return Err(stalled(index)); + } + let merged = coalesce(need, COALESCE_GAP); + let answers = fetch.fetch(&merged).await?; + for (range, bytes) in merged.into_iter().zip(answers) { + let index = range.object.0 as usize; + let Some(cache) = caches.get_mut(index) else { + continue; + }; + cache + .insert(range, bytes) + .map_err(|error| DriveError::Raster { index, error })?; + } + } + let index = readers.iter().position(Option::is_none).unwrap_or(0); + Err(stalled(index)) +} + +fn stalled(index: usize) -> DriveError { + DriveError::Raster { + index, + error: RasterError::OpenStalled { + rounds: MAX_OPEN_ROUNDS, + }, + } +} diff --git a/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/fetch/error.rs b/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/fetch/error.rs new file mode 100644 index 000000000..ae2c2eef2 --- /dev/null +++ b/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/fetch/error.rs @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! `FetchError`: why a byte-range fetch of a raster source failed, with +//! the SQLSTATE each failure reports. No variant carries a response body. + +use crate::control::security::egress::EgressError; + +/// A failed range fetch. +#[derive(Debug, thiserror::Error)] +pub enum FetchError { + #[error("'{source_name}': cannot read: {error}")] + Io { + source_name: String, + error: std::io::Error, + }, + #[error("'{source_name}' does not exist")] + NotFound { source_name: String }, + #[error("'{source_name}': refused by the egress policy: {error}")] + Egress { + source_name: String, + error: EgressError, + }, + #[error( + "'{source_name}': the server refused the request with HTTP {status}; check the CREDENTIAL" + )] + Unauthorized { source_name: String, status: u16 }, + #[error("'{source_name}' changed during the load ({what}); rerun the COPY to read one version")] + Changed { source_name: String, what: String }, + #[error( + "'{source_name}': the server ignored the byte range (HTTP {status}); a remote raster \ + needs a server that answers Range requests with 206" + )] + RangeIgnored { source_name: String, status: u16 }, + #[error("'{source_name}': HTTP {status}")] + Status { source_name: String, status: u16 }, + #[error("'{source_name}': {what}")] + Protocol { source_name: String, what: String }, + #[error("'{source_name}': the connection failed: {what}")] + Network { source_name: String, what: String }, + #[error("'{source_name}': {what}")] + Store { source_name: String, what: String }, + #[error("raster fetch task failed: {0}")] + Task(String), +} + +impl FetchError { + /// The SQLSTATE the failure reports. + pub fn sqlstate(&self) -> &'static str { + match self { + Self::Io { .. } | Self::NotFound { .. } | Self::Status { .. } | Self::Store { .. } => { + "58030" + } + Self::Egress { .. } => "42501", + Self::Unauthorized { .. } => "28000", + Self::Changed { .. } => "55000", + Self::RangeIgnored { .. } => "0A000", + Self::Protocol { .. } => "22P04", + Self::Network { .. } => "08006", + Self::Task(_) => "XX000", + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn each_failure_maps_to_its_sqlstate() { + let name = || "https://h.example/a.tif".to_string(); + assert_eq!( + FetchError::Changed { + source_name: name(), + what: "ETag".into() + } + .sqlstate(), + "55000" + ); + assert_eq!( + FetchError::RangeIgnored { + source_name: name(), + status: 200 + } + .sqlstate(), + "0A000" + ); + assert_eq!( + FetchError::Egress { + source_name: name(), + error: EgressError::IpLiteral + } + .sqlstate(), + "42501" + ); + } +} diff --git a/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/fetch/http.rs b/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/fetch/http.rs new file mode 100644 index 000000000..6f7208c56 --- /dev/null +++ b/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/fetch/http.rs @@ -0,0 +1,363 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! `HttpRange`: byte-range reads of an `http(s)://` raster. +//! +//! Each range is one `GET` with a `Range` header. The answer must be `206` +//! with a `Content-Range` that starts where the range starts and names the +//! same total length every time. The first strong `ETag` is pinned and sent +//! as `If-Match` on every later request, so a file that changes mid-load +//! fails with `55000` instead of mixing two versions. A server that ignores +//! the range (`200`) is refused: the load reads ranges only. The client is +//! the egress client: every URL and redirect hop passes the egress policy, +//! and every resolved address is checked at connect time. +//! +//! Messages name the URL without its query, so a signature in the query +//! never reaches an error or a log line. + +use nodedb_raster::ByteRange; +use reqwest::StatusCode; +use reqwest::header::{CONTENT_RANGE, ETAG, IF_MATCH, RANGE}; +use zeroize::Zeroizing; + +use super::error::FetchError; +use super::pin::{ObjectVersion, PinnedObject}; +use super::traits::{RangeFetch, fetch_bounded}; +use crate::control::security::egress::{EgressFault, classify}; + +/// How a request authenticates. +pub enum HttpAuth { + None, + /// `Authorization: Bearer `. + Bearer(Zeroizing), + /// A shared access signature appended to the URL query. + Sas(Zeroizing), +} + +/// One remote object read with HTTP range requests. +pub struct HttpRange { + client: reqwest::Client, + url: reqwest::Url, + bearer: Option>, + name: String, + pinned: PinnedObject, +} + +impl HttpRange { + /// Read `url` with `client`, authenticating with `auth`. The caller + /// checked `url` against the egress policy. + pub fn new(client: reqwest::Client, url: reqwest::Url, auth: HttpAuth) -> Self { + let name = display_name(&url); + let (url, bearer) = match auth { + HttpAuth::None => (url, None), + HttpAuth::Bearer(token) => (url, Some(token)), + HttpAuth::Sas(token) => (with_sas(url, &token), None), + }; + Self { + client, + url, + bearer, + name, + pinned: PinnedObject::new(), + } + } + + /// The bytes of `range`, fewer where the object ends. + pub async fn fetch_one(&self, range: ByteRange) -> Result>, FetchError> { + if range.is_empty() { + return Ok(Some(Vec::new())); + } + let mut request = self + .client + .get(self.url.clone()) + .header(RANGE, format!("bytes={}-{}", range.start, range.end - 1)); + if let Some(etag) = self.pinned.etag() + && !etag.starts_with("W/") + { + request = request.header(IF_MATCH, etag); + } + if let Some(token) = &self.bearer { + request = request.bearer_auth(token.as_str()); + } + let response = request.send().await.map_err(|e| self.send_error(e))?; + let status = response.status(); + match status { + StatusCode::PARTIAL_CONTENT => self.read_partial(range, response).await.map(Some), + StatusCode::RANGE_NOT_SATISFIABLE => { + if let Some(total) = header_text(&response, CONTENT_RANGE) + .and_then(|v| v.strip_prefix("bytes */").and_then(|t| t.parse().ok())) + { + self.pinned.pin( + &self.name, + ObjectVersion { + size: total, + etag: self.pinned.etag().map(str::to_string), + }, + )?; + } + Ok(Some(Vec::new())) + } + StatusCode::PRECONDITION_FAILED => Err(self.changed("its ETag no longer matches")), + StatusCode::NOT_FOUND | StatusCode::GONE => Err(FetchError::NotFound { + source_name: self.name.clone(), + }), + StatusCode::UNAUTHORIZED | StatusCode::FORBIDDEN => Err(FetchError::Unauthorized { + source_name: self.name.clone(), + status: status.as_u16(), + }), + StatusCode::OK => Err(FetchError::RangeIgnored { + source_name: self.name.clone(), + status: status.as_u16(), + }), + other => Err(FetchError::Status { + source_name: self.name.clone(), + status: other.as_u16(), + }), + } + } + + /// The body of a `206` for `range`, checked against its + /// `Content-Range` and the pinned object. + async fn read_partial( + &self, + range: ByteRange, + mut response: reqwest::Response, + ) -> Result, FetchError> { + let content_range = header_text(&response, CONTENT_RANGE) + .ok_or_else(|| self.protocol("a 206 answer has no Content-Range"))?; + let (first, last, total) = parse_content_range(&content_range) + .ok_or_else(|| self.protocol("a 206 answer has a malformed Content-Range"))?; + if first != range.start || last >= range.end || last < first { + return Err(self.protocol(format!( + "asked for bytes {}-{}, the server sent {first}-{last}", + range.start, + range.end - 1 + ))); + } + let etag = header_text(&response, ETAG); + self.pinned + .pin(&self.name, ObjectVersion { size: total, etag })?; + let expected = usize::try_from(last - first + 1) + .map_err(|_| self.protocol("the answer exceeds the address space"))?; + let mut body = Vec::with_capacity(expected); + while let Some(chunk) = response.chunk().await.map_err(|e| self.send_error(e))? { + if body.len() + chunk.len() > expected { + return Err(self.protocol(format!( + "the server sent more than the {expected} bytes its Content-Range names" + ))); + } + body.extend_from_slice(&chunk); + } + if body.len() != expected { + return Err(self.protocol(format!( + "the server sent {} of the {expected} bytes its Content-Range names", + body.len() + ))); + } + Ok(body) + } + + fn changed(&self, what: &str) -> FetchError { + FetchError::Changed { + source_name: self.name.clone(), + what: what.to_string(), + } + } + + fn protocol(&self, what: impl Into) -> FetchError { + FetchError::Protocol { + source_name: self.name.clone(), + what: what.into(), + } + } + + /// The error of a failed send or body read, with the URL removed. + fn send_error(&self, error: reqwest::Error) -> FetchError { + let source_name = self.name.clone(); + match classify(&error) { + EgressFault::Refused(error) => FetchError::Egress { source_name, error }, + EgressFault::TooManyRedirects(max) => FetchError::Protocol { + source_name, + what: format!("more than {max} redirects"), + }, + EgressFault::Other => FetchError::Network { + source_name, + what: error.without_url().to_string(), + }, + } + } +} + +impl RangeFetch for HttpRange { + async fn fetch(&self, ranges: &[ByteRange]) -> Result>>, FetchError> { + fetch_bounded(ranges, |range| self.fetch_one(range)).await + } +} + +/// `bytes -/`. A `*` total is refused: the load pins +/// the total length. +fn parse_content_range(value: &str) -> Option<(u64, u64, u64)> { + let rest = value.trim().strip_prefix("bytes ")?; + let (span, total) = rest.split_once('/')?; + let (first, last) = span.split_once('-')?; + Some(( + first.trim().parse().ok()?, + last.trim().parse().ok()?, + total.trim().parse().ok()?, + )) +} + +fn header_text(response: &reqwest::Response, name: reqwest::header::HeaderName) -> Option { + response + .headers() + .get(name) + .and_then(|v| v.to_str().ok()) + .map(str::to_string) +} + +/// The URL without its query, fragment or user info. +pub(crate) fn display_name(url: &reqwest::Url) -> String { + let mut shown = url.clone(); + shown.set_query(None); + shown.set_fragment(None); + // Clearing user info fails only for a URL that cannot hold any. + let _cleared = shown.set_username("").and(shown.set_password(None)); + shown.to_string() +} + +/// `url` with the signature `token` appended to its query. +fn with_sas(mut url: reqwest::Url, token: &str) -> reqwest::Url { + let token = token.trim_start_matches('?'); + let query = match url.query() { + Some(existing) if !existing.is_empty() => format!("{existing}&{token}"), + Some(_) | None => token.to_string(), + }; + url.set_query(Some(&query)); + url +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + use std::sync::atomic::Ordering; + + use nodedb_raster::ObjectId; + + use super::super::test_server::{Mode, serve}; + use super::*; + use crate::control::security::egress::resolver::tests::table; + use crate::control::security::egress::{EgressClientOptions, EgressPolicy, egress_client}; + + const OBJECT: &[u8] = b"0123456789abcdefghij"; + + fn policy(hosts: &[&str], cidrs: &[&str]) -> Arc { + let hosts: Vec = hosts.iter().map(|h| (*h).to_string()).collect(); + let cidrs: Vec = cidrs.iter().map(|c| (*c).to_string()).collect(); + Arc::new(EgressPolicy::from_parts(true, &hosts, &cidrs).expect("valid policy")) + } + + fn client(policy: Arc, entries: &[(&str, &str)]) -> reqwest::Client { + egress_client( + policy, + table(entries), + EgressClientOptions { + timeout: Some(std::time::Duration::from_secs(10)), + connect_timeout: std::time::Duration::from_secs(5), + read_timeout: std::time::Duration::from_secs(5), + max_redirects: 3, + }, + ) + .expect("client") + } + + fn range(start: u64, end: u64) -> ByteRange { + ByteRange::new(ObjectId(0), start, end) + } + + fn local(host: &str, port: u16) -> HttpRange { + let url: reqwest::Url = format!("http://{host}:{port}/scene.tif?sig=secret") + .parse() + .expect("url"); + let client = client(policy(&[host], &["127.0.0.0/8"]), &[(host, "127.0.0.1")]); + HttpRange::new(client, url, HttpAuth::None) + } + + #[tokio::test] + async fn ranges_read_with_the_first_etag_pinned() { + let (addr, served) = serve(OBJECT.to_vec(), Mode::Ranges).await; + let http = local("raster.test", addr.port()); + let got = http + .fetch(&[range(0, 4), range(18, 40)]) + .await + .expect("reads"); + assert_eq!(got, vec![Some(b"0123".to_vec()), Some(b"ij".to_vec())]); + let got = http.fetch(&[range(25, 30)]).await.expect("past the end"); + assert_eq!(got, vec![Some(Vec::new())]); + let again = http.fetch(&[range(4, 6)]).await.expect("pinned read"); + assert_eq!(again, vec![Some(b"45".to_vec())]); + assert!(served.if_match_seen.load(Ordering::SeqCst) >= 1); + } + + #[tokio::test] + async fn an_object_that_changes_mid_load_fails_55000() { + let (addr, served) = serve(OBJECT.to_vec(), Mode::Ranges).await; + let http = local("raster.test", addr.port()); + http.fetch(&[range(0, 4)]) + .await + .expect("first read pins v1"); + served.replace(b"another version of it".to_vec(), "\"v2\""); + let err = http.fetch(&[range(4, 8)]).await.expect_err("v2 is refused"); + assert_eq!(err.sqlstate(), "55000", "{err}"); + assert!(!err.to_string().contains("secret"), "{err}"); + } + + #[tokio::test] + async fn a_server_that_ignores_ranges_is_refused() { + let (addr, _served) = serve(OBJECT.to_vec(), Mode::IgnoreRange).await; + let http = local("raster.test", addr.port()); + let err = http + .fetch(&[range(0, 4)]) + .await + .expect_err("200 is refused"); + assert_eq!(err.sqlstate(), "0A000", "{err}"); + } + + #[tokio::test] + async fn a_rebound_name_never_reaches_the_private_address() { + let (addr, served) = serve(OBJECT.to_vec(), Mode::Ranges).await; + // http is allowed for the host, but no CIDR admits loopback. + let client = client( + policy(&["rebind.test"], &[]), + &[("rebind.test", "127.0.0.1")], + ); + let url = format!("http://rebind.test:{}/scene.tif", addr.port()) + .parse() + .expect("url"); + let http = HttpRange::new(client, url, HttpAuth::None); + let err = http.fetch(&[range(0, 4)]).await.expect_err("refused"); + assert_eq!(err.sqlstate(), "42501", "{err}"); + assert_eq!(served.requests.load(Ordering::SeqCst), 0); + } + + #[tokio::test] + async fn a_redirect_to_an_ip_literal_is_refused() { + let (addr, served) = serve(OBJECT.to_vec(), Mode::Redirect).await; + *served.location.lock().unwrap_or_else(|p| p.into_inner()) = + format!("http://127.0.0.1:{}/scene.tif", addr.port()); + let http = local("raster.test", addr.port()); + let err = http.fetch(&[range(0, 4)]).await.expect_err("hop refused"); + assert_eq!(err.sqlstate(), "42501", "{err}"); + assert_eq!(served.requests.load(Ordering::SeqCst), 1); + } + + #[test] + fn a_sas_signature_joins_the_query_and_never_the_name() { + let url: reqwest::Url = "https://acct.blob.example/c/a.tif?x=1" + .parse() + .expect("url"); + assert_eq!(display_name(&url), "https://acct.blob.example/c/a.tif"); + let signed = with_sas(url, "?sv=1&sig=abc"); + assert_eq!(signed.query(), Some("x=1&sv=1&sig=abc")); + assert_eq!(parse_content_range("bytes 0-3/20"), Some((0, 3, 20))); + assert_eq!(parse_content_range("bytes 0-3/*"), None); + } +} diff --git a/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/fetch/local.rs b/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/fetch/local.rs new file mode 100644 index 000000000..c48cffd8c --- /dev/null +++ b/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/fetch/local.rs @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! `LocalFile`: positional reads of a server file through the handle every +//! open-time check ran on. The caller checks the superuser privilege and +//! the path before it opens the file. + +use std::os::unix::fs::FileExt as _; +use std::sync::Arc; + +use nodedb_raster::ByteRange; + +use super::error::FetchError; +use super::traits::{RangeFetch, fetch_bounded}; + +/// One server file, read with `pread` on Tokio's blocking pool. +pub struct LocalFile { + file: Arc, + name: String, +} + +impl LocalFile { + /// Read `file`, named `name` in messages. + pub fn new(file: std::fs::File, name: impl Into) -> Self { + Self { + file: Arc::new(file), + name: name.into(), + } + } + + /// The bytes of `range`, fewer at the end of the file. + pub async fn fetch_one(&self, range: ByteRange) -> Result>, FetchError> { + let len = usize::try_from(range.len()).map_err(|_| FetchError::Protocol { + source_name: self.name.clone(), + what: format!( + "byte range {}..{} exceeds the address space", + range.start, range.end + ), + })?; + let file = Arc::clone(&self.file); + let read = tokio::task::spawn_blocking(move || pread_range(&file, range.start, len)) + .await + .map_err(|e| FetchError::Task(e.to_string()))?; + read.map(Some).map_err(|error| FetchError::Io { + source_name: self.name.clone(), + error, + }) + } +} + +/// Read `len` bytes at `start`, fewer at the end of the file. +fn pread_range(file: &std::fs::File, start: u64, len: usize) -> std::io::Result> { + let mut buf = vec![0u8; len]; + let mut filled = 0usize; + while filled < len { + let at = start.saturating_add(filled as u64); + match file.read_at(&mut buf[filled..], at) { + Ok(0) => break, + Ok(n) => filled += n, + Err(e) if e.kind() == std::io::ErrorKind::Interrupted => {} + Err(e) => return Err(e), + } + } + buf.truncate(filled); + Ok(buf) +} + +impl RangeFetch for LocalFile { + async fn fetch(&self, ranges: &[ByteRange]) -> Result>>, FetchError> { + fetch_bounded(ranges, |range| self.fetch_one(range)).await + } +} + +#[cfg(test)] +mod tests { + use super::*; + use nodedb_raster::ObjectId; + + #[tokio::test] + async fn reads_clip_at_the_end_of_the_file() { + let dir = tempfile::tempdir().expect("tempdir"); + let path = dir.path().join("a.bin"); + std::fs::write(&path, [1u8, 2, 3, 4, 5]).expect("write"); + let local = LocalFile::new(std::fs::File::open(&path).expect("open"), "a.bin"); + let r = |start, end| ByteRange::new(ObjectId(0), start, end); + let got = local + .fetch(&[r(1, 3), r(3, 9), r(7, 9)]) + .await + .expect("reads"); + assert_eq!(got, vec![Some(vec![2, 3]), Some(vec![4, 5]), Some(vec![])]); + } +} diff --git a/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/fetch/memory.rs b/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/fetch/memory.rs new file mode 100644 index 000000000..0f8eb65ad --- /dev/null +++ b/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/fetch/memory.rs @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! An in-memory `RangeFetch` that counts what a driver asks for. + +use std::sync::Mutex; + +use nodedb_raster::ByteRange; + +use super::error::FetchError; +use super::traits::RangeFetch; + +/// Objects held in memory. Every `fetch` call and every range it asked for +/// is recorded. +pub struct MemoryFetch { + objects: Vec>, + calls: Mutex>>, +} + +impl MemoryFetch { + /// `objects[i]` is object `i`. + pub fn new(objects: Vec>) -> Self { + Self { + objects, + calls: Mutex::new(Vec::new()), + } + } + + /// The ranges of each `fetch` call so far, in call order. + pub fn calls(&self) -> Vec> { + self.calls + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()) + .clone() + } +} + +impl RangeFetch for MemoryFetch { + async fn fetch(&self, ranges: &[ByteRange]) -> Result>>, FetchError> { + self.calls + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()) + .push(ranges.to_vec()); + Ok(ranges + .iter() + .map(|range| { + self.objects.get(range.object.0 as usize).map(|bytes| { + let len = bytes.len() as u64; + let (start, end) = (range.start.min(len), range.end.min(len)); + bytes + .get(start as usize..end.max(start) as usize) + .unwrap_or_default() + .to_vec() + }) + }) + .collect()) + } +} diff --git a/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/fetch/mod.rs b/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/fetch/mod.rs new file mode 100644 index 000000000..15ce5cd11 --- /dev/null +++ b/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/fetch/mod.rs @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pub mod error; +pub mod http; +pub mod local; +#[cfg(test)] +pub mod memory; +pub mod pin; +pub mod sources; +pub mod store; +#[cfg(test)] +pub mod test_server; +pub mod traits; + +pub use error::FetchError; +pub use http::{HttpAuth, HttpRange}; +pub use local::LocalFile; +pub use pin::{ObjectVersion, PinnedObject}; +pub use sources::{SourceFetch, SourceSet}; +pub use store::{ObjectStoreRange, S3Keys, S3Target, bucket_region}; +pub use traits::{MAX_IN_FLIGHT, RangeFetch, fetch_bounded}; diff --git a/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/fetch/pin.rs b/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/fetch/pin.rs new file mode 100644 index 000000000..95ae6e85a --- /dev/null +++ b/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/fetch/pin.rs @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! `PinnedObject`: the version of one remote object a load reads. The first +//! answer pins the object's size and ETag. Every later answer must match, +//! so an object that changes mid-load fails with `55000` instead of mixing +//! two versions. + +use std::sync::OnceLock; + +use super::error::FetchError; + +/// The size and ETag one answer reports for an object. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ObjectVersion { + pub size: u64, + pub etag: Option, +} + +/// The version of one object, pinned by the first answer. +#[derive(Debug, Default)] +pub struct PinnedObject { + pinned: OnceLock, +} + +impl PinnedObject { + /// Nothing pinned yet. + pub fn new() -> Self { + Self::default() + } + + /// The pinned version, once an answer pinned it. + pub fn get(&self) -> Option<&ObjectVersion> { + self.pinned.get() + } + + /// The pinned size, once an answer pinned it. + pub fn size(&self) -> Option { + self.get().map(|v| v.size) + } + + /// The pinned ETag, once an answer that carries one pinned it. + pub fn etag(&self) -> Option<&str> { + self.get().and_then(|v| v.etag.as_deref()) + } + + /// Pin `seen` when nothing is pinned, or check it against the pinned + /// version. An answer without an ETag matches any ETag. + pub fn pin(&self, source_name: &str, seen: ObjectVersion) -> Result<(), FetchError> { + let pinned = self.pinned.get_or_init(|| seen.clone()); + let what = if pinned.size != seen.size { + format!("its size went from {} to {} bytes", pinned.size, seen.size) + } else { + match (&pinned.etag, &seen.etag) { + (Some(a), Some(b)) if a != b => "its ETag changed".to_string(), + _ => return Ok(()), + } + }; + Err(FetchError::Changed { + source_name: source_name.to_string(), + what, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn version(size: u64, etag: Option<&str>) -> ObjectVersion { + ObjectVersion { + size, + etag: etag.map(str::to_string), + } + } + + #[test] + fn the_first_answer_pins_and_later_answers_must_match() { + let pin = PinnedObject::new(); + assert_eq!(pin.size(), None); + pin.pin("s", version(20, Some("\"v1\""))) + .expect("first pins"); + assert_eq!(pin.size(), Some(20)); + assert_eq!(pin.etag(), Some("\"v1\"")); + pin.pin("s", version(20, Some("\"v1\""))) + .expect("same version"); + pin.pin("s", version(20, None)) + .expect("no ETag matches any"); + let size = pin.pin("s", version(21, Some("\"v1\""))).expect_err("size"); + assert_eq!(size.sqlstate(), "55000"); + assert!(size.to_string().contains("20 to 21"), "{size}"); + let etag = pin.pin("s", version(20, Some("\"v2\""))).expect_err("ETag"); + assert!(etag.to_string().contains("ETag changed"), "{etag}"); + } +} diff --git a/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/fetch/sources.rs b/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/fetch/sources.rs new file mode 100644 index 000000000..354b8c809 --- /dev/null +++ b/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/fetch/sources.rs @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! `SourceSet`: the sources of one load, one transport per object. Source +//! `i` of the statement is object `i` of every range the readers name. + +use nodedb_raster::ByteRange; + +use super::error::FetchError; +use super::http::HttpRange; +use super::local::LocalFile; +use super::store::ObjectStoreRange; +use super::traits::{RangeFetch, fetch_bounded}; + +/// The transport of one source. +pub enum SourceFetch { + Local(LocalFile), + Http(HttpRange), + Store(ObjectStoreRange), +} + +impl SourceFetch { + async fn fetch_one(&self, range: ByteRange) -> Result>, FetchError> { + match self { + Self::Local(local) => local.fetch_one(range).await, + Self::Http(http) => http.fetch_one(range).await, + Self::Store(store) => store.fetch_one(range).await, + } + } +} + +/// Every source of one load. +pub struct SourceSet { + sources: Vec, +} + +impl SourceSet { + /// `sources`, in statement order. + pub fn new(sources: Vec) -> Self { + Self { sources } + } + + async fn route(&self, range: ByteRange) -> Result>, FetchError> { + // A range of an object no source holds names a missing object. + match self.sources.get(range.object.0 as usize) { + Some(source) => source.fetch_one(range).await, + None => Ok(None), + } + } +} + +impl RangeFetch for SourceSet { + async fn fetch(&self, ranges: &[ByteRange]) -> Result>>, FetchError> { + fetch_bounded(ranges, |range| self.route(range)).await + } +} diff --git a/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/fetch/store.rs b/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/fetch/store.rs new file mode 100644 index 000000000..7301b6bcb --- /dev/null +++ b/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/fetch/store.rs @@ -0,0 +1,315 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! `ObjectStoreRange`: byte-range reads of an `s3://bucket/key` raster. +//! +//! Each range is one ranged `GET`. The first answer pins the object's size +//! and ETag, and every later request sends the ETag as `If-Match`, so an +//! object that changes mid-load fails with `55000`. Requests go through the +//! egress client, so every host passes the egress policy at connect time. +//! The endpoint of an S3-compatible store passes the egress policy again at +//! COPY time, so a policy narrowed after `CREATE CREDENTIAL` holds. A +//! requester-pays bucket bills the credential's account when the COPY sets +//! `REQUESTER_PAYS true`. + +use std::sync::Arc; + +use nodedb_raster::ByteRange; +use object_store::aws::{AmazonS3, AmazonS3Builder}; +use object_store::client::{HttpClient, HttpConnector}; +use object_store::path::Path; +use object_store::{ClientOptions, GetOptions, GetRange, ObjectStore as _}; +use zeroize::Zeroizing; + +use super::error::FetchError; +use super::pin::{ObjectVersion, PinnedObject}; +use super::traits::{RangeFetch, fetch_bounded}; +use crate::control::security::egress::{EgressError, EgressPolicy}; + +/// AWS keys of a signed S3 read. +pub struct S3Keys { + pub access_key_id: Zeroizing, + pub secret_access_key: Zeroizing, + pub session_token: Option>, +} + +/// Where and how an S3 source is read. +pub struct S3Target { + pub bucket: String, + pub key: String, + pub region: String, + /// An S3-compatible endpoint in place of AWS. + pub endpoint: Option, + /// `None` reads a public bucket unsigned. + pub keys: Option, + pub requester_pays: bool, +} + +/// The connector that hands object_store the egress client. +#[derive(Debug)] +struct EgressConnector(reqwest::Client); + +impl HttpConnector for EgressConnector { + fn connect(&self, _options: &ClientOptions) -> object_store::Result { + Ok(HttpClient::new(self.0.clone())) + } +} + +/// One S3 object read with ranged `GET`s. +pub struct ObjectStoreRange { + store: Arc, + path: Path, + name: String, + pinned: PinnedObject, +} + +impl ObjectStoreRange { + /// Read `target` through `client`, the egress client built on `policy`. + pub fn new( + client: reqwest::Client, + target: S3Target, + policy: &EgressPolicy, + ) -> Result { + let name = format!("s3://{}/{}", target.bucket, target.key); + let store_error = |what: String| FetchError::Store { + source_name: name.clone(), + what, + }; + let path = Path::parse(&target.key) + .map_err(|e| store_error(format!("the key is not a valid object path: {e}")))?; + let mut builder = AmazonS3Builder::new() + .with_bucket_name(&target.bucket) + .with_region(&target.region) + .with_http_connector(EgressConnector(client)) + .with_request_payer(target.requester_pays); + if let Some(endpoint) = &target.endpoint { + let egress = |error: EgressError| FetchError::Egress { + source_name: name.clone(), + error, + }; + let url = reqwest::Url::parse(endpoint).map_err(|_| egress(EgressError::Malformed))?; + policy.check_parsed(&url).map_err(egress)?; + // The policy admits http only for an allow-listed host. + builder = builder + .with_endpoint(endpoint) + .with_allow_http(url.scheme() == "http"); + } + builder = match &target.keys { + Some(keys) => { + let signed = builder + .with_access_key_id(keys.access_key_id.as_str()) + .with_secret_access_key(keys.secret_access_key.as_str()); + match &keys.session_token { + Some(token) => signed.with_token(token.as_str()), + None => signed, + } + } + None => builder.with_skip_signature(true), + }; + let store = builder + .build() + .map_err(|e| store_error(format!("cannot configure the S3 client: {e}")))?; + Ok(Self { + store: Arc::new(store), + path, + name, + pinned: PinnedObject::new(), + }) + } + + /// The bytes of `range`, fewer where the object ends. + pub async fn fetch_one(&self, range: ByteRange) -> Result>, FetchError> { + if range.is_empty() { + return Ok(Some(Vec::new())); + } + if let Some(size) = self.pinned.size() + && range.start >= size + { + return Ok(Some(Vec::new())); + } + let options = GetOptions { + if_match: self.pinned.etag().map(str::to_string), + range: Some(GetRange::Bounded(range.start..range.end)), + ..GetOptions::default() + }; + let result = self + .store + .get_opts(&self.path, options) + .await + .map_err(|e| self.get_error(e))?; + self.pinned.pin( + &self.name, + ObjectVersion { + size: result.meta.size, + etag: result.meta.e_tag.clone(), + }, + )?; + let bytes = result.bytes().await.map_err(|e| self.get_error(e))?; + if bytes.len() as u64 > range.len() { + return Err(FetchError::Protocol { + source_name: self.name.clone(), + what: format!( + "asked for {} bytes, the store sent {}", + range.len(), + bytes.len() + ), + }); + } + Ok(Some(bytes.to_vec())) + } + + fn get_error(&self, error: object_store::Error) -> FetchError { + let source_name = self.name.clone(); + match error { + object_store::Error::NotFound { .. } if self.pinned.get().is_some() => { + FetchError::Changed { + source_name, + what: "it was removed".into(), + } + } + object_store::Error::NotFound { .. } => FetchError::NotFound { source_name }, + object_store::Error::Precondition { .. } => FetchError::Changed { + source_name, + what: "its ETag no longer matches".into(), + }, + object_store::Error::PermissionDenied { .. } => FetchError::Unauthorized { + source_name, + status: 403, + }, + object_store::Error::Unauthenticated { .. } => FetchError::Unauthorized { + source_name, + status: 401, + }, + // The enum is non-exhaustive: every other failure is a store error. + other => FetchError::Store { + source_name, + what: other.to_string(), + }, + } + } +} + +impl RangeFetch for ObjectStoreRange { + async fn fetch(&self, ranges: &[ByteRange]) -> Result>>, FetchError> { + fetch_bounded(ranges, |range| self.fetch_one(range)).await + } +} + +/// The region of public `bucket`: AWS names it in the +/// `x-amz-bucket-region` header of a `HEAD` on the bucket. +pub async fn bucket_region(client: &reqwest::Client, bucket: &str) -> Result { + let name = format!("s3://{bucket}"); + let url = format!("https://{bucket}.s3.amazonaws.com/"); + let response = client + .head(&url) + .send() + .await + .map_err(|e| FetchError::Network { + source_name: name.clone(), + what: e.without_url().to_string(), + })?; + response + .headers() + .get("x-amz-bucket-region") + .and_then(|v| v.to_str().ok()) + .map(str::to_string) + .ok_or_else(|| FetchError::Store { + source_name: name, + what: format!( + "the bucket region is unknown (HTTP {}); name it with a CREDENTIAL of TYPE 's3' \ + and its REGION", + response.status().as_u16() + ), + }) +} + +#[cfg(test)] +mod tests { + use std::sync::atomic::Ordering; + + use nodedb_raster::ObjectId; + + use super::super::test_server::{Mode, serve}; + use super::*; + use crate::control::security::egress::resolver::tests::table; + use crate::control::security::egress::{EgressClientOptions, egress_client}; + + const OBJECT: &[u8] = b"0123456789abcdefghij"; + + fn http_policy() -> Arc { + Arc::new( + EgressPolicy::from_parts(true, &["s3.test".into()], &["127.0.0.0/8".into()]) + .expect("valid policy"), + ) + } + + fn client(policy: Arc) -> reqwest::Client { + egress_client( + policy, + table(&[("s3.test", "127.0.0.1")]), + EgressClientOptions { + timeout: Some(std::time::Duration::from_secs(10)), + connect_timeout: std::time::Duration::from_secs(5), + read_timeout: std::time::Duration::from_secs(5), + max_redirects: 0, + }, + ) + .expect("client") + } + + fn target(endpoint: String) -> S3Target { + S3Target { + bucket: "bucket".into(), + key: "scene.tif".into(), + region: "us-east-1".into(), + endpoint: Some(endpoint), + keys: None, + requester_pays: false, + } + } + + fn range(start: u64, end: u64) -> ByteRange { + ByteRange::new(ObjectId(0), start, end) + } + + #[tokio::test] + async fn an_allow_listed_http_endpoint_reads_and_pins_the_object() { + let (addr, served) = serve(OBJECT.to_vec(), Mode::Ranges).await; + let policy = http_policy(); + let store = ObjectStoreRange::new( + client(Arc::clone(&policy)), + target(format!("http://s3.test:{}", addr.port())), + &policy, + ) + .expect("an allow-listed http endpoint"); + let got = store.fetch(&[range(0, 4)]).await.expect("reads"); + assert_eq!(got, vec![Some(b"0123".to_vec())]); + let past = store.fetch(&[range(25, 30)]).await.expect("past the end"); + assert_eq!(past, vec![Some(Vec::new())]); + served.replace(b"another version of it".to_vec(), "\"v2\""); + let err = store + .fetch(&[range(4, 8)]) + .await + .expect_err("v2 is refused"); + assert_eq!(err.sqlstate(), "55000", "{err}"); + assert!(served.if_match_seen.load(Ordering::SeqCst) >= 1); + } + + #[tokio::test] + async fn an_endpoint_the_policy_refuses_at_copy_time_sends_nothing() { + let (addr, served) = serve(OBJECT.to_vec(), Mode::Ranges).await; + // The credential's endpoint passed a policy that allowed http; the + // policy at COPY time does not. + let strict = Arc::new(EgressPolicy::strict()); + for endpoint in [ + format!("http://s3.test:{}", addr.port()), + format!("https://127.0.0.1:{}", addr.port()), + "not a url".to_string(), + ] { + let err = ObjectStoreRange::new(client(Arc::clone(&strict)), target(endpoint), &strict) + .err() + .expect("refused"); + assert_eq!(err.sqlstate(), "42501", "{err}"); + } + assert_eq!(served.requests.load(Ordering::SeqCst), 0); + } +} diff --git a/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/fetch/test_server.rs b/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/fetch/test_server.rs new file mode 100644 index 000000000..375e1465d --- /dev/null +++ b/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/fetch/test_server.rs @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! A local HTTP server for range-fetch tests: it serves one object with +//! byte ranges and a strong ETag, or ignores ranges, or redirects. + +use std::net::SocketAddr; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::{Arc, Mutex}; + +use axum::Router; +use axum::extract::State; +use axum::http::{HeaderMap, StatusCode, header}; +use axum::response::{IntoResponse, Response}; +use axum::routing::get; + +/// How the server answers. +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum Mode { + /// `206` with `Content-Range` and `ETag`, honouring `If-Match`. + Ranges, + /// `200` with the whole object, whatever the `Range`. + IgnoreRange, + /// `302` to `Location`. + Redirect, +} + +/// The served object and what the server saw. +pub struct Served { + pub body: Mutex>, + pub etag: Mutex, + pub requests: AtomicUsize, + pub if_match_seen: AtomicUsize, + pub mode: Mode, + pub location: Mutex, +} + +impl Served { + /// Replace the object and its ETag, as an upload mid-load does. + pub fn replace(&self, body: Vec, etag: &str) { + *self.body.lock().unwrap_or_else(|p| p.into_inner()) = body; + *self.etag.lock().unwrap_or_else(|p| p.into_inner()) = etag.to_string(); + } +} + +/// Serve `body` at `/scene.tif`, and at `/bucket/scene.tif` for a path-style +/// S3 read, on a free loopback port. +pub async fn serve(body: Vec, mode: Mode) -> (SocketAddr, Arc) { + let served = Arc::new(Served { + body: Mutex::new(body), + etag: Mutex::new("\"v1\"".to_string()), + requests: AtomicUsize::new(0), + if_match_seen: AtomicUsize::new(0), + mode, + location: Mutex::new(String::new()), + }); + let app = Router::new() + .route("/scene.tif", get(answer)) + .route("/bucket/scene.tif", get(answer)) + .with_state(Arc::clone(&served)); + let listener = tokio::net::TcpListener::bind("127.0.0.1:0") + .await + .expect("bind a loopback port"); + let addr = listener.local_addr().expect("local addr"); + tokio::spawn(async move { + axum::serve(listener, app).await.expect("serve"); + }); + (addr, served) +} + +async fn answer(State(served): State>, headers: HeaderMap) -> Response { + served.requests.fetch_add(1, Ordering::SeqCst); + let body = served + .body + .lock() + .unwrap_or_else(|p| p.into_inner()) + .clone(); + let etag = served + .etag + .lock() + .unwrap_or_else(|p| p.into_inner()) + .clone(); + match served.mode { + Mode::IgnoreRange => return (StatusCode::OK, body).into_response(), + Mode::Redirect => { + let location = served + .location + .lock() + .unwrap_or_else(|p| p.into_inner()) + .clone(); + return (StatusCode::FOUND, [(header::LOCATION, location)]).into_response(); + } + Mode::Ranges => {} + } + if let Some(wanted) = headers.get(header::IF_MATCH).and_then(|v| v.to_str().ok()) { + served.if_match_seen.fetch_add(1, Ordering::SeqCst); + if wanted != etag { + return StatusCode::PRECONDITION_FAILED.into_response(); + } + } + let total = body.len() as u64; + let Some((first, last)) = headers + .get(header::RANGE) + .and_then(|v| v.to_str().ok()) + .and_then(|v| v.strip_prefix("bytes=")) + .and_then(|v| v.split_once('-')) + .and_then(|(a, b)| Some((a.parse::().ok()?, b.parse::().ok()?))) + else { + return (StatusCode::OK, body).into_response(); + }; + if first >= total { + return ( + StatusCode::RANGE_NOT_SATISFIABLE, + [(header::CONTENT_RANGE, format!("bytes */{total}"))], + ) + .into_response(); + } + let last = last.min(total - 1); + let slice = body[first as usize..=last as usize].to_vec(); + ( + StatusCode::PARTIAL_CONTENT, + [ + ( + header::CONTENT_RANGE, + format!("bytes {first}-{last}/{total}"), + ), + (header::ETAG, etag), + ], + slice, + ) + .into_response() +} diff --git a/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/fetch/traits.rs b/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/fetch/traits.rs new file mode 100644 index 000000000..ad6740a0e --- /dev/null +++ b/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/fetch/traits.rs @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! `RangeFetch`: one transport's byte-range reads, and the bound on how +//! many run at once. + +use std::future::Future; + +use futures::{StreamExt as _, TryStreamExt as _}; +use nodedb_raster::ByteRange; + +use super::error::FetchError; + +/// Most range fetches one load runs at once. +pub const MAX_IN_FLIGHT: usize = 8; + +/// Reads byte ranges of raster source objects. Each answer is the range's +/// bytes, fewer where the object ends, or `None` when the object does not +/// exist. The raster readers bound every range they name. +pub trait RangeFetch: Send + Sync { + /// Fetch `ranges`, answering in order. + fn fetch( + &self, + ranges: &[ByteRange], + ) -> impl Future>>, FetchError>> + Send; +} + +/// Run `fetch_one` over `ranges` with at most [`MAX_IN_FLIGHT`] in flight, +/// answering in order. The first error ends the run. +pub async fn fetch_bounded( + ranges: &[ByteRange], + fetch_one: F, +) -> Result>>, FetchError> +where + F: Fn(ByteRange) -> Fut, + Fut: Future>, FetchError>>, +{ + futures::stream::iter(ranges.iter().copied().map(fetch_one)) + .buffered(MAX_IN_FLIGHT) + .try_collect() + .await +} diff --git a/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/grid.rs b/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/grid.rs deleted file mode 100644 index f650a8799..000000000 --- a/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/grid.rs +++ /dev/null @@ -1,375 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 - -//! Where each GeoTIFF pixel lands in an array: the cell coordinates of its -//! GEOREF axes, checked against the dim domains once at open. -//! -//! A pixel-mode array stores pixel indexes. The file grid must be the -//! array grid shifted by whole pixels, so file pixel `(col, row)` is cell -//! `(col + col0, row + row0)`. A world-mode array stores pixel-centre -//! world coordinates, so the cell is the file geotransform at -//! `(col + 0.5, row + 0.5)`. -//! -//! The pixel-to-cell map is affine and each coordinate is monotone in the -//! pixel index. So the four corner pixels bound every cell, and a footprint -//! inside the dim domains puts every cell inside them. - -use nodedb_array::schema::GeoTransform; -use nodedb_sql::catalog::ArrayCatalogView; -use nodedb_sql::types_array::{ArrayCoordLiteral, ArrayDimAst, ArrayDimType, ArrayDomainBound}; - -use super::super::decode::{RowError, bound_text}; - -/// Largest relative difference between the file and array linear terms. -const LINEAR_TOLERANCE: f64 = 1e-9; -/// Largest distance from a whole number, in pixels or INT64 units. -const INTEGRAL_TOLERANCE: f64 = 1e-6; -/// Largest pixel offset kept as `i64`, so `offset + u32` never overflows. -const MAX_OFFSET: f64 = (1u64 << 62) as f64; - -/// The cell coordinates of one pixel on the array's GEOREF axes. -#[derive(Debug, Clone, Copy)] -pub(super) enum CellGrid { - /// File pixel `(col, row)` is cell `(col + col0, row + row0)`. - Pixel { col0: i64, row0: i64 }, - /// The cell is the pixel centre in world coordinates. An `INT64` axis - /// holds the rounded centre, which open checks to be whole. - World { - gt: GeoTransform, - x_int: bool, - y_int: bool, - }, -} - -impl CellGrid { - /// The `(x, y)` cell coordinates of file pixel `(col, row)`. - pub(super) fn cell(&self, col: u32, row: u32) -> (ArrayCoordLiteral, ArrayCoordLiteral) { - match *self { - CellGrid::Pixel { col0, row0 } => ( - ArrayCoordLiteral::Int64(i64::from(col) + col0), - ArrayCoordLiteral::Int64(i64::from(row) + row0), - ), - CellGrid::World { gt, x_int, y_int } => { - let (x, y) = gt.pixel_to_world(f64::from(col) + 0.5, f64::from(row) + 0.5); - (world_coord(x, x_int), world_coord(y, y_int)) - } - } - } -} - -fn world_coord(value: f64, int: bool) -> ArrayCoordLiteral { - if int { - // Open proved every centre on this axis whole and inside the domain. - ArrayCoordLiteral::Int64(value.round() as i64) - } else { - ArrayCoordLiteral::Float64(value) - } -} - -/// The file facts the grid checks read. -pub(super) struct GridInput<'a> { - pub view: &'a ArrayCatalogView, - pub x_dim: &'a ArrayDimAst, - pub y_dim: &'a ArrayDimAst, - /// The array GEOREF transform. `None` is a world-mode array. - pub array_gt: Option<[f64; 6]>, - pub file_gt: [f64; 6], - pub width: u32, - pub height: u32, -} - -/// Check the file grid against the array and its footprint against the -/// dim domains. Misalignment is `22023`, a footprint outside a domain -/// `22003`. -pub(super) fn plan_grid(input: &GridInput<'_>) -> Result { - match input.array_gt { - Some(array_gt) => pixel_grid(input, array_gt), - None => world_grid(input), - } -} - -fn pixel_grid(input: &GridInput<'_>, array_gt: [f64; 6]) -> Result { - let file = input.file_gt; - let scale = [1, 2, 4, 5] - .iter() - .map(|&k| array_gt[k].abs()) - .fold(0.0_f64, f64::max); - let linear_equal = [1, 2, 4, 5] - .iter() - .all(|&k| (file[k] - array_gt[k]).abs() <= LINEAR_TOLERANCE * scale); - if !linear_equal { - return Err(misaligned( - input, - format!( - "the file pixel size and rotation {:?} differ from the array TRANSFORM {:?}", - [file[1], file[2], file[4], file[5]], - [array_gt[1], array_gt[2], array_gt[4], array_gt[5]] - ), - )); - } - let origin = GeoTransform(array_gt).world_to_pixel(file[0], file[3]); - let Some((u0, v0)) = origin else { - return Err(misaligned( - input, - "the array TRANSFORM has no inverse".into(), - )); - }; - let (Some(col0), Some(row0)) = (whole_offset(u0), whole_offset(v0)) else { - return Err(misaligned( - input, - format!("the file origin falls at array pixel ({u0}, {v0}), not on a whole pixel"), - )); - }; - let last_col = col0 + i64::from(input.width) - 1; - let last_row = row0 + i64::from(input.height) - 1; - check_int_span(input.x_dim, col0, last_col)?; - check_int_span(input.y_dim, row0, last_row)?; - Ok(CellGrid::Pixel { col0, row0 }) -} - -/// `value` rounded to `i64` when it is within the tolerance of a whole -/// number and inside the offset range. -fn whole_offset(value: f64) -> Option { - let rounded = value.round(); - let whole = (value - rounded).abs() <= INTEGRAL_TOLERANCE; - (whole && rounded.abs() <= MAX_OFFSET).then_some(rounded as i64) -} - -fn world_grid(input: &GridInput<'_>) -> Result { - let gt = GeoTransform(input.file_gt); - let (w, h) = (f64::from(input.width), f64::from(input.height)); - let corners = [ - (0.5, 0.5), - (w - 0.5, 0.5), - (0.5, h - 0.5), - (w - 0.5, h - 0.5), - ] - .map(|(u, v)| gt.pixel_to_world(u, v)); - let xs = corners.map(|(x, _)| x); - let ys = corners.map(|(_, y)| y); - let x_int = int_axis(input.x_dim)?; - let y_int = int_axis(input.y_dim)?; - let file = input.file_gt; - if x_int { - check_whole_axis(input.x_dim, &xs, [file[1], file[2]])?; - } - if y_int { - check_whole_axis(input.y_dim, &ys, [file[4], file[5]])?; - } - check_world_span(input.x_dim, &xs)?; - check_world_span(input.y_dim, &ys)?; - Ok(CellGrid::World { gt, x_int, y_int }) -} - -/// Whether a world-mode axis is `INT64`. GEOREF admits `INT64` and -/// `FLOAT64` axes only. -fn int_axis(dim: &ArrayDimAst) -> Result { - match dim.dtype { - ArrayDimType::Int64 => Ok(true), - ArrayDimType::Float64 => Ok(false), - ArrayDimType::TimestampMs | ArrayDimType::String => Err(RowError::new( - 0, - "22023", - format!("GEOREF axis '{}' cannot hold world coordinates", dim.name), - )), - } -} - -/// Every pixel centre on an `INT64` world axis must be whole: the corner -/// centres and the per-pixel steps. -fn check_whole_axis( - dim: &ArrayDimAst, - corners: &[f64; 4], - steps: [f64; 2], -) -> Result<(), RowError> { - let whole = |v: &f64| (v - v.round()).abs() <= INTEGRAL_TOLERANCE; - if corners.iter().all(whole) && steps.iter().all(whole) { - return Ok(()); - } - Err(RowError::new( - 0, - "22023", - format!( - "pixel centres on INT64 axis '{}' are not whole numbers; \ - declare it FLOAT64 or resample outside the engine", - dim.name - ), - )) -} - -fn check_world_span(dim: &ArrayDimAst, corners: &[f64; 4]) -> Result<(), RowError> { - let lo = corners.iter().copied().fold(f64::INFINITY, f64::min); - let hi = corners.iter().copied().fold(f64::NEG_INFINITY, f64::max); - match dim.dtype { - ArrayDimType::Int64 => check_int_span(dim, lo.round() as i64, hi.round() as i64), - ArrayDimType::Float64 | ArrayDimType::TimestampMs | ArrayDimType::String => { - let inside = match (&dim.lo, &dim.hi) { - (ArrayDomainBound::Float64(d_lo), ArrayDomainBound::Float64(d_hi)) => { - *d_lo <= lo && hi <= *d_hi - } - _ => false, - }; - if inside { - Ok(()) - } else { - Err(outside(dim, &lo.to_string(), &hi.to_string())) - } - } - } -} - -/// Check the integer span `[lo, hi]` of an axis against its domain. -fn check_int_span(dim: &ArrayDimAst, lo: i64, hi: i64) -> Result<(), RowError> { - let inside = match (&dim.lo, &dim.hi) { - (ArrayDomainBound::Int64(d_lo), ArrayDomainBound::Int64(d_hi)) => { - *d_lo <= lo && hi <= *d_hi - } - _ => false, - }; - if inside { - return Ok(()); - } - Err(outside(dim, &lo.to_string(), &hi.to_string())) -} - -fn outside(dim: &ArrayDimAst, lo: &str, hi: &str) -> RowError { - RowError::new( - 0, - "22003", - format!( - "the raster footprint spans [{lo}..{hi}] on dim '{}', outside its domain [{}..{}]", - dim.name, - bound_text(&dim.lo), - bound_text(&dim.hi) - ), - ) -} - -fn misaligned(input: &GridInput<'_>, why: String) -> RowError { - RowError::new( - 0, - "22023", - format!( - "raster grid does not align with array '{}': {why}; resample outside the engine", - input.view.name - ), - ) -} - -#[cfg(test)] -mod tests { - use super::*; - - fn int_dim(name: &str, lo: i64, hi: i64) -> ArrayDimAst { - ArrayDimAst { - name: name.into(), - dtype: ArrayDimType::Int64, - lo: ArrayDomainBound::Int64(lo), - hi: ArrayDomainBound::Int64(hi), - } - } - - fn float_dim(name: &str, lo: f64, hi: f64) -> ArrayDimAst { - ArrayDimAst { - name: name.into(), - dtype: ArrayDimType::Float64, - lo: ArrayDomainBound::Float64(lo), - hi: ArrayDomainBound::Float64(hi), - } - } - - fn view() -> ArrayCatalogView { - ArrayCatalogView { - name: "scene".into(), - dims: Vec::new(), - attrs: Vec::new(), - tile_extents: Vec::new(), - georef: None, - } - } - - const GT: [f64; 6] = [600_000.0, 30.0, 0.0, 700_000.0, 0.0, -30.0]; - - fn input<'a>( - view: &'a ArrayCatalogView, - x: &'a ArrayDimAst, - y: &'a ArrayDimAst, - array_gt: Option<[f64; 6]>, - file_gt: [f64; 6], - ) -> GridInput<'a> { - GridInput { - view, - x_dim: x, - y_dim: y, - array_gt, - file_gt, - width: 256, - height: 192, - } - } - - #[test] - fn a_shifted_file_lands_at_its_pixel_offset() { - let (v, x, y) = (view(), int_dim("col", 0, 1000), int_dim("row", 0, 1000)); - let file = [ - 600_000.0 + 30.0 * 10.0, - 30.0, - 0.0, - 700_000.0 - 30.0 * 4.0, - 0.0, - -30.0, - ]; - let grid = plan_grid(&input(&v, &x, &y, Some(GT), file)).expect("aligned"); - let (cx, cy) = grid.cell(0, 0); - assert_eq!(cx, ArrayCoordLiteral::Int64(10)); - assert_eq!(cy, ArrayCoordLiteral::Int64(4)); - } - - #[test] - fn a_half_pixel_shift_is_misaligned() { - let (v, x, y) = (view(), int_dim("col", 0, 1000), int_dim("row", 0, 1000)); - let file = [600_015.0, 30.0, 0.0, 700_000.0, 0.0, -30.0]; - let err = plan_grid(&input(&v, &x, &y, Some(GT), file)).expect_err("half pixel"); - assert_eq!(err.sqlstate, "22023"); - assert!( - err.message.contains("resample outside the engine"), - "{}", - err.message - ); - let scaled = [600_000.0, 10.0, 0.0, 700_000.0, 0.0, -10.0]; - let err = plan_grid(&input(&v, &x, &y, Some(GT), scaled)).expect_err("pixel size"); - assert_eq!(err.sqlstate, "22023"); - } - - #[test] - fn a_footprint_past_the_domain_is_22003() { - let (v, x, y) = (view(), int_dim("col", 0, 255), int_dim("row", 0, 99)); - let err = plan_grid(&input(&v, &x, &y, Some(GT), GT)).expect_err("rows past 99"); - assert_eq!(err.sqlstate, "22003"); - } - - #[test] - fn world_cells_are_pixel_centres() { - let v = view(); - let x = float_dim("x", 600_000.0, 607_680.0); - let y = float_dim("y", 694_240.0, 700_000.0); - let grid = plan_grid(&input(&v, &x, &y, None, GT)).expect("inside"); - let (cx, cy) = grid.cell(2, 1); - assert_eq!(cx, ArrayCoordLiteral::Float64(600_075.0)); - assert_eq!(cy, ArrayCoordLiteral::Float64(699_955.0)); - } - - #[test] - fn int_world_axes_need_whole_centres() { - let v = view(); - let x = int_dim("x", 0, 10_000_000); - let y = int_dim("y", 0, 10_000_000); - let quarter = [600_000.0, 25.0, 0.0, 700_000.0, 0.0, -25.0]; - let err = plan_grid(&input(&v, &x, &y, None, quarter)).expect_err("centres at 12.5 m"); - assert_eq!(err.sqlstate, "22023"); - let shifted = [599_985.0, 30.0, 0.0, 700_015.0, 0.0, -30.0]; - let grid = plan_grid(&input(&v, &x, &y, None, shifted)).expect("whole centres"); - let (cx, cy) = grid.cell(1, 1); - assert_eq!(cx, ArrayCoordLiteral::Int64(600_030)); - assert_eq!(cy, ArrayCoordLiteral::Int64(699_970)); - } -} diff --git a/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/mod.rs b/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/mod.rs index 1a0d8b59e..203c6f6e0 100644 --- a/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/mod.rs +++ b/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/mod.rs @@ -1,9 +1,8 @@ // SPDX-License-Identifier: BUSL-1.1 -pub mod bands; -pub mod grid; -pub mod open; -pub mod pixels; +pub mod adopt; +pub mod driver; +pub mod fetch; pub mod refusal; pub mod task; diff --git a/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/open.rs b/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/open.rs deleted file mode 100644 index 64a401dd8..000000000 --- a/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/open.rs +++ /dev/null @@ -1,429 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 - -//! The open-time checks of a GeoTIFF load. Every check runs before the -//! first block decodes, so a refused file commits no row. -//! -//! - The array must have a GEOREF (`55000`), and the file CRS must be its -//! SRID (`22023`). -//! - Bands map onto attrs by the lossless type rule (`42804`), and an attr -//! no band fills must be nullable (`23502`). -//! - The file grid must align with the array (`22023`), and its footprint -//! must fit the dim domains (`22003`). -//! - `TIME` fills the one `TIMESTAMP` dim. Any other dim has no source. -//! - The nodata value must be a value of the sample type (`22023`). - -use nodedb_raster::{BandSamples, SampleType}; -use nodedb_sql::catalog::ArrayCatalogView; -use nodedb_sql::ddl_ast::statement::CopyRasterOptions; -use nodedb_sql::types_array::{ArrayDimAst, ArrayDimType, ArrayDomainBound, ArrayGeorefAst}; -use nodedb_types::geometry::Srid; - -use super::super::decode::{RowError, bound_text}; -use super::bands::{AttrSource, map_bands}; -use super::grid::{CellGrid, GridInput, plan_grid}; - -/// What a GeoTIFF's tags say, read on the decoder thread. -#[derive(Debug, Clone)] -pub(super) struct RasterHeader { - pub width: u32, - pub height: u32, - pub bands: u16, - pub sample_type: SampleType, - /// The pixel-corner geotransform. `None` when the file has none. - pub transform: Option<[f64; 6]>, - /// The CRS the GeoKeys name. `None` when they name none. - pub srid: Option, - /// The `GDAL_NODATA` value. - pub nodata: Option, -} - -/// The source of one dim's coordinate. -#[derive(Debug, Clone, Copy, PartialEq)] -pub(super) enum CoordSource { - /// The GEOREF x axis. - X, - /// The GEOREF y axis. - Y, - /// The `TIME` instant, in ms since the Unix epoch. - Time(i64), -} - -/// The nodata value, typed by the sample type. -#[derive(Debug, Clone, Copy)] -pub(super) enum Nodata { - Int(i64), - F32(f32), - F64(f64), -} - -impl Nodata { - /// Whether sample `index` of `band` is nodata. NaN matches NaN. - pub(super) fn matches(self, band: &BandSamples, index: usize) -> bool { - match (self, band) { - (Nodata::Int(n), BandSamples::U8(v)) => { - v.get(index).is_some_and(|s| i64::from(*s) == n) - } - (Nodata::Int(n), BandSamples::U16(v)) => { - v.get(index).is_some_and(|s| i64::from(*s) == n) - } - (Nodata::Int(n), BandSamples::I16(v)) => { - v.get(index).is_some_and(|s| i64::from(*s) == n) - } - (Nodata::Int(n), BandSamples::U32(v)) => { - v.get(index).is_some_and(|s| i64::from(*s) == n) - } - (Nodata::F32(n), BandSamples::F32(v)) => v - .get(index) - .is_some_and(|s| *s == n || (s.is_nan() && n.is_nan())), - (Nodata::F64(n), BandSamples::F64(v)) => v - .get(index) - .is_some_and(|s| *s == n || (s.is_nan() && n.is_nan())), - ( - Nodata::Int(_) | Nodata::F32(_) | Nodata::F64(_), - BandSamples::U8(_) - | BandSamples::U16(_) - | BandSamples::I16(_) - | BandSamples::U32(_) - | BandSamples::F32(_) - | BandSamples::F64(_), - ) => false, - } - } -} - -/// How every pixel of an accepted file becomes a row. -#[derive(Debug)] -pub(super) struct RasterPlan { - pub(super) width: u32, - pub(super) bands: u16, - /// One source per dim, in schema order. - pub(super) coords: Vec, - /// One source per attr, in schema order. - pub(super) attrs: Vec, - /// The file bands that load. A pixel is nodata when all of them are. - pub(super) loaded: Vec, - pub(super) grid: CellGrid, - pub(super) nodata: Option, -} - -impl RasterPlan { - /// Run every open-time check of `header` against `view`. - pub(super) fn new( - view: &ArrayCatalogView, - georef: &ArrayGeorefAst, - header: &RasterHeader, - options: &CopyRasterOptions, - ) -> Result { - check_srid(view, georef, header)?; - let map = map_bands( - view, - header.bands, - header.sample_type, - options.bands.as_deref(), - )?; - let (x_index, x_dim) = georef_axis(view, &georef.x_dim)?; - let (y_index, y_dim) = georef_axis(view, &georef.y_dim)?; - let file_gt = header.transform.ok_or_else(|| { - RowError::new( - 0, - "22023", - "the file has no geotransform, so no pixel has a world position".to_string(), - ) - })?; - let grid = plan_grid(&GridInput { - view, - x_dim, - y_dim, - array_gt: georef.transform, - file_gt, - width: header.width, - height: header.height, - })?; - let coords = plan_coords(view, x_index, y_index, options.time_ms)?; - let nodata = options - .nodata - .or(header.nodata) - .map(|value| typed_nodata(value, header.sample_type)) - .transpose()?; - Ok(Self { - width: header.width, - bands: header.bands, - coords, - attrs: map.attrs, - loaded: map.loaded, - grid, - nodata, - }) - } -} - -fn check_srid( - view: &ArrayCatalogView, - georef: &ArrayGeorefAst, - header: &RasterHeader, -) -> Result<(), RowError> { - match header.srid { - Some(srid) if srid.code() == georef.srid => Ok(()), - Some(srid) => Err(RowError::new( - 0, - "22023", - format!( - "the file CRS is EPSG:{}, array '{}' GEOREF is SRID {}; \ - reproject outside the engine", - srid.code(), - view.name, - georef.srid - ), - )), - None => Err(RowError::new( - 0, - "22023", - format!( - "the file GeoKeys name no CRS, array '{}' GEOREF is SRID {}", - view.name, georef.srid - ), - )), - } -} - -/// The index and spec of the dim a GEOREF axis names. -fn georef_axis<'v>( - view: &'v ArrayCatalogView, - name: &str, -) -> Result<(usize, &'v ArrayDimAst), RowError> { - view.dims - .iter() - .enumerate() - .find(|(_, dim)| dim.name.eq_ignore_ascii_case(name)) - .ok_or_else(|| { - RowError::new( - 0, - "XX000", - format!( - "array '{}' GEOREF names dim '{name}', which it lacks", - view.name - ), - ) - }) -} - -/// The source of every dim: the GEOREF axes, then `TIME` for the one -/// `TIMESTAMP` dim. Any other dim has no source in a raster (`0A000`). -fn plan_coords( - view: &ArrayCatalogView, - x_index: usize, - y_index: usize, - time_ms: Option, -) -> Result, RowError> { - let mut time_used = false; - let mut coords = Vec::with_capacity(view.dims.len()); - for (index, dim) in view.dims.iter().enumerate() { - let source = if index == x_index { - CoordSource::X - } else if index == y_index { - CoordSource::Y - } else if dim.dtype == ArrayDimType::TimestampMs && !time_used { - time_used = true; - CoordSource::Time(time_coord(dim, time_ms)?) - } else { - return Err(RowError::new( - 0, - "0A000", - format!( - "dim '{}' has no source in a GeoTIFF; a raster fills the GEOREF axes \ - and one TIMESTAMP dim from TIME", - dim.name - ), - )); - }; - coords.push(source); - } - if time_ms.is_some() && !time_used { - return Err(RowError::new( - 0, - "42601", - format!( - "TIME is given, and array '{}' has no TIMESTAMP dim", - view.name - ), - )); - } - Ok(coords) -} - -/// The `TIME` instant for `dim`, checked against its domain. -fn time_coord(dim: &ArrayDimAst, time_ms: Option) -> Result { - let time = time_ms.ok_or_else(|| { - RowError::new( - 0, - "42601", - format!( - "dim '{}' is a TIMESTAMP; give the scene instant with WITH (TIME '')", - dim.name - ), - ) - })?; - let inside = match (&dim.lo, &dim.hi) { - ( - ArrayDomainBound::TimestampMs(lo) | ArrayDomainBound::Int64(lo), - ArrayDomainBound::TimestampMs(hi) | ArrayDomainBound::Int64(hi), - ) => *lo <= time && time <= *hi, - _ => false, - }; - if inside { - return Ok(time); - } - Err(RowError::new( - 0, - "22003", - format!( - "TIME {time} is outside dim '{}' domain [{}..{}]", - dim.name, - bound_text(&dim.lo), - bound_text(&dim.hi) - ), - )) -} - -/// `value` as a nodata of `sample`. An integer type needs a whole value in -/// its range. `f32` takes the nearest `f32`, as GDAL compares. -fn typed_nodata(value: f64, sample: SampleType) -> Result { - let range = match sample { - SampleType::U8 => Some((f64::from(u8::MIN), f64::from(u8::MAX))), - SampleType::U16 => Some((f64::from(u16::MIN), f64::from(u16::MAX))), - SampleType::I16 => Some((f64::from(i16::MIN), f64::from(i16::MAX))), - SampleType::U32 => Some((f64::from(u32::MIN), f64::from(u32::MAX))), - SampleType::F32 if value.is_finite() && value.abs() > f64::from(f32::MAX) => { - return Err(unrepresentable(value, sample)); - } - SampleType::F32 => return Ok(Nodata::F32(value as f32)), - SampleType::F64 => return Ok(Nodata::F64(value)), - }; - match range { - Some((lo, hi)) if value.fract() == 0.0 && lo <= value && value <= hi => { - Ok(Nodata::Int(value as i64)) - } - _ => Err(unrepresentable(value, sample)), - } -} - -fn unrepresentable(value: f64, sample: SampleType) -> RowError { - RowError::new( - 0, - "22023", - format!("nodata {value} is not a {sample} value, so no sample can match it"), - ) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn integer_nodata_must_be_a_sample_value() { - assert!(matches!( - typed_nodata(0.0, SampleType::U16), - Ok(Nodata::Int(0)) - )); - assert!(matches!( - typed_nodata(-9999.0, SampleType::I16), - Ok(Nodata::Int(-9999)) - )); - for (value, sample) in [ - (-1.0, SampleType::U16), - (65536.0, SampleType::U16), - (0.5, SampleType::U8), - (f64::NAN, SampleType::U32), - (1e39, SampleType::F32), - ] { - let err = typed_nodata(value, sample).expect_err("unrepresentable"); - assert_eq!(err.sqlstate, "22023", "{value} as {sample}"); - } - } - - #[test] - fn nan_nodata_matches_nan_samples() { - let band = BandSamples::F32(vec![f32::NAN, 1.0]); - let nodata = typed_nodata(f64::NAN, SampleType::F32).expect("NaN is an f32 value"); - assert!(nodata.matches(&band, 0)); - assert!(!nodata.matches(&band, 1)); - let ints = BandSamples::U16(vec![0, 7]); - assert!(Nodata::Int(0).matches(&ints, 0)); - assert!(!Nodata::Int(0).matches(&ints, 1)); - } - - fn dim( - name: &str, - dtype: ArrayDimType, - lo: ArrayDomainBound, - hi: ArrayDomainBound, - ) -> ArrayDimAst { - ArrayDimAst { - name: name.into(), - dtype, - lo, - hi, - } - } - - fn view(dims: Vec) -> ArrayCatalogView { - ArrayCatalogView { - name: "scene".into(), - dims, - attrs: Vec::new(), - tile_extents: Vec::new(), - georef: None, - } - } - - #[test] - fn time_fills_the_timestamp_dim() { - let ts = || { - dim( - "t", - ArrayDimType::TimestampMs, - ArrayDomainBound::TimestampMs(0), - ArrayDomainBound::TimestampMs(1_000), - ) - }; - let axis = |name: &str| { - dim( - name, - ArrayDimType::Int64, - ArrayDomainBound::Int64(0), - ArrayDomainBound::Int64(9), - ) - }; - let v = view(vec![ts(), axis("row"), axis("col")]); - let coords = plan_coords(&v, 2, 1, Some(500)).expect("time given"); - assert_eq!( - coords, - vec![CoordSource::Time(500), CoordSource::Y, CoordSource::X] - ); - assert_eq!( - plan_coords(&v, 2, 1, None).expect_err("no TIME").sqlstate, - "42601" - ); - assert_eq!( - plan_coords(&v, 2, 1, Some(5_000)) - .expect_err("past") - .sqlstate, - "22003" - ); - - let plain = view(vec![axis("row"), axis("col")]); - assert_eq!( - plan_coords(&plain, 1, 0, Some(5)) - .expect_err("no ts") - .sqlstate, - "42601" - ); - let band = view(vec![axis("band"), axis("row"), axis("col")]); - assert_eq!( - plan_coords(&band, 2, 1, None) - .expect_err("band dim") - .sqlstate, - "0A000" - ); - } -} diff --git a/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/pixels.rs b/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/pixels.rs deleted file mode 100644 index f4ed8d0cc..000000000 --- a/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/pixels.rs +++ /dev/null @@ -1,282 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 - -//! The rows of one decoded GeoTIFF block, read one pixel at a time. -//! -//! A row's number is the pixel's row-major ordinal in the image plus one. -//! A pixel whose loaded bands all hold nodata is skipped. A kept pixel -//! with one nodata band writes NULL there, or is refused with `23502` when -//! the attr is NOT NULL. Open checked the types and the footprint, so no -//! other check runs per pixel. -//! -//! Each kept pixel allocates its coordinate and attr vectors, sized -//! exactly. A skipped pixel allocates nothing. - -use nodedb_raster::{BandSamples, RasterBlock}; -use nodedb_sql::types_array::{ArrayAttrLiteral, ArrayCoordLiteral, ArrayInsertRow}; - -use super::super::decode::RowError; -use super::bands::AttrSource; -use super::open::{CoordSource, RasterPlan}; - -/// The rows of one block in pixel order within the block. -pub(super) struct BlockRows<'a> { - plan: &'a RasterPlan, - block: &'a RasterBlock, - next: usize, - pixels: usize, -} - -impl<'a> BlockRows<'a> { - /// Read `block` under `plan`. A block with the wrong band count or a - /// short band is a malformed file (`22P04`). - pub(super) fn new(plan: &'a RasterPlan, block: &'a RasterBlock) -> Result { - let pixels = block.window.pixels(); - if block.bands.len() != usize::from(plan.bands) { - return Err(RowError::new( - 0, - "22P04", - format!( - "GeoTIFF block {} decoded {} bands, the file declares {}", - block.index, - block.bands.len(), - plan.bands - ), - )); - } - if let Some(short) = block.bands.iter().find(|band| band.len() < pixels) { - return Err(RowError::new( - 0, - "22P04", - format!( - "GeoTIFF block {} decoded {} samples, its window holds {pixels}", - block.index, - short.len() - ), - )); - } - Ok(Self { - plan, - block, - next: 0, - pixels, - }) - } - - /// Whether every loaded band of pixel `index` holds nodata. - fn is_nodata(&self, index: usize) -> bool { - let Some(nodata) = self.plan.nodata else { - return false; - }; - self.plan.loaded.iter().all(|&band| { - self.block - .bands - .get(band) - .is_some_and(|samples| nodata.matches(samples, index)) - }) - } - - /// The row of pixel `index` of the block. - fn row(&self, index: usize) -> Result<(usize, ArrayInsertRow), RowError> { - let window = &self.block.window; - let local_row = index / window.width as usize; - let local_col = index % window.width as usize; - // The window lies inside the image, so both fit `u32`. - let row = window.row_off + local_row as u32; - let col = window.col_off + local_col as u32; - let row_no = row as usize * self.plan.width as usize + col as usize + 1; - let (x, y) = self.plan.grid.cell(col, row); - let mut coords = Vec::with_capacity(self.plan.coords.len()); - for source in &self.plan.coords { - coords.push(match source { - CoordSource::X => x.clone(), - CoordSource::Y => y.clone(), - CoordSource::Time(ms) => ArrayCoordLiteral::Int64(*ms), - }); - } - let mut attrs = Vec::with_capacity(self.plan.attrs.len()); - for source in &self.plan.attrs { - let AttrSource::Band { - band, - float, - nullable, - name, - } = source - else { - attrs.push(ArrayAttrLiteral::Null); - continue; - }; - let samples = self - .block - .bands - .get(*band) - .ok_or_else(|| missing_band(*band, row_no))?; - let is_nodata = self.plan.nodata.is_some_and(|n| n.matches(samples, index)); - let literal = match (is_nodata, *nullable) { - (true, true) => ArrayAttrLiteral::Null, - (true, false) => { - return Err(RowError::new( - row_no, - "23502", - format!( - "attr '{name}' is NOT NULL, and band {} holds nodata at \ - (pixel row {row}, col {col})", - band + 1 - ), - )); - } - (false, _) => sample_literal(samples, index, *float) - .ok_or_else(|| missing_band(*band, row_no))?, - }; - attrs.push(literal); - } - Ok((row_no, ArrayInsertRow { coords, attrs })) - } -} - -impl Iterator for BlockRows<'_> { - type Item = Result<(usize, ArrayInsertRow), RowError>; - - fn next(&mut self) -> Option { - while self.next < self.pixels { - let index = self.next; - self.next += 1; - if !self.is_nodata(index) { - return Some(self.row(index)); - } - } - None - } -} - -/// Sample `index` as an attr literal. Open admitted only lossless pairs, -/// so an integer sample widens to `f64` exactly when the attr is a float. -fn sample_literal(samples: &BandSamples, index: usize, float: bool) -> Option { - let int = |n: i64| { - if float { - ArrayAttrLiteral::Float64(n as f64) - } else { - ArrayAttrLiteral::Int64(n) - } - }; - match samples { - BandSamples::U8(v) => v.get(index).map(|s| int(i64::from(*s))), - BandSamples::U16(v) => v.get(index).map(|s| int(i64::from(*s))), - BandSamples::I16(v) => v.get(index).map(|s| int(i64::from(*s))), - BandSamples::U32(v) => v.get(index).map(|s| int(i64::from(*s))), - BandSamples::F32(v) => v - .get(index) - .map(|s| ArrayAttrLiteral::Float64(f64::from(*s))), - BandSamples::F64(v) => v.get(index).map(|s| ArrayAttrLiteral::Float64(*s)), - } -} - -fn missing_band(band: usize, row_no: usize) -> RowError { - RowError::new( - row_no, - "22P04", - format!("GeoTIFF band {} has no sample for this pixel", band + 1), - ) -} - -#[cfg(test)] -mod tests { - use super::super::grid::CellGrid; - use super::super::open::Nodata; - use super::*; - use nodedb_raster::BlockWindow; - - fn plan(nullable: bool) -> RasterPlan { - RasterPlan { - width: 4, - bands: 2, - coords: vec![CoordSource::Y, CoordSource::X], - attrs: vec![ - AttrSource::Band { - band: 0, - float: false, - nullable, - name: "b1".into(), - }, - AttrSource::Band { - band: 1, - float: true, - nullable: true, - name: "b2".into(), - }, - AttrSource::Null, - ], - loaded: vec![0, 1], - grid: CellGrid::Pixel { col0: 10, row0: 20 }, - nodata: Some(Nodata::Int(0)), - } - } - - fn block() -> RasterBlock { - RasterBlock { - index: 1, - window: BlockWindow { - col_off: 2, - row_off: 0, - width: 2, - height: 2, - }, - bands: vec![ - BandSamples::U16(vec![5, 0, 0, 7]), - BandSamples::U16(vec![6, 0, 9, 8]), - ], - } - } - - #[test] - fn rows_skip_all_nodata_pixels_and_null_partial_ones() { - let (plan, block) = (plan(true), block()); - let rows: Vec<_> = BlockRows::new(&plan, &block) - .expect("block fits") - .collect::>() - .expect("all rows load"); - let numbers: Vec = rows.iter().map(|(n, _)| *n).collect(); - // Pixel (0, 3) is nodata in both bands. Row-major ordinals: 2, 6, 7. - assert_eq!(numbers, vec![3, 7, 8]); - let (_, first) = &rows[0]; - assert_eq!( - first.coords, - vec![ArrayCoordLiteral::Int64(20), ArrayCoordLiteral::Int64(12)] - ); - assert_eq!( - first.attrs, - vec![ - ArrayAttrLiteral::Int64(5), - ArrayAttrLiteral::Float64(6.0), - ArrayAttrLiteral::Null - ] - ); - let (_, partial) = &rows[1]; - assert_eq!(partial.attrs[0], ArrayAttrLiteral::Null); - assert_eq!(partial.attrs[1], ArrayAttrLiteral::Float64(9.0)); - } - - #[test] - fn a_nodata_band_in_a_not_null_attr_names_the_pixel() { - let (plan, block) = (plan(false), block()); - let err = BlockRows::new(&plan, &block) - .expect("block fits") - .find_map(Result::err) - .expect("pixel (1, 2) is refused"); - assert_eq!(err.sqlstate, "23502"); - assert_eq!(err.row, 7); - assert!( - err.message.contains("(pixel row 1, col 2)"), - "{}", - err.message - ); - } - - #[test] - fn a_short_block_is_malformed() { - let plan = plan(true); - let mut block = block(); - block.bands[1] = BandSamples::U16(vec![1]); - let err = BlockRows::new(&plan, &block).err().expect("short band"); - assert_eq!(err.sqlstate, "22P04"); - } -} diff --git a/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/refusal.rs b/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/refusal.rs index 5a71e4ef4..50a9bbde4 100644 --- a/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/refusal.rs +++ b/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/refusal.rs @@ -1,27 +1,63 @@ // SPDX-License-Identifier: BUSL-1.1 -//! The SQLSTATE of each GeoTIFF reader error. +//! The SQLSTATE of each raster load error. //! -//! - A malformed or corrupt file is `22P04` (bad copy file format). -//! - A valid file that uses a feature the reader lacks is `0A000`. -//! - A block over the decode cap is `54000`. +//! - A malformed or corrupt file is `22P04` (bad copy file format). A +//! user-defined CRS whose GeoKeys do not form a valid definition is one, +//! and so is a corrupt compressed block. +//! - A valid file that uses a feature the reader lacks is `0A000`. A CRS +//! that is undefined, or a user-defined CRS no built-in CRS matches, is +//! one. +//! - A block or header over its cap is `54000`. //! - A read error outside the file's bytes is `58030`. +//! - A fetch failure reports the class [`FetchError::sqlstate`] names. +//! - A plan refusal reports the class [`RasterLoadError::sqlstate`] names. use nodedb_raster::RasterError; +use nodedb_raster_load::RasterLoadError; use super::super::decode::RowError; +use super::driver::DriveError; +use super::fetch::FetchError; -/// `e`, met reading `path` under a `cap`-byte decode cap, as a refusal of +impl From for RowError { + fn from(e: RasterLoadError) -> Self { + RowError::new(e.row(), e.sqlstate(), e.to_string()) + } +} + +impl From for RowError { + fn from(e: FetchError) -> Self { + RowError::new(0, e.sqlstate(), e.to_string()) + } +} + +/// `e`, met reading the sources `names` under a `cap`-byte decode cap, as a +/// refusal of the whole input. +pub(super) fn drive_refusal(e: DriveError, names: &[String], cap: usize) -> RowError { + match e { + DriveError::Raster { index, error } => { + let name = names.get(index).map_or("?", String::as_str); + raster_refusal(&error, name, cap) + } + DriveError::Fetch(error) => error.into(), + DriveError::Task(detail) => { + RowError::new(0, "XX000", format!("raster decode task failed: {detail}")) + } + } +} + +/// `e`, met reading `name` under a `cap`-byte decode cap, as a refusal of /// the whole input. -pub(super) fn raster_refusal(e: &RasterError, path: &str, cap: usize) -> RowError { +pub(super) fn raster_refusal(e: &RasterError, name: &str, cap: usize) -> RowError { let sqlstate = sqlstate(e); let message = if sqlstate == "54000" { format!( - "'{path}': {e}; the decode cap is {cap} bytes; \ + "'{name}': {e}; the decode cap is {cap} bytes; \ retile the file with `gdal_translate -co TILED=YES`" ) } else { - format!("'{path}': {e}") + format!("'{name}': {e}") }; RowError::new(0, sqlstate, message) } @@ -29,27 +65,37 @@ pub(super) fn raster_refusal(e: &RasterError, path: &str, cap: usize) -> RowErro fn sqlstate(e: &RasterError) -> &'static str { match e { RasterError::Malformed(_) - | RasterError::TagValueOutOfBounds { .. } + | RasterError::Codec(_) | RasterError::GeoKeys(_) | RasterError::GeoTagLength { .. } | RasterError::PixelScaleWithoutTiepoint | RasterError::DegenerateTransform { .. } | RasterError::MissingCrsKey { .. } + | RasterError::UserCrsParse(_) | RasterError::InvalidNodata { .. } + | RasterError::InvalidMetadata { .. } | RasterError::BlockOutOfRange { .. } - | RasterError::ShortBlock { .. } => "22P04", + | RasterError::BlockBuffers { .. } + | RasterError::ShortBlock { .. } + | RasterError::SourceMissing { .. } + | RasterError::RangeOverrun { .. } + | RasterError::OpenStalled { .. } => "22P04", RasterError::UnsupportedTiff(_) | RasterError::UnsupportedCompression { .. } | RasterError::UnsupportedSampleType { .. } | RasterError::UnsupportedPhotometric { .. } - | RasterError::UnsupportedBandLayout { .. } | RasterError::ReducedResolutionFirstImage + | RasterError::ImageOutOfRange { .. } | RasterError::GroundControlPoints | RasterError::NonAffineTransformation | RasterError::UnsupportedModelType { .. } - | RasterError::UserDefinedCrs { .. } - | RasterError::UnsupportedCrs(_) => "0A000", - RasterError::BlockTooLarge { .. } | RasterError::DecodeLimitExceeded { .. } => "54000", + | RasterError::UnknownCrs(_) + | RasterError::UnsupportedUserCrs { .. } + | RasterError::UnmatchedUserCrs { .. } + | RasterError::AmbiguousUserCrs { .. } => "0A000", + RasterError::BlockTooLarge { .. } + | RasterError::HeaderTooLarge { .. } + | RasterError::TagValueTooLarge { .. } => "54000", RasterError::Io(_) => "58030", } } @@ -82,7 +128,10 @@ mod tests { #[test] fn an_oversized_block_names_the_retile_command() { - let error = RasterError::DecodeLimitExceeded { limit_bytes: 4096 }; + let error = RasterError::BlockTooLarge { + needed_bytes: 8192, + limit_bytes: 4096, + }; let refusal = raster_refusal(&error, "/data/scene.tif", 4096); assert!( refusal.message.contains("gdal_translate -co TILED=YES"), @@ -90,4 +139,17 @@ mod tests { refusal.message ); } + + #[test] + fn a_plan_refusal_keeps_its_row_and_class() { + let row: RowError = RasterLoadError::NodataInNotNull { + row: 9, + attr: "red".into(), + band: 1, + pixel_row: 0, + pixel_col: 8, + } + .into(); + assert_eq!((row.row, row.sqlstate), (9, "23502")); + } } diff --git a/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/task.rs b/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/task.rs index 00713adff..083949260 100644 --- a/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/task.rs +++ b/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/array_load/raster/task.rs @@ -1,145 +1,226 @@ // SPDX-License-Identifier: BUSL-1.1 -//! `COPY FROM '' WITH (FORMAT geotiff)`: the decode task and -//! the load that drains it. +//! `COPY FROM WITH (FORMAT geotiff, ...)`: one transport +//! per source, then the one async driver. //! -//! One blocking task owns the file handle and the decoder. It reads the -//! tags, waits for the open-time checks, then decodes one block at a time -//! into a channel of two. The async side turns each block into rows and -//! feeds the array loader, so the load holds at most four decoded blocks -//! and the loader's few chunks. Decoding runs on the Control Plane's -//! blocking pool, never on a Data Plane core. +//! - A server path opens once through the jailed handle every check ran +//! on. The entry point checked the superuser privilege and the path. +//! - An `http(s)://` URL passes the egress policy before the first request. +//! A `CREDENTIAL` of `TYPE 'bearer'` sends its token as +//! `Authorization: Bearer`; one of `TYPE 'sas'` appends its signature to +//! the query. +//! - An `s3://bucket/key` object reads through object_store over the egress +//! client. A `CREDENTIAL` of `TYPE 's3'` signs the requests and names the +//! region; without one the bucket is read unsigned, in the region AWS +//! reports for it. +//! +//! `OVERVIEW n` opens image `n` of every source: 0 is full resolution, 1 +//! the first overview. -use std::io::BufReader; +use std::sync::Arc; +use std::time::Duration; -use nodedb_raster::{GeoTiffReader, RasterBlock, RasterError, RasterLimits}; -use nodedb_sql::ddl_ast::statement::CopyRasterOptions; -use tokio::sync::{mpsc, oneshot}; -use tokio::task::JoinHandle; +use nodedb_raster::TiffOpener; +use nodedb_sql::ddl_ast::statement::{CopyRasterOptions, CopySource}; +use zeroize::Zeroizing; +use crate::control::security::egress::{EgressClientOptions, SystemLookup, egress_client}; +use crate::control::security::source_credential::{CredentialSecret, open_secret}; use crate::control::server::shared::ddl::result::DdlError; use super::super::super::source_file::CopySourceFile; -use super::super::decode::RowError; -use super::super::dispatch::{ArrayLoadCtx, input_cap}; -use super::super::loader::{ArrayLoader, LoadInput}; -use super::open::{RasterHeader, RasterPlan}; -use super::pixels::BlockRows; -use super::refusal::raster_refusal; - -/// The most bytes one block decodes to, before the memory headroom cap. -const MAX_BLOCK_BYTES: usize = 64 * 1024 * 1024; -/// Decoded blocks queued between the decoder and the load. -const BLOCK_QUEUE: usize = 2; +use super::super::dispatch::ArrayLoadCtx; +use super::driver::drive_raster; +use super::fetch::http::display_name; +use super::fetch::{ + FetchError, HttpAuth, HttpRange, LocalFile, ObjectStoreRange, S3Keys, S3Target, SourceFetch, + SourceSet, bucket_region, +}; -type HeaderResult = Result; -type BlockResult = Result; +/// Redirects one remote request follows. +const MAX_REDIRECTS: usize = 5; +/// Cap on connecting to a remote source. +const CONNECT_TIMEOUT: Duration = Duration::from_secs(10); +/// Cap on the time between two reads of a remote answer. +const READ_TIMEOUT: Duration = Duration::from_secs(60); -/// Load the GeoTIFF at `path` into `array`. Returns the number of rows -/// loaded: every pixel but the all-nodata ones. +/// Load `sources` into `array`. Returns the number of rows loaded: every +/// pixel but the masked and all-nodata ones. pub(crate) async fn copy_raster( ctx: &ArrayLoadCtx<'_>, array: &str, - path: &str, + sources: &[CopySource], options: &CopyRasterOptions, ) -> Result { - let mut loader = ArrayLoader::open(ctx, array, LoadInput::Raster)?; - let georef = loader.view().georef.clone().ok_or_else(|| { - loader.refuse(RowError::new( - 0, - "55000", - format!( - "array '{array}' has no GEOREF, so no pixel has a cell; \ - add one with ALTER ARRAY {array} SET GEOREF (...)" - ), - )) - })?; - let cap = MAX_BLOCK_BYTES.min(input_cap(ctx.state)); - let file = CopySourceFile::open(path).await?.into_std().await; - let (header_tx, header_rx) = oneshot::channel(); - let (go_tx, go_rx) = oneshot::channel(); - let (block_tx, mut block_rx) = mpsc::channel(BLOCK_QUEUE); - let decoder = tokio::task::spawn_blocking(move || { - decode_file(file, cap, header_tx, go_rx, block_tx); - }); - - let header = match header_rx.await { - Ok(Ok(header)) => header, - Ok(Err(e)) => return Err(loader.refuse(raster_refusal(&e, path, cap))), - Err(_) => return Err(decoder_gone(decoder).await), + let secret = match &options.credential { + Some(name) => Some(resolve_credential(ctx, name)?), + None => None, }; - let plan = - RasterPlan::new(loader.view(), &georef, &header, options).map_err(|e| loader.refuse(e))?; - if go_tx.send(()).is_err() { - return Err(decoder_gone(decoder).await); - } - while let Some(next) = block_rx.recv().await { - let block = next.map_err(|e| loader.refuse(raster_refusal(&e, path, cap)))?; - let rows = BlockRows::new(&plan, &block).map_err(|e| loader.refuse(e))?; - loader.feed_rows(ctx, rows).await?; + let remote = sources.iter().any(|s| !matches!(s, CopySource::File(_))); + if secret.is_some() && !remote { + return Err(DdlError::new( + "22023", + "COPY: CREDENTIAL applies to http(s):// and s3:// sources; every source is a server \ + file", + )); } - // The channel closes once the decoder returns. A panic closes it early, - // so the join result tells a whole file from a cut-short one. - decoder - .await - .map_err(|e| DdlError::internal(format!("COPY: GeoTIFF decoder failed: {e}")))?; - loader.finish(ctx).await -} - -/// The error of a decoder that ended before it answered. -async fn decoder_gone(decoder: JoinHandle<()>) -> DdlError { - match decoder.await { - Err(e) => DdlError::internal(format!("COPY: GeoTIFF decoder failed: {e}")), - Ok(()) => DdlError::internal("COPY: GeoTIFF decoder ended before the file was read"), + let client = if remote { + Some( + egress_client( + Arc::clone(ctx.state.remote_sources.egress()), + Arc::new(SystemLookup), + EgressClientOptions { + timeout: None, + connect_timeout: CONNECT_TIMEOUT, + read_timeout: READ_TIMEOUT, + max_redirects: MAX_REDIRECTS, + }, + ) + .map_err(|e| DdlError::internal(format!("COPY: HTTP client: {e}")))?, + ) + } else { + None + }; + let mut names = Vec::with_capacity(sources.len()); + let mut fetches = Vec::with_capacity(sources.len()); + for source in sources { + let (name, fetch) = + transport(ctx, source, client.as_ref(), secret.as_ref(), options).await?; + names.push(name); + fetches.push(fetch); } + let opener = TiffOpener::new().with_image(options.overview.unwrap_or(0)); + let fetch = Arc::new(SourceSet::new(fetches)); + drive_raster(ctx, array, &names, opener, fetch, options).await } -/// The decode task. It ends early once the load drops its side: a refused -/// file, a refused row, or a cancelled statement. -fn decode_file( - file: std::fs::File, - cap: usize, - header_tx: oneshot::Sender, - go: oneshot::Receiver<()>, - blocks: mpsc::Sender, -) { - let opened = GeoTiffReader::open(BufReader::new(file), RasterLimits::new(cap)) - .and_then(|reader| header_of(&reader).map(|header| (reader, header))); - let mut reader = match opened { - Ok((reader, header)) => { - if header_tx.send(Ok(header)).is_err() { - return; - } - reader +/// The display name and transport of `source`. +async fn transport( + ctx: &ArrayLoadCtx<'_>, + source: &CopySource, + client: Option<&reqwest::Client>, + secret: Option<&CredentialSecret>, + options: &CopyRasterOptions, +) -> Result<(String, SourceFetch), DdlError> { + let client = || { + client + .cloned() + .ok_or_else(|| DdlError::internal("COPY: a remote source has no HTTP client")) + }; + match source { + CopySource::File(path) => { + let file = CopySourceFile::open(path).await?.into_std().await; + Ok(( + path.clone(), + SourceFetch::Local(LocalFile::new(file, path.clone())), + )) } - Err(e) => { - // A dropped receiver means the load already ended; nothing waits. - let _ = header_tx.send(Err(e)); - return; + CopySource::Http(text) => { + let url = reqwest::Url::parse(text) + .map_err(|_| DdlError::new("22023", "COPY: a source URL is malformed"))?; + let name = display_name(&url); + ctx.state + .remote_sources + .egress() + .check_parsed(&url) + .map_err(|error| { + fetch_error(FetchError::Egress { + source_name: name.clone(), + error, + }) + })?; + let auth = match secret { + None => HttpAuth::None, + Some(CredentialSecret::Bearer { token }) => { + HttpAuth::Bearer(Zeroizing::new(token.clone())) + } + Some(CredentialSecret::Sas { token }) => { + HttpAuth::Sas(Zeroizing::new(token.clone())) + } + Some(CredentialSecret::S3 { .. }) => { + return Err(wrong_type(&name, "s3", "s3:// sources")); + } + }; + Ok(( + name, + SourceFetch::Http(HttpRange::new(client()?, url, auth)), + )) } - }; - if go.blocking_recv().is_err() { - return; - } - for index in 0..reader.block_count() { - let block = reader.read_block(index); - let failed = block.is_err(); - if blocks.blocking_send(block).is_err() || failed { - return; + CopySource::S3 { bucket, key } => { + let name = source.display(); + let client = client()?; + let target = match secret { + Some(CredentialSecret::S3 { + access_key_id, + secret_access_key, + region, + session_token, + endpoint, + }) => S3Target { + bucket: bucket.clone(), + key: key.clone(), + region: region.clone(), + endpoint: endpoint.clone(), + keys: Some(S3Keys { + access_key_id: Zeroizing::new(access_key_id.clone()), + secret_access_key: Zeroizing::new(secret_access_key.clone()), + session_token: session_token.clone().map(Zeroizing::new), + }), + requester_pays: options.requester_pays, + }, + Some(other) => { + return Err(wrong_type(&name, other.type_name(), "http(s):// sources")); + } + None => S3Target { + bucket: bucket.clone(), + key: key.clone(), + region: bucket_region(&client, bucket).await.map_err(fetch_error)?, + endpoint: None, + keys: None, + requester_pays: options.requester_pays, + }, + }; + let store = ObjectStoreRange::new(client, target, ctx.state.remote_sources.egress()) + .map_err(fetch_error)?; + Ok((name, SourceFetch::Store(store))) } } } -fn header_of( - reader: &GeoTiffReader, -) -> Result { - Ok(RasterHeader { - width: reader.width(), - height: reader.height(), - bands: reader.band_count(), - sample_type: reader.sample_type(), - transform: reader.transform().map(|gt| gt.0), - srid: reader.srid()?, - nodata: reader.nodata(), - }) +/// Open credential `name` of the statement's tenant. +fn resolve_credential(ctx: &ArrayLoadCtx<'_>, name: &str) -> Result { + let kek = ctx + .state + .remote_sources + .credential_kek + .as_ref() + .ok_or_else(|| { + DdlError::new( + "55000", + "COPY: CREDENTIAL needs a credential key: set [credential_encryption] key_path \ + in the server config", + ) + })?; + let tenant_id = ctx.identity.tenant_id.as_u64(); + let row = ctx + .state + .credentials + .catalog() + .get_credential(tenant_id, name)? + .ok_or_else(|| { + DdlError::new("42704", format!("COPY: credential '{name}' does not exist")) + })?; + open_secret(kek, tenant_id, name, row.kind.type_name(), &row.sealed) + .map_err(|e| DdlError::new("55000", format!("COPY: credential '{name}': {e}"))) +} + +fn wrong_type(source: &str, kind: &str, applies_to: &str) -> DdlError { + DdlError::new( + "22023", + format!("COPY: '{source}': a credential of TYPE '{kind}' applies to {applies_to}"), + ) +} + +fn fetch_error(e: FetchError) -> DdlError { + DdlError::new(e.sqlstate(), format!("COPY: {e}")) } diff --git a/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/entry.rs b/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/entry.rs index 4ec8966e6..3e739c68d 100644 --- a/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/entry.rs +++ b/nodedb/src/control/server/shared/ddl/neutral/collection/copy_from/entry.rs @@ -3,9 +3,11 @@ //! Entry point: `copy_from_file`, the privilege and path checks, and the //! engine's bulk-load route, which `EngineRules::plan_bulk_load` answers. //! -//! A server-path `COPY` reads a file as the server's OS user, so only a -//! superuser may run it. The file is opened once, and every later check and -//! read uses that handle. +//! A `COPY ... FROM` reads a file as the server's OS user, or fetches a +//! remote raster from the server's network, so only a superuser may run it. +//! A server file is opened once, and every later check and read uses that +//! handle. Text formats read one server file. A raster reads one or more +//! sources: server files, `http(s)://` URLs or `s3://` objects. //! //! The result type is [`DdlResult`] / [`DdlError`] throughout. Each error //! site builds a `DdlError` directly via [`ddl_err`] so the whole call chain @@ -14,7 +16,7 @@ use nodedb_types::DatabaseId; use nodedb_sql::SqlError; -use nodedb_sql::ddl_ast::statement::{CopyFormat, CopyRasterOptions, CopyStdinFormat}; +use nodedb_sql::ddl_ast::statement::{CopyFormat, CopyRasterOptions, CopySource, CopyStdinFormat}; use crate::control::planner::context::{BulkLoadRoute, QueryContext}; use crate::control::security::identity::AuthenticatedIdentity; @@ -45,7 +47,7 @@ pub struct CopyFromOptions<'a> { pub format: Option<&'a CopyFormat>, pub delimiter: Option, pub header: bool, - /// `BANDS`, `NODATA` and `TIME`. Empty unless `format` is `GeoTiff`. + /// The raster options. Empty unless `format` is `GeoTiff`. pub raster: &'a CopyRasterOptions, } @@ -56,12 +58,12 @@ enum RowFormat { Csv(CsvOptions), } -/// Execute `COPY FROM '' [WITH (...)]`. +/// Execute `COPY FROM [WITH (...)]`. pub async fn copy_from_file( state: &SharedState, identity: &AuthenticatedIdentity, collection: &str, - path: &str, + sources: &[CopySource], options: CopyFromOptions<'_>, database_id: DatabaseId, txn_ctx: &DmlTxnCtx<'_>, @@ -75,14 +77,22 @@ pub async fn copy_from_file( require_superuser(identity, "COPY ... FROM ''")?; authorize_write_target(state, identity, database_id, collection)?; - validate_path(path, "COPY")?; + for source in sources { + if let CopySource::File(path) = source { + validate_path(path, "COPY")?; + } + } + let first = sources + .first() + .ok_or_else(|| ddl_err("42601", "COPY: FROM names no source"))?; + let shown = first.display(); // Determine format (caller has already auto-detected from extension; this is a safety net). let resolved_format = format.ok_or_else(|| { ddl_err( "42601", format!( - "COPY: cannot infer format for '{path}'; \ + "COPY: cannot infer format for '{shown}'; \ add WITH (FORMAT ndjson|json|csv|geotiff)" ), ) @@ -102,7 +112,7 @@ pub async fn copy_from_file( database_id, txn_ctx, }; - return copy_array_from_file(&ctx, collection, path, options).await; + return copy_array_from_file(&ctx, collection, sources, options).await; } let row_format = match resolved_format { @@ -119,6 +129,7 @@ pub async fn copy_from_file( )); } }; + let path = single_file(sources)?; let bytes = CopySourceFile::open(path).await?.read_all().await?; // Each row fires its collection's BEFORE, INSTEAD OF and SYNC AFTER @@ -186,7 +197,7 @@ pub(super) fn bulk_load_refusal(e: SqlError, tenant_id: nodedb_types::TenantId) async fn copy_array_from_file( ctx: &ArrayLoadCtx<'_>, array: &str, - path: &str, + sources: &[CopySource], options: CopyFromOptions<'_>, ) -> Result, DdlError> { let CopyFromOptions { @@ -198,7 +209,7 @@ async fn copy_array_from_file( match format { Some(CopyFormat::Csv) => {} Some(CopyFormat::GeoTiff) => { - let rows = copy_raster(ctx, array, path, raster).await?; + let rows = copy_raster(ctx, array, sources, raster).await?; return Ok(vec![DdlResult::Status { command: format!("COPY {rows}"), rows_affected: None, @@ -211,6 +222,7 @@ async fn copy_array_from_file( )); } } + let path = single_file(sources)?; let format = TextFormat { format: CopyStdinFormat::Csv, delimiter, @@ -233,6 +245,18 @@ async fn copy_array_from_file( }]) } +/// The one server file a text format reads. +fn single_file(sources: &[CopySource]) -> Result<&str, DdlError> { + match sources { + [CopySource::File(path)] => Ok(path), + _ => Err(ddl_err( + "0A000", + "COPY: text formats read one server file; HTTP and S3 sources and source lists \ + need FORMAT geotiff", + )), + } +} + /// Wrap a row-level error to include the row number in the message. /// /// Row-level import helpers (`import_csv`, `import_ndjson`, `import_json_array`) diff --git a/nodedb/src/control/server/shared/ddl/neutral/collection/describe_array.rs b/nodedb/src/control/server/shared/ddl/neutral/collection/describe_array.rs index aceb431cd..a36017daa 100644 --- a/nodedb/src/control/server/shared/ddl/neutral/collection/describe_array.rs +++ b/nodedb/src/control/server/shared/ddl/neutral/collection/describe_array.rs @@ -5,12 +5,15 @@ //! Arrays live in the array catalog, not in `StoredCollection`. The result //! uses the DESCRIBE COLLECTION columns: one row per dim, one row per attr, //! then `__storage`, `__tile_extents`, and `__georef` when the array has one. +//! An attr's type carries its metadata clauses in DDL form: +//! `UINT16 NODATA 0.0 SCALE 2.75e-5 OFFSET -0.2 UNITS 'reflectance'`. use nodedb_array::schema::{ArraySchema, DimType}; use nodedb_array::types::domain::DomainBound; use nodedb_types::{DatabaseId, TenantId}; use serde_json::{Map, Value as JsonValue}; +use crate::control::array_catalog::attr_meta::{meta_ast, meta_sql}; use crate::control::array_catalog::georef::{georef_ast, georef_sql}; use crate::control::server::response_shape::types::{DdlColType, ShapedRows}; use crate::control::state::SharedState; @@ -69,7 +72,12 @@ fn rows(schema: &ArraySchema) -> Vec> { } for attr in &schema.attrs { let nullable = if attr.nullable { "true" } else { "false" }; - rows.push(row(&attr.name, attr.dtype.sql_name(), nullable)); + let ty = format!( + "{}{}", + attr.dtype.sql_name(), + meta_sql(&meta_ast(&attr.meta)) + ); + rows.push(row(&attr.name, &ty, nullable)); } rows.push(row("__storage", "array", "false")); let extents: Vec = schema.tile_extents.iter().map(u64::to_string).collect(); @@ -153,10 +161,38 @@ mod tests { assert_eq!(field(&rows, "__georef"), None); } + #[test] + fn attr_rows_show_their_metadata() { + let schema = ArraySchemaBuilder::new("scene") + .dim(DimSpec::new( + "row", + DimType::Int64, + Domain::new(DomainBound::Int64(0), DomainBound::Int64(9)), + )) + .attr(AttrSpec::new("red", AttrType::UInt16, true).with_meta( + nodedb_array::schema::AttrMeta { + nodata: Some(0.0), + scale: Some(0.0000275), + offset: Some(-0.2), + units: Some("reflectance".into()), + }, + )) + .attr(AttrSpec::new("qa", AttrType::Int8, true)) + .tile_extents(vec![10]) + .build() + .unwrap(); + let rows = rows(&schema); + assert_eq!( + field(&rows, "red").as_deref(), + Some("UINT16 NODATA 0.0 SCALE 2.75e-5 OFFSET -0.2 UNITS 'reflectance'") + ); + assert_eq!(field(&rows, "qa").as_deref(), Some("INT8")); + } + #[test] fn rows_show_the_georef() { let rows = rows(&schema(Some(ArrayGeoref { - srid: nodedb_types::geometry::Srid::Utm47N, + srid: nodedb_types::geometry::Srid::new(32647).expect("srid"), x_dim: 1, y_dim: 0, transform: Some(GeoTransform([600_000.0, 10.0, 0.0, 700_000.0, 0.0, -10.0])), diff --git a/nodedb/src/control/server/shared/ddl/neutral/collection/dml/parse/statement.rs b/nodedb/src/control/server/shared/ddl/neutral/collection/dml/parse/statement.rs index 9c4e0136c..7e73d0951 100644 --- a/nodedb/src/control/server/shared/ddl/neutral/collection/dml/parse/statement.rs +++ b/nodedb/src/control/server/shared/ddl/neutral/collection/dml/parse/statement.rs @@ -36,6 +36,7 @@ pub(in crate::control::server::shared::ddl::neutral::collection) fn parse_write_ sql: &str, keyword: &str, ) -> Option> { + let crs = crate::control::crs_context::crs_context_for(state); let (sql, returning_clause) = split_returning(sql); let kw_pos = find_ascii_case_insensitive(sql, keyword)?; let after_into = sql[kw_pos + keyword.len()..].trim_start(); @@ -75,7 +76,13 @@ pub(in crate::control::server::shared::ddl::neutral::collection) fn parse_write_ // The preprocessed SQL is always INSERT INTO regardless of original keyword. Ok(Some(preprocessed)) => { return with_returning( - parse_values_form(&preprocessed.sql, "INSERT INTO ", &coll_name, coll_type), + parse_values_form( + &preprocessed.sql, + "INSERT INTO ", + &coll_name, + coll_type, + &crs, + ), returning_clause, ); } @@ -90,7 +97,7 @@ pub(in crate::control::server::shared::ddl::neutral::collection) fn parse_write_ } with_returning( - parse_values_form(sql, keyword, &coll_name, coll_type), + parse_values_form(sql, keyword, &coll_name, coll_type, &crs), returning_clause, ) } @@ -133,6 +140,7 @@ fn parse_values_form( keyword: &str, coll_name: &str, coll_type: Option, + crs: &nodedb_crs::CrsContext<'_>, ) -> Option> { let first_open = match sql.find('(') { Some(p) => p, @@ -162,7 +170,7 @@ fn parse_values_form( }; let mut rows = Vec::with_capacity(tuples.len()); for tuple in tuples { - match parse_row(&columns, tuple) { + match parse_row(&columns, tuple, crs) { Ok(row) => rows.push(row), Err(error) => return Some(Err(error)), } @@ -178,7 +186,11 @@ fn parse_values_form( } /// One `VALUES` tuple matched against the column list. -fn parse_row(columns: &[&str], tuple: &str) -> Result { +fn parse_row( + columns: &[&str], + tuple: &str, + crs: &nodedb_crs::CrsContext<'_>, +) -> Result { let values: Vec<&str> = split_values(tuple); if columns.len() != values.len() { return Err(ddl_err( @@ -201,7 +213,7 @@ fn parse_row(columns: &[&str], tuple: &str) -> Result { { doc_id = val.trim_matches('\'').to_string(); } - fields.insert(col.to_string(), parse_sql_value(val)); + fields.insert(col.to_string(), parse_sql_value(val, crs)); } if doc_id.is_empty() { doc_id = nodedb_types::id_gen::uuid_v7(); @@ -216,9 +228,15 @@ mod tests { #[test] fn values_keyword_after_unicode_identifier_preserves_original_offsets() { let sql = "INSERT INTO tffff (a) VALUES (42)"; - let parsed = parse_values_form(sql, "INSERT INTO ", "tffff", None) - .expect("statement should be recognized") - .expect("statement should parse"); + let parsed = parse_values_form( + sql, + "INSERT INTO ", + "tffff", + None, + &nodedb_crs::CrsContext::builtin_only(), + ) + .expect("statement should be recognized") + .expect("statement should parse"); assert_eq!( parsed.rows[0].fields.get("a"), Some(&nodedb_types::Value::Integer(42)) @@ -228,9 +246,15 @@ mod tests { #[test] fn every_values_row_parses_in_order() { let sql = "UPSERT INTO c (key, n) VALUES ('a', 2), ('b', 3);"; - let parsed = parse_values_form(sql, "UPSERT INTO ", "c", None) - .expect("statement should be recognized") - .expect("statement should parse"); + let parsed = parse_values_form( + sql, + "UPSERT INTO ", + "c", + None, + &nodedb_crs::CrsContext::builtin_only(), + ) + .expect("statement should be recognized") + .expect("statement should parse"); let keys: Vec<&str> = parsed.rows.iter().map(|row| row.doc_id.as_str()).collect(); assert_eq!(keys, vec!["a", "b"]); assert_eq!( diff --git a/nodedb/src/control/server/shared/ddl/neutral/collection/register.rs b/nodedb/src/control/server/shared/ddl/neutral/collection/register.rs index 1f9f2d4c2..096d7dd8b 100644 --- a/nodedb/src/control/server/shared/ddl/neutral/collection/register.rs +++ b/nodedb/src/control/server/shared/ddl/neutral/collection/register.rs @@ -412,6 +412,7 @@ pub(crate) fn build_doc_config_from_stored( vector_fields: declared_vector_fields(coll), declared_columns: declared_columns(coll), declared_key: crate::control::planner::catalog_adapter::document_declared_key(coll), + incarnation: coll.incarnation, } } @@ -438,6 +439,7 @@ async fn dispatch_register_from_stored_inner( vector_fields: config.vector_fields.clone(), declared_columns: config.declared_columns.clone(), declared_key: config.declared_key.clone(), + incarnation: config.incarnation, }, ); diff --git a/nodedb/src/control/server/shared/ddl/neutral/crdt_ops.rs b/nodedb/src/control/server/shared/ddl/neutral/crdt_ops.rs index c111bdbec..218457dea 100644 --- a/nodedb/src/control/server/shared/ddl/neutral/crdt_ops.rs +++ b/nodedb/src/control/server/shared/ddl/neutral/crdt_ops.rs @@ -213,16 +213,20 @@ pub async fn crdt_apply( // collection)`, so the policy must be handed that same key or it silently // misses a policy on a non-default database. let qualified_collection = db_qualified(database_id, collection); + let crs_set = crate::control::crs_context::crs_set_for(state); let policy = ExternalCrdtPostImagePolicy::from_identity( tenant_id, database_id, &qualified_collection, identity, "sql".into(), - &state.rls, - &audit, + crate::control::crdt_post_image_policy::PolicyServices { + rls: &state.rls, + audit: &audit, + crs_set: &crs_set, + }, ) - .with_type_guards_from(state.credentials.catalog(), database_id); + .with_type_guards_from(state, database_id); crate::control::crdt_admission::dispatch_authorized_crdt_apply_admitted( state, crate::control::crdt_admission::AuthorizedCrdtApplyAdmissionRequest { diff --git a/nodedb/src/control/server/shared/ddl/neutral/credential.rs b/nodedb/src/control/server/shared/ddl/neutral/credential.rs new file mode 100644 index 000000000..cfb555c19 --- /dev/null +++ b/nodedb/src/control/server/shared/ddl/neutral/credential.rs @@ -0,0 +1,267 @@ +// SPDX-License-Identifier: BUSL-1.1 + +//! Protocol-neutral `CREATE CREDENTIAL`, `DROP CREDENTIAL` and +//! `SHOW CREDENTIALS`. +//! +//! A credential holds the secret a remote `COPY` source authenticates with. +//! Every statement needs a superuser. `CREATE` seals the secret under the +//! `[credential_encryption]` key before it proposes the catalog entry, so +//! the metadata log, the catalog and every snapshot hold ciphertext only. +//! `SHOW CREDENTIALS` lists names, types and owners, never a secret. Each +//! statement records an audit event, and so does each refusal for lack of +//! privilege. + +use serde_json::{Map, Value as JsonValue}; + +use nodedb_sql::ddl_ast::statement::CredentialSpec; + +use crate::control::catalog_entry::CatalogEntry; +use crate::control::propose_outcome::ProposeOutcome; +use crate::control::security::audit::AuditEvent; +use crate::control::security::identity::AuthenticatedIdentity; +use crate::control::security::source_credential::{ + CredentialKind, CredentialSecret, StoredCredential, seal_secret, +}; +use crate::control::server::response_shape::types::ShapedRows; +use crate::control::state::SharedState; + +use super::super::result::{DdlError, DdlResult}; + +/// SQLSTATE of a credential name that is taken. +const DUPLICATE_OBJECT: &str = "42710"; +/// SQLSTATE of a drop of a credential that does not exist. +const UNDEFINED_OBJECT: &str = "42704"; +/// SQLSTATE of a server with no credential key. +const NOT_CONFIGURED: &str = "55000"; + +/// Handle `CREATE CREDENTIAL (TYPE ..., ...)`. +pub async fn create_credential( + state: &SharedState, + identity: &AuthenticatedIdentity, + name: &str, + spec: &CredentialSpec, +) -> Result, DdlError> { + const COMMAND: &str = "CREATE CREDENTIAL"; + require_superuser(state, identity, COMMAND)?; + let kek = state + .remote_sources + .credential_kek + .as_ref() + .ok_or_else(|| { + DdlError::new( + NOT_CONFIGURED, + "CREATE CREDENTIAL needs a credential key: set [credential_encryption] key_path \ + in the server config", + ) + })?; + let tenant_id = identity.tenant_id.as_u64(); + let catalog = state.credentials.catalog(); + if catalog.get_credential(tenant_id, name)?.is_some() { + return Err(exists(name)); + } + let (kind, secret) = secret_of(state, spec)?; + let sealed = seal_secret(kek, tenant_id, name, &secret) + .map_err(|e| DdlError::internal(format!("{COMMAND} {name}: {e}")))?; + let row = StoredCredential { + tenant_id, + name: name.to_string(), + kind, + sealed: sealed.clone(), + owner: identity.username.clone(), + created_at_ms: now_ms(), + }; + // The proposal carries the statement's audit context into the metadata + // log. `AuditCtx::for_statement` installed its redacted text. + let entry = CatalogEntry::PutCredential(Box::new(row)); + let outcome = crate::control::metadata_proposer::propose_catalog_entry_async(state, &entry) + .await + .map_err(|e| DdlError::from_error_in_context("metadata propose", &e))?; + if let ProposeOutcome::Replicated { .. } = outcome { + // A racing CREATE of the same name commits first when the log + // orders it first, and the apply keeps its row. + match catalog.get_credential(tenant_id, name)? { + Some(stored) if stored.sealed == sealed => {} + Some(_) => return Err(exists(name)), + None => { + return Err(DdlError::internal(format!( + "{COMMAND} {name}: the committed entry wrote no row" + ))); + } + } + } + audit( + state, + identity, + &format!("{COMMAND} {name} (TYPE '{}')", kind.type_name()), + ); + Ok(status(COMMAND)) +} + +/// Handle `DROP CREDENTIAL [IF EXISTS] `. +pub async fn drop_credential( + state: &SharedState, + identity: &AuthenticatedIdentity, + name: &str, + if_exists: bool, +) -> Result, DdlError> { + const COMMAND: &str = "DROP CREDENTIAL"; + require_superuser(state, identity, COMMAND)?; + let tenant_id = identity.tenant_id.as_u64(); + if state + .credentials + .catalog() + .get_credential(tenant_id, name)? + .is_none() + { + if if_exists { + return Ok(status(COMMAND)); + } + return Err(DdlError::new( + UNDEFINED_OBJECT, + format!("credential '{name}' does not exist"), + )); + } + let entry = CatalogEntry::DropCredential { + tenant_id, + name: name.to_string(), + }; + crate::control::metadata_proposer::propose_catalog_entry_async(state, &entry) + .await + .map_err(|e| DdlError::from_error_in_context("metadata propose", &e))?; + audit(state, identity, &format!("{COMMAND} {name}")); + Ok(status(COMMAND)) +} + +/// Handle `SHOW CREDENTIALS`: name, type, owner and creation time. +pub fn show_credentials( + state: &SharedState, + identity: &AuthenticatedIdentity, +) -> Result, DdlError> { + require_superuser(state, identity, "SHOW CREDENTIALS")?; + let rows = state + .credentials + .catalog() + .list_credentials(identity.tenant_id.as_u64())?; + let columns = ["name", "type", "owner", "created_at_ms"] + .map(str::to_string) + .to_vec(); + let rows = rows + .iter() + .map(|row| { + let mut out = Map::new(); + out.insert("name".into(), JsonValue::String(row.name.clone())); + out.insert( + "type".into(), + JsonValue::String(row.kind.type_name().to_string()), + ); + out.insert("owner".into(), JsonValue::String(row.owner.clone())); + out.insert( + "created_at_ms".into(), + JsonValue::String(row.created_at_ms.to_string()), + ); + out + }) + .collect(); + Ok(vec![DdlResult::Rows(ShapedRows::text_rows(columns, rows))]) +} + +/// The kind and secret `spec` describes. An S3 endpoint must pass the +/// egress policy. +fn secret_of( + state: &SharedState, + spec: &CredentialSpec, +) -> Result<(CredentialKind, CredentialSecret), DdlError> { + Ok(match spec { + CredentialSpec::S3 { + access_key_id, + secret_access_key, + region, + session_token, + endpoint, + } => { + if let Some(url) = endpoint { + state + .remote_sources + .egress() + .check_url(url) + .map_err(|e| DdlError::new("22023", format!("ENDPOINT: {e}")))?; + } + ( + CredentialKind::S3, + CredentialSecret::S3 { + access_key_id: access_key_id.expose().to_string(), + secret_access_key: secret_access_key.expose().to_string(), + region: region.clone(), + session_token: session_token.as_ref().map(|t| t.expose().to_string()), + endpoint: endpoint.clone(), + }, + ) + } + CredentialSpec::Bearer { token } => ( + CredentialKind::Bearer, + CredentialSecret::Bearer { + token: token.expose().to_string(), + }, + ), + CredentialSpec::Sas { token } => ( + CredentialKind::Sas, + CredentialSecret::Sas { + token: token.expose().to_string(), + }, + ), + }) +} + +fn exists(name: &str) -> DdlError { + DdlError::new( + DUPLICATE_OBJECT, + format!("credential '{name}' already exists; DROP CREDENTIAL {name} first"), + ) +} + +fn status(command: &str) -> Vec { + vec![DdlResult::Status { + command: command.to_string(), + rows_affected: None, + }] +} + +fn audit(state: &SharedState, identity: &AuthenticatedIdentity, detail: &str) { + state.audit_record_with_db( + AuditEvent::DdlChange, + Some(identity.tenant_id), + None, + &identity.username, + detail, + ); +} + +fn now_ms() -> u64 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| u64::try_from(d.as_millis()).unwrap_or(u64::MAX)) + .unwrap_or(0) +} + +/// Superuser gate: on denial it records `AuditEvent::PermissionDenied` and +/// returns SQLSTATE 42501. +fn require_superuser( + state: &SharedState, + identity: &AuthenticatedIdentity, + action: &str, +) -> Result<(), DdlError> { + if identity.is_superuser { + return Ok(()); + } + state.audit_record_with_db( + AuditEvent::PermissionDenied, + Some(identity.tenant_id), + None, + &identity.username, + action, + ); + Err(DdlError::new( + "42501", + format!("permission denied: {action} requires superuser"), + )) +} diff --git a/nodedb/src/control/server/shared/ddl/neutral/dsl/crdt_merge.rs b/nodedb/src/control/server/shared/ddl/neutral/dsl/crdt_merge.rs index 3a0af1d0a..3ea7f60a3 100644 --- a/nodedb/src/control/server/shared/ddl/neutral/dsl/crdt_merge.rs +++ b/nodedb/src/control/server/shared/ddl/neutral/dsl/crdt_merge.rs @@ -150,16 +150,20 @@ pub async fn crdt_merge( // collection, and the preview addresses the Data Plane by it. let qualified_collection = crate::control::planner::sql_plan_convert::convert::db_qualified(database_id, collection); + let crs_set = crate::control::crs_context::crs_set_for(state); let policy = ExternalCrdtPostImagePolicy::from_identity( tenant_id, database_id, &qualified_collection, identity, "sql".into(), - &state.rls, - &audit, + crate::control::crdt_post_image_policy::PolicyServices { + rls: &state.rls, + audit: &audit, + crs_set: &crs_set, + }, ) - .with_type_guards_from(state.credentials.catalog(), database_id); + .with_type_guards_from(state, database_id); crate::control::crdt_admission::dispatch_authorized_crdt_apply_admitted( state, crate::control::crdt_admission::AuthorizedCrdtApplyAdmissionRequest { diff --git a/nodedb/src/control/server/shared/ddl/neutral/dsl/search_fusion.rs b/nodedb/src/control/server/shared/ddl/neutral/dsl/search_fusion.rs index 8d772c93c..0c518fee1 100644 --- a/nodedb/src/control/server/shared/ddl/neutral/dsl/search_fusion.rs +++ b/nodedb/src/control/server/shared/ddl/neutral/dsl/search_fusion.rs @@ -15,7 +15,7 @@ use crate::control::state::SharedState; use crate::types::DatabaseId; use super::super::super::result::{DdlError, DdlResult}; -use super::super::graph_ops::rag_fusion::rag_fusion; +use super::super::graph_ops::rag_fusion::{RagFusionRequest, rag_fusion}; /// Run a parsed `SEARCH USING FUSION(...)` statement. pub async fn search_fusion( @@ -30,9 +30,13 @@ pub async fn search_fusion( state, identity, database_id, - collection, - params, - linearizable, + RagFusionRequest { + collection, + params, + linearizable, + // `SEARCH ... USING FUSION` reads the current graph. + temporal: nodedb_sql::temporal::TemporalScope::default(), + }, ) .await } diff --git a/nodedb/src/control/server/shared/ddl/neutral/graph_ops/algo.rs b/nodedb/src/control/server/shared/ddl/neutral/graph_ops/algo.rs index a281a0189..e28c5b1b6 100644 --- a/nodedb/src/control/server/shared/ddl/neutral/graph_ops/algo.rs +++ b/nodedb/src/control/server/shared/ddl/neutral/graph_ops/algo.rs @@ -42,6 +42,9 @@ pub struct AlgoRequest<'a> { /// The session reads linearizably: each node the algorithm reads on /// confirms its groups first. pub linearizable: bool, + /// `AS OF SYSTEM TIME` / `AS OF VALID TIME`: the algorithm runs over the + /// graph at that cut. + pub temporal: nodedb_sql::temporal::TemporalScope, } pub async fn algo( @@ -64,6 +67,7 @@ pub async fn algo( mode, personalization, linearizable, + temporal, } = request; let algorithm = resolve_algorithm(algorithm_name)?; @@ -95,6 +99,9 @@ pub async fn algo( }; let tenant_id = identity.tenant_id; + let scope = crate::control::server::graph_dispatch::graph_read_scope(state, &temporal) + .await + .map_err(|e| DdlError::from_error(&e))?; // The algorithm reads every partition of the collection's graph, on every // node that leads a data group (`graph_dispatch::whole_graph`). @@ -104,7 +111,7 @@ pub async fn algo( database_id, algorithm, params, - None, + scope, linearizable, ) .await diff --git a/nodedb/src/control/server/shared/ddl/neutral/graph_ops/dispatch.rs b/nodedb/src/control/server/shared/ddl/neutral/graph_ops/dispatch.rs index 8093d91d9..0e9c074ea 100644 --- a/nodedb/src/control/server/shared/ddl/neutral/graph_ops/dispatch.rs +++ b/nodedb/src/control/server/shared/ddl/neutral/graph_ops/dispatch.rs @@ -35,6 +35,8 @@ pub async fn dispatch_graph( dst, label, properties, + valid_from_ms, + valid_until_ms, }) => Some( edge::insert_edge( state, @@ -46,7 +48,11 @@ pub async fn dispatch_graph( dst, label, }, - properties, + edge::EdgeInsert { + properties, + valid_from_ms, + valid_until_ms, + }, txn_ctx, ) .await, @@ -85,6 +91,7 @@ pub async fn dispatch_graph( edge_labels, direction, edge_predicate, + temporal, }) => { // Every hop of the walk merges the session transaction's staged // edge writes. Idle sessions resolve to `None`. @@ -103,6 +110,7 @@ pub async fn dispatch_graph( edge_predicate, txn_id, linearizable, + temporal, }, ) .await, @@ -113,6 +121,7 @@ pub async fn dispatch_graph( node, edge_labels, direction, + temporal, }) => { // Read-your-own-writes for single-hop GRAPH reads needs the // session's active `TxnId` so the Data Plane merges this @@ -131,6 +140,7 @@ pub async fn dispatch_graph( direction, txn_id, linearizable, + temporal, }, ) .await, @@ -143,6 +153,7 @@ pub async fn dispatch_graph( max_depth, edge_labels, edge_predicate, + temporal, }) => { // Every hop of the path search merges the session transaction's // staged edge writes. Idle sessions resolve to `None`. @@ -161,6 +172,7 @@ pub async fn dispatch_graph( edge_predicate, txn_id, linearizable, + temporal, }, ) .await, @@ -179,6 +191,7 @@ pub async fn dispatch_graph( direction, mode, personalization, + temporal, }) => Some( algo::algo( state, @@ -198,18 +211,26 @@ pub async fn dispatch_graph( mode, personalization, linearizable, + temporal, }, ) .await, ), - NodedbStatement::Graph(GraphStmt::GraphRagFusion { collection, params }) => Some( + NodedbStatement::Graph(GraphStmt::GraphRagFusion { + collection, + params, + temporal, + }) => Some( rag_fusion::rag_fusion( state, identity, database_id, - collection, - params, - linearizable, + rag_fusion::RagFusionRequest { + collection, + params, + linearizable, + temporal, + }, ) .await, ), diff --git a/nodedb/src/control/server/shared/ddl/neutral/graph_ops/edge.rs b/nodedb/src/control/server/shared/ddl/neutral/graph_ops/edge.rs index d35a9ca20..7f824e216 100644 --- a/nodedb/src/control/server/shared/ddl/neutral/graph_ops/edge.rs +++ b/nodedb/src/control/server/shared/ddl/neutral/graph_ops/edge.rs @@ -31,8 +31,17 @@ fn response_affected(response: &crate::bridge::envelope::Response) -> Result`, ms since epoch. `None` takes the write's time. + pub valid_from_ms: Option, + /// `UNTIL `. `None` leaves the valid time open. + pub valid_until_ms: Option, +} + /// `GRAPH INSERT EDGE IN '' FROM '' TO '' TYPE '