File size: 6,432 Bytes
3ebd172
 
5bf53f5
 
3ebd172
5bf53f5
 
3ebd172
876e202
 
5bf53f5
 
 
 
3ebd172
876e202
3ebd172
5bf53f5
 
876e202
3ebd172
5bf53f5
 
 
 
 
876e202
3ebd172
 
 
5bf53f5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
876e202
 
5bf53f5
 
 
3ebd172
 
5bf53f5
3ebd172
 
 
 
 
 
 
 
876e202
 
5bf53f5
876e202
 
 
3ebd172
 
5bf53f5
 
 
3ebd172
 
 
5bf53f5
3ebd172
 
 
5bf53f5
3ebd172
 
876e202
3ebd172
 
 
 
876e202
3ebd172
 
 
 
 
5bf53f5
3ebd172
 
 
 
5bf53f5
 
3ebd172
 
876e202
3ebd172
 
876e202
3ebd172
876e202
3ebd172
 
 
 
5bf53f5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
876e202
3ebd172
5bf53f5
3ebd172
5bf53f5
876e202
 
 
 
 
 
 
5bf53f5
3ebd172
5bf53f5
 
3ebd172
 
 
5bf53f5
876e202
3ebd172
5bf53f5
 
 
 
 
 
 
 
 
 
 
 
 
3ebd172
5bf53f5
 
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
193
194
195
196
197
import gradio as gr
import time
import json
import os
from collections import deque, defaultdict
import gspread
from oauth2client.service_account import ServiceAccountCredentials

# --- Configuration ---
MAX_RECENT_EVENTS = 1000   # Trigger snapshot compression at this count
GRID_PRECISION = 4         # ~11 meters precision
RADIUS_BASE = 20           
GOOGLE_SHEET_NAME = "UrbanScout_Data" # Name of your Google Sheet
GOOGLE_CREDS_FILE = "credentials.json" # Path to your Service Account JSON

# --- Data Structures ---

# 1. Movement Data (Fog of War)
recent_events = deque()
archived_grid = defaultdict(int)

# 2. Static Markers (The Rental Spaces)
# We store these in memory for fast serving to map, 
# and also send them to Google Sheets for persistence/analysis.
reported_markers = [] 

# 3. Version Control
snapshot_version = 0
last_snapshot_time = time.time()

# --- Google Sheets Setup ---
def init_google_sheets():
    try:
        scope = ['https://spreadsheets.google.com/feeds', 'https://www.googleapis.com/auth/drive']
        # You need to generate this JSON from Google Cloud Console
        creds = ServiceAccountCredentials.from_json_keyfile_name(GOOGLE_CREDS_FILE, scope)
        client = gspread.authorize(creds)
        sheet = client.open(GOOGLE_SHEET_NAME).sheet1
        return sheet
    except Exception as e:
        print(f"[Warning] Google Sheets not connected: {e}")
        return None

# Initialize Sheet (Global)
gsheet = init_google_sheets()

# --- Helper Functions ---

def get_radius_by_speed(speed_kmh):
    if speed_kmh is None: return 10
    if speed_kmh < 6: return 35  # Walking
    if speed_kmh < 25: return 15 # Biking
    return 8                     # Driving

def trigger_snapshot():
    """Compress recent movement events."""
    global recent_events, snapshot_version, last_snapshot_time
    
    if len(recent_events) > MAX_RECENT_EVENTS:
        events_to_archive = []
        count_to_remove = int(MAX_RECENT_EVENTS / 2)
        for _ in range(count_to_remove):
            events_to_archive.append(recent_events.popleft())
            
        for event in events_to_archive:
            lat, lng = event['l']
            radius = event['r']
            grid_key = (round(lat, GRID_PRECISION), round(lng, GRID_PRECISION))
            if radius > archived_grid[grid_key]:
                archived_grid[grid_key] = radius
        
        snapshot_version += 1
        print(f"[Server] Snapshot v{snapshot_version}")

# --- API Endpoints ---

def sync_game_state(client_last_time, client_ver, user_id, lat, lng, speed):
    """
    Handles Fog of War updates AND returns reported markers.
    """
    current_time = time.time()
    
    # 1. Record Movement
    if lat is not None and lng is not None:
        if speed is None or speed < 150: 
            radius = get_radius_by_speed(speed)
            new_event = {
                "t": current_time,
                "u": user_id,
                "l": [lat, lng],
                "r": radius
            }
            recent_events.append(new_event)
    
    trigger_snapshot()
    
    # 2. Prepare Movement Payload
    response = {
        "server_time": current_time,
        "snapshot_version": snapshot_version,
        "sync_type": "delta",
        "payload": [],
        "markers": reported_markers # Send the pins to the client
    }
    
    if client_ver < snapshot_version:
        response["sync_type"] = "snapshot"
        response["payload"] = [[k[0], k[1], v] for k, v in archived_grid.items()]
        response["recent_events"] = [[e['l'][0], e['l'][1], e['r']] for e in recent_events]
    else:
        delta = [[e['l'][0], e['l'][1], e['r']] for e in recent_events if e['t'] > client_last_time]
        response["payload"] = delta

    return response

def report_discovery(report_json):
    """
    New Endpoint: Receives the form data from the Report Modal.
    """
    try:
        data = report_json if isinstance(report_json, dict) else json.loads(report_json)
        
        # 1. Add to In-Memory list (so other users see it immediately on map)
        marker_data = {
            "lat": data.get("lat"),
            "lng": data.get("lng"),
            "name": data.get("spaceName"),
            "type": "rental",
            "info": data.get("additionalInfo"),
            "price": data.get("price"),
            "cap": data.get("capacity"),
            "addr": data.get("address"),
            "book": data.get("bookingMethod")
        }
        reported_markers.append(marker_data)
        
        # 2. Send to Google Sheets (Async/Best Effort)
        if gsheet:
            row = [
                data.get("timestamp"),
                data.get("lat"),
                data.get("lng"),
                data.get("address"),
                data.get("spaceName"),
                data.get("rentableCount"),
                data.get("capacity"),
                data.get("price"),
                data.get("bookingMethod"),
                data.get("additionalInfo")
            ]
            gsheet.append_row(row)
            print(f"[Sheet] Logged: {data.get('spaceName')}")
            
        return {"status": "success", "message": "Discovery logged"}
        
    except Exception as e:
        print(f"Error reporting: {e}")
        return {"status": "error", "message": str(e)}

# --- Gradio App ---
with gr.Blocks() as demo:
    gr.Markdown("## City Explorer Backend")
    
    # Hidden inputs for the Game Loop
    with gr.Row(visible=False):
        in_time = gr.Number()
        in_ver = gr.Number()
        in_uid = gr.Textbox()
        in_lat = gr.Number()
        in_lng = gr.Number()
        in_speed = gr.Number()
        out_sync_json = gr.JSON()
        
    # Endpoint 1: The fast sync loop
    btn_sync = gr.Button("Sync Loop (Hidden)")
    btn_sync.click(
        fn=sync_game_state,
        inputs=[in_time, in_ver, in_uid, in_lat, in_lng, in_speed],
        outputs=out_sync_json,
        api_name="sync"
    )
    
    # Endpoint 2: The report form submission
    with gr.Row(visible=False):
        in_report_json = gr.JSON()
        out_report_status = gr.JSON()
        
    btn_report = gr.Button("Report Discovery (Hidden)")
    btn_report.click(
        fn=report_discovery,
        inputs=[in_report_json],
        outputs=out_report_status,
        api_name="report"
    )

if __name__ == "__main__":
    demo.queue().launch(server_name="0.0.0.0", server_port=7860)