Merge branch 'master' into rotated_rectangles

This commit is contained in:
artemp 2015-07-09 11:24:39 +02:00
commit ebbaf49896
52 changed files with 1330 additions and 850 deletions

View file

@ -8,42 +8,112 @@ For a complete change history, see the git log.
## 3.0.0 ## 3.0.0
- Added new and experimental `dot` symbolizer for fast rendering of points Released: July 7th, 2015
(Packaged from e6891a0)
#### Summary
The 3.0 release is a major milestone for Mapnik and includes many performance and design improvements. The is the first release to provide text shaping using the harfbuzz library. This harfbuzz support unlocks improved rendering and layer for many new languages, particularly SE Asian scripts. The internal storage for working with images and geometries has been made more flexible, faster, and strongly typed. The python bindings that were previously bundled with Mapnik have now been moved to <https://github.com/mapnik/python-mapnik> and are versioned independently.
#### Notice
- Mapnik 3.0.0 requires a compiler capable of `std=c++11`.
- It is highly recommended you use the `clang++` compiler on both OS X and Linux since it has robust c++11 support lower memory requirements.
##### Major Changes
- Improved support for International Text (now uses harfbuzz library for text shaping) - Improved support for International Text (now uses harfbuzz library for text shaping)
- Uses latest c++11 features for better performance (especially map loading)
- Uses latest C++11 features for better performance (especially map loading)
- Expressions everywhere: all symbolizer properties can now be data driven expressions (with the exception of `face-name` and `fontset-name` on the `TextSymbolizer`). - Expressions everywhere: all symbolizer properties can now be data driven expressions (with the exception of `face-name` and `fontset-name` on the `TextSymbolizer`).
- Rewritten geometry storage based on `std::vector` (#2739)
- Separate storage of polygon exterior rings and interior rings to allow for more robust clipping of parts.
- Enforces consistent winding order per OGC spec (exterior rings are CCW, interior CW)
- Reduced memory consumption for layers with many points
- Ability to adapt Mapnik geometries to boost::geometry operations (in a zero-copy way)
- Ability to have i/o grammars for json/wkt work on geometries rather than paths for better efficiency and simpler code
- Added new and experimental `dot` symbolizer for fast rendering of points
- New functions supported in expressions: `exp`, `sin`, `cos`, `tan`, `atan`, `abs`. - New functions supported in expressions: `exp`, `sin`, `cos`, `tan`, `atan`, `abs`.
- New constants supported in expressions: `PI`, `DEG_TO_RAD`, `RAD_TO_DEG` - New constants supported in expressions: `PI`, `DEG_TO_RAD`, `RAD_TO_DEG`
- Added support for a variety of different grayscale images:
- `mapnik.imageType.null`
- `mapnik.imageType.rgba8`
- `mapnik.imageType.gray8`
- `mapnik.imageType.gray8s`
- `mapnik.imageType.gray16`
- `mapnik.imageType.gray16s`
- `mapnik.imageType.gray32`
- `mapnik.imageType.gray32s`
- `mapnik.imageType.gray32f`
- `mapnik.imageType.gray64`
- `mapnik.imageType.gray64s`
- `mapnik.imageType.gray64f`
- Pattern symbolizers now support SVG input and applying transformations on them dynamically - Pattern symbolizers now support SVG input and applying transformations on them dynamically
- Experimental / interface may change: `@variables` can be passed to renderer and evaluated in expressions - Experimental / interface may change: `@variables` can be passed to renderer and evaluated in expressions
- Supports being built with clang++ using `-fvisibility=hidden -flto` for smaller binaries - Supports being built with clang++ using `-fvisibility=hidden -flto` for smaller binaries
- Supports being built with Visual Studio 2014 CTP #3 - Supports being built with Visual Studio 2014 CTP #3
- Shield icons are now pixel snapped for crisp rendering - Shield icons are now pixel snapped for crisp rendering
- `MarkersSymbolizer` now supports `avoid-edges`, `offset`, `geometry-transform`, `simplify` for `line` placement and two new `placement` options called `vertex-last` and `vertex-first` to place a single marker at the end or beginning of a path. Also `clip` is now respected when rendering markers on a LineString - `MarkersSymbolizer` now supports `avoid-edges`, `offset`, `geometry-transform`, `simplify` for `line` placement and two new `placement` options called `vertex-last` and `vertex-first` to place a single marker at the end or beginning of a path. Also `clip` is now respected when rendering markers on a LineString
geometry. geometry.
- `TextSymbolizer` now supports `smooth`, `simplify`, `halo-opacity`, `halo-comp-op`, and `halo-transform` - `TextSymbolizer` now supports `smooth`, `simplify`, `halo-opacity`, `halo-comp-op`, and `halo-transform`
- `ShieldSymbolizer` now supports `smooth`, `simplify`, `halo-opacity`, `halo-comp-op`, and `halo-transform` - `ShieldSymbolizer` now supports `smooth`, `simplify`, `halo-opacity`, `halo-comp-op`, and `halo-transform`
- The `text-transform` property of `TextSymbolizer` now supports `reverse` value to flip direction of text.
- The `TextSymbolizer` now supports `font-feature-settings` for advanced control over Opentype font rendering (https://developer.mozilla.org/en-US/docs/Web/CSS/font-feature-settings)
- New GroupSymbolizer for applying multiple symbolizers in a single layout - New GroupSymbolizer for applying multiple symbolizers in a single layout
Released ...
(Packaged from ...)
Summary: TODO
- PostGIS: Added support for rendering 3D and 4D geometries (previously silently skipped) (#44)
- AGG renderer: fixed geometry offsetting to work after smoothing to produce more consistent results (#2202) - AGG renderer: fixed geometry offsetting to work after smoothing to produce more consistent results (#2202)
- AGG renderer: increased `vertex_dist_epsilon` to ensure nearly coincident points are discarded more readily (#2196) - AGG renderer: increased `vertex_dist_epsilon` to ensure nearly coincident points are discarded more readily (#2196)
- GDAL plugin: Added back support for user driven `nodata` on rgb(a) images (#2023) - GDAL plugin
- Now keeps datasets open for the lifetime of the datasource (rather than per featureset)
- Added back support for user driven `nodata` on rgb(a) images (#2023)
- Allowed nodata to override alpha band if set on rgba images (#2023)
- Added `nodata_tolerance` option to set nearby pixels transparent (has similar effect to the `nearblack` program) (#2023)
- At process exit Mapnik core no longer calls `dlclose` on gdal.input (#2716)
- GDAL plugin: Allowed nodata to override alpha band if set on rgba images (#2023) - TopoJSON plugin
- Now supporting optional `bbox` property on layer
- Fixed support for reporting correct `feature.id()`
- Now supports `inline` string for passing data from memory
- Faster parsing via static initialization of grammars
- Fix crash on invalid arc index
- GDAL plugin: Added `nodata_tolerance` option to set nearby pixels transparent (has similar effect to the `nearblack` program) (#2023) - GeoJSON plugin
- Now supporting optional `bbox` property on layer
- Fixed support for reporting correct `feature.id()`
- Now supports `inline` string for passing data from memory
- Faster parsing via static initialization of grammars
- Added support for web fonts: .woff format (#2113) - SQLite plugin
- Fixed support for handling all column types
- CSV Plugin
- Added the ability to pass an `extent` in options
- PostGIS plugin
- Added Async support to - https://github.com/mapnik/mapnik/wiki/PostGIS-Async
- Added support for rendering 3D and 4D geometries (previously silently skipped) (#44)
- Added support for web fonts: `.woff` format (#2113)
- Added missing support for `geometry-transform` in `line-pattern` and `polygon-pattern` symbolizers (#2065) - Added missing support for `geometry-transform` in `line-pattern` and `polygon-pattern` symbolizers (#2065)
@ -51,11 +121,9 @@ Summary: TODO
- Upgraded unifont to `unifont-6.3.20131020` - Upgraded unifont to `unifont-6.3.20131020`
- CSV Plugin: added the ability to pass an `extent` in options
- Fixed crash when rendering to cairo context from python (#2031) - Fixed crash when rendering to cairo context from python (#2031)
- Moved `label-position-tolerance` from unsigned type to double - Moved `label-position-tolerance` from `unsigned` type to `double`
- Added support for more seamless blurring by rendering to a larger internal image to avoid edge effects (#1478) - Added support for more seamless blurring by rendering to a larger internal image to avoid edge effects (#1478)
@ -67,8 +135,6 @@ Summary: TODO
- Added `color-to-alpha` `image-filter` to allow for applying alpha in proportion to color similiarity (#2023) - Added `color-to-alpha` `image-filter` to allow for applying alpha in proportion to color similiarity (#2023)
- Added Async support to PostGIS plugin - https://github.com/mapnik/mapnik/wiki/PostGIS-Async
- Fixed alpha handling bug with `comp-op:dst-over` (#1995) - Fixed alpha handling bug with `comp-op:dst-over` (#1995)
- Fixed alpha handling bug with building-fill-opacity (#2011) - Fixed alpha handling bug with building-fill-opacity (#2011)
@ -133,6 +199,9 @@ are not set. (#1966)
- Added `scale-hsla` image-filter that allows scaling colors in HSL color space. RGB is converted to HSL (hue-saturation-lightness) and then each value (and the original alpha value) is stretched based on the specified scaling values. An example syntax is `scale-hsla(0,1,0,1,0,1,0,1)` which means no change because the full range will be kept (0 for lowest, 1 for highest). Other examples are: 1) `scale-hsla(0,0,0,1,0,1,0,1)` which would force all colors to be red in hue in the same way `scale-hsla(1,1,0,1,0,1,0,1)` would, 2) `scale-hsla(0,1,1,1,0,1,0,1)` which would cause all colors to become fully saturated, 3) `scale-hsla(0,1,1,1,0,1,.5,1)` which would force no colors to be any more transparent than half, and 4) `scale-hsla(0,1,1,1,0,1,0,.5)` which would force all colors to be at least half transparent. (#1954) - Added `scale-hsla` image-filter that allows scaling colors in HSL color space. RGB is converted to HSL (hue-saturation-lightness) and then each value (and the original alpha value) is stretched based on the specified scaling values. An example syntax is `scale-hsla(0,1,0,1,0,1,0,1)` which means no change because the full range will be kept (0 for lowest, 1 for highest). Other examples are: 1) `scale-hsla(0,0,0,1,0,1,0,1)` which would force all colors to be red in hue in the same way `scale-hsla(1,1,0,1,0,1,0,1)` would, 2) `scale-hsla(0,1,1,1,0,1,0,1)` which would cause all colors to become fully saturated, 3) `scale-hsla(0,1,1,1,0,1,.5,1)` which would force no colors to be any more transparent than half, and 4) `scale-hsla(0,1,1,1,0,1,0,.5)` which would force all colors to be at least half transparent. (#1954)
- The `shapeindex` tool now works correctly with point 3d geometry types
## 2.2.0 ## 2.2.0
Released June 3rd, 2013 Released June 3rd, 2013

View file

@ -5,17 +5,23 @@ Mapnik runs on Linux, OS X, Windows, and BSD systems.
To configure and build Mapnik do: To configure and build Mapnik do:
```bash ```bash
$ ./configure ./configure
$ make make
``` ```
To trigger parallel compilation you can pass a JOBS value to make: To trigger parallel compilation you can pass a JOBS value to make:
```bash ```bash
$ JOBS=4 make JOBS=4 make
``` ```
(Note that compiling Mapnik needs several GBytes of RAM. If you use parallel compilation it needs more.) Mapnik needs > 2 GB of RAM to build. If you use parallel compilation it needs more.
If you are on a system with less memory make sure you only build with one JOB:
```bash
JOBS=1 make
```
To use a Python interpreter that is not named `python` for your build, do To use a Python interpreter that is not named `python` for your build, do
something like the following instead: something like the following instead:
@ -178,4 +184,4 @@ tutorials on how to programmatically use Mapnik.
### Contributers ### Contributers
Read docs/contributing.markdown for resources for getting involved with Mapnik development. Read docs/contributing.md for resources for getting involved with Mapnik development.

View file

@ -29,19 +29,7 @@ namespace benchmark {
image_rgba8 const& dest = util::get<image_rgba8>(desc_any); image_rgba8 const& dest = util::get<image_rgba8>(desc_any);
image_rgba8 const& src = util::get<image_rgba8>(src_any); image_rgba8 const& src = util::get<image_rgba8>(src_any);
unsigned int width = src.width(); return compare(dest, src, 0, true) == 0;
unsigned int height = src.height();
if ((width != dest.width()) || height != dest.height()) return false;
for (unsigned int y = 0; y < height; ++y)
{
const unsigned int* row_from = src.get_row(y);
const unsigned int* row_to = dest.get_row(y);
for (unsigned int x = 0; x < width; ++x)
{
if (row_from[x] != row_to[x]) return false;
}
}
return true;
} }
} }

View file

@ -61,7 +61,7 @@ function install_mason_deps() {
install harfbuzz 0.9.40 libharfbuzz & install harfbuzz 0.9.40 libharfbuzz &
install jpeg_turbo 1.4.0 libjpeg & install jpeg_turbo 1.4.0 libjpeg &
install libxml2 2.9.2 libxml2 & install libxml2 2.9.2 libxml2 &
install libpng 1.6.16 libpng & install libpng 1.6.17 libpng &
install webp 0.4.2 libwebp & install webp 0.4.2 libwebp &
install icu 54.1 & install icu 54.1 &
install proj 4.8.0 libproj & install proj 4.8.0 libproj &

View file

@ -652,8 +652,8 @@ struct gray16
//-------------------------------------------------------------------- //--------------------------------------------------------------------
self_type& opacity(double a_) self_type& opacity(double a_)
{ {
if (a_ < 0) a_ = 0; if (a_ < 0) a = 0;
else if(a_ > 1) a_ = 1; else if(a_ > 1) a = 1;
else a = (value_type)uround(a_ * double(base_mask)); else a = (value_type)uround(a_ * double(base_mask));
return *this; return *this;
} }

View file

@ -53,7 +53,7 @@ struct offset_converter
, pre_first_(vertex2d::no_init) , pre_first_(vertex2d::no_init)
, pre_(vertex2d::no_init) , pre_(vertex2d::no_init)
, cur_(vertex2d::no_init) , cur_(vertex2d::no_init)
{} {}
enum status enum status
{ {
@ -238,8 +238,8 @@ private:
*/ */
static void displace(vertex2d & v, double dx, double dy, double a) static void displace(vertex2d & v, double dx, double dy, double a)
{ {
v.x += dx * std::cos(a) + dy * std::sin(a); v.x += dx * std::cos(a) - dy * std::sin(a);
v.y += dx * std::sin(a) - dy * std::cos(a); v.y += dx * std::sin(a) + dy * std::cos(a);
} }
/** /**
@ -247,8 +247,8 @@ private:
*/ */
void displace(vertex2d & v, double a) const void displace(vertex2d & v, double a) const
{ {
v.x += offset_ * std::sin(a); v.x -= offset_ * std::sin(a);
v.y -= offset_ * std::cos(a); v.y += offset_ * std::cos(a);
} }
/** /**
@ -256,8 +256,8 @@ private:
*/ */
void displace(vertex2d & v, vertex2d const& u, double a) const void displace(vertex2d & v, vertex2d const& u, double a) const
{ {
v.x = u.x + offset_ * std::sin(a); v.x = u.x - offset_ * std::sin(a);
v.y = u.y - offset_ * std::cos(a); v.y = u.y + offset_ * std::cos(a);
v.cmd = u.cmd; v.cmd = u.cmd;
} }
@ -266,16 +266,27 @@ private:
double sa = offset_ * std::sin(a); double sa = offset_ * std::sin(a);
double ca = offset_ * std::cos(a); double ca = offset_ * std::cos(a);
double h = std::tan(0.5 * (b - a)); double h = std::tan(0.5 * (b - a));
if (h > 1.5) double hsa = h * sa;
double hca = h * ca;
double abs_offset = std::abs(offset_);
if (hsa > 1.0 * abs_offset)
{ {
h = 1.5; hsa = 1.0 * abs_offset;
} }
else if (h < -1.5) else if (hsa < -1.0 * abs_offset)
{ {
h = -1.5; hsa = -1.0 * abs_offset;
} }
v.x = v.x + sa + h * ca; if (hca > 1.0 * abs_offset)
v.y = v.y - ca + h * sa; {
hca = 1.0 * abs_offset;
}
else if (hca < -1.0 * abs_offset)
{
hca = -1.0 * abs_offset;
}
v.x = v.x - sa - hca;
v.y = v.y + ca - hsa;
} }
status init_vertices() status init_vertices()
@ -288,31 +299,51 @@ private:
vertex2d v1(vertex2d::no_init); vertex2d v1(vertex2d::no_init);
vertex2d v2(vertex2d::no_init); vertex2d v2(vertex2d::no_init);
vertex2d w(vertex2d::no_init); vertex2d w(vertex2d::no_init);
vertex2d start(vertex2d::no_init);
vertex2d start_v2(vertex2d::no_init); vertex2d start_v2(vertex2d::no_init);
std::vector<vertex2d> points; std::vector<vertex2d> points;
std::vector<vertex2d> close_points; std::vector<vertex2d> close_points;
bool is_polygon = false; bool is_polygon = false;
std::size_t cpt = 0; std::size_t cpt = 0;
v0.cmd = geom_.vertex(&v0.x, &v0.y); v0.cmd = geom_.vertex(&v0.x, &v0.y);
v1.x = v0.x; v1 = v0;
v1.y = v0.y;
v1.cmd = v0.cmd;
// PUSH INITIAL // PUSH INITIAL
points.push_back(vertex2d(v0.x, v0.y, v0.cmd)); points.push_back(v0);
if (v0.cmd == SEG_END) // not enough vertices in source
{
return status_ = process;
}
start = v0;
while ((v0.cmd = geom_.vertex(&v0.x, &v0.y)) != SEG_END) while ((v0.cmd = geom_.vertex(&v0.x, &v0.y)) != SEG_END)
{ {
points.push_back(vertex2d(v0.x, v0.y, v0.cmd));
if (v0.cmd == SEG_CLOSE) if (v0.cmd == SEG_CLOSE)
{ {
is_polygon = true; is_polygon = true;
close_points.push_back(vertex2d(v1.x, v1.y, v1.cmd)); auto & prev = points.back();
if (prev.x == start.x && prev.y == start.y)
{
prev.x = v0.x; // hack
prev.y = v0.y;
prev.cmd = SEG_CLOSE; // account for dupes (line_to(move_to) + close_path) in agg poly clipper
std::size_t size = points.size();
if (size > 1) close_points.push_back(points[size - 2]);
else close_points.push_back(prev);
continue;
}
else
{
close_points.push_back(v1);
}
} }
v1.x = v0.x; else if (v0.cmd == SEG_MOVETO)
v1.y = v0.y; {
v1.cmd = v0.cmd; start = v0;
}
v1 = v0;
points.push_back(v0);
} }
// Push SEG_END // Push SEG_END
points.push_back(vertex2d(v0.x, v0.y, v0.cmd)); points.push_back(vertex2d(v0.x,v0.y,SEG_END));
std::size_t i = 0; std::size_t i = 0;
v1 = points[i++]; v1 = points[i++];
v2 = points[i++]; v2 = points[i++];
@ -326,17 +357,28 @@ private:
} }
double angle_a = 0; double angle_a = 0;
// The vector parts from v1 to v0.
double v_x1x0 = 0;
double v_y1y0 = 0;
// The vector parts from v1 to v2;
double v_x1x2 = v2.x - v1.x;
double v_y1y2 = v2.y - v1.y;
if (is_polygon) if (is_polygon)
{ {
double x = v1.x - close_points[cpt].x; v_x1x0 = close_points[cpt].x - v1.x;
double y = v1.y - close_points[cpt].y; v_y1y0 = close_points[cpt].y - v1.y;
cpt++; cpt++;
x = std::abs(x) < std::numeric_limits<double>::epsilon() ? 0 : x; angle_a = std::atan2(-v_y1y0, -v_x1x0);
y = std::abs(y) < std::numeric_limits<double>::epsilon() ? 0 : y;
angle_a = std::atan2(y, x);
} }
double angle_b = std::atan2((v2.y - v1.y), (v2.x - v1.x)); // dot product
double dot;
// determinate
double det;
double angle_b = std::atan2(v_y1y2, v_x1x2);
// Angle between the two vectors
double joint_angle; double joint_angle;
double curve_angle;
if (!is_polygon) if (!is_polygon)
{ {
@ -346,33 +388,28 @@ private:
} }
else else
{ {
joint_angle = explement_reflex_angle(angle_b - angle_a); dot = v_x1x0 * v_x1x2 + v_y1y0 * v_y1y2; // dot product
det = v_x1x0 * v_y1y2 - v_y1y0 * v_x1x2; // determinant
joint_angle = std::atan2(det, dot); // atan2(y, x) or atan2(sin, cos)
if (joint_angle < 0) joint_angle = joint_angle + 2 * M_PI;
joint_angle = std::fmod(joint_angle, 2 * M_PI);
if (offset_ > 0.0)
{
joint_angle = 2 * M_PI - joint_angle;
}
double half_turns = half_turn_segments_ * std::fabs(joint_angle);
int bulge_steps = 0; int bulge_steps = 0;
if (offset_ < 0.0) if (std::abs(joint_angle) > M_PI)
{ {
if (joint_angle > 0.0) curve_angle = explement_reflex_angle(angle_b - angle_a);
{ // Bulge steps should be determined by the inverse of the joint angle.
joint_angle = joint_angle - 2 * M_PI; double half_turns = half_turn_segments_ * std::fabs(curve_angle);
} bulge_steps = 1 + static_cast<int>(std::floor(half_turns / M_PI));
else
{
bulge_steps = 1 + static_cast<int>(std::floor(half_turns / M_PI));
}
}
else
{
if (joint_angle < 0.0)
{
joint_angle = joint_angle + 2 * M_PI;
}
else
{
bulge_steps = 1 + static_cast<int>(std::floor(half_turns / M_PI));
}
} }
if (bulge_steps == 0) if (bulge_steps == 0)
{ {
displace2(v1, angle_a, angle_b); displace2(v1, angle_a, angle_b);
@ -415,18 +452,15 @@ private:
v1.y = start_.y; v1.y = start_.y;
if (cpt < close_points.size()) if (cpt < close_points.size())
{ {
double x = v1.x - close_points[cpt].x; v_x1x2 = v1.x - close_points[cpt].x;
double y = v1.y - close_points[cpt].y; v_y1y2 = v1.y - close_points[cpt].y;
x = std::abs(x) < std::numeric_limits<double>::epsilon() ? 0.0 : x;
y = std::abs(y) < std::numeric_limits<double>::epsilon() ? 0.0 : y;
angle_b = std::atan2(y,x);
cpt++; cpt++;
} }
start_v2.x = v2.x;
start_v2.y = v2.y;
} }
start_v2.x = v2.x;
start_v2.y = v2.y;
} }
if (v2.cmd == SEG_MOVETO) if (is_polygon && v2.cmd == SEG_MOVETO)
{ {
start_.x = v2.x; start_.x = v2.x;
start_.y = v2.y; start_.y = v2.y;
@ -445,34 +479,39 @@ private:
v2.x = start_.x; v2.x = start_.x;
v2.y = start_.y; v2.y = start_.y;
} }
angle_a = angle_b;
angle_b = std::atan2((v2.y - v1.y), (v2.x - v1.x));
joint_angle = explement_reflex_angle(angle_b - angle_a);
double half_turns = half_turn_segments_ * std::fabs(joint_angle); // Switch the previous vector's direction as the origin has changed
v_x1x0 = -v_x1x2;
v_y1y0 = -v_y1y2;
// Calculate new angle_a
angle_a = std::atan2(v_y1y2, v_x1x2);
// Calculate the new vector
v_x1x2 = v2.x - v1.x;
v_y1y2 = v2.y - v1.y;
// Calculate the new angle_b
angle_b = std::atan2(v_y1y2, v_x1x2);
dot = v_x1x0 * v_x1x2 + v_y1y0 * v_y1y2; // dot product
det = v_x1x0 * v_y1y2 - v_y1y0 * v_x1x2; // determinant
joint_angle = std::atan2(det, dot); // atan2(y, x) or atan2(sin, cos)
if (joint_angle < 0) joint_angle = joint_angle + 2 * M_PI;
joint_angle = std::fmod(joint_angle, 2 * M_PI);
if (offset_ > 0.0)
{
joint_angle = 2 * M_PI - joint_angle;
}
int bulge_steps = 0; int bulge_steps = 0;
if (offset_ < 0.0) if (std::abs(joint_angle) > M_PI)
{ {
if (joint_angle > 0.0) curve_angle = explement_reflex_angle(angle_b - angle_a);
{ // Bulge steps should be determined by the inverse of the joint angle.
joint_angle = joint_angle - 2 * M_PI; double half_turns = half_turn_segments_ * std::fabs(curve_angle);
} bulge_steps = 1 + static_cast<int>(std::floor(half_turns / M_PI));
else
{
bulge_steps = 1 + static_cast<int>(std::floor(half_turns / M_PI));
}
}
else
{
if (joint_angle < 0.0)
{
joint_angle = joint_angle + 2 * M_PI;
}
else
{
bulge_steps = 1 + static_cast<int>(std::floor(half_turns / M_PI));
}
} }
#ifdef MAPNIK_LOG #ifdef MAPNIK_LOG
@ -516,10 +555,9 @@ private:
displace(w, v1, angle_a); displace(w, v1, angle_a);
w.cmd = SEG_LINETO; w.cmd = SEG_LINETO;
push_vertex(w); push_vertex(w);
for (int s = 0; ++s < bulge_steps;) for (int s = 0; ++s < bulge_steps;)
{ {
displace(w, v1, angle_a + (joint_angle * s) / bulge_steps); displace(w, v1, angle_a + (curve_angle * s) / bulge_steps);
w.cmd = SEG_LINETO; w.cmd = SEG_LINETO;
push_vertex(w); push_vertex(w);
} }

View file

@ -211,7 +211,7 @@ void render_offset_placements(placements_list const& placements,
pixel_position const& offset, pixel_position const& offset,
F render_text) { F render_text) {
for (glyph_positions_ptr glyphs : placements) for (auto const& glyphs : placements)
{ {
// move the glyphs to the correct offset // move the glyphs to the correct offset
pixel_position base_point = glyphs->get_base_point(); pixel_position base_point = glyphs->get_base_point();

View file

@ -118,9 +118,9 @@ struct render_marker_symbolizer_visitor
if (clip) // optional clip (default: true) if (clip) // optional clip (default: true)
{ {
geometry::geometry_types type = geometry::geometry_type(feature_.get_geometry()); geometry::geometry_types type = geometry::geometry_type(feature_.get_geometry());
if (type == geometry::geometry_types::Polygon) if (type == geometry::geometry_types::Polygon || type == geometry::geometry_types::MultiPolygon)
converter.template set<clip_poly_tag>(); converter.template set<clip_poly_tag>();
else if (type == geometry::geometry_types::LineString) else if (type == geometry::geometry_types::LineString || type == geometry::geometry_types::MultiLineString)
converter.template set<clip_line_tag>(); converter.template set<clip_line_tag>();
} }
@ -223,9 +223,9 @@ struct render_marker_symbolizer_visitor
if (clip) // optional clip (default: true) if (clip) // optional clip (default: true)
{ {
geometry::geometry_types type = geometry::geometry_type(feature_.get_geometry()); geometry::geometry_types type = geometry::geometry_type(feature_.get_geometry());
if (type == geometry::geometry_types::Polygon) if (type == geometry::geometry_types::Polygon || type == geometry::geometry_types::MultiPolygon)
converter.template set<clip_poly_tag>(); converter.template set<clip_poly_tag>();
else if (type == geometry::geometry_types::LineString) else if (type == geometry::geometry_types::LineString || type == geometry::geometry_types::MultiLineString)
converter.template set<clip_line_tag>(); converter.template set<clip_line_tag>();
} }
converter.template set<transform_tag>(); //always transform converter.template set<transform_tag>(); //always transform

View file

@ -44,7 +44,7 @@ extern "C"
namespace mapnik namespace mapnik
{ {
class font_face : util::noncopyable class MAPNIK_DECL font_face : util::noncopyable
{ {
public: public:
font_face(FT_Face face); font_face(FT_Face face);

View file

@ -82,7 +82,6 @@ public:
void set_marker(marker_info_ptr marker, pixel_position const& marker_pos); void set_marker(marker_info_ptr marker, pixel_position const& marker_pos);
marker_info_ptr get_marker() const; marker_info_ptr get_marker() const;
pixel_position const& marker_pos() const; pixel_position const& marker_pos() const;
box2d<double> const & bbox() const;
private: private:
std::vector<glyph_position> data_; std::vector<glyph_position> data_;
pixel_position base_point_; pixel_position base_point_;
@ -90,7 +89,7 @@ private:
pixel_position marker_pos_; pixel_position marker_pos_;
box2d<double> bbox_; box2d<double> bbox_;
}; };
using glyph_positions_ptr = std::shared_ptr<glyph_positions>; using glyph_positions_ptr = std::unique_ptr<glyph_positions>;
using placements_list = std::list<glyph_positions_ptr>; using placements_list = std::list<glyph_positions_ptr>;
} }

View file

@ -28,7 +28,9 @@
#include <mapnik/text/text_line.hpp> #include <mapnik/text/text_line.hpp>
#include <mapnik/text/face.hpp> #include <mapnik/text/face.hpp>
#include <mapnik/text/font_feature_settings.hpp> #include <mapnik/text/font_feature_settings.hpp>
#include <mapnik/text/itemizer.hpp>
#include <mapnik/safe_cast.hpp> #include <mapnik/safe_cast.hpp>
#include <mapnik/font_engine_freetype.hpp>
// stl // stl
#include <list> #include <list>
@ -38,6 +40,9 @@
#include <harfbuzz/hb.h> #include <harfbuzz/hb.h>
#include <harfbuzz/hb-ft.h> #include <harfbuzz/hb-ft.h>
// icu
#include <unicode/uscript.h>
namespace mapnik namespace mapnik
{ {

View file

@ -27,6 +27,10 @@
#include <mapnik/text/text_properties.hpp> #include <mapnik/text/text_properties.hpp>
#include <mapnik/text/text_line.hpp> #include <mapnik/text/text_line.hpp>
#include <mapnik/text/face.hpp> #include <mapnik/text/face.hpp>
#include <mapnik/text/font_feature_settings.hpp>
#include <mapnik/text/itemizer.hpp>
#include <mapnik/safe_cast.hpp>
#include <mapnik/font_engine_freetype.hpp>
#include <mapnik/debug.hpp> #include <mapnik/debug.hpp>
// stl // stl
@ -59,12 +63,11 @@ static void shape_text(text_line & line,
UErrorCode err = U_ZERO_ERROR; UErrorCode err = U_ZERO_ERROR;
mapnik::value_unicode_string shaped; mapnik::value_unicode_string shaped;
mapnik::value_unicode_string reordered; mapnik::value_unicode_string reordered;
unsigned char_index = 0;
for (auto const& text_item : list) for (auto const& text_item : list)
{ {
face_set_ptr face_set = font_manager.get_face_set(text_item.format->face_name, text_item.format->fontset); face_set_ptr face_set = font_manager.get_face_set(text_item.format_->face_name, text_item.format_->fontset);
double size = text_item.format->text_size * scale_factor; double size = text_item.format_->text_size * scale_factor;
face_set->set_unscaled_character_sizes(); face_set->set_unscaled_character_sizes();
for (auto const& face : *face_set) for (auto const& face : *face_set)
{ {
@ -87,33 +90,35 @@ static void shape_text(text_line & line,
std::size_t num_chars = static_cast<std::size_t>(num_char); std::size_t num_chars = static_cast<std::size_t>(num_char);
shaped.releaseBuffer(length); shaped.releaseBuffer(length);
bool shaped_status = true; bool shaped_status = true;
double max_glyph_height = 0;
if (U_SUCCESS(err) && (num_chars == length)) if (U_SUCCESS(err) && (num_chars == length))
{ {
unsigned char_index = 0;
U_NAMESPACE_QUALIFIER StringCharacterIterator iter(shaped); U_NAMESPACE_QUALIFIER StringCharacterIterator iter(shaped);
for (iter.setToStart(); iter.hasNext();) for (iter.setToStart(); iter.hasNext();)
{ {
UChar ch = iter.nextPostInc(); UChar ch = iter.nextPostInc();
glyph_info tmp; auto codepoint = FT_Get_Char_Index(face->get_face(), ch);
tmp.glyph_index = FT_Get_Char_Index(face->get_face(), ch); glyph_info g(codepoint,char_index,text_item.format_);
tmp.offset.clear(); //g.offset.clear();
if (tmp.glyph_index == 0) if (g.glyph_index == 0)
{ {
shaped_status = false; shaped_status = false;
break; break;
} }
if (face->glyph_dimensions(tmp)) if (face->glyph_dimensions(g))
{ {
tmp.char_index = char_index; g.face = face;
tmp.face = face; g.scale_multiplier = size / face->get_face()->units_per_EM;
tmp.format = text_item.format; double tmp_height = g.height();
tmp.scale_multiplier = size / face->get_face()->units_per_EM; if (tmp_height > max_glyph_height) max_glyph_height = tmp_height;
width_map[char_index++] += tmp.advance(); width_map[char_index++] += g.advance();
line.add_glyph(tmp, scale_factor); line.add_glyph(std::move(g), scale_factor);
} }
} }
} }
if (!shaped_status) continue; if (!shaped_status) continue;
line.update_max_char_height(face->get_char_height(size)); line.update_max_char_height(max_glyph_height);
return; return;
} }
} }

View file

@ -27,6 +27,7 @@
#include <mapnik/text/evaluated_format_properties_ptr.hpp> #include <mapnik/text/evaluated_format_properties_ptr.hpp>
#include <mapnik/value_types.hpp> #include <mapnik/value_types.hpp>
#include <mapnik/util/noncopyable.hpp> #include <mapnik/util/noncopyable.hpp>
#include <mapnik/config.hpp>
// stl // stl
#include <string> #include <string>
@ -41,7 +42,7 @@
namespace mapnik namespace mapnik
{ {
struct text_item : util::noncopyable struct MAPNIK_DECL text_item : util::noncopyable
{ {
text_item(unsigned s, text_item(unsigned s,
unsigned e, unsigned e,
@ -71,7 +72,7 @@ struct text_item : util::noncopyable
// - format // - format
// - script (http://en.wikipedia.org/wiki/Scripts_in_Unicode) // - script (http://en.wikipedia.org/wiki/Scripts_in_Unicode)
class text_itemizer class MAPNIK_DECL text_itemizer
{ {
public: public:
text_itemizer(); text_itemizer();

View file

@ -75,7 +75,7 @@ private:
// Checks for collision. // Checks for collision.
bool collision(box2d<double> const& box, const value_unicode_string &repeat_key, bool line_placement) const; bool collision(box2d<double> const& box, const value_unicode_string &repeat_key, bool line_placement) const;
// Adds marker to glyph_positions and to collision detector. Returns false if there is a collision. // Adds marker to glyph_positions and to collision detector. Returns false if there is a collision.
bool add_marker(glyph_positions_ptr glyphs, pixel_position const& pos) const; bool add_marker(glyph_positions_ptr & glyphs, pixel_position const& pos) const;
// Maps upright==auto, left-only and right-only to left,right to simplify processing. // Maps upright==auto, left-only and right-only to left,right to simplify processing.
// angle = angle of at start of line (to estimate best option for upright==auto) // angle = angle of at start of line (to estimate best option for upright==auto)
text_upright_e simplify_upright(text_upright_e upright, double angle) const; text_upright_e simplify_upright(text_upright_e upright, double angle) const;

View file

@ -17,9 +17,9 @@
#ifndef __SCRPTRUN_H #ifndef __SCRPTRUN_H
#define __SCRPTRUN_H #define __SCRPTRUN_H
#include "unicode/utypes.h" #include <unicode/utypes.h>
#include "unicode/uobject.h" #include <unicode/uobject.h>
#include "unicode/uscript.h" #include <unicode/uscript.h>
struct ScriptRecord struct ScriptRecord
{ {

View file

@ -25,6 +25,7 @@
//stl //stl
#include <vector> #include <vector>
#include <mapnik/util/noncopyable.hpp> #include <mapnik/util/noncopyable.hpp>
#include <mapnik/config.hpp>
namespace mapnik namespace mapnik
{ {
@ -35,7 +36,7 @@ struct glyph_info;
// It can be used for rendering but no text processing (like line breaking) // It can be used for rendering but no text processing (like line breaking)
// should be done! // should be done!
class text_line : util::noncopyable class MAPNIK_DECL text_line : util::noncopyable
{ {
public: public:
using glyph_vector = std::vector<glyph_info>; using glyph_vector = std::vector<glyph_info>;

View file

@ -29,7 +29,6 @@
// stl // stl
#include <functional> #include <functional>
#include <cassert>
// icu // icu
#include <unicode/unistr.h> #include <unicode/unistr.h>
@ -52,7 +51,6 @@ struct value_hasher
std::size_t operator() (value_unicode_string const& val) const std::size_t operator() (value_unicode_string const& val) const
{ {
assert(val.hashCode() > 0);
return static_cast<std::size_t>(val.hashCode()); return static_cast<std::size_t>(val.hashCode());
} }

View file

@ -23,7 +23,7 @@
#ifndef MAPNIK_VERSION_HPP #ifndef MAPNIK_VERSION_HPP
#define MAPNIK_VERSION_HPP #define MAPNIK_VERSION_HPP
#define MAPNIK_VERSION_IS_RELEASE 0 #define MAPNIK_VERSION_IS_RELEASE 1
#define MAPNIK_MAJOR_VERSION 3 #define MAPNIK_MAJOR_VERSION 3
#define MAPNIK_MINOR_VERSION 0 #define MAPNIK_MINOR_VERSION 0

View file

@ -218,7 +218,7 @@ void geojson_datasource::initialise_index(Iterator start, Iterator end)
// parse first feature to extract attributes schema. // parse first feature to extract attributes schema.
// NOTE: this doesn't yield correct answer for geoJSON in general, just an indication // NOTE: this doesn't yield correct answer for geoJSON in general, just an indication
Iterator itr2 = start + geometry_index.first; Iterator itr2 = start + geometry_index.first;
Iterator end2 = itr + geometry_index.second; Iterator end2 = itr2 + geometry_index.second;
mapnik::context_ptr ctx = std::make_shared<mapnik::context_type>(); mapnik::context_ptr ctx = std::make_shared<mapnik::context_type>();
mapnik::feature_ptr feature(mapnik::feature_factory::create(ctx,1)); mapnik::feature_ptr feature(mapnik::feature_factory::create(ctx,1));
if (!boost::spirit::qi::phrase_parse(itr2, end2, (geojson_datasource_static_feature_grammar)(boost::phoenix::ref(*feature)), space)) if (!boost::spirit::qi::phrase_parse(itr2, end2, (geojson_datasource_static_feature_grammar)(boost::phoenix::ref(*feature)), space))

View file

@ -120,7 +120,7 @@ struct thunk_renderer<image_rgba8>
render_offset_placements( render_offset_placements(
thunk.placements_, thunk.placements_,
offset_, offset_,
[&] (glyph_positions_ptr glyphs) [&] (glyph_positions_ptr const& glyphs)
{ {
marker_info_ptr mark = glyphs->get_marker(); marker_info_ptr mark = glyphs->get_marker();
if (mark) if (mark)

View file

@ -31,6 +31,7 @@
#include <mapnik/vertex_processor.hpp> #include <mapnik/vertex_processor.hpp>
#include <mapnik/renderer_common/clipping_extent.hpp> #include <mapnik/renderer_common/clipping_extent.hpp>
#include <mapnik/renderer_common/apply_vertex_converter.hpp> #include <mapnik/renderer_common/apply_vertex_converter.hpp>
#include <mapnik/geometry_type.hpp>
// agg // agg
#include "agg_basics.h" #include "agg_basics.h"
#include "agg_rendering_buffer.h" #include "agg_rendering_buffer.h"
@ -164,12 +165,19 @@ void agg_renderer<T0,T1>::process(line_symbolizer const& sym,
rasterizer_type ras(ren); rasterizer_type ras(ren);
set_join_caps_aa(sym, ras, feature, common_.vars_); set_join_caps_aa(sym, ras, feature, common_.vars_);
using vertex_converter_type = vertex_converter<clip_line_tag, transform_tag, using vertex_converter_type = vertex_converter<clip_line_tag, clip_poly_tag, transform_tag,
affine_transform_tag, affine_transform_tag,
simplify_tag, smooth_tag, simplify_tag, smooth_tag,
offset_transform_tag>; offset_transform_tag>;
vertex_converter_type converter(clip_box,sym,common_.t_,prj_trans,tr,feature,common_.vars_,common_.scale_factor_); vertex_converter_type converter(clip_box,sym,common_.t_,prj_trans,tr,feature,common_.vars_,common_.scale_factor_);
if (clip) converter.set<clip_line_tag>(); // optional clip (default: true) if (clip)
{
geometry::geometry_types type = geometry::geometry_type(feature.get_geometry());
if (type == geometry::geometry_types::Polygon || type == geometry::geometry_types::MultiPolygon)
converter.template set<clip_poly_tag>();
else if (type == geometry::geometry_types::LineString || type == geometry::geometry_types::MultiLineString)
converter.template set<clip_line_tag>();
}
converter.set<transform_tag>(); // always transform converter.set<transform_tag>(); // always transform
if (std::fabs(offset) > 0.0) converter.set<offset_transform_tag>(); // parallel offset if (std::fabs(offset) > 0.0) converter.set<offset_transform_tag>(); // parallel offset
converter.set<affine_transform_tag>(); // optional affine transform converter.set<affine_transform_tag>(); // optional affine transform
@ -183,14 +191,20 @@ void agg_renderer<T0,T1>::process(line_symbolizer const& sym,
} }
else else
{ {
using vertex_converter_type = vertex_converter<clip_line_tag, transform_tag, using vertex_converter_type = vertex_converter<clip_line_tag, clip_poly_tag, transform_tag,
affine_transform_tag, affine_transform_tag,
simplify_tag, smooth_tag, simplify_tag, smooth_tag,
offset_transform_tag, offset_transform_tag,
dash_tag, stroke_tag>; dash_tag, stroke_tag>;
vertex_converter_type converter(clip_box, sym,common_.t_,prj_trans,tr,feature,common_.vars_,common_.scale_factor_); vertex_converter_type converter(clip_box, sym,common_.t_,prj_trans,tr,feature,common_.vars_,common_.scale_factor_);
if (clip)
if (clip) converter.set<clip_line_tag>(); // optional clip (default: true) {
geometry::geometry_types type = geometry::geometry_type(feature.get_geometry());
if (type == geometry::geometry_types::Polygon || type == geometry::geometry_types::MultiPolygon)
converter.template set<clip_poly_tag>();
else if (type == geometry::geometry_types::LineString || type == geometry::geometry_types::MultiLineString)
converter.template set<clip_line_tag>();
}
converter.set<transform_tag>(); // always transform converter.set<transform_tag>(); // always transform
if (std::fabs(offset) > 0.0) converter.set<offset_transform_tag>(); // parallel offset if (std::fabs(offset) > 0.0) converter.set<offset_transform_tag>(); // parallel offset
converter.set<affine_transform_tag>(); // optional affine transform converter.set<affine_transform_tag>(); // optional affine transform

View file

@ -60,7 +60,7 @@ void agg_renderer<T0,T1>::process(shield_symbolizer const& sym,
double opacity = get<double>(sym,keys::opacity, feature, common_.vars_, 1.0); double opacity = get<double>(sym,keys::opacity, feature, common_.vars_, 1.0);
placements_list const& placements = helper.get(); placements_list const& placements = helper.get();
for (glyph_positions_ptr glyphs : placements) for (auto const& glyphs : placements)
{ {
marker_info_ptr mark = glyphs->get_marker(); marker_info_ptr mark = glyphs->get_marker();
if (mark) if (mark)

View file

@ -67,7 +67,7 @@ void agg_renderer<T0,T1>::process(text_symbolizer const& sym,
} }
placements_list const& placements = helper.get(); placements_list const& placements = helper.get();
for (glyph_positions_ptr glyphs : placements) for (auto const& glyphs : placements)
{ {
ren.render(*glyphs); ren.render(*glyphs);
} }

View file

@ -28,8 +28,8 @@
#include <mapnik/text/face.hpp> #include <mapnik/text/face.hpp>
#include <cairo-ft.h> #include <cairo-ft.h>
#include <vector>
#include <valarray>
namespace mapnik { namespace mapnik {
cairo_face::cairo_face(std::shared_ptr<font_library> const& library, face_ptr const& face) cairo_face::cairo_face(std::shared_ptr<font_library> const& library, face_ptr const& face)
@ -227,17 +227,13 @@ void cairo_context::set_line_width(double width)
void cairo_context::set_dash(dash_array const& dashes, double scale_factor) void cairo_context::set_dash(dash_array const& dashes, double scale_factor)
{ {
std::valarray<double> d(dashes.size() * 2); std::vector<double> d;
dash_array::const_iterator itr = dashes.begin(); d.reserve(dashes.size() * 2);
dash_array::const_iterator end = dashes.end(); for (auto const& dash : dashes)
std::size_t index = 0;
for (; itr != end; ++itr)
{ {
d[index++] = itr->first * scale_factor; d.emplace_back(dash.first * scale_factor);
d[index++] = itr->second * scale_factor; d.emplace_back(dash.second * scale_factor);
} }
cairo_set_dash(cairo_.get() , &d[0], static_cast<int>(d.size()), 0/*offset*/); cairo_set_dash(cairo_.get() , &d[0], static_cast<int>(d.size()), 0/*offset*/);
check_object_status_and_throw_exception(*this); check_object_status_and_throw_exception(*this);
} }

View file

@ -95,7 +95,7 @@ struct thunk_renderer
render_offset_placements( render_offset_placements(
thunk.placements_, thunk.placements_,
offset_, offset_,
[&] (glyph_positions_ptr glyphs) [&] (glyph_positions_ptr const& glyphs)
{ {
marker_info_ptr mark = glyphs->get_marker(); marker_info_ptr mark = glyphs->get_marker();
if (mark) if (mark)

View file

@ -29,6 +29,7 @@
#include <mapnik/vertex_converters.hpp> #include <mapnik/vertex_converters.hpp>
#include <mapnik/vertex_processor.hpp> #include <mapnik/vertex_processor.hpp>
#include <mapnik/renderer_common/apply_vertex_converter.hpp> #include <mapnik/renderer_common/apply_vertex_converter.hpp>
#include <mapnik/geometry_type.hpp>
namespace mapnik namespace mapnik
{ {
@ -82,6 +83,7 @@ void cairo_renderer<T>::process(line_symbolizer const& sym,
clipping_extent.pad(padding); clipping_extent.pad(padding);
} }
using vertex_converter_type = vertex_converter<clip_line_tag, using vertex_converter_type = vertex_converter<clip_line_tag,
clip_poly_tag,
transform_tag, transform_tag,
affine_transform_tag, affine_transform_tag,
simplify_tag, smooth_tag, simplify_tag, smooth_tag,
@ -89,7 +91,14 @@ void cairo_renderer<T>::process(line_symbolizer const& sym,
vertex_converter_type converter(clipping_extent,sym,common_.t_,prj_trans,tr,feature,common_.vars_,common_.scale_factor_); vertex_converter_type converter(clipping_extent,sym,common_.t_,prj_trans,tr,feature,common_.vars_,common_.scale_factor_);
if (clip) converter.set<clip_line_tag>(); // optional clip (default: true) if (clip)
{
geometry::geometry_types type = geometry::geometry_type(feature.get_geometry());
if (type == geometry::geometry_types::Polygon || type == geometry::geometry_types::MultiPolygon)
converter.template set<clip_poly_tag>();
else if (type == geometry::geometry_types::LineString || type == geometry::geometry_types::MultiLineString)
converter.template set<clip_line_tag>();
}
converter.set<transform_tag>(); // always transform converter.set<transform_tag>(); // always transform
if (std::fabs(offset) > 0.0) converter.set<offset_transform_tag>(); // parallel offset if (std::fabs(offset) > 0.0) converter.set<offset_transform_tag>(); // parallel offset
converter.set<affine_transform_tag>(); // optional affine transform converter.set<affine_transform_tag>(); // optional affine transform

View file

@ -55,7 +55,7 @@ void cairo_renderer<T>::process(shield_symbolizer const& sym,
double opacity = get<double>(sym,keys::opacity,feature, common_.vars_, 1.0); double opacity = get<double>(sym,keys::opacity,feature, common_.vars_, 1.0);
placements_list const &placements = helper.get(); placements_list const &placements = helper.get();
for (glyph_positions_ptr glyphs : placements) for (auto const& glyphs : placements)
{ {
marker_info_ptr mark = glyphs->get_marker(); marker_info_ptr mark = glyphs->get_marker();
if (mark) { if (mark) {
@ -93,7 +93,7 @@ void cairo_renderer<T>::process(text_symbolizer const& sym,
composite_mode_e halo_comp_op = get<composite_mode_e>(sym, keys::halo_comp_op, feature, common_.vars_, src_over); composite_mode_e halo_comp_op = get<composite_mode_e>(sym, keys::halo_comp_op, feature, common_.vars_, src_over);
placements_list const& placements = helper.get(); placements_list const& placements = helper.get();
for (glyph_positions_ptr glyphs : placements) for (auto const& glyphs : placements)
{ {
context_.add_text(*glyphs, face_manager_, comp_op, halo_comp_op, common_.scale_factor_); context_.add_text(*glyphs, face_manager_, comp_op, halo_comp_op, common_.scale_factor_);
} }

View file

@ -129,7 +129,7 @@ struct thunk_renderer
render_offset_placements( render_offset_placements(
thunk.placements_, thunk.placements_,
offset_, offset_,
[&] (glyph_positions_ptr glyphs) [&] (glyph_positions_ptr const& glyphs)
{ {
marker_info_ptr mark = glyphs->get_marker(); marker_info_ptr mark = glyphs->get_marker();
if (mark) if (mark)

View file

@ -31,6 +31,7 @@
#include <mapnik/vertex_converters.hpp> #include <mapnik/vertex_converters.hpp>
#include <mapnik/vertex_processor.hpp> #include <mapnik/vertex_processor.hpp>
#include <mapnik/renderer_common/apply_vertex_converter.hpp> #include <mapnik/renderer_common/apply_vertex_converter.hpp>
#include <mapnik/geometry_type.hpp>
// agg // agg
#include "agg_rasterizer_scanline_aa.h" #include "agg_rasterizer_scanline_aa.h"
#include "agg_renderer_scanline.h" #include "agg_renderer_scanline.h"
@ -89,14 +90,21 @@ void grid_renderer<T>::process(line_symbolizer const& sym,
padding *= common_.scale_factor_; padding *= common_.scale_factor_;
clipping_extent.pad(padding); clipping_extent.pad(padding);
} }
using vertex_converter_type = vertex_converter<clip_line_tag, transform_tag, using vertex_converter_type = vertex_converter<clip_line_tag, clip_poly_tag, transform_tag,
affine_transform_tag, affine_transform_tag,
simplify_tag, smooth_tag, simplify_tag, smooth_tag,
offset_transform_tag, offset_transform_tag,
dash_tag, stroke_tag>; dash_tag, stroke_tag>;
vertex_converter_type converter(clipping_extent,sym,common_.t_,prj_trans,tr,feature,common_.vars_,common_.scale_factor_); vertex_converter_type converter(clipping_extent,sym,common_.t_,prj_trans,tr,feature,common_.vars_,common_.scale_factor_);
if (clip) converter.set<clip_line_tag>(); // optional clip (default: true) if (clip)
{
geometry::geometry_types type = geometry::geometry_type(feature.get_geometry());
if (type == geometry::geometry_types::Polygon || type == geometry::geometry_types::MultiPolygon)
converter.template set<clip_poly_tag>();
else if (type == geometry::geometry_types::LineString || type == geometry::geometry_types::MultiLineString)
converter.template set<clip_line_tag>();
}
converter.set<transform_tag>(); // always transform converter.set<transform_tag>(); // always transform
if (std::fabs(offset) > 0.0) converter.set<offset_transform_tag>(); // parallel offset if (std::fabs(offset) > 0.0) converter.set<offset_transform_tag>(); // parallel offset
converter.set<affine_transform_tag>(); // optional affine transform converter.set<affine_transform_tag>(); // optional affine transform

View file

@ -64,7 +64,7 @@ void grid_renderer<T>::process(shield_symbolizer const& sym,
placements_list const& placements = helper.get(); placements_list const& placements = helper.get();
value_integer feature_id = feature.id(); value_integer feature_id = feature.id();
for (glyph_positions_ptr glyphs : placements) for (auto const& glyphs : placements)
{ {
marker_info_ptr mark = glyphs->get_marker(); marker_info_ptr mark = glyphs->get_marker();
if (mark) if (mark)

View file

@ -66,7 +66,7 @@ void grid_renderer<T>::process(text_symbolizer const& sym,
placements_list const& placements = helper.get(); placements_list const& placements = helper.get();
value_integer feature_id = feature.id(); value_integer feature_id = feature.id();
for (glyph_positions_ptr glyphs : placements) for (auto const& glyphs : placements)
{ {
ren.render(*glyphs, feature_id); ren.render(*glyphs, feature_id);
placement_found = true; placement_found = true;

View file

@ -368,6 +368,13 @@ template MAPNIK_DECL void save_to_file<image_view_any> (image_view_any const&,
std::string const&, std::string const&,
rgba_palette const& palette); rgba_palette const& palette);
template MAPNIK_DECL std::string save_to_string<image_view_rgba8> (image_view_rgba8 const&,
std::string const&);
template MAPNIK_DECL std::string save_to_string<image_view_rgba8> (image_view_rgba8 const&,
std::string const&,
rgba_palette const& palette);
template MAPNIK_DECL std::string save_to_string<image_view_any> (image_view_any const&, template MAPNIK_DECL std::string save_to_string<image_view_any> (image_view_any const&,
std::string const&); std::string const&);

View file

@ -183,11 +183,27 @@ bool placement_finder::find_point_placement(pixel_position const& pos)
// add_marker first checks for collision and then updates the detector. // add_marker first checks for collision and then updates the detector.
if (has_marker_ && !add_marker(glyphs, pos)) return false; if (has_marker_ && !add_marker(glyphs, pos)) return false;
box2d<double> label_box;
bool first = true;
for (auto const& label : labels) for (auto const& label : labels)
{ {
detector_.insert(std::get<0>(label), layouts_.text(), std::get<1>(label)); auto const& box = std::get<0>(label);
if (first)
{
label_box = box;
first = false;
}
else
{
label_box.expand_to_include(box);
}
detector_.insert(box, layouts_.text(), std::get<1>(label));
}
// do not render text off the canvas
if (extent_.intersects(label_box))
{
placements_.push_back(std::move(glyphs));
} }
placements_.push_back(glyphs);
return true; return true;
} }
@ -201,7 +217,7 @@ bool placement_finder::single_line_placement(vertex_cache &pp, text_upright_e or
vertex_cache::scoped_state begin(pp); vertex_cache::scoped_state begin(pp);
text_upright_e real_orientation = simplify_upright(orientation, pp.angle()); text_upright_e real_orientation = simplify_upright(orientation, pp.angle());
glyph_positions_ptr glyphs = std::make_shared<glyph_positions>(); glyph_positions_ptr glyphs = std::make_unique<glyph_positions>();
std::vector<box2d<double> > bboxes; std::vector<box2d<double> > bboxes;
glyphs->reserve(layouts_.glyphs_count()); glyphs->reserve(layouts_.glyphs_count());
bboxes.reserve(layouts_.glyphs_count()); bboxes.reserve(layouts_.glyphs_count());
@ -214,7 +230,8 @@ bool placement_finder::single_line_placement(vertex_cache &pp, text_upright_e or
pixel_position align_offset = layout.alignment_offset(); pixel_position align_offset = layout.alignment_offset();
pixel_position const& layout_displacement = layout.displacement(); pixel_position const& layout_displacement = layout.displacement();
double sign = (real_orientation == UPRIGHT_LEFT) ? -1 : 1; double sign = (real_orientation == UPRIGHT_LEFT) ? -1 : 1;
double offset = layout_displacement.y + 0.5 * sign * layout.height(); //double offset = 0 - (layout_displacement.y + 0.5 * sign * layout.height());
double offset = layout_displacement.y - 0.5 * sign * layout.height();
double adjust_character_spacing = .0; double adjust_character_spacing = .0;
double layout_width = layout.width(); double layout_width = layout.width();
bool adjust = layout.horizontal_alignment() == H_ADJUST; bool adjust = layout.horizontal_alignment() == H_ADJUST;
@ -233,7 +250,7 @@ bool placement_finder::single_line_placement(vertex_cache &pp, text_upright_e or
{ {
// Only subtract half the line height here and half at the end because text is automatically // Only subtract half the line height here and half at the end because text is automatically
// centered on the line // centered on the line
offset -= sign * line.height()/2; offset += sign * line.height()/2;
vertex_cache & off_pp = pp.get_offseted(offset, sign * layout_width); vertex_cache & off_pp = pp.get_offseted(offset, sign * layout_width);
vertex_cache::scoped_state off_state(off_pp); // TODO: Remove this when a clean implementation in vertex_cache::get_offseted is done vertex_cache::scoped_state off_state(off_pp); // TODO: Remove this when a clean implementation in vertex_cache::get_offseted is done
double line_width = adjust ? (line.glyphs_width() + line.space_count() * adjust_character_spacing) : line.width(); double line_width = adjust ? (line.glyphs_width() + line.space_count() * adjust_character_spacing) : line.width();
@ -301,7 +318,7 @@ bool placement_finder::single_line_placement(vertex_cache &pp, text_upright_e or
glyphs->emplace_back(glyph, pos, rot); glyphs->emplace_back(glyph, pos, rot);
} }
// See comment above // See comment above
offset -= sign * line.height()/2; offset += sign * line.height()/2;
} }
} }
@ -326,11 +343,26 @@ bool placement_finder::single_line_placement(vertex_cache &pp, text_upright_e or
return single_line_placement(pp, real_orientation == UPRIGHT_RIGHT ? UPRIGHT_LEFT : UPRIGHT_RIGHT); return single_line_placement(pp, real_orientation == UPRIGHT_RIGHT ? UPRIGHT_LEFT : UPRIGHT_RIGHT);
} }
box2d<double> label_box;
bool first = true;
for (box2d<double> const& box : bboxes) for (box2d<double> const& box : bboxes)
{ {
if (first)
{
label_box = box;
first = false;
}
else
{
label_box.expand_to_include(box);
}
detector_.insert(box, layouts_.text()); detector_.insert(box, layouts_.text());
} }
placements_.push_back(glyphs); // do not render text off the canvas
if (extent_.intersects(label_box))
{
placements_.push_back(std::move(glyphs));
}
return true; return true;
} }
@ -369,9 +401,7 @@ bool placement_finder::collision(box2d<double> const& box, value_unicode_string
margin = (text_props_->margin != 0 ? text_props_->margin : text_props_->minimum_distance) * scale_factor_; margin = (text_props_->margin != 0 ? text_props_->margin : text_props_->minimum_distance) * scale_factor_;
repeat_distance = text_props_->repeat_distance * scale_factor_; repeat_distance = text_props_->repeat_distance * scale_factor_;
} }
return !detector_.extent().intersects(box) return (text_props_->avoid_edges && !extent_.contains(box))
||
(text_props_->avoid_edges && !extent_.contains(box))
|| ||
(text_props_->minimum_padding > 0 && (text_props_->minimum_padding > 0 &&
!extent_.contains(box + (scale_factor_ * text_props_->minimum_padding))) !extent_.contains(box + (scale_factor_ * text_props_->minimum_padding)))
@ -393,7 +423,7 @@ void placement_finder::set_marker(marker_info_ptr m, box2d<double> box, bool mar
} }
bool placement_finder::add_marker(glyph_positions_ptr glyphs, pixel_position const& pos) const bool placement_finder::add_marker(glyph_positions_ptr & glyphs, pixel_position const& pos) const
{ {
pixel_position real_pos = (marker_unlocked_ ? pos : glyphs->get_base_point()) + marker_displacement_; pixel_position real_pos = (marker_unlocked_ ? pos : glyphs->get_base_point()) + marker_displacement_;
box2d<double> bbox = marker_box_; box2d<double> bbox = marker_box_;

View file

@ -92,13 +92,12 @@ void text_renderer::prepare_glyphs(glyph_positions const& positions)
template <typename T> template <typename T>
void composite_bitmap(T & pixmap, FT_Bitmap *bitmap, unsigned rgba, int x, int y, double opacity, composite_mode_e comp_op) void composite_bitmap(T & pixmap, FT_Bitmap *bitmap, unsigned rgba, int x, int y, double opacity, composite_mode_e comp_op)
{ {
int x_max=x+bitmap->width; int x_max = x + bitmap->width;
int y_max=y+bitmap->rows; int y_max = y + bitmap->rows;
int i,p,j,q;
for (i=x,p=0;i<x_max;++i,++p) for (int i = x, p = 0; i < x_max; ++i, ++p)
{ {
for (j=y,q=0;j<y_max;++j,++q) for (int j = y, q = 0; j < y_max; ++j, ++q)
{ {
unsigned gray=bitmap->buffer[q*bitmap->width+p]; unsigned gray=bitmap->buffer[q*bitmap->width+p];
if (gray) if (gray)

View file

@ -155,8 +155,14 @@ vertex_cache & vertex_cache::get_offseted(double offset, double region_width)
// find the point on the offset line closest to the current position, // find the point on the offset line closest to the current position,
// which we'll use to make the offset line aligned to this one. // which we'll use to make the offset line aligned to this one.
double seek = offseted_line->position_closest_to(current_position_); // TODO: `position_closest_to` is disable since it is too expensive:
// https://github.com/mapnik/mapnik/issues/2937
//double seek = offseted_line->position_closest_to(current_position_);
double seek = (position_ + region_width/2.0) * offseted_line->length() / length() - region_width/2.0;
if (seek < 0) seek = 0;
if (seek > offseted_line->length()) seek = offseted_line->length();
offseted_line->move(seek); offseted_line->move(seek);
return *offseted_line; return *offseted_line;
} }

@ -1 +1 @@
Subproject commit a6838f99a4c4be37989cea6718442cf4b53bd616 Subproject commit 2823c6a0ba9e642869537d8e5c1a1ca5fd3de18a

@ -1 +1 @@
Subproject commit 7080866ca1b3523fead1a25c017bd87082806eb4 Subproject commit 7de3183129a08c835a3205f5d51c85e59494ee33

View file

@ -10,7 +10,7 @@
#include <mapnik/expression.hpp> #include <mapnik/expression.hpp>
#include <mapnik/expression_evaluator.hpp> #include <mapnik/expression_evaluator.hpp>
#include <mapnik/debug.hpp> #include <mapnik/debug.hpp>
#include <mapnik/util/fs.hpp>
#include <boost/filesystem.hpp> #include <boost/filesystem.hpp>
#include <boost/range/iterator_range_core.hpp> #include <boost/range/iterator_range_core.hpp>
#include <boost/format.hpp> #include <boost/format.hpp>
@ -157,519 +157,526 @@ void require_geometry(mapnik::feature_ptr feature,
} }
} // anonymous namespace } // anonymous namespace
const bool registered = mapnik::datasource_cache::instance().register_datasources("./plugins/input/"); static const std::string csv_plugin("./plugins/input/csv.input");
const bool registered = mapnik::datasource_cache::instance().register_datasources(csv_plugin);
TEST_CASE("csv") { TEST_CASE("csv") {
REQUIRE(registered);
// make the tests silent since we intentially test error conditions that are noisy if (mapnik::util::exists(csv_plugin))
auto const severity = mapnik::logger::instance().get_severity(); {
mapnik::logger::instance().set_severity(mapnik::logger::none);
// check the CSV datasource is loaded REQUIRE(registered);
const std::vector<std::string> plugin_names =
mapnik::datasource_cache::instance().plugin_names();
const bool have_csv_plugin =
std::find(plugin_names.begin(), plugin_names.end(), "csv") != plugin_names.end();
SECTION("broken files") { // make the tests silent since we intentially test error conditions that are noisy
if (have_csv_plugin) { auto const severity = mapnik::logger::instance().get_severity();
std::vector<bfs::path> broken; mapnik::logger::instance().set_severity(mapnik::logger::none);
add_csv_files("test/data/csv/fails", broken);
add_csv_files("test/data/csv/warns", broken);
broken.emplace_back("test/data/csv/fails/does_not_exist.csv");
for (auto const &path : broken) { // check the CSV datasource is loaded
REQUIRE_THROWS(get_csv_ds(path.native())); const std::vector<std::string> plugin_names =
} mapnik::datasource_cache::instance().plugin_names();
} const bool have_csv_plugin =
} // END SECTION std::find(plugin_names.begin(), plugin_names.end(), "csv") != plugin_names.end();
SECTION("good files") { SECTION("broken files") {
if (have_csv_plugin) { if (have_csv_plugin) {
std::vector<bfs::path> good; std::vector<bfs::path> broken;
add_csv_files("test/data/csv", good); add_csv_files("test/data/csv/fails", broken);
add_csv_files("test/data/csv/warns", good); add_csv_files("test/data/csv/warns", broken);
broken.emplace_back("test/data/csv/fails/does_not_exist.csv");
for (auto const &path : good) { for (auto const &path : broken) {
auto ds = get_csv_ds(path.native(), false); REQUIRE_THROWS(get_csv_ds(path.native()));
// require a non-null pointer returned }
REQUIRE(bool(ds)); }
} } // END SECTION
}
} // END SECTION
SECTION("lon/lat detection") { SECTION("good files") {
for (auto const &lon_name : {std::string("lon"), std::string("lng")}) { if (have_csv_plugin) {
auto ds = get_csv_ds((boost::format("test/data/csv/%1%_lat.csv") % lon_name).str()); std::vector<bfs::path> good;
auto fields = ds->get_descriptor().get_descriptors(); add_csv_files("test/data/csv", good);
require_field_names(fields, {lon_name, "lat"}); add_csv_files("test/data/csv/warns", good);
require_field_types(fields, {mapnik::Integer, mapnik::Integer});
CHECK(ds->get_geometry_type() == mapnik::datasource_geometry_t::Point); for (auto const &path : good) {
auto ds = get_csv_ds(path.native(), false);
// require a non-null pointer returned
REQUIRE(bool(ds));
}
}
} // END SECTION
mapnik::query query(ds->envelope()); SECTION("lon/lat detection") {
for (auto const &field : fields) { for (auto const &lon_name : {std::string("lon"), std::string("lng")}) {
query.add_property_name(field.get_name()); auto ds = get_csv_ds((boost::format("test/data/csv/%1%_lat.csv") % lon_name).str());
} auto fields = ds->get_descriptor().get_descriptors();
auto features = ds->features(query); require_field_names(fields, {lon_name, "lat"});
auto feature = features->next(); require_field_types(fields, {mapnik::Integer, mapnik::Integer});
require_attributes(feature, { CHECK(ds->get_geometry_type() == mapnik::datasource_geometry_t::Point);
attr { lon_name, mapnik::value_integer(0) },
attr { "lat", mapnik::value_integer(0) }
});
}
} // END SECTION
SECTION("type detection") { mapnik::query query(ds->envelope());
auto ds = get_csv_ds("test/data/csv/nypd.csv"); for (auto const &field : fields) {
auto fields = ds->get_descriptor().get_descriptors(); query.add_property_name(field.get_name());
require_field_names(fields, {"Precinct", "Phone", "Address", "City", "geo_longitude", "geo_latitude", "geo_accuracy"}); }
require_field_types(fields, {mapnik::String, mapnik::String, mapnik::String, mapnik::String, mapnik::Double, mapnik::Double, mapnik::String}); auto features = ds->features(query);
auto feature = features->next();
CHECK(ds->get_geometry_type() == mapnik::datasource_geometry_t::Point); require_attributes(feature, {
CHECK(count_features(all_features(ds)) == 2); attr { lon_name, mapnik::value_integer(0) },
attr { "lat", mapnik::value_integer(0) }
});
}
} // END SECTION
auto feature = all_features(ds)->next(); SECTION("type detection") {
require_attributes(feature, { auto ds = get_csv_ds("test/data/csv/nypd.csv");
attr { "City", mapnik::value_unicode_string("New York, NY") } auto fields = ds->get_descriptor().get_descriptors();
, attr { "geo_accuracy", mapnik::value_unicode_string("house") } require_field_names(fields, {"Precinct", "Phone", "Address", "City", "geo_longitude", "geo_latitude", "geo_accuracy"});
, attr { "Phone", mapnik::value_unicode_string("(212) 334-0711") } require_field_types(fields, {mapnik::String, mapnik::String, mapnik::String, mapnik::String, mapnik::Double, mapnik::Double, mapnik::String});
, attr { "Address", mapnik::value_unicode_string("19 Elizabeth Street") }
, attr { "Precinct", mapnik::value_unicode_string("5th Precinct") }
, attr { "geo_longitude", mapnik::value_integer(-70) }
, attr { "geo_latitude", mapnik::value_integer(40) }
});
} // END SECTION
SECTION("skipping blank rows") { CHECK(ds->get_geometry_type() == mapnik::datasource_geometry_t::Point);
auto ds = get_csv_ds("test/data/csv/blank_rows.csv"); CHECK(count_features(all_features(ds)) == 2);
auto fields = ds->get_descriptor().get_descriptors();
require_field_names(fields, {"x", "y", "name"});
require_field_types(fields, {mapnik::Integer, mapnik::Integer, mapnik::String});
CHECK(ds->get_geometry_type() == mapnik::datasource_geometry_t::Point); auto feature = all_features(ds)->next();
CHECK(count_features(all_features(ds)) == 2); require_attributes(feature, {
} // END SECTION attr { "City", mapnik::value_unicode_string("New York, NY") }
, attr { "geo_accuracy", mapnik::value_unicode_string("house") }
, attr { "Phone", mapnik::value_unicode_string("(212) 334-0711") }
, attr { "Address", mapnik::value_unicode_string("19 Elizabeth Street") }
, attr { "Precinct", mapnik::value_unicode_string("5th Precinct") }
, attr { "geo_longitude", mapnik::value_integer(-70) }
, attr { "geo_latitude", mapnik::value_integer(40) }
});
} // END SECTION
SECTION("empty rows") { SECTION("skipping blank rows") {
auto ds = get_csv_ds("test/data/csv/empty_rows.csv"); auto ds = get_csv_ds("test/data/csv/blank_rows.csv");
auto fields = ds->get_descriptor().get_descriptors(); auto fields = ds->get_descriptor().get_descriptors();
require_field_names(fields, {"x", "y", "text", "date", "integer", "boolean", "float", "time", "datetime", "empty_column"}); require_field_names(fields, {"x", "y", "name"});
require_field_types(fields, {mapnik::Integer, mapnik::Integer, mapnik::String, mapnik::String, mapnik::Integer, mapnik::Boolean, mapnik::Double, mapnik::String, mapnik::String, mapnik::String}); require_field_types(fields, {mapnik::Integer, mapnik::Integer, mapnik::String});
CHECK(ds->get_geometry_type() == mapnik::datasource_geometry_t::Point); CHECK(ds->get_geometry_type() == mapnik::datasource_geometry_t::Point);
CHECK(count_features(all_features(ds)) == 4); CHECK(count_features(all_features(ds)) == 2);
} // END SECTION
auto featureset = all_features(ds); SECTION("empty rows") {
auto feature = featureset->next(); auto ds = get_csv_ds("test/data/csv/empty_rows.csv");
require_attributes(feature, { auto fields = ds->get_descriptor().get_descriptors();
attr { "x", mapnik::value_integer(0) } require_field_names(fields, {"x", "y", "text", "date", "integer", "boolean", "float", "time", "datetime", "empty_column"});
, attr { "empty_column", mapnik::value_unicode_string("") } require_field_types(fields, {mapnik::Integer, mapnik::Integer, mapnik::String, mapnik::String, mapnik::Integer, mapnik::Boolean, mapnik::Double, mapnik::String, mapnik::String, mapnik::String});
, attr { "text", mapnik::value_unicode_string("a b") }
, attr { "float", mapnik::value_double(1.0) }
, attr { "datetime", mapnik::value_unicode_string("1971-01-01T04:14:00") }
, attr { "y", mapnik::value_integer(0) }
, attr { "boolean", mapnik::value_bool(true) }
, attr { "time", mapnik::value_unicode_string("04:14:00") }
, attr { "date", mapnik::value_unicode_string("1971-01-01") }
, attr { "integer", mapnik::value_integer(40) }
});
while (bool(feature = featureset->next())) { CHECK(ds->get_geometry_type() == mapnik::datasource_geometry_t::Point);
CHECK(feature->size() == 10); CHECK(count_features(all_features(ds)) == 4);
CHECK(feature->get("empty_column") == mapnik::value_unicode_string(""));
}
} // END SECTION
SECTION("slashes") { auto featureset = all_features(ds);
auto ds = get_csv_ds("test/data/csv/has_attributes_with_slashes.csv"); auto feature = featureset->next();
auto fields = ds->get_descriptor().get_descriptors(); require_attributes(feature, {
require_field_names(fields, {"x", "y", "name"}); attr { "x", mapnik::value_integer(0) }
// NOTE: y column is integer, even though a double value is used below in the test? , attr { "empty_column", mapnik::value_unicode_string("") }
require_field_types(fields, {mapnik::Integer, mapnik::Integer, mapnik::String}); , attr { "text", mapnik::value_unicode_string("a b") }
, attr { "float", mapnik::value_double(1.0) }
, attr { "datetime", mapnik::value_unicode_string("1971-01-01T04:14:00") }
, attr { "y", mapnik::value_integer(0) }
, attr { "boolean", mapnik::value_bool(true) }
, attr { "time", mapnik::value_unicode_string("04:14:00") }
, attr { "date", mapnik::value_unicode_string("1971-01-01") }
, attr { "integer", mapnik::value_integer(40) }
});
auto featureset = all_features(ds); while (bool(feature = featureset->next())) {
require_attributes(featureset->next(), { CHECK(feature->size() == 10);
attr{"x", 0} CHECK(feature->get("empty_column") == mapnik::value_unicode_string(""));
, attr{"y", 0} }
, attr{"name", mapnik::value_unicode_string("a/a") } }); } // END SECTION
require_attributes(featureset->next(), {
attr{"x", 1}
, attr{"y", 4}
, attr{"name", mapnik::value_unicode_string("b/b") } });
require_attributes(featureset->next(), {
attr{"x", 10}
, attr{"y", 2.5}
, attr{"name", mapnik::value_unicode_string("c/c") } });
} // END SECTION
SECTION("wkt field") { SECTION("slashes") {
using mapnik::geometry::geometry_types; auto ds = get_csv_ds("test/data/csv/has_attributes_with_slashes.csv");
auto fields = ds->get_descriptor().get_descriptors();
require_field_names(fields, {"x", "y", "name"});
// NOTE: y column is integer, even though a double value is used below in the test?
require_field_types(fields, {mapnik::Integer, mapnik::Integer, mapnik::String});
auto ds = get_csv_ds("test/data/csv/wkt.csv"); auto featureset = all_features(ds);
auto fields = ds->get_descriptor().get_descriptors(); require_attributes(featureset->next(), {
require_field_names(fields, {"type"}); attr{"x", 0}
require_field_types(fields, {mapnik::String}); , attr{"y", 0}
, attr{"name", mapnik::value_unicode_string("a/a") } });
require_attributes(featureset->next(), {
attr{"x", 1}
, attr{"y", 4}
, attr{"name", mapnik::value_unicode_string("b/b") } });
require_attributes(featureset->next(), {
attr{"x", 10}
, attr{"y", 2.5}
, attr{"name", mapnik::value_unicode_string("c/c") } });
} // END SECTION
auto featureset = all_features(ds); SECTION("wkt field") {
require_geometry(featureset->next(), 1, geometry_types::Point); using mapnik::geometry::geometry_types;
require_geometry(featureset->next(), 1, geometry_types::LineString);
require_geometry(featureset->next(), 1, geometry_types::Polygon);
require_geometry(featureset->next(), 1, geometry_types::Polygon);
require_geometry(featureset->next(), 4, geometry_types::MultiPoint);
require_geometry(featureset->next(), 2, geometry_types::MultiLineString);
require_geometry(featureset->next(), 2, geometry_types::MultiPolygon);
require_geometry(featureset->next(), 2, geometry_types::MultiPolygon);
} // END SECTION
SECTION("handling of missing header") { auto ds = get_csv_ds("test/data/csv/wkt.csv");
// TODO: does this mean 'missing_header.csv' should be in the warnings auto fields = ds->get_descriptor().get_descriptors();
// subdirectory, since it doesn't work in strict mode? require_field_names(fields, {"type"});
auto ds = get_csv_ds("test/data/csv/missing_header.csv", false); require_field_types(fields, {mapnik::String});
auto fields = ds->get_descriptor().get_descriptors();
require_field_names(fields, {"one", "two", "x", "y", "_4", "aftermissing"});
auto feature = all_features(ds)->next();
REQUIRE(feature);
REQUIRE(feature->has_key("_4"));
CHECK(feature->get("_4") == mapnik::value_unicode_string("missing"));
} // END SECTION
SECTION("handling of headers that are numbers") { auto featureset = all_features(ds);
auto ds = get_csv_ds("test/data/csv/numbers_for_headers.csv"); require_geometry(featureset->next(), 1, geometry_types::Point);
auto fields = ds->get_descriptor().get_descriptors(); require_geometry(featureset->next(), 1, geometry_types::LineString);
require_field_names(fields, {"x", "y", "1990", "1991", "1992"}); require_geometry(featureset->next(), 1, geometry_types::Polygon);
auto feature = all_features(ds)->next(); require_geometry(featureset->next(), 1, geometry_types::Polygon);
require_attributes(feature, { require_geometry(featureset->next(), 4, geometry_types::MultiPoint);
attr{"x", 0} require_geometry(featureset->next(), 2, geometry_types::MultiLineString);
, attr{"y", 0} require_geometry(featureset->next(), 2, geometry_types::MultiPolygon);
, attr{"1990", 1} require_geometry(featureset->next(), 2, geometry_types::MultiPolygon);
, attr{"1991", 2} } // END SECTION
, attr{"1992", 3}
});
auto expression = mapnik::parse_expression("[1991]=2");
REQUIRE(bool(expression));
auto value = mapnik::util::apply_visitor(
mapnik::evaluate<mapnik::feature_impl, mapnik::value_type, mapnik::attributes>(
*feature, mapnik::attributes()), *expression);
CHECK(value == true);
} // END SECTION
SECTION("quoted numbers") { SECTION("handling of missing header") {
using ustring = mapnik::value_unicode_string; // TODO: does this mean 'missing_header.csv' should be in the warnings
// subdirectory, since it doesn't work in strict mode?
auto ds = get_csv_ds("test/data/csv/missing_header.csv", false);
auto fields = ds->get_descriptor().get_descriptors();
require_field_names(fields, {"one", "two", "x", "y", "_4", "aftermissing"});
auto feature = all_features(ds)->next();
REQUIRE(feature);
REQUIRE(feature->has_key("_4"));
CHECK(feature->get("_4") == mapnik::value_unicode_string("missing"));
} // END SECTION
auto ds = get_csv_ds("test/data/csv/quoted_numbers.csv"); SECTION("handling of headers that are numbers") {
auto fields = ds->get_descriptor().get_descriptors(); auto ds = get_csv_ds("test/data/csv/numbers_for_headers.csv");
require_field_names(fields, {"x", "y", "label"}); auto fields = ds->get_descriptor().get_descriptors();
auto featureset = all_features(ds); require_field_names(fields, {"x", "y", "1990", "1991", "1992"});
auto feature = all_features(ds)->next();
require_attributes(feature, {
attr{"x", 0}
, attr{"y", 0}
, attr{"1990", 1}
, attr{"1991", 2}
, attr{"1992", 3}
});
auto expression = mapnik::parse_expression("[1991]=2");
REQUIRE(bool(expression));
auto value = mapnik::util::apply_visitor(
mapnik::evaluate<mapnik::feature_impl, mapnik::value_type, mapnik::attributes>(
*feature, mapnik::attributes()), *expression);
CHECK(value == true);
} // END SECTION
require_attributes(featureset->next(), { SECTION("quoted numbers") {
attr{"x", 0}, attr{"y", 0}, attr{"label", ustring("0,0") } }); using ustring = mapnik::value_unicode_string;
require_attributes(featureset->next(), {
attr{"x", 5}, attr{"y", 5}, attr{"label", ustring("5,5") } });
require_attributes(featureset->next(), {
attr{"x", 0}, attr{"y", 5}, attr{"label", ustring("0,5") } });
require_attributes(featureset->next(), {
attr{"x", 5}, attr{"y", 0}, attr{"label", ustring("5,0") } });
require_attributes(featureset->next(), {
attr{"x", 2.5}, attr{"y", 2.5}, attr{"label", ustring("2.5,2.5") } });
} // END SECTION auto ds = get_csv_ds("test/data/csv/quoted_numbers.csv");
auto fields = ds->get_descriptor().get_descriptors();
require_field_names(fields, {"x", "y", "label"});
auto featureset = all_features(ds);
SECTION("reading newlines") { require_attributes(featureset->next(), {
for (auto const &platform : {std::string("windows"), std::string("mac")}) { attr{"x", 0}, attr{"y", 0}, attr{"label", ustring("0,0") } });
std::string file_name = (boost::format("test/data/csv/%1%_newlines.csv") % platform).str(); require_attributes(featureset->next(), {
auto ds = get_csv_ds(file_name); attr{"x", 5}, attr{"y", 5}, attr{"label", ustring("5,5") } });
auto fields = ds->get_descriptor().get_descriptors(); require_attributes(featureset->next(), {
require_field_names(fields, {"x", "y", "z"}); attr{"x", 0}, attr{"y", 5}, attr{"label", ustring("0,5") } });
require_attributes(all_features(ds)->next(), { require_attributes(featureset->next(), {
attr{"x", 1}, attr{"y", 10}, attr{"z", 9999.9999} }); attr{"x", 5}, attr{"y", 0}, attr{"label", ustring("5,0") } });
} require_attributes(featureset->next(), {
} // END SECTION attr{"x", 2.5}, attr{"y", 2.5}, attr{"label", ustring("2.5,2.5") } });
SECTION("mixed newlines") { } // END SECTION
using ustring = mapnik::value_unicode_string;
for (auto const &file : { SECTION("reading newlines") {
std::string("test/data/csv/mac_newlines_with_unix_inline.csv") for (auto const &platform : {std::string("windows"), std::string("mac")}) {
, std::string("test/data/csv/mac_newlines_with_unix_inline_escaped.csv") std::string file_name = (boost::format("test/data/csv/%1%_newlines.csv") % platform).str();
, std::string("test/data/csv/windows_newlines_with_unix_inline.csv") auto ds = get_csv_ds(file_name);
, std::string("test/data/csv/windows_newlines_with_unix_inline_escaped.csv") auto fields = ds->get_descriptor().get_descriptors();
require_field_names(fields, {"x", "y", "z"});
require_attributes(all_features(ds)->next(), {
attr{"x", 1}, attr{"y", 10}, attr{"z", 9999.9999} });
}
} // END SECTION
SECTION("mixed newlines") {
using ustring = mapnik::value_unicode_string;
for (auto const &file : {
std::string("test/data/csv/mac_newlines_with_unix_inline.csv")
, std::string("test/data/csv/mac_newlines_with_unix_inline_escaped.csv")
, std::string("test/data/csv/windows_newlines_with_unix_inline.csv")
, std::string("test/data/csv/windows_newlines_with_unix_inline_escaped.csv")
}) {
auto ds = get_csv_ds(file);
auto fields = ds->get_descriptor().get_descriptors();
require_field_names(fields, {"x", "y", "line"});
require_attributes(all_features(ds)->next(), {
attr{"x", 0}, attr{"y", 0}
, attr{"line", ustring("many\n lines\n of text\n with unix newlines")} });
}
} // END SECTION
SECTION("tabs") {
auto ds = get_csv_ds("test/data/csv/tabs_in_csv.csv");
auto fields = ds->get_descriptor().get_descriptors();
require_field_names(fields, {"x", "y", "z"});
require_attributes(all_features(ds)->next(), {
attr{"x", -122}, attr{"y", 48}, attr{"z", 0} });
} // END SECTION
SECTION("separators") {
using ustring = mapnik::value_unicode_string;
for (auto const &file : {
std::string("test/data/csv/pipe_delimiters.csv")
, std::string("test/data/csv/semicolon_delimiters.csv")
}) {
auto ds = get_csv_ds(file);
auto fields = ds->get_descriptor().get_descriptors();
require_field_names(fields, {"x", "y", "z"});
require_attributes(all_features(ds)->next(), {
attr{"x", 0}, attr{"y", 0}, attr{"z", ustring("hello")} });
}
} // END SECTION
SECTION("null and bool keywords are empty strings") {
using ustring = mapnik::value_unicode_string;
auto ds = get_csv_ds("test/data/csv/nulls_and_booleans_as_strings.csv");
auto fields = ds->get_descriptor().get_descriptors();
require_field_names(fields, {"x", "y", "null", "boolean"});
require_field_types(fields, {mapnik::Integer, mapnik::Integer, mapnik::String, mapnik::Boolean});
auto featureset = all_features(ds);
require_attributes(featureset->next(), {
attr{"x", 0}, attr{"y", 0}, attr{"null", ustring("null")}, attr{"boolean", true}});
require_attributes(featureset->next(), {
attr{"x", 0}, attr{"y", 0}, attr{"null", ustring("")}, attr{"boolean", false}});
} // END SECTION
SECTION("nonexistent query fields throw") {
auto ds = get_csv_ds("test/data/csv/lon_lat.csv");
auto fields = ds->get_descriptor().get_descriptors();
require_field_names(fields, {"lon", "lat"});
require_field_types(fields, {mapnik::Integer, mapnik::Integer});
mapnik::query query(ds->envelope());
for (auto const &field : fields) {
query.add_property_name(field.get_name());
}
// also add an invalid one, triggering throw
query.add_property_name("bogus");
REQUIRE_THROWS(ds->features(query));
} // END SECTION
SECTION("leading zeros mean strings") {
using ustring = mapnik::value_unicode_string;
auto ds = get_csv_ds("test/data/csv/leading_zeros.csv");
auto fields = ds->get_descriptor().get_descriptors();
require_field_names(fields, {"x", "y", "fips"});
require_field_types(fields, {mapnik::Integer, mapnik::Integer, mapnik::String});
auto featureset = all_features(ds);
require_attributes(featureset->next(), {
attr{"x", 0}, attr{"y", 0}, attr{"fips", ustring("001")}});
require_attributes(featureset->next(), {
attr{"x", 0}, attr{"y", 0}, attr{"fips", ustring("003")}});
require_attributes(featureset->next(), {
attr{"x", 0}, attr{"y", 0}, attr{"fips", ustring("005")}});
} // END SECTION
SECTION("advanced geometry detection") {
using row = std::pair<std::string, mapnik::datasource_geometry_t>;
for (row r : {
row{"point", mapnik::datasource_geometry_t::Point}
, row{"poly", mapnik::datasource_geometry_t::Polygon}
, row{"multi_poly", mapnik::datasource_geometry_t::Polygon}
, row{"line", mapnik::datasource_geometry_t::LineString}
}) { }) {
auto ds = get_csv_ds(file); std::string file_name = (boost::format("test/data/csv/%1%_wkt.csv") % r.first).str();
auto fields = ds->get_descriptor().get_descriptors(); auto ds = get_csv_ds(file_name);
require_field_names(fields, {"x", "y", "line"}); CHECK(ds->get_geometry_type() == r.second);
require_attributes(all_features(ds)->next(), { }
attr{"x", 0}, attr{"y", 0} } // END SECTION
, attr{"line", ustring("many\n lines\n of text\n with unix newlines")} });
}
} // END SECTION
SECTION("tabs") { SECTION("creation of CSV from in-memory strings") {
auto ds = get_csv_ds("test/data/csv/tabs_in_csv.csv"); using ustring = mapnik::value_unicode_string;
auto fields = ds->get_descriptor().get_descriptors();
require_field_names(fields, {"x", "y", "z"});
require_attributes(all_features(ds)->next(), {
attr{"x", -122}, attr{"y", 48}, attr{"z", 0} });
} // END SECTION
SECTION("separators") { for (auto const &name : {std::string("Winthrop, WA"), std::string(u8"Qu\u00e9bec")}) {
using ustring = mapnik::value_unicode_string; std::string csv_string =
(boost::format(
"wkt,Name\n"
"\"POINT (120.15 48.47)\",\"%1%\"\n"
) % name).str();
for (auto const &file : { mapnik::parameters params;
std::string("test/data/csv/pipe_delimiters.csv") params["type"] = std::string("csv");
, std::string("test/data/csv/semicolon_delimiters.csv") params["inline"] = csv_string;
}) { auto ds = mapnik::datasource_cache::instance().create(params);
auto ds = get_csv_ds(file); REQUIRE(bool(ds));
auto fields = ds->get_descriptor().get_descriptors();
require_field_names(fields, {"x", "y", "z"});
require_attributes(all_features(ds)->next(), {
attr{"x", 0}, attr{"y", 0}, attr{"z", ustring("hello")} });
}
} // END SECTION
SECTION("null and bool keywords are empty strings") { auto feature = all_features(ds)->next();
using ustring = mapnik::value_unicode_string; REQUIRE(bool(feature));
REQUIRE(feature->has_key("Name"));
CHECK(feature->get("Name") == ustring(name.c_str()));
}
} // END SECTION
auto ds = get_csv_ds("test/data/csv/nulls_and_booleans_as_strings.csv"); SECTION("geojson quoting") {
auto fields = ds->get_descriptor().get_descriptors(); using mapnik::geometry::geometry_types;
require_field_names(fields, {"x", "y", "null", "boolean"});
require_field_types(fields, {mapnik::Integer, mapnik::Integer, mapnik::String, mapnik::Boolean});
auto featureset = all_features(ds); for (auto const &file : {
require_attributes(featureset->next(), { std::string("test/data/csv/geojson_double_quote_escape.csv")
attr{"x", 0}, attr{"y", 0}, attr{"null", ustring("null")}, attr{"boolean", true}}); , std::string("test/data/csv/geojson_single_quote.csv")
require_attributes(featureset->next(), { , std::string("test/data/csv/geojson_2x_double_quote_filebakery_style.csv")
attr{"x", 0}, attr{"y", 0}, attr{"null", ustring("")}, attr{"boolean", false}}); }) {
} // END SECTION auto ds = get_csv_ds(file);
auto fields = ds->get_descriptor().get_descriptors();
require_field_names(fields, {"type"});
require_field_types(fields, {mapnik::String});
SECTION("nonexistent query fields throw") { auto featureset = all_features(ds);
auto ds = get_csv_ds("test/data/csv/lon_lat.csv"); require_geometry(featureset->next(), 1, geometry_types::Point);
auto fields = ds->get_descriptor().get_descriptors(); require_geometry(featureset->next(), 1, geometry_types::LineString);
require_field_names(fields, {"lon", "lat"}); require_geometry(featureset->next(), 1, geometry_types::Polygon);
require_field_types(fields, {mapnik::Integer, mapnik::Integer}); require_geometry(featureset->next(), 1, geometry_types::Polygon);
require_geometry(featureset->next(), 4, geometry_types::MultiPoint);
require_geometry(featureset->next(), 2, geometry_types::MultiLineString);
require_geometry(featureset->next(), 2, geometry_types::MultiPolygon);
require_geometry(featureset->next(), 2, geometry_types::MultiPolygon);
}
} // END SECTION
mapnik::query query(ds->envelope()); SECTION("blank undelimited rows are still parsed") {
for (auto const &field : fields) { using ustring = mapnik::value_unicode_string;
query.add_property_name(field.get_name());
}
// also add an invalid one, triggering throw
query.add_property_name("bogus");
REQUIRE_THROWS(ds->features(query)); // TODO: does this mean this CSV file should be in the warnings
} // END SECTION // subdirectory, since it doesn't work in strict mode?
auto ds = get_csv_ds("test/data/csv/more_headers_than_column_values.csv", false);
auto fields = ds->get_descriptor().get_descriptors();
require_field_names(fields, {"x", "y", "one", "two", "three"});
require_field_types(fields, {mapnik::Integer, mapnik::Integer, mapnik::String, mapnik::String, mapnik::String});
SECTION("leading zeros mean strings") { require_attributes(all_features(ds)->next(), {
using ustring = mapnik::value_unicode_string; attr{"x", 0}, attr{"y", 0}, attr{"one", ustring("")}, attr{"two", ustring("")}, attr{"three", ustring("")} });
} // END SECTION
auto ds = get_csv_ds("test/data/csv/leading_zeros.csv"); SECTION("fewer headers than rows throws") {
auto fields = ds->get_descriptor().get_descriptors(); REQUIRE_THROWS(get_csv_ds("test/data/csv/more_column_values_than_headers.csv"));
require_field_names(fields, {"x", "y", "fips"}); } // END SECTION
require_field_types(fields, {mapnik::Integer, mapnik::Integer, mapnik::String});
auto featureset = all_features(ds); SECTION("feature ID only incremented for valid rows") {
require_attributes(featureset->next(), { auto ds = get_csv_ds("test/data/csv/warns/feature_id_counting.csv", false);
attr{"x", 0}, attr{"y", 0}, attr{"fips", ustring("001")}}); auto fs = all_features(ds);
require_attributes(featureset->next(), {
attr{"x", 0}, attr{"y", 0}, attr{"fips", ustring("003")}});
require_attributes(featureset->next(), {
attr{"x", 0}, attr{"y", 0}, attr{"fips", ustring("005")}});
} // END SECTION
SECTION("advanced geometry detection") { // first
using row = std::pair<std::string, mapnik::datasource_geometry_t>; auto feature = fs->next();
REQUIRE(bool(feature));
CHECK(feature->id() == 1);
for (row r : { // second, should have skipped bogus one
row{"point", mapnik::datasource_geometry_t::Point} feature = fs->next();
, row{"poly", mapnik::datasource_geometry_t::Polygon} REQUIRE(bool(feature));
, row{"multi_poly", mapnik::datasource_geometry_t::Polygon} CHECK(feature->id() == 2);
, row{"line", mapnik::datasource_geometry_t::LineString}
}) {
std::string file_name = (boost::format("test/data/csv/%1%_wkt.csv") % r.first).str();
auto ds = get_csv_ds(file_name);
CHECK(ds->get_geometry_type() == r.second);
}
} // END SECTION
SECTION("creation of CSV from in-memory strings") { feature = fs->next();
using ustring = mapnik::value_unicode_string; CHECK(!feature);
} // END SECTION
for (auto const &name : {std::string("Winthrop, WA"), std::string(u8"Qu\u00e9bec")}) { SECTION("dynamically defining headers") {
std::string csv_string = using ustring = mapnik::value_unicode_string;
(boost::format( using row = std::pair<std::string, std::size_t>;
"wkt,Name\n"
"\"POINT (120.15 48.47)\",\"%1%\"\n"
) % name).str();
mapnik::parameters params; for (auto const &r : {
params["type"] = std::string("csv"); row{"test/data/csv/fails/needs_headers_two_lines.csv", 2}
params["inline"] = csv_string; , row{"test/data/csv/fails/needs_headers_one_line.csv", 1}
auto ds = mapnik::datasource_cache::instance().create(params); , row{"test/data/csv/fails/needs_headers_one_line_no_newline.csv", 1}
REQUIRE(bool(ds)); }) {
mapnik::parameters params;
params["type"] = std::string("csv");
params["file"] = r.first;
params["headers"] = "x,y,name";
auto ds = mapnik::datasource_cache::instance().create(params);
REQUIRE(bool(ds));
auto feature = all_features(ds)->next(); auto fields = ds->get_descriptor().get_descriptors();
REQUIRE(bool(feature)); require_field_names(fields, {"x", "y", "name"});
REQUIRE(feature->has_key("Name")); require_field_types(fields, {mapnik::Integer, mapnik::Integer, mapnik::String});
CHECK(feature->get("Name") == ustring(name.c_str())); require_attributes(all_features(ds)->next(), {
} attr{"x", 0}, attr{"y", 0}, attr{"name", ustring("data_name")} });
} // END SECTION REQUIRE(count_features(all_features(ds)) == r.second);
}
} // END SECTION
SECTION("geojson quoting") { #pragma GCC diagnostic push
using mapnik::geometry::geometry_types; #pragma GCC diagnostic ignored "-Wlong-long"
SECTION("64bit int fields work") {
auto ds = get_csv_ds("test/data/csv/64bit_int.csv");
auto fields = ds->get_descriptor().get_descriptors();
require_field_names(fields, {"x", "y", "bigint"});
require_field_types(fields, {mapnik::Integer, mapnik::Integer, mapnik::Integer});
for (auto const &file : { auto fs = all_features(ds);
std::string("test/data/csv/geojson_double_quote_escape.csv") auto feature = fs->next();
, std::string("test/data/csv/geojson_single_quote.csv") require_attributes(feature, {
, std::string("test/data/csv/geojson_2x_double_quote_filebakery_style.csv") attr{"x", 0}, attr{"y", 0}, attr{"bigint", 2147483648} });
}) {
auto ds = get_csv_ds(file);
auto fields = ds->get_descriptor().get_descriptors();
require_field_names(fields, {"type"});
require_field_types(fields, {mapnik::String});
auto featureset = all_features(ds); feature = fs->next();
require_geometry(featureset->next(), 1, geometry_types::Point); require_attributes(feature, {
require_geometry(featureset->next(), 1, geometry_types::LineString); attr{"x", 0}, attr{"y", 0}, attr{"bigint", 9223372036854775807ll} });
require_geometry(featureset->next(), 1, geometry_types::Polygon); require_attributes(feature, {
require_geometry(featureset->next(), 1, geometry_types::Polygon); attr{"x", 0}, attr{"y", 0}, attr{"bigint", 0x7FFFFFFFFFFFFFFFll} });
require_geometry(featureset->next(), 4, geometry_types::MultiPoint); } // END SECTION
require_geometry(featureset->next(), 2, geometry_types::MultiLineString); #pragma GCC diagnostic pop
require_geometry(featureset->next(), 2, geometry_types::MultiPolygon);
require_geometry(featureset->next(), 2, geometry_types::MultiPolygon);
}
} // END SECTION
SECTION("blank undelimited rows are still parsed") { SECTION("various number types") {
using ustring = mapnik::value_unicode_string; auto ds = get_csv_ds("test/data/csv/number_types.csv");
auto fields = ds->get_descriptor().get_descriptors();
require_field_names(fields, {"x", "y", "floats"});
require_field_types(fields, {mapnik::Integer, mapnik::Integer, mapnik::Double});
// TODO: does this mean this CSV file should be in the warnings auto fs = all_features(ds);
// subdirectory, since it doesn't work in strict mode? for (double d : { .0, +.0, 1e-06, -1e-06, 0.000001, 1.234e+16, 1.234e+16 }) {
auto ds = get_csv_ds("test/data/csv/more_headers_than_column_values.csv", false); auto feature = fs->next();
auto fields = ds->get_descriptor().get_descriptors(); REQUIRE(bool(feature));
require_field_names(fields, {"x", "y", "one", "two", "three"}); CHECK(feature->get("floats").get<mapnik::value_double>() == Approx(d));
require_field_types(fields, {mapnik::Integer, mapnik::Integer, mapnik::String, mapnik::String, mapnik::String}); }
} // END SECTION
require_attributes(all_features(ds)->next(), { SECTION("manually supplied extent") {
attr{"x", 0}, attr{"y", 0}, attr{"one", ustring("")}, attr{"two", ustring("")}, attr{"three", ustring("")} }); std::string csv_string("wkt,Name\n");
} // END SECTION mapnik::parameters params;
params["type"] = std::string("csv");
params["inline"] = csv_string;
params["extent"] = "-180,-90,180,90";
auto ds = mapnik::datasource_cache::instance().create(params);
REQUIRE(bool(ds));
SECTION("fewer headers than rows throws") { auto box = ds->envelope();
REQUIRE_THROWS(get_csv_ds("test/data/csv/more_column_values_than_headers.csv")); CHECK(box.minx() == -180);
} // END SECTION CHECK(box.miny() == -90);
CHECK(box.maxx() == 180);
CHECK(box.maxy() == 90);
} // END SECTION
SECTION("feature ID only incremented for valid rows") { SECTION("inline geojson") {
auto ds = get_csv_ds("test/data/csv/warns/feature_id_counting.csv", false); std::string csv_string = "geojson\n'{\"coordinates\":[-92.22568,38.59553],\"type\":\"Point\"}'";
auto fs = all_features(ds); mapnik::parameters params;
params["type"] = std::string("csv");
params["inline"] = csv_string;
auto ds = mapnik::datasource_cache::instance().create(params);
REQUIRE(bool(ds));
// first auto fields = ds->get_descriptor().get_descriptors();
auto feature = fs->next(); require_field_names(fields, {});
REQUIRE(bool(feature));
CHECK(feature->id() == 1);
// second, should have skipped bogus one // TODO: this originally had the following comment:
feature = fs->next(); // - re-enable after https://github.com/mapnik/mapnik/issues/2319 is fixed
REQUIRE(bool(feature)); // but that seems to have been merged and tested separately?
CHECK(feature->id() == 2); auto fs = all_features(ds);
auto feat = fs->next();
CHECK(feature_count(feat->get_geometry()) == 1);
} // END SECTION
feature = fs->next(); mapnik::logger::instance().set_severity(severity);
CHECK(!feature); }
} // END SECTION
SECTION("dynamically defining headers") {
using ustring = mapnik::value_unicode_string;
using row = std::pair<std::string, std::size_t>;
for (auto const &r : {
row{"test/data/csv/fails/needs_headers_two_lines.csv", 2}
, row{"test/data/csv/fails/needs_headers_one_line.csv", 1}
, row{"test/data/csv/fails/needs_headers_one_line_no_newline.csv", 1}
}) {
mapnik::parameters params;
params["type"] = std::string("csv");
params["file"] = r.first;
params["headers"] = "x,y,name";
auto ds = mapnik::datasource_cache::instance().create(params);
REQUIRE(bool(ds));
auto fields = ds->get_descriptor().get_descriptors();
require_field_names(fields, {"x", "y", "name"});
require_field_types(fields, {mapnik::Integer, mapnik::Integer, mapnik::String});
require_attributes(all_features(ds)->next(), {
attr{"x", 0}, attr{"y", 0}, attr{"name", ustring("data_name")} });
REQUIRE(count_features(all_features(ds)) == r.second);
}
} // END SECTION
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wlong-long"
SECTION("64bit int fields work") {
auto ds = get_csv_ds("test/data/csv/64bit_int.csv");
auto fields = ds->get_descriptor().get_descriptors();
require_field_names(fields, {"x", "y", "bigint"});
require_field_types(fields, {mapnik::Integer, mapnik::Integer, mapnik::Integer});
auto fs = all_features(ds);
auto feature = fs->next();
require_attributes(feature, {
attr{"x", 0}, attr{"y", 0}, attr{"bigint", 2147483648} });
feature = fs->next();
require_attributes(feature, {
attr{"x", 0}, attr{"y", 0}, attr{"bigint", 9223372036854775807ll} });
require_attributes(feature, {
attr{"x", 0}, attr{"y", 0}, attr{"bigint", 0x7FFFFFFFFFFFFFFFll} });
} // END SECTION
#pragma GCC diagnostic pop
SECTION("various number types") {
auto ds = get_csv_ds("test/data/csv/number_types.csv");
auto fields = ds->get_descriptor().get_descriptors();
require_field_names(fields, {"x", "y", "floats"});
require_field_types(fields, {mapnik::Integer, mapnik::Integer, mapnik::Double});
auto fs = all_features(ds);
for (double d : { .0, +.0, 1e-06, -1e-06, 0.000001, 1.234e+16, 1.234e+16 }) {
auto feature = fs->next();
REQUIRE(bool(feature));
CHECK(feature->get("floats").get<mapnik::value_double>() == Approx(d));
}
} // END SECTION
SECTION("manually supplied extent") {
std::string csv_string("wkt,Name\n");
mapnik::parameters params;
params["type"] = std::string("csv");
params["inline"] = csv_string;
params["extent"] = "-180,-90,180,90";
auto ds = mapnik::datasource_cache::instance().create(params);
REQUIRE(bool(ds));
auto box = ds->envelope();
CHECK(box.minx() == -180);
CHECK(box.miny() == -90);
CHECK(box.maxx() == 180);
CHECK(box.maxy() == 90);
} // END SECTION
SECTION("inline geojson") {
std::string csv_string = "geojson\n'{\"coordinates\":[-92.22568,38.59553],\"type\":\"Point\"}'";
mapnik::parameters params;
params["type"] = std::string("csv");
params["inline"] = csv_string;
auto ds = mapnik::datasource_cache::instance().create(params);
REQUIRE(bool(ds));
auto fields = ds->get_descriptor().get_descriptors();
require_field_names(fields, {});
// TODO: this originally had the following comment:
// - re-enable after https://github.com/mapnik/mapnik/issues/2319 is fixed
// but that seems to have been merged and tested separately?
auto fs = all_features(ds);
auto feat = fs->next();
CHECK(feature_count(feat->get_geometry()) == 1);
} // END SECTION
mapnik::logger::instance().set_severity(severity);
} // END TEST CASE } // END TEST CASE

View file

@ -4,6 +4,7 @@
#include <mapnik/color.hpp> #include <mapnik/color.hpp>
#include <mapnik/datasource.hpp> #include <mapnik/datasource.hpp>
#include <mapnik/datasource_cache.hpp> #include <mapnik/datasource_cache.hpp>
#include <mapnik/util/fs.hpp>
#include <vector> #include <vector>
#include <algorithm> #include <algorithm>
@ -19,55 +20,59 @@ SECTION("layers") {
{ {
mapnik::Map m0(100,100); mapnik::Map m0(100,100);
mapnik::Map m2(200,100); mapnik::Map m2(200,100);
mapnik::datasource_cache::instance().register_datasources("plugins/input/shape.input"); std::string shape_plugin("./plugins/input/shape.input");
mapnik::parameters p; if (mapnik::util::exists(shape_plugin))
p["type"]="shape"; {
p["file"]="demo/data/boundaries"; mapnik::datasource_cache::instance().register_datasources("plugins/input/shape.input");
p["encoding"]="latin1"; mapnik::parameters p;
auto ds0 = mapnik::datasource_cache::instance().create(p); p["type"]="shape";
p["file"]="demo/data/boundaries";
p["encoding"]="latin1";
auto ds0 = mapnik::datasource_cache::instance().create(p);
auto ds1 = ds0; // shared ptr copy auto ds1 = ds0; // shared ptr copy
REQUIRE( (ds1 == ds0) ); REQUIRE( (ds1 == ds0) );
REQUIRE( !(*ds1 != *ds0) ); REQUIRE( !(*ds1 != *ds0) );
REQUIRE( (ds1.get() == ds0.get()) ); REQUIRE( (ds1.get() == ds0.get()) );
ds1 = mapnik::datasource_cache::instance().create(p); // new with the same parameters ds1 = mapnik::datasource_cache::instance().create(p); // new with the same parameters
REQUIRE( (ds1 != ds0) ); REQUIRE( (ds1 != ds0) );
REQUIRE( (*ds1 == *ds0) ); REQUIRE( (*ds1 == *ds0) );
auto ds2 = std::move(ds1); auto ds2 = std::move(ds1);
REQUIRE( (ds2 != ds0) ); REQUIRE( (ds2 != ds0) );
REQUIRE( (*ds2 == *ds0) ); REQUIRE( (*ds2 == *ds0) );
// mapnik::layer // mapnik::layer
mapnik::layer l0("test-layer"); mapnik::layer l0("test-layer");
l0.set_datasource(ds0); l0.set_datasource(ds0);
mapnik::layer l1 = l0; // copy assignment mapnik::layer l1 = l0; // copy assignment
REQUIRE( (l1 == l0) ); REQUIRE( (l1 == l0) );
mapnik::layer l2(l0); // copy ctor mapnik::layer l2(l0); // copy ctor
REQUIRE( (l2 == l0) ); REQUIRE( (l2 == l0) );
mapnik::layer l3(mapnik::layer("test-layer")); // move ctor mapnik::layer l3(mapnik::layer("test-layer")); // move ctor
l3.set_datasource(ds2); l3.set_datasource(ds2);
REQUIRE( (l3 == l0) ); REQUIRE( (l3 == l0) );
mapnik::layer l4 = std::move(l3); mapnik::layer l4 = std::move(l3);
REQUIRE( (l4 == l0) ); // move assignment REQUIRE( (l4 == l0) ); // move assignment
m0.add_layer(l4); m0.add_layer(l4);
m0.set_background(mapnik::color("skyblue")); m0.set_background(mapnik::color("skyblue"));
m2.set_background(mapnik::color("skyblue")); m2.set_background(mapnik::color("skyblue"));
auto m1 = m0; //copy auto m1 = m0; //copy
REQUIRE( (m0 == m1) ); REQUIRE( (m0 == m1) );
REQUIRE( (m0 != m2) ); REQUIRE( (m0 != m2) );
m2 = m1; // copy m2 = m1; // copy
REQUIRE( (m2 == m1) ); REQUIRE( (m2 == m1) );
m2 = std::move(m1); m2 = std::move(m1);
REQUIRE( (m2 == m0) ); REQUIRE( (m2 == m0) );
REQUIRE( (m1 != m0) ); REQUIRE( (m1 != m0) );
REQUIRE( (m0 == m2) ); REQUIRE( (m0 == m2) );
}
} }
catch (std::exception const & ex) catch (std::exception const & ex)
{ {

View file

@ -8,6 +8,7 @@
#include <mapnik/datasource_cache.hpp> #include <mapnik/datasource_cache.hpp>
#include <mapnik/agg_renderer.hpp> #include <mapnik/agg_renderer.hpp>
#include <mapnik/expression.hpp> #include <mapnik/expression.hpp>
#include <mapnik/util/fs.hpp>
TEST_CASE("image") { TEST_CASE("image") {
@ -17,47 +18,51 @@ SECTION("painting") {
try try
{ {
datasource_cache::instance().register_datasources("plugins/input/csv.input"); std::string csv_plugin("./plugins/input/csv.input");
if (mapnik::util::exists(csv_plugin))
Map m(256, 256);
feature_type_style lines_style;
{ {
rule r; datasource_cache::instance().register_datasources(csv_plugin);
line_symbolizer line_sym;
r.append(std::move(line_sym)); Map m(256, 256);
lines_style.add_rule(std::move(r));
feature_type_style lines_style;
{
rule r;
line_symbolizer line_sym;
r.append(std::move(line_sym));
lines_style.add_rule(std::move(r));
}
m.insert_style("lines", std::move(lines_style));
feature_type_style markers_style;
{
rule r;
r.set_filter(parse_expression("False"));
markers_symbolizer mark_sym;
r.append(std::move(mark_sym));
markers_style.add_rule(std::move(r));
}
m.insert_style("markers", std::move(markers_style));
parameters p;
p["type"] = "csv";
p["separator"] = "|";
p["inline"] = "wkt\nLINESTRING(-10 0, 0 20, 10 0, 15 5)";
layer lyr("layer");
lyr.set_datasource(datasource_cache::instance().create(p));
lyr.add_style("lines");
lyr.add_style("markers");
m.add_layer(lyr);
m.zoom_all();
image_rgba8 image(m.width(), m.height());
agg_renderer<image_rgba8> ren(m, image);
ren.apply();
REQUIRE(image.painted() == true);
} }
m.insert_style("lines", std::move(lines_style));
feature_type_style markers_style;
{
rule r;
r.set_filter(parse_expression("False"));
markers_symbolizer mark_sym;
r.append(std::move(mark_sym));
markers_style.add_rule(std::move(r));
}
m.insert_style("markers", std::move(markers_style));
parameters p;
p["type"] = "csv";
p["separator"] = "|";
p["inline"] = "wkt\nLINESTRING(-10 0, 0 20, 10 0, 15 5)";
layer lyr("layer");
lyr.set_datasource(datasource_cache::instance().create(p));
lyr.add_style("lines");
lyr.add_style("markers");
m.add_layer(lyr);
m.zoom_all();
image_rgba8 image(m.width(), m.height());
agg_renderer<image_rgba8> ren(m, image);
ren.apply();
REQUIRE(image.painted() == true);
} }
catch (std::exception const & ex) catch (std::exception const & ex)
{ {

View file

@ -0,0 +1,24 @@
#include "catch.hpp"
#include <mapnik/text/icu_shaper.hpp>
#include <mapnik/text/harfbuzz_shaper.hpp>
#include <mapnik/text/font_library.hpp>
TEST_CASE("shapers compile") {
mapnik::text_line line(0,0);
mapnik::text_itemizer itemizer;
std::map<unsigned,double> width_map;
double scale_factor = 1;
mapnik::font_library fl;
mapnik::freetype_engine::font_file_mapping_type font_file_mapping;
mapnik::freetype_engine::font_memory_cache_type font_memory_cache;
mapnik::face_manager fm(fl,font_file_mapping,font_memory_cache);
mapnik::harfbuzz_shaper::shape_text(line,itemizer,
width_map,
fm,
scale_factor);
mapnik::icu_shaper::shape_text(line,itemizer,
width_map,
fm,
scale_factor);
}

View file

@ -27,14 +27,22 @@ struct fake_path
: fake_path(l.begin(), l.size()) { : fake_path(l.begin(), l.size()) {
} }
fake_path(std::vector<double> const &v) fake_path(std::vector<double> const &v, bool make_invalid = false)
: fake_path(v.begin(), v.size()) { : fake_path(v.begin(), v.size(), make_invalid) {
} }
template <typename Itr> template <typename Itr>
fake_path(Itr itr, size_t sz) { fake_path(Itr itr, size_t sz, bool make_invalid = false) {
size_t num_coords = sz >> 1; size_t num_coords = sz >> 1;
if (make_invalid)
{
num_coords++;
}
vertices_.reserve(num_coords); vertices_.reserve(num_coords);
if (make_invalid)
{
vertices_.push_back(std::make_tuple(0,0,mapnik::SEG_END));
}
for (size_t i = 0; i < num_coords; ++i) { for (size_t i = 0; i < num_coords; ++i) {
double x = *itr++; double x = *itr++;
@ -70,11 +78,37 @@ double dist(double x0, double y0, double x1, double y1)
return std::sqrt(dx*dx + dy*dy); return std::sqrt(dx*dx + dy*dy);
} }
void test_null_segment(double const &offset)
{
fake_path path = {};
mapnik::offset_converter<fake_path> off_path_new(path);
off_path_new.set_offset(offset);
double x0 = 0;
double y0 = 0;
REQUIRE(off_path_new.vertex(&x0, &y0) == mapnik::SEG_END);
REQUIRE(off_path_new.vertex(&x0, &y0) == mapnik::SEG_END);
REQUIRE(off_path_new.vertex(&x0, &y0) == mapnik::SEG_END);
}
void test_invalid_segment(double const &offset)
{
std::vector<double> v_path = {1, 1, 1, 2};
fake_path path(v_path, true);
mapnik::offset_converter<fake_path> off_path_new(path);
off_path_new.set_offset(offset);
double x0 = 0;
double y0 = 0;
REQUIRE(off_path_new.vertex(&x0, &y0) == mapnik::SEG_END);
REQUIRE(off_path_new.vertex(&x0, &y0) == mapnik::SEG_END);
REQUIRE(off_path_new.vertex(&x0, &y0) == mapnik::SEG_END);
}
void test_simple_segment(double const &offset) void test_simple_segment(double const &offset)
{ {
fake_path path = {0, 0, 1, 0}, off_path = {0, offset, 1, offset}; fake_path path = {0, 0, 1, 0}, off_path = {0, offset, 1, offset};
mapnik::offset_converter<fake_path> off_path_new(path); mapnik::offset_converter<fake_path> off_path_new(path);
off_path_new.set_offset(-offset); off_path_new.set_offset(offset);
double x0, y0, x1, y1; double x0, y0, x1, y1;
unsigned cmd0 = off_path_new.vertex(&x0, &y0); unsigned cmd0 = off_path_new.vertex(&x0, &y0);
@ -116,7 +150,7 @@ void test_straight_line(double const &offset) {
fake_path path = {0, 0, 1, 0, 9, 0, 10, 0}, fake_path path = {0, 0, 1, 0, 9, 0, 10, 0},
off_path = {0, offset, 1, offset, 9, offset, 10, offset}; off_path = {0, offset, 1, offset, 9, offset, 10, offset};
mapnik::offset_converter<fake_path> off_path_new(path); mapnik::offset_converter<fake_path> off_path_new(path);
off_path_new.set_offset(-offset); off_path_new.set_offset(offset);
double x0, y0, x1, y1; double x0, y0, x1, y1;
unsigned cmd0 = off_path_new.vertex(&x0, &y0); unsigned cmd0 = off_path_new.vertex(&x0, &y0);
@ -166,7 +200,7 @@ void test_offset_curve(double const &offset) {
fake_path path(pos), off_path(off_pos); fake_path path(pos), off_path(off_pos);
mapnik::offset_converter<fake_path> off_path_new(path); mapnik::offset_converter<fake_path> off_path_new(path);
off_path_new.set_offset(-offset); off_path_new.set_offset(offset);
double x0, y0, x1, y1; double x0, y0, x1, y1;
unsigned cmd0 = off_path_new.vertex(&x0, &y0); unsigned cmd0 = off_path_new.vertex(&x0, &y0);
@ -222,7 +256,7 @@ void test_s_shaped_curve(double const &offset) {
fake_path path(pos), off_path(off_pos); fake_path path(pos), off_path(off_pos);
mapnik::offset_converter<fake_path> off_path_new(path); mapnik::offset_converter<fake_path> off_path_new(path);
off_path_new.set_offset(-offset); off_path_new.set_offset(offset);
double x0, y0, x1, y1; double x0, y0, x1, y1;
unsigned cmd0 = off_path_new.vertex(&x0, &y0); unsigned cmd0 = off_path_new.vertex(&x0, &y0);
@ -263,6 +297,41 @@ void test_s_shaped_curve(double const &offset) {
TEST_CASE("offset converter") { TEST_CASE("offset converter") {
SECTION("null segment") {
try {
std::vector<double> offsets = { 1, -1 };
for (double offset : offsets) {
// test simple straight line segment - should be easy to
// find the correspondance here.
offset_test::test_null_segment(offset);
}
}
catch (std::exception const& ex)
{
std::cerr << ex.what() << "\n";
REQUIRE(false);
}
}
SECTION("invalid segment") {
try {
std::vector<double> offsets = { 1, -1 };
for (double offset : offsets) {
// test simple straight line segment - should be easy to
// find the correspondance here.
offset_test::test_invalid_segment(offset);
}
}
catch (std::exception const& ex)
{
std::cerr << ex.what() << "\n";
REQUIRE(false);
}
}
SECTION("simple segment") { SECTION("simple segment") {
try { try {

View file

@ -1,53 +0,0 @@
/*****************************************************************************
*
* This file is part of Mapnik (c++ mapping toolkit)
*
* Copyright (C) 2015 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
*
*****************************************************************************/
#ifndef COMPARE_IMAGES_HPP
#define COMPARE_IMAGES_HPP
// stl
#include <memory>
// mapnik
#include <mapnik/image_util.hpp>
#include <mapnik/image_reader.hpp>
namespace visual_tests
{
template <typename Image>
unsigned compare_images(Image const & actual, std::string const & reference)
{
std::unique_ptr<mapnik::image_reader> reader(mapnik::get_image_reader(reference, "png"));
if (!reader.get())
{
throw mapnik::image_reader_exception("Failed to load: " + reference);
}
mapnik::image_any ref_image_any = reader->read(0, 0, reader->width(), reader->height());
Image const & reference_image = mapnik::util::get<Image>(ref_image_any);
return mapnik::compare(actual, reference_image, 0, true);
}
}
#endif

View file

@ -26,6 +26,7 @@
// stl // stl
#include <vector> #include <vector>
#include <string> #include <string>
#include <chrono>
// boost // boost
#include <boost/filesystem.hpp> #include <boost/filesystem.hpp>
@ -35,21 +36,23 @@ namespace visual_tests
struct map_size struct map_size
{ {
map_size(int _width, int _height) : width(_width), height(_height) { } map_size(std::size_t _width, std::size_t _height) : width(_width), height(_height) { }
map_size() { } map_size() { }
unsigned width = 0; std::size_t width = 0;
unsigned height = 0; std::size_t height = 0;
}; };
struct config struct config
{ {
config() : status(true), config() : status(true),
scales({ 1.0, 2.0 }), scales({ 1.0, 2.0 }),
sizes({ { 500, 100 } }) { } sizes({ { 500, 100 } }),
tiles({ { 1, 1 } }) { }
bool status; bool status;
std::vector<double> scales; std::vector<double> scales;
std::vector<map_size> sizes; std::vector<map_size> sizes;
std::vector<map_size> tiles;
}; };
enum result_state : std::uint8_t enum result_state : std::uint8_t
@ -66,11 +69,13 @@ struct result
result_state state; result_state state;
std::string renderer_name; std::string renderer_name;
map_size size; map_size size;
map_size tiles;
double scale_factor; double scale_factor;
boost::filesystem::path actual_image_path; boost::filesystem::path actual_image_path;
boost::filesystem::path reference_image_path; boost::filesystem::path reference_image_path;
std::string error_message; std::string error_message;
unsigned diff; unsigned diff;
std::chrono::high_resolution_clock::duration duration;
}; };
using result_list = std::vector<result>; using result_list = std::vector<result>;

View file

@ -23,15 +23,16 @@
#ifndef RENDERER_HPP #ifndef RENDERER_HPP
#define RENDERER_HPP #define RENDERER_HPP
#include "compare_images.hpp"
// stl // stl
#include <sstream> #include <sstream>
#include <iomanip> #include <iomanip>
#include <fstream> #include <fstream>
#include <memory>
// mapnik // mapnik
#include <mapnik/map.hpp> #include <mapnik/map.hpp>
#include <mapnik/image_util.hpp>
#include <mapnik/image_reader.hpp>
#include <mapnik/agg_renderer.hpp> #include <mapnik/agg_renderer.hpp>
#if defined(GRID_RENDERER) #if defined(GRID_RENDERER)
#include <mapnik/grid/grid_renderer.hpp> #include <mapnik/grid/grid_renderer.hpp>
@ -56,10 +57,20 @@ struct renderer_base
using image_type = ImageType; using image_type = ImageType;
static constexpr const char * ext = ".png"; static constexpr const char * ext = ".png";
static constexpr const bool support_tiles = true;
unsigned compare(image_type const & actual, boost::filesystem::path const& reference) const unsigned compare(image_type const & actual, boost::filesystem::path const& reference) const
{ {
return compare_images(actual, reference.string()); std::unique_ptr<mapnik::image_reader> reader(mapnik::get_image_reader(reference.string(), "png"));
if (!reader.get())
{
throw mapnik::image_reader_exception("Failed to load: " + reference.string());
}
mapnik::image_any ref_image_any = reader->read(0, 0, reader->width(), reader->height());
ImageType const & reference_image = mapnik::util::get<ImageType>(ref_image_any);
return mapnik::compare(actual, reference_image, 0, true);
} }
void save(image_type const & image, boost::filesystem::path const& path) const void save(image_type const & image, boost::filesystem::path const& path) const
@ -106,6 +117,7 @@ struct svg_renderer : renderer_base<std::string>
{ {
static constexpr const char * name = "svg"; static constexpr const char * name = "svg";
static constexpr const char * ext = ".svg"; static constexpr const char * ext = ".svg";
static constexpr const bool support_tiles = false;
image_type render(mapnik::Map const & map, double scale_factor) const image_type render(mapnik::Map const & map, double scale_factor) const
{ {
@ -125,7 +137,7 @@ struct svg_renderer : renderer_base<std::string>
} }
std::string expected(std::istreambuf_iterator<char>(stream.rdbuf()),(std::istreambuf_iterator<char>())); std::string expected(std::istreambuf_iterator<char>(stream.rdbuf()),(std::istreambuf_iterator<char>()));
stream.close(); stream.close();
return std::fabs(actual.size() - expected.size()); return std::max(actual.size(), expected.size()) - std::min(actual.size(), expected.size());
} }
void save(image_type const & image, boost::filesystem::path const& path) const void save(image_type const & image, boost::filesystem::path const& path) const
@ -191,19 +203,76 @@ struct grid_renderer : renderer_base<mapnik::image_rgba8>
}; };
#endif #endif
template <typename T>
void set_rectangle(T const & src, T & dst, std::size_t x, std::size_t y)
{
mapnik::box2d<int> ext0(0, 0, dst.width(), dst.height());
mapnik::box2d<int> ext1(x, y, x + src.width(), y + src.height());
if (ext0.intersects(ext1))
{
mapnik::box2d<int> box = ext0.intersect(ext1);
for (std::size_t pix_y = box.miny(); pix_y < static_cast<std::size_t>(box.maxy()); ++pix_y)
{
typename T::pixel_type * row_to = dst.get_row(pix_y);
typename T::pixel_type const * row_from = src.get_row(pix_y - y);
for (std::size_t pix_x = box.minx(); pix_x < static_cast<std::size_t>(box.maxx()); ++pix_x)
{
row_to[pix_x] = row_from[pix_x - x];
}
}
}
}
template <typename Renderer> template <typename Renderer>
class renderer class renderer
{ {
public: public:
using renderer_type = Renderer;
using image_type = typename Renderer::image_type;
renderer(boost::filesystem::path const & _output_dir, boost::filesystem::path const & _reference_dir, bool _overwrite) renderer(boost::filesystem::path const & _output_dir, boost::filesystem::path const & _reference_dir, bool _overwrite)
: ren(), output_dir(_output_dir), reference_dir(_reference_dir), overwrite(_overwrite) : ren(), output_dir(_output_dir), reference_dir(_reference_dir), overwrite(_overwrite)
{ {
} }
result test(std::string const & name, mapnik::Map const & map, double scale_factor) const image_type render(mapnik::Map const & map, double scale_factor) const
{ {
typename Renderer::image_type image(ren.render(map, scale_factor)); return ren.render(map, scale_factor);
boost::filesystem::path reference = reference_dir / image_file_name(name, map.width(), map.height(), scale_factor, true, Renderer::ext); }
image_type render(mapnik::Map & map, double scale_factor, map_size const & tiles) const
{
mapnik::box2d<double> box = map.get_current_extent();
image_type image(map.width(), map.height());
map.resize(image.width() / tiles.width, image.height() / tiles.height);
double tile_box_width = box.width() / tiles.width;
double tile_box_height = box.height() / tiles.height;
for (std::size_t tile_y = 0; tile_y < tiles.height; tile_y++)
{
for (std::size_t tile_x = 0; tile_x < tiles.width; tile_x++)
{
mapnik::box2d<double> tile_box(
box.minx() + tile_x * tile_box_width,
box.miny() + tile_y * tile_box_height,
box.minx() + (tile_x + 1) * tile_box_width,
box.miny() + (tile_y + 1) * tile_box_height);
map.zoom_to_box(tile_box);
image_type tile(ren.render(map, scale_factor));
set_rectangle(tile, image, tile_x * tile.width(), (tiles.height - 1 - tile_y) * tile.height());
}
}
return image;
}
result report(image_type const & image,
std::string const & name,
map_size const & size,
map_size const & tiles,
double scale_factor) const
{
boost::filesystem::path reference = reference_dir / image_file_name(name, size, tiles, scale_factor, true);
bool reference_exists = boost::filesystem::exists(reference); bool reference_exists = boost::filesystem::exists(reference);
result res; result res;
@ -211,14 +280,15 @@ public:
res.name = name; res.name = name;
res.renderer_name = Renderer::name; res.renderer_name = Renderer::name;
res.scale_factor = scale_factor; res.scale_factor = scale_factor;
res.size = map_size(map.width(), map.height()); res.size = size;
res.tiles = tiles;
res.reference_image_path = reference; res.reference_image_path = reference;
res.diff = reference_exists ? ren.compare(image, reference) : 0; res.diff = reference_exists ? ren.compare(image, reference) : 0;
if (res.diff) if (res.diff)
{ {
boost::filesystem::create_directories(output_dir); boost::filesystem::create_directories(output_dir);
boost::filesystem::path path = output_dir / image_file_name(name, map.width(), map.height(), scale_factor, false, Renderer::ext); boost::filesystem::path path = output_dir / image_file_name(name, size, tiles, scale_factor, false);
res.actual_image_path = path; res.actual_image_path = path;
res.state = STATE_FAIL; res.state = STATE_FAIL;
ren.save(image, path); ren.save(image, path);
@ -235,16 +305,23 @@ public:
private: private:
std::string image_file_name(std::string const & test_name, std::string image_file_name(std::string const & test_name,
double width, map_size const & size,
double height, map_size const & tiles,
double scale_factor, double scale_factor,
bool reference, bool reference) const
std::string const& ext) const
{ {
std::stringstream s; std::stringstream s;
s << test_name << '-' << width << '-' << height << '-' s << test_name << '-' << size.width << '-' << size.height << '-';
<< std::fixed << std::setprecision(1) << scale_factor if (tiles.width > 1 || tiles.height > 1)
<< '-' << Renderer::name << (reference ? "-reference" : "") << ext; {
s << tiles.width << 'x' << tiles.height << '-';
}
s << std::fixed << std::setprecision(1) << scale_factor << '-' << Renderer::name;
if (reference)
{
s << "-reference";
}
s << Renderer::ext;
return s.str(); return s.str();
} }

View file

@ -24,6 +24,7 @@
#include <iomanip> #include <iomanip>
#include <fstream> #include <fstream>
#include <numeric> #include <numeric>
#include <map>
#include "report.hpp" #include "report.hpp"
@ -32,8 +33,12 @@ namespace visual_tests
void console_report::report(result const & r) void console_report::report(result const & r)
{ {
s << '"' << r.name << '-' << r.size.width << '-' << r.size.height << '-' << std::fixed << s << '"' << r.name << '-' << r.size.width << '-' << r.size.height;
std::setprecision(1) << r.scale_factor << "\" with " << r.renderer_name << "... "; if (r.tiles.width > 1 || r.tiles.height > 1)
{
s << '-' << r.tiles.width << 'x' << r.tiles.height;
}
s << '-' << std::fixed << std::setprecision(1) << r.scale_factor << "\" with " << r.renderer_name << "... ";
switch (r.state) switch (r.state)
{ {
@ -51,6 +56,11 @@ void console_report::report(result const & r)
break; break;
} }
if (show_duration)
{
s << " (" << std::chrono::duration_cast<std::chrono::milliseconds>(r.duration).count() << " milliseconds)";
}
s << std::endl; s << std::endl;
} }
@ -61,6 +71,10 @@ unsigned console_report::summary(result_list const & results)
unsigned fail = 0; unsigned fail = 0;
unsigned overwrite = 0; unsigned overwrite = 0;
using namespace std::chrono;
using duration_map_type = std::map<std::string, high_resolution_clock::duration>;
duration_map_type durations;
for (auto const & r : results) for (auto const & r : results)
{ {
switch (r.state) switch (r.state)
@ -70,12 +84,37 @@ unsigned console_report::summary(result_list const & results)
case STATE_ERROR: error++; break; case STATE_ERROR: error++; break;
case STATE_OVERWRITE: overwrite++; break; case STATE_OVERWRITE: overwrite++; break;
} }
if (show_duration)
{
duration_map_type::iterator duration = durations.find(r.renderer_name);
if (duration == durations.end())
{
durations.emplace(r.renderer_name, r.duration);
}
else
{
duration->second += r.duration;
}
}
} }
s << std::endl; s << std::endl;
s << "Visual rendering: " << fail << " failed / " << ok << " passed / " s << "Visual rendering: " << fail << " failed / " << ok << " passed / "
<< overwrite << " overwritten / " << error << " errors" << std::endl; << overwrite << " overwritten / " << error << " errors" << std::endl;
if (show_duration)
{
high_resolution_clock::duration total(0);
for (auto const & duration : durations)
{
s << duration.first << ": \t" << duration_cast<milliseconds>(duration.second).count()
<< " milliseconds" << std::endl;
total += duration.second;
}
s << "total: \t" << duration_cast<milliseconds>(total).count() << " milliseconds" << std::endl;
}
return fail + error; return fail + error;
} }

View file

@ -36,7 +36,7 @@ namespace visual_tests
class console_report class console_report
{ {
public: public:
console_report() : s(std::clog) console_report(bool _show_duration) : s(std::clog), show_duration(_show_duration)
{ {
} }
@ -49,12 +49,13 @@ public:
protected: protected:
std::ostream & s; std::ostream & s;
bool show_duration;
}; };
class console_short_report : public console_report class console_short_report : public console_report
{ {
public: public:
console_short_report() : console_report() console_short_report(bool _show_duration) : console_report(_show_duration)
{ {
} }

View file

@ -31,6 +31,18 @@
#include "cleanup.hpp" // run_cleanup() #include "cleanup.hpp" // run_cleanup()
#ifdef MAPNIK_LOG
using log_levels_map = std::map<std::string, mapnik::logger::severity_type>;
log_levels_map log_levels
{
{ "debug", mapnik::logger::severity_type::debug },
{ "warn", mapnik::logger::severity_type::warn },
{ "error", mapnik::logger::severity_type::error },
{ "none", mapnik::logger::severity_type::none }
};
#endif
int main(int argc, char** argv) int main(int argc, char** argv)
{ {
using namespace visual_tests; using namespace visual_tests;
@ -41,6 +53,8 @@ int main(int argc, char** argv)
("help,h", "produce usage message") ("help,h", "produce usage message")
("verbose,v", "verbose output") ("verbose,v", "verbose output")
("overwrite,o", "overwrite reference image") ("overwrite,o", "overwrite reference image")
("duration,d", "output rendering duration")
("iterations,i", po::value<std::size_t>()->default_value(1), "number of iterations for benchmarking")
("jobs,j", po::value<std::size_t>()->default_value(1), "number of parallel threads") ("jobs,j", po::value<std::size_t>()->default_value(1), "number of parallel threads")
("styles-dir", po::value<std::string>()->default_value("test/data-visual/styles"), "directory with styles") ("styles-dir", po::value<std::string>()->default_value("test/data-visual/styles"), "directory with styles")
("images-dir", po::value<std::string>()->default_value("test/data-visual/images"), "directory with reference images") ("images-dir", po::value<std::string>()->default_value("test/data-visual/images"), "directory with reference images")
@ -49,6 +63,11 @@ int main(int argc, char** argv)
("styles", po::value<std::vector<std::string>>(), "selected styles to test") ("styles", po::value<std::vector<std::string>>(), "selected styles to test")
("fonts", po::value<std::string>()->default_value("fonts"), "font search path") ("fonts", po::value<std::string>()->default_value("fonts"), "font search path")
("plugins", po::value<std::string>()->default_value("plugins/input"), "input plugins search path") ("plugins", po::value<std::string>()->default_value("plugins/input"), "input plugins search path")
#ifdef MAPNIK_LOG
("log", po::value<std::string>()->default_value(std::find_if(log_levels.begin(), log_levels.end(),
[](log_levels_map::value_type const & level) { return level.second == mapnik::logger::get_severity(); } )->first),
"log level (debug, warn, error, none)")
#endif
; ;
po::positional_options_description p; po::positional_options_description p;
@ -63,6 +82,20 @@ int main(int argc, char** argv)
return 1; return 1;
} }
#ifdef MAPNIK_LOG
std::string log_level(vm["log"].as<std::string>());
log_levels_map::const_iterator level_iter = log_levels.find(log_level);
if (level_iter == log_levels.end())
{
std::cerr << "Error: Unknown log level: " << log_level << std::endl;
return 1;
}
else
{
mapnik::logger::set_severity(level_iter->second);
}
#endif
mapnik::freetype_engine::register_fonts(vm["fonts"].as<std::string>(), true); mapnik::freetype_engine::register_fonts(vm["fonts"].as<std::string>(), true);
mapnik::datasource_cache::instance().register_datasources(vm["plugins"].as<std::string>()); mapnik::datasource_cache::instance().register_datasources(vm["plugins"].as<std::string>());
@ -77,8 +110,10 @@ int main(int argc, char** argv)
output_dir, output_dir,
vm["images-dir"].as<std::string>(), vm["images-dir"].as<std::string>(),
vm.count("overwrite"), vm.count("overwrite"),
vm["iterations"].as<std::size_t>(),
vm["jobs"].as<std::size_t>()); vm["jobs"].as<std::size_t>());
report_type report = vm.count("verbose") ? report_type((console_report())) : report_type((console_short_report())); bool show_duration = vm.count("duration");
report_type report(vm.count("verbose") ? report_type((console_report(show_duration))) : report_type((console_short_report(show_duration))));
result_list results; result_list results;
try try

View file

@ -34,32 +34,97 @@ namespace visual_tests
class renderer_visitor class renderer_visitor
{ {
public: public:
renderer_visitor(std::string const & name, mapnik::Map const & map, double scale_factor) renderer_visitor(std::string const & name,
: name_(name), map_(map), scale_factor_(scale_factor) mapnik::Map & map,
map_size const & tiles,
double scale_factor,
result_list & results,
report_type & report,
std::size_t iterations)
: name_(name),
map_(map),
tiles_(tiles),
scale_factor_(scale_factor),
results_(results),
report_(report),
iterations_(iterations)
{ {
} }
template <typename T> template <typename T, typename std::enable_if<T::renderer_type::support_tiles>::type* = nullptr>
result operator()(T const & renderer) const void operator()(T const & renderer)
{ {
return renderer.test(name_, map_, scale_factor_); test(renderer);
}
template <typename T, typename std::enable_if<!T::renderer_type::support_tiles>::type* = nullptr>
void operator()(T const & renderer)
{
if (tiles_.width == 1 && tiles_.height == 1)
{
test(renderer);
}
} }
private: private:
template <typename T>
void test(T const & renderer)
{
map_size size { map_.width(), map_.height() };
std::chrono::high_resolution_clock::time_point start(std::chrono::high_resolution_clock::now());
for (std::size_t i = iterations_ ; i > 0; i--)
{
typename T::image_type image(render(renderer));
if (i == 1)
{
std::chrono::high_resolution_clock::time_point end(std::chrono::high_resolution_clock::now());
result r(renderer.report(image, name_, size, tiles_, scale_factor_));
r.duration = end - start;
mapnik::util::apply_visitor(report_visitor(r), report_);
results_.push_back(std::move(r));
}
}
}
template <typename T, typename std::enable_if<T::renderer_type::support_tiles>::type* = nullptr>
typename T::image_type render(T const & renderer)
{
if (tiles_.width == 1 && tiles_.height == 1)
{
return renderer.render(map_, scale_factor_);
}
else
{
return renderer.render(map_, scale_factor_, tiles_);
}
}
template <typename T, typename std::enable_if<!T::renderer_type::support_tiles>::type* = nullptr>
typename T::image_type render(T const & renderer)
{
return renderer.render(map_, scale_factor_);
}
std::string const & name_; std::string const & name_;
mapnik::Map const & map_; mapnik::Map & map_;
map_size const & tiles_;
double scale_factor_; double scale_factor_;
result_list & results_;
report_type & report_;
std::size_t iterations_;
}; };
runner::runner(runner::path_type const & styles_dir, runner::runner(runner::path_type const & styles_dir,
runner::path_type const & output_dir, runner::path_type const & output_dir,
runner::path_type const & reference_dir, runner::path_type const & reference_dir,
bool overwrite, bool overwrite,
std::size_t iterations,
std::size_t jobs) std::size_t jobs)
: styles_dir_(styles_dir), : styles_dir_(styles_dir),
output_dir_(output_dir), output_dir_(output_dir),
reference_dir_(reference_dir), reference_dir_(reference_dir),
jobs_(jobs), jobs_(jobs),
iterations_(iterations),
renderers_{ renderer<agg_renderer>(output_dir_, reference_dir_, overwrite) renderers_{ renderer<agg_renderer>(output_dir_, reference_dir_, overwrite)
#if defined(HAVE_CAIRO) #if defined(HAVE_CAIRO)
,renderer<cairo_renderer>(output_dir_, reference_dir_, overwrite) ,renderer<cairo_renderer>(output_dir_, reference_dir_, overwrite)
@ -173,12 +238,12 @@ result_list runner::test_range(files_iterator begin, files_iterator end, std::re
result_list runner::test_one(runner::path_type const& style_path, config cfg, report_type & report) const result_list runner::test_one(runner::path_type const& style_path, config cfg, report_type & report) const
{ {
mapnik::Map m(cfg.sizes.front().width, cfg.sizes.front().height); mapnik::Map map(cfg.sizes.front().width, cfg.sizes.front().height);
result_list results; result_list results;
try try
{ {
mapnik::load_map(m, style_path.string(), true); mapnik::load_map(map, style_path.string(), true);
} }
catch (std::exception const& ex) catch (std::exception const& ex)
{ {
@ -191,7 +256,7 @@ result_list runner::test_one(runner::path_type const& style_path, config cfg, re
throw; throw;
} }
mapnik::parameters const & params = m.get_extra_parameters(); mapnik::parameters const & params = map.get_extra_parameters();
boost::optional<mapnik::value_integer> status = params.get<mapnik::value_integer>("status", cfg.status); boost::optional<mapnik::value_integer> status = params.get<mapnik::value_integer>("status", cfg.status);
@ -208,32 +273,52 @@ result_list runner::test_one(runner::path_type const& style_path, config cfg, re
parse_map_sizes(*sizes, cfg.sizes); parse_map_sizes(*sizes, cfg.sizes);
} }
boost::optional<std::string> tiles = params.get<std::string>("tiles");
if (tiles)
{
cfg.tiles.clear();
parse_map_sizes(*tiles, cfg.tiles);
}
boost::optional<std::string> bbox_string = params.get<std::string>("bbox");
mapnik::box2d<double> box;
if (bbox_string)
{
box.from_string(*bbox_string);
}
std::string name(style_path.stem().string()); std::string name(style_path.stem().string());
for (map_size const & size : cfg.sizes) for (auto const & size : cfg.sizes)
{ {
m.resize(size.width, size.height); for (auto const & scale_factor : cfg.scales)
boost::optional<std::string> bbox_string = params.get<std::string>("bbox");
if (bbox_string)
{ {
mapnik::box2d<double> bbox; for (auto const & tiles_count : cfg.tiles)
bbox.from_string(*bbox_string);
m.zoom_to_box(bbox);
}
else
{
m.zoom_all();
}
for (double const & scale_factor : cfg.scales)
{
for(auto const& ren : renderers_)
{ {
result r = mapnik::util::apply_visitor(renderer_visitor(name, m, scale_factor), ren); if (!tiles_count.width || !tiles_count.height)
results.emplace_back(r); {
mapnik::util::apply_visitor(report_visitor(r), report); throw std::runtime_error("Cannot render zero tiles.");
}
if (size.width % tiles_count.width || size.height % tiles_count.height)
{
throw std::runtime_error("Tile size is not an integer.");
}
for (auto const & ren : renderers_)
{
map.resize(size.width, size.height);
if (box.valid())
{
map.zoom_to_box(box);
}
else
{
map.zoom_all();
}
mapnik::util::apply_visitor(renderer_visitor(name, map, tiles_count, scale_factor, results, report, iterations_), ren);
}
} }
} }
} }

View file

@ -54,6 +54,7 @@ public:
path_type const & output_dir, path_type const & output_dir,
path_type const & reference_dir, path_type const & reference_dir,
bool overwrite, bool overwrite,
std::size_t iterations,
std::size_t jobs); std::size_t jobs);
result_list test_all(report_type & report) const; result_list test_all(report_type & report) const;
@ -70,6 +71,7 @@ private:
const path_type output_dir_; const path_type output_dir_;
const path_type reference_dir_; const path_type reference_dir_;
const std::size_t jobs_; const std::size_t jobs_;
const std::size_t iterations_;
const renderer_type renderers_[boost::mpl::size<renderer_type::types>::value]; const renderer_type renderers_[boost::mpl::size<renderer_type::types>::value];
}; };