presentation/QuickContactMap.h
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #pragma once | ||
| 2 | |||
| 3 | #include <QDeadlineTimer> | ||
| 4 | #include <QHash> | ||
| 5 | #include <QObject> | ||
| 6 | #include <QPointF> | ||
| 7 | #include <QSizeF> | ||
| 8 | #include <QStringList> | ||
| 9 | #include <QSet> | ||
| 10 | #include <QThreadPool> | ||
| 11 | #include <QVariantList> | ||
| 12 | #include <vector> | ||
| 13 | |||
| 14 | #include "data/ContactPlaceLookup.h" | ||
| 15 | #include "data/MapClusterer.h" | ||
| 16 | #include "data/MapProjection.h" | ||
| 17 | #include "presentation/PlaceLookupSource.h" | ||
| 18 | |||
| 19 | class MapMarkerModel; | ||
| 20 | class MapTileSource; | ||
| 21 | class QTimer; | ||
| 22 | |||
| 23 | // The contacts' world map for QML (Sprint 103 S103.4.1–S103.4.4, D-11, | ||
| 24 | // interaction.md §6; `backend.pim.contactMap`). | ||
| 25 | // | ||
| 26 | // The places come from the derived card_places rows that have a position | ||
| 27 | // (pinned, from the card or placed by the offline gazetteer). On activate() | ||
| 28 | // the pending rows are placed on a worker in slices (ContactPlaceIndexer, | ||
| 29 | // positions never leave the computer) and the places are read again. | ||
| 30 | // | ||
| 31 | // The viewport (centre, zoom, canvas size) lives here; QML draws `tiles` | ||
| 32 | // (image://maptile, Web Mercator slippy tiles at the nearest integer zoom, | ||
| 33 | // scaled to the fractional zoom) and `markers` (clusters of MapClusterer for | ||
| 34 | // the current zoom, only those inside the canvas, west → east). Every | ||
| 35 | // position is in canvas pixels. | ||
| 36 | class QuickContactMap final : public QObject { | ||
| 37 | 168 | Q_OBJECT | |
| 38 | Q_PROPERTY(double zoom READ zoom NOTIFY viewportChanged) | ||
| 39 | // [{source ("z/x/y"), x, y, size}] | ||
| 40 | Q_PROPERTY(QVariantList tiles READ tiles NOTIFY viewportChanged) | ||
| 41 | // [{id, x, y, count, key, keys, name, initials, avatarColor, hasPhoto, | ||
| 42 | // kind, place, sameSpot, approximate}] — a single person has count 1 and | ||
| 43 | // their key; `approximate`: placed from the address by the offline | ||
| 44 | // gazetteer (not pinned, no position on the card). | ||
| 45 | Q_PROPERTY(QVariantList markers READ markers NOTIFY markersChanged) | ||
| 46 | // F103-14: the same markers as a model with stable rows per cluster (QML). | ||
| 47 | Q_PROPERTY(QObject *markerModel READ markerModel CONSTANT) | ||
| 48 | Q_PROPERTY(int placedPeople READ placedPeople NOTIFY placesChanged) | ||
| 49 | Q_PROPERTY(int unplacedPeople READ unplacedPeople NOTIFY placesChanged) | ||
| 50 | Q_PROPERTY(bool placing READ placing NOTIFY placingChanged) | ||
| 51 | // Place kinds shown (View ▾): "home", "work", "other" (addresses), "met", "birth". | ||
| 52 | Q_PROPERTY(QStringList kinds READ kinds WRITE setKinds NOTIFY placesChanged) | ||
| 53 | // S103.4.1: "list", "map" or "graph" in the contact area, remembered (QSettings contacts/viewMode). | ||
| 54 | Q_PROPERTY(QString viewMode READ viewMode WRITE setViewMode NOTIFY viewModeChanged) | ||
| 55 | // S103.4.7: the accessible "List of places" instead of the canvas. | ||
| 56 | Q_PROPERTY(bool placesList READ placesList WRITE setPlacesList NOTIFY viewModeChanged) | ||
| 57 | // S103.4.10 (D-11): online tiles, opt-in in Settings › Privacy (QSettings | ||
| 58 | // contacts/mapTiles, contacts/mapTileServer). With them the map zooms to | ||
| 59 | // street level; the outline stays below as the fallback. | ||
| 60 | Q_PROPERTY(bool onlineTiles READ onlineTiles WRITE setOnlineTiles NOTIFY tileSettingsChanged) | ||
| 61 | // F103-24: [{id, provider, name}] of resources/contacts/map-styles.json; | ||
| 62 | // the chosen one (QSettings contacts/mapTileStyle) or "custom" (the tileServer). | ||
| 63 | Q_PROPERTY(QVariantList tileStyles READ tileStyles CONSTANT) | ||
| 64 | Q_PROPERTY(QString tileStyle READ tileStyle WRITE setTileStyle NOTIFY tileSettingsChanged) | ||
| 65 | // [{id, name, keyUrl, needsKey, hasKey}] of the catalog's providers; the | ||
| 66 | // provider of the chosen style ("custom" for an own server). | ||
| 67 | Q_PROPERTY(QVariantList tileProviders READ tileProviders NOTIFY tileSettingsChanged) | ||
| 68 | Q_PROPERTY(QString tileProvider READ tileProvider NOTIFY tileSettingsChanged) | ||
| 69 | // The chosen style's provider needs an API key and none is stored: no | ||
| 70 | // tile is fetched (keyless requests get watermark tiles), the map says why. | ||
| 71 | Q_PROPERTY(bool tileKeyMissing READ tileKeyMissing NOTIFY tileSettingsChanged) | ||
| 72 | // The server in use; setting one chooses "custom", "" the default style. | ||
| 73 | Q_PROPERTY(QString tileServer READ tileServer WRITE setTileServer NOTIFY tileSettingsChanged) | ||
| 74 | // F103-25: the tile cache on disk ("12.3 MB"), read by refreshTileCacheSize(). | ||
| 75 | Q_PROPERTY(QString tileCacheSize READ tileCacheSize NOTIFY tileCacheChanged) | ||
| 76 | // S103.1.9 (D-10): online lookup of street addresses, opt-in (QSettings | ||
| 77 | // contacts/placeLookup, contacts/placeLookupServer). With it on, the | ||
| 78 | // addresses the offline gazetteer placed only roughly are looked up in the | ||
| 79 | // background (1 per second) while the map is in use; `lookupExample` is the | ||
| 80 | // request for a sample address (the consent text shows what is sent). | ||
| 81 | Q_PROPERTY(bool onlineLookup READ onlineLookup WRITE setOnlineLookup NOTIFY lookupSettingsChanged) | ||
| 82 | Q_PROPERTY(QString lookupServer READ lookupServer WRITE setLookupServer NOTIFY lookupSettingsChanged) | ||
| 83 | Q_PROPERTY(QString lookupServerHost READ lookupServerHost NOTIFY lookupSettingsChanged) | ||
| 84 | Q_PROPERTY(QString lookupExample READ lookupExample NOTIFY lookupSettingsChanged) | ||
| 85 | Q_PROPERTY(int lookupPending READ lookupPending NOTIFY lookupPendingChanged) | ||
| 86 | Q_PROPERTY(QString attribution READ attribution NOTIFY tileSettingsChanged) | ||
| 87 | Q_PROPERTY(bool tilesUnavailable READ tilesUnavailable NOTIFY tilesUnavailableChanged) | ||
| 88 | // [{source (file URL), x, y, size}]: the cached online tiles of the view. | ||
| 89 | Q_PROPERTY(QVariantList onlineTileList READ onlineTileList NOTIFY viewportChanged) | ||
| 90 | // F103-23: the layers as models with stable rows per tile (QML), so a pan | ||
| 91 | // or zoom moves the same images instead of reloading them. `tileModel` | ||
| 92 | // also holds the level above (seamless level changes); `onlineTileModel` | ||
| 93 | // also holds stand-ins for tiles not yet loaded: the nearest cached | ||
| 94 | // ancestor's part (clip) or cached children, below the real tiles (z). | ||
| 95 | // Rows: {id, source, x, y, size, z, clip: [x, y, w, h] or [], fade}. | ||
| 96 | Q_PROPERTY(QObject *tileModel READ tileModel CONSTANT) | ||
| 97 | Q_PROPERTY(QObject *onlineTileModel READ onlineTileModel CONSTANT) | ||
| 98 | |||
| 99 | public: | ||
| 100 | // F103-23: tiles are requested once the view rested this long (a zoom | ||
| 101 | // gesture no longer starts and aborts the requests of every level). | ||
| 102 | static constexpr int WantDelayMs = 150; | ||
| 103 | explicit QuickContactMap(QString databasePath, QObject *parent = nullptr); | ||
| 104 | ~QuickContactMap() override; | ||
| 105 | |||
| 106 | 32 | double zoom() const { return m_viewport.zoom; } | |
| 107 | 7 | QVariantList tiles() const { return m_tiles; } | |
| 108 | 17 | QVariantList markers() const { return m_markers; } | |
| 109 | QObject *markerModel() const; | ||
| 110 | 63 | int placedPeople() const { return m_placedPeople; } | |
| 111 | 141 | int unplacedPeople() const { return m_unplacedPeople; } | |
| 112 | 115 | bool placing() const { return m_placing; } | |
| 113 | 32 | QStringList kinds() const { return m_kinds; } | |
| 114 | void setKinds(const QStringList &kinds); | ||
| 115 | QString viewMode() const; | ||
| 116 | void setViewMode(const QString &mode); | ||
| 117 | 25 | bool placesList() const { return m_placesList; } | |
| 118 | void setPlacesList(bool on); | ||
| 119 | // S103.4.7 (interaction.md §6.8): the shown places as a tree — | ||
| 120 | // [{country, count, cities: [{city, count, people: [{key, name, kind}]}]}], | ||
| 121 | // countries and cities by count, then name. | ||
| 122 | Q_INVOKABLE QVariantList placesTree() const; | ||
| 123 | 81 | bool onlineTiles() const { return m_onlineTiles; } | |
| 124 | void setOnlineTiles(bool on); | ||
| 125 | QVariantList tileStyles() const; | ||
| 126 | 47 | QString tileStyle() const { return m_tileStyle; } | |
| 127 | void setTileStyle(const QString &id); | ||
| 128 | QVariantList tileProviders() const; | ||
| 129 | QString tileProvider() const; | ||
| 130 | bool tileKeyMissing() const; | ||
| 131 | // A provider's API key (QSettings contacts/mapTileKeys/<provider>; local, | ||
| 132 | // never synced, never logged); "" removes it. | ||
| 133 | Q_INVOKABLE QString tileKey(const QString &provider) const; | ||
| 134 | Q_INVOKABLE void setTileKey(const QString &provider, const QString &key); | ||
| 135 | QString tileServer() const; | ||
| 136 | void setTileServer(const QString &templateUrl); | ||
| 137 | 36 | QString tileCacheSize() const { return m_tileCacheSize; } | |
| 138 | Q_INVOKABLE void refreshTileCacheSize(); | ||
| 139 | 52 | bool onlineLookup() const { return m_onlineLookup; } | |
| 140 | void setOnlineLookup(bool on); | ||
| 141 | QString lookupServer() const; | ||
| 142 | void setLookupServer(const QString &server); | ||
| 143 | QString lookupServerHost() const; | ||
| 144 | QString lookupExample() const; | ||
| 145 | int lookupPending() const; | ||
| 146 | // "" when the server address can be used, else why not (settings field). | ||
| 147 | Q_INVOKABLE QString checkLookupServer(const QString &server) const; | ||
| 148 | // *Look up exactly…* on a card (also with the setting off, after the | ||
| 149 | // user confirmed): one request for the address at `index`; the answer | ||
| 150 | // comes as addressLookedUp. False when the card has no such address. | ||
| 151 | Q_INVOKABLE bool lookUpAddress(const QString &key, int index); | ||
| 152 | 4 | PlaceLookupSource *lookupSource() const { return m_lookupSource; } // tests | |
| 153 | QObject *tileModel() const; | ||
| 154 | QObject *onlineTileModel() const; | ||
| 155 | QString attribution() const; | ||
| 156 | ✗ | bool tilesUnavailable() const { return m_tilesUnavailable; } | |
| 157 |
2/2✓ Branch 2 → 3 taken 47 times.
✓ Branch 2 → 4 taken 2 times.
|
49 | QVariantList onlineTileList() const { return m_onlineTiles ? m_onlineTileList : QVariantList{}; } |
| 158 | // "" when the server address can be used, else why not (settings field). | ||
| 159 | Q_INVOKABLE QString checkTileServer(const QString &templateUrl) const; | ||
| 160 | Q_INVOKABLE void clearTileCache(); | ||
| 161 | // Tests (network spy, G20). | ||
| 162 | 83 | MapTileSource *tileSource() const { return m_tileSource; } | |
| 163 | |||
| 164 | // The map became visible: place pending rows, then read the places. | ||
| 165 | Q_INVOKABLE void activate(); | ||
| 166 | // S106-312: the map left the screen — background address lookups stop | ||
| 167 | // (they run only while the map is in use) and reload() stays cheap. | ||
| 168 | Q_INVOKABLE void deactivate(); | ||
| 169 | 1 | bool active() const { return m_active; } // tests | |
| 170 | // S106-283: an address whose lookup failed is not asked again for this | ||
| 171 | // long; a background batch without any answer pauses the lookups as long. | ||
| 172 | static constexpr int LookupRetryMs = 10 * 60 * 1000; | ||
| 173 | 1 | void setLookupRetryMs(int ms) { m_lookupRetryMs = ms; } // tests | |
| 174 | // After contact writes or a sync (cheap when the map is hidden). | ||
| 175 | Q_INVOKABLE void reload(); | ||
| 176 | Q_INVOKABLE void setSize(double width, double height); | ||
| 177 | Q_INVOKABLE void panBy(double dx, double dy); | ||
| 178 | Q_INVOKABLE void zoomAt(double steps, double x, double y); | ||
| 179 | // `0`: every shown place in view. | ||
| 180 | Q_INVOKABLE void fitAll(); | ||
| 181 | // §6.5: when none of the person's places is in view, pan (not zoom) to | ||
| 182 | // the first one (selection from the list, graph or card). | ||
| 183 | Q_INVOKABLE void showKey(const QString &key); | ||
| 184 | // Pin mode (S103.4.5): canvas point → {latitude, longitude}; centre on a | ||
| 185 | // position at `zoom` (0 keeps the zoom). | ||
| 186 | Q_INVOKABLE QVariantMap toLatLon(double x, double y) const; | ||
| 187 | Q_INVOKABLE void centerOn(double latitude, double longitude, double zoom = 0); | ||
| 188 | // S103.6.7 `map <place>`: centre on a place of the offline gazetteer | ||
| 189 | // (city 9, region 6, country 4); false when it is unknown. | ||
| 190 | Q_INVOKABLE bool showPlace(const QString &text); | ||
| 191 | // S103.4.6 (I-7): the people with a placed address, meeting or birthplace | ||
| 192 | // within about 50 km of a place text (an event's location): {place, count} | ||
| 193 | // — {} when the place is unknown offline or only a country; the me card | ||
| 194 | // and positions known only at country level are not counted. | ||
| 195 | Q_INVOKABLE QVariantMap nearby(const QString &text) const; | ||
| 196 | // Canvas position of a world point (pin mode, labels): {x, y}. | ||
| 197 | Q_INVOKABLE QVariantMap toScreen(double latitude, double longitude) const; | ||
| 198 | |||
| 199 | // The current viewport (tests, the graph's "show on map"). | ||
| 200 | 1 | MapProjection::Viewport viewport() const { return m_viewport; } | |
| 201 | |||
| 202 | signals: | ||
| 203 | void viewportChanged(); | ||
| 204 | void markersChanged(); | ||
| 205 | void placesChanged(); | ||
| 206 | void placingChanged(); | ||
| 207 | void viewModeChanged(); | ||
| 208 | void tileSettingsChanged(); | ||
| 209 | void tilesUnavailableChanged(); | ||
| 210 | void tileCacheChanged(); | ||
| 211 | void lookupSettingsChanged(); | ||
| 212 | void lookupPendingChanged(); | ||
| 213 | // A card's *Look up exactly…* was answered: found (placeName) or not | ||
| 214 | // found, or `failed` (offline, refused). | ||
| 215 | void addressLookedUp(const QString &key, bool found, bool failed, const QString &placeName); | ||
| 216 | |||
| 217 | private: | ||
| 218 | struct Place { | ||
| 219 | QString key; | ||
| 220 | QString name; | ||
| 221 | QString initials; | ||
| 222 | int avatarColor = 0; | ||
| 223 | bool hasPhoto = false; | ||
| 224 | QString kind; | ||
| 225 | QString label; // locality or place text | ||
| 226 | QString country; | ||
| 227 | bool approximate = false; | ||
| 228 | int person = 0; // index of the person (one per key), for fast de-duplication | ||
| 229 | QPointF world; | ||
| 230 | }; | ||
| 231 | void readPlaces(); | ||
| 232 | void placeOnWorker(); | ||
| 233 | void updateTiles(); | ||
| 234 | void updateOnlineTiles(); | ||
| 235 | void applyTileStyle(); | ||
| 236 | void initLookup(); | ||
| 237 | void queueLookups(); | ||
| 238 | void stopBackgroundLookups(); | ||
| 239 | void lookupAnswered(const QString &id, const PlaceLookupSource::Result &result); | ||
| 240 | void tileSettingsApplied(); | ||
| 241 | int onlineMaxZoom() const; | ||
| 242 | void requestTiles(); | ||
| 243 | void updateMarkers(); | ||
| 244 | void rebuildClusterInput(); | ||
| 245 | const QStringList &clusterKeys(int cluster); | ||
| 246 | void setViewport(const MapProjection::Viewport &viewport); | ||
| 247 | |||
| 248 | QString m_databasePath; | ||
| 249 | QThreadPool m_pool; | ||
| 250 | MapProjection::Viewport m_viewport; | ||
| 251 | QList<Place> m_places; | ||
| 252 | int m_personCount = 0; | ||
| 253 | std::vector<int> m_seenAt; // per person: the last cluster that counted them | ||
| 254 | // Built once per read: the clusterer's input (id = index into m_places) | ||
| 255 | // and, per zoom, its clusters — a pan only moves and filters them. | ||
| 256 | QList<MapClusterer::Place> m_clusterInput; | ||
| 257 | double m_clusterZoom = -1; | ||
| 258 | QList<MapClusterer::Cluster> m_clusters; | ||
| 259 | QHash<int, QStringList> m_clusterKeys; // the people of a visible cluster, once per zoom | ||
| 260 | QStringList m_kinds{QStringLiteral("home"), QStringLiteral("work"), QStringLiteral("other"), | ||
| 261 | QStringLiteral("met"), QStringLiteral("birth")}; | ||
| 262 | QVariantList m_tiles; | ||
| 263 | QVariantList m_markers; | ||
| 264 | MapMarkerModel *m_markerModel = nullptr; | ||
| 265 | int m_placedPeople = 0; | ||
| 266 | int m_unplacedPeople = 0; | ||
| 267 | bool m_placing = false; | ||
| 268 | bool m_active = false; | ||
| 269 | bool m_fitted = false; | ||
| 270 | bool m_placesList = false; | ||
| 271 | MapTileSource *m_tileSource = nullptr; | ||
| 272 | bool m_onlineTiles = false; | ||
| 273 | bool m_tilesUnavailable = false; | ||
| 274 | bool m_tileRefreshQueued = false; | ||
| 275 | QVariantList m_onlineTileList; | ||
| 276 | MapMarkerModel *m_tileModel = nullptr; | ||
| 277 | MapMarkerModel *m_onlineTileModel = nullptr; | ||
| 278 | QString m_tileStyle; | ||
| 279 | QString m_tileCacheSize; | ||
| 280 | QSet<QString> m_standIns; // tiles shown through a stand-in last time (no fade-in then) | ||
| 281 | QHash<QString, bool> m_tileFade; // online rows shown last time → their fade | ||
| 282 | QStringList m_wantedTiles; // centre first, sent after the view rests (requestTiles) | ||
| 283 | QTimer *m_wantTimer = nullptr; | ||
| 284 | PlaceLookupSource *m_lookupSource = nullptr; | ||
| 285 | bool m_onlineLookup = false; | ||
| 286 | struct LookupRequest { | ||
| 287 | ContactPlaceLookup::Address address; | ||
| 288 | QString cardKey; // "" for the background lookups | ||
| 289 | }; | ||
| 290 | QHash<QString, LookupRequest> m_lookups; // request id → its address | ||
| 291 | QHash<QString, QDeadlineTimer> m_lookupFailed; // address cache key → when it may be asked again | ||
| 292 | QDeadlineTimer m_lookupPause; // expired: background lookups may run | ||
| 293 | bool m_batchAnswered = false; // the running background batch got an answer | ||
| 294 | int m_lookupRetryMs = LookupRetryMs; | ||
| 295 | QTimer *m_lookupReread = nullptr; // one map re-read after a burst of answers | ||
| 296 | }; | ||
| 297 |