honua 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 9dcc6198120b25c09baeb8619f31cf9069d60c7b0dd7926e45aaff7144273302
4
+ data.tar.gz: c7c86a44e9e0e26384adc9b77a79f01904dc88684f076d4abd58916603fe70b9
5
+ SHA512:
6
+ metadata.gz: 942643724880d18c28337d3ea140f9c2baebfd787e1b411f2de2e8e550d1d29a4c590b090a8da6901b4f007e2539a02f612c55d855fac30fdfd4071595989a46
7
+ data.tar.gz: 41fdf1137a3e5208fe0529a8c9b8b350fc47f9574a5b90644b0dee015a70e2a4973e8915046a06619ec19954b1e7bdc493b1783f69a5210c6f02e6019a83f0f6
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2021 dingsdax
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,184 @@
1
+ # Honua
2
+
3
+ ![](docs/header.png)
4
+
5
+ > Honua is Hawaiian for earth 🌍
6
+
7
+ Honua is a simple geographic mapping Library for Ruby. It allows you to stitch geographic map images based on map tiles, provided by a rastered tile server (in Mercator projection). It was highly inspired by [ModestMaps](https://github.com/modestmaps/).
8
+
9
+ ## Requirements
10
+
11
+ 1. [Ruby 3.0](https://www.ruby-lang.org)
12
+ 2. [libvips](https://libvips.github.io/libvips/)
13
+
14
+ ## Setup
15
+
16
+ To install, run:
17
+ ``` ruby
18
+ gem install honua
19
+ ```
20
+
21
+ Add the following to your Gemfile:
22
+
23
+ ``` ruby
24
+ gem "honua"
25
+ ```
26
+
27
+ ## Usage
28
+
29
+ ### Coordinate Systems
30
+
31
+ Like almost every popular mapping library it uses the [Web Mercator Projection](https://en.wikipedia.org/wiki/Web_Mercator_projection) as basis for conversions and its tile coordinate system.
32
+
33
+ * A [Location](lib/honua/location.rb) is a geographic place described in latitude and longitude coordinates in degrees.
34
+ * A [Coordinate](lib/honua/coordinate.rb) is a geographic place described in a tile coordinate system with columns (x), rows (y) and a zoom level.
35
+
36
+ To convert from `Location` to `Coordinate` at a certain zoom level:
37
+
38
+ 1. convert latitude and longitude to radians
39
+ 2. [reproject coordinates](https://epsg.io/transform#s_srs=4326&t_srs=3857) to Web Mercator projections
40
+ 3. transform range of `column` and `row` to 0 - 1 and shift origin to top left corner
41
+ 4. multiply `row` and `column` with number of tiles (2**zoom) and round results
42
+
43
+ ### Configuration
44
+
45
+ At minimal you need to provide the url for a [tile server](https://github.com/Overv/openstreetmap-tile-server) and the appropriate attribution text for the used tiles. If the map is too small to show the attribution it will be skipped and you need to provide it separetly. Don't forget to check out terms and service for the tile server you are planning to use.
46
+
47
+ ``` ruby
48
+ Honua.configure do |config|
49
+ # each zoom level is a directory, each column is a subdirectory, and each tile in that column is a file
50
+ config.tiles_url = 'http://localhost:8080/%<zoom>s/%<column>s/%<row>s.png'
51
+ config.attribution_text = '<b>© OpenStreetMap contributors</b>'
52
+ end
53
+ ```
54
+
55
+ Checkout [configuration.rb](lib/honua/configuration.rb) for further details.
56
+
57
+ A map is defined by a center location, map dimensions (width and height in pixels) and a [zoom level](https://wiki.openstreetmap.org/wiki/Zoom_levels). The `draw` method downloads all necessary tiles for the map, stitches them together, adds the proper attribution at the bottom and finally returns a [`Vips::Image`](https://www.rubydoc.info/gems/ruby-vips/Vips/Image).
58
+
59
+ ``` ruby
60
+ map_center = Honua::Location.new(35.689487, 139.691711) # Tokyo
61
+ map = Honua::Map.new(center: map_center, width: 600, height: 600, zoom: 12)
62
+ map_image = map.draw
63
+ map_image.write_to_file('tokyo.png') # use Vips to write to file
64
+ ```
65
+
66
+ ### Helpers
67
+
68
+ There might be use cases, where you have a bunch of location instead of map center, or you either have map dimensions or a zoom level but not both. Honua includes helpers that can be used to calculate the needed map input parameters:
69
+
70
+ * [`Honua::Helpers.map_span(locations:)`](lib/honua/helpers.rb) returns the top left and bottom right coordinates at zoom level 0
71
+ * [`Honua::Helpers.map_span(top_left:, bottom_right:)`](lib/honua/helpers.rb) returns the center coordinate based on map spanning top left and bottom right coordinates
72
+ * [`Honua::Helpers.calculate_zoom(top_left:, bottom_right:, width:, height:)`](lib/honua/helpers.rb) returns zoom level based on map spanning coordinates and map dimensions
73
+ * [`Honua::Helpers.calculate_map_dimensions(top_left:, bottom_right:, zoom:)`](lib/honua/helpers.rb) returns map dimensions (in pixels) based on map spanning coordinates and a zoom value
74
+ * [`Honua::Helpers.text_label((text:, dpi:, text_colour:, shadow_colour:))`](lib/honua/helpers.rb) generates a text label image with shadow and shadow offset, which can be places on the map.
75
+ * [`Honua::Helpers.hex2rgb(hex)`](lib/honua/helpers.rb) converts hex color strings to an array of RGB
76
+
77
+ Please refer to the [examples](docs) for how these helpers might be used.
78
+
79
+ [🏙 World & Cities](docs/world_calc_zoom.rb)
80
+
81
+ ![](docs/the_world.png)
82
+
83
+
84
+ [🌉 From SF to LA](docs/from_sf_to_la.rb)
85
+
86
+ ![](docs/california.png)
87
+
88
+
89
+ [🇨🇦 Maps of Vancouer](docs/vancouver_maps.rb)
90
+
91
+ ![](docs/vancouver_500x500z11.png)
92
+
93
+ ## Development
94
+
95
+ To contribute, run:
96
+
97
+ git clone https://github.com//honua.git
98
+ cd honua
99
+ bundle install
100
+
101
+ You can also use the IRB console for direct access to all objects:
102
+ ``` shell
103
+ bin/console
104
+ ```
105
+
106
+ ### Tile Server
107
+
108
+ If you want to use the included [tile server](https://github.com/Overv/openstreetmap-tile-server) be sure to install [Docker](https://docs.docker.com/engine/install/) and [Docker Compose](https://docs.docker.com/compose/install/).
109
+
110
+ After you downloaded an `.osm.pbf` extract and `.poly` file from e.g [Geofabrik](https://download.geofabrik.de/) for the region that you're interested in, replace the placeholders in `docker-compose.yml` and run the importer with:
111
+ ``` shell
112
+ docker compose up osmimport
113
+
114
+ ```
115
+
116
+ To start the tile server, run:
117
+ ``` shell
118
+ docker compose up osmtileserver
119
+
120
+ ```
121
+
122
+ You can now go to `localhost:8080` for a fullscreen [Leaflet](https://leafletjs.com/) based map. To use the tile server in Honua, add the following configuration in your code:
123
+ ``` ruby
124
+ Honua.configure do |config|
125
+ config.tiles_url = 'http://localhost:8080/tile/%<zoom>s/%<column>s/%<row>s.png'
126
+ config.attribution_text = '<b>© OpenStreetMap contributors</b>' # don't forget to attribute 👍
127
+ end
128
+ ```
129
+
130
+
131
+ 😏 If you want to merge 2 or more extracts you can use [osmctools](https://gitlab.com/osm-c-tools/osmctools) or [osmctools-docker](https://github.com/tobilg/osmctools-docker).
132
+
133
+ ## Tests
134
+
135
+ To test, run:
136
+ ``` shell
137
+ bundle exec rake
138
+ ```
139
+
140
+ ## Versioning
141
+
142
+ Read [Semantic Versioning](https://semver.org) for details. Briefly, it means:
143
+
144
+ - Major (X.y.z) - Incremented for any backwards incompatible public API changes.
145
+ - Minor (x.Y.z) - Incremented for new, backwards compatible, public API enhancements/fixes.
146
+ - Patch (x.y.Z) - Incremented for small, backwards compatible, bug fixes.
147
+
148
+ ## Contributions
149
+
150
+ Read [CONTRIBUTING](CONTRIBUTING.md) for details.
151
+
152
+ ## License
153
+
154
+ Copyright 2021 by dingsdax.
155
+ Read [LICENSE](LICENSE) for details.
156
+
157
+ ## History
158
+
159
+ Read [CHANGES](CHANGES.md) for details.
160
+ Built with [Gemsmith](https://www.alchemists.io/projects/gemsmith).
161
+
162
+ ## Further Reading & Credits
163
+
164
+ Some of the resources that helped building and understanding the underlying logic behind this gem.
165
+ This started out as a Ruby port of [modest-maps.js](https://github.com/modestmaps/modestmaps-js) but took on a life of its own. [ModestMaps](https://github.com/modestmaps/) and [Stamen](https://stamen.com/) are awesome! Big shoutout for their work and inspiration 🙇‍♂️.
166
+
167
+ * [Bing Maps Tile System](https://docs.microsoft.com/en-us/bingmaps/articles/bing-maps-tile-system)
168
+ * [OpenLayers FAQ](https://openlayers.org/en/latest/doc/faq.html)
169
+ * [OSM Slippy map tilenames](https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames)
170
+ * [ModestMaps Wiki](https://github.com/modestmaps/modestmaps-js/wiki)
171
+ * [Mercator Projection](https://en.wikipedia.org/wiki/Mercator_projection)
172
+ * [Tiles à la Google Maps](https://www.maptiler.com/google-maps-coordinates-tile-bounds-projection)
173
+ * [Coordinate Systems Worldwide](https://epsg.io)
174
+
175
+ ## Attributions
176
+
177
+ <div>Logo & marker icons made by <a href="https://www.freepik.com" title="Freepik">Freepik</a> from <a href="https://www.flaticon.com/" title="Flaticon">www.flaticon.com</a></div>
178
+
179
+ Toner & Terrain map tiles by Stamen Design (http://stamen.com),
180
+ under CC BY 3.0 (http://creativecommons.org/licenses/by/3.0).
181
+ Data by OpenStreetMap (http://openstreetmap.org), under ODbL (http://www.openstreetmap.org/copyright).
182
+
183
+ [Explorer Base Map header image](https://visibleearth.nasa.gov/images/147190/explorer-base-map/147193)
184
+ NASA Earth Observatory map by Joshua Stevens using data from NASA’s MODIS Land Cover, the Shuttle Radar Topography Mission (SRTM), the General Bathymetric Chart of the Oceans (GEBCO), and Natural Earth boundaries.
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'zeitwerk'
4
+ loader = Zeitwerk::Loader.for_gem
5
+ loader.setup
6
+
7
+ module Honua
8
+ Point = Struct.new(:x, :y)
9
+
10
+ class << self
11
+ def configuration
12
+ @configuration ||= Configuration.new
13
+ end
14
+
15
+ def configure
16
+ yield configuration
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Honua
4
+ class Configuration
5
+ attr_accessor :attribution_bgcolor, :attribution_fgcolor, :attribution_text,
6
+ :max_fetch_attempts, :tile_height, :tile_width,
7
+ :tiles_url, :user_agent
8
+
9
+ def initialize
10
+ # max attempts to fetch a tile until given up and returning an empty tile
11
+ @max_fetch_attempts = 3
12
+
13
+ # OSM map tiles are typically 256x256
14
+ # https://wiki.openstreetmap.org/wiki/Tiles
15
+ @tile_height = 256
16
+ @tile_width = 256
17
+
18
+ # user agent that's used to make requests to the tile server
19
+ @user_agent = Honua::Identity::VERSION_LABEL
20
+
21
+ # attribution_text can contain some Pango markup
22
+ # https://developer.gnome.org/pango/stable/pango-Markup.html
23
+ @attribution_text = nil
24
+ @attribution_fgcolor = '#fff'
25
+ @attribution_bgcolor = '#000'
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ # a coordinate is used to indentify a tile based on the zoom level and the tile grid's x/y coordinates
4
+ module Honua
5
+ class Coordinate
6
+ attr_accessor :row, :column, :zoom
7
+
8
+ def initialize(row, column, zoom = 0)
9
+ @row = row
10
+ @column = column
11
+ @zoom = zoom
12
+ end
13
+
14
+ def zoom_to(zoom_level)
15
+ Coordinate.new(
16
+ row * (2**(zoom_level - zoom)),
17
+ column * (2**(zoom_level - zoom)),
18
+ zoom_level
19
+ )
20
+ end
21
+
22
+ # the top left most coordinate within the same tile
23
+ # used to identify the tile within the the tile rid
24
+ def container
25
+ Coordinate.new(row.to_i, column.to_i, zoom)
26
+ end
27
+
28
+ def up(distance = 1)
29
+ Coordinate.new(row - distance, column, zoom)
30
+ end
31
+
32
+ def right(distance = 1)
33
+ Coordinate.new(row, column + distance, zoom)
34
+ end
35
+
36
+ def down(distance = 1)
37
+ Coordinate.new(row + distance, column, zoom)
38
+ end
39
+
40
+ def left(distance = 1)
41
+ Coordinate.new(row, column - distance, zoom)
42
+ end
43
+
44
+ def to_location
45
+ n = 2.0**zoom
46
+ lon = column / n * 360.0 - 180
47
+ lat_rad = Math.atan(Math.sinh(Math::PI * (1 - 2 * row / n)))
48
+ lat = rad2deg(lat_rad)
49
+
50
+ Location.new(lat, lon)
51
+ end
52
+
53
+ def ==(other)
54
+ row == other.row &&
55
+ column == other.column &&
56
+ zoom == other.zoom
57
+ end
58
+
59
+ private
60
+
61
+ # radians to degrees
62
+ def rad2deg(radians)
63
+ radians * (180 / Math::PI)
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Honua
4
+ class Helpers
5
+ class << self
6
+ # returns the top left and bottom right coordinates at zoom level 0
7
+ # these corners are used to define the map span
8
+ def map_span(locations:)
9
+ coordinates = locations.map(&:to_coordinate)
10
+
11
+ top_left = Coordinate.new(
12
+ coordinates.min_by(&:row).row,
13
+ coordinates.min_by(&:column).column,
14
+ coordinates.min_by(&:zoom).zoom
15
+ )
16
+
17
+ bottom_right = Coordinate.new(
18
+ coordinates.max_by(&:row).row,
19
+ coordinates.max_by(&:column).column,
20
+ coordinates.max_by(&:zoom).zoom
21
+ )
22
+
23
+ [top_left, bottom_right]
24
+ end
25
+
26
+ # returns the center coordinate based on map spanning top left and bottom right coordinates
27
+ def map_center_coordinate(top_left:, bottom_right:)
28
+ row = (top_left.row + bottom_right.row) / 2.0
29
+ column = (top_left.column + bottom_right.column) / 2.0
30
+ zoom = (top_left.zoom + bottom_right.zoom) / 2.0
31
+
32
+ Coordinate.new(row, column, zoom)
33
+ end
34
+
35
+ # returns zoom level based on map spanning coordinates and map dimensions
36
+ # shameless copy from modestmaps.js
37
+ def calculate_zoom(top_left:, bottom_right:, width:, height:)
38
+ # multiplication factor between horizontal span and map width
39
+ h_factor = (bottom_right.column - top_left.column) / (width.to_f / Honua.configuration.tile_width)
40
+
41
+ # possible horizontal zoom to fit geographical extent in map width
42
+ h_possible_zoom = top_left.zoom - (Math.log(h_factor) / Math.log(2)).ceil
43
+
44
+ # multiplication factor between vertical span and map height
45
+ v_factor = (bottom_right.row - top_left.row) / (height.to_f / Honua.configuration.tile_height)
46
+
47
+ # possible vertical zoom to fit geographical extent in map height
48
+ v_possible_zoom = top_left.zoom - (Math.log(v_factor) / Math.log(2)).ceil
49
+
50
+ # initial zoom to fit extent vertically and horizontally
51
+ [h_possible_zoom, v_possible_zoom].min
52
+ end
53
+
54
+ # returns map dimensions (in pixels) based on map spanning coordinates and a zoom value
55
+ def calculate_map_dimensions(top_left:, bottom_right:, zoom:)
56
+ top_left = top_left.zoom_to(zoom)
57
+ bottom_right = bottom_right.zoom_to(zoom)
58
+
59
+ # map width and height in pixels
60
+ width = ((bottom_right.column - top_left.column) * Honua.configuration.tile_width).to_i
61
+ height = ((bottom_right.row - top_left.row) * Honua.configuration.tile_height).to_i
62
+
63
+ [width, height]
64
+ end
65
+
66
+ # text can contain some Pango markup
67
+ # https://developer.gnome.org/pango/stable/pango-Markup.html
68
+ def text_label(text:, dpi: 100, text_colour: [0, 0, 0, 255], shadow_colour: [255, 255, 255, 150], blur: 1.5)
69
+ text_mask, = Vips::Image.text(text, dpi: dpi)
70
+
71
+ canvas_width = text_mask.width + 10
72
+ canvas_height = text_mask.height + 10
73
+ text_mask = text_mask.gravity('west', canvas_width, canvas_height)
74
+ # use 0 - 1 for the masks
75
+ text_mask /= 255
76
+
77
+ shadow_mask = text_mask.gaussblur(blur)
78
+ # credit: https://github.com/libvips/pyvips/issues/204
79
+ text_mask = text_mask * text_colour + ((text_mask * -1) + 1) * shadow_mask * shadow_colour
80
+
81
+ text_mask.unpremultiply.copy(interpretation: 'srgb')
82
+ end
83
+
84
+ # convert hex color string to something VIPS understands
85
+ def hex2rgb(hex)
86
+ color_array = (hex.match(/#(..?)(..?)(..?)/))[1..3]
87
+ color_array.map! { |x| x + x } if hex.size == 4 # e.g. #333
88
+ color_array.map(&:hex)
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Honua
4
+ module Identity
5
+ NAME = 'honua'
6
+ LABEL = 'Honua'
7
+ VERSION = '0.1.0'
8
+ VERSION_LABEL = "#{LABEL} #{VERSION}"
9
+ end
10
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Honua
4
+ # locations are actual places in the world
5
+ # they are in latitude and longitude (EPSG:4326)
6
+ Location = Struct.new(:lat, :lon) do
7
+ def to_coordinate(zoom: 0)
8
+ lat_rad = deg2rad(lat)
9
+ n = 2.0**zoom
10
+ column = ((lon + 180) / 360.0 * n)
11
+ row = ((1 - Math.log(Math.tan(lat_rad) + (1 / Math.cos(lat_rad))) / Math::PI) / 2 * n)
12
+
13
+ Coordinate.new(row, column, zoom)
14
+ end
15
+
16
+ private
17
+
18
+ # degrees to radians
19
+ def deg2rad(degrees)
20
+ (degrees * Math::PI) / 180
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'async'
4
+ require 'forwardable'
5
+ require 'ruby-vips'
6
+
7
+ module Honua
8
+ class Map
9
+ extend Forwardable
10
+
11
+ attr_reader :width, :height, :zoom
12
+
13
+ def initialize(center:, width:, height:, zoom:)
14
+ @width = width
15
+ @height = height
16
+ @reference, @offset = reference_point(center.zoom_to(zoom))
17
+ @tiles = []
18
+ @zoom = zoom
19
+ end
20
+
21
+ def_delegators 'Honua.configuration',
22
+ :tile_width, :tile_height,
23
+ :attribution_text, :attribution_fgcolor, :attribution_bgcolor
24
+
25
+ def draw
26
+ fetch_tiles
27
+ render
28
+ end
29
+
30
+ # return an x,y point on the map image for a geographical location
31
+ def location2point(location)
32
+ x = @offset.x
33
+ y = @offset.y
34
+ coordinate = location.to_coordinate(zoom: @reference.zoom)
35
+
36
+ # distance from the know coordinate offset
37
+ x += tile_width * (coordinate.column - @reference.column)
38
+ y += tile_height * (coordinate.row - @reference.row)
39
+
40
+ x += @width / 2
41
+ y += @height / 2
42
+
43
+ Point.new(x.to_i, y.to_i)
44
+ end
45
+
46
+ def point2location(point)
47
+ # TODO: to be implemented
48
+ end
49
+
50
+ private
51
+
52
+ # returns the initial tile coordinate and its offset relative to the map center
53
+ def reference_point(coordinate)
54
+ # top left coordinate of tile containing center coordinate
55
+ top_left_coordinate = coordinate.container
56
+
57
+ # initial tile position offset, assuming centered tile in grid
58
+ offset_x = ((top_left_coordinate.column - coordinate.column) * Honua.configuration.tile_width).round
59
+ offset_y = ((top_left_coordinate.row - coordinate.row) * Honua.configuration.tile_height).round
60
+ offset = Point.new(offset_x, offset_y)
61
+
62
+ [top_left_coordinate, offset]
63
+ end
64
+
65
+ def fetch_tiles
66
+ coordinate, corner = top_left
67
+
68
+ Async do
69
+ (corner.y..@height).step(tile_height).each do |y|
70
+ current_coordinate = coordinate.dup
71
+ (corner.x..@width).step(tile_width).each do |x|
72
+ Async do
73
+ @tiles << Tile.get(current_coordinate, Point.new(x, y))
74
+ end
75
+ current_coordinate = current_coordinate.right
76
+ end
77
+ coordinate = coordinate.down
78
+ end
79
+ end
80
+ end
81
+
82
+ # get top left coordinate and offset to map
83
+ def top_left
84
+ x_shift = 0
85
+ y_shift = 0
86
+
87
+ corner = Point.new(@offset.x + @width / 2, @offset.y + @height / 2)
88
+
89
+ # move left on the map until we have the starting coordinate and offset
90
+ while corner.x.positive?
91
+ corner.x -= tile_width
92
+ x_shift += 1
93
+ end
94
+
95
+ # move up on the map until we have the starting coordinate and offset
96
+ while corner.y.positive?
97
+ corner.y -= tile_height
98
+ y_shift += 1
99
+ end
100
+
101
+ coordinate = Coordinate.new(@reference.row - y_shift, @reference.column - x_shift, @reference.zoom)
102
+
103
+ [coordinate, corner]
104
+ end
105
+
106
+ # create a canvas and draw tile images based on their offset onto it
107
+ def render
108
+ canvas = Vips::Image.grey(@width, @height)
109
+
110
+ @tiles.each do |tile|
111
+ canvas = canvas.insert(tile.image, tile.offset.x, tile.offset.y) # rubocop:disable Style/RedundantSelfAssignment
112
+ end
113
+
114
+ # add attribution image to bottom corner if available & attribution fits into image
115
+ if add_attribution?
116
+ options = { x: canvas.width - attribution.width, y: canvas.height - attribution.height }
117
+ canvas = canvas.composite2(attribution, :over, **options)
118
+ end
119
+
120
+ canvas
121
+ end
122
+
123
+ # create attribution image
124
+ def attribution
125
+ @attribution ||= begin
126
+ mask = Vips::Image.text(attribution_text)
127
+ mask = mask.embed(4, 2, mask.width + 8, mask.height + 4)
128
+ mask.ifthenelse(Helpers.hex2rgb(attribution_fgcolor), Helpers.hex2rgb(attribution_bgcolor), blend: true)
129
+ end
130
+ end
131
+
132
+ def add_attribution?
133
+ !attribution_text.nil? && attribution.width < @width && attribution.height < @height
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+ require 'open-uri'
5
+ require 'ruby-vips'
6
+
7
+ # a tile is an image file representing a coordinate at a certain zoom level
8
+ # * request a tile from the tile server for a coordinate
9
+ # * use scaled version if not available at on of the next zoom levels
10
+ # * if row or column is out of bounds => start from the left again
11
+ # * if max_fetch_attempts are reached => use empty tile
12
+ module Honua
13
+ class Tile
14
+ extend Forwardable
15
+
16
+ attr_reader :image, :offset, :coordinate
17
+
18
+ def self.get(coordinate, offset)
19
+ tile = Tile.new(coordinate, offset)
20
+ tile.fetch
21
+
22
+ tile
23
+ end
24
+
25
+ def initialize(coordinate, offset)
26
+ @coordinate = coordinate
27
+ @offset = offset
28
+
29
+ @image = blank_tile
30
+ end
31
+
32
+ def_delegators :@coordinate, :column, :row, :zoom
33
+ def_delegators 'Honua.configuration', :max_fetch_attempts, :tiles_url, :tile_width, :tile_height, :user_agent
34
+
35
+ # fetch tile image through url that includes zoom level and the tile grid's x/y coordinates
36
+ def fetch(attempt = 1, scale = 1)
37
+ if attempt >= max_fetch_attempts
38
+ @image = blank_tile
39
+ else
40
+ raw_tile = if url.start_with?('file://')
41
+ File.read(url.gsub('file://', ''))
42
+ else
43
+ URI.parse(url).open('User-Agent' => user_agent, &:read)
44
+ end
45
+
46
+ @image = Vips::Image.new_from_buffer(raw_tile, '')
47
+ end
48
+
49
+ @image = @image.resize(scale) if scale > 1
50
+ rescue OpenURI::HTTPError, Errno::ENOENT
51
+ old_row = @coordinate.row
52
+ old_column = @coordinate.column
53
+
54
+ # out of bounds columns or rows => try starting from the left or top
55
+ @coordinate.row = @coordinate.row - (2**@coordinate.zoom) if @coordinate.row > (2**@coordinate.zoom - 1)
56
+ @coordinate.column = @coordinate.column - (2**@coordinate.zoom) if @coordinate.column > (2**@coordinate.zoom - 1)
57
+
58
+ @coordinate.row = @coordinate.row + (2**@coordinate.zoom) if @coordinate.row.negative?
59
+ @coordinate.column = @coordinate.column + (2**@coordinate.zoom) if @coordinate.column.negative?
60
+
61
+ return fetch(attempt + 1) if old_row != @coordinate.row || old_column != @coordinate.column # if out of bounds
62
+
63
+ # if tile was not available try next zoom level and scale
64
+ recalculate!(scale)
65
+ fetch(attempt + 1, scale * 2)
66
+ end
67
+
68
+ private
69
+
70
+ # try the next lower zoom level if tile is not available
71
+ def recalculate!(scale)
72
+ neighbor = @coordinate.zoom_to(zoom - 1)
73
+ parent = neighbor.container
74
+
75
+ col_shift = 2 * (neighbor.column - parent.column)
76
+ row_shift = 2 * (neighbor.row - parent.row)
77
+
78
+ @offset.x -= scale * tile_width * col_shift
79
+ @offset.y -= scale * tile_height * row_shift
80
+ @coordinate = parent
81
+ end
82
+
83
+ def url
84
+ format(tiles_url, zoom: zoom, column: column, row: row)
85
+ end
86
+
87
+ def blank_tile
88
+ Vips::Image.grey(tile_width, tile_height)
89
+ end
90
+ end
91
+ end
metadata ADDED
@@ -0,0 +1,103 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: honua
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Joesi D.
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-02-01 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: async
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.28'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.28'
27
+ - !ruby/object:Gem::Dependency
28
+ name: ruby-vips
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: zeitwerk
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '2.4'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '2.4'
55
+ description: |
56
+ A mapping library to stitch geographic map images based on map tiles
57
+ provided by a rastered tile server.
58
+ email:
59
+ - dingsdax@fastmail.fm
60
+ executables: []
61
+ extensions: []
62
+ extra_rdoc_files:
63
+ - README.md
64
+ - LICENSE
65
+ files:
66
+ - LICENSE
67
+ - README.md
68
+ - lib/honua.rb
69
+ - lib/honua/configuration.rb
70
+ - lib/honua/coordinate.rb
71
+ - lib/honua/helpers.rb
72
+ - lib/honua/identity.rb
73
+ - lib/honua/location.rb
74
+ - lib/honua/map.rb
75
+ - lib/honua/tile.rb
76
+ homepage: https://github.com/dingsdax/honua
77
+ licenses:
78
+ - MIT
79
+ metadata:
80
+ bug_tracker_uri: https://github.com/dingsdax/honua/issues
81
+ changelog_uri: https://github.com/dingsdax/honua/blob/master/CHANGES.md
82
+ documentation_uri: https://github.com/dingsdax/honua
83
+ source_code_uri: https://github.com/dingsdax/honua
84
+ post_install_message:
85
+ rdoc_options: []
86
+ require_paths:
87
+ - lib
88
+ required_ruby_version: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - "~>"
91
+ - !ruby/object:Gem::Version
92
+ version: '3.0'
93
+ required_rubygems_version: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ version: '0'
98
+ requirements: []
99
+ rubygems_version: 3.2.3
100
+ signing_key:
101
+ specification_version: 4
102
+ summary: A Ruby geographic mapping library
103
+ test_files: []