Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions docs/maplibre/local_titiler.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"[![image](https://jupyterlite.rtfd.io/en/latest/_static/badge.svg)](https://demo.leafmap.org/lab/index.html?path=maplibre/local_titiler.ipynb)\n",
"[![image](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/opengeos/leafmap/blob/master/docs/maplibre/local_titiler.ipynb)\n",
"[![image](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/opengeos/leafmap/HEAD)\n",
"\n",
"**Start a TiTiler server**\n",
"\n",
"Uncomment the following line to install [leafmap](https://leafmap.org) if needed."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# %pip install \"leafmap[titiler]\""
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import leafmap.maplibregl as leafmap"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"leafmap.run_titiler(show_logs=False)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"url = \"https://huggingface.co/datasets/giswqs/geospatial/resolve/main/las_vegas_test_naip.tif\""
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"m = leafmap.Map()\n",
"m.add_cog_layer(url)\n",
"m"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "tiler",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.12"
}
},
"nbformat": 4,
"nbformat_minor": 4
}
6 changes: 6 additions & 0 deletions docs/maplibre/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,12 @@ View local raster datasets with localtileserver

[![](https://i.imgur.com/Q9sQLCP.png)](https://leafmap.org/maplibre/local_raster)

## Start a TiTiler server

Start a local TiTiler server for visualizing COGs.

[![](https://github.com/user-attachments/assets/73d77340-93d5-47a7-8c1a-373d2c154110)](https://leafmap.org/maplibre/local_titiler)

## Locate the user

Geolocate the user and then track their current location on the map using the GeolocateControl.
Expand Down
85 changes: 85 additions & 0 deletions docs/notebooks/109_local_titiler.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"[![image](https://jupyterlite.rtfd.io/en/latest/_static/badge.svg)](https://demo.leafmap.org/lab/index.html?path=notebooks/109_local_titiler.ipynb)\n",
"[![image](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/opengeos/leafmap/blob/master/docs/notebooks/109_local_titiler.ipynb)\n",
"[![image](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/opengeos/leafmap/HEAD)\n",
"\n",
"**Start a TiTiler server**\n",
"\n",
"Uncomment the following line to install [leafmap](https://leafmap.org) if needed."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# %pip install \"leafmap[titiler]\""
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import leafmap"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"leafmap.run_titiler(show_logs=False)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"url = \"https://huggingface.co/datasets/giswqs/geospatial/resolve/main/las_vegas_test_naip.tif\""
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"m = leafmap.Map()\n",
"m.add_cog_layer(url)\n",
"m"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "tiler",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.12"
}
},
"nbformat": 4,
"nbformat_minor": 4
}
1 change: 1 addition & 0 deletions leafmap/foliumap.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
read_netcdf,
read_raster,
read_rasters,
run_titiler,
save_colorbar,
search_qms,
search_xyz_services,
Expand Down
1 change: 1 addition & 0 deletions leafmap/maplibregl.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
random_string,
read_geojson,
read_vector,
run_titiler,
sort_files,
stac_assets,
start_duckdb_tile_server,
Expand Down
92 changes: 91 additions & 1 deletion leafmap/stac.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,9 @@ def check_titiler_endpoint(titiler_endpoint: Optional[str] = None) -> Any:
Returns:
The titiler endpoint.
"""
if titiler_endpoint is None:
if titiler_endpoint is not None and titiler_endpoint.lower() == "local":
titiler_endpoint = run_titiler(show_logs=False)
elif titiler_endpoint is None:
if os.environ.get("TITILER_ENDPOINT") is not None:
titiler_endpoint = os.environ.get("TITILER_ENDPOINT")

Expand Down Expand Up @@ -2514,3 +2516,91 @@ def get_cog_link_from_stac_item(item_url: str) -> str:
except Exception as e:
print(f"Failed to retrieve STAC item: {e}")
return None


def run_titiler(
show_logs: bool = False,
start_port: int = 8000,
max_port: int = 8100,
return_titiler_endpoint: bool = False,
):
Comment on lines +2521 to +2526

Copilot AI Nov 12, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mixing implicit and explicit returns may indicate an error, as implicit returns always return None.

Copilot uses AI. Check for mistakes.
"""Run TiTiler as a background service on an available port.

Args:
show_logs (bool): If True, stream logs to the notebook output.
start_port (int): First port to try.
max_port (int): Last port to try (exclusive).
return_titiler_endpoint (bool): If True, return the titiler endpoint. Defaults to False.

Returns:
tuple: (endpoint, port, process)
"""

import subprocess
import socket
import atexit
import signal
import time
import threading

def find_free_port(start, end):
for port in range(start, end):
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
try:
s.bind(("127.0.0.1", port))
return port
except OSError:
continue
raise RuntimeError(f"No free port found between {start} and {end}")

port = find_free_port(start_port, max_port)

cmd = [
"uvicorn",
"titiler.application.main:app",
"--host",
"127.0.0.1",
"--port",
str(port),
"--log-level",
"info",
]

proc = subprocess.Popen(
cmd,
stdout=subprocess.PIPE if show_logs else subprocess.DEVNULL,
stderr=subprocess.STDOUT,
)

# Optionally stream logs in a background thread
if show_logs:

def stream_logs():
for line in iter(proc.stdout.readline, b""):
print(line.decode().rstrip())

threading.Thread(target=stream_logs, daemon=True).start()

# Wait a bit for startup
time.sleep(2)
endpoint = f"http://127.0.0.1:{port}"
print(f"🚀 TiTiler is running at {endpoint}")

# Register cleanup on kernel shutdown
def stop_titiler():
if proc.poll() is None:
print("🛑 Stopping TiTiler...")
proc.send_signal(signal.SIGINT)
try:
proc.wait(timeout=5)
except subprocess.TimeoutExpired:
proc.kill()

atexit.register(stop_titiler)

os.environ["TITILER_ENDPOINT"] = endpoint
os.environ["TITILER_PORT"] = str(port)
os.environ["TITILER_PROCESS"] = str(proc)

if return_titiler_endpoint:
return endpoint, port, proc
2 changes: 2 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,7 @@ nav:
- maplibre/live_update_feature.ipynb
- maplibre/local_geojson.ipynb
- maplibre/local_raster.ipynb
- maplibre/local_titiler.ipynb
- maplibre/locate_user.ipynb
- maplibre/map_tiles.ipynb
- maplibre/mapillary.ipynb
Expand Down Expand Up @@ -402,3 +403,4 @@ nav:
- notebooks/106_aws_s3.ipynb
- notebooks/107_copernicus.ipynb
- notebooks/108_add_geotiff.ipynb
- notebooks/109_local_titiler.ipynb
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ authors = [{name = "Qiusheng Wu", email = "giswqs@gmail.com"}]
[project.optional-dependencies]
backends = ["bokeh", "keplergl", "maplibre", "pydeck", "plotly"]
lidar = ["geopandas","ipygany", "ipyvtklink", "laspy", "panel", "pyntcloud[LAS]", "pyvista[all]"]
raster = ["localtileserver>=0.10.6", "jupyter-server-proxy", "rio-cogeo", "rioxarray", "netcdf4", "d2spy", "h5netcdf", "h5py", "opera-utils", "rioxarray", "psutil"]
raster = ["localtileserver>=0.10.6", "jupyter-server-proxy", "rio-cogeo", "rioxarray", "netcdf4", "d2spy", "h5netcdf", "h5py", "opera-utils", "psutil", "titiler", "uvicorn"]
viewer = ["localtileserver", "fiona"]
usgs = ["pynhd", "py3dep"]
sql = ["psycopg2", "sqlalchemy"]
Expand All @@ -41,6 +41,7 @@ ai = ["geopandas", "osmnx", "localtileserver>=0.10.4", "rastervision", "pytorch-
maplibre = ["anywidget", "geopandas", "fiona", "h3", "ipyvuetify", "localtileserver", "mapclassify", "maplibre>=0.3.1", "pmtiles", "rioxarray", "xarray"]
gdal = ["gdal", "pyproj"]
duckdb = ["duckdb", "duckdb-engine", "jupysql", "flask", "flask-cors", "jupyter-server-proxy"]
titiler = ["titiler", "uvicorn"]


[project.scripts]
Expand Down