Files
MapLibreParticleLab/src/map/map.cpp
Konstantin Käfer ea6b24d02d add text rendering code
2014-03-21 17:36:29 +01:00

488 lines
14 KiB
C++

#include <llmr/map/map.hpp>
#include <llmr/map/settings.hpp>
#include <llmr/platform/platform.hpp>
#include <llmr/style/resources.hpp>
#include <llmr/style/sprite.hpp>
#include <llmr/map/coverage.hpp>
#include <llmr/util/animation.hpp>
#include <algorithm>
using namespace llmr;
Map::Map(Settings& settings)
: settings(settings),
transform(),
texturepool(),
style(),
glyphAtlas(1024, 1024),
painter(transform, settings, style, glyphAtlas),
min_zoom(0),
max_zoom((use_raster ? kTileRasterMaxZoom : kTileVectorMaxZoom)) {
}
Map::~Map() {
settings.sync();
}
void Map::setup(float pixelRatio) {
painter.setup();
pixel_ratio = pixelRatio;
style.sprite = std::make_shared<Sprite>();
style.sprite->load(kSpriteURL, pixel_ratio);
style.loadJSON(resources::style, resources::style_size);
}
void Map::loadStyle(const uint8_t *const data, uint32_t bytes) {
style.loadJSON(data, bytes);
update();
}
void Map::loadSettings() {
transform.setAngle(settings.angle);
transform.setScale(settings.scale);
transform.setLonLat(settings.longitude, settings.latitude);
style.cascade(transform.getZoom());
update();
}
void Map::resize(uint32_t width, uint32_t height, uint32_t fb_width, uint32_t fb_height) {
transform.width = width;
transform.height = height;
transform.fb_width = fb_width;
transform.fb_height = fb_height;
transform.pixelRatio = pixel_ratio;
painter.resize(fb_width, fb_height);
update();
}
void Map::toggleRaster() {
use_raster = ! use_raster;
max_zoom = (use_raster ? kTileRasterMaxZoom : kTileVectorMaxZoom);
tiles.clear();
update();
}
void Map::moveBy(double dx, double dy, double duration) {
transform.moveBy(dx, dy, duration);
update();
transform.getLonLat(settings.longitude, settings.latitude);
settings.persist();
}
void Map::startPanning() {
transform.startPanning();
platform::restart();
}
void Map::stopPanning() {
transform.stopPanning();
platform::restart();
}
void Map::scaleBy(double ds, double cx, double cy, double duration) {
transform.scaleBy(ds, cx, cy, duration);
style.cascade(transform.getZoom());
update();
transform.getLonLat(settings.longitude, settings.latitude);
settings.scale = transform.getScale();
settings.persist();
}
void Map::startScaling() {
transform.startScaling();
platform::restart();
}
void Map::stopScaling() {
transform.stopScaling();
platform::restart();
}
void Map::rotateBy(double cx, double cy, double sx, double sy, double ex, double ey, double duration) {
transform.rotateBy(cx, cy, sx, sy, ex, ey, duration);
update();
settings.angle = transform.getAngle();
settings.persist();
}
void Map::startRotating() {
transform.startRotating();
platform::restart();
}
void Map::stopRotating() {
transform.stopRotating();
platform::restart();
}
void Map::setLonLat(double lon, double lat, double duration) {
transform.setLonLat(lon, lat, duration);
update();
transform.getLonLat(settings.longitude, settings.latitude);
settings.persist();
}
void Map::getLonLat(double& lon, double& lat) const {
transform.getLonLat(lon, lat);
}
void Map::setLonLatZoom(double lon, double lat, double zoom, double duration) {
transform.setLonLatZoom(lon, lat, zoom, duration);
style.cascade(transform.getZoom());
update();
transform.getLonLat(settings.longitude, settings.latitude);
settings.scale = transform.getScale();
settings.persist();
}
void Map::getLonLatZoom(double& lon, double& lat, double& zoom) const {
transform.getLonLatZoom(lon, lat, zoom);
}
void Map::setScale(double scale, double cx, double cy, double duration) {
transform.setScale(scale, cx, cy, duration);
style.cascade(transform.getZoom());
update();
transform.getLonLat(settings.longitude, settings.latitude);
settings.scale = transform.getScale();
settings.persist();
}
void Map::setZoom(double zoom, double duration) {
transform.setZoom(zoom, duration);
style.cascade(transform.getZoom());
update();
transform.getLonLat(settings.longitude, settings.latitude);
settings.scale = transform.getScale();
settings.persist();
}
double Map::getZoom() const {
return transform.getZoom();
}
void Map::setAngle(double angle, double x, double y, double duration) {
double dx = 0, dy = 0;
if (x >= 0 && y >= 0) {
dx = (transform.width / 2) - x;
dy = (transform.height / 2) - y;
transform.moveBy(dx, dy, duration);
}
transform.setAngle(angle, duration);
if (x >= 0 && y >= 0) {
transform.moveBy(-dx, -dy, duration);
}
update();
transform.getLonLat(settings.longitude, settings.latitude);
settings.angle = transform.getAngle();
settings.persist();
}
double Map::getScale() const {
return transform.getScale();
}
double Map::getAngle() const {
return transform.getAngle();
}
void Map::resetNorth() {
transform.setAngle(0, 0.5);
update();
settings.angle = transform.getAngle();
settings.persist();
}
void Map::resetPosition() {
transform.setAngle(0);
transform.setLonLat(0, 0);
transform.setZoom(0);
style.cascade(transform.getZoom());
update();
transform.getLonLat(settings.longitude, settings.latitude);
settings.scale = transform.getScale();
settings.angle = transform.getAngle();
settings.persist();
}
void Map::resetZoom() {
setZoom(0);
}
void Map::toggleDebug() {
settings.debug = !settings.debug;
update();
settings.persist();
}
void Map::cancelAnimations() {
transform.cancelAnimations();
}
void Map::update() {
updateTiles();
platform::restart();
}
TileData::State Map::hasTile(const Tile::ID& id) {
for (const Tile& tile : tiles) {
if (tile.id == id && tile.data) {
return tile.data->state;
}
}
return TileData::State::invalid;
}
TileData::State Map::addTile(const Tile::ID& id) {
const TileData::State state = hasTile(id);
if (state != TileData::State::invalid) {
return state;
}
tiles.emplace_front(id);
Tile& new_tile = tiles.front();
// We couldn't find the tile in the list. Create a new one.
// Try to find the associated TileData object.
const Tile::ID normalized_id = id.normalized();
auto it = std::find_if(tile_data.begin(), tile_data.end(), [&normalized_id](const std::weak_ptr<TileData>& tile_data) {
return !tile_data.expired() && tile_data.lock()->id == normalized_id;
});
if (it != tile_data.end()) {
// Create a shared_ptr handle. Note that this might be empty!
new_tile.data = it->lock();
}
if (new_tile.data && new_tile.data->state == TileData::State::obsolete) {
// Do not consider the tile if it's already obsolete.
new_tile.data.reset();
}
if (!new_tile.data) {
// If we don't find working tile data, we're just going to load it.
new_tile.data = std::make_shared<TileData>(normalized_id, style, glyphAtlas, use_raster, (pixel_ratio > 1.0));
new_tile.data->request();
tile_data.push_front(new_tile.data);
}
return new_tile.data->state;
}
/**
* Recursively find children of the given tile that are already loaded.
*
* @param id The tile ID that we should find children for.
* @param maxCoveringZoom The maximum zoom level of children to look for.
* @param retain An object that we add the found tiles to.
*
* @return boolean Whether the children found completely cover the tile.
*/
bool Map::findLoadedChildren(const Tile::ID& id, int32_t maxCoveringZoom, std::forward_list<Tile::ID>& retain) {
bool complete = true;
int32_t z = id.z;
auto ids = id.children(z + 1);
for (const Tile::ID& child_id : ids) {
const TileData::State state = hasTile(child_id);
if (state == TileData::State::parsed) {
retain.emplace_front(child_id);
} else {
complete = false;
if (z < maxCoveringZoom) {
// Go further down the hierarchy to find more unloaded children.
findLoadedChildren(child_id, maxCoveringZoom, retain);
}
}
}
return complete;
}
/**
* Find a loaded parent of the given tile.
*
* @param id The tile ID that we should find children for.
* @param minCoveringZoom The minimum zoom level of parents to look for.
* @param retain An object that we add the found tiles to.
*
* @return boolean Whether a parent was found.
*/
bool Map::findLoadedParent(const Tile::ID& id, int32_t minCoveringZoom, std::forward_list<Tile::ID>& retain) {
for (int32_t z = id.z - 1; z >= minCoveringZoom; --z) {
const Tile::ID parent_id = id.parent(z);
const TileData::State state = hasTile(parent_id);
if (state == TileData::State::parsed) {
retain.emplace_front(parent_id);
return true;
}
}
return false;
}
bool Map::updateTiles() {
bool changed = false;
// Figure out what tiles we need to load
int32_t zoom = transform.getZoom();
if (zoom > max_zoom) zoom = max_zoom;
if (zoom < min_zoom) zoom = min_zoom;
int32_t max_covering_zoom = zoom + 1;
if (max_covering_zoom > max_zoom) max_covering_zoom = max_zoom;
int32_t min_covering_zoom = zoom - 10;
if (min_covering_zoom < min_zoom) min_covering_zoom = min_zoom;
int32_t max_dim = pow(2, zoom);
// Map four viewport corners to pixel coordinates
box box = transform.mapCornersToBox(zoom);
// Performs a scanline algorithm search that covers the rectangle of the box
// and sorts them by proximity to the center.
std::forward_list<Tile::ID> required = llmr::covering_tiles(zoom, box, use_raster, (pixel_ratio > 1.0));
// Retain is a list of tiles that we shouldn't delete, even if they are not
// the most ideal tile for the current viewport. This may include tiles like
// parent or child tiles that are *already* loaded.
std::forward_list<Tile::ID> retain(required);
// Add existing child/parent tiles if the actual tile is not yet loaded
for (const Tile::ID& id : required) {
const TileData::State state = addTile(id);
if (state != TileData::State::parsed) {
if (use_raster && (transform.rotating || transform.scaling || transform.panning))
break;
// The tile we require is not yet loaded. Try to find a parent or
// child tile that we already have.
// First, try to find existing child tiles that completely cover the
// missing tile.
bool complete = findLoadedChildren(id, max_covering_zoom, retain);
// Then, if there are no complete child tiles, try to find existing
// parent tiles that completely cover the missing tile.
if (!complete) {
findLoadedParent(id, min_covering_zoom, retain);
}
}
if (state == TileData::State::initial) {
changed = true;
}
}
// Remove tiles that we definitely don't need, i.e. tiles that are not on
// the required list.
std::forward_list<Tile::ID> retain_data;
tiles.remove_if([&retain, &retain_data, &changed](const Tile & tile) {
bool obsolete = std::find(retain.begin(), retain.end(), tile.id) == retain.end();
if (obsolete) {
changed = true;
} else {
retain_data.push_front(tile.data->id);
}
return obsolete;
});
// Sort tiles by zoom level, front to back.
// We're painting front-to-back, so we want to draw more detailed tiles first
// before filling in other parts with lower zoom levels.
tiles.sort([](const Tile & a, const Tile & b) {
return a.id.z > b.id.z;
});
// Remove all the expired pointers from the list.
tile_data.remove_if([&retain_data](const std::weak_ptr<TileData>& tile_data) {
const std::shared_ptr<TileData> tile = tile_data.lock();
if (!tile) {
return true;
}
bool obsolete = std::find(retain_data.begin(), retain_data.end(), tile->id) == retain_data.end();
if (obsolete) {
tile->cancel();
return true;
} else {
return false;
}
});
return changed;
}
bool Map::render() {
if (transform.needsAnimation()) {
transform.updateAnimations();
}
if (*style.sprite->raster && !style.sprite->raster->textured) {
style.sprite->raster->setTexturepool(&texturepool);
}
bool changed = updateTiles();
painter.clear();
painter.changeMatrix();
// First, update all tile matrices with the new transform and render into
// the stencil buffer.
uint8_t i = 1;
painter.prepareClippingMask();
for (Tile& tile : tiles) {
if (tile.data && tile.data->state == TileData::State::parsed) {
// The position matrix.
transform.matrixFor(tile.matrix, tile.id);
matrix::multiply(tile.matrix, painter.projMatrix, tile.matrix);
tile.clip_id = i++;
painter.drawClippingMask(tile.matrix, tile.clip_id, !tile.data->use_raster);
}
}
painter.finishClippingMask();
for (const Tile& tile : tiles) {
if (tile.data && tile.data->state == TileData::State::parsed) {
if (tile.data->use_raster && *tile.data->raster && !tile.data->raster->textured) {
tile.data->raster->setTexturepool(&texturepool);
tile.data->raster->beginFadeInAnimation();
}
if (tile.data->use_raster && tile.data->raster->needsAnimation()) {
tile.data->raster->updateAnimations();
}
painter.render(tile);
}
}
painter.renderMatte();
return changed || transform.needsAnimation();
}