#!/usr/bin/ruby # Vector tile server # # This serves individual .pbf tiles from an .mbtiles file, plus # any static files in the static/ directory. Use it for testing tilemaker output # with a renderer such as Mapbox GL. (Your .mbtiles should be # created with compression enabled.) # # Standalone syntax: # ruby server.rb path_to.mbtiles # # It can also be run under Phusion Passenger. require 'sqlite3' require 'cgi' require 'json' begin; require 'glug' # Optional glug dependency rescue LoadError; end # | class MapServer CONTENT_TYPES = { json: "application/json", png: "image/png", pbf: "application/octet-stream", css: "text/css", js: "application/javascript" } EMPTY_TILE = ["1F8B0800FA78185E000393E2E3628F8F4FCD2D28A9D46850A86002006471443610000000"].pack('H*') EMPTY_FONT = ["0A1F0A124D6574726F706F6C697320526567756C61721209313533362D31373931"].pack('H*') def initialize(mbtiles, max_age=604800) @@mbtiles = mbtiles @@max_age = max_age MapServer.connect end def self.connect @@db = SQLite3::Database.new(@@mbtiles) Dir.chdir("static") unless Dir.pwd.include?("static") self end def read_metadata md = {} @@db.execute("SELECT name,value FROM metadata").each do |row| k,v = row md[k] = k=='json' ? JSON.parse(v) : v end md end def call(env) path = CGI.unescape( (env['REQUEST_PATH'] || env['REQUEST_URI']).sub(/^\//,'') ) if path.empty? then path='index.html' end if path =~ %r!^/?(\d+)/(\d+)/(\d+).*\.pbf! # Serve .pbf tile from mbtiles z,x,y = $1.to_i, $2.to_i, $3.to_i tms_y = 2**z - y - 1 res = @@db.execute("SELECT tile_data FROM tiles WHERE zoom_level=? AND tile_column=? AND tile_row=?", [z, x, tms_y]) blob = res.length==0 ? EMPTY_TILE : res[0][0] ['200', { 'Content-Type' => 'application/x-protobuf', 'Content-Encoding'=> 'gzip', 'Content-Length' => blob.bytesize.to_s, 'Cache-Control' => "max-age=#{@@max_age}", 'Access-Control-Allow-Origin' => '*' }, [blob]] elsif path=='metadata' # Serve mbtiles metadata ['200', {'Content-Type' => 'application/json', 'Cache-Control' => "max-age=#{@@max_age}", 'Access-Control-Allow-Origin' => '*' }, [read_metadata.to_json]] # elsif ARGV.include?(path.sub(/\.json$/,'.glug')) # # Convert .glug style to .json # # ****** fixme # glug = Glug::Stylesheet.new { instance_eval(File.read( path.sub(/\.json$/,'.glug') )) }.to_json # ['200', {'Content-Type' => 'application/json' }, [glug] ] elsif File.exist?(path) # Serve static file ct = path.match(/\.(\w+)$/) ? (CONTENT_TYPES[$1.to_sym] || 'text/html') : 'text/html' ['200', {'Content-Type' => ct, 'Cache-Control' => "max-age=#{@@max_age}", 'Access-Control-Allow-Origin' => '*'}, [File.read(path)]] elsif path=~/font.+\.pbf/ # Font not found so send dummy file ['200', { 'Content-Type' => 'application/x-protobuf', 'Content-Length' => EMPTY_FONT.bytesize.to_s, 'Access-Control-Allow-Origin' => '*' }, [EMPTY_FONT]] else # Not found puts "Couldn't find #{path}" ['404', {'Content-Type' => 'text/html'}, ["Resource at #{path} not found"]] end end # Start server if defined?(PhusionPassenger) puts "Starting Passenger server" PhusionPassenger.on_event(:starting_worker_process) do |forked| if forked then MapServer.connect end end else puts "Starting local server" require 'rackup' server = MapServer.new(ARGV[0],0) app = Proc.new { |env| server.call(env) } Rackup::Handler::WEBrick.run(app) end end