Coverage for gws-app/gws/plugin/ows_client/wms/provider.py: 0%
64 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-17 01:37 +0200
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-17 01:37 +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.gis.crs
34import gws.gis.extent
35import gws.gis.source
38from . import caps
41class Config(gws.base.ows.client.provider.Config):
42 bottomFirst: bool = False
43 """true if layers are listed from bottom to top"""
46class Object(gws.base.ows.client.provider.Object):
47 protocol = gws.OwsProtocol.WMS
49 def configure(self):
50 cc = caps.parse(self.get_capabilities(), self.cfg('bottomFirst', default=False))
52 self.metadata = cc.metadata
53 self.sourceLayers = cc.sourceLayers
54 self.version = cc.version
56 self.configure_operations(cc.operations)
58 DEFAULT_GET_FEATURE_LIMIT = 100
60 def get_features(self, search, source_layers):
61 v3 = self.version >= '1.3'
63 shape = search.shape
64 if not shape or shape.type != gws.GeometryType.point:
65 return []
67 request_crs = self.forceCrs
68 if not request_crs:
69 request_crs = gws.gis.crs.best_match(
70 shape.crs,
71 gws.gis.source.combined_crs_list(source_layers))
73 box_size_m = 500
74 box_size_deg = 1
75 box_size_px = 500
77 size = None
79 if shape.crs.uom == gws.Uom.m:
80 size = box_size_px * search.resolution
81 if shape.crs.uom == gws.Uom.deg:
82 # @TODO use search.resolution here as well
83 size = box_size_deg
84 if not size:
85 gws.log.debug('cannot request crs {crs!r}, unsupported unit')
86 return []
88 bbox = (
89 shape.x - (size / 2),
90 shape.y - (size / 2),
91 shape.x + (size / 2),
92 shape.y + (size / 2),
93 )
95 bbox = gws.gis.extent.transform(bbox, shape.crs, request_crs)
97 always_xy = self.alwaysXY or not v3
98 if request_crs.isYX and not always_xy:
99 bbox = gws.gis.extent.swap_xy(bbox)
101 layer_names = [sl.name for sl in source_layers]
103 params = {
104 'BBOX': bbox,
105 'CRS' if v3 else 'SRS': request_crs.to_string(gws.CrsFormat.epsg),
106 'WIDTH': box_size_px,
107 'HEIGHT': box_size_px,
108 'I' if v3 else 'X': box_size_px >> 1,
109 'J' if v3 else 'Y': box_size_px >> 1,
110 'LAYERS': layer_names,
111 'QUERY_LAYERS': layer_names,
112 'STYLES': [''] * len(layer_names),
113 'VERSION': self.version,
114 'FEATURE_COUNT': search.limit or self.DEFAULT_GET_FEATURE_LIMIT,
115 }
117 if search.extraParams:
118 params = gws.u.merge(params, gws.u.to_upper_dict(search.extraParams))
120 op = self.get_operation(gws.OwsVerb.GetFeatureInfo)
121 if not op:
122 return []
124 if op.preferredFormat:
125 params.setdefault('INFO_FORMAT', op.preferredFormat)
127 args = self.prepare_operation(op, params=params)
128 text = gws.base.ows.client.request.get_text(args)
130 fdata = gws.base.ows.client.featureinfo.parse(text, default_crs=request_crs, always_xy=always_xy)
132 if fdata is None:
133 gws.log.debug(f'get_features: NOT_PARSED params={params!r}')
134 return []
136 gws.log.debug(f'get_features: FOUND={len(fdata)} params={params!r}')
138 for fd in fdata:
139 if fd.shape:
140 fd.shape = fd.shape.transformed_to(shape.crs)
142 return fdata