add shapefile, kmz and geojson export to deployment zones, RT#86460
authorIvan Kohler <ivan@freeside.biz>
Fri, 26 Aug 2022 19:23:43 +0000 (12:23 -0700)
committerIvan Kohler <ivan@freeside.biz>
Fri, 26 Aug 2022 19:23:43 +0000 (12:23 -0700)
FS/FS/Mason.pm
FS/FS/deploy_zone.pm
debian/control
httemplate/browse/deploy_zone.html
httemplate/view/deploy_zone-geojson.cgi [new file with mode: 0644]
httemplate/view/deploy_zone-kmz.cgi [new file with mode: 0644]
httemplate/view/deploy_zone-shp.cgi [new file with mode: 0644]

index 8dd72ac..ebd40ad 100644 (file)
@@ -85,6 +85,7 @@ if ( -e $addl_handler_use_file ) {
     die $@ if $@;
   }
   use Text::CSV_XS;
+  use Archive::Zip;
   use Spreadsheet::WriteExcel;
   use Spreadsheet::WriteExcel::Utility;
   use OLE::Storage_Lite;
@@ -120,7 +121,10 @@ if ( -e $addl_handler_use_file ) {
   use Locale::Currency::Format;
   use Number::Phone::Country qw( noexport );
   use Business::US::USPS::WebTools::AddressStandardization;
-  use Geo::GoogleEarth::Pluggable;
+  use Geo::GoogleEarth::Pluggable 0.16;
+  use Geo::Shapelib;
+  use Geo::JSON;
+  use Geo::JSON::FeatureCollection;
   use LWP::UserAgent;
   use Storable qw( nfreeze thaw );
   use FS;
index 723b491..16ba5dd 100644 (file)
@@ -10,9 +10,14 @@ use Cpanel::JSON::XS;
 use LWP::UserAgent;
 use HTTP::Request::Common;
 
+use Geo::JSON::Polygon;
+use Geo::JSON::Feature;
+
 # update this in 2020, along with the URL for the TIGERweb service
 our $CENSUS_YEAR = 2010;
 
+our $tech_label  = FS::part_pkg_fcc_option->technology_labels;
+
 =head1 NAME
 
 FS::deploy_zone - Object methods for deploy_zone records
@@ -275,6 +280,27 @@ sub deploy_zone_vertex {
   });
 }
 
+=item shapefile_add SHAPEFILE
+
+Adds this deployment zone to the supplied Geo::Shapelib shapefile.
+
+=cut
+
+sub shapefile_add {
+  my( $self, $shapefile ) = @_;
+
+  my @coordinates = map { [ $_->longitude, $_->latitude, 0, 0 ] }
+                      $self->deploy_zone_vertex;
+  push @coordinates, $coordinates[0];
+
+  push @{$shapefile->{Shapes}}, { 'Vertices' => \@coordinates };
+  push @{$shapefile->{ShapeRecords}}, [ $tech_label->{$self->technology},
+                                        $self->adv_speed_down,
+                                        $self->adv_speed_up,
+                                      ];
+  '';
+}
+
 =item vertices_json
 
 Returns the vertex list for this zone, as a JSON string of
@@ -289,6 +315,51 @@ sub vertices_json {
   encode_json(\@vertices);
 }
 
+=item geo_json_feature
+
+Returns this zone as a Geo::JSON::Feature object
+
+=cut
+
+sub geo_json_feature {
+  my $self = shift;
+
+  my @coordinates = map { [ $_->longitude, $_->latitude ] }
+                      $self->deploy_zone_vertex;
+  push @coordinates, $coordinates[0];
+
+  Geo::JSON::Feature->new({
+    geometry   => Geo::JSON::Polygon->new({ coordinates => [ \@coordinates ] }),
+    properties => { 'Technology' => $tech_label->{$self->technology},
+                    'Down'       => $self->adv_speed_down,
+                    'Up'         => $self->adv_speed_up,
+                  },
+  })
+}
+
+=item kml_add
+
+Adds this deployment zone to the supplied Geo::GoogleEarth::Pluggable object.
+
+=cut
+
+sub kml_polygon {
+  my( $self, $kml ) = @_;
+
+  my $name = $self->description. ' ('. $self->adv_speed_down. '/'.
+                                       $self->adv_speed_up. ')';
+
+  $kml->Polygon( 'name'        => $name,
+                 'coordinates' => [ [ #outerBoundary
+                                      map { [ $_->longitude, $_->latitude, 0 ] }
+                                        $self->deploy_zone_vertex
+                                    ],
+                                    #[ #innerBoundary
+                                    #]
+                                  ]
+               );
+}
+
 =head2 SUBROUTINES
 
 =over 4
index 0f1e29f..479a150 100644 (file)
@@ -78,7 +78,8 @@ Depends: aspell-en,gnupg,ghostscript,gsfonts,gzip,
  libipc-run-safehandles-perl,libpoe-perl,libsoap-lite-perl,libxmlrpc-lite-perl,
  libhtml-tableextract-perl,libhtml-element-extended-perl,libcam-pdf-perl,
  libnet-openssh-perl,libgd-barcode-perl,sam2p,libsys-sigaction-perl,
- libgeo-googleearth-pluggable-perl,libgeo-coder-googlev3-perl,libnet-snmp-perl,
+ libgeo-googleearth-pluggable-perl (>=0.16),libgeo-coder-googlev3-perl,
+ libnet-snmp-perl,
  libcrypt-openssl-rsa-perl,libregexp-common-perl,libnet-cidr-perl,
  libregexp-ipv6-perl,libhtml-quoted-perl,libtext-password-pronounceable-perl,
  libconvert-color-perl,liburi-perl,libhtml-rewriteattributes-perl,
@@ -107,7 +108,8 @@ Depends: aspell-en,gnupg,ghostscript,gsfonts,gzip,
  libspreadsheet-parsexlsx-perl, libunicode-truncate-perl (>= 0.303-1),
  libspreadsheet-xlsx-perl, libpod-simple-perl, libwebservice-northern911-perl,
  liblocale-codes-perl, liblocale-po-perl, libgeo-uscensus-geocoding-perl,
- libnet-sftp-foreign-perl, libpdf-webkit-perl
+ libnet-sftp-foreign-perl, libpdf-webkit-perl, libgeo-shapelib-perl,
+ libgeo-json-perl
 Conflicts: libparams-classify-perl (>= 0.013-6)
 Replaces: freeside (<<4)
 Breaks: freeside (<<4)
index 5514d7d..33724e5 100644 (file)
                         'Contractual Mbps',
                         'Vertices',
                         'Census blocks',
+                        'Shapefile',
+                        'KMZ',
+                        'GeoJSON',
+                     ],
+  footer          => [ '',
+                       'All fixed zones',
+                       '',
+                       '',
+                       '',
+                       '',
+                       '',
+                       '',
+                       '<A HREF="'. $fixed_shp.  '">download</A>',
+                       '<A HREF="'. $fixed_kmz.  '">download</A>',
+                       '<A HREF="'. $fixed_json. '">download</A>',
                      ],
   fields          => [  'zonenum',
                         'description',
                         sub { my $self = shift;
                               FS::deploy_zone_block->count('zonenum = '.$self->zonenum)
                             },
+                        sub { my $self = shift;
+                              FS::deploy_zone_vertex->count('zonenum = '.$self->zonenum)
+                              ? 'download' : ''
+                            },
+                        sub { my $self = shift;
+                              FS::deploy_zone_vertex->count('zonenum = '.$self->zonenum)
+                              ? 'download' : ''
+                            },
+                        sub { my $self = shift;
+                              FS::deploy_zone_vertex->count('zonenum = '.$self->zonenum)
+                              ? 'download' : ''
+                            },
                      ],
   sort_fields     => [ 'zonenum',
                        'description',
@@ -56,7 +83,7 @@
                        '(adv_speed_down, adv_speed_up)',
                        '(cir_speed_down, cir_speed_up)',
                      ],
-  links           => [  $link_fixed, $link_fixed, ],
+  links           => [  $link_fixed, $link_fixed, '', '', '', '', '', '', $link_shp, $link_kmz, $link_json, ],
   align           => 'cllllrrr',
   nohtmlheader    => 1,
   disable_maxselect => 1,
                         'Service Type',
                         'Advertised Mbps',
                         'Vertices', # number of vertices? not so useful
+                        'Shapefile',
+                        'KMZ',
+                        'GeoJSON',
                      ],
   fields          => [  'zonenum',
                         'description',
                         sub { my $self = shift;
                               FS::deploy_zone_vertex->count('zonenum = '.$self->zonenum)
                             },
+                        sub { my $self = shift;
+                              FS::deploy_zone_vertex->count('zonenum = '.$self->zonenum)
+                              ? 'download' : ''
+                            },
+                        sub { my $self = shift;
+                              FS::deploy_zone_vertex->count('zonenum = '.$self->zonenum)
+                              ? 'download' : ''
+                            },
+                        sub { my $self = shift;
+                              FS::deploy_zone_vertex->count('zonenum = '.$self->zonenum)
+                              ? 'download' : ''
+                            },
                      ],
   sort_fields     => [ 'zonenum',
                        'description',
                        '(is_voice is not null, is_broadband is not null)',
                        '(adv_speed_down, adv_speed_up)',
                      ],
-  links           => [  '', $link_mobile, ],
+  links           => [  $link_mobile, $link_mobile, '', '', '', '', '', '', $link_shp, $link_kmz, $link_json, ],
   align           => 'clllllr',
   nohtmlheader    => 1,
   disable_maxselect => 1,
 
 <& /elements/footer.html &>
 <%init>
+
 my $curuser = $FS::CurrentUser::CurrentUser;
 my $acl_edit = $curuser->access_right('Edit FCC report configuration');
 my $acl_edit_global = $curuser->access_right('Edit FCC report configuration for all agents');
 die "access denied"
   unless $acl_edit or $acl_edit_global;
 
-my $link_fixed = [ $p.'edit/deploy_zone-fixed.html?', 'zonenum' ];
-my $link_mobile= [ $p.'edit/deploy_zone-mobile.html?', 'zonenum' ];
+my $link_fixed  = [ $p.'edit/deploy_zone-fixed.html?', 'zonenum' ];
+my $link_mobile = [ $p.'edit/deploy_zone-mobile.html?', 'zonenum' ];
+my $link_shp    = [ $p.'view/deploy_zone-shp.cgi?', 'zonenum' ];
+my $link_kmz    = [ $p.'view/deploy_zone-kmz.cgi?', 'zonenum' ];
+my $link_json   = [ $p.'view/deploy_zone-geojson.cgi?', 'zonenum' ];
+
+my $fixed_shp    = $p.'view/deploy_zone-shp.cgi?zonetype=B';
+my $fixed_kmz    = $p.'view/deploy_zone-kmz.cgi?zonetype=B';
+my $fixed_json   = $p.'view/deploy_zone-geojson.cgi?zonetype=B';
+
+my $tech_label  = FS::part_pkg_fcc_option->technology_labels;
+my $spec_label  = FS::part_pkg_fcc_option->spectrum_labels;
 
-my $tech_label = FS::part_pkg_fcc_option->technology_labels;
-my $spec_label = FS::part_pkg_fcc_option->spectrum_labels;
 </%init>
diff --git a/httemplate/view/deploy_zone-geojson.cgi b/httemplate/view/deploy_zone-geojson.cgi
new file mode 100644 (file)
index 0000000..0c9d193
--- /dev/null
@@ -0,0 +1,42 @@
+<% $content %>\
+<%init>
+
+my $curuser = $FS::CurrentUser::CurrentUser;
+my $acl_edit = $curuser->access_right('Edit FCC report configuration');
+my $acl_edit_global = $curuser->access_right('Edit FCC report configuration for all agents');
+die "access denied"
+  unless $acl_edit or $acl_edit_global;
+
+my($name, $content);
+
+my($query) = $cgi->keywords;
+if ( $query =~ /^(\d+)$/ || $cgi->param('zonenum') =~ /^(\d+$)/ ) {
+  my $zonenum = $1;
+  $name = $zonenum;
+  my $deploy_zone = qsearchs('deploy_zone', { 'zonenum' => $zonenum })
+    or die 'unknown zonenum';
+
+  $content = $deploy_zone->geo_json_feature->to_json;
+
+} elsif ( $cgi->param('zonetype') =~ /^(\w)$/ ) {
+  my $zonetype = $1;
+  $name = $zonetype;
+  my @deploy_zone = qsearch('deploy_zone', { 'zonetype' => $zonetype,
+                                             'disabled' => '',        });
+
+   my $fc = Geo::JSON::FeatureCollection->new({
+     features => [ map $_->geo_json_feature, @deploy_zone ],
+   });
+
+   $content = $fc->to_json;
+
+} else {
+  die "no zonenum or zonetype\n";
+}
+
+http_header('Content-Type'        => 'application/geo+json' );
+http_header('Content-Disposition' => "filename=$name.geojson" );
+http_header('Content-Length'      => length($content) );
+http_header('Cache-control'       => 'max-age=60' );
+
+</%init>
diff --git a/httemplate/view/deploy_zone-kmz.cgi b/httemplate/view/deploy_zone-kmz.cgi
new file mode 100644 (file)
index 0000000..d2af171
--- /dev/null
@@ -0,0 +1,42 @@
+<% $content %>\
+<%init>
+
+my $curuser = $FS::CurrentUser::CurrentUser;
+my $acl_edit = $curuser->access_right('Edit FCC report configuration');
+my $acl_edit_global = $curuser->access_right('Edit FCC report configuration for all agents');
+die "access denied"
+  unless $acl_edit or $acl_edit_global;
+
+my $kml = Geo::GoogleEarth::Pluggable->new;
+
+my $name;
+
+my($query) = $cgi->keywords;
+if ( $query =~ /^(\d+)$/ || $cgi->param('zonenum') =~ /^(\d+$)/ ) {
+  my $zonenum = $1;
+  $name = $zonenum;
+  my $deploy_zone = qsearchs('deploy_zone', { 'zonenum' => $zonenum })
+    or die 'unknown zonenum';
+
+  $deploy_zone->kml_polygon($kml);
+
+} elsif ( $cgi->param('zonetype') =~ /^(\w)$/ ) {
+  my $zonetype = $1;
+  $name = $zonetype;
+  my @deploy_zone = qsearch('deploy_zone', { 'zonetype' => $zonetype,
+                                             'disabled' => '',        });
+
+  $_->kml_polygon($kml) foreach @deploy_zone;
+
+} else {
+  die "no zonenum or zonetype\n";
+}
+
+my $content = $kml->archive;
+
+http_header('Content-Type' => 'application/vnd.google-earth.kmz' ); #kmz
+http_header('Content-Disposition' => "filename=$name.kmz" );
+http_header('Content-Length'      => length($content) );
+http_header('Cache-control'       => 'max-age=60' );
+
+</%init>
diff --git a/httemplate/view/deploy_zone-shp.cgi b/httemplate/view/deploy_zone-shp.cgi
new file mode 100644 (file)
index 0000000..4b5dfbb
--- /dev/null
@@ -0,0 +1,76 @@
+<% $content %>\
+<%init>
+
+my $curuser = $FS::CurrentUser::CurrentUser;
+my $acl_edit = $curuser->access_right('Edit FCC report configuration');
+my $acl_edit_global = $curuser->access_right('Edit FCC report configuration for all agents');
+die "access denied"
+  unless $acl_edit or $acl_edit_global;
+
+my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
+
+my %shapelib_opts = (
+  Shapetype  => Geo::Shapelib::POLYGON,
+  FieldNames => [ 'Tech', 'Down', 'Up' ],
+  FieldTypes => [ 'String:32', 'Double', 'Double' ],
+);
+
+my( $name, $shapefile );
+
+my($query) = $cgi->keywords;
+if ( $query =~ /^(\d+)$/ || $cgi->param('zonenum') =~ /^(\d+$)/ ) {
+  my $zonenum = $1;
+  $name = $zonenum;
+  my $deploy_zone = qsearchs('deploy_zone', { 'zonenum' => $zonenum })
+    or die 'unknown zonenum';
+
+  $shapefile = new Geo::Shapelib {
+    Name => "$dir/$zonenum-$$",
+    %shapelib_opts
+  };
+
+  $deploy_zone->shapefile_add($shapefile);
+
+} elsif ( $cgi->param('zonetype') =~ /^(\w)$/ ) {
+  my $zonetype = $1;
+  $name = $zonetype;
+  my @deploy_zone = qsearch('deploy_zone', { 'zonetype' => $zonetype,
+                                             'disabled' => '',        });
+
+  $shapefile = new Geo::Shapelib {
+    Name => "$dir/$zonetype-$$",
+    %shapelib_opts
+  };
+
+  $_->shapefile_add($shapefile) foreach @deploy_zone;
+
+} else {
+  die "no zonenum or zonetype\n";
+}
+
+$shapefile->set_bounds;
+
+$shapefile->save;
+
+#slurp up .shp .shx and .dbf files and put them in a zip.. return that
+#and delete the files
+
+my $content = '';
+open(my $fh, '>', \$content);
+
+my $zip = new Archive::Zip;
+$zip->addFile("$dir/$name-$$.$_", "$name.$_") foreach qw( shp shx dbf );
+unless ( $zip->writeToFileHandle($fh) == Archive::Zip::AZ_OK() ) {
+  die "failed to create .shz file\n";
+}
+close $fh;
+
+unlink("$dir/$name-$$.$_") foreach qw( shp shx dbf );
+
+#http_header('Content-Type'        => 'x-gis/x-shapefile' );
+http_header('Content-Type'        => 'archive/zip' );
+http_header('Content-Disposition' => "filename=$name.shz" );
+http_header('Content-Length'      => length($content) );
+http_header('Cache-control'       => 'max-age=60' );
+
+</%init>