#!/usr/local/bin/perl
# -----------------------------------------------------------------
#
# 'csv2ms-egs.pl' - CSV to MapSource
#
# A script to read coordinates in CSV format, convert them into
# "pol(n)ish map format", and compile them with cgpsmapper.
#
# This particular script prepares the complete mapset of
# "Europaeische Gefahrstellen" (EGS).
#
# IMPORTANT: Some parts of this script will NOT work with some
# particular versions of cgpsmapper, due to restricted functionality
# of the later versions! In particular the generation of the overwiew
# map requires a de-compilation step, which is not possible with
# recent versions of cgpsmapper.
# My workaround is to run the compilation twice: first with
# cgpsmapper-v81, then with cgpsmapper-v93 (or more recent).
#
# Thus, you need to have both versions of cgpsmapper available!
#
# -----------------------------------------------------------------
# This program is free software; you can redistribute it and/or
# modify it under the terms of the version 2 of the GNU General
# Public License as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# -----------------------------------------------------------------
#
# Copyright (c) 2007...2010 Joerg Hau <hau.joerg ät gmail.com>.
#
# Revision History:
#   2007-01-26, JHa, first draft
#   2007-01-30, JHa, essentially operational
#   2007-02-05, JHa, annoying bug in registry stuff fixed
#   2007-02-07, JHa, added overview map. 
#   2007-02-08, JHa, workaround to achieve hiding of points 
#                    at higher zoom-out levels.
#   2007-02-10, JHa, back to 2-digit version numbering again.
#   2007-05-30, JHa, added counter for "items in range".
#   2008-02-13, JHa, adapted to cgpsmapper 93c.
#   2008-11-30, JHa, Map header adapted for use with MapSource 6.14;
#                    added workaround for cgpsmapper 81 vs. 93c. 
#   2010-07-01, JHa, minor stuff for 201x years.
# -----------------------------------------------------------------

use File::Path;     # for deleting files (near end of script)
use strict;
use warnings;

$|=1;       	    # flush on (to show everything immediately)


# -----------------------------------------------------------------
# some global variables
#
my (@lon, @lat);    # arrays to hold longitude, latitude data
my $idx = 0;        # index to said arrays
my ($lon_max, $lon_min, $lat_max, $lat_min);    # min/max borders

my $DEBUG = 0;	    # set != 0 for debugging messages

# -----------------------------------------------------------------
# stuff that changes from edition to edition
# could be passed on the cmd line, too ;-)
#
my $HeaderTitle="Gefahrstellen"; # Name shown in MapSource
my $HeaderYear="2010";           # exactly 4 digits
my $HeaderVersion="06";          # exactly 2 digits

# -----------------------------------------------------------------
# Since MapSoure 6.14, this MUST be a 3-digit code. 
# Make this unique per product, then do NOT change it anymore.
#
my $ProductCode="246";

# -----------------------------------------------------------------
# auto-compose some names. Maybe I'm over-complicating things here.
#
my $MapVersion = $HeaderYear . $HeaderVersion;  # concatenate
   $MapVersion =~ s/201//;   # must be exactly 3 digits, so remove "201"

my $MapSourceName = $HeaderTitle . " " . $HeaderYear;

# This will be used to compose the names for the overview, TDB and registry files
my $HeaderFileName= "EGS" . $HeaderYear;

# This will be used to compose the names for the .img files
my $FilePrefix = $ProductCode . $MapVersion;    # makes a total of 6 chars

my $Fmt = "%02d";         # format string for sprintf(), should add up
                          # to 8 chars with $HeaderMapVersion.

# -----------------------------------------------------------------
# Full path to map compiler.
# In my case, this is a symlink to cgpsmapper093c-static (since this
# version does NOT print an annoying (and false!) text into every map)
#
# *** Read the "IMPORTANT" section at the start of this file! ***
#
my $Mapper1="./cgpsmapper81";    # for the first run
my $Mapper2="./cgpsmapper";      # for the second run

my $Mapper;                     # generic
my $MapperVersion = "93c";      # (can be) used for dynamic bugfix

# -----------------------------------------------------------------
# Full path to the contour map. This is a pre-prepared file in .MP
# format that contains the contours of European countries.
#
my $ContourMap="./contour-europe.pfm";


# -----------------------------------------------------------------
# Print usage mode
# -----------------------------------------------------------------
sub usage
{
print STDERR <<EOF
$0 - Read coordinates from CSV format and convert them
into "pol(n)ish map format", then compile with cgpsmapper.

This file creates the mapset of "Europaeische Gefahrstellen".

Copyright (c) 2007...2010 Joerg Hau <joerg.hau(at)dplanet.ch>.

This program is free software; you can redistribute it and-or
modify it under the terms of version 2 of the GNU General Public
License as published by the Free Software Foundation.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

    Usage:  $0 csv1 [csv2 ...]

Please read the information at the start of this script!

EOF
}


# -----------------------------------------------------------------
# reads point coordinates in CSV format into global hashtable
# argument: filename to read (no wildcard)
# returns: 1 if OK, else dies
# NOTE: CSV files usually store coordinates as "lon,lat",
#       while GPX data are given as "lat,lon" !
# NOTE: This can be extended easily to store labels, symbols, etc.
# -----------------------------------------------------------------
sub read_csv {
  my $csvfile = shift;

  print STDERR ("Going to read '$csvfile' ... ") if $DEBUG;
  open (CSV, "<", $csvfile) || die "Can't open '$csvfile': $!";
  while( <CSV> ) {
    if (my @values = split(',')) {  # separate at commas
      $lon[$idx] = $values[0];      # first field is longitude
      $lat[$idx] = $values[1];      # second field is latitude
      $idx++;                       # increase index counter
      }
    }
  close (CSV);
  print STDERR ("done, Index now $idx.\n") if $DEBUG;
  return $idx;
}

# -----------------------------------------------------------------
# gets min/max of dataset (from global array)
# sets/changes global variables $lon_max, $lon_min, $lat_max, $lat_min
# -----------------------------------------------------------------
sub getminmax {
  # pre-set min/max values
  $lon_max = -180;
  $lon_min = +180;
  $lat_max = -90;
  $lat_min = +90;

# go through all data points and find min, max
  for my $i ( 0 .. $idx-1 ) {
    if ($lon[$i] > $lon_max) { $lon_max = $lon[$i] };
    if ($lat[$i] > $lat_max) { $lat_max = $lat[$i] };
    if ($lon[$i] < $lon_min) { $lon_min = $lon[$i] };
    if ($lat[$i] < $lat_min) { $lat_min = $lat[$i] };
    }
}


# -----------------------------------------------------------------
# generate file for generating ;-) the overview map
# argument: file name, number of files
# -----------------------------------------------------------------
sub create_index {
my $fnam=shift;
my $maxidx=shift;

my $i;

# Note: This map will be generated from an "empty" layer. If we would 
# generate a "real" preview map of the POI, this would include _all_ POI
# ... which would slow down MapSource and overfill the display at zoom-out.
#
my $hdr="[Map]
FileName=$HeaderFileName
MapVersion=$MapVersion
ProductCode=$ProductCode
Levels=2
Level0=18
Level1=17
Zoom0=5
Zoom1=6
MapsourceName=$MapSourceName
MapSetName=$HeaderFileName
CDSetName=$HeaderTitle
Copy1=Created by Joerg_H
Copy2=This is a free map. Commercial distribution is NOT allowed!
[End-Map]

[Files]";

  print STDERR ("Writing '$fnam' ... ") if $DEBUG;
  open (OUT, ">", $fnam) || die "Can't open '$fnam' : $!";
  print OUT $hdr . "\n" || die "Error in create_index(): $!";

  for ($i = 1; $i <= $maxidx; $i++) {
    print OUT "img=" . $FilePrefix . sprintf ($Fmt, $i) . ".img\n" || die "Error in create_index(): $!";
    }
  print OUT "[END-Files]\n" || die "Error in create_index(): $!";
  close (OUT);
  print STDERR ("done.\n") if $DEBUG;
}


# -----------------------------------------------------------------
# creates header for a single .mp file
# argument: map ID (an ongoing number)
# note: OUT must be open!
# -----------------------------------------------------------------
sub print_header {
my $id=shift;

# The map will mainly consist of POIs. By default on my GPSmap 60CS,
# "naked" POI are already visible at a zoom setting of about 12 km
# (Detail="normal"), which is far too early for my purpose.
# To avoid this, we have to insert a layer with "something" that
# hides the POIs, until a pre-defined level is reached. Thus we need
# a total of 4 levels (explained from top to bottom):
#
# Level3=18 is an empty layer (required).
# Level2=19 defines a layer that is visible _until the next level_
#           (here, Level1) is reached. This layer will contain "something" 
#           to hide the POI: Theoretically an empty polygon of the size of 
#           the map is sufficient. However, cgpsmapper81 has a bug that
#           requires to give every single point his own "cover polygon".
# Level1=20 is present "just" to define the next level below the polygon(s).
#           Without this layer, the POI would only be visible once we reach
#           "their" level. - Data are identical to Level0.
# Level0=24 is the "data layer".
#
# With these settings, the POI are visible:
# - in MapSource, detail "medium": 3 km and below.
# - in MapSource, detail "maximal": 10 km and below.
#
# - on the GPSmap 60CS, detail "normal": 2 km and below.
# - on the GPSmap 60CS, detail "more": 3 km and below.
# - on the GPSmap 60CS, detail "most": 8 km and below.
#
# The GPSmap 60CX has a very different behaviour - here, the setting
# for "map points" has to be adjusted, otherwise the POI are only 
# visible when you zoom in _very_ close:
# - on the GPSmap 60Cx, detail "normal": 200 m and below.
# - on the GPSmap 60Cx, detail "more": 300 m and below.
# - on the GPSmap 60Cx, detail "most": 500 m and below.
#
# If you want the data points to be visible "even further" in MapSource,
# you will probably have to introduce yet another layer (with the same
# information as in Level0/1)

my $hdr="[IMG ID]
ID=$id
Name=$HeaderTitle $id
LblCoding=9
CopyRight=Created by Joerg_H
Transparent=Y
Elevation=m
TreSize=1000
RgnLimit=1024
DrawPriority=1
Levels=4
Level0=24
Level1=20
Level2=19
Level3=18
[END]";

print OUT "$hdr\n\n" || die "Error in print_header(): $!";
}


# -----------------------------------------------------------------
# creates border of coordinate grid as [RGN80]-set
# argument: coords of left, right, bottom, top
# note: OUT must be open!
# We use 0x004a, "definition area"
#
# NOTE: DO NOT use with cgpsmapper v81; it has a bug with datasets
#       around latitude 0; and it ignores the frame anyway ... ?
# -----------------------------------------------------------------
sub print_frame {
  my $left = shift;
  my $right = shift;
  my $bottom = shift;
  my $top = shift;

  print OUT "[RGN80]\nType=0x004a\n" || die "Error in print_frame(): $!";
  print OUT "Data2=($top,$left),($top,$right),($bottom,$right),($bottom,$left)\n" || die "Error in print_frame(): $!";
  print OUT "[END-RGN80]\n\n" || die "Error in print_frame(): $!";
}


# -----------------------------------------------------------------
# prints POI coordinates as [RGN10]-points
# argument: input lat, input lon
# note: OUT must be open!
# note: type is fixed, no label possible in this version
# -----------------------------------------------------------------
sub print_rgn {
  my $lat  = shift;
  my $lon  = shift;

  # type 0x1610 is "lighted Navaid, red" ... but only if [RGN10] is used!
  # RGN20 would be more suitable but causes problem w/ the symbol?
  #
  print OUT "[RGN10]\nType=0x1610\nEndLevel=1\n" || die "Error in print_rgn(): $!";
  print OUT "Data0=($lat,$lon)\n"  || die "Error in print_rgn(): $!";
  print OUT "[END-RGN10]\n\n"      || die "Error in print_rgn(): $!";

  if ($MapperVersion eq "81")
    {
    # the following lines are a workaround for a bug in cgpsmapper81; we create
    # an invisible Polygon above each and every point that hides it on zooming out:
    #
    my $off=0.001;
    my $left = $lon-$off;
    my $right = $lon+$off;
    my $bottom = $lat-$off;
    my $top = $lat+$off;

    print OUT "[RGN80]\nType=0x004a\n" || die "Error in print_rgn(): $!";
    print OUT "Data2=($top,$left),($top,$right),($bottom,$right),($bottom,$left)\n" || die "Error in print_rgn(): $!";
    print OUT "[END-RGN80]\n\n"      || die "Error in print_rgn(): $!";
    }
}


# -----------------------------------------------------------------
# run cgpsmapper to compile the maps
# argument: filename prefix, number of files
# -----------------------------------------------------------------
sub compile_maps {
  my $prefix=shift;
  my $maxidx=shift;

  my ($cmd, $i, $redir);

  print STDERR ("Compiling $maxidx maps ");  # show status

  # process all the individual maps
  #
  for ($i = 1; $i <= $maxidx; $i++) {
    if (1 == $i) { $redir = "> " }  # for the first map, overwrite the logfile if it exists
      else       { $redir= ">>"  }  # for all other maps, append to logfile
    
    $cmd = "$Mapper ac -l " . $prefix . sprintf ($Fmt, $i) . ".mp $redir $HeaderFileName.log";
    print STDERR ("Executing '$cmd' ..") if $DEBUG;
    (system ($cmd) == 0) || die "Error in compile_maps() at map $i: $!";
    print STDERR (".");       # one dot per map
    print STDERR (" done.\n") if $DEBUG;
    }

  # now process the overview map
  #
  $cmd = "$Mapper pv -l $HeaderFileName" . ".txt $redir $HeaderFileName.log";
  (system ($cmd) == 0) || die "Error in compile_maps() at overview map: $!";

  # a "nice to have" is the contour map of Europe. To integrate this, we need 
  # to de-compile the overview map back into .mp, add the reference to the file
  # with the contour data, then re-compile this file again.
  #
  # FIXME: Note that the de-compilation step is no longer possible with
  # some versions of cgpsmapper - you may have to run this script with
  # two different compilers!
  #
  $cmd = "$Mapper -l $HeaderFileName" . ".img $redir $HeaderFileName.log";
  (system ($cmd) == 0) || die "Error in compile_maps(), step 2: $!";

  my $fnam=$HeaderFileName . ".mp";
  open (OUT, ">>", $fnam) || die "Can't open '$fnam': $!";
  print OUT "[FILE]\nname=$ContourMap\n[END]\n" || die "Error in compile_maps(), step 3: $!";
  close OUT;

  $cmd = "$Mapper -l $HeaderFileName" . ".mp $redir $HeaderFileName.log";
  (system ($cmd) == 0) || die "Error in compile_maps(), step 4: $cmd -- $!";

  print STDERR (" done.\n");  # show status
}


# -----------------------------------------------------------------
# generate REG file for Micro$**t Windows
# argument: none; uses global variables
# Yes I know, a number of things are hardcoded here ;-)
# -----------------------------------------------------------------
sub make_reg {
  my $fnam = $HeaderFileName . ".reg";
  my $path = "C:\\\\apps\\\\Garmin\\\\$HeaderFileName";

  print STDERR ("Generating registry file '$fnam' ... ") if $DEBUG;

  open (OUT, ">", $fnam) || die "Can't open '$fnam' : $!";

  print OUT "REGEDIT4\r\n\r\n";
  print OUT "[HKEY_LOCAL_MACHINE\\SOFTWARE\\Garmin\\MapSource\\Products\\$ProductCode]\r\n";
  print OUT "\"LOC\"=\"$path\\\\img\"\r\n";
  print OUT "\"BMAP\"=\"$path\\\\$HeaderFileName.img\"\r\n";
  print OUT "\"TDB\"=\"$path\\\\$HeaderFileName.tdb\"\r\n";

  close (OUT);
  print STDERR ("done.\n") if $DEBUG;
}


# -----------------------------------------------------------------
# move files into subdirectories
# argument: max. filename index; otherwise uses global variables
# -----------------------------------------------------------------
sub pack_files {
my $f_idx = shift;
my ($i, $fnam);

print STDERR ("Moving files ... ");

# if directory exists, remove it (!without asking!),
# then create the new (empty) directrories
#
if ( -d $HeaderFileName ) {
  rmtree ($HeaderFileName) || die  "Can't rmtree '$HeaderFileName': $!";
  }
mkdir ($HeaderFileName)  || die  "Can't mkdir '$HeaderFileName': $!";
mkdir ($HeaderFileName . "/img/") || die  "Can't mkdir '$HeaderFileName/img/': $!";

# move files
#
rename ($HeaderFileName. ".reg", $HeaderFileName ."/". $HeaderFileName. ".reg") || die  "Can't move .reg file: $!";
rename ($HeaderFileName. ".img", $HeaderFileName ."/". $HeaderFileName. ".img") || die  "Can't move .img file: $!";
rename ($HeaderFileName. ".TDB", $HeaderFileName ."/". $HeaderFileName. ".TDB") || die  "Can't move .TDB file: $!";
for ($i = 1; $i <= $f_idx; $i++) {
  $fnam = $FilePrefix . sprintf ($Fmt, $i) . ".img";    # compose filename
  rename ($fnam, $HeaderFileName . "/img/". $fnam) || die  "Can't move '$fnam': $!";
  $fnam =~ s/img/mp/;                                   # prepare to delete .mp file
  (unlink ($fnam) || die  "Can't delete '$fnam': $!") unless $DEBUG;
  }
print STDERR ("done.\n");
}


# -----------------------------------------------------------------
# main program starts here
# -----------------------------------------------------------------
require Getopt::Std;
my %opt;                                   # to store the options
Getopt::Std::getopts('hd',\%opt);

# read command line options
#
if($opt{'d'}) {                             # debug messages on
    $DEBUG="1";
    }
if($opt{'h'} or @ARGV==0){                  # help
    usage();
    exit 0;
}

# read all csv files into array
#
my $file;
foreach $file (@ARGV) {
	read_csv($file);
    }

getminmax();

print STDERR "MapsourceName is \"$MapSourceName\", FileName is \"$HeaderFileName\", MapVersion is \"$FilePrefix\".\n";
print STDERR "Read $idx coordinates.\n";
print STDERR "Dataset borders: Longitude $lon_min..$lon_max, latitude $lat_min..$lat_max.\n";

# divide the coordinates into "packets" of 4 degrees lat/lon
# the range given here covers geographical Europe:
#
my ($i, $x, $y);
my $grid = 4;
$lat_min = 31;
$lat_max = 80;
$lon_min = -14;
$lon_max = 35;

print STDERR "Going to group: Longitude $lon_min..$lon_max, latitude $lat_min..$lat_max, interval $grid ...";

my $fnam_idx=1;     # starting number for maps
my $done = 0;       # just a flag, to mark if the file was already started
my $cnt = 0;        # just to count the number of "items" inside the borders

for ($y = $lat_min; $y <= $lat_max; $y+=$grid ) {       # latitude loop
  for ($x = $lon_min; $x <= $lon_max; $x+=$grid ) {     # longitude loop
    for $i ( 0 .. $idx-1 )                              # loop over all data
	  {
      if (($lat[$i] >= $y) && ($lat[$i] < $y+$grid) && ($lon[$i] >= $x) && ($lon[$i] < $x+$grid))
        {         # if a data point is inside the actual grid
        if (! $done)
          {       # if the file is not yet open, open it
          my $fnam = $FilePrefix . sprintf ($Fmt, $fnam_idx) . ".mp";
          print STDERR ("Writing '$fnam' ... ") if $DEBUG;
          open (OUT, ">", $fnam) || die "Can't open '$fnam' : $!";
          # write the header information
          print_header ($FilePrefix . (sprintf ($Fmt, $fnam_idx)));
          if ($MapperVersion ne "81")
            {
            # do not use this line with cgpsmapper81: bug with coordinates around latitude 0!
            print_frame ($x, $x+$grid, $y, $y+$grid);
            }
          $done = 1;        # set flag
          $fnam_idx +=1;    # increase counter
          }
        print_rgn ($lat[$i], $lon[$i]);   # output the data point
        $cnt++;                           # increase counter
        }
      }                 # end of data loop
      if ($done) {      # only if a file was open ;-)
        close (OUT);
        print STDERR ("done.\n") if $DEBUG;
        $done = 0;
        }
   }
}

$fnam_idx -=1;      # correct index counter by 1.
print STDERR (" done: $fnam_idx maps, $cnt points.\n");

# TODO: Intercept when mapnumber exceeds 99 ;-)?

# build the "overview" file
#
my $fnam = $HeaderFileName . ".txt";
create_index ($fnam, $fnam_idx);

# , then compile the maps, need to run this twice with different compilers; see explanation at the beginning of this file.

$Mapper = $Mapper1;
print STDERR ("First run - using compiler $Mapper.\n");
compile_maps ($FilePrefix, $fnam_idx);

$Mapper = $Mapper2;
print STDERR ("Second run - using compiler $Mapper.\n");
compile_maps ($FilePrefix, $fnam_idx);

# build registry stuff, then clean up:
make_reg();
pack_files($fnam_idx);

print STDERR ("Finished.\n");
