refactor(db)!: adopt project-routed pooled databases

Reorganize WNDB by responsibility and remove legacy scheme endpoints.\n\nRoute analysis and time-series access through project pools, preserve transactional realtime replacement, and refresh GIS materialized views after writes.\n\nAdd database architecture documentation, live pooling coverage, API contract updates, and executable container verification.\n\nBREAKING CHANGE: legacy scheme APIs and flat app.native.wndb module imports are removed.
This commit is contained in:
2026-08-25 18:35:05 +08:00
parent fdbcc5c033
commit fa188af0b1
181 changed files with 8446 additions and 33546 deletions
+1
View File
@@ -0,0 +1 @@
"""GIS persistence and network geometry operations."""
+44
View File
@@ -0,0 +1,44 @@
from typing import Any
from ..core.database import (
ChangeSet,
DatabaseCommand,
execute_command,
g_update_prefix,
read,
sql_literal,
)
def get_backdrop_schema(name: str) -> dict[str, dict[str, Any]]:
return { 'content' : {'type': 'str' , 'optional': False , 'readonly': False} }
def get_backdrop(name: str) -> dict[str, Any]:
e = read(name, "select content from gis.backdrops where id = true")
return { 'content': e['content'] }
def _set_backdrop(name: str, cs: ChangeSet) -> DatabaseCommand:
statement = f"update gis.backdrops set content = {sql_literal(cs.operations[0]['content'])} where id = true;"
change = g_update_prefix | { 'type': 'backdrop', 'content': cs.operations[0]['content'] }
return DatabaseCommand(statement, [change])
def set_backdrop(name: str, cs: ChangeSet) -> ChangeSet:
return execute_command(name, _set_backdrop(name, cs))
def inp_in_backdrop(section: list[str]) -> str:
if section == []:
return str('')
content = '\n'.join(section)
return str(f"update gis.backdrops set content = {sql_literal(content)} where id = true;")
def inp_out_backdrop(name: str) -> list[str]:
obj = str(get_backdrop(name)['content'])
return obj.split('\n')
+104
View File
@@ -0,0 +1,104 @@
from psycopg.rows import dict_row
from ..core.connection import project_connection
from ..core.database import read_all, sql_literal, try_read, write
from ..core.connection import project_connection
from ..model.elements import get_link_nodes
from psycopg.rows import dict_row
def sql_update_coord(node: str, x: float, y: float) -> str:
geom = f"st_setsrid(st_makepoint({sql_literal(x)}, {sql_literal(y)}), 900914)"
return f"update gis.node_geometries set geom = {geom} where node_id = {sql_literal(node)};"
def sql_insert_coord(node: str, x: float, y: float) -> str:
geom = f"st_setsrid(st_makepoint({sql_literal(x)}, {sql_literal(y)}), 900914)"
return f"insert into gis.node_geometries (node_id, geom) values ({sql_literal(node)}, {geom});"
def sql_delete_coord(node: str) -> str:
return f"delete from gis.node_geometries where node_id = {sql_literal(node)};"
def from_postgis_point(coord: str) -> dict[str, float]:
xy = coord.lower().removeprefix('point(').removesuffix(')').split(' ')
return { 'x': float(xy[0]), 'y': float(xy[1]) }
def get_node_coord(name: str, node: str) -> dict[str, float]:
row = try_read(
name,
"select st_astext(geom) as coord_geom from gis.node_geometries where node_id = %s",
(node,),
)
if row == None:
write(name, sql_insert_coord(node, 0.0, 0.0))
return {'x': 0.0, 'y': 0.0}
return from_postgis_point(row['coord_geom'])
# DingZQ 2025-01-03, get nodes in extent
# return node id list
# node_id:junction:x:y
def get_nodes_in_extent(name: str, x1: float, y1: float, x2: float, y2: float) -> list[str]:
nodes = []
objs = read_all(name, 'select node_id, st_astext(geom) as coord_geom from gis.node_geometries')
for obj in objs:
node_id = obj['node_id']
coord = from_postgis_point(obj['coord_geom'])
x = coord['x']
y = coord['y']
if x1 <= x <= x2 and y1 <= y <= y2:
nodes.append(f"{node_id}:junction:{x}:{y}")
return nodes
# DingZQ 2025-01-03, get links in extent
# return link id list
# link_id:pipe:node_id1:node_id2
def get_links_in_extent(name: str, x1: float, y1: float, x2: float, y2: float) -> list[str]:
node_ids = set([s.split(':')[0] for s in get_nodes_in_extent(name, x1, y1, x2, y2)])
all_link_ids = []
with project_connection(name) as conn:
with conn.cursor(row_factory=dict_row) as cur:
cur.execute("select link_id from network.pipes")
for record in cur:
all_link_ids.append(record['link_id'])
links = []
for link_id in all_link_ids:
nodes = get_link_nodes(name, link_id)
if nodes[0] in node_ids and nodes[1] in node_ids:
links.append(f"{link_id}:pipe:{nodes[0]}:{nodes[1]}")
return links
def node_has_coord(name: str, node: str) -> bool:
return try_read(
name, "select node_id from gis.node_geometries where node_id = %s", (node,)
) != None
#--------------------------------------------------------------
# [EPA2][EPA3][IN][OUT]
# id x y
#--------------------------------------------------------------
# exception ! need merge to node change set !
def inp_in_coord(line: str) -> str:
tokens = line.split()
node = tokens[0]
x, y = float(tokens[1]), float(tokens[2])
return sql_insert_coord(node, x, y)
def inp_out_coord(name: str) -> list[str]:
lines = []
objs = read_all(name, 'select node_id, st_astext(geom) as coord_geom from gis.node_geometries')
for obj in objs:
node = obj['node_id']
coord = from_postgis_point(obj['coord_geom'])
x = coord['x']
y = coord['y']
lines.append(f'{node} {x} {y}')
return lines
+139
View File
@@ -0,0 +1,139 @@
from typing import Any
from ..core.database import (
ChangeSet,
DatabaseCommand,
execute_command,
g_add_prefix,
g_delete_prefix,
g_update_prefix,
read_all,
sql_literal,
try_read,
)
def get_label_schema(name: str) -> dict[str, dict[str, Any]]:
return { 'x' : {'type': 'float' , 'optional': False , 'readonly': False},
'y' : {'type': 'float' , 'optional': False , 'readonly': False},
'label' : {'type': 'str' , 'optional': False , 'readonly': False},
'node' : {'type': 'str' , 'optional': True , 'readonly': False} }
def get_label(name: str, x: float, y: float) -> dict[str, Any]:
d = {}
d['x'] = x
d['y'] = y
l = try_read(name, "select label, node_id as node from gis.labels where geom = st_setsrid(st_makepoint(%s, %s), 900914)", (x, y))
if l == None:
d['label'] = None
d['node'] = None
else:
d['label'] = str(l['label'])
d['node'] = str(l['node']) if l['node'] != None else None
return d
class Label(object):
def __init__(self, input: dict[str, Any]) -> None:
self.type = 'label'
self.x = float(input['x'])
self.y = float(input['y'])
self.label = str(input['label'])
self.node = str(input['node']) if 'node' in input and input['node'] != None else None
self.f_type = sql_literal(self.type)
self.f_x = sql_literal(self.x)
self.f_y = sql_literal(self.y)
self.f_label = sql_literal(self.label)
self.f_node = sql_literal(self.node)
def as_dict(self) -> dict[str, Any]:
return { 'type': self.type, 'x': self.x, 'y': self.y, 'label': self.label, 'node': self.node }
def _set_label(name: str, cs: ChangeSet) -> DatabaseCommand:
raw_new = get_label(name, cs.operations[0]['x'], cs.operations[0]['y'])
new_dict = cs.operations[0]
schema = get_label_schema(name)
for key, value in schema.items():
if key in new_dict and not value['readonly']:
raw_new[key] = new_dict[key]
new = Label(raw_new)
statement = f"update gis.labels set label = {new.f_label}, node_id = {new.f_node} where geom = st_setsrid(st_makepoint({new.f_x}, {new.f_y}), 900914);"
change = g_update_prefix | new.as_dict()
return DatabaseCommand(statement, [change])
def set_label(name: str, cs: ChangeSet) -> ChangeSet:
return execute_command(name, _set_label(name, cs))
def _add_label(name: str, cs: ChangeSet) -> DatabaseCommand:
new = Label(cs.operations[0])
statement = f"insert into gis.labels (id, node_id, label, geom) values ((select coalesce(max(id) + 1, 1) from gis.labels), {new.f_node}, {new.f_label}, st_setsrid(st_makepoint({new.f_x}, {new.f_y}), 900914));"
change = g_add_prefix | new.as_dict()
return DatabaseCommand(statement, [change])
def add_label(name: str, cs: ChangeSet) -> ChangeSet:
return execute_command(name, _add_label(name, cs))
def _delete_label(name: str, cs: ChangeSet) -> DatabaseCommand:
x = float(cs.operations[0]['x'])
y = float(cs.operations[0]['y'])
f_x = sql_literal(x)
f_y = sql_literal(y)
statement = f"delete from gis.labels where geom = st_setsrid(st_makepoint({f_x}, {f_y}), 900914);"
change = g_delete_prefix | {'type': 'label', 'x': x, 'y': y}
return DatabaseCommand(statement, [change])
def delete_label(name: str, cs: ChangeSet) -> ChangeSet:
return execute_command(name, _delete_label(name, cs))
def inp_in_label(line: str) -> str:
tokens = line.split()
num = len(tokens)
has_desc = tokens[-1].startswith(';')
num_without_desc = (num - 1) if has_desc else num
x = float(tokens[0])
y = float(tokens[1])
label = str(tokens[2])
node = str(tokens[3]) if num >= 4 else None
return str(f"insert into gis.labels (id, node_id, label, geom) values ((select coalesce(max(id) + 1, 1) from gis.labels), {sql_literal(node)}, {sql_literal(label)}, st_setsrid(st_makepoint({sql_literal(x)}, {sql_literal(y)}), 900914));")
def inp_out_label(name: str) -> list[str]:
lines = []
objs = read_all(name, 'select st_x(geom) as x, st_y(geom) as y, label, node_id as node from gis.labels order by id')
for obj in objs:
x = obj['x']
y = obj['y']
label = obj['label']
node = obj['node'] if obj['node'] != None else ''
lines.append(f'{x} {y} {label} {node}')
return lines
def unset_label_by_node(name: str, node: str) -> ChangeSet:
cs = ChangeSet()
rows = read_all(name, "select st_x(geom) as x, st_y(geom) as y from gis.labels where node_id = %s", (node,))
for row in rows:
cs.append(g_update_prefix | {'type': 'label', 'x': row['x'], 'y': row['y'], 'node': None})
return cs
+312
View File
@@ -0,0 +1,312 @@
import platform
import math
from typing import Any
import pyclipper
from ..model.elements import get_node_links, get_link_nodes, is_pipe
from ..model.pipes import get_pipe
from ..core.database import read, try_read, read_all
from .coordinates import node_has_coord, get_node_coord
def from_postgis_polygon(polygon: str) -> list[tuple[float, float]]:
boundary = polygon.lower().removeprefix('polygon((').removesuffix('))').split(',')
xys = []
for pt in boundary:
xy = pt.split(' ')
xys.append((float(xy[0]), float(xy[1])))
return xys
def to_postgis_polygon(boundary: list[tuple[float, float]]) -> str:
polygon = ''
for pt in boundary:
polygon += f'{pt[0]} {pt[1]},'
return str(f'polygon(({polygon[:-1]}))')
def to_postgis_linestring(boundary: list[tuple[float, float]]) -> str:
line = ''
for pt in boundary:
line += f'{pt[0]} {pt[1]},'
return str(f'linestring({line[:-1]})')
def get_nodes_in_boundary(name: str, boundary: list[tuple[float, float]]) -> list[str]:
rows = read_all(
name,
"select node_id from gis.node_geometries "
"where st_intersects(geom, st_geomfromtext(%s, 900914)) order by node_id",
(to_postgis_polygon(boundary),),
)
return [str(row["node_id"]) for row in rows]
def _get_links_on_boundary(name: str, nodes: list[str]) -> list[str]:
links: list[str] = []
for node in nodes:
node_links = get_node_links(name, node)
for link in node_links:
if link in links:
continue
link_nodes = get_link_nodes(name, link)
if link_nodes[0] in nodes and link_nodes[1] not in nodes:
links.append(link)
elif link_nodes[0] not in nodes and link_nodes[1] in nodes:
links.append(link)
return links
def get_nodes_in_region(name: str, region_id: str) -> list[str]:
stored = read_all(
name,
"select node_id from gis.region_nodes where region_id = %s order by node_id",
(region_id,),
)
if stored:
return [str(row["node_id"]) for row in stored]
rows = read_all(
name,
"select n.node_id from gis.node_geometries n join gis.regions r "
"on st_intersects(n.geom, r.boundary) where r.id = %s order by n.node_id",
(region_id,),
)
return [str(row["node_id"]) for row in rows]
def get_links_on_region_boundary(name: str, region_id: str) -> list[str]:
nodes = get_nodes_in_region(name, region_id)
return _get_links_on_boundary(name, nodes)
def calculate_convex_hull(name: str, nodes: list[str]) -> list[tuple[float, float]]:
row = read(
name,
"select st_astext(st_convexhull(st_collect(geom))) as boundary "
"from gis.node_geometries where node_id = any(%s)",
(nodes,),
)
return from_postgis_polygon(str(row["boundary"]))
def _verify_platform():
_platform = platform.system()
if _platform != "Windows":
raise Exception(f'Platform {_platform} unsupported (not yet)')
def _normal(v: tuple[float, float]) -> tuple[float, float]:
l = math.sqrt(v[0] * v[0] + v[1] * v[1])
return (v[0] / l, v[1] / l)
def _angle(v: tuple[float, float]) -> float:
if v[0] >= 0 and v[1] >= 0:
return math.asin(v[1])
elif v[0] <= 0 and v[1] >= 0:
return math.pi - math.asin(v[1])
elif v[0] <= 0 and v[1] <= 0:
return math.asin(-v[1]) + math.pi
elif v[0] >= 0 and v[1] <= 0:
return math.pi * 2 - math.asin(-v[1])
return 0
def _angle_of_node_link(node: str, link: str, nodes, links) -> float:
n1 = node
n2 = links[link]['node1'] if n1 == links[link]['node2'] else links[link]['node2']
x1, y1 = nodes[n1]['x'], nodes[n1]['y']
x2, y2 = nodes[n2]['x'], nodes[n2]['y']
if y1 == y2:
v = ((x2 - x1) / abs(x2 - x1), 0.0)
else:
v = _normal((x2 - x1, y2 - y1))
return _angle(v)
class Topology:
def __init__(self, db: str, nodes: list[str]) -> None:
self._nodes: dict[str, Any] = {}
self._max_x_node = ''
self._node_list: list[str] = []
for node in nodes:
if not node_has_coord(db, node):
continue
if get_node_links(db, node) == 0:
continue
self._nodes[node] = get_node_coord(db, node) | { 'links': [] }
self._node_list.append(node)
if self._max_x_node == '' or self._nodes[node]['x'] > self._nodes[self._max_x_node]['x']:
self._max_x_node = node
self._links: dict[str, Any] = {}
self._link_list: list[str] = []
for node in self._nodes:
for link in get_node_links(db, node):
candidate = True
link_nodes = get_link_nodes(db, link)
for link_node in link_nodes:
if link_node not in self._nodes:
candidate = False
break
if candidate:
length = get_pipe(db, link)['length'] if is_pipe(db, link) else 0.0
self._links[link] = { 'node1' : link_nodes[0], 'node2' : link_nodes[1], 'length' : length }
self._link_list.append(link)
if link not in self._nodes[link_nodes[0]]['links']:
self._nodes[link_nodes[0]]['links'].append(link)
if link not in self._nodes[link_nodes[1]]['links']:
self._nodes[link_nodes[1]]['links'].append(link)
def nodes(self):
return self._nodes
def node_list(self):
return self._node_list
def max_x_node(self):
return self._max_x_node
def links(self):
return self._links
def link_list(self):
return self._link_list
def _calculate_boundary(cursor: str, t_nodes: dict[str, Any], t_links: dict[str, Any]) -> tuple[list[str], dict[str, list[str]], list[tuple[float, float]]]:
in_angle = 0
vertices: list[str] = []
path: dict[str, list[str]] = {}
while True:
# prevent duplicated node
if len(vertices) > 0 and cursor == vertices[-1]:
break
# prevent duplicated path
if len(vertices) >= 3 and vertices[0] == vertices[-1] and vertices[1] == cursor:
break
vertices.append(cursor)
sorted_links = []
overlapped_link = ''
for link in t_nodes[cursor]['links']:
angle = _angle_of_node_link(cursor, link, t_nodes, t_links)
if angle == in_angle:
overlapped_link = link
continue
sorted_links.append((angle, link))
# work into a branch, return
if len(sorted_links) == 0:
path[overlapped_link] = []
cursor = vertices[-2]
in_angle = _angle_of_node_link(cursor, overlapped_link, t_nodes, t_links)
continue
sorted_links = sorted(sorted_links, key=lambda s:s[0])
out_link = sorted_links[0][1]
for angle, link in sorted_links:
if angle > in_angle:
out_link = link
break
path[out_link] = []
cursor = t_links[out_link]['node1'] if cursor == t_links[out_link]['node2'] else t_links[out_link]['node2']
in_angle = _angle_of_node_link(cursor, out_link, t_nodes, t_links)
boundary: list[tuple[float, float]] = []
for node in vertices:
boundary.append((t_nodes[node]['x'], t_nodes[node]['y']))
return (vertices, path, boundary)
def _collect_new_links(in_links: dict[str, list[str]], t_nodes: dict[str, Any], t_links: dict[str, Any], new_nodes: dict[str, Any], new_links: dict[str, Any]) -> tuple[dict[str, Any], dict[str, Any]]:
for link, pts in in_links.items():
node1 = t_links[link]['node1']
node2 = t_links[link]['node2']
x1, x2 = t_nodes[node1]['x'], t_nodes[node2]['x']
y1, y2 = t_nodes[node1]['y'], t_nodes[node2]['y']
if node1 not in new_nodes:
new_nodes[node1] = { 'x': x1, 'y': y1, 'links': [] }
if node2 not in new_nodes:
new_nodes[node2] = { 'x': x2, 'y': y2, 'links': [] }
x_delta = x2 - x1
y_delta = y2 - y1
use_x = abs(x_delta) > abs(y_delta)
if len(pts) == 0:
new_links[link] = t_links[link]
else:
sorted_nodes: list[tuple[float, str]] = []
sorted_nodes.append((0.0, node1))
sorted_nodes.append((1.0, node2))
i = 0
for pt in pts:
x, y = new_nodes[pt]['x'], new_nodes[pt]['y']
percent = ((x - x1) / x_delta) if use_x else ((y - y1) / y_delta)
sorted_nodes.append((percent, pt))
i += 1
sorted_nodes = sorted(sorted_nodes, key=lambda s:s[0])
for i in range(1, len(sorted_nodes)):
l = sorted_nodes[i - 1][1]
r = sorted_nodes[i][1]
new_link = f'LINK_[{l}]_[{r}]'
new_links[new_link] = { 'node1': l, 'node2': r }
return (new_nodes, new_links)
def calculate_boundary(name: str, nodes: list[str], accurate = False) -> list[tuple[float, float]]:
topology = Topology(name, nodes)
t_nodes = topology.nodes()
t_links = topology.links()
_, _, boundary = _calculate_boundary(topology.max_x_node(), t_nodes, t_links)
return boundary
'''
# CClipper2.dll
# int inflate_paths(double* path, size_t size, double delta, int jt, int et, double miter_limit, int precision, double arc_tolerance, double** out_path, size_t* out_size);
# int simplify_paths(double* path, size_t size, double epsilon, int is_closed_path, double** out_path, size_t* out_size);
# void free_paths(double** paths);
'''
def inflate_boundary(name: str, boundary: list[tuple[float, float]], delta: float = 0.5) -> list[tuple[float, float]]:
if boundary[0] == boundary[-1]:
del(boundary[-1])
precision = 2
scale = 10 ** precision
path = [(round(x * scale), round(y * scale)) for x, y in boundary]
offset = pyclipper.PyclipperOffset(miter_limit=2.0)
offset.AddPath(path, pyclipper.JT_SQUARE, pyclipper.ET_CLOSEDPOLYGON)
solutions = offset.Execute(round(delta * scale))
if len(solutions) == 0:
return []
result: list[tuple[float, float]] = []
for x, y in solutions[0]:
result.append((x / scale, y / scale))
result.append(result[0])
return result
def inflate_region(name: str, region_id: str, delta: float = 0.5) -> list[tuple[float, float]]:
r = try_read(name, "select id, st_astext(boundary) as boundary_geom from gis.regions where id = %s", (region_id,))
if r == None:
return []
boundary = from_postgis_polygon(str(r['boundary_geom']))
return inflate_boundary(name, boundary, delta)
if __name__ == '__main__':
_verify_platform()
+127
View File
@@ -0,0 +1,127 @@
from typing import Any
from ..core.database import (
ChangeSet,
DatabaseCommand,
execute_command,
g_add_prefix,
g_delete_prefix,
g_update_prefix,
sql_literal,
try_read,
)
from .region_geometry import from_postgis_polygon, to_postgis_polygon
def get_region_schema(name: str) -> dict[str, dict[str, Any]]:
return {
"id": {"type": "str", "optional": False, "readonly": True},
"region_type": {"type": "str", "optional": False, "readonly": False},
"boundary": {"type": "tuple_list", "optional": False, "readonly": False},
}
def get_region(name: str, id: str) -> dict[str, Any]:
row = try_read(
name,
"select id, region_type, st_astext(boundary) as boundary_geom "
"from gis.regions where id = %s",
(id,),
)
if row is None:
return {}
return {
"id": str(row["id"]),
"region_type": str(row["region_type"]),
"boundary": from_postgis_polygon(str(row["boundary_geom"])),
}
def _valid_boundary(boundary: list[Any]) -> bool:
return len(boundary) >= 4 and boundary[0] == boundary[-1]
def _set_region(name: str, cs: ChangeSet) -> DatabaseCommand:
region_id = cs.operations[0]["id"]
old = get_region(name, region_id)
new = old | {
key: cs.operations[0][key]
for key in ("region_type", "boundary")
if key in cs.operations[0]
}
statement = (
"update gis.regions set "
f"region_type = {sql_literal(new['region_type'])}, "
f"boundary = st_geomfromtext({sql_literal(to_postgis_polygon(new['boundary']))}, 900914) "
f"where id = {sql_literal(region_id)};"
)
return DatabaseCommand(
statement,
[g_update_prefix | {"type": "region"} | new],
)
def set_region(name: str, cs: ChangeSet) -> ChangeSet:
operation = cs.operations[0]
if "id" not in operation or get_region(name, operation["id"]) == {}:
return ChangeSet()
if "boundary" in operation and not _valid_boundary(operation["boundary"]):
return ChangeSet()
return execute_command(name, _set_region(name, cs))
def _add_region(name: str, cs: ChangeSet) -> DatabaseCommand:
operation = cs.operations[0]
region_id = operation["id"]
region_type = str(operation.get("region_type", "none"))
boundary = operation["boundary"]
statement = (
"insert into gis.regions (id, region_type, boundary) values "
f"({sql_literal(region_id)}, {sql_literal(region_type)}, "
f"st_geomfromtext({sql_literal(to_postgis_polygon(boundary))}, 900914));"
)
value = {"type": "region", "id": region_id, "region_type": region_type, "boundary": boundary}
return DatabaseCommand(
statement,
[g_add_prefix | value],
)
def add_region(name: str, cs: ChangeSet) -> ChangeSet:
operation = cs.operations[0]
if "id" not in operation or "boundary" not in operation:
return ChangeSet()
if not _valid_boundary(operation["boundary"]):
return ChangeSet()
if get_region(name, operation["id"]) != {}:
return ChangeSet()
return execute_command(name, _add_region(name, cs))
def _delete_region(name: str, cs: ChangeSet) -> DatabaseCommand:
region_id = cs.operations[0]["id"]
statement = f"delete from gis.regions where id = {sql_literal(region_id)};"
return DatabaseCommand(
statement,
[g_delete_prefix | {"type": "region", "id": region_id}],
)
def delete_region(name: str, cs: ChangeSet) -> ChangeSet:
if "id" not in cs.operations[0] or get_region(name, cs.operations[0]["id"]) == {}:
return ChangeSet()
return execute_command(name, _delete_region(name, cs))
def inp_in_region(line: str) -> str:
tokens = line.split()
return f"insert into gis.regions (id, region_type) values ({sql_literal(tokens[0])}, {sql_literal(tokens[1])});"
def inp_in_bound(line: str) -> str:
return line.split()[0]
def inp_in_regionnodes(line: str) -> str:
tokens = line.split()
return f"insert into gis.region_nodes (region_id, node_id) values ({sql_literal(tokens[0])}, {sql_literal(tokens[1])});"
+122
View File
@@ -0,0 +1,122 @@
from typing import Any
from ..core.database import (
ChangeSet,
DatabaseCommand,
execute_command,
g_add_prefix,
g_delete_prefix,
g_update_prefix,
read_all,
sql_literal,
try_read,
)
def get_vertex_schema(name: str) -> dict[str, dict[str, Any]]:
return { 'link' : {'type': 'str' , 'optional': False , 'readonly': True },
'coords' : {'type': 'list' , 'optional': False , 'readonly': False,
'element': { 'x' : {'type': 'float' , 'optional': False , 'readonly': False },
'y' : {'type': 'float' , 'optional': False , 'readonly': False } }}}
def get_vertex(name: str, link: str) -> dict[str, Any]:
cus = read_all(name, "select st_x(geom) as x, st_y(geom) as y from gis.link_vertices where link_id = %s order by sequence_no", (link,))
cs = []
for r in cus:
cs.append({ 'x': float(r['x']), 'y': float(r['y']) })
return { 'link': link, 'coords': cs }
def _set_vertex(name: str, cs: ChangeSet) -> DatabaseCommand:
link = cs.operations[0]['link']
new = { 'link': link, 'coords': [] }
f_link = sql_literal(link)
statement = f"delete from gis.link_vertices where link_id = {f_link};"
for sequence_no, xy in enumerate(cs.operations[0]['coords']):
x, y = float(xy['x']), float(xy['y'])
f_x, f_y = sql_literal(x), sql_literal(y)
statement += f"\ninsert into gis.link_vertices (link_id, sequence_no, geom) values ({f_link}, {sequence_no}, st_setsrid(st_makepoint({f_x}, {f_y}), 900914));"
new['coords'].append({ 'x': x, 'y': y })
change = { 'type': 'vertex' } | new
return DatabaseCommand(statement, [change])
def set_vertex(name: str, cs: ChangeSet) -> ChangeSet:
result = _set_vertex(name, cs)
result.changes[0] |= g_update_prefix
return execute_command(name, result)
def _add_vertex(name: str, cs: ChangeSet) -> DatabaseCommand:
result = _set_vertex(name, cs)
result.changes[0] |= g_add_prefix
return result
def _delete_vertex(name: str, cs: ChangeSet) -> DatabaseCommand:
cs.operations[0]['coords'] = []
result = _set_vertex(name, cs)
result.changes[0] |= g_delete_prefix
return result
def add_vertex(name: str, cs: ChangeSet) -> ChangeSet:
result = _add_vertex(name, cs)
return execute_command(name, result)
def delete_vertex(name: str, cs: ChangeSet) -> ChangeSet:
result = _delete_vertex(name, cs)
return execute_command(name, result)
def get_all_vertex_links(name: str) -> list[str]:
result : list[str] = []
rows = read_all(name, 'select distinct link_id from gis.link_vertices order by link_id')
for row in rows:
result.append(str(row['link_id']))
return result
def get_all_vertices(name: str) -> list[dict[str, Any]]:
return read_all(name, 'select link_id, sequence_no, st_x(geom) as x, st_y(geom) as y from gis.link_vertices order by link_id, sequence_no')
#--------------------------------------------------------------
# [EPA2][IN][OUT]
# id x y
# [EPA3][NOT SUPPORT]
#--------------------------------------------------------------
def inp_in_vertex(line: str) -> str:
tokens = line.split()
link = tokens[0]
x = float(tokens[1])
y = float(tokens[2])
link_sql = sql_literal(link)
return f"insert into gis.link_vertices (link_id, sequence_no, geom) values ({link_sql}, (select coalesce(max(sequence_no) + 1, 0) from gis.link_vertices where link_id = {link_sql}), st_setsrid(st_makepoint({sql_literal(x)}, {sql_literal(y)}), 900914));"
def inp_out_vertex(name: str) -> list[str]:
lines = []
objs = read_all(name, "select link_id, st_x(geom) as x, st_y(geom) as y from gis.link_vertices order by link_id, sequence_no")
for obj in objs:
link = obj['link_id']
x = obj['x']
y = obj['y']
lines.append(f"{link} {x} {y}")
return lines
def delete_vertex_by_link(name: str, link: str) -> ChangeSet:
row = try_read(name, "select * from gis.link_vertices where link_id = %s", (link,))
if row == None:
return ChangeSet()
return ChangeSet(g_delete_prefix | {'type': 'vertex', 'link' : link})