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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +210 -242
app.py CHANGED
@@ -1,14 +1,10 @@
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
- # ВАЖНО: В реальном проекте храните токен в переменных окружения, а не в коде.
11
- BOT_TOKEN = "6909363967:AAGl58czIt7Vra_V8wWR7MyXkN_ayS27Soo"
12
  HOTSPOTS_FILE = 'hotspots.json'
13
 
14
  def get_all_hotspots():
@@ -39,32 +35,16 @@ def index():
39
  <link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css" />
40
  <script src="https://unpkg.com/[email protected]/dist/leaflet.js"></script>
41
  <style>
42
- :root {
43
- --tg-theme-bg-color: #000000;
44
- --tg-theme-text-color: #ffffff;
45
- --tg-theme-button-color: #007aff;
46
- --tg-theme-button-text-color: #ffffff;
47
- }
48
  body, html {
49
  margin: 0;
50
  padding: 0;
51
  width: 100%;
52
  height: 100%;
53
  overflow: hidden;
54
- background-color: var(--tg-theme-bg-color);
55
- color: var(--tg-theme-text-color);
56
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
57
- }
58
- #info-bar {
59
- position: fixed;
60
- top: 10px;
61
- left: 10px;
62
- background-color: rgba(0, 0, 0, 0.7);
63
- color: white;
64
- padding: 5px 10px;
65
- border-radius: 8px;
66
- z-index: 51;
67
- font-size: 14px;
68
  }
69
  #ar-container {
70
  position: absolute;
@@ -85,11 +65,11 @@ def index():
85
  }
86
  .hotspot {
87
  position: absolute;
88
- background-color: rgba(0, 122, 255, 0.8);
89
- color: white;
90
  padding: 10px 15px;
91
  border-radius: 10px;
92
- border: 1px solid rgba(255, 255, 255, 0.5);
93
  box-shadow: 0 4px 15px rgba(0, 0, 0, 0.5);
94
  transform: translate(-50%, -50%);
95
  transition: opacity 0.3s, transform 0.1s linear;
@@ -116,7 +96,7 @@ def index():
116
  left: 10px;
117
  width: 200px;
118
  height: 200px;
119
- background-color: rgba(0, 0, 0, 0.7);
120
  border-radius: 10px;
121
  overflow: hidden;
122
  transition: all 0.3s ease;
@@ -145,8 +125,8 @@ def index():
145
  border-radius: 0;
146
  }
147
  #toggle-map-button {
148
- background-color: rgba(0, 0, 0, 0.5);
149
- color: white;
150
  border: none;
151
  padding: 5px 10px;
152
  margin: 5px;
@@ -162,54 +142,69 @@ def index():
162
  font-size: 16px;
163
  margin: 0;
164
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
165
  </style>
166
  </head>
167
  <body>
168
- <div id="info-bar">Авторизация...</div>
 
 
 
 
169
  <video id="camera-view" playsinline autoplay muted></video>
170
  <div id="ar-container"></div>
 
171
  <div id="map-container">
172
  <div id="map"></div>
173
  <button id="toggle-map-button">Minimap</button>
174
  </div>
175
 
176
  <script>
177
- const tg = window.Telegram.WebApp;
178
-
179
  const state = {
180
  hotspots: [],
181
- currentUser: null,
182
  currentUserPosition: null,
183
- smoothedPosition: null,
184
  deviceOrientation: { alpha: 0, beta: 0, gamma: 0 },
185
- smoothedOrientation: { alpha: 0, beta: 0, gamma: 0 },
186
- cameraFov: 60,
187
  map: null,
188
  userMarker: null,
189
  hotspotMarkers: [],
190
- initialMapSet: false
 
 
 
191
  };
192
 
193
- const SMOOTHING_FACTOR_GPS = 0.05;
194
- const SMOOTHING_FACTOR_ORIENTATION = 0.1;
195
- const MAX_VISIBLE_DISTANCE = 1000;
196
 
197
- async function verifyTelegramData(initData) {
198
- try {
199
- const response = await fetch('/verify_telegram_data', {
200
- method: 'POST',
201
- headers: { 'Content-Type': 'application/json' },
202
- body: JSON.stringify({ initData: initData })
203
- });
204
- const data = await response.json();
205
- return data.ok;
206
- } catch (error) {
207
- console.error('Verification failed:', error);
208
- return false;
209
- }
210
  }
211
-
 
 
 
 
212
  async function initArApp() {
 
 
 
213
  await setupCamera();
214
  setupGPS();
215
  setupOrientationListener();
@@ -219,28 +214,29 @@ def index():
219
  requestAnimationFrame(update);
220
  }
221
 
222
- tg.onEvent('viewportChanged', () => tg.expand());
223
-
224
- window.addEventListener('load', async () => {
225
- tg.ready();
226
- tg.expand();
227
-
228
- const initData = tg.initData;
229
- const isVerified = await verifyTelegramData(initData);
230
-
231
- if (!isVerified && !tg.initDataUnsafe.query_id) {
232
- document.body.innerHTML = '<h1>Ошибка авторизации. Пожалуйста, откройте это приложение через Telegram.</h1>';
233
- return;
234
  }
 
235
 
236
- state.currentUser = tg.initDataUnsafe.user;
237
- if(state.currentUser) {
238
- document.getElementById('info-bar').innerText = `Привет, ${state.currentUser.first_name}!`;
 
 
 
 
239
  } else {
240
- document.getElementById('info-bar').innerText = 'Гость';
241
  }
242
-
243
- initArApp();
244
  });
245
 
246
  async function setupCamera() {
@@ -252,9 +248,14 @@ def index():
252
  });
253
  video.srcObject = stream;
254
  await video.play();
 
 
255
  } catch (err) {
256
- alert('Не удалось получить доступ к камере: ' + err);
 
257
  }
 
 
258
  }
259
  }
260
 
@@ -262,30 +263,35 @@ def index():
262
  if (navigator.geolocation) {
263
  navigator.geolocation.watchPosition(
264
  (position) => {
265
- state.currentUserPosition = {
266
- lat: position.coords.latitude,
267
- lon: position.coords.longitude
268
- };
269
-
270
- if (!state.smoothedPosition) {
271
- state.smoothedPosition = { ...state.currentUserPosition };
272
- } else {
273
- state.smoothedPosition.lat = (state.currentUserPosition.lat * SMOOTHING_FACTOR_GPS) + (state.smoothedPosition.lat * (1 - SMOOTHING_FACTOR_GPS));
274
- state.smoothedPosition.lon = (state.currentUserPosition.lon * SMOOTHING_FACTOR_GPS) + (state.smoothedPosition.lon * (1 - SMOOTHING_FACTOR_GPS));
275
- }
276
-
277
- if (state.userMarker && state.smoothedPosition) {
278
- state.userMarker.setLatLng([state.smoothedPosition.lat, state.smoothedPosition.lon]);
279
- if (!state.initialMapSet) {
280
- state.map.setView([state.smoothedPosition.lat, state.smoothedPosition.lon], 16);
281
- state.initialMapSet = true;
 
 
282
  }
283
  }
 
 
284
  },
285
  (error) => {
286
- alert('Не удалось получить доступ к GPS: ' + error.message);
 
287
  },
288
- { enableHighAccuracy: true, maximumAge: 0, timeout: 5000 }
289
  );
290
  } else {
291
  alert('GPS не поддерживается вашим браузером.');
@@ -293,44 +299,48 @@ def index():
293
  }
294
 
295
  function setupOrientationListener() {
296
- const handler = (event) => {
297
- if (event.alpha !== null) {
298
- state.deviceOrientation.alpha = event.alpha;
299
- state.deviceOrientation.beta = event.beta;
300
- state.deviceOrientation.gamma = event.gamma;
301
-
302
- let diff = state.deviceOrientation.alpha - state.smoothedOrientation.alpha;
303
- if (diff > 180) diff -= 360;
304
- if (diff < -180) diff += 360;
305
-
306
- state.smoothedOrientation.alpha = (state.smoothedOrientation.alpha + diff * SMOOTHING_FACTOR_ORIENTATION + 360) % 360;
307
- state.smoothedOrientation.beta = (state.deviceOrientation.beta * SMOOTHING_FACTOR_ORIENTATION) + (state.smoothedOrientation.beta * (1 - SMOOTHING_FACTOR_ORIENTATION));
308
- state.smoothedOrientation.gamma = (state.deviceOrientation.gamma * SMOOTHING_FACTOR_ORIENTATION) + (state.smoothedOrientation.gamma * (1 - SMOOTHING_FACTOR_ORIENTATION));
 
 
 
 
 
 
309
  }
310
- };
311
- if (typeof DeviceOrientationEvent.requestPermission === 'function') {
312
- DeviceOrientationEvent.requestPermission()
313
- .then(permissionState => {
314
- if (permissionState === 'granted') {
315
- window.addEventListener('deviceorientation', handler, true);
316
- } else {
317
- alert('Доступ к ориентации устройства не предоставлен.');
318
- }
319
- })
320
- .catch(console.error);
321
- } else if (window.DeviceOrientationEvent) {
322
- window.addEventListener('deviceorientation', handler, true);
323
  } else {
324
  alert('Отслеживание ориентации устройства не поддерживается.');
325
  }
326
  }
327
 
 
 
 
 
 
 
 
 
 
 
328
  function initMap() {
329
  state.map = L.map('map').setView([0, 0], 2);
330
  L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
331
- attribution: '© OpenStreetMap'
332
  }).addTo(state.map);
333
- state.userMarker = L.marker([0, 0]).addTo(state.map).bindPopup('Вы здесь');
 
334
  document.getElementById('toggle-map-button').addEventListener('click', toggleMap);
335
  }
336
 
@@ -339,8 +349,8 @@ def index():
339
  mapContainer.classList.toggle('fullscreen');
340
  setTimeout(() => {
341
  state.map.invalidateSize();
342
- if (state.smoothedPosition) {
343
- state.map.setView([state.smoothedPosition.lat, state.smoothedPosition.lon], state.map.getZoom());
344
  }
345
  }, 300);
346
  }
@@ -357,6 +367,16 @@ def index():
357
  }
358
  }
359
 
 
 
 
 
 
 
 
 
 
 
360
  function renderHotspots() {
361
  const container = document.getElementById('ar-container');
362
  container.innerHTML = '';
@@ -364,19 +384,21 @@ def index():
364
  const el = document.createElement('div');
365
  el.className = 'hotspot';
366
  el.id = `hotspot-${index}`;
367
- el.innerHTML = `${hotspot.text}<br><small>by ${hotspot.creator_username || 'Unknown'}</small>`;
368
  container.appendChild(el);
369
  });
370
  }
371
 
372
  function renderHotspotsOnMap() {
373
  if (!state.map) return;
 
374
  state.hotspotMarkers.forEach(marker => state.map.removeLayer(marker));
375
  state.hotspotMarkers = [];
 
376
  state.hotspots.forEach(hotspot => {
377
  const marker = L.marker([hotspot.lat, hotspot.lon])
378
  .addTo(state.map)
379
- .bindPopup(`${hotspot.text}<br>by ${hotspot.creator_username || 'Unknown'}`);
380
  state.hotspotMarkers.push(marker);
381
  });
382
  }
@@ -384,65 +406,49 @@ def index():
384
  function setupAddHotspotListener() {
385
  const container = document.getElementById('ar-container');
386
  container.addEventListener('click', async (event) => {
387
- if (event.target.classList.contains('hotspot')) return;
388
-
389
- if (!state.smoothedPosition) {
390
- tg.showAlert('GPS-координаты еще не определены. Подождите.');
391
  return;
392
  }
393
- if (!state.currentUser) {
394
- tg.showAlert('Не удалось определить пользователя Telegram.');
395
  return;
396
  }
397
 
398
- tg.showPopup({
399
- title: 'Новый хотспот',
400
- message: 'Введите текст для нового хотспота:',
401
- buttons: [
402
- {id: 'save', type: 'default', text: 'Сохранить'},
403
- {type: 'cancel'},
404
- ]
405
- }, async (buttonId) => {
406
- if (buttonId === 'save') {
407
- const text = 'Новый хотспот'; // Placeholder, as popup doesn't have input
408
- tg.showScanQrPopup({ text: "Введите текст для хотспота:" }, async (promptText) => {
409
- if (promptText) {
410
- const newHotspot = {
411
- text: promptText,
412
- lat: state.currentUserPosition.lat,
413
- lon: state.currentUserPosition.lon,
414
- creator_id: state.currentUser.id,
415
- creator_username: state.currentUser.username || `${state.currentUser.first_name} ${state.currentUser.last_name || ''}`.trim()
416
- };
417
- try {
418
- const response = await fetch('/hotspots', {
419
- method: 'POST',
420
- headers: { 'Content-Type': 'application/json' },
421
- body: JSON.stringify(newHotspot)
422
- });
423
- if(response.ok) {
424
- const savedHotspot = await response.json();
425
- state.hotspots.push(savedHotspot.hotspot);
426
- renderHotspots();
427
- renderHotspotsOnMap();
428
- tg.closeScanQrPopup();
429
- } else {
430
- tg.showAlert('Не удалось сохранить хотспот.');
431
- }
432
- } catch (error) {
433
- tg.showAlert('Ошибка сети при сохранении хотспота.');
434
- }
435
- }
436
- return true;
437
  });
 
 
 
 
 
 
 
 
 
 
 
438
  }
439
- });
440
  });
441
  }
442
 
443
  function haversineDistance(coords1, coords2) {
444
  function toRad(x) { return x * Math.PI / 180; }
445
- const R = 6371e3;
446
  const dLat = toRad(coords2.lat - coords1.lat);
447
  const dLon = toRad(coords2.lon - coords1.lon);
448
  const lat1 = toRad(coords1.lat);
@@ -450,7 +456,7 @@ def index():
450
  const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
451
  Math.sin(dLon / 2) * Math.sin(dLon / 2) * Math.cos(lat1) * Math.cos(lat2);
452
  const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
453
- return R * c;
454
  }
455
 
456
  function calculateBearing(start, end) {
@@ -467,7 +473,7 @@ def index():
467
  }
468
 
469
  function update() {
470
- if (!state.smoothedPosition) {
471
  requestAnimationFrame(update);
472
  return;
473
  }
@@ -479,34 +485,38 @@ def index():
479
  const el = document.getElementById(`hotspot-${index}`);
480
  if (!el) return;
481
 
482
- const distance = haversineDistance(state.smoothedPosition, hotspot);
483
 
484
  if (distance > MAX_VISIBLE_DISTANCE) {
485
  el.classList.add('hidden');
486
  return;
487
  }
488
 
489
- const bearing = calculateBearing(state.smoothedPosition, hotspot);
490
- let angleDiff = bearing - state.smoothedOrientation.alpha;
491
 
492
  if (angleDiff > 180) angleDiff -= 360;
493
  if (angleDiff < -180) angleDiff += 360;
494
 
495
- if (Math.abs(angleDiff) > state.cameraFov / 2) {
496
- el.classList.add('hidden');
497
- } else {
498
- el.classList.remove('hidden');
499
-
500
- const x = screenWidth / 2 + (angleDiff / (state.cameraFov / 2)) * (screenWidth / 2);
501
- const y = screenHeight / 2;
502
-
503
- const scale = Math.max(0.3, 1 - (distance / MAX_VISIBLE_DISTANCE));
504
-
505
- el.style.left = `${x}px`;
506
- el.style.top = `${y}px`;
507
- el.style.transform = `translate(-50%, -50%) scale(${scale})`;
508
- el.style.zIndex = Math.round(10000 - distance);
509
- }
 
 
 
 
510
  });
511
 
512
  requestAnimationFrame(update);
@@ -517,44 +527,6 @@ def index():
517
  '''
518
  return Response(html_content, mimetype='text/html')
519
 
520
- @app.route('/verify_telegram_data', methods=['POST'])
521
- def verify_telegram_data():
522
- if not request.is_json:
523
- return jsonify({"ok": False, "error": "Request must be JSON"}), 400
524
-
525
- data = request.get_json()
526
- init_data_str = data.get('initData')
527
-
528
- if not init_data_str:
529
- return jsonify({"ok": False, "error": "initData not in request"}), 400
530
-
531
- try:
532
- unquoted_data = unquote(init_data_str)
533
-
534
- data_check_list = []
535
- hash_from_telegram = ''
536
-
537
- for pair in unquoted_data.split('&'):
538
- key, value = pair.split('=', 1)
539
- if key == 'hash':
540
- hash_from_telegram = value
541
- else:
542
- data_check_list.append(f"{key}={value}")
543
-
544
- data_check_list.sort()
545
- data_check_string = "\n".join(data_check_list)
546
-
547
- secret_key = hmac.new("WebAppData".encode(), BOT_TOKEN.encode(), hashlib.sha256).digest()
548
- calculated_hash = hmac.new(secret_key, data_check_string.encode(), hashlib.sha256).hexdigest()
549
-
550
- if calculated_hash == hash_from_telegram:
551
- return jsonify({"ok": True}), 200
552
- else:
553
- return jsonify({"ok": False, "error": "Hash validation failed"}), 403
554
-
555
- except Exception as e:
556
- return jsonify({"ok": False, "error": str(e)}), 500
557
-
558
  @app.route('/hotspots', methods=['GET', 'POST'])
559
  def handle_hotspots():
560
  if request.method == 'GET':
@@ -568,26 +540,22 @@ def handle_hotspots():
568
  text = data.get('text')
569
  lat = data.get('lat')
570
  lon = data.get('lon')
571
- creator_id = data.get('creator_id')
572
- creator_username = data.get('creator_username')
573
 
574
- if not all([text, lat, lon, creator_id]):
575
- return jsonify({"error": "Missing data: text, lat, lon, or creator_id"}), 400
576
 
577
  try:
578
  new_hotspot = {
579
  "text": str(text),
580
  "lat": float(lat),
581
  "lon": float(lon),
582
- "creator_id": int(creator_id),
583
- "creator_username": str(creator_username)
584
  }
585
  save_hotspot(new_hotspot)
586
  return jsonify({"success": True, "hotspot": new_hotspot}), 201
587
- except (ValueError, TypeError):
588
- return jsonify({"error": "Invalid data types"}), 400
589
 
590
  if __name__ == '__main__':
591
- # Для локального тестирования может потребоваться запуск с SSL-сертификатом
592
- # Для развертывания на хостинге с Nginx/Caddy это не нужно
593
  app.run(host='0.0.0.0', port=7860, debug=False)
 
 
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():
 
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
  }
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;
 
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;
 
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;
 
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();
 
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() {
 
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
  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 не поддерживается вашим браузером.');
 
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);
345
  }
346
 
 
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
  }
 
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
  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
  }
391
 
392
  function renderHotspotsOnMap() {
393
  if (!state.map) return;
394
+
395
  state.hotspotMarkers.forEach(marker => state.map.removeLayer(marker));
396
  state.hotspotMarkers = [];
397
+
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
  }
 
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
  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
  }
474
 
475
  function update() {
476
+ if (!state.currentUserPosition || !state.isOrientationReady) {
477
  requestAnimationFrame(update);
478
  return;
479
  }
 
485
  const el = document.getElementById(`hotspot-${index}`);
486
  if (!el) return;
487
 
488
+ const distance = haversineDistance(state.currentUserPosition, hotspot);
489
 
490
  if (distance > MAX_VISIBLE_DISTANCE) {
491
  el.classList.add('hidden');
492
  return;
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);
 
527
  '''
528
  return Response(html_content, mimetype='text/html')
529
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
530
  @app.route('/hotspots', methods=['GET', 'POST'])
531
  def handle_hotspots():
532
  if request.method == 'GET':
 
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)