1
0
mirror of https://github.com/Rhinemann/IoT-Systems.git synced 2026-03-14 20:50:39 +02:00

add: MapView template

This commit is contained in:
Toolf 2024-02-26 14:53:24 +02:00
parent 93cc8d7378
commit e32ba94adc
8 changed files with 458 additions and 0 deletions

3
MapView/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
.idea
venv
__pycache__

251
MapView/README.md Normal file
View File

@ -0,0 +1,251 @@
## Лабораторна робота №2
### Тема
Візуалізація якості стану дорожнього покриття за допомогою фреймворку Kivy.
### Мета
Розробити програму для візуалізації руху машини на дорозі та якості дороги за допомогою даних датчиків.
### Підготовка робочого середовище, встановлення проекту
Клонування репозиторію
```
git clone git@git.comsys.kpi.ua:Diana_Tymoshenko/MapView.git
cd MapView
```
Створення віртуального середовища. Пакет virtualenv повинен бути встановлений в системі!
`python -m venv ./venv`
Активація середовища для linux
`source ./venv/bin/activate`
Активація середовища для windows
`venv\Scripts\activate`
Встановлення необхідних бібліотек
`pip install kivy mapview`
### Завдання
Для початку необхідно отримати дані з датчиків: дані акселерометра знаходяться в файлі data.csv,
GPS дані можна згенерувати за допомогою цього [ресурсу](https://www.nmeagen.org/) та записати їх у csv файл.
Для відображення мапи використовується віджет [Mapview](https://mapview.readthedocs.io/en/1.0.4/) для Kivy.
Для візуалізації руху машини можна використовувати MapMarker та рухати його відповідно GPS даних.
Для визначення стану дорожнього покриття потрібно накопичити певну кількість показників акселерометра (наприклад, 100) та
проаналізувати їх певним чином на нерівності дороги (наприклад, великі та маленькі ями, бордюр).
Для їх позначення на дорозі також використовувати маркери. Зображення для маркерів можна знайти в папці images.
Для створення та редагування маршруту машини на мапі використовуйте клас LineMapLayer та функцію
`add_point()` з файлу lineMapLayer.py. Для додавання лінії на мапу `mapview.add_layer()` з `mode="scatter"`.
Щоб створити затримку у відображення руху машини можна використовувати
функцію `kivy.clock.Clock.schedule_once()`.
### Аналіз даних акселерометра
Будемо використовувати дані тільки по осі z. В стані спокою значення по осі z становить
приблизно 16667 одиниць (або 1 g = 9.8 м/с²) (1 од = 0.00006 g).
Для аналізу нерівностей дороги можна використати функцію `scipy.signal.find_peaks()`, що
знаходить локальні максимуми (піки) вхідних даних. Локальні максимуми вказують на наявність
бордюрів або лежачих поліцейських, локальні мінімуми - на ями.
Для налаштування алгоритму знаходження максимумів використовуються такі параметри:
- `height`: мінімальна висота піків.
- `distance`: мінімальна відстань між піками.
- `prominence`: мінімальна висота піку відносно його сусідів (помітність піку).
- `width`: мінімальна ширина піку.
Важливо підібрати оптимальні параметри, щоб відсіяти шум та залишити тільки ті піки, що дійсно відображають
якість дорожнього покриття. Для більшої наочності при підбиранні параметрів можна показники акселерометра та
результати аналізу відобразити на графіку.
Для знаходження мінімуму треба відзеркалити дані (помножити на -1) та вказати негативне значення висоти.
За бажанням можна використовувати будь-який інший спосіб аналізу показників акселерометра.
**Шаблон основного файлу проєкту**
```python
from kivy.app import App
from kivy_garden.mapview import MapMarker, MapView
from kivy.clock import Clock
from lineMapLayer import LineMapLayer
class MapViewApp(App):
def __init__(self, **kwargs):
super().__init__()
# додати необхідні змінні
def on_start(self):
"""
Встановлює необхідні маркери, викликає функцію для оновлення мапи
"""
def update(self, *args):
"""
Викликається регулярно для оновлення мапи
"""
def check_road_quality(self):
"""
Аналізує дані акселерометра для подальшого визначення
та відображення ям та лежачих поліцейських
"""
def update_car_marker(self, point):
"""
Оновлює відображення маркера машини на мапі
:param point: GPS координати
"""
def set_pothole_marker(self, point):
"""
Встановлює маркер для ями
:param point: GPS координати
"""
def set_bump_marker(self, point):
"""
Встановлює маркер для лежачого поліцейського
:param point: GPS координати
"""
def build(self):
"""
Ініціалізує мапу MapView(zoom, lat, lon)
:return: мапу
"""
self.mapview = MapView()
return self.mapview
if __name__ == '__main__':
MapViewApp().run()
```
### Ідеї для підвищення оцінки
Замість читання GPS координат з файлу можна використати утиліту Linux `gpsfake`.
`gpsd` — це програма, яка збирає дані з приймача GPS і надає дані через мережу
потенційно декільком клієнтським програмам. Gpsd можна запускати як фонове завдання.
gpsd надає сервіс шляхом прив'язки до порту 2947 за замовчуванням.
Але оскільки в нас немає приймача GPS, його роботу імітує `gpsfake`.
Він відкриває PTY (псевдо-TTY), запускає екземпляр gpsd, який вважає,
що підлегла сторона PTY є його пристроєм GPS, і передає вміст
одного або кількох тестових файлів з координатами до приймача GPS.
**Встановлення пакетів**:
`sudo apt install gpsd-clients`
Щоб додати ці пакети у віртуальне середовище, відкрийте файл конфігурації
`venv/pyvenv.cfg` та змініть рядок
`include-system-site-packages = true`
В програмі треба додати рядок
`import gps`
**Використання gpsfake**
Команда `gpsfake` запускається в терміналі, але при бажанні можна викликати її із програми
за допомогою модулю `subprocess`.
`gpsfake [OPTIONS] logfile`
Опції, які можуть знадобитись:
- `-P port`: встановлює порт прослуховування.
- `-c sec`: встановлює затримку між реченнями в секундах. За замовчуванням дорівнює нулю (без затримки).
- `-1`: logfile інтерпретується лише один раз, а не зациклюється.
`logfile` - це файл, що містить коодринати в будь-якому підтримуваному форматі, включаючи,
зокрема, NMEA, SiRF, TSIP або Zodiac. Зокрема за допомогою цього [ресурсу](https://www.nmeagen.org/)
можна також згенерувати NMEA файл.
**Використання gpsd**
gpsd використовує JSON для зв’язку зі своїми клієнтами. Відповіді містять класи, а їх імена відповідають
типу повідомлення NMEA. Важливо зазначити, що ці об’єкти JSON можуть бути неповними,
якщо значення не визначено. Це може статися, коли GPS-приймач ще не отримав координати.
У такому випадку поле просто опускається. Ви можете знайти повний список команд і більш
детальний опис [тут](https://gpsd.gitlab.io/gpsd/gpsd_json.html).
Більше про gpsd, його методи та приклади використання можна дізнатися [тут](https://gpsd.io/client-howto.html).
Треба зазначити, що запускати gpsd треба в окремому потоці, бо він не може працювати в одному
циклі подій Kivy.
Нижче наведений приклад коду для використання gpsd.
```python
import gps
import threading
class GpsPoller(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
self.gpsd_client = gps.gps(port=port, mode=gps.WATCH_ENABLE) # вказати потрібний порт
self.current_gps = None # поточні координати
self.running = True
def get_gps_data(self):
return self.current_gps
def run(self):
while self.running:
response = self.gpsd_client.next()
print('response:', response)
if response['class'] == 'DEVICE':
is_activated = getattr(response, 'activated')
if is_activated == 0: # коли logfile з координатами був повністю прочитаний
self.running = False
return
if response['class'] == 'TPV' and hasattr(response, 'lat'):
print("Your position: lat = " + str(response.lat) + ", lon = " + str(response.lon))
self.current_gps = [response.lat, response.lon]
```
Для роботи з цим класом треба створити його екземпляр в основному класі програми та розпочати діяльність потоку
за допомогою методу `start()`, який викликає метод `run()`. Для отримання координат використовувати метод `get_gps_data()`.
Зверніть увагу, що перші координати надходять не одразу, тож слідкуйте за тим, яку затримку
треба поставити, щоб перша відповідь містила поля lat та lon. Також якщо при виклику команди `gpsfake`
ви використовуєте затримку між реченнями в 1 секунду, то між кожним зчитуванням координат від gpsd
треба поставити затримку в 3 секунди, бо для однієї пари координат використовується 3 речення
з logfile.
Якщо при запуску програми виникає така помилка:
```
gpsd:ERROR: SER: device open of /dev/pts/4 failed: Permission denied - retrying read-only
gpsd:ERROR: SER: read-only device open of /dev/pts/4 failed: Permission denied
gpsd:ERROR: /dev/pts/4: device activation failed, freeing device.
```
Треба змінити режим профілю безпеки AppArmor для gpsd з режиму enforced на режим complain:
```
sudo apt install apparmor-utils
sudo aa-complain /usr/sbin/gpsd
sudo apparmor-status # для перевірки режиму
```
### Корисні посилання:
- [Kivy](https://kivy.org/doc/stable/)
- [Mapview](https://mapview.readthedocs.io/en/1.0.4/)
- [scipy.signal.find_peaks](https://docs.scipy.org/doc/scipy/reference/generated/scipy.signal.find_peaks.html)
- [gpsfake](https://gpsd.gitlab.io/gpsd/gpsfake.html)
- [gpsd](https://gpsd.io/gpsd.html)
- [threading](https://docs.python.org/uk/3/library/threading.html)

1
MapView/data.csv Normal file

File diff suppressed because one or more lines are too long

BIN
MapView/images/bump.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
MapView/images/car.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

BIN
MapView/images/pothole.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

56
MapView/lab2.py Normal file
View File

@ -0,0 +1,56 @@
from kivy.app import App
from kivy_garden.mapview import MapMarker, MapView
from kivy.clock import Clock
from lineMapLayer import LineMapLayer
class MapViewApp(App):
def __init__(self, **kwargs):
super().__init__()
# додати необхідні змінні
def on_start(self):
"""
Встановлює необхідні маркери, викликає функцію для оновлення мапи
"""
def update(self, *args):
"""
Викликається регулярно для оновлення мапи
"""
def check_road_quality(self):
"""
Аналізує дані акселерометра для подальшого визначення
та відображення ям та лежачих поліцейських
"""
def update_car_marker(self, point):
"""
Оновлює відображення маркера машини на мапі
:param point: GPS координати
"""
def set_pothole_marker(self, point):
"""
Встановлює маркер для ями
:param point: GPS координати
"""
def set_bump_marker(self, point):
"""
Встановлює маркер для лежачого поліцейського
:param point: GPS координати
"""
def build(self):
"""
Ініціалізує мапу MapView(zoom, lat, lon)
:return: мапу
"""
self.mapview = MapView()
return self.mapview
if __name__ == '__main__':
MapViewApp().run()

147
MapView/lineMapLayer.py Normal file
View File

@ -0,0 +1,147 @@
from kivy_garden.mapview import MapLayer, MapMarker
from kivy.graphics import Color, Line
from kivy.graphics.context_instructions import Translate, Scale, PushMatrix, PopMatrix
from kivy_garden.mapview.utils import clamp
from kivy_garden.mapview.constants import (MIN_LONGITUDE, MAX_LONGITUDE, MIN_LATITUDE, MAX_LATITUDE)
from math import radians, log, tan, cos, pi
class LineMapLayer(MapLayer):
def __init__(self, coordinates=None, color=[0, 0, 1, 1], width=2, **kwargs):
super().__init__(**kwargs)
# if coordinates is None:
# coordinates = [[0, 0], [0, 0]]
self._coordinates = coordinates
self.color = color
self._line_points = None
self._line_points_offset = (0, 0)
self.zoom = 0
self.lon = 0
self.lat = 0
self.ms = 0
self._width = width
@property
def coordinates(self):
return self._coordinates
@coordinates.setter
def coordinates(self, coordinates):
self._coordinates = coordinates
self.invalidate_line_points()
self.clear_and_redraw()
def add_point(self, point):
if self._coordinates is None:
#self._coordinates = [point]
self._coordinates = []
self._coordinates.append(point)
# self._coordinates = [self._coordinates[-1], point]
self.invalidate_line_points()
self.clear_and_redraw()
@property
def line_points(self):
if self._line_points is None:
self.calc_line_points()
return self._line_points
@property
def line_points_offset(self):
if self._line_points is None:
self.calc_line_points()
return self._line_points_offset
def calc_line_points(self):
# Offset all points by the coordinates of the first point,
# to keep coordinates closer to zero.
# (and therefore avoid some float precision issues when drawing lines)
self._line_points_offset = (self.get_x(self.coordinates[0][1]),
self.get_y(self.coordinates[0][0]))
# Since lat is not a linear transform we must compute manually
self._line_points = [(self.get_x(lon) - self._line_points_offset[0],
self.get_y(lat) - self._line_points_offset[1])
for lat, lon in self.coordinates]
def invalidate_line_points(self):
self._line_points = None
self._line_points_offset = (0, 0)
def get_x(self, lon):
"""Get the x position on the map using this map source's projection
(0, 0) is located at the top left.
"""
return clamp(lon, MIN_LONGITUDE, MAX_LONGITUDE) * self.ms / 360.0
def get_y(self, lat):
"""Get the y position on the map using this map source's projection
(0, 0) is located at the top left.
"""
lat = radians(clamp(-lat, MIN_LATITUDE, MAX_LATITUDE))
return (1.0 - log(tan(lat) + 1.0 / cos(lat)) / pi) * self.ms / 2.0
# Function called when the MapView is moved
def reposition(self):
map_view = self.parent
# Must redraw when the zoom changes
# as the scatter transform resets for the new tiles
if self.zoom != map_view.zoom or \
self.lon != round(map_view.lon, 7) or \
self.lat != round(map_view.lat, 7):
map_source = map_view.map_source
self.ms = pow(2.0, map_view.zoom) * map_source.dp_tile_size
self.invalidate_line_points()
self.clear_and_redraw()
def clear_and_redraw(self, *args):
with self.canvas:
# Clear old line
self.canvas.clear()
self._draw_line()
def _draw_line(self, *args):
if self._coordinates is None:
return
map_view = self.parent
self.zoom = map_view.zoom
self.lon = map_view.lon
self.lat = map_view.lat
# When zooming we must undo the current scatter transform
# or the animation distorts it
scatter = map_view._scatter
sx, sy, ss = scatter.x, scatter.y, scatter.scale
# Account for map source tile size and map view zoom
vx, vy, vs = map_view.viewport_pos[0], map_view.viewport_pos[1], map_view.scale
with self.canvas:
self.opacity = 0.5
# Save the current coordinate space context
PushMatrix()
# Offset by the MapView's position in the window (always 0,0 ?)
Translate(*map_view.pos)
# Undo the scatter animation transform
Scale(1 / ss, 1 / ss, 1)
Translate(-sx, -sy)
# Apply the get window xy from transforms
Scale(vs, vs, 1)
Translate(-vx, -vy)
# Apply what we can factor out of the mapsource long, lat to x, y conversion
Translate(self.ms / 2, 0)
# Translate by the offset of the line points
# (this keeps the points closer to the origin)
Translate(*self.line_points_offset)
Color(*self.color)
Line(points=self.line_points, width=self._width)
# Retrieve the last saved coordinate space context
PopMatrix()