diff --git a/analysers/analyser_osmosis_powerline.py b/analysers/analyser_osmosis_powerline.py index 8828103c0..b02ebef26 100644 --- a/analysers/analyser_osmosis_powerline.py +++ b/analysers/analyser_osmosis_powerline.py @@ -24,28 +24,127 @@ from .Analyser_Osmosis import Analyser_Osmosis from modules.Stablehash import stablehash64 -# Power lines nodes with their voltage as array padded up to 99 zeros +# Power lines nodes with their voltage as array padded up to 99 zeros to cope with non-numerical values # Lines with no voltage get null voltage instead empty array sql01 = """ CREATE TEMP TABLE power_lines_nodes AS SELECT - ways.id as wid, - unnest(ways.nodes) AS nid, - unnest(ways.nodes[2:array_length(ways.nodes,1)]) AS nid_next, - ways.tags->'cables' as cables, - ways.tags->'circuits' as circuits, - coalesce(ways.tags->'location','overhead') as location, - voltage + w.id as wid, + unnest('{NULL}' || w.nodes[1:array_length(w.nodes, 1) - 1]) AS nid_prec, + unnest(w.nodes) AS nid, + unnest(w.nodes[2:]) AS nid_next, + w.tags->'cables' AS cables, + coalesce((w.tags->'circuits')::integer, 1) AS circuits, + coalesce(w.tags->'location', 'overhead') AS location, + voltages FROM - ways - JOIN LATERAL (SELECT array_agg(lpad(v, 99, '0')) FROM unnest(regexp_split_to_array(ways.tags->'voltage','; *')) AS t(v)) AS t(voltage) ON TRUE + ways AS w + JOIN LATERAL ( + -- Creating a voltage list consistent with number of circuits by padding with the first voltage when necessary or limiting: + -- Filling size is the difference between circuits (defaults to 1) and the number of voltages in the list + -- circuits=1 + voltage=20000;400 => voltage={20000} (no fill and limiting to 1) + -- circuits=2 + voltage=20000 => voltage={20000;20000} (filling with first value and no limiting) + -- circuits=2 + voltage=20000;400 => voltage={20000;400} (no fill and no limiting) + -- circuits=3 + voltage=20000;400 => voltage={20000;20000;400} (filling with first value and no limiting) + SELECT array_agg(lpad(v, 99, '0')) + FROM (SELECT + unnest(array_cat( + array_fill( + split_part(w.tags->'voltage', ';', 1)::text, -- voltage1 in voltage1;voltage2 + ARRAY[greatest(0, coalesce((w.tags->'circuits')::integer, 1) - (1 + length(coalesce(w.tags->'voltage', '')) - length(replace(coalesce(w.tags->'voltage',''), ';', ''))))] + ), + regexp_split_to_array(w.tags->'voltage', '; *')) + ) LIMIT coalesce((w.tags->'circuits')::integer, 1) + ) AS t(v)) AS t(voltages) + ON TRUE WHERE - ways.tags != ''::hstore AND - ways.tags?'power' AND - ways.tags->'power' IN ('line', 'minor_line', 'cable') + w.tags != ''::hstore AND + w.tags?'power' AND + w.tags->'power' IN ('line', 'minor_line', 'cable') AND + w.tags?'voltage' AND + ( + NOT w.tags?'circuits' OR + w.tags->'circuits' ~ '^[0-9]+$' + ) + +UNION ALL + +SELECT + w.id AS wid, + unnest('{NULL}' || w.nodes[1:array_length(w.nodes, 1) - 1]) AS nid_prec, + unnest(w.nodes) AS nid, + unnest(w.nodes[2:]) AS nid_next, + w.tags->'cables' AS cables, + coalesce((w.tags->'circuits')::integer, 1) AS circuits, + coalesce(w.tags->'location', 'overhead') AS location, + NULL AS voltages +FROM + ways AS w +WHERE + w.tags != ''::hstore AND + w.tags?'power' AND + w.tags->'power' IN ('line', 'minor_line', 'cable') AND + ( + NOT w.tags?'voltage' OR ( + w.tags?'circuits' AND + w.tags->'circuits' !~ '^[0-9]+$' + ) + ) +""" + +# Build junctions knowledge +# Topoedges are couples attached to a given node with their neighbors. +# Topoedges are agregated by nodes and location (two *overhead* lines between two node will give a single topoedge) +# Involved nodes are not necessary power, particularly on cables +sql02 = """ +CREATE TEMP TABLE power_lines_topoedges AS +WITH topotuples as ( + SELECT + n.wid, + n.nid # n.nid_prec AS tid, + n.nid, + n.location, + n.cables, + n.circuits, + voltages + FROM + power_lines_nodes AS n + WHERE + nid_prec IS NOT NULL + + UNION ALL + + SELECT + n.wid, + n.nid # n.nid_next as tid, + n.nid, + n.location, + n.cables, + n.circuits, + voltages + FROM + power_lines_nodes AS n + WHERE + nid_next IS NOT NULL +) + +SELECT + p.nid, + p.tid, + p.location, + count(distinct p.wid) AS cw, + sum(p.circuits::integer) AS circuits, + regexp_split_to_array(string_agg(array_to_string(p.voltages, ';'), ';'), '; *') AS voltages +FROM + topotuples p +GROUP BY + p.nid, p.tid, p.location +HAVING + bool_and(p.circuits IS NOT NULL) """ # Lone power supports +# TODO rework by using exclusion from power_lines_nodes, it will save a join and work on a lower amount of pre-selected power nodes sql10 = """ SELECT nodes.id, @@ -71,6 +170,7 @@ """ # Power lines ends with their voltages as array padded with up to 99 zeros +# TODO rework this analysis with topoedges, find topoedges that end a line and conflate them with terminators. sql20 = """ CREATE TEMP TABLE power_lines_ends AS SELECT DISTINCT ON (ends(ways.nodes)) @@ -172,11 +272,13 @@ sql26 = """ SELECT t.nid, + t.wid, ST_AsText(t.geom), t.power FROM ( SELECT u.nid, + u.wid, u.geom, u.power FROM @@ -186,6 +288,7 @@ SELECT u.nid, + u.wid, u.geom, u.power FROM @@ -197,6 +300,7 @@ SELECT u.nid, + u.wid, u.geom, u.power FROM @@ -211,31 +315,96 @@ ) AS t """ -# Power lines junctions as nodes with voltage repeated several times +# Every plain line junction that isn't transformers, termination or cross repeated twice (main and / sqrt(3)) (meaning the junction involves different voltages) +# It looks for voltage continuation on every junction. Two (or more) topoedges on a given node with the same voltage means a connection. +# TODO support partial termination (i.e termination|straight) with different voltages involved. sql30 = """ -CREATE VIEW power_lines_junctions AS -SELECT - p.nid -FROM - (SELECT nid FROM power_lines_nodes n WHERE n.voltage IS NOT NULL GROUP BY n.wid, n.nid) AS p -GROUP BY - p.nid -HAVING - COUNT(*) > 1 -""" +WITH nodes_voltage AS ( + SELECT + nid, + tid, + unnest(voltages)::varchar AS voltage + FROM + power_lines_topoedges +), +nodes_voltage_values AS ( + -- Nodes with their voltage in deca-volts + SELECT + nid, + tid, + voltage, + (voltage::numeric / 1000)::numeric(11,1)::varchar AS voltage_val, --rounding voltage in deca-volts + 'numeric' AS origin + FROM + nodes_voltage + WHERE + voltage ~ '^[0-9.]+$' + + UNION + + -- Nodes with their voltage to ground in deca-volts + SELECT + nid, + tid, + voltage AS voltage, + (voltage::numeric / (1000 * sqrt(3)))::numeric(11,1)::varchar AS voltage_val, + 'numeric' AS origin + FROM + nodes_voltage + WHERE + voltage ~ '^[0-9.]+$' + + UNION + + -- Nodes with no voltages + SELECT + nid, + tid, + voltage AS voltage, + voltage AS voltage_val, + 'varchar' AS origin + FROM + nodes_voltage + WHERE + voltage !~ '^[0-9.]+$' +), + +-- We select nodes connecting at least two edges +nodes_selected AS ( + SELECT + nid + FROM + power_lines_topoedges + GROUP BY + nid + HAVING + count(distinct tid) > 1 +), + +-- Matching selected nodes by their voltage or between voltage and voltage-to-ground independently +voltage_groups AS ( + SELECT + n.nid, + max(n.voltage) AS voltage, + n.voltage_val, + count(n.voltage) AS cv, + n.origin + FROM + nodes_voltage_values AS n + JOIN nodes_selected AS s ON + s.nid = n.nid + GROUP BY + n.nid, n.voltage_val, n.origin +) -# Every junctions that aren't transformers cross, splits or terminations repeated a single time (meaning the junction involves different voltages) -sql31 = """ SELECT - DISTINCT(j.nid), + DISTINCT(v.nid), ST_AsText(nodes.geom) FROM - power_lines_junctions j - NATURAL JOIN power_lines_nodes n + voltage_groups AS v JOIN nodes ON - n.nid = nodes.id + v.nid = nodes.id WHERE - n.voltage is not null AND ( NOT nodes.tags?'power' OR nodes.tags->'power' != 'transformer' @@ -243,25 +412,22 @@ NOT nodes.tags?'transformer' AND -- example: power=pole + transformer=* ( NOT nodes.tags?'line_management' OR - ( - NOT 'split' = ANY(string_to_array(nodes.tags->'line_management', '|')) AND - NOT 'termination' = ANY(string_to_array(nodes.tags->'line_management', '|')) AND - nodes.tags->'line_management' != 'cross' - ) + nodes.tags->'line_management' NOT IN ('cross', 'termination') ) - GROUP BY - j.nid, - n.voltage, - nodes.geom + v.nid, + nodes.geom, + v.voltage, + v.origin HAVING - COUNT(*) = 1 + (v.origin='numeric' AND sum(v.cv)=2) OR (v.origin='varchar' AND sum(v.cv)=1) """ # Non power nodes on power line and minor_line ways sql40 = """ SELECT DISTINCT ON (nodes.id) - nodes.id, + nodes.id AS nid, + ways.id AS wid, ST_AsText(nodes.geom) FROM ways @@ -362,35 +528,22 @@ """ # Find line_management and location:transition values from power lines nodes +# Two circuits in vertices query means 1 in and 1 out of a given node, so straight. # Please keep case when ordered sql70 = """ CREATE TEMP TABLE power_lines_mgmt AS -WITH topotuples as ( - (SELECT - n.wid, n.nid, n.location, n.cables, coalesce(n.circuits, CASE n.cables WHEN '3' THEN '1' ELSE NULL END) as circuits - FROM power_lines_nodes n - WHERE nid_next IS NOT NULL) - - UNION ALL - - (SELECT - n.wid, n.nid_next as nid, n.location, n.cables, coalesce(n.circuits, CASE n.cables WHEN '3' THEN '1' ELSE NULL END) as circuits - FROM power_lines_nodes n - WHERE nid_next IS NOT NULL) -),junctions as ( +WITH vertices AS ( SELECT - COUNT(distinct p.wid) as cw, - COUNT(*) as cn, - p.nid, - string_agg(CASE p.location WHEN 'overhead' THEN p.circuits ELSE NULL END,'-' order by p.circuits desc) as circuits_overhead, - string_agg(CASE WHEN p.location!='overhead' THEN p.circuits ELSE NULL END,'-' order by p.circuits desc) as circuits_elsewhere + e.nid, + string_agg(CASE e.location WHEN 'overhead' THEN e.circuits::varchar ELSE NULL END, '-' ORDER BY e.circuits desc) AS circuits_overhead, + string_agg(CASE WHEN e.location!='overhead' THEN e.circuits::varchar ELSE NULL END, '-' ORDER BY e.circuits desc) AS circuits_elsewhere FROM - topotuples p + power_lines_topoedges e GROUP BY - p.nid + e.nid HAVING - COUNT(*) > 1 AND COUNT(distinct p.wid) > 1 AND array_position(array_agg(p.circuits), NULL) IS NULL + count(*) > 1 AND sum(e.circuits) > 2 ) SELECT @@ -432,7 +585,7 @@ ELSE NULL END as location_transition FROM - junctions j + vertices j """ sql71 = """ @@ -505,11 +658,16 @@ def __init__(self, config, logger = None): self.classs[8] = self.def_class(item = 7040, level = 3, tags = ['power', 'fix:chair'], title = T_('Power support line management suggestion')) - self.callback40 = lambda res: {"class":4, "data":[self.node_full, self.positionAsText], "fix":[{"+": {"power": "tower"}}, {"+": {"power": "pole"}}]} self.callback50 = lambda res: {"class":5, "subclass": stablehash64(res[1]), "data":[self.way_full, self.positionAsText]} + def way_power(self, res): + way_data = self.apiconn.WayGet(res) + way_tags = {key: way_data["tag"][key] for key in way_data["tag"].keys() & {'power', 'voltage'}} + self.geom["way"].append({"id":res, "nd":[], "tag":way_tags}) + def analyser_osmosis_common(self): self.run(sql01) + self.run(sql02) self.run(sql10, lambda res: {"class":1, "data":[self.node_full, self.positionAsText]} ) self.run(sql20) self.run(sql21) @@ -517,10 +675,9 @@ def analyser_osmosis_common(self): self.run(sql23) self.run(sql24) self.run(sql25) - self.run(sql26, lambda res: {"class":6 if res[2] == 'minor_line' else 2, "data":[self.node_full, self.positionAsText]} ) - self.run(sql30) - self.run(sql31, lambda res: {"class":3, "data":[self.node, self.positionAsText]} ) - self.run(sql40, self.callback40) + self.run(sql26, lambda res: {"class":6 if res[3] == 'minor_line' else 2, "data":[self.node_full, self.way_power, self.positionAsText]} ) + self.run(sql30, lambda res: {"class":3, "data":[self.node, self.positionAsText]} ) + self.run(sql40, lambda res: {"class":4, "data":[self.node_full, self.way_power, self.positionAsText], "fix":[{"+": {"power": "tower"}}, {"+": {"power": "pole"}}]}) self.run(sql60, lambda res: {"class":7, "data":[self.way_full, self.any_full, self.positionAsText]} ) self.run(sql70) self.run(sql71, lambda res: {"class":8, "data":[self.node_full, self.positionAsText], "fix":self.__callback80_fix(res)} ) @@ -560,13 +717,20 @@ class Test(TestAnalyserOsmosis): def setup_class(cls): from modules import config TestAnalyserOsmosis.setup_class() - cls.analyser_conf = cls.load_osm("tests/osmosis_powerline_voltage.test.osm", - config.dir_tmp + "/tests/osmosis_powerline_voltage.test.xml") + cls.analyser_conf = cls.load_osm("tests/osmosis_powerline.test.osm", + config.dir_tmp + "/tests/osmosis_powerline.test.xml") - def test_class3(self): + def test_powergrid(self): with Analyser_Osmosis_Powerline(self.analyser_conf, self.logger) as a: a.analyser() self.root_err = self.load_errors() - self.check_err(cl="3", elems=[("node", "4")]) - self.check_num_err(1) + self.check_err(cl="1", elems=[("node", "26971")]) + self.check_err(cl="2", elems=[("node", "25874"), ("way", "1909")]) + self.check_err(cl="2", elems=[("node", "25883"), ("way", "1918")]) + self.check_err(cl="3", elems=[("node", "25950")]) + self.check_err(cl="4", elems=[("node", "26082"), ("way", "1910")]) + self.check_err(cl="6", elems=[("node", "26191"), ("way", "2088")]) + self.check_err(cl="8", elems=[("node", "25956")]) + self.check_err(cl="8", elems=[("node", "26383")]) + self.check_num_err(11) diff --git a/tests/osmosis_powerline.test.osm b/tests/osmosis_powerline.test.osm new file mode 100644 index 000000000..a229f933f --- /dev/null +++ b/tests/osmosis_powerline.test.osm @@ -0,0 +1,376 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/osmosis_powerline_voltage.test.osm b/tests/osmosis_powerline_voltage.test.osm deleted file mode 100644 index b6129bc11..000000000 --- a/tests/osmosis_powerline_voltage.test.osm +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -