Aleksmorshen commited on
Commit
faff0b3
·
verified ·
1 Parent(s): 1a7591a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +259 -240
app.py CHANGED
@@ -1,11 +1,37 @@
 
1
  import json
2
  import os
3
- import requests
4
- from flask import Flask, Response, request, jsonify
 
5
 
6
  app = Flask(__name__)
7
 
8
  HOTSPOTS_FILE = 'hotspots.json'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
 
10
  def get_all_hotspots():
11
  if not os.path.exists(HOTSPOTS_FILE):
@@ -35,16 +61,37 @@ def index():
35
  <link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css" />
36
  <script src="https://unpkg.com/[email protected]/dist/leaflet.js"></script>
37
  <style>
 
 
 
 
 
 
 
38
  body, html {
39
  margin: 0;
40
  padding: 0;
41
  width: 100%;
42
  height: 100%;
43
  overflow: hidden;
44
- background-color: #000;
45
- color: var(--tg-theme-text-color, white);
46
- font-family: Arial, sans-serif;
47
- touch-action: pan-x pan-y; /* Prevent default touch actions like pull-to-refresh */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  }
49
  #ar-container {
50
  position: absolute;
@@ -65,11 +112,10 @@ def index():
65
  }
66
  .hotspot {
67
  position: absolute;
68
- background-color: var(--tg-theme-button-color, rgba(0, 122, 255, 0.8));
69
- color: var(--tg-theme-button-text-color, white);
70
  padding: 10px 15px;
71
- border-radius: 10px;
72
- border: 1px solid var(--tg-theme-secondary-bg-color, rgba(255, 255, 255, 0.5));
73
  box-shadow: 0 4px 15px rgba(0, 0, 0, 0.5);
74
  transform: translate(-50%, -50%);
75
  transition: opacity 0.3s, transform 0.1s linear;
@@ -78,7 +124,7 @@ def index():
78
  will-change: transform, left, top, opacity;
79
  z-index: 10;
80
  text-align: center;
81
- cursor: pointer;
82
  }
83
  .hotspot small {
84
  font-size: 0.7em;
@@ -92,152 +138,126 @@ def index():
92
  }
93
  #map-container {
94
  position: fixed;
95
- bottom: 10px;
96
- left: 10px;
97
- width: 200px;
98
- height: 200px;
99
- background-color: var(--tg-theme-bg-color, rgba(0, 0, 0, 0.7));
100
- border-radius: 10px;
 
 
 
101
  overflow: hidden;
102
- transition: all 0.3s ease;
103
  z-index: 50;
104
  display: flex;
105
  flex-direction: column;
 
106
  }
107
  #map-container.fullscreen {
108
  width: 100%;
109
  height: 100%;
110
  top: 0;
111
  left: 0;
 
112
  border-radius: 0;
 
 
113
  justify-content: center;
114
  align-items: center;
 
115
  }
116
  #map {
117
  flex-grow: 1;
118
  width: 100%;
119
- height: calc(100% - 35px);
120
- border-radius: 10px;
121
  overflow: hidden;
122
  }
123
  #map-container.fullscreen #map {
124
- height: 100%;
125
  border-radius: 0;
126
  }
127
  #toggle-map-button {
128
- background-color: var(--tg-theme-secondary-bg-color, rgba(0, 0, 0, 0.5));
129
- color: var(--tg-theme-text-color, white);
 
 
 
130
  border: none;
131
- padding: 5px 10px;
132
- margin: 5px;
133
- border-radius: 5px;
134
  cursor: pointer;
135
- font-size: 12px;
136
- align-self: flex-end;
137
  }
138
- #map-container.fullscreen #toggle-map-button {
139
- position: absolute;
140
- top: 10px;
141
- right: 10px;
142
- font-size: 16px;
143
- margin: 0;
144
  }
145
- #loading-overlay {
146
- position: fixed;
147
- top: 0;
148
- left: 0;
149
- width: 100%;
150
- height: 100%;
151
- background-color: var(--tg-theme-bg-color, #111);
152
- color: var(--tg-theme-text-color, white);
153
- z-index: 100;
154
- display: flex;
155
- flex-direction: column;
156
- align-items: center;
157
- justify-content: center;
158
- font-size: 1.2em;
159
- text-align: center;
160
  }
161
  </style>
162
  </head>
163
  <body>
164
- <div id="loading-overlay">
165
- Загрузка AR-окружения...
166
- <br>Ожидание GPS и камеры...
 
 
 
 
167
  </div>
168
 
169
- <video id="camera-view" playsinline autoplay muted></video>
170
- <div id="ar-container"></div>
171
-
172
- <div id="map-container">
173
- <div id="map"></div>
174
- <button id="toggle-map-button">Minimap</button>
175
  </div>
176
 
177
  <script>
 
 
178
  const state = {
179
  hotspots: [],
180
  currentUserPosition: null,
181
- deviceOrientation: { alpha: 0, beta: 0, gamma: 0 },
182
- cameraFov: 60, /* Approximate FOV for a typical smartphone camera */
183
  map: null,
184
  userMarker: null,
185
  hotspotMarkers: [],
186
  initialMapSet: false,
187
- isGPSReady: false,
188
- isCameraReady: false,
189
- isOrientationReady: false
190
  };
191
 
192
- const MAX_VISIBLE_DISTANCE = 100; // Increased visible distance for better AR experience
193
-
194
- function showLoadingOverlay(message) {
195
- const overlay = document.getElementById('loading-overlay');
196
- overlay.innerText = message;
197
- overlay.style.display = 'flex';
198
- }
199
 
200
- function hideLoadingOverlay() {
201
- document.getElementById('loading-overlay').style.display = 'none';
202
- }
203
-
204
- async function initArApp() {
205
- Telegram.WebApp.ready();
206
- Telegram.WebApp.expand();
207
- hideLoadingOverlay();
208
- await setupCamera();
209
  setupGPS();
210
  setupOrientationListener();
211
  initMap();
212
- await loadHotspots();
213
  setupAddHotspotListener();
214
  requestAnimationFrame(update);
215
  }
216
 
217
- function checkAllReady() {
218
- if (state.isGPSReady && state.isCameraReady && state.isOrientationReady && Telegram.WebApp.initDataUnsafe.user) {
219
- initArApp();
220
- } else {
221
- let message = 'Загрузка AR-окружения...\n';
222
- if (!state.isCameraReady) message += 'Ожидание камеры...\n';
223
- if (!state.isGPSReady) message += 'Ожидание GPS...\n';
224
- if (!state.isOrientationReady) message += 'Ожидание датчика ориентации...\n';
225
- if (!Telegram.WebApp.initDataUnsafe.user) message += 'Ожидание данных пользователя Telegram...\n';
226
- showLoadingOverlay(message);
227
- }
228
- }
229
-
230
- document.addEventListener('DOMContentLoaded', () => {
231
- if (window.Telegram && Telegram.WebApp) {
232
- Telegram.WebApp.onEvent('mainButtonStateChanged', () => {
233
- // Handle Main Button if needed, currently not used
234
- });
235
- Telegram.WebApp.ready();
236
- checkAllReady();
237
- } else {
238
- alert('Это приложение предназначено для запуска в Telegram Mini App.');
239
- }
240
  });
 
 
 
 
 
 
 
 
 
 
241
 
242
  async function setupCamera() {
243
  const video = document.getElementById('camera-view');
@@ -248,14 +268,9 @@ def index():
248
  });
249
  video.srcObject = stream;
250
  await video.play();
251
- state.isCameraReady = true;
252
- checkAllReady();
253
  } catch (err) {
254
- alert('Не удалось получить доступ к камере: ' + err.message);
255
- console.error('Camera access error:', err);
256
  }
257
- } else {
258
- alert('Камера не поддерживается вашим браузером.');
259
  }
260
  }
261
 
@@ -263,82 +278,76 @@ def index():
263
  if (navigator.geolocation) {
264
  navigator.geolocation.watchPosition(
265
  (position) => {
266
- const newLat = position.coords.latitude;
267
- const newLon = position.coords.longitude;
268
- // Update position only if it has significantly changed or is first update
269
- if (!state.currentUserPosition ||
270
- Math.abs(newLat - state.currentUserPosition.lat) > 0.00001 ||
271
- Math.abs(newLon - state.currentUserPosition.lon) > 0.00001) {
272
-
273
- state.currentUserPosition = {
274
- lat: newLat,
275
- lon: newLon,
276
- accuracy: position.coords.accuracy // meters
277
- };
278
-
279
- if (state.userMarker) {
280
- state.userMarker.setLatLng([state.currentUserPosition.lat, state.currentUserPosition.lon]);
281
- if (!state.initialMapSet) {
282
- state.map.setView([state.currentUserPosition.lat, state.currentUserPosition.lon], 15);
283
- state.initialMapSet = true;
284
- }
285
  }
286
  }
287
- state.isGPSReady = true;
288
- checkAllReady();
289
  },
290
  (error) => {
291
- alert('Не удалось получить доступ к GPS: ' + error.message + '. Убедитесь, что GPS включен и разрешен для этого приложения.');
292
- console.error('GPS error:', error);
293
  },
294
- { enableHighAccuracy: true, maximumAge: 0, timeout: 10000 } // Increased timeout
295
  );
296
  } else {
297
- alert('GPS не поддерживается вашим браузером.');
298
  }
299
  }
300
 
301
  function setupOrientationListener() {
302
- if (window.DeviceOrientationEvent) {
303
- // Request permission for iOS 13+
304
- if (typeof DeviceOrientationEvent.requestPermission === 'function') {
305
- DeviceOrientationEvent.requestPermission()
306
- .then(permissionState => {
307
- if (permissionState === 'granted') {
308
- window.addEventListener('deviceorientation', handleOrientation, true);
309
- state.isOrientationReady = true;
310
- checkAllReady();
311
- } else {
312
- alert('Разрешение на отслеживание ориентации устройства отклонено.');
313
- }
314
- })
315
- .catch(console.error);
316
  } else {
317
- // Handle regular non-iOS 13+ browsers
318
- window.addEventListener('deviceorientation', handleOrientation, true);
319
- state.isOrientationReady = true;
320
- checkAllReady();
321
  }
322
- } else {
323
- alert('Отслеживание ориентации устройства не поддерживается.');
324
- }
325
- }
326
 
327
- function handleOrientation(event) {
328
- if (event.alpha !== null) {
329
- // For better stability, apply a small smoothing factor
330
- const smoothing = 0.1;
331
- state.deviceOrientation.alpha = state.deviceOrientation.alpha * (1 - smoothing) + event.alpha * smoothing;
332
- state.deviceOrientation.beta = state.deviceOrientation.beta * (1 - smoothing) + event.beta * smoothing;
333
- state.deviceOrientation.gamma = state.deviceOrientation.gamma * (1 - smoothing) + event.gamma * smoothing;
 
 
 
 
 
 
 
334
  }
335
  }
336
 
337
  function initMap() {
338
- state.map = L.map('map').setView([0, 0], 2);
339
  L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
340
- attribution: '© OpenStreetMap contributors'
341
  }).addTo(state.map);
 
 
 
 
 
 
 
342
  state.userMarker = L.marker([0, 0]).addTo(state.map).bindPopup('Ваше местоположение');
343
 
344
  document.getElementById('toggle-map-button').addEventListener('click', toggleMap);
@@ -347,12 +356,13 @@ def index():
347
  function toggleMap() {
348
  const mapContainer = document.getElementById('map-container');
349
  mapContainer.classList.toggle('fullscreen');
 
350
  setTimeout(() => {
351
  state.map.invalidateSize();
352
  if (state.currentUserPosition) {
353
  state.map.setView([state.currentUserPosition.lat, state.currentUserPosition.lon], state.map.getZoom());
354
  }
355
- }, 300);
356
  }
357
 
358
  async function loadHotspots() {
@@ -367,16 +377,6 @@ def index():
367
  }
368
  }
369
 
370
- function getCreatorDisplayName(hotspot) {
371
- if (hotspot.creator_info) {
372
- const info = hotspot.creator_info;
373
- return info.username ? `@${info.username}` :
374
- info.first_name ? `${info.first_name}${info.last_name ? ' ' + info.last_name : ''}` :
375
- 'Unknown User';
376
- }
377
- return 'Unknown';
378
- }
379
-
380
  function renderHotspots() {
381
  const container = document.getElementById('ar-container');
382
  container.innerHTML = '';
@@ -384,7 +384,7 @@ def index():
384
  const el = document.createElement('div');
385
  el.className = 'hotspot';
386
  el.id = `hotspot-${index}`;
387
- el.innerHTML = `${hotspot.text}<br><small>by ${getCreatorDisplayName(hotspot)}</small>`;
388
  container.appendChild(el);
389
  });
390
  }
@@ -398,57 +398,67 @@ def index():
398
  state.hotspots.forEach(hotspot => {
399
  const marker = L.marker([hotspot.lat, hotspot.lon])
400
  .addTo(state.map)
401
- .bindPopup(`${hotspot.text}<br>by ${getCreatorDisplayName(hotspot)}`);
402
  state.hotspotMarkers.push(marker);
403
  });
404
  }
405
 
406
  function setupAddHotspotListener() {
407
  const container = document.getElementById('ar-container');
408
- container.addEventListener('click', async (event) => {
 
 
409
  if (!state.currentUserPosition) {
410
- alert('GPS-координаты еще не определены. Подождите немного.');
411
- return;
412
- }
413
- if (!Telegram.WebApp.initDataUnsafe.user) {
414
- alert('Не удалось получить данные пользователя Telegram. Попробуйте перезапустить приложение.');
415
  return;
416
  }
417
 
418
- const text = prompt('Введите текст для нового хотспота:');
419
- if (text) {
420
- const newHotspot = {
421
- text: text,
422
- lat: state.currentUserPosition.lat,
423
- lon: state.currentUserPosition.lon,
424
- creator_info: Telegram.WebApp.initDataUnsafe.user // Store full user object
425
- };
426
-
427
- try {
428
- const response = await fetch('/hotspots', {
429
- method: 'POST',
430
- headers: { 'Content-Type': 'application/json' },
431
- body: JSON.stringify(newHotspot)
432
- });
433
- if(response.ok) {
434
- const savedHotspot = await response.json();
435
- state.hotspots.push(savedHotspot.hotspot);
436
- renderHotspots();
437
- renderHotspotsOnMap();
438
- } else {
439
- alert('Не удалось сохранить хотспот.');
 
 
 
 
 
 
 
 
 
 
 
 
 
440
  }
441
- } catch (error) {
442
- console.error('Ошибка сохранения хотспота:', error);
443
- alert('Ошибка сети при сохранении хотспота.');
444
  }
445
- }
 
 
446
  });
447
  }
448
 
449
  function haversineDistance(coords1, coords2) {
450
  function toRad(x) { return x * Math.PI / 180; }
451
- const R = 6371; // Earth's radius in kilometers
452
  const dLat = toRad(coords2.lat - coords1.lat);
453
  const dLon = toRad(coords2.lon - coords1.lon);
454
  const lat1 = toRad(coords1.lat);
@@ -456,7 +466,7 @@ def index():
456
  const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
457
  Math.sin(dLon / 2) * Math.sin(dLon / 2) * Math.cos(lat1) * Math.cos(lat2);
458
  const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
459
- return R * c * 1000; // Distance in meters
460
  }
461
 
462
  function calculateBearing(start, end) {
@@ -473,14 +483,15 @@ def index():
473
  }
474
 
475
  function update() {
476
- if (!state.currentUserPosition || !state.isOrientationReady) {
477
  requestAnimationFrame(update);
478
  return;
479
  }
480
 
481
  const screenWidth = window.innerWidth;
482
  const screenHeight = window.innerHeight;
483
-
 
484
  state.hotspots.forEach((hotspot, index) => {
485
  const el = document.getElementById(`hotspot-${index}`);
486
  if (!el) return;
@@ -493,30 +504,29 @@ def index():
493
  }
494
 
495
  const bearing = calculateBearing(state.currentUserPosition, hotspot);
496
- let angleDiff = bearing - state.deviceOrientation.alpha;
497
 
498
  if (angleDiff > 180) angleDiff -= 360;
499
  if (angleDiff < -180) angleDiff += 360;
500
 
501
- // Calculate horizontal position based on FOV
502
- // We need to map an angle difference (e.g., -30 to +30 for a 60 FOV) to screen pixels (0 to screenWidth)
503
- // (angleDiff / (cameraFov / 2)) gives a ratio from -1 to 1 for visible objects
504
- // screenWidth / 2 * (1 + ratio) gives position from 0 to screenWidth
505
- const x = screenWidth / 2 + (angleDiff / (state.cameraFov / 2)) * (screenWidth / 2);
506
-
507
- // Simple vertical positioning (can be improved with beta/gamma for tilt)
508
- const y = screenHeight / 2; // Center vertically for simplicity
509
-
510
- // Apply perspective scaling based on distance
511
- const scale = Math.max(0.3, 1 - distance / MAX_VISIBLE_DISTANCE); // Min scale 0.3
512
-
513
- // Apply inverse distance for z-index (closer elements appear above)
514
- el.style.zIndex = Math.round(1000 - distance);
515
-
516
- el.style.left = `${x}px`;
517
- el.style.top = `${y}px`;
518
- el.style.transform = `translate(-50%, -50%) scale(${scale})`;
519
- el.classList.remove('hidden');
520
  });
521
 
522
  requestAnimationFrame(update);
@@ -537,25 +547,34 @@ def handle_hotspots():
537
  return jsonify({"error": "Missing JSON in request"}), 400
538
 
539
  data = request.get_json()
 
 
 
 
 
 
 
 
 
540
  text = data.get('text')
541
  lat = data.get('lat')
542
  lon = data.get('lon')
543
- creator_info = data.get('creator_info') # This will be the Telegram user object
544
 
545
- if not all([text, lat, lon, creator_info]):
546
- return jsonify({"error": "Missing data: text, lat, lon, or creator_info"}), 400
547
 
548
  try:
549
  new_hotspot = {
550
  "text": str(text),
551
  "lat": float(lat),
552
  "lon": float(lon),
553
- "creator_info": creator_info # Store the full object as received
 
554
  }
555
  save_hotspot(new_hotspot)
556
  return jsonify({"success": True, "hotspot": new_hotspot}), 201
557
- except (ValueError, TypeError) as e:
558
- return jsonify({"error": f"Invalid data types: {e}"}), 400
559
 
560
  if __name__ == '__main__':
561
  app.run(host='0.0.0.0', port=7860, debug=False)
 
1
+ from flask import Flask, Response, request, jsonify
2
  import json
3
  import os
4
+ import hmac
5
+ import hashlib
6
+ from urllib.parse import unquote
7
 
8
  app = Flask(__name__)
9
 
10
  HOTSPOTS_FILE = 'hotspots.json'
11
+ BOT_TOKEN = "6909363967:AAGl58czIt7Vra_V8wWR7MyXkN_ayS27Soo"
12
+
13
+ def validate_init_data(init_data_str, bot_token):
14
+ try:
15
+ data_params = sorted([
16
+ (k, v) for k, v in
17
+ [p.split('=', 1) for p in init_data_str.split('&')]
18
+ if k != 'hash'
19
+ ], key=lambda x: x[0])
20
+
21
+ data_check_string = "\n".join([f"{k}={v}" for k, v in data_params])
22
+
23
+ secret_key = hmac.new("WebAppData".encode(), bot_token.encode(), hashlib.sha256).digest()
24
+ calculated_hash = hmac.new(secret_key, data_check_string.encode(), hashlib.sha256).hexdigest()
25
+
26
+ received_hash = dict(p.split('=', 1) for p in init_data_str.split('&')).get('hash')
27
+
28
+ if calculated_hash == received_hash:
29
+ user_data_encoded = dict(p.split('=', 1) for p in init_data_str.split('&')).get('user')
30
+ if user_data_encoded:
31
+ return json.loads(unquote(user_data_encoded))
32
+ return None
33
+ except Exception:
34
+ return None
35
 
36
  def get_all_hotspots():
37
  if not os.path.exists(HOTSPOTS_FILE):
 
61
  <link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css" />
62
  <script src="https://unpkg.com/[email protected]/dist/leaflet.js"></script>
63
  <style>
64
+ :root {
65
+ --tg-bg-color: var(--tg-theme-bg-color, #000000);
66
+ --tg-text-color: var(--tg-theme-text-color, #ffffff);
67
+ --tg-hint-color: var(--tg-theme-hint-color, #aaaaaa);
68
+ --tg-button-color: var(--tg-theme-button-color, #007aff);
69
+ --tg-button-text-color: var(--tg-theme-button-text-color, #ffffff);
70
+ }
71
  body, html {
72
  margin: 0;
73
  padding: 0;
74
  width: 100%;
75
  height: 100%;
76
  overflow: hidden;
77
+ background-color: var(--tg-bg-color);
78
+ color: var(--tg-text-color);
79
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
80
+ }
81
+ #app-container {
82
+ position: fixed;
83
+ top: 0;
84
+ left: 0;
85
+ width: 100%;
86
+ height: 100%;
87
+ display: flex;
88
+ flex-direction: column;
89
+ align-items: center;
90
+ justify-content: center;
91
+ }
92
+ #error-wall {
93
+ text-align: center;
94
+ padding: 20px;
95
  }
96
  #ar-container {
97
  position: absolute;
 
112
  }
113
  .hotspot {
114
  position: absolute;
115
+ background-color: var(--tg-button-color);
116
+ color: var(--tg-button-text-color);
117
  padding: 10px 15px;
118
+ border-radius: 12px;
 
119
  box-shadow: 0 4px 15px rgba(0, 0, 0, 0.5);
120
  transform: translate(-50%, -50%);
121
  transition: opacity 0.3s, transform 0.1s linear;
 
124
  will-change: transform, left, top, opacity;
125
  z-index: 10;
126
  text-align: center;
127
+ border: 1px solid rgba(255, 255, 255, 0.2);
128
  }
129
  .hotspot small {
130
  font-size: 0.7em;
 
138
  }
139
  #map-container {
140
  position: fixed;
141
+ bottom: 20px;
142
+ left: 50%;
143
+ transform: translateX(-50%);
144
+ width: 90%;
145
+ max-width: 400px;
146
+ height: 150px;
147
+ background-color: rgba(0, 0, 0, 0.7);
148
+ border: 1px solid var(--tg-hint-color);
149
+ border-radius: 15px;
150
  overflow: hidden;
151
+ transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
152
  z-index: 50;
153
  display: flex;
154
  flex-direction: column;
155
+ box-shadow: 0 5px 20px rgba(0,0,0,0.4);
156
  }
157
  #map-container.fullscreen {
158
  width: 100%;
159
  height: 100%;
160
  top: 0;
161
  left: 0;
162
+ bottom: 0;
163
  border-radius: 0;
164
+ transform: none;
165
+ max-width: none;
166
  justify-content: center;
167
  align-items: center;
168
+ background-color: var(--tg-bg-color);
169
  }
170
  #map {
171
  flex-grow: 1;
172
  width: 100%;
173
+ height: 100%;
174
+ border-radius: 15px;
175
  overflow: hidden;
176
  }
177
  #map-container.fullscreen #map {
 
178
  border-radius: 0;
179
  }
180
  #toggle-map-button {
181
+ position: absolute;
182
+ top: 8px;
183
+ right: 8px;
184
+ background-color: rgba(0, 0, 0, 0.6);
185
+ color: white;
186
  border: none;
187
+ padding: 6px 12px;
188
+ border-radius: 20px;
 
189
  cursor: pointer;
190
+ font-size: 14px;
191
+ z-index: 51;
192
  }
193
+ .leaflet-popup-content-wrapper, .leaflet-popup-tip {
194
+ background-color: var(--tg-bg-color);
195
+ color: var(--tg-text-color);
196
+ box-shadow: 0 3px 14px rgba(0,0,0,0.4);
 
 
197
  }
198
+ .leaflet-container a.leaflet-popup-close-button {
199
+ color: var(--tg-text-color);
 
 
 
 
 
 
 
 
 
 
 
 
 
200
  }
201
  </style>
202
  </head>
203
  <body>
204
+ <div id="app-container" style="display: none;">
205
+ <video id="camera-view" playsinline autoplay muted></video>
206
+ <div id="ar-container"></div>
207
+ <div id="map-container">
208
+ <div id="map"></div>
209
+ <button id="toggle-map-button">Карта</button>
210
+ </div>
211
  </div>
212
 
213
+ <div id="error-wall">
214
+ <h1>Загрузка...</h1>
215
+ <p>Это приложение должно быть открыто в Telegram.</p>
 
 
 
216
  </div>
217
 
218
  <script>
219
+ const tg = window.Telegram.WebApp;
220
+
221
  const state = {
222
  hotspots: [],
223
  currentUserPosition: null,
224
+ deviceOrientation: { alpha: 0, beta: 0, gamma: 0, smoothedAlpha: 0 },
225
+ cameraFov: 60,
226
  map: null,
227
  userMarker: null,
228
  hotspotMarkers: [],
229
  initialMapSet: false,
230
+ telegramUser: null
 
 
231
  };
232
 
233
+ const MAX_VISIBLE_DISTANCE = 10000; // 10 km
234
+ const ORIENTATION_SMOOTHING = 0.1;
 
 
 
 
 
235
 
236
+ function initArApp() {
237
+ document.getElementById('error-wall').style.display = 'none';
238
+ document.getElementById('app-container').style.display = 'block';
239
+ setupCamera();
 
 
 
 
 
240
  setupGPS();
241
  setupOrientationListener();
242
  initMap();
243
+ loadHotspots();
244
  setupAddHotspotListener();
245
  requestAnimationFrame(update);
246
  }
247
 
248
+ tg.onEvent('themeChanged', function() {
249
+ document.documentElement.className = tg.colorScheme;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
250
  });
251
+
252
+ tg.ready();
253
+ tg.expand();
254
+
255
+ if (tg.initDataUnsafe && tg.initDataUnsafe.user) {
256
+ state.telegramUser = tg.initDataUnsafe.user;
257
+ initArApp();
258
+ } else {
259
+ tg.close();
260
+ }
261
 
262
  async function setupCamera() {
263
  const video = document.getElementById('camera-view');
 
268
  });
269
  video.srcObject = stream;
270
  await video.play();
 
 
271
  } catch (err) {
272
+ tg.showAlert('Не удалось получить доступ к камере: ' + err.message);
 
273
  }
 
 
274
  }
275
  }
276
 
 
278
  if (navigator.geolocation) {
279
  navigator.geolocation.watchPosition(
280
  (position) => {
281
+ state.currentUserPosition = {
282
+ lat: position.coords.latitude,
283
+ lon: position.coords.longitude
284
+ };
285
+ if (state.userMarker) {
286
+ state.userMarker.setLatLng([state.currentUserPosition.lat, state.currentUserPosition.lon]);
287
+ if (!state.initialMapSet) {
288
+ state.map.setView([state.currentUserPosition.lat, state.currentUserPosition.lon], 16);
289
+ state.initialMapSet = true;
 
 
 
 
 
 
 
 
 
 
290
  }
291
  }
 
 
292
  },
293
  (error) => {
294
+ tg.showAlert('Не удалось получить доступ к GPS: ' + error.message);
 
295
  },
296
+ { enableHighAccuracy: true, maximumAge: 1000, timeout: 5000 }
297
  );
298
  } else {
299
+ tg.showAlert('GPS не поддерживается вашим устройством.');
300
  }
301
  }
302
 
303
  function setupOrientationListener() {
304
+ const isIOS = typeof DeviceOrientationEvent.requestPermission === 'function';
305
+
306
+ const startOrientation = () => {
307
+ if (window.DeviceOrientationEvent) {
308
+ window.addEventListener('deviceorientation', (event) => {
309
+ if (event.alpha !== null) {
310
+ let alpha = event.webkitCompassHeading || event.alpha;
311
+ state.deviceOrientation.smoothedAlpha = state.deviceOrientation.smoothedAlpha * (1 - ORIENTATION_SMOOTHING) + alpha * ORIENTATION_SMOOTHING;
312
+ state.deviceOrientation.alpha = alpha;
313
+ state.deviceOrientation.beta = event.beta;
314
+ state.deviceOrientation.gamma = event.gamma;
315
+ }
316
+ }, true);
 
317
  } else {
318
+ tg.showAlert('Отслеживание ориентации устройства не поддерживается.');
 
 
 
319
  }
320
+ };
 
 
 
321
 
322
+ if (isIOS) {
323
+ document.body.addEventListener('click', () => {
324
+ DeviceOrientationEvent.requestPermission()
325
+ .then(permissionState => {
326
+ if (permissionState === 'granted') {
327
+ startOrientation();
328
+ } else {
329
+ tg.showAlert('Доступ к ориентации устройства отклонен.');
330
+ }
331
+ })
332
+ .catch(console.error);
333
+ }, { once: true });
334
+ } else {
335
+ startOrientation();
336
  }
337
  }
338
 
339
  function initMap() {
340
+ state.map = L.map('map', {zoomControl: false}).setView([0, 0], 2);
341
  L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
342
+ attribution: '© OpenStreetMap'
343
  }).addTo(state.map);
344
+
345
+ const userIcon = L.divIcon({
346
+ html: '<div></div>',
347
+ className: 'user-marker',
348
+ iconSize: [20, 20]
349
+ });
350
+
351
  state.userMarker = L.marker([0, 0]).addTo(state.map).bindPopup('Ваше местоположение');
352
 
353
  document.getElementById('toggle-map-button').addEventListener('click', toggleMap);
 
356
  function toggleMap() {
357
  const mapContainer = document.getElementById('map-container');
358
  mapContainer.classList.toggle('fullscreen');
359
+ tg.HapticFeedback.impactOccurred('light');
360
  setTimeout(() => {
361
  state.map.invalidateSize();
362
  if (state.currentUserPosition) {
363
  state.map.setView([state.currentUserPosition.lat, state.currentUserPosition.lon], state.map.getZoom());
364
  }
365
+ }, 400);
366
  }
367
 
368
  async function loadHotspots() {
 
377
  }
378
  }
379
 
 
 
 
 
 
 
 
 
 
 
380
  function renderHotspots() {
381
  const container = document.getElementById('ar-container');
382
  container.innerHTML = '';
 
384
  const el = document.createElement('div');
385
  el.className = 'hotspot';
386
  el.id = `hotspot-${index}`;
387
+ el.innerHTML = `${hotspot.text}<br><small>@${hotspot.creator_username || 'anonymous'}</small>`;
388
  container.appendChild(el);
389
  });
390
  }
 
398
  state.hotspots.forEach(hotspot => {
399
  const marker = L.marker([hotspot.lat, hotspot.lon])
400
  .addTo(state.map)
401
+ .bindPopup(`${hotspot.text}<br>@${hotspot.creator_username || 'anonymous'}`);
402
  state.hotspotMarkers.push(marker);
403
  });
404
  }
405
 
406
  function setupAddHotspotListener() {
407
  const container = document.getElementById('ar-container');
408
+ container.addEventListener('click', (event) => {
409
+ if (event.target !== container) return;
410
+
411
  if (!state.currentUserPosition) {
412
+ tg.showAlert('GPS-координаты еще не определены. Подождите немного.');
 
 
 
 
413
  return;
414
  }
415
 
416
+ tg.showPopup({
417
+ title: 'Новый хотспот',
418
+ message: 'Введите текст для новой метки в AR.',
419
+ buttons: [
420
+ {id: 'save', type: 'default', text: 'Сохранить'},
421
+ {type: 'cancel'},
422
+ ]
423
+ }, async (buttonId, text) => {
424
+ if (buttonId === 'save' && text) {
425
+ const newHotspotData = {
426
+ text: text,
427
+ lat: state.currentUserPosition.lat,
428
+ lon: state.currentUserPosition.lon,
429
+ _auth: tg.initData
430
+ };
431
+
432
+ try {
433
+ const response = await fetch('/hotspots', {
434
+ method: 'POST',
435
+ headers: { 'Content-Type': 'application/json' },
436
+ body: JSON.stringify(newHotspotData)
437
+ });
438
+ if(response.ok) {
439
+ const savedHotspot = await response.json();
440
+ state.hotspots.push(savedHotspot.hotspot);
441
+ renderHotspots();
442
+ renderHotspotsOnMap();
443
+ tg.HapticFeedback.notificationOccurred('success');
444
+ } else {
445
+ const errorData = await response.json();
446
+ tg.showAlert('Не удалось сохранить хотспот: ' + (errorData.error || 'Server error'));
447
+ }
448
+ } catch (error) {
449
+ console.error('Ошибка сохранения хотспота:', error);
450
+ tg.showAlert('Ошибка сети при сохранении хотспота.');
451
  }
 
 
 
452
  }
453
+ });
454
+ const popupInput = document.querySelector('.telegram-popup-input');
455
+ if (popupInput) popupInput.placeholder = 'Ваше сообщение...';
456
  });
457
  }
458
 
459
  function haversineDistance(coords1, coords2) {
460
  function toRad(x) { return x * Math.PI / 180; }
461
+ const R = 6371e3;
462
  const dLat = toRad(coords2.lat - coords1.lat);
463
  const dLon = toRad(coords2.lon - coords1.lon);
464
  const lat1 = toRad(coords1.lat);
 
466
  const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
467
  Math.sin(dLon / 2) * Math.sin(dLon / 2) * Math.cos(lat1) * Math.cos(lat2);
468
  const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
469
+ return R * c;
470
  }
471
 
472
  function calculateBearing(start, end) {
 
483
  }
484
 
485
  function update() {
486
+ if (!state.currentUserPosition) {
487
  requestAnimationFrame(update);
488
  return;
489
  }
490
 
491
  const screenWidth = window.innerWidth;
492
  const screenHeight = window.innerHeight;
493
+ const halfFovRad = (state.cameraFov / 2) * (Math.PI / 180);
494
+
495
  state.hotspots.forEach((hotspot, index) => {
496
  const el = document.getElementById(`hotspot-${index}`);
497
  if (!el) return;
 
504
  }
505
 
506
  const bearing = calculateBearing(state.currentUserPosition, hotspot);
507
+ let angleDiff = bearing - state.deviceOrientation.smoothedAlpha;
508
 
509
  if (angleDiff > 180) angleDiff -= 360;
510
  if (angleDiff < -180) angleDiff += 360;
511
 
512
+ if (Math.abs(angleDiff) > state.cameraFov / 2) {
513
+ el.classList.add('hidden');
514
+ } else {
515
+ el.classList.remove('hidden');
516
+
517
+ const x = screenWidth / 2 + (angleDiff / (state.cameraFov / 2)) * (screenWidth / 2);
518
+
519
+ const pitch = state.deviceOrientation.beta || 90;
520
+ const pitchRad = (pitch - 90) * (Math.PI / 180);
521
+ const y = screenHeight / 2 - Math.tan(pitchRad) * (screenHeight / 2);
522
+
523
+ const scale = Math.max(0.4, 1.5 - (distance / 200));
524
+
525
+ el.style.left = `${x}px`;
526
+ el.style.top = `${y}px`;
527
+ el.style.transform = `translate(-50%, -50%) scale(${scale})`;
528
+ el.style.zIndex = Math.round(10000 - distance);
529
+ }
 
530
  });
531
 
532
  requestAnimationFrame(update);
 
547
  return jsonify({"error": "Missing JSON in request"}), 400
548
 
549
  data = request.get_json()
550
+ init_data_str = data.get('_auth')
551
+
552
+ if not init_data_str:
553
+ return jsonify({"error": "Authentication data missing"}), 401
554
+
555
+ user_data = validate_init_data(init_data_str, BOT_TOKEN)
556
+ if not user_data:
557
+ return jsonify({"error": "Authentication failed"}), 403
558
+
559
  text = data.get('text')
560
  lat = data.get('lat')
561
  lon = data.get('lon')
 
562
 
563
+ if not all([text, lat, lon]):
564
+ return jsonify({"error": "Missing data: text, lat, or lon"}), 400
565
 
566
  try:
567
  new_hotspot = {
568
  "text": str(text),
569
  "lat": float(lat),
570
  "lon": float(lon),
571
+ "creator_id": user_data.get('id'),
572
+ "creator_username": user_data.get('username', 'anonymous')
573
  }
574
  save_hotspot(new_hotspot)
575
  return jsonify({"success": True, "hotspot": new_hotspot}), 201
576
+ except (ValueError, TypeError):
577
+ return jsonify({"error": "Invalid data types"}), 400
578
 
579
  if __name__ == '__main__':
580
  app.run(host='0.0.0.0', port=7860, debug=False)