Map tiles
ExpTech serves radar reflectivity, satellite imagery, wind, QPESUMS precipitation forecasts, and other weather layers, plus base map, terrain, and streets-and-buildings tiles, as XYZ tiles (slippy map tiles). Weather layers update over time. Request the list endpoint for the times that exist, then put a time into the tile URL. You can add the tiles directly to MapLibre GL JS, Leaflet, OpenLayers, and other map libraries.
Layers
Section titled “Layers”| Layer | Time list | Tile URL | Zoom | Source |
|---|---|---|---|---|
| Radar reflectivity | /api/v2/tiles/radar/list |
/api/v2/tiles/radar/{time}/{z}/{x}/{y}.webp |
3–12 | Central Weather Administration (CWA) |
| Satellite imagery | /api/v2/tiles/satellite/{channel}/list |
/api/v2/tiles/satellite/{channel}/{style}/{time}/{z}/{x}/{y}.webp |
0–11 | Himawari-9 (JMA, via NOAA Open Data) |
| Wind | /api/v2/tiles/wind/{model}/list |
/api/v2/tiles/wind/{model}/{cycle}/{time}/{z}/{x}/{y}.webp |
0–11 | NOAA GFS, ECMWF open data |
| QPESUMS | /api/v2/tiles/qpesums/list |
See that page | See that page | CWA QPESUMS |
| Base map | Not required (TileJSON) | See that page | See the TileJSON | See that page |
Data license
Section titled “Data license”- Radar tilesAttribution · Commercial use OK
AttributionData: Central Weather Administration (CWA), ExpTech Studio (exptech.dev)
- Satellite tilesAttribution · Commercial use OK
AttributionData: JMA Himawari-9 via NOAA Open Data, ExpTech Studio (exptech.dev)
- Wind (GFS)Attribution · Commercial use OK
AttributionData: NOAA GFS, ExpTech Studio (exptech.dev)
- Wind (ECMWF)Attribution · Commercial use OK
AttributionData: ECMWF open data (CC BY 4.0), ExpTech Studio (exptech.dev)
- QPESUMS tilesAttribution · Commercial use OK
AttributionData: CWA QPESUMS, ExpTech Studio (exptech.dev)
- Base map, TerrainAttribution · Commercial use OK
AttributionData: ExpTech Studio (exptech.dev)
- Streets & buildingsAttribution · Commercial use OK
AttributionData: © OpenStreetMap contributors, © OpenMapTiles, ExpTech Studio (exptech.dev)
- Central Weather Administration (CWA): CWA's own data terms (such as the Open Government Data License) also apply; CWA's official releases take precedence.
- JMA Himawari-9 (via NOAA Open Data): The terms JMA and NOAA Open Data set for Himawari data also apply.
- NOAA, NOAA GFS: U.S. government data, generally free of use restrictions; do not imply NOAA endorses your product.
- ECMWF open data: ECMWF open data is licensed under CC BY 4.0: credit ECMWF, name the licence and indicate that the data was processed.
- OpenStreetMap: OpenStreetMap data is licensed under the ODbL: show "© OpenStreetMap contributors" on the map and link to https://www.openstreetmap.org/copyright .
- OpenMapTiles: Tiles built on the OpenMapTiles schema must show "© OpenMapTiles".
Where to show it, examples and commercial-use requests: Data licensing and attribution.
Authentication
Section titled “Authentication”No API Token is required. Every list and tile responds with Access-Control-Allow-Origin: *, so a browser can load them cross-origin.
Servers
Section titled “Servers”Lists and tiles for weather layers are on different hosts:
- Time lists: https://api.core-tnn1.exptech.dev
- Tiles and binary files: https://static.core-tnn1.exptech.dev
- Base map, terrain, and streets and buildings: https://lb.exptech.dev
Shared steps
Section titled “Shared steps”- Request the list endpoint for the available times.
- Expand the list into Unix seconds. Radar and satellite lists are delta-encoded. See below.
- Build tile URLs only from times in the list. A time outside the list returns 404 or 500. The server does not substitute the nearest time.
- Add the URL template to the map. Leave
{z},{x}, and{y}for the map library to fill in.
Delta-encoded time lists
Section titled “Delta-encoded time lists”A radar or satellite list is [oldest time, delta, delta, …]. Every value is Unix seconds, oldest first. The first number is the oldest time. Each later number is the gap from the previous time. When there is no data, the list is an empty array [].
[1794641400, 600, 600, 600]Decoded, that is 1794641400, 1794642000, 1794642600, and 1794643200 (2026-11-14 15:30–16:00 Taiwan time).
// [oldest, Δ, Δ, …] → [time, time, …] (Unix seconds, oldest first)function decodeTimes(list) { const out = []; for (let i = 0; i < list.length; i++) { out.push(i === 0 ? list[0] : out[i - 1] + list[i]); } return out;}The wind list is different. Each entry is an object { "t": valid time, "c": model cycle time }. See Wind.
Tile format
Section titled “Tile format”- Coordinates: XYZ (Web Mercator, EPSG:3857).
yincreases from north to south, the same as OpenStreetMap. This is the XYZ scheme, which differs from TMS. - Size and format: 256 × 256 WebP, with an alpha channel. Areas with no data are transparent.
- Zoom range: A
zoutside the range still returns 200. The body is a 1 × 1 transparent GIF (Content-Type: image/gif). Setminzoomandmaxzoomin the map library, and scale up the highest zoom level past the maximum. Do not request tiles beyond it. - File extension: A URL ending in
.pngstill returns WebP. Always use.webp.
Caching
Section titled “Caching”The bytes for a given tile URL are fixed, because the time and the model cycle are in the URL. Cache them on the client for a long time.
| Item | Response headers | Notes |
|---|---|---|
| Time lists (radar, satellite, wind, QPESUMS) | No Cache-Control |
New data shows up in the list within about 1 second. Request the list again when you need the latest time. |
| Radar tiles | No Cache-Control. Has ETag |
Send If-None-Match to get 304. |
| Satellite tiles | Cache-Control: public, max-age=300. Has ETag |
A 404 is kept for about 10 seconds. |
| Wind tiles and WND1 binaries | Cache-Control: public, max-age=1800. Has ETag |
A 404 is kept for about 10 seconds. |
| QPESUMS tiles | Not confirmed | A 404 is kept for about 5 seconds. |
List responses are gzip-compressed when the client supports it (Content-Encoding: gzip). A browser fetch decompresses them. With curl, add --compressed.
Attribution
Section titled “Attribution”On a map, set each layer’s suggested attribution (see Data license on this page) on the source’s attribution. The map library draws it in a corner of the map. When you stack layers, set attribution on each one. The radar layer in the examples below does this. For where to put the credit, and for more examples, see Data license & attribution.
Advanced examples
Section titled “Advanced examples”Two radar-reflectivity animations you can run as-is, one with MapLibre GL JS and one with Leaflet.
MapLibre GL JS
Section titled “MapLibre GL JS”Load the latest 12 radar times, preload them, and show them in turn. The base map uses OpenStreetMap’s public tiles so the example runs on its own. For a production service, use your own base map or the ExpTech base map.
<!doctype html><html lang="zh-Hant-TW"><head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>Radar reflectivity animation (MapLibre GL JS)</title> <style> html, body, #map { height: 100%; margin: 0; } #time { position: absolute; top: 10px; left: 10px; padding: 4px 8px; background: #fff; font: 14px sans-serif; } </style></head><body> <div id="map"></div> <div id="time"></div> <script> const API = "https://api.core-tnn1.exptech.dev"; const STATIC = "https://static.core-tnn1.exptech.dev"; const FRAMES = 12; // How many of the latest times the animation uses const INTERVAL = 500; // Milliseconds each frame stays on screen const OPACITY = 0.8;
function decodeTimes(list) { const out = []; for (let i = 0; i < list.length; i++) { out.push(i === 0 ? list[0] : out[i - 1] + list[i]); } return out; }
function formatTime(sec) { return new Date(sec * 1000).toLocaleString("zh-TW", { timeZone: "Asia/Taipei", hour12: false }); }
const map = new maplibregl.Map({ container: "map", center: [121, 23.7], zoom: 6, style: { version: 8, sources: { osm: { type: "raster", tiles: ["https://tile.openstreetmap.org/{z}/{x}/{y}.png"], tileSize: 256, maxzoom: 19, attribution: "© OpenStreetMap contributors", }, }, layers: [{ id: "osm", type: "raster", source: "osm" }], }, });
async function addRadar() { const res = await fetch(`${API}/api/v2/tiles/radar/list`); if (!res.ok) throw new Error(`radar list: HTTP ${res.status}`); const times = decodeTimes(await res.json()).slice(-FRAMES); if (times.length === 0) return;
// One source and layer per time. Add them all first (opacity 0) so the tiles preload, then show them in turn. times.forEach((t, i) => { map.addSource(`radar-${t}`, { type: "raster", tiles: [`${STATIC}/api/v2/tiles/radar/${t}/{z}/{x}/{y}.webp`], tileSize: 256, minzoom: 3, maxzoom: 12, attribution: "Data: Central Weather Administration (CWA), ExpTech Studio (exptech.dev)", }); map.addLayer({ id: `radar-${t}`, type: "raster", source: `radar-${t}`, paint: { "raster-opacity": i === times.length - 1 ? OPACITY : 0, "raster-fade-duration": 0, }, }); });
const label = document.getElementById("time"); let current = times.length - 1; label.textContent = formatTime(times[current]); setInterval(() => { map.setPaintProperty(`radar-${times[current]}`, "raster-opacity", 0); current = (current + 1) % times.length; map.setPaintProperty(`radar-${times[current]}`, "raster-opacity", OPACITY); label.textContent = formatTime(times[current]); }, INTERVAL); }
map.on("load", () => { addRadar().catch((err) => console.error(err)); }); </script></body></html>For the latest frame only, take times[times.length - 1] and add one source and one layer.
Leaflet
Section titled “Leaflet”<!doctype html><html lang="zh-Hant-TW"><head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>Radar reflectivity animation (Leaflet)</title> <style> html, body, #map { height: 100%; margin: 0; } .radar-time { padding: 4px 8px; background: #fff; font: 14px sans-serif; } </style></head><body> <div id="map"></div> <script> const API = "https://api.core-tnn1.exptech.dev"; const STATIC = "https://static.core-tnn1.exptech.dev"; const FRAMES = 12; const INTERVAL = 500; const OPACITY = 0.8;
function decodeTimes(list) { const out = []; for (let i = 0; i < list.length; i++) { out.push(i === 0 ? list[0] : out[i - 1] + list[i]); } return out; }
function formatTime(sec) { return new Date(sec * 1000).toLocaleString("zh-TW", { timeZone: "Asia/Taipei", hour12: false }); }
const map = L.map("map").setView([23.7, 121], 7); L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", { maxZoom: 19, attribution: "© OpenStreetMap contributors", }).addTo(map);
const label = L.control({ position: "topright" }); label.onAdd = () => L.DomUtil.create("div", "radar-time"); label.addTo(map);
async function addRadar() { const res = await fetch(`${API}/api/v2/tiles/radar/list`); if (!res.ok) throw new Error(`radar list: HTTP ${res.status}`); const times = decodeTimes(await res.json()).slice(-FRAMES); if (times.length === 0) return;
// One layer per time. Add them all first (opacity 0) so the tiles preload. const layers = times.map((t, i) => L.tileLayer(`${STATIC}/api/v2/tiles/radar/${t}/{z}/{x}/{y}.webp`, { minZoom: 3, maxNativeZoom: 12, // Above 12, scale up level-12 tiles maxZoom: 19, opacity: i === times.length - 1 ? OPACITY : 0, attribution: "Data: Central Weather Administration (CWA), ExpTech Studio (exptech.dev)", }).addTo(map), );
let current = times.length - 1; label.getContainer().textContent = formatTime(times[current]); setInterval(() => { layers[current].setOpacity(0); current = (current + 1) % layers.length; layers[current].setOpacity(OPACITY); label.getContainer().textContent = formatTime(times[current]); }, INTERVAL); }
addRadar().catch((err) => console.error(err)); </script></body></html>- Request only times from the list. For a time that does not exist, radar tiles return 500 (the body is a single newline). Satellite and wind tiles return 404 JSON
{"error":"Data not found for specified timestamp"}. - The list gains and drops times. A page left open should refresh the list on a schedule (for example every 5 minutes) and remove times that are no longer in it.
- Timestamps are Unix seconds. Do not use milliseconds for
{time}or{cycle}in the URL. - The radar list is also available on the tile host (https://static.core-tnn1.exptech.dev), but that copy is cached for about an hour and is a poor way to find the latest time. Use the list host.
- The OpenStreetMap public tiles used in the examples have a usage policy. For a production service, switch to another base map.
