├── .gitignore
├── .ruby-gemset
├── .ruby-version
├── Procfile
├── Gemfile
├── public
├── images
│ └── whoots_tiles.jpg
└── index.html
├── config.ru
├── Gemfile.lock
├── config
└── puma
│ ├── development.rb
│ └── production.rb
├── README.textile
├── whoots_app.rb
└── whoots_app.php
/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 |
--------------------------------------------------------------------------------
/.ruby-gemset:
--------------------------------------------------------------------------------
1 | whoots
2 |
--------------------------------------------------------------------------------
/.ruby-version:
--------------------------------------------------------------------------------
1 | ruby-3.3.6
2 |
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | web: bundle exec puma -C config/puma/${RACK_ENV:-development}.rb
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | gem 'rack'
4 | gem 'rack-protection'
5 | gem 'puma'
--------------------------------------------------------------------------------
/public/images/whoots_tiles.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/timwaters/whoots/HEAD/public/images/whoots_tiles.jpg
--------------------------------------------------------------------------------
/config.ru:
--------------------------------------------------------------------------------
1 | require 'rack'
2 | require 'rack/protection'
3 |
4 | require_relative './whoots_app'
5 |
6 | # Add security headers
7 | use Rack::Protection
8 | use Rack::Protection::XSSHeader
9 | use Rack::Protection::FrameOptions
10 |
11 | #use Rack::Reloader, 0 #<=- useful to uncomment for dev
12 | use Rack::Static, :urls => [""], :root => "public", :index => 'index.html'
13 | #use Rack::Directory, :index => 'index.html'
14 |
15 | run WhootsApp
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GEM
2 | remote: https://rubygems.org/
3 | specs:
4 | base64 (0.2.0)
5 | logger (1.6.6)
6 | nio4r (2.7.4)
7 | puma (6.6.0)
8 | nio4r (~> 2.0)
9 | rack (3.1.10)
10 | rack-protection (4.1.1)
11 | base64 (>= 0.1.0)
12 | logger (>= 1.6.0)
13 | rack (>= 3.0.0, < 4)
14 |
15 | PLATFORMS
16 | ruby
17 | x86_64-linux
18 |
19 | DEPENDENCIES
20 | puma
21 | rack
22 | rack-protection
23 |
24 | BUNDLED WITH
25 | 2.5.22
26 |
--------------------------------------------------------------------------------
/config/puma/development.rb:
--------------------------------------------------------------------------------
1 | # Development configuration
2 | environment 'development'
3 | directory ENV.fetch('APP_ROOT', Dir.pwd)
4 |
5 | # Development port
6 | port ENV.fetch('PORT', 9292)
7 |
8 | # Single worker for easier debugging
9 | workers 1
10 |
11 | # More threads for development to handle concurrent requests
12 | threads 1, 6
13 |
14 | # Enable code reloading
15 | worker_timeout 3600 if ENV.fetch("RACK_ENV", "development") == "development"
16 |
17 | # Development logging
18 | quiet false
19 |
20 | # Allow puma to be restarted by `rails restart` command
21 | plugin :tmp_restart
--------------------------------------------------------------------------------
/config/puma/production.rb:
--------------------------------------------------------------------------------
1 | # Production configuration for small server (128MB RAM)
2 | environment 'production'
3 | directory ENV.fetch('APP_ROOT', Dir.pwd)
4 |
5 | # Production port
6 | port ENV.fetch('PORT', 9292)
7 |
8 | # Single worker for small server
9 | workers 1
10 |
11 | # Conservative thread count
12 | threads 2, 4
13 |
14 | # Quiet logging in production
15 | quiet true
16 |
17 | # Production timeouts
18 | worker_timeout 60
19 |
20 | # Memory optimization
21 | max_heap_size = ENV.fetch('MAX_HEAP_SIZE', '64MB')
22 | out_of_band_gc_count = ENV.fetch('OOB_GC_COUNT', 1)
23 |
24 | before_fork do
25 | GC.compact if defined?(GC) && GC.respond_to?(:compact)
26 | end
27 |
28 | on_worker_boot do
29 | GC.start
30 | end
31 |
32 | # Memory monitoring
33 | worker_check_interval 30
34 | worker_timeout 30
--------------------------------------------------------------------------------
/README.textile:
--------------------------------------------------------------------------------
1 | h1. WhooTS : Rack, Ruby + mini WMS -> TMS Proxy
2 |
3 | Whoots is the tiny public wms to tms proxy
4 |
5 | Its a simple WMS to Google/OSM Scheme TMS proxy. You can use WMS servers in applications which only use those pesky "Slippy Tiles"
6 |
7 | h2. Installation
8 |
9 | You need :
10 | * Ruby!
11 | * Rack (gem install rack)
12 |
13 |
14 | h2. Usage
15 |
16 | * Gives OSM/Google Tiles, not true TMS (maybe later)
17 |
18 | http://example.com/tms/z/x/y/{layer}/http://path.to.wms.server
19 |
20 | defaults to png. Optionally pass in ?format=image/jpeg for jpeg e.g.
21 |
22 | http://whoots.mapwarper.net/tms/z/x/y/{layer}/http://path.to.wms.server?format=image/jpeg
23 |
24 |
25 |
26 | h2. Examples
27 | * 1 http://example.com/tms/!/!/!/2013/http://warper.geothings.net/maps/wms/2013
28 | *
29 | * 2a. From: http://hypercube.telascience.org/cgi-bin/mapserv?map=/home/ortelius/haiti/haiti.map&request=getMap&service=wms&version=1.1.1&format=image/jpeg&srs=epsg:4326&exceptions=application/vnd.ogc.se_inimage&layers=HAITI&
30 | *
31 | * 2b To: http://example.com/tms/!/!/!/HAITI/http://hypercube.telascience.org/cgi-bin/mapserv?map=/home/ortelius/haiti/haiti.map
32 | *
33 | * 2c Outputs: http://hypercube.telascience.org/cgi-bin/mapserv?bbox=-8051417.93739076,2107827.49199202,-8051265.06333419,2107980.36604859&format=image/png&service=WMS&version=1.1.1&request=GetMap&srs=EPSG:900913&width=256&height=256&layers=HAITI&map=/home/ortelius/haiti/haiti.map&styles=
34 |
35 | h2. Notes
36 | * Gives OSM/Google Tiles only, so make sure the WMS server support EPSG:3857
37 | * Doesn't do any caching.
38 | * See public/index.html for more examples and help!
39 |
40 |
41 | h2. puma
42 |
43 | for development:
44 | bundle exec puma -C config/puma/development.rb
45 |
46 | for production:
47 | RACK_ENV=production bundle exec puma -C config/puma/production.rb
48 |
49 |
50 |
--------------------------------------------------------------------------------
/whoots_app.rb:
--------------------------------------------------------------------------------
1 | #require 'rubygems'
2 | #require 'erb'
3 |
4 | class WhootsApp
5 |
6 | def self.call(env)
7 | req = Rack::Request.new(env)
8 |
9 | case req.path
10 | when /^\/hi\b/
11 | Rack::Response.new(["Hello World!"]).finish
12 | when /^\/tms\/\d+\/\d+\/\d+\/\w+\/*/
13 | #'/tms/:z/:x/:y/:layers/*'
14 | params = req.path.split("/")
15 |
16 | # Validate numeric parameters
17 | return [400, {'Content-Type' => 'text/plain'}, ['Invalid parameters']] unless (
18 | params[2..4].all? { |p| p.match?(/\A\d+\z/) }
19 | )
20 |
21 | layer = params[5].to_s.gsub(/[^a-zA-Z0-9\-_\.:\s%]/, '')
22 |
23 | z,x,y = params[2],params[3],params[4]
24 |
25 | scheme = params[6].strip.match?(/\Ahttps?:\z/) ? params[6].strip.chomp(':') : 'http'
26 |
27 | # Sanitize splat path to prevent directory traversal
28 | splat = "#{scheme}://" + params[6..params.length]
29 | .select { |p| p.match?(/\A[\w\-\.\/]+\z/) }
30 | .join("/")
31 | # splat = params[6..params.length].join("/")
32 | # Validate splat is not empty after sanitization
33 | return [400, {'Content-Type' => 'text/plain'}, ['Invalid path']] if splat.empty?
34 |
35 | query_params = Rack::Utils.parse_query(req.query_string)
36 |
37 | # Sanitize map parameter
38 | map = query_params["map"].to_s.gsub(/[^a-zA-Z0-9\-_\.\/]/, '')
39 |
40 | x = x.to_i
41 | y = y.to_i
42 | z = z.to_i
43 | #for Google/OSM tile scheme we need to alter the y:
44 | y = ((2**z)-y-1)
45 | #calculate the bbox
46 | bbox = get_tile_bbox(x,y,z)
47 | #build up the other params
48 | format = query_params["format"] == "image/jpeg" ? "image/jpeg" : "image/png"
49 | service = "WMS"
50 | version = "1.1.1"
51 | request = "GetMap"
52 | srs = "EPSG:3857"
53 | width = "256"
54 | height = "256"
55 | layers = layer || ""
56 |
57 | # Only include map parameter if it exists and is not empty
58 | map_param = ""
59 | unless query_params["map"].to_s.empty?
60 | map = query_params["map"].to_s.gsub(/[^a-zA-Z0-9\-_\.\/]/, '')
61 | map_param = "&map=" + map
62 | end
63 |
64 | base_url = splat
65 | url = base_url + "?"+ "bbox="+bbox+"&format="+format+"&service="+service+"&version="+version+"&request="+request+"&srs="+srs+"&width="+width+"&height="+height+"&layers="+layers+map_param+"&styles="
66 |
67 | return [302, {'Location' => url}, []]
68 | else
69 | Rack::Response.new(["Not found. Whoots"], 404).finish
70 | end
71 | end
72 |
73 |
74 |
75 | def self.get_tile_bbox(x,y,z)
76 | min_x, min_y = get_merc_coords(x * 256, y * 256, z)
77 | max_x, max_y = get_merc_coords( (x + 1) * 256, (y + 1) * 256, z )
78 | return "#{min_x},#{min_y},#{max_x},#{max_y}"
79 | end
80 |
81 | def self.get_merc_coords(x,y,z)
82 | resolution = (2 * Math::PI * 6378137 / 256) / (2 ** z)
83 | merc_x = (x * resolution -2 * Math::PI * 6378137 / 2.0)
84 | merc_y = (y * resolution - 2 * Math::PI * 6378137 / 2.0)
85 | return merc_x, merc_y
86 | end
87 |
88 |
89 | end
90 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
5 | WhooTS - the tiny public wms to tms proxyIts a simple WMS to Google/OSM Scheme TMS proxy. You can use WMS servers in applications which only use those pesky "Slippy Tiles" 8 |
9 | 10 |About: Made with Rack and Ruby by Tim Waters tim@geothings.net Blog
53 | Code available at: github