async_geotiff/style/raster/
compiler.rs1use super::{Colors, RasterMosaic, RasterPlan};
3use crate::{
4 error::{AgError, AgResult},
5 render::{colormap::builtin_lut, expression},
6 style::{
7 stretch::{Statistics, Stretch},
8 RasterStyle, TileFormat, TileStyle,
9 },
10 warp::resampler::Resampling,
11};
12use serde_json::Value;
13use std::sync::Arc;
14
15fn unsupported(path: &str) -> AgError {
16 AgError::query(path, "not supported by the Raster Style adapter")
17}
18
19fn sdk_error(error: raster_style::Error) -> AgError {
20 AgError::query(
21 if error.path.is_empty() {
22 "style"
23 } else {
24 &error.path
25 },
26 error.to_string(),
27 )
28}
29
30fn only(value: &Value, allowed: &[&str], path: &str) -> AgResult<()> {
33 if let Some(object) = value.as_object() {
34 for key in object.keys() {
35 if !allowed.contains(&key.as_str()) {
36 return Err(unsupported(&format!("{path}/{key}")));
37 }
38 }
39 }
40 Ok(())
41}
42
43impl TileStyle {
44 pub fn from_json(json: &str) -> AgResult<Self> {
47 Self::from_raster_style(&RasterStyle::from_json(json).map_err(sdk_error)?)
48 }
49
50 pub fn from_raster_query(query: &str) -> AgResult<Self> {
52 Self::from_raster_style(&raster_style::decode_query(query).map_err(sdk_error)?)
53 }
54
55 pub fn from_raster_style(style: &RasterStyle) -> AgResult<Self> {
57 let supported: &[&str] = if cfg!(feature = "mosaic") {
58 &[
59 raster_style::TILE_SCHEMA_URL,
60 raster_style::MOSAIC_SCHEMA_URL,
61 ]
62 } else {
63 &[raster_style::TILE_SCHEMA_URL]
64 };
65 style
66 .require_extension_support(supported)
67 .map_err(sdk_error)?;
68 let v = style.as_value();
69 only(
70 v,
71 &[
72 "version",
73 "renderer",
74 "stretch",
75 "resampling",
76 "nodata",
77 "effects",
78 "opacity",
79 "extensions",
80 "raster:tile",
81 "calibration",
82 "raster:mosaic",
83 "statistics",
84 ],
85 "",
86 )?;
87 let renderer = &v["renderer"];
88 only(
89 renderer,
90 &[
91 "type",
92 "bidx",
93 "expression",
94 "language",
95 "colormap_name",
96 "colormap",
97 "renderer_invert",
98 "color",
99 ],
100 "/renderer",
101 )?;
102 if v.pointer("/calibration/mode")
103 .is_some_and(|mode| mode != "none")
104 {
105 return Err(unsupported("/calibration"));
106 }
107 let kind = renderer["type"].as_str().unwrap_or_default();
108 if !["gray", "rgb", "pseudocolor", "categorized", "single_color"].contains(&kind) {
109 return Err(unsupported("/renderer/type"));
110 }
111 let inputs = compile_inputs(renderer)?;
112 let mut dependencies: Vec<_> = inputs
113 .iter()
114 .flatten()
115 .filter_map(|token| {
116 if let expression::Token::Band(b) = token {
117 Some(*b)
118 } else {
119 None
120 }
121 })
122 .collect();
123 dependencies.sort_unstable();
124 dependencies.dedup();
125
126 let mosaic = if let Some(mosaic) = v.get("raster:mosaic") {
127 if !cfg!(feature = "mosaic") {
128 return Err(unsupported("/raster:mosaic (enable the mosaic feature)"));
129 }
130 let selection = mosaic["pixel_selection"]
131 .as_str()
132 .expect("validated selection")
133 .to_owned();
134 let after_channels = mosaic["stage"] == "after_channels";
135 let rank = mosaic["rank_channel"].as_f64().unwrap_or(1.0) as usize - 1;
136 let channels = if after_channels {
137 inputs.len()
138 } else {
139 dependencies.len()
140 };
141 if matches!(selection.as_str(), "highest" | "lowest") && rank >= channels {
142 return Err(AgError::query(
143 "/raster:mosaic/rank_channel",
144 "outside the compiled channel dependencies",
145 ));
146 }
147 Some(RasterMosaic {
148 #[cfg(feature = "mosaic")]
149 selection,
150 after_channels,
151 #[cfg(feature = "mosaic")]
152 rank,
153 })
154 } else {
155 None
156 };
157
158 let stretch = &v["stretch"];
159 let method = stretch["method"].as_str().unwrap_or("none");
160 let transfer = Stretch::parse(stretch, inputs.len());
161 let statistics = Statistics::parse(&v["statistics"])?;
162
163 only(&v["effects"], &["color_formula"], "/effects")?;
164 let mut gamma = Vec::new();
165 if let Some(ops) = v
166 .pointer("/effects/color_formula")
167 .and_then(Value::as_array)
168 {
169 for op in ops {
170 if op["op"] != "gamma" {
171 return Err(unsupported("/effects/color_formula"));
172 }
173 gamma.push((
174 op["channels"]
175 .as_str()
176 .expect("normalized channels")
177 .to_owned(),
178 op["value"].as_f64().expect("validated gamma"),
179 ));
180 }
181 }
182 let data_domain = method == "none" && gamma.is_empty();
183 let colors = compile_colors(renderer, kind, data_domain)?;
184 only(
185 &v["raster:tile"],
186 &["format", "tilesize", "lossless"],
187 "/raster:tile",
188 )?;
189 let format = match v
190 .pointer("/raster:tile/format")
191 .and_then(Value::as_str)
192 .unwrap_or("png")
193 {
194 "png" => TileFormat::Png,
195 "webp" if v.pointer("/raster:tile/lossless") == Some(&Value::Bool(true)) => {
196 TileFormat::Webp
197 }
198 _ => {
199 return Err(unsupported(
200 "/raster:tile/format (use png or lossless webp)",
201 ))
202 }
203 };
204 format.ensure_available()?;
205 let resampler = |key: &str| -> AgResult<Resampling> {
206 let name = v["resampling"][key].as_str().unwrap_or("nearest");
207 if name == "mode" {
208 return Err(unsupported(&format!("/resampling/{key} (mode Alpha)")));
209 }
210 Resampling::parse(name).ok_or_else(|| unsupported(&format!("/resampling/{key}")))
211 };
212 let (statistics_key, stretch_key) = if transfer.needs_statistics() {
213 let key = serde_json::json!({
214 "bidx": renderer.get("bidx"), "expression": renderer.get("expression"),
215 "language": renderer.get("language"), "resampling": v.get("resampling"),
216 "nodata": v.get("nodata"), "calibration": v.get("calibration"),
217 "raster:mosaic": v.get("raster:mosaic"), "statistics": v.get("statistics")
218 })
219 .to_string();
220 let mut stretch = stretch.clone();
221 if let Some(object) = stretch.as_object_mut() {
222 object.remove("range_policy");
223 }
224 (key, stretch.to_string())
225 } else {
226 (String::new(), String::new())
227 };
228 let out = Self {
229 nodata: v["nodata"]
230 .as_f64()
231 .or_else(|| (v["nodata"] == "nan").then_some(f64::NAN)),
232 resampling: resampler("read")?,
233 reproject: resampler("reproject")?,
234 tilesize: v
235 .pointer("/raster:tile/tilesize")
236 .and_then(Value::as_f64)
237 .unwrap_or(256.0) as u32,
238 format,
239 raster: Arc::new(RasterPlan {
240 key: style.to_json().map_err(sdk_error)?,
241 categorical: kind == "categorized",
242 mosaic,
243 inputs,
244 dependencies,
245 stretch: transfer,
246 statistics,
247 transparent: stretch["range_policy"] == "transparent",
248 colors,
249 gamma,
250 opacity: v["opacity"].as_f64().unwrap_or(1.0),
251 statistics_key,
252 stretch_key,
253 rgb_luts: Arc::default(),
254 }),
255 };
256 if !crate::style::params::ALLOWED_TILE_SIZES.contains(&out.tilesize) {
257 return Err(unsupported(
258 "/raster:tile/tilesize (use 64, 128, 256, 512 or 1024)",
259 ));
260 }
261 Ok(out)
262 }
263
264 pub fn raster_style_json(&self) -> &str {
266 &self.raster.key
267 }
268}
269
270fn rgba_color(v: &Value) -> [u8; 4] {
271 std::array::from_fn(|i| v[i].as_f64().expect("validated RGBA") as u8)
272}
273
274fn hex_color(text: &str) -> [u8; 4] {
275 std::array::from_fn(|i| {
276 u8::from_str_radix(&text[1 + i * 2..3 + i * 2], 16).expect("normalized color")
277 })
278}
279
280fn compile_inputs(renderer: &Value) -> AgResult<Vec<Vec<expression::Token>>> {
281 let inputs: Vec<Vec<expression::Token>> = if let Some(bands) = renderer["bidx"].as_array() {
282 bands
283 .iter()
284 .map(|b| {
285 vec![expression::Token::Band(
286 b.as_f64().expect("validated band") as usize - 1,
287 )]
288 })
289 .collect()
290 } else if let Some(text) = renderer["expression"].as_str() {
291 text.split(';')
292 .map(expression::parse_expression)
293 .collect::<AgResult<_>>()?
294 } else {
295 return Err(unsupported("/renderer/index"));
296 };
297 Ok(inputs)
298}
299
300fn compile_colors(renderer: &Value, kind: &str, data_domain: bool) -> AgResult<Colors> {
301 let colors = match kind {
302 "rgb" => Colors::Rgb,
303 "gray" => Colors::Gray(renderer["renderer_invert"].as_bool().unwrap_or(false)),
304 "single_color" => Colors::Solid(hex_color(
305 renderer["color"].as_str().expect("validated color"),
306 )),
307 _ => {
308 if let Some(name) = renderer["colormap_name"].as_str() {
309 if kind == "categorized" {
310 return Err(unsupported("/renderer/colormap_name"));
311 }
312 Colors::Named(Box::new(builtin_lut(name)?))
313 } else if let Some(map) = renderer["colormap"].as_object() {
314 let mut entries = map
315 .iter()
316 .map(|(key, rgba)| {
317 Ok((
318 key.parse::<f64>()
319 .map_err(|_| unsupported("/renderer/colormap"))?,
320 rgba_color(rgba),
321 ))
322 })
323 .collect::<AgResult<Vec<_>>>()?;
324 entries.sort_unstable_by(|a, b| a.0.total_cmp(&b.0));
325 Colors::Exact(entries, data_domain)
326 } else if let Some(intervals) = renderer["colormap"].as_array() {
327 Colors::Intervals(
328 intervals
329 .iter()
330 .map(|entry| {
331 (
332 entry[0][0].as_f64().expect("validated bound"),
333 entry[0][1].as_f64().expect("validated bound"),
334 rgba_color(&entry[1]),
335 )
336 })
337 .collect(),
338 data_domain,
339 )
340 } else {
341 return Err(unsupported("/renderer/color_mapping"));
342 }
343 }
344 };
345 Ok(colors)
346}