#!/usr/bin/env perl
# -*- perl -*-

#
# Author: Slaven Rezic
#
# Copyright (C) 2025,2026 Slaven Rezic. All rights reserved.
# This program is free software; you can redistribute it and/or
# modify it under the same terms as Perl itself.
#
# WWW:  https://github.com/eserte/bbbike
#

use strict;
use warnings;
use FindBin;

use Getopt::Long;
use LWP::UserAgent;
use JSON::PP;
use lib "$FindBin::RealBin/..";
use BBBikeYAML qw(LoadFile);

use POSIX qw(floor strftime);
use Time::Local qw(timegm);
use Math::Trig;

sub latlon_to_tile {
    my ($lat, $lon, $zoom) = @_;
    my $lat_rad = deg2rad($lat);
    my $n = 2 ** $zoom;
    my $x_tile = floor(( $lon + 180.0 ) / 360.0 * $n);
    my $y_tile = floor((1 - (log(tan($lat_rad) + 1 / cos($lat_rad)) / pi())) / 2 * $n);
    return ($x_tile, $y_tile);
}

# like in mapillary-v4-fetch
my $do_cache = 1;
my $cache_time = 8 * 3600; # seconds

my $conf_file = "$ENV{HOME}/.mapillary";
my $conf = LoadFile $conf_file;
my $client_token = $conf->{client_token} || die "Can't get client_token from $conf_file";

my @tiles = ();
my $z = 12;

# XXX get from Geography::Berlin_DE or provide by option
my $min_lon = 13.051179;
my $max_lon = 13.764158;
my $min_lat = 52.337621;
my $max_lat = 52.689878;

my $mvt_impl = 'pl';

# NOTE: if using the YYYYMMDD-YYYYMMDD variant, then the 2nd date is
# usually the next day. So if just 2025-01-02 is wanted, the
# specification would be 20250102-20250103.

GetOptions(
    "cache!" => \$do_cache,
    "bbox=s" => \my $bbox,
    "mvt-impl=s" => \$mvt_impl,
    'zoom=i' => \$z,
)
    or die "usage: $0 [--[no]cache]] [--mvt-impl pl|py] [--bbox minlon,minlat,maxlon,maxlat] [--zoom ...] YYYYMMDD|YYYYMMDD-|YYYYMMDD-YYYYMMDD\n";

if ($bbox) {
    my @fields = split /,/, $bbox;
    die "--bbox must have four fields\n" if @fields != 4;
    ($min_lon,$min_lat,$max_lon,$max_lat) = @fields;
}

my $date_range = shift;
my $date_from = "";
my $date_to   = "";
if ($date_range) {
    if ($date_range =~ /^(\d{4})(\d{2})(\d{2})$/) {
	my($y,$m,$d) = ($1,$2,$3);
	$date_from = $date_range;
	$date_to   = strftime '%Y%m%d', gmtime(timegm(0,0,0,$d,$m-1,$y) + 86400);
    } elsif ($date_range =~ /^(\d{8})(?:-|\.\.)(\d{8})?$/) {
	$date_from = $1;
	$date_to   = $2 if defined $2;
    } else {
	die "Wrong date range syntax, must be: YYYYMMDD, YYYYMMDD- or YYYYMMDD-YYYYMMDD.\n";
    }
}
if (!$date_from) {
    $date_from = strftime "%Y%m%d", localtime(time-86400);
    warn "INFO: date range not given, default to $date_from-$date_to\n";
}
die "Too many arguments" if @ARGV;

my ($min_x, $min_y) = latlon_to_tile($min_lat, $min_lon, $z);
my ($max_x, $max_y) = latlon_to_tile($max_lat, $max_lon, $z);

($min_x,$max_x) = ($max_x,$min_x) if $max_x < $min_x;
($min_y,$max_y) = ($max_y,$min_y) if $max_y < $min_y;

for my $x ($min_x .. $max_x) {
    for my $y ($min_y .. $max_y) {
	push @tiles, [$z,$x,$y];
    }
}

warn "INFO: we need to fetch and process " . scalar(@tiles) . " tile(s)...\n";

my $ua;
if ($do_cache) {
    if (!eval { require LWP::UserAgent::WithCache; require HTTP::Date; 1 }) {
	die "Module missing, please install. Error: $@";
    }
    # need to patch set_cache method
    my $orig_set_cache = \&LWP::UserAgent::WithCache::set_cache;
    {
	no warnings 'redefine';
	*LWP::UserAgent::WithCache::set_cache = sub {
	    my($self, $uri, $res) = @_;

	    if ($res->header('X-Died')) {
		warn "X-Died header encountered, do not write to cache...\n";
		return;
	    }
	    if ($res->header('Client-Aborted')) {
		warn "Client-Aborted header encountered, do not write to cache...\n";
		return;
	    }

	    my $expires = time + $cache_time;
	    my $expires_formatted = HTTP::Date::time2str($expires);
	    $res->header('Expires', $expires_formatted);

	    $orig_set_cache->($self, $uri, $res);
	};
    }
    my %cache_opt = (
        'namespace'          => 'mapillary-mvt-fetch',
	'cache_root'         => "$ENV{HOME}/.cache",
        'default_expires_in' => $cache_time,
    );
    $ua = LWP::UserAgent::WithCache->new(\%cache_opt);
} else {
    $ua = LWP::UserAgent->new(keep_alive => 1);
}

$ua->agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0");

my @total_results;
for my $tile (@tiles) {
    my ($z, $x, $y) = @$tile;
    my $file = "/tmp/tile_${z}_${x}_${y}.mvt";
    warn "INFO: fetch and process $file...\n";
    #if (!-s $file) { # always use cached version
    {
	my $url = "https://tiles.mapillary.com/maps/vtp/mly1_public/2/$z/$x/$y?access_token=$client_token";
	my $response = $ua->get($url);
	if ($response->is_success) {
	    my $content = $response->content;
	    # workaround for HTTP::Response bug https://github.com/libwww-perl/HTTP-Message/issues/48, fixed in 7.01
	    if ($do_cache && $HTTP::Response::VERSION <= 7.00) {
		$content =~ s{\n$}{};
	    }
	    open my $fh, '>', "$file~" or die $!;
	    binmode $fh;
	    print $fh $content;
	    close $fh;
	    rename "$file~", $file or die $!;
	} else {
	    warn "ERROR: Failed for $url:\n" . $response->dump . "\n";
	    next;
	}
    }

    # Call Python or Perl script for mvt/protobuf decoding
    my $json;
    if ($mvt_impl eq 'py') {
	$json = `python3 $FindBin::RealBin/parse_mvt_sequences.py $file "$date_from" "$date_to"`;
    } else {
	$json = `$^X $FindBin::RealBin/parse_mvt_sequences.pl $file "$date_from" "$date_to"`;
    }
    my $results = eval { decode_json($json) };
    if ($results && ref $results eq 'ARRAY') {
	push @total_results, @$results;
    }
    # keep cached file!        unlink $file; # Optional: cleanup
}

print <<'EOF';
#: map: polar
#: line_arrow: last
#: 
EOF

for my $seq (sort {
       ($a->{start_captured_at} cmp $b->{start_captured_at})
    || ($a->{coordinates}[0][1] <=> $b->{coordinates}[0][1])
    || ($a->{coordinates}[0][0] <=> $b->{coordinates}[0][0])
    || ($a->{sequence}          cmp $b->{sequence})
} @total_results) {
    print "#: url: $seq->{url}\n";
    print "start_captured_at=$seq->{start_captured_at} creator=$seq->{creator} make=$seq->{make} end_captured_at=$seq->{end_captured_at} start_id=$seq->{start_id} sequence=$seq->{sequence}\tX " . join(" ", map { "$_->[1],$_->[0]" } @{ $seq->{coordinates} }) . "\n";
}

__END__

=head1 NAME

mapillary-mvt-fetch - Fetch, decode and display Mapillary sequences in BBBike

=head1 SYNOPSIS

mapillary-mvt-fetch [options] [DATE_SPEC]

  mapillary-mvt-fetch
  mapillary-mvt-fetch 20250101
  mapillary-mvt-fetch --bbox 13.3,52.4,13.5,52.6 20250101-20250103
  mapillary-mvt-fetch --mvt-impl py --zoom 14 20250101-

=head1 DESCRIPTION

B<mapillary-mvt-fetch> retrieves Mapillary vector tiles (MVT format),
decodes them into sequence data, and outputs the result as C<.bbd> files
understood by C<bbbike> for visualization.

The script is a high-level orchestration tool combining:

=over 4

=item *

tile computation from a bounding box

=item *

tile download from the Mapillary vector tile API

=item *

decoding via helper scripts

=item *

formatting as C<.bbd> files for BBBike display

=back

=head2 OPTIONS

=over 4

=item B<--bbox>=I<minlon,minlat,maxlon,maxlat>

Bounding box in WGS84 coordinates.

If not specified, a default bounding box (the Berlin area) is used.

=item B<--zoom>=I<level>

Zoom level used for tile computation.

Default: 12

=item B<--mvt-impl>=I<pl|py>

Select implementation used for decoding MVT tiles:

=over 4

=item *

C<pl> (default): use C<parse_mvt_sequences.pl>

=item *

C<py>: use C<parse_mvt_sequences.py>

=back

=item B<--[no]cache>

Enable or disable HTTP caching.

Default: enabled.

Uses C<LWP::UserAgent::WithCache> with cache root:

  ~/.cache

Cache expiration is 8 hours.

=back

=head2 DATE SPECIFICATION

An optional positional argument defines the date range.

Supported formats:

=over 4

=item B<YYYYMMDD>

Single day.

Internally expanded to:

  YYYYMMDD - next day

=item B<YYYYMMDD->

Open-ended range starting at the given date.

=item B<YYYYMMDD-YYYYMMDD>

Explicit range.

Note: the end date is typically specified as the B<next day>
to include a single day.

Example:

  20250102-20250103   # selects 2025-01-02

=back

If no date is given, the default is:

=over 4

=item *

yesterday (local time) as start date

=item *

no explicit end date

=back

=head2 CONFIGURATION

=head3 Mapillary access token

The Mapillary API access token is either specified with the C<--token>
option or read from F<~/.mapillary>. This file must be in YAML format
and contain:

  client_token: YOUR_TOKEN

The script aborts if no token is found.

=head2 TYPICAL USAGE

Download and transform Mapillary vector tiles, inject metadata
(creator and make), and display in BBBike:

    cd .../src/bbbike
    DATE=20260101
    ./miscsrc/mapillary-mvt-fetch $DATE > /tmp/$DATE.bbd
    ./miscsrc/mapillary-inject-metadata /tmp/$DATE.bbd
    ./bbbikeclient /tmp/$DATE.bbd

=head2 INTERNAL WORKFLOW

=head3 1. Tile computation

The bounding box is converted into tile coordinates using the standard
Web Mercator tiling scheme.

All tiles covering the bounding box at the selected zoom level are
enumerated.

=head3 2. Tile download

Each tile is fetched from:

  https://tiles.mapillary.com/maps/vtp/mly1_public/2/Z/X/Y

The access token is appended as a query parameter.

Tiles are stored temporarily as:

  /tmp/tile_Z_X_Y.mvt

=head3 3. MVT decoding

Each tile is decoded by invoking one of:

=over 4

=item *

C<parse_mvt_sequences.pl>

=item *

C<parse_mvt_sequences.py>

=back

The scripts are called internally as:

  parse_mvt_sequences.* FILE DATE_FROM DATE_TO

They return JSON, which is decoded and accumulated.

These helper scripts are not intended for direct use.

=head3 4. Output formatting

The combined results are sorted by:

=over 4

=item *

start_captured_at

=item *

latitude / longitude

=item *

sequence id

=back

The output is formatted in C<.bbd> format:

=over 4

=item *

directive lines with a direct Mapillary URL

=item *

sequence metadata (capture date, creator...)

=item *

polyline coordinates

=back

=head3 5. Display in BBBike

The output is meant to be used in C<bbbike> or C<bbbikeclient> which
displays the sequences on a map.

=head2 PREREQUISITES

=head3 General

=over 4

=item *

Perl

=item *

Network access

=item *

Valid Mapillary access token in C<~/.mapillary>

=back

=head3 C<--mvt-impl=py>

=over 4

=item *

Python 3

=item *

mapbox_vector_tile module

=back

=head3 C<--mvt-impl=pl>

=over 4

=item *

either L<Google::ProtocolBuffers::Dynamic> or L<Google::ProtocolBuffers>

=back

=head3 Optional (caching)

=over 4

=item *

L<LWP::UserAgent::WithCache>

=item *

L<HTTP::Date>

=back

=head1 SEE ALSO

L<parse_mvt_sequences.pl>,
L<parse_mvt_sequences.py>,
L<bbbikeclient>

=head1 AUTHOR

Slaven Rezic

=cut
