initial commit

This commit is contained in:
2026-08-07 19:59:27 +05:30
commit 7bd45971fd
23 changed files with 1924 additions and 0 deletions
+9
View File
@@ -0,0 +1,9 @@
*.py[codz]
__pycache__/
.mypy_cache/
.pytest_cache/
.ruff_cache/
.pdm-python
\#*.dxf
*.dxf~
.env
+12
View File
@@ -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
}
+22
View File
@@ -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}"
}
}]
}
}
+17
View File
@@ -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.
Generated
+369
View File
@@ -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"},
]
+15
View File
@@ -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
+10
View File
@@ -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")
+170
View File
@@ -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
+127
View File
@@ -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)
+165
View File
@@ -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
+369
View File
@@ -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()
+39
View File
@@ -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)
+48
View File
@@ -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)
+65
View File
@@ -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}
+217
View File
@@ -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
+19
View File
@@ -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)
+10
View File
@@ -0,0 +1,10 @@
,,A,,
,,120,,
1,3,100,,
2,4,90,,
,,80,5,3
,,B,,
,,C,,
,,30,,
4,6,20,,
,,D,,
1 A
2 120
3 1 3 100
4 2 4 90
5 80 5 3
6 B
7 C
8 30
9 4 6 20
10 D
Binary file not shown.
+78
View File
@@ -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()
+42
View File
@@ -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()
+121
View File
@@ -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()