Skip to content

Instantly share code, notes, and snippets.

@chriswhong
Last active February 22, 2024 08:46
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save chriswhong/f492cc317c661001f41bcf0795162717 to your computer and use it in GitHub Desktop.
Save chriswhong/f492cc317c661001f41bcf0795162717 to your computer and use it in GitHub Desktop.
Node.js proxy endpoint to access TMS tiles via XYZ url

TMS vs XYZ

Web map raster tile URL templates usally look like this: //{servername}/{somepath}/{z}/{x}/{y}.png

Z is the zoom level (0 being zoomed all the way out so the earth fits on a single 256px x 256px tile, 18 or higher getting you down to street level) X and Y are the tiles coordinates, but there are two different standards for where the origin of the Y coordinate is.

If you look at [http://www.maptiler.org/google-maps-coordinates-tile-bounds-projection/](this site) which shows the various tile coordinates, you'll see that the X's are identical for 'Google' and 'TMS', but the Ys are different. 'Google', aka 'XYZ' aka a few other names that nobody seems to agree on places the Y origin at the north end of the earth, while TMS places it in the south.

Re-serve a TMS tileset as an XYZ tileset using express.js

I was recently trying to consume a TMS service in my web app, and ended up proxying the TMS tileset through my own express.js api so I could re-serve it as an XYZ dataset. The osm2tms function converts the TMS y to an XYZ y, and we just pipe the response from the TMS service to the client. This gist by Tom Macwright has the conversion formulas. They are also mentioned in this MapboxGL issue

This proxy route also allows me to use a different file extension (the TMS service was using .png8 but I want my urls to just be .png

So, if you want to consume third party tiles but want to keep all of your URLs standardized, a few lines of javascript can do the trick.

const express = require('express');
const request = require('request');
const router = express.Router();
function osm2tms(z, y) {
return (Math.pow(2, z) - y - 1); // from https://github.com/mapbox/mapbox-gl-js/pull/2565/files
}
router.get('/doitt/tms/1.0.0/photo/:year/:z/:x/:y.png', (req, res) => {
const { year, z, x, y } = req.params;
const sourceTile = `https://maps1.nyc.gov/tms/1.0.0/photo/${year}/${z}/${x}/${osm2tms(z, y)}.png8`;
request(sourceTile).pipe(res);
});
module.exports = router;
@weeix
Copy link

weeix commented Feb 22, 2024

Thanks, I've ported this to Go. Here's the code:

package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
	"strconv"

	"github.com/gorilla/mux"
)

func osm2tms(z, y int) int {
	return (1 << z) - y - 1
}

func main() {
	r := mux.NewRouter()

	sourceTileBaseUrl := os.Getenv("SOURCE_TILE_BASE_URL")
	if sourceTileBaseUrl == "" {
		sourceTileBaseUrl = "https://maps1.nyc.gov/tms/1.0.0/photo/"
	}

	r.HandleFunc("/doitt/tms/1.0.0/photo/{year}/{z}/{x}/{y}.png", func(w http.ResponseWriter, r *http.Request) {
		vars := mux.Vars(r)
		year := vars["year"]
		z, _ := strconv.Atoi(vars["z"])
		x, _ := strconv.Atoi(vars["x"])
		y, _ := strconv.Atoi(vars["y"])

		sourceTile := fmt.Sprintf("%s/%s/%d/%d/%d.png8", sourceTileBaseUrl, year, z, x, osm2tms(z, y))

		resp, err := http.Get(sourceTile)
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
		defer resp.Body.Close()

		w.Header().Set("Content-Type", resp.Header.Get("Content-Type"))
		w.Header().Set("Content-Length", resp.Header.Get("Content-Length"))

		if _, err := io.Copy(w, resp.Body); err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
		}
	})

	fmt.Printf("Proxying to %s\n", sourceTileBaseUrl)
	fmt.Printf("Server listening on port 8080\n")
	http.ListenAndServe(":8080", r)
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment