Skocz do zawartości

Przetwarzanie OpenStreetMap na systemie wbudowanym #4 - Dekodowanie formatu .ltd


kronikary

Pomocna odpowiedź

 

1.thumb.png.8eb5d1cfc8d78b696685ecbb61ba7674.png

Witam w ostatniej części artykułu. Teraz zajmiemy się dekodowaniem formatu .ltd na systemie wbudowanym, a następnie wykonamy proces renderowania mapy na wyświetlaczu LCD. Zadanie dekodowania mamy o tyle ułatwione, że nie musimy wykonywać kolejny raz obciążającego etapu parsowania. Wystarczy jedynie seriami odczytywać dane z karty pamięci lub innej pamięci zewnętrznej. Problemów jednak mogą przysporzyć wszystkie operacje mające na celu renderowanie mapy.

Ten artykuł bierze udział w naszym konkursie! 🔥
Na zwycięzców czekają karty podarunkowe Allegro, m.in.: 2000 zł, 1000 zł i 500 zł.

konkurs_forbot_nagrody_1-350x147.png

Potrafisz napisać podobny poradnik? Opublikuj go na forum i zgłoś się do konkursu!
Czekamy na ciekawe teksty związane z elektroniką i programowaniem. Sprawdź szczegóły »

Wykorzystałem mikroprocesor STM32L496, który jest taktowany zegarem 80MHz. Do odczytu danych z karty SD wykorzystałem wbudowany kontroler nazwany SDMMC w trybie 1 – bit, który skonfigurowałem na pracę z częstotliwością 20MHz. Do przedstawiania danych zastosowałem wyświetlacz na magistrali SPI. Kwestie hardwarowe takie jak:

  • podłączenie pamięci zewnętrznej oraz skonfigurowanie biblioteki do jej obsługi
  • podłączenie wyświetlacza oraz skonfigurowanie biblioteki do jej obsługi

pozostawiam Czytelnikowi i omawiając kod zakładam, że zostało to już wykonane. Podczas opisywania kodu w odpowiednich miejscach zaznaczę, gdzie powinniśmy umieścić funkcje do obsługi własnych peryferiów.

image.thumb.png.8a51877b9bd3d4a9fa789eff5723e7c3.png
Drzewo plików biblioteki

Inicjalizacja mapy

Pierwszym etapem podczas odczytu mapy jest wywołanie funkcji:

MAP_RESULT init_map(char * map_name)

Jej argument przyjmuje wskaźnik do tablicy znaków, która zawiera nazwę mapy jaką chcemy otworzyć. Zwracaną wartością jest typ wyliczeniowy MAP_RESULT na podstawie którego możemy sprawdzić czy mapa o przekazanej nazwie na pewno znajduje się w folderze. W ciele funkcji jest wykonywany odczyt wszystkich informacji z pliku .hdr, a następnie są one przechowywane w pamięci RAM. Jeżeli odczyt się udał zwracana jest wartość MAP_OK. Tą funkcję musimy wywołać tylko jeden raz (dla jednej mapy).

Wyświetlanie mapy

Następnie możemy rozpocząć proces dekodowania informacji o obiektach oraz je wyświetlić. Służy do tego funkcja:

MAP_RESULT display_map(map_display_data * map_display, MAP_RESULT * map_status)

Która przyjmuje dwa argumenty. Pierwszy z nich to wskaźnik na strukturę map_display_data. Wygląda ona nastepująco:

typedef struct
	uint8_t zoom;
	char current_map_name[13];
	uint16_t angle_rotation;
	float device_location_lat, device_location_lon; 
}map_display_data;

Przekazując tę strukturę do omawianej funkcji, mapa zostanie wyświetlona na podstawie podanych w niej parametrów. Zmienna „zoom” określa stopień przybliżenia mapy. „Current_map_name” mówi samo za siebie i jest to nazwa dekodowanej mapy. Następnie mamy „angle_roatation” i określa ona obrót mapy względem pozycji podanej w „device_location_lat” oraz „device_location_lat”. Obrót ten jest w zakresie od 0° do 360°.

Drugi jest to wskaźnik na zmienną typu wyliczeniowego. Zmienna tutaj przekazana powinna być typem wyliczeniowym zwróconym podczas inicjalizacji mapy. Przekazujemy wskaźnik, ponieważ zmienna ta ulegnie zmianie. Gdy funkcja została poprawnie wykonana, mapa została wyrenderowana w pamięci wewnętrznej procesora. Musimy teraz ten obszar pamięci przenieść na ekran wyświetlacza. W tym celu wywołujemy funkcję:

void render_tft(void)
{
	ST7789_write_command(RAMWR);
	send_frame_buffer(lcd_frame_buffer, LCD_BUF_SIZE, RGB);
}

W ciele tej funkcji musimy wykorzystać naszą własną obsługę wyświetlacza. Funkcja przesyła całą ramkę wyświetlacza z pamięci wewnętrznej procesora na wyświetlacz LCD.

 

Szczegóły, szczególiki – map_render.c

Zagłębmy się jednak, co dokładnie się dzieje podczas wywołania funkcji

uint8_t display_map(map_display_data *map_display, uint8_t *map_status)
{
	if (*map_status != MAP_OK)
	{
		return *map_status;
	}
	*map_status = read_zoom_header(map_display);
	if (*map_status != MAP_OK)
	{
		return *map_status;
	}

	*map_status = render_map(map_display);
	if (*map_status != MAP_OK)
	{
		return *map_status;
	}
	return MAP_OK;
}

Oprócz sprawdzenia kodów błędów, wchodzimy do funkcji służącej do odczytania nagłówka danego stopnia zbliżenia.

*map_status = read_zoom_header(map_display);

Zgodnie z danymi, jakie przekazaliśmy w strukturze „map_display_data” zostanie otworzony odpowiedni plik stopnia zbliżenia n. Następnie zostaną wczytane wszystkie informacje nagłówkowe tego stopnia, które będą przechowywane w pamięci RAM.

f_read(&map_zoom_file, zoom_header.map_name, map_name_size, &br_zoom); 				//confirm map name
f_read(&map_zoom_file, &zoom_header.zoom_value, zoom_name_size, &br_zoom); 			//read zoom name
f_read(&map_zoom_file, &zoom_header.squares_number, squares_number_size, &br_zoom); //read number of squares
zoom_header.sqrt_square_number = sqrt(zoom_header.squares_number);
f_read(&map_zoom_file, zoom_header.square_width, 2*width_size, &br_zoom); 			//read width size of the blocks
f_read(&map_zoom_file, &zoom_header.addr_size, addr_size_bytes, &br_zoom); 			//read size of the addresses
zoom_header.squares_start_addr = f_tell(&map_zoom_file);
return MAP_OK;

Dane na temat nagłówka są kluczowe podczas dekodowania, ponieważ zawierają wszystkie niezbędne informacje do wykonania normalizacji danych.  Dodatkowo po odczytaniu ilości kafelków w danym stopniu, obliczamy pierwiastek z tej ilości, dzięki czemu nie będziemy musieli w przyszłości marnować czasu na obliczenia. Na koniec zapisujemy wskaźnik na numer bajtu, gdzie znajdują się adresy R/W do kafelków. Po udanym dekodowaniu zostaje zwrócona wartość MAP_OK.

Kolejna funkcja jaka się wykona to:

MAP_RESULT render_map(map_display_data *map_display)
{
    normalize_info(map_display);

    uint16_t array_size; //size of the array that defines number of squares
    square_data square_data;
    object objects;

    array_size = calc_squares(map_view.window_view);
    if (array_size == 0)
    {
      return MAP_NO_MEMORY;
    }
    /*Read every square that fits my screen view*/
    for (int squares_to_display = 0; squares_to_display < array_size; squares_to_display++)
    {
      read_square_addr(map_view.square_list[squares_to_display]);
      read_square_data(&square_data);

      for (int objects_types = 0; objects_types < square_data.all_objects_type; objects_types++)
      {
        read_object_info(&square_data);
        if (square_data.object_qnty != 0)
        {

          for (int object = 0; object < square_data.object_qnty; object++)
          {
            read_subobject_info(&square_data);

            //create array
            objects.nodes = square_data.node_qnty;
            objects.xy_node = (float*)malloc(4 * objects.nodes); //4*n becouse float is 4 bytes
            if (objects.xy_node == NULL)
            {
              return MAP_NO_MEMORY;
            }
            /*Read all nodes*/
            if (f_read(&map_zoom_file, objects.xy_node, 4 * objects.nodes, &br_zoom) != FR_OK)
            {

              return MAP_READ_ERROR;
            }
            normalize_data_float(objects.xy_node, 0);
            objects.max_x = objects.min_x = objects.xy_node[0];
            objects.max_y = objects.min_y = objects.xy_node[1];

            for (int points = 2; points < square_data.node_qnty; points += 2)
            {
              /*Normalize data to the screen size*/
              normalize_data_float(objects.xy_node, points);

              /*Find size of the object*/
              if (objects.xy_node[points + 1] > objects.max_y)
              {
                objects.max_y = objects.xy_node[points + 1];
              }

              else if (objects.xy_node[points + 1] < objects.min_y)
              {
                objects.min_y = objects.xy_node[points + 1];
              }

              if (objects.xy_node[points] > objects.max_x)
              {
                objects.max_x = objects.xy_node[points];
              }

              else if (objects.xy_node[points] < objects.min_x)
              {
                objects.min_x = objects.xy_node[points];
              }
            }

            if (square_data.object_type == 13)
            {
              objects.fill_color = line_colors[object_arr_ptr[square_data.object_type][square_data.subobject_type]][0];
              objects.outline_color = line_colors[object_arr_ptr[square_data.object_type][square_data.subobject_type]][1];
              objects.shortdash = line_colors[object_arr_ptr[square_data.object_type][square_data.subobject_type]][2];
              objects.line_width = line_width_ptr[map_display->zoom][object_arr_ptr[square_data.object_type][square_data.subobject_type]];
              display_line(&objects, 0);
            }
            else
            {
              if (square_data.object_type == 5)
              {
                //if object is in window view
                if (((objects.max_y < MAX_Y && objects.max_y > 0) || (objects.min_y < MAX_Y && objects.min_y > 0))
                    && ((objects.max_x < MAX_X && objects.max_x > 0) || (objects.min_x < MAX_X && objects.min_x > 0)))
                {
                  display_polygon(&objects, 7);

                  objects.fill_color = 0;
                  objects.outline_color = 0;
                  objects.shortdash = 0;
                  objects.line_width = 0;
                  display_line(&objects, 0);
                }
              }
            }
            free(objects.xy_node);
          }
        }
      }

    }
    return MAP_OK;
}

To w niej wykonują się najważniejsze procesy podczas dekodowania informacji o obiektach oraz normalizacji ich danych.

 

Normalizacja danych

Tyle razy o tym wspominałem, ale o co chodzi z tą normalizacją? Wszystkie punkty w obiekcie są zapisane jako współrzędne geograficzne jako typ danych float. Nasz dekoder natomiast używa wyświetlacza, który pracuje w zupełnie innym układzie współrzędnych i korzysta z liczb całkowitych do ustalenia który piksel ma zostać zapalony. W związku z tym musimy zamienić te współrzędne geograficzne na odpowiedni piksel na wyświetlaczu. Będziemy bazować na współrzędnych geograficznych przedstawionych jako stopnie w zapisie dziesiętnym.  Wiemy, że zakres szerokości geograficznych jest od 90° do -90°, a długości geograficznych od -180° do 180°. Natomiast zakres pikseli na wyświetlaczu wynosi (w moim przypadku) odpowiednio od 0 do 320 oraz od 0 do 240. Gdybyśmy chcieli po prostu wyświetlać całą mapę na naszym ekranie to wystarczyłaby informacja o rozmiarach mapy. Wtedy pojedynczy punkt możemy znormalizować do wymiarów wyświetlacza w taki sposób:

image.png.09835720379e94de6d07cbc31736fcac.png

gdzie:
            nlon – długość geograficzna punktu
            nlat – szerokość geograficzna punktu
            dmin – minimalna długość geograficzna dla całej mapy
            dmax – maksymalna długość geograficzna dla całej mapy
            mmin – minimalna szerokość geograficzna dla całej mapy
            mmax – maksymalna szerokość geograficzna dla całej mapy
            XLCD – szerokość wyświetlacza podana w pikselach
            YLCD – wysokość wyświetlacza podana w pikselach
My jednak chcemy wyświetlać jedynie wybrany skrawek mapy na naszym ekranie. Nadal wykorzystamy powyższe wzory, lecz z lekkimi modyfikacjami, ponieważ pod zmienne dmin, dmax, mmin, mmax podstawimy inne wartości. W związku z tym, że chcemy wyświetlać określony skrawek to możemy koordynaty tego wycinka wirtualnie ulokować na nasz wyświetlacz na mapie. Mam na myśli, nadanie pikselowi A(0,0) oraz C(240,320) pewnych współrzędnych geograficznych oraz podstawienie ich do wzorów, dzięki czemu normalizacja zostanie wykonana dla wybranego wycinka, a jej wynik poprawnie odzwierciedlony na ekranie.  Te współrzędne będą liczone wraz z wykonywaniem się programu. Będzie to polegało na tym, że bierzemy nasze współrzędne geograficzne przekazane w strukturze „map_display_data” i od nich odejmujemy lub dodajemy pewien offset, tak aby obliczyć wirtualne punkty A oraz C.

image.thumb.png.7c4879a468e9c52e871a43c26b5102b3.png
Symulacja przykładowej sytuacji wyświetlania mapy

Ponownie podświetlony obszar jest naszym wyświetlaczem. Niech nasz punkt centralny E przyjmie wartości:

E(device_location_lon, device_location_lat)

To od niego będą liczone wirtualne punkty A oraz C i aby to zrobić musimy dodać lub odjąć pewien offset.

image.png.d88c611f1ef4cf6af5bbf8bcfc5891d6.png

Gdzie:
            xE – x-owa punktu E
            yE – y-owa punkt E
            l – offset
            YLCD – wysokość wyświetlacza wyrażona w pikselach
            XLCD – szerokość wyświetlacza wyrażona w pikselach

Offset ten określany jest jako różnica długości geograficznej pomiędzy punktami A i B. Nie musimy tutaj nic liczyć, offset ten podajemy my, ponieważ to my decydujemy jak duży obszar ma być widoczny na ekranie. Im większy offset tym większy obszar zostanie wyświetlony. Małym wyjaśnieniom zasłużyła stała v. Ze względu na to, że offset określa różnicę szerokości, a nasz wyświetlacz nie jest kwadratem, offset należy przemnożyć przez różnicę wymiarów. Gdybyśmy tego nie zrobili to otrzymany obraz byłby „spłaszczony”. Pytanie dlaczego we wzorze pojawia się jeszcze *2. Przecież różnica rozmiarów wyświetlacza została uwzględniona. Otóż należy jeszcze uwzględnić fakt, że zakresy szerokości geograficznych oraz długości geograficznych różnią się dwukrotnie.

Tak się prezentuje funkcja do obliczania wirtualnych punktów wyświetlacza.

void normalize_info(map_display_data *map_display)
{
	float radians;
	float y_length;

	y_length = zoom[map_display->zoom] * SCREEN_DIF;
	radians = map_display->angle_rotation * (M_PI / 180.0);
	sin_val = sin(radians);
	cos_val = cos(radians);

	/*Get virtual coordinates*/
	map_view.window_view[0] = map_display->device_location_lon - y_length;
	map_view.window_view[1] = map_display->device_location_lat - zoom[map_display->zoom];
	map_view.window_view[2] = map_display->device_location_lon + y_length;
	map_view.window_view[3] = map_display->device_location_lat + zoom[map_display->zoom];

	/*Convert device location to pixel*/
	map_view.position_x = (((map_display->device_location_lat - map_view.window_view[1]) / (map_view.window_view[3] - map_view.window_view[1])) * MAX_X);
	map_view.position_y = (((map_display->device_location_lon - map_view.window_view[0]) / (map_view.window_view[2] - map_view.window_view[0])) * MAX_Y);
}

Dodatkowo liczona jest tutaj wartość sinusa oraz cosinusa używana do obracania mapy, a tak naprawdę każdego pojedynczego punktu podczas ich normalizacji. Algorytm do normalizacji danych został przedstawiony poniżej:

void normalize_data_float(float *node_list, uint16_t node_num)
{
	int32_t rot_x, rot_y;
	node_list[node_num] = (int32_t) (((node_list[node_num] - map_view.window_view[1]) / (map_view.window_view[3] - map_view.window_view[1])) * MAX_X);
	node_list[node_num + 1] = (int32_t) (((node_list[node_num + 1] - map_view.window_view[0]) / (map_view.window_view[2] - map_view.window_view[0])) * MAX_Y);
	/*Rotate pixel by the angle*/
	rot_x = map_view.position_x + cos_val * (node_list[node_num] - map_view.position_x)
			- sin_val * (node_list[node_num + 1] - map_view.position_y);
	rot_y = map_view.position_y + sin_val * (node_list[node_num] - map_view.position_x)
			+ cos_val * (node_list[node_num + 1] - map_view.position_y);
	node_list[node_num] = rot_x;
	node_list[node_num + 1] = rot_y;
}

Funkcja ta przyjmuje za argumenty wskaźnik na zmienną typu float, przez który przekazujemy tablicę naszych punktów, które chcemy znormalizować. Jako drugi argument przekazujemy, który punkt ma zostać znormalizowany. Algorytm ten wykonuje normalizację zgodnie z opisanym wcześniej równaniem, a następnie wykonuje jeszcze operację obrotu tego punktu względem puntku X(map_view.position_x, map_view.position_y), który został obliczony w funkcji:

void normalize_info(map_display_data *map_display)

podczas przygotowywania danych do normalizacji. Obrót znormalizowanego punktu jest wykonywany zgodnie z poniższym równaniem:

image.thumb.png.844dc7ac6e4178b5921f7eecf99de843.png

gdzie:

          XO - x-owa punktu względem którego się obracamy
          YO - x-owa punktu względem którego się obracamy
          α - kąt wyrażony w radianach
          Xt - x-owa transponowanego punktu
          Yt - y-owa transponowanego punktu

 

Obliczanie numerów ID kafelków

Pozostała nam do omówienia jeszcze jedna bardzo istotna funkcjonalność, która jest realizowana w ramach pracy dekodera. Chodzi o obliczanie, które kafelki mieszczą się w naszym obszarze do wyświetlenia. Zostało to już omówione w poprzedniej części, lecz tym razem będziemy dekodować, a nie kodować. Weźmy na warsztat jeszcze raz przykład podziału mapy na kafelki.

1206300681_Beztytuu.thumb.png.f8c00df846051625d639cedb077e87e8.png
Symulacja przykładowej sytuacji wyświetlania mapy

Ponownie podświetlony obszar jest naszym „wyświetlaczem”, natomiast zacieniowany całą mapą wgraną do urządzenia. Mamy nasz punkt E, czyli współrzędne geograficzne zawarte w strukturze „map_display_data” oraz na tym etapie wirtualne współrzędne A’ oraz C’ zostały już obliczone. Musimy teraz na podstawie naszych punktów A’ oraz C’ obliczyć, które kafelki są potrzebne do zapełnienia podświetlonego obszaru. Możemy skanować wszystkie kafelki i sprawdzać, które z nich pasują do naszego założenia. Co jeżeli mamy kilka tysięcy kafelków? Będzie to bardzo spowalniało dekodowanie. Z pomocą przyjdą nam tutaj informacje, które zostały wczytane wraz z załadowaniem nagłówka pliku .ltd. Mowa tutaj o szerokości oraz wysokości pojedynczego kafelka, ilości kafelków oraz pierwiastka z tej ilości. Na podstawie tych danych możemy znacznie przyspieszyć proces dekodowania.

uint8_t calc_squares(float *array)
{
	uint8_t a_point[2];
	uint8_t b_point[2];
	uint8_t x_size, y_size;
	uint8_t begin, value;
	uint8_t array_size = 0; //number of elements in array count from 1 not 0
	free(map_view.square_list);
	map_view.square_list = NULL;
	map_view.square_list = (uint16_t*) malloc(0); //initialize array
	if (map_view.square_list == NULL)
	{
		return 0;
	}
	a_point[0] = ((array[0] - main_header.map_size[0])) / zoom_header.square_width[0];
	a_point[1] = ((array[1] - main_header.map_size[2])) / zoom_header.square_width[1];

	b_point[0] = ((array[2] - main_header.map_size[0])) / zoom_header.square_width[0];
	b_point[1] = ((array[3] - main_header.map_size[2])) / zoom_header.square_width[1];

	x_size = (1 + b_point[0] - a_point[0]);
	y_size = (1 + b_point[1] - a_point[1]);
	begin = a_point[0] * zoom_header.sqrt_square_number + a_point[1];

	for (int x = 0; x < x_size; x++)
	{
		value = begin + x * zoom_header.sqrt_square_number;

		for (int y = 0; y < y_size; y++)
		{
			if (value >= 0 && value < zoom_header.squares_number)
			{
				map_view.square_list = (uint16_t*) realloc(map_view.square_list, (array_size + 1) * sizeof(uint16_t));
				map_view.square_list[array_size] = value;
				array_size++;

			}
			value += 1;
		}
	}

	return array_size;
}

Wpierw algorytm oblicza odległość punktu A’ x-owej od x-owej punktu A oraz y-owej punktu A’ od y-owej punktu A oraz dzieli otrzymane wartości przez szerokość pojedynczego kafelka. Analogicznie dzieje się dla punktu C’. Jako wynik otrzymujemy ilość mieszczących się kafelków „x-owych” oraz „y-owych” od punktu A do punkt A’ oraz C’. Można to przedstawić za pomocą prostych równań:

image.thumb.png.114c27097599f9bc606723242d5388aa.png

gdzie:
            n – szerokość lub wysokość pojedynczego kafelka w pliku .ltd

 Posiadając powyższe obliczone informacje, tak naprawdę wiemy, na którym kafelku x-owym oraz y-owym zaczyna się oraz kończy nasz obszar do wyświetlenia. Wystarczy teraz obliczyć różnicę pomiędzy x-owymi oraz y-owymi kafelkami, aby wiedzieć z ilu rzędów i kolumn kafelków składa się ten obszar.

image.png.c38ec3378138c057e80cb36c3e89009d.png

 

Dodawana jest jedynka ze względu na zaokrąglanie w dół poprzednio liczonych wartości.
W ten oto prosty sposób otrzymaliśmy informację z ilu kafelków x-owych oraz y-owych składa się nasz obszar do wyświetlenia. Wystarczy teraz obliczyć numer pierwszego kafelka znajdującego się w naszym oknie do wyświetlenia.

image.png.c9ef2fd802efef98008d3ed7dfe50133.png

gdzie:
            n – łączna ilość kafelków w pliku .ltd

Teraz, posiadamy obliczoną informację o ilości kafelków znajdujących się w naszym obszarze do wyświetlenia oraz numer ID pierwszego kafelka. Pozostaje jeszcze jedna sprawa. Mówiąc potocznie algorytm będzie inkrementował kafelki z dołu na górę dla każdej kolumny. Pojawi się jednak problem, gdy kafelków powyżej naszego okna do wyświetlenia jest więcej niż 0.

image.thumb.png.39f9c9dec9ad4c1a225bb89a97c61fe1.png
Symulacja przykładowej sytuacji wyświetlania mapy

Przeanalizujmy powyższy przykład. Obliczyliśmy, że kafelek 6 jest pierwszym potrzebnym obszarem do wyświetlenia. W tym przypadku wartości x_size oraz y_size są sobie równe i wynoszą 2. Na tym etapie inkrementujemy wiersze kafelków, co wykonamy raz (licząc od 1), ponieważ y_size = 2 . Gdy to się wykona, znajdziemy się w kafelku 7. Teraz powinniśmy przeskoczyć do kolejnej kolumny kafelków, ale nie wiemy jaki numer identyfikacyjny ma pierwszy kafelek w nowej kolumnie. Wynika to z tego, że powyżej kafelka 7 może być 0 kafelków lub tak jak w naszym przypadku jeszcze 2 dodatkowe (kafelek 8 i 9 lub n więcej). W związku z tym z każdym przeskokiem kolumny musimy obliczać o ile dodatkowych kafelków musimy przeskoczyć. Służy do tego to równanie:

image.png.bbf8394acafaef2802a6b0cd7772d01d.png

gdzie:
            n – łączna ilość kafelków w pliku .ltd
            x - kolumna kafelków

Algorytm z każdą iteracją pętli wierszy dodaje do pamięci kolejny numer kafelka, który musi zostać wyświetlony.

Na sam koniec omawiania funkcji:

MAP_RESULT render_map(map_display_data *map_display)

to w trakcie wyświetlania kafelków, pętla for wykonuje się tyle razy ile zostało ich zapisanych. Aby uzyskać wskaźnik R/W do danego kafelka na nośniku danych, wskaźnik R/W jest wpierw przestawiany na początek wskaźnika do adresu kafelka 0, a następnie przesuwa wskaźnik R/W na ten bajt, gdzie znajduje się wskaźnik do adresu kafelka poszukiwanego. Cały proces jest realizowany w:

void read_square_addr(uint16_t square_number)

Następnie jest realizowana funkcja:

void read_square_addr(uint16_t square_number)

w której odczytywane są wszystkie dane dotyczące kafelka. W następujących pętlach for odczytywane są dane o obiektach, ich typach, rodzajach oraz wszystkich punktach, które następnie zostają znormalizowane. Na sam koniec następuje sprawdzenie czy to jest budynek czy droga i odpowiednio wyświetlona wielolinia lub poligon. Trzeba tutaj sprawdzać typ obiektu, aby wiedzieć czy rysować wielolinie czy poligon.

 

Deskryptor

Chciałbym teraz przejść już do ostatniego pliku, jakim jest deskryptor. To jest miejsce gdzie możemy skonfigurować jak ma być wyświetlana mapa.
Plik ten umożliwia:

  • Ustawienie poziomów zbliżeń i jak szerokie mają być obszary dla każdego z nich.

    const float zoom[MAP_ZOOM_LEVELS] =
    {
    		0.0005,
    		0.001,
    		0.002,
    		0.007,
    		0.02,
    		0.1,
    		0.5,
    		1,
    };
  • Ustawienie dla każdego poziomu zbliżenia, który plik .ltd ma być otwierany
    uint8_t zoom_header_assignment[MAP_ZOOM_LEVELS] =
    {
    		3, //zoom level 0
    		3, //zoom level 1
    		3, //zoom level 2
    		0, //zoom level 3
    		0, //zoom level 4
    		0, //zoom level 5
    		0, //zoom level 6
    		0, //zoom level 7
    };

    W teorii powinno być tak, że każdy poziom zbliżenia to oddzielny plik .ltd. Trzeba jednak wziąć pod uwagę, że niekiedy tyle plików nie zostanie wyeksportowanych np. gdy mapa jest bardzo mała i szerokość poziomu zbliżenia zaczyna obejmować teren większy, niż obszar mapy. W takim wypadku powinniśmy ustawić ten sam plik dla wielu poziomów zbliżeń. Można jednak zobaczyć, że tablica ta nie jest przechowywana w pamięci Flash. Planuję w dalszych etapach kodera wprowadzić informację o tym jak mają zostać przypisane stopnie zbliżeń.

  • Ustawienie szerokości wielolinii oraz koloru. O ile kolor nie powinien się zmieniać pomiędzy poziomami zbliżeń, to szerokość rysowania wielolinii owszem. Do skonfigurowania, którą metodą mamy wyświetlać wielolinie na danym poziomie zbliżeń służy poniższa tablica 
    const uint8_t *line_width_ptr [] =
    {
    		line_width_0, //zoom level 0
    		line_width_2, //zoom level 1
    		line_width_3, //zoom level 2
    		line_width_4, //zoom level 3
    		line_width_6, //zoom level 4
    		line_width_6, //zoom level 5
    		line_width_6, //zoom level 6
    		line_width_7, //zoom level 7
    };

    Jak można zauważyć kilka poziomów zbliżeń współdzieli między sobą te same grupy. Aby skonfigurować kolory dla tych wielolinii przejdźmy do kolejnej tablicy

    const uint8_t line_colors[][3] =
    {
    		{Red,Red,3},				//default/undefined
    		{GreenYellow,Black,0},		//secondary
    		{GreenYellow,Black,0}, 		//paths
    		{GreenYellow,Black,0}, 		//service
    
    };

    Jest to tablica dwuwymiarowa, w której można skonfigurować kolor linii wewnętrzny, zewnętrzny oraz czy ma być to linia przerywana. Aby teraz wykorzystać te dwie tablice przejdźmy do przykładu tablicy opisującej drogi.

    const uint8_t highway_render[] =
    {
    //# Roads
    //# These are the principal tags for the road network.They range from the most to least important.
            1,  // motorway
            2,  // trunk
            2,  // primary
            0,  // secondary
            1,  // tertiary
            1,  // unclassified
            3,  // residential
    };

    Za pomocą tablic które mają w swojej nazwie _render spinamy ze sobą tablice definiujące kolory oraz szerokość linii. Wartość liczbowa odpowiada elementowi w tablicy kolorów i szerokości.

 

Wymagania do uruchomienia programu

Jeżeli o część sprzętową to przede wszystkim nasz procesor musi obsługiwać pamięci zewnętrzne na które możemy zapisać naszą mapę. Dodatkowo pamięć RAM mikroprocesora musi być na tyle duża, aby zmieściła się w nim ramka wyświetlacza. Ewentualnie można zastosować zewnętrzna pamięć RAM, ale wymaga to pewnych zmian. Zalecam również wybranie procesora, który posiada wbudowany FPU, ponieważ wykonujemy sporo operacji na liczbach zmiennoprzecinkowych. Odnośnie wyświetlacza należy zastosować taki z własnym buforem. Plik z mapą należy umieścić w folderze /Maps.

Jeżeli chodzi o spięcie kwestii programistycznej ze sprzętową. Zacznijmy od pliku map_render.c. Jako, że prawdopodobnie informacje o wyświetlaczu będziemy zamieszczać w oddzielnym pliku, powinniśmy tutaj dołączyć nagłówek do naszego sterownika wyświetlacza. W nagłówku tym musimy zamieścić dwie instrukcje preprocesora:

#define MAX_Y
#define MAX_X

Definiują one rozdzielczość wyświetlacza i są wykorzystywane przez plik map_render.c między innymi podczas normalizacji danych.
W pliku map_reader.c musimy dołączyć bibliotekę obsługującą nasz dostęp do pamięci zewnętrznej. Ja korzystałem z bibliotek i funkcji FatFS, które były konfigurowane przez CubeMX, dlatego mam dołączoną bibliotekę

#include <stdio.h>

Korzystając z innych bibliotek prawdopodobnie będzie konieczność podmiany wszystkich funkcji związanych z dostępem do pamięci zewnętrznej.
W pliku pliku /Graphics/render.c należy przede wszystkim napisać własne ciało funkcji

void render_tft(void)

w którym będziemy wysyłać całą ramkę danych do wyświetlacza. W pliku render.h należy pod definicją

#define LCD_BUF_SIZE

 wpisać ilość pikseli wyświetlacza.

Przed rozpoczęciem renderowania mapy, należy zainicjalizować wszystkie układy peryferyjne. Należy zainicjalizować wyświetlacz oraz zamontować pamięć zewnętrzną. Jeżeli wszystkie układy zgłosiły gotowość, możemy zainicjalizować naszą mapę. Jeżeli inicjalizacja przeszła pomyślnie to możemy rozpocząć renderowanie. Jeżeli to również się udało to możemy ją wyświetlić. Przykładowy kod do wyświetlenia mapy:

init_st7789();
SetWindow(0, MAX_X - 1, 0, MAX_Y - 1);
clear_tft(0xffff);

fresult = f_mount(&fs, "", 1);
if (fresult != FR_OK)
{
  HAL_GPIO_TogglePin(LD3_GPIO_Port, LD3_Pin);
  return 1;
}

uint8_t map_status;
map_display_data map_display;
map_display.angle_rotation = 270;
strcpy(map_display.current_map_name, "kreta");
map_display.device_location_lat = 53.11279;
map_display.device_location_lon = 23.14532;
map_display.zoom = 0;

map_status = init_map(map_display.current_map_name);
if (map_status != 0)
{
  HAL_GPIO_TogglePin(LD2_GPIO_Port, LD2_Pin);
  return 1;
}
display_map(&map_display, &map_status);
if (map_status != 0)
{
  HAL_GPIO_TogglePin(LD3_GPIO_Port, LD3_Pin);
  return 1;
}
render_tft();

Jest on realizowany w pliku głównym do którego należy dołączyć

#include "/MAP/Map_display/map_display.h"

Testy

A jak utworzony format .ltd v2 działa w praktyce? Odpowiedź znajduje się w filmiku poniżej.

Wyświetlanie wszystkich stopni zbliżeń w kolejności od największego do najmniejszego. 

signal-2021-03-30-194340.thumb.jpg.93383da54f63c586ec87f16ece7c7627.jpg
Jeden z pierwszych testów formatu .ltd v1. Widoczne cyfry na ekranie to czas potrzebny na renderowanie podany w [ms]. Warto zwrócić uwagę na to,
że rozdzielczość ekranu jest mniejsza niż w ekranach użytych na filmikach, a mimo to czas renderowania jest zatrważająco długi.

Zastanawiałem się czy nie przedstawić testów czasu renderowania, ale są one bardzo różne i zależą przede wszystkim od poziomu zbliżenia i ilości kafelków aktualnie do wyświetlenia. Mogę, więc jedynie powiedzieć, że czas minimalny jaki udało mi się uzyskać to 70ms, natomiast mapa renderuje się zawsze poniżej 1s. Da się jeszcze przyspieszyć proces renderowania (podejrzewam że o około 30%), ale pozostawię to sobie na inną okazję.

Możemy jednak zaobserwować, że nasze drogi oraz budynki są mało kontrastujące. Wynika to przede wszystkim z tego jak działa silnik graficzny i czy obsługuje rysowanie konturów. Nie jest to takie proste, ponieważ jak narysować kontur dwóch krzyżujących się dróg?

Poniżej znajduje się przykładowy filmik z wykorzystaniem formatu .ltd v2 w zastosowaniu nawigatora GPS.

Wyświetlanie pozycji oraz skrolowanie mapy

Filmik dosyć stary i nie do końca aktualny jeżeli chodzi o aplikację i osprzęt,
ale przedstawiam tutaj dokładność nawigacji GPS w połączeniu z mapami OSM.

signal-2021-03-28-035435.thumb.jpg.3a0712664ce797adc8401b4b873e930b.jpg
Przykładowy skrawek mapy

Biblioteki kodów znajdują się w załączniku.

 

Podsumowanie

W omówionym artykule przedstawiłem podstawowe informacje na temat projektu OSM oraz wyjaśniłem metodykę jaką wykorzystałem w celu wyświetlania obiektów OSM na systemie wbudowanym. Myślę, że przedstawiony format map w pełni spełnia nasze założenia. Jest lekki, nie wymaga dużej ilości pamięci RAM, a dzięki sposobie kodowania możemy w szybki sposób odczytać niezbędne informacje o obiektach. Zaprezentowany standard spełnia podstawową funkcjonalność, to znaczy może zostać wykorzystany głównie do wyświetlania obiektów wspieranych przez OSM. Chcąc przykładowo wykorzystać go do stworzenia nawigacji GPS lub wyszukiwać określonych tagów/obiektów należałoby zmodyfikować parę funkcjonalności. Myślę, że 4 części artykułu są na razie wystarczające i tą tematyką zajmiemy się kiedy indziej. 🙂 

Mam nadzieję, że przedstawionymi zagadnieniami zainteresowałem przynajmniej jednego Czytelnika. Nie będę ukrywał, że podczas podejmowania się zadania wyświetlania map na systemie wbudowanym, nie spodziewałem się niedoboru informacji oraz przykładów w sieci. Przecież z mapami mamy kontakt prawie codziennie, chociażby szukając naszej ulubionej pizzeri, a mimo to temat nie jest rozpropagowany (a może zbyt mało osób je pizze?). Wydaje mi się, że ten niedobór informacji na temat kodowania oraz wyświetlania map „offline” wynika z tego, że większość aplikacji wykorzystujących mapy są przystosowane do pracy „online”. Użytkownik jedynie pobiera z sieci niezbędne kafelki do zapełnienia obszaru, ale to w jaki sposób dane są dekodowane pozostaje pewną tajemnicą. A może pobierane kafelki nie są wektorowe tylko rastrowe? No cóż, byłoby miło stworzyć jakiś zestandaryzowany format map oraz przyjazne użytkownikowi API do ich wyświetlania. Nie mówię tu koniecznie o zastosowaniu w systemach wbudowanych, ale od czegoś trzeba zacząć.

Spis treści:

 

O mnie
Nazywam się Kuba i jestem studentem Politechniki Białostockiej na Wydziale Elektrycznym. Na forbot.pl trafiłem po raz pierwszy jakieś 8-9 lat temu, gdy nauczyciel informatyki przedstawił nam roboty minisumo. Nie wiem właściwie czemu, ale mimo braku jakiejkolwiek wiedzy o elektronice to temat mnie zaciekawił. I to na tyle, że kilka tygodni później zamówiłem pierwszą chlebopłytkę, kilka diod i mikroprocesor AVR. Od tego momentu wszystko potoczyło się błyskawicznie. Zainteresowanie przekształciło się w pasję, wraz z forbotem stworzyłem jakiegoś prostego robota typu linefollower + minisumo (który niestety nigdzie na konkurs nie pojechał, ale pomógł mi w zaliczeniu fizyki na 6 🙂 ), a potem podstawową wiedzę zacząłem wykorzystywać w innych projektach. Na mikroprocesory ARM przeszedłem dosyć późno, bo dopiero dwa lata temu, ale była to bardzo dobra decyzja. Ostatecznie próbuję pasję do elektroniki i programowania mikrokontrolerów powoli przekształcać w karierę zawodową.

MAP.zip

  • Lubię! 2
Link do komentarza
Share on other sites

Na szybko z telefonu. Co do rastra - rozmawialiśmy o tym wcześniej. Co do standardów - sprawdź rozwiązania Open Geospatial Consortium. Te kafelki średnio, ale zarówno rozwiązania online (w*s), jak i proste offline (choćby geotiff) są w praktyce standardem wymiany.

  • Pomogłeś! 1
Link do komentarza
Share on other sites

To jeszcze wyjaśnienie dłuższe. Kafelki co do swej natury są rastrem. To zresztą klasyczna kwestia w GIS-ie (Systemach Informacji Geograficznej) - model danych rastrowy i wektorowy. Wizualizacja bazy na stronie to efekt pracy renderera. Kafelki są zawsze wizualizacją kawałka bazy w określanym stylu (redakcji graficznej i tematycznej). Np. wersja rowerowa różni się od wersji podstawowej. Jest wiele rzeczy w bazie, których w standardowej wizualizacji nie widać i których nie ma na podstawowych kafelkach. Jeśli chcemy w naszej aplikacji mieć hydranty, okna życia (są w OSM), albo inny styl graficzny dla lodziarni, to musimy wyciągnąć to z wektora i odpowiednio zasymbolizować. Kafelki dają nam gotowca, ale można też pobrać stylizację standardową dla danych wektorowych (https://wiki.openstreetmap.org/wiki/Pl:Mapnik#Standardowy_styl_OSM_Mapnika).

A jeśli ktoś, bez wchodzenia w narzędzia GIS-owe, chce poznać zawartość OSM, można skorzystać z overpass przez stronę https://overpass-turbo.eu/ Przykładowe proste zapytanie o obiekty punktowe z dostępnością dla niepełnosprawnych:

node
  [wheelchair=yes]
  ({{bbox}});
out;

Pełny wykaz atrybutów i wartości (tagów, jak to się w OSM mówi) jest na wiki projektu https://wiki.openstreetmap.org/wiki/Map_Features Żeby wiedzieć, czego szukać :).

 

A standardy (wspomniane OGC) są w branży bardzo ważne że względu na interoperacyjność i wymianę danych między systemami, ale o tym już się nie będę rozwodził.

Edytowano przez Leander
  • Lubię! 2
Link do komentarza
Share on other sites

Zarejestruj się lub zaloguj, aby ukryć tę reklamę.
Zarejestruj się lub zaloguj, aby ukryć tę reklamę.

jlcpcb.jpg

jlcpcb.jpg

Produkcja i montaż PCB - wybierz sprawdzone PCBWay!
   • Darmowe płytki dla studentów i projektów non-profit
   • Tylko 5$ za 10 prototypów PCB w 24 godziny
   • Usługa projektowania PCB na zlecenie
   • Montaż PCB od 30$ + bezpłatna dostawa i szablony
   • Darmowe narzędzie do podglądu plików Gerber
Zobacz również » Film z fabryki PCBWay

Dołącz do dyskusji, napisz odpowiedź!

Jeśli masz już konto to zaloguj się teraz, aby opublikować wiadomość jako Ty. Możesz też napisać teraz i zarejestrować się później.
Uwaga: wgrywanie zdjęć i załączników dostępne jest po zalogowaniu!

Anonim
Dołącz do dyskusji! Kliknij i zacznij pisać...

×   Wklejony jako tekst z formatowaniem.   Przywróć formatowanie

  Dozwolonych jest tylko 75 emoji.

×   Twój link będzie automatycznie osadzony.   Wyświetlać jako link

×   Twoja poprzednia zawartość została przywrócona.   Wyczyść edytor

×   Nie możesz wkleić zdjęć bezpośrednio. Prześlij lub wstaw obrazy z adresu URL.

×
×
  • Utwórz nowe...

Ważne informacje

Ta strona używa ciasteczek (cookies), dzięki którym może działać lepiej. Więcej na ten temat znajdziesz w Polityce Prywatności.