Coverage for gws-app/gws/plugin/raster_layer/provider.py: 84%
62 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"""Image set provider for raster layers."""
3import fnmatch
4from typing import Optional
6import gws
7import gws.lib.shape
8import gws.lib.osx
9import gws.lib.crs
10import gws.lib.gdalx
13class Config(gws.Config):
14 """Set of georeferenced image files for a raster layer."""
16 paths: Optional[list[gws.FilePath]]
17 """Image files to show."""
18 pathPattern: Optional[str]
19 """Glob pattern for image files."""
20 crs: Optional[gws.CrsName]
21 """CRS for images that have none."""
24class ImageEntry(gws.Data):
25 """A georeferenced image file."""
27 path: str
28 """File path."""
29 bounds: gws.Bounds
30 """Image bounds in the image CRS."""
33class Object(gws.Node):
34 """Set of georeferenced image files, given as paths or a glob pattern."""
36 paths: list[str]
37 """Image file paths."""
38 crs: Optional[gws.Crs]
39 """CRS for images that have none."""
41 def configure(self):
42 p = self.cfg('crs')
43 self.crs = gws.lib.crs.require(p) if p else None
45 self.paths = []
47 p = self.cfg('paths')
48 if p:
49 self.paths = p
50 return
52 p = self.cfg('pathPattern')
53 if p:
54 pp = gws.lib.osx.parse_path(p)
55 self.paths = sorted(
56 gws.lib.osx.find_files(
57 pp.dirname,
58 fnmatch.translate(pp.filename),
59 )
60 )
61 return
63 raise gws.ConfigurationError('no paths or pathPattern specified for raster provider.')
65 def cache_hash(self):
66 """Compute a hash of the provider settings.
68 Returns:
69 Hash string, built from the paths and the CRS.
70 """
71 return gws.u.sha256([
72 self.paths,
73 self.crs.srid if self.crs else '',
74 ])
76 def enumerate_images(self, default_crs: gws.Crs) -> list[ImageEntry]:
77 """Read the bounds of the image files.
79 Files that cannot be opened, and files in a CRS other than the CRS of
80 the first image, are skipped with a configuration warning.
82 Args:
83 default_crs: CRS for images that have none.
85 Returns:
86 Image entries, all in the same CRS.
87 """
88 es1 = []
90 for path in self.paths:
91 try:
92 with gws.lib.gdalx.open_raster(path, default_crs=default_crs) as gd:
93 es1.append(ImageEntry(path=path, bounds=gd.bounds()))
94 except gws.lib.gdalx.Error as exc:
95 self.root.config_warning(f'raster_provider: {path!r}: cannot open: ({exc})')
97 if not es1:
98 return []
100 # all images must have the same CRS
101 es2 = []
102 crs = es1[0].bounds.crs
103 for e in es1:
104 if e.bounds.crs == crs:
105 es2.append(e)
106 continue
107 self.root.config_warning(f'raster_provider: {e.path!r}: wrong crs {e.bounds.crs}, must be {crs}')
109 return es2
111 def make_tile_index(self, entries: list[ImageEntry], file_name: str) -> str:
112 """Create a MapServer tile index shapefile for the images.
114 The index has a polygon per image with the file path in the
115 ``location`` column, and a spatial index.
117 Args:
118 entries: Images to index.
119 file_name: Base name of the shapefile, in the object cache directory.
121 Returns:
122 Path to the shapefile.
123 """
124 idx_path = f'{gws.c.OBJECT_CACHE_DIR}/{file_name}.shp'
126 records = []
128 for e in entries:
129 records.append(
130 gws.FeatureRecord(
131 attributes={'location': e.path},
132 shape=gws.lib.shape.from_bounds(e.bounds),
133 )
134 )
136 with gws.lib.gdalx.open_vector(idx_path, 'w') as ds:
137 la = ds.create_layer(
138 name=file_name,
139 columns={'location': gws.AttributeType.str},
140 geometry_type=gws.GeometryType.polygon,
141 crs=entries[0].bounds.crs,
142 )
143 la.insert(records)
144 ds.gdDataset.ExecuteSQL(f'CREATE SPATIAL INDEX ON {file_name}')
146 gws.log.debug(f'raster_provider: created {idx_path=}')
147 return idx_path