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

1"""Utilities for LayerCaps objects.""" 

2 

3from typing import Optional 

4 

5import gws 

6import gws.lib.extent 

7import gws.gis.zoom 

8import gws.lib.xmlx as xmlx 

9from gws.lib.xmlx import tag 

10 

11from . import core 

12 

13 

14def for_layer(layer: gws.Layer, user: gws.User, service: Optional[gws.OwsService] = None) -> core.LayerCaps: 

15 """Create ``LayerCaps`` for a layer. 

16 

17 ``isGroup`` is not set and ``children`` and ``leaves`` are empty; 

18 the caller fills them in when building the layer tree. 

19 

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. 

24 

25 Returns: 

26 The layer caps. 

27 """ 

28 

29 lc = core.LayerCaps(leaves=[], children=[]) 

30 

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) 

34 

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' 

40 

41 lc.layerName = layer.ows.layerName 

42 lc.featureName = layer.ows.featureName 

43 lc.geometryName = geom_name 

44 

45 lc.xmlNamespace = layer.ows.xmlNamespace 

46 

47 lc.hasLegend = layer.hasLegend 

48 lc.isSearchable = layer.isSearchable 

49 

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)) 

53 

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 ] 

63 

64 return lc 

65 

66 

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. 

69 

70 Args: 

71 lc: Layer caps. 

72 name: Layer name, possibly with a namespace prefix. 

73 

74 Returns: 

75 ``True`` if the names match. 

76 """ 

77 

78 return xmlx.namespace.plain_name(name) == lc.layerName 

79 

80def feature_prefix(lc: core.LayerCaps, custom_namespace_prefixes: dict) -> str: 

81 """Return the namespace prefix of the feature type. 

82 

83 Args: 

84 lc: Layer caps. 

85 custom_namespace_prefixes: Custom prefixes by namespace uri, as requested with the WFS ``NAMESPACES`` parameter. 

86 

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 """ 

91 

92 ns = lc.xmlNamespace 

93 if not ns: 

94 return '' 

95 return custom_namespace_prefixes.get(ns.uri, ns.prefix) 

96 

97 

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. 

100 

101 Args: 

102 lc: Layer caps. 

103 custom_namespace_prefixes: Custom prefixes by namespace uri. 

104 

105 Returns: 

106 The qualified name, or the plain feature name if the layer has no namespace. 

107 """ 

108 

109 prefix = feature_prefix(lc, custom_namespace_prefixes) 

110 return prefix + ':' + lc.featureName if prefix else lc.featureName 

111 

112 

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. 

115 

116 A name without a prefix matches any namespace. 

117 

118 Args: 

119 lc: Layer caps. 

120 name: Feature name, optionally with a prefix. 

121 custom_namespace_prefixes: Custom prefixes by namespace uri. 

122 

123 Returns: 

124 ``True`` if the names match. 

125 """ 

126 

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) 

131 

132 

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``. 

135 

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. 

139 

140 Args: 

141 lcs: Layer caps. 

142 user: User. 

143 

144 Returns: 

145 The schema element and XML options to serialize it. 

146 

147 Raises: 

148 ``gws.NotFoundError``: If a layer has no namespace or model, or the namespaces differ. 

149 """ 

150 

151 ns = None 

152 

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=}') 

162 

163 if not ns: 

164 raise gws.NotFoundError('xml_schema: no xmlns found') 

165 

166 opts = gws.XmlOptions() 

167 

168 schema = tag( 

169 'XSD:schema', 

170 { 

171 'targetNamespace': ns.uri, 

172 'elementFormDefault': 'qualified', 

173 }, 

174 ) 

175 schema.declare(ns) 

176 

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})) 

181 

182 seen = set() 

183 

184 for lc in lcs: 

185 if lc.featureName in seen: 

186 continue 

187 seen.add(lc.featureName) 

188 

189 elements = [] 

190 

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 ) 

205 

206 type_name = f'{lc.featureName}Type' 

207 

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)) 

215 

216 schema.append(tag('XSD:complexType', {'name': type_name}, type_def)) 

217 

218 atts = { 

219 'name': lc.featureName, 

220 'type': ns.prefix + ':' + type_name, 

221 } 

222 if ns.extendsGml: 

223 atts['substitutionGroup'] = 'gml:AbstractFeature' 

224 

225 schema.append(tag('XSD:element', atts)) 

226 

227 return schema, opts 

228 

229 

230def _xsd_type(f: gws.ModelField) -> str: 

231 """Return the XSD type for a model field.""" 

232 

233 if f.attributeType != gws.AttributeType.geometry: 

234 return _ATTR_TO_XSD.get(f.attributeType, 'xsd:string') 

235 

236 typ = gws.u.get(f, 'geometryType') 

237 if not typ: 

238 return 'gml:GeometryPropertyType' 

239 return _GEOM_TO_XSD.get(typ, 'gml:GeometryPropertyType') 

240 

241 

242# map attributes types to XSD 

243# https://www.w3.org/TR/xmlschema11-2/#built-in-primitive-datatypes 

244 

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} 

262 

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}