use v5.36; use rlib '.'; use HexGrid; use HexGrid::Pin; use MWTemplate; use MediaWiki::API; use Getopt::Long; use Carp; use Data::Dumper; # $Data::Dumper::Indent = 1; use feature "signatures"; no warnings "experimental::signatures"; # This regex is a whitespace forgiving version of /^(-?\d+),(-?\d+)/, an int pair my $coords_regex = qr/^\s*(\s*-?\d+\s*)\s*,\s*(\s*-?\d+\s*)\s*$/; my $api_url; my $region_template_name = "MapRegion"; my $subregion_template_name = "MapSubregion"; my $location_template_name = "MapLocation"; my $site_template_name = "MapSite"; my $path_template_name = "MapPath"; my $border_width = 1; my $border_colour = 'black'; my $default_path_stroke_width = 5; my $show_coords = 0; my $embed_images = 1; my $html_document = 1; my $outfile = '-'; my $regiondir; GetOptions ( 'api-url=s' => \$api_url, 'region-template-name=s' => \$region_template_name, 'subregion-template-name=s' => \$subregion_template_name, 'location-template-name=s' => \$location_template_name, 'site-template-name=s' => \$site_template_name, 'path-template-name=s' => \$path_template_name, 'border-width|bw=f' => \$border_width, 'border-colour|border-color|bc=s' => \$border_colour, 'show-coords|coords!' => \$show_coords, 'embed-images!' => \$embed_images, 'html-document!' => \$html_document, 'outfile=s' => \$outfile, 'regiondir=s' => \$regiondir ); # HexGrid::DEBUG(); $api_url // croak "Base API URL is required! Use --api-url to set"; my $grid = HexGrid->new(embed_images => $embed_images, defaults => { style => { 'stroke-width' => $border_width, stroke => $border_colour }, show_coords => $show_coords}); # Used in producing region subgrids, maps each subregion to its parent region my %regions_by_subregion; my %images_for_region_grids; my %location_grids; my $mw = MediaWiki::API->new(); $mw->{config}->{api_url} = $api_url; say STDERR "Getting Region pages"; my $region_query_results = $mw->api ( { action => 'query', generator => 'categorymembers', gcmtitle => 'Category:Regions', gcmlimit => 'max', prop => 'info|revisions', rvprop => 'content', inprop => 'url', } ) || croak $mw->{error}->{code} . ': ' . $mw->{error}->{details}; my (@tile_pages, %background_pages); foreach my $page (values %{$region_query_results->{query}{pages}}) { next if $page->{title} =~ /^Category:/; my $parsed_template = MWTemplate::Parse($page->{revisions}[0]{'*'}, $region_template_name); next unless $parsed_template; say STDERR "Processing region: $page->{title}"; my $region = $grid->make_region($page->{title}); $region->{defaults}{colour} = $parsed_template->{named_params}{colour}; $region->{defaults}{coords_colour} = $parsed_template->{named_params}{coordinates_colour} if $parsed_template->{named_params}{coordinates_colour}; if($regiondir) { $regions_by_subregion{$region->{name}} = $region->{name}; } push @tile_pages, "$page->{title}/Tiles"; push @{$background_pages{"File:$parsed_template->{named_params}{background}"}}, $region; } say STDERR "Getting Subregion pages"; my $subregion_query_results = $mw->api ( { action => 'query', generator => 'categorymembers', gcmtitle => 'Category:Subregions', gcmlimit => 'max', prop => 'info|revisions', rvprop => 'content', inprop => 'url', } ) || croak $mw->{error}->{code} . ': ' . $mw->{error}->{details}; foreach my $page (values %{$subregion_query_results->{query}{pages}}) { next if $page->{title} =~ /^Category:/; my $parsed_template = MWTemplate::Parse($page->{revisions}[0]{'*'}, $subregion_template_name); next unless $parsed_template; say STDERR "Processing subregion: $page->{title}"; my $subregion = $grid->make_region($page->{title}); $subregion->{defaults}{colour} = $parsed_template->{named_params}{colour}; $subregion->{defaults}{coords_colour} = $parsed_template->{named_params}{coordinates_colour} if $parsed_template->{named_params}{coordinates_colour}; if($regiondir) { my $region_name = $parsed_template->{positional_params}[0]; $regions_by_subregion{$subregion->{name}} = $region_name; } push @tile_pages, "$page->{title}/Tiles"; push @{$background_pages{"File:$parsed_template->{named_params}{background}"}}, $subregion; } my @location_continuations; say STDERR "Getting Location pages"; my $location_query_results = $mw->api ( { action => 'query', generator => 'categorymembers', gcmtitle => 'Category:Locations', gcmlimit => 'max', prop => 'info|revisions', rvprop => 'content', inprop => 'url', } ) || croak $mw->{error}->{code} . ': ' . $mw->{error}->{details}; foreach my $page (values %{$location_query_results->{query}{pages}}) { next if $page->{title} =~ /^Category:/; my $parsed_template = MWTemplate::Parse($page->{revisions}[0]{'*'}, $location_template_name); next unless $parsed_template; say STDERR "Processing location: $page->{title}"; my $location = $grid->make_region($page->{title}); $location->{defaults}{colour} = $parsed_template->{named_params}{colour}; $location->{defaults}{coords_colour} = $parsed_template->{named_params}{coordinates_colour} if $parsed_template->{named_params}{coordinates_colour}; push @{$background_pages{"File:$parsed_template->{named_params}{background}"}}, $location; my $region_name = $parsed_template->{positional_params}[1]; $regions_by_subregion{$location->{name}} = $region_name; next unless $parsed_template->{positional_params}[0] =~ $coords_regex; my ($nw, $sw) = ($1, $2); push @location_continuations, sub { # We create the tile after the location region has its image set, which happens later $location->make_tile_at($nw, $sw); if($regiondir) { # Locations can have their own images rendered, given a list of context tiles # To reference these tiles, they must exist in the parent grid, # so the remainder of the processing must happen after the region tile pages are processed my @coords_list = ({ nw => $nw, sw => $sw}); foreach my $coords (split /;/, $parsed_template->{named_params}{context_tiles}) { do { carp "Skipping bad spec: $coords"; next; } unless $coords =~ $coords_regex; push @coords_list, { nw => $1, sw => $2 }; } my $location_grid = $grid->subgrid_for_tiles(@coords_list); $location_grid->iter_tile( sub($tile) { # if haven't added image to grid yet, do so if($tile->image && !(exists $location_grid->{images}{$tile->image})) { $location_grid->{images}{$tile->image} = $grid->{images}{$tile->image}; } }); $location_grids{$location->{name}} = $location_grid; } }; } # To minimize API calls, and since some images may be referenced by more than one region, # all background images for all regions are put in one request. # The list of pages to get was constructed in the region, subregion, and location processing. say STDERR "Getting Background image pages"; my $background_query_results = $mw->api({ action => 'query', prop => 'imageinfo', titles => join('|', keys %background_pages), iiprop => 'url' }) || carp $mw->{error}->{code} . ': ' . $mw->{error}->{details}; foreach my $page (values %{$background_query_results->{query}{pages}}) { if($page->{imageinfo}) { say STDERR "Processing image: $page->{title}"; $grid->add_image(HexGrid::to_id($page->{title}), $page->{imageinfo}[0]{url}); # For every region which declared this image its background foreach my $subregion (@{$background_pages{$page->{title}}}) { $subregion->{defaults}{image} = HexGrid::to_id($page->{title}); if($regiondir) { # If we make subgrids for the regions, we'll need to add this image to the region's subgrid my $region_name = $regions_by_subregion{$subregion->{name}}; push @{$images_for_region_grids{$region_name}}, { name => HexGrid::to_id($page->{title}), source => $page->{imageinfo}[0]{url} }; } } } } # As above, get all tile pages in one request. # By doing so, we lose context as to which page corresponds to which region, # so we extract it from the title of the page. say STDERR "Getting Tile pages"; my $tile_query_results = $mw->api ( { action => 'query', titles => join('|', @tile_pages), prop => 'revisions', rvprop => 'content', } ) || croak $mw->{error}->{code} . ': ' . $mw->{error}->{details}; foreach my $page (values %{$tile_query_results->{query}{pages}}) { my $content = $page->{revisions}[0]{'*'}; my ($region_name) = $page->{title} =~ /(.*)\/Tiles/; say STDERR "Processing tiles for: $region_name"; my $region = $grid->{regions}{$region_name}; foreach my $coords (split /;/, $content) { do { carp "Skipping bad spec: $coords"; next; } unless $coords =~ $coords_regex; $region->make_tile_at($1,$2); } } say STDERR "Continuing Location processing"; $_->() for @location_continuations; my (%path_specs); say STDERR "Getting Path pages"; my $path_query_results = $mw->api ( { action => 'query', generator => 'categorymembers', prop => 'info|revisions', gcmtitle => 'Category:Paths', gcmlimit => 'max', rvprop => 'content', inprop => 'url', } ) || croak $mw->{error}->{code} . ': ' . $mw->{error}->{details}; foreach my $path_page_ref (values %{$path_query_results->{query}{pages}}) { next if $path_page_ref->{title} =~ /^Category:/; my $path_name = $path_page_ref->{title}; say STDERR "Processing Path $path_name"; my $path_content = $path_page_ref->{revisions}[0]{'*'}; my $parsed_template = MWTemplate::Parse($path_content, $path_template_name); next unless $parsed_template; $path_specs{$path_name} = { id => HexGrid::to_id($path_name) . "-path", tile_page => "$path_name/Tiles", colour => $parsed_template->{named_params}{colour}, stroke_width => $parsed_template->{named_params}{stroke_width} }; $path_specs{$path_name}{starts_from} = $parsed_template->{named_params}{starts_from} if $parsed_template->{named_params}{starts_from}; $path_specs{$path_name}{ends_to} = $parsed_template->{named_params}{ends_to} if $parsed_template->{named_params}{ends_to}; } say STDERR "Getting Path Tile pages"; my $path_tile_query_results = $mw->api ( { action => 'query', titles => join('|', map { $_->{tile_page} } values %path_specs), prop => 'revisions', rvprop => 'content', } ) || croak $mw->{error}->{code} . ': ' . $mw->{error}->{details}; foreach my $page (values %{$path_tile_query_results->{query}{pages}}) { my $content = $page->{revisions}[0]{'*'}; my ($path_name) = $page->{title} =~ /(.*)\/Tiles/; say STDERR "Processing tiles for: $path_name"; my %path_spec = %{$path_specs{$path_name}}; my @path_coords; foreach my $coords (split /;/, $content) { do { carp "Skipping bad spec: $coords"; next; } unless $coords =~ $coords_regex; push @path_coords, [$1,$2]; } my $path = $grid->make_path_from($path_spec{id}, \@path_coords, css_class => 'path', colour => $path_spec{colour}, style => { 'stroke-width' => $path_spec{stroke_width} // $default_path_stroke_width }); $path->{starts_from} = $HexGrid::DIR{$path_spec{starts_from}} if $path_spec{starts_from}; $path->{ends_to} = $HexGrid::DIR{$path_spec{ends_to}} if $path_spec{ends_to}; } say STDERR "Getting Site pages"; my $site_query_results = $mw->api ( { action => 'query', generator => 'categorymembers', prop => 'info|revisions', gcmtitle => 'Category:Sites', gcmlimit => 'max', rvprop => 'content', inprop => 'url', } ) || croak $mw->{error}->{code} . ': ' . $mw->{error}->{details}; my %site_icon_pages; foreach my $site_page_ref (values %{$site_query_results->{query}{pages}}) { next if $site_page_ref->{title} =~ /^Category:/; my $site_name = $site_page_ref->{title}; say STDERR "Processing Site $site_name"; my $site_url = $site_page_ref->{canonicalurl}; my $site_content = $site_page_ref->{revisions}[0]{'*'}; my $parsed_template = MWTemplate::Parse($site_content, $site_template_name); next unless $parsed_template; my ($nw,$sw) = split /,/, $parsed_template->{named_params}{coords}; my $tile = $grid->get_tile_at($nw, $sw); unless($tile) { carp "Coordinates of Site $site_name do not appear in the grid, skipping."; next; } $site_icon_pages{"File:$parsed_template->{named_params}{icon}"} = $parsed_template->{named_params}{icon}; # my $imageinfo_query_results = $mw->api({ action => 'query', # prop => 'imageinfo', # titles => "File:$parsed_template->{named_params}{icon}", # iiprop => 'url' # }) || carp $mw->{error}->{code} . ': ' . $mw->{error}->{details}; # my %image_pages = %{$imageinfo_query_results->{query}{pages}}; # my $image_url = (values %image_pages)[0]{imageinfo}[0]{url}; # $grid->add_image(HexGrid::to_id($parsed_template->{named_params}{icon}), $image_url); my $pin = HexGrid::Pin->new ( name => $site_name, id => HexGrid::to_id($site_name), icon => HexGrid::to_id($parsed_template->{named_params}{icon}), link => $site_url, description => $parsed_template->{named_params}{abstract}, popup => $html_document ); $tile->pin($pin); } say STDERR "Getting Site icon image pages"; my $site_icon_query_results = $mw->api({ action => 'query', prop => 'imageinfo', titles => join('|', keys %site_icon_pages), iiprop => 'url' }) || carp $mw->{error}->{code} . ': ' . $mw->{error}->{details}; foreach my $page (values %{$site_icon_query_results->{query}{pages}}) { if($page->{imageinfo}) { say STDERR "Processing image: $page->{title}"; $grid->add_image ( HexGrid::to_id($site_icon_pages{$page->{title}}), $page->{imageinfo}[0]{url} ); } } # Render and output open (my $fh, "> $outfile") or croak "Couldn't open $outfile for writing: $!"; say $fh ($html_document ? wrap_in_html($grid) : $grid->render); close $fh; if($regiondir) { chdir $regiondir || croak "Couldn't chdir to $regiondir: $!"; my $extension = $html_document ? 'html' : 'svg'; my %region_grid_listings; # Maps a region name to the list of its subregions (including itself) while(my ($subregion, $region) = each %regions_by_subregion) { push @{$region_grid_listings{$region}}, $subregion; } while(my ($region, $subregions) = each %region_grid_listings) { my $region_grid = $grid->subgrid_for_regions(@$subregions); if(exists $images_for_region_grids{$region}) { foreach my $image (@{$images_for_region_grids{$region}}) { $region_grid->{images}{$image->{name}} = $grid->{images}{$image->{name}}; } } say STDERR "Rendering Region $region\'s grid"; open (my $region_fh, "> $region.$extension") or croak "Couldn't open $region.extension for writing: $!"; say $region_fh ($html_document ? wrap_in_html($region_grid) : $region_grid->render); close $region_fh; } # TODO: Location grids need to import images while(my ($location_name, $location_grid) = each %location_grids) { say STDERR "Rendering Location $location_name\'s grid"; open (my $location_fh, "> $location_name.$extension") or croak "Couldn't open $location_name.$extension for writing: $!"; say $location_fh ($html_document ? wrap_in_html($location_grid) : $location_grid->render); close $location_fh; } } # Puts the rendered SVG inside an html document, # along with a bit of javascript to show popups for sites and to toggle coordinates visibility sub wrap_in_html($grid) { my $html_builder = ""; $html_builder .= "\n\n"; $html_builder .= "\n" . < function clickPin(pinId, containerId) { let popup = document.getElementById(pinId + '-popup'); popup.style.visibility = popup.style.visibility == 'visible' ? 'hidden' : 'visible'; } EOS if($show_coords) { $html_builder .= < function toggleCoords(show) { for (var elem of document.getElementsByClassName('coords')) { elem.style.visibility = show ? 'visible' : 'hidden'; } } EOS } $html_builder .= "\n" . $grid->render; $html_builder .= "\n\n"; return $html_builder; }