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
« 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.
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.
8Paths:
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.
16Example::
18 /_/adminMapCache?path=
19 /_/adminMapCache?path=<cache name>/12
20"""
22import os
23import re
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
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)
45def get_content(root: gws.Root, path: str) -> gws.ContentResponse:
46 """Render the cache viewer page, a cached tile or a page asset.
48 Args:
49 root: The configuration root.
50 path: Page, tile or asset path.
52 Returns:
53 The rendered page, the tile image or the asset.
55 Raises:
56 gws.NotFoundError: If the path is invalid or the cache or level is not found.
57 """
58 path = (path or '').strip('/')
60 if not path:
61 return _page(root, '', -1)
63 if path in _ASSETS:
64 p, mime_type = _ASSETS[path]
65 return gws.ContentResponse(contentPath=p, mimeType=mime_type)
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))
72 m = re.match(r'^(\w+)$', path)
73 if m:
74 return _page(root, m.group(1), -1)
76 m = re.match(r'^(\w+)/(\d+)$', path)
77 if m:
78 return _page(root, m.group(1), int(m.group(2)))
80 raise gws.NotFoundError(f'invalid path {path!r}')
83##
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
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}
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 }
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))
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')
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)
133 img = gws.lib.image.from_path(p)
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)
143 return gws.ContentResponse(content=img.to_bytes(gws.lib.mime.PNG), mimeType=gws.lib.mime.PNG)
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 }
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 }