ProMotion-mapbox 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +376 -0
- data/lib/ProMotion-mapbox.rb +20 -0
- data/lib/ProMotion/map/map_screen.rb +6 -0
- data/lib/ProMotion/map/map_screen_annotation.rb +74 -0
- data/lib/ProMotion/map/map_screen_module.rb +448 -0
- metadata +120 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 0a6c7b1a09e8296b378386a2508ff9803ffee0a3
|
4
|
+
data.tar.gz: 4f9f1e7093bb2981aeffe516cb783f033cfab280
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 16b2612226da961d263dc65d83f1f060238cb18d646c9fcd992961d5516534f13ab7e3c549569c23b469777ebf7bc22587835077f55eea0776bbbe43dcf11af2
|
7
|
+
data.tar.gz: ab5d5c5075abb292a91a3fb85650136fd9c2893fc017f9dfb2094120fedfe16e5a1fe95aa1d1ee6663d22fe361dc386980db74c6faf087a96f859ef06385e0c7
|
data/README.md
ADDED
@@ -0,0 +1,376 @@
|
|
1
|
+
# ProMotion-mapbox
|
2
|
+
|
3
|
+
[![Gem Version](https://badge.fury.io/rb/ProMotion-mapbox.svg)](http://badge.fury.io/rb/ProMotion-mapbox)
|
4
|
+
|
5
|
+
ProMotion-mapbox provides a PM::MapScreen, forked from the
|
6
|
+
popular RubyMotion gem [ProMotion-map](https://github.com/clearsightstudio/ProMotion-map).
|
7
|
+
|
8
|
+
## Installation
|
9
|
+
|
10
|
+
```ruby
|
11
|
+
gem 'ProMotion-mapbox'
|
12
|
+
```
|
13
|
+
```ruby
|
14
|
+
rake pod:install
|
15
|
+
```
|
16
|
+
|
17
|
+
|
18
|
+
## Usage
|
19
|
+
|
20
|
+
Easily create a map screen, complete with annotations.
|
21
|
+
|
22
|
+
*Has all the methods of PM::Screen*
|
23
|
+
|
24
|
+
```ruby
|
25
|
+
class MyMapScreen < PM::MapScreen
|
26
|
+
mapbox_setup access_token: "YOU_MAPBOX_ACCESS_TOKEN",
|
27
|
+
tile_source: "mylogin.map"
|
28
|
+
|
29
|
+
title "My Map"
|
30
|
+
start_position latitude: 35.090648651123, longitude: -82.965972900391, radius: 4
|
31
|
+
tap_to_add
|
32
|
+
|
33
|
+
def annotation_data
|
34
|
+
[{
|
35
|
+
longitude: -82.965972900391,
|
36
|
+
latitude: 35.090648651123,
|
37
|
+
title: "Rainbow Falls",
|
38
|
+
subtitle: "Nantahala National Forest",
|
39
|
+
action: :show_forest,
|
40
|
+
pin_color: :green
|
41
|
+
},{
|
42
|
+
longitude: -82.966093558105,
|
43
|
+
latitude: 35.092520895652,
|
44
|
+
title: "Turtleback Falls",
|
45
|
+
subtitle: "Nantahala National Forest",
|
46
|
+
action: :show_forest,
|
47
|
+
pin_color: :purple]
|
48
|
+
},{
|
49
|
+
longitude: -82.95916,
|
50
|
+
latitude: 35.07496,
|
51
|
+
title: "Windy Falls",
|
52
|
+
action: :show_forest
|
53
|
+
},{
|
54
|
+
longitude: -82.943031505056,
|
55
|
+
latitude: 35.102516828489,
|
56
|
+
title: "Upper Bearwallow Falls",
|
57
|
+
subtitle: "Gorges State Park",
|
58
|
+
action: :show_forest
|
59
|
+
},{
|
60
|
+
longitude: -82.956244328014,
|
61
|
+
latitude: 35.085548421623,
|
62
|
+
title: "Stairway Falls",
|
63
|
+
subtitle: "Gorges State Park",
|
64
|
+
your_param: "CustomWhatever",
|
65
|
+
action: :show_forest
|
66
|
+
}, {
|
67
|
+
coordinate: CLLocationCoordinate2DMake(35.090648651123, -82.965972900391)
|
68
|
+
title: "Rainbow Falls",
|
69
|
+
subtitle: "Nantahala National Forest",
|
70
|
+
image: UIImage.imageNamed("custom-pin"),
|
71
|
+
action: :show_forest
|
72
|
+
}]
|
73
|
+
end
|
74
|
+
|
75
|
+
def show_forest
|
76
|
+
selected = selected_annotations.first
|
77
|
+
# Do something with the selected annotation.
|
78
|
+
end
|
79
|
+
end
|
80
|
+
```
|
81
|
+
|
82
|
+
Here's a neat way to zoom into a specific marker in an animated fashion and then select the marker:
|
83
|
+
|
84
|
+
```ruby
|
85
|
+
def zoom_to_marker(marker)
|
86
|
+
set_region region(coordinate: marker.coordinate, radius: 5) # Radius are specified in nautical miles.
|
87
|
+
select_annotation marker
|
88
|
+
end
|
89
|
+
```
|
90
|
+
|
91
|
+
---
|
92
|
+
|
93
|
+
### Methods
|
94
|
+
|
95
|
+
#### annotation_data
|
96
|
+
|
97
|
+
Method that is called to get the map's annotation data and build the map. If you do not want any annotations, simply return an empty array.
|
98
|
+
|
99
|
+
All possible properties:
|
100
|
+
|
101
|
+
```ruby
|
102
|
+
{
|
103
|
+
# REQUIRED -or- use :coordinate
|
104
|
+
longitude: -82.956244328014,
|
105
|
+
latitude: 35.085548421623,
|
106
|
+
|
107
|
+
# REQUIRED -or- use :longitude & :latitude
|
108
|
+
coordinate: CLLocationCoordinate2DMake(35.085548421623, -82.956244328014)
|
109
|
+
|
110
|
+
title: "Stairway Falls", # REQUIRED
|
111
|
+
subtitle: "Gorges State Park",
|
112
|
+
image: "my_custom_image",
|
113
|
+
pin_color: :red, # Defaults to :red. Other options are :green or :purple or any UIColor
|
114
|
+
left_accessory: my_button,
|
115
|
+
right_accessory: my_other_button,
|
116
|
+
action: :my_action, # Overrides :right_accessory
|
117
|
+
action_button_type: UIButtonTypeContactAdd # Defaults to UIButtonTypeDetailDisclosure
|
118
|
+
}
|
119
|
+
```
|
120
|
+
|
121
|
+
You may pass whatever properties you want in the annotation hash, but (`:longitude` && `:latitude` || `:coordinate`), and `:title` are required.
|
122
|
+
|
123
|
+
Use `:image` to specify a custom image. Pass in a string to conserve memory and it will be converted using `UIImage.imageNamed(your_string)`. If you pass in a `UIImage`, we'll use that, but keep in mind that there will be another unnecessary copy of the UIImage in memory.
|
124
|
+
|
125
|
+
Use `:left_accessory` and `:right_accessory` to specify a custom accessory, like a button.
|
126
|
+
|
127
|
+
You can access annotation data you've arbitrarily stored in the hash by calling `annotation_instance.params[:your_param]`.
|
128
|
+
|
129
|
+
The `:action` parameter specifies a method that should be run when the detail button is tapped on the annotation. It automatically adds a `UIButtonTypeDetailDisclosure` button to the `:left_accessory`. In your method you can find out which annotation's accessory was tapped by calling `selected_annotations.first`.
|
130
|
+
|
131
|
+
#### update_annotation_data
|
132
|
+
|
133
|
+
Forces a reload of all the annotations
|
134
|
+
|
135
|
+
#### annotations
|
136
|
+
|
137
|
+
Returns an array of all the annotations.
|
138
|
+
|
139
|
+
#### center
|
140
|
+
|
141
|
+
Returns a `CLLocation2D` instance with the center coordinates of the map.
|
142
|
+
|
143
|
+
#### center=({latitude: Float, longitude: Float, animated: Boolean})
|
144
|
+
|
145
|
+
Sets the center of the map. `animated` property defaults to `true`.
|
146
|
+
|
147
|
+
#### show_user_location
|
148
|
+
|
149
|
+
Shows the user's location on the map. Must be called in the view initialization sequence on `will_appear` or _after_.
|
150
|
+
|
151
|
+
#### look_up_location(CLLocation) { |placemark, error| }
|
152
|
+
|
153
|
+
This method takes a CLLocation object and will return one to many CLPlacemark to represent nearby data.
|
154
|
+
|
155
|
+
##### iOS 8 Location Requirements
|
156
|
+
|
157
|
+
iOS 8 introduced stricter location services requirements. You are now required to add a few key/value pairs to the `Info.plist`. Add these two lines to your `Rakefile` (with your descriptions, obviously):
|
158
|
+
|
159
|
+
```ruby
|
160
|
+
app.info_plist['NSLocationAlwaysUsageDescription'] = 'Description'
|
161
|
+
app.info_plist['NSLocationWhenInUseUsageDescription'] = 'Description'
|
162
|
+
```
|
163
|
+
|
164
|
+
*Note: you need both keys to use `get_once`, so it's probably best to just include both no matter what.* See [Apple's documentation](https://developer.apple.com/library/ios/documentation/General/Reference/InfoPlistKeyReference/Articles/CocoaKeys.html#//apple_ref/doc/uid/TP40009251-SW18) on iOS 8 location services requirements for more information.
|
165
|
+
|
166
|
+
#### hide_user_location
|
167
|
+
|
168
|
+
Hides the user's location on the map.
|
169
|
+
|
170
|
+
#### showing_user_location?
|
171
|
+
|
172
|
+
Returns a `Boolean` of whether or not the map view is currently showing the user's location.
|
173
|
+
|
174
|
+
#### user_location
|
175
|
+
|
176
|
+
Returns a `CLLocation2D` object of the user's location or `nil` if the user location is not being tracked
|
177
|
+
|
178
|
+
#### zoom_to_user(radius = 0.05, animated=true)
|
179
|
+
|
180
|
+
Zooms to the user's location. If the user's location is not currently being shown on the map, it will show it first. `radius` is the distance in nautical miles from the center point (user location) to the corners of a virtual bounding box.
|
181
|
+
|
182
|
+
#### select_annotation(annotation, animated=true)
|
183
|
+
|
184
|
+
Selects a single annotation.
|
185
|
+
|
186
|
+
#### select_annotation_at(annotation_index, animated=true)
|
187
|
+
|
188
|
+
Selects a single annotation using the annotation at the index of your `annotation_data` array.
|
189
|
+
|
190
|
+
#### selected_annotation
|
191
|
+
|
192
|
+
Returns the annotation that is selected. If no annotation is selected, returns `nil`.
|
193
|
+
|
194
|
+
#### deselect_annotation(animated=false)
|
195
|
+
|
196
|
+
Deselects any selected annotation.
|
197
|
+
|
198
|
+
#### add_annotation(annotation)
|
199
|
+
|
200
|
+
Adds a new annotation to the map. Refer to `annotation_data` (above) for hash properties.
|
201
|
+
|
202
|
+
#### add_annotations(annotations)
|
203
|
+
|
204
|
+
Adds more than one annotation at a time to the map.
|
205
|
+
|
206
|
+
#### clear_annotations
|
207
|
+
|
208
|
+
Removes all annotations from the `MapScreen`.
|
209
|
+
|
210
|
+
#### zoom_to_fit_annotations({animated:true, include_user:false})
|
211
|
+
|
212
|
+
Changes the zoom and center point of the `MapScreen` to fit all the annotations. Passing `include_user` as `true` will cause the zoom to not only include the annotations from `annotation_data` but also the user pin in the zoom region calculation.
|
213
|
+
|
214
|
+
#### set_region(region, animated=true)
|
215
|
+
|
216
|
+
Sets the region of the `MapScreen`. `region` should be an instance of `MKCoordinateRegion`.
|
217
|
+
|
218
|
+
#### region(center_location,radius=10)
|
219
|
+
|
220
|
+
Mapbox API doesn't have the concept of a region. Instead, we can zoom to a virtual bounding box defined by its Sourthwest and Northeast
|
221
|
+
corners.
|
222
|
+
The ```region``` methods takes a ```center_location``` and a radius. The distance from the center to the corners (and thus the zoom level) will be the ```radius``` times 1820 meters (1 Nautical mile)
|
223
|
+
|
224
|
+
```ruby
|
225
|
+
my_region = region({
|
226
|
+
CLLocationCoordinate2D.new(35.0906,-82.965),
|
227
|
+
radius: 11
|
228
|
+
})
|
229
|
+
```
|
230
|
+
|
231
|
+
---
|
232
|
+
|
233
|
+
### Class Methods
|
234
|
+
|
235
|
+
#### start_position(latitude: Float, longitude: Float, radius: Float)
|
236
|
+
|
237
|
+
Class method to set the initial starting position of the `MapScreen`.
|
238
|
+
|
239
|
+
```ruby
|
240
|
+
class MyMapScreen < PM::MapScreen
|
241
|
+
start_position latitude: 36.10, longitude: -80.26, radius: 4
|
242
|
+
end
|
243
|
+
```
|
244
|
+
|
245
|
+
`radius` is the zoom level of the map in miles (default: 10).
|
246
|
+
|
247
|
+
#### tap_to_add(length: Float, target: Object, action: Selector, annotation: Hash)
|
248
|
+
|
249
|
+
Lets a user long press the map to drop an annotation where they pressed.
|
250
|
+
|
251
|
+
##### Default values:
|
252
|
+
|
253
|
+
You can override any of these values. The `annotation` parameter can take any options specified in the annotation documentation above except `:latitude`, `:longitude`, and `:coordinate`.
|
254
|
+
|
255
|
+
```ruby
|
256
|
+
length: 2.0,
|
257
|
+
target: self,
|
258
|
+
action: "gesture_drop_pin:",
|
259
|
+
annotation: {
|
260
|
+
title: "Dropped Pin",
|
261
|
+
animates_drop: true
|
262
|
+
}
|
263
|
+
```
|
264
|
+
|
265
|
+
##### Notifications
|
266
|
+
|
267
|
+
This feature posts two different `NSNotificationCenter` notifications:
|
268
|
+
|
269
|
+
**ProMotionMapWillAddPin:** Fired the moment the long press gesture is recognized, before the pin is added.
|
270
|
+
|
271
|
+
**ProMotionMapAddedPin:** Fired after the pin has been added to the map.
|
272
|
+
|
273
|
+
##### Example:
|
274
|
+
|
275
|
+
```ruby
|
276
|
+
# Simple Example
|
277
|
+
class MyMapScreen < PM::MapScreen
|
278
|
+
title "My Map Screen"
|
279
|
+
tap_to_add length: 1.5
|
280
|
+
def annotations
|
281
|
+
[]
|
282
|
+
end
|
283
|
+
end
|
284
|
+
```
|
285
|
+
|
286
|
+
```ruby
|
287
|
+
# A More Complex Example
|
288
|
+
class MyMapScreen < PM::MapScreen
|
289
|
+
title "My Map Screen"
|
290
|
+
tap_to_add length: 1.5, annotation: {animates_drop: true, title: "A Cool New Pin"}
|
291
|
+
def annotations
|
292
|
+
[]
|
293
|
+
end
|
294
|
+
|
295
|
+
def will_appear
|
296
|
+
NSNotificationCenter.defaultCenter.addObserver(self, selector:"pin_adding:") , name:"ProMotionMapWillAddPin", object:nil)
|
297
|
+
NSNotificationCenter.defaultCenter.addObserver(self, selector:"pin_added:") , name:"ProMotionMapAddedPin", object:nil)
|
298
|
+
end
|
299
|
+
|
300
|
+
def will_disappear
|
301
|
+
NSNotificationCenter.defaultCenter.removeObserver(self)
|
302
|
+
end
|
303
|
+
|
304
|
+
def pin_adding(notification)
|
305
|
+
# We only want one pin on the map at a time
|
306
|
+
clear_annotations
|
307
|
+
end
|
308
|
+
|
309
|
+
def pin_added(notification)
|
310
|
+
# Once the pin is dropped we want to select it
|
311
|
+
select_annotation_at(0)
|
312
|
+
end
|
313
|
+
end
|
314
|
+
```
|
315
|
+
|
316
|
+
---
|
317
|
+
|
318
|
+
### Delegate callbacks
|
319
|
+
|
320
|
+
These methods (if implemented in your `MapScreen`) will be called when the corresponding `MKMapViewDelegate` method is invoked:
|
321
|
+
|
322
|
+
```ruby
|
323
|
+
def will_change_region(animated)
|
324
|
+
# Do something when the region will change
|
325
|
+
# The animated parameter is optional so you can also define it is simply:
|
326
|
+
# def will_change_region
|
327
|
+
# end
|
328
|
+
end
|
329
|
+
|
330
|
+
def on_change_region(animated)
|
331
|
+
# Do something when the region changed
|
332
|
+
# The animated parameter is optional so you can also define it is simply:
|
333
|
+
# def on_change_region
|
334
|
+
# end
|
335
|
+
end
|
336
|
+
```
|
337
|
+
|
338
|
+
---
|
339
|
+
|
340
|
+
### CocoaTouch Property Convenience Methods
|
341
|
+
|
342
|
+
`MKMapView` contains multiple property setters and getters that can be accessed in a more ruby-like syntax:
|
343
|
+
|
344
|
+
```ruby
|
345
|
+
type # Returns a MKMapType
|
346
|
+
type = (MKMapType)new_type
|
347
|
+
|
348
|
+
zoom_enabled?
|
349
|
+
zoom_enabled = (bool)enabled
|
350
|
+
|
351
|
+
scroll_enabled?
|
352
|
+
scroll_enabled = (bool)enabled
|
353
|
+
|
354
|
+
pitch_enabled?
|
355
|
+
pitch_enabled = (bool)enabled
|
356
|
+
|
357
|
+
rotate_enabled?
|
358
|
+
rotate_enabled = (bool)enabled
|
359
|
+
```
|
360
|
+
|
361
|
+
---
|
362
|
+
|
363
|
+
### Accessors
|
364
|
+
|
365
|
+
#### `map` or `mapview`
|
366
|
+
|
367
|
+
Reference to the created RMMapView.
|
368
|
+
|
369
|
+
## Contributing
|
370
|
+
|
371
|
+
1. Fork it
|
372
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
373
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
374
|
+
4. Make some specs pass
|
375
|
+
5. Push to the branch (`git push origin my-new-feature`)
|
376
|
+
6. Create new Pull Request
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
unless defined?(Motion::Project::Config)
|
3
|
+
raise "ProMotion-mapbox must be required within a RubyMotion project."
|
4
|
+
end
|
5
|
+
|
6
|
+
require 'motion-cocoapods'
|
7
|
+
|
8
|
+
Motion::Project::App.setup do |app|
|
9
|
+
lib_dir_path = File.dirname(File.expand_path(__FILE__))
|
10
|
+
app.files << File.join(lib_dir_path, "ProMotion/map/map_screen_annotation.rb")
|
11
|
+
app.files << File.join(lib_dir_path, "ProMotion/map/map_screen_module.rb")
|
12
|
+
app.files << File.join(lib_dir_path, "ProMotion/map/map_screen.rb")
|
13
|
+
|
14
|
+
app.frameworks += %w(CoreLocation)
|
15
|
+
|
16
|
+
app.pods do
|
17
|
+
pod "Mapbox-iOS-SDK"
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module ProMotion
|
2
|
+
class MapScreenAnnotation < RMAnnotation
|
3
|
+
attr_reader :params
|
4
|
+
|
5
|
+
def initialize(params = {},map_view)
|
6
|
+
@params = params
|
7
|
+
@map_view = map_view
|
8
|
+
set_defaults
|
9
|
+
|
10
|
+
if @params[:coordinate]
|
11
|
+
@params[:latitude] = @params[:coordinate].latitude
|
12
|
+
@params[:longitude] = @params[:coordinate].longitude
|
13
|
+
@coordinate = @params[:coordinate]
|
14
|
+
initWithMapView(map_view, coordinate: @coordinate, andTitle: @params[:title])
|
15
|
+
elsif @params[:latitude] && @params[:longitude]
|
16
|
+
@coordinate = CLLocationCoordinate2D.new(@params[:latitude], @params[:longitude])
|
17
|
+
initWithMapView(map_view, coordinate: @coordinate, andTitle: @params[:title])
|
18
|
+
else
|
19
|
+
PM.logger.error("You are required to specify :latitude and :longitude or :coordinate for annotations.")
|
20
|
+
nil
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def set_defaults
|
25
|
+
@params = {
|
26
|
+
title: "Title",
|
27
|
+
pin_color: :red,
|
28
|
+
identifier: "Annotation-#{@params[:pin_color]}-#{@params[:image]}",
|
29
|
+
show_callout: true,
|
30
|
+
animates_drop: false,
|
31
|
+
maki_icon: nil,
|
32
|
+
}.merge(@params)
|
33
|
+
end
|
34
|
+
|
35
|
+
def title
|
36
|
+
@params[:title]
|
37
|
+
end
|
38
|
+
|
39
|
+
def subtitle
|
40
|
+
@params[:subtitle] ||= nil
|
41
|
+
end
|
42
|
+
|
43
|
+
def coordinate
|
44
|
+
@coordinate
|
45
|
+
end
|
46
|
+
|
47
|
+
def pin_color
|
48
|
+
@params[:pin_color]
|
49
|
+
end
|
50
|
+
|
51
|
+
def cllocation
|
52
|
+
CLLocation.alloc.initWithLatitude(@params[:latitude], longitude:@params[:longitude])
|
53
|
+
end
|
54
|
+
|
55
|
+
def setCoordinate(new_coordinate)
|
56
|
+
super
|
57
|
+
if new_coordinate.is_a? Hash
|
58
|
+
@coordinate = CLLocationCoordinate2D.new(new_coordinate[:latitude], new_coordinate[:longitude])
|
59
|
+
else
|
60
|
+
@coordinate = new_coordinate
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def method_missing(meth, *args)
|
65
|
+
if @params[meth.to_sym]
|
66
|
+
@params[meth.to_sym]
|
67
|
+
else
|
68
|
+
PM.logger.warn "The annotation parameter \"#{meth}\" does not exist on this pin."
|
69
|
+
nil
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,448 @@
|
|
1
|
+
module ProMotion
|
2
|
+
module MapScreenModule
|
3
|
+
|
4
|
+
PIN_COLORS = {
|
5
|
+
red: UIColor.redColor,
|
6
|
+
green: UIColor.greenColor,
|
7
|
+
purple: UIColor.purpleColor
|
8
|
+
}
|
9
|
+
|
10
|
+
def screen_setup
|
11
|
+
mapbox_setup
|
12
|
+
self.view = nil
|
13
|
+
self.view = RMMapView.alloc.initWithFrame(self.view.bounds, andTilesource:@tileSource)
|
14
|
+
self.view.delegate = self
|
15
|
+
|
16
|
+
check_annotation_data
|
17
|
+
@promotion_annotation_data = []
|
18
|
+
set_up_tap_to_add
|
19
|
+
end
|
20
|
+
|
21
|
+
def mapbox_setup
|
22
|
+
if self.class.respond_to?(:get_mapbox_setup) && self.class.get_mapbox_setup
|
23
|
+
setup_params = self.class.get_mapbox_setup_params
|
24
|
+
else
|
25
|
+
PM.logger.error "Missing Mapbox setup data."
|
26
|
+
end
|
27
|
+
RMConfiguration.sharedInstance.setAccessToken(setup_params[:access_token])
|
28
|
+
@tileSource = RMMapboxSource.alloc.initWithMapID(setup_params[:tile_source])
|
29
|
+
end
|
30
|
+
|
31
|
+
def view_will_appear(animated)
|
32
|
+
super
|
33
|
+
update_annotation_data
|
34
|
+
end
|
35
|
+
|
36
|
+
def view_did_appear(animated)
|
37
|
+
super
|
38
|
+
set_up_start_position
|
39
|
+
end
|
40
|
+
|
41
|
+
def check_annotation_data
|
42
|
+
PM.logger.error "Missing #annotation_data method in MapScreen #{self.class.to_s}." unless self.respond_to?(:annotation_data)
|
43
|
+
end
|
44
|
+
|
45
|
+
def update_annotation_data
|
46
|
+
clear_annotations
|
47
|
+
add_annotations annotation_data
|
48
|
+
end
|
49
|
+
|
50
|
+
def map
|
51
|
+
self.view
|
52
|
+
end
|
53
|
+
alias_method :mapview, :map
|
54
|
+
|
55
|
+
def center
|
56
|
+
self.view.centerCoordinate
|
57
|
+
end
|
58
|
+
|
59
|
+
def center=(params={})
|
60
|
+
PM.logger.error "Missing #:latitude property in call to #center=." unless params[:latitude]
|
61
|
+
PM.logger.error "Missing #:longitude property in call to #center=." unless params[:longitude]
|
62
|
+
params[:animated] ||= true
|
63
|
+
|
64
|
+
# Set the new region
|
65
|
+
self.view.setCenterCoordinate(
|
66
|
+
CLLocationCoordinate2D.new(params[:latitude], params[:longitude]),
|
67
|
+
animated:params[:animated]
|
68
|
+
)
|
69
|
+
end
|
70
|
+
|
71
|
+
def show_user_location
|
72
|
+
if location_manager.respondsToSelector('requestWhenInUseAuthorization')
|
73
|
+
location_manager.requestWhenInUseAuthorization
|
74
|
+
end
|
75
|
+
|
76
|
+
set_show_user_location true
|
77
|
+
end
|
78
|
+
|
79
|
+
def hide_user_location
|
80
|
+
set_show_user_location false
|
81
|
+
end
|
82
|
+
|
83
|
+
def set_show_user_location(show)
|
84
|
+
self.view.showsUserLocation = show
|
85
|
+
end
|
86
|
+
|
87
|
+
def showing_user_location?
|
88
|
+
self.view.showsUserLocation
|
89
|
+
end
|
90
|
+
|
91
|
+
def user_location
|
92
|
+
user_annotation.nil? ? nil : user_annotation.coordinate
|
93
|
+
end
|
94
|
+
|
95
|
+
def user_annotation
|
96
|
+
self.view.userLocation.nil? ? nil : self.view.userLocation.location
|
97
|
+
end
|
98
|
+
|
99
|
+
def zoom_to_user(radius = 0.05, animated=true)
|
100
|
+
show_user_location unless showing_user_location?
|
101
|
+
set_region(create_region(user_location,radius), animated)
|
102
|
+
end
|
103
|
+
|
104
|
+
def annotations
|
105
|
+
@promotion_annotation_data
|
106
|
+
end
|
107
|
+
|
108
|
+
def select_annotation(annotation, animated=true)
|
109
|
+
self.view.selectAnnotation(annotation, animated:animated)
|
110
|
+
end
|
111
|
+
|
112
|
+
def select_annotation_at(annotation_index, animated=true)
|
113
|
+
select_annotation(annotations[annotation_index], animated:animated)
|
114
|
+
end
|
115
|
+
|
116
|
+
def selected_annotation
|
117
|
+
self.view.selectedAnnotation
|
118
|
+
end
|
119
|
+
|
120
|
+
def deselect_annotation(animated=false)
|
121
|
+
unless selected_annotation.nil?
|
122
|
+
self.view.deselectAnnotation(selected_annotation, animated:animated)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def add_annotation(annotation)
|
127
|
+
@promotion_annotation_data << MapScreenAnnotation.new(annotation,self.view)
|
128
|
+
self.view.addAnnotation @promotion_annotation_data.last
|
129
|
+
end
|
130
|
+
|
131
|
+
def add_annotations(annotations)
|
132
|
+
@promotion_annotation_data = Array(annotations).map{|a| MapScreenAnnotation.new(a,self.view)}
|
133
|
+
self.view.addAnnotations @promotion_annotation_data
|
134
|
+
end
|
135
|
+
|
136
|
+
def clear_annotations
|
137
|
+
@promotion_annotation_data.each do |a|
|
138
|
+
self.view.removeAnnotation(a)
|
139
|
+
end
|
140
|
+
@promotion_annotation_data = []
|
141
|
+
end
|
142
|
+
|
143
|
+
def annotation_view(map_view, annotation)
|
144
|
+
return if annotation.is_a? RMUserLocation
|
145
|
+
|
146
|
+
params = annotation.params
|
147
|
+
|
148
|
+
identifier = params[:identifier]
|
149
|
+
# Set the pin properties
|
150
|
+
if params[:image]
|
151
|
+
view = RMMarker.alloc.initWithUIImage(params[:image])
|
152
|
+
else
|
153
|
+
pinColor = (PIN_COLORS[params[:pin_color]] || params[:pin_color])
|
154
|
+
view = RMMarker.alloc.initWithMapboxMarkerImage(params[:maki_icon], tintColor: pinColor)
|
155
|
+
end
|
156
|
+
view.annotation = annotation
|
157
|
+
view.canShowCallout = params[:show_callout] if view.respond_to?("canShowCallout=")
|
158
|
+
|
159
|
+
if params[:left_accessory]
|
160
|
+
view.leftCalloutAccessoryView = params[:left_accessory]
|
161
|
+
end
|
162
|
+
if params[:right_accessory]
|
163
|
+
view.rightCalloutAccessoryView = params[:right_accessory]
|
164
|
+
end
|
165
|
+
|
166
|
+
if params[:action]
|
167
|
+
button_type = params[:action_button_type] || UIButtonTypeDetailDisclosure
|
168
|
+
|
169
|
+
action_button = UIButton.buttonWithType(button_type)
|
170
|
+
action_button.addTarget(self, action: params[:action], forControlEvents:UIControlEventTouchUpInside)
|
171
|
+
|
172
|
+
view.rightCalloutAccessoryView = action_button
|
173
|
+
end
|
174
|
+
view
|
175
|
+
end
|
176
|
+
|
177
|
+
def set_start_position(params={})
|
178
|
+
params = {
|
179
|
+
latitude: 37.331789,
|
180
|
+
longitude: -122.029620,
|
181
|
+
radius: 10
|
182
|
+
}.merge(params)
|
183
|
+
initialLocation = CLLocationCoordinate2D.new(params[:latitude], params[:longitude])
|
184
|
+
region = create_region(initialLocation,params[:radius])
|
185
|
+
set_region(region, animated:false)
|
186
|
+
end
|
187
|
+
|
188
|
+
def set_up_start_position
|
189
|
+
if self.class.respond_to?(:get_start_position) && self.class.get_start_position
|
190
|
+
self.set_start_position self.class.get_start_position_params
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
def set_tap_to_add(params={})
|
195
|
+
params = {
|
196
|
+
length: 2.0,
|
197
|
+
target: self,
|
198
|
+
action: "gesture_drop_pin:",
|
199
|
+
annotation: {
|
200
|
+
title: "Dropped Pin",
|
201
|
+
animates_drop: true
|
202
|
+
}
|
203
|
+
}.merge(params)
|
204
|
+
@tap_to_add_annotation_params = params[:annotation]
|
205
|
+
|
206
|
+
lpgr = UILongPressGestureRecognizer.alloc.initWithTarget(params[:target], action:params[:action])
|
207
|
+
lpgr.minimumPressDuration = params[:length]
|
208
|
+
self.view.addGestureRecognizer(lpgr)
|
209
|
+
end
|
210
|
+
|
211
|
+
def gesture_drop_pin(gesture_recognizer)
|
212
|
+
if gesture_recognizer.state == UIGestureRecognizerStateBegan
|
213
|
+
NSNotificationCenter.defaultCenter.postNotificationName("ProMotionMapWillAddPin", object:nil)
|
214
|
+
touch_point = gesture_recognizer.locationInView(self.view)
|
215
|
+
touch_map_coordinate = self.view.convertPoint(touch_point, toCoordinateFromView:self.view)
|
216
|
+
|
217
|
+
add_annotation({
|
218
|
+
coordinate: touch_map_coordinate
|
219
|
+
}.merge(@tap_to_add_annotation_params))
|
220
|
+
NSNotificationCenter.defaultCenter.postNotificationName("ProMotionMapAddedPin", object:@promotion_annotation_data.last)
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
def set_up_tap_to_add
|
225
|
+
if self.class.respond_to?(:get_tap_to_add) && self.class.get_tap_to_add
|
226
|
+
self.set_tap_to_add self.class.get_tap_to_add_params
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
# TODO: Why is this so complex?
|
231
|
+
def zoom_to_fit_annotations(args={})
|
232
|
+
# Preserve backwards compatibility
|
233
|
+
args = {animated: args} if args == true || args == false
|
234
|
+
args = {animated: true, include_user: false}.merge(args)
|
235
|
+
|
236
|
+
ann = args[:include_user] ? (annotations + [user_annotation]).compact : annotations
|
237
|
+
|
238
|
+
#Don't attempt the rezoom of there are no pins
|
239
|
+
return if ann.count == 0
|
240
|
+
|
241
|
+
#Set some crazy boundaries
|
242
|
+
topLeft = CLLocationCoordinate2D.new(-90, 180)
|
243
|
+
bottomRight = CLLocationCoordinate2D.new(90, -180)
|
244
|
+
|
245
|
+
#Find the bounds of the pins
|
246
|
+
ann.each do |a|
|
247
|
+
topLeft.longitude = [topLeft.longitude, a.coordinate.longitude].min
|
248
|
+
topLeft.latitude = [topLeft.latitude, a.coordinate.latitude].max
|
249
|
+
bottomRight.longitude = [bottomRight.longitude, a.coordinate.longitude].max
|
250
|
+
bottomRight.latitude = [bottomRight.latitude, a.coordinate.latitude].min
|
251
|
+
end
|
252
|
+
|
253
|
+
#Find the bounds of all the pins and set the map_view
|
254
|
+
coord = CLLocationCoordinate2D.new(
|
255
|
+
topLeft.latitude - (topLeft.latitude - bottomRight.latitude) * 0.5,
|
256
|
+
topLeft.longitude + (bottomRight.longitude - topLeft.longitude) * 0.5
|
257
|
+
)
|
258
|
+
|
259
|
+
# Add some padding to the edges
|
260
|
+
span = MKCoordinateSpanMake(
|
261
|
+
((topLeft.latitude - bottomRight.latitude) * 1.075).abs,
|
262
|
+
((bottomRight.longitude - topLeft.longitude) * 1.075).abs
|
263
|
+
)
|
264
|
+
|
265
|
+
region = MKCoordinateRegionMake(coord, span)
|
266
|
+
fits = self.view.regionThatFits(region)
|
267
|
+
|
268
|
+
set_region(fits, animated: args[:animated])
|
269
|
+
end
|
270
|
+
|
271
|
+
def set_region(region, animated=true)
|
272
|
+
self.view.zoomWithLatitudeLongitudeBoundsSouthWest(
|
273
|
+
region[:southWest],
|
274
|
+
northEast: region[:northEast],
|
275
|
+
animated: animated
|
276
|
+
)
|
277
|
+
end
|
278
|
+
|
279
|
+
def deg_to_rad(angle)
|
280
|
+
angle*Math::PI/180
|
281
|
+
end
|
282
|
+
|
283
|
+
def rad_to_deg(angle)
|
284
|
+
angle*180/Math::PI
|
285
|
+
end
|
286
|
+
|
287
|
+
# Input coordinates and bearing in decimal degrees, distance in kilometers
|
288
|
+
def point_from_location_bearing_and_distance(initialLocation, bearing, distance)
|
289
|
+
distance = distance / 6371.01 # Convert to angular radians dividing by the Earth radius
|
290
|
+
bearing = deg_to_rad(bearing)
|
291
|
+
input_latitude = deg_to_rad(initialLocation.latitude)
|
292
|
+
input_longitude = deg_to_rad(initialLocation.longitude)
|
293
|
+
|
294
|
+
output_latitude = Math.asin(
|
295
|
+
Math.sin(input_latitude) * Math.cos(distance) +
|
296
|
+
Math.cos(input_latitude) * Math.sin(distance) *
|
297
|
+
Math.cos(bearing)
|
298
|
+
)
|
299
|
+
|
300
|
+
dlon = input_longitude + Math.atan2(
|
301
|
+
Math.sin(bearing) * Math.sin(distance) *
|
302
|
+
Math.cos(input_longitude), Math.cos(distance) -
|
303
|
+
Math.sin(input_longitude) * Math.sin(output_latitude)
|
304
|
+
)
|
305
|
+
|
306
|
+
output_longitude = (dlon + 3*Math::PI) % (2*Math::PI) - Math::PI
|
307
|
+
CLLocationCoordinate2DMake(rad_to_deg(output_latitude), rad_to_deg(output_longitude))
|
308
|
+
end
|
309
|
+
|
310
|
+
def create_region(initialLocation,radius=10)
|
311
|
+
return nil unless initialLocation.is_a? CLLocationCoordinate2D
|
312
|
+
radius = radius * 1.820 # Meters equivalent to 1 Nautical Mile
|
313
|
+
southWest = self.point_from_location_bearing_and_distance(initialLocation,225, radius)
|
314
|
+
northEast = self.point_from_location_bearing_and_distance(initialLocation,45, radius)
|
315
|
+
{:southWest => southWest, :northEast => northEast}
|
316
|
+
end
|
317
|
+
alias_method :region, :create_region
|
318
|
+
|
319
|
+
def look_up_address(args={}, &callback)
|
320
|
+
args[:address] = args if args.is_a? String # Assume if a string is passed that they want an address
|
321
|
+
|
322
|
+
geocoder = CLGeocoder.new
|
323
|
+
return geocoder.geocodeAddressDictionary(args[:address], completionHandler: callback) if args[:address].is_a?(Hash)
|
324
|
+
return geocoder.geocodeAddressString(args[:address].to_s, completionHandler: callback) unless args[:region]
|
325
|
+
return geocoder.geocodeAddressString(args[:address].to_s, inRegion:args[:region].to_s, completionHandler: callback) if args[:region]
|
326
|
+
end
|
327
|
+
|
328
|
+
def look_up_location(location, &callback)
|
329
|
+
location = CLLocation.alloc.initWithLatitude(location.latitude, longitude:location.longitude) if location.is_a?(CLLocationCoordinate2D)
|
330
|
+
|
331
|
+
if location.kind_of?(CLLocation)
|
332
|
+
geocoder = CLGeocoder.new
|
333
|
+
geocoder.reverseGeocodeLocation(location, completionHandler: callback)
|
334
|
+
else
|
335
|
+
PM.logger.info("You're trying to reverse geocode something that isn't a CLLocation")
|
336
|
+
callback.call nil, nil
|
337
|
+
end
|
338
|
+
end
|
339
|
+
|
340
|
+
########## Mapbox methods #################
|
341
|
+
def mapView(map_view, layerForAnnotation: annotation)
|
342
|
+
annotation_view(map_view, annotation)
|
343
|
+
end
|
344
|
+
|
345
|
+
########## Cocoa touch methods #################
|
346
|
+
def mapView(map_view, didUpdateUserLocation:userLocation)
|
347
|
+
if self.respond_to?(:on_user_location)
|
348
|
+
on_user_location(userLocation)
|
349
|
+
else
|
350
|
+
PM.logger.info "You're tracking the user's location but have not implemented the #on_user_location(location) method in MapScreen #{self.class.to_s}."
|
351
|
+
end
|
352
|
+
end
|
353
|
+
|
354
|
+
def mapView(map_view, regionWillChangeAnimated:animated)
|
355
|
+
if self.respond_to?("will_change_region:")
|
356
|
+
will_change_region(animated)
|
357
|
+
elsif self.respond_to?(:will_change_region)
|
358
|
+
will_change_region
|
359
|
+
end
|
360
|
+
end
|
361
|
+
|
362
|
+
def mapView(map_view, regionDidChangeAnimated:animated)
|
363
|
+
if self.respond_to?("on_change_region:")
|
364
|
+
on_change_region(animated)
|
365
|
+
elsif self.respond_to?(:on_change_region)
|
366
|
+
on_change_region
|
367
|
+
end
|
368
|
+
end
|
369
|
+
|
370
|
+
########## Cocoa touch Ruby counterparts #################
|
371
|
+
|
372
|
+
def deceleration_mode
|
373
|
+
map.decelerationMode
|
374
|
+
end
|
375
|
+
|
376
|
+
def deceleration_mode=(mode)
|
377
|
+
map.decelerationMode = mode
|
378
|
+
end
|
379
|
+
|
380
|
+
%w(dragging bouncing clustering).each do |meth|
|
381
|
+
define_method("#{meth}_enabled?") do
|
382
|
+
map.send("#{meth}Enabled")
|
383
|
+
end
|
384
|
+
|
385
|
+
define_method("#{meth}_enabled=") do |argument|
|
386
|
+
map.send("#{meth}Enabled=", argument)
|
387
|
+
end
|
388
|
+
end
|
389
|
+
|
390
|
+
module MapClassMethods
|
391
|
+
# Start Position
|
392
|
+
def start_position(params={})
|
393
|
+
@start_position_params = params
|
394
|
+
@start_position = true
|
395
|
+
end
|
396
|
+
|
397
|
+
def get_start_position_params
|
398
|
+
@start_position_params ||= nil
|
399
|
+
end
|
400
|
+
|
401
|
+
def get_start_position
|
402
|
+
@start_position ||= false
|
403
|
+
end
|
404
|
+
|
405
|
+
# Tap to drop pin
|
406
|
+
def tap_to_add(params={})
|
407
|
+
@tap_to_add_params = params
|
408
|
+
@tap_to_add = true
|
409
|
+
end
|
410
|
+
|
411
|
+
def get_tap_to_add_params
|
412
|
+
@tap_to_add_params ||= nil
|
413
|
+
end
|
414
|
+
|
415
|
+
def get_tap_to_add
|
416
|
+
@tap_to_add ||= false
|
417
|
+
end
|
418
|
+
|
419
|
+
# Mapbox setup
|
420
|
+
def mapbox_setup(params={})
|
421
|
+
@mapbox_setup_params = params
|
422
|
+
@mapbox_setup = true
|
423
|
+
end
|
424
|
+
|
425
|
+
def get_mapbox_setup_params
|
426
|
+
@mapbox_setup_params ||= nil
|
427
|
+
end
|
428
|
+
|
429
|
+
def get_mapbox_setup
|
430
|
+
@mapbox_setup ||= false
|
431
|
+
end
|
432
|
+
|
433
|
+
|
434
|
+
end
|
435
|
+
def self.included(base)
|
436
|
+
base.extend(MapClassMethods)
|
437
|
+
end
|
438
|
+
|
439
|
+
private
|
440
|
+
|
441
|
+
def location_manager
|
442
|
+
@location_manager ||= CLLocationManager.alloc.init
|
443
|
+
@location_manager.delegate ||= self
|
444
|
+
@location_manager
|
445
|
+
end
|
446
|
+
|
447
|
+
end
|
448
|
+
end
|
metadata
ADDED
@@ -0,0 +1,120 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ProMotion-mapbox
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Diogo Andre
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-04-13 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: ProMotion
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ~>
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '2.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ~>
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '2.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: motion-cocoapods
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: motion-stump
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ~>
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0.3'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ~>
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0.3'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: motion-redgreen
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0.1'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ~>
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0.1'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rake
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - '>='
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - '>='
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
description: Adds PM::MapScreen support to ProMotion, using Mapbox as map provider.
|
84
|
+
email:
|
85
|
+
- diogo@regattapix.com
|
86
|
+
executables: []
|
87
|
+
extensions: []
|
88
|
+
extra_rdoc_files: []
|
89
|
+
files:
|
90
|
+
- README.md
|
91
|
+
- lib/ProMotion/map/map_screen.rb
|
92
|
+
- lib/ProMotion/map/map_screen_annotation.rb
|
93
|
+
- lib/ProMotion/map/map_screen_module.rb
|
94
|
+
- lib/ProMotion-mapbox.rb
|
95
|
+
homepage: https://github.com/diogoandre/ProMotion-mapbox
|
96
|
+
licenses:
|
97
|
+
- MIT
|
98
|
+
metadata: {}
|
99
|
+
post_install_message:
|
100
|
+
rdoc_options: []
|
101
|
+
require_paths:
|
102
|
+
- lib
|
103
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
104
|
+
requirements:
|
105
|
+
- - '>='
|
106
|
+
- !ruby/object:Gem::Version
|
107
|
+
version: '0'
|
108
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
109
|
+
requirements:
|
110
|
+
- - '>='
|
111
|
+
- !ruby/object:Gem::Version
|
112
|
+
version: '0'
|
113
|
+
requirements: []
|
114
|
+
rubyforge_project:
|
115
|
+
rubygems_version: 2.0.6
|
116
|
+
signing_key:
|
117
|
+
specification_version: 4
|
118
|
+
summary: Adds PM::MapScreen support to ProMotion, using Mapbox as map provider. Forked
|
119
|
+
from Promotion-map
|
120
|
+
test_files: []
|