imgViewer2-rails 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: e54354db6de17ffc21443d2bf2b7ff8dcd6d05b7
4
+ data.tar.gz: f14a502111ee8b177f93db26f0df0ff1dd2a3032
5
+ SHA512:
6
+ metadata.gz: 6fc82488afc230471a34d0b25320b65030aa9e0d4e0d7b050773ab4d3e6d24e4002f05318405dbe603c1e0255d2eb3a022c62b1e4418c8e64bd5139cfcac1062
7
+ data.tar.gz: e94c7f1941139d4d483be60af1ddff9601bdb4955b860d7c674bde826d695b8e03cee7a6d5e92c7c163f6e6df2ebb53e38a3ee594835f96f7e43bbbcc44a0055
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2017 Wayne Mogg (https://github.com/waynegm)
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.
data/README.md ADDED
@@ -0,0 +1,34 @@
1
+ imgViewer2-Rails [![Gem Version][version-badge]][rubygems]
2
+ ===================
3
+
4
+ [imgViewer2](https://github.com/waynegm/imgViewer2), Extensible and responsive jQuery plugin to zoom and pan images based on the Leaflet mapping library rails wrap.
5
+
6
+ ## Installation
7
+
8
+ Add this line to your application's Gemfile:
9
+
10
+ gem 'imgViewer2-rails'
11
+
12
+ And then execute:
13
+
14
+ $ bundle
15
+
16
+ ## How to use
17
+
18
+ Add to your JavaScript manifest file (application.js):
19
+
20
+ ```js
21
+ //= require leaflet
22
+ //= require imgViewer2
23
+ ```
24
+
25
+ Add to your application-wide CSS file (application.css):
26
+
27
+ ```css
28
+ = require leaflet
29
+ ```
30
+
31
+ Visit [official doc](https://github.com/waynegm/imgViewer2#documentation)
32
+
33
+ [version-badge]: https://badge.fury.io/rb/imgViewer2-rails.svg
34
+ [rubygems]: https://rubygems.org/gems/imgViewer2-rails
@@ -0,0 +1,11 @@
1
+ require 'imgViewer2-rails/version'
2
+ require 'leaflet-rails'
3
+
4
+ module Jquery
5
+ module ImgViewer2
6
+ module Rails
7
+ class Engine < ::Rails::Engine
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,5 @@
1
+ module Jquery
2
+ module ImgViewer2
3
+ VERSION = '1.1.0'.freeze
4
+ end
5
+ end
@@ -0,0 +1,500 @@
1
+ /*
2
+ * imgViewer2
3
+ *
4
+ *
5
+ * Copyright (c) 2013 Wayne Mogg
6
+ * Licensed under the MIT license.
7
+ */
8
+
9
+
10
+
11
+ var waitForFinalEvent = (function () {
12
+ var timers = {};
13
+ return function (callback, ms, uniqueId) {
14
+ if (!uniqueId) {
15
+ uniqueId = "Don't call this twice without a uniqueId";
16
+ }
17
+ if (timers[uniqueId]) {
18
+ clearTimeout (timers[uniqueId]);
19
+ }
20
+ timers[uniqueId] = setTimeout(callback, ms);
21
+ };
22
+ })();
23
+ /*
24
+ * imgViewer2 plugin starts here
25
+ */
26
+ ;(function($) {
27
+ $.widget("wgm.imgViewer2", {
28
+ options: {
29
+ zoomStep: 0.5,
30
+ zoomMax: undefined,
31
+ zoomable: true,
32
+ dragable: true,
33
+ onClick: $.noop,
34
+ onReady: $.noop
35
+ },
36
+
37
+ _create: function() {
38
+ var self = this;
39
+ if (!$(this.element).is("img")) {
40
+ var elem = this.element.children()[0];
41
+ if (!$(elem).is("img")) {
42
+ $.error('imgviewer plugin can only be applied to img elements');
43
+ } else {
44
+ self.img = self.element.children()[0];
45
+ }
46
+ } else {
47
+ self.img = self.element[0];
48
+ }
49
+ // the original img element
50
+ var $img = $(self.img);
51
+ /*
52
+ * a copy of the original image to be positioned over it and manipulated to
53
+ * provide zoom and pan
54
+ */
55
+ self.view = $("<div class='viewport' />").uniqueId().appendTo("body");
56
+ var $view = $(self.view);
57
+ self.map = {};
58
+ self.bounds = {};
59
+ // a flag used to check the target image has loaded
60
+ self.ready = false;
61
+ self.resize = false;
62
+ $img.one("load",function() {
63
+ // get and some geometry information about the image
64
+ self.ready = true;
65
+ var width = $img.width(),
66
+ height = $img.height(),
67
+ offset = $img.offset();
68
+ // cache the image padding information
69
+ self.offsetPadding = {
70
+ top: parseInt($img.css('padding-top'),10),
71
+ left: parseInt($img.css('padding-left'),10),
72
+ right: parseInt($img.css('padding-right'),10),
73
+ bottom: parseInt($img.css('padding-bottom'),10)
74
+ };
75
+ /*
76
+ * cache the image margin/border size information
77
+ * because of IE8 limitations left and right borders are assumed to be the same width
78
+ * and likewise top and bottom borders
79
+ */
80
+ self.offsetBorder = {
81
+ x: Math.round(($img.outerWidth()-$img.innerWidth())/2),
82
+ y: Math.round(($img.outerHeight()-$img.innerHeight())/2)
83
+ };
84
+ /*
85
+ * define the css style for the view container using absolute positioning to
86
+ * put it directly over the original image
87
+ */
88
+ var vTop = offset.top + self.offsetBorder.y + self.offsetPadding.top,
89
+ vLeft = offset.left + self.offsetBorder.x + self.offsetPadding.left;
90
+
91
+ $view.css({
92
+ position: "absolute",
93
+ overflow: "hidden",
94
+ top: vTop+"px",
95
+ left: vLeft+"px",
96
+ width: width+"px",
97
+ height: height+"px"
98
+ });
99
+ // add the leaflet map
100
+ self.bounds = L.latLngBounds(L.latLng(0,0), L.latLng(self.img.naturalHeight,self.img.naturalWidth));
101
+ self.map = L.map($view.attr('id'), {crs:L.CRS.Simple,
102
+ minZoom: -10,
103
+ trackresize: false,
104
+ maxBoundsViscosity: 1.0,
105
+ attributionControl: false,
106
+ inertia: false,
107
+ zoomSnap: 0,
108
+ wheelPxPerZoomLevel: Math.round(36/self.options.zoomStep),
109
+ zoomDelta: self.options.zoomStep
110
+ });
111
+ self.zimg = L.imageOverlay(self.img.src, self.bounds).addTo(self.map);
112
+ self.map.options.minZoom = self.map.getBoundsZoom(self.bounds,false);
113
+ self.map.fitBounds(self.bounds);
114
+ self.bounds = self.map.getBounds();
115
+ self.map.setMaxBounds(self.bounds);
116
+ if (self.options.zoomMax !== null) {
117
+ var lzoom = self.leafletZoom(self.options.zoomMax);
118
+ if (lzoom < self.map.getZoom()) {
119
+ self.map.setZoom(lzoom);
120
+ }
121
+ self.map.options.maxZoom = lzoom;
122
+ }
123
+ if (!self.options.dragable) {
124
+ self.map.dragging.disable();
125
+ }
126
+ if (!self.options.zoomable) {
127
+ self.map.zoomControl.disable();
128
+ self.map.boxZoom.disable();
129
+ self.map.touchZoom.disable();
130
+ self.map.doubleClickZoom.disable();
131
+ self.map.scrollWheelZoom.disable();
132
+ }
133
+ self.map.on('click', function(ev) {
134
+ if (self.options.onClick !== null) {
135
+ self.options.onClick.call(self, ev.originalEvent, self.eventToImg(ev));
136
+ }
137
+ });
138
+ self.map.on('zoomend', function() {
139
+ if (self.options.zoomMax >= 1 && this.getZoom() > this.options.zoomMax) {
140
+ this.setZoom(this.options.zoomMax);
141
+ }
142
+ if (!self.resize) {
143
+ self.bounds = self.map.getBounds();
144
+ }
145
+ });
146
+ self.map.on('moveend', function() {
147
+ if (!self.resize) {
148
+ self.bounds = self.map.getBounds();
149
+ }
150
+ });
151
+ self.map.on('resize', function() {
152
+ self.map.options.minZoom = -10;
153
+ self.map.fitBounds(self.bounds,{animate:false});
154
+ self.map.options.minZoom = self.map.getBoundsZoom(L.latLngBounds(L.latLng(0,0), L.latLng(self.img.naturalHeight,self.img.naturalWidth)),true);
155
+ self.map.options.maxZoom = self.leafletZoom(self.options.zoomMax);
156
+ waitForFinalEvent(function(){
157
+ self.resize = false;
158
+ self._view_resize();
159
+ self.map.options.minZoom = -10;
160
+ self.map.fitBounds(self.bounds,{animate:false});
161
+ self.map.options.minZoom = self.map.getBoundsZoom(L.latLngBounds(L.latLng(0,0), L.latLng(self.img.naturalHeight,self.img.naturalWidth)),true);
162
+ self.map.options.maxZoom = self.leafletZoom(self.options.zoomMax);
163
+ }, 300, $img[0].id);
164
+ });
165
+ self.options.onReady.call(self);
166
+ }).each(function() {
167
+ if (this.complete) { $(this).trigger("load"); }
168
+ });
169
+ /*
170
+ /*
171
+ * Window resize handler
172
+ */
173
+ $(window).resize(function() {
174
+ if (self.ready) {
175
+ self.resize = true;
176
+ self._view_resize();
177
+ self.map.invalidateSize({animate: false});
178
+ }
179
+ });
180
+ },
181
+ /*
182
+ * View resize - the aim is to keep the view centered on the same location in the original image
183
+ */
184
+ _view_resize: function() {
185
+ if (this.ready) {
186
+ var $view = $(this.view),
187
+ $img = $(this.img),
188
+ width = $img.width(),
189
+ height = $img.height(),
190
+ offset = $img.offset(),
191
+ vTop = Math.round(offset.top + this.offsetBorder.y + this.offsetPadding.top),
192
+ vLeft = Math.round(offset.left + this.offsetBorder.x + this.offsetPadding.left);
193
+ $view.css({
194
+ top: vTop+"px",
195
+ left: vLeft+"px",
196
+ width: width+"px",
197
+ height: height+"px"
198
+ });
199
+ }
200
+ },
201
+ /*
202
+ * Remove the plugin
203
+ */
204
+ destroy: function() {
205
+ $(window).unbind("resize");
206
+ this.map.remove();
207
+ $(this.view).remove();
208
+ $.Widget.prototype.destroy.call(this);
209
+ },
210
+
211
+ _setOption: function(key, value) {
212
+ switch(key) {
213
+ case 'zoomStep':
214
+ if (parseFloat(value) <= 0 || isNaN(parseFloat(value))) {
215
+ return;
216
+ }
217
+ break;
218
+ case 'zoomMax':
219
+ if (parseFloat(value) < 1 || isNaN(parseFloat(value))) {
220
+ return;
221
+ }
222
+ break;
223
+ }
224
+ var version = $.ui.version.split('.');
225
+ if (version[0] > 1 || version[1] > 8) {
226
+ this._super(key, value);
227
+ } else {
228
+ $.Widget.prototype._setOption.apply(this, arguments);
229
+ }
230
+ switch(key) {
231
+ case 'zoomStep':
232
+ if (this.ready) {
233
+ this.map.options.zoomDelta = this.options.zoomStep;
234
+ this.map.options.wheelPxPerZoomLevel = Math.round(60/this.options.zoomStep);
235
+ }
236
+ break;
237
+ case 'zoomMax':
238
+ if (this.ready) {
239
+ lzoom = this.leafletZoom(this.options.zoomMax);
240
+ if (lzoom < this.map.getZoom()) {
241
+ this.map.setZoom(lzoom);
242
+ }
243
+ this.map.options.maxZoom = lzoom;
244
+ this.map.fire('zoomend');
245
+ }
246
+ break;
247
+ case 'zoomable':
248
+ if (this.options.zoomable) {
249
+ this.map.zoomControl.enable();
250
+ this.map.boxZoom.enable();
251
+ this.map.touchZoom.enable();
252
+ this.map.doubleClickZoom.enable();
253
+ this.map.scrollWheelZoom.enable();
254
+ } else {
255
+ this.map.zoomControl.disable();
256
+ this.map.boxZoom.disable();
257
+ this.map.touchZoom.disable();
258
+ this.map.doubleClickZoom.disable();
259
+ this.map.scrollWheelZoom.disable();
260
+ }
261
+ break;
262
+ case 'dragable':
263
+ if (this.options.dragable) {
264
+ this.map.dragging.enable();
265
+ } else {
266
+ this.map.dragging.disable();
267
+ }
268
+ break;
269
+ }
270
+ },
271
+ /*
272
+ * Test if a relative image coordinate is visible in the current view
273
+ */
274
+ isVisible: function(relx, rely) {
275
+ var view = this.getView();
276
+ if (view) {
277
+ return (relx >= view.left && relx <= view.right && rely >= view.top && rely <= view.bottom);
278
+ } else {
279
+ return false;
280
+ }
281
+ },
282
+ /*
283
+ * Convert a user supplied zoom to a Leaflet zoom
284
+ */
285
+ leafletZoom: function(zoom) {
286
+ if (this.ready && zoom !== undefined) {
287
+ var img = this.img,
288
+ map = this.map,
289
+ lzoom = map.getZoom() || 0,
290
+ size = map.getSize(),
291
+ width = img.naturalWidth,
292
+ height = img.naturalHeight,
293
+ nw = L.latLng(height/zoom,width/zoom),
294
+ se = L.latLng(0,0),
295
+ boundsSize = map.project(nw, lzoom).subtract(map.project(se, lzoom));
296
+
297
+ var scale = Math.min(size.x / boundsSize.x, -size.y / boundsSize.y);
298
+ return map.getScaleZoom(scale, lzoom);
299
+ } else {
300
+ return undefined;
301
+ }
302
+ },
303
+ /*
304
+ * Get the Leaflet map object
305
+ */
306
+ getMap: function() {
307
+ if (this.ready) {
308
+ return this.map;
309
+ }
310
+ else {
311
+ return null;
312
+ }
313
+ },
314
+ /*
315
+ * Get current zoom level
316
+ * Returned zoom will always be >=1
317
+ * a zoom of 1 means the entire image is just visible within the viewport
318
+ * a zoom of 2 means half the image is visible in the viewport etc
319
+ */
320
+ getZoom: function() {
321
+ if (this.ready) {
322
+ var img = this.img,
323
+ map = this.map,
324
+ width = img.naturalWidth,
325
+ height = img.naturalHeight,
326
+ constraint = this.options.constraint,
327
+ bounds = map.getBounds();
328
+ if (constraint == 'width' ) {
329
+ return Math.max(1, width/(bounds.getEast()-bounds.getWest()));
330
+ } else if (constraint == 'height') {
331
+ return Math.max(1,height/(bounds.getNorth()-bounds.getSouth()));
332
+ } else {
333
+ return Math.max(1, (width/(bounds.getEast()-bounds.getWest()) + height/(bounds.getNorth()-bounds.getSouth()))/2);
334
+ }
335
+ } else {
336
+ return null;
337
+ }
338
+ },
339
+ /*
340
+ * Set the zoom level
341
+ * Zoom must be >=1
342
+ * a zoom of 1 means the entire image is just visible within the viewport
343
+ * a zoom of 2 means half the image is visible in the viewport etc
344
+ */
345
+ setZoom: function( zoom ) {
346
+ if (this.ready) {
347
+ zoom = Math.max(1, zoom);
348
+ if (this.options.zoomMax !== null) {
349
+ zoom = Math.min(zoom, this.options.zoomMax);
350
+ }
351
+ var img = this.img,
352
+ map = this.map,
353
+ width = img.naturalWidth,
354
+ height = img.naturalHeight,
355
+ constraint = this.options.constraint,
356
+ center = map.getCenter(),
357
+ bounds = map.getBounds();
358
+ var hvw, hvh;
359
+ if (constraint == 'width') {
360
+ hvw = width/zoom/2;
361
+ hvh = hvw * (bounds.getNorth()-bounds.getSouth())/(bounds.getEast()-bounds.getWest());
362
+ } else if (constraint == 'height') {
363
+ hvh = height/zoom/2;
364
+ hvw = hvh * (bounds.getEast()-bounds.getWest())/(bounds.getNorth()-bounds.getSouth());
365
+ } else {
366
+ hvw = width/zoom/2;
367
+ hvh = height/zoom/2;
368
+ }
369
+
370
+ var east = center.lng + hvw,
371
+ west = center.lng - hvw,
372
+ north = center.lat + hvh,
373
+ south = center.lat - hvh;
374
+ if (west<0) {
375
+ east += west;
376
+ west = 0;
377
+ } else if (east > width) {
378
+ west -= east-width;
379
+ east = width;
380
+ }
381
+ if (south<0) {
382
+ north += south;
383
+ south = 0;
384
+ } else if (north > height) {
385
+ south -= north-height;
386
+ north = height;
387
+ }
388
+ map.fitBounds(L.latLngBounds(L.latLng(south,west), L.latLng(north,east)),{animate:false});
389
+ }
390
+ return this;
391
+ },
392
+ /*
393
+ * Get relative image coordinates of current view
394
+ */
395
+ getView: function() {
396
+ if (this.ready) {
397
+ var img = this.img,
398
+ width = img.naturalWidth,
399
+ height = img.naturalHeight,
400
+ bnds = this.map.getBounds();
401
+ return {
402
+ top: 1 - bnds.getNorth()/height,
403
+ left: bnds.getWest()/width,
404
+ bottom: 1 - bnds.getSouth()/height,
405
+ right: bnds.getEast()/width
406
+ };
407
+ } else {
408
+ return null;
409
+ }
410
+ },
411
+ /*
412
+ * Pan the view to be centred at the given relative image location
413
+ */
414
+ panTo: function(relx, rely) {
415
+ if ( this.ready && relx >= 0 && relx <= 1 && rely >= 0 && rely <=1 ) {
416
+ var img = this.img,
417
+ map = this.map,
418
+ bounds = this.bounds,
419
+ // bounds = map.getBounds(),
420
+ east = bounds.getEast(),
421
+ west = bounds.getWest(),
422
+ north = bounds.getNorth(),
423
+ south = bounds.getSouth(),
424
+ centerX = (east+west)/2,
425
+ centerY = (north+south)/2,
426
+ width = img.naturalWidth,
427
+ height = img.naturalHeight,
428
+ newY = (1-rely)*height,
429
+ newX = relx*width;
430
+ east += newX - centerX;
431
+ west += newX - centerX;
432
+ north += newY - centerY;
433
+ south += newY - centerY;
434
+ if (west<0) {
435
+ east -= west;
436
+ west = 0;
437
+ }
438
+ if (east > width) {
439
+ west -= east-width;
440
+ east = width;
441
+ }
442
+ if (south<0) {
443
+ north -= south;
444
+ south = 0;
445
+ }
446
+ if (north > height) {
447
+ south -= north-height;
448
+ north = height;
449
+ }
450
+ map.fitBounds(L.latLngBounds(L.latLng(south,west), L.latLng(north,east)),{animate:false});
451
+ }
452
+ return this;
453
+ },
454
+ /*
455
+ * Return the relative image coordinate for a Leaflet event
456
+ */
457
+ eventToImg: function(ev) {
458
+ if (this.ready) {
459
+ var img = this.img,
460
+ width = img.naturalWidth,
461
+ height = img.naturalHeight;
462
+ relx = ev.latlng.lng/width;
463
+ rely = 1 - ev.latlng.lat/height;
464
+ if (relx>=0 && relx<=1 && rely>=0 && rely<=1) {
465
+ return {x: relx, y: rely};
466
+ } else {
467
+ return null;
468
+ }
469
+ } else {
470
+ return null;
471
+ }
472
+ },
473
+ /*
474
+ * Convert relative image coordinate to Leaflet LatLng point
475
+ */
476
+ relposToLatLng: function(x,y) {
477
+ if (this.ready) {
478
+ var img = this.img,
479
+ width = img.naturalWidth,
480
+ height = img.naturalHeight;
481
+ return L.latLng((1-y)*height, x*width);
482
+ } else {
483
+ return null;
484
+ }
485
+ },
486
+ /*
487
+ * Convert relative image coordinate to Image pixel
488
+ */
489
+ relposToImage: function(pos) {
490
+ if (this.ready) {
491
+ var img = this.img,
492
+ width = img.naturalWidth,
493
+ height = img.naturalHeight;
494
+ return {x: Math.round(pos.x*width), y: Math.round(pos.y*height)};
495
+ } else {
496
+ return null;
497
+ }
498
+ }
499
+ });
500
+ })(jQuery);
metadata ADDED
@@ -0,0 +1,66 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: imgViewer2-rails
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Wayne Mogg
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-12-21 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: leaflet-rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 1.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: 1.2.0
27
+ description: Extensible and responsive jQuery plugin to zoom and pan images based
28
+ on the Leaflet mapping library.
29
+ email:
30
+ - waynegm@gmail.com
31
+ - eric@managebac.com
32
+ executables: []
33
+ extensions: []
34
+ extra_rdoc_files: []
35
+ files:
36
+ - LICENSE
37
+ - README.md
38
+ - lib/imgViewer2-rails.rb
39
+ - lib/imgViewer2-rails/version.rb
40
+ - vendor/assets/javascripts/imgViewer2.js
41
+ homepage: https://github.com/eduvo/imgViewer2-rails
42
+ licenses:
43
+ - MIT
44
+ metadata: {}
45
+ post_install_message:
46
+ rdoc_options: []
47
+ require_paths:
48
+ - lib
49
+ required_ruby_version: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ required_rubygems_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: '0'
59
+ requirements: []
60
+ rubyforge_project:
61
+ rubygems_version: 2.6.14
62
+ signing_key:
63
+ specification_version: 4
64
+ summary: Extensible and responsive jQuery plugin to zoom and pan images based on the
65
+ Leaflet mapping library.
66
+ test_files: []