portapack-mayhem/firmware/application/ui/ui_geomap.cpp
2023-02-28 20:14:15 +00:00

463 lines
12 KiB
C++

/*
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2017 Furrtek
*
* This file is part of PortaPack.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
#include "ui_geomap.hpp"
#include "portapack.hpp"
#include <cstring>
#include <stdio.h>
using namespace portapack;
#include "string_format.hpp"
#include "complex.hpp"
namespace ui {
GeoPos::GeoPos(
const Point pos,
const alt_unit altitude_unit
) : altitude_unit_(altitude_unit) {
set_parent_rect({pos, { 30 * 8, 3 * 16 }});
add_children({
&labels_position,
&field_altitude,
&text_alt_unit,
&field_lat_degrees,
&field_lat_minutes,
&field_lat_seconds,
&text_lat_decimal,
&field_lon_degrees,
&field_lon_minutes,
&field_lon_seconds,
&text_lon_decimal
});
// Defaults
set_altitude(0);
set_lat(0);
set_lon(0);
const auto changed_fn = [this](int32_t) {
float lat_value = lat();
float lon_value = lon();
text_lat_decimal.set(to_string_decimal(lat_value, 5));
text_lon_decimal.set(to_string_decimal(lon_value, 5));
if (on_change && report_change)
on_change(altitude(), lat_value, lon_value);
};
field_altitude.on_change = changed_fn;
field_lat_degrees.on_change = changed_fn;
field_lat_minutes.on_change = changed_fn;
field_lat_seconds.on_change = changed_fn;
field_lon_degrees.on_change = changed_fn;
field_lon_minutes.on_change = changed_fn;
field_lon_seconds.on_change = changed_fn;
text_alt_unit.set(altitude_unit_ ? "m" : "ft");
}
void GeoPos::set_read_only(bool v) {
for(auto child : children_)
child->set_focusable(!v);
}
// Stupid hack to avoid an event loop
void GeoPos::set_report_change(bool v) {
report_change = v;
}
void GeoPos::focus() {
field_altitude.focus();
}
void GeoPos::set_altitude(int32_t altitude) {
field_altitude.set_value(altitude);
}
void GeoPos::set_lat(float lat) {
field_lat_degrees.set_value(lat);
field_lat_minutes.set_value((uint32_t)abs(lat / (1.0 / 60)) % 60);
field_lat_seconds.set_value((uint32_t)abs(lat / (1.0 / 3600)) % 60);
}
void GeoPos::set_lon(float lon) {
field_lon_degrees.set_value(lon);
field_lon_minutes.set_value((uint32_t)abs(lon / (1.0 / 60)) % 60);
field_lon_seconds.set_value((uint32_t)abs(lon / (1.0 / 3600)) % 60);
}
float GeoPos::lat() {
if (field_lat_degrees.value() < 0) {
return -1 * ( -1 * field_lat_degrees.value() + (field_lat_minutes.value() / 60.0) + (field_lat_seconds.value() / 3600.0));
} else {
return field_lat_degrees.value() + (field_lat_minutes.value() / 60.0) + (field_lat_seconds.value() / 3600.0);
}
};
float GeoPos::lon() {
if (field_lon_degrees.value() < 0) {
return -1 * (-1 * field_lon_degrees.value() + (field_lon_minutes.value() / 60.0) + (field_lon_seconds.value() / 3600.0));
} else {
return field_lon_degrees.value() + (field_lon_minutes.value() / 60.0) + (field_lon_seconds.value() / 3600.0);
}
};
int32_t GeoPos::altitude() {
return field_altitude.value();
};
GeoMap::GeoMap(
Rect parent_rect
) : Widget { parent_rect }, markerListLen(0)
{
//set_focusable(true);
}
void GeoMap::paint(Painter& painter) {
u_int16_t line;
std::array<ui::Color, 240> map_line_buffer;
const auto r = screen_rect();
// Ony redraw map if it moved by at least 1 pixel
// or the markers list was updated
int x_diff = abs(x_pos-prev_x_pos);
int y_diff = abs(y_pos-prev_y_pos);
if (markerListUpdated || (x_diff>=3) || (y_diff>=3)) {
for (line = 0; line < r.height(); line++) {
map_file.seek(4 + ((x_pos + (map_width * (y_pos + line))) << 1));
map_file.read(map_line_buffer.data(), r.width() << 1);
display.draw_pixels({ 0, r.top() + line, r.width(), 1 }, map_line_buffer);
}
prev_x_pos = x_pos;
prev_y_pos = y_pos;
// Draw the other markers
for ( int i=0; i<markerListLen; ++i )
{
GeoMarker & item = markerList[i];
double lat_rad = sin(item.lat * pi / 180);
int x = (map_width * (item.lon+180)/360) - x_pos;
int y = (map_height - ((map_world_lon / 2 * log((1 + lat_rad) / (1 - lat_rad))) - map_offset)) - y_pos; // Offset added for the GUI
if ((x>=0) && (x<r.width()) &&
(y>10) && (y<r.height()) ) // Dont draw within symbol size of top
{
ui::Point itemPoint(x,y+r.top());
if(y>=32) { // Dont draw text if it would overlap top
// Text and symbol
draw_marker(painter, itemPoint, item.angle, item.tag, Color::blue(), Color::blue(), Color::magenta() );
} else {
// Only symbol
draw_bearing( itemPoint, item.angle, 10, Color::blue());
}
}
markerListUpdated = false;
} // Draw the other markers
}
//Draw the marker in the center
draw_marker(painter, r.center(), angle_, tag_, Color::red(), Color::white(), Color::black() );
}
bool GeoMap::on_touch(const TouchEvent event) {
if ((event.type == TouchEvent::Type::Start) && (mode_ == PROMPT)) {
set_highlighted(true);
if (on_move) {
Point p = event.point - screen_rect().center();
on_move(p.x() / 2.0 * lon_ratio, p.y() / 2.0 * lat_ratio);
return true;
}
}
return false;
}
void GeoMap::move(const float lon, const float lat) {
lon_ = lon;
lat_ = lat;
Rect map_rect = screen_rect();
// Using WGS 84/Pseudo-Mercator projection
x_pos = map_width * (lon_+180)/360 - (map_rect.width() / 2);
// Latitude calculation based on https://stackoverflow.com/a/10401734/2278659
double lat_rad = sin(lat * pi / 180);
y_pos = map_height - ((map_world_lon / 2 * log((1 + lat_rad) / (1 - lat_rad))) - map_offset) - 128; // Offset added for the GUI
// Cap position
if (x_pos > (map_width - map_rect.width()))
x_pos = map_width - map_rect.width();
if (y_pos > (map_height + map_rect.height()))
y_pos = map_height - map_rect.height();
}
bool GeoMap::init() {
auto result = map_file.open("ADSB/world_map.bin");
if (result.is_valid())
return false;
map_file.read(&map_width, 2);
map_file.read(&map_height, 2);
map_center_x = map_width >> 1;
map_center_y = map_height >> 1;
lon_ratio = 180.0 / map_center_x;
lat_ratio = -90.0 / map_center_y;
map_bottom = sin(-85.05 * pi / 180); // Map bitmap only goes from about -85 to 85 lat
map_world_lon = map_width / (2 * pi);
map_offset = (map_world_lon / 2 * log((1 + map_bottom) / (1 - map_bottom)));
return true;
}
void GeoMap::set_mode(GeoMapMode mode) {
mode_ = mode;
}
void GeoMap::draw_bearing(const Point origin, const uint16_t angle, uint32_t size, const Color color) {
Point arrow_a, arrow_b, arrow_c;
for (size_t thickness = 0; thickness < 3; thickness++) {
arrow_a = fast_polar_to_point((int)angle, size) + origin;
arrow_b = fast_polar_to_point((int)(angle + 180 - 35), size) + origin;
arrow_c = fast_polar_to_point((int)(angle + 180 + 35), size) + origin;
display.draw_line(arrow_a, arrow_b, color);
display.draw_line(arrow_b, arrow_c, color);
display.draw_line(arrow_c, arrow_a, color);
size--;
}
}
void GeoMap::draw_marker(Painter& painter, const ui::Point itemPoint, const uint16_t itemAngle, const std::string itemTag,
const Color color, const Color fontColor, const Color backColor )
{
int tagOffset = 10;
if (mode_ == PROMPT) {
// Cross
display.fill_rectangle({ itemPoint - Point(16, 1), { 32, 2 } }, color);
display.fill_rectangle({ itemPoint - Point(1, 16), { 2, 32 } }, color);
tagOffset = 16;
} else if (angle_ < 360){
//if we have a valid angle draw bearing
draw_bearing( itemPoint, itemAngle, 10, color);
tagOffset = 10;
}
else {
//draw a small cross
display.fill_rectangle({ itemPoint - Point(8, 1), { 16, 2 } }, color);
display.fill_rectangle({ itemPoint - Point(1, 8), { 2, 16 } }, color);
tagOffset = 8;
}
//center tag above point
if(itemTag.find_first_not_of(' ') != itemTag.npos){ //only draw tag if we have something other than spaces
painter.draw_string( itemPoint - Point(((int)itemTag.length() * 8 / 2), 14 + tagOffset ),
style().font, fontColor, backColor, itemTag);
}
}
void GeoMap::clear_markers()
{
markerListLen = 0;
}
MapMarkerStored GeoMap::store_marker(GeoMarker & marker)
{
MapMarkerStored ret;
// Check if it could be on screen
// Only checking one direction to reduce CPU
const auto r = screen_rect();
double lat_rad = sin(marker.lat * pi / 180);
int x = (map_width * (marker.lon+180)/360) - x_pos;
int y = (map_height - ((map_world_lon / 2 * log((1 + lat_rad) / (1 - lat_rad))) - map_offset)) - y_pos; // Offset added for the GUI
if (false==((x>=0) && (x<r.width()) && (y>10) && (y<r.height()))) // Dont draw within symbol size of top
{
ret = MARKER_NOT_STORED;
}
else if (markerListLen<NumMarkerListElements)
{
markerList[markerListLen] = marker;
markerListLen++;
markerListUpdated = true;
ret = MARKER_STORED;
} else {
ret = MARKER_LIST_FULL;
}
return ret;
}
void GeoMapView::focus() {
geopos.focus();
if (!map_opened)
nav_.display_modal("No map", "No world_map.bin file in\n/ADSB/ directory", ABORT, nullptr);
}
void GeoMapView::update_position(float lat, float lon, uint16_t angle, int32_t altitude) {
lat_ = lat;
lon_ = lon;
altitude_ = altitude;
// Stupid hack to avoid an event loop
geopos.set_report_change(false);
geopos.set_lat(lat_);
geopos.set_lon(lon_);
geopos.set_altitude(altitude_);
geopos.set_report_change(true);
geomap.set_angle(angle);
geomap.move(lon_, lat_);
geomap.set_dirty();
}
void GeoMapView::update_tag(const std::string tag) {
geomap.set_tag(tag);
}
void GeoMapView::setup() {
add_child(&geomap);
geopos.set_altitude(altitude_);
geopos.set_lat(lat_);
geopos.set_lon(lon_);
geopos.on_change = [this](int32_t altitude, float lat, float lon) {
altitude_ = altitude;
lat_ = lat;
lon_ = lon;
geomap.move(lon_, lat_);
geomap.set_dirty();
};
geomap.on_move = [this](float move_x, float move_y) {
lon_ += move_x;
lat_ += move_y;
// Stupid hack to avoid an event loop
geopos.set_report_change(false);
geopos.set_lon(lon_);
geopos.set_lat(lat_);
geopos.set_report_change(true);
geomap.move(lon_, lat_);
geomap.set_dirty();
};
}
GeoMapView::~GeoMapView() {
if (on_close_)
on_close_();
}
// Display mode
GeoMapView::GeoMapView(
NavigationView& nav,
const std::string& tag,
int32_t altitude,
GeoPos::alt_unit altitude_unit,
float lat,
float lon,
uint16_t angle,
const std::function<void(void)> on_close
) : nav_ (nav),
altitude_ (altitude),
altitude_unit_ (altitude_unit),
lat_ (lat),
lon_ (lon),
angle_ (angle),
on_close_(on_close)
{
mode_ = DISPLAY;
add_child(&geopos);
map_opened = geomap.init();
if (!map_opened) return;
setup();
geomap.set_mode(mode_);
geomap.set_tag(tag);
geomap.set_angle(angle);
geomap.move(lon_, lat_);
geopos.set_read_only(true);
}
// Prompt mode
GeoMapView::GeoMapView(
NavigationView& nav,
int32_t altitude,
GeoPos::alt_unit altitude_unit,
float lat,
float lon,
const std::function<void(int32_t, float, float)> on_done
) : nav_ (nav),
altitude_ (altitude),
altitude_unit_ (altitude_unit),
lat_ (lat),
lon_ (lon)
{
mode_ = PROMPT;
add_child(&geopos);
map_opened = geomap.init();
if (!map_opened) return;
setup();
add_child(&button_ok);
geomap.set_mode(mode_);
geomap.move(lon_, lat_);
button_ok.on_select = [this, on_done, &nav](Button&) {
if (on_done)
on_done(altitude_, lat_, lon_);
nav.pop();
};
}
void GeoMapView::clear_markers()
{
geomap.clear_markers();
}
MapMarkerStored GeoMapView::store_marker(GeoMarker & marker)
{
return geomap.store_marker(marker);
}
} /* namespace ui */