/***************************************************************************** * * This file is part of Mapnik (c++ mapping toolkit) * * Copyright (C) 2021 Artem Pavlenko * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA * *****************************************************************************/ // mapnik #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // for PROJ_ENVELOPE_POINTS #include #include #include // stl #include namespace mapnik { namespace { using E = detail::EnumStringT; constexpr detail::EnumMapT aspect_fix_mode_e_map{{ E{Map::aspect_fix_mode::GROW_BBOX, "GROW_BBOX"}, E{Map::aspect_fix_mode::GROW_CANVAS, "GROW_CANVAS"}, E{Map::aspect_fix_mode::SHRINK_BBOX, "SHRINK_BBOX"}, E{Map::aspect_fix_mode::SHRINK_CANVAS, "SHRINK_CANVAS"}, E{Map::aspect_fix_mode::ADJUST_BBOX_WIDTH, "ADJUST_BBOX_WIDTH"}, E{Map::aspect_fix_mode::ADJUST_BBOX_HEIGHT, "ADJUST_BBOX_HEIGHT"}, E{Map::aspect_fix_mode::ADJUST_CANVAS_WIDTH, "ADJUST_CANVAS_WIDTH"}, E{Map::aspect_fix_mode::ADJUST_CANVAS_HEIGHT, "ADJUST_CANVAS_HEIGHT"}, E{Map::aspect_fix_mode::RESPECT, "RESPECT"}, E{Map::aspect_fix_mode::aspect_fix_mode_MAX, ""}, }}; } // namespace IMPLEMENT_ENUM(aspect_fix_mode_e, Map::aspect_fix_mode) Map::Map() : width_(400) , height_(400) , srs_(MAPNIK_GEOGRAPHIC_PROJ) , buffer_size_(0) , background_image_comp_op_(src_over) , background_image_opacity_(1.0) , aspectFixMode_(GROW_BBOX) , base_path_("") , extra_params_() , font_directory_() , font_file_mapping_() , font_memory_cache_() {} Map::Map(int width, int height, std::string const& srs) : width_(width) , height_(height) , srs_(srs) , buffer_size_(0) , background_image_comp_op_(src_over) , background_image_opacity_(1.0) , aspectFixMode_(GROW_BBOX) , base_path_("") , extra_params_() , font_directory_() , font_file_mapping_() , font_memory_cache_() {} Map::Map(Map const& rhs) : width_(rhs.width_) , height_(rhs.height_) , srs_(rhs.srs_) , buffer_size_(rhs.buffer_size_) , background_(rhs.background_) , background_image_(rhs.background_image_) , background_image_comp_op_(rhs.background_image_comp_op_) , background_image_opacity_(rhs.background_image_opacity_) , styles_(rhs.styles_) , fontsets_(rhs.fontsets_) , layers_(rhs.layers_) , aspectFixMode_(rhs.aspectFixMode_) , current_extent_(rhs.current_extent_) , maximum_extent_(rhs.maximum_extent_) , base_path_(rhs.base_path_) , extra_params_(rhs.extra_params_) , font_directory_(rhs.font_directory_) , font_file_mapping_(rhs.font_file_mapping_) , // on copy discard memory caches font_memory_cache_() { init_proj_transforms(); } Map::Map(Map&& rhs) : width_(std::move(rhs.width_)) , height_(std::move(rhs.height_)) , srs_(std::move(rhs.srs_)) , buffer_size_(std::move(rhs.buffer_size_)) , background_(std::move(rhs.background_)) , background_image_(std::move(rhs.background_image_)) , background_image_comp_op_(std::move(rhs.background_image_comp_op_)) , background_image_opacity_(std::move(rhs.background_image_opacity_)) , styles_(std::move(rhs.styles_)) , fontsets_(std::move(rhs.fontsets_)) , layers_(std::move(rhs.layers_)) , aspectFixMode_(std::move(rhs.aspectFixMode_)) , current_extent_(std::move(rhs.current_extent_)) , maximum_extent_(std::move(rhs.maximum_extent_)) , base_path_(std::move(rhs.base_path_)) , extra_params_(std::move(rhs.extra_params_)) , font_directory_(std::move(rhs.font_directory_)) , font_file_mapping_(std::move(rhs.font_file_mapping_)) , font_memory_cache_(std::move(rhs.font_memory_cache_)) {} Map::~Map() {} Map& Map::operator=(Map rhs) { swap(*this, rhs); init_proj_transforms(); return *this; } void swap(Map& lhs, Map& rhs) { using std::swap; std::swap(lhs.width_, rhs.width_); std::swap(lhs.height_, rhs.height_); std::swap(lhs.srs_, rhs.srs_); std::swap(lhs.buffer_size_, rhs.buffer_size_); std::swap(lhs.background_, rhs.background_); std::swap(lhs.background_image_, rhs.background_image_); std::swap(lhs.background_image_comp_op_, rhs.background_image_comp_op_); std::swap(lhs.background_image_opacity_, rhs.background_image_opacity_); std::swap(lhs.styles_, rhs.styles_); std::swap(lhs.fontsets_, rhs.fontsets_); std::swap(lhs.layers_, rhs.layers_); std::swap(lhs.aspectFixMode_, rhs.aspectFixMode_); std::swap(lhs.current_extent_, rhs.current_extent_); std::swap(lhs.maximum_extent_, rhs.maximum_extent_); std::swap(lhs.base_path_, rhs.base_path_); std::swap(lhs.extra_params_, rhs.extra_params_); std::swap(lhs.font_directory_, rhs.font_directory_); std::swap(lhs.font_file_mapping_, rhs.font_file_mapping_); // on assignment discard memory caches // std::swap(lhs.font_memory_cache_,rhs.font_memory_cache_); } bool Map::operator==(Map const& rhs) const { return (width_ == rhs.width_) && (height_ == rhs.height_) && (srs_ == rhs.srs_) && (buffer_size_ == rhs.buffer_size_) && (background_ == rhs.background_) && (background_image_ == rhs.background_image_) && (background_image_comp_op_ == rhs.background_image_comp_op_) && (background_image_opacity_ == rhs.background_image_opacity_) && (styles_ == rhs.styles_) && (fontsets_ == rhs.fontsets_) && (layers_ == rhs.layers_) && (aspectFixMode_ == rhs.aspectFixMode_) && (current_extent_ == rhs.current_extent_) && (maximum_extent_ == rhs.maximum_extent_) && (base_path_ == rhs.base_path_) && (extra_params_ == rhs.extra_params_) && (font_directory_ == rhs.font_directory_) && (font_file_mapping_ == rhs.font_file_mapping_); // Note: we don't care about font_memory_cache in comparison } std::map const& Map::styles() const { return styles_; } std::map& Map::styles() { return styles_; } Map::style_iterator Map::begin_styles() { return styles_.begin(); } Map::style_iterator Map::end_styles() { return styles_.end(); } Map::const_style_iterator Map::begin_styles() const { return styles_.begin(); } Map::const_style_iterator Map::end_styles() const { return styles_.end(); } bool Map::insert_style(std::string const& name, feature_type_style const& style) { return styles_.emplace(name, style).second; } bool Map::insert_style(std::string const& name, feature_type_style&& style) { return styles_.emplace(name, std::move(style)).second; } void Map::remove_style(std::string const& name) { styles_.erase(name); } boost::optional Map::find_style(std::string const& name) const { std::map::const_iterator itr = styles_.find(name); if (itr != styles_.end()) return boost::optional(itr->second); else return boost::optional(); } bool Map::insert_fontset(std::string const& name, font_set const& fontset) { if (fontset.get_name() != name) { throw mapnik::config_error("Fontset name must match the name used to reference it on the map"); } return fontsets_.emplace(name, fontset).second; } bool Map::insert_fontset(std::string const& name, font_set&& fontset) { if (fontset.get_name() != name) { throw mapnik::config_error("Fontset name must match the name used to reference it on the map"); } return fontsets_.emplace(name, std::move(fontset)).second; } boost::optional Map::find_fontset(std::string const& name) const { std::map::const_iterator itr = fontsets_.find(name); if (itr != fontsets_.end()) return boost::optional(itr->second); else return boost::optional(); } std::map const& Map::fontsets() const { return fontsets_; } std::map& Map::fontsets() { return fontsets_; } bool Map::register_fonts(std::string const& dir, bool recurse) { font_library library; return freetype_engine::instance().register_fonts_impl(dir, library, font_file_mapping_, recurse); } bool Map::load_fonts() { bool result = false; auto const& global_mapping = freetype_engine::get_mapping(); for (auto const& kv : font_file_mapping_) // for every face-name -> idx/filepath { auto const& file_path = kv.second.second; // do not attemp to re-cache in memory if (font_memory_cache_.find(file_path) != font_memory_cache_.end()) { continue; } auto const& meta = global_mapping.find(kv.first); // no need to cache if global mapping already has same font if ((meta != global_mapping.end()) && (meta->second.second == file_path)) { continue; } mapnik::util::file file(file_path); if (file) { auto item = font_memory_cache_.emplace(file_path, std::make_pair(file.data(), file.size())); if (item.second) result = true; } } return result; } size_t Map::layer_count() const { return layers_.size(); } void Map::add_layer(layer const& l) { proj_transform_cache::init(srs_, l.srs()); layers_.emplace_back(l); } void Map::add_layer(layer&& l) { proj_transform_cache::init(srs_, l.srs()); layers_.push_back(std::move(l)); } void Map::remove_layer(size_t index) { layers_.erase(layers_.begin() + index); } void Map::remove_all() { layers_.clear(); styles_.clear(); fontsets_.clear(); font_file_mapping_.clear(); font_memory_cache_.clear(); } layer const& Map::get_layer(size_t index) const { return layers_[index]; } layer& Map::get_layer(size_t index) { return layers_[index]; } std::vector const& Map::layers() const { return layers_; } std::vector& Map::layers() { return layers_; } unsigned Map::width() const { return width_; } unsigned Map::height() const { return height_; } void Map::set_width(unsigned _width) { if (_width != width_ && _width >= MIN_MAPSIZE && _width <= MAX_MAPSIZE) { width_ = _width; fixAspectRatio(); } } void Map::set_height(unsigned _height) { if (_height != height_ && _height >= MIN_MAPSIZE && _height <= MAX_MAPSIZE) { height_ = _height; fixAspectRatio(); } } void Map::resize(unsigned _width, unsigned _height) { if ((_width != width_ || _height != height_) && _width >= MIN_MAPSIZE && _width <= MAX_MAPSIZE && _height >= MIN_MAPSIZE && _height <= MAX_MAPSIZE) { width_ = _width; height_ = _height; fixAspectRatio(); } } std::string const& Map::srs() const { return srs_; } void Map::set_srs(std::string const& _srs) { if (srs_ != _srs) init_proj_transforms(); srs_ = _srs; } void Map::set_buffer_size(int _buffer_size) { buffer_size_ = _buffer_size; } int Map::buffer_size() const { return buffer_size_; } boost::optional const& Map::background() const { return background_; } void Map::set_background(color const& c) { background_ = c; } boost::optional const& Map::background_image() const { return background_image_; } void Map::set_background_image(std::string const& image_filename) { background_image_ = image_filename; } composite_mode_e Map::background_image_comp_op() const { return background_image_comp_op_; } void Map::set_background_image_comp_op(composite_mode_e comp_op) { background_image_comp_op_ = comp_op; } float Map::background_image_opacity() const { return background_image_opacity_; } void Map::set_background_image_opacity(float opacity) { background_image_opacity_ = opacity; } void Map::set_maximum_extent(box2d const& box) { maximum_extent_.reset(box); } boost::optional> const& Map::maximum_extent() const { return maximum_extent_; } void Map::reset_maximum_extent() { maximum_extent_.reset(); } std::string const& Map::base_path() const { return base_path_; } void Map::set_base_path(std::string const& base) { base_path_ = base; } void Map::zoom(double factor) { coord2d center = current_extent_.center(); double w = factor * current_extent_.width(); double h = factor * current_extent_.height(); current_extent_ = box2d(center.x - 0.5 * w, center.y - 0.5 * h, center.x + 0.5 * w, center.y + 0.5 * h); fixAspectRatio(); } void Map::zoom_all() { try { if (layers_.empty()) { return; } box2d ext; bool success = false; bool first = true; for (auto const& layer : layers_) { if (layer.active()) { std::string const& layer_srs = layer.srs(); proj_transform const* proj_trans_ptr = proj_transform_cache::get(srs_, layer_srs); box2d layer_ext = layer.envelope(); if (proj_trans_ptr->backward(layer_ext, PROJ_ENVELOPE_POINTS)) { success = true; MAPNIK_LOG_DEBUG(map) << "map: Layer " << layer.name() << " original ext=" << layer.envelope(); MAPNIK_LOG_DEBUG(map) << "map: Layer " << layer.name() << " transformed to map srs=" << layer_ext; if (first) { ext = layer_ext; first = false; } else { ext.expand_to_include(layer_ext); } } } } if (success) { if (maximum_extent_) { ext.clip(*maximum_extent_); } zoom_to_box(ext); } else { if (maximum_extent_) { MAPNIK_LOG_ERROR(map) << "could not zoom to combined layer extents" << " so falling back to maximum-extent for zoom_all result"; zoom_to_box(*maximum_extent_); } else { std::ostringstream s; s << "could not zoom to combined layer extents " << "using zoom_all because proj4 could not " << "back project any layer extents into the map srs " << "(set map 'maximum-extent' to override layer extents)"; throw std::runtime_error(s.str()); } } } catch (proj_init_error const& ex) { throw mapnik::config_error(std::string("Projection error during map.zoom_all: ") + ex.what()); } } void Map::zoom_to_box(box2d const& box) { current_extent_ = box; fixAspectRatio(); } void Map::fixAspectRatio() { if (aspectFixMode_ == RESPECT) return; if (current_extent_.width() > 0 && current_extent_.height() > 0) { double ratio1 = static_cast(width_) / static_cast(height_); double ratio2 = current_extent_.width() / current_extent_.height(); if (ratio1 == ratio2) return; switch (aspectFixMode_) { case ADJUST_BBOX_HEIGHT: current_extent_.height(current_extent_.width() / ratio1); break; case ADJUST_BBOX_WIDTH: current_extent_.width(current_extent_.height() * ratio1); break; case ADJUST_CANVAS_HEIGHT: height_ = static_cast(std::floor(static_cast(width_) / ratio2 + 0.5)); break; case ADJUST_CANVAS_WIDTH: width_ = static_cast(std::floor(static_cast(height_) * ratio2 + 0.5)); break; case GROW_BBOX: if (ratio2 > ratio1) current_extent_.height(current_extent_.width() / ratio1); else current_extent_.width(current_extent_.height() * ratio1); break; case SHRINK_BBOX: if (ratio2 < ratio1) current_extent_.height(current_extent_.width() / ratio1); else current_extent_.width(current_extent_.height() * ratio1); break; case GROW_CANVAS: if (ratio2 > ratio1) width_ = static_cast(std::floor(static_cast(height_) * ratio2 + 0.5)); else height_ = static_cast(std::floor(static_cast(width_) / ratio2 + 0.5)); break; case SHRINK_CANVAS: if (ratio2 > ratio1) height_ = static_cast(std::floor(static_cast(width_) / ratio2 + 0.5)); else width_ = static_cast(std::floor(static_cast(height_) * ratio2 + 0.5)); break; default: if (ratio2 > ratio1) current_extent_.height(current_extent_.width() / ratio1); else current_extent_.width(current_extent_.height() * ratio1); break; } } } box2d const& Map::get_current_extent() const { return current_extent_; } box2d Map::get_buffered_extent() const { double extra = 2.0 * scale() * buffer_size_; box2d ext(current_extent_); ext.width(current_extent_.width() + extra); ext.height(current_extent_.height() + extra); return ext; } void Map::pan(int x, int y) { int dx = x - int(0.5 * width_); int dy = int(0.5 * height_) - y; double s = static_cast(width_) / current_extent_.width(); double minx = current_extent_.minx() + dx / s; double maxx = current_extent_.maxx() + dx / s; double miny = current_extent_.miny() + dy / s; double maxy = current_extent_.maxy() + dy / s; current_extent_.init(minx, miny, maxx, maxy); } void Map::pan_and_zoom(int x, int y, double factor) { pan(x, y); zoom(factor); } double Map::scale() const { if (width_ > 0) return current_extent_.width() / static_cast(width_); return current_extent_.width(); } double Map::scale_denominator() const { projection map_proj(srs_); return mapnik::scale_denominator(scale(), map_proj.is_geographic()); } view_transform Map::transform() const { return view_transform(width_, height_, current_extent_); } featureset_ptr Map::query_point(unsigned index, double x, double y) const { if (!current_extent_.valid()) { throw std::runtime_error( "query_point: map extent is not initialized, you need to set a valid extent before querying"); } if (!current_extent_.intersects(x, y)) { throw std::runtime_error("query_point: x,y coords do not intersect map extent"); } if (index < layers_.size()) { mapnik::layer const& layer = layers_[index]; mapnik::datasource_ptr ds = layer.datasource(); if (ds) { proj_transform const* proj_trans_ptr = proj_transform_cache::get(layer.srs(), srs_); double z = 0; if (!proj_trans_ptr->equal() && !proj_trans_ptr->backward(x, y, z)) { throw std::runtime_error("query_point: could not project x,y into layer srs"); } // calculate default tolerance mapnik::box2d map_ex = current_extent_; if (maximum_extent_) { map_ex.clip(*maximum_extent_); } if (!proj_trans_ptr->backward(map_ex, PROJ_ENVELOPE_POINTS)) { std::ostringstream s; s << "query_point: could not project map extent '" << map_ex << "' into layer srs for tolerance calculation"; throw std::runtime_error(s.str()); } double tol = (map_ex.maxx() - map_ex.minx()) / static_cast(width_) * 3; featureset_ptr fs = ds->features_at_point(mapnik::coord2d(x, y), tol); MAPNIK_LOG_DEBUG(map) << "map: Query at point tol=" << tol << "(" << x << "," << y << ")"; if (fs) { return std::make_shared>(fs, hit_test_filter(x, y, tol)); } } } else { std::ostringstream s; s << "Invalid layer index passed to query_point: '" << index << "'"; if (!layers_.empty()) s << " for map with " << layers_.size() << " layers(s)"; else s << " (map has no layers)"; throw std::out_of_range(s.str()); } return mapnik::make_invalid_featureset(); } featureset_ptr Map::query_map_point(unsigned index, double x, double y) const { view_transform tr = transform(); tr.backward(&x, &y); return query_point(index, x, y); } parameters const& Map::get_extra_parameters() const { return extra_params_; } parameters& Map::get_extra_parameters() { return extra_params_; } void Map::set_extra_parameters(parameters& params) { extra_params_ = params; } void Map::init_proj_transforms() { std::for_each(layers_.begin(), layers_.end(), [this](auto const& l) { proj_transform_cache::init(srs_, l.srs()); }); } } // namespace mapnik