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

#
# Author: Slaven Rezic
#
# Copyright (C) 2009,2010,2011,2012,2013,2014,2016,2017,2020,2024 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.
#
# Mail: slaven@rezic.de
# WWW:  http://www.rezic.de/eserte/
#

use strict;
use FindBin;
use lib (
	 "$FindBin::RealBin/..",
	 "$FindBin::RealBin/../lib",
	);

use Fcntl qw(SEEK_SET);
use File::Temp qw(tempfile);
use Getopt::Long;
use Tie::IxHash ();

use Geography::Berlin_DE ();
use Karte::Polar;
use Karte::Standard;
use PLZ;
use Strassen::Core;
use Strassen::Strasse;
use VectorUtil qw(get_polygon_center);

use vars qw($VERSION);
$VERSION = '1.09';

# Monkeypatch for performance reasons
# See http://rt.cpan.org/Ticket/Display.html?id=39619
if (!defined &Tie::IxHash::SCALAR) {
    *Tie::IxHash::SCALAR = sub {
	scalar @{ $_[0]->[1] };
    };
}

sub usage () {
    die <<EOF;
usage: $0 [-ignore-missing] [-use-orig] [-quiet] [-debug] [-experiment ...] [datadir] [-optimize-for garmin|mapnik|mapnik-bbbike]
       $0 [-type area|waterway|highway|railway|ferry] -single bbdfile

Either convert a whole bbbike data directory, or convert a single bbd
file.
EOF
}

my %known_experiments = map {($_,1)} qw(cycle-route addr mount);

my $single_file;
my $single_file_type;
my $fragezeichen_file;
my $ignore_missing;
my $use_orig;
my $optimize_for_string = '';
my $shorten_for_garmin;
my $quiet;
my $debug;
my $do_highway_primary_hack = 0;
my $do_print_version;
my %experiments;
my $out_file;
GetOptions("single=s" => \$single_file,
	   "type=s" => \$single_file_type,
	   "fragezeichen=s" => \$fragezeichen_file,
	   "ignore-missing!" => \$ignore_missing,
	   "use-orig!" => \$use_orig,
	   "optimize-for=s" => \$optimize_for_string,
	   "shorten-for-garmin!" => \$shorten_for_garmin,
	   "quiet!" => \$quiet,
	   "debug!" => \$debug,
	   'experiment=s@' => sub {
	       my $exp = $_[1];
	       if (!$known_experiments{$exp}) {
		   die <<EOF;
Unknown experiment '$exp'. Known experiments are:
@{[ sort keys %known_experiments ]}
EOF
	       }
	       $experiments{$exp}++;
	   },
	   'o|outfile=s' => \$out_file,
	   "version" => \$do_print_version,
	  )
    or usage;

if ($do_print_version) {
    print "bbd2osm $VERSION\n";
    exit 0;
}

use constant OPTIMIZE_FOR_NONE           => 0;
use constant OPTIMIZE_FOR_GARMIN         => 1;
use constant OPTIMIZE_FOR_MAPNIK         => 2;

use constant OPTIMIZE_FOR_MAPNIK_STYLE_NONE    => 0;
use constant OPTIMIZE_FOR_MAPNIK_STYLE_DEFAULT => 1;
use constant OPTIMIZE_FOR_MAPNIK_STYLE_BBBIKE  => 2;
my $optimize_for              = OPTIMIZE_FOR_NONE;
my $optimize_for_mapnik_style = OPTIMIZE_FOR_MAPNIK_STYLE_NONE;
if ($optimize_for_string) {
    if ($optimize_for_string !~ m{^(garmin|mapnik|mapnik-bbbike)$}) {
	warn "-optimize-for takes only value 'garmin' or 'mapnik' or 'mapnik-bbbike'\n";
	usage;
    }
    if ($optimize_for_string eq 'garmin') {
	$shorten_for_garmin = 1;
	$optimize_for = OPTIMIZE_FOR_GARMIN;
    } elsif ($optimize_for_string eq 'mapnik') {
	$optimize_for              = OPTIMIZE_FOR_MAPNIK;
	$optimize_for_mapnik_style = OPTIMIZE_FOR_MAPNIK_STYLE_DEFAULT;
    } elsif ($optimize_for_string eq 'mapnik-bbbike') {
	$optimize_for              = OPTIMIZE_FOR_MAPNIK;
	$optimize_for_mapnik_style = OPTIMIZE_FOR_MAPNIK_STYLE_BBBIKE;
    } else {
	die "Should never happen (optimize_for=$optimize_for_string)";
    }
}

my $datadir;
if (!$single_file) {
    $single_file_type and usage;
    $datadir = shift || "$FindBin::RealBin/../data";
} else {
    @ARGV and usage;
}

######################################################################
# Max. length on Garmin devices (at least etrex vista hcx) is 151
use constant MAX_LABEL_LENGTH => 151;
use constant MAX_ABBREV_LEVEL => 4;
my %ABBREV_WORD_DEFS = ( 'Uhr' => 'h',
			 'und' => '&',
			 'Montag' => 'Mo',
			 'montags' => 'mo',
			 'Freitag' => 'Fr',
			 'freitags' => 'fr',
			 'Samstag' => 'Sa',
			 'Sonntag' => 'So',
			 'pro Person' => 'p.P.',
			 'Euro' => "\x{20ac}",
			 'EUR' => "\x{20ac}",
			 'bis' => '-',
			 'stndlich' => 'stndl.',
			 'tglich' => 'tgl.',
			 'Minuten' => 'Min',
			 'April' => 'Apr',
			 'August' => 'Aug',
			 'Oktober' => 'Okt',
			 'Fahrzeit' => 'Fahrzt.',
		       );
my $ABBREV_WORD_RX = '(' . join("|", map { '\b'.quotemeta($_).'\b' } keys %ABBREV_WORD_DEFS) . ')';
$ABBREV_WORD_RX = qr{$ABBREV_WORD_RX};
######################################################################

my($wayfh, $wayfile) = tempfile(UNLINK => 1, SUFFIX => "_way.xml");
binmode $wayfh, ':utf8';
my($relationfh, $relationfile) = tempfile(UNLINK => 1, SUFFIX => "_relation.xml");
binmode $relationfh, ':utf8';

my $next_node_id = 1;
my $next_way_id = 1;
my $next_relation_id = 1;
tie my %node, 'Tie::IxHash'; # pxy => { id => $id, $k => $v, ... }

my %oneway;
my %no_access;
my %speed0;
my @remember_quality;
my @remember_handicap;
my %remember_steps; # node_id -> { steps => ... }
my %unlit;
my %green;

my %cat_to_highway = ('B'  => 'primary', # See also $do_highway_primary_hack
		      'HH' => 'primary',
		      'H'  => 'secondary',
		      'NH' => 'tertiary',
		      'N'  => 'residential',
		      'NN' => 'cycleway', # XXX
		      'BAB' => 'motorway', # or motorroad (Kfz-Strae)
		     );

my %cat_to_railway = ('U' => 'subway',
		      'S' => 'light_rail',
		      'R' => 'rail',
		      'RG' => 'rail', # XXX is there a better tag?
		      'RP' => 'narrow_gauge', # not quite correct, but in most cases a good approximation
		     );

my %cat_to_area_landuse = ('Forest'     => 'forest',
			   'Cemetery'   => 'cemetery',
			   ($optimize_for != OPTIMIZE_FOR_GARMIN ? ('Industrial' => 'industrial') : ()),
			   # 'Ae' => 'airport', # does not exist
			   'Orchard'    => 'allotments',
			   ($optimize_for != OPTIMIZE_FOR_MAPNIK ? ('Sport' => 'sports') : ()),
			   'Green'      => 'village_green',
			  );
my %cat_to_area_bbbike_landuse = (
				  ($optimize_for == OPTIMIZE_FOR_GARMIN ? ('Industrial' => 'industrial') : ()),
				 );
my %cat_to_area_leisure = ('P'          => 'park',
			   'Pabove'     => 'park',
			   ($optimize_for == OPTIMIZE_FOR_MAPNIK ? ('Sport' => 'sports_centre') : ()),
			  );

my %bbbikequality_to_smoothness = (0 => 'excellent',    # Q0
				   1 => 'good',         # Q1
				   2 => 'intermediate', # Q2
				   3 => 'bad',          # Q3
				  );

my $geo = Geography::Berlin_DE->new; # currently hardcoded for Berlin

my $street_to_citypart = do {
    if (!$datadir) { # single file mode, no $datadir set
	+{};
    } else {
	my $plz = eval { PLZ->new("$datadir/Berlin.coords.data") };
	if (!$plz) {
	    warn "WARN: could not load PLZ file ($datadir/Berlin.coords.data), continue with empty street-to-citypart mapping...\n";
	    +{};
	} else {
	    $plz->load;
	    $plz->make_any_hash(PLZ::FILE_NAME, PLZ::FILE_CITYPART);
	}
    }
};

my($min_lat, $max_lat, $min_lon, $max_lon);

if ($single_file) {
    $single_file_type ||= 'highway';
    my $subname = 'handle_' . $single_file_type . '_like';
    no strict 'refs';
    if (!defined &{$subname}) {
	die "Invalid type $single_file_type, no callback function exists for that.\n";
    }
    my $sub = \&{$subname};
    Strassen->new_stream($single_file)->read_stream($sub);
} else {
    # NODE-like
    do_file("$datadir/ampeln", \&handle_trafficsignals_like);
    if (-r "$datadir/zebrastreifen") {
	do_file("$datadir/zebrastreifen", \&handle_zebrastreifen_like);
    }
    do_file("$datadir/ubahnhof", \&handle_railway_stations_like);
    do_file("$datadir/sbahnhof", \&handle_railway_stations_like);
    do_file("$datadir/rbahnhof", \&handle_railway_stations_like);
    do_file("$datadir/gesperrt", \&handle_blocked_node_like);

    do_file("$datadir/orte", \&handle_city_like);
    do_file("$datadir/orte2", \&handle_city_like);
    do_file("$datadir/ortsschilder", \&handle_city_limit_like);
    do_file("$datadir/berlin_ortsteile", \&handle_berlin_citypart_like);

    do_file("$datadir/comments_ferry", \&handle_ferry_info_like);
    do_file("$datadir/comments_scenic", \&handle_comments_scenic_like);

    # WAY-like
    do_file("$datadir/gesperrt", \&handle_blocked_way_like);
    do_file("$datadir/handicap_s", \&handle_handicap_like);
    do_file("$datadir/handicap_l", \&handle_handicap_like);
    do_file("$datadir/qualitaet_s", \&handle_handicap_like);
    do_file("$datadir/qualitaet_l", \&handle_handicap_like);
    do_file("$datadir/nolighting", \&handle_nolighting_like);
    do_file("$datadir/green", \&handle_green_like);

    do_file("$datadir/flaechen", \&handle_area_like);

    do_file("$datadir/wasserstrassen", \&handle_waterway_like);
    do_file("$datadir/wasserumland", \&handle_waterway_like);
    do_file("$datadir/wasserumland2", \&handle_waterway_like);

    do_file("$datadir/faehren", \&handle_ferry_like);

    do_file("$datadir/strassen", \&handle_berlin_highway_like);
    do_file("$datadir/strassen_bab", \&handle_anywhere_highway_like); # don't do address handling here, so use "anywhere"
    do_file("$datadir/landstrassen", \&handle_anywhere_highway_like);
    do_file("$datadir/landstrassen2", \&handle_anywhere_highway_like);

    # XXX not enabled by default
    if ($experiments{'cycle-route'} && ($optimize_for_mapnik_style == OPTIMIZE_FOR_MAPNIK_STYLE_BBBIKE || $optimize_for == OPTIMIZE_FOR_GARMIN)) {
	do_file("$datadir/comments_route", \&handle_comments_route_like);
    }

    dump_quality();
    dump_handicap();

    # XXX not enabled by default
    if ($experiments{'mount'}) {
	do_file("$datadir/mount", \&handle_mount_like);
    }
	
    if (!$fragezeichen_file) {
	$fragezeichen_file = -r "$datadir/../tmp/fragezeichen-nextcheck.bbd" ? "$datadir/../tmp/fragezeichen-nextcheck.bbd" : "$datadir/fragezeichen";
    }
    do_file($fragezeichen_file, \&handle_fixme_like);

    do_file("$datadir/ubahn", \&handle_railway_like);
    do_file("$datadir/sbahn", \&handle_railway_like);
    do_file("$datadir/rbahn", \&handle_railway_like);

    # Use -orig files here, because it has more information (RW? etc.)
    if (-r "$datadir/radwege-orig") {
	do_file("$datadir/radwege-orig", \&handle_cycleway_like);
    }
    if (-r "$datadir/comments_cyclepath-orig") {
	do_file("$datadir/comments_cyclepath-orig", \&handle_cycleway_like);
    }

    do_file("$datadir/berlin_ortsteile", \&handle_citypart_like);
}

sub do_file {
    my($file, $cb) = @_;
    if ($file !~ m{/zebrastreifen$}) { # XXX exceptions: these usually don't have a -orig counterpart
	$file .= "-orig" if $use_orig;
    }
    eval {
	Strassen->new_stream($file)->read_stream($cb);
    };
    if ($@) {
	if ($ignore_missing) {
	    warn "Cannot find or handle $file, skipping..\n" unless $quiet;
	} else {
	    die $@;
	}
    }
}

sub handle_blocked_node_like {
    my($r, $dir) = @_;
    my($cat, $cat_attribs) = $r->[Strassen::CAT] =~ m{^([^:]+)(?:::?(.*))?};

    # Don't render if the output is map-only
    if ($optimize_for == OPTIMIZE_FOR_MAPNIK && ($cat_attribs||'') =~ /\bigndisp\b/) {
	return;
    }

    tie my %attribs, 'Tie::IxHash';
    if ($cat eq 'BNP') {
	if ($r->[Strassen::NAME] =~ m{drngelgitter}i) {
	    %attribs = (barrier => 'cycle_barrier');
	} elsif ($r->[Strassen::NAME] =~ m{(schranke|\btor\b|schwingtor)}i) {
	    %attribs = (barrier => 'gate');
	} elsif ($r->[Strassen::NAME] =~ m{(poller|pfosten)}i) {
	    %attribs = (barrier => 'bollard');
	} else {
	    # fallback, probably incorrect
	    %attribs = (barrier => 'bollard');
	}
    } elsif ($cat eq '0') {
	if ($r->[Strassen::NAME] =~ m{(\d+)\s+Stufe}) { # XXX see steps_stats.pl for more possible regexps, should go into a module!
	    $attribs{step_count} = $1;
	}
	if ($optimize_for == OPTIMIZE_FOR_GARMIN ||
	    $optimize_for == OPTIMIZE_FOR_MAPNIK) {
	    if (@{$r->[Strassen::COORDS]} == 1) {
		my @node_ids = get_node_ids($r);
		$remember_steps{$node_ids[0]} = {
						 $attribs{step_count} ? (steps => $attribs{step_count}) : (),
						};
		return; # handled later
	    } else {
		our $MULTI_NODE_STEPS_WARNING_SEEN;
		if (!$MULTI_NODE_STEPS_WARNING_SEEN++) {
		    warn "No support for multi-node steps yet...";
		}
	    }
	}
	$attribs{highway} = 'steps'; # not good: steps are in osm usually lines, not nodes!
    }
    if (%attribs) {
	get_node_ids($r, \%attribs); # as a side-effect, create the nodes
    }
}

sub handle_blocked_way_like {
    my($r, $dir) = @_;
    my($cat, $cat_attribs) = $r->[Strassen::CAT] =~ m{^([^:]+)(?:::?(.*))?};

    # Don't render if the output is map-only
    if ($optimize_for == OPTIMIZE_FOR_MAPNIK && ($cat_attribs||'') =~ /\bigndisp\b/) {
	return;
    }

    my @node_ids = map {
	my($px,$py) = $Karte::Polar::obj->trim_accuracy($Karte::Polar::obj->standard2map(split /,/, $_));
	my $node_id = maybe_add_node($px,$py);
	$node_id;
    } @{ $r->[Strassen::COORDS] };
    for my $node_id_i (1 .. $#node_ids) {
	if ($cat eq '1' || $cat eq '1s') {
	    $oneway{$node_ids[$node_id_i].' '.$node_ids[$node_id_i-1]} = 1; # in osm oneway is the open direction, not the closed
	} elsif ($cat eq '2' || $cat eq '2s') {
	    $no_access{$node_ids[$node_id_i-1].' '.$node_ids[$node_id_i]} = 1;
	    $no_access{$node_ids[$node_id_i].' '.$node_ids[$node_id_i-1]} = 1;
	}
    }
}

sub handle_handicap_like {
    my($r, $dir) = @_;
    my($cat_hin, $cat_rueck);
    if ($r->[Strassen::CAT] =~ /^(.*);(.*)$/) {
	($cat_hin, $cat_rueck) = ($1, $2);
    } else {
	($cat_hin, $cat_rueck) = ($r->[Strassen::CAT], $r->[Strassen::CAT]);
    }

    my($cat_attrib_hin, $cat_attrib_rueck);
    for my $def (
		 [\$cat_hin,   \$cat_attrib_hin],
		 [\$cat_rueck, \$cat_attrib_rueck],
		) {
	my($cat_ref, $cat_attrib_ref) = @$def;
	if (defined $$cat_ref) {
	    ($$cat_ref, $$cat_attrib_ref) = $$cat_ref =~ m{^
							   ([^:]+)
							   (?:::?(.*))?
							   $}x;
	}
    }
    if (defined $cat_attrib_hin && $cat_attrib_hin =~ m{\bigndisp\b}) {
	# This is most likely generated from an "1s" record. Don't
	# render it, because we cannot distinguish between directions
	# in the osm output.
	return;
    }

    my @node_ids = map {
	my($px,$py) = $Karte::Polar::obj->trim_accuracy($Karte::Polar::obj->standard2map(split /,/, $_));
	my $node_id = maybe_add_node($px,$py);
	$node_id;
    } @{ $r->[Strassen::COORDS] };

    if ($optimize_for == OPTIMIZE_FOR_MAPNIK) {
	# no q3/q4 hacks for map display, please
    } else {
	# hack: %speed0 has information for downgrading a highway due
	# to speed limitations (bicycle only)
	for my $node_id_i (1 .. $#node_ids) {
	    if ($cat_hin =~ m{^[qQ][34]$}) {
		$speed0{$node_ids[$node_id_i-1].' '.$node_ids[$node_id_i]} = 1; # XXX correct direction
	    }
	    if (defined $cat_rueck) {
		if ($cat_rueck =~ m{^[qQ][34]$}) {
		    $speed0{$node_ids[$node_id_i].' '.$node_ids[$node_id_i-1]} = 1;
		}
	    }
	}
    }

    for my $def (
		 ['Q', \@remember_quality],
		 ['q', \@remember_handicap],
		) {
	my($catprefix,$varref) = @$def;
	my($quality_hin) = $cat_hin =~ m{^\Q$catprefix\E(\d+)}; # note: no support for +/-
	my $quality = $quality_hin;
	if (defined $cat_rueck) {
	    my($quality_rueck) = $cat_rueck =~ m{^\Q$catprefix\E(\d+)}; # note: support for +/-?
	    if (defined $quality_rueck && (!defined $quality_hin || $quality_rueck > $quality_hin)) {
		$quality = $quality_rueck;
	    }
	}
	if (defined $quality) {
	    push @$varref, [\@node_ids, $quality];
	}
    }
}

sub handle_nolighting_like {
    my($r) = @_;
    my @node_ids = get_node_ids($r);
    for my $node_id_i (1 .. $#node_ids) {
	my($n0,$n1) = @node_ids[$node_id_i-1,$node_id_i];
	$unlit{"$n0 $n1"} = 1;
	$unlit{"$n1 $n0"} = 1;
    }
}

sub handle_green_like {
    my($r) = @_;
    (my $cat = $r->[Strassen::CAT]) =~ s{^green}{};
    my @node_ids = get_node_ids($r);
    for my $node_id_i (1 .. $#node_ids) {
	my($n0,$n1) = @node_ids[$node_id_i-1,$node_id_i];
	$green{"$n0 $n1"} = $cat;
	$green{"$n1 $n0"} = $cat;
    }
}

sub handle_mount_like {
    my($r) = @_;
    if (my($mount) = $r->[Strassen::NAME] =~ m{^Steigung ([\d\.]+)%}) {
	my $class = ($mount >= 6.0 ? 'strong' :
		     $mount >= 4.0 ? 'medium' :
		     $mount >= 2.0 ? 'light' : undef);
	if (defined $class) {
	    print $wayfh qq{<way id="$next_way_id" visible="true">\n};
	    for my $node_id (get_node_ids($r)) {
		print $wayfh qq{  <nd ref="$node_id"/>\n};
	    }
	    print $wayfh qq{  <tag k="bbbike:mountclass" v="${class}_mount" />\n};
	    # not needed: print $wayfh qq{  <tag k="bbbike:mount" v="$mount" />\n};
	    print $wayfh qq{</way>\n};
	    $next_way_id++;
	}
    } else {
	warn "Ignore unparsable mount text '$r->[Strassen::NAME]'...\n";
    }
}

sub handle_berlin_highway_like   { _handle_highway_like('Berlin', @_) }
sub handle_anywhere_highway_like { _handle_highway_like(undef, @_) }

sub _handle_highway_like {
    my($city, $r, $dir) = @_;

    my($cat, $cat_attribs);
    my $is_onedirection = 0;
    if (($cat = $r->[Strassen::CAT]) =~ s/;$//) { # "BAB;" currently exists in data
	$is_onedirection = 1; # XXX not sure what to do with this information later...
    }

    ($cat, $cat_attribs) = $cat =~ m{^([^:]+)(?:::?(.*))?};
    if ($cat eq 'Pl') {
	# XXX maybe handle later...
	return;
    }

    # Don't render if the output is map-only
    if ($optimize_for == OPTIMIZE_FOR_MAPNIK && ($cat_attribs||'') =~ /\bigndisp\b/) {
	return;
    }

    my $highway_tag = $cat_to_highway{$cat};
    if (!defined $highway_tag) {
	warn "Ignore cat='$r->[Strassen::CAT]'...\n";
	return;
    }

    my($name, @cityparts) = Strasse::split_street_citypart($r->[Strassen::NAME]);
    if (!@cityparts && defined $city && $city eq 'Berlin') {
	# Find citypart from PLZ.pm
	my $res = $street_to_citypart->{$name};
	if ($res) {
	    # XXX sorted because of hash randomization; but it would
	    # be better to have another stable order, for example the
	    # citypart which the biggest portion of the street
	    push @cityparts, sort keys %$res;
	}
    }

    my($ref_type, $ref_nr, $ref);
    if (defined $name) {
	($ref_type, $ref_nr) = Strasse::parse_street_type_nr($name);
	if (defined $ref_type) {
	    if ($ref_type eq 'BAB' || $ref_type eq 'B') {
		# Note: original osm data have "A " for motorways or
		# "B " for Bundestraen prefixed. I deliberately do
		# NOT prefix anything, because this prefix is
		# superfluous in a map view --- the shield form is
		# enough to show the type of street
		$ref = $ref_nr;
	    }
	}
    }

    if ($optimize_for_mapnik_style == OPTIMIZE_FOR_MAPNIK_STYLE_BBBIKE) {
	if ($dir && $dir->{'osm.highway'} && $dir->{'osm.highway'}->[0]) {
	    $highway_tag = $dir->{'osm.highway'}->[0];
	} else {
	    if ($highway_tag eq 'motorway') {
		if (defined $ref_type && $ref_type ne 'BAB') {
		    $highway_tag = 'motorroad';
		}
	    }
	}
    }

    my $tunnel;
    my $bridge;
    if ($cat_attribs) {
	if ($cat_attribs =~ m{Tu}) { # may be _Tu or Tu_, too
	    $tunnel = 1;
	} elsif ($cat_attribs =~ m{Br}) {
	    $bridge = 1;
	}
    }

    my @node_ids = get_node_ids($r);

    # split streets
    my @segments;
    my $last_tags;
    my @last_tags;
    my $begin_split_index = 0;
    for my $node_idx (1 .. $#node_ids) {
	my $forward_spec = $node_ids[$node_idx-1].' '.$node_ids[$node_idx];
	my $backward_spec = $node_ids[$node_idx].' '.$node_ids[$node_idx-1];
	my $in_steps = (exists $remember_steps{$node_ids[$node_idx]} ||
			exists $remember_steps{$node_ids[$node_idx-1]}
		       ) ? 1 : 0;
	my @this_tags = (
			 ($oneway{$forward_spec}||''),
			 ($oneway{$backward_spec}||''),
			 ($no_access{$forward_spec}||''),
			 ($speed0{$forward_spec}||''),
			 ($speed0{$backward_spec}||''),
			 $in_steps,
			 ($unlit{$forward_spec}||''),
			 ($green{$forward_spec}||''),
			);
	my $this_tags = join(" ", @this_tags);
	if (defined $last_tags) {
	    if ($last_tags ne $this_tags) {
		# we need to split
		push @segments, [[@node_ids[$begin_split_index..$node_idx-1]], [@last_tags]];
		$begin_split_index = $node_idx-1;
		$last_tags = $this_tags;
		@last_tags = @this_tags;
	    }
	} else {
	    $last_tags = $this_tags;
	    @last_tags = @this_tags;
	}
    }
    push @segments, [[@node_ids[$begin_split_index..$#node_ids]], [@last_tags]];

    # Usually the descriptive street names in the form "(A - B)" are
    # only clutter for the mapnik or garmin maps. Also the "[...]"
    # additions can be removed here.
    #
    # Theoretically these names could be used in garmin's address
    # search. But it seems that the longish "(A - B)" names are
    # winning over the shorter street names, so avoid them also here.
    if ($optimize_for == OPTIMIZE_FOR_GARMIN ||
	$optimize_for == OPTIMIZE_FOR_MAPNIK) {
	undef $name if $name =~ m{^\(.*\)$};
	if (defined $name) {
	    $name =~ s{\s+\[.*\]$}{};
	}
    }
    _xmlify($name);

    # This is the most reasonable scheme to set is_in/addr tags, to
    # get it best working in the "Find" screen of Garmin:
    #
    # for streets in Berlin (defined $city):
    # * is_in:county => "Berlin"
    # * addr:city => first citypart (if any)
    #
    # for streets outside Berlin (@cityparts exist)
    # * addr:city => first citypart (usually there is exactly one)
    #
    # If any of these tags is set, then also set addr:country
    tie my %is_in, 'Tie::IxHash';
    if (defined $city) {
	$is_in{'is_in:county'} = $city; # XXX lying for Berlin, so we have cityparts in addr:city and Berlin in is_in:county XXX should be activated only for $optimize_for=garmin
	if (@cityparts) {
	    $is_in{'addr:city'} = $cityparts[0];
	}
    } else {
	if (@cityparts) {
	    $is_in{'addr:city'} = $cityparts[0];
	}
    }
    if (%is_in) {
	$is_in{'addr:country'} = 'DE'; # XXX not always true, some streets are in Poland and Czech Republic
    }

    for my $segment (@segments) {
	my @node_ids = @{$segment->[0]};
	my($oneway, $oneway_reversed, $no_access, $speed0, $speed0_reversed, $in_steps, $unlit, $green) = @{$segment->[1]};

	if ($oneway_reversed) {
	    @node_ids = reverse @node_ids;
	    $oneway = 1;
	}

	my $any_speed0 = $speed0 || $speed0_reversed; # XXX for the real thing I had to split the way into two directions...
	    
	print $wayfh qq{<way id="$next_way_id" visible="true">\n};
	for my $node_id (@node_ids) {
	    print $wayfh qq{  <nd ref="$node_id"/>\n};
	}
	if (defined $name) { # $name is already xmlified
	    print $wayfh qq{  <tag k="name" v="$name" />\n};
	}
	while(my($k,$v) = each %is_in) {
	    _xmlify($v);
	    print $wayfh qq{  <tag k="$k" v="$v" />\n};
	}
	if ($in_steps) {
	    print $wayfh qq{  <tag k="highway" v="steps" />\n};
	    ##XXX NYI:
	    #if ($steps =~ m{\d+}) {
	    #print $wayfh qq{  <tag k="step_count" v="$steps" />\n};
	    #}
	} elsif ($any_speed0) {
	    print $wayfh qq{  <tag k="highway" v="footway" />\n};
	    print $wayfh qq{  <tag k="bicycle" v="yes" />\n};
	} else {
	    if ($do_highway_primary_hack && $highway_tag =~ m{^(primary|secondary)$}) {
		print $wayfh qq{  <tag k="highway" v="tertiary" />\n};
		print $wayfh qq{  <tag k="bbbike:render" v="highway_${highway_tag}" />\n};
	    } else {
		print $wayfh qq{  <tag k="highway" v="$highway_tag" />\n};
	    }
	}
	if ($oneway) {
	    print $wayfh qq{  <tag k="oneway" v="yes" />\n};
	}
	if ($no_access) {
	    print $wayfh qq{  <tag k="access" v="no" />\n};
	    print $wayfh qq{  <tag k="bicycle" v="no" />\n}; # access=no is not enough
	}
	if ($tunnel) {
	    print $wayfh qq{  <tag k="tunnel" v="yes" />\n};
	}
	if ($bridge) {
	    print $wayfh qq{  <tag k="bridge" v="yes" />\n};
	}
	if ($ref) {
	    my $xml_ref = $ref; _xmlify($xml_ref);
	    print $wayfh qq{  <tag k="ref" v="$xml_ref" />\n};
	}
	if ($unlit) {
	    print $wayfh qq{  <tag k="lit" v="no" />\n};
	}
	if ($green) {
	    print $wayfh qq{  <tag k="bbbike:green" v="$green" />\n};
	}

	if ($experiments{addr}) {
	    print $wayfh qq{  <tag k="addr:street" v="$name" />\n};
	    print $wayfh qq{  <tag k="addr:housenumber" v="0" />\n}; # XXX pure faked
	    print $wayfh qq{  <tag k="addr:country" v="DE" />\n};
	    if (@cityparts) {
		# parse street numbers out
		@cityparts = grep {
		    my($type, $nr) = Strasse::parse_street_type_nr($_);
		    !defined $type;
		} @cityparts;
		if (@cityparts) {
		    _xmlify($cityparts[0]);
		    # XXX hack: use only first
		    print $wayfh qq{  <tag k="addr:city" v="$cityparts[0]" />\n};
		}
	    }
	}

	print $wayfh qq{</way>\n};
	$next_way_id++;
    }
}

sub handle_fixme_like {
    my($r, $dir) = @_;

    my @node_ids = get_node_ids($r);

    my $name = $r->[Strassen::NAME] || 'FIXME';
    _xmlify($name);

    print $wayfh qq{<way id="$next_way_id" visible="true">\n};
    for my $node_id (@node_ids) {
	print $wayfh qq{  <nd ref="$node_id"/>\n};
    }
    print $wayfh qq{  <tag k="FIXME" v="$name" />\n};
    print $wayfh qq{</way>\n};
    $next_way_id++;
}

sub handle_fixme_cat_like {
    my($r, $dir) = @_;
    my $cat = $r->[Strassen::CAT];

    # Make it a Tie::IxHash if more attributes are added...
    my $attribs = { 'bbbike:fixme_cat' => $cat };

    get_node_ids($r, $attribs); # as a side-effect, create the nodes
}

sub handle_ropeway {
    my($r, $dir) = @_;
    my @node_ids = get_node_ids($r);
    my $name = $r->[Strassen::NAME];
    _xmlify($name);
    print $wayfh qq{<way id="$next_way_id" visible="true">\n};
    for my $node_id (@node_ids) {
	print $wayfh qq{  <nd ref="$node_id"/>\n};
    }
    print $wayfh qq{  <tag k="name" v="$name" />\n} if defined $name && length $name;
    print $wayfh qq{  <tag k="aerialway" v="cable_car" />\n}; # XXX rough approximation, might also be gondola or something else
    print $wayfh qq{</way>\n};
    $next_way_id++;
}

sub handle_railway_like {
    my($r, $dir) = @_;

    my($cat, $cat_attribs);
    #my $is_onedirection = 0; # note: oneways do not exist in railways, so this information is unlikely to be ever used
    if (($cat = $r->[Strassen::CAT]) =~ s/;$//) {
	#$is_onedirection = 1;
    }
    ($cat, $cat_attribs) = $cat =~ m{^([^:]+)(?:::?(.*))?};
    if ($cat eq 'Ropeway') {
	return handle_ropeway($r, $dir);
    }
    return if $cat !~ m{^([USR]|RP|RG)([ABC0]|Bau)?$};
    my $railway_cat = $cat_to_railway{$1};
    return if !defined $railway_cat;
    my $is_abandoned = defined $2 && $2 eq '0';
    my $is_construction = defined $2 && $2 eq 'Bau';

    my $tunnel;
    my $bridge;
    if ($cat_attribs) {
	if ($cat_attribs =~ m{Tu}) { # may be _Tu or Tu_, too
	    $tunnel = 1;
	} elsif ($cat_attribs =~ m{Br}) {
	    $bridge = 1;
	}
    }

    my @node_ids = get_node_ids($r);

    my $name = $r->[Strassen::NAME];
    if ($optimize_for == OPTIMIZE_FOR_GARMIN) {
	$name =~ s{,(?! )}{, }g; # "S1,S2" -> "S1, S2"
    }
    _xmlify($name);

    print $wayfh qq{<way id="$next_way_id" visible="true">\n};
    for my $node_id (@node_ids) {
	print $wayfh qq{  <nd ref="$node_id"/>\n};
    }
    print $wayfh qq{  <tag k="name" v="$name" />\n} if defined $name && length $name;
    if ($is_abandoned) {
	print $wayfh qq{  <tag k="railway" v="abandoned" />\n};
    } elsif ($is_construction) {
	print $wayfh qq{  <tag k="railway" v="construction" />\n};
	print $wayfh qq{  <tag k="construction" v="$railway_cat" />\n};
    } else {
	print $wayfh qq{  <tag k="railway" v="$railway_cat" />\n};
    }
    if ($tunnel) {
	print $wayfh qq{  <tag k="tunnel" v="yes" />\n};
    }
    if ($bridge) {
	print $wayfh qq{  <tag k="bridge" v="yes" />\n};
    }
    print $wayfh qq{</way>\n};
    $next_way_id++;
}

sub handle_waterway_like {
    my($r, $dir) = @_;

    my($is_area, $cat);
    if ($r->[Strassen::CAT] =~ m{^F:([^:]+)}) {
	$is_area = 1;
	$cat = $1;
    } else {
	($cat) = $r->[Strassen::CAT] =~ m{^([^:]+)};
    }
    my $is_tunnel = $r->[Strassen::CAT] =~ m{:_?Tu_?(?:$|:)}; # e.g. W::_Tu_
    # XXX handling W0,W1 etc. missing

    my @node_ids = get_node_ids($r, undef, is_area => $is_area);

    my $name = format_waterway_label($r->[Strassen::NAME]);
    _xmlify($name);

    print $wayfh qq{<way id="$next_way_id" visible="true">\n};
    for my $node_id (@node_ids) {
	print $wayfh qq{  <nd ref="$node_id"/>\n};
    }
    print $wayfh qq{  <tag k="name" v="$name" />\n} if defined $name && length $name;
    if ($is_area) {
	if ($cat eq 'I') {
	    # XXX landuse=island is not an official osm tag, just a
	    # "crutch" for mapnik etc. rendering
	    print $wayfh qq{  <tag k="landuse" v="island" />\n};
	} else {
	    print $wayfh qq{  <tag k="natural" v="water" />\n};
	}
    } else {
	print $wayfh qq{  <tag k="waterway" v="river" />\n};
    }
    if ($is_tunnel) {
	print $wayfh qq{  <tag k="tunnel" v="yes" />\n};
    }
    print $wayfh qq{</way>\n};
    $next_way_id++;
}

sub handle_ferry_like {
    my($r, $dir) = @_;

    my @node_ids = get_node_ids($r);

    my $name = $r->[Strassen::NAME];
    _xmlify($name);

    print $wayfh qq{<way id="$next_way_id" visible="true">\n};
    for my $node_id (@node_ids) {
	print $wayfh qq{  <nd ref="$node_id"/>\n};
    }
    print $wayfh qq{  <tag k="name" v="$name" />\n} if defined $name && length $name;
    print $wayfh qq{  <tag k="route" v="ferry" />\n};
    # XXX description from comments_ferry missing
    print $wayfh qq{</way>\n};
    $next_way_id++;
}

sub handle_ferry_info_like {
    my($r, $dir) = @_;

    my $label = shorten_label($r->[Strassen::NAME]);

    get_node_id_from_center($r, { note => $label });
}

sub handle_comments_scenic_like {
    my($r, $dir) = @_;
    my $cat = $r->[Strassen::CAT];
    if ($cat =~ m{^View(?:$|:)}) {
	my $label = shorten_label($r->[Strassen::NAME]);
	tie my %attribs, 'Tie::IxHash';
	%attribs = (
		    tourism => 'viewpoint',
		    name => $label,
		   );
	get_node_ids($r, \%attribs); # as a side-effect, create the nodes
    } else {
	return;
    }
}

sub handle_area_like {
    my($r, $dir) = @_;

    if ($r->[Strassen::CAT] !~ m{^F:([^:|]+)(.*)}) {
	return; # should never happen
    }
    my($cat, $rest) = ($1, $2);
    my %tags;
    if ($rest =~ m{^\|(religion):(.*)$}) { # XXX could support more attributes here
	$tags{$1} = $2;
    }
    my($landuse, $bbbike_landuse, $leisure);
    $landuse = $cat_to_area_landuse{$cat};
    if (!defined $landuse) {
	$bbbike_landuse = $cat_to_area_bbbike_landuse{$cat};
	if (!defined $bbbike_landuse) {
	    $leisure = $cat_to_area_leisure{$cat};
	    if (!defined $leisure) {
		#warn "Ignoring $cat...\n";
		return;
	    }
	}
    }

    my @node_ids = get_node_ids($r, undef, is_area => 1);
    push @node_ids, $node_ids[0] unless $node_ids[0] == $node_ids[$#node_ids];

    my $name = $r->[Strassen::NAME];
    if ($optimize_for == OPTIMIZE_FOR_MAPNIK && defined $name && $name =~ m{^\(}) { # e.g. "(Kolonien in Schmargendorf)"
	undef $name;
    }
    _xmlify($name);

    print $wayfh qq{<way id="$next_way_id" visible="true">\n};
    for my $node_id (@node_ids) {
	print $wayfh qq{  <nd ref="$node_id"/>\n};
    }
    print $wayfh qq{  <tag k="name" v="$name" />\n} if defined $name && length $name;
    if (defined $landuse) {
	print $wayfh qq{  <tag k="landuse" v="$landuse" />\n};
    } elsif (defined $bbbike_landuse) {
	print $wayfh qq{  <tag k="bbbike:landuse" v="$bbbike_landuse" />\n};
    } else {
	print $wayfh qq{  <tag k="leisure" v="$leisure" />\n};
    }
    while(my($k,$v) = each %tags) {
	_xmlify($k);
	_xmlify($v);
	print $wayfh qq{  <tag k="$k" v="$v" />\n};
    }
    print $wayfh qq{</way>\n};
    $next_way_id++;
}

sub handle_trafficsignals_like {
    my($r, $dir) = @_;
    my $cat = $r->[Strassen::CAT];

    tie my %attribs, 'Tie::IxHash';
    if      ($cat =~ m{^X}) {
	%attribs = (highway => 'traffic_signals');
    } elsif ($cat =~ m{^F}) {
	if ($optimize_for == OPTIMIZE_FOR_MAPNIK) {
	    if ($optimize_for_mapnik_style == OPTIMIZE_FOR_MAPNIK_STYLE_BBBIKE) {
		# Use a non-standard tag value here
		%attribs = (highway => 'traffic_signals_pedestrian');
	    } else {
		# Treat LSA-X and LSA-F the same, otherwise LSA-F will not
		# be rendered at all in mapnik using default or german style.
		# This is because there's no "crossing" column in the
		# planet database.
		%attribs = (highway => 'traffic_signals');
	    }
	} else {
	    %attribs = (highway => 'crossing', crossing => 'traffic_signals');
	}
    } elsif ($cat =~ m{^B}) {
	%attribs = (
		    railway => 'level_crossing',
		    ($cat =~ m{0$} ? (disused => 'yes') : ()), # not accurate, may be also seldomly used
		   );
    } else {
	# ignore
	return;
    }

    get_node_ids($r, \%attribs); # as a side-effect, create the nodes
}

sub handle_zebrastreifen_like {
    my($r) = @_;
    tie my %attribs, 'Tie::IxHash';
    %attribs = (
		highway => 'crossing',
		crossing => 'uncontrolled',
		crossing_ref => 'zebra',
	       );
    get_node_ids($r, \%attribs);
}

sub handle_ropeway_stations {
    my($r) = @_;
    my $name = $r->[Strassen::NAME];
    tie my %attribs, 'Tie::IxHash';
    %attribs = (aerialway => 'station');
    if (defined $name) {
	$attribs{name} = $name;
    }
    get_node_ids($r, \%attribs);
}    

sub handle_railway_stations_like {
    my($r) = @_;
    my $cat = $r->[Strassen::CAT];
    if ($cat eq 'Ropeway') {
	return handle_ropeway_stations($r);
    }
    my $is_ubahn = $cat =~ m{^U};
    my $is_sbahn = $cat =~ m{^S};
    my $is_abandoned = $cat =~ m{0$};
    my $is_construction = $cat =~ m{Bau$};
    my $is_optimize_for_mapnik_bbbike_style = $optimize_for_mapnik_style == OPTIMIZE_FOR_MAPNIK_STYLE_BBBIKE;
    my $name = (
		$is_optimize_for_mapnik_bbbike_style
		? ''
		: ($is_ubahn ? 'U ' : $is_sbahn ? 'S ' : '')
	       ) . $r->[Strassen::NAME];
    ##XXX $is_construction handling for mapnik_bbbike_style is missing
    tie my %attribs, 'Tie::IxHash';
    %attribs = (
		railway => ($is_construction
			    ? 'construction'
			    : 'station' # no distinction between halt and station
			   ),
		($is_abandoned ? ("abandoned" => "yes") : ()),
		($is_construction ? ("construction" => "station") : ()), # more construction-related attribs set later
		name => $name,
	       );
    if      ($is_ubahn) {
	if ($is_optimize_for_mapnik_bbbike_style) {
	    $attribs{railway} = 'station_subway';
	} else {
	    $attribs{station} = 'subway';
	}
    } elsif ($is_sbahn) {
	if ($is_optimize_for_mapnik_bbbike_style) {
	    $attribs{railway} = 'station_light_rail';
	} else {
	    $attribs{station} = 'light_rail';
	}
    } elsif ($cat =~ m{^R}) {
	# Regional/Fernbahnhof
	if ($cat =~ m{^RP}) {
	    if ($optimize_for_mapnik_style == OPTIMIZE_FOR_MAPNIK_STYLE_BBBIKE) {
		$attribs{railway} = 'station_narrow_gauge';
	    } else {
		$attribs{'bbbike:station_type'} = 'RP';
	    }
	}
    } else {
	warn "Unhandled railway station-like category $cat\n";
	return;
    }

    if ($is_construction && $attribs{station}) {
	$attribs{'construction:station'} = delete $attribs{station};
    }

    get_node_ids($r, \%attribs);
}

sub handle_city_like {
    my($r) = @_;
    my $cat = $r->[Strassen::CAT];
    my $place;
    if      ($cat >= 5) {
	$place = 'city';
    } elsif ($cat >= 3) {
	$place = 'town';
    } elsif ($cat >= 1) {
	$place = 'village';
    } else {
	$place = 'hamlet';
    }
    tie my %attribs, 'Tie::IxHash';
    %attribs = (
		name  => format_place_label($r->[Strassen::NAME]),
		place => $place,
	       );
    get_node_ids($r, \%attribs);
}

sub handle_city_limit_like {
    my($r) = @_;
    my $cat = $r->[Strassen::CAT];
    my $traffic_sign_name;
    if ($cat eq 'OS') {
	$traffic_sign_name = 'city_limit';
    } elsif ($cat eq 'OHT') {
	# kein offizielles OSM-Tag
	# englische bersetzung von "Ortshinweistafel" aus:
	# http://de.wikipedia.org/wiki/Datei:Zeichen_385.svg
	$traffic_sign_name = 'locality_name';
    } else {
	warn "Unhandled city-limit like category '$cat'";
	return;
    }
    tie my %attribs, 'Tie::IxHash';
    %attribs = (
		traffic_sign => $traffic_sign_name,
		name         => format_place_label($r->[Strassen::NAME]),
	       );
    get_node_ids($r, \%attribs);
}

sub handle_berlin_citypart_like {
    if ($optimize_for == OPTIMIZE_FOR_GARMIN) {
	# XXX not sure if this is even necessary for the Garmin...
	my($r) = @_;
	my($sxy) = join ",", get_polygon_center(map { split/,/ } @{ $r->[Strassen::COORDS] });
	get_node_id($sxy, { place => 'suburb',
			    name  => $r->[Strassen::NAME],
			    'is_in:county' => 'Berlin',
			    'addr:country' => 'DE',
			  });
	# And add Berlin:
	if ($r->[Strassen::NAME] eq 'Mitte') {
	    get_node_id($sxy, { place => 'county', # XXX lying
				name  => 'Berlin',
				'addr:country' => 'DE',
			      });
	}
    }
}

sub handle_cycleway_like {
    my($r, $dir) = @_;
    my($cat_hin, $cat_rueck);
    if ($r->[Strassen::CAT] =~ /^(.*);(.*)$/) {
	($cat_hin, $cat_rueck) = ($1, $2);
    } else {
	($cat_hin, $cat_rueck) = ($r->[Strassen::CAT], $r->[Strassen::CAT]);
    }
    for ($cat_hin, $cat_rueck) {
	if (defined $_) {
	    ($_) = $_ =~ m{^([^:]+)};
	}
    }
    my @node_ids = map {
	my($px,$py) = $Karte::Polar::obj->trim_accuracy($Karte::Polar::obj->standard2map(split /,/, $_));
	my $node_id = maybe_add_node($px,$py);
	$node_id;
    } @{ $r->[Strassen::COORDS] };

    my $extra_tags_for_dualdir = sub {
	my $extra_tags = '';
	my $hin_mandatory   = ($cat_hin||'') eq 'RW2';
	my $rueck_mandatory = ($cat_rueck||'') eq 'RW8';
	if ($hin_mandatory && $rueck_mandatory) {
	    $extra_tags .= qq{  <tag k="bbbike:usage" v="mandatory" />\n};
	} elsif ($hin_mandatory || $rueck_mandatory) {
	    $extra_tags .= qq{  <tag k="bbbike:usage" v="mixed" />\n};
	} elsif (!$hin_mandatory && !$rueck_mandatory) {
	    $extra_tags .= qq{  <tag k="bbbike:usage" v="optional" />\n};
	} else {
	    $extra_tags .= qq{  <tag k="bbbike:usage" v="unknown" />\n};
	}
	$extra_tags;
    };

    for my $def ([$cat_hin, +1], [$cat_rueck, -1]) {
	my($cat, $direction) = @$def;
	next if !defined $cat || $cat eq '';
	my $extra_tags;
	# Check for Zweirichtungsradweg first
	if (($cat_hin||'') =~ m{^(RW|RW1|RW2|RW\?)$} && ($cat_rueck||'') =~ m{^(RW8|RW8\?|RW9|RW9\?)$}) {
	    if ($direction == +1) {
		$extra_tags .= qq{  <tag k="cycleway" v="track" />\n};
		$extra_tags .= qq{  <tag k="bbbike:cycleway_type" v="dualdir" />\n};
		$extra_tags .= $extra_tags_for_dualdir->();
	    }
	} elsif (($cat_rueck||'') =~ m{^(RW|RW1|RW2|RW\?)$} && ($cat_hin||'') =~ m{^(RW8|RW8\?|RW9|RW9\?)$}) {
	    if ($direction == -1) {
		$extra_tags .= qq{  <tag k="cycleway" v="track" />\n};
		$extra_tags .= qq{  <tag k="bbbike:cycleway_type" v="dualdir" />\n};
		$extra_tags .= $extra_tags_for_dualdir->();
	    }
	} elsif ($cat eq 'RW' || $cat eq 'RW?' || $cat eq 'RW8?' || $cat eq 'RW2?') {
	    $extra_tags .= qq{  <tag k="cycleway" v="track" />\n};
	    $extra_tags .= qq{  <tag k="bbbike:usage" v="unknown" />\n};
	} elsif ($cat eq 'RW1' || $cat eq 'RW9') {
	    $extra_tags .= qq{  <tag k="cycleway" v="track" />\n};
	    $extra_tags .= qq{  <tag k="bbbike:usage" v="optional" />\n};
	} elsif ($cat eq 'RW2' || $cat eq 'RW8') {
	    $extra_tags .= qq{  <tag k="cycleway" v="track" />\n};
	    $extra_tags .= qq{  <tag k="bbbike:usage" v="mandatory" />\n};
	} elsif ($cat eq 'RW3') {
	    $extra_tags .= qq{  <tag k="cycleway" v="lane" />\n};
	    $extra_tags .= qq{  <tag k="bbbike:usage" v="optional" />\n};
	} elsif ($cat eq 'RW4') {
	    $extra_tags .= qq{  <tag k="cycleway" v="lane" />\n};
	    $extra_tags .= qq{  <tag k="bbbike:usage" v="mandatory" />\n};
	} elsif ($cat eq 'RW3?' || $cat eq 'RW4?') {
	    $extra_tags .= qq{  <tag k="cycleway" v="lane" />\n};
	    $extra_tags .= qq{  <tag k="bbbike:usage" v="unknown" />\n};
	} elsif ($cat eq 'RW5') {
	    $extra_tags .= qq{  <tag k="psv" v="yes" />\n};
	    if ($r->[Strassen::NAME()] =~ m{Busspur (?:.*)?ist immer gltig}) {
		$extra_tags .= qq{  <tag k="bbbike:times" v="always" />\n};
	    }
	} elsif ($cat eq 'RW5?') {
	    $extra_tags .= qq{  <tag k="psv" v="yes" />\n};
	    $extra_tags .= qq{  <tag k="bbbike:times" v="unknown" />\n};
	} elsif ($cat eq 'RW6') {
	    # living street XXX
	} elsif ($cat eq 'RW7') {
	    # Fahrradstrae XXX
	} elsif ($cat eq 'RW10') {
	    # Nebenfahrbahn XXX
	} elsif ($cat eq 'RW0') {
	    # kein Radweg
	} else {
	    warn "Unexpected category '$cat'...";
	}
	if ($extra_tags) {
	    print $wayfh qq{<way id="$next_way_id" visible="true">\n};
	    my @node_ids = $direction == +1 ? @node_ids : reverse(@node_ids);
	    for my $node_id (@node_ids) {
		print $wayfh qq{  <nd ref="$node_id"/>\n};
	    }
	    if ($optimize_for == OPTIMIZE_FOR_MAPNIK) {
		# avoid creating labels for cyclepaths
	    } else {
		my $name = $r->[Strassen::NAME];
		if (defined $name && $name ne '') {
		    _xmlify($name);
		    print $wayfh qq{  <tag k="name" v="$name" />\n};
		}
	    }
	    print $wayfh $extra_tags;
	    print $wayfh qq{</way>\n};
	    $next_way_id++;
	}
    }
}

## XXX very very experimental. will probably change
## i.e. ref should probably be the $nr, and image will be resolved somewhere else
## (in a template creating the mapnik include files)
sub handle_comments_route_like {
    my($r, $dir) = @_;
    if ($geo && $geo->can('parse_street_type_nr')) {
	my $name = $r->[Strassen::NAME];
	my($type, $nr, $do_round, $image) = $geo->parse_street_type_nr($name);
	if (!defined $type) {
	    ($type, $nr) = Strasse::parse_street_type_nr($name);
	}
	if (defined $type) {
	    my @node_ids = get_node_ids($r);
	    print $wayfh qq{<way id="$next_way_id" visible="true">\n};
	    for my $node_id (@node_ids) {
		print $wayfh qq{  <nd ref="$node_id"/>\n};
	    }
	    print $wayfh qq{  <tag k="route" v="bicycle" />\n};
	    if ($optimize_for == OPTIMIZE_FOR_GARMIN) {
		my $plate = $type . (defined $nr ? $nr : '');
		_xmlify($plate);
		print $wayfh qq{  <tag k="ref" v="$plate" />\n};
	    } else {
		_xmlify($name);
		print $wayfh qq{  <tag k="name" v="$name" />\n};
		if ($image) {
		    _xmlify($image);
		    print $wayfh qq{  <tag k="ref" v="$image" />\n};
		}
	    }
	    print $wayfh qq{</way>\n};
	    $next_way_id++;
	}
    }
}

# See bbbike-aux/misc/mk-green-ampel-net.pl
sub handle_trafficlightmap_like {
    my($r, $dir) = @_;
    if ($r->[Strassen::CAT] !~ m{^(green|red)X;}) {
	warn "Unexpected category $r->[Strassen::CAT], ignoring...";
	return;
    }
    my $cat = $1;
    my @node_ids = map {
	my($px,$py) = $Karte::Polar::obj->trim_accuracy($Karte::Polar::obj->standard2map(split /,/, $_));
	my $node_id = maybe_add_node($px,$py);
	$node_id;
    } @{ $r->[Strassen::COORDS] };

    print $wayfh qq{<way id="$next_way_id" visible="true">\n};
    for my $node_id (@node_ids) {
	print $wayfh qq{  <nd ref="$node_id"/>\n};
    }
#    print $wayfh qq{  <tag k="bbbike:trafficlightmap" v="$cat" />\n};
#    print $wayfh qq{  <tag k="bbbike:tlm" v="$cat" />\n};
    #XXX hack, reuse bus lane signature here
    if ($cat eq 'green') {
	print $wayfh qq{  <tag k="cycleway" v="lane" />\n};
	print $wayfh qq{  <tag k="bbbike:usage" v="mandatory" />\n};
    } elsif ($cat eq 'red') {
	print $wayfh qq{  <tag k="psv" v="yes" />\n};
	print $wayfh qq{  <tag k="bbbike:times" v="always" />\n};
    } else {
	die;
    }
    print $wayfh qq{</way>\n};
    $next_way_id++;
}

sub handle_citypart_like {
    my($r, $dir) = @_;

    my @node_ids = get_node_ids($r, undef, is_area => 1);
    push @node_ids, $node_ids[0] unless $node_ids[0] == $node_ids[$#node_ids];

    my $name = $r->[Strassen::NAME];
    _xmlify($name);

    print $wayfh qq{<way id="$next_way_id" visible="true">\n};
    for my $node_id (@node_ids) {
	print $wayfh qq{  <nd ref="$node_id"/>\n};
    }
    print $wayfh qq{  <tag k="admin_level" v="10" />\n};
    print $wayfh qq{  <tag k="boundary" v="administrative" />\n};
    print $wayfh qq{</way>\n};

    my @way_ids = $next_way_id;
    $next_way_id++;

    print $relationfh qq{<relation id="$next_relation_id">\n};
    for my $way_id (@way_ids) {
	print $relationfh qq{  <member type="way" ref="$way_id" role="outer" />\n};
    }
    print $relationfh qq{  <tag k="admin_level" v="10" />\n};
    print $relationfh qq{  <tag k="boundary" v="administrative" />\n};
    print $relationfh qq{  <tag k="name" v="$name" />\n};
    print $relationfh qq{  <tag k="name:prefix" v="Ortsteil" />\n};
    print $relationfh qq{  <tag k="place" v="suburb" />\n};
    print $relationfh qq{  <tag k="type" v="multipolygon" />\n};
    print $relationfh qq{</relation>\n};

    $next_relation_id++;
}

seek $wayfh, 0, SEEK_SET
    or die $!;
seek $relationfh, 0, SEEK_SET
    or die $!;

if (defined $out_file) {
    open STDOUT, '>', $out_file
	or die "Can't write to $out_file: $!";
}
binmode STDOUT, ':utf8';
print qq{<osm version="0.6" generator="bbd2osm">\n};
print qq{<bound box="$min_lat,$min_lon,$max_lat,$max_lon" origin="http://www.bbbike.de" />\n};
while(my(undef, $node) = each %node) {
    print qq{<node id="$node->{id}" lat="$node->{lat}" lon="$node->{lon}" user="eserte" visible="true" };
    my $node_attribs = $node->{attribs};
    if ($node_attribs) {
	print qq{>\n};
	while(my($k,$v) = each %$node_attribs) {
	    _xmlify($k);
	    _xmlify($v);
	    print qq{  <tag k="$k" v="$v"/>\n};
	}
	print qq{</node>\n};
    } else {
	print qq{/>\n};
    }
}
while(<$wayfh>) {
    print $_;
}
while(<$relationfh>) {
    print $_;
}
print qq{</osm>\n};

sub get_node_ids {
    my($r, $attribs, %opts) = @_;
    my @node_ids;
    for my $sxy (@{ $r->[Strassen::COORDS] }) {
	my($px,$py) = $Karte::Polar::obj->trim_accuracy($Karte::Polar::obj->standard2map(split /,/, $sxy));
	my $this_node_id = maybe_add_node($px, $py, $attribs);
	push @node_ids, $this_node_id;
    }
    if ($opts{is_area}) {
	if ($r->[Strassen::COORDS][0] ne $r->[Strassen::COORDS][-1]) {
	    push @node_ids, $node_ids[0];
	}
    }
    @node_ids;
}

sub get_node_id {
    my($sxy, $attribs) = @_;
    my($px,$py) = $Karte::Polar::obj->trim_accuracy($Karte::Polar::obj->standard2map(split /,/, $sxy));
    maybe_add_node($px, $py, $attribs);
}

sub get_node_id_from_center {
    my($r, $attribs) = @_;
    my $c = $r->[Strassen::COORDS];
    my $sxy;
    if (@$c % 2 == 1) {
	$sxy = $c->[@$c/2];
    } else {
	my($x0,$y0) = split /,/, $c->[@$c/2 - 1];
	my($x1,$y1) = split /,/, $c->[@$c/2];
	$sxy = (($x1-$x0)/2+$x0) . ',' . (($y1-$y0)/2+$y0);
    }
    my($px,$py) = $Karte::Polar::obj->trim_accuracy($Karte::Polar::obj->standard2map(split /,/, $sxy));
    maybe_add_node($px, $py, $attribs);
}

sub maybe_add_node {
    my($px, $py, $attribs) = @_;
    my $pxy = $px.','.$py;

    my $this_node;
    my $this_node_id;
    if (!exists $node{$pxy}) {
	$this_node_id = $next_node_id++;
	$this_node = $node{$pxy} = { id => $this_node_id, lat => $py, lon => $px };

	$min_lon = $px if !defined $min_lon || $min_lon > $px;
	$max_lon = $px if !defined $max_lon || $max_lon < $px;
	$min_lat = $py if !defined $min_lat || $min_lat > $py;
	$max_lat = $py if !defined $max_lat || $max_lat < $py;
    } else {
	$this_node = $node{$pxy};
	$this_node_id = $this_node->{id};
    }

    if ($attribs && %$attribs) {
	if (!exists $this_node->{attribs}) {
	    tie my %node_attribs, 'Tie::IxHash', %$attribs;
	    $this_node->{attribs} = \%node_attribs;
	} else {
	    my $node_attribs = $this_node->{attribs};
	    while(my($k,$v) = each %$attribs) {
		if (!exists $node_attribs->{$k}) {
		    $node_attribs->{$k} = $v;
		}
	    }
	}
    }

    $this_node_id;
}

sub _xmlify {
    return if !defined $_[0];
    $_[0] =~ s{&}{&#38;}g; # XXX hack to xml-ify
    $_[0] =~ s{"}{&#34;}g;
    $_[0] =~ s{<}{&#60;}g;
    $_[0] =~ s{>}{&#62;}g;
}

sub dump_quality {
    for my $def (@remember_quality) {
	my($node_ids, $quality) = @$def;
	print $wayfh qq{<way id="$next_way_id" visible="true">\n};
	for my $node_id (@$node_ids) {
	    print $wayfh qq{  <nd ref="$node_id"/>\n};
	}
	print $wayfh qq{  <tag k="bbbike:quality" v="Q$quality" />\n};
	my $smoothness = $bbbikequality_to_smoothness{$quality};
	if ($smoothness) {
	    print $wayfh qq{  <tag k="smoothness" v="$smoothness" />\n};
	} else {
	    warn "Cannot map quality '$quality' to smoothness level";
	}
	print $wayfh qq{</way>\n};
	$next_way_id++;
    }
}

sub dump_handicap {
    for my $def (@remember_handicap) {
	my($node_ids, $handicap) = @$def;
	print $wayfh qq{<way id="$next_way_id" visible="true">\n};
	for my $node_id (@$node_ids) {
	    print $wayfh qq{  <nd ref="$node_id"/>\n};
	}
	print $wayfh qq{  <tag k="bbbike:handicap" v="q$handicap" />\n};
	print $wayfh qq{</way>\n};
	$next_way_id++;
    }
}

sub shorten_label {
    my $s = shift;
    if ($shorten_for_garmin) {
	my $abbrev_level = 1;
	while (length $s > MAX_LABEL_LENGTH) {
	    if ($abbrev_level > MAX_LABEL_LENGTH) {
		$s =~ s{$ABBREV_WORD_RX}{$ABBREV_WORD_DEFS{$1}}g;
		if (length $s > MAX_LABEL_LENGTH) {
		    warn "Label <$s> too long and may not be shortened further...\n" if $debug;
		}
		last;
	    }
	    $s = Strasse::short($s, $abbrev_level);
	    $abbrev_level++;
	}
    }
    $s;
}

sub format_place_label {
    my $s = shift;
    my($name, $add) = split /\|/, $s;
    $name . (defined $add ? ' ' . $add : '');
}

sub format_waterway_label {
    my $s = shift;
    if ($optimize_for == OPTIMIZE_FOR_MAPNIK) {
	if (defined $s) {
	    $s =~ s{\|.*}{};
	    if ($s =~ m{^\(}) { # e.g. "(See im Tiergarten)"
		undef $s;
	    }
	}
    }
    $s;
}

__END__

=head1 NAME

bbd2osm - generate osm files out of bbd files

=head1 SYNOPSIS

Convert a whole bbbike data directory. For the default data directory
use

    bbd2osm > out.osm

For a specified data directory use

    bbd2osm /path/to/datadir > out.osm

For a single bbd file use

    bbd2osm -single in.bbd -type ... > out.osm

Refer to the usage for allowed types.

=head1 DESCRIPTION

=head2 Garmin optimizations

With the switch C<< -optimize-for=garmin >> some optimizations for
later usage with mkgmap are done:

=over

=item * steps support

mkgmap expect multi-node steps, where the original BBBike data has
only one-node steps. In this mode a hack is done to create multi-node
step segments, which exceed to the neighboring nodes.

=item * shorten labels

Maximum length on Garmin devices (verified on etrex Vista HCX) is 151.
There are some heuristics used to abbreviate some common (german)
words. Also the function L<Strasse/short> is called with the
abbreviation level 4.

=item * C<bbbike:landuse=industrial> instead of C<landuse=industrial>

The default mkgmap style file (and bbbike's adaption of the style
file) renders industrial areas only for zoom levels 19-23. The zoom
level 24 is dedicated for related buildings. The bbbike Berlin data
has no building elements, so it's useful to render industrial areas
also in level 24 (especially if the Garmin device is set to "most
detail" rendering). To make it possible to use the same style file for
OSM data and BBBike data, this optimization (or better, hack) makes
the distinction by using a custom tag C<bbbike:landuse>.

=item * add spaces in railway line number combinations

Make "S1,S2,S3" into "S1, S2, S3" --- reason is that Garmin devices
otherwise renders it as "S1,s2,s3".

=back

=head2 Mapnik optimizations

With the switch C<< -optimize-for=mapnik >> some optimizations for
later usage with mapnik (and the default osm styles) are done:

=over

=item * No q3/q4 hacks are done here

=item * Street names in "(...)" are not rendered

=item * Street name parts in "[...]" are stripped

=item * Special pedestrian traffic lights handling needed

=item * Remove "|..." part in waterways

=back

The switch C<< -optimize-for=mapnik-bbbike >> turns on optimization if
using mapnik with the special bbbike style:

=over

=item * For pedestrian-only traffic lights use the non-standard tag
value "traffic_signals_pedestrian"

=item * Railway stations may have the non-standard tag values
"station_subway", "station_light_rail", or "station_narrow_gauge"

=item * The directive C<osm.highway> will be used, providing otherwise
non-existing support for highway types like "service" or
"motorway_link"

=back

=head2 TODO

Currently the way and node ids start at 1 and increment by one. This
means clashes are possible if these data and "real" osm is mixed.
Maybe I should check if the various osm tools can handle negative or
non-numeric ids, or if there's a private id range documented.

Implement handling of BNP:...

Do again some testing and work for the C<$do_highway_primary_hack>.
The Garmin seems aggressively avoid all primary and secondary highways
when doing routing for cyclists, leading to strange routes, at least
when trying it in Berlin. The idea was to render primary and secondary
roads, but to have internally a lower class, e.g. tertiary. First
tests were not successful, though...

=head2 BUGS

If, say, a S-Bahn station and U-Bahn station share the same
coordinate, and one of both is under construction, then the complete
node appears as under construction. Currently this is the case for
S-Bhf. Schoenweide.

For railway stations of different types sharing the same coordinate
only the first one processed is displayed. Processing order is
currently: subways, light railways, railways. This means at Wuhletal
the U-Bahn symbol is displayed (and not the S-Bahn symbol,
additionally or not), at Alexanderplatz, Hauptbahnhof etc. the S-Bahn
symbol (and not the railway symbol).

A traffic_sign=locality_name might have two names, but only one is
used. Probably it would be better to use pseudo-osm arrays, that is,
join the names with a ";" character.

=head1 AUTHOR

Slaven Rezic

=cut
