Coverage for gws-app/gws/plugin/admin_action/mapcache/__init__.py: 0%

64 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-05 13:35 +0200

1"""Cache viewer: an html page to inspect cached tiles. 

2 

3The page lists all tile caches from the cache inventory (``gws.gis.cache.core``) 

4with per-level statistics, and shows the cached tiles of a selected cache and 

5level on an OpenLayers map. It is served by the ``adminMapCache`` command of the 

6``admin`` action. 

7 

8Paths: 

9 

10- empty: the overview page. 

11- ``<cache>``, ``<cache>/<z>``: the page with a cache and level selected (level 0 by default). 

12- ``<cache>/<z>/<x>/<y>.<ext>``: a single cached tile as PNG, or an empty 204 response if 

13 the tile is not cached. 

14- ``ol.js``, ``ol.css``, ``proj4.js``, ``page.js``, ``page.css``: page assets. 

15 

16Example:: 

17 

18 /_/adminMapCache?path= 

19 /_/adminMapCache?path=<cache name>/12 

20""" 

21 

22import os 

23import re 

24 

25import gws 

26import gws.lib.crs 

27import gws.lib.grid 

28import gws.lib.image 

29import gws.lib.mime 

30import gws.gis.cache.core as core 

31 

32_DIR = os.path.dirname(__file__) 

33_VENDOR_DIR = f'{gws.c.APP_DIR}/gws/lib/vendor' 

34_ASSETS = { 

35 'ol.js': (f'{_VENDOR_DIR}/openlayers/ol.js', gws.lib.mime.JS), 

36 'ol.css': (f'{_VENDOR_DIR}/openlayers/ol.css', gws.lib.mime.CSS), 

37 'proj4.js': (f'{_VENDOR_DIR}/proj4/proj4.js', gws.lib.mime.JS), 

38 'page.js': (f'{_DIR}/page.js', gws.lib.mime.JS), 

39 'page.css': (f'{_DIR}/page.css', gws.lib.mime.CSS), 

40} 

41_URL = f'{gws.c.SERVER_ENDPOINT}/adminMapCache?path=' 

42_DECOR_COLOR = (200, 0, 0, 255) 

43 

44 

45def get_content(root: gws.Root, path: str) -> gws.ContentResponse: 

46 """Render the cache viewer page, a cached tile or a page asset. 

47 

48 Args: 

49 root: The configuration root. 

50 path: Page, tile or asset path. 

51 

52 Returns: 

53 The rendered page, the tile image or the asset. 

54 

55 Raises: 

56 gws.NotFoundError: If the path is invalid or the cache or level is not found. 

57 """ 

58 path = (path or '').strip('/') 

59 

60 if not path: 

61 return _page(root, '', -1) 

62 

63 if path in _ASSETS: 

64 p, mime_type = _ASSETS[path] 

65 return gws.ContentResponse(contentPath=p, mimeType=mime_type) 

66 

67 m = re.match(r'^(\w+)/(\d+)/(\d+)/(\d+)\.\w+$', path) 

68 if m: 

69 name, z, x, y = m.groups() 

70 return _tile(root, name, int(z), int(x), int(y)) 

71 

72 m = re.match(r'^(\w+)$', path) 

73 if m: 

74 return _page(root, m.group(1), -1) 

75 

76 m = re.match(r'^(\w+)/(\d+)$', path) 

77 if m: 

78 return _page(root, m.group(1), int(m.group(2))) 

79 

80 raise gws.NotFoundError(f'invalid path {path!r}') 

81 

82 

83## 

84 

85 

86def _page(root: gws.Root, name: str, z: int) -> gws.ContentResponse: 

87 """Render the page, with a cache and level selected if ``name`` is given.""" 

88 inv = core.inventory(root) 

89 core.add_stats(inv) 

90 selected = None 

91 

92 if name: 

93 c = _find(inv, name) 

94 z = max(z, 0) 

95 if z not in [lv.z for lv in c.levels]: 

96 raise gws.NotFoundError(f'level {z} not found') 

97 selected = {'name': name, 'z': z} 

98 

99 config = { 

100 'url': _URL, 

101 'caches': {c.name: _cache_config(c) for c in inv.caches}, 

102 'selected': selected, 

103 'baseGrid': _grid_config(gws.lib.grid.for_crs(gws.lib.crs.WEBMERCATOR)), 

104 } 

105 

106 tpl = root.app.templateMgr.template_from_path(f'{_DIR}/page.cx.html') 

107 args = { 

108 'url': _URL, 

109 'caches': inv.caches, 

110 'name': name, 

111 'z': z, 

112 'config': config, 

113 } 

114 return tpl.render(gws.TemplateRenderInput(args=args)) 

115 

116 

117def _find(inv: core.Inventory, name: str) -> core.Cache: 

118 """Find a cache in the inventory by name, raise ``NotFoundError`` if not found.""" 

119 for c in inv.caches: 

120 if c.name == name: 

121 return c 

122 raise gws.NotFoundError(f'cache {name!r} not found') 

123 

124 

125def _tile(root: gws.Root, name: str, z: int, x: int, y: int) -> gws.ContentResponse: 

126 """Return a cached tile as PNG, or an empty 204 response if it is not in the store.""" 

127 c = _find(core.inventory(root), name) 

128 store = c.grabber.store 

129 p = store.path((x, y, z)) 

130 if not os.path.isfile(p): 

131 return gws.ContentResponse(status=204, content=b'', mimeType=gws.lib.mime.PNG) 

132 

133 img = gws.lib.image.from_path(p) 

134 

135 # img.add_box(_DECOR_COLOR) 

136 # text = f'{p[len(store.baseDir) + 1 :]}\n{os.path.getsize(p) / 1024:.1f}K' 

137 # draw = gws.lib.image.get_draw(img) 

138 # font = gws.lib.image.get_font(11) 

139 # x0, y0, x1, y1 = draw.multiline_textbbox((4, 3), text, font=font) 

140 # draw.rectangle((x0 - 2, y0 - 1, x1 + 2, y1 + 1), fill=(255, 255, 255, 200)) 

141 # draw.multiline_text((4, 3), text, font=font, fill=_DECOR_COLOR) 

142 

143 return gws.ContentResponse(content=img.to_bytes(gws.lib.mime.PNG), mimeType=gws.lib.mime.PNG) 

144 

145 

146def _cache_config(c: core.Cache) -> dict: 

147 """Return the client-side configuration of a cache: CRS, grid, extent and per-level statistics.""" 

148 gr = c.grabber 

149 crs = gr.targetCrs 

150 return { 

151 'crs': { 

152 'epsg': crs.epsg, 

153 'proj4text': crs.proj4text, 

154 }, 

155 'gridExtent': list(gr.grid.extent), 

156 'tileSize': gr.grid.tileSize, 

157 'extent': list(gr.extent), 

158 'ext': gr.store.extension, 

159 'resolutions': {lv.z: lv.resolution for lv in c.levels}, 

160 'ranges': {lv.z: list(lv.cachedRange) if lv.cachedRange else None for lv in c.levels}, 

161 'gridRanges': {lv.z: list(lv.gridRange) for lv in c.levels}, 

162 'counts': {lv.z: [lv.totalTiles, lv.cachedTiles] for lv in c.levels}, 

163 'srid': crs.srid, 

164 'grid': _grid_config(gr.grid), 

165 } 

166 

167 

168def _grid_config(mg: gws.MapGrid) -> dict: 

169 """Return the client-side configuration of a grid.""" 

170 return { 

171 'extent': list(mg.extent), 

172 'baseResolution': mg.baseResolution, 

173 'tileSize': mg.tileSize, 

174 }