Repository: banesullivan/localtileserver (python)

Issue:
Inconsistent colormap naming between rio-tiler and matplotlib causes layers to not render
### Problem description

When configuring a layer with a colormap name like `"Reds"` in `get_leaflet_tile_layer`, the layer is not displayed on an ipyleaflet map. Using `"viridis"` as a colormap, however, works.

The error that's reported by the `TileClient` with `debug=True` is a JSONDecodeError, as it doesn't recognise `"Reds"` as a valid colormap name and tries to parse it as JSON:

The problem occurs for localtileserver 0.10.5 and rio-tiler 7.3.0 but not when using localtileserver 0.10.3 and rio-tiler 6.8.0.

```
    c = json.loads(colormap)
        ^^^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/Caskroom/miniforge/base/envs/climaax_heatwaves/lib/python3.11/json/__init__.py", line 346, in loads
    return _default_decoder.decode(s)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/Caskroom/miniforge/base/envs/climaax_heatwaves/lib/python3.11/json/decoder.py", line 337, in decode
    c = json.loads(colormap)
        ^^^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/Caskroom/miniforge/base/envs/climaax_heatwaves/lib/python3.11/json/__init__.py", line 346, in loads
    obj, end = self.raw_decode(s, idx=_w(s, 0).end())
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/Caskroom/miniforge/base/envs/climaax_heatwaves/lib/python3.11/json/decoder.py", line 355, in raw_decode
    return _default_decoder.decode(s)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/Caskroom/miniforge/base/envs/climaax_heatwaves/lib/python3.11/json/decoder.py", line 337, in decode
    raise JSONDecodeError("Expecting value", s, err.value) from None
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
    obj, end = self.raw_decode(s, idx=_w(s, 0).end())
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/Caskroom/miniforge/base/envs/climaax_heatwaves/lib/python3.11/json/decoder.py", line 355, in raw_decode
    raise JSONDecodeError("Expecting value", s, err.value) from None
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
```

### Why is this happening?

The issue seems to be that `rio-tiler` keeps its colormaps in `rio_tiler.colormap.cmap` with lowercase names and recently stopped applying `.lower()` to colormap names before lookups (https://github.com/cogeotiff/rio-tiler/pull/742). This means that a colormap name like `"Reds"` cannot be resolved any longer, as it now must be `"reds"` to match the names in rio-tiler. Users can't adapt here, because the check in `localtileserver.tiler.palette_valid_or_raise` validates names against matplotlib's names (which still contain uppercase), not rio-tiler's. So `colormap="reds"` is rejected, even though it would work. Only all-lowercase colormap names pass validation and are interpreted correctly.

### Suggestions

I'd suggest to either adapt to rio-tiler's naming convention and validate names against their cmap list instead of matplotlib's (breaking backwards compatibility) or to apply `.lower()` to colormap names before the lookup in rio-tiler's cmap registry.

Pull request description:
Validate colormaps with rio-tiler
Resolves #235 by validating colormap names against rio-tiler's supported colormaps

Patch:
diff --git a/localtileserver/tiler/palettes.py b/localtileserver/tiler/palettes.py
index 2b24efd..d1f6528 100644
--- a/localtileserver/tiler/palettes.py
+++ b/localtileserver/tiler/palettes.py
@@ -1,43 +1,20 @@
 import logging
 
-try:
-    import cmocean  # noqa
-except ImportError:
-    pass
-try:
-    import colorcet  # noqa
-except ImportError:
-    pass
+from rio_tiler.colormap import cmap as RIO_CMAPS
 
 logger = logging.getLogger(__name__)
 
 
-def is_mpl_cmap(name: str):
-    """This will silently fail if matplotlib is not installed."""
-    try:
-        import matplotlib
-
-        matplotlib.colormaps.get_cmap(name)
-        return True
-    except ImportError:  # pragma: no cover
-        logger.error("Install matplotlib for additional colormap choices.")
-    except ValueError:
-        pass
-    return False
+def is_rio_cmap(name: str):
+    """Check whether cmap is supported by rio-tiler."""
+    return name in RIO_CMAPS.data.keys()
 
 
 def palette_valid_or_raise(name: str):
-    if not is_mpl_cmap(name):
-        raise ValueError(f"Please use a valid matplotlib colormap name. Invalid: {name}")
+    if not is_rio_cmap(name):
+        raise ValueError(f"Please use a valid rio-tiler registered colormap name. Invalid: {name}")
 
 
 def get_palettes():
     """List of available palettes."""
-    cmaps = {}
-    try:
-        import matplotlib.pyplot
-
-        cmaps["matplotlib"] = list(matplotlib.pyplot.colormaps())
-    except ImportError:  # pragma: no cover
-        logger.error("Install matplotlib for additional colormap choices.")
-    return cmaps
+    return {"matplotlib": list(RIO_CMAPS.data.keys())}


Test patch:
diff --git a/tests/test_utilities.py b/tests/test_utilities.py
index 6c07552..0e093eb 100644
--- a/tests/test_utilities.py
+++ b/tests/test_utilities.py
@@ -1,7 +1,7 @@
 import pytest
 
 from localtileserver import Report, TileClient
-from localtileserver.tiler.palettes import get_palettes, is_mpl_cmap
+from localtileserver.tiler.palettes import get_palettes, is_rio_cmap
 from localtileserver.validate import validate_cog
 
 has_mpl = False
@@ -14,14 +14,14 @@ except ImportError:
 
 
 def test_is_valid_palette_name():
-    assert is_mpl_cmap("viridis")
-    assert not is_mpl_cmap("foobar")
+    assert is_rio_cmap("viridis")
+    assert not is_rio_cmap("foobar")
 
 
 @pytest.mark.skipif(not has_mpl, reason="matplotlib not installed.")
 def test_mpl_colormaps():
-    assert is_mpl_cmap("viridis")
-    assert is_mpl_cmap("jet")
+    assert is_rio_cmap("viridis")
+    assert is_rio_cmap("jet")
 
 
 def test_report():