Пишем программу для работы с GPS-приемником на с++

Задача заключается в написании программы, которая бы взаимодействовала с внешним gps приемником через COM-порт и отмечала точкой на карте нашу текущую позицию (позицию gps-приемника). В статье будут рассмотрены такие моменты, как получение данных с gps-приемника,  определение номенклатуры требуемой карты масштаба 1:100 000,  zoom карты и просмотр карты с помощью Drag and Drop.

UP:  была ошибка при определении номенклатуры, перезалил архив и исправил статью.
Кому лень качать: в строчке, где «lvl_2=QString::number(((temp.dg_o==»E»)?sector_dg:sector_dg+30)+1);» замените «E» на «W».

UPUP:  при успешной загрузке картинки с картой в статус-бар  писалось сообщение «Ошибка загрузки изображения карты». Исправляем, добавляя отрицание «!» в условие: 

В данной статье рассмотрен пример для Qt5 и выше. Библиотека QtSerialPort в 4-й версии Qt отсутствует, и для её подключения требуются дополнительные махинации, которые здесь рассмотрены не будут. Для начала добавьте следующий код в *.pro файл:

Работа с COM-портом на удивление очень проста. Для этого мы создаем объект класса QSerialPort, и подписываем требуемые слоты на сигнал readyRead() созданного объекта. П.С. я подключал GPS-приемник к USB-порту

Например так:

Вот собственно и вся работа с gps приемником. Как только к нам приходит информация с приемника, рассылается сигнал readyRead() всем подписчикам, в нашем случае подписчиком на сигнал является слот read(). С информацией, хранящейся в переменной data, можно работать как с обычной строкой.

И так, теперь приступим к написанию самой программы в целом.

*.cpp файл приведен в самом низу.

(1), (2) — карта масштаба 1:100 000 имеет размеры 30 минут по долготе и 20 по широте. Соответственно, для определения координат точки на карте нам необходимы лишь минуты и то, которые надо обработать. По долготе нам нужен остаток от деления общего количества минут на 30, по широте — на 20:

Если у кого-то возник вопрос «почему не использовать оператор %», то отвечу — оператор остатка от деления % предназначен только для целых чисел.

Конструктор класса MainWindow:

Функция connect_port() управляет состоянием соединения с портом — для получения более подробной информации читайте комментарии к коду

Функция setPropertyPort() устанавливает параметры для работы с портом по умолчанию, о чем я писал в самом начале:

Когда поступают данные с порта создается сигнал readyRead(), на который подписан слот read():

Мы считываем данные с порта, и если если стоит птичка в чекбоксе ch_gpgga, то выводить только строки содержащие GPGGA, если нет — выводить все что поступает с приемника. Полученные данные передаем функции filter():

Мне с приемника поступала информация то посимвольно, то кусками фраз, в общем странно как-то: то ли я что-то не так настроил, то ли так и должно быть. По этому все символы я собираю воедино в переменной filter_txt. Разбиваю по «\n» (скрытый символ переноса строки) в массив строк и каждую строку обрабатываю. Из всего потока информации нас интересует только сообщение GPGGA, содержащее наши координаты. Что-то наподобие этого:

Если разбить строку по пробелам в массив, то географическая широта будет вторым элементом (нумерация массива с нуля),  географическая долгота — четвертым.  Третий элемент — если N, то северная широта, если S, то южная. Пятым элементом является тоже буква, обозначающая западную/восточную долготу (W/E). И для общего развития:

1 — время;

9 — высота;

10 — единица измерения высоты.

Возвращаемся к нашей функции filter. Найдя строку с GPGGA, вытаскиваю оттуда наши координаты и если они корректны, то создаем сигнал event_new_point.

Зачем нам нужен таймер (emulator_timer) ? — Возможно не у каждого под рукой есть GPS-приемник, по этому мы напишем функцию, которая раз в секунду будет создавать сигнал о новой точке — функция emulator():

Эта функция довольна примитивна в ней всего-лишь создаются два сигнала, первый — event_new_point(….), он говорит подписчикам, что появились новые координаты точки, которые надо обработать, вторая — event_write_console(….) , говорит, что надо вывести в нашу «консоль» (текстовое поле text) соответствующее сообщение. В идеале этот «эмулятор» должен был бы генерировать разные координаты, а не статические. Но вставить генератор случайных чисел qrand() вместо минут в координатах проблем составить не должно.

На сигнал event_new_point подписан слот new_point, который запихивает входящие данные в структуру _data и передает её на дальнейшее терзание другими функциями:

 Мы должны отделить минуты, и подготовить данные для вычисления номенклатуры.Теперь немного по сложнее — функция coords(_data). Широта и долгота с приемника поступают в довольно интересном виде: широта — dddd.dd (первые две цифры — градусы, остальное — минуты), долгота — ddddd.dd (первые 3 цифры — градусы, остальное — минуты) — хотя я по прежнему думаю, что после точки идут секунды.

Здесь все просто. Умножаем число на 100, чтобы минуты оказались после запятой (например, 5002.5528 => 50.025528). Разделяем строку по точке на две части. Теперь в одной части у нас только градусы, во второй минуты. Градусы преобразуем в число и сохраняем в структуру,  а к минутам в начале приписываем «0.» (например, «025528» =>»0.025528″) преобразуем число и умножаем на 100 (0.025528 => 2.5528) и тоже сохраняем в структуру. В дальнейшем для определения номенклатуры карты нам понадобятся только градусы, по этому к отсеченным ранее градусам прибавляем минуты деленные на 60 (60 минут = 1 градус). Лень проверять, но что-то мне подсказывает, что номенклатура и без минут буде правильно определяться.

Теперь интереснее «temp.sdg_min=temp.dg_min-((int)temp.dg_min/30)*30;».  Ранее я уже комментировал эту строку, но все же. Мы работаем с картой масштаба 1:1000 000: по широте это 20′, по долготе 30′. Делаем из этого вывод, что для отображения точки на карте нам градусы вообще не нужны, а нужны только минуты. А именно остаток от деления на 30 — для долготы, на 20 — для широты. Если мы не будем это учитывать, и, например, будем брать 32 минуты по широте, вместо правильных 12 — это уже другая карта.

Функция coords внутри себя вызывает функцию num_dot(_data) передавая ей ту же структуру:

Без комментариев, определение и принцип формирования номенклатуры читайте на википедииwikipedia.

Функция write() предназначена для добавления строки в таблицу table с координатами нашей точки:

Ничего необычного нет, идем дальше. Теперь собственно функция для отрисовки точки на карте — функция  draw_dot().

Прокомментирую только » if(temp.sh_o==»N»)y=h-dY*temp.ssh_min;else y=dY*temp.ssh_min;». Вспоминаем, что экватор —  ноль градусов широты. Простой пример: 40° северной широты откладываются от экватора в верх, а 40° южной широты — от экватора и вниз. Теперь вспоминаем, что положительное направление оси ординат (широта) в оконной системе координат является сверху вниз — как раз, как если бы координаты откладывались в южном полушарии. А в случае с северной широтой, мы отнимаем от высоты h значение dY*temp.ssh_min.

Теперь займемся украшениями. Для того чтобы сделать масштабирование карты с помощью колесика мышки, нам необходимо отфильтровать событие типа QEvent::Wheel. Для этого мы создаем свой виджет, унаследовав класс QGraphicsView:

И в функции eventFilter пишем следующее

Значение wEvent->delta() положительно, если колесико было прокручено вверх, и отрицательно — если вниз.

Для Drag and Drop, перетаскивание карты нажатием мыши, достаточно в конструкторе прописать this->setDragMode(QGraphicsView::ScrollHandDrag); Ну и для красоты спрячем скролл-бары:

Собрав это все в кучу получаем следующий mainwindow.cpp

Screenshot_1

Если нужной карты в архиве нет, ищем карту масштаба 1:1000 000, даем ей название <Номенклатура>.jpg и кидаем в images/maps/

Если у вас не отображаются изображения:

Основы навигации

Скопируйте папку image в папку сборки. В моем случае это было сюда:

path