Obrazy z kamer TRISTAR: jak pobrać aktualny obraz i pokazać kamerę na mapie?

Picture of Otwarte Dane

Otwarte Dane

https://otwartedane.gdynia.pl

Obraz z miejskiej kamery można pobrać za pomocą kilku prostych zapytań do API.

System TRISTAR udostępnia otwarte dane związane z ruchem drogowym w Gdyni. Wśród nich znajduje się lista kamer wraz z ich identyfikatorami i położeniem geograficznym oraz osobny zasób pozwalający pobrać informacje o obrazach z wybranej kamery.

To dobry przykład pokazujący, że dane przestrzenne nie muszą kończyć się na punktach na mapie. Punkt może reprezentować rzeczywisty obiekt, a jego identyfikator może prowadzić do kolejnych danych – w tym przypadku do obrazu z kamery.


Krok 1: pobieramy listę kamer

Żeby dotrzeć do właściwego endpointa API TRISTAR można wykorzystać katalog zbiorów otwartych danych:

Zbiór danych – Lista dostępnych kamer:
https://otwartedane.gdynia.pl/pl/dataset/lista-dostepnych-kamer

Zasób JSON:
https://otwartedane.gdynia.pl/pl/dataset/lista-dostepnych-kamer/resource/098b4b6a-943e-45a4-b49d-0b3e3cf5a3e8

Pierwszym właściwym krokiem jest sprawdzenie, jakie kamery są dostępne. Służy do tego endpoint:

https://api.zdiz.gdynia.pl/ri/rest/cameras

API zwraca dane w formacie JSON. Każda kamera ma między innymi identyfikator, nazwę oraz lokalizację zapisaną jako punkt GeoJSON.

Przykładowy wpis może wyglądać następująco:

{
  "id": 11006,
  "name": "Morska - Kwiatkowskiego",
  "location": {
    "type": "Point",
    "coordinates": [18.488641, 54.533152]
  }
}

Najważniejsze są tutaj dwie informacje. Pierwsza to id, czyli identyfikator kamery. Będzie potrzebny w kolejnym zapytaniu. Druga to coordinates, czyli współrzędne pozwalające umieścić kamerę na mapie.


Krok 2: odczytujemy położenie kamery

Pole location jest zapisane jako GeoJSON. Współrzędne punktu występują w kolejności:

[długość geograficzna, szerokość geograficzna]

Dla przykładowej kamery otrzymujemy:

[18.488641, 54.533152]

Te wartości można wykorzystać do zaznaczenia kamery na mapie. Warto zwrócić uwagę na kolejność współrzędnych. Jest to jedna z częstszych pomyłek podczas pracy z danymi przestrzennymi – GeoJSON zapisuje najpierw długość, a następnie szerokość geograficzną:
https://www.openstreetmap.org/?mlat=54.533152&mlon=18.488641#map=18/54.533152/18.488641


Krok 3: pytamy API o obrazy wybranej kamery

Kiedy znamy identyfikator kamery, możemy użyć go w kolejnym zapytaniu. Dla kamery o ID 11006 adres ma postać:

https://api.zdiz.gdynia.pl/ri/rest/camera_image_data?cameraId=11006

Jeżeli dla kamery dostępny jest obraz, API zwraca informacje zawierające między innymi identyfikator kamery, czas zapisania obrazu oraz pole imageUrl.

Struktura takiego wpisu wygląda następująco:

{
  "id": 1628100,
  "cameraId": 11006,
  "imageUrl": "/ri/camera/images/1628100",
  "insertTime": "2019-06-06 17:35:26"
}

Warto pamiętać, że dostępność obrazów może się zmieniać. Kamera znajdująca się na liście kamer nie musi w danej chwili mieć obrazu dostępnego przez API. Dlatego aplikacja korzystająca z danych powinna obsługiwać również pustą odpowiedź.


Krok 4: budujemy adres obrazu

Pole imageUrl zawiera ścieżkę, a nie pełny adres. Trzeba więc połączyć ją z adresem serwera API.

Jeżeli otrzymamy:

/ri/camera/images/1628100

pełny adres będzie miał postać:

https://api.zdiz.gdynia.pl/ri/camera/images/1628100

Pod tym adresem znajduje się właściwy obraz. Można go więc wykorzystać jako źródło elementu <img> na stronie internetowej.


Dlaczego nie zapisujemy adresu obrazu na stałe?

Identyfikator obrazu zmienia się wraz z pojawieniem się kolejnego zdjęcia. Nie należy więc zapisywać jednego adresu imageUrl i traktować go jako stałego adresu kamery.

Poprawna kolejność działania wygląda inaczej:

  1. wybieramy kamerę,
  2. zapamiętujemy jej identyfikator,
  3. pytamy API o obrazy dla tego identyfikatora,
  4. odczytujemy aktualny imageUrl,
  5. wyświetlamy wskazany obraz.


Jeżeli chcemy obserwować kamerę przez dłuższy czas, zapytanie o camera_image_data trzeba okresowo powtarzać. Dokumentacja TRISTAR określa ten zbiór jako dynamiczny, aktualizowany co 5 minut, dlatego w prostej aplikacji demonstracyjnej taki interwał jest wystarczający.

Obraz z miejskiej kamery można pobrać za pomocą kilku prostych zapytań do API


Dlaczego aplikacja potrzebuje małego proxy?

Mogłoby się wydawać, że skoro API TRISTAR jest dostępne przez HTTPS, aplikacja napisana w JavaScript może pobierać dane bezpośrednio z adresu api.zdiz.gdynia.pl. W przeglądarce obowiązuje jednak dodatkowy mechanizm bezpieczeństwa o nazwie CORS.

Serwer API nie udostępnia obecnie nagłówka pozwalającego aplikacji uruchomionej w innej domenie na bezpośrednie odczytanie odpowiedzi JSON za pomocą funkcji fetch(). Samo zapytanie może zakończyć się po stronie serwera poprawną odpowiedzią HTTP, ale przeglądarka nie udostępni jej kodowi JavaScript.

Dlatego w aplikacji demonstracyjnej wykorzystujemy bardzo mały skrypt pośredniczący napisany w PHP:

przeglądarka → api.php → API TRISTAR

Skrypt api.php nie tworzy własnej bazy danych, nie buforuje odpowiedzi i nie przetwarza danych TRISTAR. Jego zadaniem jest wyłącznie pobranie JSON-u z API i przekazanie go aplikacji działającej w tej samej domenie.

Dla bezpieczeństwa proxy nie pozwala również na pobieranie dowolnych adresów. Obsługuje tylko dwa endpointy potrzebne w naszym przykładzie:

ri/rest/cameras
ri/rest/camera_image_data

Dzięki temu rozwiązanie pozostaje proste, a jednocześnie aplikacja może działać w zwykłej przeglądarce.


Od API do prostej aplikacji

Do wykonania przykładu nie potrzebujemy frameworka ani rozbudowanego backendu. Aplikacja składa się z pliku HTML z prostym JavaScriptem oraz niewielkiego pliku api.php.

JavaScript nie odwołuje się już bezpośrednio do endpointów JSON TRISTAR. Zamiast tego korzysta z lokalnego proxy:

const PROXY_URL = './api.php';

async function getJson(path, params = {}) {
  const url = new URL(PROXY_URL, window.location.href);
  url.searchParams.set('path', path);

  for (const [key, value] of Object.entries(params)) {
    url.searchParams.set(key, value);
  }

  const response = await fetch(url, { cache: 'no-store' });

  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`);
  }

  return response.json();
}

Listę kamer możemy teraz pobrać w prosty sposób:

const data = await getJson('ri/rest/cameras');

A dane obrazu dla wybranej kamery:

const images = await getJson(
  'ri/rest/camera_image_data',
  { cameraId: 11006 }
);

Proxy zamienia takie zapytania na właściwe wywołania API TRISTAR.


Obraz możemy pobrać bezpośrednio

Proxy jest potrzebne do odczytywania odpowiedzi JSON przez JavaScript. Nie oznacza to jednak, że również sam obraz musi przez nie przechodzić.

Kiedy znamy już imageUrl, możemy zbudować pełny adres:

const IMAGE_BASE = 'https://api.zdiz.gdynia.pl';

const imageUrl = new URL(
  current.imageUrl,
  IMAGE_BASE
).href;

a następnie przypisać go bezpośrednio do elementu:

image.src = imageUrl;

W ten sposób JSON przechodzi przez niewielkie proxy, natomiast właściwy obraz jest pobierany przez przeglądarkę bezpośrednio z serwera TRISTAR.

Schemat działania aplikacji wygląda więc następująco:

                 dane JSON
przeglądarka → api.php → API TRISTAR
                           ↓
                        imageUrl
                           ↓
przeglądarka ←────────── obraz


Co łączy obraz z mapą?

Najciekawsze w tym przykładzie jest połączenie kilku rodzajów informacji.

Lista kamer mówi nam, jaki obiekt istnieje i gdzie się znajduje. Identyfikator kamery pozwala następnie zapytać o dane dotyczące tego konkretnego obiektu. Otrzymany imageUrl prowadzi natomiast do właściwego obrazu.

Możemy więc przejść prostą drogę:

kamera → ID → współrzędne → punkt na mapie
       ↓
       ID → dane obrazu → imageUrl → obraz

To dobry przykład wykorzystania identyfikatorów w otwartych danych. Jeden zbiór nie musi zawierać wszystkich informacji. Wspólny identyfikator pozwala połączyć kilka zasobów.


Prosta aplikacja demonstracyjna

Do tego przykładu przygotowaliśmy niewielką aplikację demonstracyjną w HTML, CSS, JavaScript i PHP. Pobiera ona listę kamer, pozwala wybrać kamerę, pokazuje jej położenie oraz próbuje pobrać najnowszy dostępny obraz.

Prosta aplikacja demonstracyjna

Kod PHP pełni wyłącznie rolę prostego pośrednika dla danych JSON. Cała logika wyboru kamery, wyświetlania informacji i odświeżania obrazu znajduje się po stronie przeglądarki.

Aplikacja okresowo ponawia zapytanie, dzięki czemu może wyświetlić nowszy obraz bez przeładowywania całej strony. Kod jest celowo prosty – jego zadaniem nie jest stworzenie kompletnego systemu monitoringu, lecz pokazanie sposobu korzystania z otwartego API.

Przykładowe aplikacje i skrypty wykorzystywane w programach rozwojowych publikujemy w repozytoriach GitHub Otwarte Dane Gdynia:
https://github.com/otwartedane-gdynia-pl/Obrazy-z-kamer-TRISTAR

Repozytorium może przechowywać cały kod przykładu. Do samodzielnego uruchomienia działającej wersji potrzebny jest jednak serwer WWW obsługujący PHP. Sam GitHub Pages udostępnia pliki statyczne i nie wykonuje skryptu api.php.

Działający przykład można wypróbować tu:
https://otwartedane.gdynia.pl/programy-rozwojowe-przyklady/obrazy-z-kamer-tristar/


Na co zwrócić uwagę?

Obraz z kamery jest dobrym przykładem danych dynamicznych. Sam fakt, że kamera występuje na liście, nie oznacza, że zawsze będzie dostępny jej aktualny obraz. Aplikacja powinna więc poprawnie reagować na brak danych.

Nie należy również mylić pola lastUpdate opisującego kamerę z polem insertTime konkretnego obrazu. Jeśli chcemy poinformować użytkownika, kiedy powstało wyświetlane zdjęcie, właściwą informacją jest czas związany z rekordem obrazu.

Warto również pamiętać, że proxy zastosowane w tym przykładzie nie jest dodatkowym źródłem danych. Dane nadal pochodzą z API TRISTAR. Skrypt jedynie umożliwia ich odczytanie przez aplikację działającą w przeglądarce.


Podsumowanie

Pobranie obrazu z kamery TRISTAR wymaga połączenia kilku prostych elementów. Najpierw pobieramy listę kamer i wybieramy interesujący nas obiekt. Z jego rekordu odczytujemy identyfikator i współrzędne. Następnie używamy identyfikatora do pobrania informacji o obrazie i z pola imageUrl budujemy jego pełny adres.

W aplikacji internetowej dochodzi jeszcze jeden niewielki element techniczny. Ponieważ przeglądarka nie może obecnie bezpośrednio odczytać odpowiedzi JSON z API TRISTAR działającego w innej domenie, wykorzystujemy proste proxy PHP. Nie zmienia ono danych i nie komplikuje sposobu korzystania z API – jedynie przekazuje odpowiedź do JavaScriptu.

Ten przykład dobrze pokazuje sposób pracy z otwartymi danymi przestrzennymi. Punkt na mapie nie musi być końcem analizy. Może być początkiem przejścia do kolejnego zasobu, aktualnej informacji albo – jak w przypadku TRISTAR – obrazu powiązanego z konkretnym miejscem.

Ostatnio dodane