File Coverage

blib/lib/Geo/Leaflet.pm
Criterion Covered Total %
statement 148 175 84.5
branch 35 56 62.5
condition 0 4 0.0
subroutine 37 41 90.2
pod 28 28 100.0
total 248 304 81.5


line stmt bran cond sub pod time code
1             package Geo::Leaflet;
2 2     2   201119 use strict;
  2         4  
  2         72  
3 2     2   8 use warnings;
  2         3  
  2         126  
4 2     2   14 use base qw{Package::New};
  2         3  
  2         1080  
5 2     2   1493 use Geo::Leaflet::TileLayer;
  2         3  
  2         74  
6 2     2   785 use Geo::Leaflet::Marker;
  2         5  
  2         54  
7 2     2   806 use Geo::Leaflet::Circle;
  2         5  
  2         53  
8 2     2   687 use Geo::Leaflet::Polygon;
  2         5  
  2         48  
9 2     2   641 use Geo::Leaflet::Polyline;
  2         5  
  2         50  
10 2     2   776 use Geo::Leaflet::Rectangle;
  2         5  
  2         49  
11 2     2   899 use Geo::Leaflet::Icon;
  2         6  
  2         60  
12 2     2   904 use Geo::Leaflet::DivIcon;
  2         5  
  2         54  
13 2     2   10 use JSON::XS qw{};
  2         2  
  2         20  
14 2     2   938 use HTML::Tiny qw{};;
  2         5517  
  2         3657  
15              
16             our $VERSION = '0.04';
17             our $PACKAGE = __PACKAGE__;
18              
19             =head1 NAME
20              
21             Geo::Leaflet - Generates a Leaflet JavaScript map web page
22              
23             =head1 SYNOPSIS
24              
25             use Geo::Leaflet;
26             my $map = Geo::Leaflet->new;
27             print $map->html;
28              
29             =head1 DESCRIPTION
30              
31             This package generates a L map web page.
32              
33             =head1 CONSTRUCTORS
34              
35             =head2 new
36              
37             Returns a map object
38              
39             my $map = Geo::Leaflet->new(
40             id => "map",
41             center => [$lat, $lon],
42             zoom => 13,
43             );
44              
45             =head1 MAP PROPERTIES
46              
47             =head2 id
48              
49             Sets and returns the html id of the map.
50              
51             Default: "map"
52              
53             =cut
54              
55             sub id {
56 2     2 1 2 my $self = shift;
57 2 50       4 $self->{'id'} = shift if @_;
58 2 100       21 $self->{'id'} = 'map' unless defined $self->{'id'};
59 2         22 return $self->{'id'};
60             }
61              
62             =head2 center
63              
64             Sets and returns the center of the map.
65              
66             $map->center([$lat, $lon]);
67             my $center = $map->center;
68              
69             Default: [38.2, -97.2]
70              
71             =cut
72              
73             sub center {
74 3     3 1 144330 my $self = shift;
75 3 50       11 $self->{'center'} = shift if @_;
76 3 100       10 $self->{'center'} = [38.2, -97.2] unless defined $self->{'center'};
77 3         8 my $error_template = "Error: $PACKAGE center expected %s (e.g., [\$lat, \$lon])";
78 3 50       21 die(sprintf($error_template, 'array reference')) unless ref($self->{'center'}) eq 'ARRAY';
79 3 50       4 die(sprintf($error_template, 'two elements' )) unless @{$self->{'center'}} == 2;
  3         10  
80 3         21 return $self->{'center'};
81             }
82              
83             =head2 zoom
84              
85             Sets and returns the zoom of the map.
86              
87             $map->zoom(4.5);
88             my $zoom = $map->zoom;
89              
90             Default: 4.5
91              
92             =cut
93              
94             sub zoom {
95 2     2 1 3 my $self = shift;
96 2 50       5 $self->{'zoom'} = shift if @_;
97 2 100       8 $self->{'zoom'} = 4.5 unless defined $self->{'zoom'};
98 2         13 return $self->{'zoom'};
99             }
100              
101             =head2 setView
102              
103             Sets the center and zoom of the map and returns the map object (i.e., matches leaflet.js interface).
104              
105             $map->setView([51.505, -0.09], 13);
106              
107             =cut
108              
109             sub setView {
110 1     1 1 2 my $self = shift;
111 1         2 my $center = shift;
112 1         1 my $zoom = shift;
113 1 50       3 $self->center($center) if defined $center;
114 1 50       2 $self->zoom($zoom) if defined $zoom;
115 1         3 return $self;
116             }
117              
118             =head2 width
119              
120             Sets and returns the percent or pixel width of the map.
121              
122             $map->width('600px');
123             $map->width('100%');
124             my $width = $map->width;
125              
126             Default: 100%
127              
128             =cut
129              
130             sub width {
131 2     2 1 2 my $self = shift;
132 2 50       4 $self->{'width'} = shift if @_;
133 2 100       4 $self->{'width'} = '100%' unless defined $self->{'width'};
134 2         5 return $self->{'width'};
135             }
136              
137             =head2 height
138              
139             Sets and returns the percent or pixel height of the map.
140              
141             $map->height('400px');
142             $map->height('100%');
143             my $height = $map->height;
144              
145             Default: 100%
146              
147             =cut
148              
149             sub height {
150 2     2 1 2 my $self = shift;
151 2 50       3 $self->{'height'} = shift if @_;
152 2 100       5 $self->{'height'} = '100%' unless defined $self->{'height'};
153 2         17 return $self->{'height'};
154             }
155              
156             =head1 HTML PROPERTIES
157              
158             =head2 title
159              
160             Sets and returns the HTML title.
161              
162             Default: "Leaflet Map"
163              
164             =cut
165              
166             sub title {
167 1     1 1 2 my $self = shift;
168 1 50       2 $self->{'title'} = shift if @_;
169 1 50       3 $self->{'title'} = 'Leaflet Map' unless defined $self->{'title'};
170 1         3 return $self->{'title'};
171             }
172              
173             =head1 TILE LAYER CONSTRUCTOR
174              
175             =head2 tileLayer
176              
177             Creates and returns a TileLayer object which is added to the map.
178              
179             $map->tileLayer(
180             url => 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
181             options => {
182             maxZoom => 19,
183             attribution => '© OpenStreetMap',
184             },
185             );
186              
187             Default: OpenStreetMaps
188              
189             See: L
190              
191             =cut
192              
193             sub tileLayer {
194 3     3 1 130671 my $self = shift;
195 3 50       9 $self->{'tileLayer'} = Geo::Leaflet::TileLayer->new(@_, JSON=>$self->JSON) if @_;
196 3 100       12 $self->{'tileLayer'} = Geo::Leaflet::TileLayer->osm((), JSON=>$self->JSON) unless defined $self->{'tileLayer'};
197 3         42 return $self->{'tileLayer'};
198             }
199              
200             =head1 ICON CONSTRUCTORS
201              
202             =head2 icon
203              
204             Represents an icon to provide when creating a marker.
205              
206             my $icon = $map->icon(
207             name => "my_icon", #must be a valid JavaScript variable name
208             options => {
209             iconUrl => "my-icon.png",
210             iconSize => [38, 95],
211             iconAnchor => [22, 94],
212             popupAnchor => [-3, -76],
213             shadowUrl => "my-icon-shadow.png",
214             shadowSize => [68, 95],
215             shadowAnchor => [22, 94],
216             }
217             );
218              
219             See: L
220              
221             =cut
222              
223             sub icon {
224 0     0 1 0 my $self = shift;
225 0         0 my $icon = Geo::Leaflet::Icon->new(@_, JSON=>$self->JSON);
226 0         0 $self->icon_objects($icon);
227 0         0 return $icon;
228             }
229              
230             =head2 divIcon
231              
232             Represents a lightweight icon for markers that uses a simple `div` element instead of an image.
233              
234             Font Awesome with defaults
235              
236             my $icon = $map->divIcon(icon_name => "bicycle");
237              
238             Font Awesome with tweaks
239              
240             my $icon = $map->divIcon(
241             icon_name => "bicycle",
242             icon_font_size => 22,
243             options => {
244             iconAnchor => [11,11],
245             },
246             );
247              
248             Other CSS options
249              
250             my $icon = $map->divIcon(
251             options => {
252             html => '',
253             iconAnchor => [13, 44],
254             }
255             );
256              
257              
258             See: https://leafletjs.com/reference.html#divicon
259              
260             =cut
261              
262             sub divIcon {
263 0     0 1 0 my $self = shift;
264 0         0 my %param = @_;
265 0   0     0 my $icon_set = $param{'icon_set'} || 'fa'; #fa is default
266 0         0 my $icon = Geo::Leaflet::DivIcon->new(%param, JSON=>$self->JSON);
267 0         0 $self->icon_sets($icon_set);
268 0         0 $self->icon_objects($icon);
269 0         0 return $icon;
270             }
271              
272             =head1 MAP OBJECT CONSTRUCTORS
273              
274             =head2 marker
275              
276             Adds a marker object to the map and returns a reference to the marker object.
277              
278             $map->marker(lat=>$lat, lon=>$lon);
279              
280             See: L
281              
282             =cut
283              
284             sub marker {
285 1     1 1 257 my $self = shift;
286 1         3 my $marker = Geo::Leaflet::Marker->new(@_, JSON=>$self->JSON);
287 1         17 $self->map_objects($marker);
288 1         3 return $marker;
289             }
290              
291             =head2 polyline
292              
293             Adds a polyline object to the map and returns a reference to the polyline object.
294              
295             my $latlngs = [[$lat, $lon], ...]
296             $map->polyline(coordinates=>$latlngs, options=>{});
297              
298             See: L
299              
300             =cut
301              
302             sub polyline {
303 0     0 1 0 my $self = shift;
304 0         0 my $polyline = Geo::Leaflet::Polyline->new(@_, JSON=>$self->JSON);
305 0         0 $self->map_objects($polyline);
306 0         0 return $polyline;
307             }
308              
309             =head2 polygon
310              
311             Adds a polygon object to the map and returns a reference to the polygon object.
312              
313             my $latlngs = [[$lat, $lon], ...]
314             $map->polygon(coordinates=>$latlngs, options=>{});
315              
316             See: L
317              
318             =cut
319              
320             sub polygon {
321 1     1 1 2 my $self = shift;
322 1         4 my $polygon = Geo::Leaflet::Polygon->new(@_, JSON=>$self->JSON);
323 1         16 $self->map_objects($polygon);
324 1         2 return $polygon;
325             }
326              
327             =head2 rectangle
328              
329             Adds a rectangle object to the map and returns a reference to the rectangle object.
330              
331             $map->rectangle(llat => $llat,
332             llon => $llon,
333             ulat => $ulat,
334             ulon => $ulon,
335             options => {});
336              
337             See: L
338              
339             =cut
340              
341             sub rectangle {
342 0     0 1 0 my $self = shift;
343 0         0 my $rectangle = Geo::Leaflet::Rectangle->new(@_, JSON=>$self->JSON);
344 0         0 $self->map_objects($rectangle);
345 0         0 return $rectangle;
346             }
347              
348             =head2 circle
349              
350             Adds a circle object to the map and returns a reference to the circle object.
351              
352             $map->circle(lat=>$lat, lon=>$lon, radius=>$radius, options=>{});
353              
354             See: L
355              
356             =cut
357              
358             sub circle {
359 1     1 1 2 my $self = shift;
360 1         3 my $circle = Geo::Leaflet::Circle->new(@_, JSON=>$self->JSON);
361 1         18 $self->map_objects($circle);
362 1         3 return $circle;
363             }
364              
365             =head1 METHODS
366              
367             =head2 html
368              
369             =cut
370              
371             sub html {
372 1     1 1 580 my $self = shift;
373 1         4 my $html = $self->HTML;
374 1         4 return join "",
375             '',
376             $html->html([
377             $html->head([
378             $html->title($self->title),
379             $self->html_head_links,
380             $self->html_head_script,
381             $self->html_head_style,
382             ]),
383             $html->body([
384             $self->html_body_div,
385             $self->html_body_script
386             ]),
387             ]);
388             }
389              
390             =head2 html_head_links
391              
392             =cut
393              
394             our %FONTS = (
395             fa => {href => 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css'},
396             );
397              
398             sub html_head_links {
399 1     1 1 84 my $self = shift;
400 1         3 my $html = $self->HTML;
401 1         6 my @links = (
402             $html->link({
403             rel => 'stylesheet',
404             href => 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css',
405             integrity => 'sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=',
406             crossorigin => '',
407             }),
408             );
409 1         94 foreach my $set (@{$self->icon_sets}) {
  1         3  
410 0 0       0 my $hash = $FONTS{$set} or die(sprintf('Error: font set "%s" is not registered in %%%s::FONTS', $set, $PACKAGE));
411 0 0       0 die(qq{Error: font set "$set" is not a hash reference}) unless ref($hash) eq 'HASH';
412 0   0     0 $hash->{'rel'} ||= 'stylesheet'; #sane default
413 0         0 push @links, $html->link($hash);
414             }
415 1         3 return @links;
416             }
417              
418             =head2 html_head_script
419              
420             =cut
421              
422             sub html_head_script {
423 1     1 1 2 my $self = shift;
424 1         1 my $html = $self->HTML;
425 1         6 return $html->script({
426             src => 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js',
427             integrity => 'sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=',
428             crossorigin => '',
429             }, ''); #empty script required
430             }
431              
432             =head2 html_head_style
433              
434             =cut
435              
436             sub html_head_style {
437 1     1 1 78 my $self = shift;
438 1         2 my $html = $self->HTML;
439 1         3 my $style_size = sprintf('width: %s; height: %s;', $self->width, $self->height);
440 1         2 my $style_full = 'html, body { height: 100%; margin: 0; } '.
441             ".leaflet-container { $style_size max-width: 100%; max-height: 100%; }";
442 1         3 return $html->style({}, $style_full);
443             }
444              
445             =head2 html_body_div
446              
447             =cut
448              
449             sub html_body_div {
450 1     1 1 94 my $self = shift;
451 1         2 my $html = $self->HTML;
452 1         2 my $style_size = sprintf('width: %s; height: %s;', $self->width, $self->height);
453 1         4 return $html->div({id => $self->id, style => $style_size});
454             }
455              
456             =head2 html_body_script
457              
458             =cut
459              
460             sub html_body_script {
461 1     1 1 93 my $self = shift;
462 1         2 my $html = $self->HTML;
463 1         4 return $html->script({}, $self->html_body_script_contents);
464             }
465              
466             =head2 html_body_script_map
467              
468             =cut
469              
470             sub html_body_script_map {
471 1     1 1 1 my $self = shift;
472 1         3 return sprintf(q{const map = L.map(%s).setView(%s, %s);},
473             $self->JSON->encode($self->id),
474             $self->JSON->encode($self->center),
475             $self->JSON->encode($self->zoom),
476             );
477             }
478              
479             =head2 html_body_script_contents
480              
481             =cut
482              
483             sub html_body_script_contents {
484 1     1 1 1 my $self = shift;
485 1         2 my $empty = '';
486 1         8 my @commands = (
487             $empty,
488             $empty,
489             $self->html_body_script_map,
490             $self->tileLayer->stringify,
491             $empty,
492             );
493 1         2 foreach my $icon (@{$self->icon_objects}) {
  1         2  
494 0         0 my $name = $icon->name;
495 0         0 push @commands, "const $name = " . $icon->stringify;
496             }
497 1         2 my $loop = 0;
498 1         1 foreach my $object (@{$self->map_objects}) {
  1         2  
499 3         4 $loop++;
500 3         11 push @commands, "const object$loop = " . $object->stringify;
501             }
502 1         2 push @commands, $empty;
503              
504 1         2 return join $empty, map {" $_\n"} @commands;
  9         28  
505             }
506              
507             =head1 DATA ACCESSORS
508              
509             =head2 map_objects
510              
511             Returns the array reference of map objects to be added to the map
512              
513             $map->map_objects($icon);
514              
515             =cut
516              
517             sub map_objects {
518 8     8 1 1431 my $self = shift;
519 8 100       45 $self->{'map_objects'} = [] unless ref($self->{'map_objects'}) eq 'ARRAY';
520 8 100       18 push @{$self->{'map_objects'}}, @_ if @_;
  3         6  
521 8         19 return $self->{'map_objects'};
522              
523             }
524              
525             =head2 icon_objects
526              
527             Returns the array reference of icon objects to be added to the map
528              
529             $map->icon_objects($icon);
530              
531             =cut
532              
533             sub icon_objects {
534 1     1 1 2 my $self = shift;
535 1 50       3 $self->{'icon_objects'} = [] unless ref($self->{'icon_objects'}) eq 'ARRAY';
536 1 50       2 push @{$self->{'icon_objects'}}, @_ if @_;
  0         0  
537 1         3 return $self->{'icon_objects'};
538             }
539              
540             =head2 icon_sets
541              
542             Returns the array reference of icon sets to be added to the map
543              
544             =cut
545              
546             sub icon_sets {
547 1     1 1 2 my $self = shift;
548 1 50       3 $self->{'icon_sets'} = [] unless ref($self->{'icon_sets'}) eq 'ARRAY';
549 1 50       2 push @{$self->{'icon_sets'}}, @_ if @_;
  0         0  
550 1         3 return $self->{'icon_sets'};
551             }
552              
553             =head1 OBJECT ACCESSORS
554              
555             =head2 HTML
556              
557             Returns an L object to generate HTML.
558              
559             =cut
560              
561             sub HTML {
562 6     6 1 7 my $self = shift;
563 6 50       13 $self->{'HTML'} = shift if @_;
564 6 100       32 $self->{'HTML'} = HTML::Tiny->new() unless defined $self->{'HTML'};
565 6         53 return $self->{'HTML'};
566             }
567              
568             =head2 JSON
569              
570             Returns a L object to generate JSON.
571              
572             =cut
573              
574             sub JSON {
575 8     8 1 10 my $self = shift;
576 8         38 $self->{'JSON'} = JSON::XS->new->allow_nonref;
577 8         56 return $self->{'JSON'};
578             }
579              
580             =head1 SEE ALSO
581              
582             L
583             https://leafletjs.com/
584              
585             =head1 AUTHOR
586              
587             Michael R. Davis
588              
589             =head1 COPYRIGHT AND LICENSE
590              
591             Copyright (C) 2024 by Michael R. Davis
592              
593             MIT LICENSE
594              
595             =cut
596              
597             1;