pre-eliminary GlyphSymbolizer implementation. TODO: XML de/serializing. more tests. remove workaround mentioned in test
This commit is contained in:
parent
31c3d20e43
commit
d098c98c5e
8 changed files with 283 additions and 66 deletions
|
@ -1,11 +1,14 @@
|
|||
#ifndef ARROW_SYMBOLIZER_HPP
|
||||
#define ARROW_SYMBOLIZER_HPP
|
||||
#ifndef GLYPH_SYMBOLIZER_HPP
|
||||
#define GLYPH_SYMBOLIZER_HPP
|
||||
|
||||
// mapnik
|
||||
#include <mapnik/raster_colorizer.hpp>
|
||||
#include <mapnik/expression_node.hpp>
|
||||
#include <mapnik/text_path.hpp>
|
||||
#include <mapnik/font_engine_freetype.hpp>
|
||||
#include <mapnik/color.hpp>
|
||||
#include <mapnik/feature.hpp>
|
||||
#include <mapnik/unicode.hpp>
|
||||
|
||||
// boost
|
||||
#include <boost/tuple/tuple.hpp>
|
||||
|
@ -143,6 +146,10 @@ struct MAPNIK_DECL glyph_symbolizer
|
|||
|
||||
text_path_ptr get_text_path(face_set_ptr const& faces,
|
||||
Feature const& feature) const;
|
||||
UnicodeString eval_char(Feature const& feature) const;
|
||||
double eval_angle(Feature const& feature) const;
|
||||
unsigned eval_size(Feature const& feature) const;
|
||||
color eval_color(Feature const& feature) const;
|
||||
|
||||
|
||||
private:
|
||||
|
|
|
@ -31,6 +31,7 @@
|
|||
#include <boost/variant.hpp>
|
||||
#include <boost/scoped_array.hpp>
|
||||
#include <boost/concept_check.hpp>
|
||||
#include <boost/lexical_cast.hpp>
|
||||
// stl
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
@ -627,6 +628,11 @@ struct to_expression_string : public boost::static_visitor<std::string>
|
|||
|
||||
struct to_double : public boost::static_visitor<double>
|
||||
{
|
||||
double operator() (int val) const
|
||||
{
|
||||
return static_cast<double>(val);
|
||||
}
|
||||
|
||||
double operator() (double val) const
|
||||
{
|
||||
return val;
|
||||
|
@ -634,19 +640,13 @@ struct to_double : public boost::static_visitor<double>
|
|||
|
||||
double operator() (std::string const& val) const
|
||||
{
|
||||
std::istringstream stream(val);
|
||||
double t;
|
||||
stream >> t;
|
||||
return t;
|
||||
return boost::lexical_cast<double>(val);
|
||||
}
|
||||
double operator() (UnicodeString const& val) const
|
||||
{
|
||||
std::string utf8;
|
||||
to_utf8(val,utf8);
|
||||
std::istringstream stream(utf8);
|
||||
double t;
|
||||
stream >> t;
|
||||
return t;
|
||||
return boost::lexical_cast<double>(utf8);
|
||||
}
|
||||
|
||||
double operator() (value_null const& val) const
|
||||
|
@ -655,6 +655,37 @@ struct to_double : public boost::static_visitor<double>
|
|||
return 0.0;
|
||||
}
|
||||
};
|
||||
|
||||
struct to_int : public boost::static_visitor<double>
|
||||
{
|
||||
int operator() (int val) const
|
||||
{
|
||||
return val;
|
||||
}
|
||||
|
||||
int operator() (double val) const
|
||||
{
|
||||
return rint(val);
|
||||
}
|
||||
|
||||
int operator() (std::string const& val) const
|
||||
{
|
||||
return boost::lexical_cast<int>(val);
|
||||
}
|
||||
int operator() (UnicodeString const& val) const
|
||||
{
|
||||
std::string utf8;
|
||||
to_utf8(val,utf8);
|
||||
return boost::lexical_cast<int>(utf8);
|
||||
}
|
||||
|
||||
int operator() (value_null const& val) const
|
||||
{
|
||||
boost::ignore_unused_variable_warning(val);
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
class value
|
||||
|
@ -733,6 +764,11 @@ public:
|
|||
return boost::apply_visitor(impl::to_double(),base_);
|
||||
}
|
||||
|
||||
double to_int() const
|
||||
{
|
||||
return boost::apply_visitor(impl::to_int(),base_);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
inline const value operator+(value const& p1,value const& p2)
|
||||
|
|
|
@ -951,6 +951,59 @@ void agg_renderer<T>::process(glyph_symbolizer const& sym,
|
|||
Feature const& feature,
|
||||
proj_transform const& prj_trans)
|
||||
{
|
||||
typedef coord_transform2<CoordTransform,geometry2d> path_type;
|
||||
|
||||
face_set_ptr faces = font_manager_.get_face_set(sym.get_face_name());
|
||||
if (faces->size() > 0)
|
||||
{
|
||||
// Get x and y from geometry and translate to pixmap coords.
|
||||
double x, y, z=0.0;
|
||||
feature.get_geometry(0).label_position(&x, &y);
|
||||
prj_trans.backward(x,y,z);
|
||||
t_.forward(&x, &y);
|
||||
|
||||
// configure text renderer
|
||||
//
|
||||
text_renderer<T> ren(pixmap_, faces);
|
||||
ren.set_pixel_size(sym.eval_size(feature));
|
||||
color fill = sym.eval_color(feature);
|
||||
ren.set_fill(fill);
|
||||
if (fill != color("transparent")) {
|
||||
ren.set_halo_fill(sym.get_halo_fill());
|
||||
ren.set_halo_radius(sym.get_halo_radius());
|
||||
}
|
||||
|
||||
// Get and render text path
|
||||
//
|
||||
text_path_ptr path = sym.get_text_path(faces, feature);
|
||||
// apply displacement
|
||||
position pos = sym.get_displacement();
|
||||
double dx = boost::get<0>(pos);
|
||||
double dy = boost::get<1>(pos);
|
||||
path->starting_x = x = x+dx;
|
||||
path->starting_y = y = y+dy;
|
||||
|
||||
// Prepare glyphs to set internal state and calculate the marker's
|
||||
// final box so we can check for a valid placement
|
||||
box2d<double> dim = ren.prepare_glyphs(path.get());
|
||||
double bsize = (dim.width()>dim.height()?dim.width():dim.height())/2;
|
||||
box2d<double> ext(
|
||||
floor(x-bsize), floor(y-bsize), ceil(x+bsize), ceil(y+bsize)
|
||||
);
|
||||
if ((sym.get_allow_overlap() || detector_.has_placement(ext)) &&
|
||||
(!sym.get_avoid_edges() || detector_.extent().contains(ext)))
|
||||
{
|
||||
// Placement is valid, render glyph and update detector.
|
||||
ren.render(x, y);
|
||||
detector_.insert(ext);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw config_error(
|
||||
"Unable to find specified font face in GlyphSymbolizer"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
template class agg_renderer<image_32>;
|
||||
|
|
|
@ -1,15 +1,110 @@
|
|||
#include <mapnik/glyph_symbolizer.hpp>
|
||||
#include <mapnik/value.hpp>
|
||||
|
||||
#include <boost/lexical_cast.hpp>
|
||||
|
||||
#include <mapnik/expression_evaluator.hpp>
|
||||
|
||||
namespace mapnik
|
||||
{
|
||||
text_path_ptr glyph_symbolizer::get_text_path(face_set_ptr const& faces,
|
||||
Feature const& feature) const
|
||||
|
||||
text_path_ptr glyph_symbolizer::get_text_path(face_set_ptr const& faces,
|
||||
Feature const& feature) const
|
||||
{
|
||||
// Try to evaulate expressions against feature
|
||||
UnicodeString char_ = eval_char(feature);
|
||||
double angle = eval_angle(feature);
|
||||
|
||||
// calculate displacement so glyph is rotated along center (default is
|
||||
// lowerbottom corner)
|
||||
string_info info(char_);
|
||||
faces->get_string_info(info);
|
||||
if (info.num_characters() != 1)
|
||||
{
|
||||
text_path_ptr path_ptr = text_path_ptr(new text_path());
|
||||
return path_ptr;
|
||||
throw config_error("'char' length must be exactly 1");
|
||||
}
|
||||
|
||||
character_info ci = info.at(0);
|
||||
std::pair<unsigned,unsigned> cdim = faces->character_dimensions(ci.character);
|
||||
double cwidth = (static_cast<double>(cdim.first))/2.0;
|
||||
double cheight = (static_cast<double>(cdim.second))/2.0;
|
||||
double xoff = cwidth*cos(angle) - cheight*sin(angle);
|
||||
double yoff = cwidth*sin(angle) + cheight*cos(angle);
|
||||
|
||||
// Create text path and add character with displacement and angle
|
||||
text_path_ptr path_ptr = text_path_ptr(new text_path());
|
||||
path_ptr->add_node(ci.character, -xoff, -yoff, angle);
|
||||
return path_ptr;
|
||||
}
|
||||
|
||||
|
||||
|
||||
UnicodeString glyph_symbolizer::eval_char(Feature const& feature) const
|
||||
{
|
||||
expression_ptr expr = get_char();
|
||||
if (!expr) throw config_error("No 'char' expression");
|
||||
value_type result = boost::apply_visitor(evaluate<Feature,value_type>(feature),*expr);
|
||||
return result.to_unicode();
|
||||
}
|
||||
|
||||
double glyph_symbolizer::eval_angle(Feature const& feature) const
|
||||
{
|
||||
double angle = 0.0;
|
||||
expression_ptr expr = get_angle();
|
||||
if (expr) {
|
||||
value_type result = boost::apply_visitor(
|
||||
evaluate<Feature,value_type>(feature),
|
||||
*expr
|
||||
);
|
||||
angle = result.to_double();
|
||||
// normalize to first rotation in case an expression has made it go past
|
||||
angle = std::fmod(angle, 360);
|
||||
angle *= (M_PI/180); // convert to radians
|
||||
if (true) { //TODO: if (get_mode()==AZIMUTH)
|
||||
// angle is an azimuth, convert into trigonometric angle
|
||||
angle = std::atan2(std::cos(angle), std::sin(angle));
|
||||
}
|
||||
if (angle<0)
|
||||
angle += 2*M_PI;
|
||||
|
||||
}
|
||||
return angle;
|
||||
}
|
||||
|
||||
unsigned glyph_symbolizer::eval_size(Feature const& feature) const
|
||||
{
|
||||
expression_ptr expr = get_size();
|
||||
if (!expr) throw config_error("No 'size' expression");
|
||||
value_type result = boost::apply_visitor(
|
||||
evaluate<Feature,value_type>(feature),
|
||||
*expr
|
||||
);
|
||||
return static_cast<unsigned>(result.to_int());
|
||||
}
|
||||
|
||||
color glyph_symbolizer::eval_color(Feature const& feature) const
|
||||
{
|
||||
raster_colorizer_ptr colorizer = get_colorizer();
|
||||
if (colorizer) {
|
||||
expression_ptr value_expr = get_value();
|
||||
if (!value_expr) {
|
||||
throw config_error(
|
||||
"Must define a 'value' expression to use a colorizer"
|
||||
);
|
||||
}
|
||||
value_type value_result = boost::apply_visitor(
|
||||
evaluate<Feature,value_type>(feature),
|
||||
*value_expr
|
||||
);
|
||||
return colorizer->get_color((float)value_result.to_double());
|
||||
} else {
|
||||
expression_ptr color_expr = get_color();
|
||||
if (color_expr) {
|
||||
value_type color_result = boost::apply_visitor(
|
||||
evaluate<Feature,value_type>(feature),
|
||||
*color_expr
|
||||
);
|
||||
return color(color_result.to_string());
|
||||
} else {
|
||||
return color("black");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // end mapnik namespace
|
||||
|
|
|
@ -1,57 +1,24 @@
|
|||
#encoding: utf8
|
||||
#!/usr/bin/env python
|
||||
|
||||
from nose.tools import *
|
||||
from utilities import execution_path, save_data, Todo
|
||||
from utilities import execution_path, save_data, Todo, contains_word
|
||||
|
||||
import os, mapnik2
|
||||
|
||||
def setup():
|
||||
# All of the paths used are relative, if we run the tests
|
||||
# from another directory we need to chdir()
|
||||
os.chdir(execution_path('.'))
|
||||
|
||||
|
||||
def test_glyph_symbolizer():
|
||||
srs = '+init=epsg:32630'
|
||||
lyr = mapnik2.Layer('arrows')
|
||||
lyr.datasource = mapnik2.Shapefile(
|
||||
file = '../data/shp/arrows.shp',
|
||||
)
|
||||
lyr.srs = srs
|
||||
_map = mapnik2.Map(256,256, srs)
|
||||
style = mapnik2.Style()
|
||||
rule = mapnik2.Rule()
|
||||
sym = mapnik2.GlyphSymbolizer("DejaVu Sans Condensed", mapnik2.Expression("'A'"))
|
||||
sym = mapnik2.GlyphSymbolizer("DejaVu Sans Condensed",
|
||||
mapnik2.Expression("'í'"))
|
||||
sym.allow_overlap = True
|
||||
sym.angle = mapnik2.Expression("[azimuth]-90")
|
||||
sym.value = mapnik2.Expression("[value]")
|
||||
sym.angle = mapnik2.Expression("[azimuth]+90") #+90 so the top of the glyph points upwards
|
||||
sym.size = mapnik2.Expression("[value]")
|
||||
sym.colorizer = mapnik2.RasterColorizer()
|
||||
for value, color in [
|
||||
( 0, "#0044cc"),
|
||||
( 10, "#00cc00"),
|
||||
( 20, "#ffff00"),
|
||||
( 30, "#ff7f00"),
|
||||
( 40, "#ff0000"),
|
||||
]:
|
||||
sym.colorizer.append_band(value, mapnik2.Color(color))
|
||||
rule.symbols.append(sym)
|
||||
style.rules.append(rule)
|
||||
_map.append_style('foo', style)
|
||||
lyr.styles.append('foo')
|
||||
_map.layers.append(lyr)
|
||||
_map.zoom_to_box(mapnik2.Box2d(0,0,8,8))
|
||||
sym.color = mapnik2.Expression("'#ff0000'")
|
||||
|
||||
_map = create_map_and_append_symbolyzer(sym)
|
||||
im = mapnik2.Image(_map.width,_map.height)
|
||||
mapnik2.render(_map, im)
|
||||
save_data('test_glyph_symbolizer.png', im.tostring('png'))
|
||||
imdata = im.tostring()
|
||||
assert len(imdata) > 0
|
||||
|
||||
raise Todo("Implement the process methods of the agg/cairo renderers for GlyphSymbolizer")
|
||||
|
||||
# we have features with 20 as a value so check that they're colored
|
||||
assert '\xff\xff\xff\x00' in imdata
|
||||
assert contains_word('\xff\x00\x00\xff', im.tostring())
|
||||
|
||||
def test_load_save_map():
|
||||
raise Todo("Implement XML de/serialization for GlyphSymbolizer")
|
||||
|
@ -62,6 +29,44 @@ def test_load_save_map():
|
|||
|
||||
out_map = mapnik2.save_map_to_string(map)
|
||||
assert 'GlyphSymbolizer' in out_map
|
||||
assert 'RasterSymbolizer' in out_map
|
||||
assert 'RasterColorizer' in out_map
|
||||
assert 'ColorBand' in out_map
|
||||
|
||||
#
|
||||
# Utilities and setup code
|
||||
#
|
||||
|
||||
def setup():
|
||||
# All of the paths used are relative, if we run the tests
|
||||
# from another directory we need to chdir()
|
||||
os.chdir(execution_path('.'))
|
||||
|
||||
def create_map_and_append_symbolyzer(sym):
|
||||
srs = '+init=epsg:32630'
|
||||
lyr = mapnik2.Layer('arrows')
|
||||
lyr.datasource = mapnik2.Shapefile(
|
||||
file = '../data/shp/arrows.shp',
|
||||
)
|
||||
lyr.srs = srs
|
||||
_map = mapnik2.Map(256,256, srs)
|
||||
style = mapnik2.Style()
|
||||
rule = mapnik2.Rule()
|
||||
#TODO Investigate why I need to add a filter which refers to the
|
||||
# feature property that the symbolizer needs so they don't reach
|
||||
# to it as nulls.
|
||||
rule.filter = mapnik2.Filter('[azimuth]>=0 and [value]>=0') #XXX
|
||||
rule.symbols.append(sym)
|
||||
|
||||
ts = mapnik2.TextSymbolizer(mapnik2.Expression('[azimuth]'),
|
||||
"DejaVu Sans Book",
|
||||
10,
|
||||
mapnik2.Color("black"))
|
||||
ts.allow_overlap = True
|
||||
rule.symbols.append(ts)
|
||||
|
||||
style.rules.append(rule)
|
||||
_map.append_style('foo', style)
|
||||
lyr.styles.append('foo')
|
||||
_map.layers.append(lyr)
|
||||
_map.zoom_to_box(mapnik2.Box2d(0,0,8,8))
|
||||
return _map
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
from nose.tools import *
|
||||
from utilities import execution_path, save_data
|
||||
from utilities import execution_path, save_data, contains_word
|
||||
|
||||
import os, mapnik2
|
||||
|
||||
|
@ -52,9 +52,8 @@ def test_dataraster_coloring():
|
|||
# save a png somewhere so we can see it
|
||||
save_data('test_dataraster_coloring.png', im.tostring('png'))
|
||||
imdata = im.tostring()
|
||||
bytes = [imdata[i:i+4] for i in xrange(0, len(imdata), 4)]
|
||||
# we have some values in the [20,30) interval so check that they're colored
|
||||
assert '\xff\xff\x00\xff' in bytes
|
||||
assert contains_word('\xff\xff\x00\xff', imdata)
|
||||
|
||||
def test_dataraster_query_point():
|
||||
srs = '+init=epsg:32630'
|
||||
|
|
|
@ -28,3 +28,24 @@ def save_data(filename, data, key='MAPNIK_TEST_DATA_DIR'):
|
|||
f.write(data)
|
||||
finally:
|
||||
f.close()
|
||||
|
||||
def contains_word(word, bytestring_):
|
||||
"""
|
||||
Checks that a bytestring contains a given word. len(bytestring) should be
|
||||
a multiple of len(word).
|
||||
|
||||
>>> contains_word("abcd", "abcd"*5)
|
||||
True
|
||||
|
||||
>>> contains_word("ab", "ba"*5)
|
||||
False
|
||||
|
||||
>>> contains_word("ab", "ab"*5+"a")
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
AssertionError: len(bytestring_) not multiple of len(word)
|
||||
"""
|
||||
n = len(word)
|
||||
assert len(bytestring_)%n == 0, "len(bytestring_) not multiple of len(word)"
|
||||
chunks = [bytestring_[i:i+n] for i in xrange(0, len(bytestring_), n)]
|
||||
return word in chunks
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
from python_tests.utilities import TodoPlugin
|
||||
from nose.plugins.doctests import Doctest
|
||||
|
||||
import nose, sys, os, getopt
|
||||
|
||||
|
@ -52,7 +53,7 @@ def main():
|
|||
print "- Running nosetests:"
|
||||
print
|
||||
|
||||
argv = [__file__, '--exe', '--with-todo']
|
||||
argv = [__file__, '--exe', '--with-todo', '--with-doctest', '--doctest-tests']
|
||||
|
||||
if not quiet:
|
||||
argv.append('-v')
|
||||
|
@ -62,7 +63,7 @@ def main():
|
|||
argv.append('-v')
|
||||
argv.append('-v')
|
||||
|
||||
if not nose.run(argv=argv, plugins=[TodoPlugin()]):
|
||||
if not nose.run(argv=argv, plugins=[TodoPlugin(), Doctest()]):
|
||||
sys.exit(1)
|
||||
else:
|
||||
sys.exit(0)
|
||||
|
|
Loading…
Add table
Reference in a new issue