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:
@@ -0,0 +1 @@
|
||||
"""GIS persistence and network geometry operations."""
|
||||
@@ -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')
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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])});"
|
||||
@@ -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})
|
||||
Reference in New Issue
Block a user