Coverage for gws-app/gws/base/ows/server/templatelib.py: 78%
128 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-16 23:09 +0200
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-16 23:09 +0200
1"""Helper functions for OWS service templates."""
3from typing import Optional, cast
5import gws
6import gws.base.metadata
7import gws.lib.gml
8import gws.lib.uom
9import gws.lib.datetimex as dtx
10import gws.lib.xmlx as xmlx
11import gws.base.ows.server as server
13from . import core, service
16# OGC 06-121r9 Table 34
17# Ordered sequence of two double values in decimal degrees, with longitude before latitude
18def ows_wgs84_bounding_box(lc: core.LayerCaps):
19 return (
20 'ows:WGS84BoundingBox',
21 ('ows:LowerCorner', coord_dms(lc.layer.wgsExtent[0]), ' ', coord_dms(lc.layer.wgsExtent[1])),
22 ('ows:UpperCorner', coord_dms(lc.layer.wgsExtent[2]), ' ', coord_dms(lc.layer.wgsExtent[3])),
23 )
26# OGC 06-121r3 sec 7.4.4
27def ows_service_identification(ta: server.TemplateArgs):
28 md = ta.service.metadata
30 return (
31 'ows:ServiceIdentification',
32 ('ows:Title', md.title),
33 ('ows:Abstract', md.abstract),
34 ows_keywords(md),
35 ('ows:ServiceType', ta.service.protocol),
36 ('ows:ServiceTypeVersion', ta.version),
37 ('ows:Fees', md.fees) if md.fees else None,
38 ('ows:AccessConstraints', md.accessConstraints) if md.accessConstraints else None,
39 )
42# OGC 06-121r3 sec 7.4.5
43def ows_service_provider(ta: server.TemplateArgs):
44 md = ta.service.metadata
46 return (
47 'ows:ServiceProvider',
48 ('ows:ProviderName', md.contactProviderName),
49 ('ows:ProviderSite', {'xlink:href': md.contactProviderSite}),
50 (
51 'ows:ServiceContact',
52 ('ows:IndividualName', md.contactPerson),
53 ('ows:PositionName', md.contactPosition),
54 (
55 'ows:ContactInfo',
56 ('ows:Phone', ('ows:Voice', md.contactPhone), ('ows:Facsimile', md.contactFax)),
57 (
58 'ows:Address',
59 ('ows:DeliveryPoint', md.contactAddress),
60 ('ows:City', md.contactCity),
61 ('ows:AdministrativeArea', md.contactArea),
62 ('ows:PostalCode', md.contactZip),
63 ('ows:Country', md.contactCountry),
64 ('ows:ElectronicMailAddress', md.contactEmail),
65 ),
66 ('ows:OnlineResource', {'xlink:href': md.contactUrl}),
67 ),
68 ('ows:Role', md.contactRole),
69 ),
70 )
73# OGC 06-121r3 table 15,16,17
74# OGC 06-121r3 11.2:
75# A URL prefix is defined as a string including... mandatory question mark
78def ows_service_url(ta: server.TemplateArgs, get=True, post=False):
79 if get:
80 yield 'ows:DCP/ows:HTTP/ows:Get', {'xlink:type': 'simple', 'xlink:href': ta.serviceUrl + '?'}
81 if post:
82 yield 'ows:DCP/ows:HTTP/ows:Post', {'xlink:type': 'simple', 'xlink:href': ta.serviceUrl}
85def ows_value(value):
86 return 'ows:Value', value
89def online_resource(url):
90 return 'OnlineResource', {'xlink:type': 'simple', 'xlink:href': url}
93# OGC 01-068r3, 6.2.2
94# The URL prefix shall end in either a '?' (in the absence of additional server-specific parameters) or a '&'.
95# OGC 06-042, 6.3.3
96# A URL prefix is defined... as a string including... mandatory question mark
99def dcp_service_url(ta: server.TemplateArgs):
100 return 'DCPType/HTTP/Get', online_resource(ta.serviceUrl + '?')
103def legend_url_nested(ta: server.TemplateArgs, lc: core.LayerCaps, size=None):
104 name = xmlx.namespace.unqualify_name(lc.layerNameQ)
105 return (
106 'LegendURL',
107 {'width': size[0], 'height': size[1]} if size else {},
108 ('Format', 'image/png'),
109 online_resource(f'{ta.serviceUrl}?request=GetLegendGraphic&layer={name}'),
110 )
113def legend_url(ta: server.TemplateArgs, lc: core.LayerCaps, size=None):
114 name = xmlx.namespace.unqualify_name(lc.layerNameQ)
115 return (
116 'LegendURL',
117 {
118 'format': 'image/png',
119 'xlink:href': f'{ta.serviceUrl}?request=GetLegendGraphic&layer={name}',
120 },
121 )
124def ows_keywords(md: gws.Metadata):
125 return [_ows_keyword_group(kg) for kg in gws.base.metadata.keyword_groups(md)]
128def _ows_keyword_group(kg: gws.base.metadata.KeywordGroup):
129 tags = []
130 for kw in kg.keywords:
131 tags.append(('ows:Keyword', kw))
132 if kg.codeSpace:
133 tags.append(('ows:Type', {'codeSpace': kg.codeSpace}, kg.typeName))
134 return 'ows:Keywords', tags
137def wms_keywords(md: gws.Metadata, with_vocabulary: bool = False):
138 tags = []
139 for kg in gws.base.metadata.keyword_groups(md):
140 for kw in kg.keywords:
141 if kg.codeSpace and with_vocabulary:
142 tags.append(('Keyword', kw, {'vocabulary': kg.codeSpace}))
143 else:
144 tags.append(('Keyword', kw))
145 return 'KeywordList', tags
148def lon_lat_envelope(lc: core.LayerCaps):
149 return (
150 'lonLatEnvelope',
151 {'srsName': 'urn:ogc:def:crs:OGC:1.3:CRS84'},
152 ('gml:pos', coord_dms(lc.layer.wgsExtent[0]), ' ', coord_dms(lc.layer.wgsExtent[1])),
153 ('gml:pos', coord_dms(lc.layer.wgsExtent[2]), ' ', coord_dms(lc.layer.wgsExtent[3])),
154 )
157# Nested format (WFS 1, WMS):
158# OGC 06-042 7.2.4.6.11
159# The "type" attribute indicates the standard... The enclosed <Format> element... etc
162def meta_links_nested(ta: server.TemplateArgs, md: gws.Metadata):
163 if md.metaLinks:
164 for ml in md.metaLinks:
165 yield meta_url_nested(ta, ml, 'MetadataURL')
168def meta_url_nested(ta: server.TemplateArgs, ml: gws.MetadataLink, name: str):
169 if ml:
170 yield name, {'type': ml.type}, ('Format', ml.format), online_resource(ta.url_for(ml.url))
173# Simple format (WFS 2)
174# OGC 09-025r1 Table 11
175# The xlink:href element shall be used to reference any metadata.
176# The optional about attribute may be used to reference the aspect of the element which includes
177# this wfs:MetadataURL element that this metadata provides more information about.
178# (whatever that means)
181def meta_links_simple(ta: server.TemplateArgs, md: gws.Metadata):
182 if md.metaLinks:
183 for ml in md.metaLinks:
184 yield meta_url_simple(ta, ml, 'MetadataURL')
187def meta_url_simple(ta: server.TemplateArgs, ml: gws.MetadataLink, name: str):
188 if ml:
189 yield name, {'xlink:href': ta.url_for(ml.url), 'about': ml.about}
192def wfs_feature_collection(ta: server.TemplateArgs):
193 return (
194 'wfs:FeatureCollection',
195 wfs_feature_collection_attributes(ta),
196 [
197 (
198 f'wfs:member/{m.layerCaps.featureNameQ if m.layerCaps else "wfs:feature"}',
199 {'gml:id': gml_format_uid(ta, m.feature.uid())},
200 wfs_feature_collection_member(ta, m),
201 )
202 for m in ta.featureCollection.members
203 ],
204 )
207def wfs_value_collection(ta: server.TemplateArgs):
208 return (
209 'wfs:ValueCollection',
210 wfs_feature_collection_attributes(ta),
211 [('wfs:member', gml_format_value(ta, val)) for val in ta.featureCollection.values],
212 )
215def wfs_feature_collection_attributes(ta):
216 return {
217 'timeStamp': ta.featureCollection.timestamp,
218 'numberMatched': ta.featureCollection.numMatched,
219 'numberReturned': ta.featureCollection.numReturned,
220 }
223def wfs_feature_collection_member(ta: server.TemplateArgs, m: server.FeatureCollectionMember):
224 geom = None
225 for name, val in m.feature.attributes.items():
226 if m.layerCaps:
227 name = xmlx.namespace.qualify_name(name, m.layerCaps.xmlNamespace)
228 if isinstance(val, gws.Shape):
229 geom = name, gml_format_value(ta, val)
230 else:
231 yield name, gml_format_value(ta, val)
232 # QGIS wants geometry as the last element
233 if geom:
234 yield geom
237def gml_format_uid(ta: server.TemplateArgs, uid):
238 if not uid:
239 return '_'
240 s = str(uid)
241 if s[0].isdigit():
242 return '_' + s
243 return s
246def gml_format_value(ta, val):
247 s, ok = xmlx.util.atom_to_string(val)
248 if ok:
249 return s
250 if isinstance(val, gws.Shape):
251 # NB Qgis wants inline gml xmlns for adhoc schemas
252 return gws.lib.gml.shape_to_element(
253 val,
254 version=ta.gmlVersion,
255 always_xy=ta.sr.alwaysXY,
256 with_inline_xmlns=True,
257 )
258 return str(val)
261# http://inspire.ec.europa.eu/schemas/common/1.0/network.xsd
262# Scenario 2: Mandatory (where appropriate) metadata elements not mapped to standard capabilities,
263# plus mandatory language parameters,
264# plus OPTIONAL MetadataUrl pointing to an INSPIRE Compliant ISO metadata document
267def inspire_extended_capabilities(ta: server.TemplateArgs):
268 md = ta.service.metadata
269 return [
270 (
271 'inspire_common:ResourceLocator',
272 ('inspire_common:URL', ta.serviceUrl),
273 ('inspire_common:MediaType', 'application/xml'),
274 ),
275 ('inspire_common:ResourceType', md.inspireResourceType),
276 ('inspire_common:TemporalReference/inspire_common:DateOfPublication', iso_date(md.dateCreated)),
277 (
278 'inspire_common:Conformity',
279 (
280 'inspire_common:Specification',
281 # {'xsi:type': 'inspire_common:citationInspireInteroperabilityRegulation'},
282 (
283 'inspire_common:Title',
284 'COMMISSION REGULATION (EU) No 1089/2010 of 23 November 2010 implementing Directive 2007/2/EC of the European Parliament and of the Council as regards interoperability of spatial data sets and services',
285 ),
286 ('inspire_common:DateOfPublication', '2010-12-08'),
287 ('inspire_common:URI', 'OJ:L:2010:323:0011:0102:EN:PDF'),
288 (
289 'inspire_common:ResourceLocator',
290 ('inspire_common:URL', 'http://eur-lex.europa.eu/LexUriServ/LexUriServ.do?uri=OJ:L:2010:323:0011:0102:EN:PDF'),
291 ('inspire_common:MediaType', 'application/pdf'),
292 ),
293 ),
294 ('inspire_common:Degree', md.inspireDegreeOfConformity),
295 ),
296 (
297 'inspire_common:MetadataPointOfContact',
298 ('inspire_common:OrganisationName', md.contactOrganization),
299 ('inspire_common:EmailAddress', md.contactEmail),
300 ),
301 ('inspire_common:MetadataDate', iso_date(md.dateCreated)),
302 ('inspire_common:SpatialDataServiceType', md.inspireSpatialDataServiceType),
303 ('inspire_common:MandatoryKeyword/inspire_common:KeywordValue', md.inspireMandatoryKeyword),
304 (
305 'inspire_common:Keyword',
306 (
307 'inspire_common:OriginatingControlledVocabulary',
308 ('inspire_common:Title', 'INSPIRE themes'),
309 ('inspire_common:DateOfPublication', '2008-06-01'),
310 ),
311 ('inspire_common:KeywordValue', md.inspireThemeNameEn),
312 ),
313 (
314 'inspire_common:SupportedLanguages',
315 ('inspire_common:DefaultLanguage/inspire_common:Language', md.languageBib),
316 ('inspire_common:SupportedLanguage/inspire_common:Language', md.languageBib),
317 ),
318 ('inspire_common:ResponseLanguage/inspire_common:Language', md.languageBib),
319 ]
322def coord_dms(n):
323 prec = gws.lib.uom.DEFAULT_PRECISION[gws.Uom.deg]
324 return f'{round(n, prec):.{prec}f}'
327def coord_m(n):
328 prec = gws.lib.uom.DEFAULT_PRECISION[gws.Uom.m]
329 return f'{round(n, prec):.{prec}f}'
332def iso_date(d):
333 dd = dtx.parse(d)
334 return dtx.to_iso_date_string(dd) if dd else ''
337def iso_datetime(d):
338 dd = dtx.parse(d)
339 return dtx.to_iso_string(dd, with_tz=':') if dd else ''
342def namespaces_from_caps(ta: server.TemplateArgs) -> dict[str, gws.XmlNamespace]:
343 return {lc.xmlNamespace.xmlns: lc.xmlNamespace for lc in ta.layerCapsList if lc.xmlNamespace is not None}
346def to_xml_response(
347 ta: server.TemplateArgs,
348 tag,
349 namespaces: Optional[dict[str, gws.XmlNamespace]] = None,
350 default_namespace: Optional[gws.XmlNamespace] = None,
351) -> gws.ContentResponse:
352 if ta.sr.isSoap:
353 tag = ['soap:Envelope', ('soap:Header', ''), ('soap:Body', tag)]
354 namespaces = namespaces or {}
355 namespaces['soap'] = xmlx.namespace.require('soap')
357 el = xmlx.tag(*tag)
358 opts = gws.XmlOptions(
359 namespaces=namespaces,
360 defaultNamespace=default_namespace,
361 withNamespaceDeclarations=True,
362 withSchemaLocations=True,
363 withXmlDeclaration=True,
364 customXmlns=ta.sr.customXmlns,
365 )
367 return cast(service.Object, ta.sr.service).xml_response(el, opts)
370def to_xml_response_with_doctype(
371 ta: server.TemplateArgs,
372 tag,
373 doctype: str,
374) -> gws.ContentResponse:
375 if ta.sr.isSoap:
376 tag = ['soap:Envelope', ('soap:Header', ''), ('soap:Body', tag)]
378 el = xmlx.tag(*tag)
379 opts = gws.XmlOptions(
380 doctype=doctype,
381 withNamespaceDeclarations=False,
382 withSchemaLocations=False,
383 withXmlDeclaration=True,
384 )
385 return cast(service.Object, ta.sr.service).xml_response(el, opts)