1
0
mirror of https://github.com/f4exb/sdrangel.git synced 2024-11-03 23:41:16 -05:00
sdrangel/plugins/feature/map/mapmodel.cpp
Jon Beniston 9c7aa8b333 Map Updates
Allow OpenSkyNetwork DB, OpenAIP and OurAirports DB stuctures to be
shared by different plugins, to speed up loading.
Perform map anti-aliasing on the whole map, rather than just info boxes,
to improve rendering speed when there are many items. Add map
multisampling as a preference.
Add plotting of airspaces, airports, navaids on Map feature.
Add support for polylines and polygons to be plotted on Map feature.
Add support for images to 2D Map feature.
Add distance and name filters to Map feature.
Filter map items when zoomed out or if off screen, to improve rendering
performance.
Add UK DAB, FM and AM transmitters to Map feature.
Use labelless maps for 2D transmit maps in Map feature (same as in ADS-B
demod).
2023-02-14 14:46:08 +00:00

1365 lines
46 KiB
C++

///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2021 Jon Beniston, M7RCE //
// //
// 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 as version 3 of the License, 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 V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include <QGeoRectangle>
#include "channel/channelwebapiutils.h"
#include "maincore.h"
#include "mapmodel.h"
#include "mapgui.h"
#include "map.h"
#include "SWGTargetAzimuthElevation.h"
QVariant MapModel::data(const QModelIndex &index, int role) const
{
int row = index.row();
if ((row < 0) || (row >= m_items.count())) {
return QVariant();
}
switch (role)
{
case itemSettingsRole:
return QVariant::fromValue(m_items[row]->m_itemSettings);
case nameRole:
return QVariant::fromValue(m_items[row]->m_name);
case labelRole:
return QVariant::fromValue(m_items[row]->m_label);
case positionRole:
{
// Coordinates to display the label at
QGeoCoordinate coords;
coords.setLatitude(m_items[row]->m_latitude);
coords.setLongitude(m_items[row]->m_longitude);
coords.setAltitude(m_items[row]->m_altitude);
return QVariant::fromValue(coords);
}
case mapImageMinZoomRole:
// Minimum zoom level at which this is visible
return QVariant::fromValue(m_items[row]->m_itemSettings->m_2DMinZoom);
default:
return QVariant();
}
}
bool MapModel::setData(const QModelIndex &index, const QVariant& value, int role)
{
(void) value;
(void) role;
int row = index.row();
if ((row < 0) || (row >= m_items.count())) {
return false;
}
return true;
}
void MapModel::add(MapItem *item)
{
beginInsertRows(QModelIndex(), rowCount(), rowCount());
m_items.append(item);
m_itemsHash.insert(item->m_hashKey, item);
endInsertRows();
}
void MapModel::update(const QObject *sourcePipe, SWGSDRangel::SWGMapItem *swgMapItem, const QString &group)
{
QString name = *swgMapItem->getName();
// Add, update or delete and item
MapItem *item = findMapItem(sourcePipe, name);
if (item != nullptr)
{
QString image = *swgMapItem->getImage();
if (image.isEmpty())
{
// Delete the item
remove(item);
// Need to call update, for it to be removed in 3D map
// Item is set to not be available from this point in time
// It will still be available if time is set in the past
item->update(swgMapItem);
}
else
{
// Update the item
item->update(swgMapItem);
update(item);
}
}
else
{
// Make sure not a duplicate request to delete
QString image = *swgMapItem->getImage();
if (!image.isEmpty())
{
// Add new item
item = newMapItem(sourcePipe, group, m_gui->getItemSettings(group), swgMapItem);
add(item);
// Add to 3D Map (we don't appear to get a dataChanged signal when adding)
update3D(item);
}
}
}
void MapModel::update(MapItem *item)
{
int row = m_items.indexOf(item);
if (row >= 0)
{
QModelIndex idx = index(row);
emit dataChanged(idx, idx);
}
}
void MapModel::remove(MapItem *item)
{
int row = m_items.indexOf(item);
if (row >= 0)
{
QString key = m_items[row]->m_hashKey;
beginRemoveRows(QModelIndex(), row, row);
m_items.removeAt(row);
m_itemsHash.remove(key);
endRemoveRows();
}
}
void MapModel::removeAll()
{
if (m_items.count() > 0)
{
beginRemoveRows(QModelIndex(), 0, m_items.count() - 1);
m_items.clear();
m_itemsHash.clear();
endRemoveRows();
}
}
// After new settings are deserialised, we need to update
// pointers to item settings for all existing items
void MapModel::updateItemSettings(QHash<QString, MapSettings::MapItemSettings *> m_itemSettings)
{
for (auto item : m_items)
{
if (m_itemSettings.contains(item->m_group)) {
item->m_itemSettings = m_itemSettings[item->m_group];
}
}
}
void MapModel::allUpdated()
{
if (m_items.count() > 0) {
emit dataChanged(index(0), index(m_items.count()-1));
}
}
MapItem *MapModel::findMapItem(const QObject *source, const QString& name)
{
QString key = source->objectName() + name;
if (m_itemsHash.contains(key)) {
return m_itemsHash.value(key);
}
return nullptr;
}
QHash<int, QByteArray> MapModel::roleNames() const
{
QHash<int, QByteArray> roles;
roles[itemSettingsRole] = "itemSettings";
roles[nameRole] = "name";
roles[labelRole] = "label";
roles[positionRole] = "position";
roles[mapImageMinZoomRole] = "mapImageMinZoom";
return roles;
}
// Slot called on dataChanged signal, to update 3D map
void MapModel::update3DMap(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector<int> &roles)
{
(void) roles;
for (int row = topLeft.row(); row <= bottomRight.row(); row++) {
update3D(m_items[row]);
}
}
MapItem *ImageMapModel::newMapItem(const QObject *sourcePipe, const QString &group, MapSettings::MapItemSettings *itemSettings, SWGSDRangel::SWGMapItem *mapItem)
{
return new ImageMapItem(sourcePipe, group, itemSettings, mapItem);
}
QVariant ImageMapModel::data(const QModelIndex &index, int role) const
{
int row = index.row();
if ((row < 0) || (row >= m_items.count())) {
return QVariant();
}
ImageMapItem *mapItem = (ImageMapItem*)m_items[row];
switch (role)
{
case imageRole:
return QVariant::fromValue(mapItem->m_image);
case imageZoomLevelRole:
return QVariant::fromValue(mapItem->m_imageZoomLevel);
case boundsRole:
return QVariant::fromValue(mapItem->m_bounds);
default:
return MapModel::data(index, role);
}
}
void ImageMapModel::update3D(MapItem *item)
{
CesiumInterface *cesium = m_gui->cesium();
if (cesium)
{
ImageMapItem *imageItem = (ImageMapItem *)item;
if (!imageItem->m_image.isEmpty())
{
/*qDebug() << "ImageMapModel::update3D - " << imageItem->m_name
<< imageItem->m_bounds.right()
<< imageItem->m_bounds.left()
<< imageItem->m_bounds.top()
<< imageItem->m_bounds.bottom()
; */
cesium->updateImage(imageItem->m_name,
imageItem->m_bounds.topRight().longitude(),
imageItem->m_bounds.bottomLeft().longitude(),
imageItem->m_bounds.topRight().latitude(),
imageItem->m_bounds.bottomLeft().latitude(),
imageItem->m_altitude,
imageItem->m_image);
}
else
{
qDebug() << "ImageMapModel::update3D - removeImage " << imageItem->m_name;
cesium->removeImage(imageItem->m_name);
}
}
}
MapItem *PolygonMapModel::newMapItem(const QObject *sourcePipe, const QString &group, MapSettings::MapItemSettings *itemSettings, SWGSDRangel::SWGMapItem *mapItem)
{
return new PolygonMapItem(sourcePipe, group, itemSettings, mapItem);
}
QVariant PolygonMapModel::data(const QModelIndex &index, int role) const
{
int row = index.row();
if ((row < 0) || (row >= m_items.count())) {
return QVariant();
}
PolygonMapItem *polygonItem = ((PolygonMapItem *)m_items[row]);
switch (role)
{
case borderColorRole:
return QVariant::fromValue(QColor(0x00, 0x00, 0x00, 0x00)); // Transparent
case fillColorRole:
if (m_items[row]->m_itemSettings->m_display2DTrack) {
return QVariant::fromValue(QColor::fromRgba(m_items[row]->m_itemSettings->m_2DTrackColor));
} else {
return QVariant::fromValue(QColor(0x00, 0x00, 0x00, 0x00)); // Transparent
}
case polygonRole:
return polygonItem->m_polygon;
case boundsRole:
return QVariant::fromValue(polygonItem->m_bounds);
default:
return MapModel::data(index, role);
}
}
void PolygonMapModel::update3D(MapItem *item)
{
CesiumInterface *cesium = m_gui->cesium();
if (cesium) {
cesium->update((PolygonMapItem *)item);
}
}
MapItem *PolylineMapModel::newMapItem(const QObject *sourcePipe, const QString &group, MapSettings::MapItemSettings *itemSettings, SWGSDRangel::SWGMapItem *mapItem)
{
return new PolylineMapItem(sourcePipe, group, itemSettings, mapItem);
}
QVariant PolylineMapModel::data(const QModelIndex &index, int role) const
{
int row = index.row();
if ((row < 0) || (row >= m_items.count())) {
return QVariant();
}
PolylineMapItem *polylineItem = (PolylineMapItem *)m_items[row];
switch (role)
{
case lineColorRole:
return QVariant::fromValue(QColor::fromRgba(m_items[row]->m_itemSettings->m_2DTrackColor));
case coordinatesRole:
return QVariant::fromValue(polylineItem->m_polyline);
case boundsRole:
return QVariant::fromValue(polylineItem->m_bounds);
default:
return MapModel::data(index, role);
}
}
void PolylineMapModel::update3D(MapItem *item)
{
CesiumInterface *cesium = m_gui->cesium();
if (cesium) {
cesium->update((PolylineMapItem *)item);
}
}
ObjectMapModel::ObjectMapModel(MapGUI *gui) :
MapModel(gui),
m_target(-1)
{
//connect(this, &ObjectMapModel::dataChanged, this, &ObjectMapModel::update3DMap);
}
Q_INVOKABLE void ObjectMapModel::add(MapItem *item)
{
m_selected.append(false);
MapModel::add(item);
}
/*void ObjectMapModel::update(const QObject *sourcePipe, SWGSDRangel::SWGMapItem *swgMapItem, const QString &group)
{
QString name = *swgMapItem->getName();
// Add, update or delete and item
ObjectMapItem *item = (ObjectMapItem *)MapModel::findMapItem(sourcePipe, name);
if (item != nullptr)
{
QString image = *swgMapItem->getImage();
if (image.isEmpty())
{
// Delete the item
remove(item);
// Need to call update, for it to be removed in 3D map
// Item is set to not be available from this point in time
// It will still be available if time is set in the past
item->update(swgMapItem);
}
else
{
// Update the item
item->update(swgMapItem);
splitTracks(item); // ******** diff from base FIXME
update(item);
}
}
else
{
// Make sure not a duplicate request to delete
QString image = *swgMapItem->getImage();
if (!image.isEmpty())
{
// Add new item
item = (ObjectMapItem *)newMapItem(sourcePipe, group, m_gui->getItemSettings(group), swgMapItem);
add(item);
// Add to 3D Map (we don't appear to get a dataChanged signal when adding)
update3D(item);
}
}
}*/
MapItem *ObjectMapModel::newMapItem(const QObject *sourcePipe, const QString &group, MapSettings::MapItemSettings *itemSettings, SWGSDRangel::SWGMapItem *mapItem)
{
return new ObjectMapItem(sourcePipe, group, itemSettings, mapItem);
}
void ObjectMapModel::update3D(MapItem *item)
{
CesiumInterface *cesium = m_gui->cesium();
if (cesium)
{
ObjectMapItem *objectMapItem = (ObjectMapItem *)item;
cesium->update(objectMapItem, isTarget(objectMapItem), isSelected3D(objectMapItem));
playAnimations(objectMapItem);
}
}
// Slot called on dataChanged signal, to update 3D map
/*void ObjectMapModel::update3DMap(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector<int> &roles)
{
(void) roles;
CesiumInterface *cesium = m_gui->cesium();
if (cesium)
{
for (int row = topLeft.row(); row <= bottomRight.row(); row++)
{
ObjectMapItem *item = (ObjectMapItem *)m_items[row];
cesium->update(item, isTarget(item), isSelected3D(item));
playAnimations(item);
}
}
} */
void ObjectMapModel::playAnimations(ObjectMapItem *item)
{
CesiumInterface *cesium = m_gui->cesium();
if (cesium)
{
for (auto animation : item->m_animations) {
m_gui->cesium()->playAnimation(item->m_name, animation);
}
}
qDeleteAll(item->m_animations);
item->m_animations.clear();
}
void ObjectMapModel::update(MapItem *item)
{
splitTracks((ObjectMapItem *)item);
MapModel::update(item);
int row = m_items.indexOf(item);
if (row >= 0)
{
if (row == m_target) {
updateTarget();
}
}
}
void ObjectMapModel::remove(MapItem *item)
{
int row = m_items.indexOf(item);
if (row >= 0)
{
m_selected.removeAt(row);
if (row == m_target) {
m_target = -1;
} else if (row < m_target) {
m_target--;
}
MapModel::remove(item);
}
}
void ObjectMapModel::removeAll()
{
MapModel::removeAll();
m_selected.clear();
}
QHash<int, QByteArray> ObjectMapModel::roleNames() const
{
QHash<int, QByteArray> roles = MapModel::roleNames();
//roles[itemSettingsRole] = "itemSettings";
//roles[nameRole] = "name";
//roles[positionRole] = "position";
//roles[mapTextRole] = "mapText";
roles[MapModel::labelRole] = "mapText";
roles[mapTextVisibleRole] = "mapTextVisible";
roles[mapImageVisibleRole] = "mapImageVisible";
roles[mapImageRole] = "mapImage";
roles[mapImageRotationRole] = "mapImageRotation";
//roles[mapImageMinZoomRole] = "mapImageMinZoom";
roles[bubbleColourRole] = "bubbleColour";
roles[selectedRole] = "selected";
roles[targetRole] = "target";
roles[frequencyRole] = "frequency";
roles[frequencyStringRole] = "frequencyString";
roles[predictedGroundTrack1Role] = "predictedGroundTrack1";
roles[predictedGroundTrack2Role] = "predictedGroundTrack2";
roles[groundTrack1Role] = "groundTrack1";
roles[groundTrack2Role] = "groundTrack2";
roles[groundTrackColorRole] = "groundTrackColor";
roles[predictedGroundTrackColorRole] = "predictedGroundTrackColor";
roles[hasTracksRole] = "hasTracks";
return roles;
}
void ObjectMapModel::updateTarget()
{
// Calculate range, azimuth and elevation to object from station
AzEl *azEl = m_gui->getAzEl();
azEl->setTarget(m_items[m_target]->m_latitude, m_items[m_target]->m_longitude, m_items[m_target]->m_altitude);
azEl->calculate();
// Send to Rotator Controllers
QList<ObjectPipe*> rotatorPipes;
MainCore::instance()->getMessagePipes().getMessagePipes(this, "target", rotatorPipes);
for (const auto& pipe : rotatorPipes)
{
MessageQueue *messageQueue = qobject_cast<MessageQueue*>(pipe->m_element);
SWGSDRangel::SWGTargetAzimuthElevation *swgTarget = new SWGSDRangel::SWGTargetAzimuthElevation();
swgTarget->setName(new QString(m_items[m_target]->m_name));
swgTarget->setAzimuth(azEl->getAzimuth());
swgTarget->setElevation(azEl->getElevation());
messageQueue->push(MainCore::MsgTargetAzimuthElevation::create(m_gui->getMap(), swgTarget));
}
}
void ObjectMapModel::setTarget(const QString& name)
{
if (name.isEmpty())
{
QModelIndex idx = index(-1);
setData(idx, QVariant(-1), ObjectMapModel::targetRole);
}
else
{
QModelIndex idx = findMapItemIndex(name);
setData(idx, QVariant(idx.row()), ObjectMapModel::targetRole);
}
}
bool ObjectMapModel::isTarget(const ObjectMapItem *mapItem) const
{
if (m_target < 0) {
return false;
} else {
return m_items[m_target] == mapItem;
}
}
// FIXME: This should use Z order - rather than adding/removing
// but I couldn't quite get it to work
Q_INVOKABLE void ObjectMapModel::moveToFront(int oldRow)
{
// Last item in list is drawn on top, so remove than add to end of list
if (oldRow < m_items.size() - 1)
{
bool wasTarget = m_target == oldRow;
ObjectMapItem *item = (ObjectMapItem *)m_items[oldRow];
bool wasSelected = m_selected[oldRow];
remove(item);
add(item);
int newRow = m_items.size() - 1;
if (wasTarget) {
m_target = newRow;
}
m_selected[newRow] = wasSelected;
QModelIndex idx = index(newRow);
emit dataChanged(idx, idx);
}
}
Q_INVOKABLE void ObjectMapModel::moveToBack(int oldRow)
{
// First item in list is drawn first, so remove item then add to front of list
if ((oldRow < m_items.size()) && (oldRow > 0))
{
bool wasTarget = m_target == oldRow;
int newRow = 0;
// See: https://forum.qt.io/topic/122991/changing-the-order-mapquickitems-are-drawn-on-a-map
//QModelIndex parent;
//beginMoveRows(parent, oldRow, oldRow, parent, newRow);
beginResetModel();
m_items.move(oldRow, newRow);
m_selected.move(oldRow, newRow);
if (wasTarget) {
m_target = newRow;
} else if (m_target >= 0) {
m_target++;
}
//endMoveRows();
endResetModel();
//emit dataChanged(index(oldRow), index(newRow));
}
}
// FIXME: This should potentially return a list, as we have have multiple items with the same name
// from different sources
ObjectMapItem *ObjectMapModel::findMapItem(const QString& name)
{
QListIterator<MapItem *> i(m_items);
while (i.hasNext())
{
MapItem *item = i.next();
if (item->m_name == name) {
return (ObjectMapItem *)item;
}
}
return nullptr;
}
QModelIndex ObjectMapModel::findMapItemIndex(const QString& name)
{
int idx = 0;
QListIterator<MapItem *> i(m_items);
while (i.hasNext())
{
MapItem *item = i.next();
if (item->m_name == name) {
return index(idx);
}
idx++;
}
return index(-1);
}
QVariant ObjectMapModel::data(const QModelIndex &index, int role) const
{
int row = index.row();
if ((row < 0) || (row >= m_items.count())) {
return QVariant();
}
ObjectMapItem *mapItem = (ObjectMapItem*)m_items[row];
switch (role)
{
case labelRole: // mapTextRole)
{
// Create the text to go in the bubble next to the image
QString name = mapItem->m_label.isEmpty() ? mapItem->m_name :mapItem->m_label;
if (row == m_target)
{
AzEl *azEl = m_gui->getAzEl();
QString text = QString("%1<br>Az: %2%5 El: %3%5 Dist: %4 km<br>Coords: %6, %7")
.arg(m_selected[row] ? mapItem->m_text : name)
.arg(std::round(azEl->getAzimuth()))
.arg(std::round(azEl->getElevation()))
.arg(std::round(azEl->getDistance() / 1000.0))
.arg(QChar(0xb0))
.arg(mapItem->m_latitude)
.arg(mapItem->m_longitude);
return QVariant::fromValue(text);
}
else if (m_selected[row])
{
return QVariant::fromValue(mapItem->m_text);
}
else
{
return QVariant::fromValue(name);
}
}
case mapTextVisibleRole:
return QVariant::fromValue((m_selected[row] || m_displayNames) && mapItem->m_itemSettings->m_display2DLabel);
case mapImageVisibleRole:
return QVariant::fromValue(mapItem->m_itemSettings->m_display2DIcon);
case mapImageRole:
return QVariant::fromValue(mapItem->m_image); // Set an image to use
case mapImageRotationRole:
return QVariant::fromValue(mapItem->m_imageRotation); // Angle to rotate image by
case bubbleColourRole:
{
// Select a background colour for the text bubble next to the item
if (m_selected[row]) {
return QVariant::fromValue(QColor("lightgreen"));
} else {
return QVariant::fromValue(QColor("lightblue"));
}
}
case selectedRole:
return QVariant::fromValue(m_selected[row]);
case targetRole:
return QVariant::fromValue(m_target == row);
case frequencyRole:
return QVariant::fromValue(mapItem->m_frequency);
case frequencyStringRole:
return QVariant::fromValue(mapItem->m_frequencyString);
case predictedGroundTrack1Role:
{
if ( (m_displayAllGroundTracks || (m_displaySelectedGroundTracks && m_selected[row]))
&& mapItem->m_itemSettings->m_display2DTrack) {
return mapItem->m_predictedTrack1;
} else {
return QVariantList();
}
}
case predictedGroundTrack2Role:
{
if ( (m_displayAllGroundTracks || (m_displaySelectedGroundTracks && m_selected[row]))
&& mapItem->m_itemSettings->m_display2DTrack) {
return mapItem->m_predictedTrack2;
} else {
return QVariantList();
}
}
case groundTrack1Role:
{
if ( (m_displayAllGroundTracks || (m_displaySelectedGroundTracks && m_selected[row]))
&& mapItem->m_itemSettings->m_display2DTrack) {
return mapItem->m_takenTrack1;
} else {
return QVariantList();
}
}
case groundTrack2Role:
{
if ( (m_displayAllGroundTracks || (m_displaySelectedGroundTracks && m_selected[row]))
&& mapItem->m_itemSettings->m_display2DTrack) {
return mapItem->m_takenTrack2;
} else {
return QVariantList();
}
}
case groundTrackColorRole:
return QVariant::fromValue(QColor::fromRgb(mapItem->m_itemSettings->m_2DTrackColor));
case predictedGroundTrackColorRole:
return QVariant::fromValue(QColor::fromRgb(mapItem->m_itemSettings->m_2DTrackColor).lighter());
case hasTracksRole:
{
bool hasTracks = (m_displayAllGroundTracks || (m_displaySelectedGroundTracks && m_selected[row]))
&& ( (mapItem->m_predictedTrack1.size() > 1)
|| (mapItem->m_predictedTrack2.size() > 1)
|| (mapItem->m_takenTrack1.size() > 1)
|| (mapItem->m_takenTrack2.size() > 1)
);
return QVariant::fromValue(hasTracks);
}
default:
return MapModel::data(index, role);
}
}
bool ObjectMapModel::setData(const QModelIndex &idx, const QVariant& value, int role)
{
int row = idx.row();
if ((row < 0) || (row >= m_items.count()))
return false;
if (role == selectedRole)
{
m_selected[row] = value.toBool();
emit dataChanged(idx, idx);
return true;
}
else if (role == targetRole)
{
if (m_target >= 0)
{
// Update text bubble for old target
QModelIndex oldIdx = index(m_target);
m_target = -1;
emit dataChanged(oldIdx, oldIdx);
}
m_target = row;
updateTarget();
emit dataChanged(idx, idx);
return true;
}
return true;
}
void ObjectMapModel::setDisplayNames(bool displayNames)
{
m_displayNames = displayNames;
allUpdated();
}
void ObjectMapModel::setDisplaySelectedGroundTracks(bool displayGroundTracks)
{
m_displaySelectedGroundTracks = displayGroundTracks;
allUpdated();
}
void ObjectMapModel::setDisplayAllGroundTracks(bool displayGroundTracks)
{
m_displayAllGroundTracks = displayGroundTracks;
allUpdated();
}
void ObjectMapModel::setFrequency(double frequency)
{
// Set as centre frequency
ChannelWebAPIUtils::setCenterFrequency(0, frequency);
}
void ObjectMapModel::track3D(int index)
{
if (index < m_items.count())
{
MapItem *item = m_items[index];
m_gui->track3D(item->m_name);
}
}
void ObjectMapModel::splitTracks(ObjectMapItem *item)
{
if (item->m_takenTrackCoords.size() > 1)
{
splitTrack(item->m_takenTrackCoords, item->m_takenTrack, item->m_takenTrack1, item->m_takenTrack2,
item->m_takenStart1, item->m_takenStart2, item->m_takenEnd1, item->m_takenEnd2);
}
if (item->m_predictedTrackCoords.size() > 1)
{
splitTrack(item->m_predictedTrackCoords, item->m_predictedTrack, item->m_predictedTrack1, item->m_predictedTrack2,
item->m_predictedStart1, item->m_predictedStart2, item->m_predictedEnd1, item->m_predictedEnd2);
}
}
void ObjectMapModel::interpolateEast(QGeoCoordinate *c1, QGeoCoordinate *c2, double x, QGeoCoordinate *ci, bool offScreen)
{
double x1 = c1->longitude();
double y1 = c1->latitude();
double x2 = c2->longitude();
double y2 = c2->latitude();
double y;
if (x2 < x1)
x2 += 360.0;
if (x < x1)
x += 360.0;
y = interpolate(x1, y1, x2, y2, x);
if (x > 180)
x -= 360.0;
if (offScreen)
x -= 0.000000001;
else
x += 0.000000001;
ci->setLongitude(x);
ci->setLatitude(y);
ci->setAltitude(c1->altitude());
}
void ObjectMapModel::interpolateWest(QGeoCoordinate *c1, QGeoCoordinate *c2, double x, QGeoCoordinate *ci, bool offScreen)
{
double x1 = c1->longitude();
double y1 = c1->latitude();
double x2 = c2->longitude();
double y2 = c2->latitude();
double y;
if (x2 > x1)
x2 -= 360.0;
if (x > x1)
x -= 360.0;
y = interpolate(x1, y1, x2, y2, x);
if (x < -180)
x += 360.0;
if (offScreen)
x += 0.000000001;
else
x -= 0.000000001;
ci->setLongitude(x);
ci->setLatitude(y);
ci->setAltitude(c1->altitude());
}
static bool isOnScreen(double lon, double bottomLeftLongitude, double bottomRightLongitude, double width, bool antimed)
{
bool onScreen = false;
if (width == 360)
onScreen = true;
else if (!antimed)
onScreen = (lon > bottomLeftLongitude) && (lon <= bottomRightLongitude);
else
onScreen = (lon > bottomLeftLongitude) || (lon <= bottomRightLongitude);
return onScreen;
}
static bool crossesAntimeridian(double prevLon, double lon)
{
bool crosses = false;
if ((prevLon > 90) && (lon < -90))
crosses = true; // West to East
else if ((prevLon < -90) && (lon > 90))
crosses = true; // East to West
return crosses;
}
static bool crossesAntimeridianEast(double prevLon, double lon)
{
bool crosses = false;
if ((prevLon > 90) && (lon < -90))
crosses = true; // West to East
return crosses;
}
static bool crossesAntimeridianWest(double prevLon, double lon)
{
bool crosses = false;
if ((prevLon < -90) && (lon > 90))
crosses = true; // East to West
return crosses;
}
static bool crossesEdge(double lon, double prevLon, double bottomLeftLongitude, double bottomRightLongitude)
{
// Determine if antimerdian is between the two points
if (!crossesAntimeridian(prevLon, lon))
{
bool crosses = false;
if ((prevLon <= bottomRightLongitude) && (lon > bottomRightLongitude))
crosses = true; // Crosses right edge East
else if ((prevLon >= bottomRightLongitude) && (lon < bottomRightLongitude))
crosses = true; // Crosses right edge West
else if ((prevLon >= bottomLeftLongitude) && (lon < bottomLeftLongitude))
crosses = true; // Crosses left edge West
else if ((prevLon <= bottomLeftLongitude) && (lon > bottomLeftLongitude))
crosses = true; // Crosses left edge East
return crosses;
}
else
{
// Determine which point and the edge the antimerdian is between
bool prevLonToRightCrossesAnti = crossesAntimeridianEast(prevLon, bottomRightLongitude);
bool rightToLonCrossesAnti = crossesAntimeridianEast(bottomRightLongitude, lon);
bool prevLonToLeftCrossesAnti = crossesAntimeridianWest(prevLon, bottomLeftLongitude);
bool leftToLonCrossesAnti = crossesAntimeridianWest(bottomLeftLongitude, lon);
bool crosses = false;
if ( ((prevLon > bottomRightLongitude) && prevLonToRightCrossesAnti && (lon > bottomRightLongitude))
|| ((prevLon <= bottomRightLongitude) && (lon <= bottomRightLongitude) && rightToLonCrossesAnti)
)
crosses = true; // Crosses right edge East
else if ( ((prevLon < bottomRightLongitude) && prevLonToRightCrossesAnti && (lon < bottomRightLongitude))
|| ((prevLon >= bottomRightLongitude) && (lon >= bottomRightLongitude) && rightToLonCrossesAnti)
)
crosses = true; // Crosses right edge West
else if ( ((prevLon < bottomLeftLongitude) && prevLonToLeftCrossesAnti && (lon < bottomLeftLongitude))
|| ((prevLon >= bottomLeftLongitude) && (lon >= bottomLeftLongitude) && leftToLonCrossesAnti)
)
crosses = true; // Crosses left edge West
else if ( ((prevLon > bottomLeftLongitude) && prevLonToLeftCrossesAnti && (lon > bottomLeftLongitude))
|| ((prevLon <= bottomLeftLongitude) && (lon <= bottomLeftLongitude) && leftToLonCrossesAnti)
)
crosses = true; // Crosses left edge East
return crosses;
}
}
void ObjectMapModel::interpolate(QGeoCoordinate *c1, QGeoCoordinate *c2, double bottomLeftLongitude, double bottomRightLongitude, QGeoCoordinate* ci, bool offScreen)
{
double x1 = c1->longitude();
double x2 = c2->longitude();
double crossesAnti = crossesAntimeridian(x1, x2);
double x;
// Need to work out which edge we're interpolating too
// and whether antimeridian is in the way, as that flips x1<x2 to x1>x2
if (((x1 < x2) && !crossesAnti) || ((x1 > x2) && crossesAnti))
{
x = offScreen ? bottomRightLongitude : bottomLeftLongitude;
interpolateEast(c1, c2, x, ci, offScreen);
}
else
{
x = offScreen ? bottomLeftLongitude : bottomRightLongitude;
interpolateWest(c1, c2, x, ci, offScreen);
}
}
void ObjectMapModel::splitTrack(const QList<QGeoCoordinate *>& coords, const QVariantList& track,
QVariantList& track1, QVariantList& track2,
QGeoCoordinate& start1, QGeoCoordinate& start2,
QGeoCoordinate& end1, QGeoCoordinate& end2)
{
/*
QStringList l;
for (int i = 0; i < track.size(); i++)
{
QGeoCoordinate c = track[i].value<QGeoCoordinate>();
l.append(QString("%1").arg((int)c.longitude()));
}
qDebug() << "Init T: " << l;
*/
QQuickItem* map = m_gui->getMapItem();
QVariant rectVariant;
QMetaObject::invokeMethod(map, "mapRect", Q_RETURN_ARG(QVariant, rectVariant));
QGeoRectangle rect = qvariant_cast<QGeoRectangle>(rectVariant);
double bottomLeftLongitude = rect.bottomLeft().longitude();
double bottomRightLongitude = rect.bottomRight().longitude();
int width = round(rect.width());
bool antimed = (width == 360) || (bottomLeftLongitude > bottomRightLongitude);
/*
qDebug() << "Anitmed visible: " << antimed;
qDebug() << "bottomLeftLongitude: " << bottomLeftLongitude;
qDebug() << "bottomRightLongitude: " << bottomRightLongitude;
*/
track1.clear();
track2.clear();
double lon, prevLon;
bool onScreen, prevOnScreen;
QList<QVariantList *> tracks({&track1, &track2});
QList<QGeoCoordinate *> ends({&end1, &end2});
QList<QGeoCoordinate *> starts({&start1, &start2});
int trackIdx = 0;
for (int i = 0; i < coords.size(); i++)
{
lon = coords[i]->longitude();
if (i == 0)
{
prevLon = lon;
prevOnScreen = true; // To avoid interpolation for first point
}
// Can be onscreen after having crossed edge from other side
// Or can be onscreen after previously having been off screen
onScreen = isOnScreen(lon, bottomLeftLongitude, bottomRightLongitude, width, antimed);
bool crossedEdge = crossesEdge(lon, prevLon, bottomLeftLongitude, bottomRightLongitude);
if ((onScreen && !crossedEdge) || (onScreen && !prevOnScreen))
{
if ((i > 0) && (tracks[trackIdx]->size() == 0)) // Could also use (onScreen && !prevOnScreen)?
{
if (trackIdx >= starts.size())
break;
// Interpolate from edge of screen
interpolate(coords[i-1], coords[i], bottomLeftLongitude, bottomRightLongitude, starts[trackIdx], false);
tracks[trackIdx]->append(QVariant::fromValue(*starts[trackIdx]));
}
tracks[trackIdx]->append(track[i]);
}
else if (tracks[trackIdx]->size() > 0)
{
// Either we've crossed to the other side, or have gone off screen
if (trackIdx >= ends.size())
break;
// Interpolate to edge of screen
interpolate(coords[i-1], coords[i], bottomLeftLongitude, bottomRightLongitude, ends[trackIdx], true);
tracks[trackIdx]->append(QVariant::fromValue(*ends[trackIdx]));
// Start new track
trackIdx++;
if (trackIdx >= tracks.size())
{
// This can happen with highly retrograde orbits, where trace 90% of period
// will cover more than 360 degrees - delete last point as Map
// will not be able to display it properly
tracks[trackIdx-1]->removeLast();
break;
}
if (onScreen)
{
// Interpolate from edge of screen
interpolate(coords[i-1], coords[i], bottomLeftLongitude, bottomRightLongitude, starts[trackIdx], false);
tracks[trackIdx]->append(QVariant::fromValue(*starts[trackIdx]));
tracks[trackIdx]->append(track[i]);
}
}
prevLon = lon;
prevOnScreen = onScreen;
}
/*
l.clear();
for (int i = 0; i < track1.size(); i++)
{
QGeoCoordinate c = track1[i].value<QGeoCoordinate>();
if (!c.isValid())
l.append("Invalid!");
else
l.append(QString("%1").arg(c.longitude(), 0, 'f', 1));
}
qDebug() << "T1: " << l;
l.clear();
for (int i = 0; i < track2.size(); i++)
{
QGeoCoordinate c = track2[i].value<QGeoCoordinate>();
if (!c.isValid())
l.append("Invalid!");
else
l.append(QString("%1").arg(c.longitude(), 0, 'f', 1));
}
qDebug() << "T2: " << l;
*/
}
void ObjectMapModel::viewChanged(double bottomLeftLongitude, double bottomRightLongitude)
{
(void) bottomRightLongitude;
if (!std::isnan(bottomLeftLongitude))
{
for (int row = 0; row < m_items.size(); row++)
{
ObjectMapItem *item = (ObjectMapItem *)m_items[row];
if (m_items[row]->m_itemSettings->m_enabled)
{
if (item->m_takenTrackCoords.size() > 1)
{
splitTrack(item->m_takenTrackCoords, item->m_takenTrack, item->m_takenTrack1, item->m_takenTrack2,
item->m_takenStart1, item->m_takenStart2, item->m_takenEnd1, item->m_takenEnd2);
QModelIndex idx = index(row);
QVector<int> roles = {groundTrack1Role, groundTrack2Role};
emit dataChanged(idx, idx, roles);
}
if (item->m_predictedTrackCoords.size() > 1)
{
splitTrack(item->m_predictedTrackCoords, item->m_predictedTrack, item->m_predictedTrack1, item->m_predictedTrack2,
item->m_predictedStart1, item->m_predictedStart2, item->m_predictedEnd1, item->m_predictedEnd2);
QModelIndex idx = index(row);
QVector<int> roles = {predictedGroundTrack1Role, predictedGroundTrack2Role};
emit dataChanged(idx, idx, roles);
}
}
}
}
}
bool ObjectMapFilter::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
{
QModelIndex index0 = sourceModel()->index(sourceRow, 0, sourceParent);
MapSettings::MapItemSettings *itemSettings = sourceModel()->data(index0, ObjectMapModel::itemSettingsRole).value<MapSettings::MapItemSettings *>();
if (!itemSettings->m_enabled) {
return false;
}
// Don't show tiny items when we're zoomed out
int minZoom = sourceModel()->data(index0, ObjectMapModel::mapImageMinZoomRole).toInt();
if (minZoom - 3 >= m_zoomLevel) {
return false;
}
// Don't show off-screen items, unless they have tracks, as these may be on screen
// In the future, we could calculate a bounding box for tracks, if helpful for performance
QGeoCoordinate coord = sourceModel()->data(index0, ObjectMapModel::positionRole).value<QGeoCoordinate>();
float lat = coord.latitude();
float lon = coord.longitude();
bool onScreen = (lat >= m_bottomRightLatitude) && (lat <= m_topLeftLatitude) && (lon >= m_topLeftLongitude) && (lon <= m_bottomRightLongitude);
if (!onScreen)
{
bool hasTracks = sourceModel()->data(index0, ObjectMapModel::hasTracksRole).toBool();
if (!hasTracks) {
return false;
}
}
// Apply user filter
if (!itemSettings->m_filterName.isEmpty())
{
QString name = sourceModel()->data(index0, ObjectMapModel::nameRole).toString();
QRegularExpressionMatch match = itemSettings->m_filterNameRE.match(name);
if (!match.hasMatch()) {
return false;
}
}
if (itemSettings->m_filterDistance > 0)
{
QGeoCoordinate position = sourceModel()->data(index0, ObjectMapModel::positionRole).value<QGeoCoordinate>();
if (m_position.distanceTo(position) > itemSettings->m_filterDistance) {
return false;
}
}
return true;
}
void ObjectMapFilter::viewChanged(double topLeftLongitude, double topLeftLatitude, double bottomRightLongitude, double bottomRightLatitude, double zoomLevel)
{
m_zoomLevel = zoomLevel;
if (!std::isnan(topLeftLongitude))
{
m_topLeftLongitude = topLeftLongitude;
m_topLeftLatitude = topLeftLatitude;
m_bottomRightLongitude = bottomRightLongitude;
m_bottomRightLatitude = bottomRightLatitude;
}
invalidateFilter();
}
Q_INVOKABLE int ObjectMapFilter::mapRowToSource(int row)
{
QModelIndex sourceIndex = mapToSource(index(row, 0));
return sourceIndex.row();
}
bool PolygonFilter::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
{
QModelIndex index0 = sourceModel()->index(sourceRow, 0, sourceParent);
MapSettings::MapItemSettings *itemSettings = sourceModel()->data(index0, PolygonMapModel::itemSettingsRole).value<MapSettings::MapItemSettings *>();
if (!itemSettings->m_enabled) {
return false;
}
// Don't show tiny items when we're zoomed out
int minZoom = sourceModel()->data(index0, PolygonMapModel::mapImageMinZoomRole).toInt();
if (minZoom - 3 >= m_zoomLevel) {
return false;
}
// Filter off-screen items
QGeoRectangle bounds = sourceModel()->data(index0, PolygonMapModel::boundsRole).value<QGeoRectangle>();
if (!m_view.intersects(bounds)) {
return false;
}
// Apply user filter
if (!itemSettings->m_filterName.isEmpty())
{
QString name = sourceModel()->data(index0, PolygonMapModel::nameRole).toString();
QRegularExpressionMatch match = itemSettings->m_filterNameRE.match(name);
if (!match.hasMatch()) {
return false;
}
}
if (itemSettings->m_filterDistance > 0)
{
QGeoCoordinate position = sourceModel()->data(index0, PolygonMapModel::positionRole).value<QGeoCoordinate>();
if (m_position.distanceTo(position) > itemSettings->m_filterDistance) {
return false;
}
}
return true;
}
void PolygonFilter::viewChanged(double topLeftLongitude, double topLeftLatitude, double bottomRightLongitude, double bottomRightLatitude, double zoomLevel)
{
m_zoomLevel = zoomLevel;
if (!std::isnan(topLeftLongitude)) {
m_view = QGeoRectangle(QGeoCoordinate(topLeftLatitude, topLeftLongitude), QGeoCoordinate(bottomRightLatitude, bottomRightLongitude));
}
invalidateFilter();
}
Q_INVOKABLE int PolygonFilter::mapRowToSource(int row)
{
QModelIndex sourceIndex = mapToSource(index(row, 0));
return sourceIndex.row();
}
bool PolylineFilter::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
{
QModelIndex index0 = sourceModel()->index(sourceRow, 0, sourceParent);
MapSettings::MapItemSettings *itemSettings = sourceModel()->data(index0, PolylineMapModel::itemSettingsRole).value<MapSettings::MapItemSettings *>();
if (!itemSettings->m_enabled) {
return false;
}
// Don't show tiny items when we're zoomed out
int minZoom = sourceModel()->data(index0, PolylineMapModel::mapImageMinZoomRole).toInt();
if (minZoom - 3 >= m_zoomLevel) {
return false;
}
// Filter off-screen items
QGeoRectangle bounds = sourceModel()->data(index0, PolylineMapModel::boundsRole).value<QGeoRectangle>();
if (!m_view.intersects(bounds)) {
return false;
}
// Apply user filter
if (!itemSettings->m_filterName.isEmpty())
{
QString name = sourceModel()->data(index0, PolylineMapModel::nameRole).toString();
QRegularExpressionMatch match = itemSettings->m_filterNameRE.match(name);
if (!match.hasMatch()) {
return false;
}
}
if (itemSettings->m_filterDistance > 0)
{
QGeoCoordinate position = sourceModel()->data(index0, PolylineMapModel::positionRole).value<QGeoCoordinate>();
if (m_position.distanceTo(position) > itemSettings->m_filterDistance) {
return false;
}
}
return true;
}
void PolylineFilter::viewChanged(double topLeftLongitude, double topLeftLatitude, double bottomRightLongitude, double bottomRightLatitude, double zoomLevel)
{
m_zoomLevel = zoomLevel;
if (!std::isnan(topLeftLongitude)) {
m_view = QGeoRectangle(QGeoCoordinate(topLeftLatitude, topLeftLongitude), QGeoCoordinate(bottomRightLatitude, bottomRightLongitude));
}
invalidateFilter();
}
Q_INVOKABLE int PolylineFilter::mapRowToSource(int row)
{
QModelIndex sourceIndex = mapToSource(index(row, 0));
return sourceIndex.row();
}
bool ImageFilter::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
{
QModelIndex index0 = sourceModel()->index(sourceRow, 0, sourceParent);
MapSettings::MapItemSettings *itemSettings = sourceModel()->data(index0, ImageMapModel::itemSettingsRole).value<MapSettings::MapItemSettings *>();
if (!itemSettings->m_enabled || !itemSettings->m_display2DIcon) {
return false;
}
// Don't show tiny items when we're zoomed out
int minZoom = sourceModel()->data(index0, ImageMapModel::mapImageMinZoomRole).toInt();
if (minZoom - 3 >= m_zoomLevel) {
return false;
}
// Filter off-screen items
QGeoRectangle bounds = sourceModel()->data(index0, ImageMapModel::boundsRole).value<QGeoRectangle>();
if (!m_view.intersects(bounds)) {
return false;
}
// Apply user filter
if (!itemSettings->m_filterName.isEmpty())
{
QString name = sourceModel()->data(index0, ImageMapModel::nameRole).toString();
QRegularExpressionMatch match = itemSettings->m_filterNameRE.match(name);
if (!match.hasMatch()) {
return false;
}
}
if (itemSettings->m_filterDistance > 0)
{
QGeoCoordinate position = sourceModel()->data(index0, ImageMapModel::positionRole).value<QGeoCoordinate>();
if (m_position.distanceTo(position) > itemSettings->m_filterDistance) {
return false;
}
}
return true;
}
void ImageFilter::viewChanged(double topLeftLongitude, double topLeftLatitude, double bottomRightLongitude, double bottomRightLatitude, double zoomLevel)
{
m_zoomLevel = zoomLevel;
if (!std::isnan(topLeftLongitude)) {
m_view = QGeoRectangle(QGeoCoordinate(topLeftLatitude, topLeftLongitude), QGeoCoordinate(bottomRightLatitude, bottomRightLongitude));
}
invalidateFilter();
}
Q_INVOKABLE int ImageFilter::mapRowToSource(int row)
{
QModelIndex sourceIndex = mapToSource(index(row, 0));
return sourceIndex.row();
}
void PolygonFilter::setPosition(const QGeoCoordinate& position)
{
m_position = position;
invalidateFilter();
}
void PolylineFilter::setPosition(const QGeoCoordinate& position)
{
m_position = position;
invalidateFilter();
}
void ObjectMapFilter::setPosition(const QGeoCoordinate& position)
{
m_position = position;
invalidateFilter();
}
void ImageFilter::setPosition(const QGeoCoordinate& position)
{
m_position = position;
invalidateFilter();
}