Coverage for gws-app/gws/base/ows/server/layer_caps.py: 74%

100 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-10-16 23:09 +0200

1"""Utilities to deal with LayerCaps objects.""" 

2 

3from typing import Optional 

4 

5import gws 

6import gws.lib.extent 

7import gws.lib.uom 

8import gws.lib.xmlx as xmlx 

9 

10from . import core 

11 

12 

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

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

15 

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

17 

18 lc.layer = layer 

19 lc.title = layer.title 

20 lc.model = layer.root.app.modelMgr.find_model(layer.ows, layer, user=user, access=gws.Access.read) 

21 

22 geom_name = layer.ows.geometryName 

23 if not geom_name and lc.model: 

24 geom_name = lc.model.geometryName 

25 if not geom_name: 

26 geom_name = 'geometry' 

27 

28 lc.layerName = layer.ows.layerName 

29 lc.featureName = layer.ows.featureName 

30 lc.geometryName = geom_name 

31 

32 lc.xmlNamespace = layer.ows.xmlNamespace 

33 

34 if lc.xmlNamespace: 

35 lc.layerNameQ = xmlx.namespace.qualify_name(lc.layerName, lc.xmlNamespace) 

36 lc.featureNameQ = xmlx.namespace.qualify_name(lc.featureName, lc.xmlNamespace) 

37 lc.geometryNameQ = xmlx.namespace.qualify_name(lc.geometryName, lc.xmlNamespace) 

38 else: 

39 lc.layerNameQ = lc.layerName 

40 lc.featureNameQ = lc.featureName 

41 lc.geometryNameQ = lc.geometryName 

42 

43 lc.hasLegend = layer.hasLegend 

44 lc.isSearchable = layer.isSearchable 

45 

46 scales = [gws.lib.uom.res_to_scale(r) for r in layer.resolutions] 

47 lc.minScale = int(min(scales)) 

48 lc.maxScale = int(max(scales)) 

49 

50 lc.bounds = [] 

51 if service: 

52 lc.bounds = [ 

53 gws.Bounds( 

54 crs=b.crs, 

55 extent=gws.lib.extent.transform_from_wgs(layer.wgsExtent, b.crs), 

56 ) 

57 for b in service.supportedBounds 

58 ] 

59 

60 return lc 

61 

62 

63def layer_name_matches(lc: core.LayerCaps, name: str) -> bool: 

64 """Check if the layer name in the caps matches the given name.""" 

65 

66 if ':' in name: 

67 return name == lc.layerNameQ 

68 else: 

69 return name == lc.layerName 

70 

71 

72def feature_name_matches(lc: core.LayerCaps, name: str, xmlns_replacements: dict) -> bool: 

73 """Check if the feature name in the caps matches the given name.""" 

74 

75 if name == lc.featureNameQ: 

76 return True 

77 

78 if ':' not in name: 

79 return name == lc.featureName 

80 

81 custom_xmlns, _, name = xmlx.namespace.split_name(name) 

82 if name == lc.featureName and lc.xmlNamespace and lc.xmlNamespace.uid in xmlns_replacements: 

83 return xmlns_replacements[lc.xmlNamespace.uid] == custom_xmlns 

84 

85 return False 

86 

87 

88def xml_schema(lcs: list[core.LayerCaps], user: gws.User) -> tuple[gws.XmlElement, gws.XmlOptions]: 

89 """Create an ad-hoc XML Schema for a list of `LayerCaps`.""" 

90 

91 ns = None 

92 

93 for lc in lcs: 

94 if not lc.xmlNamespace: 

95 raise gws.NotFoundError(f'xml_schema: {lc.layer.uid}: no xmlns') 

96 if not lc.model: 

97 raise gws.NotFoundError(f'xml_schema: {lc.layer.uid}: no model') 

98 if not ns: 

99 ns = lc.xmlNamespace 

100 elif lc.xmlNamespace.xmlns != ns.xmlns: 

101 raise gws.NotFoundError(f'xml_schema: {lc.layer.uid}: wrong xmlns: {ns.xmlns=} {lc.xmlNamespace.xmlns=}') 

102 

103 if not ns: 

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

105 

106 opts = gws.XmlOptions() 

107 opts.namespaces = {} 

108 opts.namespaces[ns.xmlns] = ns 

109 

110 tag = [ 

111 'xsd:schema', 

112 { 

113 'targetNamespace': ns.uri, 

114 'elementFormDefault': 'qualified', 

115 }, 

116 ] 

117 

118 if ns.extendsGml: 

119 gml = xmlx.namespace.require('gml') 

120 opts.namespaces[gml.xmlns] = gml 

121 tag.append(['xsd:import', {'namespace': gml.uri, 'schemaLocation': gml.schemaLocation}]) 

122 

123 seen = set() 

124 

125 for lc in lcs: 

126 if lc.featureName in seen: 

127 continue 

128 seen.add(lc.featureName) 

129 

130 elements = [] 

131 

132 for f in gws.u.require(lc.model).fields: 

133 if user.can_read(f): 

134 elements.append( 

135 [ 

136 'xsd:element', 

137 { 

138 'maxOccurs': '1', 

139 'minOccurs': '0', 

140 'nillable': 'false' if f.isRequired else 'true', 

141 'name': f.name, 

142 'type': _xsd_type(f), 

143 }, 

144 ] 

145 ) 

146 

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

148 

149 type_def = [] 

150 type_def.append('xsd:complexContent') 

151 if ns.extendsGml: 

152 type_def.append( 

153 ['xsd:extension', {'base': 'gml:AbstractFeatureType'}, ['xsd:sequence', elements]], 

154 ) 

155 else: 

156 type_def.append(['xsd:sequence', elements]) 

157 

158 tag.append(['xsd:complexType', {'name': type_name}, type_def]) 

159 

160 atts = { 

161 'name': lc.featureName, 

162 'type': ns.xmlns + ':' + type_name, 

163 } 

164 if ns.extendsGml: 

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

166 

167 tag.append(['xsd:element', atts]) 

168 

169 return xmlx.tag(*tag), opts 

170 

171 

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

173 """Get the XSD type for a model field.""" 

174 

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

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

177 

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

179 if not typ: 

180 return 'gml:GeometryPropertyType' 

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

182 

183 

184# map attributes types to XSD 

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

186 

187_ATTR_TO_XSD = { 

188 gws.AttributeType.bool: 'xsd:boolean', 

189 gws.AttributeType.bytes: 'xsd:hexBinary', 

190 gws.AttributeType.date: 'xsd:date', 

191 gws.AttributeType.datetime: 'xsd:dateTime', 

192 gws.AttributeType.feature: '', 

193 gws.AttributeType.featurelist: '', 

194 gws.AttributeType.file: '', 

195 gws.AttributeType.float: 'xsd:float', 

196 gws.AttributeType.floatlist: '', 

197 gws.AttributeType.geometry: 'gml:GeometryPropertyType', 

198 gws.AttributeType.int: 'xsd:decimal', 

199 gws.AttributeType.intlist: '', 

200 gws.AttributeType.str: 'xsd:string', 

201 gws.AttributeType.strlist: '', 

202 gws.AttributeType.time: 'xsd:time', 

203} 

204 

205_GEOM_TO_XSD = { 

206 gws.GeometryType.point: 'gml:PointPropertyType', 

207 gws.GeometryType.linestring: 'gml:CurvePropertyType', 

208 gws.GeometryType.polygon: 'gml:SurfacePropertyType', 

209 gws.GeometryType.multipoint: 'gml:MultiPointPropertyType', 

210 gws.GeometryType.multilinestring: 'gml:MultiCurvePropertyType', 

211 gws.GeometryType.multipolygon: 'gml:MultiSurfacePropertyType', 

212 gws.GeometryType.multicurve: 'gml:MultiCurvePropertyType', 

213 gws.GeometryType.multisurface: 'gml:MultiSurfacePropertyType', 

214 gws.GeometryType.linearring: 'gml:LinearRingPropertyType', 

215 gws.GeometryType.tin: 'gml:TriangulatedSurfacePropertyType', 

216 gws.GeometryType.surface: 'gml:SurfacePropertyType', 

217}