feat(qgis): add editable traffic signal assemblies
This commit is contained in:
@@ -7,8 +7,10 @@ const { execFileSync } = require("child_process");
|
||||
const { JsStreetNetwork } = require("osm2streets-js-node");
|
||||
const { qgisPaths } = require("./lib/tool-paths");
|
||||
const { buildCustomTurnLaneArrows } = require("./lib/turn-lane-arrows");
|
||||
const { readTrafficSignalFeatures } = require("./lib/traffic-signals");
|
||||
const {
|
||||
SCENE_LAYERS,
|
||||
AUXILIARY_EDIT_LAYERS,
|
||||
SCENE_FILE,
|
||||
SCENE_STYLE_FILE,
|
||||
layerFile,
|
||||
@@ -39,6 +41,11 @@ const clipPad = Number(config.clipPad);
|
||||
const canvasPad = Number(config.canvasPad);
|
||||
const previewPad = Number(config.previewPad);
|
||||
const layerPrefix = config.layerPrefix || "osm2streets";
|
||||
const trafficSignalLayer = AUXILIARY_EDIT_LAYERS.find((layer) => layer.id === "traffic_signal_assemblies");
|
||||
if (!trafficSignalLayer) throw new Error("Missing traffic_signal_assemblies auxiliary layer definition");
|
||||
const trafficSignalAssembliesPath = path.resolve(
|
||||
config.trafficSignalAssemblies || path.join(outDir, trafficSignalLayer.file),
|
||||
);
|
||||
|
||||
if (!Number.isFinite(arrowScale) || arrowScale <= 0) {
|
||||
throw new Error(`Invalid arrowScale: ${config.arrowScale}`);
|
||||
@@ -116,6 +123,11 @@ fs.writeFileSync(
|
||||
for (const layer of SCENE_LAYERS) {
|
||||
writeJson(path.join(outDir, layerFile(layer)), split[layer.splitKey]);
|
||||
}
|
||||
writeJson(trafficSignalAssembliesPath, readTrafficSignalFeatures(
|
||||
path.join(outDir, "vehicle_stop_lines.geojson"),
|
||||
path.join(outDir, "intersection_surface.geojson"),
|
||||
inputPath,
|
||||
));
|
||||
if (arrowMergeTriangles) {
|
||||
normalizeLaneArrows(path.join(outDir, "lane_arrows_webscale.geojson"), arrowOutlineSimplifyMeters);
|
||||
split.laneArrows = JSON.parse(fs.readFileSync(path.join(outDir, "lane_arrows_webscale.geojson"), "utf8"));
|
||||
@@ -134,6 +146,7 @@ const ogrEnv = qgis.env;
|
||||
SCENE_LAYERS.forEach((layer, index) => {
|
||||
importLayer(gpkgPath, path.join(outDir, layerFile(layer)), layer.id, index > 0, ogrEnv);
|
||||
});
|
||||
importLayer(gpkgPath, trafficSignalAssembliesPath, trafficSignalLayer.id, true, ogrEnv);
|
||||
|
||||
const qgisScript = path.join(outDir, "_create_qgis_project.py");
|
||||
const previewFeature = split.crosswalks.features[0] || split.laneArrows.features[0] || split.roadSurface.features[0];
|
||||
@@ -149,6 +162,7 @@ fs.writeFileSync(qgisScript, makeQgisScript({
|
||||
layerPrefix,
|
||||
canvasExtent: config.canvasExtent || extentString(expandBounds(bbox, canvasPad)),
|
||||
previewExtent: config.previewExtent || defaultPreviewExtent,
|
||||
trafficSignalSymbolPath: path.join(repoRoot, "assets", "qgis", "traffic-signal-direction.svg"),
|
||||
}));
|
||||
|
||||
execFileSync(qgisPython, [qgisScript], {
|
||||
@@ -1437,16 +1451,24 @@ from qgis.PyQt.QtGui import QColor, QImage, QPainter
|
||||
from qgis.core import (
|
||||
QgsApplication,
|
||||
QgsCoordinateReferenceSystem,
|
||||
QgsEditorWidgetSetup,
|
||||
QgsFillSymbol,
|
||||
QgsMarkerSymbol,
|
||||
QgsMapRendererCustomPainterJob,
|
||||
QgsMapSettings,
|
||||
QgsProject,
|
||||
QgsPalLayerSettings,
|
||||
QgsProperty,
|
||||
QgsRectangle,
|
||||
QgsSingleSymbolRenderer,
|
||||
QgsSymbolLayer,
|
||||
QgsSvgMarkerSymbolLayer,
|
||||
QgsVectorLayerSimpleLabeling,
|
||||
QgsVectorLayer,
|
||||
)
|
||||
|
||||
QGIS_PREFIX = ${JSON.stringify(options.qgisPrefix)}
|
||||
TRAFFIC_SIGNAL_SYMBOL = ${JSON.stringify(options.trafficSignalSymbolPath)}
|
||||
GPKG = ${JSON.stringify(options.gpkgPath)}
|
||||
PROJECT_PATH = ${JSON.stringify(options.projectPath)}
|
||||
PREVIEW_PATH = ${JSON.stringify(options.previewPath)}
|
||||
@@ -1475,6 +1497,38 @@ def make_layer(layer_name, title, color, outline="0,0,0,0", outline_width="0"):
|
||||
layer.setRenderer(QgsSingleSymbolRenderer(fill_symbol(color, outline, outline_width)))
|
||||
return layer
|
||||
|
||||
def make_signal_layer():
|
||||
layer = QgsVectorLayer(f"{GPKG}|layername=traffic_signal_assemblies", f"{LAYER_PREFIX} traffic signal assemblies", "ogr")
|
||||
if not layer.isValid():
|
||||
raise RuntimeError("Invalid traffic signal assemblies layer")
|
||||
symbol = QgsMarkerSymbol()
|
||||
svg_layer = QgsSvgMarkerSymbolLayer(TRAFFIC_SIGNAL_SYMBOL, 9)
|
||||
svg_layer.setDataDefinedProperty(
|
||||
QgsSymbolLayer.Property.Angle,
|
||||
QgsProperty.fromField("heading_deg"),
|
||||
)
|
||||
symbol.changeSymbolLayer(0, svg_layer)
|
||||
layer.setRenderer(QgsSingleSymbolRenderer(symbol))
|
||||
labels = QgsPalLayerSettings()
|
||||
labels.fieldName = "if(trim(display_id) = '', signal_uid, display_id)"
|
||||
labels.isExpression = True
|
||||
layer.setLabeling(QgsVectorLayerSimpleLabeling(labels))
|
||||
layer.setLabelsEnabled(True)
|
||||
for field_name in ("signal_uid", "control_id", "approach_id", "source_way_id", "stop_lon", "stop_lat"):
|
||||
index = layer.fields().indexOf(field_name)
|
||||
if index >= 0:
|
||||
layer.setFieldConstraint(index, 1)
|
||||
form = layer.editFormConfig()
|
||||
form.setReadOnly(index, True)
|
||||
layer.setEditFormConfig(form)
|
||||
enabled_index = layer.fields().indexOf("enabled")
|
||||
if enabled_index >= 0:
|
||||
layer.setEditorWidgetSetup(enabled_index, QgsEditorWidgetSetup("CheckBox", {"CheckedState": "1", "UncheckedState": "0"}))
|
||||
phase_index = layer.fields().indexOf("phase_group")
|
||||
if phase_index >= 0:
|
||||
layer.setEditorWidgetSetup(phase_index, QgsEditorWidgetSetup("ValueMap", {"map": [{"Phase 0": 0}, {"Phase 1": 1}]}))
|
||||
return layer
|
||||
|
||||
QgsApplication.setPrefixPath(QGIS_PREFIX, True)
|
||||
app = QgsApplication([], False)
|
||||
app.initQgis()
|
||||
@@ -1495,12 +1549,16 @@ layers = {
|
||||
)
|
||||
for spec in LAYER_SPECS
|
||||
}
|
||||
signal_layer = make_signal_layer()
|
||||
layers["traffic_signal_assemblies"] = signal_layer
|
||||
draw_order = [spec["id"] for spec in LAYER_SPECS]
|
||||
for key in draw_order:
|
||||
project.addMapLayer(layers[key], False)
|
||||
project.addMapLayer(signal_layer, False)
|
||||
root = project.layerTreeRoot()
|
||||
for key in draw_order:
|
||||
root.insertLayer(0, layers[key])
|
||||
root.insertLayer(0, signal_layer)
|
||||
if not project.write(PROJECT_PATH):
|
||||
raise RuntimeError(f"Failed to write {PROJECT_PATH}")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user