diff --git a/Gemfile.lock b/Gemfile.lock index 6570010..560aacf 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - mapstatic (0.0.2) + mapstatic (0.1) mini_magick (~> 4.9) typhoeus (~> 1.3) @@ -18,7 +18,7 @@ GEM ffi (>= 1.3.0) ffi (1.11.1) hashdiff (0.4.0) - mini_magick (4.9.3) + mini_magick (4.9.5) public_suffix (3.1.1) rake (12.3.2) rspec (3.8.0) @@ -50,7 +50,7 @@ PLATFORMS DEPENDENCIES awesome_print (< 2.0) mapstatic! - rake (~> 12) + rake rspec (~> 3) thor (>= 0.19.0, < 2.0) vcr (~> 3) diff --git a/README.md b/README.md index 158055d..64ae558 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ A CLI and Ruby Gem for generating static maps from map tile servers. ## Installation gem install mapstatic - + if you want to use command line for creating maps then additional gems are required gem install mapstatic @@ -22,31 +22,42 @@ if you want to use command line for creating maps then additional gems are requi There are two ways to generate a static map from the mapstatic CLI. 1. Specifying a bounding box and zoom level + + The command below generates a map of the UK using the [OpenStreetMap](http://www.openstreetmap.org/) tileset. The width and height of the resulting image (`uk.png`) are determined by the bounding box and the zoom level. If you don't know the bounding box of the area then [this is a useful tool](http://boundingbox.klokantech.com/). + + ```.bash + mapstatic map uk.png \ + --zoom=5 \ + -- bbox=-11.29,49.78,2.45,58.78 + ``` + + ![UK](http://matchingnotes.com/images/uk.png) + 2. Specifying a center lat, center lng, width, height and zoom level -The command below generates a map of the UK using the [OpenStreetMap](http://www.openstreetmap.org/) tileset. The width and height of the resulting image (`uk.png`) are determined by the bounding box and the zoom level. If you don't know the bounding box of the area then [this is a useful tool](http://boundingbox.klokantech.com/). + Alternatively, you can specify a central latitude and longitude and specify the width and height. -```.bash -mapstatic map uk.png \ - --zoom=5 \ - --bbox=-11.29,49.78,2.45,58.78 -``` + ```.bash + mapstatic map silicon-roundabout.png \ + --zoom=18 \ + --lat=51.52567 \ + --lng=-0.08750 \ + --width=600 \ + --height=300 + ``` -![UK](http://matchingnotes.com/images/uk.png) +![Silicon Roundabout](http://matchingnotes.com/images/silicon-roundabout.png) -Alternatively, you can specify a central latitude and longitude and specify the width and height. +Optionally a gpx file can be specified. In that case, the routes and tracks contained in that file will be drawn on top of the map. The map view will be automatically adjusted to fit the given gpx route data. ```.bash -mapstatic map silicon-roundabout.png \ - --zoom=18 \ - --lat=51.52567 \ - --lng=-0.08750 \ +mapstatic map map.png \ + --zoom=12 \ --width=600 \ - --height=300 + --height=300 \ + --gpx=file.gpx ``` -![Silicon Roundabout](http://matchingnotes.com/images/silicon-roundabout.png) - ## Changing the provider Mapstatic can generate maps from any slippy map tile server. The tile provider can be specified with a URL template @@ -88,15 +99,38 @@ Mapstatic can be used in your application code to generate maps and get metadata ```.ruby require 'mapstatic' + +# Initialize map = Mapstatic::Map.new( - :zoom => 12, - :bbox => "-0.218894,51.450943,0.014382,51.553755", - :provider => 'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png' + width: 400, + height: 200 + zoom: 11, + provider: 'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png' ) -map.render_map 'london.png' + +# optional: set geojson data layer +geojson_data = { + type: "FeatureCollection", + features: ... # Currently only LineString is supported by Mapstatic +} +map.geojson = geojson_data +map.fit_bounds # Call this to set map dimensions so that geojson data fits into map area + +# Render to file +map.to_file 'london.png' map.metadata # Returns the map metadata + +# You can also just render the image without writing it to a file. +# This will produce a MiniMagic image object. +image = map.to_image ``` +### Supported GeoJSON feature types + +* LineString + +To add support for more types, inherit a new class from Mapstatic::Painter, implement required methods, and add the class to painter_class_for method in `renderer.rb`. + ## License Mapstatic is licensed under the MIT license. diff --git a/Rakefile b/Rakefile index 80cc96d..c929717 100644 --- a/Rakefile +++ b/Rakefile @@ -10,48 +10,33 @@ RSpec::Core::RakeTask.new do |t| t.rspec_opts = %w(--format documentation --colour) end - task :default => ["spec"] -# This builds the actual gem. For details of what all these options -# mean, and other ones you can add, check the documentation here: -# -# http://rubygems.org/read/chapter/20 -# spec = Gem::Specification.new do |s| - - # Change these as appropriate s.name = "mapstatic" s.version = Mapstatic::VERSION s.summary = "Static Map Generator" - s.author = "James Croft" + s.authors = ["James Croft", "Mika Haulo", "Tim Neems", "Olli Huotari", "Michael O'Toole"] s.email = "james@matchingnotes.com" s.homepage = "https://github.com/crofty/mapstatic" + s.license = "MIT" - s.has_rdoc = true - # You should probably have a README of some kind. Change the filename - # as appropriate - # s.extra_rdoc_files = %w(README) - # s.rdoc_options = %w(--main README) - - # Add any extra files to include in the gem (like your README) s.files = %w(Gemfile Gemfile.lock) + Dir.glob("{spec,lib}/**/*") s.require_paths = ["lib"] s.executables << 'mapstatic' - # If you want to depend on other gems, add them here, along with any - # relevant versions - # s.add_dependency("some_other_gem", "~> 0.1.0") s.add_dependency('mini_magick', '~> 4.9') s.add_dependency('typhoeus', '~> 1.3') + s.add_dependency('nokogiri', '~> 1.10') - s.add_development_dependency('thor', ['>= 0.19.0', '< 2.0']) # install these if you want to use command line env - s.add_development_dependency('awesome_print', '< 2.0') # install these if you want to use command line env - - s.add_development_dependency('rake') # needed so that 'bundle exec rake' will work as expected + s.add_development_dependency('rake', '~> 12') # needed so that 'bundle exec rake' will work as expected s.add_development_dependency('rspec', '~> 3') s.add_development_dependency('vcr', '~> 3') s.add_development_dependency('webmock', '~> 2') + + # install these if you want to use command line env + s.add_development_dependency('thor', ['>= 0.19.0', '< 2.0']) + s.add_development_dependency('awesome_print', '< 2.0') end # This task actually builds the gem. We also regenerate a static diff --git a/lib/mapstatic.rb b/lib/mapstatic.rb index 2eacb8c..4e14f72 100644 --- a/lib/mapstatic.rb +++ b/lib/mapstatic.rb @@ -11,6 +11,11 @@ def self.options require 'mapstatic/errors' require 'mapstatic/version' require 'mapstatic/conversion' +require 'mapstatic/bounding_box' require 'mapstatic/map' require 'mapstatic/tile' require 'mapstatic/tile_source' +require 'mapstatic/renderer' +require 'mapstatic/painter' +require 'mapstatic/painter/null_painter' +require 'mapstatic/painter/line_string_painter' diff --git a/lib/mapstatic/bounding_box.rb b/lib/mapstatic/bounding_box.rb new file mode 100644 index 0000000..2cac0ee --- /dev/null +++ b/lib/mapstatic/bounding_box.rb @@ -0,0 +1,92 @@ +module Mapstatic + class BoundingBox + attr_accessor :left, :right, :top, :bottom + + def initialize(params={}) + @left = params.fetch(:left) + @bottom = params.fetch(:bottom) + @right = params.fetch(:right) + @top = params.fetch(:top) + end + + def to_a + [left, bottom, right, top] + end + alias_method :to_latlng_coordinates, :to_a + + def to_xy_coordinates(zoom) + [ + Conversion.lng_to_x(left, zoom), + Conversion.lat_to_y(bottom, zoom), + Conversion.lng_to_x(right, zoom), + Conversion.lat_to_y(top, zoom) + ] + end + + def center + lat = (bottom + top) / 2 + lng = (left + right) / 2 + + {lat: lat, lng: lng} + end + + def center=(lat:, lng:) + delta_lat = lat - center[:lat] + delta_lng = lng - center[:lng] + + @left += delta_lng + @bottom += delta_lat + @right += delta_lng + @top += delta_lat + end + + def width_at(zoom) + delta = Conversion.lng_to_x(right, zoom) - Conversion.lng_to_x(left, zoom) + (delta * Map::TILE_SIZE).abs + end + + def height_at(zoom) + delta = Conversion.lat_to_y(top, zoom) - Conversion.lat_to_y(bottom, zoom) + (delta * Map::TILE_SIZE).abs + end + + def fits_in?(other) + left >= other.left and right <= other.right and top <= other.top and bottom >= other.bottom + end + + def contains?(other) + other.fits_in? self + end + + def set_to(other) + @left = other.left + @right = other.right + @top = other.top + @bottom = other.bottom + end + + def self.for(coordinates) + lngs = coordinates.map {|point| point[0]} + lats = coordinates.map {|point| point[1]} + + left = lngs.min + bottom = lats.min + right = lngs.max + top = lats.max + + BoundingBox.new top: top, bottom: bottom, left: left, right: right + end + + def self.from(center_lat:, center_lng:, width:, height:, zoom:) + x = Conversion.lng_to_x(center_lng, zoom) + y = Conversion.lat_to_y(center_lat, zoom) + + left = Conversion.x_to_lng( x - ( width / 2 ), zoom) + right = Conversion.x_to_lng( x + ( width / 2 ), zoom) + bottom = Conversion.y_to_lat( y + ( height / 2 ), zoom) + top = Conversion.y_to_lat( y - ( height / 2 ), zoom) + + BoundingBox.new top: top, bottom: bottom, left: left, right: right + end + end +end diff --git a/lib/mapstatic/cli.rb b/lib/mapstatic/cli.rb index c486546..472f195 100644 --- a/lib/mapstatic/cli.rb +++ b/lib/mapstatic/cli.rb @@ -1,6 +1,8 @@ require 'mapstatic' +require 'mapstatic/gpx_file' require 'awesome_print' require 'thor' +require 'json' class Mapstatic::CLI < Thor @@ -29,24 +31,53 @@ class Mapstatic::CLI < Thor OpenStreetMap contributors). You can generate a map using any tile set by passing the --provider option. + + To draw a gpx track on top of the map, you can pass a file with --gpx: + + $ mapstatic map uk.png --zoom=6 --width=320 --height=290 --gpx=file.gpx LONGDESC - option :zoom, :required => true + option :zoom option :provider, :default => 'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png' option :bbox option :lat option :lng option :width, :default => 256 option :height, :default => 256 + option :gpx option :dryrun, :type => :boolean, :default => false def map(filename) params = Hash[options.map{|(k,v)| [k.to_sym,v]}] + if params[:bbox] + bbox = params[:bbox].split(",").map { |c| c.to_f } + params[:bbox] = bbox + end + map = Mapstatic::Map.new(params) + if options[:gpx] + gpx_file = Mapstatic::GpxFile.new options[:gpx] + geojson_data = gpx_file.geojson_data + + # Drawing only one geojson feature is supported at the moment. So just pick + # the first one on the file. + first_track = geojson_data[:features].first + map.geojson = first_track + map.fit_bounds + end + map.render_map(filename) unless options[:dryrun] - ap map.metadata + + metadata = { + map_bbox: map.viewport.to_a.join(','), + width: map.width.to_i, + height: map.height.to_i, + zoom: map.zoom + } + + ap metadata end end diff --git a/lib/mapstatic/conversion.rb b/lib/mapstatic/conversion.rb index 2412558..5e84496 100644 --- a/lib/mapstatic/conversion.rb +++ b/lib/mapstatic/conversion.rb @@ -1,28 +1,41 @@ module Mapstatic - class Conversion - - def lng_to_x(lng, zoom) + def self.lng_to_x(lng, zoom) n = 2 ** zoom ((lng.to_f + 180) / 360) * n end - def x_to_lng(x, zoom) + def self.x_to_lng(x, zoom) n = 2.0 ** zoom lon_deg = x / n * 360.0 - 180.0 end - def lat_to_y(lat, zoom) + def self.lat_to_y(lat, zoom) n = 2 ** zoom lat_rad = (lat / 180) * Math::PI (1 - Math.log( Math.tan(lat_rad) + (1 / Math.cos(lat_rad)) ) / Math::PI) / 2 * n end - def y_to_lat(y, zoom) + def self.y_to_lat(y, zoom) n = 2.0 ** zoom lat_rad = Math.atan(Math.sinh(Math::PI * (1 - 2 * y / n))) lat_deg = lat_rad / (Math::PI / 180.0) end - end + # Convert pixel coordinate x from Earth perspective (i.e the reference point, or the 0 value + # is at the prime meridian) to image perspective. The zero point of the image depends on its + # bounding box. + def self.x_to_px(x, bbox_center_x, bbox_width, tile_size) + px = (x - bbox_center_x) * tile_size + bbox_width / 2 + px.round + end + + # Convert pixel coordinate y from Earth perspective (i.e the reference point, or the 0 value + # is at the equator) to image perspective. The zero point of the image depends on its + # bounding box. + def self.y_to_px(y, bbox_center_y, bbox_height, tile_size) + px = (y - bbox_center_y) * tile_size + bbox_height / 2 + px.round + end + end end diff --git a/lib/mapstatic/gpx_file.rb b/lib/mapstatic/gpx_file.rb new file mode 100644 index 0000000..601e85c --- /dev/null +++ b/lib/mapstatic/gpx_file.rb @@ -0,0 +1,61 @@ +require 'nokogiri' + +module Mapstatic + class GpxFile + attr_reader :tracks, :routes + + def initialize(filename) + @filename = filename + @tracks = [] + @routes = [] + parse + end + + def geojson_data + features = features_from(@tracks) + features_from(@routes) + + { + type: "FeatureCollection", + features: features + } + end + + def to_geojson + geojson_data.to_json + end + + def self.to_geojson(filename) + GpxFile.new(filename).to_geojson + end + + private + + def parse + xml = Nokogiri::XML File.open(@filename) + + xml.css("trk").each do |trk| + @tracks << trk.css("trkpt").map do |pt| + [pt.attributes["lon"].value.to_f, pt.attributes["lat"].value.to_f] + end + end + + xml.css("rte").each do |rte| + @routes << rte.css("rtept").map do |pt| + [pt.attributes["lon"].value.to_f, pt.attributes["lat"].value.to_f] + end + end + end + + def features_from(tracks) + tracks.map do |track| + { + type: "Feature", + geometry: { + type: "LineString", + coordinates: track + } + } + end + end + end +end diff --git a/lib/mapstatic/map.rb b/lib/mapstatic/map.rb index 9842737..6a3f766 100644 --- a/lib/mapstatic/map.rb +++ b/lib/mapstatic/map.rb @@ -1,162 +1,99 @@ require 'mini_magick' +require 'json' module Mapstatic - class Map TILE_SIZE = 256 + MAX_ZOOM = 19 + MIN_ZOOM = 0 - attr_reader :zoom, :lat, :lng, :width, :height + attr_reader :lat, :lng, :viewport, :geojson, :zoom attr_accessor :tile_source def initialize(params={}) + @zoom = params.fetch(:zoom, 0).to_i + if params[:bbox] - @bounding_box = params[:bbox].split(',').map(&:to_f) + left, bottom, right, top = params[:bbox] + @viewport = BoundingBox.new top: top, bottom: bottom, left: left, right: right + else + @width = params.fetch(:width).to_i + @height = params.fetch(:height).to_i + lat = params.fetch(:lat, 0).to_f + lng = params.fetch(:lng, 0).to_f + + @viewport = BoundingBox.from( + center_lat: lat, + center_lng: lng, + width: @width.to_f / TILE_SIZE, + height: @height.to_f / TILE_SIZE, + zoom: @zoom + ) + end + + if params[:tile_source] + @tile_source = TileSource.new(params[:tile_source]) else - @lat = params.fetch(:lat).to_f - @lng = params.fetch(:lng).to_f - @width = params.fetch(:width).to_f - @height = params.fetch(:height).to_f + @tile_source = TileSource.new("http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png") end - @zoom = params.fetch(:zoom).to_i - @tile_source = TileSource.new(params[:provider]) end def width - @width ||= begin - left, bottom, right, top = bounding_box_in_tiles - (right - left) * TILE_SIZE + @width || begin + delta = Conversion.lng_to_x(viewport.right, zoom) - Conversion.lng_to_x(viewport.left, zoom) + (delta * TILE_SIZE).abs end end def height - @height ||= begin - left, bottom, right, top = bounding_box_in_tiles - (bottom - top) * TILE_SIZE + @height || begin + delta = Conversion.lat_to_y(viewport.top, zoom) - Conversion.lat_to_y(viewport.bottom, zoom) + (delta * TILE_SIZE).abs end end - def to_image - base_image = create_uncropped_image - base_image = fill_image_with_tiles(base_image) - crop_to_size base_image - base_image - end - - def render_map(filename) - to_image.write filename - end - - def metadata - { - :bbox => bounding_box.join(','), - :width => width.to_i, - :height => height.to_i, - :zoom => zoom, - :number_of_tiles => required_tiles.length, - } - end - - private - - def x_tile_space - Conversion.new.lng_to_x(lng, zoom) - end - - def y_tile_space - Conversion.new.lat_to_y(lat, zoom) - end - - def width_tile_space - width / TILE_SIZE - end - - def height_tile_space - height / TILE_SIZE - end - - def bounding_box - @bounding_box ||= begin - converter = Conversion.new - left = converter.x_to_lng( x_tile_space - (width_tile_space / 2), zoom) - right = converter.x_to_lng( x_tile_space + ( width_tile_space / 2 ), zoom) - top = converter.y_to_lat( y_tile_space - ( height_tile_space / 2 ), zoom) - bottom = converter.y_to_lat( y_tile_space + ( height_tile_space / 2 ), zoom) - - [ left, bottom, right, top ] + def zoom=(new_zoom) + @zoom = new_zoom + center = @viewport.center + @viewport = BoundingBox.from( + center_lat: center[:lat], + center_lng: center[:lng], + width: width.to_f / TILE_SIZE, + height: height.to_f / TILE_SIZE, + zoom: @zoom + ) + end + + def geojson=(data) + if data.is_a? String + @geojson = JSON.parse data + elsif data.is_a? Hash + # This looks really ugly, but it's just a quick and dirty way to ensure that keys in + # the hash are strings, not symbols. + @geojson = JSON.parse data.to_json end end - def bounding_box_in_tiles - left, bottom, right, top = bounding_box - converter = Conversion.new - [ - converter.lng_to_x(left, zoom), - converter.lat_to_y(bottom, zoom), - converter.lng_to_x(right, zoom), - converter.lat_to_y(top, zoom) - ] - end + def fit_bounds + return if @geojson.nil? - def required_x_tiles - left, bottom, right, top = bounding_box_in_tiles - Range.new(*[left, right].map(&:floor)).to_a - end - - def required_y_tiles - left, bottom, right, top = bounding_box_in_tiles - Range.new(*[top, bottom].map(&:floor)).to_a - end - - def required_tiles - required_y_tiles.map do |y| - required_x_tiles.map{|x| Tile.new(x,y,zoom) } - end.flatten - end - - def map_tiles - @map_tiles ||= tile_source.get_tiles(required_tiles) - end - - def crop_to_size(image) - distance_from_left = (bounding_box_in_tiles[0] - required_x_tiles[0]) * TILE_SIZE - distance_from_top = (bounding_box_in_tiles[3] - required_y_tiles[0]) * TILE_SIZE - - image.crop "#{width}x#{height}+#{distance_from_left}+#{distance_from_top}" - end - - def create_uncropped_image - image = MiniMagick::Image.read(map_tiles[0]) + coordinates = @geojson["geometry"]["coordinates"] + geojson_bbox = BoundingBox.for(coordinates) + @viewport.center = {lat: geojson_bbox.center[:lat], lng: geojson_bbox.center[:lng]} - uncropped_width = required_x_tiles.length * TILE_SIZE - uncropped_height = required_y_tiles.length * TILE_SIZE - - image.combine_options do |c| - c.background 'none' - c.extent [uncropped_width,uncropped_height].join('x') + MAX_ZOOM.downto(MIN_ZOOM) do |zoom| + self.zoom = zoom + break if geojson_bbox.fits_in? @viewport end - - image end - def fill_image_with_tiles(image) - start = 0 - - required_y_tiles.length.times do |row| - length = required_x_tiles.length - - map_tiles.slice(start, length).each_with_index do |tile, column| - image = image.composite( MiniMagick::Image.read(tile) ) do |c| - c.geometry "+#{ (column) * TILE_SIZE }+#{ (row) * TILE_SIZE }" - end - end - - start += length - end - - image + def to_image + Renderer.new(self).render end + def to_file(filename) + Renderer.new(self).render_to(filename) + end + alias_method :render_map, :to_file end - - end diff --git a/lib/mapstatic/painter.rb b/lib/mapstatic/painter.rb new file mode 100644 index 0000000..894f473 --- /dev/null +++ b/lib/mapstatic/painter.rb @@ -0,0 +1,25 @@ +module Mapstatic + class Painter + attr_reader :feature + attr_reader :map + attr_accessor :stroke_width + attr_accessor :stroke_color + + # Implement this method in a subclass. + def self.accept?(geometry_type) + false + end + + def initialize(params={}) + @map = params.fetch(:map) + @feature = params.fetch(:feature) + @stroke_color = params.fetch(:stroke_color, "rgb(51, 136, 255)") + @stroke_width = params.fetch(:stroke_width, 4) + end + + # Implement this method in a subclass, have it return an ImageMagick Image. + def paint_to(image, viewport) + raise NotImplementedError + end + end +end diff --git a/lib/mapstatic/painter/line_string_painter.rb b/lib/mapstatic/painter/line_string_painter.rb new file mode 100644 index 0000000..0564a44 --- /dev/null +++ b/lib/mapstatic/painter/line_string_painter.rb @@ -0,0 +1,48 @@ +module Mapstatic + class Painter::LineStringPainter < Painter + def self.accept?(geometry_type) + geometry_type == "LineString" + end + + def paint_to(image, viewport) + # Convert coordinates to the corresponding pixel locations on + # image canvas. + # This is a two step process: + # 1. Convert latlng-coordinates to pixel-coordinates. + # 2. Convert pixel coordinates from Earth perspective to image perspective. + # All conversions require the zoom level we're working on, but the second step + # also requires a new reference, which is the image bounding box (calculated above). + # Also be careful when working on latlng-coordinates in array form - make sure + # which order they are in. + + coordinates = feature["geometry"]["coordinates"] + + xy_points = coordinates.map do |coordinate| + px = Conversion.x_to_px( + Conversion.lng_to_x(coordinate[0], map.zoom), + Conversion.lng_to_x(viewport.center[:lng], map.zoom), + viewport.width_at(map.zoom), + Map::TILE_SIZE + ) + + py = Conversion.y_to_px( + Conversion.lat_to_y(coordinate[1], map.zoom), + Conversion.lat_to_y(viewport.center[:lat], map.zoom), + viewport.height_at(map.zoom), + Map::TILE_SIZE + ) + + "#{px},#{py}" + end + + image.combine_options do |c| + c.fill "none" + c.stroke stroke_color + c.strokewidth stroke_width + c.draw "polyline #{xy_points.join(" ").strip}" + end + + image + end + end +end diff --git a/lib/mapstatic/painter/null_painter.rb b/lib/mapstatic/painter/null_painter.rb new file mode 100644 index 0000000..4f65b60 --- /dev/null +++ b/lib/mapstatic/painter/null_painter.rb @@ -0,0 +1,12 @@ +module Mapstatic + class Painter::NullPainter < Painter + def self.accept?(geometry_type) + true + end + + def paint_to(image, viewport) + # Do nothing, just return the original image. + image + end + end +end diff --git a/lib/mapstatic/renderer.rb b/lib/mapstatic/renderer.rb new file mode 100644 index 0000000..3063a4a --- /dev/null +++ b/lib/mapstatic/renderer.rb @@ -0,0 +1,110 @@ +module Mapstatic + class Renderer + def initialize(map) + @map = map + end + + def render + fetch_tiles + create_uncropped_image + fill_image_with_tiles + draw_geometry if @map.geojson + crop_to_size + @image + end + + def render_to(filename) + render.write filename + end + + private + + def fetch_tiles + @tiles = @map.tile_source.get_tiles(required_tiles) + end + + def required_tiles + required_y_tiles.map do |y| + required_x_tiles.map{|x| Tile.new(x, y, @map.zoom) } + end.flatten + end + + def required_x_tiles + left, bottom, right, top = @map.viewport.to_xy_coordinates(@map.zoom) + Range.new(*[left, right].map(&:floor).sort).to_a + end + + def required_y_tiles + left, bottom, right, top = @map.viewport.to_xy_coordinates(@map.zoom) + Range.new(*[bottom, top].map(&:floor).sort).to_a + end + + def create_uncropped_image + @image = MiniMagick::Image.read(@tiles[0]) + + uncropped_width = required_x_tiles.length * Map::TILE_SIZE + uncropped_height = required_y_tiles.length * Map::TILE_SIZE + + @image.combine_options do |c| + c.background 'none' + c.extent [uncropped_width, uncropped_height].join('x') + end + end + + def fill_image_with_tiles + start = 0 + + required_y_tiles.length.times do |row| + length = required_x_tiles.length + + @tiles.slice(start, length).each_with_index do |tile, column| + @image = @image.composite( MiniMagick::Image.read(tile) ) do |c| + c.geometry "+#{ (column) * Map::TILE_SIZE }+#{ (row) * Map::TILE_SIZE }" + end + end + + start += length + end + end + + def draw_geometry + if @map.geojson["type"] == "Feature" + features = [@map.geojson] + elsif @map.geojson["type"] == "FeatureCollection" + features = @map.geojson["features"] + end + + left = Conversion.x_to_lng(required_x_tiles.first, @map.zoom) + top = Conversion.y_to_lat(required_y_tiles.first, @map.zoom) + + # The +1s here are for getting the bottom right location for each tile - the tile + # number itself points to the top left corner. + right = Conversion.x_to_lng(required_x_tiles.last+1, @map.zoom) + bottom = Conversion.y_to_lat(required_y_tiles.last+1, @map.zoom) + + uncropped_viewport = BoundingBox.new left: left, bottom: bottom, right: right, top: top + + features&.each do |feature| + painter_for(feature).paint_to(@image, uncropped_viewport) + end + end + + def crop_to_size + distance_from_left = (@map.viewport.to_xy_coordinates(@map.zoom)[0] - required_x_tiles[0]) * Map::TILE_SIZE + distance_from_top = (@map.viewport.to_xy_coordinates(@map.zoom)[3] - required_y_tiles[0]) * Map::TILE_SIZE + + @image.crop "#{@map.width}x#{@map.height}+#{distance_from_left}+#{distance_from_top}" + end + + def painter_for(feature) + painter_class_for(feature["geometry"]["type"]).new(map: @map, feature: feature) + end + + def painter_class_for(feature_type) + # To add more painters, inherit a new class from Mapstatic::Painter, implement + # required methods, and add the class to this array. + painters = [Painter::LineStringPainter] + painter_class = painters.detect {|klass| klass.accept? feature_type} || Painter::NullPainter + end + end +end diff --git a/lib/mapstatic/version.rb b/lib/mapstatic/version.rb index c2a49b6..0e87a4d 100644 --- a/lib/mapstatic/version.rb +++ b/lib/mapstatic/version.rb @@ -1,3 +1,3 @@ module Mapstatic - VERSION = '0.0.2' + VERSION = '0.1' end diff --git a/mapstatic.gemspec b/mapstatic.gemspec index a807a68..fd269ad 100644 --- a/mapstatic.gemspec +++ b/mapstatic.gemspec @@ -1,18 +1,19 @@ # -*- encoding: utf-8 -*- -# stub: mapstatic 0.0.2 ruby lib +# stub: mapstatic 0.1 ruby lib Gem::Specification.new do |s| s.name = "mapstatic".freeze - s.version = "0.0.2" + s.version = "0.1" s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version= s.require_paths = ["lib".freeze] - s.authors = ["James Croft".freeze] - s.date = "2019-07-01" + s.authors = ["James Croft".freeze, "Mika Haulo".freeze, "Tim Neems".freeze, "Olli Huotari".freeze, "Michael O'Toole".freeze] + s.date = "2019-09-28" s.email = "james@matchingnotes.com".freeze s.executables = ["mapstatic".freeze] - s.files = ["Gemfile".freeze, "Gemfile.lock".freeze, "bin/mapstatic".freeze, "lib/mapstatic".freeze, "lib/mapstatic.rb".freeze, "lib/mapstatic/cli.rb".freeze, "lib/mapstatic/conversion.rb".freeze, "lib/mapstatic/errors.rb".freeze, "lib/mapstatic/map.rb".freeze, "lib/mapstatic/tile.rb".freeze, "lib/mapstatic/tile_source.rb".freeze, "lib/mapstatic/version.rb".freeze, "spec/fixtures".freeze, "spec/fixtures/maps".freeze, "spec/fixtures/maps/london.png".freeze, "spec/fixtures/maps/thames.png".freeze, "spec/fixtures/vcr_cassettes".freeze, "spec/fixtures/vcr_cassettes/osm-london-fail.yml".freeze, "spec/fixtures/vcr_cassettes/osm-london.yml".freeze, "spec/fixtures/vcr_cassettes/osm-thames.yml".freeze, "spec/models".freeze, "spec/models/map_spec.rb".freeze, "spec/spec_helper.rb".freeze] + s.files = ["Gemfile".freeze, "Gemfile.lock".freeze, "bin/mapstatic".freeze, "lib/mapstatic".freeze, "lib/mapstatic.rb".freeze, "lib/mapstatic/bounding_box.rb".freeze, "lib/mapstatic/cli.rb".freeze, "lib/mapstatic/conversion.rb".freeze, "lib/mapstatic/errors.rb".freeze, "lib/mapstatic/gpx_file.rb".freeze, "lib/mapstatic/map.rb".freeze, "lib/mapstatic/painter".freeze, "lib/mapstatic/painter.rb".freeze, "lib/mapstatic/painter/line_string_painter.rb".freeze, "lib/mapstatic/painter/null_painter.rb".freeze, "lib/mapstatic/renderer.rb".freeze, "lib/mapstatic/tile.rb".freeze, "lib/mapstatic/tile_source.rb".freeze, "lib/mapstatic/version.rb".freeze, "spec/fixtures".freeze, "spec/fixtures/gpx".freeze, "spec/fixtures/gpx/hervanta.gpx".freeze, "spec/fixtures/gpx/joensuu.gpx".freeze, "spec/fixtures/maps".freeze, "spec/fixtures/maps/london.png".freeze, "spec/fixtures/maps/thames.png".freeze, "spec/fixtures/vcr_cassettes".freeze, "spec/fixtures/vcr_cassettes/osm-london-fail.yml".freeze, "spec/fixtures/vcr_cassettes/osm-london.yml".freeze, "spec/fixtures/vcr_cassettes/osm-thames.yml".freeze, "spec/models".freeze, "spec/models/bounding_box_spec.rb".freeze, "spec/models/gpx_file_spec.rb".freeze, "spec/models/line_string_painter_spec.rb".freeze, "spec/models/map_spec.rb".freeze, "spec/models/null_painter_spec.rb".freeze, "spec/models/painter_spec.rb".freeze, "spec/spec_helper.rb".freeze] s.homepage = "https://github.com/crofty/mapstatic".freeze + s.licenses = ["MIT".freeze] s.rubygems_version = "3.0.3".freeze s.summary = "Static Map Generator".freeze @@ -22,30 +23,33 @@ Gem::Specification.new do |s| if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then s.add_runtime_dependency(%q.freeze, ["~> 4.9"]) s.add_runtime_dependency(%q.freeze, ["~> 1.3"]) - s.add_development_dependency(%q.freeze, [">= 0.19.0", "< 2.0"]) - s.add_development_dependency(%q.freeze, ["< 2.0"]) - s.add_development_dependency(%q.freeze, [">= 0"]) + s.add_runtime_dependency(%q.freeze, ["~> 1.10"]) + s.add_development_dependency(%q.freeze, ["~> 12"]) s.add_development_dependency(%q.freeze, ["~> 3"]) s.add_development_dependency(%q.freeze, ["~> 3"]) s.add_development_dependency(%q.freeze, ["~> 2"]) + s.add_development_dependency(%q.freeze, [">= 0.19.0", "< 2.0"]) + s.add_development_dependency(%q.freeze, ["< 2.0"]) else s.add_dependency(%q.freeze, ["~> 4.9"]) s.add_dependency(%q.freeze, ["~> 1.3"]) - s.add_dependency(%q.freeze, [">= 0.19.0", "< 2.0"]) - s.add_dependency(%q.freeze, ["< 2.0"]) - s.add_dependency(%q.freeze, [">= 0"]) + s.add_dependency(%q.freeze, ["~> 1.10"]) + s.add_dependency(%q.freeze, ["~> 12"]) s.add_dependency(%q.freeze, ["~> 3"]) s.add_dependency(%q.freeze, ["~> 3"]) s.add_dependency(%q.freeze, ["~> 2"]) + s.add_dependency(%q.freeze, [">= 0.19.0", "< 2.0"]) + s.add_dependency(%q.freeze, ["< 2.0"]) end else s.add_dependency(%q.freeze, ["~> 4.9"]) s.add_dependency(%q.freeze, ["~> 1.3"]) - s.add_dependency(%q.freeze, [">= 0.19.0", "< 2.0"]) - s.add_dependency(%q.freeze, ["< 2.0"]) - s.add_dependency(%q.freeze, [">= 0"]) + s.add_dependency(%q.freeze, ["~> 1.10"]) + s.add_dependency(%q.freeze, ["~> 12"]) s.add_dependency(%q.freeze, ["~> 3"]) s.add_dependency(%q.freeze, ["~> 3"]) s.add_dependency(%q.freeze, ["~> 2"]) + s.add_dependency(%q.freeze, [">= 0.19.0", "< 2.0"]) + s.add_dependency(%q.freeze, ["< 2.0"]) end end diff --git a/spec/fixtures/gpx/hervanta.gpx b/spec/fixtures/gpx/hervanta.gpx new file mode 100644 index 0000000..2bc61b7 --- /dev/null +++ b/spec/fixtures/gpx/hervanta.gpx @@ -0,0 +1,225 @@ + + + + Hervanta + + + + Hervanta + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spec/fixtures/gpx/joensuu.gpx b/spec/fixtures/gpx/joensuu.gpx new file mode 100644 index 0000000..8e1bc14 --- /dev/null +++ b/spec/fixtures/gpx/joensuu.gpx @@ -0,0 +1,300 @@ + + + + Joensuu + + + + Joensuu + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spec/models/bounding_box_spec.rb b/spec/models/bounding_box_spec.rb new file mode 100644 index 0000000..4fc0fe7 --- /dev/null +++ b/spec/models/bounding_box_spec.rb @@ -0,0 +1,82 @@ +require 'spec_helper' + +describe Mapstatic::BoundingBox do + it "should require initial values" do + expect do + Mapstatic::BoundingBox.new + end.to raise_error KeyError + end + + it "should return its data as arrays" do + left = -0.169851 + bottom = 51.480829 + right = 0.027421 + top = 51.513658 + + bbox = Mapstatic::BoundingBox.new left: left, bottom: bottom, right: right, top: top + + expect(bbox.to_latlng_coordinates.is_a?(Array)).to be(true) + + zoom = 12 + expect(bbox.to_xy_coordinates(zoom).is_a?(Array)).to be(true) + end + + it "should calculate centerpoint correctly" do + left = -100.0 + bottom = -50.0 + right = 100.0 + top = 50.0 + + bbox = Mapstatic::BoundingBox.new left: left, bottom: bottom, right: right, top: top + center = bbox.center + + expect(center[:lat]).to eq(0) + expect(center[:lng]).to eq(0) + end + + it "should detect nested boxes correctly" do + outer = {left: -100.0, bottom: -50.0, right: 100.0, top: 50.0} + inner = {left: -90.0, bottom: -40.0, right: 90.0, top: 40.0} + intersecting = {left: -90.0, bottom: -60.0, right: 110.0, top: 50.0} + + outer_box = Mapstatic::BoundingBox.new outer + inner_box = Mapstatic::BoundingBox.new inner + intersecting_box = Mapstatic::BoundingBox.new intersecting + + expect(inner_box.fits_in? outer_box).to be(true) + expect(outer_box.contains? inner_box).to be(true) + expect(inner_box.contains? outer_box).to be(false) + expect(outer_box.fits_in? inner_box).to be(false) + expect(intersecting_box.fits_in? outer_box).to be(false) + end + + it "should build a box around given coordinates" do + coordinates = [[0.0, 0.0], [10.0, 10.0]] + bbox = Mapstatic::BoundingBox.for coordinates + + expect(bbox.left).to be <= 0 + expect(bbox.bottom).to be <= 0 + expect(bbox.right).to be >= 10 + expect(bbox.top).to be >= 10 + end + + it "should construct a box based on center coordinates and dimensions" do + outer_box = Mapstatic::BoundingBox.from( + center_lat: 0, + center_lng: 0, + width: 200, + height: 200, + zoom: 12 + ) + + inner_box = Mapstatic::BoundingBox.from( + center_lat: 0, + center_lng: 0, + width: 150, + height: 150, + zoom: 12 + ) + + expect(inner_box.fits_in? outer_box).to be(true) + end +end diff --git a/spec/models/gpx_file_spec.rb b/spec/models/gpx_file_spec.rb new file mode 100644 index 0000000..901cc56 --- /dev/null +++ b/spec/models/gpx_file_spec.rb @@ -0,0 +1,36 @@ +require 'spec_helper' + +describe Mapstatic::GpxFile do + it "should require filename" do + expect do + Mapstatic::GpxFile.new + end.to raise_error ArgumentError + end + + it "should parse gpx file correctly" do + gpx = Mapstatic::GpxFile.new "spec/fixtures/gpx/joensuu.gpx" + expect(gpx.tracks.length).to eq 1 + expect(gpx.routes.length).to eq 0 + expect(gpx.tracks.first.length).to eq 288 + end + + it "should output proper geojson data" do + gpx = Mapstatic::GpxFile.new "spec/fixtures/gpx/joensuu.gpx" + output = gpx.geojson_data + + expect(output.is_a? Hash).to be true + expect(output[:type]).to eq "FeatureCollection" + expect(output[:features].is_a? Array).to be true + expect(output[:features].count).to eq 1 + + feature = output[:features].first + + expect(feature[:type]).to eq "Feature" + expect(feature[:geometry][:type]).to eq "LineString" + expect(feature[:geometry][:coordinates].is_a? Array).to be true + + expect do + JSON.parse gpx.to_geojson + end.not_to raise_error + end +end diff --git a/spec/models/line_string_painter_spec.rb b/spec/models/line_string_painter_spec.rb new file mode 100644 index 0000000..2aa0212 --- /dev/null +++ b/spec/models/line_string_painter_spec.rb @@ -0,0 +1,38 @@ +require 'spec_helper' + +describe Mapstatic::Painter::LineStringPainter do + it "should only accept LineString geometry type" do + expect(Mapstatic::Painter::LineStringPainter.accept? "LineString").to be(true) + expect(Mapstatic::Painter::LineStringPainter.accept? "Point").to be(false) + expect(Mapstatic::Painter::LineStringPainter.accept? "Polygon").to be(false) + expect(Mapstatic::Painter::LineStringPainter.accept? "MultiPoint").to be(false) + expect(Mapstatic::Painter::LineStringPainter.accept? "MultiLineString").to be(false) + expect(Mapstatic::Painter::LineStringPainter.accept? "MultiPolygon").to be(false) + end + + it "should draw without errors" do + test_file = tempfile + FileUtils.cp "spec/fixtures/maps/london.png", test_file + image = MiniMagick::Image.new test_file + image.resize "256x256" + + map = Mapstatic::Map.new( + lat: 51.515579783755925, + lng: -0.1373291015625, + zoom: 11, + width: 256, + height: 256, + ) + feature = line_string.to_json + map.geojson = feature + map.fit_bounds + + painter = Mapstatic::Painter::LineStringPainter.new(map: map, feature: JSON.parse(feature)) + painter.paint_to image, map.viewport + + expect(image.type).to eq("PNG") + + File.delete test_file + end + +end diff --git a/spec/models/map_spec.rb b/spec/models/map_spec.rb index 3b4614b..c89259b 100644 --- a/spec/models/map_spec.rb +++ b/spec/models/map_spec.rb @@ -1,17 +1,85 @@ require 'spec_helper' describe Mapstatic::Map do + it "returns correct width and height" do + map = Mapstatic::Map.new( + lat: 51.515579783755925, + lng: -0.1373291015625, + zoom: 12, + width: 256, + height: 256, + ) + + expect(map.width).to eql(256) + expect(map.height).to eql(256) + end + + it "doesn't crash when trying to fit bounds with no geojson data provided" do + map = Mapstatic::Map.new( + lat: 51.515579783755925, + lng: -0.1373291015625, + zoom: 12, + width: 256, + height: 256, + ) + + expect do + map.fit_bounds + end.not_to raise_error + end + + it "should store width and height as integers" do + map = Mapstatic::Map.new( + lat: 51.515579783755925, + lng: -0.1373291015625, + zoom: 12, + width: "256", # Notice that width and height are given as strings + height: "256", + ) + + expect(map.width.is_a? Integer).to be true + expect(map.height.is_a? Integer).to be true + end + + it "should be able to take geojson data as a Hash" do + map = Mapstatic::Map.new( + lat: 51.515579783755925, + lng: -0.1373291015625, + zoom: 12, + width: 256, + height: 256, + ) + + expect do + map.geojson = line_string + map.fit_bounds + end.not_to raise_error + end + + it "should be able to take geojson data as a GeoJSON string" do + map = Mapstatic::Map.new( + lat: 51.515579783755925, + lng: -0.1373291015625, + zoom: 12, + width: 256, + height: 256, + ) + + expect do + map.geojson = line_string.to_json + map.fit_bounds + end.not_to raise_error + end describe "the resulting image" do it "is the correct image when got via lat lng" do output_path = 'london.png' map = Mapstatic::Map.new( - :lat => 51.515579783755925, - :lng => -0.1373291015625, - :zoom => 11, - :width => 256, - :height => 256, - :provider => 'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png' + lat: 51.515579783755925, + lng: -0.1373291015625, + zoom: 11, + width: 256, + height: 256, ) VCR.use_cassette('osm-london') do map.render_map output_path @@ -23,9 +91,8 @@ it "is the correct image when got via bounding box" do output_path = 'london.png' map = Mapstatic::Map.new( - :bbox => "-0.2252197265625,51.4608524464555,-0.0494384765625,51.570241445811234", - :zoom => 11, - :provider => 'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png' + bbox: [-0.2252197265625,51.4608524464555,-0.0494384765625,51.570241445811234], + zoom: 11, ) VCR.use_cassette('osm-london') do map.render_map output_path @@ -34,13 +101,11 @@ File.delete output_path end - it "renders the correct image" do output_path = 'thames.png' map = Mapstatic::Map.new( - :provider => 'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', - :zoom => 12, - :bbox => '-0.169851,51.480829,0.027421,51.513658' + bbox: [-0.169851,51.480829,0.027421,51.513658], + zoom: 12, ) VCR.use_cassette('osm-thames') do map.render_map output_path @@ -48,10 +113,6 @@ images_are_identical(output_path, 'spec/fixtures/maps/thames.png') File.delete output_path end - - def images_are_identical(image1, image2) - `compare -metric MAE #{image1} #{image2} null: 2>&1`.chomp.should == "0 (0)" - end end describe '#render_map' do @@ -59,9 +120,8 @@ def images_are_identical(image1, image2) it 'raises TileRequestError' do output_path = 'london.png' map = Mapstatic::Map.new( - :bbox => '-0.2252197265625,51.4608524464555,-0.0494384765625,51.570241445811234', - :zoom => 11, - :provider => 'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png' + bbox: [-0.2252197265625,51.4608524464555,-0.0494384765625,51.570241445811234], + zoom: 11, ) expect do @@ -79,13 +139,19 @@ def images_are_identical(image1, image2) context "when calculated from the bounding box" do it "doubles with each zoom level" do - bbox = '-11.29,49.78,2.45,59.71' + bbox = [-11.29,49.78,2.45,59.71] - image = Mapstatic::Map.new( :zoom => 6, :bbox => bbox) - expect( image.width.to_i ).to eql( 625 ) + map1 = Mapstatic::Map.new( + bbox: bbox, + zoom: 6, + ) + expect( map1.width.to_i ).to eql( 625 ) - image = Mapstatic::Map.new( :zoom => 7, :bbox => bbox) - expect( image.width.to_i ).to eql( 1250 ) + map2 = Mapstatic::Map.new( + bbox: bbox, + zoom: map1.zoom + 1, + ) + expect( map2.width.to_i ).to eql( map1.width.to_i * 2 ) end end @@ -96,13 +162,18 @@ def images_are_identical(image1, image2) context "when calculated from the bounding box" do it "doubles with each zoom level" do - bbox = '-11.29,49.78,2.45,59.71' - - image = Mapstatic::Map.new( :zoom => 2, :bbox => bbox) - expect( image.height.to_i ).to eql( 49 ) + bbox = [-11.29, 49.78, 2.45, 59.71] + map1 = Mapstatic::Map.new( + bbox: bbox, + zoom: 2, + ) + expect( map1.height.to_i ).to eql( 49 ) - image = Mapstatic::Map.new( :zoom => 3, :bbox => bbox) - expect( image.height.to_i ).to eql( 98 ) + map2 = Mapstatic::Map.new( + bbox: bbox, + zoom: 3, + ) + expect( map2.height.to_i ).to eql( map1.height.to_i * 2 ) end end diff --git a/spec/models/null_painter_spec.rb b/spec/models/null_painter_spec.rb new file mode 100644 index 0000000..3d8f672 --- /dev/null +++ b/spec/models/null_painter_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper' + +describe Mapstatic::Painter::NullPainter do + it "should accept any geometry type, even garbage" do + expect(Mapstatic::Painter::NullPainter.accept? "LineString").to be(true) + expect(Mapstatic::Painter::NullPainter.accept? "foo").to be(true) + end + + it "should draw without errors" do + test_file = tempfile + FileUtils.cp "spec/fixtures/maps/london.png", test_file + image = MiniMagick::Image.new test_file + image.resize "256x256" + + map = Mapstatic::Map.new( + lat: 51.515579783755925, + lng: -0.1373291015625, + zoom: 11, + width: 256, + height: 256, + ) + feature = line_string + map.geojson = feature + map.fit_bounds + + painter = Mapstatic::Painter::NullPainter.new(map: map, feature: feature) + painter.paint_to image, map.viewport + + expect(image.type).to eq("PNG") + end + +end diff --git a/spec/models/painter_spec.rb b/spec/models/painter_spec.rb new file mode 100644 index 0000000..8344f0f --- /dev/null +++ b/spec/models/painter_spec.rb @@ -0,0 +1,7 @@ +require 'spec_helper' + +describe Mapstatic::Painter do + it "should not accept any geometry type" do + expect(Mapstatic::Painter.accept? "LineString").to be(false) + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index b13b2fb..7ad88de 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,7 +1,38 @@ require 'mapstatic' +require 'mapstatic/gpx_file' require 'vcr' +require 'json' VCR.configure do |c| c.cassette_library_dir = 'spec/fixtures/vcr_cassettes' c.hook_into :webmock end + +def images_are_identical(image1, image2) + expect(`compare -metric MAE #{image1} #{image2} null: 2>&1`.chomp).to eq("0 (0)") +end + +def line_string + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [[-0.3481, 51.5283], [0,2208, 51,4462]] + } + } +end + +def geojson_data + { + type: "FeatureCollection", + features: [line_string] + } +end + +def tempfile + file = Tempfile.new ["mapstatic", ".png"] + filename = file.path + file.close + file.unlink + filename +end