HTML-Genealogy-Map

 view release on metacpan or  search on metacpan

ged2map  view on Meta::CPAN

	}
}

print 'Found ', scalar(@events), " events with location data.\n";
print "Geocoding locations...\n";

# Geocode all events
my @geocoded_events;
my %cache;

foreach my $event (@events) {
	my $place = $event->{place};

	# Check cache
	unless (exists $cache{$place}) {
		my $location = $geocoder->geocode(location => $place);
		if ($location && $location->{lat} && $location->{lon}) {
			$cache{$place} = {
				lat => $location->{lat},
				lon => $location->{lon},
			};
			print "\tGeocoded: $place\n";
			sleep 1 if($location->{'geocoder'} !~ /^Geo::Coder::Free/);	# Be nice to geocoding service

		} else {
			print "\tFailed to geocode: $place\n";
			$cache{$place} = undef;
			sleep 1;	# Be nice to geocoding service
		}
	}

	if ($cache{$place}) {
		push @geocoded_events, {
			%$event,
			lat => $cache{$place}{lat},
			lon => $cache{$place}{lon},
		};
	}
}

print 'Successfully geocoded ', scalar(@geocoded_events), " events.\n";
print "Generating map...\n";

# Group events by location
my %location_groups;
foreach my $event (@geocoded_events) {
	my $key = sprintf("%.6f,%.6f", $event->{lat}, $event->{lon});
	push @{$location_groups{$key}}, $event;
}

# Generate map based on available API key
if ($google_key) {
	generate_google_map(\%location_groups, $output_file, $google_key);
} else {
	generate_osm_map(\%location_groups, $output_file);
}

print "Map saved to $output_file\n";

# Generate HTML for grouped events
sub generate_popup_html {
	my ($events) = @_;

	my $place = $events->[0]{place};
	my $event_count = scalar(@$events);

	# Add scrollable container if more than 5 events
	my $container_start = '';
	my $container_end = '';
	if ($event_count > 5) {
		$container_start = '<div style="max-height: 300px; overflow-y: auto;">';
		$container_end = '</div>';
	}

	my $html = "<b>$place</b><br><br>$container_start";

	# Group by type
	my %by_type;
	foreach my $event (@$events) {
		push @{$by_type{$event->{type}}}, $event;
	}

	# Sort function for dates
	my $sort_by_date = sub {
		return 0 if(($a->{'date'} =~ /^Unknown/i) || ($b->{'date'} =~ /^Unknown/));
		return Date::Cmp::datecmp($a->{'date'}, $b->{'date'});
	};

	# Add births
	if ($by_type{birth}) {
		$html .= "<b>Births:</b><br>";
		foreach my $event (sort $sort_by_date @{$by_type{birth}}) {
			$html .= sprintf(
				'<span style="color: green; font-size: 20px;">●</span> %s (%s)<br>',
				$event->{name},
				$event->{date}
			);
		}
		$html .= "<br>";
	}

	# Add marriages
	if ($by_type{marriage}) {
		$html .= "<b>Marriages:</b><br>";
		foreach my $event (sort $sort_by_date @{$by_type{marriage}}) {
			$html .= sprintf(
				'<span style="color: blue; font-size: 20px;">●</span> %s (%s)<br>',
				$event->{name},
				$event->{date}
			);
		}
		$html .= "<br>";
	}

	# Add deaths
	if ($by_type{death}) {
		$html .= "<b>Deaths:</b><br>";
		foreach my $event (sort $sort_by_date @{$by_type{death}}) {
			$html .= sprintf(
				'<span style="color: red; font-size: 20px;">●</span> %s (%s)<br>',
				$event->{name},
				$event->{date}
			);
		}
	}

	$html .= $container_end;

	return $html;
}

# Generate Google Maps
sub generate_google_map {
	my ($location_groups, $file, $key) = @_;

	my $map = HTML::GoogleMaps::V3->new(
		key => $key,
		height => '600px',
		width => '100%',
	);

	# Add markers for each location
	my $first = 1;
	foreach my $loc_key (keys %$location_groups) {
		my $events = $location_groups->{$loc_key};
		my ($lat, $lon) = split /,/, $loc_key;

		my $html = generate_popup_html($events);

		$map->add_marker(
			point => [$lat, $lon],
			html => $html,
		);

		# Center on first location
		if ($first) {
			$map->center([$lat, $lon]);
			$map->zoom(4);
			$first = 0;
		}
	}

	# Generate and save HTML
	open my $fh, '>', $file or die "Cannot open $file: $!";
	print $fh render($map);
	close $fh;
}

# Generate OpenStreetMap using HTML::OSM
sub generate_osm_map {
	my ($location_groups, $file) = @_;

	# Create HTML::OSM object
	my $osm = HTML::OSM->new(zoom => 12);

	# Add markers for each location
	foreach my $loc_key (keys %$location_groups) {
		my $events = $location_groups->{$loc_key};
		my ($lat, $lon) = split /,/, $loc_key;

		my $html = generate_popup_html($events);

		$osm->add_marker(
			point => [$lat, $lon],
			html => $html,
		);
	}

	# Find location with most events
	my ($center_lat, $center_lon) = (0, 0);
	my $max_events = 0;
	foreach my $loc_key (keys %$location_groups) {
		my $event_count = scalar(@{$location_groups->{$loc_key}});
		if ($event_count > $max_events) {
			$max_events = $event_count;
			($center_lat, $center_lon) = split /,/, $loc_key;
		}
	}

	$osm->center([$center_lat, $center_lon]);

	# Generate and save HTML
	open my $fh, '>', $file or die "Cannot open $file: $!";
	print $fh render($osm);
	close $fh;
}

# Generate HTML
sub render {
	my $map = $_[0];
	my ($head, $body) = $map->onload_render();

	my $html = <<"HTML";
<!DOCTYPE html>
<html>
<head>
	<title>GEDCOM Events Map</title>
	<meta charset="utf-8" />
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<style>
		body { margin: 0; padding: 0; font-family: Arial, sans-serif; }
		#map {
			margin: 0 auto; /* This centers it horizontally */
			height: 90vh;
			width: 80% !important;
		}
		#map > * {
			margin: 0 auto; /* This centers child elements */
			width: 100% !important; /* Use 100% to fill the parent, not 80% */
			height: 100% !important;
		}
	</style>
	$head
</head>
<body>
	<div id="map">
	$body
	</div>
</body>
</html>
HTML



( run in 0.808 second using v1.01-cache-2.11-cpan-364913b4093 )