Coverage for gws-app/gws/base/ows/server/layer_caps.py: 81%
93 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-05 13:35 +0200
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-05 13:35 +0200
1"""Utilities for LayerCaps objects."""
3from typing import Optional
5import gws
6import gws.lib.extent
7import gws.gis.zoom
8import gws.lib.xmlx as xmlx
9from gws.lib.xmlx import tag
11from . import core
14def for_layer(layer: gws.Layer, user: gws.User, service: Optional[gws.OwsService] = None) -> core.LayerCaps:
15 """Create ``LayerCaps`` for a layer.
17 ``isGroup`` is not set and ``children`` and ``leaves`` are empty;
18 the caller fills them in when building the layer tree.
20 Args:
21 layer: Layer object.
22 user: User, used to find a readable model for the layer.
23 service: Service; if given, ``bounds`` are computed for its supported bounds.
25 Returns:
26 The layer caps.
27 """
29 lc = core.LayerCaps(leaves=[], children=[])
31 lc.layer = layer
32 lc.title = layer.title
33 lc.model = layer.root.app.modelMgr.find_model(layer.ows, layer, user=user, access=gws.Access.read)
35 geom_name = layer.ows.geometryName
36 if not geom_name and lc.model:
37 geom_name = lc.model.geometryName
38 if not geom_name:
39 geom_name = 'geometry'
41 lc.layerName = layer.ows.layerName
42 lc.featureName = layer.ows.featureName
43 lc.geometryName = geom_name
45 lc.xmlNamespace = layer.ows.xmlNamespace
47 lc.hasLegend = layer.hasLegend
48 lc.isSearchable = layer.isSearchable
50 scales = [gws.gis.zoom.res_to_scale(r, layer.mapCrs) for r in layer.resolutions]
51 lc.minScale = int(min(scales))
52 lc.maxScale = int(max(scales))
54 lc.bounds = []
55 if service:
56 lc.bounds = [
57 gws.Bounds(
58 crs=b.crs,
59 extent=gws.lib.extent.transform_from_wgs(layer.wgsExtent, b.crs),
60 )
61 for b in service.supportedBounds
62 ]
64 return lc
67def layer_name_matches(lc: core.LayerCaps, name: str) -> bool:
68 """Check if the layer name in the caps matches the given name, ignoring a prefix.
70 Args:
71 lc: Layer caps.
72 name: Layer name, possibly with a namespace prefix.
74 Returns:
75 ``True`` if the names match.
76 """
78 return xmlx.namespace.plain_name(name) == lc.layerName
80def feature_prefix(lc: core.LayerCaps, custom_namespace_prefixes: dict) -> str:
81 """Return the namespace prefix of the feature type.
83 Args:
84 lc: Layer caps.
85 custom_namespace_prefixes: Custom prefixes by namespace uri, as requested with the WFS ``NAMESPACES`` parameter.
87 Returns:
88 The custom prefix for the layer namespace if there is one, otherwise the namespace prefix,
89 or an empty string if the layer has no namespace.
90 """
92 ns = lc.xmlNamespace
93 if not ns:
94 return ''
95 return custom_namespace_prefixes.get(ns.uri, ns.prefix)
98def qualified_feature_name(lc: core.LayerCaps, custom_namespace_prefixes: dict) -> str:
99 """Return the feature type name as a QName (``prefix:name``), for use in text content.
101 Args:
102 lc: Layer caps.
103 custom_namespace_prefixes: Custom prefixes by namespace uri.
105 Returns:
106 The qualified name, or the plain feature name if the layer has no namespace.
107 """
109 prefix = feature_prefix(lc, custom_namespace_prefixes)
110 return prefix + ':' + lc.featureName if prefix else lc.featureName
113def feature_name_matches(lc: core.LayerCaps, name: str, custom_namespace_prefixes: dict) -> bool:
114 """Check if the feature name in the caps matches the given name, which may be a QName.
116 A name without a prefix matches any namespace.
118 Args:
119 lc: Layer caps.
120 name: Feature name, optionally with a prefix.
121 custom_namespace_prefixes: Custom prefixes by namespace uri.
123 Returns:
124 ``True`` if the names match.
125 """
127 _, prefix, pname = xmlx.namespace.parse_name(name)
128 if pname != lc.featureName:
129 return False
130 return not prefix or prefix == feature_prefix(lc, custom_namespace_prefixes)
133def xml_schema(lcs: list[core.LayerCaps], user: gws.User) -> tuple[gws.XmlElement, gws.XmlOptions]:
134 """Create an ad-hoc XML Schema for a list of ``LayerCaps``.
136 All layers must have a model and share the same XML namespace. The schema
137 contains a complex type and an element for each feature type, with elements
138 for the model fields the user can read.
140 Args:
141 lcs: Layer caps.
142 user: User.
144 Returns:
145 The schema element and XML options to serialize it.
147 Raises:
148 ``gws.NotFoundError``: If a layer has no namespace or model, or the namespaces differ.
149 """
151 ns = None
153 for lc in lcs:
154 if not lc.xmlNamespace:
155 raise gws.NotFoundError(f'xml_schema: {lc.layer.uid}: no xmlns')
156 if not lc.model:
157 raise gws.NotFoundError(f'xml_schema: {lc.layer.uid}: no model')
158 if not ns:
159 ns = lc.xmlNamespace
160 elif lc.xmlNamespace.prefix != ns.prefix:
161 raise gws.NotFoundError(f'xml_schema: {lc.layer.uid}: wrong xmlns: {ns.prefix=} {lc.xmlNamespace.prefix=}')
163 if not ns:
164 raise gws.NotFoundError('xml_schema: no xmlns found')
166 opts = gws.XmlOptions()
168 schema = tag(
169 'XSD:schema',
170 {
171 'targetNamespace': ns.uri,
172 'elementFormDefault': 'qualified',
173 },
174 )
175 schema.declare(ns)
177 if ns.extendsGml:
178 gml = xmlx.namespace.c.GML
179 schema.declare(gml)
180 schema.append(tag('XSD:import', {'namespace': gml.uri, 'schemaLocation': gml.schemaLocation}))
182 seen = set()
184 for lc in lcs:
185 if lc.featureName in seen:
186 continue
187 seen.add(lc.featureName)
189 elements = []
191 for f in gws.u.require(lc.model).fields:
192 if user.can_read(f):
193 elements.append(
194 tag(
195 'XSD:element',
196 {
197 'maxOccurs': '1',
198 'minOccurs': '0',
199 'nillable': 'false' if f.isRequired else 'true',
200 'name': f.name,
201 'type': _xsd_type(f),
202 },
203 )
204 )
206 type_name = f'{lc.featureName}Type'
208 if ns.extendsGml:
209 type_def = tag(
210 'XSD:complexContent',
211 tag('XSD:extension', {'base': 'gml:AbstractFeatureType'}, tag('XSD:sequence', elements)),
212 )
213 else:
214 type_def = tag('XSD:complexContent', tag('XSD:sequence', elements))
216 schema.append(tag('XSD:complexType', {'name': type_name}, type_def))
218 atts = {
219 'name': lc.featureName,
220 'type': ns.prefix + ':' + type_name,
221 }
222 if ns.extendsGml:
223 atts['substitutionGroup'] = 'gml:AbstractFeature'
225 schema.append(tag('XSD:element', atts))
227 return schema, opts
230def _xsd_type(f: gws.ModelField) -> str:
231 """Return the XSD type for a model field."""
233 if f.attributeType != gws.AttributeType.geometry:
234 return _ATTR_TO_XSD.get(f.attributeType, 'xsd:string')
236 typ = gws.u.get(f, 'geometryType')
237 if not typ:
238 return 'gml:GeometryPropertyType'
239 return _GEOM_TO_XSD.get(typ, 'gml:GeometryPropertyType')
242# map attributes types to XSD
243# https://www.w3.org/TR/xmlschema11-2/#built-in-primitive-datatypes
245_ATTR_TO_XSD = {
246 gws.AttributeType.bool: 'xsd:boolean',
247 gws.AttributeType.bytes: 'xsd:hexBinary',
248 gws.AttributeType.date: 'xsd:date',
249 gws.AttributeType.datetime: 'xsd:dateTime',
250 gws.AttributeType.feature: '',
251 gws.AttributeType.featurelist: '',
252 gws.AttributeType.file: '',
253 gws.AttributeType.float: 'xsd:float',
254 gws.AttributeType.floatlist: '',
255 gws.AttributeType.geometry: 'gml:GeometryPropertyType',
256 gws.AttributeType.int: 'xsd:decimal',
257 gws.AttributeType.intlist: '',
258 gws.AttributeType.str: 'xsd:string',
259 gws.AttributeType.strlist: '',
260 gws.AttributeType.time: 'xsd:time',
261}
263_GEOM_TO_XSD = {
264 gws.GeometryType.point: 'gml:PointPropertyType',
265 gws.GeometryType.linestring: 'gml:CurvePropertyType',
266 gws.GeometryType.polygon: 'gml:SurfacePropertyType',
267 gws.GeometryType.multipoint: 'gml:MultiPointPropertyType',
268 gws.GeometryType.multilinestring: 'gml:MultiCurvePropertyType',
269 gws.GeometryType.multipolygon: 'gml:MultiSurfacePropertyType',
270 gws.GeometryType.multicurve: 'gml:MultiCurvePropertyType',
271 gws.GeometryType.multisurface: 'gml:MultiSurfacePropertyType',
272 gws.GeometryType.linearring: 'gml:LinearRingPropertyType',
273 gws.GeometryType.tin: 'gml:TriangulatedSurfacePropertyType',
274 gws.GeometryType.surface: 'gml:SurfacePropertyType',
275}