Skip to content

Commit d81ceab

Browse files
committed
[targets] added new converter commands
1 parent de515da commit d81ceab

File tree

4 files changed

+358
-18
lines changed

4 files changed

+358
-18
lines changed

src/instrumentman/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,11 @@ def cli_import() -> None:
116116
"""Import external data and convert it for use with other commands."""
117117

118118

119+
@cli.group("convert") # type: ignore[misc]
120+
def cli_convert() -> None:
121+
"""Convert between various file formats."""
122+
123+
119124
@cli.group("calculate", aliases=["calc"]) # type: ignore[misc]
120125
def cli_calc() -> None:
121126
"""Preform calculations from measurement results."""
@@ -174,3 +179,5 @@ def cli_upload() -> None:
174179
cli_upload.add_command(datatransfer.cli_upload)
175180
cli_upload.add_command(settings.cli_upload)
176181
cli_upload.add_command(station.cli_upload)
182+
cli_convert.add_command(setup.cli_convert_csv_to_targets)
183+
cli_convert.add_command(setup.cli_convert_targets_to_csv)

src/instrumentman/setup/__init__.py

Lines changed: 126 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
argument,
66
option,
77
IntRange,
8-
Choice
8+
Choice,
9+
File
910
)
1011

1112
from ..utils import (
@@ -47,6 +48,130 @@ def cli_measure(**kwargs: Any) -> None:
4748
main_measure(**kwargs)
4849

4950

51+
@extra_command(
52+
"csv-targets",
53+
params=None,
54+
context_settings={"auto_envvar_prefix": None}
55+
) # type: ignore[misc]
56+
@argument(
57+
"input",
58+
help="Source file to convert",
59+
type=File("r", encoding="utf8")
60+
)
61+
@argument(
62+
"output",
63+
help="Target file to save result to",
64+
type=File("wt", encoding="utf8", lazy=True)
65+
)
66+
@option(
67+
"-c",
68+
"--column",
69+
"columns",
70+
help="Data column (pt, e, n and z are mandatory to specify)",
71+
type=Choice(
72+
["ignore", "pt", "e", "n", "z", "prism", "ht"]
73+
),
74+
multiple=True,
75+
default=()
76+
)
77+
@option(
78+
"--skip",
79+
help="Number of header rows to skip",
80+
type=IntRange(0),
81+
default=0
82+
)
83+
@option(
84+
"-d",
85+
"--delimiter",
86+
help="Column delimiter character",
87+
type=str,
88+
default=","
89+
)
90+
@option(
91+
"--reflector",
92+
help="Reflector at the targets (set only if CSV has no prism column)",
93+
type=Choice(
94+
(
95+
'ROUND',
96+
'MINI',
97+
'TAPE',
98+
'THREESIXTY',
99+
'USER1',
100+
'USER2',
101+
'USER3',
102+
'MINI360',
103+
'MINIZERO',
104+
'NDSTAPE',
105+
'GRZ121',
106+
'MPR122'
107+
)
108+
)
109+
)
110+
@option(
111+
"--height",
112+
help="Target height",
113+
type=float
114+
)
115+
def cli_convert_csv_to_targets(**kwargs: Any) -> None:
116+
"""Convert a CSV file containing coordinates to a target definition."""
117+
from .convert import main_csv_to_targets
118+
119+
main_csv_to_targets(**kwargs)
120+
121+
122+
@extra_command(
123+
"targets-csv",
124+
params=None,
125+
context_settings={"auto_envvar_prefix": None}
126+
) # type: ignore[misc]
127+
@argument(
128+
"input",
129+
help="Source file to convert",
130+
type=File("r", encoding="utf8")
131+
)
132+
@argument(
133+
"output",
134+
help="Target file to save result to",
135+
type=File("wt", encoding="utf8", lazy=True)
136+
)
137+
@option(
138+
"-c",
139+
"--column",
140+
"columns",
141+
help="Data column to output",
142+
type=Choice(
143+
["pt", "e", "n", "z", "prism", "ht"]
144+
),
145+
multiple=True,
146+
default=(),
147+
required=True
148+
)
149+
@option(
150+
"--header/--no-header",
151+
help="Write header row",
152+
type=bool,
153+
default=True
154+
)
155+
@option(
156+
"-d",
157+
"--delimiter",
158+
help="Column delimiter character",
159+
type=str,
160+
default=","
161+
)
162+
@option(
163+
"-p",
164+
"--precision",
165+
help="Number of decimals to output",
166+
type=IntRange(0)
167+
)
168+
def cli_convert_targets_to_csv(**kwargs: Any) -> None:
169+
""""""
170+
from .convert import main_targets_to_csv
171+
172+
main_targets_to_csv(**kwargs)
173+
174+
50175
@extra_command(
51176
"targets",
52177
params=None,

src/instrumentman/setup/convert.py

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
from io import TextIOWrapper
2+
import csv
3+
from typing import cast, Callable
4+
5+
from click_extra import prompt, Choice
6+
from jsonschema import ValidationError
7+
from geocompy.data import Coordinate
8+
from geocompy.geo.gcdata import Prism
9+
10+
from ..utils import echo_red
11+
from ..targets import (
12+
TargetList,
13+
TargetPoint,
14+
load_targets_from_json,
15+
export_targets_to_json
16+
)
17+
18+
19+
def main_csv_to_targets(
20+
input: TextIOWrapper,
21+
output: TextIOWrapper,
22+
columns: tuple[str],
23+
skip: int = 0,
24+
delimiter: str = ",",
25+
reflector: str | None = None,
26+
height: float | None = None
27+
) -> None:
28+
for i in range(skip):
29+
next(input)
30+
31+
def get_column_index(
32+
columns: tuple[str],
33+
name: str,
34+
mandatory: bool = False
35+
) -> int | None:
36+
try:
37+
return columns.index(name)
38+
except ValueError:
39+
if mandatory:
40+
echo_red(f"Mandatory '{name}' was not specified")
41+
exit(1)
42+
43+
return None
44+
45+
def get_prism(
46+
pt: str,
47+
row: list[str],
48+
idx_prism: int | None,
49+
reflector: str | None
50+
) -> Prism:
51+
if idx_prism is not None:
52+
return Prism[row[idx_prism]]
53+
54+
if reflector is not None:
55+
return Prism[reflector]
56+
57+
return Prism[
58+
prompt(
59+
f"Reflector type of {pt}",
60+
type=Choice(
61+
(
62+
'ROUND',
63+
'MINI',
64+
'TAPE',
65+
'THREESIXTY',
66+
'USER1',
67+
'USER2',
68+
'USER3',
69+
'MINI360',
70+
'MINIZERO',
71+
'NDSTAPE',
72+
'GRZ121',
73+
'MPR122'
74+
)
75+
)
76+
)
77+
]
78+
79+
def get_height(
80+
pt: str,
81+
row: list[str],
82+
idx_height: int | None,
83+
height: float | None
84+
) -> float:
85+
if idx_height is not None:
86+
return float(row[idx_height])
87+
88+
if height is not None:
89+
return height
90+
91+
return cast(
92+
float,
93+
prompt(
94+
f"Target height of {pt}",
95+
type=float
96+
)
97+
)
98+
99+
targets = TargetList()
100+
idx_pt = cast(int, get_column_index(columns, "pt"))
101+
idx_e = cast(int, get_column_index(columns, "e", True))
102+
idx_n = cast(int, get_column_index(columns, "n", True))
103+
idx_z = cast(int, get_column_index(columns, "z", True))
104+
idx_prism = get_column_index(columns, "prism")
105+
idx_height = get_column_index(columns, "ht")
106+
for row in csv.reader(input, delimiter=delimiter, lineterminator="\n"):
107+
name = row[idx_pt]
108+
east = float(row[idx_e])
109+
north = float(row[idx_n])
110+
up = float(row[idx_z])
111+
prism = get_prism(name, row, idx_prism, reflector)
112+
ht = get_height(name, row, idx_height, height)
113+
try:
114+
targets.add_target(
115+
TargetPoint(
116+
name,
117+
prism,
118+
ht,
119+
Coordinate(east, north, up)
120+
)
121+
)
122+
except ValueError:
123+
echo_red(f"Duplicate point '{name}' in source files")
124+
exit(1)
125+
126+
export_targets_to_json(
127+
output,
128+
targets
129+
)
130+
131+
132+
def main_targets_to_csv(
133+
input: TextIOWrapper,
134+
output: TextIOWrapper,
135+
columns: tuple[str],
136+
header: bool = True,
137+
delimiter: str = ",",
138+
precision: int | None = None
139+
) -> None:
140+
def make_formatter(
141+
precision: int | None
142+
) -> Callable[[float], str | float]:
143+
if precision is None:
144+
return lambda x: x
145+
146+
fmt = f"{{:.{precision}f}}"
147+
return lambda x: fmt.format(x)
148+
149+
try:
150+
targets = load_targets_from_json(input)
151+
except ValidationError:
152+
echo_red("Target definition file is not valid")
153+
exit(1)
154+
155+
writer = csv.writer(output, delimiter=delimiter, lineterminator="\n")
156+
if header:
157+
writer.writerow(columns)
158+
159+
formatter = make_formatter(precision)
160+
for t in targets:
161+
fields = {
162+
"pt": t.name,
163+
"e": formatter(t.coords.e),
164+
"n": formatter(t.coords.n),
165+
"z": formatter(t.coords.z),
166+
"ht": formatter(t.height),
167+
"prism": t.prism.name
168+
}
169+
writer.writerow((fields[c] for c in columns))

0 commit comments

Comments
 (0)