File Coverage

blib/lib/Geo/Leaflet.pm
Criterion Covered Total %
statement 127 141 90.0
branch 27 40 67.5
condition n/a
subroutine 33 36 91.6
pod 24 24 100.0
total 211 241 87.5


line stmt bran cond sub pod time code
1             package Geo::Leaflet;
2 2     2   247336 use strict;
  2         5  
  2         79  
3 2     2   9 use warnings;
  2         4  
  2         122  
4 2     2   10 use base qw{Package::New};
  2         4  
  2         1325  
5 2     2   1545 use Geo::Leaflet::tileLayer;
  2         7  
  2         83  
6 2     2   1059 use Geo::Leaflet::marker;
  2         9  
  2         83  
7 2     2   1010 use Geo::Leaflet::circle;
  2         8  
  2         75  
8 2     2   1008 use Geo::Leaflet::polygon;
  2         7  
  2         74  
9 2     2   1038 use Geo::Leaflet::polyline;
  2         6  
  2         86  
10 2     2   1045 use Geo::Leaflet::rectangle;
  2         7  
  2         79  
11 2     2   931 use Geo::Leaflet::icon;
  2         10  
  2         78  
12 2     2   14 use JSON::XS qw{};
  2         4  
  2         36  
13 2     2   1080 use HTML::Tiny qw{};;
  2         7749  
  2         3962  
14              
15             our $VERSION = '0.03';
16             our $PACKAGE = __PACKAGE__;
17             our @OBJECTS = ();
18             our @ICONS = ();
19              
20             =head1 NAME
21              
22             Geo::Leaflet - Generates a Leaflet JavaScript map web page
23              
24             =head1 SYNOPSIS
25              
26             use Geo::Leaflet;
27             my $map = Geo::Leaflet->new;
28             print $map->html;
29              
30             =head1 DESCRIPTION
31              
32             This package generates a L map web page.
33              
34             =head1 CONSTRUCTORS
35              
36             =head2 new
37              
38             Returns a map object
39              
40             my $map = Geo::Leaflet->new(
41             id => "map",
42             center => [$lat, $lon],
43             zoom => 13,
44             );
45              
46             =head1 MAP PROPERTIES
47              
48             =head2 id
49              
50             Sets and returns the html id of the map.
51              
52             Default: "map"
53              
54             =cut
55              
56             sub id {
57 2     2 1 5 my $self = shift;
58 2 50       7 $self->{'id'} = shift if @_;
59 2 100       7 $self->{'id'} = 'map' unless defined $self->{'id'};
60 2         15 return $self->{'id'};
61             }
62              
63             =head2 center
64              
65             Sets and returns the center of the map.
66              
67             $map->center([$lat, $lon]);
68             my $center = $map->center;
69              
70             Default: [38.2, -97.2]
71              
72             =cut
73              
74             sub center {
75 3     3 1 214061 my $self = shift;
76 3 50       12 $self->{'center'} = shift if @_;
77 3 100       12 $self->{'center'} = [38.2, -97.2] unless defined $self->{'center'};
78 3         18 my $error_template = "Error: $PACKAGE center expected %s (e.g., [\$lat, \$lon])";
79 3 50       11 die(sprintf($error_template, 'array reference')) unless ref($self->{'center'}) eq 'ARRAY';
80 3 50       6 die(sprintf($error_template, 'two elements' )) unless @{$self->{'center'}} == 2;
  3         10  
81 3         22 return $self->{'center'};
82             }
83              
84             =head2 zoom
85              
86             Sets and returns the zoom of the map.
87              
88             $map->zoom(4.5);
89             my $zoom = $map->zoom;
90              
91             Default: 4.5
92              
93             =cut
94              
95             sub zoom {
96 2     2 1 4 my $self = shift;
97 2 50       7 $self->{'zoom'} = shift if @_;
98 2 100       19 $self->{'zoom'} = 4.5 unless defined $self->{'zoom'};
99 2         21 return $self->{'zoom'};
100             }
101              
102             =head2 setView
103              
104             Sets the center and zoom of the map and returns the map object (i.e., matches leaflet.js interface).
105              
106             $map->setView([51.505, -0.09], 13);
107              
108             =cut
109              
110             sub setView {
111 1     1 1 2 my $self = shift;
112 1         1 my $center = shift;
113 1         1 my $zoom = shift;
114 1 50       4 $self->center($center) if defined $center;
115 1 50       2 $self->zoom($zoom) if defined $zoom;
116 1         3 return $self;
117             }
118              
119             =head2 width
120              
121             Sets and returns the percent or pixel width of the map.
122              
123             $map->width('600px');
124             $map->width('100%');
125             my $width = $map->width;
126              
127             Default: 100%
128              
129             =cut
130              
131             sub width {
132 2     2 1 4 my $self = shift;
133 2 50       26 $self->{'width'} = shift if @_;
134 2 100       7 $self->{'width'} = '100%' unless defined $self->{'width'};
135 2         8 return $self->{'width'};
136             }
137              
138             =head2 height
139              
140             Sets and returns the percent or pixel height of the map.
141              
142             $map->height('400px');
143             $map->height('100%');
144             my $height = $map->height;
145              
146             Default: 100%
147              
148             =cut
149              
150             sub height {
151 2     2 1 5 my $self = shift;
152 2 50       5 $self->{'height'} = shift if @_;
153 2 100       7 $self->{'height'} = '100%' unless defined $self->{'height'};
154 2         10 return $self->{'height'};
155             }
156              
157             =head1 HTML PROPERTIES
158              
159             =head2 title
160              
161             Sets and returns the HTML title.
162              
163             Default: "Leaflet Map"
164              
165             =cut
166              
167             sub title {
168 1     1 1 3 my $self = shift;
169 1 50       3 $self->{'title'} = shift if @_;
170 1 50       5 $self->{'title'} = 'Leaflet Map' unless defined $self->{'title'};
171 1         5 return $self->{'title'};
172             }
173              
174             =head1 TILE LAYER CONSTRUCTOR
175              
176             =head2 tileLayer
177              
178             Creates and returns a tileLayer object which is added to the map.
179              
180             $map->tileLayer(
181             url => 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
182             options => {
183             maxZoom => 19,
184             attribution => '© OpenStreetMap',
185             },
186             );
187              
188             Default: OpenStreetMaps
189              
190             See: L
191              
192             =cut
193              
194             sub tileLayer {
195 3     3 1 232317 my $self = shift;
196 3 50       11 $self->{'tileLayer'} = Geo::Leaflet::tileLayer->new(@_, JSON=>$self->JSON) if @_;
197 3 100       16 $self->{'tileLayer'} = Geo::Leaflet::tileLayer->osm((), JSON=>$self->JSON) unless defined $self->{'tileLayer'};
198 3         52 return $self->{'tileLayer'};
199             }
200              
201             =head1 ICON CONSTRUCTOR
202              
203             =head2 icon
204              
205             my $icon = $map->icon(
206             name => "my_icon", #must be a valid JavaScript variable name
207             options => {
208             iconUrl => "my-icon.png",
209             iconSize => [38, 95],
210             iconAnchor => [22, 94],
211             popupAnchor => [-3, -76],
212             shadowUrl => "my-icon-shadow.png",
213             shadowSize => [68, 95],
214             shadowAnchor => [22, 94],
215             }
216             );
217              
218             See: L
219              
220             =cut
221              
222             sub icon {
223 0     0 1 0 my $self = shift;
224 0         0 my $icon = Geo::Leaflet::icon->new(@_, JSON=>$self->JSON);
225 0         0 push @ICONS, $icon;
226 0         0 return $icon;
227             }
228              
229             =head1 MAP OBJECT CONSTRUCTORS
230              
231             =head2 marker
232              
233             Adds a marker object to the map and returns a reference to the marker object.
234              
235             $map->marker(lat=>$lat, lon=>$lon);
236              
237             See: L
238              
239             =cut
240              
241             sub marker {
242 1     1 1 501 my $self = shift;
243 1         5 my $marker = Geo::Leaflet::marker->new(@_, JSON=>$self->JSON);
244 1         33 push @OBJECTS, $marker;
245 1         4 return $marker;
246             }
247              
248             =head2 polyline
249              
250             Adds a polyline object to the map and returns a reference to the polyline object.
251              
252             my $latlngs = [[$lat, $lon], ...]
253             $map->polyline(coordinates=>$latlngs, options=>{});
254              
255             See: L
256              
257             =cut
258              
259             sub polyline {
260 0     0 1 0 my $self = shift;
261 0         0 my $polyline = Geo::Leaflet::polyline->new(@_, JSON=>$self->JSON);
262 0         0 push @OBJECTS, $polyline;
263 0         0 return $polyline;
264             }
265              
266             =head2 polygon
267              
268             Adds a polygon object to the map and returns a reference to the polygon object.
269              
270             my $latlngs = [[$lat, $lon], ...]
271             $map->polygon(coordinates=>$latlngs, options=>{});
272              
273             See: L
274              
275             =cut
276              
277             sub polygon {
278 1     1 1 1416 my $self = shift;
279 1         6 my $polygon = Geo::Leaflet::polygon->new(@_, JSON=>$self->JSON);
280 1         48 push @OBJECTS, $polygon;
281 1         3 return $polygon;
282             }
283              
284             =head2 rectangle
285              
286             Adds a rectangle object to the map and returns a reference to the rectangle object.
287              
288             $map->rectangle(llat => $llat,
289             llon => $llon,
290             ulat => $ulat,
291             ulon => $ulon,
292             options => {});
293              
294             See: L
295              
296             =cut
297              
298             sub rectangle {
299 0     0 1 0 my $self = shift;
300 0         0 my $rectangle = Geo::Leaflet::rectangle->new(@_, JSON=>$self->JSON);
301 0         0 push @OBJECTS, $rectangle;
302 0         0 return $rectangle;
303             }
304              
305             =head2 circle
306              
307             Adds a circle object to the map and returns a reference to the circle object.
308              
309             $map->circle(lat=>$lat, lon=>$lon, radius=>$radius, options=>{});
310              
311             See: L
312              
313             =cut
314              
315             sub circle {
316 1     1 1 2462 my $self = shift;
317 1         5 my $circle = Geo::Leaflet::circle->new(@_, JSON=>$self->JSON);
318 1         51 push @OBJECTS, $circle;
319 1         4 return $circle;
320             }
321              
322             =head1 METHODS
323              
324             =head2 html
325              
326             =cut
327              
328             sub html {
329 1     1 1 2337 my $self = shift;
330 1         7 my $html = $self->HTML;
331 1         5 return $html->html([
332             $html->head([
333             $html->title($self->title),
334             $self->html_head_link,
335             $self->html_head_script,
336             $self->html_head_style,
337             ]),
338             $html->body([
339             $self->html_body_div,
340             $self->html_body_script
341             ]),
342             ]);
343             }
344              
345             =head2 html_head_link
346              
347             =cut
348              
349             sub html_head_link {
350 1     1 1 93 my $self = shift;
351 1         4 my $html = $self->HTML;
352 1         9 return $html->link({
353             rel => 'stylesheet',
354             href => 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css',
355             integrity => 'sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=',
356             crossorigin => '',
357             });
358             }
359              
360             =head2 html_head_script
361              
362             =cut
363              
364             sub html_head_script {
365 1     1 1 158 my $self = shift;
366 1         3 my $html = $self->HTML;
367 1         8 return $html->script({
368             src => 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js',
369             integrity => 'sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=',
370             crossorigin => '',
371             }, ''); #empty script required
372             }
373              
374             =head2 html_head_style
375              
376             =cut
377              
378             sub html_head_style {
379 1     1 1 188 my $self = shift;
380 1         4 my $html = $self->HTML;
381 1         4 my $style_size = sprintf('width: %s; height: %s;', $self->width, $self->height);
382 1         4 my $style_full = 'html, body { height: 100%; margin: 0; } '.
383             ".leaflet-container { $style_size max-width: 100%; max-height: 100%; }";
384 1         5 return $html->style({}, $style_full);
385             }
386              
387             =head2 html_body_div
388              
389             =cut
390              
391             sub html_body_div {
392 1     1 1 192 my $self = shift;
393 1         4 my $html = $self->HTML;
394 1         3 my $style_size = sprintf('width: %s; height: %s;', $self->width, $self->height);
395 1         5 return $html->div({id => $self->id, style => $style_size});
396             }
397              
398             =head2 html_body_script
399              
400             =cut
401              
402             sub html_body_script {
403 1     1 1 111 my $self = shift;
404 1         3 my $html = $self->HTML;
405 1         33 return $html->script({}, $self->html_body_script_contents);
406             }
407              
408             =head2 html_body_script_map
409              
410             =cut
411              
412             sub html_body_script_map {
413 1     1 1 3 my $self = shift;
414 1         3 return sprintf(q{const map = L.map(%s).setView(%s, %s);},
415             $self->JSON->encode($self->id),
416             $self->JSON->encode($self->center),
417             $self->JSON->encode($self->zoom),
418             );
419             }
420              
421             =head2 html_body_script_contents
422              
423             =cut
424              
425             sub html_body_script_contents {
426 1     1 1 4 my $self = shift;
427 1         3 my $empty = '';
428 1         3 my @commands = (
429             $empty,
430             $empty,
431             $self->html_body_script_map,
432             $self->tileLayer->stringify,
433             $empty,
434             );
435 1         5 foreach my $icon (@ICONS) {
436 0         0 my $name = $icon->name;
437 0         0 push @commands, "const $name = " . $icon->stringify;
438             }
439 1         2 my $loop = 0;
440 1         3 foreach my $object (@OBJECTS) {
441 3         6 $loop++;
442 3         19 push @commands, "const object$loop = " . $object->stringify;
443             }
444 1         3 push @commands, $empty;
445              
446 1         4 return join $empty, map {" $_\n"} @commands;
  9         28  
447             }
448              
449             =head1 OBJECT ACCESSORS
450              
451             =head2 HTML
452              
453             Returns an L object to generate HTML.
454              
455             =cut
456              
457             sub HTML {
458 6     6 1 11 my $self = shift;
459 6 50       20 $self->{'HTML'} = shift if @_;
460 6 100       28 $self->{'HTML'} = HTML::Tiny->new unless defined $self->{'HTML'};
461 6         84 return $self->{'HTML'};
462             }
463              
464             =head2 JSON
465              
466             Returns a L object to generate JSON.
467              
468             =cut
469              
470             sub JSON {
471 8     8 1 16 my $self = shift;
472 8         58 $self->{'JSON'} = JSON::XS->new->allow_nonref;
473 8         74 return $self->{'JSON'};
474             }
475              
476             =head1 SEE ALSO
477              
478             L
479             https://leafletjs.com/
480              
481             =head1 AUTHOR
482              
483             Michael R. Davis
484              
485             =head1 COPYRIGHT AND LICENSE
486              
487             Copyright (C) 2024 by Michael R. Davis
488              
489             MIT LICENSE
490              
491             =cut
492              
493             1;