File size: 6,231 Bytes
47c367a
 
 
 
 
 
 
 
be196c4
47c367a
 
be196c4
47c367a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
be196c4
47c367a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
be196c4
47c367a
 
 
 
 
 
 
 
 
 
 
be196c4
47c367a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
be196c4
47c367a
be196c4
47c367a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
be196c4
47c367a
 
 
 
 
 
be196c4
47c367a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
be196c4
47c367a
 
 
 
 
 
 
 
be196c4
47c367a
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
import gradio as gr
import pandas as pd
import folium
from geopy.geocoders import Nominatim
from geopy.extra.rate_limiter import RateLimiter
import tempfile
from typing import Optional, Tuple
import warnings

# Suppress warnings
warnings.filterwarnings("ignore")

# Historical Tile Providers
HISTORICAL_TILES = {
    "David Rumsey (1790)": {
        "url": "https://map1.davidrumsey.com/tiles/rumsey/SDSC1790/{z}/{x}/{y}.png",
        "attr": "David Rumsey Map Collection",
        "min_year": 1700,
        "max_year": 1800
    },
    "David Rumsey (1860)": {
        "url": "https://map1.davidrumsey.com/tiles/rumsey/SDSC1860/{z}/{x}/{y}.png",
        "attr": "David Rumsey Map Collection", 
        "min_year": 1801,
        "max_year": 1900
    },
    "Stamen (1915)": {
        "url": "https://stamen-tiles.a.ssl.fastly.net/toner-lite/{z}/{x}/{y}.png",
        "attr": "Stamen Maps",
        "min_year": 1901,
        "max_year": 1920
    },
    "OpenHistoricalMap": {
        "url": "https://tile.openhistoricalmap.org/{z}/{x}/{y}.png",
        "attr": "OpenHistoricalMap",
        "min_year": 1700,
        "max_year": 2023
    }
}

class Geocoder:
    def __init__(self):
        self.geolocator = Nominatim(user_agent="historical_mapper", timeout=10)
        self.geocode = RateLimiter(self.geolocator.geocode, min_delay_seconds=1)
        self.cache = {}
    
    def get_coords(self, location: str) -> Optional[Tuple[float, float]]:
        if not location or pd.isna(location):
            return None
        if location in self.cache:
            return self.cache[location]
        
        try:
            result = self.geocode(location)
            if result:
                coords = (result.latitude, result.longitude)
                self.cache[location] = coords
                return coords
        except Exception as e:
            print(f"Geocoding failed for '{location}': {str(e)}")
        
        self.cache[location] = None
        return None

def get_tile_layer(year: int):
    """Select the most appropriate tile layer for the given year"""
    for name, config in HISTORICAL_TILES.items():
        if config["min_year"] <= year <= config["max_year"]:
            return folium.TileLayer(
                tiles=config["url"],
                attr=config["attr"],
                name=name,
                overlay=False
            )
    return folium.TileLayer("OpenStreetMap")

def create_historical_map(df: pd.DataFrame, location_col: str, year: int = 1900) -> str:
    geocoder = Geocoder()
    
    # Create map with historical base layer
    base_layer = get_tile_layer(year)
    m = folium.Map(location=[40, -10], zoom_start=2, control_scale=True)
    base_layer.add_to(m)
    
    # Add all other historical layers as options
    for name, config in HISTORICAL_TILES.items():
        if config["url"] != base_layer.tiles:
            folium.TileLayer(
                tiles=config["url"],
                attr=config["attr"],
                name=f"{name} ({config['min_year']}-{config['max_year']})",
                overlay=False
            ).add_to(m)
    
    # Add markers with historical styling
    coords_list = []
    for loc in df[location_col].dropna().unique():
        coords = geocoder.get_coords(str(loc))
        if coords:
            folium.Marker(
                location=coords,
                popup=f"<b>{loc}</b><br>Year: {year}",
                icon=folium.Icon(
                    color="red",
                    icon="info-sign",
                    prefix="fa"
                )
            ).add_to(m)
            coords_list.append(coords)
    
    # Add layer control and fit bounds
    folium.LayerControl().add_to(m)
    if coords_list:
        m.fit_bounds(coords_list)
    
    return m._repr_html_()

def process_file(file_obj, location_col, year):
    try:
        # Read input file
        df = pd.read_excel(file_obj.name)
        
        # Validate column exists
        if location_col not in df.columns:
            return None, f"Column '{location_col}' not found", None
        
        # Create historical map
        map_html = create_historical_map(df, location_col, year)
        
        # Save processed data
        with tempfile.NamedTemporaryFile(suffix=".xlsx", delete=False) as tmp:
            df.to_excel(tmp.name, index=False)
            processed_path = tmp.name
        
        # Generate stats
        stats = (
            f"Total locations: {len(df)}\n"
            f"Unique places: {df[location_col].nunique()}\n"
            f"Map year: {year}"
        )
        
        return (
            f"<div style='width:100%; height:70vh'>{map_html}</div>",
            stats,
            processed_path
        )
    
    except Exception as e:
        return None, f"Error: {str(e)}", None

# Gradio Interface
with gr.Blocks(title="Historical Map Explorer", theme=gr.themes.Soft()) as app:
    gr.Markdown("# Historical Location Mapper")
    
    with gr.Row():
        with gr.Column():
            file_input = gr.File(
                label="Upload Excel File",
                file_types=[".xlsx", ".xls"],
                type="filepath"
            )
            location_col = gr.Textbox(
                label="Location Column Name",
                value="locations",
                placeholder="Enter exact column name with locations"
            )
            year = gr.Slider(
                minimum=1700,
                maximum=2023,
                value=1900,
                step=1,
                label="Map Year"
            )
            map_btn = gr.Button("Generate Historical Map", variant="primary")
        
        with gr.Column():
            map_display = gr.HTML(
                label="Historical Map",
                value="<div style='text-align:center;padding:20px;'>"
                      "Map will appear here after processing</div>"
            )
            stats_output = gr.Textbox(label="Statistics")
            download_output = gr.File(label="Download Processed Data")

    map_btn.click(
        process_file,
        inputs=[file_input, location_col, year],
        outputs=[map_display, stats_output, download_output]
    )

if __name__ == "__main__":
    app.launch()