Coverage for gws-app/gws/plugin/ows_server/wmts/__init__.py: 50%

96 statements  

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

1"""WMTS Service. 

2 

3Implements WMTS 1.0.0. 

4This implementation only supports ``GET`` requests with ``KVP`` encoding. 

5 

6References: 

7 - OGC 07-057r7 (https://portal.ogc.org/files/?artifact_id=35326) 

8""" 

9 

10import gws 

11import gws.config.util 

12import gws.base.ows.server as server 

13import gws.lib.extent 

14import gws.lib.mime 

15import gws.gis.render 

16import gws.lib.uom 

17 

18gws.ext.new.owsService('wmts') 

19 

20 

21class Config(server.service.Config): 

22 """WMTS Service configuration""" 

23 

24 pass 

25 

26 

27_DEFAULT_TEMPLATES = [ 

28 gws.Config( 

29 type='py', 

30 path=gws.u.dirname(__file__) + '/templates/getCapabilities.cx.py', 

31 subject='ows.GetCapabilities', 

32 mimeTypes=[gws.lib.mime.XML], 

33 access=gws.c.PUBLIC, 

34 ), 

35] 

36 

37_DEFAULT_METADATA = gws.Metadata( 

38 inspireDegreeOfConformity='notEvaluated', 

39 inspireMandatoryKeyword='infoMapAccessService', 

40 inspireResourceType='service', 

41 inspireSpatialDataServiceType='view', 

42 isoScope='dataset', 

43 isoSpatialRepresentationType='vector', 

44) 

45 

46 

47class Object(server.service.Object): 

48 protocol = gws.OwsProtocol.WMTS 

49 supportedVersions = ['1.0.0'] 

50 isRasterService = True 

51 isOwsCommon = True 

52 

53 tileMatrixSets: list[gws.TileMatrixSet] 

54 tileSize = 256 

55 

56 def configure(self): 

57 gws.config.util.configure_templates_for(self, extra=_DEFAULT_TEMPLATES) 

58 

59 # @TODO different matrix sets per layer 

60 self.tileMatrixSets = [] 

61 for b in self.supportedBounds: 

62 # see https://docs.opengeospatial.org/is/13-082r2/13-082r2.html#29 

63 self.tileMatrixSets.append( 

64 gws.TileMatrixSet( 

65 uid=f'TMS_{b.crs.srid}', 

66 crs=b.crs, 

67 matrices=self.make_tile_matrices(b.extent, 0, 16, self.tileSize), 

68 ) 

69 ) 

70 

71 def configure_operations(self): 

72 self.supportedOperations = [ 

73 gws.OwsOperation( 

74 verb=gws.OwsVerb.GetCapabilities, 

75 formats=self.available_formats(gws.OwsVerb.GetCapabilities), 

76 handlerName='handle_get_capabilities', 

77 ), 

78 gws.OwsOperation( 

79 verb=gws.OwsVerb.GetLegendGraphic, 

80 formats=self.available_formats(gws.OwsVerb.GetLegendGraphic), 

81 handlerName='handle_get_legend_graphic', 

82 ), 

83 gws.OwsOperation( 

84 verb=gws.OwsVerb.GetTile, 

85 formats=self.available_formats(gws.OwsVerb.GetTile), 

86 handlerName='handle_get_tile', 

87 ), 

88 ] 

89 

90 def make_tile_matrices(self, extent, min_zoom, max_zoom, tile_size): 

91 ms = [] 

92 

93 w, h = gws.lib.extent.size(extent) 

94 

95 for z in range(min_zoom, max_zoom + 1): 

96 size = 1 << z 

97 res = w / (tile_size * size) 

98 ms.append( 

99 gws.TileMatrix( 

100 uid=f'{z:02d}', 

101 scale=gws.lib.uom.res_to_scale(res), 

102 x=extent[0], 

103 y=extent[3], # north origin 

104 tileWidth=tile_size, 

105 tileHeight=tile_size, 

106 width=size, 

107 height=size, 

108 extent=extent, 

109 ) 

110 ) 

111 

112 return ms 

113 

114 ## 

115 

116 def init_request(self, req): 

117 sr = super().init_request(req) 

118 sr.require_project() 

119 return sr 

120 

121 def layer_is_compatible(self, layer: gws.Layer): 

122 return not layer.isGroup and layer.canRenderBox 

123 

124 ## 

125 

126 def handle_get_capabilities(self, sr: server.request.Object): 

127 return self.template_response( 

128 sr, 

129 sr.requested_format('FORMAT'), 

130 layerCapsList=sr.layerCapsList, 

131 tileMatrixSets=self.tileMatrixSets, 

132 ) 

133 

134 def handle_get_tile(self, sr: server.request.Object): 

135 lcs = self.requested_layer_caps(sr) 

136 if len(lcs) != 1: 

137 raise server.error.InvalidParameterValue('LAYER') 

138 

139 tms_uid = sr.string_param('TILEMATRIXSET') 

140 tm_uid = sr.string_param('TILEMATRIX') 

141 row = sr.int_param('TILEROW') 

142 col = sr.int_param('TILECOL') 

143 

144 bounds = self.bounds_for_tile(tms_uid, tm_uid, row, col) 

145 if not bounds: 

146 raise server.error.TileOutOfRange() 

147 gws.log.debug(f'WMTS: bounds for tile {tms_uid=} {tm_uid=} {row=} {col=}: {bounds}') 

148 

149 mime = sr.requested_format('FORMAT') 

150 

151 mri = gws.MapRenderInput( 

152 backgroundColor=None, 

153 bbox=bounds.extent, 

154 crs=bounds.crs, 

155 mapSize=(self.tileSize, self.tileSize, gws.Uom.px), 

156 planes=[ 

157 gws.MapRenderInputPlane( 

158 type=gws.MapRenderInputPlaneType.imageLayer, 

159 layer=lc.layer, 

160 ) 

161 for lc in lcs 

162 ], 

163 ) 

164 

165 mro = gws.gis.render.render_map(mri) 

166 

167 if self.root.app.developer_option('ows.annotate_wmts'): 

168 e = bounds.extent 

169 text = f'{tm_uid} {row} {col}\n{e[0]}\n{e[1]}\n{e[2]}\n{e[3]}' 

170 mro.planes[0].image = mro.planes[0].image.add_text(text, x=10, y=10).add_box() 

171 

172 return self.image_response(sr, mro.planes[0].image, mime) 

173 

174 def handle_get_legend_graphic(self, sr: server.request.Object): 

175 lcs = self.requested_layer_caps(sr) 

176 return self.render_legend(sr, lcs, sr.requested_format('FORMAT')) 

177 

178 ## 

179 

180 def requested_layer_caps(self, sr: server.request.Object): 

181 lcs = [] 

182 

183 for name in sr.list_param('LAYER'): 

184 for lc in sr.layerCapsList: 

185 if not server.layer_caps.layer_name_matches(lc, name): 

186 continue 

187 lcs.append(lc) 

188 

189 if not lcs: 

190 raise server.error.LayerNotDefined() 

191 

192 return gws.u.uniq(lcs) 

193 

194 def bounds_for_tile(self, tms_uid, tm_uid, row, col): 

195 tms = self.get_matrix_set(tms_uid) 

196 if not tms: 

197 return 

198 tm = self.get_matrix(tms, tm_uid) 

199 if not tm: 

200 return 

201 

202 w, h = gws.lib.extent.size(tm.extent) 

203 span = w / tm.width 

204 

205 x = tm.x + col * span 

206 y = tm.y - row * span 

207 

208 bbox = x, y - span, x + span, y 

209 return gws.Bounds(crs=tms.crs, extent=bbox) 

210 

211 def get_matrix_set(self, tms_uid): 

212 for tms in self.tileMatrixSets: 

213 if tms.uid == tms_uid: 

214 return tms 

215 

216 def get_matrix(self, tms: gws.TileMatrixSet, tm_uid): 

217 for tm in tms.matrices: 

218 if tm.uid == tm_uid: 

219 return tm