Coverage for gws-app/gws/plugin/ows_client/wms/provider.py: 0%
65 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-16 22:59 +0200
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-16 22:59 +0200
1"""WMS provider.
3References.
5 - OGC 01-068r3: WMS 1.1.1
6 - OGC 06-042: WMS 1.3.0
8see also https://docs.geoserver.org/latest/en/user/services/wms/reference.html
10A note on layer order:
12Internally we always list source layers topmost layer first,
13which corresponds to the layer tree display.
15WMS capabilities are assumed to be top-first by default,
16for servers with bottom-first caps, set ``bottomFirst=True``,
17in which case the capabilities parser will revert all layer lists.
19The order of GetMap is always bottom first:
21> A WMS shall render the requested layers by drawing the leftmost in the list bottommost,
22> the next one over that, and so on. (OGC 06-042, 7.3.3.3)
24therefore when invoking GetMap, our layer lists should be reversed.
26"""
28from typing import Optional, cast
30import gws
31import gws.base.ows.client
32import gws.config.util
33import gws.lib.crs
34import gws.lib.extent
35import gws.gis.source
38from . import caps
41class Config(gws.base.ows.client.provider.Config):
42 """WMS provider configuration."""
44 bottomFirst: bool = False
45 """True if layers are listed from bottom to top."""
48class Object(gws.base.ows.client.provider.Object):
49 protocol = gws.OwsProtocol.WMS
51 def configure(self):
52 cc = caps.parse(self.get_capabilities(), self.cfg('bottomFirst', default=False))
54 self.metadata = cc.metadata
55 self.sourceLayers = cc.sourceLayers
56 self.version = cc.version
58 self.configure_operations(cc.operations)
60 DEFAULT_GET_FEATURE_LIMIT = 100
62 def get_features(self, search, source_layers):
63 v3 = self.version >= '1.3'
65 shape = search.shape
66 if not shape or shape.type != gws.GeometryType.point:
67 return []
69 request_crs = self.forceCrs
70 if not request_crs:
71 request_crs = gws.lib.crs.best_match(
72 shape.crs,
73 gws.gis.source.combined_crs_list(source_layers))
75 box_size_m = 500
76 box_size_deg = 1
77 box_size_px = 500
79 size = None
81 if shape.crs.uom == gws.Uom.m:
82 size = box_size_px * search.resolution
83 if shape.crs.uom == gws.Uom.deg:
84 # @TODO use search.resolution here as well
85 size = box_size_deg
86 if not size:
87 gws.log.debug('cannot request crs {crs!r}, unsupported unit')
88 return []
90 bbox = (
91 shape.x - (size / 2),
92 shape.y - (size / 2),
93 shape.x + (size / 2),
94 shape.y + (size / 2),
95 )
97 bbox = gws.lib.extent.transform(bbox, shape.crs, request_crs)
99 always_xy = self.alwaysXY or not v3
100 if request_crs.isYX and not always_xy:
101 bbox = gws.lib.extent.swap_xy(bbox)
103 layer_names = [sl.name for sl in source_layers]
105 params = {
106 'BBOX': bbox,
107 'CRS' if v3 else 'SRS': request_crs.to_string(gws.CrsFormat.epsg),
108 'WIDTH': box_size_px,
109 'HEIGHT': box_size_px,
110 'I' if v3 else 'X': box_size_px >> 1,
111 'J' if v3 else 'Y': box_size_px >> 1,
112 'LAYERS': layer_names,
113 'QUERY_LAYERS': layer_names,
114 'STYLES': [''] * len(layer_names),
115 'VERSION': self.version,
116 'FEATURE_COUNT': search.limit or self.DEFAULT_GET_FEATURE_LIMIT,
117 }
119 if search.extraParams:
120 params = gws.u.merge(params, gws.u.to_upper_dict(search.extraParams))
122 op = self.get_operation(gws.OwsVerb.GetFeatureInfo)
123 if not op:
124 return []
126 if op.preferredFormat:
127 params.setdefault('INFO_FORMAT', op.preferredFormat)
129 args = self.prepare_operation(op, params=params)
130 text = gws.base.ows.client.request.get_text(args)
132 try:
133 records = gws.base.ows.client.featureinfo.parse(text, default_crs=request_crs, always_xy=self.alwaysXY)
134 except gws.Error as exc:
135 gws.log.error(f'get_features: parse error: {exc!r}')
136 return []
138 gws.log.debug(f'get_features: FOUND={len(records)} params={params!r}')
140 for rec in records:
141 if rec.shape:
142 rec.shape = rec.shape.transformed_to(shape.crs)
144 return records