Compare commits
7 commits
master
...
marker-ref
Author | SHA1 | Date | |
---|---|---|---|
|
99071d1253 | ||
|
8aae463c10 | ||
|
6b33feceab | ||
|
006aadcb89 | ||
|
a50051bded | ||
|
8abda7c7ae | ||
|
3a44fecf6f |
16 changed files with 651 additions and 233 deletions
99
bindings/python/mapnik_marker_cache.cpp
Normal file
99
bindings/python/mapnik_marker_cache.cpp
Normal file
|
@ -0,0 +1,99 @@
|
|||
/*****************************************************************************
|
||||
*
|
||||
* This file is part of Mapnik (c++ mapping toolkit)
|
||||
*
|
||||
* Copyright (C) 2006 Artem Pavlenko, Jean-Francois Doyon
|
||||
*
|
||||
* 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
|
||||
*
|
||||
*****************************************************************************/
|
||||
|
||||
#include <mapnik/utils.hpp>
|
||||
#include <mapnik/marker.hpp>
|
||||
#include <mapnik/marker_cache.hpp>
|
||||
#include <mapnik/graphics.hpp>
|
||||
|
||||
// boost
|
||||
#include <boost/make_shared.hpp>
|
||||
#include <boost/python.hpp>
|
||||
#include <boost/noncopyable.hpp>
|
||||
|
||||
namespace {
|
||||
|
||||
bool add_marker_from_image(mapnik::marker_cache & cache, std::string const& uri, mapnik::image_32 const& im)
|
||||
{
|
||||
boost::optional<mapnik::image_ptr> imagep(boost::make_shared<mapnik::image_data_32>(im.data()));
|
||||
return cache.insert_marker(uri,boost::make_shared<mapnik::marker>(imagep),true);
|
||||
}
|
||||
|
||||
bool add_marker_from_svg(mapnik::marker_cache & cache, std::string const& uri, mapnik::svg_storage_type const& svg)
|
||||
{
|
||||
mapnik::svg_path_ptr marker_path(boost::make_shared<mapnik::svg_storage_type>(svg));
|
||||
return cache.insert_marker(uri,boost::make_shared<mapnik::marker>(marker_path),true);
|
||||
}
|
||||
|
||||
boost::python::object get_marker(boost::shared_ptr<mapnik::marker_cache> const& cache, std::string const& uri)
|
||||
{
|
||||
mapnik::marker_cache::iterator_type itr = cache->search(uri);
|
||||
mapnik::marker_cache::iterator_type end = cache->end();
|
||||
if (itr != end)
|
||||
{
|
||||
if (itr->second->is_bitmap())
|
||||
{
|
||||
mapnik::image_data_32 const& im = *itr->second->get_bitmap_data()->get();
|
||||
return boost::python::object(boost::make_shared<mapnik::image_32>(im));
|
||||
}
|
||||
return boost::python::object(*(itr->second->get_vector_data()));
|
||||
}
|
||||
return boost::python::object();
|
||||
}
|
||||
|
||||
boost::python::list get_keys(boost::shared_ptr<mapnik::marker_cache> const& cache)
|
||||
{
|
||||
boost::python::list l;
|
||||
mapnik::marker_cache::iterator_type itr = cache->begin();
|
||||
mapnik::marker_cache::iterator_type end = cache->end();
|
||||
for (;itr != end; ++itr)
|
||||
{
|
||||
l.append(itr->first);
|
||||
}
|
||||
return l;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void export_marker_cache()
|
||||
{
|
||||
using mapnik::marker_cache;
|
||||
using mapnik::singleton;
|
||||
using mapnik::CreateUsingNew;
|
||||
using namespace boost::python;
|
||||
class_<singleton<marker_cache,CreateUsingNew>,boost::noncopyable>("Singleton",no_init)
|
||||
.def("instance",&singleton<marker_cache,CreateUsingNew>::instance,
|
||||
return_value_policy<reference_existing_object>())
|
||||
.staticmethod("instance")
|
||||
;
|
||||
|
||||
class_<marker_cache,bases<singleton<marker_cache,CreateUsingNew> >,
|
||||
boost::noncopyable>("MarkerCache",no_init)
|
||||
.def("clear",&marker_cache::clear)
|
||||
.def("remove",&marker_cache::remove)
|
||||
.def("size",&marker_cache::size)
|
||||
.def("put",&add_marker_from_image)
|
||||
.def("put",&add_marker_from_svg)
|
||||
.def("get",&get_marker)
|
||||
.def("keys",&get_keys)
|
||||
;
|
||||
}
|
|
@ -34,6 +34,7 @@ void export_envelope();
|
|||
void export_query();
|
||||
void export_geometry();
|
||||
void export_palette();
|
||||
void export_svg();
|
||||
void export_image();
|
||||
void export_image_view();
|
||||
void export_gamma_method();
|
||||
|
@ -64,6 +65,7 @@ void export_text_placement();
|
|||
void export_shield_symbolizer();
|
||||
void export_debug_symbolizer();
|
||||
void export_font_engine();
|
||||
void export_marker_cache();
|
||||
void export_projection();
|
||||
void export_proj_transform();
|
||||
void export_view_transform();
|
||||
|
@ -441,6 +443,7 @@ BOOST_PYTHON_MODULE(_mapnik)
|
|||
export_color();
|
||||
export_envelope();
|
||||
export_palette();
|
||||
export_svg();
|
||||
export_image();
|
||||
export_image_view();
|
||||
export_gamma_method();
|
||||
|
@ -466,6 +469,7 @@ BOOST_PYTHON_MODULE(_mapnik)
|
|||
export_shield_symbolizer();
|
||||
export_debug_symbolizer();
|
||||
export_font_engine();
|
||||
export_marker_cache();
|
||||
export_projection();
|
||||
export_proj_transform();
|
||||
export_view_transform();
|
||||
|
|
62
bindings/python/mapnik_svg.cpp
Normal file
62
bindings/python/mapnik_svg.cpp
Normal file
|
@ -0,0 +1,62 @@
|
|||
/*****************************************************************************
|
||||
*
|
||||
* This file is part of Mapnik (c++ mapping toolkit)
|
||||
*
|
||||
* Copyright (C) 2006 Artem Pavlenko, Jean-Francois Doyon
|
||||
*
|
||||
* 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
|
||||
*
|
||||
*****************************************************************************/
|
||||
|
||||
// boost
|
||||
#include <boost/python.hpp>
|
||||
#include <boost/python/module.hpp>
|
||||
#include <boost/python/def.hpp>
|
||||
#include <boost/make_shared.hpp>
|
||||
|
||||
// mapnik
|
||||
#include <mapnik/marker.hpp>
|
||||
|
||||
namespace {
|
||||
|
||||
mapnik::svg_path_ptr open_from_file(std::string const& filename)
|
||||
{
|
||||
return mapnik::read_svg_marker(filename);
|
||||
}
|
||||
|
||||
mapnik::svg_path_ptr fromstring(std::string const& svg)
|
||||
{
|
||||
return mapnik::read_svg_marker(svg,true);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void export_svg()
|
||||
{
|
||||
using namespace boost::python;
|
||||
using mapnik::svg_storage_type;
|
||||
|
||||
class_<svg_storage_type,boost::shared_ptr<svg_storage_type> >("SVG","This class represents an svg object.",no_init)
|
||||
.def("width",&svg_storage_type::width)
|
||||
.def("height",&svg_storage_type::height)
|
||||
.def("extent",make_function(&svg_storage_type::bounding_box,
|
||||
return_value_policy<copy_const_reference>()))
|
||||
.def("open",&open_from_file)
|
||||
.staticmethod("open")
|
||||
.def("fromstring",&fromstring)
|
||||
.staticmethod("fromstring")
|
||||
;
|
||||
|
||||
}
|
|
@ -57,6 +57,7 @@ private:
|
|||
public:
|
||||
image_32(int width,int height);
|
||||
image_32(image_32 const& rhs);
|
||||
explicit image_32(image_data_32 const& rhs);
|
||||
#ifdef HAVE_CAIRO
|
||||
explicit image_32(cairo_surface_ptr const& surface);
|
||||
#endif
|
||||
|
|
|
@ -62,6 +62,19 @@ struct MAPNIK_DECL image_reader : private mapnik::noncopyable
|
|||
virtual ~image_reader() {}
|
||||
};
|
||||
|
||||
struct image_reader_guard
|
||||
{
|
||||
image_reader_guard(image_reader * reader)
|
||||
: r_(reader) {}
|
||||
|
||||
~image_reader_guard()
|
||||
{
|
||||
if (r_) delete r_;
|
||||
}
|
||||
image_reader * r_;
|
||||
};
|
||||
|
||||
|
||||
bool register_image_reader(std::string const& type,image_reader* (*)(std::string const&));
|
||||
bool register_image_reader(std::string const& type,image_reader* (*)(char const*, std::size_t));
|
||||
|
||||
|
|
|
@ -27,18 +27,13 @@
|
|||
#include <mapnik/global.hpp>
|
||||
#include <mapnik/image_data.hpp>
|
||||
#include <mapnik/svg/svg_path_attributes.hpp>
|
||||
#include <mapnik/svg/svg_storage.hpp>
|
||||
#include <mapnik/svg/svg_path_adapter.hpp>
|
||||
#include <mapnik/svg/svg_storage.hpp>
|
||||
#include <mapnik/noncopyable.hpp>
|
||||
|
||||
// agg
|
||||
#include "agg_path_storage.h"
|
||||
|
||||
// boost
|
||||
#include <boost/unordered_map.hpp>
|
||||
#include <boost/shared_ptr.hpp>
|
||||
#include <boost/optional.hpp>
|
||||
#include <boost/make_shared.hpp>
|
||||
|
||||
// stl
|
||||
#include <cassert>
|
||||
|
@ -51,6 +46,14 @@ typedef agg::pod_bvector<mapnik::svg::path_attributes> attr_storage;
|
|||
typedef mapnik::svg::svg_storage<mapnik::svg::svg_path_storage,attr_storage> svg_storage_type;
|
||||
typedef boost::shared_ptr<svg_storage_type> svg_path_ptr;
|
||||
typedef boost::shared_ptr<image_data_32> image_ptr;
|
||||
|
||||
class marker;
|
||||
|
||||
typedef boost::shared_ptr<marker> marker_ptr;
|
||||
|
||||
mapnik::svg_path_ptr read_svg_marker(std::string const& uri, bool from_string=false);
|
||||
mapnik::image_ptr read_bitmap_marker(std::string const& uri, bool from_string=false);
|
||||
|
||||
/**
|
||||
* A class to hold either vector or bitmap marker data. This allows these to be treated equally
|
||||
* in the image caches and most of the render paths.
|
||||
|
@ -58,96 +61,24 @@ typedef boost::shared_ptr<image_data_32> image_ptr;
|
|||
class marker: private mapnik::noncopyable
|
||||
{
|
||||
public:
|
||||
marker()
|
||||
{
|
||||
// create default OGC 4x4 black pixel
|
||||
bitmap_data_ = boost::optional<mapnik::image_ptr>(boost::make_shared<image_data_32>(4,4));
|
||||
(*bitmap_data_)->set(0xff000000);
|
||||
}
|
||||
|
||||
marker(boost::optional<mapnik::image_ptr> const& data)
|
||||
: bitmap_data_(data)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
marker(boost::optional<mapnik::svg_path_ptr> const& data)
|
||||
: vector_data_(data)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
marker(marker const& rhs)
|
||||
: bitmap_data_(rhs.bitmap_data_),
|
||||
vector_data_(rhs.vector_data_)
|
||||
{}
|
||||
|
||||
box2d<double> bounding_box() const
|
||||
{
|
||||
if (is_vector())
|
||||
{
|
||||
return (*vector_data_)->bounding_box();
|
||||
}
|
||||
if (is_bitmap())
|
||||
{
|
||||
double width = (*bitmap_data_)->width();
|
||||
double height = (*bitmap_data_)->height();
|
||||
return box2d<double>(0, 0, width, height);
|
||||
}
|
||||
return box2d<double>();
|
||||
}
|
||||
|
||||
inline double width() const
|
||||
{
|
||||
if (is_bitmap())
|
||||
{
|
||||
return (*bitmap_data_)->width();
|
||||
}
|
||||
else if (is_vector())
|
||||
{
|
||||
return (*vector_data_)->bounding_box().width();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
inline double height() const
|
||||
{
|
||||
if (is_bitmap())
|
||||
{
|
||||
return (*bitmap_data_)->height();
|
||||
}
|
||||
else if (is_vector())
|
||||
{
|
||||
return (*vector_data_)->bounding_box().height();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
inline bool is_bitmap() const
|
||||
{
|
||||
return bitmap_data_;
|
||||
}
|
||||
|
||||
inline bool is_vector() const
|
||||
{
|
||||
return vector_data_;
|
||||
}
|
||||
|
||||
boost::optional<mapnik::image_ptr> get_bitmap_data() const
|
||||
{
|
||||
return bitmap_data_;
|
||||
}
|
||||
|
||||
boost::optional<mapnik::svg_path_ptr> get_vector_data() const
|
||||
{
|
||||
return vector_data_;
|
||||
}
|
||||
|
||||
marker();
|
||||
marker(boost::optional<mapnik::image_ptr> const& data);
|
||||
marker(boost::optional<mapnik::svg_path_ptr> const& data);
|
||||
marker(marker const& rhs);
|
||||
box2d<double> bounding_box() const;
|
||||
double width() const;
|
||||
double height() const;
|
||||
bool is_bitmap() const;
|
||||
bool is_vector() const;
|
||||
boost::optional<mapnik::image_ptr> get_bitmap_data() const;
|
||||
boost::optional<mapnik::svg_path_ptr> get_vector_data() const;
|
||||
private:
|
||||
boost::optional<mapnik::image_ptr> bitmap_data_;
|
||||
boost::optional<mapnik::svg_path_ptr> vector_data_;
|
||||
|
||||
};
|
||||
|
||||
|
||||
}
|
||||
|
||||
#endif // MAPNIK_MARKER_HPP
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
#define MAPNIK_MARKER_CACHE_HPP
|
||||
|
||||
// mapnik
|
||||
#include <mapnik/marker.hpp>
|
||||
#include <mapnik/utils.hpp>
|
||||
#include <mapnik/config.hpp>
|
||||
#include <mapnik/noncopyable.hpp>
|
||||
|
@ -36,11 +37,6 @@
|
|||
namespace mapnik
|
||||
{
|
||||
|
||||
class marker;
|
||||
|
||||
typedef boost::shared_ptr<marker> marker_ptr;
|
||||
|
||||
|
||||
class MAPNIK_DECL marker_cache :
|
||||
public singleton <marker_cache, CreateUsingNew>,
|
||||
private mapnik::noncopyable
|
||||
|
@ -49,15 +45,24 @@ class MAPNIK_DECL marker_cache :
|
|||
private:
|
||||
marker_cache();
|
||||
~marker_cache();
|
||||
bool insert_marker(std::string const& key, marker_ptr path);
|
||||
boost::unordered_map<std::string,marker_ptr> marker_cache_;
|
||||
bool insert_svg(std::string const& name, std::string const& svg_string);
|
||||
boost::unordered_map<std::string,std::string> svg_cache_;
|
||||
public:
|
||||
typedef boost::unordered_map<std::string, marker_ptr>::const_iterator iterator_type;
|
||||
typedef boost::unordered_map<std::string, marker_ptr>::size_type size_type;
|
||||
bool insert_marker(std::string const& key, marker_ptr path, bool override=false);
|
||||
std::string known_svg_prefix_;
|
||||
std::string known_image_prefix_;
|
||||
void init();
|
||||
bool is_uri(std::string const& path);
|
||||
boost::optional<marker_ptr> find(std::string const& key, bool update_cache = false);
|
||||
bool is_svg_uri(std::string const& path);
|
||||
bool is_image_uri(std::string const& path);
|
||||
boost::optional<marker_ptr> find(std::string const& uri, bool update_cache = false);
|
||||
iterator_type search(std::string const& uri) const { return marker_cache_.find(uri); }
|
||||
void clear();
|
||||
bool remove(std::string const& uri);
|
||||
size_type size() const { return marker_cache_.size(); }
|
||||
iterator_type begin() const { return marker_cache_.begin(); }
|
||||
iterator_type end() const { return marker_cache_.end(); }
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
@ -24,9 +24,7 @@
|
|||
#define MAPNIK_SVG_PARSER_HPP
|
||||
|
||||
// mapnik
|
||||
#include <mapnik/svg/svg_path_attributes.hpp>
|
||||
#include <mapnik/svg/svg_converter.hpp>
|
||||
#include <mapnik/svg/svg_path_adapter.hpp>
|
||||
#include <mapnik/gradient.hpp>
|
||||
#include <mapnik/noncopyable.hpp>
|
||||
|
||||
|
|
|
@ -25,19 +25,28 @@
|
|||
|
||||
// mapnik
|
||||
#include <mapnik/box2d.hpp>
|
||||
#include <mapnik/noncopyable.hpp>
|
||||
|
||||
namespace mapnik {
|
||||
namespace svg {
|
||||
|
||||
template <typename VertexSource ,typename AttributeSource>
|
||||
class svg_storage : mapnik::noncopyable
|
||||
template <typename VertexSource,typename AttributeSource>
|
||||
class svg_storage
|
||||
{
|
||||
public:
|
||||
svg_storage() :
|
||||
source_(),
|
||||
attributes_(),
|
||||
bounding_box_(),
|
||||
svg_width_(0),
|
||||
svg_height_(0) {}
|
||||
|
||||
svg_storage(svg_storage const& rhs)
|
||||
: source_(rhs.source_),
|
||||
attributes_(rhs.attributes_),
|
||||
bounding_box_(rhs.bounding_box_),
|
||||
svg_width_(rhs.svg_width_),
|
||||
svg_height_(rhs.svg_height_) {}
|
||||
|
||||
VertexSource & source() // FIXME!! make const
|
||||
{
|
||||
return source_;
|
||||
|
|
|
@ -109,6 +109,7 @@ else: # unix, non-macos
|
|||
|
||||
source = Split(
|
||||
"""
|
||||
marker.cpp
|
||||
debug_symbolizer.cpp
|
||||
request.cpp
|
||||
well_known_srs.cpp
|
||||
|
|
|
@ -53,6 +53,12 @@ image_32::image_32(const image_32& rhs)
|
|||
data_(rhs.data_),
|
||||
painted_(rhs.painted_) {}
|
||||
|
||||
image_32::image_32(const image_data_32& rhs)
|
||||
:width_(rhs.width()),
|
||||
height_(rhs.height()),
|
||||
data_(rhs),
|
||||
painted_(false) {}
|
||||
|
||||
#ifdef HAVE_CAIRO
|
||||
image_32::image_32(cairo_surface_ptr const& surface)
|
||||
:width_(cairo_image_surface_get_width(&*surface)),
|
||||
|
|
168
src/marker.cpp
Normal file
168
src/marker.cpp
Normal file
|
@ -0,0 +1,168 @@
|
|||
/*****************************************************************************
|
||||
*
|
||||
* This file is part of Mapnik (c++ mapping toolkit)
|
||||
*
|
||||
* Copyright (C) 2013 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 <mapnik/marker.hpp>
|
||||
#include <mapnik/svg/svg_converter.hpp>
|
||||
#include <mapnik/svg/svg_path_adapter.hpp>
|
||||
#include <mapnik/svg/svg_parser.hpp>
|
||||
#include <mapnik/image_util.hpp>
|
||||
#include <mapnik/image_reader.hpp>
|
||||
|
||||
// agg
|
||||
#include "agg_rendering_buffer.h"
|
||||
#include "agg_pixfmt_rgba.h"
|
||||
|
||||
// boost
|
||||
#include <boost/make_shared.hpp>
|
||||
|
||||
namespace mapnik
|
||||
{
|
||||
|
||||
mapnik::svg_path_ptr read_svg_marker(std::string const& uri, bool from_string)
|
||||
{
|
||||
using namespace mapnik::svg;
|
||||
svg_path_ptr marker_path(boost::make_shared<svg_storage_type>());
|
||||
vertex_stl_adapter<svg_path_storage> stl_storage(marker_path->source());
|
||||
svg_path_adapter svg_path(stl_storage);
|
||||
svg_converter_type svg(svg_path, marker_path->attributes());
|
||||
svg_parser p(svg);
|
||||
if (from_string)
|
||||
{
|
||||
p.parse_from_string(uri);
|
||||
}
|
||||
else
|
||||
{
|
||||
p.parse(uri);
|
||||
}
|
||||
double lox,loy,hix,hiy;
|
||||
svg.bounding_rect(&lox, &loy, &hix, &hiy);
|
||||
marker_path->set_bounding_box(lox,loy,hix,hiy);
|
||||
marker_path->set_dimensions(svg.width(),svg.height());
|
||||
return marker_path;
|
||||
}
|
||||
|
||||
mapnik::image_ptr read_bitmap_marker(std::string const& uri, bool from_string)
|
||||
{
|
||||
mapnik::image_reader * reader = NULL;
|
||||
mapnik::image_reader_guard guard(reader);
|
||||
if (from_string)
|
||||
{
|
||||
reader = mapnik::get_image_reader(uri.data(),uri.size());
|
||||
}
|
||||
else
|
||||
{
|
||||
reader = mapnik::get_image_reader(uri);
|
||||
}
|
||||
if (!reader) throw std::runtime_error("could not intialize reader for: '" + uri + "'");
|
||||
unsigned width = reader->width();
|
||||
unsigned height = reader->height();
|
||||
BOOST_ASSERT(width > 0 && height > 0);
|
||||
mapnik::image_ptr image(boost::make_shared<mapnik::image_data_32>(width,height));
|
||||
reader->read(0,0,*image);
|
||||
if (!reader->premultiplied_alpha())
|
||||
{
|
||||
agg::rendering_buffer buffer(image->getBytes(),image->width(),image->height(),image->width() * 4);
|
||||
agg::pixfmt_rgba32 pixf(buffer);
|
||||
pixf.premultiply();
|
||||
}
|
||||
return image;
|
||||
}
|
||||
|
||||
|
||||
marker::marker()
|
||||
{
|
||||
// create default OGC 4x4 black pixel
|
||||
bitmap_data_ = boost::optional<mapnik::image_ptr>(boost::make_shared<image_data_32>(4,4));
|
||||
(*bitmap_data_)->set(0xff000000);
|
||||
}
|
||||
|
||||
marker::marker(boost::optional<mapnik::image_ptr> const& data)
|
||||
: bitmap_data_(data)
|
||||
{
|
||||
}
|
||||
|
||||
marker::marker(boost::optional<mapnik::svg_path_ptr> const& data)
|
||||
: vector_data_(data)
|
||||
{
|
||||
}
|
||||
|
||||
marker::marker(marker const& rhs)
|
||||
: bitmap_data_(rhs.bitmap_data_),
|
||||
vector_data_(rhs.vector_data_)
|
||||
{}
|
||||
|
||||
box2d<double> marker::bounding_box() const
|
||||
{
|
||||
if (is_vector())
|
||||
{
|
||||
return (*vector_data_)->bounding_box();
|
||||
}
|
||||
if (is_bitmap())
|
||||
{
|
||||
double width = (*bitmap_data_)->width();
|
||||
double height = (*bitmap_data_)->height();
|
||||
return box2d<double>(0, 0, width, height);
|
||||
}
|
||||
return box2d<double>();
|
||||
}
|
||||
|
||||
|
||||
double marker::width() const
|
||||
{
|
||||
if (is_bitmap())
|
||||
return (*bitmap_data_)->width();
|
||||
else if (is_vector())
|
||||
return (*vector_data_)->bounding_box().width();
|
||||
return 0;
|
||||
}
|
||||
|
||||
double marker::height() const
|
||||
{
|
||||
if (is_bitmap())
|
||||
return (*bitmap_data_)->height();
|
||||
else if (is_vector())
|
||||
return (*vector_data_)->bounding_box().height();
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool marker::is_bitmap() const
|
||||
{
|
||||
return bitmap_data_;
|
||||
}
|
||||
|
||||
bool marker::is_vector() const
|
||||
{
|
||||
return vector_data_;
|
||||
}
|
||||
|
||||
boost::optional<mapnik::image_ptr> marker::get_bitmap_data() const
|
||||
{
|
||||
return bitmap_data_;
|
||||
}
|
||||
|
||||
boost::optional<mapnik::svg_path_ptr> marker::get_vector_data() const
|
||||
{
|
||||
return vector_data_;
|
||||
}
|
||||
|
||||
}
|
|
@ -24,13 +24,7 @@
|
|||
#include <mapnik/debug.hpp>
|
||||
#include <mapnik/marker.hpp>
|
||||
#include <mapnik/marker_cache.hpp>
|
||||
#include <mapnik/svg/svg_parser.hpp>
|
||||
#include <mapnik/svg/svg_storage.hpp>
|
||||
#include <mapnik/svg/svg_converter.hpp>
|
||||
#include <mapnik/svg/svg_path_adapter.hpp>
|
||||
#include <mapnik/svg/svg_path_attributes.hpp>
|
||||
#include <mapnik/image_util.hpp>
|
||||
#include <mapnik/image_reader.hpp>
|
||||
|
||||
// boost
|
||||
#include <boost/assert.hpp>
|
||||
|
@ -38,36 +32,39 @@
|
|||
#include <boost/algorithm/string.hpp>
|
||||
#include <boost/make_shared.hpp>
|
||||
|
||||
// agg
|
||||
#include "agg_rendering_buffer.h"
|
||||
#include "agg_pixfmt_rgba.h"
|
||||
|
||||
namespace mapnik
|
||||
{
|
||||
|
||||
marker_cache::marker_cache()
|
||||
: known_svg_prefix_("shape://")
|
||||
: known_svg_prefix_("shape://"),
|
||||
known_image_prefix_("image://")
|
||||
{
|
||||
insert_svg("ellipse",
|
||||
"<?xml version='1.0' standalone='no'?>"
|
||||
"<svg width='100%' height='100%' version='1.1' xmlns='http://www.w3.org/2000/svg'>"
|
||||
"<ellipse rx='5' ry='5' fill='#0000FF' stroke='black' stroke-width='.5'/>"
|
||||
"</svg>");
|
||||
insert_svg("arrow",
|
||||
"<?xml version='1.0' standalone='no'?>"
|
||||
"<svg width='100%' height='100%' version='1.1' xmlns='http://www.w3.org/2000/svg'>"
|
||||
"<path fill='#0000FF' stroke='black' stroke-width='.5' d='m 31.698405,7.5302648 -8.910967,-6.0263712 0.594993,4.8210971 -18.9822542,0 0,2.4105482 18.9822542,0 -0.594993,4.8210971 z'/>"
|
||||
"</svg>");
|
||||
init();
|
||||
}
|
||||
|
||||
marker_cache::~marker_cache() {}
|
||||
|
||||
void marker_cache::init()
|
||||
{
|
||||
std::string ellipse =
|
||||
"<?xml version='1.0' standalone='no'?>"
|
||||
"<svg width='100%' height='100%' version='1.1' xmlns='http://www.w3.org/2000/svg'>"
|
||||
"<ellipse rx='5' ry='5' fill='#0000FF' stroke='black' stroke-width='.5'/>"
|
||||
"</svg>";
|
||||
marker_cache_.insert(std::make_pair("shape://ellipse",boost::make_shared<marker>(read_svg_marker(ellipse,true))));
|
||||
std::string arrow =
|
||||
"<?xml version='1.0' standalone='no'?>"
|
||||
"<svg width='100%' height='100%' version='1.1' xmlns='http://www.w3.org/2000/svg'>"
|
||||
"<path fill='#0000FF' stroke='black' stroke-width='.5' d='m 31.698405,7.5302648 -8.910967,-6.0263712 0.594993,4.8210971 -18.9822542,0 0,2.4105482 18.9822542,0 -0.594993,4.8210971 z'/>"
|
||||
"</svg>";
|
||||
marker_cache_.insert(std::make_pair("shape://arrow",boost::make_shared<marker>(read_svg_marker(arrow,true))));
|
||||
}
|
||||
|
||||
void marker_cache::clear()
|
||||
{
|
||||
#ifdef MAPNIK_THREADSAFE
|
||||
mutex::scoped_lock lock(mutex_);
|
||||
#endif
|
||||
typedef boost::unordered_map<std::string, marker_ptr>::const_iterator iterator_type;
|
||||
iterator_type itr = marker_cache_.begin();
|
||||
while(itr != marker_cache_.end())
|
||||
{
|
||||
|
@ -82,147 +79,103 @@ void marker_cache::clear()
|
|||
}
|
||||
}
|
||||
|
||||
bool marker_cache::is_uri(std::string const& path)
|
||||
{
|
||||
return boost::algorithm::starts_with(path,known_svg_prefix_);
|
||||
}
|
||||
|
||||
bool marker_cache::insert_svg(std::string const& name, std::string const& svg_string)
|
||||
{
|
||||
std::string key = known_svg_prefix_ + name;
|
||||
typedef boost::unordered_map<std::string, std::string>::const_iterator iterator_type;
|
||||
iterator_type itr = svg_cache_.find(key);
|
||||
if (itr == svg_cache_.end())
|
||||
{
|
||||
return svg_cache_.insert(std::make_pair(key,svg_string)).second;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool marker_cache::insert_marker(std::string const& uri, marker_ptr path)
|
||||
bool marker_cache::remove(std::string const& uri)
|
||||
{
|
||||
#ifdef MAPNIK_THREADSAFE
|
||||
mutex::scoped_lock lock(mutex_);
|
||||
#endif
|
||||
iterator_type itr = marker_cache_.find(uri);
|
||||
if (itr != marker_cache_.end())
|
||||
{
|
||||
marker_cache_.erase(itr);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool marker_cache::is_uri(std::string const& uri)
|
||||
{
|
||||
return is_svg_uri(uri) || is_image_uri(uri);
|
||||
}
|
||||
|
||||
bool marker_cache::is_svg_uri(std::string const& uri)
|
||||
{
|
||||
return boost::algorithm::starts_with(uri,known_svg_prefix_);
|
||||
}
|
||||
|
||||
bool marker_cache::is_image_uri(std::string const& uri)
|
||||
{
|
||||
return boost::algorithm::starts_with(uri,known_image_prefix_);
|
||||
}
|
||||
|
||||
bool marker_cache::insert_marker(std::string const& uri, marker_ptr path, bool override)
|
||||
{
|
||||
#ifdef MAPNIK_THREADSAFE
|
||||
mutex::scoped_lock lock(mutex_);
|
||||
#endif
|
||||
if (!override)
|
||||
{
|
||||
return marker_cache_.insert(std::make_pair(uri,path)).second;
|
||||
}
|
||||
else
|
||||
{
|
||||
typedef boost::unordered_map<std::string, marker_ptr>::iterator non_const_iterator_type;
|
||||
std::pair<non_const_iterator_type,bool> result = marker_cache_.insert(std::make_pair(uri,path));
|
||||
if (!result.second)
|
||||
{
|
||||
result.first->second = path;
|
||||
}
|
||||
return result.second;
|
||||
}
|
||||
}
|
||||
|
||||
boost::optional<marker_ptr> marker_cache::find(std::string const& uri,
|
||||
bool update_cache)
|
||||
{
|
||||
|
||||
boost::optional<marker_ptr> result;
|
||||
if (uri.empty())
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
#ifdef MAPNIK_THREADSAFE
|
||||
mutex::scoped_lock lock(mutex_);
|
||||
#endif
|
||||
typedef boost::unordered_map<std::string, marker_ptr>::const_iterator iterator_type;
|
||||
iterator_type itr = marker_cache_.find(uri);
|
||||
if (itr != marker_cache_.end())
|
||||
{
|
||||
result.reset(itr->second);
|
||||
return result;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// if uri references a built-in marker
|
||||
if (is_uri(uri))
|
||||
if (boost::filesystem::exists(boost::filesystem::path(uri)))
|
||||
{
|
||||
boost::unordered_map<std::string, std::string>::const_iterator mark_itr = svg_cache_.find(uri);
|
||||
if (mark_itr == svg_cache_.end())
|
||||
{
|
||||
MAPNIK_LOG_ERROR(marker_cache) << "Marker does not exist: " << uri;
|
||||
return result;
|
||||
}
|
||||
std::string known_svg_string = mark_itr->second;
|
||||
using namespace mapnik::svg;
|
||||
svg_path_ptr marker_path(boost::make_shared<svg_storage_type>());
|
||||
vertex_stl_adapter<svg_path_storage> stl_storage(marker_path->source());
|
||||
svg_path_adapter svg_path(stl_storage);
|
||||
svg_converter_type svg(svg_path, marker_path->attributes());
|
||||
svg_parser p(svg);
|
||||
p.parse_from_string(known_svg_string);
|
||||
//svg.arrange_orientations();
|
||||
double lox,loy,hix,hiy;
|
||||
svg.bounding_rect(&lox, &loy, &hix, &hiy);
|
||||
marker_path->set_bounding_box(lox,loy,hix,hiy);
|
||||
marker_path->set_dimensions(svg.width(),svg.height());
|
||||
marker_ptr mark(boost::make_shared<marker>(marker_path));
|
||||
result.reset(mark);
|
||||
if (update_cache)
|
||||
{
|
||||
marker_cache_.insert(std::make_pair(uri,*result));
|
||||
}
|
||||
}
|
||||
// otherwise assume file-based
|
||||
else
|
||||
{
|
||||
boost::filesystem::path path(uri);
|
||||
if (!exists(path))
|
||||
{
|
||||
MAPNIK_LOG_ERROR(marker_cache) << "Marker does not exist: " << uri;
|
||||
return result;
|
||||
}
|
||||
if (is_svg(uri))
|
||||
{
|
||||
using namespace mapnik::svg;
|
||||
svg_path_ptr marker_path(boost::make_shared<svg_storage_type>());
|
||||
vertex_stl_adapter<svg_path_storage> stl_storage(marker_path->source());
|
||||
svg_path_adapter svg_path(stl_storage);
|
||||
svg_converter_type svg(svg_path, marker_path->attributes());
|
||||
svg_parser p(svg);
|
||||
p.parse(uri);
|
||||
//svg.arrange_orientations();
|
||||
double lox,loy,hix,hiy;
|
||||
svg.bounding_rect(&lox, &loy, &hix, &hiy);
|
||||
marker_path->set_bounding_box(lox,loy,hix,hiy);
|
||||
marker_path->set_dimensions(svg.width(),svg.height());
|
||||
marker_ptr mark(boost::make_shared<marker>(marker_path));
|
||||
result.reset(mark);
|
||||
result.reset(boost::make_shared<marker>(read_svg_marker(uri)));
|
||||
}
|
||||
else
|
||||
{
|
||||
result.reset(boost::make_shared<marker>(read_bitmap_marker(uri)));
|
||||
}
|
||||
}
|
||||
if (result)
|
||||
{
|
||||
if (update_cache)
|
||||
{
|
||||
marker_cache_.insert(std::make_pair(uri,*result));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO - support reading images from string
|
||||
std::auto_ptr<mapnik::image_reader> reader(mapnik::get_image_reader(uri));
|
||||
if (reader.get())
|
||||
{
|
||||
unsigned width = reader->width();
|
||||
unsigned height = reader->height();
|
||||
BOOST_ASSERT(width > 0 && height > 0);
|
||||
mapnik::image_ptr image(boost::make_shared<mapnik::image_data_32>(width,height));
|
||||
reader->read(0,0,*image);
|
||||
if (!reader->premultiplied_alpha())
|
||||
{
|
||||
agg::rendering_buffer buffer(image->getBytes(),image->width(),image->height(),image->width() * 4);
|
||||
agg::pixfmt_rgba32 pixf(buffer);
|
||||
pixf.premultiply();
|
||||
}
|
||||
marker_ptr mark(boost::make_shared<marker>(image));
|
||||
result.reset(mark);
|
||||
if (update_cache)
|
||||
{
|
||||
marker_cache_.insert(std::make_pair(uri,*result));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
MAPNIK_LOG_ERROR(marker_cache) << "could not intialize reader for: '" << uri << "'";
|
||||
}
|
||||
}
|
||||
MAPNIK_LOG_ERROR(marker_cache) << "Marker does not exist: " << uri;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
catch (std::exception const& ex)
|
||||
{
|
||||
MAPNIK_LOG_ERROR(marker_cache) << "Exception caught while loading: '" << uri << "' (" << ex.what() << ")";
|
||||
MAPNIK_LOG_ERROR(marker_cache) << ex.what();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
|
|
@ -1060,11 +1060,11 @@ void svg_parser::parse(std::string const& filename)
|
|||
xmlTextReaderPtr reader = xmlNewTextReaderFilename(filename.c_str());
|
||||
if (reader == NULL)
|
||||
{
|
||||
MAPNIK_LOG_ERROR(svg_parser) << "Unable to open '" << filename << "'";
|
||||
throw std::runtime_error("Unable to open '" + filename + "'");
|
||||
}
|
||||
else if (!parse_reader(*this,reader))
|
||||
{
|
||||
MAPNIK_LOG_ERROR(svg_parser) << "Unable to parse '" << filename << "'";
|
||||
throw std::runtime_error("Unable to parse '" + filename + "'");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1074,11 +1074,11 @@ void svg_parser::parse_from_string(std::string const& svg)
|
|||
(XML_PARSE_NOBLANKS | XML_PARSE_NOCDATA | XML_PARSE_NOERROR | XML_PARSE_NOWARNING));
|
||||
if (reader == NULL)
|
||||
{
|
||||
MAPNIK_LOG_ERROR(svg_parser) << "Unable to parse '" << svg << "'";
|
||||
throw std::runtime_error("Unable to parse '" + svg + "'");
|
||||
}
|
||||
else if (!parse_reader(*this,reader))
|
||||
{
|
||||
MAPNIK_LOG_ERROR(svg_parser) << "Unable to parse '" << svg << "'";
|
||||
throw std::runtime_error("Unable to parse '" + svg + "'");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
125
tests/python_tests/marker_cache_test.py
Normal file
125
tests/python_tests/marker_cache_test.py
Normal file
|
@ -0,0 +1,125 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
from utilities import execution_path
|
||||
from nose.tools import *
|
||||
import mapnik
|
||||
import threading
|
||||
|
||||
def setup():
|
||||
# All of the paths used are relative, if we run the tests
|
||||
# from another directory we need to chdir()
|
||||
os.chdir(execution_path('.'))
|
||||
|
||||
def test_svg_put_to_marker_cache():
|
||||
cache = mapnik.MarkerCache.instance()
|
||||
cache.clear()
|
||||
eq_(cache.size(),2)
|
||||
svg_file = '../data/svg/octocat.svg'
|
||||
svg = mapnik.SVG.open(svg_file)
|
||||
cache.put(svg_file,svg)
|
||||
eq_(cache.size(),3)
|
||||
eq_(svg_file in cache.keys(),True)
|
||||
|
||||
def test_svg_put_and_clear_marker_cache():
|
||||
cache = mapnik.MarkerCache.instance()
|
||||
cache.clear()
|
||||
eq_(cache.size(),2)
|
||||
svg_file = '../data/svg/octocat.svg'
|
||||
svg = mapnik.SVG.open(svg_file)
|
||||
cache.put(svg_file,svg)
|
||||
eq_(cache.size(),3)
|
||||
eq_(svg_file in cache.keys(),True)
|
||||
cache.clear()
|
||||
eq_(svg_file in cache.keys(),False)
|
||||
eq_(cache.size(),2)
|
||||
|
||||
def test_svg_put_and_remove_marker_cache():
|
||||
cache = mapnik.MarkerCache.instance()
|
||||
cache.clear()
|
||||
eq_(cache.size(),2)
|
||||
svg_file = '../data/svg/octocat.svg'
|
||||
svg = mapnik.SVG.open(svg_file)
|
||||
cache.put(svg_file,svg)
|
||||
eq_(cache.size(),3)
|
||||
eq_(svg_file in cache.keys(),True)
|
||||
cache.remove(svg_file)
|
||||
eq_(svg_file in cache.keys(),False)
|
||||
eq_(cache.size(),2)
|
||||
|
||||
def test_image_put_and_clear_marker_cache():
|
||||
cache = mapnik.MarkerCache.instance()
|
||||
cache.clear()
|
||||
eq_(cache.size(),2)
|
||||
image_file = '../data/images/marker.png'
|
||||
image = mapnik.Image.open(image_file)
|
||||
cache.put(image_file,image)
|
||||
eq_(cache.size(),3)
|
||||
eq_(image_file in cache.keys(),True)
|
||||
cache.clear()
|
||||
eq_(image_file in cache.keys(),False)
|
||||
eq_(cache.size(),2)
|
||||
|
||||
def test_image_put_and_clear_marker_cache():
|
||||
cache = mapnik.MarkerCache.instance()
|
||||
cache.clear()
|
||||
eq_(cache.size(),2)
|
||||
image_file = '../data/images/marker.png'
|
||||
image = mapnik.Image.open(image_file)
|
||||
cache.put(image_file,image)
|
||||
eq_(cache.size(),3)
|
||||
eq_(image_file in cache.keys(),True)
|
||||
eq_(cache.remove(image_file),True)
|
||||
# removing twice should return False for no successful removal
|
||||
eq_(cache.remove(image_file),False)
|
||||
eq_(image_file in cache.keys(),False)
|
||||
eq_(cache.size(),2)
|
||||
|
||||
def test_image_put_and_get_image_in_marker_cache():
|
||||
cache = mapnik.MarkerCache.instance()
|
||||
cache.clear()
|
||||
eq_(cache.size(),2)
|
||||
image_file = '../data/images/marker.png'
|
||||
image = mapnik.Image.open(image_file)
|
||||
cache.put(image_file,image)
|
||||
new_im = cache.get(image_file)
|
||||
eq_(image.tostring(),new_im.tostring())
|
||||
|
||||
def test_image_put_and_get_svg_in_marker_cache():
|
||||
cache = mapnik.MarkerCache.instance()
|
||||
cache.clear()
|
||||
eq_(cache.size(),2)
|
||||
image_file = '../data/svg/rect.svg'
|
||||
image = mapnik.SVG.open(image_file)
|
||||
cache.put(image_file,image)
|
||||
new_im = cache.get(image_file)
|
||||
eq_(image.width(),new_im.width())
|
||||
|
||||
def test_marker_cache_override():
|
||||
cache = mapnik.MarkerCache.instance()
|
||||
cache.clear()
|
||||
eq_(cache.size(),2)
|
||||
image_file = '../data/images/marker.png'
|
||||
image = mapnik.Image.open(image_file)
|
||||
cache.put(image_file,image)
|
||||
alt_im = mapnik.Image(4,4)
|
||||
result = cache.put(image_file,alt_im)
|
||||
# putting a item for which a key already exists should return False
|
||||
eq_(result,False)
|
||||
alt_im_copy = cache.get(image_file)
|
||||
eq_(alt_im.tostring(),alt_im_copy.tostring())
|
||||
|
||||
|
||||
def test_threaded_reads_and_writes():
|
||||
threads = []
|
||||
for i in range(100):
|
||||
t = threading.Thread(target=test_image_put_and_clear_marker_cache)
|
||||
t.start()
|
||||
threads.append(t)
|
||||
for t in threads:
|
||||
t.join()
|
||||
|
||||
if __name__ == "__main__":
|
||||
setup()
|
||||
[eval(run)() for run in dir() if 'test_' in run]
|
43
tests/python_tests/svg_test.py
Normal file
43
tests/python_tests/svg_test.py
Normal file
|
@ -0,0 +1,43 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
from utilities import execution_path
|
||||
from nose.tools import *
|
||||
import mapnik
|
||||
import threading
|
||||
|
||||
def setup():
|
||||
# All of the paths used are relative, if we run the tests
|
||||
# from another directory we need to chdir()
|
||||
os.chdir(execution_path('.'))
|
||||
|
||||
def test_svg_loading_from_file():
|
||||
svg_file = '../data/svg/octocat.svg'
|
||||
svg = mapnik.SVG.open(svg_file)
|
||||
# TODO - invalid numbers
|
||||
eq_(svg.width(),0)
|
||||
eq_(svg.height(),0)
|
||||
expected = mapnik.Box2d(0.00700000000001,-4.339,378.46,332.606)
|
||||
actual = svg.extent()
|
||||
assert_almost_equal(expected.minx,actual.minx, places=7)
|
||||
assert_almost_equal(expected.miny,actual.miny, places=7)
|
||||
assert_almost_equal(expected.maxx,actual.maxx, places=7)
|
||||
assert_almost_equal(expected.maxy,actual.maxy, places=7)
|
||||
|
||||
def test_svg_loading_from_string():
|
||||
svg_file = '../data/svg/octocat.svg'
|
||||
svg = mapnik.SVG.fromstring(open(svg_file,'rb').read())
|
||||
# TODO - invalid numbers
|
||||
eq_(svg.width(),0)
|
||||
eq_(svg.height(),0)
|
||||
expected = mapnik.Box2d(0.00700000000001,-4.339,378.46,332.606)
|
||||
actual = svg.extent()
|
||||
assert_almost_equal(expected.minx,actual.minx, places=7)
|
||||
assert_almost_equal(expected.miny,actual.miny, places=7)
|
||||
assert_almost_equal(expected.maxx,actual.maxx, places=7)
|
||||
assert_almost_equal(expected.maxy,actual.maxy, places=7)
|
||||
|
||||
if __name__ == "__main__":
|
||||
setup()
|
||||
[eval(run)() for run in dir() if 'test_' in run]
|
Loading…
Reference in a new issue