#!/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, 2008 Hau . # # 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. # ----------------------------------------------------------------- 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="2008"; # exactly 4 digits my $HeaderVersion="11"; # 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/200//; # must be exactly 3 digits, so remove "200" 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 <. 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( ) { 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: # - 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. # - in MapSource, detail "medium": 3 km and below. # - in MapSource, detail "maximal": 10 km 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");