diff --git a/frogpilot/third_party/flatbuffers-25.9.23.dist-info/LICENSE b/frogpilot/third_party/flatbuffers-25.9.23.dist-info/LICENSE
new file mode 100644
index 00000000..d6456956
--- /dev/null
+++ b/frogpilot/third_party/flatbuffers-25.9.23.dist-info/LICENSE
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/frogpilot/third_party/flatbuffers-25.9.23.dist-info/METADATA b/frogpilot/third_party/flatbuffers-25.9.23.dist-info/METADATA
new file mode 100644
index 00000000..519d950a
--- /dev/null
+++ b/frogpilot/third_party/flatbuffers-25.9.23.dist-info/METADATA
@@ -0,0 +1,20 @@
+Metadata-Version: 2.1
+Name: flatbuffers
+Version: 25.9.23
+Summary: The FlatBuffers serialization format for Python
+Home-page: https://google.github.io/flatbuffers/
+Author: Derek Bailey
+Author-email: derekbailey@google.com
+License: Apache 2.0
+Project-URL: Documentation, https://google.github.io/flatbuffers/
+Project-URL: Source, https://github.com/google/flatbuffers
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: Apache Software License
+Classifier: Operating System :: OS Independent
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: 2
+Classifier: Programming Language :: Python :: 3
+Classifier: Topic :: Software Development :: Libraries :: Python Modules
+License-File: ../LICENSE
+
+Python runtime library for use with the `Flatbuffers `_ serialization format.
diff --git a/frogpilot/third_party/flatbuffers-25.9.23.dist-info/RECORD b/frogpilot/third_party/flatbuffers-25.9.23.dist-info/RECORD
new file mode 100644
index 00000000..364f3392
--- /dev/null
+++ b/frogpilot/third_party/flatbuffers-25.9.23.dist-info/RECORD
@@ -0,0 +1,15 @@
+flatbuffers/__init__.py,sha256=vJZrqZOOTKdBNMa_iTKUA6WJG_c_NzKGpFXOe1Igtiw,751
+flatbuffers/_version.py,sha256=GVL6M_yJfoAklDfbfTYFV72LDbIU-YgRXL4d1yX3EVw,695
+flatbuffers/builder.py,sha256=uusDhSDKpnLLz6KR4vflC7T74VNwQew9QRkRuxGZTDg,25048
+flatbuffers/compat.py,sha256=ihBSpWDCSL-vgLSyZtcu8LX3ZI3wz9LhtqItY2GQZgg,2373
+flatbuffers/encode.py,sha256=2Or3mgWRAkJiWg-GgYasDU4zIHpQU3W06fmIhwbz5uM,1550
+flatbuffers/flexbuffers.py,sha256=yF8Wr4Lo8WJb-pj9NNaIYxLwzlHHyTroM0iO8fyDwbU,44454
+flatbuffers/number_types.py,sha256=ijO0QcJiuxlQegoBOed0v9m0DdzTZHWxpTBZUqzsWHA,3762
+flatbuffers/packer.py,sha256=LNWym8YgFRqHjcPeGpYY3inCGWH6XnbkQKtAPtFEVas,1164
+flatbuffers/table.py,sha256=ciYTmq_CzAuYpb3KAVnl75M84ieChfbyKne-dFHzwwU,4818
+flatbuffers/util.py,sha256=mRVQ1VoHp0MJMNtRTUGVzALwN4T_C-U14tMbj99py2A,1608
+flatbuffers-25.9.23.dist-info/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
+flatbuffers-25.9.23.dist-info/METADATA,sha256=tTKSAMim3fxiII0atPOplikAqxp8vZwSsKE-vUlqFcE,875
+flatbuffers-25.9.23.dist-info/WHEEL,sha256=Kh9pAotZVRFj97E15yTA4iADqXdQfIVTHcNaZTjxeGM,110
+flatbuffers-25.9.23.dist-info/top_level.txt,sha256=UXVWLA8ys6HeqTz6rfKesocUq6ln-ZL8mhZC_cq5BEc,12
+flatbuffers-25.9.23.dist-info/RECORD,,
diff --git a/frogpilot/third_party/reactivex-4.1.0.dist-info/WHEEL b/frogpilot/third_party/flatbuffers-25.9.23.dist-info/WHEEL
similarity index 53%
rename from frogpilot/third_party/reactivex-4.1.0.dist-info/WHEEL
rename to frogpilot/third_party/flatbuffers-25.9.23.dist-info/WHEEL
index 90df3495..0c3c990c 100644
--- a/frogpilot/third_party/reactivex-4.1.0.dist-info/WHEEL
+++ b/frogpilot/third_party/flatbuffers-25.9.23.dist-info/WHEEL
@@ -1,4 +1,6 @@
Wheel-Version: 1.0
-Generator: poetry-core 2.2.1
+Generator: bdist_wheel (0.45.1)
Root-Is-Purelib: true
+Tag: py2-none-any
Tag: py3-none-any
+
diff --git a/frogpilot/third_party/flatbuffers-25.9.23.dist-info/top_level.txt b/frogpilot/third_party/flatbuffers-25.9.23.dist-info/top_level.txt
new file mode 100644
index 00000000..adf11d69
--- /dev/null
+++ b/frogpilot/third_party/flatbuffers-25.9.23.dist-info/top_level.txt
@@ -0,0 +1 @@
+flatbuffers
diff --git a/frogpilot/third_party/flatbuffers/__init__.py b/frogpilot/third_party/flatbuffers/__init__.py
new file mode 100644
index 00000000..55ef9377
--- /dev/null
+++ b/frogpilot/third_party/flatbuffers/__init__.py
@@ -0,0 +1,19 @@
+# Copyright 2014 Google Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from . import util
+from ._version import __version__
+from .builder import Builder
+from .compat import range_func as compat_range
+from .table import Table
diff --git a/frogpilot/third_party/flatbuffers/_version.py b/frogpilot/third_party/flatbuffers/_version.py
new file mode 100644
index 00000000..368e6d08
--- /dev/null
+++ b/frogpilot/third_party/flatbuffers/_version.py
@@ -0,0 +1,17 @@
+# Copyright 2019 Google Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Placeholder, to be updated during the release process
+# by the setup.py
+__version__ = "25.9.23"
diff --git a/frogpilot/third_party/flatbuffers/builder.py b/frogpilot/third_party/flatbuffers/builder.py
new file mode 100644
index 00000000..71d0eba7
--- /dev/null
+++ b/frogpilot/third_party/flatbuffers/builder.py
@@ -0,0 +1,870 @@
+# Copyright 2014 Google Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import warnings
+
+from . import compat
+from . import encode
+from . import number_types as N
+from . import packer
+from .compat import memoryview_type
+from .compat import NumpyRequiredForThisFeature, import_numpy
+from .compat import range_func
+from .number_types import (SOffsetTFlags, UOffsetTFlags, VOffsetTFlags)
+
+np = import_numpy()
+## @file
+## @addtogroup flatbuffers_python_api
+## @{
+
+
+## @cond FLATBUFFERS_INTERNAL
+class OffsetArithmeticError(RuntimeError):
+ """Error caused by an Offset arithmetic error.
+
+ Probably caused by bad writing of fields. This is considered an unreachable
+ situation in normal circumstances.
+ """
+
+ pass
+
+
+class IsNotNestedError(RuntimeError):
+ """Error caused by using a Builder to write Object data when not inside
+
+ an Object.
+ """
+
+ pass
+
+
+class IsNestedError(RuntimeError):
+ """Error caused by using a Builder to begin an Object when an Object is
+
+ already being built.
+ """
+
+ pass
+
+
+class StructIsNotInlineError(RuntimeError):
+ """Error caused by using a Builder to write a Struct at a location that
+
+ is not the current Offset.
+ """
+
+ pass
+
+
+class BuilderSizeError(RuntimeError):
+ """Error caused by causing a Builder to exceed the hardcoded limit of 2
+
+ gigabytes.
+ """
+
+ pass
+
+
+class BuilderNotFinishedError(RuntimeError):
+ """Error caused by not calling `Finish` before calling `Output`."""
+
+ pass
+
+
+class EndVectorLengthMismatched(RuntimeError):
+ """The number of elements passed to EndVector does not match the number
+
+ specified in StartVector.
+ """
+
+ pass
+
+
+# VtableMetadataFields is the count of metadata fields in each vtable.
+VtableMetadataFields = 2
+## @endcond
+
+
+class Builder(object):
+ """A Builder is used to construct one or more FlatBuffers.
+
+ Typically, Builder objects will be used from code generated by the `flatc`
+ compiler.
+
+ A Builder constructs byte buffers in a last-first manner for simplicity and
+ performance during reading.
+
+ Internally, a Builder is a state machine for creating FlatBuffer objects.
+
+ It holds the following internal state:
+ - Bytes: an array of bytes.
+ - current_vtable: a list of integers.
+ - vtables: a hash of vtable entries.
+
+ Attributes:
+ Bytes: The internal `bytearray` for the Builder.
+ finished: A boolean determining if the Builder has been finalized.
+ """
+
+ ## @cond FLATBUFFERS_INTENRAL
+ __slots__ = (
+ "Bytes",
+ "current_vtable",
+ "head",
+ "minalign",
+ "objectEnd",
+ "vtables",
+ "nested",
+ "forceDefaults",
+ "finished",
+ "vectorNumElems",
+ "sharedStrings",
+ )
+
+ """Maximum buffer size constant, in bytes.
+
+ Builder will never allow it's buffer grow over this size.
+ Currently equals 2Gb.
+ """
+ MAX_BUFFER_SIZE = 2**31
+ ## @endcond
+
+ def __init__(self, initialSize=1024):
+ """Initializes a Builder of size `initial_size`.
+
+ The internal buffer is grown as needed.
+ """
+
+ if not (0 <= initialSize <= Builder.MAX_BUFFER_SIZE):
+ msg = "flatbuffers: Cannot create Builder larger than 2 gigabytes."
+ raise BuilderSizeError(msg)
+
+ self.Bytes = bytearray(initialSize)
+ ## @cond FLATBUFFERS_INTERNAL
+ self.current_vtable = None
+ self.head = UOffsetTFlags.py_type(initialSize)
+ self.minalign = 1
+ self.objectEnd = None
+ self.vtables = {}
+ self.nested = False
+ self.forceDefaults = False
+ self.sharedStrings = {}
+ ## @endcond
+ self.finished = False
+
+ def Clear(self) -> None:
+ ## @cond FLATBUFFERS_INTERNAL
+ self.current_vtable = None
+ self.head = UOffsetTFlags.py_type(len(self.Bytes))
+ self.minalign = 1
+ self.objectEnd = None
+ self.vtables = {}
+ self.nested = False
+ self.forceDefaults = False
+ self.sharedStrings = {}
+ self.vectorNumElems = None
+ ## @endcond
+ self.finished = False
+
+ def Output(self):
+ """Return the portion of the buffer that has been used for writing data.
+
+ This is the typical way to access the FlatBuffer data inside the
+ builder. If you try to access `Builder.Bytes` directly, you would need
+ to manually index it with `Head()`, since the buffer is constructed
+ backwards.
+
+ It raises BuilderNotFinishedError if the buffer has not been finished
+ with `Finish`.
+ """
+
+ if not self.finished:
+ raise BuilderNotFinishedError()
+
+ return self.Bytes[self.Head() :]
+
+ ## @cond FLATBUFFERS_INTERNAL
+ def StartObject(self, numfields):
+ """StartObject initializes bookkeeping for writing a new object."""
+
+ self.assertNotNested()
+
+ # use 32-bit offsets so that arithmetic doesn't overflow.
+ self.current_vtable = [0 for _ in range_func(numfields)]
+ self.objectEnd = self.Offset()
+ self.nested = True
+
+ def WriteVtable(self):
+ """WriteVtable serializes the vtable for the current object, if needed.
+
+ Before writing out the vtable, this checks pre-existing vtables for
+ equality to this one. If an equal vtable is found, point the object to
+ the existing vtable and return.
+
+ Because vtable values are sensitive to alignment of object data, not
+ all logically-equal vtables will be deduplicated.
+
+ A vtable has the following format:
+
+
+ * N, where N is the number of fields
+ in the schema for this type. Includes deprecated fields.
+ Thus, a vtable is made of 2 + N elements, each VOffsetT bytes wide.
+
+ An object has the following format:
+
+ +
+ """
+
+ # Prepend a zero scalar to the object. Later in this function we'll
+ # write an offset here that points to the object's vtable:
+ self.PrependSOffsetTRelative(0)
+
+ objectOffset = self.Offset()
+
+ vtKey = []
+ trim = True
+ for elem in reversed(self.current_vtable):
+ if elem == 0:
+ if trim:
+ continue
+ else:
+ elem = objectOffset - elem
+ trim = False
+
+ vtKey.append(elem)
+
+ vtKey = tuple(vtKey)
+ vt2Offset = self.vtables.get(vtKey)
+ if vt2Offset is None:
+ # Did not find a vtable, so write this one to the buffer.
+
+ # Write out the current vtable in reverse , because
+ # serialization occurs in last-first order:
+ i = len(self.current_vtable) - 1
+ trailing = 0
+ trim = True
+ while i >= 0:
+ off = 0
+ elem = self.current_vtable[i]
+ i -= 1
+
+ if elem == 0:
+ if trim:
+ trailing += 1
+ continue
+ else:
+ # Forward reference to field;
+ # use 32bit number to ensure no overflow:
+ off = objectOffset - elem
+ trim = False
+
+ self.PrependVOffsetT(off)
+
+ # The two metadata fields are written last.
+
+ # First, store the object bytesize:
+ objectSize = UOffsetTFlags.py_type(objectOffset - self.objectEnd)
+ self.PrependVOffsetT(VOffsetTFlags.py_type(objectSize))
+
+ # Second, store the vtable bytesize:
+ vBytes = len(self.current_vtable) - trailing + VtableMetadataFields
+ vBytes *= N.VOffsetTFlags.bytewidth
+ self.PrependVOffsetT(VOffsetTFlags.py_type(vBytes))
+
+ # Next, write the offset to the new vtable in the
+ # already-allocated SOffsetT at the beginning of this object:
+ objectStart = SOffsetTFlags.py_type(len(self.Bytes) - objectOffset)
+ encode.Write(
+ packer.soffset,
+ self.Bytes,
+ objectStart,
+ SOffsetTFlags.py_type(self.Offset() - objectOffset),
+ )
+
+ # Finally, store this vtable in memory for future
+ # deduplication:
+ self.vtables[vtKey] = self.Offset()
+ else:
+ # Found a duplicate vtable.
+ objectStart = SOffsetTFlags.py_type(len(self.Bytes) - objectOffset)
+ self.head = UOffsetTFlags.py_type(objectStart)
+
+ # Write the offset to the found vtable in the
+ # already-allocated SOffsetT at the beginning of this object:
+ encode.Write(
+ packer.soffset,
+ self.Bytes,
+ self.Head(),
+ SOffsetTFlags.py_type(vt2Offset - objectOffset),
+ )
+
+ self.current_vtable = None
+ return objectOffset
+
+ def EndObject(self):
+ """EndObject writes data necessary to finish object construction."""
+ self.assertNested()
+ self.nested = False
+ return self.WriteVtable()
+
+ def growByteBuffer(self):
+ """Doubles the size of the byteslice, and copies the old data towards
+
+ the end of the new buffer (since we build the buffer backwards).
+ """
+ if len(self.Bytes) == Builder.MAX_BUFFER_SIZE:
+ msg = "flatbuffers: cannot grow buffer beyond 2 gigabytes"
+ raise BuilderSizeError(msg)
+
+ newSize = min(len(self.Bytes) * 2, Builder.MAX_BUFFER_SIZE)
+ if newSize == 0:
+ newSize = 1
+ bytes2 = bytearray(newSize)
+ bytes2[newSize - len(self.Bytes) :] = self.Bytes
+ self.Bytes = bytes2
+
+ ## @endcond
+
+ def Head(self):
+ """Get the start of useful data in the underlying byte buffer.
+
+ Note: unlike other functions, this value is interpreted as from the
+ left.
+ """
+ ## @cond FLATBUFFERS_INTERNAL
+ return self.head
+ ## @endcond
+
+ ## @cond FLATBUFFERS_INTERNAL
+ def Offset(self):
+ """Offset relative to the end of the buffer."""
+ return UOffsetTFlags.py_type(len(self.Bytes) - self.Head())
+
+ def Pad(self, n):
+ """Pad places zeros at the current offset."""
+ for i in range_func(n):
+ self.Place(0, N.Uint8Flags)
+
+ def Prep(self, size, additionalBytes):
+ """Prep prepares to write an element of `size` after `additional_bytes`
+
+ have been written, e.g. if you write a string, you need to align
+ such the int length field is aligned to SizeInt32, and the string
+ data follows it directly.
+ If all you need to do is align, `additionalBytes` will be 0.
+ """
+
+ # Track the biggest thing we've ever aligned to.
+ if size > self.minalign:
+ self.minalign = size
+
+ # Find the amount of alignment needed such that `size` is properly
+ # aligned after `additionalBytes`:
+ alignSize = (~(len(self.Bytes) - self.Head() + additionalBytes)) + 1
+ alignSize &= size - 1
+
+ # Reallocate the buffer if needed:
+ while self.Head() < alignSize + size + additionalBytes:
+ oldBufSize = len(self.Bytes)
+ self.growByteBuffer()
+ updated_head = self.head + len(self.Bytes) - oldBufSize
+ self.head = UOffsetTFlags.py_type(updated_head)
+ self.Pad(alignSize)
+
+ def PrependSOffsetTRelative(self, off):
+ """PrependSOffsetTRelative prepends an SOffsetT, relative to where it
+
+ will be written.
+ """
+
+ # Ensure alignment is already done:
+ self.Prep(N.SOffsetTFlags.bytewidth, 0)
+ if not (off <= self.Offset()):
+ msg = "flatbuffers: Offset arithmetic error."
+ raise OffsetArithmeticError(msg)
+ off2 = self.Offset() - off + N.SOffsetTFlags.bytewidth
+ self.PlaceSOffsetT(off2)
+
+ ## @endcond
+
+ def PrependUOffsetTRelative(self, off):
+ """Prepends an unsigned offset into vector data, relative to where it
+
+ will be written.
+ """
+
+ # Ensure alignment is already done:
+ self.Prep(N.UOffsetTFlags.bytewidth, 0)
+ if not (off <= self.Offset()):
+ msg = "flatbuffers: Offset arithmetic error."
+ raise OffsetArithmeticError(msg)
+ off2 = self.Offset() - off + N.UOffsetTFlags.bytewidth
+ self.PlaceUOffsetT(off2)
+
+ ## @cond FLATBUFFERS_INTERNAL
+ def StartVector(self, elemSize, numElems, alignment):
+ """StartVector initializes bookkeeping for writing a new vector.
+
+ A vector has the following format:
+ -
+ - +, where T is the type of elements of this vector.
+ """
+
+ self.assertNotNested()
+ self.nested = True
+ self.vectorNumElems = numElems
+ self.Prep(N.Uint32Flags.bytewidth, elemSize * numElems)
+ self.Prep(alignment, elemSize * numElems) # In case alignment > int.
+ return self.Offset()
+
+ ## @endcond
+
+ def EndVector(self, numElems=None):
+ """EndVector writes data necessary to finish vector construction."""
+
+ self.assertNested()
+ ## @cond FLATBUFFERS_INTERNAL
+ self.nested = False
+ ## @endcond
+
+ if numElems:
+ warnings.warn("numElems is deprecated.", DeprecationWarning, stacklevel=2)
+ if numElems != self.vectorNumElems:
+ raise EndVectorLengthMismatched()
+
+ # we already made space for this, so write without PrependUint32
+ self.PlaceUOffsetT(self.vectorNumElems)
+ self.vectorNumElems = None
+ return self.Offset()
+
+ def CreateSharedString(self, s, encoding="utf-8", errors="strict"):
+ """CreateSharedString checks if the string is already written to the buffer
+
+ before calling CreateString.
+ """
+
+ if s in self.sharedStrings:
+ return self.sharedStrings[s]
+
+ off = self.CreateString(s, encoding, errors)
+ self.sharedStrings[s] = off
+
+ return off
+
+ def CreateString(self, s, encoding="utf-8", errors="strict"):
+ """CreateString writes a null-terminated byte string as a vector."""
+
+ self.assertNotNested()
+ ## @cond FLATBUFFERS_INTERNAL
+ self.nested = True
+ ## @endcond
+
+ if isinstance(s, compat.string_types):
+ x = s.encode(encoding, errors)
+ elif isinstance(s, compat.binary_types):
+ x = s
+ else:
+ raise TypeError("non-string passed to CreateString")
+
+ self.Prep(N.UOffsetTFlags.bytewidth, (len(x) + 1) * N.Uint8Flags.bytewidth)
+ self.Place(0, N.Uint8Flags)
+
+ l = UOffsetTFlags.py_type(len(s))
+ ## @cond FLATBUFFERS_INTERNAL
+ self.head = UOffsetTFlags.py_type(self.Head() - l)
+ ## @endcond
+ self.Bytes[self.Head() : self.Head() + l] = x
+
+ self.vectorNumElems = len(x)
+ return self.EndVector()
+
+ def CreateByteVector(self, x):
+ """CreateString writes a byte vector."""
+
+ self.assertNotNested()
+ ## @cond FLATBUFFERS_INTERNAL
+ self.nested = True
+ ## @endcond
+
+ if not isinstance(x, compat.binary_types):
+ raise TypeError("non-byte vector passed to CreateByteVector")
+
+ self.Prep(N.UOffsetTFlags.bytewidth, len(x) * N.Uint8Flags.bytewidth)
+
+ l = UOffsetTFlags.py_type(len(x))
+ ## @cond FLATBUFFERS_INTERNAL
+ self.head = UOffsetTFlags.py_type(self.Head() - l)
+ ## @endcond
+ self.Bytes[self.Head() : self.Head() + l] = x
+
+ self.vectorNumElems = len(x)
+ return self.EndVector()
+
+ def CreateNumpyVector(self, x):
+ """CreateNumpyVector writes a numpy array into the buffer."""
+
+ if np is None:
+ # Numpy is required for this feature
+ raise NumpyRequiredForThisFeature("Numpy was not found.")
+
+ if not isinstance(x, np.ndarray):
+ raise TypeError("non-numpy-ndarray passed to CreateNumpyVector")
+
+ if x.dtype.kind not in ["b", "i", "u", "f"]:
+ raise TypeError("numpy-ndarray holds elements of unsupported datatype")
+
+ if x.ndim > 1:
+ raise TypeError("multidimensional-ndarray passed to CreateNumpyVector")
+
+ self.StartVector(x.itemsize, x.size, x.dtype.alignment)
+
+ # Ensure little endian byte ordering
+ if x.dtype.str[0] == "<":
+ x_lend = x
+ else:
+ x_lend = x.byteswap(inplace=False)
+
+ # Calculate total length
+ l = UOffsetTFlags.py_type(x_lend.itemsize * x_lend.size)
+ ## @cond FLATBUFFERS_INTERNAL
+ self.head = UOffsetTFlags.py_type(self.Head() - l)
+ ## @endcond
+
+ # tobytes ensures c_contiguous ordering
+ self.Bytes[self.Head() : self.Head() + l] = x_lend.tobytes(order="C")
+
+ self.vectorNumElems = x.size
+ return self.EndVector()
+
+ ## @cond FLATBUFFERS_INTERNAL
+ def assertNested(self):
+ """Check that we are in the process of building an object."""
+
+ if not self.nested:
+ raise IsNotNestedError()
+
+ def assertNotNested(self):
+ """Check that no other objects are being built while making this object.
+
+ If not, raise an exception.
+ """
+
+ if self.nested:
+ raise IsNestedError()
+
+ def assertStructIsInline(self, obj):
+ """Structs are always stored inline, so need to be created right
+
+ where they are used. You'll get this error if you created it
+ elsewhere.
+ """
+
+ N.enforce_number(obj, N.UOffsetTFlags)
+ if obj != self.Offset():
+ msg = (
+ "flatbuffers: Tried to write a Struct at an Offset that "
+ "is different from the current Offset of the Builder."
+ )
+ raise StructIsNotInlineError(msg)
+
+ def Slot(self, slotnum):
+ """Slot sets the vtable key `voffset` to the current location in the
+
+ buffer.
+ """
+ self.assertNested()
+ self.current_vtable[slotnum] = self.Offset()
+
+ ## @endcond
+
+ def __Finish(self, rootTable, sizePrefix, file_identifier=None):
+ """Finish finalizes a buffer, pointing to the given `rootTable`."""
+ N.enforce_number(rootTable, N.UOffsetTFlags)
+
+ prepSize = N.UOffsetTFlags.bytewidth
+ if file_identifier is not None:
+ prepSize += N.Int32Flags.bytewidth
+ if sizePrefix:
+ prepSize += N.Int32Flags.bytewidth
+ self.Prep(self.minalign, prepSize)
+
+ if file_identifier is not None:
+ self.Prep(N.UOffsetTFlags.bytewidth, encode.FILE_IDENTIFIER_LENGTH)
+
+ # Convert bytes object file_identifier to an array of 4 8-bit integers,
+ # and use big-endian to enforce size compliance.
+ # https://docs.python.org/2/library/struct.html#format-characters
+ file_identifier = N.struct.unpack(">BBBB", file_identifier)
+ for i in range(encode.FILE_IDENTIFIER_LENGTH - 1, -1, -1):
+ # Place the bytes of the file_identifer in reverse order:
+ self.Place(file_identifier[i], N.Uint8Flags)
+
+ self.PrependUOffsetTRelative(rootTable)
+ if sizePrefix:
+ size = len(self.Bytes) - self.Head()
+ N.enforce_number(size, N.Int32Flags)
+ self.PrependInt32(size)
+ self.finished = True
+ return self.Head()
+
+ def Finish(self, rootTable, file_identifier=None):
+ """Finish finalizes a buffer, pointing to the given `rootTable`."""
+ return self.__Finish(rootTable, False, file_identifier=file_identifier)
+
+ def FinishSizePrefixed(self, rootTable, file_identifier=None):
+ """Finish finalizes a buffer, pointing to the given `rootTable`,
+
+ with the size prefixed.
+ """
+ return self.__Finish(rootTable, True, file_identifier=file_identifier)
+
+ ## @cond FLATBUFFERS_INTERNAL
+ def Prepend(self, flags, off):
+ self.Prep(flags.bytewidth, 0)
+ self.Place(off, flags)
+
+ def PrependSlot(self, flags, o, x, d):
+ if x is not None:
+ N.enforce_number(x, flags)
+ if d is not None:
+ N.enforce_number(d, flags)
+ if x != d or (self.forceDefaults and d is not None):
+ self.Prepend(flags, x)
+ self.Slot(o)
+
+ def PrependBoolSlot(self, *args):
+ self.PrependSlot(N.BoolFlags, *args)
+
+ def PrependByteSlot(self, *args):
+ self.PrependSlot(N.Uint8Flags, *args)
+
+ def PrependUint8Slot(self, *args):
+ self.PrependSlot(N.Uint8Flags, *args)
+
+ def PrependUint16Slot(self, *args):
+ self.PrependSlot(N.Uint16Flags, *args)
+
+ def PrependUint32Slot(self, *args):
+ self.PrependSlot(N.Uint32Flags, *args)
+
+ def PrependUint64Slot(self, *args):
+ self.PrependSlot(N.Uint64Flags, *args)
+
+ def PrependInt8Slot(self, *args):
+ self.PrependSlot(N.Int8Flags, *args)
+
+ def PrependInt16Slot(self, *args):
+ self.PrependSlot(N.Int16Flags, *args)
+
+ def PrependInt32Slot(self, *args):
+ self.PrependSlot(N.Int32Flags, *args)
+
+ def PrependInt64Slot(self, *args):
+ self.PrependSlot(N.Int64Flags, *args)
+
+ def PrependFloat32Slot(self, *args):
+ self.PrependSlot(N.Float32Flags, *args)
+
+ def PrependFloat64Slot(self, *args):
+ self.PrependSlot(N.Float64Flags, *args)
+
+ def PrependUOffsetTRelativeSlot(self, o, x, d):
+ """PrependUOffsetTRelativeSlot prepends an UOffsetT onto the object at
+
+ vtable slot `o`. If value `x` equals default `d`, then the slot will
+ be set to zero and no other data will be written.
+ """
+
+ if x != d or self.forceDefaults:
+ self.PrependUOffsetTRelative(x)
+ self.Slot(o)
+
+ def PrependStructSlot(self, v, x, d):
+ """PrependStructSlot prepends a struct onto the object at vtable slot `o`.
+
+ Structs are stored inline, so nothing additional is being added. In
+ generated code, `d` is always 0.
+ """
+
+ N.enforce_number(d, N.UOffsetTFlags)
+ if x != d:
+ self.assertStructIsInline(x)
+ self.Slot(v)
+
+ ## @endcond
+
+ def PrependBool(self, x):
+ """Prepend a `bool` to the Builder buffer.
+
+ Note: aligns and checks for space.
+ """
+ self.Prepend(N.BoolFlags, x)
+
+ def PrependByte(self, x):
+ """Prepend a `byte` to the Builder buffer.
+
+ Note: aligns and checks for space.
+ """
+ self.Prepend(N.Uint8Flags, x)
+
+ def PrependUint8(self, x):
+ """Prepend an `uint8` to the Builder buffer.
+
+ Note: aligns and checks for space.
+ """
+ self.Prepend(N.Uint8Flags, x)
+
+ def PrependUint16(self, x):
+ """Prepend an `uint16` to the Builder buffer.
+
+ Note: aligns and checks for space.
+ """
+ self.Prepend(N.Uint16Flags, x)
+
+ def PrependUint32(self, x):
+ """Prepend an `uint32` to the Builder buffer.
+
+ Note: aligns and checks for space.
+ """
+ self.Prepend(N.Uint32Flags, x)
+
+ def PrependUint64(self, x):
+ """Prepend an `uint64` to the Builder buffer.
+
+ Note: aligns and checks for space.
+ """
+ self.Prepend(N.Uint64Flags, x)
+
+ def PrependInt8(self, x):
+ """Prepend an `int8` to the Builder buffer.
+
+ Note: aligns and checks for space.
+ """
+ self.Prepend(N.Int8Flags, x)
+
+ def PrependInt16(self, x):
+ """Prepend an `int16` to the Builder buffer.
+
+ Note: aligns and checks for space.
+ """
+ self.Prepend(N.Int16Flags, x)
+
+ def PrependInt32(self, x):
+ """Prepend an `int32` to the Builder buffer.
+
+ Note: aligns and checks for space.
+ """
+ self.Prepend(N.Int32Flags, x)
+
+ def PrependInt64(self, x):
+ """Prepend an `int64` to the Builder buffer.
+
+ Note: aligns and checks for space.
+ """
+ self.Prepend(N.Int64Flags, x)
+
+ def PrependFloat32(self, x):
+ """Prepend a `float32` to the Builder buffer.
+
+ Note: aligns and checks for space.
+ """
+ self.Prepend(N.Float32Flags, x)
+
+ def PrependFloat64(self, x):
+ """Prepend a `float64` to the Builder buffer.
+
+ Note: aligns and checks for space.
+ """
+ self.Prepend(N.Float64Flags, x)
+
+ def ForceDefaults(self, forceDefaults):
+ """In order to save space, fields that are set to their default value
+
+ don't get serialized into the buffer. Forcing defaults provides a
+ way to manually disable this optimization. When set to `True`, will
+ always serialize default values.
+ """
+ self.forceDefaults = forceDefaults
+
+ ##############################################################
+
+ ## @cond FLATBUFFERS_INTERNAL
+ def PrependVOffsetT(self, x):
+ self.Prepend(N.VOffsetTFlags, x)
+
+ def Place(self, x, flags):
+ """Place prepends a value specified by `flags` to the Builder,
+
+ without checking for available space.
+ """
+
+ N.enforce_number(x, flags)
+ self.head = self.head - flags.bytewidth
+ encode.Write(flags.packer_type, self.Bytes, self.Head(), x)
+
+ def PlaceVOffsetT(self, x):
+ """PlaceVOffsetT prepends a VOffsetT to the Builder, without checking
+
+ for space.
+ """
+ N.enforce_number(x, N.VOffsetTFlags)
+ self.head = self.head - N.VOffsetTFlags.bytewidth
+ encode.Write(packer.voffset, self.Bytes, self.Head(), x)
+
+ def PlaceSOffsetT(self, x):
+ """PlaceSOffsetT prepends a SOffsetT to the Builder, without checking
+
+ for space.
+ """
+ N.enforce_number(x, N.SOffsetTFlags)
+ self.head = self.head - N.SOffsetTFlags.bytewidth
+ encode.Write(packer.soffset, self.Bytes, self.Head(), x)
+
+ def PlaceUOffsetT(self, x):
+ """PlaceUOffsetT prepends a UOffsetT to the Builder, without checking
+
+ for space.
+ """
+ N.enforce_number(x, N.UOffsetTFlags)
+ self.head = self.head - N.UOffsetTFlags.bytewidth
+ encode.Write(packer.uoffset, self.Bytes, self.Head(), x)
+
+ ## @endcond
+
+
+## @cond FLATBUFFERS_INTERNAL
+def vtableEqual(a, objectStart, b):
+ """vtableEqual compares an unwritten vtable to a written vtable."""
+
+ N.enforce_number(objectStart, N.UOffsetTFlags)
+
+ if len(a) * N.VOffsetTFlags.bytewidth != len(b):
+ return False
+
+ for i, elem in enumerate(a):
+ x = encode.Get(packer.voffset, b, i * N.VOffsetTFlags.bytewidth)
+
+ # Skip vtable entries that indicate a default value.
+ if x == 0 and elem == 0:
+ pass
+ else:
+ y = objectStart - elem
+ if x != y:
+ return False
+ return True
+
+
+## @endcond
+## @}
diff --git a/frogpilot/third_party/flatbuffers/compat.py b/frogpilot/third_party/flatbuffers/compat.py
new file mode 100644
index 00000000..5668ad70
--- /dev/null
+++ b/frogpilot/third_party/flatbuffers/compat.py
@@ -0,0 +1,91 @@
+# Copyright 2016 Google Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""A tiny version of `six` to help with backwards compability.
+
+Also includes compatibility helpers for numpy.
+"""
+
+import sys
+
+PY2 = sys.version_info[0] == 2
+PY26 = sys.version_info[0:2] == (2, 6)
+PY27 = sys.version_info[0:2] == (2, 7)
+PY275 = sys.version_info[0:3] >= (2, 7, 5)
+PY3 = sys.version_info[0] == 3
+PY34 = sys.version_info[0:2] >= (3, 4)
+
+if PY3:
+ import importlib.machinery
+
+ string_types = (str,)
+ binary_types = (bytes, bytearray)
+ range_func = range
+ memoryview_type = memoryview
+ struct_bool_decl = "?"
+else:
+ import imp
+
+ string_types = (unicode,)
+ if PY26 or PY27:
+ binary_types = (str, bytearray)
+ else:
+ binary_types = (str,)
+ range_func = xrange
+ if PY26 or (PY27 and not PY275):
+ memoryview_type = buffer
+ struct_bool_decl = "= 0
+
+ if value < (1 << 8):
+ return BitWidth.W8
+ elif value < (1 << 16):
+ return BitWidth.W16
+ elif value < (1 << 32):
+ return BitWidth.W32
+ elif value < (1 << 64):
+ return BitWidth.W64
+ else:
+ raise ValueError('value is too big to encode: %s' % value)
+
+ @staticmethod
+ def I(value):
+ """Returns the minimum `BitWidth` to encode signed integer value."""
+ # -2^(n-1) <= value < 2^(n-1)
+ # -2^n <= 2 * value < 2^n
+ # 2 * value < 2^n, when value >= 0 or 2 * (-value) <= 2^n, when value < 0
+ # 2 * value < 2^n, when value >= 0 or 2 * (-value) - 1 < 2^n, when value < 0
+ #
+ # if value >= 0:
+ # return BitWidth.U(2 * value)
+ # else:
+ # return BitWidth.U(2 * (-value) - 1) # ~x = -x - 1
+ value *= 2
+ return BitWidth.U(value if value >= 0 else ~value)
+
+ @staticmethod
+ def F(value):
+ """Returns the `BitWidth` to encode floating point value."""
+ if struct.unpack(' 0:
+ i = first
+ step = count // 2
+ i += step
+ if pred(values[i], value):
+ i += 1
+ first = i
+ count -= step + 1
+ else:
+ count = step
+ return first
+
+
+# https://en.cppreference.com/w/cpp/algorithm/binary_search
+def _BinarySearch(values, value, pred=lambda x, y: x < y):
+ """Implementation of C++ std::binary_search() algorithm."""
+ index = _LowerBound(values, value, pred)
+ if index != len(values) and not pred(value, values[index]):
+ return index
+ return -1
+
+
+class Type(enum.IntEnum):
+ """Supported types of encoded data.
+
+ These are used as the upper 6 bits of a type field to indicate the actual
+ type.
+ """
+
+ NULL = 0
+ INT = 1
+ UINT = 2
+ FLOAT = 3
+ # Types above stored inline, types below store an offset.
+ KEY = 4
+ STRING = 5
+ INDIRECT_INT = 6
+ INDIRECT_UINT = 7
+ INDIRECT_FLOAT = 8
+ MAP = 9
+ VECTOR = 10 # Untyped.
+
+ VECTOR_INT = 11 # Typed any size (stores no type table).
+ VECTOR_UINT = 12
+ VECTOR_FLOAT = 13
+ VECTOR_KEY = 14
+ # DEPRECATED, use VECTOR or VECTOR_KEY instead.
+ # Read test.cpp/FlexBuffersDeprecatedTest() for details on why.
+ VECTOR_STRING_DEPRECATED = 15
+
+ VECTOR_INT2 = 16 # Typed tuple (no type table, no size field).
+ VECTOR_UINT2 = 17
+ VECTOR_FLOAT2 = 18
+ VECTOR_INT3 = 19 # Typed triple (no type table, no size field).
+ VECTOR_UINT3 = 20
+ VECTOR_FLOAT3 = 21
+ VECTOR_INT4 = 22 # Typed quad (no type table, no size field).
+ VECTOR_UINT4 = 23
+ VECTOR_FLOAT4 = 24
+
+ BLOB = 25
+ BOOL = 26
+ VECTOR_BOOL = 36 # To do the same type of conversion of type to vector type
+
+ @staticmethod
+ def Pack(type_, bit_width):
+ return (int(type_) << 2) | bit_width
+
+ @staticmethod
+ def Unpack(packed_type):
+ return 1 << (packed_type & 0b11), Type(packed_type >> 2)
+
+ @staticmethod
+ def IsInline(type_):
+ return type_ <= Type.FLOAT or type_ == Type.BOOL
+
+ @staticmethod
+ def IsTypedVector(type_):
+ return (
+ Type.VECTOR_INT <= type_ <= Type.VECTOR_STRING_DEPRECATED
+ or type_ == Type.VECTOR_BOOL
+ )
+
+ @staticmethod
+ def IsTypedVectorElementType(type_):
+ return Type.INT <= type_ <= Type.STRING or type_ == Type.BOOL
+
+ @staticmethod
+ def ToTypedVectorElementType(type_):
+ if not Type.IsTypedVector(type_):
+ raise ValueError('must be typed vector type')
+
+ return Type(type_ - Type.VECTOR_INT + Type.INT)
+
+ @staticmethod
+ def IsFixedTypedVector(type_):
+ return Type.VECTOR_INT2 <= type_ <= Type.VECTOR_FLOAT4
+
+ @staticmethod
+ def IsFixedTypedVectorElementType(type_):
+ return Type.INT <= type_ <= Type.FLOAT
+
+ @staticmethod
+ def ToFixedTypedVectorElementType(type_):
+ if not Type.IsFixedTypedVector(type_):
+ raise ValueError('must be fixed typed vector type')
+
+ # 3 types each, starting from length 2.
+ fixed_type = type_ - Type.VECTOR_INT2
+ return Type(fixed_type % 3 + Type.INT), fixed_type // 3 + 2
+
+ @staticmethod
+ def ToTypedVector(element_type, fixed_len=0):
+ """Converts element type to corresponding vector type.
+
+ Args:
+ element_type: vector element type
+ fixed_len: number of elements: 0 for typed vector; 2, 3, or 4 for fixed
+ typed vector.
+
+ Returns:
+ Typed vector type or fixed typed vector type.
+ """
+ if fixed_len == 0:
+ if not Type.IsTypedVectorElementType(element_type):
+ raise ValueError('must be typed vector element type')
+ else:
+ if not Type.IsFixedTypedVectorElementType(element_type):
+ raise ValueError('must be fixed typed vector element type')
+
+ offset = element_type - Type.INT
+ if fixed_len == 0:
+ return Type(offset + Type.VECTOR_INT) # TypedVector
+ elif fixed_len == 2:
+ return Type(offset + Type.VECTOR_INT2) # FixedTypedVector
+ elif fixed_len == 3:
+ return Type(offset + Type.VECTOR_INT3) # FixedTypedVector
+ elif fixed_len == 4:
+ return Type(offset + Type.VECTOR_INT4) # FixedTypedVector
+ else:
+ raise ValueError('unsupported fixed_len: %s' % fixed_len)
+
+
+class Buf:
+ """Class to access underlying buffer object starting from the given offset."""
+
+ def __init__(self, buf, offset):
+ self._buf = buf
+ self._offset = offset if offset >= 0 else len(buf) + offset
+ self._length = len(buf) - self._offset
+
+ def __getitem__(self, key):
+ if isinstance(key, slice):
+ return self._buf[_ShiftSlice(key, self._offset, self._length)]
+ elif isinstance(key, int):
+ return self._buf[self._offset + key]
+ else:
+ raise TypeError('invalid key type')
+
+ def __setitem__(self, key, value):
+ if isinstance(key, slice):
+ self._buf[_ShiftSlice(key, self._offset, self._length)] = value
+ elif isinstance(key, int):
+ self._buf[self._offset + key] = key
+ else:
+ raise TypeError('invalid key type')
+
+ def __repr__(self):
+ return 'buf[%d:]' % self._offset
+
+ def Find(self, sub):
+ """Returns the lowest index where the sub subsequence is found."""
+ return self._buf[self._offset :].find(sub)
+
+ def Slice(self, offset):
+ """Returns new `Buf` which starts from the given offset."""
+ return Buf(self._buf, self._offset + offset)
+
+ def Indirect(self, offset, byte_width):
+ """Return new `Buf` based on the encoded offset (indirect encoding)."""
+ return self.Slice(offset - _Unpack(U, self[offset : offset + byte_width]))
+
+
+class Object:
+ """Base class for all non-trivial data accessors."""
+
+ __slots__ = '_buf', '_byte_width'
+
+ def __init__(self, buf, byte_width):
+ self._buf = buf
+ self._byte_width = byte_width
+
+ @property
+ def ByteWidth(self):
+ return self._byte_width
+
+
+class Sized(Object):
+ """Base class for all data accessors which need to read encoded size."""
+
+ __slots__ = ('_size',)
+
+ def __init__(self, buf, byte_width, size=0):
+ super().__init__(buf, byte_width)
+ if size == 0:
+ self._size = _Unpack(U, self.SizeBytes)
+ else:
+ self._size = size
+
+ @property
+ def SizeBytes(self):
+ return self._buf[-self._byte_width : 0]
+
+ def __len__(self):
+ return self._size
+
+
+class Blob(Sized):
+ """Data accessor for the encoded blob bytes."""
+
+ __slots__ = ()
+
+ @property
+ def Bytes(self):
+ return self._buf[0 : len(self)]
+
+ def __repr__(self):
+ return 'Blob(%s, size=%d)' % (self._buf, len(self))
+
+
+class String(Sized):
+ """Data accessor for the encoded string bytes."""
+
+ __slots__ = ()
+
+ @property
+ def Bytes(self):
+ return self._buf[0 : len(self)]
+
+ def Mutate(self, value):
+ """Mutates underlying string bytes in place.
+
+ Args:
+ value: New string to replace the existing one. New string must have less
+ or equal UTF-8-encoded bytes than the existing one to successfully
+ mutate underlying byte buffer.
+
+ Returns:
+ Whether the value was mutated or not.
+ """
+ encoded = value.encode('utf-8')
+ n = len(encoded)
+ if n <= len(self):
+ self._buf[-self._byte_width : 0] = _Pack(U, n, self._byte_width)
+ self._buf[0:n] = encoded
+ self._buf[n : len(self)] = bytearray(len(self) - n)
+ return True
+ return False
+
+ def __str__(self):
+ return self.Bytes.decode('utf-8')
+
+ def __repr__(self):
+ return 'String(%s, size=%d)' % (self._buf, len(self))
+
+
+class Key(Object):
+ """Data accessor for the encoded key bytes."""
+
+ __slots__ = ()
+
+ def __init__(self, buf, byte_width):
+ assert byte_width == 1
+ super().__init__(buf, byte_width)
+
+ @property
+ def Bytes(self):
+ return self._buf[0 : len(self)]
+
+ def __len__(self):
+ return self._buf.Find(0)
+
+ def __str__(self):
+ return self.Bytes.decode('ascii')
+
+ def __repr__(self):
+ return 'Key(%s, size=%d)' % (self._buf, len(self))
+
+
+class Vector(Sized):
+ """Data accessor for the encoded vector bytes."""
+
+ __slots__ = ()
+
+ def __getitem__(self, index):
+ if index < 0 or index >= len(self):
+ raise IndexError(
+ 'vector index %s is out of [0, %d) range' % (index, len(self))
+ )
+
+ packed_type = self._buf[len(self) * self._byte_width + index]
+ buf = self._buf.Slice(index * self._byte_width)
+ return Ref.PackedType(buf, self._byte_width, packed_type)
+
+ @property
+ def Value(self):
+ """Returns the underlying encoded data as a list object."""
+ return [e.Value for e in self]
+
+ def __repr__(self):
+ return 'Vector(%s, byte_width=%d, size=%d)' % (
+ self._buf,
+ self._byte_width,
+ self._size,
+ )
+
+
+class TypedVector(Sized):
+ """Data accessor for the encoded typed vector or fixed typed vector bytes."""
+
+ __slots__ = '_element_type', '_size'
+
+ def __init__(self, buf, byte_width, element_type, size=0):
+ super().__init__(buf, byte_width, size)
+
+ if element_type == Type.STRING:
+ # These can't be accessed as strings, since we don't know the bit-width
+ # of the size field, see the declaration of
+ # FBT_VECTOR_STRING_DEPRECATED above for details.
+ # We change the type here to be keys, which are a subtype of strings,
+ # and will ignore the size field. This will truncate strings with
+ # embedded nulls.
+ element_type = Type.KEY
+
+ self._element_type = element_type
+
+ @property
+ def Bytes(self):
+ return self._buf[: self._byte_width * len(self)]
+
+ @property
+ def ElementType(self):
+ return self._element_type
+
+ def __getitem__(self, index):
+ if index < 0 or index >= len(self):
+ raise IndexError(
+ 'vector index %s is out of [0, %d) range' % (index, len(self))
+ )
+
+ buf = self._buf.Slice(index * self._byte_width)
+ return Ref(buf, self._byte_width, 1, self._element_type)
+
+ @property
+ def Value(self):
+ """Returns underlying data as list object."""
+ if not self:
+ return []
+
+ if self._element_type is Type.BOOL:
+ return [bool(e) for e in _UnpackVector(U, self.Bytes, len(self))]
+ elif self._element_type is Type.INT:
+ return list(_UnpackVector(I, self.Bytes, len(self)))
+ elif self._element_type is Type.UINT:
+ return list(_UnpackVector(U, self.Bytes, len(self)))
+ elif self._element_type is Type.FLOAT:
+ return list(_UnpackVector(F, self.Bytes, len(self)))
+ elif self._element_type is Type.KEY:
+ return [e.AsKey for e in self]
+ elif self._element_type is Type.STRING:
+ return [e.AsString for e in self]
+ else:
+ raise TypeError('unsupported element_type: %s' % self._element_type)
+
+ def __repr__(self):
+ return 'TypedVector(%s, byte_width=%d, element_type=%s, size=%d)' % (
+ self._buf,
+ self._byte_width,
+ self._element_type,
+ self._size,
+ )
+
+
+class Map(Vector):
+ """Data accessor for the encoded map bytes."""
+
+ @staticmethod
+ def CompareKeys(a, b):
+ if isinstance(a, Ref):
+ a = a.AsKeyBytes
+ if isinstance(b, Ref):
+ b = b.AsKeyBytes
+ return a < b
+
+ def __getitem__(self, key):
+ if isinstance(key, int):
+ return super().__getitem__(key)
+
+ index = _BinarySearch(self.Keys, key.encode('ascii'), self.CompareKeys)
+ if index != -1:
+ return super().__getitem__(index)
+
+ raise KeyError(key)
+
+ @property
+ def Keys(self):
+ byte_width = _Unpack(
+ U, self._buf[-2 * self._byte_width : -self._byte_width]
+ )
+ buf = self._buf.Indirect(-3 * self._byte_width, self._byte_width)
+ return TypedVector(buf, byte_width, Type.KEY)
+
+ @property
+ def Values(self):
+ return Vector(self._buf, self._byte_width)
+
+ @property
+ def Value(self):
+ return {k.Value: v.Value for k, v in zip(self.Keys, self.Values)}
+
+ def __repr__(self):
+ return 'Map(%s, size=%d)' % (self._buf, len(self))
+
+
+class Ref:
+ """Data accessor for the encoded data bytes."""
+
+ __slots__ = '_buf', '_parent_width', '_byte_width', '_type'
+
+ @staticmethod
+ def PackedType(buf, parent_width, packed_type):
+ byte_width, type_ = Type.Unpack(packed_type)
+ return Ref(buf, parent_width, byte_width, type_)
+
+ def __init__(self, buf, parent_width, byte_width, type_):
+ self._buf = buf
+ self._parent_width = parent_width
+ self._byte_width = byte_width
+ self._type = type_
+
+ def __repr__(self):
+ return 'Ref(%s, parent_width=%d, byte_width=%d, type_=%s)' % (
+ self._buf,
+ self._parent_width,
+ self._byte_width,
+ self._type,
+ )
+
+ @property
+ def _Bytes(self):
+ return self._buf[: self._parent_width]
+
+ def _ConvertError(self, target_type):
+ raise TypeError('cannot convert %s to %s' % (self._type, target_type))
+
+ def _Indirect(self):
+ return self._buf.Indirect(0, self._parent_width)
+
+ @property
+ def IsNull(self):
+ return self._type is Type.NULL
+
+ @property
+ def IsBool(self):
+ return self._type is Type.BOOL
+
+ @property
+ def AsBool(self):
+ if self._type is Type.BOOL:
+ return bool(_Unpack(U, self._Bytes))
+ else:
+ return self.AsInt != 0
+
+ def MutateBool(self, value):
+ """Mutates underlying boolean value bytes in place.
+
+ Args:
+ value: New boolean value.
+
+ Returns:
+ Whether the value was mutated or not.
+ """
+ return self.IsBool and _Mutate(
+ U, self._buf, value, self._parent_width, BitWidth.W8
+ )
+
+ @property
+ def IsNumeric(self):
+ return self.IsInt or self.IsFloat
+
+ @property
+ def IsInt(self):
+ return self._type in (
+ Type.INT,
+ Type.INDIRECT_INT,
+ Type.UINT,
+ Type.INDIRECT_UINT,
+ )
+
+ @property
+ def AsInt(self):
+ """Returns current reference as integer value."""
+ if self.IsNull:
+ return 0
+ elif self.IsBool:
+ return int(self.AsBool)
+ elif self._type is Type.INT:
+ return _Unpack(I, self._Bytes)
+ elif self._type is Type.INDIRECT_INT:
+ return _Unpack(I, self._Indirect()[: self._byte_width])
+ if self._type is Type.UINT:
+ return _Unpack(U, self._Bytes)
+ elif self._type is Type.INDIRECT_UINT:
+ return _Unpack(U, self._Indirect()[: self._byte_width])
+ elif self.IsString:
+ return len(self.AsString)
+ elif self.IsKey:
+ return len(self.AsKey)
+ elif self.IsBlob:
+ return len(self.AsBlob)
+ elif self.IsVector:
+ return len(self.AsVector)
+ elif self.IsTypedVector:
+ return len(self.AsTypedVector)
+ elif self.IsFixedTypedVector:
+ return len(self.AsFixedTypedVector)
+ else:
+ raise self._ConvertError(Type.INT)
+
+ def MutateInt(self, value):
+ """Mutates underlying integer value bytes in place.
+
+ Args:
+ value: New integer value. It must fit to the byte size of the existing
+ encoded value.
+
+ Returns:
+ Whether the value was mutated or not.
+ """
+ if self._type is Type.INT:
+ return _Mutate(I, self._buf, value, self._parent_width, BitWidth.I(value))
+ elif self._type is Type.INDIRECT_INT:
+ return _Mutate(
+ I, self._Indirect(), value, self._byte_width, BitWidth.I(value)
+ )
+ elif self._type is Type.UINT:
+ return _Mutate(U, self._buf, value, self._parent_width, BitWidth.U(value))
+ elif self._type is Type.INDIRECT_UINT:
+ return _Mutate(
+ U, self._Indirect(), value, self._byte_width, BitWidth.U(value)
+ )
+ else:
+ return False
+
+ @property
+ def IsFloat(self):
+ return self._type in (Type.FLOAT, Type.INDIRECT_FLOAT)
+
+ @property
+ def AsFloat(self):
+ """Returns current reference as floating point value."""
+ if self.IsNull:
+ return 0.0
+ elif self.IsBool:
+ return float(self.AsBool)
+ elif self.IsInt:
+ return float(self.AsInt)
+ elif self._type is Type.FLOAT:
+ return _Unpack(F, self._Bytes)
+ elif self._type is Type.INDIRECT_FLOAT:
+ return _Unpack(F, self._Indirect()[: self._byte_width])
+ elif self.IsString:
+ return float(self.AsString)
+ elif self.IsVector:
+ return float(len(self.AsVector))
+ elif self.IsTypedVector():
+ return float(len(self.AsTypedVector))
+ elif self.IsFixedTypedVector():
+ return float(len(self.FixedTypedVector))
+ else:
+ raise self._ConvertError(Type.FLOAT)
+
+ def MutateFloat(self, value):
+ """Mutates underlying floating point value bytes in place.
+
+ Args:
+ value: New float value. It must fit to the byte size of the existing
+ encoded value.
+
+ Returns:
+ Whether the value was mutated or not.
+ """
+ if self._type is Type.FLOAT:
+ return _Mutate(
+ F,
+ self._buf,
+ value,
+ self._parent_width,
+ BitWidth.B(self._parent_width),
+ )
+ elif self._type is Type.INDIRECT_FLOAT:
+ return _Mutate(
+ F,
+ self._Indirect(),
+ value,
+ self._byte_width,
+ BitWidth.B(self._byte_width),
+ )
+ else:
+ return False
+
+ @property
+ def IsKey(self):
+ return self._type is Type.KEY
+
+ @property
+ def AsKeyBytes(self):
+ if self.IsKey:
+ return Key(self._Indirect(), self._byte_width).Bytes
+ else:
+ raise self._ConvertError(Type.KEY)
+
+ @property
+ def AsKey(self):
+ if self.IsKey:
+ return str(Key(self._Indirect(), self._byte_width))
+ else:
+ raise self._ConvertError(Type.KEY)
+
+ @property
+ def IsString(self):
+ return self._type is Type.STRING
+
+ @property
+ def AsStringBytes(self):
+ if self.IsString:
+ return String(self._Indirect(), self._byte_width).Bytes
+ elif self.IsKey:
+ return self.AsKeyBytes
+ else:
+ raise self._ConvertError(Type.STRING)
+
+ @property
+ def AsString(self):
+ if self.IsString:
+ return str(String(self._Indirect(), self._byte_width))
+ elif self.IsKey:
+ return self.AsKey
+ else:
+ raise self._ConvertError(Type.STRING)
+
+ def MutateString(self, value):
+ return String(self._Indirect(), self._byte_width).Mutate(value)
+
+ @property
+ def IsBlob(self):
+ return self._type is Type.BLOB
+
+ @property
+ def AsBlob(self):
+ if self.IsBlob:
+ return Blob(self._Indirect(), self._byte_width).Bytes
+ else:
+ raise self._ConvertError(Type.BLOB)
+
+ @property
+ def IsAnyVector(self):
+ return self.IsVector or self.IsTypedVector or self.IsFixedTypedVector()
+
+ @property
+ def IsVector(self):
+ return self._type in (Type.VECTOR, Type.MAP)
+
+ @property
+ def AsVector(self):
+ if self.IsVector:
+ return Vector(self._Indirect(), self._byte_width)
+ else:
+ raise self._ConvertError(Type.VECTOR)
+
+ @property
+ def IsTypedVector(self):
+ return Type.IsTypedVector(self._type)
+
+ @property
+ def AsTypedVector(self):
+ if self.IsTypedVector:
+ return TypedVector(
+ self._Indirect(),
+ self._byte_width,
+ Type.ToTypedVectorElementType(self._type),
+ )
+ else:
+ raise self._ConvertError('TYPED_VECTOR')
+
+ @property
+ def IsFixedTypedVector(self):
+ return Type.IsFixedTypedVector(self._type)
+
+ @property
+ def AsFixedTypedVector(self):
+ if self.IsFixedTypedVector:
+ element_type, size = Type.ToFixedTypedVectorElementType(self._type)
+ return TypedVector(self._Indirect(), self._byte_width, element_type, size)
+ else:
+ raise self._ConvertError('FIXED_TYPED_VECTOR')
+
+ @property
+ def IsMap(self):
+ return self._type is Type.MAP
+
+ @property
+ def AsMap(self):
+ if self.IsMap:
+ return Map(self._Indirect(), self._byte_width)
+ else:
+ raise self._ConvertError(Type.MAP)
+
+ @property
+ def Value(self):
+ """Converts current reference to value of corresponding type.
+
+ This is equivalent to calling `AsInt` for integer values, `AsFloat` for
+ floating point values, etc.
+
+ Returns:
+ Value of corresponding type.
+ """
+ if self.IsNull:
+ return None
+ elif self.IsBool:
+ return self.AsBool
+ elif self.IsInt:
+ return self.AsInt
+ elif self.IsFloat:
+ return self.AsFloat
+ elif self.IsString:
+ return self.AsString
+ elif self.IsKey:
+ return self.AsKey
+ elif self.IsBlob:
+ return self.AsBlob
+ elif self.IsMap:
+ return self.AsMap.Value
+ elif self.IsVector:
+ return self.AsVector.Value
+ elif self.IsTypedVector:
+ return self.AsTypedVector.Value
+ elif self.IsFixedTypedVector:
+ return self.AsFixedTypedVector.Value
+ else:
+ raise TypeError('cannot convert %r to value' % self)
+
+
+def _IsIterable(obj):
+ try:
+ iter(obj)
+ return True
+ except TypeError:
+ return False
+
+
+class Value:
+ """Class to represent given value during the encoding process."""
+
+ @staticmethod
+ def Null():
+ return Value(0, Type.NULL, BitWidth.W8)
+
+ @staticmethod
+ def Bool(value):
+ return Value(value, Type.BOOL, BitWidth.W8)
+
+ @staticmethod
+ def Int(value, bit_width):
+ return Value(value, Type.INT, bit_width)
+
+ @staticmethod
+ def UInt(value, bit_width):
+ return Value(value, Type.UINT, bit_width)
+
+ @staticmethod
+ def Float(value, bit_width):
+ return Value(value, Type.FLOAT, bit_width)
+
+ @staticmethod
+ def Key(offset):
+ return Value(offset, Type.KEY, BitWidth.W8)
+
+ def __init__(self, value, type_, min_bit_width):
+ self._value = value
+ self._type = type_
+
+ # For scalars: of itself, for vector: of its elements, for string: length.
+ self._min_bit_width = min_bit_width
+
+ @property
+ def Value(self):
+ return self._value
+
+ @property
+ def Type(self):
+ return self._type
+
+ @property
+ def MinBitWidth(self):
+ return self._min_bit_width
+
+ def StoredPackedType(self, parent_bit_width=BitWidth.W8):
+ return Type.Pack(self._type, self.StoredWidth(parent_bit_width))
+
+ # We have an absolute offset, but want to store a relative offset
+ # elem_index elements beyond the current buffer end. Since whether
+ # the relative offset fits in a certain byte_width depends on
+ # the size of the elements before it (and their alignment), we have
+ # to test for each size in turn.
+ def ElemWidth(self, buf_size, elem_index=0):
+ if Type.IsInline(self._type):
+ return self._min_bit_width
+ for byte_width in 1, 2, 4, 8:
+ offset_loc = (
+ buf_size
+ + _PaddingBytes(buf_size, byte_width)
+ + elem_index * byte_width
+ )
+ bit_width = BitWidth.U(offset_loc - self._value)
+ if byte_width == (1 << bit_width):
+ return bit_width
+ raise ValueError('relative offset is too big')
+
+ def StoredWidth(self, parent_bit_width=BitWidth.W8):
+ if Type.IsInline(self._type):
+ return max(self._min_bit_width, parent_bit_width)
+ return self._min_bit_width
+
+ def __repr__(self):
+ return 'Value(%s, %s, %s)' % (self._value, self._type, self._min_bit_width)
+
+ def __str__(self):
+ return str(self._value)
+
+
+def InMap(func):
+ def wrapper(self, *args, **kwargs):
+ if isinstance(args[0], str):
+ self.Key(args[0])
+ func(self, *args[1:], **kwargs)
+ else:
+ func(self, *args, **kwargs)
+
+ return wrapper
+
+
+def InMapForString(func):
+ def wrapper(self, *args):
+ if len(args) == 1:
+ func(self, args[0])
+ elif len(args) == 2:
+ self.Key(args[0])
+ func(self, args[1])
+ else:
+ raise ValueError('invalid number of arguments')
+
+ return wrapper
+
+
+class Pool:
+ """Collection of (data, offset) pairs sorted by data for quick access."""
+
+ def __init__(self):
+ self._pool = [] # sorted list of (data, offset) tuples
+
+ def FindOrInsert(self, data, offset):
+ do = data, offset
+ index = _BinarySearch(self._pool, do, lambda a, b: a[0] < b[0])
+ if index != -1:
+ _, offset = self._pool[index]
+ return offset
+ self._pool.insert(index, do)
+ return None
+
+ def Clear(self):
+ self._pool = []
+
+ @property
+ def Elements(self):
+ return [data for data, _ in self._pool]
+
+
+class Builder:
+ """Helper class to encode structural data into flexbuffers format."""
+
+ def __init__(
+ self,
+ share_strings=False,
+ share_keys=True,
+ force_min_bit_width=BitWidth.W8,
+ ):
+ self._share_strings = share_strings
+ self._share_keys = share_keys
+ self._force_min_bit_width = force_min_bit_width
+
+ self._string_pool = Pool()
+ self._key_pool = Pool()
+
+ self._finished = False
+ self._buf = bytearray()
+ self._stack = []
+
+ def __len__(self):
+ return len(self._buf)
+
+ @property
+ def StringPool(self):
+ return self._string_pool
+
+ @property
+ def KeyPool(self):
+ return self._key_pool
+
+ def Clear(self):
+ self._string_pool.Clear()
+ self._key_pool.Clear()
+ self._finished = False
+ self._buf = bytearray()
+ self._stack = []
+
+ def Finish(self):
+ """Finishes encoding process and returns underlying buffer."""
+ if self._finished:
+ raise RuntimeError('builder has been already finished')
+
+ # If you hit this exception, you likely have objects that were never
+ # included in a parent. You need to have exactly one root to finish a
+ # buffer. Check your Start/End calls are matched, and all objects are inside
+ # some other object.
+ if len(self._stack) != 1:
+ raise RuntimeError('internal stack size must be one')
+
+ value = self._stack[0]
+ byte_width = self._Align(value.ElemWidth(len(self._buf)))
+ self._WriteAny(value, byte_width=byte_width) # Root value
+ self._Write(U, value.StoredPackedType(), byte_width=1) # Root type
+ self._Write(U, byte_width, byte_width=1) # Root size
+
+ self.finished = True
+ return self._buf
+
+ def _ReadKey(self, offset):
+ key = self._buf[offset:]
+ return key[: key.find(0)]
+
+ def _Align(self, alignment):
+ byte_width = 1 << alignment
+ self._buf.extend(b'\x00' * _PaddingBytes(len(self._buf), byte_width))
+ return byte_width
+
+ def _Write(self, fmt, value, byte_width):
+ self._buf.extend(_Pack(fmt, value, byte_width))
+
+ def _WriteVector(self, fmt, values, byte_width):
+ self._buf.extend(_PackVector(fmt, values, byte_width))
+
+ def _WriteOffset(self, offset, byte_width):
+ relative_offset = len(self._buf) - offset
+ assert byte_width == 8 or relative_offset < (1 << (8 * byte_width))
+ self._Write(U, relative_offset, byte_width)
+
+ def _WriteAny(self, value, byte_width):
+ fmt = {
+ Type.NULL: U,
+ Type.BOOL: U,
+ Type.INT: I,
+ Type.UINT: U,
+ Type.FLOAT: F,
+ }.get(value.Type)
+ if fmt:
+ self._Write(fmt, value.Value, byte_width)
+ else:
+ self._WriteOffset(value.Value, byte_width)
+
+ def _WriteBlob(self, data, append_zero, type_):
+ bit_width = BitWidth.U(len(data))
+ byte_width = self._Align(bit_width)
+ self._Write(U, len(data), byte_width)
+ loc = len(self._buf)
+ self._buf.extend(data)
+ if append_zero:
+ self._buf.append(0)
+ self._stack.append(Value(loc, type_, bit_width))
+ return loc
+
+ def _WriteScalarVector(self, element_type, byte_width, elements, fixed):
+ """Writes scalar vector elements to the underlying buffer."""
+ bit_width = BitWidth.B(byte_width)
+ # If you get this exception, you're trying to write a vector with a size
+ # field that is bigger than the scalars you're trying to write (e.g. a
+ # byte vector > 255 elements). For such types, write a "blob" instead.
+ if BitWidth.U(len(elements)) > bit_width:
+ raise ValueError('too many elements for the given byte_width')
+
+ self._Align(bit_width)
+ if not fixed:
+ self._Write(U, len(elements), byte_width)
+
+ loc = len(self._buf)
+
+ fmt = {Type.INT: I, Type.UINT: U, Type.FLOAT: F}.get(element_type)
+ if not fmt:
+ raise TypeError('unsupported element_type')
+ self._WriteVector(fmt, elements, byte_width)
+
+ type_ = Type.ToTypedVector(element_type, len(elements) if fixed else 0)
+ self._stack.append(Value(loc, type_, bit_width))
+ return loc
+
+ def _CreateVector(self, elements, typed, fixed, keys=None):
+ """Writes vector elements to the underlying buffer."""
+ length = len(elements)
+
+ if fixed and not typed:
+ raise ValueError('fixed vector must be typed')
+
+ # Figure out smallest bit width we can store this vector with.
+ bit_width = max(self._force_min_bit_width, BitWidth.U(length))
+ prefix_elems = 1 # Vector size
+ if keys:
+ bit_width = max(bit_width, keys.ElemWidth(len(self._buf)))
+ prefix_elems += 2 # Offset to the keys vector and its byte width.
+
+ vector_type = Type.KEY
+ # Check bit widths and types for all elements.
+ for i, e in enumerate(elements):
+ bit_width = max(bit_width, e.ElemWidth(len(self._buf), prefix_elems + i))
+
+ if typed:
+ if i == 0:
+ vector_type = e.Type
+ else:
+ if vector_type != e.Type:
+ raise RuntimeError('typed vector elements must be of the same type')
+
+ if fixed and not Type.IsFixedTypedVectorElementType(vector_type):
+ raise RuntimeError('must be fixed typed vector element type')
+
+ byte_width = self._Align(bit_width)
+ # Write vector. First the keys width/offset if available, and size.
+ if keys:
+ self._WriteOffset(keys.Value, byte_width)
+ self._Write(U, 1 << keys.MinBitWidth, byte_width)
+
+ if not fixed:
+ self._Write(U, length, byte_width)
+
+ # Then the actual data.
+ loc = len(self._buf)
+ for e in elements:
+ self._WriteAny(e, byte_width)
+
+ # Then the types.
+ if not typed:
+ for e in elements:
+ self._buf.append(e.StoredPackedType(bit_width))
+
+ if keys:
+ type_ = Type.MAP
+ else:
+ if typed:
+ type_ = Type.ToTypedVector(vector_type, length if fixed else 0)
+ else:
+ type_ = Type.VECTOR
+
+ return Value(loc, type_, bit_width)
+
+ def _PushIndirect(self, value, type_, bit_width):
+ byte_width = self._Align(bit_width)
+ loc = len(self._buf)
+ fmt = {Type.INDIRECT_INT: I, Type.INDIRECT_UINT: U, Type.INDIRECT_FLOAT: F}[
+ type_
+ ]
+ self._Write(fmt, value, byte_width)
+ self._stack.append(Value(loc, type_, bit_width))
+
+ @InMapForString
+ def String(self, value):
+ """Encodes string value."""
+ reset_to = len(self._buf)
+ encoded = value.encode('utf-8')
+ loc = self._WriteBlob(encoded, append_zero=True, type_=Type.STRING)
+ if self._share_strings:
+ prev_loc = self._string_pool.FindOrInsert(encoded, loc)
+ if prev_loc is not None:
+ del self._buf[reset_to:]
+ self._stack[-1]._value = loc = prev_loc # pylint: disable=protected-access
+
+ return loc
+
+ @InMap
+ def Blob(self, value):
+ """Encodes binary blob value.
+
+ Args:
+ value: A byte/bytearray value to encode
+
+ Returns:
+ Offset of the encoded value in underlying the byte buffer.
+ """
+ return self._WriteBlob(value, append_zero=False, type_=Type.BLOB)
+
+ def Key(self, value):
+ """Encodes key value.
+
+ Args:
+ value: A byte/bytearray/str value to encode. Byte object must not contain
+ zero bytes. String object must be convertible to ASCII.
+
+ Returns:
+ Offset of the encoded value in the underlying byte buffer.
+ """
+ if isinstance(value, (bytes, bytearray)):
+ encoded = value
+ else:
+ encoded = value.encode('ascii')
+
+ if 0 in encoded:
+ raise ValueError('key contains zero byte')
+
+ loc = len(self._buf)
+ self._buf.extend(encoded)
+ self._buf.append(0)
+ if self._share_keys:
+ prev_loc = self._key_pool.FindOrInsert(encoded, loc)
+ if prev_loc is not None:
+ del self._buf[loc:]
+ loc = prev_loc
+
+ self._stack.append(Value.Key(loc))
+ return loc
+
+ def Null(self, key=None):
+ """Encodes None value."""
+ if key:
+ self.Key(key)
+ self._stack.append(Value.Null())
+
+ @InMap
+ def Bool(self, value):
+ """Encodes boolean value.
+
+ Args:
+ value: A boolean value.
+ """
+ self._stack.append(Value.Bool(value))
+
+ @InMap
+ def Int(self, value, byte_width=0):
+ """Encodes signed integer value.
+
+ Args:
+ value: A signed integer value.
+ byte_width: Number of bytes to use: 1, 2, 4, or 8.
+ """
+ bit_width = BitWidth.I(value) if byte_width == 0 else BitWidth.B(byte_width)
+ self._stack.append(Value.Int(value, bit_width))
+
+ @InMap
+ def IndirectInt(self, value, byte_width=0):
+ """Encodes signed integer value indirectly.
+
+ Args:
+ value: A signed integer value.
+ byte_width: Number of bytes to use: 1, 2, 4, or 8.
+ """
+ bit_width = BitWidth.I(value) if byte_width == 0 else BitWidth.B(byte_width)
+ self._PushIndirect(value, Type.INDIRECT_INT, bit_width)
+
+ @InMap
+ def UInt(self, value, byte_width=0):
+ """Encodes unsigned integer value.
+
+ Args:
+ value: An unsigned integer value.
+ byte_width: Number of bytes to use: 1, 2, 4, or 8.
+ """
+ bit_width = BitWidth.U(value) if byte_width == 0 else BitWidth.B(byte_width)
+ self._stack.append(Value.UInt(value, bit_width))
+
+ @InMap
+ def IndirectUInt(self, value, byte_width=0):
+ """Encodes unsigned integer value indirectly.
+
+ Args:
+ value: An unsigned integer value.
+ byte_width: Number of bytes to use: 1, 2, 4, or 8.
+ """
+ bit_width = BitWidth.U(value) if byte_width == 0 else BitWidth.B(byte_width)
+ self._PushIndirect(value, Type.INDIRECT_UINT, bit_width)
+
+ @InMap
+ def Float(self, value, byte_width=0):
+ """Encodes floating point value.
+
+ Args:
+ value: A floating point value.
+ byte_width: Number of bytes to use: 4 or 8.
+ """
+ bit_width = BitWidth.F(value) if byte_width == 0 else BitWidth.B(byte_width)
+ self._stack.append(Value.Float(value, bit_width))
+
+ @InMap
+ def IndirectFloat(self, value, byte_width=0):
+ """Encodes floating point value indirectly.
+
+ Args:
+ value: A floating point value.
+ byte_width: Number of bytes to use: 4 or 8.
+ """
+ bit_width = BitWidth.F(value) if byte_width == 0 else BitWidth.B(byte_width)
+ self._PushIndirect(value, Type.INDIRECT_FLOAT, bit_width)
+
+ def _StartVector(self):
+ """Starts vector construction."""
+ return len(self._stack)
+
+ def _EndVector(self, start, typed, fixed):
+ """Finishes vector construction by encodung its elements."""
+ vec = self._CreateVector(self._stack[start:], typed, fixed)
+ del self._stack[start:]
+ self._stack.append(vec)
+ return vec.Value
+
+ @contextlib.contextmanager
+ def Vector(self, key=None):
+ if key:
+ self.Key(key)
+
+ try:
+ start = self._StartVector()
+ yield self
+ finally:
+ self._EndVector(start, typed=False, fixed=False)
+
+ @InMap
+ def VectorFromElements(self, elements):
+ """Encodes sequence of any elements as a vector.
+
+ Args:
+ elements: sequence of elements, they may have different types.
+ """
+ with self.Vector():
+ for e in elements:
+ self.Add(e)
+
+ @contextlib.contextmanager
+ def TypedVector(self, key=None):
+ if key:
+ self.Key(key)
+
+ try:
+ start = self._StartVector()
+ yield self
+ finally:
+ self._EndVector(start, typed=True, fixed=False)
+
+ @InMap
+ def TypedVectorFromElements(self, elements, element_type=None):
+ """Encodes sequence of elements of the same type as typed vector.
+
+ Args:
+ elements: Sequence of elements, they must be of the same type.
+ element_type: Suggested element type. Setting it to None means determining
+ correct value automatically based on the given elements.
+ """
+ if isinstance(elements, array.array):
+ if elements.typecode == 'f':
+ self._WriteScalarVector(Type.FLOAT, 4, elements, fixed=False)
+ elif elements.typecode == 'd':
+ self._WriteScalarVector(Type.FLOAT, 8, elements, fixed=False)
+ elif elements.typecode in ('b', 'h', 'i', 'l', 'q'):
+ self._WriteScalarVector(
+ Type.INT, elements.itemsize, elements, fixed=False
+ )
+ elif elements.typecode in ('B', 'H', 'I', 'L', 'Q'):
+ self._WriteScalarVector(
+ Type.UINT, elements.itemsize, elements, fixed=False
+ )
+ else:
+ raise ValueError('unsupported array typecode: %s' % elements.typecode)
+ else:
+ add = self.Add if element_type is None else self.Adder(element_type)
+ with self.TypedVector():
+ for e in elements:
+ add(e)
+
+ @InMap
+ def FixedTypedVectorFromElements(
+ self, elements, element_type=None, byte_width=0
+ ):
+ """Encodes sequence of elements of the same type as fixed typed vector.
+
+ Args:
+ elements: Sequence of elements, they must be of the same type. Allowed
+ types are `Type.INT`, `Type.UINT`, `Type.FLOAT`. Allowed number of
+ elements are 2, 3, or 4.
+ element_type: Suggested element type. Setting it to None means determining
+ correct value automatically based on the given elements.
+ byte_width: Number of bytes to use per element. For `Type.INT` and
+ `Type.UINT`: 1, 2, 4, or 8. For `Type.FLOAT`: 4 or 8. Setting it to 0
+ means determining correct value automatically based on the given
+ elements.
+ """
+ if not 2 <= len(elements) <= 4:
+ raise ValueError('only 2, 3, or 4 elements are supported')
+
+ types = {type(e) for e in elements}
+ if len(types) != 1:
+ raise TypeError('all elements must be of the same type')
+
+ (type_,) = types
+
+ if element_type is None:
+ element_type = {int: Type.INT, float: Type.FLOAT}.get(type_)
+ if not element_type:
+ raise TypeError('unsupported element_type: %s' % type_)
+
+ if byte_width == 0:
+ width = {
+ Type.UINT: BitWidth.U,
+ Type.INT: BitWidth.I,
+ Type.FLOAT: BitWidth.F,
+ }[element_type]
+ byte_width = 1 << max(width(e) for e in elements)
+
+ self._WriteScalarVector(element_type, byte_width, elements, fixed=True)
+
+ def _StartMap(self):
+ """Starts map construction."""
+ return len(self._stack)
+
+ def _EndMap(self, start):
+ """Finishes map construction by encodung its elements."""
+ # Interleaved keys and values on the stack.
+ stack = self._stack[start:]
+
+ if len(stack) % 2 != 0:
+ raise RuntimeError('must be even number of keys and values')
+
+ for key in stack[::2]:
+ if key.Type is not Type.KEY:
+ raise RuntimeError('all map keys must be of %s type' % Type.KEY)
+
+ pairs = zip(stack[::2], stack[1::2]) # [(key, value), ...]
+ pairs = sorted(pairs, key=lambda pair: self._ReadKey(pair[0].Value))
+
+ del self._stack[start:]
+ for pair in pairs:
+ self._stack.extend(pair)
+
+ keys = self._CreateVector(self._stack[start::2], typed=True, fixed=False)
+ values = self._CreateVector(
+ self._stack[start + 1 :: 2], typed=False, fixed=False, keys=keys
+ )
+
+ del self._stack[start:]
+ self._stack.append(values)
+ return values.Value
+
+ @contextlib.contextmanager
+ def Map(self, key=None):
+ if key:
+ self.Key(key)
+
+ try:
+ start = self._StartMap()
+ yield self
+ finally:
+ self._EndMap(start)
+
+ def MapFromElements(self, elements):
+ start = self._StartMap()
+ for k, v in elements.items():
+ self.Key(k)
+ self.Add(v)
+ self._EndMap(start)
+
+ def Adder(self, type_):
+ return {
+ Type.BOOL: self.Bool,
+ Type.INT: self.Int,
+ Type.INDIRECT_INT: self.IndirectInt,
+ Type.UINT: self.UInt,
+ Type.INDIRECT_UINT: self.IndirectUInt,
+ Type.FLOAT: self.Float,
+ Type.INDIRECT_FLOAT: self.IndirectFloat,
+ Type.KEY: self.Key,
+ Type.BLOB: self.Blob,
+ Type.STRING: self.String,
+ }[type_]
+
+ @InMapForString
+ def Add(self, value):
+ """Encodes value of any supported type."""
+ if value is None:
+ self.Null()
+ elif isinstance(value, bool):
+ self.Bool(value)
+ elif isinstance(value, int):
+ self.Int(value)
+ elif isinstance(value, float):
+ self.Float(value)
+ elif isinstance(value, str):
+ self.String(value)
+ elif isinstance(value, (bytes, bytearray)):
+ self.Blob(value)
+ elif isinstance(value, dict):
+ with self.Map():
+ for k, v in value.items():
+ self.Key(k)
+ self.Add(v)
+ elif isinstance(value, array.array):
+ self.TypedVectorFromElements(value)
+ elif _IsIterable(value):
+ self.VectorFromElements(value)
+ else:
+ raise TypeError('unsupported python type: %s' % type(value))
+
+ @property
+ def LastValue(self):
+ return self._stack[-1]
+
+ @InMap
+ def ReuseValue(self, value):
+ self._stack.append(value)
+
+
+def GetRoot(buf):
+ """Returns root `Ref` object for the given buffer."""
+ if len(buf) < 3:
+ raise ValueError('buffer is too small')
+ byte_width = buf[-1]
+ return Ref.PackedType(
+ Buf(buf, -(2 + byte_width)), byte_width, packed_type=buf[-2]
+ )
+
+
+def Dumps(obj):
+ """Returns bytearray with the encoded python object."""
+ fbb = Builder()
+ fbb.Add(obj)
+ return fbb.Finish()
+
+
+def Loads(buf):
+ """Returns python object decoded from the buffer."""
+ return GetRoot(buf).Value
diff --git a/frogpilot/third_party/flatbuffers/number_types.py b/frogpilot/third_party/flatbuffers/number_types.py
new file mode 100644
index 00000000..e47f66f1
--- /dev/null
+++ b/frogpilot/third_party/flatbuffers/number_types.py
@@ -0,0 +1,182 @@
+# Copyright 2014 Google Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import collections
+import struct
+
+from . import packer
+from .compat import NumpyRequiredForThisFeature, import_numpy
+
+np = import_numpy()
+
+# For reference, see:
+# https://docs.python.org/2/library/ctypes.html#ctypes-fundamental-data-types-2
+
+# These classes could be collections.namedtuple instances, but those are new
+# in 2.6 and we want to work towards 2.5 compatability.
+
+
+class BoolFlags(object):
+ bytewidth = 1
+ min_val = False
+ max_val = True
+ py_type = bool
+ name = "bool"
+ packer_type = packer.boolean
+
+
+class Uint8Flags(object):
+ bytewidth = 1
+ min_val = 0
+ max_val = (2**8) - 1
+ py_type = int
+ name = "uint8"
+ packer_type = packer.uint8
+
+
+class Uint16Flags(object):
+ bytewidth = 2
+ min_val = 0
+ max_val = (2**16) - 1
+ py_type = int
+ name = "uint16"
+ packer_type = packer.uint16
+
+
+class Uint32Flags(object):
+ bytewidth = 4
+ min_val = 0
+ max_val = (2**32) - 1
+ py_type = int
+ name = "uint32"
+ packer_type = packer.uint32
+
+
+class Uint64Flags(object):
+ bytewidth = 8
+ min_val = 0
+ max_val = (2**64) - 1
+ py_type = int
+ name = "uint64"
+ packer_type = packer.uint64
+
+
+class Int8Flags(object):
+ bytewidth = 1
+ min_val = -(2**7)
+ max_val = (2**7) - 1
+ py_type = int
+ name = "int8"
+ packer_type = packer.int8
+
+
+class Int16Flags(object):
+ bytewidth = 2
+ min_val = -(2**15)
+ max_val = (2**15) - 1
+ py_type = int
+ name = "int16"
+ packer_type = packer.int16
+
+
+class Int32Flags(object):
+ bytewidth = 4
+ min_val = -(2**31)
+ max_val = (2**31) - 1
+ py_type = int
+ name = "int32"
+ packer_type = packer.int32
+
+
+class Int64Flags(object):
+ bytewidth = 8
+ min_val = -(2**63)
+ max_val = (2**63) - 1
+ py_type = int
+ name = "int64"
+ packer_type = packer.int64
+
+
+class Float32Flags(object):
+ bytewidth = 4
+ min_val = None
+ max_val = None
+ py_type = float
+ name = "float32"
+ packer_type = packer.float32
+
+
+class Float64Flags(object):
+ bytewidth = 8
+ min_val = None
+ max_val = None
+ py_type = float
+ name = "float64"
+ packer_type = packer.float64
+
+
+class SOffsetTFlags(Int32Flags):
+ pass
+
+
+class UOffsetTFlags(Uint32Flags):
+ pass
+
+
+class VOffsetTFlags(Uint16Flags):
+ pass
+
+
+def valid_number(n, flags):
+ if flags.min_val is None and flags.max_val is None:
+ return True
+ return flags.min_val <= n <= flags.max_val
+
+
+def enforce_number(n, flags):
+ if flags.min_val is None and flags.max_val is None:
+ return
+ if not flags.min_val <= n <= flags.max_val:
+ raise TypeError("bad number %s for type %s" % (str(n), flags.name))
+
+
+def float32_to_uint32(n):
+ packed = struct.pack("<1f", n)
+ (converted,) = struct.unpack("<1L", packed)
+ return converted
+
+
+def uint32_to_float32(n):
+ packed = struct.pack("<1L", n)
+ (unpacked,) = struct.unpack("<1f", packed)
+ return unpacked
+
+
+def float64_to_uint64(n):
+ packed = struct.pack("<1d", n)
+ (converted,) = struct.unpack("<1Q", packed)
+ return converted
+
+
+def uint64_to_float64(n):
+ packed = struct.pack("<1Q", n)
+ (unpacked,) = struct.unpack("<1d", packed)
+ return unpacked
+
+
+def to_numpy_type(number_type):
+ if np is not None:
+ return np.dtype(number_type.name).newbyteorder("<")
+ else:
+ raise NumpyRequiredForThisFeature("Numpy was not found.")
diff --git a/frogpilot/third_party/flatbuffers/packer.py b/frogpilot/third_party/flatbuffers/packer.py
new file mode 100644
index 00000000..0296e52b
--- /dev/null
+++ b/frogpilot/third_party/flatbuffers/packer.py
@@ -0,0 +1,41 @@
+# Copyright 2016 Google Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Provide pre-compiled struct packers for encoding and decoding.
+
+See: https://docs.python.org/2/library/struct.html#format-characters
+"""
+
+import struct
+from . import compat
+
+
+boolean = struct.Struct(compat.struct_bool_decl)
+
+uint8 = struct.Struct("
+Maintainer-Email: AJ Friend
+License:
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Intended Audience :: Developers
+Classifier: Intended Audience :: Science/Research
+Classifier: License :: OSI Approved :: Apache Software License
+Classifier: Programming Language :: C
+Classifier: Programming Language :: Cython
+Classifier: Programming Language :: Python :: 3.8
+Classifier: Programming Language :: Python :: 3.9
+Classifier: Programming Language :: Python :: 3.10
+Classifier: Programming Language :: Python :: 3.11
+Classifier: Programming Language :: Python :: 3.12
+Classifier: Programming Language :: Python :: 3.13
+Classifier: Operating System :: MacOS :: MacOS X
+Classifier: Operating System :: POSIX :: Linux
+Classifier: Operating System :: Microsoft :: Windows
+Classifier: Topic :: Scientific/Engineering :: GIS
+Project-URL: Homepage, https://github.com/uber/h3-py
+Project-URL: Documentation, https://uber.github.io/h3-py/
+Project-URL: Bug Tracker, https://github.com/uber/h3-py/issues
+Project-URL: Discussions, https://github.com/uber/h3-py/discussions
+Project-URL: Changelog, https://uber.github.io/h3-py/_changelog.html
+Requires-Python: >=3.8
+Provides-Extra: numpy
+Requires-Dist: numpy; extra == "numpy"
+Provides-Extra: test
+Requires-Dist: pytest; extra == "test"
+Requires-Dist: pytest-cov; extra == "test"
+Requires-Dist: ruff; extra == "test"
+Requires-Dist: numpy; extra == "test"
+Provides-Extra: all
+Requires-Dist: h3[test]; extra == "all"
+Requires-Dist: jupyter-book; extra == "all"
+Requires-Dist: sphinx>=7.3.3; extra == "all"
+Requires-Dist: jupyterlab; extra == "all"
+Requires-Dist: jupyterlab-geojson; extra == "all"
+Requires-Dist: geopandas; extra == "all"
+Requires-Dist: geodatasets; extra == "all"
+Requires-Dist: matplotlib; extra == "all"
+Requires-Dist: contextily; extra == "all"
+Requires-Dist: cartopy; extra == "all"
+Requires-Dist: geoviews; extra == "all"
+Description-Content-Type: text/markdown
+
+
+
+# **h3-py**: Uber's H3 Hexagonal Hierarchical Geospatial Indexing System in Python
+
+[](https://badge.fury.io/py/h3)
+[](https://pypistats.org/packages/h3)
+[](https://anaconda.org/conda-forge/h3-py)
+[](https://github.com/uber/h3/releases/tag/v4.3.0)
+[](https://github.com/uber/h3-py/blob/master/LICENSE)
+
+[](https://github.com/uber/h3-py/actions)
+[](https://github.com/uber/h3-py/blob/master/.github/workflows/lint_and_coverage.yml#L31)
+
+
+Python bindings for the [H3 core library](https://h3geo.org/).
+
+- Documentation: [uber.github.io/h3-py](https://uber.github.io/h3-py)
+- GitHub repo: [github.com/uber/h3-py](https://github.com/uber/h3-py)
+
+## Installation
+
+From [PyPI](https://pypi.org/project/h3/):
+
+```console
+pip install h3
+```
+
+From [conda](https://github.com/conda-forge/h3-py-feedstock):
+
+```console
+conda config --add channels conda-forge
+conda install h3-py
+```
+
+
+## Usage
+
+```python
+>>> import h3
+>>> lat, lng = 37.769377, -122.388903
+>>> resolution = 9
+>>> h3.latlng_to_cell(lat, lng, resolution)
+'89283082e73ffff'
+```
+
+
+## APIs
+
+[api_comparison]: https://uber.github.io/h3-py/api_comparison
+[api_quick]: https://uber.github.io/h3-py/api_quick
+
+We provide [multiple APIs][api_comparison] in `h3-py`.
+
+- All APIs have the same set of functions;
+ see the [API reference][api_quick].
+- The APIs differ only in their input/output formats;
+ see the [API comparison page][api_comparison].
+
+
+## Example gallery
+
+Browse [a collection of example notebooks](https://github.com/uber/h3-py-notebooks),
+and if you have examples or visualizations of your own, please feel free
+to contribute!
+
+[walkthrough]: https://nbviewer.jupyter.org/github/uber/h3-py-notebooks/blob/master/notebooks/usage.ipynb
+
+We also have an introductory [walkthrough of the API][walkthrough].
+
+
+## Versioning
+
+
+
+`h3-py` wraps the [H3 core library](https://github.com/uber/h3),
+which is written in C.
+The C and Python projects each employ
+[semantic versioning](https://semver.org/),
+where versions take the form `X.Y.Z`.
+
+The `h3-py` version string is guaranteed to match the C library string
+in both *major* and *minor* numbers (`X.Y`), but may differ on the
+*patch* (`Z`) number.
+This convention provides users with information on breaking changes and
+feature additions, while providing downstream bindings (like this one!)
+with the versioning freedom to fix bugs.
+
+Use `h3.versions()` to see the version numbers for both
+`h3-py` and the C library. For example,
+
+```python
+>>> import h3
+>>> h3.versions()
+{'c': '4.1.0', 'python': '4.1.1'}
+```
diff --git a/frogpilot/third_party/h3-4.3.1.dist-info/RECORD b/frogpilot/third_party/h3-4.3.1.dist-info/RECORD
new file mode 100644
index 00000000..3ff11f42
--- /dev/null
+++ b/frogpilot/third_party/h3-4.3.1.dist-info/RECORD
@@ -0,0 +1,50 @@
+h3/CMakeLists.txt,sha256=8zQ0011t8bmsJw_GiKOFbkTZnquBh6ESCMZyLrm9u_U,22
+h3/__init__.py,sha256=-i_HfsVLg1wW2wX8p74WIxHXi7NPO1GSVtjILstnS8U,575
+h3/_h3shape.py,sha256=hhsBJyuy5MqDhG_BixzbCTOfQgaO6Gf2olFEYd6iLKE,8559
+h3/_version.py,sha256=Tjq_BBPmWroGgfnrf7u9TpAWss9iA5PnWSv8kkZkJY8,88
+h3/_cy/CMakeLists.txt,sha256=-h6Dk_V8VYydmDht9zCOv7wbSOl_G4MtlajIrj2D8hE,1528
+h3/_cy/__init__.py,sha256=-f0Zqm46bnFuV0-Vs2FJMhODzbL1Oow-zuRJQS5dBFs,2488
+h3/_cy/cells.cpython-312-aarch64-linux-gnu.so,sha256=IT938GoA2i_KXkLbzfy84nZB85Jl9g4KSm7rdHWBHPY,469320
+h3/_cy/cells.pxd,sha256=9GydN6Nc4vJFU0nVyCP8BvDA4P702OX2LDQdkAnHv9I,1326
+h3/_cy/cells.pyx,sha256=TcEjufzELbfLGxQ10IA1aFX-nQt8sDx6JT_HhKQtsNc,9517
+h3/_cy/edges.cpython-312-aarch64-linux-gnu.so,sha256=8nCk6r3A9PISVMLdzHTiatYt3dqNDeFkQ-sfemWmvYM,402824
+h3/_cy/edges.pxd,sha256=_zUOkV8aeWhk_xjicSWBDHNOp3vMEeg8o2Da06iYn38,561
+h3/_cy/edges.pyx,sha256=1ECoK-OP3_BplwLZqwqNwItMLNIja6kVMzMeo7maHz8,2487
+h3/_cy/error_system.cpython-312-aarch64-linux-gnu.so,sha256=R4fc8to7fKrP7QBXX5KB7SXQzasH_vo81f7U6bGs1mk,202360
+h3/_cy/error_system.pxd,sha256=Dd2LjHcmlkCcs5Tr_HSp7xcn8YsQq2Evk4HxtIZF9m4,109
+h3/_cy/error_system.pyx,sha256=gyg7NPuY2nfsj2QnJC11OTZUCXLvNybpcgNC25Jor7E,6908
+h3/_cy/h3api.h,sha256=lQHNByNGZG6AkVjpIB2OZEv7fnAbBfCGqe4oqx0Hgd0,26418
+h3/_cy/h3lib.pxd,sha256=d37c0MiHuWQL28ogaxiLjKq1P4lh_gdIPRZrjizHi2g,6955
+h3/_cy/latlng.cpython-312-aarch64-linux-gnu.so,sha256=Nr-18UTddm3XyqtmVqrxgbWPbihilri0Od-gE2FRr1Q,471768
+h3/_cy/latlng.pxd,sha256=WjOjY4jH_2lK6hnaVXCusqLg2DvvZ4rvAJyXeCQd2uk,266
+h3/_cy/latlng.pyx,sha256=FW8L4LEr6l7qhsRL28mFakVLHnfqSCKQDQ6Ued-nLks,8370
+h3/_cy/memory.cpython-312-aarch64-linux-gnu.so,sha256=FXPAzIgzILQh4LxXlOchgkLY6_kpFWmzb3zsIFFL9lg,336208
+h3/_cy/memory.pxd,sha256=jXGN68hdcPUyA_yFCesZxCksHwmHMO4Ah-I9ZwYBvXs,236
+h3/_cy/memory.pyx,sha256=KZo65H3rXn290iH05ZNkyt78mKEGIaVjzmgYdOon_PU,7103
+h3/_cy/to_multipoly.cpython-312-aarch64-linux-gnu.so,sha256=4r6p7uq0B_Ln85xK_P9HvC59LtTR-ZAXGYXMG8CUwBk,402440
+h3/_cy/to_multipoly.pyx,sha256=bpaP3rGl5mtWTp1aH0ksL-gMi_tobSxjnZvxX6B2R4o,1369
+h3/_cy/util.cpython-312-aarch64-linux-gnu.so,sha256=zIQQ7i19XEZiT4I6hMg3RdnXxb0ZDjRCU1ohQ9pwoxY,267856
+h3/_cy/util.pxd,sha256=9JeCVVOOxfd3QoK4q_W-yIhmEASq_X2Y-A_Crn2bvZg,346
+h3/_cy/util.pyx,sha256=9woUsIwPG18UmjDbw8QFNnIXT62HMOUKGUmGIKt_ONo,2348
+h3/_cy/vertex.cpython-312-aarch64-linux-gnu.so,sha256=5-tDMKX5uWCv-2W3gVPhAwFbcB8f5q092KLuUa5oChA,337024
+h3/_cy/vertex.pxd,sha256=lnRECjEFpohFr7iB6d2UrTN-s37UzXnSpaP_UV5866I,229
+h3/_cy/vertex.pyx,sha256=_26ur2q3CZzb0BGQ85Lvbjq9dRWscvjAsKlgmCAqVHc,910
+h3/api/__init__.py,sha256=X6P438-1N_2jWlFKNZ8q_jUop7ICRhwwa4I0RuxtQIQ,114
+h3/api/basic_int/__init__.py,sha256=pg0QGCIGIyT0WTnD2vDqP-LyW0m5E1qd9F19l6wf3Rs,25924
+h3/api/basic_int/_convert.py,sha256=GmVFFTT-mMHJ_Newx-6aF8TAtKBTrXglMx5RNV9w0Nk,187
+h3/api/basic_str/__init__.py,sha256=pg0QGCIGIyT0WTnD2vDqP-LyW0m5E1qd9F19l6wf3Rs,25924
+h3/api/basic_str/_convert.py,sha256=9B7uHdbUJhDb0GUEdSg-dUQpNCmvPviK6_DX99YS5UE,256
+h3/api/memview_int/__init__.py,sha256=pg0QGCIGIyT0WTnD2vDqP-LyW0m5E1qd9F19l6wf3Rs,25924
+h3/api/memview_int/_convert.py,sha256=ef5Omw0GJsUlR8r66O1p8f9IEEhCeTdDjlvN2A0urjA,105
+h3/api/numpy_int/__init__.py,sha256=pg0QGCIGIyT0WTnD2vDqP-LyW0m5E1qd9F19l6wf3Rs,25924
+h3/api/numpy_int/_convert.py,sha256=dhZLocPjucl6Uu872k1tAcuWXBRT29oUG8qFMoleulE,288
+include/h3/h3api.h,sha256=lQHNByNGZG6AkVjpIB2OZEv7fnAbBfCGqe4oqx0Hgd0,26418
+lib64/libh3.a,sha256=daDl9sRycc5tHGPbiHyd-IYfmyaXFQKnB-xES26wv4Q,170004
+lib64/cmake/h3/h3Config.cmake,sha256=QwISc49jy-ZH9Ba1YCQnick54sapi_pZDEJjDJyMrb4,959
+lib64/cmake/h3/h3ConfigVersion.cmake,sha256=Nf3JY8h9E9o1YHZ9TuUc_XN5CyS3yxa4pp5PTIEViKM,2762
+lib64/cmake/h3/h3Targets-release.cmake,sha256=pnJffyu5LDg0GeSMTTwBoVLamlDKo8XrjrCEyN7aP0w,797
+lib64/cmake/h3/h3Targets.cmake,sha256=K18pICoM546T1tLsCSvdLlnLf1-GyFYPqMq8G0mZobM,4217
+h3-4.3.1.dist-info/METADATA,sha256=XJDmH460i2LSmjPAibQR_6XHyedrvDe_0tXZM3v-7n0,18459
+h3-4.3.1.dist-info/WHEEL,sha256=uOikQdbS4EYP0OE7pa4p-weSIqjfixseckdKxHaHlvE,198
+h3-4.3.1.dist-info/RECORD,,
+h3-4.3.1.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
diff --git a/frogpilot/third_party/h3-4.3.1.dist-info/WHEEL b/frogpilot/third_party/h3-4.3.1.dist-info/WHEEL
new file mode 100644
index 00000000..e433f053
--- /dev/null
+++ b/frogpilot/third_party/h3-4.3.1.dist-info/WHEEL
@@ -0,0 +1,7 @@
+Wheel-Version: 1.0
+Generator: scikit-build-core 0.11.5
+Root-Is-Purelib: false
+Tag: cp312-cp312-manylinux_2_17_aarch64
+Tag: cp312-cp312-manylinux2014_aarch64
+Tag: cp312-cp312-manylinux_2_28_aarch64
+
diff --git a/frogpilot/third_party/h3-4.3.1.dist-info/licenses/LICENSE b/frogpilot/third_party/h3-4.3.1.dist-info/licenses/LICENSE
new file mode 100644
index 00000000..d6456956
--- /dev/null
+++ b/frogpilot/third_party/h3-4.3.1.dist-info/licenses/LICENSE
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/frogpilot/third_party/h3/CMakeLists.txt b/frogpilot/third_party/h3/CMakeLists.txt
new file mode 100644
index 00000000..9d48e667
--- /dev/null
+++ b/frogpilot/third_party/h3/CMakeLists.txt
@@ -0,0 +1 @@
+add_subdirectory(_cy)
diff --git a/frogpilot/third_party/h3/__init__.py b/frogpilot/third_party/h3/__init__.py
new file mode 100644
index 00000000..4f7cb32f
--- /dev/null
+++ b/frogpilot/third_party/h3/__init__.py
@@ -0,0 +1,29 @@
+# flake8: noqa
+
+from .api.basic_str import *
+from ._version import __version__
+
+from ._cy import (
+ UnknownH3ErrorCode,
+ H3BaseException,
+
+ H3GridNavigationError,
+ H3MemoryError,
+ H3ValueError,
+
+ H3FailedError,
+ H3DomainError,
+ H3LatLngDomainError,
+ H3ResDomainError,
+ H3CellInvalidError,
+ H3DirEdgeInvalidError,
+ H3UndirEdgeInvalidError,
+ H3VertexInvalidError,
+ H3PentagonError,
+ H3DuplicateInputError,
+ H3NotNeighborsError,
+ H3ResMismatchError,
+ H3MemoryAllocError,
+ H3MemoryBoundsError,
+ H3OptionInvalidError,
+)
diff --git a/frogpilot/third_party/h3/_cy/CMakeLists.txt b/frogpilot/third_party/h3/_cy/CMakeLists.txt
new file mode 100644
index 00000000..2cb2b6f3
--- /dev/null
+++ b/frogpilot/third_party/h3/_cy/CMakeLists.txt
@@ -0,0 +1,53 @@
+list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR})
+
+include_directories(${CMAKE_CURRENT_SOURCE_DIR})
+
+macro(add_cython_file filename)
+ add_custom_command(
+ OUTPUT "${filename}.c"
+ COMMENT
+ "Making ${CMAKE_CURRENT_BINARY_DIR}/${filename}.c from ${CMAKE_CURRENT_SOURCE_DIR}/${filename}.pyx"
+ COMMAND Python::Interpreter -m cython
+ "${CMAKE_CURRENT_SOURCE_DIR}/${filename}.pyx" --output-file "${filename}.c" -I ${CMAKE_CURRENT_SOURCE_DIR}
+ DEPENDS "${filename}.pyx"
+ VERBATIM)
+
+ python_add_library(${filename} MODULE "${filename}.c" WITH_SOABI)
+
+ set_property(TARGET ${filename} PROPERTY C_STANDARD 99)
+ target_link_libraries(${filename} PRIVATE h3)
+ install(TARGETS ${filename} LIBRARY DESTINATION ${SKBUILD_PROJECT_NAME}/_cy)
+endmacro()
+
+# GLOB pattern is recommended against
+# https://cmake.org/cmake/help/v3.14/command/file.html?highlight=file#filesystem
+add_cython_file(cells)
+add_cython_file(edges)
+add_cython_file(error_system)
+add_cython_file(latlng)
+add_cython_file(memory)
+add_cython_file(vertex)
+
+add_cython_file(to_multipoly)
+add_cython_file(util)
+
+# Include pyx and pxd files in distribution for use by Cython API
+install(
+ FILES
+ cells.pxd
+ cells.pyx
+ edges.pxd
+ edges.pyx
+ error_system.pyx
+ h3lib.pxd
+ latlng.pxd
+ latlng.pyx
+ memory.pxd
+ memory.pyx
+ util.pxd
+ util.pyx
+ vertex.pxd
+ vertex.pyx
+ DESTINATION
+ ${SKBUILD_PROJECT_NAME}/_cy
+)
diff --git a/frogpilot/third_party/h3/_cy/__init__.py b/frogpilot/third_party/h3/_cy/__init__.py
new file mode 100644
index 00000000..627f3b97
--- /dev/null
+++ b/frogpilot/third_party/h3/_cy/__init__.py
@@ -0,0 +1,112 @@
+# flake8: noqa
+
+"""
+This module should serve as the interface between the C/Cython code and
+the Python code. That is, it is an internal API.
+This module should import all the Cython functions we
+intend to expose to be used in pure Python code, and each of the H3-py
+APIs should *only* reference functions and symbols listed here.
+
+These functions should handle input validation, guard against the
+possibility of segfaults, raise appropriate errors, and handle memory
+management. The API wrapping code around this should focus on the cosmetic
+function interface and input conversion (string to int, for instance).
+"""
+
+from .cells import (
+ is_valid_cell,
+ is_pentagon,
+ get_base_cell_number,
+ get_resolution,
+ cell_to_parent,
+ grid_distance,
+ grid_disk,
+ grid_ring,
+ cell_to_children_size,
+ cell_to_children,
+ cell_to_child_pos,
+ child_pos_to_cell,
+ compact_cells,
+ uncompact_cells,
+ get_num_cells,
+ average_hexagon_area,
+ cell_area,
+ grid_path_cells,
+ is_res_class_iii,
+ get_pentagons,
+ get_res0_cells,
+ cell_to_center_child,
+ get_icosahedron_faces,
+ cell_to_local_ij,
+ local_ij_to_cell,
+)
+
+from .edges import (
+ are_neighbor_cells,
+ cells_to_directed_edge,
+ is_valid_directed_edge,
+ get_directed_edge_origin,
+ get_directed_edge_destination,
+ directed_edge_to_cells,
+ origin_to_directed_edges,
+ average_hexagon_edge_length,
+ edge_length,
+)
+
+from .latlng import (
+ latlng_to_cell,
+ cell_to_latlng,
+ polygon_to_cells,
+ polygons_to_cells,
+ polygon_to_cells_experimental,
+ polygons_to_cells_experimental,
+ cell_to_boundary,
+ directed_edge_to_boundary,
+ great_circle_distance,
+)
+
+from .vertex import (
+ cell_to_vertex,
+ cell_to_vertexes,
+ vertex_to_latlng,
+ is_valid_vertex,
+)
+
+from .to_multipoly import (
+ cells_to_multi_polygon
+)
+
+from .util import (
+ c_version,
+ str_to_int,
+ int_to_str,
+)
+
+from .memory import (
+ iter_to_mv,
+)
+
+from .error_system import (
+ UnknownH3ErrorCode,
+ H3BaseException,
+
+ H3GridNavigationError,
+ H3MemoryError,
+ H3ValueError,
+
+ H3FailedError,
+ H3DomainError,
+ H3LatLngDomainError,
+ H3ResDomainError,
+ H3CellInvalidError,
+ H3DirEdgeInvalidError,
+ H3UndirEdgeInvalidError,
+ H3VertexInvalidError,
+ H3PentagonError,
+ H3DuplicateInputError,
+ H3NotNeighborsError,
+ H3ResMismatchError,
+ H3MemoryAllocError,
+ H3MemoryBoundsError,
+ H3OptionInvalidError,
+)
diff --git a/frogpilot/third_party/h3/_cy/cells.cpython-312-aarch64-linux-gnu.so b/frogpilot/third_party/h3/_cy/cells.cpython-312-aarch64-linux-gnu.so
new file mode 100644
index 00000000..b0207a5c
Binary files /dev/null and b/frogpilot/third_party/h3/_cy/cells.cpython-312-aarch64-linux-gnu.so differ
diff --git a/frogpilot/third_party/h3/_cy/cells.pxd b/frogpilot/third_party/h3/_cy/cells.pxd
new file mode 100644
index 00000000..06b35ee6
--- /dev/null
+++ b/frogpilot/third_party/h3/_cy/cells.pxd
@@ -0,0 +1,27 @@
+from .h3lib cimport bool, int64_t, H3int
+
+cpdef bool is_valid_cell(H3int h)
+cpdef bool is_pentagon(H3int h)
+cpdef int get_base_cell_number(H3int h) except -1
+cpdef int get_resolution(H3int h) except -1
+cpdef int grid_distance(H3int h1, H3int h2) except -1
+cpdef H3int[:] grid_disk(H3int h, int k)
+cpdef H3int[:] grid_ring(H3int h, int k)
+cpdef H3int cell_to_parent(H3int h, res=*) except 0
+cpdef int64_t cell_to_children_size(H3int h, res=*) except -1
+cpdef H3int[:] cell_to_children(H3int h, res=*)
+cpdef H3int cell_to_center_child(H3int h, res=*) except 0
+cpdef int64_t cell_to_child_pos(H3int child, int parent_res) except -1
+cpdef H3int child_pos_to_cell(H3int parent, int child_res, int64_t child_pos) except 0
+cpdef H3int[:] compact_cells(const H3int[:] hu)
+cpdef H3int[:] uncompact_cells(const H3int[:] hc, int res)
+cpdef int64_t get_num_cells(int resolution) except -1
+cpdef double average_hexagon_area(int resolution, unit=*) except -1
+cpdef double cell_area(H3int h, unit=*) except -1
+cpdef H3int[:] grid_path_cells(H3int start, H3int end)
+cpdef bool is_res_class_iii(H3int h)
+cpdef H3int[:] get_pentagons(int res)
+cpdef H3int[:] get_res0_cells()
+cpdef get_icosahedron_faces(H3int h)
+cpdef (int, int) cell_to_local_ij(H3int origin, H3int h) except *
+cpdef H3int local_ij_to_cell(H3int origin, int i, int j) except 0
diff --git a/frogpilot/third_party/h3/_cy/cells.pyx b/frogpilot/third_party/h3/_cy/cells.pyx
new file mode 100644
index 00000000..928b665c
--- /dev/null
+++ b/frogpilot/third_party/h3/_cy/cells.pyx
@@ -0,0 +1,419 @@
+cimport h3lib
+from .h3lib cimport bool, int64_t, H3int, H3ErrorCodes
+
+from .util cimport (
+ check_cell,
+ check_res, # we don't use?
+ check_distance,
+)
+
+from .error_system cimport (
+ check_for_error,
+ check_for_error_msg,
+)
+
+from .memory cimport (
+ H3MemoryManager,
+ int_mv,
+)
+
+# todo: add notes about Cython exception handling
+
+
+# bool is a python type, so we don't need the except clause
+cpdef bool is_valid_cell(H3int h):
+ """Validates an H3 cell (hexagon or pentagon)
+
+ Returns
+ -------
+ boolean
+ """
+ return h3lib.isValidCell(h) == 1
+
+
+cpdef bool is_pentagon(H3int h):
+ return h3lib.isPentagon(h) == 1
+
+
+cpdef int get_base_cell_number(H3int h) except -1:
+ check_cell(h)
+
+ return h3lib.getBaseCellNumber(h)
+
+
+cpdef int get_resolution(H3int h) except -1:
+ """Returns the resolution of an H3 Index
+ 0--15
+ """
+ check_cell(h)
+
+ return h3lib.getResolution(h)
+
+
+cpdef int grid_distance(H3int h1, H3int h2) except -1:
+ """ Compute the grid distance between two cells
+ """
+ cdef:
+ int64_t distance
+
+ check_cell(h1)
+ check_cell(h2)
+
+ check_for_error(
+ h3lib.gridDistance(h1, h2, &distance)
+ )
+
+ return distance
+
+cpdef H3int[:] grid_disk(H3int h, int k):
+ """ Return cells at grid distance `<= k` from `h`.
+ """
+ cdef:
+ int64_t n
+
+ check_cell(h)
+ check_distance(k)
+
+ check_for_error(
+ h3lib.maxGridDiskSize(k, &n)
+ )
+
+ hmm = H3MemoryManager(n)
+ check_for_error(
+ h3lib.gridDisk(h, k, hmm.ptr)
+ )
+ mv = hmm.to_mv()
+
+ return mv
+
+
+cpdef H3int[:] grid_ring(H3int h, int k):
+ """ Return cells at grid distance `== k` from `h`.
+ Collection is "hollow" for k >= 1.
+ """
+ check_cell(h)
+ check_distance(k)
+
+ n = 6*k if k > 0 else 1
+ hmm = H3MemoryManager(n)
+ check_for_error(
+ h3lib.gridRing(h, k, hmm.ptr)
+ )
+ mv = hmm.to_mv()
+
+ return mv
+
+
+cpdef H3int cell_to_parent(H3int h, res=None) except 0:
+ cdef:
+ H3int parent
+
+ check_cell(h)
+ if res is None:
+ res = get_resolution(h) - 1
+
+ err = h3lib.cellToParent(h, res, &parent)
+ if err:
+ msg = 'Invalid parent resolution {} for cell {}.'
+ msg = msg.format(res, hex(h))
+ check_for_error_msg(err, msg)
+
+ return parent
+
+
+cpdef int64_t cell_to_children_size(H3int h, res=None) except -1:
+ cdef:
+ int64_t n
+
+ check_cell(h)
+ if res is None:
+ res = get_resolution(h) + 1
+
+ err = h3lib.cellToChildrenSize(h, res, &n)
+ if err:
+ msg = 'Invalid child resolution {} for cell {}.'
+ msg = msg.format(res, hex(h))
+ check_for_error_msg(err, msg)
+
+ return n
+
+
+cpdef H3int[:] cell_to_children(H3int h, res=None):
+ check_cell(h)
+ if res is None:
+ res = get_resolution(h) + 1
+
+ n = cell_to_children_size(h, res)
+
+ hmm = H3MemoryManager(n)
+ check_for_error(
+ h3lib.cellToChildren(h, res, hmm.ptr)
+ )
+ mv = hmm.to_mv()
+
+ return mv
+
+
+
+cpdef H3int cell_to_center_child(H3int h, res=None) except 0:
+ cdef:
+ H3int child
+
+ check_cell(h)
+ if res is None:
+ res = get_resolution(h) + 1
+
+ err = h3lib.cellToCenterChild(h, res, &child)
+ if err:
+ msg = 'Invalid child resolution {} for cell {}.'
+ msg = msg.format(res, hex(h))
+ check_for_error_msg(err, msg)
+
+ return child
+
+
+cpdef int64_t cell_to_child_pos(H3int child, int parent_res) except -1:
+ cdef:
+ int64_t child_pos
+
+ check_cell(child)
+ err = h3lib.cellToChildPos(child, parent_res, &child_pos)
+ if err:
+ msg = "Couldn't find child pos of cell {} at res {}."
+ msg = msg.format(hex(child), parent_res)
+ check_for_error_msg(err, msg)
+
+ return child_pos
+
+
+cpdef H3int child_pos_to_cell(H3int parent, int child_res, int64_t child_pos) except 0:
+ cdef:
+ H3int child
+
+ check_cell(parent)
+ err = h3lib.childPosToCell(child_pos, parent, child_res, &child)
+ if err:
+ msg = "Couldn't find child with pos {} at res {} from parent {}."
+ msg = msg.format(child_pos, child_res, hex(parent))
+ check_for_error_msg(err, msg)
+
+ return child
+
+
+cpdef H3int[:] compact_cells(const H3int[:] hu):
+ # todo: fix this with my own Cython object "wrapper" class?
+ # everything has a .ptr interface?
+ # todo: the Clib can handle 0-len arrays because it **avoids**
+ # dereferencing the pointer, but Cython's syntax of
+ # `&hu[0]` **requires** a dereference. For Cython, checking for array
+ # length of zero and returning early seems like the easiest solution.
+ # note: open to better ideas!
+
+ if len(hu) == 0:
+ return H3MemoryManager(0).to_mv()
+
+ for h in hu: ## todo: should we have an array version? would that be faster?
+ check_cell(h)
+
+ cdef size_t n = len(hu)
+ hmm = H3MemoryManager(n)
+ check_for_error(
+ h3lib.compactCells(&hu[0], hmm.ptr, n)
+ )
+ mv = hmm.to_mv()
+
+ return mv
+
+
+# todo: https://stackoverflow.com/questions/50684977/cython-exception-type-for-a-function-returning-a-typed-memoryview
+# apparently, memoryviews are python objects, so we don't need to do the except clause
+cpdef H3int[:] uncompact_cells(const H3int[:] hc, int res):
+ # todo: the Clib can handle 0-len arrays because it **avoids**
+ # dereferencing the pointer, but Cython's syntax of
+ # `&hc[0]` **requires** a dereference. For Cython, checking for array
+ # length of zero and returning early seems like the easiest solution.
+ # note: open to better ideas!
+ cdef:
+ int64_t n
+
+
+ if len(hc) == 0:
+ return H3MemoryManager(0).to_mv()
+
+ for h in hc:
+ check_cell(h)
+
+ check_for_error(
+ h3lib.uncompactCellsSize(&hc[0], len(hc), res, &n)
+ )
+
+ hmm = H3MemoryManager(n)
+ check_for_error(
+ h3lib.uncompactCells(
+ &hc[0], # todo: symmetry here with the wrapper object might be nice. hc.ptr / hc.n
+ len(hc),
+ hmm.ptr,
+ hmm.n,
+ res
+ )
+ )
+
+ mv = hmm.to_mv()
+
+ return mv
+
+
+cpdef int64_t get_num_cells(int resolution) except -1:
+ cdef:
+ int64_t num_cells
+
+ check_for_error(
+ h3lib.getNumCells(resolution, &num_cells)
+ )
+
+ return num_cells
+
+
+cpdef double average_hexagon_area(int resolution, unit='km^2') except -1:
+ cdef:
+ double area
+
+ check_for_error(
+ h3lib.getHexagonAreaAvgKm2(resolution, &area)
+ )
+
+ # todo: multiple units
+ convert = {
+ 'km^2': 1.0,
+ 'm^2': 1000*1000.0
+ }
+
+ try:
+ area *= convert[unit]
+ except:
+ raise ValueError('Unknown unit: {}'.format(unit))
+
+ return area
+
+
+cpdef double cell_area(H3int h, unit='km^2') except -1:
+ cdef:
+ double area
+
+ if unit == 'rads^2':
+ err = h3lib.cellAreaRads2(h, &area)
+ elif unit == 'km^2':
+ err = h3lib.cellAreaKm2(h, &area)
+ elif unit == 'm^2':
+ err = h3lib.cellAreaM2(h, &area)
+ else:
+ raise ValueError('Unknown unit: {}'.format(unit))
+
+ check_for_error(err)
+
+ return area
+
+
+cdef _could_not_find_line(err, start, end):
+ msg = "Couldn't find line between cells {} and {}"
+ msg = msg.format(hex(start), hex(end))
+
+ check_for_error_msg(err, msg)
+
+cpdef H3int[:] grid_path_cells(H3int start, H3int end):
+ cdef:
+ int64_t n
+
+ # todo: can we segfault here with invalid inputs?
+ # Can we trust the c library to validate the start/end cells?
+ # probably applies to all size/work pairs of functions...
+ err = h3lib.gridPathCellsSize(start, end, &n)
+
+ _could_not_find_line(err, start, end)
+
+ hmm = H3MemoryManager(n)
+ err = h3lib.gridPathCells(start, end, hmm.ptr)
+
+ _could_not_find_line(err, start, end)
+
+ # todo: probably here too?
+ mv = hmm.to_mv()
+
+ return mv
+
+cpdef bool is_res_class_iii(H3int h):
+ return h3lib.isResClassIII(h) == 1
+
+
+cpdef H3int[:] get_pentagons(int res):
+ n = h3lib.pentagonCount()
+
+ hmm = H3MemoryManager(n)
+ check_for_error(
+ h3lib.getPentagons(res, hmm.ptr)
+ )
+ mv = hmm.to_mv()
+
+ return mv
+
+
+cpdef H3int[:] get_res0_cells():
+ n = h3lib.res0CellCount()
+
+ hmm = H3MemoryManager(n)
+ check_for_error(
+ h3lib.getRes0Cells(hmm.ptr)
+ )
+ mv = hmm.to_mv()
+
+ return mv
+
+# oh, this is returning a set??
+# todo: convert to int[:]?
+cpdef get_icosahedron_faces(H3int h):
+ cdef:
+ int n
+ int[:] faces ## todo: weird, this needs to be specified to avoid errors. cython bug?
+
+ check_for_error(
+ h3lib.maxFaceCount(h, &n)
+ )
+
+ faces = int_mv(n)
+ check_for_error(
+ h3lib.getIcosahedronFaces(h, &faces[0])
+ )
+
+ # todo: wait? do faces start from 0 or 1?
+ # we could do this check/processing in the int_mv object
+ out = [f for f in faces if f >= 0]
+
+ return out
+
+
+cpdef (int, int) cell_to_local_ij(H3int origin, H3int h) except *:
+ cdef:
+ h3lib.CoordIJ c
+
+ err = h3lib.cellToLocalIj(origin, h, 0, &c)
+ if err:
+ msg = "Couldn't find local (i,j) between cells {} and {}."
+ msg = msg.format(hex(origin), hex(h))
+ check_for_error_msg(err, msg)
+
+ return c.i, c.j
+
+cpdef H3int local_ij_to_cell(H3int origin, int i, int j) except 0:
+ cdef:
+ h3lib.CoordIJ c
+ H3int out
+
+ c.i, c.j = i, j
+
+ err = h3lib.localIjToCell(origin, &c, 0, &out)
+ if err:
+ msg = "Couldn't find cell at local ({},{}) from cell {}."
+ msg = msg.format(i, j, hex(origin))
+ check_for_error_msg(err, msg)
+
+ return out
diff --git a/frogpilot/third_party/h3/_cy/edges.cpython-312-aarch64-linux-gnu.so b/frogpilot/third_party/h3/_cy/edges.cpython-312-aarch64-linux-gnu.so
new file mode 100644
index 00000000..0865612e
Binary files /dev/null and b/frogpilot/third_party/h3/_cy/edges.cpython-312-aarch64-linux-gnu.so differ
diff --git a/frogpilot/third_party/h3/_cy/edges.pxd b/frogpilot/third_party/h3/_cy/edges.pxd
new file mode 100644
index 00000000..b7fcf849
--- /dev/null
+++ b/frogpilot/third_party/h3/_cy/edges.pxd
@@ -0,0 +1,11 @@
+from .h3lib cimport bool, H3int
+
+cpdef bool are_neighbor_cells(H3int h1, H3int h2)
+cpdef H3int cells_to_directed_edge(H3int origin, H3int destination) except *
+cpdef bool is_valid_directed_edge(H3int e)
+cpdef H3int get_directed_edge_origin(H3int e) except 1
+cpdef H3int get_directed_edge_destination(H3int e) except 1
+cpdef (H3int, H3int) directed_edge_to_cells(H3int e) except *
+cpdef H3int[:] origin_to_directed_edges(H3int origin)
+cpdef double average_hexagon_edge_length(int resolution, unit=*) except -1
+cpdef double edge_length(H3int e, unit=*) except -1
diff --git a/frogpilot/third_party/h3/_cy/edges.pyx b/frogpilot/third_party/h3/_cy/edges.pyx
new file mode 100644
index 00000000..bd0aa7c0
--- /dev/null
+++ b/frogpilot/third_party/h3/_cy/edges.pyx
@@ -0,0 +1,114 @@
+cimport h3lib
+from .h3lib cimport bool, H3int
+
+from .error_system cimport check_for_error
+
+from .memory cimport H3MemoryManager
+
+# todo: make bint
+cpdef bool are_neighbor_cells(H3int h1, H3int h2):
+ cdef:
+ int out
+
+ err = h3lib.areNeighborCells(h1, h2, &out)
+
+ # note: we are intentionally not raising an error here, and just
+ # returning false.
+ # todo: is this choice consistent across the Python and C libs?
+ if err:
+ return False
+
+ return out == 1
+
+
+cpdef H3int cells_to_directed_edge(H3int origin, H3int destination) except *:
+ cdef:
+ int neighbor_out
+ H3int out
+
+ check_for_error(
+ h3lib.cellsToDirectedEdge(origin, destination, &out)
+ )
+
+ return out
+
+
+cpdef bool is_valid_directed_edge(H3int e):
+ return h3lib.isValidDirectedEdge(e) == 1
+
+cpdef H3int get_directed_edge_origin(H3int e) except 1:
+ cdef:
+ H3int out
+
+ check_for_error(
+ h3lib.getDirectedEdgeOrigin(e, &out)
+ )
+
+ return out
+
+cpdef H3int get_directed_edge_destination(H3int e) except 1:
+ cdef:
+ H3int out
+
+ check_for_error(
+ h3lib.getDirectedEdgeDestination(e, &out)
+ )
+
+ return out
+
+cpdef (H3int, H3int) directed_edge_to_cells(H3int e) except *:
+ # todo: use directed_edge_to_cells in h3lib
+ return get_directed_edge_origin(e), get_directed_edge_destination(e)
+
+cpdef H3int[:] origin_to_directed_edges(H3int origin):
+ """ Returns the 6 (or 5 for pentagons) directed edges
+ for the given origin cell
+ """
+
+ hmm = H3MemoryManager(6)
+ check_for_error(
+ h3lib.originToDirectedEdges(origin, hmm.ptr)
+ )
+ mv = hmm.to_mv()
+
+ return mv
+
+
+cpdef double average_hexagon_edge_length(int resolution, unit='km') except -1:
+ cdef:
+ double length
+
+ check_for_error(
+ h3lib.getHexagonEdgeLengthAvgKm(resolution, &length)
+ )
+
+ # todo: multiple units
+ convert = {
+ 'km': 1.0,
+ 'm': 1000.0
+ }
+
+ try:
+ length *= convert[unit]
+ except:
+ raise ValueError('Unknown unit: {}'.format(unit))
+
+ return length
+
+
+cpdef double edge_length(H3int e, unit='km') except -1:
+ cdef:
+ double length
+
+ if unit == 'rads':
+ err = h3lib.edgeLengthRads(e, &length)
+ elif unit == 'km':
+ err = h3lib.edgeLengthKm(e, &length)
+ elif unit == 'm':
+ err = h3lib.edgeLengthM(e, &length)
+ else:
+ raise ValueError('Unknown unit: {}'.format(unit))
+
+ check_for_error(err)
+
+ return length
diff --git a/frogpilot/third_party/h3/_cy/error_system.cpython-312-aarch64-linux-gnu.so b/frogpilot/third_party/h3/_cy/error_system.cpython-312-aarch64-linux-gnu.so
new file mode 100644
index 00000000..9d73f453
Binary files /dev/null and b/frogpilot/third_party/h3/_cy/error_system.cpython-312-aarch64-linux-gnu.so differ
diff --git a/frogpilot/third_party/h3/_cy/error_system.pxd b/frogpilot/third_party/h3/_cy/error_system.pxd
new file mode 100644
index 00000000..af8c709b
--- /dev/null
+++ b/frogpilot/third_party/h3/_cy/error_system.pxd
@@ -0,0 +1,3 @@
+from .h3lib cimport H3Error
+cdef check_for_error(H3Error err)
+cdef check_for_error_msg(H3Error err, str msg)
diff --git a/frogpilot/third_party/h3/_cy/error_system.pyx b/frogpilot/third_party/h3/_cy/error_system.pyx
new file mode 100644
index 00000000..fbdd9185
--- /dev/null
+++ b/frogpilot/third_party/h3/_cy/error_system.pyx
@@ -0,0 +1,231 @@
+"""
+Exceptions from the h3-py library have three possible sources:
+
+- the Python code
+- the Cython code
+- the underlying H3 C library code
+
+The Python and Cython `h3-py` code will only raise standard Python
+built-in exceptions; **no custom** exception classes will be used.
+
+Conversely, many functions in the H3 C library return a `uint32_t`
+error code (aliased as type `H3Error`).
+When these errors happen (and `h3-py` can't recover from them internally),
+they are passed up to the Python/Cython code, where their
+`uint32_t` error values are converted to **custom** Python exception types.
+These custom exception classes all inherit from `H3BaseException`.
+
+There is a 1-1 correspondence between the concrete subclasses of
+`H3BaseException` and the H3 C library `H3ErrorCodes` values.
+The correspondence is intentional, so that the user can refer to the
+H3 C library documentation on these errors.
+
+The (`uint32_t` <-> Exception) correspondence should be clear from
+the names of each error/exception, but the explicit mapping is given by
+a dictionary in the code below.
+
+Note that some "abstract" subclasses of `H3BaseException` are also included to
+group the exceptions by type. (We say "abstract" because Python has no easy
+way to make true abstract exception classes.)
+
+These "abstract" exceptions will never be raised directly by `h3-py`, but they
+allow the user to catch general groups of errors.
+Note that `h3-py` will only ever directly raise
+the "concrete" exception classes.
+
+Summarizing, all exceptions originating from the C library inherit from
+`H3BaseException`, which has both "abstract" and "concrete" subclasses.
+
+**Abstract classes**:
+
+- H3BaseException
+- H3ValueError
+- H3MemoryError
+- H3GridNavigationError
+
+**Concrete classes**:
+
+- H3FailedError
+- H3DomainError
+- H3LatLngDomainError
+- H3ResDomainError
+- H3CellInvalidError
+- H3DirEdgeInvalidError
+- H3UndirEdgeInvalidError
+- H3VertexInvalidError
+- H3PentagonError
+- H3DuplicateInputError
+- H3NotNeighborsError
+- H3ResMismatchError
+- H3MemoryAllocError
+- H3MemoryBoundsError
+- H3OptionInvalidError
+
+
+# TODO: add tests verifying that concrete exception classes have the right error codes associated with them
+"""
+
+from contextlib import contextmanager
+
+from .h3lib cimport (
+ H3Error,
+
+ # H3ErrorCodes enum values
+ E_SUCCESS,
+ E_FAILED,
+ E_DOMAIN,
+ E_LATLNG_DOMAIN,
+ E_RES_DOMAIN,
+ E_CELL_INVALID,
+ E_DIR_EDGE_INVALID,
+ E_UNDIR_EDGE_INVALID,
+ E_VERTEX_INVALID,
+ E_PENTAGON,
+ E_DUPLICATE_INPUT,
+ E_NOT_NEIGHBORS,
+ E_RES_MISMATCH,
+ E_MEMORY_ALLOC,
+ E_MEMORY_BOUNDS,
+ E_OPTION_INVALID,
+)
+
+@contextmanager
+def _the_error(obj):
+ """
+ Syntactic maple syrup for grouping exception definitions.
+ The associated `with` statement ends up as a not-half-bad
+ approximation to a valid sentence fragment.
+
+ This provides sort of a "pretend scope", in that it allows for
+ block indentation which helps to visually indicate the "scope"
+ of the `... as e` statement. Just note that Python doesn't treat the
+ `with` block as a "true" separate scope.
+
+ Note that this doesn't actually do anything context-manager-y, outside
+ of the variable assignment and block indentation.
+ """
+ yield obj
+
+
+#
+# Base exception for C library error codes
+#
+class H3BaseException(Exception):
+ """ Base H3 exception class.
+
+ Concrete subclasses of this class correspond to specific
+ error codes from the C library.
+
+ Base/abstract subclasses will have `h3_error_code = None`, while
+ concrete subclasses will have `h3_error_code` equal to their associated
+ C library error code.
+ """
+ h3_error_code = None
+
+
+#
+# A few "abstract" exceptions; organizational.
+#
+with _the_error(H3BaseException) as e:
+ class H3ValueError(e, ValueError): ...
+ class H3MemoryError(e, MemoryError): ...
+ class H3GridNavigationError(e, RuntimeError): ...
+
+
+#
+# Concrete exceptions
+#
+class UnknownH3ErrorCode(H3BaseException):
+ """
+ Indicates that the h3-py Python bindings have received an
+ unrecognized error code from the C library.
+
+ This should never happen. Please report if you get this error.
+
+ Note that this exception is *outside* of the
+ H3BaseException class hierarchy.
+ """
+ pass
+
+with _the_error(H3BaseException) as e:
+ class H3FailedError(e): ...
+
+with _the_error(H3GridNavigationError) as e:
+ class H3PentagonError(e): ...
+
+with _the_error(H3MemoryError) as e:
+ class H3MemoryAllocError(e): ...
+ class H3MemoryBoundsError(e): ...
+
+with _the_error(H3ValueError) as e:
+ class H3DomainError(e): ...
+ class H3LatLngDomainError(e): ...
+ class H3ResDomainError(e): ...
+ class H3CellInvalidError(e): ...
+ class H3DirEdgeInvalidError(e): ...
+ class H3UndirEdgeInvalidError(e): ...
+ class H3VertexInvalidError(e): ...
+ class H3DuplicateInputError(e): ...
+ class H3NotNeighborsError(e): ...
+ class H3ResMismatchError(e): ...
+ class H3OptionInvalidError(e): ...
+
+
+"""
+This defines a mapping between uint32_t error codes and concrete Python
+exception classes.
+Note that we intentionally omit E_SUCCESS, as it isn't an actual error.
+"""
+error_mapping = {
+ E_FAILED: H3FailedError,
+ E_DOMAIN: H3DomainError,
+ E_LATLNG_DOMAIN: H3LatLngDomainError,
+ E_RES_DOMAIN: H3ResDomainError,
+ E_CELL_INVALID: H3CellInvalidError,
+ E_DIR_EDGE_INVALID: H3DirEdgeInvalidError,
+ E_UNDIR_EDGE_INVALID: H3UndirEdgeInvalidError,
+ E_VERTEX_INVALID: H3VertexInvalidError,
+ E_PENTAGON: H3PentagonError,
+ E_DUPLICATE_INPUT: H3DuplicateInputError,
+ E_NOT_NEIGHBORS: H3NotNeighborsError,
+ E_RES_MISMATCH: H3ResMismatchError,
+ E_MEMORY_ALLOC: H3MemoryAllocError,
+ E_MEMORY_BOUNDS: H3MemoryBoundsError,
+ E_OPTION_INVALID: H3OptionInvalidError,
+}
+
+# Go back and modify the class definitions so that each concrete exception
+# stores its associated error code.
+for code, ex in error_mapping.items():
+ ex.h3_error_code = code
+
+
+#
+# Helper functions
+#
+
+# TODO: Move the helpers to util?
+# TODO: Unclear how/where to expose these functions. cdef/cpdef?
+
+cdef code_to_exception(H3Error err):
+ if err == E_SUCCESS:
+ return None
+ elif err in error_mapping:
+ return error_mapping[err]
+ else:
+ raise UnknownH3ErrorCode(err)
+
+cdef check_for_error(H3Error err):
+ ex = code_to_exception(err)
+ if ex:
+ raise ex
+
+# todo: There's no easy way to do `*args` in `cdef` functions, but I'm also
+# not sure this even needs to be a Cython `cdef` function at all, or that
+# any of the other helper functions need to be in Cython.
+# todo: Revisit after we've played with this a bit.
+# todo: also: maybe the extra messages aren't that much more helpful...
+cdef check_for_error_msg(H3Error err, str msg):
+ ex = code_to_exception(err)
+ if ex:
+ raise ex(msg)
diff --git a/frogpilot/third_party/h3/_cy/h3api.h b/frogpilot/third_party/h3/_cy/h3api.h
new file mode 100644
index 00000000..719b252e
--- /dev/null
+++ b/frogpilot/third_party/h3/_cy/h3api.h
@@ -0,0 +1,809 @@
+/*
+ * Copyright 2016-2021 Uber Technologies, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/** @file h3api.h
+ * @brief Primary H3 core library entry points.
+ *
+ * This file defines the public API of the H3 library. Incompatible changes to
+ * these functions require the library's major version be increased.
+ */
+
+#ifndef H3API_H
+#define H3API_H
+
+/*
+ * Preprocessor code to support renaming (prefixing) the public API.
+ * All public functions should be wrapped in H3_EXPORT so they can be
+ * renamed.
+ */
+#ifdef H3_PREFIX
+#define XTJOIN(a, b) a##b
+#define TJOIN(a, b) XTJOIN(a, b)
+
+/* export joins the user provided prefix with our exported function name */
+#define H3_EXPORT(name) TJOIN(H3_PREFIX, name)
+#else
+#define H3_EXPORT(name) name
+#endif
+
+/* Windows DLL requires attributes indicating what to export */
+#if _WIN32 && BUILD_SHARED_LIBS
+#if BUILDING_H3
+#define DECLSPEC __declspec(dllexport)
+#else
+#define DECLSPEC __declspec(dllimport)
+#endif
+#else
+#define DECLSPEC
+#endif
+
+/* For uint64_t */
+#include
+/* For size_t */
+#include
+
+/*
+ * H3 is compiled as C, not C++ code. `extern "C"` is needed for C++ code
+ * to be able to use the library.
+ */
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/** @brief Identifier for an object (cell, edge, etc) in the H3 system.
+ *
+ * The H3Index fits within a 64-bit unsigned integer.
+ */
+typedef uint64_t H3Index;
+
+/**
+ * Invalid index used to indicate an error from latLngToCell and related
+ * functions or missing data in arrays of H3 indices. Analogous to NaN in
+ * floating point.
+ */
+#define H3_NULL 0
+
+/** @brief Result code (success or specific error) from an H3 operation */
+typedef uint32_t H3Error;
+
+typedef enum {
+ E_SUCCESS = 0, // Success (no error)
+ E_FAILED =
+ 1, // The operation failed but a more specific error is not available
+ E_DOMAIN = 2, // Argument was outside of acceptable range (when a more
+ // specific error code is not available)
+ E_LATLNG_DOMAIN =
+ 3, // Latitude or longitude arguments were outside of acceptable range
+ E_RES_DOMAIN = 4, // Resolution argument was outside of acceptable range
+ E_CELL_INVALID = 5, // `H3Index` cell argument was not valid
+ E_DIR_EDGE_INVALID = 6, // `H3Index` directed edge argument was not valid
+ E_UNDIR_EDGE_INVALID =
+ 7, // `H3Index` undirected edge argument was not valid
+ E_VERTEX_INVALID = 8, // `H3Index` vertex argument was not valid
+ E_PENTAGON = 9, // Pentagon distortion was encountered which the algorithm
+ // could not handle it
+ E_DUPLICATE_INPUT = 10, // Duplicate input was encountered in the arguments
+ // and the algorithm could not handle it
+ E_NOT_NEIGHBORS = 11, // `H3Index` cell arguments were not neighbors
+ E_RES_MISMATCH =
+ 12, // `H3Index` cell arguments had incompatible resolutions
+ E_MEMORY_ALLOC = 13, // Necessary memory allocation failed
+ E_MEMORY_BOUNDS = 14, // Bounds of provided memory were not large enough
+ E_OPTION_INVALID = 15 // Mode or flags argument was not valid.
+} H3ErrorCodes;
+
+/** @defgroup describeH3Error describeH3Error
+ * Functions for describeH3Error
+ * @{
+ */
+/** @brief converts the provided H3Error value into a description string */
+DECLSPEC const char *H3_EXPORT(describeH3Error)(H3Error err);
+/** @} */
+
+/* library version numbers generated from VERSION file */
+// clang-format off
+#define H3_VERSION_MAJOR 4
+#define H3_VERSION_MINOR 3
+#define H3_VERSION_PATCH 0
+// clang-format on
+
+/** Maximum number of cell boundary vertices; worst case is pentagon:
+ * 5 original verts + 5 edge crossings
+ */
+#define MAX_CELL_BNDRY_VERTS 10
+
+/** @struct LatLng
+ @brief latitude/longitude in radians
+*/
+typedef struct {
+ double lat; ///< latitude in radians
+ double lng; ///< longitude in radians
+} LatLng;
+
+/** @struct CellBoundary
+ @brief cell boundary in latitude/longitude
+*/
+typedef struct {
+ int numVerts; ///< number of vertices
+ LatLng verts[MAX_CELL_BNDRY_VERTS]; ///< vertices in ccw order
+} CellBoundary;
+
+/** @struct GeoLoop
+ * @brief similar to CellBoundary, but requires more alloc work
+ */
+typedef struct {
+ int numVerts;
+ LatLng *verts;
+} GeoLoop;
+
+/** @struct GeoPolygon
+ * @brief Simplified core of GeoJSON Polygon coordinates definition
+ */
+typedef struct {
+ GeoLoop geoloop; ///< exterior boundary of the polygon
+ int numHoles; ///< number of elements in the array pointed to by holes
+ GeoLoop *holes; ///< interior boundaries (holes) in the polygon
+} GeoPolygon;
+
+/** @struct GeoMultiPolygon
+ * @brief Simplified core of GeoJSON MultiPolygon coordinates definition
+ */
+typedef struct {
+ int numPolygons;
+ GeoPolygon *polygons;
+} GeoMultiPolygon;
+
+/**
+ * Values representing polyfill containment modes, to be used in
+ * the `flags` bit field for `polygonToCellsExperimental`.
+ */
+typedef enum {
+ CONTAINMENT_CENTER = 0, ///< Cell center is contained in the shape
+ CONTAINMENT_FULL = 1, ///< Cell is fully contained in the shape
+ CONTAINMENT_OVERLAPPING = 2, ///< Cell overlaps the shape at any point
+ CONTAINMENT_OVERLAPPING_BBOX = 3, ///< Cell bounding box overlaps shape
+ CONTAINMENT_INVALID = 4 ///< This mode is invalid and should not be used
+} ContainmentMode;
+
+/** @struct LinkedLatLng
+ * @brief A coordinate node in a linked geo structure, part of a linked list
+ */
+typedef struct LinkedLatLng LinkedLatLng;
+struct LinkedLatLng {
+ LatLng vertex;
+ LinkedLatLng *next;
+};
+
+/** @struct LinkedGeoLoop
+ * @brief A loop node in a linked geo structure, part of a linked list
+ */
+typedef struct LinkedGeoLoop LinkedGeoLoop;
+struct LinkedGeoLoop {
+ LinkedLatLng *first;
+ LinkedLatLng *last;
+ LinkedGeoLoop *next;
+};
+
+/** @struct LinkedGeoPolygon
+ * @brief A polygon node in a linked geo structure, part of a linked list.
+ */
+typedef struct LinkedGeoPolygon LinkedGeoPolygon;
+struct LinkedGeoPolygon {
+ LinkedGeoLoop *first;
+ LinkedGeoLoop *last;
+ LinkedGeoPolygon *next;
+};
+
+/** @struct CoordIJ
+ * @brief IJ hexagon coordinates
+ *
+ * Each axis is spaced 120 degrees apart.
+ */
+typedef struct {
+ int i; ///< i component
+ int j; ///< j component
+} CoordIJ;
+
+/** @defgroup latLngToCell latLngToCell
+ * Functions for latLngToCell
+ * @{
+ */
+/** @brief find the H3 index of the resolution res cell containing the lat/lng
+ */
+DECLSPEC H3Error H3_EXPORT(latLngToCell)(const LatLng *g, int res,
+ H3Index *out);
+/** @} */
+
+/** @defgroup cellToLatLng cellToLatLng
+ * Functions for cellToLatLng
+ * @{
+ */
+/** @brief find the lat/lng center point g of the cell h3 */
+DECLSPEC H3Error H3_EXPORT(cellToLatLng)(H3Index h3, LatLng *g);
+/** @} */
+
+/** @defgroup cellToBoundary cellToBoundary
+ * Functions for cellToBoundary
+ * @{
+ */
+/** @brief give the cell boundary in lat/lng coordinates for the cell h3 */
+DECLSPEC H3Error H3_EXPORT(cellToBoundary)(H3Index h3, CellBoundary *gp);
+/** @} */
+
+/** @defgroup gridDisk gridDisk
+ * Functions for gridDisk
+ * @{
+ */
+/** @brief maximum number of hexagons in k-ring */
+DECLSPEC H3Error H3_EXPORT(maxGridDiskSize)(int k, int64_t *out);
+
+/** @brief hexagons neighbors in all directions, assuming no pentagons */
+DECLSPEC H3Error H3_EXPORT(gridDiskUnsafe)(H3Index origin, int k, H3Index *out);
+/** @} */
+
+/** @brief hexagons neighbors in all directions, assuming no pentagons,
+ * reporting distance from origin */
+DECLSPEC H3Error H3_EXPORT(gridDiskDistancesUnsafe)(H3Index origin, int k,
+ H3Index *out,
+ int *distances);
+
+/** @brief hexagons neighbors in all directions reporting distance from origin
+ */
+DECLSPEC H3Error H3_EXPORT(gridDiskDistancesSafe)(H3Index origin, int k,
+ H3Index *out, int *distances);
+
+/** @brief collection of hex rings sorted by ring for all given hexagons */
+DECLSPEC H3Error H3_EXPORT(gridDisksUnsafe)(H3Index *h3Set, int length, int k,
+ H3Index *out);
+
+/** @brief hexagon neighbors in all directions */
+DECLSPEC H3Error H3_EXPORT(gridDisk)(H3Index origin, int k, H3Index *out);
+/** @} */
+
+/** @defgroup gridDiskDistances gridDiskDistances
+ * Functions for gridDiskDistances
+ * @{
+ */
+/** @brief hexagon neighbors in all directions, reporting distance from origin
+ */
+DECLSPEC H3Error H3_EXPORT(gridDiskDistances)(H3Index origin, int k,
+ H3Index *out, int *distances);
+/** @} */
+
+/** @defgroup gridRing gridRing
+ * Functions for gridRing
+ * @{
+ */
+/** @brief maximum number of hexagons in hollow k-ring */
+DECLSPEC H3Error H3_EXPORT(maxGridRingSize)(int k, int64_t *out);
+
+/** @brief hollow hexagon ring k distance from origin */
+DECLSPEC H3Error H3_EXPORT(gridRingUnsafe)(H3Index origin, int k, H3Index *out);
+
+/** @brief hollow hexagon ring k distance from origin */
+DECLSPEC H3Error H3_EXPORT(gridRing)(H3Index origin, int k, H3Index *out);
+/** @} */
+
+/** @defgroup polygonToCells polygonToCells
+ * Functions for polygonToCells
+ * @{
+ */
+/** @brief maximum number of cells that could be in the polygon */
+DECLSPEC H3Error H3_EXPORT(maxPolygonToCellsSize)(const GeoPolygon *geoPolygon,
+ int res, uint32_t flags,
+ int64_t *out);
+
+/** @brief cells within the given polygon */
+DECLSPEC H3Error H3_EXPORT(polygonToCells)(const GeoPolygon *geoPolygon,
+ int res, uint32_t flags,
+ H3Index *out);
+/** @} */
+
+/** @defgroup polygonToCellsExperimental polygonToCellsExperimental
+ * Functions for polygonToCellsExperimental.
+ * This is an experimental-only API and is subject to change in minor versions.
+ * @{
+ */
+/** @brief maximum number of cells that could be in the polygon */
+DECLSPEC H3Error H3_EXPORT(maxPolygonToCellsSizeExperimental)(
+ const GeoPolygon *polygon, int res, uint32_t flags, int64_t *out);
+
+/** @brief cells within the given polygon */
+DECLSPEC H3Error H3_EXPORT(polygonToCellsExperimental)(
+ const GeoPolygon *polygon, int res, uint32_t flags, int64_t size,
+ H3Index *out);
+/** @} */
+
+/** @defgroup cellsToMultiPolygon cellsToMultiPolygon
+ * Functions for cellsToMultiPolygon (currently a binding-only concept)
+ * @{
+ */
+/** @brief Create a LinkedGeoPolygon from a set of contiguous hexagons */
+DECLSPEC H3Error H3_EXPORT(cellsToLinkedMultiPolygon)(const H3Index *h3Set,
+ const int numHexes,
+ LinkedGeoPolygon *out);
+
+/** @brief Free all memory created for a LinkedGeoPolygon */
+DECLSPEC void H3_EXPORT(destroyLinkedMultiPolygon)(LinkedGeoPolygon *polygon);
+/** @} */
+
+/** @defgroup degsToRads degsToRads
+ * Functions for degsToRads
+ * @{
+ */
+/** @brief converts degrees to radians */
+DECLSPEC double H3_EXPORT(degsToRads)(double degrees);
+/** @} */
+
+/** @defgroup radsToDegs radsToDegs
+ * Functions for radsToDegs
+ * @{
+ */
+/** @brief converts radians to degrees */
+DECLSPEC double H3_EXPORT(radsToDegs)(double radians);
+/** @} */
+
+/** @defgroup greatCircleDistance greatCircleDistance
+ * Functions for distance
+ * @{
+ */
+/** @brief "great circle distance" between pairs of LatLng points in radians*/
+DECLSPEC double H3_EXPORT(greatCircleDistanceRads)(const LatLng *a,
+ const LatLng *b);
+
+/** @brief "great circle distance" between pairs of LatLng points in
+ * kilometers*/
+DECLSPEC double H3_EXPORT(greatCircleDistanceKm)(const LatLng *a,
+ const LatLng *b);
+
+/** @brief "great circle distance" between pairs of LatLng points in meters*/
+DECLSPEC double H3_EXPORT(greatCircleDistanceM)(const LatLng *a,
+ const LatLng *b);
+/** @} */
+
+/** @defgroup getHexagonAreaAvg getHexagonAreaAvg
+ * Functions for getHexagonAreaAvg
+ * @{
+ */
+/** @brief average hexagon area in square kilometers (excludes pentagons) */
+DECLSPEC H3Error H3_EXPORT(getHexagonAreaAvgKm2)(int res, double *out);
+
+/** @brief average hexagon area in square meters (excludes pentagons) */
+DECLSPEC H3Error H3_EXPORT(getHexagonAreaAvgM2)(int res, double *out);
+/** @} */
+
+/** @defgroup cellArea cellArea
+ * Functions for cellArea
+ * @{
+ */
+/** @brief exact area for a specific cell (hexagon or pentagon) in radians^2 */
+DECLSPEC H3Error H3_EXPORT(cellAreaRads2)(H3Index h, double *out);
+
+/** @brief exact area for a specific cell (hexagon or pentagon) in kilometers^2
+ */
+DECLSPEC H3Error H3_EXPORT(cellAreaKm2)(H3Index h, double *out);
+
+/** @brief exact area for a specific cell (hexagon or pentagon) in meters^2 */
+DECLSPEC H3Error H3_EXPORT(cellAreaM2)(H3Index h, double *out);
+/** @} */
+
+/** @defgroup getHexagonEdgeLengthAvg getHexagonEdgeLengthAvg
+ * Functions for getHexagonEdgeLengthAvg
+ * @{
+ */
+/** @brief average hexagon edge length in kilometers (excludes pentagons) */
+DECLSPEC H3Error H3_EXPORT(getHexagonEdgeLengthAvgKm)(int res, double *out);
+
+/** @brief average hexagon edge length in meters (excludes pentagons) */
+DECLSPEC H3Error H3_EXPORT(getHexagonEdgeLengthAvgM)(int res, double *out);
+/** @} */
+
+/** @defgroup edgeLength edgeLength
+ * Functions for edgeLength
+ * @{
+ */
+/** @brief exact length for a specific directed edge in radians*/
+DECLSPEC H3Error H3_EXPORT(edgeLengthRads)(H3Index edge, double *length);
+
+/** @brief exact length for a specific directed edge in kilometers*/
+DECLSPEC H3Error H3_EXPORT(edgeLengthKm)(H3Index edge, double *length);
+
+/** @brief exact length for a specific directed edge in meters*/
+DECLSPEC H3Error H3_EXPORT(edgeLengthM)(H3Index edge, double *length);
+/** @} */
+
+/** @defgroup getNumCells getNumCells
+ * Functions for getNumCells
+ * @{
+ */
+/** @brief number of cells (hexagons and pentagons) for a given resolution
+ *
+ * It works out to be `2 + 120*7^r` for resolution `r`.
+ *
+ * # Mathematical notes
+ *
+ * Let h(n) be the number of children n levels below
+ * a single *hexagon*.
+ *
+ * Then h(n) = 7^n.
+ *
+ * Let p(n) be the number of children n levels below
+ * a single *pentagon*.
+ *
+ * Then p(0) = 1, and p(1) = 6, since each pentagon
+ * has 5 hexagonal immediate children and 1 pentagonal
+ * immediate child.
+ *
+ * In general, we have the recurrence relation
+ *
+ * p(n) = 5*h(n-1) + p(n-1)
+ * = 5*7^(n-1) + p(n-1).
+ *
+ * Working through the recurrence, we get that
+ *
+ * p(n) = 1 + 5*\sum_{k=1}^n 7^{k-1}
+ * = 1 + 5*(7^n - 1)/6,
+ *
+ * using the closed form for a geometric series.
+ *
+ * Using the closed forms for h(n) and p(n), we can
+ * get a closed form for the total number of cells
+ * at resolution r:
+ *
+ * c(r) = 12*p(r) + 110*h(r)
+ * = 2 + 120*7^r.
+ *
+ *
+ * @param res H3 cell resolution
+ *
+ * @return number of cells at resolution `res`
+ */
+DECLSPEC H3Error H3_EXPORT(getNumCells)(int res, int64_t *out);
+/** @} */
+
+/** @defgroup getRes0Cells getRes0Cells
+ * Functions for getRes0Cells
+ * @{
+ */
+/** @brief returns the number of resolution 0 cells (hexagons and pentagons) */
+DECLSPEC int H3_EXPORT(res0CellCount)(void);
+
+/** @brief provides all base cells in H3Index format*/
+DECLSPEC H3Error H3_EXPORT(getRes0Cells)(H3Index *out);
+/** @} */
+
+/** @defgroup getPentagons getPentagons
+ * Functions for getPentagons
+ * @{
+ */
+/** @brief returns the number of pentagons per resolution */
+DECLSPEC int H3_EXPORT(pentagonCount)(void);
+
+/** @brief generates all pentagons at the specified resolution */
+DECLSPEC H3Error H3_EXPORT(getPentagons)(int res, H3Index *out);
+/** @} */
+
+/** @defgroup getResolution getResolution
+ * Functions for getResolution
+ * @{
+ */
+/** @brief returns the resolution of the provided H3 index
+ * Works on both cells and directed edges. */
+DECLSPEC int H3_EXPORT(getResolution)(H3Index h);
+/** @} */
+
+/** @defgroup getBaseCellNumber getBaseCellNumber
+ * Functions for getBaseCellNumber
+ * @{
+ */
+/** @brief returns the base cell "number" (0 to 121) of the provided H3 cell
+ *
+ * Note: Technically works on H3 edges, but will return base cell of the
+ * origin cell. */
+DECLSPEC int H3_EXPORT(getBaseCellNumber)(H3Index h);
+/** @} */
+
+/** @defgroup stringToH3 stringToH3
+ * Functions for stringToH3
+ * @{
+ */
+/** @brief converts the canonical string format to H3Index format */
+DECLSPEC H3Error H3_EXPORT(stringToH3)(const char *str, H3Index *out);
+/** @} */
+
+/** @defgroup h3ToString h3ToString
+ * Functions for h3ToString
+ * @{
+ */
+/** @brief converts an H3Index to a canonical string */
+DECLSPEC H3Error H3_EXPORT(h3ToString)(H3Index h, char *str, size_t sz);
+/** @} */
+
+/** @defgroup isValidCell isValidCell
+ * Functions for isValidCell
+ * @{
+ */
+/** @brief confirms if an H3Index is a valid cell (hexagon or pentagon)
+ * In particular, returns 0 (False) for H3 directed edges or invalid data
+ */
+DECLSPEC int H3_EXPORT(isValidCell)(H3Index h);
+/** @} */
+
+/** @defgroup cellToParent cellToParent
+ * Functions for cellToParent
+ * @{
+ */
+/** @brief returns the parent (or grandparent, etc) cell of the given cell
+ */
+DECLSPEC H3Error H3_EXPORT(cellToParent)(H3Index h, int parentRes,
+ H3Index *parent);
+/** @} */
+
+/** @defgroup cellToChildren cellToChildren
+ * Functions for cellToChildren
+ * @{
+ */
+/** @brief determines the exact number of children (or grandchildren, etc)
+ * that would be returned for the given cell */
+DECLSPEC H3Error H3_EXPORT(cellToChildrenSize)(H3Index h, int childRes,
+ int64_t *out);
+
+/** @brief provides the children (or grandchildren, etc) of the given cell */
+DECLSPEC H3Error H3_EXPORT(cellToChildren)(H3Index h, int childRes,
+ H3Index *children);
+/** @} */
+
+/** @defgroup cellToCenterChild cellToCenterChild
+ * Functions for cellToCenterChild
+ * @{
+ */
+/** @brief returns the center child of the given cell at the specified
+ * resolution */
+DECLSPEC H3Error H3_EXPORT(cellToCenterChild)(H3Index h, int childRes,
+ H3Index *child);
+/** @} */
+
+/** @defgroup cellToChildPos cellToChildPos
+ * Functions for cellToChildPos
+ * @{
+ */
+/** @brief Returns the position of the cell within an ordered list of all
+ * children of the cell's parent at the specified resolution */
+DECLSPEC H3Error H3_EXPORT(cellToChildPos)(H3Index child, int parentRes,
+ int64_t *out);
+/** @} */
+
+/** @defgroup childPosToCell childPosToCell
+ * Functions for childPosToCell
+ * @{
+ */
+/** @brief Returns the child cell at a given position within an ordered list of
+ * all children at the specified resolution */
+DECLSPEC H3Error H3_EXPORT(childPosToCell)(int64_t childPos, H3Index parent,
+ int childRes, H3Index *child);
+/** @} */
+
+/** @defgroup compactCells compactCells
+ * Functions for compactCells
+ * @{
+ */
+/** @brief compacts the given set of hexagons as best as possible */
+DECLSPEC H3Error H3_EXPORT(compactCells)(const H3Index *h3Set,
+ H3Index *compactedSet,
+ const int64_t numHexes);
+/** @} */
+
+/** @defgroup uncompactCells uncompactCells
+ * Functions for uncompactCells
+ * @{
+ */
+/** @brief determines the exact number of hexagons that will be uncompacted
+ * from the compacted set */
+DECLSPEC H3Error H3_EXPORT(uncompactCellsSize)(const H3Index *compactedSet,
+ const int64_t numCompacted,
+ const int res, int64_t *out);
+
+/** @brief uncompacts the compacted hexagon set */
+DECLSPEC H3Error H3_EXPORT(uncompactCells)(const H3Index *compactedSet,
+ const int64_t numCompacted,
+ H3Index *outSet,
+ const int64_t numOut, const int res);
+/** @} */
+
+/** @defgroup isResClassIII isResClassIII
+ * Functions for isResClassIII
+ * @{
+ */
+/** @brief determines if a hexagon is Class III (or Class II) */
+DECLSPEC int H3_EXPORT(isResClassIII)(H3Index h);
+/** @} */
+
+/** @defgroup isPentagon isPentagon
+ * Functions for isPentagon
+ * @{
+ */
+/** @brief determines if an H3 cell is a pentagon */
+DECLSPEC int H3_EXPORT(isPentagon)(H3Index h);
+/** @} */
+
+/** @defgroup getIcosahedronFaces getIcosahedronFaces
+ * Functions for getIcosahedronFaces
+ * @{
+ */
+/** @brief Max number of icosahedron faces intersected by an index */
+DECLSPEC H3Error H3_EXPORT(maxFaceCount)(H3Index h3, int *out);
+
+/** @brief Find all icosahedron faces intersected by a given H3 index */
+DECLSPEC H3Error H3_EXPORT(getIcosahedronFaces)(H3Index h3, int *out);
+/** @} */
+
+/** @defgroup areNeighborCells areNeighborCells
+ * Functions for areNeighborCells
+ * @{
+ */
+/** @brief returns whether or not the provided hexagons border */
+DECLSPEC H3Error H3_EXPORT(areNeighborCells)(H3Index origin,
+ H3Index destination, int *out);
+/** @} */
+
+/** @defgroup cellsToDirectedEdge cellsToDirectedEdge
+ * Functions for cellsToDirectedEdge
+ * @{
+ */
+/** @brief returns the directed edge H3Index for the specified origin and
+ * destination */
+DECLSPEC H3Error H3_EXPORT(cellsToDirectedEdge)(H3Index origin,
+ H3Index destination,
+ H3Index *out);
+/** @} */
+
+/** @defgroup isValidDirectedEdge isValidDirectedEdge
+ * Functions for isValidDirectedEdge
+ * @{
+ */
+/** @brief returns whether the H3Index is a valid directed edge */
+DECLSPEC int H3_EXPORT(isValidDirectedEdge)(H3Index edge);
+/** @} */
+
+/** @defgroup getDirectedEdgeOrigin \
+ * getDirectedEdgeOrigin
+ * Functions for getDirectedEdgeOrigin
+ * @{
+ */
+/** @brief Returns the origin hexagon H3Index from the directed edge
+ * H3Index */
+DECLSPEC H3Error H3_EXPORT(getDirectedEdgeOrigin)(H3Index edge, H3Index *out);
+/** @} */
+
+/** @defgroup getDirectedEdgeDestination \
+ * getDirectedEdgeDestination
+ * Functions for getDirectedEdgeDestination
+ * @{
+ */
+/** @brief Returns the destination hexagon H3Index from the directed edge
+ * H3Index */
+DECLSPEC H3Error H3_EXPORT(getDirectedEdgeDestination)(H3Index edge,
+ H3Index *out);
+/** @} */
+
+/** @defgroup directedEdgeToCells \
+ * directedEdgeToCells
+ * Functions for directedEdgeToCells
+ * @{
+ */
+/** @brief Returns the origin and destination hexagons from the directed
+ * edge H3Index */
+DECLSPEC H3Error H3_EXPORT(directedEdgeToCells)(H3Index edge,
+ H3Index *originDestination);
+/** @} */
+
+/** @defgroup originToDirectedEdges \
+ * originToDirectedEdges
+ * Functions for originToDirectedEdges
+ * @{
+ */
+/** @brief Returns the 6 (or 5 for pentagons) edges associated with the H3Index
+ */
+DECLSPEC H3Error H3_EXPORT(originToDirectedEdges)(H3Index origin,
+ H3Index *edges);
+/** @} */
+
+/** @defgroup directedEdgeToBoundary directedEdgeToBoundary
+ * Functions for directedEdgeToBoundary
+ * @{
+ */
+/** @brief Returns the CellBoundary containing the coordinates of the edge */
+DECLSPEC H3Error H3_EXPORT(directedEdgeToBoundary)(H3Index edge,
+ CellBoundary *gb);
+/** @} */
+
+/** @defgroup cellToVertex cellToVertex
+ * Functions for cellToVertex
+ * @{
+ */
+/** @brief Returns a single vertex for a given cell, as an H3 index */
+DECLSPEC H3Error H3_EXPORT(cellToVertex)(H3Index origin, int vertexNum,
+ H3Index *out);
+/** @} */
+
+/** @defgroup cellToVertexes cellToVertexes
+ * Functions for cellToVertexes
+ * @{
+ */
+/** @brief Returns all vertexes for a given cell, as H3 indexes */
+DECLSPEC H3Error H3_EXPORT(cellToVertexes)(H3Index origin, H3Index *vertexes);
+/** @} */
+
+/** @defgroup vertexToLatLng vertexToLatLng
+ * Functions for vertexToLatLng
+ * @{
+ */
+/** @brief Returns a single vertex for a given cell, as an H3 index */
+DECLSPEC H3Error H3_EXPORT(vertexToLatLng)(H3Index vertex, LatLng *point);
+/** @} */
+
+/** @defgroup isValidVertex isValidVertex
+ * Functions for isValidVertex
+ * @{
+ */
+/** @brief Whether the input is a valid H3 vertex */
+DECLSPEC int H3_EXPORT(isValidVertex)(H3Index vertex);
+/** @} */
+
+/** @defgroup gridDistance gridDistance
+ * Functions for gridDistance
+ * @{
+ */
+/** @brief Returns grid distance between two indexes */
+DECLSPEC H3Error H3_EXPORT(gridDistance)(H3Index origin, H3Index h3,
+ int64_t *distance);
+/** @} */
+
+/** @defgroup gridPathCells gridPathCells
+ * Functions for gridPathCells
+ * @{
+ */
+/** @brief Number of indexes in a line connecting two indexes */
+DECLSPEC H3Error H3_EXPORT(gridPathCellsSize)(H3Index start, H3Index end,
+ int64_t *size);
+
+/** @brief Line of h3 indexes connecting two indexes */
+DECLSPEC H3Error H3_EXPORT(gridPathCells)(H3Index start, H3Index end,
+ H3Index *out);
+/** @} */
+
+/** @defgroup cellToLocalIj cellToLocalIj
+ * Functions for cellToLocalIj
+ * @{
+ */
+/** @brief Returns two dimensional coordinates for the given index */
+DECLSPEC H3Error H3_EXPORT(cellToLocalIj)(H3Index origin, H3Index h3,
+ uint32_t mode, CoordIJ *out);
+/** @} */
+
+/** @defgroup localIjToCell localIjToCell
+ * Functions for localIjToCell
+ * @{
+ */
+/** @brief Returns index for the given two dimensional coordinates */
+DECLSPEC H3Error H3_EXPORT(localIjToCell)(H3Index origin, const CoordIJ *ij,
+ uint32_t mode, H3Index *out);
+/** @} */
+
+#ifdef __cplusplus
+} // extern "C"
+#endif
+
+#endif
diff --git a/frogpilot/third_party/h3/_cy/h3lib.pxd b/frogpilot/third_party/h3/_cy/h3lib.pxd
new file mode 100644
index 00000000..e94f655b
--- /dev/null
+++ b/frogpilot/third_party/h3/_cy/h3lib.pxd
@@ -0,0 +1,193 @@
+# cython: c_string_type=unicode, c_string_encoding=utf8
+from cpython cimport bool
+from libc.stdint cimport uint32_t, uint64_t, int64_t
+
+ctypedef object H3str
+
+cdef extern from 'h3api.h':
+ cdef int H3_VERSION_MAJOR
+ cdef int H3_VERSION_MINOR
+ cdef int H3_VERSION_PATCH
+
+ ctypedef uint64_t H3int 'H3Index'
+
+ ctypedef uint32_t H3Error
+ ctypedef enum H3ErrorCodes:
+ E_SUCCESS = 0
+ E_FAILED = 1
+ E_DOMAIN = 2
+ E_LATLNG_DOMAIN = 3
+ E_RES_DOMAIN = 4
+ E_CELL_INVALID = 5
+ E_DIR_EDGE_INVALID = 6
+ E_UNDIR_EDGE_INVALID = 7
+ E_VERTEX_INVALID = 8
+ E_PENTAGON = 9
+ E_DUPLICATE_INPUT = 10
+ E_NOT_NEIGHBORS = 11
+ E_RES_MISMATCH = 12
+ E_MEMORY_ALLOC = 13
+ E_MEMORY_BOUNDS = 14
+ E_OPTION_INVALID = 15
+
+ ctypedef struct LatLng:
+ double lat # in radians
+ double lng # in radians
+
+ ctypedef struct CellBoundary:
+ int num_verts 'numVerts'
+ LatLng verts[10] # MAX_CELL_BNDRY_VERTS
+
+ ctypedef struct CoordIJ:
+ int i
+ int j
+
+ ctypedef struct LinkedLatLng:
+ LatLng data 'vertex'
+ LinkedLatLng *next
+
+ # renaming these for clarity
+ ctypedef struct LinkedGeoLoop:
+ LinkedLatLng *data 'first'
+ LinkedLatLng *_data_last 'last' # not needed in Cython bindings
+ LinkedGeoLoop *next
+
+ ctypedef struct LinkedGeoPolygon:
+ LinkedGeoLoop *data 'first'
+ LinkedGeoLoop *_data_last 'last' # not needed in Cython bindings
+ LinkedGeoPolygon *next
+
+ ctypedef struct GeoLoop:
+ int numVerts
+ LatLng *verts
+
+ ctypedef struct GeoPolygon:
+ GeoLoop geoloop
+ int numHoles
+ GeoLoop *holes
+
+ int isValidCell(H3int h) nogil
+ int isPentagon(H3int h) nogil
+ int isResClassIII(H3int h) nogil
+ int isValidDirectedEdge(H3int edge) nogil
+ int isValidVertex(H3int v) nogil
+
+ double degsToRads(double degrees) nogil
+ double radsToDegs(double radians) nogil
+
+ int getResolution(H3int h) nogil
+ int getBaseCellNumber(H3int h) nogil
+
+ H3Error latLngToCell(const LatLng *g, int res, H3int *out) nogil
+ H3Error cellToLatLng(H3int h, LatLng *) nogil
+ H3Error gridDistance(H3int h1, H3int h2, int64_t *distance) nogil
+
+ H3Error cellToVertex(H3int cell, int vertexNum, H3int *out) nogil
+ H3Error cellToVertexes(H3int cell, H3int *vertexes) nogil
+ H3Error vertexToLatLng(H3int vertex, LatLng *coord) nogil
+
+ H3Error maxGridDiskSize(int k, int64_t *out) nogil # num/out/N?
+ H3Error gridDisk(H3int h, int k, H3int *out) nogil
+
+ H3Error cellToParent( H3int h, int parentRes, H3int *parent) nogil
+ H3Error cellToCenterChild(H3int h, int childRes, H3int *child) nogil
+ H3Error cellToChildPos(H3int child, int parentRes, int64_t *out) nogil
+ H3Error childPosToCell(int64_t childPos, H3int parent, int childRes, H3int *child) nogil
+
+ H3Error cellToChildrenSize(H3int h, int childRes, int64_t *num) nogil # num/out/N?
+ H3Error cellToChildren( H3int h, int childRes, H3int *children) nogil
+
+ H3Error compactCells(
+ const H3int *cells_u,
+ H3int *cells_c,
+ const int num_u
+ ) nogil
+ H3Error uncompactCellsSize(
+ const H3int *cells_c,
+ const int64_t num_c,
+ const int res,
+ int64_t *num_u
+ ) nogil
+ H3Error uncompactCells(
+ const H3int *cells_c,
+ const int num_c,
+ H3int *cells_u,
+ const int num_u,
+ const int res
+ ) nogil
+
+ H3Error getNumCells(int res, int64_t *out) nogil
+ int pentagonCount() nogil
+ int res0CellCount() nogil
+ H3Error getPentagons(int res, H3int *out) nogil
+ H3Error getRes0Cells(H3int *out) nogil
+
+ H3Error gridPathCellsSize(H3int start, H3int end, int64_t *size) nogil
+ H3Error gridPathCells(H3int start, H3int end, H3int *out) nogil
+
+ H3Error getHexagonAreaAvgKm2(int res, double *out) nogil
+ H3Error getHexagonAreaAvgM2(int res, double *out) nogil
+
+ H3Error cellAreaRads2(H3int h, double *out) nogil
+ H3Error cellAreaKm2(H3int h, double *out) nogil
+ H3Error cellAreaM2(H3int h, double *out) nogil
+
+ H3Error maxFaceCount(H3int h, int *out) nogil
+ H3Error getIcosahedronFaces(H3int h3, int *out) nogil
+
+ H3Error cellToLocalIj(H3int origin, H3int h3, uint32_t mode, CoordIJ *out) nogil
+ H3Error localIjToCell(H3int origin, const CoordIJ *ij, uint32_t mode, H3int *out) nogil
+
+ H3Error gridDiskDistances(H3int origin, int k, H3int *out, int *distances) nogil
+ H3Error gridRing(H3int origin, int k, H3int *out) nogil
+ H3Error gridRingUnsafe(H3int origin, int k, H3int *out) nogil
+
+ H3Error areNeighborCells(H3int origin, H3int destination, int *out) nogil
+ H3Error cellsToDirectedEdge(H3int origin, H3int destination, H3int *out) nogil
+ H3Error getDirectedEdgeOrigin(H3int edge, H3int *out) nogil
+ H3Error getDirectedEdgeDestination(H3int edge, H3int *out) nogil
+ H3Error originToDirectedEdges(H3int origin, H3int *edges) nogil
+ # todo: directedEdgeToCells
+
+ H3Error getHexagonEdgeLengthAvgKm(int res, double *out) nogil
+ H3Error getHexagonEdgeLengthAvgM(int res, double *out) nogil
+
+ H3Error edgeLengthRads(H3int edge, double *out) nogil
+ H3Error edgeLengthKm(H3int edge, double *out) nogil
+ H3Error edgeLengthM(H3int edge, double *out) nogil
+
+ H3Error cellToBoundary(H3int h3, CellBoundary *gp) nogil
+ H3Error directedEdgeToBoundary(H3int edge, CellBoundary *gb) nogil
+
+ double greatCircleDistanceRads(const LatLng *a, const LatLng *b) nogil
+ double greatCircleDistanceKm(const LatLng *a, const LatLng *b) nogil
+ double greatCircleDistanceM(const LatLng *a, const LatLng *b) nogil
+
+ H3Error cellsToLinkedMultiPolygon(const H3int *h3Set, const int numCells, LinkedGeoPolygon *out)
+ void destroyLinkedMultiPolygon(LinkedGeoPolygon *polygon)
+
+ H3Error maxPolygonToCellsSize(const GeoPolygon *geoPolygon, int res, uint32_t flags, uint64_t *count)
+ H3Error polygonToCells(const GeoPolygon *geoPolygon, int res, uint32_t flags, H3int *out)
+
+ H3Error maxPolygonToCellsSizeExperimental(const GeoPolygon *geoPolygon, int res, uint32_t flags, uint64_t *count)
+ H3Error polygonToCellsExperimental(const GeoPolygon *geoPolygon, int res, uint32_t flags, uint64_t sz, H3int *out)
+
+ # ctypedef struct GeoMultiPolygon:
+ # int numPolygons
+ # GeoPolygon *polygons
+
+ # int hexRange(H3int origin, int k, H3int *out)
+
+ # int hexRangeDistances(H3int origin, int k, H3int *out, int *distances)
+
+ # int hexRanges(H3int *h3Set, int length, int k, H3int *out)
+
+ # void h3SetToLinkedGeo(const H3int *h3Set, const int numCells, LinkedGeoPolygon *out)
+
+ # void destroyLinkedPolygon(LinkedGeoPolygon *polygon)
+
+ # H3int stringToH3(const char *str)
+
+ # void h3ToString(H3int h, char *str, size_t sz)
+
+ # void getH3intesFromUnidirectionalEdge(H3int edge, H3int *originDestination)
diff --git a/frogpilot/third_party/h3/_cy/latlng.cpython-312-aarch64-linux-gnu.so b/frogpilot/third_party/h3/_cy/latlng.cpython-312-aarch64-linux-gnu.so
new file mode 100644
index 00000000..133658ff
Binary files /dev/null and b/frogpilot/third_party/h3/_cy/latlng.cpython-312-aarch64-linux-gnu.so differ
diff --git a/frogpilot/third_party/h3/_cy/latlng.pxd b/frogpilot/third_party/h3/_cy/latlng.pxd
new file mode 100644
index 00000000..40dfc349
--- /dev/null
+++ b/frogpilot/third_party/h3/_cy/latlng.pxd
@@ -0,0 +1,7 @@
+from .h3lib cimport H3int
+
+cpdef H3int latlng_to_cell(double lat, double lng, int res) except 1
+cpdef (double, double) cell_to_latlng(H3int h) except *
+cpdef double great_circle_distance(
+ double lat1, double lng1,
+ double lat2, double lng2, unit=*) except -1
diff --git a/frogpilot/third_party/h3/_cy/latlng.pyx b/frogpilot/third_party/h3/_cy/latlng.pyx
new file mode 100644
index 00000000..d83a9d3d
--- /dev/null
+++ b/frogpilot/third_party/h3/_cy/latlng.pyx
@@ -0,0 +1,325 @@
+from libc.stdint cimport uint64_t
+
+cimport h3lib
+from h3lib cimport bool, H3int
+
+from .util cimport (
+ check_cell,
+ check_edge,
+ check_res,
+ deg2coord,
+ coord2deg
+)
+
+from .error_system cimport check_for_error
+
+from .memory cimport H3MemoryManager
+
+# TODO: We might be OK with taking the GIL for the functions in this module
+from libc.stdlib cimport (
+ # malloc as h3_malloc, # not used
+ calloc as h3_calloc,
+ realloc as h3_realloc,
+ free as h3_free,
+)
+
+
+cpdef H3int latlng_to_cell(double lat, double lng, int res) except 1:
+ cdef:
+ h3lib.LatLng c
+ H3int out
+
+ c = deg2coord(lat, lng)
+
+ check_for_error(
+ h3lib.latLngToCell(&c, res, &out)
+ )
+
+ return out
+
+
+cpdef (double, double) cell_to_latlng(H3int h) except *:
+ """Map an H3 cell into its centroid geo-coordinate (lat/lng)"""
+ cdef:
+ h3lib.LatLng c
+
+ check_cell(h)
+ # todo: think about: if you give this an invalid cell, should it still return a lat/lng?
+ # idea: safe and unsafe APIs?
+
+ check_for_error(
+ h3lib.cellToLatLng(h, &c)
+ )
+
+ return coord2deg(c)
+
+
+cdef h3lib.GeoLoop make_geoloop(latlngs) except *:
+ """
+ The returned `GeoLoop` must be freed with a call to `free_geoloop`.
+
+ Parameters
+ ----------
+ latlngs : list or tuple
+ GeoLoop: A sequence of >= 3 (lat, lng) pairs where the last
+ element may or may not be same as the first (to form a closed loop).
+ The order of the pairs may be either clockwise or counterclockwise.
+ """
+ cdef:
+ h3lib.GeoLoop gl
+
+ gl.numVerts = len(latlngs)
+
+ # todo: need for memory management
+ # can automatically free?
+ gl.verts = h3_calloc(gl.numVerts, sizeof(h3lib.LatLng))
+
+ for i, (lat, lng) in enumerate(latlngs):
+ gl.verts[i] = deg2coord(lat, lng)
+
+ return gl
+
+
+cdef free_geoloop(h3lib.GeoLoop* gl):
+ h3_free(gl.verts)
+ gl.verts = NULL
+
+
+cdef class GeoPolygon:
+ cdef:
+ h3lib.GeoPolygon gp
+
+ def __cinit__(self, outer, holes=None):
+ """
+
+ Parameters
+ ----------
+ outer : list or tuple
+ GeoLoop
+ A GeoLoop is a sequence of >= 3 (lat, lng) pairs where the last
+ element may or may not be same as the first (to form a closed loop).
+ The order of the pairs may be either clockwise or counterclockwise.
+ holes : list or tuple
+ A sequence of GeoLoops
+ """
+ if holes is None:
+ holes = []
+
+ self.gp.geoloop = make_geoloop(outer)
+ self.gp.numHoles = len(holes)
+ self.gp.holes = NULL
+
+ if len(holes) > 0:
+ self.gp.holes = h3_calloc(len(holes), sizeof(h3lib.GeoLoop))
+ for i, hole in enumerate(holes):
+ self.gp.holes[i] = make_geoloop(hole)
+
+
+ def __dealloc__(self):
+ free_geoloop(&self.gp.geoloop)
+
+ for i in range(self.gp.numHoles):
+ free_geoloop(&self.gp.holes[i])
+
+ h3_free(self.gp.holes)
+
+
+def polygon_to_cells(outer, int res, holes=None):
+ """ Get the set of cells whose center is contained in a polygon.
+
+ The polygon is defined similarity to the GeoJson standard, with an exterior
+ `outer` ring of lat/lng points, and a list of `holes`, each of which are also
+ rings of lat/lng points.
+
+ Each ring may be in clockwise or counter-clockwise order
+ (right-hand rule or not), and may or may not be a closed loop (where the last
+ element is equal to the first).
+ The GeoJSON spec requires the right-hand rule and a closed loop, but
+ this function relaxes those constraints.
+
+ Unlike the GeoJson standard, the elements of the lat/lng pairs of each
+ ring are in lat/lng order, instead of lng/lat order.
+
+ We'll handle translation to different formats in the Python code,
+ rather than the Cython code.
+
+ Parameters
+ ----------
+ outer : list or tuple
+ A ring given by a sequence of lat/lng pairs.
+ res : int
+ The resolution of the output hexagons
+ holes : list or tuple
+ A collection of rings, each given by a sequence of lat/lng pairs.
+ These describe any the "holes" in the polygon.
+ """
+ cdef:
+ uint64_t n
+
+ check_res(res)
+
+ if not outer:
+ return H3MemoryManager(0).to_mv()
+
+ gp = GeoPolygon(outer, holes=holes)
+
+ check_for_error(
+ h3lib.maxPolygonToCellsSize(&gp.gp, res, 0, &n)
+ )
+
+ hmm = H3MemoryManager(n)
+ check_for_error(
+ h3lib.polygonToCells(&gp.gp, res, 0, hmm.ptr)
+ )
+ mv = hmm.to_mv()
+
+ return mv
+
+
+def polygons_to_cells(polygons, int res):
+ mvs = [
+ polygon_to_cells(outer=poly.outer, res=res, holes=poly.holes)
+ for poly in polygons
+ ]
+
+ n = sum(map(len, mvs))
+ hmm = H3MemoryManager(n)
+
+ # probably super inefficient, but it is working!
+ # tood: move this to C
+ k = 0
+ for mv in mvs:
+ for v in mv:
+ hmm.ptr[k] = v
+ k += 1
+
+ return hmm.to_mv()
+
+
+def polygon_to_cells_experimental(outer, int res, int flag, holes=None):
+ """ Get the set of cells whose center is contained in a polygon.
+
+ The polygon is defined similarity to the GeoJson standard, with an exterior
+ `outer` ring of lat/lng points, and a list of `holes`, each of which are also
+ rings of lat/lng points.
+
+ Each ring may be in clockwise or counter-clockwise order
+ (right-hand rule or not), and may or may not be a closed loop (where the last
+ element is equal to the first).
+ The GeoJSON spec requires the right-hand rule and a closed loop, but
+ this function relaxes those constraints.
+
+ Unlike the GeoJson standard, the elements of the lat/lng pairs of each
+ ring are in lat/lng order, instead of lng/lat order.
+
+ We'll handle translation to different formats in the Python code,
+ rather than the Cython code.
+
+ Parameters
+ ----------
+ outer : list or tuple
+ A ring given by a sequence of lat/lng pairs.
+ res : int
+ The resolution of the output hexagons
+ flag : int
+ Polygon to cells flag, such as containment mode.
+ holes : list or tuple
+ A collection of rings, each given by a sequence of lat/lng pairs.
+ These describe any the "holes" in the polygon.
+ """
+ cdef:
+ uint64_t n
+
+ check_res(res)
+
+ if not outer:
+ return H3MemoryManager(0).to_mv()
+
+ gp = GeoPolygon(outer, holes=holes)
+
+ check_for_error(
+ h3lib.maxPolygonToCellsSizeExperimental(&gp.gp, res, flag, &n)
+ )
+
+ hmm = H3MemoryManager(n)
+ check_for_error(
+ h3lib.polygonToCellsExperimental(&gp.gp, res, flag, n, hmm.ptr)
+ )
+ mv = hmm.to_mv()
+
+ return mv
+
+
+def polygons_to_cells_experimental(polygons, int res, int flag):
+ mvs = [
+ polygon_to_cells_experimental(outer=poly.outer, res=res, holes=poly.holes, flag=flag)
+ for poly in polygons
+ ]
+
+ n = sum(map(len, mvs))
+ hmm = H3MemoryManager(n)
+
+ # probably super inefficient, but it is working!
+ # tood: move this to C
+ k = 0
+ for mv in mvs:
+ for v in mv:
+ hmm.ptr[k] = v
+ k += 1
+
+ return hmm.to_mv()
+
+
+def cell_to_boundary(H3int h):
+ """Compose an array of geo-coordinates that outlines a hexagonal cell"""
+ cdef:
+ h3lib.CellBoundary gb
+
+ check_cell(h)
+
+ h3lib.cellToBoundary(h, &gb)
+
+ verts = tuple(
+ coord2deg(gb.verts[i])
+ for i in range(gb.num_verts)
+ )
+
+ return verts
+
+
+def directed_edge_to_boundary(H3int edge):
+ """ Returns the CellBoundary containing the coordinates of the edge
+ """
+ cdef:
+ h3lib.CellBoundary gb
+
+ check_edge(edge)
+
+ h3lib.directedEdgeToBoundary(edge, &gb)
+
+ # todo: move this verts transform into the CellBoundary object
+ verts = tuple(
+ coord2deg(gb.verts[i])
+ for i in range(gb.num_verts)
+ )
+
+ return verts
+
+
+cpdef double great_circle_distance(
+ double lat1, double lng1,
+ double lat2, double lng2, unit='km') except -1:
+
+ a = deg2coord(lat1, lng1)
+ b = deg2coord(lat2, lng2)
+
+ if unit == 'rads':
+ d = h3lib.greatCircleDistanceRads(&a, &b)
+ elif unit == 'km':
+ d = h3lib.greatCircleDistanceKm(&a, &b)
+ elif unit == 'm':
+ d = h3lib.greatCircleDistanceM(&a, &b)
+ else:
+ raise ValueError('Unknown unit: {}'.format(unit))
+
+ return d
diff --git a/frogpilot/third_party/h3/_cy/memory.cpython-312-aarch64-linux-gnu.so b/frogpilot/third_party/h3/_cy/memory.cpython-312-aarch64-linux-gnu.so
new file mode 100644
index 00000000..60a02d66
Binary files /dev/null and b/frogpilot/third_party/h3/_cy/memory.cpython-312-aarch64-linux-gnu.so differ
diff --git a/frogpilot/third_party/h3/_cy/memory.pxd b/frogpilot/third_party/h3/_cy/memory.pxd
new file mode 100644
index 00000000..5e259e25
--- /dev/null
+++ b/frogpilot/third_party/h3/_cy/memory.pxd
@@ -0,0 +1,12 @@
+from .h3lib cimport H3int
+
+cdef class H3MemoryManager:
+ cdef:
+ size_t n
+ H3int* ptr
+
+ cdef H3int[:] to_mv(self)
+ cdef H3int[:] to_mv_keep_zeros(self)
+
+cdef int[:] int_mv(size_t n)
+cpdef H3int[:] iter_to_mv(cells)
diff --git a/frogpilot/third_party/h3/_cy/memory.pyx b/frogpilot/third_party/h3/_cy/memory.pyx
new file mode 100644
index 00000000..0852105f
--- /dev/null
+++ b/frogpilot/third_party/h3/_cy/memory.pyx
@@ -0,0 +1,248 @@
+from cython.view cimport array
+from .h3lib cimport H3int
+
+"""
+### Memory allocation options
+
+We have a few options for the memory allocation functions.
+There's a trade-off between using the Python allocators which let Python
+track memory usage and offers some optimizations vs the system
+allocators, which do not need to acquire the GIL.
+"""
+
+"""
+System allocation functions. These do not acquire the GIL.
+"""
+from libc.stdlib cimport (
+ # malloc as h3_malloc, # not used
+ calloc as h3_calloc,
+ realloc as h3_realloc,
+ free as h3_free,
+)
+
+
+"""
+PyMem_Raw* functions should just be wrappers around system allocators
+also given in libc.stdlib. These functions do not acquire the GIL.
+
+Note that these do not have a calloc function until py 3.5 and Cython 3.0,
+so we would need to zero-out memory manually.
+
+https://python.readthedocs.io/en/stable/c-api/memory.html#raw-memory-interface
+"""
+# from cpython.mem cimport (
+# PyMem_RawMalloc as h3_malloc,
+# # PyMem_RawCalloc as h3_calloc, # only in Python >=3.5 (and Cython >=3.0?)
+# PyMem_RawRealloc as h3_realloc,
+# PyMem_RawFree as h3_free,
+# )
+
+
+"""
+These functions use the Python allocator (instead of the system allocator),
+which offers some optimizations for Python, and allows Python to track
+memory usage. However, these functions must acquire the GIL.
+
+Note that these do not have a calloc function until py 3.5 and Cython 3.0,
+so we would need to zero-out memory manually.
+
+https://cython.readthedocs.io/en/stable/src/tutorial/memory_allocation.html
+https://python.readthedocs.io/en/stable/c-api/memory.html#memory-interface
+"""
+# from cpython.mem cimport (
+# PyMem_Malloc as h3_malloc,
+# # PyMem_Calloc as h3_calloc, # only in Python >=3.5 (and Cython >=3.0?)
+# PyMem_Realloc as h3_realloc,
+# PyMem_Free as h3_free,
+# )
+
+
+cdef size_t move_nonzeros(H3int* a, size_t n):
+ """ Move nonzero elements to front of array `a` of length `n`.
+ Return the number of nonzero elements.
+
+ Loop invariant: Everything *before* `i` or *after* `j` is "done".
+ Move `i` and `j` inwards until they equal, and exit.
+ You can move `i` forward until there's a zero in front of it.
+ You can move `j` backward until there's a nonzero to the left of it.
+ Anything to the right of `j` is "junk" that can be reallocated.
+
+ | a | b | 0 | c | d | ... |
+ ^ ^
+ i j
+
+
+ | a | b | d | c | d | ... |
+ ^ ^
+ i j
+ """
+ cdef:
+ size_t i = 0
+ size_t j = n
+
+ while i < j:
+ if a[j-1] == 0:
+ j -= 1
+ continue
+
+ if a[i] != 0:
+ i += 1
+ continue
+
+ # if we're here, we know:
+ # a[i] == 0
+ # a[j-1] != 0
+ # i < j
+ # so we can swap! (actually, move a[j-1] -> a[i])
+ a[i] = a[j-1]
+ j -= 1
+
+ return i
+
+
+cdef H3int[:] empty_memory_view():
+ # todo: get rid of this?
+ # there's gotta be a better way to do this...
+ # create an empty cython.view.array?
+ cdef:
+ H3int a[1]
+
+ return (a)[:0]
+
+
+cdef _remove_zeros(H3MemoryManager x):
+ x.n = move_nonzeros(x.ptr, x.n)
+
+ if x.n == 0:
+ h3_free(x.ptr)
+ x.ptr = NULL
+ else:
+ x.ptr = h3_realloc(x.ptr, x.n*sizeof(H3int))
+ if not x.ptr:
+ raise MemoryError()
+
+
+cdef H3int[:] _copy_to_mv(const H3int* ptr, size_t n):
+ cdef:
+ array arr
+
+ arr = ptr
+ arr.callback_free_data = h3_free
+
+ return arr
+
+
+cdef H3int[:] _create_mv(H3MemoryManager x):
+ if x.n == 0:
+ h3_free(x.ptr)
+ x.ptr = NULL
+ mv = empty_memory_view()
+ else:
+ mv = _copy_to_mv(x.ptr, x.n)
+
+ # responsibility for the memory moves from this object to the array/memoryview
+ x.ptr = NULL
+ x.n = 0
+
+ return mv
+
+
+"""
+TODO: The not None declaration for the argument automatically rejects None values as input, which would otherwise be allowed. The reason why None is allowed by default is that it is conveniently used for return arguments:
+ https://cython.readthedocs.io/en/latest/src/userguide/memoryviews.html#syntax
+
+TODO: potential optimization: https://cython.readthedocs.io/en/latest/src/userguide/memoryviews.html#performance-disabling-initialization-checks
+
+## future improvements:
+
+- abolish any appearance of &thing[0]. (i.e., identical interfaces)
+- can i make the interface for all these memory views identical?
+"""
+
+cdef class H3MemoryManager:
+ """
+ Cython object in charge of allocating and freeing memory for arrays
+ of H3 indexes.
+
+ Initially allocates memory and provides access through `self.ptr` and
+ `self.n`.
+
+ The `to_mv()` function removes responsibility for the allocated memory
+ from this object to a memory view object. A memory view object automatically
+ deallocates its memory during garbage collection.
+
+ If the H3MemoryManager is garbage collected before running `to_mv()`,
+ it will deallocate its memory itself.
+
+ This pattern is useful for a few reasons:
+
+ - provide convenient access to the raw memory pointer and length for passing
+ to h3lib functions
+ - remove zeroes from the array output (some h3lib functions may return
+ results with zeros/H3NULL values)
+ - cython and python array types have weird interfaces; memoryviews are
+ much cleaner
+
+ If we find a better way to do these then this class may no longer be
+ necessary.
+
+ TODO: consider a context manager pattern
+ """
+ def __cinit__(self, size_t n):
+ self.n = n
+ self.ptr = h3_calloc(self.n, sizeof(H3int))
+
+ if not self.ptr:
+ raise MemoryError()
+
+ cdef H3int[:] to_mv_keep_zeros(self):
+ # todo: this could be a private method
+ return _create_mv(self)
+
+ cdef H3int[:] to_mv(self):
+ _remove_zeros(self)
+ return _create_mv(self)
+
+ def __dealloc__(self):
+ # If the memory has been handed off to a memoryview, this pointer
+ # should be NULL, and deallocing on NULL is fine.
+ # If the pointer is *not* NULL, then this means the MemoryManager
+ # has is still responsible for the memory (it hasn't given the memory away to another object).
+ h3_free(self.ptr)
+
+
+"""
+todo: combine with the H3MemoryManager using fused types?
+https://cython.readthedocs.io/en/stable/src/userguide/fusedtypes.html
+"""
+cdef int[:] int_mv(size_t n):
+ cdef:
+ array arr
+
+ if n == 0:
+ raise MemoryError()
+ else:
+ ptr = h3_calloc(n, sizeof(int))
+ if ptr is NULL:
+ raise MemoryError()
+
+ arr = ptr
+ arr.callback_free_data = h3_free
+
+ return arr
+
+
+cpdef H3int[:] iter_to_mv(cells):
+ """ cells needs to be an iterable that knows its size...
+ or should we have it match the np.fromiter function, which infers if not available?
+ """
+ cdef:
+ H3int[:] mv
+
+ n = len(cells)
+ mv = H3MemoryManager(n).to_mv_keep_zeros()
+
+ for i,h in enumerate(cells):
+ mv[i] = h
+
+ return mv
diff --git a/frogpilot/third_party/h3/_cy/to_multipoly.cpython-312-aarch64-linux-gnu.so b/frogpilot/third_party/h3/_cy/to_multipoly.cpython-312-aarch64-linux-gnu.so
new file mode 100644
index 00000000..081d3f83
Binary files /dev/null and b/frogpilot/third_party/h3/_cy/to_multipoly.cpython-312-aarch64-linux-gnu.so differ
diff --git a/frogpilot/third_party/h3/_cy/to_multipoly.pyx b/frogpilot/third_party/h3/_cy/to_multipoly.pyx
new file mode 100644
index 00000000..db29baa5
--- /dev/null
+++ b/frogpilot/third_party/h3/_cy/to_multipoly.pyx
@@ -0,0 +1,60 @@
+cimport h3lib
+from h3lib cimport H3int
+from .util cimport check_cell, coord2deg
+
+
+# todo: it's driving me crazy that these three functions are all essentially the same linked list walker...
+# grumble: no way to do iterators in with cdef functions!
+cdef walk_polys(const h3lib.LinkedGeoPolygon* L):
+ out = []
+ while L:
+ out += [walk_loops(L.data)]
+ L = L.next
+
+ return out
+
+
+cdef walk_loops(const h3lib.LinkedGeoLoop* L):
+ out = []
+ while L:
+ out += [walk_coords(L.data)]
+ L = L.next
+
+ return out
+
+
+cdef walk_coords(const h3lib.LinkedLatLng* L):
+ out = []
+ while L:
+ out += [coord2deg(L.data)]
+ L = L.next
+
+ return out
+
+# todo: tuples instead of lists?
+def _to_multi_polygon(const H3int[:] cells):
+ cdef:
+ h3lib.LinkedGeoPolygon polygon
+
+ for h in cells:
+ check_cell(h)
+
+ h3lib.cellsToLinkedMultiPolygon(&cells[0], len(cells), &polygon)
+
+ out = walk_polys(&polygon)
+
+ # we're still responsible for cleaning up the passed in `polygon`,
+ # but not a problem here, since it is stack allocated
+ h3lib.destroyLinkedMultiPolygon(&polygon)
+
+ return out
+
+
+def cells_to_multi_polygon(const H3int[:] cells):
+ # todo: gotta be a more elegant way to handle these...
+ if len(cells) == 0:
+ return []
+
+ multipoly = _to_multi_polygon(cells)
+
+ return multipoly
diff --git a/frogpilot/third_party/h3/_cy/util.cpython-312-aarch64-linux-gnu.so b/frogpilot/third_party/h3/_cy/util.cpython-312-aarch64-linux-gnu.so
new file mode 100644
index 00000000..4f3eab6e
Binary files /dev/null and b/frogpilot/third_party/h3/_cy/util.cpython-312-aarch64-linux-gnu.so differ
diff --git a/frogpilot/third_party/h3/_cy/util.pxd b/frogpilot/third_party/h3/_cy/util.pxd
new file mode 100644
index 00000000..4d7d962c
--- /dev/null
+++ b/frogpilot/third_party/h3/_cy/util.pxd
@@ -0,0 +1,13 @@
+from .h3lib cimport H3int, H3str, LatLng
+
+cdef LatLng deg2coord(double lat, double lng) nogil
+cdef (double, double) coord2deg(LatLng c) nogil
+
+cpdef H3int str_to_int(H3str h) except? 0
+cpdef H3str int_to_str(H3int x)
+
+cdef check_cell(H3int h)
+cdef check_edge(H3int e)
+cdef check_res(int res)
+cdef check_vertex(H3int v)
+cdef check_distance(int k)
diff --git a/frogpilot/third_party/h3/_cy/util.pyx b/frogpilot/third_party/h3/_cy/util.pyx
new file mode 100644
index 00000000..7b8a9f20
--- /dev/null
+++ b/frogpilot/third_party/h3/_cy/util.pyx
@@ -0,0 +1,89 @@
+from .h3lib cimport H3int, H3str, isValidCell, isValidDirectedEdge, isValidVertex
+
+cimport h3lib
+
+from .error_system import (
+ H3ResDomainError,
+ H3DomainError,
+ H3DirEdgeInvalidError,
+ H3CellInvalidError,
+ H3VertexInvalidError
+)
+
+cdef h3lib.LatLng deg2coord(double lat, double lng) nogil:
+ cdef:
+ h3lib.LatLng c
+
+ c.lat = h3lib.degsToRads(lat)
+ c.lng = h3lib.degsToRads(lng)
+
+ return c
+
+
+cdef (double, double) coord2deg(h3lib.LatLng c) nogil:
+ return (
+ h3lib.radsToDegs(c.lat),
+ h3lib.radsToDegs(c.lng)
+ )
+
+
+cpdef basestring c_version():
+ v = (
+ h3lib.H3_VERSION_MAJOR,
+ h3lib.H3_VERSION_MINOR,
+ h3lib.H3_VERSION_PATCH,
+ )
+
+ return '{}.{}.{}'.format(*v)
+
+
+cpdef H3int str_to_int(H3str h) except? 0:
+ return int(h, 16)
+
+
+cpdef H3str int_to_str(H3int x):
+ """ Convert H3 integer to hex string representation
+
+ Need to be careful in Python 2 because `hex(x)` may return a string
+ with a trailing `L` character (denoting a "large" integer).
+ The formatting approach below avoids this.
+
+ Also need to be careful about unicode/str differences.
+ """
+ return '{:x}'.format(x)
+
+
+cdef check_cell(H3int h):
+ """ Check if valid H3 "cell" (hexagon or pentagon).
+
+ Does not check if a valid H3 edge, for example.
+
+ Since this function is used by multiple interfaces (int or str),
+ we want the error message to be informative to the user
+ in either case.
+
+ We use the builtin `hex` function instead of `int_to_str` to
+ prepend `0x` to indicate that this **integer** representation
+ is incorrect, but in a format that is easily compared to
+ `str` inputs.
+ """
+ if isValidCell(h) == 0:
+ raise H3CellInvalidError('Integer is not a valid H3 cell: {}'.format(hex(h)))
+
+cdef check_edge(H3int e):
+ if isValidDirectedEdge(e) == 0:
+ raise H3DirEdgeInvalidError('Integer is not a valid H3 edge: {}'.format(hex(e)))
+
+cdef check_vertex(H3int v):
+ if isValidVertex(v) == 0:
+ raise H3VertexInvalidError('Integer is not a valid H3 vertex: {}'.format(hex(v)))
+
+cdef check_res(int res):
+ if (res < 0) or (res > 15):
+ raise H3ResDomainError(res)
+
+cdef check_distance(int k):
+ if k < 0:
+ raise H3DomainError(
+ 'Grid distances must be nonnegative. Received: {}'.format(k)
+ )
diff --git a/frogpilot/third_party/h3/_cy/vertex.cpython-312-aarch64-linux-gnu.so b/frogpilot/third_party/h3/_cy/vertex.cpython-312-aarch64-linux-gnu.so
new file mode 100644
index 00000000..25556dc1
Binary files /dev/null and b/frogpilot/third_party/h3/_cy/vertex.cpython-312-aarch64-linux-gnu.so differ
diff --git a/frogpilot/third_party/h3/_cy/vertex.pxd b/frogpilot/third_party/h3/_cy/vertex.pxd
new file mode 100644
index 00000000..98d890ce
--- /dev/null
+++ b/frogpilot/third_party/h3/_cy/vertex.pxd
@@ -0,0 +1,6 @@
+from .h3lib cimport bool, H3int
+
+cpdef H3int cell_to_vertex(H3int h, int vertex_num) except 1
+cpdef H3int[:] cell_to_vertexes(H3int h)
+cpdef (double, double) vertex_to_latlng(H3int v) except *
+cpdef bool is_valid_vertex(H3int v)
diff --git a/frogpilot/third_party/h3/_cy/vertex.pyx b/frogpilot/third_party/h3/_cy/vertex.pyx
new file mode 100644
index 00000000..26a674af
--- /dev/null
+++ b/frogpilot/third_party/h3/_cy/vertex.pyx
@@ -0,0 +1,54 @@
+cimport h3lib
+from h3lib cimport bool, H3int
+
+from .util cimport (
+ check_cell,
+ check_vertex,
+ coord2deg
+)
+
+from .error_system cimport check_for_error
+
+from .memory cimport H3MemoryManager
+
+
+cpdef H3int cell_to_vertex(H3int h, int vertex_num) except 1:
+ cdef:
+ H3int out
+
+ check_cell(h)
+
+ check_for_error(
+ h3lib.cellToVertex(h, vertex_num, &out)
+ )
+
+ return out
+
+cpdef H3int[:] cell_to_vertexes(H3int h):
+ cdef:
+ H3int out
+
+ check_cell(h)
+
+ hmm = H3MemoryManager(6)
+ check_for_error(
+ h3lib.cellToVertexes(h, hmm.ptr)
+ )
+ mv = hmm.to_mv()
+
+ return mv
+
+cpdef (double, double) vertex_to_latlng(H3int v) except *:
+ cdef:
+ h3lib.LatLng c
+
+ check_vertex(v)
+
+ check_for_error(
+ h3lib.vertexToLatLng(v, &c)
+ )
+
+ return coord2deg(c)
+
+cpdef bool is_valid_vertex(H3int v):
+ return h3lib.isValidVertex(v) == 1
diff --git a/frogpilot/third_party/h3/_h3shape.py b/frogpilot/third_party/h3/_h3shape.py
new file mode 100644
index 00000000..5c362951
--- /dev/null
+++ b/frogpilot/third_party/h3/_h3shape.py
@@ -0,0 +1,338 @@
+from abc import ABCMeta, abstractmethod
+
+
+class H3Shape(metaclass=ABCMeta):
+ """
+ Abstract parent class of ``LatLngPoly`` and ``LatLngMultiPoly``.
+ """
+ @property
+ @abstractmethod
+ def __geo_interface__(self):
+ """ https://github.com/pytest-dev/pytest-cov/issues/428 """
+
+
+class LatLngPoly(H3Shape):
+ """
+ Container for loops of lat/lng points describing a polygon, possibly with holes.
+
+ Attributes
+ ----------
+ outer : list[tuple[float, float]]
+ List of lat/lng points describing the outer loop of the polygon
+
+ holes : list[list[tuple[float, float]]]
+ List of loops of lat/lng points describing the holes of the polygon
+
+ Examples
+ --------
+
+ A polygon with a single outer ring consisting of 4 points, having no holes:
+
+ >>> LatLngPoly(
+ ... [(37.68, -122.54), (37.68, -122.34), (37.82, -122.34), (37.82, -122.54)],
+ ... )
+
+
+ The same polygon, but with one hole consisting of 3 points:
+
+ >>> LatLngPoly(
+ ... [(37.68, -122.54), (37.68, -122.34), (37.82, -122.34), (37.82, -122.54)],
+ ... [(37.76, -122.51), (37.76, -122.44), (37.81, -122.51)],
+ ... )
+
+
+ The same as above, but with one additional hole, made up of 5 points:
+
+ >>> LatLngPoly(
+ ... [(37.68, -122.54), (37.68, -122.34), (37.82, -122.34), (37.82, -122.54)],
+ ... [(37.76, -122.51), (37.76, -122.44), (37.81, -122.51)],
+ ... [(37.71, -122.43), (37.71, -122.37), (37.73, -122.37), (37.75, -122.41),
+ ... (37.73, -122.43)],
+ ... )
+
+ """
+ def __init__(self, outer, *holes):
+ loops = [outer] + list(holes)
+ for loop in loops:
+ if len(loop) in (1, 2):
+ raise ValueError('Non-empty LatLngPoly loops need at least 3 points.')
+
+ point_dimensions = set(map(len, loop))
+ # empty set is possible for empty polygons, so we check if a subset
+ if not (point_dimensions <= {2}):
+ raise ValueError('LatLngPoly only accepts 2D points: lat/lng.')
+
+ self.outer = tuple(_open_ring(outer))
+ self.holes = tuple(
+ _open_ring(hole)
+ for hole in holes
+ )
+
+ def __repr__(self):
+ return ''.format(self.loopcode)
+
+ def __len__(self):
+ """
+ Should this be the number of points in the outer loop,
+ the number of holes (or +1 for the outer loop)?
+ """
+ raise NotImplementedError('No clear definition of length for LatLngPoly.')
+
+ @property
+ def loopcode(self):
+ """ Short code for describing the length of the outer loop and each hole
+
+ Example: ``[382/(18, 6, 6)]`` indicates an outer loop of 382 points,
+ along with 3 holes with 18, 6, and 6 points, respectively.
+
+ Example: ``[15]`` indicates an outer loop of 15 points and no holes.
+ """
+ outer = len(self.outer)
+ holes = tuple(map(len, self.holes))
+
+ outer = str(outer)
+
+ if holes:
+ out = outer + '/' + str(holes)
+ else:
+ out = outer
+
+ return '[' + out + ']'
+
+ @property
+ def __geo_interface__(self):
+ ll2 = _polygon_to_LL2(self)
+ gj_dict = _LL2_to_geojson_dict(ll2)
+
+ return gj_dict
+
+
+class LatLngMultiPoly(H3Shape):
+ """
+ Container for multiple ``LatLngPoly`` polygons.
+
+ Attributes
+ ----------
+ polys : list[LatLngPoly]
+ List of lat/lng points describing the outer loop of the polygon
+ """
+ def __init__(self, *polys):
+ self.polys = tuple(polys)
+
+ for p in self.polys:
+ if not isinstance(p, LatLngPoly):
+ raise ValueError('LatLngMultiPoly requires each input to be an LatLngPoly object, instead got: ' + str(p)) # noqa
+
+ def __repr__(self):
+ out = [p.loopcode for p in self.polys]
+ out = ', '.join(out)
+ out = ''.format(out)
+ return out
+
+ def __iter__(self):
+ return iter(self.polys)
+
+ def __len__(self):
+ """ Give the number of polygons in this multi-polygon.
+ """
+
+ """
+ TODO: Pandas series or dataframe representation changes depending
+ on if __len__ is defined.
+
+ I'd prefer the one that states `LatLngMultiPoly`.
+ It seems like Pandas is assuming an iterable is best-described
+ by its elements when choosing the representation.
+
+ when __len__ *IS NOT* defined:
+
+ 0
+ 1 , , )
+ 1 (, , , , , , <...
+ """
+ return len(self.polys)
+
+ def __getitem__(self, index):
+ return self.polys[index]
+
+ @property
+ def __geo_interface__(self):
+ ll3 = _mpoly_to_LL3(self)
+ gj_dict = _LL3_to_geojson_dict(ll3)
+
+ return gj_dict
+
+
+"""
+Helpers for cells_to_geojson and geojson_to_cells.
+
+Dealing with GeoJSON Polygons and MultiPolygons can be confusing because
+there are so many nested lists. To help keep track, we use the following
+symbols to denote different levels of nesting.
+
+LL0: lat/lng or lng/lat pair
+LL1: list of LL0s
+LL2: list of LL1s (i.e., a polygon with holes)
+LL3: list of LL2s (i.e., several polygons with holes)
+
+
+## TODO
+
+- Allow user to specify "container" in `cells_to_geojson`.
+ - That is, they may want a MultiPolygon even if the output fits in a Polygon
+ - 'auto', Polygon, MultiPolygon, FeatureCollection, GeometryCollection, ...
+"""
+
+
+def _mpoly_to_LL3(mpoly):
+ ll3 = tuple(
+ _polygon_to_LL2(poly)
+ for poly in mpoly
+ )
+
+ return ll3
+
+
+def _LL3_to_mpoly(ll3):
+ polys = [
+ _LL2_to_polygon(ll2)
+ for ll2 in ll3
+ ]
+
+ mpoly = LatLngMultiPoly(*polys)
+
+ return mpoly
+
+
+def _polygon_to_LL2(poly):
+ ll2 = [poly.outer] + list(poly.holes)
+ ll2 = tuple(
+ _close_ring(_swap_latlng(ll1))
+ for ll1 in ll2
+ )
+
+ return ll2
+
+
+def _remove_z(ll1):
+ ll1 = [(a, b) for a, b, *z in ll1]
+ return ll1
+
+
+def _LL2_to_polygon(ll2):
+ ll2 = [
+ _remove_z(ll1)
+ for ll1 in ll2
+ ]
+
+ ll2 = [
+ _swap_latlng(ll1)
+ for ll1 in ll2
+ ]
+ h3poly = LatLngPoly(*ll2)
+
+ return h3poly
+
+
+def _LL2_to_geojson_dict(ll2):
+ gj_dict = {
+ 'type': 'Polygon',
+ 'coordinates': ll2,
+ }
+
+ return gj_dict
+
+
+def _LL3_to_geojson_dict(ll3):
+ gj_dict = {
+ 'type': 'MultiPolygon',
+ 'coordinates': ll3,
+ }
+
+ return gj_dict
+
+
+def _swap_latlng(ll1):
+ ll1 = tuple(
+ (b, a) for a, b in ll1
+ )
+ return ll1
+
+
+def _close_ring(ll1):
+ """
+ Idempotent
+ """
+ if ll1 and (ll1[0] != ll1[-1]):
+ ll1 = tuple(ll1) + (ll1[0],)
+
+ return ll1
+
+
+def _open_ring(ll1):
+ """
+ Idempotent
+ """
+ if ll1 and (ll1[0] == ll1[-1]):
+ ll1 = ll1[:-1]
+
+ return ll1
+
+
+def geo_to_h3shape(geo):
+ """
+ Translate from ``__geo_interface__`` to H3Shape.
+
+ ``geo`` either implements ``__geo_interface__`` or is a dict matching the format
+
+ Returns
+ -------
+ H3Shape
+ """
+
+ # geo can be dict, a __geo_interface__, a string, LatLngPoly or LatLngMultiPoly
+ if isinstance(geo, H3Shape):
+ return geo
+
+ if hasattr(geo, '__geo_interface__'):
+ # get dict
+ geo = geo.__geo_interface__
+
+ assert isinstance(geo, dict) # todo: remove
+
+ t = geo['type']
+ coord = geo['coordinates']
+
+ if t == 'Polygon':
+ ll2 = coord
+ shape = _LL2_to_polygon(ll2)
+ elif t == 'MultiPolygon':
+ ll3 = coord
+ shape = _LL3_to_mpoly(ll3)
+ else:
+ raise ValueError('Unrecognized type: ' + str(t))
+
+ return shape
+
+
+def h3shape_to_geo(h3shape):
+ """
+ Translate from an ``H3Shape`` to a ``__geo_interface__`` dict.
+
+ ``h3shape`` should be either ``LatLngPoly`` or ``LatLngMultiPoly``
+
+ Returns
+ -------
+ dict
+ """
+ return h3shape.__geo_interface__
diff --git a/frogpilot/third_party/h3/_version.py b/frogpilot/third_party/h3/_version.py
new file mode 100644
index 00000000..712565a5
--- /dev/null
+++ b/frogpilot/third_party/h3/_version.py
@@ -0,0 +1,3 @@
+from importlib import metadata
+
+__version__ = metadata.version(__package__ or __name__)
diff --git a/frogpilot/third_party/h3/api/__init__.py b/frogpilot/third_party/h3/api/__init__.py
new file mode 100644
index 00000000..435b13a1
--- /dev/null
+++ b/frogpilot/third_party/h3/api/__init__.py
@@ -0,0 +1,6 @@
+# flake8: noqa
+
+from . import basic_int
+from . import basic_str
+from . import memview_int
+from . import numpy_int
diff --git a/frogpilot/third_party/h3/api/basic_int/__init__.py b/frogpilot/third_party/h3/api/basic_int/__init__.py
new file mode 100644
index 00000000..09f7deb8
--- /dev/null
+++ b/frogpilot/third_party/h3/api/basic_int/__init__.py
@@ -0,0 +1,1240 @@
+# This file is **symlinked** across the APIs to ensure they are exactly the same.
+from typing import Literal
+
+from ... import _cy
+from ..._h3shape import (
+ H3Shape,
+ LatLngPoly,
+ LatLngMultiPoly,
+ geo_to_h3shape,
+ h3shape_to_geo,
+)
+
+from ._convert import (
+ _in_scalar,
+ _out_scalar,
+ _in_collection,
+ _out_collection,
+)
+
+
+def versions():
+ """
+ Version numbers for the Python (wrapper) and C (wrapped) libraries.
+
+ Versions are output as strings of the form ``'X.Y.Z'``.
+ C and Python should match on ``X`` (major) and ``Y`` (minor),
+ but may differ on ``Z`` (patch).
+
+ Returns
+ -------
+ dict like ``{'c': 'X.Y.Z', 'python': 'A.B.C'}``
+ """
+ from ..._version import __version__
+
+ v = {
+ 'c': _cy.c_version(),
+ 'python': __version__,
+ }
+
+ return v
+
+
+def str_to_int(h):
+ """
+ Converts a hexadecimal string to an H3 64-bit integer index.
+
+ Parameters
+ ----------
+ h : str
+ Hexadecimal string like ``'89754e64993ffff'``
+
+ Returns
+ -------
+ int
+ Unsigned 64-bit integer
+ """
+ return _cy.str_to_int(h)
+
+
+def int_to_str(x):
+ """
+ Converts an H3 64-bit integer index to a hexadecimal string.
+
+ Parameters
+ ----------
+ x : int
+ Unsigned 64-bit integer
+
+ Returns
+ -------
+ str
+ Hexadecimal string like ``'89754e64993ffff'``
+ """
+ return _cy.int_to_str(x)
+
+
+def get_num_cells(res):
+ """
+ Return the total number of *cells* (hexagons and pentagons)
+ for the given resolution.
+
+ Returns
+ -------
+ int
+ """
+ return _cy.get_num_cells(res)
+
+
+def average_hexagon_area(res, unit='km^2'):
+ """
+ Return the average area of an H3 *hexagon*
+ for the given resolution.
+
+ This average *excludes* pentagons.
+
+ Parameters
+ ----------
+ res : int
+ H3 resolution
+ unit: str
+ Unit for area result (``'km^2'``, ``'m^2'``, or ``'rads^2'``)
+
+ Returns
+ -------
+ float
+ """
+ return _cy.average_hexagon_area(res, unit)
+
+
+def average_hexagon_edge_length(res, unit='km'):
+ """
+ Return the average *hexagon* edge length
+ for the given resolution.
+
+ This average *excludes* pentagons.
+
+ Parameters
+ ----------
+ res : int
+ H3 resolution
+ unit: str
+ Unit for length result (``'km'``, ``'m'``, or ``'rads'``)
+
+ Returns
+ -------
+ float
+ """
+ return _cy.average_hexagon_edge_length(res, unit)
+
+
+def is_valid_cell(h):
+ """
+ Validates an H3 cell (hexagon or pentagon).
+
+ Returns
+ -------
+ bool
+ """
+ try:
+ h = _in_scalar(h)
+ return _cy.is_valid_cell(h)
+ except (ValueError, TypeError):
+ return False
+
+
+def is_valid_directed_edge(edge):
+ """
+ Validates an H3 unidirectional edge.
+
+ Returns
+ -------
+ bool
+ """
+ try:
+ e = _in_scalar(edge)
+ return _cy.is_valid_directed_edge(e)
+ except (ValueError, TypeError):
+ return False
+
+
+def latlng_to_cell(lat, lng, res):
+ """
+ Return the cell containing the (lat, lng) point
+ for a given resolution.
+
+ Returns
+ -------
+ H3Cell
+
+ """
+ return _out_scalar(_cy.latlng_to_cell(lat, lng, res))
+
+
+def cell_to_latlng(h):
+ """
+ Return the center point of an H3 cell as a lat/lng pair.
+
+ Parameters
+ ----------
+ h : H3Cell
+
+ Returns
+ -------
+ lat : float
+ Latitude
+ lng : float
+ Longitude
+ """
+ h = _in_scalar(h)
+ return _cy.cell_to_latlng(h)
+
+
+def get_resolution(h):
+ """
+ Return the resolution of an H3 cell.
+
+ Parameters
+ ----------
+ h : H3Cell
+
+ Returns
+ -------
+ int
+ """
+ # todo: could also work for edges
+ h = _in_scalar(h)
+ return _cy.get_resolution(h)
+
+
+def cell_to_parent(h, res=None):
+ """
+ Get the parent of a cell.
+
+ Parameters
+ ----------
+ h : H3Cell
+ res : int or None, optional
+ The resolution for the parent
+ If ``None``, then ``res = resolution(h) - 1``
+
+ Returns
+ -------
+ H3Cell
+ """
+ h = _in_scalar(h)
+ p = _cy.cell_to_parent(h, res)
+ p = _out_scalar(p)
+
+ return p
+
+
+def grid_distance(h1, h2):
+ """
+ Compute the grid distance between two cells.
+
+ The grid distance is defined as the length of the shortest
+ path between the cells in the graph formed by connecting
+ adjacent cells.
+
+ This function will raise an exception if the
+ cells are too far apart to compute the distance.
+
+ Parameters
+ ----------
+ h1 : H3Cell
+ h2 : H3Cell
+
+ Returns
+ -------
+ int
+ """
+ h1 = _in_scalar(h1)
+ h2 = _in_scalar(h2)
+
+ d = _cy.grid_distance(h1, h2)
+
+ return d
+
+
+def cell_to_boundary(h):
+ """
+ Return tuple of lat/lng pairs describing the cell boundary.
+
+ Parameters
+ ----------
+ h : H3Cell
+
+ Returns
+ -------
+ tuple of (lat, lng) tuples
+ """
+ h = _in_scalar(h)
+ return _cy.cell_to_boundary(h)
+
+
+def grid_disk(h, k=1):
+ """
+ Return unordered collection of cells with grid distance ``<= k`` from ``h``.
+ That is, the "filled-in" disk.
+
+ Parameters
+ ----------
+ h : H3Cell
+ k : int
+ Size of disk.
+
+ Returns
+ -------
+ unordered collection of H3Cell
+
+ Notes
+ -----
+ There is currently no guaranteed order of the output cells.
+ """
+ h = _in_scalar(h)
+ mv = _cy.grid_disk(h, k)
+
+ return _out_collection(mv)
+
+
+def grid_ring(h, k=1):
+ """
+ Return unordered collection of cells with grid distance ``== k`` from ``h``.
+ That is, the "hollow" ring.
+
+ Parameters
+ ----------
+ h : H3Cell
+ k : int
+ Size of ring.
+
+ Returns
+ -------
+ unordered collection of H3Cell
+
+ Notes
+ -----
+ There is currently no guaranteed order of the output cells.
+ """
+ h = _in_scalar(h)
+ mv = _cy.grid_ring(h, k)
+
+ return _out_collection(mv)
+
+
+def cell_to_children_size(h, res=None):
+ """
+ Number of children at resolution ``res`` of given cell.
+
+ Parameters
+ ----------
+ h : H3Cell
+ res : int or None, optional
+ The resolution for the children.
+ If ``None``, then ``res = resolution(h) + 1``
+
+ Returns
+ -------
+ int
+ Count of children
+ """
+ h = _in_scalar(h)
+ return _cy.cell_to_children_size(h, res)
+
+
+def cell_to_children(h, res=None):
+ """
+ Children of a cell as an unordered collection.
+
+ Parameters
+ ----------
+ h : H3Cell
+ res : int or None, optional
+ The resolution for the children.
+ If ``None``, then ``res = resolution(h) + 1``
+
+ Returns
+ -------
+ unordered collection of H3Cell
+
+ Notes
+ -----
+ There is currently no guaranteed order of the output cells.
+ """
+ h = _in_scalar(h)
+ mv = _cy.cell_to_children(h, res)
+
+ return _out_collection(mv)
+
+
+def cell_to_child_pos(child, res_parent):
+ """
+ Child position index of given cell, with respect to its parent at ``res_parent``.
+
+ The reverse operation can be done with ``child_pos_to_cell``.
+
+ Parameters
+ ----------
+ child : H3Cell
+ res_parent : int
+
+ Returns
+ -------
+ int
+ Integer index of the child with respect to parent cell.
+ """
+ child = _in_scalar(child)
+ return _cy.cell_to_child_pos(child, res_parent)
+
+
+def child_pos_to_cell(parent, res_child, child_pos):
+ """
+ Get child H3 cell from a parent cell, child resolution, and child position index.
+
+ The reverse operation can be done with ``cell_to_child_pos``.
+
+ Parameters
+ ----------
+ parent : H3Cell
+ res_child : int
+ Child cell resolution
+ child_pos : int
+ Integer position of child cell, releative to parent.
+
+
+ Returns
+ -------
+ H3Cell
+ """
+ parent = _in_scalar(parent)
+ child = _cy.child_pos_to_cell(parent, res_child, child_pos)
+ child = _out_scalar(child)
+
+ return child
+
+
+# todo: nogil for expensive C operation?
+def compact_cells(cells):
+ """
+ Compact a collection of H3 cells by combining
+ smaller cells into larger cells, if all child cells
+ are present. Input cells must all share the same resolution.
+
+ Parameters
+ ----------
+ cells : iterable of H3 Cells
+
+ Returns
+ -------
+ unordered collection of H3Cell
+
+ Notes
+ -----
+ There is currently no guaranteed order of the output cells.
+ """
+ hu = _in_collection(cells)
+ hc = _cy.compact_cells(hu)
+
+ return _out_collection(hc)
+
+
+def uncompact_cells(cells, res):
+ """
+ Reverse the ``compact_cells`` operation.
+
+ Return a collection of H3 cells, all of resolution ``res``.
+
+ Parameters
+ ----------
+ cells : iterable of H3Cell
+ res : int
+ Resolution of desired output cells.
+
+ Returns
+ -------
+ unordered collection of H3Cell
+
+ Notes
+ -----
+ There is currently no guaranteed order of the output cells.
+ """
+ # TODO: add test to make sure an error is returned when input contains cell
+ # smaller than output res.
+
+ hc = _in_collection(cells)
+ hu = _cy.uncompact_cells(hc, res)
+
+ return _out_collection(hu)
+
+
+def polygon_to_cells(h3shape, res):
+ """
+ Alias for ``h3shape_to_cells``.
+ """
+ return h3shape_to_cells(h3shape, res)
+
+
+def h3shape_to_cells(h3shape, res):
+ """
+ Return the collection of H3 cells at a given resolution whose center points
+ are contained within an ``LatLngPoly`` or ``LatLngMultiPoly``.
+
+ Parameters
+ ----------
+ h3shape : ``H3Shape``
+ res : int
+ Resolution of the output cells
+
+ Returns
+ -------
+ list of H3Cell
+
+ Examples
+ --------
+
+ >>> poly = LatLngPoly(
+ ... [(37.68, -122.54), (37.68, -122.34), (37.82, -122.34),
+ ... (37.82, -122.54)],
+ ... )
+ >>> h3.h3shape_to_cells(poly, 6)
+ ['862830807ffffff',
+ '862830827ffffff',
+ '86283082fffffff',
+ '862830877ffffff',
+ '862830947ffffff',
+ '862830957ffffff',
+ '86283095fffffff']
+
+ Notes
+ -----
+ There is currently no guaranteed order of the output cells.
+ """
+
+ # todo: not sure if i want this dispatch logic here. maybe in the objects?
+ if isinstance(h3shape, LatLngPoly):
+ poly = h3shape
+ mv = _cy.polygon_to_cells(poly.outer, res=res, holes=poly.holes)
+ elif isinstance(h3shape, LatLngMultiPoly):
+ mpoly = h3shape
+ mv = _cy.polygons_to_cells(mpoly.polys, res=res)
+ elif isinstance(h3shape, H3Shape):
+ raise ValueError('Unrecognized H3Shape: ' + str(h3shape))
+ else:
+ raise ValueError('Unrecognized type: ' + str(type(h3shape)))
+
+ return _out_collection(mv)
+
+
+def polygon_to_cells_experimental(
+ h3shape: H3Shape,
+ res: int,
+ contain: Literal['center', 'full', 'overlap', 'bbox_overlap'] = 'center',
+):
+ """
+ Alias for ``h3shape_to_cells_experimental``.
+ """
+ return h3shape_to_cells_experimental(h3shape, res, contain)
+
+
+def h3shape_to_cells_experimental(
+ h3shape: H3Shape,
+ res: int,
+ contain: Literal['center', 'full', 'overlap', 'bbox_overlap'] = 'center',
+):
+ """
+ Experimental function similar to ``h3shape_to_cells``, but with support for
+ multiple cell containment modes.
+
+ Using ``contain='center'`` should give identical behavior as
+ ``h3shape_to_cells``.
+
+ Note that this function is **experimental** and has no API stability gaurantees
+ across versions, so it may change in the future.
+
+
+ Parameters
+ ----------
+ h3shape : ``H3Shape``
+ res : int
+ Resolution of the output cells
+ contain : {'center', 'full', 'overlap', 'bbox_overlap'}, optional
+ Specifies the containment condition.
+ - 'center': Cell center is contained in shape
+ - 'full': Cell is fully contained in shape
+ - 'overlap': Cell is partially contained in shape
+ - 'bbox_overlap': Cell bounding box is partially contained in shape
+
+ Default is 'center'.
+
+ Returns
+ -------
+ list of H3Cell
+
+ Examples
+ --------
+
+ >>> poly = LatLngPoly(
+ ... [(37.68, -122.54), (37.68, -122.34), (37.82, -122.34),
+ ... (37.82, -122.54)],
+ ... )
+ >>> h3.h3shape_to_cells_experimental(poly, 6, 'center')
+ ['862830807ffffff',
+ '862830827ffffff',
+ '86283082fffffff',
+ '862830877ffffff',
+ '862830947ffffff',
+ '862830957ffffff',
+ '86283095fffffff']
+
+ Notes
+ -----
+ There is currently no guaranteed order of the output cells.
+ """
+
+ contain_modes = {
+ 'center': 0,
+ 'full': 1,
+ 'overlap': 2,
+ 'bbox_overlap': 3,
+ }
+
+ flag = contain_modes[contain]
+
+ # todo: not sure if i want this dispatch logic here. maybe in the objects?
+ if isinstance(h3shape, LatLngPoly):
+ poly = h3shape
+ mv = _cy.polygon_to_cells_experimental(
+ poly.outer,
+ res = res,
+ holes = poly.holes,
+ flag = flag,
+ )
+ elif isinstance(h3shape, LatLngMultiPoly):
+ mpoly = h3shape
+ mv = _cy.polygons_to_cells_experimental(mpoly.polys, res=res, flag=flag)
+ elif isinstance(h3shape, H3Shape):
+ raise ValueError('Unrecognized H3Shape: ' + str(h3shape))
+ else:
+ raise ValueError('Unrecognized type: ' + str(type(h3shape)))
+
+ return _out_collection(mv)
+
+
+def cells_to_h3shape(cells, *, tight=True):
+ """
+ Return an ``H3Shape`` describing the area covered by a collection of H3 cells.
+ Will return ``LatLngPoly`` or ``LatLngMultiPoly``.
+
+ Parameters
+ ----------
+ cells : iterable of H3 cells
+ tight : bool
+ If True, return ``LatLngPoly`` if possible.
+ If False, always return ``LatLngMultiPoly``.
+
+ Returns
+ -------
+ LatLngPoly | LatLngMultiPoly
+
+ Examples
+ --------
+
+ >>> cells = ['8428309ffffffff', '842830dffffffff']
+ >>> h3.cells_to_h3shape(cells, tight=True)
+
+ >>> h3.cells_to_h3shape(cells, tight=False)
+
+ """
+ cells = _in_collection(cells)
+ mpoly = _cy.cells_to_multi_polygon(cells)
+
+ polys = [LatLngPoly(*poly) for poly in mpoly]
+ out = LatLngMultiPoly(*polys)
+
+ if tight and len(out) == 1:
+ out = out[0]
+
+ return out
+
+
+def geo_to_cells(geo, res):
+ """Convert from ``__geo_interface__`` to cells.
+
+ Parameters
+ ----------
+ geo : an object implementing ``__geo_interface__`` or a dictionary in that format.
+ Both ``LatLngPoly`` and ``LatLngMultiPoly`` implement the interface.
+ res : int
+ Resolution of desired output cells.
+
+ Notes
+ -----
+ There is currently no guaranteed order of the output cells.
+ """
+ h3shape = geo_to_h3shape(geo)
+ return h3shape_to_cells(h3shape, res)
+
+
+def cells_to_geo(cells, tight=True):
+ """
+ Convert from cells to a ``__geo_interface__`` dict.
+
+ Parameters
+ ----------
+ cells : iterable of H3 Cells
+ tight : bool
+ When ``True``, returns an ``LatLngPoly`` when possible.
+ When ``False``, always returns an ``LatLngMultiPoly``.
+
+ Returns
+ -------
+ dict
+ in `__geo_interface__` format
+ """
+ h3shape = cells_to_h3shape(cells, tight=tight)
+ return h3shape_to_geo(h3shape)
+
+
+def is_pentagon(h):
+ """
+ Identify if an H3 cell is a pentagon.
+
+ Parameters
+ ----------
+ h : H3Index
+
+ Returns
+ -------
+ bool
+ ``True`` if input is a valid H3 cell which is a pentagon.
+
+ Notes
+ -----
+ A pentagon should *also* pass ``is_valid_cell()``.
+ Will return ``False`` for valid H3Edge.
+ """
+ return _cy.is_pentagon(_in_scalar(h))
+
+
+def get_base_cell_number(h):
+ """
+ Return the base cell *number* (``0`` to ``121``) of the given cell.
+
+ The base cell *number* and the H3Index are two different representations
+ of the same cell: the parent cell of resolution ``0``.
+
+ The base cell *number* is encoded within the corresponding
+ H3Index.
+
+ todo: could work with edges
+
+ Parameters
+ ----------
+ h : H3Cell
+
+ Returns
+ -------
+ int
+ """
+ return _cy.get_base_cell_number(_in_scalar(h))
+
+
+def are_neighbor_cells(h1, h2):
+ """
+ Returns ``True`` if ``h1`` and ``h2`` are neighboring cells.
+
+ Parameters
+ ----------
+ h1 : H3Cell
+ h2 : H3Cell
+
+ Returns
+ -------
+ bool
+ """
+ h1 = _in_scalar(h1)
+ h2 = _in_scalar(h2)
+
+ return _cy.are_neighbor_cells(h1, h2)
+
+
+def cells_to_directed_edge(origin, destination):
+ """
+ Create an H3 Index denoting a unidirectional edge.
+
+ The edge is constructed from neighboring cells ``origin`` and
+ ``destination``.
+
+ Parameters
+ ----------
+ origin : H3Cell
+ destination : H3Cell
+
+ Raises
+ ------
+ ValueError
+ When cells are not adjacent.
+
+ Returns
+ -------
+ H3Edge
+ """
+ o = _in_scalar(origin)
+ d = _in_scalar(destination)
+ e = _cy.cells_to_directed_edge(o, d)
+ e = _out_scalar(e)
+
+ return e
+
+
+def get_directed_edge_origin(e):
+ """
+ Origin cell from an H3 directed edge.
+
+ Parameters
+ ----------
+ e : H3Edge
+
+ Returns
+ -------
+ H3Cell
+ """
+ e = _in_scalar(e)
+ o = _cy.get_directed_edge_origin(e)
+ o = _out_scalar(o)
+
+ return o
+
+
+def get_directed_edge_destination(e):
+ """
+ Destination cell from an H3 directed edge.
+
+ Parameters
+ ----------
+ e : H3Edge
+
+ Returns
+ -------
+ H3Cell
+ """
+ e = _in_scalar(e)
+ d = _cy.get_directed_edge_destination(e)
+ d = _out_scalar(d)
+
+ return d
+
+
+def directed_edge_to_cells(e):
+ """
+ Return (origin, destination) tuple from H3 directed edge
+
+ Parameters
+ ----------
+ e : H3Edge
+
+ Returns
+ -------
+ H3Cell
+ Origin cell of edge
+ H3Cell
+ Destination cell of edge
+ """
+ e = _in_scalar(e)
+ o, d = _cy.directed_edge_to_cells(e)
+ o, d = _out_scalar(o), _out_scalar(d)
+
+ return o, d
+
+
+def origin_to_directed_edges(origin):
+ """
+ Return all directed edges starting from ``origin`` cell.
+
+ Parameters
+ ----------
+ origin : H3Cell
+
+ Returns
+ -------
+ unordered collection of H3Edge
+ """
+ mv = _cy.origin_to_directed_edges(_in_scalar(origin))
+
+ return _out_collection(mv)
+
+
+def directed_edge_to_boundary(edge):
+ """
+ Returns points representing the edge (line of points
+ describing the boundary between two cells).
+
+ Parameters
+ ----------
+ edge : H3Edge
+
+ Returns
+ -------
+ tuple of (lat, lng) tuples
+ """
+ return _cy.directed_edge_to_boundary(_in_scalar(edge))
+
+
+def grid_path_cells(start, end):
+ """
+ Returns the ordered collection of cells denoting a
+ minimum-length non-unique path between cells.
+
+ Parameters
+ ----------
+ start : H3Cell
+ end : H3Cell
+
+ Returns
+ -------
+ ordered collection of H3Cell
+ Starting with ``start``, and ending with ``end``.
+ """
+ mv = _cy.grid_path_cells(_in_scalar(start), _in_scalar(end))
+
+ return _out_collection(mv)
+
+
+def is_res_class_III(h):
+ """
+ Determine if cell has orientation "Class II" or "Class III".
+
+ The orientation of pentagons/hexagons on the icosahedron can be one
+ of two types: "Class II" or "Class III".
+
+ All cells within a resolution have the same type, and the type
+ alternates between resolutions.
+
+ "Class II" cells have resolutions: 0,2,4,6,8,10,12,14
+ "Class III" cells have resolutions: 1,3,5,7,9,11,13,15
+
+ Parameters
+ ----------
+ h : H3Cell
+
+ Returns
+ -------
+ bool
+ ``True`` if ``h`` is "Class III".
+ ``False`` if ``h`` is "Class II".
+
+ References
+ ----------
+ 1. https://uber.github.io/h3/#/documentation/core-library/coordinate-systems
+ """
+ return _cy.is_res_class_iii(_in_scalar(h))
+
+
+def get_pentagons(res):
+ """
+ Return all pentagons at a given resolution.
+
+ Parameters
+ ----------
+ res : int
+ Resolution of the pentagons
+
+ Returns
+ -------
+ unordered collection of H3Cell
+ """
+ mv = _cy.get_pentagons(res)
+
+ return _out_collection(mv)
+
+
+def get_res0_cells():
+ """
+ Return all cells at resolution 0.
+
+ Parameters
+ ----------
+ None
+
+ Returns
+ -------
+ unordered collection of H3Cell
+
+ Notes
+ -----
+ There is currently no guaranteed order of the output cells.
+ """
+ mv = _cy.get_res0_cells()
+
+ return _out_collection(mv)
+
+
+def cell_to_center_child(h, res=None):
+ """
+ Get the center child of a cell at some finer resolution.
+
+ Parameters
+ ----------
+ h : H3Cell
+ res : int or None, optional
+ The resolution for the child cell
+ If ``None``, then ``res = resolution(h) + 1``
+
+ Returns
+ -------
+ H3Cell
+ """
+ h = _in_scalar(h)
+ p = _cy.cell_to_center_child(h, res)
+ p = _out_scalar(p)
+
+ return p
+
+
+def get_icosahedron_faces(h):
+ """
+ Return icosahedron faces intersecting a given H3 cell.
+
+ There are twenty possible faces, ranging from 0--19.
+
+ Note: Every interface returns a Python ``set`` of ``int``.
+
+ Parameters
+ ----------
+ h : H3Cell
+
+ Returns
+ -------
+ Python ``set`` of ``int``
+ """
+ h = _in_scalar(h)
+ faces = _cy.get_icosahedron_faces(h)
+
+ return faces
+
+
+def cell_to_local_ij(origin, h):
+ """
+ Return local (i,j) coordinates of cell ``h`` in relation to ``origin`` cell
+
+
+ Parameters
+ ----------
+ origin : H3Cell
+ Origin/central cell for defining i,j coordinates.
+ h: H3Cell
+ Destination cell whose i,j coordinates we'd like, based off
+ of the origin cell.
+
+
+ Returns
+ -------
+ Tuple (i, j) of integer local coordinates of cell ``h``
+
+
+ Notes
+ -----
+
+ The ``origin`` cell does not define (0, 0) for the IJ coordinate space.
+ (0, 0) refers to the center of the base cell containing origin at the
+ resolution of `origin`.
+ Subtracting the IJ coordinates of ``origin`` from every cell would get
+ you the property of (0, 0) being the ``origin``.
+
+ This is done so we don't need to keep recomputing the coordinates of
+ ``origin`` if not needed.
+ """
+ origin = _in_scalar(origin)
+ h = _in_scalar(h)
+
+ i, j = _cy.cell_to_local_ij(origin, h)
+
+ return i, j
+
+
+def local_ij_to_cell(origin, i, j):
+ """
+ Return cell at local (i,j) position relative to the ``origin`` cell.
+
+ Parameters
+ ----------
+ origin : H3Cell
+ Origin/central cell for defining i,j coordinates.
+ i, j: int
+ Integer coordinates with respect to ``origin`` cell.
+
+
+ Returns
+ -------
+ H3Cell at local (i,j) position relative to the ``origin`` cell
+
+
+ Notes
+ -----
+
+ The ``origin`` cell does not define (0, 0) for the IJ coordinate space.
+ (0, 0) refers to the center of the base cell containing origin at the
+ resolution of ``origin``.
+ Subtracting the IJ coordinates of ``origin`` from every cell would get
+ you the property of (0, 0) being the ``origin``.
+
+ This is done so we don't need to keep recomputing the coordinates of
+ ``origin`` if not needed.
+ """
+ origin = _in_scalar(origin)
+
+ h = _cy.local_ij_to_cell(origin, i, j)
+ h = _out_scalar(h)
+
+ return h
+
+
+def cell_area(h, unit='km^2'):
+ """
+ Compute the spherical surface area of a specific H3 cell.
+
+ Parameters
+ ----------
+ h : H3Cell
+ unit: str
+ Unit for area result (``'km^2'``, ``'m^2'``, or ``'rads^2'``)
+
+
+ Returns
+ -------
+ The area of the H3 cell in the given units
+
+
+ Notes
+ -----
+ This function breaks the cell into spherical triangles, and computes
+ their spherical area.
+ The function uses the spherical distance calculation given by
+ ``great_circle_distance()``.
+ """
+ h = _in_scalar(h)
+
+ return _cy.cell_area(h, unit=unit)
+
+
+def edge_length(e, unit='km'):
+ """
+ Compute the spherical length of a specific H3 edge.
+
+ Parameters
+ ----------
+ h : H3Cell
+ unit: str
+ Unit for length result (``'km'``, ``'m'``, or ``'rads'``)
+
+
+ Returns
+ -------
+ The length of the edge in the given units
+
+
+ Notes
+ -----
+ This function uses the spherical distance calculation given by
+ ``great_circle_distance()``.
+ """
+ e = _in_scalar(e)
+
+ return _cy.edge_length(e, unit=unit)
+
+
+def great_circle_distance(latlng1, latlng2, unit='km'):
+ """
+ Compute the spherical distance between two (lat, lng) points.
+ AKA: great circle distance or "haversine" distance.
+
+ todo: overload to allow two cell inputs?
+
+ Parameters
+ ----------
+ latlng1 : tuple
+ (lat, lng) tuple in degrees
+ latlng2 : tuple
+ (lat, lng) tuple in degrees
+ unit: str
+ Unit for distance result (``'km'``, ``'m'``, or ``'rads'``)
+
+ Returns
+ -------
+ The spherical distance between the points in the given units
+ """
+ lat1, lng1 = latlng1
+ lat2, lng2 = latlng2
+ return _cy.great_circle_distance(
+ lat1, lng1,
+ lat2, lng2,
+ unit = unit
+ )
+
+
+def cell_to_vertex(h, vertex_num):
+ """
+ Return a (specified) vertex of an H3 cell.
+
+ Parameters
+ ----------
+ h : H3Cell
+ vertex_num : int
+ Vertex number (0-5)
+
+ Returns
+ -------
+ The vertex
+ """
+ h = _in_scalar(h)
+ h = _cy.cell_to_vertex(h, vertex_num)
+ return _out_scalar(h)
+
+
+def cell_to_vertexes(h):
+ """
+ Return a list of vertexes of an H3 cell.
+ The list will be of length 5 for pentagons and 6 for hexagons.
+
+ Parameters
+ ----------
+ h : H3Cell
+
+ Returns
+ -------
+ A list of vertexes
+ """
+ h = _in_scalar(h)
+ mv = _cy.cell_to_vertexes(h)
+ return _out_collection(mv)
+
+
+def vertex_to_latlng(v):
+ """
+ Return latitude and longitude of a vertex.
+
+ Returns
+ -------
+ lat : float
+ Latitude
+ lng : float
+ Longitude
+ """
+ v = _in_scalar(v)
+ return _cy.vertex_to_latlng(v)
+
+
+def is_valid_vertex(v):
+ """
+ Validates an H3 vertex.
+
+ Returns
+ -------
+ bool
+ """
+ try:
+ v = _in_scalar(v)
+ return _cy.is_valid_vertex(v)
+ except (ValueError, TypeError):
+ return False
diff --git a/frogpilot/third_party/h3/api/basic_int/_convert.py b/frogpilot/third_party/h3/api/basic_int/_convert.py
new file mode 100644
index 00000000..51f47283
--- /dev/null
+++ b/frogpilot/third_party/h3/api/basic_int/_convert.py
@@ -0,0 +1,17 @@
+from ... import _cy
+
+
+def _in_scalar(x):
+ return x
+
+
+_out_scalar = _in_scalar
+
+
+def _in_collection(cells):
+ it = list(cells)
+
+ return _cy.iter_to_mv(it)
+
+
+_out_collection = list
diff --git a/frogpilot/third_party/h3/api/basic_str/__init__.py b/frogpilot/third_party/h3/api/basic_str/__init__.py
new file mode 100644
index 00000000..09f7deb8
--- /dev/null
+++ b/frogpilot/third_party/h3/api/basic_str/__init__.py
@@ -0,0 +1,1240 @@
+# This file is **symlinked** across the APIs to ensure they are exactly the same.
+from typing import Literal
+
+from ... import _cy
+from ..._h3shape import (
+ H3Shape,
+ LatLngPoly,
+ LatLngMultiPoly,
+ geo_to_h3shape,
+ h3shape_to_geo,
+)
+
+from ._convert import (
+ _in_scalar,
+ _out_scalar,
+ _in_collection,
+ _out_collection,
+)
+
+
+def versions():
+ """
+ Version numbers for the Python (wrapper) and C (wrapped) libraries.
+
+ Versions are output as strings of the form ``'X.Y.Z'``.
+ C and Python should match on ``X`` (major) and ``Y`` (minor),
+ but may differ on ``Z`` (patch).
+
+ Returns
+ -------
+ dict like ``{'c': 'X.Y.Z', 'python': 'A.B.C'}``
+ """
+ from ..._version import __version__
+
+ v = {
+ 'c': _cy.c_version(),
+ 'python': __version__,
+ }
+
+ return v
+
+
+def str_to_int(h):
+ """
+ Converts a hexadecimal string to an H3 64-bit integer index.
+
+ Parameters
+ ----------
+ h : str
+ Hexadecimal string like ``'89754e64993ffff'``
+
+ Returns
+ -------
+ int
+ Unsigned 64-bit integer
+ """
+ return _cy.str_to_int(h)
+
+
+def int_to_str(x):
+ """
+ Converts an H3 64-bit integer index to a hexadecimal string.
+
+ Parameters
+ ----------
+ x : int
+ Unsigned 64-bit integer
+
+ Returns
+ -------
+ str
+ Hexadecimal string like ``'89754e64993ffff'``
+ """
+ return _cy.int_to_str(x)
+
+
+def get_num_cells(res):
+ """
+ Return the total number of *cells* (hexagons and pentagons)
+ for the given resolution.
+
+ Returns
+ -------
+ int
+ """
+ return _cy.get_num_cells(res)
+
+
+def average_hexagon_area(res, unit='km^2'):
+ """
+ Return the average area of an H3 *hexagon*
+ for the given resolution.
+
+ This average *excludes* pentagons.
+
+ Parameters
+ ----------
+ res : int
+ H3 resolution
+ unit: str
+ Unit for area result (``'km^2'``, ``'m^2'``, or ``'rads^2'``)
+
+ Returns
+ -------
+ float
+ """
+ return _cy.average_hexagon_area(res, unit)
+
+
+def average_hexagon_edge_length(res, unit='km'):
+ """
+ Return the average *hexagon* edge length
+ for the given resolution.
+
+ This average *excludes* pentagons.
+
+ Parameters
+ ----------
+ res : int
+ H3 resolution
+ unit: str
+ Unit for length result (``'km'``, ``'m'``, or ``'rads'``)
+
+ Returns
+ -------
+ float
+ """
+ return _cy.average_hexagon_edge_length(res, unit)
+
+
+def is_valid_cell(h):
+ """
+ Validates an H3 cell (hexagon or pentagon).
+
+ Returns
+ -------
+ bool
+ """
+ try:
+ h = _in_scalar(h)
+ return _cy.is_valid_cell(h)
+ except (ValueError, TypeError):
+ return False
+
+
+def is_valid_directed_edge(edge):
+ """
+ Validates an H3 unidirectional edge.
+
+ Returns
+ -------
+ bool
+ """
+ try:
+ e = _in_scalar(edge)
+ return _cy.is_valid_directed_edge(e)
+ except (ValueError, TypeError):
+ return False
+
+
+def latlng_to_cell(lat, lng, res):
+ """
+ Return the cell containing the (lat, lng) point
+ for a given resolution.
+
+ Returns
+ -------
+ H3Cell
+
+ """
+ return _out_scalar(_cy.latlng_to_cell(lat, lng, res))
+
+
+def cell_to_latlng(h):
+ """
+ Return the center point of an H3 cell as a lat/lng pair.
+
+ Parameters
+ ----------
+ h : H3Cell
+
+ Returns
+ -------
+ lat : float
+ Latitude
+ lng : float
+ Longitude
+ """
+ h = _in_scalar(h)
+ return _cy.cell_to_latlng(h)
+
+
+def get_resolution(h):
+ """
+ Return the resolution of an H3 cell.
+
+ Parameters
+ ----------
+ h : H3Cell
+
+ Returns
+ -------
+ int
+ """
+ # todo: could also work for edges
+ h = _in_scalar(h)
+ return _cy.get_resolution(h)
+
+
+def cell_to_parent(h, res=None):
+ """
+ Get the parent of a cell.
+
+ Parameters
+ ----------
+ h : H3Cell
+ res : int or None, optional
+ The resolution for the parent
+ If ``None``, then ``res = resolution(h) - 1``
+
+ Returns
+ -------
+ H3Cell
+ """
+ h = _in_scalar(h)
+ p = _cy.cell_to_parent(h, res)
+ p = _out_scalar(p)
+
+ return p
+
+
+def grid_distance(h1, h2):
+ """
+ Compute the grid distance between two cells.
+
+ The grid distance is defined as the length of the shortest
+ path between the cells in the graph formed by connecting
+ adjacent cells.
+
+ This function will raise an exception if the
+ cells are too far apart to compute the distance.
+
+ Parameters
+ ----------
+ h1 : H3Cell
+ h2 : H3Cell
+
+ Returns
+ -------
+ int
+ """
+ h1 = _in_scalar(h1)
+ h2 = _in_scalar(h2)
+
+ d = _cy.grid_distance(h1, h2)
+
+ return d
+
+
+def cell_to_boundary(h):
+ """
+ Return tuple of lat/lng pairs describing the cell boundary.
+
+ Parameters
+ ----------
+ h : H3Cell
+
+ Returns
+ -------
+ tuple of (lat, lng) tuples
+ """
+ h = _in_scalar(h)
+ return _cy.cell_to_boundary(h)
+
+
+def grid_disk(h, k=1):
+ """
+ Return unordered collection of cells with grid distance ``<= k`` from ``h``.
+ That is, the "filled-in" disk.
+
+ Parameters
+ ----------
+ h : H3Cell
+ k : int
+ Size of disk.
+
+ Returns
+ -------
+ unordered collection of H3Cell
+
+ Notes
+ -----
+ There is currently no guaranteed order of the output cells.
+ """
+ h = _in_scalar(h)
+ mv = _cy.grid_disk(h, k)
+
+ return _out_collection(mv)
+
+
+def grid_ring(h, k=1):
+ """
+ Return unordered collection of cells with grid distance ``== k`` from ``h``.
+ That is, the "hollow" ring.
+
+ Parameters
+ ----------
+ h : H3Cell
+ k : int
+ Size of ring.
+
+ Returns
+ -------
+ unordered collection of H3Cell
+
+ Notes
+ -----
+ There is currently no guaranteed order of the output cells.
+ """
+ h = _in_scalar(h)
+ mv = _cy.grid_ring(h, k)
+
+ return _out_collection(mv)
+
+
+def cell_to_children_size(h, res=None):
+ """
+ Number of children at resolution ``res`` of given cell.
+
+ Parameters
+ ----------
+ h : H3Cell
+ res : int or None, optional
+ The resolution for the children.
+ If ``None``, then ``res = resolution(h) + 1``
+
+ Returns
+ -------
+ int
+ Count of children
+ """
+ h = _in_scalar(h)
+ return _cy.cell_to_children_size(h, res)
+
+
+def cell_to_children(h, res=None):
+ """
+ Children of a cell as an unordered collection.
+
+ Parameters
+ ----------
+ h : H3Cell
+ res : int or None, optional
+ The resolution for the children.
+ If ``None``, then ``res = resolution(h) + 1``
+
+ Returns
+ -------
+ unordered collection of H3Cell
+
+ Notes
+ -----
+ There is currently no guaranteed order of the output cells.
+ """
+ h = _in_scalar(h)
+ mv = _cy.cell_to_children(h, res)
+
+ return _out_collection(mv)
+
+
+def cell_to_child_pos(child, res_parent):
+ """
+ Child position index of given cell, with respect to its parent at ``res_parent``.
+
+ The reverse operation can be done with ``child_pos_to_cell``.
+
+ Parameters
+ ----------
+ child : H3Cell
+ res_parent : int
+
+ Returns
+ -------
+ int
+ Integer index of the child with respect to parent cell.
+ """
+ child = _in_scalar(child)
+ return _cy.cell_to_child_pos(child, res_parent)
+
+
+def child_pos_to_cell(parent, res_child, child_pos):
+ """
+ Get child H3 cell from a parent cell, child resolution, and child position index.
+
+ The reverse operation can be done with ``cell_to_child_pos``.
+
+ Parameters
+ ----------
+ parent : H3Cell
+ res_child : int
+ Child cell resolution
+ child_pos : int
+ Integer position of child cell, releative to parent.
+
+
+ Returns
+ -------
+ H3Cell
+ """
+ parent = _in_scalar(parent)
+ child = _cy.child_pos_to_cell(parent, res_child, child_pos)
+ child = _out_scalar(child)
+
+ return child
+
+
+# todo: nogil for expensive C operation?
+def compact_cells(cells):
+ """
+ Compact a collection of H3 cells by combining
+ smaller cells into larger cells, if all child cells
+ are present. Input cells must all share the same resolution.
+
+ Parameters
+ ----------
+ cells : iterable of H3 Cells
+
+ Returns
+ -------
+ unordered collection of H3Cell
+
+ Notes
+ -----
+ There is currently no guaranteed order of the output cells.
+ """
+ hu = _in_collection(cells)
+ hc = _cy.compact_cells(hu)
+
+ return _out_collection(hc)
+
+
+def uncompact_cells(cells, res):
+ """
+ Reverse the ``compact_cells`` operation.
+
+ Return a collection of H3 cells, all of resolution ``res``.
+
+ Parameters
+ ----------
+ cells : iterable of H3Cell
+ res : int
+ Resolution of desired output cells.
+
+ Returns
+ -------
+ unordered collection of H3Cell
+
+ Notes
+ -----
+ There is currently no guaranteed order of the output cells.
+ """
+ # TODO: add test to make sure an error is returned when input contains cell
+ # smaller than output res.
+
+ hc = _in_collection(cells)
+ hu = _cy.uncompact_cells(hc, res)
+
+ return _out_collection(hu)
+
+
+def polygon_to_cells(h3shape, res):
+ """
+ Alias for ``h3shape_to_cells``.
+ """
+ return h3shape_to_cells(h3shape, res)
+
+
+def h3shape_to_cells(h3shape, res):
+ """
+ Return the collection of H3 cells at a given resolution whose center points
+ are contained within an ``LatLngPoly`` or ``LatLngMultiPoly``.
+
+ Parameters
+ ----------
+ h3shape : ``H3Shape``
+ res : int
+ Resolution of the output cells
+
+ Returns
+ -------
+ list of H3Cell
+
+ Examples
+ --------
+
+ >>> poly = LatLngPoly(
+ ... [(37.68, -122.54), (37.68, -122.34), (37.82, -122.34),
+ ... (37.82, -122.54)],
+ ... )
+ >>> h3.h3shape_to_cells(poly, 6)
+ ['862830807ffffff',
+ '862830827ffffff',
+ '86283082fffffff',
+ '862830877ffffff',
+ '862830947ffffff',
+ '862830957ffffff',
+ '86283095fffffff']
+
+ Notes
+ -----
+ There is currently no guaranteed order of the output cells.
+ """
+
+ # todo: not sure if i want this dispatch logic here. maybe in the objects?
+ if isinstance(h3shape, LatLngPoly):
+ poly = h3shape
+ mv = _cy.polygon_to_cells(poly.outer, res=res, holes=poly.holes)
+ elif isinstance(h3shape, LatLngMultiPoly):
+ mpoly = h3shape
+ mv = _cy.polygons_to_cells(mpoly.polys, res=res)
+ elif isinstance(h3shape, H3Shape):
+ raise ValueError('Unrecognized H3Shape: ' + str(h3shape))
+ else:
+ raise ValueError('Unrecognized type: ' + str(type(h3shape)))
+
+ return _out_collection(mv)
+
+
+def polygon_to_cells_experimental(
+ h3shape: H3Shape,
+ res: int,
+ contain: Literal['center', 'full', 'overlap', 'bbox_overlap'] = 'center',
+):
+ """
+ Alias for ``h3shape_to_cells_experimental``.
+ """
+ return h3shape_to_cells_experimental(h3shape, res, contain)
+
+
+def h3shape_to_cells_experimental(
+ h3shape: H3Shape,
+ res: int,
+ contain: Literal['center', 'full', 'overlap', 'bbox_overlap'] = 'center',
+):
+ """
+ Experimental function similar to ``h3shape_to_cells``, but with support for
+ multiple cell containment modes.
+
+ Using ``contain='center'`` should give identical behavior as
+ ``h3shape_to_cells``.
+
+ Note that this function is **experimental** and has no API stability gaurantees
+ across versions, so it may change in the future.
+
+
+ Parameters
+ ----------
+ h3shape : ``H3Shape``
+ res : int
+ Resolution of the output cells
+ contain : {'center', 'full', 'overlap', 'bbox_overlap'}, optional
+ Specifies the containment condition.
+ - 'center': Cell center is contained in shape
+ - 'full': Cell is fully contained in shape
+ - 'overlap': Cell is partially contained in shape
+ - 'bbox_overlap': Cell bounding box is partially contained in shape
+
+ Default is 'center'.
+
+ Returns
+ -------
+ list of H3Cell
+
+ Examples
+ --------
+
+ >>> poly = LatLngPoly(
+ ... [(37.68, -122.54), (37.68, -122.34), (37.82, -122.34),
+ ... (37.82, -122.54)],
+ ... )
+ >>> h3.h3shape_to_cells_experimental(poly, 6, 'center')
+ ['862830807ffffff',
+ '862830827ffffff',
+ '86283082fffffff',
+ '862830877ffffff',
+ '862830947ffffff',
+ '862830957ffffff',
+ '86283095fffffff']
+
+ Notes
+ -----
+ There is currently no guaranteed order of the output cells.
+ """
+
+ contain_modes = {
+ 'center': 0,
+ 'full': 1,
+ 'overlap': 2,
+ 'bbox_overlap': 3,
+ }
+
+ flag = contain_modes[contain]
+
+ # todo: not sure if i want this dispatch logic here. maybe in the objects?
+ if isinstance(h3shape, LatLngPoly):
+ poly = h3shape
+ mv = _cy.polygon_to_cells_experimental(
+ poly.outer,
+ res = res,
+ holes = poly.holes,
+ flag = flag,
+ )
+ elif isinstance(h3shape, LatLngMultiPoly):
+ mpoly = h3shape
+ mv = _cy.polygons_to_cells_experimental(mpoly.polys, res=res, flag=flag)
+ elif isinstance(h3shape, H3Shape):
+ raise ValueError('Unrecognized H3Shape: ' + str(h3shape))
+ else:
+ raise ValueError('Unrecognized type: ' + str(type(h3shape)))
+
+ return _out_collection(mv)
+
+
+def cells_to_h3shape(cells, *, tight=True):
+ """
+ Return an ``H3Shape`` describing the area covered by a collection of H3 cells.
+ Will return ``LatLngPoly`` or ``LatLngMultiPoly``.
+
+ Parameters
+ ----------
+ cells : iterable of H3 cells
+ tight : bool
+ If True, return ``LatLngPoly`` if possible.
+ If False, always return ``LatLngMultiPoly``.
+
+ Returns
+ -------
+ LatLngPoly | LatLngMultiPoly
+
+ Examples
+ --------
+
+ >>> cells = ['8428309ffffffff', '842830dffffffff']
+ >>> h3.cells_to_h3shape(cells, tight=True)
+
+ >>> h3.cells_to_h3shape(cells, tight=False)
+
+ """
+ cells = _in_collection(cells)
+ mpoly = _cy.cells_to_multi_polygon(cells)
+
+ polys = [LatLngPoly(*poly) for poly in mpoly]
+ out = LatLngMultiPoly(*polys)
+
+ if tight and len(out) == 1:
+ out = out[0]
+
+ return out
+
+
+def geo_to_cells(geo, res):
+ """Convert from ``__geo_interface__`` to cells.
+
+ Parameters
+ ----------
+ geo : an object implementing ``__geo_interface__`` or a dictionary in that format.
+ Both ``LatLngPoly`` and ``LatLngMultiPoly`` implement the interface.
+ res : int
+ Resolution of desired output cells.
+
+ Notes
+ -----
+ There is currently no guaranteed order of the output cells.
+ """
+ h3shape = geo_to_h3shape(geo)
+ return h3shape_to_cells(h3shape, res)
+
+
+def cells_to_geo(cells, tight=True):
+ """
+ Convert from cells to a ``__geo_interface__`` dict.
+
+ Parameters
+ ----------
+ cells : iterable of H3 Cells
+ tight : bool
+ When ``True``, returns an ``LatLngPoly`` when possible.
+ When ``False``, always returns an ``LatLngMultiPoly``.
+
+ Returns
+ -------
+ dict
+ in `__geo_interface__` format
+ """
+ h3shape = cells_to_h3shape(cells, tight=tight)
+ return h3shape_to_geo(h3shape)
+
+
+def is_pentagon(h):
+ """
+ Identify if an H3 cell is a pentagon.
+
+ Parameters
+ ----------
+ h : H3Index
+
+ Returns
+ -------
+ bool
+ ``True`` if input is a valid H3 cell which is a pentagon.
+
+ Notes
+ -----
+ A pentagon should *also* pass ``is_valid_cell()``.
+ Will return ``False`` for valid H3Edge.
+ """
+ return _cy.is_pentagon(_in_scalar(h))
+
+
+def get_base_cell_number(h):
+ """
+ Return the base cell *number* (``0`` to ``121``) of the given cell.
+
+ The base cell *number* and the H3Index are two different representations
+ of the same cell: the parent cell of resolution ``0``.
+
+ The base cell *number* is encoded within the corresponding
+ H3Index.
+
+ todo: could work with edges
+
+ Parameters
+ ----------
+ h : H3Cell
+
+ Returns
+ -------
+ int
+ """
+ return _cy.get_base_cell_number(_in_scalar(h))
+
+
+def are_neighbor_cells(h1, h2):
+ """
+ Returns ``True`` if ``h1`` and ``h2`` are neighboring cells.
+
+ Parameters
+ ----------
+ h1 : H3Cell
+ h2 : H3Cell
+
+ Returns
+ -------
+ bool
+ """
+ h1 = _in_scalar(h1)
+ h2 = _in_scalar(h2)
+
+ return _cy.are_neighbor_cells(h1, h2)
+
+
+def cells_to_directed_edge(origin, destination):
+ """
+ Create an H3 Index denoting a unidirectional edge.
+
+ The edge is constructed from neighboring cells ``origin`` and
+ ``destination``.
+
+ Parameters
+ ----------
+ origin : H3Cell
+ destination : H3Cell
+
+ Raises
+ ------
+ ValueError
+ When cells are not adjacent.
+
+ Returns
+ -------
+ H3Edge
+ """
+ o = _in_scalar(origin)
+ d = _in_scalar(destination)
+ e = _cy.cells_to_directed_edge(o, d)
+ e = _out_scalar(e)
+
+ return e
+
+
+def get_directed_edge_origin(e):
+ """
+ Origin cell from an H3 directed edge.
+
+ Parameters
+ ----------
+ e : H3Edge
+
+ Returns
+ -------
+ H3Cell
+ """
+ e = _in_scalar(e)
+ o = _cy.get_directed_edge_origin(e)
+ o = _out_scalar(o)
+
+ return o
+
+
+def get_directed_edge_destination(e):
+ """
+ Destination cell from an H3 directed edge.
+
+ Parameters
+ ----------
+ e : H3Edge
+
+ Returns
+ -------
+ H3Cell
+ """
+ e = _in_scalar(e)
+ d = _cy.get_directed_edge_destination(e)
+ d = _out_scalar(d)
+
+ return d
+
+
+def directed_edge_to_cells(e):
+ """
+ Return (origin, destination) tuple from H3 directed edge
+
+ Parameters
+ ----------
+ e : H3Edge
+
+ Returns
+ -------
+ H3Cell
+ Origin cell of edge
+ H3Cell
+ Destination cell of edge
+ """
+ e = _in_scalar(e)
+ o, d = _cy.directed_edge_to_cells(e)
+ o, d = _out_scalar(o), _out_scalar(d)
+
+ return o, d
+
+
+def origin_to_directed_edges(origin):
+ """
+ Return all directed edges starting from ``origin`` cell.
+
+ Parameters
+ ----------
+ origin : H3Cell
+
+ Returns
+ -------
+ unordered collection of H3Edge
+ """
+ mv = _cy.origin_to_directed_edges(_in_scalar(origin))
+
+ return _out_collection(mv)
+
+
+def directed_edge_to_boundary(edge):
+ """
+ Returns points representing the edge (line of points
+ describing the boundary between two cells).
+
+ Parameters
+ ----------
+ edge : H3Edge
+
+ Returns
+ -------
+ tuple of (lat, lng) tuples
+ """
+ return _cy.directed_edge_to_boundary(_in_scalar(edge))
+
+
+def grid_path_cells(start, end):
+ """
+ Returns the ordered collection of cells denoting a
+ minimum-length non-unique path between cells.
+
+ Parameters
+ ----------
+ start : H3Cell
+ end : H3Cell
+
+ Returns
+ -------
+ ordered collection of H3Cell
+ Starting with ``start``, and ending with ``end``.
+ """
+ mv = _cy.grid_path_cells(_in_scalar(start), _in_scalar(end))
+
+ return _out_collection(mv)
+
+
+def is_res_class_III(h):
+ """
+ Determine if cell has orientation "Class II" or "Class III".
+
+ The orientation of pentagons/hexagons on the icosahedron can be one
+ of two types: "Class II" or "Class III".
+
+ All cells within a resolution have the same type, and the type
+ alternates between resolutions.
+
+ "Class II" cells have resolutions: 0,2,4,6,8,10,12,14
+ "Class III" cells have resolutions: 1,3,5,7,9,11,13,15
+
+ Parameters
+ ----------
+ h : H3Cell
+
+ Returns
+ -------
+ bool
+ ``True`` if ``h`` is "Class III".
+ ``False`` if ``h`` is "Class II".
+
+ References
+ ----------
+ 1. https://uber.github.io/h3/#/documentation/core-library/coordinate-systems
+ """
+ return _cy.is_res_class_iii(_in_scalar(h))
+
+
+def get_pentagons(res):
+ """
+ Return all pentagons at a given resolution.
+
+ Parameters
+ ----------
+ res : int
+ Resolution of the pentagons
+
+ Returns
+ -------
+ unordered collection of H3Cell
+ """
+ mv = _cy.get_pentagons(res)
+
+ return _out_collection(mv)
+
+
+def get_res0_cells():
+ """
+ Return all cells at resolution 0.
+
+ Parameters
+ ----------
+ None
+
+ Returns
+ -------
+ unordered collection of H3Cell
+
+ Notes
+ -----
+ There is currently no guaranteed order of the output cells.
+ """
+ mv = _cy.get_res0_cells()
+
+ return _out_collection(mv)
+
+
+def cell_to_center_child(h, res=None):
+ """
+ Get the center child of a cell at some finer resolution.
+
+ Parameters
+ ----------
+ h : H3Cell
+ res : int or None, optional
+ The resolution for the child cell
+ If ``None``, then ``res = resolution(h) + 1``
+
+ Returns
+ -------
+ H3Cell
+ """
+ h = _in_scalar(h)
+ p = _cy.cell_to_center_child(h, res)
+ p = _out_scalar(p)
+
+ return p
+
+
+def get_icosahedron_faces(h):
+ """
+ Return icosahedron faces intersecting a given H3 cell.
+
+ There are twenty possible faces, ranging from 0--19.
+
+ Note: Every interface returns a Python ``set`` of ``int``.
+
+ Parameters
+ ----------
+ h : H3Cell
+
+ Returns
+ -------
+ Python ``set`` of ``int``
+ """
+ h = _in_scalar(h)
+ faces = _cy.get_icosahedron_faces(h)
+
+ return faces
+
+
+def cell_to_local_ij(origin, h):
+ """
+ Return local (i,j) coordinates of cell ``h`` in relation to ``origin`` cell
+
+
+ Parameters
+ ----------
+ origin : H3Cell
+ Origin/central cell for defining i,j coordinates.
+ h: H3Cell
+ Destination cell whose i,j coordinates we'd like, based off
+ of the origin cell.
+
+
+ Returns
+ -------
+ Tuple (i, j) of integer local coordinates of cell ``h``
+
+
+ Notes
+ -----
+
+ The ``origin`` cell does not define (0, 0) for the IJ coordinate space.
+ (0, 0) refers to the center of the base cell containing origin at the
+ resolution of `origin`.
+ Subtracting the IJ coordinates of ``origin`` from every cell would get
+ you the property of (0, 0) being the ``origin``.
+
+ This is done so we don't need to keep recomputing the coordinates of
+ ``origin`` if not needed.
+ """
+ origin = _in_scalar(origin)
+ h = _in_scalar(h)
+
+ i, j = _cy.cell_to_local_ij(origin, h)
+
+ return i, j
+
+
+def local_ij_to_cell(origin, i, j):
+ """
+ Return cell at local (i,j) position relative to the ``origin`` cell.
+
+ Parameters
+ ----------
+ origin : H3Cell
+ Origin/central cell for defining i,j coordinates.
+ i, j: int
+ Integer coordinates with respect to ``origin`` cell.
+
+
+ Returns
+ -------
+ H3Cell at local (i,j) position relative to the ``origin`` cell
+
+
+ Notes
+ -----
+
+ The ``origin`` cell does not define (0, 0) for the IJ coordinate space.
+ (0, 0) refers to the center of the base cell containing origin at the
+ resolution of ``origin``.
+ Subtracting the IJ coordinates of ``origin`` from every cell would get
+ you the property of (0, 0) being the ``origin``.
+
+ This is done so we don't need to keep recomputing the coordinates of
+ ``origin`` if not needed.
+ """
+ origin = _in_scalar(origin)
+
+ h = _cy.local_ij_to_cell(origin, i, j)
+ h = _out_scalar(h)
+
+ return h
+
+
+def cell_area(h, unit='km^2'):
+ """
+ Compute the spherical surface area of a specific H3 cell.
+
+ Parameters
+ ----------
+ h : H3Cell
+ unit: str
+ Unit for area result (``'km^2'``, ``'m^2'``, or ``'rads^2'``)
+
+
+ Returns
+ -------
+ The area of the H3 cell in the given units
+
+
+ Notes
+ -----
+ This function breaks the cell into spherical triangles, and computes
+ their spherical area.
+ The function uses the spherical distance calculation given by
+ ``great_circle_distance()``.
+ """
+ h = _in_scalar(h)
+
+ return _cy.cell_area(h, unit=unit)
+
+
+def edge_length(e, unit='km'):
+ """
+ Compute the spherical length of a specific H3 edge.
+
+ Parameters
+ ----------
+ h : H3Cell
+ unit: str
+ Unit for length result (``'km'``, ``'m'``, or ``'rads'``)
+
+
+ Returns
+ -------
+ The length of the edge in the given units
+
+
+ Notes
+ -----
+ This function uses the spherical distance calculation given by
+ ``great_circle_distance()``.
+ """
+ e = _in_scalar(e)
+
+ return _cy.edge_length(e, unit=unit)
+
+
+def great_circle_distance(latlng1, latlng2, unit='km'):
+ """
+ Compute the spherical distance between two (lat, lng) points.
+ AKA: great circle distance or "haversine" distance.
+
+ todo: overload to allow two cell inputs?
+
+ Parameters
+ ----------
+ latlng1 : tuple
+ (lat, lng) tuple in degrees
+ latlng2 : tuple
+ (lat, lng) tuple in degrees
+ unit: str
+ Unit for distance result (``'km'``, ``'m'``, or ``'rads'``)
+
+ Returns
+ -------
+ The spherical distance between the points in the given units
+ """
+ lat1, lng1 = latlng1
+ lat2, lng2 = latlng2
+ return _cy.great_circle_distance(
+ lat1, lng1,
+ lat2, lng2,
+ unit = unit
+ )
+
+
+def cell_to_vertex(h, vertex_num):
+ """
+ Return a (specified) vertex of an H3 cell.
+
+ Parameters
+ ----------
+ h : H3Cell
+ vertex_num : int
+ Vertex number (0-5)
+
+ Returns
+ -------
+ The vertex
+ """
+ h = _in_scalar(h)
+ h = _cy.cell_to_vertex(h, vertex_num)
+ return _out_scalar(h)
+
+
+def cell_to_vertexes(h):
+ """
+ Return a list of vertexes of an H3 cell.
+ The list will be of length 5 for pentagons and 6 for hexagons.
+
+ Parameters
+ ----------
+ h : H3Cell
+
+ Returns
+ -------
+ A list of vertexes
+ """
+ h = _in_scalar(h)
+ mv = _cy.cell_to_vertexes(h)
+ return _out_collection(mv)
+
+
+def vertex_to_latlng(v):
+ """
+ Return latitude and longitude of a vertex.
+
+ Returns
+ -------
+ lat : float
+ Latitude
+ lng : float
+ Longitude
+ """
+ v = _in_scalar(v)
+ return _cy.vertex_to_latlng(v)
+
+
+def is_valid_vertex(v):
+ """
+ Validates an H3 vertex.
+
+ Returns
+ -------
+ bool
+ """
+ try:
+ v = _in_scalar(v)
+ return _cy.is_valid_vertex(v)
+ except (ValueError, TypeError):
+ return False
diff --git a/frogpilot/third_party/h3/api/basic_str/_convert.py b/frogpilot/third_party/h3/api/basic_str/_convert.py
new file mode 100644
index 00000000..a6c0ba0d
--- /dev/null
+++ b/frogpilot/third_party/h3/api/basic_str/_convert.py
@@ -0,0 +1,14 @@
+from ... import _cy
+
+_in_scalar = _cy.str_to_int
+_out_scalar = _cy.int_to_str
+
+
+def _in_collection(cells):
+ it = [_cy.str_to_int(h) for h in cells]
+
+ return _cy.iter_to_mv(it)
+
+
+def _out_collection(mv):
+ return list(_cy.int_to_str(h) for h in mv)
diff --git a/frogpilot/third_party/h3/api/memview_int/__init__.py b/frogpilot/third_party/h3/api/memview_int/__init__.py
new file mode 100644
index 00000000..09f7deb8
--- /dev/null
+++ b/frogpilot/third_party/h3/api/memview_int/__init__.py
@@ -0,0 +1,1240 @@
+# This file is **symlinked** across the APIs to ensure they are exactly the same.
+from typing import Literal
+
+from ... import _cy
+from ..._h3shape import (
+ H3Shape,
+ LatLngPoly,
+ LatLngMultiPoly,
+ geo_to_h3shape,
+ h3shape_to_geo,
+)
+
+from ._convert import (
+ _in_scalar,
+ _out_scalar,
+ _in_collection,
+ _out_collection,
+)
+
+
+def versions():
+ """
+ Version numbers for the Python (wrapper) and C (wrapped) libraries.
+
+ Versions are output as strings of the form ``'X.Y.Z'``.
+ C and Python should match on ``X`` (major) and ``Y`` (minor),
+ but may differ on ``Z`` (patch).
+
+ Returns
+ -------
+ dict like ``{'c': 'X.Y.Z', 'python': 'A.B.C'}``
+ """
+ from ..._version import __version__
+
+ v = {
+ 'c': _cy.c_version(),
+ 'python': __version__,
+ }
+
+ return v
+
+
+def str_to_int(h):
+ """
+ Converts a hexadecimal string to an H3 64-bit integer index.
+
+ Parameters
+ ----------
+ h : str
+ Hexadecimal string like ``'89754e64993ffff'``
+
+ Returns
+ -------
+ int
+ Unsigned 64-bit integer
+ """
+ return _cy.str_to_int(h)
+
+
+def int_to_str(x):
+ """
+ Converts an H3 64-bit integer index to a hexadecimal string.
+
+ Parameters
+ ----------
+ x : int
+ Unsigned 64-bit integer
+
+ Returns
+ -------
+ str
+ Hexadecimal string like ``'89754e64993ffff'``
+ """
+ return _cy.int_to_str(x)
+
+
+def get_num_cells(res):
+ """
+ Return the total number of *cells* (hexagons and pentagons)
+ for the given resolution.
+
+ Returns
+ -------
+ int
+ """
+ return _cy.get_num_cells(res)
+
+
+def average_hexagon_area(res, unit='km^2'):
+ """
+ Return the average area of an H3 *hexagon*
+ for the given resolution.
+
+ This average *excludes* pentagons.
+
+ Parameters
+ ----------
+ res : int
+ H3 resolution
+ unit: str
+ Unit for area result (``'km^2'``, ``'m^2'``, or ``'rads^2'``)
+
+ Returns
+ -------
+ float
+ """
+ return _cy.average_hexagon_area(res, unit)
+
+
+def average_hexagon_edge_length(res, unit='km'):
+ """
+ Return the average *hexagon* edge length
+ for the given resolution.
+
+ This average *excludes* pentagons.
+
+ Parameters
+ ----------
+ res : int
+ H3 resolution
+ unit: str
+ Unit for length result (``'km'``, ``'m'``, or ``'rads'``)
+
+ Returns
+ -------
+ float
+ """
+ return _cy.average_hexagon_edge_length(res, unit)
+
+
+def is_valid_cell(h):
+ """
+ Validates an H3 cell (hexagon or pentagon).
+
+ Returns
+ -------
+ bool
+ """
+ try:
+ h = _in_scalar(h)
+ return _cy.is_valid_cell(h)
+ except (ValueError, TypeError):
+ return False
+
+
+def is_valid_directed_edge(edge):
+ """
+ Validates an H3 unidirectional edge.
+
+ Returns
+ -------
+ bool
+ """
+ try:
+ e = _in_scalar(edge)
+ return _cy.is_valid_directed_edge(e)
+ except (ValueError, TypeError):
+ return False
+
+
+def latlng_to_cell(lat, lng, res):
+ """
+ Return the cell containing the (lat, lng) point
+ for a given resolution.
+
+ Returns
+ -------
+ H3Cell
+
+ """
+ return _out_scalar(_cy.latlng_to_cell(lat, lng, res))
+
+
+def cell_to_latlng(h):
+ """
+ Return the center point of an H3 cell as a lat/lng pair.
+
+ Parameters
+ ----------
+ h : H3Cell
+
+ Returns
+ -------
+ lat : float
+ Latitude
+ lng : float
+ Longitude
+ """
+ h = _in_scalar(h)
+ return _cy.cell_to_latlng(h)
+
+
+def get_resolution(h):
+ """
+ Return the resolution of an H3 cell.
+
+ Parameters
+ ----------
+ h : H3Cell
+
+ Returns
+ -------
+ int
+ """
+ # todo: could also work for edges
+ h = _in_scalar(h)
+ return _cy.get_resolution(h)
+
+
+def cell_to_parent(h, res=None):
+ """
+ Get the parent of a cell.
+
+ Parameters
+ ----------
+ h : H3Cell
+ res : int or None, optional
+ The resolution for the parent
+ If ``None``, then ``res = resolution(h) - 1``
+
+ Returns
+ -------
+ H3Cell
+ """
+ h = _in_scalar(h)
+ p = _cy.cell_to_parent(h, res)
+ p = _out_scalar(p)
+
+ return p
+
+
+def grid_distance(h1, h2):
+ """
+ Compute the grid distance between two cells.
+
+ The grid distance is defined as the length of the shortest
+ path between the cells in the graph formed by connecting
+ adjacent cells.
+
+ This function will raise an exception if the
+ cells are too far apart to compute the distance.
+
+ Parameters
+ ----------
+ h1 : H3Cell
+ h2 : H3Cell
+
+ Returns
+ -------
+ int
+ """
+ h1 = _in_scalar(h1)
+ h2 = _in_scalar(h2)
+
+ d = _cy.grid_distance(h1, h2)
+
+ return d
+
+
+def cell_to_boundary(h):
+ """
+ Return tuple of lat/lng pairs describing the cell boundary.
+
+ Parameters
+ ----------
+ h : H3Cell
+
+ Returns
+ -------
+ tuple of (lat, lng) tuples
+ """
+ h = _in_scalar(h)
+ return _cy.cell_to_boundary(h)
+
+
+def grid_disk(h, k=1):
+ """
+ Return unordered collection of cells with grid distance ``<= k`` from ``h``.
+ That is, the "filled-in" disk.
+
+ Parameters
+ ----------
+ h : H3Cell
+ k : int
+ Size of disk.
+
+ Returns
+ -------
+ unordered collection of H3Cell
+
+ Notes
+ -----
+ There is currently no guaranteed order of the output cells.
+ """
+ h = _in_scalar(h)
+ mv = _cy.grid_disk(h, k)
+
+ return _out_collection(mv)
+
+
+def grid_ring(h, k=1):
+ """
+ Return unordered collection of cells with grid distance ``== k`` from ``h``.
+ That is, the "hollow" ring.
+
+ Parameters
+ ----------
+ h : H3Cell
+ k : int
+ Size of ring.
+
+ Returns
+ -------
+ unordered collection of H3Cell
+
+ Notes
+ -----
+ There is currently no guaranteed order of the output cells.
+ """
+ h = _in_scalar(h)
+ mv = _cy.grid_ring(h, k)
+
+ return _out_collection(mv)
+
+
+def cell_to_children_size(h, res=None):
+ """
+ Number of children at resolution ``res`` of given cell.
+
+ Parameters
+ ----------
+ h : H3Cell
+ res : int or None, optional
+ The resolution for the children.
+ If ``None``, then ``res = resolution(h) + 1``
+
+ Returns
+ -------
+ int
+ Count of children
+ """
+ h = _in_scalar(h)
+ return _cy.cell_to_children_size(h, res)
+
+
+def cell_to_children(h, res=None):
+ """
+ Children of a cell as an unordered collection.
+
+ Parameters
+ ----------
+ h : H3Cell
+ res : int or None, optional
+ The resolution for the children.
+ If ``None``, then ``res = resolution(h) + 1``
+
+ Returns
+ -------
+ unordered collection of H3Cell
+
+ Notes
+ -----
+ There is currently no guaranteed order of the output cells.
+ """
+ h = _in_scalar(h)
+ mv = _cy.cell_to_children(h, res)
+
+ return _out_collection(mv)
+
+
+def cell_to_child_pos(child, res_parent):
+ """
+ Child position index of given cell, with respect to its parent at ``res_parent``.
+
+ The reverse operation can be done with ``child_pos_to_cell``.
+
+ Parameters
+ ----------
+ child : H3Cell
+ res_parent : int
+
+ Returns
+ -------
+ int
+ Integer index of the child with respect to parent cell.
+ """
+ child = _in_scalar(child)
+ return _cy.cell_to_child_pos(child, res_parent)
+
+
+def child_pos_to_cell(parent, res_child, child_pos):
+ """
+ Get child H3 cell from a parent cell, child resolution, and child position index.
+
+ The reverse operation can be done with ``cell_to_child_pos``.
+
+ Parameters
+ ----------
+ parent : H3Cell
+ res_child : int
+ Child cell resolution
+ child_pos : int
+ Integer position of child cell, releative to parent.
+
+
+ Returns
+ -------
+ H3Cell
+ """
+ parent = _in_scalar(parent)
+ child = _cy.child_pos_to_cell(parent, res_child, child_pos)
+ child = _out_scalar(child)
+
+ return child
+
+
+# todo: nogil for expensive C operation?
+def compact_cells(cells):
+ """
+ Compact a collection of H3 cells by combining
+ smaller cells into larger cells, if all child cells
+ are present. Input cells must all share the same resolution.
+
+ Parameters
+ ----------
+ cells : iterable of H3 Cells
+
+ Returns
+ -------
+ unordered collection of H3Cell
+
+ Notes
+ -----
+ There is currently no guaranteed order of the output cells.
+ """
+ hu = _in_collection(cells)
+ hc = _cy.compact_cells(hu)
+
+ return _out_collection(hc)
+
+
+def uncompact_cells(cells, res):
+ """
+ Reverse the ``compact_cells`` operation.
+
+ Return a collection of H3 cells, all of resolution ``res``.
+
+ Parameters
+ ----------
+ cells : iterable of H3Cell
+ res : int
+ Resolution of desired output cells.
+
+ Returns
+ -------
+ unordered collection of H3Cell
+
+ Notes
+ -----
+ There is currently no guaranteed order of the output cells.
+ """
+ # TODO: add test to make sure an error is returned when input contains cell
+ # smaller than output res.
+
+ hc = _in_collection(cells)
+ hu = _cy.uncompact_cells(hc, res)
+
+ return _out_collection(hu)
+
+
+def polygon_to_cells(h3shape, res):
+ """
+ Alias for ``h3shape_to_cells``.
+ """
+ return h3shape_to_cells(h3shape, res)
+
+
+def h3shape_to_cells(h3shape, res):
+ """
+ Return the collection of H3 cells at a given resolution whose center points
+ are contained within an ``LatLngPoly`` or ``LatLngMultiPoly``.
+
+ Parameters
+ ----------
+ h3shape : ``H3Shape``
+ res : int
+ Resolution of the output cells
+
+ Returns
+ -------
+ list of H3Cell
+
+ Examples
+ --------
+
+ >>> poly = LatLngPoly(
+ ... [(37.68, -122.54), (37.68, -122.34), (37.82, -122.34),
+ ... (37.82, -122.54)],
+ ... )
+ >>> h3.h3shape_to_cells(poly, 6)
+ ['862830807ffffff',
+ '862830827ffffff',
+ '86283082fffffff',
+ '862830877ffffff',
+ '862830947ffffff',
+ '862830957ffffff',
+ '86283095fffffff']
+
+ Notes
+ -----
+ There is currently no guaranteed order of the output cells.
+ """
+
+ # todo: not sure if i want this dispatch logic here. maybe in the objects?
+ if isinstance(h3shape, LatLngPoly):
+ poly = h3shape
+ mv = _cy.polygon_to_cells(poly.outer, res=res, holes=poly.holes)
+ elif isinstance(h3shape, LatLngMultiPoly):
+ mpoly = h3shape
+ mv = _cy.polygons_to_cells(mpoly.polys, res=res)
+ elif isinstance(h3shape, H3Shape):
+ raise ValueError('Unrecognized H3Shape: ' + str(h3shape))
+ else:
+ raise ValueError('Unrecognized type: ' + str(type(h3shape)))
+
+ return _out_collection(mv)
+
+
+def polygon_to_cells_experimental(
+ h3shape: H3Shape,
+ res: int,
+ contain: Literal['center', 'full', 'overlap', 'bbox_overlap'] = 'center',
+):
+ """
+ Alias for ``h3shape_to_cells_experimental``.
+ """
+ return h3shape_to_cells_experimental(h3shape, res, contain)
+
+
+def h3shape_to_cells_experimental(
+ h3shape: H3Shape,
+ res: int,
+ contain: Literal['center', 'full', 'overlap', 'bbox_overlap'] = 'center',
+):
+ """
+ Experimental function similar to ``h3shape_to_cells``, but with support for
+ multiple cell containment modes.
+
+ Using ``contain='center'`` should give identical behavior as
+ ``h3shape_to_cells``.
+
+ Note that this function is **experimental** and has no API stability gaurantees
+ across versions, so it may change in the future.
+
+
+ Parameters
+ ----------
+ h3shape : ``H3Shape``
+ res : int
+ Resolution of the output cells
+ contain : {'center', 'full', 'overlap', 'bbox_overlap'}, optional
+ Specifies the containment condition.
+ - 'center': Cell center is contained in shape
+ - 'full': Cell is fully contained in shape
+ - 'overlap': Cell is partially contained in shape
+ - 'bbox_overlap': Cell bounding box is partially contained in shape
+
+ Default is 'center'.
+
+ Returns
+ -------
+ list of H3Cell
+
+ Examples
+ --------
+
+ >>> poly = LatLngPoly(
+ ... [(37.68, -122.54), (37.68, -122.34), (37.82, -122.34),
+ ... (37.82, -122.54)],
+ ... )
+ >>> h3.h3shape_to_cells_experimental(poly, 6, 'center')
+ ['862830807ffffff',
+ '862830827ffffff',
+ '86283082fffffff',
+ '862830877ffffff',
+ '862830947ffffff',
+ '862830957ffffff',
+ '86283095fffffff']
+
+ Notes
+ -----
+ There is currently no guaranteed order of the output cells.
+ """
+
+ contain_modes = {
+ 'center': 0,
+ 'full': 1,
+ 'overlap': 2,
+ 'bbox_overlap': 3,
+ }
+
+ flag = contain_modes[contain]
+
+ # todo: not sure if i want this dispatch logic here. maybe in the objects?
+ if isinstance(h3shape, LatLngPoly):
+ poly = h3shape
+ mv = _cy.polygon_to_cells_experimental(
+ poly.outer,
+ res = res,
+ holes = poly.holes,
+ flag = flag,
+ )
+ elif isinstance(h3shape, LatLngMultiPoly):
+ mpoly = h3shape
+ mv = _cy.polygons_to_cells_experimental(mpoly.polys, res=res, flag=flag)
+ elif isinstance(h3shape, H3Shape):
+ raise ValueError('Unrecognized H3Shape: ' + str(h3shape))
+ else:
+ raise ValueError('Unrecognized type: ' + str(type(h3shape)))
+
+ return _out_collection(mv)
+
+
+def cells_to_h3shape(cells, *, tight=True):
+ """
+ Return an ``H3Shape`` describing the area covered by a collection of H3 cells.
+ Will return ``LatLngPoly`` or ``LatLngMultiPoly``.
+
+ Parameters
+ ----------
+ cells : iterable of H3 cells
+ tight : bool
+ If True, return ``LatLngPoly`` if possible.
+ If False, always return ``LatLngMultiPoly``.
+
+ Returns
+ -------
+ LatLngPoly | LatLngMultiPoly
+
+ Examples
+ --------
+
+ >>> cells = ['8428309ffffffff', '842830dffffffff']
+ >>> h3.cells_to_h3shape(cells, tight=True)
+
+ >>> h3.cells_to_h3shape(cells, tight=False)
+
+ """
+ cells = _in_collection(cells)
+ mpoly = _cy.cells_to_multi_polygon(cells)
+
+ polys = [LatLngPoly(*poly) for poly in mpoly]
+ out = LatLngMultiPoly(*polys)
+
+ if tight and len(out) == 1:
+ out = out[0]
+
+ return out
+
+
+def geo_to_cells(geo, res):
+ """Convert from ``__geo_interface__`` to cells.
+
+ Parameters
+ ----------
+ geo : an object implementing ``__geo_interface__`` or a dictionary in that format.
+ Both ``LatLngPoly`` and ``LatLngMultiPoly`` implement the interface.
+ res : int
+ Resolution of desired output cells.
+
+ Notes
+ -----
+ There is currently no guaranteed order of the output cells.
+ """
+ h3shape = geo_to_h3shape(geo)
+ return h3shape_to_cells(h3shape, res)
+
+
+def cells_to_geo(cells, tight=True):
+ """
+ Convert from cells to a ``__geo_interface__`` dict.
+
+ Parameters
+ ----------
+ cells : iterable of H3 Cells
+ tight : bool
+ When ``True``, returns an ``LatLngPoly`` when possible.
+ When ``False``, always returns an ``LatLngMultiPoly``.
+
+ Returns
+ -------
+ dict
+ in `__geo_interface__` format
+ """
+ h3shape = cells_to_h3shape(cells, tight=tight)
+ return h3shape_to_geo(h3shape)
+
+
+def is_pentagon(h):
+ """
+ Identify if an H3 cell is a pentagon.
+
+ Parameters
+ ----------
+ h : H3Index
+
+ Returns
+ -------
+ bool
+ ``True`` if input is a valid H3 cell which is a pentagon.
+
+ Notes
+ -----
+ A pentagon should *also* pass ``is_valid_cell()``.
+ Will return ``False`` for valid H3Edge.
+ """
+ return _cy.is_pentagon(_in_scalar(h))
+
+
+def get_base_cell_number(h):
+ """
+ Return the base cell *number* (``0`` to ``121``) of the given cell.
+
+ The base cell *number* and the H3Index are two different representations
+ of the same cell: the parent cell of resolution ``0``.
+
+ The base cell *number* is encoded within the corresponding
+ H3Index.
+
+ todo: could work with edges
+
+ Parameters
+ ----------
+ h : H3Cell
+
+ Returns
+ -------
+ int
+ """
+ return _cy.get_base_cell_number(_in_scalar(h))
+
+
+def are_neighbor_cells(h1, h2):
+ """
+ Returns ``True`` if ``h1`` and ``h2`` are neighboring cells.
+
+ Parameters
+ ----------
+ h1 : H3Cell
+ h2 : H3Cell
+
+ Returns
+ -------
+ bool
+ """
+ h1 = _in_scalar(h1)
+ h2 = _in_scalar(h2)
+
+ return _cy.are_neighbor_cells(h1, h2)
+
+
+def cells_to_directed_edge(origin, destination):
+ """
+ Create an H3 Index denoting a unidirectional edge.
+
+ The edge is constructed from neighboring cells ``origin`` and
+ ``destination``.
+
+ Parameters
+ ----------
+ origin : H3Cell
+ destination : H3Cell
+
+ Raises
+ ------
+ ValueError
+ When cells are not adjacent.
+
+ Returns
+ -------
+ H3Edge
+ """
+ o = _in_scalar(origin)
+ d = _in_scalar(destination)
+ e = _cy.cells_to_directed_edge(o, d)
+ e = _out_scalar(e)
+
+ return e
+
+
+def get_directed_edge_origin(e):
+ """
+ Origin cell from an H3 directed edge.
+
+ Parameters
+ ----------
+ e : H3Edge
+
+ Returns
+ -------
+ H3Cell
+ """
+ e = _in_scalar(e)
+ o = _cy.get_directed_edge_origin(e)
+ o = _out_scalar(o)
+
+ return o
+
+
+def get_directed_edge_destination(e):
+ """
+ Destination cell from an H3 directed edge.
+
+ Parameters
+ ----------
+ e : H3Edge
+
+ Returns
+ -------
+ H3Cell
+ """
+ e = _in_scalar(e)
+ d = _cy.get_directed_edge_destination(e)
+ d = _out_scalar(d)
+
+ return d
+
+
+def directed_edge_to_cells(e):
+ """
+ Return (origin, destination) tuple from H3 directed edge
+
+ Parameters
+ ----------
+ e : H3Edge
+
+ Returns
+ -------
+ H3Cell
+ Origin cell of edge
+ H3Cell
+ Destination cell of edge
+ """
+ e = _in_scalar(e)
+ o, d = _cy.directed_edge_to_cells(e)
+ o, d = _out_scalar(o), _out_scalar(d)
+
+ return o, d
+
+
+def origin_to_directed_edges(origin):
+ """
+ Return all directed edges starting from ``origin`` cell.
+
+ Parameters
+ ----------
+ origin : H3Cell
+
+ Returns
+ -------
+ unordered collection of H3Edge
+ """
+ mv = _cy.origin_to_directed_edges(_in_scalar(origin))
+
+ return _out_collection(mv)
+
+
+def directed_edge_to_boundary(edge):
+ """
+ Returns points representing the edge (line of points
+ describing the boundary between two cells).
+
+ Parameters
+ ----------
+ edge : H3Edge
+
+ Returns
+ -------
+ tuple of (lat, lng) tuples
+ """
+ return _cy.directed_edge_to_boundary(_in_scalar(edge))
+
+
+def grid_path_cells(start, end):
+ """
+ Returns the ordered collection of cells denoting a
+ minimum-length non-unique path between cells.
+
+ Parameters
+ ----------
+ start : H3Cell
+ end : H3Cell
+
+ Returns
+ -------
+ ordered collection of H3Cell
+ Starting with ``start``, and ending with ``end``.
+ """
+ mv = _cy.grid_path_cells(_in_scalar(start), _in_scalar(end))
+
+ return _out_collection(mv)
+
+
+def is_res_class_III(h):
+ """
+ Determine if cell has orientation "Class II" or "Class III".
+
+ The orientation of pentagons/hexagons on the icosahedron can be one
+ of two types: "Class II" or "Class III".
+
+ All cells within a resolution have the same type, and the type
+ alternates between resolutions.
+
+ "Class II" cells have resolutions: 0,2,4,6,8,10,12,14
+ "Class III" cells have resolutions: 1,3,5,7,9,11,13,15
+
+ Parameters
+ ----------
+ h : H3Cell
+
+ Returns
+ -------
+ bool
+ ``True`` if ``h`` is "Class III".
+ ``False`` if ``h`` is "Class II".
+
+ References
+ ----------
+ 1. https://uber.github.io/h3/#/documentation/core-library/coordinate-systems
+ """
+ return _cy.is_res_class_iii(_in_scalar(h))
+
+
+def get_pentagons(res):
+ """
+ Return all pentagons at a given resolution.
+
+ Parameters
+ ----------
+ res : int
+ Resolution of the pentagons
+
+ Returns
+ -------
+ unordered collection of H3Cell
+ """
+ mv = _cy.get_pentagons(res)
+
+ return _out_collection(mv)
+
+
+def get_res0_cells():
+ """
+ Return all cells at resolution 0.
+
+ Parameters
+ ----------
+ None
+
+ Returns
+ -------
+ unordered collection of H3Cell
+
+ Notes
+ -----
+ There is currently no guaranteed order of the output cells.
+ """
+ mv = _cy.get_res0_cells()
+
+ return _out_collection(mv)
+
+
+def cell_to_center_child(h, res=None):
+ """
+ Get the center child of a cell at some finer resolution.
+
+ Parameters
+ ----------
+ h : H3Cell
+ res : int or None, optional
+ The resolution for the child cell
+ If ``None``, then ``res = resolution(h) + 1``
+
+ Returns
+ -------
+ H3Cell
+ """
+ h = _in_scalar(h)
+ p = _cy.cell_to_center_child(h, res)
+ p = _out_scalar(p)
+
+ return p
+
+
+def get_icosahedron_faces(h):
+ """
+ Return icosahedron faces intersecting a given H3 cell.
+
+ There are twenty possible faces, ranging from 0--19.
+
+ Note: Every interface returns a Python ``set`` of ``int``.
+
+ Parameters
+ ----------
+ h : H3Cell
+
+ Returns
+ -------
+ Python ``set`` of ``int``
+ """
+ h = _in_scalar(h)
+ faces = _cy.get_icosahedron_faces(h)
+
+ return faces
+
+
+def cell_to_local_ij(origin, h):
+ """
+ Return local (i,j) coordinates of cell ``h`` in relation to ``origin`` cell
+
+
+ Parameters
+ ----------
+ origin : H3Cell
+ Origin/central cell for defining i,j coordinates.
+ h: H3Cell
+ Destination cell whose i,j coordinates we'd like, based off
+ of the origin cell.
+
+
+ Returns
+ -------
+ Tuple (i, j) of integer local coordinates of cell ``h``
+
+
+ Notes
+ -----
+
+ The ``origin`` cell does not define (0, 0) for the IJ coordinate space.
+ (0, 0) refers to the center of the base cell containing origin at the
+ resolution of `origin`.
+ Subtracting the IJ coordinates of ``origin`` from every cell would get
+ you the property of (0, 0) being the ``origin``.
+
+ This is done so we don't need to keep recomputing the coordinates of
+ ``origin`` if not needed.
+ """
+ origin = _in_scalar(origin)
+ h = _in_scalar(h)
+
+ i, j = _cy.cell_to_local_ij(origin, h)
+
+ return i, j
+
+
+def local_ij_to_cell(origin, i, j):
+ """
+ Return cell at local (i,j) position relative to the ``origin`` cell.
+
+ Parameters
+ ----------
+ origin : H3Cell
+ Origin/central cell for defining i,j coordinates.
+ i, j: int
+ Integer coordinates with respect to ``origin`` cell.
+
+
+ Returns
+ -------
+ H3Cell at local (i,j) position relative to the ``origin`` cell
+
+
+ Notes
+ -----
+
+ The ``origin`` cell does not define (0, 0) for the IJ coordinate space.
+ (0, 0) refers to the center of the base cell containing origin at the
+ resolution of ``origin``.
+ Subtracting the IJ coordinates of ``origin`` from every cell would get
+ you the property of (0, 0) being the ``origin``.
+
+ This is done so we don't need to keep recomputing the coordinates of
+ ``origin`` if not needed.
+ """
+ origin = _in_scalar(origin)
+
+ h = _cy.local_ij_to_cell(origin, i, j)
+ h = _out_scalar(h)
+
+ return h
+
+
+def cell_area(h, unit='km^2'):
+ """
+ Compute the spherical surface area of a specific H3 cell.
+
+ Parameters
+ ----------
+ h : H3Cell
+ unit: str
+ Unit for area result (``'km^2'``, ``'m^2'``, or ``'rads^2'``)
+
+
+ Returns
+ -------
+ The area of the H3 cell in the given units
+
+
+ Notes
+ -----
+ This function breaks the cell into spherical triangles, and computes
+ their spherical area.
+ The function uses the spherical distance calculation given by
+ ``great_circle_distance()``.
+ """
+ h = _in_scalar(h)
+
+ return _cy.cell_area(h, unit=unit)
+
+
+def edge_length(e, unit='km'):
+ """
+ Compute the spherical length of a specific H3 edge.
+
+ Parameters
+ ----------
+ h : H3Cell
+ unit: str
+ Unit for length result (``'km'``, ``'m'``, or ``'rads'``)
+
+
+ Returns
+ -------
+ The length of the edge in the given units
+
+
+ Notes
+ -----
+ This function uses the spherical distance calculation given by
+ ``great_circle_distance()``.
+ """
+ e = _in_scalar(e)
+
+ return _cy.edge_length(e, unit=unit)
+
+
+def great_circle_distance(latlng1, latlng2, unit='km'):
+ """
+ Compute the spherical distance between two (lat, lng) points.
+ AKA: great circle distance or "haversine" distance.
+
+ todo: overload to allow two cell inputs?
+
+ Parameters
+ ----------
+ latlng1 : tuple
+ (lat, lng) tuple in degrees
+ latlng2 : tuple
+ (lat, lng) tuple in degrees
+ unit: str
+ Unit for distance result (``'km'``, ``'m'``, or ``'rads'``)
+
+ Returns
+ -------
+ The spherical distance between the points in the given units
+ """
+ lat1, lng1 = latlng1
+ lat2, lng2 = latlng2
+ return _cy.great_circle_distance(
+ lat1, lng1,
+ lat2, lng2,
+ unit = unit
+ )
+
+
+def cell_to_vertex(h, vertex_num):
+ """
+ Return a (specified) vertex of an H3 cell.
+
+ Parameters
+ ----------
+ h : H3Cell
+ vertex_num : int
+ Vertex number (0-5)
+
+ Returns
+ -------
+ The vertex
+ """
+ h = _in_scalar(h)
+ h = _cy.cell_to_vertex(h, vertex_num)
+ return _out_scalar(h)
+
+
+def cell_to_vertexes(h):
+ """
+ Return a list of vertexes of an H3 cell.
+ The list will be of length 5 for pentagons and 6 for hexagons.
+
+ Parameters
+ ----------
+ h : H3Cell
+
+ Returns
+ -------
+ A list of vertexes
+ """
+ h = _in_scalar(h)
+ mv = _cy.cell_to_vertexes(h)
+ return _out_collection(mv)
+
+
+def vertex_to_latlng(v):
+ """
+ Return latitude and longitude of a vertex.
+
+ Returns
+ -------
+ lat : float
+ Latitude
+ lng : float
+ Longitude
+ """
+ v = _in_scalar(v)
+ return _cy.vertex_to_latlng(v)
+
+
+def is_valid_vertex(v):
+ """
+ Validates an H3 vertex.
+
+ Returns
+ -------
+ bool
+ """
+ try:
+ v = _in_scalar(v)
+ return _cy.is_valid_vertex(v)
+ except (ValueError, TypeError):
+ return False
diff --git a/frogpilot/third_party/h3/api/memview_int/_convert.py b/frogpilot/third_party/h3/api/memview_int/_convert.py
new file mode 100644
index 00000000..2f054fd2
--- /dev/null
+++ b/frogpilot/third_party/h3/api/memview_int/_convert.py
@@ -0,0 +1,8 @@
+def _id(x):
+ return x
+
+
+_in_scalar = _id
+_out_scalar = _id
+_in_collection = _id
+_out_collection = _id
diff --git a/frogpilot/third_party/h3/api/numpy_int/__init__.py b/frogpilot/third_party/h3/api/numpy_int/__init__.py
new file mode 100644
index 00000000..09f7deb8
--- /dev/null
+++ b/frogpilot/third_party/h3/api/numpy_int/__init__.py
@@ -0,0 +1,1240 @@
+# This file is **symlinked** across the APIs to ensure they are exactly the same.
+from typing import Literal
+
+from ... import _cy
+from ..._h3shape import (
+ H3Shape,
+ LatLngPoly,
+ LatLngMultiPoly,
+ geo_to_h3shape,
+ h3shape_to_geo,
+)
+
+from ._convert import (
+ _in_scalar,
+ _out_scalar,
+ _in_collection,
+ _out_collection,
+)
+
+
+def versions():
+ """
+ Version numbers for the Python (wrapper) and C (wrapped) libraries.
+
+ Versions are output as strings of the form ``'X.Y.Z'``.
+ C and Python should match on ``X`` (major) and ``Y`` (minor),
+ but may differ on ``Z`` (patch).
+
+ Returns
+ -------
+ dict like ``{'c': 'X.Y.Z', 'python': 'A.B.C'}``
+ """
+ from ..._version import __version__
+
+ v = {
+ 'c': _cy.c_version(),
+ 'python': __version__,
+ }
+
+ return v
+
+
+def str_to_int(h):
+ """
+ Converts a hexadecimal string to an H3 64-bit integer index.
+
+ Parameters
+ ----------
+ h : str
+ Hexadecimal string like ``'89754e64993ffff'``
+
+ Returns
+ -------
+ int
+ Unsigned 64-bit integer
+ """
+ return _cy.str_to_int(h)
+
+
+def int_to_str(x):
+ """
+ Converts an H3 64-bit integer index to a hexadecimal string.
+
+ Parameters
+ ----------
+ x : int
+ Unsigned 64-bit integer
+
+ Returns
+ -------
+ str
+ Hexadecimal string like ``'89754e64993ffff'``
+ """
+ return _cy.int_to_str(x)
+
+
+def get_num_cells(res):
+ """
+ Return the total number of *cells* (hexagons and pentagons)
+ for the given resolution.
+
+ Returns
+ -------
+ int
+ """
+ return _cy.get_num_cells(res)
+
+
+def average_hexagon_area(res, unit='km^2'):
+ """
+ Return the average area of an H3 *hexagon*
+ for the given resolution.
+
+ This average *excludes* pentagons.
+
+ Parameters
+ ----------
+ res : int
+ H3 resolution
+ unit: str
+ Unit for area result (``'km^2'``, ``'m^2'``, or ``'rads^2'``)
+
+ Returns
+ -------
+ float
+ """
+ return _cy.average_hexagon_area(res, unit)
+
+
+def average_hexagon_edge_length(res, unit='km'):
+ """
+ Return the average *hexagon* edge length
+ for the given resolution.
+
+ This average *excludes* pentagons.
+
+ Parameters
+ ----------
+ res : int
+ H3 resolution
+ unit: str
+ Unit for length result (``'km'``, ``'m'``, or ``'rads'``)
+
+ Returns
+ -------
+ float
+ """
+ return _cy.average_hexagon_edge_length(res, unit)
+
+
+def is_valid_cell(h):
+ """
+ Validates an H3 cell (hexagon or pentagon).
+
+ Returns
+ -------
+ bool
+ """
+ try:
+ h = _in_scalar(h)
+ return _cy.is_valid_cell(h)
+ except (ValueError, TypeError):
+ return False
+
+
+def is_valid_directed_edge(edge):
+ """
+ Validates an H3 unidirectional edge.
+
+ Returns
+ -------
+ bool
+ """
+ try:
+ e = _in_scalar(edge)
+ return _cy.is_valid_directed_edge(e)
+ except (ValueError, TypeError):
+ return False
+
+
+def latlng_to_cell(lat, lng, res):
+ """
+ Return the cell containing the (lat, lng) point
+ for a given resolution.
+
+ Returns
+ -------
+ H3Cell
+
+ """
+ return _out_scalar(_cy.latlng_to_cell(lat, lng, res))
+
+
+def cell_to_latlng(h):
+ """
+ Return the center point of an H3 cell as a lat/lng pair.
+
+ Parameters
+ ----------
+ h : H3Cell
+
+ Returns
+ -------
+ lat : float
+ Latitude
+ lng : float
+ Longitude
+ """
+ h = _in_scalar(h)
+ return _cy.cell_to_latlng(h)
+
+
+def get_resolution(h):
+ """
+ Return the resolution of an H3 cell.
+
+ Parameters
+ ----------
+ h : H3Cell
+
+ Returns
+ -------
+ int
+ """
+ # todo: could also work for edges
+ h = _in_scalar(h)
+ return _cy.get_resolution(h)
+
+
+def cell_to_parent(h, res=None):
+ """
+ Get the parent of a cell.
+
+ Parameters
+ ----------
+ h : H3Cell
+ res : int or None, optional
+ The resolution for the parent
+ If ``None``, then ``res = resolution(h) - 1``
+
+ Returns
+ -------
+ H3Cell
+ """
+ h = _in_scalar(h)
+ p = _cy.cell_to_parent(h, res)
+ p = _out_scalar(p)
+
+ return p
+
+
+def grid_distance(h1, h2):
+ """
+ Compute the grid distance between two cells.
+
+ The grid distance is defined as the length of the shortest
+ path between the cells in the graph formed by connecting
+ adjacent cells.
+
+ This function will raise an exception if the
+ cells are too far apart to compute the distance.
+
+ Parameters
+ ----------
+ h1 : H3Cell
+ h2 : H3Cell
+
+ Returns
+ -------
+ int
+ """
+ h1 = _in_scalar(h1)
+ h2 = _in_scalar(h2)
+
+ d = _cy.grid_distance(h1, h2)
+
+ return d
+
+
+def cell_to_boundary(h):
+ """
+ Return tuple of lat/lng pairs describing the cell boundary.
+
+ Parameters
+ ----------
+ h : H3Cell
+
+ Returns
+ -------
+ tuple of (lat, lng) tuples
+ """
+ h = _in_scalar(h)
+ return _cy.cell_to_boundary(h)
+
+
+def grid_disk(h, k=1):
+ """
+ Return unordered collection of cells with grid distance ``<= k`` from ``h``.
+ That is, the "filled-in" disk.
+
+ Parameters
+ ----------
+ h : H3Cell
+ k : int
+ Size of disk.
+
+ Returns
+ -------
+ unordered collection of H3Cell
+
+ Notes
+ -----
+ There is currently no guaranteed order of the output cells.
+ """
+ h = _in_scalar(h)
+ mv = _cy.grid_disk(h, k)
+
+ return _out_collection(mv)
+
+
+def grid_ring(h, k=1):
+ """
+ Return unordered collection of cells with grid distance ``== k`` from ``h``.
+ That is, the "hollow" ring.
+
+ Parameters
+ ----------
+ h : H3Cell
+ k : int
+ Size of ring.
+
+ Returns
+ -------
+ unordered collection of H3Cell
+
+ Notes
+ -----
+ There is currently no guaranteed order of the output cells.
+ """
+ h = _in_scalar(h)
+ mv = _cy.grid_ring(h, k)
+
+ return _out_collection(mv)
+
+
+def cell_to_children_size(h, res=None):
+ """
+ Number of children at resolution ``res`` of given cell.
+
+ Parameters
+ ----------
+ h : H3Cell
+ res : int or None, optional
+ The resolution for the children.
+ If ``None``, then ``res = resolution(h) + 1``
+
+ Returns
+ -------
+ int
+ Count of children
+ """
+ h = _in_scalar(h)
+ return _cy.cell_to_children_size(h, res)
+
+
+def cell_to_children(h, res=None):
+ """
+ Children of a cell as an unordered collection.
+
+ Parameters
+ ----------
+ h : H3Cell
+ res : int or None, optional
+ The resolution for the children.
+ If ``None``, then ``res = resolution(h) + 1``
+
+ Returns
+ -------
+ unordered collection of H3Cell
+
+ Notes
+ -----
+ There is currently no guaranteed order of the output cells.
+ """
+ h = _in_scalar(h)
+ mv = _cy.cell_to_children(h, res)
+
+ return _out_collection(mv)
+
+
+def cell_to_child_pos(child, res_parent):
+ """
+ Child position index of given cell, with respect to its parent at ``res_parent``.
+
+ The reverse operation can be done with ``child_pos_to_cell``.
+
+ Parameters
+ ----------
+ child : H3Cell
+ res_parent : int
+
+ Returns
+ -------
+ int
+ Integer index of the child with respect to parent cell.
+ """
+ child = _in_scalar(child)
+ return _cy.cell_to_child_pos(child, res_parent)
+
+
+def child_pos_to_cell(parent, res_child, child_pos):
+ """
+ Get child H3 cell from a parent cell, child resolution, and child position index.
+
+ The reverse operation can be done with ``cell_to_child_pos``.
+
+ Parameters
+ ----------
+ parent : H3Cell
+ res_child : int
+ Child cell resolution
+ child_pos : int
+ Integer position of child cell, releative to parent.
+
+
+ Returns
+ -------
+ H3Cell
+ """
+ parent = _in_scalar(parent)
+ child = _cy.child_pos_to_cell(parent, res_child, child_pos)
+ child = _out_scalar(child)
+
+ return child
+
+
+# todo: nogil for expensive C operation?
+def compact_cells(cells):
+ """
+ Compact a collection of H3 cells by combining
+ smaller cells into larger cells, if all child cells
+ are present. Input cells must all share the same resolution.
+
+ Parameters
+ ----------
+ cells : iterable of H3 Cells
+
+ Returns
+ -------
+ unordered collection of H3Cell
+
+ Notes
+ -----
+ There is currently no guaranteed order of the output cells.
+ """
+ hu = _in_collection(cells)
+ hc = _cy.compact_cells(hu)
+
+ return _out_collection(hc)
+
+
+def uncompact_cells(cells, res):
+ """
+ Reverse the ``compact_cells`` operation.
+
+ Return a collection of H3 cells, all of resolution ``res``.
+
+ Parameters
+ ----------
+ cells : iterable of H3Cell
+ res : int
+ Resolution of desired output cells.
+
+ Returns
+ -------
+ unordered collection of H3Cell
+
+ Notes
+ -----
+ There is currently no guaranteed order of the output cells.
+ """
+ # TODO: add test to make sure an error is returned when input contains cell
+ # smaller than output res.
+
+ hc = _in_collection(cells)
+ hu = _cy.uncompact_cells(hc, res)
+
+ return _out_collection(hu)
+
+
+def polygon_to_cells(h3shape, res):
+ """
+ Alias for ``h3shape_to_cells``.
+ """
+ return h3shape_to_cells(h3shape, res)
+
+
+def h3shape_to_cells(h3shape, res):
+ """
+ Return the collection of H3 cells at a given resolution whose center points
+ are contained within an ``LatLngPoly`` or ``LatLngMultiPoly``.
+
+ Parameters
+ ----------
+ h3shape : ``H3Shape``
+ res : int
+ Resolution of the output cells
+
+ Returns
+ -------
+ list of H3Cell
+
+ Examples
+ --------
+
+ >>> poly = LatLngPoly(
+ ... [(37.68, -122.54), (37.68, -122.34), (37.82, -122.34),
+ ... (37.82, -122.54)],
+ ... )
+ >>> h3.h3shape_to_cells(poly, 6)
+ ['862830807ffffff',
+ '862830827ffffff',
+ '86283082fffffff',
+ '862830877ffffff',
+ '862830947ffffff',
+ '862830957ffffff',
+ '86283095fffffff']
+
+ Notes
+ -----
+ There is currently no guaranteed order of the output cells.
+ """
+
+ # todo: not sure if i want this dispatch logic here. maybe in the objects?
+ if isinstance(h3shape, LatLngPoly):
+ poly = h3shape
+ mv = _cy.polygon_to_cells(poly.outer, res=res, holes=poly.holes)
+ elif isinstance(h3shape, LatLngMultiPoly):
+ mpoly = h3shape
+ mv = _cy.polygons_to_cells(mpoly.polys, res=res)
+ elif isinstance(h3shape, H3Shape):
+ raise ValueError('Unrecognized H3Shape: ' + str(h3shape))
+ else:
+ raise ValueError('Unrecognized type: ' + str(type(h3shape)))
+
+ return _out_collection(mv)
+
+
+def polygon_to_cells_experimental(
+ h3shape: H3Shape,
+ res: int,
+ contain: Literal['center', 'full', 'overlap', 'bbox_overlap'] = 'center',
+):
+ """
+ Alias for ``h3shape_to_cells_experimental``.
+ """
+ return h3shape_to_cells_experimental(h3shape, res, contain)
+
+
+def h3shape_to_cells_experimental(
+ h3shape: H3Shape,
+ res: int,
+ contain: Literal['center', 'full', 'overlap', 'bbox_overlap'] = 'center',
+):
+ """
+ Experimental function similar to ``h3shape_to_cells``, but with support for
+ multiple cell containment modes.
+
+ Using ``contain='center'`` should give identical behavior as
+ ``h3shape_to_cells``.
+
+ Note that this function is **experimental** and has no API stability gaurantees
+ across versions, so it may change in the future.
+
+
+ Parameters
+ ----------
+ h3shape : ``H3Shape``
+ res : int
+ Resolution of the output cells
+ contain : {'center', 'full', 'overlap', 'bbox_overlap'}, optional
+ Specifies the containment condition.
+ - 'center': Cell center is contained in shape
+ - 'full': Cell is fully contained in shape
+ - 'overlap': Cell is partially contained in shape
+ - 'bbox_overlap': Cell bounding box is partially contained in shape
+
+ Default is 'center'.
+
+ Returns
+ -------
+ list of H3Cell
+
+ Examples
+ --------
+
+ >>> poly = LatLngPoly(
+ ... [(37.68, -122.54), (37.68, -122.34), (37.82, -122.34),
+ ... (37.82, -122.54)],
+ ... )
+ >>> h3.h3shape_to_cells_experimental(poly, 6, 'center')
+ ['862830807ffffff',
+ '862830827ffffff',
+ '86283082fffffff',
+ '862830877ffffff',
+ '862830947ffffff',
+ '862830957ffffff',
+ '86283095fffffff']
+
+ Notes
+ -----
+ There is currently no guaranteed order of the output cells.
+ """
+
+ contain_modes = {
+ 'center': 0,
+ 'full': 1,
+ 'overlap': 2,
+ 'bbox_overlap': 3,
+ }
+
+ flag = contain_modes[contain]
+
+ # todo: not sure if i want this dispatch logic here. maybe in the objects?
+ if isinstance(h3shape, LatLngPoly):
+ poly = h3shape
+ mv = _cy.polygon_to_cells_experimental(
+ poly.outer,
+ res = res,
+ holes = poly.holes,
+ flag = flag,
+ )
+ elif isinstance(h3shape, LatLngMultiPoly):
+ mpoly = h3shape
+ mv = _cy.polygons_to_cells_experimental(mpoly.polys, res=res, flag=flag)
+ elif isinstance(h3shape, H3Shape):
+ raise ValueError('Unrecognized H3Shape: ' + str(h3shape))
+ else:
+ raise ValueError('Unrecognized type: ' + str(type(h3shape)))
+
+ return _out_collection(mv)
+
+
+def cells_to_h3shape(cells, *, tight=True):
+ """
+ Return an ``H3Shape`` describing the area covered by a collection of H3 cells.
+ Will return ``LatLngPoly`` or ``LatLngMultiPoly``.
+
+ Parameters
+ ----------
+ cells : iterable of H3 cells
+ tight : bool
+ If True, return ``LatLngPoly`` if possible.
+ If False, always return ``LatLngMultiPoly``.
+
+ Returns
+ -------
+ LatLngPoly | LatLngMultiPoly
+
+ Examples
+ --------
+
+ >>> cells = ['8428309ffffffff', '842830dffffffff']
+ >>> h3.cells_to_h3shape(cells, tight=True)
+
+ >>> h3.cells_to_h3shape(cells, tight=False)
+
+ """
+ cells = _in_collection(cells)
+ mpoly = _cy.cells_to_multi_polygon(cells)
+
+ polys = [LatLngPoly(*poly) for poly in mpoly]
+ out = LatLngMultiPoly(*polys)
+
+ if tight and len(out) == 1:
+ out = out[0]
+
+ return out
+
+
+def geo_to_cells(geo, res):
+ """Convert from ``__geo_interface__`` to cells.
+
+ Parameters
+ ----------
+ geo : an object implementing ``__geo_interface__`` or a dictionary in that format.
+ Both ``LatLngPoly`` and ``LatLngMultiPoly`` implement the interface.
+ res : int
+ Resolution of desired output cells.
+
+ Notes
+ -----
+ There is currently no guaranteed order of the output cells.
+ """
+ h3shape = geo_to_h3shape(geo)
+ return h3shape_to_cells(h3shape, res)
+
+
+def cells_to_geo(cells, tight=True):
+ """
+ Convert from cells to a ``__geo_interface__`` dict.
+
+ Parameters
+ ----------
+ cells : iterable of H3 Cells
+ tight : bool
+ When ``True``, returns an ``LatLngPoly`` when possible.
+ When ``False``, always returns an ``LatLngMultiPoly``.
+
+ Returns
+ -------
+ dict
+ in `__geo_interface__` format
+ """
+ h3shape = cells_to_h3shape(cells, tight=tight)
+ return h3shape_to_geo(h3shape)
+
+
+def is_pentagon(h):
+ """
+ Identify if an H3 cell is a pentagon.
+
+ Parameters
+ ----------
+ h : H3Index
+
+ Returns
+ -------
+ bool
+ ``True`` if input is a valid H3 cell which is a pentagon.
+
+ Notes
+ -----
+ A pentagon should *also* pass ``is_valid_cell()``.
+ Will return ``False`` for valid H3Edge.
+ """
+ return _cy.is_pentagon(_in_scalar(h))
+
+
+def get_base_cell_number(h):
+ """
+ Return the base cell *number* (``0`` to ``121``) of the given cell.
+
+ The base cell *number* and the H3Index are two different representations
+ of the same cell: the parent cell of resolution ``0``.
+
+ The base cell *number* is encoded within the corresponding
+ H3Index.
+
+ todo: could work with edges
+
+ Parameters
+ ----------
+ h : H3Cell
+
+ Returns
+ -------
+ int
+ """
+ return _cy.get_base_cell_number(_in_scalar(h))
+
+
+def are_neighbor_cells(h1, h2):
+ """
+ Returns ``True`` if ``h1`` and ``h2`` are neighboring cells.
+
+ Parameters
+ ----------
+ h1 : H3Cell
+ h2 : H3Cell
+
+ Returns
+ -------
+ bool
+ """
+ h1 = _in_scalar(h1)
+ h2 = _in_scalar(h2)
+
+ return _cy.are_neighbor_cells(h1, h2)
+
+
+def cells_to_directed_edge(origin, destination):
+ """
+ Create an H3 Index denoting a unidirectional edge.
+
+ The edge is constructed from neighboring cells ``origin`` and
+ ``destination``.
+
+ Parameters
+ ----------
+ origin : H3Cell
+ destination : H3Cell
+
+ Raises
+ ------
+ ValueError
+ When cells are not adjacent.
+
+ Returns
+ -------
+ H3Edge
+ """
+ o = _in_scalar(origin)
+ d = _in_scalar(destination)
+ e = _cy.cells_to_directed_edge(o, d)
+ e = _out_scalar(e)
+
+ return e
+
+
+def get_directed_edge_origin(e):
+ """
+ Origin cell from an H3 directed edge.
+
+ Parameters
+ ----------
+ e : H3Edge
+
+ Returns
+ -------
+ H3Cell
+ """
+ e = _in_scalar(e)
+ o = _cy.get_directed_edge_origin(e)
+ o = _out_scalar(o)
+
+ return o
+
+
+def get_directed_edge_destination(e):
+ """
+ Destination cell from an H3 directed edge.
+
+ Parameters
+ ----------
+ e : H3Edge
+
+ Returns
+ -------
+ H3Cell
+ """
+ e = _in_scalar(e)
+ d = _cy.get_directed_edge_destination(e)
+ d = _out_scalar(d)
+
+ return d
+
+
+def directed_edge_to_cells(e):
+ """
+ Return (origin, destination) tuple from H3 directed edge
+
+ Parameters
+ ----------
+ e : H3Edge
+
+ Returns
+ -------
+ H3Cell
+ Origin cell of edge
+ H3Cell
+ Destination cell of edge
+ """
+ e = _in_scalar(e)
+ o, d = _cy.directed_edge_to_cells(e)
+ o, d = _out_scalar(o), _out_scalar(d)
+
+ return o, d
+
+
+def origin_to_directed_edges(origin):
+ """
+ Return all directed edges starting from ``origin`` cell.
+
+ Parameters
+ ----------
+ origin : H3Cell
+
+ Returns
+ -------
+ unordered collection of H3Edge
+ """
+ mv = _cy.origin_to_directed_edges(_in_scalar(origin))
+
+ return _out_collection(mv)
+
+
+def directed_edge_to_boundary(edge):
+ """
+ Returns points representing the edge (line of points
+ describing the boundary between two cells).
+
+ Parameters
+ ----------
+ edge : H3Edge
+
+ Returns
+ -------
+ tuple of (lat, lng) tuples
+ """
+ return _cy.directed_edge_to_boundary(_in_scalar(edge))
+
+
+def grid_path_cells(start, end):
+ """
+ Returns the ordered collection of cells denoting a
+ minimum-length non-unique path between cells.
+
+ Parameters
+ ----------
+ start : H3Cell
+ end : H3Cell
+
+ Returns
+ -------
+ ordered collection of H3Cell
+ Starting with ``start``, and ending with ``end``.
+ """
+ mv = _cy.grid_path_cells(_in_scalar(start), _in_scalar(end))
+
+ return _out_collection(mv)
+
+
+def is_res_class_III(h):
+ """
+ Determine if cell has orientation "Class II" or "Class III".
+
+ The orientation of pentagons/hexagons on the icosahedron can be one
+ of two types: "Class II" or "Class III".
+
+ All cells within a resolution have the same type, and the type
+ alternates between resolutions.
+
+ "Class II" cells have resolutions: 0,2,4,6,8,10,12,14
+ "Class III" cells have resolutions: 1,3,5,7,9,11,13,15
+
+ Parameters
+ ----------
+ h : H3Cell
+
+ Returns
+ -------
+ bool
+ ``True`` if ``h`` is "Class III".
+ ``False`` if ``h`` is "Class II".
+
+ References
+ ----------
+ 1. https://uber.github.io/h3/#/documentation/core-library/coordinate-systems
+ """
+ return _cy.is_res_class_iii(_in_scalar(h))
+
+
+def get_pentagons(res):
+ """
+ Return all pentagons at a given resolution.
+
+ Parameters
+ ----------
+ res : int
+ Resolution of the pentagons
+
+ Returns
+ -------
+ unordered collection of H3Cell
+ """
+ mv = _cy.get_pentagons(res)
+
+ return _out_collection(mv)
+
+
+def get_res0_cells():
+ """
+ Return all cells at resolution 0.
+
+ Parameters
+ ----------
+ None
+
+ Returns
+ -------
+ unordered collection of H3Cell
+
+ Notes
+ -----
+ There is currently no guaranteed order of the output cells.
+ """
+ mv = _cy.get_res0_cells()
+
+ return _out_collection(mv)
+
+
+def cell_to_center_child(h, res=None):
+ """
+ Get the center child of a cell at some finer resolution.
+
+ Parameters
+ ----------
+ h : H3Cell
+ res : int or None, optional
+ The resolution for the child cell
+ If ``None``, then ``res = resolution(h) + 1``
+
+ Returns
+ -------
+ H3Cell
+ """
+ h = _in_scalar(h)
+ p = _cy.cell_to_center_child(h, res)
+ p = _out_scalar(p)
+
+ return p
+
+
+def get_icosahedron_faces(h):
+ """
+ Return icosahedron faces intersecting a given H3 cell.
+
+ There are twenty possible faces, ranging from 0--19.
+
+ Note: Every interface returns a Python ``set`` of ``int``.
+
+ Parameters
+ ----------
+ h : H3Cell
+
+ Returns
+ -------
+ Python ``set`` of ``int``
+ """
+ h = _in_scalar(h)
+ faces = _cy.get_icosahedron_faces(h)
+
+ return faces
+
+
+def cell_to_local_ij(origin, h):
+ """
+ Return local (i,j) coordinates of cell ``h`` in relation to ``origin`` cell
+
+
+ Parameters
+ ----------
+ origin : H3Cell
+ Origin/central cell for defining i,j coordinates.
+ h: H3Cell
+ Destination cell whose i,j coordinates we'd like, based off
+ of the origin cell.
+
+
+ Returns
+ -------
+ Tuple (i, j) of integer local coordinates of cell ``h``
+
+
+ Notes
+ -----
+
+ The ``origin`` cell does not define (0, 0) for the IJ coordinate space.
+ (0, 0) refers to the center of the base cell containing origin at the
+ resolution of `origin`.
+ Subtracting the IJ coordinates of ``origin`` from every cell would get
+ you the property of (0, 0) being the ``origin``.
+
+ This is done so we don't need to keep recomputing the coordinates of
+ ``origin`` if not needed.
+ """
+ origin = _in_scalar(origin)
+ h = _in_scalar(h)
+
+ i, j = _cy.cell_to_local_ij(origin, h)
+
+ return i, j
+
+
+def local_ij_to_cell(origin, i, j):
+ """
+ Return cell at local (i,j) position relative to the ``origin`` cell.
+
+ Parameters
+ ----------
+ origin : H3Cell
+ Origin/central cell for defining i,j coordinates.
+ i, j: int
+ Integer coordinates with respect to ``origin`` cell.
+
+
+ Returns
+ -------
+ H3Cell at local (i,j) position relative to the ``origin`` cell
+
+
+ Notes
+ -----
+
+ The ``origin`` cell does not define (0, 0) for the IJ coordinate space.
+ (0, 0) refers to the center of the base cell containing origin at the
+ resolution of ``origin``.
+ Subtracting the IJ coordinates of ``origin`` from every cell would get
+ you the property of (0, 0) being the ``origin``.
+
+ This is done so we don't need to keep recomputing the coordinates of
+ ``origin`` if not needed.
+ """
+ origin = _in_scalar(origin)
+
+ h = _cy.local_ij_to_cell(origin, i, j)
+ h = _out_scalar(h)
+
+ return h
+
+
+def cell_area(h, unit='km^2'):
+ """
+ Compute the spherical surface area of a specific H3 cell.
+
+ Parameters
+ ----------
+ h : H3Cell
+ unit: str
+ Unit for area result (``'km^2'``, ``'m^2'``, or ``'rads^2'``)
+
+
+ Returns
+ -------
+ The area of the H3 cell in the given units
+
+
+ Notes
+ -----
+ This function breaks the cell into spherical triangles, and computes
+ their spherical area.
+ The function uses the spherical distance calculation given by
+ ``great_circle_distance()``.
+ """
+ h = _in_scalar(h)
+
+ return _cy.cell_area(h, unit=unit)
+
+
+def edge_length(e, unit='km'):
+ """
+ Compute the spherical length of a specific H3 edge.
+
+ Parameters
+ ----------
+ h : H3Cell
+ unit: str
+ Unit for length result (``'km'``, ``'m'``, or ``'rads'``)
+
+
+ Returns
+ -------
+ The length of the edge in the given units
+
+
+ Notes
+ -----
+ This function uses the spherical distance calculation given by
+ ``great_circle_distance()``.
+ """
+ e = _in_scalar(e)
+
+ return _cy.edge_length(e, unit=unit)
+
+
+def great_circle_distance(latlng1, latlng2, unit='km'):
+ """
+ Compute the spherical distance between two (lat, lng) points.
+ AKA: great circle distance or "haversine" distance.
+
+ todo: overload to allow two cell inputs?
+
+ Parameters
+ ----------
+ latlng1 : tuple
+ (lat, lng) tuple in degrees
+ latlng2 : tuple
+ (lat, lng) tuple in degrees
+ unit: str
+ Unit for distance result (``'km'``, ``'m'``, or ``'rads'``)
+
+ Returns
+ -------
+ The spherical distance between the points in the given units
+ """
+ lat1, lng1 = latlng1
+ lat2, lng2 = latlng2
+ return _cy.great_circle_distance(
+ lat1, lng1,
+ lat2, lng2,
+ unit = unit
+ )
+
+
+def cell_to_vertex(h, vertex_num):
+ """
+ Return a (specified) vertex of an H3 cell.
+
+ Parameters
+ ----------
+ h : H3Cell
+ vertex_num : int
+ Vertex number (0-5)
+
+ Returns
+ -------
+ The vertex
+ """
+ h = _in_scalar(h)
+ h = _cy.cell_to_vertex(h, vertex_num)
+ return _out_scalar(h)
+
+
+def cell_to_vertexes(h):
+ """
+ Return a list of vertexes of an H3 cell.
+ The list will be of length 5 for pentagons and 6 for hexagons.
+
+ Parameters
+ ----------
+ h : H3Cell
+
+ Returns
+ -------
+ A list of vertexes
+ """
+ h = _in_scalar(h)
+ mv = _cy.cell_to_vertexes(h)
+ return _out_collection(mv)
+
+
+def vertex_to_latlng(v):
+ """
+ Return latitude and longitude of a vertex.
+
+ Returns
+ -------
+ lat : float
+ Latitude
+ lng : float
+ Longitude
+ """
+ v = _in_scalar(v)
+ return _cy.vertex_to_latlng(v)
+
+
+def is_valid_vertex(v):
+ """
+ Validates an H3 vertex.
+
+ Returns
+ -------
+ bool
+ """
+ try:
+ v = _in_scalar(v)
+ return _cy.is_valid_vertex(v)
+ except (ValueError, TypeError):
+ return False
diff --git a/frogpilot/third_party/h3/api/numpy_int/_convert.py b/frogpilot/third_party/h3/api/numpy_int/_convert.py
new file mode 100644
index 00000000..6b293cc9
--- /dev/null
+++ b/frogpilot/third_party/h3/api/numpy_int/_convert.py
@@ -0,0 +1,15 @@
+def _in_scalar(x):
+ return x
+
+
+_out_scalar = _in_scalar
+
+
+def _in_collection(x):
+ import numpy as np
+ # array is copied only if dtype does not match
+ # `list`s should work, but not `set`s of integers
+ return np.asarray(x, dtype='uint64')
+
+
+_out_collection = _in_collection
diff --git a/frogpilot/third_party/reactivex-4.1.0.dist-info/INSTALLER b/frogpilot/third_party/reactivex-4.1.0.dist-info/INSTALLER
deleted file mode 100644
index a1b589e3..00000000
--- a/frogpilot/third_party/reactivex-4.1.0.dist-info/INSTALLER
+++ /dev/null
@@ -1 +0,0 @@
-pip
diff --git a/frogpilot/third_party/reactivex-4.1.0.dist-info/METADATA b/frogpilot/third_party/reactivex-4.1.0.dist-info/METADATA
deleted file mode 100644
index d8df2356..00000000
--- a/frogpilot/third_party/reactivex-4.1.0.dist-info/METADATA
+++ /dev/null
@@ -1,180 +0,0 @@
-Metadata-Version: 2.4
-Name: reactivex
-Version: 4.1.0
-Summary: ReactiveX (Rx) for Python
-License: MIT
-License-File: LICENSE
-Author: Dag Brattli
-Author-email: dag@brattli.net
-Requires-Python: >=3.8,<4.0
-Classifier: Development Status :: 5 - Production/Stable
-Classifier: Environment :: Other Environment
-Classifier: Intended Audience :: Developers
-Classifier: License :: OSI Approved :: MIT License
-Classifier: Operating System :: OS Independent
-Classifier: Programming Language :: Python :: 3
-Classifier: Programming Language :: Python :: 3.8
-Classifier: Programming Language :: Python :: 3.9
-Classifier: Programming Language :: Python :: 3.10
-Classifier: Programming Language :: Python :: 3.11
-Classifier: Programming Language :: Python :: 3.12
-Classifier: Programming Language :: Python :: 3.13
-Classifier: Programming Language :: Python :: 3.14
-Classifier: Programming Language :: Python :: Implementation :: CPython
-Classifier: Topic :: Software Development :: Libraries :: Python Modules
-Requires-Dist: typing-extensions (>=4.1.1,<5.0.0)
-Project-URL: Documentation, https://rxpy.readthedocs.io/en/latest/
-Project-URL: Homepage, http://reactivex.io
-Project-URL: Repository, https://github.com/ReactiveX/RxPY
-Description-Content-Type: text/x-rst
-
-===============================
-The ReactiveX for Python (RxPY)
-===============================
-
-.. image:: https://github.com/ReactiveX/RxPY/workflows/Python%20package/badge.svg
- :target: https://github.com/ReactiveX/RxPY/actions
- :alt: Build Status
-
-.. image:: https://img.shields.io/coveralls/ReactiveX/RxPY.svg
- :target: https://coveralls.io/github/ReactiveX/RxPY
- :alt: Coverage Status
-
-.. image:: https://img.shields.io/pypi/v/reactivex.svg
- :target: https://pypi.org/project/reactivex/
- :alt: PyPY Package Version
-
-.. image:: https://img.shields.io/readthedocs/rxpy.svg
- :target: https://readthedocs.org/projects/rxpy/builds/
- :alt: Documentation Status
-
-
-*A library for composing asynchronous and event-based programs using observable
-collections and query operator functions in Python*
-
-ReactiveX for Python v4
------------------------
-
-For v3.X please go to the `v3 branch
-`_.
-
-ReactiveX for Python v4.x runs on `Python `_ 3.9 or above. To
-install:
-
-.. code:: console
-
- pip3 install reactivex
-
-
-About ReactiveX
----------------
-
-ReactiveX for Python (RxPY) is a library for composing asynchronous and event-based
-programs using observable sequences and pipable query operators in Python. Using Rx,
-developers represent asynchronous data streams with Observables, query asynchronous data
-streams using operators, and parameterize concurrency in data/event streams using
-Schedulers.
-
-.. code:: python
-
- import reactivex as rx
- from reactivex import operators as ops
-
- source = rx.of("Alpha", "Beta", "Gamma", "Delta", "Epsilon")
-
- composed = source.pipe(
- ops.map(lambda s: len(s)),
- ops.filter(lambda i: i >= 5)
- )
- composed.subscribe(lambda value: print("Received {0}".format(value)))
-
-
-Learning ReactiveX
-------------------
-
-Read the `documentation
-`_ to learn
-the principles of ReactiveX and get the complete reference of the available
-operators.
-
-If you need to migrate code from RxPY v1.x or v3.x, read the `migration
-`_ section.
-
-There is also a list of third party documentation available `here
-`_.
-
-
-Community
-----------
-
-Join the conversation on GitHub `Discussions
-`_! if you have any questions or
-suggestions.
-
-Differences from .NET and RxJS
-------------------------------
-
-ReactiveX for Python is a fairly complete implementation of
-`Rx `_ with more than
-`120 operators `_, and
-over `1300 passing unit-tests `_. RxPY
-is mostly a direct port of RxJS, but also borrows a bit from Rx.NET and RxJava in
-terms of threading and blocking operators.
-
-ReactiveX for Python follows `PEP 8 `_, so
-all function and method names are ``snake_cased`` i.e lowercase with words separated by
-underscores as necessary to improve readability.
-
-Thus .NET code such as:
-
-.. code:: c#
-
- var group = source.GroupBy(i => i % 3);
-
-
-need to be written with an ``_`` in Python:
-
-.. code:: python
-
- group = source.pipe(ops.group_by(lambda i: i % 3))
-
-With ReactiveX for Python you should use `named keyword arguments
-`_ instead of positional arguments when an
-operator has multiple optional arguments. RxPY will not try to detect which arguments
-you are giving to the operator (or not).
-
-Development
------------
-
-This project is managed using `Poetry `_. Code is formatted
-using `Black `_, `isort
-`_. Code is statically type checked using `pyright
-`_ and `mypy `_.
-
-If you want to take advantage of the default VSCode integration, then
-first configure Poetry to make its virtual environment in the
-repository:
-
-.. code:: console
-
- poetry config virtualenvs.in-project true
-
-After cloning the repository, activate the tooling:
-
-.. code:: console
-
- poetry install
- poetry run pre-commit install
-
-Run unit tests:
-
-.. code:: console
-
- poetry run pytest
-
-Run code checks (manually):
-
-.. code:: console
-
- poetry run pre-commit run --all-files
-
diff --git a/frogpilot/third_party/reactivex-4.1.0.dist-info/RECORD b/frogpilot/third_party/reactivex-4.1.0.dist-info/RECORD
deleted file mode 100644
index e3432149..00000000
--- a/frogpilot/third_party/reactivex-4.1.0.dist-info/RECORD
+++ /dev/null
@@ -1,225 +0,0 @@
-reactivex-4.1.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
-reactivex-4.1.0.dist-info/METADATA,sha256=rSqPVN97BAmfb3t-XajHhZYG_BsViZQqFQsJaF3jXsc,5697
-reactivex-4.1.0.dist-info/RECORD,,
-reactivex-4.1.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
-reactivex-4.1.0.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
-reactivex-4.1.0.dist-info/licenses/LICENSE,sha256=vModcyjYiECG6tvSqOZvENUzjfLXk0FeVtBS9pUy_zE,1112
-reactivex/__init__.py,sha256=xUhiKlZmQgCnlBvx_072mRMdisi_z9HuINSW7wvzgvY,39756
-reactivex/_version.py,sha256=jegJMYuSW90VKiMotVdNAakBvYVPirm-A5oeyJVuhH8,22
-reactivex/abc/__init__.py,sha256=qvD2QcDdd3vL89eEhzBqcRpDnHGHMAOLWYr4TqtJpdM,594
-reactivex/abc/disposable.py,sha256=DIE-c0MwHEmU1IYD_9v8WNisgn47X0VYJGCAIb9anBo,826
-reactivex/abc/observable.py,sha256=Q2UQLzOILTgEWmKa_coUB_YFV7U4Cytr9014mJdTXy0,1334
-reactivex/abc/observer.py,sha256=m8zfu2MR3Iuc0kIOfEVGzQUBIoKrmZGHRFLPd_Orhic,1150
-reactivex/abc/periodicscheduler.py,sha256=dviU37-fPN_rRR9g1OyOxrW5d3zP_AKIfcodoK_NDo8,1344
-reactivex/abc/scheduler.py,sha256=DLWlL5uhFClEE3OKIMraodxqP0qT_9spkC6kCLjowsM,4362
-reactivex/abc/startable.py,sha256=C_L9Q-ooHslSNthYapLwEJgG-KD5cSkOCnDeTa-rw0o,266
-reactivex/abc/subject.py,sha256=1j5jJrvdlYaK4oFeObjpD1M_gKkZBAPiVnqR9WjOL1w,1907
-reactivex/disposable/__init__.py,sha256=EqOgh0GrHv578Dwq3APgPjB8eYD4vVE1qQJ056LhCrs,658
-reactivex/disposable/booleandisposable.py,sha256=jbH7Qbkcjon7YkvbtWjHOd_h2DJaNg2NFd2tg0cwMh0,482
-reactivex/disposable/compositedisposable.py,sha256=nCinmCt--aCyltvZ5v2Kj8c4JddLx04Ihpd9zwmGhkw,2816
-reactivex/disposable/disposable.py,sha256=qJHh7EWE6k5zcI_p9BwUooFHAD3VkndgExvAbJSpEC0,1132
-reactivex/disposable/multipleassignmentdisposable.py,sha256=Xc-orSPzypavx2bvXFjzbEkF5ov-UPbOhMZpgir8ROY,1419
-reactivex/disposable/refcountdisposable.py,sha256=op7U4ZVPGz26qBCW-UWdV4GDoBqgztUxqcMzlurvEXU,2484
-reactivex/disposable/scheduleddisposable.py,sha256=BYaETQ9LysWPYc21XFiA0TLRbWMu-eDSXXdznSrAE8Y,1149
-reactivex/disposable/serialdisposable.py,sha256=pJsWQyTV2hCDWn8o8bY8MeL0ERYeqGpVV4OaRZHrUj4,1723
-reactivex/disposable/singleassignmentdisposable.py,sha256=pzCdcP-eJQGoXm24phls8QPAcLPS_hkcwOdxErv5O7s,1595
-reactivex/internal/__init__.py,sha256=P2T5xg1uq3zUHf5GRHZd4m2RxUH2Rpd_HPq0k7WC8L4,730
-reactivex/internal/basic.py,sha256=xi-tuNm2dScLEjZBWDqFV9jQ4MwOrRWu91zJI_GD_Rk,673
-reactivex/internal/concurrency.py,sha256=4j9LDDVZvaHgki3mSF01NCv8MOI_xEcPwVrDijfvxtM,691
-reactivex/internal/constants.py,sha256=mCLIqaIhFDTe83oAoPmRNdBHJFINCjvOKqJlgYMGSiI,132
-reactivex/internal/exceptions.py,sha256=eQ6z4Me2hdtRn3NGPGKZtB76TmI1R4nDIpnbxJQBY-s,1003
-reactivex/internal/priorityqueue.py,sha256=GC1OWyGHk3sDPCtBkgyJs5T1iadXLX6i8UtPcg6ibP4,1491
-reactivex/internal/utils.py,sha256=GtHsPfim2syw-q6_bpvoE40jdvAHL4Ag5A2veTvaNtw,1809
-reactivex/notification.py,sha256=O54boOqdr-_Uy4uTjqm9bCKdH1vxsr4Ln9rd4GR8ebA,6298
-reactivex/observable/__init__.py,sha256=rx4_4eShoAgulw7fgEHjP6o7kXMPKlPp0Ht9nvD9rOY,213
-reactivex/observable/amb.py,sha256=s82aaIXF94z8faaCahpfu7vATnhM2JX1I3QH8Iv4SZw,677
-reactivex/observable/case.py,sha256=nfG5i1j5L9oUJYEx3YlqjoA0IERz3jsdkFCWXAW9jIw,893
-reactivex/observable/catch.py,sha256=iV2TcivGIjHWb2S4lhlX84xkKOHfX7FFyLh-GJCn0YY,2566
-reactivex/observable/combinelatest.py,sha256=BBz05LZiFBov8-SvdpdORhU0VydgYzqPRaET16r8gkU,2223
-reactivex/observable/concat.py,sha256=UvMYssA20vhf-tkNAq9SDGIXWNH2D_Q6lxkLjgAVAZ8,1884
-reactivex/observable/connectableobservable.py,sha256=fh16uLWJSgDTTUtl_sUTWL9hPc-tWtfN4DK8-43oBq8,2756
-reactivex/observable/defer.py,sha256=sfHQm6IlrwwkY3bAkxOWLVvQJGLsLzTUytbN543KvmI,1408
-reactivex/observable/empty.py,sha256=jFELiog8feo5HxCLZ1BdetmRxKynypBUYdXoTh5Y5fg,616
-reactivex/observable/forkjoin.py,sha256=XEhWxx20gbT2ZL9slMT0QGWA45iniCq_egcZiOe4Sio,2189
-reactivex/observable/fromcallback.py,sha256=zZQu9UWOI1CfDzlcyoLae6qeZIFdxkWpu7HiGQw0OxY,1878
-reactivex/observable/fromfuture.py,sha256=tI7-HbCiA0hVoZZfpUPW-bRbX7XcHtRA93FmfX5m3wo,1405
-reactivex/observable/fromiterable.py,sha256=wgVVoYnSUGHNKBU6ZT7QAtjksQpDUvKcCWCO1NU6Uk0,1652
-reactivex/observable/generate.py,sha256=RWYxDukbOAtfFC5xPhjuH1OuDgw5x0frKW7uTRjdXp8,1647
-reactivex/observable/generatewithrelativetime.py,sha256=2L9pwfY2sx2T23B6P4ApV042UaS2UXFbc3a3UrGo3jI,2711
-reactivex/observable/groupedobservable.py,sha256=aaySQ7vx5bHwXsHZxpaCJjI5Y87baL07_PnZRjP7jQE,1300
-reactivex/observable/ifthen.py,sha256=fOoKAg9Rw7t6E83uM3aRoK8iEbLEwexab5mSBtpRHJg,1667
-reactivex/observable/interval.py,sha256=nE4p-DD1DjThkVYJ9SJWWGbWrNFkUV3DzVffk_A1L8I,271
-reactivex/observable/marbles.py,sha256=YLb9gsGWfynMtCO_9tmICUWlBulRTeY1erNvTUeMChM,9254
-reactivex/observable/merge.py,sha256=EOZONxkkgq20oB9Q1_lPch0BXwR3KU6FpG8FDnqZ-7M,284
-reactivex/observable/never.py,sha256=HmnZPq2b3bcJyKW2iZLeEAhH-gWxAZjcpeqirVpxFaE,605
-reactivex/observable/observable.py,sha256=3_BiiaSizmfaGb-nbICQAhf6MlH4hynM_5l63gwZZcM,11482
-reactivex/observable/onerrorresumenext.py,sha256=U095TAz5LQ_bi9k_Gsf0nwi6zlkNAeAFz6wpgO-Eh4I,2222
-reactivex/observable/range.py,sha256=fCpgUh8_R3UNBeuvy_sQPLvEui_qSASWEbXk-uZZqm4,2151
-reactivex/observable/repeat.py,sha256=gts_BxFEobJxr4RvB37g6iX9R4Vei0dYOogq1e7NmZk,890
-reactivex/observable/returnvalue.py,sha256=bmyU60ZBPgj_jB3II7CoYXitcahDOs8Nprgm579GfUI,1933
-reactivex/observable/start.py,sha256=PNbLPq_D5oV7T5TMIayBRCcYy9oalmBu5ea7noiB-4g,1091
-reactivex/observable/startasync.py,sha256=BOu75YH2oa0I23bsz89fVxtc7pZEqzgxIQQX3RpVZ_4,409
-reactivex/observable/throw.py,sha256=T8ccsu_ikGFk8incIrKFEng_TXojlPCdY_UiXEYzhn8,754
-reactivex/observable/timer.py,sha256=MkxxL3feuzG3kqpVHcoYXvTjfbzlliX3jVcIlPPy9Eg,4498
-reactivex/observable/toasync.py,sha256=AzUHl7-CFBTWsjWr15eIwNReZMMyXTnfJTDlJIzPwBw,1694
-reactivex/observable/using.py,sha256=Y3ZbxO_dS9SGaI6zwjVXFOZmtdtQ1FHadamoE03tTjg,1689
-reactivex/observable/withlatestfrom.py,sha256=wIs-YV8AD-dA60mtLLnRnIGUDF7IaVrRSnL3npD8dEU,1987
-reactivex/observable/zip.py,sha256=DHR_4WGQ8HTdZwvoKL-ykdTYArEvYA-PvYdzKSPEmKA,2828
-reactivex/observer/__init__.py,sha256=kzNSuynaSXVopszVbAHemaMi8QckLukg3HapjFd5qTg,268
-reactivex/observer/autodetachobserver.py,sha256=AtokqzYC9YoWmf4krJw3KoZU3o-l9QOG3BZokaSeuVs,1710
-reactivex/observer/observeonobserver.py,sha256=4VXsFvS9XLySlllx92G6XRbzXoymbDuge7wfGJUdrmM,494
-reactivex/observer/observer.py,sha256=LfG-WtWE9rReIxZcjsDGInbdOG8k6rLy4Jt9FzWExZA,3520
-reactivex/observer/scheduledobserver.py,sha256=EsaN8BP9ZaPbCzhMJbVjWxcoGk0-WHX3Z3MJrNrcd8k,2189
-reactivex/operators/__init__.py,sha256=25nkSxdpoFYqp25XakOGXYuHaHSRS1r_5drUm42vfnI,132458
-reactivex/operators/_all.py,sha256=Q_E4tAY6CNNqRxJAd-bkupKEwhj29CSR9VvFCjj-DTE,497
-reactivex/operators/_amb.py,sha256=ivtjF1nHWykfYuwRBVEj1RgsSueUftK6inmQ9zVIxsI,3076
-reactivex/operators/_asobservable.py,sha256=7XeRHYCbf0nRywnRBKOa6FeJZoUPpOdYas04IUiKH-8,821
-reactivex/operators/_average.py,sha256=E3WHAmQsa45QFbrkNrp2aW-hQJJdNXWe8ivOJ4Wkn1Y,1685
-reactivex/operators/_buffer.py,sha256=aoBaRCbQXZgaFSmO1Re6tMm4_TCLP0Lya5UKeEZdqXQ,2206
-reactivex/operators/_bufferwithtime.py,sha256=8IljnQa_nmm6_2GUbyKI3MEpHH5KbqTYix5dmiB3hGU,619
-reactivex/operators/_bufferwithtimeorcount.py,sha256=GvagRPU_gnM4xJ_HOdZ7XYmMf4FNGv0vCsP-nqWbNNc,557
-reactivex/operators/_catch.py,sha256=9ffgARW8H51S7dNB-ZThc28BNVZQXkfjQvVPqZ-sMco,2575
-reactivex/operators/_combinelatest.py,sha256=45UDZGrQtm0PX2gmaLexKNWWoVx01fR4MCWWtxnSmYk,798
-reactivex/operators/_concat.py,sha256=8ziLOjVcxs96Vc4msyP_11U7lYi7F9X1F6whXSYAHK0,695
-reactivex/operators/_contains.py,sha256=j3Iva5NadCLrzp0lu12OlRZZiNoVAxBWG9HTG5kGuy8,581
-reactivex/operators/_count.py,sha256=dwsii7oxDq2HRdSq7MOHzMHOgbjiBPHu3ZoLR2FCmuA,558
-reactivex/operators/_debounce.py,sha256=sNWOLGZmIunYjKfslzwNQqDqSO7WM1m8b6qATJ2A07w,5562
-reactivex/operators/_defaultifempty.py,sha256=Ld-4sxUO-yHHYQRHIMy4aCWasy1rDZJNV05Szjokk6k,1456
-reactivex/operators/_delay.py,sha256=uzyQywJAv5tiQ5DKknu6ESecW117eaXGIzUwf370JGI,4722
-reactivex/operators/_delaysubscription.py,sha256=xLSAFuGMtHQdWjw8S1h2o46255nkbd0chDuHSZw3sxA,994
-reactivex/operators/_delaywithmapper.py,sha256=ZoOjh8-ehIZKjN7lLmQ-7zVTf7US-CBusuYIsZuDnCQ,3511
-reactivex/operators/_dematerialize.py,sha256=Ai4yXRnbqs5qor3fy5PyUJkx9i-L09PuK5Uv9c20t1s,1091
-reactivex/operators/_distinct.py,sha256=q_Rb9uJz5gAf3QeNe2bg8U71T4My3QrnvTPZArBGcbg,2516
-reactivex/operators/_distinctuntilchanged.py,sha256=-SnZF4qheq3FwpFyk5ZZWCB6z6wT37xIwcrHZe6t5kc,2869
-reactivex/operators/_do.py,sha256=NaRy0fcHdzVPRLqLE1VSPLbZyYwWip3cPiiJTXnrVJI,10795
-reactivex/operators/_dowhile.py,sha256=uXEnZLz_tfSW8rUXVnki5UlP0tvRbZ3TPEC7q3QJ61s,817
-reactivex/operators/_elementatordefault.py,sha256=J1Nb2WpOs2iIxOnP84TdVuHPl4n9ySM_lVmAfdqR69E,1546
-reactivex/operators/_exclusive.py,sha256=Q5p_Duez_i2xxy0BkTyO_VdxVFBbWSAbhgWodgiY47k,2479
-reactivex/operators/_expand.py,sha256=q9aUkvffLhzam_O0-xhgGprvkhfmZNaXbtU2c4SCqTo,3078
-reactivex/operators/_filter.py,sha256=x4GU4BH0wRkh_xnA5Js3JaDQ_wZ7Krp0w99TnC-xir8,2807
-reactivex/operators/_finallyaction.py,sha256=GCF-sBwTg4aqh7cnyRbN4JIAnHZZQEPjWDXQdGsR9cs,1302
-reactivex/operators/_find.py,sha256=GE1xEG06I8ojugiTy6qCq4od15EWSPmGMCTz54Dawy0,1433
-reactivex/operators/_first.py,sha256=cvcl5Mx3btw9m48J3J5ubTobOTI0YYLxeFE6tSRKR74,1176
-reactivex/operators/_firstordefault.py,sha256=1ZMxi0mr5uuyUAz6XrA5Y3G94O18A1HnIjeFTWxiT-s,2557
-reactivex/operators/_flatmap.py,sha256=R9MnotDaMPTie7FK-fZcMkmke2KF42AZl6y5pAa410U,4279
-reactivex/operators/_forkjoin.py,sha256=qkQKndnP2DcY60nAp6Cjq_tJuyS9Z3IGwmGebD5Imk4,843
-reactivex/operators/_groupby.py,sha256=FLN5ykLpHalUQbk2H5ENm33tgoRYyDS6kBcQ0_-ZCM0,852
-reactivex/operators/_groupbyuntil.py,sha256=mO-xW1vBlBPMhw1fjOA7K3RRon9MIsrJy8jbRWw9jzY,6308
-reactivex/operators/_groupjoin.py,sha256=9N-WQsfOwFT0oqvcM5KyKj6wH6rY-5wLe6vo-8MDArY,5886
-reactivex/operators/_ignoreelements.py,sha256=6szHhX7hjEf5UBRBGQslyUHm03XeT5d41A_cWKKkoUs,929
-reactivex/operators/_isempty.py,sha256=jeVsO7KUJ3TrKns3616gGpGP7k8yOvj4pr0LZZQP9jc,534
-reactivex/operators/_join.py,sha256=7KZAECRvb339vlZYfAatrFLGGYSjYgN7adZvaSPs41E,4592
-reactivex/operators/_last.py,sha256=0DV-uHOH_0I6qR6shWISXi1-sb8Rk_5GrPUN28_-RA8,1138
-reactivex/operators/_lastordefault.py,sha256=DsgpzcoFqZ-dV4BafyR_fU5Gu6dJebuQd4Bhw-3aols,1968
-reactivex/operators/_map.py,sha256=lmBF2kPEJoNKqaxbUkF4J90s5rgCHWrv8mnDcql-lHk,2129
-reactivex/operators/_materialize.py,sha256=Su1xY0GrbjfuXi2WvDBSq3Far1M3ZpoXsmSfXN1GwKg,1455
-reactivex/operators/_max.py,sha256=ZgzlqX9zBqllmzVSbxjabRMMqk9s1uT_3IyUhEASY54,995
-reactivex/operators/_maxby.py,sha256=-0gH-YJuz-p8cQWXJOqAKn1s50vnvSbi5r32tJ92lHg,1018
-reactivex/operators/_merge.py,sha256=04vqPKtnLxpB9vUcHABsR7YWCGW4mycPWVtPp1Hu0r0,5185
-reactivex/operators/_min.py,sha256=jEd9YXx9_TATN33g17PSwEErLDpsxhMTb7sfoIQUOCQ,1173
-reactivex/operators/_minby.py,sha256=zAV1m2yG59hzJCGdsf_fayk0Inmw1pBm8e6fg-_rZhQ,2501
-reactivex/operators/_multicast.py,sha256=Spuwm-rwyaSgL5C-HPijjGmVavttQnHznovVCX6uqfg,2939
-reactivex/operators/_observeon.py,sha256=j-mneWR9fXq_XmOqdLvC0rn0FNhf8cbjsh09mFqT_4c,1212
-reactivex/operators/_onerrorresumenext.py,sha256=tGol6SbZlrQge2Je0HUfSvdN6rURAXe03K3EoY6Yr5U,419
-reactivex/operators/_pairwise.py,sha256=8Nx6OaDjTAxu-nK_nziObjt1-sQskH1EKFsiTBw91zI,1670
-reactivex/operators/_partition.py,sha256=O4mANzYnvFZZed6x92GK_oMV10zCkz6lTUD-5U7hjzk,3022
-reactivex/operators/_pluck.py,sha256=l6uteIyv2tVPxPDb36STgoumAIH9YmJNAcX3vRFyG0U,1208
-reactivex/operators/_publish.py,sha256=PFHQQInn6dYuKeSFn8oJX6NfsRQde5xNva1S0BGdbnU,2240
-reactivex/operators/_publishvalue.py,sha256=SVoAvEwF0re7U9oYNFVTb5NewpRTZY4Q-VvBFK5Un14,901
-reactivex/operators/_reduce.py,sha256=sKpDIPFr7pw8Gi1cz3HZbidRnc3-hKgb1Zq2FkcOQoQ,1570
-reactivex/operators/_repeat.py,sha256=ru1fh1IL-_l5AP2KawhsIK6nrPvnZrjxsX-cARWi8B4,1069
-reactivex/operators/_replay.py,sha256=pz4yYMQ4pRuEkZoXv4j0SBmTNNQ8xXBrTsvV2rI_b7E,2430
-reactivex/operators/_retry.py,sha256=xNdJe6OAhHgouYlso82bW-Ce-1scAXfa5GddfYVYilQ,1083
-reactivex/operators/_sample.py,sha256=t7V1V9I8giJjSgEKPTE-8sxX-4zUJipItEfPz0kAAEw,2123
-reactivex/operators/_scan.py,sha256=BpzkQl3FBQMWPWkc5gcc52pWLWnCiYiHErgq13od7ZA,1732
-reactivex/operators/_sequenceequal.py,sha256=I11OjMgjMUZOrqmCPYz1_5kDlpVxKnnou2YuhS2nOCQ,4148
-reactivex/operators/_single.py,sha256=oCDg0DPsGHWgWhVe4E1GHAOI0CGLRpwK4i3nxzo0iuo,1139
-reactivex/operators/_singleordefault.py,sha256=qWKXGNIuMdZkmZag-LNJSbmYtdxLy3sN3diKHFHCooM,2840
-reactivex/operators/_skip.py,sha256=rMZBIjjtg2eNDIWgXCMzPnTLXqC9d7vu1cZhgkHopBo,1332
-reactivex/operators/_skiplast.py,sha256=95Nq8Onfafjqyd_-0kjH_elFEYm9YDumTBfjsGoCPRs,1590
-reactivex/operators/_skiplastwithtime.py,sha256=MqEMeEEwI1wCfCK1eWYsqYntmYpoWWh8gozymfXaq4M,2352
-reactivex/operators/_skipuntil.py,sha256=UUYfpCHnXU4Eg7tS-ljRDIH_Zjzf79ckYBkb6ihZw68,2133
-reactivex/operators/_skipuntilwithtime.py,sha256=O3lC8eDkjUuQcTUsHJBNEjs6HXXCkkITEbXGuD6XSlc,2252
-reactivex/operators/_skipwhile.py,sha256=JDPrpNX9HS62GVyT422pznnXfpkO06xbR3JFll-tWfc,2205
-reactivex/operators/_skipwithtime.py,sha256=JtRB0BsPDDF_P32zJ0K1e6iF0H5f6ZgS7Ao2ULNrL9c,2220
-reactivex/operators/_slice.py,sha256=w4TutKAOt8IzNGANLv7RqDro5gOIdMaTCcLgVLV0gBs,2311
-reactivex/operators/_some.py,sha256=8RSPk3mX4hm8geo49CTu8UGVkQqMHChncr1y2RXIGnA,1656
-reactivex/operators/_startswith.py,sha256=wbhJz2cHLwPVnd9lsJAcdcgn0srSZks6pxIHElvE_Hc,689
-reactivex/operators/_subscribeon.py,sha256=WPL3l6F79pYH-__8oyy0hoCtI6JCQ6J6QBjFmE-rM5Q,1744
-reactivex/operators/_sum.py,sha256=xtgLpIgK7qh5tjh0WpefDns_LzpAGAX9sWRIOd_J3KE,511
-reactivex/operators/_switchlatest.py,sha256=FYEPWYipGrQb4sgNVRHdYyM7B3ybc5Hm_6su471TRr8,2908
-reactivex/operators/_take.py,sha256=xZZykFL90MOz7Uy7DtYHAc26QiRFNbgRNfpMFx5ijuQ,1437
-reactivex/operators/_takelast.py,sha256=WwkJKxdIg73qNF-mxQnUx6aTO1vYxn1p_q_9_11DPfI,1619
-reactivex/operators/_takelastbuffer.py,sha256=vGrQdVPblAHzbNVPMvzIlRkMafD5yoN30WJy5L5cwc0,1695
-reactivex/operators/_takelastwithtime.py,sha256=foNqsRSSrZYp2ixrenCR-xzED2HaYKNFxtecUGx6pb4,2338
-reactivex/operators/_takeuntil.py,sha256=LHq_Lr1l3nk_YBCPyP0vsemhSKRZpXWLumhPdbPpDjI,1515
-reactivex/operators/_takeuntilwithtime.py,sha256=094FkE1vH2fBkQAEC-fN9awc4gs1bmMU1Pu12L2raQk,1706
-reactivex/operators/_takewhile.py,sha256=ZmPTbx_ROPF9JS_fqcG3oqs2SsOoCiebOccFwNbZ-cM,3612
-reactivex/operators/_takewithtime.py,sha256=jajLiqRc4sIPsRIaqjcir5yCB3zSnrogoaMXMgDKF1s,1850
-reactivex/operators/_throttlefirst.py,sha256=HAHaIK1FQ8fSOkwsenemsDh4AdlVk8Yrd0bUYBlhWx8,1887
-reactivex/operators/_timeinterval.py,sha256=q7FqU8hWO9_-_FwckgoumeLYpCx4upeq_jsF0o_RTZ0,1553
-reactivex/operators/_timeout.py,sha256=6FfEwqmfodvqtXvBxEXCmpdTvsYCiQ_AnOOJAwxlqOE,3248
-reactivex/operators/_timeoutwithmapper.py,sha256=VjM_DoqylWBY7R6_tY2WtoLymml-yIqgfDCV28Pd23Q,4408
-reactivex/operators/_timestamp.py,sha256=P1_NUuEJiCSe9s0whhdfEyFvGdIhNB6aKNxkvkhd9FI,1347
-reactivex/operators/_todict.py,sha256=YIXOJLMW4NKXJOxkfXMyQdfT9slM8s8DGtdbXl_QlzY,1972
-reactivex/operators/_tofuture.py,sha256=iEcdUPqaGDweTvciRkTEqSR_Xk3zjSrNfC8ukOnn-Hc,2061
-reactivex/operators/_toiterable.py,sha256=6dw1ph6-cihnyEsnHULlyMzCx1wYkASx2alKDYZPvwU,1170
-reactivex/operators/_tomarbles.py,sha256=kmQmhK2qJtIRcWxIKY09vFXAcms_9XSd9lsXpMFf4ME,2314
-reactivex/operators/_toset.py,sha256=2XIcr5xUURaf6qlvy7590znLs2wyJuLTf5rm2alpd9s,996
-reactivex/operators/_whiledo.py,sha256=-3t6FTMTIGjZHE2vfdRsWzclzotYsQjI_hYV-eOLZfk,1071
-reactivex/operators/_window.py,sha256=dpBG0GK_nW8_fbC19ceCMl6A_Io9EkrCWtIClvrqVvM,5176
-reactivex/operators/_windowwithcount.py,sha256=9ueOFkZ3JtJ16Tn1Bsu3kqzgKoD1jV-h3SDCLqLMcyg,2698
-reactivex/operators/_windowwithtime.py,sha256=RFuyNs0pWqC7_FgCI9pJUp8PuLC2Q9un-uy-K3G34Rg,3859
-reactivex/operators/_windowwithtimeorcount.py,sha256=SUx5Id39IqjGSeAYa_qD5yrb_HOPUJOdNWHbvWKdtEk,3113
-reactivex/operators/_withlatestfrom.py,sha256=v7fwpdHcPBxP8Fvmf-4_XVo_K0rfhcC2i2hx8GQNmjE,898
-reactivex/operators/_zip.py,sha256=h0ay0I7OtWFyJpT5-8s_FG6YbWK0vJdS7xYMfGQLSu4,2383
-reactivex/operators/connectable/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
-reactivex/operators/connectable/_refcount.py,sha256=f8d3B0lUrROMjX4sm7tvaCsyaBnRQm6S1HNmedXm_jg,1419
-reactivex/pipe.py,sha256=yO-T0-cNJwUR5hgPuu8zoOrRaVlplwK5WHw3I7wP5Fw,4207
-reactivex/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
-reactivex/run.py,sha256=LKeIqZG8yMtu3vLBCtxWKa5RUYCTzZnuKdnQzDNciFM,1709
-reactivex/scheduler/__init__.py,sha256=N0qMXVK3FlQJ1h25kVr9dLshBMWayEeWcNcfFen4bnY,856
-reactivex/scheduler/catchscheduler.py,sha256=KoYfPlLyhQvNRC3ue9E74tAAJuy0GlZNgu04Q3RMnB0,6130
-reactivex/scheduler/currentthreadscheduler.py,sha256=cJv0UjzT92FSe3sWxRzi4b-irV-2_xsVhjjLV4lxoGA,2657
-reactivex/scheduler/eventloop/__init__.py,sha256=wyCjpar6vxFyPvv_FM0N56FTinxnTqGs9MMBWY_2Gsc,468
-reactivex/scheduler/eventloop/asyncioscheduler.py,sha256=rfG7D34YxbhuMtT4OoZlEjsDE6Sqo78IEG63pvDCD6k,3790
-reactivex/scheduler/eventloop/asynciothreadsafescheduler.py,sha256=WPWLsxnzRviwbdDfg5HnXISExXk0ZjouOVEMY4mQ36s,5026
-reactivex/scheduler/eventloop/eventletscheduler.py,sha256=5ENX1cEw1KPDXz4YmrWcfkKFs5VK9SI_xiBPa-Kmu1E,3660
-reactivex/scheduler/eventloop/geventscheduler.py,sha256=aZDZY3eUWE3rtBBNaVFBz-jpAX7TdqPh3l6p94MaWGo,3700
-reactivex/scheduler/eventloop/ioloopscheduler.py,sha256=FSu0ROfJ3RYBp6VeZlji23UjB7P0qlQUQl4hyS4GXsk,3941
-reactivex/scheduler/eventloop/twistedscheduler.py,sha256=ZDhU3cGwVzpnFZHtD7VSbGhZmv3AjwBWjQGHNOsoz74,3373
-reactivex/scheduler/eventloopscheduler.py,sha256=bWz0mIWu76lqnRV-zoZ9LLPXQJF-otm05vFHbGxJiok,7421
-reactivex/scheduler/historicalscheduler.py,sha256=7ytM1qNWBBSnxWMoiOUCtSnot8AfTKEz0f_nBzGjacM,620
-reactivex/scheduler/immediatescheduler.py,sha256=0ZZgDRcA3U89X-IP0vED711HHla8lxfuGAFFSuM4q3o,3121
-reactivex/scheduler/mainloop/__init__.py,sha256=Lwy7VFLsygEMpVUQ0wkOsdFe-slCHK1gedydyOA2Dwc,325
-reactivex/scheduler/mainloop/gtkscheduler.py,sha256=KLLa1B1SHh35HBHe5UZpjjk1D9KlBVAktyohlnnVbjw,4399
-reactivex/scheduler/mainloop/pygamescheduler.py,sha256=yAglz1rCFRUtKiI4Dfc32bKyls7-US7ELrTJHylVMDM,3514
-reactivex/scheduler/mainloop/qtscheduler.py,sha256=rI7MBuKtnyCAVoyLj9zUc1kWT4zWVd-mfpdUIYFcJ2A,4476
-reactivex/scheduler/mainloop/tkinterscheduler.py,sha256=PSmKn06nEo5HW4jNdqKTktZiSDM6z5JFBxcf3qw0vso,2987
-reactivex/scheduler/mainloop/wxscheduler.py,sha256=Z1SjND81O3DNgqNg7C-6xRCcvRVIDwFyM_YnmyMGJK8,5522
-reactivex/scheduler/newthreadscheduler.py,sha256=K7l0ovBthzpG3uxn3t1MQRHqlbRVyhI0Boko8-d4X-0,4199
-reactivex/scheduler/periodicscheduler.py,sha256=m4ghE-odJrhiJASy6U1yviVYu18olBvjDNgnTX2LMpk,1878
-reactivex/scheduler/scheduleditem.py,sha256=SnWep-Zk3o2EvgfcaHrJeAJqmSdt3LK3KSpNb8L6k80,1433
-reactivex/scheduler/scheduler.py,sha256=L5nIuOC-a3StedJErx4iL3FY9ayh3QntEwlZpA3h4bs,5400
-reactivex/scheduler/threadpoolscheduler.py,sha256=yAE_hgTlcvV4GIsgVwc5XxZmg13-1Hf8aWaoz3eKYXo,1254
-reactivex/scheduler/timeoutscheduler.py,sha256=IJUzB0yfCDF_aFRYONT4kifZx0wTGA0KMacT_TKlUt0,3516
-reactivex/scheduler/trampoline.py,sha256=3sp1sV1NgNJK78enC1jqbReSj7jf_j9GNfibM-SfKcs,1847
-reactivex/scheduler/trampolinescheduler.py,sha256=mcU14rsCf03kIDbsF0weWH-BJnGul4WoNUYx4yIh9f0,3788
-reactivex/scheduler/virtualtimescheduler.py,sha256=JbT8K7ewVzubQ3odxfpCI2FNRl3cIHfkv_-cZT10OTM,7693
-reactivex/subject/__init__.py,sha256=ICiGMqswVSo5wqaZzLF04Tz66pY_zAcyJRff2-XoT7A,229
-reactivex/subject/asyncsubject.py,sha256=4y7fIOSQmS7lhaEzg-3O7BOPJjRghV49OItPuhefK9g,2569
-reactivex/subject/behaviorsubject.py,sha256=1WY5d6ntgkUCnn3ZZ3ifvP4yoGUDH08ODVJN7dHRUQI,1997
-reactivex/subject/innersubscription.py,sha256=sukkq8AQ6WkJfTsLTYh51sUwB3BbR4DsJ9M2Vkyc0qg,703
-reactivex/subject/replaysubject.py,sha256=07yT1Aq8G5cSKvlsvE4OljKpMCDb3Myh3tCAqnDnMp4,4610
-reactivex/subject/subject.py,sha256=Q8nj-PHurq2QaNH_tbtJIZ5lRDkNUoen0NDDZjpF3bE,3148
-reactivex/testing/__init__.py,sha256=hPkeUUVcp0MiwNTn1jii5UZXkJaz51S44GV7U9SA86E,356
-reactivex/testing/coldobservable.py,sha256=IfXBEpHqD5XdfBsxg90UlVzIIiarew8NbyB6P9e294U,1943
-reactivex/testing/hotobservable.py,sha256=N89Kj7eS92b_xrSa8ACI9BiMrj-0Ze_gA4dv0GEwKFs,2081
-reactivex/testing/marbles.py,sha256=DQ9viMf-aqrpUKjLxE6exat9YUjmRBwokWDTK6vVxNI,5765
-reactivex/testing/mockdisposable.py,sha256=GZxvMeGSwwDI-N1C856XwGPkPdN0CNZkf7C80GQ3fPQ,436
-reactivex/testing/mockobserver.py,sha256=03y2EOIN5xdZYtEDWlvGQ-ZrTLUdFw3MZfjCDsmnKVs,780
-reactivex/testing/reactivetest.py,sha256=xgwA9RMu7lhyJVhh6c-QqGX5-CKJ8VWy-sipt-GlBH4,2119
-reactivex/testing/recorded.py,sha256=8eL6IOZuBb12rp_UAlWiU2YiiKOUkd92PQzBi5pXG8A,1095
-reactivex/testing/subscription.py,sha256=VCVc6kZJmR8c89rkaAECmFth8NOkz4tUs45PjnnUNnk,701
-reactivex/testing/testscheduler.py,sha256=CYlbxcFzdfdx2XVXkIVdWTVB9lSC5DoaoxfSRQw63HI,5881
-reactivex/typing.py,sha256=IRRs3TNKClakoVm26NHRei0ZtEFbKPC27AaKdp7ZzNI,1367
diff --git a/frogpilot/third_party/reactivex-4.1.0.dist-info/licenses/LICENSE b/frogpilot/third_party/reactivex-4.1.0.dist-info/licenses/LICENSE
deleted file mode 100644
index 5b99afc2..00000000
--- a/frogpilot/third_party/reactivex-4.1.0.dist-info/licenses/LICENSE
+++ /dev/null
@@ -1,20 +0,0 @@
-# The MIT License
-
-Copyright 2013-2022, Dag Brattli, Microsoft Corp., and Contributors.
-
-Permission is hereby granted, free of charge, to any person obtaining a copy of
-this software and associated documentation files (the "Software"), to deal in
-the Software without restriction, including without limitation the rights to
-use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
-the Software, and to permit persons to whom the Software is furnished to do so,
-subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
-FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
-COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
-IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
-CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/frogpilot/third_party/timezonefinder-8.1.0.dist-info/METADATA b/frogpilot/third_party/timezonefinder-8.1.0.dist-info/METADATA
new file mode 100644
index 00000000..2695a4cd
--- /dev/null
+++ b/frogpilot/third_party/timezonefinder-8.1.0.dist-info/METADATA
@@ -0,0 +1,3 @@
+Metadata-Version: 2.1
+Name: timezonefinder
+Version: 8.1.0
diff --git a/frogpilot/third_party/timezonefinder-8.1.0.dist-info/top_level.txt b/frogpilot/third_party/timezonefinder-8.1.0.dist-info/top_level.txt
new file mode 100644
index 00000000..4c2251e4
--- /dev/null
+++ b/frogpilot/third_party/timezonefinder-8.1.0.dist-info/top_level.txt
@@ -0,0 +1 @@
+timezonefinder
diff --git a/frogpilot/third_party/timezonefinder/__init__.py b/frogpilot/third_party/timezonefinder/__init__.py
new file mode 100644
index 00000000..637c9c6c
--- /dev/null
+++ b/frogpilot/third_party/timezonefinder/__init__.py
@@ -0,0 +1,25 @@
+from timezonefinder.timezonefinder import (
+ TimezoneFinder,
+ TimezoneFinderL,
+)
+
+# Import module-level functions
+from timezonefinder.global_functions import (
+ timezone_at,
+ timezone_at_land,
+ unique_timezone_at,
+ certain_timezone_at,
+ get_geometry,
+)
+
+# https://docs.python.org/3/tutorial/modules.html#importing-from-a-package
+# determines which objects will be imported with "import *"
+__all__ = (
+ "TimezoneFinder",
+ "TimezoneFinderL",
+ "timezone_at",
+ "timezone_at_land",
+ "unique_timezone_at",
+ "certain_timezone_at",
+ "get_geometry",
+)
diff --git a/frogpilot/third_party/timezonefinder/_numba_replacements.py b/frogpilot/third_party/timezonefinder/_numba_replacements.py
new file mode 100644
index 00000000..ee42c8c1
--- /dev/null
+++ b/frogpilot/third_party/timezonefinder/_numba_replacements.py
@@ -0,0 +1,52 @@
+"""'transparent' numba functionality replacements
+
+njit decorator
+data types
+
+dtype_2int_tuple = typeof((1, 1))
+@njit(b1(i4, i4, i4[:, :]), cache=True)
+@njit(dtype_2int_tuple(f8, f8), cache=True)
+"""
+
+
+# decorator
+def njit(*args, **kwargs):
+ def wrapper(f):
+ return f
+
+ return wrapper
+
+
+class SubscriptAndCallable:
+ def __init__(self, *args, **kwargs):
+ pass
+
+ def __class_getitem__(cls, item):
+ return None
+
+ def __call__(self, arg):
+ # for example int64(1) must work
+ return arg
+
+
+# DTYPES
+
+
+class f8(SubscriptAndCallable):
+ pass
+
+
+class i8(SubscriptAndCallable):
+ pass
+
+
+class i4(SubscriptAndCallable):
+ pass
+
+
+class boolean(SubscriptAndCallable):
+ pass
+
+
+class Array(SubscriptAndCallable):
+ pass
diff --git a/frogpilot/third_party/timezonefinder/build.py b/frogpilot/third_party/timezonefinder/build.py
new file mode 100644
index 00000000..756ea29b
--- /dev/null
+++ b/frogpilot/third_party/timezonefinder/build.py
@@ -0,0 +1,49 @@
+"""optionally builds inside polygon algorithm C extension
+
+Resources:
+https://github.com/FirefoxMetzger/mini-extension
+https://stackoverflow.com/questions/60073711/how-to-build-c-extensions-via-poetry
+https://github.com/libmbd/libmbd/blob/master/build.py
+"""
+
+import pathlib
+import re
+from typing import Optional
+import warnings
+
+import cffi
+
+EXTENSION_NAME = "inside_polygon_ext"
+H_FILE_NAME = "inside_polygon_int.h"
+C_FILE_NAME = "inside_polygon_int.c"
+EXTENSION_PATH = pathlib.Path("timezonefinder") / "inside_poly_extension"
+h_file_path = EXTENSION_PATH / H_FILE_NAME
+c_file_path = EXTENSION_PATH / C_FILE_NAME
+
+ffibuilder: Optional[cffi.FFI] = None
+try:
+ ffibuilder = cffi.FFI()
+except Exception as exc:
+ # Clang extension should be fully optional
+ warnings.warn(
+ f"C lang extension cannot be build, since cffi failed with this error: {exc}"
+ )
+
+if ffibuilder is not None:
+ ffibuilder.set_source(
+ "timezonefinder." + EXTENSION_NAME,
+ source='#include "inside_polygon_int.h"',
+ sources=[str(c_file_path)],
+ include_dirs=[str(EXTENSION_PATH)],
+ )
+ with open(h_file_path) as h_file:
+ # cffi does not like our preprocessor directives, so we remove them
+ lns = h_file.read().splitlines()
+ flt = filter(lambda ln: not re.match(r" *#", ln), lns)
+
+ ffibuilder.cdef("\n".join(flt))
+
+
+if __name__ == "__main__":
+ if ffibuilder:
+ ffibuilder.compile(verbose=True)
diff --git a/frogpilot/third_party/timezonefinder/command_line.py b/frogpilot/third_party/timezonefinder/command_line.py
new file mode 100644
index 00000000..e7fe1ba0
--- /dev/null
+++ b/frogpilot/third_party/timezonefinder/command_line.py
@@ -0,0 +1,122 @@
+import argparse
+import contextlib
+import os
+import sys
+import tempfile
+from typing import Callable, Generator
+
+from timezonefinder import (
+ TimezoneFinderL,
+ timezone_at,
+ certain_timezone_at,
+ timezone_at_land,
+)
+
+
+@contextlib.contextmanager
+def redirect_stdout_to_temp_file() -> Generator[str, None, None]:
+ """
+ Context manager that redirects stdout to a temporary file for the duration of the context.
+ The temporary file is created but not deleted when the context exits.
+ Returns the path to the temporary file.
+ """
+ # Save the original stdout
+ original_stdout = sys.stdout
+
+ # Create a temporary file that will NOT be automatically deleted
+ temp_fd, temp_path = tempfile.mkstemp(text=True)
+ temp_file = os.fdopen(temp_fd, "w")
+
+ try:
+ # Redirect stdout to the temporary file
+ sys.stdout = temp_file
+ yield temp_path
+ finally:
+ # Restore the original stdout and close the file
+ sys.stdout = original_stdout
+ temp_file.close()
+
+
+def get_timezone_function(function_id: int) -> Callable:
+ """
+ Get the appropriate timezone function based on the function ID.
+ Uses global functions when available, otherwise creates instances as needed.
+ """
+ # Use global functions for TimezoneFinder methods
+ if function_id == 0:
+ return timezone_at
+ elif function_id == 1:
+ return certain_timezone_at
+ elif function_id == 5:
+ return timezone_at_land
+
+ # For TimezoneFinderL methods, still create an instance
+ tf_instance = TimezoneFinderL()
+ functions = {
+ 3: tf_instance.timezone_at,
+ 4: tf_instance.timezone_at_land,
+ }
+ return functions[function_id]
+
+
+def main() -> None:
+ parser = argparse.ArgumentParser(description="parse TimezoneFinder parameters")
+ parser.add_argument("lng", type=float, help="longitude to be queried")
+ parser.add_argument("lat", type=float, help="latitude to be queried")
+ parser.add_argument("-v", action="store_true", help="verbosity flag")
+ parser.add_argument(
+ "-f",
+ "--function",
+ type=int,
+ choices=[0, 1, 3, 4, 5],
+ default=0,
+ help="function to be called:"
+ "0: TimezoneFinder.timezone_at(), "
+ "1: TimezoneFinder.certain_timezone_at(), "
+ "2: removed, "
+ "3: TimezoneFinderL.timezone_at(), "
+ "4: TimezoneFinderL.timezone_at_land(), "
+ "5: TimezoneFinder.timezone_at_land(), ",
+ )
+ parsed_args = parser.parse_args() # takes input from sys.argv
+ timezone_function = get_timezone_function(parsed_args.function)
+
+ verbose_mode = parsed_args.v
+
+ # Always redirect stdout to a temp file
+ with redirect_stdout_to_temp_file() as temp_file_path:
+ print("\n" + "=" * 60)
+ print("TIMEZONEFINDER LOOKUP DETAILS")
+ print("-" * 60)
+ print(f"Coordinates: {parsed_args.lat:.6f}°, {parsed_args.lng:.6f}° (lat, lng)")
+ print(
+ f"Function {timezone_function.__name__} (function ID: {parsed_args.function})"
+ )
+
+ # Execute the timezone function
+ tz = timezone_function(lng=parsed_args.lng, lat=parsed_args.lat)
+
+ if tz:
+ print(f"Result: Found timezone '{tz}'")
+ else:
+ print("Result: No timezone found at this location")
+ print("=" * 60)
+
+ if verbose_mode:
+ # In verbose mode, print the contents of the temp file
+ try:
+ with open(temp_file_path) as f:
+ captured_output = f.read().strip()
+ if captured_output:
+ print(captured_output)
+ except Exception as e:
+ print(f"Warning: Could not read captured output: {e}")
+ else:
+ # In non-verbose mode, just print the result
+ print(tz if tz else "")
+
+ # Always clean up the temp file
+ try:
+ os.remove(temp_file_path)
+ except Exception:
+ pass
diff --git a/frogpilot/third_party/timezonefinder/configs.py b/frogpilot/third_party/timezonefinder/configs.py
new file mode 100644
index 00000000..7a8f31d5
--- /dev/null
+++ b/frogpilot/third_party/timezonefinder/configs.py
@@ -0,0 +1,89 @@
+import os
+from pathlib import Path
+from typing import Any, Dict, List, Tuple, Union
+
+import numpy as np
+
+# SHORTCUT SETTINGS
+# h3 library
+SHORTCUT_H3_RES: int = 3
+
+OCEAN_TIMEZONE_PREFIX = r"Etc/GMT"
+
+# PATHS
+PACKAGE_DIR = Path(__file__).parent
+DEFAULT_DATA_DIR = PACKAGE_DIR / "data"
+
+
+# i = signed 4byte integer
+NR_BYTES_I = 4
+# IMPORTANT: all values between -180 and 180 degree must fit into the domain of i4!
+# is the same as testing if 360 fits into the domain of I4 (unsigned!)
+MAX_ALLOWED_COORD_VAL = 2 ** (8 * NR_BYTES_I - 1)
+
+# from math import floor,log10
+# DECIMAL_PLACES_SHIFT = floor(log10(MAX_ALLOWED_COORD_VAL/180.0)) # == 7
+DECIMAL_PLACES_SHIFT = 7
+INT2COORD_FACTOR = 10 ** (-DECIMAL_PLACES_SHIFT)
+COORD2INT_FACTOR = 10**DECIMAL_PLACES_SHIFT
+MAX_LNG_VAL = 180.0
+MAX_LAT_VAL = 90.0
+MAX_LNG_VAL_INT = int(MAX_LNG_VAL * COORD2INT_FACTOR)
+MAX_LAT_VAL_INT = int(MAX_LAT_VAL * COORD2INT_FACTOR)
+MAX_INT_VAL = MAX_LNG_VAL_INT
+assert MAX_INT_VAL < MAX_ALLOWED_COORD_VAL
+
+# TYPES
+# used in Numba JIT compiled function signatures in utils_numba.py
+# NOTE: Changes in the global settings might not immediately affect
+# the functions due to caching!
+
+# Type alias for flexibility with integer types (pure int or numpy integer scalars)
+IntegerLike = Union[int, np.integer]
+
+# hexagon id to list of polygon ids
+ShortcutMapping = Dict[int, np.ndarray]
+CoordPairs = List[Tuple[float, float]]
+CoordLists = List[List[float]]
+IntLists = List[List[int]]
+
+
+# zone id storage settings ---------------------------------------------------
+
+_ZONE_ID_DTYPE_ALIASES: Dict[str, "np.dtype[Any]"] = {
+ "uint8": np.dtype(" str:
+ """Normalise user provided dtype keys to canonical form."""
+ return key.lower().strip()
+
+
+def get_zone_id_dtype(name: str) -> "np.dtype[Any]":
+ """Return the configured numpy dtype for storing zone IDs."""
+
+ try:
+ return _ZONE_ID_DTYPE_ALIASES[_normalise_zone_id_dtype_key(name)]
+ except KeyError as exc: # pragma: no cover - defensive, validated on import
+ valid = ", ".join(sorted(_ZONE_ID_DTYPE_ALIASES))
+ raise ValueError(
+ f"Unsupported zone id dtype '{name}'. Choose one of: {valid}"
+ ) from exc
+
+
+def zone_id_dtype_to_string(dtype: np.dtype) -> str:
+ """Return the little-endian numpy dtype string for serialisation."""
+
+ return dtype.newbyteorder("<").str
+
+
+def available_zone_id_dtype_names() -> Tuple[str, ...]:
+ """Return the supported zone id dtype names."""
+
+ return tuple(sorted(_ZONE_ID_DTYPE_ALIASES))
+
+
+DEFAULT_ZONE_ID_DTYPE_NAME = os.getenv("TIMEZONEFINDER_ZONE_ID_DTYPE", "uint8")
+DEFAULT_ZONE_ID_DTYPE = get_zone_id_dtype(DEFAULT_ZONE_ID_DTYPE_NAME)
diff --git a/frogpilot/third_party/timezonefinder/coord_accessors.py b/frogpilot/third_party/timezonefinder/coord_accessors.py
new file mode 100644
index 00000000..a402d538
--- /dev/null
+++ b/frogpilot/third_party/timezonefinder/coord_accessors.py
@@ -0,0 +1,173 @@
+"""
+Coordinate accessors for timezonefinder.
+
+This module provides classes for accessing polygon coordinates
+either directly from file or from preloaded memory.
+"""
+
+from abc import ABC, abstractmethod
+import mmap
+from pathlib import Path
+from typing import Dict
+
+import numpy as np
+
+from timezonefinder import utils
+from timezonefinder.flatbuf.generated.polygons.PolygonCollection import (
+ PolygonCollection,
+)
+from timezonefinder.flatbuf.io.polygons import (
+ get_polygon_collection,
+ read_polygon_array_from_binary,
+)
+
+
+class AbstractCoordAccessor(ABC):
+ """Abstract base class defining the interface for coordinate accessors."""
+
+ @abstractmethod
+ def __init__(self, coordinate_file_path: Path):
+ """
+ Initialize the coordinate accessor.
+
+ Args:
+ coordinate_file_path: Path to the coordinate file
+ """
+ pass
+
+ @abstractmethod
+ def __getitem__(self, idx: int) -> np.ndarray:
+ """
+ Get the polygon coordinates for the given index.
+
+ Args:
+ idx: The polygon index
+
+ Returns:
+ A numpy array containing the polygon coordinates
+ """
+ pass
+
+ def __del__(self):
+ """
+ Ensure resources are cleaned up when the object is destroyed.
+ """
+ self.cleanup()
+
+ @abstractmethod
+ def cleanup(self) -> None:
+ """Clean up resources."""
+ pass
+
+
+class FileCoordAccessor(AbstractCoordAccessor):
+ """Accessor that reads polygon coordinates from the file on demand."""
+
+ def __init__(self, coordinate_file_path: Path):
+ """
+ Initialize the file-based coordinate accessor.
+
+ Args:
+ coordinate_file_path: Path to the coordinate file
+ """
+ self.coordinate_file_path = coordinate_file_path
+ # Initialize file resources using proper resource management.
+ try:
+ # Use memory-mapped file for on-demand reading
+ self.coord_file: object = open(self.coordinate_file_path, "rb")
+ # Create memory map
+ self.coord_buf: mmap.mmap = mmap.mmap(
+ self.coord_file.fileno(), 0, access=mmap.ACCESS_READ
+ )
+ self.polygon_collection: PolygonCollection = get_polygon_collection(
+ self.coord_buf
+ )
+ except Exception:
+ # Clean up any partially initialized resources
+ self.cleanup()
+ raise
+
+ def __getitem__(self, idx: int) -> np.ndarray:
+ """
+ Get the polygon coordinates for the given index.
+
+ Args:
+ idx: The polygon index
+
+ Returns:
+ A numpy array containing the polygon coordinates
+ """
+ return read_polygon_array_from_binary(self.polygon_collection, idx)
+
+ def cleanup(self) -> None:
+ """Clean up resources."""
+ utils.close_resource(self.coord_file)
+ utils.close_resource(self.coord_buf)
+ del self.polygon_collection
+
+
+class MemoryCoordAccessor(AbstractCoordAccessor):
+ """Accessor that preloads all polygon coordinates into memory."""
+
+ def __init__(self, coordinate_file_path: Path):
+ """
+ Initialize the memory-based coordinate accessor.
+
+ Args:
+ coordinate_file_path: Path to the coordinate file
+ """
+ # Read entire file into memory
+ with open(coordinate_file_path, "rb") as f:
+ coord_buf = f.read()
+
+ # Initialize polygon collection
+ polygon_collection = get_polygon_collection(coord_buf)
+
+ # Get number of polygons
+ num_polygons = polygon_collection.PolygonsLength()
+
+ # Preload all polygons
+ self.polygons: Dict[int, np.ndarray] = {}
+ for idx in range(num_polygons):
+ self.polygons[idx] = read_polygon_array_from_binary(polygon_collection, idx)
+
+ # Once polygons are loaded, we don't need to keep polygon_collection or coord_buf references
+ # They'll be garbage collected
+
+ def __getitem__(self, idx: int) -> np.ndarray:
+ """
+ Get the polygon coordinates for the given index.
+
+ Args:
+ idx: The polygon index
+
+ Returns:
+ A numpy array containing the polygon coordinates
+ """
+ return self.polygons[idx]
+
+ def cleanup(self) -> None:
+ """Clean up resources."""
+ del self.polygons
+ # Just clear the dictionary, no file resources to clean up
+ if hasattr(self, "polygons"):
+ self.polygons.clear()
+
+
+def create_coord_accessor(
+ coordinate_file_path: Path, in_memory: bool
+) -> AbstractCoordAccessor:
+ """
+ Factory function to create the appropriate coordinate accessor.
+
+ Args:
+ coordinate_file_path: Path to the coordinate file
+ in_memory: Whether to use in-memory mode
+
+ Returns:
+ An instance of a coordinate accessor
+ """
+ if in_memory:
+ return MemoryCoordAccessor(coordinate_file_path)
+ else:
+ return FileCoordAccessor(coordinate_file_path)
diff --git a/frogpilot/third_party/timezonefinder/data/boundaries/coordinates.fbs b/frogpilot/third_party/timezonefinder/data/boundaries/coordinates.fbs
new file mode 100644
index 00000000..554ce5c1
Binary files /dev/null and b/frogpilot/third_party/timezonefinder/data/boundaries/coordinates.fbs differ
diff --git a/frogpilot/third_party/timezonefinder/data/boundaries/xmax.npy b/frogpilot/third_party/timezonefinder/data/boundaries/xmax.npy
new file mode 100644
index 00000000..8e674caf
Binary files /dev/null and b/frogpilot/third_party/timezonefinder/data/boundaries/xmax.npy differ
diff --git a/frogpilot/third_party/timezonefinder/data/boundaries/xmin.npy b/frogpilot/third_party/timezonefinder/data/boundaries/xmin.npy
new file mode 100644
index 00000000..b9b2f521
Binary files /dev/null and b/frogpilot/third_party/timezonefinder/data/boundaries/xmin.npy differ
diff --git a/frogpilot/third_party/timezonefinder/data/boundaries/ymax.npy b/frogpilot/third_party/timezonefinder/data/boundaries/ymax.npy
new file mode 100644
index 00000000..30f2c598
Binary files /dev/null and b/frogpilot/third_party/timezonefinder/data/boundaries/ymax.npy differ
diff --git a/frogpilot/third_party/timezonefinder/data/boundaries/ymin.npy b/frogpilot/third_party/timezonefinder/data/boundaries/ymin.npy
new file mode 100644
index 00000000..c605a094
Binary files /dev/null and b/frogpilot/third_party/timezonefinder/data/boundaries/ymin.npy differ
diff --git a/frogpilot/third_party/timezonefinder/data/hole_registry.json b/frogpilot/third_party/timezonefinder/data/hole_registry.json
new file mode 100644
index 00000000..cde3fd6c
--- /dev/null
+++ b/frogpilot/third_party/timezonefinder/data/hole_registry.json
@@ -0,0 +1,302 @@
+{
+ "10": [
+ 1,
+ 0
+ ],
+ "151": [
+ 2,
+ 21
+ ],
+ "153": [
+ 1,
+ 23
+ ],
+ "155": [
+ 2,
+ 24
+ ],
+ "167": [
+ 1,
+ 26
+ ],
+ "177": [
+ 1,
+ 27
+ ],
+ "19": [
+ 3,
+ 1
+ ],
+ "208": [
+ 1,
+ 28
+ ],
+ "218": [
+ 21,
+ 29
+ ],
+ "231": [
+ 1,
+ 50
+ ],
+ "268": [
+ 2,
+ 51
+ ],
+ "316": [
+ 8,
+ 53
+ ],
+ "389": [
+ 3,
+ 61
+ ],
+ "391": [
+ 2,
+ 64
+ ],
+ "40": [
+ 8,
+ 4
+ ],
+ "41": [
+ 6,
+ 12
+ ],
+ "410": [
+ 5,
+ 66
+ ],
+ "414": [
+ 2,
+ 71
+ ],
+ "481": [
+ 1,
+ 73
+ ],
+ "488": [
+ 2,
+ 74
+ ],
+ "515": [
+ 1,
+ 76
+ ],
+ "518": [
+ 1,
+ 77
+ ],
+ "522": [
+ 16,
+ 78
+ ],
+ "685": [
+ 2,
+ 94
+ ],
+ "721": [
+ 15,
+ 96
+ ],
+ "725": [
+ 44,
+ 111
+ ],
+ "728": [
+ 2,
+ 155
+ ],
+ "729": [
+ 37,
+ 157
+ ],
+ "730": [
+ 20,
+ 194
+ ],
+ "733": [
+ 1,
+ 214
+ ],
+ "736": [
+ 28,
+ 215
+ ],
+ "737": [
+ 3,
+ 243
+ ],
+ "741": [
+ 1,
+ 246
+ ],
+ "742": [
+ 17,
+ 247
+ ],
+ "743": [
+ 4,
+ 264
+ ],
+ "748": [
+ 2,
+ 268
+ ],
+ "750": [
+ 1,
+ 270
+ ],
+ "753": [
+ 2,
+ 271
+ ],
+ "754": [
+ 5,
+ 273
+ ],
+ "755": [
+ 3,
+ 278
+ ],
+ "756": [
+ 14,
+ 281
+ ],
+ "761": [
+ 12,
+ 295
+ ],
+ "762": [
+ 3,
+ 307
+ ],
+ "765": [
+ 1,
+ 310
+ ],
+ "769": [
+ 1,
+ 311
+ ],
+ "771": [
+ 7,
+ 312
+ ],
+ "776": [
+ 21,
+ 319
+ ],
+ "781": [
+ 3,
+ 340
+ ],
+ "790": [
+ 1,
+ 343
+ ],
+ "793": [
+ 5,
+ 344
+ ],
+ "800": [
+ 2,
+ 349
+ ],
+ "804": [
+ 1,
+ 351
+ ],
+ "806": [
+ 2,
+ 352
+ ],
+ "809": [
+ 2,
+ 354
+ ],
+ "810": [
+ 4,
+ 356
+ ],
+ "812": [
+ 13,
+ 360
+ ],
+ "814": [
+ 17,
+ 373
+ ],
+ "817": [
+ 2,
+ 390
+ ],
+ "819": [
+ 1,
+ 392
+ ],
+ "820": [
+ 11,
+ 393
+ ],
+ "824": [
+ 2,
+ 404
+ ],
+ "828": [
+ 13,
+ 406
+ ],
+ "829": [
+ 1,
+ 419
+ ],
+ "830": [
+ 4,
+ 420
+ ],
+ "833": [
+ 6,
+ 424
+ ],
+ "834": [
+ 13,
+ 430
+ ],
+ "838": [
+ 6,
+ 443
+ ],
+ "841": [
+ 4,
+ 449
+ ],
+ "844": [
+ 32,
+ 453
+ ],
+ "848": [
+ 34,
+ 485
+ ],
+ "851": [
+ 37,
+ 519
+ ],
+ "854": [
+ 22,
+ 556
+ ],
+ "855": [
+ 1,
+ 578
+ ],
+ "91": [
+ 1,
+ 18
+ ],
+ "98": [
+ 2,
+ 19
+ ]
+}
diff --git a/frogpilot/third_party/timezonefinder/data/holes/coordinates.fbs b/frogpilot/third_party/timezonefinder/data/holes/coordinates.fbs
new file mode 100644
index 00000000..9c286587
Binary files /dev/null and b/frogpilot/third_party/timezonefinder/data/holes/coordinates.fbs differ
diff --git a/frogpilot/third_party/timezonefinder/data/holes/xmax.npy b/frogpilot/third_party/timezonefinder/data/holes/xmax.npy
new file mode 100644
index 00000000..fd0bf1db
Binary files /dev/null and b/frogpilot/third_party/timezonefinder/data/holes/xmax.npy differ
diff --git a/frogpilot/third_party/timezonefinder/data/holes/xmin.npy b/frogpilot/third_party/timezonefinder/data/holes/xmin.npy
new file mode 100644
index 00000000..0380c9c7
Binary files /dev/null and b/frogpilot/third_party/timezonefinder/data/holes/xmin.npy differ
diff --git a/frogpilot/third_party/timezonefinder/data/holes/ymax.npy b/frogpilot/third_party/timezonefinder/data/holes/ymax.npy
new file mode 100644
index 00000000..18c258d1
Binary files /dev/null and b/frogpilot/third_party/timezonefinder/data/holes/ymax.npy differ
diff --git a/frogpilot/third_party/timezonefinder/data/holes/ymin.npy b/frogpilot/third_party/timezonefinder/data/holes/ymin.npy
new file mode 100644
index 00000000..146d61b7
Binary files /dev/null and b/frogpilot/third_party/timezonefinder/data/holes/ymin.npy differ
diff --git a/frogpilot/third_party/timezonefinder/data/hybrid_shortcuts_uint8.fbs b/frogpilot/third_party/timezonefinder/data/hybrid_shortcuts_uint8.fbs
new file mode 100644
index 00000000..c3b4eecb
Binary files /dev/null and b/frogpilot/third_party/timezonefinder/data/hybrid_shortcuts_uint8.fbs differ
diff --git a/frogpilot/third_party/timezonefinder/data/timezone_names.txt b/frogpilot/third_party/timezonefinder/data/timezone_names.txt
new file mode 100644
index 00000000..d2f5a2ef
--- /dev/null
+++ b/frogpilot/third_party/timezonefinder/data/timezone_names.txt
@@ -0,0 +1,92 @@
+Etc/UTC
+Africa/Abidjan
+Europe/Moscow
+Africa/Lagos
+Africa/Johannesburg
+Africa/Cairo
+Africa/Casablanca
+Europe/Paris
+America/Adak
+America/Anchorage
+America/Caracas
+America/Sao_Paulo
+America/Lima
+America/Mexico_City
+America/Denver
+America/Chicago
+America/Phoenix
+America/New_York
+America/Halifax
+America/Havana
+America/Los_Angeles
+America/Miquelon
+America/Noronha
+America/Nuuk
+America/Santiago
+America/St_Johns
+Asia/Manila
+Asia/Jakarta
+Australia/Brisbane
+Australia/Sydney
+Asia/Karachi
+Pacific/Auckland
+Antarctica/Troll
+Pacific/Fiji
+Asia/Dubai
+Asia/Beirut
+Asia/Dhaka
+Asia/Tokyo
+Asia/Kolkata
+Europe/Athens
+Asia/Gaza
+Asia/Jerusalem
+Asia/Kabul
+Asia/Kathmandu
+Asia/Sakhalin
+Asia/Tehran
+Asia/Yangon
+Atlantic/Azores
+Europe/Lisbon
+Atlantic/Cape_Verde
+Australia/Adelaide
+Australia/Darwin
+Australia/Eucla
+Australia/Lord_Howe
+Europe/Chisinau
+Europe/Dublin
+Europe/London
+Pacific/Tongatapu
+Pacific/Chatham
+Pacific/Easter
+Pacific/Gambier
+Pacific/Honolulu
+Pacific/Kiritimati
+Pacific/Marquesas
+Pacific/Pago_Pago
+Pacific/Norfolk
+Pacific/Pitcairn
+Etc/GMT-12
+Etc/GMT-11
+Etc/GMT-10
+Etc/GMT-9
+Etc/GMT-8
+Etc/GMT-7
+Etc/GMT-6
+Etc/GMT-5
+Etc/GMT-4
+Etc/GMT-3
+Etc/GMT-2
+Etc/GMT-1
+Etc/GMT
+Etc/GMT+1
+Etc/GMT+2
+Etc/GMT+3
+Etc/GMT+4
+Etc/GMT+5
+Etc/GMT+6
+Etc/GMT+7
+Etc/GMT+8
+Etc/GMT+9
+Etc/GMT+10
+Etc/GMT+11
+Etc/GMT+12
diff --git a/frogpilot/third_party/timezonefinder/data/zone_ids.npy b/frogpilot/third_party/timezonefinder/data/zone_ids.npy
new file mode 100644
index 00000000..8841d0da
Binary files /dev/null and b/frogpilot/third_party/timezonefinder/data/zone_ids.npy differ
diff --git a/frogpilot/third_party/timezonefinder/data/zone_positions.npy b/frogpilot/third_party/timezonefinder/data/zone_positions.npy
new file mode 100644
index 00000000..6bb52475
Binary files /dev/null and b/frogpilot/third_party/timezonefinder/data/zone_positions.npy differ
diff --git a/frogpilot/third_party/timezonefinder/flatbuf/__init__.py b/frogpilot/third_party/timezonefinder/flatbuf/__init__.py
new file mode 100644
index 00000000..f3af734a
--- /dev/null
+++ b/frogpilot/third_party/timezonefinder/flatbuf/__init__.py
@@ -0,0 +1,5 @@
+"""FlatBuffer schemas, bindings, and IO helpers used by timezonefinder."""
+
+from . import generated, io, schemas
+
+__all__ = ["generated", "io", "schemas"]
diff --git a/frogpilot/third_party/timezonefinder/flatbuf/generated/__init__.py b/frogpilot/third_party/timezonefinder/flatbuf/generated/__init__.py
new file mode 100644
index 00000000..8d058dab
--- /dev/null
+++ b/frogpilot/third_party/timezonefinder/flatbuf/generated/__init__.py
@@ -0,0 +1,7 @@
+"""Auto-generated FlatBuffer bindings grouped by domain."""
+
+__all__ = [
+ "polygons",
+ "shortcuts_uint8",
+ "shortcuts_uint16",
+]
diff --git a/frogpilot/third_party/timezonefinder/flatbuf/generated/polygons/Polygon.py b/frogpilot/third_party/timezonefinder/flatbuf/generated/polygons/Polygon.py
new file mode 100644
index 00000000..b3ef1e82
--- /dev/null
+++ b/frogpilot/third_party/timezonefinder/flatbuf/generated/polygons/Polygon.py
@@ -0,0 +1,92 @@
+# automatically generated by the FlatBuffers compiler, do not modify
+
+# namespace: polygons
+
+import flatbuffers
+from flatbuffers.compat import import_numpy
+
+np = import_numpy()
+
+
+class Polygon:
+ __slots__ = ["_tab"]
+
+ @classmethod
+ def GetRootAs(cls, buf, offset=0):
+ n = flatbuffers.encode.Get(flatbuffers.packer.uoffset, buf, offset)
+ x = Polygon()
+ x.Init(buf, n + offset)
+ return x
+
+ @classmethod
+ def GetRootAsPolygon(cls, buf, offset=0):
+ """This method is deprecated. Please switch to GetRootAs."""
+ return cls.GetRootAs(buf, offset)
+
+ # Polygon
+ def Init(self, buf, pos):
+ self._tab = flatbuffers.table.Table(buf, pos)
+
+ # Polygon
+ def Coords(self, j):
+ o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4))
+ if o != 0:
+ a = self._tab.Vector(o)
+ return self._tab.Get(
+ flatbuffers.number_types.Int32Flags,
+ a + flatbuffers.number_types.UOffsetTFlags.py_type(j * 4),
+ )
+ return 0
+
+ # Polygon
+ def CoordsAsNumpy(self):
+ o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4))
+ if o != 0:
+ return self._tab.GetVectorAsNumpy(flatbuffers.number_types.Int32Flags, o)
+ return 0
+
+ # Polygon
+ def CoordsLength(self):
+ o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4))
+ if o != 0:
+ return self._tab.VectorLen(o)
+ return 0
+
+ # Polygon
+ def CoordsIsNone(self):
+ o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4))
+ return o == 0
+
+
+def PolygonStart(builder):
+ builder.StartObject(1)
+
+
+def Start(builder):
+ PolygonStart(builder)
+
+
+def PolygonAddCoords(builder, coords):
+ builder.PrependUOffsetTRelativeSlot(
+ 0, flatbuffers.number_types.UOffsetTFlags.py_type(coords), 0
+ )
+
+
+def AddCoords(builder, coords):
+ PolygonAddCoords(builder, coords)
+
+
+def PolygonStartCoordsVector(builder, numElems):
+ return builder.StartVector(4, numElems, 4)
+
+
+def StartCoordsVector(builder, numElems):
+ return PolygonStartCoordsVector(builder, numElems)
+
+
+def PolygonEnd(builder):
+ return builder.EndObject()
+
+
+def End(builder):
+ return PolygonEnd(builder)
diff --git a/frogpilot/third_party/timezonefinder/flatbuf/generated/polygons/PolygonCollection.py b/frogpilot/third_party/timezonefinder/flatbuf/generated/polygons/PolygonCollection.py
new file mode 100644
index 00000000..fa5164db
--- /dev/null
+++ b/frogpilot/third_party/timezonefinder/flatbuf/generated/polygons/PolygonCollection.py
@@ -0,0 +1,88 @@
+# automatically generated by the FlatBuffers compiler, do not modify
+
+# namespace: polygons
+
+import flatbuffers
+from flatbuffers.compat import import_numpy
+
+np = import_numpy()
+
+
+class PolygonCollection:
+ __slots__ = ["_tab"]
+
+ @classmethod
+ def GetRootAs(cls, buf, offset=0):
+ n = flatbuffers.encode.Get(flatbuffers.packer.uoffset, buf, offset)
+ x = PolygonCollection()
+ x.Init(buf, n + offset)
+ return x
+
+ @classmethod
+ def GetRootAsPolygonCollection(cls, buf, offset=0):
+ """This method is deprecated. Please switch to GetRootAs."""
+ return cls.GetRootAs(buf, offset)
+
+ # PolygonCollection
+ def Init(self, buf, pos):
+ self._tab = flatbuffers.table.Table(buf, pos)
+
+ # PolygonCollection
+ def Polygons(self, j):
+ o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4))
+ if o != 0:
+ x = self._tab.Vector(o)
+ x += flatbuffers.number_types.UOffsetTFlags.py_type(j) * 4
+ x = self._tab.Indirect(x)
+ from timezonefinder.flatbuf.generated.polygons.Polygon import Polygon
+
+ obj = Polygon()
+ obj.Init(self._tab.Bytes, x)
+ return obj
+ return None
+
+ # PolygonCollection
+ def PolygonsLength(self):
+ o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4))
+ if o != 0:
+ return self._tab.VectorLen(o)
+ return 0
+
+ # PolygonCollection
+ def PolygonsIsNone(self):
+ o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4))
+ return o == 0
+
+
+def PolygonCollectionStart(builder):
+ builder.StartObject(1)
+
+
+def Start(builder):
+ PolygonCollectionStart(builder)
+
+
+def PolygonCollectionAddPolygons(builder, polygons):
+ builder.PrependUOffsetTRelativeSlot(
+ 0, flatbuffers.number_types.UOffsetTFlags.py_type(polygons), 0
+ )
+
+
+def AddPolygons(builder, polygons):
+ PolygonCollectionAddPolygons(builder, polygons)
+
+
+def PolygonCollectionStartPolygonsVector(builder, numElems):
+ return builder.StartVector(4, numElems, 4)
+
+
+def StartPolygonsVector(builder, numElems):
+ return PolygonCollectionStartPolygonsVector(builder, numElems)
+
+
+def PolygonCollectionEnd(builder):
+ return builder.EndObject()
+
+
+def End(builder):
+ return PolygonCollectionEnd(builder)
diff --git a/frogpilot/third_party/reactivex-4.1.0.dist-info/REQUESTED b/frogpilot/third_party/timezonefinder/flatbuf/generated/polygons/__init__.py
similarity index 100%
rename from frogpilot/third_party/reactivex-4.1.0.dist-info/REQUESTED
rename to frogpilot/third_party/timezonefinder/flatbuf/generated/polygons/__init__.py
diff --git a/frogpilot/third_party/timezonefinder/flatbuf/generated/shortcuts_uint16/HybridShortcutCollection.py b/frogpilot/third_party/timezonefinder/flatbuf/generated/shortcuts_uint16/HybridShortcutCollection.py
new file mode 100644
index 00000000..10df14e4
--- /dev/null
+++ b/frogpilot/third_party/timezonefinder/flatbuf/generated/shortcuts_uint16/HybridShortcutCollection.py
@@ -0,0 +1,90 @@
+# automatically generated by the FlatBuffers compiler, do not modify
+
+# namespace: shortcuts_uint16
+
+import flatbuffers
+from flatbuffers.compat import import_numpy
+
+np = import_numpy()
+
+
+class HybridShortcutCollection:
+ __slots__ = ["_tab"]
+
+ @classmethod
+ def GetRootAs(cls, buf, offset=0):
+ n = flatbuffers.encode.Get(flatbuffers.packer.uoffset, buf, offset)
+ x = HybridShortcutCollection()
+ x.Init(buf, n + offset)
+ return x
+
+ @classmethod
+ def GetRootAsHybridShortcutCollection(cls, buf, offset=0):
+ """This method is deprecated. Please switch to GetRootAs."""
+ return cls.GetRootAs(buf, offset)
+
+ # HybridShortcutCollection
+ def Init(self, buf, pos):
+ self._tab = flatbuffers.table.Table(buf, pos)
+
+ # HybridShortcutCollection
+ def Entries(self, j):
+ o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4))
+ if o != 0:
+ x = self._tab.Vector(o)
+ x += flatbuffers.number_types.UOffsetTFlags.py_type(j) * 4
+ x = self._tab.Indirect(x)
+ from timezonefinder.flatbuf.generated.shortcuts_uint16.HybridShortcutEntry import (
+ HybridShortcutEntry,
+ )
+
+ obj = HybridShortcutEntry()
+ obj.Init(self._tab.Bytes, x)
+ return obj
+ return None
+
+ # HybridShortcutCollection
+ def EntriesLength(self):
+ o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4))
+ if o != 0:
+ return self._tab.VectorLen(o)
+ return 0
+
+ # HybridShortcutCollection
+ def EntriesIsNone(self):
+ o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4))
+ return o == 0
+
+
+def HybridShortcutCollectionStart(builder):
+ builder.StartObject(1)
+
+
+def Start(builder):
+ HybridShortcutCollectionStart(builder)
+
+
+def HybridShortcutCollectionAddEntries(builder, entries):
+ builder.PrependUOffsetTRelativeSlot(
+ 0, flatbuffers.number_types.UOffsetTFlags.py_type(entries), 0
+ )
+
+
+def AddEntries(builder, entries):
+ HybridShortcutCollectionAddEntries(builder, entries)
+
+
+def HybridShortcutCollectionStartEntriesVector(builder, numElems):
+ return builder.StartVector(4, numElems, 4)
+
+
+def StartEntriesVector(builder, numElems):
+ return HybridShortcutCollectionStartEntriesVector(builder, numElems)
+
+
+def HybridShortcutCollectionEnd(builder):
+ return builder.EndObject()
+
+
+def End(builder):
+ return HybridShortcutCollectionEnd(builder)
diff --git a/frogpilot/third_party/timezonefinder/flatbuf/generated/shortcuts_uint16/HybridShortcutEntry.py b/frogpilot/third_party/timezonefinder/flatbuf/generated/shortcuts_uint16/HybridShortcutEntry.py
new file mode 100644
index 00000000..e22c5b5a
--- /dev/null
+++ b/frogpilot/third_party/timezonefinder/flatbuf/generated/shortcuts_uint16/HybridShortcutEntry.py
@@ -0,0 +1,97 @@
+# automatically generated by the FlatBuffers compiler, do not modify
+
+# namespace: shortcuts_uint16
+
+import flatbuffers
+from flatbuffers.compat import import_numpy
+
+np = import_numpy()
+
+
+class HybridShortcutEntry:
+ __slots__ = ["_tab"]
+
+ @classmethod
+ def GetRootAs(cls, buf, offset=0):
+ n = flatbuffers.encode.Get(flatbuffers.packer.uoffset, buf, offset)
+ x = HybridShortcutEntry()
+ x.Init(buf, n + offset)
+ return x
+
+ @classmethod
+ def GetRootAsHybridShortcutEntry(cls, buf, offset=0):
+ """This method is deprecated. Please switch to GetRootAs."""
+ return cls.GetRootAs(buf, offset)
+
+ # HybridShortcutEntry
+ def Init(self, buf, pos):
+ self._tab = flatbuffers.table.Table(buf, pos)
+
+ # HybridShortcutEntry
+ def HexId(self):
+ o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4))
+ if o != 0:
+ return self._tab.Get(
+ flatbuffers.number_types.Uint64Flags, o + self._tab.Pos
+ )
+ return 0
+
+ # HybridShortcutEntry
+ def ValueType(self):
+ o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(6))
+ if o != 0:
+ return self._tab.Get(flatbuffers.number_types.Uint8Flags, o + self._tab.Pos)
+ return 0
+
+ # HybridShortcutEntry
+ def Value(self):
+ o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(8))
+ if o != 0:
+ from flatbuffers.table import Table
+
+ obj = Table(bytearray(), 0)
+ self._tab.Union(obj, o)
+ return obj
+ return None
+
+
+def HybridShortcutEntryStart(builder):
+ builder.StartObject(3)
+
+
+def Start(builder):
+ HybridShortcutEntryStart(builder)
+
+
+def HybridShortcutEntryAddHexId(builder, hexId):
+ builder.PrependUint64Slot(0, hexId, 0)
+
+
+def AddHexId(builder, hexId):
+ HybridShortcutEntryAddHexId(builder, hexId)
+
+
+def HybridShortcutEntryAddValueType(builder, valueType):
+ builder.PrependUint8Slot(1, valueType, 0)
+
+
+def AddValueType(builder, valueType):
+ HybridShortcutEntryAddValueType(builder, valueType)
+
+
+def HybridShortcutEntryAddValue(builder, value):
+ builder.PrependUOffsetTRelativeSlot(
+ 2, flatbuffers.number_types.UOffsetTFlags.py_type(value), 0
+ )
+
+
+def AddValue(builder, value):
+ HybridShortcutEntryAddValue(builder, value)
+
+
+def HybridShortcutEntryEnd(builder):
+ return builder.EndObject()
+
+
+def End(builder):
+ return HybridShortcutEntryEnd(builder)
diff --git a/frogpilot/third_party/timezonefinder/flatbuf/generated/shortcuts_uint16/PolygonList.py b/frogpilot/third_party/timezonefinder/flatbuf/generated/shortcuts_uint16/PolygonList.py
new file mode 100644
index 00000000..0c0eefe9
--- /dev/null
+++ b/frogpilot/third_party/timezonefinder/flatbuf/generated/shortcuts_uint16/PolygonList.py
@@ -0,0 +1,92 @@
+# automatically generated by the FlatBuffers compiler, do not modify
+
+# namespace: shortcuts_uint16
+
+import flatbuffers
+from flatbuffers.compat import import_numpy
+
+np = import_numpy()
+
+
+class PolygonList:
+ __slots__ = ["_tab"]
+
+ @classmethod
+ def GetRootAs(cls, buf, offset=0):
+ n = flatbuffers.encode.Get(flatbuffers.packer.uoffset, buf, offset)
+ x = PolygonList()
+ x.Init(buf, n + offset)
+ return x
+
+ @classmethod
+ def GetRootAsPolygonList(cls, buf, offset=0):
+ """This method is deprecated. Please switch to GetRootAs."""
+ return cls.GetRootAs(buf, offset)
+
+ # PolygonList
+ def Init(self, buf, pos):
+ self._tab = flatbuffers.table.Table(buf, pos)
+
+ # PolygonList
+ def PolyIds(self, j):
+ o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4))
+ if o != 0:
+ a = self._tab.Vector(o)
+ return self._tab.Get(
+ flatbuffers.number_types.Uint16Flags,
+ a + flatbuffers.number_types.UOffsetTFlags.py_type(j * 2),
+ )
+ return 0
+
+ # PolygonList
+ def PolyIdsAsNumpy(self):
+ o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4))
+ if o != 0:
+ return self._tab.GetVectorAsNumpy(flatbuffers.number_types.Uint16Flags, o)
+ return 0
+
+ # PolygonList
+ def PolyIdsLength(self):
+ o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4))
+ if o != 0:
+ return self._tab.VectorLen(o)
+ return 0
+
+ # PolygonList
+ def PolyIdsIsNone(self):
+ o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4))
+ return o == 0
+
+
+def PolygonListStart(builder):
+ builder.StartObject(1)
+
+
+def Start(builder):
+ PolygonListStart(builder)
+
+
+def PolygonListAddPolyIds(builder, polyIds):
+ builder.PrependUOffsetTRelativeSlot(
+ 0, flatbuffers.number_types.UOffsetTFlags.py_type(polyIds), 0
+ )
+
+
+def AddPolyIds(builder, polyIds):
+ PolygonListAddPolyIds(builder, polyIds)
+
+
+def PolygonListStartPolyIdsVector(builder, numElems):
+ return builder.StartVector(2, numElems, 2)
+
+
+def StartPolyIdsVector(builder, numElems):
+ return PolygonListStartPolyIdsVector(builder, numElems)
+
+
+def PolygonListEnd(builder):
+ return builder.EndObject()
+
+
+def End(builder):
+ return PolygonListEnd(builder)
diff --git a/frogpilot/third_party/timezonefinder/flatbuf/generated/shortcuts_uint16/ShortcutValue.py b/frogpilot/third_party/timezonefinder/flatbuf/generated/shortcuts_uint16/ShortcutValue.py
new file mode 100644
index 00000000..73bb67dd
--- /dev/null
+++ b/frogpilot/third_party/timezonefinder/flatbuf/generated/shortcuts_uint16/ShortcutValue.py
@@ -0,0 +1,9 @@
+# automatically generated by the FlatBuffers compiler, do not modify
+
+# namespace: shortcuts_uint16
+
+
+class ShortcutValue:
+ NONE = 0
+ UniqueZone = 1
+ PolygonList = 2
diff --git a/frogpilot/third_party/timezonefinder/flatbuf/generated/shortcuts_uint16/UniqueZone.py b/frogpilot/third_party/timezonefinder/flatbuf/generated/shortcuts_uint16/UniqueZone.py
new file mode 100644
index 00000000..08641f9c
--- /dev/null
+++ b/frogpilot/third_party/timezonefinder/flatbuf/generated/shortcuts_uint16/UniqueZone.py
@@ -0,0 +1,61 @@
+# automatically generated by the FlatBuffers compiler, do not modify
+
+# namespace: shortcuts_uint16
+
+import flatbuffers
+from flatbuffers.compat import import_numpy
+
+np = import_numpy()
+
+
+class UniqueZone:
+ __slots__ = ["_tab"]
+
+ @classmethod
+ def GetRootAs(cls, buf, offset=0):
+ n = flatbuffers.encode.Get(flatbuffers.packer.uoffset, buf, offset)
+ x = UniqueZone()
+ x.Init(buf, n + offset)
+ return x
+
+ @classmethod
+ def GetRootAsUniqueZone(cls, buf, offset=0):
+ """This method is deprecated. Please switch to GetRootAs."""
+ return cls.GetRootAs(buf, offset)
+
+ # UniqueZone
+ def Init(self, buf, pos):
+ self._tab = flatbuffers.table.Table(buf, pos)
+
+ # UniqueZone
+ def ZoneId(self):
+ o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4))
+ if o != 0:
+ return self._tab.Get(
+ flatbuffers.number_types.Uint16Flags, o + self._tab.Pos
+ )
+ return 0
+
+
+def UniqueZoneStart(builder):
+ builder.StartObject(1)
+
+
+def Start(builder):
+ UniqueZoneStart(builder)
+
+
+def UniqueZoneAddZoneId(builder, zoneId):
+ builder.PrependUint16Slot(0, zoneId, 0)
+
+
+def AddZoneId(builder, zoneId):
+ UniqueZoneAddZoneId(builder, zoneId)
+
+
+def UniqueZoneEnd(builder):
+ return builder.EndObject()
+
+
+def End(builder):
+ return UniqueZoneEnd(builder)
diff --git a/frogpilot/third_party/timezonefinder/flatbuf/generated/shortcuts_uint16/__init__.py b/frogpilot/third_party/timezonefinder/flatbuf/generated/shortcuts_uint16/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/frogpilot/third_party/timezonefinder/flatbuf/generated/shortcuts_uint8/HybridShortcutCollection.py b/frogpilot/third_party/timezonefinder/flatbuf/generated/shortcuts_uint8/HybridShortcutCollection.py
new file mode 100644
index 00000000..d64bbf03
--- /dev/null
+++ b/frogpilot/third_party/timezonefinder/flatbuf/generated/shortcuts_uint8/HybridShortcutCollection.py
@@ -0,0 +1,90 @@
+# automatically generated by the FlatBuffers compiler, do not modify
+
+# namespace: shortcuts_uint8
+
+import flatbuffers
+from flatbuffers.compat import import_numpy
+
+np = import_numpy()
+
+
+class HybridShortcutCollection:
+ __slots__ = ["_tab"]
+
+ @classmethod
+ def GetRootAs(cls, buf, offset=0):
+ n = flatbuffers.encode.Get(flatbuffers.packer.uoffset, buf, offset)
+ x = HybridShortcutCollection()
+ x.Init(buf, n + offset)
+ return x
+
+ @classmethod
+ def GetRootAsHybridShortcutCollection(cls, buf, offset=0):
+ """This method is deprecated. Please switch to GetRootAs."""
+ return cls.GetRootAs(buf, offset)
+
+ # HybridShortcutCollection
+ def Init(self, buf, pos):
+ self._tab = flatbuffers.table.Table(buf, pos)
+
+ # HybridShortcutCollection
+ def Entries(self, j):
+ o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4))
+ if o != 0:
+ x = self._tab.Vector(o)
+ x += flatbuffers.number_types.UOffsetTFlags.py_type(j) * 4
+ x = self._tab.Indirect(x)
+ from timezonefinder.flatbuf.generated.shortcuts_uint8.HybridShortcutEntry import (
+ HybridShortcutEntry,
+ )
+
+ obj = HybridShortcutEntry()
+ obj.Init(self._tab.Bytes, x)
+ return obj
+ return None
+
+ # HybridShortcutCollection
+ def EntriesLength(self):
+ o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4))
+ if o != 0:
+ return self._tab.VectorLen(o)
+ return 0
+
+ # HybridShortcutCollection
+ def EntriesIsNone(self):
+ o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4))
+ return o == 0
+
+
+def HybridShortcutCollectionStart(builder):
+ builder.StartObject(1)
+
+
+def Start(builder):
+ HybridShortcutCollectionStart(builder)
+
+
+def HybridShortcutCollectionAddEntries(builder, entries):
+ builder.PrependUOffsetTRelativeSlot(
+ 0, flatbuffers.number_types.UOffsetTFlags.py_type(entries), 0
+ )
+
+
+def AddEntries(builder, entries):
+ HybridShortcutCollectionAddEntries(builder, entries)
+
+
+def HybridShortcutCollectionStartEntriesVector(builder, numElems):
+ return builder.StartVector(4, numElems, 4)
+
+
+def StartEntriesVector(builder, numElems):
+ return HybridShortcutCollectionStartEntriesVector(builder, numElems)
+
+
+def HybridShortcutCollectionEnd(builder):
+ return builder.EndObject()
+
+
+def End(builder):
+ return HybridShortcutCollectionEnd(builder)
diff --git a/frogpilot/third_party/timezonefinder/flatbuf/generated/shortcuts_uint8/HybridShortcutEntry.py b/frogpilot/third_party/timezonefinder/flatbuf/generated/shortcuts_uint8/HybridShortcutEntry.py
new file mode 100644
index 00000000..e9b844cb
--- /dev/null
+++ b/frogpilot/third_party/timezonefinder/flatbuf/generated/shortcuts_uint8/HybridShortcutEntry.py
@@ -0,0 +1,97 @@
+# automatically generated by the FlatBuffers compiler, do not modify
+
+# namespace: shortcuts_uint8
+
+import flatbuffers
+from flatbuffers.compat import import_numpy
+
+np = import_numpy()
+
+
+class HybridShortcutEntry:
+ __slots__ = ["_tab"]
+
+ @classmethod
+ def GetRootAs(cls, buf, offset=0):
+ n = flatbuffers.encode.Get(flatbuffers.packer.uoffset, buf, offset)
+ x = HybridShortcutEntry()
+ x.Init(buf, n + offset)
+ return x
+
+ @classmethod
+ def GetRootAsHybridShortcutEntry(cls, buf, offset=0):
+ """This method is deprecated. Please switch to GetRootAs."""
+ return cls.GetRootAs(buf, offset)
+
+ # HybridShortcutEntry
+ def Init(self, buf, pos):
+ self._tab = flatbuffers.table.Table(buf, pos)
+
+ # HybridShortcutEntry
+ def HexId(self):
+ o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4))
+ if o != 0:
+ return self._tab.Get(
+ flatbuffers.number_types.Uint64Flags, o + self._tab.Pos
+ )
+ return 0
+
+ # HybridShortcutEntry
+ def ValueType(self):
+ o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(6))
+ if o != 0:
+ return self._tab.Get(flatbuffers.number_types.Uint8Flags, o + self._tab.Pos)
+ return 0
+
+ # HybridShortcutEntry
+ def Value(self):
+ o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(8))
+ if o != 0:
+ from flatbuffers.table import Table
+
+ obj = Table(bytearray(), 0)
+ self._tab.Union(obj, o)
+ return obj
+ return None
+
+
+def HybridShortcutEntryStart(builder):
+ builder.StartObject(3)
+
+
+def Start(builder):
+ HybridShortcutEntryStart(builder)
+
+
+def HybridShortcutEntryAddHexId(builder, hexId):
+ builder.PrependUint64Slot(0, hexId, 0)
+
+
+def AddHexId(builder, hexId):
+ HybridShortcutEntryAddHexId(builder, hexId)
+
+
+def HybridShortcutEntryAddValueType(builder, valueType):
+ builder.PrependUint8Slot(1, valueType, 0)
+
+
+def AddValueType(builder, valueType):
+ HybridShortcutEntryAddValueType(builder, valueType)
+
+
+def HybridShortcutEntryAddValue(builder, value):
+ builder.PrependUOffsetTRelativeSlot(
+ 2, flatbuffers.number_types.UOffsetTFlags.py_type(value), 0
+ )
+
+
+def AddValue(builder, value):
+ HybridShortcutEntryAddValue(builder, value)
+
+
+def HybridShortcutEntryEnd(builder):
+ return builder.EndObject()
+
+
+def End(builder):
+ return HybridShortcutEntryEnd(builder)
diff --git a/frogpilot/third_party/timezonefinder/flatbuf/generated/shortcuts_uint8/PolygonList.py b/frogpilot/third_party/timezonefinder/flatbuf/generated/shortcuts_uint8/PolygonList.py
new file mode 100644
index 00000000..59a90e2a
--- /dev/null
+++ b/frogpilot/third_party/timezonefinder/flatbuf/generated/shortcuts_uint8/PolygonList.py
@@ -0,0 +1,92 @@
+# automatically generated by the FlatBuffers compiler, do not modify
+
+# namespace: shortcuts_uint8
+
+import flatbuffers
+from flatbuffers.compat import import_numpy
+
+np = import_numpy()
+
+
+class PolygonList:
+ __slots__ = ["_tab"]
+
+ @classmethod
+ def GetRootAs(cls, buf, offset=0):
+ n = flatbuffers.encode.Get(flatbuffers.packer.uoffset, buf, offset)
+ x = PolygonList()
+ x.Init(buf, n + offset)
+ return x
+
+ @classmethod
+ def GetRootAsPolygonList(cls, buf, offset=0):
+ """This method is deprecated. Please switch to GetRootAs."""
+ return cls.GetRootAs(buf, offset)
+
+ # PolygonList
+ def Init(self, buf, pos):
+ self._tab = flatbuffers.table.Table(buf, pos)
+
+ # PolygonList
+ def PolyIds(self, j):
+ o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4))
+ if o != 0:
+ a = self._tab.Vector(o)
+ return self._tab.Get(
+ flatbuffers.number_types.Uint16Flags,
+ a + flatbuffers.number_types.UOffsetTFlags.py_type(j * 2),
+ )
+ return 0
+
+ # PolygonList
+ def PolyIdsAsNumpy(self):
+ o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4))
+ if o != 0:
+ return self._tab.GetVectorAsNumpy(flatbuffers.number_types.Uint16Flags, o)
+ return 0
+
+ # PolygonList
+ def PolyIdsLength(self):
+ o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4))
+ if o != 0:
+ return self._tab.VectorLen(o)
+ return 0
+
+ # PolygonList
+ def PolyIdsIsNone(self):
+ o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4))
+ return o == 0
+
+
+def PolygonListStart(builder):
+ builder.StartObject(1)
+
+
+def Start(builder):
+ PolygonListStart(builder)
+
+
+def PolygonListAddPolyIds(builder, polyIds):
+ builder.PrependUOffsetTRelativeSlot(
+ 0, flatbuffers.number_types.UOffsetTFlags.py_type(polyIds), 0
+ )
+
+
+def AddPolyIds(builder, polyIds):
+ PolygonListAddPolyIds(builder, polyIds)
+
+
+def PolygonListStartPolyIdsVector(builder, numElems):
+ return builder.StartVector(2, numElems, 2)
+
+
+def StartPolyIdsVector(builder, numElems):
+ return PolygonListStartPolyIdsVector(builder, numElems)
+
+
+def PolygonListEnd(builder):
+ return builder.EndObject()
+
+
+def End(builder):
+ return PolygonListEnd(builder)
diff --git a/frogpilot/third_party/timezonefinder/flatbuf/generated/shortcuts_uint8/ShortcutValue.py b/frogpilot/third_party/timezonefinder/flatbuf/generated/shortcuts_uint8/ShortcutValue.py
new file mode 100644
index 00000000..13259ff5
--- /dev/null
+++ b/frogpilot/third_party/timezonefinder/flatbuf/generated/shortcuts_uint8/ShortcutValue.py
@@ -0,0 +1,9 @@
+# automatically generated by the FlatBuffers compiler, do not modify
+
+# namespace: shortcuts_uint8
+
+
+class ShortcutValue:
+ NONE = 0
+ UniqueZone = 1
+ PolygonList = 2
diff --git a/frogpilot/third_party/timezonefinder/flatbuf/generated/shortcuts_uint8/UniqueZone.py b/frogpilot/third_party/timezonefinder/flatbuf/generated/shortcuts_uint8/UniqueZone.py
new file mode 100644
index 00000000..06237ca9
--- /dev/null
+++ b/frogpilot/third_party/timezonefinder/flatbuf/generated/shortcuts_uint8/UniqueZone.py
@@ -0,0 +1,59 @@
+# automatically generated by the FlatBuffers compiler, do not modify
+
+# namespace: shortcuts_uint8
+
+import flatbuffers
+from flatbuffers.compat import import_numpy
+
+np = import_numpy()
+
+
+class UniqueZone:
+ __slots__ = ["_tab"]
+
+ @classmethod
+ def GetRootAs(cls, buf, offset=0):
+ n = flatbuffers.encode.Get(flatbuffers.packer.uoffset, buf, offset)
+ x = UniqueZone()
+ x.Init(buf, n + offset)
+ return x
+
+ @classmethod
+ def GetRootAsUniqueZone(cls, buf, offset=0):
+ """This method is deprecated. Please switch to GetRootAs."""
+ return cls.GetRootAs(buf, offset)
+
+ # UniqueZone
+ def Init(self, buf, pos):
+ self._tab = flatbuffers.table.Table(buf, pos)
+
+ # UniqueZone
+ def ZoneId(self):
+ o = flatbuffers.number_types.UOffsetTFlags.py_type(self._tab.Offset(4))
+ if o != 0:
+ return self._tab.Get(flatbuffers.number_types.Uint8Flags, o + self._tab.Pos)
+ return 0
+
+
+def UniqueZoneStart(builder):
+ builder.StartObject(1)
+
+
+def Start(builder):
+ UniqueZoneStart(builder)
+
+
+def UniqueZoneAddZoneId(builder, zoneId):
+ builder.PrependUint8Slot(0, zoneId, 0)
+
+
+def AddZoneId(builder, zoneId):
+ UniqueZoneAddZoneId(builder, zoneId)
+
+
+def UniqueZoneEnd(builder):
+ return builder.EndObject()
+
+
+def End(builder):
+ return UniqueZoneEnd(builder)
diff --git a/frogpilot/third_party/timezonefinder/flatbuf/generated/shortcuts_uint8/__init__.py b/frogpilot/third_party/timezonefinder/flatbuf/generated/shortcuts_uint8/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/frogpilot/third_party/timezonefinder/flatbuf/io/__init__.py b/frogpilot/third_party/timezonefinder/flatbuf/io/__init__.py
new file mode 100644
index 00000000..b82173ae
--- /dev/null
+++ b/frogpilot/third_party/timezonefinder/flatbuf/io/__init__.py
@@ -0,0 +1,27 @@
+"""Utilities for reading and writing FlatBuffer assets."""
+
+from .polygons import (
+ flatten_polygon_coords,
+ reshape_to_polygon_coords,
+ get_coordinate_path,
+ write_polygon_collection_flatbuffer,
+ get_polygon_collection,
+ read_polygon_array_from_binary,
+)
+from .hybrid_shortcuts import (
+ get_hybrid_shortcut_file_path,
+ write_hybrid_shortcuts_flatbuffers,
+ read_hybrid_shortcuts_binary,
+)
+
+__all__ = [
+ "flatten_polygon_coords",
+ "reshape_to_polygon_coords",
+ "get_coordinate_path",
+ "write_polygon_collection_flatbuffer",
+ "get_polygon_collection",
+ "read_polygon_array_from_binary",
+ "get_hybrid_shortcut_file_path",
+ "write_hybrid_shortcuts_flatbuffers",
+ "read_hybrid_shortcuts_binary",
+]
diff --git a/frogpilot/third_party/timezonefinder/flatbuf/io/hybrid_shortcuts.py b/frogpilot/third_party/timezonefinder/flatbuf/io/hybrid_shortcuts.py
new file mode 100644
index 00000000..1ba54293
--- /dev/null
+++ b/frogpilot/third_party/timezonefinder/flatbuf/io/hybrid_shortcuts.py
@@ -0,0 +1,369 @@
+"""Utilities for working with optimized hybrid shortcut FlatBuffer data."""
+
+from pathlib import Path
+from typing import Any, Callable, Dict, List, Union
+from dataclasses import dataclass
+
+import flatbuffers
+import numpy as np
+
+from timezonefinder.configs import DEFAULT_DATA_DIR
+
+# Static imports for uint8 schema
+from timezonefinder.flatbuf.generated.shortcuts_uint8.HybridShortcutCollection import (
+ HybridShortcutCollection as HybridShortcutCollectionUint8,
+ HybridShortcutCollectionAddEntries as HybridShortcutCollectionAddEntriesUint8,
+ HybridShortcutCollectionEnd as HybridShortcutCollectionEndUint8,
+ HybridShortcutCollectionStart as HybridShortcutCollectionStartUint8,
+ HybridShortcutCollectionStartEntriesVector as HybridShortcutCollectionStartEntriesVectorUint8,
+)
+from timezonefinder.flatbuf.generated.shortcuts_uint8.HybridShortcutEntry import (
+ HybridShortcutEntryAddHexId as HybridShortcutEntryAddHexIdUint8,
+ HybridShortcutEntryAddValue as HybridShortcutEntryAddValueUint8,
+ HybridShortcutEntryAddValueType as HybridShortcutEntryAddValueTypeUint8,
+ HybridShortcutEntryEnd as HybridShortcutEntryEndUint8,
+ HybridShortcutEntryStart as HybridShortcutEntryStartUint8,
+)
+from timezonefinder.flatbuf.generated.shortcuts_uint8.UniqueZone import (
+ UniqueZone as UniqueZoneUint8,
+ UniqueZoneAddZoneId as UniqueZoneAddZoneIdUint8,
+ UniqueZoneEnd as UniqueZoneEndUint8,
+ UniqueZoneStart as UniqueZoneStartUint8,
+)
+from timezonefinder.flatbuf.generated.shortcuts_uint8.PolygonList import (
+ PolygonList as PolygonListUint8,
+ PolygonListAddPolyIds as PolygonListAddPolyIdsUint8,
+ PolygonListEnd as PolygonListEndUint8,
+ PolygonListStart as PolygonListStartUint8,
+ PolygonListStartPolyIdsVector as PolygonListStartPolyIdsVectorUint8,
+)
+from timezonefinder.flatbuf.generated.shortcuts_uint8.ShortcutValue import (
+ ShortcutValue as ShortcutValueUint8,
+)
+
+# Static imports for uint16 schema
+from timezonefinder.flatbuf.generated.shortcuts_uint16.HybridShortcutCollection import (
+ HybridShortcutCollection as HybridShortcutCollectionUint16,
+ HybridShortcutCollectionAddEntries as HybridShortcutCollectionAddEntriesUint16,
+ HybridShortcutCollectionEnd as HybridShortcutCollectionEndUint16,
+ HybridShortcutCollectionStart as HybridShortcutCollectionStartUint16,
+ HybridShortcutCollectionStartEntriesVector as HybridShortcutCollectionStartEntriesVectorUint16,
+)
+from timezonefinder.flatbuf.generated.shortcuts_uint16.HybridShortcutEntry import (
+ HybridShortcutEntryAddHexId as HybridShortcutEntryAddHexIdUint16,
+ HybridShortcutEntryAddValue as HybridShortcutEntryAddValueUint16,
+ HybridShortcutEntryAddValueType as HybridShortcutEntryAddValueTypeUint16,
+ HybridShortcutEntryEnd as HybridShortcutEntryEndUint16,
+ HybridShortcutEntryStart as HybridShortcutEntryStartUint16,
+)
+from timezonefinder.flatbuf.generated.shortcuts_uint16.UniqueZone import (
+ UniqueZone as UniqueZoneUint16,
+ UniqueZoneAddZoneId as UniqueZoneAddZoneIdUint16,
+ UniqueZoneEnd as UniqueZoneEndUint16,
+ UniqueZoneStart as UniqueZoneStartUint16,
+)
+from timezonefinder.flatbuf.generated.shortcuts_uint16.PolygonList import (
+ PolygonList as PolygonListUint16,
+ PolygonListAddPolyIds as PolygonListAddPolyIdsUint16,
+ PolygonListEnd as PolygonListEndUint16,
+ PolygonListStart as PolygonListStartUint16,
+ PolygonListStartPolyIdsVector as PolygonListStartPolyIdsVectorUint16,
+)
+from timezonefinder.flatbuf.generated.shortcuts_uint16.ShortcutValue import (
+ ShortcutValue as ShortcutValueUint16,
+)
+
+
+@dataclass
+class SchemaImports:
+ """Container for schema-specific imports to eliminate magic strings."""
+
+ # Collection functions
+ collection_start: Callable[..., Any]
+ collection_add_entries: Callable[..., Any]
+ collection_end: Callable[..., Any]
+ collection_start_entries_vector: Callable[..., Any]
+
+ # Entry functions
+ entry_start: Callable[..., Any]
+ entry_add_hex_id: Callable[..., Any]
+ entry_add_value_type: Callable[..., Any]
+ entry_add_value: Callable[..., Any]
+ entry_end: Callable[..., Any]
+
+ # UniqueZone functions
+ unique_zone_start: Callable[..., Any]
+ unique_zone_add_zone_id: Callable[..., Any]
+ unique_zone_end: Callable[..., Any]
+
+ # PolygonList functions
+ polygon_list_start: Callable[..., Any]
+ polygon_list_add_poly_ids: Callable[..., Any]
+ polygon_list_end: Callable[..., Any]
+ polygon_list_start_poly_ids_vector: Callable[..., Any]
+
+ # ShortcutValue enum
+ shortcut_value: Any
+
+ # Validation parameters
+ max_zone_id: int
+ dtype_name: str
+
+
+@dataclass
+class ReadSchemaImports:
+ """Container for read-specific schema imports."""
+
+ collection: Any
+ unique_zone: Any
+ polygon_list: Any
+ shortcut_value: Any
+
+
+def get_hybrid_shortcut_file_path(
+ zone_id_dtype: np.dtype, output_path: Path = DEFAULT_DATA_DIR
+) -> Path:
+ """Return the path to the appropriate hybrid shortcut FlatBuffer binary file."""
+ if zone_id_dtype.itemsize == 1:
+ return output_path / "hybrid_shortcuts_uint8.fbs"
+ elif zone_id_dtype.itemsize == 2:
+ return output_path / "hybrid_shortcuts_uint16.fbs"
+ else:
+ raise ValueError(
+ f"Unsupported zone_id_dtype: {zone_id_dtype}. Use uint8 or uint16."
+ )
+
+
+def _validate_zone_id_dtype(zone_id_dtype: np.dtype) -> np.dtype:
+ """Validate and normalize zone ID dtype."""
+ dtype = np.dtype(zone_id_dtype)
+ if dtype.kind != "u":
+ raise ValueError(f"Zone id dtype must be unsigned integer, got {dtype}")
+ if dtype.itemsize not in (1, 2):
+ raise ValueError(
+ f"Zone id dtype must be 1 or 2 bytes, got {dtype.itemsize} bytes"
+ )
+ return dtype.newbyteorder("<")
+
+
+def write_hybrid_shortcuts_flatbuffers(
+ hybrid_mapping: Dict[int, Union[int, List[int]]],
+ zone_id_dtype: np.dtype,
+ output_file: Path,
+) -> None:
+ """
+ Write hybrid shortcut mapping to the appropriate optimized FlatBuffer binary file.
+
+ Args:
+ hybrid_mapping: Dictionary mapping H3 hexagon IDs to either:
+ - int: unique zone ID (when all polygons share same zone)
+ - List[int]: list of polygon IDs (when multiple zones)
+ zone_id_dtype: numpy dtype for zone IDs (uint8 or uint16)
+ output_file: Path to save the FlatBuffer file
+ """
+ print(f"Writing {len(hybrid_mapping)} optimized hybrid shortcuts to {output_file}")
+
+ dtype = _validate_zone_id_dtype(zone_id_dtype)
+ _write_hybrid_shortcuts_generic(hybrid_mapping, dtype, output_file)
+
+
+def _write_hybrid_shortcuts_generic(
+ hybrid_mapping: Dict[int, Union[int, List[int]]],
+ zone_id_dtype: np.dtype,
+ output_file: Path,
+) -> None:
+ """Write hybrid shortcuts using the appropriate schema based on dtype."""
+ if zone_id_dtype.itemsize == 1:
+ # uint8 schema imports
+ schema = SchemaImports(
+ collection_start=HybridShortcutCollectionStartUint8,
+ collection_add_entries=HybridShortcutCollectionAddEntriesUint8,
+ collection_end=HybridShortcutCollectionEndUint8,
+ collection_start_entries_vector=HybridShortcutCollectionStartEntriesVectorUint8,
+ entry_start=HybridShortcutEntryStartUint8,
+ entry_add_hex_id=HybridShortcutEntryAddHexIdUint8,
+ entry_add_value_type=HybridShortcutEntryAddValueTypeUint8,
+ entry_add_value=HybridShortcutEntryAddValueUint8,
+ entry_end=HybridShortcutEntryEndUint8,
+ unique_zone_start=UniqueZoneStartUint8,
+ unique_zone_add_zone_id=UniqueZoneAddZoneIdUint8,
+ unique_zone_end=UniqueZoneEndUint8,
+ polygon_list_start=PolygonListStartUint8,
+ polygon_list_add_poly_ids=PolygonListAddPolyIdsUint8,
+ polygon_list_end=PolygonListEndUint8,
+ polygon_list_start_poly_ids_vector=PolygonListStartPolyIdsVectorUint8,
+ shortcut_value=ShortcutValueUint8,
+ max_zone_id=255,
+ dtype_name="uint8",
+ )
+ else:
+ # uint16 schema imports
+ schema = SchemaImports(
+ collection_start=HybridShortcutCollectionStartUint16,
+ collection_add_entries=HybridShortcutCollectionAddEntriesUint16,
+ collection_end=HybridShortcutCollectionEndUint16,
+ collection_start_entries_vector=HybridShortcutCollectionStartEntriesVectorUint16,
+ entry_start=HybridShortcutEntryStartUint16,
+ entry_add_hex_id=HybridShortcutEntryAddHexIdUint16,
+ entry_add_value_type=HybridShortcutEntryAddValueTypeUint16,
+ entry_add_value=HybridShortcutEntryAddValueUint16,
+ entry_end=HybridShortcutEntryEndUint16,
+ unique_zone_start=UniqueZoneStartUint16,
+ unique_zone_add_zone_id=UniqueZoneAddZoneIdUint16,
+ unique_zone_end=UniqueZoneEndUint16,
+ polygon_list_start=PolygonListStartUint16,
+ polygon_list_add_poly_ids=PolygonListAddPolyIdsUint16,
+ polygon_list_end=PolygonListEndUint16,
+ polygon_list_start_poly_ids_vector=PolygonListStartPolyIdsVectorUint16,
+ shortcut_value=ShortcutValueUint16,
+ max_zone_id=65535,
+ dtype_name="uint16",
+ )
+
+ _write_hybrid_shortcuts_with_schema(hybrid_mapping, output_file, schema)
+
+
+def _write_hybrid_shortcuts_with_schema(
+ hybrid_mapping: Dict[int, Union[int, List[int]]],
+ output_file: Path,
+ schema: SchemaImports,
+) -> None:
+ """Write hybrid shortcuts using the provided schema imports."""
+ builder = flatbuffers.Builder(0)
+ entry_offsets = []
+
+ # Validate zone IDs fit in dtype
+ for value in hybrid_mapping.values():
+ if isinstance(value, int) and value > schema.max_zone_id:
+ raise ValueError(
+ f"Zone ID {value} exceeds {schema.dtype_name} maximum ({schema.max_zone_id})"
+ )
+
+ for hex_id, value in hybrid_mapping.items():
+ if isinstance(value, int):
+ # Create UniqueZone with direct storage
+ schema.unique_zone_start(builder)
+ schema.unique_zone_add_zone_id(builder, value)
+ unique_zone_offset = schema.unique_zone_end(builder)
+
+ # Create entry with UniqueZone
+ schema.entry_start(builder)
+ schema.entry_add_hex_id(builder, hex_id)
+ schema.entry_add_value_type(builder, schema.shortcut_value.UniqueZone)
+ schema.entry_add_value(builder, unique_zone_offset)
+ entry_offset = schema.entry_end(builder)
+
+ else:
+ # Create PolygonList
+ poly_ids = list(value)
+ schema.polygon_list_start_poly_ids_vector(builder, len(poly_ids))
+ for i in range(len(poly_ids) - 1, -1, -1):
+ builder.PrependUint16(poly_ids[i])
+ poly_ids_vector = builder.EndVector()
+
+ schema.polygon_list_start(builder)
+ schema.polygon_list_add_poly_ids(builder, poly_ids_vector)
+ polygon_list_offset = schema.polygon_list_end(builder)
+
+ # Create entry with PolygonList
+ schema.entry_start(builder)
+ schema.entry_add_hex_id(builder, hex_id)
+ schema.entry_add_value_type(builder, schema.shortcut_value.PolygonList)
+ schema.entry_add_value(builder, polygon_list_offset)
+ entry_offset = schema.entry_end(builder)
+
+ entry_offsets.append(entry_offset)
+
+ # Create entries vector
+ schema.collection_start_entries_vector(builder, len(entry_offsets))
+ for offset in reversed(entry_offsets):
+ builder.PrependUOffsetTRelative(offset)
+ entries_vector = builder.EndVector()
+
+ # Create HybridShortcutCollection
+ schema.collection_start(builder)
+ schema.collection_add_entries(builder, entries_vector)
+ collection = schema.collection_end(builder)
+
+ builder.Finish(collection)
+
+ # Write to file
+ with open(output_file, "wb") as f:
+ f.write(builder.Output())
+
+
+def read_hybrid_shortcuts_binary(
+ file_path: Path,
+) -> Dict[int, Union[int, np.ndarray]]:
+ """
+ Read hybrid shortcut mapping from an optimized FlatBuffer binary file.
+
+ Auto-detects whether the file uses uint8 or uint16 schema based on filename.
+
+ Args:
+ file_path: Path to the hybrid shortcut FlatBuffer file
+
+ Returns:
+ Dictionary mapping H3 hexagon IDs to either:
+ - int: unique zone ID (when all polygons share same zone)
+ - np.ndarray: array of polygon IDs (when multiple zones)
+ """
+ # Determine schema type from filename and select appropriate imports
+ if "uint8" in file_path.name:
+ schema = ReadSchemaImports(
+ collection=HybridShortcutCollectionUint8,
+ unique_zone=UniqueZoneUint8,
+ polygon_list=PolygonListUint8,
+ shortcut_value=ShortcutValueUint8,
+ )
+ elif "uint16" in file_path.name:
+ schema = ReadSchemaImports(
+ collection=HybridShortcutCollectionUint16,
+ unique_zone=UniqueZoneUint16,
+ polygon_list=PolygonListUint16,
+ shortcut_value=ShortcutValueUint16,
+ )
+ else:
+ raise ValueError(
+ f"Cannot determine schema from filename: {file_path.name}. "
+ "Filename must include 'uint8' or 'uint16'."
+ )
+
+ return _read_hybrid_shortcuts_with_schema(file_path, schema)
+
+
+def _read_hybrid_shortcuts_with_schema(
+ file_path: Path, schema: ReadSchemaImports
+) -> Dict[int, Union[int, np.ndarray]]:
+ """Read hybrid shortcuts using the provided schema imports."""
+ with open(file_path, "rb") as f:
+ buf = f.read()
+
+ # mypy: GetRootAs is a class method on FlatBuffers classes
+ collection = schema.collection.GetRootAs(buf, 0) # type: ignore
+
+ hybrid_mapping: Dict[int, Union[int, np.ndarray]] = {}
+ for i in range(collection.EntriesLength()):
+ entry = collection.Entries(i)
+ hex_id = entry.HexId()
+
+ # Determine value type and extract data
+ value_type = entry.ValueType()
+ value = entry.Value()
+
+ if value_type == schema.shortcut_value.UniqueZone:
+ unique_zone = schema.unique_zone() # type: ignore
+ unique_zone.Init(value.Bytes, value.Pos)
+ zone_id = unique_zone.ZoneId() # Direct zone ID, no lookup needed
+ hybrid_mapping[hex_id] = int(zone_id)
+
+ elif value_type == schema.shortcut_value.PolygonList:
+ polygon_list = schema.polygon_list() # type: ignore
+ polygon_list.Init(value.Bytes, value.Pos)
+ poly_ids = polygon_list.PolyIdsAsNumpy()
+ hybrid_mapping[hex_id] = poly_ids
+
+ else:
+ raise ValueError(f"Unknown ShortcutValue type: {value_type}")
+
+ return hybrid_mapping
diff --git a/frogpilot/third_party/timezonefinder/flatbuf/io/polygons.py b/frogpilot/third_party/timezonefinder/flatbuf/io/polygons.py
new file mode 100644
index 00000000..d966fdf0
--- /dev/null
+++ b/frogpilot/third_party/timezonefinder/flatbuf/io/polygons.py
@@ -0,0 +1,132 @@
+import flatbuffers
+import mmap
+import numpy as np
+from pathlib import Path
+from typing import List, Union
+
+from timezonefinder.configs import DEFAULT_DATA_DIR
+from timezonefinder.flatbuf.generated.polygons.Polygon import (
+ PolygonStart,
+ PolygonEnd,
+ PolygonAddCoords,
+ PolygonStartCoordsVector,
+)
+from timezonefinder.flatbuf.generated.polygons.PolygonCollection import (
+ PolygonCollection,
+ PolygonCollectionStart,
+ PolygonCollectionEnd,
+ PolygonCollectionAddPolygons,
+ PolygonCollectionStartPolygonsVector,
+)
+
+
+def flatten_polygon_coords(polygon: np.ndarray) -> np.ndarray:
+ """Convert polygon coordinates from shape (2, N) to a flattened [x0, y0, x1, y1, ...] array.
+
+ Args:
+ polygon: Array of polygon coordinates with shape (2, N)
+ where the first row contains x coordinates and the second row contains y coordinates
+
+ Returns:
+ Flattened 1D array of coordinates in the format [x0, y0, x1, y1, ...]
+ """
+ return polygon.ravel(order="F")
+
+
+def reshape_to_polygon_coords(coords: np.ndarray) -> np.ndarray:
+ """Reshape flattened coordinates to the format (2, N).
+
+ Args:
+ coords: Flattened 1D array of coordinates in the format [x0, y0, x1, y1, ...]
+
+ Returns:
+ Array of polygon coordinates with shape (2, N)
+ where the first row contains x coordinates and the second row contains y coordinates
+ """
+ return coords.reshape(2, -1, order="F")
+
+
+def get_coordinate_path(data_dir: Path = DEFAULT_DATA_DIR) -> Path:
+ """Return the path to the boundaries flatbuffer file."""
+ return data_dir / "coordinates.fbs"
+
+
+def write_polygon_collection_flatbuffer(
+ file_path: Path, polygons: List[np.ndarray]
+) -> None:
+ """Write a collection of polygons to a flatbuffer file using a single coordinate vector.
+
+ Args:
+ file_path: Path to save the flatbuffer file
+ polygons: List of polygon coordinates as numpy arrays with shape (2, N)
+ where the first row contains x coordinates and the second row contains y coordinates
+
+ Returns:
+ None
+ """
+ print(f"writing {len(polygons)} polygons to binary file {file_path}")
+ builder = flatbuffers.Builder(0)
+ polygon_offsets = []
+
+ # Create each polygon and store its offset
+ for polygon in polygons:
+ # Flatten coordinates to [x0, y0, x1, y1, ...] format
+ coords = flatten_polygon_coords(polygon)
+
+ # Create coords vector
+ PolygonStartCoordsVector(builder, len(coords))
+ for coord in reversed(coords):
+ builder.PrependInt32(int(coord)) # Use signed 32-bit integer
+ coords_offset = builder.EndVector()
+
+ # Create polygon
+ PolygonStart(builder)
+ PolygonAddCoords(builder, coords_offset) # Use Coords for combined vector
+ polygon_offsets.append(PolygonEnd(builder))
+
+ # Create polygon vector
+ PolygonCollectionStartPolygonsVector(builder, len(polygon_offsets))
+ for offset in reversed(polygon_offsets):
+ builder.PrependUOffsetTRelative(offset)
+ polygons_offset = builder.EndVector()
+
+ # Create root table
+ PolygonCollectionStart(builder)
+ PolygonCollectionAddPolygons(builder, polygons_offset)
+ collection_offset = PolygonCollectionEnd(builder)
+
+ # Finish buffer
+ builder.Finish(collection_offset)
+
+ # Write to file
+ with open(file_path, "wb") as f:
+ buf = builder.Output()
+ f.write(buf)
+
+
+def get_polygon_collection(buf: Union[bytes, mmap.mmap]) -> PolygonCollection:
+ """Load a PolygonCollection from a file path.
+
+ Args:
+ buf: A binary stream or memory-mapped file containing the flatbuffer data.
+
+ Returns: PolygonCollection
+ """
+ return PolygonCollection.GetRootAs(buf, 0)
+
+
+def read_polygon_array_from_binary(
+ poly_collection: PolygonCollection, idx: int
+) -> np.ndarray:
+ """Read a polygon's coordinates from a FlatBuffers collection."""
+ # value checks not required as this is a private function
+ # processed polygon indices are expected to be in range
+ # nr_polygons = collection.PolygonsLength()
+ # if idx >= nr_polygons:
+ # raise IndexError(
+ # f"Index {idx} out of bounds for collection with {nr_polygons} polygons."
+ # )
+ poly = poly_collection.Polygons(idx)
+ coords = poly.CoordsAsNumpy() # flat 1D array of coordinates
+ # Reshape to (2, N) format
+ return reshape_to_polygon_coords(coords)
diff --git a/frogpilot/third_party/timezonefinder/flatbuf/schemas/__init__.py b/frogpilot/third_party/timezonefinder/flatbuf/schemas/__init__.py
new file mode 100644
index 00000000..b90805ec
--- /dev/null
+++ b/frogpilot/third_party/timezonefinder/flatbuf/schemas/__init__.py
@@ -0,0 +1,7 @@
+"""FlatBuffer schema definitions for timezonefinder."""
+
+__all__ = [
+ "polygons",
+ "hybrid_shortcuts_uint8",
+ "hybrid_shortcuts_uint16",
+]
diff --git a/frogpilot/third_party/timezonefinder/flatbuf/schemas/hybrid_shortcuts_uint16.fbs b/frogpilot/third_party/timezonefinder/flatbuf/schemas/hybrid_shortcuts_uint16.fbs
new file mode 100644
index 00000000..ff4558f7
--- /dev/null
+++ b/frogpilot/third_party/timezonefinder/flatbuf/schemas/hybrid_shortcuts_uint16.fbs
@@ -0,0 +1,37 @@
+namespace timezonefinder.flatbuf.generated.shortcuts_uint16;
+
+// Table representing a unique zone ID (when all polygons in hex share same zone)
+// Optimized for uint16 zone IDs (0-65535)
+table UniqueZone {
+ // Direct zone ID storage - 2 bytes per zone ID
+ zone_id: ushort;
+}
+
+// Table representing a list of polygon IDs (when hex contains multiple zones)
+table PolygonList {
+ // List of polygon IDs (fixed uint16 as per requirement)
+ poly_ids: [ushort];
+}
+
+// Union type for the shortcut value - either unique zone or polygon list
+union ShortcutValue {
+ UniqueZone,
+ PolygonList
+}
+
+// Individual shortcut entry mapping H3 hex ID to its value
+table HybridShortcutEntry {
+ // H3 hexagon ID (uint64)
+ hex_id: ulong;
+ // The value for this hex - either unique zone or polygon list
+ value: ShortcutValue;
+}
+
+// Root collection containing all shortcut entries
+// Optimized for uint16 zone IDs - no metadata overhead
+table HybridShortcutCollection {
+ // All shortcut entries
+ entries: [HybridShortcutEntry];
+}
+
+root_type HybridShortcutCollection;
diff --git a/frogpilot/third_party/timezonefinder/flatbuf/schemas/hybrid_shortcuts_uint8.fbs b/frogpilot/third_party/timezonefinder/flatbuf/schemas/hybrid_shortcuts_uint8.fbs
new file mode 100644
index 00000000..6551cfe8
--- /dev/null
+++ b/frogpilot/third_party/timezonefinder/flatbuf/schemas/hybrid_shortcuts_uint8.fbs
@@ -0,0 +1,37 @@
+namespace timezonefinder.flatbuf.generated.shortcuts_uint8;
+
+// Table representing a unique zone ID (when all polygons in hex share same zone)
+// Optimized for uint8 zone IDs (0-255)
+table UniqueZone {
+ // Direct zone ID storage - 1 byte per zone ID
+ zone_id: ubyte;
+}
+
+// Table representing a list of polygon IDs (when hex contains multiple zones)
+table PolygonList {
+ // List of polygon IDs (fixed uint16 as per requirement)
+ poly_ids: [ushort];
+}
+
+// Union type for the shortcut value - either unique zone or polygon list
+union ShortcutValue {
+ UniqueZone,
+ PolygonList
+}
+
+// Individual shortcut entry mapping H3 hex ID to its value
+table HybridShortcutEntry {
+ // H3 hexagon ID (uint64)
+ hex_id: ulong;
+ // The value for this hex - either unique zone or polygon list
+ value: ShortcutValue;
+}
+
+// Root collection containing all shortcut entries
+// Optimized for uint8 zone IDs - no metadata overhead
+table HybridShortcutCollection {
+ // All shortcut entries
+ entries: [HybridShortcutEntry];
+}
+
+root_type HybridShortcutCollection;
diff --git a/frogpilot/third_party/timezonefinder/flatbuf/schemas/polygons.fbs b/frogpilot/third_party/timezonefinder/flatbuf/schemas/polygons.fbs
new file mode 100644
index 00000000..831b0afd
--- /dev/null
+++ b/frogpilot/third_party/timezonefinder/flatbuf/schemas/polygons.fbs
@@ -0,0 +1,12 @@
+namespace timezonefinder.flatbuf.generated.polygons;
+
+table Polygon {
+ // combined x and y coordinates stored as a single vector
+ coords:[int];
+}
+
+table PolygonCollection {
+ polygons:[Polygon];
+}
+
+root_type PolygonCollection;
diff --git a/frogpilot/third_party/timezonefinder/global_functions.py b/frogpilot/third_party/timezonefinder/global_functions.py
new file mode 100644
index 00000000..95778c3f
--- /dev/null
+++ b/frogpilot/third_party/timezonefinder/global_functions.py
@@ -0,0 +1,126 @@
+"""
+This module provides global functions that use a singleton instance of TimezoneFinder.
+
+Note on thread safety: These global functions are not thread-safe. If you need to use
+TimezoneFinder in a multi-threaded environment, create separate TimezoneFinder instances
+for each thread.
+"""
+
+from typing import List, Optional, Union
+
+from timezonefinder.timezonefinder import TimezoneFinder
+from timezonefinder.configs import CoordPairs, CoordLists
+
+# Use a global variable to store the singleton instance
+TF_INSTANCE: TimezoneFinder
+
+
+def _get_tf_instance() -> TimezoneFinder:
+ """Get or create the global TimezoneFinder instance
+
+ Lazy initialization: delayed memory allocation until actually needed
+ required because, the package might be used with a user defined instance
+ and duplicate initialisation overhead must be avoided!
+ """
+ global TF_INSTANCE
+ try:
+ return TF_INSTANCE
+ except NameError:
+ # If TF_INSTANCE is not defined, create it
+ TF_INSTANCE = TimezoneFinder()
+ return TF_INSTANCE
+
+
+def timezone_at(*, lng: float, lat: float) -> Optional[str]:
+ """
+ Looks up in which timezone the given coordinate is included in.
+ Uses the global TimezoneFinder instance.
+
+ Note: This function is not thread-safe. For multi-threaded environments,
+ create separate TimezoneFinder instances.
+
+ :param lng: longitude of the point in degree (-180.0 to 180.0)
+ :param lat: latitude in degree (90.0 to -90.0)
+ :return: the timezone name of a matching polygon or None
+ """
+ return _get_tf_instance().timezone_at(lng=lng, lat=lat)
+
+
+def timezone_at_land(*, lng: float, lat: float) -> Optional[str]:
+ """
+ Computes in which land timezone a point is included in.
+ Uses the global TimezoneFinder instance.
+
+ Note: This function is not thread-safe. For multi-threaded environments,
+ create separate TimezoneFinder instances.
+
+ :param lng: longitude of the point in degree (-180.0 to 180.0)
+ :param lat: latitude in degree (90.0 to -90.0)
+ :return: the timezone name of a matching polygon or
+ ``None`` when an ocean timezone ("Etc/GMT+-XX") has been matched.
+ """
+ return _get_tf_instance().timezone_at_land(lng=lng, lat=lat)
+
+
+def unique_timezone_at(*, lng: float, lat: float) -> Optional[str]:
+ """
+ Returns the name of a unique zone within the corresponding shortcut.
+ Uses the global TimezoneFinder instance.
+
+ Note: This function is not thread-safe. For multi-threaded environments,
+ create separate TimezoneFinder instances.
+
+ :param lng: longitude of the point in degree (-180.0 to 180.0)
+ :param lat: latitude in degree (90.0 to -90.0)
+ :return: the timezone name of the unique zone or ``None`` if there are no or multiple zones in this shortcut
+ """
+ return _get_tf_instance().unique_timezone_at(lng=lng, lat=lat)
+
+
+def certain_timezone_at(*, lng: float, lat: float) -> Optional[str]:
+ """
+ Checks in which timezone polygon the point is certainly included in.
+ Uses the global TimezoneFinder instance.
+
+ Note: This function is not thread-safe. For multi-threaded environments,
+ create separate TimezoneFinder instances.
+
+ .. note:: this is only meaningful when you have compiled your own timezone data
+ where there are areas without timezone polygon coverage.
+ Otherwise, some timezone will always be matched and the functionality is equal to using `.timezone_at()`
+ -> useless to actually test all polygons.
+
+ .. note:: using this function is less performant than `.timezone_at()`
+
+ :param lng: longitude of the point in degree
+ :param lat: latitude in degree
+ :return: the timezone name of the polygon the point is included in or `None`
+ """
+ return _get_tf_instance().certain_timezone_at(lng=lng, lat=lat)
+
+
+def get_geometry(
+ tz_name: Optional[str] = "",
+ tz_id: Optional[int] = 0,
+ use_id: bool = False,
+ coords_as_pairs: bool = False,
+) -> List[List[Union[CoordPairs, CoordLists]]]:
+ """
+ Retrieves the geometry of a timezone polygon.
+ Uses the global TimezoneFinder instance.
+
+ Note: This function is not thread-safe. For multi-threaded environments,
+ create separate TimezoneFinder instances.
+
+ :param tz_name: one of the names in ``timezone_names.json`` or ``self.timezone_names``
+ :param tz_id: the id of the timezone (=index in ``self.timezone_names``)
+ :param use_id: if ``True`` uses ``tz_id`` instead of ``tz_name``
+ :param coords_as_pairs: determines the structure of the polygon representation
+ :return: a data structure representing the multipolygon of this timezone
+ output format: ``[ [polygon1, hole1, hole2...], [polygon2, ...], ...]``
+ and each polygon and hole is itself formatted like: ``([longitudes], [latitudes])``
+ or ``[(lng1,lat1), (lng2,lat2),...]`` if ``coords_as_pairs=True``.
+ """
+ return _get_tf_instance().get_geometry(
+ tz_name=tz_name, tz_id=tz_id, use_id=use_id, coords_as_pairs=coords_as_pairs
+ )
diff --git a/frogpilot/third_party/timezonefinder/inside_poly_extension/inside_polygon_int.c b/frogpilot/third_party/timezonefinder/inside_poly_extension/inside_polygon_int.c
new file mode 100644
index 00000000..67defef2
--- /dev/null
+++ b/frogpilot/third_party/timezonefinder/inside_poly_extension/inside_polygon_int.c
@@ -0,0 +1,69 @@
+#include "inside_polygon_int.h"
+#include
+
+bool inside_polygon_int(int x, int y, int nr_coords, int x_coords[],
+ int y_coords[]) {
+ // naive implementation, vulnerable to overflow:
+ // bool inside;
+ // for (int i = 0, j = nr_coords - 1; i < nr_coords; j = i++) {
+ // if (((y_coords[i] > y) != (y_coords[j] > y)) &&
+ // (x < (x_coords[j] - x_coords[i]) * (y - y_coords[i]) /
+ // (y_coords[j] - y_coords[i]) +
+ // x_coords[i])) {
+ // inside = !inside;
+ // }
+ // }
+ // return inside;
+
+ bool inside, y_gt_y1, y_gt_y2, x_le_x1, x_le_x2;
+ long y1, y2, x1, x2, slope1, slope2; // int64 precision
+ int i, j;
+
+ inside = false;
+ // the edge from the last to the first point is checked first
+ j = nr_coords - 1;
+ y_gt_y1 = y > y_coords[j];
+ for (i = 0; i < nr_coords; j = i++) {
+ y_gt_y2 = y > y_coords[i];
+ if (y_gt_y1 ^ y_gt_y2) { // XOR
+ // [p1-p2] crosses horizontal line in p
+ // only count crossings "right" of the point ( >= x)
+ x_le_x1 = x <= x_coords[j];
+ x_le_x2 = x <= x_coords[i];
+ if (x_le_x1 || x_le_x2) {
+ if (x_le_x1 && x_le_x2) {
+ // p1 and p2 are both to the right -> valid crossing
+ inside = !inside;
+ } else {
+ // compare the slope of the line [p1-p2] and [p-p2]
+ // depending on the position of p2 this determines whether
+ // the polygon edge is right or left of the point
+ // to avoid expensive division the divisors (of the slope dy/dx)
+ // are brought to the other side ( dy/dx > a == dy > a * dx )
+ // only one of the points is to the right
+ // NOTE: int64 precision required to prevent overflow
+ y1 = y_coords[j];
+ y2 = y_coords[i];
+ x1 = x_coords[j];
+ x2 = x_coords[i];
+ slope1 = (y2 - y) * (x2 - x1);
+ slope2 = (y2 - y1) * (x2 - x);
+ // NOTE: accept slope equality to also detect if p lies directly
+ // on an edge
+ if (y_gt_y1) {
+ if (slope1 <= slope2) {
+ inside = !inside;
+ }
+ } else { // NOT y_gt_y1
+ if (slope1 >= slope2) {
+ inside = !inside;
+ }
+ }
+ }
+ }
+ }
+ // next point
+ y_gt_y1 = y_gt_y2;
+ }
+ return inside;
+}
diff --git a/frogpilot/third_party/timezonefinder/inside_poly_extension/inside_polygon_int.h b/frogpilot/third_party/timezonefinder/inside_poly_extension/inside_polygon_int.h
new file mode 100644
index 00000000..d536fe31
--- /dev/null
+++ b/frogpilot/third_party/timezonefinder/inside_poly_extension/inside_polygon_int.h
@@ -0,0 +1,4 @@
+#include
+
+bool inside_polygon_int(int x, int y, int nr_coords, int x_coords[],
+ int y_coords[]);
diff --git a/frogpilot/third_party/timezonefinder/np_binary_helpers.py b/frogpilot/third_party/timezonefinder/np_binary_helpers.py
new file mode 100644
index 00000000..dbd93660
--- /dev/null
+++ b/frogpilot/third_party/timezonefinder/np_binary_helpers.py
@@ -0,0 +1,49 @@
+"""
+Utility functions for handling .npy numpy binary files related to timezone data.
+"""
+
+from pathlib import Path
+
+import numpy as np
+
+
+def get_zone_ids_path(path: Path) -> Path:
+ """Return the path to the zone_ids.npy file in the given directory."""
+ return path / "zone_ids.npy"
+
+
+def get_zone_positions_path(path: Path) -> Path:
+ """Return the path to the zone_positions.npy file in the given directory."""
+ return path / "zone_positions.npy"
+
+
+def get_xmax_path(path: Path) -> Path:
+ """Return the path to the xmax.npy file in the given directory."""
+ return path / "xmax.npy"
+
+
+def get_xmin_path(path: Path) -> Path:
+ """Return the path to the xmin.npy file in the given directory."""
+ return path / "xmin.npy"
+
+
+def get_ymax_path(path: Path) -> Path:
+ """Return the path to the ymax.npy file in the given directory."""
+ return path / "ymax.npy"
+
+
+def get_ymin_path(path: Path) -> Path:
+ """Return the path to the ymin.npy file in the given directory."""
+ return path / "ymin.npy"
+
+
+def store_per_polygon_vector(file_path: Path, vector: np.ndarray) -> None:
+ """Store a vector as a .npy file in the specified file path."""
+ print(f"Storing vector to {file_path}")
+ np.save(file_path, vector)
+
+
+def read_per_polygon_vector(file_path: Path) -> np.ndarray:
+ """Read a vector from a .npy file in the specified file path."""
+ vector = np.load(file_path)
+ return vector
diff --git a/frogpilot/third_party/timezonefinder/polygon_array.py b/frogpilot/third_party/timezonefinder/polygon_array.py
new file mode 100644
index 00000000..3c824bb5
--- /dev/null
+++ b/frogpilot/third_party/timezonefinder/polygon_array.py
@@ -0,0 +1,140 @@
+from pathlib import Path
+from typing import Iterable, Union
+
+import numpy as np
+
+from timezonefinder.configs import IntegerLike
+
+from timezonefinder import utils
+from timezonefinder.coord_accessors import AbstractCoordAccessor, create_coord_accessor
+from timezonefinder.flatbuf.io.polygons import (
+ get_coordinate_path,
+)
+from timezonefinder.np_binary_helpers import (
+ get_xmax_path,
+ get_xmin_path,
+ get_ymax_path,
+ get_ymin_path,
+ read_per_polygon_vector,
+)
+
+
+class PolygonArray:
+ xmin: np.ndarray
+ xmax: np.ndarray
+ ymin: np.ndarray
+ ymax: np.ndarray
+ coordinates: AbstractCoordAccessor
+
+ def __init__(
+ self,
+ data_location: Union[str, Path],
+ in_memory: bool = False,
+ ):
+ """
+ Initialize the PolygonArray.
+ :param data_location: The path to the binary data files to use.
+ :param in_memory: Whether to completely read and keep the coordinate data in memory as numpy.
+ """
+ self.in_memory = in_memory
+ self.data_location: Path = Path(data_location)
+
+ xmin_path = get_xmin_path(self.data_location)
+ xmax_path = get_xmax_path(self.data_location)
+ ymin_path = get_ymin_path(self.data_location)
+ ymax_path = get_ymax_path(self.data_location)
+
+ # read all per polygon vectors directly into memory (no matter the memory mode)
+ self.xmin = read_per_polygon_vector(xmin_path)
+ self.xmax = read_per_polygon_vector(xmax_path)
+ self.ymin = read_per_polygon_vector(ymin_path)
+ self.ymax = read_per_polygon_vector(ymax_path)
+
+ coordinate_file_path = get_coordinate_path(self.data_location)
+ # Initialize the appropriate coordinate accessor based on memory mode
+ self.coordinates = create_coord_accessor(coordinate_file_path, self.in_memory)
+
+ def __del__(self):
+ """Clean up resources when the object is destroyed."""
+ del self.coordinates
+ del self.xmin
+ del self.xmax
+ del self.ymin
+ del self.ymax
+
+ def __len__(self) -> int:
+ """
+ Get the number of polygons in the collection.
+ :return: Number of polygons
+ """
+ return len(self.xmin)
+
+ def outside_bbox(self, poly_id: IntegerLike, x: int, y: int) -> bool:
+ """
+ Check if a point is outside the bounding box of a polygon.
+
+ :param poly_id: Polygon ID
+ :param x: X-coordinate of the point
+ :param y: Y-coordinate of the point
+ :return: True if the point is outside the boundaries, False otherwise
+ """
+ if x > self.xmax[poly_id]:
+ return True
+ if x < self.xmin[poly_id]:
+ return True
+ if y > self.ymax[poly_id]:
+ return True
+ if y < self.ymin[poly_id]:
+ return True
+ return False
+
+ def coords_of(self, idx: IntegerLike) -> np.ndarray:
+ """
+ Get the polygon coordinates for the given index.
+
+ Args:
+ idx: The polygon index
+
+ Returns:
+ A numpy array containing the polygon coordinates
+ """
+ return self.coordinates[idx]
+
+ def pip(self, poly_id: IntegerLike, x: int, y: int) -> bool:
+ """
+ Point in polygon (PIP) test.
+
+ :param poly_id: Polygon ID
+ :param x: X-coordinate of the point
+ :param y: Y-coordinate of the point
+ :return: True if the point is inside the polygon, False otherwise
+ """
+ polygon = self.coords_of(poly_id)
+ return utils.inside_polygon(x, y, polygon)
+
+ def pip_with_bbox_check(self, poly_id: IntegerLike, x: int, y: int) -> bool:
+ """
+ Point in polygon (PIP) test with bounding box check.
+
+ :param poly_id: Polygon ID
+ :param x: X-coordinate of the point
+ :param y: Y-coordinate of the point
+ :return: True if the point is inside the polygon, False otherwise
+ """
+ if self.outside_bbox(poly_id, x, y):
+ return False
+ return self.pip(poly_id, x, y)
+
+ def in_any_polygon(self, poly_ids: Iterable[int], x: int, y: int) -> bool:
+ """
+ Check if a point is inside any of the specified polygons.
+
+ :param poly_ids: An iterable of polygon IDs
+ :param x: X-coordinate of the point
+ :param y: Y-coordinate of the point
+ :return: True if the point is inside any polygon, False otherwise
+ """
+ for poly_id in poly_ids:
+ if self.pip_with_bbox_check(poly_id, x, y):
+ return True
+ return False
diff --git a/frogpilot/third_party/timezonefinder/py.typed b/frogpilot/third_party/timezonefinder/py.typed
new file mode 100644
index 00000000..e69de29b
diff --git a/frogpilot/third_party/timezonefinder/timezonefinder.py b/frogpilot/third_party/timezonefinder/timezonefinder.py
new file mode 100644
index 00000000..29787977
--- /dev/null
+++ b/frogpilot/third_party/timezonefinder/timezonefinder.py
@@ -0,0 +1,590 @@
+import json
+from abc import ABC, abstractmethod
+from pathlib import Path
+from typing import Dict, Iterable, List, Optional, Tuple, Union
+import numpy as np
+from h3.api import numpy_int as h3
+
+from timezonefinder.np_binary_helpers import (
+ get_zone_ids_path,
+ get_zone_positions_path,
+ read_per_polygon_vector,
+)
+from timezonefinder.polygon_array import PolygonArray
+from timezonefinder import utils, utils_clang
+from timezonefinder.configs import (
+ DEFAULT_DATA_DIR,
+ SHORTCUT_H3_RES,
+ CoordLists,
+ CoordPairs,
+ IntegerLike,
+)
+
+from timezonefinder.flatbuf.io.hybrid_shortcuts import (
+ get_hybrid_shortcut_file_path,
+ read_hybrid_shortcuts_binary,
+)
+from timezonefinder.zone_names import read_zone_names
+
+
+class AbstractTimezoneFinder(ABC):
+ # prevent dynamic attribute assignment (-> safe memory)
+ """
+ Abstract base class for TimezoneFinder instances
+ """
+
+ __slots__ = [
+ "data_location",
+ "shortcut_mapping",
+ "in_memory",
+ "_fromfile",
+ "timezone_names",
+ "zone_ids",
+ "holes_dir",
+ "boundaries_dir",
+ "boundaries",
+ "holes",
+ ]
+
+ zone_ids: np.ndarray
+ shortcut_mapping: Dict[int, Union[int, np.ndarray]]
+ """
+ List of attribute names that store opened binary data files.
+ """
+
+ def __init__(
+ self,
+ bin_file_location: Optional[Union[str, Path]] = None,
+ in_memory: bool = False,
+ ):
+ """
+ Initialize the AbstractTimezoneFinder.
+ :param bin_file_location: The path to the binary data files to use. If None, uses native package data.
+ :param in_memory: ignored. All binary files will be read into memory (few MB). Only used for polygon coordinate data.
+ """
+ if bin_file_location is None:
+ bin_file_location = DEFAULT_DATA_DIR
+ self.data_location: Path = Path(bin_file_location)
+
+ self.timezone_names = read_zone_names(self.data_location)
+
+ # Load hybrid shortcut file - contains both zone IDs (for unique zones) and polygon arrays (for ambiguous zones)
+ zone_ids_path = get_zone_ids_path(self.data_location)
+ zone_ids_temp = read_per_polygon_vector(zone_ids_path)
+ zone_id_dtype = zone_ids_temp.dtype
+
+ path2shortcut = get_hybrid_shortcut_file_path(zone_id_dtype, self.data_location)
+ self.shortcut_mapping = read_hybrid_shortcuts_binary(path2shortcut)
+
+ zone_ids_path = get_zone_ids_path(self.data_location)
+ self.zone_ids = read_per_polygon_vector(zone_ids_path)
+
+ def _iter_boundary_ids_of_zone(self, zone_id: int) -> Iterable[int]:
+ """
+ Yield the boundary polygon IDs for a given zone ID.
+
+ :param zone_id: ID of the zone
+ :yield: boundary polygon IDs
+ """
+ # load only on demand. used when shortcuts contain zone IDs (hybrid optimization)
+ zone_positions_path = get_zone_positions_path(self.data_location)
+ zone_positions = np.load(zone_positions_path, mmap_mode="r")
+ first_boundary_id_zone = zone_positions[zone_id]
+ # read the id of the first boundary polygon of the consequent zone
+ # NOTE: this has also been added for the last zone
+ first_boundary_id_next = zone_positions[zone_id + 1]
+ yield from range(first_boundary_id_zone, first_boundary_id_next)
+
+ @property
+ def nr_of_zones(self) -> int:
+ """
+ Get the number of timezones.
+
+ :rtype: int
+ """
+ return len(self.timezone_names)
+
+ @staticmethod
+ def using_numba() -> bool:
+ """
+ Check if Numba is being used.
+
+ :rtype: bool
+ :return: True if Numba is being used to JIT compile helper functions
+ """
+ return utils.using_numba
+
+ @staticmethod
+ def using_clang_pip() -> bool:
+ """
+ :return: True if the compiled C implementation of the point in polygon algorithm is being used
+ """
+ return utils.inside_polygon == utils_clang.pt_in_poly_clang
+
+ def zone_id_of(self, boundary_id: IntegerLike) -> int:
+ """
+ Get the zone ID of a polygon.
+
+ :param boundary_id: The ID of the polygon.
+ :type boundary_id: int
+ :rtype: int
+ """
+ try:
+ return int(self.zone_ids[boundary_id])
+ except TypeError:
+ raise ValueError(f"zone_ids is not set in directory {self.data_location}.")
+
+ def zone_ids_of(self, boundary_ids: np.ndarray) -> np.ndarray:
+ """
+ Get the zone IDs of multiple boundary polygons.
+
+ :param boundary_ids: An array of boundary polygon IDs.
+ :return: array of corresponding timezone IDs.
+ """
+ return self.zone_ids[boundary_ids]
+
+ def zone_name_from_id(self, zone_id: int) -> str:
+ """
+ Get the zone name from a zone ID.
+
+ :param zone_id: The ID of the zone.
+ :return: The name of the zone.
+ :raises ValueError: If the timezone could not be found.
+ """
+ try:
+ return self.timezone_names[zone_id]
+ except IndexError:
+ raise ValueError("timezone could not be found. index error.")
+
+ def zone_name_from_boundary_id(self, boundary_id: IntegerLike) -> str:
+ """
+ Get the zone name from a boundary polygon ID.
+
+ :param boundary_id: The ID of the boundary polygon.
+ :return: The name of the zone.
+ """
+ zone_id = self.zone_id_of(boundary_id)
+ return self.zone_name_from_id(zone_id)
+
+ def _iter_boundaries_in_shortcut(self, *, lng: float, lat: float) -> Iterable[int]:
+ """
+ Iterate over boundary polygon IDs in the shortcut corresponding to the given coordinates.
+
+ :param lng: The longitude of the point in degrees (-180.0 to 180.0).
+ :param lat: The latitude of the point in degrees (90.0 to -90.0).
+ :yield: Boundary polygon IDs.
+ """
+ hex_id = h3.latlng_to_cell(lat, lng, SHORTCUT_H3_RES)
+
+ # Handle shortcuts (hybrid structure) - if it's a zone ID, get all polygons for that zone
+ shortcut_value = self.shortcut_mapping.get(hex_id)
+ if shortcut_value is None:
+ return
+ elif isinstance(shortcut_value, int):
+ # Zone ID - get all boundary polygons for this zone
+ # Most polygons will be quickly ruled out by bbox check
+ yield from self._iter_boundary_ids_of_zone(shortcut_value)
+ else:
+ # Polygon array
+ yield from shortcut_value
+
+ @abstractmethod
+ def timezone_at(self, *, lng: float, lat: float) -> Optional[str]:
+ """looks up in which timezone the given coordinate is included in
+
+ :param lng: longitude of the point in degree (-180.0 to 180.0)
+ :param lat: latitude in degree (90.0 to -90.0)
+ :return: the timezone name of a matching polygon or None
+ """
+ ...
+
+ def timezone_at_land(self, *, lng: float, lat: float) -> Optional[str]:
+ """computes in which land timezone a point is included in
+
+ Especially for large polygons it is expensive to check if a point is really included.
+ To speed things up there are "shortcuts" being used (stored in a binary file),
+ which have been precomputed and store which timezone polygons have to be checked.
+
+ :param lng: longitude of the point in degree (-180.0 to 180.0)
+ :param lat: latitude in degree (90.0 to -90.0)
+ :return: the timezone name of a matching polygon or
+ ``None`` when an ocean timezone ("Etc/GMT+-XX") has been matched.
+ """
+ tz_name = self.timezone_at(lng=lng, lat=lat)
+ if tz_name is not None and utils.is_ocean_timezone(tz_name):
+ return None
+ return tz_name
+
+ def unique_timezone_at(self, *, lng: float, lat: float) -> Optional[str]:
+ """returns the name of a unique zone within the corresponding shortcut
+
+ :param lng: longitude of the point in degree (-180.0 to 180.0)
+ :param lat: latitude in degree (90.0 to -90.0)
+ :return: the timezone name of the unique zone or ``None`` if there are no or multiple zones in this shortcut
+ """
+ lng, lat = utils.validate_coordinates(lng, lat)
+ hex_id = h3.latlng_to_cell(lat, lng, SHORTCUT_H3_RES)
+
+ # Shortcuts behavior (hybrid structure with precomputed uniqueness)
+ shortcut_value = self.shortcut_mapping.get(hex_id)
+ if shortcut_value is None:
+ return None
+ elif isinstance(shortcut_value, int):
+ # Zone ID - this is a precomputed unique zone
+ unique_id = shortcut_value
+ else:
+ # Polygon array - by definition not unique (would be stored as int if unique)
+ return None
+
+ return self.zone_name_from_id(unique_id)
+
+ def cleanup(self) -> None:
+ """Clean up resources. Override in subclasses as needed."""
+ pass
+
+ def __enter__(self):
+ """Enter the runtime context for the TimezoneFinder."""
+ return self
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ """Exit the runtime context and clean up resources."""
+ self.cleanup()
+ return False
+
+
+class TimezoneFinderL(AbstractTimezoneFinder):
+ """a 'light' version of the TimezoneFinder class for quickly suggesting a timezone for a point on earth
+
+ Instead of using timezone polygon data like ``TimezoneFinder``,
+ this class only uses a precomputed 'shortcut' to suggest a probable result:
+ the most common zone in a rectangle of a half degree of latitude and one degree of longitude
+ """
+
+ def __init__(
+ self, bin_file_location: Optional[str] = None, in_memory: bool = False
+ ):
+ super().__init__(bin_file_location, in_memory)
+
+ def timezone_at(self, *, lng: float, lat: float) -> Optional[str]:
+ """instantly returns the name of the most common zone within the corresponding shortcut
+
+ Note: 'most common' in this context means that the boundary polygons with the most coordinates in sum
+ occurring in the corresponding shortcut belong to this zone.
+
+ :param lng: longitude of the point in degree (-180.0 to 180.0)
+ :param lat: latitude in degree (90.0 to -90.0)
+ :return: the timezone name of the most common zone or None if there are no timezone polygons in this shortcut
+ """
+ lng, lat = utils.validate_coordinates(lng, lat)
+ # Inline fast-path to minimize helper overhead
+ hex_id = h3.latlng_to_cell(lat, lng, SHORTCUT_H3_RES)
+
+ shortcut_value = self.shortcut_mapping.get(hex_id)
+ if shortcut_value is None:
+ return None
+ elif isinstance(shortcut_value, int):
+ # Zone ID - unique zone case
+ return self.zone_name_from_id(shortcut_value)
+ else:
+ # Polygon array - get the last polygon (most common zone)
+ if len(shortcut_value) == 0:
+ return None
+ poly_of_biggest_zone = shortcut_value[-1]
+ # poly_of_biggest_zone is a numpy scalar from array indexing, but mypy sees it as ndarray
+ # This is safe: array element access returns a numpy integer scalar compatible with IntegerLike
+ most_common_id = self.zone_id_of(poly_of_biggest_zone) # type: ignore[arg-type]
+ return self.zone_name_from_id(most_common_id)
+
+
+class TimezoneFinder(AbstractTimezoneFinder):
+ """Class for quickly finding the timezone of a point on earth offline.
+
+ Because of indexing ("shortcuts"), not all timezone polygons have to be tested during a query.
+
+ Opens the required timezone polygon data in binary files to enable fast access.
+ For a detailed documentation of data management please refer to the code documentation of
+ `file_converter.py `__
+
+ :ivar binary_data_attributes: the names of all attributes which store the opened binary data files
+
+ :param bin_file_location: path to the binary data files to use, None if native package data should be used
+ :param in_memory: Whether to completely read and keep the coordinate data in memory as numpy arrays.
+ """
+
+ # __slots__ declared in parents are available in child classes. However, child subclasses will get a __dict__
+ # and __weakref__ unless they also define __slots__ (which should only contain names of any additional slots).
+ __slots__ = [
+ "hole_registry",
+ "_boundaries_file",
+ "_holes_file",
+ ]
+
+ def __init__(
+ self, bin_file_location: Optional[str] = None, in_memory: bool = False
+ ):
+ super().__init__(bin_file_location, in_memory)
+ self.holes_dir = utils.get_holes_dir(self.data_location)
+ self.boundaries_dir = utils.get_boundaries_dir(self.data_location)
+ self.boundaries = PolygonArray(
+ data_location=self.boundaries_dir, in_memory=in_memory
+ )
+ self.holes = PolygonArray(data_location=self.holes_dir, in_memory=in_memory)
+
+ # stores for which polygons (how many) holes exits and the id of the first of those holes
+ # since there are very few entries it is feasible to keep them in the memory
+ self.hole_registry = self._load_hole_registry()
+
+ def __del__(self) -> None:
+ """Clean up resources when the object is destroyed."""
+ del self.boundaries
+ del self.holes
+ del self.hole_registry
+
+ def _load_hole_registry(self) -> Dict[int, Tuple[int, int]]:
+ """
+ Load and convert the hole registry from JSON file, converting keys to int.
+ """
+ path = utils.get_hole_registry_path(self.data_location)
+ with open(path, encoding="utf-8") as json_file:
+ hole_registry_tmp = json.loads(json_file.read())
+ # convert the json string keys to int
+ return {int(k): v for k, v in hole_registry_tmp.items()}
+
+ @property
+ def nr_of_polygons(self) -> int:
+ return len(self.boundaries)
+
+ @property
+ def nr_of_holes(self) -> int:
+ return len(self.holes)
+
+ def coords_of(self, boundary_id: IntegerLike = 0) -> np.ndarray:
+ """
+ Get the coordinates of a boundary polygon from the FlatBuffers collection.
+
+ :param boundary_id: The index of the polygon.
+ :return: Array of coordinates.
+ """
+ return self.boundaries.coords_of(boundary_id)
+
+ def _iter_hole_ids_of(self, boundary_id: IntegerLike) -> Iterable[int]:
+ """
+ Yield the hole IDs for a given boundary polygon id.
+
+ :param boundary_id: id of the boundary polygon
+ :yield: Hole IDs
+ """
+ try:
+ amount_of_holes, first_hole_id = self.hole_registry[int(boundary_id)]
+ except KeyError:
+ return
+ for i in range(amount_of_holes):
+ yield first_hole_id + i
+
+ def _holes_of_poly(self, boundary_id: IntegerLike) -> Iterable[np.ndarray]:
+ """
+ Get the hole coordinates of a boundary polygon from the FlatBuffers collection.
+
+ :param boundary_id: id of the boundary polygon
+ :yield: Generator of hole coordinates
+ """
+ for hole_id in self._iter_hole_ids_of(boundary_id):
+ yield self.holes.coords_of(hole_id)
+
+ def get_polygon(
+ self, boundary_id: IntegerLike, coords_as_pairs: bool = False
+ ) -> List[Union[CoordPairs, CoordLists]]:
+ """
+ Get the polygon coordinates of a given boundary polygon including its holes.
+
+ :param boundary_id: ID of the boundary polygon
+ :param coords_as_pairs: If True, returns coordinates as pairs (lng, lat).
+ If False, returns coordinates as separate lists of longitudes and latitudes.
+ :return: List of polygon coordinates
+ """
+ list_of_converted_polygons = []
+ if coords_as_pairs:
+ conversion_method = utils.convert2coord_pairs
+ else:
+ conversion_method = utils.convert2coords
+ list_of_converted_polygons.append(
+ conversion_method(self.coords_of(boundary_id=boundary_id))
+ )
+
+ for hole in self._holes_of_poly(boundary_id):
+ list_of_converted_polygons.append(conversion_method(hole))
+
+ return list_of_converted_polygons
+
+ def get_geometry(
+ self,
+ tz_name: Optional[str] = "",
+ tz_id: Optional[int] = 0,
+ use_id: bool = False,
+ coords_as_pairs: bool = False,
+ ) -> List[List[Union[CoordPairs, CoordLists]]]:
+ """retrieves the geometry of a timezone: multiple boundary polygons with holes
+
+ :param tz_name: one of the names in ``timezone_names.json`` or ``self.timezone_names``
+ :param tz_id: the id of the timezone (=index in ``self.timezone_names``)
+ :param use_id: if ``True`` uses ``tz_id`` instead of ``tz_name``
+ :param coords_as_pairs: determines the structure of the polygon representation
+ :return: a data structure representing the multipolygon of this timezone
+ output format: ``[ [polygon1, hole1, hole2...], [polygon2, ...], ...]``
+ and each polygon and hole is itself formatted like: ``([longitudes], [latitudes])``
+ or ``[(lng1,lat1), (lng2,lat2),...]`` if ``coords_as_pairs=True``.
+ """
+
+ if use_id:
+ if not isinstance(tz_id, int):
+ raise TypeError("the zone id must be given as int.")
+ if tz_id < 0 or tz_id >= self.nr_of_zones:
+ raise ValueError(
+ f"the given zone id {tz_id} is invalid (value range: 0 - {self.nr_of_zones - 1}."
+ )
+ else:
+ if tz_name is None:
+ raise ValueError("no timezone name given.")
+ try:
+ tz_id = self.timezone_names.index(tz_name)
+ except ValueError:
+ raise ValueError("The timezone '", tz_name, "' does not exist.")
+ if tz_id is None:
+ raise ValueError("no timezone id given.")
+
+ return [
+ self.get_polygon(boundary_id, coords_as_pairs)
+ for boundary_id in self._iter_boundary_ids_of_zone(tz_id)
+ ]
+
+ def inside_of_polygon(self, boundary_id: IntegerLike, x: int, y: int) -> bool:
+ """
+ Check if a point is inside a boundary polygon.
+
+ :param boundary_id: boundary polygon ID
+ :param x: X-coordinate of the point
+ :param y: Y-coordinate of the point
+ :return: True if the point lies inside the boundary polygon, False if outside or in a hole.
+ """
+ # avoid running the expensive PIP algorithm at any cost
+ # -> check bboxes first
+ if self.boundaries.outside_bbox(boundary_id, x, y):
+ return False
+
+ # NOTE: holes are much smaller (fewer points) -> less expensive to check
+ # -> check holes before the boundary
+ hole_id_iter = self._iter_hole_ids_of(boundary_id)
+ if self.holes.in_any_polygon(hole_id_iter, x, y):
+ # the point is within one of the holes
+ # it is excluded fromn this boundary polygon
+ return False
+
+ return self.boundaries.pip(boundary_id, x, y)
+
+ def timezone_at(self, *, lng: float, lat: float) -> Optional[str]:
+ """
+ Find the timezone for a given point using hybrid shortcuts, considering both land and ocean timezones.
+
+ Uses precomputed hybrid shortcuts to reduce the number of polygons checked. Returns the timezone name
+ of the matched polygon, which may be an ocean timezone ("Etc/GMT+-XX") if applicable.
+
+ Since ocean timezones span the whole globe, some timezone will always be matched!
+ `None` can only be returned when using custom timezone data without such ocean timezones.
+
+ :param lng: longitude of the point in degrees (-180.0 to 180.0)
+ :param lat: latitude of the point in degrees (90.0 to -90.0)
+ :return: the timezone name of the matched polygon, or None if no match is found.
+ """
+ # NOTE: performance critical code. avoid helper function call overhead as much as possible
+ lng, lat = utils.validate_coordinates(lng, lat)
+ hex_id = h3.latlng_to_cell(lat, lng, SHORTCUT_H3_RES)
+
+ # Get shortcut value (hybrid optimization)
+ shortcut_value = self.shortcut_mapping.get(hex_id)
+ if shortcut_value is None:
+ # NOTE: hypothetical case, with ocean data every shortcut maps to at least one boundary polygon
+ return None
+
+ if isinstance(shortcut_value, int):
+ # Direct zone ID - optimal case for performance
+ return self.zone_name_from_id(shortcut_value)
+
+ # Polygon array case - need to check polygons
+ possible_boundaries = shortcut_value
+ nr_possible_polygons = len(possible_boundaries)
+ if nr_possible_polygons == 0:
+ return None
+ # NOTE: the length 1 case can never occur here, since this is covered by the unique zone shortcut
+
+ # create a list of all the timezone ids of all possible boundary polygons
+ zone_ids = self.zone_ids_of(possible_boundaries)
+
+ last_zone_change_idx = utils.get_last_change_idx(zone_ids)
+ # NOTE: the case last_zone_change_idx == 0 is covered by the unique zone shortcut
+
+ # ATTENTION: the polygons are stored converted to 32-bit ints,
+ # convert the query coordinates in the same fashion in order to make the data formats match
+ # x = longitude y = latitude both converted to 8byte int
+ x = utils.coord2int(lng)
+ y = utils.coord2int(lat)
+
+ # check until the point is included in one of the possible boundary polygons
+ for i, boundary_id in enumerate(possible_boundaries):
+ if i >= last_zone_change_idx:
+ # avoid expensive PIP checks when no other zone can be matched anymore
+ break
+
+ if self.inside_of_polygon(boundary_id, x, y):
+ zone_id = zone_ids[i]
+ return self.zone_name_from_id(int(zone_id))
+
+ # since it is the last possible option,
+ # the polygons of the last possible zone don't actually have to be checked
+ # -> instantly return the last zone
+ zone_id = zone_ids[-1]
+ return self.zone_name_from_id(int(zone_id))
+
+ def certain_timezone_at(self, *, lng: float, lat: float) -> Optional[str]:
+ """checks in which timezone polygon the point is certainly included in using hybrid shortcuts
+
+ .. note:: this is only meaningful when you have compiled your own timezone data
+ where there are areas without timezone polygon coverage.
+ Otherwise, some timezone will always be matched and the functionality is equal to using `.timezone_at()`
+ -> useless to actually test all polygons.
+
+ .. note:: using this function is less performant than `.timezone_at()`
+
+ :param lng: longitude of the point in degree
+ :param lat: latitude of the point in degree
+ :return: the timezone name of the polygon the point is included in or `None`
+ """
+ lng, lat = utils.validate_coordinates(lng, lat)
+ hex_id = h3.latlng_to_cell(lat, lng, SHORTCUT_H3_RES)
+
+ # Get shortcut value (hybrid optimization)
+ shortcut_value = self.shortcut_mapping.get(hex_id)
+ if shortcut_value is None:
+ return None
+
+ # ATTENTION: the polygons are stored converted to 32-bit ints,
+ # convert the query coordinates in the same fashion in order to make the data formats match
+ # x = longitude y = latitude both converted to 8byte int
+ x = utils.coord2int(lng)
+ y = utils.coord2int(lat)
+
+ # check if the query point is found to be truly included in one of the possible boundary polygons
+ if isinstance(shortcut_value, int):
+ # For zone IDs, iterate directly over boundary polygons for that zone
+ # Most polygons will be quickly ruled out by bbox check
+ boundary_ids = self._iter_boundary_ids_of_zone(shortcut_value)
+ else:
+ # Polygon array case - iterate directly over the array
+ boundary_ids = shortcut_value
+
+ for boundary_id in boundary_ids:
+ if self.inside_of_polygon(boundary_id, x, y):
+ zone_id = self.zone_id_of(boundary_id)
+ return self.zone_name_from_id(zone_id)
+
+ # none of the boundary polygon candidates truly matched
+ return None
diff --git a/frogpilot/third_party/timezonefinder/utils.py b/frogpilot/third_party/timezonefinder/utils.py
new file mode 100644
index 00000000..1eebbe93
--- /dev/null
+++ b/frogpilot/third_party/timezonefinder/utils.py
@@ -0,0 +1,83 @@
+"""utility functions"""
+
+from pathlib import Path
+import re
+from typing import Any, Callable, Tuple
+
+import numpy as np
+
+from timezonefinder.configs import (
+ DEFAULT_DATA_DIR,
+ OCEAN_TIMEZONE_PREFIX,
+)
+from timezonefinder import utils_numba, utils_clang
+
+
+# make numba functions available via utils
+using_numba = utils_numba.using_numba
+clang_extension_loaded = utils_clang.clang_extension_loaded
+is_valid_lat = utils_numba.is_valid_lat
+is_valid_lng = utils_numba.is_valid_lng
+coord2int = utils_numba.coord2int
+int2coord = utils_numba.int2coord
+convert2coords = utils_numba.convert2coords
+convert2coord_pairs = utils_numba.convert2coord_pairs
+get_last_change_idx = utils_numba.get_last_change_idx
+
+
+inside_polygon: Callable[[int, int, np.ndarray], bool]
+# at import time fix which "point-in-polygon" implementation will be used
+if clang_extension_loaded and not using_numba:
+ # use the C implementation only if Numba is not present
+ inside_polygon = utils_clang.pt_in_poly_clang
+else:
+ # use the (JIT compiled) python function if Numba is present or the C extension cannot be loaded
+ inside_polygon = utils_numba.pt_in_poly_python
+
+
+def validate_lat(lat: float) -> None:
+ if not is_valid_lat(lat):
+ raise ValueError(f"The given latitude {lat} is out of bounds")
+
+
+def validate_lng(lng: float) -> None:
+ if not is_valid_lng(lng):
+ raise ValueError(f"The given longitude {lng} is out of bounds")
+
+
+def validate_coordinates(lng: float, lat: float) -> Tuple[float, float]:
+ lng, lat = float(lng), float(lat)
+ validate_lng(lng)
+ validate_lat(lat)
+ return lng, lat
+
+
+def close_resource(obj: Any) -> None:
+ """Safely close a resource, ignoring specific expected errors."""
+ if obj is None:
+ return
+ try:
+ obj.close()
+ except (AttributeError, OSError, ValueError):
+ pass
+
+
+def is_ocean_timezone(timezone_name: str) -> bool:
+ if re.match(OCEAN_TIMEZONE_PREFIX, timezone_name) is None:
+ return False
+ return True
+
+
+def get_boundaries_dir(data_dir: Path = DEFAULT_DATA_DIR) -> Path:
+ """Return the path to the boundaries directory."""
+ return data_dir / "boundaries"
+
+
+def get_holes_dir(data_dir: Path = DEFAULT_DATA_DIR) -> Path:
+ """Return the path to the holes directory."""
+ return data_dir / "holes"
+
+
+def get_hole_registry_path(data_dir: Path = DEFAULT_DATA_DIR) -> Path:
+ """Return the path to the hole registry file."""
+ return data_dir / "hole_registry.json"
diff --git a/frogpilot/third_party/timezonefinder/utils_clang.py b/frogpilot/third_party/timezonefinder/utils_clang.py
new file mode 100644
index 00000000..75bb94b3
--- /dev/null
+++ b/frogpilot/third_party/timezonefinder/utils_clang.py
@@ -0,0 +1,44 @@
+from typing import Final, Optional
+
+import cffi
+
+import numpy as np
+
+ffi: Optional[cffi.FFI] = None
+try:
+ # Note: IDE might complain as this import comes from a cffi C extension
+ from timezonefinder import inside_polygon_ext # type: ignore
+
+ clang_extension_loaded = True
+ ffi = cffi.FFI()
+
+except ImportError:
+ clang_extension_loaded = False
+ inside_polygon_ext = None
+
+INT_LIST_REP: Final[str] = "int []"
+
+
+def pt_in_poly_clang(x: int, y: int, coords: np.ndarray) -> bool:
+ """wrapper of the point in polygon test algorithm C extension
+
+ ATTENTION: the input numpy arrays must have a C_CONTIGUOUS memory layout
+ https://numpy.org/doc/stable/reference/generated/numpy.ascontiguousarray.html?highlight=ascontiguousarray#numpy.ascontiguousarray
+ """
+ if ffi is None:
+ raise ValueError(
+ "Trying to use the clang implementation of the point in polygon algorithm "
+ "while the C extension in not loaded."
+ )
+ x_coords = coords[0]
+ y_coords = coords[1]
+ nr_coords = len(x_coords)
+
+ y_coords = np.ascontiguousarray(y_coords)
+ x_coords = np.ascontiguousarray(x_coords)
+ x_coords_ffi = ffi.from_buffer(INT_LIST_REP, x_coords)
+ y_coords_ffi = ffi.from_buffer(INT_LIST_REP, y_coords)
+ contained = inside_polygon_ext.lib.inside_polygon_int(
+ x, y, nr_coords, x_coords_ffi, y_coords_ffi
+ )
+ return contained
diff --git a/frogpilot/third_party/timezonefinder/utils_numba.py b/frogpilot/third_party/timezonefinder/utils_numba.py
new file mode 100644
index 00000000..a50c4698
--- /dev/null
+++ b/frogpilot/third_party/timezonefinder/utils_numba.py
@@ -0,0 +1,201 @@
+"""performance critical utility functions
+
+JIT compiled for efficiency in case `numba` is installed
+
+TODO Numba Ahead-Of-Time Compilation:
+cc = CC('precompiled_helpers', )
+# Uncomment the following line to print out the compilation steps
+cc.verbose = True
+
+if __name__ == "__main__":
+ cc.compile()
+"""
+
+import numpy as np
+
+from timezonefinder.configs import (
+ COORD2INT_FACTOR,
+ INT2COORD_FACTOR,
+ CoordLists,
+ CoordPairs,
+)
+
+try:
+ from numba import njit, boolean, i4, f8
+ from numba.types import Array
+
+ using_numba = True
+except ImportError:
+ using_numba = False
+ # replace Numba functionality with "transparent" implementations
+ from timezonefinder._numba_replacements import njit, boolean, Array, i4, f8
+
+
+# For Fortran-ordered arrays (F-contiguous), use the 'order="F"' in the Numba signature
+# F order is natural for the used coordinate schema:
+# coords = [x_coords, y_coords]
+# x_coords = coords[0]
+CoordType = Array(i4, 2, "F", True, aligned=True)
+
+
+# @cc.export('inside_polygon', 'b1(i4, i4, i4[:, :])')
+@njit(boolean(i4, i4, CoordType), cache=True)
+def pt_in_poly_python(x: int, y: int, coords: np.ndarray) -> bool:
+ """
+ Implementing the ray casting point in polygon test algorithm
+ cf. https://en.wikipedia.org/wiki/Point_in_polygon#Ray_casting_algorithm
+ :param x:
+ :param y:
+ :param coords: a polygon represented by a list containing two lists (x and y coordinates):
+ [ [x1,x2,x3...], [y1,y2,y3...]]
+ those lists are actually numpy arrays which are being read directly from a binary file
+ :return: true if the point (x,y) lies within the polygon
+
+ Some overflow considerations for the critical part of comparing the line segment slopes:
+
+ (y2 - y) * (x2 - x1) <= delta_y_max * delta_x_max
+ (y2 - y1) * (x2 - x) <= delta_y_max * delta_x_max
+ delta_y_max * delta_x_max = 180 * 360 < 65 x10^3
+
+ Instead of calculating with float I decided using just ints (by multiplying with 10^7). That gives us:
+
+ delta_y_max * delta_x_max = 180x10^7 * 360x10^7
+ delta_y_max * delta_x_max <= 65x10^17
+
+ So these numbers need up to log_2(65 x10^17) ~ 63 bits to be represented! Even though values this big should never
+ occur in practice (timezone polygons do not span the whole lng lat coordinate space),
+ 32bit accuracy hence is not safe to use here!
+ pure Python automatically uses the appropriate int data type preventing overflow
+ (cf. https://www.python.org/dev/peps/pep-0237/),
+ but here the data types are numpy internal static data types. The data is stored as int32
+ -> use int64 when comparing slopes!
+
+ slower naive implementation:
+ j = nr_coords - 1
+ for i in range(nr_coords):
+ if ((y_coords[i] > y) != (y_coords[j] > y)) and (
+ x
+ < (int64(x_coords[j]) - int64(x_coords[i]))
+ * (int64(y) - int64(y_coords[i]))
+ / (int64(y_coords[j]) - int64(y_coords[i]))
+ + int64(x_coords[i])
+ ):
+ inside = not inside
+ j = i
+ i += 1
+ """
+ x_coords = coords[0]
+ y_coords = coords[1]
+ nr_coords = len(x_coords)
+ inside = False
+
+ # the edge from the last to the first point is checked first
+ y1 = y_coords[-1]
+ y_gt_y1 = y > y1
+ for i in range(nr_coords):
+ y2 = y_coords[i]
+ y_gt_y2 = y > y2
+ if y_gt_y1 ^ y_gt_y2: # XOR
+ # [p1-p2] crosses horizontal line in p
+ x1 = x_coords[i - 1]
+ x2 = x_coords[i]
+ # only count crossings "right" of the point ( >= x)
+ x_le_x1 = x <= x1
+ x_le_x2 = x <= x2
+ if x_le_x1 or x_le_x2:
+ if x_le_x1 and x_le_x2:
+ # p1 and p2 are both to the right -> valid crossing
+ inside = not inside
+ else:
+ # compare the slope of the line [p1-p2] and [p-p2]
+ # depending on the position of p2 this determines whether
+ # the polygon edge is right or left of the point
+ # to avoid expensive division the divisors (of the slope dy/dx) are brought to the other side
+ # ( dy/dx > a == dy > a * dx )
+ # only one of the points is to the right
+ # NOTE: int64 precision required to prevent overflow
+ y_64 = np.int64(y)
+ y1_64 = np.int64(y1)
+ y2_64 = np.int64(y2)
+ x_64 = np.int64(x)
+ x1_64 = np.int64(x1)
+ x2_64 = np.int64(x2)
+ slope1 = (y2_64 - y_64) * (x2_64 - x1_64)
+ slope2 = (y2_64 - y1_64) * (x2_64 - x_64)
+ # NOTE: accept slope equality to also detect if p lies directly on an edge
+ if y_gt_y1:
+ if slope1 <= slope2:
+ inside = not inside
+ elif slope1 >= slope2: # NOT y_gt_y1
+ inside = not inside
+
+ # next point
+ y1 = y2
+ y_gt_y1 = y_gt_y2
+
+ return inside
+
+
+@njit(cache=True)
+def get_last_change_idx(lst: np.ndarray) -> int:
+ """
+ :param lst: list of entries
+ :return: returns the index to the element for which all following elements are equal
+ """
+ nr_entries = lst.shape[0]
+ if nr_entries <= 1:
+ return 0
+ # at least 2 elements
+ last_elem = lst[-1]
+ for ptr in range(2, nr_entries + 1):
+ # Note: from the back
+ element = lst[-ptr]
+ if element != last_elem:
+ # return the last pointer value
+ # Attention: convert into positive "absolute" index first
+ return nr_entries - ptr + 1
+ # Note: all entries are the same -> ptr will be 0
+ return 0
+
+
+# @cc.export('int2coord', f8(i4))
+@njit(f8(i4), cache=True)
+def int2coord(i4: int) -> float:
+ return float(i4 * INT2COORD_FACTOR)
+
+
+# @cc.export('coord2int', i4(f8))
+@njit(i4(f8), cache=True)
+def coord2int(double: float) -> int:
+ return int(double * COORD2INT_FACTOR)
+
+
+@njit(cache=True)
+def convert2coords(polygon_data: np.ndarray) -> CoordLists:
+ # return a tuple of coordinate lists
+ return [
+ [int2coord(x) for x in polygon_data[0]],
+ [int2coord(y) for y in polygon_data[1]],
+ ]
+
+
+@njit(cache=True)
+def convert2coord_pairs(polygon_data: np.ndarray) -> CoordPairs:
+ # return a list of coordinate tuples (x,y)
+ x_coords = polygon_data[0]
+ y_coords = polygon_data[1]
+ nr_coords = len(x_coords)
+ coodinate_list = [
+ (int2coord(x_coords[i]), int2coord(y_coords[i])) for i in range(nr_coords)
+ ]
+ return coodinate_list
+
+
+@njit(boolean(f8), cache=True)
+def is_valid_lat(lat: float) -> bool:
+ return -90.0 <= lat <= 90.0
+
+
+@njit(boolean(f8), cache=True)
+def is_valid_lng(lng: float) -> bool:
+ return -180.0 <= lng <= 180.0
diff --git a/frogpilot/third_party/timezonefinder/zone_names.py b/frogpilot/third_party/timezonefinder/zone_names.py
new file mode 100644
index 00000000..8b0cf7bf
--- /dev/null
+++ b/frogpilot/third_party/timezonefinder/zone_names.py
@@ -0,0 +1,41 @@
+from pathlib import Path
+from typing import List
+
+from timezonefinder.configs import DEFAULT_DATA_DIR
+
+
+def get_zone_names_path(output_path: Path = DEFAULT_DATA_DIR) -> Path:
+ """Get the path to the timezone names text file."""
+ return output_path / "timezone_names.txt"
+
+
+def write_zone_names(
+ zone_names: List[str], output_path: Path = DEFAULT_DATA_DIR
+) -> None:
+ """
+ Write timezone names to a text file.
+
+ Args:
+ zone_names: List of timezone names.
+ output_path: Directory where the output file will be written.
+ """
+ path = get_zone_names_path(output_path)
+ with open(path, "w", encoding="utf-8") as f:
+ f.write("\n".join(zone_names))
+ f.write("\n") # write a newline at the end of the file
+
+
+def read_zone_names(path: Path) -> List[str]:
+ """
+ Read timezone names from a text file.
+
+ Args:
+ path: Path to the timezone names text file.
+ If None, the default path will be used.
+
+ Returns:
+ List of timezone names.
+ """
+ file_path = get_zone_names_path(path)
+ with open(file_path, encoding="utf-8") as f:
+ return [line.strip() for line in f if line.strip()]
diff --git a/system/timed.py b/system/timed.py
old mode 100755
new mode 100644
index 69851738..1044b9ab
--- a/system/timed.py
+++ b/system/timed.py
@@ -1,7 +1,9 @@
#!/usr/bin/env python3
import datetime
+import os
import subprocess
import time
+from timezonefinder import TimezoneFinder
from typing import NoReturn
import cereal.messaging as messaging
@@ -9,6 +11,7 @@ from openpilot.common.time_helpers import min_date, system_time_valid
from openpilot.common.swaglog import cloudlog
from openpilot.common.params import Params
from openpilot.common.gps import get_gps_location_service
+from openpilot.system.hardware import AGNOS
def set_time(new_time):
@@ -25,6 +28,22 @@ def set_time(new_time):
# FrogPilot variables
+def set_timezone(timezone):
+ valid_timezones = subprocess.check_output("timedatectl list-timezones", shell=True, encoding="utf8").strip().split("\n")
+ if timezone not in valid_timezones:
+ cloudlog.error(f"Timezone not supported {timezone}")
+ return
+
+ cloudlog.debug(f"Setting timezone to {timezone}")
+ try:
+ if AGNOS:
+ tzpath = os.path.join("/usr/share/zoneinfo/", timezone)
+ subprocess.check_call(f"sudo su -c 'ln -snf {tzpath} /data/etc/tmptime && mv /data/etc/tmptime /data/etc/localtime'", shell=True)
+ subprocess.check_call(f"sudo su -c 'echo \'{timezone}\' > /data/etc/timezone'", shell=True)
+ else:
+ subprocess.check_call(f"sudo timedatectl set-timezone {timezone}", shell=True)
+ except subprocess.CalledProcessError:
+ cloudlog.exception(f"Error setting timezone to {timezone}")
def main() -> NoReturn:
@@ -43,6 +62,11 @@ def main() -> NoReturn:
sm = messaging.SubMaster([gps_location_service])
# FrogPilot variables
+ tf = TimezoneFinder()
+
+ last_timezone = params.get("Timezone")
+ if last_timezone is not None:
+ set_timezone(last_timezone)
while True:
sm.update(1000)
@@ -64,6 +88,11 @@ def main() -> NoReturn:
set_time(gps_time)
# FrogPilot variables
+ timezone = tf.timezone_at(lng=gps.longitude, lat=gps.latitude)
+ if timezone is not None and timezone != last_timezone:
+ set_timezone(timezone)
+ params.put_nonblocking("Timezone", timezone)
+ last_timezone = timezone
time.sleep(10)