jekyll-apple-maps 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 5507e6244102f89cacead6f1bb80f138d42501dbcce290f3acc30c2955fc2cbe
4
+ data.tar.gz: 886821f616922e5aa68ff16d0a52a60e5650048df572cb08c73c747498196290
5
+ SHA512:
6
+ metadata.gz: b4d7b68571c9f9a22dc10cbc2ae3f7a7aa1abd523153928cdd9a069ee275c4d62e5e0053d986eb1bca0f249e8646816877d97f84a96859d56a2d70992af085ba
7
+ data.tar.gz: '09731d93e886215f1c131b1b6a87c68bfd10ace7d7465cb7ea47ab627f16f88e80657b5ba4bdcaafe87dbaa4b67f52bb3c808663e213cb1ca8ebe2b5671b25c9'
data/README.md ADDED
@@ -0,0 +1,190 @@
1
+ # jekyll-apple-maps
2
+ [![CircleCI](https://dl.circleci.com/status-badge/img/circleci/aW12qZgMpxbXNYTbdzFe5/FS3eDPqnpMpZJ2cKYi2aN9/tree/main.svg?style=svg)](https://dl.circleci.com/status-badge/redirect/circleci/aW12qZgMpxbXNYTbdzFe5/FS3eDPqnpMpZJ2cKYi2aN9/tree/main) [![codecov](https://codecov.io/gh/ZekeSnider/jekyll-apple-maps/graph/badge.svg?token=2V7NFD77OL)](https://codecov.io/gh/ZekeSnider/jekyll-apple-maps)
3
+
4
+ ![Hero image for jekyll-apple-maps](/assets/hero_image.png)
5
+
6
+ This gem provides [Jekyll](https://jekyllrb.com) integrations for the [Apple Maps server APIs](https://developer.apple.com/documentation/applemapsserverapi/). It allows you to include Apple Maps content in your site using Jekyll Liquid tags. Currently it supports the following APIs:
7
+
8
+ + [Snapshots](https://developer.apple.com/documentation/snapshots)
9
+ + Supports both light and dark modes with dynamic `picture` tags
10
+ + Caches images to avoid regenerating images with the same parameters, saving on API calls
11
+ + Supports all parameters currently accepted by the Snapshots API
12
+ + Cleans up asset images for maps that are no longer being used
13
+
14
+ ## Installation
15
+
16
+ 1. Install gem
17
+
18
+ This plugin is available as the [jekyll-apple-maps RubyGem](https://rubygems.org/gems/jekyll-apple-maps). You can add it to your project by adding this line to your `Gemfile`: `gem 'jekyll-apple-maps'`. Then run `bundle` to install the gem.
19
+
20
+ 2. Add to configuration
21
+
22
+ After the gem has been installed, you need to add to your site's `_config.yml` to configure the plugin.
23
+
24
+ ```
25
+ plugins:
26
+ - jekyll-apple-maps
27
+ ```
28
+
29
+ You can also optionally override the `referer` parameter in your `_config.yml`. This can be useful when serving locally, where your site's URL is `localhost` by default.
30
+
31
+ ```
32
+ apple-maps:
33
+ referer: https://example.com
34
+ ```
35
+
36
+ 3. Add API Key
37
+
38
+ To use this plugin, you'll need an Apple Developer account to generate an API key. This plugin uses a "Web Snapshots" Maps token, you can use the steps [listed here](https://developer.apple.com/documentation/mapkitjs/creating_a_maps_token) to generate one.
39
+
40
+ > [!NOTE]
41
+ > This plugin uses your Jekyll site's `site.url` as the `referer` header on requests to the Apple Maps API by default. As mentioned above, you can also override it in your `_config.yml` file. When creating an API key you should either specify your site's url as the domain restriction, or generate a key without a domain restriction.
42
+
43
+ Once you have your API key, you should set the the `APPLE_MAPS_SNAPSHOT_API_KEY` environment variable.
44
+
45
+ `export APPLE_MAPS_SNAPSHOT_API_KEY=your_api_key_here`
46
+
47
+ ## Usage
48
+
49
+ ### Snapshots
50
+
51
+ The `apple_maps_snapshot_block` creates an Apple Maps image snapshot using the specified parameters. The [Apple Docs](https://developer.apple.com/documentation/snapshots/create_a_maps_web_snapshot) provide more details on each of the API parameters.
52
+
53
+ The following parameters are accepted by the block:
54
+ + `center` - Coordinates of the center of the map. Can be set to `auto` if annotations and/or overlays are specified. Defaults to `auto`.
55
+ + `map_type` - What type of map view to use. Options are: `standard`, `hybrid`, `satellite`, and `mutedStandard`. Defaults to `standard`.
56
+ + `show_poi` - Whether to display places of interest on the map. Defaults to `true`.
57
+ + `language` - Language to use for map labels. Defaults to `en-US`.
58
+ + `span` - Coordinate span of how much to display around the map's center. Defaults to `nil`.
59
+ + `zoom` - Zoom level with the range of `3` to `20`. Defaults to `12`.
60
+ + `width` - Pixel width of the image. Defaults to `600`.
61
+ + `height` - Pixel height of the image. Defaults to `300`.
62
+ + `scale` - The pixel density of the image. Valid values are `1`, `2`, `3`. Defaults to `2`.
63
+ + `color_schemes` - Array of which color schemes to generate for the map. Options are `light` and `dark`. Defaults to both (`['light', 'dark']`).
64
+ + `overlays` - An array of [overlay objects](https://developer.apple.com/documentation/snapshots/overlay). Defaults to empty `[]`.
65
+ + `annotations` - An array of [annotation objects](https://developer.apple.com/documentation/snapshots/annotation). Defaults to empty `[]`.
66
+ + `overlays_styles` - An array of [overlay style objects](https://developer.apple.com/documentation/snapshots/overlaystyle). Defaults to empty `[]`.
67
+ + `images` - An array of [image objects](https://developer.apple.com/documentation/snapshots/image) for annotations. Defaults to empty `[]`.
68
+
69
+ #### Examples
70
+ A map with a single annotation
71
+ ```
72
+ {% apple_maps_snapshot_block %}
73
+ center: "33.24767,-115.73192"
74
+ show_poi: true
75
+ zoom: 6
76
+ width: 600
77
+ height: 400
78
+ annotations: [
79
+ {
80
+ "point": "33.24767,-115.73192",
81
+ "color":"449944",
82
+ "glyphText": "S",
83
+ "markerStyle": "large"
84
+ }
85
+ ]
86
+ {% endapple_maps_snapshot_block %}
87
+ ```
88
+ ![Example map using a single annotation](/assets/single_annotation.png)
89
+
90
+ Using an image annotation
91
+ ```
92
+ {% apple_maps_snapshot_block %}
93
+ center: "33.24767,-115.73192"
94
+ show_poi: false
95
+ zoom: 8
96
+ width: 600
97
+ height: 400
98
+ color_schemes: ["dark"]
99
+ annotations: [
100
+ {
101
+ "markerStyle": "img",
102
+ "imgIdx": 0,
103
+ "point":"33.24767,-115.73192",
104
+ "color":"449944",
105
+ "offset": "0,15"
106
+ }
107
+ ]
108
+ images: [
109
+ {
110
+ "url": "https://www.iconfinder.com/icons/2376758/download/png/48",
111
+ "height": 48,
112
+ "width": 48
113
+ }
114
+ ]
115
+ {% endapple_maps_snapshot_block %}
116
+ ```
117
+ ![Example map using an image annotation](/assets/image_annotation.png)
118
+
119
+ A map with multiple annotations
120
+ ```
121
+ {% apple_maps_snapshot_block %}
122
+ center: "37.772318, -122.447326"
123
+ zoom: 11.5
124
+ width: 600
125
+ height: 400
126
+ annotations: [
127
+ {
128
+ "point": "37.819724, -122.478557",
129
+ "color":"red",
130
+ "glyphText": "G",
131
+ "markerStyle": "large"
132
+ },
133
+ {
134
+ "point": "37.750472,-122.484132",
135
+ "color": "blue",
136
+ "glyphText": "S",
137
+ "markerStyle": "large"
138
+ },
139
+ {
140
+ "point": "37.755217, -122.452776",
141
+ "color": "red",
142
+ "markerStyle": "balloon"
143
+ },
144
+ {
145
+ "point": "37.778457, -122.389238",
146
+ "color": "orange",
147
+ "markerStyle": "dot"
148
+ }
149
+ ]
150
+ {% endapple_maps_snapshot_block %}
151
+ ```
152
+ ![Example map using multiple annotations](/assets/multiple_annotations.png)
153
+
154
+ ## Rate limiting
155
+ Apple specifies the following limits on usage of the Apple Maps Server APIs. This plugin caches snapshot images for the same parameters to avoid regenerating images. But if you initially generate a large number of snapshots (>25,000), you may exceed this limit.
156
+
157
+ ```
158
+ The service provides up to 25,000 service calls per day per team between Apple Maps Server API and MapKit JS. If your app exceeds this quota, the service returns an HTTP 429 error (Too Many Requests) and your app needs to retry later. If your app requires a larger daily quota, submit a quota increase request form.
159
+
160
+ MapKit JS provides a free daily limit of 250,000 map views and 25,000 service calls per Apple Developer Program membership. For additional capacity needs, contact us.
161
+ https://developer.apple.com/maps/web/
162
+ ```
163
+
164
+ ## Development
165
+
166
+ To execute the test suite locally:
167
+ ```
168
+ bundle exec rspec
169
+ ```
170
+
171
+ To build and use the gem locally:
172
+ ```
173
+ gem build jekyll-apple-maps.gemspec
174
+ gem install ./jekyll-apple-maps-1.0.0.gem
175
+ ```
176
+
177
+ You can also use the local version of this gem from your gemfile:
178
+ ```
179
+ gem 'jekyll-apple-maps', path: '/PathHere/jekyll-apple-maps'
180
+ ```
181
+
182
+ There's also a CLI utility for testing templates.
183
+ + `-s` Is the source directory where the maps assets should be written to
184
+ + `-r` Is the referer header to use for the request
185
+ + `-h` prints help options
186
+
187
+ When you execute the script you'll paste in the full template text (as seen above in examples).
188
+ ```
189
+ ./script/render.rb -s /YourUser/Developer/jekyll-test -r https://example.com
190
+ ```
@@ -0,0 +1,37 @@
1
+ require 'net/http'
2
+ require 'uri'
3
+ require 'json'
4
+ require 'digest'
5
+
6
+ module Jekyll
7
+ module AppleMaps
8
+ class AppleMapsClient
9
+ class AppleMapsNetworkError < StandardError; end
10
+
11
+ @@snapshot_base_url = "https://snapshot.apple-mapkit.com/api/v1/snapshot"
12
+ @@snapshot_uri = URI(@@snapshot_base_url)
13
+
14
+ def initialize(api_key)
15
+ @api_key = api_key
16
+ end
17
+
18
+ def fetch_snapshot(query, referer)
19
+ query[:token] = @api_key
20
+ uri = @@snapshot_uri.dup
21
+ uri.query = URI.encode_www_form(query)
22
+
23
+ http = Net::HTTP.new(uri.host, uri.port)
24
+ http.use_ssl = true
25
+ request = Net::HTTP::Get.new(uri)
26
+ request['referer'] = referer
27
+ response = http.request(request)
28
+ unless response.is_a?(Net::HTTPSuccess)
29
+ query.delete(:token)
30
+ raise AppleMapsNetworkError, "Failed to generate map snapshot. Response: #{response.body}, Query: #{query}"
31
+ end
32
+
33
+ response.body
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,141 @@
1
+ require 'net/http'
2
+ require 'uri'
3
+ require 'json'
4
+ require 'digest'
5
+ require_relative './client.rb'
6
+
7
+ module Jekyll
8
+ module AppleMaps
9
+ class SnapshotBlock < Liquid::Block
10
+ class AppleMapsError < StandardError; end
11
+
12
+ @@used_snapshots = Set.new
13
+ @@color_schemes = ['light', 'dark']
14
+ @@log_prefix = "AppleMapsSnapshotBlock:"
15
+
16
+ def initialize(tag_name, markup, options)
17
+ super
18
+
19
+ api_key = ENV['APPLE_MAPS_SNAPSHOT_API_KEY']
20
+ unless api_key
21
+ log_and_raise("Apple Maps API key not found")
22
+ end
23
+ @client = AppleMapsClient.new(api_key)
24
+ end
25
+
26
+ def render(context)
27
+ content = super
28
+ params = YAML.safe_load(content)
29
+ width = params['width'] || 600
30
+ height = params['height'] || 300
31
+ size = "#{width}x#{height}"
32
+ annotations = params['annotations'] || []
33
+ overlays = params['overlays'] || []
34
+ overlay_styles = params['overlay_styles'] || []
35
+ images = params['images'] || []
36
+ color_schemes = params['color_schemes'] || ['light', 'dark']
37
+ color_schemes.uniq!
38
+ show_poi = params['show_poi'] || true ? 1 : 0
39
+
40
+ if color_schemes.empty?
41
+ log_and_raise("Color Schemes cannot be empty")
42
+ end
43
+
44
+ unless color_schemes.all? { |scheme| @@color_schemes.include?(scheme) }
45
+ log_and_raise("Invalid color scheme specified, only 'light' and 'dark' are allowed")
46
+ end
47
+
48
+ query = {
49
+ center: params['center'] || 'auto',
50
+ annotations: annotations.to_json,
51
+ overlays: overlays.to_json,
52
+ overlayStyles: overlay_styles.to_json,
53
+ imgs: images.to_json,
54
+ size: size,
55
+ z: params['zoom'] || 12,
56
+ t: params['map_type'] || 'standard',
57
+ scale: params['scale'] || 2,
58
+ lang: params['language'] || 'en-US',
59
+ spn: params['span'] || nil,
60
+ poi: show_poi,
61
+ }
62
+ query.compact!
63
+
64
+ image_relative_paths = color_schemes.to_h do |color_scheme|
65
+ query[:colorScheme] = color_scheme
66
+ [color_scheme, get_relative_path(context, query)]
67
+ end
68
+
69
+ result_tag = "<picture>"
70
+ image_relative_paths.each do |color_scheme, relative_path|
71
+ result_tag << "<source srcset='/#{relative_path}' media='(prefers-color-scheme: #{color_scheme})'>"
72
+ end
73
+ result_tag << "<img src='/#{image_relative_paths.first}' alt='Map of location'>"
74
+ result_tag << "</picture>"
75
+
76
+ return result_tag
77
+ end
78
+
79
+ def self.cleanup(site)
80
+ Jekyll.logger.info @@log_prefix, "Starting cleanup of unused snapshots"
81
+
82
+ maps_dir = File.join(site.source, 'assets', 'maps')
83
+ return unless File.directory?(maps_dir)
84
+
85
+ Dir.glob(File.join(maps_dir, 'apple_maps_snapshot_*.png')).each do |file|
86
+ filename = File.basename(file)
87
+ unless @@used_snapshots.include?(filename)
88
+ File.delete(file)
89
+ Jekyll.logger.info @@log_prefix, "Deleted unused map snapshot: #{filename}"
90
+ end
91
+ end
92
+
93
+ Jekyll.logger.info @@log_prefix, "Cleanup completed"
94
+ end
95
+
96
+ private
97
+
98
+ def get_relative_path(context, query)
99
+ hash = Digest::SHA256.hexdigest(query.to_s)
100
+ filename = "apple_maps_snapshot_#{hash}.png"
101
+ relative_path = "assets/maps/#{filename}"
102
+ full_path = File.join(context.registers[:site].source, relative_path)
103
+
104
+ @@used_snapshots.add(filename)
105
+
106
+ if File.exist?(full_path)
107
+ return relative_path
108
+ end
109
+
110
+ Jekyll.logger.info @@log_prefix, "Fetching new snapshot from Apple Maps API"
111
+ image_data = @client.fetch_snapshot(query, get_referer(context))
112
+
113
+ FileUtils.mkdir_p(File.dirname(full_path))
114
+ static_file = Jekyll::StaticFile.new(context.registers[:site], context.registers[:site].source,
115
+ File.dirname(relative_path), filename)
116
+ FileUtils.mkdir_p(File.dirname(static_file.path))
117
+ File.open(static_file.path, 'wb') do |file|
118
+ file.write(image_data)
119
+ end
120
+ context.registers[:site].static_files << static_file
121
+
122
+ return relative_path
123
+ end
124
+
125
+ def get_referer(context)
126
+ context.registers[:site].config.dig('apple_maps', 'referer') || context.registers[:site].config['url']
127
+ end
128
+
129
+ def log_and_raise(message)
130
+ Jekyll.logger.error @@log_prefix, message
131
+ raise AppleMapsError, message
132
+ end
133
+ end
134
+ end
135
+ end
136
+
137
+ Liquid::Template.register_tag('apple_maps_snapshot_block', Jekyll::AppleMaps::SnapshotBlock)
138
+
139
+ Jekyll::Hooks.register :site, :post_write do |site|
140
+ Jekyll::AppleMaps::SnapshotBlock.cleanup(site)
141
+ end
@@ -0,0 +1,5 @@
1
+ module Jekyll
2
+ module AppleMaps
3
+ VERSION = "1.0.0"
4
+ end
5
+ end
@@ -0,0 +1,3 @@
1
+ require 'jekyll'
2
+
3
+ require_relative "jekyll/apple-maps/snapshot_block.rb"
metadata ADDED
@@ -0,0 +1,175 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: jekyll-apple-maps
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Zeke Snider
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2024-08-26 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: jekyll
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '4.0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '4.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: simplecov
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.17'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.17'
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '2.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '2.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '13.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '13.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '3.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '3.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: pry
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '0.14'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '0.14'
97
+ - !ruby/object:Gem::Dependency
98
+ name: liquid
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '4.0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '4.0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: webmock
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: 3.23.1
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: 3.23.1
125
+ - !ruby/object:Gem::Dependency
126
+ name: fakefs
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '2.5'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '2.5'
139
+ description: Provides tags for the Jekyll blogging engine to generate Apple Maps content
140
+ for your site.
141
+ email:
142
+ - zekesnider@me.com
143
+ executables: []
144
+ extensions: []
145
+ extra_rdoc_files: []
146
+ files:
147
+ - README.md
148
+ - lib/jekyll-apple-maps.rb
149
+ - lib/jekyll/apple_maps/client.rb
150
+ - lib/jekyll/apple_maps/snapshot_block.rb
151
+ - lib/jekyll/apple_maps/version.rb
152
+ homepage: https://github.com/zekesnider/jekyll-apple-maps
153
+ licenses:
154
+ - Apache-2.0
155
+ metadata: {}
156
+ post_install_message:
157
+ rdoc_options: []
158
+ require_paths:
159
+ - lib
160
+ required_ruby_version: !ruby/object:Gem::Requirement
161
+ requirements:
162
+ - - ">="
163
+ - !ruby/object:Gem::Version
164
+ version: '0'
165
+ required_rubygems_version: !ruby/object:Gem::Requirement
166
+ requirements:
167
+ - - ">="
168
+ - !ruby/object:Gem::Version
169
+ version: '0'
170
+ requirements: []
171
+ rubygems_version: 3.1.6
172
+ signing_key:
173
+ specification_version: 4
174
+ summary: Apple Maps plugin for Jekyll
175
+ test_files: []