Compare commits
25
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
488d4bd125 | ||
|
|
02557d0fdf | ||
|
|
16d6474bb9 | ||
|
|
10615b31f4 | ||
|
|
ab51838db5 | ||
|
|
e3e858233d | ||
|
|
cc2d5c6200 | ||
|
|
b2ee167132 | ||
|
|
600a3c7aff | ||
|
|
8f9cf4c9fb | ||
|
|
18573cb3be | ||
|
|
6b0d94c54e | ||
|
|
a9f65893f0 | ||
|
|
dcaca4c883 | ||
|
|
dbd5fca40b | ||
|
|
ecb1e49d8d | ||
|
|
ad6c7ea3e1 | ||
|
|
1bb425e419 | ||
|
|
b8f36bab7a | ||
|
|
aacba1e5eb | ||
|
|
9efeff5aff | ||
|
|
dd74f1e98f | ||
|
|
7cd71ba366 | ||
|
|
927da6fe29 | ||
|
|
44972dc9e0 |
+14
@@ -1,3 +1,17 @@
|
|||||||
|
"""sn_plan41 – QGIS-Plugin für thematische Linklisten und Datenquellen.
|
||||||
|
Verwaltet Excel-basierte Link-Listen und Fachdatenabruf.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
def classFactory(iface):
|
def classFactory(iface):
|
||||||
|
"""Factory-Funktion für QGIS.
|
||||||
|
Wird von QGIS aufgerufen, um die Plugin-Instanz zu erstellen.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
iface: QGIS-Schnittstellenobjekt
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Plan41: Instanziiertes Plugin
|
||||||
|
"""
|
||||||
from .main import Plan41
|
from .main import Plan41
|
||||||
return Plan41(iface)
|
return Plan41(iface)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,597 @@
|
|||||||
|
<!DOCTYPE qgis PUBLIC 'http://mrcc.com/qgis.dtd' 'SYSTEM'>
|
||||||
|
<qgis autoRefreshMode="Disabled" autoRefreshTime="0" readOnly="0" styleCategories="AllStyleCategories" simplifyLocal="1" symbologyReferenceScale="-1" minScale="100000000" maxScale="0" labelsEnabled="1" simplifyDrawingHints="1" version="3.40.7-Bratislava" simplifyAlgorithm="0" hasScaleBasedVisibilityFlag="0" simplifyMaxScale="1" simplifyDrawingTol="1">
|
||||||
|
<flags>
|
||||||
|
<Identifiable>1</Identifiable>
|
||||||
|
<Removable>1</Removable>
|
||||||
|
<Searchable>1</Searchable>
|
||||||
|
<Private>0</Private>
|
||||||
|
</flags>
|
||||||
|
<temporal enabled="0" limitMode="0" startField="" mode="0" durationField="Anzahl" endField="" endExpression="" durationUnit="min" fixedDuration="0" accumulate="0" startExpression="">
|
||||||
|
<fixedRange>
|
||||||
|
<start></start>
|
||||||
|
<end></end>
|
||||||
|
</fixedRange>
|
||||||
|
</temporal>
|
||||||
|
<elevation zscale="1" type="IndividualFeatures" symbology="Line" respectLayerSymbol="1" extrusionEnabled="0" clamping="Terrain" showMarkerSymbolInSurfacePlots="0" extrusion="0" zoffset="0" binding="Centroid">
|
||||||
|
<data-defined-properties>
|
||||||
|
<Option type="Map">
|
||||||
|
<Option type="QString" name="name" value=""/>
|
||||||
|
<Option name="properties"/>
|
||||||
|
<Option type="QString" name="type" value="collection"/>
|
||||||
|
</Option>
|
||||||
|
</data-defined-properties>
|
||||||
|
<profileLineSymbol>
|
||||||
|
<symbol alpha="1" type="line" name="" force_rhr="0" clip_to_extent="1" is_animated="0" frame_rate="10">
|
||||||
|
<data_defined_properties>
|
||||||
|
<Option type="Map">
|
||||||
|
<Option type="QString" name="name" value=""/>
|
||||||
|
<Option name="properties"/>
|
||||||
|
<Option type="QString" name="type" value="collection"/>
|
||||||
|
</Option>
|
||||||
|
</data_defined_properties>
|
||||||
|
<layer enabled="1" id="{806bb2f8-e515-41c5-931f-aa961a636da4}" locked="0" class="SimpleLine" pass="0">
|
||||||
|
<Option type="Map">
|
||||||
|
<Option type="QString" name="align_dash_pattern" value="0"/>
|
||||||
|
<Option type="QString" name="capstyle" value="square"/>
|
||||||
|
<Option type="QString" name="customdash" value="5;2"/>
|
||||||
|
<Option type="QString" name="customdash_map_unit_scale" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option type="QString" name="customdash_unit" value="MM"/>
|
||||||
|
<Option type="QString" name="dash_pattern_offset" value="0"/>
|
||||||
|
<Option type="QString" name="dash_pattern_offset_map_unit_scale" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option type="QString" name="dash_pattern_offset_unit" value="MM"/>
|
||||||
|
<Option type="QString" name="draw_inside_polygon" value="0"/>
|
||||||
|
<Option type="QString" name="joinstyle" value="bevel"/>
|
||||||
|
<Option type="QString" name="line_color" value="231,113,72,255,rgb:0.90588235294117647,0.44313725490196076,0.28235294117647058,1"/>
|
||||||
|
<Option type="QString" name="line_style" value="solid"/>
|
||||||
|
<Option type="QString" name="line_width" value="0.6"/>
|
||||||
|
<Option type="QString" name="line_width_unit" value="MM"/>
|
||||||
|
<Option type="QString" name="offset" value="0"/>
|
||||||
|
<Option type="QString" name="offset_map_unit_scale" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option type="QString" name="offset_unit" value="MM"/>
|
||||||
|
<Option type="QString" name="ring_filter" value="0"/>
|
||||||
|
<Option type="QString" name="trim_distance_end" value="0"/>
|
||||||
|
<Option type="QString" name="trim_distance_end_map_unit_scale" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option type="QString" name="trim_distance_end_unit" value="MM"/>
|
||||||
|
<Option type="QString" name="trim_distance_start" value="0"/>
|
||||||
|
<Option type="QString" name="trim_distance_start_map_unit_scale" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option type="QString" name="trim_distance_start_unit" value="MM"/>
|
||||||
|
<Option type="QString" name="tweak_dash_pattern_on_corners" value="0"/>
|
||||||
|
<Option type="QString" name="use_custom_dash" value="0"/>
|
||||||
|
<Option type="QString" name="width_map_unit_scale" value="3x:0,0,0,0,0,0"/>
|
||||||
|
</Option>
|
||||||
|
<data_defined_properties>
|
||||||
|
<Option type="Map">
|
||||||
|
<Option type="QString" name="name" value=""/>
|
||||||
|
<Option name="properties"/>
|
||||||
|
<Option type="QString" name="type" value="collection"/>
|
||||||
|
</Option>
|
||||||
|
</data_defined_properties>
|
||||||
|
</layer>
|
||||||
|
</symbol>
|
||||||
|
</profileLineSymbol>
|
||||||
|
<profileFillSymbol>
|
||||||
|
<symbol alpha="1" type="fill" name="" force_rhr="0" clip_to_extent="1" is_animated="0" frame_rate="10">
|
||||||
|
<data_defined_properties>
|
||||||
|
<Option type="Map">
|
||||||
|
<Option type="QString" name="name" value=""/>
|
||||||
|
<Option name="properties"/>
|
||||||
|
<Option type="QString" name="type" value="collection"/>
|
||||||
|
</Option>
|
||||||
|
</data_defined_properties>
|
||||||
|
<layer enabled="1" id="{5fceef15-676f-4c5c-bb67-84e236dc3eab}" locked="0" class="SimpleFill" pass="0">
|
||||||
|
<Option type="Map">
|
||||||
|
<Option type="QString" name="border_width_map_unit_scale" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option type="QString" name="color" value="231,113,72,255,rgb:0.90588235294117647,0.44313725490196076,0.28235294117647058,1"/>
|
||||||
|
<Option type="QString" name="joinstyle" value="bevel"/>
|
||||||
|
<Option type="QString" name="offset" value="0,0"/>
|
||||||
|
<Option type="QString" name="offset_map_unit_scale" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option type="QString" name="offset_unit" value="MM"/>
|
||||||
|
<Option type="QString" name="outline_color" value="165,81,51,255,rgb:0.6470588235294118,0.31651789120317386,0.20167849240863661,1"/>
|
||||||
|
<Option type="QString" name="outline_style" value="solid"/>
|
||||||
|
<Option type="QString" name="outline_width" value="0.2"/>
|
||||||
|
<Option type="QString" name="outline_width_unit" value="MM"/>
|
||||||
|
<Option type="QString" name="style" value="solid"/>
|
||||||
|
</Option>
|
||||||
|
<data_defined_properties>
|
||||||
|
<Option type="Map">
|
||||||
|
<Option type="QString" name="name" value=""/>
|
||||||
|
<Option name="properties"/>
|
||||||
|
<Option type="QString" name="type" value="collection"/>
|
||||||
|
</Option>
|
||||||
|
</data_defined_properties>
|
||||||
|
</layer>
|
||||||
|
</symbol>
|
||||||
|
</profileFillSymbol>
|
||||||
|
<profileMarkerSymbol>
|
||||||
|
<symbol alpha="1" type="marker" name="" force_rhr="0" clip_to_extent="1" is_animated="0" frame_rate="10">
|
||||||
|
<data_defined_properties>
|
||||||
|
<Option type="Map">
|
||||||
|
<Option type="QString" name="name" value=""/>
|
||||||
|
<Option name="properties"/>
|
||||||
|
<Option type="QString" name="type" value="collection"/>
|
||||||
|
</Option>
|
||||||
|
</data_defined_properties>
|
||||||
|
<layer enabled="1" id="{6c4ff12c-7cf1-4ddc-a6e1-b7f01dda66ad}" locked="0" class="SimpleMarker" pass="0">
|
||||||
|
<Option type="Map">
|
||||||
|
<Option type="QString" name="angle" value="0"/>
|
||||||
|
<Option type="QString" name="cap_style" value="square"/>
|
||||||
|
<Option type="QString" name="color" value="231,113,72,255,rgb:0.90588235294117647,0.44313725490196076,0.28235294117647058,1"/>
|
||||||
|
<Option type="QString" name="horizontal_anchor_point" value="1"/>
|
||||||
|
<Option type="QString" name="joinstyle" value="bevel"/>
|
||||||
|
<Option type="QString" name="name" value="diamond"/>
|
||||||
|
<Option type="QString" name="offset" value="0,0"/>
|
||||||
|
<Option type="QString" name="offset_map_unit_scale" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option type="QString" name="offset_unit" value="MM"/>
|
||||||
|
<Option type="QString" name="outline_color" value="165,81,51,255,rgb:0.6470588235294118,0.31651789120317386,0.20167849240863661,1"/>
|
||||||
|
<Option type="QString" name="outline_style" value="solid"/>
|
||||||
|
<Option type="QString" name="outline_width" value="0.2"/>
|
||||||
|
<Option type="QString" name="outline_width_map_unit_scale" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option type="QString" name="outline_width_unit" value="MM"/>
|
||||||
|
<Option type="QString" name="scale_method" value="diameter"/>
|
||||||
|
<Option type="QString" name="size" value="3"/>
|
||||||
|
<Option type="QString" name="size_map_unit_scale" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option type="QString" name="size_unit" value="MM"/>
|
||||||
|
<Option type="QString" name="vertical_anchor_point" value="1"/>
|
||||||
|
</Option>
|
||||||
|
<data_defined_properties>
|
||||||
|
<Option type="Map">
|
||||||
|
<Option type="QString" name="name" value=""/>
|
||||||
|
<Option name="properties"/>
|
||||||
|
<Option type="QString" name="type" value="collection"/>
|
||||||
|
</Option>
|
||||||
|
</data_defined_properties>
|
||||||
|
</layer>
|
||||||
|
</symbol>
|
||||||
|
</profileMarkerSymbol>
|
||||||
|
</elevation>
|
||||||
|
<renderer-v2 referencescale="-1" type="singleSymbol" forceraster="0" enableorderby="0" symbollevels="0">
|
||||||
|
<symbols>
|
||||||
|
<symbol alpha="1" type="fill" name="0" force_rhr="0" clip_to_extent="1" is_animated="0" frame_rate="10">
|
||||||
|
<data_defined_properties>
|
||||||
|
<Option type="Map">
|
||||||
|
<Option type="QString" name="name" value=""/>
|
||||||
|
<Option name="properties"/>
|
||||||
|
<Option type="QString" name="type" value="collection"/>
|
||||||
|
</Option>
|
||||||
|
</data_defined_properties>
|
||||||
|
<layer enabled="1" id="{37953f01-68d9-43be-b9d8-546ca454487e}" locked="0" class="SimpleFill" pass="0">
|
||||||
|
<Option type="Map">
|
||||||
|
<Option type="QString" name="border_width_map_unit_scale" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option type="QString" name="color" value="0,85,255,255,rgb:0,0.33333333333333331,1,1"/>
|
||||||
|
<Option type="QString" name="joinstyle" value="bevel"/>
|
||||||
|
<Option type="QString" name="offset" value="0,0"/>
|
||||||
|
<Option type="QString" name="offset_map_unit_scale" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option type="QString" name="offset_unit" value="MM"/>
|
||||||
|
<Option type="QString" name="outline_color" value="0,85,255,255,rgb:0,0.33333333333333331,1,1"/>
|
||||||
|
<Option type="QString" name="outline_style" value="solid"/>
|
||||||
|
<Option type="QString" name="outline_width" value="1.06"/>
|
||||||
|
<Option type="QString" name="outline_width_unit" value="MM"/>
|
||||||
|
<Option type="QString" name="style" value="no"/>
|
||||||
|
</Option>
|
||||||
|
<data_defined_properties>
|
||||||
|
<Option type="Map">
|
||||||
|
<Option type="QString" name="name" value=""/>
|
||||||
|
<Option name="properties"/>
|
||||||
|
<Option type="QString" name="type" value="collection"/>
|
||||||
|
</Option>
|
||||||
|
</data_defined_properties>
|
||||||
|
</layer>
|
||||||
|
</symbol>
|
||||||
|
</symbols>
|
||||||
|
<rotation/>
|
||||||
|
<sizescale/>
|
||||||
|
<data-defined-properties>
|
||||||
|
<Option type="Map">
|
||||||
|
<Option type="QString" name="name" value=""/>
|
||||||
|
<Option name="properties"/>
|
||||||
|
<Option type="QString" name="type" value="collection"/>
|
||||||
|
</Option>
|
||||||
|
</data-defined-properties>
|
||||||
|
</renderer-v2>
|
||||||
|
<selection mode="Default">
|
||||||
|
<selectionColor invalid="1"/>
|
||||||
|
<selectionSymbol>
|
||||||
|
<symbol alpha="1" type="fill" name="" force_rhr="0" clip_to_extent="1" is_animated="0" frame_rate="10">
|
||||||
|
<data_defined_properties>
|
||||||
|
<Option type="Map">
|
||||||
|
<Option type="QString" name="name" value=""/>
|
||||||
|
<Option name="properties"/>
|
||||||
|
<Option type="QString" name="type" value="collection"/>
|
||||||
|
</Option>
|
||||||
|
</data_defined_properties>
|
||||||
|
<layer enabled="1" id="{37c84ab2-1f8e-4d9f-90bb-400976543063}" locked="0" class="SimpleFill" pass="0">
|
||||||
|
<Option type="Map">
|
||||||
|
<Option type="QString" name="border_width_map_unit_scale" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option type="QString" name="color" value="0,0,255,255,rgb:0,0,1,1"/>
|
||||||
|
<Option type="QString" name="joinstyle" value="bevel"/>
|
||||||
|
<Option type="QString" name="offset" value="0,0"/>
|
||||||
|
<Option type="QString" name="offset_map_unit_scale" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option type="QString" name="offset_unit" value="MM"/>
|
||||||
|
<Option type="QString" name="outline_color" value="35,35,35,255,rgb:0.13725490196078433,0.13725490196078433,0.13725490196078433,1"/>
|
||||||
|
<Option type="QString" name="outline_style" value="solid"/>
|
||||||
|
<Option type="QString" name="outline_width" value="0.26"/>
|
||||||
|
<Option type="QString" name="outline_width_unit" value="MM"/>
|
||||||
|
<Option type="QString" name="style" value="solid"/>
|
||||||
|
</Option>
|
||||||
|
<data_defined_properties>
|
||||||
|
<Option type="Map">
|
||||||
|
<Option type="QString" name="name" value=""/>
|
||||||
|
<Option name="properties"/>
|
||||||
|
<Option type="QString" name="type" value="collection"/>
|
||||||
|
</Option>
|
||||||
|
</data_defined_properties>
|
||||||
|
</layer>
|
||||||
|
</symbol>
|
||||||
|
</selectionSymbol>
|
||||||
|
</selection>
|
||||||
|
<labeling type="rule-based">
|
||||||
|
<rules key="{31adca09-e30f-437a-bc70-ef63b20670ed}">
|
||||||
|
<rule filter="left("MKZ",3)='222'" key="{79a5bb52-55b4-45ee-89fe-d90e0fe95f62}" description="Wege,geplant">
|
||||||
|
<settings calloutType="simple">
|
||||||
|
<text-style fontKerning="0" useSubstitutions="0" namedStyle="Bold" previewBkgrdColor="255,255,255,255,rgb:1,1,1,1" fontWeight="75" tabStopDistanceMapUnitScale="3x:0,0,0,0,0,0" fontUnderline="0" tabStopDistance="80" fieldName="left("MKZ",3)|| '\n' || '―'|| '\n' || right("MKZ",4)" textColor="5,125,180,255,rgb:0.0196078431372549,0.49019607843137253,0.70588235294117652,1" fontStrikeout="0" forcedBold="0" forcedItalic="0" allowHtml="0" multilineHeight="0.59999999999999998" fontSize="10" fontWordSpacing="-0.6875" stretchFactor="100" blendMode="0" fontSizeMapUnitScale="3x:0,0,0,0,0,0" textOrientation="horizontal" fontItalic="0" fontLetterSpacing="-0.5625" tabStopDistanceUnit="Point" multilineHeightUnit="Percentage" fontFamily="Arial" fontSizeUnit="Point" capitalization="0" isExpression="1" legendString="Aa" textOpacity="1">
|
||||||
|
<families/>
|
||||||
|
<text-buffer bufferColor="255,0,0,255,hsv:0,1,1,1" bufferNoFill="1" bufferSize="1" bufferSizeMapUnitScale="3x:0,0,0,0,0,0" bufferSizeUnits="MM" bufferDraw="0" bufferJoinStyle="128" bufferOpacity="1" bufferBlendMode="0"/>
|
||||||
|
<text-mask maskedSymbolLayers="" maskJoinStyle="128" maskEnabled="0" maskType="0" maskSize2="1.5" maskSizeUnits="MM" maskSizeMapUnitScale="3x:0,0,0,0,0,0" maskSize="1.5" maskOpacity="1"/>
|
||||||
|
<background shapeJoinStyle="64" shapeBlendMode="0" shapeRotationType="0" shapeRadiiX="0" shapeRadiiY="0" shapeSVGFile="" shapeSizeType="0" shapeBorderWidthMapUnitScale="3x:0,0,0,0,0,0" shapeSizeUnit="Point" shapeRadiiMapUnitScale="3x:0,0,0,0,0,0" shapeFillColor="255,255,255,255,rgb:1,1,1,1" shapeOffsetMapUnitScale="3x:0,0,0,0,0,0" shapeDraw="1" shapeRotation="0" shapeType="5" shapeSizeMapUnitScale="3x:0,0,0,0,0,0" shapeOffsetUnit="Point" shapeRadiiUnit="Point" shapeSizeX="3" shapeOpacity="1" shapeBorderWidth="0" shapeOffsetY="0" shapeBorderColor="128,128,128,255,rgb:0.50196078431372548,0.50196078431372548,0.50196078431372548,1" shapeBorderWidthUnit="Point" shapeSizeY="0" shapeOffsetX="0">
|
||||||
|
<symbol alpha="1" type="marker" name="markerSymbol" force_rhr="0" clip_to_extent="1" is_animated="0" frame_rate="10">
|
||||||
|
<data_defined_properties>
|
||||||
|
<Option type="Map">
|
||||||
|
<Option type="QString" name="name" value=""/>
|
||||||
|
<Option name="properties"/>
|
||||||
|
<Option type="QString" name="type" value="collection"/>
|
||||||
|
</Option>
|
||||||
|
</data_defined_properties>
|
||||||
|
<layer enabled="1" id="" locked="0" class="SimpleMarker" pass="0">
|
||||||
|
<Option type="Map">
|
||||||
|
<Option type="QString" name="angle" value="0"/>
|
||||||
|
<Option type="QString" name="cap_style" value="square"/>
|
||||||
|
<Option type="QString" name="color" value="255,255,255,255,rgb:1,1,1,1"/>
|
||||||
|
<Option type="QString" name="horizontal_anchor_point" value="1"/>
|
||||||
|
<Option type="QString" name="joinstyle" value="bevel"/>
|
||||||
|
<Option type="QString" name="name" value="circle"/>
|
||||||
|
<Option type="QString" name="offset" value="0,0"/>
|
||||||
|
<Option type="QString" name="offset_map_unit_scale" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option type="QString" name="offset_unit" value="MM"/>
|
||||||
|
<Option type="QString" name="outline_color" value="8,135,203,255,rgb:0.03137254901960784,0.52941176470588236,0.79607843137254897,1"/>
|
||||||
|
<Option type="QString" name="outline_style" value="solid"/>
|
||||||
|
<Option type="QString" name="outline_width" value="0.6"/>
|
||||||
|
<Option type="QString" name="outline_width_map_unit_scale" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option type="QString" name="outline_width_unit" value="MM"/>
|
||||||
|
<Option type="QString" name="scale_method" value="diameter"/>
|
||||||
|
<Option type="QString" name="size" value="2"/>
|
||||||
|
<Option type="QString" name="size_map_unit_scale" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option type="QString" name="size_unit" value="MM"/>
|
||||||
|
<Option type="QString" name="vertical_anchor_point" value="1"/>
|
||||||
|
</Option>
|
||||||
|
<data_defined_properties>
|
||||||
|
<Option type="Map">
|
||||||
|
<Option type="QString" name="name" value=""/>
|
||||||
|
<Option name="properties"/>
|
||||||
|
<Option type="QString" name="type" value="collection"/>
|
||||||
|
</Option>
|
||||||
|
</data_defined_properties>
|
||||||
|
</layer>
|
||||||
|
</symbol>
|
||||||
|
<symbol alpha="1" type="fill" name="fillSymbol" force_rhr="0" clip_to_extent="1" is_animated="0" frame_rate="10">
|
||||||
|
<data_defined_properties>
|
||||||
|
<Option type="Map">
|
||||||
|
<Option type="QString" name="name" value=""/>
|
||||||
|
<Option name="properties"/>
|
||||||
|
<Option type="QString" name="type" value="collection"/>
|
||||||
|
</Option>
|
||||||
|
</data_defined_properties>
|
||||||
|
<layer enabled="1" id="" locked="0" class="SimpleFill" pass="0">
|
||||||
|
<Option type="Map">
|
||||||
|
<Option type="QString" name="border_width_map_unit_scale" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option type="QString" name="color" value="255,255,255,255,rgb:1,1,1,1"/>
|
||||||
|
<Option type="QString" name="joinstyle" value="bevel"/>
|
||||||
|
<Option type="QString" name="offset" value="0,0"/>
|
||||||
|
<Option type="QString" name="offset_map_unit_scale" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option type="QString" name="offset_unit" value="MM"/>
|
||||||
|
<Option type="QString" name="outline_color" value="128,128,128,255,rgb:0.50196078431372548,0.50196078431372548,0.50196078431372548,1"/>
|
||||||
|
<Option type="QString" name="outline_style" value="no"/>
|
||||||
|
<Option type="QString" name="outline_width" value="0"/>
|
||||||
|
<Option type="QString" name="outline_width_unit" value="Point"/>
|
||||||
|
<Option type="QString" name="style" value="solid"/>
|
||||||
|
</Option>
|
||||||
|
<data_defined_properties>
|
||||||
|
<Option type="Map">
|
||||||
|
<Option type="QString" name="name" value=""/>
|
||||||
|
<Option name="properties"/>
|
||||||
|
<Option type="QString" name="type" value="collection"/>
|
||||||
|
</Option>
|
||||||
|
</data_defined_properties>
|
||||||
|
</layer>
|
||||||
|
</symbol>
|
||||||
|
</background>
|
||||||
|
<shadow shadowOffsetAngle="135" shadowRadiusAlphaOnly="0" shadowUnder="0" shadowOffsetMapUnitScale="3x:0,0,0,0,0,0" shadowRadius="1.5" shadowOffsetDist="1" shadowScale="100" shadowColor="0,0,0,255,rgb:0,0,0,1" shadowOffsetUnit="MM" shadowRadiusUnit="MM" shadowBlendMode="6" shadowOpacity="0.69999999999999996" shadowDraw="0" shadowRadiusMapUnitScale="3x:0,0,0,0,0,0" shadowOffsetGlobal="1"/>
|
||||||
|
<dd_properties>
|
||||||
|
<Option type="Map">
|
||||||
|
<Option type="QString" name="name" value=""/>
|
||||||
|
<Option name="properties"/>
|
||||||
|
<Option type="QString" name="type" value="collection"/>
|
||||||
|
</Option>
|
||||||
|
</dd_properties>
|
||||||
|
<substitutions/>
|
||||||
|
</text-style>
|
||||||
|
<text-format useMaxLineLengthForAutoWrap="1" formatNumbers="0" leftDirectionSymbol="<" rightDirectionSymbol=">" reverseDirectionSymbol="0" decimals="3" wrapChar="" autoWrapLength="0" placeDirectionSymbol="0" plussign="0" multilineAlign="1" addDirectionSymbol="0"/>
|
||||||
|
<placement placementFlags="14" placement="4" distUnits="MM" maximumDistance="0" fitInPolygonOnly="0" offsetUnits="MM" repeatDistance="0" geometryGeneratorEnabled="0" rotationUnit="AngleDegrees" lineAnchorTextPoint="CenterOfText" lineAnchorClipping="1" centroidWhole="0" priority="6" geometryGeneratorType="PointGeometry" prioritization="PreferCloser" repeatDistanceMapUnitScale="3x:0,0,0,0,0,0" overrunDistance="0" rotationAngle="0" quadOffset="4" dist="5" overlapHandling="PreventOverlap" offsetType="0" layerType="PolygonGeometry" repeatDistanceUnits="MM" predefinedPositionOrder="TR,TL,BR,BL,R,L,TSR,BSR" overrunDistanceMapUnitScale="3x:0,0,0,0,0,0" centroidInside="0" maximumDistanceUnit="MM" allowDegraded="0" maxCurvedCharAngleOut="-25" polygonPlacementFlags="3" labelOffsetMapUnitScale="3x:0,0,0,0,0,0" geometryGenerator="" lineAnchorType="0" lineAnchorPercent="0.5" xOffset="0" preserveRotation="0" maxCurvedCharAngleIn="25" overrunDistanceUnit="MM" yOffset="0" maximumDistanceMapUnitScale="3x:0,0,0,0,0,0" distMapUnitScale="3x:0,0,0,0,0,0"/>
|
||||||
|
<rendering obstacle="1" scaleVisibility="0" maxNumLabels="2000" drawLabels="1" labelPerPart="0" obstacleFactor="1.2" fontMinPixelSize="3" scaleMin="0" unplacedVisibility="0" zIndex="0" upsidedownLabels="0" fontLimitPixelSize="0" limitNumLabels="0" minFeatureSize="0" fontMaxPixelSize="10000" scaleMax="0" obstacleType="1" mergeLines="0"/>
|
||||||
|
<dd_properties>
|
||||||
|
<Option type="Map">
|
||||||
|
<Option type="QString" name="name" value=""/>
|
||||||
|
<Option name="properties"/>
|
||||||
|
<Option type="QString" name="type" value="collection"/>
|
||||||
|
</Option>
|
||||||
|
</dd_properties>
|
||||||
|
<callout type="simple">
|
||||||
|
<Option type="Map">
|
||||||
|
<Option type="QString" name="anchorPoint" value="pole_of_inaccessibility"/>
|
||||||
|
<Option type="int" name="blendMode" value="0"/>
|
||||||
|
<Option type="Map" name="ddProperties">
|
||||||
|
<Option type="QString" name="name" value=""/>
|
||||||
|
<Option name="properties"/>
|
||||||
|
<Option type="QString" name="type" value="collection"/>
|
||||||
|
</Option>
|
||||||
|
<Option type="bool" name="drawToAllParts" value="false"/>
|
||||||
|
<Option type="QString" name="enabled" value="0"/>
|
||||||
|
<Option type="QString" name="labelAnchorPoint" value="point_on_exterior"/>
|
||||||
|
<Option type="QString" name="lineSymbol" value="<symbol alpha="1" type="line" name="symbol" force_rhr="0" clip_to_extent="1" is_animated="0" frame_rate="10"><data_defined_properties><Option type="Map"><Option type="QString" name="name" value=""/><Option name="properties"/><Option type="QString" name="type" value="collection"/></Option></data_defined_properties><layer enabled="1" id="{03dc8a93-2fef-4767-9787-e3607853696d}" locked="0" class="SimpleLine" pass="0"><Option type="Map"><Option type="QString" name="align_dash_pattern" value="0"/><Option type="QString" name="capstyle" value="square"/><Option type="QString" name="customdash" value="5;2"/><Option type="QString" name="customdash_map_unit_scale" value="3x:0,0,0,0,0,0"/><Option type="QString" name="customdash_unit" value="MM"/><Option type="QString" name="dash_pattern_offset" value="0"/><Option type="QString" name="dash_pattern_offset_map_unit_scale" value="3x:0,0,0,0,0,0"/><Option type="QString" name="dash_pattern_offset_unit" value="MM"/><Option type="QString" name="draw_inside_polygon" value="0"/><Option type="QString" name="joinstyle" value="bevel"/><Option type="QString" name="line_color" value="60,60,60,255,rgb:0.23529411764705882,0.23529411764705882,0.23529411764705882,1"/><Option type="QString" name="line_style" value="solid"/><Option type="QString" name="line_width" value="0.3"/><Option type="QString" name="line_width_unit" value="MM"/><Option type="QString" name="offset" value="0"/><Option type="QString" name="offset_map_unit_scale" value="3x:0,0,0,0,0,0"/><Option type="QString" name="offset_unit" value="MM"/><Option type="QString" name="ring_filter" value="0"/><Option type="QString" name="trim_distance_end" value="0"/><Option type="QString" name="trim_distance_end_map_unit_scale" value="3x:0,0,0,0,0,0"/><Option type="QString" name="trim_distance_end_unit" value="MM"/><Option type="QString" name="trim_distance_start" value="0"/><Option type="QString" name="trim_distance_start_map_unit_scale" value="3x:0,0,0,0,0,0"/><Option type="QString" name="trim_distance_start_unit" value="MM"/><Option type="QString" name="tweak_dash_pattern_on_corners" value="0"/><Option type="QString" name="use_custom_dash" value="0"/><Option type="QString" name="width_map_unit_scale" value="3x:0,0,0,0,0,0"/></Option><data_defined_properties><Option type="Map"><Option type="QString" name="name" value=""/><Option name="properties"/><Option type="QString" name="type" value="collection"/></Option></data_defined_properties></layer></symbol>"/>
|
||||||
|
<Option type="double" name="minLength" value="0"/>
|
||||||
|
<Option type="QString" name="minLengthMapUnitScale" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option type="QString" name="minLengthUnit" value="MM"/>
|
||||||
|
<Option type="double" name="offsetFromAnchor" value="0"/>
|
||||||
|
<Option type="QString" name="offsetFromAnchorMapUnitScale" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option type="QString" name="offsetFromAnchorUnit" value="MM"/>
|
||||||
|
<Option type="double" name="offsetFromLabel" value="0"/>
|
||||||
|
<Option type="QString" name="offsetFromLabelMapUnitScale" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option type="QString" name="offsetFromLabelUnit" value="MM"/>
|
||||||
|
</Option>
|
||||||
|
</callout>
|
||||||
|
</settings>
|
||||||
|
</rule>
|
||||||
|
</rules>
|
||||||
|
</labeling>
|
||||||
|
<customproperties>
|
||||||
|
<Option type="Map">
|
||||||
|
<Option type="List" name="dualview/previewExpressions">
|
||||||
|
<Option type="QString" value=""NAME""/>
|
||||||
|
</Option>
|
||||||
|
<Option type="int" name="embeddedWidgets/count" value="0"/>
|
||||||
|
<Option type="QString" name="geopdf/groupName" value="Verfahren"/>
|
||||||
|
<Option name="variableNames"/>
|
||||||
|
<Option name="variableValues"/>
|
||||||
|
<Option type="QString" name="vln_karten/api_path" value="/maps/p41/27027"/>
|
||||||
|
<Option type="QString" name="vln_karten/dataset" value="p41"/>
|
||||||
|
<Option type="QString" name="vln_karten/verfahren" value="27027"/>
|
||||||
|
</Option>
|
||||||
|
</customproperties>
|
||||||
|
<blendMode>0</blendMode>
|
||||||
|
<featureBlendMode>0</featureBlendMode>
|
||||||
|
<layerOpacity>1</layerOpacity>
|
||||||
|
<geometryOptions removeDuplicateNodes="0" geometryPrecision="0">
|
||||||
|
<activeChecks/>
|
||||||
|
<checkConfiguration type="Map">
|
||||||
|
<Option type="Map" name="QgsGeometryGapCheck">
|
||||||
|
<Option type="double" name="allowedGapsBuffer" value="0"/>
|
||||||
|
<Option type="bool" name="allowedGapsEnabled" value="false"/>
|
||||||
|
<Option type="QString" name="allowedGapsLayer" value=""/>
|
||||||
|
</Option>
|
||||||
|
</checkConfiguration>
|
||||||
|
</geometryOptions>
|
||||||
|
<legend type="default-vector" showLabelLegend="0"/>
|
||||||
|
<referencedLayers/>
|
||||||
|
<fieldConfiguration>
|
||||||
|
<field name="Anzahl" configurationFlags="NoFlag">
|
||||||
|
<editWidget type="Range">
|
||||||
|
<config>
|
||||||
|
<Option/>
|
||||||
|
</config>
|
||||||
|
</editWidget>
|
||||||
|
</field>
|
||||||
|
<field name="Baumart" configurationFlags="NoFlag">
|
||||||
|
<editWidget type="TextEdit">
|
||||||
|
<config>
|
||||||
|
<Option/>
|
||||||
|
</config>
|
||||||
|
</editWidget>
|
||||||
|
</field>
|
||||||
|
<field name="Belag" configurationFlags="NoFlag">
|
||||||
|
<editWidget type="TextEdit">
|
||||||
|
<config>
|
||||||
|
<Option/>
|
||||||
|
</config>
|
||||||
|
</editWidget>
|
||||||
|
</field>
|
||||||
|
<field name="Fahrbahn" configurationFlags="NoFlag">
|
||||||
|
<editWidget type="TextEdit">
|
||||||
|
<config>
|
||||||
|
<Option/>
|
||||||
|
</config>
|
||||||
|
</editWidget>
|
||||||
|
</field>
|
||||||
|
<field name="Krone" configurationFlags="NoFlag">
|
||||||
|
<editWidget type="Range">
|
||||||
|
<config>
|
||||||
|
<Option/>
|
||||||
|
</config>
|
||||||
|
</editWidget>
|
||||||
|
</field>
|
||||||
|
<field name="MKZ" configurationFlags="NoFlag">
|
||||||
|
<editWidget type="TextEdit">
|
||||||
|
<config>
|
||||||
|
<Option/>
|
||||||
|
</config>
|
||||||
|
</editWidget>
|
||||||
|
</field>
|
||||||
|
<field name="NAME" configurationFlags="NoFlag">
|
||||||
|
<editWidget type="TextEdit">
|
||||||
|
<config>
|
||||||
|
<Option/>
|
||||||
|
</config>
|
||||||
|
</editWidget>
|
||||||
|
</field>
|
||||||
|
<field name="VKZ" configurationFlags="NoFlag">
|
||||||
|
<editWidget type="TextEdit">
|
||||||
|
<config>
|
||||||
|
<Option/>
|
||||||
|
</config>
|
||||||
|
</editWidget>
|
||||||
|
</field>
|
||||||
|
<field name="qm" configurationFlags="NoFlag">
|
||||||
|
<editWidget type="Range">
|
||||||
|
<config>
|
||||||
|
<Option/>
|
||||||
|
</config>
|
||||||
|
</editWidget>
|
||||||
|
</field>
|
||||||
|
</fieldConfiguration>
|
||||||
|
<aliases>
|
||||||
|
<alias name="" index="0" field="Anzahl"/>
|
||||||
|
<alias name="" index="1" field="Baumart"/>
|
||||||
|
<alias name="" index="2" field="Belag"/>
|
||||||
|
<alias name="" index="3" field="Fahrbahn"/>
|
||||||
|
<alias name="" index="4" field="Krone"/>
|
||||||
|
<alias name="" index="5" field="MKZ"/>
|
||||||
|
<alias name="" index="6" field="NAME"/>
|
||||||
|
<alias name="" index="7" field="VKZ"/>
|
||||||
|
<alias name="" index="8" field="qm"/>
|
||||||
|
</aliases>
|
||||||
|
<splitPolicies>
|
||||||
|
<policy policy="Duplicate" field="Anzahl"/>
|
||||||
|
<policy policy="Duplicate" field="Baumart"/>
|
||||||
|
<policy policy="Duplicate" field="Belag"/>
|
||||||
|
<policy policy="Duplicate" field="Fahrbahn"/>
|
||||||
|
<policy policy="Duplicate" field="Krone"/>
|
||||||
|
<policy policy="Duplicate" field="MKZ"/>
|
||||||
|
<policy policy="Duplicate" field="NAME"/>
|
||||||
|
<policy policy="Duplicate" field="VKZ"/>
|
||||||
|
<policy policy="Duplicate" field="qm"/>
|
||||||
|
</splitPolicies>
|
||||||
|
<duplicatePolicies>
|
||||||
|
<policy policy="Duplicate" field="Anzahl"/>
|
||||||
|
<policy policy="Duplicate" field="Baumart"/>
|
||||||
|
<policy policy="Duplicate" field="Belag"/>
|
||||||
|
<policy policy="Duplicate" field="Fahrbahn"/>
|
||||||
|
<policy policy="Duplicate" field="Krone"/>
|
||||||
|
<policy policy="Duplicate" field="MKZ"/>
|
||||||
|
<policy policy="Duplicate" field="NAME"/>
|
||||||
|
<policy policy="Duplicate" field="VKZ"/>
|
||||||
|
<policy policy="Duplicate" field="qm"/>
|
||||||
|
</duplicatePolicies>
|
||||||
|
<defaults>
|
||||||
|
<default expression="" applyOnUpdate="0" field="Anzahl"/>
|
||||||
|
<default expression="" applyOnUpdate="0" field="Baumart"/>
|
||||||
|
<default expression="" applyOnUpdate="0" field="Belag"/>
|
||||||
|
<default expression="" applyOnUpdate="0" field="Fahrbahn"/>
|
||||||
|
<default expression="" applyOnUpdate="0" field="Krone"/>
|
||||||
|
<default expression="" applyOnUpdate="0" field="MKZ"/>
|
||||||
|
<default expression="" applyOnUpdate="0" field="NAME"/>
|
||||||
|
<default expression="" applyOnUpdate="0" field="VKZ"/>
|
||||||
|
<default expression="" applyOnUpdate="0" field="qm"/>
|
||||||
|
</defaults>
|
||||||
|
<constraints>
|
||||||
|
<constraint exp_strength="0" notnull_strength="0" constraints="0" unique_strength="0" field="Anzahl"/>
|
||||||
|
<constraint exp_strength="0" notnull_strength="0" constraints="0" unique_strength="0" field="Baumart"/>
|
||||||
|
<constraint exp_strength="0" notnull_strength="0" constraints="0" unique_strength="0" field="Belag"/>
|
||||||
|
<constraint exp_strength="0" notnull_strength="0" constraints="0" unique_strength="0" field="Fahrbahn"/>
|
||||||
|
<constraint exp_strength="0" notnull_strength="0" constraints="0" unique_strength="0" field="Krone"/>
|
||||||
|
<constraint exp_strength="0" notnull_strength="0" constraints="0" unique_strength="0" field="MKZ"/>
|
||||||
|
<constraint exp_strength="0" notnull_strength="0" constraints="0" unique_strength="0" field="NAME"/>
|
||||||
|
<constraint exp_strength="0" notnull_strength="0" constraints="0" unique_strength="0" field="VKZ"/>
|
||||||
|
<constraint exp_strength="0" notnull_strength="0" constraints="0" unique_strength="0" field="qm"/>
|
||||||
|
</constraints>
|
||||||
|
<constraintExpressions>
|
||||||
|
<constraint exp="" desc="" field="Anzahl"/>
|
||||||
|
<constraint exp="" desc="" field="Baumart"/>
|
||||||
|
<constraint exp="" desc="" field="Belag"/>
|
||||||
|
<constraint exp="" desc="" field="Fahrbahn"/>
|
||||||
|
<constraint exp="" desc="" field="Krone"/>
|
||||||
|
<constraint exp="" desc="" field="MKZ"/>
|
||||||
|
<constraint exp="" desc="" field="NAME"/>
|
||||||
|
<constraint exp="" desc="" field="VKZ"/>
|
||||||
|
<constraint exp="" desc="" field="qm"/>
|
||||||
|
</constraintExpressions>
|
||||||
|
<expressionfields/>
|
||||||
|
<attributeactions>
|
||||||
|
<defaultAction value="{00000000-0000-0000-0000-000000000000}" key="Canvas"/>
|
||||||
|
</attributeactions>
|
||||||
|
<attributetableconfig sortExpression="" actionWidgetStyle="dropDown" sortOrder="0">
|
||||||
|
<columns>
|
||||||
|
<column type="field" name="Anzahl" width="-1" hidden="0"/>
|
||||||
|
<column type="field" name="Baumart" width="-1" hidden="0"/>
|
||||||
|
<column type="field" name="Belag" width="-1" hidden="0"/>
|
||||||
|
<column type="field" name="Fahrbahn" width="-1" hidden="0"/>
|
||||||
|
<column type="field" name="Krone" width="-1" hidden="0"/>
|
||||||
|
<column type="field" name="MKZ" width="-1" hidden="0"/>
|
||||||
|
<column type="field" name="NAME" width="-1" hidden="0"/>
|
||||||
|
<column type="field" name="VKZ" width="-1" hidden="0"/>
|
||||||
|
<column type="field" name="qm" width="-1" hidden="0"/>
|
||||||
|
<column type="actions" width="-1" hidden="1"/>
|
||||||
|
</columns>
|
||||||
|
</attributetableconfig>
|
||||||
|
<conditionalstyles>
|
||||||
|
<rowstyles/>
|
||||||
|
<fieldstyles/>
|
||||||
|
</conditionalstyles>
|
||||||
|
<storedexpressions/>
|
||||||
|
<editform tolerant="1"></editform>
|
||||||
|
<editforminit/>
|
||||||
|
<editforminitcodesource>0</editforminitcodesource>
|
||||||
|
<editforminitfilepath></editforminitfilepath>
|
||||||
|
<editforminitcode><![CDATA[# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
QGIS-Formulare können eine Python-Funktion haben,, die aufgerufen wird, wenn sich das Formular öffnet
|
||||||
|
|
||||||
|
Diese Funktion kann verwendet werden um dem Formular Extralogik hinzuzufügen.
|
||||||
|
|
||||||
|
Der Name der Funktion wird im Feld "Python Init-Function" angegeben
|
||||||
|
Ein Beispiel folgt:
|
||||||
|
"""
|
||||||
|
from qgis.PyQt.QtWidgets import QWidget
|
||||||
|
|
||||||
|
def my_form_open(dialog, layer, feature):
|
||||||
|
geom = feature.geometry()
|
||||||
|
control = dialog.findChild(QWidget, "MyLineEdit")
|
||||||
|
]]></editforminitcode>
|
||||||
|
<featformsuppress>0</featformsuppress>
|
||||||
|
<editorlayout>generatedlayout</editorlayout>
|
||||||
|
<editable>
|
||||||
|
<field name="Anzahl" editable="1"/>
|
||||||
|
<field name="Baumart" editable="1"/>
|
||||||
|
<field name="Belag" editable="1"/>
|
||||||
|
<field name="Fahrbahn" editable="1"/>
|
||||||
|
<field name="Krone" editable="1"/>
|
||||||
|
<field name="MKZ" editable="1"/>
|
||||||
|
<field name="NAME" editable="1"/>
|
||||||
|
<field name="VKZ" editable="1"/>
|
||||||
|
<field name="qm" editable="1"/>
|
||||||
|
</editable>
|
||||||
|
<labelOnTop>
|
||||||
|
<field name="Anzahl" labelOnTop="0"/>
|
||||||
|
<field name="Baumart" labelOnTop="0"/>
|
||||||
|
<field name="Belag" labelOnTop="0"/>
|
||||||
|
<field name="Fahrbahn" labelOnTop="0"/>
|
||||||
|
<field name="Krone" labelOnTop="0"/>
|
||||||
|
<field name="MKZ" labelOnTop="0"/>
|
||||||
|
<field name="NAME" labelOnTop="0"/>
|
||||||
|
<field name="VKZ" labelOnTop="0"/>
|
||||||
|
<field name="qm" labelOnTop="0"/>
|
||||||
|
</labelOnTop>
|
||||||
|
<reuseLastValue>
|
||||||
|
<field reuseLastValue="0" name="Anzahl"/>
|
||||||
|
<field reuseLastValue="0" name="Baumart"/>
|
||||||
|
<field reuseLastValue="0" name="Belag"/>
|
||||||
|
<field reuseLastValue="0" name="Fahrbahn"/>
|
||||||
|
<field reuseLastValue="0" name="Krone"/>
|
||||||
|
<field reuseLastValue="0" name="MKZ"/>
|
||||||
|
<field reuseLastValue="0" name="NAME"/>
|
||||||
|
<field reuseLastValue="0" name="VKZ"/>
|
||||||
|
<field reuseLastValue="0" name="qm"/>
|
||||||
|
</reuseLastValue>
|
||||||
|
<dataDefinedFieldProperties/>
|
||||||
|
<widgets/>
|
||||||
|
<previewExpression>"NAME"</previewExpression>
|
||||||
|
<mapTip enabled="1"></mapTip>
|
||||||
|
<layerGeometryType>2</layerGeometryType>
|
||||||
|
</qgis>
|
||||||
@@ -1,3 +1,17 @@
|
|||||||
|
- VLN-API zum Laden der Plan41-Maßnahmen integriert
|
||||||
|
---
|
||||||
|
Version 26.4.3-unstable:
|
||||||
|
---
|
||||||
|
Version 26.4.2-testing:
|
||||||
|
---
|
||||||
|
Version 26.04.1-unstable:
|
||||||
|
---
|
||||||
|
Version 26.3.3-unstable:
|
||||||
|
---
|
||||||
|
Version 26.3.2-unstable:
|
||||||
|
---
|
||||||
|
Version 26.3.1-testing:
|
||||||
|
- unstable als testing veröffentlicht
|
||||||
---
|
---
|
||||||
Version 26.3.1-unstable:
|
Version 26.3.1-unstable:
|
||||||
- erster Test als echtes PlugIn
|
- erster Test als echtes PlugIn
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
# sn_plan41/main.py
|
"""sn_plan41/main.py – Einstiegspunkt des sn_plan41 Plugins.
|
||||||
|
Orchestriert die UI und den Datenabruf für Thematische Linklisten.
|
||||||
|
"""
|
||||||
|
|
||||||
from qgis.utils import plugins
|
from qgis.utils import plugins
|
||||||
from sn_basis.ui.dockmanager import DockManager
|
from sn_basis.ui.dockmanager import DockManager
|
||||||
@@ -8,6 +10,9 @@ from sn_basis.modules.Pruefmanager import Pruefmanager
|
|||||||
|
|
||||||
|
|
||||||
class Plan41:
|
class Plan41:
|
||||||
|
"""Einstiegspunkt des sn_plan41-Plugins.
|
||||||
|
Verwaltet die DockWidget-Initialisierung und den Datenflusss.
|
||||||
|
"""
|
||||||
def __init__(self, iface):
|
def __init__(self, iface):
|
||||||
self.iface = iface
|
self.iface = iface
|
||||||
self.pruefmanager=Pruefmanager(ui_modus="qgis")
|
self.pruefmanager=Pruefmanager(ui_modus="qgis")
|
||||||
|
|||||||
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
[general]
|
[general]
|
||||||
version=26.3.1-unstable
|
version=26.4.3-unstable
|
||||||
name=LNO Sachsen | Plan41
|
name=LNO Sachsen | Plan41
|
||||||
description=Plugin zum Erzeugen der Pläne nach §38 und §41
|
description=Plugin zum Erzeugen der Pläne nach §38 und §41
|
||||||
author=Daniel Helbig, Michael Otto
|
author=Daniel Helbig, Michael Otto
|
||||||
homepage=https://entwicklung.flurneuordnung-sachsen.de/AG_QGIS/Plugin_SN_Plan41
|
homepage=https://entwicklung.flurneuordnung-sachsen.de/AG_QGIS/Plugin_SN_Plan41
|
||||||
tracker=https://entwicklung.flurneuordnung-sachsen.de/AG_QGIS/Plugin_SN_Plan41/issues
|
tracker=https://entwicklung.flurneuordnung-sachsen.de/AG_QGIS/Plugin_SN_Plan41/issues
|
||||||
repository=https://entwicklung.flurneuordnung-sachsen.de/AG_QGIS/Plugin_Test_Action/src/branch/unstable/
|
repository=https://entwicklung.flurneuordnung-sachsen.de/AG_QGIS/Plugin_Test_Action/src/branch/unstable/
|
||||||
|
QGISMaximumVersion=4.99
|
||||||
qgisMinimumVersion=3.40
|
qgisMinimumVersion=3.40
|
||||||
experimental=true
|
experimental=true
|
||||||
deprecated=false
|
deprecated=false
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
#sn_plan41/modules/listenauswerter.py
|
"""sn_plan41/modules/listenauswerter.py – Validierung von Linklisten.
|
||||||
|
Prüft Zeilen aus DataDict auf Vollständigkeit und Konsistenz.
|
||||||
|
"""
|
||||||
|
|
||||||
from typing import Any, Dict, List, Mapping, Optional, Tuple
|
from typing import Any, Dict, List, Mapping, Optional, Tuple
|
||||||
from collections.abc import Mapping as _Mapping
|
from collections.abc import Mapping as _Mapping
|
||||||
# Prüfer-Typen (werden als Instanzen erwartet)
|
# Prüfer-Typen (werden als Instanzen erwartet)
|
||||||
|
|||||||
@@ -0,0 +1,248 @@
|
|||||||
|
"""HTTP-Client für die VLN-Manager-API.
|
||||||
|
|
||||||
|
API-Vertrag (Stand 2026-06-08):
|
||||||
|
|
||||||
|
Basis-URL: https://api.flurneuordnung-sachsen.de/v2
|
||||||
|
|
||||||
|
POST /person/login
|
||||||
|
Body: {"mail": "...", "password": "..."}
|
||||||
|
Antwort: {"data": {"userauth": "<url_token>", "id": ...}}
|
||||||
|
Das userauth-Token ist der API-Key (= PERSON.url_token).
|
||||||
|
|
||||||
|
GET /tgen -> Teilnehmergemeinschaften (Verfahren)
|
||||||
|
|
||||||
|
GET /maps/<layer>/{vkz} -> GeoJSON FeatureCollection (EPSG:25833)
|
||||||
|
PUT /maps/<layer>/{vkz} <- GeoJSON (ersetzt den Layer dieser VKZ)
|
||||||
|
Antwort: {"data": {"vkz", "layer", "art", "speicher_id"}, "status": "ok"}
|
||||||
|
|
||||||
|
Layer: umringe, p41, st, kas, we
|
||||||
|
Auth nach Login über den Header "X-API-Key".
|
||||||
|
Fehlerformat: RFC 7807 (application/problem+json).
|
||||||
|
|
||||||
|
Es wird QgsBlockingNetworkRequest verwendet, damit Proxy- und
|
||||||
|
Zertifikatseinstellungen aus QGIS automatisch greifen.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
from sn_basis.functions.qt_wrapper import QUrl, QNetworkRequest
|
||||||
|
from sn_basis.functions.qgiscore_wrapper import QgsBlockingNetworkRequest
|
||||||
|
|
||||||
|
# Fest verdrahtete Produktiv-API des VLN Managers.
|
||||||
|
DEFAULT_BASE_URL = "https://api.flurneuordnung-sachsen.de/v2"
|
||||||
|
|
||||||
|
|
||||||
|
class ApiError(Exception):
|
||||||
|
"""Fehler bei der Kommunikation mit der Karten-API."""
|
||||||
|
|
||||||
|
|
||||||
|
class AuthError(ApiError):
|
||||||
|
"""API-Key fehlt, ist abgelaufen oder hat keine Berechtigung (401/403)."""
|
||||||
|
|
||||||
|
|
||||||
|
class KartenApiClient:
|
||||||
|
def __init__(self, base_url=DEFAULT_BASE_URL, api_key=None, mail=None):
|
||||||
|
self.base_url = base_url.rstrip("/")
|
||||||
|
self.api_key = api_key
|
||||||
|
self.mail = mail
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_authenticated(self):
|
||||||
|
return bool(self.api_key)
|
||||||
|
|
||||||
|
def login(self, mail, password):
|
||||||
|
"""Meldet den Benutzer an. Die API liefert das userauth-Token
|
||||||
|
(= API-Key), das für alle weiteren Requests verwendet wird."""
|
||||||
|
data = self._request(
|
||||||
|
"POST",
|
||||||
|
"/person/login",
|
||||||
|
payload={"mail": mail, "password": password},
|
||||||
|
with_auth=False,
|
||||||
|
)
|
||||||
|
payload = data.get("data") if isinstance(data, dict) else None
|
||||||
|
if not isinstance(payload, dict):
|
||||||
|
payload = data if isinstance(data, dict) else {}
|
||||||
|
api_key = payload.get("userauth")
|
||||||
|
if not api_key:
|
||||||
|
raise ApiError(
|
||||||
|
"Login-Antwort enthielt kein userauth-Token. "
|
||||||
|
"Erhaltene Antwort: %s" % json.dumps(data)[:300]
|
||||||
|
)
|
||||||
|
self.api_key = api_key
|
||||||
|
self.mail = mail
|
||||||
|
return api_key
|
||||||
|
|
||||||
|
def logout(self):
|
||||||
|
self.api_key = None
|
||||||
|
self.mail = None
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Verfahren (Teilnehmergemeinschaften)
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def get_verfahren(self):
|
||||||
|
"""Liste der Verfahren (TGs) als [{"vkz": ..., "name": ...}, ...],
|
||||||
|
sortiert nach VKZ. Quelle: GET /tgen."""
|
||||||
|
data = self._request("GET", "/tgen")
|
||||||
|
|
||||||
|
rows = None
|
||||||
|
if isinstance(data, list):
|
||||||
|
rows = data
|
||||||
|
elif isinstance(data, dict):
|
||||||
|
for key in ("data", "rows"):
|
||||||
|
value = data.get(key)
|
||||||
|
if isinstance(value, list):
|
||||||
|
rows = value
|
||||||
|
break
|
||||||
|
if isinstance(value, dict) and isinstance(value.get("rows"), list):
|
||||||
|
rows = value["rows"]
|
||||||
|
break
|
||||||
|
if rows is None:
|
||||||
|
raise ApiError(
|
||||||
|
"Antwort von /tgen hat ein unerwartetes Format: %s"
|
||||||
|
% json.dumps(data)[:200]
|
||||||
|
)
|
||||||
|
|
||||||
|
verfahren = []
|
||||||
|
for row in rows:
|
||||||
|
if not isinstance(row, dict):
|
||||||
|
continue
|
||||||
|
vkz = row.get("vkz") or row.get("VKZ")
|
||||||
|
name = (
|
||||||
|
row.get("kurzname")
|
||||||
|
or row.get("Kurzname")
|
||||||
|
or row.get("name")
|
||||||
|
or row.get("Name")
|
||||||
|
or ""
|
||||||
|
)
|
||||||
|
if vkz:
|
||||||
|
verfahren.append({"vkz": str(vkz), "name": str(name)})
|
||||||
|
verfahren.sort(key=lambda v: v["vkz"])
|
||||||
|
return verfahren
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Karten-Layer
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def load_layer_complete(self, layer_key, vkz, page_size=2000):
|
||||||
|
"""Lädt ALLE Objekte eines Layers für eine VKZ aus KARTE_OBJEKT.
|
||||||
|
|
||||||
|
Nutzt den Listen-Endpunkt /maps/<layer>?vkz=… mit limit/offset-Paging,
|
||||||
|
bis keine weitere Seite mehr kommt."""
|
||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
|
features = []
|
||||||
|
offset = 0
|
||||||
|
while True:
|
||||||
|
data = self._request(
|
||||||
|
"GET",
|
||||||
|
"/maps/%s?vkz=%s&limit=%d&offset=%d"
|
||||||
|
% (quote(str(layer_key)), quote(str(vkz)), page_size, offset),
|
||||||
|
)
|
||||||
|
if not isinstance(data, dict) or data.get("type") != "FeatureCollection":
|
||||||
|
raise ApiError(
|
||||||
|
"Antwort von /maps/%s ist keine GeoJSON FeatureCollection."
|
||||||
|
% layer_key
|
||||||
|
)
|
||||||
|
page = data.get("features", [])
|
||||||
|
features.extend(page)
|
||||||
|
if len(page) < page_size:
|
||||||
|
break
|
||||||
|
offset += page_size
|
||||||
|
return {"type": "FeatureCollection", "features": features}
|
||||||
|
|
||||||
|
def load_feature_collection(self, path):
|
||||||
|
"""Lädt einen Layer als GeoJSON FeatureCollection (dict)."""
|
||||||
|
data = self._request("GET", path)
|
||||||
|
if isinstance(data, dict):
|
||||||
|
if data.get("type") == "FeatureCollection":
|
||||||
|
return data
|
||||||
|
inner = data.get("data")
|
||||||
|
if isinstance(inner, dict) and inner.get("type") == "FeatureCollection":
|
||||||
|
return inner
|
||||||
|
raise ApiError(
|
||||||
|
"Antwort von %s ist keine GeoJSON FeatureCollection." % path
|
||||||
|
)
|
||||||
|
|
||||||
|
def save_feature_collection(self, path, feature_collection):
|
||||||
|
"""Schreibt eine GeoJSON FeatureCollection zurück an die API
|
||||||
|
(PUT ersetzt den kompletten Layer der jeweiligen VKZ)."""
|
||||||
|
return self._request("PUT", path, payload=feature_collection)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Intern
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _request(self, method, path, payload=None, with_auth=True):
|
||||||
|
if with_auth and not self.is_authenticated:
|
||||||
|
raise AuthError("Nicht angemeldet — bitte zuerst einloggen.")
|
||||||
|
|
||||||
|
request = QNetworkRequest(QUrl(self.base_url + path))
|
||||||
|
request.setHeader(
|
||||||
|
QNetworkRequest.KnownHeaders.ContentTypeHeader, "application/json"
|
||||||
|
)
|
||||||
|
request.setRawHeader(b"Accept", b"application/json")
|
||||||
|
if with_auth:
|
||||||
|
request.setRawHeader(b"X-API-Key", self.api_key.encode("utf-8"))
|
||||||
|
|
||||||
|
body = b""
|
||||||
|
if payload is not None:
|
||||||
|
body = json.dumps(payload).encode("utf-8")
|
||||||
|
|
||||||
|
blocking = QgsBlockingNetworkRequest()
|
||||||
|
if method == "GET":
|
||||||
|
error = blocking.get(request)
|
||||||
|
elif method == "POST":
|
||||||
|
error = blocking.post(request, body)
|
||||||
|
elif method == "PUT":
|
||||||
|
error = blocking.put(request, body)
|
||||||
|
else:
|
||||||
|
raise ApiError("Nicht unterstützte HTTP-Methode: %s" % method)
|
||||||
|
|
||||||
|
reply = blocking.reply()
|
||||||
|
content = bytes(reply.content())
|
||||||
|
status = reply.attribute(
|
||||||
|
QNetworkRequest.Attribute.HttpStatusCodeAttribute
|
||||||
|
)
|
||||||
|
status = int(status) if status is not None else None
|
||||||
|
|
||||||
|
if status in (401, 403):
|
||||||
|
raise AuthError(
|
||||||
|
"Keine Berechtigung für %s %s (HTTP %s)%s"
|
||||||
|
% (method, path, status, self._error_detail(content))
|
||||||
|
)
|
||||||
|
if error != QgsBlockingNetworkRequest.NoError:
|
||||||
|
raise ApiError(
|
||||||
|
"%s %s fehlgeschlagen: %s%s"
|
||||||
|
% (method, path, blocking.errorMessage(), self._error_detail(content))
|
||||||
|
)
|
||||||
|
if status is not None and status >= 400:
|
||||||
|
raise ApiError(
|
||||||
|
"%s %s lieferte HTTP %s%s"
|
||||||
|
% (method, path, status, self._error_detail(content))
|
||||||
|
)
|
||||||
|
|
||||||
|
if not content:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return json.loads(content.decode("utf-8"))
|
||||||
|
except ValueError:
|
||||||
|
raise ApiError(
|
||||||
|
"Antwort von %s ist kein gültiges JSON." % path
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _error_detail(content):
|
||||||
|
"""Liest title/detail aus einer RFC-7807-Fehlerantwort."""
|
||||||
|
if not content:
|
||||||
|
return ""
|
||||||
|
try:
|
||||||
|
problem = json.loads(content.decode("utf-8"))
|
||||||
|
parts = [
|
||||||
|
str(problem[k]) for k in ("title", "detail") if problem.get(k)
|
||||||
|
]
|
||||||
|
if parts:
|
||||||
|
return " — " + ": ".join(parts)
|
||||||
|
except (ValueError, AttributeError):
|
||||||
|
pass
|
||||||
|
return " — " + content[:300].decode("utf-8", "replace")
|
||||||
@@ -0,0 +1,378 @@
|
|||||||
|
"""
|
||||||
|
sn_plan41/modules/vln_api_logic.py – Fachlogik für die VLN-API-Integration in Tab A.
|
||||||
|
|
||||||
|
Kapselt:
|
||||||
|
- Anmeldung / Abmeldung (KartenApiClient aus vln_karten)
|
||||||
|
- Persistierung des API-Keys in QSettings
|
||||||
|
- Verfahrensliste laden (GET /tgen)
|
||||||
|
- Persistierung der VKZ-Auswahl in der Projektdatei
|
||||||
|
- Plan 41 laden (2 Layer + Stile aus sn_plan41/assets/)
|
||||||
|
- Aktiven Layer hochladen
|
||||||
|
|
||||||
|
Voraussetzung: Das Plugin `vln_karten` muss installiert sein.
|
||||||
|
Wenn nicht vorhanden, liefert :attr:`is_available` False; die UI
|
||||||
|
kann dann einen entsprechenden Hinweis anzeigen.
|
||||||
|
|
||||||
|
Sitzungsablauf bei AuthError:
|
||||||
|
- :meth:`handle_session_expired` aufrufen → API-Key löschen + logout
|
||||||
|
- In der UI danach :meth:`_update_vln_ui_state` aufrufen und
|
||||||
|
QMessageBox.warning anzeigen. Kein automatischer Login-Dialog.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from sn_basis.functions.qt_wrapper import QSettings
|
||||||
|
from sn_basis.functions.qgiscore_wrapper import QgsProject
|
||||||
|
from sn_basis.functions.sys_wrapper import get_plugin_root, join_path
|
||||||
|
from sn_basis.functions.ly_style_wrapper import apply_style_from_path
|
||||||
|
|
||||||
|
# Lokale API-Module — fallenübrig, sobald QGIS verfügbar ist.
|
||||||
|
# In der reinen Python-Testumgebung (kein qgis-Paket) schlagen die
|
||||||
|
# qgis-Imports in den Untermodulen fehl; daher der try/except-Guard.
|
||||||
|
try:
|
||||||
|
from sn_plan41.modules.vln_api_client import KartenApiClient, ApiError, AuthError
|
||||||
|
from sn_plan41.modules.vln_layer_manager import (
|
||||||
|
feature_collection_to_layers,
|
||||||
|
plugin_layers,
|
||||||
|
layers_to_feature_collection,
|
||||||
|
layer_api_path,
|
||||||
|
PROP_DATASET,
|
||||||
|
PROP_VERFAHREN,
|
||||||
|
)
|
||||||
|
VLN_KARTEN_AVAILABLE = True
|
||||||
|
except (ImportError, ModuleNotFoundError):
|
||||||
|
KartenApiClient = None # type: ignore[assignment,misc]
|
||||||
|
ApiError = Exception # type: ignore[assignment,misc]
|
||||||
|
AuthError = Exception # type: ignore[assignment,misc]
|
||||||
|
VLN_KARTEN_AVAILABLE = False
|
||||||
|
|
||||||
|
# Geometrietyp-Konstanten (QGIS intern: 1 = Linie, 2 = Fläche)
|
||||||
|
try:
|
||||||
|
from qgis.core import Qgis as _Qgis
|
||||||
|
_GEOM_LINE = _Qgis.GeometryType.Line
|
||||||
|
_GEOM_POLYGON = _Qgis.GeometryType.Polygon
|
||||||
|
except (ImportError, AttributeError):
|
||||||
|
try:
|
||||||
|
from qgis.core import QgsWkbTypes as _QgsWkbTypes
|
||||||
|
_GEOM_LINE = _QgsWkbTypes.LineGeometry
|
||||||
|
_GEOM_POLYGON = _QgsWkbTypes.PolygonGeometry
|
||||||
|
except (ImportError, AttributeError):
|
||||||
|
_GEOM_LINE = 1
|
||||||
|
_GEOM_POLYGON = 2
|
||||||
|
|
||||||
|
# Konstanten
|
||||||
|
SETTINGS_GROUP = "vln_karten"
|
||||||
|
PROJECT_SCOPE = "vln_karten"
|
||||||
|
PROJECT_KEY_VKZ = "/vkz"
|
||||||
|
|
||||||
|
P41_DATASET_KEY = "p41"
|
||||||
|
P41_LABEL = "Plan 41 (Wege- und Gewässerplan)"
|
||||||
|
P41_PATH_TEMPLATE = "/maps/p41/{vkz}"
|
||||||
|
P41_GEOMETRY = "MultiPolygon"
|
||||||
|
|
||||||
|
STYLE_FLAECHE = "QGIS_P41_API_Layer_flaeche.qml"
|
||||||
|
STYLE_LINIE = "QGIS_P41_API_Layer_Linie.qml"
|
||||||
|
|
||||||
|
|
||||||
|
class VlnApiLogic:
|
||||||
|
"""
|
||||||
|
Kapselt die VLN-API-Fachlogik für den Plan41-Tab.
|
||||||
|
|
||||||
|
Alle Methoden, die Netzwerkkommunikation erfordern, können
|
||||||
|
``ApiError`` oder ``AuthError`` werfen – die UI ist für die
|
||||||
|
Fehlerdarstellung zuständig.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, pruefmanager=None) -> None:
|
||||||
|
self.pruefmanager = pruefmanager
|
||||||
|
self._client: Optional[object] = None # KartenApiClient | None
|
||||||
|
|
||||||
|
# API-Client aus gespeicherten Credentials initialisieren
|
||||||
|
if VLN_KARTEN_AVAILABLE:
|
||||||
|
api_key, mail = self.load_stored_credentials()
|
||||||
|
if api_key:
|
||||||
|
self._client = KartenApiClient(api_key=api_key, mail=mail)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Verfügbarkeit
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def is_available() -> bool:
|
||||||
|
"""True, wenn die lokalen API-Module geladen werden konnten
|
||||||
|
(erfordert eine QGIS-Laufzeitumgebung)."""
|
||||||
|
return VLN_KARTEN_AVAILABLE
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Authentifizierungsstatus
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_authenticated(self) -> bool:
|
||||||
|
"""True, wenn ein gültiger API-Key vorhanden ist."""
|
||||||
|
if self._client is None:
|
||||||
|
return False
|
||||||
|
return bool(getattr(self._client, "is_authenticated", False))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def mail(self) -> Optional[str]:
|
||||||
|
"""E-Mail-Adresse des angemeldeten Benutzers oder None."""
|
||||||
|
if self._client is None:
|
||||||
|
return None
|
||||||
|
return getattr(self._client, "mail", None)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# QSettings: API-Key persistieren
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def load_stored_credentials(self) -> tuple:
|
||||||
|
"""Liest API-Key und Mail aus QSettings.
|
||||||
|
Gibt ``(api_key, mail)`` zurück – leere Strings wenn nicht vorhanden.
|
||||||
|
"""
|
||||||
|
settings = QSettings()
|
||||||
|
settings.beginGroup(SETTINGS_GROUP)
|
||||||
|
api_key = settings.value("api_key", "")
|
||||||
|
mail = settings.value("mail", "")
|
||||||
|
settings.endGroup()
|
||||||
|
return api_key or None, mail or None
|
||||||
|
|
||||||
|
def save_api_key(self, api_key: str, mail: str) -> None:
|
||||||
|
"""Speichert API-Key und Mail in QSettings."""
|
||||||
|
settings = QSettings()
|
||||||
|
settings.beginGroup(SETTINGS_GROUP)
|
||||||
|
settings.setValue("api_key", api_key)
|
||||||
|
settings.setValue("mail", mail or "")
|
||||||
|
settings.endGroup()
|
||||||
|
|
||||||
|
def clear_api_key(self) -> None:
|
||||||
|
"""Entfernt API-Key aus QSettings."""
|
||||||
|
settings = QSettings()
|
||||||
|
settings.beginGroup(SETTINGS_GROUP)
|
||||||
|
settings.remove("api_key")
|
||||||
|
settings.endGroup()
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Login / Logout
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def login(self, mail: str, password: str) -> None:
|
||||||
|
"""
|
||||||
|
Meldet den Benutzer an und speichert den API-Key.
|
||||||
|
|
||||||
|
:raises ApiError: Bei ungültigen Zugangsdaten oder Netzwerkproblem.
|
||||||
|
:raises RuntimeError: Wenn QGIS nicht verfügbar ist (kein qgis-Paket).
|
||||||
|
"""
|
||||||
|
if not VLN_KARTEN_AVAILABLE:
|
||||||
|
raise RuntimeError(
|
||||||
|
"VLN-API-Module nicht verfügbar (QGIS-Laufzeitumgebung benötigt)."
|
||||||
|
)
|
||||||
|
client = KartenApiClient()
|
||||||
|
client.login(mail, password) # wirft ApiError bei Fehler
|
||||||
|
self._client = client
|
||||||
|
self.save_api_key(client.api_key, mail)
|
||||||
|
|
||||||
|
def logout(self) -> None:
|
||||||
|
"""Meldet den Benutzer ab (lokal, kein API-Aufruf)."""
|
||||||
|
if self._client is not None:
|
||||||
|
getattr(self._client, "logout", lambda: None)()
|
||||||
|
self._client = None
|
||||||
|
|
||||||
|
def handle_session_expired(self) -> None:
|
||||||
|
"""
|
||||||
|
Behandelt einen abgelaufenen API-Key:
|
||||||
|
entfernt ihn aus QSettings und setzt den Client zurück.
|
||||||
|
"""
|
||||||
|
self.clear_api_key()
|
||||||
|
self.logout()
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Verfahrensliste
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def get_verfahren(self) -> list:
|
||||||
|
"""
|
||||||
|
Lädt alle Verfahren (TGs) vom Server.
|
||||||
|
|
||||||
|
:returns: Sortierte Liste von ``{"vkz": ..., "name": ...}``-Dicts.
|
||||||
|
:raises AuthError: Wenn nicht angemeldet oder Session abgelaufen.
|
||||||
|
:raises ApiError: Bei sonstigem Netzwerkfehler.
|
||||||
|
"""
|
||||||
|
if self._client is None:
|
||||||
|
raise AuthError("Nicht angemeldet.")
|
||||||
|
return self._client.get_verfahren()
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# VKZ-Persistenz in Projektdatei
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def load_stored_vkz(self) -> Optional[str]:
|
||||||
|
"""Liest die zuletzt gewählte VKZ aus der Projektdatei."""
|
||||||
|
try:
|
||||||
|
vkz, _ok = QgsProject.instance().readEntry(
|
||||||
|
PROJECT_SCOPE, PROJECT_KEY_VKZ, ""
|
||||||
|
)
|
||||||
|
return vkz if vkz else None
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def save_vkz(self, vkz: Optional[str]) -> None:
|
||||||
|
"""Schreibt die gewählte VKZ in die Projektdatei."""
|
||||||
|
if not vkz:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
QgsProject.instance().writeEntry(PROJECT_SCOPE, PROJECT_KEY_VKZ, vkz)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Plan 41 laden
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def get_p41_layers(self, vkz: str) -> list:
|
||||||
|
"""Gibt alle bereits geladenen P41-Layer für diese VKZ zurück."""
|
||||||
|
return plugin_layers(P41_DATASET_KEY, vkz)
|
||||||
|
|
||||||
|
def remove_p41_layers(self, vkz: str) -> None:
|
||||||
|
"""Entfernt alle P41-Layer dieser VKZ aus dem Projekt."""
|
||||||
|
existing = plugin_layers(P41_DATASET_KEY, vkz)
|
||||||
|
if existing:
|
||||||
|
QgsProject.instance().removeMapLayers([l.id() for l in existing])
|
||||||
|
|
||||||
|
def load_p41(self, vkz: str) -> list:
|
||||||
|
"""
|
||||||
|
Lädt den Plan-41-Datensatz für eine VKZ vom Server und legt
|
||||||
|
Memory-Layer im Projekt an. Auf die entstandenen Layer werden
|
||||||
|
die passenden QML-Stile aus ``sn_plan41/assets/`` angewendet.
|
||||||
|
|
||||||
|
:returns: Liste der erzeugten ``QgsVectorLayer``.
|
||||||
|
:raises AuthError: Wenn nicht angemeldet oder Session abgelaufen.
|
||||||
|
:raises ApiError: Bei sonstigem Netzwerkfehler.
|
||||||
|
:raises RuntimeError: Wenn QGIS nicht verfügbar ist.
|
||||||
|
"""
|
||||||
|
if not VLN_KARTEN_AVAILABLE:
|
||||||
|
raise RuntimeError(
|
||||||
|
"VLN-API-Module nicht verfügbar (QGIS-Laufzeitumgebung benötigt)."
|
||||||
|
)
|
||||||
|
if self._client is None:
|
||||||
|
raise AuthError("Nicht angemeldet.")
|
||||||
|
|
||||||
|
path = P41_PATH_TEMPLATE.format(vkz=vkz)
|
||||||
|
feature_collection = self._client.load_layer_complete(
|
||||||
|
P41_DATASET_KEY, vkz
|
||||||
|
)
|
||||||
|
base_name = "%s %s" % (P41_LABEL, vkz)
|
||||||
|
layers = feature_collection_to_layers(
|
||||||
|
feature_collection,
|
||||||
|
base_name,
|
||||||
|
P41_GEOMETRY,
|
||||||
|
P41_DATASET_KEY,
|
||||||
|
vkz,
|
||||||
|
path,
|
||||||
|
)
|
||||||
|
self._apply_p41_styles(layers)
|
||||||
|
return layers
|
||||||
|
|
||||||
|
def _apply_p41_styles(self, layers: list) -> None:
|
||||||
|
"""Wendet den passenden QML-Stil auf jeden Layer an."""
|
||||||
|
assets_dir = join_path(get_plugin_root(), "sn_plan41", "assets")
|
||||||
|
for layer in layers:
|
||||||
|
try:
|
||||||
|
gtype = layer.geometryType()
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if gtype == _GEOM_POLYGON:
|
||||||
|
style_file = STYLE_FLAECHE
|
||||||
|
elif gtype == _GEOM_LINE:
|
||||||
|
style_file = STYLE_LINIE
|
||||||
|
else:
|
||||||
|
continue # Punkt- und None-Layer erhalten keinen Stil
|
||||||
|
|
||||||
|
style_path = join_path(assets_dir, style_file)
|
||||||
|
apply_style_from_path(layer, style_path)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Upload
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def upload_active_layer(self, active_layer) -> tuple:
|
||||||
|
"""
|
||||||
|
Lädt alle Teil-Layer des aktiven Layers zum Server hoch.
|
||||||
|
|
||||||
|
:param active_layer: Aktiver ``QgsVectorLayer`` (aus ``iface.activeLayer()``).
|
||||||
|
:returns: ``(success: bool, message: str)``
|
||||||
|
:raises AuthError: Wenn Session abgelaufen.
|
||||||
|
:raises ApiError: Bei Netzwerkfehler.
|
||||||
|
"""
|
||||||
|
if not VLN_KARTEN_AVAILABLE:
|
||||||
|
return False, (
|
||||||
|
"VLN-API-Module nicht verfügbar (QGIS-Laufzeitumgebung benötigt)."
|
||||||
|
)
|
||||||
|
|
||||||
|
if self._client is None:
|
||||||
|
raise AuthError("Nicht angemeldet.")
|
||||||
|
|
||||||
|
path = layer_api_path(active_layer) if active_layer else None
|
||||||
|
if not path:
|
||||||
|
return (
|
||||||
|
False,
|
||||||
|
"Der aktive Layer wurde nicht über 'vln_karten' geladen. "
|
||||||
|
"Bitte einen Plan41-Layer aus dem Projekt auswählen.",
|
||||||
|
)
|
||||||
|
|
||||||
|
dataset_key = active_layer.customProperty(PROP_DATASET)
|
||||||
|
vkz = active_layer.customProperty(PROP_VERFAHREN)
|
||||||
|
siblings = plugin_layers(dataset_key, vkz)
|
||||||
|
|
||||||
|
for layer in siblings:
|
||||||
|
if layer.isEditable() and not layer.commitChanges():
|
||||||
|
return (
|
||||||
|
False,
|
||||||
|
"Die Bearbeitungssitzung von '%s' konnte nicht "
|
||||||
|
"gespeichert werden." % layer.name(),
|
||||||
|
)
|
||||||
|
|
||||||
|
feature_collection = layers_to_feature_collection(siblings)
|
||||||
|
self._client.save_feature_collection(path, feature_collection)
|
||||||
|
|
||||||
|
total = sum(layer.featureCount() for layer in siblings)
|
||||||
|
return (
|
||||||
|
True,
|
||||||
|
"%d Objekte aus %d Layer(n) erfolgreich hochgeladen."
|
||||||
|
% (total, len(siblings)),
|
||||||
|
)
|
||||||
|
|
||||||
|
def upload_summary(self, active_layer) -> Optional[dict]:
|
||||||
|
"""
|
||||||
|
Gibt eine Zusammenfassung der zu hochladenden Layer zurück
|
||||||
|
(für den Bestätigungs-Dialog in der UI), ohne den Upload
|
||||||
|
tatsächlich auszuführen.
|
||||||
|
|
||||||
|
:returns: Dict mit ``dataset_label``, ``vkz``, ``siblings``
|
||||||
|
(Layer-Liste), ``total`` (Objektanzahl), ``listing`` (str)
|
||||||
|
oder ``None`` wenn kein gültiger Layer.
|
||||||
|
"""
|
||||||
|
if not VLN_KARTEN_AVAILABLE or active_layer is None:
|
||||||
|
return None
|
||||||
|
path = layer_api_path(active_layer)
|
||||||
|
if not path:
|
||||||
|
return None
|
||||||
|
|
||||||
|
dataset_key = active_layer.customProperty(PROP_DATASET)
|
||||||
|
vkz = active_layer.customProperty(PROP_VERFAHREN)
|
||||||
|
siblings = plugin_layers(dataset_key, vkz)
|
||||||
|
|
||||||
|
total = sum(l.featureCount() for l in siblings)
|
||||||
|
listing = "\n".join(
|
||||||
|
"• %s (%d Objekte)" % (l.name(), l.featureCount())
|
||||||
|
for l in siblings
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"dataset_label": P41_LABEL if dataset_key == P41_DATASET_KEY else dataset_key,
|
||||||
|
"vkz": vkz,
|
||||||
|
"siblings": siblings,
|
||||||
|
"total": total,
|
||||||
|
"listing": listing,
|
||||||
|
}
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
"""Konvertierung GeoJSON <-> QGIS-Layer für die VLN-API.
|
||||||
|
|
||||||
|
Lokale Kopie des Layer-Managers aus dem vln_karten-Plugin — sn_plan41
|
||||||
|
benötigt kein separat installiertes vln_karten-Plugin mehr.
|
||||||
|
|
||||||
|
Die API liefert je Layer (z.B. Plan 41) gemischte Geometrietypen in einer
|
||||||
|
FeatureCollection: Punkte, Linien, Polygone sowie deren Multi-Varianten.
|
||||||
|
QGIS-Memory-Layer können nur einen Geometrietyp halten — beim Laden wird
|
||||||
|
deshalb nach Geometrie-Familie (Punkte/Linien/Flächen) in bis zu drei
|
||||||
|
Layer gesplittet; Single-Geometrien werden zu Multi befördert.
|
||||||
|
|
||||||
|
Beim Hochladen müssen alle Teil-Layer desselben Datensatzes wieder zu
|
||||||
|
EINER FeatureCollection vereint werden, weil der PUT der API den
|
||||||
|
kompletten Layer-Bestand der VKZ ersetzt.
|
||||||
|
|
||||||
|
Custom-Properties an Layern (kompatibel mit vln_karten, falls beide
|
||||||
|
Plugins gleichzeitig installiert sind):
|
||||||
|
vln_karten/dataset — z.B. "p41"
|
||||||
|
vln_karten/verfahren — VKZ-Nummer
|
||||||
|
vln_karten/api_path — API-Pfad für Upload
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
from sn_basis.functions.qgiscore_wrapper import (
|
||||||
|
QgsJsonExporter,
|
||||||
|
QgsJsonUtils,
|
||||||
|
QgsProject,
|
||||||
|
QgsVectorLayer,
|
||||||
|
GEOM_POINT,
|
||||||
|
GEOM_LINE,
|
||||||
|
GEOM_POLYGON,
|
||||||
|
)
|
||||||
|
|
||||||
|
_GEOM_POINT = GEOM_POINT
|
||||||
|
_GEOM_LINE = GEOM_LINE
|
||||||
|
_GEOM_POLYGON = GEOM_POLYGON
|
||||||
|
|
||||||
|
# Die VLN-API liefert Koordinaten in ETRS89 / UTM Zone 33 (EPSG:25833).
|
||||||
|
GEOJSON_CRS = "EPSG:25833"
|
||||||
|
|
||||||
|
# Custom-Property-Schlüssel — identisch mit vln_karten für Kompatibilität.
|
||||||
|
PROP_DATASET = "vln_karten/dataset"
|
||||||
|
PROP_VERFAHREN = "vln_karten/verfahren"
|
||||||
|
PROP_PATH = "vln_karten/api_path"
|
||||||
|
|
||||||
|
# Geometrie-Familie → (Memory-Provider-Typ, Namenszusatz).
|
||||||
|
# Reihenfolge = Lade-Reihenfolge: Flächen zuerst, damit Punkte im
|
||||||
|
# Layerbaum oben landen und nicht verdeckt werden.
|
||||||
|
_FAMILIES = (
|
||||||
|
("polygon", "MultiPolygon", "Flächen"),
|
||||||
|
("line", "MultiLineString", "Linien"),
|
||||||
|
("point", "MultiPoint", "Punkte"),
|
||||||
|
("none", "None", "ohne Geometrie"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _family(feature):
|
||||||
|
"""Geometrie-Familie eines Features. Unbekannte Typen (z.B.
|
||||||
|
GeometryCollection) landen bei 'none'."""
|
||||||
|
if not feature.hasGeometry() or feature.geometry().isNull():
|
||||||
|
return "none"
|
||||||
|
gtype = feature.geometry().type()
|
||||||
|
if gtype == _GEOM_POINT:
|
||||||
|
return "point"
|
||||||
|
if gtype == _GEOM_LINE:
|
||||||
|
return "line"
|
||||||
|
if gtype == _GEOM_POLYGON:
|
||||||
|
return "polygon"
|
||||||
|
return "none"
|
||||||
|
|
||||||
|
|
||||||
|
def memory_layer_from_features(
|
||||||
|
uri_type, name, fields, features, dataset_key, verfahren_nr, api_path, promote=True
|
||||||
|
):
|
||||||
|
"""Baut einen Memory-Layer aus fertigen QgsFeatures, setzt die
|
||||||
|
Plugin-Custom-Properties und hängt ihn ins Projekt."""
|
||||||
|
uri = uri_type if uri_type == "None" else "%s?crs=%s" % (uri_type, GEOJSON_CRS)
|
||||||
|
layer = QgsVectorLayer(uri, name, "memory")
|
||||||
|
if not layer.isValid():
|
||||||
|
raise RuntimeError("Memory-Layer '%s' konnte nicht erzeugt werden." % name)
|
||||||
|
provider = layer.dataProvider()
|
||||||
|
provider.addAttributes(fields.toList())
|
||||||
|
layer.updateFields()
|
||||||
|
prepared = []
|
||||||
|
for feature in features:
|
||||||
|
geometry = feature.geometry()
|
||||||
|
if promote and not geometry.isNull() and not geometry.isMultipart():
|
||||||
|
geometry.convertToMultiType()
|
||||||
|
feature.setGeometry(geometry)
|
||||||
|
prepared.append(feature)
|
||||||
|
provider.addFeatures(prepared)
|
||||||
|
layer.updateExtents()
|
||||||
|
layer.setCustomProperty(PROP_DATASET, dataset_key)
|
||||||
|
layer.setCustomProperty(PROP_VERFAHREN, verfahren_nr)
|
||||||
|
layer.setCustomProperty(PROP_PATH, api_path)
|
||||||
|
return layer
|
||||||
|
|
||||||
|
|
||||||
|
def feature_collection_to_layers(
|
||||||
|
feature_collection, base_name, default_geometry, dataset_key, verfahren_nr, api_path
|
||||||
|
):
|
||||||
|
"""Erzeugt aus einer GeoJSON FeatureCollection je vorkommender
|
||||||
|
Geometrie-Familie einen Memory-Layer und hängt sie ins Projekt.
|
||||||
|
Liefert die Liste der erzeugten Layer."""
|
||||||
|
text = json.dumps(feature_collection)
|
||||||
|
fields = QgsJsonUtils.stringToFields(text)
|
||||||
|
features = QgsJsonUtils.stringToFeatureList(text, fields)
|
||||||
|
|
||||||
|
groups = {}
|
||||||
|
for feature in features:
|
||||||
|
groups.setdefault(_family(feature), []).append(feature)
|
||||||
|
|
||||||
|
layers = []
|
||||||
|
multiple = len(groups) > 1
|
||||||
|
|
||||||
|
def make_layer(uri_type, name, group_features, promote):
|
||||||
|
layers.append(
|
||||||
|
memory_layer_from_features(
|
||||||
|
uri_type, name, fields, group_features,
|
||||||
|
dataset_key, verfahren_nr, api_path, promote=promote,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if not groups:
|
||||||
|
# Leerer Datensatz: ein leerer Layer im Default-Typ, damit der
|
||||||
|
# Nutzer digitalisieren und hochladen kann.
|
||||||
|
make_layer(default_geometry, base_name, [], promote=False)
|
||||||
|
else:
|
||||||
|
for family, uri_type, suffix in _FAMILIES:
|
||||||
|
if family not in groups:
|
||||||
|
continue
|
||||||
|
name = "%s — %s" % (base_name, suffix) if multiple else base_name
|
||||||
|
make_layer(uri_type, name, groups[family], promote=(family != "none"))
|
||||||
|
|
||||||
|
QgsProject.instance().addMapLayers(layers)
|
||||||
|
return layers
|
||||||
|
|
||||||
|
|
||||||
|
def plugin_layers(dataset_key, verfahren_nr):
|
||||||
|
"""Alle Layer im Projekt, die zu diesem Datensatz und dieser VKZ
|
||||||
|
gehören (z.B. die Punkte-/Linien-/Flächen-Teil-Layer von Plan 41)."""
|
||||||
|
result = []
|
||||||
|
for layer in QgsProject.instance().mapLayers().values():
|
||||||
|
if (
|
||||||
|
isinstance(layer, QgsVectorLayer)
|
||||||
|
and layer.customProperty(PROP_DATASET) == dataset_key
|
||||||
|
and str(layer.customProperty(PROP_VERFAHREN)) == str(verfahren_nr)
|
||||||
|
):
|
||||||
|
result.append(layer)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def layers_to_feature_collection(layers):
|
||||||
|
"""Vereint die Features mehrerer Layer zu EINER GeoJSON
|
||||||
|
FeatureCollection (für den PUT, der den Serverbestand ersetzt).
|
||||||
|
|
||||||
|
Keine Transformation nach WGS84: Die API erwartet die Koordinaten im
|
||||||
|
selben CRS, in dem sie sie liefert (EPSG:25833)."""
|
||||||
|
merged = []
|
||||||
|
for layer in layers:
|
||||||
|
exporter = QgsJsonExporter(layer)
|
||||||
|
exporter.setSourceCrs(layer.crs())
|
||||||
|
exporter.setTransformGeometries(False)
|
||||||
|
collection = json.loads(
|
||||||
|
exporter.exportFeatures(list(layer.getFeatures()))
|
||||||
|
)
|
||||||
|
merged.extend(collection.get("features", []))
|
||||||
|
return {"type": "FeatureCollection", "features": merged}
|
||||||
|
|
||||||
|
|
||||||
|
def layer_api_path(layer):
|
||||||
|
"""API-Pfad, unter dem dieser Layer geladen wurde — oder None,
|
||||||
|
wenn der Layer nicht von diesem Plugin stammt."""
|
||||||
|
if not isinstance(layer, QgsVectorLayer):
|
||||||
|
return None
|
||||||
|
return layer.customProperty(PROP_PATH) or None
|
||||||
+3
-1
@@ -1 +1,3 @@
|
|||||||
#Testordner
|
"""sn_plan41/tests – Test-Suite für das sn_plan41 Plugin.
|
||||||
|
Enthält Unit-Tests und Integrationstests für Tab-Logik und Datengrabber.
|
||||||
|
"""
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ Zentraler Test-Runner für sn_plan41.
|
|||||||
Wrapper-konform, QGIS-unabhängig, CI- und IDE-fähig.
|
Wrapper-konform, QGIS-unabhängig, CI- und IDE-fähig.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import io
|
||||||
import unittest
|
import unittest
|
||||||
import datetime
|
import datetime
|
||||||
import inspect
|
import inspect
|
||||||
@@ -12,6 +13,19 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Stdout auf UTF-8 umstellen, damit Emoji-Print-Statements in
|
||||||
|
# qt_wrapper.py (Mock-Modus) auf Windows-Konsolen (CP1252) nicht zu
|
||||||
|
# UnicodeEncodeError führen.
|
||||||
|
if hasattr(sys.stdout, "reconfigure"):
|
||||||
|
try:
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
elif hasattr(sys.stdout, "buffer"):
|
||||||
|
sys.stdout = io.TextIOWrapper(
|
||||||
|
sys.stdout.buffer, encoding="utf-8", errors="replace"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
# Plugin-Roots bestimmen
|
# Plugin-Roots bestimmen
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import unittest
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
|
||||||
|
from sn_plan41.modules.listenauswerter import Listenauswerter
|
||||||
|
|
||||||
|
|
||||||
|
class _DummyPruefmanager:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.seen = []
|
||||||
|
|
||||||
|
def verarbeite(self, ergebnis: Any):
|
||||||
|
self.seen.append(ergebnis)
|
||||||
|
return ergebnis
|
||||||
|
|
||||||
|
|
||||||
|
class _DummyStilPruefer:
|
||||||
|
def __init__(self, ok: bool = True) -> None:
|
||||||
|
self.ok = ok
|
||||||
|
|
||||||
|
def pruefe(self, _path: str) -> pruef_ergebnis:
|
||||||
|
if self.ok:
|
||||||
|
return pruef_ergebnis(ok=True, meldung="ok", aktion="ok", kontext=None)
|
||||||
|
return pruef_ergebnis(ok=False, meldung="stil fehler", aktion="falsche_endung", kontext=None)
|
||||||
|
|
||||||
|
|
||||||
|
class TestListenauswerter(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_validate_rows_accepts_valid_row_and_normalizes_provider(self):
|
||||||
|
manager = _DummyPruefmanager()
|
||||||
|
validator = Listenauswerter(manager, _DummyStilPruefer(ok=True))
|
||||||
|
|
||||||
|
data = {"rows": [{"ident": "A1", "Inhalt": "Test", "Link": "http://x", "Provider": "wfs"}]}
|
||||||
|
validated, processed = validator.validate_rows(data)
|
||||||
|
|
||||||
|
self.assertEqual(len(validated["rows"]), 1)
|
||||||
|
self.assertEqual(validated["rows"][0]["Provider"], "WFS")
|
||||||
|
self.assertEqual(processed, [])
|
||||||
|
|
||||||
|
def test_validate_rows_rejects_missing_required_fields(self):
|
||||||
|
manager = _DummyPruefmanager()
|
||||||
|
validator = Listenauswerter(manager, _DummyStilPruefer(ok=True))
|
||||||
|
|
||||||
|
data = {"rows": [{"ident": "", "Link": "", "Provider": ""}]}
|
||||||
|
validated, processed = validator.validate_rows(data)
|
||||||
|
|
||||||
|
self.assertEqual(validated["rows"], [])
|
||||||
|
self.assertEqual(len(processed), 1)
|
||||||
|
self.assertEqual(processed[0].aktion, "pflichtfelder_fehlen")
|
||||||
|
|
||||||
|
def test_validate_rows_sets_stildatei_none_on_style_failure(self):
|
||||||
|
manager = _DummyPruefmanager()
|
||||||
|
validator = Listenauswerter(manager, _DummyStilPruefer(ok=False))
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"ident": "A1",
|
||||||
|
"Inhalt": "Thema",
|
||||||
|
"Link": "http://x",
|
||||||
|
"Provider": "WMS",
|
||||||
|
"Stildatei": "broken.qml",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
validated, processed = validator.validate_rows(data)
|
||||||
|
|
||||||
|
self.assertEqual(len(validated["rows"]), 1)
|
||||||
|
self.assertIsNone(validated["rows"][0]["stildatei"])
|
||||||
|
self.assertEqual(len(processed), 1)
|
||||||
|
self.assertEqual(processed[0].aktion, "falsche_endung")
|
||||||
|
|
||||||
|
def test_validate_rows_handles_non_mapping_rows_edge_case(self):
|
||||||
|
manager = _DummyPruefmanager()
|
||||||
|
validator = Listenauswerter(manager, _DummyStilPruefer(ok=True))
|
||||||
|
|
||||||
|
validated, processed = validator.validate_rows({"rows": ["invalid", 123, None]})
|
||||||
|
|
||||||
|
self.assertEqual(validated, {"rows": []})
|
||||||
|
self.assertEqual(processed, [])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
+78
-121
@@ -1,152 +1,109 @@
|
|||||||
import unittest
|
import unittest
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from tempfile import TemporaryDirectory
|
from tempfile import TemporaryDirectory
|
||||||
|
from typing import Any, cast
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
from sn_plan41.ui.tab_a_logic import TabALogic # type: ignore
|
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
|
||||||
from sn_basis.functions.variable_wrapper import get_variable # type: ignore
|
from sn_basis.functions.variable_wrapper import get_variable
|
||||||
from sn_basis.functions.sys_wrapper import file_exists # type: ignore
|
from sn_plan41.ui.tab_a_logic import TabALogic
|
||||||
|
|
||||||
|
|
||||||
|
class _DummyPruefmanager:
|
||||||
|
def verarbeite(self, ergebnis):
|
||||||
|
return ergebnis
|
||||||
|
|
||||||
|
|
||||||
class TestTabALogic(unittest.TestCase):
|
class TestTabALogic(unittest.TestCase):
|
||||||
|
|
||||||
# -----------------------------------------------------
|
def _make_logic(self) -> TabALogic:
|
||||||
# 1. Verfahrens-DB setzen und laden
|
return TabALogic(
|
||||||
# -----------------------------------------------------
|
pruefmanager=cast(Any, _DummyPruefmanager()),
|
||||||
def test_verfahrens_db_set_and_load(self):
|
link_pruefer=cast(Any, object()),
|
||||||
logic = TabALogic()
|
stil_pruefer=cast(Any, object()),
|
||||||
|
)
|
||||||
|
|
||||||
with TemporaryDirectory() as tmp:
|
def test_verfahrens_db_set_and_load_roundtrip(self):
|
||||||
db_path = Path(tmp) / "test.gpkg"
|
logic = self._make_logic()
|
||||||
db_path.write_text("")
|
logic.set_verfahrens_db("C:/tmp/v_db.gpkg")
|
||||||
|
self.assertEqual(logic.load_verfahrens_db(), "C:/tmp/v_db.gpkg")
|
||||||
|
|
||||||
logic.set_verfahrens_db(str(db_path))
|
|
||||||
|
|
||||||
stored = get_variable("verfahrens_db", scope="project")
|
|
||||||
self.assertEqual(stored, str(db_path))
|
|
||||||
|
|
||||||
loaded = logic.load_verfahrens_db()
|
|
||||||
self.assertEqual(loaded, str(db_path))
|
|
||||||
|
|
||||||
# -----------------------------------------------------
|
|
||||||
# 2. Verfahrens-DB löschen
|
|
||||||
# -----------------------------------------------------
|
|
||||||
def test_verfahrens_db_clear(self):
|
def test_verfahrens_db_clear(self):
|
||||||
logic = TabALogic()
|
logic = self._make_logic()
|
||||||
|
|
||||||
logic.set_verfahrens_db(None)
|
logic.set_verfahrens_db(None)
|
||||||
|
self.assertEqual(get_variable("verfahrens_db", scope="project"), "")
|
||||||
|
|
||||||
stored = get_variable("verfahrens_db", scope="project")
|
def test_linkliste_set_and_load_existing_path(self):
|
||||||
self.assertEqual(stored, "")
|
logic = self._make_logic()
|
||||||
|
|
||||||
# -----------------------------------------------------
|
|
||||||
# 3. Neue Verfahrens-DB anlegen
|
|
||||||
# -----------------------------------------------------
|
|
||||||
def test_create_new_verfahrens_db(self):
|
|
||||||
logic = TabALogic()
|
|
||||||
|
|
||||||
with TemporaryDirectory() as tmp:
|
|
||||||
db_path = Path(tmp) / "neu.gpkg"
|
|
||||||
|
|
||||||
result = logic.create_new_verfahrens_db(str(db_path))
|
|
||||||
|
|
||||||
self.assertTrue(result)
|
|
||||||
self.assertTrue(file_exists(db_path))
|
|
||||||
|
|
||||||
stored = get_variable("verfahrens_db", scope="project")
|
|
||||||
self.assertEqual(stored, str(db_path))
|
|
||||||
|
|
||||||
def test_create_new_verfahrens_db_with_none_path(self):
|
|
||||||
logic = TabALogic()
|
|
||||||
result = logic.create_new_verfahrens_db(None)
|
|
||||||
self.assertFalse(result)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------
|
|
||||||
# 4. Linkliste setzen und laden
|
|
||||||
# -----------------------------------------------------
|
|
||||||
def test_linkliste_set_and_load(self):
|
|
||||||
logic = TabALogic()
|
|
||||||
|
|
||||||
with TemporaryDirectory() as tmp:
|
with TemporaryDirectory() as tmp:
|
||||||
link_path = Path(tmp) / "links.xlsx"
|
link_path = Path(tmp) / "links.xlsx"
|
||||||
link_path.write_text("dummy")
|
link_path.write_text("x", encoding="utf-8")
|
||||||
|
|
||||||
logic.set_linkliste(str(link_path))
|
logic.set_linkliste(str(link_path))
|
||||||
|
self.assertEqual(logic.load_linkliste(), str(link_path))
|
||||||
|
|
||||||
stored = get_variable("linkliste", scope="project")
|
def test_linkliste_load_returns_none_when_file_missing(self):
|
||||||
self.assertEqual(stored, str(link_path))
|
logic = self._make_logic()
|
||||||
|
logic.set_linkliste("C:/tmp/missing.xlsx")
|
||||||
|
self.assertIsNone(logic.load_linkliste())
|
||||||
|
|
||||||
loaded = logic.load_linkliste()
|
@patch("sn_plan41.ui.tab_a_logic.get_plugin_root", return_value="C:/plugins")
|
||||||
self.assertEqual(loaded, str(link_path))
|
@patch("sn_plan41.ui.tab_a_logic.join_path", return_value="C:/plugins/sn_plan41/assets/Linkliste.xlsx")
|
||||||
|
def test_resolve_linkliste_empty_uses_standard_and_persists(self, _mock_join_path, _mock_root):
|
||||||
|
logic = self._make_logic()
|
||||||
|
resolved = logic._resolve_linkliste(None)
|
||||||
|
|
||||||
# -----------------------------------------------------
|
self.assertEqual(resolved, "C:/plugins/sn_plan41/assets/Linkliste.xlsx")
|
||||||
# 5. Linkliste löschen
|
self.assertEqual(get_variable("linkliste", scope="project"), "C:/plugins/sn_plan41/assets/Linkliste.xlsx")
|
||||||
# -----------------------------------------------------
|
|
||||||
def test_linkliste_clear(self):
|
|
||||||
logic = TabALogic()
|
|
||||||
|
|
||||||
logic.set_linkliste(None)
|
@patch("sn_plan41.ui.tab_a_logic.Dateipruefer")
|
||||||
|
def test_resolve_linkliste_returns_none_when_pruefmanager_rejects(self, mock_dateipruefer):
|
||||||
|
logic = self._make_logic()
|
||||||
|
mock_dateipruefer.return_value.pruefe.return_value = pruef_ergebnis(
|
||||||
|
ok=False,
|
||||||
|
meldung="Ungültig",
|
||||||
|
aktion="datei_nicht_gefunden",
|
||||||
|
kontext=None,
|
||||||
|
)
|
||||||
|
|
||||||
stored = get_variable("linkliste", scope="project")
|
resolved = logic._resolve_linkliste("C:/tmp/missing.xlsx")
|
||||||
self.assertEqual(stored, "")
|
self.assertIsNone(resolved)
|
||||||
|
|
||||||
# -----------------------------------------------------
|
def test_resolve_raumfilter_without_filter_returns_none(self):
|
||||||
# 6. Layer-ID speichern
|
logic = self._make_logic()
|
||||||
# -----------------------------------------------------
|
result = logic._resolve_raumfilter("ohne", "C:/tmp/v_db.gpkg")
|
||||||
def test_verfahrensgebiet_layer_id_storage(self):
|
self.assertIsNone(result)
|
||||||
logic = TabALogic()
|
|
||||||
|
|
||||||
class MockLayer:
|
@patch.object(TabALogic, "_get_verfahrensgebiet_layer", return_value="layer_obj")
|
||||||
def id(self):
|
def test_resolve_raumfilter_verfahrensgebiet_uses_layer_lookup(self, mock_get_layer):
|
||||||
return "layer-123"
|
logic = self._make_logic()
|
||||||
|
result = logic._resolve_raumfilter("Verfahrensgebiet", "C:/tmp/v_db.gpkg")
|
||||||
|
|
||||||
logic.save_verfahrensgebiet_layer(MockLayer())
|
self.assertEqual(result, "layer_obj")
|
||||||
|
mock_get_layer.assert_called_once()
|
||||||
|
|
||||||
stored = get_variable("verfahrensgebiet_layer", scope="project")
|
def test_extract_datenabruf_error_maps_merges_reason_maps(self):
|
||||||
self.assertEqual(stored, "layer-123")
|
logic = self._make_logic()
|
||||||
|
results = [
|
||||||
|
pruef_ergebnis(
|
||||||
|
ok=False,
|
||||||
|
meldung="Head failed",
|
||||||
|
aktion="url_nicht_erreichbar",
|
||||||
|
kontext={"ident": "A1", "thema": "Dienst A", "error": "Timeout"},
|
||||||
|
),
|
||||||
|
pruef_ergebnis(
|
||||||
|
ok=False,
|
||||||
|
meldung="Fetch failed",
|
||||||
|
aktion="datenabruf",
|
||||||
|
kontext={"fehler": {"Dienst B": "HTTP 500", "Dienst A": "ignored"}},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
# -----------------------------------------------------
|
merged = logic._extract_datenabruf_error_maps(results)
|
||||||
# 7. Ungültiger Layer wird ignoriert
|
|
||||||
# -----------------------------------------------------
|
|
||||||
def test_invalid_layer_is_rejected(self):
|
|
||||||
logic = TabALogic()
|
|
||||||
|
|
||||||
class InvalidLayer:
|
self.assertEqual(merged["by_ident"]["A1"], "Timeout")
|
||||||
pass
|
self.assertEqual(merged["by_thema"]["Dienst A"], "Timeout")
|
||||||
|
self.assertEqual(merged["by_thema"]["Dienst B"], "HTTP 500")
|
||||||
logic.save_verfahrensgebiet_layer(InvalidLayer())
|
|
||||||
|
|
||||||
stored = get_variable("verfahrensgebiet_layer", scope="project")
|
|
||||||
self.assertEqual(stored, "")
|
|
||||||
#-----------------------------------------------------
|
|
||||||
# 8. Layer-ID wirft Exception
|
|
||||||
#----------------------------------------------------
|
|
||||||
def test_layer_id_raises_exception(self):
|
|
||||||
logic = TabALogic()
|
|
||||||
|
|
||||||
class BadLayer:
|
|
||||||
def id(self):
|
|
||||||
raise RuntimeError("boom")
|
|
||||||
|
|
||||||
logic.save_verfahrensgebiet_layer(BadLayer())
|
|
||||||
|
|
||||||
stored = get_variable("verfahrensgebiet_layer", scope="project")
|
|
||||||
self.assertEqual(stored, "")
|
|
||||||
# -----------------------------------------------------
|
|
||||||
# 11. Layer ID wird leer zurückgegeben
|
|
||||||
# -----------------------------------------------------
|
|
||||||
def test_layer_id_returns_empty(self):
|
|
||||||
logic = TabALogic()
|
|
||||||
|
|
||||||
class EmptyLayer:
|
|
||||||
def id(self):
|
|
||||||
return ""
|
|
||||||
|
|
||||||
logic.save_verfahrensgebiet_layer(EmptyLayer())
|
|
||||||
|
|
||||||
stored = get_variable("verfahrensgebiet_layer", scope="project")
|
|
||||||
self.assertEqual(stored, "")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import unittest
|
||||||
|
from unittest.mock import patch
|
||||||
|
from typing import Any, cast
|
||||||
|
|
||||||
|
from sn_plan41.ui.tab_a_logic import TabALogic
|
||||||
|
|
||||||
|
|
||||||
|
class _DummyPruefmanager:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.hinweise: list[tuple[str, str]] = []
|
||||||
|
|
||||||
|
def verarbeite(self, ergebnis):
|
||||||
|
return ergebnis
|
||||||
|
|
||||||
|
def zeige_hinweis(self, titel: str, meldung: str) -> None:
|
||||||
|
self.hinweise.append((titel, meldung))
|
||||||
|
|
||||||
|
|
||||||
|
class _DummyDateiErgebnis:
|
||||||
|
def __init__(self, ok: bool = True, kontext: str = "C:/tmp/v_db.gpkg") -> None:
|
||||||
|
self.ok = ok
|
||||||
|
self.kontext = kontext
|
||||||
|
|
||||||
|
|
||||||
|
class _DummyProgress:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.closed = False
|
||||||
|
|
||||||
|
def set_total(self, _value: int) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def set_value(self, _value: int) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def set_label(self, _value: str) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def close(self) -> None:
|
||||||
|
self.closed = True
|
||||||
|
|
||||||
|
|
||||||
|
class _DummyProject:
|
||||||
|
def mapLayer(self, _layer_id: str):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class _DummyGrabber:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TestTabAPreconditions(unittest.TestCase):
|
||||||
|
|
||||||
|
def _make_logic(self) -> tuple[TabALogic, _DummyPruefmanager]:
|
||||||
|
pruefmanager = _DummyPruefmanager()
|
||||||
|
logic = TabALogic(
|
||||||
|
pruefmanager=cast(Any, pruefmanager),
|
||||||
|
link_pruefer=cast(Any, object()),
|
||||||
|
stil_pruefer=cast(Any, object()),
|
||||||
|
)
|
||||||
|
return logic, pruefmanager
|
||||||
|
|
||||||
|
def test_get_verfahrensgebiet_layer_shows_hint_when_layer_id_missing(self):
|
||||||
|
logic, pruefmanager = self._make_logic()
|
||||||
|
|
||||||
|
with patch.object(logic, "load_verfahrensgebiet_layer_id", return_value=None):
|
||||||
|
layer = logic._get_verfahrensgebiet_layer()
|
||||||
|
|
||||||
|
self.assertIsNone(layer)
|
||||||
|
self.assertEqual(len(pruefmanager.hinweise), 1)
|
||||||
|
self.assertIn("Verfahrensgebiet", pruefmanager.hinweise[0][0])
|
||||||
|
|
||||||
|
@patch("sn_plan41.ui.tab_a_logic.QgsProject.instance", return_value=_DummyProject())
|
||||||
|
def test_get_verfahrensgebiet_layer_shows_hint_when_layer_not_in_project(self, _mock_project_instance):
|
||||||
|
logic, pruefmanager = self._make_logic()
|
||||||
|
|
||||||
|
with patch.object(logic, "load_verfahrensgebiet_layer_id", return_value="layer_404"):
|
||||||
|
layer = logic._get_verfahrensgebiet_layer()
|
||||||
|
|
||||||
|
self.assertIsNone(layer)
|
||||||
|
self.assertEqual(len(pruefmanager.hinweise), 1)
|
||||||
|
self.assertIn("Verfahrensgebiet", pruefmanager.hinweise[0][0])
|
||||||
|
|
||||||
|
@patch("sn_plan41.ui.tab_a_logic.Dateipruefer")
|
||||||
|
def test_pipeline_aborts_and_shows_hint_when_verfahrensgebiet_missing(
|
||||||
|
self,
|
||||||
|
mock_dateipruefer,
|
||||||
|
):
|
||||||
|
logic, pruefmanager = self._make_logic()
|
||||||
|
logic.data_grabber = cast(Any, _DummyGrabber())
|
||||||
|
|
||||||
|
mock_dateipruefer.return_value.pruefe.return_value = _DummyDateiErgebnis(ok=True, kontext="C:/tmp/v_db.gpkg")
|
||||||
|
|
||||||
|
progress = _DummyProgress()
|
||||||
|
|
||||||
|
with patch("sn_plan41.ui.tab_a_logic.create_progress_dialog", return_value=progress):
|
||||||
|
with patch.object(logic, "_resolve_linkliste", return_value="C:/tmp/linkliste.xlsx"):
|
||||||
|
with patch.object(logic, "load_verfahrensgebiet_layer_id", return_value=None):
|
||||||
|
result = logic._on_run_pipeline(
|
||||||
|
source="C:/tmp/v_db.gpkg",
|
||||||
|
linkliste=None,
|
||||||
|
raumfilter="Verfahrensgebiet",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIsNone(result)
|
||||||
|
self.assertEqual(len(pruefmanager.hinweise), 1)
|
||||||
|
self.assertIn("Verfahrensgebiet", pruefmanager.hinweise[0][0])
|
||||||
|
self.assertTrue(progress.closed)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
+94
-37
@@ -1,56 +1,113 @@
|
|||||||
"""
|
|
||||||
Smoke-Tests für TabA UI (sn_plan41/ui/tab_a_ui.py)
|
|
||||||
|
|
||||||
Ziel:
|
|
||||||
- UI kann erstellt werden
|
|
||||||
- Callbacks crashen nicht
|
|
||||||
- Keine Qt-Verhaltensprüfung
|
|
||||||
"""
|
|
||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
from sn_plan41.ui.tab_a_ui import TabA #type:ignore
|
from sn_basis.functions.variable_wrapper import get_variable
|
||||||
|
from sn_plan41.ui.tab_a_ui import TabA
|
||||||
|
|
||||||
|
|
||||||
|
class _DummySignal:
|
||||||
|
def connect(self, _callback):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class _DummyToggleButton:
|
||||||
|
def __init__(self):
|
||||||
|
self.arrow = None
|
||||||
|
self.style = ""
|
||||||
|
|
||||||
|
def setArrowType(self, arrow):
|
||||||
|
self.arrow = arrow
|
||||||
|
|
||||||
|
def setStyleSheet(self, style):
|
||||||
|
self.style = style
|
||||||
|
|
||||||
|
|
||||||
|
class _DummyWidget:
|
||||||
|
def __init__(self):
|
||||||
|
self.visible = None
|
||||||
|
|
||||||
|
def setVisible(self, visible):
|
||||||
|
self.visible = visible
|
||||||
|
|
||||||
|
|
||||||
|
class _DummyFileWidget:
|
||||||
|
def __init__(self):
|
||||||
|
self._path = ""
|
||||||
|
|
||||||
|
def setFilePath(self, path):
|
||||||
|
self._path = path
|
||||||
|
|
||||||
|
def filePath(self):
|
||||||
|
return self._path
|
||||||
|
|
||||||
|
|
||||||
|
class _DummyCombo:
|
||||||
|
def __init__(self, value="Pufferlayer"):
|
||||||
|
self._value = value
|
||||||
|
|
||||||
|
def currentText(self):
|
||||||
|
return self._value
|
||||||
|
|
||||||
|
|
||||||
|
class _DummyPruefmanager:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class _DummyDataGrabber:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class TestTabAUI(unittest.TestCase):
|
class TestTabAUI(unittest.TestCase):
|
||||||
|
|
||||||
# -----------------------------------------------------
|
def _make_tab_without_qt(self):
|
||||||
# 1. UI kann erstellt werden
|
tab = TabA.__new__(TabA)
|
||||||
# -----------------------------------------------------
|
tab.group_button = _DummyToggleButton()
|
||||||
def test_tab_a_ui_can_be_created(self):
|
tab.group_content = _DummyWidget()
|
||||||
tab = TabA(parent=None,build_ui=False)
|
tab.optional_button = _DummyToggleButton()
|
||||||
|
tab.optional_content = _DummyWidget()
|
||||||
|
tab.file_widget = _DummyFileWidget()
|
||||||
|
tab.linkliste_widget = _DummyFileWidget()
|
||||||
|
tab._raumfilter_combo = _DummyCombo("ohne")
|
||||||
|
tab.verfahrens_db = None
|
||||||
|
tab.lokale_linkliste = None
|
||||||
|
tab.logic = None
|
||||||
|
tab.pruefmanager = None
|
||||||
|
tab.data_grabber = None
|
||||||
|
return tab
|
||||||
|
|
||||||
self.assertIsNotNone(tab)
|
def test_tab_a_callbacks_can_be_called_without_crash(self):
|
||||||
self.assertEqual(tab.tab_title, "Daten")
|
tab = self._make_tab_without_qt()
|
||||||
|
|
||||||
# -----------------------------------------------------
|
|
||||||
# 2. Toggle-Callbacks crashen nicht
|
|
||||||
# -----------------------------------------------------
|
|
||||||
def test_tab_a_toggle_callbacks_do_not_crash(self):
|
|
||||||
tab = TabA(parent=None,build_ui=False)
|
|
||||||
|
|
||||||
tab._toggle_group(True)
|
tab._toggle_group(True)
|
||||||
tab._toggle_group(False)
|
tab._toggle_group(False)
|
||||||
|
|
||||||
tab._toggle_optional(True)
|
tab._toggle_optional(True)
|
||||||
tab._toggle_optional(False)
|
tab._toggle_optional(False)
|
||||||
|
|
||||||
# -----------------------------------------------------
|
|
||||||
# 3. Datei-Callbacks akzeptieren leere Eingaben
|
|
||||||
# -----------------------------------------------------
|
|
||||||
def test_tab_a_file_callbacks_accept_empty_input(self):
|
|
||||||
tab = TabA(parent=None,build_ui=False)
|
|
||||||
|
|
||||||
tab._on_verfahrens_db_changed("")
|
tab._on_verfahrens_db_changed("")
|
||||||
tab._on_linkliste_changed("")
|
tab._on_linkliste_changed("")
|
||||||
|
|
||||||
# -----------------------------------------------------
|
self.assertEqual(tab.group_content.visible, False)
|
||||||
# 4. Layer-Callback akzeptiert None
|
self.assertEqual(tab.optional_content.visible, False)
|
||||||
# -----------------------------------------------------
|
|
||||||
def test_tab_a_layer_callback_accepts_none(self):
|
|
||||||
tab = TabA(parent=None,build_ui=False)
|
|
||||||
|
|
||||||
tab._on_layer_changed(None)
|
def test_tab_a_raumfilter_callback_persists_value(self):
|
||||||
|
tab = self._make_tab_without_qt()
|
||||||
|
|
||||||
|
tab._on_raumfilter_changed("ohne")
|
||||||
|
self.assertEqual(get_variable("Raumfilter", scope="project"), "ohne")
|
||||||
|
|
||||||
|
@patch("sn_plan41.ui.tab_a_ui.TabALogic")
|
||||||
|
def test_set_services_initializes_logic_and_injects_data_grabber(self, mock_logic_cls):
|
||||||
|
tab = self._make_tab_without_qt()
|
||||||
|
mock_logic = mock_logic_cls.return_value
|
||||||
|
|
||||||
|
pm = _DummyPruefmanager()
|
||||||
|
dg = _DummyDataGrabber()
|
||||||
|
|
||||||
|
tab.set_services(pruefmanager=pm, data_grabber=dg)
|
||||||
|
|
||||||
|
self.assertIs(tab.pruefmanager, pm)
|
||||||
|
self.assertIs(tab.data_grabber, dg)
|
||||||
|
self.assertIs(tab.logic, mock_logic)
|
||||||
|
self.assertIs(tab.logic.data_grabber, dg)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -0,0 +1,473 @@
|
|||||||
|
"""
|
||||||
|
Tests für sn_plan41/modules/vln_api_logic.py
|
||||||
|
|
||||||
|
Alle externen Abhängigkeiten (vln_karten, QSettings, QgsProject, Netzwerk)
|
||||||
|
werden mit unittest.mock gemockt, sodass keine QGIS-Laufzeitumgebung nötig ist.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
from typing import Any, Optional
|
||||||
|
from unittest.mock import MagicMock, patch, call
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Helpers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _make_logic(api_key: Optional[str] = None, mail: Optional[str] = None):
|
||||||
|
"""Erzeugt eine VlnApiLogic-Instanz mit gemockten QSettings."""
|
||||||
|
from sn_plan41.modules.vln_api_logic import VlnApiLogic
|
||||||
|
|
||||||
|
with patch("sn_plan41.modules.vln_api_logic.QSettings") as mock_qs_cls:
|
||||||
|
mock_qs = MagicMock()
|
||||||
|
mock_qs.value.side_effect = lambda key, default="": (
|
||||||
|
api_key if key == "api_key" else (mail if key == "mail" else default)
|
||||||
|
)
|
||||||
|
mock_qs_cls.return_value = mock_qs
|
||||||
|
|
||||||
|
if api_key and VlnApiLogic.is_available():
|
||||||
|
with patch(
|
||||||
|
"sn_plan41.modules.vln_api_logic.KartenApiClient"
|
||||||
|
) as mock_client_cls:
|
||||||
|
mock_client = MagicMock()
|
||||||
|
mock_client.is_authenticated = True
|
||||||
|
mock_client.api_key = api_key
|
||||||
|
mock_client.mail = mail
|
||||||
|
mock_client_cls.return_value = mock_client
|
||||||
|
logic = VlnApiLogic()
|
||||||
|
else:
|
||||||
|
logic = VlnApiLogic()
|
||||||
|
|
||||||
|
return logic
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Tests: Verfügbarkeit
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class TestVlnApiLogicAvailability(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_is_available_reflects_import_state(self):
|
||||||
|
from sn_plan41.modules.vln_api_logic import VlnApiLogic
|
||||||
|
result = VlnApiLogic.is_available()
|
||||||
|
# Ergebnis hängt davon ab, ob vln_karten installiert ist —
|
||||||
|
# wir prüfen nur, dass ein bool zurückkommt
|
||||||
|
self.assertIsInstance(result, bool)
|
||||||
|
|
||||||
|
def test_is_available_false_when_vln_karten_missing(self):
|
||||||
|
"""Wenn VLN_KARTEN_AVAILABLE=False, liefert is_available() False."""
|
||||||
|
import sn_plan41.modules.vln_api_logic as module
|
||||||
|
original = module.VLN_KARTEN_AVAILABLE
|
||||||
|
try:
|
||||||
|
module.VLN_KARTEN_AVAILABLE = False
|
||||||
|
from sn_plan41.modules.vln_api_logic import VlnApiLogic
|
||||||
|
with patch.object(module, "VLN_KARTEN_AVAILABLE", False):
|
||||||
|
self.assertFalse(VlnApiLogic.is_available())
|
||||||
|
finally:
|
||||||
|
module.VLN_KARTEN_AVAILABLE = original
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Tests: Credentials-Persistenz
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class TestVlnApiLogicCredentials(unittest.TestCase):
|
||||||
|
|
||||||
|
def _make(self):
|
||||||
|
from sn_plan41.modules.vln_api_logic import VlnApiLogic
|
||||||
|
|
||||||
|
with patch("sn_plan41.modules.vln_api_logic.QSettings"):
|
||||||
|
with patch("sn_plan41.modules.vln_api_logic.KartenApiClient"):
|
||||||
|
return VlnApiLogic()
|
||||||
|
|
||||||
|
def test_save_api_key_calls_qsettings(self):
|
||||||
|
logic = self._make()
|
||||||
|
with patch("sn_plan41.modules.vln_api_logic.QSettings") as mock_qs_cls:
|
||||||
|
mock_qs = MagicMock()
|
||||||
|
mock_qs_cls.return_value = mock_qs
|
||||||
|
logic.save_api_key("token123", "user@test.de")
|
||||||
|
|
||||||
|
mock_qs.setValue.assert_any_call("api_key", "token123")
|
||||||
|
mock_qs.setValue.assert_any_call("mail", "user@test.de")
|
||||||
|
mock_qs.beginGroup.assert_called_with("vln_karten")
|
||||||
|
|
||||||
|
def test_clear_api_key_calls_remove(self):
|
||||||
|
logic = self._make()
|
||||||
|
with patch("sn_plan41.modules.vln_api_logic.QSettings") as mock_qs_cls:
|
||||||
|
mock_qs = MagicMock()
|
||||||
|
mock_qs_cls.return_value = mock_qs
|
||||||
|
logic.clear_api_key()
|
||||||
|
|
||||||
|
mock_qs.remove.assert_called_with("api_key")
|
||||||
|
|
||||||
|
def test_load_stored_credentials_returns_api_key_and_mail(self):
|
||||||
|
logic = self._make()
|
||||||
|
with patch("sn_plan41.modules.vln_api_logic.QSettings") as mock_qs_cls:
|
||||||
|
mock_qs = MagicMock()
|
||||||
|
mock_qs.value.side_effect = lambda key, default="": {
|
||||||
|
"api_key": "mykey",
|
||||||
|
"mail": "user@test.de",
|
||||||
|
}.get(key, default)
|
||||||
|
mock_qs_cls.return_value = mock_qs
|
||||||
|
|
||||||
|
api_key, mail = logic.load_stored_credentials()
|
||||||
|
|
||||||
|
self.assertEqual(api_key, "mykey")
|
||||||
|
self.assertEqual(mail, "user@test.de")
|
||||||
|
|
||||||
|
def test_load_stored_credentials_returns_none_for_empty(self):
|
||||||
|
logic = self._make()
|
||||||
|
with patch("sn_plan41.modules.vln_api_logic.QSettings") as mock_qs_cls:
|
||||||
|
mock_qs = MagicMock()
|
||||||
|
mock_qs.value.return_value = ""
|
||||||
|
mock_qs_cls.return_value = mock_qs
|
||||||
|
|
||||||
|
api_key, mail = logic.load_stored_credentials()
|
||||||
|
|
||||||
|
self.assertIsNone(api_key)
|
||||||
|
self.assertIsNone(mail)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Tests: Login / Logout
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class TestVlnApiLogicLogin(unittest.TestCase):
|
||||||
|
|
||||||
|
def _make(self):
|
||||||
|
from sn_plan41.modules.vln_api_logic import VlnApiLogic
|
||||||
|
|
||||||
|
with patch("sn_plan41.modules.vln_api_logic.QSettings"):
|
||||||
|
with patch("sn_plan41.modules.vln_api_logic.KartenApiClient"):
|
||||||
|
return VlnApiLogic()
|
||||||
|
|
||||||
|
def test_login_sets_client_and_saves_key(self):
|
||||||
|
import sn_plan41.modules.vln_api_logic as module
|
||||||
|
if not module.VLN_KARTEN_AVAILABLE:
|
||||||
|
self.skipTest("vln_karten nicht verfügbar")
|
||||||
|
|
||||||
|
logic = self._make()
|
||||||
|
|
||||||
|
mock_client = MagicMock()
|
||||||
|
mock_client.api_key = "newtoken"
|
||||||
|
|
||||||
|
with patch("sn_plan41.modules.vln_api_logic.KartenApiClient", return_value=mock_client):
|
||||||
|
with patch.object(logic, "save_api_key") as mock_save:
|
||||||
|
logic.login("user@test.de", "secret")
|
||||||
|
|
||||||
|
mock_client.login.assert_called_once_with("user@test.de", "secret")
|
||||||
|
mock_save.assert_called_once_with("newtoken", "user@test.de")
|
||||||
|
|
||||||
|
def test_login_raises_runtime_error_without_qgis_modules(self):
|
||||||
|
import sn_plan41.modules.vln_api_logic as module
|
||||||
|
logic = self._make()
|
||||||
|
with patch.object(module, "VLN_KARTEN_AVAILABLE", False):
|
||||||
|
with self.assertRaises(RuntimeError):
|
||||||
|
logic.login("user@test.de", "secret")
|
||||||
|
|
||||||
|
def test_login_propagates_api_error(self):
|
||||||
|
import sn_plan41.modules.vln_api_logic as module
|
||||||
|
if not module.VLN_KARTEN_AVAILABLE:
|
||||||
|
self.skipTest("vln_karten nicht verfügbar")
|
||||||
|
|
||||||
|
logic = self._make()
|
||||||
|
mock_client = MagicMock()
|
||||||
|
mock_client.login.side_effect = module.ApiError("Ungültige Zugangsdaten")
|
||||||
|
|
||||||
|
with patch("sn_plan41.modules.vln_api_logic.KartenApiClient", return_value=mock_client):
|
||||||
|
with self.assertRaises(Exception):
|
||||||
|
logic.login("user@test.de", "wrong")
|
||||||
|
|
||||||
|
def test_logout_clears_client(self):
|
||||||
|
logic = self._make()
|
||||||
|
logic._client = MagicMock()
|
||||||
|
logic.logout()
|
||||||
|
self.assertIsNone(logic._client)
|
||||||
|
|
||||||
|
def test_is_authenticated_false_after_logout(self):
|
||||||
|
logic = self._make()
|
||||||
|
logic._client = None
|
||||||
|
self.assertFalse(logic.is_authenticated)
|
||||||
|
|
||||||
|
def test_handle_session_expired_clears_key_and_client(self):
|
||||||
|
logic = self._make()
|
||||||
|
with patch.object(logic, "clear_api_key") as mock_clear:
|
||||||
|
with patch.object(logic, "logout") as mock_logout:
|
||||||
|
logic.handle_session_expired()
|
||||||
|
|
||||||
|
mock_clear.assert_called_once()
|
||||||
|
mock_logout.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Tests: Verfahrensliste
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class TestVlnApiLogicVerfahren(unittest.TestCase):
|
||||||
|
|
||||||
|
def _make_authenticated(self):
|
||||||
|
from sn_plan41.modules.vln_api_logic import VlnApiLogic
|
||||||
|
|
||||||
|
with patch("sn_plan41.modules.vln_api_logic.QSettings"):
|
||||||
|
with patch("sn_plan41.modules.vln_api_logic.KartenApiClient"):
|
||||||
|
logic = VlnApiLogic()
|
||||||
|
|
||||||
|
mock_client = MagicMock()
|
||||||
|
mock_client.is_authenticated = True
|
||||||
|
logic._client = mock_client
|
||||||
|
return logic, mock_client
|
||||||
|
|
||||||
|
def test_get_verfahren_raises_when_not_authenticated(self):
|
||||||
|
from sn_plan41.modules.vln_api_logic import VlnApiLogic
|
||||||
|
|
||||||
|
with patch("sn_plan41.modules.vln_api_logic.QSettings"):
|
||||||
|
with patch("sn_plan41.modules.vln_api_logic.KartenApiClient"):
|
||||||
|
logic = VlnApiLogic()
|
||||||
|
logic._client = None
|
||||||
|
|
||||||
|
with self.assertRaises(Exception):
|
||||||
|
logic.get_verfahren()
|
||||||
|
|
||||||
|
def test_get_verfahren_delegates_to_client(self):
|
||||||
|
logic, mock_client = self._make_authenticated()
|
||||||
|
mock_client.get_verfahren.return_value = [
|
||||||
|
{"vkz": "27010", "name": "Alpha"},
|
||||||
|
{"vkz": "27001", "name": "Beta"},
|
||||||
|
]
|
||||||
|
result = logic.get_verfahren()
|
||||||
|
mock_client.get_verfahren.assert_called_once()
|
||||||
|
self.assertEqual(len(result), 2)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Tests: VKZ-Persistenz
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class TestVlnApiLogicVkz(unittest.TestCase):
|
||||||
|
|
||||||
|
def _make(self):
|
||||||
|
from sn_plan41.modules.vln_api_logic import VlnApiLogic
|
||||||
|
|
||||||
|
with patch("sn_plan41.modules.vln_api_logic.QSettings"):
|
||||||
|
with patch("sn_plan41.modules.vln_api_logic.KartenApiClient"):
|
||||||
|
return VlnApiLogic()
|
||||||
|
|
||||||
|
def test_load_stored_vkz_reads_from_project(self):
|
||||||
|
logic = self._make()
|
||||||
|
mock_project = MagicMock()
|
||||||
|
mock_project.readEntry.return_value = ("27010", True)
|
||||||
|
|
||||||
|
with patch("sn_plan41.modules.vln_api_logic.QgsProject") as mock_qgsproj_cls:
|
||||||
|
mock_qgsproj_cls.instance.return_value = mock_project
|
||||||
|
vkz = logic.load_stored_vkz()
|
||||||
|
|
||||||
|
self.assertEqual(vkz, "27010")
|
||||||
|
mock_project.readEntry.assert_called_once_with("vln_karten", "/vkz", "")
|
||||||
|
|
||||||
|
def test_load_stored_vkz_returns_none_for_empty(self):
|
||||||
|
logic = self._make()
|
||||||
|
mock_project = MagicMock()
|
||||||
|
mock_project.readEntry.return_value = ("", True)
|
||||||
|
|
||||||
|
with patch("sn_plan41.modules.vln_api_logic.QgsProject") as mock_qgsproj_cls:
|
||||||
|
mock_qgsproj_cls.instance.return_value = mock_project
|
||||||
|
vkz = logic.load_stored_vkz()
|
||||||
|
|
||||||
|
self.assertIsNone(vkz)
|
||||||
|
|
||||||
|
def test_save_vkz_writes_to_project(self):
|
||||||
|
logic = self._make()
|
||||||
|
mock_project = MagicMock()
|
||||||
|
|
||||||
|
with patch("sn_plan41.modules.vln_api_logic.QgsProject") as mock_qgsproj_cls:
|
||||||
|
mock_qgsproj_cls.instance.return_value = mock_project
|
||||||
|
logic.save_vkz("27010")
|
||||||
|
|
||||||
|
mock_project.writeEntry.assert_called_once_with("vln_karten", "/vkz", "27010")
|
||||||
|
|
||||||
|
def test_save_vkz_does_nothing_for_none(self):
|
||||||
|
logic = self._make()
|
||||||
|
mock_project = MagicMock()
|
||||||
|
|
||||||
|
with patch("sn_plan41.modules.vln_api_logic.QgsProject") as mock_qgsproj_cls:
|
||||||
|
mock_qgsproj_cls.instance.return_value = mock_project
|
||||||
|
logic.save_vkz(None)
|
||||||
|
|
||||||
|
mock_project.writeEntry.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Tests: Plan 41 laden (Stil-Anwendung)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class TestVlnApiLogicLoadP41(unittest.TestCase):
|
||||||
|
|
||||||
|
def _make(self):
|
||||||
|
from sn_plan41.modules.vln_api_logic import VlnApiLogic
|
||||||
|
|
||||||
|
with patch("sn_plan41.modules.vln_api_logic.QSettings"):
|
||||||
|
with patch("sn_plan41.modules.vln_api_logic.KartenApiClient"):
|
||||||
|
logic = VlnApiLogic()
|
||||||
|
|
||||||
|
mock_client = MagicMock()
|
||||||
|
mock_client.is_authenticated = True
|
||||||
|
mock_client.load_layer_complete.return_value = {
|
||||||
|
"type": "FeatureCollection",
|
||||||
|
"features": [],
|
||||||
|
}
|
||||||
|
logic._client = mock_client
|
||||||
|
return logic
|
||||||
|
|
||||||
|
def test_load_p41_raises_without_vln_karten(self):
|
||||||
|
import sn_plan41.modules.vln_api_logic as module
|
||||||
|
logic = self._make()
|
||||||
|
with patch.object(module, "VLN_KARTEN_AVAILABLE", False):
|
||||||
|
with self.assertRaises(RuntimeError):
|
||||||
|
logic.load_p41("27010")
|
||||||
|
|
||||||
|
def test_load_p41_applies_styles_to_polygon_and_line_layers(self):
|
||||||
|
import sn_plan41.modules.vln_api_logic as module
|
||||||
|
if not module.VLN_KARTEN_AVAILABLE:
|
||||||
|
self.skipTest("vln_karten nicht verfügbar")
|
||||||
|
|
||||||
|
logic = self._make()
|
||||||
|
|
||||||
|
# Zwei Mock-Layer: einer Polygon (2), einer Linie (1)
|
||||||
|
mock_layer_polygon = MagicMock()
|
||||||
|
mock_layer_polygon.geometryType.return_value = module._GEOM_POLYGON
|
||||||
|
|
||||||
|
mock_layer_line = MagicMock()
|
||||||
|
mock_layer_line.geometryType.return_value = module._GEOM_LINE
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"sn_plan41.modules.vln_api_logic.feature_collection_to_layers",
|
||||||
|
return_value=[mock_layer_polygon, mock_layer_line],
|
||||||
|
):
|
||||||
|
with patch(
|
||||||
|
"sn_plan41.modules.vln_api_logic.apply_style_from_path"
|
||||||
|
) as mock_style:
|
||||||
|
with patch("sn_plan41.modules.vln_api_logic.join_path", side_effect=lambda *p: "/".join(p)):
|
||||||
|
with patch("sn_plan41.modules.vln_api_logic.get_plugin_root", return_value="/plugins"):
|
||||||
|
logic.load_p41("27010")
|
||||||
|
|
||||||
|
# Prüfen, dass der richtige Stil für jeden Geometrietyp verwendet wurde
|
||||||
|
style_calls = [str(c) for c in mock_style.call_args_list]
|
||||||
|
polygon_style_used = any(
|
||||||
|
module.STYLE_FLAECHE in c for c in style_calls
|
||||||
|
)
|
||||||
|
line_style_used = any(
|
||||||
|
module.STYLE_LINIE in c for c in style_calls
|
||||||
|
)
|
||||||
|
self.assertTrue(polygon_style_used, "Flächen-Stil wurde nicht angewendet")
|
||||||
|
self.assertTrue(line_style_used, "Linien-Stil wurde nicht angewendet")
|
||||||
|
|
||||||
|
def test_load_p41_no_style_for_point_layer(self):
|
||||||
|
import sn_plan41.modules.vln_api_logic as module
|
||||||
|
if not module.VLN_KARTEN_AVAILABLE:
|
||||||
|
self.skipTest("vln_karten nicht verfügbar")
|
||||||
|
|
||||||
|
logic = self._make()
|
||||||
|
|
||||||
|
mock_layer_point = MagicMock()
|
||||||
|
mock_layer_point.geometryType.return_value = 0 # Point
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"sn_plan41.modules.vln_api_logic.feature_collection_to_layers",
|
||||||
|
return_value=[mock_layer_point],
|
||||||
|
):
|
||||||
|
with patch(
|
||||||
|
"sn_plan41.modules.vln_api_logic.apply_style_from_path"
|
||||||
|
) as mock_style:
|
||||||
|
with patch("sn_plan41.modules.vln_api_logic.join_path", side_effect=lambda *p: "/".join(p)):
|
||||||
|
with patch("sn_plan41.modules.vln_api_logic.get_plugin_root", return_value="/plugins"):
|
||||||
|
logic.load_p41("27010")
|
||||||
|
|
||||||
|
mock_style.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Tests: Upload
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class TestVlnApiLogicUpload(unittest.TestCase):
|
||||||
|
|
||||||
|
def _make(self):
|
||||||
|
from sn_plan41.modules.vln_api_logic import VlnApiLogic
|
||||||
|
|
||||||
|
with patch("sn_plan41.modules.vln_api_logic.QSettings"):
|
||||||
|
with patch("sn_plan41.modules.vln_api_logic.KartenApiClient"):
|
||||||
|
logic = VlnApiLogic()
|
||||||
|
|
||||||
|
mock_client = MagicMock()
|
||||||
|
mock_client.is_authenticated = True
|
||||||
|
logic._client = mock_client
|
||||||
|
return logic
|
||||||
|
|
||||||
|
def test_upload_returns_error_for_layer_without_api_path(self):
|
||||||
|
import sn_plan41.modules.vln_api_logic as module
|
||||||
|
if not module.VLN_KARTEN_AVAILABLE:
|
||||||
|
self.skipTest("vln_karten nicht verfügbar")
|
||||||
|
|
||||||
|
logic = self._make()
|
||||||
|
mock_layer = MagicMock()
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"sn_plan41.modules.vln_api_logic.layer_api_path", return_value=None
|
||||||
|
):
|
||||||
|
success, message = logic.upload_active_layer(mock_layer)
|
||||||
|
|
||||||
|
self.assertFalse(success)
|
||||||
|
self.assertIn("nicht", message.lower())
|
||||||
|
|
||||||
|
def test_upload_returns_error_for_none_layer(self):
|
||||||
|
import sn_plan41.modules.vln_api_logic as module
|
||||||
|
if not module.VLN_KARTEN_AVAILABLE:
|
||||||
|
self.skipTest("vln_karten nicht verfügbar")
|
||||||
|
|
||||||
|
logic = self._make()
|
||||||
|
with patch(
|
||||||
|
"sn_plan41.modules.vln_api_logic.layer_api_path", return_value=None
|
||||||
|
):
|
||||||
|
success, message = logic.upload_active_layer(None)
|
||||||
|
|
||||||
|
self.assertFalse(success)
|
||||||
|
|
||||||
|
def test_upload_returns_false_without_qgis_modules(self):
|
||||||
|
import sn_plan41.modules.vln_api_logic as module
|
||||||
|
logic = self._make()
|
||||||
|
with patch.object(module, "VLN_KARTEN_AVAILABLE", False):
|
||||||
|
success, message = logic.upload_active_layer(MagicMock())
|
||||||
|
|
||||||
|
self.assertFalse(success)
|
||||||
|
|
||||||
|
def test_upload_calls_save_feature_collection(self):
|
||||||
|
import sn_plan41.modules.vln_api_logic as module
|
||||||
|
if not module.VLN_KARTEN_AVAILABLE:
|
||||||
|
self.skipTest("vln_karten nicht verfügbar")
|
||||||
|
|
||||||
|
logic = self._make()
|
||||||
|
|
||||||
|
mock_layer = MagicMock()
|
||||||
|
mock_layer.isEditable.return_value = False
|
||||||
|
mock_layer.featureCount.return_value = 5
|
||||||
|
|
||||||
|
fc = {"type": "FeatureCollection", "features": []}
|
||||||
|
|
||||||
|
with patch("sn_plan41.modules.vln_api_logic.layer_api_path", return_value="/maps/p41/27010"):
|
||||||
|
with patch("sn_plan41.modules.vln_api_logic.plugin_layers", return_value=[mock_layer]):
|
||||||
|
with patch("sn_plan41.modules.vln_api_logic.layers_to_feature_collection", return_value=fc):
|
||||||
|
success, message = logic.upload_active_layer(mock_layer)
|
||||||
|
|
||||||
|
logic._client.save_feature_collection.assert_called_once_with("/maps/p41/27010", fc)
|
||||||
|
self.assertTrue(success)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
+22
-3
@@ -1,16 +1,35 @@
|
|||||||
#sn_plan41/ui/dockwidget.py
|
"""sn_plan41/ui/dockwidget.py – Haupt-DockWidget für sn_plan41.
|
||||||
|
Verwaltet die Tabs für Linkvalidierung, Druck und Einstellungen.
|
||||||
|
"""
|
||||||
|
|
||||||
from sn_basis.ui.tabs.settings_tab import SettingsTab
|
from sn_basis.ui.tabs.settings_tab import SettingsTab
|
||||||
from sn_plan41.ui.tab_a_ui import TabA
|
from sn_plan41.ui.tab_a_ui import TabA
|
||||||
from sn_plan41.ui.tab_b_ui import TabB
|
from sn_basis.ui.tabs.print_tab import PrintTab
|
||||||
from sn_basis.ui.base_dockwidget import BaseDockWidget
|
from sn_basis.ui.base_dockwidget import BaseDockWidget
|
||||||
from sn_basis.functions.qt_wrapper import QTabWidget
|
from sn_basis.functions.qt_wrapper import QTabWidget
|
||||||
from sn_basis.functions.message_wrapper import error
|
from sn_basis.functions.message_wrapper import error
|
||||||
|
|
||||||
|
|
||||||
class DockWidget(BaseDockWidget):
|
class DockWidget(BaseDockWidget):
|
||||||
tabs = [TabA, TabB,SettingsTab]
|
"""Haupt-DockWidget für sn_plan41 mit Tab-Verwaltung.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
tabs: Liste von Tab-Klassen (TabA, PrintTab, SettingsTab)
|
||||||
|
pruefmanager: Prüfmanager-Instanz für Validierung
|
||||||
|
data_grabber: DataGrabber-Instanz für Fachdatenabruf
|
||||||
|
"""
|
||||||
|
|
||||||
|
tabs = [TabA, PrintTab, SettingsTab]
|
||||||
|
|
||||||
def __init__(self, parent=None, subtitle="", pruefmanager=None, data_grabber=None):
|
def __init__(self, parent=None, subtitle="", pruefmanager=None, data_grabber=None):
|
||||||
|
"""Initialisiert das DockWidget mit Services und Tabs.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
parent: Übergeordnetes Widget
|
||||||
|
subtitle: Untertitel des DockWidgets
|
||||||
|
pruefmanager: Prüfmanager-Instanz
|
||||||
|
data_grabber: DataGrabber-Instanz
|
||||||
|
"""
|
||||||
super().__init__(parent, subtitle)
|
super().__init__(parent, subtitle)
|
||||||
|
|
||||||
# Services als Attribute speichern
|
# Services als Attribute speichern
|
||||||
|
|||||||
-1191
File diff suppressed because it is too large
Load Diff
+221
-53
@@ -12,6 +12,7 @@ import os
|
|||||||
import datetime
|
import datetime
|
||||||
import json
|
import json
|
||||||
import tempfile
|
import tempfile
|
||||||
|
import html
|
||||||
|
|
||||||
from sn_basis.functions.qgiscore_wrapper import (
|
from sn_basis.functions.qgiscore_wrapper import (
|
||||||
QgsVectorFileWriter,
|
QgsVectorFileWriter,
|
||||||
@@ -63,9 +64,88 @@ class TabALogic:
|
|||||||
self.link_pruefer = link_pruefer
|
self.link_pruefer = link_pruefer
|
||||||
self.stil_pruefer = stil_pruefer
|
self.stil_pruefer = stil_pruefer
|
||||||
self.data_grabber: Optional[DataGrabber] = None
|
self.data_grabber: Optional[DataGrabber] = None
|
||||||
|
|
||||||
|
class _SilentPruefmanagerProxy:
|
||||||
|
"""Proxy, der Prüfergebnisse ohne UI-Meldungen verarbeitet."""
|
||||||
|
|
||||||
|
def __init__(self, outer: "TabALogic", fallback: Optional[Any]) -> None:
|
||||||
|
self._outer = outer
|
||||||
|
self._fallback = fallback
|
||||||
|
self.results: List[Any] = []
|
||||||
|
|
||||||
|
def verarbeite(self, ergebnis: Any) -> Any:
|
||||||
|
self.results.append(ergebnis)
|
||||||
|
ok = bool(getattr(ergebnis, "ok", False))
|
||||||
|
if not ok:
|
||||||
|
aktion = getattr(ergebnis, "aktion", "unbekannt")
|
||||||
|
meldung = getattr(ergebnis, "meldung", "")
|
||||||
|
self._outer._log(f"[SILENT-PRUEFMANAGER] {aktion}: {meldung}")
|
||||||
|
return ergebnis
|
||||||
|
|
||||||
|
def __getattr__(self, name: str) -> Any:
|
||||||
|
if self._fallback is not None:
|
||||||
|
return getattr(self._fallback, name)
|
||||||
|
raise AttributeError(name)
|
||||||
|
|
||||||
def _log(self, msg: str) -> None:
|
def _log(self, msg: str) -> None:
|
||||||
print(f"[TabALogic] {msg}")
|
print(f"[TabALogic] {msg}")
|
||||||
|
|
||||||
|
def _create_silent_pruefmanager(self) -> "TabALogic._SilentPruefmanagerProxy":
|
||||||
|
return TabALogic._SilentPruefmanagerProxy(self, self.pruefmanager)
|
||||||
|
|
||||||
|
def _extract_datenabruf_error_maps(self, datenabruf_results: List[Any]) -> Dict[str, Dict[str, str]]:
|
||||||
|
by_ident: Dict[str, str] = {}
|
||||||
|
by_thema: Dict[str, str] = {}
|
||||||
|
|
||||||
|
for result in datenabruf_results or []:
|
||||||
|
if not result:
|
||||||
|
continue
|
||||||
|
context = getattr(result, "kontext", None)
|
||||||
|
if not isinstance(context, dict):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if getattr(result, "aktion", None) == "url_nicht_erreichbar":
|
||||||
|
ident = str(context.get("ident") or "").strip()
|
||||||
|
thema = str(context.get("thema") or "").strip()
|
||||||
|
reason = str(context.get("error") or getattr(result, "meldung", "") or "unbekannter Fehler").strip()
|
||||||
|
if ident and reason:
|
||||||
|
by_ident[ident] = reason
|
||||||
|
if thema and reason:
|
||||||
|
by_thema[thema] = reason
|
||||||
|
|
||||||
|
if getattr(result, "aktion", None) == "datenabruf":
|
||||||
|
fehler_map = context.get("fehler")
|
||||||
|
if isinstance(fehler_map, dict):
|
||||||
|
for thema_key, value in fehler_map.items():
|
||||||
|
thema = str(thema_key or "").strip()
|
||||||
|
reason = str(value or "").strip()
|
||||||
|
if thema and reason and thema not in by_thema:
|
||||||
|
by_thema[thema] = reason
|
||||||
|
|
||||||
|
return {"by_ident": by_ident, "by_thema": by_thema}
|
||||||
|
|
||||||
|
def _zeige_verfahrensgebiet_hinweis(self) -> None:
|
||||||
|
if not self.pruefmanager:
|
||||||
|
return
|
||||||
|
show_hint = getattr(self.pruefmanager, "zeige_hinweis", None)
|
||||||
|
if callable(show_hint):
|
||||||
|
show_hint(
|
||||||
|
"Verfahrensgebiet fehlt",
|
||||||
|
"Kein gueltiger Verfahrensgebiet-Layer vorhanden. "
|
||||||
|
"Bitte zuerst in sn_verfahrensgebiet den Layer ueber 'Aus ALKIS laden' laden "
|
||||||
|
"oder im Dropdown einen gueltigen Layer auswaehlen.",
|
||||||
|
)
|
||||||
|
|
||||||
|
def _close_progress_dialog(self, progress: Optional[Any]) -> None:
|
||||||
|
if progress is None:
|
||||||
|
return
|
||||||
|
close = getattr(progress, "close", None)
|
||||||
|
if callable(close):
|
||||||
|
try:
|
||||||
|
close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
# -------------------------------
|
# -------------------------------
|
||||||
# Verfahrens-Datenbank (Pfad-Management)
|
# Verfahrens-Datenbank (Pfad-Management)
|
||||||
# -------------------------------
|
# -------------------------------
|
||||||
@@ -155,12 +235,6 @@ class TabALogic:
|
|||||||
# -------------------------------
|
# -------------------------------
|
||||||
# Verfahrensgebiet-Layer
|
# Verfahrensgebiet-Layer
|
||||||
# -------------------------------
|
# -------------------------------
|
||||||
|
|
||||||
def save_verfahrensgebiet_layer(self, layer: QgsVectorLayer) -> None:
|
|
||||||
"""Speichert die Verfahrensgebiet-Layer-ID, unter Annahme, dass der Layer prevalidiert ist."""
|
|
||||||
layer_id = layer.id() if layer is not None else ""
|
|
||||||
set_variable("verfahrensgebiet_layer", layer_id or "", scope="project")
|
|
||||||
|
|
||||||
def load_verfahrensgebiet_layer_id(self) -> Optional[str]:
|
def load_verfahrensgebiet_layer_id(self) -> Optional[str]:
|
||||||
value = get_variable("verfahrensgebiet_layer", scope="project")
|
value = get_variable("verfahrensgebiet_layer", scope="project")
|
||||||
return value or None
|
return value or None
|
||||||
@@ -217,6 +291,7 @@ class TabALogic:
|
|||||||
linkliste_final = self._resolve_linkliste(linkliste)
|
linkliste_final = self._resolve_linkliste(linkliste)
|
||||||
if linkliste_final is None:
|
if linkliste_final is None:
|
||||||
self._log("Linkliste kann nicht aufgelöst werden")
|
self._log("Linkliste kann nicht aufgelöst werden")
|
||||||
|
self._close_progress_dialog(progress)
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
self._log(f"Linkliste final: '{linkliste_final}'")
|
self._log(f"Linkliste final: '{linkliste_final}'")
|
||||||
@@ -224,6 +299,7 @@ class TabALogic:
|
|||||||
raumfilter_layer = self._resolve_raumfilter(raumfilter, final_pfad)
|
raumfilter_layer = self._resolve_raumfilter(raumfilter, final_pfad)
|
||||||
if raumfilter in ("Verfahrensgebiet", "Pufferlayer") and raumfilter_layer is None:
|
if raumfilter in ("Verfahrensgebiet", "Pufferlayer") and raumfilter_layer is None:
|
||||||
self._log(f"Raumfilter '{raumfilter}' nicht verfügbar")
|
self._log(f"Raumfilter '{raumfilter}' nicht verfügbar")
|
||||||
|
self._close_progress_dialog(progress)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# 4) Lade-Status initialisieren (funktioniert ab Bestätigung überschreiben/anhängen)
|
# 4) Lade-Status initialisieren (funktioniert ab Bestätigung überschreiben/anhängen)
|
||||||
@@ -236,7 +312,16 @@ class TabALogic:
|
|||||||
progress.set_label("Prüfe Eingaben...")
|
progress.set_label("Prüfe Eingaben...")
|
||||||
|
|
||||||
# 5) Daten aus Linkliste laden und prüfen
|
# 5) Daten aus Linkliste laden und prüfen
|
||||||
source_dict, grabber_summary = self.data_grabber.run(linkliste_final)
|
silent_pruefmanager = self._create_silent_pruefmanager()
|
||||||
|
local_data_grabber = DataGrabber(
|
||||||
|
silent_pruefmanager,
|
||||||
|
datei_pruefer_cls=self.data_grabber._datei_pruefer_cls,
|
||||||
|
link_pruefer=self.data_grabber.link_pruefer,
|
||||||
|
layer_pruefer=self.data_grabber.layer_pruefer,
|
||||||
|
stil_pruefer=self.data_grabber.stil_pruefer,
|
||||||
|
excel_importer_cls=self.data_grabber._excel_importer_cls,
|
||||||
|
)
|
||||||
|
source_dict, grabber_summary = local_data_grabber.run(linkliste_final)
|
||||||
self._log(f"DataGrabber: {grabber_summary.meldung} [{grabber_summary.aktion}]")
|
self._log(f"DataGrabber: {grabber_summary.meldung} [{grabber_summary.aktion}]")
|
||||||
|
|
||||||
# DEBUG: detaillierter Status
|
# DEBUG: detaillierter Status
|
||||||
@@ -249,6 +334,7 @@ class TabALogic:
|
|||||||
if not source_dict.get("rows"):
|
if not source_dict.get("rows"):
|
||||||
self._log("Keine validen Linkliste-Einträge für Datenabruf")
|
self._log("Keine validen Linkliste-Einträge für Datenabruf")
|
||||||
print("[TabALogic] ... STOP: rows:", len(source_dict.get("rows", [])))
|
print("[TabALogic] ... STOP: rows:", len(source_dict.get("rows", [])))
|
||||||
|
self._close_progress_dialog(progress)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
total_rows = len(source_dict.get("rows", []))
|
total_rows = len(source_dict.get("rows", []))
|
||||||
@@ -266,7 +352,8 @@ class TabALogic:
|
|||||||
self._log("Warnung: DataGrabber meldet fehlerhafte Zeilen, fahre mit Validierungsdaten fort")
|
self._log("Warnung: DataGrabber meldet fehlerhafte Zeilen, fahre mit Validierungsdaten fort")
|
||||||
|
|
||||||
# 5) Datenabruf (aus validierten Zeilen)
|
# 5) Datenabruf (aus validierten Zeilen)
|
||||||
datenabruf = Datenabruf(self.pruefmanager)
|
silent_pruefmanager = self._create_silent_pruefmanager()
|
||||||
|
datenabruf = Datenabruf(silent_pruefmanager)
|
||||||
result_dict, datenabruf_results = datenabruf.datenabruf(
|
result_dict, datenabruf_results = datenabruf.datenabruf(
|
||||||
result_dict=source_dict,
|
result_dict=source_dict,
|
||||||
raumfilter=raumfilter,
|
raumfilter=raumfilter,
|
||||||
@@ -296,7 +383,7 @@ class TabALogic:
|
|||||||
progress.set_label("Fachdaten laden abgeschlossen. Bitte OK klicken, um den Dialog zu schließen.")
|
progress.set_label("Fachdaten laden abgeschlossen. Bitte OK klicken, um den Dialog zu schließen.")
|
||||||
|
|
||||||
# 7) Log-Datei schreiben
|
# 7) Log-Datei schreiben
|
||||||
self._write_markdown_log(final_pfad, source_dict, pipeline_context, load_summary)
|
self._write_html_log(final_pfad, source_dict, pipeline_context, load_summary)
|
||||||
|
|
||||||
print("=" * 60 + "\n")
|
print("=" * 60 + "\n")
|
||||||
return pipeline_context
|
return pipeline_context
|
||||||
@@ -310,7 +397,15 @@ class TabALogic:
|
|||||||
aborted = False
|
aborted = False
|
||||||
if not rows:
|
if not rows:
|
||||||
self._log("Keine Dienste zum Laden")
|
self._log("Keine Dienste zum Laden")
|
||||||
return
|
return {
|
||||||
|
"row_count": 0,
|
||||||
|
"loaded_count": 0,
|
||||||
|
"skipped_outside": 0,
|
||||||
|
"aborted": False,
|
||||||
|
"row_stats": [],
|
||||||
|
"layer_call_status": {},
|
||||||
|
"raumfilter_name": pipeline_context.get("raumfilter_name", "unbekannt"),
|
||||||
|
}
|
||||||
|
|
||||||
final_pfad = pipeline_context.get("source") or ""
|
final_pfad = pipeline_context.get("source") or ""
|
||||||
use_datenschreiber = bool(final_pfad)
|
use_datenschreiber = bool(final_pfad)
|
||||||
@@ -340,7 +435,12 @@ class TabALogic:
|
|||||||
raumfilter_extent = raumfilter_layer.extent()
|
raumfilter_extent = raumfilter_layer.extent()
|
||||||
|
|
||||||
temp_layers: List[Any] = []
|
temp_layers: List[Any] = []
|
||||||
layer_loader = LayerLoader(self.pruefmanager, stil_pruefer=self.stil_pruefer, layer_pruefer=self.link_pruefer)
|
silent_pruefmanager = self._create_silent_pruefmanager()
|
||||||
|
layer_loader = LayerLoader(silent_pruefmanager, stil_pruefer=self.stil_pruefer, layer_pruefer=self.link_pruefer)
|
||||||
|
|
||||||
|
error_maps = self._extract_datenabruf_error_maps(pipeline_context.get("datenabruf_results", []))
|
||||||
|
error_by_ident = error_maps.get("by_ident", {})
|
||||||
|
error_by_thema = error_maps.get("by_thema", {})
|
||||||
|
|
||||||
# Statistiken für Log: Raumfilter-Info pro Dienst
|
# Statistiken für Log: Raumfilter-Info pro Dienst
|
||||||
row_stats: List[Dict[str, Any]] = []
|
row_stats: List[Dict[str, Any]] = []
|
||||||
@@ -357,6 +457,7 @@ class TabALogic:
|
|||||||
daten_map = (pipeline_context.get("result_dict") or {}).get("daten", {})
|
daten_map = (pipeline_context.get("result_dict") or {}).get("daten", {})
|
||||||
fetched_features = daten_map.get(ident, []) if isinstance(daten_map, dict) else []
|
fetched_features = daten_map.get(ident, []) if isinstance(daten_map, dict) else []
|
||||||
fetched_count = len(fetched_features) if isinstance(fetched_features, list) else None
|
fetched_count = len(fetched_features) if isinstance(fetched_features, list) else None
|
||||||
|
base_reason = error_by_ident.get(ident) or error_by_thema.get(thema)
|
||||||
|
|
||||||
if progress is not None:
|
if progress is not None:
|
||||||
progress.set_label(f"Lade Dienst {idx}/{total}: {thema}")
|
progress.set_label(f"Lade Dienst {idx}/{total}: {thema}")
|
||||||
@@ -385,6 +486,8 @@ class TabALogic:
|
|||||||
if not layer:
|
if not layer:
|
||||||
layer_call_status[thema] = "layer_nicht_ladbar"
|
layer_call_status[thema] = "layer_nicht_ladbar"
|
||||||
self._log(f"[DEBUG] Layeraufruf fehlgeschlagen: thema='{thema}'")
|
self._log(f"[DEBUG] Layeraufruf fehlgeschlagen: thema='{thema}'")
|
||||||
|
reason = base_reason or "Layer konnte nicht geladen werden (Dienst nicht erreichbar oder Link fehlerhaft)"
|
||||||
|
self._log(f"[DEBUG] Dienst nicht geladen: {thema} | Grund: {reason}")
|
||||||
row_stats.append({
|
row_stats.append({
|
||||||
"dienst": thema,
|
"dienst": thema,
|
||||||
"provider": provider,
|
"provider": provider,
|
||||||
@@ -394,7 +497,7 @@ class TabALogic:
|
|||||||
"total_features": None,
|
"total_features": None,
|
||||||
"filtered_features": None,
|
"filtered_features": None,
|
||||||
"status": "layer_nicht_ladbar",
|
"status": "layer_nicht_ladbar",
|
||||||
"raumfilter": raumfilter_name,
|
"reason": reason,
|
||||||
})
|
})
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -448,6 +551,7 @@ class TabALogic:
|
|||||||
layer_call_status[thema] = "raumfilter_ausserhalb"
|
layer_call_status[thema] = "raumfilter_ausserhalb"
|
||||||
self._log(f"Dienst {thema} ist außerhalb des Raumfilters")
|
self._log(f"Dienst {thema} ist außerhalb des Raumfilters")
|
||||||
skipped_outside += 1
|
skipped_outside += 1
|
||||||
|
reason = "Keine Objekte innerhalb des Raumfilters"
|
||||||
row_stats.append({
|
row_stats.append({
|
||||||
"dienst": thema,
|
"dienst": thema,
|
||||||
"provider": provider,
|
"provider": provider,
|
||||||
@@ -457,7 +561,7 @@ class TabALogic:
|
|||||||
"total_features": total_features,
|
"total_features": total_features,
|
||||||
"filtered_features": 0,
|
"filtered_features": 0,
|
||||||
"status": "außerhalb",
|
"status": "außerhalb",
|
||||||
"raumfilter": raumfilter_name,
|
"reason": reason,
|
||||||
})
|
})
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -473,7 +577,7 @@ class TabALogic:
|
|||||||
"total_features": total_features,
|
"total_features": total_features,
|
||||||
"filtered_features": filtered_features,
|
"filtered_features": filtered_features,
|
||||||
"status": "geladen",
|
"status": "geladen",
|
||||||
"raumfilter": raumfilter_name,
|
"reason": "geladen",
|
||||||
})
|
})
|
||||||
layer_call_status[thema] = "geladen"
|
layer_call_status[thema] = "geladen"
|
||||||
self._log(f"[DEBUG] Dienst geladen: thema='{thema}', provider='{provider}', filtered_features={filtered_features}")
|
self._log(f"[DEBUG] Dienst geladen: thema='{thema}', provider='{provider}', filtered_features={filtered_features}")
|
||||||
@@ -671,75 +775,118 @@ class TabALogic:
|
|||||||
self._log(f"[DEBUG] Fehler beim Erzeugen lokaler Featureschicht für {thema}: {exc}")
|
self._log(f"[DEBUG] Fehler beim Erzeugen lokaler Featureschicht für {thema}: {exc}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _write_markdown_log(
|
def _write_html_log(
|
||||||
self,
|
self,
|
||||||
final_pfad: str,
|
final_pfad: str,
|
||||||
source_dict: DataDict,
|
source_dict: DataDict,
|
||||||
pipeline_context: Dict[str, Any],
|
pipeline_context: Dict[str, Any],
|
||||||
load_summary: Dict[str, Any],
|
load_summary: Dict[str, Any],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Schreibt den Pipeline-Log (Markdown)."""
|
"""Schreibt den Pipeline-Log als HTML-Datei."""
|
||||||
lines = [
|
lines = [
|
||||||
"# Plan41 Fachdaten-Ladevorgang",
|
"<!doctype html>",
|
||||||
"",
|
"<html lang='de'>",
|
||||||
f"**Datum**: {datetime.datetime.now().isoformat()}",
|
"<head>",
|
||||||
f"**Verfahrens-DB**: {final_pfad or 'temporär'}",
|
" <meta charset='utf-8'>",
|
||||||
f"**Linkliste**: {pipeline_context.get('linkliste')}",
|
" <title>Plan41 Fachdaten-Ladevorgang</title>",
|
||||||
"",
|
" <style>",
|
||||||
"## Zusammenfassung",
|
" body { font-family: Arial, sans-serif; margin: 20px; color: #222; }",
|
||||||
"",
|
" h1, h2 { margin-bottom: 8px; }",
|
||||||
f"- **Zeilen gesamt**: {load_summary.get('row_count', 0)}",
|
" .meta p { margin: 4px 0; }",
|
||||||
f"- **Geladene Dienste**: {load_summary.get('loaded_count', 0)}",
|
" table { border-collapse: collapse; width: 100%; margin: 12px 0 24px; }",
|
||||||
f"- **Außerhalb Raumfilter**: {load_summary.get('skipped_outside', 0)}",
|
" th, td { border: 1px solid #ccc; padding: 6px 8px; text-align: left; vertical-align: top; }",
|
||||||
f"- **Abgebrochen**: {load_summary.get('aborted', False)}",
|
" th { background: #f2f2f2; }",
|
||||||
f"- **Raumfilter**: {load_summary.get('raumfilter_name', 'unbekannt')}",
|
" .ok { color: #1b5e20; font-weight: 600; }",
|
||||||
f"- **Raumfilter-Typ**: {pipeline_context.get('raumfilter_name', 'unbekannt')}",
|
" .warn { color: #8d6e00; font-weight: 600; }",
|
||||||
"",
|
" .err { color: #b71c1c; font-weight: 600; }",
|
||||||
"## Dienstliste",
|
" </style>",
|
||||||
"",
|
"</head>",
|
||||||
"| Dienst | Provider | Linkadresse | Aufrufstatus | Ergebnisstatus |",
|
"<body>",
|
||||||
"|---|---|---|---|---|",
|
"<h1>Plan41 Fachdaten-Ladevorgang</h1>",
|
||||||
|
"<div class='meta'>",
|
||||||
|
f" <p><strong>Datum:</strong> {datetime.datetime.now().isoformat()}</p>",
|
||||||
|
f" <p><strong>Verfahrens-DB:</strong> {final_pfad or 'temporär'}</p>",
|
||||||
|
f" <p><strong>Linkliste:</strong> {pipeline_context.get('linkliste')}</p>",
|
||||||
|
f" <p><strong>Raumfilter:</strong> {load_summary.get('raumfilter_name', 'unbekannt')}</p>",
|
||||||
|
"</div>",
|
||||||
|
"<h2>Zusammenfassung</h2>",
|
||||||
|
"<ul>",
|
||||||
|
f" <li>Zeilen gesamt: {load_summary.get('row_count', 0)}</li>",
|
||||||
|
f" <li>Geladene Dienste: {load_summary.get('loaded_count', 0)}</li>",
|
||||||
|
f" <li>Außerhalb Raumfilter: {load_summary.get('skipped_outside', 0)}</li>",
|
||||||
|
f" <li>Abgebrochen: {load_summary.get('aborted', False)}</li>",
|
||||||
|
"</ul>",
|
||||||
|
"<h2>Dienstliste</h2>",
|
||||||
|
"<table>",
|
||||||
|
"<thead><tr><th>Dienst</th><th>Provider</th><th>Linkadresse</th><th>Aufrufstatus</th></tr></thead>",
|
||||||
|
"<tbody>",
|
||||||
]
|
]
|
||||||
|
|
||||||
status_by_dienst = {
|
|
||||||
str(stat.get("dienst", "")): str(stat.get("status", "n/a"))
|
|
||||||
for stat in load_summary.get("row_stats", [])
|
|
||||||
}
|
|
||||||
aufrufstatus_by_dienst = {
|
aufrufstatus_by_dienst = {
|
||||||
str(key): str(value)
|
str(key): str(value)
|
||||||
for key, value in (load_summary.get("layer_call_status", {}) or {}).items()
|
for key, value in (load_summary.get("layer_call_status", {}) or {}).items()
|
||||||
}
|
}
|
||||||
|
aufrufstatus_labels = {
|
||||||
|
"layeraufruf_start": "Layer-Aufruf gestartet",
|
||||||
|
"layeraufruf_ok": "Layer-Aufruf erfolgreich",
|
||||||
|
"geladen": "Geladen",
|
||||||
|
"layer_nicht_ladbar": "Nicht ladbar",
|
||||||
|
"raumfilter_ausserhalb": "Außerhalb Raumfilter",
|
||||||
|
"abbruch_vor_layeraufruf": "Abbruch vor Layer-Aufruf",
|
||||||
|
"abbruch_nach_layeraufruf": "Abbruch nach Layer-Aufruf",
|
||||||
|
"abbruch_nach_raumfilter": "Abbruch nach Raumfilter",
|
||||||
|
"nicht_aufgerufen": "Nicht aufgerufen",
|
||||||
|
}
|
||||||
|
status_labels = {
|
||||||
|
"geladen": "Geladen",
|
||||||
|
"außerhalb": "Außerhalb Raumfilter",
|
||||||
|
"layer_nicht_ladbar": "Nicht ladbar",
|
||||||
|
}
|
||||||
|
|
||||||
for row in source_dict.get('rows', []):
|
for row in source_dict.get('rows', []):
|
||||||
dienst = row.get('Inhalt') or row.get('ident') or ''
|
dienst = row.get('Inhalt') or row.get('ident') or ''
|
||||||
provider = row.get('Provider') or ''
|
provider = row.get('Provider') or ''
|
||||||
link = row.get('Link') or ''
|
link = row.get('Link') or ''
|
||||||
aufrufstatus = aufrufstatus_by_dienst.get(str(dienst), "nicht_aufgerufen")
|
aufrufstatus = aufrufstatus_by_dienst.get(str(dienst), "nicht_aufgerufen")
|
||||||
ergebnisstatus = status_by_dienst.get(str(dienst), "n/a")
|
aufrufstatus_text = aufrufstatus_labels.get(str(aufrufstatus), str(aufrufstatus))
|
||||||
lines.append(f"| {dienst} | {provider} | {link} | {aufrufstatus} | {ergebnisstatus} |")
|
lines.append(
|
||||||
|
f"<tr><td>{html.escape(str(dienst))}</td><td>{html.escape(str(provider))}</td><td>{html.escape(str(link))}</td><td>{html.escape(str(aufrufstatus_text))}</td></tr>"
|
||||||
|
)
|
||||||
|
|
||||||
lines.extend([
|
lines.extend([
|
||||||
"",
|
"</tbody>",
|
||||||
"## Raumfilter-Statistik",
|
"</table>",
|
||||||
"",
|
"<h2>Raumfilter-Statistik</h2>",
|
||||||
"| Dienst | Provider | Linkadresse | Datenabruf-Objekte | Gesamt-Objekte | Gefilterte Objekte | Raumfilter | Status |",
|
"<table>",
|
||||||
"|---|---|---|---|---|---|---|---|",
|
"<thead><tr><th>Dienst</th><th>Datenabruf-Objekte</th><th>Gesamt-Objekte</th><th>Gefilterte Objekte</th><th>Status</th></tr></thead>",
|
||||||
|
"<tbody>",
|
||||||
])
|
])
|
||||||
|
|
||||||
for stat in load_summary.get('row_stats', []):
|
for stat in load_summary.get('row_stats', []):
|
||||||
|
status = str(stat.get('status', 'n/a'))
|
||||||
|
status_label = status_labels.get(status, status)
|
||||||
|
reason = str(stat.get('reason', '') or '')
|
||||||
|
status_text = status_label if not reason else f"{status_label}: {reason}"
|
||||||
lines.append(
|
lines.append(
|
||||||
f"| {stat.get('dienst', '')} | {stat.get('provider', '')} | {stat.get('link', '')} | {stat.get('datenabruf_features', 'n/a')} | {stat.get('total_features', 'n/a')} | {stat.get('filtered_features', 'n/a')} | {stat.get('raumfilter', '')} | {stat.get('status', 'n/a')} |"
|
f"<tr><td>{html.escape(str(stat.get('dienst', '')))}</td><td>{html.escape(str(stat.get('datenabruf_features', 'n/a')))}</td><td>{html.escape(str(stat.get('total_features', 'n/a')))}</td><td>{html.escape(str(stat.get('filtered_features', 'n/a')))}</td><td>{html.escape(status_text)}</td></tr>"
|
||||||
)
|
)
|
||||||
|
|
||||||
markdown = "\n".join(lines)
|
lines.extend([
|
||||||
|
"</tbody>",
|
||||||
|
"</table>",
|
||||||
|
"</body>",
|
||||||
|
"</html>",
|
||||||
|
])
|
||||||
|
|
||||||
|
html_text = "\n".join(lines)
|
||||||
|
|
||||||
if final_pfad:
|
if final_pfad:
|
||||||
log_dir = os.path.dirname(final_pfad)
|
log_dir = os.path.dirname(final_pfad)
|
||||||
os.makedirs(log_dir, exist_ok=True)
|
os.makedirs(log_dir, exist_ok=True)
|
||||||
log_file = os.path.join(log_dir, "plan41_lade_log.md")
|
log_file = os.path.join(log_dir, "plan41_lade_log.html")
|
||||||
try:
|
try:
|
||||||
with open(log_file, "w", encoding="utf-8") as fh:
|
with open(log_file, "w", encoding="utf-8") as fh:
|
||||||
fh.write(markdown)
|
fh.write(html_text)
|
||||||
|
|
||||||
self.pruefmanager.verarbeite(
|
self.pruefmanager.verarbeite(
|
||||||
pruef_ergebnis(
|
pruef_ergebnis(
|
||||||
@@ -754,15 +901,33 @@ class TabALogic:
|
|||||||
self.pruefmanager.verarbeite(
|
self.pruefmanager.verarbeite(
|
||||||
pruef_ergebnis(
|
pruef_ergebnis(
|
||||||
ok=False,
|
ok=False,
|
||||||
meldung=f"Fehler beim Schreiben des Logle (md): {exc}",
|
meldung=f"Fehler beim Schreiben des Logs (html): {exc}",
|
||||||
aktion="log_schreiben_fehlgeschlagen",
|
aktion="log_schreiben_fehlgeschlagen",
|
||||||
kontext={"error": str(exc)},
|
kontext={"error": str(exc)},
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
warning("Lade-Log", f"Konnte Datei nicht schreiben: {exc}", duration=10)
|
warning("Lade-Log", f"Konnte Datei nicht schreiben: {exc}", duration=10)
|
||||||
else:
|
else:
|
||||||
# temporärer Modus: nur anzeigen
|
try:
|
||||||
info("Lade-Log (temporär)", markdown, duration=20)
|
tmp_dir = os.path.join(tempfile.gettempdir(), "sn_plan41")
|
||||||
|
os.makedirs(tmp_dir, exist_ok=True)
|
||||||
|
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
|
log_file = os.path.join(tmp_dir, f"plan41_lade_log_{timestamp}.html")
|
||||||
|
|
||||||
|
with open(log_file, "w", encoding="utf-8") as fh:
|
||||||
|
fh.write(html_text)
|
||||||
|
|
||||||
|
self._log(f"Lade-Log (temporär) gespeichert: {log_file}")
|
||||||
|
|
||||||
|
if hasattr(os, "startfile"):
|
||||||
|
try:
|
||||||
|
os.startfile(log_file)
|
||||||
|
except Exception as exc:
|
||||||
|
self._log(f"Konnte temporäre Log-Datei nicht öffnen: {exc}")
|
||||||
|
|
||||||
|
info("Lade-Log (temporär)", f"Lade-Protokoll gespeichert: {log_file}", duration=20)
|
||||||
|
except Exception as exc:
|
||||||
|
warning("Lade-Log", f"Konnte temporäre Log-Datei nicht schreiben: {exc}", duration=10)
|
||||||
|
|
||||||
|
|
||||||
def _clone_layer_with_extent(self, layer: QgsVectorLayer, extent, thema: str) -> QgsVectorLayer | None:
|
def _clone_layer_with_extent(self, layer: QgsVectorLayer, extent, thema: str) -> QgsVectorLayer | None:
|
||||||
@@ -875,15 +1040,18 @@ class TabALogic:
|
|||||||
|
|
||||||
if not layer_id:
|
if not layer_id:
|
||||||
self._log("❌ Keine Layer-ID gespeichert")
|
self._log("❌ Keine Layer-ID gespeichert")
|
||||||
|
self._zeige_verfahrensgebiet_hinweis()
|
||||||
return None
|
return None
|
||||||
|
|
||||||
layer = QgsProject.instance().mapLayer(layer_id)
|
layer = QgsProject.instance().mapLayer(layer_id)
|
||||||
if not layer:
|
if not layer:
|
||||||
self._log("❌ Layer-ID existiert nicht im Projekt")
|
self._log("❌ Layer-ID existiert nicht im Projekt")
|
||||||
|
self._zeige_verfahrensgebiet_hinweis()
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if not self.is_valid_verfahrensgebiet_layer(layer):
|
if not self.is_valid_verfahrensgebiet_layer(layer):
|
||||||
self._log("❌ Layer ist kein gültiger Vektorlayer")
|
self._log("❌ Layer ist kein gültiger Vektorlayer")
|
||||||
|
self._zeige_verfahrensgebiet_hinweis()
|
||||||
return None
|
return None
|
||||||
|
|
||||||
self._log(f"Verfahrensgebiet-Layer OK: '{layer.name()}'")
|
self._log(f"Verfahrensgebiet-Layer OK: '{layer.name()}'")
|
||||||
|
|||||||
+366
-51
@@ -9,7 +9,9 @@ from typing import Optional
|
|||||||
from sn_basis.functions.qt_wrapper import (
|
from sn_basis.functions.qt_wrapper import (
|
||||||
QWidget,
|
QWidget,
|
||||||
QVBoxLayout,
|
QVBoxLayout,
|
||||||
|
QHBoxLayout,
|
||||||
QLabel,
|
QLabel,
|
||||||
|
QLineEdit,
|
||||||
QPushButton,
|
QPushButton,
|
||||||
QToolButton,
|
QToolButton,
|
||||||
QFileDialog,
|
QFileDialog,
|
||||||
@@ -32,7 +34,8 @@ from sn_basis.modules.DataGrabber import DataGrabber
|
|||||||
from sn_basis.modules.Dateipruefer import Dateipruefer
|
from sn_basis.modules.Dateipruefer import Dateipruefer
|
||||||
from sn_plan41.ui.tab_a_logic import TabALogic
|
from sn_plan41.ui.tab_a_logic import TabALogic
|
||||||
from sn_basis.modules.linkpruefer import Linkpruefer
|
from sn_basis.modules.linkpruefer import Linkpruefer
|
||||||
from sn_basis.modules.stilpruefer import Stilpruefer
|
from sn_basis.modules.stilpruefer import Stilpruefer
|
||||||
|
from sn_plan41.modules.vln_api_logic import VlnApiLogic
|
||||||
|
|
||||||
# Konstanten
|
# Konstanten
|
||||||
RAUMFILTER_VAR = "Raumfilter"
|
RAUMFILTER_VAR = "Raumfilter"
|
||||||
@@ -78,7 +81,11 @@ class TabA(QWidget):
|
|||||||
|
|
||||||
# UI-Referenzen
|
# UI-Referenzen
|
||||||
self._raumfilter_combo: Optional[QComboBox] = None
|
self._raumfilter_combo: Optional[QComboBox] = None
|
||||||
|
|
||||||
|
# VLN-API
|
||||||
|
self.vln_logic: Optional[VlnApiLogic] = None
|
||||||
|
self._vln_restoring: bool = False
|
||||||
|
|
||||||
self._build_ui()
|
self._build_ui()
|
||||||
self._restore_state()
|
self._restore_state()
|
||||||
|
|
||||||
@@ -92,14 +99,17 @@ class TabA(QWidget):
|
|||||||
self.pruefmanager = pruefmanager
|
self.pruefmanager = pruefmanager
|
||||||
self.data_grabber = data_grabber
|
self.data_grabber = data_grabber
|
||||||
self.logic = TabALogic(
|
self.logic = TabALogic(
|
||||||
pruefmanager=self.pruefmanager,
|
pruefmanager=self.pruefmanager,
|
||||||
link_pruefer=Linkpruefer(),
|
link_pruefer=Linkpruefer(),
|
||||||
stil_pruefer=Stilpruefer(),
|
stil_pruefer=Stilpruefer(),
|
||||||
)
|
)
|
||||||
|
|
||||||
# DataGrabber in die Logik injizieren
|
|
||||||
self.logic.data_grabber = self.data_grabber
|
self.logic.data_grabber = self.data_grabber
|
||||||
|
|
||||||
|
# VLN-API-Logik initialisieren
|
||||||
|
self.vln_logic = VlnApiLogic(self.pruefmanager)
|
||||||
|
self._update_vln_ui_state()
|
||||||
|
self._restore_vln_state()
|
||||||
|
|
||||||
def _build_ui(self) -> None:
|
def _build_ui(self) -> None:
|
||||||
"""Erstellt die komplette UI-Hierarchie mit allen Gruppen."""
|
"""Erstellt die komplette UI-Hierarchie mit allen Gruppen."""
|
||||||
main_layout = QVBoxLayout()
|
main_layout = QVBoxLayout()
|
||||||
@@ -166,16 +176,7 @@ class TabA(QWidget):
|
|||||||
self.optional_content.setVisible(False)
|
self.optional_content.setVisible(False)
|
||||||
main_layout.addWidget(self.optional_content)
|
main_layout.addWidget(self.optional_content)
|
||||||
|
|
||||||
# === LAYER-AUSWAHL + RAUMFILTER ===
|
# === RAUMFILTER ===
|
||||||
layer_label = QLabel("Verfahrensgebiet-Layer auswählen")
|
|
||||||
layer_label.setStyleSheet("font-weight: bold; margin-top: 6px;")
|
|
||||||
main_layout.addWidget(layer_label)
|
|
||||||
|
|
||||||
self.layer_combo = QgsMapLayerComboBox()
|
|
||||||
self.layer_combo.setFilters(QgsMapLayerProxyModel.VectorLayer)
|
|
||||||
self.layer_combo.layerChanged.connect(self._on_layer_changed)
|
|
||||||
main_layout.addWidget(self.layer_combo)
|
|
||||||
|
|
||||||
main_layout.addWidget(QLabel("Raumfilter"))
|
main_layout.addWidget(QLabel("Raumfilter"))
|
||||||
self._raumfilter_combo = QComboBox(self)
|
self._raumfilter_combo = QComboBox(self)
|
||||||
self._raumfilter_combo.setToolTip("Wählt die räumliche Bezugsfläche für die Datenextraktion.")
|
self._raumfilter_combo.setToolTip("Wählt die räumliche Bezugsfläche für die Datenextraktion.")
|
||||||
@@ -189,7 +190,84 @@ class TabA(QWidget):
|
|||||||
self.btn_pipeline.clicked.connect(self._on_load_fachdaten)
|
self.btn_pipeline.clicked.connect(self._on_load_fachdaten)
|
||||||
main_layout.addWidget(self.btn_pipeline)
|
main_layout.addWidget(self.btn_pipeline)
|
||||||
|
|
||||||
|
# === VLN API / PLAN 41 ===
|
||||||
|
self._vln_group_button = QToolButton()
|
||||||
|
self._vln_group_button.setText("VLN API / Plan 41")
|
||||||
|
self._vln_group_button.setCheckable(True)
|
||||||
|
self._vln_group_button.setChecked(False)
|
||||||
|
self._vln_group_button.setToolButtonStyle(ToolButtonTextBesideIcon)
|
||||||
|
self._vln_group_button.setArrowType(ArrowRight)
|
||||||
|
self._vln_group_button.setStyleSheet("font-weight: bold; margin-top: 8px;")
|
||||||
|
self._vln_group_button.toggled.connect(self._toggle_vln_group)
|
||||||
|
main_layout.addWidget(self._vln_group_button)
|
||||||
|
|
||||||
|
self._vln_group_content = QWidget()
|
||||||
|
self._vln_group_content.setSizePolicy(SizePolicyPreferred, SizePolicyMaximum)
|
||||||
|
vln_layout = QVBoxLayout()
|
||||||
|
vln_layout.setSpacing(4)
|
||||||
|
vln_layout.setContentsMargins(10, 4, 4, 8)
|
||||||
|
|
||||||
|
# Status
|
||||||
|
self._vln_status_label = QLabel("Nicht angemeldet")
|
||||||
|
self._vln_status_label.setStyleSheet("color: gray; font-style: italic;")
|
||||||
|
vln_layout.addWidget(self._vln_status_label)
|
||||||
|
|
||||||
|
# E-Mail und Passwort
|
||||||
|
vln_layout.addWidget(QLabel("E-Mail:"))
|
||||||
|
self._vln_mail_edit = QLineEdit()
|
||||||
|
self._vln_mail_edit.setPlaceholderText("mail@beispiel.de")
|
||||||
|
vln_layout.addWidget(self._vln_mail_edit)
|
||||||
|
|
||||||
|
vln_layout.addWidget(QLabel("Passwort:"))
|
||||||
|
self._vln_pass_edit = QLineEdit()
|
||||||
|
try:
|
||||||
|
self._vln_pass_edit.setEchoMode(QLineEdit.EchoMode.Password)
|
||||||
|
except AttributeError:
|
||||||
|
self._vln_pass_edit.setEchoMode(QLineEdit.Password)
|
||||||
|
vln_layout.addWidget(self._vln_pass_edit)
|
||||||
|
|
||||||
|
# Login / Logout Buttons
|
||||||
|
btn_row = QHBoxLayout()
|
||||||
|
self._btn_vln_login = QPushButton("Anmelden")
|
||||||
|
self._btn_vln_login.clicked.connect(self._on_vln_login)
|
||||||
|
self._btn_vln_logout = QPushButton("Abmelden")
|
||||||
|
self._btn_vln_logout.clicked.connect(self._on_vln_logout)
|
||||||
|
btn_row.addWidget(self._btn_vln_login)
|
||||||
|
btn_row.addWidget(self._btn_vln_logout)
|
||||||
|
vln_layout.addLayout(btn_row)
|
||||||
|
|
||||||
|
# Verfahren
|
||||||
|
vln_layout.addWidget(QLabel("Verfahren:"))
|
||||||
|
self._vln_verfahren_combo = QComboBox()
|
||||||
|
self._vln_verfahren_combo.setToolTip("Verfahren (Teilnehmergemeinschaft) wählen")
|
||||||
|
self._vln_verfahren_combo.setEnabled(False)
|
||||||
|
self._vln_verfahren_combo.currentIndexChanged.connect(
|
||||||
|
self._on_vln_verfahren_changed
|
||||||
|
)
|
||||||
|
vln_layout.addWidget(self._vln_verfahren_combo)
|
||||||
|
|
||||||
|
# Plan 41 laden
|
||||||
|
self._btn_p41_laden = QPushButton("Plan 41 (Wege- und Gewässerplan) laden")
|
||||||
|
self._btn_p41_laden.setToolTip(
|
||||||
|
"Lädt Flächen- und Linienlayer für das gewählte Verfahren vom Server"
|
||||||
|
)
|
||||||
|
self._btn_p41_laden.setEnabled(False)
|
||||||
|
self._btn_p41_laden.clicked.connect(self._on_p41_laden)
|
||||||
|
vln_layout.addWidget(self._btn_p41_laden)
|
||||||
|
|
||||||
|
# Upload
|
||||||
|
self._btn_upload = QPushButton("Aktiven Layer hochladen")
|
||||||
|
self._btn_upload.setToolTip(
|
||||||
|
"Lädt alle Plan-41-Layer des aktiven Verfahrens zum Server hoch"
|
||||||
|
)
|
||||||
|
self._btn_upload.setEnabled(False)
|
||||||
|
self._btn_upload.clicked.connect(self._on_upload_active)
|
||||||
|
vln_layout.addWidget(self._btn_upload)
|
||||||
|
|
||||||
|
self._vln_group_content.setLayout(vln_layout)
|
||||||
|
self._vln_group_content.setVisible(False)
|
||||||
|
main_layout.addWidget(self._vln_group_content)
|
||||||
|
|
||||||
main_layout.addStretch(1)
|
main_layout.addStretch(1)
|
||||||
self.setLayout(main_layout)
|
self.setLayout(main_layout)
|
||||||
|
|
||||||
@@ -213,21 +291,281 @@ class TabA(QWidget):
|
|||||||
self.lokale_linkliste = link
|
self.lokale_linkliste = link
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
# Raumfilter (schon im _build_ui behandelt)
|
||||||
|
|
||||||
# Layer
|
def _restore_vln_state(self) -> None:
|
||||||
|
"""Stellt die zuletzt gewählte VKZ aus der Projektdatei wieder her."""
|
||||||
|
if self.vln_logic is None or not self.vln_logic.is_authenticated:
|
||||||
|
return
|
||||||
try:
|
try:
|
||||||
layer_id = get_variable("tab_a_layer_id", scope="project")
|
self._populate_verfahren_combo(restore_vkz=True)
|
||||||
if layer_id:
|
|
||||||
layer = QgsProject.instance().mapLayer(layer_id)
|
|
||||||
if layer and self.layer_combo:
|
|
||||||
self.layer_combo.setLayer(layer)
|
|
||||||
self._pufferlayer = layer
|
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Raumfilter (schon im _build_ui behandelt)
|
|
||||||
|
|
||||||
# === UI CALLBACKS ===
|
# === UI CALLBACKS ===
|
||||||
|
|
||||||
|
def _toggle_vln_group(self, checked: bool) -> None:
|
||||||
|
self._vln_group_button.setArrowType(ArrowDown if checked else ArrowRight)
|
||||||
|
self._vln_group_content.setVisible(checked)
|
||||||
|
|
||||||
|
def _update_vln_ui_state(self) -> None:
|
||||||
|
"""Passt alle VLN-Controls an den aktuellen Authentifizierungsstatus an."""
|
||||||
|
if self.vln_logic is None:
|
||||||
|
authenticated = False
|
||||||
|
available = False
|
||||||
|
else:
|
||||||
|
authenticated = self.vln_logic.is_authenticated
|
||||||
|
available = self.vln_logic.is_available()
|
||||||
|
|
||||||
|
if not available:
|
||||||
|
self._vln_status_label.setText(
|
||||||
|
"Plugin 'vln_karten' nicht installiert"
|
||||||
|
)
|
||||||
|
self._vln_status_label.setStyleSheet("color: red; font-style: italic;")
|
||||||
|
self._btn_vln_login.setEnabled(False)
|
||||||
|
elif authenticated:
|
||||||
|
mail = (self.vln_logic.mail or "API-Key") if self.vln_logic else ""
|
||||||
|
self._vln_status_label.setText("Angemeldet: %s" % mail)
|
||||||
|
self._vln_status_label.setStyleSheet(
|
||||||
|
"color: green; font-style: italic;"
|
||||||
|
)
|
||||||
|
self._btn_vln_login.setEnabled(False)
|
||||||
|
else:
|
||||||
|
self._vln_status_label.setText("Nicht angemeldet")
|
||||||
|
self._vln_status_label.setStyleSheet("color: gray; font-style: italic;")
|
||||||
|
self._btn_vln_login.setEnabled(True)
|
||||||
|
|
||||||
|
self._btn_vln_logout.setEnabled(authenticated)
|
||||||
|
self._vln_verfahren_combo.setEnabled(authenticated)
|
||||||
|
vkz_chosen = (
|
||||||
|
authenticated
|
||||||
|
and self._vln_verfahren_combo.currentData() is not None
|
||||||
|
)
|
||||||
|
self._btn_p41_laden.setEnabled(vkz_chosen)
|
||||||
|
self._btn_upload.setEnabled(authenticated)
|
||||||
|
|
||||||
|
def _populate_verfahren_combo(self, restore_vkz: bool = False) -> None:
|
||||||
|
"""Füllt die Verfahren-Combo und wählt ggf. die gespeicherte VKZ."""
|
||||||
|
if self.vln_logic is None:
|
||||||
|
return
|
||||||
|
verfahren = self.vln_logic.get_verfahren()
|
||||||
|
self._vln_restoring = True
|
||||||
|
self._vln_verfahren_combo.clear()
|
||||||
|
self._vln_verfahren_combo.addItem("— Verfahren wählen —", None)
|
||||||
|
for v in verfahren:
|
||||||
|
label = (
|
||||||
|
"%s — %s" % (v["vkz"], v["name"])
|
||||||
|
if v["name"]
|
||||||
|
else v["vkz"]
|
||||||
|
)
|
||||||
|
self._vln_verfahren_combo.addItem(label, v["vkz"])
|
||||||
|
self._vln_restoring = False
|
||||||
|
|
||||||
|
if restore_vkz:
|
||||||
|
stored_vkz = self.vln_logic.load_stored_vkz()
|
||||||
|
if stored_vkz:
|
||||||
|
idx = self._vln_verfahren_combo.findData(stored_vkz)
|
||||||
|
if idx >= 0:
|
||||||
|
self._vln_verfahren_combo.setCurrentIndex(idx)
|
||||||
|
|
||||||
|
self._vln_verfahren_combo.setEnabled(True)
|
||||||
|
self._update_vln_ui_state()
|
||||||
|
|
||||||
|
def _on_vln_login(self) -> None:
|
||||||
|
"""Führt den API-Login durch."""
|
||||||
|
if self.vln_logic is None:
|
||||||
|
return
|
||||||
|
mail = self._vln_mail_edit.text().strip()
|
||||||
|
password = self._vln_pass_edit.text()
|
||||||
|
if not mail or not password:
|
||||||
|
QMessageBox.warning(
|
||||||
|
self,
|
||||||
|
"Anmeldung",
|
||||||
|
"Bitte E-Mail-Adresse und Passwort eingeben.",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
self.vln_logic.login(mail, password)
|
||||||
|
except Exception as exc:
|
||||||
|
QMessageBox.warning(self, "Anmeldung fehlgeschlagen", str(exc))
|
||||||
|
return
|
||||||
|
|
||||||
|
self._vln_pass_edit.clear()
|
||||||
|
try:
|
||||||
|
self._populate_verfahren_combo(restore_vkz=True)
|
||||||
|
except Exception as exc:
|
||||||
|
QMessageBox.warning(
|
||||||
|
self,
|
||||||
|
"Verfahrensliste",
|
||||||
|
"Anmeldung erfolgreich, aber Verfahrensliste konnte nicht "
|
||||||
|
"geladen werden: %s" % exc,
|
||||||
|
)
|
||||||
|
self._update_vln_ui_state()
|
||||||
|
|
||||||
|
def _on_vln_logout(self) -> None:
|
||||||
|
"""Meldet den Benutzer lokal ab."""
|
||||||
|
if self.vln_logic is None:
|
||||||
|
return
|
||||||
|
self.vln_logic.logout()
|
||||||
|
self._vln_restoring = True
|
||||||
|
self._vln_verfahren_combo.clear()
|
||||||
|
self._vln_verfahren_combo.setEnabled(False)
|
||||||
|
self._vln_restoring = False
|
||||||
|
self._update_vln_ui_state()
|
||||||
|
|
||||||
|
def _on_vln_verfahren_changed(self, _index: int) -> None:
|
||||||
|
"""Persistiert die gewählte VKZ."""
|
||||||
|
if self._vln_restoring or self.vln_logic is None:
|
||||||
|
return
|
||||||
|
vkz = self._vln_verfahren_combo.currentData()
|
||||||
|
self.vln_logic.save_vkz(vkz)
|
||||||
|
self._update_vln_ui_state()
|
||||||
|
|
||||||
|
def _on_p41_laden(self) -> None:
|
||||||
|
"""Lädt den Plan-41-Datensatz für die gewählte VKZ."""
|
||||||
|
if self.vln_logic is None:
|
||||||
|
return
|
||||||
|
vkz = self._vln_verfahren_combo.currentData()
|
||||||
|
if not vkz:
|
||||||
|
QMessageBox.warning(
|
||||||
|
self, "Plan 41 laden", "Bitte zuerst ein Verfahren wählen."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Bereits geladene Layer prüfen
|
||||||
|
existing = self.vln_logic.get_p41_layers(vkz)
|
||||||
|
if existing:
|
||||||
|
try:
|
||||||
|
answer = QMessageBox.question(
|
||||||
|
self,
|
||||||
|
"Layer ersetzen",
|
||||||
|
"Plan 41 für VKZ %s ist bereits geladen.\n"
|
||||||
|
"Beim Neuladen werden die bestehenden Layer ersetzt. Fortfahren?"
|
||||||
|
% vkz,
|
||||||
|
)
|
||||||
|
# Qt6: StandardButton.Yes, Qt5: QMessageBox.Yes
|
||||||
|
yes = getattr(
|
||||||
|
getattr(QMessageBox, "StandardButton", QMessageBox),
|
||||||
|
"Yes",
|
||||||
|
QMessageBox.Yes if hasattr(QMessageBox, "Yes") else None,
|
||||||
|
)
|
||||||
|
if answer != yes:
|
||||||
|
return
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self.vln_logic.remove_p41_layers(vkz)
|
||||||
|
|
||||||
|
try:
|
||||||
|
layers = self.vln_logic.load_p41(vkz)
|
||||||
|
except Exception as exc:
|
||||||
|
# AuthError: Sitzung abgelaufen
|
||||||
|
try:
|
||||||
|
from sn_plan41.modules.vln_api_client import AuthError as _AuthError
|
||||||
|
if isinstance(exc, _AuthError):
|
||||||
|
self.vln_logic.handle_session_expired()
|
||||||
|
self._on_vln_logout()
|
||||||
|
QMessageBox.warning(
|
||||||
|
self,
|
||||||
|
"Sitzung abgelaufen",
|
||||||
|
"Der API-Key ist nicht mehr gültig — bitte neu anmelden.",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
QMessageBox.warning(self, "Laden fehlgeschlagen", str(exc))
|
||||||
|
return
|
||||||
|
|
||||||
|
total = sum(l.featureCount() for l in layers)
|
||||||
|
detail = ", ".join(
|
||||||
|
"%s: %d" % (l.name(), l.featureCount()) for l in layers
|
||||||
|
)
|
||||||
|
if total == 0:
|
||||||
|
QMessageBox.warning(
|
||||||
|
self,
|
||||||
|
"Plan 41 laden",
|
||||||
|
"Keine Objekte vorhanden — ein leerer Layer wurde angelegt.",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
from sn_basis.functions.message_wrapper import info
|
||||||
|
info(
|
||||||
|
"Plan 41 geladen",
|
||||||
|
"%d Layer geladen (%s)." % (len(layers), detail),
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _on_upload_active(self) -> None:
|
||||||
|
"""Lädt den aktiven Layer (und Geschwister-Layer) zum Server hoch."""
|
||||||
|
if self.vln_logic is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Zusammenfassung für Bestaetigungs-Dialog ermitteln
|
||||||
|
try:
|
||||||
|
from sn_basis.functions.qgisui_wrapper import iface as _iface
|
||||||
|
active_layer = _iface.activeLayer() if _iface else None
|
||||||
|
except Exception:
|
||||||
|
active_layer = None
|
||||||
|
|
||||||
|
summary = self.vln_logic.upload_summary(active_layer)
|
||||||
|
if summary is None:
|
||||||
|
QMessageBox.warning(
|
||||||
|
self,
|
||||||
|
"Hochladen nicht möglich",
|
||||||
|
"Der aktive Layer wurde nicht über 'vln_karten' geladen.",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
answer = QMessageBox.question(
|
||||||
|
self,
|
||||||
|
"Zum Server hochladen",
|
||||||
|
"%s für VKZ %s übertragen?\n\n%s\n\n"
|
||||||
|
"%d Objekte; der bisherige Bestand dieser VKZ wird ersetzt."
|
||||||
|
% (
|
||||||
|
summary["dataset_label"],
|
||||||
|
summary["vkz"],
|
||||||
|
summary["listing"],
|
||||||
|
summary["total"],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
yes = getattr(
|
||||||
|
getattr(QMessageBox, "StandardButton", QMessageBox),
|
||||||
|
"Yes",
|
||||||
|
QMessageBox.Yes if hasattr(QMessageBox, "Yes") else None,
|
||||||
|
)
|
||||||
|
if answer != yes:
|
||||||
|
return
|
||||||
|
except Exception:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
success, message = self.vln_logic.upload_active_layer(active_layer)
|
||||||
|
except Exception as exc:
|
||||||
|
try:
|
||||||
|
from sn_plan41.modules.vln_api_client import AuthError as _AuthError
|
||||||
|
if isinstance(exc, _AuthError):
|
||||||
|
self.vln_logic.handle_session_expired()
|
||||||
|
self._on_vln_logout()
|
||||||
|
QMessageBox.warning(
|
||||||
|
self,
|
||||||
|
"Sitzung abgelaufen",
|
||||||
|
"Der API-Key ist nicht mehr gültig — bitte neu anmelden.",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
QMessageBox.warning(self, "Hochladen fehlgeschlagen", str(exc))
|
||||||
|
return
|
||||||
|
|
||||||
|
if success:
|
||||||
|
try:
|
||||||
|
from sn_basis.functions.message_wrapper import info
|
||||||
|
info("Upload erfolgreich", message)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
QMessageBox.warning(self, "Hochladen fehlgeschlagen", message)
|
||||||
def _toggle_group(self, checked: bool) -> None:
|
def _toggle_group(self, checked: bool) -> None:
|
||||||
"""Zeigt/verbirgt Verfahrens-DB-Gruppe."""
|
"""Zeigt/verbirgt Verfahrens-DB-Gruppe."""
|
||||||
self.group_button.setArrowType(ArrowDown if checked else ArrowRight)
|
self.group_button.setArrowType(ArrowDown if checked else ArrowRight)
|
||||||
@@ -249,20 +587,6 @@ class TabA(QWidget):
|
|||||||
self.lokale_linkliste = path
|
self.lokale_linkliste = path
|
||||||
set_variable("tab_a_linkliste", path, scope="project")
|
set_variable("tab_a_linkliste", path, scope="project")
|
||||||
|
|
||||||
def _on_layer_changed(self, layer) -> None:
|
|
||||||
"""Persistiert Layer-Auswahl und registriert Verfahrensgebiet."""
|
|
||||||
self._pufferlayer = layer
|
|
||||||
|
|
||||||
if not layer:
|
|
||||||
return
|
|
||||||
|
|
||||||
# UI-State speichern
|
|
||||||
set_variable("tab_a_layer_id", layer.id(), scope="project")
|
|
||||||
|
|
||||||
# 🔹 NEU: Verfahrensgebiet explizit registrieren
|
|
||||||
if self.logic:
|
|
||||||
self.logic.save_verfahrensgebiet_layer(layer)
|
|
||||||
|
|
||||||
|
|
||||||
def _on_raumfilter_changed(self, value: str) -> None:
|
def _on_raumfilter_changed(self, value: str) -> None:
|
||||||
"""Persistieret Raumfilter-Auswahl."""
|
"""Persistieret Raumfilter-Auswahl."""
|
||||||
@@ -296,15 +620,6 @@ class TabA(QWidget):
|
|||||||
source = self.file_widget.filePath()
|
source = self.file_widget.filePath()
|
||||||
raumfilter = self._raumfilter_combo.currentText()
|
raumfilter = self._raumfilter_combo.currentText()
|
||||||
linkliste = self.linkliste_widget.filePath()
|
linkliste = self.linkliste_widget.filePath()
|
||||||
|
|
||||||
if self.logic and self.layer_combo:
|
|
||||||
layer = self.layer_combo.currentLayer()
|
|
||||||
else:
|
|
||||||
layer = None
|
|
||||||
|
|
||||||
if layer and layer.name() == "Verfahrensgebiet":
|
|
||||||
self.logic.save_verfahrensgebiet_layer(layer)
|
|
||||||
|
|
||||||
if self.logic:
|
if self.logic:
|
||||||
try:
|
try:
|
||||||
self.logic._on_run_pipeline(source, linkliste, raumfilter)
|
self.logic._on_run_pipeline(source, linkliste, raumfilter)
|
||||||
|
|||||||
@@ -1,680 +0,0 @@
|
|||||||
"""
|
|
||||||
sn_plan41/ui/tab_b_logic.py – Fachlogik für Tab B (Druck)
|
|
||||||
"""
|
|
||||||
from __future__ import annotations
|
|
||||||
import math
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from sn_basis.functions.qt_wrapper import QVariant
|
|
||||||
from sn_basis.functions.variable_wrapper import set_variable, get_variable
|
|
||||||
from sn_basis.functions.qgiscore_wrapper import get_layer_extent
|
|
||||||
from sn_basis.functions.qgiscore_wrapper import (
|
|
||||||
QgsProject,
|
|
||||||
QgsVectorLayer,
|
|
||||||
QgsGeometry,
|
|
||||||
QgsFeature,
|
|
||||||
QgsField,
|
|
||||||
QgsVectorFileWriter,
|
|
||||||
)
|
|
||||||
from sn_basis.functions.sys_wrapper import get_plugin_root, join_path, file_exists
|
|
||||||
from sn_basis.modules.Pruefmanager import Pruefmanager
|
|
||||||
from sn_basis.modules.layerpruefer import Layerpruefer
|
|
||||||
from sn_plan41.ui.layout import Layout
|
|
||||||
|
|
||||||
|
|
||||||
KARTENNAME_VAR = "kartenname"
|
|
||||||
PLOTMASSSTAB_VAR = "plotmassstab"
|
|
||||||
VIEW_VAR = "view"
|
|
||||||
ZIELGROESSE_VAR = "zielgroesse"
|
|
||||||
FORMFAKTOR_VAR = "formfaktor"
|
|
||||||
KARTENNAME_38 = "§38"
|
|
||||||
KARTENNAME_41 = "§41"
|
|
||||||
MASSSTAB_WIE_KARTENFENSTER = "Wie Kartenfenster"
|
|
||||||
THEMA_WIE_KARTENFENSTER = "wie kartenfenster"
|
|
||||||
|
|
||||||
KARTENNAME_BY_AUSWAHL = {
|
|
||||||
KARTENNAME_38: "Planungsübersicht §38 FlurbG",
|
|
||||||
KARTENNAME_41: "Karte zum Plan über die gemeinschaftlichen und öffentlichen Anlagen (§ 41 FlurbG)",
|
|
||||||
}
|
|
||||||
|
|
||||||
PLOTMASSSTAB_BY_AUSWAHL = {
|
|
||||||
"1:5.000": "5000",
|
|
||||||
"1:10.000": "10000",
|
|
||||||
"1:15.000": "15000",
|
|
||||||
"1:20.000": "20000",
|
|
||||||
"1:25.000": "25000",
|
|
||||||
"1:50.000": "50000",
|
|
||||||
"1:100.000": "100000",
|
|
||||||
}
|
|
||||||
|
|
||||||
# Breite x Höhe in mm (Hochformat, DIN-Standard)
|
|
||||||
DIN_GROESSEN: dict[str, tuple[int, int]] = {
|
|
||||||
"DIN A0": (841, 1189),
|
|
||||||
"DIN A1": (594, 841),
|
|
||||||
"DIN A2": (420, 594),
|
|
||||||
"DIN A3": (297, 420),
|
|
||||||
"DIN A4": (210, 297),
|
|
||||||
}
|
|
||||||
DIN_STANDARD = "DIN A0"
|
|
||||||
|
|
||||||
class TabBLogic:
|
|
||||||
"""
|
|
||||||
Kapselt die Fachlogik von Tab B.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, pruefmanager: Pruefmanager) -> None:
|
|
||||||
self.pruefmanager = pruefmanager
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _wkt_rect(x_min: float, y_min: float, x_max: float, y_max: float) -> str:
|
|
||||||
return (
|
|
||||||
f"POLYGON(({x_min} {y_min}, {x_max} {y_min}, {x_max} {y_max}, "
|
|
||||||
f"{x_min} {y_max}, {x_min} {y_min}))"
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _set_topological_editing_enabled() -> None:
|
|
||||||
project = QgsProject.instance()
|
|
||||||
set_topological = getattr(project, "setTopologicalEditing", None)
|
|
||||||
if callable(set_topological):
|
|
||||||
try:
|
|
||||||
set_topological(True)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _apply_atlas_style(layer: Any) -> None:
|
|
||||||
style_path = join_path(get_plugin_root(), "sn_plan41", "assets", "atlasobjekte.qml")
|
|
||||||
if not file_exists(style_path):
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
ok, _ = layer.loadNamedStyle(str(style_path))
|
|
||||||
if ok:
|
|
||||||
getattr(layer, "triggerRepaint", lambda: None)()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _find_tile_grid_for_roll_atlas(
|
|
||||||
kartenbild_w_mm: float,
|
|
||||||
kartenbild_h_mm: float,
|
|
||||||
din_dims: tuple[int, int],
|
|
||||||
respect_max_sheet_size: bool = False,
|
|
||||||
) -> tuple[int, int, float, float]:
|
|
||||||
"""Bestimmt ein Atlas-Raster für Endlosrolle, das Kacheln statt Streifen erzeugt.
|
|
||||||
|
|
||||||
Ziel: Kachel-Seitenverhältnis möglichst nah am Einzelblatt-Kartenfenster
|
|
||||||
(inkl. Rändern) bei zugleich moderater Seitenanzahl.
|
|
||||||
"""
|
|
||||||
if kartenbild_w_mm <= 0 or kartenbild_h_mm <= 0:
|
|
||||||
return 1, 1, max(1.0, kartenbild_w_mm), max(1.0, kartenbild_h_mm)
|
|
||||||
|
|
||||||
# Einzelblatt-Kartenfenster für beide Orientierungen
|
|
||||||
dim_w, dim_h = float(din_dims[0]), float(din_dims[1])
|
|
||||||
orientation_candidates: list[tuple[float, float]] = [
|
|
||||||
(dim_w - 210.0, dim_h - 20.0),
|
|
||||||
(dim_h - 210.0, dim_w - 20.0),
|
|
||||||
]
|
|
||||||
|
|
||||||
best_score = math.inf
|
|
||||||
best_result: tuple[int, int, float, float] | None = None
|
|
||||||
|
|
||||||
for target_w, target_h in orientation_candidates:
|
|
||||||
if target_w <= 0 or target_h <= 0:
|
|
||||||
continue
|
|
||||||
|
|
||||||
target_aspect = target_w / target_h
|
|
||||||
px0 = max(1, int(round(kartenbild_w_mm / target_w)))
|
|
||||||
py0 = max(1, int(round(kartenbild_h_mm / target_h)))
|
|
||||||
|
|
||||||
for pages_x in range(max(1, px0 - 3), px0 + 4):
|
|
||||||
for pages_y in range(max(1, py0 - 3), py0 + 4):
|
|
||||||
tile_w = kartenbild_w_mm / pages_x
|
|
||||||
tile_h = kartenbild_h_mm / pages_y
|
|
||||||
if tile_w <= 0 or tile_h <= 0:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Im Blatt-Modus darf die resultierende Atlasseite die
|
|
||||||
# gewählte Zielgröße (inkl. Orientierung) nicht überschreiten.
|
|
||||||
if respect_max_sheet_size and (tile_w > target_w or tile_h > target_h):
|
|
||||||
continue
|
|
||||||
|
|
||||||
tile_aspect = tile_w / tile_h
|
|
||||||
# 0 bei perfekter Übereinstimmung; symmetrisch für >1/<1
|
|
||||||
aspect_error = abs(math.log(tile_aspect / target_aspect))
|
|
||||||
|
|
||||||
# Streifen bestrafen
|
|
||||||
strip_penalty = 0.0
|
|
||||||
if tile_aspect < 0.5:
|
|
||||||
strip_penalty = abs(math.log(tile_aspect / 0.5))
|
|
||||||
elif tile_aspect > 2.0:
|
|
||||||
strip_penalty = abs(math.log(tile_aspect / 2.0))
|
|
||||||
|
|
||||||
page_count = pages_x * pages_y
|
|
||||||
|
|
||||||
# Gewichtung: zuerst Formatnähe, dann Streifenvermeidung,
|
|
||||||
# danach Seitenzahl minimieren.
|
|
||||||
score = (aspect_error * 12.0) + (strip_penalty * 6.0) + (page_count * 0.20)
|
|
||||||
|
|
||||||
if score < best_score:
|
|
||||||
best_score = score
|
|
||||||
best_result = (pages_x, pages_y, tile_w, tile_h)
|
|
||||||
|
|
||||||
if best_result is None:
|
|
||||||
if respect_max_sheet_size:
|
|
||||||
fallback_candidates: list[tuple[int, int, float, float, int]] = []
|
|
||||||
for target_w, target_h in orientation_candidates:
|
|
||||||
if target_w <= 0 or target_h <= 0:
|
|
||||||
continue
|
|
||||||
pages_x = max(1, math.ceil(kartenbild_w_mm / target_w))
|
|
||||||
pages_y = max(1, math.ceil(kartenbild_h_mm / target_h))
|
|
||||||
tile_w = kartenbild_w_mm / pages_x
|
|
||||||
tile_h = kartenbild_h_mm / pages_y
|
|
||||||
fallback_candidates.append((pages_x, pages_y, tile_w, tile_h, pages_x * pages_y))
|
|
||||||
|
|
||||||
if fallback_candidates:
|
|
||||||
fallback_candidates.sort(key=lambda entry: (entry[4], abs(math.log((entry[2] / entry[3]) if entry[3] > 0 else 1.0))))
|
|
||||||
fx, fy, fw, fh, _ = fallback_candidates[0]
|
|
||||||
return fx, fy, fw, fh
|
|
||||||
|
|
||||||
return 1, 1, kartenbild_w_mm, kartenbild_h_mm
|
|
||||||
|
|
||||||
return best_result
|
|
||||||
|
|
||||||
def _create_atlasobjekte_layer(
|
|
||||||
self,
|
|
||||||
layer: Any,
|
|
||||||
extent: Any,
|
|
||||||
pages_x: int,
|
|
||||||
pages_y: int,
|
|
||||||
seite_karte_w: float,
|
|
||||||
seite_karte_h: float,
|
|
||||||
massstab_zahl: float,
|
|
||||||
) -> Any | None:
|
|
||||||
layer_crs = layer.crs() if hasattr(layer, "crs") else None
|
|
||||||
crs_authid = layer_crs.authid() if layer_crs is not None and hasattr(layer_crs, "authid") else "EPSG:25832"
|
|
||||||
atlas_layer = QgsVectorLayer(f"Polygon?crs={crs_authid}", "Atlasobjekte", "memory")
|
|
||||||
if not atlas_layer or not atlas_layer.isValid():
|
|
||||||
return None
|
|
||||||
|
|
||||||
provider = atlas_layer.dataProvider()
|
|
||||||
provider.addAttributes([
|
|
||||||
QgsField("Seitenzahl", QVariant.Int),
|
|
||||||
])
|
|
||||||
atlas_layer.updateFields()
|
|
||||||
|
|
||||||
tile_w_m = seite_karte_w * massstab_zahl / 1000.0
|
|
||||||
tile_h_m = seite_karte_h * massstab_zahl / 1000.0
|
|
||||||
|
|
||||||
x_min = extent.xMinimum()
|
|
||||||
y_min = extent.yMinimum()
|
|
||||||
x_max = extent.xMaximum()
|
|
||||||
y_max = extent.yMaximum()
|
|
||||||
|
|
||||||
seitenzahl = 1
|
|
||||||
features = []
|
|
||||||
for row_idx in range(pages_y):
|
|
||||||
tile_y_max = y_max - row_idx * tile_h_m
|
|
||||||
tile_y_min = tile_y_max - tile_h_m
|
|
||||||
|
|
||||||
for col_idx in range(pages_x):
|
|
||||||
tile_x_min = x_min + col_idx * tile_w_m
|
|
||||||
tile_x_max = tile_x_min + tile_w_m
|
|
||||||
|
|
||||||
tile_geom = QgsGeometry.fromWkt(
|
|
||||||
self._wkt_rect(tile_x_min, tile_y_min, tile_x_max, tile_y_max)
|
|
||||||
)
|
|
||||||
|
|
||||||
feat = QgsFeature(atlas_layer.fields())
|
|
||||||
feat.setGeometry(tile_geom)
|
|
||||||
feat["Seitenzahl"] = seitenzahl
|
|
||||||
features.append(feat)
|
|
||||||
seitenzahl += 1
|
|
||||||
|
|
||||||
if not features:
|
|
||||||
return None
|
|
||||||
|
|
||||||
provider.addFeatures(features)
|
|
||||||
atlas_layer.updateExtents()
|
|
||||||
self._set_topological_editing_enabled()
|
|
||||||
|
|
||||||
verfahrens_db = get_variable("verfahrens_db", scope="project") or ""
|
|
||||||
print(f"[TabBLogic] Atlasobjekte: verfahrens_db='{verfahrens_db}'")
|
|
||||||
if not verfahrens_db:
|
|
||||||
QgsProject.instance().addMapLayer(atlas_layer)
|
|
||||||
self._apply_atlas_style(atlas_layer)
|
|
||||||
print("[TabBLogic] Atlasobjekte temporär ins Projekt geladen")
|
|
||||||
return atlas_layer
|
|
||||||
|
|
||||||
opts = QgsVectorFileWriter.SaveVectorOptions()
|
|
||||||
opts.driverName = "GPKG"
|
|
||||||
opts.fileEncoding = "UTF-8"
|
|
||||||
opts.layerName = "Atlasobjekte"
|
|
||||||
if file_exists(verfahrens_db):
|
|
||||||
opts.actionOnExistingFile = QgsVectorFileWriter.CreateOrOverwriteLayer
|
|
||||||
else:
|
|
||||||
opts.actionOnExistingFile = QgsVectorFileWriter.CreateOrOverwriteFile
|
|
||||||
|
|
||||||
err_result = QgsVectorFileWriter.writeAsVectorFormatV3(
|
|
||||||
atlas_layer,
|
|
||||||
verfahrens_db,
|
|
||||||
QgsProject.instance().transformContext(),
|
|
||||||
opts,
|
|
||||||
)
|
|
||||||
|
|
||||||
# QGIS-Versionen liefern hier entweder nur WriterError
|
|
||||||
# oder ein Tupel (WriterError, msg, newPath, layerName).
|
|
||||||
err_code = err_result[0] if isinstance(err_result, tuple) else err_result
|
|
||||||
if err_code != QgsVectorFileWriter.NoError:
|
|
||||||
print(f"[TabBLogic] Atlasobjekte schreiben fehlgeschlagen: err={err_result}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
for existing in QgsProject.instance().mapLayersByName("Atlasobjekte"):
|
|
||||||
try:
|
|
||||||
QgsProject.instance().removeMapLayer(existing.id())
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
uri = f"{verfahrens_db}|layername=Atlasobjekte"
|
|
||||||
loaded_layer = QgsVectorLayer(uri, "Atlasobjekte", "ogr")
|
|
||||||
if not loaded_layer or not loaded_layer.isValid():
|
|
||||||
print(f"[TabBLogic] Atlasobjekte laden aus GPKG fehlgeschlagen: uri='{uri}'")
|
|
||||||
return None
|
|
||||||
|
|
||||||
QgsProject.instance().addMapLayer(loaded_layer)
|
|
||||||
self._apply_atlas_style(loaded_layer)
|
|
||||||
print("[TabBLogic] Atlasobjekte aus Verfahrens-DB geladen und gestylt")
|
|
||||||
return loaded_layer
|
|
||||||
|
|
||||||
def set_kartenname_for_auswahl(self, auswahl: str) -> None:
|
|
||||||
"""Setzt die Projektvariable ``kartenname`` anhand der Kartennamen-Auswahl."""
|
|
||||||
kartenname = KARTENNAME_BY_AUSWAHL.get(auswahl, "")
|
|
||||||
set_variable(KARTENNAME_VAR, kartenname, scope="project")
|
|
||||||
|
|
||||||
def set_plotmassstab_for_auswahl(self, auswahl: str, aktueller_massstab: float | None = None) -> None:
|
|
||||||
"""Setzt die Projektvariable ``plotmassstab`` anhand der Maßstabsauswahl."""
|
|
||||||
if auswahl == MASSSTAB_WIE_KARTENFENSTER:
|
|
||||||
if aktueller_massstab and aktueller_massstab > 0:
|
|
||||||
set_variable(PLOTMASSSTAB_VAR, str(int(round(aktueller_massstab))), scope="project")
|
|
||||||
else:
|
|
||||||
set_variable(PLOTMASSSTAB_VAR, "", scope="project")
|
|
||||||
return
|
|
||||||
|
|
||||||
value = PLOTMASSSTAB_BY_AUSWAHL.get(auswahl, "")
|
|
||||||
set_variable(PLOTMASSSTAB_VAR, value, scope="project")
|
|
||||||
|
|
||||||
def set_view_for_auswahl(self, auswahl: str) -> None:
|
|
||||||
"""Setzt die Projektvariable ``view`` auf ``aktuell`` oder den Namen des gewählten Layerthemas."""
|
|
||||||
if auswahl == THEMA_WIE_KARTENFENSTER:
|
|
||||||
set_variable(VIEW_VAR, "aktuell", scope="project")
|
|
||||||
return
|
|
||||||
|
|
||||||
set_variable(VIEW_VAR, auswahl or "", scope="project")
|
|
||||||
|
|
||||||
def set_zielgroesse_for_auswahl(self, auswahl: str) -> None:
|
|
||||||
"""Setzt ``zielgroesse`` auf den gewählten DIN-Namen."""
|
|
||||||
set_variable(ZIELGROESSE_VAR, auswahl if auswahl in DIN_GROESSEN else DIN_STANDARD, scope="project")
|
|
||||||
|
|
||||||
def set_formfaktor(self, endlosrolle: bool) -> None:
|
|
||||||
"""Setzt die Projektvariable ``formfaktor`` auf ``Endlosrolle`` oder ``Blatt``."""
|
|
||||||
set_variable(FORMFAKTOR_VAR, "Endlosrolle" if endlosrolle else "Blatt", scope="project")
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────────────────────
|
|
||||||
# Pipeline: Druckvorlage_anlegen
|
|
||||||
# ─────────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
def druckvorlage_anlegen(
|
|
||||||
self,
|
|
||||||
layer: object,
|
|
||||||
kartenname_auswahl: str,
|
|
||||||
massstab_auswahl: str,
|
|
||||||
zielgroesse: str,
|
|
||||||
formfaktor: bool,
|
|
||||||
) -> dict:
|
|
||||||
"""Pipeline 'Druckvorlage_anlegen'.
|
|
||||||
|
|
||||||
Prüft Parameter, berechnet Plotgröße und stellt bei Bedarf Atlas-Rückfrage.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
layer:
|
|
||||||
Aktuell gewählter Verfahrensgebiet-Layer aus Tab A.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
dict
|
|
||||||
``ok`` (bool): Ob die Pipeline erfolgreich durchlaufen werden soll.
|
|
||||||
``switch_to_tab_a`` (bool): Ob Tab A aktiviert werden soll.
|
|
||||||
``atlas_seiten`` (int): Anzahl benötigter Seiten (1 = kein Atlas).
|
|
||||||
"""
|
|
||||||
# ─── 1. Verfahrensgebiet-Layer prüfen ─────────────────────────────
|
|
||||||
lp = Layerpruefer(layer=layer)
|
|
||||||
ergebnis = lp.pruefe()
|
|
||||||
if not ergebnis.ok:
|
|
||||||
self.pruefmanager.zeige_hinweis(
|
|
||||||
"Verfahrensgebiets-Layer angeben",
|
|
||||||
"Verfahrensgebiets-Layer angeben",
|
|
||||||
)
|
|
||||||
return {"ok": False, "switch_to_tab_a": True, "atlas_seiten": 0}
|
|
||||||
|
|
||||||
layer_id = getattr(layer, "id", lambda: "")() or ""
|
|
||||||
set_variable("verfahrensgebietslayer", layer_id, scope="project")
|
|
||||||
|
|
||||||
# ─── 2. Kartenname prüfen ─────────────────────────────────────────
|
|
||||||
if kartenname_auswahl not in (KARTENNAME_38, KARTENNAME_41):
|
|
||||||
self.pruefmanager.zeige_hinweis(
|
|
||||||
"Kartennamen wählen",
|
|
||||||
"Kartennamen wählen",
|
|
||||||
)
|
|
||||||
return {"ok": False, "switch_to_tab_a": False, "atlas_seiten": 0}
|
|
||||||
|
|
||||||
# ─── 3. Maßstab ermitteln ─────────────────────────────────────────
|
|
||||||
if massstab_auswahl == MASSSTAB_WIE_KARTENFENSTER:
|
|
||||||
massstab_str = get_variable(PLOTMASSSTAB_VAR, scope="project") or ""
|
|
||||||
try:
|
|
||||||
massstab_zahl = float(massstab_str)
|
|
||||||
except (ValueError, TypeError):
|
|
||||||
massstab_zahl = 0.0
|
|
||||||
else:
|
|
||||||
massstab_zahl = float(PLOTMASSSTAB_BY_AUSWAHL.get(massstab_auswahl, 0))
|
|
||||||
|
|
||||||
if massstab_zahl <= 0:
|
|
||||||
self.pruefmanager.zeige_hinweis(
|
|
||||||
"Maßstab fehlt",
|
|
||||||
"Kein gültiger Maßstab angegeben.",
|
|
||||||
)
|
|
||||||
return {"ok": False, "switch_to_tab_a": False, "atlas_seiten": 0}
|
|
||||||
|
|
||||||
# ─── 4. Kartenbild berechnen ──────────────────────────────────────
|
|
||||||
# Der Layer wird als metrisch projiziert (Einheit: m) vorausgesetzt,
|
|
||||||
# wie es für deutsche Planungslagen (z.B. EPSG:25832) üblich ist.
|
|
||||||
extent = get_layer_extent(layer)
|
|
||||||
if extent is None:
|
|
||||||
self.pruefmanager.zeige_hinweis(
|
|
||||||
"Fehler",
|
|
||||||
"Layer-Ausdehnung konnte nicht ermittelt werden.",
|
|
||||||
)
|
|
||||||
return {"ok": False, "switch_to_tab_a": False, "atlas_seiten": 0}
|
|
||||||
|
|
||||||
# Naturgröße (m) → Papiergröße (mm): mm = m * 1000 / massstab
|
|
||||||
kartenbild_w = extent.width() * 1000.0 / massstab_zahl
|
|
||||||
kartenbild_h = extent.height() * 1000.0 / massstab_zahl
|
|
||||||
|
|
||||||
# ─── 5. Plotgröße = Kartenbild + Randabstand (x+210 mm, y+20 mm) ──
|
|
||||||
plotgroesse_w = kartenbild_w + 210.0
|
|
||||||
plotgroesse_h = kartenbild_h + 20.0
|
|
||||||
|
|
||||||
# ─── 6. Zielgröße bestimmen ───────────────────────────────────────
|
|
||||||
din_dims = DIN_GROESSEN.get(zielgroesse, DIN_GROESSEN[DIN_STANDARD])
|
|
||||||
if formfaktor: # Endlosrolle: X-Richtung entspricht der Plotgröße
|
|
||||||
ziel_w, ziel_h = plotgroesse_w, float(min(din_dims))
|
|
||||||
else:
|
|
||||||
ziel_w, ziel_h = float(din_dims[0]), float(din_dims[1])
|
|
||||||
|
|
||||||
if ziel_w < DIN_GROESSEN["DIN A4"][0] or ziel_h < DIN_GROESSEN["DIN A4"][1]:
|
|
||||||
self.pruefmanager.zeige_hinweis(
|
|
||||||
"Blattgröße zu klein",
|
|
||||||
"Die Zielgröße darf nicht kleiner als DIN A4 sein.",
|
|
||||||
)
|
|
||||||
return {"ok": False, "switch_to_tab_a": False, "atlas_seiten": 0}
|
|
||||||
|
|
||||||
# ─── 7. Passt auf ein Blatt? -> Layout erzeugen ───────────────────
|
|
||||||
print(f"[TabBLogic] plotgroesse=({plotgroesse_w:.1f}x{plotgroesse_h:.1f}), "
|
|
||||||
f"zielgroesse=({ziel_w:.1f}x{ziel_h:.1f}), passt={plotgroesse_w <= ziel_w and plotgroesse_h <= ziel_h}")
|
|
||||||
if plotgroesse_w > ziel_w or plotgroesse_h > ziel_h:
|
|
||||||
if plotgroesse_w <= ziel_h and plotgroesse_h <= ziel_w:
|
|
||||||
ziel_w, ziel_h = ziel_h, ziel_w
|
|
||||||
|
|
||||||
if plotgroesse_w <= ziel_w and plotgroesse_h <= ziel_h:
|
|
||||||
kartenname = get_variable(KARTENNAME_VAR, scope="project") or KARTENNAME_BY_AUSWAHL.get(
|
|
||||||
kartenname_auswahl, "Vorlage"
|
|
||||||
)
|
|
||||||
thema = get_variable(VIEW_VAR, scope="project") or ""
|
|
||||||
print(f"[TabBLogic] frage_text aufrufen, default='{kartenname}', thema='{thema}'")
|
|
||||||
vorlage_name, bestaetigt = self.pruefmanager.frage_text(
|
|
||||||
"Neue Vorlage anlegen",
|
|
||||||
"Bezeichnung der Vorlage:",
|
|
||||||
default_text=kartenname,
|
|
||||||
)
|
|
||||||
print(f"[TabBLogic] frage_text Ergebnis: name='{vorlage_name}', bestaetigt={bestaetigt}")
|
|
||||||
if not bestaetigt:
|
|
||||||
return {"ok": False, "switch_to_tab_a": False, "atlas_seiten": 0}
|
|
||||||
|
|
||||||
vorlage_name = (vorlage_name or "").strip() or kartenname
|
|
||||||
|
|
||||||
print(f"[TabBLogic] Rufe Layout().create_single_page_layout auf: name='{vorlage_name}', "
|
|
||||||
f"page=({ziel_w}x{ziel_h}), map=({kartenbild_w:.1f}x{kartenbild_h:.1f}), "
|
|
||||||
f"massstab={massstab_zahl}, thema='{thema}'")
|
|
||||||
try:
|
|
||||||
Layout().create_single_page_layout(
|
|
||||||
name=vorlage_name,
|
|
||||||
page_width_mm=ziel_w,
|
|
||||||
page_height_mm=ziel_h,
|
|
||||||
map_width_mm=kartenbild_w,
|
|
||||||
map_height_mm=kartenbild_h,
|
|
||||||
extent=extent,
|
|
||||||
plotmassstab=massstab_zahl,
|
|
||||||
thema=thema,
|
|
||||||
)
|
|
||||||
print("[TabBLogic] create_single_page_layout erfolgreich abgeschlossen")
|
|
||||||
except ValueError as exc:
|
|
||||||
print(f"[TabBLogic] ValueError: {exc}")
|
|
||||||
self.pruefmanager.zeige_hinweis("Vorlage anlegen", str(exc))
|
|
||||||
return {"ok": False, "switch_to_tab_a": False, "atlas_seiten": 0}
|
|
||||||
except Exception as exc:
|
|
||||||
print(f"[TabBLogic] Exception: {exc!r}")
|
|
||||||
self.pruefmanager.zeige_hinweis("Vorlage anlegen", f"Die Vorlage konnte nicht angelegt werden: {exc}")
|
|
||||||
return {"ok": False, "switch_to_tab_a": False, "atlas_seiten": 0}
|
|
||||||
|
|
||||||
return {"ok": True, "switch_to_tab_a": False, "atlas_seiten": 1}
|
|
||||||
|
|
||||||
# ─── 8. Atlas: Anzahl Seiten berechnen ────────────────────────────
|
|
||||||
# Nutzbarer Kartenbereich pro Atlasseite (abzüglich gleichem Randabstand)
|
|
||||||
seite_karte_w = ziel_w - 210.0
|
|
||||||
seite_karte_h = ziel_h - 20.0
|
|
||||||
|
|
||||||
if seite_karte_w <= 0 or seite_karte_h <= 0:
|
|
||||||
self.pruefmanager.zeige_hinweis(
|
|
||||||
"Blattgröße zu klein",
|
|
||||||
"Die gewählte Zielgröße ist kleiner als der Mindest-Randabstand. "
|
|
||||||
"Bitte eine größere Blattgröße wählen.",
|
|
||||||
)
|
|
||||||
return {"ok": False, "switch_to_tab_a": False, "atlas_seiten": 0}
|
|
||||||
|
|
||||||
pages_x = math.ceil(kartenbild_w / seite_karte_w)
|
|
||||||
pages_y = math.ceil(kartenbild_h / seite_karte_h)
|
|
||||||
|
|
||||||
# Für Atlas in beiden Modi (Endlosrolle + Blatt): Seitenraster so wählen,
|
|
||||||
# dass Atlasobjekte näher am Einzelblattformat liegen und keine Streifen entstehen.
|
|
||||||
opt_pages_x, opt_pages_y, opt_tile_w_mm, opt_tile_h_mm = self._find_tile_grid_for_roll_atlas(
|
|
||||||
kartenbild_w_mm=kartenbild_w,
|
|
||||||
kartenbild_h_mm=kartenbild_h,
|
|
||||||
din_dims=din_dims,
|
|
||||||
respect_max_sheet_size=(not formfaktor),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Endlosrolle: Nur die Breite darf wachsen, die Höhe muss innerhalb
|
|
||||||
# der gewählten Zielhöhe bleiben.
|
|
||||||
if formfaktor:
|
|
||||||
max_tile_h_mm = max(1.0, ziel_h - 20.0)
|
|
||||||
if opt_tile_h_mm > max_tile_h_mm:
|
|
||||||
required_pages_y = max(1, math.ceil(kartenbild_h / max_tile_h_mm))
|
|
||||||
opt_pages_y = max(opt_pages_y, required_pages_y)
|
|
||||||
opt_tile_h_mm = kartenbild_h / opt_pages_y
|
|
||||||
|
|
||||||
pages_x = max(1, opt_pages_x)
|
|
||||||
pages_y = max(1, opt_pages_y)
|
|
||||||
seite_karte_w = max(1.0, opt_tile_w_mm)
|
|
||||||
seite_karte_h = max(1.0, opt_tile_h_mm)
|
|
||||||
modus = "Endlosrolle" if formfaktor else "Blatt"
|
|
||||||
print(
|
|
||||||
f"[TabBLogic] {modus} Rasteroptimierung: pages_x={pages_x}, pages_y={pages_y}, "
|
|
||||||
f"tile_mm=({seite_karte_w:.1f}x{seite_karte_h:.1f}), "
|
|
||||||
f"tile_aspect={seite_karte_w / seite_karte_h:.3f}"
|
|
||||||
)
|
|
||||||
if formfaktor:
|
|
||||||
max_tile_h_mm = max(1.0, ziel_h - 20.0)
|
|
||||||
print(
|
|
||||||
f"[TabBLogic] Endlosrolle Höhenlimit: tile_h={seite_karte_h:.1f}mm, "
|
|
||||||
f"max_tile_h={max_tile_h_mm:.1f}mm, within_limit={seite_karte_h <= max_tile_h_mm}"
|
|
||||||
)
|
|
||||||
|
|
||||||
anzahl_seiten = pages_x * pages_y
|
|
||||||
|
|
||||||
ja = self.pruefmanager.frage_ja_nein(
|
|
||||||
"Ausdruck als Atlas anlegen?",
|
|
||||||
f"Für die ausgewählten Parameter sind {anzahl_seiten} Einzelseiten erforderlich.\n"
|
|
||||||
"Ausdruck als Atlas anlegen?",
|
|
||||||
default=True,
|
|
||||||
)
|
|
||||||
print(f"[TabBLogic] Atlas-Rückfrage: ja={ja}, geplante_seiten={anzahl_seiten}")
|
|
||||||
if not ja:
|
|
||||||
return {"ok": False, "switch_to_tab_a": False, "atlas_seiten": anzahl_seiten}
|
|
||||||
|
|
||||||
atlas_layer = self._create_atlasobjekte_layer(
|
|
||||||
layer=layer,
|
|
||||||
extent=extent,
|
|
||||||
pages_x=pages_x,
|
|
||||||
pages_y=pages_y,
|
|
||||||
seite_karte_w=seite_karte_w,
|
|
||||||
seite_karte_h=seite_karte_h,
|
|
||||||
massstab_zahl=massstab_zahl,
|
|
||||||
)
|
|
||||||
if atlas_layer is None:
|
|
||||||
self.pruefmanager.zeige_hinweis(
|
|
||||||
"Atlasobjekte",
|
|
||||||
"Atlasobjekte-Layer konnte nicht erzeugt werden.",
|
|
||||||
)
|
|
||||||
return {"ok": False, "switch_to_tab_a": False, "atlas_seiten": 0}
|
|
||||||
print("[TabBLogic] Atlasobjekte-Layer erfolgreich erzeugt")
|
|
||||||
|
|
||||||
try:
|
|
||||||
anzahl_seiten = int(atlas_layer.featureCount())
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Berechne die Kachelgröße aus den Atlasobjekten.
|
|
||||||
# Für die Layout-Kartengröße muss die größte Atlas-Kachel passen,
|
|
||||||
# sonst werden größere Atlasobjekte abgeschnitten.
|
|
||||||
total_w_m = 0.0
|
|
||||||
total_h_m = 0.0
|
|
||||||
min_w_m = math.inf
|
|
||||||
min_h_m = math.inf
|
|
||||||
max_w_m = 0.0
|
|
||||||
max_h_m = 0.0
|
|
||||||
feature_count = 0
|
|
||||||
get_features = getattr(atlas_layer, "getFeatures", None)
|
|
||||||
if callable(get_features):
|
|
||||||
try:
|
|
||||||
for feat in get_features():
|
|
||||||
geom = feat.geometry() if hasattr(feat, "geometry") else None
|
|
||||||
if geom is None or geom.isEmpty():
|
|
||||||
continue
|
|
||||||
bbox = geom.boundingBox() if hasattr(geom, "boundingBox") else None
|
|
||||||
if bbox is None:
|
|
||||||
continue
|
|
||||||
feat_w_m = float(bbox.width())
|
|
||||||
feat_h_m = float(bbox.height())
|
|
||||||
total_w_m += feat_w_m
|
|
||||||
total_h_m += feat_h_m
|
|
||||||
min_w_m = min(min_w_m, feat_w_m)
|
|
||||||
min_h_m = min(min_h_m, feat_h_m)
|
|
||||||
max_w_m = max(max_w_m, feat_w_m)
|
|
||||||
max_h_m = max(max_h_m, feat_h_m)
|
|
||||||
feature_count += 1
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if feature_count > 0:
|
|
||||||
avg_tile_w_m = total_w_m / feature_count
|
|
||||||
avg_tile_h_m = total_h_m / feature_count
|
|
||||||
if not math.isfinite(min_w_m):
|
|
||||||
min_w_m = avg_tile_w_m
|
|
||||||
if not math.isfinite(min_h_m):
|
|
||||||
min_h_m = avg_tile_h_m
|
|
||||||
target_tile_w_m = max_w_m
|
|
||||||
target_tile_h_m = max_h_m
|
|
||||||
else:
|
|
||||||
avg_tile_w_m = seite_karte_w * massstab_zahl / 1000.0
|
|
||||||
avg_tile_h_m = seite_karte_h * massstab_zahl / 1000.0
|
|
||||||
min_w_m = avg_tile_w_m
|
|
||||||
min_h_m = avg_tile_h_m
|
|
||||||
max_w_m = avg_tile_w_m
|
|
||||||
max_h_m = avg_tile_h_m
|
|
||||||
target_tile_w_m = avg_tile_w_m
|
|
||||||
target_tile_h_m = avg_tile_h_m
|
|
||||||
|
|
||||||
# Konvertiere Kachelgrößen zu mm
|
|
||||||
avg_tile_w_mm = avg_tile_w_m * 1000.0 / massstab_zahl
|
|
||||||
avg_tile_h_mm = avg_tile_h_m * 1000.0 / massstab_zahl
|
|
||||||
min_tile_w_mm = min_w_m * 1000.0 / massstab_zahl
|
|
||||||
min_tile_h_mm = min_h_m * 1000.0 / massstab_zahl
|
|
||||||
max_tile_w_mm = max_w_m * 1000.0 / massstab_zahl
|
|
||||||
max_tile_h_mm = max_h_m * 1000.0 / massstab_zahl
|
|
||||||
target_tile_w_mm = target_tile_w_m * 1000.0 / massstab_zahl
|
|
||||||
target_tile_h_mm = target_tile_h_m * 1000.0 / massstab_zahl
|
|
||||||
|
|
||||||
# Layout-Kartengröße = größte Kachelgröße
|
|
||||||
atlas_map_w = max(1.0, target_tile_w_mm)
|
|
||||||
atlas_map_h = max(1.0, target_tile_h_mm)
|
|
||||||
atlas_page_w = atlas_map_w + 210.0
|
|
||||||
atlas_page_h = atlas_map_h + 20.0
|
|
||||||
|
|
||||||
# Debug: Atlasobjekte Geometrien
|
|
||||||
print(
|
|
||||||
f"[TabBLogic] Atlasobjekte Geometrien (Meter): "
|
|
||||||
f"total_w={total_w_m:.1f}m, total_h={total_h_m:.1f}m, "
|
|
||||||
f"min_w={min_w_m:.1f}m, min_h={min_h_m:.1f}m, "
|
|
||||||
f"avg_w={avg_tile_w_m:.1f}m, avg_h={avg_tile_h_m:.1f}m, "
|
|
||||||
f"max_w={max_w_m:.1f}m, max_h={max_h_m:.1f}m, features={feature_count}"
|
|
||||||
)
|
|
||||||
print(
|
|
||||||
f"[TabBLogic] Atlas layout Größen: "
|
|
||||||
f"page=(x=10, y=10, w={atlas_page_w:.1f}mm, h={atlas_page_h:.1f}mm), "
|
|
||||||
f"map=(x=10, y=10, w={atlas_map_w:.1f}mm, h={atlas_map_h:.1f}mm), "
|
|
||||||
f"kachel_min_mm=({min_tile_w_mm:.1f}x{min_tile_h_mm:.1f}), "
|
|
||||||
f"kachel_avg_mm=({avg_tile_w_mm:.1f}x{avg_tile_h_mm:.1f}), "
|
|
||||||
f"kachel_max_mm=({max_tile_w_mm:.1f}x{max_tile_h_mm:.1f})"
|
|
||||||
)
|
|
||||||
|
|
||||||
kartenname = get_variable(KARTENNAME_VAR, scope="project") or KARTENNAME_BY_AUSWAHL.get(
|
|
||||||
kartenname_auswahl, "Atlas"
|
|
||||||
)
|
|
||||||
thema = get_variable(VIEW_VAR, scope="project") or ""
|
|
||||||
vorlage_name, bestaetigt = self.pruefmanager.frage_text(
|
|
||||||
"Neue Atlasvorlage anlegen",
|
|
||||||
"Bezeichnung der Vorlage:",
|
|
||||||
default_text=f"{kartenname} Atlas",
|
|
||||||
)
|
|
||||||
print(f"[TabBLogic] Atlas frage_text Ergebnis: name='{vorlage_name}', bestaetigt={bestaetigt}")
|
|
||||||
if not bestaetigt:
|
|
||||||
return {"ok": False, "switch_to_tab_a": False, "atlas_seiten": anzahl_seiten}
|
|
||||||
|
|
||||||
vorlage_name = (vorlage_name or "").strip() or f"{kartenname} Atlas"
|
|
||||||
|
|
||||||
try:
|
|
||||||
print(
|
|
||||||
f"[TabBLogic] Rufe create_atlas_layout auf: name='{vorlage_name}', "
|
|
||||||
f"page=({atlas_page_w:.1f}x{atlas_page_h:.1f}), "
|
|
||||||
f"map=({atlas_map_w:.1f}x{atlas_map_h:.1f}), massstab={massstab_zahl}, thema='{thema}'"
|
|
||||||
)
|
|
||||||
Layout().create_atlas_layout(
|
|
||||||
name=vorlage_name,
|
|
||||||
page_width_mm=atlas_page_w,
|
|
||||||
page_height_mm=atlas_page_h,
|
|
||||||
map_width_mm=atlas_map_w,
|
|
||||||
map_height_mm=atlas_map_h,
|
|
||||||
extent=extent,
|
|
||||||
plotmassstab=massstab_zahl,
|
|
||||||
atlas_layer=atlas_layer,
|
|
||||||
thema=thema,
|
|
||||||
)
|
|
||||||
print("[TabBLogic] create_atlas_layout erfolgreich abgeschlossen")
|
|
||||||
except ValueError as exc:
|
|
||||||
self.pruefmanager.zeige_hinweis("Atlasvorlage anlegen", str(exc))
|
|
||||||
return {"ok": False, "switch_to_tab_a": False, "atlas_seiten": 0}
|
|
||||||
except Exception as exc:
|
|
||||||
self.pruefmanager.zeige_hinweis("Atlasvorlage anlegen", f"Die Atlasvorlage konnte nicht angelegt werden: {exc}")
|
|
||||||
return {"ok": False, "switch_to_tab_a": False, "atlas_seiten": 0}
|
|
||||||
|
|
||||||
return {"ok": True, "switch_to_tab_a": False, "atlas_seiten": anzahl_seiten}
|
|
||||||
-432
@@ -1,432 +0,0 @@
|
|||||||
"""
|
|
||||||
sn_plan41/ui/tab_b_ui.py – UI für Tab B (Druck)
|
|
||||||
"""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from sn_basis.functions.qt_wrapper import (
|
|
||||||
QWidget,
|
|
||||||
QVBoxLayout,
|
|
||||||
QLabel,
|
|
||||||
QComboBox,
|
|
||||||
QCheckBox,
|
|
||||||
QHBoxLayout,
|
|
||||||
QPushButton,
|
|
||||||
)
|
|
||||||
from sn_basis.functions.qgiscore_wrapper import QgsProject
|
|
||||||
from sn_basis.functions.qgisui_wrapper import iface
|
|
||||||
from sn_basis.functions.variable_wrapper import get_variable, set_variable
|
|
||||||
|
|
||||||
# Services (werden von DockWidget injiziert)
|
|
||||||
from sn_basis.modules.Pruefmanager import Pruefmanager
|
|
||||||
from sn_basis.modules.DataGrabber import DataGrabber
|
|
||||||
from sn_plan41.ui.tab_b_logic import (
|
|
||||||
TabBLogic,
|
|
||||||
MASSSTAB_WIE_KARTENFENSTER,
|
|
||||||
PLOTMASSSTAB_BY_AUSWAHL,
|
|
||||||
THEMA_WIE_KARTENFENSTER,
|
|
||||||
DIN_GROESSEN,
|
|
||||||
DIN_STANDARD,
|
|
||||||
ZIELGROESSE_VAR,
|
|
||||||
FORMFAKTOR_VAR,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
KARTENNAME_VAR = "tab_b_kartenname"
|
|
||||||
KARTENNAME_PLACEHOLDER = "Kartenname wählen"
|
|
||||||
KARTENNAME_38 = "§38"
|
|
||||||
KARTENNAME_41 = "§41"
|
|
||||||
MASSSTAB_VAR = "tab_b_massstab"
|
|
||||||
THEMA_VAR = "tab_b_thema"
|
|
||||||
ZIELGROESSE_UI_VAR = "tab_b_zielgroesse"
|
|
||||||
FORMFAKTOR_UI_VAR = "tab_b_formfaktor"
|
|
||||||
|
|
||||||
class TabB(QWidget):
|
|
||||||
"""
|
|
||||||
UI-Klasse für Tab B (Druck) des Plan41-Plugins.
|
|
||||||
|
|
||||||
Zuständig für:
|
|
||||||
- Auswahl des Druckthemas
|
|
||||||
- Auswahl der Druckparameter
|
|
||||||
- Start der Vorlagenanlage (Druck über QGIS-Druckfunktion)
|
|
||||||
|
|
||||||
Services (Pruefmanager, DataGrabber) werden zur Laufzeit vom DockWidget injiziert.
|
|
||||||
Alle fachlichen Prüfungen laufen über den zentralen Pruefmanager.
|
|
||||||
"""
|
|
||||||
|
|
||||||
tab_title = "Druck" #: Tab-Titel für BaseDockWidget
|
|
||||||
|
|
||||||
def __init__(self, parent: Optional[QWidget] = None):
|
|
||||||
"""
|
|
||||||
Initialisiert die UI-Struktur.
|
|
||||||
|
|
||||||
Services werden später über :meth:`set_services` injiziert.
|
|
||||||
|
|
||||||
:param parent: Parent-Widget (typischerweise DockWidget)
|
|
||||||
"""
|
|
||||||
super().__init__(parent)
|
|
||||||
|
|
||||||
# Services (werden von DockWidget gesetzt)
|
|
||||||
self.pruefmanager: Optional[Pruefmanager] = None
|
|
||||||
|
|
||||||
self.logic: Optional[TabBLogic] = None
|
|
||||||
self._kartenname_combo: Optional[QComboBox] = None
|
|
||||||
self._massstab_combo: Optional[QComboBox] = None
|
|
||||||
self._thema_combo: Optional[QComboBox] = None
|
|
||||||
self._theme_signal_connected = False
|
|
||||||
self._connected_theme_collection: object = None # Referenz für sauberes Trennen
|
|
||||||
self._zielgroesse_combo: Optional[QComboBox] = None
|
|
||||||
self._endlosrolle_cb: Optional[QCheckBox] = None
|
|
||||||
self._btn_vorlage_erstellen: Optional[QPushButton] = None
|
|
||||||
|
|
||||||
self._build_ui()
|
|
||||||
self._restore_state()
|
|
||||||
self._connect_theme_collection_signals()
|
|
||||||
self._connect_project_signals()
|
|
||||||
|
|
||||||
def set_services(self, pruefmanager: Pruefmanager, data_grabber: DataGrabber) -> None:
|
|
||||||
"""Injiziert Services vom übergeordneten DockWidget."""
|
|
||||||
_ = data_grabber
|
|
||||||
self.pruefmanager = pruefmanager
|
|
||||||
self.logic = TabBLogic(pruefmanager=self.pruefmanager)
|
|
||||||
|
|
||||||
if self._kartenname_combo:
|
|
||||||
self.logic.set_kartenname_for_auswahl(self._kartenname_combo.currentText())
|
|
||||||
if self._massstab_combo:
|
|
||||||
self.logic.set_plotmassstab_for_auswahl(
|
|
||||||
self._massstab_combo.currentText(),
|
|
||||||
self._get_current_canvas_scale(),
|
|
||||||
)
|
|
||||||
if self._thema_combo:
|
|
||||||
self.logic.set_view_for_auswahl(self._thema_combo.currentText())
|
|
||||||
if self._zielgroesse_combo:
|
|
||||||
self.logic.set_zielgroesse_for_auswahl(self._zielgroesse_combo.currentText())
|
|
||||||
if self._endlosrolle_cb:
|
|
||||||
self.logic.set_formfaktor(self._endlosrolle_cb.isChecked())
|
|
||||||
|
|
||||||
def _build_ui(self) -> None:
|
|
||||||
"""Erstellt die reduzierte UI für die Themenauswahl."""
|
|
||||||
main_layout = QVBoxLayout()
|
|
||||||
main_layout.setSpacing(4)
|
|
||||||
main_layout.setContentsMargins(4, 4, 4, 4)
|
|
||||||
|
|
||||||
kartenname_label = QLabel("Kartenname")
|
|
||||||
kartenname_label.setStyleSheet("font-weight: bold; margin-top: 6px;")
|
|
||||||
main_layout.addWidget(kartenname_label)
|
|
||||||
|
|
||||||
self._kartenname_combo = QComboBox(self)
|
|
||||||
self._kartenname_combo.addItem(KARTENNAME_PLACEHOLDER)
|
|
||||||
self._kartenname_combo.addItem(KARTENNAME_38)
|
|
||||||
self._kartenname_combo.addItem(KARTENNAME_41)
|
|
||||||
self._kartenname_combo.currentTextChanged.connect(self._on_kartenname_changed)
|
|
||||||
main_layout.addWidget(self._kartenname_combo)
|
|
||||||
|
|
||||||
massstab_label = QLabel("Maßstab")
|
|
||||||
massstab_label.setStyleSheet("font-weight: bold; margin-top: 6px;")
|
|
||||||
main_layout.addWidget(massstab_label)
|
|
||||||
|
|
||||||
self._massstab_combo = QComboBox(self)
|
|
||||||
self._massstab_combo.addItem(MASSSTAB_WIE_KARTENFENSTER)
|
|
||||||
self._massstab_combo.addItems(list(PLOTMASSSTAB_BY_AUSWAHL.keys()))
|
|
||||||
self._massstab_combo.currentTextChanged.connect(self._on_massstab_changed)
|
|
||||||
main_layout.addWidget(self._massstab_combo)
|
|
||||||
|
|
||||||
thema_label = QLabel("Thema")
|
|
||||||
thema_label.setStyleSheet("font-weight: bold; margin-top: 6px;")
|
|
||||||
main_layout.addWidget(thema_label)
|
|
||||||
|
|
||||||
self._thema_combo = QComboBox(self)
|
|
||||||
self._thema_combo.addItem(THEMA_WIE_KARTENFENSTER)
|
|
||||||
self._thema_combo.addItems(self._get_gespeicherte_themen())
|
|
||||||
self._thema_combo.currentTextChanged.connect(self._on_thema_changed)
|
|
||||||
main_layout.addWidget(self._thema_combo)
|
|
||||||
|
|
||||||
zielgroesse_label = QLabel("max. Blattgröße")
|
|
||||||
zielgroesse_label.setStyleSheet("font-weight: bold; margin-top: 6px;")
|
|
||||||
main_layout.addWidget(zielgroesse_label)
|
|
||||||
|
|
||||||
zielgroesse_row = QHBoxLayout()
|
|
||||||
zielgroesse_row.setSpacing(6)
|
|
||||||
self._zielgroesse_combo = QComboBox(self)
|
|
||||||
self._zielgroesse_combo.addItems(list(DIN_GROESSEN.keys()))
|
|
||||||
self._zielgroesse_combo.setCurrentText(DIN_STANDARD)
|
|
||||||
self._zielgroesse_combo.currentTextChanged.connect(self._on_zielgroesse_changed)
|
|
||||||
zielgroesse_row.addWidget(self._zielgroesse_combo)
|
|
||||||
self._endlosrolle_cb = QCheckBox("Endlosrolle", self)
|
|
||||||
self._endlosrolle_cb.setChecked(False)
|
|
||||||
self._endlosrolle_cb.stateChanged.connect(self._on_formfaktor_changed)
|
|
||||||
zielgroesse_row.addWidget(self._endlosrolle_cb)
|
|
||||||
main_layout.addLayout(zielgroesse_row)
|
|
||||||
|
|
||||||
self._btn_vorlage_erstellen = QPushButton("Vorlage erstellen", self)
|
|
||||||
self._btn_vorlage_erstellen.clicked.connect(self._on_vorlage_erstellen)
|
|
||||||
main_layout.addWidget(self._btn_vorlage_erstellen)
|
|
||||||
|
|
||||||
main_layout.addStretch(1)
|
|
||||||
self.setLayout(main_layout)
|
|
||||||
|
|
||||||
def _restore_state(self) -> None:
|
|
||||||
"""Stellt die gespeicherten Combobox-Zustände wieder her."""
|
|
||||||
if not self._kartenname_combo or not self._massstab_combo or not self._thema_combo:
|
|
||||||
return
|
|
||||||
if not self._zielgroesse_combo or not self._endlosrolle_cb:
|
|
||||||
return
|
|
||||||
|
|
||||||
saved_kartenname = get_variable(KARTENNAME_VAR, scope="project")
|
|
||||||
if saved_kartenname in (KARTENNAME_38, KARTENNAME_41):
|
|
||||||
self._kartenname_combo.setCurrentText(saved_kartenname)
|
|
||||||
else:
|
|
||||||
self._kartenname_combo.setCurrentText(KARTENNAME_PLACEHOLDER)
|
|
||||||
|
|
||||||
saved_massstab = get_variable(MASSSTAB_VAR, scope="project")
|
|
||||||
valid_massstaebe = [MASSSTAB_WIE_KARTENFENSTER, *PLOTMASSSTAB_BY_AUSWAHL.keys()]
|
|
||||||
if saved_massstab in valid_massstaebe:
|
|
||||||
self._massstab_combo.setCurrentText(saved_massstab)
|
|
||||||
else:
|
|
||||||
self._massstab_combo.setCurrentText(MASSSTAB_WIE_KARTENFENSTER)
|
|
||||||
|
|
||||||
aktuelle_themen = [THEMA_WIE_KARTENFENSTER, *self._get_gespeicherte_themen()]
|
|
||||||
self._thema_combo.clear()
|
|
||||||
self._thema_combo.addItems(aktuelle_themen)
|
|
||||||
|
|
||||||
saved_thema = get_variable(THEMA_VAR, scope="project")
|
|
||||||
if saved_thema in aktuelle_themen:
|
|
||||||
self._thema_combo.setCurrentText(saved_thema)
|
|
||||||
else:
|
|
||||||
self._thema_combo.setCurrentText(THEMA_WIE_KARTENFENSTER)
|
|
||||||
|
|
||||||
saved_zielgroesse = get_variable(ZIELGROESSE_UI_VAR, scope="project")
|
|
||||||
if saved_zielgroesse in DIN_GROESSEN:
|
|
||||||
self._zielgroesse_combo.setCurrentText(saved_zielgroesse)
|
|
||||||
else:
|
|
||||||
self._zielgroesse_combo.setCurrentText(DIN_STANDARD)
|
|
||||||
|
|
||||||
saved_formfaktor = get_variable(FORMFAKTOR_UI_VAR, scope="project")
|
|
||||||
self._endlosrolle_cb.setChecked(saved_formfaktor == "Endlosrolle")
|
|
||||||
|
|
||||||
def _on_kartenname_changed(self, value: str) -> None:
|
|
||||||
"""Persistiert die Kartennamen-Auswahl und setzt ``kartenname``."""
|
|
||||||
if value in (KARTENNAME_38, KARTENNAME_41):
|
|
||||||
set_variable(KARTENNAME_VAR, value, scope="project")
|
|
||||||
else:
|
|
||||||
set_variable(KARTENNAME_VAR, "", scope="project")
|
|
||||||
|
|
||||||
if self.logic:
|
|
||||||
self.logic.set_kartenname_for_auswahl(value)
|
|
||||||
|
|
||||||
def _on_massstab_changed(self, value: str) -> None:
|
|
||||||
"""Persistiert Maßstabsauswahl und setzt ``plotmassstab``."""
|
|
||||||
set_variable(MASSSTAB_VAR, value, scope="project")
|
|
||||||
|
|
||||||
if self.logic:
|
|
||||||
self.logic.set_plotmassstab_for_auswahl(value, self._get_current_canvas_scale())
|
|
||||||
|
|
||||||
def _on_thema_changed(self, value: str) -> None:
|
|
||||||
"""Persistiert die Thema-Auswahl und setzt ``view``."""
|
|
||||||
set_variable(THEMA_VAR, value, scope="project")
|
|
||||||
if self.logic:
|
|
||||||
self.logic.set_view_for_auswahl(value)
|
|
||||||
|
|
||||||
def _on_zielgroesse_changed(self, value: str) -> None:
|
|
||||||
"""Persistiert Blattgröße und setzt ``zielgroesse``."""
|
|
||||||
set_variable(ZIELGROESSE_UI_VAR, value, scope="project")
|
|
||||||
if self.logic:
|
|
||||||
self.logic.set_zielgroesse_for_auswahl(value)
|
|
||||||
|
|
||||||
def _on_formfaktor_changed(self, state: int) -> None:
|
|
||||||
"""Persistiert Endlosrolle-Zustand und setzt ``formfaktor``."""
|
|
||||||
checked = bool(state)
|
|
||||||
set_variable(FORMFAKTOR_UI_VAR, "Endlosrolle" if checked else "Blatt", scope="project")
|
|
||||||
if self.logic:
|
|
||||||
self.logic.set_formfaktor(checked)
|
|
||||||
|
|
||||||
def _on_vorlage_erstellen(self) -> None:
|
|
||||||
"""Startet die Pipeline Druckvorlage_anlegen."""
|
|
||||||
print("[TabB] _on_vorlage_erstellen aufgerufen")
|
|
||||||
if not self.logic or not self.pruefmanager:
|
|
||||||
print(f"[TabB] Abbruch: logic={self.logic}, pruefmanager={self.pruefmanager}")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Layer direkt aus Tab A lesen (unabhängig von Projektvariable)
|
|
||||||
layer = None
|
|
||||||
tab_a = self._get_tab_a_widget()
|
|
||||||
print(f"[TabB] tab_a Widget: {tab_a!r}")
|
|
||||||
if tab_a is not None:
|
|
||||||
layer_combo = getattr(tab_a, "layer_combo", None)
|
|
||||||
print(f"[TabB] layer_combo: {layer_combo!r}")
|
|
||||||
if layer_combo is not None:
|
|
||||||
layer = layer_combo.currentLayer()
|
|
||||||
print(f"[TabB] layer: {layer!r} (Name: {getattr(layer, 'name', lambda: '–')()})")
|
|
||||||
|
|
||||||
print(f"[TabB] Rufe druckvorlage_anlegen auf mit layer={layer!r}, "
|
|
||||||
f"kartenname={self._kartenname_combo.currentText() if self._kartenname_combo else '?'}, "
|
|
||||||
f"massstab={self._massstab_combo.currentText() if self._massstab_combo else '?'}, "
|
|
||||||
f"zielgroesse={self._zielgroesse_combo.currentText() if self._zielgroesse_combo else '?'}, "
|
|
||||||
f"formfaktor={self._endlosrolle_cb.isChecked() if self._endlosrolle_cb else '?'}")
|
|
||||||
|
|
||||||
result = self.logic.druckvorlage_anlegen(
|
|
||||||
layer=layer,
|
|
||||||
kartenname_auswahl=self._kartenname_combo.currentText() if self._kartenname_combo else "",
|
|
||||||
massstab_auswahl=self._massstab_combo.currentText() if self._massstab_combo else "",
|
|
||||||
zielgroesse=self._zielgroesse_combo.currentText() if self._zielgroesse_combo else DIN_STANDARD,
|
|
||||||
formfaktor=self._endlosrolle_cb.isChecked() if self._endlosrolle_cb else False,
|
|
||||||
)
|
|
||||||
|
|
||||||
print(f"[TabB] druckvorlage_anlegen Ergebnis: {result}")
|
|
||||||
if result.get("switch_to_tab_a"):
|
|
||||||
self._aktiviere_tab_a()
|
|
||||||
|
|
||||||
def _get_tab_widget(self):
|
|
||||||
"""Findet das übergeordnete QTabWidget anhand des ``tabBar``-Attributs."""
|
|
||||||
try:
|
|
||||||
widget = self.parent()
|
|
||||||
while widget is not None:
|
|
||||||
if hasattr(widget, "tabBar") and hasattr(widget, "setCurrentIndex"):
|
|
||||||
return widget
|
|
||||||
parent_fn = getattr(widget, "parent", None)
|
|
||||||
widget = parent_fn() if callable(parent_fn) else None
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _get_tab_a_widget(self):
|
|
||||||
"""Gibt die Tab-A-Widget-Instanz zurück (Index 0 im übergeordneten QTabWidget)."""
|
|
||||||
tab_widget = self._get_tab_widget()
|
|
||||||
if tab_widget is None:
|
|
||||||
return None
|
|
||||||
try:
|
|
||||||
return tab_widget.widget(0)
|
|
||||||
except Exception:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _aktiviere_tab_a(self) -> None:
|
|
||||||
"""Wechselt den aktiven Reiter auf Tab A im übergeordneten QTabWidget."""
|
|
||||||
tab_widget = self._get_tab_widget()
|
|
||||||
if tab_widget is not None:
|
|
||||||
try:
|
|
||||||
tab_widget.setCurrentIndex(0)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def _connect_project_signals(self) -> None:
|
|
||||||
"""Verbindet QgsProject-Signale für Projektwechsel/-neuladen."""
|
|
||||||
project = QgsProject.instance()
|
|
||||||
for signal_name in ("readProject", "newProjectCreated", "cleared"):
|
|
||||||
signal = getattr(project, signal_name, None)
|
|
||||||
if signal is None:
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
signal.connect(self._on_project_changed)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def _on_project_changed(self, *args) -> None:
|
|
||||||
"""Reagiert auf Projektwechsel: Signale neu binden, Combobox und State auffrischen."""
|
|
||||||
_ = args
|
|
||||||
# Alte Theme-Collection-Signals zuerst trennen
|
|
||||||
self._disconnect_theme_collection_signals()
|
|
||||||
# Neu verbinden für das jetzt geladene Projekt
|
|
||||||
self._theme_signal_connected = False
|
|
||||||
self._connect_theme_collection_signals()
|
|
||||||
# Ansicht-Liste + gespeicherten State wiederherstellen
|
|
||||||
self._restore_state()
|
|
||||||
|
|
||||||
def _disconnect_theme_collection_signals(self) -> None:
|
|
||||||
"""Trennt Signale der alten Theme-Collection sauber."""
|
|
||||||
collection = self._connected_theme_collection
|
|
||||||
if collection is not None:
|
|
||||||
for signal_name in ("mapThemesChanged", "changed", "themeChanged"):
|
|
||||||
try:
|
|
||||||
signal = getattr(collection, signal_name, None)
|
|
||||||
except RuntimeError:
|
|
||||||
break
|
|
||||||
except Exception:
|
|
||||||
continue
|
|
||||||
if signal is None:
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
signal.disconnect(self._refresh_thema_combo_live)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
self._connected_theme_collection = None
|
|
||||||
self._theme_signal_connected = False
|
|
||||||
|
|
||||||
def _connect_theme_collection_signals(self) -> None:
|
|
||||||
"""Verbindet Signale der Theme-Collection für Live-Aktualisierung der Themenliste."""
|
|
||||||
if self._theme_signal_connected:
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
theme_collection = QgsProject.instance().mapThemeCollection()
|
|
||||||
except Exception:
|
|
||||||
return
|
|
||||||
|
|
||||||
if theme_collection is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
connected_any = False
|
|
||||||
for signal_name in ("mapThemesChanged", "changed", "themeChanged"):
|
|
||||||
signal = getattr(theme_collection, signal_name, None)
|
|
||||||
if signal is None:
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
signal.connect(self._refresh_thema_combo_live)
|
|
||||||
connected_any = True
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if connected_any:
|
|
||||||
self._connected_theme_collection = theme_collection
|
|
||||||
self._theme_signal_connected = connected_any
|
|
||||||
|
|
||||||
def _refresh_thema_combo_live(self, *args) -> None:
|
|
||||||
"""Aktualisiert die Thema-Combobox bei Änderungen gespeicherter Layerthemen."""
|
|
||||||
_ = args
|
|
||||||
if not self._thema_combo:
|
|
||||||
return
|
|
||||||
|
|
||||||
vorherige_auswahl = self._thema_combo.currentText() or THEMA_WIE_KARTENFENSTER
|
|
||||||
eintraege = [THEMA_WIE_KARTENFENSTER, *self._get_gespeicherte_themen()]
|
|
||||||
|
|
||||||
self._thema_combo.blockSignals(True)
|
|
||||||
self._thema_combo.clear()
|
|
||||||
self._thema_combo.addItems(eintraege)
|
|
||||||
|
|
||||||
if vorherige_auswahl in eintraege:
|
|
||||||
self._thema_combo.setCurrentText(vorherige_auswahl)
|
|
||||||
else:
|
|
||||||
self._thema_combo.setCurrentText(THEMA_WIE_KARTENFENSTER)
|
|
||||||
self._thema_combo.blockSignals(False)
|
|
||||||
|
|
||||||
self._on_thema_changed(self._thema_combo.currentText())
|
|
||||||
|
|
||||||
def _get_gespeicherte_themen(self) -> list[str]:
|
|
||||||
"""Liefert die Namen der im Projekt gespeicherten Layerthemen (QgsMapThemeCollection)."""
|
|
||||||
try:
|
|
||||||
theme_collection = QgsProject.instance().mapThemeCollection()
|
|
||||||
if theme_collection is None:
|
|
||||||
return []
|
|
||||||
themes = theme_collection.mapThemes()
|
|
||||||
except Exception:
|
|
||||||
return []
|
|
||||||
|
|
||||||
namen: list[str] = []
|
|
||||||
for theme_name in themes:
|
|
||||||
name = str(theme_name or "").strip()
|
|
||||||
if name and name not in namen:
|
|
||||||
namen.append(name)
|
|
||||||
|
|
||||||
return namen
|
|
||||||
|
|
||||||
def _get_current_canvas_scale(self) -> float | None:
|
|
||||||
"""Liest den aktuellen Maßstab aus der Kartensicht."""
|
|
||||||
try:
|
|
||||||
canvas = iface.mapCanvas()
|
|
||||||
if canvas is None:
|
|
||||||
return None
|
|
||||||
scale = canvas.scale()
|
|
||||||
return float(scale) if scale else None
|
|
||||||
except Exception:
|
|
||||||
return None
|
|
||||||
|
|
||||||
@@ -1,9 +1,24 @@
|
|||||||
|
"""sn_plan41/ui/tabs/tab_b.py – Beispiel-Tab für zusätzliche Funktionen.
|
||||||
|
Ein Tab für zukünftige Erweiterungen.
|
||||||
|
"""
|
||||||
|
|
||||||
from qgis.PyQt.QtWidgets import QWidget, QVBoxLayout, QLabel, QTextEdit
|
from qgis.PyQt.QtWidgets import QWidget, QVBoxLayout, QLabel, QTextEdit
|
||||||
|
|
||||||
class TabB(QWidget):
|
class TabB(QWidget):
|
||||||
|
"""Beispiel-Tab für sn_plan41.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
tab_title: Anzeigetitel des Tabs ("Tab B")
|
||||||
|
"""
|
||||||
|
|
||||||
tab_title = "Tab B"
|
tab_title = "Tab B"
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
|
"""Initialisiert den Tab mit Beispielinhalt.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
parent: Übergeordnetes Widget
|
||||||
|
"""
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
layout = QVBoxLayout()
|
layout = QVBoxLayout()
|
||||||
layout.addWidget(QLabel("Plugin2 – Tab B"))
|
layout.addWidget(QLabel("Plugin2 – Tab B"))
|
||||||
|
|||||||
Reference in New Issue
Block a user