kronikary Napisano Marzec 29, 2021 Udostępnij Napisano Marzec 29, 2021 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. 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. 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: 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. 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. 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: 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. 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ń: 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. 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. 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. 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: 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. 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. 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: Przetwarzanie OpenStreetMap na systemie wbudowanym #1 - Format .osm Przetwarzanie OpenStreetMap na systemie wbudowanym #2 - Parsowanie formatu .osm Przetwarzanie OpenStreetMap na systemie wbudowanym #3 - Kodowanie formatu .ltd Przetwarzanie OpenStreetMap na systemie wbudowanym #4 - Dekodowanie formatu .ltd 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 2 Link do komentarza Share on other sites More sharing options...
Treker (Damian Szymański) Kwiecień 3, 2021 Udostępnij Kwiecień 3, 2021 @kronikary wpis został właśnie zaakceptowany i jest już widoczny publicznie 🚀 1 Link do komentarza Share on other sites More sharing options...
Leander Kwiecień 3, 2021 Udostępnij Kwiecień 3, 2021 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. 1 Link do komentarza Share on other sites More sharing options...
Popularny post Leander Kwiecień 3, 2021 Popularny post Udostępnij Kwiecień 3, 2021 (edytowany) 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 Kwiecień 3, 2021 przez Leander 4 Link do komentarza Share on other sites More sharing options...
Polecacz 101 Zarejestruj się lub zaloguj, aby ukryć tę reklamę. Zarejestruj się lub zaloguj, aby ukryć tę reklamę. 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
Pomocna odpowiedź
Bądź aktywny - zaloguj się lub utwórz konto!
Tylko zarejestrowani użytkownicy mogą komentować zawartość tej strony
Utwórz konto w ~20 sekund!
Zarejestruj nowe konto, to proste!
Zarejestruj się »Zaloguj się
Posiadasz własne konto? Użyj go!
Zaloguj się »