|
| 1 | +# python |
| 2 | + |
| 3 | +# This script exports selected line features from the active QGIS layer |
| 4 | +# to a SQL file that can be used to insert them into the 'infrastructure_link' |
| 5 | +# table in a PostGIS database. It calculates the ellipsoidal length of each |
| 6 | +# feature using the GRS80 ellipsoid and transforms geometries to EPSG:4326 |
| 7 | +# for storage as geography(LineStringZ,4326). |
| 8 | +# |
| 9 | +# Configuration options at the top of the script allow customization of |
| 10 | +# the output file path, table name, direction, external link source, |
| 11 | +# ID field, and batch size for inserts. |
| 12 | +# |
| 13 | +# Open the script in the QGIS Python console and run it with an active layer |
| 14 | +# containing selected (with one of the selection tools) line features to |
| 15 | +# generate the SQL file. If no features are selected, the entire layer will |
| 16 | +# be exported. |
| 17 | + |
| 18 | + |
| 19 | +from qgis.core import ( |
| 20 | + QgsProject, |
| 21 | + QgsCoordinateReferenceSystem, |
| 22 | + QgsCoordinateTransform, |
| 23 | + QgsDistanceArea, |
| 24 | +) |
| 25 | +from qgis.utils import iface |
| 26 | + |
| 27 | +import uuid |
| 28 | + |
| 29 | +# Config |
| 30 | +output_path = '/tmp/tram_infraLinks.sql' |
| 31 | +table_name = 'infrastructure_network.infrastructure_link' |
| 32 | +direction = 'bidirectional' |
| 33 | +external_link_source = 'temp_hsl_tram' |
| 34 | +id_field = 'mtk_id' |
| 35 | +batch_size = 50 |
| 36 | + |
| 37 | +layer = iface.activeLayer() |
| 38 | +if layer is None: |
| 39 | + raise RuntimeError('No active layer.') |
| 40 | + |
| 41 | +proj = QgsProject.instance() |
| 42 | +src_crs = layer.crs() |
| 43 | +dst_crs = QgsCoordinateReferenceSystem('EPSG:4326') |
| 44 | +transform = QgsCoordinateTransform(src_crs, dst_crs, proj) |
| 45 | + |
| 46 | +# Ellipsoidal length calculator on GRS80 (EPSG:7019) |
| 47 | +dist = QgsDistanceArea() |
| 48 | +dist.setSourceCrs(src_crs, proj.transformContext()) |
| 49 | +ok = False |
| 50 | +try: |
| 51 | + ok = dist.setEllipsoid('EPSG:7019') |
| 52 | +except Exception: |
| 53 | + ok = False |
| 54 | +if not ok: |
| 55 | + dist.setEllipsoid('GRS80') # fallback |
| 56 | + |
| 57 | +def sql_quote(val): |
| 58 | + if val is None: |
| 59 | + return 'NULL' |
| 60 | + return "'" + str(val).replace("'", "''") + "'" |
| 61 | + |
| 62 | +# Determine feature source: selected or full layer |
| 63 | +selected_count = layer.selectedFeatureCount() |
| 64 | +if selected_count > 0: |
| 65 | + features = list(layer.selectedFeatures()) |
| 66 | + export_count = selected_count |
| 67 | +else: |
| 68 | + print("No features selected; exporting entire layer.") |
| 69 | + features = list(layer.getFeatures()) |
| 70 | + export_count = layer.featureCount() |
| 71 | + |
| 72 | +lines = [] |
| 73 | +lines.append('-- SQL export generated by QGIS Python console') |
| 74 | +lines.append(f'-- Source layer: {layer.name()}') |
| 75 | +lines.append(f'-- Exported features (source): {export_count}') |
| 76 | +lines.append('BEGIN;') |
| 77 | + |
| 78 | +# Add infralink type |
| 79 | +lines.append(""" |
| 80 | +-- Create schema and tables if they don't exist. Needed in digiroad-import repo when generating mbtiles. |
| 81 | +CREATE SCHEMA IF NOT EXISTS infrastructure_network; |
| 82 | +
|
| 83 | +CREATE TABLE IF NOT EXISTS infrastructure_network.external_source ( |
| 84 | + value text NOT NULL |
| 85 | +); |
| 86 | +
|
| 87 | +CREATE TABLE infrastructure_network.vehicle_submode_on_infrastructure_link ( |
| 88 | + infrastructure_link_id uuid NOT NULL, |
| 89 | + vehicle_submode text NOT NULL |
| 90 | +); |
| 91 | +
|
| 92 | +CREATE TABLE IF NOT EXISTS infrastructure_network.infrastructure_link ( |
| 93 | + infrastructure_link_id uuid DEFAULT public.gen_random_uuid() NOT NULL, |
| 94 | + direction text NOT NULL, |
| 95 | + shape public.geography(LineStringZ,4326) NOT NULL, |
| 96 | + estimated_length_in_metres double precision, |
| 97 | + external_link_id text NOT NULL, |
| 98 | + external_link_source text NOT NULL |
| 99 | +); |
| 100 | +
|
| 101 | +-- Add the temporary link external source type is it doesn't exist |
| 102 | +INSERT INTO infrastructure_network.external_source VALUES ('temp_hsl_tram') ON CONFLICT DO NOTHING; |
| 103 | +
|
| 104 | +""") |
| 105 | + |
| 106 | + |
| 107 | +batch_values = [] |
| 108 | + |
| 109 | +# Diagnostics |
| 110 | +written = 0 # rows added to VALUES |
| 111 | +seen = 0 # features iterated |
| 112 | +failed_transform = 0 |
| 113 | +skipped_empty = 0 |
| 114 | +skipped_non_line = 0 |
| 115 | +non_tram = 0 |
| 116 | + |
| 117 | +def flush_batch(): |
| 118 | + if not batch_values: |
| 119 | + return |
| 120 | + values_sql = ',\n '.join(batch_values) |
| 121 | + insert_sql = ( |
| 122 | + f"INSERT INTO {table_name} " |
| 123 | + "(infrastructure_link_id, direction, shape, estimated_length_in_metres, external_link_id, external_link_source)\n" |
| 124 | + f"VALUES\n {values_sql};" |
| 125 | + ) |
| 126 | + lines.append(insert_sql) |
| 127 | + batch_values.clear() |
| 128 | + |
| 129 | +field_names = layer.fields().names() |
| 130 | + |
| 131 | +for f in features: |
| 132 | + if f['kohdeluokka'] not in (14141, 14142): |
| 133 | + non_tram += 1 |
| 134 | + continue |
| 135 | + |
| 136 | + seen += 1 |
| 137 | + geom = f.geometry() |
| 138 | + if geom is None or geom.isEmpty(): |
| 139 | + skipped_empty += 1 |
| 140 | + continue |
| 141 | + |
| 142 | + # 1) Ellipsoidal length in metres |
| 143 | + try: |
| 144 | + length_m = dist.measureLength(geom) |
| 145 | + except Exception: |
| 146 | + length_m = None |
| 147 | + |
| 148 | + # 2) Try to merge multipart lines to single, if any |
| 149 | + try: |
| 150 | + merged = geom.lineMerge() |
| 151 | + if merged and not merged.isEmpty(): |
| 152 | + geom = merged |
| 153 | + except Exception: |
| 154 | + pass |
| 155 | + |
| 156 | + # 3) Transform geometry to EPSG:4326 for WKT Z output |
| 157 | + try: |
| 158 | + geom.transform(transform) |
| 159 | + except Exception: |
| 160 | + failed_transform += 1 |
| 161 | + continue |
| 162 | + |
| 163 | + wkt = geom.asWkt() # LINESTRING Z (...) expected |
| 164 | + up = wkt.upper() |
| 165 | + if not up.startswith('LINESTRING'): |
| 166 | + # Skip non-lines if target column is geography(LineStringZ,4326) |
| 167 | + skipped_non_line += 1 |
| 168 | + continue |
| 169 | + |
| 170 | + external_id = f[id_field] if id_field in field_names else None |
| 171 | + length_sql = 'NULL' if length_m is None else f"{float(length_m)}" |
| 172 | + ext_id_sql = sql_quote(external_id) |
| 173 | + |
| 174 | + infrastructure_link_id = str(uuid.uuid4()) |
| 175 | + |
| 176 | + # geography from EWKT in 4326 with Z |
| 177 | + shape_sql = ( |
| 178 | + f"CAST(ST_GeogFromText('SRID=4326;{wkt}') AS geography(LineStringZ,4326))" |
| 179 | + ) |
| 180 | + |
| 181 | + value_sql = ( |
| 182 | + f"('{infrastructure_link_id}', '{direction}', {shape_sql}, {length_sql}, {ext_id_sql}, '{external_link_source}')" |
| 183 | + ) |
| 184 | + batch_values.append(value_sql) |
| 185 | + written += 1 |
| 186 | + |
| 187 | + if len(batch_values) >= batch_size: |
| 188 | + flush_batch() |
| 189 | + |
| 190 | +flush_batch() |
| 191 | + |
| 192 | + |
| 193 | +lines.append(""" |
| 194 | +-- Select all infralink ids from 'infrastructure_link' table and insert them to the 'vehicle_submode_on_infrastructure_link' table |
| 195 | +-- along with static 'generic_tram' vehicle submode info |
| 196 | +
|
| 197 | +INSERT INTO infrastructure_network.vehicle_submode_on_infrastructure_link |
| 198 | + SELECT il.infrastructure_link_id, |
| 199 | + 'generic_tram' AS vehicle_submode |
| 200 | + FROM infrastructure_network.infrastructure_link il |
| 201 | + WHERE il.external_link_source = 'temp_hsl_tram' |
| 202 | +ON CONFLICT DO NOTHING; |
| 203 | +
|
| 204 | +
|
| 205 | +COMMIT; |
| 206 | +""") |
| 207 | + |
| 208 | +with open(output_path, 'w', encoding='utf-8') as fh: |
| 209 | + fh.write('\n'.join(lines)) |
| 210 | + |
| 211 | +print(f"Wrote SQL to {output_path} with {written} value rows from {seen} features; skipped non_tram: {non_tram}, skipped empty: {skipped_empty}, failed_transform: {failed_transform}, non-line: {skipped_non_line}.") |
0 commit comments