initial commit
This commit is contained in:
@@ -0,0 +1,9 @@
|
|||||||
|
*.py[codz]
|
||||||
|
__pycache__/
|
||||||
|
.mypy_cache/
|
||||||
|
.pytest_cache/
|
||||||
|
.ruff_cache/
|
||||||
|
.pdm-python
|
||||||
|
\#*.dxf
|
||||||
|
*.dxf~
|
||||||
|
.env
|
||||||
Vendored
+12
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"python.envFile": "${workspaceFolder}/.env",
|
||||||
|
"python.testing.unittestArgs": [
|
||||||
|
"-v",
|
||||||
|
"-s",
|
||||||
|
"./tests",
|
||||||
|
"-p",
|
||||||
|
"test_*.py"
|
||||||
|
],
|
||||||
|
"python.testing.pytestEnabled": false,
|
||||||
|
"python.testing.unittestEnabled": true
|
||||||
|
}
|
||||||
Vendored
+22
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"folders": [
|
||||||
|
{
|
||||||
|
"path": ".."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": {},
|
||||||
|
"launch": {
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [{
|
||||||
|
"name": "Python: Current File",
|
||||||
|
"type": "debugpy",
|
||||||
|
"request": "launch",
|
||||||
|
"program": "${file}",
|
||||||
|
"console": "integratedTerminal",
|
||||||
|
"cwd": "${fileDirname}",
|
||||||
|
"env": {
|
||||||
|
"PYTHONPATH": "${workspaceFolder}${pathSeparator}${env:PYTHONPATH}"
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# Readme
|
||||||
|
|
||||||
|
Surveying terms can be checked in https://surveyingpedia.com/glossary
|
||||||
|
|
||||||
|
## Terms
|
||||||
|
|
||||||
|
- Reference point: A fixed survey station or control point used as the origin for measurements
|
||||||
|
- Baseline: The main survey line between two reference points, along which chainages are measured
|
||||||
|
- Chainage: The linear distance measured along the baseline from a fixed starting point
|
||||||
|
- Offset: A perpendicular distance measured from the baseline to locate a feature
|
||||||
|
- Tie measurement: General term for measurements taken to “tie” features to the baseline, often recorded in a tie table.
|
||||||
|
- Detail point: A sub-point or feature (tree, culvert, corner) located by offsets from the baseline
|
||||||
|
- Tie table / Offset table: The tabular record of chainages and offsets used to reconstruct the sketch.
|
||||||
|
- Tie sketch: The graphical sketch showing how offsets are taken from the baseline to locate features.
|
||||||
|
- Plot corners: The boundary-defining points of the parcel. Corners are usually labeled A, B, C, D, … in clockwise sequence around the plot
|
||||||
|
- Field book: The official record where chainages, offsets, and sketches are entered during survey work.
|
||||||
|
|
||||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,369 @@
|
|||||||
|
# This file is @generated by PDM.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
|
||||||
|
[metadata]
|
||||||
|
groups = ["default"]
|
||||||
|
strategy = ["inherit_metadata"]
|
||||||
|
lock_version = "4.5.0"
|
||||||
|
content_hash = "sha256:807c6594612226bbdd8f9404d6192f7fa6a1f0d16b3b0daab32f02725bde6505"
|
||||||
|
|
||||||
|
[[metadata.targets]]
|
||||||
|
requires_python = "==3.14.*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "contourpy"
|
||||||
|
version = "1.3.3"
|
||||||
|
requires_python = ">=3.11"
|
||||||
|
summary = "Python library for calculating contours of 2D quadrilateral grids"
|
||||||
|
groups = ["default"]
|
||||||
|
dependencies = [
|
||||||
|
"numpy>=1.25",
|
||||||
|
]
|
||||||
|
files = [
|
||||||
|
{file = "contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a"},
|
||||||
|
{file = "contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77"},
|
||||||
|
{file = "contourpy-1.3.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5"},
|
||||||
|
{file = "contourpy-1.3.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4"},
|
||||||
|
{file = "contourpy-1.3.3-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36"},
|
||||||
|
{file = "contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3"},
|
||||||
|
{file = "contourpy-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b"},
|
||||||
|
{file = "contourpy-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36"},
|
||||||
|
{file = "contourpy-1.3.3-cp314-cp314-win32.whl", hash = "sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d"},
|
||||||
|
{file = "contourpy-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd"},
|
||||||
|
{file = "contourpy-1.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339"},
|
||||||
|
{file = "contourpy-1.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772"},
|
||||||
|
{file = "contourpy-1.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77"},
|
||||||
|
{file = "contourpy-1.3.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13"},
|
||||||
|
{file = "contourpy-1.3.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe"},
|
||||||
|
{file = "contourpy-1.3.3-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f"},
|
||||||
|
{file = "contourpy-1.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0"},
|
||||||
|
{file = "contourpy-1.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4"},
|
||||||
|
{file = "contourpy-1.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f"},
|
||||||
|
{file = "contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae"},
|
||||||
|
{file = "contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc"},
|
||||||
|
{file = "contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b"},
|
||||||
|
{file = "contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cycler"
|
||||||
|
version = "0.12.1"
|
||||||
|
requires_python = ">=3.8"
|
||||||
|
summary = "Composable style cycles"
|
||||||
|
groups = ["default"]
|
||||||
|
files = [
|
||||||
|
{file = "cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30"},
|
||||||
|
{file = "cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "et-xmlfile"
|
||||||
|
version = "2.0.0"
|
||||||
|
requires_python = ">=3.8"
|
||||||
|
summary = "An implementation of lxml.xmlfile for the standard library"
|
||||||
|
groups = ["default"]
|
||||||
|
files = [
|
||||||
|
{file = "et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa"},
|
||||||
|
{file = "et_xmlfile-2.0.0.tar.gz", hash = "sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ezdxf"
|
||||||
|
version = "1.4.3"
|
||||||
|
requires_python = ">=3.10"
|
||||||
|
summary = "A Python package to create/manipulate DXF drawings."
|
||||||
|
groups = ["default"]
|
||||||
|
dependencies = [
|
||||||
|
"fonttools",
|
||||||
|
"numpy",
|
||||||
|
"pyparsing>=2.0.1",
|
||||||
|
"typing-extensions>=4.6.0",
|
||||||
|
]
|
||||||
|
files = [
|
||||||
|
{file = "ezdxf-1.4.3-py3-none-any.whl", hash = "sha256:19e464aa4525dca3f1dabce165308de7ac262f1122b3c3986320cbec9e8ca6be"},
|
||||||
|
{file = "ezdxf-1.4.3.tar.gz", hash = "sha256:403adf7ce305877f6c9f3c007fe2e5c5df504dfb797032122abedd7170176764"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fonttools"
|
||||||
|
version = "4.62.1"
|
||||||
|
requires_python = ">=3.10"
|
||||||
|
summary = "Tools to manipulate font files"
|
||||||
|
groups = ["default"]
|
||||||
|
files = [
|
||||||
|
{file = "fonttools-4.62.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fa1d16210b6b10a826d71bed68dd9ec24a9e218d5a5e2797f37c573e7ec215ca"},
|
||||||
|
{file = "fonttools-4.62.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:aa69d10ed420d8121118e628ad47d86e4caa79ba37f968597b958f6cceab7eca"},
|
||||||
|
{file = "fonttools-4.62.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd13b7999d59c5eb1c2b442eb2d0c427cb517a0b7a1f5798fc5c9e003f5ff782"},
|
||||||
|
{file = "fonttools-4.62.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8d337fdd49a79b0d51c4da87bc38169d21c3abbf0c1aa9367eff5c6656fb6dae"},
|
||||||
|
{file = "fonttools-4.62.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d241cdc4a67b5431c6d7f115fdf63335222414995e3a1df1a41e1182acd4bcc7"},
|
||||||
|
{file = "fonttools-4.62.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c05557a78f8fa514da0f869556eeda40887a8abc77c76ee3f74cf241778afd5a"},
|
||||||
|
{file = "fonttools-4.62.1-cp314-cp314-win32.whl", hash = "sha256:49a445d2f544ce4a69338694cad575ba97b9a75fff02720da0882d1a73f12800"},
|
||||||
|
{file = "fonttools-4.62.1-cp314-cp314-win_amd64.whl", hash = "sha256:1eecc128c86c552fb963fe846ca4e011b1be053728f798185a1687502f6d398e"},
|
||||||
|
{file = "fonttools-4.62.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:1596aeaddf7f78e21e68293c011316a25267b3effdaccaf4d59bc9159d681b82"},
|
||||||
|
{file = "fonttools-4.62.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8f8fca95d3bb3208f59626a4b0ea6e526ee51f5a8ad5d91821c165903e8d9260"},
|
||||||
|
{file = "fonttools-4.62.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee91628c08e76f77b533d65feb3fbe6d9dad699f95be51cf0d022db94089cdc4"},
|
||||||
|
{file = "fonttools-4.62.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5f37df1cac61d906e7b836abe356bc2f34c99d4477467755c216b72aa3dc748b"},
|
||||||
|
{file = "fonttools-4.62.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:92bb00a947e666169c99b43753c4305fc95a890a60ef3aeb2a6963e07902cc87"},
|
||||||
|
{file = "fonttools-4.62.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:bdfe592802ef939a0e33106ea4a318eeb17822c7ee168c290273cbd5fabd746c"},
|
||||||
|
{file = "fonttools-4.62.1-cp314-cp314t-win32.whl", hash = "sha256:b820fcb92d4655513d8402d5b219f94481c4443d825b4372c75a2072aa4b357a"},
|
||||||
|
{file = "fonttools-4.62.1-cp314-cp314t-win_amd64.whl", hash = "sha256:59b372b4f0e113d3746b88985f1c796e7bf830dd54b28374cd85c2b8acd7583e"},
|
||||||
|
{file = "fonttools-4.62.1-py3-none-any.whl", hash = "sha256:7487782e2113861f4ddcc07c3436450659e3caa5e470b27dc2177cade2d8e7fd"},
|
||||||
|
{file = "fonttools-4.62.1.tar.gz", hash = "sha256:e54c75fd6041f1122476776880f7c3c3295ffa31962dc6ebe2543c00dca58b5d"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "kiwisolver"
|
||||||
|
version = "1.5.0"
|
||||||
|
requires_python = ">=3.10"
|
||||||
|
summary = "A fast implementation of the Cassowary constraint solver"
|
||||||
|
groups = ["default"]
|
||||||
|
files = [
|
||||||
|
{file = "kiwisolver-1.5.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8df31fe574b8b3993cc61764f40941111b25c2d9fea13d3ce24a49907cd2d615"},
|
||||||
|
{file = "kiwisolver-1.5.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1d49a49ac4cbfb7c1375301cd1ec90169dfeae55ff84710d782260ce77a75a02"},
|
||||||
|
{file = "kiwisolver-1.5.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0cbe94b69b819209a62cb27bdfa5dc2a8977d8de2f89dfd97ba4f53ed3af754e"},
|
||||||
|
{file = "kiwisolver-1.5.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80aa065ffd378ff784822a6d7c3212f2d5f5e9c3589614b5c228b311fd3063ac"},
|
||||||
|
{file = "kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e7f886f47ab881692f278ae901039a234e4025a68e6dfab514263a0b1c4ae05"},
|
||||||
|
{file = "kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5060731cc3ed12ca3a8b57acd4aeca5bbc2f49216dd0bec1650a1acd89486bcd"},
|
||||||
|
{file = "kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a4aa69609f40fce3cbc3f87b2061f042eee32f94b8f11db707b66a26461591a"},
|
||||||
|
{file = "kiwisolver-1.5.0-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:d168fda2dbff7b9b5f38e693182d792a938c31db4dac3a80a4888de603c99554"},
|
||||||
|
{file = "kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:413b820229730d358efd838ecbab79902fe97094565fdc80ddb6b0a18c18a581"},
|
||||||
|
{file = "kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5124d1ea754509b09e53738ec185584cc609aae4a3b510aaf4ed6aa047ef9303"},
|
||||||
|
{file = "kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e4415a8db000bf49a6dd1c478bf70062eaacff0f462b92b0ba68791a905861f9"},
|
||||||
|
{file = "kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d618fd27420381a4f6044faa71f46d8bfd911bd077c555f7138ed88729bfbe79"},
|
||||||
|
{file = "kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5092eb5b1172947f57d6ea7d89b2f29650414e4293c47707eb499ec07a0ac796"},
|
||||||
|
{file = "kiwisolver-1.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:d76e2d8c75051d58177e762164d2e9ab92886534e3a12e795f103524f221dd8e"},
|
||||||
|
{file = "kiwisolver-1.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:fa6248cd194edff41d7ea9425ced8ca3a6f838bfb295f6f1d6e6bb694a8518df"},
|
||||||
|
{file = "kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d1ffeb80b5676463d7a7d56acbe8e37a20ce725570e09549fe738e02ca6b7e1e"},
|
||||||
|
{file = "kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bc4d8e252f532ab46a1de9349e2d27b91fce46736a9eedaa37beaca66f574ed4"},
|
||||||
|
{file = "kiwisolver-1.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6783e069732715ad0c3ce96dbf21dbc2235ab0593f2baf6338101f70371f4028"},
|
||||||
|
{file = "kiwisolver-1.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e7c4c09a490dc4d4a7f8cbee56c606a320f9dc28cf92a7157a39d1ce7676a657"},
|
||||||
|
{file = "kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a075bd7bd19c70cf67c8badfa36cf7c5d8de3c9ddb8420c51e10d9c50e94920"},
|
||||||
|
{file = "kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bdd3e53429ff02aa319ba59dfe4ceeec345bf46cf180ec2cf6fd5b942e7975e9"},
|
||||||
|
{file = "kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cdcb35dc9d807259c981a85531048ede628eabcffb3239adf3d17463518992d"},
|
||||||
|
{file = "kiwisolver-1.5.0-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:70d593af6a6ca332d1df73d519fddb5148edb15cd90d5f0155e3746a6d4fcc65"},
|
||||||
|
{file = "kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:377815a8616074cabbf3f53354e1d040c35815a134e01d7614b7692e4bf8acfa"},
|
||||||
|
{file = "kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0255a027391d52944eae1dbb5d4cc5903f57092f3674e8e544cdd2622826b3f0"},
|
||||||
|
{file = "kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:012b1eb16e28718fa782b5e61dc6f2da1f0792ca73bd05d54de6cb9561665fc9"},
|
||||||
|
{file = "kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0e3aafb33aed7479377e5e9a82e9d4bf87063741fc99fc7ae48b0f16e32bdd6f"},
|
||||||
|
{file = "kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7a116ae737f0000343218c4edf5bd45893bfeaff0993c0b215d7124c9f77646"},
|
||||||
|
{file = "kiwisolver-1.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:1dd9b0b119a350976a6d781e7278ec7aca0b201e1a9e2d23d9804afecb6ca681"},
|
||||||
|
{file = "kiwisolver-1.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:58f812017cd2985c21fbffb4864d59174d4903dd66fa23815e74bbc7a0e2dd57"},
|
||||||
|
{file = "kiwisolver-1.5.0.tar.gz", hash = "sha256:d4193f3d9dc3f6f79aaed0e5637f45d98850ebf01f7ca20e69457f3e8946b66a"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "matplotlib"
|
||||||
|
version = "3.10.8"
|
||||||
|
requires_python = ">=3.10"
|
||||||
|
summary = "Python plotting package"
|
||||||
|
groups = ["default"]
|
||||||
|
dependencies = [
|
||||||
|
"contourpy>=1.0.1",
|
||||||
|
"cycler>=0.10",
|
||||||
|
"fonttools>=4.22.0",
|
||||||
|
"kiwisolver>=1.3.1",
|
||||||
|
"numpy>=1.23",
|
||||||
|
"packaging>=20.0",
|
||||||
|
"pillow>=8",
|
||||||
|
"pyparsing>=3",
|
||||||
|
"python-dateutil>=2.7",
|
||||||
|
]
|
||||||
|
files = [
|
||||||
|
{file = "matplotlib-3.10.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b53285e65d4fa4c86399979e956235deb900be5baa7fc1218ea67fbfaeaadd6f"},
|
||||||
|
{file = "matplotlib-3.10.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32f8dce744be5569bebe789e46727946041199030db8aeb2954d26013a0eb26b"},
|
||||||
|
{file = "matplotlib-3.10.8-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4cf267add95b1c88300d96ca837833d4112756045364f5c734a2276038dae27d"},
|
||||||
|
{file = "matplotlib-3.10.8-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2cf5bd12cecf46908f286d7838b2abc6c91cda506c0445b8223a7c19a00df008"},
|
||||||
|
{file = "matplotlib-3.10.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:41703cc95688f2516b480f7f339d8851a6035f18e100ee6a32bc0b8536a12a9c"},
|
||||||
|
{file = "matplotlib-3.10.8-cp314-cp314-win_amd64.whl", hash = "sha256:83d282364ea9f3e52363da262ce32a09dfe241e4080dcedda3c0db059d3c1f11"},
|
||||||
|
{file = "matplotlib-3.10.8-cp314-cp314-win_arm64.whl", hash = "sha256:2c1998e92cd5999e295a731bcb2911c75f597d937341f3030cc24ef2733d78a8"},
|
||||||
|
{file = "matplotlib-3.10.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b5a2b97dbdc7d4f353ebf343744f1d1f1cca8aa8bfddb4262fcf4306c3761d50"},
|
||||||
|
{file = "matplotlib-3.10.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3f5c3e4da343bba819f0234186b9004faba952cc420fbc522dc4e103c1985908"},
|
||||||
|
{file = "matplotlib-3.10.8-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f62550b9a30afde8c1c3ae450e5eb547d579dd69b25c2fc7a1c67f934c1717a"},
|
||||||
|
{file = "matplotlib-3.10.8-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:495672de149445ec1b772ff2c9ede9b769e3cb4f0d0aa7fa730d7f59e2d4e1c1"},
|
||||||
|
{file = "matplotlib-3.10.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:595ba4d8fe983b88f0eec8c26a241e16d6376fe1979086232f481f8f3f67494c"},
|
||||||
|
{file = "matplotlib-3.10.8-cp314-cp314t-win_amd64.whl", hash = "sha256:25d380fe8b1dc32cf8f0b1b448470a77afb195438bafdf1d858bfb876f3edf7b"},
|
||||||
|
{file = "matplotlib-3.10.8-cp314-cp314t-win_arm64.whl", hash = "sha256:113bb52413ea508ce954a02c10ffd0d565f9c3bc7f2eddc27dfe1731e71c7b5f"},
|
||||||
|
{file = "matplotlib-3.10.8.tar.gz", hash = "sha256:2299372c19d56bcd35cf05a2738308758d32b9eaed2371898d8f5bd33f084aa3"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "numpy"
|
||||||
|
version = "2.4.4"
|
||||||
|
requires_python = ">=3.11"
|
||||||
|
summary = "Fundamental package for array computing in Python"
|
||||||
|
groups = ["default"]
|
||||||
|
files = [
|
||||||
|
{file = "numpy-2.4.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f169b9a863d34f5d11b8698ead99febeaa17a13ca044961aa8e2662a6c7766a0"},
|
||||||
|
{file = "numpy-2.4.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2483e4584a1cb3092da4470b38866634bafb223cbcd551ee047633fd2584599a"},
|
||||||
|
{file = "numpy-2.4.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:2d19e6e2095506d1736b7d80595e0f252d76b89f5e715c35e06e937679ea7d7a"},
|
||||||
|
{file = "numpy-2.4.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:6a246d5914aa1c820c9443ddcee9c02bec3e203b0c080349533fae17727dfd1b"},
|
||||||
|
{file = "numpy-2.4.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:989824e9faf85f96ec9c7761cd8d29c531ad857bfa1daa930cba85baaecf1a9a"},
|
||||||
|
{file = "numpy-2.4.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27a8d92cd10f1382a67d7cf4db7ce18341b66438bdd9f691d7b0e48d104c2a9d"},
|
||||||
|
{file = "numpy-2.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e44319a2953c738205bf3354537979eaa3998ed673395b964c1176083dd46252"},
|
||||||
|
{file = "numpy-2.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e892aff75639bbef0d2a2cfd55535510df26ff92f63c92cd84ef8d4ba5a5557f"},
|
||||||
|
{file = "numpy-2.4.4-cp314-cp314-win32.whl", hash = "sha256:1378871da56ca8943c2ba674530924bb8ca40cd228358a3b5f302ad60cf875fc"},
|
||||||
|
{file = "numpy-2.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:715d1c092715954784bc79e1174fc2a90093dc4dc84ea15eb14dad8abdcdeb74"},
|
||||||
|
{file = "numpy-2.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:2c194dd721e54ecad9ad387c1d35e63dce5c4450c6dc7dd5611283dda239aabb"},
|
||||||
|
{file = "numpy-2.4.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2aa0613a5177c264ff5921051a5719d20095ea586ca88cc802c5c218d1c67d3e"},
|
||||||
|
{file = "numpy-2.4.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:42c16925aa5a02362f986765f9ebabf20de75cdefdca827d14315c568dcab113"},
|
||||||
|
{file = "numpy-2.4.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:874f200b2a981c647340f841730fc3a2b54c9d940566a3c4149099591e2c4c3d"},
|
||||||
|
{file = "numpy-2.4.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9b39d38a9bd2ae1becd7eac1303d031c5c110ad31f2b319c6e7d98b135c934d"},
|
||||||
|
{file = "numpy-2.4.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b268594bccac7d7cf5844c7732e3f20c50921d94e36d7ec9b79e9857694b1b2f"},
|
||||||
|
{file = "numpy-2.4.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac6b31e35612a26483e20750126d30d0941f949426974cace8e6b5c58a3657b0"},
|
||||||
|
{file = "numpy-2.4.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8e3ed142f2728df44263aaf5fb1f5b0b99f4070c553a0d7f033be65338329150"},
|
||||||
|
{file = "numpy-2.4.4-cp314-cp314t-win32.whl", hash = "sha256:dddbbd259598d7240b18c9d87c56a9d2fb3b02fe266f49a7c101532e78c1d871"},
|
||||||
|
{file = "numpy-2.4.4-cp314-cp314t-win_amd64.whl", hash = "sha256:a7164afb23be6e37ad90b2f10426149fd75aee07ca55653d2aa41e66c4ef697e"},
|
||||||
|
{file = "numpy-2.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:ba203255017337d39f89bdd58417f03c4426f12beed0440cfd933cb15f8669c7"},
|
||||||
|
{file = "numpy-2.4.4.tar.gz", hash = "sha256:2d390634c5182175533585cc89f3608a4682ccb173cc9bb940b2881c8d6f8fa0"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "openpyxl"
|
||||||
|
version = "3.1.5"
|
||||||
|
requires_python = ">=3.8"
|
||||||
|
summary = "A Python library to read/write Excel 2010 xlsx/xlsm files"
|
||||||
|
groups = ["default"]
|
||||||
|
dependencies = [
|
||||||
|
"et-xmlfile",
|
||||||
|
]
|
||||||
|
files = [
|
||||||
|
{file = "openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2"},
|
||||||
|
{file = "openpyxl-3.1.5.tar.gz", hash = "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "packaging"
|
||||||
|
version = "26.1"
|
||||||
|
requires_python = ">=3.8"
|
||||||
|
summary = "Core utilities for Python packages"
|
||||||
|
groups = ["default"]
|
||||||
|
files = [
|
||||||
|
{file = "packaging-26.1-py3-none-any.whl", hash = "sha256:5d9c0669c6285e491e0ced2eee587eaf67b670d94a19e94e3984a481aba6802f"},
|
||||||
|
{file = "packaging-26.1.tar.gz", hash = "sha256:f042152b681c4bfac5cae2742a55e103d27ab2ec0f3d88037136b6bfe7c9c5de"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pillow"
|
||||||
|
version = "12.2.0"
|
||||||
|
requires_python = ">=3.10"
|
||||||
|
summary = "Python Imaging Library (fork)"
|
||||||
|
groups = ["default"]
|
||||||
|
files = [
|
||||||
|
{file = "pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8"},
|
||||||
|
{file = "pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b"},
|
||||||
|
{file = "pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295"},
|
||||||
|
{file = "pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed"},
|
||||||
|
{file = "pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae"},
|
||||||
|
{file = "pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601"},
|
||||||
|
{file = "pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be"},
|
||||||
|
{file = "pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f"},
|
||||||
|
{file = "pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286"},
|
||||||
|
{file = "pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50"},
|
||||||
|
{file = "pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104"},
|
||||||
|
{file = "pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7"},
|
||||||
|
{file = "pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150"},
|
||||||
|
{file = "pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1"},
|
||||||
|
{file = "pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463"},
|
||||||
|
{file = "pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3"},
|
||||||
|
{file = "pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166"},
|
||||||
|
{file = "pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe"},
|
||||||
|
{file = "pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd"},
|
||||||
|
{file = "pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e"},
|
||||||
|
{file = "pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06"},
|
||||||
|
{file = "pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43"},
|
||||||
|
{file = "pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354"},
|
||||||
|
{file = "pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1"},
|
||||||
|
{file = "pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb"},
|
||||||
|
{file = "pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyparsing"
|
||||||
|
version = "3.3.2"
|
||||||
|
requires_python = ">=3.9"
|
||||||
|
summary = "pyparsing - Classes and methods to define and execute parsing grammars"
|
||||||
|
groups = ["default"]
|
||||||
|
files = [
|
||||||
|
{file = "pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d"},
|
||||||
|
{file = "pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "python-dateutil"
|
||||||
|
version = "2.9.0.post0"
|
||||||
|
requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
|
||||||
|
summary = "Extensions to the standard Python datetime module"
|
||||||
|
groups = ["default"]
|
||||||
|
dependencies = [
|
||||||
|
"six>=1.5",
|
||||||
|
]
|
||||||
|
files = [
|
||||||
|
{file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"},
|
||||||
|
{file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "scipy"
|
||||||
|
version = "1.17.1"
|
||||||
|
requires_python = ">=3.11"
|
||||||
|
summary = "Fundamental algorithms for scientific computing in Python"
|
||||||
|
groups = ["default"]
|
||||||
|
dependencies = [
|
||||||
|
"numpy<2.7,>=1.26.4",
|
||||||
|
]
|
||||||
|
files = [
|
||||||
|
{file = "scipy-1.17.1-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:a48a72c77a310327f6a3a920092fa2b8fd03d7deaa60f093038f22d98e096717"},
|
||||||
|
{file = "scipy-1.17.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:45abad819184f07240d8a696117a7aacd39787af9e0b719d00285549ed19a1e9"},
|
||||||
|
{file = "scipy-1.17.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3fd1fcdab3ea951b610dc4cef356d416d5802991e7e32b5254828d342f7b7e0b"},
|
||||||
|
{file = "scipy-1.17.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7bdf2da170b67fdf10bca777614b1c7d96ae3ca5794fd9587dce41eb2966e866"},
|
||||||
|
{file = "scipy-1.17.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:adb2642e060a6549c343603a3851ba76ef0b74cc8c079a9a58121c7ec9fe2350"},
|
||||||
|
{file = "scipy-1.17.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eee2cfda04c00a857206a4330f0c5e3e56535494e30ca445eb19ec624ae75118"},
|
||||||
|
{file = "scipy-1.17.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d2650c1fb97e184d12d8ba010493ee7b322864f7d3d00d3f9bb97d9c21de4068"},
|
||||||
|
{file = "scipy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:08b900519463543aa604a06bec02461558a6e1cef8fdbb8098f77a48a83c8118"},
|
||||||
|
{file = "scipy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:3877ac408e14da24a6196de0ddcace62092bfc12a83823e92e49e40747e52c19"},
|
||||||
|
{file = "scipy-1.17.1-cp314-cp314-win_arm64.whl", hash = "sha256:f8885db0bc2bffa59d5c1b72fad7a6a92d3e80e7257f967dd81abb553a90d293"},
|
||||||
|
{file = "scipy-1.17.1-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:1cc682cea2ae55524432f3cdff9e9a3be743d52a7443d0cba9017c23c87ae2f6"},
|
||||||
|
{file = "scipy-1.17.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:2040ad4d1795a0ae89bfc7e8429677f365d45aa9fd5e4587cf1ea737f927b4a1"},
|
||||||
|
{file = "scipy-1.17.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:131f5aaea57602008f9822e2115029b55d4b5f7c070287699fe45c661d051e39"},
|
||||||
|
{file = "scipy-1.17.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9cdc1a2fcfd5c52cfb3045feb399f7b3ce822abdde3a193a6b9a60b3cb5854ca"},
|
||||||
|
{file = "scipy-1.17.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e3dcd57ab780c741fde8dc68619de988b966db759a3c3152e8e9142c26295ad"},
|
||||||
|
{file = "scipy-1.17.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9956e4d4f4a301ebf6cde39850333a6b6110799d470dbbb1e25326ac447f52a"},
|
||||||
|
{file = "scipy-1.17.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a4328d245944d09fd639771de275701ccadf5f781ba0ff092ad141e017eccda4"},
|
||||||
|
{file = "scipy-1.17.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a77cbd07b940d326d39a1d1b37817e2ee4d79cb30e7338f3d0cddffae70fcaa2"},
|
||||||
|
{file = "scipy-1.17.1-cp314-cp314t-win_amd64.whl", hash = "sha256:eb092099205ef62cd1782b006658db09e2fed75bffcae7cc0d44052d8aa0f484"},
|
||||||
|
{file = "scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21"},
|
||||||
|
{file = "scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "six"
|
||||||
|
version = "1.17.0"
|
||||||
|
requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
|
||||||
|
summary = "Python 2 and 3 compatibility utilities"
|
||||||
|
groups = ["default"]
|
||||||
|
files = [
|
||||||
|
{file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"},
|
||||||
|
{file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typing-extensions"
|
||||||
|
version = "4.15.0"
|
||||||
|
requires_python = ">=3.9"
|
||||||
|
summary = "Backported and Experimental Type Hints for Python 3.9+"
|
||||||
|
groups = ["default"]
|
||||||
|
files = [
|
||||||
|
{file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"},
|
||||||
|
{file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "xlrd"
|
||||||
|
version = "2.0.2"
|
||||||
|
requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7"
|
||||||
|
summary = "Library for developers to extract data from Microsoft Excel (tm) .xls spreadsheet files"
|
||||||
|
groups = ["default"]
|
||||||
|
files = [
|
||||||
|
{file = "xlrd-2.0.2-py2.py3-none-any.whl", hash = "sha256:ea762c3d29f4cca48d82df517b6d89fbce4db3107f9d78713e48cd321d5c9aa9"},
|
||||||
|
{file = "xlrd-2.0.2.tar.gz", hash = "sha256:08b5e25de58f21ce71dc7db3b3b8106c1fa776f3024c54e45b45b374e89234c9"},
|
||||||
|
]
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
[project]
|
||||||
|
name = "surveyladder"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Default template for PDM package"
|
||||||
|
authors = [
|
||||||
|
{name = "Alexander Rossmanith", email = ""},
|
||||||
|
]
|
||||||
|
dependencies = ["ezdxf>=1.4.3", "matplotlib>=3.10.8", "numpy>=2.4.4", "xlrd>=2.0.2", "openpyxl>=3.1.5", "scipy>=1.17.1"]
|
||||||
|
requires-python = "==3.14.*"
|
||||||
|
readme = "README.md"
|
||||||
|
license = {text = "MIT"}
|
||||||
|
|
||||||
|
|
||||||
|
[tool.pdm]
|
||||||
|
distribution = false
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import pathlib
|
||||||
|
|
||||||
|
script_file = pathlib.Path(__file__)
|
||||||
|
script_dir = script_file.parent
|
||||||
|
project_root = script_dir.parent
|
||||||
|
dotenv_path = project_root / '.env'
|
||||||
|
|
||||||
|
with open(dotenv_path, 'w') as f:
|
||||||
|
f.write(f"# This file is automatically generated by the setup script {script_dir.name}/{script_file.name}. Do not edit it manually.\n")
|
||||||
|
f.write(f"PYTHONPATH={project_root};{project_root / 'surveyladder'}\n")
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
import ezdxf
|
||||||
|
import numpy
|
||||||
|
|
||||||
|
from geo import bounding_box, poly_area, poly_centroid
|
||||||
|
|
||||||
|
|
||||||
|
def to_dxf(sketch, layer_prefix=None):
|
||||||
|
doc = ezdxf.new('R2010')
|
||||||
|
|
||||||
|
if layer_prefix is None:
|
||||||
|
layer_prefix = ""
|
||||||
|
|
||||||
|
# --- Define layers upfront ---
|
||||||
|
# name, color, linetype, lineweight, visible
|
||||||
|
# Colors are given by AutoCAD color index ACI
|
||||||
|
# 1= red, 2 = yellow, 3 = green, 4 = cyan, 5 = blue, 6 = magenta, 7 = white
|
||||||
|
layers = [
|
||||||
|
("LINES", 2, "Continuous", 0.25, True),
|
||||||
|
("SUBDIVISIONS", 7, "Continuous", 0.35, True),
|
||||||
|
("SUBDIVISION_LABELS", 7, "Continuous", 0.00, True),
|
||||||
|
("CORNER_POINTS", 1, "Continuous", 0.25, True),
|
||||||
|
("CORNER_LABELS", 1, "Continuous", 0.00, True),
|
||||||
|
("CORNER_DISTANCES", 1, "Continuous", 0.25, True),
|
||||||
|
("TIE_TABLE_POINTS", 6, "Continuous", 0.25, True),
|
||||||
|
("TIE_TABLE_LABELS", 6, "Continuous", 0.00, True),
|
||||||
|
("CHAINAGE_POINTS", 4, "Continuous", 0.25, True),
|
||||||
|
("CHAINAGE_LABELS", 4, "Continuous", 0.00, True),
|
||||||
|
]
|
||||||
|
for name, color, linetype, lineweight, visible in layers:
|
||||||
|
if name not in doc.layers:
|
||||||
|
layer = doc.layers.new(name=layer_prefix+name, dxfattribs={
|
||||||
|
"color": color,
|
||||||
|
"linetype": linetype,
|
||||||
|
"lineweight": lineweight,
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
layer = doc.layers[layer_prefix+name]
|
||||||
|
if visible:
|
||||||
|
layer.on()
|
||||||
|
else:
|
||||||
|
layer.off()
|
||||||
|
|
||||||
|
msp = doc.modelspace()
|
||||||
|
|
||||||
|
point_positions = sketch.get_point_positions()
|
||||||
|
|
||||||
|
# --- Draw struts (lines) ---
|
||||||
|
for strut in sketch.struts:
|
||||||
|
endpoint_positions = [point_positions[label] for label in strut]
|
||||||
|
msp.add_line(*endpoint_positions, dxfattribs={'layer': layer_prefix+'LINES'})
|
||||||
|
|
||||||
|
# --- Draw subdivision polygons + labels ---
|
||||||
|
for subdivision_label, poly_labels in sketch.subdivisions.items():
|
||||||
|
if len(poly_labels) > 0:
|
||||||
|
polygon = numpy.array([point_positions[label] for label in poly_labels])
|
||||||
|
msp.add_lwpolyline(polygon, close=True, dxfattribs={'layer': layer_prefix+'SUBDIVISIONS'})
|
||||||
|
area = abs(poly_area(polygon))
|
||||||
|
centroid = poly_centroid(polygon)
|
||||||
|
mtext = msp.add_mtext(
|
||||||
|
f"{subdivision_label}\n{area:.0f} sq.m\n{area/40.47:.1f} cents",
|
||||||
|
dxfattribs={'layer': layer_prefix+'SUBDIVISION_LABELS', 'char_height': 2.5}
|
||||||
|
)
|
||||||
|
mtext.set_location((centroid[0], centroid[1]))
|
||||||
|
mtext.dxf.attachment_point = 5 # MIDDLE_CENTER
|
||||||
|
|
||||||
|
# --- Draw points + labels ---
|
||||||
|
for point in sketch.points:
|
||||||
|
if point.source:
|
||||||
|
points_layer = point.source.upper() + "_POINTS"
|
||||||
|
labels_layer = point.source.upper() + "_LABELS"
|
||||||
|
else:
|
||||||
|
points_layer = 'POINTS'
|
||||||
|
labels_layer = 'LABELS'
|
||||||
|
msp.add_point((point.pos[0], point.pos[1]), dxfattribs={'layer': layer_prefix+points_layer})
|
||||||
|
msp.add_text(
|
||||||
|
point.label,
|
||||||
|
dxfattribs={
|
||||||
|
'insert': (point.pos[0], point.pos[1]),
|
||||||
|
'height': 2.5,
|
||||||
|
'layer': layer_prefix+labels_layer
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Draw dimension lines
|
||||||
|
|
||||||
|
for dim in sketch.dimensions:
|
||||||
|
label1, label2, distance = dim
|
||||||
|
pos1 = point_positions[label1]
|
||||||
|
pos2 = point_positions[label2]
|
||||||
|
dim_entity = msp.add_aligned_dim(
|
||||||
|
p1=(pos1[0], pos1[1]),
|
||||||
|
p2=(pos2[0], pos2[1]),
|
||||||
|
distance=0,
|
||||||
|
text=f"{distance:.1f}",
|
||||||
|
dxfattribs={'layer': layer_prefix+'CORNER_DISTANCES'}
|
||||||
|
)
|
||||||
|
dim_entity.set_text_align(halign="center", valign="below")
|
||||||
|
dim_entity.render()
|
||||||
|
|
||||||
|
# --- Set up the viewport to fit the sketch ---
|
||||||
|
|
||||||
|
xmin, xmax, ymin, ymax = bounding_box(point_positions.values())
|
||||||
|
cx = (xmin + xmax) / 2
|
||||||
|
cy = (ymin + ymax) / 2
|
||||||
|
doc.set_modelspace_vport(
|
||||||
|
center=(cx, cy), # center of the view
|
||||||
|
height=(ymax - ymin), # vertical size of the view
|
||||||
|
)
|
||||||
|
return doc
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def merge_dxf(doc1: ezdxf.document.Drawing, doc2: ezdxf.document.Drawing,
|
||||||
|
prefix1: str = "", prefix2: str = "") -> ezdxf.document.Drawing:
|
||||||
|
"""
|
||||||
|
Merge two DXF documents into one. Entities from doc1 go to layers prefixed with prefix1,
|
||||||
|
entities from doc2 go to layers prefixed with prefix2. Layer attributes are preserved.
|
||||||
|
"""
|
||||||
|
merged_doc = ezdxf.new(doc1.dxfversion)
|
||||||
|
msp = merged_doc.modelspace()
|
||||||
|
|
||||||
|
def ensure_layer(src_layer, prefix):
|
||||||
|
name = f"{prefix}{src_layer.dxf.name}"
|
||||||
|
if name in merged_doc.layers:
|
||||||
|
return name
|
||||||
|
# src_layer.dxfattribs() returns a dict; remove keys not allowed by layers.new
|
||||||
|
attribs = src_layer.dxfattribs().copy()
|
||||||
|
attribs.pop("name", None)
|
||||||
|
# create new layer with same attributes
|
||||||
|
merged_doc.layers.new(name=name, dxfattribs=attribs)
|
||||||
|
return name
|
||||||
|
|
||||||
|
# copy layers from both docs (with prefix)
|
||||||
|
for layer in doc1.layers:
|
||||||
|
ensure_layer(layer, prefix1)
|
||||||
|
for layer in doc2.layers:
|
||||||
|
ensure_layer(layer, prefix2)
|
||||||
|
|
||||||
|
def copy_entities(src_doc, prefix):
|
||||||
|
for e in src_doc.modelspace():
|
||||||
|
# get source layer name (some entities may not have dxf.layer)
|
||||||
|
src_layer = getattr(e.dxf, "layer", None)
|
||||||
|
target_layer = f"{prefix}{src_layer}" if src_layer else None
|
||||||
|
|
||||||
|
# make a deep copy of entity
|
||||||
|
new = e.copy()
|
||||||
|
|
||||||
|
# assign to target layer if available
|
||||||
|
if target_layer:
|
||||||
|
# ensure the layer exists (defensive)
|
||||||
|
if target_layer not in merged_doc.layers:
|
||||||
|
merged_doc.layers.new(name=target_layer)
|
||||||
|
try:
|
||||||
|
new.dxf.layer = target_layer
|
||||||
|
except Exception:
|
||||||
|
# some entity types may not support layer assignment; skip setting layer
|
||||||
|
pass
|
||||||
|
|
||||||
|
# add entity to merged modelspace via msp.add_entity or modelspace add functions
|
||||||
|
# prefer msp.add_entity for a direct copy object
|
||||||
|
if isinstance(new, ezdxf.entities.DXFEntity):
|
||||||
|
msp.add_entity(new)
|
||||||
|
else:
|
||||||
|
# fallback: use generic add if needed (should rarely happen)
|
||||||
|
msp.add_entity(new)
|
||||||
|
|
||||||
|
copy_entities(doc1, prefix1)
|
||||||
|
copy_entities(doc2, prefix2)
|
||||||
|
|
||||||
|
return merged_doc
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
import math
|
||||||
|
|
||||||
|
import numpy
|
||||||
|
|
||||||
|
|
||||||
|
def distance(p1, p2):
|
||||||
|
return numpy.linalg.norm(p2 - p1)
|
||||||
|
|
||||||
|
|
||||||
|
def poly_area(polygon):
|
||||||
|
"""
|
||||||
|
returns signed polygon area
|
||||||
|
positive if points are ordered counter-clockwise,
|
||||||
|
negative if points are ordered clockwise.
|
||||||
|
"""
|
||||||
|
assert polygon.shape[0] >= 3, "polygon should have at least 3 points"
|
||||||
|
assert polygon.shape[1] == 2, "polygon should be a list of 2D points"
|
||||||
|
x = polygon[:, 0]
|
||||||
|
y = polygon[:, 1]
|
||||||
|
return 0.5 * (numpy.dot(y, numpy.roll(x, 1)) - numpy.dot(x, numpy.roll(y, 1)))
|
||||||
|
|
||||||
|
|
||||||
|
def poly_centroid(polygon):
|
||||||
|
"""
|
||||||
|
returns polygon centroid
|
||||||
|
"""
|
||||||
|
assert polygon.shape[0] >= 3, "polygon should have at least 3 points"
|
||||||
|
assert polygon.shape[1] == 2, "polygon should be a list of 2D points"
|
||||||
|
x = polygon[:, 0]
|
||||||
|
y = polygon[:, 1]
|
||||||
|
A = poly_area(polygon) # signed area
|
||||||
|
factor = (y * numpy.roll(x, 1) - numpy.roll(y, 1) * x)
|
||||||
|
Cx = (1/(6*A)) * numpy.sum((x + numpy.roll(x, 1)) * factor)
|
||||||
|
Cy = (1/(6*A)) * numpy.sum((y + numpy.roll(y, 1)) * factor)
|
||||||
|
return numpy.array([Cx, Cy])
|
||||||
|
|
||||||
|
|
||||||
|
def chainage_to_xy(A, B, chainage):
|
||||||
|
"""
|
||||||
|
Gets point on line from A to B
|
||||||
|
|
||||||
|
Args:
|
||||||
|
A: Start point of line
|
||||||
|
B: End point of line
|
||||||
|
chainage: distance from A of point on line
|
||||||
|
"""
|
||||||
|
vec_ab = B-A
|
||||||
|
return A + vec_ab / numpy.linalg.norm(vec_ab) * chainage
|
||||||
|
|
||||||
|
|
||||||
|
def chainage_offset_to_xy(A, B, chainage, offset):
|
||||||
|
"""
|
||||||
|
Gets point with perpendicular offset on line from B to A.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
A: Start point of line
|
||||||
|
B: End point of line
|
||||||
|
chainage: distance from A of closest point on line
|
||||||
|
offset: perpendicular distance of point from line
|
||||||
|
"""
|
||||||
|
P = chainage_to_xy(A, B, chainage)
|
||||||
|
line_direction = B-A
|
||||||
|
perpendicular_direction = numpy.array([-line_direction[1], line_direction[0]])
|
||||||
|
Q = chainage_to_xy(P, P + perpendicular_direction, offset)
|
||||||
|
return Q
|
||||||
|
|
||||||
|
|
||||||
|
def trilaterate_xy(A: str, a: float, B: str, b: float) -> numpy.ndarray:
|
||||||
|
"""
|
||||||
|
Calculates the coordinates of point C of a triangle given by two points
|
||||||
|
A and B and the distances a and b from these points to C.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
A: coordinates of point A as a numpy array [x, y]
|
||||||
|
a: distance from point A to point C
|
||||||
|
B: coordinates of point B as a numpy array [x, y]
|
||||||
|
b: distance from point B to point C
|
||||||
|
"""
|
||||||
|
vec_c = B-A
|
||||||
|
c = numpy.linalg.norm(vec_c)
|
||||||
|
orientation_c = math.atan2(vec_c[1], vec_c[0])
|
||||||
|
cos_alpha = (a*a+c*c-b*b)/(2*a*c) # cosine rule
|
||||||
|
alpha = math.acos(cos_alpha)
|
||||||
|
cx = A[0] + a * math.cos(orientation_c + alpha)
|
||||||
|
cy = A[1] + a * math.sin(orientation_c + alpha)
|
||||||
|
C = numpy.array([cx, cy])
|
||||||
|
return C
|
||||||
|
|
||||||
|
|
||||||
|
def intersect_lines_xy(p, q):
|
||||||
|
"""
|
||||||
|
Find intersection point of two lines.
|
||||||
|
Each point is a tuple (x, y).
|
||||||
|
Returns (x, y) if intersection exists, else None if lines are parallel.
|
||||||
|
"""
|
||||||
|
p1, p2 = p
|
||||||
|
q1, q2 = q
|
||||||
|
|
||||||
|
# Direction vectors
|
||||||
|
d1 = p2 - p1
|
||||||
|
d2 = q2 - q1
|
||||||
|
|
||||||
|
# Solve system: p1 + t*d1 = q1 + u*d2
|
||||||
|
A = numpy.array([[d1[0], -d2[0]], [d1[1], -d2[1]]])
|
||||||
|
b = q1 - p1
|
||||||
|
|
||||||
|
det = numpy.linalg.det(A)
|
||||||
|
if abs(det) < 1e-10:
|
||||||
|
# Lines are parallel (or coincident)
|
||||||
|
return None
|
||||||
|
|
||||||
|
t, u = numpy.linalg.solve(A, b)
|
||||||
|
intersection = p1 + t * d1
|
||||||
|
return intersection
|
||||||
|
|
||||||
|
|
||||||
|
def get_point_by_distance_at_angle(P, dist, angle_deg):
|
||||||
|
angle = numpy.deg2rad(angle_deg)
|
||||||
|
dir_vec = numpy.array([numpy.cos(angle), numpy.sin(angle)])
|
||||||
|
Q = P + dist * dir_vec
|
||||||
|
return Q
|
||||||
|
|
||||||
|
|
||||||
|
def bounding_box(positions):
|
||||||
|
xs = [p[0] for p in positions]
|
||||||
|
ys = [p[1] for p in positions]
|
||||||
|
return min(xs), max(xs), min(ys), max(ys)
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
import csv
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from solver import DirectSolver
|
||||||
|
import openpyxl
|
||||||
|
|
||||||
|
from util import is_corner_label
|
||||||
|
|
||||||
|
from sketch import Point
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class OffsetPoint:
|
||||||
|
"""
|
||||||
|
A point on the ladder with a label, distance along the line, and offset from the line.
|
||||||
|
Together with the definition of the line (start and end points), this allows us to calculate the
|
||||||
|
position of the point in 2D space.
|
||||||
|
"""
|
||||||
|
|
||||||
|
label: str
|
||||||
|
distance_on_line: float
|
||||||
|
offset: float
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class LadderSegment:
|
||||||
|
|
||||||
|
"""
|
||||||
|
A segment between two seed points, containing the distance between the seed points and
|
||||||
|
all the offset points in between.
|
||||||
|
"""
|
||||||
|
|
||||||
|
start_label: str
|
||||||
|
end_label: str
|
||||||
|
distance: float
|
||||||
|
offset_points: list[OffsetPoint]
|
||||||
|
|
||||||
|
|
||||||
|
def extract_all_corner_labels(ladder_segments):
|
||||||
|
corner_labels = list()
|
||||||
|
for segment in ladder_segments:
|
||||||
|
if segment.start_label not in corner_labels:
|
||||||
|
corner_labels.append(segment.start_label)
|
||||||
|
if segment.end_label not in corner_labels:
|
||||||
|
corner_labels.append(segment.end_label)
|
||||||
|
return sorted(corner_labels)
|
||||||
|
|
||||||
|
|
||||||
|
def load_ladder_xls(filename):
|
||||||
|
"""
|
||||||
|
Loads survey ladder from xlsx file
|
||||||
|
"""
|
||||||
|
workbook = openpyxl.load_workbook(filename)
|
||||||
|
worksheet = workbook.active
|
||||||
|
rows = []
|
||||||
|
def to_str(val):
|
||||||
|
if val is None:
|
||||||
|
return ""
|
||||||
|
return str(val)
|
||||||
|
for row_index in range(1, worksheet.max_row + 1):
|
||||||
|
row = [to_str(worksheet.cell(row=row_index, column=col_index).value) for col_index in range(1, 6)]
|
||||||
|
rows.append(row)
|
||||||
|
return create_ladder(rows)
|
||||||
|
|
||||||
|
|
||||||
|
def load_ladder_csv(filename, invert_segments=False):
|
||||||
|
"""
|
||||||
|
Loads survey ladder from csv file
|
||||||
|
"""
|
||||||
|
rows = load_csv_rows(filename)
|
||||||
|
return create_ladder(rows, invert_segments=invert_segments)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def load_chainage_csv(filename):
|
||||||
|
"""
|
||||||
|
Loads chainage points from csv file
|
||||||
|
"""
|
||||||
|
return load_csv_rows(filename)
|
||||||
|
|
||||||
|
|
||||||
|
def load_csv_rows(filename):
|
||||||
|
with open(filename, newline="") as csvfile:
|
||||||
|
reader = csv.reader(csvfile)
|
||||||
|
return [row for row in reader]
|
||||||
|
|
||||||
|
|
||||||
|
def create_ladder(rows, invert_segments=False):
|
||||||
|
"""
|
||||||
|
Creates a survey ladder from a list of rows, where each row is a 5-tuple of (left_label, left_offset, distance_on_line, right_offset, right_label).
|
||||||
|
"""
|
||||||
|
# column 1 contains the labels of the ladder points on the left side.
|
||||||
|
# column 2 contains the offsets of the ladder points on the left side.
|
||||||
|
# column 3 contains the labels of the reference points, and the distances between them.
|
||||||
|
# column 4 contains the offsets of the ladder points on the right side.
|
||||||
|
# column 5 contains the distances of the ladder points on the right side.
|
||||||
|
segments = []
|
||||||
|
for (start_row, end_row) in get_segment_borders(rows):
|
||||||
|
start_label = str(rows[start_row][2])
|
||||||
|
end_label = str(rows[end_row][2])
|
||||||
|
if invert_segments:
|
||||||
|
start_label, end_label = end_label, start_label
|
||||||
|
distance = parse_distance(rows[start_row+1][2])
|
||||||
|
offset_points = []
|
||||||
|
for i in range(start_row + 2, end_row):
|
||||||
|
offset_points.extend(parse_offset_points(rows[i]))
|
||||||
|
segments.append(LadderSegment(start_label, end_label, distance, offset_points))
|
||||||
|
return segments
|
||||||
|
|
||||||
|
|
||||||
|
def parse_offset_points(row):
|
||||||
|
points = []
|
||||||
|
distance_on_line = parse_distance(row[2])
|
||||||
|
left_label = row[0]
|
||||||
|
right_label = row[4]
|
||||||
|
if left_label:
|
||||||
|
left_label = str(left_label)
|
||||||
|
left_offset = parse_distance(row[1])
|
||||||
|
points.append(OffsetPoint(left_label, distance_on_line, left_offset))
|
||||||
|
if right_label:
|
||||||
|
right_label = str(right_label)
|
||||||
|
right_offset = parse_distance(row[3])
|
||||||
|
points.append(OffsetPoint(right_label, distance_on_line, -right_offset))
|
||||||
|
return points
|
||||||
|
|
||||||
|
|
||||||
|
def get_segment_borders(rows):
|
||||||
|
segment_borders = []
|
||||||
|
start_row = None
|
||||||
|
for i, row in enumerate(rows):
|
||||||
|
if is_corner_label(row[2]):
|
||||||
|
if start_row is None:
|
||||||
|
start_row = i
|
||||||
|
else:
|
||||||
|
end_row = i
|
||||||
|
segment_borders.append((start_row, end_row))
|
||||||
|
start_row = None
|
||||||
|
return segment_borders
|
||||||
|
|
||||||
|
|
||||||
|
def parse_distance(val):
|
||||||
|
if isinstance(val, str):
|
||||||
|
val = val.strip().lower()
|
||||||
|
if val == "line":
|
||||||
|
return 0.0
|
||||||
|
if (val.startswith("(") and val.endswith(")")):
|
||||||
|
val = val[1:-1]
|
||||||
|
return float(val)
|
||||||
|
else:
|
||||||
|
return float(val)
|
||||||
|
|
||||||
|
|
||||||
|
def get_corner_points(ladder_segments):
|
||||||
|
origin_label = ladder_segments[0].start_label
|
||||||
|
corner_labels = extract_all_corner_labels(ladder_segments)
|
||||||
|
solver = DirectSolver(corner_labels, origin_label=origin_label)
|
||||||
|
for segment in ladder_segments:
|
||||||
|
solver.add_distance(segment.start_label, segment.end_label, segment.distance)
|
||||||
|
for offset_point in segment.offset_points:
|
||||||
|
if is_corner_label(offset_point.label):
|
||||||
|
solver.add_offset(offset_point.label, segment.start_label, segment.end_label, offset_point.distance_on_line, offset_point.offset)
|
||||||
|
P = solver.solve()
|
||||||
|
points = list()
|
||||||
|
for label in corner_labels:
|
||||||
|
points.append(Point(label, P[label], "corner"))
|
||||||
|
return points
|
||||||
@@ -0,0 +1,369 @@
|
|||||||
|
import json
|
||||||
|
import math
|
||||||
|
import pathlib
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from geo import intersect_lines_xy, chainage_to_xy, get_point_by_distance_at_angle, trilaterate_xy
|
||||||
|
from ladder import load_ladder_csv, load_chainage_csv, get_corner_points
|
||||||
|
from sketch import Sketch, Point
|
||||||
|
from dxf import to_dxf, merge_dxf
|
||||||
|
from plot import plot
|
||||||
|
|
||||||
|
def main():
|
||||||
|
|
||||||
|
sketch_1987 = load_sketch(
|
||||||
|
R"C:\Data\Rat killer\Survey Sketches\Official\1987-12-01\sketch.json")
|
||||||
|
dxf_1987 = to_dxf(sketch_1987)
|
||||||
|
dxf_1987.saveas("survey_1987_sketch.dxf")
|
||||||
|
|
||||||
|
sketch_2006 = load_sketch(
|
||||||
|
R"C:\Data\Rat killer\Survey Sketches\Official\2006-10-16 1016 subdivision 1,3,2,4,5,6\sketch.json")
|
||||||
|
dxf_2006 = to_dxf(sketch_2006)
|
||||||
|
dxf_2006.saveas("survey_2006_sketch.dxf")
|
||||||
|
|
||||||
|
merge_dxf(dxf_1987, dxf_2006, "1987", "2006").saveas("survey_1987_2006_sketch.dxf")
|
||||||
|
|
||||||
|
def load_sketch(sketchinfo_filepath):
|
||||||
|
sketchinfo_filepath = pathlib.Path(sketchinfo_filepath)
|
||||||
|
sketch_dir = sketchinfo_filepath.parent
|
||||||
|
sketchinfo = json.load(open(sketchinfo_filepath))
|
||||||
|
sketch = Sketch()
|
||||||
|
invert_segments = sketchinfo["tie_table"]["invert_segments"]
|
||||||
|
ladder = load_ladder_csv(sketch_dir/"tie_table.csv", invert_segments=invert_segments)
|
||||||
|
P = get_corner_points(ladder)
|
||||||
|
sketch = Sketch(points=P, subdivisions=sketchinfo["subdivisions"])
|
||||||
|
sketch.add_ladder_points(ladder)
|
||||||
|
point_positions = sketch.get_point_positions()
|
||||||
|
chainage_points = load_chainage_csv(sketch_dir / "chainage.csv")
|
||||||
|
for label, start_label, end_label, chainage in chainage_points:
|
||||||
|
start_point = point_positions[start_label]
|
||||||
|
end_point = point_positions[end_label]
|
||||||
|
chainage = float(chainage)
|
||||||
|
point_positions[label] = chainage_to_xy(start_point, end_point, chainage)
|
||||||
|
sketch.points.append(Point(label, chainage_to_xy(start_point, end_point, chainage), "chainage"))
|
||||||
|
sketch.reorient(
|
||||||
|
sketchinfo["origin"],
|
||||||
|
sketchinfo["northing"]["line"],
|
||||||
|
math.radians(sketchinfo["northing"]["orientation"]))
|
||||||
|
for seg in ladder:
|
||||||
|
sketch.add_dimension_line(seg.start_label, seg.end_label, seg.distance)
|
||||||
|
return sketch
|
||||||
|
|
||||||
|
|
||||||
|
def survey_shibu_ikka_sketch_march7():
|
||||||
|
P = dict()
|
||||||
|
P["D"] = np.array([0, 0])
|
||||||
|
triangulation = [
|
||||||
|
("C", "D", 130.5),
|
||||||
|
("C", "F", 138.5),
|
||||||
|
("C", "G", 133.2),
|
||||||
|
("D", "F", 161.3),
|
||||||
|
]
|
||||||
|
P["F"] = np.array(P["D"] + [0, -161.3])
|
||||||
|
P["C"] = trilaterate_xy(P["F"], 138.5, P["D"], 130.5)
|
||||||
|
P["G"] = trilaterate_xy(P["F"], 157.6, P["C"], 133.2)
|
||||||
|
P["B"] = trilaterate_xy(P["F"], 145.5, P["C"], 12.3)
|
||||||
|
P["A"] = trilaterate_xy(P["G"], 120.8, P["B"], 58.0)
|
||||||
|
sketch = Sketch(points=P)
|
||||||
|
|
||||||
|
ladder = ([
|
||||||
|
("F", "D", [
|
||||||
|
("29", 2.7, 153.5),
|
||||||
|
("28", 12.9, 123.2),
|
||||||
|
("27", 13.2, 114.6),
|
||||||
|
("E", -2.1, 161.2),
|
||||||
|
("14", 11.2, 106.9),
|
||||||
|
("26", 10.4, 104.2),
|
||||||
|
("25", 24.6, 99.9),
|
||||||
|
("24", 28.4, 99.0),
|
||||||
|
("23", 29.4, 98.2),
|
||||||
|
("22", 23.5, 97.2),
|
||||||
|
("21", 28.7, 91.8),
|
||||||
|
("21", 27.5, 81.8),
|
||||||
|
("20", 24.2, 52.6),
|
||||||
|
]),
|
||||||
|
("G", "C", [
|
||||||
|
("B", 10.0, 126.0),
|
||||||
|
]),
|
||||||
|
("F", "C", [
|
||||||
|
("16", -34.0, 62.3),
|
||||||
|
("13", -36.7, 91.0),
|
||||||
|
]),
|
||||||
|
("G", "A", [
|
||||||
|
("B", -56.9, 112.6),
|
||||||
|
("12", 16.8, 97.2),
|
||||||
|
("11", 11.1, 62.8),
|
||||||
|
]),
|
||||||
|
("F", "G", [
|
||||||
|
("10", -2.6, 130.2),
|
||||||
|
]),
|
||||||
|
("F", "E", [
|
||||||
|
("9", -0.2, 159.0),
|
||||||
|
("8", 1.4, 157.8),
|
||||||
|
]),
|
||||||
|
("D", "C", [
|
||||||
|
("7", 4.4, 117.5),
|
||||||
|
("6", -3.4, 110.2),
|
||||||
|
("5", -2.0, 107.1),
|
||||||
|
("4", -0.4, 98.6),
|
||||||
|
("3", 3.8, 69.2),
|
||||||
|
("18", 8.1, 50.8),
|
||||||
|
("17", 7.4, 50.4),
|
||||||
|
("16a", 5.7, 30.7),
|
||||||
|
("15", 7.1, 30.0),
|
||||||
|
("2", 0.0, 24.4),
|
||||||
|
("15", 21.2, 49.0),
|
||||||
|
]),
|
||||||
|
("B", "A", [
|
||||||
|
("1", 2.1, 15.4),])
|
||||||
|
])
|
||||||
|
sketch.add_ladder_points(ladder)
|
||||||
|
sketch.points["8_F_1"] = chainage_to_xy(sketch.points["8"], sketch.points["F"], 2.1)
|
||||||
|
sketch.points["F_10_1"] = chainage_to_xy(sketch.points["F"], sketch.points["10"], 13.0)
|
||||||
|
sketch.add_polyline_to_struts(("D", "2", "3", "4", "5", "6", "7", "C", "B", "1", "A", "12", "11", "G", "10", "F", "8", "9", "E", "D"))
|
||||||
|
sketch.add_polyline_to_struts(("7", "13", "14"))
|
||||||
|
sketch.add_polyline_to_struts(("F_10_1", "20", "21", "15"))
|
||||||
|
sketch.reorient(origin_label="G", direction_labels=("D", "F"), angle=math.radians(-100))
|
||||||
|
return sketch
|
||||||
|
|
||||||
|
|
||||||
|
def survey_1016_3_revised_sketch():
|
||||||
|
#lad = load_ladder_xls("data/2016_3_revised_sketch_ladder.xlsx")
|
||||||
|
#P = get_seed_points(lad)
|
||||||
|
# P["B"] = trilaterate_xy(P["F"], 145.5, P["C"], 12.3)
|
||||||
|
P = dict()
|
||||||
|
P["D"] = np.array([0, 0])
|
||||||
|
P["F"] = np.array(P["D"] + [0, -161.3])
|
||||||
|
P["C"] = trilaterate_xy(P["F"], 138.5, P["D"], 130.5)
|
||||||
|
P["B"] = trilaterate_xy(P["F"], 145.5, P["C"], 12.3)
|
||||||
|
P["G"] = trilaterate_xy(P["F"], 157.6, P["B"], 126.0)
|
||||||
|
ladder = ([
|
||||||
|
("F", "D", [
|
||||||
|
("30", 2.7, 153.5),
|
||||||
|
("29", 12.9, 123.5),
|
||||||
|
("28", 13.2, 114.6),
|
||||||
|
("E", -2.1, 161.2),
|
||||||
|
("14", 11.2, 106.9),
|
||||||
|
("27", 10.4, 104.2),
|
||||||
|
("26", 24.6, 99.9),
|
||||||
|
("25", 28.4, 99.0),
|
||||||
|
("24", 29.4, 98.2),
|
||||||
|
("23", 23.5, 97.2),
|
||||||
|
("22", 28.7, 91.8),
|
||||||
|
("21", 27.5, 81.8),
|
||||||
|
("20", 24.2, 52.6),
|
||||||
|
]),
|
||||||
|
("G", "C", [
|
||||||
|
("B0", 10.0, 126.0),
|
||||||
|
]),
|
||||||
|
("F", "C", [
|
||||||
|
("13", -36.7, 91.0),
|
||||||
|
("16_old", -34.0, 62.3)
|
||||||
|
]),
|
||||||
|
("F", "G", [
|
||||||
|
("10", -2.6, 130.0),
|
||||||
|
]),
|
||||||
|
("F", "E", [
|
||||||
|
("9", -0.2, 159.0),
|
||||||
|
("8", 1.4, 157.8),
|
||||||
|
]),
|
||||||
|
("D", "C", [
|
||||||
|
("7", 4.4, 117.5),
|
||||||
|
("6", -3.4, 110.2),
|
||||||
|
("5", -2.0, 107.1),
|
||||||
|
("4", -0.4, 98.6),
|
||||||
|
("3", 3.8, 69.2),
|
||||||
|
("19", 8.1, 50.8),
|
||||||
|
("18", 7.4, 50.4),
|
||||||
|
("17", 5.7, 30.7),
|
||||||
|
("16", 7.1, 30.0),
|
||||||
|
("2", 0.0, 24.4),
|
||||||
|
("15", 21.2, 49.0),
|
||||||
|
])
|
||||||
|
])
|
||||||
|
sketch = Sketch(points=P)
|
||||||
|
sketch.add_ladder_points(ladder)
|
||||||
|
|
||||||
|
sketch.points["C1"] = chainage_to_xy(sketch.points["7"], sketch.points["C"], 2.8)
|
||||||
|
|
||||||
|
sketch.points["F1"] = chainage_to_xy(sketch.points["F"], sketch.points["10"], 13.3)
|
||||||
|
sketch.points["F2"] = chainage_to_xy(sketch.points["F1"], sketch.points["10"], 29.5)
|
||||||
|
sketch.points["F3"] = chainage_to_xy(sketch.points["F2"], sketch.points["10"], 30.6)
|
||||||
|
sketch.points["F4"] = chainage_to_xy(sketch.points["F3"], sketch.points["10"], 26.4)
|
||||||
|
|
||||||
|
sketch.points["101"] = chainage_to_xy(sketch.points["26"], sketch.points["18"], 12.4718)
|
||||||
|
sketch.points["27a"] = chainage_to_xy(sketch.points["14"], sketch.points["16"], 7.9762)
|
||||||
|
sketch.points["102"] = chainage_to_xy(sketch.points["27a"], sketch.points["16"], 4.5389)
|
||||||
|
sketch.points["3a"] = chainage_to_xy(sketch.points["3"], sketch.points["F2"], 7.20)
|
||||||
|
sketch.points["4a"] = chainage_to_xy(sketch.points["3"], sketch.points["4"], 21.9)
|
||||||
|
sketch.points["8a"] = chainage_to_xy(sketch.points["8"], sketch.points["F"], 4)
|
||||||
|
sketch.points["18a"] = chainage_to_xy(sketch.points["18"], sketch.points["3a"], 4.1)
|
||||||
|
sketch.points["18b"] = chainage_to_xy(sketch.points["18a"], sketch.points["18"], 3.65)
|
||||||
|
sketch.points["F8"] = chainage_to_xy(sketch.points["F"], sketch.points["8"], 110.7)
|
||||||
|
sketch.points["F9"] = chainage_to_xy(sketch.points["F"], sketch.points["8"], 110.7)
|
||||||
|
sketch.points["14F3"] = chainage_to_xy(sketch.points["F9"], sketch.points["8"], 16.3)
|
||||||
|
sketch.points["17line30"] = chainage_to_xy(sketch.points["17"], sketch.points["30"], 4.2)
|
||||||
|
|
||||||
|
sketch.reorient(origin_label="G", direction_labels=("D", "F"), angle=math.radians(-100))
|
||||||
|
|
||||||
|
sketch.struts = (
|
||||||
|
[("D", "E"),
|
||||||
|
("25", "24"), ("24", "22"), ("22", "21"), ("21", "20"), ("20", "F1"),
|
||||||
|
("7", "13"), ("13", "22"),
|
||||||
|
("8", "F"), ("C1", "F4"),
|
||||||
|
("3", "F2"), ("8a", "30"), ("F8", "14"),
|
||||||
|
("101", "102"), ("18", "3a"), ("18a", "25"),
|
||||||
|
("F3", "4a"), ("E", "9"), ("8", "9"), ("30", "17"),
|
||||||
|
("29", "14F3"), ("17line30", "29")]
|
||||||
|
)
|
||||||
|
sketch.add_polyline_to_struts(["C", "B", "G"])
|
||||||
|
sketch.add_polyline_to_struts(["23", "27", "16", "17", "18", "18b", "23"])
|
||||||
|
sketch.add_polyline_to_struts(["D", "2", "3", "4", "5", "6", "7", "C"])
|
||||||
|
sketch.add_polyline_to_struts(["F", "F1", "F2", "F3", "F4", "10", "G"])
|
||||||
|
return sketch
|
||||||
|
|
||||||
|
|
||||||
|
def survey_1016_3_sketch():
|
||||||
|
"""
|
||||||
|
Sketch based on survey 2016-3 from 2006
|
||||||
|
"""
|
||||||
|
P = dict()
|
||||||
|
P["D"] = np.array([0, 0])
|
||||||
|
P["F"] = np.array(P["D"] + [0, -161.3])
|
||||||
|
P["C"] = trilaterate_xy(P["F"], 138.5, P["D"], 130.5)
|
||||||
|
P["B"] = trilaterate_xy(P["F"], 145.5, P["C"], 12.3)
|
||||||
|
P["G"] = trilaterate_xy(P["F"], 157.6, P["B"], 126.0)
|
||||||
|
sketch = Sketch(points=P)
|
||||||
|
|
||||||
|
ladder = ([
|
||||||
|
("F", "D", [
|
||||||
|
("E", -2.1, 161.2),
|
||||||
|
("14", 12.0, 110.7),
|
||||||
|
]),
|
||||||
|
("G", "C", [
|
||||||
|
("B", 10.0, 126.0),
|
||||||
|
]),
|
||||||
|
("F", "C", [
|
||||||
|
("13", -36.7, 91.0),
|
||||||
|
("16", -34.0, 62.3),
|
||||||
|
("16new", -34.1, 62.2)
|
||||||
|
]),
|
||||||
|
("F", "G", [
|
||||||
|
("10", -2.6, 130.2),
|
||||||
|
]),
|
||||||
|
("F", "E", [
|
||||||
|
("9", -0.2, 159.0),
|
||||||
|
("8", 1.4, 157.8),
|
||||||
|
]),
|
||||||
|
("D", "C", [
|
||||||
|
("7", 4.4, 117.5),
|
||||||
|
("6", -3.4, 110.2),
|
||||||
|
("5", -2.0, 107.1),
|
||||||
|
("4", -0.4, 98.6),
|
||||||
|
("3", 3.8, 69.2),
|
||||||
|
("2", 0.0, 24.4),
|
||||||
|
("15", 21.2, 49.0),
|
||||||
|
])
|
||||||
|
])
|
||||||
|
sketch = Sketch(points=P)
|
||||||
|
sketch.add_ladder_points(ladder)
|
||||||
|
|
||||||
|
sketch.points["F-10-1"] = chainage_to_xy(sketch.points["F"], sketch.points["10"], 13.0)
|
||||||
|
sketch.points["F-10-2"] = chainage_to_xy(sketch.points["F-10-1"], sketch.points["10"], 29.8)
|
||||||
|
sketch.points["F-10-3"] = chainage_to_xy(sketch.points["F-10-2"], sketch.points["10"], 30.6)
|
||||||
|
sketch.points["F-10-4"] = chainage_to_xy(sketch.points["F-10-3"], sketch.points["10"], 26.4)
|
||||||
|
sketch.points["F-10-5"] = chainage_to_xy(sketch.points["F-10-4"], sketch.points["10"], 23.0)
|
||||||
|
|
||||||
|
sketch.points["F-8-1"] = chainage_to_xy(sketch.points["F"], sketch.points["8"], 112.5)
|
||||||
|
sketch.points["F-8-2"] = chainage_to_xy(sketch.points["F"], sketch.points["8"], 123.5)
|
||||||
|
|
||||||
|
sketch.points["7-C-1"] = chainage_to_xy(sketch.points["7"], sketch.points["C"], 2.8)
|
||||||
|
|
||||||
|
#sketch.points["3-2-1"] = chainage_to_xy(sketch.points["3"], sketch.points["2"], 21.5)
|
||||||
|
#sketch.points["3-2-2"] = chainage_to_xy(sketch.points["3-2-1"], sketch.points["2"], 18.1)
|
||||||
|
|
||||||
|
sketch.points["2-3-1"] = chainage_to_xy(sketch.points["2"], sketch.points["3"], 3.7)
|
||||||
|
sketch.points["2-3-2"] = chainage_to_xy(sketch.points["2-3-1"], sketch.points["3"], 18.4)
|
||||||
|
sketch.points["2-3-3"] = chainage_to_xy(sketch.points["2-3-2"], sketch.points["3"], 21.5)
|
||||||
|
|
||||||
|
sketch.points["3-4-1"] = chainage_to_xy(sketch.points["3"], sketch.points["4"], 21.9)
|
||||||
|
|
||||||
|
sketch.points["i-1"] = intersect_lines_xy(
|
||||||
|
(sketch.points["7"], sketch.points["13"]),
|
||||||
|
(sketch.points["3-4-1"], sketch.points["F-10-3"]))
|
||||||
|
|
||||||
|
sketch.points["i-2"] = intersect_lines_xy(
|
||||||
|
(sketch.points["7"], sketch.points["13"]),
|
||||||
|
(sketch.points["2-3-3"], sketch.points["F-10-2"]))
|
||||||
|
|
||||||
|
sketch.points["i-3"] = intersect_lines_xy(
|
||||||
|
(sketch.points["13"], sketch.points["F-8-2"]),
|
||||||
|
(sketch.points["15"], sketch.points["16"]))
|
||||||
|
|
||||||
|
sketch.points["i-4"] = intersect_lines_xy(
|
||||||
|
(sketch.points["13"], sketch.points["F-8-2"]),
|
||||||
|
(sketch.points["2-3-1"], sketch.points["14"]))
|
||||||
|
|
||||||
|
|
||||||
|
sketch.add_polyline_to_struts(["D", "E", "9", "8", "F", "10", "G"])
|
||||||
|
sketch.add_polyline_to_struts(["D", "2", "3", "4", "5", "6", "7", "C", "B"])
|
||||||
|
sketch.add_polyline_to_struts(["F-10-1", "16", "15", "2-3-2"])
|
||||||
|
sketch.add_polyline_to_struts(["7", "13", "F-8-2"])
|
||||||
|
sketch.add_polyline_to_struts(["2-3-1", "14", "F-8-1"])
|
||||||
|
|
||||||
|
sketch.add_polyline_to_struts(["F-10-2", "2-3-3"])
|
||||||
|
sketch.add_polyline_to_struts(["F-10-3", "3-4-1"])
|
||||||
|
sketch.add_polyline_to_struts(["F-10-4", "7-C-1"])
|
||||||
|
|
||||||
|
sketch.subdivisions["1"] = ["7", "6", "5", "4", "3-4-1", "i-1"]
|
||||||
|
sketch.subdivisions["2"] = ["F-10-3", "F-10-4", "7-C-1", "7", "i-1"]
|
||||||
|
sketch.subdivisions["3"] = ["8", "9", "E", "D", "2", "2-3-1", "i-4", "F-8-2"]
|
||||||
|
sketch.subdivisions["4"] = ["i-4", "14", "F-8-1", "F-8-2"]
|
||||||
|
sketch.subdivisions["7"] = ["2-3-3", "3", "3-4-1", "i-1", "i-2"]
|
||||||
|
sketch.subdivisions["8"] = ["2-3-2", "2-3-3", "i-2", "13", "i-3", "15"]
|
||||||
|
sketch.subdivisions["9"] = ["i-3", "i-4", "2-3-1", "2-3-2", "15"]
|
||||||
|
sketch.subdivisions["10"] = ["i-2", "i-1", "F-10-3", "F-10-2"]
|
||||||
|
sketch.subdivisions["11"] = ["i-3", "16", "F-10-1", "F-10-2", "i-2", "13"]
|
||||||
|
sketch.subdivisions["12"] = ["F", "F-8-1", "14", "i-4", "i-3", "16", "F-10-1"]
|
||||||
|
sketch.subdivisions["?"] = ["10", "F-10-4", "7-C-1", "C", "B"]
|
||||||
|
|
||||||
|
sketch.reorient(origin_label="G", direction_labels=("D", "F"), angle=math.radians(-93))
|
||||||
|
|
||||||
|
return sketch
|
||||||
|
|
||||||
|
def shibu_ikka_sketch_march7():
|
||||||
|
sketch = survey_1016_3_sketch()
|
||||||
|
sketch.points["BM"] = chainage_to_xy(sketch.points["2-3-1"], sketch.points["14"], 3.7)
|
||||||
|
sketch.points["8-F-1"] = chainage_to_xy(sketch.points["8"], sketch.points["F"], 2.1)
|
||||||
|
sketch.points["8-F-2"] = chainage_to_xy(sketch.points["8-F-1"], sketch.points["F"], 26.8)
|
||||||
|
return sketch
|
||||||
|
|
||||||
|
def survey_2010():
|
||||||
|
A = np.array([0, 0])
|
||||||
|
B = np.array(A + [17.2, 0])
|
||||||
|
C = np.array(B + [51.9, 0])
|
||||||
|
D = np.array(C + [13.4, 0])
|
||||||
|
E = np.array(D + [29.5, 0])
|
||||||
|
H = trilaterate_xy(C, 28.5, D, 29.0)
|
||||||
|
G = trilaterate_xy(C, 27.4, H, 11.3)
|
||||||
|
F = trilaterate_xy(A, 13.6, G, 67.6)
|
||||||
|
I = trilaterate_xy(H, 24.2, D, 35.2)
|
||||||
|
K = trilaterate_xy(I, 1.65 + 3.65, D, 31.6)
|
||||||
|
J = chainage_to_xy(I, K, 1.65)
|
||||||
|
# L = trilaterate_xy(K, 15.35, E, 12.1)
|
||||||
|
print(np.linalg.norm(K-E))
|
||||||
|
print(12.1 + 15.35)
|
||||||
|
M = get_point_by_distance_at_angle(J, 28.3, 21.5)
|
||||||
|
N = get_point_by_distance_at_angle(K, 29.9, 21.5)
|
||||||
|
print(J, M)
|
||||||
|
print(K, N)
|
||||||
|
|
||||||
|
polygon = np.array([A, B, C, D, E, K, N, M, J, I, H, G, F])
|
||||||
|
# polygon = np.array([A, B, C, D, E, L, K, N, M, J, I, H, G, F])
|
||||||
|
struts = [(C, G), (C, H), (D, I), (D, K), (D, H), (J, K)]
|
||||||
|
return Sketch(points=None, subdivisions=[("Area", polygon)], struts=struts)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import numpy
|
||||||
|
|
||||||
|
from geo import poly_area, poly_centroid
|
||||||
|
|
||||||
|
|
||||||
|
def plot(ax, sketch):
|
||||||
|
"""plots a property"""
|
||||||
|
for poly_labels in sketch.subdivisons:
|
||||||
|
polygon = numpy.array([sketch.subdivisons[label] for label in poly_labels])
|
||||||
|
plot_polygon(ax, polygon)
|
||||||
|
area_in_sq_m = abs(poly_area(polygon))
|
||||||
|
area_in_cents = area_in_sq_m / 40.47
|
||||||
|
centroid = poly_centroid(polygon)
|
||||||
|
add_textbox(ax, centroid, f"area = {area_in_cents:.1f} cents")
|
||||||
|
for label, pos in sketch.points.items():
|
||||||
|
add_textbox(ax, pos, label)
|
||||||
|
for strut in sketch.struts:
|
||||||
|
endpoint_positions = [sketch.points[label] for label in strut]
|
||||||
|
plot_line(ax, *endpoint_positions)
|
||||||
|
|
||||||
|
|
||||||
|
def plot_polygon(ax, polygon):
|
||||||
|
"""plots polygon given by list of point coordinates"""
|
||||||
|
if numpy.array(polygon).size == 0:
|
||||||
|
return
|
||||||
|
for A, B in zip(polygon, numpy.roll(polygon, 1, 0)):
|
||||||
|
plot_line(ax, A, B)
|
||||||
|
|
||||||
|
|
||||||
|
def plot_line(ax, A, B):
|
||||||
|
"""plots a line from A to B"""
|
||||||
|
ax.plot([A[0], B[0]], [A[1], B[1]], color='black', linewidth=1)
|
||||||
|
|
||||||
|
|
||||||
|
def add_textbox(ax, pos, text):
|
||||||
|
ax.text(pos[0], pos[1], text,
|
||||||
|
horizontalalignment='left',
|
||||||
|
# bbox=dict(facecolor='white', alpha=0.6),
|
||||||
|
fontsize=12.5)
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import ezdxf
|
||||||
|
|
||||||
|
def remove_empty_layers(input_file: str, output_file: str):
|
||||||
|
# Load the DXF document
|
||||||
|
doc = ezdxf.readfile(input_file)
|
||||||
|
|
||||||
|
# Collect all layers that have entities across ALL layouts
|
||||||
|
used_layers = set()
|
||||||
|
for layout in doc.layouts:
|
||||||
|
for entity in layout:
|
||||||
|
used_layers.add(entity.dxf.layer)
|
||||||
|
|
||||||
|
# Also scan block definitions (important for symbols, inserts, etc.)
|
||||||
|
for block in doc.blocks:
|
||||||
|
for entity in block:
|
||||||
|
used_layers.add(entity.dxf.layer)
|
||||||
|
|
||||||
|
# Identify layers to remove
|
||||||
|
layers_to_remove = []
|
||||||
|
for layer in doc.layers:
|
||||||
|
if layer.dxf.name not in used_layers:
|
||||||
|
layers_to_remove.append(layer.dxf.name)
|
||||||
|
|
||||||
|
# Remove empty layers
|
||||||
|
for layer_name in layers_to_remove:
|
||||||
|
print(f"Removing empty layer: {layer_name}")
|
||||||
|
doc.layers.remove(layer_name)
|
||||||
|
|
||||||
|
# Save the cleaned DXF
|
||||||
|
doc.saveas(output_file)
|
||||||
|
|
||||||
|
# Summary report
|
||||||
|
total_layers = len(doc.layers)
|
||||||
|
removed_count = len(layers_to_remove)
|
||||||
|
retained_count = total_layers
|
||||||
|
|
||||||
|
print("\n--- Summary Report ---")
|
||||||
|
print(f"Total layers after cleanup: {total_layers}")
|
||||||
|
print(f"Layers removed: {removed_count}")
|
||||||
|
print(f"Layers retained: {retained_count}")
|
||||||
|
print(f"Saved cleaned DXF to {output_file}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Example usage
|
||||||
|
input_path = R"C:\Data\Rat killer\Resurvey docs\Shibu Ikka Survey\Kitten\2026-04-30 Overlay Feb19 with Taluk Survey V2.dxf"
|
||||||
|
output_path = R"C:\Data\Rat killer\Resurvey docs\Shibu Ikka Survey\Kitten\2026-04-30 Overlay Feb19 with Taluk Survey V2 cleaned.dxf"
|
||||||
|
remove_empty_layers(input_path, output_path)
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
import numpy
|
||||||
|
|
||||||
|
from geo import chainage_offset_to_xy
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Point:
|
||||||
|
|
||||||
|
label: str
|
||||||
|
pos: numpy.ndarray
|
||||||
|
source: str
|
||||||
|
|
||||||
|
|
||||||
|
class Sketch:
|
||||||
|
|
||||||
|
def __init__(self, points=None, struts=None, subdivisions=None, dimensions=None):
|
||||||
|
self.points = points if points is not None else list()
|
||||||
|
self.subdivisions = subdivisions if subdivisions is not None else dict()
|
||||||
|
self.struts = struts if struts is not None else []
|
||||||
|
self.dimensions = dimensions if dimensions is not None else []
|
||||||
|
|
||||||
|
def reorient(self, origin_label, direction_labels, angle):
|
||||||
|
"""
|
||||||
|
Reorients the sketch so that the origin point is at (0, 0) and the line from the
|
||||||
|
origin point to the direction point has the specified angle with the positive x-axis.
|
||||||
|
"""
|
||||||
|
point_positions = self.get_point_positions()
|
||||||
|
origin = point_positions[origin_label]
|
||||||
|
direction_point_0 = point_positions[direction_labels[0]]
|
||||||
|
direction_point_1 = point_positions[direction_labels[1]]
|
||||||
|
current_angle = numpy.arctan2(direction_point_1[1] - direction_point_0[1], direction_point_1[0] - direction_point_0[0])
|
||||||
|
rotation_angle = angle - current_angle
|
||||||
|
rotation_matrix = numpy.array([[numpy.cos(rotation_angle), -numpy.sin(rotation_angle)],
|
||||||
|
[numpy.sin(rotation_angle), numpy.cos(rotation_angle)]])
|
||||||
|
self.points = ([
|
||||||
|
Point(label=p.label, pos=rotation_matrix @ (p.pos - origin), source=p.source)
|
||||||
|
for p in self.points
|
||||||
|
])
|
||||||
|
|
||||||
|
def add_ladder_points(self, ladder):
|
||||||
|
point_positions = self.get_point_positions()
|
||||||
|
for seg in ladder:
|
||||||
|
for p in seg.offset_points:
|
||||||
|
self.points.append(Point(
|
||||||
|
label=p.label,
|
||||||
|
pos=chainage_offset_to_xy(
|
||||||
|
point_positions[seg.start_label],
|
||||||
|
point_positions[seg.end_label],
|
||||||
|
p.distance_on_line,
|
||||||
|
p.offset
|
||||||
|
),
|
||||||
|
source="tie_table"
|
||||||
|
))
|
||||||
|
|
||||||
|
def add_polyline_to_struts(self, labels):
|
||||||
|
for label1, label2 in zip(labels, labels[1:]):
|
||||||
|
self.struts.append((label1, label2))
|
||||||
|
|
||||||
|
def add_dimension_line(self, label1, label2, distance):
|
||||||
|
self.dimensions.append((label1, label2, distance))
|
||||||
|
|
||||||
|
|
||||||
|
def get_point_positions(self):
|
||||||
|
return {p.label: p.pos for p in self.points}
|
||||||
@@ -0,0 +1,217 @@
|
|||||||
|
import collections
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
from scipy.optimize import minimize
|
||||||
|
|
||||||
|
from geo import poly_area, chainage_offset_to_xy, trilaterate_xy
|
||||||
|
from util import corner_labels_are_ascending
|
||||||
|
|
||||||
|
|
||||||
|
class DirectSolver:
|
||||||
|
|
||||||
|
def __init__(self, point_labels, origin_label=None):
|
||||||
|
if not point_labels:
|
||||||
|
raise ValueError("No point labels given.")
|
||||||
|
self.point_labels = point_labels
|
||||||
|
self.label_to_index = {label: index for index, label in enumerate(point_labels)}
|
||||||
|
self.distances = collections.defaultdict(list)
|
||||||
|
self.offsets = collections.defaultdict(list)
|
||||||
|
self.origin_label = origin_label if origin_label is not None else self.point_labels[0]
|
||||||
|
|
||||||
|
def add_distance(self, label1: str, label2: str, distance: float):
|
||||||
|
self.distances[label1].append((label2, distance))
|
||||||
|
self.distances[label2].append((label1, distance))
|
||||||
|
|
||||||
|
def add_offset(self, label: str, start_label: str, end_label: str, chainage: float, offset: float):
|
||||||
|
self.offsets[label].append((start_label, end_label, chainage, offset))
|
||||||
|
|
||||||
|
|
||||||
|
def solve(self):
|
||||||
|
self.sanity_check()
|
||||||
|
points = {} # label -> np.array([x, y])
|
||||||
|
unprocessed_labels = set(self.point_labels)
|
||||||
|
|
||||||
|
L0 = self.origin_label
|
||||||
|
D0 = self.distances[L0][0]
|
||||||
|
L1 = D0[0]
|
||||||
|
d0 = D0[1]
|
||||||
|
|
||||||
|
points[L0] = np.array([0.0, 0.0])
|
||||||
|
unprocessed_labels.remove(L0)
|
||||||
|
points[L1] = np.array([d0, 0.0])
|
||||||
|
unprocessed_labels.remove(L1)
|
||||||
|
|
||||||
|
def next_trilateration_candidate():
|
||||||
|
for label in unprocessed_labels:
|
||||||
|
distances_to_processed_neighbors = [(neighbor_label, d) for (neighbor_label, d) in self.distances[label] if neighbor_label not in unprocessed_labels]
|
||||||
|
if len(distances_to_processed_neighbors) >= 2:
|
||||||
|
return label, distances_to_processed_neighbors
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
def next_offset_candidate():
|
||||||
|
for label in unprocessed_labels:
|
||||||
|
if label in self.offsets:
|
||||||
|
for start_label, end_label, chainage, offset in self.offsets[label]:
|
||||||
|
if start_label in points and end_label in points:
|
||||||
|
return label, (start_label, end_label, chainage, offset)
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
while unprocessed_labels:
|
||||||
|
label, distances_to_processed_neighbors = next_trilateration_candidate()
|
||||||
|
while label is not None:
|
||||||
|
neighbor_1 = distances_to_processed_neighbors[0][0]
|
||||||
|
distance_1 = distances_to_processed_neighbors[0][1]
|
||||||
|
neighbor_2 = distances_to_processed_neighbors[1][0]
|
||||||
|
distance_2 = distances_to_processed_neighbors[1][1]
|
||||||
|
if corner_labels_are_ascending(label, neighbor_1, neighbor_2):
|
||||||
|
neighbor_1, neighbor_2 = neighbor_2, neighbor_1
|
||||||
|
distance_1, distance_2 = distance_2, distance_1
|
||||||
|
points[label] = trilaterate_xy(points[neighbor_1], distance_1, points[neighbor_2], distance_2)
|
||||||
|
unprocessed_labels.remove(label)
|
||||||
|
label, distances_to_processed_neighbors = next_trilateration_candidate()
|
||||||
|
if not unprocessed_labels:
|
||||||
|
break
|
||||||
|
label, chainage_offset = next_offset_candidate()
|
||||||
|
if label is None:
|
||||||
|
raise ValueError("Could not solve for all points, remaining unprocessed labels: " + str(unprocessed_labels))
|
||||||
|
else:
|
||||||
|
(start_label, end_label, chainage, offset) = chainage_offset
|
||||||
|
points[label] = chainage_offset_to_xy(points[start_label], points[end_label], chainage, offset)
|
||||||
|
unprocessed_labels.remove(label)
|
||||||
|
|
||||||
|
return points
|
||||||
|
|
||||||
|
|
||||||
|
def sanity_check(self):
|
||||||
|
if len(self.point_labels) < 2:
|
||||||
|
raise ValueError("At least two points are required to solve for coordinates")
|
||||||
|
if len(self.distances) < 1:
|
||||||
|
raise ValueError("Not enough distance constraints to determine coordinates")
|
||||||
|
for label, neighbors in self.distances.items():
|
||||||
|
if len(neighbors) < 2 and label not in self.offsets:
|
||||||
|
raise ValueError(f"Label {label} has less than 2 neighbors, cannot determine position")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class OptimizationSolver:
|
||||||
|
|
||||||
|
def __init__(self, point_labels):
|
||||||
|
self.point_labels = point_labels
|
||||||
|
self.label_to_index = {label: index for index, label in enumerate(point_labels)}
|
||||||
|
self.distances = []
|
||||||
|
self.offsets = []
|
||||||
|
|
||||||
|
def add_distance(self, label1, label2, distance):
|
||||||
|
self.distances.append((label1, label2, distance))
|
||||||
|
|
||||||
|
def add_offset(self, label, start_label, end_label, chainage, offset):
|
||||||
|
self.offsets.append((label, start_label, end_label, chainage, offset))
|
||||||
|
|
||||||
|
def solve(self):
|
||||||
|
self.sanity_check()
|
||||||
|
|
||||||
|
n_points = len(self.point_labels)
|
||||||
|
points0 = np.random.rand(n_points * 2).reshape((n_points, 2))
|
||||||
|
|
||||||
|
constraints = build_orientation_constraints(self.point_labels)
|
||||||
|
|
||||||
|
initial_points = points0.flatten()
|
||||||
|
# result = minimize(
|
||||||
|
# self.cost_function, initial_points, method='COBYLA', constraints=constraints, options={'tol': 1.0E-3, 'maxiter': 10000})
|
||||||
|
result = minimize(
|
||||||
|
self.cost_function, initial_points, method='BFGS', options={'gtol': 1.0E-3, 'maxiter': 10000})
|
||||||
|
if not result.success:
|
||||||
|
raise ValueError("Could not solve for seed points, %s", result.message)
|
||||||
|
points = result.x.reshape((n_points, 2))
|
||||||
|
return {label: points[i] for label, i in self.label_to_index.items()}
|
||||||
|
|
||||||
|
|
||||||
|
def cost_function(self, flat_points):
|
||||||
|
return self.distances_cost(flat_points) + self.offsets_cost(flat_points) + self.orientation_cost(flat_points)
|
||||||
|
|
||||||
|
|
||||||
|
def distances_cost(self, pts):
|
||||||
|
pts = pts.reshape((-1, 2))
|
||||||
|
total_cost = 0.0
|
||||||
|
for label1, label2, expected_distance in self.distances:
|
||||||
|
index1, index2 = self.label_to_index[label1], self.label_to_index[label2]
|
||||||
|
calculated_distance = np.linalg.norm(pts[index2] - pts[index1])
|
||||||
|
total_cost += (calculated_distance - expected_distance) ** 2
|
||||||
|
return total_cost
|
||||||
|
|
||||||
|
|
||||||
|
def offsets_cost(self, pts):
|
||||||
|
pts = pts.reshape((-1, 2))
|
||||||
|
total_cost = 0.0
|
||||||
|
for label, start_label, end_label, chainage, offset in self.offsets:
|
||||||
|
start_point = pts[self.label_to_index[start_label]]
|
||||||
|
end_point = pts[self.label_to_index[end_label]]
|
||||||
|
actual_position = pts[self.label_to_index[label]]
|
||||||
|
if np.linalg.norm(end_point - start_point) == 0:
|
||||||
|
total_cost += 10000 # (2 * offset) ** 2
|
||||||
|
else:
|
||||||
|
expected_position = chainage_offset_to_xy(start_point, end_point, chainage, offset)
|
||||||
|
total_cost += np.linalg.norm(actual_position - expected_position) ** 2
|
||||||
|
return total_cost
|
||||||
|
|
||||||
|
|
||||||
|
def orientation_cost(self, pts):
|
||||||
|
pts = pts.reshape((-1, 2))
|
||||||
|
total_cost = 0.0
|
||||||
|
total_cost += np.sum(pts[0]**2) # penalize deviation from origin
|
||||||
|
total_cost += pts[1,1]**2 # penalize deviation from x-axis
|
||||||
|
return total_cost
|
||||||
|
|
||||||
|
def sanity_check(self):
|
||||||
|
if len(self.point_labels) < 2:
|
||||||
|
raise ValueError("At least two points are required to solve for coordinates")
|
||||||
|
if len(self.distances) < 1:
|
||||||
|
raise ValueError("Not enough distance constraints to determine coordinates")
|
||||||
|
# for label, neighbors in extract_neighbors_from_distances(self.point_labels, self.distances).items():
|
||||||
|
# if len(neighbors) < 2 and label not in self.offsets:
|
||||||
|
# raise ValueError(f"Label {label} has less than 2 neighbors, cannot determine position")
|
||||||
|
|
||||||
|
|
||||||
|
def orientation_constraint_factory(i, j, k, sign):
|
||||||
|
"""
|
||||||
|
Returns a constraint function enforcing orient(A,B,C) >= 0
|
||||||
|
for points with indices i, j, k in the variable vector.
|
||||||
|
"""
|
||||||
|
def constraint(vars):
|
||||||
|
points = vars.reshape((-1, 2))
|
||||||
|
return sign * poly_area(points[[i,j,k]]) # must be >= 0
|
||||||
|
return constraint
|
||||||
|
|
||||||
|
|
||||||
|
def build_orientation_constraints(point_labels):
|
||||||
|
"""
|
||||||
|
Build orientation constraints for all alphabetically ordered triples
|
||||||
|
(A,B,C), (B,C,D), ... up to n_points.
|
||||||
|
"""
|
||||||
|
n_points = len(point_labels)
|
||||||
|
cons = []
|
||||||
|
for i in range(n_points-2):
|
||||||
|
j, k = i+1, i+2
|
||||||
|
a = point_labels[i]
|
||||||
|
b = point_labels[j]
|
||||||
|
c = point_labels[k]
|
||||||
|
if (a < b < c) or (b < c < a) or (c < a < b):
|
||||||
|
sign = -1 # clockwise order
|
||||||
|
else:
|
||||||
|
sign = 1 # counter-clockwise order
|
||||||
|
cons.append({
|
||||||
|
'type': 'ineq',
|
||||||
|
'fun': orientation_constraint_factory(i, j, k, sign)
|
||||||
|
})
|
||||||
|
return cons
|
||||||
|
|
||||||
|
|
||||||
|
def extract_neighbors_from_distances(point_labels, distances):
|
||||||
|
neighbors = {label: set() for label in point_labels}
|
||||||
|
for label1, label2, _ in distances:
|
||||||
|
neighbors[label1].add(label2)
|
||||||
|
neighbors[label2].add(label1)
|
||||||
|
return neighbors
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
def is_corner_label(label):
|
||||||
|
return is_single_uppercase_letter(label)
|
||||||
|
|
||||||
|
|
||||||
|
def is_single_uppercase_letter(s: str) -> bool:
|
||||||
|
"""
|
||||||
|
Check if the string is exactly one uppercase letter (A-Z).
|
||||||
|
"""
|
||||||
|
if not isinstance(s, str):
|
||||||
|
return False
|
||||||
|
return len(s) == 1 and s.isupper() and s.isalpha()
|
||||||
|
|
||||||
|
|
||||||
|
def corner_labels_are_ascending(label1, label2, label3):
|
||||||
|
if not (is_corner_label(label1) and is_corner_label(label2) and is_corner_label(label3)):
|
||||||
|
raise ValueError("Labels must be single uppercase letters")
|
||||||
|
if label1 == label2 or label2 == label3 or label1 == label3:
|
||||||
|
raise ValueError("Labels must be distinct")
|
||||||
|
return (label1 < label2 < label3) or (label2 < label3 < label1) or (label3 < label1 < label2)
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
,,A,,
|
||||||
|
,,120,,
|
||||||
|
1,3,100,,
|
||||||
|
2,4,90,,
|
||||||
|
,,80,5,3
|
||||||
|
,,B,,
|
||||||
|
,,C,,
|
||||||
|
,,30,,
|
||||||
|
4,6,20,,
|
||||||
|
,,D,,
|
||||||
|
Binary file not shown.
@@ -0,0 +1,78 @@
|
|||||||
|
import unittest
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from surveyladder.geo import chainage_to_xy, chainage_offset_to_xy, poly_area, poly_centroid, trilaterate_xy
|
||||||
|
|
||||||
|
|
||||||
|
class TestPolyArea(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_poly_area(self):
|
||||||
|
polygon = np.array([[0, 0], [4, 0], [4, 3]])
|
||||||
|
self.assertEqual(poly_area(polygon), 6)
|
||||||
|
|
||||||
|
|
||||||
|
class TestTrilaterateXY(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_trilaterate_xy_on_line(self):
|
||||||
|
A = np.array([0, 0])
|
||||||
|
B = np.array([10, 0])
|
||||||
|
a = 5
|
||||||
|
b = 5
|
||||||
|
C = trilaterate_xy(A, a, B, b)
|
||||||
|
np.testing.assert_almost_equal(C, [5, 0])
|
||||||
|
|
||||||
|
def test_trilaterate_xy_right_triangle(self):
|
||||||
|
A = np.array([0, 0])
|
||||||
|
B = np.array([4, 0])
|
||||||
|
a = 5
|
||||||
|
b = 3
|
||||||
|
C = trilaterate_xy(A, a, B, b)
|
||||||
|
np.testing.assert_almost_equal(C, [4, 3])
|
||||||
|
|
||||||
|
def test_trilaterate_xy_right_triangle_swapped(self):
|
||||||
|
A = np.array([4, 0])
|
||||||
|
B = np.array([0, 0])
|
||||||
|
a = 3
|
||||||
|
b = 5
|
||||||
|
C = trilaterate_xy(A, a, B, b)
|
||||||
|
np.testing.assert_almost_equal(C, [4, -3])
|
||||||
|
|
||||||
|
|
||||||
|
class TestChainageToXY(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_positive_distance(self):
|
||||||
|
np.testing.assert_almost_equal(
|
||||||
|
chainage_to_xy(np.array([3, 5]), np.array([6, 9]), 10),
|
||||||
|
[9, 13])
|
||||||
|
|
||||||
|
def test_negative_distance(self):
|
||||||
|
np.testing.assert_almost_equal(
|
||||||
|
chainage_to_xy(np.array([3, 5]), np.array([6, 9]), -5),
|
||||||
|
[0, 1])
|
||||||
|
|
||||||
|
|
||||||
|
class TestChainageOffsetToXY(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_positive_offset(self):
|
||||||
|
A = np.array([0, 0])
|
||||||
|
B = np.array([0, 1])
|
||||||
|
a_distance = 5
|
||||||
|
perp_distance = 2
|
||||||
|
P = chainage_offset_to_xy(A, B, a_distance, perp_distance)
|
||||||
|
np.testing.assert_almost_equal(P, [-2, 5])
|
||||||
|
|
||||||
|
class TestPolyCentroid(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_pos_square(self):
|
||||||
|
# mathematically positive point ordering
|
||||||
|
poly = np.array([[0,0], [1,0], [1,2], [0,2]])
|
||||||
|
np.testing.assert_almost_equal(poly_centroid(poly), [0.5, 1])
|
||||||
|
|
||||||
|
def test_neg_square(self):
|
||||||
|
# mathematically negative point ordering
|
||||||
|
poly = np.array([[0,0], [1,0], [1,-2], [0,-2]])
|
||||||
|
np.testing.assert_almost_equal(poly_centroid(poly), [0.5, -1])
|
||||||
|
|
||||||
|
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import unittest
|
||||||
|
|
||||||
|
from surveyladder.ladder import load_ladder_csv, load_ladder_xls, get_seed_points, LadderSegment, OffsetPoint
|
||||||
|
|
||||||
|
class TestGetSeedPoints(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_get_seed_points(self):
|
||||||
|
ladder_segments = ([
|
||||||
|
LadderSegment("D", "F", 161.3, []),
|
||||||
|
LadderSegment("C", "G", 133.2, []),
|
||||||
|
LadderSegment("C", "F", 138.5, []),
|
||||||
|
LadderSegment("G", "F", 157.6, []),
|
||||||
|
LadderSegment("C", "D", 130.5, [])
|
||||||
|
])
|
||||||
|
points = get_seed_points(ladder_segments)
|
||||||
|
self.assertEqual(len(points), 4)
|
||||||
|
for seg in ladder_segments:
|
||||||
|
start_point = points[seg.start_label]
|
||||||
|
end_point = points[seg.end_label]
|
||||||
|
actual_distance = ((start_point[0] - end_point[0]) ** 2 + (start_point[1] - end_point[1]) ** 2) ** 0.5
|
||||||
|
self.assertAlmostEqual(actual_distance, seg.distance, places=1)
|
||||||
|
|
||||||
|
|
||||||
|
class TestLadder(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_load_ladder_xls(self):
|
||||||
|
self.maxDiff = None
|
||||||
|
segments = load_ladder_xls("tests/data/ladder.xlsx")
|
||||||
|
self.assertEqual(segments, ([
|
||||||
|
LadderSegment('A', 'B', 120.0, [OffsetPoint("1", 100.0, 3.0), OffsetPoint("2", 90.0, 4.0), OffsetPoint("3", 80.0, -5.0)]),
|
||||||
|
LadderSegment('C', 'D', 30.0, [OffsetPoint("4", 20.0, 6.0)])
|
||||||
|
]))
|
||||||
|
|
||||||
|
def test_load_ladder_csv(self):
|
||||||
|
self.maxDiff = None
|
||||||
|
segments = load_ladder_csv("tests/data/ladder.csv")
|
||||||
|
self.assertEqual(segments, ([
|
||||||
|
LadderSegment('A', 'B', 120.0, [OffsetPoint("1", 100.0, 3.0), OffsetPoint("2", 90.0, 4.0), OffsetPoint("3", 80.0, -5.0)]),
|
||||||
|
LadderSegment('C', 'D', 30.0, [OffsetPoint("4", 20.0, 6.0)])
|
||||||
|
]))
|
||||||
|
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
import unittest
|
||||||
|
|
||||||
|
import numpy
|
||||||
|
|
||||||
|
from solver import OptimizationSolver, DirectSolver
|
||||||
|
from surveyladder.geo import distance, chainage_offset_to_xy, poly_area
|
||||||
|
|
||||||
|
|
||||||
|
class TestDirectSolver(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_sanity_check_no_distances(self):
|
||||||
|
solver = DirectSolver(['A', 'B'])
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
solver.sanity_check()
|
||||||
|
|
||||||
|
def test_sanity_check_not_enough_neighbors(self):
|
||||||
|
solver = DirectSolver(['A', 'B', 'C'])
|
||||||
|
solver.add_distance('A', 'B', 3)
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
solver.sanity_check()
|
||||||
|
|
||||||
|
def test_simple_triangle_only_distances(self):
|
||||||
|
solver = DirectSolver(['A', 'B', 'C'])
|
||||||
|
solver.add_distance('A', 'B', 3)
|
||||||
|
solver.add_distance('B', 'C', 4)
|
||||||
|
solver.add_distance('A', 'C', 5)
|
||||||
|
|
||||||
|
points = solver.solve()
|
||||||
|
|
||||||
|
numpy.testing.assert_almost_equal(distance(points['A'], points['B']), 3, decimal=2)
|
||||||
|
numpy.testing.assert_almost_equal(distance(points['B'], points['C']), 4, decimal=2)
|
||||||
|
numpy.testing.assert_almost_equal(distance(points['A'], points['C']), 5, decimal=2)
|
||||||
|
|
||||||
|
|
||||||
|
class TestSolver(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_distances_cost_optimal_solution(self):
|
||||||
|
solver = OptimizationSolver(['A', 'B', 'C'])
|
||||||
|
solver.add_distance('A', 'B', 3)
|
||||||
|
solver.add_distance('B', 'C', 4)
|
||||||
|
solver.add_distance('A', 'C', 5)
|
||||||
|
optimal_solution = numpy.array([[0, 0], [3, 0], [3, 4]]).flatten()
|
||||||
|
cost = solver.distances_cost(optimal_solution)
|
||||||
|
self.assertAlmostEqual(cost, 0.0, places=3)
|
||||||
|
|
||||||
|
def test_distances_cost_suboptimal_solution(self):
|
||||||
|
solver = OptimizationSolver(['A', 'B', 'C'])
|
||||||
|
solver.add_distance('A', 'B', 3)
|
||||||
|
solver.add_distance('B', 'C', 4)
|
||||||
|
solver.add_distance('A', 'C', 5)
|
||||||
|
suboptimal_solution = numpy.array([[0, 0], [6, 0], [6, 8]]).flatten()
|
||||||
|
# distance(A, B) = 6 instead of 3 -> (6-3)^2 = 9
|
||||||
|
# distance(B, C) = 8 instead of 4 -> (8-4)^2 = 16
|
||||||
|
# distance(A, C) = 10 instead of 5 -> (10-5)^2 = 25
|
||||||
|
# total cost = 9 + 16 + 25 = 50
|
||||||
|
cost = solver.distances_cost(suboptimal_solution)
|
||||||
|
self.assertAlmostEqual(cost, 50.0, places=3)
|
||||||
|
|
||||||
|
def test_offsets_cost_optimal_solution(self):
|
||||||
|
solver = OptimizationSolver(['A', 'B', 'C'])
|
||||||
|
solver.add_distance('A', 'B', 10)
|
||||||
|
solver.add_offset('C', 'A', 'B', 5, 2)
|
||||||
|
optimal_solution = numpy.array([[0, 0], [10, 0], [5, 2]]).flatten()
|
||||||
|
cost = solver.offsets_cost(optimal_solution)
|
||||||
|
self.assertAlmostEqual(cost, 0.0, places=3)
|
||||||
|
|
||||||
|
def test_offsets_cost_suboptimal_solution(self):
|
||||||
|
solver = OptimizationSolver(['A', 'B', 'C'])
|
||||||
|
solver.add_distance('A', 'B', 10)
|
||||||
|
solver.add_offset('C', 'A', 'B', 5, 2)
|
||||||
|
suboptimal_solution = numpy.array([[0, 0], [10, 0], [5, 4]]).flatten()
|
||||||
|
cost = solver.offsets_cost(suboptimal_solution)
|
||||||
|
# The expected position of C is (5, 2), but the actual position is (5, 4),
|
||||||
|
# so the cost should be (distance((5, 4), (5, 2)))^2 = 2^2 = 4
|
||||||
|
self.assertAlmostEqual(cost, 4.0, places=3)
|
||||||
|
|
||||||
|
|
||||||
|
def test_simple_triangle_only_distances_ccw(self):
|
||||||
|
solver = OptimizationSolver(['A', 'B', 'C'])
|
||||||
|
solver.add_distance('A', 'B', 3)
|
||||||
|
solver.add_distance('B', 'C', 4)
|
||||||
|
solver.add_distance('A', 'C', 5)
|
||||||
|
|
||||||
|
points = solver.solve()
|
||||||
|
|
||||||
|
numpy.testing.assert_almost_equal(distance(points['A'], points['B']), 3, decimal=2)
|
||||||
|
numpy.testing.assert_almost_equal(distance(points['B'], points['C']), 4, decimal=2)
|
||||||
|
numpy.testing.assert_almost_equal(distance(points['A'], points['C']), 5, decimal=2)
|
||||||
|
|
||||||
|
triangle = numpy.array([points['A'], points['B'], points['C']])
|
||||||
|
self.assertAlmostEqual(poly_area(triangle), 6.0, places=3)
|
||||||
|
|
||||||
|
|
||||||
|
def test_simple_triangle_only_distances_cw(self):
|
||||||
|
solver = OptimizationSolver(['A', 'C', 'B'])
|
||||||
|
solver.add_distance('A', 'C', 3)
|
||||||
|
solver.add_distance('B', 'C', 4)
|
||||||
|
solver.add_distance('A', 'B', 5)
|
||||||
|
|
||||||
|
points = solver.solve()
|
||||||
|
|
||||||
|
numpy.testing.assert_almost_equal(distance(points['A'], points['C']), 3, decimal=2)
|
||||||
|
numpy.testing.assert_almost_equal(distance(points['B'], points['C']), 4, decimal=2)
|
||||||
|
numpy.testing.assert_almost_equal(distance(points['A'], points['B']), 5, decimal=2)
|
||||||
|
|
||||||
|
triangle = numpy.array([points['A'], points['C'], points['B']])
|
||||||
|
self.assertAlmostEqual(poly_area(triangle), -6.0, places=3)
|
||||||
|
|
||||||
|
|
||||||
|
def test_simple_triangle_with_offset(self):
|
||||||
|
solver = OptimizationSolver(['A', 'B', 'C'])
|
||||||
|
solver.add_distance('A', 'B', 10)
|
||||||
|
solver.add_offset('C', 'A', 'B', 5, 2)
|
||||||
|
|
||||||
|
points = solver.solve()
|
||||||
|
|
||||||
|
numpy.testing.assert_almost_equal(distance(points['A'], points['B']), 10, decimal=2)
|
||||||
|
numpy.testing.assert_almost_equal(points['C'], chainage_offset_to_xy(points['A'], points['B'], 5, 2), decimal=2)
|
||||||
|
|
||||||
|
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user