How to generate your interactive footprint map
Visit at π±https://map-l20m.onrender.com
Run the code by yourselves
π Click here to see the code
import folium
import pandas as pd
import json
import os
from geopy.geocoders import Nominatim
from typing import List, Dict, Optional
import webbrowser
from datetime import datetime, timedelta
import warnings
from dateutil import parser
import re
warnings.filterwarnings('ignore')
class PersonalizedWorldMap:
def __init__(self, user_name: str = "visitor"):
"""
Initialize personalized map
Args:
user_name: User name for personalization
"""
self.user_name = user_name
self.map = None
self.cities = []
self.geolocator = Nominatim(user_agent="personal_world_map")
self.colors = ['red', 'blue', 'green', 'purple', 'orange',
'darkred', 'lightred', 'beige', 'darkblue',
'darkgreen', 'cadetblue', 'darkpurple',
'white', 'pink', 'lightblue', 'lightgreen']
# Create user-specific directory
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
self.output_dir = f"maps/{user_name}_{timestamp}"
os.makedirs(self.output_dir, exist_ok=True)
print(f"Hi, {user_name}! Mark your footprint and record your travel.")
print("=" * 50)
def _create_base_map(self):
"""Create base world map"""
self.map = folium.Map(
location=[0, 20], # Center of world map
zoom_start=2, # Initial zoom level
control_scale=True, # Show scale (default)
tiles='OpenStreetMap',
width='100%', # Map container size
height='100%'
)
# Add multiple map layers
tile_layers = {
'OpenStreetMap': 'OpenStreetMap',
'Satellite Image': 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
'Topographic Map': 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer/tile/{z}/{y}/{x}',
'Dark Theme': 'CartoDB dark_matter'
}
for name, tile in tile_layers.items():
folium.TileLayer(
tile,
name=name,
attr='Map data Β© OpenStreetMap contributors'
).add_to(self.map)
def get_coordinates(self, city_name: str) -> Optional[tuple]:
"""
Get city coordinates (auto-complete country information)
Args:
city_name: City name (can include country, e.g., "Beijing, China")
Returns:
(latitude, longitude) or None
"""
try:
# Try direct query
location = self.geolocator.geocode(city_name, timeout=10)
# If not found, try adding country information
if not location and ',' not in city_name:
# Common country suffixes
location = self.geolocator.geocode(f"{city_name}, China", timeout=10)
if not location:
location = self.geolocator.geocode(f"{city_name}, USA", timeout=10)
if not location:
location = self.geolocator.geocode(f"{city_name}", timeout=10)
if location:
return (location.latitude, location.longitude)
else:
print(f"β οΈ Could not find {city_name}")
return None
except Exception as e:
print(f"β Error when searching for {city_name}: {e}")
return None
def _parse_timestamp_string(self, timestamp_str: str) -> Dict:
"""
Parse timestamp string with multiple format support
Args:
timestamp_str: The timestamp string to parse
Returns:
Dict containing parsed time information
"""
timestamp_str = str(timestamp_str).strip()
original_str = timestamp_str
# Debug information
print(f" π Parsing timestamp: '{original_str}'")
# 1. Check if it's a year range (e.g., "2020-2023")
year_range_match = re.match(r'^(\d{4})\s*-\s*(\d{4})$', timestamp_str)
if year_range_match:
start_year = int(year_range_match.group(1))
end_year = int(year_range_match.group(2))
visit_date = datetime(start_year, 1, 1)
return {
'visit_date': visit_date,
'visit_year': start_year,
'visit_month': 1,
'display_date': f"{start_year}-{end_year}",
'is_range': True,
'original_timestamp': original_str
}
# 2. Check if it's just a year (e.g., "2023")
if timestamp_str.isdigit() and len(timestamp_str) == 4:
year = int(timestamp_str)
visit_date = datetime(year, 1, 1)
return {
'visit_date': visit_date,
'visit_year': year,
'visit_month': 1,
'display_date': f"{year}",
'is_range': False,
'original_timestamp': original_str
}
# 3. Check if it's a Unix timestamp (numeric)
if timestamp_str.replace('.', '', 1).isdigit():
try:
timestamp_float = float(timestamp_str)
visit_date = datetime.fromtimestamp(timestamp_float)
return {
'visit_date': visit_date,
'visit_year': visit_date.year,
'visit_month': visit_date.month,
'display_date': visit_date.strftime("%B %d, %Y"),
'is_range': False,
'original_timestamp': original_str
}
except (ValueError, OSError):
pass
# 4. Try multiple datetime formats
datetime_formats = [
# ISO formats
'%Y-%m-%dT%H:%M:%S',
'%Y-%m-%dT%H:%M:%S.%f',
'%Y-%m-%dT%H:%M:%SZ',
# Standard formats
'%Y-%m-%d %H:%M:%S',
'%Y-%m-d %H:%M',
'%Y-%m-%d',
# Slash formats
'%Y/%m/%d %H:%M:%S',
'%Y/%m/%d %H:%M',
'%Y/%m/%d',
# Day-first formats
'%d/%m/%Y %H:%M:%S',
'%d/%m/%Y %H:%M',
'%d/%m/%Y',
# Month-first (US) formats
'%m/%d/%Y %H:%M:%S',
'%m/%d/%Y %H:%M',
'%m/%d/%Y',
# Year-month format
'%Y-%m',
'%Y/%m',
# Text month formats
'%B %d, %Y',
'%b %d, %Y',
'%d %B %Y',
'%d %b %Y',
]
for fmt in datetime_formats:
try:
visit_date = datetime.strptime(timestamp_str, fmt)
# Determine display format based on what was parsed
if fmt == '%Y-%m' or fmt == '%Y/%m':
display_date = visit_date.strftime("%B %Y")
elif fmt == '%Y':
display_date = visit_date.strftime("%Y")
else:
display_date = visit_date.strftime("%B %d, %Y")
return {
'visit_date': visit_date,
'visit_year': visit_date.year,
'visit_month': visit_date.month,
'display_date': display_date,
'is_range': False,
'original_timestamp': original_str
}
except ValueError:
continue
# 5. Try using dateutil parser as fallback
try:
visit_date = parser.parse(timestamp_str, fuzzy=True)
return {
'visit_date': visit_date,
'visit_year': visit_date.year,
'visit_month': visit_date.month,
'display_date': visit_date.strftime("%B %d, %Y"),
'is_range': False,
'original_timestamp': original_str
}
except:
pass
# 6. If all parsing fails, use current date with warning
print(f" β οΈ Could not parse timestamp '{original_str}', using current date")
visit_date = datetime.now()
return {
'visit_date': visit_date,
'visit_year': visit_date.year,
'visit_month': visit_date.month,
'display_date': visit_date.strftime("%B %d, %Y") + " (estimated)",
'is_range': False,
'original_timestamp': original_str
}
def _get_visit_time(self, city_name: str) -> Dict:
"""
Get user visit time
Args:
city_name: City name
Returns:
Dict: Information containing visit date and timestamp
"""
print(f"\nπ
Record visit time for {city_name}")
print("-" * 40)
while True:
print("\nSelect time input method:")
print(" 1. Enter specific date (YYYY-MM-DD)")
print(" 2. Enter year and month (YYYY-MM)")
print(" 3. Enter year only (YYYY)")
print(" 4. Use current time")
print(" 5. Time range (YYYY-YYYY)")
time_choice = input("Choose (1-5, default: 4): ").strip()
try:
if time_choice == '1':
# Enter specific date
date_str = input("Enter visit date (YYYY-MM-DD, e.g., 2023-05-20): ").strip()
time_info = self._parse_timestamp_string(date_str)
if time_info['display_date'].endswith("(estimated)"):
# Re-prompt if parsing failed
print("β Invalid date format, please try again")
continue
elif time_choice == '2':
# Enter year and month
month_str = input("Enter visit month (YYYY-MM, e.g., 2023-05): ").strip()
time_info = self._parse_timestamp_string(month_str)
if time_info['display_date'].endswith("(estimated)"):
print("β Invalid month format, please try again")
continue
elif time_choice == '3':
# Enter year only
year_str = input("Enter visit year (YYYY, e.g., 2023): ").strip()
time_info = self._parse_timestamp_string(year_str)
if time_info['display_date'].endswith("(estimated)"):
print("β Invalid year format, please try again")
continue
elif time_choice == '5':
# Time range
range_str = input("Enter visit time range (YYYY-YYYY, e.g., 2020-2023): ").strip()
time_info = self._parse_timestamp_string(range_str)
if not time_info.get('is_range', False):
print("β Invalid range format, please try again")
continue
else:
# Use current time (default)
visit_date = datetime.now()
time_info = {
'visit_date': visit_date,
'visit_year': visit_date.year,
'visit_month': visit_date.month,
'display_date': visit_date.strftime("%B %d, %Y"),
'is_range': False,
'original_timestamp': visit_date.isoformat()
}
# Confirm time input
print(f"\nβ
Recorded time: {time_info['display_date']}")
confirm = input("Confirm time? (y/n, default: y): ").strip().lower()
if confirm != 'n':
# Add timestamp for consistency
time_info['timestamp'] = time_info['visit_date'].timestamp()
return time_info
except Exception as e:
print(f"β Time format error: {e}")
print("Please try again...")
def _generate_year_marker(self, city_name: str, lat: float, lon: float,
visit_year: int, color: str, note: str = "") -> folium.Marker:
"""
Generate marker with year indicator
Args:
city_name: City name
lat: Latitude
lon: Longitude
visit_year: Visit year
color: Marker color
note: Note
Returns:
folium.Marker: Marker with year indicator
"""
# Create custom icon with year
icon_html = f"""
<div style="background-color: {color};
width: 40px;
height: 40px;
border-radius: 50%;
border: 2px solid white;
box-shadow: 0 2px 5px rgba(0,0,0,0.3);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
font-family: Arial, sans-serif;">
{str(visit_year)[-2:]}
</div>
"""
# Create popup window
popup_html = f"""
<div style="width: 250px;">
<div style="background-color:{color}; padding:8px; border-radius:5px; margin-bottom:10px;">
<h3 style="color:white; margin:0; font-size:16px;">π {city_name}</h3>
</div>
<div style="margin-bottom:10px;">
<p><b>π
Visit Time:</b><br>{visit_year}</p>
<p><b>π Coordinates:</b><br>{lat:.4f}, {lon:.4f}</p>
<p><b>π€ User:</b><br>{self.user_name}</p>
</div>
<div style="background-color:#f5f5f5; padding:8px; border-radius:3px; margin:10px 0;">
<p style="margin:0; font-size:12px;"><b>π Note:</b><br>{note if note else 'No note'}</p>
</div>
<hr style="margin:10px 0;">
<p style="font-size:11px; color:#666; text-align:center;">
<i>Global Footprints β’ Lasting Memories</i>
</p>
</div>
"""
# Create custom icon
icon = folium.DivIcon(
html=icon_html,
icon_size=(40, 40),
icon_anchor=(20, 20)
)
# Create marker
return folium.Marker(
location=[lat, lon],
popup=folium.Popup(popup_html, max_width=300),
tooltip=f"{city_name} ({visit_year})",
icon=icon
)
def _add_city_marker_with_time(self, city_name: str, lat: float, lon: float,
color: str, time_info: Dict, note: str = ""):
"""Add city marker with time information to map"""
if not self.map:
self._create_base_map()
# Create popup window with time information
popup_html = f"""
<div style="width: 250px;">
<div style="background-color:{color}; padding:8px; border-radius:5px; margin-bottom:10px;">
<h3 style="color:white; margin:0; font-size:16px;">π {city_name}</h3>
</div>
<div style="margin-bottom:10px;">
<p><b>π
Visit Time:</b><br>{time_info['display_date']}</p>
<p><b>π Coordinates:</b><br>{lat:.4f}, {lon:.4f}</p>
<p><b>π€ User:</b><br>{self.user_name}</p>
</div>
<div style="background-color:#f5f5f5; padding:8px; border-radius:3px; margin:10px 0;">
<p style="margin:0; font-size:12px;"><b>π Note:</b><br>{note if note else 'No note'}</p>
</div>
<hr style="margin:10px 0;">
<p style="font-size:11px; color:#666; text-align:center;">
<i>Global Footprints β’ Lasting Memories</i>
</p>
</div>
"""
# Use default icon
marker = folium.Marker(
location=[lat, lon],
popup=folium.Popup(popup_html, max_width=300),
tooltip=f"{city_name} ({time_info['display_date']})",
icon=folium.Icon(color=color, icon='glyphicon glyphicon-map-marker', prefix='glyphicon')
)
# Add year marker if year information exists
if 'visit_year' in time_info and time_info['visit_year']:
year_marker = self._generate_year_marker(city_name, lat, lon,
time_info['visit_year'], color, note)
year_marker.add_to(self.map)
marker.add_to(self.map)
def add_city_interactive(self) -> bool:
"""
Interactive city marking (including visit time)
Returns:
bool: Whether to continue adding
"""
print("\n" + "=" * 50)
print("Choose your city (input 'q': exit, 'l': check marked cities)")
print("=" * 50)
while True:
city_input = input("\nπ Enter city name (e.g., Beijing, Chicago): ").strip()
if city_input.lower() == 'q':
return False
elif city_input.lower() == 'l':
self.show_selected_cities()
continue
elif not city_input:
continue
# Get coordinates
print(f"π Searching for {city_input}...")
coordinates = self.get_coordinates(city_input)
if coordinates:
lat, lon = coordinates
print(f"β
Found: {city_input} - Coordinates: ({lat:.4f}, {lon:.4f})")
# Get visit time
time_info = self._get_visit_time(city_input)
# Let user choose marker style
print("\nπ¨ Choose marker color:")
for i, color in enumerate(self.colors[:10], 1):
print(f" {i}. {color}")
color_choice = input("Choose color (1-10, default: 1): ").strip()
try:
color_idx = int(color_choice) - 1 if color_choice else 0
color = self.colors[min(max(color_idx, 0), 9)]
except:
color = 'red'
# Add marker
self._add_city_marker_with_time(city_input, lat, lon, color, time_info)
# Ask to add note
note = input("π Add a note (optional, press Enter to skip): ").strip()
# Add to city list
city_data = {
'name': city_input,
'latitude': lat,
'longitude': lon,
'color': color,
'note': note,
'timestamp': datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
'visit_date': time_info['visit_date'].strftime("%Y-%m-%d"),
'visit_year': time_info['visit_year'],
'display_date': time_info['display_date'],
'is_range': time_info.get('is_range', False),
'original_timestamp': time_info.get('original_timestamp', '')
}
if time_info.get('visit_month'):
city_data['visit_month'] = time_info['visit_month']
self.cities.append(city_data)
print(f"β
Marked city: {city_input} ({time_info['display_date']})")
# Ask to continue
continue_add = input("\nContinue marking cities? (y/n, default: y): ").strip().lower()
if continue_add == 'n':
return False
else:
print(f"β Not found: {city_input}")
retry = input("Try again? (y/n, default: y): ").strip().lower()
if retry == 'n':
continue_add = input("Continue marking other cities? (y/n, default: y): ").strip().lower()
if continue_add == 'n':
return False
def show_selected_cities(self):
"""Show list of selected cities (including time information)"""
if not self.cities:
print("π No cities marked yet")
return
print("\nπ Marked cities:")
print("=" * 80)
print(f"{'ID':<4} {'City':<20} {'Visit Time':<15} {'Latitude':<10} {'Longitude':<10} {'Note':<15}")
print("-" * 80)
for i, city in enumerate(self.cities, 1):
name = city['name'][:18] + '..' if len(city['name']) > 18 else city['name']
visit_time = city['display_date'][:13] + '..' if len(city['display_date']) > 13 else city['display_date']
note = city['note'][:13] + '..' if len(city['note']) > 13 else city['note']
print(f"{i:<4} {name:<20} {visit_time:<15} {city['latitude']:<10.4f} {city['longitude']:<10.4f} {note:<15}")
print("=" * 80)
def add_cities_from_file(self, file_path: str):
"""Import cities in bulk from file with enhanced timestamp parsing"""
try:
# Create base map first
if not self.map:
self._create_base_map()
print(f"π Loading file: {file_path}")
print("=" * 60)
if file_path.endswith('.csv'):
# Read CSV file
df = pd.read_csv(file_path)
# Check required columns
required_columns = ['name']
optional_columns = ['color', 'note', 'timestamp']
# Check if required column exists
if 'name' not in df.columns:
print(f"β Missing required column: 'name'")
print(f" Available columns: {', '.join(df.columns)}")
return
print(f"β
CSV file loaded successfully. Found {len(df)} cities.")
print("\nSupported timestamp formats:")
print(" β’ YYYY-MM-DD HH:MM:SS (e.g., 2023-05-15 10:30:00)")
print(" β’ YYYY-MM-DD (e.g., 2023-05-15)")
print(" β’ YYYY (e.g., 2023)")
print(" β’ YYYY-YYYY (e.g., 2020-2023 for time range)")
print(" β’ Unix timestamp (e.g., 1684146600)")
print(" β’ Various date formats with / separator")
print("=" * 60)
success_count = 0
failed_cities = []
for index, row in df.iterrows():
try:
# Extract city name
city_name = str(row['name']).strip()
# Get coordinates automatically
print(f"\nπ Processing {index + 1:3d}. {city_name}")
coordinates = self.get_coordinates(city_name)
if not coordinates:
print(f" β Could not find coordinates for {city_name}")
failed_cities.append(city_name)
continue
lat, lon = coordinates
print(f" β
Coordinates: ({lat:.4f}, {lon:.4f})")
# Get color (with default)
if 'color' in df.columns and pd.notna(row['color']):
color = str(row['color']).strip().lower()
# Validate color
if color not in self.colors:
print(f" β οΈ Color '{color}' not in predefined colors, using 'blue'")
color = 'blue'
else:
color = 'blue'
# Get note (optional)
if 'note' in df.columns and pd.notna(row['note']):
note = str(row['note'])
else:
note = ""
# Parse timestamp (with default)
if 'timestamp' in df.columns and pd.notna(row['timestamp']):
timestamp_str = str(row['timestamp'])
else:
# Use current date as default
timestamp_str = datetime.now().strftime("%Y-%m-%d")
time_info = self._parse_timestamp_string(timestamp_str)
# Validate parsed year
current_year = datetime.now().year
if time_info['visit_year'] < 1900 or time_info['visit_year'] > current_year + 1:
print(f" β οΈ Unreasonable year {time_info['visit_year']}, adjusting to {current_year}")
time_info['visit_date'] = time_info['visit_date'].replace(year=current_year)
time_info['visit_year'] = current_year
time_info['display_date'] = f"{current_year} (adjusted)"
# Create complete time_info for marker
marker_time_info = {
'timestamp': time_info['visit_date'].timestamp(),
'visit_date': time_info['visit_date'],
'visit_year': time_info['visit_year'],
'visit_month': time_info['visit_month'],
'display_date': time_info['display_date'],
'is_range': time_info.get('is_range', False)
}
# Add marker to map
self._add_city_marker_with_time(city_name, lat, lon, color, marker_time_info, note)
# Add to cities list
city_data = {
'name': city_name,
'latitude': lat,
'longitude': lon,
'color': color,
'note': note,
'timestamp': timestamp_str,
'visit_date': time_info['visit_date'].strftime("%Y-%m-%d"),
'visit_year': time_info['visit_year'],
'visit_month': time_info['visit_month'],
'display_date': time_info['display_date'],
'is_range': time_info.get('is_range', False),
'original_timestamp': time_info.get('original_timestamp', timestamp_str)
}
self.cities.append(city_data)
success_count += 1
print(
f" β
Added: {city_name:<20} β Year: {time_info['visit_year']:4d} | Display: {time_info['display_date']}")
except Exception as e:
print(f" β Error processing row {index + 1}: {e}")
failed_cities.append(city_name if 'city_name' in locals() else f"Row {index + 1}")
continue
elif file_path.endswith('.json'):
# Read JSON file
with open(file_path, 'r', encoding='utf-8') as f:
cities_data = json.load(f)
print(f"β
JSON file loaded successfully. Found {len(cities_data)} cities.")
print("=" * 60)
success_count = 0
failed_cities = []
for index, city in enumerate(cities_data):
try:
# Extract city name
city_name = str(city['name']).strip()
# Get coordinates automatically
print(f"\nπ Processing {index + 1:3d}. {city_name}")
coordinates = self.get_coordinates(city_name)
if not coordinates:
print(f" β Could not find coordinates for {city_name}")
failed_cities.append(city_name)
continue
lat, lon = coordinates
print(f" β
Coordinates: ({lat:.4f}, {lon:.4f})")
# Get color (with default)
if 'color' in city and city['color']:
color = str(city['color']).strip().lower()
# Validate color
if color not in self.colors:
print(f" β οΈ Color '{color}' not in predefined colors, using 'blue'")
color = 'blue'
else:
color = 'blue'
# Get note (optional)
if 'note' in city and city['note']:
note = str(city['note'])
else:
note = ""
# Parse timestamp (with default)
if 'timestamp' in city and city['timestamp']:
timestamp_str = str(city['timestamp'])
else:
# Use current date as default
timestamp_str = datetime.now().strftime("%Y-%m-%d")
time_info = self._parse_timestamp_string(timestamp_str)
# Validate parsed year
current_year = datetime.now().year
if time_info['visit_year'] < 1900 or time_info['visit_year'] > current_year + 1:
print(f" β οΈ Unreasonable year {time_info['visit_year']}, adjusting to {current_year}")
time_info['visit_date'] = time_info['visit_date'].replace(year=current_year)
time_info['visit_year'] = current_year
time_info['display_date'] = f"{current_year} (adjusted)"
# Create complete time_info for marker
marker_time_info = {
'timestamp': time_info['visit_date'].timestamp(),
'visit_date': time_info['visit_date'],
'visit_year': time_info['visit_year'],
'visit_month': time_info['visit_month'],
'display_date': time_info['display_date'],
'is_range': time_info.get('is_range', False)
}
# Add marker to map
self._add_city_marker_with_time(city_name, lat, lon, color, marker_time_info, note)
# Add to cities list
city_data = {
'name': city_name,
'latitude': lat,
'longitude': lon,
'color': color,
'note': note,
'timestamp': timestamp_str,
'visit_date': time_info['visit_date'].strftime("%Y-%m-%d"),
'visit_year': time_info['visit_year'],
'visit_month': time_info['visit_month'],
'display_date': time_info['display_date'],
'is_range': time_info.get('is_range', False),
'original_timestamp': time_info.get('original_timestamp', timestamp_str)
}
self.cities.append(city_data)
success_count += 1
print(
f" β
Added: {city_name:<20} β Year: {time_info['visit_year']:4d} | Display: {time_info['display_date']}")
except Exception as e:
print(f" β Error processing city {index + 1}: {e}")
failed_cities.append(city_name if 'city_name' in locals() else f"City {index + 1}")
continue
else:
print("β Unsupported file format. Please use CSV or JSON.")
return
print("\n" + "=" * 60)
print(
f"β
Successfully imported {success_count} out of {len(df) if file_path.endswith('.csv') else len(cities_data)} cities")
if failed_cities:
print(f"β Failed to import {len(failed_cities)} cities:")
for city in failed_cities[:10]: # Show first 10 failed cities
print(f" - {city}")
if len(failed_cities) > 10:
print(f" ... and {len(failed_cities) - 10} more")
# Show import summary
if self.cities:
years = [city['visit_year'] for city in self.cities if 'visit_year' in city]
if years:
print(f"\nπ Import Summary:")
print(f" β’ Total imported: {len(self.cities)} cities")
print(f" β’ Earliest year: {min(years)}")
print(f" β’ Latest year: {max(years)}")
print(f" β’ Time ranges: {sum(1 for city in self.cities if city.get('is_range', False))}")
print(f" β’ Unique years: {len(set(years))}")
except FileNotFoundError:
print(f"β File not found: {file_path}")
except pd.errors.EmptyDataError:
print("β The file is empty")
except Exception as e:
print(f"β Failed to import file: {e}")
import traceback
traceback.print_exc()
def save_map(self):
"""Save personalized map and related data"""
if not self.cities:
print("β οΈ No cities marked yet, cannot save map")
return
# Add layer control
folium.LayerControl().add_to(self.map)
# Add title
title_html = f'''
<div style="position: fixed;
top: 10px;
left: 50px;
z-index: 1000;
background-color: white;
padding: 10px;
border-radius: 5px;
box-shadow: 0 2px 6px rgba(0,0,0,0.3);
font-family: Arial, sans-serif;">
<h3 style="margin: 0; color: #333;">π {self.user_name}'s Footprint Map</h3>
<p style="margin: 5px 0 0 0; color: #666; font-size: 12px;">
Marked {len(self.cities)} cities β’ Created: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
</p>
</div>
'''
self.map.get_root().html.add_child(folium.Element(title_html))
# Add timeline legend
if any('visit_year' in city for city in self.cities):
legend_html = '''
<div style="position: fixed;
bottom: 50px;
right: 50px;
z-index: 1000;
background-color: white;
padding: 10px;
border-radius: 5px;
box-shadow: 0 2px 6px rgba(0,0,0,0.3);
font-family: Arial, sans-serif;
font-size: 12px;">
<h4 style="margin: 0 0 10px 0; color: #333;">π Timeline Legend</h4>
<div style="display: flex; align-items: center; margin-bottom: 5px;">
<div style="width: 20px; height: 20px; border-radius: 50%; background-color: red; margin-right: 10px;"></div>
<span>Number inside circle shows last two digits of visit year</span>
</div>
</div>
'''
self.map.get_root().html.add_child(folium.Element(legend_html))
# Generate filenames
map_filename = f"{self.output_dir}/{self.user_name}_World_Footprint_Map.html"
data_filename = f"{self.output_dir}/{self.user_name}_City_Data.json"
csv_filename = f"{self.output_dir}/{self.user_name}_City_Data.csv"
# Save map
self.map.save(map_filename)
# Save data
with open(data_filename, 'w', encoding='utf-8') as f:
json.dump(self.cities, f, ensure_ascii=False, indent=2)
# Save CSV with all columns including timestamp
df = pd.DataFrame(self.cities)
df.to_csv(csv_filename, index=False, encoding='utf-8-sig')
print("\n" + "=" * 50)
print("β
Successfully generated your footprint map!")
print("=" * 50)
print(f"π Files saved to: {self.output_dir}/")
print(f"πΊοΈ Interactive map: {map_filename}")
print(f"π Data files: {data_filename} and {csv_filename}")
print(f"π Total marked cities: {len(self.cities)}")
print("=" * 50)
# Automatically open in browser
try:
webbrowser.open(f"file://{os.path.abspath(map_filename)}")
print("π Opening map in browser...")
except:
print("π‘ Double-click the HTML file in File Explorer to open the map")
def run(self):
"""Run main program"""
print("""
Welcome to the Global Footprint Marking System!
Features:
1. Search and mark any city worldwide
2. Record visit time (year/month/specific date)
3. Customize marker colors and styles
4. Add personal notes
5. Generate personalized timeline map
6. Export map and data files
7. Import cities from CSV/JSON files
""")
# Show supported timestamp formats
print("π
Supported timestamp formats for CSV/JSON import:")
print("=" * 60)
print("1. Standard: 2023-05-15 10:30:00")
print("2. Date only: 2023-05-15")
print("3. Year only: 2023")
print("4. Year range: 2020-2023")
print("5. Unix timestamp: 1684146600")
print("6. ISO format: 2023-05-15T10:30:00")
print("7. Slash format: 2023/05/15 10:30:00")
print("8. US format: 05/15/2023 10:30:00")
print("=" * 60)
print()
# Show file format requirements
print("π File import requirements:")
print("=" * 60)
print("CSV Format:")
print(" Required column: name (city name)")
print(" Optional columns: color, note, timestamp")
print()
print("JSON Format:")
print(" Required field: name (city name)")
print(" Optional fields: color, note, timestamp")
print("=" * 60)
print()
# Choose input method
print("Choose city input method:")
print(" 1. Manual input (interactive)")
print(" 2. Import from file (CSV/JSON)")
choice = input("Choose (1/2, default: 1): ").strip()
if choice == '2':
file_path = input("Enter file path (CSV or JSON): ").strip()
if os.path.exists(file_path):
# Import cities from file
self.add_cities_from_file(file_path)
# Ask if user wants to add more cities manually
if self.cities:
add_more = input("\nDo you want to add more cities manually? (y/n, default: n): ").strip().lower()
if add_more == 'y':
# Create base map if not already created
if not self.map:
self._create_base_map()
# Interactive city adding
self.add_city_interactive()
else:
print(f"β File does not exist: {file_path}")
print("Switching to manual input mode")
choice = '1'
if choice == '1' or choice == '':
# Create base map
self._create_base_map()
# Interactive city adding
self.add_city_interactive()
# Save results
if self.cities:
self.save_map()
# Display statistics
print("\nπ Statistics:")
if self.cities:
df = pd.DataFrame(self.cities)
# Sort by visit year
if 'visit_year' in df.columns:
df = df.sort_values('visit_year')
print("\nSorted by visit year:")
print(df[['name', 'visit_year', 'display_date', 'color']].to_string(index=False))
# Year statistics
print(f"\nπ
Visit year distribution:")
year_counts = df['visit_year'].value_counts().sort_index()
for year, count in year_counts.items():
print(f" {year}: {count} cities")
else:
print("\nβ οΈ No cities marked, exiting program")
# ========== Main Program Entry ==========
def main():
"""Main function"""
print("=" * 60)
print("π Global Footprints - Personal Travel Recording System")
print("=" * 60)
# Get user information
user_name = input("Enter your name to create a personalized footprint map: ").strip()
if not user_name:
user_name = "Visitor"
# Create and run system
map_system = PersonalizedWorldMap(user_name)
map_system.run()
print("\nYour footprint map has been saved in the 'maps/' directory")
print("Share your HTML file with friends to showcase your travel footprints!")
if __name__ == "__main__":
# Ensure necessary libraries are installed
try:
import folium
import pandas
from geopy.geocoders import Nominatim
from dateutil import parser
except ImportError:
print("Installing necessary libraries...")
import subprocess
import sys
libraries = ['folium', 'pandas', 'geopy', 'python-dateutil']
for lib in libraries:
subprocess.check_call([sys.executable, "-m", "pip", "install", lib])
print("Libraries installed successfully, please run the program again")
exit(0)
main()Notes for using the code:
1. Path format of uploaded files
your path\city_file.csv
2. Contents of uploaded files
| name | latitude | longitude | color | note | timestamp |
|---|---|---|---|---|---|
| Xiamen | 24.54 | 118.08 | darkred | 2021/9/1 0:00 |
* This table shows an example of detailed contents. But in this interactive program, you just need to type the name of city and choose the option of color when running the python code.