snapedge GEOMETRY;
BEGIN
+ -- 0. Check arguments
+ IF geometrytype(apoint) != 'POINT' THEN
+ RAISE EXCEPTION 'Invalid geometry type (%) passed to TopoGeo_AddPoint, expected POINT', geometrytype(apoint);
+ END IF;
+
-- 1. Check if any existing node falls within tolerance
-- and if so pick the closest
sql := 'SELECT a.node_id FROM '
RAISE DEBUG '%', sql;
EXECUTE sql INTO rec;
IF rec IS NOT NULL THEN
- RAISE DEBUG 'Splitting edge %', rec.edge_id;
-- project point to line, split edge by point
- prj := st_closestpoint(rec.geom, apoint);
+ prj := ST_ClosestPoint(rec.geom, apoint);
+ RAISE DEBUG 'Splitting edge % with closest point %', rec.edge_id, ST_AsText(prj);
IF NOT ST_Contains(rec.geom, prj) THEN
RAISE DEBUG ' Snapping edge to contain closest point';
-- Snap the edge geometry to the projected point
id := topology.ST_ModEdgeSplit(atopology, rec.edge_id, prj);
ELSE
RAISE DEBUG 'No existing edge within tolerance distance';
- id := ST_AddIsoNode(atopology, NULL, apoint);
+ id := topology.ST_AddIsoNode(atopology, NULL, apoint);
END IF;
RETURN id;
--
-- Add a LineString into a topology
--
+-- }{
CREATE OR REPLACE FUNCTION topology.TopoGeo_addLinestring(atopology varchar, aline geometry, tolerance float8 DEFAULT 0)
- RETURNS void AS
+ RETURNS SETOF int AS
$$
DECLARE
+ rec RECORD;
+ sql TEXT;
+ set1 GEOMETRY;
+ set2 GEOMETRY;
+ snapped GEOMETRY;
+ noded GEOMETRY;
+ start_node INTEGER;
+ end_node INTEGER;
+ id INTEGER;
BEGIN
- RAISE EXCEPTION 'TopoGeo_AddLineString not implemented yet';
+
+ -- 0. Check arguments
+ IF geometrytype(aline) != 'LINESTRING' THEN
+ RAISE EXCEPTION 'Invalid geometry type (%) passed to TopoGeo_AddPoint, expected LINESTRING', geometrytype(aline);
+ END IF;
+
+ -- 1. Self-node
+ noded := ST_UnaryUnion(aline);
+ RAISE DEBUG 'Self-noded: %', ST_AsText(noded);
+
+ -- 2. Node to edges and nodes falling within tolerance distance
+ sql := 'WITH nearby AS ( SELECT geom FROM '
+ || quote_ident(atopology)
+ || '.node WHERE ST_DWithin(geom,'
+ || quote_literal(aline::text) || '::geometry,'
+ || tolerance || ') UNION ALL SELECT geom FROM '
+ || quote_ident(atopology)
+ || '.edge WHERE ST_DWithin(geom,'
+ || quote_literal(aline::text) || '::geometry,'
+ || tolerance || ') ) SELECT st_collect(geom) FROM nearby;';
+ RAISE DEBUG '%', sql;
+ EXECUTE sql INTO set1;
+ IF set1 IS NOT NULL THEN
+ snapped := ST_Snap(noded, set1, tolerance);
+ RAISE DEBUG 'Snapped: %', ST_AsText(snapped);
+ noded := ST_Difference(snapped, set1);
+ RAISE DEBUG 'Difference: %', ST_AsText(noded);
+ set2 := ST_Intersection(snapped, set1);
+ RAISE DEBUG 'Intersection: %', ST_AsText(set2);
+ noded := ST_Union(noded, set2);
+ END IF;
+
+ -- 3. For each (now-noded) segment, insert an edge
+ FOR rec IN SELECT (ST_Dump(noded)).geom LOOP
+
+ -- TODO: skip point elements ?
+
+ RAISE DEBUG 'Adding edge %', ST_AsText(rec.geom);
+
+ start_node := topology.TopoGeo_AddPoint(atopology,
+ ST_StartPoint(rec.geom),
+ tolerance);
+ RAISE DEBUG ' Start Node: %', start_node;
+
+ end_node := topology.TopoGeo_AddPoint(atopology,
+ ST_EndPoint(rec.geom),
+ tolerance);
+ RAISE DEBUG ' End Node: %', end_node;
+
+ -- Added endpoints may have drifted due to tolerance, so
+ -- we need to re-snap the edge to the new nodes before adding it
+ sql := 'SELECT ST_Collect(geom) FROM ' || quote_ident(atopology)
+ || '.node WHERE node_id IN (' || start_node || ',' || end_node || ')';
+ RAISE DEBUG '%', sql;
+ EXECUTE sql INTO STRICT set2;
+ RAISE DEBUG 'Endnodes: %', ST_AsText(set2);
+ snapped := ST_Snap(rec.geom, set2, tolerance);
+ RAISE DEBUG 'Snapped edge: %', ST_AsText(snapped);
+
+ -- Check if the so-snapped edge _now_ exists
+ sql := 'SELECT edge_id FROM ' || quote_ident(atopology)
+ || '.edge_data WHERE ST_Equals(geom, ' || quote_literal(snapped::text)
+ || '::geometry)';
+ RAISE DEBUG '%', sql;
+ EXECUTE sql INTO id;
+ IF id IS NULL THEN
+ id := topology.ST_AddEdgeModFace(atopology, start_node, end_node,
+ snapped);
+ RAISE DEBUG 'New edge id: %', id;
+ ELSE
+ RAISE DEBUG 'Old edge id: %', id;
+ END IF;
+
+ RETURN NEXT id;
+
+ END LOOP;
+
+ RETURN;
END
$$
LANGUAGE 'plpgsql';
regress/topoelement.sql \
regress/topoelementarray_agg.sql \
regress/topogeo_addpoint.sql \
+ regress/topogeo_addlinestring \
regress/topogeometry_type.sql \
regress/topo2.5d.sql \
regress/totopogeom.sql \
--- /dev/null
+\set VERBOSITY terse
+set client_min_messages to ERROR;
+
+\i load_topology.sql
+
+-- Save max node id
+select 'node'::text as what, max(node_id) INTO city_data.limits FROM city_data.node;
+INSERT INTO city_data.limits select 'edge'::text as what, max(edge_id) FROM city_data.edge;
+SELECT 'max',* from city_data.limits;
+
+-- Check changes since last saving, save more
+-- {
+CREATE OR REPLACE FUNCTION check_changes()
+RETURNS TABLE (o text)
+AS $$
+DECLARE
+ rec RECORD;
+ sql text;
+BEGIN
+ -- Check effect on nodes
+ sql := 'SELECT n.node_id, ''N|'' || n.node_id || ''|'' ||
+ COALESCE(n.containing_face::text,'''') || ''|'' ||
+ ST_AsText(ST_SnapToGrid(n.geom, 0.2))::text as xx
+ FROM city_data.node n WHERE n.node_id > (
+ SELECT max FROM city_data.limits WHERE what = ''node''::text )
+ ORDER BY n.node_id';
+
+ FOR rec IN EXECUTE sql LOOP
+ o := rec.xx;
+ RETURN NEXT;
+ END LOOP;
+
+ -- Check effect on edges (there should be one split)
+ sql := '
+ WITH node_limits AS ( SELECT max FROM city_data.limits WHERE what = ''node''::text ),
+ edge_limits AS ( SELECT max FROM city_data.limits WHERE what = ''edge''::text )
+ SELECT ''E|'' || e.edge_id || ''|sn'' || e.start_node || ''|en'' || e.end_node :: text as xx
+ FROM city_data.edge e, node_limits nl, edge_limits el
+ WHERE e.start_node > nl.max
+ OR e.end_node > nl.max
+ OR e.edge_id > el.max
+ ORDER BY e.edge_id;
+ ';
+
+ FOR rec IN EXECUTE sql LOOP
+ o := rec.xx;
+ RETURN NEXT;
+ END LOOP;
+
+ UPDATE city_data.limits SET max = (SELECT max(n.node_id) FROM city_data.node n) WHERE what = 'node';
+ UPDATE city_data.limits SET max = (SELECT max(e.edge_id) FROM city_data.edge e) WHERE what = 'edge';
+
+END;
+$$ LANGUAGE 'plpgsql';
+-- }
+
+
+-- Invalid calls
+SELECT 'invalid', TopoGeo_addLineString('city_data', 'MULTILINESTRING((36 26, 38 30))');
+SELECT 'invalid', TopoGeo_addLineString('city_data', 'POINT(36 26)');
+
+-- Isolated edge in universal face
+SELECT 'iso_uni', TopoGeo_addLineString('city_data', 'LINESTRING(36 26, 38 30)');
+SELECT check_changes();
+
+-- Isolated point in face 5
+SELECT 'iso_f5', TopoGeo_addLineString('city_data', 'LINESTRING(37 20, 43 19, 41 16)');
+SELECT check_changes();
+
+-- Existing isolated edge
+SELECT 'iso_ex', TopoGeo_addLineString('city_data', 'LINESTRING(36 26, 38 30)');
+SELECT check_changes();
+
+-- Existing isolated edge within tolerance
+SELECT 'iso_ex_tol', TopoGeo_addLineString('city_data', 'LINESTRING(36 27, 38 31)', 2);
+SELECT check_changes();
+
+-- Existing non-isolated edge
+SELECT 'noniso_ex', TopoGeo_addLineString('city_data', 'LINESTRING(35 6, 35 14)');
+SELECT check_changes();
+
+-- Existing non-isolated edge within tolerance
+SELECT 'noniso_ex_tol', TopoGeo_addLineString('city_data', 'LINESTRING(35 7, 35 13)', 2);
+SELECT check_changes();
+
+-- Fully contained
+SELECT 'contained', TopoGeo_addLineString('city_data', 'LINESTRING(35 8, 35 12)');
+SELECT check_changes();
+
+-- Overlapping
+SELECT 'overlap', TopoGeo_addLineString('city_data', 'LINESTRING(45 22, 49 22)') ORDER BY 2;
+SELECT check_changes();
+
+-- Crossing
+SELECT 'cross', TopoGeo_addLineString('city_data', 'LINESTRING(49 18, 44 17)') ORDER BY 2;
+SELECT check_changes();
+
+-- Snapping (and splitting a face)
+SELECT 'snap', TopoGeo_addLineString('city_data', 'LINESTRING(18 22.2, 22.5 22.2, 21.2 20.5)', 1) ORDER BY 2;
+SELECT check_changes();
+SELECT 'snap_again', TopoGeo_addLineString('city_data', 'LINESTRING(18 22.2, 22.5 22.2, 21.2 20.5)', 1) ORDER BY 2;
+SELECT check_changes();
+
+-- A mix of crossing and overlapping, splitting another face
+SELECT 'crossover', TopoGeo_addLineString('city_data', 'LINESTRING(9 18, 9 20, 21 10, 21 7)') ORDER BY 2;
+SELECT check_changes();
+SELECT 'crossover_again', TopoGeo_addLineString('city_data', 'LINESTRING(9 18, 9 20, 21 10, 21 7)') ORDER BY 2;
+SELECT check_changes();
+
+-- Fully containing
+SELECT 'contains', TopoGeo_addLineString('city_data', 'LINESTRING(14 34, 13 35, 10 35, 9 35, 7 36)') ORDER BY 2;
+SELECT check_changes();
+
+DROP FUNCTION check_changes();
+SELECT DropTopology('city_data');
--- /dev/null
+BEGIN
+t
+9
+22
+26
+COMMIT
+max|node|22
+max|edge|26
+ERROR: Invalid geometry type (MULTILINESTRING) passed to TopoGeo_AddPoint, expected LINESTRING
+ERROR: Invalid geometry type (POINT) passed to TopoGeo_AddPoint, expected LINESTRING
+iso_uni|27
+N|23||POINT(36 26)
+N|24||POINT(38 30)
+E|27|sn23|en24
+iso_f5|28
+N|25||POINT(37 20)
+N|26||POINT(41 16)
+E|28|sn25|en26
+iso_ex|27
+iso_ex_tol|27
+noniso_ex|18
+noniso_ex_tol|18
+contained|29
+N|27||POINT(35 8)
+N|28||POINT(35 12)
+E|18|sn10|en27
+E|29|sn27|en28
+E|30|sn28|en13
+overlap|31
+overlap|32
+N|29||POINT(49 22)
+N|30||POINT(45 22)
+E|8|sn18|en30
+E|31|sn19|en29
+E|32|sn30|en19
+cross|34
+cross|35
+N|31||POINT(49 18)
+N|32||POINT(47 17.6)
+N|33||POINT(44 17)
+E|15|sn12|en32
+E|33|sn32|en19
+E|34|sn31|en32
+E|35|sn32|en33
+snap|7
+snap|36
+snap|39
+N|34||POINT(18 22)
+N|35||POINT(22.4 22)
+N|36||POINT(21 20.4)
+E|6|sn16|en34
+E|7|sn17|en35
+E|19|sn14|en36
+E|36|sn34|en17
+E|37|sn35|en18
+E|38|sn36|en17
+E|39|sn35|en36
+snap_again|7
+snap_again|36
+snap_again|39
+crossover|42
+crossover|44
+crossover|45
+crossover|46
+N|37||POINT(9 20)
+N|38||POINT(16.2 14)
+N|39||POINT(21 10)
+N|40||POINT(9 18)
+N|41||POINT(21 7)
+E|9|sn15|en38
+E|20|sn9|en41
+E|21|sn15|en40
+E|40|sn37|en16
+E|41|sn38|en14
+E|42|sn37|en38
+E|43|sn39|en14
+E|44|sn38|en39
+E|45|sn40|en37
+E|46|sn41|en39
+crossover_again|42
+crossover_again|44
+crossover_again|45
+crossover_again|46
+contains|25
+contains|47
+contains|48
+contains|49
+N|42||POINT(14 34)
+N|43||POINT(7 36)
+N|44||POINT(10 35)
+E|25|sn21|en44
+E|47|sn42|en22
+E|48|sn21|en43
+E|49|sn44|en22
+Topology 'city_data' dropped