pre-eliminary GlyphSymbolizer implementation. TODO: XML de/serializing. more tests. remove workaround mentioned in test

This commit is contained in:
Alberto Valverde 2010-03-18 20:05:08 +00:00
parent 31c3d20e43
commit d098c98c5e
8 changed files with 283 additions and 66 deletions

View file

@ -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:

View file

@ -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)

View file

@ -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>;

View file

@ -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

View file

@ -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

View file

@ -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'

View file

@ -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

View file

@ -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)