Discussion:
[python-mapnik] branch master created (now 48fc3e3)
Sebastiaan Couwenberg
2015-06-26 17:33:33 UTC
Permalink
This is an automated email from the git hooks/post-receive script.

sebastic pushed a change to branch master
in repository python-mapnik.

at 48fc3e3 Only build for Python 2.

This branch includes the following new commits:

new 5b38c93 Imported Upstream version 0.0~20150619-e477887
new 87907a2 Add initial Debian packaging.
new 48fc3e3 Only build for Python 2.

The 3 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "adds" were already present in the repository and have only
been added to this reference.
--
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-grass/python-mapnik.git
Sebastiaan Couwenberg
2015-06-26 17:33:33 UTC
Permalink
This is an automated email from the git hooks/post-receive script.

sebastic pushed a commit to branch master
in repository python-mapnik.

commit 5b38c932ba42f34bbb44e81b60317e4218e47b47
Author: Bas Couwenberg <***@xs4all.nl>
Date: Fri Jun 26 18:52:06 2015 +0200

Imported Upstream version 0.0~20150619-e477887
---
.gitignore | 20 +
.gitmodules | 6 +
.travis.yml | 63 ++
AUTHORS.md | 5 +
CHANGELOG.md | 6 +
COPYING | 502 +++++++++
README.md | 52 +
bootstrap.sh | 70 ++
build.py | 120 ++
mapnik/__init__.py | 1073 ++++++++++++++++++
mapnik/mapnik_settings.py | 13 +
mapnik/printing.py | 1027 +++++++++++++++++
setup.cfg | 2 +
setup.py | 226 ++++
src/boost_std_shared_shim.hpp | 49 +
src/mapnik_color.cpp | 130 +++
src/mapnik_coord.cpp | 73 ++
src/mapnik_datasource.cpp | 217 ++++
src/mapnik_datasource_cache.cpp | 104 ++
src/mapnik_enumeration.hpp | 88 ++
src/mapnik_enumeration_wrapper_converter.hpp | 45 +
src/mapnik_envelope.cpp | 301 +++++
src/mapnik_expression.cpp | 111 ++
src/mapnik_feature.cpp | 237 ++++
src/mapnik_featureset.cpp | 93 ++
src/mapnik_font_engine.cpp | 60 +
src/mapnik_fontset.cpp | 64 ++
src/mapnik_gamma_method.cpp | 49 +
src/mapnik_geometry.cpp | 290 +++++
src/mapnik_grid.cpp | 95 ++
src/mapnik_grid_view.cpp | 64 ++
src/mapnik_image.cpp | 471 ++++++++
src/mapnik_image_view.cpp | 128 +++
src/mapnik_label_collision_detector.cpp | 131 +++
src/mapnik_layer.cpp | 388 +++++++
src/mapnik_logger.cpp | 83 ++
src/mapnik_map.cpp | 543 +++++++++
src/mapnik_palette.cpp | 70 ++
src/mapnik_parameters.cpp | 246 ++++
src/mapnik_proj_transform.cpp | 154 +++
src/mapnik_projection.cpp | 125 +++
src/mapnik_python.cpp | 1072 ++++++++++++++++++
src/mapnik_query.cpp | 107 ++
src/mapnik_raster_colorizer.cpp | 241 ++++
src/mapnik_rule.cpp | 100 ++
src/mapnik_scaling_method.cpp | 58 +
src/mapnik_style.cpp | 118 ++
src/mapnik_svg.hpp | 56 +
src/mapnik_svg_generator_grammar.cpp | 27 +
src/mapnik_symbolizer.cpp | 422 +++++++
src/mapnik_text_placement.cpp | 587 ++++++++++
src/mapnik_threads.hpp | 109 ++
src/mapnik_value_converter.hpp | 90 ++
src/mapnik_view_transform.cpp | 92 ++
src/python_grid_utils.cpp | 405 +++++++
src/python_grid_utils.hpp | 79 ++
src/python_optional.hpp | 198 ++++
src/python_to_value.hpp | 122 ++
test/python_tests/__init__.py | 0
.../agg_rasterizer_integer_overflow_test.py | 71 ++
test/python_tests/box2d_test.py | 176 +++
test/python_tests/buffer_clear_test.py | 61 +
test/python_tests/cairo_test.py | 196 ++++
test/python_tests/color_test.py | 115 ++
test/python_tests/compare_test.py | 112 ++
test/python_tests/compositing_test.py | 258 +++++
test/python_tests/copy_test.py | 93 ++
test/python_tests/csv_test.py | 604 ++++++++++
test/python_tests/datasource_test.py | 168 +++
test/python_tests/datasource_xml_template_test.py | 23 +
test/python_tests/extra_map_props_test.py | 54 +
test/python_tests/feature_id_test.py | 66 ++
test/python_tests/feature_test.py | 110 ++
test/python_tests/filter_test.py | 451 ++++++++
test/python_tests/fontset_test.py | 41 +
test/python_tests/geojson_plugin_test.py | 126 +++
test/python_tests/geometry_io_test.py | 273 +++++
test/python_tests/grayscale_test.py | 13 +
test/python_tests/image_encoding_speed_test.py | 124 +++
test/python_tests/image_filters_test.py | 68 ++
test/python_tests/image_test.py | 346 ++++++
test/python_tests/image_tiff_test.py | 335 ++++++
test/python_tests/images/actual.png | Bin 0 -> 899 bytes
test/python_tests/images/composited/clear.png | Bin 0 -> 334 bytes
test/python_tests/images/composited/color.png | Bin 0 -> 13905 bytes
test/python_tests/images/composited/color_burn.png | Bin 0 -> 14804 bytes
.../python_tests/images/composited/color_dodge.png | Bin 0 -> 14898 bytes
test/python_tests/images/composited/contrast.png | Bin 0 -> 10630 bytes
test/python_tests/images/composited/darken.png | Bin 0 -> 14551 bytes
test/python_tests/images/composited/difference.png | Bin 0 -> 14926 bytes
test/python_tests/images/composited/divide.png | Bin 0 -> 10492 bytes
test/python_tests/images/composited/dst.png | Bin 0 -> 7521 bytes
test/python_tests/images/composited/dst_atop.png | Bin 0 -> 11764 bytes
test/python_tests/images/composited/dst_in.png | Bin 0 -> 7563 bytes
test/python_tests/images/composited/dst_out.png | Bin 0 -> 9501 bytes
test/python_tests/images/composited/dst_over.png | Bin 0 -> 14402 bytes
test/python_tests/images/composited/exclusion.png | Bin 0 -> 14219 bytes
.../images/composited/grain_extract.png | Bin 0 -> 9149 bytes
.../python_tests/images/composited/grain_merge.png | Bin 0 -> 13368 bytes
test/python_tests/images/composited/hard_light.png | Bin 0 -> 15018 bytes
test/python_tests/images/composited/hue.png | Bin 0 -> 13240 bytes
test/python_tests/images/composited/invert.png | Bin 0 -> 14130 bytes
test/python_tests/images/composited/invert_rgb.png | Bin 0 -> 13952 bytes
test/python_tests/images/composited/lighten.png | Bin 0 -> 14758 bytes
.../python_tests/images/composited/linear_burn.png | Bin 0 -> 10261 bytes
.../images/composited/linear_dodge.png | Bin 0 -> 14279 bytes
test/python_tests/images/composited/minus.png | Bin 0 -> 12486 bytes
test/python_tests/images/composited/multiply.png | Bin 0 -> 14948 bytes
test/python_tests/images/composited/overlay.png | Bin 0 -> 15167 bytes
test/python_tests/images/composited/plus.png | Bin 0 -> 13667 bytes
test/python_tests/images/composited/saturation.png | Bin 0 -> 13561 bytes
test/python_tests/images/composited/screen.png | Bin 0 -> 14839 bytes
test/python_tests/images/composited/soft_light.png | Bin 0 -> 15000 bytes
test/python_tests/images/composited/src.png | Bin 0 -> 8085 bytes
test/python_tests/images/composited/src_atop.png | Bin 0 -> 11651 bytes
test/python_tests/images/composited/src_in.png | Bin 0 -> 8163 bytes
test/python_tests/images/composited/src_out.png | Bin 0 -> 10273 bytes
test/python_tests/images/composited/src_over.png | Bin 0 -> 14368 bytes
test/python_tests/images/composited/value.png | Bin 0 -> 13720 bytes
test/python_tests/images/composited/xor.png | Bin 0 -> 14733 bytes
test/python_tests/images/expected.png | Bin 0 -> 1263 bytes
.../pycairo/cairo-cairo-expected-reduced.png | Bin 0 -> 2117 bytes
.../images/pycairo/cairo-cairo-expected.pdf | Bin 0 -> 4340 bytes
.../images/pycairo/cairo-cairo-expected.png | Bin 0 -> 3624 bytes
.../images/pycairo/cairo-cairo-expected.svg | 47 +
.../pycairo/cairo-surface-expected.building.pdf | Bin 0 -> 7467 bytes
.../pycairo/cairo-surface-expected.building.svg | 261 +++++
.../pycairo/cairo-surface-expected.point.pdf | Bin 0 -> 29928 bytes
.../pycairo/cairo-surface-expected.point.svg | 413 +++++++
.../pycairo/cairo-surface-expected.polygon.pdf | Bin 0 -> 5622 bytes
.../pycairo/cairo-surface-expected.polygon.svg | 35 +
test/python_tests/images/style-comp-op/clear.png | Bin 0 -> 334 bytes
test/python_tests/images/style-comp-op/color.png | Bin 0 -> 13762 bytes
.../images/style-comp-op/color_burn.png | Bin 0 -> 14900 bytes
.../images/style-comp-op/color_dodge.png | Bin 0 -> 14503 bytes
.../python_tests/images/style-comp-op/contrast.png | Bin 0 -> 14532 bytes
test/python_tests/images/style-comp-op/darken.png | Bin 0 -> 14321 bytes
.../images/style-comp-op/difference.png | Bin 0 -> 14680 bytes
test/python_tests/images/style-comp-op/divide.png | Bin 0 -> 3626 bytes
test/python_tests/images/style-comp-op/dst.png | Bin 0 -> 11358 bytes
.../python_tests/images/style-comp-op/dst_atop.png | Bin 0 -> 7660 bytes
test/python_tests/images/style-comp-op/dst_in.png | Bin 0 -> 7660 bytes
test/python_tests/images/style-comp-op/dst_out.png | Bin 0 -> 14889 bytes
.../python_tests/images/style-comp-op/dst_over.png | Bin 0 -> 11358 bytes
.../images/style-comp-op/exclusion.png | Bin 0 -> 14354 bytes
.../images/style-comp-op/grain_extract.png | Bin 0 -> 7252 bytes
.../images/style-comp-op/grain_merge.png | Bin 0 -> 14206 bytes
.../images/style-comp-op/hard_light.png | Bin 0 -> 14839 bytes
test/python_tests/images/style-comp-op/hue.png | Bin 0 -> 12920 bytes
test/python_tests/images/style-comp-op/invert.png | Bin 0 -> 13521 bytes
test/python_tests/images/style-comp-op/lighten.png | Bin 0 -> 12953 bytes
.../images/style-comp-op/linear_burn.png | Bin 0 -> 2362 bytes
.../images/style-comp-op/linear_dodge.png | Bin 0 -> 14392 bytes
test/python_tests/images/style-comp-op/minus.png | Bin 0 -> 14359 bytes
.../python_tests/images/style-comp-op/multiply.png | Bin 0 -> 14663 bytes
test/python_tests/images/style-comp-op/overlay.png | Bin 0 -> 14378 bytes
test/python_tests/images/style-comp-op/plus.png | Bin 0 -> 14392 bytes
.../images/style-comp-op/saturation.png | Bin 0 -> 13640 bytes
test/python_tests/images/style-comp-op/screen.png | Bin 0 -> 14240 bytes
.../images/style-comp-op/soft_light.png | Bin 0 -> 14104 bytes
test/python_tests/images/style-comp-op/src.png | Bin 0 -> 4942 bytes
.../python_tests/images/style-comp-op/src_atop.png | Bin 0 -> 14778 bytes
test/python_tests/images/style-comp-op/src_in.png | Bin 0 -> 4942 bytes
test/python_tests/images/style-comp-op/src_out.png | Bin 0 -> 334 bytes
.../python_tests/images/style-comp-op/src_over.png | Bin 0 -> 14767 bytes
test/python_tests/images/style-comp-op/value.png | Bin 0 -> 14450 bytes
test/python_tests/images/style-comp-op/xor.png | Bin 0 -> 14934 bytes
.../images/style-image-filter/agg-stack-blur22.png | Bin 0 -> 33700 bytes
.../images/style-image-filter/blur.png | Bin 0 -> 27212 bytes
.../images/style-image-filter/edge-detect.png | Bin 0 -> 22467 bytes
.../images/style-image-filter/emboss.png | Bin 0 -> 24564 bytes
.../images/style-image-filter/gray.png | Bin 0 -> 23594 bytes
.../images/style-image-filter/invert.png | Bin 0 -> 24135 bytes
.../images/style-image-filter/none.png | Bin 0 -> 24072 bytes
.../images/style-image-filter/sharpen.png | Bin 0 -> 22765 bytes
.../images/style-image-filter/sobel.png | Bin 0 -> 23934 bytes
.../images/style-image-filter/x-gradient.png | Bin 0 -> 27052 bytes
.../images/style-image-filter/y-gradient.png | Bin 0 -> 27276 bytes
test/python_tests/images/support/a.png | Bin 0 -> 7543 bytes
test/python_tests/images/support/b.png | Bin 0 -> 8084 bytes
.../images/support/dataraster_coloring.png | Bin 0 -> 2879 bytes
.../encoding-opts/aerial_rgba-png+e=miniz.png | Bin 0 -> 47214 bytes
.../support/encoding-opts/aerial_rgba-png+t=0.png | Bin 0 -> 46310 bytes
.../support/encoding-opts/aerial_rgba-png.png | Bin 0 -> 46310 bytes
.../encoding-opts/aerial_rgba-png32+e=miniz.png | Bin 0 -> 160552 bytes
.../encoding-opts/aerial_rgba-png32+t=0.png | Bin 0 -> 143537 bytes
.../support/encoding-opts/aerial_rgba-png32.png | Bin 0 -> 160268 bytes
.../encoding-opts/aerial_rgba-png8+e=miniz.png | Bin 0 -> 47293 bytes
.../encoding-opts/aerial_rgba-png8+m=h+c=1+t=0.png | Bin 0 -> 103 bytes
.../encoding-opts/aerial_rgba-png8+m=h+c=1.png | Bin 0 -> 103 bytes
.../encoding-opts/aerial_rgba-png8+m=h+t=0.png | Bin 0 -> 46506 bytes
.../encoding-opts/aerial_rgba-png8+m=h+t=1.png | Bin 0 -> 46506 bytes
.../encoding-opts/aerial_rgba-png8+m=h+t=2.png | Bin 0 -> 46506 bytes
.../support/encoding-opts/aerial_rgba-png8+m=h.png | Bin 0 -> 46506 bytes
.../encoding-opts/aerial_rgba-png8+m=o+c=1+t=0.png | Bin 0 -> 103 bytes
.../encoding-opts/aerial_rgba-png8+m=o+c=1.png | Bin 0 -> 103 bytes
.../encoding-opts/aerial_rgba-png8+m=o+t=0.png | Bin 0 -> 43267 bytes
.../encoding-opts/aerial_rgba-png8+m=o+t=1.png | Bin 0 -> 43267 bytes
.../encoding-opts/aerial_rgba-png8+m=o+t=2.png | Bin 0 -> 43267 bytes
.../support/encoding-opts/aerial_rgba-png8+m=o.png | Bin 0 -> 43267 bytes
.../aerial_rgba-webp+alpha=false.webp | Bin 0 -> 10544 bytes
.../aerial_rgba-webp+alpha_compression=0.webp | Bin 0 -> 10544 bytes
.../aerial_rgba-webp+alpha_filtering=2.webp | Bin 0 -> 10544 bytes
.../aerial_rgba-webp+alpha_quality=50.webp | Bin 0 -> 10544 bytes
.../aerial_rgba-webp+autofilter=0.webp | Bin 0 -> 10544 bytes
.../aerial_rgba-webp+filter_sharpness=4.webp | Bin 0 -> 10544 bytes
.../aerial_rgba-webp+filter_strength=50.webp | Bin 0 -> 10544 bytes
...erial_rgba-webp+filter_type=1+autofilter=1.webp | Bin 0 -> 10544 bytes
.../encoding-opts/aerial_rgba-webp+method=0.webp | Bin 0 -> 11778 bytes
.../encoding-opts/aerial_rgba-webp+method=6.webp | Bin 0 -> 10010 bytes
.../aerial_rgba-webp+partition_limit=50.webp | Bin 0 -> 10572 bytes
.../aerial_rgba-webp+partitions=3.webp | Bin 0 -> 10544 bytes
.../encoding-opts/aerial_rgba-webp+pass=10.webp | Bin 0 -> 10526 bytes
.../aerial_rgba-webp+preprocessing=1.webp | Bin 0 -> 10546 bytes
.../encoding-opts/aerial_rgba-webp+quality=64.webp | Bin 0 -> 9338 bytes
.../encoding-opts/aerial_rgba-webp+segments=3.webp | Bin 0 -> 10528 bytes
.../aerial_rgba-webp+sns_strength=50.webp | Bin 0 -> 10544 bytes
.../aerial_rgba-webp+target_PSNR=.5.webp | Bin 0 -> 10544 bytes
.../aerial_rgba-webp+target_size=100.webp | Bin 0 -> 10544 bytes
.../support/encoding-opts/aerial_rgba-webp.webp | Bin 0 -> 10544 bytes
.../support/encoding-opts/blank-png+e=miniz.png | Bin 0 -> 103 bytes
.../images/support/encoding-opts/blank-png+t=0.png | Bin 0 -> 103 bytes
.../images/support/encoding-opts/blank-png.png | Bin 0 -> 103 bytes
.../support/encoding-opts/blank-png32+e=miniz.png | Bin 0 -> 985 bytes
.../support/encoding-opts/blank-png32+t=0.png | Bin 0 -> 851 bytes
.../images/support/encoding-opts/blank-png32.png | Bin 0 -> 915 bytes
.../support/encoding-opts/blank-png8+e=miniz.png | Bin 0 -> 103 bytes
.../encoding-opts/blank-png8+m=h+c=1+t=0.png | Bin 0 -> 103 bytes
.../support/encoding-opts/blank-png8+m=h+c=1.png | Bin 0 -> 103 bytes
.../support/encoding-opts/blank-png8+m=h+t=0.png | Bin 0 -> 103 bytes
.../support/encoding-opts/blank-png8+m=h+t=1.png | Bin 0 -> 103 bytes
.../support/encoding-opts/blank-png8+m=h+t=2.png | Bin 0 -> 103 bytes
.../support/encoding-opts/blank-png8+m=h.png | Bin 0 -> 103 bytes
.../encoding-opts/blank-png8+m=o+c=1+t=0.png | Bin 0 -> 103 bytes
.../support/encoding-opts/blank-png8+m=o+c=1.png | Bin 0 -> 103 bytes
.../support/encoding-opts/blank-png8+m=o+t=0.png | Bin 0 -> 103 bytes
.../support/encoding-opts/blank-png8+m=o+t=1.png | Bin 0 -> 103 bytes
.../support/encoding-opts/blank-png8+m=o+t=2.png | Bin 0 -> 103 bytes
.../support/encoding-opts/blank-png8+m=o.png | Bin 0 -> 103 bytes
.../encoding-opts/blank-webp+alpha=false.webp | Bin 0 -> 180 bytes
.../blank-webp+alpha_compression=0.webp | Bin 0 -> 65744 bytes
.../blank-webp+alpha_filtering=2.webp | Bin 0 -> 224 bytes
.../encoding-opts/blank-webp+alpha_quality=50.webp | Bin 0 -> 224 bytes
.../encoding-opts/blank-webp+autofilter=0.webp | Bin 0 -> 224 bytes
.../blank-webp+filter_sharpness=4.webp | Bin 0 -> 224 bytes
.../blank-webp+filter_strength=50.webp | Bin 0 -> 224 bytes
.../blank-webp+filter_type=1+autofilter=1.webp | Bin 0 -> 220 bytes
.../support/encoding-opts/blank-webp+method=0.webp | Bin 0 -> 282 bytes
.../support/encoding-opts/blank-webp+method=6.webp | Bin 0 -> 240 bytes
.../blank-webp+partition_limit=50.webp | Bin 0 -> 224 bytes
.../encoding-opts/blank-webp+partitions=3.webp | Bin 0 -> 224 bytes
.../support/encoding-opts/blank-webp+pass=10.webp | Bin 0 -> 224 bytes
.../encoding-opts/blank-webp+preprocessing=1.webp | Bin 0 -> 224 bytes
.../encoding-opts/blank-webp+quality=64.webp | Bin 0 -> 222 bytes
.../encoding-opts/blank-webp+segments=3.webp | Bin 0 -> 222 bytes
.../encoding-opts/blank-webp+sns_strength=50.webp | Bin 0 -> 224 bytes
.../encoding-opts/blank-webp+target_PSNR=.5.webp | Bin 0 -> 224 bytes
.../encoding-opts/blank-webp+target_size=100.webp | Bin 0 -> 224 bytes
.../images/support/encoding-opts/blank-webp.webp | Bin 0 -> 224 bytes
.../images/support/encoding-opts/png8-17cols.png | Bin 0 -> 192 bytes
.../images/support/encoding-opts/png8-2px.A.png | Bin 0 -> 351 bytes
.../images/support/encoding-opts/png8-2px.png | Bin 0 -> 351 bytes
.../images/support/encoding-opts/png8-9cols.png | Bin 0 -> 171 bytes
.../support/encoding-opts/solid-png+e=miniz.png | Bin 0 -> 116 bytes
.../images/support/encoding-opts/solid-png+t=0.png | Bin 0 -> 103 bytes
.../images/support/encoding-opts/solid-png.png | Bin 0 -> 116 bytes
.../support/encoding-opts/solid-png32+e=miniz.png | Bin 0 -> 334 bytes
.../support/encoding-opts/solid-png32+t=0.png | Bin 0 -> 270 bytes
.../images/support/encoding-opts/solid-png32.png | Bin 0 -> 334 bytes
.../support/encoding-opts/solid-png8+e=miniz.png | Bin 0 -> 116 bytes
.../encoding-opts/solid-png8+m=h+c=1+t=0.png | Bin 0 -> 103 bytes
.../support/encoding-opts/solid-png8+m=h+c=1.png | Bin 0 -> 116 bytes
.../support/encoding-opts/solid-png8+m=h+t=0.png | Bin 0 -> 103 bytes
.../support/encoding-opts/solid-png8+m=h+t=1.png | Bin 0 -> 116 bytes
.../support/encoding-opts/solid-png8+m=h+t=2.png | Bin 0 -> 116 bytes
.../support/encoding-opts/solid-png8+m=h.png | Bin 0 -> 116 bytes
.../encoding-opts/solid-png8+m=o+c=1+t=0.png | Bin 0 -> 103 bytes
.../support/encoding-opts/solid-png8+m=o+c=1.png | Bin 0 -> 116 bytes
.../support/encoding-opts/solid-png8+m=o+t=0.png | Bin 0 -> 103 bytes
.../support/encoding-opts/solid-png8+m=o+t=1.png | Bin 0 -> 116 bytes
.../support/encoding-opts/solid-png8+m=o+t=2.png | Bin 0 -> 116 bytes
.../support/encoding-opts/solid-png8+m=o.png | Bin 0 -> 116 bytes
.../encoding-opts/solid-webp+alpha=false.webp | Bin 0 -> 200 bytes
.../solid-webp+alpha_compression=0.webp | Bin 0 -> 200 bytes
.../solid-webp+alpha_filtering=2.webp | Bin 0 -> 200 bytes
.../encoding-opts/solid-webp+alpha_quality=50.webp | Bin 0 -> 200 bytes
.../encoding-opts/solid-webp+autofilter=0.webp | Bin 0 -> 200 bytes
.../solid-webp+filter_sharpness=4.webp | Bin 0 -> 200 bytes
.../solid-webp+filter_strength=50.webp | Bin 0 -> 200 bytes
.../solid-webp+filter_type=1+autofilter=1.webp | Bin 0 -> 196 bytes
.../support/encoding-opts/solid-webp+method=0.webp | Bin 0 -> 258 bytes
.../support/encoding-opts/solid-webp+method=6.webp | Bin 0 -> 216 bytes
.../solid-webp+partition_limit=50.webp | Bin 0 -> 200 bytes
.../encoding-opts/solid-webp+partitions=3.webp | Bin 0 -> 200 bytes
.../support/encoding-opts/solid-webp+pass=10.webp | Bin 0 -> 200 bytes
.../encoding-opts/solid-webp+preprocessing=1.webp | Bin 0 -> 200 bytes
.../encoding-opts/solid-webp+quality=64.webp | Bin 0 -> 196 bytes
.../encoding-opts/solid-webp+segments=3.webp | Bin 0 -> 200 bytes
.../encoding-opts/solid-webp+sns_strength=50.webp | Bin 0 -> 200 bytes
.../encoding-opts/solid-webp+target_PSNR=.5.webp | Bin 0 -> 200 bytes
.../encoding-opts/solid-webp+target_size=100.webp | Bin 0 -> 200 bytes
.../images/support/encoding-opts/solid-webp.webp | Bin 0 -> 200 bytes
.../images/support/mapnik-layer-buffer-size.png | Bin 0 -> 2461 bytes
.../support/mapnik-marker-ellipse-render1.png | Bin 0 -> 15850 bytes
.../support/mapnik-marker-ellipse-render2.png | Bin 0 -> 13992 bytes
.../mapnik-merc2merc-reprojection-render1.png | Bin 0 -> 44731 bytes
.../mapnik-merc2merc-reprojection-render2.png | Bin 0 -> 44643 bytes
.../mapnik-merc2wgs84-reprojection-render.png | Bin 0 -> 40505 bytes
.../images/support/mapnik-palette-test.png | Bin 0 -> 12129 bytes
.../support/mapnik-python-circle-render1.png | Bin 0 -> 126206 bytes
.../images/support/mapnik-python-point-render1.png | Bin 0 -> 4165 bytes
.../images/support/mapnik-style-level-opacity.png | Bin 0 -> 42459 bytes
.../mapnik-wgs842merc-reprojection-render.png | Bin 0 -> 48074 bytes
.../images/support/marker-in-center-not-placed.png | Bin 0 -> 116 bytes
.../images/support/marker-in-center.png | Bin 0 -> 250 bytes
.../marker-text-line-scale-factor-0.005.png | Bin 0 -> 1877 bytes
.../support/marker-text-line-scale-factor-0.1.png | Bin 0 -> 3851 bytes
.../marker-text-line-scale-factor-0.899.png | Bin 0 -> 17229 bytes
.../support/marker-text-line-scale-factor-1.5.png | Bin 0 -> 11502 bytes
.../support/marker-text-line-scale-factor-1.png | Bin 0 -> 18310 bytes
.../support/marker-text-line-scale-factor-10.png | Bin 0 -> 8348 bytes
.../support/marker-text-line-scale-factor-100.png | Bin 0 -> 2696 bytes
.../marker-text-line-scale-factor-1e-05.png | Bin 0 -> 1637 bytes
.../support/marker-text-line-scale-factor-2.png | Bin 0 -> 11823 bytes
.../support/marker-text-line-scale-factor-5.png | Bin 0 -> 13987 bytes
...data_subquery-data_16bsi_subquery-16BSI-135.png | Bin 0 -> 87 bytes
...data_subquery-data_16bui_subquery-16BUI-126.png | Bin 0 -> 87 bytes
.../data_subquery-data_2bui_subquery-2BUI-3.png | Bin 0 -> 87 bytes
.../data_subquery-data_32bf_subquery-32BF-450.png | Bin 0 -> 87 bytes
...data_subquery-data_32bsi_subquery-32BSI-264.png | Bin 0 -> 87 bytes
...data_subquery-data_32bui_subquery-32BUI-255.png | Bin 0 -> 87 bytes
.../data_subquery-data_4bui_subquery-4BUI-15.png | Bin 0 -> 87 bytes
.../data_subquery-data_64bf_subquery-64BF-3072.png | Bin 0 -> 87 bytes
.../data_subquery-data_8bsi_subquery-8BSI-69.png | Bin 0 -> 87 bytes
.../data_subquery-data_8bui_subquery-8BUI-63.png | Bin 0 -> 87 bytes
...subquery-grayscale_16bsi_subquery-16BSI-144.png | Bin 0 -> 96 bytes
...subquery-grayscale_16bui_subquery-16BUI-126.png | Bin 0 -> 96 bytes
...ale_subquery-grayscale_2bui_subquery-2BUI-3.png | Bin 0 -> 96 bytes
...subquery-grayscale_32bsi_subquery-32BSI-129.png | Bin 0 -> 96 bytes
...subquery-grayscale_32bui_subquery-32BUI-255.png | Bin 0 -> 92 bytes
...le_subquery-grayscale_4bui_subquery-4BUI-15.png | Bin 0 -> 96 bytes
...le_subquery-grayscale_8bsi_subquery-8BSI-69.png | Bin 0 -> 96 bytes
...le_subquery-grayscale_8bui_subquery-8BUI-63.png | Bin 0 -> 95 bytes
...ui-nodataedge-rgb_8bui C T_64x64 Cl--1-box1.png | Bin 0 -> 124081 bytes
...ui-nodataedge-rgb_8bui C T_64x64 Cl--1-box2.png | Bin 0 -> 4378 bytes
...nodataedge-rgb_8bui C T_64x64 Sc Cl--1-box1.png | Bin 0 -> 124277 bytes
...nodataedge-rgb_8bui C T_64x64 Sc Cl--1-box2.png | Bin 0 -> 4378 bytes
...ui-nodataedge-rgb_8bui C T_64x64 Sc--0-box1.png | Bin 0 -> 124277 bytes
...ui-nodataedge-rgb_8bui C T_64x64 Sc--0-box2.png | Bin 0 -> 4378 bytes
..._8bui-nodataedge-rgb_8bui C T_64x64--0-box1.png | Bin 0 -> 124081 bytes
..._8bui-nodataedge-rgb_8bui C T_64x64--0-box2.png | Bin 0 -> 4378 bytes
.../rgba_8bui-rgba_8bui C O_2 Cl-2-1-box1.png | Bin 0 -> 12436 bytes
.../rgba_8bui-rgba_8bui C O_2 Cl-2-1-box2.png | Bin 0 -> 3647 bytes
.../rgba_8bui-rgba_8bui C O_2 Sc Cl-2-1-box1.png | Bin 0 -> 12436 bytes
.../rgba_8bui-rgba_8bui C O_2 Sc Cl-2-1-box2.png | Bin 0 -> 3647 bytes
.../rgba_8bui-rgba_8bui C O_2 Sc-2-0-box1.png | Bin 0 -> 12436 bytes
.../rgba_8bui-rgba_8bui C O_2 Sc-2-0-box2.png | Bin 0 -> 3637 bytes
.../rgba_8bui-rgba_8bui C O_2-2-0-box1.png | Bin 0 -> 12436 bytes
.../rgba_8bui-rgba_8bui C O_2-2-0-box2.png | Bin 0 -> 3637 bytes
...ba_8bui-rgba_8bui C T_16x16 O_2 Cl-2-1-box1.png | Bin 0 -> 12436 bytes
...ba_8bui-rgba_8bui C T_16x16 O_2 Cl-2-1-box2.png | Bin 0 -> 3650 bytes
...8bui-rgba_8bui C T_16x16 O_2 Sc Cl-2-1-box1.png | Bin 0 -> 12436 bytes
...8bui-rgba_8bui C T_16x16 O_2 Sc Cl-2-1-box2.png | Bin 0 -> 3650 bytes
...ba_8bui-rgba_8bui C T_16x16 O_2 Sc-2-0-box1.png | Bin 0 -> 12436 bytes
...ba_8bui-rgba_8bui C T_16x16 O_2 Sc-2-0-box2.png | Bin 0 -> 3638 bytes
.../rgba_8bui-rgba_8bui C T_16x16 O_2-2-0-box1.png | Bin 0 -> 12436 bytes
.../rgba_8bui-rgba_8bui C T_16x16 O_2-2-0-box2.png | Bin 0 -> 3638 bytes
.../rgba_8bui-rgba_8bui O_2 Cl-2-1-box1.png | Bin 0 -> 12436 bytes
.../rgba_8bui-rgba_8bui O_2 Cl-2-1-box2.png | Bin 0 -> 3647 bytes
.../rgba_8bui-rgba_8bui O_2 Sc Cl-2-1-box1.png | Bin 0 -> 12436 bytes
.../rgba_8bui-rgba_8bui O_2 Sc Cl-2-1-box2.png | Bin 0 -> 3647 bytes
.../rgba_8bui-rgba_8bui O_2 Sc-2-0-box1.png | Bin 0 -> 12436 bytes
.../rgba_8bui-rgba_8bui O_2 Sc-2-0-box2.png | Bin 0 -> 3637 bytes
.../pgraster/rgba_8bui-rgba_8bui O_2-2-0-box1.png | Bin 0 -> 12436 bytes
.../pgraster/rgba_8bui-rgba_8bui O_2-2-0-box2.png | Bin 0 -> 3637 bytes
...rgba_8bui-rgba_8bui T_16x16 O_2 Cl-2-1-box1.png | Bin 0 -> 12436 bytes
...rgba_8bui-rgba_8bui T_16x16 O_2 Cl-2-1-box2.png | Bin 0 -> 3650 bytes
...a_8bui-rgba_8bui T_16x16 O_2 Sc Cl-2-1-box1.png | Bin 0 -> 12436 bytes
...a_8bui-rgba_8bui T_16x16 O_2 Sc Cl-2-1-box2.png | Bin 0 -> 3650 bytes
...rgba_8bui-rgba_8bui T_16x16 O_2 Sc-2-0-box1.png | Bin 0 -> 12436 bytes
...rgba_8bui-rgba_8bui T_16x16 O_2 Sc-2-0-box2.png | Bin 0 -> 3638 bytes
.../rgba_8bui-rgba_8bui T_16x16 O_2-2-0-box1.png | Bin 0 -> 12436 bytes
.../rgba_8bui-rgba_8bui T_16x16 O_2-2-0-box2.png | Bin 0 -> 3638 bytes
...rgba_8bui_subquery-8BUI-255-0-0-255-255-255.png | Bin 0 -> 93 bytes
test/python_tests/images/support/raster-alpha.png | Bin 0 -> 4442 bytes
.../python_tests/images/support/raster_warping.png | Bin 0 -> 1464 bytes
.../raster_warping_does_not_overclip_source.png | Bin 0 -> 1099 bytes
test/python_tests/images/support/spacing.png | Bin 0 -> 48120 bytes
.../images/support/transparency/aerial_rgb.png | Bin 0 -> 143537 bytes
.../images/support/transparency/aerial_rgba.png | Bin 0 -> 160268 bytes
.../images/support/transparency/white0.png | Bin 0 -> 242 bytes
.../images/support/transparency/white0.webp | Bin 0 -> 738 bytes
.../images/support/transparency/white1.png | Bin 0 -> 257 bytes
.../images/support/transparency/white2.png | Bin 0 -> 258 bytes
test/python_tests/introspection_test.py | 61 +
test/python_tests/json_feature_properties_test.py | 102 ++
test/python_tests/layer_buffer_size_test.py | 35 +
test/python_tests/layer_modification_test.py | 75 ++
test/python_tests/layer_test.py | 28 +
test/python_tests/load_map_test.py | 82 ++
test/python_tests/map_query_test.py | 104 ++
test/python_tests/mapnik_logger_test.py | 18 +
test/python_tests/mapnik_test_data_test.py | 60 +
.../python_tests/markers_complex_rendering_test.py | 43 +
test/python_tests/memory_datasource_test.py | 34 +
test/python_tests/multi_tile_raster_test.py | 68 ++
test/python_tests/object_test.py | 569 ++++++++++
test/python_tests/ogr_and_shape_geometries_test.py | 43 +
test/python_tests/ogr_test.py | 157 +++
test/python_tests/osm_test.py | 62 ++
test/python_tests/palette_test.py | 54 +
test/python_tests/parameters_test.py | 61 +
test/python_tests/pgraster_test.py | 763 +++++++++++++
test/python_tests/pickling_test.py | 44 +
test/python_tests/png_encoding_test.py | 218 ++++
test/python_tests/pngsuite_test.py | 35 +
test/python_tests/postgis_test.py | 1177 ++++++++++++++++++++
test/python_tests/projection_test.py | 151 +++
test/python_tests/python_plugin_test.py | 160 +++
test/python_tests/query_test.py | 37 +
test/python_tests/query_tolerance_test.py | 43 +
test/python_tests/raster_colorizer_test.py | 90 ++
test/python_tests/raster_symbolizer_test.py | 217 ++++
test/python_tests/rasterlite_test.py | 38 +
test/python_tests/render_grid_test.py | 356 ++++++
test/python_tests/render_test.py | 241 ++++
test/python_tests/reprojection_test.py | 92 ++
test/python_tests/save_map_test.py | 76 ++
test/python_tests/shapefile_test.py | 113 ++
test/python_tests/shapeindex_test.py | 51 +
test/python_tests/sqlite_rtree_test.py | 169 +++
test/python_tests/sqlite_test.py | 501 +++++++++
test/python_tests/style_test.py | 18 +
test/python_tests/topojson_plugin_test.py | 91 ++
test/python_tests/utilities.py | 102 ++
test/python_tests/webp_encoding_test.py | 164 +++
test/run_tests.py | 91 ++
test/visual.py | 331 ++++++
438 files changed, 23142 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..331398c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,20 @@
+.DS_Store
+*.gcov
+*.gcda
+*.gcno
+*~
+*.o
+*.pyc
+*.os
+*.so
+*.a
+*.swp
+*.dylib
+build/
+dist/
+mapnik/paths.py
+*.egg-info/
+.eggs/
+.mason/
+mason_packages/
+mapnik/plugins
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..cf5011a
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,6 @@
+[submodule "test/data-visual"]
+ path = test/data-visual
+ url = https://github.com/mapnik/test-data-visual.git
+[submodule "test/data"]
+ path = test/data
+ url = https://github.com/mapnik/test-data.git
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..9e32a79
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,63 @@
+language: cpp
+
+sudo: false
+
+git:
+ submodules: true
+ depth: 10
+
+matrix:
+ include:
+ - os: linux
+ compiler: clang
+ - os: osx
+ compiler: clang
+
+env:
+ global:
+ - secure: "CqhZDPctJcpXGPpmIPK5usD/O+2HYawW3434oDufVS9uG/+C7aHzKzi8cuZ7n/REHqJMzy7gJfp6DiyF2QowpnN1L2W0FSJ9VOgj4JQF2Wsupo6gJkq6/CW2Fa35PhQHsv29bfyqtIq+R5SBVAieBe/Lh2P144RwRliGRopGQ68="
+ - secure: "idk4fdU49i546Zs6Fxha14H05eRJ1G/D6NPRaie8M8o+xySnEqf+TyA9/HU8QH7cFvroSLuHJ1U7TmwnR+sXy4XBlIfHLi4u2MN+l/q014GG7T2E2xYcTauqjB4ldToRsDQwe5Dq0gZCMsHLPspWPjL9twfp+Ds7qgcFhTsct0s="
+
+addons:
+ postgresql: "9.4"
+ apt:
+ sources:
+ - ubuntu-toolchain-r-test
+ - llvm-toolchain-precise-3.5
+ packages:
+ - clang-3.5
+
+before_install:
+ - export COMMIT_MESSAGE=$(git show -s --format=%B $TRAVIS_COMMIT | tr -d '\n')
+ - export MASON_BUILD=true
+ - if [[ $(uname -s) == 'Linux' ]]; then
+ psql -U postgres -c 'create database template_postgis;' -U postgres;
+ psql -U postgres -c 'create extension postgis;' -d template_postgis -U postgres;
+ export CXX="clang++-3.5";
+ export CC="clang++-3.5";
+ export PYTHONPATH=$(pwd)/mason_packages/.link/lib/python2.7/site-packages;
+ else
+ export PYTHONPATH=$(pwd)/mason_packages/.link/lib/python/site-packages;
+ fi;
+ - PYTHONUSERBASE=$(pwd)/mason_packages/.link pip install --user nose
+ - PYTHONUSERBASE=$(pwd)/mason_packages/.link pip install --user wheel
+ - PYTHONUSERBASE=$(pwd)/mason_packages/.link pip install --user twine
+ - python --version
+
+install:
+ - python setup.py install --prefix $(pwd)/mason_packages/.link
+
+before_script:
+ - python test/run_tests.py -q
+
+script:
+ - python test/visual.py -q
+ - if [[ ${COMMIT_MESSAGE} =~ "[publish]" ]]; then
+ python setup.py bdist_wheel;
+ if [[ $(uname -s) == 'Linux' ]]; then
+ export PRE_DISTS='dist/*.whl';
+ rename 's/linux_x86_64/any/;' $PRE_DISTS;
+ fi;
+ export DISTS='dist/*';
+ $(pwd)/mason_packages/.link/bin/twine upload -u $PYPI_USER -p $PYPI_PASSWORD $DISTS ;
+ fi;
diff --git a/AUTHORS.md b/AUTHORS.md
new file mode 100644
index 0000000..794bf76
--- /dev/null
+++ b/AUTHORS.md
@@ -0,0 +1,5 @@
+## Mapnik Python Binding Contributors
+
+* Artem Pavlenko
+* Dane Springmeyer
+* Blake Thompson
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..8727fb4
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,6 @@
+# Mapnik Python
+
+# Version 0.1.0
+
+ - Intial python bindings seperate from those of the core mapnik code
+ - For changes previous to this please see the core mapnik changelog
diff --git a/COPYING b/COPYING
new file mode 100644
index 0000000..e5ab03e
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,502 @@
+ GNU LESSER GENERAL PUBLIC LICENSE
+ Version 2.1, February 1999
+
+ Copyright (C) 1991, 1999 Free Software Foundation, Inc.
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+[This is the first released version of the Lesser GPL. It also counts
+ as the successor of the GNU Library Public License, version 2, hence
+ the version number 2.1.]
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+Licenses are intended to guarantee your freedom to share and change
+free software--to make sure the software is free for all its users.
+
+ This license, the Lesser General Public License, applies to some
+specially designated software packages--typically libraries--of the
+Free Software Foundation and other authors who decide to use it. You
+can use it too, but we suggest you first think carefully about whether
+this license or the ordinary General Public License is the better
+strategy to use in any particular case, based on the explanations below.
+
+ When we speak of free software, we are referring to freedom of use,
+not price. Our General Public Licenses are designed to make sure that
+you have the freedom to distribute copies of free software (and charge
+for this service if you wish); that you receive source code or can get
+it if you want it; that you can change the software and use pieces of
+it in new free programs; and that you are informed that you can do
+these things.
+
+ To protect your rights, we need to make restrictions that forbid
+distributors to deny you these rights or to ask you to surrender these
+rights. These restrictions translate to certain responsibilities for
+you if you distribute copies of the library or if you modify it.
+
+ For example, if you distribute copies of the library, whether gratis
+or for a fee, you must give the recipients all the rights that we gave
+you. You must make sure that they, too, receive or can get the source
+code. If you link other code with the library, you must provide
+complete object files to the recipients, so that they can relink them
+with the library after making changes to the library and recompiling
+it. And you must show them these terms so they know their rights.
+
+ We protect your rights with a two-step method: (1) we copyright the
+library, and (2) we offer you this license, which gives you legal
+permission to copy, distribute and/or modify the library.
+
+ To protect each distributor, we want to make it very clear that
+there is no warranty for the free library. Also, if the library is
+modified by someone else and passed on, the recipients should know
+that what they have is not the original version, so that the original
+author's reputation will not be affected by problems that might be
+introduced by others.
+
+ Finally, software patents pose a constant threat to the existence of
+any free program. We wish to make sure that a company cannot
+effectively restrict the users of a free program by obtaining a
+restrictive license from a patent holder. Therefore, we insist that
+any patent license obtained for a version of the library must be
+consistent with the full freedom of use specified in this license.
+
+ Most GNU software, including some libraries, is covered by the
+ordinary GNU General Public License. This license, the GNU Lesser
+General Public License, applies to certain designated libraries, and
+is quite different from the ordinary General Public License. We use
+this license for certain libraries in order to permit linking those
+libraries into non-free programs.
+
+ When a program is linked with a library, whether statically or using
+a shared library, the combination of the two is legally speaking a
+combined work, a derivative of the original library. The ordinary
+General Public License therefore permits such linking only if the
+entire combination fits its criteria of freedom. The Lesser General
+Public License permits more lax criteria for linking other code with
+the library.
+
+ We call this license the "Lesser" General Public License because it
+does Less to protect the user's freedom than the ordinary General
+Public License. It also provides other free software developers Less
+of an advantage over competing non-free programs. These disadvantages
+are the reason we use the ordinary General Public License for many
+libraries. However, the Lesser license provides advantages in certain
+special circumstances.
+
+ For example, on rare occasions, there may be a special need to
+encourage the widest possible use of a certain library, so that it becomes
+a de-facto standard. To achieve this, non-free programs must be
+allowed to use the library. A more frequent case is that a free
+library does the same job as widely used non-free libraries. In this
+case, there is little to gain by limiting the free library to free
+software only, so we use the Lesser General Public License.
+
+ In other cases, permission to use a particular library in non-free
+programs enables a greater number of people to use a large body of
+free software. For example, permission to use the GNU C Library in
+non-free programs enables many more people to use the whole GNU
+operating system, as well as its variant, the GNU/Linux operating
+system.
+
+ Although the Lesser General Public License is Less protective of the
+users' freedom, it does ensure that the user of a program that is
+linked with the Library has the freedom and the wherewithal to run
+that program using a modified version of the Library.
+
+ The precise terms and conditions for copying, distribution and
+modification follow. Pay close attention to the difference between a
+"work based on the library" and a "work that uses the library". The
+former contains code derived from the library, whereas the latter must
+be combined with the library in order to run.
+
+ GNU LESSER GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License Agreement applies to any software library or other
+program which contains a notice placed by the copyright holder or
+other authorized party saying it may be distributed under the terms of
+this Lesser General Public License (also called "this License").
+Each licensee is addressed as "you".
+
+ A "library" means a collection of software functions and/or data
+prepared so as to be conveniently linked with application programs
+(which use some of those functions and data) to form executables.
+
+ The "Library", below, refers to any such software library or work
+which has been distributed under these terms. A "work based on the
+Library" means either the Library or any derivative work under
+copyright law: that is to say, a work containing the Library or a
+portion of it, either verbatim or with modifications and/or translated
+straightforwardly into another language. (Hereinafter, translation is
+included without limitation in the term "modification".)
+
+ "Source code" for a work means the preferred form of the work for
+making modifications to it. For a library, complete source code means
+all the source code for all modules it contains, plus any associated
+interface definition files, plus the scripts used to control compilation
+and installation of the library.
+
+ Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running a program using the Library is not restricted, and output from
+such a program is covered only if its contents constitute a work based
+on the Library (independent of the use of the Library in a tool for
+writing it). Whether that is true depends on what the Library does
+and what the program that uses the Library does.
+
+ 1. You may copy and distribute verbatim copies of the Library's
+complete source code as you receive it, in any medium, provided that
+you conspicuously and appropriately publish on each copy an
+appropriate copyright notice and disclaimer of warranty; keep intact
+all the notices that refer to this License and to the absence of any
+warranty; and distribute a copy of this License along with the
+Library.
+
+ You may charge a fee for the physical act of transferring a copy,
+and you may at your option offer warranty protection in exchange for a
+fee.
+
+ 2. You may modify your copy or copies of the Library or any portion
+of it, thus forming a work based on the Library, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) The modified work must itself be a software library.
+
+ b) You must cause the files modified to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ c) You must cause the whole of the work to be licensed at no
+ charge to all third parties under the terms of this License.
+
+ d) If a facility in the modified Library refers to a function or a
+ table of data to be supplied by an application program that uses
+ the facility, other than as an argument passed when the facility
+ is invoked, then you must make a good faith effort to ensure that,
+ in the event an application does not supply such function or
+ table, the facility still operates, and performs whatever part of
+ its purpose remains meaningful.
+
+ (For example, a function in a library to compute square roots has
+ a purpose that is entirely well-defined independent of the
+ application. Therefore, Subsection 2d requires that any
+ application-supplied function or table used by this function must
+ be optional: if the application does not supply it, the square
+ root function must still compute square roots.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Library,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Library, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote
+it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Library.
+
+In addition, mere aggregation of another work not based on the Library
+with the Library (or with a work based on the Library) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may opt to apply the terms of the ordinary GNU General Public
+License instead of this License to a given copy of the Library. To do
+this, you must alter all the notices that refer to this License, so
+that they refer to the ordinary GNU General Public License, version 2,
+instead of to this License. (If a newer version than version 2 of the
+ordinary GNU General Public License has appeared, then you can specify
+that version instead if you wish.) Do not make any other change in
+these notices.
+
+ Once this change is made in a given copy, it is irreversible for
+that copy, so the ordinary GNU General Public License applies to all
+subsequent copies and derivative works made from that copy.
+
+ This option is useful when you wish to copy part of the code of
+the Library into a program that is not a library.
+
+ 4. You may copy and distribute the Library (or a portion or
+derivative of it, under Section 2) in object code or executable form
+under the terms of Sections 1 and 2 above provided that you accompany
+it with the complete corresponding machine-readable source code, which
+must be distributed under the terms of Sections 1 and 2 above on a
+medium customarily used for software interchange.
+
+ If distribution of object code is made by offering access to copy
+from a designated place, then offering equivalent access to copy the
+source code from the same place satisfies the requirement to
+distribute the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 5. A program that contains no derivative of any portion of the
+Library, but is designed to work with the Library by being compiled or
+linked with it, is called a "work that uses the Library". Such a
+work, in isolation, is not a derivative work of the Library, and
+therefore falls outside the scope of this License.
+
+ However, linking a "work that uses the Library" with the Library
+creates an executable that is a derivative of the Library (because it
+contains portions of the Library), rather than a "work that uses the
+library". The executable is therefore covered by this License.
+Section 6 states terms for distribution of such executables.
+
+ When a "work that uses the Library" uses material from a header file
+that is part of the Library, the object code for the work may be a
+derivative work of the Library even though the source code is not.
+Whether this is true is especially significant if the work can be
+linked without the Library, or if the work is itself a library. The
+threshold for this to be true is not precisely defined by law.
+
+ If such an object file uses only numerical parameters, data
+structure layouts and accessors, and small macros and small inline
+functions (ten lines or less in length), then the use of the object
+file is unrestricted, regardless of whether it is legally a derivative
+work. (Executables containing this object code plus portions of the
+Library will still fall under Section 6.)
+
+ Otherwise, if the work is a derivative of the Library, you may
+distribute the object code for the work under the terms of Section 6.
+Any executables containing that work also fall under Section 6,
+whether or not they are linked directly with the Library itself.
+
+ 6. As an exception to the Sections above, you may also combine or
+link a "work that uses the Library" with the Library to produce a
+work containing portions of the Library, and distribute that work
+under terms of your choice, provided that the terms permit
+modification of the work for the customer's own use and reverse
+engineering for debugging such modifications.
+
+ You must give prominent notice with each copy of the work that the
+Library is used in it and that the Library and its use are covered by
+this License. You must supply a copy of this License. If the work
+during execution displays copyright notices, you must include the
+copyright notice for the Library among them, as well as a reference
+directing the user to the copy of this License. Also, you must do one
+of these things:
+
+ a) Accompany the work with the complete corresponding
+ machine-readable source code for the Library including whatever
+ changes were used in the work (which must be distributed under
+ Sections 1 and 2 above); and, if the work is an executable linked
+ with the Library, with the complete machine-readable "work that
+ uses the Library", as object code and/or source code, so that the
+ user can modify the Library and then relink to produce a modified
+ executable containing the modified Library. (It is understood
+ that the user who changes the contents of definitions files in the
+ Library will not necessarily be able to recompile the application
+ to use the modified definitions.)
+
+ b) Use a suitable shared library mechanism for linking with the
+ Library. A suitable mechanism is one that (1) uses at run time a
+ copy of the library already present on the user's computer system,
+ rather than copying library functions into the executable, and (2)
+ will operate properly with a modified version of the library, if
+ the user installs one, as long as the modified version is
+ interface-compatible with the version that the work was made with.
+
+ c) Accompany the work with a written offer, valid for at
+ least three years, to give the same user the materials
+ specified in Subsection 6a, above, for a charge no more
+ than the cost of performing this distribution.
+
+ d) If distribution of the work is made by offering access to copy
+ from a designated place, offer equivalent access to copy the above
+ specified materials from the same place.
+
+ e) Verify that the user has already received a copy of these
+ materials or that you have already sent this user a copy.
+
+ For an executable, the required form of the "work that uses the
+Library" must include any data and utility programs needed for
+reproducing the executable from it. However, as a special exception,
+the materials to be distributed need not include anything that is
+normally distributed (in either source or binary form) with the major
+components (compiler, kernel, and so on) of the operating system on
+which the executable runs, unless that component itself accompanies
+the executable.
+
+ It may happen that this requirement contradicts the license
+restrictions of other proprietary libraries that do not normally
+accompany the operating system. Such a contradiction means you cannot
+use both them and the Library together in an executable that you
+distribute.
+
+ 7. You may place library facilities that are a work based on the
+Library side-by-side in a single library together with other library
+facilities not covered by this License, and distribute such a combined
+library, provided that the separate distribution of the work based on
+the Library and of the other library facilities is otherwise
+permitted, and provided that you do these two things:
+
+ a) Accompany the combined library with a copy of the same work
+ based on the Library, uncombined with any other library
+ facilities. This must be distributed under the terms of the
+ Sections above.
+
+ b) Give prominent notice with the combined library of the fact
+ that part of it is a work based on the Library, and explaining
+ where to find the accompanying uncombined form of the same work.
+
+ 8. You may not copy, modify, sublicense, link with, or distribute
+the Library except as expressly provided under this License. Any
+attempt otherwise to copy, modify, sublicense, link with, or
+distribute the Library is void, and will automatically terminate your
+rights under this License. However, parties who have received copies,
+or rights, from you under this License will not have their licenses
+terminated so long as such parties remain in full compliance.
+
+ 9. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Library or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Library (or any work based on the
+Library), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Library or works based on it.
+
+ 10. Each time you redistribute the Library (or any work based on the
+Library), the recipient automatically receives a license from the
+original licensor to copy, distribute, link with or modify the Library
+subject to these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties with
+this License.
+
+ 11. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Library at all. For example, if a patent
+license would not permit royalty-free redistribution of the Library by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Library.
+
+If any portion of this section is held invalid or unenforceable under any
+particular circumstance, the balance of the section is intended to apply,
+and the section as a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 12. If the distribution and/or use of the Library is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Library under this License may add
+an explicit geographical distribution limitation excluding those countries,
+so that distribution is permitted only in or among countries not thus
+excluded. In such case, this License incorporates the limitation as if
+written in the body of this License.
+
+ 13. The Free Software Foundation may publish revised and/or new
+versions of the Lesser General Public License from time to time.
+Such new versions will be similar in spirit to the present version,
+but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Library
+specifies a version number of this License which applies to it and
+"any later version", you have the option of following the terms and
+conditions either of that version or of any later version published by
+the Free Software Foundation. If the Library does not specify a
+license version number, you may choose any version ever published by
+the Free Software Foundation.
+
+ 14. If you wish to incorporate parts of the Library into other free
+programs whose distribution conditions are incompatible with these,
+write to the author to ask for permission. For software which is
+copyrighted by the Free Software Foundation, write to the Free
+Software Foundation; we sometimes make exceptions for this. Our
+decision will be guided by the two goals of preserving the free status
+of all derivatives of our free software and of promoting the sharing
+and reuse of software generally.
+
+ NO WARRANTY
+
+ 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
+WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
+KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
+LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
+THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
+WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
+AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
+FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
+CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
+LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Libraries
+
+ If you develop a new library, and you want it to be of the greatest
+possible use to the public, we recommend making it free software that
+everyone can redistribute and change. You can do so by permitting
+redistribution under these terms (or, alternatively, under the terms of the
+ordinary General Public License).
+
+ To apply these terms, attach the following notices to the library. It is
+safest to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least the
+"copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the library's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ 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 Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+Also add information on how to contact you by electronic and paper mail.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the library, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the
+ library `Frob' (a library for tweaking knobs) written by James Random Hacker.
+
+ <signature of Ty Coon>, 1 April 1990
+ Ty Coon, President of Vice
+
+That's all there is to it!
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..502d8c5
--- /dev/null
+++ b/README.md
@@ -0,0 +1,52 @@
+
+[![Build Status](https://travis-ci.org/mapnik/python-mapnik.svg)](https://travis-ci.org/mapnik/python-mapnik)
+
+Python bindings for Mapnik.
+
+## Installation
+
+Eventually we hope that many people will simply be able to `pip install mapnik` in order to get prebuilt binaries,
+this currently does not work though. So for now here are the instructions
+
+### Create a virtual environment
+
+It is highly suggested that you [a python virtualenv](http://docs.python-guide.org/en/latest/dev/virtualenvs/) when developing
+on mapnik.
+
+### Building from Mason
+
+If you do not have mapnik built from source and simply wish to develop from the latest version in [mapnik master branch](https://github.com/mapnik/mapnik) you can setup your environment with a mason build. In order to trigger a mason build prior to building you must set the `MASON_BUILD` environment variable.
+
+```bash
+export MASON_BUILD=true
+```
+
+After this is done simply follow the directions as per a source build.
+
+### Building from Source
+
+Assuming that you built your own mapnik from source, and you have run `make install`. Set any compiler or linking environment variables as necessary so that your installation of mapnik is found. Next simply run one of the two methods:
+
+```
+python setup.py develop
+```
+
+If you wish to are currently developing on mapnik-python and wish to change the code in place and immediately have python changes reflected in your environment.
+
+```
+python setup.py install
+```
+
+If you wish to just install the package
+
+## Testing
+
+Once you have installed you can test the package by running:
+
+```
+git submodule update --init
+python setup.py test
+```
+
+The test data in `./test/data` and `./test/data-visual` are standalone modules. If you need to update them see https://github.com/mapnik/mapnik/blob/master/docs/contributing.markdown#testing
+
diff --git a/bootstrap.sh b/bootstrap.sh
new file mode 100755
index 0000000..806f3f5
--- /dev/null
+++ b/bootstrap.sh
@@ -0,0 +1,70 @@
+#!/usr/bin/env bash
+
+function setup_mason() {
+ if [[ ! -d ./.mason ]]; then
+ git clone --depth 1 https://github.com/mapbox/mason.git ./.mason
+ else
+ echo "Updating to latest mason"
+ (cd ./.mason && git pull)
+ fi
+ export MASON_DIR=$(pwd)/.mason
+ export PATH=$(pwd)/.mason:$PATH
+ export CXX=${CXX:-clang++}
+ export CC=${CXX:-clang++}
+}
+
+function install() {
+ MASON_PLATFORM_ID=$(mason env MASON_PLATFORM_ID)
+ if [[ ! -d ./mason_packages/${MASON_PLATFORM_ID}/${1}/ ]]; then
+ mason install $1 $2
+ mason link $1 $2
+ fi
+}
+
+function install_mason_deps() {
+ install mapnik 3.0.0-rc3
+ install protobuf 2.6.1
+ install freetype 2.5.4
+ install harfbuzz 2cd5323
+ install jpeg_turbo 1.4.0
+ install libxml2 2.9.2
+ install libpng 1.6.16
+ install webp 0.4.2
+ install icu 54.1
+ install proj 4.8.0
+ install libtiff 4.0.4beta
+ install boost 1.57.0
+ install boost_libsystem 1.57.0
+ install boost_libthread 1.57.0
+ install boost_libfilesystem 1.57.0
+ install boost_libprogram_options 1.57.0
+ install boost_libpython 1.57.0
+ install boost_libregex 1.57.0
+ install boost_libpython 1.57.0
+ install pixman 0.32.6
+ install cairo 1.12.18
+}
+
+function setup_runtime_settings() {
+ local MASON_LINKED_ABS=$(pwd)/mason_packages/.link
+ export PROJ_LIB=${MASON_LINKED_ABS}/share/proj
+ export ICU_DATA=${MASON_LINKED_ABS}/share/icu/54.1
+ export GDAL_DATA=${MASON_LINKED_ABS}/share/gdal
+ if [[ $(uname -s) == 'Darwin' ]]; then
+ export DYLD_LIBRARY_PATH=$(pwd)/mason_packages/.link/lib:${DYLD_LIBRARY_PATH}
+ else
+ export LD_LIBRARY_PATH=$(pwd)/mason_packages/.link/lib:${LD_LIBRARY_PATH}
+ fi
+ export PATH=$(pwd)/mason_packages/.link/bin:${PATH}
+}
+
+function main() {
+ setup_mason
+ install_mason_deps
+ setup_runtime_settings
+ echo "Ready, now run:"
+ echo ""
+ echo " make test"
+}
+
+main
diff --git a/build.py b/build.py
new file mode 100644
index 0000000..0f94826
--- /dev/null
+++ b/build.py
@@ -0,0 +1,120 @@
+import glob
+import os
+from subprocess import Popen, PIPE
+from distutils import sysconfig
+
+Import('env')
+
+def call(cmd, silent=True):
+ stdin, stderr = Popen(cmd, shell=True, stdout=PIPE, stderr=PIPE).communicate()
+ if not stderr:
+ return stdin.strip()
+ elif not silent:
+ print stderr
+
+
+prefix = env['PREFIX']
+target_path = os.path.normpath(sysconfig.get_python_lib() + os.path.sep + env['MAPNIK_NAME'])
+
+py_env = env.Clone()
+
+py_env.Append(CPPPATH = sysconfig.get_python_inc())
+
+py_env.Append(CPPDEFINES = env['LIBMAPNIK_DEFINES'])
+
+py_env['LIBS'] = [env['MAPNIK_NAME'],'libboost_python']
+
+link_all_libs = env['LINKING'] == 'static' or env['RUNTIME_LINK'] == 'static'
+
+# even though boost_thread is no longer used in mapnik core
+# we need to link in for boost_python to avoid missing symbol: _ZN5boost6detail12get_tss_dataEPKv / boost::detail::get_tss_data
+py_env.AppendUnique(LIBS = 'boost_thread%s' % env['BOOST_APPEND'])
+
+if link_all_libs:
+ py_env.AppendUnique(LIBS=env['LIBMAPNIK_LIBS'])
+
+# note: on linux -lrt must be linked after thread to avoid: undefined symbol: clock_gettime
+if env['RUNTIME_LINK'] == 'static' and env['PLATFORM'] == 'Linux':
+ py_env.AppendUnique(LIBS='rt')
+
+# TODO - do solaris/fedora need direct linking too?
+python_link_flag = ''
+if env['PLATFORM'] == 'Darwin':
+ python_link_flag = '-undefined dynamic_lookup'
+
+paths = '''
+"""Configuration paths of Mapnik fonts and input plugins (auto-generated by SCons)."""
+
+from os.path import normpath,join,dirname
+
+mapniklibpath = '%s'
+mapniklibpath = normpath(join(dirname(__file__),mapniklibpath))
+'''
+
+paths += "inputpluginspath = join(mapniklibpath,'input')\n"
+
+if env['SYSTEM_FONTS']:
+ paths += "fontscollectionpath = normpath('%s')\n" % env['SYSTEM_FONTS']
+else:
+ paths += "fontscollectionpath = join(mapniklibpath,'fonts')\n"
+
+paths += "__all__ = [mapniklibpath,inputpluginspath,fontscollectionpath]\n"
+
+if not os.path.exists(env['MAPNIK_NAME']):
+ os.mkdir(env['MAPNIK_NAME'])
+
+file('mapnik/paths.py','w').write(paths % (env['MAPNIK_LIB_DIR']))
+
+# force open perms temporarily so that `sudo scons install`
+# does not later break simple non-install non-sudo rebuild
+try:
+ os.chmod('mapnik/paths.py',0666)
+except: pass
+
+# install the shared object beside the module directory
+sources = glob.glob('src/*.cpp')
+
+if 'install' in COMMAND_LINE_TARGETS:
+ # install the core mapnik python files, including '__init__.py'
+ init_files = glob.glob('mapnik/*.py')
+ if 'mapnik/paths.py' in init_files:
+ init_files.remove('mapnik/paths.py')
+ init_module = env.Install(target_path, init_files)
+ env.Alias(target='install', source=init_module)
+ # fix perms and install the custom generated 'paths.py'
+ targetp = os.path.join(target_path,'paths.py')
+ env.Alias("install", targetp)
+ # use env.Command rather than env.Install
+ # to enable setting proper perms on `paths.py`
+ env.Command( targetp, 'mapnik/paths.py',
+ [
+ Copy("$TARGET","$SOURCE"),
+ Chmod("$TARGET", 0644),
+ ])
+
+if 'uninstall' not in COMMAND_LINE_TARGETS:
+ if env['HAS_CAIRO']:
+ py_env.Append(CPPPATH = env['CAIRO_CPPPATHS'])
+ py_env.Append(CPPDEFINES = '-DHAVE_CAIRO')
+ if link_all_libs:
+ py_env.Append(LIBS=env['CAIRO_ALL_LIBS'])
+
+ if env['HAS_PYCAIRO']:
+ py_env.Append(CPPDEFINES = '-DHAVE_PYCAIRO')
+ py_env.Append(CPPPATH = env['PYCAIRO_PATHS'])
+
+py_env.Append(LINKFLAGS=python_link_flag)
+py_env.AppendUnique(LIBS='mapnik-json')
+py_env.AppendUnique(LIBS='mapnik-wkt')
+
+_mapnik = py_env.LoadableModule('mapnik/_mapnik', sources, LDMODULEPREFIX='', LDMODULESUFFIX='.so')
+
+Depends(_mapnik, env.subst('../../src/%s' % env['MAPNIK_LIB_NAME']))
+Depends(_mapnik, env.subst('../../src/json/libmapnik-json${LIBSUFFIX}'))
+Depends(_mapnik, env.subst('../../src/wkt/libmapnik-wkt${LIBSUFFIX}'))
+
+if 'uninstall' not in COMMAND_LINE_TARGETS:
+ pymapniklib = env.Install(target_path,_mapnik)
+ py_env.Alias(target='install',source=pymapniklib)
+
+env['create_uninstall_target'](env, target_path)
diff --git a/mapnik/__init__.py b/mapnik/__init__.py
new file mode 100644
index 0000000..3eef555
--- /dev/null
+++ b/mapnik/__init__.py
@@ -0,0 +1,1073 @@
+#
+# This file is part of Mapnik (C++/Python mapping toolkit)
+# Copyright (C) 2014 Artem Pavlenko
+#
+# Mapnik is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or any later version.
+#
+# This program 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+#
+
+"""Mapnik Python module.
+
+Boost Python bindings to the Mapnik C++ shared library.
+
+Several things happen when you do:
+
+ >>> import mapnik
+
+ 1) Mapnik C++ objects are imported via the '__init__.py' from the '_mapnik.so' shared object
+ (_mapnik.pyd on win) which references libmapnik.so (linux), libmapnik.dylib (mac), or
+ mapnik.dll (win32).
+
+ 2) The paths to the input plugins and font directories are imported from the 'paths.py'
+ file which was constructed and installed during SCons installation.
+
+ 3) All available input plugins and TrueType fonts are automatically registered.
+
+ 4) Boost Python metaclass injectors are used in the '__init__.py' to extend several
+ objects adding extra convenience when accessed via Python.
+
+"""
+
+import itertools
+import os
+import warnings
+try:
+ import json
+except ImportError:
+ import simplejson as json
+
+def bootstrap_env():
+ """
+ If an optional settings file exists, inherit its
+ environment settings before loading the mapnik library.
+
+ This feature is intended for customized packages of mapnik.
+
+ The settings file should be a python file with an 'env' variable
+ that declares a dictionary of key:value pairs to push into the
+ global process environment, if not already set, like:
+
+ env = {'ICU_DATA':'/usr/local/share/icu/'}
+ """
+ if os.path.exists(os.path.join(os.path.dirname(__file__),'mapnik_settings.py')):
+ from mapnik_settings import env
+ process_keys = os.environ.keys()
+ for key, value in env.items():
+ if key not in process_keys:
+ os.environ[key] = value
+
+bootstrap_env()
+
+from _mapnik import *
+
+import printing
+printing.renderer = render
+
+# The base Boost.Python class
+BoostPythonMetaclass = Coord.__class__
+
+class _MapnikMetaclass(BoostPythonMetaclass):
+ def __init__(self, name, bases, dict):
+ for b in bases:
+ if type(b) not in (self, type):
+ for k,v in list(dict.items()):
+ if hasattr(b, k):
+ setattr(b, '_c_'+k, getattr(b, k))
+ setattr(b,k,v)
+ return type.__init__(self, name, bases, dict)
+
+# metaclass injector compatible with both python 2 and 3
+# http://mikewatkins.ca/2008/11/29/python-2-and-3-metaclasses/
+_injector = _MapnikMetaclass('_injector', (object, ), {})
+
+def Filter(*args,**kwargs):
+ warnings.warn("'Filter' is deprecated and will be removed in Mapnik 3.x, use 'Expression' instead",
+ DeprecationWarning, 2)
+ return Expression(*args, **kwargs)
+
+class Envelope(Box2d):
+ def __init__(self, *args, **kwargs):
+ warnings.warn("'Envelope' is deprecated and will be removed in Mapnik 3.x, use 'Box2d' instead",
+ DeprecationWarning, 2)
+ Box2d.__init__(self, *args, **kwargs)
+
+class _Coord(Coord,_injector):
+ """
+ Represents a point with two coordinates (either lon/lat or x/y).
+
+ Following operators are defined for Coord:
+
+ Addition and subtraction of Coord objects:
+
+ >>> Coord(10, 10) + Coord(20, 20)
+ Coord(30.0, 30.0)
+ >>> Coord(10, 10) - Coord(20, 20)
+ Coord(-10.0, -10.0)
+
+ Addition, subtraction, multiplication and division between
+ a Coord and a float:
+
+ >>> Coord(10, 10) + 1
+ Coord(11.0, 11.0)
+ >>> Coord(10, 10) - 1
+ Coord(-9.0, -9.0)
+ >>> Coord(10, 10) * 2
+ Coord(20.0, 20.0)
+ >>> Coord(10, 10) / 2
+ Coord(5.0, 5.0)
+
+ Equality of coords (as pairwise equality of components):
+ >>> Coord(10, 10) is Coord(10, 10)
+ False
+ >>> Coord(10, 10) == Coord(10, 10)
+ True
+ """
+ def __repr__(self):
+ return 'Coord(%s,%s)' % (self.x, self.y)
+
+ def forward(self, projection):
+ """
+ Projects the point from the geographic coordinate
+ space into the cartesian space. The x component is
+ considered to be longitude, the y component the
+ latitude.
+
+ Returns the easting (x) and northing (y) as a
+ coordinate pair.
+
+ Example: Project the geographic coordinates of the
+ city center of Stuttgart into the local
+ map projection (GK Zone 3/DHDN, EPSG 31467)
+ >>> p = Projection('+init=epsg:31467')
+ >>> Coord(9.1, 48.7).forward(p)
+ Coord(3507360.12813,5395719.2749)
+ """
+ return forward_(self, projection)
+
+ def inverse(self, projection):
+ """
+ Projects the point from the cartesian space
+ into the geographic space. The x component is
+ considered to be the easting, the y component
+ to be the northing.
+
+ Returns the longitude (x) and latitude (y) as a
+ coordinate pair.
+
+ Example: Project the cartesian coordinates of the
+ city center of Stuttgart in the local
+ map projection (GK Zone 3/DHDN, EPSG 31467)
+ into geographic coordinates:
+ >>> p = Projection('+init=epsg:31467')
+ >>> Coord(3507360.12813,5395719.2749).inverse(p)
+ Coord(9.1, 48.7)
+ """
+ return inverse_(self, projection)
+
+class _Box2d(Box2d,_injector):
+ """
+ Represents a spatial envelope (i.e. bounding box).
+
+
+ Following operators are defined for Box2d:
+
+ Addition:
+ e1 + e2 is equvalent to e1.expand_to_include(e2) but yields
+ a new envelope instead of modifying e1
+
+ Subtraction:
+ Currently e1 - e2 returns e1.
+
+ Multiplication and division with floats:
+ Multiplication and division change the width and height of the envelope
+ by the given factor without modifying its center..
+
+ That is, e1 * x is equivalent to:
+ e1.width(x * e1.width())
+ e1.height(x * e1.height()),
+ except that a new envelope is created instead of modifying e1.
+
+ e1 / x is equivalent to e1 * (1.0/x).
+
+ Equality: two envelopes are equal if their corner points are equal.
+ """
+
+ def __repr__(self):
+ return 'Box2d(%s,%s,%s,%s)' % \
+ (self.minx,self.miny,self.maxx,self.maxy)
+
+ def forward(self, projection):
+ """
+ Projects the envelope from the geographic space
+ into the cartesian space by projecting its corner
+ points.
+
+ See also:
+ Coord.forward(self, projection)
+ """
+ return forward_(self, projection)
+
+ def inverse(self, projection):
+ """
+ Projects the envelope from the cartesian space
+ into the geographic space by projecting its corner
+ points.
+
+ See also:
+ Coord.inverse(self, projection).
+ """
+ return inverse_(self, projection)
+
+class _Projection(Projection,_injector):
+
+ def __repr__(self):
+ return "Projection('%s')" % self.params()
+
+ def forward(self,obj):
+ """
+ Projects the given object (Box2d or Coord)
+ from the geographic space into the cartesian space.
+
+ See also:
+ Box2d.forward(self, projection),
+ Coord.forward(self, projection).
+ """
+ return forward_(obj,self)
+
+ def inverse(self,obj):
+ """
+ Projects the given object (Box2d or Coord)
+ from the cartesian space into the geographic space.
+
+ See also:
+ Box2d.inverse(self, projection),
+ Coord.inverse(self, projection).
+ """
+ return inverse_(obj,self)
+
+class _Feature(Feature,_injector):
+ __geo_interface__ = property(lambda self: json.loads(self.to_geojson()))
+
+class _Geometry(Geometry,_injector):
+ __geo_interface__ = property(lambda self: json.loads(self.to_geojson()))
+
+class _Datasource(Datasource,_injector):
+
+ def all_features(self,fields=None,variables={}):
+ query = Query(self.envelope())
+ query.set_variables(variables);
+ attributes = fields or self.fields()
+ for fld in attributes:
+ query.add_property_name(fld)
+ return self.features(query).features
+
+ def featureset(self,fields=None,variables={}):
+ query = Query(self.envelope())
+ query.set_variables(variables);
+ attributes = fields or self.fields()
+ for fld in attributes:
+ query.add_property_name(fld)
+ return self.features(query)
+
+class _Color(Color,_injector):
+ def __repr__(self):
+ return "Color(R=%d,G=%d,B=%d,A=%d)" % (self.r,self.g,self.b,self.a)
+
+class _SymbolizerBase(SymbolizerBase,_injector):
+ # back compatibility
+ @property
+ def filename(self):
+ return self['file']
+
+ @filename.setter
+ def filename(self, val):
+ self['file'] = val
+
+def _add_symbol_method_to_symbolizers(vars=globals()):
+
+ def symbol_for_subcls(self):
+ return self
+
+ def symbol_for_cls(self):
+ return getattr(self,self.type())()
+
+ for name, obj in vars.items():
+ if name.endswith('Symbolizer') and not name.startswith('_'):
+ if name == 'Symbolizer':
+ symbol = symbol_for_cls
+ else:
+ symbol = symbol_for_subcls
+ type('dummy', (obj,_injector), {'symbol': symbol})
+_add_symbol_method_to_symbolizers()
+
+def Datasource(**keywords):
+ """Wrapper around CreateDatasource.
+
+ Create a Mapnik Datasource using a dictionary of parameters.
+
+ Keywords must include:
+
+ type='plugin_name' # e.g. type='gdal'
+
+ See the convenience factory methods of each input plugin for
+ details on additional required keyword arguments.
+
+ """
+
+ return CreateDatasource(keywords)
+
+# convenience factory methods
+
+def Shapefile(**keywords):
+ """Create a Shapefile Datasource.
+
+ Required keyword arguments:
+ file -- path to shapefile without extension
+
+ Optional keyword arguments:
+ base -- path prefix (default None)
+ encoding -- file encoding (default 'utf-8')
+
+ >>> from mapnik import Shapefile, Layer
+ >>> shp = Shapefile(base='/home/mapnik/data',file='world_borders')
+ >>> lyr = Layer('Shapefile Layer')
+ >>> lyr.datasource = shp
+
+ """
+ keywords['type'] = 'shape'
+ return CreateDatasource(keywords)
+
+def CSV(**keywords):
+ """Create a CSV Datasource.
+
+ Required keyword arguments:
+ file -- path to csv
+
+ Optional keyword arguments:
+ inline -- inline CSV string (if provided 'file' argument will be ignored and non-needed)
+ base -- path prefix (default None)
+ encoding -- file encoding (default 'utf-8')
+ row_limit -- integer limit of rows to return (default: 0)
+ strict -- throw an error if an invalid row is encountered
+ escape -- The escape character to use for parsing data
+ quote -- The quote character to use for parsing data
+ separator -- The separator character to use for parsing data
+ headers -- A comma separated list of header names that can be set to add headers to data that lacks them
+ filesize_max -- The maximum filesize in MB that will be accepted
+
+ >>> from mapnik import CSV
+ >>> csv = CSV(file='test.csv')
+
+ >>> from mapnik import CSV
+ >>> csv = CSV(inline='''wkt,Name\n"POINT (120.15 48.47)","Winthrop, WA"''')
+
+ For more information see https://github.com/mapnik/mapnik/wiki/CSV-Plugin
+
+ """
+ keywords['type'] = 'csv'
+ return CreateDatasource(keywords)
+
+def GeoJSON(**keywords):
+ """Create a GeoJSON Datasource.
+
+ Required keyword arguments:
+ file -- path to json
+
+ Optional keyword arguments:
+ encoding -- file encoding (default 'utf-8')
+ base -- path prefix (default None)
+
+ >>> from mapnik import GeoJSON
+ >>> geojson = GeoJSON(file='test.json')
+
+ """
+ keywords['type'] = 'geojson'
+ return CreateDatasource(keywords)
+
+def PostGIS(**keywords):
+ """Create a PostGIS Datasource.
+
+ Required keyword arguments:
+ dbname -- database name to connect to
+ table -- table name or subselect query
+
+ *Note: if using subselects for the 'table' value consider also
+ passing the 'geometry_field' and 'srid' and 'extent_from_subquery'
+ options and/or specifying the 'geometry_table' option.
+
+ Optional db connection keyword arguments:
+ user -- database user to connect as (default: see postgres docs)
+ password -- password for database user (default: see postgres docs)
+ host -- portgres hostname (default: see postgres docs)
+ port -- postgres port (default: see postgres docs)
+ initial_size -- integer size of connection pool (default: 1)
+ max_size -- integer max of connection pool (default: 10)
+ persist_connection -- keep connection open (default: True)
+
+ Optional table-level keyword arguments:
+ extent -- manually specified data extent (comma delimited string, default: None)
+ estimate_extent -- boolean, direct PostGIS to use the faster, less accurate `estimate_extent` over `extent` (default: False)
+ extent_from_subquery -- boolean, direct Mapnik to query Postgis for the extent of the raw 'table' value (default: uses 'geometry_table')
+ geometry_table -- specify geometry table to use to look up metadata (default: automatically parsed from 'table' value)
+ geometry_field -- specify geometry field to use (default: first entry in geometry_columns)
+ srid -- specify srid to use (default: auto-detected from geometry_field)
+ row_limit -- integer limit of rows to return (default: 0)
+ cursor_size -- integer size of binary cursor to use (default: 0, no binary cursor is used)
+
+ >>> from mapnik import PostGIS, Layer
+ >>> params = dict(dbname=env['MAPNIK_NAME'],table='osm',user='postgres',password='gis')
+ >>> params['estimate_extent'] = False
+ >>> params['extent'] = '-20037508,-19929239,20037508,19929239'
+ >>> postgis = PostGIS(**params)
+ >>> lyr = Layer('PostGIS Layer')
+ >>> lyr.datasource = postgis
+
+ """
+ keywords['type'] = 'postgis'
+ return CreateDatasource(keywords)
+
+def PgRaster(**keywords):
+ """Create a PgRaster Datasource.
+
+ Required keyword arguments:
+ dbname -- database name to connect to
+ table -- table name or subselect query
+
+ *Note: if using subselects for the 'table' value consider also
+ passing the 'raster_field' and 'srid' and 'extent_from_subquery'
+ options and/or specifying the 'raster_table' option.
+
+ Optional db connection keyword arguments:
+ user -- database user to connect as (default: see postgres docs)
+ password -- password for database user (default: see postgres docs)
+ host -- portgres hostname (default: see postgres docs)
+ port -- postgres port (default: see postgres docs)
+ initial_size -- integer size of connection pool (default: 1)
+ max_size -- integer max of connection pool (default: 10)
+ persist_connection -- keep connection open (default: True)
+
+ Optional table-level keyword arguments:
+ extent -- manually specified data extent (comma delimited string, default: None)
+ estimate_extent -- boolean, direct PostGIS to use the faster, less accurate `estimate_extent` over `extent` (default: False)
+ extent_from_subquery -- boolean, direct Mapnik to query Postgis for the extent of the raw 'table' value (default: uses 'geometry_table')
+ raster_table -- specify geometry table to use to look up metadata (default: automatically parsed from 'table' value)
+ raster_field -- specify geometry field to use (default: first entry in raster_columns)
+ srid -- specify srid to use (default: auto-detected from geometry_field)
+ row_limit -- integer limit of rows to return (default: 0)
+ cursor_size -- integer size of binary cursor to use (default: 0, no binary cursor is used)
+ use_overviews -- boolean, use overviews when available (default: false)
+ prescale_rasters -- boolean, scale rasters on the db side (default: false)
+ clip_rasters -- boolean, clip rasters on the db side (default: false)
+ band -- integer, if non-zero interprets the given band (1-based offset) as a data raster (default: 0)
+
+ >>> from mapnik import PgRaster, Layer
+ >>> params = dict(dbname='mapnik',table='osm',user='postgres',password='gis')
+ >>> params['estimate_extent'] = False
+ >>> params['extent'] = '-20037508,-19929239,20037508,19929239'
+ >>> pgraster = PgRaster(**params)
+ >>> lyr = Layer('PgRaster Layer')
+ >>> lyr.datasource = pgraster
+
+ """
+ keywords['type'] = 'pgraster'
+ return CreateDatasource(keywords)
+
+def Raster(**keywords):
+ """Create a Raster (Tiff) Datasource.
+
+ Required keyword arguments:
+ file -- path to stripped or tiled tiff
+ lox -- lowest (min) x/longitude of tiff extent
+ loy -- lowest (min) y/latitude of tiff extent
+ hix -- highest (max) x/longitude of tiff extent
+ hiy -- highest (max) y/latitude of tiff extent
+
+ Hint: lox,loy,hix,hiy make a Mapnik Box2d
+
+ Optional keyword arguments:
+ base -- path prefix (default None)
+ multi -- whether the image is in tiles on disk (default False)
+
+ Multi-tiled keyword arguments:
+ x_width -- virtual image number of tiles in X direction (required)
+ y_width -- virtual image number of tiles in Y direction (required)
+ tile_size -- if an image is in tiles, how large are the tiles (default 256)
+ tile_stride -- if an image is in tiles, what's the increment between rows/cols (default 1)
+
+ >>> from mapnik import Raster, Layer
+ >>> raster = Raster(base='/home/mapnik/data',file='elevation.tif',lox=-122.8,loy=48.5,hix=-122.7,hiy=48.6)
+ >>> lyr = Layer('Tiff Layer')
+ >>> lyr.datasource = raster
+
+ """
+ keywords['type'] = 'raster'
+ return CreateDatasource(keywords)
+
+def Gdal(**keywords):
+ """Create a GDAL Raster Datasource.
+
+ Required keyword arguments:
+ file -- path to GDAL supported dataset
+
+ Optional keyword arguments:
+ base -- path prefix (default None)
+ shared -- boolean, open GdalDataset in shared mode (default: False)
+ bbox -- tuple (minx, miny, maxx, maxy). If specified, overrides the bbox detected by GDAL.
+
+ >>> from mapnik import Gdal, Layer
+ >>> dataset = Gdal(base='/home/mapnik/data',file='elevation.tif')
+ >>> lyr = Layer('GDAL Layer from TIFF file')
+ >>> lyr.datasource = dataset
+
+ """
+ keywords['type'] = 'gdal'
+ if 'bbox' in keywords:
+ if isinstance(keywords['bbox'], (tuple, list)):
+ keywords['bbox'] = ','.join([str(item) for item in keywords['bbox']])
+ return CreateDatasource(keywords)
+
+def Occi(**keywords):
+ """Create a Oracle Spatial (10g) Vector Datasource.
+
+ Required keyword arguments:
+ user -- database user to connect as
+ password -- password for database user
+ host -- oracle host to connect to (does not refer to SID in tsnames.ora)
+ table -- table name or subselect query
+
+ Optional keyword arguments:
+ initial_size -- integer size of connection pool (default 1)
+ max_size -- integer max of connection pool (default 10)
+ extent -- manually specified data extent (comma delimited string, default None)
+ estimate_extent -- boolean, direct Oracle to use the faster, less accurate estimate_extent() over extent() (default False)
+ encoding -- file encoding (default 'utf-8')
+ geometry_field -- specify geometry field (default 'GEOLOC')
+ use_spatial_index -- boolean, force the use of the spatial index (default True)
+
+ >>> from mapnik import Occi, Layer
+ >>> params = dict(host='myoracle',user='scott',password='tiger',table='test')
+ >>> params['estimate_extent'] = False
+ >>> params['extent'] = '-20037508,-19929239,20037508,19929239'
+ >>> oracle = Occi(**params)
+ >>> lyr = Layer('Oracle Spatial Layer')
+ >>> lyr.datasource = oracle
+ """
+ keywords['type'] = 'occi'
+ return CreateDatasource(keywords)
+
+def Ogr(**keywords):
+ """Create a OGR Vector Datasource.
+
+ Required keyword arguments:
+ file -- path to OGR supported dataset
+ layer -- name of layer to use within datasource (optional if layer_by_index or layer_by_sql is used)
+
+ Optional keyword arguments:
+ layer_by_index -- choose layer by index number instead of by layer name or sql.
+ layer_by_sql -- choose layer by sql query number instead of by layer name or index.
+ base -- path prefix (default None)
+ encoding -- file encoding (default 'utf-8')
+
+ >>> from mapnik import Ogr, Layer
+ >>> datasource = Ogr(base='/home/mapnik/data',file='rivers.geojson',layer='OGRGeoJSON')
+ >>> lyr = Layer('OGR Layer from GeoJSON file')
+ >>> lyr.datasource = datasource
+
+ """
+ keywords['type'] = 'ogr'
+ return CreateDatasource(keywords)
+
+def SQLite(**keywords):
+ """Create a SQLite Datasource.
+
+ Required keyword arguments:
+ file -- path to SQLite database file
+ table -- table name or subselect query
+
+ Optional keyword arguments:
+ base -- path prefix (default None)
+ encoding -- file encoding (default 'utf-8')
+ extent -- manually specified data extent (comma delimited string, default None)
+ metadata -- name of auxillary table containing record for table with xmin, ymin, xmax, ymax, and f_table_name
+ geometry_field -- name of geometry field (default 'the_geom')
+ key_field -- name of primary key field (default 'OGC_FID')
+ row_offset -- specify a custom integer row offset (default 0)
+ row_limit -- specify a custom integer row limit (default 0)
+ wkb_format -- specify a wkb type of 'spatialite' (default None)
+ use_spatial_index -- boolean, instruct sqlite plugin to use Rtree spatial index (default True)
+
+ >>> from mapnik import SQLite, Layer
+ >>> sqlite = SQLite(base='/home/mapnik/data',file='osm.db',table='osm',extent='-20037508,-19929239,20037508,19929239')
+ >>> lyr = Layer('SQLite Layer')
+ >>> lyr.datasource = sqlite
+
+ """
+ keywords['type'] = 'sqlite'
+ return CreateDatasource(keywords)
+
+def Rasterlite(**keywords):
+ """Create a Rasterlite Datasource.
+
+ Required keyword arguments:
+ file -- path to Rasterlite database file
+ table -- table name or subselect query
+
+ Optional keyword arguments:
+ base -- path prefix (default None)
+ extent -- manually specified data extent (comma delimited string, default None)
+
+ >>> from mapnik import Rasterlite, Layer
+ >>> rasterlite = Rasterlite(base='/home/mapnik/data',file='osm.db',table='osm',extent='-20037508,-19929239,20037508,19929239')
+ >>> lyr = Layer('Rasterlite Layer')
+ >>> lyr.datasource = rasterlite
+
+ """
+ keywords['type'] = 'rasterlite'
+ return CreateDatasource(keywords)
+
+def Osm(**keywords):
+ """Create a Osm Datasource.
+
+ Required keyword arguments:
+ file -- path to OSM file
+
+ Optional keyword arguments:
+ encoding -- file encoding (default 'utf-8')
+ url -- url to fetch data (default None)
+ bbox -- data bounding box for fetching data (default None)
+
+ >>> from mapnik import Osm, Layer
+ >>> datasource = Osm(file='test.osm')
+ >>> lyr = Layer('Osm Layer')
+ >>> lyr.datasource = datasource
+
+ """
+ # note: parser only supports libxml2 so not exposing option
+ # parser -- xml parser to use (default libxml2)
+ keywords['type'] = 'osm'
+ return CreateDatasource(keywords)
+
+def Python(**keywords):
+ """Create a Python Datasource.
+
+ >>> from mapnik import Python, PythonDatasource
+ >>> datasource = Python('PythonDataSource')
+ >>> lyr = Layer('Python datasource')
+ >>> lyr.datasource = datasource
+ """
+ keywords['type'] = 'python'
+ return CreateDatasource(keywords)
+
+def MemoryDatasource(**keywords):
+ """Create a Memory Datasource.
+
+ Optional keyword arguments:
+ (TODO)
+ """
+ params = Parameters()
+ params.append(Parameter('type','memory'))
+ return MemoryDatasourceBase(params)
+
+class PythonDatasource(object):
+ """A base class for a Python data source.
+
+ Optional arguments:
+ envelope -- a mapnik.Box2d (minx, miny, maxx, maxy) envelope of the data source, default (-180,-90,180,90)
+ geometry_type -- one of the DataGeometryType enumeration values, default Point
+ data_type -- one of the DataType enumerations, default Vector
+ """
+ def __init__(self, envelope=None, geometry_type=None, data_type=None):
+ self.envelope = envelope or Box2d(-180, -90, 180, 90)
+ self.geometry_type = geometry_type or DataGeometryType.Point
+ self.data_type = data_type or DataType.Vector
+
+ def features(self, query):
+ """Return an iterable which yields instances of Feature for features within the passed query.
+
+ Required arguments:
+ query -- a Query instance specifying the region for which features should be returned
+ """
+ return None
+
+ def features_at_point(self, point):
+ """Rarely uses. Return an iterable which yields instances of Feature for the specified point."""
+ return None
+
+ @classmethod
+ def wkb_features(cls, keys, features):
+ """A convenience function to wrap an iterator yielding pairs of WKB format geometry and dictionaries of
+ key-value pairs into mapnik features. Return this from PythonDatasource.features() passing it a sequence of keys
+ to appear in the output and an iterator yielding features.
+
+ For example. One might have a features() method in a derived class like the following:
+
+ def features(self, query):
+ # ... create WKB features feat1 and feat2
+
+ return mapnik.PythonDatasource.wkb_features(
+ keys = ( 'name', 'author' ),
+ features = [
+ (feat1, { 'name': 'feat1', 'author': 'alice' }),
+ (feat2, { 'name': 'feat2', 'author': 'bob' }),
+ ]
+ )
+
+ """
+ ctx = Context()
+ [ctx.push(x) for x in keys]
+
+ def make_it(feat, idx):
+ f = Feature(ctx, idx)
+ geom, attrs = feat
+ f.add_geometries_from_wkb(geom)
+ for k, v in attrs.iteritems():
+ f[k] = v
+ return f
+
+ return itertools.imap(make_it, features, itertools.count(1))
+
+ @classmethod
+ def wkt_features(cls, keys, features):
+ """A convenience function to wrap an iterator yielding pairs of WKT format geometry and dictionaries of
+ key-value pairs into mapnik features. Return this from PythonDatasource.features() passing it a sequence of keys
+ to appear in the output and an iterator yielding features.
+
+ For example. One might have a features() method in a derived class like the following:
+
+ def features(self, query):
+ # ... create WKT features feat1 and feat2
+
+ return mapnik.PythonDatasource.wkt_features(
+ keys = ( 'name', 'author' ),
+ features = [
+ (feat1, { 'name': 'feat1', 'author': 'alice' }),
+ (feat2, { 'name': 'feat2', 'author': 'bob' }),
+ ]
+ )
+
+ """
+ ctx = Context()
+ [ctx.push(x) for x in keys]
+
+ def make_it(feat, idx):
+ f = Feature(ctx, idx)
+ geom, attrs = feat
+ f.add_geometries_from_wkt(geom)
+ for k, v in attrs.iteritems():
+ f[k] = v
+ return f
+
+ return itertools.imap(make_it, features, itertools.count(1))
+
+class _TextSymbolizer(TextSymbolizer,_injector):
+ @property
+ def name(self):
+ if isinstance(self.properties.format_tree, FormattingText):
+ return self.properties.format_tree.text
+ else:
+ # There is no single expression which could be returned as name
+ raise RuntimeError("TextSymbolizer uses complex formatting features, but old compatibility interface is used to access it. Use self.properties.format_tree instead.")
+
+ @name.setter
+ def name(self, name):
+ self.properties.format_tree = FormattingText(name)
+
+ @property
+ def text_size(self):
+ return self.format.text_size
+
+ @text_size.setter
+ def text_size(self, text_size):
+ self.format.text_size = text_size
+
+ @property
+ def face_name(self):
+ return self.format.face_name
+
+ @face_name.setter
+ def face_name(self, face_name):
+ self.format.face_name = face_name
+
+
+ @property
+ def fontset(self):
+ return self.format.fontset
+
+ @fontset.setter
+ def fontset(self, fontset):
+ self.format.fontset = fontset
+
+
+ @property
+ def character_spacing(self):
+ return self.format.character_spacing
+
+ @character_spacing.setter
+ def character_spacing(self, character_spacing):
+ self.format.character_spacing = character_spacing
+
+
+ @property
+ def line_spacing(self):
+ return self.format.line_spacing
+
+ @line_spacing.setter
+ def line_spacing(self, line_spacing):
+ self.format.line_spacing = line_spacing
+
+
+ @property
+ def text_opacity(self):
+ return self.format.text_opacity
+
+ @text_opacity.setter
+ def text_opacity(self, text_opacity):
+ self.format.text_opacity = text_opacity
+
+
+ @property
+ def wrap_before(self):
+ return self.format.wrap_before
+
+ @wrap_before.setter
+ def wrap_before(self, wrap_before):
+ self.format.wrap_before = wrap_before
+
+
+ @property
+ def text_transform(self):
+ return self.format.text_transform
+
+ @text_transform.setter
+ def text_transform(self, text_transform):
+ self.format.text_transform = text_transform
+
+
+ @property
+ def fill(self):
+ return self.format.fill
+
+ @fill.setter
+ def fill(self, fill):
+ self.format.fill = fill
+
+
+ @property
+ def halo_fill(self):
+ return self.format.halo_fill
+
+ @halo_fill.setter
+ def halo_fill(self, halo_fill):
+ self.format.halo_fill = halo_fill
+
+
+
+ @property
+ def halo_radius(self):
+ return self.format.halo_radius
+
+ @halo_radius.setter
+ def halo_radius(self, halo_radius):
+ self.format.halo_radius = halo_radius
+
+
+ @property
+ def label_placement(self):
+ return self.properties.label_placement
+
+ @label_placement.setter
+ def label_placement(self, label_placement):
+ self.properties.label_placement = label_placement
+
+
+
+ @property
+ def horizontal_alignment(self):
+ return self.properties.horizontal_alignment
+
+ @horizontal_alignment.setter
+ def horizontal_alignment(self, horizontal_alignment):
+ self.properties.horizontal_alignment = horizontal_alignment
+
+
+
+ @property
+ def justify_alignment(self):
+ return self.properties.justify_alignment
+
+ @justify_alignment.setter
+ def justify_alignment(self, justify_alignment):
+ self.properties.justify_alignment = justify_alignment
+
+
+
+ @property
+ def vertical_alignment(self):
+ return self.properties.vertical_alignment
+
+ @vertical_alignment.setter
+ def vertical_alignment(self, vertical_alignment):
+ self.properties.vertical_alignment = vertical_alignment
+
+
+
+ @property
+ def orientation(self):
+ return self.properties.orientation
+
+ @orientation.setter
+ def orientation(self, orientation):
+ self.properties.orientation = orientation
+
+
+
+ @property
+ def displacement(self):
+ return self.properties.displacement
+
+ @displacement.setter
+ def displacement(self, displacement):
+ self.properties.displacement = displacement
+
+
+
+ @property
+ def label_spacing(self):
+ return self.properties.label_spacing
+
+ @label_spacing.setter
+ def label_spacing(self, label_spacing):
+ self.properties.label_spacing = label_spacing
+
+
+
+ @property
+ def label_position_tolerance(self):
+ return self.properties.label_position_tolerance
+
+ @label_position_tolerance.setter
+ def label_position_tolerance(self, label_position_tolerance):
+ self.properties.label_position_tolerance = label_position_tolerance
+
+
+
+ @property
+ def avoid_edges(self):
+ return self.properties.avoid_edges
+
+ @avoid_edges.setter
+ def avoid_edges(self, avoid_edges):
+ self.properties.avoid_edges = avoid_edges
+
+
+
+ @property
+ def minimum_distance(self):
+ return self.properties.minimum_distance
+
+ @minimum_distance.setter
+ def minimum_distance(self, minimum_distance):
+ self.properties.minimum_distance = minimum_distance
+
+
+
+ @property
+ def minimum_padding(self):
+ return self.properties.minimum_padding
+
+ @minimum_padding.setter
+ def minimum_padding(self, minimum_padding):
+ self.properties.minimum_padding = minimum_padding
+
+
+
+ @property
+ def minimum_path_length(self):
+ return self.properties.minimum_path_length
+
+ @minimum_path_length.setter
+ def minimum_path_length(self, minimum_path_length):
+ self.properties.minimum_path_length = minimum_path_length
+
+
+
+ @property
+ def maximum_angle_char_delta(self):
+ return self.properties.maximum_angle_char_delta
+
+ @maximum_angle_char_delta.setter
+ def maximum_angle_char_delta(self, maximum_angle_char_delta):
+ self.properties.maximum_angle_char_delta = maximum_angle_char_delta
+
+
+ @property
+ def allow_overlap(self):
+ return self.properties.allow_overlap
+
+ @allow_overlap.setter
+ def allow_overlap(self, allow_overlap):
+ self.properties.allow_overlap = allow_overlap
+
+
+
+ @property
+ def text_ratio(self):
+ return self.properties.text_ratio
+
+ @text_ratio.setter
+ def text_ratio(self, text_ratio):
+ self.properties.text_ratio = text_ratio
+
+
+
+ @property
+ def wrap_width(self):
+ return self.properties.wrap_width
+
+ @wrap_width.setter
+ def wrap_width(self, wrap_width):
+ self.properties.wrap_width = wrap_width
+
+
+def mapnik_version_from_string(version_string):
+ """Return the Mapnik version from a string."""
+ n = version_string.split('.')
+ return (int(n[0]) * 100000) + (int(n[1]) * 100) + (int(n[2]));
+
+def register_plugins(path=None):
+ """Register plugins located by specified path"""
+ if not path:
+ if os.environ.has_key('MAPNIK_INPUT_PLUGINS_DIRECTORY'):
+ path = os.environ.get('MAPNIK_INPUT_PLUGINS_DIRECTORY')
+ else:
+ from paths import inputpluginspath
+ path = inputpluginspath
+ DatasourceCache.register_datasources(path)
+
+def register_fonts(path=None,valid_extensions=['.ttf','.otf','.ttc','.pfa','.pfb','.ttc','.dfont','.woff']):
+ """Recursively register fonts using path argument as base directory"""
+ if not path:
+ if os.environ.has_key('MAPNIK_FONT_DIRECTORY'):
+ path = os.environ.get('MAPNIK_FONT_DIRECTORY')
+ else:
+ from paths import fontscollectionpath
+ path = fontscollectionpath
+ for dirpath, _, filenames in os.walk(path):
+ for filename in filenames:
+ if os.path.splitext(filename.lower())[1] in valid_extensions:
+ FontEngine.instance().register_font(os.path.join(dirpath, filename))
+
+# auto-register known plugins and fonts
+register_plugins()
+register_fonts()
diff --git a/mapnik/mapnik_settings.py b/mapnik/mapnik_settings.py
new file mode 100644
index 0000000..6c48cea
--- /dev/null
+++ b/mapnik/mapnik_settings.py
@@ -0,0 +1,13 @@
+import os
+mapnik_data_dir = os.path.dirname(os.path.realpath(__file__))
+
+env = {}
+icu_path = os.path.join(mapnik_data_dir, 'plugins', 'icu')
+if os.path.isdir(icu_path):
+ env['ICU_DATA'] = icu_path
+gdal_path = os.path.join(mapnik_data_dir, 'plugins', 'gdal')
+if os.path.isdir(gdal_path):
+ env['GDAL_DATA'] = gdal_path
+proj_path = os.path.join(mapnik_data_dir, 'plugins', 'proj')
+if os.path.isdir(proj_path):
+ env['PROJ_LIB'] = proj_path
diff --git a/mapnik/printing.py b/mapnik/printing.py
new file mode 100644
index 0000000..e61f7c0
--- /dev/null
+++ b/mapnik/printing.py
@@ -0,0 +1,1027 @@
+# -*- coding: utf-8 -*-
+
+"""Mapnik classes to assist in creating printable maps
+
+basic usage is along the lines of
+
+import mapnik
+
+page = mapnik.printing.PDFPrinter()
+m = mapnik.Map(100,100)
+mapnik.load_map(m, "my_xml_map_description", True)
+m.zoom_all()
+page.render_map(m,"my_output_file.pdf")
+
+see the documentation of mapnik.printing.PDFPrinter() for options
+
+"""
+from __future__ import absolute_import
+
+from . import render, Map, Box2d, Layer, Feature, Projection, Coord, Style, Geometry
+import math
+import os
+import tempfile
+
+try:
+ import cairo
+ HAS_PYCAIRO_MODULE = True
+except ImportError:
+ HAS_PYCAIRO_MODULE = False
+
+try:
+ import pangocairo
+ import pango
+ HAS_PANGOCAIRO_MODULE = True
+except ImportError:
+ HAS_PANGOCAIRO_MODULE = False
+
+try:
+ import pyPdf
+ HAS_PYPDF = True
+except ImportError:
+ HAS_PYPDF = False
+
+class centering:
+ """Style of centering to use with the map, the default is constrained
+
+ none: map will be placed flush with the margin/box in the top left corner
+ constrained: map will be centered on the most constrained axis (for a portrait page
+ and a square map this will be horizontally)
+ unconstrained: map will be centered on the unconstrained axis
+ vertical:
+ horizontal:
+ both:
+ """
+ none=0
+ constrained=1
+ unconstrained=2
+ vertical=3
+ horizontal=4
+ both=5
+
+"""Some predefined page sizes custom sizes can also be passed
+a tuple of the page width and height in meters"""
+pagesizes = {
+ "a0": (0.841000,1.189000),
+ "a0l": (1.189000,0.841000),
+ "b0": (1.000000,1.414000),
+ "b0l": (1.414000,1.000000),
+ "c0": (0.917000,1.297000),
+ "c0l": (1.297000,0.917000),
+ "a1": (0.594000,0.841000),
+ "a1l": (0.841000,0.594000),
+ "b1": (0.707000,1.000000),
+ "b1l": (1.000000,0.707000),
+ "c1": (0.648000,0.917000),
+ "c1l": (0.917000,0.648000),
+ "a2": (0.420000,0.594000),
+ "a2l": (0.594000,0.420000),
+ "b2": (0.500000,0.707000),
+ "b2l": (0.707000,0.500000),
+ "c2": (0.458000,0.648000),
+ "c2l": (0.648000,0.458000),
+ "a3": (0.297000,0.420000),
+ "a3l": (0.420000,0.297000),
+ "b3": (0.353000,0.500000),
+ "b3l": (0.500000,0.353000),
+ "c3": (0.324000,0.458000),
+ "c3l": (0.458000,0.324000),
+ "a4": (0.210000,0.297000),
+ "a4l": (0.297000,0.210000),
+ "b4": (0.250000,0.353000),
+ "b4l": (0.353000,0.250000),
+ "c4": (0.229000,0.324000),
+ "c4l": (0.324000,0.229000),
+ "a5": (0.148000,0.210000),
+ "a5l": (0.210000,0.148000),
+ "b5": (0.176000,0.250000),
+ "b5l": (0.250000,0.176000),
+ "c5": (0.162000,0.229000),
+ "c5l": (0.229000,0.162000),
+ "a6": (0.105000,0.148000),
+ "a6l": (0.148000,0.105000),
+ "b6": (0.125000,0.176000),
+ "b6l": (0.176000,0.125000),
+ "c6": (0.114000,0.162000),
+ "c6l": (0.162000,0.114000),
+ "a7": (0.074000,0.105000),
+ "a7l": (0.105000,0.074000),
+ "b7": (0.088000,0.125000),
+ "b7l": (0.125000,0.088000),
+ "c7": (0.081000,0.114000),
+ "c7l": (0.114000,0.081000),
+ "a8": (0.052000,0.074000),
+ "a8l": (0.074000,0.052000),
+ "b8": (0.062000,0.088000),
+ "b8l": (0.088000,0.062000),
+ "c8": (0.057000,0.081000),
+ "c8l": (0.081000,0.057000),
+ "a9": (0.037000,0.052000),
+ "a9l": (0.052000,0.037000),
+ "b9": (0.044000,0.062000),
+ "b9l": (0.062000,0.044000),
+ "c9": (0.040000,0.057000),
+ "c9l": (0.057000,0.040000),
+ "a10": (0.026000,0.037000),
+ "a10l": (0.037000,0.026000),
+ "b10": (0.031000,0.044000),
+ "b10l": (0.044000,0.031000),
+ "c10": (0.028000,0.040000),
+ "c10l": (0.040000,0.028000),
+ "letter": (0.216,0.279),
+ "letterl": (0.279,0.216),
+ "legal": (0.216,0.356),
+ "legall": (0.356,0.216),
+}
+"""size of a pt in meters"""
+pt_size=0.0254/72.0
+
+def m2pt(x):
+ """convert distance from meters to points"""
+ return x/pt_size
+
+def pt2m(x):
+ """convert distance from points to meters"""
+ return x*pt_size
+
+def m2in(x):
+ """convert distance from meters to inches"""
+ return x/0.0254
+
+def m2px(x,resolution):
+ """convert distance from meters to pixels at the given resolution in DPI/PPI"""
+ return m2in(x)*resolution
+
+class resolutions:
+ """some predefined resolutions in DPI"""
+ dpi72=72
+ dpi150=150
+ dpi300=300
+ dpi600=600
+
+def any_scale(scale):
+ """Scale helper function that allows any scale"""
+ return scale
+
+def sequence_scale(scale,scale_sequence):
+ """Default scale helper, this rounds scale to a 'sensible' value"""
+ factor = math.floor(math.log10(scale))
+ norm = scale/(10**factor)
+
+ for s in scale_sequence:
+ if norm <= s:
+ return s*10**factor
+ return scale_sequence[0]*10**(factor+1)
+
+def default_scale(scale):
+ """Default scale helper, this rounds scale to a 'sensible' value"""
+ return sequence_scale(scale, (1,1.25,1.5,1.75,2,2.5,3,4,5,6,7.5,8,9,10))
+
+def deg_min_sec_scale(scale):
+ for x in (1.0/3600,
+ 2.0/3600,
+ 5.0/3600,
+ 10.0/3600,
+ 30.0/3600,
+ 1.0/60,
+ 2.0/60,
+ 5.0/60,
+ 10.0/60,
+ 30.0/60,
+ 1,
+ 2,
+ 5,
+ 10,
+ 30,
+ 60
+ ):
+ if scale < x:
+ return x
+ else:
+ return x
+
+def format_deg_min_sec(value):
+ deg = math.floor(value)
+ min = math.floor((value-deg)/(1.0/60))
+ sec = int((value - deg*1.0/60)/1.0/3600)
+ return "%d°%d'%d\"" % (deg,min,sec)
+
+def round_grid_generator(first,last,step):
+ val = (math.floor(first / step) + 1) * step
+ yield val
+ while val < last:
+ val += step
+ yield val
+
+
+def convert_pdf_pages_to_layers(filename,output_name=None,layer_names=(),reverse_all_but_last=True):
+ """
+ opens the given multipage PDF and converts each page to be a layer in a single page PDF
+ layer_names should be a sequence of the user visible names of the layers, if not given
+ or if shorter than num pages generic names will be given to the unnamed layers
+
+ if output_name is not provided a temporary file will be used for the conversion which
+ will then be copied back over the source file.
+
+ requires pyPdf >= 1.13 to be available"""
+
+
+ if not HAS_PYPDF:
+ raise Exception("pyPdf Not available")
+
+ infile = file(filename, 'rb')
+ if output_name:
+ outfile = file(output_name, 'wb')
+ else:
+ (outfd,outfilename) = tempfile.mkstemp(dir=os.path.dirname(filename))
+ outfile = os.fdopen(outfd,'wb')
+
+ i = pyPdf.PdfFileReader(infile)
+ o = pyPdf.PdfFileWriter()
+
+ template_page_size = i.pages[0].mediaBox
+ op = o.addBlankPage(width=template_page_size.getWidth(),height=template_page_size.getHeight())
+
+ contentkey = pyPdf.generic.NameObject('/Contents')
+ resourcekey = pyPdf.generic.NameObject('/Resources')
+ propertieskey = pyPdf.generic.NameObject('/Properties')
+ op[contentkey] = pyPdf.generic.ArrayObject()
+ op[resourcekey] = pyPdf.generic.DictionaryObject()
+ properties = pyPdf.generic.DictionaryObject()
+ ocgs = pyPdf.generic.ArrayObject()
+
+ for (i, p) in enumerate(i.pages):
+ # first start an OCG for the layer
+ ocgname = pyPdf.generic.NameObject('/oc%d' % i)
+ ocgstart = pyPdf.generic.DecodedStreamObject()
+ ocgstart._data = "/OC %s BDC\n" % ocgname
+ ocgend = pyPdf.generic.DecodedStreamObject()
+ ocgend._data = "EMC\n"
+ if isinstance(p['/Contents'],pyPdf.generic.ArrayObject):
+ p[pyPdf.generic.NameObject('/Contents')].insert(0,ocgstart)
+ p[pyPdf.generic.NameObject('/Contents')].append(ocgend)
+ else:
+ p[pyPdf.generic.NameObject('/Contents')] = pyPdf.generic.ArrayObject((ocgstart,p['/Contents'],ocgend))
+
+ op.mergePage(p)
+
+ ocg = pyPdf.generic.DictionaryObject()
+ ocg[pyPdf.generic.NameObject('/Type')] = pyPdf.generic.NameObject('/OCG')
+ if len(layer_names) > i:
+ ocg[pyPdf.generic.NameObject('/Name')] = pyPdf.generic.TextStringObject(layer_names[i])
+ else:
+ ocg[pyPdf.generic.NameObject('/Name')] = pyPdf.generic.TextStringObject('Layer %d' % (i+1))
+ indirect_ocg = o._addObject(ocg)
+ properties[ocgname] = indirect_ocg
+ ocgs.append(indirect_ocg)
+
+ op[resourcekey][propertieskey] = o._addObject(properties)
+
+ ocproperties = pyPdf.generic.DictionaryObject()
+ ocproperties[pyPdf.generic.NameObject('/OCGs')] = ocgs
+ defaultview = pyPdf.generic.DictionaryObject()
+ defaultview[pyPdf.generic.NameObject('/Name')] = pyPdf.generic.TextStringObject('Default')
+ defaultview[pyPdf.generic.NameObject('/BaseState ')] = pyPdf.generic.NameObject('/ON ')
+ defaultview[pyPdf.generic.NameObject('/ON')] = ocgs
+ if reverse_all_but_last:
+ defaultview[pyPdf.generic.NameObject('/Order')] = pyPdf.generic.ArrayObject(reversed(ocgs[:-1]))
+ defaultview[pyPdf.generic.NameObject('/Order')].append(ocgs[-1])
+ else:
+ defaultview[pyPdf.generic.NameObject('/Order')] = pyPdf.generic.ArrayObject(reversed(ocgs))
+ defaultview[pyPdf.generic.NameObject('/OFF')] = pyPdf.generic.ArrayObject()
+
+ ocproperties[pyPdf.generic.NameObject('/D')] = o._addObject(defaultview)
+
+ o._root.getObject()[pyPdf.generic.NameObject('/OCProperties')] = o._addObject(ocproperties)
+
+ o.write(outfile)
+
+ outfile.close()
+ infile.close()
+
+ if not output_name:
+ os.rename(outfilename, filename)
+
+class PDFPrinter:
+ """Main class for creating PDF print outs, basically contruct an instance
+ with appropriate options and then call render_map with your mapnik map
+ """
+ def __init__(self,
+ pagesize=pagesizes["a4"],
+ margin=0.005,
+ box=None,
+ percent_box=None,
+ scale=default_scale,
+ resolution=resolutions.dpi72,
+ preserve_aspect=True,
+ centering=centering.constrained,
+ is_latlon=False,
+ use_ocg_layers=False):
+ """Creates a cairo surface and context to render a PDF with.
+
+ pagesize: tuple of page size in meters, see predefined sizes in pagessizes dict (default a4)
+ margin: page margin in meters (default 0.01)
+ box: box within the page to render the map into (will not render over margin). This should be
+ a Mapnik Box2d object. Default is the full page within the margin
+ percent_box: as per box, but specified as a percent (0->1) of the full page size. If both box
+ and percent_box are specified percent_box will be used.
+ scale: scale helper to use when rounding the map scale. This should be a function that
+ takes a single float and returns a float which is at least as large as the value
+ passed in. This is a 1:x scale.
+ resolution: the resolution to render non vector elements at (in DPI), defaults to 72 DPI
+ preserve_aspect: whether to preserve map aspect ratio. This defaults to True and it
+ is recommended you do not change it unless you know what you are doing
+ scales and so on will not work if this is False.
+ centering: Centering rules for maps where the scale rounding has reduced the map size.
+ This should be a value from the centering class. The default is to center on the
+ maps constrained axis, typically this will be horizontal for portrait pages and
+ vertical for landscape pages.
+ is_latlon: Is the map in lat lon degrees. If true magic anti meridian logic is enabled
+ use_ocg_layers: Create OCG layers in the PDF, requires pyPdf >= 1.13
+ """
+ self._pagesize = pagesize
+ self._margin = margin
+ self._box = box
+ self._scale = scale
+ self._resolution = resolution
+ self._preserve_aspect = preserve_aspect
+ self._centering = centering
+ self._is_latlon = is_latlon
+ self._use_ocg_layers = use_ocg_layers
+
+ self._s = None
+ self._layer_names = []
+ self._filename = None
+
+ self.map_box = None
+ self.scale = None
+
+ # don't both to round the scale if they are not preserving the aspect ratio
+ if not preserve_aspect:
+ self._scale = any_scale
+
+ if percent_box:
+ self._box = Box2d(percent_box[0]*pagesize[0],percent_box[1]*pagesize[1],
+ percent_box[2]*pagesize[0],percent_box[3]*pagesize[1])
+
+ if not HAS_PYCAIRO_MODULE:
+ raise Exception("PDF rendering only available when pycairo is available")
+
+ self.font_name = "DejaVu Sans"
+
+ def finish(self):
+ if self._s:
+ self._s.finish()
+ self._s = None
+
+ if self._use_ocg_layers:
+ convert_pdf_pages_to_layers(self._filename,layer_names=self._layer_names + ["Legend and Information"],reverse_all_but_last=True)
+
+ def add_geospatial_pdf_header(self,m,filename,epsg=None,wkt=None):
+ """ Postprocessing step to add geospatial PDF information to PDF file as per
+ PDF standard 1.7 extension level 3 (also in draft PDF v2 standard at time of writing)
+
+ one of either the epsg code or wkt text for the projection must be provided
+
+ Should be called *after* the page has had .finish() called"""
+ if HAS_PYPDF and (epsg or wkt):
+ infile=file(filename,'rb')
+ (outfd,outfilename) = tempfile.mkstemp(dir=os.path.dirname(filename))
+ outfile = os.fdopen(outfd,'wb')
+
+ i=pyPdf.PdfFileReader(infile)
+ o=pyPdf.PdfFileWriter()
+
+ # preserve OCProperties at document root if we have one
+ if i.trailer['/Root'].has_key(pyPdf.generic.NameObject('/OCProperties')):
+ o._root.getObject()[pyPdf.generic.NameObject('/OCProperties')] = i.trailer['/Root'].getObject()[pyPdf.generic.NameObject('/OCProperties')]
+
+ for p in i.pages:
+ gcs = pyPdf.generic.DictionaryObject()
+ gcs[pyPdf.generic.NameObject('/Type')]=pyPdf.generic.NameObject('/PROJCS')
+ if epsg:
+ gcs[pyPdf.generic.NameObject('/EPSG')]=pyPdf.generic.NumberObject(int(epsg))
+ if wkt:
+ gcs[pyPdf.generic.NameObject('/WKT')]=pyPdf.generic.TextStringObject(wkt)
+
+ measure = pyPdf.generic.DictionaryObject()
+ measure[pyPdf.generic.NameObject('/Type')]=pyPdf.generic.NameObject('/Measure')
+ measure[pyPdf.generic.NameObject('/Subtype')]=pyPdf.generic.NameObject('/GEO')
+ measure[pyPdf.generic.NameObject('/GCS')]=gcs
+ bounds=pyPdf.generic.ArrayObject()
+ for x in (0.0,0.0,0.0,1.0,1.0,1.0,1.0,0.0):
+ bounds.append(pyPdf.generic.FloatObject(str(x)))
+ measure[pyPdf.generic.NameObject('/Bounds')]=bounds
+ measure[pyPdf.generic.NameObject('/LPTS')]=bounds
+ gpts=pyPdf.generic.ArrayObject()
+
+ proj=Projection(m.srs)
+ env=m.envelope()
+ for x in ((env.minx, env.miny), (env.minx, env.maxy), (env.maxx, env.maxy), (env.maxx, env.miny)):
+ latlon_corner=proj.inverse(Coord(*x))
+ # these are in lat,lon order according to the standard
+ gpts.append(pyPdf.generic.FloatObject(str(latlon_corner.y)))
+ gpts.append(pyPdf.generic.FloatObject(str(latlon_corner.x)))
+ measure[pyPdf.generic.NameObject('/GPTS')]=gpts
+
+ vp=pyPdf.generic.DictionaryObject()
+ vp[pyPdf.generic.NameObject('/Type')]=pyPdf.generic.NameObject('/Viewport')
+ bbox=pyPdf.generic.ArrayObject()
+
+ for x in self.map_box:
+ bbox.append(pyPdf.generic.FloatObject(str(x)))
+ vp[pyPdf.generic.NameObject('/BBox')]=bbox
+ vp[pyPdf.generic.NameObject('/Measure')]=measure
+
+ vpa = pyPdf.generic.ArrayObject()
+ vpa.append(vp)
+ p[pyPdf.generic.NameObject('/VP')]=vpa
+ o.addPage(p)
+
+ o.write(outfile)
+ infile=None
+ outfile.close()
+ os.rename(outfilename,filename)
+
+
+ def get_context(self):
+ """allow access so that extra 'bits' can be rendered to the page directly"""
+ return cairo.Context(self._s)
+
+ def get_width(self):
+ return self._pagesize[0]
+
+ def get_height(self):
+ return self._pagesize[1]
+
+ def get_margin(self):
+ return self._margin
+
+ def write_text(self,ctx,text,box_width=None,size=10, fill_color=(0.0, 0.0, 0.0), alignment=None):
+ if HAS_PANGOCAIRO_MODULE:
+ (attr,t,accel) = pango.parse_markup(text)
+ pctx = pangocairo.CairoContext(ctx)
+ l = pctx.create_layout()
+ l.set_attributes(attr)
+ fd = pango.FontDescription("%s %d" % (self.font_name,size))
+ l.set_font_description(fd)
+ if box_width:
+ l.set_width(int(box_width*pango.SCALE))
+ if alignment:
+ l.set_alignment(alignment)
+ pctx.update_layout(l)
+ l.set_text(t)
+ pctx.set_source_rgb(*fill_color)
+ pctx.show_layout(l)
+ return l.get_pixel_extents()[0]
+
+ else:
+ ctx.rel_move_to(0,size)
+ ctx.select_font_face(self.font_name, cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL)
+ ctx.set_font_size(size)
+ ctx.show_text(text)
+ ctx.rel_move_to(0,size)
+ return (0,0,len(text)*size,size)
+
+ def _get_context(self):
+ if HAS_PANGOCAIRO_MODULE:
+ return
+ elif HAS_PYCAIRO_MODULE:
+ return cairo.Context(self._s)
+ return None
+
+ def _get_render_area(self):
+ """return a bounding box with the area of the page we are allowed to render out map to
+ in page coordinates (i.e. meters)
+ """
+ # take off our page margins
+ render_area = Box2d(self._margin,self._margin,self._pagesize[0]-self._margin,self._pagesize[1]-self._margin)
+
+ #then if user specified a box to render get intersection with that
+ if self._box:
+ return render_area.intersect(self._box)
+
+ return render_area
+
+ def _get_render_area_size(self):
+ """Get the width and height (in meters) of the area we can render the map to, returned as a tuple"""
+ render_area = self._get_render_area()
+ return (render_area.width(),render_area.height())
+
+ def _is_h_contrained(self,m):
+ """Test if the map size is constrained on the horizontal or vertical axes"""
+ available_area = self._get_render_area_size()
+ map_aspect = m.envelope().width()/m.envelope().height()
+ page_aspect = available_area[0]/available_area[1]
+
+ return map_aspect > page_aspect
+
+ def _get_meta_info_corner(self,render_size,m):
+ """Get the corner (in page coordinates) of a possibly
+ sensible place to render metadata such as a legend or scale"""
+ (x,y) = self._get_render_corner(render_size,m)
+ if self._is_h_contrained(m):
+ y += render_size[1]+0.005
+ x = self._margin
+ else:
+ x += render_size[0]+0.005
+ y = self._margin
+
+ return (x,y)
+
+ def _get_render_corner(self,render_size,m):
+ """Get the corner of the box we should render our map into"""
+ available_area = self._get_render_area()
+
+ x=available_area[0]
+ y=available_area[1]
+
+ h_is_contrained = self._is_h_contrained(m)
+
+ if (self._centering == centering.both or
+ self._centering == centering.horizontal or
+ (self._centering == centering.constrained and h_is_contrained) or
+ (self._centering == centering.unconstrained and not h_is_contrained)):
+ x+=(available_area.width()-render_size[0])/2
+
+ if (self._centering == centering.both or
+ self._centering == centering.vertical or
+ (self._centering == centering.constrained and not h_is_contrained) or
+ (self._centering == centering.unconstrained and h_is_contrained)):
+ y+=(available_area.height()-render_size[1])/2
+ return (x,y)
+
+ def _get_map_pixel_size(self, width_page_m, height_page_m):
+ """for a given map size in paper coordinates return a tuple of the map 'pixel' size we
+ should create at the defined resolution"""
+ return (int(m2px(width_page_m,self._resolution)), int(m2px(height_page_m,self._resolution)))
+
+ def render_map(self,m, filename):
+ """Render the given map to filename"""
+
+ # store this for later so we can post process the PDF
+ self._filename = filename
+
+ # work out the best scale to render out map at given the available space
+ (eff_width,eff_height) = self._get_render_area_size()
+ map_aspect = m.envelope().width()/m.envelope().height()
+ page_aspect = eff_width/eff_height
+
+ scalex=m.envelope().width()/eff_width
+ scaley=m.envelope().height()/eff_height
+
+ scale=max(scalex,scaley)
+
+ rounded_mapscale=self._scale(scale)
+ scalefactor = scale/rounded_mapscale
+ mapw=eff_width*scalefactor
+ maph=eff_height*scalefactor
+ if self._preserve_aspect:
+ if map_aspect > page_aspect:
+ maph=mapw*(1/map_aspect)
+ else:
+ mapw=maph*map_aspect
+
+ # set the map size so that raster elements render at the correct resolution
+ m.resize(*self._get_map_pixel_size(mapw,maph))
+ # calculate the translation for the map starting point
+ (tx,ty) = self._get_render_corner((mapw,maph),m)
+
+ # create our cairo surface and context and then render the map into it
+ self._s = cairo.PDFSurface(filename, m2pt(self._pagesize[0]),m2pt(self._pagesize[1]))
+ ctx=cairo.Context(self._s)
+
+ for l in m.layers:
+ # extract the layer names for naming layers if we use OCG
+ self._layer_names.append(l.name)
+
+ layer_map = Map(m.width,m.height,m.srs)
+ layer_map.layers.append(l)
+ for s in l.styles:
+ layer_map.append_style(s,m.find_style(s))
+ layer_map.zoom_to_box(m.envelope())
+
+ def render_map():
+ ctx.save()
+ ctx.translate(m2pt(tx),m2pt(ty))
+ #cairo defaults to 72dpi
+ ctx.scale(72.0/self._resolution,72.0/self._resolution)
+ render(layer_map, ctx)
+ ctx.restore()
+
+ # antimeridian
+ render_map()
+ if self._is_latlon and (m.envelope().minx < -180 or m.envelope().maxx > 180):
+ old_env = m.envelope()
+ if m.envelope().minx < -180:
+ delta = 360
+ else:
+ delta = -360
+ m.zoom_to_box(Box2d(old_env.minx+delta,old_env.miny,old_env.maxx+delta,old_env.maxy))
+ render_map()
+ # restore the original env
+ m.zoom_to_box(old_env)
+
+ if self._use_ocg_layers:
+ self._s.show_page()
+
+ self.scale = rounded_mapscale
+ self.map_box = Box2d(tx,ty,tx+mapw,ty+maph)
+
+ def render_on_map_lat_lon_grid(self,m,dec_degrees=True):
+ # don't render lat_lon grid if we are already in latlon
+ if self._is_latlon:
+ return
+ p2=Projection(m.srs)
+
+ latlon_bounds = p2.inverse(m.envelope())
+ if p2.inverse(m.envelope().center()).x > latlon_bounds.maxx:
+ latlon_bounds = Box2d(latlon_bounds.maxx,latlon_bounds.miny,latlon_bounds.minx+360,latlon_bounds.maxy)
+
+ if p2.inverse(m.envelope().center()).y > latlon_bounds.maxy:
+ latlon_bounds = Box2d(latlon_bounds.miny,latlon_bounds.maxy,latlon_bounds.maxx,latlon_bounds.miny+360)
+
+ latlon_mapwidth = latlon_bounds.width()
+ # render an extra 20% so we generally won't miss the ends of lines
+ latlon_buffer = 0.2*latlon_mapwidth
+ if dec_degrees:
+ latlon_divsize = default_scale(latlon_mapwidth/7.0)
+ else:
+ latlon_divsize = deg_min_sec_scale(latlon_mapwidth/7.0)
+ latlon_interpsize = latlon_mapwidth/m.width
+
+ self._render_lat_lon_axis(m,p2,latlon_bounds.minx,latlon_bounds.maxx,latlon_bounds.miny,latlon_bounds.maxy,latlon_buffer,latlon_interpsize,latlon_divsize,dec_degrees,True)
+ self._render_lat_lon_axis(m,p2,latlon_bounds.miny,latlon_bounds.maxy,latlon_bounds.minx,latlon_bounds.maxx,latlon_buffer,latlon_interpsize,latlon_divsize,dec_degrees,False)
+
+ def _render_lat_lon_axis(self,m,p2,x1,x2,y1,y2,latlon_buffer,latlon_interpsize,latlon_divsize,dec_degrees,is_x_axis):
+ ctx=cairo.Context(self._s)
+ ctx.set_source_rgb(1,0,0)
+ ctx.set_line_width(1)
+ latlon_labelsize = 6
+
+ ctx.translate(m2pt(self.map_box.minx),m2pt(self.map_box.miny))
+ ctx.rectangle(0,0,m2pt(self.map_box.width()),m2pt(self.map_box.height()))
+ ctx.clip()
+
+ ctx.select_font_face("DejaVu", cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL)
+ ctx.set_font_size(latlon_labelsize)
+
+ box_top = self.map_box.height()
+ if not is_x_axis:
+ ctx.translate(m2pt(self.map_box.width()/2),m2pt(self.map_box.height()/2))
+ ctx.rotate(-math.pi/2)
+ ctx.translate(-m2pt(self.map_box.height()/2),-m2pt(self.map_box.width()/2))
+ box_top = self.map_box.width()
+
+ for xvalue in round_grid_generator(x1 - latlon_buffer,x2 + latlon_buffer,latlon_divsize):
+ yvalue = y1 - latlon_buffer
+ start_cross = None
+ end_cross = None
+ while yvalue < y2+latlon_buffer:
+ if is_x_axis:
+ start = m.view_transform().forward(p2.forward(Coord(xvalue,yvalue)))
+ else:
+ temp = m.view_transform().forward(p2.forward(Coord(yvalue,xvalue)))
+ start = Coord(m2pt(self.map_box.height())-temp.y,temp.x)
+ yvalue += latlon_interpsize
+ if is_x_axis:
+ end = m.view_transform().forward(p2.forward(Coord(xvalue,yvalue)))
+ else:
+ temp = m.view_transform().forward(p2.forward(Coord(yvalue,xvalue)))
+ end = Coord(m2pt(self.map_box.height())-temp.y,temp.x)
+
+ ctx.move_to(start.x,start.y)
+ ctx.line_to(end.x,end.y)
+ ctx.stroke()
+
+ if cmp(start.y, 0) != cmp(end.y,0):
+ start_cross = end.x
+ if cmp(start.y,m2pt(self.map_box.height())) != cmp(end.y, m2pt(self.map_box.height())):
+ end_cross = end.x
+
+ if dec_degrees:
+ line_text = "%g" % (xvalue)
+ else:
+ line_text = format_deg_min_sec(xvalue)
+ if start_cross:
+ ctx.move_to(start_cross+2,latlon_labelsize)
+ ctx.show_text(line_text)
+ if end_cross:
+ ctx.move_to(end_cross+2,m2pt(box_top)-2)
+ ctx.show_text(line_text)
+
+ def render_on_map_scale(self,m):
+ (div_size,page_div_size) = self._get_sensible_scalebar_size(m)
+
+ first_value_x = (math.floor(m.envelope().minx / div_size) + 1) * div_size
+ first_value_x_percent = (first_value_x-m.envelope().minx)/m.envelope().width()
+ self._render_scale_axis(first_value_x,first_value_x_percent,self.map_box.minx,self.map_box.maxx,page_div_size,div_size,self.map_box.miny,self.map_box.maxy,True)
+
+ first_value_y = (math.floor(m.envelope().miny / div_size) + 1) * div_size
+ first_value_y_percent = (first_value_y-m.envelope().miny)/m.envelope().height()
+ self._render_scale_axis(first_value_y,first_value_y_percent,self.map_box.miny,self.map_box.maxy,page_div_size,div_size,self.map_box.minx,self.map_box.maxx,False)
+
+ if self._use_ocg_layers:
+ self._s.show_page()
+ self._layer_names.append("Coordinate Grid Overlay")
+
+ def _get_sensible_scalebar_size(self,m,width=-1):
+ # aim for about 8 divisions across the map
+ # also make sure we can fit the bar with in page area width if specified
+ div_size = sequence_scale(m.envelope().width()/8, [1,2,5])
+ page_div_size = self.map_box.width()*div_size/m.envelope().width()
+ while width > 0 and page_div_size > width:
+ div_size /=2
+ page_div_size /= 2
+ return (div_size,page_div_size)
+
+ def _render_box(self,ctx,x,y,w,h,text=None,stroke_color=(0,0,0),fill_color=(0,0,0)):
+ ctx.set_line_width(1)
+ ctx.set_source_rgb(*fill_color)
+ ctx.rectangle(x,y,w,h)
+ ctx.fill()
+
+ ctx.set_source_rgb(*stroke_color)
+ ctx.rectangle(x,y,w,h)
+ ctx.stroke()
+
+ if text:
+ ctx.move_to(x+1,y)
+ self.write_text(ctx,text,fill_color=[1-z for z in fill_color],size=h-2)
+
+ def _render_scale_axis(self,first,first_percent,start,end,page_div_size,div_size,boundary_start,boundary_end,is_x_axis):
+ prev = start
+ text = None
+ fill=(0,0,0)
+ border_size=8
+ value = first_percent * (end-start) + start
+ label_value = first-div_size
+ if self._is_latlon and label_value < -180:
+ label_value += 360
+
+ ctx=cairo.Context(self._s)
+
+ if not is_x_axis:
+ ctx.translate(m2pt(self.map_box.center().x),m2pt(self.map_box.center().y))
+ ctx.rotate(-math.pi/2)
+ ctx.translate(-m2pt(self.map_box.center().y),-m2pt(self.map_box.center().x))
+
+ while value < end:
+ ctx.move_to(m2pt(value),m2pt(boundary_start))
+ ctx.line_to(m2pt(value),m2pt(boundary_end))
+ ctx.set_source_rgb(0.5,0.5,0.5)
+ ctx.set_line_width(1)
+ ctx.stroke()
+
+ for bar in (m2pt(boundary_start)-border_size,m2pt(boundary_end)):
+ self._render_box(ctx,m2pt(prev),bar,m2pt(value-prev),border_size,text,fill_color=fill)
+
+ prev = value
+ value+=page_div_size
+ fill = [1-z for z in fill]
+ label_value += div_size
+ if self._is_latlon and label_value > 180:
+ label_value -= 360
+ text = "%d" % label_value
+ else:
+ for bar in (m2pt(boundary_start)-border_size,m2pt(boundary_end)):
+ self._render_box(ctx,m2pt(prev),bar,m2pt(end-prev),border_size,fill_color=fill)
+
+
+ def render_scale(self,m,ctx=None,width=0.05):
+ """ m: map to render scale for
+ ctx: A cairo context to render the scale to. If this is None (the default) then
+ automatically create a context and choose the best location for the scale bar.
+ width: Width of area available to render scale bar in (in m)
+
+ will return the size of the rendered scale block in pts
+ """
+
+ (w,h) = (0,0)
+
+ # don't render scale if we are lat lon
+ # dont report scale if we have warped the aspect ratio
+ if self._preserve_aspect and not self._is_latlon:
+ bar_size=8.0
+ box_count=3
+ if ctx is None:
+ ctx=cairo.Context(self._s)
+ (tx,ty) = self._get_meta_info_corner((self.map_box.width(),self.map_box.height()),m)
+ ctx.translate(tx,ty)
+
+ (div_size,page_div_size) = self._get_sensible_scalebar_size(m, width/box_count)
+
+
+ div_unit = "m"
+ if div_size > 1000:
+ div_size /= 1000
+ div_unit = "km"
+
+ text = "0%s" % div_unit
+ ctx.save()
+ if width > 0:
+ ctx.translate(m2pt(width-box_count*page_div_size)/2,0)
+ for ii in range(box_count):
+ fill=(ii%2,)*3
+ self._render_box(ctx, m2pt(ii*page_div_size), h, m2pt(page_div_size), bar_size, text, fill_color=fill)
+ fill = [1-z for z in fill]
+ text = "%g%s" % ((ii+1)*div_size,div_unit)
+ #else:
+ # self._render_box(ctx, m2pt(box_count*page_div_size), h, m2pt(page_div_size), bar_size, text, fill_color=(1,1,1), stroke_color=(1,1,1))
+ w = (box_count)*page_div_size
+ h += bar_size
+ ctx.restore()
+
+ if width > 0:
+ box_width=m2pt(width)
+ else:
+ box_width = None
+
+ font_size=6
+ ctx.move_to(0,h)
+ if HAS_PANGOCAIRO_MODULE:
+ alignment = pango.ALIGN_CENTER
+ else:
+ alignment = None
+
+ text_ext=self.write_text(ctx,"Scale 1:%d" % self.scale,box_width=box_width,size=font_size, alignment=alignment)
+ h+=text_ext[3]+2
+
+ return (w,h)
+
+ def render_legend(self,m, page_break=False, ctx=None, collumns=1,width=None, height=None, item_per_rule=False, attribution={}, legend_item_box_size=(0.015,0.0075)):
+ """ m: map to render legend for
+ ctx: A cairo context to render the legend to. If this is None (the default) then
+ automatically create a context and choose the best location for the legend.
+ width: Width of area available to render legend in (in m)
+ page_break: move to next page if legen over flows this one
+ collumns: number of collumns available in legend box
+ attribution: additional text that will be rendered in gray under the layer name. keyed by layer name
+ legend_item_box_size: two tuple with width and height of legend item box size in meters
+
+ will return the size of the rendered block in pts
+ """
+
+ (w,h) = (0,0)
+ if self._s:
+ if ctx is None:
+ ctx=cairo.Context(self._s)
+ (tx,ty) = self._get_meta_info_corner((self.map_box.width(),self.map_box.height()),m)
+ ctx.translate(m2pt(tx),m2pt(ty))
+ width = self._pagesize[0]-2*tx
+ height = self._pagesize[1]-self._margin-ty
+
+ x=0
+ y=0
+ if width:
+ cwidth = width/collumns
+ w=m2pt(width)
+ else:
+ cwidth = None
+ current_collumn = 0
+
+ processed_layers = []
+ for l in reversed(m.layers):
+ have_layer_header = False
+ added_styles={}
+ layer_title = l.name
+ if layer_title in processed_layers:
+ continue
+ processed_layers.append(layer_title)
+
+ # check through the features to find which combinations of styles are active
+ # for each unique combination add a legend entry
+ for f in l.datasource.all_features():
+ if f.num_geometries() > 0:
+ active_rules = []
+ rule_text = ""
+ for s in l.styles:
+ st = m.find_style(s)
+ for r in st.rules:
+ # we need to do the scale test here as well so we don't
+ # add unused scale rules to the legend description
+ if ((not r.filter) or r.filter.evaluate(f) == '1') and \
+ r.min_scale <= m.scale_denominator() and m.scale_denominator() < r.max_scale:
+ active_rules.append((s,r.name))
+ if r.filter and str(r.filter) != "true":
+ if len(rule_text) > 0:
+ rule_text += " AND "
+ if r.name:
+ rule_text += r.name
+ else:
+ rule_text += str(r.filter)
+ active_rules = tuple(active_rules)
+ if added_styles.has_key(active_rules):
+ continue
+
+ added_styles[active_rules] = (f,rule_text)
+ if not item_per_rule:
+ break
+ else:
+ added_styles[l] = (None,None)
+
+ legend_items = added_styles.keys()
+ legend_items.sort()
+ for li in legend_items:
+ if True:
+ (f,rule_text) = added_styles[li]
+
+
+ legend_map_size = (int(m2pt(legend_item_box_size[0])),int(m2pt(legend_item_box_size[1])))
+ lemap=Map(legend_map_size[0],legend_map_size[1],srs=m.srs)
+ if m.background:
+ lemap.background = m.background
+ # the buffer is needed to ensure that text labels that overflow the edge of the
+ # map still render for the legend
+ lemap.buffer_size=1000
+ for s in l.styles:
+ sty=m.find_style(s)
+ lestyle = Style()
+ for r in sty.rules:
+ for sym in r.symbols:
+ try:
+ sym.avoid_edges=False
+ except:
+ print "**** Cant set avoid edges for rule", r.name
+ if r.min_scale <= m.scale_denominator() and m.scale_denominator() < r.max_scale:
+ lerule = r
+ lerule.min_scale = 0
+ lerule.max_scale = float("inf")
+ lestyle.rules.append(lerule)
+ lemap.append_style(s,lestyle)
+
+ ds = MemoryDatasource()
+ if f is None:
+ ds=l.datasource
+ layer_srs = l.srs
+ elif f.envelope().width() == 0:
+ ds.add_feature(Feature(f.id(),Geometry2d.from_wkt("POINT(0 0)"),**f.attributes))
+ lemap.zoom_to_box(Box2d(-1,-1,1,1))
+ layer_srs = m.srs
+ else:
+ ds.add_feature(f)
+ layer_srs = l.srs
+
+ lelayer = Layer("LegendLayer",layer_srs)
+ lelayer.datasource = ds
+ for s in l.styles:
+ lelayer.styles.append(s)
+ lemap.layers.append(lelayer)
+
+ if f is None or f.envelope().width() != 0:
+ lemap.zoom_all()
+ lemap.zoom(1.1)
+
+ item_size = legend_map_size[1]
+ if not have_layer_header:
+ item_size += 8
+
+ if y+item_size > m2pt(height):
+ current_collumn += 1
+ y=0
+ if current_collumn >= collumns:
+ if page_break:
+ self._s.show_page()
+ x=0
+ current_collumn = 0
+ else:
+ break
+
+ if not have_layer_header and item_per_rule:
+ ctx.move_to(x+m2pt(current_collumn*cwidth),y)
+ e=self.write_text(ctx, l.name, m2pt(cwidth), 8)
+ y+=e[3]+2
+ have_layer_header = True
+ ctx.save()
+ ctx.translate(x+m2pt(current_collumn*cwidth),y)
+ #extra save around map render as it sets up a clip box and doesn't clear it
+ ctx.save()
+ render(lemap, ctx)
+ ctx.restore()
+
+ ctx.rectangle(0,0,*legend_map_size)
+ ctx.set_source_rgb(0.5,0.5,0.5)
+ ctx.set_line_width(1)
+ ctx.stroke()
+ ctx.restore()
+
+ ctx.move_to(x+legend_map_size[0]+m2pt(current_collumn*cwidth)+2,y)
+ legend_entry_size = legend_map_size[1]
+ legend_text_size = 0
+ if not item_per_rule:
+ rule_text = layer_title
+ if rule_text:
+ e=self.write_text(ctx, rule_text, m2pt(cwidth-legend_item_box_size[0]-0.005), 6)
+ legend_text_size += e[3]
+ ctx.rel_move_to(0,e[3])
+ if attribution.has_key(layer_title):
+ e=self.write_text(ctx, attribution[layer_title], m2pt(cwidth-legend_item_box_size[0]-0.005), 6, fill_color=(0.5,0.5,0.5))
+ legend_text_size += e[3]
+
+ if legend_text_size > legend_entry_size:
+ legend_entry_size=legend_text_size
+
+ y+=legend_entry_size +2
+ if y > h:
+ h = y
+ return (w,h)
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..f4ca59d
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,2 @@
+[nosetests]
+verbosity=1
diff --git a/setup.py b/setup.py
new file mode 100755
index 0000000..2980471
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,226 @@
+#! /usr/bin/env python
+
+from distutils import sysconfig
+from setuptools import setup, Extension
+import os
+import subprocess
+import sys
+import shutil
+import re
+
+cflags = sysconfig.get_config_var('CFLAGS')
+sysconfig._config_vars['CFLAGS'] = re.sub(' +', ' ', cflags.replace('-g', '').replace('-Os', '').replace('-arch i386', ''))
+opt = sysconfig.get_config_var('OPT')
+sysconfig._config_vars['OPT'] = re.sub(' +', ' ', opt.replace('-g', '').replace('-Os', ''))
+ldshared = sysconfig.get_config_var('LDSHARED')
+sysconfig._config_vars['LDSHARED'] = re.sub(' +', ' ', ldshared.replace('-g', '').replace('-Os', '').replace('-arch i386', ''))
+ldflags = sysconfig.get_config_var('LDFLAGS')
+sysconfig._config_vars['LDFLAGS'] = re.sub(' +', ' ', ldflags.replace('-g', '').replace('-Os', '').replace('-arch i386', ''))
+pycflags = sysconfig.get_config_var('PY_CFLAGS')
+sysconfig._config_vars['PY_CFLAGS'] = re.sub(' +', ' ', pycflags.replace('-g', '').replace('-Os', '').replace('-arch i386', ''))
+sysconfig._config_vars['CFLAGSFORSHARED'] = ''
+os.environ['ARCHFLAGS'] = ''
+
+if os.environ.get("MASON_BUILD", "false") == "true":
+ # run bootstrap.sh to get mason builds
+ subprocess.call(['./bootstrap.sh'])
+ mapnik_config = 'mason_packages/.link/bin/mapnik-config'
+ mason_build = True
+else:
+ mapnik_config = 'mapnik-config'
+ mason_build = False
+
+boost_python_lib = os.environ.get("BOOST_PYTHON_LIB", 'boost_python')
+
+try:
+ linkflags = subprocess.check_output([mapnik_config, '--libs']).rstrip('\n').split(' ')
+ lib_path = linkflags[0][2:]
+ linkflags.extend(subprocess.check_output([mapnik_config, '--ldflags']).rstrip('\n').split(' '))
+except:
+ raise Exception("Failed to find proper linking flags from mapnik config");
+
+## Dynamically make the mapnik/paths.py file if it doesn't exist.
+if os.path.isfile('mapnik/paths.py'):
+ create_paths = False
+else:
+ create_paths = True
+ f_paths = open('mapnik/paths.py', 'w')
+ f_paths.write('import os\n')
+ f_paths.write('\n')
+
+if mason_build:
+ try:
+ if sys.platform == 'darwin':
+ base_f = 'libmapnik.dylib'
+ else:
+ base_f = 'libmapnik.so.3.0'
+ f = os.path.join(lib_path, base_f)
+ shutil.copyfile(f, os.path.join('mapnik', base_f))
+ except shutil.Error:
+ pass
+ input_plugin_path = subprocess.check_output([mapnik_config, '--input-plugins']).rstrip('\n')
+ input_plugin_files = os.listdir(input_plugin_path)
+ input_plugin_files = [os.path.join(input_plugin_path, f) for f in input_plugin_files]
+ if not os.path.exists(os.path.join('mapnik','plugins','input')):
+ os.makedirs(os.path.join('mapnik','plugins', 'input'))
+ for f in input_plugin_files:
+ try:
+ shutil.copyfile(f, os.path.join('mapnik', 'plugins', 'input', os.path.basename(f)))
+ except shutil.Error:
+ pass
+ font_path = subprocess.check_output([mapnik_config, '--fonts']).rstrip('\n')
+ font_files = os.listdir(font_path)
+ font_files = [os.path.join(font_path, f) for f in font_files]
+ if not os.path.exists(os.path.join('mapnik','plugins','fonts')):
+ os.makedirs(os.path.join('mapnik','plugins','fonts'))
+ for f in font_files:
+ try:
+ shutil.copyfile(f, os.path.join('mapnik','plugins','fonts', os.path.basename(f)))
+ except shutil.Error:
+ pass
+ if create_paths:
+ f_paths.write('mapniklibpath = os.path.join(os.path.dirname(os.path.realpath(__file__)), "plugins")\n')
+elif create_paths:
+ f_paths.write("mapniklibpath = '"+lib_path+"/mapnik'\n")
+ f_paths.write('mapniklibpath = os.path.normpath(mapniklibpath)\n')
+
+if create_paths:
+ f_paths.write("inputpluginspath = os.path.join(mapniklibpath,'input')\n")
+ f_paths.write("fontscollectionpath = os.path.join(mapniklibpath,'fonts')\n")
+ f_paths.write("__all__ = [mapniklibpath,inputpluginspath,fontscollectionpath]\n")
+ f_paths.close()
+
+
+if not mason_build:
+ icu_path = subprocess.check_output([mapnik_config, '--icu-data']).rstrip('\n')
+else:
+ icu_path = 'mason_packages/.link/share/icu/'
+if icu_path:
+ icu_files = os.listdir(icu_path)
+ icu_files = [os.path.join(icu_path, f) for f in icu_files]
+ if not os.path.exists(os.path.join('mapnik','plugins','icu')):
+ os.makedirs(os.path.join('mapnik','plugins','icu'))
+ for f in icu_files:
+ try:
+ shutil.copyfile(f, os.path.join('mapnik','plugins','icu', os.path.basename(f)))
+ except shutil.Error:
+ pass
+
+if not mason_build:
+ gdal_path = subprocess.check_output([mapnik_config, '--gdal-data']).rstrip('\n')
+else:
+ gdal_path = 'mason_packages/.link/share/gdal/'
+ if os.path.exists('mason_packages/.link/share/gdal/gdal/'):
+ gdal_path = 'mason_packages/.link/share/gdal/gdal/'
+if gdal_path:
+ gdal_files = os.listdir(gdal_path)
+ gdal_files = [os.path.join(gdal_path, f) for f in gdal_files]
+ if not os.path.exists(os.path.join('mapnik','plugins','gdal')):
+ os.makedirs(os.path.join('mapnik','plugins','gdal'))
+ for f in gdal_files:
+ try:
+ shutil.copyfile(f, os.path.join('mapnik','plugins','gdal', os.path.basename(f)))
+ except shutil.Error:
+ pass
+
+if not mason_build:
+ proj_path = subprocess.check_output([mapnik_config, '--proj-lib']).rstrip('\n')
+else:
+ proj_path = 'mason_packages/.link/share/proj/'
+ if os.path.exists('mason_packages/.link/share/proj/proj/'):
+ proj_path = 'mason_packages/.link/share/proj/proj/'
+if proj_path:
+ proj_files = os.listdir(proj_path)
+ proj_files = [os.path.join(proj_path, f) for f in proj_files]
+ if not os.path.exists(os.path.join('mapnik','plugins','proj')):
+ os.makedirs(os.path.join('mapnik','plugins','proj'))
+ for f in proj_files:
+ try:
+ shutil.copyfile(f, os.path.join('mapnik','plugins','proj', os.path.basename(f)))
+ except shutil.Error:
+ pass
+
+extra_comp_args = subprocess.check_output([mapnik_config, '--cflags']).rstrip('\n').split(' ')
+
+if sys.platform == 'darwin':
+ extra_comp_args.append('-mmacosx-version-min=10.8')
+ linkflags.append('-mmacosx-version-min=10.8')
+else:
+ linkflags.append('-lrt')
+ linkflags.append('-Wl,-z,origin')
+ linkflags.append('-Wl,-rpath=$ORIGIN')
+
+if os.environ.get("CC",False) == False:
+ os.environ["CC"] = subprocess.check_output([mapnik_config, '--cxx']).rstrip('\n')
+if os.environ.get("CXX",False) == False:
+ os.environ["CXX"] = subprocess.check_output([mapnik_config, '--cxx']).rstrip('\n')
+
+setup(
+ name = "mapnik",
+ version = "0.1",
+ packages = ['mapnik'],
+ author = "Blake Thompson",
+ author_email = "***@gmail.com",
+ description = "Python bindings for Mapnik",
+ license = "GNU LESSER GENERAL PUBLIC LICENSE",
+ keywords = "mapnik mapbox mapping carteography",
+ url = "http://mapnik.org/",
+ tests_require = [
+ 'nose',
+ ],
+ package_data = {
+ 'mapnik': ['libmapnik.*', 'plugins/*/*'],
+ },
+ test_suite = 'nose.collector',
+ ext_modules = [
+ Extension('mapnik._mapnik', [
+ 'src/mapnik_color.cpp',
+ 'src/mapnik_coord.cpp',
+ 'src/mapnik_datasource.cpp',
+ 'src/mapnik_datasource_cache.cpp',
+ 'src/mapnik_envelope.cpp',
+ 'src/mapnik_expression.cpp',
+ 'src/mapnik_feature.cpp',
+ 'src/mapnik_featureset.cpp',
+ 'src/mapnik_font_engine.cpp',
+ 'src/mapnik_fontset.cpp',
+ 'src/mapnik_gamma_method.cpp',
+ 'src/mapnik_geometry.cpp',
+ 'src/mapnik_grid.cpp',
+ 'src/mapnik_grid_view.cpp',
+ 'src/mapnik_image.cpp',
+ 'src/mapnik_image_view.cpp',
+ 'src/mapnik_label_collision_detector.cpp',
+ 'src/mapnik_layer.cpp',
+ 'src/mapnik_logger.cpp',
+ 'src/mapnik_map.cpp',
+ 'src/mapnik_palette.cpp',
+ 'src/mapnik_parameters.cpp',
+ 'src/mapnik_proj_transform.cpp',
+ 'src/mapnik_projection.cpp',
+ 'src/mapnik_python.cpp',
+ 'src/mapnik_query.cpp',
+ 'src/mapnik_raster_colorizer.cpp',
+ 'src/mapnik_rule.cpp',
+ 'src/mapnik_scaling_method.cpp',
+ 'src/mapnik_style.cpp',
+ 'src/mapnik_svg_generator_grammar.cpp',
+ 'src/mapnik_symbolizer.cpp',
+ 'src/mapnik_text_placement.cpp',
+ 'src/mapnik_view_transform.cpp',
+ 'src/python_grid_utils.cpp',
+ ],
+ language='c++',
+ libraries = [
+ 'mapnik',
+ 'mapnik-wkt',
+ 'mapnik-json',
+ 'boost_thread',
+ 'boost_system',
+ boost_python_lib,
+ ],
+ extra_compile_args = extra_comp_args,
+ extra_link_args = linkflags,
+ )
+ ]
+)
diff --git a/src/boost_std_shared_shim.hpp b/src/boost_std_shared_shim.hpp
new file mode 100644
index 0000000..8b603e5
--- /dev/null
+++ b/src/boost_std_shared_shim.hpp
@@ -0,0 +1,49 @@
+/*****************************************************************************
+ *
+ * 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 MAPNIK_PYTHON_BOOST_STD_SHARED_SHIM
+#define MAPNIK_PYTHON_BOOST_STD_SHARED_SHIM
+
+// boost
+#include <boost/version.hpp>
+#include <boost/config.hpp>
+
+#if BOOST_VERSION < 105300 || defined BOOST_NO_CXX11_SMART_PTR
+
+// https://github.com/mapnik/mapnik/issues/2022
+#include <memory>
+
+namespace boost {
+template<class T> const T* get_pointer(std::shared_ptr<T> const& p)
+{
+ return p.get();
+}
+
+template<class T> T* get_pointer(std::shared_ptr<T>& p)
+{
+ return p.get();
+}
+} // namespace boost
+
+#endif
+
+#endif // MAPNIK_PYTHON_BOOST_STD_SHARED_SHIM
diff --git a/src/mapnik_color.cpp b/src/mapnik_color.cpp
new file mode 100644
index 0000000..54f0c9a
--- /dev/null
+++ b/src/mapnik_color.cpp
@@ -0,0 +1,130 @@
+/*****************************************************************************
+ *
+ * This file is part of Mapnik (c++ mapping toolkit)
+ *
+ * Copyright (C) 2015 Artem Pavlenko, Jean-Francois Doyon
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ *****************************************************************************/
+
+#include <mapnik/config.hpp>
+#include "boost_std_shared_shim.hpp"
+
+// boost
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#pragma GCC diagnostic ignored "-Wunused-local-typedef"
+#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
+
+#include <boost/python.hpp>
+#pragma GCC diagnostic pop
+
+//mapnik
+#include <mapnik/color.hpp>
+
+
+using mapnik::color;
+
+struct color_pickle_suite : boost::python::pickle_suite
+{
+ static boost::python::tuple
+ getinitargs(const color& c)
+ {
+ using namespace boost::python;
+ return boost::python::make_tuple(c.red(),c.green(),c.blue(),c.alpha());
+ }
+};
+
+void export_color ()
+{
+ using namespace boost::python;
+ class_<color>("Color", init<int,int,int,int>(
+ ( arg("r"), arg("g"), arg("b"), arg("a") ),
+ "Creates a new color from its RGB components\n"
+ "and an alpha value.\n"
+ "All values between 0 and 255.\n")
+ )
+ .def(init<int,int,int,int,bool>(
+ ( arg("r"), arg("g"), arg("b"), arg("a"), arg("premultiplied") ),
+ "Creates a new color from its RGB components\n"
+ "and an alpha value.\n"
+ "All values between 0 and 255.\n")
+ )
+ .def(init<int,int,int>(
+ ( arg("r"), arg("g"), arg("b") ),
+ "Creates a new color from its RGB components.\n"
+ "All values between 0 and 255.\n")
+ )
+ .def(init<uint32_t>(
+ ( arg("val") ),
+ "Creates a new color from an unsigned integer.\n"
+ "All values between 0 and 2^32-1\n")
+ )
+ .def(init<uint32_t, bool>(
+ ( arg("val"), arg("premultiplied") ),
+ "Creates a new color from an unsigned integer.\n"
+ "All values between 0 and 2^32-1\n")
+ )
+ .def(init<std::string>(
+ ( arg("color_string") ),
+ "Creates a new color from its CSS string representation.\n"
+ "The string may be a CSS color name (e.g. 'blue')\n"
+ "or a hex color string (e.g. '#0000ff').\n")
+ )
+ .def(init<std::string, bool>(
+ ( arg("color_string"), arg("premultiplied") ),
+ "Creates a new color from its CSS string representation.\n"
+ "The string may be a CSS color name (e.g. 'blue')\n"
+ "or a hex color string (e.g. '#0000ff').\n")
+ )
+ .add_property("r",
+ &color::red,
+ &color::set_red,
+ "Gets or sets the red component.\n"
+ "The value is between 0 and 255.\n")
+ .add_property("g",
+ &color::green,
+ &color::set_green,
+ "Gets or sets the green component.\n"
+ "The value is between 0 and 255.\n")
+ .add_property("b",
+ &color::blue,
+ &color::set_blue,
+ "Gets or sets the blue component.\n"
+ "The value is between 0 and 255.\n")
+ .add_property("a",
+ &color::alpha,
+ &color::set_alpha,
+ "Gets or sets the alpha component.\n"
+ "The value is between 0 and 255.\n")
+ .def(self == self)
+ .def(self != self)
+ .def_pickle(color_pickle_suite())
+ .def("__str__",&color::to_string)
+ .def("set_premultiplied",&color::set_premultiplied)
+ .def("get_premultiplied",&color::get_premultiplied)
+ .def("premultiply",&color::premultiply)
+ .def("demultiply",&color::demultiply)
+ .def("packed",&color::rgba)
+ .def("to_hex_string",&color::to_hex_string,
+ "Returns the hexadecimal representation of this color.\n"
+ "\n"
+ "Example:\n"
+ ">>> c = Color('blue')\n"
+ ">>> c.to_hex_string()\n"
+ "'#0000ff'\n")
+ ;
+}
diff --git a/src/mapnik_coord.cpp b/src/mapnik_coord.cpp
new file mode 100644
index 0000000..7c480f2
--- /dev/null
+++ b/src/mapnik_coord.cpp
@@ -0,0 +1,73 @@
+/*****************************************************************************
+ *
+ * This file is part of Mapnik (c++ mapping toolkit)
+ *
+ * Copyright (C) 2015 Artem Pavlenko, Jean-Francois Doyon
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ *****************************************************************************/
+#include <mapnik/config.hpp>
+#include "boost_std_shared_shim.hpp"
+
+// boost
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#pragma GCC diagnostic ignored "-Wunused-local-typedef"
+#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
+
+#include <boost/python.hpp>
+#pragma GCC diagnostic pop
+
+
+// mapnik
+#include <mapnik/coord.hpp>
+
+using mapnik::coord;
+
+struct coord_pickle_suite : boost::python::pickle_suite
+{
+ static boost::python::tuple
+ getinitargs(const coord<double,2>& c)
+ {
+ using namespace boost::python;
+ return boost::python::make_tuple(c.x,c.y);
+ }
+};
+
+void export_coord()
+{
+ using namespace boost::python;
+ class_<coord<double,2> >("Coord",init<double, double>(
+ // class docstring is in mapnik/__init__.py, class _Coord
+ (arg("x"), arg("y")),
+ "Constructs a new point with the given coordinates.\n")
+ )
+ .def_pickle(coord_pickle_suite())
+ .def_readwrite("x", &coord<double,2>::x,
+ "Gets or sets the x/lon coordinate of the point.\n")
+ .def_readwrite("y", &coord<double,2>::y,
+ "Gets or sets the y/lat coordinate of the point.\n")
+ .def(self == self) // __eq__
+ .def(self + self) // __add__
+ .def(self + float())
+ .def(float() + self)
+ .def(self - self) // __sub__
+ .def(self - float())
+ .def(self * float()) //__mult__
+ .def(float() * self)
+ .def(self / float()) // __div__
+ ;
+}
diff --git a/src/mapnik_datasource.cpp b/src/mapnik_datasource.cpp
new file mode 100644
index 0000000..b11ecd7
--- /dev/null
+++ b/src/mapnik_datasource.cpp
@@ -0,0 +1,217 @@
+/*****************************************************************************
+ *
+ * This file is part of Mapnik (c++ mapping toolkit)
+ *
+ * Copyright (C) 2015 Artem Pavlenko, Jean-Francois Doyon
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ *****************************************************************************/
+
+#include <mapnik/config.hpp>
+
+// boost
+#include "boost_std_shared_shim.hpp"
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#pragma GCC diagnostic ignored "-Wunused-local-typedef"
+#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
+
+#include <boost/python.hpp>
+#include <boost/noncopyable.hpp>
+#include <boost/version.hpp>
+#pragma GCC diagnostic pop
+
+// stl
+#include <vector>
+
+// mapnik
+#include <mapnik/box2d.hpp>
+#include <mapnik/datasource.hpp>
+#include <mapnik/datasource_cache.hpp>
+#include <mapnik/feature_layer_desc.hpp>
+#include <mapnik/memory_datasource.hpp>
+
+
+using mapnik::datasource;
+using mapnik::memory_datasource;
+using mapnik::layer_descriptor;
+using mapnik::attribute_descriptor;
+using mapnik::parameters;
+
+namespace
+{
+//user-friendly wrapper that uses Python dictionary
+using namespace boost::python;
+std::shared_ptr<mapnik::datasource> create_datasource(dict const& d)
+{
+ mapnik::parameters params;
+ boost::python::list keys=d.keys();
+ for (int i=0; i < len(keys); ++i)
+ {
+ std::string key = extract<std::string>(keys[i]);
+ object obj = d[key];
+ if (PyUnicode_Check(obj.ptr()))
+ {
+ PyObject* temp = PyUnicode_AsUTF8String(obj.ptr());
+ if (temp)
+ {
+#if PY_VERSION_HEX >= 0x03000000
+ char* c_str = PyBytes_AsString(temp);
+#else
+ char* c_str = PyString_AsString(temp);
+#endif
+ params[key] = std::string(c_str);
+ Py_DecRef(temp);
+ }
+ continue;
+ }
+
+ extract<std::string> ex0(obj);
+ extract<mapnik::value_integer> ex1(obj);
+ extract<double> ex2(obj);
+ if (ex0.check())
+ {
+ params[key] = ex0();
+ }
+ else if (ex1.check())
+ {
+ params[key] = ex1();
+ }
+ else if (ex2.check())
+ {
+ params[key] = ex2();
+ }
+ }
+
+ return mapnik::datasource_cache::instance().create(params);
+}
+
+boost::python::dict describe(std::shared_ptr<mapnik::datasource> const& ds)
+{
+ boost::python::dict description;
+ mapnik::layer_descriptor ld = ds->get_descriptor();
+ description["type"] = ds->type();
+ description["name"] = ld.get_name();
+ description["geometry_type"] = ds->get_geometry_type();
+ description["encoding"] = ld.get_encoding();
+ for (auto const& param : ld.get_extra_parameters())
+ {
+ description[param.first] = param.second;
+ }
+ return description;
+}
+
+boost::python::list fields(std::shared_ptr<mapnik::datasource> const& ds)
+{
+ boost::python::list flds;
+ if (ds)
+ {
+ layer_descriptor ld = ds->get_descriptor();
+ std::vector<attribute_descriptor> const& desc_ar = ld.get_descriptors();
+ std::vector<attribute_descriptor>::const_iterator it = desc_ar.begin();
+ std::vector<attribute_descriptor>::const_iterator end = desc_ar.end();
+ for (; it != end; ++it)
+ {
+ flds.append(it->get_name());
+ }
+ }
+ return flds;
+}
+boost::python::list field_types(std::shared_ptr<mapnik::datasource> const& ds)
+{
+ boost::python::list fld_types;
+ if (ds)
+ {
+ layer_descriptor ld = ds->get_descriptor();
+ std::vector<attribute_descriptor> const& desc_ar = ld.get_descriptors();
+ std::vector<attribute_descriptor>::const_iterator it = desc_ar.begin();
+ std::vector<attribute_descriptor>::const_iterator end = desc_ar.end();
+ for (; it != end; ++it)
+ {
+ unsigned type = it->get_type();
+ if (type == mapnik::Integer)
+ // this crashes, so send back strings instead
+ //fld_types.append(boost::python::object(boost::python::handle<>(&PyInt_Type)));
+ fld_types.append(boost::python::str("int"));
+ else if (type == mapnik::Float)
+ fld_types.append(boost::python::str("float"));
+ else if (type == mapnik::Double)
+ fld_types.append(boost::python::str("float"));
+ else if (type == mapnik::String)
+ fld_types.append(boost::python::str("str"));
+ else if (type == mapnik::Boolean)
+ fld_types.append(boost::python::str("bool"));
+ else if (type == mapnik::Geometry)
+ fld_types.append(boost::python::str("geometry"));
+ else if (type == mapnik::Object)
+ fld_types.append(boost::python::str("object"));
+ else
+ fld_types.append(boost::python::str("unknown"));
+ }
+ }
+ return fld_types;
+}}
+
+mapnik::parameters const& (mapnik::datasource::*params_const)() const = &mapnik::datasource::params;
+
+
+void export_datasource()
+{
+ using namespace boost::python;
+
+ enum_<mapnik::datasource::datasource_t>("DataType")
+ .value("Vector",mapnik::datasource::Vector)
+ .value("Raster",mapnik::datasource::Raster)
+ ;
+
+ enum_<mapnik::datasource_geometry_t>("DataGeometryType")
+ .value("Point",mapnik::datasource_geometry_t::Point)
+ .value("LineString",mapnik::datasource_geometry_t::LineString)
+ .value("Polygon",mapnik::datasource_geometry_t::Polygon)
+ .value("Collection",mapnik::datasource_geometry_t::Collection)
+ ;
+
+ class_<datasource,std::shared_ptr<datasource>,
+ boost::noncopyable>("Datasource",no_init)
+ .def("type",&datasource::type)
+ .def("geometry_type",&datasource::get_geometry_type)
+ .def("describe",&describe)
+ .def("envelope",&datasource::envelope)
+ .def("features",&datasource::features)
+ .def("fields",&fields)
+ .def("field_types",&field_types)
+ .def("features_at_point",&datasource::features_at_point, (arg("coord"),arg("tolerance")=0))
+ .def("params",make_function(params_const,return_value_policy<copy_const_reference>()),
+ "The configuration parameters of the data source. "
+ "These vary depending on the type of data source.")
+ .def(self == self)
+ ;
+
+ def("CreateDatasource",&create_datasource);
+
+ class_<memory_datasource,
+ bases<datasource>, std::shared_ptr<memory_datasource>,
+ boost::noncopyable>("MemoryDatasourceBase", init<parameters>())
+ .def("add_feature",&memory_datasource::push,
+ "Adds a Feature:\n"
+ ">>> ms = MemoryDatasource()\n"
+ ">>> feature = Feature(1)\n"
+ ">>> ms.add_feature(Feature(1))\n")
+ .def("num_features",&memory_datasource::size)
+ ;
+
+ implicitly_convertible<std::shared_ptr<memory_datasource>,std::shared_ptr<datasource> >();
+}
diff --git a/src/mapnik_datasource_cache.cpp b/src/mapnik_datasource_cache.cpp
new file mode 100644
index 0000000..7122468
--- /dev/null
+++ b/src/mapnik_datasource_cache.cpp
@@ -0,0 +1,104 @@
+/*****************************************************************************
+ *
+ * This file is part of Mapnik (c++ mapping toolkit)
+ *
+ * Copyright (C) 2015 Artem Pavlenko, Jean-Francois Doyon
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ *****************************************************************************/
+
+#include <mapnik/config.hpp>
+
+// boost
+#include "boost_std_shared_shim.hpp"
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#pragma GCC diagnostic ignored "-Wunused-local-typedef"
+#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
+#include <boost/python.hpp>
+#include <boost/noncopyable.hpp>
+#pragma GCC diagnostic pop
+
+#include <mapnik/value_types.hpp>
+#include <mapnik/params.hpp>
+#include <mapnik/datasource.hpp>
+#include <mapnik/datasource_cache.hpp>
+
+namespace {
+
+using namespace boost::python;
+
+std::shared_ptr<mapnik::datasource> create_datasource(const dict& d)
+{
+ mapnik::parameters params;
+ boost::python::list keys=d.keys();
+ for (int i=0; i<len(keys); ++i)
+ {
+ std::string key = extract<std::string>(keys[i]);
+ object obj = d[key];
+ extract<std::string> ex0(obj);
+ extract<mapnik::value_integer> ex1(obj);
+ extract<double> ex2(obj);
+
+ if (ex0.check())
+ {
+ params[key] = ex0();
+ }
+ else if (ex1.check())
+ {
+ params[key] = ex1();
+ }
+ else if (ex2.check())
+ {
+ params[key] = ex2();
+ }
+ }
+
+ return mapnik::datasource_cache::instance().create(params);
+}
+
+void register_datasources(std::string const& path)
+{
+ mapnik::datasource_cache::instance().register_datasources(path);
+}
+
+std::vector<std::string> plugin_names()
+{
+ return mapnik::datasource_cache::instance().plugin_names();
+}
+
+std::string plugin_directories()
+{
+ return mapnik::datasource_cache::instance().plugin_directories();
+}
+
+}
+
+void export_datasource_cache()
+{
+ using mapnik::datasource_cache;
+ class_<datasource_cache,
+ boost::noncopyable>("DatasourceCache",no_init)
+ .def("create",&create_datasource)
+ .staticmethod("create")
+ .def("register_datasources",&register_datasources)
+ .staticmethod("register_datasources")
+ .def("plugin_names",&plugin_names)
+ .staticmethod("plugin_names")
+ .def("plugin_directories",&plugin_directories)
+ .staticmethod("plugin_directories")
+ ;
+}
diff --git a/src/mapnik_enumeration.hpp b/src/mapnik_enumeration.hpp
new file mode 100644
index 0000000..ce2266a
--- /dev/null
+++ b/src/mapnik_enumeration.hpp
@@ -0,0 +1,88 @@
+/*****************************************************************************
+ *
+ * This file is part of Mapnik (c++ mapping toolkit)
+ *
+ * Copyright (C) 2015 Artem Pavlenko, Jean-Francois Doyon
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ *****************************************************************************/
+#ifndef MAPNIK_PYTHON_BINDING_ENUMERATION_INCLUDED
+#define MAPNIK_PYTHON_BINDING_ENUMERATION_INCLUDED
+
+#include <boost/python/converter/registered.hpp> // for registered
+#include <boost/python/enum.hpp> // for enum_
+#include <boost/python/implicit.hpp> // for implicitly_convertible
+#include <boost/python/to_python_converter.hpp>
+
+namespace mapnik {
+
+template <typename EnumWrapper>
+class enumeration_ :
+ public boost::python::enum_<typename EnumWrapper::native_type>
+{
+ // some short cuts
+ using base_type = boost::python::enum_<typename EnumWrapper::native_type>;
+ using native_type = typename EnumWrapper::native_type;
+public:
+ enumeration_() :
+ base_type( EnumWrapper::get_name().c_str() )
+ {
+ init();
+ }
+ enumeration_(const char * python_alias) :
+ base_type( python_alias )
+ {
+ init();
+ }
+ enumeration_(const char * python_alias, const char * doc) :
+ base_type( python_alias, doc )
+ {
+ init();
+ }
+
+private:
+ struct converter
+ {
+ static PyObject* convert(EnumWrapper const& v)
+ {
+ // Redirect conversion to a static method of our base class's
+ // base class. A free template converter will not work because
+ // the base_type::base typedef is protected.
+ // Lets hope MSVC agrees that this is legal C++
+ using namespace boost::python::converter;
+ return base_type::base::to_python(
+ registered<native_type>::converters.m_class_object
+ , static_cast<long>( v ));
+
+ }
+ };
+
+ void init() {
+ boost::python::implicitly_convertible<native_type, EnumWrapper>();
+ boost::python::to_python_converter<EnumWrapper, converter >();
+
+ for (unsigned i = 0; i < EnumWrapper::MAX; ++i)
+ {
+ // Register the strings already defined for this enum.
+ base_type::value( EnumWrapper::get_string( i ), native_type( i ) );
+ }
+ }
+
+};
+
+} // end of namespace mapnik
+
+#endif // MAPNIK_PYTHON_BINDING_ENUMERATION_INCLUDED
diff --git a/src/mapnik_enumeration_wrapper_converter.hpp b/src/mapnik_enumeration_wrapper_converter.hpp
new file mode 100644
index 0000000..45e5f7f
--- /dev/null
+++ b/src/mapnik_enumeration_wrapper_converter.hpp
@@ -0,0 +1,45 @@
+/*****************************************************************************
+ *
+ * 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 MAPNIK_BINDINGS_PYTHON_ENUMERATION_WRAPPPER
+#define MAPNIK_BINDINGS_PYTHON_ENUMERATION_WRAPPPER
+
+// mapnik
+#include <mapnik/symbolizer.hpp>
+
+// boost
+#include <boost/python.hpp>
+
+
+namespace boost { namespace python {
+
+ struct mapnik_enumeration_wrapper_to_python
+ {
+ static PyObject* convert(mapnik::enumeration_wrapper const& v)
+ {
+ return ::PyLong_FromLongLong(v.value); // FIXME: this is a temp hack!!
+ }
+ };
+
+}}
+
+#endif // MAPNIK_BINDINGS_PYTHON_ENUMERATION_WRAPPPER
diff --git a/src/mapnik_envelope.cpp b/src/mapnik_envelope.cpp
new file mode 100644
index 0000000..2104c4f
--- /dev/null
+++ b/src/mapnik_envelope.cpp
@@ -0,0 +1,301 @@
+/*****************************************************************************
+ *
+ * This file is part of Mapnik (c++ mapping toolkit)
+ *
+ * Copyright (C) 2015 Artem Pavlenko, Jean-Francois Doyon
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ *****************************************************************************/
+
+#include <mapnik/config.hpp>
+
+// boost
+#include "boost_std_shared_shim.hpp"
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#pragma GCC diagnostic ignored "-Wunused-local-typedef"
+#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
+#include <boost/python.hpp>
+#pragma GCC diagnostic pop
+
+// mapnik
+#include <mapnik/box2d.hpp>
+#include <mapnik/value_error.hpp>
+
+using mapnik::coord;
+using mapnik::box2d;
+
+struct envelope_pickle_suite : boost::python::pickle_suite
+{
+ static boost::python::tuple
+ getinitargs(const box2d<double>& e)
+ {
+ using namespace boost::python;
+ return boost::python::make_tuple(e.minx(),e.miny(),e.maxx(),e.maxy());
+ }
+};
+
+box2d<double> from_string(std::string const& s)
+{
+ box2d<double> bbox;
+ bool success = bbox.from_string(s);
+ if (success)
+ {
+ return bbox;
+ }
+ else
+ {
+ std::stringstream ss;
+ ss << "Could not parse bbox from string: '" << s << "'";
+ throw mapnik::value_error(ss.str());
+ }
+}
+
+//define overloads here
+void (box2d<double>::*width_p1)(double) = &box2d<double>::width;
+double (box2d<double>::*width_p2)() const = &box2d<double>::width;
+
+void (box2d<double>::*height_p1)(double) = &box2d<double>::height;
+double (box2d<double>::*height_p2)() const = &box2d<double>::height;
+
+void (box2d<double>::*expand_to_include_p1)(double,double) = &box2d<double>::expand_to_include;
+void (box2d<double>::*expand_to_include_p2)(coord<double,2> const& ) = &box2d<double>::expand_to_include;
+void (box2d<double>::*expand_to_include_p3)(box2d<double> const& ) = &box2d<double>::expand_to_include;
+
+bool (box2d<double>::*contains_p1)(double,double) const = &box2d<double>::contains;
+bool (box2d<double>::*contains_p2)(coord<double,2> const&) const = &box2d<double>::contains;
+bool (box2d<double>::*contains_p3)(box2d<double> const&) const = &box2d<double>::contains;
+
+//intersects
+bool (box2d<double>::*intersects_p1)(double,double) const = &box2d<double>::intersects;
+bool (box2d<double>::*intersects_p2)(coord<double,2> const&) const = &box2d<double>::intersects;
+bool (box2d<double>::*intersects_p3)(box2d<double> const&) const = &box2d<double>::intersects;
+
+// intersect
+box2d<double> (box2d<double>::*intersect)(box2d<double> const&) const = &box2d<double>::intersect;
+
+// re_center
+void (box2d<double>::*re_center_p1)(double,double) = &box2d<double>::re_center;
+void (box2d<double>::*re_center_p2)(coord<double,2> const& ) = &box2d<double>::re_center;
+
+// clip
+void (box2d<double>::*clip)(box2d<double> const&) = &box2d<double>::clip;
+
+// pad
+void (box2d<double>::*pad)(double) = &box2d<double>::pad;
+
+// deepcopy
+box2d<double> box2d_deepcopy(box2d<double> & obj, boost::python::dict const&)
+{
+ // FIXME::ignore memo for now
+ box2d<double> result(obj);
+ return result;
+}
+
+void export_envelope()
+{
+ using namespace boost::python;
+ class_<box2d<double> >("Box2d",
+ // class docstring is in mapnik/__init__.py, class _Coord
+ init<double,double,double,double>(
+ (arg("minx"),arg("miny"),arg("maxx"),arg("maxy")),
+ "Constructs a new envelope from the coordinates\n"
+ "of its lower left and upper right corner points.\n"))
+ .def(init<>("Equivalent to Box2d(0, 0, -1, -1).\n"))
+ .def(init<const coord<double,2>&, const coord<double,2>&>(
+ (arg("ll"),arg("ur")),
+ "Equivalent to Box2d(ll.x, ll.y, ur.x, ur.y).\n"))
+ .def("from_string",from_string)
+ .staticmethod("from_string")
+ .add_property("minx", &box2d<double>::minx,
+ "X coordinate for the lower left corner")
+ .add_property("miny", &box2d<double>::miny,
+ "Y coordinate for the lower left corner")
+ .add_property("maxx", &box2d<double>::maxx,
+ "X coordinate for the upper right corner")
+ .add_property("maxy", &box2d<double>::maxy,
+ "Y coordinate for the upper right corner")
+ .def("center", &box2d<double>::center,
+ "Returns the coordinates of the center of the bounding box.\n"
+ "\n"
+ "Example:\n"
+ ">>> e = Box2d(0, 0, 100, 100)\n"
+ ">>> e.center()\n"
+ "Coord(50, 50)\n")
+ .def("center", re_center_p1,
+ (arg("x"), arg("y")),
+ "Moves the envelope so that the given coordinates become its new center.\n"
+ "The width and the height are preserved.\n"
+ "\n "
+ "Example:\n"
+ ">>> e = Box2d(0, 0, 100, 100)\n"
+ ">>> e.center(60, 60)\n"
+ ">>> e.center()\n"
+ "Coord(60.0,60.0)\n"
+ ">>> (e.width(), e.height())\n"
+ "(100.0, 100.0)\n"
+ ">>> e\n"
+ "Box2d(10.0, 10.0, 110.0, 110.0)\n"
+ )
+ .def("center", re_center_p2,
+ (arg("Coord")),
+ "Moves the envelope so that the given coordinates become its new center.\n"
+ "The width and the height are preserved.\n"
+ "\n "
+ "Example:\n"
+ ">>> e = Box2d(0, 0, 100, 100)\n"
+ ">>> e.center(Coord60, 60)\n"
+ ">>> e.center()\n"
+ "Coord(60.0,60.0)\n"
+ ">>> (e.width(), e.height())\n"
+ "(100.0, 100.0)\n"
+ ">>> e\n"
+ "Box2d(10.0, 10.0, 110.0, 110.0)\n"
+ )
+ .def("clip", clip,
+ (arg("other")),
+ "Clip the envelope based on the bounds of another envelope.\n"
+ "\n "
+ "Example:\n"
+ ">>> e = Box2d(0, 0, 100, 100)\n"
+ ">>> c = Box2d(-50, -50, 50, 50)\n"
+ ">>> e.clip(c)\n"
+ ">>> e\n"
+ "Box2d(0.0,0.0,50.0,50.0\n"
+ )
+ .def("pad", pad,
+ (arg("padding")),
+ "Pad the envelope based on a padding value.\n"
+ "\n "
+ "Example:\n"
+ ">>> e = Box2d(0, 0, 100, 100)\n"
+ ">>> e.pad(10)\n"
+ ">>> e\n"
+ "Box2d(-10.0,-10.0,110.0,110.0\n"
+ )
+ .def("width", width_p1,
+ (arg("new_width")),
+ "Sets the width to new_width of the envelope preserving its center.\n"
+ "\n "
+ "Example:\n"
+ ">>> e = Box2d(0, 0, 100, 100)\n"
+ ">>> e.width(120)\n"
+ ">>> e.center()\n"
+ "Coord(50.0,50.0)\n"
+ ">>> e\n"
+ "Box2d(-10.0, 0.0, 110.0, 100.0)\n"
+ )
+ .def("width", width_p2,
+ "Returns the width of this envelope.\n"
+ )
+ .def("height", height_p1,
+ (arg("new_height")),
+ "Sets the height to new_height of the envelope preserving its center.\n"
+ "\n "
+ "Example:\n"
+ ">>> e = Box2d(0, 0, 100, 100)\n"
+ ">>> e.height(120)\n"
+ ">>> e.center()\n"
+ "Coord(50.0,50.0)\n"
+ ">>> e\n"
+ "Box2d(0.0, -10.0, 100.0, 110.0)\n"
+ )
+ .def("height", height_p2,
+ "Returns the height of this envelope.\n"
+ )
+ .def("expand_to_include",expand_to_include_p1,
+ (arg("x"),arg("y")),
+ "Expands this envelope to include the point given by x and y.\n"
+ "\n"
+ "Example:\n",
+ ">>> e = Box2d(0, 0, 100, 100)\n"
+ ">>> e.expand_to_include(110, 110)\n"
+ ">>> e\n"
+ "Box2d(0.0, 00.0, 110.0, 110.0)\n"
+ )
+ .def("expand_to_include",expand_to_include_p2,
+ (arg("p")),
+ "Equivalent to expand_to_include(p.x, p.y)\n"
+ )
+ .def("expand_to_include",expand_to_include_p3,
+ (arg("other")),
+ "Equivalent to:\n"
+ " expand_to_include(other.minx, other.miny)\n"
+ " expand_to_include(other.maxx, other.maxy)\n"
+ )
+ .def("contains",contains_p1,
+ (arg("x"),arg("y")),
+ "Returns True iff this envelope contains the point\n"
+ "given by x and y.\n"
+ )
+ .def("contains",contains_p2,
+ (arg("p")),
+ "Equivalent to contains(p.x, p.y)\n"
+ )
+ .def("contains",contains_p3,
+ (arg("other")),
+ "Equivalent to:\n"
+ " contains(other.minx, other.miny) and contains(other.maxx, other.maxy)\n"
+ )
+ .def("intersects",intersects_p1,
+ (arg("x"),arg("y")),
+ "Returns True iff this envelope intersects the point\n"
+ "given by x and y.\n"
+ "\n"
+ "Note: For points, intersection is equivalent\n"
+ "to containment, i.e. the following holds:\n"
+ " e.contains(x, y) == e.intersects(x, y)\n"
+ )
+ .def("intersects",intersects_p2,
+ (arg("p")),
+ "Equivalent to contains(p.x, p.y)\n")
+ .def("intersects",intersects_p3,
+ (arg("other")),
+ "Returns True iff this envelope intersects the other envelope,\n"
+ "This relationship is symmetric."
+ "\n"
+ "Example:\n"
+ ">>> e1 = Box2d(0, 0, 100, 100)\n"
+ ">>> e2 = Box2d(50, 50, 150, 150)\n"
+ ">>> e1.intersects(e2)\n"
+ "True\n"
+ ">>> e1.contains(e2)\n"
+ "False\n"
+ )
+ .def("intersect",intersect,
+ (arg("other")),
+ "Returns the overlap of this envelope and the other envelope\n"
+ "as a new envelope.\n"
+ "\n"
+ "Example:\n"
+ ">>> e1 = Box2d(0, 0, 100, 100)\n"
+ ">>> e2 = Box2d(50, 50, 150, 150)\n"
+ ">>> e1.intersect(e2)\n"
+ "Box2d(50.0, 50.0, 100.0, 100.0)\n"
+ )
+ .def(self == self) // __eq__
+ .def(self != self) // __neq__
+ .def(self + self) // __add__
+ .def(self * float()) // __mult__
+ .def(float() * self)
+ .def(self / float()) // __div__
+ .def("__getitem__",&box2d<double>::operator[])
+ .def("valid",&box2d<double>::valid)
+ .def_pickle(envelope_pickle_suite())
+ .def("__deepcopy__", &box2d_deepcopy)
+ ;
+
+}
diff --git a/src/mapnik_expression.cpp b/src/mapnik_expression.cpp
new file mode 100644
index 0000000..0a07482
--- /dev/null
+++ b/src/mapnik_expression.cpp
@@ -0,0 +1,111 @@
+/*****************************************************************************
+ *
+ * This file is part of Mapnik (c++ mapping toolkit)
+ *
+ * Copyright (C) 2015 Artem Pavlenko, Jean-Francois Doyon
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ *****************************************************************************/
+
+#include <mapnik/config.hpp>
+#include "python_to_value.hpp"
+
+// boost
+#include "boost_std_shared_shim.hpp"
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#pragma GCC diagnostic ignored "-Wunused-local-typedef"
+#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
+
+#include <boost/python.hpp>
+#include <boost/noncopyable.hpp>
+#pragma GCC diagnostic pop
+
+// mapnik
+#include <mapnik/util/variant.hpp>
+#include <mapnik/feature.hpp>
+#include <mapnik/expression.hpp>
+#include <mapnik/expression_string.hpp>
+#include <mapnik/expression_evaluator.hpp>
+#include <mapnik/parse_path.hpp>
+#include <mapnik/value.hpp>
+
+using mapnik::expression_ptr;
+using mapnik::parse_expression;
+using mapnik::to_expression_string;
+using mapnik::path_expression_ptr;
+
+
+// expression
+expression_ptr parse_expression_(std::string const& wkt)
+{
+ return parse_expression(wkt);
+}
+
+std::string expression_to_string_(mapnik::expr_node const& expr)
+{
+ return mapnik::to_expression_string(expr);
+}
+
+mapnik::value expression_evaluate_(mapnik::expr_node const& expr, mapnik::feature_impl const& f, boost::python::dict const& d)
+{
+ // will be auto-converted to proper python type by `mapnik_value_to_python`
+ return mapnik::util::apply_visitor(mapnik::evaluate<mapnik::feature_impl,mapnik::value,mapnik::attributes>(f,mapnik::dict2attr(d)),expr);
+}
+
+bool expression_evaluate_to_bool_(mapnik::expr_node const& expr, mapnik::feature_impl const& f, boost::python::dict const& d)
+{
+ return mapnik::util::apply_visitor(mapnik::evaluate<mapnik::feature_impl,mapnik::value,mapnik::attributes>(f,mapnik::dict2attr(d)),expr).to_bool();
+}
+
+// path expression
+path_expression_ptr parse_path_(std::string const& path)
+{
+ return mapnik::parse_path(path);
+}
+
+std::string path_to_string_(mapnik::path_expression const& expr)
+{
+ return mapnik::path_processor_type::to_string(expr);
+}
+
+std::string path_evaluate_(mapnik::path_expression const& expr, mapnik::feature_impl const& f)
+{
+ return mapnik::path_processor_type::evaluate(expr, f);
+}
+
+void export_expression()
+{
+ using namespace boost::python;
+ class_<mapnik::expr_node ,boost::noncopyable>("Expression",
+ "TODO"
+ "",no_init)
+ .def("evaluate", &expression_evaluate_,(arg("feature"),arg("variables")=boost::python::dict()))
+ .def("to_bool", &expression_evaluate_to_bool_,(arg("feature"),arg("variables")=boost::python::dict()))
+ .def("__str__",&expression_to_string_);
+ ;
+
+ def("Expression",&parse_expression_,(arg("expr")),"Expression string");
+
+ class_<mapnik::path_expression ,boost::noncopyable>("PathExpression",
+ "TODO"
+ "",no_init)
+ .def("evaluate", &path_evaluate_) // note: "pass" is a reserved word in Python
+ .def("__str__",&path_to_string_);
+ ;
+
+ def("PathExpression",&parse_path_,(arg("expr")),"PathExpression string");
+}
diff --git a/src/mapnik_feature.cpp b/src/mapnik_feature.cpp
new file mode 100644
index 0000000..a80ab15
--- /dev/null
+++ b/src/mapnik_feature.cpp
@@ -0,0 +1,237 @@
+/*****************************************************************************
+ *
+ * This file is part of Mapnik (c++ mapping toolkit)
+ *
+ * Copyright (C) 2015 Artem Pavlenko, Jean-Francois Doyon
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ *****************************************************************************/
+
+#include <mapnik/config.hpp>
+
+// boost
+#include "boost_std_shared_shim.hpp"
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#pragma GCC diagnostic ignored "-Wunused-local-typedef"
+#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
+
+#include <boost/python.hpp>
+#include <boost/python/iterator.hpp>
+#include <boost/python/call_method.hpp>
+#include <boost/python/tuple.hpp>
+#include <boost/python/to_python_converter.hpp>
+#include <boost/noncopyable.hpp>
+#pragma GCC diagnostic pop
+
+// mapnik
+#include <mapnik/value_types.hpp>
+#include <mapnik/feature.hpp>
+#include <mapnik/feature_factory.hpp>
+#include <mapnik/feature_kv_iterator.hpp>
+#include <mapnik/datasource.hpp>
+#include <mapnik/wkb.hpp>
+//#include <mapnik/wkt/wkt_factory.hpp>
+#include <mapnik/json/feature_parser.hpp>
+#include <mapnik/json/feature_generator.hpp>
+
+// stl
+#include <stdexcept>
+
+namespace {
+
+using mapnik::geometry_utils;
+using mapnik::context_type;
+using mapnik::context_ptr;
+using mapnik::feature_kv_iterator;
+
+mapnik::feature_ptr from_geojson_impl(std::string const& json, mapnik::context_ptr const& ctx)
+{
+ mapnik::feature_ptr feature(mapnik::feature_factory::create(ctx,1));
+ if (!mapnik::json::from_geojson(json,*feature))
+ {
+ throw std::runtime_error("Failed to parse geojson feature");
+ }
+ return feature;
+}
+
+std::string feature_to_geojson(mapnik::feature_impl const& feature)
+{
+ std::string json;
+ if (!mapnik::json::to_geojson(json,feature))
+ {
+ throw std::runtime_error("Failed to generate GeoJSON");
+ }
+ return json;
+}
+
+mapnik::value __getitem__(mapnik::feature_impl const& feature, std::string const& name)
+{
+ return feature.get(name);
+}
+
+mapnik::value __getitem2__(mapnik::feature_impl const& feature, std::size_t index)
+{
+ return feature.get(index);
+}
+
+void __setitem__(mapnik::feature_impl & feature, std::string const& name, mapnik::value const& val)
+{
+ feature.put_new(name,val);
+}
+
+boost::python::dict attributes(mapnik::feature_impl const& f)
+{
+ boost::python::dict attributes;
+ feature_kv_iterator itr = f.begin();
+ feature_kv_iterator end = f.end();
+
+ for ( ;itr!=end; ++itr)
+ {
+ attributes[std::get<0>(*itr)] = std::get<1>(*itr);
+ }
+
+ return attributes;
+}
+
+} // end anonymous namespace
+
+struct unicode_string_from_python_str
+{
+ unicode_string_from_python_str()
+ {
+ boost::python::converter::registry::push_back(
+ &convertible,
+ &construct,
+ boost::python::type_id<mapnik::value_unicode_string>());
+ }
+
+ static void* convertible(PyObject* obj_ptr)
+ {
+ if (!(
+#if PY_VERSION_HEX >= 0x03000000
+ PyBytes_Check(obj_ptr)
+#else
+ PyString_Check(obj_ptr)
+#endif
+ || PyUnicode_Check(obj_ptr)))
+ return 0;
+ return obj_ptr;
+ }
+
+ static void construct(
+ PyObject* obj_ptr,
+ boost::python::converter::rvalue_from_python_stage1_data* data)
+ {
+ char * value=0;
+ if (PyUnicode_Check(obj_ptr)) {
+ PyObject *encoded = PyUnicode_AsEncodedString(obj_ptr, "utf8", "replace");
+ if (encoded) {
+#if PY_VERSION_HEX >= 0x03000000
+ value = PyBytes_AsString(encoded);
+#else
+ value = PyString_AsString(encoded);
+#endif
+ Py_DecRef(encoded);
+ }
+ } else {
+#if PY_VERSION_HEX >= 0x03000000
+ value = PyBytes_AsString(obj_ptr);
+#else
+ value = PyString_AsString(obj_ptr);
+#endif
+ }
+ if (value == 0) boost::python::throw_error_already_set();
+ void* storage = (
+ (boost::python::converter::rvalue_from_python_storage<mapnik::value_unicode_string>*)
+ data)->storage.bytes;
+ new (storage) mapnik::value_unicode_string(value);
+ data->convertible = storage;
+ }
+};
+
+
+struct value_null_from_python
+{
+ value_null_from_python()
+ {
+ boost::python::converter::registry::push_back(
+ &convertible,
+ &construct,
+ boost::python::type_id<mapnik::value_null>());
+ }
+
+ static void* convertible(PyObject* obj_ptr)
+ {
+ if (obj_ptr == Py_None) return obj_ptr;
+ return 0;
+ }
+
+ static void construct(
+ PyObject* obj_ptr,
+ boost::python::converter::rvalue_from_python_stage1_data* data)
+ {
+ if (obj_ptr != Py_None) boost::python::throw_error_already_set();
+ void* storage = (
+ (boost::python::converter::rvalue_from_python_storage<mapnik::value_null>*)
+ data)->storage.bytes;
+ new (storage) mapnik::value_null();
+ data->convertible = storage;
+ }
+};
+
+void export_feature()
+{
+ using namespace boost::python;
+
+ // Python to mapnik::value converters
+ // NOTE: order matters here. For example value_null must be listed before
+ // bool otherwise Py_None will be interpreted as bool (false)
+ implicitly_convertible<mapnik::value_unicode_string,mapnik::value>();
+ implicitly_convertible<mapnik::value_null,mapnik::value>();
+ implicitly_convertible<mapnik::value_integer,mapnik::value>();
+ implicitly_convertible<mapnik::value_double,mapnik::value>();
+ implicitly_convertible<mapnik::value_bool,mapnik::value>();
+
+ // http://misspent.wordpress.com/2009/09/27/how-to-write-boost-python-converters/
+ unicode_string_from_python_str();
+ value_null_from_python();
+
+ class_<context_type,context_ptr,boost::noncopyable>
+ ("Context",init<>("Default ctor."))
+ .def("push", &context_type::push)
+ ;
+
+ class_<mapnik::feature_impl,std::shared_ptr<mapnik::feature_impl>,
+ boost::noncopyable>("Feature",init<context_ptr,mapnik::value_integer>("Default ctor."))
+ .def("id",&mapnik::feature_impl::id)
+ .add_property("geometry",
+ make_function(&mapnik::feature_impl::get_geometry,return_value_policy<reference_existing_object>()),
+ &mapnik::feature_impl::set_geometry_copy)
+ .def("envelope", &mapnik::feature_impl::envelope)
+ .def("has_key", &mapnik::feature_impl::has_key)
+ .add_property("attributes",&attributes)
+ .def("__setitem__",&__setitem__)
+ .def("__contains__",&__getitem__)
+ .def("__getitem__",&__getitem__)
+ .def("__getitem__",&__getitem2__)
+ .def("__len__", &mapnik::feature_impl::size)
+ .def("context",&mapnik::feature_impl::context)
+ .def("to_geojson",&feature_to_geojson)
+ .def("from_geojson",from_geojson_impl)
+ .staticmethod("from_geojson")
+ ;
+}
diff --git a/src/mapnik_featureset.cpp b/src/mapnik_featureset.cpp
new file mode 100644
index 0000000..f239a78
--- /dev/null
+++ b/src/mapnik_featureset.cpp
@@ -0,0 +1,93 @@
+/*****************************************************************************
+ *
+ * This file is part of Mapnik (c++ mapping toolkit)
+ *
+ * Copyright (C) 2015 Artem Pavlenko, Jean-Francois Doyon
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ *****************************************************************************/
+
+#include <mapnik/config.hpp>
+
+// boost
+#include "boost_std_shared_shim.hpp"
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#pragma GCC diagnostic ignored "-Wunused-local-typedef"
+#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
+
+#include <boost/python.hpp>
+#include <boost/noncopyable.hpp>
+#pragma GCC diagnostic pop
+
+// mapnik
+#include <mapnik/feature.hpp>
+#include <mapnik/datasource.hpp>
+
+namespace {
+using namespace boost::python;
+
+inline list features(mapnik::featureset_ptr const& itr)
+{
+ list l;
+ while (true)
+ {
+ mapnik::feature_ptr fp = itr->next();
+ if (!fp)
+ {
+ break;
+ }
+ l.append(fp);
+ }
+ return l;
+}
+
+inline object pass_through(object const& o) { return o; }
+
+inline mapnik::feature_ptr next(mapnik::featureset_ptr const& itr)
+{
+ mapnik::feature_ptr f = itr->next();
+ if (!f)
+ {
+ PyErr_SetString(PyExc_StopIteration, "No more features.");
+ boost::python::throw_error_already_set();
+ }
+
+ return f;
+}
+
+}
+
+void export_featureset()
+{
+ using namespace boost::python;
+ class_<mapnik::Featureset,std::shared_ptr<mapnik::Featureset>,
+ boost::noncopyable>("Featureset",no_init)
+ .def("__iter__",pass_through)
+ .def("next",next)
+ .add_property("features",features,
+ "The list of features.\n"
+ "\n"
+ "Usage:\n"
+ ">>> m.query_map_point(0, 10, 10)\n"
+ "<mapnik._mapnik.Featureset object at 0x1004d2938>\n"
+ ">>> fs = m.query_map_point(0, 10, 10)\n"
+ ">>> for f in fs.features:\n"
+ ">>> print f\n"
+ "<mapnik.Feature object at 0x105e64140>\n"
+ )
+ ;
+}
diff --git a/src/mapnik_font_engine.cpp b/src/mapnik_font_engine.cpp
new file mode 100644
index 0000000..e3a881f
--- /dev/null
+++ b/src/mapnik_font_engine.cpp
@@ -0,0 +1,60 @@
+/*****************************************************************************
+ *
+ * This file is part of Mapnik (c++ mapping toolkit)
+ *
+ * Copyright (C) 2015 Artem Pavlenko, Jean-Francois Doyon
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ *****************************************************************************/
+
+#include <mapnik/config.hpp>
+
+// boost
+#include "boost_std_shared_shim.hpp"
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#pragma GCC diagnostic ignored "-Wunused-local-typedef"
+#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
+
+#include <boost/python.hpp>
+#include <boost/noncopyable.hpp>
+#pragma GCC diagnostic pop
+
+#include <mapnik/font_engine_freetype.hpp>
+#include <mapnik/util/singleton.hpp>
+
+void export_font_engine()
+{
+ using mapnik::freetype_engine;
+ using mapnik::singleton;
+ using mapnik::CreateStatic;
+ using namespace boost::python;
+ class_<singleton<freetype_engine,CreateStatic>,boost::noncopyable>("Singleton",no_init)
+ .def("instance",&singleton<freetype_engine,CreateStatic>::instance,
+ return_value_policy<reference_existing_object>())
+ .staticmethod("instance")
+ ;
+
+ class_<freetype_engine,bases<singleton<freetype_engine,CreateStatic> >,
+ boost::noncopyable>("FontEngine",no_init)
+ .def("register_font",&freetype_engine::register_font)
+ .def("register_fonts",&freetype_engine::register_fonts)
+ .def("face_names",&freetype_engine::face_names)
+ .staticmethod("register_font")
+ .staticmethod("register_fonts")
+ .staticmethod("face_names")
+ ;
+}
diff --git a/src/mapnik_fontset.cpp b/src/mapnik_fontset.cpp
new file mode 100644
index 0000000..9d109a7
--- /dev/null
+++ b/src/mapnik_fontset.cpp
@@ -0,0 +1,64 @@
+/*****************************************************************************
+ *
+ * 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
+ *
+ *****************************************************************************/
+
+#include <mapnik/config.hpp>
+
+// boost
+#include "boost_std_shared_shim.hpp"
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#pragma GCC diagnostic ignored "-Wunused-local-typedef"
+#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
+
+#include <boost/python.hpp>
+#pragma GCC diagnostic pop
+
+//mapnik
+#include <mapnik/font_set.hpp>
+
+
+using mapnik::font_set;
+
+void export_fontset ()
+{
+ using namespace boost::python;
+ class_<font_set>("FontSet", init<std::string const&>("default fontset constructor")
+ )
+ .add_property("name",
+ make_function(&font_set::get_name,return_value_policy<copy_const_reference>()),
+ &font_set::set_name,
+ "Get/Set the name of the FontSet.\n"
+ )
+ .def("add_face_name",&font_set::add_face_name,
+ (arg("name")),
+ "Add a face-name to the fontset.\n"
+ "\n"
+ "Example:\n"
+ ">>> fs = Fontset('book-fonts')\n"
+ ">>> fs.add_face_name('DejaVu Sans Book')\n")
+ .add_property("names",make_function
+ (&font_set::get_face_names,
+ return_value_policy<reference_existing_object>()),
+ "List of face names belonging to a FontSet.\n"
+ )
+ ;
+}
diff --git a/src/mapnik_gamma_method.cpp b/src/mapnik_gamma_method.cpp
new file mode 100644
index 0000000..9e6b478
--- /dev/null
+++ b/src/mapnik_gamma_method.cpp
@@ -0,0 +1,49 @@
+/*****************************************************************************
+ *
+ * This file is part of Mapnik (c++ mapping toolkit)
+ *
+ * Copyright (C) 2015 Artem Pavlenko, Jean-Francois Doyon
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ *****************************************************************************/
+
+#include <mapnik/config.hpp>
+
+// boost
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#pragma GCC diagnostic ignored "-Wunused-local-typedef"
+#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
+
+#include <boost/python.hpp>
+#pragma GCC diagnostic pop
+
+#include <mapnik/symbolizer_enumerations.hpp>
+#include "mapnik_enumeration.hpp"
+
+void export_gamma_method()
+{
+ using namespace boost::python;
+
+ mapnik::enumeration_<mapnik::gamma_method_e>("gamma_method")
+ .value("POWER", mapnik::GAMMA_POWER)
+ .value("LINEAR",mapnik::GAMMA_LINEAR)
+ .value("NONE", mapnik::GAMMA_NONE)
+ .value("THRESHOLD", mapnik::GAMMA_THRESHOLD)
+ .value("MULTIPLY", mapnik::GAMMA_MULTIPLY)
+ ;
+
+}
diff --git a/src/mapnik_geometry.cpp b/src/mapnik_geometry.cpp
new file mode 100644
index 0000000..dee9de4
--- /dev/null
+++ b/src/mapnik_geometry.cpp
@@ -0,0 +1,290 @@
+/*****************************************************************************
+ *
+ * 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
+ *
+ *****************************************************************************/
+
+#include <mapnik/config.hpp>
+
+// boost
+#include "boost_std_shared_shim.hpp"
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#pragma GCC diagnostic ignored "-Wunused-local-typedef"
+#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
+
+#include <boost/python.hpp>
+#include <boost/python/def.hpp>
+#include <boost/python/exception_translator.hpp>
+#include <boost/python/manage_new_object.hpp>
+#include <boost/python/iterator.hpp>
+#include <boost/noncopyable.hpp>
+#include <boost/version.hpp>
+#pragma GCC diagnostic pop
+
+// mapnik
+#include <mapnik/geometry.hpp>
+#include <mapnik/geometry_type.hpp>
+#include <mapnik/geometry_envelope.hpp>
+#include <mapnik/geometry_is_valid.hpp>
+#include <mapnik/geometry_is_simple.hpp>
+#include <mapnik/geometry_is_empty.hpp>
+#include <mapnik/geometry_correct.hpp>
+#include <mapnik/geometry_centroid.hpp>
+
+#include <mapnik/wkt/wkt_factory.hpp> // from_wkt
+#include <mapnik/json/geometry_parser.hpp> // from_geojson
+#include <mapnik/util/geometry_to_geojson.hpp> // to_geojson
+#include <mapnik/util/geometry_to_wkb.hpp> // to_wkb
+#include <mapnik/util/geometry_to_wkt.hpp> // to_wkt
+//#include <mapnik/util/geometry_to_svg.hpp>
+#include <mapnik/wkb.hpp>
+
+
+// stl
+#include <stdexcept>
+
+namespace {
+
+std::shared_ptr<mapnik::geometry::geometry<double> > from_wkb_impl(std::string const& wkb)
+{
+ std::shared_ptr<mapnik::geometry::geometry<double> > geom = std::make_shared<mapnik::geometry::geometry<double> >();
+ try
+ {
+ *geom = mapnik::geometry_utils::from_wkb(wkb.c_str(), wkb.size());
+ }
+ catch (...)
+ {
+ throw std::runtime_error("Failed to parse WKB");
+ }
+ return geom;
+}
+
+std::shared_ptr<mapnik::geometry::geometry<double> > from_wkt_impl(std::string const& wkt)
+{
+ std::shared_ptr<mapnik::geometry::geometry<double> > geom = std::make_shared<mapnik::geometry::geometry<double> >();
+ if (!mapnik::from_wkt(wkt, *geom))
+ throw std::runtime_error("Failed to parse WKT geometry");
+ return geom;
+}
+
+std::shared_ptr<mapnik::geometry::geometry<double> > from_geojson_impl(std::string const& json)
+{
+ std::shared_ptr<mapnik::geometry::geometry<double> > geom = std::make_shared<mapnik::geometry::geometry<double> >();
+ if (!mapnik::json::from_geojson(json, *geom))
+ throw std::runtime_error("Failed to parse geojson geometry");
+ return geom;
+}
+
+}
+
+inline std::string boost_version()
+{
+ std::ostringstream s;
+ s << BOOST_VERSION/100000 << "." << BOOST_VERSION/100 % 1000 << "." << BOOST_VERSION % 100;
+ return s.str();
+}
+
+PyObject* to_wkb_impl(mapnik::geometry::geometry<double> const& geom, mapnik::wkbByteOrder byte_order)
+{
+ mapnik::util::wkb_buffer_ptr wkb = mapnik::util::to_wkb(geom,byte_order);
+ if (wkb)
+ {
+ return
+#if PY_VERSION_HEX >= 0x03000000
+ ::PyBytes_FromStringAndSize
+#else
+ ::PyString_FromStringAndSize
+#endif
+ ((const char*)wkb->buffer(),wkb->size());
+ }
+ else
+ {
+ Py_RETURN_NONE;
+ }
+}
+
+std::string to_geojson_impl(mapnik::geometry::geometry<double> const& geom)
+{
+ std::string wkt;
+ if (!mapnik::util::to_geojson(wkt, geom))
+ {
+ throw std::runtime_error("Generate JSON failed");
+ }
+ return wkt;
+}
+
+std::string to_wkt_impl(mapnik::geometry::geometry<double> const& geom)
+{
+ std::string wkt;
+ if (!mapnik::util::to_wkt(wkt,geom))
+ {
+ throw std::runtime_error("Generate WKT failed");
+ }
+ return wkt;
+}
+
+mapnik::geometry::geometry_types geometry_type_impl(mapnik::geometry::geometry<double> const& geom)
+{
+ return mapnik::geometry::geometry_type(geom);
+}
+
+mapnik::box2d<double> geometry_envelope_impl(mapnik::geometry::geometry<double> const& geom)
+{
+ return mapnik::geometry::envelope(geom);
+}
+
+// only Boost >= 1.56 contains the is_valid and is_simple functions
+#if BOOST_VERSION >= 105600
+bool geometry_is_valid_impl(mapnik::geometry::geometry<double> const& geom)
+{
+ return mapnik::geometry::is_valid(geom);
+}
+
+bool geometry_is_simple_impl(mapnik::geometry::geometry<double> const& geom)
+{
+ return mapnik::geometry::is_simple(geom);
+}
+#endif
+
+bool geometry_is_empty_impl(mapnik::geometry::geometry<double> const& geom)
+{
+ return mapnik::geometry::is_empty(geom);
+}
+
+void geometry_correct_impl(mapnik::geometry::geometry<double> & geom)
+{
+ mapnik::geometry::correct(geom);
+}
+
+void polygon_set_exterior_impl(mapnik::geometry::polygon<double> & poly, mapnik::geometry::linear_ring<double> const& ring)
+{
+ poly.exterior_ring = ring; // copy
+}
+
+void polygon_add_hole_impl(mapnik::geometry::polygon<double> & poly, mapnik::geometry::linear_ring<double> const& ring)
+{
+ poly.interior_rings.push_back(ring); // copy
+}
+
+mapnik::geometry::point<double> geometry_centroid_impl(mapnik::geometry::geometry<double> const& geom)
+{
+ mapnik::geometry::point<double> pt;
+ mapnik::geometry::centroid(geom, pt);
+ return pt;
+}
+
+
+void export_geometry()
+{
+ using namespace boost::python;
+
+ implicitly_convertible<mapnik::geometry::point<double>, mapnik::geometry::geometry<double> >();
+ implicitly_convertible<mapnik::geometry::line_string<double>, mapnik::geometry::geometry<double> >();
+ implicitly_convertible<mapnik::geometry::polygon<double>, mapnik::geometry::geometry<double> >();
+ enum_<mapnik::geometry::geometry_types>("GeometryType")
+ .value("Unknown",mapnik::geometry::geometry_types::Unknown)
+ .value("Point",mapnik::geometry::geometry_types::Point)
+ .value("LineString",mapnik::geometry::geometry_types::LineString)
+ .value("Polygon",mapnik::geometry::geometry_types::Polygon)
+ .value("MultiPoint",mapnik::geometry::geometry_types::MultiPoint)
+ .value("MultiLineString",mapnik::geometry::geometry_types::MultiLineString)
+ .value("MultiPolygon",mapnik::geometry::geometry_types::MultiPolygon)
+ .value("GeometryCollection",mapnik::geometry::geometry_types::GeometryCollection)
+ ;
+
+ enum_<mapnik::wkbByteOrder>("wkbByteOrder")
+ .value("XDR",mapnik::wkbXDR)
+ .value("NDR",mapnik::wkbNDR)
+ ;
+
+ using mapnik::geometry::geometry;
+ using mapnik::geometry::point;
+ using mapnik::geometry::line_string;
+ using mapnik::geometry::linear_ring;
+ using mapnik::geometry::polygon;
+
+ class_<point<double> >("Point", init<double, double>((arg("x"), arg("y")),
+ "Constructs a new Point object\n"))
+ .add_property("x", &point<double>::x, "X coordinate")
+ .add_property("y", &point<double>::y, "Y coordinate")
+#if BOOST_VERSION >= 105600
+ .def("is_valid", &geometry_is_valid_impl)
+ .def("is_simple", &geometry_is_simple_impl)
+#endif
+ .def("to_geojson",&to_geojson_impl)
+ .def("to_wkb",&to_wkb_impl)
+ .def("to_wkt",&to_wkt_impl)
+ ;
+
+ class_<line_string<double> >("LineString", init<>(
+ "Constructs a new LineString object\n"))
+ .def("add_coord", &line_string<double>::add_coord, "Adds coord")
+#if BOOST_VERSION >= 105600
+ .def("is_valid", &geometry_is_valid_impl)
+ .def("is_simple", &geometry_is_simple_impl)
+#endif
+ .def("to_geojson",&to_geojson_impl)
+ .def("to_wkb",&to_wkb_impl)
+ .def("to_wkt",&to_wkt_impl)
+ ;
+
+ class_<linear_ring<double> >("LinearRing", init<>(
+ "Constructs a new LinearRtring object\n"))
+ .def("add_coord", &linear_ring<double>::add_coord, "Adds coord")
+ ;
+
+ class_<polygon<double> >("Polygon", init<>(
+ "Constructs a new Polygon object\n"))
+ .add_property("exterior_ring", &polygon<double>::exterior_ring , "Exterior ring")
+ .def("add_hole", &polygon_add_hole_impl, "Add interior ring")
+ .def("num_rings", polygon_set_exterior_impl, "Number of rings (at least 1)")
+#if BOOST_VERSION >= 105600
+ .def("is_valid", &geometry_is_valid_impl)
+ .def("is_simple", &geometry_is_simple_impl)
+#endif
+ .def("to_geojson",&to_geojson_impl)
+ .def("to_wkb",&to_wkb_impl)
+ .def("to_wkt",&to_wkt_impl)
+ ;
+
+ class_<geometry<double>, std::shared_ptr<geometry<double> >, boost::noncopyable>("Geometry",no_init)
+ .def("envelope",&geometry_envelope_impl)
+ .def("from_geojson", from_geojson_impl)
+ .def("from_wkt", from_wkt_impl)
+ .def("from_wkb", from_wkb_impl)
+ .staticmethod("from_geojson")
+ .staticmethod("from_wkt")
+ .staticmethod("from_wkb")
+ .def("__str__",&to_wkt_impl)
+ .def("type",&geometry_type_impl)
+#if BOOST_VERSION >= 105600
+ .def("is_valid", &geometry_is_valid_impl)
+ .def("is_simple", &geometry_is_simple_impl)
+#endif
+ .def("is_empty", &geometry_is_empty_impl)
+ .def("correct", &geometry_correct_impl)
+ .def("centroid",&geometry_centroid_impl)
+ .def("to_wkb",&to_wkb_impl)
+ .def("to_wkt",&to_wkt_impl)
+ .def("to_geojson",&to_geojson_impl)
+ //.def("to_svg",&to_svg)
+ // TODO add other geometry_type methods
+ ;
+}
diff --git a/src/mapnik_grid.cpp b/src/mapnik_grid.cpp
new file mode 100644
index 0000000..c1f4b12
--- /dev/null
+++ b/src/mapnik_grid.cpp
@@ -0,0 +1,95 @@
+/*****************************************************************************
+ *
+ * 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
+ *
+ *****************************************************************************/
+
+#if defined(GRID_RENDERER)
+
+#include <mapnik/config.hpp>
+
+// boost
+#include "boost_std_shared_shim.hpp"
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#pragma GCC diagnostic ignored "-Wunused-local-typedef"
+#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
+
+#include <boost/python.hpp>
+#include <boost/python/module.hpp>
+#include <boost/python/def.hpp>
+#pragma GCC diagnostic pop
+
+// mapnik
+#include <mapnik/grid/grid.hpp>
+#include "python_grid_utils.hpp"
+
+using namespace boost::python;
+
+// help compiler see template definitions
+static dict (*encode)( mapnik::grid const&, std::string const& , bool, unsigned int) = mapnik::grid_encode;
+
+bool painted(mapnik::grid const& grid)
+{
+ return grid.painted();
+}
+
+mapnik::grid::value_type get_pixel(mapnik::grid const& grid, int x, int y)
+{
+ if (x < static_cast<int>(grid.width()) && y < static_cast<int>(grid.height()))
+ {
+ mapnik::grid::data_type const & data = grid.data();
+ return data(x,y);
+ }
+ PyErr_SetString(PyExc_IndexError, "invalid x,y for grid dimensions");
+ boost::python::throw_error_already_set();
+ return 0;
+}
+
+void export_grid()
+{
+ class_<mapnik::grid,std::shared_ptr<mapnik::grid> >(
+ "Grid",
+ "This class represents a feature hitgrid.",
+ init<int,int,std::string>(
+ ( boost::python::arg("width"), boost::python::arg("height"),boost::python::arg("key")="__id__"),
+ "Create a mapnik.Grid object\n"
+ ))
+ .def("painted",&painted)
+ .def("width",&mapnik::grid::width)
+ .def("height",&mapnik::grid::height)
+ .def("view",&mapnik::grid::get_view)
+ .def("get_pixel",&get_pixel)
+ .def("clear",&mapnik::grid::clear)
+ .def("encode",encode,
+ ( boost::python::arg("encoding")="utf", boost::python::arg("features")=true,boost::python::arg("resolution")=4 ),
+ "Encode the grid as as optimized json\n"
+ )
+ .add_property("key",
+ make_function(&mapnik::grid::get_key,return_value_policy<copy_const_reference>()),
+ &mapnik::grid::set_key,
+ "Get/Set key to be used as unique indentifier for features\n"
+ "The value should either be __id__ to refer to the feature.id()\n"
+ "or some globally unique integer or string attribute field\n"
+ )
+ ;
+
+}
+
+#endif
diff --git a/src/mapnik_grid_view.cpp b/src/mapnik_grid_view.cpp
new file mode 100644
index 0000000..2357c6b
--- /dev/null
+++ b/src/mapnik_grid_view.cpp
@@ -0,0 +1,64 @@
+/*****************************************************************************
+ *
+ * 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
+ *
+ *****************************************************************************/
+
+#if defined(GRID_RENDERER)
+
+#include <mapnik/config.hpp>
+
+// boost
+#include "boost_std_shared_shim.hpp"
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#pragma GCC diagnostic ignored "-Wunused-local-typedef"
+#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
+
+#include <boost/python.hpp>
+#include <boost/python/module.hpp>
+#include <boost/python/def.hpp>
+#pragma GCC diagnostic pop
+
+// mapnik
+#include <string>
+#include <mapnik/grid/grid_view.hpp>
+#include <mapnik/grid/grid.hpp>
+#include "python_grid_utils.hpp"
+
+using namespace boost::python;
+
+// help compiler see template definitions
+static dict (*encode)( mapnik::grid_view const&, std::string const& , bool, unsigned int) = mapnik::grid_encode;
+
+void export_grid_view()
+{
+ class_<mapnik::grid_view,
+ std::shared_ptr<mapnik::grid_view> >("GridView",
+ "This class represents a feature hitgrid subset.",no_init)
+ .def("width",&mapnik::grid_view::width)
+ .def("height",&mapnik::grid_view::height)
+ .def("encode",encode,
+ ( boost::python::arg("encoding")="utf",boost::python::arg("add_features")=true,boost::python::arg("resolution")=4 ),
+ "Encode the grid as as optimized json\n"
+ )
+ ;
+}
+
+#endif
diff --git a/src/mapnik_image.cpp b/src/mapnik_image.cpp
new file mode 100644
index 0000000..6e6aeac
--- /dev/null
+++ b/src/mapnik_image.cpp
@@ -0,0 +1,471 @@
+/*****************************************************************************
+ *
+ * This file is part of Mapnik (c++ mapping toolkit)
+ *
+ * Copyright (C) 2015 Artem Pavlenko, Jean-Francois Doyon
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ *****************************************************************************/
+
+#include <mapnik/config.hpp>
+
+// boost
+#include "boost_std_shared_shim.hpp"
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#pragma GCC diagnostic ignored "-Wunused-local-typedef"
+#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
+
+#include <boost/python.hpp>
+#include <boost/python/module.hpp>
+#include <boost/python/def.hpp>
+#pragma GCC diagnostic pop
+
+// mapnik
+#include <mapnik/color.hpp>
+#include <mapnik/palette.hpp>
+#include <mapnik/image_util.hpp>
+#include <mapnik/image_copy.hpp>
+#include <mapnik/image_reader.hpp>
+#include <mapnik/image_compositing.hpp>
+#include <mapnik/image_view_any.hpp>
+
+// cairo
+#if defined(HAVE_CAIRO) && defined(HAVE_PYCAIRO)
+#include <mapnik/cairo/cairo_context.hpp>
+#include <mapnik/cairo/cairo_image_util.hpp>
+#include <pycairo.h>
+#include <cairo.h>
+#endif
+
+using mapnik::image_any;
+using mapnik::image_reader;
+using mapnik::get_image_reader;
+using mapnik::type_from_filename;
+using mapnik::save_to_file;
+
+using namespace boost::python;
+
+// output 'raw' pixels
+PyObject* tostring1( image_any const& im)
+{
+ return
+#if PY_VERSION_HEX >= 0x03000000
+ ::PyBytes_FromStringAndSize
+#else
+ ::PyString_FromStringAndSize
+#endif
+ ((const char*)im.bytes(),im.size());
+}
+
+// encode (png,jpeg)
+PyObject* tostring2(image_any const & im, std::string const& format)
+{
+ std::string s = mapnik::save_to_string(im, format);
+ return
+#if PY_VERSION_HEX >= 0x03000000
+ ::PyBytes_FromStringAndSize
+#else
+ ::PyString_FromStringAndSize
+#endif
+ (s.data(),s.size());
+}
+
+PyObject* tostring3(image_any const & im, std::string const& format, mapnik::rgba_palette const& pal)
+{
+ std::string s = mapnik::save_to_string(im, format, pal);
+ return
+#if PY_VERSION_HEX >= 0x03000000
+ ::PyBytes_FromStringAndSize
+#else
+ ::PyString_FromStringAndSize
+#endif
+ (s.data(),s.size());
+}
+
+
+void save_to_file1(mapnik::image_any const& im, std::string const& filename)
+{
+ save_to_file(im,filename);
+}
+
+void save_to_file2(mapnik::image_any const& im, std::string const& filename, std::string const& type)
+{
+ save_to_file(im,filename,type);
+}
+
+void save_to_file3(mapnik::image_any const& im, std::string const& filename, std::string const& type, mapnik::rgba_palette const& pal)
+{
+ save_to_file(im,filename,type,pal);
+}
+
+mapnik::image_view_any get_view(mapnik::image_any const& data,unsigned x,unsigned y, unsigned w,unsigned h)
+{
+ return mapnik::create_view(data,x,y,w,h);
+}
+
+bool is_solid(mapnik::image_any const& im)
+{
+ return mapnik::is_solid(im);
+}
+
+void fill_color(mapnik::image_any & im, mapnik::color const& c)
+{
+ mapnik::fill(im, c);
+}
+
+void fill_int(mapnik::image_any & im, int val)
+{
+ mapnik::fill(im, val);
+}
+
+void fill_double(mapnik::image_any & im, double val)
+{
+ mapnik::fill(im, val);
+}
+
+std::shared_ptr<image_any> copy(mapnik::image_any const& im, mapnik::image_dtype type, double offset, double scaling)
+{
+ return std::make_shared<image_any>(mapnik::image_copy(im, type, offset, scaling));
+}
+
+unsigned compare(mapnik::image_any const& im1, mapnik::image_any const& im2, double threshold, bool alpha)
+{
+ return mapnik::compare(im1, im2, threshold, alpha);
+}
+
+struct get_pixel_visitor
+{
+ get_pixel_visitor(unsigned x, unsigned y)
+ : x_(x), y_(y) {}
+
+ object operator() (mapnik::image_null const&)
+ {
+ throw std::runtime_error("Can not return a null image from a pixel (shouldn't have reached here)");
+ }
+
+ template <typename T>
+ object operator() (T const& im)
+ {
+ using pixel_type = typename T::pixel_type;
+ return object(mapnik::get_pixel<pixel_type>(im, x_, y_));
+ }
+
+ private:
+ unsigned x_;
+ unsigned y_;
+};
+
+object get_pixel(mapnik::image_any const& im, unsigned x, unsigned y, bool get_color)
+{
+ if (x < static_cast<unsigned>(im.width()) && y < static_cast<unsigned>(im.height()))
+ {
+ if (get_color)
+ {
+ return object(
+ mapnik::get_pixel<mapnik::color>(im, x, y)
+ );
+ }
+ else
+ {
+ return mapnik::util::apply_visitor(get_pixel_visitor(x, y), im);
+ }
+ }
+ PyErr_SetString(PyExc_IndexError, "invalid x,y for image dimensions");
+ boost::python::throw_error_already_set();
+ return object();
+}
+
+void set_pixel_color(mapnik::image_any & im, unsigned x, unsigned y, mapnik::color const& c)
+{
+ if (x >= static_cast<int>(im.width()) && y >= static_cast<int>(im.height()))
+ {
+ PyErr_SetString(PyExc_IndexError, "invalid x,y for image dimensions");
+ boost::python::throw_error_already_set();
+ return;
+ }
+ mapnik::set_pixel(im, x, y, c);
+}
+
+void set_pixel_double(mapnik::image_any & im, unsigned x, unsigned y, double val)
+{
+ if (x >= static_cast<int>(im.width()) && y >= static_cast<int>(im.height()))
+ {
+ PyErr_SetString(PyExc_IndexError, "invalid x,y for image dimensions");
+ boost::python::throw_error_already_set();
+ return;
+ }
+ mapnik::set_pixel(im, x, y, val);
+}
+
+void set_pixel_int(mapnik::image_any & im, unsigned x, unsigned y, int val)
+{
+ if (x >= static_cast<int>(im.width()) && y >= static_cast<int>(im.height()))
+ {
+ PyErr_SetString(PyExc_IndexError, "invalid x,y for image dimensions");
+ boost::python::throw_error_already_set();
+ return;
+ }
+ mapnik::set_pixel(im, x, y, val);
+}
+
+unsigned get_type(mapnik::image_any & im)
+{
+ return im.get_dtype();
+}
+
+std::shared_ptr<image_any> open_from_file(std::string const& filename)
+{
+ boost::optional<std::string> type = type_from_filename(filename);
+ if (type)
+ {
+ std::unique_ptr<image_reader> reader(get_image_reader(filename,*type));
+ if (reader.get())
+ {
+ return std::make_shared<image_any>(reader->read(0,0,reader->width(),reader->height()));
+ }
+ throw mapnik::image_reader_exception("Failed to load: " + filename);
+ }
+ throw mapnik::image_reader_exception("Unsupported image format:" + filename);
+}
+
+std::shared_ptr<image_any> fromstring(std::string const& str)
+{
+ std::unique_ptr<image_reader> reader(get_image_reader(str.c_str(),str.size()));
+ if (reader.get())
+ {
+ return std::make_shared<image_any>(reader->read(0,0,reader->width(), reader->height()));
+ }
+ throw mapnik::image_reader_exception("Failed to load image from buffer" );
+}
+
+std::shared_ptr<image_any> frombuffer(PyObject * obj)
+{
+ void const* buffer=0;
+ Py_ssize_t buffer_len;
+ if (PyObject_AsReadBuffer(obj, &buffer, &buffer_len) == 0)
+ {
+ std::unique_ptr<image_reader> reader(get_image_reader(reinterpret_cast<char const*>(buffer),buffer_len));
+ if (reader.get())
+ {
+ return std::make_shared<image_any>(reader->read(0,0,reader->width(),reader->height()));
+ }
+ }
+ throw mapnik::image_reader_exception("Failed to load image from buffer" );
+}
+
+void set_grayscale_to_alpha(image_any & im)
+{
+ mapnik::set_grayscale_to_alpha(im);
+}
+
+void set_grayscale_to_alpha_c(image_any & im, mapnik::color const& c)
+{
+ mapnik::set_grayscale_to_alpha(im, c);
+}
+
+void set_color_to_alpha(image_any & im, mapnik::color const& c)
+{
+ mapnik::set_color_to_alpha(im, c);
+}
+
+void apply_opacity(image_any & im, float opacity)
+{
+ mapnik::apply_opacity(im, opacity);
+}
+
+bool premultiplied(image_any &im)
+{
+ return im.get_premultiplied();
+}
+
+bool premultiply(image_any & im)
+{
+ return mapnik::premultiply_alpha(im);
+}
+
+bool demultiply(image_any & im)
+{
+ return mapnik::demultiply_alpha(im);
+}
+
+void clear(image_any & im)
+{
+ mapnik::fill(im, 0);
+}
+
+void composite(image_any & dst, image_any & src, mapnik::composite_mode_e mode, float opacity, int dx, int dy)
+{
+ bool demultiply_dst = mapnik::premultiply_alpha(dst);
+ bool demultiply_src = mapnik::premultiply_alpha(src);
+ mapnik::composite(dst,src,mode,opacity,dx,dy);
+ if (demultiply_dst)
+ {
+ mapnik::demultiply_alpha(dst);
+ }
+ if (demultiply_src)
+ {
+ mapnik::demultiply_alpha(src);
+ }
+}
+
+#if defined(HAVE_CAIRO) && defined(HAVE_PYCAIRO)
+std::shared_ptr<image_any> from_cairo(PycairoSurface* py_surface)
+{
+ mapnik::cairo_surface_ptr surface(cairo_surface_reference(py_surface->surface), mapnik::cairo_surface_closer());
+ mapnik::image_rgba8 image = mapnik::image_rgba8(cairo_image_surface_get_width(&*surface), cairo_image_surface_get_height(&*surface));
+ cairo_image_to_rgba8(image, surface);
+ return std::make_shared<image_any>(std::move(image));
+}
+#endif
+
+void export_image()
+{
+ using namespace boost::python;
+ // NOTE: must match list in include/mapnik/image_compositing.hpp
+ enum_<mapnik::composite_mode_e>("CompositeOp")
+ .value("clear", mapnik::clear)
+ .value("src", mapnik::src)
+ .value("dst", mapnik::dst)
+ .value("src_over", mapnik::src_over)
+ .value("dst_over", mapnik::dst_over)
+ .value("src_in", mapnik::src_in)
+ .value("dst_in", mapnik::dst_in)
+ .value("src_out", mapnik::src_out)
+ .value("dst_out", mapnik::dst_out)
+ .value("src_atop", mapnik::src_atop)
+ .value("dst_atop", mapnik::dst_atop)
+ .value("xor", mapnik::_xor)
+ .value("plus", mapnik::plus)
+ .value("minus", mapnik::minus)
+ .value("multiply", mapnik::multiply)
+ .value("screen", mapnik::screen)
+ .value("overlay", mapnik::overlay)
+ .value("darken", mapnik::darken)
+ .value("lighten", mapnik::lighten)
+ .value("color_dodge", mapnik::color_dodge)
+ .value("color_burn", mapnik::color_burn)
+ .value("hard_light", mapnik::hard_light)
+ .value("soft_light", mapnik::soft_light)
+ .value("difference", mapnik::difference)
+ .value("exclusion", mapnik::exclusion)
+ .value("contrast", mapnik::contrast)
+ .value("invert", mapnik::invert)
+ .value("grain_merge", mapnik::grain_merge)
+ .value("grain_extract", mapnik::grain_extract)
+ .value("hue", mapnik::hue)
+ .value("saturation", mapnik::saturation)
+ .value("color", mapnik::_color)
+ .value("value", mapnik::_value)
+ .value("linear_dodge", mapnik::linear_dodge)
+ .value("linear_burn", mapnik::linear_burn)
+ .value("divide", mapnik::divide)
+ ;
+
+ enum_<mapnik::image_dtype>("ImageType")
+ .value("rgba8", mapnik::image_dtype_rgba8)
+ .value("gray8", mapnik::image_dtype_gray8)
+ .value("gray8s", mapnik::image_dtype_gray8s)
+ .value("gray16", mapnik::image_dtype_gray16)
+ .value("gray16s", mapnik::image_dtype_gray16s)
+ .value("gray32", mapnik::image_dtype_gray32)
+ .value("gray32s", mapnik::image_dtype_gray32s)
+ .value("gray32f", mapnik::image_dtype_gray32f)
+ .value("gray64", mapnik::image_dtype_gray64)
+ .value("gray64s", mapnik::image_dtype_gray64s)
+ .value("gray64f", mapnik::image_dtype_gray64f)
+ ;
+
+ class_<image_any,std::shared_ptr<image_any>, boost::noncopyable >("Image","This class represents a image.",init<int,int>())
+ .def(init<int,int,mapnik::image_dtype>())
+ .def(init<int,int,mapnik::image_dtype,bool>())
+ .def(init<int,int,mapnik::image_dtype,bool,bool>())
+ .def(init<int,int,mapnik::image_dtype,bool,bool,bool>())
+ .def("width",&image_any::width)
+ .def("height",&image_any::height)
+ .def("view",&get_view)
+ .def("painted",&image_any::painted)
+ .def("is_solid",&is_solid)
+ .def("fill",&fill_color)
+ .def("fill",&fill_int)
+ .def("fill",&fill_double)
+ .def("set_grayscale_to_alpha",&set_grayscale_to_alpha, "Set the grayscale values to the alpha channel of the Image")
+ .def("set_grayscale_to_alpha",&set_grayscale_to_alpha_c, "Set the grayscale values to the alpha channel of the Image")
+ .def("set_color_to_alpha",&set_color_to_alpha, "Set a given color to the alpha channel of the Image")
+ .def("apply_opacity",&apply_opacity, "Set the opacity of the Image relative to the current alpha of each pixel.")
+ .def("composite",&composite,
+ ( arg("self"),
+ arg("image"),
+ arg("mode")=mapnik::src_over,
+ arg("opacity")=1.0f,
+ arg("dx")=0,
+ arg("dy")=0
+ ))
+ .def("compare",&compare,
+ ( arg("self"),
+ arg("image"),
+ arg("threshold")=0.0,
+ arg("alpha")=true
+ ))
+ .def("copy",&copy,
+ ( arg("self"),
+ arg("type"),
+ arg("offset")=0.0,
+ arg("scaling")=1.0
+ ))
+ .add_property("offset",
+ &image_any::get_offset,
+ &image_any::set_offset,
+ "Gets or sets the offset component.\n")
+ .add_property("scaling",
+ &image_any::get_scaling,
+ &image_any::set_scaling,
+ "Gets or sets the offset component.\n")
+ .def("premultiplied",&premultiplied)
+ .def("premultiply",&premultiply)
+ .def("demultiply",&demultiply)
+ .def("set_pixel",&set_pixel_color)
+ .def("set_pixel",&set_pixel_double)
+ .def("set_pixel",&set_pixel_int)
+ .def("get_pixel",&get_pixel,
+ ( arg("self"),
+ arg("x"),
+ arg("y"),
+ arg("get_color")=false
+ ))
+ .def("get_type",&get_type)
+ .def("clear",&clear)
+ //TODO(haoyu) The method name 'tostring' might be confusing since they actually return bytes in Python 3
+
+ .def("tostring",&tostring1)
+ .def("tostring",&tostring2)
+ .def("tostring",&tostring3)
+ .def("save", &save_to_file1)
+ .def("save", &save_to_file2)
+ .def("save", &save_to_file3)
+ .def("open",open_from_file)
+ .staticmethod("open")
+ .def("frombuffer",&frombuffer)
+ .staticmethod("frombuffer")
+ .def("fromstring",&fromstring)
+ .staticmethod("fromstring")
+#if defined(HAVE_CAIRO) && defined(HAVE_PYCAIRO)
+ .def("from_cairo",&from_cairo)
+ .staticmethod("from_cairo")
+#endif
+ ;
+
+}
diff --git a/src/mapnik_image_view.cpp b/src/mapnik_image_view.cpp
new file mode 100644
index 0000000..07832fb
--- /dev/null
+++ b/src/mapnik_image_view.cpp
@@ -0,0 +1,128 @@
+/*****************************************************************************
+ *
+ * This file is part of Mapnik (c++ mapping toolkit)
+ *
+ * Copyright (C) 2015 Artem Pavlenko, Jean-Francois Doyon
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ *****************************************************************************/
+
+#include <mapnik/config.hpp>
+
+// boost
+#include "boost_std_shared_shim.hpp"
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#pragma GCC diagnostic ignored "-Wunused-local-typedef"
+#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
+
+#include <boost/python.hpp>
+#include <boost/python/module.hpp>
+#include <boost/python/def.hpp>
+#pragma GCC diagnostic pop
+
+// mapnik
+#include <mapnik/image.hpp>
+#include <mapnik/image_view.hpp>
+#include <mapnik/image_view_any.hpp>
+#include <mapnik/image_util.hpp>
+#include <mapnik/palette.hpp>
+#include <sstream>
+
+using mapnik::image_view_any;
+using mapnik::save_to_file;
+
+// output 'raw' pixels
+PyObject* view_tostring1(image_view_any const& view)
+{
+ std::ostringstream ss(std::ios::out|std::ios::binary);
+ mapnik::view_to_stream(view, ss);
+ return
+#if PY_VERSION_HEX >= 0x03000000
+ ::PyBytes_FromStringAndSize
+#else
+ ::PyString_FromStringAndSize
+#endif
+ ((const char*)ss.str().c_str(),ss.str().size());
+}
+
+// encode (png,jpeg)
+PyObject* view_tostring2(image_view_any const & view, std::string const& format)
+{
+ std::string s = save_to_string(view, format);
+ return
+#if PY_VERSION_HEX >= 0x03000000
+ ::PyBytes_FromStringAndSize
+#else
+ ::PyString_FromStringAndSize
+#endif
+ (s.data(),s.size());
+}
+
+PyObject* view_tostring3(image_view_any const & view, std::string const& format, mapnik::rgba_palette const& pal)
+{
+ std::string s = save_to_string(view, format, pal);
+ return
+#if PY_VERSION_HEX >= 0x03000000
+ ::PyBytes_FromStringAndSize
+#else
+ ::PyString_FromStringAndSize
+#endif
+ (s.data(),s.size());
+}
+
+bool is_solid(image_view_any const& view)
+{
+ return mapnik::is_solid(view);
+}
+
+void save_view1(image_view_any const& view,
+ std::string const& filename)
+{
+ save_to_file(view,filename);
+}
+
+void save_view2(image_view_any const& view,
+ std::string const& filename,
+ std::string const& type)
+{
+ save_to_file(view,filename,type);
+}
+
+void save_view3(image_view_any const& view,
+ std::string const& filename,
+ std::string const& type,
+ mapnik::rgba_palette const& pal)
+{
+ save_to_file(view,filename,type,pal);
+}
+
+
+void export_image_view()
+{
+ using namespace boost::python;
+ class_<image_view_any>("ImageView","A view into an image.",no_init)
+ .def("width",&image_view_any::width)
+ .def("height",&image_view_any::height)
+ .def("is_solid",&is_solid)
+ .def("tostring",&view_tostring1)
+ .def("tostring",&view_tostring2)
+ .def("tostring",&view_tostring3)
+ .def("save",&save_view1)
+ .def("save",&save_view2)
+ .def("save",&save_view3)
+ ;
+}
diff --git a/src/mapnik_label_collision_detector.cpp b/src/mapnik_label_collision_detector.cpp
new file mode 100644
index 0000000..9e5a6cb
--- /dev/null
+++ b/src/mapnik_label_collision_detector.cpp
@@ -0,0 +1,131 @@
+/*****************************************************************************
+ *
+ * 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
+ *
+ *****************************************************************************/
+
+#include <mapnik/config.hpp>
+
+// boost
+#include "boost_std_shared_shim.hpp"
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#pragma GCC diagnostic ignored "-Wunused-local-typedef"
+#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
+
+#include <boost/python.hpp>
+#include <boost/python/module.hpp>
+#include <boost/python/def.hpp>
+#pragma GCC diagnostic pop
+
+#include <mapnik/label_collision_detector.hpp>
+#include <mapnik/map.hpp>
+
+#include <list>
+
+using mapnik::label_collision_detector4;
+using mapnik::box2d;
+using mapnik::Map;
+
+namespace
+{
+
+std::shared_ptr<label_collision_detector4>
+create_label_collision_detector_from_extent(box2d<double> const &extent)
+{
+ return std::make_shared<label_collision_detector4>(extent);
+}
+
+std::shared_ptr<label_collision_detector4>
+create_label_collision_detector_from_map(Map const &m)
+{
+ double buffer = m.buffer_size();
+ box2d<double> extent(-buffer, -buffer, m.width() + buffer, m.height() + buffer);
+ return std::make_shared<label_collision_detector4>(extent);
+}
+
+boost::python::list
+make_label_boxes(std::shared_ptr<label_collision_detector4> det)
+{
+ boost::python::list boxes;
+
+ for (label_collision_detector4::query_iterator jtr = det->begin();
+ jtr != det->end(); ++jtr)
+ {
+ boxes.append<box2d<double> >(jtr->get().box);
+ }
+
+ return boxes;
+}
+
+}
+
+void export_label_collision_detector()
+{
+ using namespace boost::python;
+
+ // for overload resolution
+ void (label_collision_detector4::*insert_box)(box2d<double> const &) = &label_collision_detector4::insert;
+
+ class_<label_collision_detector4, std::shared_ptr<label_collision_detector4>, boost::noncopyable>
+ ("LabelCollisionDetector",
+ "Object to detect collisions between labels, used in the rendering process.",
+ no_init)
+
+ .def("__init__", make_constructor(create_label_collision_detector_from_extent),
+ "Creates an empty collision detection object with a given extent. Note "
+ "that the constructor from Map objects is a sensible default and usually "
+ "what you want to do.\n"
+ "\n"
+ "Example:\n"
+ ">>> m = Map(size_x, size_y)\n"
+ ">>> buf_sz = m.buffer_size\n"
+ ">>> extent = mapnik.Box2d(-buf_sz, -buf_sz, m.width + buf_sz, m.height + buf_sz)\n"
+ ">>> detector = mapnik.LabelCollisionDetector(extent)")
+
+ .def("__init__", make_constructor(create_label_collision_detector_from_map),
+ "Creates an empty collision detection object matching the given Map object. "
+ "The created detector will have the same size, including the buffer, as the "
+ "map object. This is usually what you want to do.\n"
+ "\n"
+ "Example:\n"
+ ">>> m = Map(size_x, size_y)\n"
+ ">>> detector = mapnik.LabelCollisionDetector(m)")
+
+ .def("extent", &label_collision_detector4::extent, return_value_policy<copy_const_reference>(),
+ "Returns the total extent (bounding box) of all labels inside the detector.\n"
+ "\n"
+ "Example:\n"
+ ">>> detector.extent()\n"
+ "Box2d(573.252589209,494.789179821,584.261023823,496.83610261)")
+
+ .def("boxes", &make_label_boxes,
+ "Returns a list of all the label boxes inside the detector.")
+
+ .def("insert", insert_box,
+ "Insert a 2d box into the collision detector. This can be used to ensure that "
+ "some space is left clear on the map for later overdrawing, for example by "
+ "non-Mapnik processes.\n"
+ "\n"
+ "Example:\n"
+ ">>> m = Map(size_x, size_y)\n"
+ ">>> detector = mapnik.LabelCollisionDetector(m)"
+ ">>> detector.insert(mapnik.Box2d(196, 254, 291, 389))")
+ ;
+}
diff --git a/src/mapnik_layer.cpp b/src/mapnik_layer.cpp
new file mode 100644
index 0000000..0dad77c
--- /dev/null
+++ b/src/mapnik_layer.cpp
@@ -0,0 +1,388 @@
+/*****************************************************************************
+ *
+ * This file is part of Mapnik (c++ mapping toolkit)
+ *
+ * Copyright (C) 2015 Artem Pavlenko, Jean-Francois Doyon
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ *****************************************************************************/
+
+#include <mapnik/config.hpp>
+
+// boost
+#include "boost_std_shared_shim.hpp"
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#pragma GCC diagnostic ignored "-Wunused-local-typedef"
+#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
+
+#include <boost/python.hpp>
+#include <boost/python/suite/indexing/vector_indexing_suite.hpp>
+#pragma GCC diagnostic pop
+
+// mapnik
+#include <mapnik/layer.hpp>
+#include <mapnik/datasource.hpp>
+#include <mapnik/datasource_cache.hpp>
+
+using mapnik::layer;
+using mapnik::parameters;
+using mapnik::datasource_cache;
+
+
+struct layer_pickle_suite : boost::python::pickle_suite
+{
+ static boost::python::tuple
+ getinitargs(const layer& l)
+ {
+ return boost::python::make_tuple(l.name(),l.srs());
+ }
+
+ static boost::python::tuple
+ getstate(const layer& l)
+ {
+ boost::python::list s;
+ std::vector<std::string> const& style_names = l.styles();
+ for (unsigned i = 0; i < style_names.size(); ++i)
+ {
+ s.append(style_names[i]);
+ }
+ return boost::python::make_tuple(l.clear_label_cache(),l.minimum_scale_denominator(),l.maximum_scale_denominator(),l.queryable(),l.datasource()->params(),l.cache_features(),s);
+ }
+
+ static void
+ setstate (layer& l, boost::python::tuple state)
+ {
+ using namespace boost::python;
+ if (len(state) != 9)
+ {
+ PyErr_SetObject(PyExc_ValueError,
+ ("expected 9-item tuple in call to __setstate__; got %s"
+ % state).ptr()
+ );
+ throw_error_already_set();
+ }
+
+ l.set_clear_label_cache(extract<bool>(state[0]));
+
+ l.set_minimum_scale_denominator(extract<double>(state[1]));
+
+ l.set_maximum_scale_denominator(extract<double>(state[2]));
+
+ l.set_queryable(extract<bool>(state[3]));
+
+ mapnik::parameters params = extract<parameters>(state[4]);
+ l.set_datasource(datasource_cache::instance().create(params));
+
+ boost::python::list s = extract<boost::python::list>(state[5]);
+ for (int i=0;i<len(s);++i)
+ {
+ l.add_style(extract<std::string>(s[i]));
+ }
+
+ l.set_cache_features(extract<bool>(state[6]));
+ }
+};
+
+std::vector<std::string> & (mapnik::layer::*_styles_)() = &mapnik::layer::styles;
+
+void set_maximum_extent(mapnik::layer & l, boost::optional<mapnik::box2d<double> > const& box)
+{
+ if (box)
+ {
+ l.set_maximum_extent(*box);
+ }
+ else
+ {
+ l.reset_maximum_extent();
+ }
+}
+
+void set_buffer_size(mapnik::layer & l, boost::optional<int> const& buffer_size)
+{
+ if (buffer_size)
+ {
+ l.set_buffer_size(*buffer_size);
+ }
+ else
+ {
+ l.reset_buffer_size();
+ }
+}
+
+PyObject * get_buffer_size(mapnik::layer & l)
+{
+ boost::optional<int> buffer_size = l.buffer_size();
+ if (buffer_size)
+ {
+#if PY_VERSION_HEX >= 0x03000000
+ return PyLong_FromLong(*buffer_size);
+#else
+ return PyInt_FromLong(*buffer_size);
+#endif
+ }
+ else
+ {
+ Py_RETURN_NONE;
+ }
+}
+
+void export_layer()
+{
+ using namespace boost::python;
+ class_<std::vector<std::string> >("Names")
+ .def(vector_indexing_suite<std::vector<std::string>,true >())
+ ;
+
+ class_<layer>("Layer", "A Mapnik map layer.", init<std::string const&,optional<std::string const&> >(
+ "Create a Layer with a named string and, optionally, an srs string.\n"
+ "\n"
+ "The srs can be either a Proj.4 epsg code ('+init=epsg:<code>') or\n"
+ "of a Proj.4 literal ('+proj=<literal>').\n"
+ "If no srs is specified it will default to '+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs'\n"
+ "\n"
+ "Usage:\n"
+ ">>> from mapnik import Layer\n"
+ ">>> lyr = Layer('My Layer','+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs')\n"
+ ">>> lyr\n"
+ "<mapnik._mapnik.Layer object at 0x6a270>\n"
+ ))
+
+ .def_pickle(layer_pickle_suite())
+
+ .def("envelope",&layer::envelope,
+ "Return the geographic envelope/bounding box."
+ "\n"
+ "Determined based on the layer datasource.\n"
+ "\n"
+ "Usage:\n"
+ ">>> from mapnik import Layer\n"
+ ">>> lyr = Layer('My Layer','+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs')\n"
+ ">>> lyr.envelope()\n"
+ "box2d(-1.0,-1.0,0.0,0.0) # default until a datasource is loaded\n"
+ )
+
+ .def("visible", &layer::visible,
+ "Return True if this layer's data is active and visible at a given scale_denom.\n"
+ "\n"
+ "Otherwise returns False.\n"
+ "Accepts a scale value as an integer or float input.\n"
+ "Will return False if:\n"
+ "\tscale_denom >= minimum_scale_denominator - 1e-6\n"
+ "\tor:\n"
+ "\tscale_denom < maximum_scale_denominator + 1e-6\n"
+ "\n"
+ "Usage:\n"
+ ">>> from mapnik import Layer\n"
+ ">>> lyr = Layer('My Layer','+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs')\n"
+ ">>> lyr.visible(1.0/1000000)\n"
+ "True\n"
+ ">>> lyr.active = False\n"
+ ">>> lyr.visible(1.0/1000000)\n"
+ "False\n"
+ )
+
+ .add_property("active",
+ &layer::active,
+ &layer::set_active,
+ "Get/Set whether this layer is active and will be rendered (same as status property).\n"
+ "\n"
+ "Usage:\n"
+ ">>> from mapnik import Layer\n"
+ ">>> lyr = Layer('My Layer','+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs')\n"
+ ">>> lyr.active\n"
+ "True # Active by default\n"
+ ">>> lyr.active = False # set False to disable layer rendering\n"
+ ">>> lyr.active\n"
+ "False\n"
+ )
+
+ .add_property("status",
+ &layer::active,
+ &layer::set_active,
+ "Get/Set whether this layer is active and will be rendered.\n"
+ "\n"
+ "Usage:\n"
+ ">>> from mapnik import Layer\n"
+ ">>> lyr = Layer('My Layer','+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs')\n"
+ ">>> lyr.status\n"
+ "True # Active by default\n"
+ ">>> lyr.status = False # set False to disable layer rendering\n"
+ ">>> lyr.status\n"
+ "False\n"
+ )
+
+ .add_property("clear_label_cache",
+ &layer::clear_label_cache,
+ &layer::set_clear_label_cache,
+ "Get/Set whether to clear the label collision detector cache for this layer during rendering\n"
+ "\n"
+ "Usage:\n"
+ ">>> lyr.clear_label_cache\n"
+ "False # False by default, meaning label positions from other layers will impact placement \n"
+ ">>> lyr.clear_label_cache = True # set to True to clear the label collision detector cache\n"
+ )
+
+ .add_property("cache_features",
+ &layer::cache_features,
+ &layer::set_cache_features,
+ "Get/Set whether features should be cached during rendering if used between multiple styles\n"
+ "\n"
+ "Usage:\n"
+ ">>> lyr.cache_features\n"
+ "False # False by default\n"
+ ">>> lyr.cache_features = True # set to True to enable feature caching\n"
+ )
+
+ .add_property("datasource",
+ &layer::datasource,
+ &layer::set_datasource,
+ "The datasource attached to this layer.\n"
+ "\n"
+ "Usage:\n"
+ ">>> from mapnik import Layer, Datasource\n"
+ ">>> lyr = Layer('My Layer','+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs')\n"
+ ">>> lyr.datasource = Datasource(type='shape',file='world_borders')\n"
+ ">>> lyr.datasource\n"
+ "<mapnik.Datasource object at 0x65470>\n"
+ )
+
+ .add_property("buffer_size",
+ &get_buffer_size,
+ &set_buffer_size,
+ "Get/Set the size of buffer around layer in pixels.\n"
+ "\n"
+ "Usage:\n"
+ ">>> print(l.buffer_size)\n"
+ "None # None by default\n"
+ ">>> l.buffer_size = 2\n"
+ ">>> l.buffer_size\n"
+ "2\n"
+ )
+
+ .add_property("maximum_extent",make_function
+ (&layer::maximum_extent,return_value_policy<copy_const_reference>()),
+ &set_maximum_extent,
+ "The maximum extent of the map.\n"
+ "\n"
+ "Usage:\n"
+ ">>> m.maximum_extent = Box2d(-180,-90,180,90)\n"
+ )
+
+ .add_property("maximum_scale_denominator",
+ &layer::maximum_scale_denominator,
+ &layer::set_maximum_scale_denominator,
+ "Get/Set the maximum scale denominator of the layer.\n"
+ "\n"
+ "Usage:\n"
+ ">>> from mapnik import Layer\n"
+ ">>> lyr = Layer('My Layer','+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs')\n"
+ ">>> lyr.maximum_scale_denominator\n"
+ "1.7976931348623157e+308 # default is the numerical maximum\n"
+ ">>> lyr.maximum_scale_denominator = 1.0/1000000\n"
+ ">>> lyr.maximum_scale_denominator\n"
+ "9.9999999999999995e-07\n"
+ )
+
+ .add_property("minimum_scale_denominator",
+ &layer::minimum_scale_denominator,
+ &layer::set_minimum_scale_denominator,
+ "Get/Set the minimum scale demoninator of the layer.\n"
+ "\n"
+ "Usage:\n"
+ ">>> from mapnik import Layer\n"
+ ">>> lyr = Layer('My Layer','+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs')\n"
+ ">>> lyr.minimum_scale_denominator # default is 0\n"
+ "0.0\n"
+ ">>> lyr.minimum_scale_denominator = 1.0/1000000\n"
+ ">>> lyr.minimum_scale_denominator\n"
+ "9.9999999999999995e-07\n"
+ )
+
+ .add_property("name",
+ make_function(&layer::name, return_value_policy<copy_const_reference>()),
+ &layer::set_name,
+ "Get/Set the name of the layer.\n"
+ "\n"
+ "Usage:\n"
+ ">>> from mapnik import Layer\n"
+ ">>> lyr = Layer('My Layer','+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs')\n"
+ ">>> lyr.name\n"
+ "'My Layer'\n"
+ ">>> lyr.name = 'New Name'\n"
+ ">>> lyr.name\n"
+ "'New Name'\n"
+ )
+
+ .add_property("queryable",
+ &layer::queryable,
+ &layer::set_queryable,
+ "Get/Set whether this layer is queryable.\n"
+ "\n"
+ "Usage:\n"
+ ">>> from mapnik import layer\n"
+ ">>> lyr = layer('My layer','+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs')\n"
+ ">>> lyr.queryable\n"
+ "False # Not queryable by default\n"
+ ">>> lyr.queryable = True\n"
+ ">>> lyr.queryable\n"
+ "True\n"
+ )
+
+ .add_property("srs",
+ make_function(&layer::srs,return_value_policy<copy_const_reference>()),
+ &layer::set_srs,
+ "Get/Set the SRS of the layer.\n"
+ "\n"
+ "Usage:\n"
+ ">>> from mapnik import layer\n"
+ ">>> lyr = layer('My layer','+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs')\n"
+ ">>> lyr.srs\n"
+ "'+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs' # The default srs if not initialized with custom srs\n"
+ ">>> # set to google mercator with Proj.4 literal\n"
+ "... \n"
+ ">>> lyr.srs = '+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0.0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs +over'\n"
+ )
+
+ .add_property("group_by",
+ make_function(&layer::group_by,return_value_policy<copy_const_reference>()),
+ &layer::set_group_by,
+ "Get/Set the optional layer group name.\n"
+ "\n"
+ "More details at https://github.com/mapnik/mapnik/wiki/Grouped-rendering:\n"
+ )
+
+ .add_property("styles",
+ make_function(_styles_,return_value_policy<reference_existing_object>()),
+ "The styles list attached to this layer.\n"
+ "\n"
+ "Usage:\n"
+ ">>> from mapnik import layer\n"
+ ">>> lyr = layer('My layer','+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs')\n"
+ ">>> lyr.styles\n"
+ "<mapnik._mapnik.Names object at 0x6d3e8>\n"
+ ">>> len(lyr.styles)\n"
+ "0\n # no styles until you append them\n"
+ "lyr.styles.append('My Style') # mapnik uses named styles for flexibility\n"
+ ">>> len(lyr.styles)\n"
+ "1\n"
+ ">>> lyr.styles[0]\n"
+ "'My Style'\n"
+ )
+ // comparison
+ .def(self == self)
+ ;
+}
diff --git a/src/mapnik_logger.cpp b/src/mapnik_logger.cpp
new file mode 100644
index 0000000..6a1689f
--- /dev/null
+++ b/src/mapnik_logger.cpp
@@ -0,0 +1,83 @@
+/*****************************************************************************
+ *
+ * This file is part of Mapnik (c++ mapping toolkit)
+ *
+ * Copyright (C) 2015 Artem Pavlenko, Jean-Francois Doyon
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ *****************************************************************************/
+
+#include <mapnik/config.hpp>
+
+// boost
+#include "boost_std_shared_shim.hpp"
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#pragma GCC diagnostic ignored "-Wunused-local-typedef"
+#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
+
+#include <boost/python.hpp>
+#include <boost/noncopyable.hpp>
+#pragma GCC diagnostic pop
+
+#include <mapnik/debug.hpp>
+#include <mapnik/util/singleton.hpp>
+#include "mapnik_enumeration.hpp"
+
+void export_logger()
+{
+ using mapnik::logger;
+ using mapnik::singleton;
+ using mapnik::CreateStatic;
+ using namespace boost::python;
+
+ class_<singleton<logger,CreateStatic>,boost::noncopyable>("Singleton",no_init)
+ .def("instance",&singleton<logger,CreateStatic>::instance,
+ return_value_policy<reference_existing_object>())
+ .staticmethod("instance")
+ ;
+
+ enum_<mapnik::logger::severity_type>("severity_type")
+ .value("Debug", logger::debug)
+ .value("Warn", logger::warn)
+ .value("Error", logger::error)
+ .value("None", logger::none)
+ ;
+
+ class_<logger,bases<singleton<logger,CreateStatic> >,
+ boost::noncopyable>("logger",no_init)
+ .def("get_severity", &logger::get_severity)
+ .def("set_severity", &logger::set_severity)
+ .def("get_object_severity", &logger::get_object_severity)
+ .def("set_object_severity", &logger::set_object_severity)
+ .def("clear_object_severity", &logger::clear_object_severity)
+ .def("get_format", &logger::get_format)
+ .def("set_format", &logger::set_format)
+ .def("str", &logger::str)
+ .def("use_file", &logger::use_file)
+ .def("use_console", &logger::use_console)
+ .staticmethod("get_severity")
+ .staticmethod("set_severity")
+ .staticmethod("get_object_severity")
+ .staticmethod("set_object_severity")
+ .staticmethod("clear_object_severity")
+ .staticmethod("get_format")
+ .staticmethod("set_format")
+ .staticmethod("str")
+ .staticmethod("use_file")
+ .staticmethod("use_console")
+ ;
+}
diff --git a/src/mapnik_map.cpp b/src/mapnik_map.cpp
new file mode 100644
index 0000000..8797c04
--- /dev/null
+++ b/src/mapnik_map.cpp
@@ -0,0 +1,543 @@
+/*****************************************************************************
+ *
+ * This file is part of Mapnik (c++ mapping toolkit)
+ *
+ * Copyright (C) 2015 Artem Pavlenko, Jean-Francois Doyon
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ *****************************************************************************/
+
+#include <mapnik/config.hpp>
+
+// boost
+#include "boost_std_shared_shim.hpp"
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#pragma GCC diagnostic ignored "-Wunused-local-typedef"
+#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
+
+#include <boost/python.hpp>
+#include <boost/noncopyable.hpp>
+#include <boost/python/suite/indexing/vector_indexing_suite.hpp>
+#include <boost/python/iterator.hpp>
+#include <boost/iterator/transform_iterator.hpp>
+#pragma GCC diagnostic pop
+
+// mapnik
+#include <mapnik/rule.hpp>
+#include <mapnik/layer.hpp>
+#include <mapnik/map.hpp>
+#include <mapnik/projection.hpp>
+#include <mapnik/view_transform.hpp>
+#include <mapnik/feature_type_style.hpp>
+#include "mapnik_enumeration.hpp"
+
+using mapnik::color;
+using mapnik::coord;
+using mapnik::box2d;
+using mapnik::layer;
+using mapnik::Map;
+
+std::vector<layer>& (Map::*layers_nonconst)() = &Map::layers;
+std::vector<layer> const& (Map::*layers_const)() const = &Map::layers;
+mapnik::parameters& (Map::*params_nonconst)() = &Map::get_extra_parameters;
+
+void insert_style(mapnik::Map & m, std::string const& name, mapnik::feature_type_style const& style)
+{
+ m.insert_style(name,style);
+}
+
+void insert_fontset(mapnik::Map & m, std::string const& name, mapnik::font_set const& fontset)
+{
+ m.insert_fontset(name,fontset);
+}
+
+mapnik::feature_type_style find_style(mapnik::Map const& m, std::string const& name)
+{
+ boost::optional<mapnik::feature_type_style const&> style = m.find_style(name);
+ if (!style)
+ {
+ PyErr_SetString(PyExc_KeyError, "Invalid style name");
+ boost::python::throw_error_already_set();
+ }
+ return *style;
+}
+
+mapnik::font_set find_fontset(mapnik::Map const& m, std::string const& name)
+{
+ boost::optional<mapnik::font_set const&> fontset = m.find_fontset(name);
+ if (!fontset)
+ {
+ PyErr_SetString(PyExc_KeyError, "Invalid font_set name");
+ boost::python::throw_error_already_set();
+ }
+ return *fontset;
+}
+
+// TODO - we likely should allow indexing by negative number from python
+// for now, protect against negative values and kindly throw
+mapnik::featureset_ptr query_point(mapnik::Map const& m, int index, double x, double y)
+{
+ if (index < 0){
+ PyErr_SetString(PyExc_IndexError, "Please provide a layer index >= 0");
+ boost::python::throw_error_already_set();
+ }
+ unsigned idx = index;
+ return m.query_point(idx, x, y);
+}
+
+mapnik::featureset_ptr query_map_point(mapnik::Map const& m, int index, double x, double y)
+{
+ if (index < 0){
+ PyErr_SetString(PyExc_IndexError, "Please provide a layer index >= 0");
+ boost::python::throw_error_already_set();
+ }
+ unsigned idx = index;
+ return m.query_map_point(idx, x, y);
+}
+
+void set_maximum_extent(mapnik::Map & m, boost::optional<mapnik::box2d<double> > const& box)
+{
+ if (box)
+ {
+ m.set_maximum_extent(*box);
+ }
+ else
+ {
+ m.reset_maximum_extent();
+ }
+}
+
+struct extract_style
+{
+ using result_type = boost::python::tuple;
+ result_type operator() (std::map<std::string, mapnik::feature_type_style>::value_type const& val) const
+ {
+ return boost::python::make_tuple(val.first,val.second);
+ }
+};
+
+using style_extract_iterator = boost::transform_iterator<extract_style, Map::const_style_iterator>;
+using style_range = std::pair<style_extract_iterator,style_extract_iterator>;
+
+style_range _styles_ (mapnik::Map const& m)
+{
+ return style_range(
+ boost::make_transform_iterator<extract_style>(m.begin_styles(), extract_style()),
+ boost::make_transform_iterator<extract_style>(m.end_styles(), extract_style()));
+}
+
+void export_map()
+{
+ using namespace boost::python;
+
+ // aspect ratio fix modes
+ mapnik::enumeration_<mapnik::aspect_fix_mode_e>("aspect_fix_mode")
+ .value("GROW_BBOX", mapnik::Map::GROW_BBOX)
+ .value("GROW_CANVAS",mapnik::Map::GROW_CANVAS)
+ .value("SHRINK_BBOX",mapnik::Map::SHRINK_BBOX)
+ .value("SHRINK_CANVAS",mapnik::Map::SHRINK_CANVAS)
+ .value("ADJUST_BBOX_WIDTH",mapnik::Map::ADJUST_BBOX_WIDTH)
+ .value("ADJUST_BBOX_HEIGHT",mapnik::Map::ADJUST_BBOX_HEIGHT)
+ .value("ADJUST_CANVAS_WIDTH",mapnik::Map::ADJUST_CANVAS_WIDTH)
+ .value("ADJUST_CANVAS_HEIGHT", mapnik::Map::ADJUST_CANVAS_HEIGHT)
+ .value("RESPECT", mapnik::Map::RESPECT)
+ ;
+
+ class_<std::vector<layer> >("Layers")
+ .def(vector_indexing_suite<std::vector<layer> >())
+ ;
+
+ class_<style_range>("StyleRange")
+ .def("__iter__",
+ boost::python::range(&style_range::first, &style_range::second))
+ ;
+
+ class_<Map>("Map","The map object.",init<int,int,optional<std::string const&> >(
+ ( arg("width"),arg("height"),arg("srs") ),
+ "Create a Map with a width and height as integers and, optionally,\n"
+ "an srs string either with a Proj.4 epsg code ('+init=epsg:<code>')\n"
+ "or with a Proj.4 literal ('+proj=<literal>').\n"
+ "If no srs is specified the map will default to '+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs'\n"
+ "\n"
+ "Usage:\n"
+ ">>> from mapnik import Map\n"
+ ">>> m = Map(600,400)\n"
+ ">>> m\n"
+ "<mapnik._mapnik.Map object at 0x6a240>\n"
+ ">>> m.srs\n"
+ "'+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs'\n"
+ ))
+
+ .def("append_style",insert_style,
+ (arg("style_name"),arg("style_object")),
+ "Insert a Mapnik Style onto the map by appending it.\n"
+ "\n"
+ "Usage:\n"
+ ">>> sty\n"
+ "<mapnik._mapnik.Style object at 0x6a330>\n"
+ ">>> m.append_style('Style Name', sty)\n"
+ "True # style object added to map by name\n"
+ ">>> m.append_style('Style Name', sty)\n"
+ "False # you can only append styles with unique names\n"
+ )
+
+ .def("append_fontset",insert_fontset,
+ (arg("fontset")),
+ "Add a FontSet to the map."
+ )
+
+ .def("buffered_envelope",
+ &Map::get_buffered_extent,
+ "Get the Box2d() of the Map given\n"
+ "the Map.buffer_size.\n"
+ "\n"
+ "Usage:\n"
+ ">>> m = Map(600,400)\n"
+ ">>> m.envelope()\n"
+ "Box2d(-1.0,-1.0,0.0,0.0)\n"
+ ">>> m.buffered_envelope()\n"
+ "Box2d(-1.0,-1.0,0.0,0.0)\n"
+ ">>> m.buffer_size = 1\n"
+ ">>> m.buffered_envelope()\n"
+ "Box2d(-1.02222222222,-1.02222222222,0.0222222222222,0.0222222222222)\n"
+ )
+
+ .def("envelope",
+ make_function(&Map::get_current_extent,
+ return_value_policy<copy_const_reference>()),
+ "Return the Map Box2d object\n"
+ "and print the string representation\n"
+ "of the current extent of the map.\n"
+ "\n"
+ "Usage:\n"
+ ">>> m.envelope()\n"
+ "Box2d(-0.185833333333,-0.96,0.189166666667,-0.71)\n"
+ ">>> dir(m.envelope())\n"
+ "...'center', 'contains', 'expand_to_include', 'forward',\n"
+ "...'height', 'intersect', 'intersects', 'inverse', 'maxx',\n"
+ "...'maxy', 'minx', 'miny', 'width'\n"
+ )
+
+ .def("find_fontset",find_fontset,
+ (arg("name")),
+ "Find a fontset by name."
+ )
+
+ .def("find_style",
+ find_style,
+ (arg("name")),
+ "Query the Map for a style by name and return\n"
+ "a style object if found or raise KeyError\n"
+ "style if not found.\n"
+ "\n"
+ "Usage:\n"
+ ">>> m.find_style('Style Name')\n"
+ "<mapnik._mapnik.Style object at 0x654f0>\n"
+ )
+
+ .add_property("styles", _styles_)
+
+ .def("pan",&Map::pan,
+ (arg("x"),arg("y")),
+ "Set the Map center at a given x,y location\n"
+ "as integers in the coordinates of the pixmap or map surface.\n"
+ "\n"
+ "Usage:\n"
+ ">>> m = Map(600,400)\n"
+ ">>> m.envelope().center()\n"
+ "Coord(-0.5,-0.5) # default Map center\n"
+ ">>> m.pan(-1,-1)\n"
+ ">>> m.envelope().center()\n"
+ "Coord(0.00166666666667,-0.835)\n"
+ )
+
+ .def("pan_and_zoom",&Map::pan_and_zoom,
+ (arg("x"),arg("y"),arg("factor")),
+ "Set the Map center at a given x,y location\n"
+ "and zoom factor as a float.\n"
+ "\n"
+ "Usage:\n"
+ ">>> m = Map(600,400)\n"
+ ">>> m.envelope().center()\n"
+ "Coord(-0.5,-0.5) # default Map center\n"
+ ">>> m.scale()\n"
+ "-0.0016666666666666668\n"
+ ">>> m.pan_and_zoom(-1,-1,0.25)\n"
+ ">>> m.scale()\n"
+ "0.00062500000000000001\n"
+ )
+
+ .def("query_map_point",query_map_point,
+ (arg("layer_idx"),arg("pixel_x"),arg("pixel_y")),
+ "Query a Map Layer (by layer index) for features \n"
+ "intersecting the given x,y location in the pixel\n"
+ "coordinates of the rendered map image.\n"
+ "Layer index starts at 0 (first layer in map).\n"
+ "Will return a Mapnik Featureset if successful\n"
+ "otherwise will return None.\n"
+ "\n"
+ "Usage:\n"
+ ">>> featureset = m.query_map_point(0,200,200)\n"
+ ">>> featureset\n"
+ "<mapnik._mapnik.Featureset object at 0x23b0b0>\n"
+ ">>> featureset.features\n"
+ ">>> [<mapnik.Feature object at 0x3995630>]\n"
+ )
+
+ .def("query_point",query_point,
+ (arg("layer idx"),arg("x"),arg("y")),
+ "Query a Map Layer (by layer index) for features \n"
+ "intersecting the given x,y location in the coordinates\n"
+ "of map projection.\n"
+ "Layer index starts at 0 (first layer in map).\n"
+ "Will return a Mapnik Featureset if successful\n"
+ "otherwise will return None.\n"
+ "\n"
+ "Usage:\n"
+ ">>> featureset = m.query_point(0,-122,48)\n"
+ ">>> featureset\n"
+ "<mapnik._mapnik.Featureset object at 0x23b0b0>\n"
+ ">>> featureset.features\n"
+ ">>> [<mapnik.Feature object at 0x3995630>]\n"
+ )
+
+ .def("remove_all",&Map::remove_all,
+ "Remove all Mapnik Styles and layers from the Map.\n"
+ "\n"
+ "Usage:\n"
+ ">>> m.remove_all()\n"
+ )
+
+ .def("remove_style",&Map::remove_style,
+ (arg("style_name")),
+ "Remove a Mapnik Style from the map.\n"
+ "\n"
+ "Usage:\n"
+ ">>> m.remove_style('Style Name')\n"
+ )
+
+ .def("resize",&Map::resize,
+ (arg("width"),arg("height")),
+ "Resize a Mapnik Map.\n"
+ "\n"
+ "Usage:\n"
+ ">>> m.resize(64,64)\n"
+ )
+
+ .def("scale", &Map::scale,
+ "Return the Map Scale.\n"
+ "Usage:\n"
+ "\n"
+ ">>> m.scale()\n"
+ )
+
+ .def("scale_denominator", &Map::scale_denominator,
+ "Return the Map Scale Denominator.\n"
+ "Usage:\n"
+ "\n"
+ ">>> m.scale_denominator()\n"
+ )
+
+ .def("view_transform",&Map::transform,
+ "Return the map ViewTransform object\n"
+ "which is used internally to convert between\n"
+ "geographic coordinates and screen coordinates.\n"
+ "\n"
+ "Usage:\n"
+ ">>> m.view_transform()\n"
+ )
+
+ .def("zoom",&Map::zoom,
+ (arg("factor")),
+ "Zoom in or out by a given factor.\n"
+ "positive number larger than 1, zooms out\n"
+ "positive number smaller than 1, zooms in\n"
+ "\n"
+ "Usage:\n"
+ "\n"
+ ">>> m.zoom(0.25)\n"
+ )
+
+ .def("zoom_all",&Map::zoom_all,
+ "Set the geographical extent of the map\n"
+ "to the combined extents of all active layers.\n"
+ "\n"
+ "Usage:\n"
+ ">>> m.zoom_all()\n"
+ )
+
+ .def("zoom_to_box",&Map::zoom_to_box,
+ (arg("Boxd2")),
+ "Set the geographical extent of the map\n"
+ "by specifying a Mapnik Box2d.\n"
+ "\n"
+ "Usage:\n"
+ ">>> extext = Box2d(-180.0, -90.0, 180.0, 90.0)\n"
+ ">>> m.zoom_to_box(extent)\n"
+ )
+
+ .add_property("parameters",make_function(params_nonconst,return_value_policy<reference_existing_object>()),"TODO")
+
+ .add_property("aspect_fix_mode",
+ &Map::get_aspect_fix_mode,
+ &Map::set_aspect_fix_mode,
+ // TODO - how to add arg info to properties?
+ //(arg("aspect_fix_mode")),
+ "Get/Set aspect fix mode.\n"
+ "Usage:\n"
+ "\n"
+ ">>> m.aspect_fix_mode = aspect_fix_mode.GROW_BBOX\n"
+ )
+
+ .add_property("background",make_function
+ (&Map::background,return_value_policy<copy_const_reference>()),
+ &Map::set_background,
+ "The background color of the map (same as background_color property).\n"
+ "\n"
+ "Usage:\n"
+ ">>> m.background = Color('steelblue')\n"
+ )
+
+ .add_property("background_color",make_function
+ (&Map::background,return_value_policy<copy_const_reference>()),
+ &Map::set_background,
+ "The background color of the map.\n"
+ "\n"
+ "Usage:\n"
+ ">>> m.background_color = Color('steelblue')\n"
+ )
+
+ .add_property("background_image",make_function
+ (&Map::background_image,return_value_policy<copy_const_reference>()),
+ &Map::set_background_image,
+ "The optional background image of the map.\n"
+ "\n"
+ "Usage:\n"
+ ">>> m.background_image = '/path/to/image.png'\n"
+ )
+
+ .add_property("background_image_comp_op",&Map::background_image_comp_op,
+ &Map::set_background_image_comp_op,
+ "The background image compositing operation.\n"
+ "\n"
+ "Usage:\n"
+ ">>> m.background_image_comp_op = mapnik.CompositeOp.src_over\n"
+ )
+
+ .add_property("background_image_opacity",&Map::background_image_opacity,
+ &Map::set_background_image_opacity,
+ "The background image opacity.\n"
+ "\n"
+ "Usage:\n"
+ ">>> m.background_image_opacity = 1.0\n"
+ )
+
+ .add_property("base",
+ make_function(&Map::base_path,return_value_policy<copy_const_reference>()),
+ &Map::set_base_path,
+ "The base path of the map where any files using relative \n"
+ "paths will be interpreted as relative to.\n"
+ "\n"
+ "Usage:\n"
+ ">>> m.base_path = '.'\n"
+ )
+
+ .add_property("buffer_size",
+ &Map::buffer_size,
+ &Map::set_buffer_size,
+ "Get/Set the size of buffer around map in pixels.\n"
+ "\n"
+ "Usage:\n"
+ ">>> m.buffer_size\n"
+ "0 # zero by default\n"
+ ">>> m.buffer_size = 2\n"
+ ">>> m.buffer_size\n"
+ "2\n"
+ )
+
+ .add_property("height",
+ &Map::height,
+ &Map::set_height,
+ "Get/Set the height of the map in pixels.\n"
+ "Minimum settable size is 16 pixels.\n"
+ "\n"
+ "Usage:\n"
+ ">>> m.height\n"
+ "400\n"
+ ">>> m.height = 600\n"
+ ">>> m.height\n"
+ "600\n"
+ )
+
+ .add_property("layers",make_function
+ (layers_nonconst,return_value_policy<reference_existing_object>()),
+ "The list of map layers.\n"
+ "\n"
+ "Usage:\n"
+ ">>> m.layers\n"
+ "<mapnik._mapnik.layers object at 0x6d458>"
+ ">>> m.layers[0]\n"
+ "<mapnik._mapnik.layer object at 0x5fe130>\n"
+ )
+
+ .add_property("maximum_extent",make_function
+ (&Map::maximum_extent,return_value_policy<copy_const_reference>()),
+ &set_maximum_extent,
+ "The maximum extent of the map.\n"
+ "\n"
+ "Usage:\n"
+ ">>> m.maximum_extent = Box2d(-180,-90,180,90)\n"
+ )
+
+ .add_property("srs",
+ make_function(&Map::srs,return_value_policy<copy_const_reference>()),
+ &Map::set_srs,
+ "Spatial reference in Proj.4 format.\n"
+ "Either an epsg code or proj literal.\n"
+ "For example, a proj literal:\n"
+ "\t'+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs'\n"
+ "and a proj epsg code:\n"
+ "\t'+init=epsg:4326'\n"
+ "\n"
+ "Note: using epsg codes requires the installation of\n"
+ "the Proj.4 'epsg' data file normally found in '/usr/local/share/proj'\n"
+ "\n"
+ "Usage:\n"
+ ">>> m.srs\n"
+ "'+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs' # The default srs if not initialized with custom srs\n"
+ ">>> # set to google mercator with Proj.4 literal\n"
+ "... \n"
+ ">>> m.srs = '+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0.0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs +over'\n"
+ )
+
+ .add_property("width",
+ &Map::width,
+ &Map::set_width,
+ "Get/Set the width of the map in pixels.\n"
+ "Minimum settable size is 16 pixels.\n"
+ "\n"
+ "Usage:\n"
+ ">>> m.width\n"
+ "600\n"
+ ">>> m.width = 800\n"
+ ">>> m.width\n"
+ "800\n"
+ )
+ // comparison
+ .def(self == self)
+ ;
+}
diff --git a/src/mapnik_palette.cpp b/src/mapnik_palette.cpp
new file mode 100644
index 0000000..982dbdb
--- /dev/null
+++ b/src/mapnik_palette.cpp
@@ -0,0 +1,70 @@
+/*****************************************************************************
+ *
+ * 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
+ *
+ *****************************************************************************/
+
+#include <mapnik/config.hpp>
+
+// boost
+#include "boost_std_shared_shim.hpp"
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#pragma GCC diagnostic ignored "-Wunused-local-typedef"
+#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
+
+#include <boost/python.hpp>
+#include <boost/noncopyable.hpp>
+#pragma GCC diagnostic pop
+
+//mapnik
+#include <mapnik/palette.hpp>
+
+// stl
+#include <stdexcept>
+
+static std::shared_ptr<mapnik::rgba_palette> make_palette( std::string const& palette, std::string const& format )
+{
+ mapnik::rgba_palette::palette_type type = mapnik::rgba_palette::PALETTE_RGBA;
+ if (format == "rgb")
+ type = mapnik::rgba_palette::PALETTE_RGB;
+ else if (format == "act")
+ type = mapnik::rgba_palette::PALETTE_ACT;
+ else
+ throw std::runtime_error("invalid type passed for mapnik.Palette: must be either rgba, rgb, or act");
+ return std::make_shared<mapnik::rgba_palette>(palette, type);
+}
+
+void export_palette ()
+{
+ using namespace boost::python;
+
+ class_<mapnik::rgba_palette,
+ std::shared_ptr<mapnik::rgba_palette>,
+ boost::noncopyable >("Palette",no_init)
+ //, init<std::string,std::string>(
+ // ( arg("palette"), arg("type")),
+ // "Creates a new color palette from a file\n"
+ // )
+ .def( "__init__", boost::python::make_constructor(make_palette))
+ .def("to_string", &mapnik::rgba_palette::to_string,
+ "Returns the palette as a string.\n"
+ )
+ ;
+}
diff --git a/src/mapnik_parameters.cpp b/src/mapnik_parameters.cpp
new file mode 100644
index 0000000..febf96a
--- /dev/null
+++ b/src/mapnik_parameters.cpp
@@ -0,0 +1,246 @@
+/*****************************************************************************
+ *
+ * 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
+ *
+ *****************************************************************************/
+
+#include <mapnik/config.hpp>
+
+// boost
+#include "boost_std_shared_shim.hpp"
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#pragma GCC diagnostic ignored "-Wunused-local-typedef"
+#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
+
+#include <boost/python.hpp>
+#pragma GCC diagnostic pop
+
+// mapnik
+#include <mapnik/debug.hpp>
+#include <mapnik/params.hpp>
+#include <mapnik/unicode.hpp>
+#include <mapnik/value_types.hpp>
+#include <mapnik/value.hpp>
+// stl
+#include <iterator>
+
+using mapnik::parameter;
+using mapnik::parameters;
+
+struct parameter_pickle_suite : boost::python::pickle_suite
+{
+ static boost::python::tuple
+ getinitargs(const parameter& p)
+ {
+ using namespace boost::python;
+ return boost::python::make_tuple(p.first,p.second);
+ }
+};
+
+struct parameters_pickle_suite : boost::python::pickle_suite
+{
+ static boost::python::tuple
+ getstate(const parameters& p)
+ {
+ using namespace boost::python;
+ dict d;
+ parameters::const_iterator pos=p.begin();
+ while(pos!=p.end())
+ {
+ d[pos->first] = pos->second;
+ ++pos;
+ }
+ return boost::python::make_tuple(d);
+ }
+
+ static void setstate(parameters& p, boost::python::tuple state)
+ {
+ using namespace boost::python;
+ if (len(state) != 1)
+ {
+ PyErr_SetObject(PyExc_ValueError,
+ ("expected 1-item tuple in call to __setstate__; got %s"
+ % state).ptr()
+ );
+ throw_error_already_set();
+ }
+
+ dict d = extract<dict>(state[0]);
+ boost::python::list keys = d.keys();
+ for (int i=0; i<len(keys); ++i)
+ {
+ std::string key = extract<std::string>(keys[i]);
+ object obj = d[key];
+ extract<std::string> ex0(obj);
+ extract<mapnik::value_integer> ex1(obj);
+ extract<double> ex2(obj);
+ extract<mapnik::value_unicode_string> ex3(obj);
+
+ // TODO - this is never hit - we need proper python string -> std::string to get invoked here
+ if (ex0.check())
+ {
+ p[key] = ex0();
+ }
+ else if (ex1.check())
+ {
+ p[key] = ex1();
+ }
+ else if (ex2.check())
+ {
+ p[key] = ex2();
+ }
+ else if (ex3.check())
+ {
+ std::string buffer;
+ mapnik::to_utf8(ex3(),buffer);
+ p[key] = buffer;
+ }
+ else
+ {
+ MAPNIK_LOG_DEBUG(bindings) << "parameters_pickle_suite: Could not unpickle key=" << key;
+ }
+ }
+ }
+};
+
+
+mapnik::value_holder get_params_by_key1(mapnik::parameters const& p, std::string const& key)
+{
+ parameters::const_iterator pos = p.find(key);
+ if (pos != p.end())
+ {
+ // will be auto-converted to proper python type by `mapnik_params_to_python`
+ return pos->second;
+ }
+ return mapnik::value_null();
+}
+
+mapnik::value_holder get_params_by_key2(mapnik::parameters const& p, std::string const& key)
+{
+ parameters::const_iterator pos = p.find(key);
+ if (pos == p.end())
+ {
+ PyErr_SetString(PyExc_KeyError, key.c_str());
+ boost::python::throw_error_already_set();
+ }
+ // will be auto-converted to proper python type by `mapnik_params_to_python`
+ return pos->second;
+}
+
+mapnik::parameter get_params_by_index(mapnik::parameters const& p, int index)
+{
+ if (index < 0 || static_cast<unsigned>(index) > p.size())
+ {
+ PyErr_SetString(PyExc_IndexError, "Index is out of range");
+ throw boost::python::error_already_set();
+ }
+
+ parameters::const_iterator itr = p.begin();
+ std::advance(itr, index);
+ if (itr != p.end())
+ {
+ return *itr;
+ }
+ PyErr_SetString(PyExc_IndexError, "Index is out of range");
+ throw boost::python::error_already_set();
+}
+
+unsigned get_params_size(mapnik::parameters const& p)
+{
+ return p.size();
+}
+
+void add_parameter(mapnik::parameters & p, mapnik::parameter const& param)
+{
+ p[param.first] = param.second;
+}
+
+mapnik::value_holder get_param(mapnik::parameter const& p, int index)
+{
+ if (index == 0)
+ {
+ return p.first;
+ }
+ else if (index == 1)
+ {
+ return p.second;
+ }
+ else
+ {
+ PyErr_SetString(PyExc_IndexError, "Index is out of range");
+ throw boost::python::error_already_set();
+ }
+}
+
+std::shared_ptr<mapnik::parameter> create_parameter(mapnik::value_unicode_string const& key, mapnik::value_holder const& value)
+{
+ std::string key_utf8;
+ mapnik::to_utf8(key, key_utf8);
+ return std::make_shared<mapnik::parameter>(key_utf8,value);
+}
+
+bool contains(mapnik::parameters const& p, std::string const& key)
+{
+ parameters::const_iterator pos = p.find(key);
+ return pos != p.end();
+}
+
+// needed for Python_Unicode to std::string (utf8) conversion
+
+std::shared_ptr<mapnik::parameter> create_parameter_from_string(mapnik::value_unicode_string const& key, mapnik::value_unicode_string const& ustr)
+{
+ std::string key_utf8;
+ std::string ustr_utf8;
+ mapnik::to_utf8(key, key_utf8);
+ mapnik::to_utf8(ustr,ustr_utf8);
+ return std::make_shared<mapnik::parameter>(key_utf8, ustr_utf8);
+}
+
+void export_parameters()
+{
+ using namespace boost::python;
+ implicitly_convertible<std::string,mapnik::value_holder>();
+ implicitly_convertible<mapnik::value_null,mapnik::value_holder>();
+ implicitly_convertible<mapnik::value_integer,mapnik::value_holder>();
+ implicitly_convertible<mapnik::value_double,mapnik::value_holder>();
+
+ class_<parameter,std::shared_ptr<parameter> >("Parameter",no_init)
+ .def("__init__", make_constructor(create_parameter),
+ "Create a mapnik.Parameter from a pair of values, the first being a string\n"
+ "and the second being either a string, and integer, or a float")
+ .def("__init__", make_constructor(create_parameter_from_string),
+ "Create a mapnik.Parameter from a pair of values, the first being a string\n"
+ "and the second being either a string, and integer, or a float")
+
+ .def_pickle(parameter_pickle_suite())
+ .def("__getitem__",get_param)
+ ;
+
+ class_<parameters>("Parameters",init<>())
+ .def_pickle(parameters_pickle_suite())
+ .def("get",get_params_by_key1)
+ .def("__getitem__",get_params_by_key2)
+ .def("__getitem__",get_params_by_index)
+ .def("__len__",get_params_size)
+ .def("__contains__",contains)
+ .def("append",add_parameter)
+ .def("iteritems",iterator<parameters>())
+ ;
+}
diff --git a/src/mapnik_proj_transform.cpp b/src/mapnik_proj_transform.cpp
new file mode 100644
index 0000000..8f25a90
--- /dev/null
+++ b/src/mapnik_proj_transform.cpp
@@ -0,0 +1,154 @@
+/*****************************************************************************
+ *
+ * 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
+ *
+ *****************************************************************************/
+
+#include <mapnik/config.hpp>
+
+
+// boost
+#include "boost_std_shared_shim.hpp"
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#pragma GCC diagnostic ignored "-Wunused-local-typedef"
+#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
+
+#include <boost/python.hpp>
+#include <boost/noncopyable.hpp>
+#pragma GCC diagnostic pop
+
+// mapnik
+#include <mapnik/proj_transform.hpp>
+#include <mapnik/projection.hpp>
+#include <mapnik/coord.hpp>
+#include <mapnik/box2d.hpp>
+
+// stl
+#include <stdexcept>
+
+
+using mapnik::proj_transform;
+using mapnik::projection;
+
+struct proj_transform_pickle_suite : boost::python::pickle_suite
+{
+ static boost::python::tuple
+ getinitargs(const proj_transform& p)
+ {
+ using namespace boost::python;
+ return boost::python::make_tuple(p.source(),p.dest());
+ }
+};
+
+namespace {
+
+mapnik::coord2d forward_transform_c(mapnik::proj_transform& t, mapnik::coord2d const& c)
+{
+ double x = c.x;
+ double y = c.y;
+ double z = 0.0;
+ if (!t.forward(x,y,z)) {
+ std::ostringstream s;
+ s << "Failed to forward project "
+ << "from " << t.source().params() << " to: " << t.dest().params();
+ throw std::runtime_error(s.str());
+ }
+ return mapnik::coord2d(x,y);
+}
+
+mapnik::coord2d backward_transform_c(mapnik::proj_transform& t, mapnik::coord2d const& c)
+{
+ double x = c.x;
+ double y = c.y;
+ double z = 0.0;
+ if (!t.backward(x,y,z)) {
+ std::ostringstream s;
+ s << "Failed to back project "
+ << "from " << t.dest().params() << " to: " << t.source().params();
+ throw std::runtime_error(s.str());
+ }
+ return mapnik::coord2d(x,y);
+}
+
+mapnik::box2d<double> forward_transform_env(mapnik::proj_transform& t, mapnik::box2d<double> const & box)
+{
+ mapnik::box2d<double> new_box = box;
+ if (!t.forward(new_box)) {
+ std::ostringstream s;
+ s << "Failed to forward project "
+ << "from " << t.source().params() << " to: " << t.dest().params();
+ throw std::runtime_error(s.str());
+ }
+ return new_box;
+}
+
+mapnik::box2d<double> backward_transform_env(mapnik::proj_transform& t, mapnik::box2d<double> const & box)
+{
+ mapnik::box2d<double> new_box = box;
+ if (!t.backward(new_box)){
+ std::ostringstream s;
+ s << "Failed to back project "
+ << "from " << t.dest().params() << " to: " << t.source().params();
+ throw std::runtime_error(s.str());
+ }
+ return new_box;
+}
+
+mapnik::box2d<double> forward_transform_env_p(mapnik::proj_transform& t, mapnik::box2d<double> const & box, unsigned int points)
+{
+ mapnik::box2d<double> new_box = box;
+ if (!t.forward(new_box,points)) {
+ std::ostringstream s;
+ s << "Failed to forward project "
+ << "from " << t.source().params() << " to: " << t.dest().params();
+ throw std::runtime_error(s.str());
+ }
+ return new_box;
+}
+
+mapnik::box2d<double> backward_transform_env_p(mapnik::proj_transform& t, mapnik::box2d<double> const & box, unsigned int points)
+{
+ mapnik::box2d<double> new_box = box;
+ if (!t.backward(new_box,points)){
+ std::ostringstream s;
+ s << "Failed to back project "
+ << "from " << t.dest().params() << " to: " << t.source().params();
+ throw std::runtime_error(s.str());
+ }
+ return new_box;
+}
+
+}
+
+void export_proj_transform ()
+{
+ using namespace boost::python;
+
+ class_<proj_transform, boost::noncopyable>("ProjTransform", init< projection const&, projection const& >())
+ .def_pickle(proj_transform_pickle_suite())
+ .def("forward", forward_transform_c)
+ .def("backward",backward_transform_c)
+ .def("forward", forward_transform_env)
+ .def("backward",backward_transform_env)
+ .def("forward", forward_transform_env_p)
+ .def("backward",backward_transform_env_p)
+ ;
+
+}
diff --git a/src/mapnik_projection.cpp b/src/mapnik_projection.cpp
new file mode 100644
index 0000000..d194d56
--- /dev/null
+++ b/src/mapnik_projection.cpp
@@ -0,0 +1,125 @@
+/*****************************************************************************
+ *
+ * This file is part of Mapnik (c++ mapping toolkit)
+ *
+ * Copyright (C) 2015 Artem Pavlenko, Jean-Francois Doyon
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ *****************************************************************************/
+
+#include <mapnik/config.hpp>
+
+// boost
+#include "boost_std_shared_shim.hpp"
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#pragma GCC diagnostic ignored "-Wunused-local-typedef"
+#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
+
+#include <boost/python.hpp>
+#pragma GCC diagnostic pop
+
+// mapnik
+#include <mapnik/coord.hpp>
+#include <mapnik/box2d.hpp>
+#include <mapnik/projection.hpp>
+
+using mapnik::projection;
+
+struct projection_pickle_suite : boost::python::pickle_suite
+{
+ static boost::python::tuple
+ getinitargs(const projection& p)
+ {
+ using namespace boost::python;
+ return boost::python::make_tuple(p.params());
+ }
+};
+
+namespace {
+mapnik::coord2d forward_pt(mapnik::coord2d const& pt,
+ mapnik::projection const& prj)
+{
+ double x = pt.x;
+ double y = pt.y;
+ prj.forward(x,y);
+ return mapnik::coord2d(x,y);
+}
+
+mapnik::coord2d inverse_pt(mapnik::coord2d const& pt,
+ mapnik::projection const& prj)
+{
+ double x = pt.x;
+ double y = pt.y;
+ prj.inverse(x,y);
+ return mapnik::coord2d(x,y);
+}
+
+mapnik::box2d<double> forward_env(mapnik::box2d<double> const & box,
+ mapnik::projection const& prj)
+{
+ double minx = box.minx();
+ double miny = box.miny();
+ double maxx = box.maxx();
+ double maxy = box.maxy();
+ prj.forward(minx,miny);
+ prj.forward(maxx,maxy);
+ return mapnik::box2d<double>(minx,miny,maxx,maxy);
+}
+
+mapnik::box2d<double> inverse_env(mapnik::box2d<double> const & box,
+ mapnik::projection const& prj)
+{
+ double minx = box.minx();
+ double miny = box.miny();
+ double maxx = box.maxx();
+ double maxy = box.maxy();
+ prj.inverse(minx,miny);
+ prj.inverse(maxx,maxy);
+ return mapnik::box2d<double>(minx,miny,maxx,maxy);
+}
+
+}
+
+void export_projection ()
+{
+ using namespace boost::python;
+
+ class_<projection>("Projection", "Represents a map projection.",init<std::string const&>(
+ (arg("proj4_string")),
+ "Constructs a new projection from its PROJ.4 string representation.\n"
+ "\n"
+ "The constructor will throw a RuntimeError in case the projection\n"
+ "cannot be initialized.\n"
+ )
+ )
+ .def_pickle(projection_pickle_suite())
+ .def ("params", make_function(&projection::params,
+ return_value_policy<copy_const_reference>()),
+ "Returns the PROJ.4 string for this projection.\n")
+ .def ("expanded",&projection::expanded,
+ "normalize PROJ.4 definition by expanding +init= syntax\n")
+ .add_property ("geographic", &projection::is_geographic,
+ "This property is True if the projection is a geographic projection\n"
+ "(i.e. it uses lon/lat coordinates)\n")
+ ;
+
+ def("forward_",&forward_pt);
+ def("inverse_",&inverse_pt);
+ def("forward_",&forward_env);
+ def("inverse_",&inverse_env);
+
+}
diff --git a/src/mapnik_python.cpp b/src/mapnik_python.cpp
new file mode 100644
index 0000000..b4c4c3b
--- /dev/null
+++ b/src/mapnik_python.cpp
@@ -0,0 +1,1072 @@
+/*****************************************************************************
+ *
+ * This file is part of Mapnik (c++ mapping toolkit)
+ *
+ * Copyright (C) 2015 Artem Pavlenko, Jean-Francois Doyon
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ *****************************************************************************/
+
+#include <mapnik/config.hpp>
+
+// boost
+#include "boost_std_shared_shim.hpp"
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#pragma GCC diagnostic ignored "-Wunused-local-typedef"
+#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
+#include "python_to_value.hpp"
+#include <boost/python/args.hpp> // for keywords, arg, etc
+#include <boost/python/converter/from_python.hpp>
+#include <boost/python/def.hpp> // for def
+#include <boost/python/detail/defaults_gen.hpp>
+#include <boost/python/detail/none.hpp> // for none
+#include <boost/python/dict.hpp> // for dict
+#include <boost/python/exception_translator.hpp>
+#include <boost/python/list.hpp> // for list
+#include <boost/python/module.hpp> // for BOOST_PYTHON_MODULE
+#include <boost/python/object_core.hpp> // for get_managed_object
+#include <boost/python/register_ptr_to_python.hpp>
+#include <boost/python/to_python_converter.hpp>
+#pragma GCC diagnostic pop
+
+// stl
+#include <stdexcept>
+#include <fstream>
+
+void export_color();
+void export_coord();
+void export_layer();
+void export_parameters();
+void export_envelope();
+void export_query();
+void export_geometry();
+void export_palette();
+void export_image();
+void export_image_view();
+void export_gamma_method();
+void export_scaling_method();
+#if defined(GRID_RENDERER)
+void export_grid();
+void export_grid_view();
+#endif
+void export_map();
+void export_python();
+void export_expression();
+void export_rule();
+void export_style();
+void export_feature();
+void export_featureset();
+void export_fontset();
+void export_datasource();
+void export_datasource_cache();
+void export_symbolizer();
+void export_markers_symbolizer();
+void export_point_symbolizer();
+void export_line_symbolizer();
+void export_line_pattern_symbolizer();
+void export_polygon_symbolizer();
+void export_building_symbolizer();
+void export_polygon_pattern_symbolizer();
+void export_raster_symbolizer();
+void export_text_placement();
+void export_shield_symbolizer();
+void export_debug_symbolizer();
+void export_group_symbolizer();
+void export_font_engine();
+void export_projection();
+void export_proj_transform();
+void export_view_transform();
+void export_raster_colorizer();
+void export_label_collision_detector();
+void export_logger();
+
+#include <mapnik/version.hpp>
+#include <mapnik/map.hpp>
+#include <mapnik/datasource.hpp>
+#include <mapnik/layer.hpp>
+#include <mapnik/agg_renderer.hpp>
+#include <mapnik/rule.hpp>
+#include <mapnik/image_util.hpp>
+#include <mapnik/image_any.hpp>
+#include <mapnik/load_map.hpp>
+#include <mapnik/value_error.hpp>
+#include <mapnik/save_map.hpp>
+#include <mapnik/scale_denominator.hpp>
+#if defined(GRID_RENDERER)
+#include "python_grid_utils.hpp"
+#endif
+#include "mapnik_value_converter.hpp"
+#include "mapnik_enumeration_wrapper_converter.hpp"
+#include "mapnik_threads.hpp"
+#include "python_optional.hpp"
+#include <mapnik/marker_cache.hpp>
+#if defined(SHAPE_MEMORY_MAPPED_FILE)
+#include <mapnik/mapped_memory_cache.hpp>
+#endif
+
+#if defined(SVG_RENDERER)
+#include <mapnik/svg/output/svg_renderer.hpp>
+#endif
+
+namespace mapnik {
+ class font_set;
+ class layer;
+ class color;
+ class label_collision_detector4;
+}
+void clear_cache()
+{
+ mapnik::marker_cache::instance().clear();
+#if defined(SHAPE_MEMORY_MAPPED_FILE)
+ mapnik::mapped_memory_cache::instance().clear();
+#endif
+}
+
+#if defined(HAVE_CAIRO)
+#include <mapnik/cairo_io.hpp>
+#include <mapnik/cairo/cairo_renderer.hpp>
+#include <cairo.h>
+#endif
+
+#if defined(HAVE_PYCAIRO)
+#include <boost/python/type_id.hpp>
+#include <boost/python/converter/registry.hpp>
+#include <pycairo.h>
+static Pycairo_CAPI_t *Pycairo_CAPI;
+static void *extract_surface(PyObject* op)
+{
+ if (PyObject_TypeCheck(op, const_cast<PyTypeObject*>(Pycairo_CAPI->Surface_Type)))
+ {
+ return op;
+ }
+ else
+ {
+ return 0;
+ }
+}
+
+static void *extract_context(PyObject* op)
+{
+ if (PyObject_TypeCheck(op, const_cast<PyTypeObject*>(Pycairo_CAPI->Context_Type)))
+ {
+ return op;
+ }
+ else
+ {
+ return 0;
+ }
+}
+
+void register_cairo()
+{
+#if PY_MAJOR_VERSION >= 3
+ Pycairo_CAPI = (Pycairo_CAPI_t*) PyCapsule_Import(const_cast<char *>("cairo.CAPI"), 0);
+#else
+ Pycairo_CAPI = (Pycairo_CAPI_t*) PyCObject_Import(const_cast<char *>("cairo"), const_cast<char *>("CAPI"));
+#endif
+ if (Pycairo_CAPI == nullptr) return;
+
+ boost::python::converter::registry::insert(&extract_surface, boost::python::type_id<PycairoSurface>());
+ boost::python::converter::registry::insert(&extract_context, boost::python::type_id<PycairoContext>());
+}
+#endif
+
+using mapnik::python_thread;
+using mapnik::python_unblock_auto_block;
+#ifdef MAPNIK_DEBUG
+bool python_thread::thread_support = true;
+#endif
+boost::thread_specific_ptr<PyThreadState> python_thread::state;
+
+struct agg_renderer_visitor_1
+{
+ agg_renderer_visitor_1(mapnik::Map const& m, double scale_factor, unsigned offset_x, unsigned offset_y)
+ : m_(m), scale_factor_(scale_factor), offset_x_(offset_x), offset_y_(offset_y) {}
+
+ template <typename T>
+ void operator() (T & pixmap)
+ {
+ throw std::runtime_error("This image type is not currently supported for rendering.");
+ }
+
+ private:
+ mapnik::Map const& m_;
+ double scale_factor_;
+ unsigned offset_x_;
+ unsigned offset_y_;
+};
+
+template <>
+void agg_renderer_visitor_1::operator()<mapnik::image_rgba8> (mapnik::image_rgba8 & pixmap)
+{
+ mapnik::agg_renderer<mapnik::image_rgba8> ren(m_,pixmap,scale_factor_,offset_x_, offset_y_);
+ ren.apply();
+}
+
+struct agg_renderer_visitor_2
+{
+ agg_renderer_visitor_2(mapnik::Map const &m, std::shared_ptr<mapnik::label_collision_detector4> detector,
+ double scale_factor, unsigned offset_x, unsigned offset_y)
+ : m_(m), detector_(detector), scale_factor_(scale_factor), offset_x_(offset_x), offset_y_(offset_y) {}
+
+ template <typename T>
+ void operator() (T & pixmap)
+ {
+ throw std::runtime_error("This image type is not currently supported for rendering.");
+ }
+
+ private:
+ mapnik::Map const& m_;
+ std::shared_ptr<mapnik::label_collision_detector4> detector_;
+ double scale_factor_;
+ unsigned offset_x_;
+ unsigned offset_y_;
+};
+
+template <>
+void agg_renderer_visitor_2::operator()<mapnik::image_rgba8> (mapnik::image_rgba8 & pixmap)
+{
+ mapnik::agg_renderer<mapnik::image_rgba8> ren(m_,pixmap,detector_, scale_factor_,offset_x_, offset_y_);
+ ren.apply();
+}
+
+struct agg_renderer_visitor_3
+{
+ agg_renderer_visitor_3(mapnik::Map const& m, mapnik::request const& req, mapnik::attributes const& vars,
+ double scale_factor, unsigned offset_x, unsigned offset_y)
+ : m_(m), req_(req), vars_(vars), scale_factor_(scale_factor), offset_x_(offset_x), offset_y_(offset_y) {}
+
+ template <typename T>
+ void operator() (T & pixmap)
+ {
+ throw std::runtime_error("This image type is not currently supported for rendering.");
+ }
+
+ private:
+ mapnik::Map const& m_;
+ mapnik::request const& req_;
+ mapnik::attributes const& vars_;
+ double scale_factor_;
+ unsigned offset_x_;
+ unsigned offset_y_;
+
+};
+
+template <>
+void agg_renderer_visitor_3::operator()<mapnik::image_rgba8> (mapnik::image_rgba8 & pixmap)
+{
+ mapnik::agg_renderer<mapnik::image_rgba8> ren(m_,req_, vars_, pixmap, scale_factor_, offset_x_, offset_y_);
+ ren.apply();
+}
+
+struct agg_renderer_visitor_4
+{
+ agg_renderer_visitor_4(mapnik::Map const& m, double scale_factor, unsigned offset_x, unsigned offset_y,
+ mapnik::layer const& layer, std::set<std::string>& names)
+ : m_(m), scale_factor_(scale_factor), offset_x_(offset_x), offset_y_(offset_y),
+ layer_(layer), names_(names) {}
+
+ template <typename T>
+ void operator() (T & pixmap)
+ {
+ throw std::runtime_error("This image type is not currently supported for rendering.");
+ }
+
+ private:
+ mapnik::Map const& m_;
+ double scale_factor_;
+ unsigned offset_x_;
+ unsigned offset_y_;
+ mapnik::layer const& layer_;
+ std::set<std::string> & names_;
+};
+
+template <>
+void agg_renderer_visitor_4::operator()<mapnik::image_rgba8> (mapnik::image_rgba8 & pixmap)
+{
+ mapnik::agg_renderer<mapnik::image_rgba8> ren(m_,pixmap,scale_factor_,offset_x_, offset_y_);
+ ren.apply(layer_, names_);
+}
+
+
+void render(mapnik::Map const& map,
+ mapnik::image_any& image,
+ double scale_factor = 1.0,
+ unsigned offset_x = 0u,
+ unsigned offset_y = 0u)
+{
+ python_unblock_auto_block b;
+ mapnik::util::apply_visitor(agg_renderer_visitor_1(map, scale_factor, offset_x, offset_y), image);
+}
+
+void render_with_vars(mapnik::Map const& map,
+ mapnik::image_any& image,
+ boost::python::dict const& d,
+ double scale_factor = 1.0,
+ unsigned offset_x = 0u,
+ unsigned offset_y = 0u)
+{
+ mapnik::attributes vars = mapnik::dict2attr(d);
+ mapnik::request req(map.width(),map.height(),map.get_current_extent());
+ req.set_buffer_size(map.buffer_size());
+ python_unblock_auto_block b;
+ mapnik::util::apply_visitor(agg_renderer_visitor_3(map, req, vars, scale_factor, offset_x, offset_y), image);
+}
+
+void render_with_detector(
+ mapnik::Map const& map,
+ mapnik::image_any &image,
+ std::shared_ptr<mapnik::label_collision_detector4> detector,
+ double scale_factor = 1.0,
+ unsigned offset_x = 0u,
+ unsigned offset_y = 0u)
+{
+ python_unblock_auto_block b;
+ mapnik::util::apply_visitor(agg_renderer_visitor_2(map, detector, scale_factor, offset_x, offset_y), image);
+}
+
+void render_layer2(mapnik::Map const& map,
+ mapnik::image_any& image,
+ unsigned layer_idx,
+ double scale_factor,
+ unsigned offset_x,
+ unsigned offset_y)
+{
+ std::vector<mapnik::layer> const& layers = map.layers();
+ std::size_t layer_num = layers.size();
+ if (layer_idx >= layer_num) {
+ std::ostringstream s;
+ s << "Zero-based layer index '" << layer_idx << "' not valid, only '"
+ << layer_num << "' layers are in map\n";
+ throw std::runtime_error(s.str());
+ }
+
+ python_unblock_auto_block b;
+ mapnik::layer const& layer = layers[layer_idx];
+ std::set<std::string> names;
+ mapnik::util::apply_visitor(agg_renderer_visitor_4(map, scale_factor, offset_x, offset_y, layer, names), image);
+}
+
+#if defined(HAVE_CAIRO) && defined(HAVE_PYCAIRO)
+
+void render3(mapnik::Map const& map,
+ PycairoSurface* py_surface,
+ double scale_factor = 1.0,
+ unsigned offset_x = 0,
+ unsigned offset_y = 0)
+{
+ python_unblock_auto_block b;
+ mapnik::cairo_surface_ptr surface(cairo_surface_reference(py_surface->surface), mapnik::cairo_surface_closer());
+ mapnik::cairo_renderer<mapnik::cairo_ptr> ren(map,mapnik::create_context(surface),scale_factor,offset_x,offset_y);
+ ren.apply();
+}
+
+void render4(mapnik::Map const& map, PycairoSurface* py_surface)
+{
+ python_unblock_auto_block b;
+ mapnik::cairo_surface_ptr surface(cairo_surface_reference(py_surface->surface), mapnik::cairo_surface_closer());
+ mapnik::cairo_renderer<mapnik::cairo_ptr> ren(map,mapnik::create_context(surface));
+ ren.apply();
+}
+
+void render5(mapnik::Map const& map,
+ PycairoContext* py_context,
+ double scale_factor = 1.0,
+ unsigned offset_x = 0,
+ unsigned offset_y = 0)
+{
+ python_unblock_auto_block b;
+ mapnik::cairo_ptr context(cairo_reference(py_context->ctx), mapnik::cairo_closer());
+ mapnik::cairo_renderer<mapnik::cairo_ptr> ren(map,context,scale_factor,offset_x, offset_y);
+ ren.apply();
+}
+
+void render6(mapnik::Map const& map, PycairoContext* py_context)
+{
+ python_unblock_auto_block b;
+ mapnik::cairo_ptr context(cairo_reference(py_context->ctx), mapnik::cairo_closer());
+ mapnik::cairo_renderer<mapnik::cairo_ptr> ren(map,context);
+ ren.apply();
+}
+void render_with_detector2(
+ mapnik::Map const& map,
+ PycairoContext* py_context,
+ std::shared_ptr<mapnik::label_collision_detector4> detector)
+{
+ python_unblock_auto_block b;
+ mapnik::cairo_ptr context(cairo_reference(py_context->ctx), mapnik::cairo_closer());
+ mapnik::cairo_renderer<mapnik::cairo_ptr> ren(map,context,detector);
+ ren.apply();
+}
+
+void render_with_detector3(
+ mapnik::Map const& map,
+ PycairoContext* py_context,
+ std::shared_ptr<mapnik::label_collision_detector4> detector,
+ double scale_factor = 1.0,
+ unsigned offset_x = 0u,
+ unsigned offset_y = 0u)
+{
+ python_unblock_auto_block b;
+ mapnik::cairo_ptr context(cairo_reference(py_context->ctx), mapnik::cairo_closer());
+ mapnik::cairo_renderer<mapnik::cairo_ptr> ren(map,context,detector,scale_factor,offset_x,offset_y);
+ ren.apply();
+}
+
+void render_with_detector4(
+ mapnik::Map const& map,
+ PycairoSurface* py_surface,
+ std::shared_ptr<mapnik::label_collision_detector4> detector)
+{
+ python_unblock_auto_block b;
+ mapnik::cairo_surface_ptr surface(cairo_surface_reference(py_surface->surface), mapnik::cairo_surface_closer());
+ mapnik::cairo_renderer<mapnik::cairo_ptr> ren(map, mapnik::create_context(surface), detector);
+ ren.apply();
+}
+
+void render_with_detector5(
+ mapnik::Map const& map,
+ PycairoSurface* py_surface,
+ std::shared_ptr<mapnik::label_collision_detector4> detector,
+ double scale_factor = 1.0,
+ unsigned offset_x = 0u,
+ unsigned offset_y = 0u)
+{
+ python_unblock_auto_block b;
+ mapnik::cairo_surface_ptr surface(cairo_surface_reference(py_surface->surface), mapnik::cairo_surface_closer());
+ mapnik::cairo_renderer<mapnik::cairo_ptr> ren(map, mapnik::create_context(surface), detector, scale_factor, offset_x, offset_y);
+ ren.apply();
+}
+
+#endif
+
+
+void render_tile_to_file(mapnik::Map const& map,
+ unsigned offset_x, unsigned offset_y,
+ unsigned width, unsigned height,
+ std::string const& file,
+ std::string const& format)
+{
+ mapnik::image_any image(width,height);
+ render(map,image,1.0,offset_x, offset_y);
+ mapnik::save_to_file(image,file,format);
+}
+
+void render_to_file1(mapnik::Map const& map,
+ std::string const& filename,
+ std::string const& format)
+{
+ if (format == "svg-ng")
+ {
+#if defined(SVG_RENDERER)
+ std::ofstream file (filename.c_str(), std::ios::out|std::ios::trunc|std::ios::binary);
+ if (!file)
+ {
+ throw mapnik::image_writer_exception("could not open file for writing: " + filename);
+ }
+ using iter_type = std::ostream_iterator<char>;
+ iter_type output_stream_iterator(file);
+ mapnik::svg_renderer<iter_type> ren(map,output_stream_iterator);
+ ren.apply();
+#else
+ throw mapnik::image_writer_exception("SVG backend not available, cannot write to format: " + format);
+#endif
+ }
+ else if (format == "pdf" || format == "svg" || format =="ps" || format == "ARGB32" || format == "RGB24")
+ {
+#if defined(HAVE_CAIRO)
+ mapnik::save_to_cairo_file(map,filename,format,1.0);
+#else
+ throw mapnik::image_writer_exception("Cairo backend not available, cannot write to format: " + format);
+#endif
+ }
+ else
+ {
+ mapnik::image_any image(map.width(),map.height());
+ render(map,image,1.0,0,0);
+ mapnik::save_to_file(image,filename,format);
+ }
+}
+
+void render_to_file2(mapnik::Map const& map,std::string const& filename)
+{
+ std::string format = mapnik::guess_type(filename);
+ if (format == "pdf" || format == "svg" || format =="ps")
+ {
+#if defined(HAVE_CAIRO)
+ mapnik::save_to_cairo_file(map,filename,format,1.0);
+#else
+ throw mapnik::image_writer_exception("Cairo backend not available, cannot write to format: " + format);
+#endif
+ }
+ else
+ {
+ mapnik::image_any image(map.width(),map.height());
+ render(map,image,1.0,0,0);
+ mapnik::save_to_file(image,filename);
+ }
+}
+
+void render_to_file3(mapnik::Map const& map,
+ std::string const& filename,
+ std::string const& format,
+ double scale_factor = 1.0
+ )
+{
+ if (format == "svg-ng")
+ {
+#if defined(SVG_RENDERER)
+ std::ofstream file (filename.c_str(), std::ios::out|std::ios::trunc|std::ios::binary);
+ if (!file)
+ {
+ throw mapnik::image_writer_exception("could not open file for writing: " + filename);
+ }
+ using iter_type = std::ostream_iterator<char>;
+ iter_type output_stream_iterator(file);
+ mapnik::svg_renderer<iter_type> ren(map,output_stream_iterator,scale_factor);
+ ren.apply();
+#else
+ throw mapnik::image_writer_exception("SVG backend not available, cannot write to format: " + format);
+#endif
+ }
+ else if (format == "pdf" || format == "svg" || format =="ps" || format == "ARGB32" || format == "RGB24")
+ {
+#if defined(HAVE_CAIRO)
+ mapnik::save_to_cairo_file(map,filename,format,scale_factor);
+#else
+ throw mapnik::image_writer_exception("Cairo backend not available, cannot write to format: " + format);
+#endif
+ }
+ else
+ {
+ mapnik::image_any image(map.width(),map.height());
+ render(map,image,scale_factor,0,0);
+ mapnik::save_to_file(image,filename,format);
+ }
+}
+
+double scale_denominator(mapnik::Map const& map, bool geographic)
+{
+ return mapnik::scale_denominator(map.scale(), geographic);
+}
+
+// http://docs.python.org/c-api/exceptions.html#standard-exceptions
+void value_error_translator(mapnik::value_error const & ex)
+{
+ PyErr_SetString(PyExc_ValueError, ex.what());
+}
+
+void runtime_error_translator(std::runtime_error const & ex)
+{
+ PyErr_SetString(PyExc_RuntimeError, ex.what());
+}
+
+void out_of_range_error_translator(std::out_of_range const & ex)
+{
+ PyErr_SetString(PyExc_IndexError, ex.what());
+}
+
+void standard_error_translator(std::exception const & ex)
+{
+ PyErr_SetString(PyExc_RuntimeError, ex.what());
+}
+
+unsigned mapnik_version()
+{
+ return MAPNIK_VERSION;
+}
+
+std::string mapnik_version_string()
+{
+ return MAPNIK_VERSION_STRING;
+}
+
+bool has_proj4()
+{
+#if defined(MAPNIK_USE_PROJ4)
+ return true;
+#else
+ return false;
+#endif
+}
+
+bool has_svg_renderer()
+{
+#if defined(SVG_RENDERER)
+ return true;
+#else
+ return false;
+#endif
+}
+
+bool has_grid_renderer()
+{
+#if defined(GRID_RENDERER)
+ return true;
+#else
+ return false;
+#endif
+}
+
+bool has_jpeg()
+{
+#if defined(HAVE_JPEG)
+ return true;
+#else
+ return false;
+#endif
+}
+
+bool has_png()
+{
+#if defined(HAVE_PNG)
+ return true;
+#else
+ return false;
+#endif
+}
+
+bool has_tiff()
+{
+#if defined(HAVE_TIFF)
+ return true;
+#else
+ return false;
+#endif
+}
+
+bool has_webp()
+{
+#if defined(HAVE_WEBP)
+ return true;
+#else
+ return false;
+#endif
+}
+
+// indicator for cairo rendering support inside libmapnik
+bool has_cairo()
+{
+#if defined(HAVE_CAIRO)
+ return true;
+#else
+ return false;
+#endif
+}
+
+// indicator for pycairo support in the python bindings
+bool has_pycairo()
+{
+#if defined(HAVE_CAIRO) && defined(HAVE_PYCAIRO)
+#if PY_MAJOR_VERSION >= 3
+ Pycairo_CAPI = (Pycairo_CAPI_t*) PyCapsule_Import(const_cast<char *>("cairo.CAPI"), 0);
+#else
+ Pycairo_CAPI = (Pycairo_CAPI_t*) PyCObject_Import(const_cast<char *>("cairo"), const_cast<char *>("CAPI"));
+#endif
+ if (Pycairo_CAPI == nullptr){
+ /*
+ Case where pycairo support has been compiled into
+ mapnik but at runtime the cairo python module
+ is unable to be imported and therefore Pycairo surfaces
+ and contexts cannot be passed to mapnik.render()
+ */
+ return false;
+ }
+ return true;
+#else
+ return false;
+#endif
+}
+
+
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-local-typedef"
+BOOST_PYTHON_FUNCTION_OVERLOADS(load_map_overloads, load_map, 2, 4)
+BOOST_PYTHON_FUNCTION_OVERLOADS(load_map_string_overloads, load_map_string, 2, 4)
+BOOST_PYTHON_FUNCTION_OVERLOADS(save_map_overloads, save_map, 2, 3)
+BOOST_PYTHON_FUNCTION_OVERLOADS(save_map_to_string_overloads, save_map_to_string, 1, 2)
+BOOST_PYTHON_FUNCTION_OVERLOADS(render_overloads, render, 2, 5)
+BOOST_PYTHON_FUNCTION_OVERLOADS(render_with_detector_overloads, render_with_detector, 3, 6)
+#pragma GCC diagnostic pop
+
+BOOST_PYTHON_MODULE(_mapnik)
+{
+
+ using namespace boost::python;
+
+ using mapnik::load_map;
+ using mapnik::load_map_string;
+ using mapnik::save_map;
+ using mapnik::save_map_to_string;
+
+ register_exception_translator<std::exception>(&standard_error_translator);
+ register_exception_translator<std::out_of_range>(&out_of_range_error_translator);
+ register_exception_translator<mapnik::value_error>(&value_error_translator);
+ register_exception_translator<std::runtime_error>(&runtime_error_translator);
+#if defined(HAVE_CAIRO) && defined(HAVE_PYCAIRO)
+ register_cairo();
+#endif
+ export_query();
+ export_geometry();
+ export_feature();
+ export_featureset();
+ export_fontset();
+ export_datasource();
+ export_parameters();
+ export_color();
+ export_envelope();
+ export_palette();
+ export_image();
+ export_image_view();
+ export_gamma_method();
+ export_scaling_method();
+#if defined(GRID_RENDERER)
+ export_grid();
+ export_grid_view();
+#endif
+ export_expression();
+ export_rule();
+ export_style();
+ export_layer();
+ export_datasource_cache();
+ export_symbolizer();
+ export_markers_symbolizer();
+ export_point_symbolizer();
+ export_line_symbolizer();
+ export_line_pattern_symbolizer();
+ export_polygon_symbolizer();
+ export_building_symbolizer();
+ export_polygon_pattern_symbolizer();
+ export_raster_symbolizer();
+ export_text_placement();
+ export_shield_symbolizer();
+ export_debug_symbolizer();
+ export_group_symbolizer();
+ export_font_engine();
+ export_projection();
+ export_proj_transform();
+ export_view_transform();
+ export_coord();
+ export_map();
+ export_raster_colorizer();
+ export_label_collision_detector();
+ export_logger();
+
+ def("clear_cache", &clear_cache,
+ "\n"
+ "Clear all global caches of markers and mapped memory regions.\n"
+ "\n"
+ "Usage:\n"
+ ">>> from mapnik import clear_cache\n"
+ ">>> clear_cache()\n"
+ );
+
+ def("render_to_file",&render_to_file1,
+ "\n"
+ "Render Map to file using explicit image type.\n"
+ "\n"
+ "Usage:\n"
+ ">>> from mapnik import Map, render_to_file, load_map\n"
+ ">>> m = Map(256,256)\n"
+ ">>> load_map(m,'mapfile.xml')\n"
+ ">>> render_to_file(m,'image32bit.png','png')\n"
+ "\n"
+ "8 bit (paletted) PNG can be requested with 'png256':\n"
+ ">>> render_to_file(m,'8bit_image.png','png256')\n"
+ "\n"
+ "JPEG quality can be controlled by adding a suffix to\n"
+ "'jpeg' between 0 and 100 (default is 85):\n"
+ ">>> render_to_file(m,'top_quality.jpeg','jpeg100')\n"
+ ">>> render_to_file(m,'medium_quality.jpeg','jpeg50')\n"
+ );
+
+ def("render_to_file",&render_to_file2,
+ "\n"
+ "Render Map to file (type taken from file extension)\n"
+ "\n"
+ "Usage:\n"
+ ">>> from mapnik import Map, render_to_file, load_map\n"
+ ">>> m = Map(256,256)\n"
+ ">>> render_to_file(m,'image.jpeg')\n"
+ "\n"
+ );
+
+ def("render_to_file",&render_to_file3,
+ "\n"
+ "Render Map to file using explicit image type and scale factor.\n"
+ "\n"
+ "Usage:\n"
+ ">>> from mapnik import Map, render_to_file, load_map\n"
+ ">>> m = Map(256,256)\n"
+ ">>> scale_factor = 4\n"
+ ">>> render_to_file(m,'image.jpeg',scale_factor)\n"
+ "\n"
+ );
+
+ def("render_tile_to_file",&render_tile_to_file,
+ "\n"
+ "TODO\n"
+ "\n"
+ );
+
+ def("render_with_vars",&render_with_vars,
+ (arg("map"),
+ arg("image"),
+ arg("vars"),
+ arg("scale_factor")=1.0,
+ arg("offset_x")=0,
+ arg("offset_y")=0
+ )
+ );
+
+ def("render", &render, render_overloads(
+ "\n"
+ "Render Map to an AGG image_any using offsets\n"
+ "\n"
+ "Usage:\n"
+ ">>> from mapnik import Map, Image, render, load_map\n"
+ ">>> m = Map(256,256)\n"
+ ">>> load_map(m,'mapfile.xml')\n"
+ ">>> im = Image(m.width,m.height)\n"
+ ">>> scale_factor=2.0\n"
+ ">>> offset = [100,50]\n"
+ ">>> render(m,im)\n"
+ ">>> render(m,im,scale_factor)\n"
+ ">>> render(m,im,scale_factor,offset[0],offset[1])\n"
+ "\n"
+ ));
+
+ def("render_with_detector", &render_with_detector, render_with_detector_overloads(
+ "\n"
+ "Render Map to an AGG image_any using a pre-constructed detector.\n"
+ "\n"
+ "Usage:\n"
+ ">>> from mapnik import Map, Image, LabelCollisionDetector, render_with_detector, load_map\n"
+ ">>> m = Map(256,256)\n"
+ ">>> load_map(m,'mapfile.xml')\n"
+ ">>> im = Image(m.width,m.height)\n"
+ ">>> detector = LabelCollisionDetector(m)\n"
+ ">>> render_with_detector(m, im, detector)\n"
+ ));
+
+ def("render_layer", &render_layer2,
+ (arg("map"),
+ arg("image"),
+ arg("layer"),
+ arg("scale_factor")=1.0,
+ arg("offset_x")=0,
+ arg("offset_y")=0
+ )
+ );
+
+#if defined(GRID_RENDERER)
+ def("render_layer", &mapnik::render_layer_for_grid,
+ (arg("map"),
+ arg("grid"),
+ arg("layer"),
+ arg("fields")=boost::python::list(),
+ arg("scale_factor")=1.0,
+ arg("offset_x")=0,
+ arg("offset_y")=0
+ )
+ );
+#endif
+
+#if defined(HAVE_CAIRO) && defined(HAVE_PYCAIRO)
+ def("render",&render3,
+ "\n"
+ "Render Map to Cairo Surface using offsets\n"
+ "\n"
+ "Usage:\n"
+ ">>> from mapnik import Map, render, load_map\n"
+ ">>> from cairo import SVGSurface\n"
+ ">>> m = Map(256,256)\n"
+ ">>> load_map(m,'mapfile.xml')\n"
+ ">>> surface = SVGSurface('image.svg', m.width, m.height)\n"
+ ">>> render(m,surface,1,1)\n"
+ "\n"
+ );
+
+ def("render",&render4,
+ "\n"
+ "Render Map to Cairo Surface\n"
+ "\n"
+ "Usage:\n"
+ ">>> from mapnik import Map, render, load_map\n"
+ ">>> from cairo import SVGSurface\n"
+ ">>> m = Map(256,256)\n"
+ ">>> load_map(m,'mapfile.xml')\n"
+ ">>> surface = SVGSurface('image.svg', m.width, m.height)\n"
+ ">>> render(m,surface)\n"
+ "\n"
+ );
+
+ def("render",&render5,
+ "\n"
+ "Render Map to Cairo Context using offsets\n"
+ "\n"
+ "Usage:\n"
+ ">>> from mapnik import Map, render, load_map\n"
+ ">>> from cairo import SVGSurface, Context\n"
+ ">>> surface = SVGSurface('image.svg', m.width, m.height)\n"
+ ">>> ctx = Context(surface)\n"
+ ">>> load_map(m,'mapfile.xml')\n"
+ ">>> render(m,context,1,1)\n"
+ "\n"
+ );
+
+ def("render",&render6,
+ "\n"
+ "Render Map to Cairo Context\n"
+ "\n"
+ "Usage:\n"
+ ">>> from mapnik import Map, render, load_map\n"
+ ">>> from cairo import SVGSurface, Context\n"
+ ">>> surface = SVGSurface('image.svg', m.width, m.height)\n"
+ ">>> ctx = Context(surface)\n"
+ ">>> load_map(m,'mapfile.xml')\n"
+ ">>> render(m,context)\n"
+ "\n"
+ );
+
+ def("render_with_detector", &render_with_detector2,
+ "\n"
+ "Render Map to Cairo Context using a pre-constructed detector.\n"
+ "\n"
+ "Usage:\n"
+ ">>> from mapnik import Map, LabelCollisionDetector, render_with_detector, load_map\n"
+ ">>> from cairo import SVGSurface, Context\n"
+ ">>> surface = SVGSurface('image.svg', m.width, m.height)\n"
+ ">>> ctx = Context(surface)\n"
+ ">>> m = Map(256,256)\n"
+ ">>> load_map(m,'mapfile.xml')\n"
+ ">>> detector = LabelCollisionDetector(m)\n"
+ ">>> render_with_detector(m, ctx, detector)\n"
+ );
+
+ def("render_with_detector", &render_with_detector3,
+ "\n"
+ "Render Map to Cairo Context using a pre-constructed detector, scale and offsets.\n"
+ "\n"
+ "Usage:\n"
+ ">>> from mapnik import Map, LabelCollisionDetector, render_with_detector, load_map\n"
+ ">>> from cairo import SVGSurface, Context\n"
+ ">>> surface = SVGSurface('image.svg', m.width, m.height)\n"
+ ">>> ctx = Context(surface)\n"
+ ">>> m = Map(256,256)\n"
+ ">>> load_map(m,'mapfile.xml')\n"
+ ">>> detector = LabelCollisionDetector(m)\n"
+ ">>> render_with_detector(m, ctx, detector, 1, 1, 1)\n"
+ );
+
+ def("render_with_detector", &render_with_detector4,
+ "\n"
+ "Render Map to Cairo Surface using a pre-constructed detector.\n"
+ "\n"
+ "Usage:\n"
+ ">>> from mapnik import Map, LabelCollisionDetector, render_with_detector, load_map\n"
+ ">>> from cairo import SVGSurface, Context\n"
+ ">>> surface = SVGSurface('image.svg', m.width, m.height)\n"
+ ">>> m = Map(256,256)\n"
+ ">>> load_map(m,'mapfile.xml')\n"
+ ">>> detector = LabelCollisionDetector(m)\n"
+ ">>> render_with_detector(m, surface, detector)\n"
+ );
+
+ def("render_with_detector", &render_with_detector5,
+ "\n"
+ "Render Map to Cairo Surface using a pre-constructed detector, scale and offsets.\n"
+ "\n"
+ "Usage:\n"
+ ">>> from mapnik import Map, LabelCollisionDetector, render_with_detector, load_map\n"
+ ">>> from cairo import SVGSurface, Context\n"
+ ">>> surface = SVGSurface('image.svg', m.width, m.height)\n"
+ ">>> m = Map(256,256)\n"
+ ">>> load_map(m,'mapfile.xml')\n"
+ ">>> detector = LabelCollisionDetector(m)\n"
+ ">>> render_with_detector(m, surface, detector, 1, 1, 1)\n"
+ );
+
+#endif
+
+ def("scale_denominator", &scale_denominator,
+ (arg("map"),arg("is_geographic")),
+ "\n"
+ "Return the Map Scale Denominator.\n"
+ "Also available as Map.scale_denominator()\n"
+ "\n"
+ "Usage:\n"
+ "\n"
+ ">>> from mapnik import Map, Projection, scale_denominator, load_map\n"
+ ">>> m = Map(256,256)\n"
+ ">>> load_map(m,'mapfile.xml')\n"
+ ">>> scale_denominator(m,Projection(m.srs).geographic)\n"
+ "\n"
+ );
+
+ def("load_map", &load_map, load_map_overloads());
+
+ def("load_map_from_string", &load_map_string, load_map_string_overloads());
+
+ def("save_map", &save_map, save_map_overloads());
+/*
+ "\n"
+ "Save Map object to XML file\n"
+ "\n"
+ "Usage:\n"
+ ">>> from mapnik import Map, load_map, save_map\n"
+ ">>> m = Map(256,256)\n"
+ ">>> load_map(m,'mapfile_wgs84.xml')\n"
+ ">>> m.srs\n"
+ "'+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs'\n"
+ ">>> m.srs = '+init=espg:3395'\n"
+ ">>> save_map(m,'mapfile_mercator.xml')\n"
+ "\n"
+ );
+*/
+
+ def("save_map_to_string", &save_map_to_string, save_map_to_string_overloads());
+ def("mapnik_version", &mapnik_version,"Get the Mapnik version number");
+ def("mapnik_version_string", &mapnik_version_string,"Get the Mapnik version string");
+ def("has_proj4", &has_proj4, "Get proj4 status");
+ def("has_jpeg", &has_jpeg, "Get jpeg read/write support status");
+ def("has_png", &has_png, "Get png read/write support status");
+ def("has_tiff", &has_tiff, "Get tiff read/write support status");
+ def("has_webp", &has_webp, "Get webp read/write support status");
+ def("has_svg_renderer", &has_svg_renderer, "Get svg_renderer status");
+ def("has_grid_renderer", &has_grid_renderer, "Get grid_renderer status");
+ def("has_cairo", &has_cairo, "Get cairo library status");
+ def("has_pycairo", &has_pycairo, "Get pycairo module status");
+
+ python_optional<mapnik::font_set>();
+ python_optional<mapnik::color>();
+ python_optional<mapnik::box2d<double> >();
+ python_optional<mapnik::composite_mode_e>();
+ python_optional<mapnik::datasource_geometry_t>();
+ python_optional<std::string>();
+ python_optional<unsigned>();
+ python_optional<double>();
+ python_optional<float>();
+ python_optional<bool>();
+ python_optional<int>();
+ python_optional<mapnik::text_transform_e>();
+ register_ptr_to_python<mapnik::expression_ptr>();
+ register_ptr_to_python<mapnik::path_expression_ptr>();
+ to_python_converter<mapnik::value_holder,mapnik_param_to_python>();
+ to_python_converter<mapnik::value,mapnik_value_to_python>();
+ to_python_converter<mapnik::enumeration_wrapper,mapnik_enumeration_wrapper_to_python>();
+}
diff --git a/src/mapnik_query.cpp b/src/mapnik_query.cpp
new file mode 100644
index 0000000..0172abe
--- /dev/null
+++ b/src/mapnik_query.cpp
@@ -0,0 +1,107 @@
+/*****************************************************************************
+ *
+ * This file is part of Mapnik (c++ mapping toolkit)
+ *
+ * Copyright (C) 2015 Artem Pavlenko, Jean-Francois Doyon
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ *****************************************************************************/
+
+#include <mapnik/config.hpp>
+
+// boost
+#include "boost_std_shared_shim.hpp"
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#pragma GCC diagnostic ignored "-Wunused-local-typedef"
+#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
+
+#include "python_to_value.hpp"
+#include <boost/python.hpp>
+#pragma GCC diagnostic pop
+
+// mapnik
+#include <mapnik/query.hpp>
+#include <mapnik/box2d.hpp>
+
+#include <string>
+#include <set>
+
+using mapnik::query;
+using mapnik::box2d;
+
+namespace python = boost::python;
+
+struct resolution_to_tuple
+{
+ static PyObject* convert(query::resolution_type const& x)
+ {
+ python::object tuple(python::make_tuple(std::get<0>(x), std::get<1>(x)));
+ return python::incref(tuple.ptr());
+ }
+
+ static PyTypeObject const* get_pytype()
+ {
+ return &PyTuple_Type;
+ }
+};
+
+struct names_to_list
+{
+ static PyObject* convert(std::set<std::string> const& names)
+ {
+ boost::python::list l;
+ for ( std::string const& name : names )
+ {
+ l.append(name);
+ }
+ return python::incref(l.ptr());
+ }
+
+ static PyTypeObject const* get_pytype()
+ {
+ return &PyList_Type;
+ }
+};
+
+namespace {
+
+ void set_variables(mapnik::query & q, boost::python::dict const& d)
+ {
+ mapnik::attributes vars = mapnik::dict2attr(d);
+ q.set_variables(vars);
+ }
+}
+
+void export_query()
+{
+ using namespace boost::python;
+
+ to_python_converter<query::resolution_type, resolution_to_tuple> ();
+ to_python_converter<std::set<std::string>, names_to_list> ();
+
+ class_<query>("Query", "a spatial query data object",
+ init<box2d<double>,query::resolution_type const&,double>() )
+ .def(init<box2d<double> >())
+ .add_property("resolution",make_function(&query::resolution,
+ return_value_policy<copy_const_reference>()))
+ .add_property("bbox", make_function(&query::get_bbox,
+ return_value_policy<copy_const_reference>()) )
+ .add_property("property_names", make_function(&query::property_names,
+ return_value_policy<copy_const_reference>()) )
+ .def("add_property_name", &query::add_property_name)
+ .def("set_variables",&set_variables);
+}
diff --git a/src/mapnik_raster_colorizer.cpp b/src/mapnik_raster_colorizer.cpp
new file mode 100644
index 0000000..833ba6f
--- /dev/null
+++ b/src/mapnik_raster_colorizer.cpp
@@ -0,0 +1,241 @@
+/*****************************************************************************
+ *
+ * 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
+ *
+ *****************************************************************************/
+
+#include <mapnik/config.hpp>
+
+// boost
+#include "boost_std_shared_shim.hpp"
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#pragma GCC diagnostic ignored "-Wunused-local-typedef"
+#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
+
+#include <boost/python.hpp>
+#include <boost/python/suite/indexing/vector_indexing_suite.hpp>
+#pragma GCC diagnostic pop
+
+// mapnik
+#include <mapnik/raster_colorizer.hpp>
+#include <mapnik/symbolizer.hpp>
+
+using mapnik::raster_colorizer;
+using mapnik::raster_colorizer_ptr;
+using mapnik::symbolizer_base;
+using mapnik::colorizer_stop;
+using mapnik::colorizer_stops;
+using mapnik::colorizer_mode_enum;
+using mapnik::color;
+using mapnik::COLORIZER_INHERIT;
+using mapnik::COLORIZER_LINEAR;
+using mapnik::COLORIZER_DISCRETE;
+using mapnik::COLORIZER_EXACT;
+
+
+namespace {
+void add_stop(raster_colorizer_ptr & rc, colorizer_stop & stop)
+{
+ rc->add_stop(stop);
+}
+
+void add_stop2(raster_colorizer_ptr & rc, float v)
+{
+ colorizer_stop stop(v, rc->get_default_mode(), rc->get_default_color());
+ rc->add_stop(stop);
+}
+
+void add_stop3(raster_colorizer_ptr &rc, float v, color c)
+{
+ colorizer_stop stop(v, rc->get_default_mode(), c);
+ rc->add_stop(stop);
+}
+
+void add_stop4(raster_colorizer_ptr &rc, float v, colorizer_mode_enum m)
+{
+ colorizer_stop stop(v, m, rc->get_default_color());
+ rc->add_stop(stop);
+}
+
+void add_stop5(raster_colorizer_ptr &rc, float v, colorizer_mode_enum m, color c)
+{
+ colorizer_stop stop(v, m, c);
+ rc->add_stop(stop);
+}
+
+mapnik::color get_color(raster_colorizer_ptr &rc, float value)
+{
+ unsigned rgba = rc->get_color(value);
+ unsigned r = (rgba & 0xff);
+ unsigned g = (rgba >> 8 ) & 0xff;
+ unsigned b = (rgba >> 16) & 0xff;
+ unsigned a = (rgba >> 24) & 0xff;
+ return mapnik::color(r,g,b,a);
+}
+
+colorizer_stops const& get_stops(raster_colorizer_ptr & rc)
+{
+ return rc->get_stops();
+}
+
+}
+
+void export_raster_colorizer()
+{
+ using namespace boost::python;
+
+ implicitly_convertible<raster_colorizer_ptr, mapnik::symbolizer_base::value_type>();
+
+ class_<raster_colorizer,raster_colorizer_ptr>("RasterColorizer",
+ "A Raster Colorizer object.",
+ init<colorizer_mode_enum, color>(args("default_mode","default_color"))
+ )
+ .def(init<>())
+ .add_property("default_color",
+ make_function(&raster_colorizer::get_default_color, return_value_policy<reference_existing_object>()),
+ &raster_colorizer::set_default_color,
+ "The default color for stops added without a color (mapnik.Color).\n")
+ .add_property("default_mode",
+ &raster_colorizer::get_default_mode_enum,
+ &raster_colorizer::set_default_mode_enum,
+ "The default mode (mapnik.ColorizerMode).\n"
+ "\n"
+ "If a stop is added without a mode, then it will inherit this default mode\n")
+ .add_property("stops",
+ make_function(get_stops,return_value_policy<reference_existing_object>()),
+ "The list of stops this RasterColorizer contains\n")
+ .add_property("epsilon",
+ &raster_colorizer::get_epsilon,
+ &raster_colorizer::set_epsilon,
+ "Comparison epsilon value for exact mode\n"
+ "\n"
+ "When comparing values in exact mode, values need only be within epsilon to match.\n")
+
+
+ .def("add_stop", add_stop,
+ (arg("ColorizerStop")),
+ "Add a colorizer stop to the raster colorizer.\n"
+ "\n"
+ "Usage:\n"
+ ">>> colorizer = mapnik.RasterColorizer()\n"
+ ">>> color = mapnik.Color(\"#0044cc\")\n"
+ ">>> stop = mapnik.ColorizerStop(3, mapnik.COLORIZER_INHERIT, color)\n"
+ ">>> colorizer.add_stop(stop)\n"
+ )
+ .def("add_stop", add_stop2,
+ (arg("value")),
+ "Add a colorizer stop to the raster colorizer, using the default mode and color.\n"
+ "\n"
+ "Usage:\n"
+ ">>> default_color = mapnik.Color(\"#0044cc\")\n"
+ ">>> colorizer = mapnik.RasterColorizer(mapnik.COLORIZER_LINEAR, default_color)\n"
+ ">>> colorizer.add_stop(100)\n"
+ )
+ .def("add_stop", add_stop3,
+ (arg("value")),
+ "Add a colorizer stop to the raster colorizer, using the default mode.\n"
+ "\n"
+ "Usage:\n"
+ ">>> default_color = mapnik.Color(\"#0044cc\")\n"
+ ">>> colorizer = mapnik.RasterColorizer(mapnik.COLORIZER_LINEAR, default_color)\n"
+ ">>> colorizer.add_stop(100, mapnik.Color(\"#123456\"))\n"
+ )
+ .def("add_stop", add_stop4,
+ (arg("value")),
+ "Add a colorizer stop to the raster colorizer, using the default color.\n"
+ "\n"
+ "Usage:\n"
+ ">>> default_color = mapnik.Color(\"#0044cc\")\n"
+ ">>> colorizer = mapnik.RasterColorizer(mapnik.COLORIZER_LINEAR, default_color)\n"
+ ">>> colorizer.add_stop(100, mapnik.COLORIZER_EXACT)\n"
+ )
+ .def("add_stop", add_stop5,
+ (arg("value")),
+ "Add a colorizer stop to the raster colorizer.\n"
+ "\n"
+ "Usage:\n"
+ ">>> default_color = mapnik.Color(\"#0044cc\")\n"
+ ">>> colorizer = mapnik.RasterColorizer(mapnik.COLORIZER_LINEAR, default_color)\n"
+ ">>> colorizer.add_stop(100, mapnik.COLORIZER_DISCRETE, mapnik.Color(\"#112233\"))\n"
+ )
+ .def("get_color", get_color,
+ "Get the color assigned to a certain value in raster data.\n"
+ "\n"
+ "Usage:\n"
+ ">>> colorizer = mapnik.RasterColorizer()\n"
+ ">>> color = mapnik.Color(\"#0044cc\")\n"
+ ">>> colorizer.add_stop(0, mapnik.COLORIZER_DISCRETE, mapnik.Color(\"#000000\"))\n"
+ ">>> colorizer.add_stop(100, mapnik.COLORIZER_DISCRETE, mapnik.Color(\"#0E0A06\"))\n"
+ ">>> colorizer.get_color(50)\n"
+ "Color('#070503')\n"
+ )
+ ;
+
+
+
+ class_<colorizer_stops>("ColorizerStops",
+ "A RasterColorizer's collection of ordered color stops.\n"
+ "This class is not meant to be instantiated from python. However, "
+ "it can be accessed at a RasterColorizer's \"stops\" attribute for "
+ "introspection purposes",
+ no_init)
+ .def(vector_indexing_suite<colorizer_stops>())
+ ;
+
+ enum_<colorizer_mode_enum>("ColorizerMode")
+ .value("COLORIZER_INHERIT", COLORIZER_INHERIT)
+ .value("COLORIZER_LINEAR", COLORIZER_LINEAR)
+ .value("COLORIZER_DISCRETE", COLORIZER_DISCRETE)
+ .value("COLORIZER_EXACT", COLORIZER_EXACT)
+ .export_values()
+ ;
+
+
+ class_<colorizer_stop>("ColorizerStop",init<float, colorizer_mode_enum, color const&>(
+ "A Colorizer Stop object.\n"
+ "Create with a value, ColorizerMode, and Color\n"
+ "\n"
+ "Usage:"
+ ">>> color = mapnik.Color(\"#fff000\")\n"
+ ">>> stop= mapnik.ColorizerStop(42.42, mapnik.COLORIZER_LINEAR, color)\n"
+ ))
+ .add_property("color",
+ make_function(&colorizer_stop::get_color, return_value_policy<reference_existing_object>()),
+ &colorizer_stop::set_color,
+ "The stop color (mapnik.Color).\n")
+ .add_property("value",
+ &colorizer_stop::get_value,
+ &colorizer_stop::set_value,
+ "The stop value.\n")
+ .add_property("label",
+ make_function(&colorizer_stop::get_label, return_value_policy<copy_const_reference>()),
+ &colorizer_stop::set_label,
+ "The stop label.\n")
+ .add_property("mode",
+ &colorizer_stop::get_mode_enum,
+ &colorizer_stop::set_mode_enum,
+ "The stop mode (mapnik.ColorizerMode).\n"
+ "\n"
+ "If this is COLORIZER_INHERIT then it will inherit the default mode\n"
+ " from the RasterColorizer it is added to.\n")
+ .def(self == self)
+ .def("__str__",&colorizer_stop::to_string)
+ ;
+}
diff --git a/src/mapnik_rule.cpp b/src/mapnik_rule.cpp
new file mode 100644
index 0000000..a9210ee
--- /dev/null
+++ b/src/mapnik_rule.cpp
@@ -0,0 +1,100 @@
+/*****************************************************************************
+ *
+ * This file is part of Mapnik (c++ mapping toolkit)
+ *
+ * Copyright (C) 2015 Artem Pavlenko, Jean-Francois Doyon
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ *****************************************************************************/
+
+#include <mapnik/config.hpp>
+
+// boost
+#include "boost_std_shared_shim.hpp"
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#pragma GCC diagnostic ignored "-Wunused-local-typedef"
+#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
+
+#include <boost/python.hpp>
+#include <boost/python/implicit.hpp>
+#include <boost/python/suite/indexing/vector_indexing_suite.hpp>
+#pragma GCC diagnostic pop
+
+// mapnik
+#include <mapnik/rule.hpp>
+#include <mapnik/expression.hpp>
+#include <mapnik/expression_string.hpp>
+
+using mapnik::rule;
+using mapnik::expr_node;
+using mapnik::expression_ptr;
+using mapnik::point_symbolizer;
+using mapnik::line_symbolizer;
+using mapnik::line_pattern_symbolizer;
+using mapnik::polygon_symbolizer;
+using mapnik::polygon_pattern_symbolizer;
+using mapnik::raster_symbolizer;
+using mapnik::shield_symbolizer;
+using mapnik::text_symbolizer;
+using mapnik::building_symbolizer;
+using mapnik::markers_symbolizer;
+using mapnik::group_symbolizer;
+using mapnik::symbolizer;
+using mapnik::to_expression_string;
+
+void export_rule()
+{
+ using namespace boost::python;
+ implicitly_convertible<point_symbolizer,symbolizer>();
+ implicitly_convertible<line_symbolizer,symbolizer>();
+ implicitly_convertible<line_pattern_symbolizer,symbolizer>();
+ implicitly_convertible<polygon_symbolizer,symbolizer>();
+ implicitly_convertible<building_symbolizer,symbolizer>();
+ implicitly_convertible<polygon_pattern_symbolizer,symbolizer>();
+ implicitly_convertible<raster_symbolizer,symbolizer>();
+ implicitly_convertible<shield_symbolizer,symbolizer>();
+ implicitly_convertible<text_symbolizer,symbolizer>();
+ implicitly_convertible<markers_symbolizer,symbolizer>();
+ implicitly_convertible<group_symbolizer,symbolizer>();
+
+ class_<rule::symbolizers>("Symbolizers",init<>("TODO"))
+ .def(vector_indexing_suite<rule::symbolizers>())
+ ;
+
+ class_<rule>("Rule",init<>("default constructor"))
+ .def(init<std::string const&,
+ boost::python::optional<double,double> >())
+ .add_property("name",make_function
+ (&rule::get_name,
+ return_value_policy<copy_const_reference>()),
+ &rule::set_name)
+ .add_property("filter",make_function
+ (&rule::get_filter,return_value_policy<copy_const_reference>()),
+ &rule::set_filter)
+ .add_property("min_scale",&rule::get_min_scale,&rule::set_min_scale)
+ .add_property("max_scale",&rule::get_max_scale,&rule::set_max_scale)
+ .def("set_else",&rule::set_else)
+ .def("has_else",&rule::has_else_filter)
+ .def("set_also",&rule::set_also)
+ .def("has_also",&rule::has_also_filter)
+ .def("active",&rule::active)
+ .add_property("symbols",make_function
+ (&rule::get_symbolizers,return_value_policy<reference_existing_object>()))
+ .add_property("copy_symbols",make_function
+ (&rule::get_symbolizers,return_value_policy<copy_const_reference>()))
+ ;
+}
diff --git a/src/mapnik_scaling_method.cpp b/src/mapnik_scaling_method.cpp
new file mode 100644
index 0000000..cdb8b34
--- /dev/null
+++ b/src/mapnik_scaling_method.cpp
@@ -0,0 +1,58 @@
+/*****************************************************************************
+ *
+ * This file is part of Mapnik (c++ mapping toolkit)
+ *
+ * Copyright (C) 2015 Artem Pavlenko, Jean-Francois Doyon
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ *****************************************************************************/
+
+
+#include <mapnik/image_scaling.hpp>
+
+// boost
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#pragma GCC diagnostic ignored "-Wunused-local-typedef"
+#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
+
+#include <boost/python.hpp>
+#pragma GCC diagnostic pop
+
+void export_scaling_method()
+{
+ using namespace boost::python;
+
+ enum_<mapnik::scaling_method_e>("scaling_method")
+ .value("NEAR", mapnik::SCALING_NEAR)
+ .value("BILINEAR", mapnik::SCALING_BILINEAR)
+ .value("BICUBIC", mapnik::SCALING_BICUBIC)
+ .value("SPLINE16", mapnik::SCALING_SPLINE16)
+ .value("SPLINE36", mapnik::SCALING_SPLINE36)
+ .value("HANNING", mapnik::SCALING_HANNING)
+ .value("HAMMING", mapnik::SCALING_HAMMING)
+ .value("HERMITE", mapnik::SCALING_HERMITE)
+ .value("KAISER", mapnik::SCALING_KAISER)
+ .value("QUADRIC", mapnik::SCALING_QUADRIC)
+ .value("CATROM", mapnik::SCALING_CATROM)
+ .value("GAUSSIAN", mapnik::SCALING_GAUSSIAN)
+ .value("BESSEL", mapnik::SCALING_BESSEL)
+ .value("MITCHELL", mapnik::SCALING_MITCHELL)
+ .value("SINC", mapnik::SCALING_SINC)
+ .value("LANCZOS", mapnik::SCALING_LANCZOS)
+ .value("BLACKMAN", mapnik::SCALING_BLACKMAN)
+ ;
+}
diff --git a/src/mapnik_style.cpp b/src/mapnik_style.cpp
new file mode 100644
index 0000000..1ddff2d
--- /dev/null
+++ b/src/mapnik_style.cpp
@@ -0,0 +1,118 @@
+/*****************************************************************************
+ *
+ * This file is part of Mapnik (c++ mapping toolkit)
+ *
+ * Copyright (C) 2015 Artem Pavlenko, Jean-Francois Doyon
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ *****************************************************************************/
+
+#include <mapnik/config.hpp>
+
+// boost
+#include "boost_std_shared_shim.hpp"
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#pragma GCC diagnostic ignored "-Wunused-local-typedef"
+#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
+
+#include <boost/python.hpp>
+#include <boost/python/suite/indexing/vector_indexing_suite.hpp>
+#pragma GCC diagnostic pop
+
+// mapnik
+#include <mapnik/value_error.hpp>
+#include <mapnik/rule.hpp>
+#include "mapnik_enumeration.hpp"
+#include <mapnik/feature_type_style.hpp>
+#include <mapnik/image_filter_types.hpp> // generate_image_filters
+
+using mapnik::feature_type_style;
+using mapnik::rules;
+using mapnik::rule;
+
+std::string get_image_filters(feature_type_style & style)
+{
+ std::string filters_str;
+ std::back_insert_iterator<std::string> sink(filters_str);
+ generate_image_filters(sink, style.image_filters());
+ return filters_str;
+}
+
+void set_image_filters(feature_type_style & style, std::string const& filters)
+{
+ std::vector<mapnik::filter::filter_type> new_filters;
+ bool result = parse_image_filters(filters, new_filters);
+ if (!result)
+ {
+ throw mapnik::value_error("failed to parse image-filters: '" + filters + "'");
+ }
+#ifdef _WINDOWS
+ style.image_filters() = new_filters;
+ // FIXME : https://svn.boost.org/trac/boost/ticket/2839
+#else
+ style.image_filters() = std::move(new_filters);
+#endif
+}
+
+void export_style()
+{
+ using namespace boost::python;
+
+ mapnik::enumeration_<mapnik::filter_mode_e>("filter_mode")
+ .value("ALL",mapnik::FILTER_ALL)
+ .value("FIRST",mapnik::FILTER_FIRST)
+ ;
+
+ class_<rules>("Rules",init<>("default ctor"))
+ .def(vector_indexing_suite<rules>())
+ ;
+ class_<feature_type_style>("Style",init<>("default style constructor"))
+
+ .add_property("rules",make_function
+ (&feature_type_style::get_rules,
+ return_value_policy<reference_existing_object>()),
+ "List of rules belonging to a style as rule objects.\n"
+ "\n"
+ "Usage:\n"
+ ">>> for r in m.find_style('style 1').rules:\n"
+ ">>> print r\n"
+ "<mapnik._mapnik.Rule object at 0x100549910>\n"
+ "<mapnik._mapnik.Rule object at 0x100549980>\n"
+ )
+ .add_property("filter_mode",
+ &feature_type_style::get_filter_mode,
+ &feature_type_style::set_filter_mode,
+ "Set/get the filter mode of the style")
+ .add_property("opacity",
+ &feature_type_style::get_opacity,
+ &feature_type_style::set_opacity,
+ "Set/get the opacity of the style")
+ .add_property("comp_op",
+ &feature_type_style::comp_op,
+ &feature_type_style::set_comp_op,
+ "Set/get the comp-op (composite operation) of the style")
+ .add_property("image_filters_inflate",
+ &feature_type_style::image_filters_inflate,
+ &feature_type_style::image_filters_inflate,
+ "Set/get the image_filters_inflate property of the style")
+ .add_property("image_filters",
+ get_image_filters,
+ set_image_filters,
+ "Set/get the comp-op (composite operation) of the style")
+ ;
+
+}
diff --git a/src/mapnik_svg.hpp b/src/mapnik_svg.hpp
new file mode 100644
index 0000000..418ee05
--- /dev/null
+++ b/src/mapnik_svg.hpp
@@ -0,0 +1,56 @@
+/*****************************************************************************
+ *
+ * This file is part of Mapnik (c++ mapping toolkit)
+ *
+ * Copyright (C) 2010 Robert Coup
+ *
+ * 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 MAPNIK_PYTHON_BINDING_SVG_INCLUDED
+#define MAPNIK_PYTHON_BINDING_SVG_INCLUDED
+
+// mapnik
+#include <mapnik/parse_transform.hpp>
+#include <mapnik/symbolizer.hpp>
+#include <mapnik/value_error.hpp>
+
+namespace mapnik {
+using namespace boost::python;
+
+template <class T>
+std::string get_svg_transform(T& symbolizer)
+{
+ return symbolizer.get_image_transform_string();
+}
+
+template <class T>
+void set_svg_transform(T& symbolizer, std::string const& transform_wkt)
+{
+ transform_list_ptr trans_expr = mapnik::parse_transform(transform_wkt);
+ if (!trans_expr)
+ {
+ std::stringstream ss;
+ ss << "Could not parse transform from '"
+ << transform_wkt
+ << "', expected SVG transform attribute";
+ throw mapnik::value_error(ss.str());
+ }
+ symbolizer.set_image_transform(trans_expr);
+}
+
+} // end of namespace mapnik
+
+#endif // MAPNIK_PYTHON_BINDING_SVG_INCLUDED
diff --git a/src/mapnik_svg_generator_grammar.cpp b/src/mapnik_svg_generator_grammar.cpp
new file mode 100644
index 0000000..5c02b6e
--- /dev/null
+++ b/src/mapnik_svg_generator_grammar.cpp
@@ -0,0 +1,27 @@
+/*****************************************************************************
+ *
+ * This file is part of Mapnik (c++ mapping toolkit)
+ *
+ * Copyright (C) 2015 Artem Pavlenko, Jean-Francois Doyon
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ *****************************************************************************/
+
+#include <mapnik/svg/geometry_svg_generator_impl.hpp>
+#include <string>
+
+using sink_type = std::back_insert_iterator<std::string>;
+template struct mapnik::svg::svg_path_generator<sink_type, mapnik::vertex_adapter>;
diff --git a/src/mapnik_symbolizer.cpp b/src/mapnik_symbolizer.cpp
new file mode 100644
index 0000000..4bd03f8
--- /dev/null
+++ b/src/mapnik_symbolizer.cpp
@@ -0,0 +1,422 @@
+/*****************************************************************************
+ *
+ * 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
+ *
+ *****************************************************************************/
+
+#include <mapnik/config.hpp>
+
+// boost
+#include "boost_std_shared_shim.hpp"
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#pragma GCC diagnostic ignored "-Wunused-local-typedef"
+#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
+
+#include <boost/python.hpp>
+#include <boost/python/suite/indexing/map_indexing_suite.hpp>
+#pragma GCC diagnostic pop
+
+// mapnik
+#include <mapnik/symbolizer.hpp>
+#include <mapnik/symbolizer_hash.hpp>
+#include <mapnik/symbolizer_utils.hpp>
+#include <mapnik/symbolizer_keys.hpp>
+#include <mapnik/image_util.hpp>
+#include <mapnik/parse_path.hpp>
+#include <mapnik/path_expression.hpp>
+#include "mapnik_enumeration.hpp"
+#include "mapnik_svg.hpp"
+#include <mapnik/expression_node.hpp>
+#include <mapnik/value_error.hpp>
+#include <mapnik/marker_cache.hpp> // for known_svg_prefix_
+#include <mapnik/group/group_layout.hpp>
+#include <mapnik/group/group_rule.hpp>
+#include <mapnik/group/group_symbolizer_properties.hpp>
+#include <mapnik/util/variant.hpp>
+
+// stl
+#include <sstream>
+
+using mapnik::symbolizer;
+using mapnik::point_symbolizer;
+using mapnik::line_symbolizer;
+using mapnik::line_pattern_symbolizer;
+using mapnik::polygon_symbolizer;
+using mapnik::polygon_pattern_symbolizer;
+using mapnik::raster_symbolizer;
+using mapnik::shield_symbolizer;
+using mapnik::text_symbolizer;
+using mapnik::building_symbolizer;
+using mapnik::markers_symbolizer;
+using mapnik::debug_symbolizer;
+using mapnik::group_symbolizer;
+using mapnik::symbolizer_base;
+using mapnik::color;
+using mapnik::path_processor_type;
+using mapnik::path_expression_ptr;
+using mapnik::guess_type;
+using mapnik::expression_ptr;
+using mapnik::parse_path;
+
+
+namespace {
+using namespace boost::python;
+void __setitem__(mapnik::symbolizer_base & sym, std::string const& name, mapnik::symbolizer_base::value_type const& val)
+{
+ put(sym, mapnik::get_key(name), val);
+}
+
+std::shared_ptr<mapnik::symbolizer_base::value_type> numeric_wrapper(const object& arg)
+{
+ std::shared_ptr<mapnik::symbolizer_base::value_type> result;
+ if (PyBool_Check(arg.ptr()))
+ {
+ mapnik::value_bool val = extract<mapnik::value_bool>(arg);
+ result.reset(new mapnik::symbolizer_base::value_type(val));
+ }
+ else if (PyFloat_Check(arg.ptr()))
+ {
+ mapnik::value_double val = extract<mapnik::value_double>(arg);
+ result.reset(new mapnik::symbolizer_base::value_type(val));
+ }
+ else
+ {
+ mapnik::value_integer val = extract<mapnik::value_integer>(arg);
+ result.reset(new mapnik::symbolizer_base::value_type(val));
+ }
+ return result;
+}
+
+struct extract_python_object
+{
+ using result_type = boost::python::object;
+
+ template <typename T>
+ auto operator() (T const& val) const -> result_type
+ {
+ return result_type(val); // wrap into python object
+ }
+};
+
+boost::python::object __getitem__(mapnik::symbolizer_base const& sym, std::string const& name)
+{
+ using const_iterator = symbolizer_base::cont_type::const_iterator;
+ mapnik::keys key = mapnik::get_key(name);
+ const_iterator itr = sym.properties.find(key);
+ if (itr != sym.properties.end())
+ {
+ return mapnik::util::apply_visitor(extract_python_object(), itr->second);
+ }
+ //mapnik::property_meta_type const& meta = mapnik::get_meta(key);
+ //return mapnik::util::apply_visitor(extract_python_object(), std::get<1>(meta));
+ return boost::python::object();
+}
+
+/*
+std::string __str__(mapnik::symbolizer const& sym)
+{
+ return mapnik::util::apply_visitor(mapnik::symbolizer_to_json(), sym);
+}
+*/
+
+std::string get_symbolizer_type(symbolizer const& sym)
+{
+ return mapnik::symbolizer_name(sym); // FIXME - do we need this ?
+}
+
+std::size_t hash_impl(symbolizer const& sym)
+{
+ return mapnik::util::apply_visitor(mapnik::symbolizer_hash_visitor(), sym);
+}
+
+template <typename T>
+std::size_t hash_impl_2(T const& sym)
+{
+ return mapnik::symbolizer_hash::value<T>(sym);
+}
+
+struct extract_underlying_type_visitor
+{
+ template <typename T>
+ boost::python::object operator() (T const& sym) const
+ {
+ return boost::python::object(sym);
+ }
+};
+
+boost::python::object extract_underlying_type(symbolizer const& sym)
+{
+ return mapnik::util::apply_visitor(extract_underlying_type_visitor(), sym);
+}
+
+}
+
+void export_symbolizer()
+{
+ using namespace boost::python;
+
+ //implicitly_convertible<mapnik::value_bool, mapnik::symbolizer_base::value_type>();
+ implicitly_convertible<mapnik::value_integer, mapnik::symbolizer_base::value_type>();
+ implicitly_convertible<mapnik::value_double, mapnik::symbolizer_base::value_type>();
+ implicitly_convertible<std::string, mapnik::symbolizer_base::value_type>();
+ implicitly_convertible<mapnik::color, mapnik::symbolizer_base::value_type>();
+ implicitly_convertible<mapnik::expression_ptr, mapnik::symbolizer_base::value_type>();
+ implicitly_convertible<mapnik::enumeration_wrapper, mapnik::symbolizer_base::value_type>();
+ implicitly_convertible<std::shared_ptr<mapnik::group_symbolizer_properties>, mapnik::symbolizer_base::value_type>();
+
+ enum_<mapnik::keys>("keys")
+ .value("gamma", mapnik::keys::gamma)
+ .value("gamma_method",mapnik::keys::gamma_method)
+ ;
+
+ class_<symbolizer>("Symbolizer",no_init)
+ .def("type",get_symbolizer_type)
+ .def("__hash__",hash_impl)
+ .def("extract", extract_underlying_type)
+ ;
+
+ class_<symbolizer_base::value_type>("NumericWrapper")
+ .def("__init__", make_constructor(numeric_wrapper))
+ ;
+
+ class_<symbolizer_base>("SymbolizerBase",no_init)
+ .def("__setitem__",&__setitem__)
+ .def("__setattr__",&__setitem__)
+ .def("__getitem__",&__getitem__)
+ .def("__getattr__",&__getitem__)
+ //.def("__str__", &__str__)
+ .def(self == self) // __eq__
+ ;
+}
+
+
+void export_shield_symbolizer()
+{
+ using namespace boost::python;
+ class_< shield_symbolizer, bases<text_symbolizer> >("ShieldSymbolizer",
+ init<>("Default ctor"))
+ .def("__hash__",hash_impl_2<shield_symbolizer>)
+ ;
+
+}
+
+void export_polygon_symbolizer()
+{
+ using namespace boost::python;
+
+ class_<polygon_symbolizer, bases<symbolizer_base> >("PolygonSymbolizer",
+ init<>("Default ctor"))
+ .def("__hash__",hash_impl_2<polygon_symbolizer>)
+ ;
+
+}
+
+void export_polygon_pattern_symbolizer()
+{
+ using namespace boost::python;
+
+ mapnik::enumeration_<mapnik::pattern_alignment_e>("pattern_alignment")
+ .value("LOCAL",mapnik::LOCAL_ALIGNMENT)
+ .value("GLOBAL",mapnik::GLOBAL_ALIGNMENT)
+ ;
+
+ class_<polygon_pattern_symbolizer>("PolygonPatternSymbolizer",
+ init<>("Default ctor"))
+ .def("__hash__",hash_impl_2<polygon_pattern_symbolizer>)
+ ;
+}
+
+void export_raster_symbolizer()
+{
+ using namespace boost::python;
+
+ class_<raster_symbolizer, bases<symbolizer_base> >("RasterSymbolizer",
+ init<>("Default ctor"))
+ ;
+}
+
+void export_point_symbolizer()
+{
+ using namespace boost::python;
+
+ mapnik::enumeration_<mapnik::point_placement_e>("point_placement")
+ .value("CENTROID",mapnik::CENTROID_POINT_PLACEMENT)
+ .value("INTERIOR",mapnik::INTERIOR_POINT_PLACEMENT)
+ ;
+
+ class_<point_symbolizer, bases<symbolizer_base> >("PointSymbolizer",
+ init<>("Default Point Symbolizer - 4x4 black square"))
+ .def("__hash__",hash_impl_2<point_symbolizer>)
+ ;
+}
+
+void export_markers_symbolizer()
+{
+ using namespace boost::python;
+
+ mapnik::enumeration_<mapnik::marker_placement_e>("marker_placement")
+ .value("POINT_PLACEMENT",mapnik::MARKER_POINT_PLACEMENT)
+ .value("INTERIOR_PLACEMENT",mapnik::MARKER_INTERIOR_PLACEMENT)
+ .value("LINE_PLACEMENT",mapnik::MARKER_LINE_PLACEMENT)
+ ;
+
+ mapnik::enumeration_<mapnik::marker_multi_policy_e>("marker_multi_policy")
+ .value("EACH",mapnik::MARKER_EACH_MULTI)
+ .value("WHOLE",mapnik::MARKER_WHOLE_MULTI)
+ .value("LARGEST",mapnik::MARKER_LARGEST_MULTI)
+ ;
+
+ class_<markers_symbolizer, bases<symbolizer_base> >("MarkersSymbolizer",
+ init<>("Default Markers Symbolizer - circle"))
+ .def("__hash__",hash_impl_2<markers_symbolizer>)
+ ;
+}
+
+
+void export_line_symbolizer()
+{
+ using namespace boost::python;
+
+ mapnik::enumeration_<mapnik::line_rasterizer_e>("line_rasterizer")
+ .value("FULL",mapnik::RASTERIZER_FULL)
+ .value("FAST",mapnik::RASTERIZER_FAST)
+ ;
+
+ mapnik::enumeration_<mapnik::line_cap_e>("stroke_linecap",
+ "The possible values for a line cap used when drawing\n"
+ "with a stroke.\n")
+ .value("BUTT_CAP",mapnik::BUTT_CAP)
+ .value("SQUARE_CAP",mapnik::SQUARE_CAP)
+ .value("ROUND_CAP",mapnik::ROUND_CAP)
+ ;
+
+ mapnik::enumeration_<mapnik::line_join_e>("stroke_linejoin",
+ "The possible values for the line joining mode\n"
+ "when drawing with a stroke.\n")
+ .value("MITER_JOIN",mapnik::MITER_JOIN)
+ .value("MITER_REVERT_JOIN",mapnik::MITER_REVERT_JOIN)
+ .value("ROUND_JOIN",mapnik::ROUND_JOIN)
+ .value("BEVEL_JOIN",mapnik::BEVEL_JOIN)
+ ;
+
+
+ class_<line_symbolizer, bases<symbolizer_base> >("LineSymbolizer",
+ init<>("Default LineSymbolizer - 1px solid black"))
+ .def("__hash__",hash_impl_2<line_symbolizer>)
+ ;
+}
+
+void export_line_pattern_symbolizer()
+{
+ using namespace boost::python;
+
+ class_<line_pattern_symbolizer, bases<symbolizer_base> >("LinePatternSymbolizer",
+ init<> ("Default LinePatternSymbolizer"))
+ .def("__hash__",hash_impl_2<line_pattern_symbolizer>)
+ ;
+}
+
+void export_debug_symbolizer()
+{
+ using namespace boost::python;
+
+ mapnik::enumeration_<mapnik::debug_symbolizer_mode_e>("debug_symbolizer_mode")
+ .value("COLLISION",mapnik::DEBUG_SYM_MODE_COLLISION)
+ .value("VERTEX",mapnik::DEBUG_SYM_MODE_VERTEX)
+ ;
+
+ class_<debug_symbolizer, bases<symbolizer_base> >("DebugSymbolizer",
+ init<>("Default debug Symbolizer"))
+ .def("__hash__",hash_impl_2<debug_symbolizer>)
+ ;
+}
+
+void export_building_symbolizer()
+{
+ using namespace boost::python;
+
+ class_<building_symbolizer, bases<symbolizer_base> >("BuildingSymbolizer",
+ init<>("Default BuildingSymbolizer"))
+ .def("__hash__",hash_impl_2<building_symbolizer>)
+ ;
+
+}
+
+namespace {
+
+void group_symbolizer_properties_set_layout_simple(mapnik::group_symbolizer_properties &p,
+ mapnik::simple_row_layout &s)
+{
+ p.set_layout(s);
+}
+
+void group_symbolizer_properties_set_layout_pair(mapnik::group_symbolizer_properties &p,
+ mapnik::pair_layout &s)
+{
+ p.set_layout(s);
+}
+
+std::shared_ptr<mapnik::group_rule> group_rule_construct1(mapnik::expression_ptr p)
+{
+ return std::make_shared<mapnik::group_rule>(p, mapnik::expression_ptr());
+}
+
+} // anonymous namespace
+
+void export_group_symbolizer()
+{
+ using namespace boost::python;
+ using mapnik::group_rule;
+ using mapnik::simple_row_layout;
+ using mapnik::pair_layout;
+ using mapnik::group_symbolizer_properties;
+
+ class_<group_rule, std::shared_ptr<group_rule> >("GroupRule",
+ init<expression_ptr, expression_ptr>())
+ .def("__init__", boost::python::make_constructor(group_rule_construct1))
+ .def("append", &group_rule::append)
+ .def("set_filter", &group_rule::set_filter)
+ .def("set_repeat_key", &group_rule::set_repeat_key)
+ ;
+
+ class_<simple_row_layout>("SimpleRowLayout")
+ .def("item_margin", &simple_row_layout::get_item_margin)
+ .def("set_item_margin", &simple_row_layout::set_item_margin)
+ ;
+
+ class_<pair_layout>("PairLayout")
+ .def("item_margin", &simple_row_layout::get_item_margin)
+ .def("set_item_margin", &simple_row_layout::set_item_margin)
+ .def("max_difference", &pair_layout::get_max_difference)
+ .def("set_max_difference", &pair_layout::set_max_difference)
+ ;
+
+ class_<group_symbolizer_properties, std::shared_ptr<group_symbolizer_properties> >("GroupSymbolizerProperties")
+ .def("add_rule", &group_symbolizer_properties::add_rule)
+ .def("set_layout", &group_symbolizer_properties_set_layout_simple)
+ .def("set_layout", &group_symbolizer_properties_set_layout_pair)
+ ;
+
+ class_<group_symbolizer, bases<symbolizer_base> >("GroupSymbolizer",
+ init<>("Default GroupSymbolizer"))
+ .def("__hash__",hash_impl_2<group_symbolizer>)
+ ;
+
+}
diff --git a/src/mapnik_text_placement.cpp b/src/mapnik_text_placement.cpp
new file mode 100644
index 0000000..468a70f
--- /dev/null
+++ b/src/mapnik_text_placement.cpp
@@ -0,0 +1,587 @@
+/*****************************************************************************
+ *
+ * This file is part of Mapnik (c++ mapping toolkit)
+ *
+ * Copyright (C) 2015 Artem Pavlenko, Jean-Francois Doyon
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ *****************************************************************************/
+
+#include <mapnik/config.hpp>
+
+// boost
+#include "boost_std_shared_shim.hpp"
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#pragma GCC diagnostic ignored "-Wunused-local-typedef"
+#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
+
+#include <boost/python.hpp>
+#include <boost/python/stl_iterator.hpp>
+#include <boost/noncopyable.hpp>
+#pragma GCC diagnostic pop
+
+#include <mapnik/text/text_properties.hpp>
+#include <mapnik/text/placements/simple.hpp>
+#include <mapnik/text/placements/list.hpp>
+#include <mapnik/text/formatting/text.hpp>
+#include <mapnik/text/formatting/list.hpp>
+#include <mapnik/text/formatting/format.hpp>
+#include <mapnik/text/formatting/layout.hpp>
+#include <mapnik/text/text_layout.hpp>
+#include <mapnik/symbolizer.hpp>
+
+#include "mapnik_enumeration.hpp"
+#include "mapnik_threads.hpp"
+
+using namespace mapnik;
+
+/* Notes:
+ Overriding functions in inherited classes:
+ boost.python documentation doesn't really tell you how to do it.
+ But this helps:
+ http://www.gamedev.net/topic/446225-inheritance-in-boostpython/
+
+ register_ptr_to_python is required for wrapped classes, but not for unwrapped.
+
+ Functions don't have to be members of the class, but can also be
+ normal functions taking a ref to the class as first parameter.
+*/
+
+namespace {
+
+using namespace boost::python;
+
+// This class works around a feature in boost python.
+// See http://osdir.com/ml/python.c++/2003-11/msg00158.html
+
+template <typename T,
+ typename X1 = boost::python::detail::not_specified,
+ typename X2 = boost::python::detail::not_specified,
+ typename X3 = boost::python::detail::not_specified>
+class class_with_converter : public boost::python::class_<T, X1, X2, X3>
+{
+public:
+ using self = class_with_converter<T,X1,X2,X3>;
+ // Construct with the class name, with or without docstring, and default __init__() function
+ class_with_converter(char const* name, char const* doc = 0) : boost::python::class_<T, X1, X2, X3>(name, doc) { }
+
+ // Construct with class name, no docstring, and an uncallable __init__ function
+ class_with_converter(char const* name, boost::python::no_init_t y) : boost::python::class_<T, X1, X2, X3>(name, y) { }
+
+ // Construct with class name, docstring, and an uncallable __init__ function
+ class_with_converter(char const* name, char const* doc, boost::python::no_init_t y) : boost::python::class_<T, X1, X2, X3>(name, doc, y) { }
+
+ // Construct with class name and init<> function
+ template <class DerivedT> class_with_converter(char const* name, boost::python::init_base<DerivedT> const& i)
+ : boost::python::class_<T, X1, X2, X3>(name, i) { }
+
+ // Construct with class name, docstring and init<> function
+ template <class DerivedT>
+ inline class_with_converter(char const* name, char const* doc, boost::python::init_base<DerivedT> const& i)
+ : boost::python::class_<T, X1, X2, X3>(name, doc, i) { }
+
+ template <class D>
+ self& def_readwrite_convert(char const* name, D const& d, char const* /*doc*/=0)
+ {
+ this->add_property(name,
+ boost::python::make_getter(d, boost::python::return_value_policy<boost::python::return_by_value>()),
+ boost::python::make_setter(d, boost::python::default_call_policies()));
+ return *this;
+ }
+};
+
+/*
+boost::python::tuple get_displacement(text_layout_properties const& t)
+{
+ return boost::python::make_tuple(0.0,0.0);// FIXME t.displacement.x, t.displacement.y);
+}
+
+void set_displacement(text_layout_properties &t, boost::python::tuple arg)
+{
+ if (len(arg) != 2)
+ {
+ PyErr_SetObject(PyExc_ValueError,
+ ("expected 2-item tuple in call to set_displacement; got %s"
+ % arg).ptr()
+ );
+ throw_error_already_set();
+ }
+
+ //double x = extract<double>(arg[0]);
+ //double y = extract<double>(arg[1]);
+ //t.displacement.set(x, y); FIXME
+}
+
+
+struct NodeWrap
+ : formatting::node, wrapper<formatting::node>
+{
+ NodeWrap()
+ : formatting::node(), wrapper<formatting::node>() {}
+
+ void apply(evaluated_format_properties_ptr p, feature_impl const& feature, attributes const& vars, text_layout &output) const
+ {
+ python_block_auto_unblock b;
+ this->get_override("apply")(ptr(&p), ptr(&feature), ptr(&vars), ptr(&output));
+ }
+
+ virtual void add_expressions(expression_set &output) const
+ {
+ override o = this->get_override("add_expressions");
+ if (o)
+ {
+ python_block_auto_unblock b;
+ o(ptr(&output));
+ } else
+ {
+ formatting::node::add_expressions(output);
+ }
+ }
+
+ void default_add_expressions(expression_set &output) const
+ {
+ formatting::node::add_expressions(output);
+ }
+};
+*/
+/*
+struct TextNodeWrap
+ : formatting::text_node, wrapper<formatting::text_node>
+{
+ TextNodeWrap(expression_ptr expr)
+ : formatting::text_node(expr), wrapper<formatting::text_node>() {}
+
+ TextNodeWrap(std::string expr_text)
+ : formatting::text_node(expr_text), wrapper<formatting::text_node>() {}
+
+ virtual void apply(evaluated_format_properties_ptr p, feature_impl const& feature, attributes const& vars, text_layout &output) const
+ {
+ if(override o = this->get_override("apply"))
+ {
+ python_block_auto_unblock b;
+ o(ptr(&p), ptr(&feature), ptr(&vars), ptr(&output));
+ }
+ else
+ {
+ formatting::text_node::apply(p, feature, vars, output);
+ }
+ }
+
+ void default_apply(evaluated_format_properties_ptr p, feature_impl const& feature, attributes const& vars, text_layout &output) const
+ {
+ formatting::text_node::apply(p, feature, vars, output);
+ }
+};
+*/
+/*
+struct FormatNodeWrap
+ : formatting::format_node, wrapper<formatting::format_node>
+{
+ virtual void apply(evaluated_format_properties_ptr p, feature_impl const& feature, attributes const& vars, text_layout &output) const
+ {
+ if(override o = this->get_override("apply"))
+ {
+ python_block_auto_unblock b;
+ o(ptr(&p), ptr(&feature), ptr(&vars), ptr(&output));
+ }
+ else
+ {
+ formatting::format_node::apply(p, feature, vars ,output);
+ }
+ }
+
+ void default_apply(evaluated_format_properties_ptr p, feature_impl const& feature, attributes const& vars, text_layout &output) const
+ {
+ formatting::format_node::apply(p, feature, vars, output);
+ }
+};
+
+struct ExprFormatWrap: formatting::expression_format, wrapper<formatting::expression_format>
+{
+ virtual void apply(evaluated_format_properties_ptr p, feature_impl const& feature, attributes const& vars, text_layout &output) const
+ {
+ if(override o = this->get_override("apply"))
+ {
+ python_block_auto_unblock b;
+ o(ptr(&p), ptr(&feature), ptr(&vars), ptr(&output));
+ }
+ else
+ {
+ formatting::expression_format::apply(p, feature, vars, output);
+ }
+ }
+
+ void default_apply(evaluated_format_properties_ptr p, feature_impl const& feature, attributes const& vars, text_layout &output) const
+ {
+ formatting::expression_format::apply(p, feature, vars, output);
+ }
+};
+
+struct LayoutNodeWrap: formatting::layout_node, wrapper<formatting::layout_node>
+{
+ virtual void apply(evaluated_format_properties_ptr p, feature_impl const& feature, attributes const& vars, text_layout &output) const
+ {
+ if(override o = this->get_override("apply"))
+ {
+ python_block_auto_unblock b;
+ o(ptr(&p), ptr(&feature), ptr(&vars), ptr(&output));
+ }
+ else
+ {
+ formatting::layout_node::apply(p, feature, vars, output);
+ }
+ }
+
+ void default_apply(evaluated_format_properties_ptr p, feature_impl const& feature, attributes const& vars, text_layout &output) const
+ {
+ formatting::layout_node::apply(p, feature, vars, output);
+ }
+};
+
+struct ListNodeWrap: formatting::list_node, wrapper<formatting::list_node>
+{
+ //Default constructor
+ ListNodeWrap() : formatting::list_node(), wrapper<formatting::list_node>()
+ {
+ }
+
+ //Special constructor: Takes a python sequence as its argument
+ ListNodeWrap(object l) : formatting::list_node(), wrapper<formatting::list_node>()
+ {
+ stl_input_iterator<formatting::node_ptr> begin(l), end;
+ while (begin != end)
+ {
+ children_.push_back(*begin);
+ ++begin;
+ }
+ }
+
+ // TODO: Add constructor taking variable number of arguments.
+ http://wiki.python.org/moin/boost.python/HowTo#A.22Raw.22_function
+
+ virtual void apply(evaluated_format_properties_ptr p, feature_impl const& feature, attributes const& vars, text_layout &output) const
+ {
+ if(override o = this->get_override("apply"))
+ {
+ python_block_auto_unblock b;
+ o(ptr(&p), ptr(&feature), ptr(&vars), ptr(&output));
+ }
+ else
+ {
+ formatting::list_node::apply(p, feature, vars, output);
+ }
+ }
+
+ void default_apply(evaluated_format_properties_ptr p, feature_impl const& feature, attributes const& vars, text_layout &output) const
+ {
+ formatting::list_node::apply(p, feature, vars, output);
+ }
+
+ inline void IndexError(){
+ PyErr_SetString(PyExc_IndexError, "Index out of range");
+ throw_error_already_set();
+ }
+
+ unsigned get_length()
+ {
+ return children_.size();
+ }
+
+ formatting::node_ptr get_item(int i)
+ {
+ if (i < 0) i+= children_.size();
+ if (i < static_cast<int>(children_.size())) return children_[i];
+ IndexError();
+ return formatting::node_ptr(); //Avoid compiler warning
+ }
+
+ void set_item(int i, formatting::node_ptr ptr)
+ {
+ if (i < 0) i+= children_.size();
+ if (i < static_cast<int>(children_.size())) children_[i] = ptr;
+ IndexError();
+ }
+
+ void append(formatting::node_ptr ptr)
+ {
+ children_.push_back(ptr);
+ }
+};
+*/
+/*
+struct TextPlacementsWrap: text_placements, wrapper<text_placements>
+{
+ text_placement_info_ptr get_placement_info(double scale_factor_) const
+ {
+ python_block_auto_unblock b;
+ //return this->get_override("get_placement_info")();
+ return text_placement_info_ptr();
+ }
+};
+
+struct TextPlacementInfoWrap: text_placement_info, wrapper<text_placement_info>
+{
+ TextPlacementInfoWrap(text_placements const* parent,
+ double scale_factor_)
+ : text_placement_info(parent, scale_factor_)
+ {
+
+ }
+
+ bool next()
+ {
+ python_block_auto_unblock b;
+ return this->get_override("next")();
+ }
+};
+
+void insert_expression(expression_set *set, expression_ptr p)
+{
+ set->insert(p);
+}
+
+
+evaluated_format_properties_ptr get_format(text_symbolizer const& sym)
+{
+ return sym.get_placement_options()->defaults.format;
+}
+
+void set_format(text_symbolizer const& sym, evaluated_format_properties_ptr format)
+{
+ sym.get_placement_options()->defaults.format = format;
+}
+
+text_symbolizer_properties & get_properties(text_symbolizer const& sym)
+{
+ return sym.get_placement_options()->defaults;
+}
+
+void set_properties(text_symbolizer const& sym, text_symbolizer_properties & defaults)
+{
+ sym.get_placement_options()->defaults = defaults;
+}
+*/
+}
+
+void export_text_placement()
+{
+ /*
+ using namespace boost::python;
+
+ enumeration_<label_placement_e>("label_placement")
+ .value("LINE_PLACEMENT",LINE_PLACEMENT)
+ .value("POINT_PLACEMENT",POINT_PLACEMENT)
+ .value("VERTEX_PLACEMENT",VERTEX_PLACEMENT)
+ .value("INTERIOR_PLACEMENT",INTERIOR_PLACEMENT)
+ ;
+ enumeration_<vertical_alignment_e>("vertical_alignment")
+ .value("TOP",V_TOP)
+ .value("MIDDLE",V_MIDDLE)
+ .value("BOTTOM",V_BOTTOM)
+ .value("AUTO",V_AUTO)
+ ;
+
+ enumeration_<horizontal_alignment_e>("horizontal_alignment")
+ .value("LEFT",H_LEFT)
+ .value("MIDDLE",H_MIDDLE)
+ .value("RIGHT",H_RIGHT)
+ .value("AUTO",H_AUTO)
+ ;
+
+ enumeration_<justify_alignment_e>("justify_alignment")
+ .value("LEFT",J_LEFT)
+ .value("MIDDLE",J_MIDDLE)
+ .value("RIGHT",J_RIGHT)
+ .value("AUTO", J_AUTO)
+ ;
+
+ enumeration_<text_transform_e>("text_transform")
+ .value("NONE",NONE)
+ .value("UPPERCASE",UPPERCASE)
+ .value("LOWERCASE",LOWERCASE)
+ .value("CAPITALIZE",CAPITALIZE)
+ ;
+
+ enumeration_<halo_rasterizer_e>("halo_rasterizer")
+ .value("FULL",HALO_RASTERIZER_FULL)
+ .value("FAST",HALO_RASTERIZER_FAST)
+ ;
+ */
+ class_<text_symbolizer>("TextSymbolizer",
+ init<>())
+ ;
+ /*
+
+ class_with_converter<text_symbolizer_properties>
+ ("TextSymbolizerProperties")
+ .def_readwrite_convert("label_placement", &text_symbolizer_properties::label_placement)
+ .def_readwrite_convert("upright", &text_symbolizer_properties::upright)
+ .def_readwrite("label_spacing", &text_symbolizer_properties::label_spacing)
+ .def_readwrite("label_position_tolerance", &text_symbolizer_properties::label_position_tolerance)
+ .def_readwrite("avoid_edges", &text_symbolizer_properties::avoid_edges)
+ .def_readwrite("margin", &text_symbolizer_properties::margin)
+ .def_readwrite("repeat_distance", &text_symbolizer_properties::repeat_distance)
+ .def_readwrite("minimum_distance", &text_symbolizer_properties::minimum_distance)
+ .def_readwrite("minimum_padding", &text_symbolizer_properties::minimum_padding)
+ .def_readwrite("minimum_path_length", &text_symbolizer_properties::minimum_path_length)
+ .def_readwrite("maximum_angle_char_delta", &text_symbolizer_properties::max_char_angle_delta)
+ .def_readwrite("allow_overlap", &text_symbolizer_properties::allow_overlap)
+ .def_readwrite("largest_bbox_only", &text_symbolizer_properties::largest_bbox_only)
+ .def_readwrite("layout_defaults", &text_symbolizer_properties::layout_defaults)
+ //.def_readwrite("format", &text_symbolizer_properties::format)
+ .add_property ("format_tree",
+ &text_symbolizer_properties::format_tree,
+ &text_symbolizer_properties::set_format_tree);
+ //from_xml, to_xml operate on mapnik's internal XML tree and don't make sense in python.
+ // add_expressions isn't useful in python either. The result is only needed by
+ // attribute_collector (which isn't exposed in python) and
+ // it just calls add_expressions of the associated formatting tree.
+ // set_old_style expression is just a compatibility wrapper and doesn't need to be exposed in python.
+ ;
+
+ class_with_converter<text_layout_properties>
+ ("TextLayoutProperties")
+ .def_readwrite_convert("horizontal_alignment", &text_layout_properties::halign)
+ .def_readwrite_convert("justify_alignment", &text_layout_properties::jalign)
+ .def_readwrite_convert("vertical_alignment", &text_layout_properties::valign)
+ .def_readwrite("text_ratio", &text_layout_properties::text_ratio)
+ .def_readwrite("wrap_width", &text_layout_properties::wrap_width)
+ .def_readwrite("wrap_before", &text_layout_properties::wrap_before)
+ .def_readwrite("orientation", &text_layout_properties::orientation)
+ .def_readwrite("rotate_displacement", &text_layout_properties::rotate_displacement)
+ .add_property("displacement", &get_displacement, &set_displacement);
+
+ class_with_converter<detail::evaluated_format_properties>
+ ("CharProperties")
+ .def_readwrite_convert("text_transform", &detail::evaluated_format_properties::text_transform)
+ .def_readwrite_convert("fontset", &detail::evaluated_format_properties::fontset)
+ .def(init<detail::evaluated_format_properties const&>()) //Copy constructor
+ .def_readwrite("face_name", &detail::evaluated_format_properties::face_name)
+ .def_readwrite("text_size", &detail::evaluated_format_properties::text_size)
+ .def_readwrite("character_spacing", &detail::evaluated_format_properties::character_spacing)
+ .def_readwrite("line_spacing", &detail::evaluated_format_properties::line_spacing)
+ .def_readwrite("text_opacity", &detail::evaluated_format_properties::text_opacity)
+ .def_readwrite("fill", &detail::evaluated_format_properties::fill)
+ .def_readwrite("halo_fill", &detail::evaluated_format_properties::halo_fill)
+ .def_readwrite("halo_radius", &evaluated_format_properties::halo_radius)
+ //from_xml, to_xml operate on mapnik's internal XML tree and don't make sense in python.
+ ;
+ class_<TextPlacementsWrap,
+ std::shared_ptr<TextPlacementsWrap>,
+ boost::noncopyable>
+ ("TextPlacements")
+ .def_readwrite("defaults", &text_placements::defaults)
+ //.def("get_placement_info", pure_virtual(&text_placements::get_placement_info))
+ // TODO: add_expressions()
+ ;
+ register_ptr_to_python<std::shared_ptr<text_placements> >();
+
+ class_<TextPlacementInfoWrap,
+ std::shared_ptr<TextPlacementInfoWrap>,
+ boost::noncopyable>
+ ("TextPlacementInfo",
+ init<text_placements const*, double>())
+ .def("next", pure_virtual(&text_placement_info::next))
+ .def_readwrite("properties", &text_placement_info::properties)
+ .def_readwrite("scale_factor", &text_placement_info::scale_factor)
+ ;
+ register_ptr_to_python<std::shared_ptr<text_placement_info> >();
+
+
+ class_<expression_set,std::shared_ptr<expression_set>,
+ boost::noncopyable>("ExpressionSet")
+ .def("insert", &insert_expression);
+ ;
+
+ class_<formatting::node,std::shared_ptr<formatting::node>,
+ boost::noncopyable>("FormattingNode")
+ .def("apply", pure_virtual(&formatting::node::apply))
+ .def("add_expressions", pure_virtual(&formatting::node::add_expressions))
+ .def("to_xml", pure_virtual(&formatting::node::to_xml))
+ ;
+
+ register_ptr_to_python<std::shared_ptr<formatting::node> >();
+
+ class_<formatting::text_node,
+ std::shared_ptr<formatting::text_node>,
+ bases<formatting::node>,boost::noncopyable>("FormattingText", init<expression_ptr>())
+ .def(init<std::string>())
+ .def("apply", &formatting::text_node::apply)//, &TextNodeWrap::default_apply)
+ .add_property("text",&formatting::text_node::get_text, &formatting::text_node::set_text)
+ ;
+
+ register_ptr_to_python<std::shared_ptr<formatting::text_node> >();
+
+ class_with_converter<FormatNodeWrap,
+ std::shared_ptr<FormatNodeWrap>,
+ bases<formatting::node>,
+ boost::noncopyable>
+ ("FormattingFormat")
+ .def_readwrite_convert("text_size", &formatting::format_node::text_size)
+ .def_readwrite_convert("face_name", &formatting::format_node::face_name)
+ .def_readwrite_convert("character_spacing", &formatting::format_node::character_spacing)
+ .def_readwrite_convert("line_spacing", &formatting::format_node::line_spacing)
+ .def_readwrite_convert("text_opacity", &formatting::format_node::text_opacity)
+ .def_readwrite_convert("text_transform", &formatting::format_node::text_transform)
+ .def_readwrite_convert("fill", &formatting::format_node::fill)
+ .def_readwrite_convert("halo_fill", &formatting::format_node::halo_fill)
+ .def_readwrite_convert("halo_radius", &formatting::format_node::halo_radius)
+ .def("apply", &formatting::format_node::apply, &FormatNodeWrap::default_apply)
+ .add_property("child",
+ &formatting::format_node::get_child,
+ &formatting::format_node::set_child)
+ ;
+ register_ptr_to_python<std::shared_ptr<formatting::format_node> >();
+
+ class_<ListNodeWrap,
+ std::shared_ptr<ListNodeWrap>,
+ bases<formatting::node>,
+ boost::noncopyable>
+ ("FormattingList", init<>())
+ .def(init<list>())
+ .def("append", &formatting::list_node::push_back)
+ .def("apply", &formatting::list_node::apply, &ListNodeWrap::default_apply)
+ .def("__len__", &ListNodeWrap::get_length)
+ .def("__getitem__", &ListNodeWrap::get_item)
+ .def("__setitem__", &ListNodeWrap::set_item)
+ .def("append", &ListNodeWrap::append)
+ ;
+
+ register_ptr_to_python<std::shared_ptr<formatting::list_node> >();
+
+ class_<ExprFormatWrap,
+ std::shared_ptr<ExprFormatWrap>,
+ bases<formatting::node>,
+ boost::noncopyable>
+ ("FormattingExpressionFormat")
+ .def_readwrite("text_size", &formatting::expression_format::text_size)
+ .def_readwrite("face_name", &formatting::expression_format::face_name)
+ .def_readwrite("character_spacing", &formatting::expression_format::character_spacing)
+ .def_readwrite("line_spacing", &formatting::expression_format::line_spacing)
+ .def_readwrite("text_opacity", &formatting::expression_format::text_opacity)
+ .def_readwrite("fill", &formatting::expression_format::fill)
+ .def_readwrite("halo_fill", &formatting::expression_format::halo_fill)
+ .def_readwrite("halo_radius", &formatting::expression_format::halo_radius)
+ .def("apply", &formatting::expression_format::apply, &ExprFormatWrap::default_apply)
+ .add_property("child",
+ &formatting::expression_format::get_child,
+ &formatting::expression_format::set_child)
+ ;
+ register_ptr_to_python<std::shared_ptr<formatting::expression_format> >();
+*/
+ //TODO: registry
+}
diff --git a/src/mapnik_threads.hpp b/src/mapnik_threads.hpp
new file mode 100644
index 0000000..25b5587
--- /dev/null
+++ b/src/mapnik_threads.hpp
@@ -0,0 +1,109 @@
+/*****************************************************************************
+ *
+ * This file is part of Mapnik (c++ mapping toolkit)
+ *
+ * Copyright (C) 2015 Artem Pavlenko, Jean-Francois Doyon
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ *****************************************************************************/
+#ifndef MAPNIK_THREADS_HPP
+#define MAPNIK_THREADS_HPP
+
+#include <boost/thread/tss.hpp>
+#include <Python.h>
+
+namespace mapnik {
+class python_thread
+{
+ /* Docs:
+ http://docs.python.org/c-api/init.html#thread-state-and-the-global-interpreter-lock
+ */
+public:
+ static void unblock()
+ {
+#ifdef MAPNIK_DEBUG
+ if (state.get())
+ {
+ std::cerr << "ERROR: Python threads are already unblocked. "
+ "Unblocking again will loose the current state and "
+ "might crash later. Aborting!\n";
+ abort(); //This is a serious error and can't be handled in any other sane way
+ }
+#endif
+ PyThreadState *_save = 0; //Name defined by python
+ Py_UNBLOCK_THREADS;
+ state.reset(_save);
+#ifdef MAPNIK_DEBUG
+ if (!_save) {
+ thread_support = false;
+ }
+#endif
+ }
+
+ static void block()
+ {
+#ifdef MAPNIK_DEBUG
+ if (thread_support && !state.get())
+ {
+ std::cerr << "ERROR: Trying to restore python thread state, "
+ "but no state is saved. Can't continue and also "
+ "can't raise an exception because the python "
+ "interpreter might be non-function. Aborting!\n";
+ abort();
+ }
+#endif
+ PyThreadState *_save = state.release(); //Name defined by python
+ Py_BLOCK_THREADS;
+ }
+
+private:
+ static boost::thread_specific_ptr<PyThreadState> state;
+#ifdef MAPNIK_DEBUG
+ static bool thread_support;
+#endif
+};
+
+class python_block_auto_unblock
+{
+public:
+ python_block_auto_unblock()
+ {
+ python_thread::block();
+ }
+
+ ~python_block_auto_unblock()
+ {
+ python_thread::unblock();
+ }
+};
+
+class python_unblock_auto_block
+{
+public:
+ python_unblock_auto_block()
+ {
+ python_thread::unblock();
+ }
+
+ ~python_unblock_auto_block()
+ {
+ python_thread::block();
+ }
+};
+
+} //namespace
+
+#endif // MAPNIK_THREADS_HPP
diff --git a/src/mapnik_value_converter.hpp b/src/mapnik_value_converter.hpp
new file mode 100644
index 0000000..dbb9e87
--- /dev/null
+++ b/src/mapnik_value_converter.hpp
@@ -0,0 +1,90 @@
+/*****************************************************************************
+ *
+ * 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 MAPNIK_PYTHON_BINDING_VALUE_CONVERTER_INCLUDED
+#define MAPNIK_PYTHON_BINDING_VALUE_CONVERTER_INCLUDED
+
+// mapnik
+#include <mapnik/value.hpp>
+#include <mapnik/util/variant.hpp>
+// boost
+#include <boost/python.hpp>
+#include <boost/implicit_cast.hpp>
+
+namespace boost { namespace python {
+
+ struct value_converter
+ {
+ PyObject * operator() (mapnik::value_integer val) const
+ {
+ return ::PyLong_FromLongLong(val);
+ }
+
+ PyObject * operator() (mapnik::value_double val) const
+ {
+ return ::PyFloat_FromDouble(val);
+ }
+
+ PyObject * operator() (mapnik::value_bool val) const
+ {
+ return ::PyBool_FromLong(val);
+ }
+
+ PyObject * operator() (std::string const& s) const
+ {
+ return ::PyUnicode_DecodeUTF8(s.c_str(),implicit_cast<ssize_t>(s.length()),0);
+ }
+
+ PyObject * operator() (mapnik::value_unicode_string const& s) const
+ {
+ std::string buffer;
+ mapnik::to_utf8(s,buffer);
+ return ::PyUnicode_DecodeUTF8(buffer.c_str(),implicit_cast<ssize_t>(buffer.length()),0);
+ }
+
+ PyObject * operator() (mapnik::value_null const& /*s*/) const
+ {
+ Py_RETURN_NONE;
+ }
+ };
+
+
+ struct mapnik_value_to_python
+ {
+ static PyObject* convert(mapnik::value const& v)
+ {
+ return mapnik::util::apply_visitor(value_converter(),v);
+ }
+
+ };
+
+ struct mapnik_param_to_python
+ {
+ static PyObject* convert(mapnik::value_holder const& v)
+ {
+ return mapnik::util::apply_visitor(value_converter(),v);
+ }
+ };
+
+
+}}
+
+#endif // MAPNIK_PYTHON_BINDING_VALUE_CONVERTER_INCLUDED
diff --git a/src/mapnik_view_transform.cpp b/src/mapnik_view_transform.cpp
new file mode 100644
index 0000000..ee81914
--- /dev/null
+++ b/src/mapnik_view_transform.cpp
@@ -0,0 +1,92 @@
+/*****************************************************************************
+ *
+ * This file is part of Mapnik (c++ mapping toolkit)
+ *
+ * Copyright (C) 2015 Artem Pavlenko, Jean-Francois Doyon
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ *****************************************************************************/
+
+#include <mapnik/config.hpp>
+
+// boost
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#pragma GCC diagnostic ignored "-Wunused-local-typedef"
+#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
+
+#include <boost/python.hpp>
+#pragma GCC diagnostic pop
+
+// mapnik
+#include <mapnik/view_transform.hpp>
+
+using mapnik::view_transform;
+
+struct view_transform_pickle_suite : boost::python::pickle_suite
+{
+ static boost::python::tuple
+ getinitargs(const view_transform& c)
+ {
+ using namespace boost::python;
+ return boost::python::make_tuple(c.width(),c.height(),c.extent());
+ }
+};
+
+namespace {
+
+mapnik::coord2d forward_point(mapnik::view_transform const& t, mapnik::coord2d const& in)
+{
+ mapnik::coord2d out(in);
+ t.forward(out);
+ return out;
+}
+
+mapnik::coord2d backward_point(mapnik::view_transform const& t, mapnik::coord2d const& in)
+{
+ mapnik::coord2d out(in);
+ t.backward(out);
+ return out;
+}
+
+mapnik::box2d<double> forward_envelope(mapnik::view_transform const& t, mapnik::box2d<double> const& in)
+{
+ return t.forward(in);
+}
+
+mapnik::box2d<double> backward_envelope(mapnik::view_transform const& t, mapnik::box2d<double> const& in)
+{
+ return t.backward(in);
+}
+}
+
+void export_view_transform()
+{
+ using namespace boost::python;
+ using mapnik::box2d;
+ using mapnik::coord2d;
+
+ class_<view_transform>("ViewTransform",init<int,int,box2d<double> const& > (
+ "Create a ViewTransform with a width and height as integers and extent"))
+ .def_pickle(view_transform_pickle_suite())
+ .def("forward", forward_point)
+ .def("backward",backward_point)
+ .def("forward", forward_envelope)
+ .def("backward",backward_envelope)
+ .def("scale_x",&view_transform::scale_x)
+ .def("scale_y",&view_transform::scale_y)
+ ;
+}
diff --git a/src/python_grid_utils.cpp b/src/python_grid_utils.cpp
new file mode 100644
index 0000000..ec4c321
--- /dev/null
+++ b/src/python_grid_utils.cpp
@@ -0,0 +1,405 @@
+/*****************************************************************************
+ *
+ * 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
+ *
+ *****************************************************************************/
+
+#if defined(GRID_RENDERER)
+
+#include <mapnik/config.hpp>
+
+// boost
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#pragma GCC diagnostic ignored "-Wunused-local-typedef"
+#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
+
+#include <boost/python.hpp>
+#pragma GCC diagnostic pop
+
+// mapnik
+#include <mapnik/map.hpp>
+#include <mapnik/layer.hpp>
+#include <mapnik/debug.hpp>
+#include <mapnik/grid/grid_renderer.hpp>
+#include <mapnik/grid/grid.hpp>
+#include <mapnik/grid/grid_util.hpp>
+#include <mapnik/grid/grid_view.hpp>
+#include <mapnik/value_error.hpp>
+#include <mapnik/feature.hpp>
+#include <mapnik/feature_kv_iterator.hpp>
+#include "python_grid_utils.hpp"
+
+// stl
+#include <stdexcept>
+
+namespace mapnik {
+
+
+template <typename T>
+void grid2utf(T const& grid_type,
+ boost::python::list& l,
+ std::vector<typename T::lookup_type>& key_order)
+{
+ using keys_type = std::map< typename T::lookup_type, typename T::value_type>;
+ using keys_iterator = typename keys_type::iterator;
+
+ typename T::data_type const& data = grid_type.data();
+ typename T::feature_key_type const& feature_keys = grid_type.get_feature_keys();
+ typename T::feature_key_type::const_iterator feature_pos;
+
+ keys_type keys;
+ // start counting at utf8 codepoint 32, aka space character
+ std::uint16_t codepoint = 32;
+
+ unsigned array_size = data.width();
+ for (unsigned y = 0; y < data.height(); ++y)
+ {
+ std::uint16_t idx = 0;
+ const std::unique_ptr<Py_UNICODE[]> line(new Py_UNICODE[array_size]);
+ typename T::value_type const* row = data.get_row(y);
+ for (unsigned x = 0; x < data.width(); ++x)
+ {
+ typename T::value_type feature_id = row[x];
+ feature_pos = feature_keys.find(feature_id);
+ if (feature_pos != feature_keys.end())
+ {
+ mapnik::grid::lookup_type val = feature_pos->second;
+ keys_iterator key_pos = keys.find(val);
+ if (key_pos == keys.end())
+ {
+ // Create a new entry for this key. Skip the codepoints that
+ // can't be encoded directly in JSON.
+ if (codepoint == 34) ++codepoint; // Skip "
+ else if (codepoint == 92) ++codepoint; // Skip backslash
+ if (feature_id == mapnik::grid::base_mask)
+ {
+ keys[""] = codepoint;
+ key_order.push_back("");
+ }
+ else
+ {
+ keys[val] = codepoint;
+ key_order.push_back(val);
+ }
+ line[idx++] = static_cast<Py_UNICODE>(codepoint);
+ ++codepoint;
+ }
+ else
+ {
+ line[idx++] = static_cast<Py_UNICODE>(key_pos->second);
+ }
+ }
+ // else, shouldn't get here...
+ }
+ l.append(boost::python::object(
+ boost::python::handle<>(
+ PyUnicode_FromUnicode(line.get(), array_size))));
+ }
+}
+
+
+template <typename T>
+void grid2utf(T const& grid_type,
+ boost::python::list& l,
+ std::vector<typename T::lookup_type>& key_order,
+ unsigned int resolution)
+{
+ using keys_type = std::map< typename T::lookup_type, typename T::value_type>;
+ using keys_iterator = typename keys_type::iterator;
+
+ typename T::feature_key_type const& feature_keys = grid_type.get_feature_keys();
+ typename T::feature_key_type::const_iterator feature_pos;
+
+ keys_type keys;
+ // start counting at utf8 codepoint 32, aka space character
+ std::uint16_t codepoint = 32;
+
+ unsigned array_size = std::ceil(grid_type.width()/static_cast<float>(resolution));
+ for (unsigned y = 0; y < grid_type.height(); y=y+resolution)
+ {
+ std::uint16_t idx = 0;
+ const std::unique_ptr<Py_UNICODE[]> line(new Py_UNICODE[array_size]);
+ mapnik::grid::value_type const* row = grid_type.get_row(y);
+ for (unsigned x = 0; x < grid_type.width(); x=x+resolution)
+ {
+ typename T::value_type feature_id = row[x];
+ feature_pos = feature_keys.find(feature_id);
+ if (feature_pos != feature_keys.end())
+ {
+ mapnik::grid::lookup_type val = feature_pos->second;
+ keys_iterator key_pos = keys.find(val);
+ if (key_pos == keys.end())
+ {
+ // Create a new entry for this key. Skip the codepoints that
+ // can't be encoded directly in JSON.
+ if (codepoint == 34) ++codepoint; // Skip "
+ else if (codepoint == 92) ++codepoint; // Skip backslash
+ if (feature_id == mapnik::grid::base_mask)
+ {
+ keys[""] = codepoint;
+ key_order.push_back("");
+ }
+ else
+ {
+ keys[val] = codepoint;
+ key_order.push_back(val);
+ }
+ line[idx++] = static_cast<Py_UNICODE>(codepoint);
+ ++codepoint;
+ }
+ else
+ {
+ line[idx++] = static_cast<Py_UNICODE>(key_pos->second);
+ }
+ }
+ // else, shouldn't get here...
+ }
+ l.append(boost::python::object(
+ boost::python::handle<>(
+ PyUnicode_FromUnicode(line.get(), array_size))));
+ }
+}
+
+
+template <typename T>
+void grid2utf2(T const& grid_type,
+ boost::python::list& l,
+ std::vector<typename T::lookup_type>& key_order,
+ unsigned int resolution)
+{
+ using keys_type = std::map< typename T::lookup_type, typename T::value_type>;
+ using keys_iterator = typename keys_type::iterator;
+
+ typename T::data_type const& data = grid_type.data();
+ typename T::feature_key_type const& feature_keys = grid_type.get_feature_keys();
+ typename T::feature_key_type::const_iterator feature_pos;
+
+ keys_type keys;
+ // start counting at utf8 codepoint 32, aka space character
+ uint16_t codepoint = 32;
+
+ mapnik::grid::data_type target(data.width()/resolution,data.height()/resolution);
+ mapnik::scale_grid(target,grid_type.data(),0.0,0.0);
+
+ unsigned array_size = target.width();
+ for (unsigned y = 0; y < target.height(); ++y)
+ {
+ uint16_t idx = 0;
+ const std::unique_ptr<Py_UNICODE[]> line(new Py_UNICODE[array_size]);
+ mapnik::grid::value_type * row = target.get_row(y);
+ unsigned x;
+ for (x = 0; x < target.width(); ++x)
+ {
+ feature_pos = feature_keys.find(row[x]);
+ if (feature_pos != feature_keys.end())
+ {
+ mapnik::grid::lookup_type val = feature_pos->second;
+ keys_iterator key_pos = keys.find(val);
+ if (key_pos == keys.end())
+ {
+ // Create a new entry for this key. Skip the codepoints that
+ // can't be encoded directly in JSON.
+ if (codepoint == 34) ++codepoint; // Skip "
+ else if (codepoint == 92) ++codepoint; // Skip backslash
+ keys[val] = codepoint;
+ key_order.push_back(val);
+ line[idx++] = static_cast<Py_UNICODE>(codepoint);
+ ++codepoint;
+ }
+ else
+ {
+ line[idx++] = static_cast<Py_UNICODE>(key_pos->second);
+ }
+ }
+ // else, shouldn't get here...
+ }
+ l.append(boost::python::object(
+ boost::python::handle<>(
+ PyUnicode_FromUnicode(line.get(), array_size))));
+ }
+}
+
+
+template <typename T>
+void write_features(T const& grid_type,
+ boost::python::dict& feature_data,
+ std::vector<typename T::lookup_type> const& key_order)
+{
+ typename T::feature_type const& g_features = grid_type.get_grid_features();
+ if (g_features.size() <= 0)
+ {
+ return;
+ }
+
+ std::set<std::string> const& attributes = grid_type.get_fields();
+ typename T::feature_type::const_iterator feat_end = g_features.end();
+ for ( std::string const& key_item :key_order )
+ {
+ if (key_item.empty())
+ {
+ continue;
+ }
+
+ typename T::feature_type::const_iterator feat_itr = g_features.find(key_item);
+ if (feat_itr == feat_end)
+ {
+ continue;
+ }
+
+ bool found = false;
+ boost::python::dict feat;
+ mapnik::feature_ptr feature = feat_itr->second;
+ for ( std::string const& attr : attributes )
+ {
+ if (attr == "__id__")
+ {
+ feat[attr.c_str()] = feature->id();
+ }
+ else if (feature->has_key(attr))
+ {
+ found = true;
+ feat[attr.c_str()] = feature->get(attr);
+ }
+ }
+
+ if (found)
+ {
+ feature_data[feat_itr->first] = feat;
+ }
+ }
+}
+
+template <typename T>
+void grid_encode_utf(T const& grid_type,
+ boost::python::dict & json,
+ bool add_features,
+ unsigned int resolution)
+{
+ // convert buffer to utf and gather key order
+ boost::python::list l;
+ std::vector<typename T::lookup_type> key_order;
+
+ if (resolution != 1) {
+ // resample on the fly - faster, less accurate
+ mapnik::grid2utf<T>(grid_type,l,key_order,resolution);
+
+ // resample first - slower, more accurate
+ //mapnik::grid2utf2<T>(grid_type,l,key_order,resolution);
+ }
+ else
+ {
+ mapnik::grid2utf<T>(grid_type,l,key_order);
+ }
+
+ // convert key order to proper python list
+ boost::python::list keys_a;
+ for ( typename T::lookup_type const& key_id : key_order )
+ {
+ keys_a.append(key_id);
+ }
+
+ // gather feature data
+ boost::python::dict feature_data;
+ if (add_features) {
+ mapnik::write_features<T>(grid_type,feature_data,key_order);
+ }
+
+ json["grid"] = l;
+ json["keys"] = keys_a;
+ json["data"] = feature_data;
+
+}
+
+template <typename T>
+boost::python::dict grid_encode( T const& grid, std::string const& format, bool add_features, unsigned int resolution)
+{
+ if (format == "utf") {
+ boost::python::dict json;
+ grid_encode_utf<T>(grid,json,add_features,resolution);
+ return json;
+ }
+ else
+ {
+ std::stringstream s;
+ s << "'utf' is currently the only supported encoding format.";
+ throw mapnik::value_error(s.str());
+ }
+}
+
+template boost::python::dict grid_encode( mapnik::grid const& grid, std::string const& format, bool add_features, unsigned int resolution);
+template boost::python::dict grid_encode( mapnik::grid_view const& grid, std::string const& format, bool add_features, unsigned int resolution);
+
+void render_layer_for_grid(mapnik::Map const& map,
+ mapnik::grid & grid,
+ unsigned layer_idx,
+ boost::python::list const& fields,
+ double scale_factor,
+ unsigned offset_x,
+ unsigned offset_y)
+{
+ std::vector<mapnik::layer> const& layers = map.layers();
+ std::size_t layer_num = layers.size();
+ if (layer_idx >= layer_num) {
+ std::ostringstream s;
+ s << "Zero-based layer index '" << layer_idx << "' not valid, only '"
+ << layer_num << "' layers are in map\n";
+ throw std::runtime_error(s.str());
+ }
+
+ // convert python list to std::set
+ boost::python::ssize_t num_fields = boost::python::len(fields);
+ for(boost::python::ssize_t i=0; i<num_fields; i++) {
+ boost::python::extract<std::string> name(fields[i]);
+ if (name.check())
+ {
+ grid.add_field(name());
+ }
+ else
+ {
+ std::stringstream s;
+ s << "list of field names must be strings";
+ throw mapnik::value_error(s.str());
+ }
+ }
+
+ // copy field names
+ std::set<std::string> attributes = grid.get_fields();
+ // todo - make this a static constant
+ std::string known_id_key = "__id__";
+ if (attributes.find(known_id_key) != attributes.end())
+ {
+ attributes.erase(known_id_key);
+ }
+
+ std::string join_field = grid.get_key();
+ if (known_id_key != join_field &&
+ attributes.find(join_field) == attributes.end())
+ {
+ attributes.insert(join_field);
+ }
+
+ mapnik::grid_renderer<mapnik::grid> ren(map,grid,scale_factor,offset_x,offset_y);
+ mapnik::layer const& layer = layers[layer_idx];
+ ren.apply(layer,attributes);
+}
+
+}
+
+#endif
diff --git a/src/python_grid_utils.hpp b/src/python_grid_utils.hpp
new file mode 100644
index 0000000..a15a026
--- /dev/null
+++ b/src/python_grid_utils.hpp
@@ -0,0 +1,79 @@
+/*****************************************************************************
+ *
+ * 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 MAPNIK_PYTHON_BINDING_GRID_UTILS_INCLUDED
+#define MAPNIK_PYTHON_BINDING_GRID_UTILS_INCLUDED
+
+// boost
+#include <boost/python.hpp>
+
+// mapnik
+#include <mapnik/map.hpp>
+#include <mapnik/grid/grid.hpp>
+
+namespace mapnik {
+
+
+template <typename T>
+void grid2utf(T const& grid_type,
+ boost::python::list& l,
+ std::vector<typename T::lookup_type>& key_order);
+
+
+template <typename T>
+void grid2utf(T const& grid_type,
+ boost::python::list& l,
+ std::vector<typename T::lookup_type>& key_order,
+ unsigned int resolution);
+
+
+template <typename T>
+void grid2utf2(T const& grid_type,
+ boost::python::list& l,
+ std::vector<typename T::lookup_type>& key_order,
+ unsigned int resolution);
+
+
+template <typename T>
+void write_features(T const& grid_type,
+ boost::python::dict& feature_data,
+ std::vector<typename T::lookup_type> const& key_order);
+
+template <typename T>
+void grid_encode_utf(T const& grid_type,
+ boost::python::dict & json,
+ bool add_features,
+ unsigned int resolution);
+
+template <typename T>
+boost::python::dict grid_encode( T const& grid, std::string const& format, bool add_features, unsigned int resolution);
+
+void render_layer_for_grid(const mapnik::Map& map,
+ mapnik::grid& grid,
+ unsigned layer_idx, // TODO - layer by name or index
+ boost::python::list const& fields,
+ double scale_factor,
+ unsigned offset_x,
+ unsigned offset_y);
+
+}
+
+#endif // MAPNIK_PYTHON_BINDING_GRID_UTILS_INCLUDED
diff --git a/src/python_optional.hpp b/src/python_optional.hpp
new file mode 100644
index 0000000..45db528
--- /dev/null
+++ b/src/python_optional.hpp
@@ -0,0 +1,198 @@
+/*****************************************************************************
+ *
+ * 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
+ *
+ *****************************************************************************/
+
+#include <boost/optional/optional.hpp>
+#include <boost/python.hpp>
+
+#include <mapnik/util/noncopyable.hpp>
+
+// boost::optional<T> to/from converter from John Wiegley
+
+template <typename T, typename TfromPy>
+struct object_from_python
+{
+ object_from_python() {
+ boost::python::converter::registry::push_back
+ (&TfromPy::convertible, &TfromPy::construct,
+ boost::python::type_id<T>());
+ }
+};
+
+template <typename T, typename TtoPy, typename TfromPy>
+struct register_python_conversion
+{
+ register_python_conversion() {
+ boost::python::to_python_converter<T, TtoPy>();
+ object_from_python<T, TfromPy>();
+ }
+};
+
+template <typename T>
+struct python_optional : public mapnik::util::noncopyable
+{
+ struct optional_to_python
+ {
+ static PyObject * convert(const boost::optional<T>& value)
+ {
+ return (value ? boost::python::to_python_value<T>()(*value) :
+ boost::python::detail::none());
+ }
+ };
+
+ struct optional_from_python
+ {
+ static void * convertible(PyObject * source)
+ {
+ using namespace boost::python::converter;
+
+ if (source == Py_None)
+ return source;
+
+ const registration& converters(registered<T>::converters);
+
+ if (implicit_rvalue_convertible_from_python(source,
+ converters)) {
+ rvalue_from_python_stage1_data data =
+ rvalue_from_python_stage1(source, converters);
+ return rvalue_from_python_stage2(source, data, converters);
+ }
+ return 0;
+ }
+
+ static void construct(PyObject * source,
+ boost::python::converter::rvalue_from_python_stage1_data * data)
+ {
+ using namespace boost::python::converter;
+
+ void * const storage = ((rvalue_from_python_storage<T> *)
+ data)->storage.bytes;
+
+ if (data->convertible == source) // == None
+ new (storage) boost::optional<T>(); // A Boost uninitialized value
+ else
+ new (storage) boost::optional<T>(*static_cast<T *>(data->convertible));
+
+ data->convertible = storage;
+ }
+ };
+
+ explicit python_optional()
+ {
+ register_python_conversion<boost::optional<T>,
+ optional_to_python, optional_from_python>();
+ }
+};
+
+// to/from boost::optional<bool>
+template <>
+struct python_optional<float> : public mapnik::util::noncopyable
+{
+ struct optional_to_python
+ {
+ static PyObject * convert(const boost::optional<float>& value)
+ {
+ return (value ? PyFloat_FromDouble(*value) :
+ boost::python::detail::none());
+ }
+ };
+
+ struct optional_from_python
+ {
+ static void * convertible(PyObject * source)
+ {
+ using namespace boost::python::converter;
+
+ if (source == Py_None || PyFloat_Check(source))
+ return source;
+ return 0;
+ }
+
+ static void construct(PyObject * source,
+ boost::python::converter::rvalue_from_python_stage1_data * data)
+ {
+ using namespace boost::python::converter;
+ void * const storage = ((rvalue_from_python_storage<boost::optional<bool> > *)
+ data)->storage.bytes;
+ if (source == Py_None) // == None
+ new (storage) boost::optional<float>(); // A Boost uninitialized value
+ else
+ new (storage) boost::optional<float>(PyFloat_AsDouble(source));
+ data->convertible = storage;
+ }
+ };
+
+ explicit python_optional()
+ {
+ register_python_conversion<boost::optional<float>,
+ optional_to_python, optional_from_python>();
+ }
+};
+
+// to/from boost::optional<float>
+template <>
+struct python_optional<bool> : public mapnik::util::noncopyable
+{
+ struct optional_to_python
+ {
+ static PyObject * convert(const boost::optional<bool>& value)
+ {
+ if (value)
+ {
+ if (*value) Py_RETURN_TRUE;
+ else Py_RETURN_FALSE;
+ }
+ else return boost::python::detail::none();
+ }
+ };
+ struct optional_from_python
+ {
+ static void * convertible(PyObject * source)
+ {
+ using namespace boost::python::converter;
+
+ if (source == Py_None || PyBool_Check(source))
+ return source;
+ return 0;
+ }
+
+ static void construct(PyObject * source,
+ boost::python::converter::rvalue_from_python_stage1_data * data)
+ {
+ using namespace boost::python::converter;
+ void * const storage = ((rvalue_from_python_storage<boost::optional<bool> > *)
+ data)->storage.bytes;
+ if (source == Py_None) // == None
+ new (storage) boost::optional<bool>(); // A Boost uninitialized value
+ else
+ {
+ new (storage) boost::optional<bool>(source == Py_True ? true : false);
+ }
+ data->convertible = storage;
+ }
+ };
+
+ explicit python_optional()
+ {
+ register_python_conversion<boost::optional<bool>,
+ optional_to_python, optional_from_python>();
+ }
+};
diff --git a/src/python_to_value.hpp b/src/python_to_value.hpp
new file mode 100644
index 0000000..6ad9250
--- /dev/null
+++ b/src/python_to_value.hpp
@@ -0,0 +1,122 @@
+/*****************************************************************************
+ *
+ * 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 MAPNIK_PYTHON_BINDING_PYTHON_TO_VALUE
+#define MAPNIK_PYTHON_BINDING_PYTHON_TO_VALUE
+
+// boost
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#pragma GCC diagnostic ignored "-Wunused-local-typedef"
+#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
+#include <boost/python.hpp>
+#pragma GCC diagnostic pop
+
+// mapnik
+#include <mapnik/value.hpp>
+#include <mapnik/unicode.hpp>
+#include <mapnik/attribute.hpp>
+
+namespace mapnik {
+
+ static mapnik::attributes dict2attr(boost::python::dict const& d)
+ {
+ using namespace boost::python;
+ mapnik::attributes vars;
+ mapnik::transcoder tr_("utf8");
+ boost::python::list keys=d.keys();
+ for (int i=0; i < len(keys); ++i)
+ {
+ std::string key;
+ object obj_key = keys[i];
+ if (PyUnicode_Check(obj_key.ptr()))
+ {
+ PyObject* temp = PyUnicode_AsUTF8String(obj_key.ptr());
+ if (temp)
+ {
+ #if PY_VERSION_HEX >= 0x03000000
+ char* c_str = PyBytes_AsString(temp);
+ #else
+ char* c_str = PyString_AsString(temp);
+ #endif
+ key = c_str;
+ Py_DecRef(temp);
+ }
+ }
+ else
+ {
+ key = extract<std::string>(keys[i]);
+ }
+ object obj = d[key];
+ if (PyUnicode_Check(obj.ptr()))
+ {
+ PyObject* temp = PyUnicode_AsUTF8String(obj.ptr());
+ if (temp)
+ {
+ #if PY_VERSION_HEX >= 0x03000000
+ char* c_str = PyBytes_AsString(temp);
+ #else
+ char* c_str = PyString_AsString(temp);
+ #endif
+ vars[key] = tr_.transcode(c_str);
+ Py_DecRef(temp);
+ }
+ continue;
+ }
+
+ if (PyBool_Check(obj.ptr()))
+ {
+ extract<mapnik::value_bool> ex(obj);
+ if (ex.check())
+ {
+ vars[key] = ex();
+ }
+ }
+ else if (PyFloat_Check(obj.ptr()))
+ {
+ extract<mapnik::value_double> ex(obj);
+ if (ex.check())
+ {
+ vars[key] = ex();
+ }
+ }
+ else
+ {
+ extract<mapnik::value_integer> ex(obj);
+ if (ex.check())
+ {
+ vars[key] = ex();
+ }
+ else
+ {
+ extract<std::string> ex0(obj);
+ if (ex0.check())
+ {
+ vars[key] = tr_.transcode(ex0().c_str());
+ }
+ }
+ }
+ }
+ return vars;
+ }
+}
+
+#endif // MAPNIK_PYTHON_BINDING_PYTHON_TO_VALUE
diff --git a/test/python_tests/__init__.py b/test/python_tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/test/python_tests/agg_rasterizer_integer_overflow_test.py b/test/python_tests/agg_rasterizer_integer_overflow_test.py
new file mode 100644
index 0000000..bfd8128
--- /dev/null
+++ b/test/python_tests/agg_rasterizer_integer_overflow_test.py
@@ -0,0 +1,71 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from nose.tools import eq_
+from utilities import run_all
+import mapnik
+import json
+
+# geojson box of the world
+geojson = { "type": "Feature", "properties": { }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -17963313.143242701888084, -6300857.11560364998877 ], [ -17963313.143242701888084, 13071343.332991421222687 ], [ 7396658.353099936619401, 13071343.332991421222687 ], [ 7396658.353099936619401, -6300857.11560364998877 ], [ -17963313.143242701888084, -6300857.11560364998877 ] ] ] } }
+
+def test_that_coordinates_do_not_overflow_and_polygon_is_rendered_memory():
+ expected_color = mapnik.Color('white')
+ projection = '+init=epsg:4326'
+ ds = mapnik.MemoryDatasource()
+ context = mapnik.Context()
+ feat = mapnik.Feature.from_geojson(json.dumps(geojson),context)
+ ds.add_feature(feat)
+ s = mapnik.Style()
+ r = mapnik.Rule()
+ sym = mapnik.PolygonSymbolizer()
+ sym.fill = expected_color
+ r.symbols.append(sym)
+ s.rules.append(r)
+ lyr = mapnik.Layer('Layer',projection)
+ lyr.datasource = ds
+ lyr.styles.append('style')
+ m = mapnik.Map(256,256,projection)
+ m.background_color = mapnik.Color('green')
+ m.append_style('style',s)
+ m.layers.append(lyr)
+ # 17/20864/45265.png
+ m.zoom_to_box(mapnik.Box2d(-13658379.710221574,6197514.253362091,-13657768.213995293,6198125.749588372))
+ # works 15/5216/11316.png
+ #m.zoom_to_box(mapnik.Box2d(-13658379.710221574,6195679.764683247,-13655933.72531645,6198125.749588372))
+ im = mapnik.Image(256,256)
+ mapnik.render(m,im)
+ eq_(im.get_pixel(128,128),expected_color.packed())
+
+def test_that_coordinates_do_not_overflow_and_polygon_is_rendered_csv():
+ expected_color = mapnik.Color('white')
+ projection = '+init=epsg:4326'
+ ds = mapnik.MemoryDatasource()
+ context = mapnik.Context()
+ feat = mapnik.Feature.from_geojson(json.dumps(geojson),context)
+ ds.add_feature(feat)
+ geojson_string = "geojson\n'%s'" % json.dumps(geojson['geometry'])
+ ds = mapnik.Datasource(**{'type':'csv','inline':geojson_string})
+ s = mapnik.Style()
+ r = mapnik.Rule()
+ sym = mapnik.PolygonSymbolizer()
+ sym.fill = expected_color
+ r.symbols.append(sym)
+ s.rules.append(r)
+ lyr = mapnik.Layer('Layer',projection)
+ lyr.datasource = ds
+ lyr.styles.append('style')
+ m = mapnik.Map(256,256,projection)
+ m.background_color = mapnik.Color('green')
+ m.append_style('style',s)
+ m.layers.append(lyr)
+ # 17/20864/45265.png
+ m.zoom_to_box(mapnik.Box2d(-13658379.710221574,6197514.253362091,-13657768.213995293,6198125.749588372))
+ # works 15/5216/11316.png
+ #m.zoom_to_box(mapnik.Box2d(-13658379.710221574,6195679.764683247,-13655933.72531645,6198125.749588372))
+ im = mapnik.Image(256,256)
+ mapnik.render(m,im)
+ eq_(im.get_pixel(128,128),expected_color.packed())
+
+if __name__ == "__main__":
+ exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/box2d_test.py b/test/python_tests/box2d_test.py
new file mode 100644
index 0000000..c441002
--- /dev/null
+++ b/test/python_tests/box2d_test.py
@@ -0,0 +1,176 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from nose.tools import eq_,assert_true,assert_almost_equal,assert_false
+from utilities import run_all
+import mapnik
+
+def test_coord_init():
+ c = mapnik.Coord(100, 100)
+
+ eq_(c.x, 100)
+ eq_(c.y, 100)
+
+def test_coord_multiplication():
+ c = mapnik.Coord(100, 100)
+ c *= 2
+
+ eq_(c.x, 200)
+ eq_(c.y, 200)
+
+def test_envelope_init():
+ e = mapnik.Box2d(100, 100, 200, 200)
+
+ assert_true(e.contains(100, 100))
+ assert_true(e.contains(100, 200))
+ assert_true(e.contains(200, 200))
+ assert_true(e.contains(200, 100))
+
+ assert_true(e.contains(e.center()))
+
+ assert_false(e.contains(99.9, 99.9))
+ assert_false(e.contains(99.9, 200.1))
+ assert_false(e.contains(200.1, 200.1))
+ assert_false(e.contains(200.1, 99.9))
+
+ eq_(e.width(), 100)
+ eq_(e.height(), 100)
+
+ eq_(e.minx, 100)
+ eq_(e.miny, 100)
+
+ eq_(e.maxx, 200)
+ eq_(e.maxy, 200)
+
+ eq_(e[0],100)
+ eq_(e[1],100)
+ eq_(e[2],200)
+ eq_(e[3],200)
+ eq_(e[0],e[-4])
+ eq_(e[1],e[-3])
+ eq_(e[2],e[-2])
+ eq_(e[3],e[-1])
+
+ c = e.center()
+
+ eq_(c.x, 150)
+ eq_(c.y, 150)
+
+def test_envelope_static_init():
+ e = mapnik.Box2d.from_string('100 100 200 200')
+ e2 = mapnik.Box2d.from_string('100,100,200,200')
+ e3 = mapnik.Box2d.from_string('100 , 100 , 200 , 200')
+ eq_(e,e2)
+ eq_(e,e3)
+
+ assert_true(e.contains(100, 100))
+ assert_true(e.contains(100, 200))
+ assert_true(e.contains(200, 200))
+ assert_true(e.contains(200, 100))
+
+ assert_true(e.contains(e.center()))
+
+ assert_false(e.contains(99.9, 99.9))
+ assert_false(e.contains(99.9, 200.1))
+ assert_false(e.contains(200.1, 200.1))
+ assert_false(e.contains(200.1, 99.9))
+
+ eq_(e.width(), 100)
+ eq_(e.height(), 100)
+
+ eq_(e.minx, 100)
+ eq_(e.miny, 100)
+
+ eq_(e.maxx, 200)
+ eq_(e.maxy, 200)
+
+ eq_(e[0],100)
+ eq_(e[1],100)
+ eq_(e[2],200)
+ eq_(e[3],200)
+ eq_(e[0],e[-4])
+ eq_(e[1],e[-3])
+ eq_(e[2],e[-2])
+ eq_(e[3],e[-1])
+
+ c = e.center()
+
+ eq_(c.x, 150)
+ eq_(c.y, 150)
+
+def test_envelope_multiplication():
+ # no width then no impact of multiplication
+ a = mapnik.Box2d(100, 100, 100, 100)
+ a *= 5
+ eq_(a.minx,100)
+ eq_(a.miny,100)
+ eq_(a.maxx,100)
+ eq_(a.maxy,100)
+
+ a = mapnik.Box2d(100.0, 100.0, 100.0, 100.0)
+ a *= 5
+ eq_(a.minx,100)
+ eq_(a.miny,100)
+ eq_(a.maxx,100)
+ eq_(a.maxy,100)
+
+ a = mapnik.Box2d(100.0, 100.0, 100.001, 100.001)
+ a *= 5
+ assert_almost_equal(a.minx, 99.9979, places=3)
+ assert_almost_equal(a.miny, 99.9979, places=3)
+ assert_almost_equal(a.maxx, 100.0030, places=3)
+ assert_almost_equal(a.maxy, 100.0030, places=3)
+
+ e = mapnik.Box2d(100, 100, 200, 200)
+ e *= 2
+ eq_(e.minx,50)
+ eq_(e.miny,50)
+ eq_(e.maxx,250)
+ eq_(e.maxy,250)
+
+ assert_true(e.contains(50, 50))
+ assert_true(e.contains(50, 250))
+ assert_true(e.contains(250, 250))
+ assert_true(e.contains(250, 50))
+
+ assert_false(e.contains(49.9, 49.9))
+ assert_false(e.contains(49.9, 250.1))
+ assert_false(e.contains(250.1, 250.1))
+ assert_false(e.contains(250.1, 49.9))
+
+ assert_true(e.contains(e.center()))
+
+ eq_(e.width(), 200)
+ eq_(e.height(), 200)
+
+ eq_(e.minx, 50)
+ eq_(e.miny, 50)
+
+ eq_(e.maxx, 250)
+ eq_(e.maxy, 250)
+
+ c = e.center()
+
+ eq_(c.x, 150)
+ eq_(c.y, 150)
+
+def test_envelope_clipping():
+ e1 = mapnik.Box2d(-180,-90,180,90)
+ e2 = mapnik.Box2d(-120,40,-110,48)
+ e1.clip(e2)
+ eq_(e1,e2)
+
+ # madagascar in merc
+ e1 = mapnik.Box2d(4772116.5490, -2744395.0631, 5765186.4203, -1609458.0673)
+ e2 = mapnik.Box2d(5124338.3753, -2240522.1727, 5207501.8621, -2130452.8520)
+ e1.clip(e2)
+ eq_(e1,e2)
+
+ # nz in lon/lat
+ e1 = mapnik.Box2d(163.8062, -47.1897, 179.3628, -33.9069)
+ e2 = mapnik.Box2d(173.7378, -39.6395, 174.4849, -38.9252)
+ e1.clip(e2)
+ eq_(e1,e2)
+
+if __name__ == "__main__":
+ exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/buffer_clear_test.py b/test/python_tests/buffer_clear_test.py
new file mode 100644
index 0000000..b4b3bda
--- /dev/null
+++ b/test/python_tests/buffer_clear_test.py
@@ -0,0 +1,61 @@
+import os, mapnik
+from nose.tools import eq_
+from utilities import execution_path, run_all
+
+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_clearing_image_data():
+ im = mapnik.Image(256,256)
+ # make sure it equals itself
+ bytes = im.tostring()
+ eq_(im.tostring(),bytes)
+ # set background, then clear
+ im.fill(mapnik.Color('green'))
+ eq_(im.tostring()!=bytes,True)
+ # clear image, should now equal original
+ im.clear()
+ eq_(im.tostring(),bytes)
+
+def make_map():
+ ds = mapnik.MemoryDatasource()
+ context = mapnik.Context()
+ context.push('Name')
+ pixel_key = 1
+ f = mapnik.Feature(context,pixel_key)
+ f['Name'] = str(pixel_key)
+ f.geometry=mapnik.Geometry.from_wkt('POLYGON ((0 0, 0 256, 256 256, 256 0, 0 0))')
+ ds.add_feature(f)
+ s = mapnik.Style()
+ r = mapnik.Rule()
+ symb = mapnik.PolygonSymbolizer()
+ r.symbols.append(symb)
+ s.rules.append(r)
+ lyr = mapnik.Layer('Places')
+ lyr.datasource = ds
+ lyr.styles.append('places_labels')
+ width,height = 256,256
+ m = mapnik.Map(width,height)
+ m.append_style('places_labels',s)
+ m.layers.append(lyr)
+ m.zoom_all()
+ return m
+
+if mapnik.has_grid_renderer():
+ def test_clearing_grid_data():
+ g = mapnik.Grid(256,256)
+ utf = g.encode()
+ # make sure it equals itself
+ eq_(g.encode(),utf)
+ m = make_map()
+ mapnik.render_layer(m,g,layer=0,fields=['__id__','Name'])
+ eq_(g.encode()!=utf,True)
+ # clear grid, should now match original
+ g.clear()
+ eq_(g.encode(),utf)
+
+if __name__ == "__main__":
+ setup()
+ exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/cairo_test.py b/test/python_tests/cairo_test.py
new file mode 100644
index 0000000..3c626d4
--- /dev/null
+++ b/test/python_tests/cairo_test.py
@@ -0,0 +1,196 @@
+#!/usr/bin/env python
+
+import os
+import shutil
+import mapnik
+from nose.tools import eq_
+from utilities import execution_path, run_all
+
+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 make_tmp_map():
+ m = mapnik.Map(512,512)
+ m.background_color = mapnik.Color('steelblue')
+ ds = mapnik.MemoryDatasource()
+ context = mapnik.Context()
+ context.push('Name')
+ f = mapnik.Feature(context,1)
+ f['Name'] = 'Hello'
+ f.geometry = mapnik.Geometry.from_wkt('POINT (0 0)')
+ ds.add_feature(f)
+ s = mapnik.Style()
+ r = mapnik.Rule()
+ sym = mapnik.MarkersSymbolizer()
+ sym.allow_overlap = True
+ r.symbols.append(sym)
+ s.rules.append(r)
+ lyr = mapnik.Layer('Layer')
+ lyr.datasource = ds
+ lyr.styles.append('style')
+ m.append_style('style',s)
+ m.layers.append(lyr)
+ return m
+
+def draw_title(m,ctx,text,size=10,color=mapnik.Color('black')):
+ """ Draw a Map Title near the top of a page."""
+ middle = m.width/2.0
+ ctx.set_source_rgba(*cairo_color(color))
+ ctx.select_font_face("DejaVu Sans Book", cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL)
+ ctx.set_font_size(size)
+ x_bearing, y_bearing, width, height = ctx.text_extents(text)[:4]
+ ctx.move_to(middle - width / 2 - x_bearing, 20.0 - height / 2 - y_bearing)
+ ctx.show_text(text)
+
+def draw_neatline(m,ctx):
+ w,h = m.width, m.height
+ ctx.set_source_rgba(*cairo_color(mapnik.Color('black')))
+ outline = [
+ [0,0],[w,0],[w,h],[0,h]
+ ]
+ ctx.set_line_width(1)
+ for idx,pt in enumerate(outline):
+ if (idx == 0):
+ ctx.move_to(*pt)
+ else:
+ ctx.line_to(*pt)
+ ctx.close_path()
+ inset = 6
+ inline = [
+ [inset,inset],[w-inset,inset],[w-inset,h-inset],[inset,h-inset]
+ ]
+ ctx.set_line_width(inset/2)
+ for idx,pt in enumerate(inline):
+ if (idx == 0):
+ ctx.move_to(*pt)
+ else:
+ ctx.line_to(*pt)
+ ctx.close_path()
+ ctx.stroke()
+
+def cairo_color(c):
+ """ Return a Cairo color tuple from a Mapnik Color."""
+ ctx_c = (c.r/255.0,c.g/255.0,c.b/255.0,c.a/255.0)
+ return ctx_c
+
+if mapnik.has_pycairo():
+ import cairo
+
+ def test_passing_pycairo_context_svg():
+ m = make_tmp_map()
+ m.zoom_to_box(mapnik.Box2d(-180,-90,180,90))
+ test_cairo_file = '/tmp/mapnik-cairo-context-test.svg'
+ surface = cairo.SVGSurface(test_cairo_file, m.width, m.height)
+ expected_cairo_file = './images/pycairo/cairo-cairo-expected.svg'
+ context = cairo.Context(surface)
+ mapnik.render(m,context)
+ draw_title(m,context,"Hello Map",size=20)
+ draw_neatline(m,context)
+ surface.finish()
+ if not os.path.exists(expected_cairo_file) or os.environ.get('UPDATE'):
+ print 'generated expected cairo surface file %s' % expected_cairo_file
+ shutil.copy(test_cairo_file,expected_cairo_file)
+ diff = abs(os.stat(expected_cairo_file).st_size-os.stat(test_cairo_file).st_size)
+ msg = 'diff in size (%s) between actual (%s) and expected(%s)' % (diff,test_cairo_file,'tests/python_tests/'+ expected_cairo_file)
+ eq_( diff < 1500, True, msg)
+ os.remove(test_cairo_file)
+
+ def test_passing_pycairo_context_pdf():
+ m = make_tmp_map()
+ m.zoom_to_box(mapnik.Box2d(-180,-90,180,90))
+ test_cairo_file = '/tmp/mapnik-cairo-context-test.pdf'
+ surface = cairo.PDFSurface(test_cairo_file, m.width, m.height)
+ expected_cairo_file = './images/pycairo/cairo-cairo-expected.pdf'
+ context = cairo.Context(surface)
+ mapnik.render(m,context)
+ draw_title(m,context,"Hello Map",size=20)
+ draw_neatline(m,context)
+ surface.finish()
+ if not os.path.exists(expected_cairo_file) or os.environ.get('UPDATE'):
+ print 'generated expected cairo surface file %s' % expected_cairo_file
+ shutil.copy(test_cairo_file,expected_cairo_file)
+ diff = abs(os.stat(expected_cairo_file).st_size-os.stat(test_cairo_file).st_size)
+ msg = 'diff in size (%s) between actual (%s) and expected(%s)' % (diff,test_cairo_file,'tests/python_tests/'+ expected_cairo_file)
+ eq_( diff < 1500, True, msg)
+ os.remove(test_cairo_file)
+
+ def test_passing_pycairo_context_png():
+ m = make_tmp_map()
+ m.zoom_to_box(mapnik.Box2d(-180,-90,180,90))
+ test_cairo_file = '/tmp/mapnik-cairo-context-test.png'
+ surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, m.width, m.height)
+ expected_cairo_file = './images/pycairo/cairo-cairo-expected.png'
+ expected_cairo_file2 = './images/pycairo/cairo-cairo-expected-reduced.png'
+ context = cairo.Context(surface)
+ mapnik.render(m,context)
+ draw_title(m,context,"Hello Map",size=20)
+ draw_neatline(m,context)
+ surface.write_to_png(test_cairo_file)
+ reduced_color_image = test_cairo_file.replace('png','-mapnik.png')
+ im = mapnik.Image.from_cairo(surface)
+ im.save(reduced_color_image,'png8')
+ surface.finish()
+ if not os.path.exists(expected_cairo_file) or os.environ.get('UPDATE'):
+ print 'generated expected cairo surface file %s' % expected_cairo_file
+ shutil.copy(test_cairo_file,expected_cairo_file)
+ diff = abs(os.stat(expected_cairo_file).st_size-os.stat(test_cairo_file).st_size)
+ msg = 'diff in size (%s) between actual (%s) and expected(%s)' % (diff,test_cairo_file,'tests/python_tests/'+ expected_cairo_file)
+ eq_( diff < 500, True, msg)
+ os.remove(test_cairo_file)
+ if not os.path.exists(expected_cairo_file2) or os.environ.get('UPDATE'):
+ print 'generated expected cairo surface file %s' % expected_cairo_file2
+ shutil.copy(reduced_color_image,expected_cairo_file2)
+ diff = abs(os.stat(expected_cairo_file2).st_size-os.stat(reduced_color_image).st_size)
+ msg = 'diff in size (%s) between actual (%s) and expected(%s)' % (diff,reduced_color_image,'tests/python_tests/'+ expected_cairo_file2)
+ eq_( diff < 500, True, msg)
+ os.remove(reduced_color_image)
+
+ if 'sqlite' in mapnik.DatasourceCache.plugin_names():
+ def _pycairo_surface(type,sym):
+ test_cairo_file = '/tmp/mapnik-cairo-surface-test.%s.%s' % (sym,type)
+ expected_cairo_file = './images/pycairo/cairo-surface-expected.%s.%s' % (sym,type)
+ m = mapnik.Map(256,256)
+ mapnik.load_map(m,'../data/good_maps/%s_symbolizer.xml' % sym)
+ m.zoom_all()
+ if hasattr(cairo,'%sSurface' % type.upper()):
+ surface = getattr(cairo,'%sSurface' % type.upper())(test_cairo_file, m.width,m.height)
+ mapnik.render(m, surface)
+ surface.finish()
+ if not os.path.exists(expected_cairo_file) or os.environ.get('UPDATE'):
+ print 'generated expected cairo surface file %s' % expected_cairo_file
+ shutil.copy(test_cairo_file,expected_cairo_file)
+ diff = abs(os.stat(expected_cairo_file).st_size-os.stat(test_cairo_file).st_size)
+ msg = 'diff in size (%s) between actual (%s) and expected(%s)' % (diff,test_cairo_file,'tests/python_tests/'+ expected_cairo_file)
+ if os.uname()[0] == 'Darwin':
+ eq_( diff < 2100, True, msg)
+ else:
+ eq_( diff < 23000, True, msg)
+ os.remove(test_cairo_file)
+ return True
+ else:
+ print 'skipping cairo.%s test since surface is not available' % type.upper()
+ return True
+
+ def test_pycairo_svg_surface1():
+ eq_(_pycairo_surface('svg','point'),True)
+
+ def test_pycairo_svg_surface2():
+ eq_(_pycairo_surface('svg','building'),True)
+
+ def test_pycairo_svg_surface3():
+ eq_(_pycairo_surface('svg','polygon'),True)
+
+ def test_pycairo_pdf_surface1():
+ eq_(_pycairo_surface('pdf','point'),True)
+
+ def test_pycairo_pdf_surface2():
+ eq_(_pycairo_surface('pdf','building'),True)
+
+ def test_pycairo_pdf_surface3():
+ eq_(_pycairo_surface('pdf','polygon'),True)
+
+if __name__ == "__main__":
+ setup()
+ exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/color_test.py b/test/python_tests/color_test.py
new file mode 100644
index 0000000..900faf1
--- /dev/null
+++ b/test/python_tests/color_test.py
@@ -0,0 +1,115 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import sys
+import os, mapnik
+from timeit import Timer, time
+from nose.tools import *
+from utilities import execution_path, run_all, get_unique_colors
+
+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_color_init():
+ c = mapnik.Color(12, 128, 255)
+ eq_(c.r, 12)
+ eq_(c.g, 128)
+ eq_(c.b, 255)
+ eq_(c.a, 255)
+ eq_(False, c.get_premultiplied())
+ c = mapnik.Color(16, 32, 64, 128)
+ eq_(c.r, 16)
+ eq_(c.g, 32)
+ eq_(c.b, 64)
+ eq_(c.a, 128)
+ eq_(False, c.get_premultiplied())
+ c = mapnik.Color(16, 32, 64, 128,True)
+ eq_(c.r, 16)
+ eq_(c.g, 32)
+ eq_(c.b, 64)
+ eq_(c.a, 128)
+ eq_(True, c.get_premultiplied())
+ c = mapnik.Color('rgba(16,32,64,0.5)')
+ eq_(c.r, 16)
+ eq_(c.g, 32)
+ eq_(c.b, 64)
+ eq_(c.a, 128)
+ eq_(False, c.get_premultiplied())
+ c = mapnik.Color('rgba(16,32,64,0.5)', True)
+ eq_(c.r, 16)
+ eq_(c.g, 32)
+ eq_(c.b, 64)
+ eq_(c.a, 128)
+ eq_(True, c.get_premultiplied())
+ hex_str = '#10204080'
+ c = mapnik.Color(hex_str)
+ eq_(c.r, 16)
+ eq_(c.g, 32)
+ eq_(c.b, 64)
+ eq_(c.a, 128)
+ eq_(hex_str, c.to_hex_string())
+ eq_(False, c.get_premultiplied())
+ c = mapnik.Color(hex_str, True)
+ eq_(c.r, 16)
+ eq_(c.g, 32)
+ eq_(c.b, 64)
+ eq_(c.a, 128)
+ eq_(hex_str, c.to_hex_string())
+ eq_(True, c.get_premultiplied())
+ rgba_int = 2151686160
+ c = mapnik.Color(rgba_int)
+ eq_(c.r, 16)
+ eq_(c.g, 32)
+ eq_(c.b, 64)
+ eq_(c.a, 128)
+ eq_(rgba_int, c.packed())
+ eq_(False, c.get_premultiplied())
+ c = mapnik.Color(rgba_int, True)
+ eq_(c.r, 16)
+ eq_(c.g, 32)
+ eq_(c.b, 64)
+ eq_(c.a, 128)
+ eq_(rgba_int, c.packed())
+ eq_(True, c.get_premultiplied())
+
+def test_color_properties():
+ c = mapnik.Color(16, 32, 64, 128)
+ eq_(c.r, 16)
+ eq_(c.g, 32)
+ eq_(c.b, 64)
+ eq_(c.a, 128)
+ c.r = 17
+ eq_(c.r, 17)
+ c.g = 33
+ eq_(c.g, 33)
+ c.b = 65
+ eq_(c.b, 65)
+ c.a = 128
+ eq_(c.a, 128)
+
+def test_color_premultiply():
+ c = mapnik.Color(16, 33, 255, 128)
+ eq_(c.premultiply(), True)
+ eq_(c.r, 8)
+ eq_(c.g, 17)
+ eq_(c.b, 128)
+ eq_(c.a, 128)
+ # Repeating it again should do nothing
+ eq_(c.premultiply(), False)
+ eq_(c.r, 8)
+ eq_(c.g, 17)
+ eq_(c.b, 128)
+ eq_(c.a, 128)
+ c.demultiply()
+ c.demultiply()
+ # This will not return the same values as before but we expect that
+ eq_(c.r,15)
+ eq_(c.g,33)
+ eq_(c.b,255)
+ eq_(c.a,128)
+
+if __name__ == "__main__":
+ setup()
+ exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/compare_test.py b/test/python_tests/compare_test.py
new file mode 100644
index 0000000..f4b6563
--- /dev/null
+++ b/test/python_tests/compare_test.py
@@ -0,0 +1,112 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import os, mapnik
+from nose.tools import *
+from utilities import execution_path, run_all
+
+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_another_compare():
+ im = mapnik.Image(5,5)
+ im2 = mapnik.Image(5,5)
+ im2.fill(mapnik.Color('rgba(255,255,255,0)'))
+ eq_(im.compare(im2,16), im.width() * im.height())
+
+def test_compare_rgba8():
+ im = mapnik.Image(5,5,mapnik.ImageType.rgba8)
+ im.fill(mapnik.Color(0,0,0,0))
+ eq_(im.compare(im), 0)
+ im2 = mapnik.Image(5,5,mapnik.ImageType.rgba8)
+ im2.fill(mapnik.Color(0,0,0,0))
+ eq_(im.compare(im2), 0)
+ eq_(im2.compare(im), 0)
+ im2.fill(mapnik.Color(0,0,0,12))
+ eq_(im.compare(im2), 25)
+ eq_(im.compare(im2, 0, False), 0)
+ im3 = mapnik.Image(5,5,mapnik.ImageType.rgba8)
+ im3.set_pixel(0,0, mapnik.Color(0,0,0,0))
+ im3.set_pixel(0,1, mapnik.Color(1,1,1,1))
+ im3.set_pixel(1,0, mapnik.Color(2,2,2,2))
+ im3.set_pixel(1,1, mapnik.Color(3,3,3,3))
+ eq_(im.compare(im3), 3)
+ eq_(im.compare(im3,1),2)
+ eq_(im.compare(im3,2),1)
+ eq_(im.compare(im3,3),0)
+
+def test_compare_2_image():
+ im = mapnik.Image(5,5)
+ im.set_pixel(0,0, mapnik.Color(254, 254, 254, 254))
+ im.set_pixel(4,4, mapnik.Color('white'))
+ im2 = mapnik.Image(5,5)
+ eq_(im2.compare(im,16), 2)
+
+def test_compare_dimensions():
+ im = mapnik.Image(2,2)
+ im2 = mapnik.Image(3,3)
+ eq_(im.compare(im2), 4)
+ eq_(im2.compare(im), 9)
+
+def test_compare_gray8():
+ im = mapnik.Image(2,2,mapnik.ImageType.gray8)
+ im.fill(0)
+ eq_(im.compare(im), 0)
+ im2 = mapnik.Image(2,2,mapnik.ImageType.gray8)
+ im2.fill(0)
+ eq_(im.compare(im2), 0)
+ eq_(im2.compare(im), 0)
+ eq_(im.compare(im2, 0, False), 0)
+ im3 = mapnik.Image(2,2,mapnik.ImageType.gray8)
+ im3.set_pixel(0,0,0)
+ im3.set_pixel(0,1,1)
+ im3.set_pixel(1,0,2)
+ im3.set_pixel(1,1,3)
+ eq_(im.compare(im3),3)
+ eq_(im.compare(im3,1),2)
+ eq_(im.compare(im3,2),1)
+ eq_(im.compare(im3,3),0)
+
+def test_compare_gray16():
+ im = mapnik.Image(2,2,mapnik.ImageType.gray16)
+ im.fill(0)
+ eq_(im.compare(im), 0)
+ im2 = mapnik.Image(2,2,mapnik.ImageType.gray16)
+ im2.fill(0)
+ eq_(im.compare(im2), 0)
+ eq_(im2.compare(im), 0)
+ eq_(im.compare(im2, 0, False), 0)
+ im3 = mapnik.Image(2,2,mapnik.ImageType.gray16)
+ im3.set_pixel(0,0,0)
+ im3.set_pixel(0,1,1)
+ im3.set_pixel(1,0,2)
+ im3.set_pixel(1,1,3)
+ eq_(im.compare(im3),3)
+ eq_(im.compare(im3,1),2)
+ eq_(im.compare(im3,2),1)
+ eq_(im.compare(im3,3),0)
+
+def test_compare_gray32f():
+ im = mapnik.Image(2,2,mapnik.ImageType.gray32f)
+ im.fill(0.5)
+ eq_(im.compare(im), 0)
+ im2 = mapnik.Image(2,2,mapnik.ImageType.gray32f)
+ im2.fill(0.5)
+ eq_(im.compare(im2), 0)
+ eq_(im2.compare(im), 0)
+ eq_(im.compare(im2, 0, False), 0)
+ im3 = mapnik.Image(2,2,mapnik.ImageType.gray32f)
+ im3.set_pixel(0,0,0.5)
+ im3.set_pixel(0,1,1.5)
+ im3.set_pixel(1,0,2.5)
+ im3.set_pixel(1,1,3.5)
+ eq_(im.compare(im3),3)
+ eq_(im.compare(im3,1.0),2)
+ eq_(im.compare(im3,2.0),1)
+ eq_(im.compare(im3,3.0),0)
+
+if __name__ == "__main__":
+ setup()
+ exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/compositing_test.py b/test/python_tests/compositing_test.py
new file mode 100644
index 0000000..a0c8255
--- /dev/null
+++ b/test/python_tests/compositing_test.py
@@ -0,0 +1,258 @@
+#encoding: utf8
+
+from nose.tools import eq_
+import os
+from utilities import execution_path, run_all
+from utilities import get_unique_colors, pixel2channels, side_by_side_image
+import mapnik
+
+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 is_pre(color,alpha):
+ return (color*255.0/alpha) <= 255
+
+def debug_image(image,step=2):
+ for x in range(0,image.width(),step):
+ for y in range(0,image.height(),step):
+ pixel = image.get_pixel(x,y)
+ red,green,blue,alpha = pixel2channels(pixel)
+ print "rgba(%s,%s,%s,%s) at %s,%s" % (red,green,blue,alpha,x,y)
+
+def replace_style(m, name, style):
+ m.remove_style(name)
+ m.append_style(name, style)
+
+# note: it is impossible to know for all pixel colors
+# we can only detect likely cases of non premultiplied colors
+def validate_pixels_are_not_premultiplied(image):
+ over_alpha = False
+ transparent = True
+ fully_opaque = True
+ for x in range(0,image.width(),2):
+ for y in range(0,image.height(),2):
+ pixel = image.get_pixel(x,y)
+ red,green,blue,alpha = pixel2channels(pixel)
+ if alpha > 0:
+ transparent = False
+ if alpha < 255:
+ fully_opaque = False
+ color_max = max(red,green,blue)
+ if color_max > alpha:
+ over_alpha = True
+ return over_alpha or transparent or fully_opaque
+
+def validate_pixels_are_not_premultiplied2(image):
+ looks_not_multiplied = False
+ for x in range(0,image.width(),2):
+ for y in range(0,image.height(),2):
+ pixel = image.get_pixel(x,y)
+ red,green,blue,alpha = pixel2channels(pixel)
+ #each value of the color channels will never be bigger than that of the alpha channel.
+ if alpha > 0:
+ if red > 0 and red > alpha:
+ print 'red: %s, a: %s' % (red,alpha)
+ looks_not_multiplied = True
+ return looks_not_multiplied
+
+def validate_pixels_are_premultiplied(image):
+ bad_pixels = []
+ for x in range(0,image.width(),2):
+ for y in range(0,image.height(),2):
+ pixel = image.get_pixel(x,y)
+ red,green,blue,alpha = pixel2channels(pixel)
+ if alpha > 0:
+ pixel = image.get_pixel(x,y)
+ is_valid = ((0 <= red <= alpha) and is_pre(red,alpha)) \
+ and ((0 <= green <= alpha) and is_pre(green,alpha)) \
+ and ((0 <= blue <= alpha) and is_pre(blue,alpha)) \
+ and (alpha >= 0 and alpha <= 255)
+ if not is_valid:
+ bad_pixels.append("rgba(%s,%s,%s,%s) at %s,%s" % (red,green,blue,alpha,x,y))
+ num_bad = len(bad_pixels)
+ return (num_bad == 0,bad_pixels)
+
+def test_compare_images():
+ b = mapnik.Image.open('./images/support/b.png')
+ b.premultiply()
+ num_ops = len(mapnik.CompositeOp.names)
+ successes = []
+ fails = []
+ for name in mapnik.CompositeOp.names:
+ a = mapnik.Image.open('./images/support/a.png')
+ a.premultiply()
+ a.composite(b,getattr(mapnik.CompositeOp,name))
+ actual = '/tmp/mapnik-comp-op-test-' + name + '.png'
+ expected = 'images/composited/' + name + '.png'
+ valid = validate_pixels_are_premultiplied(a)
+ if not valid[0]:
+ fails.append('%s not validly premultiplied!:\n\t %s pixels (%s)' % (name,len(valid[1]),valid[1][0]))
+ a.demultiply()
+ if not validate_pixels_are_not_premultiplied(a):
+ fails.append('%s not validly demultiplied' % (name))
+ a.save(actual,'png32')
+ if not os.path.exists(expected) or os.environ.get('UPDATE'):
+ print 'generating expected test image: %s' % expected
+ a.save(expected,'png32')
+ expected_im = mapnik.Image.open(expected)
+ # compare them
+ if a.tostring('png32') == expected_im.tostring('png32'):
+ successes.append(name)
+ else:
+ fails.append('failed comparing actual (%s) and expected(%s)' % (actual,'tests/python_tests/'+ expected))
+ fail_im = side_by_side_image(expected_im, a)
+ fail_im.save('/tmp/mapnik-comp-op-test-' + name + '.fail.png','png32')
+ eq_(len(successes),num_ops,'\n'+'\n'.join(fails))
+ b.demultiply()
+ # b will be slightly modified by pre and then de multiplication rounding errors
+ # TODO - write test to ensure the image is 99% the same.
+ #expected_b = mapnik.Image.open('./images/support/b.png')
+ #b.save('/tmp/mapnik-comp-op-test-original-mask.png')
+ #eq_(b.tostring('png32'),expected_b.tostring('png32'), '/tmp/mapnik-comp-op-test-original-mask.png is no longer equivalent to original mask: ./images/support/b.png')
+
+def test_pre_multiply_status():
+ b = mapnik.Image.open('./images/support/b.png')
+ # not premultiplied yet, should appear that way
+ result = validate_pixels_are_not_premultiplied(b)
+ eq_(result,True)
+ # not yet premultiplied therefore should return false
+ result = validate_pixels_are_premultiplied(b)
+ eq_(result[0],False)
+ # now actually premultiply the pixels
+ b.premultiply()
+ # now checking if premultiplied should succeed
+ result = validate_pixels_are_premultiplied(b)
+ eq_(result[0],True)
+ # should now not appear to look not premultiplied
+ result = validate_pixels_are_not_premultiplied(b)
+ eq_(result,False)
+ # now actually demultiply the pixels
+ b.demultiply()
+ # should now appear demultiplied
+ result = validate_pixels_are_not_premultiplied(b)
+ eq_(result,True)
+
+def test_pre_multiply_status_of_map1():
+ m = mapnik.Map(256,256)
+ im = mapnik.Image(m.width,m.height)
+ eq_(validate_pixels_are_not_premultiplied(im),True)
+ mapnik.render(m,im)
+ eq_(validate_pixels_are_not_premultiplied(im),True)
+
+def test_pre_multiply_status_of_map2():
+ m = mapnik.Map(256,256)
+ m.background = mapnik.Color(1,1,1,255)
+ im = mapnik.Image(m.width,m.height)
+ eq_(validate_pixels_are_not_premultiplied(im),True)
+ mapnik.render(m,im)
+ eq_(validate_pixels_are_not_premultiplied(im),True)
+
+if 'shape' in mapnik.DatasourceCache.plugin_names():
+ def test_style_level_comp_op():
+ m = mapnik.Map(256, 256)
+ mapnik.load_map(m, '../data/good_maps/style_level_comp_op.xml')
+ m.zoom_all()
+ successes = []
+ fails = []
+ for name in mapnik.CompositeOp.names:
+ # find_style returns a copy of the style object
+ style_markers = m.find_style("markers")
+ style_markers.comp_op = getattr(mapnik.CompositeOp, name)
+ # replace the original style with the modified one
+ replace_style(m, "markers", style_markers)
+ im = mapnik.Image(m.width, m.height)
+ mapnik.render(m, im)
+ actual = '/tmp/mapnik-style-comp-op-' + name + '.png'
+ expected = 'images/style-comp-op/' + name + '.png'
+ im.save(actual,'png32')
+ if not os.path.exists(expected) or os.environ.get('UPDATE'):
+ print 'generating expected test image: %s' % expected
+ im.save(expected,'png32')
+ expected_im = mapnik.Image.open(expected)
+ # compare them
+ if im.tostring('png32') == expected_im.tostring('png32'):
+ successes.append(name)
+ else:
+ fails.append('failed comparing actual (%s) and expected(%s)' % (actual,'tests/python_tests/'+ expected))
+ fail_im = side_by_side_image(expected_im, im)
+ fail_im.save('/tmp/mapnik-style-comp-op-' + name + '.fail.png','png32')
+ eq_(len(fails), 0, '\n'+'\n'.join(fails))
+
+ def test_style_level_opacity():
+ m = mapnik.Map(512,512)
+ mapnik.load_map(m,'../data/good_maps/style_level_opacity_and_blur.xml')
+ m.zoom_all()
+ im = mapnik.Image(512,512)
+ mapnik.render(m,im)
+ actual = '/tmp/mapnik-style-level-opacity.png'
+ expected = 'images/support/mapnik-style-level-opacity.png'
+ im.save(actual,'png32')
+ expected_im = mapnik.Image.open(expected)
+ eq_(im.tostring('png32'),expected_im.tostring('png32'), 'failed comparing actual (%s) and expected (%s)' % (actual,'tests/python_tests/'+ expected))
+
+def test_rounding_and_color_expectations():
+ m = mapnik.Map(1,1)
+ m.background = mapnik.Color('rgba(255,255,255,.4999999)')
+ im = mapnik.Image(m.width,m.height)
+ mapnik.render(m,im)
+ eq_(get_unique_colors(im),['rgba(255,255,255,127)'])
+ m = mapnik.Map(1,1)
+ m.background = mapnik.Color('rgba(255,255,255,.5)')
+ im = mapnik.Image(m.width,m.height)
+ mapnik.render(m,im)
+ eq_(get_unique_colors(im),['rgba(255,255,255,128)'])
+ im_file = mapnik.Image.open('../data/images/stripes_pattern.png')
+ eq_(get_unique_colors(im_file),['rgba(0,0,0,0)', 'rgba(74,74,74,255)'])
+ # should have no effect
+ im_file.premultiply()
+ eq_(get_unique_colors(im_file),['rgba(0,0,0,0)', 'rgba(74,74,74,255)'])
+ im_file.apply_opacity(.5)
+ # should have effect now that image has transparency
+ im_file.premultiply()
+ eq_(get_unique_colors(im_file),['rgba(0,0,0,0)', 'rgba(37,37,37,127)'])
+ # should restore to original nonpremultiplied colors
+ im_file.demultiply()
+ eq_(get_unique_colors(im_file),['rgba(0,0,0,0)', 'rgba(74,74,74,127)'])
+
+
+def test_background_image_and_background_color():
+ m = mapnik.Map(8,8)
+ m.background = mapnik.Color('rgba(255,255,255,.5)')
+ m.background_image = '../data/images/stripes_pattern.png'
+ im = mapnik.Image(m.width,m.height)
+ mapnik.render(m,im)
+ eq_(get_unique_colors(im),['rgba(255,255,255,128)', 'rgba(74,74,74,255)'])
+
+def test_background_image_with_alpha_and_background_color():
+ m = mapnik.Map(10,10)
+ m.background = mapnik.Color('rgba(255,255,255,.5)')
+ m.background_image = '../data/images/yellow_half_trans.png'
+ im = mapnik.Image(m.width,m.height)
+ mapnik.render(m,im)
+ eq_(get_unique_colors(im),['rgba(255,255,85,191)'])
+
+def test_background_image_with_alpha_and_background_color_against_composited_control():
+ m = mapnik.Map(10,10)
+ m.background = mapnik.Color('rgba(255,255,255,.5)')
+ m.background_image = '../data/images/yellow_half_trans.png'
+ im = mapnik.Image(m.width,m.height)
+ mapnik.render(m,im)
+ # create and composite the expected result
+ im1 = mapnik.Image(10,10)
+ im1.fill(mapnik.Color('rgba(255,255,255,.5)'))
+ im1.premultiply()
+ im2 = mapnik.Image(10,10)
+ im2.fill(mapnik.Color('rgba(255,255,0,.5)'))
+ im2.premultiply()
+ im1.composite(im2)
+ im1.demultiply()
+ # compare image rendered (compositing in `agg_renderer<T>::setup`)
+ # vs image composited via python bindings
+ #raise Todo("looks like we need to investigate PNG color rounding when saving")
+ #eq_(get_unique_colors(im),get_unique_colors(im1))
+
+if __name__ == "__main__":
+ setup()
+ exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/copy_test.py b/test/python_tests/copy_test.py
new file mode 100644
index 0000000..d3cf9b1
--- /dev/null
+++ b/test/python_tests/copy_test.py
@@ -0,0 +1,93 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import os, mapnik
+from nose.tools import *
+from utilities import execution_path, run_all
+
+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_image_16_8_simple():
+ im = mapnik.Image(2,2,mapnik.ImageType.gray16)
+ im.set_pixel(0,0, 256)
+ im.set_pixel(0,1, 999)
+ im.set_pixel(1,0, 5)
+ im.set_pixel(1,1, 2)
+ im2 = im.copy(mapnik.ImageType.gray8)
+ eq_(im2.get_pixel(0,0), 255)
+ eq_(im2.get_pixel(0,1), 255)
+ eq_(im2.get_pixel(1,0), 5)
+ eq_(im2.get_pixel(1,1), 2)
+ # Cast back!
+ im = im2.copy(mapnik.ImageType.gray16)
+ eq_(im.get_pixel(0,0), 255)
+ eq_(im.get_pixel(0,1), 255)
+ eq_(im.get_pixel(1,0), 5)
+ eq_(im.get_pixel(1,1), 2)
+
+def test_image_32f_8_simple():
+ im = mapnik.Image(2,2,mapnik.ImageType.gray32f)
+ im.set_pixel(0,0, 120.1234)
+ im.set_pixel(0,1, -23.4)
+ im.set_pixel(1,0, 120.6)
+ im.set_pixel(1,1, 360.2)
+ im2 = im.copy(mapnik.ImageType.gray8)
+ eq_(im2.get_pixel(0,0), 120)
+ eq_(im2.get_pixel(0,1), 0)
+ eq_(im2.get_pixel(1,0), 120) # Notice this is truncated!
+ eq_(im2.get_pixel(1,1), 255)
+
+def test_image_offset_and_scale():
+ im = mapnik.Image(2,2,mapnik.ImageType.gray16)
+ eq_(im.offset, 0.0)
+ eq_(im.scaling, 1.0)
+ im.offset = 1.0
+ im.scaling = 2.0
+ eq_(im.offset, 1.0)
+ eq_(im.scaling, 2.0)
+
+def test_image_16_8_scale_and_offset():
+ im = mapnik.Image(2,2,mapnik.ImageType.gray16)
+ im.set_pixel(0,0, 256)
+ im.set_pixel(0,1, 258)
+ im.set_pixel(1,0, 99999)
+ im.set_pixel(1,1, 615)
+ offset = 255
+ scaling = 3
+ im2 = im.copy(mapnik.ImageType.gray8, offset, scaling)
+ eq_(im2.get_pixel(0,0), 0)
+ eq_(im2.get_pixel(0,1), 1)
+ eq_(im2.get_pixel(1,0), 255)
+ eq_(im2.get_pixel(1,1), 120)
+ # pixels will be a little off due to offsets in reverting!
+ im3 = im2.copy(mapnik.ImageType.gray16)
+ eq_(im3.get_pixel(0,0), 255) # Rounding error with ints
+ eq_(im3.get_pixel(0,1), 258) # same
+ eq_(im3.get_pixel(1,0), 1020) # The other one was way out of range for our scale/offset
+ eq_(im3.get_pixel(1,1), 615) # same
+
+def test_image_16_32f_scale_and_offset():
+ im = mapnik.Image(2,2,mapnik.ImageType.gray16)
+ im.set_pixel(0,0, 256)
+ im.set_pixel(0,1, 258)
+ im.set_pixel(1,0, 0)
+ im.set_pixel(1,1, 615)
+ offset = 255
+ scaling = 3.2
+ im2 = im.copy(mapnik.ImageType.gray32f, offset, scaling)
+ eq_(im2.get_pixel(0,0), 0.3125)
+ eq_(im2.get_pixel(0,1), 0.9375)
+ eq_(im2.get_pixel(1,0), -79.6875)
+ eq_(im2.get_pixel(1,1), 112.5)
+ im3 = im2.copy(mapnik.ImageType.gray16)
+ eq_(im3.get_pixel(0,0), 256)
+ eq_(im3.get_pixel(0,1), 258)
+ eq_(im3.get_pixel(1,0), 0)
+ eq_(im3.get_pixel(1,1), 615)
+
+if __name__ == "__main__":
+ setup()
+ exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/csv_test.py b/test/python_tests/csv_test.py
new file mode 100644
index 0000000..5011f57
--- /dev/null
+++ b/test/python_tests/csv_test.py
@@ -0,0 +1,604 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import glob
+from nose.tools import eq_,raises
+from utilities import execution_path
+
+import os, mapnik
+
+default_logging_severity = mapnik.logger.get_severity()
+
+def setup():
+ # make the tests silent since we intentially test error conditions that are noisy
+ mapnik.logger.set_severity(mapnik.severity_type.None)
+ # All of the paths used are relative, if we run the tests
+ # from another directory we need to chdir()
+ os.chdir(execution_path('.'))
+
+def teardown():
+ mapnik.logger.set_severity(default_logging_severity)
+
+if 'csv' in mapnik.DatasourceCache.plugin_names():
+
+ def get_csv_ds(filename):
+ return mapnik.Datasource(type='csv',file=os.path.join('../data/csv/',filename))
+
+ def test_broken_files(visual=False):
+ broken = glob.glob("../data/csv/fails/*.*")
+ broken.extend(glob.glob("../data/csv/warns/*.*"))
+
+ # Add a filename that doesn't exist
+ broken.append("../data/csv/fails/does_not_exist.csv")
+
+ for csv in broken:
+ if visual:
+ try:
+ mapnik.Datasource(type='csv',file=csv,strict=True)
+ print '\x1b[33mfailed: should have thrown\x1b[0m',csv
+ except Exception:
+ print '\x1b[1;32m✓ \x1b[0m', csv
+
+ def test_good_files(visual=False):
+ good_files = glob.glob("../data/csv/*.*")
+ good_files.extend(glob.glob("../data/csv/warns/*.*"))
+ ignorable = os.path.join('..','data','csv','long_lat.vrt')
+ good_files.remove(ignorable)
+
+ for csv in good_files:
+ if visual:
+ try:
+ mapnik.Datasource(type='csv',file=csv)
+ print '\x1b[1;32m✓ \x1b[0m', csv
+ except Exception, e:
+ print '\x1b[33mfailed: should not have thrown\x1b[0m',csv,str(e)
+
+ def test_lon_lat_detection(**kwargs):
+ ds = get_csv_ds('lon_lat.csv')
+ eq_(len(ds.fields()),2)
+ eq_(ds.fields(),['lon','lat'])
+ eq_(ds.field_types(),['int','int'])
+ query = mapnik.Query(ds.envelope())
+ for fld in ds.fields():
+ query.add_property_name(fld)
+ fs = ds.features(query)
+ desc = ds.describe()
+ eq_(desc['geometry_type'],mapnik.DataGeometryType.Point)
+ feat = fs.next()
+ attr = {'lon': 0, 'lat': 0}
+ eq_(feat.attributes,attr)
+
+ def test_lng_lat_detection(**kwargs):
+ ds = get_csv_ds('lng_lat.csv')
+ eq_(len(ds.fields()),2)
+ eq_(ds.fields(),['lng','lat'])
+ eq_(ds.field_types(),['int','int'])
+ query = mapnik.Query(ds.envelope())
+ for fld in ds.fields():
+ query.add_property_name(fld)
+ fs = ds.features(query)
+ desc = ds.describe()
+ eq_(desc['geometry_type'],mapnik.DataGeometryType.Point)
+ feat = fs.next()
+ attr = {'lng': 0, 'lat': 0}
+ eq_(feat.attributes,attr)
+
+ def test_type_detection(**kwargs):
+ ds = get_csv_ds('nypd.csv')
+ eq_(ds.fields(),['Precinct','Phone','Address','City','geo_longitude','geo_latitude','geo_accuracy'])
+ eq_(ds.field_types(),['str','str','str','str','float','float','str'])
+ feat = ds.featureset().next()
+ attr = {'City': u'New York, NY', 'geo_accuracy': u'house', 'Phone': u'(212) 334-0711', 'Address': u'19 Elizabeth Street', 'Precinct': u'5th Precinct', 'geo_longitude': -70, 'geo_latitude': 40}
+ eq_(feat.attributes,attr)
+ eq_(len(ds.all_features()),2)
+ desc = ds.describe()
+ eq_(desc['geometry_type'],mapnik.DataGeometryType.Point)
+ eq_(desc['name'],'csv')
+ eq_(desc['type'],mapnik.DataType.Vector)
+ eq_(desc['encoding'],'utf-8')
+
+ def test_skipping_blank_rows(**kwargs):
+ ds = get_csv_ds('blank_rows.csv')
+ eq_(ds.fields(),['x','y','name'])
+ eq_(ds.field_types(),['int','int','str'])
+ eq_(len(ds.all_features()),2)
+ desc = ds.describe()
+ eq_(desc['geometry_type'],mapnik.DataGeometryType.Point)
+ eq_(desc['name'],'csv')
+ eq_(desc['type'],mapnik.DataType.Vector)
+ eq_(desc['encoding'],'utf-8')
+
+ def test_empty_rows(**kwargs):
+ ds = get_csv_ds('empty_rows.csv')
+ eq_(len(ds.fields()),10)
+ eq_(len(ds.field_types()),10)
+ eq_(ds.fields(),['x', 'y', 'text', 'date', 'integer', 'boolean', 'float', 'time', 'datetime', 'empty_column'])
+ eq_(ds.field_types(),['int', 'int', 'str', 'str', 'int', 'bool', 'float', 'str', 'str', 'str'])
+ fs = ds.featureset()
+ attr = {'x': 0, 'empty_column': u'', 'text': u'a b', 'float': 1.0, 'datetime': u'1971-01-01T04:14:00', 'y': 0, 'boolean': True, 'time': u'04:14:00', 'date': u'1971-01-01', 'integer': 40}
+ first = True
+ for feat in fs:
+ if first:
+ first=False
+ eq_(feat.attributes,attr)
+ eq_(len(feat),10)
+ eq_(feat['empty_column'],u'')
+
+ desc = ds.describe()
+ eq_(desc['geometry_type'],mapnik.DataGeometryType.Point)
+ eq_(desc['name'],'csv')
+ eq_(desc['type'],mapnik.DataType.Vector)
+ eq_(desc['encoding'],'utf-8')
+
+ def test_slashes(**kwargs):
+ ds = get_csv_ds('has_attributes_with_slashes.csv')
+ eq_(len(ds.fields()),3)
+ fs = ds.all_features()
+ eq_(fs[0].attributes,{'x':0,'y':0,'name':u'a/a'})
+ eq_(fs[1].attributes,{'x':1,'y':4,'name':u'b/b'})
+ eq_(fs[2].attributes,{'x':10,'y':2.5,'name':u'c/c'})
+ desc = ds.describe()
+ eq_(desc['geometry_type'],mapnik.DataGeometryType.Point)
+ eq_(desc['name'],'csv')
+ eq_(desc['type'],mapnik.DataType.Vector)
+ eq_(desc['encoding'],'utf-8')
+
+ def test_wkt_field(**kwargs):
+ ds = get_csv_ds('wkt.csv')
+ eq_(len(ds.fields()),1)
+ eq_(ds.fields(),['type'])
+ eq_(ds.field_types(),['str'])
+ fs = ds.all_features()
+ #eq_(len(fs[0].geometries()),1)
+ eq_(fs[0].geometry.type(),mapnik.GeometryType.Point)
+ #eq_(len(fs[1].geometries()),1)
+ eq_(fs[1].geometry.type(),mapnik.GeometryType.LineString)
+ #eq_(len(fs[2].geometries()),1)
+ eq_(fs[2].geometry.type(),mapnik.GeometryType.Polygon)
+ #eq_(len(fs[3].geometries()),1) # one geometry, two parts
+ eq_(fs[3].geometry.type(),mapnik.GeometryType.Polygon)
+ #eq_(len(fs[4].geometries()),4)
+ eq_(fs[4].geometry.type(),mapnik.GeometryType.MultiPoint)
+ #eq_(len(fs[5].geometries()),2)
+ eq_(fs[5].geometry.type(),mapnik.GeometryType.MultiLineString)
+ #eq_(len(fs[6].geometries()),2)
+ eq_(fs[6].geometry.type(),mapnik.GeometryType.MultiPolygon)
+ #eq_(len(fs[7].geometries()),2)
+ eq_(fs[7].geometry.type(),mapnik.GeometryType.MultiPolygon)
+ desc = ds.describe()
+ eq_(desc['geometry_type'],mapnik.DataGeometryType.Collection)
+ eq_(desc['name'],'csv')
+ eq_(desc['type'],mapnik.DataType.Vector)
+ eq_(desc['encoding'],'utf-8')
+
+ def test_handling_of_missing_header(**kwargs):
+ ds = get_csv_ds('missing_header.csv')
+ eq_(len(ds.fields()),6)
+ eq_(ds.fields(),['one','two','x','y','_4','aftermissing'])
+ fs = ds.featureset()
+ feat = fs.next()
+ eq_(feat['_4'],'missing')
+ desc = ds.describe()
+ eq_(desc['geometry_type'],mapnik.DataGeometryType.Point)
+ eq_(desc['name'],'csv')
+ eq_(desc['type'],mapnik.DataType.Vector)
+ eq_(desc['encoding'],'utf-8')
+
+ def test_handling_of_headers_that_are_numbers(**kwargs):
+ ds = get_csv_ds('numbers_for_headers.csv')
+ eq_(len(ds.fields()),5)
+ eq_(ds.fields(),['x','y','1990','1991','1992'])
+ fs = ds.featureset()
+ feat = fs.next()
+ eq_(feat['x'],0)
+ eq_(feat['y'],0)
+ eq_(feat['1990'],1)
+ eq_(feat['1991'],2)
+ eq_(feat['1992'],3)
+ eq_(mapnik.Expression("[1991]=2").evaluate(feat),True)
+
+ def test_quoted_numbers(**kwargs):
+ ds = get_csv_ds('points.csv')
+ eq_(len(ds.fields()),6)
+ eq_(ds.fields(),['lat','long','name','nr','color','placements'])
+ fs = ds.all_features()
+ eq_(fs[0]['placements'],"N,S,E,W,SW,10,5")
+ eq_(fs[1]['placements'],"N,S,E,W,SW,10,5")
+ eq_(fs[2]['placements'],"N,S,E,W,SW,10,5")
+ eq_(fs[3]['placements'],"N,S,E,W,SW,10,5")
+ eq_(fs[4]['placements'],"N,S,E,W,SW,10,5")
+ desc = ds.describe()
+ eq_(desc['geometry_type'],mapnik.DataGeometryType.Point)
+ eq_(desc['name'],'csv')
+ eq_(desc['type'],mapnik.DataType.Vector)
+ eq_(desc['encoding'],'utf-8')
+
+ def test_reading_windows_newlines(**kwargs):
+ ds = get_csv_ds('windows_newlines.csv')
+ eq_(len(ds.fields()),3)
+ feats = ds.all_features()
+ eq_(len(feats),1)
+ fs = ds.featureset()
+ feat = fs.next()
+ eq_(feat['x'],1)
+ eq_(feat['y'],10)
+ eq_(feat['z'],9999.9999)
+ desc = ds.describe()
+ eq_(desc['geometry_type'],mapnik.DataGeometryType.Point)
+ eq_(desc['name'],'csv')
+ eq_(desc['type'],mapnik.DataType.Vector)
+ eq_(desc['encoding'],'utf-8')
+
+ def test_reading_mac_newlines(**kwargs):
+ ds = get_csv_ds('mac_newlines.csv')
+ eq_(len(ds.fields()),3)
+ feats = ds.all_features()
+ eq_(len(feats),1)
+ fs = ds.featureset()
+ feat = fs.next()
+ eq_(feat['x'],1)
+ eq_(feat['y'],10)
+ eq_(feat['z'],9999.9999)
+ desc = ds.describe()
+ eq_(desc['geometry_type'],mapnik.DataGeometryType.Point)
+ eq_(desc['name'],'csv')
+ eq_(desc['type'],mapnik.DataType.Vector)
+ eq_(desc['encoding'],'utf-8')
+
+ def check_newlines(filename):
+ ds = get_csv_ds(filename)
+ eq_(len(ds.fields()),3)
+ feats = ds.all_features()
+ eq_(len(feats),1)
+ fs = ds.featureset()
+ feat = fs.next()
+ eq_(feat['x'],0)
+ eq_(feat['y'],0)
+ eq_(feat['line'],'many\n lines\n of text\n with unix newlines')
+ desc = ds.describe()
+ eq_(desc['geometry_type'],mapnik.DataGeometryType.Point)
+ eq_(desc['name'],'csv')
+ eq_(desc['type'],mapnik.DataType.Vector)
+ eq_(desc['encoding'],'utf-8')
+
+ def test_mixed_mac_unix_newlines(**kwargs):
+ check_newlines('mac_newlines_with_unix_inline.csv')
+
+ def test_mixed_mac_unix_newlines_escaped(**kwargs):
+ check_newlines('mac_newlines_with_unix_inline_escaped.csv')
+
+ # To hard to support this case
+ #def test_mixed_unix_windows_newlines(**kwargs):
+ # check_newlines('unix_newlines_with_windows_inline.csv')
+
+ # To hard to support this case
+ #def test_mixed_unix_windows_newlines_escaped(**kwargs):
+ # check_newlines('unix_newlines_with_windows_inline_escaped.csv')
+
+ def test_mixed_windows_unix_newlines(**kwargs):
+ check_newlines('windows_newlines_with_unix_inline.csv')
+
+ def test_mixed_windows_unix_newlines_escaped(**kwargs):
+ check_newlines('windows_newlines_with_unix_inline_escaped.csv')
+
+ def test_tabs(**kwargs):
+ ds = get_csv_ds('tabs_in_csv.csv')
+ eq_(len(ds.fields()),3)
+ eq_(ds.fields(),['x','y','z'])
+ fs = ds.featureset()
+ feat = fs.next()
+ eq_(feat['x'],-122)
+ eq_(feat['y'],48)
+ eq_(feat['z'],0)
+ desc = ds.describe()
+ eq_(desc['geometry_type'],mapnik.DataGeometryType.Point)
+ eq_(desc['name'],'csv')
+ eq_(desc['type'],mapnik.DataType.Vector)
+ eq_(desc['encoding'],'utf-8')
+
+ def test_separator_pipes(**kwargs):
+ ds = get_csv_ds('pipe_delimiters.csv')
+ eq_(len(ds.fields()),3)
+ eq_(ds.fields(),['x','y','z'])
+ fs = ds.featureset()
+ feat = fs.next()
+ eq_(feat['x'],0)
+ eq_(feat['y'],0)
+ eq_(feat['z'],'hello')
+ desc = ds.describe()
+ eq_(desc['geometry_type'],mapnik.DataGeometryType.Point)
+ eq_(desc['name'],'csv')
+ eq_(desc['type'],mapnik.DataType.Vector)
+ eq_(desc['encoding'],'utf-8')
+
+ def test_separator_semicolon(**kwargs):
+ ds = get_csv_ds('semicolon_delimiters.csv')
+ eq_(len(ds.fields()),3)
+ eq_(ds.fields(),['x','y','z'])
+ fs = ds.featureset()
+ feat = fs.next()
+ eq_(feat['x'],0)
+ eq_(feat['y'],0)
+ eq_(feat['z'],'hello')
+ desc = ds.describe()
+ eq_(desc['geometry_type'],mapnik.DataGeometryType.Point)
+ eq_(desc['name'],'csv')
+ eq_(desc['type'],mapnik.DataType.Vector)
+ eq_(desc['encoding'],'utf-8')
+
+ def test_that_null_and_bool_keywords_are_empty_strings(**kwargs):
+ ds = get_csv_ds('nulls_and_booleans_as_strings.csv')
+ eq_(len(ds.fields()),4)
+ eq_(ds.fields(),['x','y','null','boolean'])
+ eq_(ds.field_types(),['int', 'int', 'str', 'bool'])
+ fs = ds.featureset()
+ feat = fs.next()
+ eq_(feat['x'],0)
+ eq_(feat['y'],0)
+ eq_(feat['null'],'null')
+ eq_(feat['boolean'],True)
+ feat = fs.next()
+ eq_(feat['x'],0)
+ eq_(feat['y'],0)
+ eq_(feat['null'],'')
+ eq_(feat['boolean'],False)
+ desc = ds.describe()
+ eq_(desc['geometry_type'],mapnik.DataGeometryType.Point)
+
+ @raises(RuntimeError)
+ def test_that_nonexistant_query_field_throws(**kwargs):
+ ds = get_csv_ds('lon_lat.csv')
+ eq_(len(ds.fields()),2)
+ eq_(ds.fields(),['lon','lat'])
+ eq_(ds.field_types(),['int','int'])
+ query = mapnik.Query(ds.envelope())
+ for fld in ds.fields():
+ query.add_property_name(fld)
+ # also add an invalid one, triggering throw
+ query.add_property_name('bogus')
+ ds.features(query)
+
+ def test_that_leading_zeros_mean_strings(**kwargs):
+ ds = get_csv_ds('leading_zeros.csv')
+ eq_(len(ds.fields()),3)
+ eq_(ds.fields(),['x','y','fips'])
+ eq_(ds.field_types(),['int','int','str'])
+ fs = ds.featureset()
+ feat = fs.next()
+ eq_(feat['x'],0)
+ eq_(feat['y'],0)
+ eq_(feat['fips'],'001')
+ feat = fs.next()
+ eq_(feat['x'],0)
+ eq_(feat['y'],0)
+ eq_(feat['fips'],'003')
+ feat = fs.next()
+ eq_(feat['x'],0)
+ eq_(feat['y'],0)
+ eq_(feat['fips'],'005')
+ desc = ds.describe()
+ eq_(desc['geometry_type'],mapnik.DataGeometryType.Point)
+
+ def test_advanced_geometry_detection(**kwargs):
+ ds = get_csv_ds('point_wkt.csv')
+ eq_(ds.describe()['geometry_type'],mapnik.DataGeometryType.Point)
+ ds = get_csv_ds('poly_wkt.csv')
+ eq_(ds.describe()['geometry_type'],mapnik.DataGeometryType.Polygon)
+ ds = get_csv_ds('multi_poly_wkt.csv')
+ eq_(ds.describe()['geometry_type'],mapnik.DataGeometryType.Polygon)
+ ds = get_csv_ds('line_wkt.csv')
+ eq_(ds.describe()['geometry_type'],mapnik.DataGeometryType.LineString)
+
+ def test_creation_of_csv_from_in_memory_string(**kwargs):
+ csv_string = '''
+ wkt,Name
+ "POINT (120.15 48.47)","Winthrop, WA"
+ ''' # csv plugin will test lines <= 10 chars for being fully blank
+ ds = mapnik.Datasource(**{"type":"csv","inline":csv_string})
+ eq_(ds.describe()['geometry_type'],mapnik.DataGeometryType.Point)
+ fs = ds.featureset()
+ feat = fs.next()
+ eq_(feat['Name'],u"Winthrop, WA")
+
+ def test_creation_of_csv_from_in_memory_string_with_uft8(**kwargs):
+ csv_string = '''
+ wkt,Name
+ "POINT (120.15 48.47)","Québec"
+ ''' # csv plugin will test lines <= 10 chars for being fully blank
+ ds = mapnik.Datasource(**{"type":"csv","inline":csv_string})
+ eq_(ds.describe()['geometry_type'],mapnik.DataGeometryType.Point)
+ fs = ds.featureset()
+ feat = fs.next()
+ eq_(feat['Name'],u"Québec")
+
+ def validate_geojson_datasource(ds):
+ eq_(len(ds.fields()),1)
+ eq_(ds.fields(),['type'])
+ eq_(ds.field_types(),['str'])
+ fs = ds.all_features()
+ #eq_(len(fs[0].geometries()),1)
+ eq_(fs[0].geometry.type(),mapnik.GeometryType.Point)
+ #eq_(len(fs[1].geometries()),1)
+ eq_(fs[1].geometry.type(),mapnik.GeometryType.LineString)
+ #eq_(len(fs[2].geometries()),1)
+ eq_(fs[2].geometry.type(), mapnik.GeometryType.Polygon)
+ #eq_(len(fs[3].geometries()),1) # one geometry, two parts
+ eq_(fs[3].geometry.type(),mapnik.GeometryType.Polygon)
+ #eq_(len(fs[4].geometries()),4)
+ eq_(fs[4].geometry.type(),mapnik.GeometryType.MultiPoint)
+ #eq_(len(fs[5].geometries()),2)
+ eq_(fs[5].geometry.type(),mapnik.GeometryType.MultiLineString)
+ #eq_(len(fs[6].geometries()),2)
+ eq_(fs[6].geometry.type(),mapnik.GeometryType.MultiPolygon)
+ #eq_(len(fs[7].geometries()),2)
+ eq_(fs[7].geometry.type(),mapnik.GeometryType.MultiPolygon)
+ desc = ds.describe()
+ eq_(desc['geometry_type'],mapnik.DataGeometryType.Collection)
+ eq_(desc['name'],'csv')
+ eq_(desc['type'],mapnik.DataType.Vector)
+ eq_(desc['encoding'],'utf-8')
+
+ def test_json_field1(**kwargs):
+ ds = get_csv_ds('geojson_double_quote_escape.csv')
+ validate_geojson_datasource(ds)
+
+ def test_json_field2(**kwargs):
+ ds = get_csv_ds('geojson_single_quote.csv')
+ validate_geojson_datasource(ds)
+
+ def test_json_field3(**kwargs):
+ ds = get_csv_ds('geojson_2x_double_quote_filebakery_style.csv')
+ validate_geojson_datasource(ds)
+
+ def test_that_blank_undelimited_rows_are_still_parsed(**kwargs):
+ ds = get_csv_ds('more_headers_than_column_values.csv')
+ eq_(len(ds.fields()),5)
+ eq_(ds.fields(),['x','y','one', 'two','three'])
+ eq_(ds.field_types(),['int','int','str','str','str'])
+ fs = ds.featureset()
+ feat = fs.next()
+ eq_(feat['x'],0)
+ eq_(feat['y'],0)
+ eq_(feat['one'],'')
+ eq_(feat['two'],'')
+ eq_(feat['three'],'')
+ desc = ds.describe()
+ eq_(desc['geometry_type'],mapnik.DataGeometryType.Point)
+
+ @raises(RuntimeError)
+ def test_that_fewer_headers_than_rows_throws(**kwargs):
+ # this has invalid header # so throw
+ get_csv_ds('more_column_values_than_headers.csv')
+
+ def test_that_feature_id_only_incremented_for_valid_rows(**kwargs):
+ ds = mapnik.Datasource(type='csv',
+ file=os.path.join('../data/csv/warns','feature_id_counting.csv'))
+ eq_(len(ds.fields()),3)
+ eq_(ds.fields(),['x','y','id'])
+ eq_(ds.field_types(),['int','int','int'])
+ fs = ds.featureset()
+ # first
+ feat = fs.next()
+ eq_(feat['x'],0)
+ eq_(feat['y'],0)
+ eq_(feat['id'],1)
+ # second, should have skipped bogus one
+ feat = fs.next()
+ eq_(feat['x'],0)
+ eq_(feat['y'],0)
+ eq_(feat['id'],2)
+ desc = ds.describe()
+ eq_(desc['geometry_type'],mapnik.DataGeometryType.Point)
+ eq_(len(ds.all_features()),2)
+
+ def test_dynamically_defining_headers1(**kwargs):
+ ds = mapnik.Datasource(type='csv',
+ file=os.path.join('../data/csv/fails','needs_headers_two_lines.csv'),
+ headers='x,y,name')
+ eq_(len(ds.fields()),3)
+ eq_(ds.fields(),['x','y','name'])
+ eq_(ds.field_types(),['int','int','str'])
+ fs = ds.featureset()
+ feat = fs.next()
+ eq_(feat['x'],0)
+ eq_(feat['y'],0)
+ eq_(feat['name'],'data_name')
+ desc = ds.describe()
+ eq_(desc['geometry_type'],mapnik.DataGeometryType.Point)
+ eq_(len(ds.all_features()),2)
+
+ def test_dynamically_defining_headers2(**kwargs):
+ ds = mapnik.Datasource(type='csv',
+ file=os.path.join('../data/csv/fails','needs_headers_one_line.csv'),
+ headers='x,y,name')
+ eq_(len(ds.fields()),3)
+ eq_(ds.fields(),['x','y','name'])
+ eq_(ds.field_types(),['int','int','str'])
+ fs = ds.featureset()
+ feat = fs.next()
+ eq_(feat['x'],0)
+ eq_(feat['y'],0)
+ eq_(feat['name'],'data_name')
+ desc = ds.describe()
+ eq_(desc['geometry_type'],mapnik.DataGeometryType.Point)
+ eq_(len(ds.all_features()),1)
+
+ def test_dynamically_defining_headers3(**kwargs):
+ ds = mapnik.Datasource(type='csv',
+ file=os.path.join('../data/csv/fails','needs_headers_one_line_no_newline.csv'),
+ headers='x,y,name')
+ eq_(len(ds.fields()),3)
+ eq_(ds.fields(),['x','y','name'])
+ eq_(ds.field_types(),['int','int','str'])
+ fs = ds.featureset()
+ feat = fs.next()
+ eq_(feat['x'],0)
+ eq_(feat['y'],0)
+ eq_(feat['name'],'data_name')
+ desc = ds.describe()
+ eq_(desc['geometry_type'],mapnik.DataGeometryType.Point)
+ eq_(len(ds.all_features()),1)
+
+ def test_that_64bit_int_fields_work(**kwargs):
+ ds = get_csv_ds('64bit_int.csv')
+ eq_(len(ds.fields()),3)
+ eq_(ds.fields(),['x','y','bigint'])
+ eq_(ds.field_types(),['int','int','int'])
+ fs = ds.featureset()
+ feat = fs.next()
+ eq_(feat['bigint'],2147483648)
+ feat = fs.next()
+ eq_(feat['bigint'],9223372036854775807)
+ eq_(feat['bigint'],0x7FFFFFFFFFFFFFFF)
+ desc = ds.describe()
+ eq_(desc['geometry_type'],mapnik.DataGeometryType.Point)
+ eq_(len(ds.all_features()),2)
+
+ def test_various_number_types(**kwargs):
+ ds = get_csv_ds('number_types.csv')
+ eq_(len(ds.fields()),3)
+ eq_(ds.fields(),['x','y','floats'])
+ eq_(ds.field_types(),['int','int','float'])
+ fs = ds.featureset()
+ feat = fs.next()
+ eq_(feat['floats'],.0)
+ feat = fs.next()
+ eq_(feat['floats'],+.0)
+ feat = fs.next()
+ eq_(feat['floats'],1e-06)
+ feat = fs.next()
+ eq_(feat['floats'],-1e-06)
+ feat = fs.next()
+ eq_(feat['floats'],0.000001)
+ feat = fs.next()
+ eq_(feat['floats'],1.234e+16)
+ feat = fs.next()
+ eq_(feat['floats'],1.234e+16)
+ desc = ds.describe()
+ eq_(desc['geometry_type'],mapnik.DataGeometryType.Point)
+ eq_(len(ds.all_features()),8)
+
+ def test_manually_supplied_extent(**kwargs):
+ csv_string = '''
+ wkt,Name
+ '''
+ ds = mapnik.Datasource(**{"type":"csv","extent":"-180,-90,180,90","inline":csv_string})
+ b = ds.envelope()
+ eq_(b.minx,-180)
+ eq_(b.miny,-90)
+ eq_(b.maxx,180)
+ eq_(b.maxy,90)
+
+ def test_inline_geojson(**kwargs):
+ csv_string = "geojson\n'{\"coordinates\":[-92.22568,38.59553],\"type\":\"Point\"}'"
+ ds = mapnik.Datasource(**{"type":"csv","inline":csv_string})
+ eq_(len(ds.fields()),0)
+ eq_(ds.fields(),[])
+ # FIXME - re-enable after https://github.com/mapnik/mapnik/issues/2319 is fixed
+ #fs = ds.featureset()
+ #feat = fs.next()
+ #eq_(feat.num_geometries(),1)
+
+if __name__ == "__main__":
+ setup()
+ [eval(run)(visual=True) for run in dir() if 'test_' in run]
diff --git a/test/python_tests/datasource_test.py b/test/python_tests/datasource_test.py
new file mode 100644
index 0000000..4ada3dc
--- /dev/null
+++ b/test/python_tests/datasource_test.py
@@ -0,0 +1,168 @@
+#!/usr/bin/env python
+
+from nose.tools import eq_, raises
+from utilities import execution_path, run_all
+import os, mapnik
+from itertools import groupby
+
+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_that_datasources_exist():
+ if len(mapnik.DatasourceCache.plugin_names()) == 0:
+ print '***NOTICE*** - no datasource plugins have been loaded'
+
+# adapted from raster_symboliser_test#test_dataraster_query_point
+@raises(RuntimeError)
+def test_vrt_referring_to_missing_files():
+ srs = '+init=epsg:32630'
+ if 'gdal' in mapnik.DatasourceCache.plugin_names():
+ lyr = mapnik.Layer('dataraster')
+ lyr.datasource = mapnik.Gdal(
+ file = '../data/raster/missing_raster.vrt',
+ band = 1,
+ )
+ lyr.srs = srs
+ _map = mapnik.Map(256, 256, srs)
+ _map.layers.append(lyr)
+
+ # center of extent of raster
+ x, y = 556113.0,4381428.0 # center of extent of raster
+
+ _map.zoom_all()
+
+ # Fancy stuff to supress output of error
+ # open 2 fds
+ null_fds = [os.open(os.devnull, os.O_RDWR) for x in xrange(2)]
+ # save the current file descriptors to a tuple
+ save = os.dup(1), os.dup(2)
+ # put /dev/null fds on 1 and 2
+ os.dup2(null_fds[0], 1)
+ os.dup2(null_fds[1], 2)
+
+ # *** run the function ***
+ try:
+ # Should RuntimeError here
+ _map.query_point(0, x, y).features
+ finally:
+ # restore file descriptors so I can print the results
+ os.dup2(save[0], 1)
+ os.dup2(save[1], 2)
+ # close the temporary fds
+ os.close(null_fds[0])
+ os.close(null_fds[1])
+
+
+def test_field_listing():
+ if 'shape' in mapnik.DatasourceCache.plugin_names():
+ ds = mapnik.Shapefile(file='../data/shp/poly.shp')
+ fields = ds.fields()
+ eq_(fields, ['AREA', 'EAS_ID', 'PRFEDEA'])
+ desc = ds.describe()
+ eq_(desc['geometry_type'],mapnik.DataGeometryType.Polygon)
+ eq_(desc['name'],'shape')
+ eq_(desc['type'],mapnik.DataType.Vector)
+ eq_(desc['encoding'],'utf-8')
+
+def test_total_feature_count_shp():
+ if 'shape' in mapnik.DatasourceCache.plugin_names():
+ ds = mapnik.Shapefile(file='../data/shp/poly.shp')
+ features = ds.all_features()
+ num_feats = len(features)
+ eq_(num_feats, 10)
+
+def test_total_feature_count_json():
+ if 'ogr' in mapnik.DatasourceCache.plugin_names():
+ ds = mapnik.Ogr(file='../data/json/points.geojson',layer_by_index=0)
+ desc = ds.describe()
+ eq_(desc['geometry_type'],mapnik.DataGeometryType.Point)
+ eq_(desc['name'],'ogr')
+ eq_(desc['type'],mapnik.DataType.Vector)
+ eq_(desc['encoding'],'utf-8')
+ features = ds.all_features()
+ num_feats = len(features)
+ eq_(num_feats, 5)
+
+def test_sqlite_reading():
+ if 'sqlite' in mapnik.DatasourceCache.plugin_names():
+ ds = mapnik.SQLite(file='../data/sqlite/world.sqlite',table_by_index=0)
+ desc = ds.describe()
+ eq_(desc['geometry_type'],mapnik.DataGeometryType.Polygon)
+ eq_(desc['name'],'sqlite')
+ eq_(desc['type'],mapnik.DataType.Vector)
+ eq_(desc['encoding'],'utf-8')
+ features = ds.all_features()
+ num_feats = len(features)
+ eq_(num_feats, 245)
+
+def test_reading_json_from_string():
+ json = open('../data/json/points.geojson','r').read()
+ if 'ogr' in mapnik.DatasourceCache.plugin_names():
+ ds = mapnik.Ogr(file=json,layer_by_index=0)
+ features = ds.all_features()
+ num_feats = len(features)
+ eq_(num_feats, 5)
+
+def test_feature_envelope():
+ if 'shape' in mapnik.DatasourceCache.plugin_names():
+ ds = mapnik.Shapefile(file='../data/shp/poly.shp')
+ features = ds.all_features()
+ for feat in features:
+ env = feat.envelope()
+ contains = ds.envelope().contains(env)
+ eq_(contains, True)
+ intersects = ds.envelope().contains(env)
+ eq_(intersects, True)
+
+def test_feature_attributes():
+ if 'shape' in mapnik.DatasourceCache.plugin_names():
+ ds = mapnik.Shapefile(file='../data/shp/poly.shp')
+ features = ds.all_features()
+ feat = features[0]
+ attrs = {'PRFEDEA': u'35043411', 'EAS_ID': 168, 'AREA': 215229.266}
+ eq_(feat.attributes, attrs)
+ eq_(ds.fields(),['AREA', 'EAS_ID', 'PRFEDEA'])
+ eq_(ds.field_types(),['float','int','str'])
+
+def test_ogr_layer_by_sql():
+ if 'ogr' in mapnik.DatasourceCache.plugin_names():
+ ds = mapnik.Ogr(file='../data/shp/poly.shp', layer_by_sql='SELECT * FROM poly WHERE EAS_ID = 168')
+ features = ds.all_features()
+ num_feats = len(features)
+ eq_(num_feats, 1)
+
+def test_hit_grid():
+
+ def rle_encode(l):
+ """ encode a list of strings with run-length compression """
+ return ["%d:%s" % (len(list(group)), name) for name, group in groupby(l)]
+
+ m = mapnik.Map(256,256);
+ try:
+ mapnik.load_map(m,'../data/good_maps/agg_poly_gamma_map.xml');
+ m.zoom_all()
+ join_field = 'NAME'
+ fg = [] # feature grid
+ for y in range(0, 256, 4):
+ for x in range(0, 256, 4):
+ featureset = m.query_map_point(0,x,y)
+ added = False
+ for feature in featureset.features:
+ fg.append(feature[join_field])
+ added = True
+ if not added:
+ fg.append('')
+ hit_list = '|'.join(rle_encode(fg))
+ eq_(hit_list[:16],'730:|2:Greenland')
+ eq_(hit_list[-12:],'1:Chile|812:')
+ except RuntimeError, e:
+ # only test datasources that we have installed
+ if not 'Could not create datasource' in str(e):
+ raise RuntimeError(str(e))
+
+
+if __name__ == '__main__':
+ setup()
+ exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/datasource_xml_template_test.py b/test/python_tests/datasource_xml_template_test.py
new file mode 100644
index 0000000..38a73a3
--- /dev/null
+++ b/test/python_tests/datasource_xml_template_test.py
@@ -0,0 +1,23 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import os
+from utilities import execution_path, run_all
+import mapnik
+
+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_datasource_template_is_working():
+ m = mapnik.Map(256,256)
+ try:
+ mapnik.load_map(m,'../data/good_maps/datasource.xml')
+ except RuntimeError, e:
+ if "Required parameter 'type'" in str(e):
+ raise RuntimeError(e)
+
+if __name__ == "__main__":
+ setup()
+ exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/extra_map_props_test.py b/test/python_tests/extra_map_props_test.py
new file mode 100644
index 0000000..045cddb
--- /dev/null
+++ b/test/python_tests/extra_map_props_test.py
@@ -0,0 +1,54 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from nose.tools import eq_
+from utilities import execution_path, run_all
+import os, mapnik
+
+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_arbitrary_parameters_attached_to_map():
+ m = mapnik.Map(256,256)
+ mapnik.load_map(m,'../data/good_maps/extra_arbitary_map_parameters.xml')
+ eq_(len(m.parameters),5)
+ eq_(m.parameters['key'],'value2')
+ eq_(m.parameters['key3'],'value3')
+ eq_(m.parameters['unicode'],u'iván')
+ eq_(m.parameters['integer'],10)
+ eq_(m.parameters['decimal'],.999)
+ m2 = mapnik.Map(256,256)
+ for k,v in m.parameters:
+ m2.parameters.append(mapnik.Parameter(k,v))
+ eq_(len(m2.parameters),5)
+ eq_(m2.parameters['key'],'value2')
+ eq_(m2.parameters['key3'],'value3')
+ eq_(m2.parameters['unicode'],u'iván')
+ eq_(m2.parameters['integer'],10)
+ eq_(m2.parameters['decimal'],.999)
+ map_string = mapnik.save_map_to_string(m)
+ m3 = mapnik.Map(256,256)
+ mapnik.load_map_from_string(m3,map_string)
+ eq_(len(m3.parameters),5)
+ eq_(m3.parameters['key'],'value2')
+ eq_(m3.parameters['key3'],'value3')
+ eq_(m3.parameters['unicode'],u'iván')
+ eq_(m3.parameters['integer'],10)
+ eq_(m3.parameters['decimal'],.999)
+
+
+def test_serializing_arbitrary_parameters():
+ m = mapnik.Map(256,256)
+ m.parameters.append(mapnik.Parameter('width',m.width))
+ m.parameters.append(mapnik.Parameter('height',m.height))
+
+ m2 = mapnik.Map(1,1)
+ mapnik.load_map_from_string(m2,mapnik.save_map_to_string(m))
+ eq_(m2.parameters['width'],m.width)
+ eq_(m2.parameters['height'],m.height)
+
+if __name__ == "__main__":
+ setup()
+ exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/feature_id_test.py b/test/python_tests/feature_id_test.py
new file mode 100644
index 0000000..66c20cc
--- /dev/null
+++ b/test/python_tests/feature_id_test.py
@@ -0,0 +1,66 @@
+#!/usr/bin/env python
+
+from nose.tools import eq_
+from utilities import execution_path, run_all
+import os, mapnik
+import itertools
+
+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 compare_shape_between_mapnik_and_ogr(shapefile,query=None):
+ plugins = mapnik.DatasourceCache.plugin_names()
+ if 'shape' in plugins and 'ogr' in plugins:
+ ds1 = mapnik.Ogr(file=shapefile,layer_by_index=0)
+ ds2 = mapnik.Shapefile(file=shapefile)
+ if query:
+ fs1 = ds1.features(query)
+ fs2 = ds2.features(query)
+ else:
+ fs1 = ds1.featureset()
+ fs2 = ds2.featureset()
+ count = 0;
+ for feat1,feat2 in itertools.izip(fs1,fs2):
+ count += 1
+ eq_(feat1.id(),feat2.id(),
+ '%s : ogr feature id %s "%s" does not equal shapefile feature id %s "%s"'
+ % (count,feat1.id(),str(feat1.attributes), feat2.id(),str(feat2.attributes)))
+ return True
+
+
+def test_shapefile_line_featureset_id():
+ compare_shape_between_mapnik_and_ogr('../data/shp/polylines.shp')
+
+def test_shapefile_polygon_featureset_id():
+ compare_shape_between_mapnik_and_ogr('../data/shp/poly.shp')
+
+def test_shapefile_polygon_feature_query_id():
+ bbox = (15523428.2632, 4110477.6323, -11218494.8310, 7495720.7404)
+ query = mapnik.Query(mapnik.Box2d(*bbox))
+ if 'ogr' in mapnik.DatasourceCache.plugin_names():
+ ds = mapnik.Ogr(file='../data/shp/world_merc.shp',layer_by_index=0)
+ for fld in ds.fields():
+ query.add_property_name(fld)
+ compare_shape_between_mapnik_and_ogr('../data/shp/world_merc.shp',query)
+
+def test_feature_hit_count():
+ pass
+ #raise Todo("need to optimize multigeom bbox handling in shapeindex: https://github.com/mapnik/mapnik/issues/783")
+ # results in different results between shp and ogr!
+ #bbox = (-14284551.8434, 2074195.1992, -7474929.8687, 8140237.7628)
+ #bbox = (1113194.91,4512803.085,2226389.82,6739192.905)
+ #query = mapnik.Query(mapnik.Box2d(*bbox))
+ #if 'ogr' in mapnik.DatasourceCache.plugin_names():
+ # ds1 = mapnik.Ogr(file='../data/shp/world_merc.shp',layer_by_index=0)
+ # for fld in ds1.fields():
+ # query.add_property_name(fld)
+ # ds2 = mapnik.Shapefile(file='../data/shp/world_merc.shp')
+ # count1 = len(ds1.features(query).features)
+ # count2 = len(ds2.features(query).features)
+ # eq_(count1,count2,"Feature count differs between OGR driver (%s features) and Shapefile Driver (%s features) when querying the same bbox" % (count1,count2))
+
+if __name__ == "__main__":
+ setup()
+ exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/feature_test.py b/test/python_tests/feature_test.py
new file mode 100644
index 0000000..5574cc7
--- /dev/null
+++ b/test/python_tests/feature_test.py
@@ -0,0 +1,110 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from nose.tools import eq_,raises
+from utilities import run_all
+
+import mapnik
+from binascii import unhexlify
+
+def test_default_constructor():
+ f = mapnik.Feature(mapnik.Context(),1)
+ eq_(f is not None,True)
+
+def test_feature_geo_interface():
+ ctx = mapnik.Context()
+ feat = mapnik.Feature(ctx,1)
+ feat.geometry = mapnik.Geometry.from_wkt('Point (0 0)')
+ eq_(feat.__geo_interface__['geometry'],{u'type': u'Point', u'coordinates': [0, 0]})
+
+def test_python_extended_constructor():
+ context = mapnik.Context()
+ context.push('foo')
+ context.push('foo')
+ f = mapnik.Feature(context,1)
+ wkt = 'POLYGON ((35 10, 10 20, 15 40, 45 45, 35 10),(20 30, 35 35, 30 20, 20 30))'
+ f.geometry = mapnik.Geometry.from_wkt(wkt)
+ f['foo'] = 'bar'
+ eq_(f['foo'], 'bar')
+ eq_(f.envelope(),mapnik.Box2d(10.0,10.0,45.0,45.0))
+ # reset
+ f['foo'] = u"avión"
+ eq_(f['foo'], u"avión")
+ f['foo'] = 1.4
+ eq_(f['foo'], 1.4)
+ f['foo'] = True
+ eq_(f['foo'], True)
+
+def test_add_geom_wkb():
+# POLYGON ((30 10, 10 20, 20 40, 40 40, 30 10))
+ wkb = '010300000001000000050000000000000000003e4000000000000024400000000000002440000000000000344000000000000034400000000000004440000000000000444000000000000044400000000000003e400000000000002440'
+ geometry = mapnik.Geometry.from_wkb(unhexlify(wkb))
+ eq_(geometry.is_valid(), True)
+ eq_(geometry.is_simple(), True)
+ eq_(geometry.envelope(), mapnik.Box2d(10.0,10.0,40.0,40.0))
+ geometry.correct()
+ # valid after calling correct
+ eq_(geometry.is_valid(), True)
+
+def test_feature_expression_evaluation():
+ context = mapnik.Context()
+ context.push('name')
+ f = mapnik.Feature(context,1)
+ f['name'] = 'a'
+ eq_(f['name'],u'a')
+ expr = mapnik.Expression("[name]='a'")
+ evaluated = expr.evaluate(f)
+ eq_(evaluated,True)
+ num_attributes = len(f)
+ eq_(num_attributes,1)
+ eq_(f.id(),1)
+
+# https://github.com/mapnik/mapnik/issues/933
+def test_feature_expression_evaluation_missing_attr():
+ context = mapnik.Context()
+ context.push('name')
+ f = mapnik.Feature(context,1)
+ f['name'] = u'a'
+ eq_(f['name'],u'a')
+ expr = mapnik.Expression("[fielddoesnotexist]='a'")
+ eq_(f.has_key('fielddoesnotexist'),False)
+ try:
+ expr.evaluate(f)
+ except Exception, e:
+ eq_("Key does not exist" in str(e),True)
+ num_attributes = len(f)
+ eq_(num_attributes,1)
+ eq_(f.id(),1)
+
+# https://github.com/mapnik/mapnik/issues/934
+def test_feature_expression_evaluation_attr_with_spaces():
+ context = mapnik.Context()
+ context.push('name with space')
+ f = mapnik.Feature(context,1)
+ f['name with space'] = u'a'
+ eq_(f['name with space'],u'a')
+ expr = mapnik.Expression("[name with space]='a'")
+ eq_(str(expr),"([name with space]='a')")
+ eq_(expr.evaluate(f),True)
+
+# https://github.com/mapnik/mapnik/issues/2390
+@raises(RuntimeError)
+def test_feature_from_geojson():
+ ctx = mapnik.Context()
+ inline_string = """
+ {
+ "geometry" : {
+ "coordinates" : [ 0,0 ]
+ "type" : "Point"
+ },
+ "type" : "Feature",
+ "properties" : {
+ "this":"that"
+ "known":"nope because missing comma"
+ }
+ }
+ """
+ mapnik.Feature.from_geojson(inline_string,ctx)
+
+if __name__ == "__main__":
+ exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/filter_test.py b/test/python_tests/filter_test.py
new file mode 100644
index 0000000..34845ce
--- /dev/null
+++ b/test/python_tests/filter_test.py
@@ -0,0 +1,451 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from nose.tools import eq_,raises
+from utilities import run_all
+import mapnik
+
+if hasattr(mapnik,'Expression'):
+ mapnik.Filter = mapnik.Expression
+
+map_ = '''<Map>
+ <Style name="s">
+ <Rule>
+ <Filter><![CDATA[(([region]>=0) and ([region]<=50))]]></Filter>
+ </Rule>
+ <Rule>
+ <Filter><![CDATA[([region]>=0) and ([region]<=50)]]></Filter>
+ </Rule>
+ <Rule>
+ <Filter>
+
+ <![CDATA[
+
+ ([region] >= 0)
+
+ and
+
+ ([region] <= 50)
+ ]]>
+
+ </Filter>
+ </Rule>
+ <Rule>
+ <Filter>([region]&gt;=0) and ([region]&lt;=50)</Filter>
+ </Rule>
+ <Rule>
+ <Filter>
+ ([region] &gt;= 0)
+ and
+ ([region] &lt;= 50)
+ </Filter>
+ </Rule>
+
+ </Style>
+ <Style name="s2" filter-mode="first">
+ <Rule>
+ </Rule>
+ <Rule>
+ </Rule>
+ </Style>
+</Map>'''
+
+def test_filter_init():
+ m = mapnik.Map(1,1)
+ mapnik.load_map_from_string(m,map_)
+ filters = []
+ filters.append(mapnik.Filter("([region]>=0) and ([region]<=50)"))
+ filters.append(mapnik.Filter("(([region]>=0) and ([region]<=50))"))
+ filters.append(mapnik.Filter("((([region]>=0) and ([region]<=50)))"))
+ filters.append(mapnik.Filter('((([region]>=0) and ([region]<=50)))'))
+ filters.append(mapnik.Filter('''((([region]>=0) and ([region]<=50)))'''))
+ filters.append(mapnik.Filter('''
+ ((([region]>=0)
+ and
+ ([region]<=50)))
+ '''))
+ filters.append(mapnik.Filter('''
+ ([region]>=0)
+ and
+ ([region]<=50)
+ '''))
+ filters.append(mapnik.Filter('''
+ ([region]
+ >=
+ 0)
+ and
+ ([region]
+ <=
+ 50)
+ '''))
+
+ s = m.find_style('s')
+
+ for r in s.rules:
+ filters.append(r.filter)
+
+ first = filters[0]
+ for f in filters:
+ eq_(str(first),str(f))
+
+ s = m.find_style('s2')
+
+ eq_(s.filter_mode,mapnik.filter_mode.FIRST)
+
+
+def test_geometry_type_eval():
+ # clashing field called 'mapnik::geometry'
+ context2 = mapnik.Context()
+ context2.push('mapnik::geometry_type')
+ f = mapnik.Feature(context2,0)
+ f["mapnik::geometry_type"] = 'sneaky'
+ expr = mapnik.Expression("[mapnik::geometry_type]")
+ eq_(expr.evaluate(f),0)
+
+ expr = mapnik.Expression("[mapnik::geometry_type]")
+ context = mapnik.Context()
+
+ # no geometry
+ f = mapnik.Feature(context,0)
+ eq_(expr.evaluate(f),0)
+ eq_(mapnik.Expression("[mapnik::geometry_type]=0").evaluate(f),True)
+
+ # POINT = 1
+ f = mapnik.Feature(context,0)
+ f.geometry = mapnik.Geometry.from_wkt('POINT(10 40)')
+ eq_(expr.evaluate(f),1)
+ eq_(mapnik.Expression("[mapnik::geometry_type]=point").evaluate(f),True)
+
+ # LINESTRING = 2
+ f = mapnik.Feature(context,0)
+ f.geometry = mapnik.Geometry.from_wkt('LINESTRING (30 10, 10 30, 40 40)')
+ eq_(expr.evaluate(f),2)
+ eq_(mapnik.Expression("[mapnik::geometry_type] = linestring").evaluate(f),True)
+
+ # POLYGON = 3
+ f = mapnik.Feature(context,0)
+ f.geometry = mapnik.Geometry.from_wkt('POLYGON ((30 10, 10 20, 20 40, 40 40, 30 10))')
+ eq_(expr.evaluate(f),3)
+ eq_(mapnik.Expression("[mapnik::geometry_type] = polygon").evaluate(f),True)
+
+ # COLLECTION = 4
+ f = mapnik.Feature(context,0)
+ geom = mapnik.Geometry.from_wkt('GEOMETRYCOLLECTION(POLYGON((1 1,2 1,2 2,1 2,1 1)),POINT(2 3),LINESTRING(2 3,3 4))')
+ f.geometry = geom;
+ eq_(expr.evaluate(f),4)
+ eq_(mapnik.Expression("[mapnik::geometry_type] = collection").evaluate(f),True)
+
+def test_regex_match():
+ context = mapnik.Context()
+ context.push('name')
+ f = mapnik.Feature(context,0)
+ f["name"] = 'test'
+ expr = mapnik.Expression("[name].match('test')")
+ eq_(expr.evaluate(f),True) # 1 == True
+
+def test_unicode_regex_match():
+ context = mapnik.Context()
+ context.push('name')
+ f = mapnik.Feature(context,0)
+ f["name"] = 'Québec'
+ expr = mapnik.Expression("[name].match('Québec')")
+ eq_(expr.evaluate(f),True) # 1 == True
+
+def test_regex_replace():
+ context = mapnik.Context()
+ context.push('name')
+ f = mapnik.Feature(context,0)
+ f["name"] = 'test'
+ expr = mapnik.Expression("[name].replace('(\B)|( )','$1 ')")
+ eq_(expr.evaluate(f),'t e s t')
+
+def test_unicode_regex_replace_to_str():
+ expr = mapnik.Expression("[name].replace('(\B)|( )','$1 ')")
+ eq_(str(expr),"[name].replace('(\B)|( )','$1 ')")
+
+def test_unicode_regex_replace():
+ context = mapnik.Context()
+ context.push('name')
+ f = mapnik.Feature(context,0)
+ f["name"] = 'Québec'
+ expr = mapnik.Expression("[name].replace('(\B)|( )','$1 ')")
+ # will fail if -DBOOST_REGEX_HAS_ICU is not defined
+ eq_(expr.evaluate(f), u'Q u é b e c')
+
+def test_float_precision():
+ context = mapnik.Context()
+ context.push('num')
+ f = mapnik.Feature(context,0)
+ f["num1"] = 1.0000
+ f["num2"] = 1.0001
+ eq_(f["num1"],1.0000)
+ eq_(f["num2"],1.0001)
+ expr = mapnik.Expression("[num1] = 1.0000")
+ eq_(expr.evaluate(f),True)
+ expr = mapnik.Expression("[num1].match('1')")
+ eq_(expr.evaluate(f),True)
+ expr = mapnik.Expression("[num2] = 1.0001")
+ eq_(expr.evaluate(f),True)
+ expr = mapnik.Expression("[num2].match('1.0001')")
+ eq_(expr.evaluate(f),True)
+
+def test_string_matching_on_precision():
+ context = mapnik.Context()
+ context.push('num')
+ f = mapnik.Feature(context,0)
+ f["num"] = "1.0000"
+ eq_(f["num"],"1.0000")
+ expr = mapnik.Expression("[num].match('.*(^0|00)$')")
+ eq_(expr.evaluate(f),True)
+
+def test_creation_of_null_value():
+ context = mapnik.Context()
+ context.push('nv')
+ f = mapnik.Feature(context,0)
+ f["nv"] = None
+ eq_(f["nv"],None)
+ eq_(f["nv"] is None,True)
+ # test boolean
+ f["nv"] = 0
+ eq_(f["nv"],0)
+ eq_(f["nv"] is not None,True)
+
+def test_creation_of_bool():
+ context = mapnik.Context()
+ context.push('bool')
+ f = mapnik.Feature(context,0)
+ f["bool"] = True
+ eq_(f["bool"],True)
+ # TODO - will become int of 1 do to built in boost python conversion
+ # https://github.com/mapnik/mapnik/issues/1873
+ eq_(isinstance(f["bool"],bool) or isinstance(f["bool"],long),True)
+ f["bool"] = False
+ eq_(f["bool"],False)
+ eq_(isinstance(f["bool"],bool) or isinstance(f["bool"],long),True)
+ # test NoneType
+ f["bool"] = None
+ eq_(f["bool"],None)
+ eq_(isinstance(f["bool"],bool) or isinstance(f["bool"],long),False)
+ # test integer
+ f["bool"] = 0
+ eq_(f["bool"],0)
+ # https://github.com/mapnik/mapnik/issues/1873
+ # ugh, boost_python's built into converter does not work right
+ #eq_(isinstance(f["bool"],bool),False)
+
+null_equality = [
+ ['hello',False,unicode],
+ [u'',False,unicode],
+ [0,False,long],
+ [123,False,long],
+ [0.0,False,float],
+ [123.123,False,float],
+ [.1,False,float],
+ [False,False,long], # TODO - should become bool: https://github.com/mapnik/mapnik/issues/1873
+ [True,False,long], # TODO - should become bool: https://github.com/mapnik/mapnik/issues/1873
+ [None,True,None],
+ [2147483648,False,long],
+ [922337203685477580,False,long]
+]
+
+def test_expressions_with_null_equality():
+ for eq in null_equality:
+ context = mapnik.Context()
+ f = mapnik.Feature(context,0)
+ f["prop"] = eq[0]
+ eq_(f["prop"],eq[0])
+ if eq[0] is None:
+ eq_(f["prop"] is None, True)
+ else:
+ eq_(isinstance(f['prop'],eq[2]),True,'%s is not an instance of %s' % (f['prop'],eq[2]))
+ expr = mapnik.Expression("[prop] = null")
+ eq_(expr.evaluate(f),eq[1])
+ expr = mapnik.Expression("[prop] is null")
+ eq_(expr.evaluate(f),eq[1])
+
+def test_expressions_with_null_equality2():
+ for eq in null_equality:
+ context = mapnik.Context()
+ f = mapnik.Feature(context,0)
+ f["prop"] = eq[0]
+ eq_(f["prop"],eq[0])
+ if eq[0] is None:
+ eq_(f["prop"] is None, True)
+ else:
+ eq_(isinstance(f['prop'],eq[2]),True,'%s is not an instance of %s' % (f['prop'],eq[2]))
+ # TODO - support `is not` syntax:
+ # https://github.com/mapnik/mapnik/issues/796
+ expr = mapnik.Expression("not [prop] is null")
+ eq_(expr.evaluate(f),not eq[1])
+ # https://github.com/mapnik/mapnik/issues/1642
+ expr = mapnik.Expression("[prop] != null")
+ eq_(expr.evaluate(f),not eq[1])
+
+truthyness = [
+ [u'hello',True,unicode],
+ [u'',False,unicode],
+ [0,False,long],
+ [123,True,long],
+ [0.0,False,float],
+ [123.123,True,float],
+ [.1,True,float],
+ [False,False,long], # TODO - should become bool: https://github.com/mapnik/mapnik/issues/1873
+ [True,True,long], # TODO - should become bool: https://github.com/mapnik/mapnik/issues/1873
+ [None,False,None],
+ [2147483648,True,long],
+ [922337203685477580,True,long]
+]
+
+def test_expressions_for_thruthyness():
+ context = mapnik.Context()
+ for eq in truthyness:
+ f = mapnik.Feature(context,0)
+ f["prop"] = eq[0]
+ eq_(f["prop"],eq[0])
+ if eq[0] is None:
+ eq_(f["prop"] is None, True)
+ else:
+ eq_(isinstance(f['prop'],eq[2]),True,'%s is not an instance of %s' % (f['prop'],eq[2]))
+ expr = mapnik.Expression("[prop]")
+ eq_(expr.to_bool(f),eq[1])
+ expr = mapnik.Expression("not [prop]")
+ eq_(expr.to_bool(f),not eq[1])
+ expr = mapnik.Expression("! [prop]")
+ eq_(expr.to_bool(f),not eq[1])
+ # also test if feature does not have property at all
+ f2 = mapnik.Feature(context,1)
+ # no property existing will return value_null since
+ # https://github.com/mapnik/mapnik/commit/562fada9d0f680f59b2d9f396c95320a0d753479#include/mapnik/feature.hpp
+ eq_(f2["prop"] is None,True)
+ expr = mapnik.Expression("[prop]")
+ eq_(expr.evaluate(f2),None)
+ eq_(expr.to_bool(f2),False)
+
+# https://github.com/mapnik/mapnik/issues/1859
+def test_if_null_and_empty_string_are_equal():
+ context = mapnik.Context()
+ f = mapnik.Feature(context,0)
+ f["empty"] = u""
+ f["null"] = None
+ # ensure base assumptions are good
+ eq_(mapnik.Expression("[empty] = ''").to_bool(f),True)
+ eq_(mapnik.Expression("[null] = null").to_bool(f),True)
+ eq_(mapnik.Expression("[empty] != ''").to_bool(f),False)
+ eq_(mapnik.Expression("[null] != null").to_bool(f),False)
+ # now test expected behavior
+ eq_(mapnik.Expression("[null] = ''").to_bool(f),False)
+ eq_(mapnik.Expression("[empty] = null").to_bool(f),False)
+ eq_(mapnik.Expression("[empty] != null").to_bool(f),True)
+ # this one is the back compatibility shim
+ eq_(mapnik.Expression("[null] != ''").to_bool(f),False)
+
+def test_filtering_nulls_and_empty_strings():
+ context = mapnik.Context()
+ f = mapnik.Feature(context,0)
+ f["prop"] = u"hello"
+ eq_(f["prop"],u"hello")
+ eq_(mapnik.Expression("[prop]").to_bool(f),True)
+ eq_(mapnik.Expression("! [prop]").to_bool(f),False)
+ eq_(mapnik.Expression("[prop] != null").to_bool(f),True)
+ eq_(mapnik.Expression("[prop] != ''").to_bool(f),True)
+ eq_(mapnik.Expression("[prop] != null and [prop] != ''").to_bool(f),True)
+ eq_(mapnik.Expression("[prop] != null or [prop] != ''").to_bool(f),True)
+ f["prop2"] = u""
+ eq_(f["prop2"],u"")
+ eq_(mapnik.Expression("[prop2]").to_bool(f),False)
+ eq_(mapnik.Expression("! [prop2]").to_bool(f),True)
+ eq_(mapnik.Expression("[prop2] != null").to_bool(f),True)
+ eq_(mapnik.Expression("[prop2] != ''").to_bool(f),False)
+ eq_(mapnik.Expression("[prop2] = ''").to_bool(f),True)
+ eq_(mapnik.Expression("[prop2] != null or [prop2] != ''").to_bool(f),True)
+ eq_(mapnik.Expression("[prop2] != null and [prop2] != ''").to_bool(f),False)
+ f["prop3"] = None
+ eq_(f["prop3"],None)
+ eq_(mapnik.Expression("[prop3]").to_bool(f),False)
+ eq_(mapnik.Expression("! [prop3]").to_bool(f),True)
+ eq_(mapnik.Expression("[prop3] != null").to_bool(f),False)
+ eq_(mapnik.Expression("[prop3] = null").to_bool(f),True)
+
+ # https://github.com/mapnik/mapnik/issues/1859
+ #eq_(mapnik.Expression("[prop3] != ''").to_bool(f),True)
+ eq_(mapnik.Expression("[prop3] != ''").to_bool(f),False)
+
+ eq_(mapnik.Expression("[prop3] = ''").to_bool(f),False)
+
+ # https://github.com/mapnik/mapnik/issues/1859
+ #eq_(mapnik.Expression("[prop3] != null or [prop3] != ''").to_bool(f),True)
+ eq_(mapnik.Expression("[prop3] != null or [prop3] != ''").to_bool(f),False)
+
+ eq_(mapnik.Expression("[prop3] != null and [prop3] != ''").to_bool(f),False)
+ # attr not existing should behave the same as prop3
+ eq_(mapnik.Expression("[prop4]").to_bool(f),False)
+ eq_(mapnik.Expression("! [prop4]").to_bool(f),True)
+ eq_(mapnik.Expression("[prop4] != null").to_bool(f),False)
+ eq_(mapnik.Expression("[prop4] = null").to_bool(f),True)
+
+ # https://github.com/mapnik/mapnik/issues/1859
+ ##eq_(mapnik.Expression("[prop4] != ''").to_bool(f),True)
+ eq_(mapnik.Expression("[prop4] != ''").to_bool(f),False)
+
+ eq_(mapnik.Expression("[prop4] = ''").to_bool(f),False)
+
+ # https://github.com/mapnik/mapnik/issues/1859
+ ##eq_(mapnik.Expression("[prop4] != null or [prop4] != ''").to_bool(f),True)
+ eq_(mapnik.Expression("[prop4] != null or [prop4] != ''").to_bool(f),False)
+
+ eq_(mapnik.Expression("[prop4] != null and [prop4] != ''").to_bool(f),False)
+ f["prop5"] = False
+ eq_(f["prop5"],False)
+ eq_(mapnik.Expression("[prop5]").to_bool(f),False)
+ eq_(mapnik.Expression("! [prop5]").to_bool(f),True)
+ eq_(mapnik.Expression("[prop5] != null").to_bool(f),True)
+ eq_(mapnik.Expression("[prop5] = null").to_bool(f),False)
+ eq_(mapnik.Expression("[prop5] != ''").to_bool(f),True)
+ eq_(mapnik.Expression("[prop5] = ''").to_bool(f),False)
+ eq_(mapnik.Expression("[prop5] != null or [prop5] != ''").to_bool(f),True)
+ eq_(mapnik.Expression("[prop5] != null and [prop5] != ''").to_bool(f),True)
+ # note, we need to do [prop5] != 0 here instead of false due to this bug:
+ # https://github.com/mapnik/mapnik/issues/1873
+ eq_(mapnik.Expression("[prop5] != null and [prop5] != '' and [prop5] != 0").to_bool(f),False)
+
+# https://github.com/mapnik/mapnik/issues/1872
+def test_falseyness_comparision():
+ context = mapnik.Context()
+ f = mapnik.Feature(context,0)
+ f["prop"] = 0
+ eq_(mapnik.Expression("[prop]").to_bool(f),False)
+ eq_(mapnik.Expression("[prop] = false").to_bool(f),True)
+ eq_(mapnik.Expression("not [prop] != false").to_bool(f),True)
+ eq_(mapnik.Expression("not [prop] = true").to_bool(f),True)
+ eq_(mapnik.Expression("[prop] = true").to_bool(f),False)
+ eq_(mapnik.Expression("[prop] != true").to_bool(f),True)
+
+# https://github.com/mapnik/mapnik/issues/1806, fixed by https://github.com/mapnik/mapnik/issues/1872
+def test_truthyness_comparision():
+ context = mapnik.Context()
+ f = mapnik.Feature(context,0)
+ f["prop"] = 1
+ eq_(mapnik.Expression("[prop]").to_bool(f),True)
+ eq_(mapnik.Expression("[prop] = false").to_bool(f),False)
+ eq_(mapnik.Expression("not [prop] != false").to_bool(f),False)
+ eq_(mapnik.Expression("not [prop] = true").to_bool(f),False)
+ eq_(mapnik.Expression("[prop] = true").to_bool(f),True)
+ eq_(mapnik.Expression("[prop] != true").to_bool(f),False)
+
+def test_division_by_zero():
+ expr = mapnik.Expression('[a]/[b]')
+ c = mapnik.Context()
+ c.push('a')
+ c.push('b')
+ f = mapnik.Feature(c,0);
+ f['a'] = 1
+ f['b'] = 0
+ eq_(expr.evaluate(f),None)
+
+@raises(RuntimeError)
+def test_invalid_syntax1():
+ mapnik.Expression('abs()')
+
+
+if __name__ == "__main__":
+ exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/fontset_test.py b/test/python_tests/fontset_test.py
new file mode 100644
index 0000000..ee8fd7d
--- /dev/null
+++ b/test/python_tests/fontset_test.py
@@ -0,0 +1,41 @@
+#!/usr/bin/env python
+
+from nose.tools import eq_
+from utilities import execution_path, run_all
+import os, mapnik
+
+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_loading_fontset_from_map():
+ m = mapnik.Map(256,256)
+ mapnik.load_map(m,'../data/good_maps/fontset.xml',True)
+ fs = m.find_fontset('book-fonts')
+ eq_(len(fs.names),2)
+ eq_(list(fs.names),['DejaVu Sans Book','DejaVu Sans Oblique'])
+
+# def test_loading_fontset_from_python():
+# m = mapnik.Map(256,256)
+# fset = mapnik.FontSet('foo')
+# fset.add_face_name('Comic Sans')
+# fset.add_face_name('Papyrus')
+# eq_(fset.name,'foo')
+# fset.name = 'my-set'
+# eq_(fset.name,'my-set')
+# m.append_fontset('my-set', fset)
+# sty = mapnik.Style()
+# rule = mapnik.Rule()
+# tsym = mapnik.TextSymbolizer()
+# eq_(tsym.fontset,None)
+# tsym.fontset = fset
+# rule.symbols.append(tsym)
+# sty.rules.append(rule)
+# m.append_style('Style',sty)
+# serialized_map = mapnik.save_map_to_string(m)
+# eq_('fontset-name="my-set"' in serialized_map,True)
+
+if __name__ == "__main__":
+ setup()
+ exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/geojson_plugin_test.py b/test/python_tests/geojson_plugin_test.py
new file mode 100644
index 0000000..ef7c74a
--- /dev/null
+++ b/test/python_tests/geojson_plugin_test.py
@@ -0,0 +1,126 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from nose.tools import eq_,assert_almost_equal
+from utilities import execution_path, run_all
+import os, mapnik
+try:
+ import json
+except ImportError:
+ import simplejson as json
+
+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('.'))
+
+if 'geojson' in mapnik.DatasourceCache.plugin_names():
+
+ def test_geojson_init():
+ ds = mapnik.Datasource(type='geojson',file='../data/json/escaped.geojson')
+ e = ds.envelope()
+ assert_almost_equal(e.minx, -81.705583, places=7)
+ assert_almost_equal(e.miny, 41.480573, places=6)
+ assert_almost_equal(e.maxx, -81.705583, places=5)
+ assert_almost_equal(e.maxy, 41.480573, places=3)
+
+ def test_geojson_properties():
+ ds = mapnik.Datasource(type='geojson',file='../data/json/escaped.geojson')
+ f = ds.features_at_point(ds.envelope().center()).features[0]
+ eq_(len(ds.fields()),7)
+ desc = ds.describe()
+ eq_(desc['geometry_type'],mapnik.DataGeometryType.Point)
+
+ eq_(f['name'], u'Test')
+ eq_(f['int'], 1)
+ eq_(f['description'], u'Test: \u005C')
+ eq_(f['spaces'], u'this has spaces')
+ eq_(f['double'], 1.1)
+ eq_(f['boolean'], True)
+ eq_(f['NOM_FR'], u'Qu\xe9bec')
+ eq_(f['NOM_FR'], u'Québec')
+
+ ds = mapnik.Datasource(type='geojson',file='../data/json/escaped.geojson')
+ f = ds.all_features()[0]
+ eq_(len(ds.fields()),7)
+
+ desc = ds.describe()
+ eq_(desc['geometry_type'],mapnik.DataGeometryType.Point)
+
+ eq_(f['name'], u'Test')
+ eq_(f['int'], 1)
+ eq_(f['description'], u'Test: \u005C')
+ eq_(f['spaces'], u'this has spaces')
+ eq_(f['double'], 1.1)
+ eq_(f['boolean'], True)
+ eq_(f['NOM_FR'], u'Qu\xe9bec')
+ eq_(f['NOM_FR'], u'Québec')
+ def test_large_geojson_properties():
+ ds = mapnik.Datasource(type='geojson',file='../data/json/escaped.geojson',cache_features = False)
+ f = ds.features_at_point(ds.envelope().center()).features[0]
+ eq_(len(ds.fields()),7)
+ desc = ds.describe()
+ eq_(desc['geometry_type'],mapnik.DataGeometryType.Point)
+
+ eq_(f['name'], u'Test')
+ eq_(f['int'], 1)
+ eq_(f['description'], u'Test: \u005C')
+ eq_(f['spaces'], u'this has spaces')
+ eq_(f['double'], 1.1)
+ eq_(f['boolean'], True)
+ eq_(f['NOM_FR'], u'Qu\xe9bec')
+ eq_(f['NOM_FR'], u'Québec')
+
+ ds = mapnik.Datasource(type='geojson',file='../data/json/escaped.geojson')
+ f = ds.all_features()[0]
+ eq_(len(ds.fields()),7)
+
+ desc = ds.describe()
+ eq_(desc['geometry_type'],mapnik.DataGeometryType.Point)
+
+ eq_(f['name'], u'Test')
+ eq_(f['int'], 1)
+ eq_(f['description'], u'Test: \u005C')
+ eq_(f['spaces'], u'this has spaces')
+ eq_(f['double'], 1.1)
+ eq_(f['boolean'], True)
+ eq_(f['NOM_FR'], u'Qu\xe9bec')
+ eq_(f['NOM_FR'], u'Québec')
+
+ def test_geojson_from_in_memory_string():
+ # will silently fail since it is a geometry and needs to be a featurecollection.
+ #ds = mapnik.Datasource(type='geojson',inline='{"type":"LineString","coordinates":[[0,0],[10,10]]}')
+ # works since it is a featurecollection
+ ds = mapnik.Datasource(type='geojson',inline='{ "type":"FeatureCollection", "features": [ { "type":"Feature", "properties":{"name":"test"}, "geometry": { "type":"LineString","coordinates":[[0,0],[10,10]] } } ]}')
+ eq_(len(ds.fields()),1)
+ f = ds.all_features()[0]
+ desc = ds.describe()
+ eq_(desc['geometry_type'],mapnik.DataGeometryType.LineString)
+ eq_(f['name'], u'test')
+
+# @raises(RuntimeError)
+ def test_that_nonexistant_query_field_throws(**kwargs):
+ ds = mapnik.Datasource(type='geojson',file='../data/json/escaped.geojson')
+ eq_(len(ds.fields()),7)
+ # TODO - this sorting is messed up
+ #eq_(ds.fields(),['name', 'int', 'double', 'description', 'boolean', 'NOM_FR'])
+ #eq_(ds.field_types(),['str', 'int', 'float', 'str', 'bool', 'str'])
+# TODO - should geojson plugin throw like others?
+# query = mapnik.Query(ds.envelope())
+# for fld in ds.fields():
+# query.add_property_name(fld)
+# # also add an invalid one, triggering throw
+# query.add_property_name('bogus')
+# fs = ds.features(query)
+
+ def test_parsing_feature_collection_with_top_level_properties():
+ ds = mapnik.Datasource(type='geojson',file='../data/json/feature_collection_level_properties.json')
+ f = ds.all_features()[0]
+
+ desc = ds.describe()
+ eq_(desc['geometry_type'],mapnik.DataGeometryType.Point)
+ eq_(f['feat_name'], u'feat_value')
+
+if __name__ == "__main__":
+ setup()
+ exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/geometry_io_test.py b/test/python_tests/geometry_io_test.py
new file mode 100644
index 0000000..58e4f36
--- /dev/null
+++ b/test/python_tests/geometry_io_test.py
@@ -0,0 +1,273 @@
+#encoding: utf8
+
+from nose.tools import eq_,raises
+import os
+from utilities import execution_path, run_all
+import mapnik
+from binascii import unhexlify
+
+try:
+ import json
+except ImportError:
+ import simplejson as json
+
+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('.'))
+
+wkts = [
+ [mapnik.GeometryType.Point,"POINT(30 10)", "01010000000000000000003e400000000000002440"],
+ [mapnik.GeometryType.Point,"POINT(30.0 10.0)", "01010000000000000000003e400000000000002440"],
+ [mapnik.GeometryType.Point,"POINT(30.1 10.1)", "01010000009a99999999193e403333333333332440"],
+ [mapnik.GeometryType.LineString,"LINESTRING(30 10,10 30,40 40)", "0102000000030000000000000000003e40000000000000244000000000000024400000000000003e4000000000000044400000000000004440"],
+ [mapnik.GeometryType.Polygon,"POLYGON((30 10,10 20,20 40,40 40,30 10))", "010300000001000000050000000000000000003e4000000000000024400000000000002440000000000000344000000000000034400000000000004440000000000000444000000000000044400000000000003e400000000000002440"],
+ [mapnik.GeometryType.Polygon,"POLYGON((35 10,10 20,15 40,45 45,35 10),(20 30,35 35,30 20,20 30))","0103000000020000000500000000000000008041400000000000002440000000000000244000000000000034400000000000002e40000000000000444000000000008046400000000000804640000000000080414000000000000024400400000000000000000034400000000000003e40000000000080414000000000008041400000000000003e40000000000000344000000000000034400000000000003e40"],
+ [mapnik.GeometryType.MultiPoint,"MULTIPOINT((10 40),(40 30),(20 20),(30 10))","010400000004000000010100000000000000000024400000000000004440010100000000000000000044400000000000003e4001010000000000000000003440000000000000344001010000000000000000003e400000000000002440"],
+ [mapnik.GeometryType.MultiLineString,"MULTILINESTRING((10 10,20 20,10 40),(40 40,30 30,40 20,30 10))","010500000002000000010200000003000000000000000000244000000000000024400000000000003440000000000000344000000000000024400000000000004440010200000004000000000000000000444000000000000044400000000000003e400000000000003e40000000000000444000000000000034400000000000003e400000000000002440"],
+ [mapnik.GeometryType.MultiPolygon,"MULTIPOLYGON(((30 20,10 40,45 40,30 20)),((15 5,40 10,10 20,5 10,15 5)))","010600000002000000010300000001000000040000000000000000003e40000000000000344000000000000024400000000000004440000000000080464000000000000044400000000000003e400000000000003440010300000001000000050000000000000000002e4000000000000014400000000000004440000000000000244000000000000024400000000000003440000000000000144000000000000024400000000000002e400000000000001440"],
+ [mapnik.GeometryType.MultiPolygon,"MULTIPOLYGON(((40 40,20 45,45 30,40 40)),((20 35,45 20,30 5,10 10,10 30,20 35),(30 20,20 25,20 15,30 20)))","01060000000200000001030000000100000004000000000000000000444000000000000044400000000000003440000000000080464000000000008046400000000000003e40000000000000444000000000000044400103000000020000000600000000000000000034400000000000804140000000000080464000000000000034400000000000003e40000000000000144000000000000024400000000000002440000000000000244000 [...]
+ [mapnik.GeometryType.GeometryCollection,"GEOMETRYCOLLECTION(POLYGON((1 1,2 1,2 2,1 2,1 1)),POINT(2 3),LINESTRING(2 3,3 4))","01070000000300000001030000000100000005000000000000000000f03f000000000000f03f0000000000000040000000000000f03f00000000000000400000000000000040000000000000f03f0000000000000040000000000000f03f000000000000f03f0101000000000000000000004000000000000008400102000000020000000000000000000040000000000000084000000000000008400000000000001040"],
+ [mapnik.GeometryType.Polygon,"POLYGON((-178.32319 71.518365,-178.321586 71.518439,-178.259635 71.510688,-178.304862 71.513129,-178.32319 71.518365),(-178.32319 71.518365,-178.341544 71.517524,-178.32244 71.505439,-178.215323 71.478034,-178.193473 71.47663,-178.147757 71.485175,-178.124442 71.481879,-178.005729 71.448615,-178.017203 71.441413,-178.054191 71.428778,-178.047049 71.425727,-178.033439 71.417792,-178.026236 71.415107,-178.030082 71.413459,-178.039908 71.40766,-177.970878 7 [...]
+ [mapnik.GeometryType.MultiPolygon,"MULTIPOLYGON(((-178.32319 71.518365,-178.321586 71.518439,-178.259635 71.510688,-178.304862 71.513129,-178.32319 71.518365)),((-178.32319 71.518365,-178.341544 71.517524,-178.32244 71.505439,-178.215323 71.478034,-178.193473 71.47663,-178.147757 71.485175,-178.124442 71.481879,-178.005729 71.448615,-178.017203 71.441413,-178.054191 71.428778,-178.047049 71.425727,-178.033439 71.417792,-178.026236 71.415107,-178.030082 71.413459,-178.039908 71.40766, [...]
+]
+
+
+geojson = [
+ [mapnik.GeometryType.Point,'{"type":"Point","coordinates":[30,10]}'],
+ [mapnik.GeometryType.Point,'{"type":"Point","coordinates":[30.0,10.0]}'],
+ [mapnik.GeometryType.Point,'{"type":"Point","coordinates":[30.1,10.1]}'],
+ [mapnik.GeometryType.LineString,'{"type":"LineString","coordinates":[[30.0,10.0],[10.0,30.0],[40.0,40.0]]}'],
+ [mapnik.GeometryType.Polygon,'{"type":"Polygon","coordinates":[[[30.0,10.0],[10.0,20.0],[20.0,40.0],[40.0,40.0],[30.0,10.0]]]}'],
+ [mapnik.GeometryType.Polygon,'{"type":"Polygon","coordinates":[[[35.0,10.0],[10.0,20.0],[15.0,40.0],[45.0,45.0],[35.0,10.0]],[[20.0,30.0],[35.0,35.0],[30.0,20.0],[20.0,30.0]]]}'],
+ [mapnik.GeometryType.MultiPoint,'{"type":"MultiPoint","coordinates":[[10.0,40.0],[40.0,30.0],[20.0,20.0],[30.0,10.0]]}'],
+ [mapnik.GeometryType.MultiLineString,'{"type":"MultiLineString","coordinates":[[[10.0,10.0],[20.0,20.0],[10.0,40.0]],[[40.0,40.0],[30.0,30.0],[40.0,20.0],[30.0,10.0]]]}'],
+ [mapnik.GeometryType.MultiPolygon,'{"type":"MultiPolygon","coordinates":[[[[30.0,20.0],[10.0,40.0],[45.0,40.0],[30.0,20.0]]],[[[15.0,5.0],[40.0,10.0],[10.0,20.0],[5.0,10.0],[15.0,5.0]]]]}'],
+ [mapnik.GeometryType.MultiPolygon,'{"type":"MultiPolygon","coordinates":[[[[40.0,40.0],[20.0,45.0],[45.0,30.0],[40.0,40.0]]],[[[20.0,35.0],[45.0,20.0],[30.0,5.0],[10.0,10.0],[10.0,30.0],[20.0,35.0]],[[30.0,20.0],[20.0,25.0],[20.0,15.0],[30.0,20.0]]]]}'],
+ [mapnik.GeometryType.GeometryCollection,'{"type":"GeometryCollection","geometries":[{"type":"Polygon","coordinates":[[[1.0,1.0],[2.0,1.0],[2.0,2.0],[1.0,2.0],[1.0,1.0]]]},{"type":"Point","coordinates":[2.0,3.0]},{"type":"LineString","coordinates":[[2.0,3.0],[3.0,4.0]]}]}'],
+ [mapnik.GeometryType.Polygon,'{"type":"Polygon","coordinates":[[[-178.32319,71.518365],[-178.321586,71.518439],[-178.259635,71.510688],[-178.304862,71.513129],[-178.32319,71.518365]],[[-178.32319,71.518365],[-178.341544,71.517524],[-178.32244,71.505439],[-178.215323,71.478034],[-178.193473,71.47663],[-178.147757,71.485175],[-178.124442,71.481879],[-178.005729,71.448615],[-178.017203,71.441413],[-178.054191,71.428778],[-178.047049,71.425727],[-178.033439,71.417792],[-178.026236,71.415 [...]
+ [mapnik.GeometryType.MultiPolygon,'{"type":"MultiPolygon","coordinates":[[[[-178.32319,71.518365],[-178.321586,71.518439],[-178.259635,71.510688],[-178.304862,71.513129],[-178.32319,71.518365]]],[[[-178.32319,71.518365],[-178.341544,71.517524],[-178.32244,71.505439],[-178.215323,71.478034],[-178.193473,71.47663],[-178.147757,71.485175],[-178.124442,71.481879],[-178.005729,71.448615],[-178.017203,71.441413],[-178.054191,71.428778],[-178.047049,71.425727],[-178.033439,71.417792],[-178. [...]
+]
+
+geojson_reversed = [
+ '{"coordinates":[30,10],"type":"Point"}',
+ '{"coordinates":[30.0,10.0],"type":"Point"}',
+ '{"coordinates":[30.1,10.1],"type":"Point"}',
+ '{"coordinates":[[30.0,10.0],[10.0,30.0],[40.0,40.0]],"type":"LineString"}',
+ '{"coordinates":[[[30.0,10.0],[10.0,20.0],[20.0,40.0],[40.0,40.0],[30.0,10.0]]],"type":"Polygon"}',
+ '{"coordinates":[[[35.0,10.0],[10.0,20.0],[15.0,40.0],[45.0,45.0],[35.0,10.0]],[[20.0,30.0],[35.0,35.0],[30.0,20.0],[20.0,30.0]]],"type":"Polygon"}',
+ '{"coordinates":[[10.0,40.0],[40.0,30.0],[20.0,20.0],[30.0,10.0]],"type":"MultiPoint"}',
+ '{"coordinates":[[[10.0,10.0],[20.0,20.0],[10.0,40.0]],[[40.0,40.0],[30.0,30.0],[40.0,20.0],[30.0,10.0]]],"type":"MultiLineString"}',
+ '{"coordinates":[[[[30.0,20.0],[10.0,40.0],[45.0,40.0],[30.0,20.0]]],[[[15.0,5.0],[40.0,10.0],[10.0,20.0],[5.0,10.0],[15.0,5.0]]]],"type":"MultiPolygon"}',
+ '{"coordinates":[[[[40.0,40.0],[20.0,45.0],[45.0,30.0],[40.0,40.0]]],[[[20.0,35.0],[45.0,20.0],[30.0,5.0],[10.0,10.0],[10.0,30.0],[20.0,35.0]],[[30.0,20.0],[20.0,25.0],[20.0,15.0],[30.0,20.0]]]],"type":"MultiPolygon"}',
+ '{"geometries":[{"coordinates":[[[1.0,1.0],[2.0,1.0],[2.0,2.0],[1.0,2.0],[1.0,1.0]]],"type":"Polygon"},{"coordinates":[2.0,3.0],"type":"Point"},{"coordinates":[[2.0,3.0],[3.0,4.0]],"type":"LineString"}],"type":"GeometryCollection"}',
+ '{"coordinates":[[[-178.32319,71.518365],[-178.321586,71.518439],[-178.259635,71.510688],[-178.304862,71.513129],[-178.32319,71.518365]],[[-178.32319,71.518365],[-178.341544,71.517524],[-178.32244,71.505439],[-178.215323,71.478034],[-178.193473,71.47663],[-178.147757,71.485175],[-178.124442,71.481879],[-178.005729,71.448615],[-178.017203,71.441413],[-178.054191,71.428778],[-178.047049,71.425727],[-178.033439,71.417792],[-178.026236,71.415107],[-178.030082,71.413459],[-178.039908,71.4 [...]
+ '{"coordinates":[[[[-178.32319,71.518365],[-178.321586,71.518439],[-178.259635,71.510688],[-178.304862,71.513129],[-178.32319,71.518365]]],[[[-178.32319,71.518365],[-178.341544,71.517524],[-178.32244,71.505439],[-178.215323,71.478034],[-178.193473,71.47663],[-178.147757,71.485175],[-178.124442,71.481879],[-178.005729,71.448615],[-178.017203,71.441413],[-178.054191,71.428778],[-178.047049,71.425727],[-178.033439,71.417792],[-178.026236,71.415107],[-178.030082,71.413459],[-178.039908,7 [...]
+]
+
+geojson_nulls = [
+ '{ "type": "Feature", "properties": { }, "geometry": null }',
+ '{ "type": "Feature", "properties": { }, "geometry": { "type": "Point", "coordinates": [] }}',
+ '{ "type": "Feature", "properties": { }, "geometry": { "type": "LineString", "coordinates": [ [] ] }}',
+ '{ "type": "Feature", "properties": { }, "geometry": { "type": "Polygon", "coordinates": [ [ [] ] ] } }',
+ '{ "type": "Feature", "properties": { }, "geometry": { "coordinates": [], "type": "Point" }}',
+ '{ "type": "Feature", "properties": { }, "geometry": { "coordinates": [ [] ], "type": "LineString" }}',
+ '{ "type": "Feature", "properties": { }, "geometry": { "coordinates": [ [ [] ] ], "type": "Polygon" } }',
+ '{ "type": "Feature", "properties": { }, "geometry": { "type": "MultiPoint", "coordinates": [ [] ] }}',
+ '{ "type": "Feature", "properties": { }, "geometry": { "type": "MultiPoint", "coordinates": [ [],[] ] }}',
+ '{ "type": "Feature", "properties": { }, "geometry": { "type": "MultiLineString", "coordinates": [ [] ] }}',
+ '{ "type": "Feature", "properties": { }, "geometry": { "type": "MultiLineString", "coordinates": [ [ [] ] ] }}',
+ '{ "type": "Feature", "properties": { }, "geometry": { "type": "MultiPolygon", "coordinates": [ [] ] }}',
+ '{ "type": "Feature", "properties": { }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [] ] ] }}',
+ '{ "type": "Feature", "properties": { }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [] ] ] ] }}',
+]
+
+# valid, but empty wkb's (http://trac.osgeo.org/postgis/wiki/DevWikiEmptyGeometry)
+empty_wkbs = [
+ # TODO - this is messed up: round trips as MULTIPOINT EMPTY
+ # template_postgis=# select ST_AsText(ST_GeomFromEWKB(decode(encode(ST_GeomFromText('POINT EMPTY'),'hex'),'hex')));
+ # st_astext
+ #------------------
+ # MULTIPOINT EMPTY
+ #(1 row)
+ #[ mapnik.GeometryType.Point, "Point EMPTY", '010400000000000000'],
+ [ mapnik.GeometryType.MultiPoint, "MULTIPOINT EMPTY", '010400000000000000'],
+ [ mapnik.GeometryType.LineString, "LINESTRING EMPTY", '010200000000000000'],
+ [ mapnik.GeometryType.LineString, "LINESTRING EMPTY", '010200000000000000' ],
+ [ mapnik.GeometryType.MultiLineString, "MULTILINESTRING EMPTY", '010500000000000000'],
+ [ mapnik.GeometryType.Polygon, "Polygon EMPTY", '010300000000000000'],
+ [ mapnik.GeometryType.GeometryCollection, "GEOMETRYCOLLECTION EMPTY", '010700000000000000'],
+ [ mapnik.GeometryType.GeometryCollection, "GEOMETRYCOLLECTION(LINESTRING EMPTY,LINESTRING EMPTY)", '010700000000000000'],
+ [ mapnik.GeometryType.GeometryCollection, "GEOMETRYCOLLECTION(POINT EMPTY,POINT EMPTY)", '010700000000000000'],
+]
+
+partially_empty_wkb = [
+ # TODO - currently this is not considered empty
+ # even though one part is
+ [ mapnik.GeometryType.GeometryCollection, "GEOMETRYCOLLECTION(MULTILINESTRING((10 10,20 20,10 40),(40 40,30 30,40 20,30 10)),LINESTRING EMPTY)", '010700000002000000010500000002000000010200000003000000000000000000244000000000000024400000000000003440000000000000344000000000000024400000000000004440010200000004000000000000000000444000000000000044400000000000003e400000000000003e40000000000000444000000000000034400000000000003e400000000000002440010200000000000000'],
+ [ mapnik.GeometryType.GeometryCollection, "GEOMETRYCOLLECTION(POINT EMPTY,POINT(0 0))", '010700000002000000010400000000000000010100000000000000000000000000000000000000'],
+ [ mapnik.GeometryType.GeometryCollection, "GEOMETRYCOLLECTION(POINT EMPTY,MULTIPOINT(0 0))", '010700000002000000010400000000000000010400000001000000010100000000000000000000000000000000000000'],
+]
+
+# unsupported types
+unsupported_wkb = [
+ [ "MULTIPOLYGON EMPTY", '010600000000000000'],
+ [ "TRIANGLE EMPTY", '011100000000000000'],
+ [ "CircularString EMPTY", '010800000000000000'],
+ [ "CurvePolygon EMPTY", '010A00000000000000'],
+ [ "CompoundCurve EMPTY", '010900000000000000'],
+ [ "MultiCurve EMPTY", '010B00000000000000'],
+ [ "MultiSurface EMPTY", '010C00000000000000'],
+ [ "PolyhedralSurface EMPTY", '010F00000000000000'],
+ [ "TIN EMPTY", '011000000000000000'],
+ # TODO - a few bogus inputs
+ # enable if we start range checking to avoid crashing on invalid input?
+ # https://github.com/mapnik/mapnik/issues/2236
+ #[ "", '' ],
+ #[ "00", '01' ],
+ #[ "0000", '0104' ],
+]
+
+def test_path_geo_interface():
+ geom = mapnik.Geometry.from_wkt('POINT(0 0)')
+ eq_(geom.__geo_interface__,{u'type': u'Point', u'coordinates': [0, 0]})
+
+def test_valid_wkb_parsing():
+ count = 0
+ for wkb in empty_wkbs:
+ geom = mapnik.Geometry.from_wkb(unhexlify(wkb[2]))
+ eq_(geom.is_empty(),True)
+ eq_(geom.type(),wkb[0])
+
+ for wkb in wkts:
+ geom = mapnik.Geometry.from_wkb(unhexlify(wkb[2]))
+ eq_(geom.is_empty(),False)
+ eq_(geom.type(),wkb[0])
+
+def test_wkb_parsing_error():
+ count = 0
+ for wkb in unsupported_wkb:
+ try:
+ geom = mapnik.Geometry.from_wkb(unhexlify(wkb))
+ # should not get here
+ eq_(True,False)
+ except:
+ pass
+ assert True
+
+# for partially empty wkbs don't currently look empty right now
+# since the enclosing container has objects
+def test_empty_wkb_parsing():
+ count = 0
+ for wkb in partially_empty_wkb:
+ geom = mapnik.Geometry.from_wkb(unhexlify(wkb[2]))
+ eq_(geom.type(),wkb[0])
+ eq_(geom.is_empty(),False)
+
+def test_geojson_parsing():
+ geometries = []
+ count = 0
+ for j in geojson:
+ count += 1
+ geometries.append(mapnik.Geometry.from_geojson(j[1]))
+ eq_(count,len(geometries))
+
+def test_geojson_parsing_reversed():
+ for idx,j in enumerate(geojson_reversed):
+ g1 = mapnik.Geometry.from_geojson(j)
+ g2 = mapnik.Geometry.from_geojson(geojson[idx][1])
+ eq_(g1.to_geojson(), g2.to_geojson())
+
+# http://geojson.org/geojson-spec.html#positions
+def test_geojson_point_positions():
+ input_json = '{"type":"Point","coordinates":[30,10]}'
+ geom = mapnik.Geometry.from_geojson(input_json)
+ eq_(geom.to_geojson(),input_json)
+ # should ignore all but the first two
+ geom = mapnik.Geometry.from_geojson('{"type":"Point","coordinates":[30,10,50,50,50,50]}')
+ eq_(geom.to_geojson(),input_json)
+
+def test_geojson_point_positions2():
+ input_json = '{"type":"LineString","coordinates":[[30,10],[10,30],[40,40]]}'
+ geom = mapnik.Geometry.from_geojson(input_json)
+ eq_(geom.to_geojson(),input_json)
+
+ # should ignore all but the first two
+ geom = mapnik.Geometry.from_geojson('{"type":"LineString","coordinates":[[30.0,10.0,0,0,0],[10.0,30.0,0,0,0],[40.0,40.0,0,0,0]]}')
+ eq_(geom.to_geojson(),input_json)
+
+def compare_wkb_from_wkt(wkt,type):
+ geom = mapnik.Geometry.from_wkt(wkt)
+ eq_(geom.type(),type)
+
+def compare_wkt_to_geojson(idx,wkt,num=None):
+ geom = mapnik.Geometry.from_wkt(wkt)
+ # ensure both have same result
+ gj = geom.to_geojson()
+ eq_(len(gj) > 1,True)
+ a = json.loads(gj)
+ e = json.loads(geojson[idx][1])
+ eq_(a,e)
+
+def test_wkt_simple():
+ for wkt in wkts:
+ try:
+ geom = mapnik.Geometry.from_wkt(wkt[1])
+ eq_(geom.type(),wkt[0])
+ except RuntimeError, e:
+ raise RuntimeError('%s %s' % (e, wkt))
+
+def test_wkb_simple():
+ for wkt in wkts:
+ try:
+ compare_wkb_from_wkt(wkt[1],wkt[0])
+ except RuntimeError, e:
+ raise RuntimeError('%s %s' % (e, wkt))
+
+def test_wkt_to_geojson():
+ idx = -1
+ for wkt in wkts:
+ try:
+ idx += 1
+ compare_wkt_to_geojson(idx,wkt[1],wkt[0])
+ except RuntimeError, e:
+ raise RuntimeError('%s %s' % (e, wkt))
+
+def test_wkt_rounding():
+ # currently fails because we use output precision of 6 - should we make configurable? https://github.com/mapnik/mapnik/issues/1009
+ # if precision is set to 15 still fails due to very subtle rounding issues
+ wkt = "POLYGON((7.904185 54.180426,7.89918 54.178168,7.897715 54.182318,7.893565 54.183111,7.890391 54.187567,7.885874 54.19068,7.879893 54.193915,7.894541 54.194647,7.900645 54.19068,7.904185 54.180426))"
+ geom = mapnik.Geometry.from_wkt(wkt)
+ eq_(geom.type(),mapnik.GeometryType.Polygon)
+
+def test_wkt_collection_flattening():
+ wkt = 'GEOMETRYCOLLECTION(POLYGON((1 1,2 1,2 2,1 2,1 1)),POLYGON((40 40,20 45,45 30,40 40)),POLYGON((20 35,45 20,30 5,10 10,10 30,20 35),(30 20,20 25,20 15,30 20)),LINESTRING(2 3,3 4))'
+ # currently fails as the MULTIPOLYGON inside will be returned as multiple polygons - not a huge deal - should we worry?
+ #wkt = "GEOMETRYCOLLECTION(POLYGON((1 1,2 1,2 2,1 2,1 1)),MULTIPOLYGON(((40 40,20 45,45 30,40 40)),((20 35,45 20,30 5,10 10,10 30,20 35),(30 20,20 25,20 15,30 20))),LINESTRING(2 3,3 4))"
+ geom = mapnik.Geometry.from_wkt(wkt)
+ eq_(geom.type(),mapnik.GeometryType.GeometryCollection)
+
+def test_creating_feature_from_geojson():
+ json_feat = {
+ "type": "Feature",
+ "geometry": {"type": "Point", "coordinates": [-122,48]},
+ "properties": {"name": "value"}
+ }
+ ctx = mapnik.Context()
+ feat = mapnik.Feature.from_geojson(json.dumps(json_feat),ctx)
+ eq_(feat.id(),1)
+ eq_(feat['name'],u'value')
+
+def test_handling_geojson_null_geoms():
+ for j in geojson_nulls:
+ ctx = mapnik.Context()
+ out_json = mapnik.Feature.from_geojson(j,ctx).to_geojson()
+ expected = '{"type":"Feature","id":1,"geometry":null,"properties":{}}'
+ eq_(out_json,expected)
+ # ensure it round trips
+ eq_(mapnik.Feature.from_geojson(out_json,ctx).to_geojson(),expected)
+
+
+if __name__ == "__main__":
+ setup()
+ exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/grayscale_test.py b/test/python_tests/grayscale_test.py
new file mode 100644
index 0000000..2bcf836
--- /dev/null
+++ b/test/python_tests/grayscale_test.py
@@ -0,0 +1,13 @@
+import mapnik
+from nose.tools import eq_
+from utilities import run_all
+
+def test_grayscale_conversion():
+ im = mapnik.Image(2,2)
+ im.fill(mapnik.Color('white'))
+ im.set_grayscale_to_alpha()
+ pixel = im.get_pixel(0,0)
+ eq_((pixel >> 24) & 0xff,255);
+
+if __name__ == "__main__":
+ exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/image_encoding_speed_test.py b/test/python_tests/image_encoding_speed_test.py
new file mode 100644
index 0000000..75bbc85
--- /dev/null
+++ b/test/python_tests/image_encoding_speed_test.py
@@ -0,0 +1,124 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import os, mapnik
+from timeit import Timer, time
+from utilities import execution_path, run_all
+
+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('.'))
+
+combinations = ['png',
+ 'png8',
+ 'png8:m=o',
+ 'png8:m=h',
+ 'png8:m=o:t=0',
+ 'png8:m=o:t=1',
+ 'png8:m=o:t=2',
+ 'png8:m=h:t=0',
+ 'png8:m=h:t=1',
+ 'png8:m=h:t=2',
+ 'png:z=1',
+ 'png:z=1:t=0', # forces rbg, no a
+ 'png8:z=1',
+ 'png8:z=1:m=o',
+ 'png8:z=1:m=h',
+ 'png8:z=1:c=1',
+ 'png8:z=1:c=24',
+ 'png8:z=1:c=64',
+ 'png8:z=1:c=128',
+ 'png8:z=1:c=200',
+ 'png8:z=1:c=255',
+ 'png8:z=9:c=64',
+ 'png8:z=9:c=128',
+ 'png8:z=9:c=200',
+ 'png8:z=1:c=50:m=h',
+ 'png8:z=1:c=1:m=o',
+ 'png8:z=1:c=1:m=o:s=filtered',
+ 'png:z=1:s=filtered',
+ 'png:z=1:s=huff',
+ 'png:z=1:s=rle',
+ 'png8:m=h:g=2.0',
+ 'png8:m=h:g=1.0',
+ 'png:e=miniz',
+ 'png8:e=miniz'
+ ]
+
+tiles = [
+'blank',
+'solid',
+'many_colors',
+'aerial_24'
+]
+
+iterations = 10
+
+def do_encoding():
+
+ global image
+
+ results = {}
+ sortable = {}
+
+ def run(func, im, format, t):
+ global image
+ image = im
+ start = time.time()
+ set = t.repeat(iterations,1)
+ elapsed = (time.time() - start)
+ min_ = min(set)*1000
+ avg = (sum(set)/len(set))*1000
+ name = func.__name__ + ' ' + format
+ results[name] = [min_,avg,elapsed*1000,name,len(func())]
+ sortable[name] = [min_]
+
+ if 'blank' in tiles:
+ def blank():
+ return eval('image.tostring("%s")' % c)
+ blank_im = mapnik.Image(512,512)
+ for c in combinations:
+ t = Timer(blank)
+ run(blank,blank_im,c,t)
+
+ if 'solid' in tiles:
+ def solid():
+ return eval('image.tostring("%s")' % c)
+ solid_im = mapnik.Image(512,512)
+ solid_im.fill(mapnik.Color("#f2efe9"))
+ for c in combinations:
+ t = Timer(solid)
+ run(solid,solid_im,c,t)
+
+ if 'many_colors' in tiles:
+ def many_colors():
+ return eval('image.tostring("%s")' % c)
+ # lots of colors: Loading Image...
+ many_colors_im = mapnik.Image.open('../data/images/13_4194_2747.png')
+ for c in combinations:
+ t = Timer(many_colors)
+ run(many_colors,many_colors_im,c,t)
+
+ if 'aerial_24' in tiles:
+ def aerial_24():
+ return eval('image.tostring("%s")' % c)
+ aerial_24_im = mapnik.Image.open('../data/images/12_654_1580.png')
+ for c in combinations:
+ t = Timer(aerial_24)
+ run(aerial_24,aerial_24_im,c,t)
+
+ for key, value in sorted(sortable.iteritems(), key=lambda (k,v): (v,k)):
+ s = results[key]
+ min_ = str(s[0])[:6]
+ avg = str(s[1])[:6]
+ elapsed = str(s[2])[:6]
+ name = s[3]
+ size = s[4]
+ print 'min: %sms | avg: %sms | total: %sms | len: %s <-- %s' % (min_,avg,elapsed,size,name)
+
+
+if __name__ == "__main__":
+ setup()
+ do_encoding()
+ exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/image_filters_test.py b/test/python_tests/image_filters_test.py
new file mode 100644
index 0000000..269d64c
--- /dev/null
+++ b/test/python_tests/image_filters_test.py
@@ -0,0 +1,68 @@
+#!/usr/bin/env python
+
+from nose.tools import eq_
+from utilities import execution_path, run_all
+from utilities import side_by_side_image
+import os, mapnik
+import re
+
+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 replace_style(m, name, style):
+ m.remove_style(name)
+ m.append_style(name, style)
+
+def test_append():
+ s = mapnik.Style()
+ eq_(s.image_filters,'')
+ s.image_filters = 'gray'
+ eq_(s.image_filters,'gray')
+ s.image_filters = 'sharpen'
+ eq_(s.image_filters,'sharpen')
+
+if 'shape' in mapnik.DatasourceCache.plugin_names():
+ def test_style_level_image_filter():
+ m = mapnik.Map(256, 256)
+ mapnik.load_map(m, '../data/good_maps/style_level_image_filter.xml')
+ m.zoom_all()
+ successes = []
+ fails = []
+ for name in ("", "agg-stack-blur(2,2)", "blur",
+ "edge-detect", "emboss", "gray", "invert",
+ "sharpen", "sobel", "x-gradient", "y-gradient"):
+ if name == "":
+ filename = "none"
+ else:
+ filename = re.sub(r"[^-_a-z.0-9]", "", name)
+ # find_style returns a copy of the style object
+ style_markers = m.find_style("markers")
+ style_markers.image_filters = name
+ style_labels = m.find_style("labels")
+ style_labels.image_filters = name
+ # replace the original style with the modified one
+ replace_style(m, "markers", style_markers)
+ replace_style(m, "labels", style_labels)
+ im = mapnik.Image(m.width, m.height)
+ mapnik.render(m, im)
+ actual = '/tmp/mapnik-style-image-filter-' + filename + '.png'
+ expected = 'images/style-image-filter/' + filename + '.png'
+ im.save(actual,"png32")
+ if not os.path.exists(expected) or os.environ.get('UPDATE'):
+ print 'generating expected test image: %s' % expected
+ im.save(expected,'png32')
+ expected_im = mapnik.Image.open(expected)
+ # compare them
+ if im.tostring('png32') == expected_im.tostring('png32'):
+ successes.append(name)
+ else:
+ fails.append('failed comparing actual (%s) and expected(%s)' % (actual,'tests/python_tests/'+ expected))
+ fail_im = side_by_side_image(expected_im, im)
+ fail_im.save('/tmp/mapnik-style-image-filter-' + filename + '.fail.png','png32')
+ eq_(len(fails), 0, '\n'+'\n'.join(fails))
+
+if __name__ == "__main__":
+ setup()
+ exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/image_test.py b/test/python_tests/image_test.py
new file mode 100644
index 0000000..189f8be
--- /dev/null
+++ b/test/python_tests/image_test.py
@@ -0,0 +1,346 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import os, mapnik
+from nose.tools import eq_,raises, assert_almost_equal
+from utilities import execution_path, run_all, get_unique_colors
+
+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_type():
+ im = mapnik.Image(256, 256)
+ eq_(im.get_type(), mapnik.ImageType.rgba8)
+ im = mapnik.Image(256, 256, mapnik.ImageType.gray8)
+ eq_(im.get_type(), mapnik.ImageType.gray8)
+
+def test_image_premultiply():
+ im = mapnik.Image(256,256)
+ eq_(im.premultiplied(),False)
+ # Premultiply should return true that it worked
+ eq_(im.premultiply(), True)
+ eq_(im.premultiplied(),True)
+ # Premultipling again should return false as nothing should happen
+ eq_(im.premultiply(), False)
+ eq_(im.premultiplied(),True)
+ # Demultiply should return true that it worked
+ eq_(im.demultiply(), True)
+ eq_(im.premultiplied(),False)
+ # Demultiply again should not work and return false as it did nothing
+ eq_(im.demultiply(), False)
+ eq_(im.premultiplied(),False)
+
+def test_image_premultiply_values():
+ im = mapnik.Image(256,256)
+ im.fill(mapnik.Color(16, 33, 255, 128))
+ im.premultiply()
+ c = im.get_pixel(0,0, True)
+ eq_(c.r, 8)
+ eq_(c.g, 17)
+ eq_(c.b, 128)
+ eq_(c.a, 128)
+ im.demultiply()
+ # Do to the nature of this operation the result will not be exactly the same
+ c = im.get_pixel(0,0,True)
+ eq_(c.r,15)
+ eq_(c.g,33)
+ eq_(c.b,255)
+ eq_(c.a,128)
+
+def test_apply_opacity():
+ im = mapnik.Image(4,4)
+ im.fill(mapnik.Color(128,128,128,128))
+ im.apply_opacity(0.75);
+ c = im.get_pixel(0,0,True)
+ eq_(c.r,128)
+ eq_(c.g,128)
+ eq_(c.b,128)
+ eq_(c.a,96)
+
+def test_background():
+ im = mapnik.Image(256,256)
+ eq_(im.premultiplied(), False)
+ im.fill(mapnik.Color(32,64,125,128))
+ eq_(im.premultiplied(), False)
+ c = im.get_pixel(0,0,True)
+ eq_(c.get_premultiplied(), False)
+ eq_(c.r,32)
+ eq_(c.g,64)
+ eq_(c.b,125)
+ eq_(c.a,128)
+ # Now again with a premultiplied alpha
+ im.fill(mapnik.Color(32,64,125,128,True))
+ eq_(im.premultiplied(), True)
+ c = im.get_pixel(0,0,True)
+ eq_(c.get_premultiplied(), True)
+ eq_(c.r,32)
+ eq_(c.g,64)
+ eq_(c.b,125)
+ eq_(c.a,128)
+
+def test_set_and_get_pixel():
+ # Create an image that is not premultiplied
+ im = mapnik.Image(256,256)
+ c0 = mapnik.Color(16,33,255,128)
+ c0_pre = mapnik.Color(16,33,255,128, True)
+ im.set_pixel(0,0,c0)
+ im.set_pixel(1,1,c0_pre)
+ # No differences for non premultiplied pixels
+ c1_int = mapnik.Color(im.get_pixel(0,0))
+ eq_(c0.r, c1_int.r)
+ eq_(c0.g, c1_int.g)
+ eq_(c0.b, c1_int.b)
+ eq_(c0.a, c1_int.a)
+ c1 = im.get_pixel(0,0,True)
+ eq_(c0.r, c1.r)
+ eq_(c0.g, c1.g)
+ eq_(c0.b, c1.b)
+ eq_(c0.a, c1.a)
+ # The premultiplied Color should be demultiplied before being applied.
+ c0_pre.demultiply()
+ c1_int = mapnik.Color(im.get_pixel(1,1))
+ eq_(c0_pre.r, c1_int.r)
+ eq_(c0_pre.g, c1_int.g)
+ eq_(c0_pre.b, c1_int.b)
+ eq_(c0_pre.a, c1_int.a)
+ c1 = im.get_pixel(1,1,True)
+ eq_(c0_pre.r, c1.r)
+ eq_(c0_pre.g, c1.g)
+ eq_(c0_pre.b, c1.b)
+ eq_(c0_pre.a, c1.a)
+
+ # Now create a new image that is premultiplied
+ im = mapnik.Image(256,256, mapnik.ImageType.rgba8, True, True)
+ c0 = mapnik.Color(16,33,255,128)
+ c0_pre = mapnik.Color(16,33,255,128, True)
+ im.set_pixel(0,0,c0)
+ im.set_pixel(1,1,c0_pre)
+ # It should have put pixels that are the same as premultiplied so premultiply c0
+ c0.premultiply()
+ c1_int = mapnik.Color(im.get_pixel(0,0))
+ eq_(c0.r, c1_int.r)
+ eq_(c0.g, c1_int.g)
+ eq_(c0.b, c1_int.b)
+ eq_(c0.a, c1_int.a)
+ c1 = im.get_pixel(0,0,True)
+ eq_(c0.r, c1.r)
+ eq_(c0.g, c1.g)
+ eq_(c0.b, c1.b)
+ eq_(c0.a, c1.a)
+ # The premultiplied Color should be the same though
+ c1_int = mapnik.Color(im.get_pixel(1,1))
+ eq_(c0_pre.r, c1_int.r)
+ eq_(c0_pre.g, c1_int.g)
+ eq_(c0_pre.b, c1_int.b)
+ eq_(c0_pre.a, c1_int.a)
+ c1 = im.get_pixel(1,1,True)
+ eq_(c0_pre.r, c1.r)
+ eq_(c0_pre.g, c1.g)
+ eq_(c0_pre.b, c1.b)
+ eq_(c0_pre.a, c1.a)
+
+def test_pixel_gray8():
+ im = mapnik.Image(4,4,mapnik.ImageType.gray8)
+ val_list = range(20)
+ for v in val_list:
+ im.set_pixel(0,0, v)
+ eq_(im.get_pixel(0,0), v)
+ im.set_pixel(0,0, -v)
+ eq_(im.get_pixel(0,0), 0)
+
+def test_pixel_gray8s():
+ im = mapnik.Image(4,4,mapnik.ImageType.gray8s)
+ val_list = range(20)
+ for v in val_list:
+ im.set_pixel(0,0, v)
+ eq_(im.get_pixel(0,0), v)
+ im.set_pixel(0,0, -v)
+ eq_(im.get_pixel(0,0), -v)
+
+def test_pixel_gray16():
+ im = mapnik.Image(4,4,mapnik.ImageType.gray16)
+ val_list = range(20)
+ for v in val_list:
+ im.set_pixel(0,0, v)
+ eq_(im.get_pixel(0,0), v)
+ im.set_pixel(0,0, -v)
+ eq_(im.get_pixel(0,0), 0)
+
+def test_pixel_gray16s():
+ im = mapnik.Image(4,4,mapnik.ImageType.gray16s)
+ val_list = range(20)
+ for v in val_list:
+ im.set_pixel(0,0, v)
+ eq_(im.get_pixel(0,0), v)
+ im.set_pixel(0,0, -v)
+ eq_(im.get_pixel(0,0), -v)
+
+def test_pixel_gray32():
+ im = mapnik.Image(4,4,mapnik.ImageType.gray32)
+ val_list = range(20)
+ for v in val_list:
+ im.set_pixel(0,0, v)
+ eq_(im.get_pixel(0,0), v)
+ im.set_pixel(0,0, -v)
+ eq_(im.get_pixel(0,0), 0)
+
+def test_pixel_gray32s():
+ im = mapnik.Image(4,4,mapnik.ImageType.gray32s)
+ val_list = range(20)
+ for v in val_list:
+ im.set_pixel(0,0, v)
+ eq_(im.get_pixel(0,0), v)
+ im.set_pixel(0,0, -v)
+ eq_(im.get_pixel(0,0), -v)
+
+def test_pixel_gray64():
+ im = mapnik.Image(4,4,mapnik.ImageType.gray64)
+ val_list = range(20)
+ for v in val_list:
+ im.set_pixel(0,0, v)
+ eq_(im.get_pixel(0,0), v)
+ im.set_pixel(0,0, -v)
+ eq_(im.get_pixel(0,0), 0)
+
+def test_pixel_gray64s():
+ im = mapnik.Image(4,4,mapnik.ImageType.gray64s)
+ val_list = range(20)
+ for v in val_list:
+ im.set_pixel(0,0, v)
+ eq_(im.get_pixel(0,0), v)
+ im.set_pixel(0,0, -v)
+ eq_(im.get_pixel(0,0), -v)
+
+def test_pixel_floats():
+ im = mapnik.Image(4,4,mapnik.ImageType.gray32f)
+ val_list = [0.9, 0.99, 0.999, 0.9999, 0.99999, 1, 1.0001, 1.001, 1.01, 1.1]
+ for v in val_list:
+ im.set_pixel(0,0, v)
+ assert_almost_equal(im.get_pixel(0,0), v)
+ im.set_pixel(0,0, -v)
+ assert_almost_equal(im.get_pixel(0,0), -v)
+
+def test_pixel_doubles():
+ im = mapnik.Image(4,4,mapnik.ImageType.gray64f)
+ val_list = [0.9, 0.99, 0.999, 0.9999, 0.99999, 1, 1.0001, 1.001, 1.01, 1.1]
+ for v in val_list:
+ im.set_pixel(0,0, v)
+ assert_almost_equal(im.get_pixel(0,0), v)
+ im.set_pixel(0,0, -v)
+ assert_almost_equal(im.get_pixel(0,0), -v)
+
+def test_pixel_overflow():
+ im = mapnik.Image(4,4,mapnik.ImageType.gray8)
+ im.set_pixel(0,0,256)
+ eq_(im.get_pixel(0,0),255)
+
+def test_pixel_underflow():
+ im = mapnik.Image(4,4,mapnik.ImageType.gray8)
+ im.set_pixel(0,0,-1)
+ eq_(im.get_pixel(0,0),0)
+ im = mapnik.Image(4,4,mapnik.ImageType.gray16)
+ im.set_pixel(0,0,-1)
+ eq_(im.get_pixel(0,0),0)
+
+@raises(IndexError)
+def test_set_pixel_out_of_range_1():
+ im = mapnik.Image(4,4)
+ c = mapnik.Color('blue')
+ im.set_pixel(5,5,c)
+
+@raises(OverflowError)
+def test_set_pixel_out_of_range_2():
+ im = mapnik.Image(4,4)
+ c = mapnik.Color('blue')
+ im.set_pixel(-1,1,c)
+
+@raises(IndexError)
+def test_get_pixel_out_of_range_1():
+ im = mapnik.Image(4,4)
+ c = im.get_pixel(5,5)
+
+@raises(OverflowError)
+def test_get_pixel_out_of_range_2():
+ im = mapnik.Image(4,4)
+ c = im.get_pixel(-1,1)
+
+@raises(IndexError)
+def test_get_pixel_color_out_of_range_1():
+ im = mapnik.Image(4,4)
+ c = im.get_pixel(5,5,True)
+
+@raises(OverflowError)
+def test_get_pixel_color_out_of_range_2():
+ im = mapnik.Image(4,4)
+ c = im.get_pixel(-1,1,True)
+
+def test_set_color_to_alpha():
+ im = mapnik.Image(256,256)
+ im.fill(mapnik.Color('rgba(12,12,12,255)'))
+ eq_(get_unique_colors(im), ['rgba(12,12,12,255)'])
+ im.set_color_to_alpha(mapnik.Color('rgba(12,12,12,0)'))
+ eq_(get_unique_colors(im), ['rgba(0,0,0,0)'])
+
+@raises(RuntimeError)
+def test_negative_image_dimensions():
+ # TODO - this may have regressed in https://github.com/mapnik/mapnik/commit/4f3521ac24b61fc8ae8fd344a16dc3a5fdf15af7
+ im = mapnik.Image(-40,40)
+ # should not get here
+ eq_(im.width(),0)
+ eq_(im.height(),0)
+
+def test_jpeg_round_trip():
+ filepath = '/tmp/mapnik-jpeg-io.jpeg'
+ im = mapnik.Image(255,267)
+ im.fill(mapnik.Color('rgba(1,2,3,.5)'))
+ im.save(filepath,'jpeg')
+ im2 = mapnik.Image.open(filepath)
+ im3 = mapnik.Image.fromstring(open(filepath,'r').read())
+ eq_(im.width(),im2.width())
+ eq_(im.height(),im2.height())
+ eq_(im.width(),im3.width())
+ eq_(im.height(),im3.height())
+ eq_(len(im.tostring()),len(im2.tostring()))
+ eq_(len(im.tostring('jpeg')),len(im2.tostring('jpeg')))
+ eq_(len(im.tostring()),len(im3.tostring()))
+ eq_(len(im.tostring('jpeg')),len(im3.tostring('jpeg')))
+
+def test_png_round_trip():
+ filepath = '/tmp/mapnik-png-io.png'
+ im = mapnik.Image(255,267)
+ im.fill(mapnik.Color('rgba(1,2,3,.5)'))
+ im.save(filepath,'png')
+ im2 = mapnik.Image.open(filepath)
+ im3 = mapnik.Image.fromstring(open(filepath,'r').read())
+ eq_(im.width(),im2.width())
+ eq_(im.height(),im2.height())
+ eq_(im.width(),im3.width())
+ eq_(im.height(),im3.height())
+ eq_(len(im.tostring()),len(im2.tostring()))
+ eq_(len(im.tostring('png')),len(im2.tostring('png')))
+ eq_(len(im.tostring('png8')),len(im2.tostring('png8')))
+ eq_(len(im.tostring()),len(im3.tostring()))
+ eq_(len(im.tostring('png')),len(im3.tostring('png')))
+ eq_(len(im.tostring('png8')),len(im3.tostring('png8')))
+
+def test_image_open_from_string():
+ filepath = '../data/images/dummy.png'
+ im1 = mapnik.Image.open(filepath)
+ im2 = mapnik.Image.fromstring(open(filepath,'rb').read())
+ eq_(im1.width(),im2.width())
+ length = len(im1.tostring())
+ eq_(length,len(im2.tostring()))
+ eq_(len(mapnik.Image.fromstring(im1.tostring('png')).tostring()),length)
+ eq_(len(mapnik.Image.fromstring(im1.tostring('jpeg')).tostring()),length)
+ eq_(len(mapnik.Image.frombuffer(buffer(im1.tostring('png'))).tostring()),length)
+ eq_(len(mapnik.Image.frombuffer(buffer(im1.tostring('jpeg'))).tostring()),length)
+
+ # TODO - https://github.com/mapnik/mapnik/issues/1831
+ eq_(len(mapnik.Image.fromstring(im1.tostring('tiff')).tostring()),length)
+ eq_(len(mapnik.Image.frombuffer(buffer(im1.tostring('tiff'))).tostring()),length)
+
+if __name__ == "__main__":
+ setup()
+ exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/image_tiff_test.py b/test/python_tests/image_tiff_test.py
new file mode 100644
index 0000000..e0535d0
--- /dev/null
+++ b/test/python_tests/image_tiff_test.py
@@ -0,0 +1,335 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import os, mapnik
+import hashlib
+from nose.tools import eq_, assert_not_equal
+from utilities import execution_path, run_all
+
+def hashstr(var):
+ return hashlib.md5(var).hexdigest()
+
+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_tiff_round_trip_scanline():
+ filepath = '/tmp/mapnik-tiff-io-scanline.tiff'
+ im = mapnik.Image(255,267)
+ im.fill(mapnik.Color('rgba(12,255,128,.5)'))
+ org_str = hashstr(im.tostring())
+ im.save(filepath,'tiff:method=scanline')
+ im2 = mapnik.Image.open(filepath)
+ im3 = mapnik.Image.fromstring(open(filepath,'r').read())
+ eq_(im.width(),im2.width())
+ eq_(im.height(),im2.height())
+ eq_(im.width(),im3.width())
+ eq_(im.height(),im3.height())
+ eq_(hashstr(im.tostring()), org_str)
+ # This won't be the same the first time around because the im is not premultiplied and im2 is
+ assert_not_equal(hashstr(im.tostring()),hashstr(im2.tostring()))
+ assert_not_equal(hashstr(im.tostring('tiff:method=scanline')),hashstr(im2.tostring('tiff:method=scanline')))
+ # Now premultiply
+ im.premultiply()
+ eq_(hashstr(im.tostring()),hashstr(im2.tostring()))
+ eq_(hashstr(im.tostring('tiff:method=scanline')),hashstr(im2.tostring('tiff:method=scanline')))
+ eq_(hashstr(im2.tostring()),hashstr(im3.tostring()))
+ eq_(hashstr(im2.tostring('tiff:method=scanline')),hashstr(im3.tostring('tiff:method=scanline')))
+
+def test_tiff_round_trip_stripped():
+ filepath = '/tmp/mapnik-tiff-io-stripped.tiff'
+ im = mapnik.Image(255,267)
+ im.fill(mapnik.Color('rgba(12,255,128,.5)'))
+ org_str = hashstr(im.tostring())
+ im.save(filepath,'tiff:method=stripped')
+ im2 = mapnik.Image.open(filepath)
+ im2.save('/tmp/mapnik-tiff-io-stripped2.tiff','tiff:method=stripped')
+ im3 = mapnik.Image.fromstring(open(filepath,'r').read())
+ eq_(im.width(),im2.width())
+ eq_(im.height(),im2.height())
+ eq_(im.width(),im3.width())
+ eq_(im.height(),im3.height())
+ # Because one will end up with UNASSOC alpha tag which internally the TIFF reader will premultiply, the first to string will not be the same due to the
+ # difference in tags.
+ assert_not_equal(hashstr(im.tostring()),hashstr(im2.tostring()))
+ assert_not_equal(hashstr(im.tostring('tiff:method=stripped')),hashstr(im2.tostring('tiff:method=stripped')))
+ # Now if we premultiply they will be exactly the same
+ im.premultiply()
+ eq_(hashstr(im.tostring()),hashstr(im2.tostring()))
+ eq_(hashstr(im.tostring('tiff:method=stripped')),hashstr(im2.tostring('tiff:method=stripped')))
+ eq_(hashstr(im2.tostring()),hashstr(im3.tostring()))
+ # Both of these started out premultiplied, so this round trip should be exactly the same!
+ eq_(hashstr(im2.tostring('tiff:method=stripped')),hashstr(im3.tostring('tiff:method=stripped')))
+
+def test_tiff_round_trip_rows_stripped():
+ filepath = '/tmp/mapnik-tiff-io-rows_stripped.tiff'
+ filepath2 = '/tmp/mapnik-tiff-io-rows_stripped2.tiff'
+ im = mapnik.Image(255,267)
+ im.fill(mapnik.Color('rgba(12,255,128,.5)'))
+ c = im.get_pixel(0,0,True)
+ eq_(c.r, 12)
+ eq_(c.g, 255)
+ eq_(c.b, 128)
+ eq_(c.a, 128)
+ eq_(c.get_premultiplied(), False)
+ im.save(filepath,'tiff:method=stripped:rows_per_strip=8')
+ im2 = mapnik.Image.open(filepath)
+ c2 = im2.get_pixel(0,0,True)
+ eq_(c2.r, 6)
+ eq_(c2.g, 128)
+ eq_(c2.b, 64)
+ eq_(c2.a, 128)
+ eq_(c2.get_premultiplied(), True)
+ im2.save(filepath2,'tiff:method=stripped:rows_per_strip=8')
+ im3 = mapnik.Image.fromstring(open(filepath,'r').read())
+ eq_(im.width(),im2.width())
+ eq_(im.height(),im2.height())
+ eq_(im.width(),im3.width())
+ eq_(im.height(),im3.height())
+ # Because one will end up with UNASSOC alpha tag which internally the TIFF reader will premultiply, the first to string will not be the same due to the
+ # difference in tags.
+ assert_not_equal(hashstr(im.tostring()),hashstr(im2.tostring()))
+ assert_not_equal(hashstr(im.tostring('tiff:method=stripped:rows_per_strip=8')),hashstr(im2.tostring('tiff:method=stripped:rows_per_strip=8')))
+ # Now premultiply the first image and they will be the same!
+ im.premultiply()
+ eq_(hashstr(im.tostring('tiff:method=stripped:rows_per_strip=8')),hashstr(im2.tostring('tiff:method=stripped:rows_per_strip=8')))
+ eq_(hashstr(im2.tostring()),hashstr(im3.tostring()))
+ # Both of these started out premultiplied, so this round trip should be exactly the same!
+ eq_(hashstr(im2.tostring('tiff:method=stripped:rows_per_strip=8')),hashstr(im3.tostring('tiff:method=stripped:rows_per_strip=8')))
+
+def test_tiff_round_trip_buffered_tiled():
+ filepath = '/tmp/mapnik-tiff-io-buffered-tiled.tiff'
+ filepath2 = '/tmp/mapnik-tiff-io-buffered-tiled2.tiff'
+ filepath3 = '/tmp/mapnik-tiff-io-buffered-tiled3.tiff'
+ im = mapnik.Image(255,267)
+ im.fill(mapnik.Color('rgba(33,255,128,.5)'))
+ c = im.get_pixel(0,0,True)
+ eq_(c.r, 33)
+ eq_(c.g, 255)
+ eq_(c.b, 128)
+ eq_(c.a, 128)
+ eq_(c.get_premultiplied(), False)
+ im.save(filepath,'tiff:method=tiled:tile_width=32:tile_height=32')
+ im2 = mapnik.Image.open(filepath)
+ c2 = im2.get_pixel(0,0,True)
+ eq_(c2.r, 17)
+ eq_(c2.g, 128)
+ eq_(c2.b, 64)
+ eq_(c2.a, 128)
+ eq_(c2.get_premultiplied(), True)
+ im3 = mapnik.Image.fromstring(open(filepath,'r').read())
+ im2.save(filepath2, 'tiff:method=tiled:tile_width=32:tile_height=32')
+ im3.save(filepath3, 'tiff:method=tiled:tile_width=32:tile_height=32')
+ eq_(im.width(),im2.width())
+ eq_(im.height(),im2.height())
+ eq_(im.width(),im3.width())
+ eq_(im.height(),im3.height())
+ # Because one will end up with UNASSOC alpha tag which internally the TIFF reader will premultiply, the first to string will not be the same due to the
+ # difference in tags.
+ assert_not_equal(hashstr(im.tostring()),hashstr(im2.tostring()))
+ assert_not_equal(hashstr(im.tostring('tiff:method=tiled:tile_width=32:tile_height=32')),hashstr(im2.tostring('tiff:method=tiled:tile_width=32:tile_height=32')))
+ # Now premultiply the first image and they should be the same
+ im.premultiply()
+ eq_(hashstr(im.tostring()),hashstr(im2.tostring()))
+ eq_(hashstr(im.tostring('tiff:method=tiled:tile_width=32:tile_height=32')),hashstr(im2.tostring('tiff:method=tiled:tile_width=32:tile_height=32')))
+ eq_(hashstr(im2.tostring()),hashstr(im3.tostring()))
+ # Both of these started out premultiplied, so this round trip should be exactly the same!
+ eq_(hashstr(im2.tostring('tiff:method=tiled:tile_width=32:tile_height=32')),hashstr(im3.tostring('tiff:method=tiled:tile_width=32:tile_height=32')))
+
+def test_tiff_round_trip_tiled():
+ filepath = '/tmp/mapnik-tiff-io-tiled.tiff'
+ im = mapnik.Image(256,256)
+ im.fill(mapnik.Color('rgba(1,255,128,.5)'))
+ im.save(filepath,'tiff:method=tiled')
+ im2 = mapnik.Image.open(filepath)
+ im3 = mapnik.Image.fromstring(open(filepath,'r').read())
+ eq_(im.width(),im2.width())
+ eq_(im.height(),im2.height())
+ eq_(im.width(),im3.width())
+ eq_(im.height(),im3.height())
+ # Because one will end up with UNASSOC alpha tag which internally the TIFF reader will premultiply, the first to string will not be the same due to the
+ # difference in tags.
+ assert_not_equal(hashstr(im.tostring()),hashstr(im2.tostring()))
+ assert_not_equal(hashstr(im.tostring('tiff:method=tiled')),hashstr(im2.tostring('tiff:method=tiled')))
+ # Now premultiply the first image and they will be exactly the same.
+ im.premultiply()
+ eq_(hashstr(im.tostring()),hashstr(im2.tostring()))
+ eq_(hashstr(im.tostring('tiff:method=tiled')),hashstr(im2.tostring('tiff:method=tiled')))
+ eq_(hashstr(im2.tostring()),hashstr(im3.tostring()))
+ # Both of these started out premultiplied, so this round trip should be exactly the same!
+ eq_(hashstr(im2.tostring('tiff:method=tiled')),hashstr(im3.tostring('tiff:method=tiled')))
+
+
+def test_tiff_rgb8_compare():
+ filepath1 = '../data/tiff/ndvi_256x256_rgb8_striped.tif'
+ filepath2 = '/tmp/mapnik-tiff-rgb8.tiff'
+ im = mapnik.Image.open(filepath1)
+ im.save(filepath2,'tiff')
+ im2 = mapnik.Image.open(filepath2)
+ eq_(im.width(),im2.width())
+ eq_(im.height(),im2.height())
+ eq_(hashstr(im.tostring()),hashstr(im2.tostring()))
+ eq_(hashstr(im.tostring('tiff')),hashstr(im2.tostring('tiff')))
+ # should not be a blank image
+ eq_(hashstr(im.tostring("tiff")) != hashstr(mapnik.Image(im.width(),im.height(),mapnik.ImageType.rgba8).tostring("tiff")),True)
+
+def test_tiff_rgba8_compare_scanline():
+ filepath1 = '../data/tiff/ndvi_256x256_rgba8_striped.tif'
+ filepath2 = '/tmp/mapnik-tiff-rgba8-scanline.tiff'
+ im = mapnik.Image.open(filepath1)
+ im.save(filepath2,'tiff:method=scanline')
+ im2 = mapnik.Image.open(filepath2)
+ eq_(im.width(),im2.width())
+ eq_(im.height(),im2.height())
+ eq_(hashstr(im.tostring()),hashstr(im2.tostring()))
+ eq_(hashstr(im.tostring('tiff:method=scanline')),hashstr(im2.tostring('tiff:method=scanline')))
+ # should not be a blank image
+ eq_(hashstr(im.tostring("tiff")) != hashstr(mapnik.Image(im.width(),im.height(),mapnik.ImageType.rgba8).tostring("tiff")),True)
+
+def test_tiff_rgba8_compare_stripped():
+ filepath1 = '../data/tiff/ndvi_256x256_rgba8_striped.tif'
+ filepath2 = '/tmp/mapnik-tiff-rgba8-stripped.tiff'
+ im = mapnik.Image.open(filepath1)
+ im.save(filepath2,'tiff:method=stripped')
+ im2 = mapnik.Image.open(filepath2)
+ eq_(im.width(),im2.width())
+ eq_(im.height(),im2.height())
+ eq_(hashstr(im.tostring()),hashstr(im2.tostring()))
+ eq_(hashstr(im.tostring('tiff:method=stripped')),hashstr(im2.tostring('tiff:method=stripped')))
+ # should not be a blank image
+ eq_(hashstr(im.tostring("tiff")) != hashstr(mapnik.Image(im.width(),im.height(),mapnik.ImageType.rgba8).tostring("tiff")),True)
+
+def test_tiff_rgba8_compare_tiled():
+ filepath1 = '../data/tiff/ndvi_256x256_rgba8_striped.tif'
+ filepath2 = '/tmp/mapnik-tiff-rgba8-stripped.tiff'
+ im = mapnik.Image.open(filepath1)
+ im.save(filepath2,'tiff:method=tiled')
+ im2 = mapnik.Image.open(filepath2)
+ eq_(im.width(),im2.width())
+ eq_(im.height(),im2.height())
+ eq_(hashstr(im.tostring()),hashstr(im2.tostring()))
+ eq_(hashstr(im.tostring('tiff:method=tiled')),hashstr(im2.tostring('tiff:method=tiled')))
+ # should not be a blank image
+ eq_(hashstr(im.tostring("tiff")) != hashstr(mapnik.Image(im.width(),im.height(),mapnik.ImageType.rgba8).tostring("tiff")),True)
+
+def test_tiff_gray8_compare_scanline():
+ filepath1 = '../data/tiff/ndvi_256x256_gray8_striped.tif'
+ filepath2 = '/tmp/mapnik-tiff-gray8-scanline.tiff'
+ im = mapnik.Image.open(filepath1)
+ im.save(filepath2,'tiff:method=scanline')
+ im2 = mapnik.Image.open(filepath2)
+ eq_(im.width(),im2.width())
+ eq_(im.height(),im2.height())
+ eq_(hashstr(im.tostring()),hashstr(im2.tostring()))
+ eq_(hashstr(im.tostring('tiff:method=scanline')),hashstr(im2.tostring('tiff:method=scanline')))
+ # should not be a blank image
+ eq_(hashstr(im.tostring("tiff")) != hashstr(mapnik.Image(im.width(),im.height(),mapnik.ImageType.gray8).tostring("tiff")),True)
+
+def test_tiff_gray8_compare_stripped():
+ filepath1 = '../data/tiff/ndvi_256x256_gray8_striped.tif'
+ filepath2 = '/tmp/mapnik-tiff-gray8-stripped.tiff'
+ im = mapnik.Image.open(filepath1)
+ im.save(filepath2,'tiff:method=stripped')
+ im2 = mapnik.Image.open(filepath2)
+ eq_(im.width(),im2.width())
+ eq_(im.height(),im2.height())
+ eq_(hashstr(im.tostring()),hashstr(im2.tostring()))
+ eq_(hashstr(im.tostring('tiff:method=stripped')),hashstr(im2.tostring('tiff:method=stripped')))
+ # should not be a blank image
+ eq_(hashstr(im.tostring("tiff")) != hashstr(mapnik.Image(im.width(),im.height(),mapnik.ImageType.gray8).tostring("tiff")),True)
+
+def test_tiff_gray8_compare_tiled():
+ filepath1 = '../data/tiff/ndvi_256x256_gray8_striped.tif'
+ filepath2 = '/tmp/mapnik-tiff-gray8-tiled.tiff'
+ im = mapnik.Image.open(filepath1)
+ im.save(filepath2,'tiff:method=tiled')
+ im2 = mapnik.Image.open(filepath2)
+ eq_(im.width(),im2.width())
+ eq_(im.height(),im2.height())
+ eq_(hashstr(im.tostring()),hashstr(im2.tostring()))
+ eq_(hashstr(im.tostring('tiff:method=tiled')),hashstr(im2.tostring('tiff:method=tiled')))
+ # should not be a blank image
+ eq_(hashstr(im.tostring("tiff")) != hashstr(mapnik.Image(im.width(),im.height(),mapnik.ImageType.gray8).tostring("tiff")),True)
+
+def test_tiff_gray16_compare_scanline():
+ filepath1 = '../data/tiff/ndvi_256x256_gray16_striped.tif'
+ filepath2 = '/tmp/mapnik-tiff-gray16-scanline.tiff'
+ im = mapnik.Image.open(filepath1)
+ im.save(filepath2,'tiff:method=scanline')
+ im2 = mapnik.Image.open(filepath2)
+ eq_(im.width(),im2.width())
+ eq_(im.height(),im2.height())
+ eq_(hashstr(im.tostring()),hashstr(im2.tostring()))
+ eq_(hashstr(im.tostring('tiff:method=scanline')),hashstr(im2.tostring('tiff:method=scanline')))
+ # should not be a blank image
+ eq_(hashstr(im.tostring("tiff")) != hashstr(mapnik.Image(im.width(),im.height(),mapnik.ImageType.gray16).tostring("tiff")),True)
+
+def test_tiff_gray16_compare_stripped():
+ filepath1 = '../data/tiff/ndvi_256x256_gray16_striped.tif'
+ filepath2 = '/tmp/mapnik-tiff-gray16-stripped.tiff'
+ im = mapnik.Image.open(filepath1)
+ im.save(filepath2,'tiff:method=stripped')
+ im2 = mapnik.Image.open(filepath2)
+ eq_(im.width(),im2.width())
+ eq_(im.height(),im2.height())
+ eq_(hashstr(im.tostring()),hashstr(im2.tostring()))
+ eq_(hashstr(im.tostring('tiff:method=stripped')),hashstr(im2.tostring('tiff:method=stripped')))
+ # should not be a blank image
+ eq_(hashstr(im.tostring("tiff")) != hashstr(mapnik.Image(im.width(),im.height(),mapnik.ImageType.gray16).tostring("tiff")),True)
+
+def test_tiff_gray16_compare_tiled():
+ filepath1 = '../data/tiff/ndvi_256x256_gray16_striped.tif'
+ filepath2 = '/tmp/mapnik-tiff-gray16-tiled.tiff'
+ im = mapnik.Image.open(filepath1)
+ im.save(filepath2,'tiff:method=tiled')
+ im2 = mapnik.Image.open(filepath2)
+ eq_(im.width(),im2.width())
+ eq_(im.height(),im2.height())
+ eq_(hashstr(im.tostring()),hashstr(im2.tostring()))
+ eq_(hashstr(im.tostring('tiff:method=tiled')),hashstr(im2.tostring('tiff:method=tiled')))
+ # should not be a blank image
+ eq_(hashstr(im.tostring("tiff")) != hashstr(mapnik.Image(im.width(),im.height(),mapnik.ImageType.gray16).tostring("tiff")),True)
+
+def test_tiff_gray32f_compare_scanline():
+ filepath1 = '../data/tiff/ndvi_256x256_gray32f_striped.tif'
+ filepath2 = '/tmp/mapnik-tiff-gray32f-scanline.tiff'
+ im = mapnik.Image.open(filepath1)
+ im.save(filepath2,'tiff:method=scanline')
+ im2 = mapnik.Image.open(filepath2)
+ eq_(im.width(),im2.width())
+ eq_(im.height(),im2.height())
+ eq_(hashstr(im.tostring()),hashstr(im2.tostring()))
+ eq_(hashstr(im.tostring('tiff:method=scanline')),hashstr(im2.tostring('tiff:method=scanline')))
+ # should not be a blank image
+ eq_(hashstr(im.tostring("tiff")) != hashstr(mapnik.Image(im.width(),im.height(),mapnik.ImageType.gray32f).tostring("tiff")),True)
+
+def test_tiff_gray32f_compare_stripped():
+ filepath1 = '../data/tiff/ndvi_256x256_gray32f_striped.tif'
+ filepath2 = '/tmp/mapnik-tiff-gray32f-stripped.tiff'
+ im = mapnik.Image.open(filepath1)
+ im.save(filepath2,'tiff:method=stripped')
+ im2 = mapnik.Image.open(filepath2)
+ eq_(im.width(),im2.width())
+ eq_(im.height(),im2.height())
+ eq_(hashstr(im.tostring()),hashstr(im2.tostring()))
+ eq_(hashstr(im.tostring('tiff:method=stripped')),hashstr(im2.tostring('tiff:method=stripped')))
+ # should not be a blank image
+ eq_(hashstr(im.tostring("tiff")) != hashstr(mapnik.Image(im.width(),im.height(),mapnik.ImageType.gray32f).tostring("tiff")),True)
+
+def test_tiff_gray32f_compare_tiled():
+ filepath1 = '../data/tiff/ndvi_256x256_gray32f_striped.tif'
+ filepath2 = '/tmp/mapnik-tiff-gray32f-tiled.tiff'
+ im = mapnik.Image.open(filepath1)
+ im.save(filepath2,'tiff:method=tiled')
+ im2 = mapnik.Image.open(filepath2)
+ eq_(im.width(),im2.width())
+ eq_(im.height(),im2.height())
+ eq_(hashstr(im.tostring()),hashstr(im2.tostring()))
+ eq_(hashstr(im.tostring('tiff:method=tiled')),hashstr(im2.tostring('tiff:method=tiled')))
+ # should not be a blank image
+ eq_(hashstr(im.tostring("tiff")) != hashstr(mapnik.Image(im.width(),im.height(),mapnik.ImageType.gray32f).tostring("tiff")),True)
+
+if __name__ == "__main__":
+ setup()
+ exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/images/actual.png b/test/python_tests/images/actual.png
new file mode 100644
index 0000000..adfa856
Binary files /dev/null and b/test/python_tests/images/actual.png differ
diff --git a/test/python_tests/images/composited/clear.png b/test/python_tests/images/composited/clear.png
new file mode 100644
index 0000000..4fe9ab3
Binary files /dev/null and b/test/python_tests/images/composited/clear.png differ
diff --git a/test/python_tests/images/composited/color.png b/test/python_tests/images/composited/color.png
new file mode 100644
index 0000000..f9a6879
Binary files /dev/null and b/test/python_tests/images/composited/color.png differ
diff --git a/test/python_tests/images/composited/color_burn.png b/test/python_tests/images/composited/color_burn.png
new file mode 100644
index 0000000..af985bd
Binary files /dev/null and b/test/python_tests/images/composited/color_burn.png differ
diff --git a/test/python_tests/images/composited/color_dodge.png b/test/python_tests/images/composited/color_dodge.png
new file mode 100644
index 0000000..6c45b85
Binary files /dev/null and b/test/python_tests/images/composited/color_dodge.png differ
diff --git a/test/python_tests/images/composited/contrast.png b/test/python_tests/images/composited/contrast.png
new file mode 100644
index 0000000..54ea219
Binary files /dev/null and b/test/python_tests/images/composited/contrast.png differ
diff --git a/test/python_tests/images/composited/darken.png b/test/python_tests/images/composited/darken.png
new file mode 100644
index 0000000..4324c0a
Binary files /dev/null and b/test/python_tests/images/composited/darken.png differ
diff --git a/test/python_tests/images/composited/difference.png b/test/python_tests/images/composited/difference.png
new file mode 100644
index 0000000..312bded
Binary files /dev/null and b/test/python_tests/images/composited/difference.png differ
diff --git a/test/python_tests/images/composited/divide.png b/test/python_tests/images/composited/divide.png
new file mode 100644
index 0000000..0a4b24f
Binary files /dev/null and b/test/python_tests/images/composited/divide.png differ
diff --git a/test/python_tests/images/composited/dst.png b/test/python_tests/images/composited/dst.png
new file mode 100644
index 0000000..14be353
Binary files /dev/null and b/test/python_tests/images/composited/dst.png differ
diff --git a/test/python_tests/images/composited/dst_atop.png b/test/python_tests/images/composited/dst_atop.png
new file mode 100644
index 0000000..845c370
Binary files /dev/null and b/test/python_tests/images/composited/dst_atop.png differ
diff --git a/test/python_tests/images/composited/dst_in.png b/test/python_tests/images/composited/dst_in.png
new file mode 100644
index 0000000..1664be0
Binary files /dev/null and b/test/python_tests/images/composited/dst_in.png differ
diff --git a/test/python_tests/images/composited/dst_out.png b/test/python_tests/images/composited/dst_out.png
new file mode 100644
index 0000000..eb943bc
Binary files /dev/null and b/test/python_tests/images/composited/dst_out.png differ
diff --git a/test/python_tests/images/composited/dst_over.png b/test/python_tests/images/composited/dst_over.png
new file mode 100644
index 0000000..51fe08e
Binary files /dev/null and b/test/python_tests/images/composited/dst_over.png differ
diff --git a/test/python_tests/images/composited/exclusion.png b/test/python_tests/images/composited/exclusion.png
new file mode 100644
index 0000000..6cf4fa7
Binary files /dev/null and b/test/python_tests/images/composited/exclusion.png differ
diff --git a/test/python_tests/images/composited/grain_extract.png b/test/python_tests/images/composited/grain_extract.png
new file mode 100644
index 0000000..cfa03e1
Binary files /dev/null and b/test/python_tests/images/composited/grain_extract.png differ
diff --git a/test/python_tests/images/composited/grain_merge.png b/test/python_tests/images/composited/grain_merge.png
new file mode 100644
index 0000000..78de8b5
Binary files /dev/null and b/test/python_tests/images/composited/grain_merge.png differ
diff --git a/test/python_tests/images/composited/hard_light.png b/test/python_tests/images/composited/hard_light.png
new file mode 100644
index 0000000..9d878de
Binary files /dev/null and b/test/python_tests/images/composited/hard_light.png differ
diff --git a/test/python_tests/images/composited/hue.png b/test/python_tests/images/composited/hue.png
new file mode 100644
index 0000000..96ed7a6
Binary files /dev/null and b/test/python_tests/images/composited/hue.png differ
diff --git a/test/python_tests/images/composited/invert.png b/test/python_tests/images/composited/invert.png
new file mode 100644
index 0000000..03e8e94
Binary files /dev/null and b/test/python_tests/images/composited/invert.png differ
diff --git a/test/python_tests/images/composited/invert_rgb.png b/test/python_tests/images/composited/invert_rgb.png
new file mode 100644
index 0000000..5a8904f
Binary files /dev/null and b/test/python_tests/images/composited/invert_rgb.png differ
diff --git a/test/python_tests/images/composited/lighten.png b/test/python_tests/images/composited/lighten.png
new file mode 100644
index 0000000..3b8a860
Binary files /dev/null and b/test/python_tests/images/composited/lighten.png differ
diff --git a/test/python_tests/images/composited/linear_burn.png b/test/python_tests/images/composited/linear_burn.png
new file mode 100644
index 0000000..37ec4b7
Binary files /dev/null and b/test/python_tests/images/composited/linear_burn.png differ
diff --git a/test/python_tests/images/composited/linear_dodge.png b/test/python_tests/images/composited/linear_dodge.png
new file mode 100644
index 0000000..848ddca
Binary files /dev/null and b/test/python_tests/images/composited/linear_dodge.png differ
diff --git a/test/python_tests/images/composited/minus.png b/test/python_tests/images/composited/minus.png
new file mode 100644
index 0000000..46a7647
Binary files /dev/null and b/test/python_tests/images/composited/minus.png differ
diff --git a/test/python_tests/images/composited/multiply.png b/test/python_tests/images/composited/multiply.png
new file mode 100644
index 0000000..0c6880f
Binary files /dev/null and b/test/python_tests/images/composited/multiply.png differ
diff --git a/test/python_tests/images/composited/overlay.png b/test/python_tests/images/composited/overlay.png
new file mode 100644
index 0000000..77df0d3
Binary files /dev/null and b/test/python_tests/images/composited/overlay.png differ
diff --git a/test/python_tests/images/composited/plus.png b/test/python_tests/images/composited/plus.png
new file mode 100644
index 0000000..6656c63
Binary files /dev/null and b/test/python_tests/images/composited/plus.png differ
diff --git a/test/python_tests/images/composited/saturation.png b/test/python_tests/images/composited/saturation.png
new file mode 100644
index 0000000..52e9d6c
Binary files /dev/null and b/test/python_tests/images/composited/saturation.png differ
diff --git a/test/python_tests/images/composited/screen.png b/test/python_tests/images/composited/screen.png
new file mode 100644
index 0000000..df69486
Binary files /dev/null and b/test/python_tests/images/composited/screen.png differ
diff --git a/test/python_tests/images/composited/soft_light.png b/test/python_tests/images/composited/soft_light.png
new file mode 100644
index 0000000..954bef3
Binary files /dev/null and b/test/python_tests/images/composited/soft_light.png differ
diff --git a/test/python_tests/images/composited/src.png b/test/python_tests/images/composited/src.png
new file mode 100644
index 0000000..70aa18f
Binary files /dev/null and b/test/python_tests/images/composited/src.png differ
diff --git a/test/python_tests/images/composited/src_atop.png b/test/python_tests/images/composited/src_atop.png
new file mode 100644
index 0000000..5621a09
Binary files /dev/null and b/test/python_tests/images/composited/src_atop.png differ
diff --git a/test/python_tests/images/composited/src_in.png b/test/python_tests/images/composited/src_in.png
new file mode 100644
index 0000000..c2dbc51
Binary files /dev/null and b/test/python_tests/images/composited/src_in.png differ
diff --git a/test/python_tests/images/composited/src_out.png b/test/python_tests/images/composited/src_out.png
new file mode 100644
index 0000000..4df0d0a
Binary files /dev/null and b/test/python_tests/images/composited/src_out.png differ
diff --git a/test/python_tests/images/composited/src_over.png b/test/python_tests/images/composited/src_over.png
new file mode 100644
index 0000000..fcba78a
Binary files /dev/null and b/test/python_tests/images/composited/src_over.png differ
diff --git a/test/python_tests/images/composited/value.png b/test/python_tests/images/composited/value.png
new file mode 100644
index 0000000..70bcf4e
Binary files /dev/null and b/test/python_tests/images/composited/value.png differ
diff --git a/test/python_tests/images/composited/xor.png b/test/python_tests/images/composited/xor.png
new file mode 100644
index 0000000..b6f2f2f
Binary files /dev/null and b/test/python_tests/images/composited/xor.png differ
diff --git a/test/python_tests/images/expected.png b/test/python_tests/images/expected.png
new file mode 100644
index 0000000..5a27b46
Binary files /dev/null and b/test/python_tests/images/expected.png differ
diff --git a/test/python_tests/images/pycairo/cairo-cairo-expected-reduced.png b/test/python_tests/images/pycairo/cairo-cairo-expected-reduced.png
new file mode 100644
index 0000000..b99dc91
Binary files /dev/null and b/test/python_tests/images/pycairo/cairo-cairo-expected-reduced.png differ
diff --git a/test/python_tests/images/pycairo/cairo-cairo-expected.pdf b/test/python_tests/images/pycairo/cairo-cairo-expected.pdf
new file mode 100644
index 0000000..220a9b2
Binary files /dev/null and b/test/python_tests/images/pycairo/cairo-cairo-expected.pdf differ
diff --git a/test/python_tests/images/pycairo/cairo-cairo-expected.png b/test/python_tests/images/pycairo/cairo-cairo-expected.png
new file mode 100644
index 0000000..3a99f5e
Binary files /dev/null and b/test/python_tests/images/pycairo/cairo-cairo-expected.png differ
diff --git a/test/python_tests/images/pycairo/cairo-cairo-expected.svg b/test/python_tests/images/pycairo/cairo-cairo-expected.svg
new file mode 100644
index 0000000..18d7343
--- /dev/null
+++ b/test/python_tests/images/pycairo/cairo-cairo-expected.svg
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="512pt" height="512pt" viewBox="0 0 512 512" version="1.1">
+<defs>
+<g>
+<symbol overflow="visible" id="glyph0-0">
+<path style="stroke:none;" d="M 9.371094 -12.722656 L 9.464844 -12.722656 L 9.5625 -12.703125 L 9.273438 -12.703125 Z M 1.59375 -12.722656 L 1.6875 -12.722656 L 1.785156 -12.703125 L 1.496094 -12.703125 Z M 9.273438 -12.703125 L 9.5625 -12.703125 L 9.652344 -12.667969 L 9.183594 -12.667969 Z M 1.496094 -12.703125 L 1.785156 -12.703125 L 1.875 -12.667969 L 1.40625 -12.667969 Z M 9.183594 -12.667969 L 9.652344 -12.667969 L 9.734375 -12.617188 L 9.101562 -12.617188 Z M 1.40625 -12.667969 L [...]
+</symbol>
+<symbol overflow="visible" id="glyph0-1">
+<path style="stroke:none;" d="M 5.25 -8.832031 L 5.347656 -8.828125 L 4.941406 -8.828125 Z M 4.941406 -8.828125 L 5.347656 -8.828125 L 5.832031 -8.808594 L 4.726562 -8.808594 Z M 4.726562 -8.808594 L 5.832031 -8.808594 L 5.882812 -8.800781 L 4.644531 -8.800781 Z M 4.644531 -8.800781 L 5.882812 -8.800781 L 6.363281 -8.726562 L 4.191406 -8.726562 Z M 4.191406 -8.726562 L 6.363281 -8.726562 L 6.421875 -8.710938 L 4.097656 -8.710938 Z M 4.097656 -8.710938 L 6.421875 -8.710938 L 6.832031 -8.6 [...]
+</symbol>
+<symbol overflow="visible" id="glyph0-2">
+<path style="stroke:none;" d="M 1.59375 -12.722656 L 1.6875 -12.722656 L 1.785156 -12.703125 L 1.496094 -12.703125 Z M 1.496094 -12.703125 L 1.785156 -12.703125 L 1.875 -12.667969 L 1.40625 -12.667969 Z M 1.40625 -12.667969 L 1.875 -12.667969 L 1.957031 -12.617188 L 1.324219 -12.617188 Z M 1.324219 -12.617188 L 1.957031 -12.617188 L 2.03125 -12.550781 L 1.25 -12.550781 Z M 1.25 -12.550781 L 2.03125 -12.550781 L 2.089844 -12.472656 L 1.191406 -12.472656 Z M 1.191406 -12.472656 L 2.089844 [...]
+</symbol>
+<symbol overflow="visible" id="glyph0-3">
+<path style="stroke:none;" d="M 5.25 -8.832031 L 5.546875 -8.828125 L 4.941406 -8.828125 Z M 4.941406 -8.828125 L 5.546875 -8.828125 L 5.855469 -8.800781 L 4.644531 -8.800781 Z M 4.644531 -8.800781 L 5.855469 -8.800781 L 6.390625 -8.710938 L 4.097656 -8.710938 Z M 4.097656 -8.710938 L 6.390625 -8.710938 L 6.878906 -8.570312 L 3.609375 -8.570312 Z M 3.609375 -8.570312 L 6.878906 -8.570312 L 7.117188 -8.476562 L 3.367188 -8.476562 Z M 3.367188 -8.476562 L 7.117188 -8.476562 L 7.140625 -8.4 [...]
+</symbol>
+<symbol overflow="visible" id="glyph0-4">
+<path style="stroke:none;" d=""/>
+</symbol>
+<symbol overflow="visible" id="glyph0-5">
+<path style="stroke:none;" d="M 10.480469 -12.722656 L 10.574219 -12.722656 L 10.671875 -12.703125 L 10.382812 -12.703125 Z M 1.59375 -12.722656 L 1.6875 -12.722656 L 1.785156 -12.703125 L 1.496094 -12.703125 Z M 10.382812 -12.703125 L 10.671875 -12.703125 L 10.761719 -12.667969 L 10.292969 -12.667969 Z M 1.496094 -12.703125 L 1.785156 -12.703125 L 1.875 -12.667969 L 1.40625 -12.667969 Z M 10.292969 -12.667969 L 10.761719 -12.667969 L 10.84375 -12.617188 L 10.210938 -12.617188 Z M 1.4062 [...]
+</symbol>
+<symbol overflow="visible" id="glyph0-6">
+<path style="stroke:none;" d="M 5.25 -8.832031 L 5.425781 -8.828125 L 4.941406 -8.828125 Z M 4.941406 -8.828125 L 5.425781 -8.828125 L 5.769531 -8.820312 L 4.855469 -8.820312 Z M 8.257812 -8.832031 L 8.351562 -8.832031 L 8.449219 -8.8125 L 8.160156 -8.8125 Z M 4.855469 -8.820312 L 5.769531 -8.820312 L 5.941406 -8.800781 L 4.644531 -8.800781 Z M 8.160156 -8.8125 L 8.449219 -8.8125 L 8.539062 -8.777344 L 8.070312 -8.777344 Z M 4.644531 -8.800781 L 5.941406 -8.800781 L 6.28125 -8.761719 L 4 [...]
+</symbol>
+<symbol overflow="visible" id="glyph0-7">
+<path style="stroke:none;" d="M 4.695312 -8.832031 L 4.992188 -8.828125 L 4.613281 -8.828125 Z M 1.59375 -8.832031 L 1.6875 -8.832031 L 1.785156 -8.8125 L 1.496094 -8.8125 Z M 4.613281 -8.828125 L 4.992188 -8.828125 L 5.25 -8.804688 L 4.140625 -8.804688 Z M 4.140625 -8.804688 L 5.25 -8.804688 L 5.289062 -8.800781 L 4.105469 -8.800781 Z M 1.496094 -8.8125 L 1.785156 -8.8125 L 1.875 -8.777344 L 1.40625 -8.777344 Z M 4.105469 -8.800781 L 5.289062 -8.800781 L 5.457031 -8.773438 L 3.878906 -8 [...]
+</symbol>
+</g>
+</defs>
+<g id="surface1">
+<rect x="0" y="0" width="512" height="512" style="fill:rgb(27.45098%,50.980392%,70.588235%);fill-opacity:1;stroke:none;"/>
+<path style="fill-rule:nonzero;fill:rgb(0%,0%,100%);fill-opacity:1;stroke-width:0.5;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:4;" d="M 5 0 L 4.503906 2.167969 L 3.117188 3.910156 L 1.113281 4.875 L -1.113281 4.875 L -3.117188 3.910156 L -4.503906 2.167969 L -5 0 L -4.503906 -2.167969 L -3.117188 -3.910156 L -1.113281 -4.875 L 1.113281 -4.875 L 3.117188 -3.910156 L 4.503906 -2.167969 Z " transform="matrix(1,0,0,1,256,256)"/>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-0" x="218.722656" y="24.5"/>
+ <use xlink:href="#glyph0-1" x="229.778212" y="24.5"/>
+ <use xlink:href="#glyph0-2" x="239.722656" y="24.5"/>
+ <use xlink:href="#glyph0-2" x="243.000434" y="24.5"/>
+ <use xlink:href="#glyph0-3" x="246.278212" y="24.5"/>
+ <use xlink:href="#glyph0-4" x="256.778212" y="24.5"/>
+ <use xlink:href="#glyph0-5" x="261.167101" y="24.5"/>
+ <use xlink:href="#glyph0-6" x="273.333767" y="24.5"/>
+ <use xlink:href="#glyph0-7" x="283.278212" y="24.5"/>
+</g>
+<path style="fill:none;stroke-width:3;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 0 0 L 512 0 L 512 512 L 0 512 Z M 6 6 L 506 6 L 506 506 L 6 506 Z "/>
+</g>
+</svg>
diff --git a/test/python_tests/images/pycairo/cairo-surface-expected.building.pdf b/test/python_tests/images/pycairo/cairo-surface-expected.building.pdf
new file mode 100644
index 0000000..11559bb
Binary files /dev/null and b/test/python_tests/images/pycairo/cairo-surface-expected.building.pdf differ
diff --git a/test/python_tests/images/pycairo/cairo-surface-expected.building.svg b/test/python_tests/images/pycairo/cairo-surface-expected.building.svg
new file mode 100644
index 0000000..78bc15e
--- /dev/null
+++ b/test/python_tests/images/pycairo/cairo-surface-expected.building.svg
@@ -0,0 +1,261 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="256pt" height="256pt" viewBox="0 0 256 256" version="1.1">
+<g id="surface90">
+<rect x="0" y="0" width="256" height="256" style="fill:rgb(27.45098%,50.980392%,70.588235%);fill-opacity:1;stroke:none;"/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 132.222656 27.054688 L 141.789062 23.054688 L 141.789062 20.75 L 132.222656 24.746094 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 132.507812 28.515625 L 132.222656 27.054688 L 132.222656 24.746094 L 132.507812 26.207031 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 141.789062 23.054688 L 145.058594 32.933594 L 145.058594 30.628906 L 141.789062 20.75 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 108.777344 39.203125 L 132.507812 28.515625 L 132.507812 26.207031 L 108.777344 36.894531 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 127.984375 38.507812 L 122.578125 41.546875 L 122.578125 39.238281 L 127.984375 36.203125 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 102.367188 41.585938 L 108.777344 39.203125 L 108.777344 36.894531 L 102.367188 39.277344 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 131.019531 45.429688 L 127.984375 38.507812 L 127.984375 36.203125 L 131.019531 43.121094 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 122.578125 41.546875 L 118.730469 49.234375 L 118.730469 46.929688 L 122.578125 39.238281 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 105.6875 50.042969 L 102.367188 41.585938 L 102.367188 39.277344 L 105.6875 47.734375 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 134.109375 51.578125 L 131.019531 45.429688 L 131.019531 43.121094 L 134.109375 49.273438 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 118.730469 49.234375 L 115.65625 56.117188 L 115.65625 53.808594 L 118.730469 46.929688 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 115.65625 56.117188 L 105.6875 50.042969 L 105.6875 47.734375 L 115.65625 53.808594 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 136.332031 59.265625 L 134.109375 51.578125 L 134.109375 49.273438 L 136.332031 56.960938 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 135.882812 66.1875 L 136.332031 59.265625 L 136.332031 56.960938 L 135.882812 63.878906 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 145.058594 32.933594 L 157.566406 68.800781 L 157.566406 66.496094 L 145.058594 30.628906 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 135.675781 71.570312 L 135.882812 66.1875 L 135.882812 63.878906 L 135.675781 69.261719 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 157.566406 68.800781 L 159.464844 73.835938 L 159.464844 71.53125 L 157.566406 66.496094 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 139.804688 81.023438 L 135.675781 71.570312 L 135.675781 69.261719 L 139.804688 78.71875 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 159.464844 73.835938 L 139.804688 81.023438 L 139.804688 78.71875 L 159.464844 71.53125 "/>
+<path style="fill:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(80%,80%,0%);stroke-opacity:0.7;stroke-miterlimit:10;" d="M 115.65625 56.117188 L 105.6875 50.042969 L 102.367188 41.585938 L 108.777344 39.203125 L 132.507812 28.515625 L 132.222656 27.054688 L 141.789062 23.054688 L 145.058594 32.933594 L 157.566406 68.800781 L 159.464844 73.835938 L 139.804688 81.023438 L 135.675781 71.570312 L 135.882812 66.1875 L 136.332031 59.265625 L 134.109375 51.578125 L 13 [...]
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,0%);fill-opacity:0.7;" d="M 115.65625 53.808594 L 105.6875 47.734375 L 102.367188 39.277344 L 108.777344 36.894531 L 132.507812 26.207031 L 132.222656 24.746094 L 141.789062 20.75 L 145.058594 30.628906 L 157.566406 66.496094 L 159.464844 71.53125 L 139.804688 78.71875 L 135.675781 69.261719 L 135.882812 63.878906 L 136.332031 56.960938 L 134.109375 49.273438 L 131.019531 43.121094 L 127.984375 36.203125 L 122.578125 39.23828 [...]
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 132.222656 27.054688 L 132.507812 28.515625 L 132.507812 26.207031 L 132.222656 24.746094 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 132.507812 28.515625 L 108.777344 39.203125 L 108.777344 36.894531 L 132.507812 26.207031 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 101.800781 39.277344 L 132.222656 27.054688 L 132.222656 24.746094 L 101.800781 36.972656 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 108.777344 39.203125 L 102.367188 41.585938 L 102.367188 39.277344 L 108.777344 36.894531 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 102.367188 41.585938 L 105.6875 50.042969 L 105.6875 47.734375 L 102.367188 39.277344 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 64.152344 54.578125 L 101.800781 39.277344 L 101.800781 36.972656 L 64.152344 52.269531 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 105.6875 50.042969 L 115.65625 56.117188 L 115.65625 53.808594 L 105.6875 47.734375 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 53.773438 58.652344 L 64.152344 54.578125 L 64.152344 52.269531 L 53.773438 56.347656 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 56.199219 60.996094 L 54.90625 61.496094 L 54.90625 59.191406 L 56.199219 58.691406 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 54.90625 61.496094 L 53.773438 58.652344 L 53.773438 56.347656 L 54.90625 59.191406 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 115.65625 56.117188 L 112.578125 61.574219 L 112.578125 59.265625 L 115.65625 53.808594 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 62.101562 68.839844 L 56.199219 60.996094 L 56.199219 58.691406 L 62.101562 66.53125 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 88.605469 74.414062 L 84.253906 74.566406 L 84.253906 72.261719 L 88.605469 72.105469 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 105.039062 74.605469 L 88.605469 74.414062 L 88.605469 72.105469 L 105.039062 72.300781 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 112.578125 61.574219 L 105.039062 74.605469 L 105.039062 72.300781 L 112.578125 59.265625 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 84.253906 74.566406 L 79.761719 74.644531 L 79.761719 72.335938 L 84.253906 72.261719 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 79.761719 74.644531 L 68.664062 79.027344 L 68.664062 76.71875 L 79.761719 72.335938 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 67.753906 79.296875 L 62.101562 68.839844 L 62.101562 66.53125 L 67.753906 76.988281 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 68.664062 79.027344 L 67.753906 79.296875 L 67.753906 76.988281 L 68.664062 76.71875 "/>
+<path style="fill:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(80%,80%,0%);stroke-opacity:0.7;stroke-miterlimit:10;" d="M 132.222656 27.054688 L 132.507812 28.515625 L 108.777344 39.203125 L 102.367188 41.585938 L 105.6875 50.042969 L 115.65625 56.117188 L 112.578125 61.574219 L 105.039062 74.605469 L 88.605469 74.414062 L 84.253906 74.566406 L 79.761719 74.644531 L 68.664062 79.027344 L 67.753906 79.296875 L 62.101562 68.839844 L 56.199219 60.996094 L 54.9062 [...]
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,0%);fill-opacity:0.7;" d="M 132.222656 24.746094 L 132.507812 26.207031 L 108.777344 36.894531 L 102.367188 39.277344 L 105.6875 47.734375 L 115.65625 53.808594 L 112.578125 59.265625 L 105.039062 72.300781 L 88.605469 72.105469 L 84.253906 72.261719 L 79.761719 72.335938 L 68.664062 76.71875 L 67.753906 76.988281 L 62.101562 66.53125 L 56.199219 58.691406 L 54.90625 59.191406 L 53.773438 56.347656 L 64.152344 52.269531 L 101 [...]
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 122.578125 41.546875 L 127.984375 38.507812 L 127.984375 36.203125 L 122.578125 39.238281 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 127.984375 38.507812 L 131.019531 45.429688 L 131.019531 43.121094 L 127.984375 36.203125 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 118.730469 49.234375 L 122.578125 41.546875 L 122.578125 39.238281 L 118.730469 46.929688 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 131.019531 45.429688 L 134.109375 51.578125 L 134.109375 49.273438 L 131.019531 43.121094 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 115.65625 56.117188 L 118.730469 49.234375 L 118.730469 46.929688 L 115.65625 53.808594 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 134.109375 51.578125 L 136.332031 59.265625 L 136.332031 56.960938 L 134.109375 49.273438 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 112.578125 61.574219 L 115.65625 56.117188 L 115.65625 53.808594 L 112.578125 59.265625 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 136.332031 59.265625 L 135.882812 66.1875 L 135.882812 63.878906 L 136.332031 56.960938 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 135.882812 66.1875 L 129.527344 66.6875 L 129.527344 64.378906 L 135.882812 63.878906 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 129.527344 66.6875 L 124.503906 68.453125 L 124.503906 66.148438 L 129.527344 64.378906 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 124.503906 68.453125 L 121.441406 69.992188 L 121.441406 67.6875 L 124.503906 66.148438 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 84.253906 74.566406 L 88.605469 74.414062 L 88.605469 72.105469 L 84.253906 72.261719 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 105.039062 74.605469 L 112.578125 61.574219 L 112.578125 59.265625 L 105.039062 72.300781 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 88.605469 74.414062 L 105.039062 74.605469 L 105.039062 72.300781 L 88.605469 72.105469 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 79.761719 74.644531 L 84.253906 74.566406 L 84.253906 72.261719 L 79.761719 72.335938 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 68.664062 79.027344 L 79.761719 74.644531 L 79.761719 72.335938 L 68.664062 76.71875 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 67.753906 79.296875 L 68.664062 79.027344 L 68.664062 76.71875 L 67.753906 76.988281 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 121.441406 69.992188 L 109.082031 80.371094 L 109.082031 78.066406 L 121.441406 67.6875 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 109.082031 80.371094 L 104.925781 81.371094 L 104.925781 79.066406 L 109.082031 78.066406 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 61.679688 81.753906 L 67.753906 79.296875 L 67.753906 76.988281 L 61.679688 79.449219 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 104.925781 81.371094 L 102.195312 83.253906 L 102.195312 80.949219 L 104.925781 79.066406 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 102.195312 83.253906 L 101.664062 85.136719 L 101.664062 82.832031 L 102.195312 80.949219 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 101.664062 85.136719 L 100.078125 88.445312 L 100.078125 86.136719 L 101.664062 82.832031 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 100.078125 88.445312 L 97.773438 91.671875 L 97.773438 89.367188 L 100.078125 86.136719 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 97.773438 91.671875 L 94.757812 93.558594 L 94.757812 91.25 L 97.773438 89.367188 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 94.757812 93.558594 L 91.441406 100.015625 L 91.441406 97.707031 L 94.757812 91.25 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 91.441406 100.015625 L 71.058594 108.050781 L 71.058594 105.742188 L 91.441406 97.707031 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 71.058594 108.050781 L 61.679688 81.753906 L 61.679688 79.449219 L 71.058594 105.742188 "/>
+<path style="fill:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(80%,80%,0%);stroke-opacity:0.7;stroke-miterlimit:10;" d="M 115.65625 56.117188 L 118.730469 49.234375 L 122.578125 41.546875 L 127.984375 38.507812 L 131.019531 45.429688 L 134.109375 51.578125 L 136.332031 59.265625 L 135.882812 66.1875 L 129.527344 66.6875 L 124.503906 68.453125 L 121.441406 69.992188 L 109.082031 80.371094 L 104.925781 81.371094 L 102.195312 83.253906 L 101.664062 85.136719 L 10 [...]
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,0%);fill-opacity:0.7;" d="M 115.65625 53.808594 L 118.730469 46.929688 L 122.578125 39.238281 L 127.984375 36.203125 L 131.019531 43.121094 L 134.109375 49.273438 L 136.332031 56.960938 L 135.882812 63.878906 L 129.527344 64.378906 L 124.503906 66.148438 L 121.441406 67.6875 L 109.082031 78.066406 L 104.925781 79.066406 L 102.195312 80.949219 L 101.664062 82.832031 L 100.078125 86.136719 L 97.773438 89.367188 L 94.757812 91.2 [...]
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 53.773438 58.652344 L 54.90625 61.496094 L 54.90625 59.191406 L 53.773438 56.347656 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 44.109375 62.304688 L 53.773438 58.652344 L 53.773438 56.347656 L 44.109375 59.996094 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 0 79.527344 L 44.109375 62.304688 L 44.109375 59.996094 L 0 77.21875 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 54.90625 61.496094 L 61.679688 81.753906 L 61.679688 79.449219 L 54.90625 59.191406 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 23.488281 86.253906 L 21.667969 87.945312 L 21.667969 85.636719 L 23.488281 83.945312 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 28.25 90.710938 L 23.488281 86.253906 L 23.488281 83.945312 L 28.25 88.40625 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 21.667969 87.945312 L 16.679688 93.402344 L 16.679688 91.097656 L 21.667969 85.636719 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 7.113281 96.59375 L 0 79.527344 L 0 77.21875 L 7.113281 94.289062 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 16.679688 93.402344 L 11.117188 99.59375 L 11.117188 97.285156 L 16.679688 91.097656 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 11.117188 99.59375 L 7.113281 96.59375 L 7.113281 94.289062 L 11.117188 97.285156 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 14.351562 105.242188 L 28.25 90.710938 L 28.25 88.40625 L 14.351562 102.9375 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 10.238281 106.011719 L 14.351562 105.242188 L 14.351562 102.9375 L 10.238281 103.707031 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 61.679688 81.753906 L 71.058594 108.050781 L 71.058594 105.742188 L 61.679688 79.449219 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 10.171875 111.96875 L 10.238281 106.011719 L 10.238281 103.707031 L 10.171875 109.664062 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 9.871094 116.082031 L 10.171875 111.96875 L 10.171875 109.664062 L 9.871094 113.777344 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 54.027344 114.699219 L 44.699219 118.351562 L 44.699219 116.042969 L 54.027344 112.394531 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 71.058594 108.050781 L 76.140625 119.121094 L 76.140625 116.8125 L 71.058594 105.742188 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 11.242188 120.925781 L 9.871094 116.082031 L 9.871094 113.777344 L 11.242188 118.621094 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 76.140625 119.121094 L 72.308594 122.695312 L 72.308594 120.390625 L 76.140625 116.8125 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 44.699219 118.351562 L 33.3125 123.15625 L 33.3125 120.851562 L 44.699219 116.042969 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 14.414062 130.113281 L 11.242188 120.925781 L 11.242188 118.621094 L 14.414062 127.808594 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 33.3125 123.15625 L 14.414062 130.113281 L 14.414062 127.808594 L 33.3125 120.851562 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 72.308594 122.695312 L 69.605469 130.535156 L 69.605469 128.230469 L 72.308594 120.390625 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 61.425781 133.496094 L 54.027344 114.699219 L 54.027344 112.394531 L 61.425781 131.191406 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 69.605469 130.535156 L 61.425781 133.496094 L 61.425781 131.191406 L 69.605469 128.230469 "/>
+<path style="fill:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(80%,80%,0%);stroke-opacity:0.7;stroke-miterlimit:10;" d="M 53.773438 58.652344 L 54.90625 61.496094 L 61.679688 81.753906 L 71.058594 108.050781 L 76.140625 119.121094 L 72.308594 122.695312 L 69.605469 130.535156 L 61.425781 133.496094 L 54.027344 114.699219 L 44.699219 118.351562 L 33.3125 123.15625 L 14.414062 130.113281 L 11.242188 120.925781 L 9.871094 116.082031 L 10.171875 111.96875 L 10.238 [...]
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,0%);fill-opacity:0.7;" d="M 53.773438 56.347656 L 54.90625 59.191406 L 61.679688 79.449219 L 71.058594 105.742188 L 76.140625 116.8125 L 72.308594 120.390625 L 69.605469 128.230469 L 61.425781 131.191406 L 54.027344 112.394531 L 44.699219 116.042969 L 33.3125 120.851562 L 14.414062 127.808594 L 11.242188 118.621094 L 9.871094 113.777344 L 10.171875 109.664062 L 10.238281 103.707031 L 14.351562 102.9375 L 28.25 88.40625 L 23.4 [...]
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 54.90625 61.496094 L 56.199219 60.996094 L 56.199219 58.691406 L 54.90625 59.191406 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 56.199219 60.996094 L 62.101562 68.839844 L 62.101562 66.53125 L 56.199219 58.691406 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 62.101562 68.839844 L 67.753906 79.296875 L 67.753906 76.988281 L 62.101562 66.53125 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 67.753906 79.296875 L 61.679688 81.753906 L 61.679688 79.449219 L 67.753906 76.988281 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 61.679688 81.753906 L 54.90625 61.496094 L 54.90625 59.191406 L 61.679688 79.449219 "/>
+<path style="fill:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(80%,80%,0%);stroke-opacity:0.7;stroke-miterlimit:10;" d="M 54.90625 61.496094 L 56.199219 60.996094 L 62.101562 68.839844 L 67.753906 79.296875 L 61.679688 81.753906 Z M 54.90625 61.496094 L 54.90625 59.191406 M 56.199219 60.996094 L 56.199219 58.691406 M 62.101562 68.839844 L 62.101562 66.53125 M 67.753906 79.296875 L 67.753906 76.988281 M 61.679688 81.753906 L 61.679688 79.449219 M 54.90625 59.19 [...]
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,0%);fill-opacity:0.7;" d="M 54.90625 59.191406 L 56.199219 58.691406 L 62.101562 66.53125 L 67.753906 76.988281 L 61.679688 79.449219 Z "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 129.527344 66.6875 L 135.882812 66.1875 L 135.882812 63.878906 L 129.527344 64.378906 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 124.503906 68.453125 L 129.527344 66.6875 L 129.527344 64.378906 L 124.503906 66.148438 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 121.441406 69.992188 L 124.503906 68.453125 L 124.503906 66.148438 L 121.441406 67.6875 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 135.882812 66.1875 L 135.675781 71.570312 L 135.675781 69.261719 L 135.882812 63.878906 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 109.082031 80.371094 L 121.441406 69.992188 L 121.441406 67.6875 L 109.082031 78.066406 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 135.675781 71.570312 L 139.804688 81.023438 L 139.804688 78.71875 L 135.675781 69.261719 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 104.925781 81.371094 L 109.082031 80.371094 L 109.082031 78.066406 L 104.925781 79.066406 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 102.195312 83.253906 L 104.925781 81.371094 L 104.925781 79.066406 L 102.195312 80.949219 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 101.664062 85.136719 L 102.195312 83.253906 L 102.195312 80.949219 L 101.664062 82.832031 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 139.804688 81.023438 L 127.082031 86.292969 L 127.082031 83.984375 L 139.804688 78.71875 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 100.078125 88.445312 L 101.664062 85.136719 L 101.664062 82.832031 L 100.078125 86.136719 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 109.199219 89.058594 L 101.835938 91.441406 L 101.835938 89.136719 L 109.199219 86.753906 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 97.773438 91.671875 L 100.078125 88.445312 L 100.078125 86.136719 L 97.773438 89.367188 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 110.335938 92.902344 L 109.199219 89.058594 L 109.199219 86.753906 L 110.335938 90.597656 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 127.082031 86.292969 L 110.335938 92.902344 L 110.335938 90.597656 L 127.082031 83.984375 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 94.757812 93.558594 L 97.773438 91.671875 L 97.773438 89.367188 L 94.757812 91.25 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 101.835938 91.441406 L 103.257812 95.363281 L 103.257812 93.058594 L 101.835938 89.136719 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 91.441406 100.015625 L 94.757812 93.558594 L 94.757812 91.25 L 91.441406 97.707031 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 103.257812 95.363281 L 91.441406 100.015625 L 91.441406 97.707031 L 103.257812 93.058594 "/>
+<path style="fill:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(80%,80%,0%);stroke-opacity:0.7;stroke-miterlimit:10;" d="M 135.882812 66.1875 L 135.675781 71.570312 L 139.804688 81.023438 L 127.082031 86.292969 L 110.335938 92.902344 L 109.199219 89.058594 L 101.835938 91.441406 L 103.257812 95.363281 L 91.441406 100.015625 L 94.757812 93.558594 L 97.773438 91.671875 L 100.078125 88.445312 L 101.664062 85.136719 L 102.195312 83.253906 L 104.925781 81.371094 L 1 [...]
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,0%);fill-opacity:0.7;" d="M 135.882812 63.878906 L 135.675781 69.261719 L 139.804688 78.71875 L 127.082031 83.984375 L 110.335938 90.597656 L 109.199219 86.753906 L 101.835938 89.136719 L 103.257812 93.058594 L 91.441406 97.707031 L 94.757812 91.25 L 97.773438 89.367188 L 100.078125 86.136719 L 101.664062 82.832031 L 102.195312 80.949219 L 104.925781 79.066406 L 109.082031 78.066406 L 121.441406 67.6875 L 124.503906 66.148438 [...]
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 159.464844 73.835938 L 170.804688 68.917969 L 170.804688 66.609375 L 159.464844 71.53125 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 170.804688 68.917969 L 173.171875 76.296875 L 173.171875 73.992188 L 170.804688 66.609375 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 139.804688 81.023438 L 159.464844 73.835938 L 159.464844 71.53125 L 139.804688 78.71875 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 173.171875 76.296875 L 176.019531 82.679688 L 176.019531 80.371094 L 173.171875 73.992188 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 127.082031 86.292969 L 139.804688 81.023438 L 139.804688 78.71875 L 127.082031 83.984375 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 149.34375 87.90625 L 143.125 89.828125 L 143.125 87.523438 L 149.34375 85.601562 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 174.757812 89.945312 L 171.078125 89.90625 L 171.078125 87.597656 L 174.757812 87.636719 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 176.019531 82.679688 L 183.402344 90.136719 L 183.402344 87.828125 L 176.019531 80.371094 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 128.355469 90.597656 L 127.082031 86.292969 L 127.082031 83.984375 L 128.355469 88.289062 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 143.125 89.828125 L 136.710938 92.828125 L 136.710938 90.519531 L 143.125 87.523438 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 171.078125 89.90625 L 169.109375 93.441406 L 169.109375 91.136719 L 171.078125 87.597656 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 183.402344 90.136719 L 184.109375 93.789062 L 184.109375 91.480469 L 183.402344 87.828125 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 184.109375 93.789062 L 174.757812 89.945312 L 174.757812 87.636719 L 184.109375 91.480469 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 136.710938 92.828125 L 130.351562 95.019531 L 130.351562 92.710938 L 136.710938 90.519531 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 130.351562 95.019531 L 128.355469 90.597656 L 128.355469 88.289062 L 130.351562 92.710938 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 169.109375 93.441406 L 185.757812 103.707031 L 185.757812 101.398438 L 169.109375 91.136719 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 155.929688 105.933594 L 149.34375 87.90625 L 149.34375 85.601562 L 155.929688 103.628906 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 155.917969 110.203125 L 155.929688 105.933594 L 155.929688 103.628906 L 155.917969 107.894531 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 185.757812 103.707031 L 182.722656 112.007812 L 182.722656 109.703125 L 185.757812 101.398438 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 182.722656 112.007812 L 181.617188 113.125 L 181.617188 110.816406 L 182.722656 109.703125 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 181.617188 113.125 L 179.890625 116.3125 L 179.890625 114.007812 L 181.617188 110.816406 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 153.800781 116.507812 L 155.917969 110.203125 L 155.917969 107.894531 L 153.800781 114.199219 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 179.890625 116.3125 L 174.433594 119.734375 L 174.433594 117.429688 L 179.890625 114.007812 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 151.734375 120.082031 L 153.800781 116.507812 L 153.800781 114.199219 L 151.734375 117.773438 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 174.433594 119.734375 L 167.976562 121.773438 L 167.976562 119.464844 L 174.433594 117.429688 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 147.519531 123.503906 L 151.734375 120.082031 L 151.734375 117.773438 L 147.519531 121.195312 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 167.976562 121.773438 L 157.070312 125.578125 L 157.070312 123.273438 L 167.976562 119.464844 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 141.585938 126.386719 L 147.519531 123.503906 L 147.519531 121.195312 L 141.585938 124.078125 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 144.289062 132.804688 L 141.585938 126.386719 L 141.585938 124.078125 L 144.289062 130.5 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 157.070312 125.578125 L 144.289062 132.804688 L 144.289062 130.5 L 157.070312 123.273438 "/>
+<path style="fill:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(80%,80%,0%);stroke-opacity:0.7;stroke-miterlimit:10;" d="M 159.464844 73.835938 L 170.804688 68.917969 L 173.171875 76.296875 L 176.019531 82.679688 L 183.402344 90.136719 L 184.109375 93.789062 L 174.757812 89.945312 L 171.078125 89.90625 L 169.109375 93.441406 L 185.757812 103.707031 L 182.722656 112.007812 L 181.617188 113.125 L 179.890625 116.3125 L 174.433594 119.734375 L 167.976562 121.773438 [...]
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,0%);fill-opacity:0.7;" d="M 159.464844 71.53125 L 170.804688 66.609375 L 173.171875 73.992188 L 176.019531 80.371094 L 183.402344 87.828125 L 184.109375 91.480469 L 174.757812 87.636719 L 171.078125 87.597656 L 169.109375 91.136719 L 185.757812 101.398438 L 182.722656 109.703125 L 181.617188 110.816406 L 179.890625 114.007812 L 174.433594 117.429688 L 167.976562 119.464844 L 157.070312 123.273438 L 144.289062 130.5 L 141.5859 [...]
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 247.820312 71.339844 L 250.59375 70.03125 L 250.59375 67.722656 L 247.820312 69.03125 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 245.570312 72.53125 L 247.820312 71.339844 L 247.820312 69.03125 L 245.570312 70.222656 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 241.550781 74.835938 L 245.570312 72.53125 L 245.570312 70.222656 L 241.550781 72.53125 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 213.167969 74.914062 L 216.90625 70.414062 L 216.90625 68.109375 L 213.167969 72.605469 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 250.59375 70.03125 L 252.679688 76.488281 L 252.679688 74.183594 L 250.59375 67.722656 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 216.90625 70.414062 L 228.015625 79.488281 L 228.015625 77.179688 L 216.90625 68.109375 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 205.175781 79.601562 L 213.167969 74.914062 L 213.167969 72.605469 L 205.175781 77.296875 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 242.207031 79.796875 L 241.550781 74.835938 L 241.550781 72.53125 L 242.207031 77.488281 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 201.0625 81.371094 L 205.175781 79.601562 L 205.175781 77.296875 L 201.0625 79.066406 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 200.394531 81.488281 L 201.0625 81.371094 L 201.0625 79.066406 L 200.394531 79.179688 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 252.679688 76.488281 L 254.8125 83.101562 L 254.8125 80.792969 L 252.679688 74.183594 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 192.929688 83.832031 L 200.394531 81.488281 L 200.394531 79.179688 L 192.929688 81.523438 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 228.765625 84.601562 L 242.207031 79.796875 L 242.207031 77.488281 L 228.765625 82.292969 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 228.015625 79.488281 L 228.765625 84.601562 L 228.765625 82.292969 L 228.015625 77.179688 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 254.8125 83.101562 L 256 85.5625 L 256 83.253906 L 254.8125 80.792969 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 256 85.5625 L 255.28125 85.714844 L 255.28125 83.410156 L 256 83.253906 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 188.046875 87.058594 L 192.929688 83.832031 L 192.929688 81.523438 L 188.046875 84.753906 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 171.078125 89.90625 L 174.757812 89.945312 L 174.757812 87.636719 L 171.078125 87.597656 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 183.402344 90.136719 L 188.046875 87.058594 L 188.046875 84.753906 L 183.402344 87.828125 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 169.109375 93.441406 L 171.078125 89.90625 L 171.078125 87.597656 L 169.109375 91.136719 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 184.109375 93.789062 L 183.402344 90.136719 L 183.402344 87.828125 L 184.109375 91.480469 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 174.757812 89.945312 L 184.109375 93.789062 L 184.109375 91.480469 L 174.757812 87.636719 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 255.28125 85.714844 L 224.476562 96.902344 L 224.476562 94.59375 L 255.28125 83.410156 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 224.476562 96.902344 L 222.519531 98.167969 L 222.519531 95.863281 L 224.476562 94.59375 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 222.519531 98.167969 L 221.703125 98.9375 L 221.703125 96.632812 L 222.519531 95.863281 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 221.703125 98.9375 L 220.648438 100.207031 L 220.648438 97.902344 L 221.703125 96.632812 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 220.648438 100.207031 L 218.683594 102.28125 L 218.683594 99.976562 L 220.648438 97.902344 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 185.757812 103.707031 L 169.109375 93.441406 L 169.109375 91.136719 L 185.757812 101.398438 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 218.683594 102.28125 L 217.1875 107.625 L 217.1875 105.320312 L 218.683594 99.976562 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 217.1875 107.625 L 217.277344 109.933594 L 217.277344 107.625 L 217.1875 105.320312 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 182.722656 112.007812 L 185.757812 103.707031 L 185.757812 101.398438 L 182.722656 109.703125 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 181.617188 113.125 L 182.722656 112.007812 L 182.722656 109.703125 L 181.617188 110.816406 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 179.890625 116.3125 L 181.617188 113.125 L 181.617188 110.816406 L 179.890625 114.007812 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 174.433594 119.734375 L 179.890625 116.3125 L 179.890625 114.007812 L 174.433594 117.429688 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 167.976562 121.773438 L 174.433594 119.734375 L 174.433594 117.429688 L 167.976562 119.464844 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 157.070312 125.578125 L 167.976562 121.773438 L 167.976562 119.464844 L 157.070312 123.273438 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 138.746094 132.382812 L 144.289062 132.804688 L 144.289062 130.5 L 138.746094 130.074219 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 144.289062 132.804688 L 157.070312 125.578125 L 157.070312 123.273438 L 144.289062 130.5 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 217.277344 109.933594 L 221.75 133.035156 L 221.75 130.730469 L 217.277344 107.625 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 221.75 133.035156 L 217.414062 146.066406 L 217.414062 143.761719 L 221.75 130.730469 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 217.414062 146.066406 L 216.40625 148.449219 L 216.40625 146.144531 L 217.414062 143.761719 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 94.914062 153.0625 L 138.746094 132.382812 L 138.746094 130.074219 L 94.914062 150.757812 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 216.40625 148.449219 L 209.457031 155.371094 L 209.457031 153.0625 L 216.40625 146.144531 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 90.503906 158.40625 L 94.914062 153.0625 L 94.914062 150.757812 L 90.503906 156.101562 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 209.457031 155.371094 L 197.367188 172.246094 L 197.367188 169.9375 L 209.457031 153.0625 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 197.367188 172.246094 L 193.015625 173.746094 L 193.015625 171.4375 L 197.367188 169.9375 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 193.015625 173.746094 L 186.796875 175.4375 L 186.796875 173.128906 L 193.015625 171.4375 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 186.796875 175.4375 L 179.820312 175.511719 L 179.820312 173.207031 L 186.796875 173.128906 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 179.820312 175.511719 L 175.621094 173.015625 L 175.621094 170.707031 L 179.820312 173.207031 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 175.621094 173.015625 L 151.179688 179.433594 L 151.179688 177.128906 L 175.621094 170.707031 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 151.179688 179.433594 L 146.535156 182.738281 L 146.535156 180.433594 L 151.179688 177.128906 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 146.535156 182.738281 L 144.859375 184.625 L 144.859375 182.316406 L 146.535156 180.433594 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 144.859375 184.625 L 144.503906 190.695312 L 144.503906 188.390625 L 144.859375 182.316406 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 144.503906 190.695312 L 150.546875 200.269531 L 150.546875 197.960938 L 144.503906 188.390625 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 108.660156 207.035156 L 90.503906 158.40625 L 90.503906 156.101562 L 108.660156 204.726562 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 150.546875 200.269531 L 153.316406 218.875 L 153.316406 216.566406 L 150.546875 197.960938 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 153.316406 218.875 L 145.023438 226.640625 L 145.023438 224.332031 L 153.316406 216.566406 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 117.859375 231.675781 L 108.660156 207.035156 L 108.660156 204.726562 L 117.859375 229.367188 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 145.023438 226.640625 L 118.382812 232.945312 L 118.382812 230.636719 L 145.023438 224.332031 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 118.382812 232.945312 L 117.859375 231.675781 L 117.859375 229.367188 L 118.382812 230.636719 "/>
+<path style="fill:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(80%,80%,0%);stroke-opacity:0.7;stroke-miterlimit:10;" d="M 183.402344 90.136719 L 188.046875 87.058594 L 192.929688 83.832031 L 200.394531 81.488281 L 201.0625 81.371094 L 205.175781 79.601562 L 213.167969 74.914062 L 216.90625 70.414062 L 228.015625 79.488281 L 228.765625 84.601562 L 242.207031 79.796875 L 241.550781 74.835938 L 245.570312 72.53125 L 247.820312 71.339844 L 250.59375 70.03125 L 252 [...]
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,0%);fill-opacity:0.7;" d="M 183.402344 87.828125 L 188.046875 84.753906 L 192.929688 81.523438 L 200.394531 79.179688 L 201.0625 79.066406 L 205.175781 77.296875 L 213.167969 72.605469 L 216.90625 68.109375 L 228.015625 77.179688 L 228.765625 82.292969 L 242.207031 77.488281 L 241.550781 72.53125 L 245.570312 70.222656 L 247.820312 69.03125 L 250.59375 67.722656 L 252.679688 74.183594 L 254.8125 80.792969 L 256 83.253906 L 25 [...]
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 143.125 89.828125 L 149.34375 87.90625 L 149.34375 85.601562 L 143.125 87.523438 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 127.082031 86.292969 L 128.355469 90.597656 L 128.355469 88.289062 L 127.082031 83.984375 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 136.710938 92.828125 L 143.125 89.828125 L 143.125 87.523438 L 136.710938 90.519531 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 110.335938 92.902344 L 127.082031 86.292969 L 127.082031 83.984375 L 110.335938 90.597656 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 128.355469 90.597656 L 130.351562 95.019531 L 130.351562 92.710938 L 128.355469 88.289062 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 130.351562 95.019531 L 136.710938 92.828125 L 136.710938 90.519531 L 130.351562 92.710938 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 103.257812 95.363281 L 110.335938 92.902344 L 110.335938 90.597656 L 103.257812 93.058594 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 91.441406 100.015625 L 103.257812 95.363281 L 103.257812 93.058594 L 91.441406 97.707031 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 149.34375 87.90625 L 155.929688 105.933594 L 155.929688 103.628906 L 149.34375 85.601562 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 71.058594 108.050781 L 91.441406 100.015625 L 91.441406 97.707031 L 71.058594 105.742188 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 155.929688 105.933594 L 155.917969 110.203125 L 155.917969 107.894531 L 155.929688 103.628906 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 155.917969 110.203125 L 153.800781 116.507812 L 153.800781 114.199219 L 155.917969 107.894531 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 76.140625 119.121094 L 71.058594 108.050781 L 71.058594 105.742188 L 76.140625 116.8125 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 153.800781 116.507812 L 151.734375 120.082031 L 151.734375 117.773438 L 153.800781 114.199219 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 151.734375 120.082031 L 147.519531 123.503906 L 147.519531 121.195312 L 151.734375 117.773438 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 147.519531 123.503906 L 141.585938 126.386719 L 141.585938 124.078125 L 147.519531 121.195312 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 79.503906 129.191406 L 76.140625 119.121094 L 76.140625 116.8125 L 79.503906 126.886719 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 144.289062 132.804688 L 138.746094 132.382812 L 138.746094 130.074219 L 144.289062 130.5 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 141.585938 126.386719 L 144.289062 132.804688 L 144.289062 130.5 L 141.585938 124.078125 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 83.292969 139.1875 L 79.503906 129.191406 L 79.503906 126.886719 L 83.292969 136.878906 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 86.144531 145.105469 L 83.292969 139.1875 L 83.292969 136.878906 L 86.144531 142.800781 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 86.664062 147.644531 L 86.144531 145.105469 L 86.144531 142.800781 L 86.664062 145.335938 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 138.746094 132.382812 L 94.914062 153.0625 L 94.914062 150.757812 L 138.746094 130.074219 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 90.121094 157.292969 L 86.664062 147.644531 L 86.664062 145.335938 L 90.121094 154.984375 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 94.914062 153.0625 L 90.503906 158.40625 L 90.503906 156.101562 L 94.914062 150.757812 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 90.503906 158.40625 L 90.121094 157.292969 L 90.121094 154.984375 L 90.503906 156.101562 "/>
+<path style="fill:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(80%,80%,0%);stroke-opacity:0.7;stroke-miterlimit:10;" d="M 110.335938 92.902344 L 127.082031 86.292969 L 128.355469 90.597656 L 130.351562 95.019531 L 136.710938 92.828125 L 143.125 89.828125 L 149.34375 87.90625 L 155.929688 105.933594 L 155.917969 110.203125 L 153.800781 116.507812 L 151.734375 120.082031 L 147.519531 123.503906 L 141.585938 126.386719 L 144.289062 132.804688 L 138.746094 132.382 [...]
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,0%);fill-opacity:0.7;" d="M 110.335938 90.597656 L 127.082031 83.984375 L 128.355469 88.289062 L 130.351562 92.710938 L 136.710938 90.519531 L 143.125 87.523438 L 149.34375 85.601562 L 155.929688 103.628906 L 155.917969 107.894531 L 153.800781 114.199219 L 151.734375 117.773438 L 147.519531 121.195312 L 141.585938 124.078125 L 144.289062 130.5 L 138.746094 130.074219 L 94.914062 150.757812 L 90.503906 156.101562 L 90.121094 1 [...]
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 101.835938 91.441406 L 109.199219 89.058594 L 109.199219 86.753906 L 101.835938 89.136719 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 109.199219 89.058594 L 110.335938 92.902344 L 110.335938 90.597656 L 109.199219 86.753906 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 110.335938 92.902344 L 103.257812 95.363281 L 103.257812 93.058594 L 110.335938 90.597656 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(80%,80%,0%);fill-opacity:0.7;" d="M 103.257812 95.363281 L 101.835938 91.441406 L 101.835938 89.136719 L 103.257812 93.058594 "/>
+<path style="fill:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(80%,80%,0%);stroke-opacity:0.7;stroke-miterlimit:10;" d="M 110.335938 92.902344 L 103.257812 95.363281 L 101.835938 91.441406 L 109.199219 89.058594 Z M 101.835938 91.441406 L 101.835938 89.136719 M 109.199219 89.058594 L 109.199219 86.753906 M 110.335938 92.902344 L 110.335938 90.597656 M 103.257812 95.363281 L 103.257812 93.058594 M 110.335938 90.597656 L 103.257812 93.058594 L 101.835938 89.1367 [...]
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,0%);fill-opacity:0.7;" d="M 110.335938 90.597656 L 103.257812 93.058594 L 101.835938 89.136719 L 109.199219 86.753906 Z "/>
+</g>
+</svg>
diff --git a/test/python_tests/images/pycairo/cairo-surface-expected.point.pdf b/test/python_tests/images/pycairo/cairo-surface-expected.point.pdf
new file mode 100644
index 0000000..eff8ca6
Binary files /dev/null and b/test/python_tests/images/pycairo/cairo-surface-expected.point.pdf differ
diff --git a/test/python_tests/images/pycairo/cairo-surface-expected.point.svg b/test/python_tests/images/pycairo/cairo-surface-expected.point.svg
new file mode 100644
index 0000000..0b73c8c
--- /dev/null
+++ b/test/python_tests/images/pycairo/cairo-surface-expected.point.svg
@@ -0,0 +1,413 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="256pt" height="256pt" viewBox="0 0 256 256" version="1.1">
+<defs>
+<g>
+<symbol overflow="visible" id="glyph0-0">
+<path style="stroke:none;" d="M 0.5 1.765625 L 0.5 -7.046875 L 5.5 -7.046875 L 5.5 1.765625 Z M 1.0625 1.21875 L 4.9375 1.21875 L 4.9375 -6.484375 L 1.0625 -6.484375 Z "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-1">
+<path style="stroke:none;" d="M 3.421875 -2.75 C 2.703125 -2.75 2.203125 -2.664062 1.921875 -2.5 C 1.640625 -2.332031 1.5 -2.050781 1.5 -1.65625 C 1.5 -1.332031 1.601562 -1.078125 1.8125 -0.890625 C 2.019531 -0.703125 2.304688 -0.609375 2.671875 -0.609375 C 3.171875 -0.609375 3.570312 -0.785156 3.875 -1.140625 C 4.175781 -1.492188 4.328125 -1.960938 4.328125 -2.546875 L 4.328125 -2.75 Z M 5.21875 -3.125 L 5.21875 0 L 4.328125 0 L 4.328125 -0.828125 C 4.117188 -0.492188 3.859375 -0.25 3.5 [...]
+</symbol>
+<symbol overflow="visible" id="glyph0-2">
+<path style="stroke:none;" d="M 1.828125 -7.015625 L 1.828125 -5.46875 L 3.6875 -5.46875 L 3.6875 -4.765625 L 1.828125 -4.765625 L 1.828125 -1.796875 C 1.828125 -1.359375 1.890625 -1.070312 2.015625 -0.9375 C 2.140625 -0.8125 2.390625 -0.75 2.765625 -0.75 L 3.6875 -0.75 L 3.6875 0 L 2.765625 0 C 2.066406 0 1.582031 -0.128906 1.3125 -0.390625 C 1.050781 -0.648438 0.921875 -1.117188 0.921875 -1.796875 L 0.921875 -4.765625 L 0.265625 -4.765625 L 0.265625 -5.46875 L 0.921875 -5.46875 L 0.921 [...]
+</symbol>
+<symbol overflow="visible" id="glyph0-3">
+<path style="stroke:none;" d="M 5.625 -2.953125 L 5.625 -2.515625 L 1.484375 -2.515625 C 1.523438 -1.898438 1.710938 -1.429688 2.046875 -1.109375 C 2.378906 -0.785156 2.84375 -0.625 3.4375 -0.625 C 3.78125 -0.625 4.113281 -0.664062 4.4375 -0.75 C 4.769531 -0.832031 5.09375 -0.957031 5.40625 -1.125 L 5.40625 -0.28125 C 5.082031 -0.144531 4.75 -0.0390625 4.40625 0.03125 C 4.070312 0.101562 3.734375 0.140625 3.390625 0.140625 C 2.515625 0.140625 1.820312 -0.109375 1.3125 -0.609375 C 0.80078 [...]
+</symbol>
+<symbol overflow="visible" id="glyph0-4">
+<path style="stroke:none;" d="M 4.421875 -5.3125 L 4.421875 -4.453125 C 4.171875 -4.585938 3.910156 -4.6875 3.640625 -4.75 C 3.367188 -4.8125 3.082031 -4.84375 2.78125 -4.84375 C 2.34375 -4.84375 2.007812 -4.773438 1.78125 -4.640625 C 1.5625 -4.503906 1.453125 -4.300781 1.453125 -4.03125 C 1.453125 -3.820312 1.53125 -3.65625 1.6875 -3.53125 C 1.84375 -3.414062 2.164062 -3.304688 2.65625 -3.203125 L 2.953125 -3.125 C 3.597656 -2.988281 4.050781 -2.796875 4.3125 -2.546875 C 4.582031 -2.296 [...]
+</symbol>
+<symbol overflow="visible" id="glyph0-5">
+<path style="stroke:none;" d=""/>
+</symbol>
+<symbol overflow="visible" id="glyph0-6">
+<path style="stroke:none;" d="M 3.09375 -7.59375 C 2.664062 -6.84375 2.34375 -6.097656 2.125 -5.359375 C 1.914062 -4.628906 1.8125 -3.890625 1.8125 -3.140625 C 1.8125 -2.390625 1.914062 -1.644531 2.125 -0.90625 C 2.34375 -0.164062 2.664062 0.570312 3.09375 1.3125 L 2.3125 1.3125 C 1.832031 0.550781 1.46875 -0.195312 1.21875 -0.9375 C 0.976562 -1.675781 0.859375 -2.410156 0.859375 -3.140625 C 0.859375 -3.867188 0.976562 -4.597656 1.21875 -5.328125 C 1.457031 -6.066406 1.820312 -6.820312 2 [...]
+</symbol>
+<symbol overflow="visible" id="glyph0-7">
+<path style="stroke:none;" d="M 0.9375 -5.46875 L 1.84375 -5.46875 L 1.84375 0.09375 C 1.84375 0.789062 1.707031 1.296875 1.4375 1.609375 C 1.175781 1.921875 0.75 2.078125 0.15625 2.078125 L -0.1875 2.078125 L -0.1875 1.3125 L 0.0625 1.3125 C 0.40625 1.3125 0.632812 1.234375 0.75 1.078125 C 0.875 0.921875 0.9375 0.59375 0.9375 0.09375 Z M 0.9375 -7.59375 L 1.84375 -7.59375 L 1.84375 -6.453125 L 0.9375 -6.453125 Z "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-8">
+<path style="stroke:none;" d="M 1.8125 -0.828125 L 1.8125 2.078125 L 0.90625 2.078125 L 0.90625 -5.46875 L 1.8125 -5.46875 L 1.8125 -4.640625 C 2 -4.960938 2.234375 -5.203125 2.515625 -5.359375 C 2.804688 -5.515625 3.15625 -5.59375 3.5625 -5.59375 C 4.226562 -5.59375 4.765625 -5.328125 5.171875 -4.796875 C 5.585938 -4.273438 5.796875 -3.585938 5.796875 -2.734375 C 5.796875 -1.867188 5.585938 -1.171875 5.171875 -0.640625 C 4.765625 -0.117188 4.226562 0.140625 3.5625 0.140625 C 3.15625 0.1 [...]
+</symbol>
+<symbol overflow="visible" id="glyph0-9">
+<path style="stroke:none;" d="M 4.546875 -2.796875 C 4.546875 -3.453125 4.410156 -3.957031 4.140625 -4.3125 C 3.867188 -4.664062 3.492188 -4.84375 3.015625 -4.84375 C 2.523438 -4.84375 2.144531 -4.664062 1.875 -4.3125 C 1.613281 -3.957031 1.484375 -3.453125 1.484375 -2.796875 C 1.484375 -2.148438 1.613281 -1.644531 1.875 -1.28125 C 2.144531 -0.925781 2.523438 -0.75 3.015625 -0.75 C 3.492188 -0.75 3.867188 -0.925781 4.140625 -1.28125 C 4.410156 -1.644531 4.546875 -2.148438 4.546875 -2.796 [...]
+</symbol>
+<symbol overflow="visible" id="glyph0-10">
+<path style="stroke:none;" d="M 0.796875 -7.59375 L 1.578125 -7.59375 C 2.066406 -6.820312 2.429688 -6.066406 2.671875 -5.328125 C 2.921875 -4.597656 3.046875 -3.867188 3.046875 -3.140625 C 3.046875 -2.410156 2.921875 -1.675781 2.671875 -0.9375 C 2.429688 -0.195312 2.066406 0.550781 1.578125 1.3125 L 0.796875 1.3125 C 1.234375 0.570312 1.554688 -0.164062 1.765625 -0.90625 C 1.984375 -1.644531 2.09375 -2.390625 2.09375 -3.140625 C 2.09375 -3.890625 1.984375 -4.628906 1.765625 -5.359375 C [...]
+</symbol>
+<symbol overflow="visible" id="glyph0-11">
+<path style="stroke:none;" d="M 3.421875 -6.3125 L 2.078125 -2.6875 L 4.765625 -2.6875 Z M 2.859375 -7.296875 L 3.984375 -7.296875 L 6.765625 0 L 5.734375 0 L 5.0625 -1.875 L 1.78125 -1.875 L 1.125 0 L 0.078125 0 Z "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-12">
+<path style="stroke:none;" d="M 0.84375 -2.15625 L 0.84375 -5.46875 L 1.75 -5.46875 L 1.75 -2.1875 C 1.75 -1.675781 1.847656 -1.289062 2.046875 -1.03125 C 2.253906 -0.769531 2.554688 -0.640625 2.953125 -0.640625 C 3.441406 -0.640625 3.828125 -0.789062 4.109375 -1.09375 C 4.390625 -1.40625 4.53125 -1.832031 4.53125 -2.375 L 4.53125 -5.46875 L 5.4375 -5.46875 L 5.4375 0 L 4.53125 0 L 4.53125 -0.84375 C 4.3125 -0.507812 4.054688 -0.257812 3.765625 -0.09375 C 3.484375 0.0625 3.148438 0.14062 [...]
+</symbol>
+<symbol overflow="visible" id="glyph0-13">
+<path style="stroke:none;" d="M 4.109375 -4.625 C 4.003906 -4.6875 3.894531 -4.726562 3.78125 -4.75 C 3.664062 -4.78125 3.535156 -4.796875 3.390625 -4.796875 C 2.878906 -4.796875 2.488281 -4.628906 2.21875 -4.296875 C 1.945312 -3.972656 1.8125 -3.5 1.8125 -2.875 L 1.8125 0 L 0.90625 0 L 0.90625 -5.46875 L 1.8125 -5.46875 L 1.8125 -4.625 C 2 -4.957031 2.242188 -5.203125 2.546875 -5.359375 C 2.847656 -5.515625 3.21875 -5.59375 3.65625 -5.59375 C 3.71875 -5.59375 3.785156 -5.585938 3.859375 [...]
+</symbol>
+<symbol overflow="visible" id="glyph0-14">
+<path style="stroke:none;" d="M 0.9375 -7.59375 L 1.84375 -7.59375 L 1.84375 0 L 0.9375 0 Z "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-15">
+<path style="stroke:none;" d="M 0.9375 -5.46875 L 1.84375 -5.46875 L 1.84375 0 L 0.9375 0 Z M 0.9375 -7.59375 L 1.84375 -7.59375 L 1.84375 -6.453125 L 0.9375 -6.453125 Z "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-16">
+<path style="stroke:none;" d="M 1.96875 -3.484375 L 1.96875 -0.8125 L 3.546875 -0.8125 C 4.078125 -0.8125 4.46875 -0.921875 4.71875 -1.140625 C 4.976562 -1.359375 5.109375 -1.695312 5.109375 -2.15625 C 5.109375 -2.601562 4.976562 -2.9375 4.71875 -3.15625 C 4.46875 -3.375 4.078125 -3.484375 3.546875 -3.484375 Z M 1.96875 -6.484375 L 1.96875 -4.28125 L 3.421875 -4.28125 C 3.910156 -4.28125 4.269531 -4.367188 4.5 -4.546875 C 4.738281 -4.734375 4.859375 -5.007812 4.859375 -5.375 C 4.859375 - [...]
+</symbol>
+<symbol overflow="visible" id="glyph0-17">
+<path style="stroke:none;" d="M 0.546875 -5.46875 L 4.8125 -5.46875 L 4.8125 -4.65625 L 1.4375 -0.71875 L 4.8125 -0.71875 L 4.8125 0 L 0.4375 0 L 0.4375 -0.828125 L 3.8125 -4.75 L 0.546875 -4.75 Z "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-18">
+<path style="stroke:none;" d="M 0.296875 -5.46875 L 1.25 -5.46875 L 2.953125 -0.875 L 4.671875 -5.46875 L 5.625 -5.46875 L 3.5625 0 L 2.34375 0 Z "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-19">
+<path style="stroke:none;" d="M 2.4375 -3.921875 C 2.132812 -3.660156 1.914062 -3.394531 1.78125 -3.125 C 1.644531 -2.863281 1.578125 -2.59375 1.578125 -2.3125 C 1.578125 -1.832031 1.75 -1.4375 2.09375 -1.125 C 2.4375 -0.8125 2.867188 -0.65625 3.390625 -0.65625 C 3.703125 -0.65625 3.988281 -0.703125 4.25 -0.796875 C 4.519531 -0.898438 4.773438 -1.054688 5.015625 -1.265625 Z M 3.125 -4.46875 L 5.59375 -1.921875 C 5.789062 -2.210938 5.941406 -2.523438 6.046875 -2.859375 C 6.160156 -3.19140 [...]
+</symbol>
+<symbol overflow="visible" id="glyph0-20">
+<path style="stroke:none;" d="M 3.71875 -7.59375 L 3.71875 -6.84375 L 2.859375 -6.84375 C 2.535156 -6.84375 2.3125 -6.773438 2.1875 -6.640625 C 2.0625 -6.515625 2 -6.285156 2 -5.953125 L 2 -5.46875 L 3.46875 -5.46875 L 3.46875 -4.765625 L 2 -4.765625 L 2 0 L 1.09375 0 L 1.09375 -4.765625 L 0.234375 -4.765625 L 0.234375 -5.46875 L 1.09375 -5.46875 L 1.09375 -5.84375 C 1.09375 -6.457031 1.234375 -6.898438 1.515625 -7.171875 C 1.796875 -7.453125 2.242188 -7.59375 2.859375 -7.59375 Z "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-21">
+<path style="stroke:none;" d="M 0.984375 -7.296875 L 2.453125 -7.296875 L 4.3125 -2.328125 L 6.1875 -7.296875 L 7.65625 -7.296875 L 7.65625 0 L 6.6875 0 L 6.6875 -6.40625 L 4.8125 -1.40625 L 3.8125 -1.40625 L 1.9375 -6.40625 L 1.9375 0 L 0.984375 0 Z "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-22">
+<path style="stroke:none;" d="M 3.0625 -4.84375 C 2.582031 -4.84375 2.203125 -4.65625 1.921875 -4.28125 C 1.640625 -3.90625 1.5 -3.390625 1.5 -2.734375 C 1.5 -2.078125 1.632812 -1.5625 1.90625 -1.1875 C 2.1875 -0.8125 2.570312 -0.625 3.0625 -0.625 C 3.539062 -0.625 3.921875 -0.8125 4.203125 -1.1875 C 4.484375 -1.5625 4.625 -2.078125 4.625 -2.734375 C 4.625 -3.378906 4.484375 -3.890625 4.203125 -4.265625 C 3.921875 -4.648438 3.539062 -4.84375 3.0625 -4.84375 Z M 3.0625 -5.59375 C 3.84375 [...]
+</symbol>
+<symbol overflow="visible" id="glyph0-23">
+<path style="stroke:none;" d="M 5.484375 -3.296875 L 5.484375 0 L 4.59375 0 L 4.59375 -3.265625 C 4.59375 -3.785156 4.488281 -4.171875 4.28125 -4.421875 C 4.082031 -4.679688 3.78125 -4.8125 3.375 -4.8125 C 2.894531 -4.8125 2.515625 -4.65625 2.234375 -4.34375 C 1.953125 -4.039062 1.8125 -3.625 1.8125 -3.09375 L 1.8125 0 L 0.90625 0 L 0.90625 -5.46875 L 1.8125 -5.46875 L 1.8125 -4.625 C 2.03125 -4.945312 2.285156 -5.1875 2.578125 -5.34375 C 2.867188 -5.507812 3.203125 -5.59375 3.578125 -5. [...]
+</symbol>
+<symbol overflow="visible" id="glyph0-24">
+<path style="stroke:none;" d="M 7.078125 -7.59375 L 7.078125 -6.84375 L 6.21875 -6.84375 C 5.894531 -6.84375 5.671875 -6.773438 5.546875 -6.640625 C 5.421875 -6.515625 5.359375 -6.285156 5.359375 -5.953125 L 5.359375 -5.46875 L 6.84375 -5.46875 L 6.84375 -4.765625 L 5.359375 -4.765625 L 5.359375 0 L 4.453125 0 L 4.453125 -4.765625 L 2 -4.765625 L 2 0 L 1.09375 0 L 1.09375 -4.765625 L 0.234375 -4.765625 L 0.234375 -5.46875 L 1.09375 -5.46875 L 1.09375 -5.84375 C 1.09375 -6.457031 1.234375 [...]
+</symbol>
+<symbol overflow="visible" id="glyph0-25">
+<path style="stroke:none;" d="M 4.875 -5.265625 L 4.875 -4.421875 C 4.625 -4.554688 4.367188 -4.660156 4.109375 -4.734375 C 3.859375 -4.804688 3.601562 -4.84375 3.34375 -4.84375 C 2.757812 -4.84375 2.304688 -4.65625 1.984375 -4.28125 C 1.660156 -3.914062 1.5 -3.398438 1.5 -2.734375 C 1.5 -2.066406 1.660156 -1.546875 1.984375 -1.171875 C 2.304688 -0.804688 2.757812 -0.625 3.34375 -0.625 C 3.601562 -0.625 3.859375 -0.65625 4.109375 -0.71875 C 4.367188 -0.789062 4.625 -0.898438 4.875 -1.046 [...]
+</symbol>
+<symbol overflow="visible" id="glyph0-26">
+<path style="stroke:none;" d="M 5.484375 -3.296875 L 5.484375 0 L 4.59375 0 L 4.59375 -3.265625 C 4.59375 -3.785156 4.488281 -4.171875 4.28125 -4.421875 C 4.082031 -4.679688 3.78125 -4.8125 3.375 -4.8125 C 2.894531 -4.8125 2.515625 -4.65625 2.234375 -4.34375 C 1.953125 -4.039062 1.8125 -3.625 1.8125 -3.09375 L 1.8125 0 L 0.90625 0 L 0.90625 -7.59375 L 1.8125 -7.59375 L 1.8125 -4.625 C 2.03125 -4.945312 2.285156 -5.1875 2.578125 -5.34375 C 2.867188 -5.507812 3.203125 -5.59375 3.578125 -5. [...]
+</symbol>
+<symbol overflow="visible" id="glyph0-27">
+<path style="stroke:none;" d="M 4.546875 -4.640625 L 4.546875 -7.59375 L 5.4375 -7.59375 L 5.4375 0 L 4.546875 0 L 4.546875 -0.828125 C 4.359375 -0.492188 4.117188 -0.25 3.828125 -0.09375 C 3.535156 0.0625 3.1875 0.140625 2.78125 0.140625 C 2.125 0.140625 1.585938 -0.117188 1.171875 -0.640625 C 0.753906 -1.171875 0.546875 -1.867188 0.546875 -2.734375 C 0.546875 -3.585938 0.753906 -4.273438 1.171875 -4.796875 C 1.585938 -5.328125 2.125 -5.59375 2.78125 -5.59375 C 3.1875 -5.59375 3.535156 [...]
+</symbol>
+<symbol overflow="visible" id="glyph0-28">
+<path style="stroke:none;" d="M 5.484375 -5.46875 L 3.515625 -2.8125 L 5.59375 0 L 4.53125 0 L 2.9375 -2.15625 L 1.34375 0 L 0.28125 0 L 2.40625 -2.859375 L 0.46875 -5.46875 L 1.53125 -5.46875 L 2.984375 -3.515625 L 4.421875 -5.46875 Z "/>
+</symbol>
+</g>
+<image id="image75" width="16" height="16" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAABmJLR0QA/wD/AP+gvaeTAAAB40lEQVQokT2Su24UQRREz+2+PTM7+2ANESSAbCMIiPgKQAYJRGjJCL6Xv3BowN717Ly6i2At8pLq6FRRQQU1dcQxSAahSpxsGQ5vlE80o4yERBHBzQtBRgGArJSKJ66vPwfrCGCQAQoYhuN1aMAhYFQ16zVd92Gaz6RNHtCEtMiFIgZhTspIlFgXE5Xz58+FhV8eb8wmSsYMi90YYnwKLRgxOYGYeLTl9u7TOJ2W0iiHuT+i2zghnR+6q80aLAaLNAvaln74lsuraVxISFHyUpjmtuj079379YLGAYL7A/ehfzFPrYpLUSUNPVKSXt/uvrctjeNAlVitOBw+DuMzaSG [...]
+<image id="image78" width="16" height="16" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAABmJLR0QA/wD/AP+gvaeTAAAATUlEQVQokZXNSwrAAAgD0bH3v7PdFIqQ+HEZ3mDgL1OMcdI2cFoHjRbBoKMGoy4fNvoPlvoL9hqIkwaek+4CqW3gtA4aLYJeZ9Zg1MALX2IYF9KsZQAAAAAASUVORK5CYII="/>
+<image id="image81" width="16" height="16" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAABmJLR0QA/wD/AP+gvaeTAAAATUlEQVQokZXNSwrAAAgD0bH3v7PdFIqQ+HEZ3mDgL1OMcdI2cFoHjRbBoKMGoy4fNvoPlvoL9hqIkwaek+4CqW3gtA4aLYJeZ9Zg1MALX2IYF9KsZQAAAAAASUVORK5CYII="/>
+<linearGradient id="linear0" gradientUnits="userSpaceOnUse" x1="0" y1="-30" x2="0" y2="-10" gradientTransform="matrix(1,0,0,1,53.809602,179.402308)">
+<stop offset="0" style="stop-color:rgb(0%,50.196078%,0%);stop-opacity:1;"/>
+<stop offset="1" style="stop-color:rgb(0%,50.196078%,0%);stop-opacity:0;"/>
+</linearGradient>
+<radialGradient id="radial0" gradientUnits="userSpaceOnUse" cx="-2.5" cy="-22.5" fx="0.5" fy="-20.5" r="5" >
+<stop offset="0" style="stop-color:rgb(100%,0%,0%);stop-opacity:1;"/>
+<stop offset="0.5" style="stop-color:rgb(100%,25.098039%,25.098039%);stop-opacity:1;"/>
+<stop offset="0.6" style="stop-color:rgb(50.196078%,100%,50.196078%);stop-opacity:1;"/>
+<stop offset="0.75" style="stop-color:rgb(100%,67.45098%,67.45098%);stop-opacity:1;"/>
+<stop offset="1" style="stop-color:rgb(100%,100%,100%);stop-opacity:1;"/>
+</radialGradient>
+<image id="image84" width="16" height="16" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAABmJLR0QA/wD/AP+gvaeTAAAATUlEQVQokZXNSwrAAAgD0bH3v7PdFIqQ+HEZ3mDgL1OMcdI2cFoHjRbBoKMGoy4fNvoPlvoL9hqIkwaek+4CqW3gtA4aLYJeZ9Zg1MALX2IYF9KsZQAAAAAASUVORK5CYII="/>
+<image id="image87" width="4" height="4" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAECAIAAAAmkwkpAAAABmJLR0QA/wD/AP+gvaeTAAAADElEQVQImWNgIB0AAAA0AAEjQ4N1AAAAAElFTkSuQmCC"/>
+</defs>
+<g id="surface71">
+<rect x="0" y="0" width="256" height="256" style="fill:rgb(70.980392%,81.568627%,81.568627%);fill-opacity:1;stroke:none;"/>
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 2.640625 127.453125 C 1.921875 127.453125 1.421875 127.539062 1.140625 127.703125 C 0.859375 127.871094 0.71875 128.152344 0.71875 128.546875 C 0.71875 128.871094 0.820312 129.125 1.03125 129.3125 C 1.238281 129.5 1.523438 129.59375 1.890625 129.59375 C 2.390625 129.59375 2.789062 129.417969 3.09375 129.0625 C 3.394531 128.710938 3.546875 [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 7.175781 123.1875 L 7.175781 124.734375 L 9.035156 124.734375 L 9.035156 125.4375 L 7.175781 125.4375 L 7.175781 128.40625 C 7.175781 128.84375 7.238281 129.132812 7.363281 129.265625 C 7.488281 129.390625 7.738281 129.453125 8.113281 129.453125 L 9.035156 129.453125 L 9.035156 130.203125 L 8.113281 130.203125 C 7.414062 130.203125 6.9296 [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 14.890625 127.25 L 14.890625 127.6875 L 10.75 127.6875 C 10.789062 128.304688 10.976562 128.773438 11.3125 129.09375 C 11.644531 129.417969 12.109375 129.578125 12.703125 129.578125 C 13.046875 129.578125 13.378906 129.539062 13.703125 129.453125 C 14.035156 129.371094 14.359375 129.246094 14.671875 129.078125 L 14.671875 129.921875 C 14. [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 19.839844 124.890625 L 19.839844 125.75 C 19.589844 125.617188 19.328125 125.515625 19.058594 125.453125 C 18.785156 125.390625 18.5 125.359375 18.199219 125.359375 C 17.761719 125.359375 17.425781 125.429688 17.199219 125.5625 C 16.980469 125.699219 16.871094 125.902344 16.871094 126.171875 C 16.871094 126.382812 16.949219 126.546875 17. [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 26.902344 122.609375 C 26.472656 123.359375 26.152344 124.105469 25.933594 124.84375 C 25.722656 125.574219 25.621094 126.3125 25.621094 127.0625 C 25.621094 127.8125 25.722656 128.558594 25.933594 129.296875 C 26.152344 130.039062 26.472656 130.773438 26.902344 131.515625 L 26.121094 131.515625 C 25.640625 130.753906 25.277344 130.007812 [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 28.648438 124.734375 L 29.554688 124.734375 L 29.554688 130.296875 C 29.554688 130.992188 29.417969 131.5 29.148438 131.8125 C 28.886719 132.125 28.460938 132.28125 27.867188 132.28125 L 27.523438 132.28125 L 27.523438 131.515625 L 27.773438 131.515625 C 28.117188 131.515625 28.34375 131.4375 28.460938 131.28125 C 28.585938 131.125 28.648 [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 32.300781 129.375 L 32.300781 132.28125 L 31.394531 132.28125 L 31.394531 124.734375 L 32.300781 124.734375 L 32.300781 125.5625 C 32.488281 125.242188 32.722656 125 33.003906 124.84375 C 33.292969 124.6875 33.644531 124.609375 34.050781 124.609375 C 34.714844 124.609375 35.253906 124.875 35.660156 125.40625 C 36.074219 125.929688 36.2851 [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 41.382812 127.40625 C 41.382812 126.75 41.246094 126.246094 40.976562 125.890625 C 40.703125 125.539062 40.328125 125.359375 39.851562 125.359375 C 39.359375 125.359375 38.980469 125.539062 38.710938 125.890625 C 38.449219 126.246094 38.320312 126.75 38.320312 127.40625 C 38.320312 128.054688 38.449219 128.558594 38.710938 128.921875 C 38 [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 43.980469 122.609375 L 44.761719 122.609375 C 45.25 123.382812 45.613281 124.136719 45.855469 124.875 C 46.105469 125.605469 46.230469 126.335938 46.230469 127.0625 C 46.230469 127.792969 46.105469 128.527344 45.855469 129.265625 C 45.613281 130.007812 45.25 130.753906 44.761719 131.515625 L 43.980469 131.515625 C 44.417969 130.773438 44. [...]
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-1" x="-0.78125" y="130.204817"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-2" x="5.346679" y="130.204817"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-3" x="9.267578" y="130.204817"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-4" x="15.419921" y="130.204817"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-5" x="20.629882" y="130.204817"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-6" x="23.808593" y="130.204817"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-7" x="27.709961" y="130.204817"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-8" x="30.488281" y="130.204817"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-9" x="36.835937" y="130.204817"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-10" x="43.183593" y="130.204817"/>
+</g>
+<use xlink:href="#image75" transform="matrix(1,0,0,1,-8,92.517317)"/>
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 222.296875 191.632812 L 220.953125 195.257812 L 223.640625 195.257812 Z M 221.734375 190.648438 L 222.859375 190.648438 L 225.640625 197.945312 L 224.609375 197.945312 L 223.9375 196.070312 L 220.65625 196.070312 L 220 197.945312 L 218.953125 197.945312 Z "/>
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 226.5625 195.789062 L 226.5625 192.476562 L 227.46875 192.476562 L 227.46875 195.757812 C 227.46875 196.269531 227.566406 196.65625 227.765625 196.914062 C 227.972656 197.175781 228.273438 197.304688 228.671875 197.304688 C 229.160156 197.304688 229.546875 197.15625 229.828125 196.851562 C 230.109375 196.539062 230.25 196.113281 230.25 19 [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 236.476562 192.632812 L 236.476562 193.492188 C 236.226562 193.359375 235.964844 193.257812 235.695312 193.195312 C 235.421875 193.132812 235.136719 193.101562 234.835938 193.101562 C 234.398438 193.101562 234.0625 193.171875 233.835938 193.304688 C 233.617188 193.441406 233.507812 193.644531 233.507812 193.914062 C 233.507812 194.125 233 [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 239.09375 190.929688 L 239.09375 192.476562 L 240.953125 192.476562 L 240.953125 193.179688 L 239.09375 193.179688 L 239.09375 196.148438 C 239.09375 196.585938 239.15625 196.875 239.28125 197.007812 C 239.40625 197.132812 239.65625 197.195312 240.03125 197.195312 L 240.953125 197.195312 L 240.953125 197.945312 L 240.03125 197.945312 C 23 [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 245.296875 193.320312 C 245.191406 193.257812 245.082031 193.21875 244.96875 193.195312 C 244.851562 193.164062 244.722656 193.148438 244.578125 193.148438 C 244.066406 193.148438 243.675781 193.316406 243.40625 193.648438 C 243.132812 193.972656 243 194.445312 243 195.070312 L 243 197.945312 L 242.09375 197.945312 L 242.09375 192.476562 [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 248.71875 195.195312 C 248 195.195312 247.5 195.28125 247.21875 195.445312 C 246.9375 195.613281 246.796875 195.894531 246.796875 196.289062 C 246.796875 196.613281 246.898438 196.867188 247.109375 197.054688 C 247.316406 197.242188 247.601562 197.335938 247.96875 197.335938 C 248.46875 197.335938 248.867188 197.160156 249.171875 196.8046 [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 252.363281 190.351562 L 253.269531 190.351562 L 253.269531 197.945312 L 252.363281 197.945312 Z "/>
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 255.140625 192.476562 L 256.046875 192.476562 L 256.046875 197.945312 L 255.140625 197.945312 Z M 255.140625 190.351562 L 256.046875 190.351562 L 256.046875 191.492188 L 255.140625 191.492188 Z "/>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-11" x="218.875978" y="197.94629"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-12" x="225.716799" y="197.94629"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-4" x="232.054689" y="197.94629"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-2" x="237.26465" y="197.94629"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-13" x="241.185549" y="197.94629"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-1" x="245.296877" y="197.94629"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-14" x="251.424807" y="197.94629"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-15" x="254.203127" y="197.94629"/>
+</g>
+<use xlink:href="#image78" transform="matrix(1,0,0,1,248.000002,160.88379)"/>
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 15.496094 184.980469 L 15.496094 187.652344 L 17.074219 187.652344 C 17.605469 187.652344 17.996094 187.542969 18.246094 187.324219 C 18.503906 187.105469 18.636719 186.769531 18.636719 186.308594 C 18.636719 185.863281 18.503906 185.527344 18.246094 185.308594 C 17.996094 185.089844 17.605469 184.980469 17.074219 184.980469 Z M 15.496094 [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 24.496094 183.839844 C 24.390625 183.777344 24.28125 183.738281 24.167969 183.714844 C 24.050781 183.683594 23.921875 183.667969 23.777344 183.667969 C 23.265625 183.667969 22.875 183.835938 22.605469 184.167969 C 22.332031 184.492188 22.199219 184.964844 22.199219 185.589844 L 22.199219 188.464844 L 21.292969 188.464844 L 21.292969 182.9 [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 27.917969 185.714844 C 27.199219 185.714844 26.699219 185.800781 26.417969 185.964844 C 26.136719 186.132812 25.996094 186.414062 25.996094 186.808594 C 25.996094 187.132812 26.097656 187.386719 26.308594 187.574219 C 26.515625 187.761719 26.800781 187.855469 27.167969 187.855469 C 27.667969 187.855469 28.066406 187.679688 28.371094 187.3 [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 31.171875 182.996094 L 35.4375 182.996094 L 35.4375 183.808594 L 32.0625 187.746094 L 35.4375 187.746094 L 35.4375 188.464844 L 31.0625 188.464844 L 31.0625 187.636719 L 34.4375 183.714844 L 31.171875 183.714844 Z "/>
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 36.8125 182.996094 L 37.71875 182.996094 L 37.71875 188.464844 L 36.8125 188.464844 Z M 36.8125 180.871094 L 37.71875 180.871094 L 37.71875 182.011719 L 36.8125 182.011719 Z "/>
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 39.589844 180.871094 L 40.496094 180.871094 L 40.496094 188.464844 L 39.589844 188.464844 Z "/>
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 47.703125 180.871094 C 47.273438 181.621094 46.953125 182.367188 46.734375 183.105469 C 46.523438 183.835938 46.421875 184.574219 46.421875 185.324219 C 46.421875 186.074219 46.523438 186.820312 46.734375 187.558594 C 46.953125 188.300781 47.273438 189.035156 47.703125 189.777344 L 46.921875 189.777344 C 46.441406 189.015625 46.078125 188 [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 52.933594 183.152344 L 52.933594 184.011719 C 52.683594 183.878906 52.421875 183.777344 52.152344 183.714844 C 51.878906 183.652344 51.59375 183.621094 51.292969 183.621094 C 50.855469 183.621094 50.519531 183.691406 50.292969 183.824219 C 50.074219 183.960938 49.964844 184.164062 49.964844 184.433594 C 49.964844 184.644531 50.042969 184. [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 54.019531 182.996094 L 54.972656 182.996094 L 56.675781 187.589844 L 58.394531 182.996094 L 59.347656 182.996094 L 57.285156 188.464844 L 56.066406 188.464844 Z "/>
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 64.1875 185.667969 C 64.1875 185.011719 64.050781 184.507812 63.78125 184.152344 C 63.507812 183.800781 63.132812 183.621094 62.65625 183.621094 C 62.164062 183.621094 61.785156 183.800781 61.515625 184.152344 C 61.253906 184.507812 61.125 185.011719 61.125 185.667969 C 61.125 186.316406 61.253906 186.820312 61.515625 187.183594 C 61.7851 [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 71.601562 184.542969 C 71.296875 184.804688 71.078125 185.070312 70.945312 185.339844 C 70.808594 185.601562 70.742188 185.871094 70.742188 186.152344 C 70.742188 186.632812 70.914062 187.027344 71.257812 187.339844 C 71.601562 187.652344 72.03125 187.808594 72.554688 187.808594 C 72.867188 187.808594 73.152344 187.761719 73.414062 187.66 [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 81.96875 181.449219 L 81.96875 182.996094 L 83.828125 182.996094 L 83.828125 183.699219 L 81.96875 183.699219 L 81.96875 186.667969 C 81.96875 187.105469 82.03125 187.394531 82.15625 187.527344 C 82.28125 187.652344 82.53125 187.714844 82.90625 187.714844 L 83.828125 187.714844 L 83.828125 188.464844 L 82.90625 188.464844 C 82.207031 188. [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 85 182.996094 L 85.90625 182.996094 L 85.90625 188.464844 L 85 188.464844 Z M 85 180.871094 L 85.90625 180.871094 L 85.90625 182.011719 L 85 182.011719 Z "/>
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 90.558594 180.871094 L 90.558594 181.621094 L 89.699219 181.621094 C 89.375 181.621094 89.152344 181.691406 89.027344 181.824219 C 88.902344 181.949219 88.839844 182.179688 88.839844 182.511719 L 88.839844 182.996094 L 90.308594 182.996094 L 90.308594 183.699219 L 88.839844 183.699219 L 88.839844 188.464844 L 87.933594 188.464844 L 87.933 [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 91.160156 180.871094 L 91.941406 180.871094 C 92.429688 181.644531 92.792969 182.398438 93.035156 183.136719 C 93.285156 183.867188 93.410156 184.597656 93.410156 185.324219 C 93.410156 186.054688 93.285156 186.789062 93.035156 187.527344 C 92.792969 188.269531 92.429688 189.015625 91.941406 189.777344 L 91.160156 189.777344 C 91.597656 1 [...]
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-16" x="13.526085" y="188.464808"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-13" x="20.386436" y="188.464808"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-1" x="24.497764" y="188.464808"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-17" x="30.625694" y="188.464808"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-15" x="35.874717" y="188.464808"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-14" x="38.653038" y="188.464808"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-5" x="41.431358" y="188.464808"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-6" x="44.610069" y="188.464808"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-4" x="48.511436" y="188.464808"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-18" x="53.721397" y="188.464808"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-9" x="59.639366" y="188.464808"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-5" x="65.987022" y="188.464808"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-19" x="69.165733" y="188.464808"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-5" x="76.963585" y="188.464808"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-2" x="80.142296" y="188.464808"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-15" x="84.063194" y="188.464808"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-20" x="86.841514" y="188.464808"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-10" x="90.362022" y="188.464808"/>
+</g>
+<use xlink:href="#image81" transform="matrix(1,0,0,1,45.894737,151.402308)"/>
+<path style=" stroke:none;fill-rule:nonzero;fill:url(#linear0);" d="M 63.808594 159.402344 L 63.132812 160.847656 L 61.199219 162.097656 L 58.265625 162.984375 L 54.730469 163.386719 L 51.074219 163.25 L 47.785156 162.59375 L 45.308594 161.507812 L 43.980469 160.136719 L 43.980469 158.667969 L 45.308594 157.296875 L 47.785156 156.210938 L 51.074219 155.554688 L 54.730469 155.417969 L 58.265625 155.820312 L 61.199219 156.707031 L 63.132812 157.957031 Z "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,100%);fill-opacity:1;" d="M 57.808594 159.402344 L 57.539062 163.015625 L 56.765625 166.140625 L 55.59375 168.355469 L 54.179688 169.359375 L 52.714844 169.019531 L 51.398438 167.382812 L 50.410156 164.667969 L 49.878906 161.238281 L 49.878906 157.566406 L 50.410156 154.136719 L 51.398438 151.421875 L 52.714844 149.785156 L 54.179688 149.445312 L 55.59375 150.449219 L 56.765625 152.664062 L 57.539062 155.789062 Z "/>
+<path style="fill-rule:nonzero;fill:url(#radial0);stroke-width:0.4;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,50.196078%,0%);stroke-opacity:1;stroke-miterlimit:4;" d="M 4.998991 -19.999964 L 4.502898 -17.831996 L 3.116179 -16.089808 L 1.112273 -15.124964 L -1.11429 -15.124964 L -3.118196 -16.089808 L -4.504915 -17.831996 L -5.001009 -19.999964 L -4.504915 -22.167933 L -3.118196 -23.910121 L -1.11429 -24.874964 L 1.112273 -24.874964 L 3.116179 -23.910121 L 4.502898 -22.167933 [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 185.652344 115.796875 L 187.121094 115.796875 L 188.980469 120.765625 L 190.855469 115.796875 L 192.324219 115.796875 L 192.324219 123.09375 L 191.355469 123.09375 L 191.355469 116.6875 L 189.480469 121.6875 L 188.480469 121.6875 L 186.605469 116.6875 L 186.605469 123.09375 L 185.652344 123.09375 Z "/>
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 196.359375 118.25 C 195.878906 118.25 195.5 118.4375 195.21875 118.8125 C 194.9375 119.1875 194.796875 119.703125 194.796875 120.359375 C 194.796875 121.015625 194.929688 121.53125 195.203125 121.90625 C 195.484375 122.28125 195.867188 122.46875 196.359375 122.46875 C 196.835938 122.46875 197.21875 122.28125 197.5 121.90625 C 197.78125 12 [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 204.898438 119.796875 L 204.898438 123.09375 L 204.007812 123.09375 L 204.007812 119.828125 C 204.007812 119.308594 203.902344 118.921875 203.695312 118.671875 C 203.496094 118.414062 203.195312 118.28125 202.789062 118.28125 C 202.308594 118.28125 201.929688 118.4375 201.648438 118.75 C 201.367188 119.054688 201.226562 119.46875 201.2265 [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 210.300781 120.296875 C 210.300781 119.640625 210.164062 119.136719 209.894531 118.78125 C 209.621094 118.429688 209.246094 118.25 208.769531 118.25 C 208.277344 118.25 207.898438 118.429688 207.628906 118.78125 C 207.367188 119.136719 207.238281 119.640625 207.238281 120.296875 C 207.238281 120.945312 207.367188 121.449219 207.628906 121 [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 215.164062 118.25 C 214.683594 118.25 214.304688 118.4375 214.023438 118.8125 C 213.742188 119.1875 213.601562 119.703125 213.601562 120.359375 C 213.601562 121.015625 213.734375 121.53125 214.007812 121.90625 C 214.289062 122.28125 214.671875 122.46875 215.164062 122.46875 C 215.640625 122.46875 216.023438 122.28125 216.304688 121.90625 [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 219.15625 115.5 L 220.0625 115.5 L 220.0625 123.09375 L 219.15625 123.09375 Z "/>
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 221.933594 117.625 L 222.839844 117.625 L 222.839844 123.09375 L 221.933594 123.09375 Z M 221.933594 115.5 L 222.839844 115.5 L 222.839844 116.640625 L 221.933594 116.640625 Z "/>
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 227.195312 120.34375 C 226.476562 120.34375 225.976562 120.429688 225.695312 120.59375 C 225.414062 120.761719 225.273438 121.042969 225.273438 121.4375 C 225.273438 121.761719 225.375 122.015625 225.585938 122.203125 C 225.792969 122.390625 226.078125 122.484375 226.445312 122.484375 C 226.945312 122.484375 227.34375 122.308594 227.64843 [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 236.175781 115.5 C 235.746094 116.25 235.425781 116.996094 235.207031 117.734375 C 234.996094 118.464844 234.894531 119.203125 234.894531 119.953125 C 234.894531 120.703125 234.996094 121.449219 235.207031 122.1875 C 235.425781 122.929688 235.746094 123.664062 236.175781 124.40625 L 235.394531 124.40625 C 234.914062 123.644531 234.550781 [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 238.8125 116.078125 L 238.8125 117.625 L 240.671875 117.625 L 240.671875 118.328125 L 238.8125 118.328125 L 238.8125 121.296875 C 238.8125 121.734375 238.875 122.023438 239 122.15625 C 239.125 122.28125 239.375 122.34375 239.75 122.34375 L 240.671875 122.34375 L 240.671875 123.09375 L 239.75 123.09375 C 239.050781 123.09375 238.566406 122 [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 241.839844 117.625 L 242.746094 117.625 L 242.746094 123.09375 L 241.839844 123.09375 Z M 241.839844 115.5 L 242.746094 115.5 L 242.746094 116.640625 L 241.839844 116.640625 Z "/>
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 250.761719 115.5 L 250.761719 116.25 L 249.902344 116.25 C 249.578125 116.25 249.355469 116.320312 249.230469 116.453125 C 249.105469 116.578125 249.042969 116.808594 249.042969 117.140625 L 249.042969 117.625 L 250.527344 117.625 L 250.527344 118.328125 L 249.042969 118.328125 L 249.042969 123.09375 L 248.136719 123.09375 L 248.136719 11 [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 251.367188 115.5 L 252.148438 115.5 C 252.636719 116.273438 253 117.027344 253.242188 117.765625 C 253.492188 118.496094 253.617188 119.226562 253.617188 119.953125 C 253.617188 120.683594 253.492188 121.417969 253.242188 122.15625 C 253 122.898438 252.636719 123.644531 252.148438 124.40625 L 251.367188 124.40625 C 251.804688 123.664062 2 [...]
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-21" x="184.668808" y="123.092488"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-22" x="193.296738" y="123.092488"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-23" x="199.414902" y="123.092488"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-9" x="205.752792" y="123.092488"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-22" x="212.100448" y="123.092488"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-14" x="218.218613" y="123.092488"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-15" x="220.996933" y="123.092488"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-1" x="223.775253" y="123.092488"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-5" x="229.903183" y="123.092488"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-6" x="233.081894" y="123.092488"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-2" x="236.983261" y="123.092488"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-15" x="240.904159" y="123.092488"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-24" x="243.68248" y="123.092488"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-10" x="250.572128" y="123.092488"/>
+</g>
+<use xlink:href="#image84" transform="matrix(1,0,0,1,211.571152,86.029988)"/>
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 20.992188 201.113281 L 20.992188 201.957031 C 20.742188 201.824219 20.484375 201.71875 20.226562 201.644531 C 19.976562 201.574219 19.71875 201.535156 19.460938 201.535156 C 18.875 201.535156 18.421875 201.722656 18.101562 202.097656 C 17.777344 202.464844 17.617188 202.980469 17.617188 203.644531 C 17.617188 204.3125 17.777344 204.832031 [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 27.097656 203.082031 L 27.097656 206.378906 L 26.207031 206.378906 L 26.207031 203.113281 C 26.207031 202.59375 26.101562 202.207031 25.894531 201.957031 C 25.695312 201.699219 25.394531 201.566406 24.988281 201.566406 C 24.507812 201.566406 24.128906 201.722656 23.847656 202.035156 C 23.566406 202.339844 23.425781 202.753906 23.425781 20 [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 28.890625 200.910156 L 29.796875 200.910156 L 29.796875 206.378906 L 28.890625 206.378906 Z M 28.890625 198.785156 L 29.796875 198.785156 L 29.796875 199.925781 L 28.890625 199.925781 Z "/>
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 31.667969 198.785156 L 32.574219 198.785156 L 32.574219 206.378906 L 31.667969 206.378906 Z "/>
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 39.132812 203.425781 L 39.132812 203.863281 L 34.992188 203.863281 C 35.03125 204.480469 35.21875 204.949219 35.554688 205.269531 C 35.886719 205.59375 36.351562 205.753906 36.945312 205.753906 C 37.289062 205.753906 37.621094 205.714844 37.945312 205.628906 C 38.277344 205.546875 38.601562 205.421875 38.914062 205.253906 L 38.914062 206. [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 11.441406 210.347656 C 11.011719 211.097656 10.691406 211.84375 10.472656 212.582031 C 10.261719 213.3125 10.160156 214.050781 10.160156 214.800781 C 10.160156 215.550781 10.261719 216.296875 10.472656 217.035156 C 10.691406 217.777344 11.011719 218.511719 11.441406 219.253906 L 10.660156 219.253906 C 10.179688 218.492188 9.816406 217.746 [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 16.792969 213.300781 L 16.792969 210.347656 L 17.683594 210.347656 L 17.683594 217.941406 L 16.792969 217.941406 L 16.792969 217.113281 C 16.605469 217.449219 16.363281 217.691406 16.074219 217.847656 C 15.78125 218.003906 15.433594 218.082031 15.027344 218.082031 C 14.371094 218.082031 13.832031 217.824219 13.417969 217.300781 C 13 216.7 [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 24.21875 214.988281 L 24.21875 215.425781 L 20.078125 215.425781 C 20.117188 216.042969 20.304688 216.511719 20.640625 216.832031 C 20.972656 217.15625 21.4375 217.316406 22.03125 217.316406 C 22.375 217.316406 22.707031 217.277344 23.03125 217.191406 C 23.363281 217.109375 23.6875 216.984375 24 216.816406 L 24 217.660156 C 23.675781 217. [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 28.464844 210.347656 L 28.464844 211.097656 L 27.605469 211.097656 C 27.28125 211.097656 27.058594 211.167969 26.933594 211.300781 C 26.808594 211.425781 26.746094 211.65625 26.746094 211.988281 L 26.746094 212.472656 L 28.214844 212.472656 L 28.214844 213.175781 L 26.746094 213.175781 L 26.746094 217.941406 L 25.839844 217.941406 L 25.83 [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 31.691406 215.191406 C 30.972656 215.191406 30.472656 215.277344 30.191406 215.441406 C 29.910156 215.609375 29.769531 215.890625 29.769531 216.285156 C 29.769531 216.609375 29.871094 216.863281 30.082031 217.050781 C 30.289062 217.238281 30.574219 217.332031 30.941406 217.332031 C 31.441406 217.332031 31.839844 217.15625 32.144531 216.80 [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 35.238281 215.785156 L 35.238281 212.472656 L 36.144531 212.472656 L 36.144531 215.753906 C 36.144531 216.265625 36.242188 216.652344 36.441406 216.910156 C 36.648438 217.171875 36.949219 217.300781 37.347656 217.300781 C 37.835938 217.300781 38.222656 217.152344 38.503906 216.847656 C 38.785156 216.535156 38.925781 216.109375 38.925781 2 [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 41.671875 210.347656 L 42.578125 210.347656 L 42.578125 217.941406 L 41.671875 217.941406 Z "/>
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 45.339844 210.925781 L 45.339844 212.472656 L 47.199219 212.472656 L 47.199219 213.175781 L 45.339844 213.175781 L 45.339844 216.144531 C 45.339844 216.582031 45.402344 216.871094 45.527344 217.003906 C 45.652344 217.128906 45.902344 217.191406 46.277344 217.191406 L 47.199219 217.191406 L 47.199219 217.941406 L 46.277344 217.941406 C 45. [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 21.96875 224.660156 C 21.488281 224.660156 21.109375 224.847656 20.828125 225.222656 C 20.546875 225.597656 20.40625 226.113281 20.40625 226.769531 C 20.40625 227.425781 20.539062 227.941406 20.8125 228.316406 C 21.09375 228.691406 21.476562 228.878906 21.96875 228.878906 C 22.445312 228.878906 22.828125 228.691406 23.109375 228.316406 C [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 29.574219 226.707031 C 29.574219 226.050781 29.4375 225.546875 29.167969 225.191406 C 28.894531 224.839844 28.519531 224.660156 28.042969 224.660156 C 27.550781 224.660156 27.171875 224.839844 26.902344 225.191406 C 26.640625 225.546875 26.511719 226.050781 26.511719 226.707031 C 26.511719 227.355469 26.640625 227.859375 26.902344 228.222 [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 36.25 224.238281 L 36.25 225.082031 C 36 224.949219 35.742188 224.84375 35.484375 224.769531 C 35.234375 224.699219 34.976562 224.660156 34.71875 224.660156 C 34.132812 224.660156 33.679688 224.847656 33.359375 225.222656 C 33.035156 225.589844 32.875 226.105469 32.875 226.769531 C 32.875 227.4375 33.035156 227.957031 33.359375 228.332031 [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 17.507812 240.238281 L 17.507812 243.144531 L 16.601562 243.144531 L 16.601562 235.597656 L 17.507812 235.597656 L 17.507812 236.425781 C 17.695312 236.105469 17.929688 235.863281 18.210938 235.707031 C 18.5 235.550781 18.851562 235.472656 19.257812 235.472656 C 19.921875 235.472656 20.460938 235.738281 20.867188 236.269531 C 21.28125 236 [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 22.980469 235.597656 L 23.886719 235.597656 L 23.886719 241.066406 L 22.980469 241.066406 Z M 22.980469 233.472656 L 23.886719 233.472656 L 23.886719 234.613281 L 22.980469 234.613281 Z "/>
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 30.304688 235.597656 L 28.335938 238.253906 L 30.414062 241.066406 L 29.351562 241.066406 L 27.757812 238.910156 L 26.164062 241.066406 L 25.101562 241.066406 L 27.226562 238.207031 L 25.289062 235.597656 L 26.351562 235.597656 L 27.804688 237.550781 L 29.242188 235.597656 Z "/>
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 36.054688 238.113281 L 36.054688 238.550781 L 31.914062 238.550781 C 31.953125 239.167969 32.140625 239.636719 32.476562 239.957031 C 32.808594 240.28125 33.273438 240.441406 33.867188 240.441406 C 34.210938 240.441406 34.542969 240.402344 34.867188 240.316406 C 35.199219 240.234375 35.523438 240.109375 35.835938 239.941406 L 35.835938 24 [...]
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 37.519531 233.472656 L 38.425781 233.472656 L 38.425781 241.066406 L 37.519531 241.066406 Z "/>
+<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke:rgb(100%,100%,100%);stroke-opacity:1;stroke-miterlimit:10;" d="M 40.160156 233.472656 L 40.941406 233.472656 C 41.429688 234.246094 41.792969 235 42.035156 235.738281 C 42.285156 236.46875 42.410156 237.199219 42.410156 237.925781 C 42.410156 238.65625 42.285156 239.390625 42.035156 240.128906 C 41.792969 240.871094 41.429688 241.617188 40.941406 242.378906 L 40.160156 242.378906 C 40.597656 241.636719 [...]
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-25" x="16.116835" y="206.377778"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-26" x="21.614882" y="206.377778"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-15" x="27.952772" y="206.377778"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-14" x="30.731093" y="206.377778"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-3" x="33.509413" y="206.377778"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-5" x="39.661757" y="206.377778"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-6" x="8.345839" y="217.940278"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-27" x="12.247206" y="217.940278"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-3" x="18.594862" y="217.940278"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-20" x="24.747206" y="217.940278"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-1" x="28.267714" y="217.940278"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-12" x="34.395643" y="217.940278"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-14" x="40.733534" y="217.940278"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-2" x="43.511854" y="217.940278"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-5" x="47.432753" y="217.940278"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-22" x="18.907362" y="229.502778"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-9" x="25.025526" y="229.502778"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-25" x="31.373182" y="229.502778"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-5" x="36.871229" y="229.502778"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-8" x="15.694471" y="241.065278"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-15" x="22.042128" y="241.065278"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-28" x="24.820448" y="241.065278"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-3" x="30.4308" y="241.065278"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-14" x="36.583143" y="241.065278"/>
+</g>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-10" x="39.361464" y="241.065278"/>
+</g>
+<use xlink:href="#image87" transform="matrix(1,0,0,1,27.478651,191.877778)"/>
+</g>
+</svg>
diff --git a/test/python_tests/images/pycairo/cairo-surface-expected.polygon.pdf b/test/python_tests/images/pycairo/cairo-surface-expected.polygon.pdf
new file mode 100644
index 0000000..201bb9b
Binary files /dev/null and b/test/python_tests/images/pycairo/cairo-surface-expected.polygon.pdf differ
diff --git a/test/python_tests/images/pycairo/cairo-surface-expected.polygon.svg b/test/python_tests/images/pycairo/cairo-surface-expected.polygon.svg
new file mode 100644
index 0000000..4f2a943
--- /dev/null
+++ b/test/python_tests/images/pycairo/cairo-surface-expected.polygon.svg
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="256pt" height="256pt" viewBox="0 0 256 256" version="1.1">
+<defs>
+<image id="image98" width="16" height="16" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAABmJLR0QA/wD/AP+gvaeTAAAATUlEQVQokZXNSwrAAAgD0bH3v7PdFIqQ+HEZ3mDgL1OMcdI2cFoHjRbBoKMGoy4fNvoPlvoL9hqIkwaek+4CqW3gtA4aLYJeZ9Zg1MALX2IYF9KsZQAAAAAASUVORK5CYII="/>
+<image id="image101" width="16" height="16" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAABmJLR0QA/wD/AP+gvaeTAAAATUlEQVQokZXNSwrAAAgD0bH3v7PdFIqQ+HEZ3mDgL1OMcdI2cFoHjRbBoKMGoy4fNvoPlvoL9hqIkwaek+4CqW3gtA4aLYJeZ9Zg1MALX2IYF9KsZQAAAAAASUVORK5CYII="/>
+<image id="image104" width="16" height="16" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAABmJLR0QA/wD/AP+gvaeTAAAATUlEQVQokZXNSwrAAAgD0bH3v7PdFIqQ+HEZ3mDgL1OMcdI2cFoHjRbBoKMGoy4fNvoPlvoL9hqIkwaek+4CqW3gtA4aLYJeZ9Zg1MALX2IYF9KsZQAAAAAASUVORK5CYII="/>
+<image id="image107" width="16" height="16" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAABmJLR0QA/wD/AP+gvaeTAAAATUlEQVQokZXNSwrAAAgD0bH3v7PdFIqQ+HEZ3mDgL1OMcdI2cFoHjRbBoKMGoy4fNvoPlvoL9hqIkwaek+4CqW3gtA4aLYJeZ9Zg1MALX2IYF9KsZQAAAAAASUVORK5CYII="/>
+<image id="image110" width="16" height="16" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAABmJLR0QA/wD/AP+gvaeTAAAATUlEQVQokZXNSwrAAAgD0bH3v7PdFIqQ+HEZ3mDgL1OMcdI2cFoHjRbBoKMGoy4fNvoPlvoL9hqIkwaek+4CqW3gtA4aLYJeZ9Zg1MALX2IYF9KsZQAAAAAASUVORK5CYII="/>
+<image id="image113" width="16" height="16" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAABmJLR0QA/wD/AP+gvaeTAAAATUlEQVQokZXNSwrAAAgD0bH3v7PdFIqQ+HEZ3mDgL1OMcdI2cFoHjRbBoKMGoy4fNvoPlvoL9hqIkwaek+4CqW3gtA4aLYJeZ9Zg1MALX2IYF9KsZQAAAAAASUVORK5CYII="/>
+<image id="image116" width="16" height="16" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAABmJLR0QA/wD/AP+gvaeTAAAATUlEQVQokZXNSwrAAAgD0bH3v7PdFIqQ+HEZ3mDgL1OMcdI2cFoHjRbBoKMGoy4fNvoPlvoL9hqIkwaek+4CqW3gtA4aLYJeZ9Zg1MALX2IYF9KsZQAAAAAASUVORK5CYII="/>
+<image id="image119" width="16" height="16" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAABmJLR0QA/wD/AP+gvaeTAAAATUlEQVQokZXNSwrAAAgD0bH3v7PdFIqQ+HEZ3mDgL1OMcdI2cFoHjRbBoKMGoy4fNvoPlvoL9hqIkwaek+4CqW3gtA4aLYJeZ9Zg1MALX2IYF9KsZQAAAAAASUVORK5CYII="/>
+<image id="image122" width="16" height="16" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAABmJLR0QA/wD/AP+gvaeTAAAATUlEQVQokZXNSwrAAAgD0bH3v7PdFIqQ+HEZ3mDgL1OMcdI2cFoHjRbBoKMGoy4fNvoPlvoL9hqIkwaek+4CqW3gtA4aLYJeZ9Zg1MALX2IYF9KsZQAAAAAASUVORK5CYII="/>
+</defs>
+<g id="surface94">
+<path style=" stroke:none;fill-rule:evenodd;fill:rgb(90.196078%,90.196078%,98.039216%);fill-opacity:0.5;" d="M 115.65625 56.117188 L 105.6875 50.042969 L 102.367188 41.585938 L 108.777344 39.203125 L 132.507812 28.515625 L 132.222656 27.054688 L 141.789062 23.054688 L 145.058594 32.933594 L 157.566406 68.800781 L 159.464844 73.835938 L 139.804688 81.023438 L 135.675781 71.570312 L 135.882812 66.1875 L 136.332031 59.265625 L 134.109375 51.578125 L 131.019531 45.429688 L 127.984375 38.5078 [...]
+<use xlink:href="#image98" transform="matrix(1,0,0,1,127.94131,42.841104)"/>
+<path style=" stroke:none;fill-rule:evenodd;fill:rgb(90.196078%,90.196078%,98.039216%);fill-opacity:0.5;" d="M 132.222656 27.054688 L 132.507812 28.515625 L 108.777344 39.203125 L 102.367188 41.585938 L 105.6875 50.042969 L 115.65625 56.117188 L 112.578125 61.574219 L 105.039062 74.605469 L 88.605469 74.414062 L 84.253906 74.566406 L 79.761719 74.644531 L 68.664062 79.027344 L 67.753906 79.296875 L 62.101562 68.839844 L 56.199219 60.996094 L 54.90625 61.496094 L 53.773438 58.652344 L 64. [...]
+<use xlink:href="#image101" transform="matrix(1,0,0,1,79.836035,51.528053)"/>
+<path style=" stroke:none;fill-rule:evenodd;fill:rgb(90.196078%,90.196078%,98.039216%);fill-opacity:0.5;" d="M 115.65625 56.117188 L 118.730469 49.234375 L 122.578125 41.546875 L 127.984375 38.507812 L 131.019531 45.429688 L 134.109375 51.578125 L 136.332031 59.265625 L 135.882812 66.1875 L 129.527344 66.6875 L 124.503906 68.453125 L 121.441406 69.992188 L 109.082031 80.371094 L 104.925781 81.371094 L 102.195312 83.253906 L 101.664062 85.136719 L 100.078125 88.445312 L 97.773438 91.67187 [...]
+<use xlink:href="#image104" transform="matrix(1,0,0,1,90.102079,69.169472)"/>
+<path style=" stroke:none;fill-rule:evenodd;fill:rgb(90.196078%,90.196078%,98.039216%);fill-opacity:0.5;" d="M 53.773438 58.652344 L 54.90625 61.496094 L 61.679688 81.753906 L 71.058594 108.050781 L 76.140625 119.121094 L 72.308594 122.695312 L 69.605469 130.535156 L 61.425781 133.496094 L 54.027344 114.699219 L 44.699219 118.351562 L 33.3125 123.15625 L 14.414062 130.113281 L 11.242188 120.925781 L 9.871094 116.082031 L 10.171875 111.96875 L 10.238281 106.011719 L 14.351562 105.242188 L [...]
+<use xlink:href="#image107" transform="matrix(1,0,0,1,31.167216,88.825)"/>
+<path style=" stroke:none;fill-rule:evenodd;fill:rgb(90.196078%,90.196078%,98.039216%);fill-opacity:0.5;" d="M 54.90625 61.496094 L 56.199219 60.996094 L 62.101562 68.839844 L 67.753906 79.296875 L 61.679688 81.753906 Z "/>
+<use xlink:href="#image110" transform="matrix(1,0,0,1,53.212824,64.674519)"/>
+<path style=" stroke:none;fill-rule:evenodd;fill:rgb(90.196078%,90.196078%,98.039216%);fill-opacity:0.5;" d="M 135.882812 66.1875 L 135.675781 71.570312 L 139.804688 81.023438 L 127.082031 86.292969 L 110.335938 92.902344 L 109.199219 89.058594 L 101.835938 91.441406 L 103.257812 95.363281 L 91.441406 100.015625 L 94.757812 93.558594 L 97.773438 91.671875 L 100.078125 88.445312 L 101.664062 85.136719 L 102.195312 83.253906 L 104.925781 81.371094 L 109.082031 80.371094 L 121.441406 69.992 [...]
+<use xlink:href="#image113" transform="matrix(1,0,0,1,111.187238,73.378811)"/>
+<path style=" stroke:none;fill-rule:evenodd;fill:rgb(90.196078%,90.196078%,98.039216%);fill-opacity:0.5;" d="M 159.464844 73.835938 L 170.804688 68.917969 L 173.171875 76.296875 L 176.019531 82.679688 L 183.402344 90.136719 L 184.109375 93.789062 L 174.757812 89.945312 L 171.078125 89.90625 L 169.109375 93.441406 L 185.757812 103.707031 L 182.722656 112.007812 L 181.617188 113.125 L 179.890625 116.3125 L 174.433594 119.734375 L 167.976562 121.773438 L 157.070312 125.578125 L 144.289062 1 [...]
+<use xlink:href="#image116" transform="matrix(1,0,0,1,153.144399,90.01164)"/>
+<path style=" stroke:none;fill-rule:evenodd;fill:rgb(90.196078%,90.196078%,98.039216%);fill-opacity:0.5;" d="M 183.402344 90.136719 L 188.046875 87.058594 L 192.929688 83.832031 L 200.394531 81.488281 L 201.0625 81.371094 L 205.175781 79.601562 L 213.167969 74.914062 L 216.90625 70.414062 L 228.015625 79.488281 L 228.765625 84.601562 L 242.207031 79.796875 L 241.550781 74.835938 L 245.570312 72.53125 L 247.820312 71.339844 L 250.59375 70.03125 L 252.679688 76.488281 L 254.8125 83.101562 [...]
+<use xlink:href="#image119" transform="matrix(1,0,0,1,155.527409,142.794669)"/>
+<path style=" stroke:none;fill-rule:evenodd;fill:rgb(90.196078%,90.196078%,98.039216%);fill-opacity:0.5;" d="M 110.335938 92.902344 L 127.082031 86.292969 L 128.355469 90.597656 L 130.351562 95.019531 L 136.710938 92.828125 L 143.125 89.828125 L 149.34375 87.90625 L 155.929688 105.933594 L 155.917969 110.203125 L 153.800781 116.507812 L 151.734375 120.082031 L 147.519531 123.503906 L 141.585938 126.386719 L 144.289062 132.804688 L 138.746094 132.382812 L 94.914062 153.0625 L 90.503906 15 [...]
+<use xlink:href="#image122" transform="matrix(1,0,0,1,105.830723,109.774724)"/>
+<path style=" stroke:none;fill-rule:evenodd;fill:rgb(90.196078%,90.196078%,98.039216%);fill-opacity:0.5;" d="M 110.335938 92.902344 L 103.257812 95.363281 L 101.835938 91.441406 L 109.199219 89.058594 Z "/>
+</g>
+</svg>
diff --git a/test/python_tests/images/style-comp-op/clear.png b/test/python_tests/images/style-comp-op/clear.png
new file mode 100644
index 0000000..4fe9ab3
Binary files /dev/null and b/test/python_tests/images/style-comp-op/clear.png differ
diff --git a/test/python_tests/images/style-comp-op/color.png b/test/python_tests/images/style-comp-op/color.png
new file mode 100644
index 0000000..81dae90
Binary files /dev/null and b/test/python_tests/images/style-comp-op/color.png differ
diff --git a/test/python_tests/images/style-comp-op/color_burn.png b/test/python_tests/images/style-comp-op/color_burn.png
new file mode 100644
index 0000000..97a2700
Binary files /dev/null and b/test/python_tests/images/style-comp-op/color_burn.png differ
diff --git a/test/python_tests/images/style-comp-op/color_dodge.png b/test/python_tests/images/style-comp-op/color_dodge.png
new file mode 100644
index 0000000..932484b
Binary files /dev/null and b/test/python_tests/images/style-comp-op/color_dodge.png differ
diff --git a/test/python_tests/images/style-comp-op/contrast.png b/test/python_tests/images/style-comp-op/contrast.png
new file mode 100644
index 0000000..34b0a0a
Binary files /dev/null and b/test/python_tests/images/style-comp-op/contrast.png differ
diff --git a/test/python_tests/images/style-comp-op/darken.png b/test/python_tests/images/style-comp-op/darken.png
new file mode 100644
index 0000000..851ea68
Binary files /dev/null and b/test/python_tests/images/style-comp-op/darken.png differ
diff --git a/test/python_tests/images/style-comp-op/difference.png b/test/python_tests/images/style-comp-op/difference.png
new file mode 100644
index 0000000..7caf8dd
Binary files /dev/null and b/test/python_tests/images/style-comp-op/difference.png differ
diff --git a/test/python_tests/images/style-comp-op/divide.png b/test/python_tests/images/style-comp-op/divide.png
new file mode 100644
index 0000000..6f3aa77
Binary files /dev/null and b/test/python_tests/images/style-comp-op/divide.png differ
diff --git a/test/python_tests/images/style-comp-op/dst.png b/test/python_tests/images/style-comp-op/dst.png
new file mode 100644
index 0000000..68e0b39
Binary files /dev/null and b/test/python_tests/images/style-comp-op/dst.png differ
diff --git a/test/python_tests/images/style-comp-op/dst_atop.png b/test/python_tests/images/style-comp-op/dst_atop.png
new file mode 100644
index 0000000..bae1c89
Binary files /dev/null and b/test/python_tests/images/style-comp-op/dst_atop.png differ
diff --git a/test/python_tests/images/style-comp-op/dst_in.png b/test/python_tests/images/style-comp-op/dst_in.png
new file mode 100644
index 0000000..bae1c89
Binary files /dev/null and b/test/python_tests/images/style-comp-op/dst_in.png differ
diff --git a/test/python_tests/images/style-comp-op/dst_out.png b/test/python_tests/images/style-comp-op/dst_out.png
new file mode 100644
index 0000000..03e60dd
Binary files /dev/null and b/test/python_tests/images/style-comp-op/dst_out.png differ
diff --git a/test/python_tests/images/style-comp-op/dst_over.png b/test/python_tests/images/style-comp-op/dst_over.png
new file mode 100644
index 0000000..68e0b39
Binary files /dev/null and b/test/python_tests/images/style-comp-op/dst_over.png differ
diff --git a/test/python_tests/images/style-comp-op/exclusion.png b/test/python_tests/images/style-comp-op/exclusion.png
new file mode 100644
index 0000000..2cbe25e
Binary files /dev/null and b/test/python_tests/images/style-comp-op/exclusion.png differ
diff --git a/test/python_tests/images/style-comp-op/grain_extract.png b/test/python_tests/images/style-comp-op/grain_extract.png
new file mode 100644
index 0000000..9bc59c8
Binary files /dev/null and b/test/python_tests/images/style-comp-op/grain_extract.png differ
diff --git a/test/python_tests/images/style-comp-op/grain_merge.png b/test/python_tests/images/style-comp-op/grain_merge.png
new file mode 100644
index 0000000..5dd3d47
Binary files /dev/null and b/test/python_tests/images/style-comp-op/grain_merge.png differ
diff --git a/test/python_tests/images/style-comp-op/hard_light.png b/test/python_tests/images/style-comp-op/hard_light.png
new file mode 100644
index 0000000..0ad6fe5
Binary files /dev/null and b/test/python_tests/images/style-comp-op/hard_light.png differ
diff --git a/test/python_tests/images/style-comp-op/hue.png b/test/python_tests/images/style-comp-op/hue.png
new file mode 100644
index 0000000..2955c1a
Binary files /dev/null and b/test/python_tests/images/style-comp-op/hue.png differ
diff --git a/test/python_tests/images/style-comp-op/invert.png b/test/python_tests/images/style-comp-op/invert.png
new file mode 100644
index 0000000..5e97592
Binary files /dev/null and b/test/python_tests/images/style-comp-op/invert.png differ
diff --git a/test/python_tests/images/style-comp-op/lighten.png b/test/python_tests/images/style-comp-op/lighten.png
new file mode 100644
index 0000000..4d44fc5
Binary files /dev/null and b/test/python_tests/images/style-comp-op/lighten.png differ
diff --git a/test/python_tests/images/style-comp-op/linear_burn.png b/test/python_tests/images/style-comp-op/linear_burn.png
new file mode 100644
index 0000000..458772c
Binary files /dev/null and b/test/python_tests/images/style-comp-op/linear_burn.png differ
diff --git a/test/python_tests/images/style-comp-op/linear_dodge.png b/test/python_tests/images/style-comp-op/linear_dodge.png
new file mode 100644
index 0000000..c94969c
Binary files /dev/null and b/test/python_tests/images/style-comp-op/linear_dodge.png differ
diff --git a/test/python_tests/images/style-comp-op/minus.png b/test/python_tests/images/style-comp-op/minus.png
new file mode 100644
index 0000000..83d68aa
Binary files /dev/null and b/test/python_tests/images/style-comp-op/minus.png differ
diff --git a/test/python_tests/images/style-comp-op/multiply.png b/test/python_tests/images/style-comp-op/multiply.png
new file mode 100644
index 0000000..87b2452
Binary files /dev/null and b/test/python_tests/images/style-comp-op/multiply.png differ
diff --git a/test/python_tests/images/style-comp-op/overlay.png b/test/python_tests/images/style-comp-op/overlay.png
new file mode 100644
index 0000000..44a864b
Binary files /dev/null and b/test/python_tests/images/style-comp-op/overlay.png differ
diff --git a/test/python_tests/images/style-comp-op/plus.png b/test/python_tests/images/style-comp-op/plus.png
new file mode 100644
index 0000000..c94969c
Binary files /dev/null and b/test/python_tests/images/style-comp-op/plus.png differ
diff --git a/test/python_tests/images/style-comp-op/saturation.png b/test/python_tests/images/style-comp-op/saturation.png
new file mode 100644
index 0000000..fcbf639
Binary files /dev/null and b/test/python_tests/images/style-comp-op/saturation.png differ
diff --git a/test/python_tests/images/style-comp-op/screen.png b/test/python_tests/images/style-comp-op/screen.png
new file mode 100644
index 0000000..43ab7ac
Binary files /dev/null and b/test/python_tests/images/style-comp-op/screen.png differ
diff --git a/test/python_tests/images/style-comp-op/soft_light.png b/test/python_tests/images/style-comp-op/soft_light.png
new file mode 100644
index 0000000..5d5487a
Binary files /dev/null and b/test/python_tests/images/style-comp-op/soft_light.png differ
diff --git a/test/python_tests/images/style-comp-op/src.png b/test/python_tests/images/style-comp-op/src.png
new file mode 100644
index 0000000..a7a96f5
Binary files /dev/null and b/test/python_tests/images/style-comp-op/src.png differ
diff --git a/test/python_tests/images/style-comp-op/src_atop.png b/test/python_tests/images/style-comp-op/src_atop.png
new file mode 100644
index 0000000..1f04875
Binary files /dev/null and b/test/python_tests/images/style-comp-op/src_atop.png differ
diff --git a/test/python_tests/images/style-comp-op/src_in.png b/test/python_tests/images/style-comp-op/src_in.png
new file mode 100644
index 0000000..a7a96f5
Binary files /dev/null and b/test/python_tests/images/style-comp-op/src_in.png differ
diff --git a/test/python_tests/images/style-comp-op/src_out.png b/test/python_tests/images/style-comp-op/src_out.png
new file mode 100644
index 0000000..4fe9ab3
Binary files /dev/null and b/test/python_tests/images/style-comp-op/src_out.png differ
diff --git a/test/python_tests/images/style-comp-op/src_over.png b/test/python_tests/images/style-comp-op/src_over.png
new file mode 100644
index 0000000..42d7b16
Binary files /dev/null and b/test/python_tests/images/style-comp-op/src_over.png differ
diff --git a/test/python_tests/images/style-comp-op/value.png b/test/python_tests/images/style-comp-op/value.png
new file mode 100644
index 0000000..8fd064d
Binary files /dev/null and b/test/python_tests/images/style-comp-op/value.png differ
diff --git a/test/python_tests/images/style-comp-op/xor.png b/test/python_tests/images/style-comp-op/xor.png
new file mode 100644
index 0000000..b062cb6
Binary files /dev/null and b/test/python_tests/images/style-comp-op/xor.png differ
diff --git a/test/python_tests/images/style-image-filter/agg-stack-blur22.png b/test/python_tests/images/style-image-filter/agg-stack-blur22.png
new file mode 100644
index 0000000..1d1b7ca
Binary files /dev/null and b/test/python_tests/images/style-image-filter/agg-stack-blur22.png differ
diff --git a/test/python_tests/images/style-image-filter/blur.png b/test/python_tests/images/style-image-filter/blur.png
new file mode 100644
index 0000000..ec6fc7f
Binary files /dev/null and b/test/python_tests/images/style-image-filter/blur.png differ
diff --git a/test/python_tests/images/style-image-filter/edge-detect.png b/test/python_tests/images/style-image-filter/edge-detect.png
new file mode 100644
index 0000000..74eff4d
Binary files /dev/null and b/test/python_tests/images/style-image-filter/edge-detect.png differ
diff --git a/test/python_tests/images/style-image-filter/emboss.png b/test/python_tests/images/style-image-filter/emboss.png
new file mode 100644
index 0000000..bf74d99
Binary files /dev/null and b/test/python_tests/images/style-image-filter/emboss.png differ
diff --git a/test/python_tests/images/style-image-filter/gray.png b/test/python_tests/images/style-image-filter/gray.png
new file mode 100644
index 0000000..7ee05f5
Binary files /dev/null and b/test/python_tests/images/style-image-filter/gray.png differ
diff --git a/test/python_tests/images/style-image-filter/invert.png b/test/python_tests/images/style-image-filter/invert.png
new file mode 100644
index 0000000..52bcf95
Binary files /dev/null and b/test/python_tests/images/style-image-filter/invert.png differ
diff --git a/test/python_tests/images/style-image-filter/none.png b/test/python_tests/images/style-image-filter/none.png
new file mode 100644
index 0000000..245966d
Binary files /dev/null and b/test/python_tests/images/style-image-filter/none.png differ
diff --git a/test/python_tests/images/style-image-filter/sharpen.png b/test/python_tests/images/style-image-filter/sharpen.png
new file mode 100644
index 0000000..8599186
Binary files /dev/null and b/test/python_tests/images/style-image-filter/sharpen.png differ
diff --git a/test/python_tests/images/style-image-filter/sobel.png b/test/python_tests/images/style-image-filter/sobel.png
new file mode 100644
index 0000000..c1b7092
Binary files /dev/null and b/test/python_tests/images/style-image-filter/sobel.png differ
diff --git a/test/python_tests/images/style-image-filter/x-gradient.png b/test/python_tests/images/style-image-filter/x-gradient.png
new file mode 100644
index 0000000..fdc5f74
Binary files /dev/null and b/test/python_tests/images/style-image-filter/x-gradient.png differ
diff --git a/test/python_tests/images/style-image-filter/y-gradient.png b/test/python_tests/images/style-image-filter/y-gradient.png
new file mode 100644
index 0000000..b84a491
Binary files /dev/null and b/test/python_tests/images/style-image-filter/y-gradient.png differ
diff --git a/test/python_tests/images/support/a.png b/test/python_tests/images/support/a.png
new file mode 100644
index 0000000..3d0cc72
Binary files /dev/null and b/test/python_tests/images/support/a.png differ
diff --git a/test/python_tests/images/support/b.png b/test/python_tests/images/support/b.png
new file mode 100644
index 0000000..6eca9e1
Binary files /dev/null and b/test/python_tests/images/support/b.png differ
diff --git a/test/python_tests/images/support/dataraster_coloring.png b/test/python_tests/images/support/dataraster_coloring.png
new file mode 100644
index 0000000..da3cac4
Binary files /dev/null and b/test/python_tests/images/support/dataraster_coloring.png differ
diff --git a/test/python_tests/images/support/encoding-opts/aerial_rgba-png+e=miniz.png b/test/python_tests/images/support/encoding-opts/aerial_rgba-png+e=miniz.png
new file mode 100644
index 0000000..4cc101b
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/aerial_rgba-png+e=miniz.png differ
diff --git a/test/python_tests/images/support/encoding-opts/aerial_rgba-png+t=0.png b/test/python_tests/images/support/encoding-opts/aerial_rgba-png+t=0.png
new file mode 100644
index 0000000..b2aa991
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/aerial_rgba-png+t=0.png differ
diff --git a/test/python_tests/images/support/encoding-opts/aerial_rgba-png.png b/test/python_tests/images/support/encoding-opts/aerial_rgba-png.png
new file mode 100644
index 0000000..b2aa991
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/aerial_rgba-png.png differ
diff --git a/test/python_tests/images/support/encoding-opts/aerial_rgba-png32+e=miniz.png b/test/python_tests/images/support/encoding-opts/aerial_rgba-png32+e=miniz.png
new file mode 100644
index 0000000..046b4fc
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/aerial_rgba-png32+e=miniz.png differ
diff --git a/test/python_tests/images/support/encoding-opts/aerial_rgba-png32+t=0.png b/test/python_tests/images/support/encoding-opts/aerial_rgba-png32+t=0.png
new file mode 100644
index 0000000..16481a4
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/aerial_rgba-png32+t=0.png differ
diff --git a/test/python_tests/images/support/encoding-opts/aerial_rgba-png32.png b/test/python_tests/images/support/encoding-opts/aerial_rgba-png32.png
new file mode 100644
index 0000000..0f15a35
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/aerial_rgba-png32.png differ
diff --git a/test/python_tests/images/support/encoding-opts/aerial_rgba-png8+e=miniz.png b/test/python_tests/images/support/encoding-opts/aerial_rgba-png8+e=miniz.png
new file mode 100644
index 0000000..d7dd172
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/aerial_rgba-png8+e=miniz.png differ
diff --git a/test/python_tests/images/support/encoding-opts/aerial_rgba-png8+m=h+c=1+t=0.png b/test/python_tests/images/support/encoding-opts/aerial_rgba-png8+m=h+c=1+t=0.png
new file mode 100644
index 0000000..d6e19b7
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/aerial_rgba-png8+m=h+c=1+t=0.png differ
diff --git a/test/python_tests/images/support/encoding-opts/aerial_rgba-png8+m=h+c=1.png b/test/python_tests/images/support/encoding-opts/aerial_rgba-png8+m=h+c=1.png
new file mode 100644
index 0000000..d6e19b7
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/aerial_rgba-png8+m=h+c=1.png differ
diff --git a/test/python_tests/images/support/encoding-opts/aerial_rgba-png8+m=h+t=0.png b/test/python_tests/images/support/encoding-opts/aerial_rgba-png8+m=h+t=0.png
new file mode 100644
index 0000000..c6c7ab9
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/aerial_rgba-png8+m=h+t=0.png differ
diff --git a/test/python_tests/images/support/encoding-opts/aerial_rgba-png8+m=h+t=1.png b/test/python_tests/images/support/encoding-opts/aerial_rgba-png8+m=h+t=1.png
new file mode 100644
index 0000000..c6c7ab9
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/aerial_rgba-png8+m=h+t=1.png differ
diff --git a/test/python_tests/images/support/encoding-opts/aerial_rgba-png8+m=h+t=2.png b/test/python_tests/images/support/encoding-opts/aerial_rgba-png8+m=h+t=2.png
new file mode 100644
index 0000000..c6c7ab9
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/aerial_rgba-png8+m=h+t=2.png differ
diff --git a/test/python_tests/images/support/encoding-opts/aerial_rgba-png8+m=h.png b/test/python_tests/images/support/encoding-opts/aerial_rgba-png8+m=h.png
new file mode 100644
index 0000000..c6c7ab9
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/aerial_rgba-png8+m=h.png differ
diff --git a/test/python_tests/images/support/encoding-opts/aerial_rgba-png8+m=o+c=1+t=0.png b/test/python_tests/images/support/encoding-opts/aerial_rgba-png8+m=o+c=1+t=0.png
new file mode 100644
index 0000000..ad0aca7
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/aerial_rgba-png8+m=o+c=1+t=0.png differ
diff --git a/test/python_tests/images/support/encoding-opts/aerial_rgba-png8+m=o+c=1.png b/test/python_tests/images/support/encoding-opts/aerial_rgba-png8+m=o+c=1.png
new file mode 100644
index 0000000..ad0aca7
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/aerial_rgba-png8+m=o+c=1.png differ
diff --git a/test/python_tests/images/support/encoding-opts/aerial_rgba-png8+m=o+t=0.png b/test/python_tests/images/support/encoding-opts/aerial_rgba-png8+m=o+t=0.png
new file mode 100644
index 0000000..4ab31b6
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/aerial_rgba-png8+m=o+t=0.png differ
diff --git a/test/python_tests/images/support/encoding-opts/aerial_rgba-png8+m=o+t=1.png b/test/python_tests/images/support/encoding-opts/aerial_rgba-png8+m=o+t=1.png
new file mode 100644
index 0000000..4ab31b6
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/aerial_rgba-png8+m=o+t=1.png differ
diff --git a/test/python_tests/images/support/encoding-opts/aerial_rgba-png8+m=o+t=2.png b/test/python_tests/images/support/encoding-opts/aerial_rgba-png8+m=o+t=2.png
new file mode 100644
index 0000000..4ab31b6
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/aerial_rgba-png8+m=o+t=2.png differ
diff --git a/test/python_tests/images/support/encoding-opts/aerial_rgba-png8+m=o.png b/test/python_tests/images/support/encoding-opts/aerial_rgba-png8+m=o.png
new file mode 100644
index 0000000..4ab31b6
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/aerial_rgba-png8+m=o.png differ
diff --git a/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+alpha=false.webp b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+alpha=false.webp
new file mode 100644
index 0000000..f2a3cfa
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+alpha=false.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+alpha_compression=0.webp b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+alpha_compression=0.webp
new file mode 100644
index 0000000..f2a3cfa
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+alpha_compression=0.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+alpha_filtering=2.webp b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+alpha_filtering=2.webp
new file mode 100644
index 0000000..f2a3cfa
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+alpha_filtering=2.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+alpha_quality=50.webp b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+alpha_quality=50.webp
new file mode 100644
index 0000000..f2a3cfa
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+alpha_quality=50.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+autofilter=0.webp b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+autofilter=0.webp
new file mode 100644
index 0000000..f2a3cfa
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+autofilter=0.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+filter_sharpness=4.webp b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+filter_sharpness=4.webp
new file mode 100644
index 0000000..f29a97e
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+filter_sharpness=4.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+filter_strength=50.webp b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+filter_strength=50.webp
new file mode 100644
index 0000000..87c1fe6
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+filter_strength=50.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+filter_type=1+autofilter=1.webp b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+filter_type=1+autofilter=1.webp
new file mode 100644
index 0000000..752148d
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+filter_type=1+autofilter=1.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+method=0.webp b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+method=0.webp
new file mode 100644
index 0000000..f0f3838
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+method=0.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+method=6.webp b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+method=6.webp
new file mode 100644
index 0000000..be253e2
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+method=6.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+partition_limit=50.webp b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+partition_limit=50.webp
new file mode 100644
index 0000000..837ff3b
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+partition_limit=50.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+partitions=3.webp b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+partitions=3.webp
new file mode 100644
index 0000000..f2a3cfa
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+partitions=3.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+pass=10.webp b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+pass=10.webp
new file mode 100644
index 0000000..7d71893
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+pass=10.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+preprocessing=1.webp b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+preprocessing=1.webp
new file mode 100644
index 0000000..f5e64b7
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+preprocessing=1.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+quality=64.webp b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+quality=64.webp
new file mode 100644
index 0000000..efd13ad
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+quality=64.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+segments=3.webp b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+segments=3.webp
new file mode 100644
index 0000000..967cc57
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+segments=3.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+sns_strength=50.webp b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+sns_strength=50.webp
new file mode 100644
index 0000000..f2a3cfa
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+sns_strength=50.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+target_PSNR=.5.webp b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+target_PSNR=.5.webp
new file mode 100644
index 0000000..f2a3cfa
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+target_PSNR=.5.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+target_size=100.webp b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+target_size=100.webp
new file mode 100644
index 0000000..f2a3cfa
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp+target_size=100.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/aerial_rgba-webp.webp b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp.webp
new file mode 100644
index 0000000..f2a3cfa
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/aerial_rgba-webp.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/blank-png+e=miniz.png b/test/python_tests/images/support/encoding-opts/blank-png+e=miniz.png
new file mode 100644
index 0000000..334ffe5
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/blank-png+e=miniz.png differ
diff --git a/test/python_tests/images/support/encoding-opts/blank-png+t=0.png b/test/python_tests/images/support/encoding-opts/blank-png+t=0.png
new file mode 100644
index 0000000..49c789a
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/blank-png+t=0.png differ
diff --git a/test/python_tests/images/support/encoding-opts/blank-png.png b/test/python_tests/images/support/encoding-opts/blank-png.png
new file mode 100644
index 0000000..49c789a
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/blank-png.png differ
diff --git a/test/python_tests/images/support/encoding-opts/blank-png32+e=miniz.png b/test/python_tests/images/support/encoding-opts/blank-png32+e=miniz.png
new file mode 100644
index 0000000..8708c32
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/blank-png32+e=miniz.png differ
diff --git a/test/python_tests/images/support/encoding-opts/blank-png32+t=0.png b/test/python_tests/images/support/encoding-opts/blank-png32+t=0.png
new file mode 100644
index 0000000..50e3890
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/blank-png32+t=0.png differ
diff --git a/test/python_tests/images/support/encoding-opts/blank-png32.png b/test/python_tests/images/support/encoding-opts/blank-png32.png
new file mode 100644
index 0000000..04469ac
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/blank-png32.png differ
diff --git a/test/python_tests/images/support/encoding-opts/blank-png8+e=miniz.png b/test/python_tests/images/support/encoding-opts/blank-png8+e=miniz.png
new file mode 100644
index 0000000..334ffe5
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/blank-png8+e=miniz.png differ
diff --git a/test/python_tests/images/support/encoding-opts/blank-png8+m=h+c=1+t=0.png b/test/python_tests/images/support/encoding-opts/blank-png8+m=h+c=1+t=0.png
new file mode 100644
index 0000000..49c789a
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/blank-png8+m=h+c=1+t=0.png differ
diff --git a/test/python_tests/images/support/encoding-opts/blank-png8+m=h+c=1.png b/test/python_tests/images/support/encoding-opts/blank-png8+m=h+c=1.png
new file mode 100644
index 0000000..49c789a
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/blank-png8+m=h+c=1.png differ
diff --git a/test/python_tests/images/support/encoding-opts/blank-png8+m=h+t=0.png b/test/python_tests/images/support/encoding-opts/blank-png8+m=h+t=0.png
new file mode 100644
index 0000000..49c789a
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/blank-png8+m=h+t=0.png differ
diff --git a/test/python_tests/images/support/encoding-opts/blank-png8+m=h+t=1.png b/test/python_tests/images/support/encoding-opts/blank-png8+m=h+t=1.png
new file mode 100644
index 0000000..49c789a
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/blank-png8+m=h+t=1.png differ
diff --git a/test/python_tests/images/support/encoding-opts/blank-png8+m=h+t=2.png b/test/python_tests/images/support/encoding-opts/blank-png8+m=h+t=2.png
new file mode 100644
index 0000000..49c789a
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/blank-png8+m=h+t=2.png differ
diff --git a/test/python_tests/images/support/encoding-opts/blank-png8+m=h.png b/test/python_tests/images/support/encoding-opts/blank-png8+m=h.png
new file mode 100644
index 0000000..49c789a
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/blank-png8+m=h.png differ
diff --git a/test/python_tests/images/support/encoding-opts/blank-png8+m=o+c=1+t=0.png b/test/python_tests/images/support/encoding-opts/blank-png8+m=o+c=1+t=0.png
new file mode 100644
index 0000000..49c789a
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/blank-png8+m=o+c=1+t=0.png differ
diff --git a/test/python_tests/images/support/encoding-opts/blank-png8+m=o+c=1.png b/test/python_tests/images/support/encoding-opts/blank-png8+m=o+c=1.png
new file mode 100644
index 0000000..49c789a
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/blank-png8+m=o+c=1.png differ
diff --git a/test/python_tests/images/support/encoding-opts/blank-png8+m=o+t=0.png b/test/python_tests/images/support/encoding-opts/blank-png8+m=o+t=0.png
new file mode 100644
index 0000000..49c789a
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/blank-png8+m=o+t=0.png differ
diff --git a/test/python_tests/images/support/encoding-opts/blank-png8+m=o+t=1.png b/test/python_tests/images/support/encoding-opts/blank-png8+m=o+t=1.png
new file mode 100644
index 0000000..49c789a
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/blank-png8+m=o+t=1.png differ
diff --git a/test/python_tests/images/support/encoding-opts/blank-png8+m=o+t=2.png b/test/python_tests/images/support/encoding-opts/blank-png8+m=o+t=2.png
new file mode 100644
index 0000000..49c789a
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/blank-png8+m=o+t=2.png differ
diff --git a/test/python_tests/images/support/encoding-opts/blank-png8+m=o.png b/test/python_tests/images/support/encoding-opts/blank-png8+m=o.png
new file mode 100644
index 0000000..49c789a
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/blank-png8+m=o.png differ
diff --git a/test/python_tests/images/support/encoding-opts/blank-webp+alpha=false.webp b/test/python_tests/images/support/encoding-opts/blank-webp+alpha=false.webp
new file mode 100644
index 0000000..da95b42
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/blank-webp+alpha=false.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/blank-webp+alpha_compression=0.webp b/test/python_tests/images/support/encoding-opts/blank-webp+alpha_compression=0.webp
new file mode 100644
index 0000000..2e264d4
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/blank-webp+alpha_compression=0.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/blank-webp+alpha_filtering=2.webp b/test/python_tests/images/support/encoding-opts/blank-webp+alpha_filtering=2.webp
new file mode 100644
index 0000000..a7369dc
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/blank-webp+alpha_filtering=2.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/blank-webp+alpha_quality=50.webp b/test/python_tests/images/support/encoding-opts/blank-webp+alpha_quality=50.webp
new file mode 100644
index 0000000..10cea1c
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/blank-webp+alpha_quality=50.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/blank-webp+autofilter=0.webp b/test/python_tests/images/support/encoding-opts/blank-webp+autofilter=0.webp
new file mode 100644
index 0000000..a7369dc
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/blank-webp+autofilter=0.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/blank-webp+filter_sharpness=4.webp b/test/python_tests/images/support/encoding-opts/blank-webp+filter_sharpness=4.webp
new file mode 100644
index 0000000..932a4de
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/blank-webp+filter_sharpness=4.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/blank-webp+filter_strength=50.webp b/test/python_tests/images/support/encoding-opts/blank-webp+filter_strength=50.webp
new file mode 100644
index 0000000..2e65b9b
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/blank-webp+filter_strength=50.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/blank-webp+filter_type=1+autofilter=1.webp b/test/python_tests/images/support/encoding-opts/blank-webp+filter_type=1+autofilter=1.webp
new file mode 100644
index 0000000..7e3bd76
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/blank-webp+filter_type=1+autofilter=1.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/blank-webp+method=0.webp b/test/python_tests/images/support/encoding-opts/blank-webp+method=0.webp
new file mode 100644
index 0000000..5c64924
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/blank-webp+method=0.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/blank-webp+method=6.webp b/test/python_tests/images/support/encoding-opts/blank-webp+method=6.webp
new file mode 100644
index 0000000..ef84f4c
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/blank-webp+method=6.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/blank-webp+partition_limit=50.webp b/test/python_tests/images/support/encoding-opts/blank-webp+partition_limit=50.webp
new file mode 100644
index 0000000..a7369dc
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/blank-webp+partition_limit=50.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/blank-webp+partitions=3.webp b/test/python_tests/images/support/encoding-opts/blank-webp+partitions=3.webp
new file mode 100644
index 0000000..a7369dc
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/blank-webp+partitions=3.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/blank-webp+pass=10.webp b/test/python_tests/images/support/encoding-opts/blank-webp+pass=10.webp
new file mode 100644
index 0000000..a7369dc
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/blank-webp+pass=10.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/blank-webp+preprocessing=1.webp b/test/python_tests/images/support/encoding-opts/blank-webp+preprocessing=1.webp
new file mode 100644
index 0000000..a7369dc
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/blank-webp+preprocessing=1.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/blank-webp+quality=64.webp b/test/python_tests/images/support/encoding-opts/blank-webp+quality=64.webp
new file mode 100644
index 0000000..0eb26aa
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/blank-webp+quality=64.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/blank-webp+segments=3.webp b/test/python_tests/images/support/encoding-opts/blank-webp+segments=3.webp
new file mode 100644
index 0000000..af3082b
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/blank-webp+segments=3.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/blank-webp+sns_strength=50.webp b/test/python_tests/images/support/encoding-opts/blank-webp+sns_strength=50.webp
new file mode 100644
index 0000000..a7369dc
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/blank-webp+sns_strength=50.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/blank-webp+target_PSNR=.5.webp b/test/python_tests/images/support/encoding-opts/blank-webp+target_PSNR=.5.webp
new file mode 100644
index 0000000..a7369dc
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/blank-webp+target_PSNR=.5.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/blank-webp+target_size=100.webp b/test/python_tests/images/support/encoding-opts/blank-webp+target_size=100.webp
new file mode 100644
index 0000000..a7369dc
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/blank-webp+target_size=100.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/blank-webp.webp b/test/python_tests/images/support/encoding-opts/blank-webp.webp
new file mode 100644
index 0000000..a7369dc
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/blank-webp.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/png8-17cols.png b/test/python_tests/images/support/encoding-opts/png8-17cols.png
new file mode 100644
index 0000000..22f4c35
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/png8-17cols.png differ
diff --git a/test/python_tests/images/support/encoding-opts/png8-2px.A.png b/test/python_tests/images/support/encoding-opts/png8-2px.A.png
new file mode 100644
index 0000000..f047b08
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/png8-2px.A.png differ
diff --git a/test/python_tests/images/support/encoding-opts/png8-2px.png b/test/python_tests/images/support/encoding-opts/png8-2px.png
new file mode 100644
index 0000000..f047b08
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/png8-2px.png differ
diff --git a/test/python_tests/images/support/encoding-opts/png8-9cols.png b/test/python_tests/images/support/encoding-opts/png8-9cols.png
new file mode 100644
index 0000000..a781b37
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/png8-9cols.png differ
diff --git a/test/python_tests/images/support/encoding-opts/solid-png+e=miniz.png b/test/python_tests/images/support/encoding-opts/solid-png+e=miniz.png
new file mode 100644
index 0000000..c3e5db4
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/solid-png+e=miniz.png differ
diff --git a/test/python_tests/images/support/encoding-opts/solid-png+t=0.png b/test/python_tests/images/support/encoding-opts/solid-png+t=0.png
new file mode 100644
index 0000000..0d7dee0
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/solid-png+t=0.png differ
diff --git a/test/python_tests/images/support/encoding-opts/solid-png.png b/test/python_tests/images/support/encoding-opts/solid-png.png
new file mode 100644
index 0000000..a4c4de3
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/solid-png.png differ
diff --git a/test/python_tests/images/support/encoding-opts/solid-png32+e=miniz.png b/test/python_tests/images/support/encoding-opts/solid-png32+e=miniz.png
new file mode 100644
index 0000000..f155fce
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/solid-png32+e=miniz.png differ
diff --git a/test/python_tests/images/support/encoding-opts/solid-png32+t=0.png b/test/python_tests/images/support/encoding-opts/solid-png32+t=0.png
new file mode 100644
index 0000000..21fffbf
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/solid-png32+t=0.png differ
diff --git a/test/python_tests/images/support/encoding-opts/solid-png32.png b/test/python_tests/images/support/encoding-opts/solid-png32.png
new file mode 100644
index 0000000..4fe9ab3
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/solid-png32.png differ
diff --git a/test/python_tests/images/support/encoding-opts/solid-png8+e=miniz.png b/test/python_tests/images/support/encoding-opts/solid-png8+e=miniz.png
new file mode 100644
index 0000000..c3e5db4
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/solid-png8+e=miniz.png differ
diff --git a/test/python_tests/images/support/encoding-opts/solid-png8+m=h+c=1+t=0.png b/test/python_tests/images/support/encoding-opts/solid-png8+m=h+c=1+t=0.png
new file mode 100644
index 0000000..0d7dee0
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/solid-png8+m=h+c=1+t=0.png differ
diff --git a/test/python_tests/images/support/encoding-opts/solid-png8+m=h+c=1.png b/test/python_tests/images/support/encoding-opts/solid-png8+m=h+c=1.png
new file mode 100644
index 0000000..a4c4de3
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/solid-png8+m=h+c=1.png differ
diff --git a/test/python_tests/images/support/encoding-opts/solid-png8+m=h+t=0.png b/test/python_tests/images/support/encoding-opts/solid-png8+m=h+t=0.png
new file mode 100644
index 0000000..0d7dee0
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/solid-png8+m=h+t=0.png differ
diff --git a/test/python_tests/images/support/encoding-opts/solid-png8+m=h+t=1.png b/test/python_tests/images/support/encoding-opts/solid-png8+m=h+t=1.png
new file mode 100644
index 0000000..a4c4de3
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/solid-png8+m=h+t=1.png differ
diff --git a/test/python_tests/images/support/encoding-opts/solid-png8+m=h+t=2.png b/test/python_tests/images/support/encoding-opts/solid-png8+m=h+t=2.png
new file mode 100644
index 0000000..a4c4de3
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/solid-png8+m=h+t=2.png differ
diff --git a/test/python_tests/images/support/encoding-opts/solid-png8+m=h.png b/test/python_tests/images/support/encoding-opts/solid-png8+m=h.png
new file mode 100644
index 0000000..a4c4de3
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/solid-png8+m=h.png differ
diff --git a/test/python_tests/images/support/encoding-opts/solid-png8+m=o+c=1+t=0.png b/test/python_tests/images/support/encoding-opts/solid-png8+m=o+c=1+t=0.png
new file mode 100644
index 0000000..0d7dee0
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/solid-png8+m=o+c=1+t=0.png differ
diff --git a/test/python_tests/images/support/encoding-opts/solid-png8+m=o+c=1.png b/test/python_tests/images/support/encoding-opts/solid-png8+m=o+c=1.png
new file mode 100644
index 0000000..a4c4de3
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/solid-png8+m=o+c=1.png differ
diff --git a/test/python_tests/images/support/encoding-opts/solid-png8+m=o+t=0.png b/test/python_tests/images/support/encoding-opts/solid-png8+m=o+t=0.png
new file mode 100644
index 0000000..0d7dee0
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/solid-png8+m=o+t=0.png differ
diff --git a/test/python_tests/images/support/encoding-opts/solid-png8+m=o+t=1.png b/test/python_tests/images/support/encoding-opts/solid-png8+m=o+t=1.png
new file mode 100644
index 0000000..a4c4de3
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/solid-png8+m=o+t=1.png differ
diff --git a/test/python_tests/images/support/encoding-opts/solid-png8+m=o+t=2.png b/test/python_tests/images/support/encoding-opts/solid-png8+m=o+t=2.png
new file mode 100644
index 0000000..a4c4de3
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/solid-png8+m=o+t=2.png differ
diff --git a/test/python_tests/images/support/encoding-opts/solid-png8+m=o.png b/test/python_tests/images/support/encoding-opts/solid-png8+m=o.png
new file mode 100644
index 0000000..a4c4de3
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/solid-png8+m=o.png differ
diff --git a/test/python_tests/images/support/encoding-opts/solid-webp+alpha=false.webp b/test/python_tests/images/support/encoding-opts/solid-webp+alpha=false.webp
new file mode 100644
index 0000000..cbd85ab
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/solid-webp+alpha=false.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/solid-webp+alpha_compression=0.webp b/test/python_tests/images/support/encoding-opts/solid-webp+alpha_compression=0.webp
new file mode 100644
index 0000000..cbd85ab
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/solid-webp+alpha_compression=0.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/solid-webp+alpha_filtering=2.webp b/test/python_tests/images/support/encoding-opts/solid-webp+alpha_filtering=2.webp
new file mode 100644
index 0000000..cbd85ab
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/solid-webp+alpha_filtering=2.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/solid-webp+alpha_quality=50.webp b/test/python_tests/images/support/encoding-opts/solid-webp+alpha_quality=50.webp
new file mode 100644
index 0000000..cbd85ab
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/solid-webp+alpha_quality=50.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/solid-webp+autofilter=0.webp b/test/python_tests/images/support/encoding-opts/solid-webp+autofilter=0.webp
new file mode 100644
index 0000000..cbd85ab
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/solid-webp+autofilter=0.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/solid-webp+filter_sharpness=4.webp b/test/python_tests/images/support/encoding-opts/solid-webp+filter_sharpness=4.webp
new file mode 100644
index 0000000..1eadaa1
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/solid-webp+filter_sharpness=4.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/solid-webp+filter_strength=50.webp b/test/python_tests/images/support/encoding-opts/solid-webp+filter_strength=50.webp
new file mode 100644
index 0000000..621e6cc
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/solid-webp+filter_strength=50.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/solid-webp+filter_type=1+autofilter=1.webp b/test/python_tests/images/support/encoding-opts/solid-webp+filter_type=1+autofilter=1.webp
new file mode 100644
index 0000000..1372321
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/solid-webp+filter_type=1+autofilter=1.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/solid-webp+method=0.webp b/test/python_tests/images/support/encoding-opts/solid-webp+method=0.webp
new file mode 100644
index 0000000..e7cff65
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/solid-webp+method=0.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/solid-webp+method=6.webp b/test/python_tests/images/support/encoding-opts/solid-webp+method=6.webp
new file mode 100644
index 0000000..5a76594
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/solid-webp+method=6.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/solid-webp+partition_limit=50.webp b/test/python_tests/images/support/encoding-opts/solid-webp+partition_limit=50.webp
new file mode 100644
index 0000000..cbd85ab
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/solid-webp+partition_limit=50.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/solid-webp+partitions=3.webp b/test/python_tests/images/support/encoding-opts/solid-webp+partitions=3.webp
new file mode 100644
index 0000000..cbd85ab
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/solid-webp+partitions=3.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/solid-webp+pass=10.webp b/test/python_tests/images/support/encoding-opts/solid-webp+pass=10.webp
new file mode 100644
index 0000000..cbd85ab
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/solid-webp+pass=10.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/solid-webp+preprocessing=1.webp b/test/python_tests/images/support/encoding-opts/solid-webp+preprocessing=1.webp
new file mode 100644
index 0000000..cbd85ab
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/solid-webp+preprocessing=1.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/solid-webp+quality=64.webp b/test/python_tests/images/support/encoding-opts/solid-webp+quality=64.webp
new file mode 100644
index 0000000..fba30ff
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/solid-webp+quality=64.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/solid-webp+segments=3.webp b/test/python_tests/images/support/encoding-opts/solid-webp+segments=3.webp
new file mode 100644
index 0000000..84ba1a8
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/solid-webp+segments=3.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/solid-webp+sns_strength=50.webp b/test/python_tests/images/support/encoding-opts/solid-webp+sns_strength=50.webp
new file mode 100644
index 0000000..cbd85ab
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/solid-webp+sns_strength=50.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/solid-webp+target_PSNR=.5.webp b/test/python_tests/images/support/encoding-opts/solid-webp+target_PSNR=.5.webp
new file mode 100644
index 0000000..cbd85ab
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/solid-webp+target_PSNR=.5.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/solid-webp+target_size=100.webp b/test/python_tests/images/support/encoding-opts/solid-webp+target_size=100.webp
new file mode 100644
index 0000000..cbd85ab
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/solid-webp+target_size=100.webp differ
diff --git a/test/python_tests/images/support/encoding-opts/solid-webp.webp b/test/python_tests/images/support/encoding-opts/solid-webp.webp
new file mode 100644
index 0000000..cbd85ab
Binary files /dev/null and b/test/python_tests/images/support/encoding-opts/solid-webp.webp differ
diff --git a/test/python_tests/images/support/mapnik-layer-buffer-size.png b/test/python_tests/images/support/mapnik-layer-buffer-size.png
new file mode 100644
index 0000000..f1de74a
Binary files /dev/null and b/test/python_tests/images/support/mapnik-layer-buffer-size.png differ
diff --git a/test/python_tests/images/support/mapnik-marker-ellipse-render1.png b/test/python_tests/images/support/mapnik-marker-ellipse-render1.png
new file mode 100644
index 0000000..7854c56
Binary files /dev/null and b/test/python_tests/images/support/mapnik-marker-ellipse-render1.png differ
diff --git a/test/python_tests/images/support/mapnik-marker-ellipse-render2.png b/test/python_tests/images/support/mapnik-marker-ellipse-render2.png
new file mode 100644
index 0000000..c2a4963
Binary files /dev/null and b/test/python_tests/images/support/mapnik-marker-ellipse-render2.png differ
diff --git a/test/python_tests/images/support/mapnik-merc2merc-reprojection-render1.png b/test/python_tests/images/support/mapnik-merc2merc-reprojection-render1.png
new file mode 100644
index 0000000..2b49eb1
Binary files /dev/null and b/test/python_tests/images/support/mapnik-merc2merc-reprojection-render1.png differ
diff --git a/test/python_tests/images/support/mapnik-merc2merc-reprojection-render2.png b/test/python_tests/images/support/mapnik-merc2merc-reprojection-render2.png
new file mode 100644
index 0000000..e2c237d
Binary files /dev/null and b/test/python_tests/images/support/mapnik-merc2merc-reprojection-render2.png differ
diff --git a/test/python_tests/images/support/mapnik-merc2wgs84-reprojection-render.png b/test/python_tests/images/support/mapnik-merc2wgs84-reprojection-render.png
new file mode 100644
index 0000000..718d60a
Binary files /dev/null and b/test/python_tests/images/support/mapnik-merc2wgs84-reprojection-render.png differ
diff --git a/test/python_tests/images/support/mapnik-palette-test.png b/test/python_tests/images/support/mapnik-palette-test.png
new file mode 100644
index 0000000..94ae976
Binary files /dev/null and b/test/python_tests/images/support/mapnik-palette-test.png differ
diff --git a/test/python_tests/images/support/mapnik-python-circle-render1.png b/test/python_tests/images/support/mapnik-python-circle-render1.png
new file mode 100644
index 0000000..cb5eba4
Binary files /dev/null and b/test/python_tests/images/support/mapnik-python-circle-render1.png differ
diff --git a/test/python_tests/images/support/mapnik-python-point-render1.png b/test/python_tests/images/support/mapnik-python-point-render1.png
new file mode 100644
index 0000000..b131d86
Binary files /dev/null and b/test/python_tests/images/support/mapnik-python-point-render1.png differ
diff --git a/test/python_tests/images/support/mapnik-style-level-opacity.png b/test/python_tests/images/support/mapnik-style-level-opacity.png
new file mode 100644
index 0000000..91c8d47
Binary files /dev/null and b/test/python_tests/images/support/mapnik-style-level-opacity.png differ
diff --git a/test/python_tests/images/support/mapnik-wgs842merc-reprojection-render.png b/test/python_tests/images/support/mapnik-wgs842merc-reprojection-render.png
new file mode 100644
index 0000000..d0afcd2
Binary files /dev/null and b/test/python_tests/images/support/mapnik-wgs842merc-reprojection-render.png differ
diff --git a/test/python_tests/images/support/marker-in-center-not-placed.png b/test/python_tests/images/support/marker-in-center-not-placed.png
new file mode 100644
index 0000000..a4c4de3
Binary files /dev/null and b/test/python_tests/images/support/marker-in-center-not-placed.png differ
diff --git a/test/python_tests/images/support/marker-in-center.png b/test/python_tests/images/support/marker-in-center.png
new file mode 100644
index 0000000..7845d4f
Binary files /dev/null and b/test/python_tests/images/support/marker-in-center.png differ
diff --git a/test/python_tests/images/support/marker-text-line-scale-factor-0.005.png b/test/python_tests/images/support/marker-text-line-scale-factor-0.005.png
new file mode 100644
index 0000000..5ba0afc
Binary files /dev/null and b/test/python_tests/images/support/marker-text-line-scale-factor-0.005.png differ
diff --git a/test/python_tests/images/support/marker-text-line-scale-factor-0.1.png b/test/python_tests/images/support/marker-text-line-scale-factor-0.1.png
new file mode 100644
index 0000000..306424d
Binary files /dev/null and b/test/python_tests/images/support/marker-text-line-scale-factor-0.1.png differ
diff --git a/test/python_tests/images/support/marker-text-line-scale-factor-0.899.png b/test/python_tests/images/support/marker-text-line-scale-factor-0.899.png
new file mode 100644
index 0000000..cb2c651
Binary files /dev/null and b/test/python_tests/images/support/marker-text-line-scale-factor-0.899.png differ
diff --git a/test/python_tests/images/support/marker-text-line-scale-factor-1.5.png b/test/python_tests/images/support/marker-text-line-scale-factor-1.5.png
new file mode 100644
index 0000000..a1b34a3
Binary files /dev/null and b/test/python_tests/images/support/marker-text-line-scale-factor-1.5.png differ
diff --git a/test/python_tests/images/support/marker-text-line-scale-factor-1.png b/test/python_tests/images/support/marker-text-line-scale-factor-1.png
new file mode 100644
index 0000000..c86c5fa
Binary files /dev/null and b/test/python_tests/images/support/marker-text-line-scale-factor-1.png differ
diff --git a/test/python_tests/images/support/marker-text-line-scale-factor-10.png b/test/python_tests/images/support/marker-text-line-scale-factor-10.png
new file mode 100644
index 0000000..8a7842f
Binary files /dev/null and b/test/python_tests/images/support/marker-text-line-scale-factor-10.png differ
diff --git a/test/python_tests/images/support/marker-text-line-scale-factor-100.png b/test/python_tests/images/support/marker-text-line-scale-factor-100.png
new file mode 100644
index 0000000..f1c0b52
Binary files /dev/null and b/test/python_tests/images/support/marker-text-line-scale-factor-100.png differ
diff --git a/test/python_tests/images/support/marker-text-line-scale-factor-1e-05.png b/test/python_tests/images/support/marker-text-line-scale-factor-1e-05.png
new file mode 100644
index 0000000..0f36830
Binary files /dev/null and b/test/python_tests/images/support/marker-text-line-scale-factor-1e-05.png differ
diff --git a/test/python_tests/images/support/marker-text-line-scale-factor-2.png b/test/python_tests/images/support/marker-text-line-scale-factor-2.png
new file mode 100644
index 0000000..e3ad67f
Binary files /dev/null and b/test/python_tests/images/support/marker-text-line-scale-factor-2.png differ
diff --git a/test/python_tests/images/support/marker-text-line-scale-factor-5.png b/test/python_tests/images/support/marker-text-line-scale-factor-5.png
new file mode 100644
index 0000000..2be5f2d
Binary files /dev/null and b/test/python_tests/images/support/marker-text-line-scale-factor-5.png differ
diff --git a/test/python_tests/images/support/pgraster/data_subquery-data_16bsi_subquery-16BSI-135.png b/test/python_tests/images/support/pgraster/data_subquery-data_16bsi_subquery-16BSI-135.png
new file mode 100644
index 0000000..e6fad0d
Binary files /dev/null and b/test/python_tests/images/support/pgraster/data_subquery-data_16bsi_subquery-16BSI-135.png differ
diff --git a/test/python_tests/images/support/pgraster/data_subquery-data_16bui_subquery-16BUI-126.png b/test/python_tests/images/support/pgraster/data_subquery-data_16bui_subquery-16BUI-126.png
new file mode 100644
index 0000000..e6fad0d
Binary files /dev/null and b/test/python_tests/images/support/pgraster/data_subquery-data_16bui_subquery-16BUI-126.png differ
diff --git a/test/python_tests/images/support/pgraster/data_subquery-data_2bui_subquery-2BUI-3.png b/test/python_tests/images/support/pgraster/data_subquery-data_2bui_subquery-2BUI-3.png
new file mode 100644
index 0000000..e6fad0d
Binary files /dev/null and b/test/python_tests/images/support/pgraster/data_subquery-data_2bui_subquery-2BUI-3.png differ
diff --git a/test/python_tests/images/support/pgraster/data_subquery-data_32bf_subquery-32BF-450.png b/test/python_tests/images/support/pgraster/data_subquery-data_32bf_subquery-32BF-450.png
new file mode 100644
index 0000000..e6fad0d
Binary files /dev/null and b/test/python_tests/images/support/pgraster/data_subquery-data_32bf_subquery-32BF-450.png differ
diff --git a/test/python_tests/images/support/pgraster/data_subquery-data_32bsi_subquery-32BSI-264.png b/test/python_tests/images/support/pgraster/data_subquery-data_32bsi_subquery-32BSI-264.png
new file mode 100644
index 0000000..e6fad0d
Binary files /dev/null and b/test/python_tests/images/support/pgraster/data_subquery-data_32bsi_subquery-32BSI-264.png differ
diff --git a/test/python_tests/images/support/pgraster/data_subquery-data_32bui_subquery-32BUI-255.png b/test/python_tests/images/support/pgraster/data_subquery-data_32bui_subquery-32BUI-255.png
new file mode 100644
index 0000000..e6fad0d
Binary files /dev/null and b/test/python_tests/images/support/pgraster/data_subquery-data_32bui_subquery-32BUI-255.png differ
diff --git a/test/python_tests/images/support/pgraster/data_subquery-data_4bui_subquery-4BUI-15.png b/test/python_tests/images/support/pgraster/data_subquery-data_4bui_subquery-4BUI-15.png
new file mode 100644
index 0000000..e6fad0d
Binary files /dev/null and b/test/python_tests/images/support/pgraster/data_subquery-data_4bui_subquery-4BUI-15.png differ
diff --git a/test/python_tests/images/support/pgraster/data_subquery-data_64bf_subquery-64BF-3072.png b/test/python_tests/images/support/pgraster/data_subquery-data_64bf_subquery-64BF-3072.png
new file mode 100644
index 0000000..e6fad0d
Binary files /dev/null and b/test/python_tests/images/support/pgraster/data_subquery-data_64bf_subquery-64BF-3072.png differ
diff --git a/test/python_tests/images/support/pgraster/data_subquery-data_8bsi_subquery-8BSI-69.png b/test/python_tests/images/support/pgraster/data_subquery-data_8bsi_subquery-8BSI-69.png
new file mode 100644
index 0000000..e6fad0d
Binary files /dev/null and b/test/python_tests/images/support/pgraster/data_subquery-data_8bsi_subquery-8BSI-69.png differ
diff --git a/test/python_tests/images/support/pgraster/data_subquery-data_8bui_subquery-8BUI-63.png b/test/python_tests/images/support/pgraster/data_subquery-data_8bui_subquery-8BUI-63.png
new file mode 100644
index 0000000..e6fad0d
Binary files /dev/null and b/test/python_tests/images/support/pgraster/data_subquery-data_8bui_subquery-8BUI-63.png differ
diff --git a/test/python_tests/images/support/pgraster/grayscale_subquery-grayscale_16bsi_subquery-16BSI-144.png b/test/python_tests/images/support/pgraster/grayscale_subquery-grayscale_16bsi_subquery-16BSI-144.png
new file mode 100644
index 0000000..719c7e0
Binary files /dev/null and b/test/python_tests/images/support/pgraster/grayscale_subquery-grayscale_16bsi_subquery-16BSI-144.png differ
diff --git a/test/python_tests/images/support/pgraster/grayscale_subquery-grayscale_16bui_subquery-16BUI-126.png b/test/python_tests/images/support/pgraster/grayscale_subquery-grayscale_16bui_subquery-16BUI-126.png
new file mode 100644
index 0000000..a6aa1a6
Binary files /dev/null and b/test/python_tests/images/support/pgraster/grayscale_subquery-grayscale_16bui_subquery-16BUI-126.png differ
diff --git a/test/python_tests/images/support/pgraster/grayscale_subquery-grayscale_2bui_subquery-2BUI-3.png b/test/python_tests/images/support/pgraster/grayscale_subquery-grayscale_2bui_subquery-2BUI-3.png
new file mode 100644
index 0000000..62aa163
Binary files /dev/null and b/test/python_tests/images/support/pgraster/grayscale_subquery-grayscale_2bui_subquery-2BUI-3.png differ
diff --git a/test/python_tests/images/support/pgraster/grayscale_subquery-grayscale_32bsi_subquery-32BSI-129.png b/test/python_tests/images/support/pgraster/grayscale_subquery-grayscale_32bsi_subquery-32BSI-129.png
new file mode 100644
index 0000000..b134b2d
Binary files /dev/null and b/test/python_tests/images/support/pgraster/grayscale_subquery-grayscale_32bsi_subquery-32BSI-129.png differ
diff --git a/test/python_tests/images/support/pgraster/grayscale_subquery-grayscale_32bui_subquery-32BUI-255.png b/test/python_tests/images/support/pgraster/grayscale_subquery-grayscale_32bui_subquery-32BUI-255.png
new file mode 100644
index 0000000..5f8035a
Binary files /dev/null and b/test/python_tests/images/support/pgraster/grayscale_subquery-grayscale_32bui_subquery-32BUI-255.png differ
diff --git a/test/python_tests/images/support/pgraster/grayscale_subquery-grayscale_4bui_subquery-4BUI-15.png b/test/python_tests/images/support/pgraster/grayscale_subquery-grayscale_4bui_subquery-4BUI-15.png
new file mode 100644
index 0000000..2667c06
Binary files /dev/null and b/test/python_tests/images/support/pgraster/grayscale_subquery-grayscale_4bui_subquery-4BUI-15.png differ
diff --git a/test/python_tests/images/support/pgraster/grayscale_subquery-grayscale_8bsi_subquery-8BSI-69.png b/test/python_tests/images/support/pgraster/grayscale_subquery-grayscale_8bsi_subquery-8BSI-69.png
new file mode 100644
index 0000000..85abadd
Binary files /dev/null and b/test/python_tests/images/support/pgraster/grayscale_subquery-grayscale_8bsi_subquery-8BSI-69.png differ
diff --git a/test/python_tests/images/support/pgraster/grayscale_subquery-grayscale_8bui_subquery-8BUI-63.png b/test/python_tests/images/support/pgraster/grayscale_subquery-grayscale_8bui_subquery-8BUI-63.png
new file mode 100644
index 0000000..06d6249
Binary files /dev/null and b/test/python_tests/images/support/pgraster/grayscale_subquery-grayscale_8bui_subquery-8BUI-63.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-nodataedge-rgb_8bui C T_64x64 Cl--1-box1.png b/test/python_tests/images/support/pgraster/rgba_8bui-nodataedge-rgb_8bui C T_64x64 Cl--1-box1.png
new file mode 100644
index 0000000..cae6205
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-nodataedge-rgb_8bui C T_64x64 Cl--1-box1.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-nodataedge-rgb_8bui C T_64x64 Cl--1-box2.png b/test/python_tests/images/support/pgraster/rgba_8bui-nodataedge-rgb_8bui C T_64x64 Cl--1-box2.png
new file mode 100644
index 0000000..846981b
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-nodataedge-rgb_8bui C T_64x64 Cl--1-box2.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-nodataedge-rgb_8bui C T_64x64 Sc Cl--1-box1.png b/test/python_tests/images/support/pgraster/rgba_8bui-nodataedge-rgb_8bui C T_64x64 Sc Cl--1-box1.png
new file mode 100644
index 0000000..4fdf9ff
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-nodataedge-rgb_8bui C T_64x64 Sc Cl--1-box1.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-nodataedge-rgb_8bui C T_64x64 Sc Cl--1-box2.png b/test/python_tests/images/support/pgraster/rgba_8bui-nodataedge-rgb_8bui C T_64x64 Sc Cl--1-box2.png
new file mode 100644
index 0000000..846981b
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-nodataedge-rgb_8bui C T_64x64 Sc Cl--1-box2.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-nodataedge-rgb_8bui C T_64x64 Sc--0-box1.png b/test/python_tests/images/support/pgraster/rgba_8bui-nodataedge-rgb_8bui C T_64x64 Sc--0-box1.png
new file mode 100644
index 0000000..4fdf9ff
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-nodataedge-rgb_8bui C T_64x64 Sc--0-box1.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-nodataedge-rgb_8bui C T_64x64 Sc--0-box2.png b/test/python_tests/images/support/pgraster/rgba_8bui-nodataedge-rgb_8bui C T_64x64 Sc--0-box2.png
new file mode 100644
index 0000000..846981b
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-nodataedge-rgb_8bui C T_64x64 Sc--0-box2.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-nodataedge-rgb_8bui C T_64x64--0-box1.png b/test/python_tests/images/support/pgraster/rgba_8bui-nodataedge-rgb_8bui C T_64x64--0-box1.png
new file mode 100644
index 0000000..cae6205
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-nodataedge-rgb_8bui C T_64x64--0-box1.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-nodataedge-rgb_8bui C T_64x64--0-box2.png b/test/python_tests/images/support/pgraster/rgba_8bui-nodataedge-rgb_8bui C T_64x64--0-box2.png
new file mode 100644
index 0000000..846981b
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-nodataedge-rgb_8bui C T_64x64--0-box2.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C O_2 Cl-2-1-box1.png b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C O_2 Cl-2-1-box1.png
new file mode 100644
index 0000000..981cf74
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C O_2 Cl-2-1-box1.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C O_2 Cl-2-1-box2.png b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C O_2 Cl-2-1-box2.png
new file mode 100644
index 0000000..0669f52
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C O_2 Cl-2-1-box2.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C O_2 Sc Cl-2-1-box1.png b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C O_2 Sc Cl-2-1-box1.png
new file mode 100644
index 0000000..981cf74
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C O_2 Sc Cl-2-1-box1.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C O_2 Sc Cl-2-1-box2.png b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C O_2 Sc Cl-2-1-box2.png
new file mode 100644
index 0000000..0669f52
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C O_2 Sc Cl-2-1-box2.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C O_2 Sc-2-0-box1.png b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C O_2 Sc-2-0-box1.png
new file mode 100644
index 0000000..981cf74
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C O_2 Sc-2-0-box1.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C O_2 Sc-2-0-box2.png b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C O_2 Sc-2-0-box2.png
new file mode 100644
index 0000000..0413518
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C O_2 Sc-2-0-box2.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C O_2-2-0-box1.png b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C O_2-2-0-box1.png
new file mode 100644
index 0000000..981cf74
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C O_2-2-0-box1.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C O_2-2-0-box2.png b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C O_2-2-0-box2.png
new file mode 100644
index 0000000..0413518
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C O_2-2-0-box2.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C T_16x16 O_2 Cl-2-1-box1.png b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C T_16x16 O_2 Cl-2-1-box1.png
new file mode 100644
index 0000000..981cf74
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C T_16x16 O_2 Cl-2-1-box1.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C T_16x16 O_2 Cl-2-1-box2.png b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C T_16x16 O_2 Cl-2-1-box2.png
new file mode 100644
index 0000000..62e35be
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C T_16x16 O_2 Cl-2-1-box2.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C T_16x16 O_2 Sc Cl-2-1-box1.png b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C T_16x16 O_2 Sc Cl-2-1-box1.png
new file mode 100644
index 0000000..981cf74
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C T_16x16 O_2 Sc Cl-2-1-box1.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C T_16x16 O_2 Sc Cl-2-1-box2.png b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C T_16x16 O_2 Sc Cl-2-1-box2.png
new file mode 100644
index 0000000..62e35be
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C T_16x16 O_2 Sc Cl-2-1-box2.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C T_16x16 O_2 Sc-2-0-box1.png b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C T_16x16 O_2 Sc-2-0-box1.png
new file mode 100644
index 0000000..981cf74
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C T_16x16 O_2 Sc-2-0-box1.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C T_16x16 O_2 Sc-2-0-box2.png b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C T_16x16 O_2 Sc-2-0-box2.png
new file mode 100644
index 0000000..5460a38
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C T_16x16 O_2 Sc-2-0-box2.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C T_16x16 O_2-2-0-box1.png b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C T_16x16 O_2-2-0-box1.png
new file mode 100644
index 0000000..981cf74
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C T_16x16 O_2-2-0-box1.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C T_16x16 O_2-2-0-box2.png b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C T_16x16 O_2-2-0-box2.png
new file mode 100644
index 0000000..5460a38
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui C T_16x16 O_2-2-0-box2.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui O_2 Cl-2-1-box1.png b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui O_2 Cl-2-1-box1.png
new file mode 100644
index 0000000..981cf74
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui O_2 Cl-2-1-box1.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui O_2 Cl-2-1-box2.png b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui O_2 Cl-2-1-box2.png
new file mode 100644
index 0000000..0669f52
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui O_2 Cl-2-1-box2.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui O_2 Sc Cl-2-1-box1.png b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui O_2 Sc Cl-2-1-box1.png
new file mode 100644
index 0000000..981cf74
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui O_2 Sc Cl-2-1-box1.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui O_2 Sc Cl-2-1-box2.png b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui O_2 Sc Cl-2-1-box2.png
new file mode 100644
index 0000000..0669f52
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui O_2 Sc Cl-2-1-box2.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui O_2 Sc-2-0-box1.png b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui O_2 Sc-2-0-box1.png
new file mode 100644
index 0000000..981cf74
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui O_2 Sc-2-0-box1.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui O_2 Sc-2-0-box2.png b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui O_2 Sc-2-0-box2.png
new file mode 100644
index 0000000..0413518
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui O_2 Sc-2-0-box2.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui O_2-2-0-box1.png b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui O_2-2-0-box1.png
new file mode 100644
index 0000000..981cf74
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui O_2-2-0-box1.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui O_2-2-0-box2.png b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui O_2-2-0-box2.png
new file mode 100644
index 0000000..0413518
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui O_2-2-0-box2.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui T_16x16 O_2 Cl-2-1-box1.png b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui T_16x16 O_2 Cl-2-1-box1.png
new file mode 100644
index 0000000..981cf74
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui T_16x16 O_2 Cl-2-1-box1.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui T_16x16 O_2 Cl-2-1-box2.png b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui T_16x16 O_2 Cl-2-1-box2.png
new file mode 100644
index 0000000..62e35be
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui T_16x16 O_2 Cl-2-1-box2.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui T_16x16 O_2 Sc Cl-2-1-box1.png b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui T_16x16 O_2 Sc Cl-2-1-box1.png
new file mode 100644
index 0000000..981cf74
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui T_16x16 O_2 Sc Cl-2-1-box1.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui T_16x16 O_2 Sc Cl-2-1-box2.png b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui T_16x16 O_2 Sc Cl-2-1-box2.png
new file mode 100644
index 0000000..62e35be
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui T_16x16 O_2 Sc Cl-2-1-box2.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui T_16x16 O_2 Sc-2-0-box1.png b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui T_16x16 O_2 Sc-2-0-box1.png
new file mode 100644
index 0000000..981cf74
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui T_16x16 O_2 Sc-2-0-box1.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui T_16x16 O_2 Sc-2-0-box2.png b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui T_16x16 O_2 Sc-2-0-box2.png
new file mode 100644
index 0000000..5460a38
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui T_16x16 O_2 Sc-2-0-box2.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui T_16x16 O_2-2-0-box1.png b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui T_16x16 O_2-2-0-box1.png
new file mode 100644
index 0000000..981cf74
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui T_16x16 O_2-2-0-box1.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui T_16x16 O_2-2-0-box2.png b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui T_16x16 O_2-2-0-box2.png
new file mode 100644
index 0000000..5460a38
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_8bui-rgba_8bui T_16x16 O_2-2-0-box2.png differ
diff --git a/test/python_tests/images/support/pgraster/rgba_subquery-rgba_8bui_subquery-8BUI-255-0-0-255-255-255.png b/test/python_tests/images/support/pgraster/rgba_subquery-rgba_8bui_subquery-8BUI-255-0-0-255-255-255.png
new file mode 100644
index 0000000..d83016d
Binary files /dev/null and b/test/python_tests/images/support/pgraster/rgba_subquery-rgba_8bui_subquery-8BUI-255-0-0-255-255-255.png differ
diff --git a/test/python_tests/images/support/raster-alpha.png b/test/python_tests/images/support/raster-alpha.png
new file mode 100644
index 0000000..3c79bb3
Binary files /dev/null and b/test/python_tests/images/support/raster-alpha.png differ
diff --git a/test/python_tests/images/support/raster_warping.png b/test/python_tests/images/support/raster_warping.png
new file mode 100644
index 0000000..7a6dea7
Binary files /dev/null and b/test/python_tests/images/support/raster_warping.png differ
diff --git a/test/python_tests/images/support/raster_warping_does_not_overclip_source.png b/test/python_tests/images/support/raster_warping_does_not_overclip_source.png
new file mode 100644
index 0000000..0d6ccc5
Binary files /dev/null and b/test/python_tests/images/support/raster_warping_does_not_overclip_source.png differ
diff --git a/test/python_tests/images/support/spacing.png b/test/python_tests/images/support/spacing.png
new file mode 100644
index 0000000..8d6bb40
Binary files /dev/null and b/test/python_tests/images/support/spacing.png differ
diff --git a/test/python_tests/images/support/transparency/aerial_rgb.png b/test/python_tests/images/support/transparency/aerial_rgb.png
new file mode 100644
index 0000000..16481a4
Binary files /dev/null and b/test/python_tests/images/support/transparency/aerial_rgb.png differ
diff --git a/test/python_tests/images/support/transparency/aerial_rgba.png b/test/python_tests/images/support/transparency/aerial_rgba.png
new file mode 100644
index 0000000..0f15a35
Binary files /dev/null and b/test/python_tests/images/support/transparency/aerial_rgba.png differ
diff --git a/test/python_tests/images/support/transparency/white0.png b/test/python_tests/images/support/transparency/white0.png
new file mode 100644
index 0000000..955861a
Binary files /dev/null and b/test/python_tests/images/support/transparency/white0.png differ
diff --git a/test/python_tests/images/support/transparency/white0.webp b/test/python_tests/images/support/transparency/white0.webp
new file mode 100644
index 0000000..f276b81
Binary files /dev/null and b/test/python_tests/images/support/transparency/white0.webp differ
diff --git a/test/python_tests/images/support/transparency/white1.png b/test/python_tests/images/support/transparency/white1.png
new file mode 100644
index 0000000..db4e827
Binary files /dev/null and b/test/python_tests/images/support/transparency/white1.png differ
diff --git a/test/python_tests/images/support/transparency/white2.png b/test/python_tests/images/support/transparency/white2.png
new file mode 100644
index 0000000..136e098
Binary files /dev/null and b/test/python_tests/images/support/transparency/white2.png differ
diff --git a/test/python_tests/introspection_test.py b/test/python_tests/introspection_test.py
new file mode 100644
index 0000000..afb1cc2
--- /dev/null
+++ b/test/python_tests/introspection_test.py
@@ -0,0 +1,61 @@
+#!/usr/bin/env python
+
+import os
+from nose.tools import eq_
+from utilities import execution_path, run_all
+
+import mapnik
+
+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_introspect_symbolizers():
+ # create a symbolizer
+ p = mapnik.PointSymbolizer()
+ p.file = "../data/images/dummy.png"
+ p.allow_overlap = True
+ p.opacity = 0.5
+
+ eq_(p.allow_overlap, True)
+ eq_(p.opacity, 0.5)
+ eq_(p.filename,'../data/images/dummy.png')
+
+ # make sure the defaults
+ # are what we think they are
+ eq_(p.allow_overlap, True)
+ eq_(p.opacity,0.5)
+ eq_(p.filename,'../data/images/dummy.png')
+
+ # contruct objects to hold it
+ r = mapnik.Rule()
+ r.symbols.append(p)
+ s = mapnik.Style()
+ s.rules.append(r)
+ m = mapnik.Map(0,0)
+ m.append_style('s',s)
+
+ # try to figure out what is
+ # in the map and make sure
+ # style is there and the same
+
+ s2 = m.find_style('s')
+ rules = s2.rules
+ eq_(len(rules),1)
+ r2 = rules[0]
+ syms = r2.symbols
+ eq_(len(syms),1)
+
+ ## TODO here, we can do...
+ sym = syms[0]
+ p2 = sym.extract()
+ assert isinstance(p2,mapnik.PointSymbolizer)
+
+ eq_(p2.allow_overlap, True)
+ eq_(p2.opacity, 0.5)
+ eq_(p2.filename,'../data/images/dummy.png')
+
+if __name__ == "__main__":
+ setup()
+ exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/json_feature_properties_test.py b/test/python_tests/json_feature_properties_test.py
new file mode 100644
index 0000000..47f2428
--- /dev/null
+++ b/test/python_tests/json_feature_properties_test.py
@@ -0,0 +1,102 @@
+#encoding: utf8
+
+from nose.tools import eq_
+import mapnik
+from utilities import run_all
+try:
+ import json
+except ImportError:
+ import simplejson as json
+
+chars = [
+ {
+ "name":"single_quote",
+ "test": "string with ' quote",
+ "json": '{"type":"Feature","id":1,"geometry":null,"properties":{"name":"string with \' quote"}}'
+ },
+ {
+ "name":"escaped_single_quote",
+ "test":"string with \' quote",
+ "json":'{"type":"Feature","id":1,"geometry":null,"properties":{"name":"string with \' quote"}}'
+ },
+ {
+ "name":"double_quote",
+ "test":'string with " quote',
+ "json":'{"type":"Feature","id":1,"geometry":null,"properties":{"name":"string with \\" quote"}}'
+ },
+ {
+ "name":"double_quote2",
+ "test":"string with \" quote",
+ "json":'{"type":"Feature","id":1,"geometry":null,"properties":{"name":"string with \\" quote"}}'
+ },
+ {
+ "name":"reverse_solidus", # backslash
+ "test":"string with \\ quote",
+ "json":'{"type":"Feature","id":1,"geometry":null,"properties":{"name":"string with \\\ quote"}}'
+ },
+ {
+ "name":"solidus", # forward slash
+ "test":"string with / quote",
+ "json":'{"type":"Feature","id":1,"geometry":null,"properties":{"name":"string with / quote"}}'
+ },
+ {
+ "name":"backspace",
+ "test":"string with \b quote",
+ "json":'{"type":"Feature","id":1,"geometry":null,"properties":{"name":"string with \\b quote"}}'
+ },
+ {
+ "name":"formfeed",
+ "test":"string with \f quote",
+ "json":'{"type":"Feature","id":1,"geometry":null,"properties":{"name":"string with \\f quote"}}'
+ },
+ {
+ "name":"newline",
+ "test":"string with \n quote",
+ "json":'{"type":"Feature","id":1,"geometry":null,"properties":{"name":"string with \\n quote"}}'
+ },
+ {
+ "name":"carriage_return",
+ "test":"string with \r quote",
+ "json":'{"type":"Feature","id":1,"geometry":null,"properties":{"name":"string with \\r quote"}}'
+ },
+ {
+ "name":"horiztonal_tab",
+ "test":"string with \t quote",
+ "json":'{"type":"Feature","id":1,"geometry":null,"properties":{"name":"string with \\t quote"}}'
+ },
+ # remainder are c++ reserved, but not json
+ {
+ "name":"vert_tab",
+ "test":"string with \v quote",
+ "json":'{"type":"Feature","id":1,"geometry":null,"properties":{"name":"string with \\u000b quote"}}'
+ },
+ {
+ "name":"alert",
+ "test":"string with \a quote",
+ "json":'{"type":"Feature","id":1,"geometry":null,"properties":{"name":"string with \u0007 quote"}}'
+ }
+]
+
+ctx = mapnik.Context()
+ctx.push('name')
+
+def test_char_escaping():
+ for char in chars:
+ feat = mapnik.Feature(ctx,1)
+ expected = char['test']
+ feat["name"] = expected
+ eq_(feat["name"],expected)
+ # confirm the python json module
+ # is working as we would expect
+ pyjson2 = json.loads(char['json'])
+ eq_(pyjson2['properties']['name'],expected)
+ # confirm our behavior is the same as python json module
+ # for the original string
+ geojson_feat_string = feat.to_geojson()
+ eq_(geojson_feat_string,char['json'],"Mapnik's json escaping is not to spec: actual(%s) and expected(%s) for %s" % (geojson_feat_string,char['json'],char['name']))
+ # and the round tripped string
+ pyjson = json.loads(geojson_feat_string)
+ eq_(pyjson['properties']['name'],expected)
+
+if __name__ == "__main__":
+ exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/layer_buffer_size_test.py b/test/python_tests/layer_buffer_size_test.py
new file mode 100644
index 0000000..83765a7
--- /dev/null
+++ b/test/python_tests/layer_buffer_size_test.py
@@ -0,0 +1,35 @@
+#coding=utf8
+import os
+import mapnik
+from utilities import execution_path, run_all
+from nose.tools import eq_
+
+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('.'))
+
+if 'sqlite' in mapnik.DatasourceCache.plugin_names():
+
+ # the negative buffer on the layer should
+ # override the postive map buffer leading
+ # only one point to be rendered in the map
+ def test_layer_buffer_size_1():
+ m = mapnik.Map(512,512)
+ eq_(m.buffer_size,0)
+ mapnik.load_map(m,'../data/good_maps/layer_buffer_size_reduction.xml')
+ eq_(m.buffer_size,256)
+ eq_(m.layers[0].buffer_size,-150)
+ m.zoom_all()
+ im = mapnik.Image(m.width,m.height)
+ mapnik.render(m,im)
+ actual = '/tmp/mapnik-layer-buffer-size.png'
+ expected = 'images/support/mapnik-layer-buffer-size.png'
+ im.save(actual,"png32")
+ expected_im = mapnik.Image.open(expected)
+ eq_(im.tostring('png32'),expected_im.tostring('png32'), 'failed comparing actual (%s) and expected (%s)' % (actual,'tests/python_tests/'+ expected))
+
+
+if __name__ == "__main__":
+ setup()
+ exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/layer_modification_test.py b/test/python_tests/layer_modification_test.py
new file mode 100644
index 0000000..7517ac2
--- /dev/null
+++ b/test/python_tests/layer_modification_test.py
@@ -0,0 +1,75 @@
+#!/usr/bin/env python
+
+import os
+from nose.tools import eq_
+from utilities import execution_path, run_all
+import mapnik
+
+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_adding_datasource_to_layer():
+ map_string = '''<?xml version="1.0" encoding="utf-8"?>
+<Map>
+
+ <Layer name="world_borders">
+ <StyleName>world_borders_style</StyleName>
+ <StyleName>point_style</StyleName>
+ <!-- leave datasource empty -->
+ <!--
+ <Datasource>
+ <Parameter name="file">../data/shp/world_merc.shp</Parameter>
+ <Parameter name="type">shape</Parameter>
+ </Datasource>
+ -->
+ </Layer>
+
+</Map>
+'''
+ m = mapnik.Map(256, 256)
+
+ try:
+ mapnik.load_map_from_string(m, map_string)
+
+ # validate it loaded fine
+ eq_(m.layers[0].styles[0],'world_borders_style')
+ eq_(m.layers[0].styles[1],'point_style')
+ eq_(len(m.layers),1)
+
+ # also assign a variable reference to that layer
+ # below we will test that this variable references
+ # the same object that is attached to the map
+ lyr = m.layers[0]
+
+ # ensure that there was no datasource for the layer...
+ eq_(m.layers[0].datasource,None)
+ eq_(lyr.datasource,None)
+
+ # also note that since the srs was black it defaulted to wgs84
+ eq_(m.layers[0].srs,'+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs')
+ eq_(lyr.srs,'+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs')
+
+ # now add a datasource one...
+ ds = mapnik.Shapefile(file='../data/shp/world_merc.shp')
+ m.layers[0].datasource = ds
+
+ # now ensure it is attached
+ eq_(m.layers[0].datasource.describe()['name'],"shape")
+ eq_(lyr.datasource.describe()['name'],"shape")
+
+ # and since we have now added a shapefile in spherical mercator, adjust the projection
+ lyr.srs = '+proj=merc +lon_0=0 +lat_ts=0 +x_0=0 +y_0=0 +ellps=WGS84 +datum=WGS84 +units=m +no_defs'
+
+ # test that assignment
+ eq_(m.layers[0].srs,'+proj=merc +lon_0=0 +lat_ts=0 +x_0=0 +y_0=0 +ellps=WGS84 +datum=WGS84 +units=m +no_defs')
+ eq_(lyr.srs,'+proj=merc +lon_0=0 +lat_ts=0 +x_0=0 +y_0=0 +ellps=WGS84 +datum=WGS84 +units=m +no_defs')
+ except RuntimeError, e:
+ # only test datasources that we have installed
+ if not 'Could not create datasource' in str(e):
+ raise RuntimeError(e)
+
+if __name__ == "__main__":
+ setup()
+ exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/layer_test.py b/test/python_tests/layer_test.py
new file mode 100644
index 0000000..00ea434
--- /dev/null
+++ b/test/python_tests/layer_test.py
@@ -0,0 +1,28 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from nose.tools import eq_
+from utilities import run_all
+import mapnik
+
+# Map initialization
+def test_layer_init():
+ l = mapnik.Layer('test')
+ eq_(l.name,'test')
+ eq_(l.srs,'+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs')
+ eq_(l.envelope(),mapnik.Box2d())
+ eq_(l.clear_label_cache,False)
+ eq_(l.cache_features,False)
+ eq_(l.visible(1),True)
+ eq_(l.active,True)
+ eq_(l.datasource,None)
+ eq_(l.queryable,False)
+ eq_(l.minimum_scale_denominator,0.0)
+ eq_(l.maximum_scale_denominator > 1e+6,True)
+ eq_(l.group_by,"")
+ eq_(l.maximum_extent,None)
+ eq_(l.buffer_size,None)
+ eq_(len(l.styles),0)
+
+if __name__ == "__main__":
+ exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/load_map_test.py b/test/python_tests/load_map_test.py
new file mode 100644
index 0000000..5eb211e
--- /dev/null
+++ b/test/python_tests/load_map_test.py
@@ -0,0 +1,82 @@
+#!/usr/bin/env python
+
+from nose.tools import eq_
+from utilities import execution_path, run_all
+
+import os, glob, mapnik
+
+default_logging_severity = mapnik.logger.get_severity()
+
+def setup():
+ # make the tests silent to suppress unsupported params from harfbuzz tests
+ # TODO: remove this after harfbuzz branch merges
+ mapnik.logger.set_severity(mapnik.severity_type.None)
+ # All of the paths used are relative, if we run the tests
+ # from another directory we need to chdir()
+ os.chdir(execution_path('.'))
+
+def teardown():
+ mapnik.logger.set_severity(default_logging_severity)
+
+def test_broken_files():
+ default_logging_severity = mapnik.logger.get_severity()
+ mapnik.logger.set_severity(mapnik.severity_type.None)
+ broken_files = glob.glob("../data/broken_maps/*.xml")
+ # Add a filename that doesn't exist
+ broken_files.append("../data/broken/does_not_exist.xml")
+
+ failures = [];
+ for filename in broken_files:
+ try:
+ m = mapnik.Map(512, 512)
+ strict = True
+ mapnik.load_map(m, filename, strict)
+ failures.append('Loading broken map (%s) did not raise RuntimeError!' % filename)
+ except RuntimeError:
+ pass
+ eq_(len(failures),0,'\n'+'\n'.join(failures))
+ mapnik.logger.set_severity(default_logging_severity)
+
+def test_can_parse_xml_with_deprecated_properties():
+ default_logging_severity = mapnik.logger.get_severity()
+ mapnik.logger.set_severity(mapnik.severity_type.None)
+ files_with_deprecated_props = glob.glob("../data/deprecated_maps/*.xml")
+
+ failures = [];
+ for filename in files_with_deprecated_props:
+ try:
+ m = mapnik.Map(512, 512)
+ strict = True
+ mapnik.load_map(m, filename, strict)
+ base_path = os.path.dirname(filename)
+ mapnik.load_map_from_string(m,open(filename,'rb').read(),strict,base_path)
+ except RuntimeError, e:
+ # only test datasources that we have installed
+ if not 'Could not create datasource' in str(e) \
+ and not 'could not connect' in str(e):
+ failures.append('Failed to load valid map %s (%s)' % (filename,e))
+ eq_(len(failures),0,'\n'+'\n'.join(failures))
+ mapnik.logger.set_severity(default_logging_severity)
+
+def test_good_files():
+ good_files = glob.glob("../data/good_maps/*.xml")
+ good_files.extend(glob.glob("../visual_tests/styles/*.xml"))
+
+ failures = [];
+ for filename in good_files:
+ try:
+ m = mapnik.Map(512, 512)
+ strict = True
+ mapnik.load_map(m, filename, strict)
+ base_path = os.path.dirname(filename)
+ mapnik.load_map_from_string(m,open(filename,'rb').read(),strict,base_path)
+ except RuntimeError, e:
+ # only test datasources that we have installed
+ if not 'Could not create datasource' in str(e) \
+ and not 'could not connect' in str(e):
+ failures.append('Failed to load valid map %s (%s)' % (filename,e))
+ eq_(len(failures),0,'\n'+'\n'.join(failures))
+
+if __name__ == "__main__":
+ setup()
+ exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/map_query_test.py b/test/python_tests/map_query_test.py
new file mode 100644
index 0000000..4035f7a
--- /dev/null
+++ b/test/python_tests/map_query_test.py
@@ -0,0 +1,104 @@
+#!/usr/bin/env python
+
+from nose.tools import eq_,raises,assert_almost_equal
+from utilities import execution_path, run_all
+import os, mapnik
+
+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('.'))
+
+# map has no layers
+@raises(IndexError)
+def test_map_query_throw1():
+ m = mapnik.Map(256,256)
+ m.zoom_to_box(mapnik.Box2d(-1,-1,0,0))
+ m.query_point(0,0,0)
+
+# only positive indexes
+@raises(IndexError)
+def test_map_query_throw2():
+ m = mapnik.Map(256,256)
+ m.query_point(-1,0,0)
+
+# map has never been zoomed (nodata)
+@raises(RuntimeError)
+def test_map_query_throw3():
+ m = mapnik.Map(256,256)
+ m.query_point(0,0,0)
+
+if 'shape' in mapnik.DatasourceCache.plugin_names():
+ # map has never been zoomed (even with data)
+ @raises(RuntimeError)
+ def test_map_query_throw4():
+ m = mapnik.Map(256,256)
+ mapnik.load_map(m,'../data/good_maps/agg_poly_gamma_map.xml')
+ m.query_point(0,0,0)
+
+ # invalid coords in general (do not intersect)
+ @raises(RuntimeError)
+ def test_map_query_throw5():
+ m = mapnik.Map(256,256)
+ mapnik.load_map(m,'../data/good_maps/agg_poly_gamma_map.xml')
+ m.zoom_all()
+ m.query_point(0,9999999999999999,9999999999999999)
+
+ def test_map_query_works1():
+ m = mapnik.Map(256,256)
+ mapnik.load_map(m,'../data/good_maps/wgs842merc_reprojection.xml')
+ merc_bounds = mapnik.Box2d(-20037508.34,-20037508.34,20037508.34,20037508.34)
+ m.maximum_extent = merc_bounds
+ m.zoom_all()
+ fs = m.query_point(0,-11012435.5376, 4599674.6134) # somewhere in kansas
+ feat = fs.next()
+ eq_(feat.attributes['NAME_FORMA'],u'United States of America')
+
+ def test_map_query_works2():
+ m = mapnik.Map(256,256)
+ mapnik.load_map(m,'../data/good_maps/merc2wgs84_reprojection.xml')
+ wgs84_bounds = mapnik.Box2d(-179.999999975,-85.0511287776,179.999999975,85.0511287776)
+ m.maximum_extent = wgs84_bounds
+ # caution - will go square due to evil aspect_fix_mode backhandedness
+ m.zoom_all()
+ #mapnik.render_to_file(m,'works2.png')
+ # validate that aspect_fix_mode modified the bbox reasonably
+ e = m.envelope()
+ assert_almost_equal(e.minx, -179.999999975, places=7)
+ assert_almost_equal(e.miny, -167.951396161, places=7)
+ assert_almost_equal(e.maxx, 179.999999975, places=7)
+ assert_almost_equal(e.maxy, 192.048603789, places=7)
+ fs = m.query_point(0,-98.9264, 38.1432) # somewhere in kansas
+ feat = fs.next()
+ eq_(feat.attributes['NAME'],u'United States')
+
+ def test_map_query_in_pixels_works1():
+ m = mapnik.Map(256,256)
+ mapnik.load_map(m,'../data/good_maps/wgs842merc_reprojection.xml')
+ merc_bounds = mapnik.Box2d(-20037508.34,-20037508.34,20037508.34,20037508.34)
+ m.maximum_extent = merc_bounds
+ m.zoom_all()
+ fs = m.query_map_point(0,55,100) # somewhere in middle of us
+ feat = fs.next()
+ eq_(feat.attributes['NAME_FORMA'],u'United States of America')
+
+ def test_map_query_in_pixels_works2():
+ m = mapnik.Map(256,256)
+ mapnik.load_map(m,'../data/good_maps/merc2wgs84_reprojection.xml')
+ wgs84_bounds = mapnik.Box2d(-179.999999975,-85.0511287776,179.999999975,85.0511287776)
+ m.maximum_extent = wgs84_bounds
+ # caution - will go square due to evil aspect_fix_mode backhandedness
+ m.zoom_all()
+ # validate that aspect_fix_mode modified the bbox reasonably
+ e = m.envelope()
+ assert_almost_equal(e.minx, -179.999999975, places=7)
+ assert_almost_equal(e.miny, -167.951396161, places=7)
+ assert_almost_equal(e.maxx, 179.999999975, places=7)
+ assert_almost_equal(e.maxy, 192.048603789, places=7)
+ fs = m.query_map_point(0,55,100) # somewhere in Canada
+ feat = fs.next()
+ eq_(feat.attributes['NAME'],u'Canada')
+
+if __name__ == "__main__":
+ setup()
+ exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/mapnik_logger_test.py b/test/python_tests/mapnik_logger_test.py
new file mode 100644
index 0000000..c27ff46
--- /dev/null
+++ b/test/python_tests/mapnik_logger_test.py
@@ -0,0 +1,18 @@
+#!/usr/bin/env python
+from nose.tools import eq_
+from utilities import run_all
+import mapnik
+
+def test_logger_init():
+ eq_(mapnik.severity_type.Debug,0)
+ eq_(mapnik.severity_type.Warn,1)
+ eq_(mapnik.severity_type.Error,2)
+ eq_(mapnik.severity_type.None,3)
+ default = mapnik.logger.get_severity()
+ mapnik.logger.set_severity(mapnik.severity_type.Debug)
+ eq_(mapnik.logger.get_severity(),mapnik.severity_type.Debug)
+ mapnik.logger.set_severity(default)
+ eq_(mapnik.logger.get_severity(),default)
+
+if __name__ == "__main__":
+ exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/mapnik_test_data_test.py b/test/python_tests/mapnik_test_data_test.py
new file mode 100644
index 0000000..b4226e1
--- /dev/null
+++ b/test/python_tests/mapnik_test_data_test.py
@@ -0,0 +1,60 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from utilities import execution_path, run_all
+import os, mapnik
+from glob import glob
+
+default_logging_severity = mapnik.logger.get_severity()
+
+def setup():
+ mapnik.logger.set_severity(mapnik.severity_type.None)
+ # All of the paths used are relative, if we run the tests
+ # from another directory we need to chdir()
+ os.chdir(execution_path('.'))
+
+def teardown():
+ mapnik.logger.set_severity(default_logging_severity)
+
+plugin_mapping = {
+ '.csv' : ['csv'],
+ '.json': ['geojson','ogr'],
+ '.tif' : ['gdal'],
+ #'.tif' : ['gdal','raster'],
+ '.kml' : ['ogr'],
+ '.gpx' : ['ogr'],
+ '.vrt' : ['gdal']
+}
+
+def test_opening_data():
+ # https://github.com/mapbox/mapnik-test-data
+ # cd tests/data
+ # git clone --depth 1 https://github.com/mapbox/mapnik-test-data
+ if os.path.exists('../data/mapnik-test-data/'):
+ files = glob('../data/mapnik-test-data/data/*/*.*')
+ for filepath in files:
+ ext = os.path.splitext(filepath)[1]
+ if plugin_mapping.get(ext):
+ #print 'testing opening %s' % filepath
+ if 'topo' in filepath:
+ kwargs = {'type': 'ogr','file': filepath}
+ kwargs['layer_by_index'] = 0
+ try:
+ mapnik.Datasource(**kwargs)
+ except Exception, e:
+ print 'could not open, %s: %s' % (kwargs,e)
+ else:
+ for plugin in plugin_mapping[ext]:
+ kwargs = {'type': plugin,'file': filepath}
+ if plugin is 'ogr':
+ kwargs['layer_by_index'] = 0
+ try:
+ mapnik.Datasource(**kwargs)
+ except Exception, e:
+ print 'could not open, %s: %s' % (kwargs,e)
+ #else:
+ # print 'skipping opening %s' % filepath
+
+if __name__ == "__main__":
+ setup()
+ exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/markers_complex_rendering_test.py b/test/python_tests/markers_complex_rendering_test.py
new file mode 100644
index 0000000..efce684
--- /dev/null
+++ b/test/python_tests/markers_complex_rendering_test.py
@@ -0,0 +1,43 @@
+#coding=utf8
+import os
+import mapnik
+from utilities import execution_path, run_all
+from nose.tools import eq_
+
+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('.'))
+
+if 'csv' in mapnik.DatasourceCache.plugin_names():
+ def test_marker_ellipse_render1():
+ m = mapnik.Map(256,256)
+ mapnik.load_map(m,'../data/good_maps/marker_ellipse_transform.xml')
+ m.zoom_all()
+ im = mapnik.Image(m.width,m.height)
+ mapnik.render(m,im)
+ actual = '/tmp/mapnik-marker-ellipse-render1.png'
+ expected = 'images/support/mapnik-marker-ellipse-render1.png'
+ im.save(actual,'png32')
+ if os.environ.get('UPDATE'):
+ im.save(expected,'png32')
+ expected_im = mapnik.Image.open(expected)
+ eq_(im.tostring('png32'),expected_im.tostring('png32'), 'failed comparing actual (%s) and expected (%s)' % (actual,'tests/python_tests/'+ expected))
+
+ def test_marker_ellipse_render2():
+ m = mapnik.Map(256,256)
+ mapnik.load_map(m,'../data/good_maps/marker_ellipse_transform2.xml')
+ m.zoom_all()
+ im = mapnik.Image(m.width,m.height)
+ mapnik.render(m,im)
+ actual = '/tmp/mapnik-marker-ellipse-render2.png'
+ expected = 'images/support/mapnik-marker-ellipse-render2.png'
+ im.save(actual,'png32')
+ if os.environ.get('UPDATE'):
+ im.save(expected,'png32')
+ expected_im = mapnik.Image.open(expected)
+ eq_(im.tostring('png32'),expected_im.tostring('png32'), 'failed comparing actual (%s) and expected (%s)' % (actual,'tests/python_tests/'+ expected))
+
+if __name__ == "__main__":
+ setup()
+ exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/memory_datasource_test.py b/test/python_tests/memory_datasource_test.py
new file mode 100644
index 0000000..bd82bea
--- /dev/null
+++ b/test/python_tests/memory_datasource_test.py
@@ -0,0 +1,34 @@
+#encoding: utf8
+import mapnik
+from utilities import run_all
+from nose.tools import eq_
+
+def test_add_feature():
+ md = mapnik.MemoryDatasource()
+ eq_(md.num_features(), 0)
+ context = mapnik.Context()
+ context.push('foo')
+ feature = mapnik.Feature(context,1)
+ feature['foo'] = 'bar'
+ feature.geometry = mapnik.Geometry.from_wkt('POINT(2 3)')
+ md.add_feature(feature)
+ eq_(md.num_features(), 1)
+
+ featureset = md.features_at_point(mapnik.Coord(2,3))
+ retrieved = []
+
+ for feat in featureset:
+ retrieved.append(feat)
+
+ eq_(len(retrieved), 1)
+ f = retrieved[0]
+ eq_(f['foo'], 'bar')
+
+ featureset = md.features_at_point(mapnik.Coord(20,30))
+ retrieved = []
+ for feat in featureset:
+ retrieved.append(feat)
+ eq_(len(retrieved), 0)
+
+if __name__ == "__main__":
+ exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/multi_tile_raster_test.py b/test/python_tests/multi_tile_raster_test.py
new file mode 100644
index 0000000..7dda876
--- /dev/null
+++ b/test/python_tests/multi_tile_raster_test.py
@@ -0,0 +1,68 @@
+#!/usr/bin/env python
+
+from nose.tools import eq_
+from utilities import execution_path, run_all
+import os, mapnik
+
+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_multi_tile_policy():
+ srs = '+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs'
+ lyr = mapnik.Layer('raster')
+ if 'raster' in mapnik.DatasourceCache.plugin_names():
+ lyr.datasource = mapnik.Raster(
+ file = '../data/raster_tiles/${x}/${y}.tif',
+ lox = -180,
+ loy = -90,
+ hix = 180,
+ hiy = 90,
+ multi = 1,
+ tile_size = 256,
+ x_width = 2,
+ y_width = 2
+ )
+ lyr.srs = srs
+ _map = mapnik.Map(256, 256, srs)
+ style = mapnik.Style()
+ rule = mapnik.Rule()
+ sym = mapnik.RasterSymbolizer()
+ 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(lyr.envelope())
+
+ im = mapnik.Image(_map.width, _map.height)
+ mapnik.render(_map, im)
+
+ # test green chunk
+ eq_(im.view(0,64,1,1).tostring(), '\x00\xff\x00\xff')
+ eq_(im.view(127,64,1,1).tostring(), '\x00\xff\x00\xff')
+ eq_(im.view(0,127,1,1).tostring(), '\x00\xff\x00\xff')
+ eq_(im.view(127,127,1,1).tostring(), '\x00\xff\x00\xff')
+
+ # test blue chunk
+ eq_(im.view(128,64,1,1).tostring(), '\x00\x00\xff\xff')
+ eq_(im.view(255,64,1,1).tostring(), '\x00\x00\xff\xff')
+ eq_(im.view(128,127,1,1).tostring(), '\x00\x00\xff\xff')
+ eq_(im.view(255,127,1,1).tostring(), '\x00\x00\xff\xff')
+
+ # test red chunk
+ eq_(im.view(0,128,1,1).tostring(), '\xff\x00\x00\xff')
+ eq_(im.view(127,128,1,1).tostring(), '\xff\x00\x00\xff')
+ eq_(im.view(0,191,1,1).tostring(), '\xff\x00\x00\xff')
+ eq_(im.view(127,191,1,1).tostring(), '\xff\x00\x00\xff')
+
+ # test magenta chunk
+ eq_(im.view(128,128,1,1).tostring(), '\xff\x00\xff\xff')
+ eq_(im.view(255,128,1,1).tostring(), '\xff\x00\xff\xff')
+ eq_(im.view(128,191,1,1).tostring(), '\xff\x00\xff\xff')
+ eq_(im.view(255,191,1,1).tostring(), '\xff\x00\xff\xff')
+
+if __name__ == "__main__":
+ setup()
+ exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/object_test.py b/test/python_tests/object_test.py
new file mode 100644
index 0000000..0f23e71
--- /dev/null
+++ b/test/python_tests/object_test.py
@@ -0,0 +1,569 @@
+# #!/usr/bin/env python
+# # -*- coding: utf-8 -*-
+
+# import os
+# from nose.tools import *
+# from utilities import execution_path, run_all
+# import tempfile
+
+# import mapnik
+
+# 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_debug_symbolizer():
+# s = mapnik.DebugSymbolizer()
+# eq_(s.mode,mapnik.debug_symbolizer_mode.collision)
+
+# def test_raster_symbolizer():
+# s = mapnik.RasterSymbolizer()
+# eq_(s.comp_op,mapnik.CompositeOp.src_over) # note: mode is deprecated
+# eq_(s.scaling,mapnik.scaling_method.NEAR)
+# eq_(s.opacity,1.0)
+# eq_(s.colorizer,None)
+# eq_(s.filter_factor,-1)
+# eq_(s.mesh_size,16)
+# eq_(s.premultiplied,None)
+# s.premultiplied = True
+# eq_(s.premultiplied,True)
+
+# def test_line_pattern():
+# s = mapnik.LinePatternSymbolizer(mapnik.PathExpression('../data/images/dummy.png'))
+# eq_(s.filename, '../data/images/dummy.png')
+# eq_(s.smooth,0.0)
+# eq_(s.transform,'')
+# eq_(s.offset,0.0)
+# eq_(s.comp_op,mapnik.CompositeOp.src_over)
+# eq_(s.clip,True)
+
+# def test_line_symbolizer():
+# s = mapnik.LineSymbolizer()
+# eq_(s.rasterizer, mapnik.line_rasterizer.FULL)
+# eq_(s.smooth,0.0)
+# eq_(s.comp_op,mapnik.CompositeOp.src_over)
+# eq_(s.clip,True)
+# eq_(s.stroke.width, 1)
+# eq_(s.stroke.opacity, 1)
+# eq_(s.stroke.color, mapnik.Color('black'))
+# eq_(s.stroke.line_cap, mapnik.line_cap.BUTT_CAP)
+# eq_(s.stroke.line_join, mapnik.line_join.MITER_JOIN)
+
+# l = mapnik.LineSymbolizer(mapnik.Color('blue'), 5.0)
+
+# eq_(l.stroke.width, 5)
+# eq_(l.stroke.opacity, 1)
+# eq_(l.stroke.color, mapnik.Color('blue'))
+# eq_(l.stroke.line_cap, mapnik.line_cap.BUTT_CAP)
+# eq_(l.stroke.line_join, mapnik.line_join.MITER_JOIN)
+
+# s = mapnik.Stroke(mapnik.Color('blue'), 5.0)
+# l = mapnik.LineSymbolizer(s)
+
+# eq_(l.stroke.width, 5)
+# eq_(l.stroke.opacity, 1)
+# eq_(l.stroke.color, mapnik.Color('blue'))
+# eq_(l.stroke.line_cap, mapnik.line_cap.BUTT_CAP)
+# eq_(l.stroke.line_join, mapnik.line_join.MITER_JOIN)
+
+# def test_line_symbolizer_stroke_reference():
+# l = mapnik.LineSymbolizer(mapnik.Color('green'),0.1)
+# l.stroke.add_dash(.1,.1)
+# l.stroke.add_dash(.1,.1)
+# eq_(l.stroke.get_dashes(), [(.1,.1),(.1,.1)])
+# eq_(l.stroke.color,mapnik.Color('green'))
+# eq_(l.stroke.opacity,1.0)
+# assert_almost_equal(l.stroke.width,0.1)
+
+# # https://github.com/mapnik/mapnik/issues/1427
+# def test_stroke_dash_api():
+# stroke = mapnik.Stroke()
+# dashes = [(1.0,1.0)]
+# stroke.dasharray = dashes
+# eq_(stroke.dasharray, dashes)
+# stroke.add_dash(.1,.1)
+# dashes.append((.1,.1))
+# eq_(stroke.dasharray, dashes)
+
+
+# def test_text_symbolizer():
+# s = mapnik.TextSymbolizer()
+# eq_(s.comp_op,mapnik.CompositeOp.src_over)
+# eq_(s.clip,True)
+# eq_(s.halo_rasterizer,mapnik.halo_rasterizer.FULL)
+
+# # https://github.com/mapnik/mapnik/issues/1420
+# eq_(s.text_transform, mapnik.text_transform.NONE)
+
+# # old args required method
+# ts = mapnik.TextSymbolizer(mapnik.Expression('[Field_Name]'), 'Font Name', 8, mapnik.Color('black'))
+# # eq_(str(ts.name), str(mapnik2.Expression('[Field_Name]'))) name field is no longer supported
+# eq_(ts.format.face_name, 'Font Name')
+# eq_(ts.format.text_size, 8)
+# eq_(ts.format.fill, mapnik.Color('black'))
+# eq_(ts.properties.label_placement, mapnik.label_placement.POINT_PLACEMENT)
+# eq_(ts.properties.horizontal_alignment, mapnik.horizontal_alignment.AUTO)
+
+# def test_shield_symbolizer_init():
+# s = mapnik.ShieldSymbolizer(mapnik.Expression('[Field Name]'), 'DejaVu Sans Bold', 6, mapnik.Color('#000000'), mapnik.PathExpression('../data/images/dummy.png'))
+# eq_(s.comp_op,mapnik.CompositeOp.src_over)
+# eq_(s.clip,True)
+# eq_(s.displacement, (0.0,0.0))
+# eq_(s.allow_overlap, False)
+# eq_(s.avoid_edges, False)
+# eq_(s.character_spacing,0)
+# #eq_(str(s.name), str(mapnik2.Expression('[Field Name]'))) name field is no longer supported
+# eq_(s.face_name, 'DejaVu Sans Bold')
+# eq_(s.allow_overlap, False)
+# eq_(s.fill, mapnik.Color('#000000'))
+# eq_(s.halo_fill, mapnik.Color('rgb(255,255,255)'))
+# eq_(s.halo_radius, 0)
+# eq_(s.label_placement, mapnik.label_placement.POINT_PLACEMENT)
+# eq_(s.minimum_distance, 0.0)
+# eq_(s.text_ratio, 0)
+# eq_(s.text_size, 6)
+# eq_(s.wrap_width, 0)
+# eq_(s.vertical_alignment, mapnik.vertical_alignment.AUTO)
+# eq_(s.label_spacing, 0)
+# eq_(s.label_position_tolerance, 0)
+# # 22.5 * M_PI/180.0 initialized by default
+# assert_almost_equal(s.max_char_angle_delta, 0.39269908169872414)
+
+# eq_(s.text_transform, mapnik.text_transform.NONE)
+# eq_(s.line_spacing, 0)
+# eq_(s.character_spacing, 0)
+
+# # r1341
+# eq_(s.wrap_before, False)
+# eq_(s.horizontal_alignment, mapnik.horizontal_alignment.AUTO)
+# eq_(s.justify_alignment, mapnik.justify_alignment.AUTO)
+# eq_(s.opacity, 1.0)
+
+# # r2300
+# eq_(s.minimum_padding, 0.0)
+
+# # was mixed with s.opacity
+# eq_(s.text_opacity, 1.0)
+
+# eq_(s.shield_displacement, (0.0,0.0))
+# # TODO - the pattern in bindings seems to be to get/set
+# # strings for PathExpressions... should we pass objects?
+# eq_(s.filename, '../data/images/dummy.png')
+
+# # 11c34b1: default transform list is empty, not identity matrix
+# eq_(s.transform, '')
+
+# eq_(s.fontset, None)
+
+# # ShieldSymbolizer missing image file
+# # images paths are now PathExpressions are evaluated at runtime
+# # so it does not make sense to throw...
+# #@raises(RuntimeError)
+# #def test_shieldsymbolizer_missing_image():
+# # s = mapnik.ShieldSymbolizer(mapnik.Expression('[Field Name]'), 'DejaVu Sans Bold', 6, mapnik.Color('#000000'), mapnik.PathExpression('../#data/images/broken.png'))
+
+# def test_shield_symbolizer_modify():
+# s = mapnik.ShieldSymbolizer(mapnik.Expression('[Field Name]'), 'DejaVu Sans Bold', 6, mapnik.Color('#000000'), mapnik.PathExpression('../data/images/dummy.png'))
+# # transform expression
+# def check_transform(expr, expect_str=None):
+# s.transform = expr
+# eq_(s.transform, expr if expect_str is None else expect_str)
+# check_transform("matrix(1 2 3 4 5 6)", "matrix(1, 2, 3, 4, 5, 6)")
+# check_transform("matrix(1, 2, 3, 4, 5, 6 +7)", "matrix(1, 2, 3, 4, 5, (6+7))")
+# check_transform("rotate([a])")
+# check_transform("rotate([a] -2)", "rotate(([a]-2))")
+# check_transform("rotate([a] -2 -3)", "rotate([a], -2, -3)")
+# check_transform("rotate([a] -2 -3 -4)", "rotate(((([a]-2)-3)-4))")
+# check_transform("rotate([a] -2, 3, 4)", "rotate(([a]-2), 3, 4)")
+# check_transform("translate([tx]) rotate([a])")
+# check_transform("scale([sx], [sy]/2)")
+# # TODO check expected failures
+
+# def test_point_symbolizer():
+# p = mapnik.PointSymbolizer()
+# eq_(p.filename,'')
+# eq_(p.transform,'')
+# eq_(p.opacity,1.0)
+# eq_(p.allow_overlap,False)
+# eq_(p.ignore_placement,False)
+# eq_(p.comp_op,mapnik.CompositeOp.src_over)
+# eq_(p.placement, mapnik.point_placement.CENTROID)
+
+# p = mapnik.PointSymbolizer(mapnik.PathExpression("../data/images/dummy.png"))
+# p.allow_overlap = True
+# p.opacity = 0.5
+# p.ignore_placement = True
+# p.placement = mapnik.point_placement.INTERIOR
+# eq_(p.allow_overlap, True)
+# eq_(p.opacity, 0.5)
+# eq_(p.filename,'../data/images/dummy.png')
+# eq_(p.ignore_placement,True)
+# eq_(p.placement, mapnik.point_placement.INTERIOR)
+
+# def test_markers_symbolizer():
+# p = mapnik.MarkersSymbolizer()
+# eq_(p.allow_overlap, False)
+# eq_(p.opacity,1.0)
+# eq_(p.fill_opacity,None)
+# eq_(p.filename,'shape://ellipse')
+# eq_(p.placement,mapnik.marker_placement.POINT_PLACEMENT)
+# eq_(p.multi_policy,mapnik.marker_multi_policy.EACH)
+# eq_(p.fill,None)
+# eq_(p.ignore_placement,False)
+# eq_(p.spacing,100)
+# eq_(p.max_error,0.2)
+# eq_(p.width,None)
+# eq_(p.height,None)
+# eq_(p.transform,'')
+# eq_(p.clip,True)
+# eq_(p.comp_op,mapnik.CompositeOp.src_over)
+
+
+# p.width = mapnik.Expression('12')
+# p.height = mapnik.Expression('12')
+# eq_(str(p.width),'12')
+# eq_(str(p.height),'12')
+
+# p.width = mapnik.Expression('[field] + 2')
+# p.height = mapnik.Expression('[field] + 2')
+# eq_(str(p.width),'([field]+2)')
+# eq_(str(p.height),'([field]+2)')
+
+# stroke = mapnik.Stroke()
+# stroke.color = mapnik.Color('black')
+# stroke.width = 1.0
+
+# p.stroke = stroke
+# p.fill = mapnik.Color('white')
+# p.allow_overlap = True
+# p.opacity = 0.5
+# p.fill_opacity = 0.5
+# p.placement = mapnik.marker_placement.LINE_PLACEMENT
+# p.multi_policy = mapnik.marker_multi_policy.WHOLE
+
+# eq_(p.allow_overlap, True)
+# eq_(p.opacity, 0.5)
+# eq_(p.fill_opacity, 0.5)
+# eq_(p.multi_policy,mapnik.marker_multi_policy.WHOLE)
+# eq_(p.placement,mapnik.marker_placement.LINE_PLACEMENT)
+
+# #https://github.com/mapnik/mapnik/issues/1285
+# #https://github.com/mapnik/mapnik/issues/1427
+# p.marker_type = 'arrow'
+# eq_(p.marker_type,'shape://arrow')
+# eq_(p.filename,'shape://arrow')
+
+
+# # PointSymbolizer missing image file
+# # images paths are now PathExpressions are evaluated at runtime
+# # so it does not make sense to throw...
+# #@raises(RuntimeError)
+# #def test_pointsymbolizer_missing_image():
+# # p = mapnik.PointSymbolizer(mapnik.PathExpression("../data/images/broken.png"))
+
+# def test_polygon_symbolizer():
+# p = mapnik.PolygonSymbolizer()
+# eq_(p.smooth,0.0)
+# eq_(p.comp_op,mapnik.CompositeOp.src_over)
+# eq_(p.clip,True)
+# eq_(p.fill, mapnik.Color('gray'))
+# eq_(p.fill_opacity, 1)
+
+# p = mapnik.PolygonSymbolizer(mapnik.Color('blue'))
+
+# eq_(p.fill, mapnik.Color('blue'))
+# eq_(p.fill_opacity, 1)
+
+# def test_building_symbolizer_init():
+# p = mapnik.BuildingSymbolizer()
+
+# eq_(p.fill, mapnik.Color('gray'))
+# eq_(p.fill_opacity, 1)
+# eq_(p.height,None)
+
+# def test_group_symbolizer_init():
+# s = mapnik.GroupSymbolizer()
+
+# p = mapnik.GroupSymbolizerProperties()
+
+# l = mapnik.PairLayout()
+# l.item_margin = 5.0
+# p.set_layout(l)
+
+# r = mapnik.GroupRule(mapnik.Expression("[name%1]"))
+# r.append(mapnik.PointSymbolizer())
+# p.add_rule(r)
+# s.symbolizer_properties = p
+
+# eq_(s.comp_op,mapnik.CompositeOp.src_over)
+
+# def test_stroke_init():
+# s = mapnik.Stroke()
+
+# eq_(s.width, 1)
+# eq_(s.opacity, 1)
+# eq_(s.color, mapnik.Color('black'))
+# eq_(s.line_cap, mapnik.line_cap.BUTT_CAP)
+# eq_(s.line_join, mapnik.line_join.MITER_JOIN)
+# eq_(s.gamma,1.0)
+
+# s = mapnik.Stroke(mapnik.Color('blue'), 5.0)
+# s.gamma = .5
+
+# eq_(s.width, 5)
+# eq_(s.opacity, 1)
+# eq_(s.color, mapnik.Color('blue'))
+# eq_(s.gamma, .5)
+# eq_(s.line_cap, mapnik.line_cap.BUTT_CAP)
+# eq_(s.line_join, mapnik.line_join.MITER_JOIN)
+
+# def test_stroke_dash_arrays():
+# s = mapnik.Stroke()
+# s.add_dash(1,2)
+# s.add_dash(3,4)
+# s.add_dash(5,6)
+
+# eq_(s.get_dashes(), [(1,2),(3,4),(5,6)])
+
+# def test_map_init():
+# m = mapnik.Map(256, 256)
+
+# eq_(m.width, 256)
+# eq_(m.height, 256)
+# eq_(m.srs, '+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs')
+# eq_(m.base, '')
+# eq_(m.maximum_extent, None)
+# eq_(m.background_image, None)
+# eq_(m.background_image_comp_op, mapnik.CompositeOp.src_over)
+# eq_(m.background_image_opacity, 1.0)
+
+# m = mapnik.Map(256, 256, '+proj=latlong')
+# eq_(m.srs, '+proj=latlong')
+
+# def test_map_style_access():
+# m = mapnik.Map(256, 256)
+# sty = mapnik.Style()
+# m.append_style("style",sty)
+# styles = list(m.styles)
+# eq_(len(styles),1)
+# eq_(styles[0][0],'style')
+# # returns a copy so let's just check it is the right instance
+# eq_(isinstance(styles[0][1],mapnik.Style),True)
+
+# def test_map_maximum_extent_modification():
+# m = mapnik.Map(256, 256)
+# eq_(m.maximum_extent, None)
+# m.maximum_extent = mapnik.Box2d()
+# eq_(m.maximum_extent, mapnik.Box2d())
+# m.maximum_extent = None
+# eq_(m.maximum_extent, None)
+
+# # Map initialization from string
+# def test_map_init_from_string():
+# map_string = '''<Map background-color="steelblue" base="./" srs="+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs">
+# <Style name="My Style">
+# <Rule>
+# <PolygonSymbolizer fill="#f2eff9"/>
+# <LineSymbolizer stroke="rgb(50%,50%,50%)" stroke-width="0.1"/>
+# </Rule>
+# </Style>
+# <Layer name="boundaries">
+# <StyleName>My Style</StyleName>
+# <Datasource>
+# <Parameter name="type">shape</Parameter>
+# <Parameter name="file">../../demo/data/boundaries</Parameter>
+# </Datasource>
+# </Layer>
+# </Map>'''
+
+# m = mapnik.Map(600, 300)
+# eq_(m.base, '')
+# try:
+# mapnik.load_map_from_string(m, map_string)
+# eq_(m.base, './')
+# mapnik.load_map_from_string(m, map_string, False, "") # this "" will have no effect
+# eq_(m.base, './')
+
+# tmp_dir = tempfile.gettempdir()
+# try:
+# mapnik.load_map_from_string(m, map_string, False, tmp_dir)
+# except RuntimeError:
+# pass # runtime error expected because shapefile path should be wrong and datasource will throw
+# eq_(m.base, tmp_dir) # tmp_dir will be set despite the exception because load_map mostly worked
+# m.base = 'foo'
+# mapnik.load_map_from_string(m, map_string, True, ".")
+# eq_(m.base, '.')
+# except RuntimeError, e:
+# # only test datasources that we have installed
+# if not 'Could not create datasource' in str(e):
+# raise RuntimeError(e)
+
+# # Color initialization
+# @raises(Exception) # Boost.Python.ArgumentError
+# def test_color_init_errors():
+# c = mapnik.Color()
+
+# @raises(RuntimeError)
+# def test_color_init_errors():
+# c = mapnik.Color('foo') # mapnik config
+
+# def test_color_init():
+# c = mapnik.Color('blue')
+
+# eq_(c.a, 255)
+# eq_(c.r, 0)
+# eq_(c.g, 0)
+# eq_(c.b, 255)
+
+# eq_(c.to_hex_string(), '#0000ff')
+
+# c = mapnik.Color('#f2eff9')
+
+# eq_(c.a, 255)
+# eq_(c.r, 242)
+# eq_(c.g, 239)
+# eq_(c.b, 249)
+
+# eq_(c.to_hex_string(), '#f2eff9')
+
+# c = mapnik.Color('rgb(50%,50%,50%)')
+
+# eq_(c.a, 255)
+# eq_(c.r, 128)
+# eq_(c.g, 128)
+# eq_(c.b, 128)
+
+# eq_(c.to_hex_string(), '#808080')
+
+# c = mapnik.Color(0, 64, 128)
+
+# eq_(c.a, 255)
+# eq_(c.r, 0)
+# eq_(c.g, 64)
+# eq_(c.b, 128)
+
+# eq_(c.to_hex_string(), '#004080')
+
+# c = mapnik.Color(0, 64, 128, 192)
+
+# eq_(c.a, 192)
+# eq_(c.r, 0)
+# eq_(c.g, 64)
+# eq_(c.b, 128)
+
+# eq_(c.to_hex_string(), '#004080c0')
+
+# def test_color_equality():
+
+# c1 = mapnik.Color('blue')
+# c2 = mapnik.Color(0,0,255)
+# c3 = mapnik.Color('black')
+
+# c3.r = 0
+# c3.g = 0
+# c3.b = 255
+# c3.a = 255
+
+# eq_(c1, c2)
+# eq_(c1, c3)
+
+# c1 = mapnik.Color(0, 64, 128)
+# c2 = mapnik.Color(0, 64, 128)
+# c3 = mapnik.Color(0, 0, 0)
+
+# c3.r = 0
+# c3.g = 64
+# c3.b = 128
+
+# eq_(c1, c2)
+# eq_(c1, c3)
+
+# c1 = mapnik.Color(0, 64, 128, 192)
+# c2 = mapnik.Color(0, 64, 128, 192)
+# c3 = mapnik.Color(0, 0, 0, 255)
+
+# c3.r = 0
+# c3.g = 64
+# c3.b = 128
+# c3.a = 192
+
+# eq_(c1, c2)
+# eq_(c1, c3)
+
+# c1 = mapnik.Color('rgb(50%,50%,50%)')
+# c2 = mapnik.Color(128, 128, 128, 255)
+# c3 = mapnik.Color('#808080')
+# c4 = mapnik.Color('gray')
+
+# eq_(c1, c2)
+# eq_(c1, c3)
+# eq_(c1, c4)
+
+# c1 = mapnik.Color('hsl(0, 100%, 50%)') # red
+# c2 = mapnik.Color('hsl(120, 100%, 50%)') # lime
+# c3 = mapnik.Color('hsla(240, 100%, 50%, 0.5)') # semi-transparent solid blue
+
+# eq_(c1, mapnik.Color('red'))
+# eq_(c2, mapnik.Color('lime'))
+# eq_(c3, mapnik.Color(0,0,255,128))
+
+# def test_rule_init():
+# min_scale = 5
+# max_scale = 10
+
+# r = mapnik.Rule()
+
+# eq_(r.name, '')
+# eq_(r.min_scale, 0)
+# eq_(r.max_scale, float('inf'))
+# eq_(r.has_else(), False)
+# eq_(r.has_also(), False)
+
+# r = mapnik.Rule()
+
+# r.set_else(True)
+# eq_(r.has_else(), True)
+# eq_(r.has_also(), False)
+
+# r = mapnik.Rule()
+
+# r.set_also(True)
+# eq_(r.has_else(), False)
+# eq_(r.has_also(), True)
+
+# r = mapnik.Rule("Name")
+
+# eq_(r.name, 'Name')
+# eq_(r.min_scale, 0)
+# eq_(r.max_scale, float('inf'))
+# eq_(r.has_else(), False)
+# eq_(r.has_also(), False)
+
+# r = mapnik.Rule("Name")
+
+# eq_(r.name, 'Name')
+# eq_(r.min_scale, 0)
+# eq_(r.max_scale, float('inf'))
+# eq_(r.has_else(), False)
+# eq_(r.has_also(), False)
+
+# r = mapnik.Rule("Name", min_scale)
+
+# eq_(r.name, 'Name')
+# eq_(r.min_scale, min_scale)
+# eq_(r.max_scale, float('inf'))
+# eq_(r.has_else(), False)
+# eq_(r.has_also(), False)
+
+# r = mapnik.Rule("Name", min_scale, max_scale)
+
+# eq_(r.name, 'Name')
+# eq_(r.min_scale, min_scale)
+# eq_(r.max_scale, max_scale)
+# eq_(r.has_else(), False)
+# eq_(r.has_also(), False)
+
+# if __name__ == "__main__":
+# setup()
+# run_all(eval(x) for x in dir() if x.startswith("test_"))
diff --git a/test/python_tests/ogr_and_shape_geometries_test.py b/test/python_tests/ogr_and_shape_geometries_test.py
new file mode 100644
index 0000000..5c6918e
--- /dev/null
+++ b/test/python_tests/ogr_and_shape_geometries_test.py
@@ -0,0 +1,43 @@
+#!/usr/bin/env python
+
+from nose.tools import eq_
+from utilities import execution_path, run_all
+import os, mapnik
+
+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('.'))
+
+# TODO - fix truncation in shapefile...
+polys = ["POLYGON ((30 10, 10 20, 20 40, 40 40, 30 10))",
+ "POLYGON ((35 10, 10 20, 15 40, 45 45, 35 10),(20 30, 35 35, 30 20, 20 30))",
+ "MULTIPOLYGON (((30 20, 10 40, 45 40, 30 20)),((15 5, 40 10, 10 20, 5 10, 15 5)))"
+ "MULTIPOLYGON (((40 40, 20 45, 45 30, 40 40)),((20 35, 45 20, 30 5, 10 10, 10 30, 20 35),(30 20, 20 25, 20 15, 30 20)))"
+ ]
+
+plugins = mapnik.DatasourceCache.plugin_names()
+if 'shape' in plugins and 'ogr' in plugins:
+
+ def ensure_geometries_are_interpreted_equivalently(filename):
+ ds1 = mapnik.Ogr(file=filename,layer_by_index=0)
+ ds2 = mapnik.Shapefile(file=filename)
+ fs1 = ds1.featureset()
+ fs2 = ds2.featureset()
+ count = 0;
+ import itertools
+ for feat1,feat2 in itertools.izip(fs1, fs2):
+ count += 1
+ eq_(feat1.attributes,feat2.attributes)
+ # TODO - revisit this: https://github.com/mapnik/mapnik/issues/1093
+ # eq_(feat1.to_geojson(),feat2.to_geojson())
+ #eq_(feat1.geometries().to_wkt(),feat2.geometries().to_wkt())
+ #eq_(feat1.geometries().to_wkb(mapnik.wkbByteOrder.NDR),feat2.geometries().to_wkb(mapnik.wkbByteOrder.NDR))
+ #eq_(feat1.geometries().to_wkb(mapnik.wkbByteOrder.XDR),feat2.geometries().to_wkb(mapnik.wkbByteOrder.XDR))
+
+ def test_simple_polys():
+ ensure_geometries_are_interpreted_equivalently('../data/shp/wkt_poly.shp')
+
+if __name__ == "__main__":
+ setup()
+ exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/ogr_test.py b/test/python_tests/ogr_test.py
new file mode 100644
index 0000000..905eda2
--- /dev/null
+++ b/test/python_tests/ogr_test.py
@@ -0,0 +1,157 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from nose.tools import eq_,assert_almost_equal,raises
+from utilities import execution_path, run_all
+import os, mapnik
+
+try:
+ import json
+except ImportError:
+ import simplejson as json
+
+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('.'))
+
+if 'ogr' in mapnik.DatasourceCache.plugin_names():
+
+ # Shapefile initialization
+ def test_shapefile_init():
+ ds = mapnik.Ogr(file='../data/shp/boundaries.shp',layer_by_index=0)
+ e = ds.envelope()
+ assert_almost_equal(e.minx, -11121.6896651, places=7)
+ assert_almost_equal(e.miny, -724724.216526, places=6)
+ assert_almost_equal(e.maxx, 2463000.67866, places=5)
+ assert_almost_equal(e.maxy, 1649661.267, places=3)
+ meta = ds.describe()
+ eq_(meta['geometry_type'],mapnik.DataGeometryType.Polygon)
+ eq_('+proj=lcc' in meta['proj4'],True)
+
+ # Shapefile properties
+ def test_shapefile_properties():
+ ds = mapnik.Ogr(file='../data/shp/boundaries.shp',layer_by_index=0)
+ f = ds.features_at_point(ds.envelope().center(), 0.001).features[0]
+ eq_(ds.geometry_type(),mapnik.DataGeometryType.Polygon)
+
+ eq_(f['CGNS_FID'], u'6f733341ba2011d892e2080020a0f4c9')
+ eq_(f['COUNTRY'], u'CAN')
+ eq_(f['F_CODE'], u'FA001')
+ eq_(f['NAME_EN'], u'Quebec')
+ eq_(f['Shape_Area'], 1512185733150.0)
+ eq_(f['Shape_Leng'], 19218883.724300001)
+ meta = ds.describe()
+ eq_(meta['geometry_type'],mapnik.DataGeometryType.Polygon)
+ # NOTE: encoding is latin1 but gdal >= 1.9 should now expose utf8 encoded features
+ # See SHAPE_ENCODING for overriding: http://gdal.org/ogr/drv_shapefile.html
+ # Failure for the NOM_FR field is expected for older gdal
+ #eq_(f['NOM_FR'], u'Qu\xe9bec')
+ #eq_(f['NOM_FR'], u'Québec')
+
+ @raises(RuntimeError)
+ def test_that_nonexistant_query_field_throws(**kwargs):
+ ds = mapnik.Ogr(file='../data/shp/world_merc.shp',layer_by_index=0)
+ eq_(len(ds.fields()),11)
+ eq_(ds.fields(),['FIPS', 'ISO2', 'ISO3', 'UN', 'NAME', 'AREA', 'POP2005', 'REGION', 'SUBREGION', 'LON', 'LAT'])
+ eq_(ds.field_types(),['str', 'str', 'str', 'int', 'str', 'int', 'int', 'int', 'int', 'float', 'float'])
+ query = mapnik.Query(ds.envelope())
+ for fld in ds.fields():
+ query.add_property_name(fld)
+ # also add an invalid one, triggering throw
+ query.add_property_name('bogus')
+ ds.features(query)
+
+ # disabled because OGR prints an annoying error: ERROR 1: Invalid Point object. Missing 'coordinates' member.
+ #def test_handling_of_null_features():
+ # ds = mapnik.Ogr(file='../data/json/null_feature.geojson',layer_by_index=0)
+ # fs = ds.all_features()
+ # eq_(len(fs),1)
+
+ # OGR plugin extent parameter
+ def test_ogr_extent_parameter():
+ ds = mapnik.Ogr(file='../data/shp/world_merc.shp',layer_by_index=0,extent='-1,-1,1,1')
+ e = ds.envelope()
+ eq_(e.minx,-1)
+ eq_(e.miny,-1)
+ eq_(e.maxx,1)
+ eq_(e.maxy,1)
+ meta = ds.describe()
+ eq_(meta['geometry_type'],mapnik.DataGeometryType.Polygon)
+ eq_('+proj=merc' in meta['proj4'],True)
+
+ def test_ogr_reading_gpx_waypoint():
+ ds = mapnik.Ogr(file='../data/gpx/empty.gpx',layer='waypoints')
+ e = ds.envelope()
+ eq_(e.minx,-122)
+ eq_(e.miny,48)
+ eq_(e.maxx,-122)
+ eq_(e.maxy,48)
+ meta = ds.describe()
+ eq_(meta['geometry_type'],mapnik.DataGeometryType.Point)
+ eq_('+proj=longlat' in meta['proj4'],True)
+
+ def test_ogr_empty_data_should_not_throw():
+ default_logging_severity = mapnik.logger.get_severity()
+ mapnik.logger.set_severity(mapnik.severity_type.None)
+ # use logger to silence expected warnings
+ for layer in ['routes', 'tracks', 'route_points', 'track_points']:
+ ds = mapnik.Ogr(file='../data/gpx/empty.gpx',layer=layer)
+ e = ds.envelope()
+ eq_(e.minx,0)
+ eq_(e.miny,0)
+ eq_(e.maxx,0)
+ eq_(e.maxy,0)
+ mapnik.logger.set_severity(default_logging_severity)
+ meta = ds.describe()
+ eq_(meta['geometry_type'],mapnik.DataGeometryType.Point)
+ eq_('+proj=longlat' in meta['proj4'],True)
+
+ # disabled because OGR prints an annoying error: ERROR 1: Invalid Point object. Missing 'coordinates' member.
+ #def test_handling_of_null_features():
+ # ds = mapnik.Ogr(file='../data/json/null_feature.geojson',layer_by_index=0)
+ # fs = ds.all_features()
+ # eq_(len(fs),1)
+
+ def test_geometry_type():
+ ds = mapnik.Ogr(file='../data/csv/wkt.csv',layer_by_index=0)
+ e = ds.envelope()
+ assert_almost_equal(e.minx, 1.0, places=1)
+ assert_almost_equal(e.miny, 1.0, places=1)
+ assert_almost_equal(e.maxx, 45.0, places=1)
+ assert_almost_equal(e.maxy, 45.0, places=1)
+ meta = ds.describe()
+ eq_(meta['geometry_type'],mapnik.DataGeometryType.Point)
+ #eq_('+proj=longlat' in meta['proj4'],True)
+ fs = ds.featureset()
+ feat = fs.next()
+ actual = json.loads(feat.to_geojson())
+ eq_(actual,{u'geometry': {u'type': u'Point', u'coordinates': [30, 10]}, u'type': u'Feature', u'id': 2, u'properties': {u'type': u'point', u'WKT': u' POINT (30 10)'}})
+ feat = fs.next()
+ actual = json.loads(feat.to_geojson())
+ eq_(actual,{u'geometry': {u'type': u'LineString', u'coordinates': [[30, 10], [10, 30], [40, 40]]}, u'type': u'Feature', u'id': 3, u'properties': {u'type': u'linestring', u'WKT': u' LINESTRING (30 10, 10 30, 40 40)'}})
+ feat = fs.next()
+ actual = json.loads(feat.to_geojson())
+ eq_(actual,{u'geometry': {u'type': u'Polygon', u'coordinates': [[[30, 10], [40, 40], [20, 40], [10, 20], [30, 10]]]}, u'type': u'Feature', u'id': 4, u'properties': {u'type': u'polygon', u'WKT': u' POLYGON ((30 10, 10 20, 20 40, 40 40, 30 10))'}})
+ feat = fs.next()
+ actual = json.loads(feat.to_geojson())
+ eq_(actual,{u'geometry': {u'type': u'Polygon', u'coordinates': [[[35, 10], [45, 45], [15, 40], [10, 20], [35, 10]], [[20, 30], [35, 35], [30, 20], [20, 30]]]}, u'type': u'Feature', u'id': 5, u'properties': {u'type': u'polygon', u'WKT': u' POLYGON ((35 10, 10 20, 15 40, 45 45, 35 10),(20 30, 35 35, 30 20, 20 30))'}})
+ feat = fs.next()
+ actual = json.loads(feat.to_geojson())
+ eq_(actual,{u'geometry': {u'type': u'MultiPoint', u'coordinates': [[10, 40], [40, 30], [20, 20], [30, 10]]}, u'type': u'Feature', u'id': 6, u'properties': {u'type': u'multipoint', u'WKT': u' MULTIPOINT ((10 40), (40 30), (20 20), (30 10))'}})
+ feat = fs.next()
+ actual = json.loads(feat.to_geojson())
+ eq_(actual,{u'geometry': {u'type': u'MultiLineString', u'coordinates': [[[10, 10], [20, 20], [10, 40]], [[40, 40], [30, 30], [40, 20], [30, 10]]]}, u'type': u'Feature', u'id': 7, u'properties': {u'type': u'multilinestring', u'WKT': u' MULTILINESTRING ((10 10, 20 20, 10 40),(40 40, 30 30, 40 20, 30 10))'}})
+ feat = fs.next()
+ actual = json.loads(feat.to_geojson())
+ eq_(actual,{u'geometry': {u'type': u'MultiPolygon', u'coordinates': [[[[30, 20], [45, 40], [10, 40], [30, 20]]], [[[15, 5], [40, 10], [10, 20], [5, 10], [15, 5]]]]}, u'type': u'Feature', u'id': 8, u'properties': {u'type': u'multipolygon', u'WKT': u' MULTIPOLYGON (((30 20, 10 40, 45 40, 30 20)),((15 5, 40 10, 10 20, 5 10, 15 5)))'}})
+ feat = fs.next()
+ actual = json.loads(feat.to_geojson())
+ eq_(actual,{u'geometry': {u'type': u'MultiPolygon', u'coordinates': [[[[40, 40], [20, 45], [45, 30], [40, 40]]], [[[20, 35], [10, 30], [10, 10], [30, 5], [45, 20], [20, 35]], [[30, 20], [20, 15], [20, 25], [30, 20]]]]}, u'type': u'Feature', u'id': 9, u'properties': {u'type': u'multipolygon', u'WKT': u' MULTIPOLYGON (((40 40, 20 45, 45 30, 40 40)),((20 35, 45 20, 30 5, 10 10, 10 30, 20 35),(30 20, 20 25, 20 15, 30 20)))'}})
+ feat = fs.next()
+ actual = json.loads(feat.to_geojson())
+ eq_(actual,{u'geometry': {u'type': u'GeometryCollection', u'geometries': [{u'type': u'Polygon', u'coordinates': [[[1, 1], [2, 1], [2, 2], [1, 2], [1, 1]]]}, {u'type': u'Point', u'coordinates': [2, 3]}, {u'type': u'LineString', u'coordinates': [[2, 3], [3, 4]]}]}, u'type': u'Feature', u'id': 10, u'properties': {u'type': u'collection', u'WKT': u' GEOMETRYCOLLECTION(POLYGON((1 1,2 1,2 2,1 2,1 1)),POINT(2 3),LINESTRING(2 3,3 4))'}})
+
+if __name__ == "__main__":
+ setup()
+ exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/osm_test.py b/test/python_tests/osm_test.py
new file mode 100644
index 0000000..b9f5196
--- /dev/null
+++ b/test/python_tests/osm_test.py
@@ -0,0 +1,62 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from nose.tools import eq_
+from utilities import execution_path, run_all
+import os, mapnik
+
+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('.'))
+
+if 'osm' in mapnik.DatasourceCache.plugin_names():
+
+ # osm initialization
+ def test_osm_init():
+ ds = mapnik.Osm(file='../data/osm/nodes.osm')
+
+ e = ds.envelope()
+
+ # these are hardcoded in the plugin… ugh
+ eq_(e.minx >= -180.0,True)
+ eq_(e.miny >= -90.0,True)
+ eq_(e.maxx <= 180.0,True)
+ eq_(e.maxy <= 90,True)
+
+ def test_that_nonexistant_query_field_throws(**kwargs):
+ ds = mapnik.Osm(file='../data/osm/nodes.osm')
+ eq_(len(ds.fields()),0)
+ query = mapnik.Query(ds.envelope())
+ for fld in ds.fields():
+ query.add_property_name(fld)
+ # also add an invalid one, triggering throw
+ query.add_property_name('bogus')
+ ds.features(query)
+
+ def test_that_64bit_int_fields_work():
+ ds = mapnik.Osm(file='../data/osm/64bit.osm')
+ eq_(len(ds.fields()),4)
+ eq_(ds.fields(),['bigint', 'highway', 'junction', 'note'])
+ eq_(ds.field_types(),['str', 'str', 'str', 'str'])
+ fs = ds.featureset()
+ feat = fs.next()
+ eq_(feat.to_geojson(),'{"type":"Feature","id":890,"geometry":{"type":"Point","coordinates":[-61.7960248,17.1415874]},"properties":{}}')
+ eq_(feat.id(),4294968186)
+ eq_(feat['bigint'], None)
+ feat = fs.next()
+ eq_(feat['bigint'],'9223372036854775807')
+
+ def test_reading_ways():
+ ds = mapnik.Osm(file='../data/osm/ways.osm')
+ eq_(len(ds.fields()),0)
+ eq_(ds.fields(),[])
+ eq_(ds.field_types(),[])
+ feat = ds.all_features()[4]
+ eq_(feat.to_geojson(),'{"type":"Feature","id":1,"geometry":{"type":"LineString","coordinates":[[0,2],[0,-2]]},"properties":{}}')
+ eq_(feat.id(),1)
+
+
+if __name__ == "__main__":
+ setup()
+ exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/palette_test.py b/test/python_tests/palette_test.py
new file mode 100644
index 0000000..9b30895
--- /dev/null
+++ b/test/python_tests/palette_test.py
@@ -0,0 +1,54 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from nose.tools import eq_
+from utilities import execution_path, run_all
+import os, mapnik
+
+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('.'))
+
+expected_64 = '[Palette 64 colors #494746 #c37631 #89827c #d1955c #7397b9 #fc9237 #a09f9c #fbc147 #9bb3ce #b7c9a1 #b5d29c #c4b9aa #cdc4a5 #d5c8a3 #c1d7aa #ccc4b6 #dbd19c #b2c4d5 #eae487 #c9c8c6 #e4db99 #c9dcb5 #dfd3ac #cbd2c2 #d6cdbc #dbd2b6 #c0ceda #ece597 #f7ef86 #d7d3c3 #dfcbc3 #d1d0cd #d1e2bf #d3dec1 #dbd3c4 #e6d8b6 #f4ef91 #d3d3cf #cad5de #ded7c9 #dfdbce #fcf993 #ffff8a #dbd9d7 #dbe7cd #d4dce2 #e4ded3 #ebe3c9 #e0e2e2 #f4edc3 #fdfcae #e9e5dc #f4edda #eeebe4 #fefdc5 #e7edf2 #edf4e5 #f [...]
+
+expected_256 = '[Palette 256 colors #272727 #3c3c3c #484847 #564b41 #605243 #6a523e #555555 #785941 #5d5d5d #746856 #676767 #956740 #ba712e #787777 #cb752a #c27c3d #b68049 #dc8030 #df9e10 #878685 #e1a214 #928b82 #a88a70 #ea8834 #e7a81d #cb8d55 #909090 #94938c #e18f48 #f68d36 #6f94b7 #e1ab2e #8e959b #c79666 #999897 #ff9238 #ef9447 #a99a88 #f1b32c #919ca6 #a1a09f #f0b04b #8aa4bf #f8bc39 #b3ac8f #d1a67a #e3b857 #a8a8a7 #ffc345 #a2adb9 #afaeab #f9ab69 #afbba4 #c4c48a #b4b2af #dec177 #9ab2cf [...]
+
+expected_rgb = '[Palette 2 colors #ff00ff #ffffff]'
+
+def test_reading_palettes():
+ act = open('../data/palettes/palette64.act','rb')
+ palette = mapnik.Palette(act.read(),'act')
+ eq_(palette.to_string(),expected_64);
+ act = open('../data/palettes/palette256.act','rb')
+ palette = mapnik.Palette(act.read(),'act')
+ eq_(palette.to_string(),expected_256);
+ palette = mapnik.Palette('\xff\x00\xff\xff\xff\xff', 'rgb')
+ eq_(palette.to_string(),expected_rgb);
+
+if 'shape' in mapnik.DatasourceCache.plugin_names():
+
+ def test_render_with_palette():
+ m = mapnik.Map(600,400)
+ mapnik.load_map(m,'../data/good_maps/agg_poly_gamma_map.xml')
+ m.zoom_all()
+ im = mapnik.Image(m.width,m.height)
+ mapnik.render(m,im)
+ act = open('../data/palettes/palette256.act','rb')
+ palette = mapnik.Palette(act.read(),'act')
+ # test saving directly to filesystem
+ im.save('/tmp/mapnik-palette-test.png','png',palette)
+ expected = './images/support/mapnik-palette-test.png'
+ if os.environ.get('UPDATE'):
+ im.save(expected,"png",palette);
+
+ # test saving to a string
+ open('/tmp/mapnik-palette-test2.png','wb').write(im.tostring('png',palette));
+ # compare the two methods
+ eq_(mapnik.Image.open('/tmp/mapnik-palette-test.png').tostring('png32'),mapnik.Image.open('/tmp/mapnik-palette-test2.png').tostring('png32'),'%s not eq to %s' % ('/tmp/mapnik-palette-test.png','/tmp/mapnik-palette-test2.png'))
+ # compare to expected
+ eq_(mapnik.Image.open('/tmp/mapnik-palette-test.png').tostring('png32'),mapnik.Image.open(expected).tostring('png32'),'%s not eq to %s' % ('/tmp/mapnik-palette-test.png',expected))
+
+if __name__ == "__main__":
+ setup()
+ exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/parameters_test.py b/test/python_tests/parameters_test.py
new file mode 100644
index 0000000..1587fbd
--- /dev/null
+++ b/test/python_tests/parameters_test.py
@@ -0,0 +1,61 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import os
+import sys
+from nose.tools import eq_
+from utilities import execution_path, run_all
+import mapnik
+
+def setup():
+ os.chdir(execution_path('.'))
+
+def test_parameter_null():
+ p = mapnik.Parameter('key',None)
+ eq_(p[0],'key')
+ eq_(p[1],None)
+
+def test_parameter_string():
+ p = mapnik.Parameter('key','value')
+ eq_(p[0],'key')
+ eq_(p[1],'value')
+
+def test_parameter_unicode():
+ p = mapnik.Parameter('key',u'value')
+ eq_(p[0],'key')
+ eq_(p[1],u'value')
+
+def test_parameter_integer():
+ p = mapnik.Parameter('int',sys.maxint)
+ eq_(p[0],'int')
+ eq_(p[1],sys.maxint)
+
+def test_parameter_double():
+ p = mapnik.Parameter('double',float(sys.maxint))
+ eq_(p[0],'double')
+ eq_(p[1],float(sys.maxint))
+
+def test_parameter_boolean():
+ p = mapnik.Parameter('boolean',True)
+ eq_(p[0],'boolean')
+ eq_(p[1],True)
+ eq_(bool(p[1]),True)
+
+
+def test_parameters():
+ params = mapnik.Parameters()
+ p = mapnik.Parameter('float',1.0777)
+ eq_(p[0],'float')
+ eq_(p[1],1.0777)
+
+ params.append(p)
+
+ eq_(params[0][0],'float')
+ eq_(params[0][1],1.0777)
+
+ eq_(params.get('float'),1.0777)
+
+
+if __name__ == "__main__":
+ setup()
+ exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/pgraster_test.py b/test/python_tests/pgraster_test.py
new file mode 100644
index 0000000..dc7584f
--- /dev/null
+++ b/test/python_tests/pgraster_test.py
@@ -0,0 +1,763 @@
+#!/usr/bin/env python
+
+from nose.tools import eq_,assert_almost_equal
+import atexit
+import time
+from utilities import execution_path, run_all, side_by_side_image
+from subprocess import Popen, PIPE
+import os, mapnik
+import sys
+import re
+from binascii import hexlify
+
+MAPNIK_TEST_DBNAME = 'mapnik-tmp-pgraster-test-db'
+POSTGIS_TEMPLATE_DBNAME = 'template_postgis'
+DEBUG_OUTPUT=False
+
+def log(msg):
+ if DEBUG_OUTPUT:
+ print msg
+
+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 call(cmd,silent=False):
+ stdin, stderr = Popen(cmd, shell=True, stdout=PIPE, stderr=PIPE).communicate()
+ if not stderr:
+ return stdin.strip()
+ elif not silent and 'error' in stderr.lower() \
+ or 'not found' in stderr.lower() \
+ or 'could not connect' in stderr.lower() \
+ or 'bad connection' in stderr.lower() \
+ or 'not recognized as an internal' in stderr.lower():
+ raise RuntimeError(stderr.strip())
+
+def psql_can_connect():
+ """Test ability to connect to a postgis template db with no options.
+
+ Basically, to run these tests your user must have full read
+ access over unix sockets without supplying a password. This
+ keeps these tests simple and focused on postgis not on postgres
+ auth issues.
+ """
+ try:
+ call('psql %s -c "select postgis_version()"' % POSTGIS_TEMPLATE_DBNAME)
+ return True
+ except RuntimeError:
+ print 'Notice: skipping pgraster tests (connection)'
+ return False
+
+def psql_run(cmd):
+ cmd = 'psql --set ON_ERROR_STOP=1 %s -c "%s"' % \
+ (MAPNIK_TEST_DBNAME, cmd.replace('"', '\\"'))
+ log('DEBUG: running ' + cmd)
+ call(cmd)
+
+def raster2pgsql_on_path():
+ """Test for presence of raster2pgsql on the user path.
+
+ We require this program to load test data into a temporarily database.
+ """
+ try:
+ call('raster2pgsql')
+ return True
+ except RuntimeError:
+ print 'Notice: skipping pgraster tests (raster2pgsql)'
+ return False
+
+def createdb_and_dropdb_on_path():
+ """Test for presence of dropdb/createdb on user path.
+
+ We require these programs to setup and teardown the testing db.
+ """
+ try:
+ call('createdb --help')
+ call('dropdb --help')
+ return True
+ except RuntimeError:
+ print 'Notice: skipping pgraster tests (createdb/dropdb)'
+ return False
+
+def postgis_setup():
+ call('dropdb %s' % MAPNIK_TEST_DBNAME,silent=True)
+ call('createdb -T %s %s' % (POSTGIS_TEMPLATE_DBNAME,MAPNIK_TEST_DBNAME),silent=False)
+
+def postgis_takedown():
+ pass
+ # fails as the db is in use: https://github.com/mapnik/mapnik/issues/960
+ #call('dropdb %s' % MAPNIK_TEST_DBNAME)
+
+def import_raster(filename, tabname, tilesize, constraint, overview):
+ log('tile: ' + tilesize + ' constraints: ' + str(constraint) \
+ + ' overviews: ' + overview)
+ cmd = 'raster2pgsql -Y -I -q'
+ if constraint:
+ cmd += ' -C'
+ if tilesize:
+ cmd += ' -t ' + tilesize
+ if overview:
+ cmd += ' -l ' + overview
+ cmd += ' %s %s | psql --set ON_ERROR_STOP=1 -q %s' % (os.path.abspath(os.path.normpath(filename)),tabname,MAPNIK_TEST_DBNAME)
+ log('Import call: ' + cmd)
+ call(cmd)
+
+def drop_imported(tabname, overview):
+ psql_run('DROP TABLE IF EXISTS "' + tabname + '";')
+ if overview:
+ for of in overview.split(','):
+ psql_run('DROP TABLE IF EXISTS "o_' + of + '_' + tabname + '";')
+
+def compare_images(expected,im):
+ expected = os.path.join(os.path.dirname(expected),os.path.basename(expected).replace(':','_'))
+ if not os.path.exists(expected) or os.environ.get('UPDATE'):
+ print 'generating expected image %s' % expected
+ im.save(expected,'png32')
+ expected_im = mapnik.Image.open(expected)
+ diff = expected.replace('.png','-diff.png')
+ if len(im.tostring("png32")) != len(expected_im.tostring("png32")):
+ compared = side_by_side_image(expected_im, im)
+ compared.save(diff)
+ assert False,'images do not match, check diff at %s' % diff
+ else:
+ if os.path.exists(diff): os.unlink(diff)
+ return True
+
+if 'pgraster' in mapnik.DatasourceCache.plugin_names() \
+ and createdb_and_dropdb_on_path() \
+ and psql_can_connect() \
+ and raster2pgsql_on_path():
+
+ # initialize test database
+ postgis_setup()
+
+ # [old]dataraster.tif, 2283x1913 int16 single-band
+ # dataraster-small.tif, 457x383 int16 single-band
+ def _test_dataraster_16bsi_rendering(lbl, overview, rescale, clip):
+ if rescale:
+ lbl += ' Sc'
+ if clip:
+ lbl += ' Cl'
+ ds = mapnik.PgRaster(dbname=MAPNIK_TEST_DBNAME,table='"dataRaster"',
+ band=1,use_overviews=1 if overview else 0,
+ prescale_rasters=rescale,clip_rasters=clip)
+ fs = ds.featureset()
+ feature = fs.next()
+ eq_(feature['rid'],1)
+ lyr = mapnik.Layer('dataraster_16bsi')
+ lyr.datasource = ds
+ expenv = mapnik.Box2d(-14637, 3903178, 1126863, 4859678)
+ env = lyr.envelope()
+ # As the input size is a prime number both horizontally
+ # and vertically, we expect the extent of the overview
+ # tables to be a pixel wider than the original, whereas
+ # the pixel size in geographical units depends on the
+ # overview factor. So we start with the original pixel size
+ # as base scale and multiply by the overview factor.
+ # NOTE: the overview table extent only grows north and east
+ pixsize = 500 # see gdalinfo dataraster.tif
+ pixsize = 2497 # see gdalinfo dataraster-small.tif
+ tol = pixsize * max(overview.split(',')) if overview else 0
+ assert_almost_equal(env.minx, expenv.minx)
+ assert_almost_equal(env.miny, expenv.miny, delta=tol)
+ assert_almost_equal(env.maxx, expenv.maxx, delta=tol)
+ assert_almost_equal(env.maxy, expenv.maxy)
+ mm = mapnik.Map(256, 256)
+ style = mapnik.Style()
+ col = mapnik.RasterColorizer();
+ col.default_mode = mapnik.COLORIZER_DISCRETE;
+ col.add_stop(0, mapnik.Color(0x40,0x40,0x40,255));
+ col.add_stop(10, mapnik.Color(0x80,0x80,0x80,255));
+ col.add_stop(20, mapnik.Color(0xa0,0xa0,0xa0,255));
+ sym = mapnik.RasterSymbolizer()
+ sym.colorizer = col
+ rule = mapnik.Rule()
+ rule.symbols.append(sym)
+ style.rules.append(rule)
+ mm.append_style('foo', style)
+ lyr.styles.append('foo')
+ mm.layers.append(lyr)
+ mm.zoom_to_box(expenv)
+ im = mapnik.Image(mm.width, mm.height)
+ t0 = time.time() # we want wall time to include IO waits
+ mapnik.render(mm, im)
+ lap = time.time() - t0
+ log('T ' + str(lap) + ' -- ' + lbl + ' E:full')
+ # no data
+ eq_(im.view(1,1,1,1).tostring(), '\x00\x00\x00\x00')
+ eq_(im.view(255,255,1,1).tostring(), '\x00\x00\x00\x00')
+ eq_(im.view(195,116,1,1).tostring(), '\x00\x00\x00\x00')
+ # A0A0A0
+ eq_(im.view(100,120,1,1).tostring(), '\xa0\xa0\xa0\xff')
+ eq_(im.view( 75, 80,1,1).tostring(), '\xa0\xa0\xa0\xff')
+ # 808080
+ eq_(im.view( 74,170,1,1).tostring(), '\x80\x80\x80\xff')
+ eq_(im.view( 30, 50,1,1).tostring(), '\x80\x80\x80\xff')
+ # 404040
+ eq_(im.view(190, 70,1,1).tostring(), '\x40\x40\x40\xff')
+ eq_(im.view(140,170,1,1).tostring(), '\x40\x40\x40\xff')
+
+ # Now zoom over a portion of the env (1/10)
+ newenv = mapnik.Box2d(273663,4024478,330738,4072303)
+ mm.zoom_to_box(newenv)
+ t0 = time.time() # we want wall time to include IO waits
+ mapnik.render(mm, im)
+ lap = time.time() - t0
+ log('T ' + str(lap) + ' -- ' + lbl + ' E:1/10')
+ # nodata
+ eq_(hexlify(im.view(255,255,1,1).tostring()), '00000000')
+ eq_(hexlify(im.view(200,254,1,1).tostring()), '00000000')
+ # A0A0A0
+ eq_(hexlify(im.view(90,232,1,1).tostring()), 'a0a0a0ff')
+ eq_(hexlify(im.view(96,245,1,1).tostring()), 'a0a0a0ff')
+ # 808080
+ eq_(hexlify(im.view(1,1,1,1).tostring()), '808080ff')
+ eq_(hexlify(im.view(128,128,1,1).tostring()), '808080ff')
+ # 404040
+ eq_(hexlify(im.view(255, 0,1,1).tostring()), '404040ff')
+
+ def _test_dataraster_16bsi(lbl, tilesize, constraint, overview):
+ import_raster('../data/raster/dataraster-small.tif', 'dataRaster', tilesize, constraint, overview)
+ if constraint:
+ lbl += ' C'
+ if tilesize:
+ lbl += ' T:' + tilesize
+ if overview:
+ lbl += ' O:' + overview
+ for prescale in [0,1]:
+ for clip in [0,1]:
+ _test_dataraster_16bsi_rendering(lbl, overview, prescale, clip)
+ drop_imported('dataRaster', overview)
+
+ def test_dataraster_16bsi():
+ #for tilesize in ['','256x256']:
+ for tilesize in ['256x256']:
+ for constraint in [0,1]:
+ #for overview in ['','4','2,16']:
+ for overview in ['','2']:
+ _test_dataraster_16bsi('data_16bsi', tilesize, constraint, overview)
+
+ # river.tiff, RGBA 8BUI
+ def _test_rgba_8bui_rendering(lbl, overview, rescale, clip):
+ if rescale:
+ lbl += ' Sc'
+ if clip:
+ lbl += ' Cl'
+ ds = mapnik.PgRaster(dbname=MAPNIK_TEST_DBNAME,table='(select * from "River") foo',
+ use_overviews=1 if overview else 0,
+ prescale_rasters=rescale,clip_rasters=clip)
+ fs = ds.featureset()
+ feature = fs.next()
+ eq_(feature['rid'],1)
+ lyr = mapnik.Layer('rgba_8bui')
+ lyr.datasource = ds
+ expenv = mapnik.Box2d(0, -210, 256, 0)
+ env = lyr.envelope()
+ # As the input size is a prime number both horizontally
+ # and vertically, we expect the extent of the overview
+ # tables to be a pixel wider than the original, whereas
+ # the pixel size in geographical units depends on the
+ # overview factor. So we start with the original pixel size
+ # as base scale and multiply by the overview factor.
+ # NOTE: the overview table extent only grows north and east
+ pixsize = 1 # see gdalinfo river.tif
+ tol = pixsize * max(overview.split(',')) if overview else 0
+ assert_almost_equal(env.minx, expenv.minx)
+ assert_almost_equal(env.miny, expenv.miny, delta=tol)
+ assert_almost_equal(env.maxx, expenv.maxx, delta=tol)
+ assert_almost_equal(env.maxy, expenv.maxy)
+ mm = mapnik.Map(256, 256)
+ style = mapnik.Style()
+ sym = mapnik.RasterSymbolizer()
+ rule = mapnik.Rule()
+ rule.symbols.append(sym)
+ style.rules.append(rule)
+ mm.append_style('foo', style)
+ lyr.styles.append('foo')
+ mm.layers.append(lyr)
+ mm.zoom_to_box(expenv)
+ im = mapnik.Image(mm.width, mm.height)
+ t0 = time.time() # we want wall time to include IO waits
+ mapnik.render(mm, im)
+ lap = time.time() - t0
+ log('T ' + str(lap) + ' -- ' + lbl + ' E:full')
+ expected = 'images/support/pgraster/%s-%s-%s-%s-box1.png' % (lyr.name,lbl,overview,clip)
+ compare_images(expected,im)
+ # no data
+ eq_(hexlify(im.view(3,3,1,1).tostring()), '00000000')
+ eq_(hexlify(im.view(250,250,1,1).tostring()), '00000000')
+ # full opaque river color
+ eq_(hexlify(im.view(175,118,1,1).tostring()), 'b9d8f8ff')
+ # half-transparent pixel
+ pxstr = hexlify(im.view(122,138,1,1).tostring())
+ apat = ".*(..)$"
+ match = re.match(apat, pxstr)
+ assert match, 'pixel ' + pxstr + ' does not match pattern ' + apat
+ alpha = match.group(1)
+ assert alpha != 'ff' and alpha != '00', \
+ 'unexpected full transparent/opaque pixel: ' + alpha
+
+ # Now zoom over a portion of the env (1/10)
+ newenv = mapnik.Box2d(166,-105,191,-77)
+ mm.zoom_to_box(newenv)
+ t0 = time.time() # we want wall time to include IO waits
+ im = mapnik.Image(mm.width, mm.height)
+ mapnik.render(mm, im)
+ lap = time.time() - t0
+ log('T ' + str(lap) + ' -- ' + lbl + ' E:1/10')
+ expected = 'images/support/pgraster/%s-%s-%s-%s-box2.png' % (lyr.name,lbl,overview,clip)
+ compare_images(expected,im)
+ # no data
+ eq_(hexlify(im.view(255,255,1,1).tostring()), '00000000')
+ eq_(hexlify(im.view(200,40,1,1).tostring()), '00000000')
+ # full opaque river color
+ eq_(hexlify(im.view(100,168,1,1).tostring()), 'b9d8f8ff')
+ # half-transparent pixel
+ pxstr = hexlify(im.view(122,138,1,1).tostring())
+ apat = ".*(..)$"
+ match = re.match(apat, pxstr)
+ assert match, 'pixel ' + pxstr + ' does not match pattern ' + apat
+ alpha = match.group(1)
+ assert alpha != 'ff' and alpha != '00', \
+ 'unexpected full transparent/opaque pixel: ' + alpha
+
+ def _test_rgba_8bui(lbl, tilesize, constraint, overview):
+ import_raster('../data/raster/river.tiff', 'River', tilesize, constraint, overview)
+ if constraint:
+ lbl += ' C'
+ if tilesize:
+ lbl += ' T:' + tilesize
+ if overview:
+ lbl += ' O:' + overview
+ for prescale in [0,1]:
+ for clip in [0,1]:
+ _test_rgba_8bui_rendering(lbl, overview, prescale, clip)
+ drop_imported('River', overview)
+
+ def test_rgba_8bui():
+ for tilesize in ['','16x16']:
+ for constraint in [0,1]:
+ for overview in ['2']:
+ _test_rgba_8bui('rgba_8bui', tilesize, constraint, overview)
+
+ # nodata-edge.tif, RGB 8BUI
+ def _test_rgb_8bui_rendering(lbl, tnam, overview, rescale, clip):
+ if rescale:
+ lbl += ' Sc'
+ if clip:
+ lbl += ' Cl'
+ ds = mapnik.PgRaster(dbname=MAPNIK_TEST_DBNAME,table=tnam,
+ use_overviews=1 if overview else 0,
+ prescale_rasters=rescale,clip_rasters=clip)
+ fs = ds.featureset()
+ feature = fs.next()
+ eq_(feature['rid'],1)
+ lyr = mapnik.Layer('rgba_8bui')
+ lyr.datasource = ds
+ expenv = mapnik.Box2d(-12329035.7652168,4508650.39854396, \
+ -12328653.0279471,4508957.34625536)
+ env = lyr.envelope()
+ # As the input size is a prime number both horizontally
+ # and vertically, we expect the extent of the overview
+ # tables to be a pixel wider than the original, whereas
+ # the pixel size in geographical units depends on the
+ # overview factor. So we start with the original pixel size
+ # as base scale and multiply by the overview factor.
+ # NOTE: the overview table extent only grows north and east
+ pixsize = 2 # see gdalinfo nodata-edge.tif
+ tol = pixsize * max(overview.split(',')) if overview else 0
+ assert_almost_equal(env.minx, expenv.minx, places=0)
+ assert_almost_equal(env.miny, expenv.miny, delta=tol)
+ assert_almost_equal(env.maxx, expenv.maxx, delta=tol)
+ assert_almost_equal(env.maxy, expenv.maxy, places=0)
+ mm = mapnik.Map(256, 256)
+ style = mapnik.Style()
+ sym = mapnik.RasterSymbolizer()
+ rule = mapnik.Rule()
+ rule.symbols.append(sym)
+ style.rules.append(rule)
+ mm.append_style('foo', style)
+ lyr.styles.append('foo')
+ mm.layers.append(lyr)
+ mm.zoom_to_box(expenv)
+ im = mapnik.Image(mm.width, mm.height)
+ t0 = time.time() # we want wall time to include IO waits
+ mapnik.render(mm, im)
+ lap = time.time() - t0
+ log('T ' + str(lap) + ' -- ' + lbl + ' E:full')
+ expected = 'images/support/pgraster/%s-%s-%s-%s-%s-box1.png' % (lyr.name,tnam,lbl,overview,clip)
+ compare_images(expected,im)
+ # no data
+ eq_(hexlify(im.view(3,16,1,1).tostring()), '00000000')
+ eq_(hexlify(im.view(128,16,1,1).tostring()), '00000000')
+ eq_(hexlify(im.view(250,16,1,1).tostring()), '00000000')
+ eq_(hexlify(im.view(3,240,1,1).tostring()), '00000000')
+ eq_(hexlify(im.view(128,240,1,1).tostring()), '00000000')
+ eq_(hexlify(im.view(250,240,1,1).tostring()), '00000000')
+ # dark brown
+ eq_(hexlify(im.view(174,39,1,1).tostring()), 'c3a698ff')
+ # dark gray
+ eq_(hexlify(im.view(195,132,1,1).tostring()), '575f62ff')
+ # Now zoom over a portion of the env (1/10)
+ newenv = mapnik.Box2d(-12329035.7652168, 4508926.651484220, \
+ -12328997.49148983,4508957.34625536)
+ mm.zoom_to_box(newenv)
+ t0 = time.time() # we want wall time to include IO waits
+ im = mapnik.Image(mm.width, mm.height)
+ mapnik.render(mm, im)
+ lap = time.time() - t0
+ log('T ' + str(lap) + ' -- ' + lbl + ' E:1/10')
+ expected = 'images/support/pgraster/%s-%s-%s-%s-%s-box2.png' % (lyr.name,tnam,lbl,overview,clip)
+ compare_images(expected,im)
+ # no data
+ eq_(hexlify(im.view(3,16,1,1).tostring()), '00000000')
+ eq_(hexlify(im.view(128,16,1,1).tostring()), '00000000')
+ eq_(hexlify(im.view(250,16,1,1).tostring()), '00000000')
+ # black
+ eq_(hexlify(im.view(3,42,1,1).tostring()), '000000ff')
+ eq_(hexlify(im.view(3,134,1,1).tostring()), '000000ff')
+ eq_(hexlify(im.view(3,244,1,1).tostring()), '000000ff')
+ # gray
+ eq_(hexlify(im.view(135,157,1,1).tostring()), '4e555bff')
+ # brown
+ eq_(hexlify(im.view(195,223,1,1).tostring()), 'f2cdbaff')
+
+ def _test_rgb_8bui(lbl, tilesize, constraint, overview):
+ tnam = 'nodataedge'
+ import_raster('../data/raster/nodata-edge.tif', tnam, tilesize, constraint, overview)
+ if constraint:
+ lbl += ' C'
+ if tilesize:
+ lbl += ' T:' + tilesize
+ if overview:
+ lbl += ' O:' + overview
+ for prescale in [0,1]:
+ for clip in [0,1]:
+ _test_rgb_8bui_rendering(lbl, tnam, overview, prescale, clip)
+ #drop_imported(tnam, overview)
+
+ def test_rgb_8bui():
+ for tilesize in ['64x64']:
+ for constraint in [1]:
+ for overview in ['']:
+ _test_rgb_8bui('rgb_8bui', tilesize, constraint, overview)
+
+ def _test_grayscale_subquery(lbl,pixtype,value):
+ #
+ # 3 8 13
+ # +---+---+---+
+ # 3 | v | v | v | NOTE: writes different color
+ # +---+---+---+ in 13,8 and 8,13
+ # 8 | v | v | a |
+ # +---+---+---+
+ # 13 | v | b | v |
+ # +---+---+---+
+ #
+ val_a = value/3;
+ val_b = val_a*2;
+ sql = "(select 3 as i, " \
+ " ST_SetValues(" \
+ " ST_SetValues(" \
+ " ST_AsRaster(" \
+ " ST_MakeEnvelope(0,0,14,14), " \
+ " 1.0, -1.0, '%s', %s" \
+ " ), " \
+ " 11, 6, 4, 5, %s::float8" \
+ " )," \
+ " 6, 11, 5, 4, %s::float8" \
+ " ) as \"R\"" \
+ ") as foo" % (pixtype,value, val_a, val_b)
+ rescale = 0
+ clip = 0
+ if rescale:
+ lbl += ' Sc'
+ if clip:
+ lbl += ' Cl'
+ ds = mapnik.PgRaster(dbname=MAPNIK_TEST_DBNAME, table=sql,
+ raster_field='"R"', use_overviews=1,
+ prescale_rasters=rescale,clip_rasters=clip)
+ fs = ds.featureset()
+ feature = fs.next()
+ eq_(feature['i'],3)
+ lyr = mapnik.Layer('grayscale_subquery')
+ lyr.datasource = ds
+ expenv = mapnik.Box2d(0,0,14,14)
+ env = lyr.envelope()
+ assert_almost_equal(env.minx, expenv.minx, places=0)
+ assert_almost_equal(env.miny, expenv.miny, places=0)
+ assert_almost_equal(env.maxx, expenv.maxx, places=0)
+ assert_almost_equal(env.maxy, expenv.maxy, places=0)
+ mm = mapnik.Map(15, 15)
+ style = mapnik.Style()
+ sym = mapnik.RasterSymbolizer()
+ rule = mapnik.Rule()
+ rule.symbols.append(sym)
+ style.rules.append(rule)
+ mm.append_style('foo', style)
+ lyr.styles.append('foo')
+ mm.layers.append(lyr)
+ mm.zoom_to_box(expenv)
+ im = mapnik.Image(mm.width, mm.height)
+ t0 = time.time() # we want wall time to include IO waits
+ mapnik.render(mm, im)
+ lap = time.time() - t0
+ log('T ' + str(lap) + ' -- ' + lbl + ' E:full')
+ expected = 'images/support/pgraster/%s-%s-%s-%s.png' % (lyr.name,lbl,pixtype,value)
+ compare_images(expected,im)
+ h = format(value, '02x')
+ hex_v = h+h+h+'ff'
+ h = format(val_a, '02x')
+ hex_a = h+h+h+'ff'
+ h = format(val_b, '02x')
+ hex_b = h+h+h+'ff'
+ eq_(hexlify(im.view( 3, 3,1,1).tostring()), hex_v);
+ eq_(hexlify(im.view( 8, 3,1,1).tostring()), hex_v);
+ eq_(hexlify(im.view(13, 3,1,1).tostring()), hex_v);
+ eq_(hexlify(im.view( 3, 8,1,1).tostring()), hex_v);
+ eq_(hexlify(im.view( 8, 8,1,1).tostring()), hex_v);
+ eq_(hexlify(im.view(13, 8,1,1).tostring()), hex_a);
+ eq_(hexlify(im.view( 3,13,1,1).tostring()), hex_v);
+ eq_(hexlify(im.view( 8,13,1,1).tostring()), hex_b);
+ eq_(hexlify(im.view(13,13,1,1).tostring()), hex_v);
+
+ def test_grayscale_2bui_subquery():
+ _test_grayscale_subquery('grayscale_2bui_subquery', '2BUI', 3)
+
+ def test_grayscale_4bui_subquery():
+ _test_grayscale_subquery('grayscale_4bui_subquery', '4BUI', 15)
+
+ def test_grayscale_8bui_subquery():
+ _test_grayscale_subquery('grayscale_8bui_subquery', '8BUI', 63)
+
+ def test_grayscale_8bsi_subquery():
+ # NOTE: we're using a positive integer because Mapnik
+ # does not support negative data values anyway
+ _test_grayscale_subquery('grayscale_8bsi_subquery', '8BSI', 69)
+
+ def test_grayscale_16bui_subquery():
+ _test_grayscale_subquery('grayscale_16bui_subquery', '16BUI', 126)
+
+ def test_grayscale_16bsi_subquery():
+ # NOTE: we're using a positive integer because Mapnik
+ # does not support negative data values anyway
+ _test_grayscale_subquery('grayscale_16bsi_subquery', '16BSI', 144)
+
+ def test_grayscale_32bui_subquery():
+ _test_grayscale_subquery('grayscale_32bui_subquery', '32BUI', 255)
+
+ def test_grayscale_32bsi_subquery():
+ # NOTE: we're using a positive integer because Mapnik
+ # does not support negative data values anyway
+ _test_grayscale_subquery('grayscale_32bsi_subquery', '32BSI', 129)
+
+ def _test_data_subquery(lbl, pixtype, value):
+ #
+ # 3 8 13
+ # +---+---+---+
+ # 3 | v | v | v | NOTE: writes different values
+ # +---+---+---+ in 13,8 and 8,13
+ # 8 | v | v | a |
+ # +---+---+---+
+ # 13 | v | b | v |
+ # +---+---+---+
+ #
+ val_a = value/3;
+ val_b = val_a*2;
+ sql = "(select 3 as i, " \
+ " ST_SetValues(" \
+ " ST_SetValues(" \
+ " ST_AsRaster(" \
+ " ST_MakeEnvelope(0,0,14,14), " \
+ " 1.0, -1.0, '%s', %s" \
+ " ), " \
+ " 11, 6, 5, 5, %s::float8" \
+ " )," \
+ " 6, 11, 5, 5, %s::float8" \
+ " ) as \"R\"" \
+ ") as foo" % (pixtype,value, val_a, val_b)
+ overview = ''
+ rescale = 0
+ clip = 0
+ if rescale:
+ lbl += ' Sc'
+ if clip:
+ lbl += ' Cl'
+ ds = mapnik.PgRaster(dbname=MAPNIK_TEST_DBNAME, table=sql,
+ raster_field='R', use_overviews=0 if overview else 0,
+ band=1, prescale_rasters=rescale, clip_rasters=clip)
+ fs = ds.featureset()
+ feature = fs.next()
+ eq_(feature['i'],3)
+ lyr = mapnik.Layer('data_subquery')
+ lyr.datasource = ds
+ expenv = mapnik.Box2d(0,0,14,14)
+ env = lyr.envelope()
+ assert_almost_equal(env.minx, expenv.minx, places=0)
+ assert_almost_equal(env.miny, expenv.miny, places=0)
+ assert_almost_equal(env.maxx, expenv.maxx, places=0)
+ assert_almost_equal(env.maxy, expenv.maxy, places=0)
+ mm = mapnik.Map(15, 15)
+ style = mapnik.Style()
+ col = mapnik.RasterColorizer();
+ col.default_mode = mapnik.COLORIZER_DISCRETE;
+ col.add_stop(val_a, mapnik.Color(0xff,0x00,0x00,255));
+ col.add_stop(val_b, mapnik.Color(0x00,0xff,0x00,255));
+ col.add_stop(value, mapnik.Color(0x00,0x00,0xff,255));
+ sym = mapnik.RasterSymbolizer()
+ sym.colorizer = col
+ rule = mapnik.Rule()
+ rule.symbols.append(sym)
+ style.rules.append(rule)
+ mm.append_style('foo', style)
+ lyr.styles.append('foo')
+ mm.layers.append(lyr)
+ mm.zoom_to_box(expenv)
+ im = mapnik.Image(mm.width, mm.height)
+ t0 = time.time() # we want wall time to include IO waits
+ mapnik.render(mm, im)
+ lap = time.time() - t0
+ log('T ' + str(lap) + ' -- ' + lbl + ' E:full')
+ expected = 'images/support/pgraster/%s-%s-%s-%s.png' % (lyr.name,lbl,pixtype,value)
+ compare_images(expected,im)
+
+ def test_data_2bui_subquery():
+ _test_data_subquery('data_2bui_subquery', '2BUI', 3)
+
+ def test_data_4bui_subquery():
+ _test_data_subquery('data_4bui_subquery', '4BUI', 15)
+
+ def test_data_8bui_subquery():
+ _test_data_subquery('data_8bui_subquery', '8BUI', 63)
+
+ def test_data_8bsi_subquery():
+ # NOTE: we're using a positive integer because Mapnik
+ # does not support negative data values anyway
+ _test_data_subquery('data_8bsi_subquery', '8BSI', 69)
+
+ def test_data_16bui_subquery():
+ _test_data_subquery('data_16bui_subquery', '16BUI', 126)
+
+ def test_data_16bsi_subquery():
+ # NOTE: we're using a positive integer because Mapnik
+ # does not support negative data values anyway
+ _test_data_subquery('data_16bsi_subquery', '16BSI', 135)
+
+ def test_data_32bui_subquery():
+ _test_data_subquery('data_32bui_subquery', '32BUI', 255)
+
+ def test_data_32bsi_subquery():
+ # NOTE: we're using a positive integer because Mapnik
+ # does not support negative data values anyway
+ _test_data_subquery('data_32bsi_subquery', '32BSI', 264)
+
+ def test_data_32bf_subquery():
+ _test_data_subquery('data_32bf_subquery', '32BF', 450)
+
+ def test_data_64bf_subquery():
+ _test_data_subquery('data_64bf_subquery', '64BF', 3072)
+
+ def _test_rgba_subquery(lbl, pixtype, r, g, b, a, g1, b1):
+ #
+ # 3 8 13
+ # +---+---+---+
+ # 3 | v | v | h | NOTE: writes different alpha
+ # +---+---+---+ in 13,8 and 8,13
+ # 8 | v | v | a |
+ # +---+---+---+
+ # 13 | v | b | v |
+ # +---+---+---+
+ #
+ sql = "(select 3 as i, " \
+ " ST_SetValues(" \
+ " ST_SetValues(" \
+ " ST_AddBand(" \
+ " ST_AddBand(" \
+ " ST_AddBand(" \
+ " ST_AsRaster(" \
+ " ST_MakeEnvelope(0,0,14,14), " \
+ " 1.0, -1.0, '%s', %s" \
+ " )," \
+ " '%s', %d::float" \
+ " ), " \
+ " '%s', %d::float" \
+ " ), " \
+ " '%s', %d::float" \
+ " ), " \
+ " 2, 11, 6, 4, 5, %s::float8" \
+ " )," \
+ " 3, 6, 11, 5, 4, %s::float8" \
+ " ) as r" \
+ ") as foo" % (pixtype, r, pixtype, g, pixtype, b, pixtype, a, g1, b1)
+ overview = ''
+ rescale = 0
+ clip = 0
+ if rescale:
+ lbl += ' Sc'
+ if clip:
+ lbl += ' Cl'
+ ds = mapnik.PgRaster(dbname=MAPNIK_TEST_DBNAME, table=sql,
+ raster_field='r', use_overviews=0 if overview else 0,
+ prescale_rasters=rescale, clip_rasters=clip)
+ fs = ds.featureset()
+ feature = fs.next()
+ eq_(feature['i'],3)
+ lyr = mapnik.Layer('rgba_subquery')
+ lyr.datasource = ds
+ expenv = mapnik.Box2d(0,0,14,14)
+ env = lyr.envelope()
+ assert_almost_equal(env.minx, expenv.minx, places=0)
+ assert_almost_equal(env.miny, expenv.miny, places=0)
+ assert_almost_equal(env.maxx, expenv.maxx, places=0)
+ assert_almost_equal(env.maxy, expenv.maxy, places=0)
+ mm = mapnik.Map(15, 15)
+ style = mapnik.Style()
+ sym = mapnik.RasterSymbolizer()
+ rule = mapnik.Rule()
+ rule.symbols.append(sym)
+ style.rules.append(rule)
+ mm.append_style('foo', style)
+ lyr.styles.append('foo')
+ mm.layers.append(lyr)
+ mm.zoom_to_box(expenv)
+ im = mapnik.Image(mm.width, mm.height)
+ t0 = time.time() # we want wall time to include IO waits
+ mapnik.render(mm, im)
+ lap = time.time() - t0
+ log('T ' + str(lap) + ' -- ' + lbl + ' E:full')
+ expected = 'images/support/pgraster/%s-%s-%s-%s-%s-%s-%s-%s-%s.png' % (lyr.name,lbl, pixtype, r, g, b, a, g1, b1)
+ compare_images(expected,im)
+ hex_v = format(r << 24 | g << 16 | b << 8 | a, '08x')
+ hex_a = format(r << 24 | g1 << 16 | b << 8 | a, '08x')
+ hex_b = format(r << 24 | g << 16 | b1 << 8 | a, '08x')
+ eq_(hexlify(im.view( 3, 3,1,1).tostring()), hex_v);
+ eq_(hexlify(im.view( 8, 3,1,1).tostring()), hex_v);
+ eq_(hexlify(im.view(13, 3,1,1).tostring()), hex_v);
+ eq_(hexlify(im.view( 3, 8,1,1).tostring()), hex_v);
+ eq_(hexlify(im.view( 8, 8,1,1).tostring()), hex_v);
+ eq_(hexlify(im.view(13, 8,1,1).tostring()), hex_a);
+ eq_(hexlify(im.view( 3,13,1,1).tostring()), hex_v);
+ eq_(hexlify(im.view( 8,13,1,1).tostring()), hex_b);
+ eq_(hexlify(im.view(13,13,1,1).tostring()), hex_v);
+
+ def test_rgba_8bui_subquery():
+ _test_rgba_subquery('rgba_8bui_subquery', '8BUI', 255, 0, 0, 255, 255, 255)
+
+ #def test_rgba_16bui_subquery():
+ # _test_rgba_subquery('rgba_16bui_subquery', '16BUI', 65535, 0, 0, 65535, 65535, 65535)
+
+ #def test_rgba_32bui_subquery():
+ # _test_rgba_subquery('rgba_32bui_subquery', '32BUI')
+
+ atexit.register(postgis_takedown)
+
+def enabled(tname):
+ enabled = len(sys.argv) < 2 or tname in sys.argv
+ if not enabled:
+ print "Skipping " + tname + " as not explicitly enabled"
+ return enabled
+
+if __name__ == "__main__":
+ setup()
+ fail = run_all(eval(x) for x in dir() if x.startswith("test_") and enabled(x))
+ exit(fail)
diff --git a/test/python_tests/pickling_test.py b/test/python_tests/pickling_test.py
new file mode 100644
index 0000000..7a3572d
--- /dev/null
+++ b/test/python_tests/pickling_test.py
@@ -0,0 +1,44 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import os
+from nose.tools import eq_
+from utilities import execution_path, run_all
+
+import mapnik, pickle
+
+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_color_pickle():
+ c = mapnik.Color('blue')
+
+ eq_(pickle.loads(pickle.dumps(c)), c)
+
+ c = mapnik.Color(0, 64, 128)
+
+ eq_(pickle.loads(pickle.dumps(c)), c)
+
+ c = mapnik.Color(0, 64, 128, 192)
+
+ eq_(pickle.loads(pickle.dumps(c)), c)
+
+def test_envelope_pickle():
+ e = mapnik.Box2d(100, 100, 200, 200)
+
+ eq_(pickle.loads(pickle.dumps(e)), e)
+
+def test_parameters_pickle():
+ params = mapnik.Parameters()
+ params.append(mapnik.Parameter('oh',str('yeah')))
+
+ params2 = pickle.loads(pickle.dumps(params,pickle.HIGHEST_PROTOCOL))
+
+ eq_(params[0][0],params2[0][0])
+ eq_(params[0][1],params2[0][1])
+
+if __name__ == "__main__":
+ setup()
+ exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/png_encoding_test.py b/test/python_tests/png_encoding_test.py
new file mode 100644
index 0000000..568edfd
--- /dev/null
+++ b/test/python_tests/png_encoding_test.py
@@ -0,0 +1,218 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import os, mapnik
+from nose.tools import eq_
+from utilities import execution_path, run_all
+
+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('.'))
+
+if mapnik.has_png():
+ tmp_dir = '/tmp/mapnik-png/'
+ if not os.path.exists(tmp_dir):
+ os.makedirs(tmp_dir)
+
+ opts = [
+ 'png32',
+ 'png32:t=0',
+ 'png8:m=o',
+ 'png8:m=o:c=1',
+ 'png8:m=o:t=0',
+ 'png8:m=o:c=1:t=0',
+ 'png8:m=o:t=1',
+ 'png8:m=o:t=2',
+ 'png8:m=h',
+ 'png8:m=h:c=1',
+ 'png8:m=h:t=0',
+ 'png8:m=h:c=1:t=0',
+ 'png8:m=h:t=1',
+ 'png8:m=h:t=2',
+ 'png32:e=miniz',
+ 'png8:e=miniz'
+ ]
+
+ # Todo - use itertools.product
+ #z_opts = range(1,9+1)
+ #t_opts = range(0,2+1)
+
+ def gen_filepath(name,format):
+ return os.path.join('images/support/encoding-opts',name+'-'+format.replace(":","+")+'.png')
+
+ generate = os.environ.get('UPDATE')
+
+ def test_expected_encodings():
+ # blank image
+ im = mapnik.Image(256,256)
+ for opt in opts:
+ expected = gen_filepath('solid',opt)
+ actual = os.path.join(tmp_dir,os.path.basename(expected))
+ if generate or not os.path.exists(expected):
+ print 'generating expected image %s' % expected
+ im.save(expected,opt)
+ else:
+ im.save(actual,opt)
+ eq_(mapnik.Image.open(actual).tostring('png32'),
+ mapnik.Image.open(expected).tostring('png32'),
+ '%s (actual) not == to %s (expected)' % (actual,expected))
+
+ # solid image
+ im.fill(mapnik.Color('green'))
+ for opt in opts:
+ expected = gen_filepath('blank',opt)
+ actual = os.path.join(tmp_dir,os.path.basename(expected))
+ if generate or not os.path.exists(expected):
+ print 'generating expected image %s' % expected
+ im.save(expected,opt)
+ else:
+ im.save(actual,opt)
+ eq_(mapnik.Image.open(actual).tostring('png32'),
+ mapnik.Image.open(expected).tostring('png32'),
+ '%s (actual) not == to %s (expected)' % (actual,expected))
+
+ # aerial
+ im = mapnik.Image.open('./images/support/transparency/aerial_rgba.png')
+ for opt in opts:
+ expected = gen_filepath('aerial_rgba',opt)
+ actual = os.path.join(tmp_dir,os.path.basename(expected))
+ if generate or not os.path.exists(expected):
+ print 'generating expected image %s' % expected
+ im.save(expected,opt)
+ else:
+ im.save(actual,opt)
+ eq_(mapnik.Image.open(actual).tostring('png32'),
+ mapnik.Image.open(expected).tostring('png32'),
+ '%s (actual) not == to %s (expected)' % (actual,expected))
+
+ def test_transparency_levels():
+ # create partial transparency image
+ im = mapnik.Image(256,256)
+ im.fill(mapnik.Color('rgba(255,255,255,.5)'))
+ c2 = mapnik.Color('rgba(255,255,0,.2)')
+ c3 = mapnik.Color('rgb(0,255,255)')
+ for y in range(0,im.height()/2):
+ for x in range(0,im.width()/2):
+ im.set_pixel(x,y,c2)
+ for y in range(im.height()/2,im.height()):
+ for x in range(im.width()/2,im.width()):
+ im.set_pixel(x,y,c3)
+
+ t0 = tmp_dir + 'white0.png'
+ t2 = tmp_dir + 'white2.png'
+ t1 = tmp_dir + 'white1.png'
+
+ # octree
+ format = 'png8:m=o:t=0'
+ im.save(t0,format)
+ im_in = mapnik.Image.open(t0)
+ t0_len = len(im_in.tostring(format))
+ eq_(t0_len,len(mapnik.Image.open('images/support/transparency/white0.png').tostring(format)))
+ format = 'png8:m=o:t=1'
+ im.save(t1,format)
+ im_in = mapnik.Image.open(t1)
+ t1_len = len(im_in.tostring(format))
+ eq_(len(im.tostring(format)),len(mapnik.Image.open('images/support/transparency/white1.png').tostring(format)))
+ format = 'png8:m=o:t=2'
+ im.save(t2,format)
+ im_in = mapnik.Image.open(t2)
+ t2_len = len(im_in.tostring(format))
+ eq_(len(im.tostring(format)),len(mapnik.Image.open('images/support/transparency/white2.png').tostring(format)))
+
+ eq_(t0_len < t1_len < t2_len,True)
+
+ # hextree
+ format = 'png8:m=h:t=0'
+ im.save(t0,format)
+ im_in = mapnik.Image.open(t0)
+ t0_len = len(im_in.tostring(format))
+ eq_(t0_len,len(mapnik.Image.open('images/support/transparency/white0.png').tostring(format)))
+ format = 'png8:m=h:t=1'
+ im.save(t1,format)
+ im_in = mapnik.Image.open(t1)
+ t1_len = len(im_in.tostring(format))
+ eq_(len(im.tostring(format)),len(mapnik.Image.open('images/support/transparency/white1.png').tostring(format)))
+ format = 'png8:m=h:t=2'
+ im.save(t2,format)
+ im_in = mapnik.Image.open(t2)
+ t2_len = len(im_in.tostring(format))
+ eq_(len(im.tostring(format)),len(mapnik.Image.open('images/support/transparency/white2.png').tostring(format)))
+
+ eq_(t0_len < t1_len < t2_len,True)
+
+ def test_transparency_levels_aerial():
+ im = mapnik.Image.open('../data/images/12_654_1580.png')
+ im_in = mapnik.Image.open('./images/support/transparency/aerial_rgba.png')
+ eq_(len(im.tostring('png8')),len(im_in.tostring('png8')))
+ eq_(len(im.tostring('png32')),len(im_in.tostring('png32')))
+
+ im_in = mapnik.Image.open('./images/support/transparency/aerial_rgb.png')
+ eq_(len(im.tostring('png32')),len(im_in.tostring('png32')))
+ eq_(len(im.tostring('png32:t=0')),len(im_in.tostring('png32:t=0')))
+ eq_(len(im.tostring('png32:t=0')) == len(im_in.tostring('png32')), False)
+ eq_(len(im.tostring('png8')),len(im_in.tostring('png8')))
+ eq_(len(im.tostring('png8:t=0')),len(im_in.tostring('png8:t=0')))
+ # unlike png32 paletted images without alpha will look the same even if no alpha is forced
+ eq_(len(im.tostring('png8:t=0')) == len(im_in.tostring('png8')), True)
+ eq_(len(im.tostring('png8:t=0:m=o')) == len(im_in.tostring('png8:m=o')), True)
+
+ def test_9_colors_hextree():
+ expected = './images/support/encoding-opts/png8-9cols.png'
+ im = mapnik.Image.open(expected)
+ t0 = tmp_dir + 'png-encoding-9-colors.result-hextree.png'
+ im.save(t0, 'png8:m=h')
+ eq_(mapnik.Image.open(t0).tostring(),
+ mapnik.Image.open(expected).tostring(),
+ '%s (actual) not == to %s (expected)' % (t0, expected))
+
+ def test_9_colors_octree():
+ expected = './images/support/encoding-opts/png8-9cols.png'
+ im = mapnik.Image.open(expected)
+ t0 = tmp_dir + 'png-encoding-9-colors.result-octree.png'
+ im.save(t0, 'png8:m=o')
+ eq_(mapnik.Image.open(t0).tostring(),
+ mapnik.Image.open(expected).tostring(),
+ '%s (actual) not == to %s (expected)' % (t0, expected))
+
+ def test_17_colors_hextree():
+ expected = './images/support/encoding-opts/png8-17cols.png'
+ im = mapnik.Image.open(expected)
+ t0 = tmp_dir + 'png-encoding-17-colors.result-hextree.png'
+ im.save(t0, 'png8:m=h')
+ eq_(mapnik.Image.open(t0).tostring(),
+ mapnik.Image.open(expected).tostring(),
+ '%s (actual) not == to %s (expected)' % (t0, expected))
+
+ def test_17_colors_octree():
+ expected = './images/support/encoding-opts/png8-17cols.png'
+ im = mapnik.Image.open(expected)
+ t0 = tmp_dir + 'png-encoding-17-colors.result-octree.png'
+ im.save(t0, 'png8:m=o')
+ eq_(mapnik.Image.open(t0).tostring(),
+ mapnik.Image.open(expected).tostring(),
+ '%s (actual) not == to %s (expected)' % (t0, expected))
+
+ def test_2px_regression_hextree():
+ im = mapnik.Image.open('./images/support/encoding-opts/png8-2px.A.png')
+ expected = './images/support/encoding-opts/png8-2px.png'
+
+ t0 = tmp_dir + 'png-encoding-2px.result-hextree.png'
+ im.save(t0, 'png8:m=h')
+ eq_(mapnik.Image.open(t0).tostring(),
+ mapnik.Image.open(expected).tostring(),
+ '%s (actual) not == to %s (expected)' % (t0, expected))
+
+ def test_2px_regression_octree():
+ im = mapnik.Image.open('./images/support/encoding-opts/png8-2px.A.png')
+ expected = './images/support/encoding-opts/png8-2px.png'
+ t0 = tmp_dir + 'png-encoding-2px.result-octree.png'
+ im.save(t0, 'png8:m=o')
+ eq_(mapnik.Image.open(t0).tostring(),
+ mapnik.Image.open(expected).tostring(),
+ '%s (actual) not == to %s (expected)' % (t0, expected))
+
+
+if __name__ == "__main__":
+ setup()
+ exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/pngsuite_test.py b/test/python_tests/pngsuite_test.py
new file mode 100644
index 0000000..4c933eb
--- /dev/null
+++ b/test/python_tests/pngsuite_test.py
@@ -0,0 +1,35 @@
+#!/usr/bin/env python
+
+import os
+import mapnik
+from nose.tools import assert_raises
+from utilities import execution_path, run_all
+
+datadir = '../data/pngsuite'
+
+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 assert_broken_file(fname):
+ assert_raises(RuntimeError, lambda: mapnik.Image.open(fname))
+
+def assert_good_file(fname):
+ assert mapnik.Image.open(fname)
+
+def get_pngs(good):
+ files = [ x for x in os.listdir(datadir) if x.endswith('.png') ]
+ return [ os.path.join(datadir, x) for x in files if good != x.startswith('x') ]
+
+def test_good_pngs():
+ for x in get_pngs(True):
+ yield assert_good_file, x
+
+def test_broken_pngs():
+ for x in get_pngs(False):
+ yield assert_broken_file, x
+
+if __name__ == "__main__":
+ setup()
+ exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/postgis_test.py b/test/python_tests/postgis_test.py
new file mode 100644
index 0000000..42e40cc
--- /dev/null
+++ b/test/python_tests/postgis_test.py
@@ -0,0 +1,1177 @@
+#!/usr/bin/env python
+
+from nose.tools import eq_,raises
+import atexit
+from utilities import execution_path, run_all
+from subprocess import Popen, PIPE
+import os, mapnik
+import threading
+
+
+MAPNIK_TEST_DBNAME = 'mapnik-tmp-postgis-test-db'
+POSTGIS_TEMPLATE_DBNAME = 'template_postgis'
+SHAPEFILE = os.path.join(execution_path('.'),'../data/shp/world_merc.shp')
+
+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 call(cmd,silent=False):
+ stdin, stderr = Popen(cmd, shell=True, stdout=PIPE, stderr=PIPE).communicate()
+ if not stderr:
+ return stdin.strip()
+ elif not silent and 'error' in stderr.lower() \
+ or 'not found' in stderr.lower() \
+ or 'could not connect' in stderr.lower() \
+ or 'bad connection' in stderr.lower() \
+ or 'not recognized as an internal' in stderr.lower():
+ raise RuntimeError(stderr.strip())
+
+def psql_can_connect():
+ """Test ability to connect to a postgis template db with no options.
+
+ Basically, to run these tests your user must have full read
+ access over unix sockets without supplying a password. This
+ keeps these tests simple and focused on postgis not on postgres
+ auth issues.
+ """
+ try:
+ call('psql %s -c "select postgis_version()"' % POSTGIS_TEMPLATE_DBNAME)
+ return True
+ except RuntimeError:
+ print 'Notice: skipping postgis tests (connection)'
+ return False
+
+def shp2pgsql_on_path():
+ """Test for presence of shp2pgsql on the user path.
+
+ We require this program to load test data into a temporarily database.
+ """
+ try:
+ call('shp2pgsql')
+ return True
+ except RuntimeError:
+ print 'Notice: skipping postgis tests (shp2pgsql)'
+ return False
+
+def createdb_and_dropdb_on_path():
+ """Test for presence of dropdb/createdb on user path.
+
+ We require these programs to setup and teardown the testing db.
+ """
+ try:
+ call('createdb --help')
+ call('dropdb --help')
+ return True
+ except RuntimeError:
+ print 'Notice: skipping postgis tests (createdb/dropdb)'
+ return False
+
+insert_table_1 = """
+CREATE TABLE test(gid serial PRIMARY KEY, geom geometry);
+INSERT INTO test(geom) values (GeomFromEWKT('SRID=4326;POINT(0 0)'));
+INSERT INTO test(geom) values (GeomFromEWKT('SRID=4326;POINT(-2 2)'));
+INSERT INTO test(geom) values (GeomFromEWKT('SRID=4326;MULTIPOINT(2 1,1 2)'));
+INSERT INTO test(geom) values (GeomFromEWKT('SRID=4326;LINESTRING(0 0,1 1,1 2)'));
+INSERT INTO test(geom) values (GeomFromEWKT('SRID=4326;MULTILINESTRING((1 0,0 1,3 2),(3 2,5 4))'));
+INSERT INTO test(geom) values (GeomFromEWKT('SRID=4326;POLYGON((0 0,4 0,4 4,0 4,0 0),(1 1, 2 1, 2 2, 1 2,1 1))'));
+INSERT INTO test(geom) values (GeomFromEWKT('SRID=4326;MULTIPOLYGON(((1 1,3 1,3 3,1 3,1 1),(1 1,2 1,2 2,1 2,1 1)), ((-1 -1,-1 -2,-2 -2,-2 -1,-1 -1)))'));
+INSERT INTO test(geom) values (GeomFromEWKT('SRID=4326;GEOMETRYCOLLECTION(POLYGON((1 1, 2 1, 2 2, 1 2,1 1)),POINT(2 3),LINESTRING(2 3,3 4))'));
+"""
+
+insert_table_2 = """
+CREATE TABLE test2(manual_id int4 PRIMARY KEY, geom geometry);
+INSERT INTO test2(manual_id, geom) values (0, GeomFromEWKT('SRID=4326;POINT(0 0)'));
+INSERT INTO test2(manual_id, geom) values (1, GeomFromEWKT('SRID=4326;POINT(0 0)'));
+INSERT INTO test2(manual_id, geom) values (1000, GeomFromEWKT('SRID=4326;POINT(0 0)'));
+INSERT INTO test2(manual_id, geom) values (-1000, GeomFromEWKT('SRID=4326;POINT(0 0)'));
+INSERT INTO test2(manual_id, geom) values (2147483647, GeomFromEWKT('SRID=4326;POINT(0 0)'));
+INSERT INTO test2(manual_id, geom) values (-2147483648, GeomFromEWKT('SRID=4326;POINT(0 0)'));
+"""
+
+insert_table_3 = """
+CREATE TABLE test3(non_id bigint, manual_id int4, geom geometry);
+INSERT INTO test3(non_id, manual_id, geom) values (9223372036854775807, 0, GeomFromEWKT('SRID=4326;POINT(0 0)'));
+INSERT INTO test3(non_id, manual_id, geom) values (9223372036854775807, 1, GeomFromEWKT('SRID=4326;POINT(0 0)'));
+INSERT INTO test3(non_id, manual_id, geom) values (9223372036854775807, 1000, GeomFromEWKT('SRID=4326;POINT(0 0)'));
+INSERT INTO test3(non_id, manual_id, geom) values (9223372036854775807, -1000, GeomFromEWKT('SRID=4326;POINT(0 0)'));
+INSERT INTO test3(non_id, manual_id, geom) values (9223372036854775807, 2147483647, GeomFromEWKT('SRID=4326;POINT(0 0)'));
+INSERT INTO test3(non_id, manual_id, geom) values (9223372036854775807, -2147483648, GeomFromEWKT('SRID=4326;POINT(0 0)'));
+"""
+
+insert_table_4 = """
+CREATE TABLE test4(non_id int4, manual_id int8 PRIMARY KEY, geom geometry);
+INSERT INTO test4(non_id, manual_id, geom) values (0, 0, GeomFromEWKT('SRID=4326;POINT(0 0)'));
+INSERT INTO test4(non_id, manual_id, geom) values (0, 1, GeomFromEWKT('SRID=4326;POINT(0 0)'));
+INSERT INTO test4(non_id, manual_id, geom) values (0, 1000, GeomFromEWKT('SRID=4326;POINT(0 0)'));
+INSERT INTO test4(non_id, manual_id, geom) values (0, -1000, GeomFromEWKT('SRID=4326;POINT(0 0)'));
+INSERT INTO test4(non_id, manual_id, geom) values (0, 2147483647, GeomFromEWKT('SRID=4326;POINT(0 0)'));
+INSERT INTO test4(non_id, manual_id, geom) values (0, -2147483648, GeomFromEWKT('SRID=4326;POINT(0 0)'));
+"""
+
+insert_table_5 = """
+CREATE TABLE test5(non_id int4, manual_id numeric PRIMARY KEY, geom geometry);
+INSERT INTO test5(non_id, manual_id, geom) values (0, -1, GeomFromEWKT('SRID=4326;POINT(0 0)'));
+INSERT INTO test5(non_id, manual_id, geom) values (0, 1, GeomFromEWKT('SRID=4326;POINT(0 0)'));
+"""
+
+insert_table_5b = '''
+CREATE TABLE "tableWithMixedCase"(gid serial PRIMARY KEY, geom geometry);
+INSERT INTO "tableWithMixedCase"(geom) values (ST_MakePoint(0,0));
+INSERT INTO "tableWithMixedCase"(geom) values (ST_MakePoint(0,1));
+INSERT INTO "tableWithMixedCase"(geom) values (ST_MakePoint(1,0));
+INSERT INTO "tableWithMixedCase"(geom) values (ST_MakePoint(1,1));
+'''
+
+insert_table_6 = '''
+CREATE TABLE test6(first_id int4, second_id int4,PRIMARY KEY (first_id,second_id), geom geometry);
+INSERT INTO test6(first_id, second_id, geom) values (0, 0, GeomFromEWKT('SRID=4326;POINT(0 0)'));
+'''
+
+insert_table_7 = '''
+CREATE TABLE test7(gid serial PRIMARY KEY, geom geometry);
+INSERT INTO test7(gid, geom) values (1, GeomFromEWKT('SRID=4326;GEOMETRYCOLLECTION(MULTILINESTRING((10 10,20 20,10 40),(40 40,30 30,40 20,30 10)),LINESTRING EMPTY)'));
+'''
+
+insert_table_8 = '''
+CREATE TABLE test8(gid serial PRIMARY KEY,int_field bigint, geom geometry);
+INSERT INTO test8(gid, int_field, geom) values (1, 2147483648, ST_MakePoint(1,1));
+INSERT INTO test8(gid, int_field, geom) values (2, 922337203685477580, ST_MakePoint(1,1));
+'''
+
+insert_table_9 = '''
+CREATE TABLE test9(gid serial PRIMARY KEY, name varchar, geom geometry);
+INSERT INTO test9(gid, name, geom) values (1, 'name', ST_MakePoint(1,1));
+INSERT INTO test9(gid, name, geom) values (2, '', ST_MakePoint(1,1));
+INSERT INTO test9(gid, name, geom) values (3, null, ST_MakePoint(1,1));
+'''
+
+insert_table_10 = '''
+CREATE TABLE test10(gid serial PRIMARY KEY, bool_field boolean, geom geometry);
+INSERT INTO test10(gid, bool_field, geom) values (1, TRUE, ST_MakePoint(1,1));
+INSERT INTO test10(gid, bool_field, geom) values (2, FALSE, ST_MakePoint(1,1));
+INSERT INTO test10(gid, bool_field, geom) values (3, null, ST_MakePoint(1,1));
+'''
+
+insert_table_11 = """
+CREATE TABLE test11(gid serial PRIMARY KEY, label varchar(40), geom geometry);
+INSERT INTO test11(label,geom) values ('label_1',GeomFromEWKT('SRID=4326;POINT(0 0)'));
+INSERT INTO test11(label,geom) values ('label_2',GeomFromEWKT('SRID=4326;POINT(-2 2)'));
+INSERT INTO test11(label,geom) values ('label_3',GeomFromEWKT('SRID=4326;MULTIPOINT(2 1,1 2)'));
+INSERT INTO test11(label,geom) values ('label_4',GeomFromEWKT('SRID=4326;LINESTRING(0 0,1 1,1 2)'));
+INSERT INTO test11(label,geom) values ('label_5',GeomFromEWKT('SRID=4326;MULTILINESTRING((1 0,0 1,3 2),(3 2,5 4))'));
+INSERT INTO test11(label,geom) values ('label_6',GeomFromEWKT('SRID=4326;POLYGON((0 0,4 0,4 4,0 4,0 0),(1 1, 2 1, 2 2, 1 2,1 1))'));
+INSERT INTO test11(label,geom) values ('label_7',GeomFromEWKT('SRID=4326;MULTIPOLYGON(((1 1,3 1,3 3,1 3,1 1),(1 1,2 1,2 2,1 2,1 1)), ((-1 -1,-1 -2,-2 -2,-2 -1,-1 -1)))'));
+INSERT INTO test11(label,geom) values ('label_8',GeomFromEWKT('SRID=4326;GEOMETRYCOLLECTION(POLYGON((1 1, 2 1, 2 2, 1 2,1 1)),POINT(2 3),LINESTRING(2 3,3 4))'));
+"""
+
+insert_table_12 = """
+CREATE TABLE test12(gid serial PRIMARY KEY, name varchar(40), geom geometry);
+INSERT INTO test12(name,geom) values ('Point',GeomFromEWKT('SRID=4326;POINT(0 0)'));
+INSERT INTO test12(name,geom) values ('PointZ',GeomFromEWKT('SRID=4326;POINTZ(0 0 0)'));
+INSERT INTO test12(name,geom) values ('PointM',GeomFromEWKT('SRID=4326;POINTM(0 0 0)'));
+INSERT INTO test12(name,geom) values ('PointZM',GeomFromEWKT('SRID=4326;POINTZM(0 0 0 0)'));
+INSERT INTO test12(name,geom) values ('MultiPoint',GeomFromEWKT('SRID=4326;MULTIPOINT(0 0, 1 1)'));
+INSERT INTO test12(name,geom) values ('MultiPointZ',GeomFromEWKT('SRID=4326;MULTIPOINTZ(0 0 0, 1 1 1)'));
+INSERT INTO test12(name,geom) values ('MultiPointM',GeomFromEWKT('SRID=4326;MULTIPOINTM(0 0 0, 1 1 1)'));
+INSERT INTO test12(name,geom) values ('MultiPointZM',GeomFromEWKT('SRID=4326;MULTIPOINTZM(0 0 0 0, 1 1 1 1)'));
+INSERT INTO test12(name,geom) values ('LineString',GeomFromEWKT('SRID=4326;LINESTRING(0 0, 1 1)'));
+INSERT INTO test12(name,geom) values ('LineStringZ',GeomFromEWKT('SRID=4326;LINESTRINGZ(0 0 0, 1 1 1)'));
+INSERT INTO test12(name,geom) values ('LineStringM',GeomFromEWKT('SRID=4326;LINESTRINGM(0 0 0, 1 1 1)'));
+INSERT INTO test12(name,geom) values ('LineStringZM',GeomFromEWKT('SRID=4326;LINESTRINGZM(0 0 0 0, 1 1 1 1)'));
+INSERT INTO test12(name,geom) values ('Polygon',GeomFromEWKT('SRID=4326;POLYGON((0 0, 1 1, 2 2, 0 0))'));
+INSERT INTO test12(name,geom) values ('PolygonZ',GeomFromEWKT('SRID=4326;POLYGONZ((0 0 0, 1 1 1, 2 2 2, 0 0 0))'));
+INSERT INTO test12(name,geom) values ('PolygonM',GeomFromEWKT('SRID=4326;POLYGONZ((0 0 0, 1 1 1, 2 2 2, 0 0 0))'));
+INSERT INTO test12(name,geom) values ('PolygonZM',GeomFromEWKT('SRID=4326;POLYGONZM((0 0 0 0, 1 1 1 1, 2 2 2 2, 0 0 0 0))'));
+INSERT INTO test12(name,geom) values ('MultiLineString',GeomFromEWKT('SRID=4326;MULTILINESTRING((0 0, 1 1),(2 2, 3 3))'));
+INSERT INTO test12(name,geom) values ('MultiLineStringZ',GeomFromEWKT('SRID=4326;MULTILINESTRINGZ((0 0 0, 1 1 1),(2 2 2, 3 3 3))'));
+INSERT INTO test12(name,geom) values ('MultiLineStringM',GeomFromEWKT('SRID=4326;MULTILINESTRINGM((0 0 0, 1 1 1),(2 2 2, 3 3 3))'));
+INSERT INTO test12(name,geom) values ('MultiLineStringZM',GeomFromEWKT('SRID=4326;MULTILINESTRINGZM((0 0 0 0, 1 1 1 1),(2 2 2 2, 3 3 3 3))'));
+INSERT INTO test12(name,geom) values ('MultiPolygon',GeomFromEWKT('SRID=4326;MULTIPOLYGON(((0 0, 1 1, 2 2, 0 0)),((0 0, 1 1, 2 2, 0 0)))'));
+INSERT INTO test12(name,geom) values ('MultiPolygonZ',GeomFromEWKT('SRID=4326;MULTIPOLYGONZ(((0 0 0, 1 1 1, 2 2 2, 0 0 0)),((0 0 0, 1 1 1, 2 2 2, 0 0 0)))'));
+INSERT INTO test12(name,geom) values ('MultiPolygonM',GeomFromEWKT('SRID=4326;MULTIPOLYGONM(((0 0 0, 1 1 1, 2 2 2, 0 0 0)),((0 0 0, 1 1 1, 2 2 2, 0 0 0)))'));
+INSERT INTO test12(name,geom) values ('MultiPolygonZM',GeomFromEWKT('SRID=4326;MULTIPOLYGONZM(((0 0 0 0, 1 1 1 1, 2 2 2 2, 0 0 0 0)),((0 0 0 0, 1 1 1 1, 2 2 2 2, 0 0 0 0)))'));
+"""
+
+
+def postgis_setup():
+ call('dropdb %s' % MAPNIK_TEST_DBNAME,silent=True)
+ call('createdb -T %s %s' % (POSTGIS_TEMPLATE_DBNAME,MAPNIK_TEST_DBNAME),silent=False)
+ call('shp2pgsql -s 3857 -g geom -W LATIN1 %s world_merc | psql -q %s' % (SHAPEFILE,MAPNIK_TEST_DBNAME), silent=True)
+ call('''psql -q %s -c "CREATE TABLE \"empty\" (key serial);SELECT AddGeometryColumn('','empty','geom','-1','GEOMETRY',4);"''' % MAPNIK_TEST_DBNAME,silent=False)
+ call('''psql -q %s -c "%s"''' % (MAPNIK_TEST_DBNAME,insert_table_1),silent=False)
+ call('''psql -q %s -c "%s"''' % (MAPNIK_TEST_DBNAME,insert_table_2),silent=False)
+ call('''psql -q %s -c "%s"''' % (MAPNIK_TEST_DBNAME,insert_table_3),silent=False)
+ call('''psql -q %s -c "%s"''' % (MAPNIK_TEST_DBNAME,insert_table_4),silent=False)
+ call('''psql -q %s -c "%s"''' % (MAPNIK_TEST_DBNAME,insert_table_5),silent=False)
+ call("""psql -q %s -c '%s'""" % (MAPNIK_TEST_DBNAME,insert_table_5b),silent=False)
+ call('''psql -q %s -c "%s"''' % (MAPNIK_TEST_DBNAME,insert_table_6),silent=False)
+ call('''psql -q %s -c "%s"''' % (MAPNIK_TEST_DBNAME,insert_table_7),silent=False)
+ call('''psql -q %s -c "%s"''' % (MAPNIK_TEST_DBNAME,insert_table_8),silent=False)
+ call('''psql -q %s -c "%s"''' % (MAPNIK_TEST_DBNAME,insert_table_9),silent=False)
+ call('''psql -q %s -c "%s"''' % (MAPNIK_TEST_DBNAME,insert_table_10),silent=False)
+ call('''psql -q %s -c "%s"''' % (MAPNIK_TEST_DBNAME,insert_table_11),silent=False)
+ call('''psql -q %s -c "%s"''' % (MAPNIK_TEST_DBNAME,insert_table_12),silent=False)
+
+def postgis_takedown():
+ pass
+ # fails as the db is in use: https://github.com/mapnik/mapnik/issues/960
+ #call('dropdb %s' % MAPNIK_TEST_DBNAME)
+
+if 'postgis' in mapnik.DatasourceCache.plugin_names() \
+ and createdb_and_dropdb_on_path() \
+ and psql_can_connect() \
+ and shp2pgsql_on_path():
+
+ # initialize test database
+ postgis_setup()
+
+ def test_feature():
+ ds = mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,table='world_merc')
+ fs = ds.featureset()
+ feature = fs.next()
+ eq_(feature['gid'],1)
+ eq_(feature['fips'],u'AC')
+ eq_(feature['iso2'],u'AG')
+ eq_(feature['iso3'],u'ATG')
+ eq_(feature['un'],28)
+ eq_(feature['name'],u'Antigua and Barbuda')
+ eq_(feature['area'],44)
+ eq_(feature['pop2005'],83039)
+ eq_(feature['region'],19)
+ eq_(feature['subregion'],29)
+ eq_(feature['lon'],-61.783)
+ eq_(feature['lat'],17.078)
+ meta = ds.describe()
+ eq_(meta['srid'],3857)
+ eq_(meta.get('key_field'),None)
+ eq_(meta['encoding'],u'UTF8')
+ eq_(meta['geometry_type'],mapnik.DataGeometryType.Polygon)
+
+ def test_subquery():
+ ds = mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,table='(select * from world_merc) as w')
+ fs = ds.featureset()
+ feature = fs.next()
+ eq_(feature['gid'],1)
+ eq_(feature['fips'],u'AC')
+ eq_(feature['iso2'],u'AG')
+ eq_(feature['iso3'],u'ATG')
+ eq_(feature['un'],28)
+ eq_(feature['name'],u'Antigua and Barbuda')
+ eq_(feature['area'],44)
+ eq_(feature['pop2005'],83039)
+ eq_(feature['region'],19)
+ eq_(feature['subregion'],29)
+ eq_(feature['lon'],-61.783)
+ eq_(feature['lat'],17.078)
+ meta = ds.describe()
+ eq_(meta['srid'],3857)
+ eq_(meta.get('key_field'),None)
+ eq_(meta['encoding'],u'UTF8')
+ eq_(meta['geometry_type'],mapnik.DataGeometryType.Polygon)
+
+ ds = mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,table='(select gid,geom,fips as _fips from world_merc) as w')
+ fs = ds.featureset()
+ feature = fs.next()
+ eq_(feature['gid'],1)
+ eq_(feature['_fips'],u'AC')
+ eq_(len(feature),2)
+ meta = ds.describe()
+ eq_(meta['srid'],3857)
+ eq_(meta.get('key_field'),None)
+ eq_(meta['encoding'],u'UTF8')
+ eq_(meta['geometry_type'],mapnik.DataGeometryType.Polygon)
+
+ def test_bad_connection():
+ try:
+ ds = mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,
+ table='test',
+ max_size=20,
+ geometry_field='geom',
+ user="rolethatdoesnotexist")
+ except Exception, e:
+ assert 'role "rolethatdoesnotexist" does not exist' in str(e)
+
+ def test_empty_db():
+ ds = mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,table='empty')
+ fs = ds.featureset()
+ feature = None
+ try:
+ feature = fs.next()
+ except StopIteration:
+ pass
+ eq_(feature,None)
+ meta = ds.describe()
+ eq_(meta['srid'],-1)
+ eq_(meta.get('key_field'),None)
+ eq_(meta['encoding'],u'UTF8')
+ eq_(meta['geometry_type'],None)
+
+ def test_manual_srid():
+ ds = mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,srid=99, table='empty')
+ fs = ds.featureset()
+ feature = None
+ try:
+ feature = fs.next()
+ except StopIteration:
+ pass
+ eq_(feature,None)
+ meta = ds.describe()
+ eq_(meta['srid'],99)
+ eq_(meta.get('key_field'),None)
+ eq_(meta['encoding'],u'UTF8')
+ eq_(meta['geometry_type'],None)
+
+ def test_geometry_detection():
+ ds = mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,table='test',
+ geometry_field='geom')
+ meta = ds.describe()
+ eq_(meta['srid'],4326)
+ eq_(meta.get('key_field'),None)
+ eq_(meta['geometry_type'],mapnik.DataGeometryType.Collection)
+
+ # will fail with postgis 2.0 because it automatically adds a geometry_columns entry
+ #ds = mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,table='test',
+ # geometry_field='geom',
+ # row_limit=1)
+ #eq_(ds.describe()['geometry_type'],mapnik.DataGeometryType.Point)
+
+ @raises(RuntimeError)
+ def test_that_nonexistant_query_field_throws(**kwargs):
+ ds = mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,table='empty')
+ eq_(len(ds.fields()),1)
+ eq_(ds.fields(),['key'])
+ eq_(ds.field_types(),['int'])
+ query = mapnik.Query(ds.envelope())
+ for fld in ds.fields():
+ query.add_property_name(fld)
+ # also add an invalid one, triggering throw
+ query.add_property_name('bogus')
+ ds.features(query)
+
+ def test_auto_detection_of_unique_feature_id_32_bit():
+ ds = mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,table='test2',
+ geometry_field='geom',
+ autodetect_key_field=True)
+ fs = ds.featureset()
+ eq_(fs.next()['manual_id'],0)
+ eq_(fs.next()['manual_id'],1)
+ eq_(fs.next()['manual_id'],1000)
+ eq_(fs.next()['manual_id'],-1000)
+ eq_(fs.next()['manual_id'],2147483647)
+ eq_(fs.next()['manual_id'],-2147483648)
+
+ fs = ds.featureset()
+ eq_(fs.next().id(),0)
+ eq_(fs.next().id(),1)
+ eq_(fs.next().id(),1000)
+ eq_(fs.next().id(),-1000)
+ eq_(fs.next().id(),2147483647)
+ eq_(fs.next().id(),-2147483648)
+ meta = ds.describe()
+ eq_(meta['srid'],4326)
+ eq_(meta.get('key_field'),u'manual_id')
+ eq_(meta['geometry_type'],mapnik.DataGeometryType.Point)
+
+ def test_auto_detection_will_fail_since_no_primary_key():
+ ds = mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,table='test3',
+ geometry_field='geom',
+ autodetect_key_field=False)
+ fs = ds.featureset()
+ feat = fs.next()
+ eq_(feat['manual_id'],0)
+ # will fail: https://github.com/mapnik/mapnik/issues/895
+ #eq_(feat['non_id'],9223372036854775807)
+ eq_(fs.next()['manual_id'],1)
+ eq_(fs.next()['manual_id'],1000)
+ eq_(fs.next()['manual_id'],-1000)
+ eq_(fs.next()['manual_id'],2147483647)
+ eq_(fs.next()['manual_id'],-2147483648)
+
+ # since no valid primary key will be detected the fallback
+ # is auto-incrementing counter
+ fs = ds.featureset()
+ eq_(fs.next().id(),1)
+ eq_(fs.next().id(),2)
+ eq_(fs.next().id(),3)
+ eq_(fs.next().id(),4)
+ eq_(fs.next().id(),5)
+ eq_(fs.next().id(),6)
+
+ meta = ds.describe()
+ eq_(meta['srid'],4326)
+ eq_(meta.get('key_field'),None)
+ eq_(meta['geometry_type'],mapnik.DataGeometryType.Point)
+
+ @raises(RuntimeError)
+ def test_auto_detection_will_fail_and_should_throw():
+ ds = mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,table='test3',
+ geometry_field='geom',
+ autodetect_key_field=True)
+ ds.featureset()
+
+ def test_auto_detection_of_unique_feature_id_64_bit():
+ ds = mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,table='test4',
+ geometry_field='geom',
+ autodetect_key_field=True)
+ fs = ds.featureset()
+ eq_(fs.next()['manual_id'],0)
+ eq_(fs.next()['manual_id'],1)
+ eq_(fs.next()['manual_id'],1000)
+ eq_(fs.next()['manual_id'],-1000)
+ eq_(fs.next()['manual_id'],2147483647)
+ eq_(fs.next()['manual_id'],-2147483648)
+
+ fs = ds.featureset()
+ eq_(fs.next().id(),0)
+ eq_(fs.next().id(),1)
+ eq_(fs.next().id(),1000)
+ eq_(fs.next().id(),-1000)
+ eq_(fs.next().id(),2147483647)
+ eq_(fs.next().id(),-2147483648)
+
+ meta = ds.describe()
+ eq_(meta['srid'],4326)
+ eq_(meta.get('key_field'),u'manual_id')
+ eq_(meta['geometry_type'],mapnik.DataGeometryType.Point)
+
+ def test_disabled_auto_detection_and_subquery():
+ ds = mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,table='''(select geom, 'a'::varchar as name from test2) as t''',
+ geometry_field='geom',
+ autodetect_key_field=False)
+ fs = ds.featureset()
+ feat = fs.next()
+ eq_(feat.id(),1)
+ eq_(feat['name'],'a')
+ feat = fs.next()
+ eq_(feat.id(),2)
+ eq_(feat['name'],'a')
+ feat = fs.next()
+ eq_(feat.id(),3)
+ eq_(feat['name'],'a')
+ feat = fs.next()
+ eq_(feat.id(),4)
+ eq_(feat['name'],'a')
+ feat = fs.next()
+ eq_(feat.id(),5)
+ eq_(feat['name'],'a')
+ feat = fs.next()
+ eq_(feat.id(),6)
+ eq_(feat['name'],'a')
+
+ meta = ds.describe()
+ eq_(meta['srid'],4326)
+ eq_(meta.get('key_field'),None)
+ eq_(meta['geometry_type'],mapnik.DataGeometryType.Point)
+
+ def test_auto_detection_and_subquery_including_key():
+ ds = mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,table='''(select geom, manual_id from test2) as t''',
+ geometry_field='geom',
+ autodetect_key_field=True)
+ fs = ds.featureset()
+ eq_(fs.next()['manual_id'],0)
+ eq_(fs.next()['manual_id'],1)
+ eq_(fs.next()['manual_id'],1000)
+ eq_(fs.next()['manual_id'],-1000)
+ eq_(fs.next()['manual_id'],2147483647)
+ eq_(fs.next()['manual_id'],-2147483648)
+
+ fs = ds.featureset()
+ eq_(fs.next().id(),0)
+ eq_(fs.next().id(),1)
+ eq_(fs.next().id(),1000)
+ eq_(fs.next().id(),-1000)
+ eq_(fs.next().id(),2147483647)
+ eq_(fs.next().id(),-2147483648)
+
+ meta = ds.describe()
+ eq_(meta['srid'],4326)
+ eq_(meta.get('key_field'),u'manual_id')
+ eq_(meta['geometry_type'],mapnik.DataGeometryType.Point)
+
+ @raises(RuntimeError)
+ def test_auto_detection_of_invalid_numeric_primary_key():
+ mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,table='''(select geom, manual_id::numeric from test2) as t''',
+ geometry_field='geom',
+ autodetect_key_field=True)
+
+ @raises(RuntimeError)
+ def test_auto_detection_of_invalid_multiple_keys():
+ mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,table='''test6''',
+ geometry_field='geom',
+ autodetect_key_field=True)
+
+ @raises(RuntimeError)
+ def test_auto_detection_of_invalid_multiple_keys_subquery():
+ mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,table='''(select first_id,second_id,geom from test6) as t''',
+ geometry_field='geom',
+ autodetect_key_field=True)
+
+ def test_manually_specified_feature_id_field():
+ ds = mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,table='test4',
+ geometry_field='geom',
+ key_field='manual_id',
+ autodetect_key_field=True)
+ fs = ds.featureset()
+ eq_(fs.next()['manual_id'],0)
+ eq_(fs.next()['manual_id'],1)
+ eq_(fs.next()['manual_id'],1000)
+ eq_(fs.next()['manual_id'],-1000)
+ eq_(fs.next()['manual_id'],2147483647)
+ eq_(fs.next()['manual_id'],-2147483648)
+
+ fs = ds.featureset()
+ eq_(fs.next().id(),0)
+ eq_(fs.next().id(),1)
+ eq_(fs.next().id(),1000)
+ eq_(fs.next().id(),-1000)
+ eq_(fs.next().id(),2147483647)
+ eq_(fs.next().id(),-2147483648)
+
+ meta = ds.describe()
+ eq_(meta['srid'],4326)
+ eq_(meta.get('key_field'),u'manual_id')
+ eq_(meta['geometry_type'],mapnik.DataGeometryType.Point)
+
+ def test_numeric_type_feature_id_field():
+ ds = mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,table='test5',
+ geometry_field='geom',
+ autodetect_key_field=False)
+ fs = ds.featureset()
+ eq_(fs.next()['manual_id'],-1)
+ eq_(fs.next()['manual_id'],1)
+
+ fs = ds.featureset()
+ eq_(fs.next().id(),1)
+ eq_(fs.next().id(),2)
+
+ meta = ds.describe()
+ eq_(meta['srid'],4326)
+ eq_(meta.get('key_field'),None)
+ eq_(meta['geometry_type'],mapnik.DataGeometryType.Point)
+
+ def test_querying_table_with_mixed_case():
+ ds = mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,table='"tableWithMixedCase"',
+ geometry_field='geom',
+ autodetect_key_field=True)
+ fs = ds.featureset()
+ for id in range(1,5):
+ eq_(fs.next().id(),id)
+
+ meta = ds.describe()
+ eq_(meta['srid'],-1)
+ eq_(meta.get('key_field'),u'gid')
+ eq_(meta['geometry_type'],mapnik.DataGeometryType.Point)
+
+ def test_querying_subquery_with_mixed_case():
+ ds = mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,table='(SeLeCt * FrOm "tableWithMixedCase") as MixedCaseQuery',
+ geometry_field='geom',
+ autodetect_key_field=True)
+ fs = ds.featureset()
+ for id in range(1,5):
+ eq_(fs.next().id(),id)
+
+ meta = ds.describe()
+ eq_(meta['srid'],-1)
+ eq_(meta.get('key_field'),u'gid')
+ eq_(meta['geometry_type'],mapnik.DataGeometryType.Point)
+
+ def test_bbox_token_in_subquery1():
+ ds = mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,table='''
+ (SeLeCt * FrOm "tableWithMixedCase" where geom && !bbox! ) as MixedCaseQuery''',
+ geometry_field='geom',
+ autodetect_key_field=True)
+ fs = ds.featureset()
+ for id in range(1,5):
+ eq_(fs.next().id(),id)
+
+ meta = ds.describe()
+ eq_(meta['srid'],-1)
+ eq_(meta.get('key_field'),u'gid')
+ eq_(meta['geometry_type'],mapnik.DataGeometryType.Point)
+
+ def test_bbox_token_in_subquery2():
+ ds = mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,table='''
+ (SeLeCt * FrOm "tableWithMixedCase" where ST_Intersects(geom,!bbox!) ) as MixedCaseQuery''',
+ geometry_field='geom',
+ autodetect_key_field=True)
+ fs = ds.featureset()
+ for id in range(1,5):
+ eq_(fs.next().id(),id)
+
+ meta = ds.describe()
+ eq_(meta['srid'],-1)
+ eq_(meta.get('key_field'),u'gid')
+ eq_(meta['geometry_type'],mapnik.DataGeometryType.Point)
+
+ def test_empty_geom():
+ ds = mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,table='test7',
+ geometry_field='geom')
+ fs = ds.featureset()
+ eq_(fs.next()['gid'],1)
+
+ meta = ds.describe()
+ eq_(meta['srid'],4326)
+ eq_(meta.get('key_field'),None)
+ eq_(meta['geometry_type'],mapnik.DataGeometryType.Collection)
+
+ def create_ds():
+ ds = mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,
+ table='test',
+ max_size=20,
+ geometry_field='geom')
+ fs = ds.all_features()
+ eq_(len(fs),8)
+
+ meta = ds.describe()
+ eq_(meta['srid'],4326)
+ eq_(meta.get('key_field'),None)
+ eq_(meta['geometry_type'],mapnik.DataGeometryType.Collection)
+
+ def test_threaded_create(NUM_THREADS=100):
+ # run one to start before thread loop
+ # to ensure that a throw stops the test
+ # from running all threads
+ create_ds()
+ runs = 0
+ for i in range(NUM_THREADS):
+ t = threading.Thread(target=create_ds)
+ t.start()
+ t.join()
+ runs +=1
+ eq_(runs,NUM_THREADS)
+
+ def create_ds_and_error():
+ try:
+ ds = mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,
+ table='asdfasdfasdfasdfasdf',
+ max_size=20)
+ ds.all_features()
+ except Exception, e:
+ eq_('in executeQuery' in str(e),True)
+
+ def test_threaded_create2(NUM_THREADS=10):
+ for i in range(NUM_THREADS):
+ t = threading.Thread(target=create_ds_and_error)
+ t.start()
+ t.join()
+
+ def test_that_64bit_int_fields_work():
+ ds = mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,
+ table='test8',
+ geometry_field='geom')
+ eq_(len(ds.fields()),2)
+ eq_(ds.fields(),['gid','int_field'])
+ eq_(ds.field_types(),['int','int'])
+ fs = ds.featureset()
+ feat = fs.next()
+ eq_(feat.id(),1)
+ eq_(feat['gid'],1)
+ eq_(feat['int_field'],2147483648)
+ feat = fs.next()
+ eq_(feat.id(),2)
+ eq_(feat['gid'],2)
+ eq_(feat['int_field'],922337203685477580)
+
+ meta = ds.describe()
+ eq_(meta['srid'],-1)
+ eq_(meta.get('key_field'),None)
+ eq_(meta['geometry_type'],mapnik.DataGeometryType.Point)
+
+ def test_persist_connection_off():
+ # NOTE: max_size should be equal or greater than
+ # the pool size. There's currently no API to
+ # check nor set that size, but the current
+ # default is 20, so we use that value. See
+ # http://github.com/mapnik/mapnik/issues/863
+ max_size = 20
+ for i in range(0, max_size+1):
+ ds = mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,
+ max_size=1, # unused
+ persist_connection=False,
+ table='(select ST_MakePoint(0,0) as g, pg_backend_pid() as p, 1 as v) as w',
+ geometry_field='g')
+ fs = ds.featureset()
+ eq_(fs.next()['v'], 1)
+
+ meta = ds.describe()
+ eq_(meta['srid'],-1)
+ eq_(meta['geometry_type'],mapnik.DataGeometryType.Point)
+
+ def test_null_comparision():
+ ds = mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,table='test9',
+ geometry_field='geom')
+ fs = ds.featureset()
+ feat = fs.next()
+
+ meta = ds.describe()
+ eq_(meta['srid'],-1)
+ eq_(meta.get('key_field'),None)
+ eq_(meta['geometry_type'],mapnik.DataGeometryType.Point)
+
+ eq_(feat['gid'],1)
+ eq_(feat['name'],'name')
+ eq_(mapnik.Expression("[name] = 'name'").evaluate(feat),True)
+ eq_(mapnik.Expression("[name] = ''").evaluate(feat),False)
+ eq_(mapnik.Expression("[name] = null").evaluate(feat),False)
+ eq_(mapnik.Expression("[name] = true").evaluate(feat),False)
+ eq_(mapnik.Expression("[name] = false").evaluate(feat),False)
+ eq_(mapnik.Expression("[name] != 'name'").evaluate(feat),False)
+ eq_(mapnik.Expression("[name] != ''").evaluate(feat),True)
+ eq_(mapnik.Expression("[name] != null").evaluate(feat),True)
+ eq_(mapnik.Expression("[name] != true").evaluate(feat),True)
+ eq_(mapnik.Expression("[name] != false").evaluate(feat),True)
+
+ feat = fs.next()
+ eq_(feat['gid'],2)
+ eq_(feat['name'],'')
+ eq_(mapnik.Expression("[name] = 'name'").evaluate(feat),False)
+ eq_(mapnik.Expression("[name] = ''").evaluate(feat),True)
+ eq_(mapnik.Expression("[name] = null").evaluate(feat),False)
+ eq_(mapnik.Expression("[name] = true").evaluate(feat),False)
+ eq_(mapnik.Expression("[name] = false").evaluate(feat),False)
+ eq_(mapnik.Expression("[name] != 'name'").evaluate(feat),True)
+ eq_(mapnik.Expression("[name] != ''").evaluate(feat),False)
+ eq_(mapnik.Expression("[name] != null").evaluate(feat),True)
+ eq_(mapnik.Expression("[name] != true").evaluate(feat),True)
+ eq_(mapnik.Expression("[name] != false").evaluate(feat),True)
+
+ feat = fs.next()
+ eq_(feat['gid'],3)
+ eq_(feat['name'],None) # null
+ eq_(mapnik.Expression("[name] = 'name'").evaluate(feat),False)
+ eq_(mapnik.Expression("[name] = ''").evaluate(feat),False)
+ eq_(mapnik.Expression("[name] = null").evaluate(feat),True)
+ eq_(mapnik.Expression("[name] = true").evaluate(feat),False)
+ eq_(mapnik.Expression("[name] = false").evaluate(feat),False)
+ eq_(mapnik.Expression("[name] != 'name'").evaluate(feat),True)
+ # https://github.com/mapnik/mapnik/issues/1859
+ eq_(mapnik.Expression("[name] != ''").evaluate(feat),False)
+ eq_(mapnik.Expression("[name] != null").evaluate(feat),False)
+ eq_(mapnik.Expression("[name] != true").evaluate(feat),True)
+ eq_(mapnik.Expression("[name] != false").evaluate(feat),True)
+
+ def test_null_comparision2():
+ ds = mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,table='test10',
+ geometry_field='geom')
+ fs = ds.featureset()
+ feat = fs.next()
+
+ meta = ds.describe()
+ eq_(meta['srid'],-1)
+ eq_(meta.get('key_field'),None)
+ eq_(meta['geometry_type'],mapnik.DataGeometryType.Point)
+
+ eq_(feat['gid'],1)
+ eq_(feat['bool_field'],True)
+ eq_(mapnik.Expression("[bool_field] = 'name'").evaluate(feat),False)
+ eq_(mapnik.Expression("[bool_field] = ''").evaluate(feat),False)
+ eq_(mapnik.Expression("[bool_field] = null").evaluate(feat),False)
+ eq_(mapnik.Expression("[bool_field] = true").evaluate(feat),True)
+ eq_(mapnik.Expression("[bool_field] = false").evaluate(feat),False)
+ eq_(mapnik.Expression("[bool_field] != 'name'").evaluate(feat),True)
+ eq_(mapnik.Expression("[bool_field] != ''").evaluate(feat),True) # in 2.1.x used to be False
+ eq_(mapnik.Expression("[bool_field] != null").evaluate(feat),True) # in 2.1.x used to be False
+ eq_(mapnik.Expression("[bool_field] != true").evaluate(feat),False)
+ eq_(mapnik.Expression("[bool_field] != false").evaluate(feat),True)
+
+ feat = fs.next()
+ eq_(feat['gid'],2)
+ eq_(feat['bool_field'],False)
+ eq_(mapnik.Expression("[bool_field] = 'name'").evaluate(feat),False)
+ eq_(mapnik.Expression("[bool_field] = ''").evaluate(feat),False)
+ eq_(mapnik.Expression("[bool_field] = null").evaluate(feat),False)
+ eq_(mapnik.Expression("[bool_field] = true").evaluate(feat),False)
+ eq_(mapnik.Expression("[bool_field] = false").evaluate(feat),True)
+ eq_(mapnik.Expression("[bool_field] != 'name'").evaluate(feat),True)
+ eq_(mapnik.Expression("[bool_field] != ''").evaluate(feat),True)
+ eq_(mapnik.Expression("[bool_field] != null").evaluate(feat),True) # in 2.1.x used to be False
+ eq_(mapnik.Expression("[bool_field] != true").evaluate(feat),True)
+ eq_(mapnik.Expression("[bool_field] != false").evaluate(feat),False)
+
+ feat = fs.next()
+ eq_(feat['gid'],3)
+ eq_(feat['bool_field'],None) # null
+ eq_(mapnik.Expression("[bool_field] = 'name'").evaluate(feat),False)
+ eq_(mapnik.Expression("[bool_field] = ''").evaluate(feat),False)
+ eq_(mapnik.Expression("[bool_field] = null").evaluate(feat),True)
+ eq_(mapnik.Expression("[bool_field] = true").evaluate(feat),False)
+ eq_(mapnik.Expression("[bool_field] = false").evaluate(feat),False)
+ eq_(mapnik.Expression("[bool_field] != 'name'").evaluate(feat),True) # in 2.1.x used to be False
+ # https://github.com/mapnik/mapnik/issues/1859
+ eq_(mapnik.Expression("[bool_field] != ''").evaluate(feat),False)
+ eq_(mapnik.Expression("[bool_field] != null").evaluate(feat),False)
+ eq_(mapnik.Expression("[bool_field] != true").evaluate(feat),True) # in 2.1.x used to be False
+ eq_(mapnik.Expression("[bool_field] != false").evaluate(feat),True) # in 2.1.x used to be False
+
+ # https://github.com/mapnik/mapnik/issues/1816
+ def test_exception_message_reporting():
+ try:
+ mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,table='doesnotexist')
+ except Exception, e:
+ eq_(e.message != 'unidentifiable C++ exception', True)
+
+ def test_null_id_field():
+ opts = {'type':'postgis',
+ 'dbname':MAPNIK_TEST_DBNAME,
+ 'geometry_field':'geom',
+ 'table':"(select null::bigint as osm_id, GeomFromEWKT('SRID=4326;POINT(0 0)') as geom) as tmp"}
+ ds = mapnik.Datasource(**opts)
+ fs = ds.featureset()
+ feat = fs.next()
+ eq_(feat.id(),1L)
+ eq_(feat['osm_id'],None)
+
+ meta = ds.describe()
+ eq_(meta['srid'],4326)
+ eq_(meta.get('key_field'),None)
+ eq_(meta['geometry_type'],mapnik.DataGeometryType.Point)
+
+ @raises(StopIteration)
+ def test_null_key_field():
+ opts = {'type':'postgis',
+ "key_field": 'osm_id',
+ 'dbname':MAPNIK_TEST_DBNAME,
+ 'geometry_field':'geom',
+ 'table':"(select null::bigint as osm_id, GeomFromEWKT('SRID=4326;POINT(0 0)') as geom) as tmp"}
+ ds = mapnik.Datasource(**opts)
+ fs = ds.featureset()
+ fs.next() ## should throw since key_field is null: StopIteration: No more features.
+
+ def test_psql_error_should_not_break_connection_pool():
+ # Bad request, will trigger an error when returning result
+ ds_bad = mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,table="""(SELECT geom as geom,label::int from test11) as failure_table""",
+ max_async_connection=5,geometry_field='geom',srid=4326)
+
+ # Good request
+ ds_good = mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,table="test",
+ max_async_connection=5,geometry_field='geom',srid=4326)
+
+ # This will/should trigger a PSQL error
+ failed = False
+ try:
+ fs = ds_bad.featureset()
+ for feature in fs:
+ pass
+ except RuntimeError, e:
+ assert 'invalid input syntax for integer' in str(e)
+ failed = True
+
+ eq_(failed,True)
+
+ # Should be ok
+ fs = ds_good.featureset()
+ count = 0
+ for feature in fs:
+ count += 1
+ eq_(count,8)
+
+
+ def test_psql_error_should_give_back_connections_opened_for_lower_layers_to_the_pool():
+ map1 = mapnik.Map(600,300)
+ s = mapnik.Style()
+ r = mapnik.Rule()
+ r.symbols.append(mapnik.PolygonSymbolizer())
+ s.rules.append(r)
+ map1.append_style('style',s)
+
+ # This layer will fail after a while
+ buggy_s = mapnik.Style()
+ buggy_r = mapnik.Rule()
+ buggy_r.symbols.append(mapnik.PolygonSymbolizer())
+ buggy_r.filter = mapnik.Filter("[fips] = 'FR'")
+ buggy_s.rules.append(buggy_r)
+ map1.append_style('style for buggy layer',buggy_s)
+ buggy_layer = mapnik.Layer('this layer is buggy at runtime')
+ # We ensure the query wille be long enough
+ buggy_layer.datasource = mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,table='(SELECT geom as geom, pg_sleep(0.1), fips::int from world_merc) as failure_tabl',
+ max_async_connection=2, max_size=2,asynchronous_request = True, geometry_field='geom')
+ buggy_layer.styles.append('style for buggy layer')
+
+ # The query for this layer will be sent, then the previous layer will raise an exception before results are read
+ forced_canceled_layer = mapnik.Layer('this layer will be canceled when an exception stops map rendering')
+ forced_canceled_layer.datasource = mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,table='world_merc',
+ max_async_connection=2, max_size=2, asynchronous_request = True, geometry_field='geom')
+ forced_canceled_layer.styles.append('style')
+
+ map1.layers.append(buggy_layer)
+ map1.layers.append(forced_canceled_layer)
+ map1.zoom_all()
+ map2 = mapnik.Map(600,300)
+ map2.background = mapnik.Color('steelblue')
+ s = mapnik.Style()
+ r = mapnik.Rule()
+ r.symbols.append(mapnik.LineSymbolizer())
+ r.symbols.append(mapnik.LineSymbolizer())
+ s.rules.append(r)
+ map2.append_style('style',s)
+ layer1 = mapnik.Layer('layer1')
+ layer1.datasource = mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,table='world_merc',
+ max_async_connection=2, max_size=2, asynchronous_request = True, geometry_field='geom')
+ layer1.styles.append('style')
+ map2.layers.append(layer1)
+ map2.zoom_all()
+
+ # We expect this to trigger a PSQL error
+ try:
+ mapnik.render_to_file(map1,'/tmp/mapnik-postgis-test-map1.png', 'png')
+ # Test must fail if error was not raised just above
+ eq_(False,True)
+ except RuntimeError, e:
+ assert 'invalid input syntax for integer' in str(e)
+ pass
+ # This used to raise an exception before correction of issue 2042
+ mapnik.render_to_file(map2,'/tmp/mapnik-postgis-test-map2.png', 'png')
+
+ def test_handling_of_zm_dimensions():
+ ds = mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,
+ table='(select gid,ST_CoordDim(geom) as dim,name,geom from test12) as tmp',
+ geometry_field='geom')
+ eq_(len(ds.fields()),3)
+ eq_(ds.fields(),['gid', 'dim', 'name'])
+ eq_(ds.field_types(),['int', 'int', 'str'])
+ fs = ds.featureset()
+
+ meta = ds.describe()
+ eq_(meta['srid'],4326)
+ eq_(meta.get('key_field'),None)
+ # Note: this is incorrect because we only check first couple geoms
+ eq_(meta['geometry_type'],mapnik.DataGeometryType.Point)
+
+ # Point (2d)
+ feat = fs.next()
+ eq_(feat.id(),1)
+ eq_(feat['gid'],1)
+ eq_(feat['dim'],2)
+ eq_(feat['name'],'Point')
+ eq_(feat.geometry.to_wkt(),'POINT(0 0)')
+
+ # PointZ
+ feat = fs.next()
+ eq_(feat.id(),2)
+ eq_(feat['gid'],2)
+ eq_(feat['dim'],3)
+ eq_(feat['name'],'PointZ')
+ eq_(feat.geometry.to_wkt(),'POINT(0 0)')
+
+ # PointM
+ feat = fs.next()
+ eq_(feat.id(),3)
+ eq_(feat['gid'],3)
+ eq_(feat['dim'],3)
+ eq_(feat['name'],'PointM')
+ eq_(feat.geometry.to_wkt(),'POINT(0 0)')
+
+ # PointZM
+ feat = fs.next()
+ eq_(feat.id(),4)
+ eq_(feat['gid'],4)
+ eq_(feat['dim'],4)
+ eq_(feat['name'],'PointZM')
+
+ eq_(feat.geometry.to_wkt(),'POINT(0 0)')
+ # MultiPoint
+ feat = fs.next()
+ eq_(feat.id(),5)
+ eq_(feat['gid'],5)
+ eq_(feat['dim'],2)
+ eq_(feat['name'],'MultiPoint')
+ eq_(feat.geometry.to_wkt(),'MULTIPOINT(0 0,1 1)')
+
+ # MultiPointZ
+ feat = fs.next()
+ eq_(feat.id(),6)
+ eq_(feat['gid'],6)
+ eq_(feat['dim'],3)
+ eq_(feat['name'],'MultiPointZ')
+ eq_(feat.geometry.to_wkt(),'MULTIPOINT(0 0,1 1)')
+
+ # MultiPointM
+ feat = fs.next()
+ eq_(feat.id(),7)
+ eq_(feat['gid'],7)
+ eq_(feat['dim'],3)
+ eq_(feat['name'],'MultiPointM')
+ eq_(feat.geometry.to_wkt(),'MULTIPOINT(0 0,1 1)')
+
+ # MultiPointZM
+ feat = fs.next()
+ eq_(feat.id(),8)
+ eq_(feat['gid'],8)
+ eq_(feat['dim'],4)
+ eq_(feat['name'],'MultiPointZM')
+ eq_(feat.geometry.to_wkt(),'MULTIPOINT(0 0,1 1)')
+
+ # LineString
+ feat = fs.next()
+ eq_(feat.id(),9)
+ eq_(feat['gid'],9)
+ eq_(feat['dim'],2)
+ eq_(feat['name'],'LineString')
+ eq_(feat.geometry.to_wkt(),'LINESTRING(0 0,1 1)')
+
+ # LineStringZ
+ feat = fs.next()
+ eq_(feat.id(),10)
+ eq_(feat['gid'],10)
+ eq_(feat['dim'],3)
+ eq_(feat['name'],'LineStringZ')
+ eq_(feat.geometry.to_wkt(),'LINESTRING(0 0,1 1)')
+
+ # LineStringM
+ feat = fs.next()
+ eq_(feat.id(),11)
+ eq_(feat['gid'],11)
+ eq_(feat['dim'],3)
+ eq_(feat['name'],'LineStringM')
+ eq_(feat.geometry.to_wkt(),'LINESTRING(0 0,1 1)')
+
+ # LineStringZM
+ feat = fs.next()
+ eq_(feat.id(),12)
+ eq_(feat['gid'],12)
+ eq_(feat['dim'],4)
+ eq_(feat['name'],'LineStringZM')
+ eq_(feat.geometry.to_wkt(),'LINESTRING(0 0,1 1)')
+
+ # Polygon
+ feat = fs.next()
+ eq_(feat.id(),13)
+ eq_(feat['gid'],13)
+ eq_(feat['name'],'Polygon')
+ eq_(feat.geometry.to_wkt(),'POLYGON((0 0,1 1,2 2,0 0))')
+
+ # PolygonZ
+ feat = fs.next()
+ eq_(feat.id(),14)
+ eq_(feat['gid'],14)
+ eq_(feat['name'],'PolygonZ')
+ eq_(feat.geometry.to_wkt(),'POLYGON((0 0,1 1,2 2,0 0))')
+
+ # PolygonM
+ feat = fs.next()
+ eq_(feat.id(),15)
+ eq_(feat['gid'],15)
+ eq_(feat['name'],'PolygonM')
+ eq_(feat.geometry.to_wkt(),'POLYGON((0 0,1 1,2 2,0 0))')
+
+ # PolygonZM
+ feat = fs.next()
+ eq_(feat.id(),16)
+ eq_(feat['gid'],16)
+ eq_(feat['name'],'PolygonZM')
+ eq_(feat.geometry.to_wkt(),'POLYGON((0 0,1 1,2 2,0 0))')
+
+ # MultiLineString
+ feat = fs.next()
+ eq_(feat.id(),17)
+ eq_(feat['gid'],17)
+ eq_(feat['name'],'MultiLineString')
+ eq_(feat.geometry.to_wkt(),'MULTILINESTRING((0 0,1 1),(2 2,3 3))')
+
+ # MultiLineStringZ
+ feat = fs.next()
+ eq_(feat.id(),18)
+ eq_(feat['gid'],18)
+ eq_(feat['name'],'MultiLineStringZ')
+ eq_(feat.geometry.to_wkt(),'MULTILINESTRING((0 0,1 1),(2 2,3 3))')
+
+ # MultiLineStringM
+ feat = fs.next()
+ eq_(feat.id(),19)
+ eq_(feat['gid'],19)
+ eq_(feat['name'],'MultiLineStringM')
+ eq_(feat.geometry.to_wkt(),'MULTILINESTRING((0 0,1 1),(2 2,3 3))')
+
+ # MultiLineStringZM
+ feat = fs.next()
+ eq_(feat.id(),20)
+ eq_(feat['gid'],20)
+ eq_(feat['name'],'MultiLineStringZM')
+ eq_(feat.geometry.to_wkt(),'MULTILINESTRING((0 0,1 1),(2 2,3 3))')
+
+ # MultiPolygon
+ feat = fs.next()
+ eq_(feat.id(),21)
+ eq_(feat['gid'],21)
+ eq_(feat['name'],'MultiPolygon')
+ eq_(feat.geometry.to_wkt(),'MULTIPOLYGON(((0 0,1 1,2 2,0 0)),((0 0,1 1,2 2,0 0)))')
+
+ # MultiPolygonZ
+ feat = fs.next()
+ eq_(feat.id(),22)
+ eq_(feat['gid'],22)
+ eq_(feat['name'],'MultiPolygonZ')
+ eq_(feat.geometry.to_wkt(),'MULTIPOLYGON(((0 0,1 1,2 2,0 0)),((0 0,1 1,2 2,0 0)))')
+
+ # MultiPolygonM
+ feat = fs.next()
+ eq_(feat.id(),23)
+ eq_(feat['gid'],23)
+ eq_(feat['name'],'MultiPolygonM')
+ eq_(feat.geometry.to_wkt(),'MULTIPOLYGON(((0 0,1 1,2 2,0 0)),((0 0,1 1,2 2,0 0)))')
+
+ # MultiPolygonZM
+ feat = fs.next()
+ eq_(feat.id(),24)
+ eq_(feat['gid'],24)
+ eq_(feat['name'],'MultiPolygonZM')
+ eq_(feat.geometry.to_wkt(),'MULTIPOLYGON(((0 0,1 1,2 2,0 0)),((0 0,1 1,2 2,0 0)))')
+
+ def test_variable_in_subquery1():
+ ds = mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,table='''
+ (select * from test where @zoom = 30 ) as tmp''',
+ geometry_field='geom', srid=4326,
+ autodetect_key_field=True)
+ fs = ds.featureset(variables={'zoom':30})
+ for id in range(1,5):
+ eq_(fs.next().id(),id)
+
+ meta = ds.describe()
+ eq_(meta['srid'],4326)
+ eq_(meta.get('key_field'),"gid")
+ eq_(meta['geometry_type'],None)
+
+ # currently needs manual `geometry_table` passed
+ # to avoid misparse of `geometry_table`
+ # in the future ideally this would not need manual `geometry_table`
+ # https://github.com/mapnik/mapnik/issues/2718
+ # currently `bogus` would be picked automatically for geometry_table
+ def test_broken_parsing_of_comments():
+ ds = mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,table='''
+ (select * FROM test) AS data
+ -- select this from bogus''',
+ geometry_table='test')
+ fs = ds.featureset()
+ for id in range(1,5):
+ eq_(fs.next().id(),id)
+
+ meta = ds.describe()
+ eq_(meta['srid'],4326)
+ eq_(meta['geometry_type'],mapnik.DataGeometryType.Collection)
+
+ # same
+ # to avoid misparse of `geometry_table`
+ # in the future ideally this would not need manual `geometry_table`
+ # https://github.com/mapnik/mapnik/issues/2718
+ # currently nothing would be picked automatically for geometry_table
+ def test_broken_parsing_of_comments():
+ ds = mapnik.PostGIS(dbname=MAPNIK_TEST_DBNAME,table='''
+ (select * FROM test) AS data
+ -- select this from bogus.''',
+ geometry_table='test')
+ fs = ds.featureset()
+ for id in range(1,5):
+ eq_(fs.next().id(),id)
+
+ meta = ds.describe()
+ eq_(meta['srid'],4326)
+ eq_(meta['geometry_type'],mapnik.DataGeometryType.Collection)
+
+
+ atexit.register(postgis_takedown)
+
+if __name__ == "__main__":
+ setup()
+ exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/projection_test.py b/test/python_tests/projection_test.py
new file mode 100644
index 0000000..a7bdc14
--- /dev/null
+++ b/test/python_tests/projection_test.py
@@ -0,0 +1,151 @@
+#!/usr/bin/env python
+
+from nose.tools import eq_,assert_almost_equal
+
+import mapnik
+import math
+from utilities import run_all, assert_box2d_almost_equal
+
+# Tests that exercise map projections.
+
+def test_normalizing_definition():
+ p = mapnik.Projection('+init=epsg:4326')
+ expanded = p.expanded()
+ eq_('+proj=longlat' in expanded,True)
+
+
+# Trac Ticket #128
+def test_wgs84_inverse_forward():
+ p = mapnik.Projection('+init=epsg:4326')
+
+ c = mapnik.Coord(3.01331418311, 43.3333092669)
+ e = mapnik.Box2d(-122.54345245, 45.12312553, 68.2335581353, 48.231231233)
+
+ # It appears that the y component changes very slightly, is this OK?
+ # so we test for 'almost equal float values'
+
+ assert_almost_equal(p.inverse(c).y, c.y)
+ assert_almost_equal(p.inverse(c).x, c.x)
+
+ assert_almost_equal(p.forward(c).y, c.y)
+ assert_almost_equal(p.forward(c).x, c.x)
+
+ assert_almost_equal(p.inverse(e).center().y, e.center().y)
+ assert_almost_equal(p.inverse(e).center().x, e.center().x)
+
+ assert_almost_equal(p.forward(e).center().y, e.center().y)
+ assert_almost_equal(p.forward(e).center().x, e.center().x)
+
+ assert_almost_equal(c.inverse(p).y, c.y)
+ assert_almost_equal(c.inverse(p).x, c.x)
+
+ assert_almost_equal(c.forward(p).y, c.y)
+ assert_almost_equal(c.forward(p).x, c.x)
+
+ assert_almost_equal(e.inverse(p).center().y, e.center().y)
+ assert_almost_equal(e.inverse(p).center().x, e.center().x)
+
+ assert_almost_equal(e.forward(p).center().y, e.center().y)
+ assert_almost_equal(e.forward(p).center().x, e.center().x)
+
+def wgs2merc(lon,lat):
+ x = lon * 20037508.34 / 180;
+ y = math.log(math.tan((90 + lat) * math.pi / 360)) / (math.pi / 180);
+ y = y * 20037508.34 / 180;
+ return [x,y];
+
+def merc2wgs(x,y):
+ x = (x / 20037508.34) * 180;
+ y = (y / 20037508.34) * 180;
+ y = 180 / math.pi * (2 * math.atan(math.exp(y * math.pi/180)) - math.pi/2);
+ if x > 180: x = 180;
+ if x < -180: x = -180;
+ if y > 85.0511: y = 85.0511;
+ if y < -85.0511: y = -85.0511;
+ return [x,y]
+
+#echo -109 37 | cs2cs -f "%.10f" +init=epsg:4326 +to +init=epsg:3857
+#-12133824.4964668211 4439106.7872505859 0.0000000000
+
+## todo
+# benchmarks
+# better well known detection
+# better srs matching with strip/trim
+# python copy to avoid crash
+
+def test_proj_transform_between_init_and_literal():
+ one = mapnik.Projection('+init=epsg:4326')
+ two = mapnik.Projection('+init=epsg:3857')
+ tr1 = mapnik.ProjTransform(one,two)
+ tr1b = mapnik.ProjTransform(two,one)
+ wgs84 = '+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs'
+ merc = '+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0.0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs +over'
+ src = mapnik.Projection(wgs84)
+ dest = mapnik.Projection(merc)
+ tr2 = mapnik.ProjTransform(src,dest)
+ tr2b = mapnik.ProjTransform(dest,src)
+ for x in xrange(-180,180,10):
+ for y in xrange(-60,60,10):
+ coord = mapnik.Coord(x,y)
+ merc_coord1 = tr1.forward(coord)
+ merc_coord2 = tr1b.backward(coord)
+ merc_coord3 = tr2.forward(coord)
+ merc_coord4 = tr2b.backward(coord)
+ eq_(math.fabs(merc_coord1.x - merc_coord1.x) < 1,True)
+ eq_(math.fabs(merc_coord1.x - merc_coord2.x) < 1,True)
+ eq_(math.fabs(merc_coord1.x - merc_coord3.x) < 1,True)
+ eq_(math.fabs(merc_coord1.x - merc_coord4.x) < 1,True)
+ eq_(math.fabs(merc_coord1.y - merc_coord1.y) < 1,True)
+ eq_(math.fabs(merc_coord1.y - merc_coord2.y) < 1,True)
+ eq_(math.fabs(merc_coord1.y - merc_coord3.y) < 1,True)
+ eq_(math.fabs(merc_coord1.y - merc_coord4.y) < 1,True)
+ lon_lat_coord1 = tr1.backward(merc_coord1)
+ lon_lat_coord2 = tr1b.forward(merc_coord2)
+ lon_lat_coord3 = tr2.backward(merc_coord3)
+ lon_lat_coord4 = tr2b.forward(merc_coord4)
+ eq_(math.fabs(coord.x - lon_lat_coord1.x) < 1,True)
+ eq_(math.fabs(coord.x - lon_lat_coord2.x) < 1,True)
+ eq_(math.fabs(coord.x - lon_lat_coord3.x) < 1,True)
+ eq_(math.fabs(coord.x - lon_lat_coord4.x) < 1,True)
+ eq_(math.fabs(coord.y - lon_lat_coord1.y) < 1,True)
+ eq_(math.fabs(coord.y - lon_lat_coord2.y) < 1,True)
+ eq_(math.fabs(coord.y - lon_lat_coord3.y) < 1,True)
+ eq_(math.fabs(coord.y - lon_lat_coord4.y) < 1,True)
+
+
+# Github Issue #2648
+def test_proj_antimeridian_bbox():
+ # this is logic from feature_style_processor::prepare_layer()
+ PROJ_ENVELOPE_POINTS = 20 # include/mapnik/config.hpp
+
+ prjGeog = mapnik.Projection('+init=epsg:4326')
+ prjProj = mapnik.Projection('+init=epsg:2193')
+ prj_trans_fwd = mapnik.ProjTransform(prjProj, prjGeog)
+ prj_trans_rev = mapnik.ProjTransform(prjGeog, prjProj)
+
+ # bad = mapnik.Box2d(-177.31453250437079, -62.33374815225163, 178.02778363316355, -24.584597490955804)
+ better = mapnik.Box2d(-180.0, -62.33374815225163, 180.0, -24.584597490955804)
+
+ buffered_query_ext = mapnik.Box2d(274000, 3087000, 3327000, 7173000)
+ fwd_ext = prj_trans_fwd.forward(buffered_query_ext, PROJ_ENVELOPE_POINTS)
+ assert_box2d_almost_equal(fwd_ext, better)
+
+ # check the same logic works for .backward()
+ ext = mapnik.Box2d(274000, 3087000, 3327000, 7173000)
+ rev_ext = prj_trans_rev.backward(ext, PROJ_ENVELOPE_POINTS)
+ assert_box2d_almost_equal(rev_ext, better)
+
+ # checks for not being snapped (ie. not antimeridian)
+ normal = mapnik.Box2d(148.766759749,-60.1222810238,159.95484893,-24.9771195151)
+ buffered_query_ext = mapnik.Box2d(274000, 3087000, 276000, 7173000)
+ fwd_ext = prj_trans_fwd.forward(buffered_query_ext, PROJ_ENVELOPE_POINTS)
+ assert_box2d_almost_equal(fwd_ext, normal)
+
+ # check the same logic works for .backward()
+ ext = mapnik.Box2d(274000, 3087000, 276000, 7173000)
+ rev_ext = prj_trans_rev.backward(ext, PROJ_ENVELOPE_POINTS)
+ assert_box2d_almost_equal(rev_ext, normal)
+
+
+if __name__ == "__main__":
+ exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/python_plugin_test.py b/test/python_tests/python_plugin_test.py
new file mode 100644
index 0000000..a39272f
--- /dev/null
+++ b/test/python_tests/python_plugin_test.py
@@ -0,0 +1,160 @@
+# #!/usr/bin/env python
+# # -*- coding: utf-8 -*-
+
+# import os
+# import math
+# import mapnik
+# import sys
+# from utilities import execution_path, run_all
+# from nose.tools import *
+
+# 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('.'))
+
+# class PointDatasource(mapnik.PythonDatasource):
+# def __init__(self):
+# super(PointDatasource, self).__init__(
+# geometry_type = mapnik.DataGeometryType.Point,
+# envelope = mapnik.Box2d(0,-10,100,110),
+# data_type = mapnik.DataType.Vector
+# )
+
+# def features(self, query):
+# return mapnik.PythonDatasource.wkt_features(
+# keys = ('label',),
+# features = (
+# ( 'POINT (5 6)', { 'label': 'foo-bar'} ),
+# ( 'POINT (60 50)', { 'label': 'buzz-quux'} ),
+# )
+# )
+
+# class ConcentricCircles(object):
+# def __init__(self, centre, bounds, step=1):
+# self.centre = centre
+# self.bounds = bounds
+# self.step = step
+
+# class Iterator(object):
+# def __init__(self, container):
+# self.container = container
+
+# centre = self.container.centre
+# bounds = self.container.bounds
+# step = self.container.step
+
+# self.radius = step
+
+# def next(self):
+# points = []
+# for alpha in xrange(0, 361, 5):
+# x = math.sin(math.radians(alpha)) * self.radius + self.container.centre[0]
+# y = math.cos(math.radians(alpha)) * self.radius + self.container.centre[1]
+# points.append('%s %s' % (x,y))
+# circle = 'POLYGON ((' + ','.join(points) + '))'
+
+# # has the circle grown so large that the boundary is entirely within it?
+# tl = (self.container.bounds.maxx, self.container.bounds.maxy)
+# tr = (self.container.bounds.maxx, self.container.bounds.maxy)
+# bl = (self.container.bounds.minx, self.container.bounds.miny)
+# br = (self.container.bounds.minx, self.container.bounds.miny)
+# def within_circle(p):
+# delta_x = p[0] - self.container.centre[0]
+# delta_y = p[0] - self.container.centre[0]
+# return delta_x*delta_x + delta_y*delta_y < self.radius*self.radius
+
+# if all(within_circle(p) for p in (tl,tr,bl,br)):
+# raise StopIteration()
+
+# self.radius += self.container.step
+# return ( circle, { } )
+
+# def __iter__(self):
+# return ConcentricCircles.Iterator(self)
+
+# class CirclesDatasource(mapnik.PythonDatasource):
+# def __init__(self, centre_x=-20, centre_y=0, step=10):
+# super(CirclesDatasource, self).__init__(
+# geometry_type = mapnik.DataGeometryType.Polygon,
+# envelope = mapnik.Box2d(-180, -90, 180, 90),
+# data_type = mapnik.DataType.Vector
+# )
+
+# # note that the plugin loader will set all arguments to strings and will not try to parse them
+# centre_x = int(centre_x)
+# centre_y = int(centre_y)
+# step = int(step)
+
+# self.centre_x = centre_x
+# self.centre_y = centre_y
+# self.step = step
+
+# def features(self, query):
+# centre = (self.centre_x, self.centre_y)
+
+# return mapnik.PythonDatasource.wkt_features(
+# keys = (),
+# features = ConcentricCircles(centre, query.bbox, self.step)
+# )
+
+# if 'python' in mapnik.DatasourceCache.plugin_names():
+# # make sure we can load from ourself as a module
+# sys.path.append(execution_path('.'))
+
+# def test_python_point_init():
+# ds = mapnik.Python(factory='python_plugin_test:PointDatasource')
+# e = ds.envelope()
+
+# assert_almost_equal(e.minx, 0, places=7)
+# assert_almost_equal(e.miny, -10, places=7)
+# assert_almost_equal(e.maxx, 100, places=7)
+# assert_almost_equal(e.maxy, 110, places=7)
+
+# def test_python_circle_init():
+# ds = mapnik.Python(factory='python_plugin_test:CirclesDatasource')
+# e = ds.envelope()
+
+# assert_almost_equal(e.minx, -180, places=7)
+# assert_almost_equal(e.miny, -90, places=7)
+# assert_almost_equal(e.maxx, 180, places=7)
+# assert_almost_equal(e.maxy, 90, places=7)
+
+# def test_python_circle_init_with_args():
+# ds = mapnik.Python(factory='python_plugin_test:CirclesDatasource', centre_x=40, centre_y=7)
+# e = ds.envelope()
+
+# assert_almost_equal(e.minx, -180, places=7)
+# assert_almost_equal(e.miny, -90, places=7)
+# assert_almost_equal(e.maxx, 180, places=7)
+# assert_almost_equal(e.maxy, 90, places=7)
+
+# def test_python_point_rendering():
+# m = mapnik.Map(512,512)
+# mapnik.load_map(m,'../data/python_plugin/python_point_datasource.xml')
+# m.zoom_all()
+# im = mapnik.Image(512,512)
+# mapnik.render(m,im)
+# actual = '/tmp/mapnik-python-point-render1.png'
+# expected = 'images/support/mapnik-python-point-render1.png'
+# im.save(actual)
+# expected_im = mapnik.Image.open(expected)
+# eq_(im.tostring('png32'),expected_im.tostring('png32'),
+# 'failed comparing actual (%s) and expected (%s)' % (actual,'tests/python_tests/'+ expected))
+
+# def test_python_circle_rendering():
+# m = mapnik.Map(512,512)
+# mapnik.load_map(m,'../data/python_plugin/python_circle_datasource.xml')
+# m.zoom_all()
+# im = mapnik.Image(512,512)
+# mapnik.render(m,im)
+# actual = '/tmp/mapnik-python-circle-render1.png'
+# expected = 'images/support/mapnik-python-circle-render1.png'
+# im.save(actual)
+# expected_im = mapnik.Image.open(expected)
+# eq_(im.tostring('png32'),expected_im.tostring('png32'),
+# 'failed comparing actual (%s) and expected (%s)' % (actual,'tests/python_tests/'+ expected))
+
+# if __name__ == "__main__":
+# setup()
+# run_all(eval(x) for x in dir() if x.startswith("test_"))
diff --git a/test/python_tests/query_test.py b/test/python_tests/query_test.py
new file mode 100644
index 0000000..8da3534
--- /dev/null
+++ b/test/python_tests/query_test.py
@@ -0,0 +1,37 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import os, mapnik
+
+from nose.tools import eq_,assert_almost_equal,raises
+from utilities import execution_path, run_all
+
+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_query_init():
+ bbox = (-180, -90, 180, 90)
+ query = mapnik.Query(mapnik.Box2d(*bbox))
+ r = query.resolution
+ assert_almost_equal(r[0], 1.0, places=7)
+ assert_almost_equal(r[1], 1.0, places=7)
+ # https://github.com/mapnik/mapnik/issues/1762
+ eq_(query.property_names,[])
+ query.add_property_name('migurski')
+ eq_(query.property_names,['migurski'])
+
+# Converting *from* tuples *to* resolutions is not yet supported
+@raises(TypeError)
+def test_query_resolution():
+ bbox = (-180, -90, 180, 90)
+ init_res = (4.5, 6.7)
+ query = mapnik.Query(mapnik.Box2d(*bbox), init_res)
+ r = query.resolution
+ assert_almost_equal(r[0], init_res[0], places=7)
+ assert_almost_equal(r[1], init_res[1], places=7)
+
+if __name__ == "__main__":
+ setup()
+ exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/query_tolerance_test.py b/test/python_tests/query_tolerance_test.py
new file mode 100644
index 0000000..97c1b3e
--- /dev/null
+++ b/test/python_tests/query_tolerance_test.py
@@ -0,0 +1,43 @@
+#!/usr/bin/env python
+
+from nose.tools import eq_
+from utilities import execution_path, run_all
+import os, mapnik
+
+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('.'))
+
+if 'shape' in mapnik.DatasourceCache.plugin_names():
+ def test_query_tolerance():
+ srs = '+init=epsg:4326'
+ lyr = mapnik.Layer('test')
+ ds = mapnik.Shapefile(file='../data/shp/arrows.shp')
+ lyr.datasource = ds
+ lyr.srs = srs
+ _width = 256
+ _map = mapnik.Map(_width,_width, srs)
+ _map.layers.append(lyr)
+ # zoom determines tolerance
+ _map.zoom_all()
+ _map_env = _map.envelope()
+ tol = (_map_env.maxx - _map_env.minx) / _width * 3
+ # 0.046875 for arrows.shp and zoom_all
+ eq_(tol,0.046875)
+ # check point really exists
+ x, y = 2.0, 4.0
+ features = _map.query_point(0,x,y).features
+ eq_(len(features),1)
+ # check inside tolerance limit
+ x = 2.0 + tol * 0.9
+ features = _map.query_point(0,x,y).features
+ eq_(len(features),1)
+ # check outside tolerance limit
+ x = 2.0 + tol * 1.1
+ features = _map.query_point(0,x,y).features
+ eq_(len(features),0)
+
+if __name__ == "__main__":
+ setup()
+ exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/raster_colorizer_test.py b/test/python_tests/raster_colorizer_test.py
new file mode 100644
index 0000000..6fb0102
--- /dev/null
+++ b/test/python_tests/raster_colorizer_test.py
@@ -0,0 +1,90 @@
+#coding=utf8
+import os
+import mapnik
+from utilities import execution_path, run_all
+from nose.tools import eq_
+
+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('.'))
+
+#test discrete colorizer mode
+def test_get_color_discrete():
+ #setup
+ colorizer = mapnik.RasterColorizer();
+ colorizer.default_color = mapnik.Color(0,0,0,0);
+ colorizer.default_mode = mapnik.COLORIZER_DISCRETE;
+
+ colorizer.add_stop(10, mapnik.Color(100,100,100,100));
+ colorizer.add_stop(20, mapnik.Color(200,200,200,200));
+
+ #should be default colour
+ eq_(colorizer.get_color(-50), mapnik.Color(0,0,0,0));
+ eq_(colorizer.get_color(0), mapnik.Color(0,0,0,0));
+
+ #now in stop 1
+ eq_(colorizer.get_color(10), mapnik.Color(100,100,100,100));
+ eq_(colorizer.get_color(19), mapnik.Color(100,100,100,100));
+
+ #now in stop 2
+ eq_(colorizer.get_color(20), mapnik.Color(200,200,200,200));
+ eq_(colorizer.get_color(1000), mapnik.Color(200,200,200,200));
+
+#test exact colorizer mode
+def test_get_color_exact():
+ #setup
+ colorizer = mapnik.RasterColorizer();
+ colorizer.default_color = mapnik.Color(0,0,0,0);
+ colorizer.default_mode = mapnik.COLORIZER_EXACT;
+
+ colorizer.add_stop(10, mapnik.Color(100,100,100,100));
+ colorizer.add_stop(20, mapnik.Color(200,200,200,200));
+
+ #should be default colour
+ eq_(colorizer.get_color(-50), mapnik.Color(0,0,0,0));
+ eq_(colorizer.get_color(11), mapnik.Color(0,0,0,0));
+ eq_(colorizer.get_color(20.001), mapnik.Color(0,0,0,0));
+
+ #should be stop 1
+ eq_(colorizer.get_color(10), mapnik.Color(100,100,100,100));
+
+ #should be stop 2
+ eq_(colorizer.get_color(20), mapnik.Color(200,200,200,200));
+
+#test linear colorizer mode
+def test_get_color_linear():
+ #setup
+ colorizer = mapnik.RasterColorizer();
+ colorizer.default_color = mapnik.Color(0,0,0,0);
+ colorizer.default_mode = mapnik.COLORIZER_LINEAR;
+
+ colorizer.add_stop(10, mapnik.Color(100,100,100,100));
+ colorizer.add_stop(20, mapnik.Color(200,200,200,200));
+
+ #should be default colour
+ eq_(colorizer.get_color(-50), mapnik.Color(0,0,0,0));
+ eq_(colorizer.get_color(9.9), mapnik.Color(0,0,0,0));
+
+ #should be stop 1
+ eq_(colorizer.get_color(10), mapnik.Color(100,100,100,100));
+
+ #should be stop 2
+ eq_(colorizer.get_color(20), mapnik.Color(200,200,200,200));
+
+ #half way between stops 1 and 2
+ eq_(colorizer.get_color(15), mapnik.Color(150,150,150,150));
+
+ #after stop 2
+ eq_(colorizer.get_color(100), mapnik.Color(200,200,200,200));
+
+def test_stop_label():
+ stop = mapnik.ColorizerStop(1, mapnik.COLORIZER_LINEAR, mapnik.Color('red'))
+ assert not stop.label
+ label = u"32º C".encode('utf8')
+ stop.label = label
+ assert stop.label == label, stop.label
+
+if __name__ == "__main__":
+ setup()
+ exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/raster_symbolizer_test.py b/test/python_tests/raster_symbolizer_test.py
new file mode 100644
index 0000000..9092118
--- /dev/null
+++ b/test/python_tests/raster_symbolizer_test.py
@@ -0,0 +1,217 @@
+#!/usr/bin/env python
+
+from nose.tools import eq_
+from utilities import execution_path, run_all, get_unique_colors
+
+import os, mapnik
+
+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_dataraster_coloring():
+ srs = '+init=epsg:32630'
+ lyr = mapnik.Layer('dataraster')
+ if 'gdal' in mapnik.DatasourceCache.plugin_names():
+ lyr.datasource = mapnik.Gdal(
+ file = '../data/raster/dataraster.tif',
+ band = 1,
+ )
+ lyr.srs = srs
+ _map = mapnik.Map(256,256, srs)
+ style = mapnik.Style()
+ rule = mapnik.Rule()
+ sym = mapnik.RasterSymbolizer()
+ # Assigning a colorizer to the RasterSymbolizer tells the later
+ # that it should use it to colorize the raw data raster
+ colorizer = mapnik.RasterColorizer(mapnik.COLORIZER_DISCRETE, mapnik.Color("transparent"))
+
+ for value, color in [
+ ( 0, "#0044cc"),
+ ( 10, "#00cc00"),
+ ( 20, "#ffff00"),
+ ( 30, "#ff7f00"),
+ ( 40, "#ff0000"),
+ ( 50, "#ff007f"),
+ ( 60, "#ff00ff"),
+ ( 70, "#cc00cc"),
+ ( 80, "#990099"),
+ ( 90, "#660066"),
+ ( 200, "transparent"),
+ ]:
+ colorizer.add_stop(value, mapnik.Color(color))
+ sym.colorizer = colorizer;
+ 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(lyr.envelope())
+
+ im = mapnik.Image(_map.width,_map.height)
+ mapnik.render(_map, im)
+ expected_file = './images/support/dataraster_coloring.png'
+ actual_file = '/tmp/' + os.path.basename(expected_file)
+ im.save(actual_file,'png32')
+ if not os.path.exists(expected_file) or os.environ.get('UPDATE'):
+ im.save(expected_file,'png32')
+ actual = mapnik.Image.open(actual_file)
+ expected = mapnik.Image.open(expected_file)
+ eq_(actual.tostring('png32'),expected.tostring('png32'), 'failed comparing actual (%s) and expected (%s)' % (actual_file,expected_file))
+
+def test_dataraster_query_point():
+ srs = '+init=epsg:32630'
+ lyr = mapnik.Layer('dataraster')
+ if 'gdal' in mapnik.DatasourceCache.plugin_names():
+ lyr.datasource = mapnik.Gdal(
+ file = '../data/raster/dataraster.tif',
+ band = 1,
+ )
+ lyr.srs = srs
+ _map = mapnik.Map(256,256, srs)
+ _map.layers.append(lyr)
+
+ x, y = 556113.0,4381428.0 # center of extent of raster
+ _map.zoom_all()
+ features = _map.query_point(0,x,y).features
+ assert len(features) == 1
+ feat = features[0]
+ center = feat.envelope().center()
+ assert center.x==x and center.y==y, center
+ value = feat['value']
+ assert value == 18.0, value
+
+ # point inside map extent but outside raster extent
+ current_box = _map.envelope()
+ current_box.expand_to_include(-427417,4477517)
+ _map.zoom_to_box(current_box)
+ features = _map.query_point(0,-427417,4477517).features
+ assert len(features) == 0
+
+ # point inside raster extent with nodata
+ features = _map.query_point(0,126850,4596050).features
+ assert len(features) == 0
+
+def test_load_save_map():
+ map = mapnik.Map(256,256)
+ in_map = "../data/good_maps/raster_symbolizer.xml"
+ try:
+ mapnik.load_map(map, in_map)
+
+ out_map = mapnik.save_map_to_string(map)
+ assert 'RasterSymbolizer' in out_map
+ assert 'RasterColorizer' in out_map
+ assert 'stop' in out_map
+ except RuntimeError, e:
+ # only test datasources that we have installed
+ if not 'Could not create datasource' in str(e):
+ raise RuntimeError(str(e))
+
+def test_raster_with_alpha_blends_correctly_with_background():
+ WIDTH = 500
+ HEIGHT = 500
+
+ map = mapnik.Map(WIDTH, HEIGHT)
+ WHITE = mapnik.Color(255, 255, 255)
+ map.background = WHITE
+
+ style = mapnik.Style()
+ rule = mapnik.Rule()
+ symbolizer = mapnik.RasterSymbolizer()
+ symbolizer.scaling = mapnik.scaling_method.BILINEAR
+
+ rule.symbols.append(symbolizer)
+ style.rules.append(rule)
+
+ map.append_style('raster_style', style)
+
+ map_layer = mapnik.Layer('test_layer')
+ filepath = '../data/raster/white-alpha.png'
+ if 'gdal' in mapnik.DatasourceCache.plugin_names():
+ map_layer.datasource = mapnik.Gdal(file=filepath)
+ map_layer.styles.append('raster_style')
+ map.layers.append(map_layer)
+
+ map.zoom_all()
+
+ mim = mapnik.Image(WIDTH, HEIGHT)
+
+ mapnik.render(map, mim)
+ mim.tostring()
+ # All white is expected
+ eq_(get_unique_colors(mim),['rgba(254,254,254,255)'])
+
+def test_raster_warping():
+ lyrSrs = "+init=epsg:32630"
+ mapSrs = '+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs'
+ lyr = mapnik.Layer('dataraster', lyrSrs)
+ if 'gdal' in mapnik.DatasourceCache.plugin_names():
+ lyr.datasource = mapnik.Gdal(
+ file = '../data/raster/dataraster.tif',
+ band = 1,
+ )
+ sym = mapnik.RasterSymbolizer()
+ sym.colorizer = mapnik.RasterColorizer(mapnik.COLORIZER_DISCRETE, mapnik.Color(255,255,0))
+ rule = mapnik.Rule()
+ rule.symbols.append(sym)
+ style = mapnik.Style()
+ style.rules.append(rule)
+ _map = mapnik.Map(256,256, mapSrs)
+ _map.append_style('foo', style)
+ lyr.styles.append('foo')
+ _map.layers.append(lyr)
+ map_proj = mapnik.Projection(mapSrs)
+ layer_proj = mapnik.Projection(lyrSrs)
+ prj_trans = mapnik.ProjTransform(map_proj,
+ layer_proj)
+ _map.zoom_to_box(prj_trans.backward(lyr.envelope()))
+
+ im = mapnik.Image(_map.width,_map.height)
+ mapnik.render(_map, im)
+ expected_file = './images/support/raster_warping.png'
+ actual_file = '/tmp/' + os.path.basename(expected_file)
+ im.save(actual_file,'png32')
+ if not os.path.exists(expected_file) or os.environ.get('UPDATE'):
+ im.save(expected_file,'png32')
+ actual = mapnik.Image.open(actual_file)
+ expected = mapnik.Image.open(expected_file)
+ eq_(actual.tostring('png32'),expected.tostring('png32'), 'failed comparing actual (%s) and expected (%s)' % (actual_file,expected_file))
+
+def test_raster_warping_does_not_overclip_source():
+ lyrSrs = "+init=epsg:32630"
+ mapSrs = '+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs'
+ lyr = mapnik.Layer('dataraster', lyrSrs)
+ if 'gdal' in mapnik.DatasourceCache.plugin_names():
+ lyr.datasource = mapnik.Gdal(
+ file = '../data/raster/dataraster.tif',
+ band = 1,
+ )
+ sym = mapnik.RasterSymbolizer()
+ sym.colorizer = mapnik.RasterColorizer(mapnik.COLORIZER_DISCRETE, mapnik.Color(255,255,0))
+ rule = mapnik.Rule()
+ rule.symbols.append(sym)
+ style = mapnik.Style()
+ style.rules.append(rule)
+ _map = mapnik.Map(256,256, mapSrs)
+ _map.background=mapnik.Color('white')
+ _map.append_style('foo', style)
+ lyr.styles.append('foo')
+ _map.layers.append(lyr)
+ _map.zoom_to_box(mapnik.Box2d(3,42,4,43))
+
+ im = mapnik.Image(_map.width,_map.height)
+ mapnik.render(_map, im)
+ expected_file = './images/support/raster_warping_does_not_overclip_source.png'
+ actual_file = '/tmp/' + os.path.basename(expected_file)
+ im.save(actual_file,'png32')
+ if not os.path.exists(expected_file) or os.environ.get('UPDATE'):
+ im.save(expected_file,'png32')
+ actual = mapnik.Image.open(actual_file)
+ expected = mapnik.Image.open(expected_file)
+ eq_(actual.tostring('png32'),expected.tostring('png32'), 'failed comparing actual (%s) and expected (%s)' % (actual_file,expected_file))
+
+if __name__ == "__main__":
+ setup()
+ exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/rasterlite_test.py b/test/python_tests/rasterlite_test.py
new file mode 100644
index 0000000..b15b157
--- /dev/null
+++ b/test/python_tests/rasterlite_test.py
@@ -0,0 +1,38 @@
+#!/usr/bin/env python
+
+from nose.tools import eq_,assert_almost_equal
+from utilities import execution_path, run_all
+
+import os, mapnik
+
+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('.'))
+
+
+if 'rasterlite' in mapnik.DatasourceCache.plugin_names():
+
+ def test_rasterlite():
+ ds = mapnik.Rasterlite(
+ file = '../data/rasterlite/globe.sqlite',
+ table = 'globe'
+ )
+ e = ds.envelope()
+
+ assert_almost_equal(e.minx,-180, places=5)
+ assert_almost_equal(e.miny, -90, places=5)
+ assert_almost_equal(e.maxx, 180, places=5)
+ assert_almost_equal(e.maxy, 90, places=5)
+ eq_(len(ds.fields()),0)
+ query = mapnik.Query(ds.envelope())
+ for fld in ds.fields():
+ query.add_property_name(fld)
+ fs = ds.features(query)
+ feat = fs.next()
+ eq_(feat.id(),1)
+ eq_(feat.attributes,{})
+
+if __name__ == "__main__":
+ setup()
+ exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/render_grid_test.py b/test/python_tests/render_grid_test.py
new file mode 100644
index 0000000..85c7401
--- /dev/null
+++ b/test/python_tests/render_grid_test.py
@@ -0,0 +1,356 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from nose.tools import eq_,raises
+from utilities import execution_path, run_all
+import os, mapnik
+
+try:
+ import json
+except ImportError:
+ import simplejson as json
+
+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('.'))
+
+if mapnik.has_grid_renderer():
+ def show_grids(name,g1,g2):
+ g1_file = '/tmp/mapnik-%s-actual.json' % name
+ open(g1_file,'w').write(json.dumps(g1,sort_keys=True))
+ g2_file = '/tmp/mapnik-%s-expected.json' % name
+ open(g2_file,'w').write(json.dumps(g2,sort_keys=True))
+ val = 'JSON does not match ->\n'
+ if g1['grid'] != g2['grid']:
+ val += ' X grid does not match\n'
+ else:
+ val += ' ✓ grid matches\n'
+ if g1['data'].keys() != g2['data'].keys():
+ val += ' X data does not match\n'
+ else:
+ val += ' ✓ data matches\n'
+ if g1['keys'] != g2['keys']:
+ val += ' X keys do not\n'
+ else:
+ val += ' ✓ keys match\n'
+ val += '\n\t%s\n\t%s' % (g1_file,g2_file)
+ return val
+
+ def show_grids2(name,g1,g2):
+ g2_expected = '../data/grids/mapnik-%s-actual.json' % name
+ if not os.path.exists(g2_expected):
+ # create test fixture based on actual results
+ open(g2_expected,'a+').write(json.dumps(g1,sort_keys=True))
+ return
+ g1_file = '/tmp/mapnik-%s-actual.json' % name
+ open(g1_file,'w').write(json.dumps(g1,sort_keys=True))
+ val = 'JSON does not match ->\n'
+ if g1['grid'] != g2['grid']:
+ val += ' X grid does not match\n'
+ else:
+ val += ' ✓ grid matches\n'
+ if g1['data'].keys() != g2['data'].keys():
+ val += ' X data does not match\n'
+ else:
+ val += ' ✓ data matches\n'
+ if g1['keys'] != g2['keys']:
+ val += ' X keys do not\n'
+ else:
+ val += ' ✓ keys match\n'
+ val += '\n\t%s\n\t%s' % (g1_file,g2_expected)
+ return val
+
+ # previous rendering using agg ellipse directly
+ grid_correct_new = {"data": {"North East": {"Name": "North East"}, "North West": {"Name": "North West"}, "South East": {"Name": "South East"}, "South West": {"Name": "South West"}}, "grid": [" ", " ", " ", " ", " [...]
+
+ # newer rendering using svg
+ grid_correct_new2 = {"data": {"North East": {"Name": "North East"}, "North West": {"Name": "North West"}, "South East": {"Name": "South East"}, "South West": {"Name": "South West"}}, "grid": [" ", " ", " ", " ", " [...]
+
+ grid_correct_new3 = {"data": {"North East": {"Name": "North East"}, "North West": {"Name": "North West"}, "South East": {"Name": "South East"}, "South West": {"Name": "South West"}}, "grid": [" ", " ", " ", " ", " [...]
+
+ def resolve(grid,row,col):
+ """ Resolve the attributes for a given pixel in a grid.
+ """
+ row = grid['grid'][row]
+ utf_val = row[col]
+ #http://docs.python.org/library/functions.html#ord
+ codepoint = ord(utf_val)
+ if (codepoint >= 93):
+ codepoint-=1
+ if (codepoint >= 35):
+ codepoint-=1
+ codepoint -= 32
+ key = grid['keys'][codepoint]
+ return grid['data'].get(key)
+
+
+ def create_grid_map(width,height,sym):
+ ds = mapnik.MemoryDatasource()
+ context = mapnik.Context()
+ context.push('Name')
+ f = mapnik.Feature(context,1)
+ f['Name'] = 'South East'
+ f.geometry = mapnik.Geometry.from_wkt('POINT (143.10 -38.60)')
+ ds.add_feature(f)
+
+ f = mapnik.Feature(context,2)
+ f['Name'] = 'South West'
+ f.geometry = mapnik.Geometry.from_wkt('POINT (142.48 -38.60)')
+ ds.add_feature(f)
+
+ f = mapnik.Feature(context,3)
+ f['Name'] = 'North West'
+ f.geometry = mapnik.Geometry.from_wkt('POINT (142.48 -38.38)')
+ ds.add_feature(f)
+
+ f = mapnik.Feature(context,4)
+ f['Name'] = 'North East'
+ f.geometry = mapnik.Geometry.from_wkt('POINT (143.10 -38.38)')
+ ds.add_feature(f)
+ s = mapnik.Style()
+ r = mapnik.Rule()
+ sym.allow_overlap = True
+ r.symbols.append(sym)
+ s.rules.append(r)
+ lyr = mapnik.Layer('Places')
+ lyr.datasource = ds
+ lyr.styles.append('places_labels')
+ m = mapnik.Map(width,height)
+ m.append_style('places_labels',s)
+ m.layers.append(lyr)
+ return m
+
+
+ def test_render_grid():
+ """ test render_grid method"""
+ width,height = 256,256
+ sym = mapnik.MarkersSymbolizer()
+ sym.width = mapnik.Expression('10')
+ sym.height = mapnik.Expression('10')
+ m = create_grid_map(width,height,sym)
+ ul_lonlat = mapnik.Coord(142.30,-38.20)
+ lr_lonlat = mapnik.Coord(143.40,-38.80)
+ m.zoom_to_box(mapnik.Box2d(ul_lonlat,lr_lonlat))
+
+ # new method
+ grid = mapnik.Grid(m.width,m.height,key='Name')
+ mapnik.render_layer(m,grid,layer=0,fields=['Name'])
+ utf1 = grid.encode('utf',resolution=4)
+ eq_(utf1,grid_correct_new3,show_grids('new-markers',utf1,grid_correct_new3))
+
+ # check a full view is the same as a full image
+ grid_view = grid.view(0,0,width,height)
+ # for kicks check at full res too
+ utf3 = grid.encode('utf',resolution=1)
+ utf4 = grid_view.encode('utf',resolution=1)
+ eq_(utf3['grid'],utf4['grid'])
+ eq_(utf3['keys'],utf4['keys'])
+ eq_(utf3['data'],utf4['data'])
+
+ eq_(resolve(utf4,0,0),None)
+
+ # resolve some center points in the
+ # resampled view
+ utf5 = grid_view.encode('utf',resolution=4)
+ eq_(resolve(utf5,25,10),{"Name": "North West"})
+ eq_(resolve(utf5,25,46),{"Name": "North East"})
+ eq_(resolve(utf5,38,10),{"Name": "South West"})
+ eq_(resolve(utf5,38,46),{"Name": "South East"})
+
+
+ grid_feat_id = {'keys': ['', '3', '4', '2', '1'], 'data': {'1': {'Name': 'South East'}, '3': {'Name': u'North West'}, '2': {'Name': 'South West'}, '4': {'Name': 'North East'}}, 'grid': [' ', ' ', ' ', ' ', ' [...]
+
+ grid_feat_id2 = {"data": {"1": {"Name": "South East"}, "2": {"Name": "South West"}, "3": {"Name": "North West"}, "4": {"Name": "North East"}}, "grid": [" ", " ", " ", " ", " [...]
+
+ grid_feat_id3 = {"data": {"1": {"Name": "South East", "__id__": 1}, "2": {"Name": "South West", "__id__": 2}, "3": {"Name": "North West", "__id__": 3}, "4": {"Name": "North East", "__id__": 4}}, "grid": [" ", " ", " ", " ", " [...]
+
+ def test_render_grid3():
+ """ test using feature id"""
+ width,height = 256,256
+ sym = mapnik.MarkersSymbolizer()
+ sym.width = mapnik.Expression('10')
+ sym.height = mapnik.Expression('10')
+ m = create_grid_map(width,height,sym)
+ ul_lonlat = mapnik.Coord(142.30,-38.20)
+ lr_lonlat = mapnik.Coord(143.40,-38.80)
+ m.zoom_to_box(mapnik.Box2d(ul_lonlat,lr_lonlat))
+
+ grid = mapnik.Grid(m.width,m.height,key='__id__')
+ mapnik.render_layer(m,grid,layer=0,fields=['__id__','Name'])
+ utf1 = grid.encode('utf',resolution=4)
+ eq_(utf1,grid_feat_id3,show_grids('id-markers',utf1,grid_feat_id3))
+ # check a full view is the same as a full image
+ grid_view = grid.view(0,0,width,height)
+ # for kicks check at full res too
+ utf3 = grid.encode('utf',resolution=1)
+ utf4 = grid_view.encode('utf',resolution=1)
+ eq_(utf3['grid'],utf4['grid'])
+ eq_(utf3['keys'],utf4['keys'])
+ eq_(utf3['data'],utf4['data'])
+
+ eq_(resolve(utf4,0,0),None)
+
+ # resolve some center points in the
+ # resampled view
+ utf5 = grid_view.encode('utf',resolution=4)
+ eq_(resolve(utf5,25,10),{"Name": "North West","__id__": 3})
+ eq_(resolve(utf5,25,46),{"Name": "North East","__id__": 4})
+ eq_(resolve(utf5,38,10),{"Name": "South West","__id__": 2})
+ eq_(resolve(utf5,38,46),{"Name": "South East","__id__": 1})
+
+
+ def gen_grid_for_id(pixel_key):
+ ds = mapnik.MemoryDatasource()
+ context = mapnik.Context()
+ context.push('Name')
+ f = mapnik.Feature(context,pixel_key)
+ f['Name'] = str(pixel_key)
+ f.geometry = mapnik.Geometry.from_wkt('POLYGON ((0 0, 0 256, 256 256, 256 0, 0 0))')
+ ds.add_feature(f)
+ s = mapnik.Style()
+ r = mapnik.Rule()
+ symb = mapnik.PolygonSymbolizer()
+ r.symbols.append(symb)
+ s.rules.append(r)
+ lyr = mapnik.Layer('Places')
+ lyr.datasource = ds
+ lyr.styles.append('places_labels')
+ width,height = 256,256
+ m = mapnik.Map(width,height)
+ m.append_style('places_labels',s)
+ m.layers.append(lyr)
+ m.zoom_all()
+ grid = mapnik.Grid(m.width,m.height,key='__id__')
+ mapnik.render_layer(m,grid,layer=0,fields=['__id__','Name'])
+ return grid
+
+ def test_negative_id():
+ grid = gen_grid_for_id(-1)
+ eq_(grid.get_pixel(128,128),-1)
+ utf1 = grid.encode('utf',resolution=4)
+ eq_(utf1['keys'],['-1'])
+
+ def test_32bit_int_id():
+ int32 = 2147483647
+ grid = gen_grid_for_id(int32)
+ eq_(grid.get_pixel(128,128),int32)
+ utf1 = grid.encode('utf',resolution=4)
+ eq_(utf1['keys'],[str(int32)])
+ max_neg = -(int32)
+ grid = gen_grid_for_id(max_neg)
+ eq_(grid.get_pixel(128,128),max_neg)
+ utf1 = grid.encode('utf',resolution=4)
+ eq_(utf1['keys'],[str(max_neg)])
+
+ def test_64bit_int_id():
+ int64 = 0x7FFFFFFFFFFFFFFF
+ grid = gen_grid_for_id(int64)
+ eq_(grid.get_pixel(128,128),int64)
+ utf1 = grid.encode('utf',resolution=4)
+ eq_(utf1['keys'],[str(int64)])
+ max_neg = -(int64)
+ grid = gen_grid_for_id(max_neg)
+ eq_(grid.get_pixel(128,128),max_neg)
+ utf1 = grid.encode('utf',resolution=4)
+ eq_(utf1['keys'],[str(max_neg)])
+
+ def test_id_zero():
+ grid = gen_grid_for_id(0)
+ eq_(grid.get_pixel(128,128),0)
+ utf1 = grid.encode('utf',resolution=4)
+ eq_(utf1['keys'],['0'])
+
+ line_expected = {"keys": ["", "1"], "data": {"1": {"Name": "1"}}, "grid": [" !", " !! ", " !! ", " !! ", " !! ", " !! ", " [...]
+
+ def test_line_rendering():
+ ds = mapnik.MemoryDatasource()
+ context = mapnik.Context()
+ context.push('Name')
+ pixel_key = 1
+ f = mapnik.Feature(context,pixel_key)
+ f['Name'] = str(pixel_key)
+ f.geometry = mapnik.Geometry.from_wkt('LINESTRING (30 10, 10 30, 40 40)')
+ ds.add_feature(f)
+ s = mapnik.Style()
+ r = mapnik.Rule()
+ symb = mapnik.LineSymbolizer()
+ r.symbols.append(symb)
+ s.rules.append(r)
+ lyr = mapnik.Layer('Places')
+ lyr.datasource = ds
+ lyr.styles.append('places_labels')
+ width,height = 256,256
+ m = mapnik.Map(width,height)
+ m.append_style('places_labels',s)
+ m.layers.append(lyr)
+ m.zoom_all()
+ #mapnik.render_to_file(m,'test.png')
+ grid = mapnik.Grid(m.width,m.height,key='__id__')
+ mapnik.render_layer(m,grid,layer=0,fields=['Name'])
+ utf1 = grid.encode()
+ eq_(utf1,line_expected,show_grids('line',utf1,line_expected))
+
+ point_expected = {"data": {"1": {"Name": "South East"}, "2": {"Name": "South West"}, "3": {"Name": "North West"}, "4": {"Name": "North East"}}, "grid": [" ", " ", " ", " ", " [...]
+
+ def test_point_symbolizer_grid():
+ width,height = 256,256
+ sym = mapnik.PointSymbolizer()
+ sym.file = '../data/images/dummy.png'
+ m = create_grid_map(width,height,sym)
+ ul_lonlat = mapnik.Coord(142.30,-38.20)
+ lr_lonlat = mapnik.Coord(143.40,-38.80)
+ m.zoom_to_box(mapnik.Box2d(ul_lonlat,lr_lonlat))
+ grid = mapnik.Grid(m.width,m.height)
+ mapnik.render_layer(m,grid,layer=0,fields=['Name'])
+ utf1 = grid.encode()
+ eq_(utf1,point_expected,show_grids('point-sym',utf1,point_expected))
+
+ test_point_symbolizer_grid.requires_data = True
+
+ # should throw because this is a mis-usage
+ # https://github.com/mapnik/mapnik/issues/1325
+ @raises(RuntimeError)
+ def test_render_to_grid_multiple_times():
+ # create map with two layers
+ m = mapnik.Map(256,256)
+ s = mapnik.Style()
+ r = mapnik.Rule()
+ sym = mapnik.MarkersSymbolizer()
+ sym.allow_overlap = True
+ r.symbols.append(sym)
+ s.rules.append(r)
+ m.append_style('points',s)
+
+ # NOTE: we use a csv datasource here
+ # because the memorydatasource fails silently for
+ # queries requesting fields that do not exist in the datasource
+ ds1 = mapnik.Datasource(**{"type":"csv","inline":'''
+ wkt,Name
+ "POINT (143.10 -38.60)",South East'''})
+ lyr1 = mapnik.Layer('One')
+ lyr1.datasource = ds1
+ lyr1.styles.append('points')
+ m.layers.append(lyr1)
+
+ ds2 = mapnik.Datasource(**{"type":"csv","inline":'''
+ wkt,Value
+ "POINT (142.48 -38.60)",South West'''})
+ lyr2 = mapnik.Layer('Two')
+ lyr2.datasource = ds2
+ lyr2.styles.append('points')
+ m.layers.append(lyr2)
+
+ ul_lonlat = mapnik.Coord(142.30,-38.20)
+ lr_lonlat = mapnik.Coord(143.40,-38.80)
+ m.zoom_to_box(mapnik.Box2d(ul_lonlat,lr_lonlat))
+ grid = mapnik.Grid(m.width,m.height)
+ mapnik.render_layer(m,grid,layer=0,fields=['Name'])
+ # should throw right here since Name will be a property now on the `grid` object
+ # and it is not found on the second layer
+ mapnik.render_layer(m,grid,layer=1,fields=['Value'])
+ grid.encode()
+
+if __name__ == "__main__":
+ setup()
+ exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/render_test.py b/test/python_tests/render_test.py
new file mode 100644
index 0000000..197d010
--- /dev/null
+++ b/test/python_tests/render_test.py
@@ -0,0 +1,241 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from nose.tools import eq_,raises
+import tempfile
+import os, mapnik
+from utilities import execution_path, run_all
+
+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_simplest_render():
+ m = mapnik.Map(256, 256)
+ im = mapnik.Image(m.width, m.height)
+ eq_(im.painted(),False)
+ eq_(im.is_solid(),True)
+ mapnik.render(m, im)
+ eq_(im.painted(),False)
+ eq_(im.is_solid(),True)
+ s = im.tostring()
+ eq_(s, 256 * 256 * '\x00\x00\x00\x00')
+
+def test_render_image_to_string():
+ im = mapnik.Image(256, 256)
+ im.fill(mapnik.Color('black'))
+ eq_(im.painted(),False)
+ eq_(im.is_solid(),True)
+ s = im.tostring()
+ eq_(s, 256 * 256 * '\x00\x00\x00\xff')
+
+def test_non_solid_image():
+ im = mapnik.Image(256, 256)
+ im.fill(mapnik.Color('black'))
+ eq_(im.painted(),False)
+ eq_(im.is_solid(),True)
+ # set one pixel to a different color
+ im.set_pixel(0,0,mapnik.Color('white'))
+ eq_(im.painted(),False)
+ eq_(im.is_solid(),False)
+
+def test_non_solid_image_view():
+ im = mapnik.Image(256, 256)
+ im.fill(mapnik.Color('black'))
+ view = im.view(0,0,256,256)
+ eq_(view.is_solid(),True)
+ # set one pixel to a different color
+ im.set_pixel(0,0,mapnik.Color('white'))
+ eq_(im.is_solid(),False)
+ # view, since it is the exact dimensions of the image
+ # should also be non-solid
+ eq_(view.is_solid(),False)
+ # but not a view that excludes the single diff pixel
+ view2 = im.view(1,1,256,256)
+ eq_(view2.is_solid(),True)
+
+def test_setting_alpha():
+ w,h = 256,256
+ im1 = mapnik.Image(w,h)
+ # white, half transparent
+ c1 = mapnik.Color('rgba(255,255,255,.5)')
+ im1.fill(c1)
+ eq_(im1.painted(),False)
+ eq_(im1.is_solid(),True)
+ # pure white
+ im2 = mapnik.Image(w,h)
+ c2 = mapnik.Color('rgba(255,255,255,1)')
+ im2.fill(c2)
+ im2.apply_opacity(c1.a/255.0)
+ eq_(im2.painted(),False)
+ eq_(im2.is_solid(),True)
+ eq_(len(im1.tostring('png32')), len(im2.tostring('png32')))
+
+def test_render_image_to_file():
+ im = mapnik.Image(256, 256)
+ im.fill(mapnik.Color('black'))
+ if mapnik.has_jpeg():
+ im.save('test.jpg')
+ im.save('test.png', 'png')
+ if os.path.exists('test.jpg'):
+ os.remove('test.jpg')
+ else:
+ return False
+ if os.path.exists('test.png'):
+ os.remove('test.png')
+ else:
+ return False
+
+def get_paired_images(w,h,mapfile):
+ tmp_map = 'tmp_map.xml'
+ m = mapnik.Map(w,h)
+ mapnik.load_map(m,mapfile)
+ im = mapnik.Image(w,h)
+ m.zoom_all()
+ mapnik.render(m,im)
+ mapnik.save_map(m,tmp_map)
+ m2 = mapnik.Map(w,h)
+ mapnik.load_map(m2,tmp_map)
+ im2 = mapnik.Image(w,h)
+ m2.zoom_all()
+ mapnik.render(m2,im2)
+ os.remove(tmp_map)
+ return im,im2
+
+def test_render_from_serialization():
+ try:
+ im,im2 = get_paired_images(100,100,'../data/good_maps/building_symbolizer.xml')
+ eq_(im.tostring('png32'),im2.tostring('png32'))
+
+ im,im2 = get_paired_images(100,100,'../data/good_maps/polygon_symbolizer.xml')
+ eq_(im.tostring('png32'),im2.tostring('png32'))
+ except RuntimeError, e:
+ # only test datasources that we have installed
+ if not 'Could not create datasource' in str(e):
+ raise RuntimeError(e)
+
+def test_render_points():
+ if not mapnik.has_cairo(): return
+ # create and populate point datasource (WGS84 lat-lon coordinates)
+ ds = mapnik.MemoryDatasource()
+ context = mapnik.Context()
+ context.push('Name')
+ f = mapnik.Feature(context,1)
+ f['Name'] = 'Westernmost Point'
+ f.geometry = mapnik.Geometry.from_wkt('POINT (142.48 -38.38)')
+ ds.add_feature(f)
+
+ f = mapnik.Feature(context,2)
+ f['Name'] = 'Southernmost Point'
+ f.geometry = mapnik.Geometry.from_wkt('POINT (143.10 -38.60)')
+ ds.add_feature(f)
+
+ # create layer/rule/style
+ s = mapnik.Style()
+ r = mapnik.Rule()
+ symb = mapnik.PointSymbolizer()
+ symb.allow_overlap = True
+ r.symbols.append(symb)
+ s.rules.append(r)
+ lyr = mapnik.Layer('Places','+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs')
+ lyr.datasource = ds
+ lyr.styles.append('places_labels')
+ # latlon bounding box corners
+ ul_lonlat = mapnik.Coord(142.30,-38.20)
+ lr_lonlat = mapnik.Coord(143.40,-38.80)
+ # render for different projections
+ projs = {
+ 'google': '+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0.0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs +over',
+ 'latlon': '+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs',
+ 'merc': '+proj=merc +datum=WGS84 +k=1.0 +units=m +over +no_defs',
+ 'utm': '+proj=utm +zone=54 +datum=WGS84'
+ }
+ for projdescr in projs.iterkeys():
+ m = mapnik.Map(1000, 500, projs[projdescr])
+ m.append_style('places_labels',s)
+ m.layers.append(lyr)
+ dest_proj = mapnik.Projection(projs[projdescr])
+ src_proj = mapnik.Projection('+init=epsg:4326')
+ tr = mapnik.ProjTransform(src_proj,dest_proj)
+ m.zoom_to_box(tr.forward(mapnik.Box2d(ul_lonlat,lr_lonlat)))
+ # Render to SVG so that it can be checked how many points are there with string comparison
+ svg_file = os.path.join(tempfile.gettempdir(), 'mapnik-render-points-%s.svg' % projdescr)
+ mapnik.render_to_file(m, svg_file)
+ num_points_present = len(ds.all_features())
+ svg = open(svg_file,'r').read()
+ num_points_rendered = svg.count('<image ')
+ eq_(num_points_present, num_points_rendered, "Not all points were rendered (%d instead of %d) at projection %s" % (num_points_rendered, num_points_present, projdescr))
+
+@raises(RuntimeError)
+def test_render_with_scale_factor_zero_throws():
+ m = mapnik.Map(256,256)
+ im = mapnik.Image(256, 256)
+ mapnik.render(m,im,0.0)
+
+def test_render_with_detector():
+ ds = mapnik.MemoryDatasource()
+ context = mapnik.Context()
+ geojson = '{ "type": "Feature", "geometry": { "type": "Point", "coordinates": [ 0, 0 ] } }'
+ ds.add_feature(mapnik.Feature.from_geojson(geojson,context))
+ s = mapnik.Style()
+ r = mapnik.Rule()
+ lyr = mapnik.Layer('point')
+ lyr.datasource = ds
+ lyr.styles.append('point')
+ symb = mapnik.MarkersSymbolizer()
+ symb.allow_overlap = False
+ r.symbols.append(symb)
+ s.rules.append(r)
+ m = mapnik.Map(256,256)
+ m.append_style('point',s)
+ m.layers.append(lyr)
+ m.zoom_to_box(mapnik.Box2d(-180,-85,180,85))
+ im = mapnik.Image(256, 256)
+ mapnik.render(m,im)
+ expected_file = './images/support/marker-in-center.png'
+ actual_file = '/tmp/' + os.path.basename(expected_file)
+ #im.save(expected_file,'png8')
+ im.save(actual_file,'png8')
+ actual = mapnik.Image.open(expected_file)
+ expected = mapnik.Image.open(expected_file)
+ eq_(actual.tostring('png32'),expected.tostring('png32'), 'failed comparing actual (%s) and expected (%s)' % (actual_file,expected_file))
+ # now render will a collision detector that should
+ # block out the placement of this point
+ detector = mapnik.LabelCollisionDetector(m)
+ eq_(detector.extent(),mapnik.Box2d(-0.0,-0.0,m.width,m.height))
+ eq_(detector.extent(),mapnik.Box2d(-0.0,-0.0,256.0,256.0))
+ eq_(detector.boxes(),[])
+ detector.insert(detector.extent())
+ eq_(detector.boxes(),[detector.extent()])
+ im2 = mapnik.Image(256, 256)
+ mapnik.render_with_detector(m, im2, detector)
+ expected_file_collision = './images/support/marker-in-center-not-placed.png'
+ #im2.save(expected_file_collision,'png8')
+ actual_file = '/tmp/' + os.path.basename(expected_file_collision)
+ im2.save(actual_file,'png8')
+
+
+if 'shape' in mapnik.DatasourceCache.plugin_names():
+
+ def test_render_with_scale_factor():
+ m = mapnik.Map(256,256)
+ mapnik.load_map(m,'../data/good_maps/marker-text-line.xml')
+ m.zoom_all()
+ sizes = [.00001,.005,.1,.899,1,1.5,2,5,10,100]
+ for size in sizes:
+ im = mapnik.Image(256, 256)
+ mapnik.render(m,im,size)
+ expected_file = './images/support/marker-text-line-scale-factor-%s.png' % size
+ actual_file = '/tmp/' + os.path.basename(expected_file)
+ im.save(actual_file,'png32')
+ if os.environ.get('UPDATE'):
+ im.save(expected_file,'png32')
+ # we save and re-open here so both png8 images are ready as full color png
+ actual = mapnik.Image.open(actual_file)
+ expected = mapnik.Image.open(expected_file)
+ eq_(actual.tostring('png32'),expected.tostring('png32'), 'failed comparing actual (%s) and expected (%s)' % (actual_file,expected_file))
+
+if __name__ == "__main__":
+ setup()
+ exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/reprojection_test.py b/test/python_tests/reprojection_test.py
new file mode 100644
index 0000000..1382db5
--- /dev/null
+++ b/test/python_tests/reprojection_test.py
@@ -0,0 +1,92 @@
+#coding=utf8
+import os
+import mapnik
+from utilities import execution_path, run_all
+from nose.tools import eq_
+
+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('.'))
+
+if 'shape' in mapnik.DatasourceCache.plugin_names():
+
+ #@raises(RuntimeError)
+ def test_zoom_all_will_fail():
+ m = mapnik.Map(512,512)
+ mapnik.load_map(m,'../data/good_maps/wgs842merc_reprojection.xml')
+ m.zoom_all()
+
+ def test_zoom_all_will_work_with_max_extent():
+ m = mapnik.Map(512,512)
+ mapnik.load_map(m,'../data/good_maps/wgs842merc_reprojection.xml')
+ merc_bounds = mapnik.Box2d(-20037508.34,-20037508.34,20037508.34,20037508.34)
+ m.maximum_extent = merc_bounds
+ m.zoom_all()
+ # note - fixAspectRatio is being called, then re-clipping to maxextent
+ # which makes this hard to predict
+ #eq_(m.envelope(),merc_bounds)
+
+ #m = mapnik.Map(512,512)
+ #mapnik.load_map(m,'../data/good_maps/wgs842merc_reprojection.xml')
+ #merc_bounds = mapnik.Box2d(-20037508.34,-20037508.34,20037508.34,20037508.34)
+ #m.zoom_to_box(merc_bounds)
+ #eq_(m.envelope(),merc_bounds)
+
+
+ def test_visual_zoom_all_rendering1():
+ m = mapnik.Map(512,512)
+ mapnik.load_map(m,'../data/good_maps/wgs842merc_reprojection.xml')
+ merc_bounds = mapnik.Box2d(-20037508.34,-20037508.34,20037508.34,20037508.34)
+ m.maximum_extent = merc_bounds
+ m.zoom_all()
+ im = mapnik.Image(512,512)
+ mapnik.render(m,im)
+ actual = '/tmp/mapnik-wgs842merc-reprojection-render.png'
+ expected = 'images/support/mapnik-wgs842merc-reprojection-render.png'
+ im.save(actual,'png32')
+ expected_im = mapnik.Image.open(expected)
+ eq_(im.tostring('png32'),expected_im.tostring('png32'), 'failed comparing actual (%s) and expected (%s)' % (actual,'test/python_tests/'+ expected))
+
+ def test_visual_zoom_all_rendering2():
+ m = mapnik.Map(512,512)
+ mapnik.load_map(m,'../data/good_maps/merc2wgs84_reprojection.xml')
+ m.zoom_all()
+ im = mapnik.Image(512,512)
+ mapnik.render(m,im)
+ actual = '/tmp/mapnik-merc2wgs84-reprojection-render.png'
+ expected = 'images/support/mapnik-merc2wgs84-reprojection-render.png'
+ im.save(actual,'png32')
+ expected_im = mapnik.Image.open(expected)
+ eq_(im.tostring('png32'),expected_im.tostring('png32'), 'failed comparing actual (%s) and expected (%s)' % (actual,'test/python_tests/'+ expected))
+
+ # maximum-extent read from map.xml
+ def test_visual_zoom_all_rendering3():
+ m = mapnik.Map(512,512)
+ mapnik.load_map(m,'../data/good_maps/bounds_clipping.xml')
+ m.zoom_all()
+ im = mapnik.Image(512,512)
+ mapnik.render(m,im)
+ actual = '/tmp/mapnik-merc2merc-reprojection-render1.png'
+ expected = 'images/support/mapnik-merc2merc-reprojection-render1.png'
+ im.save(actual,'png32')
+ expected_im = mapnik.Image.open(expected)
+ eq_(im.tostring('png32'),expected_im.tostring('png32'), 'failed comparing actual (%s) and expected (%s)' % (actual,'test/python_tests/'+ expected))
+
+ # no maximum-extent
+ def test_visual_zoom_all_rendering4():
+ m = mapnik.Map(512,512)
+ mapnik.load_map(m,'../data/good_maps/bounds_clipping.xml')
+ m.maximum_extent = None
+ m.zoom_all()
+ im = mapnik.Image(512,512)
+ mapnik.render(m,im)
+ actual = '/tmp/mapnik-merc2merc-reprojection-render2.png'
+ expected = 'images/support/mapnik-merc2merc-reprojection-render2.png'
+ im.save(actual,'png32')
+ expected_im = mapnik.Image.open(expected)
+ eq_(im.tostring('png32'),expected_im.tostring('png32'), 'failed comparing actual (%s) and expected (%s)' % (actual,'test/python_tests/'+ expected))
+
+if __name__ == "__main__":
+ setup()
+ exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/save_map_test.py b/test/python_tests/save_map_test.py
new file mode 100644
index 0000000..d7c1f03
--- /dev/null
+++ b/test/python_tests/save_map_test.py
@@ -0,0 +1,76 @@
+#!/usr/bin/env python
+
+from nose.tools import eq_
+from utilities import execution_path, run_all
+import tempfile
+
+import os, glob, mapnik
+
+default_logging_severity = mapnik.logger.get_severity()
+
+def setup():
+ # make the tests silent to suppress unsupported params from harfbuzz tests
+ # TODO: remove this after harfbuzz branch merges
+ mapnik.logger.set_severity(mapnik.severity_type.None)
+ # All of the paths used are relative, if we run the tests
+ # from another directory we need to chdir()
+ os.chdir(execution_path('.'))
+
+def teardown():
+ mapnik.logger.set_severity(default_logging_severity)
+
+def compare_map(xml):
+ m = mapnik.Map(256, 256)
+ absolute_base = os.path.abspath(os.path.dirname(xml))
+ try:
+ mapnik.load_map(m, xml, False, absolute_base)
+ except RuntimeError, e:
+ # only test datasources that we have installed
+ if not 'Could not create datasource' in str(e) \
+ and not 'could not connect' in str(e):
+ raise RuntimeError(str(e))
+ return
+ (handle, test_map) = tempfile.mkstemp(suffix='.xml', prefix='mapnik-temp-map1-')
+ os.close(handle)
+ (handle, test_map2) = tempfile.mkstemp(suffix='.xml', prefix='mapnik-temp-map2-')
+ os.close(handle)
+ if os.path.exists(test_map):
+ os.remove(test_map)
+ mapnik.save_map(m, test_map)
+ new_map = mapnik.Map(256, 256)
+ mapnik.load_map(new_map, test_map,False,absolute_base)
+ open(test_map2,'w').write(mapnik.save_map_to_string(new_map))
+ diff = ' diff -u %s %s' % (os.path.abspath(test_map),os.path.abspath(test_map2))
+ try:
+ eq_(open(test_map).read(),open(test_map2).read())
+ except AssertionError, e:
+ raise AssertionError('serialized map "%s" not the same after being reloaded, \ncompare with command:\n\n$%s' % (xml,diff))
+
+ if os.path.exists(test_map):
+ os.remove(test_map)
+ else:
+ # Fail, the map wasn't written
+ return False
+
+def test_compare_map():
+ good_maps = glob.glob("../data/good_maps/*.xml")
+ good_maps = [os.path.normpath(p) for p in good_maps]
+ # remove one map that round trips CDATA differently, but this is okay
+ ignorable = os.path.join('..','data','good_maps','empty_parameter2.xml')
+ good_maps.remove(ignorable)
+ for m in good_maps:
+ compare_map(m)
+
+ for m in glob.glob("../visual_tests/styles/*.xml"):
+ compare_map(m)
+
+# TODO - enforce that original xml does not equal first saved xml
+def test_compare_map_deprecations():
+ dep = glob.glob("../data/deprecated_maps/*.xml")
+ dep = [os.path.normpath(p) for p in dep]
+ for m in dep:
+ compare_map(m)
+
+if __name__ == "__main__":
+ setup()
+ exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/shapefile_test.py b/test/python_tests/shapefile_test.py
new file mode 100644
index 0000000..eccf30c
--- /dev/null
+++ b/test/python_tests/shapefile_test.py
@@ -0,0 +1,113 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from nose.tools import eq_,assert_almost_equal,raises
+from utilities import execution_path, run_all
+import os, mapnik
+
+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('.'))
+
+if 'shape' in mapnik.DatasourceCache.plugin_names():
+
+ # Shapefile initialization
+ def test_shapefile_init():
+ s = mapnik.Shapefile(file='../data/shp/boundaries')
+
+ e = s.envelope()
+
+ assert_almost_equal(e.minx, -11121.6896651, places=7)
+ assert_almost_equal(e.miny, -724724.216526, places=6)
+ assert_almost_equal(e.maxx, 2463000.67866, places=5)
+ assert_almost_equal(e.maxy, 1649661.267, places=3)
+
+ # Shapefile properties
+ def test_shapefile_properties():
+ s = mapnik.Shapefile(file='../data/shp/boundaries', encoding='latin1')
+ f = s.features_at_point(s.envelope().center()).features[0]
+
+ eq_(f['CGNS_FID'], u'6f733341ba2011d892e2080020a0f4c9')
+ eq_(f['COUNTRY'], u'CAN')
+ eq_(f['F_CODE'], u'FA001')
+ eq_(f['NAME_EN'], u'Quebec')
+ # this seems to break if icu data linking is not working
+ eq_(f['NOM_FR'], u'Qu\xe9bec')
+ eq_(f['NOM_FR'], u'Québec')
+ eq_(f['Shape_Area'], 1512185733150.0)
+ eq_(f['Shape_Leng'], 19218883.724300001)
+
+ @raises(RuntimeError)
+ def test_that_nonexistant_query_field_throws(**kwargs):
+ ds = mapnik.Shapefile(file='../data/shp/world_merc')
+ eq_(len(ds.fields()),11)
+ eq_(ds.fields(),['FIPS', 'ISO2', 'ISO3', 'UN', 'NAME', 'AREA', 'POP2005', 'REGION', 'SUBREGION', 'LON', 'LAT'])
+ eq_(ds.field_types(),['str', 'str', 'str', 'int', 'str', 'int', 'int', 'int', 'int', 'float', 'float'])
+ query = mapnik.Query(ds.envelope())
+ for fld in ds.fields():
+ query.add_property_name(fld)
+ # also add an invalid one, triggering throw
+ query.add_property_name('bogus')
+ ds.features(query)
+
+ def test_dbf_logical_field_is_boolean():
+ ds = mapnik.Shapefile(file='../data/shp/long_lat')
+ eq_(len(ds.fields()),7)
+ eq_(ds.fields(),['LONG', 'LAT', 'LOGICAL_TR', 'LOGICAL_FA', 'CHARACTER', 'NUMERIC', 'DATE'])
+ eq_(ds.field_types(),['str', 'str', 'bool', 'bool', 'str', 'float', 'str'])
+ query = mapnik.Query(ds.envelope())
+ for fld in ds.fields():
+ query.add_property_name(fld)
+ feat = ds.all_features()[0]
+ eq_(feat.id(),1)
+ eq_(feat['LONG'],'0')
+ eq_(feat['LAT'],'0')
+ eq_(feat['LOGICAL_TR'],True)
+ eq_(feat['LOGICAL_FA'],False)
+ eq_(feat['CHARACTER'],'254')
+ eq_(feat['NUMERIC'],32)
+ eq_(feat['DATE'],'20121202')
+
+ # created by hand in qgis 1.8.0
+ def test_shapefile_point2d_from_qgis():
+ ds = mapnik.Shapefile(file='../data/shp/points/qgis.shp')
+ eq_(len(ds.fields()),2)
+ eq_(ds.fields(),['id','name'])
+ eq_(ds.field_types(),['int','str'])
+ eq_(len(ds.all_features()),3)
+
+ # ogr2ogr tests/data/shp/3dpoint/ogr_zfield.shp tests/data/shp/3dpoint/qgis.shp -zfield id
+ def test_shapefile_point_z_from_qgis():
+ ds = mapnik.Shapefile(file='../data/shp/points/ogr_zfield.shp')
+ eq_(len(ds.fields()),2)
+ eq_(ds.fields(),['id','name'])
+ eq_(ds.field_types(),['int','str'])
+ eq_(len(ds.all_features()),3)
+
+ def test_shapefile_multipoint_from_qgis():
+ ds = mapnik.Shapefile(file='../data/shp/points/qgis_multi.shp')
+ eq_(len(ds.fields()),2)
+ eq_(ds.fields(),['id','name'])
+ eq_(ds.field_types(),['int','str'])
+ eq_(len(ds.all_features()),1)
+
+ # pointzm from arcinfo
+ def test_shapefile_point_zm_from_arcgis():
+ ds = mapnik.Shapefile(file='../data/shp/points/poi.shp')
+ eq_(len(ds.fields()),7)
+ eq_(ds.fields(),['interst_id', 'state_d', 'cnty_name', 'latitude', 'longitude', 'Name', 'Website'])
+ eq_(ds.field_types(),['str', 'str', 'str', 'float', 'float', 'str', 'str'])
+ eq_(len(ds.all_features()),17)
+
+ # copy of the above with ogr2ogr that makes m record 14 instead of 18
+ def test_shapefile_point_zm_from_ogr():
+ ds = mapnik.Shapefile(file='../data/shp/points/poi_ogr.shp')
+ eq_(len(ds.fields()),7)
+ eq_(ds.fields(),['interst_id', 'state_d', 'cnty_name', 'latitude', 'longitude', 'Name', 'Website'])
+ eq_(ds.field_types(),['str', 'str', 'str', 'float', 'float', 'str', 'str'])
+ eq_(len(ds.all_features()),17)
+
+if __name__ == "__main__":
+ setup()
+ exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/shapeindex_test.py b/test/python_tests/shapeindex_test.py
new file mode 100644
index 0000000..4de19a5
--- /dev/null
+++ b/test/python_tests/shapeindex_test.py
@@ -0,0 +1,51 @@
+#!/usr/bin/env python
+
+from nose.tools import eq_
+from utilities import execution_path, run_all
+from subprocess import Popen, PIPE
+import shutil
+import os
+import fnmatch
+import mapnik
+
+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_shapeindex():
+ # first copy shapefiles to tmp directory
+ source_dir = '../data/shp/'
+ working_dir = '/tmp/mapnik-shp-tmp/'
+ if os.path.exists(working_dir):
+ shutil.rmtree(working_dir)
+ shutil.copytree(source_dir,working_dir)
+ matches = []
+ for root, dirnames, filenames in os.walk('%s' % source_dir):
+ for filename in fnmatch.filter(filenames, '*.shp'):
+ matches.append(os.path.join(root, filename))
+ for shp in matches:
+ source_file = os.path.join(source_dir,os.path.relpath(shp,source_dir))
+ dest_file = os.path.join(working_dir,os.path.relpath(shp,source_dir))
+ ds = mapnik.Shapefile(file=source_file)
+ count = 0;
+ fs = ds.featureset()
+ try:
+ while (fs.next()):
+ count = count+1
+ except StopIteration:
+ pass
+ stdin, stderr = Popen('shapeindex %s' % dest_file, shell=True, stdout=PIPE, stderr=PIPE).communicate()
+ ds2 = mapnik.Shapefile(file=dest_file)
+ count2 = 0;
+ fs = ds.featureset()
+ try:
+ while (fs.next()):
+ count2 = count2+1
+ except StopIteration:
+ pass
+ eq_(count,count2)
+
+if __name__ == "__main__":
+ setup()
+ exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/sqlite_rtree_test.py b/test/python_tests/sqlite_rtree_test.py
new file mode 100644
index 0000000..3036e29
--- /dev/null
+++ b/test/python_tests/sqlite_rtree_test.py
@@ -0,0 +1,169 @@
+#!/usr/bin/env python
+
+from nose.tools import eq_
+from utilities import execution_path, run_all
+import threading
+
+import os, mapnik
+import sqlite3
+
+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('.'))
+
+NUM_THREADS = 10
+TOTAL = 245
+
+def create_ds(test_db,table):
+ ds = mapnik.SQLite(file=test_db,table=table)
+ ds.all_features()
+ del ds
+
+if 'sqlite' in mapnik.DatasourceCache.plugin_names():
+
+ def test_rtree_creation():
+ test_db = '../data/sqlite/world.sqlite'
+ index = test_db +'.index'
+ table = 'world_merc'
+
+ if os.path.exists(index):
+ os.unlink(index)
+
+ threads = []
+ for i in range(NUM_THREADS):
+ t = threading.Thread(target=create_ds,args=(test_db,table))
+ t.start()
+ threads.append(t)
+
+ for i in threads:
+ i.join()
+
+ eq_(os.path.exists(index),True)
+ conn = sqlite3.connect(index)
+ cur = conn.cursor()
+ try:
+ cur.execute("Select count(*) from idx_%s_GEOMETRY" % table.replace("'",""))
+ conn.commit()
+ eq_(cur.fetchone()[0],TOTAL)
+ except sqlite3.OperationalError:
+ # don't worry about testing # of index records if
+ # python's sqlite module does not support rtree
+ pass
+ cur.close()
+ conn.close()
+
+ ds = mapnik.SQLite(file=test_db,table=table)
+ fs = ds.all_features()
+ del ds
+ eq_(len(fs),TOTAL)
+ os.unlink(index)
+ ds = mapnik.SQLite(file=test_db,table=table,use_spatial_index=False)
+ fs = ds.all_features()
+ del ds
+ eq_(len(fs),TOTAL)
+ eq_(os.path.exists(index),False)
+
+ ds = mapnik.SQLite(file=test_db,table=table,use_spatial_index=True)
+ fs = ds.all_features()
+ #TODO - this loop is not releasing something
+ # because it causes the unlink below to fail on windows
+ # as the file is still open
+ #for feat in fs:
+ # query = mapnik.Query(feat.envelope())
+ # selected = ds.features(query)
+ # eq_(len(selected.features)>=1,True)
+ del ds
+
+ eq_(os.path.exists(index),True)
+ os.unlink(index)
+
+ test_rtree_creation.requires_data = True
+
+ def test_geometry_round_trip():
+ test_db = '/tmp/mapnik-sqlite-point.db'
+ ogr_metadata = True
+
+ # create test db
+ conn = sqlite3.connect(test_db)
+ cur = conn.cursor()
+ cur.execute('''
+ CREATE TABLE IF NOT EXISTS point_table
+ (id INTEGER PRIMARY KEY AUTOINCREMENT, geometry BLOB, name varchar)
+ ''')
+ # optional: but nice if we want to read with ogr
+ if ogr_metadata:
+ cur.execute('''CREATE TABLE IF NOT EXISTS geometry_columns (
+ f_table_name VARCHAR,
+ f_geometry_column VARCHAR,
+ geometry_type INTEGER,
+ coord_dimension INTEGER,
+ srid INTEGER,
+ geometry_format VARCHAR )''')
+ cur.execute('''INSERT INTO geometry_columns
+ (f_table_name, f_geometry_column, geometry_format,
+ geometry_type, coord_dimension, srid) VALUES
+ ('point_table','geometry','WKB', 1, 1, 4326)''')
+ conn.commit()
+ cur.close()
+
+ # add a point as wkb (using mapnik) to match how an ogr created db looks
+ x = -122 # longitude
+ y = 48 # latitude
+ wkt = 'POINT(%s %s)' % (x,y)
+ # little endian wkb (mapnik will auto-detect and ready either little or big endian (XDR))
+ wkb = mapnik.Geometry.from_wkt(wkt).to_wkb(mapnik.wkbByteOrder.NDR)
+ values = (None,sqlite3.Binary(wkb),"test point")
+ cur = conn.cursor()
+ cur.execute('''INSERT into "point_table" (id,geometry,name) values (?,?,?)''',values)
+ conn.commit()
+ cur.close()
+ conn.close()
+
+ def make_wkb_point(x,y):
+ import struct
+ byteorder = 1; # little endian
+ endianess = ''
+ if byteorder == 1:
+ endianess = '<'
+ else:
+ endianess = '>'
+ geom_type = 1; # for a point
+ return struct.pack('%sbldd' % endianess, byteorder, geom_type, x, y)
+
+ # confirm the wkb matches a manually formed wkb
+ wkb2 = make_wkb_point(x,y)
+ eq_(wkb,wkb2)
+
+ # ensure we can read this data back out properly with mapnik
+ ds = mapnik.Datasource(**{'type':'sqlite','file':test_db, 'table':'point_table'})
+ fs = ds.featureset()
+ feat = fs.next()
+ eq_(feat.id(),1)
+ eq_(feat['name'],'test point')
+ geom = feat.geometry;
+ eq_(geom.to_wkt(),'POINT(-122 48)')
+ del ds
+
+ # ensure it matches data read with just sqlite
+ conn = sqlite3.connect(test_db)
+ cur = conn.cursor()
+ cur.execute('''SELECT * from point_table''')
+ conn.commit()
+ result = cur.fetchone()
+ cur.close()
+ feat_id = result[0]
+ eq_(feat_id,1)
+ name = result[2]
+ eq_(name,'test point')
+ geom_wkb_blob = result[1]
+ eq_(str(geom_wkb_blob),geom.to_wkb(mapnik.wkbByteOrder.NDR))
+ new_geom = mapnik.Geometry.from_wkb(str(geom_wkb_blob))
+ eq_(new_geom.to_wkt(),geom.to_wkt())
+ conn.close()
+ os.unlink(test_db)
+
+if __name__ == "__main__":
+ setup()
+ returncode = run_all(eval(x) for x in dir() if x.startswith("test_"))
+ exit(returncode)
diff --git a/test/python_tests/sqlite_test.py b/test/python_tests/sqlite_test.py
new file mode 100644
index 0000000..69b8a6d
--- /dev/null
+++ b/test/python_tests/sqlite_test.py
@@ -0,0 +1,501 @@
+#!/usr/bin/env python
+
+from nose.tools import eq_, raises
+from utilities import execution_path, run_all
+import os
+import mapnik
+
+
+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 teardown():
+ index = '../data/sqlite/world.sqlite.index'
+ if os.path.exists(index):
+ os.unlink(index)
+
+if 'sqlite' in mapnik.DatasourceCache.plugin_names():
+
+ def test_attachdb_with_relative_file():
+ # The point table and index is in the qgis_spatiallite.sqlite
+ # database. If either is not found, then this fails
+ ds = mapnik.SQLite(file='../data/sqlite/world.sqlite',
+ table='point',
+ attachdb='***@qgis_spatiallite.sqlite'
+ )
+ fs = ds.featureset()
+ feature = fs.next()
+ eq_(feature['pkuid'],1)
+
+ test_attachdb_with_relative_file.requires_data = True
+
+ def test_attachdb_with_multiple_files():
+ ds = mapnik.SQLite(file='../data/sqlite/world.sqlite',
+ table='attachedtest',
+ attachdb='scratch1@:memory:,scratch2@:memory:',
+ initdb='''
+ create table scratch1.attachedtest (the_geom);
+ create virtual table scratch2.idx_attachedtest_the_geom using rtree(pkid,xmin,xmax,ymin,ymax);
+ insert into scratch2.idx_attachedtest_the_geom values (1,-7799225.5,-7778571.0,1393264.125,1417719.375);
+ '''
+ )
+ fs = ds.featureset()
+ feature = None
+ try :
+ feature = fs.next()
+ except StopIteration:
+ pass
+ # the above should not throw but will result in no features
+ eq_(feature,None)
+
+ test_attachdb_with_multiple_files.requires_data = True
+
+ def test_attachdb_with_absolute_file():
+ # The point table and index is in the qgis_spatiallite.sqlite
+ # database. If either is not found, then this fails
+ ds = mapnik.SQLite(file=os.getcwd() + '/../data/sqlite/world.sqlite',
+ table='point',
+ attachdb='***@qgis_spatiallite.sqlite'
+ )
+ fs = ds.featureset()
+ feature = fs.next()
+ eq_(feature['pkuid'],1)
+
+ test_attachdb_with_absolute_file.requires_data = True
+
+ def test_attachdb_with_index():
+ ds = mapnik.SQLite(file='../data/sqlite/world.sqlite',
+ table='attachedtest',
+ attachdb='scratch@:memory:',
+ initdb='''
+ create table scratch.attachedtest (the_geom);
+ create virtual table scratch.idx_attachedtest_the_geom using rtree(pkid,xmin,xmax,ymin,ymax);
+ insert into scratch.idx_attachedtest_the_geom values (1,-7799225.5,-7778571.0,1393264.125,1417719.375);
+ '''
+ )
+
+ fs = ds.featureset()
+ feature = None
+ try :
+ feature = fs.next()
+ except StopIteration:
+ pass
+ eq_(feature,None)
+
+ test_attachdb_with_index.requires_data = True
+
+ def test_attachdb_with_explicit_index():
+ ds = mapnik.SQLite(file='../data/sqlite/world.sqlite',
+ table='attachedtest',
+ index_table='myindex',
+ attachdb='scratch@:memory:',
+ initdb='''
+ create table scratch.attachedtest (the_geom);
+ create virtual table scratch.myindex using rtree(pkid,xmin,xmax,ymin,ymax);
+ insert into scratch.myindex values (1,-7799225.5,-7778571.0,1393264.125,1417719.375);
+ '''
+ )
+ fs = ds.featureset()
+ feature = None
+ try:
+ feature = fs.next()
+ except StopIteration:
+ pass
+ eq_(feature,None)
+
+ test_attachdb_with_explicit_index.requires_data = True
+
+ def test_attachdb_with_sql_join():
+ ds = mapnik.SQLite(file='../data/sqlite/world.sqlite',
+ table='(select * from world_merc INNER JOIN business on world_merc.iso3 = business.ISO3 limit 100)',
+ attachdb='***@business.sqlite'
+ )
+ eq_(len(ds.fields()),29)
+ eq_(ds.fields(),['OGC_FID', 'fips', 'iso2', 'iso3', 'un', 'name', 'area', 'pop2005', 'region', 'subregion', 'lon', 'lat', 'ISO3:1', '1995', '1996', '1997', '1998', '1999', '2000', '2001', '2002', '2003', '2004', '2005', '2006', '2007', '2008', '2009', '2010'])
+ eq_(ds.field_types(),['int', 'str', 'str', 'str', 'int', 'str', 'int', 'int', 'int', 'int', 'float', 'float', 'str', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int'])
+ fs = ds.featureset()
+ feature = fs.next()
+ eq_(feature.id(),1)
+ expected = {
+ 1995:0,
+ 1996:0,
+ 1997:0,
+ 1998:0,
+ 1999:0,
+ 2000:0,
+ 2001:0,
+ 2002:0,
+ 2003:0,
+ 2004:0,
+ 2005:0,
+ 2006:0,
+ 2007:0,
+ 2008:0,
+ 2009:0,
+ 2010:0,
+ # this appears to be sqlites way of
+ # automatically handling clashing column names
+ 'ISO3:1':'ATG',
+ 'OGC_FID':1,
+ 'area':44,
+ 'fips':u'AC',
+ 'iso2':u'AG',
+ 'iso3':u'ATG',
+ 'lat':17.078,
+ 'lon':-61.783,
+ 'name':u'Antigua and Barbuda',
+ 'pop2005':83039,
+ 'region':19,
+ 'subregion':29,
+ 'un':28
+ }
+ for k,v in expected.items():
+ try:
+ eq_(feature[str(k)],v)
+ except:
+ #import pdb;pdb.set_trace()
+ print 'invalid key/v %s/%s for: %s' % (k,v,feature)
+
+ test_attachdb_with_sql_join.requires_data = True
+
+ def test_attachdb_with_sql_join_count():
+ ds = mapnik.SQLite(file='../data/sqlite/world.sqlite',
+ table='(select * from world_merc INNER JOIN business on world_merc.iso3 = business.ISO3 limit 100)',
+ attachdb='***@business.sqlite'
+ )
+ eq_(len(ds.fields()),29)
+ eq_(ds.fields(),['OGC_FID', 'fips', 'iso2', 'iso3', 'un', 'name', 'area', 'pop2005', 'region', 'subregion', 'lon', 'lat', 'ISO3:1', '1995', '1996', '1997', '1998', '1999', '2000', '2001', '2002', '2003', '2004', '2005', '2006', '2007', '2008', '2009', '2010'])
+ eq_(ds.field_types(),['int', 'str', 'str', 'str', 'int', 'str', 'int', 'int', 'int', 'int', 'float', 'float', 'str', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int'])
+ eq_(len(ds.all_features()),100)
+
+ test_attachdb_with_sql_join_count.requires_data = True
+
+ def test_attachdb_with_sql_join_count2():
+ '''
+ sqlite3 world.sqlite
+ attach database 'business.sqlite' as business;
+ select count(*) from world_merc INNER JOIN business on world_merc.iso3 = business.ISO3;
+ '''
+ ds = mapnik.SQLite(file='../data/sqlite/world.sqlite',
+ table='(select * from world_merc INNER JOIN business on world_merc.iso3 = business.ISO3)',
+ attachdb='***@business.sqlite'
+ )
+ eq_(len(ds.fields()),29)
+ eq_(ds.fields(),['OGC_FID', 'fips', 'iso2', 'iso3', 'un', 'name', 'area', 'pop2005', 'region', 'subregion', 'lon', 'lat', 'ISO3:1', '1995', '1996', '1997', '1998', '1999', '2000', '2001', '2002', '2003', '2004', '2005', '2006', '2007', '2008', '2009', '2010'])
+ eq_(ds.field_types(),['int', 'str', 'str', 'str', 'int', 'str', 'int', 'int', 'int', 'int', 'float', 'float', 'str', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int'])
+ eq_(len(ds.all_features()),192)
+
+ test_attachdb_with_sql_join_count2.requires_data = True
+
+ def test_attachdb_with_sql_join_count3():
+ '''
+ select count(*) from (select * from world_merc where 1=1) as world_merc INNER JOIN business on world_merc.iso3 = business.ISO3;
+ '''
+ ds = mapnik.SQLite(file='../data/sqlite/world.sqlite',
+ table='(select * from (select * from world_merc where !intersects!) as world_merc INNER JOIN business on world_merc.iso3 = business.ISO3)',
+ attachdb='***@business.sqlite'
+ )
+ eq_(len(ds.fields()),29)
+ eq_(ds.fields(),['OGC_FID', 'fips', 'iso2', 'iso3', 'un', 'name', 'area', 'pop2005', 'region', 'subregion', 'lon', 'lat', 'ISO3:1', '1995', '1996', '1997', '1998', '1999', '2000', '2001', '2002', '2003', '2004', '2005', '2006', '2007', '2008', '2009', '2010'])
+ eq_(ds.field_types(),['int', 'str', 'str', 'str', 'int', 'str', 'int', 'int', 'int', 'int', 'float', 'float', 'str', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int'])
+ eq_(len(ds.all_features()),192)
+
+ test_attachdb_with_sql_join_count3.requires_data = True
+
+ def test_attachdb_with_sql_join_count4():
+ '''
+ select count(*) from (select * from world_merc where 1=1) as world_merc INNER JOIN business on world_merc.iso3 = business.ISO3;
+ '''
+ ds = mapnik.SQLite(file='../data/sqlite/world.sqlite',
+ table='(select * from (select * from world_merc where !intersects! limit 1) as world_merc INNER JOIN business on world_merc.iso3 = business.ISO3)',
+ attachdb='***@business.sqlite'
+ )
+ eq_(len(ds.fields()),29)
+ eq_(ds.fields(),['OGC_FID', 'fips', 'iso2', 'iso3', 'un', 'name', 'area', 'pop2005', 'region', 'subregion', 'lon', 'lat', 'ISO3:1', '1995', '1996', '1997', '1998', '1999', '2000', '2001', '2002', '2003', '2004', '2005', '2006', '2007', '2008', '2009', '2010'])
+ eq_(ds.field_types(),['int', 'str', 'str', 'str', 'int', 'str', 'int', 'int', 'int', 'int', 'float', 'float', 'str', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int'])
+ eq_(len(ds.all_features()),1)
+
+ test_attachdb_with_sql_join_count4.requires_data = True
+
+ def test_attachdb_with_sql_join_count5():
+ '''
+ select count(*) from (select * from world_merc where 1=1) as world_merc INNER JOIN business on world_merc.iso3 = business.ISO3;
+ '''
+ ds = mapnik.SQLite(file='../data/sqlite/world.sqlite',
+ table='(select * from (select * from world_merc where !intersects! and 1=2) as world_merc INNER JOIN business on world_merc.iso3 = business.ISO3)',
+ attachdb='***@business.sqlite'
+ )
+ # nothing is able to join to business so we don't pick up business schema
+ eq_(len(ds.fields()),12)
+ eq_(ds.fields(),['OGC_FID', 'fips', 'iso2', 'iso3', 'un', 'name', 'area', 'pop2005', 'region', 'subregion', 'lon', 'lat'])
+ eq_(ds.field_types(),['int', 'str', 'str', 'str', 'int', 'str', 'int', 'int', 'int', 'int', 'float', 'float'])
+ eq_(len(ds.all_features()),0)
+
+ test_attachdb_with_sql_join_count5.requires_data = True
+
+ def test_subqueries():
+ ds = mapnik.SQLite(file='../data/sqlite/world.sqlite',
+ table='world_merc',
+ )
+ fs = ds.featureset()
+ feature = fs.next()
+ eq_(feature['OGC_FID'],1)
+ eq_(feature['fips'],u'AC')
+ eq_(feature['iso2'],u'AG')
+ eq_(feature['iso3'],u'ATG')
+ eq_(feature['un'],28)
+ eq_(feature['name'],u'Antigua and Barbuda')
+ eq_(feature['area'],44)
+ eq_(feature['pop2005'],83039)
+ eq_(feature['region'],19)
+ eq_(feature['subregion'],29)
+ eq_(feature['lon'],-61.783)
+ eq_(feature['lat'],17.078)
+
+ ds = mapnik.SQLite(file='../data/sqlite/world.sqlite',
+ table='(select * from world_merc)',
+ )
+ fs = ds.featureset()
+ feature = fs.next()
+ eq_(feature['OGC_FID'],1)
+ eq_(feature['fips'],u'AC')
+ eq_(feature['iso2'],u'AG')
+ eq_(feature['iso3'],u'ATG')
+ eq_(feature['un'],28)
+ eq_(feature['name'],u'Antigua and Barbuda')
+ eq_(feature['area'],44)
+ eq_(feature['pop2005'],83039)
+ eq_(feature['region'],19)
+ eq_(feature['subregion'],29)
+ eq_(feature['lon'],-61.783)
+ eq_(feature['lat'],17.078)
+
+ ds = mapnik.SQLite(file='../data/sqlite/world.sqlite',
+ table='(select OGC_FID,GEOMETRY from world_merc)',
+ )
+ fs = ds.featureset()
+ feature = fs.next()
+ eq_(feature['OGC_FID'],1)
+ eq_(len(feature),1)
+
+ ds = mapnik.SQLite(file='../data/sqlite/world.sqlite',
+ table='(select GEOMETRY,OGC_FID,fips from world_merc)',
+ )
+ fs = ds.featureset()
+ feature = fs.next()
+ eq_(feature['OGC_FID'],1)
+ eq_(feature['fips'],u'AC')
+
+ # same as above, except with alias like postgres requires
+ # TODO - should we try to make this work?
+ #ds = mapnik.SQLite(file='../data/sqlite/world.sqlite',
+ # table='(select GEOMETRY,rowid as aliased_id,fips from world_merc) as table',
+ # key_field='aliased_id'
+ # )
+ #fs = ds.featureset()
+ #feature = fs.next()
+ #eq_(feature['aliased_id'],1)
+ #eq_(feature['fips'],u'AC')
+
+ ds = mapnik.SQLite(file='../data/sqlite/world.sqlite',
+ table='(select GEOMETRY,OGC_FID,OGC_FID as rowid,fips from world_merc)',
+ )
+ fs = ds.featureset()
+ feature = fs.next()
+ eq_(feature['rowid'],1)
+ eq_(feature['fips'],u'AC')
+
+ test_subqueries.requires_data = True
+
+ def test_empty_db():
+ ds = mapnik.SQLite(file='../data/sqlite/empty.db',
+ table='empty',
+ )
+ fs = ds.featureset()
+ feature = None
+ try:
+ feature = fs.next()
+ except StopIteration:
+ pass
+ eq_(feature,None)
+
+ test_empty_db.requires_data = True
+
+ @raises(RuntimeError)
+ def test_that_nonexistant_query_field_throws(**kwargs):
+ ds = mapnik.SQLite(file='../data/sqlite/empty.db',
+ table='empty',
+ )
+ eq_(len(ds.fields()),25)
+ eq_(ds.fields(),['OGC_FID', 'scalerank', 'labelrank', 'featurecla', 'sovereignt', 'sov_a3', 'adm0_dif', 'level', 'type', 'admin', 'adm0_a3', 'geou_dif', 'name', 'abbrev', 'postal', 'name_forma', 'terr_', 'name_sort', 'map_color', 'pop_est', 'gdp_md_est', 'fips_10_', 'iso_a2', 'iso_a3', 'iso_n3'])
+ eq_(ds.field_types(),['int', 'int', 'int', 'str', 'str', 'str', 'float', 'float', 'str', 'str', 'str', 'float', 'str', 'str', 'str', 'str', 'str', 'str', 'float', 'float', 'float', 'float', 'str', 'str', 'float'])
+ query = mapnik.Query(ds.envelope())
+ for fld in ds.fields():
+ query.add_property_name(fld)
+ # also add an invalid one, triggering throw
+ query.add_property_name('bogus')
+ ds.features(query)
+
+ test_that_nonexistant_query_field_throws.requires_data = True
+
+ def test_intersects_token1():
+ ds = mapnik.SQLite(file='../data/sqlite/empty.db',
+ table='(select * from empty where !intersects!)',
+ )
+ fs = ds.featureset()
+ feature = None
+ try :
+ feature = fs.next()
+ except StopIteration:
+ pass
+ eq_(feature,None)
+
+ test_intersects_token1.requires_data = True
+
+ def test_intersects_token2():
+ ds = mapnik.SQLite(file='../data/sqlite/empty.db',
+ table='(select * from empty where "a"!="b" and !intersects!)',
+ )
+ fs = ds.featureset()
+ feature = None
+ try :
+ feature = fs.next()
+ except StopIteration:
+ pass
+ eq_(feature,None)
+
+ test_intersects_token2.requires_data = True
+
+ def test_intersects_token3():
+ ds = mapnik.SQLite(file='../data/sqlite/empty.db',
+ table='(select * from empty where "a"!="b" and !intersects!)',
+ )
+ fs = ds.featureset()
+ feature = None
+ try :
+ feature = fs.next()
+ except StopIteration:
+ pass
+ eq_(feature,None)
+
+ test_intersects_token3.requires_data = True
+
+ # https://github.com/mapnik/mapnik/issues/1537
+ # this works because key_field is manually set
+ def test_db_with_one_text_column():
+ # form up an in-memory test db
+ wkb = '010100000000000000000000000000000000000000'
+ ds = mapnik.SQLite(file=':memory:',
+ table='test1',
+ initdb='''
+ create table test1 (alias TEXT,geometry BLOB);
+ insert into test1 values ("test",x'%s');
+ ''' % wkb,
+ extent='-180,-60,180,60',
+ use_spatial_index=False,
+ key_field='alias'
+ )
+ eq_(len(ds.fields()),1)
+ eq_(ds.fields(),['alias'])
+ eq_(ds.field_types(),['str'])
+ fs = ds.all_features()
+ eq_(len(fs),1)
+ feat = fs[0]
+ eq_(feat.id(),0) # should be 1?
+ eq_(feat['alias'],'test')
+ eq_(feat.geometry.to_wkt(),'POINT(0 0)')
+
+ def test_db_with_one_untyped_column():
+ # form up an in-memory test db
+ wkb = '010100000000000000000000000000000000000000'
+ ds = mapnik.SQLite(file=':memory:',
+ table='test1',
+ initdb='''
+ create table test1 (geometry BLOB, untyped);
+ insert into test1 values (x'%s', 'untyped');
+ ''' % wkb,
+ extent='-180,-60,180,60',
+ use_spatial_index=False,
+ key_field='rowid'
+ )
+
+ # ensure the untyped column is found
+ eq_(len(ds.fields()),2)
+ eq_(ds.fields(),['rowid', 'untyped'])
+ eq_(ds.field_types(),['int', 'str'])
+
+ def test_db_with_one_untyped_column_using_subquery():
+ # form up an in-memory test db
+ wkb = '010100000000000000000000000000000000000000'
+ ds = mapnik.SQLite(file=':memory:',
+ table='(SELECT rowid, geometry, untyped FROM test1)',
+ initdb='''
+ create table test1 (geometry BLOB, untyped);
+ insert into test1 values (x'%s', 'untyped');
+ ''' % wkb,
+ extent='-180,-60,180,60',
+ use_spatial_index=False,
+ key_field='rowid'
+ )
+
+ # ensure the untyped column is found
+ eq_(len(ds.fields()),3)
+ eq_(ds.fields(),['rowid', 'untyped', 'rowid'])
+ eq_(ds.field_types(),['int', 'str', 'int'])
+
+
+ def test_that_64bit_int_fields_work():
+ ds = mapnik.SQLite(file='../data/sqlite/64bit_int.sqlite',
+ table='int_table',
+ use_spatial_index=False
+ )
+ eq_(len(ds.fields()),3)
+ eq_(ds.fields(),['OGC_FID','id','bigint'])
+ eq_(ds.field_types(),['int','int','int'])
+ fs = ds.featureset()
+ feat = fs.next()
+ eq_(feat.id(),1)
+ eq_(feat['OGC_FID'],1)
+ eq_(feat['bigint'],2147483648)
+ feat = fs.next()
+ eq_(feat.id(),2)
+ eq_(feat['OGC_FID'],2)
+ eq_(feat['bigint'],922337203685477580)
+
+ test_that_64bit_int_fields_work.requires_data = True
+
+ def test_null_id_field():
+ # silence null key warning: https://github.com/mapnik/mapnik/issues/1889
+ default_logging_severity = mapnik.logger.get_severity()
+ mapnik.logger.set_severity(mapnik.severity_type.None)
+ # form up an in-memory test db
+ wkb = '010100000000000000000000000000000000000000'
+ # note: the osm_id should be declared INTEGER PRIMARY KEY
+ # but in this case we intentionally do not make this a valid pkey
+ # otherwise sqlite would turn the null into a valid, serial id
+ ds = mapnik.SQLite(file=':memory:',
+ table='test1',
+ initdb='''
+ create table test1 (osm_id INTEGER,geometry BLOB);
+ insert into test1 values (null,x'%s');
+ ''' % wkb,
+ extent='-180,-60,180,60',
+ use_spatial_index=False,
+ key_field='osm_id'
+ )
+ fs = ds.featureset()
+ feature = None
+ try :
+ feature = fs.next()
+ except StopIteration:
+ pass
+ eq_(feature,None)
+ mapnik.logger.set_severity(default_logging_severity)
+
+if __name__ == "__main__":
+ setup()
+ result = run_all(eval(x) for x in dir() if x.startswith("test_"))
+ teardown()
+ exit(result)
diff --git a/test/python_tests/style_test.py b/test/python_tests/style_test.py
new file mode 100644
index 0000000..7bc782a
--- /dev/null
+++ b/test/python_tests/style_test.py
@@ -0,0 +1,18 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from nose.tools import eq_
+from utilities import run_all
+import mapnik
+
+def test_style_init():
+ s = mapnik.Style()
+ eq_(s.filter_mode,mapnik.filter_mode.ALL)
+ eq_(len(s.rules),0)
+ eq_(s.opacity,1)
+ eq_(s.comp_op,None)
+ eq_(s.image_filters,"")
+ eq_(s.image_filters_inflate,False)
+
+if __name__ == "__main__":
+ exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/topojson_plugin_test.py b/test/python_tests/topojson_plugin_test.py
new file mode 100644
index 0000000..a5f3e57
--- /dev/null
+++ b/test/python_tests/topojson_plugin_test.py
@@ -0,0 +1,91 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from nose.tools import eq_,assert_almost_equal
+from utilities import execution_path, run_all
+import os, mapnik
+
+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('.'))
+
+if 'topojson' in mapnik.DatasourceCache.plugin_names():
+
+ def test_topojson_init():
+ # topojson tests/data/json/escaped.geojson -o tests/data/json/escaped.topojson --properties
+ # topojson version 1.4.2
+ ds = mapnik.Datasource(type='topojson',file='../data/json/escaped.topojson')
+ e = ds.envelope()
+ assert_almost_equal(e.minx, -81.705583, places=7)
+ assert_almost_equal(e.miny, 41.480573, places=6)
+ assert_almost_equal(e.maxx, -81.705583, places=5)
+ assert_almost_equal(e.maxy, 41.480573, places=3)
+
+ def test_topojson_properties():
+ ds = mapnik.Datasource(type='topojson',file='../data/json/escaped.topojson')
+ f = ds.features_at_point(ds.envelope().center()).features[0]
+ eq_(len(ds.fields()),7)
+ desc = ds.describe()
+ eq_(desc['geometry_type'],mapnik.DataGeometryType.Point)
+
+ eq_(f['name'], u'Test')
+ eq_(f['int'], 1)
+ eq_(f['description'], u'Test: \u005C')
+ eq_(f['spaces'], u'this has spaces')
+ eq_(f['double'], 1.1)
+ eq_(f['boolean'], True)
+ eq_(f['NOM_FR'], u'Qu\xe9bec')
+ eq_(f['NOM_FR'], u'Québec')
+
+ ds = mapnik.Datasource(type='topojson',file='../data/json/escaped.topojson')
+ f = ds.all_features()[0]
+ eq_(len(ds.fields()),7)
+
+ desc = ds.describe()
+ eq_(desc['geometry_type'],mapnik.DataGeometryType.Point)
+
+ eq_(f['name'], u'Test')
+ eq_(f['int'], 1)
+ eq_(f['description'], u'Test: \u005C')
+ eq_(f['spaces'], u'this has spaces')
+ eq_(f['double'], 1.1)
+ eq_(f['boolean'], True)
+ eq_(f['NOM_FR'], u'Qu\xe9bec')
+ eq_(f['NOM_FR'], u'Québec')
+
+ def test_geojson_from_in_memory_string():
+ ds = mapnik.Datasource(type='topojson',inline=open('../data/json/escaped.topojson','r').read())
+ f = ds.all_features()[0]
+ eq_(len(ds.fields()),7)
+
+ desc = ds.describe()
+ eq_(desc['geometry_type'],mapnik.DataGeometryType.Point)
+
+ eq_(f['name'], u'Test')
+ eq_(f['int'], 1)
+ eq_(f['description'], u'Test: \u005C')
+ eq_(f['spaces'], u'this has spaces')
+ eq_(f['double'], 1.1)
+ eq_(f['boolean'], True)
+ eq_(f['NOM_FR'], u'Qu\xe9bec')
+ eq_(f['NOM_FR'], u'Québec')
+
+# @raises(RuntimeError)
+ def test_that_nonexistant_query_field_throws(**kwargs):
+ ds = mapnik.Datasource(type='topojson',file='../data/json/escaped.topojson')
+ eq_(len(ds.fields()),7)
+ # TODO - this sorting is messed up
+ eq_(ds.fields(),['name', 'int', 'description', 'spaces', 'double', 'boolean', 'NOM_FR'])
+ eq_(ds.field_types(),['str', 'int', 'str', 'str', 'float', 'bool', 'str'])
+# TODO - should topojson plugin throw like others?
+# query = mapnik.Query(ds.envelope())
+# for fld in ds.fields():
+# query.add_property_name(fld)
+# # also add an invalid one, triggering throw
+# query.add_property_name('bogus')
+# fs = ds.features(query)
+
+if __name__ == "__main__":
+ setup()
+ exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/python_tests/utilities.py b/test/python_tests/utilities.py
new file mode 100644
index 0000000..fe02c7d
--- /dev/null
+++ b/test/python_tests/utilities.py
@@ -0,0 +1,102 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from nose.plugins.errorclass import ErrorClass, ErrorClassPlugin
+from nose.tools import assert_almost_equal
+
+import os, sys, traceback
+import mapnik
+
+HERE = os.path.dirname(__file__)
+
+def execution_path(filename):
+ return os.path.join(os.path.dirname(sys._getframe(1).f_code.co_filename), filename)
+
+class Todo(Exception):
+ pass
+
+class TodoPlugin(ErrorClassPlugin):
+ name = "todo"
+
+ todo = ErrorClass(Todo, label='TODO', isfailure=False)
+
+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
+
+def pixel2channels(pixel):
+ alpha = (pixel >> 24) & 0xff
+ red = pixel & 0xff
+ green = (pixel >> 8) & 0xff
+ blue = (pixel >> 16) & 0xff
+ return red,green,blue,alpha
+
+def pixel2rgba(pixel):
+ return 'rgba(%s,%s,%s,%s)' % pixel2channels(pixel)
+
+def get_unique_colors(im):
+ pixels = []
+ for x in range(im.width()):
+ for y in range(im.height()):
+ pixel = im.get_pixel(x,y)
+ if pixel not in pixels:
+ pixels.append(pixel)
+ pixels = sorted(pixels)
+ return map(pixel2rgba,pixels)
+
+def run_all(iterable):
+ failed = 0
+ for test in iterable:
+ try:
+ test()
+ sys.stderr.write("\x1b[32m✓ \x1b[m" + test.__name__ + "\x1b[m\n")
+ except:
+ exc_type, exc_value, exc_tb = sys.exc_info()
+ failed += 1
+ sys.stderr.write("\x1b[31m✘ \x1b[m" + test.__name__ + "\x1b[m\n")
+ for mline in traceback.format_exception_only(exc_type, exc_value):
+ for line in mline.rstrip().split("\n"):
+ sys.stderr.write(" \x1b[31m" + line + "\x1b[m\n")
+ sys.stderr.write(" Traceback:\n")
+ for mline in traceback.format_tb(exc_tb):
+ for line in mline.rstrip().split("\n"):
+ if not 'utilities.py' in line and not 'trivial.py' in line and not line.strip() == 'test()':
+ sys.stderr.write(" " + line + "\n")
+ sys.stderr.flush()
+ return failed
+
+def side_by_side_image(left_im, right_im):
+ width = left_im.width() + 1 + right_im.width()
+ height = max(left_im.height(), right_im.height())
+ im = mapnik.Image(width, height)
+ im.composite(left_im,mapnik.CompositeOp.src_over,1.0,0,0)
+ if width > 80:
+ im.composite(mapnik.Image.open(HERE+'/images/expected.png'),mapnik.CompositeOp.difference,1.0,0,0)
+ im.composite(right_im,mapnik.CompositeOp.src_over,1.0,left_im.width() + 1, 0)
+ if width > 80:
+ im.composite(mapnik.Image.open(HERE+'/images/actual.png'),mapnik.CompositeOp.difference,1.0,left_im.width() + 1, 0)
+ return im
+
+def assert_box2d_almost_equal(a, b, msg=None):
+ msg = msg or ("%r != %r" % (a, b))
+ assert_almost_equal(a.minx, b.minx, msg=msg)
+ assert_almost_equal(a.maxx, b.maxx, msg=msg)
+ assert_almost_equal(a.miny, b.miny, msg=msg)
+ assert_almost_equal(a.maxy, b.maxy, msg=msg)
diff --git a/test/python_tests/webp_encoding_test.py b/test/python_tests/webp_encoding_test.py
new file mode 100644
index 0000000..91e23fc
--- /dev/null
+++ b/test/python_tests/webp_encoding_test.py
@@ -0,0 +1,164 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import os, mapnik
+from nose.tools import raises,eq_
+from utilities import execution_path, run_all
+
+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('.'))
+
+if mapnik.has_webp():
+ tmp_dir = '/tmp/mapnik-webp/'
+ if not os.path.exists(tmp_dir):
+ os.makedirs(tmp_dir)
+
+ opts = [
+ 'webp',
+ 'webp:method=0',
+ 'webp:method=6',
+ 'webp:quality=64',
+ 'webp:alpha=false',
+ 'webp:partitions=3',
+ 'webp:preprocessing=1',
+ 'webp:partition_limit=50',
+ 'webp:pass=10',
+ 'webp:alpha_quality=50',
+ 'webp:alpha_filtering=2',
+ 'webp:alpha_compression=0',
+ 'webp:autofilter=0',
+ 'webp:filter_type=1:autofilter=1',
+ 'webp:filter_sharpness=4',
+ 'webp:filter_strength=50',
+ 'webp:sns_strength=50',
+ 'webp:segments=3',
+ 'webp:target_PSNR=.5',
+ 'webp:target_size=100'
+ ]
+
+
+ def gen_filepath(name,format):
+ return os.path.join('images/support/encoding-opts',name+'-'+format.replace(":","+")+'.webp')
+
+ def test_quality_threshold():
+ im = mapnik.Image(256,256)
+ im.tostring('webp:quality=99.99000')
+ im.tostring('webp:quality=0')
+ im.tostring('webp:quality=0.001')
+
+ @raises(RuntimeError)
+ def test_quality_threshold_invalid():
+ im = mapnik.Image(256,256)
+ im.tostring('webp:quality=101')
+
+ @raises(RuntimeError)
+ def test_quality_threshold_invalid2():
+ im = mapnik.Image(256,256)
+ im.tostring('webp:quality=-1')
+
+ @raises(RuntimeError)
+ def test_quality_threshold_invalid3():
+ im = mapnik.Image(256,256)
+ im.tostring('webp:quality=101.1')
+
+ generate = os.environ.get('UPDATE')
+
+ def test_expected_encodings():
+ fails = []
+ try:
+ for opt in opts:
+ im = mapnik.Image(256,256)
+ expected = gen_filepath('blank',opt)
+ actual = os.path.join(tmp_dir,os.path.basename(expected))
+ if generate or not os.path.exists(expected):
+ print 'generating expected image %s' % expected
+ im.save(expected,opt)
+ im.save(actual,opt)
+ try:
+ expected_bytes = mapnik.Image.open(expected).tostring()
+ except RuntimeError:
+ # this will happen if libweb is old, since it cannot open images created by more recent webp
+ print 'warning, cannot open webp expected image (your libwebp is likely too old)'
+ continue
+ if mapnik.Image.open(actual).tostring() != expected_bytes:
+ fails.append('%s (actual) not == to %s (expected)' % (actual,expected))
+
+ for opt in opts:
+ im = mapnik.Image(256,256)
+ im.fill(mapnik.Color('green'))
+ expected = gen_filepath('solid',opt)
+ actual = os.path.join(tmp_dir,os.path.basename(expected))
+ if generate or not os.path.exists(expected):
+ print 'generating expected image %s' % expected
+ im.save(expected,opt)
+ im.save(actual,opt)
+ try:
+ expected_bytes = mapnik.Image.open(expected).tostring()
+ except RuntimeError:
+ # this will happen if libweb is old, since it cannot open images created by more recent webp
+ print 'warning, cannot open webp expected image (your libwebp is likely too old)'
+ continue
+ if mapnik.Image.open(actual).tostring() != expected_bytes:
+ fails.append('%s (actual) not == to %s (expected)' % (actual,expected))
+
+ for opt in opts:
+ im = mapnik.Image.open('images/support/transparency/aerial_rgba.png')
+ expected = gen_filepath('aerial_rgba',opt)
+ actual = os.path.join(tmp_dir,os.path.basename(expected))
+ if generate or not os.path.exists(expected):
+ print 'generating expected image %s' % expected
+ im.save(expected,opt)
+ im.save(actual,opt)
+ try:
+ expected_bytes = mapnik.Image.open(expected).tostring()
+ except RuntimeError:
+ # this will happen if libweb is old, since it cannot open images created by more recent webp
+ print 'warning, cannot open webp expected image (your libwebp is likely too old)'
+ continue
+ if mapnik.Image.open(actual).tostring() != expected_bytes:
+ fails.append('%s (actual) not == to %s (expected)' % (actual,expected))
+ # disabled to avoid failures on ubuntu when using old webp packages
+ #eq_(fails,[],'\n'+'\n'.join(fails))
+ except RuntimeError, e:
+ print e
+
+ def test_transparency_levels():
+ try:
+ # create partial transparency image
+ im = mapnik.Image(256,256)
+ im.fill(mapnik.Color('rgba(255,255,255,.5)'))
+ c2 = mapnik.Color('rgba(255,255,0,.2)')
+ c3 = mapnik.Color('rgb(0,255,255)')
+ for y in range(0,im.height()/2):
+ for x in range(0,im.width()/2):
+ im.set_pixel(x,y,c2)
+ for y in range(im.height()/2,im.height()):
+ for x in range(im.width()/2,im.width()):
+ im.set_pixel(x,y,c3)
+
+ t0 = tmp_dir + 'white0-actual.webp'
+
+ # octree
+ format = 'webp'
+ expected = 'images/support/transparency/white0.webp'
+ if generate or not os.path.exists(expected):
+ im.save('images/support/transparency/white0.webp')
+ im.save(t0,format)
+ im_in = mapnik.Image.open(t0)
+ t0_len = len(im_in.tostring(format))
+ try:
+ expected_bytes = mapnik.Image.open(expected).tostring(format)
+ except RuntimeError:
+ # this will happen if libweb is old, since it cannot open images created by more recent webp
+ print 'warning, cannot open webp expected image (your libwebp is likely too old)'
+ return
+ eq_(t0_len,len(expected_bytes))
+ except RuntimeError, e:
+ print e
+
+
+if __name__ == "__main__":
+ setup()
+ exit(run_all(eval(x) for x in dir() if x.startswith("test_")))
diff --git a/test/run_tests.py b/test/run_tests.py
new file mode 100755
index 0000000..edf7974
--- /dev/null
+++ b/test/run_tests.py
@@ -0,0 +1,91 @@
+#!/usr/bin/env python
+
+import sys
+
+try:
+ import nose
+except ImportError, e:
+ sys.stderr.write("Unable to run python tests: the third party 'nose' module is required\nTo install 'nose' do:\n\tsudo pip install nose (or on debian systems: apt-get install python-nose): %s\n" % e)
+ sys.exit(1)
+
+import mapnik
+from python_tests.utilities import TodoPlugin
+from nose.plugins.doctests import Doctest
+
+import nose, sys, os, getopt
+
+def usage():
+ print("test.py -h | --help")
+ print("test.py [-q | -v] [-p | --prefix <path>]")
+
+def main():
+ try:
+ opts, args = getopt.getopt(sys.argv[1:], "hvqp:", ["help", "prefix="])
+ except getopt.GetoptError,err:
+ print(str(err))
+ usage()
+ sys.exit(2)
+
+ prefix = None
+ verbose = False
+ quiet = False
+
+ for o, a in opts:
+ if o == "-q":
+ quiet = True
+ elif o == "-v":
+ verbose = True
+ elif o in ("-h", "--help"):
+ usage()
+ sys.exit()
+ elif o in ("-p", "--prefix"):
+ prefix = a
+ else:
+ assert False, "Unhandled option"
+
+ if quiet and verbose:
+ usage()
+ sys.exit(2)
+
+ if prefix:
+ # Allow python to find libraries for testing on the buildbot
+ sys.path.insert(0, os.path.join(prefix, "lib/python%s/site-packages" % sys.version[:3]))
+
+ import mapnik
+
+ if not quiet:
+ print("- mapnik path: %s" % mapnik.__file__)
+ if hasattr(mapnik,'_mapnik'):
+ print("- _mapnik.so path: %s" % mapnik._mapnik.__file__)
+ if hasattr(mapnik,'inputpluginspath'):
+ print ("- Input plugins path: %s" % mapnik.inputpluginspath)
+ if os.environ.has_key('MAPNIK_INPUT_PLUGINS_DIRECTORY'):
+ print ("- MAPNIK_INPUT_PLUGINS_DIRECTORY env: %s" % os.environ.get('MAPNIK_INPUT_PLUGINS_DIRECTORY'))
+ if hasattr(mapnik,'fontscollectionpath'):
+ print("- Font path: %s" % mapnik.fontscollectionpath)
+ if os.environ.has_key('MAPNIK_FONT_DIRECTORY'):
+ print ("- MAPNIK_FONT_DIRECTORY env: %s" % os.environ.get('MAPNIK_FONT_DIRECTORY'))
+ print('')
+ print("- Running nosetests:")
+ print('')
+
+ argv = [__file__, '--exe', '--with-todo', '--with-doctest', '--doctest-tests']
+
+ if not quiet:
+ argv.append('-v')
+
+ if verbose:
+ # 3 * '-v' gets us debugging information from nose
+ argv.append('-v')
+ argv.append('-v')
+
+ dirname = os.path.dirname(sys.argv[0])
+ argv.extend(['-w', os.path.join(dirname,'python_tests')])
+
+ if not nose.run(argv=argv, plugins=[TodoPlugin(), Doctest()]):
+ sys.exit(1)
+ else:
+ sys.exit(0)
+
+if __name__ == "__main__":
+ main()
diff --git a/test/visual.py b/test/visual.py
new file mode 100755
index 0000000..32ad7f4
--- /dev/null
+++ b/test/visual.py
@@ -0,0 +1,331 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import os
+import sys
+import mapnik
+import shutil
+import platform
+import glob
+
+#mapnik.logger.set_severity(mapnik.severity_type.None)
+#mapnik.logger.set_severity(mapnik.severity_type.Debug)
+
+try:
+ import json
+except ImportError:
+ import simplejson as json
+
+visual_output_dir = "/tmp/mapnik-visual-images"
+
+defaults = {
+ 'status': True,
+ 'sizes': [(500, 100)],
+ 'scales':[1.0,2.0],
+ 'agg': True,
+ 'cairo': mapnik.has_cairo(),
+ 'grid': mapnik.has_grid_renderer()
+}
+
+cairo_threshold = 10
+agg_threshold = 0
+grid_threshold = 5
+if 'Linux' == platform.uname()[0]:
+ # we assume if linux then you are running packaged cairo
+ # which is older than the 1.12.14 version we used on OS X
+ # to generate the expected images, so we'll rachet back the threshold
+ # https://github.com/mapnik/mapnik/issues/1868
+ cairo_threshold = 230
+ agg_threshold = 12
+ grid_threshold = 6
+
+def render_cairo(m, output, scale_factor):
+ mapnik.render_to_file(m, output, 'ARGB32', scale_factor)
+ # open and re-save as png8 to save space
+ new_im = mapnik.Image.open(output)
+ new_im.save(output, 'png32')
+
+def render_grid(m, output, scale_factor):
+ grid = mapnik.Grid(m.width, m.height)
+ mapnik.render_layer(m, grid, layer=0, scale_factor=scale_factor)
+ utf1 = grid.encode('utf', resolution=4)
+ open(output,'wb').write(json.dumps(utf1, indent=1))
+
+def render_agg(m, output, scale_factor):
+ mapnik.render_to_file(m, output, 'png32', scale_factor),
+
+renderers = [
+ { 'name': 'agg',
+ 'render': render_agg,
+ 'compare': lambda actual, reference: compare(actual, reference, alpha=True),
+ 'threshold': agg_threshold,
+ 'filetype': 'png',
+ 'dir': 'images'
+ },
+ { 'name': 'cairo',
+ 'render': render_cairo,
+ 'compare': lambda actual, reference: compare(actual, reference, alpha=False),
+ 'threshold': cairo_threshold,
+ 'filetype': 'png',
+ 'dir': 'images'
+ },
+ { 'name': 'grid',
+ 'render': render_grid,
+ 'compare': lambda actual, reference: compare_grids(actual, reference, alpha=False),
+ 'threshold': grid_threshold,
+ 'filetype': 'json',
+ 'dir': 'grids'
+ }
+]
+
+COMPUTE_THRESHOLD = 16
+
+# testcase images are generated on OS X
+# so they should exactly match
+if platform.uname()[0] == 'Darwin':
+ COMPUTE_THRESHOLD = 2
+
+# compare two images and return number of different pixels
+def compare(actual, expected, alpha=True):
+ im1 = mapnik.Image.open(actual)
+ im2 = mapnik.Image.open(expected)
+ return im1.compare(im2,COMPUTE_THRESHOLD, alpha)
+
+def compare_grids(actual, expected, threshold=0, alpha=True):
+ global errors
+ global passed
+ im1 = json.loads(open(actual).read())
+ im2 = json.loads(open(expected).read())
+ # TODO - real diffing
+ if not im1['data'] == im2['data']:
+ return 99999999
+ if not im1['keys'] == im2['keys']:
+ return 99999999
+ grid1 = im1['grid']
+ grid2 = im2['grid']
+ # dimensions must be exact
+ width1 = len(grid1[0])
+ width2 = len(grid2[0])
+ if not width1 == width2:
+ return 99999999
+ height1 = len(grid1)
+ height2 = len(grid2)
+ if not height1 == height2:
+ return 99999999
+ diff = 0;
+ for y in range(0,height1-1):
+ row1 = grid1[y]
+ row2 = grid2[y]
+ width = min(len(row1),len(row2))
+ for w in range(0,width):
+ if row1[w] != row2[w]:
+ diff += 1
+ return diff
+
+dirname = os.path.join(os.path.dirname(__file__),'data-visual')
+
+class Reporting:
+ DIFF = 1
+ NOT_FOUND = 2
+ OTHER = 3
+ REPLACE = 4
+ def __init__(self, quiet, overwrite_failures = False):
+ self.quiet = quiet
+ self.passed = 0
+ self.failed = 0
+ self.overwrite_failures = overwrite_failures
+ self.errors = [ #(type, actual, expected, diff, message)
+ ]
+
+ def result_fail(self, actual, expected, diff):
+ self.failed += 1
+ if self.quiet:
+ if platform.uname()[0] == 'Windows':
+ sys.stderr.write('.')
+ else:
+ sys.stderr.write('\x1b[31m.\x1b[0m')
+ else:
+ print '\x1b[31m✘\x1b[0m (\x1b[34m%u different pixels\x1b[0m)' % diff
+
+ if self.overwrite_failures:
+ self.errors.append((self.REPLACE, actual, expected, diff, None))
+ contents = open(actual, 'r').read()
+ open(expected, 'wb').write(contents)
+ else:
+ self.errors.append((self.DIFF, actual, expected, diff, None))
+
+ def result_pass(self, actual, expected, diff):
+ self.passed += 1
+ if self.quiet:
+ if platform.uname()[0] == 'Windows':
+ sys.stderr.write('.')
+ else:
+ sys.stderr.write('\x1b[32m.\x1b[0m')
+ else:
+ if platform.uname()[0] == 'Windows':
+ print '\x1b[32m✓\x1b[0m'
+ else:
+ print '✓'
+
+ def not_found(self, actual, expected):
+ self.failed += 1
+ self.errors.append((self.NOT_FOUND, actual, expected, 0, None))
+ if self.quiet:
+ sys.stderr.write('\x1b[33m.\x1b[0m')
+ else:
+ print '\x1b[33m?\x1b[0m (\x1b[34mReference file not found, creating\x1b[0m)'
+ contents = open(actual, 'r').read()
+ open(expected, 'wb').write(contents)
+
+ def other_error(self, expected, message):
+ self.failed += 1
+ self.errors.append((self.OTHER, None, expected, 0, message))
+ if self.quiet:
+ sys.stderr.write('\x1b[31m.\x1b[0m')
+ else:
+ print '\x1b[31m✘\x1b[0m (\x1b[34m%s\x1b[0m)' % message
+
+ def make_html_item(self,actual,expected,diff):
+ item = '''
+ <div class="expected">
+ <a href="%s">
+ <img src="%s" width="100%s">
+ </a>
+ </div>
+ ''' % (expected,expected,'%')
+ item += '<div class="text">%s</div>' % (diff)
+ item += '''
+ <div class="actual">
+ <a href="%s">
+ <img src="%s" width="100%s">
+ </a>
+ </div>
+ ''' % (actual,actual,'%')
+ return item
+
+ def summary(self):
+ if len(self.errors) == 0:
+ print '\nAll %s visual tests passed: \x1b[1;32m✓ \x1b[0m' % self.passed
+ return 0
+ sortable_errors = []
+ print "\nVisual rendering: %s failed / %s passed" % (len(self.errors), self.passed)
+ for idx, error in enumerate(self.errors):
+ if error[0] == self.OTHER:
+ print str(idx+1) + ") \x1b[31mfailure to run test:\x1b[0m %s (\x1b[34m%s\x1b[0m)" % (error[2],error[4])
+ elif error[0] == self.NOT_FOUND:
+ print str(idx+1) + ") Generating reference image: '%s'" % error[2]
+ continue
+ elif error[0] == self.DIFF:
+ print str(idx+1) + ") \x1b[34m%s different pixels\x1b[0m:\n\t%s (\x1b[31mactual\x1b[0m)\n\t%s (\x1b[32mexpected\x1b[0m)" % (error[3], error[1], error[2])
+ if '.png' in error[1]: # ignore grids
+ sortable_errors.append((error[3],error))
+ elif error[0] == self.REPLACE:
+ print str(idx+1) + ") \x1b[31mreplaced reference with new version:\x1b[0m %s" % error[2]
+ if len(sortable_errors):
+ # drop failure results in folder
+ vdir = os.path.join(visual_output_dir,'visual-test-results')
+ if not os.path.exists(vdir):
+ os.makedirs(vdir)
+ html_template = open(os.path.join(dirname,'index.html'),'r').read()
+ name = 'index.html'
+ failures_realpath = os.path.join(vdir,name)
+ html_out = open(failures_realpath,'w+')
+ sortable_errors.sort(reverse=True)
+ html_body = ''
+ for item in sortable_errors:
+ # copy images into single directory
+ actual = item[1][1]
+ expected = item[1][2]
+ diff = item[0]
+ actual_new = os.path.join(vdir,os.path.basename(actual))
+ shutil.copy(actual,actual_new)
+ expected_new = os.path.join(vdir,os.path.basename(expected))
+ shutil.copy(expected,expected_new)
+ html_body += self.make_html_item(os.path.relpath(actual_new,vdir),os.path.relpath(expected_new,vdir),diff)
+ html_out.write(html_template.replace('{{RESULTS}}',html_body))
+ print 'View failures by opening %s' % failures_realpath
+ return 1
+
+def render(filename, config, scale_factor, reporting):
+ m = mapnik.Map(*config['sizes'][0])
+
+ try:
+ mapnik.load_map(m, os.path.join(dirname, "styles", filename), True)
+
+ if not (m.parameters['status'] if ('status' in m.parameters) else config['status']):
+ return
+ except Exception, e:
+ if 'Could not create datasource' in str(e) \
+ or 'Bad connection' in str(e):
+ return m
+ reporting.other_error(filename, repr(e))
+ return m
+
+ sizes = config['sizes'];
+ if 'sizes' in m.parameters:
+ sizes = [[int(i) for i in size.split(',')] for size in m.parameters['sizes'].split(';')]
+
+ for size in sizes:
+ m.width, m.height = size
+
+ if 'bbox' in m.parameters:
+ bbox = mapnik.Box2d.from_string(str(m.parameters['bbox']))
+ m.zoom_to_box(bbox)
+ else:
+ m.zoom_all()
+
+ name = filename[0:-4]
+ postfix = "%s-%d-%d-%s" % (name, m.width, m.height, scale_factor)
+ for renderer in renderers:
+ if config.get(renderer['name'], True):
+ expected = os.path.join(dirname, renderer['dir'], '%s-%s-reference.%s' %
+ (postfix, renderer['name'], renderer['filetype']))
+ actual = os.path.join(visual_output_dir, '%s-%s.%s' %
+ (postfix, renderer['name'], renderer['filetype']))
+ if not quiet:
+ print "\"%s\" with %s..." % (postfix, renderer['name']),
+ try:
+ renderer['render'](m, actual, scale_factor)
+ if not os.path.exists(expected):
+ reporting.not_found(actual, expected)
+ else:
+ diff = renderer['compare'](actual, expected)
+ if diff > renderer['threshold']:
+ reporting.result_fail(actual, expected, diff)
+ else:
+ reporting.result_pass(actual, expected, diff)
+ except Exception, e:
+ reporting.other_error(expected, repr(e))
+ return m
+
+if __name__ == "__main__":
+ if '-q' in sys.argv:
+ quiet = True
+ sys.argv.remove('-q')
+ else:
+ quiet = False
+
+ if '--overwrite' in sys.argv:
+ overwrite_failures = True
+ sys.argv.remove('--overwrite')
+ else:
+ overwrite_failures = False
+
+ files = None
+ if len(sys.argv) > 1:
+ files = [name + ".xml" for name in sys.argv[1:]]
+ else:
+ files = [os.path.basename(file) for file in glob.glob(os.path.join(dirname, "styles/*.xml"))]
+
+ if not os.path.exists(visual_output_dir):
+ os.makedirs(visual_output_dir)
+
+ reporting = Reporting(quiet, overwrite_failures)
+ try:
+ for filename in files:
+ config = dict(defaults)
+ for scale_factor in config['scales']:
+ m = render(filename, config, scale_factor, reporting)
+ except KeyboardInterrupt:
+ pass
+ sys.exit(reporting.summary())
--
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-grass/python-mapnik.git
Sebastiaan Couwenberg
2015-06-26 17:33:34 UTC
Permalink
This is an automated email from the git hooks/post-receive script.

sebastic pushed a commit to branch master
in repository python-mapnik.

commit 87907a2c7e1e6796f5fddfb238ed13f463eda554
Author: Bas Couwenberg <***@xs4all.nl>
Date: Fri Jun 26 19:04:25 2015 +0200

Add initial Debian packaging.
---
debian/changelog | 5 +++++
debian/compat | 1 +
debian/control | 56 ++++++++++++++++++++++++++++++++++++++++++++++++
debian/copyright | 52 ++++++++++++++++++++++++++++++++++++++++++++
debian/gbp.conf | 16 ++++++++++++++
debian/get-orig-source | 19 ++++++++++++++++
debian/rules | 20 +++++++++++++++++
debian/upstream/metadata | 6 ++++++
debian/watch | 3 +++
9 files changed, 178 insertions(+)

diff --git a/debian/changelog b/debian/changelog
new file mode 100644
index 0000000..332cac9
--- /dev/null
+++ b/debian/changelog
@@ -0,0 +1,5 @@
+python-mapnik (0.0~20150619-e477887-1) UNRELEASED; urgency=medium
+
+ * Initial release. (Closes: #XXXXXX)
+
+ -- Bas Couwenberg <***@debian.org> Fri, 26 Jun 2015 18:52:18 +0200
diff --git a/debian/compat b/debian/compat
new file mode 100644
index 0000000..ec63514
--- /dev/null
+++ b/debian/compat
@@ -0,0 +1 @@
+9
diff --git a/debian/control b/debian/control
new file mode 100644
index 0000000..6cca41c
--- /dev/null
+++ b/debian/control
@@ -0,0 +1,56 @@
+Source: python-mapnik
+Maintainer: Debian GIS Project <pkg-grass-***@lists.alioth.debian.org>
+Uploaders: Bas Couwenberg <***@debian.org>
+Section: python
+Priority: optional
+Build-Depends: debhelper (>= 9),
+ dh-python,
+ libboost-python-dev,
+ libmapnik-dev,
+ python-all-dev,
+ python-setuptools,
+ python3-all-dev,
+ python3-setuptools
+Standards-Version: 3.9.6
+Vcs-Browser: http://anonscm.debian.org/cgit/pkg-grass/python-mapnik.git
+Vcs-Git: git://anonscm.debian.org/pkg-grass/python-mapnik.git
+Homepage: https://github.com/mapnik/python-mapnik
+X-Python-Version: >= 2.5
+X-Python3-Version: >= 3.2
+
+Package: python-mapnik
+Architecture: any
+Depends: ${python:Depends},
+ ${shlibs:Depends},
+ ${misc:Depends}
+Provides: ${python:Provides}
+Description: Python 2 interface to the mapnik library
+ Mapnik is an OpenSource C++ toolkit for developing GIS
+ (Geographic Information Systems) applications. At the core is a C++
+ shared library providing algorithms/patterns for spatial data access and
+ visualization.
+ .
+ Essentially a collection of geographic objects (map, layer, datasource,
+ feature, geometry), the library doesn't rely on "windowing systems" and
+ is intended to work in multi-threaded environments
+ .
+ This package contains the bindings for Python 2.
+
+Package: python3-mapnik
+Architecture: any
+Depends: ${python3:Depends},
+ ${shlibs:Depends},
+ ${misc:Depends}
+Provides: ${python3:Provides}
+Description: Python 3 interface to the mapnik library
+ Mapnik is an OpenSource C++ toolkit for developing GIS
+ (Geographic Information Systems) applications. At the core is a C++
+ shared library providing algorithms/patterns for spatial data access and
+ visualization.
+ .
+ Essentially a collection of geographic objects (map, layer, datasource,
+ feature, geometry), the library doesn't rely on "windowing systems" and
+ is intended to work in multi-threaded environments
+ .
+ This package contains the bindings for Python 3.
+
diff --git a/debian/copyright b/debian/copyright
new file mode 100644
index 0000000..ab41c68
--- /dev/null
+++ b/debian/copyright
@@ -0,0 +1,52 @@
+Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+Upstream-Name: python-mapnik
+Upstream-Contact: https://github.com/mapnik/python-mapnik/issues
+Source: https://github.com/mapnik/python-mapnik
+
+Files: *
+Copyright: 2015, Artem Pavlenko
+ 2015, Jean-Francois Doyon
+ 2010, Robert Coup
+License: LGPL-2.1+
+
+Files: mapnik/__init__.py
+Copyright: 2014, Artem Pavlenko
+License: GPL-2+
+
+Files: debian/*
+Copyright: Bas Couwenberg <***@debian.org>
+License: GPL-2+
+
+License: GPL-2+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+ .
+ This program 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 General Public License for more details.
+ .
+ You should have received a copy of the GNU General Public License along
+ with this program; if not, write to the Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ .
+ On Debian systems, the complete text of version 2 of the GNU General
+ Public License can be found in `/usr/share/common-licenses/GPL-2'.
+
+License: LGPL-2.1+
+ 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.
+ .
+ On Debian systems, the full text of the GNU Lesser General Public
+ License version 2.1 can be found in the file
+ `/usr/share/common-licenses/LGPL-2.1'.
+
diff --git a/debian/gbp.conf b/debian/gbp.conf
new file mode 100644
index 0000000..21d0417
--- /dev/null
+++ b/debian/gbp.conf
@@ -0,0 +1,16 @@
+[DEFAULT]
+
+# The default name for the upstream branch is "upstream".
+# Change it if the name is different (for instance, "master").
+upstream-branch = upstream
+
+# The default name for the Debian branch is "master".
+# Change it if the name is different (for instance, "debian/unstable").
+debian-branch = master
+
+# git-import-orig uses the following names for the upstream tags.
+# Change the value if you are not using git-import-orig
+upstream-tag = upstream/%(version)s
+
+# Always use pristine-tar.
+pristine-tar = True
diff --git a/debian/get-orig-source b/debian/get-orig-source
new file mode 100755
index 0000000..f612216
--- /dev/null
+++ b/debian/get-orig-source
@@ -0,0 +1,19 @@
+#!/bin/bash
+
+REMOTE=upstream
+BRANCH=${REMOTE}/master
+
+if [ $(git remote show ${REMOTE} | wc -l) -eq 0 ]; then
+ git remote add ${REMOTE} https://github.com/mapnik/python-mapnik.git
+fi
+
+git fetch ${REMOTE} --no-tags
+
+PACKAGE=$(dpkg-parsechangelog | grep ^Source: | awk '{print $2}')
+
+COMMIT=$(git log -n1 --format=format:%h ${BRANCH})
+DATE=$(date +%Y%m%d --date="@$(git log -n1 --format=format:%ct ${BRANCH})")
+
+VERSION="0.0~${DATE}-${COMMIT}"
+
+git archive --format=tar.gz --prefix=${PACKAGE}-${VERSION}/ -o ../${PACKAGE}_${VERSION}.orig.tar.gz ${BRANCH}
diff --git a/debian/rules b/debian/rules
new file mode 100755
index 0000000..4633a79
--- /dev/null
+++ b/debian/rules
@@ -0,0 +1,20 @@
+#!/usr/bin/make -f
+# -*- makefile -*-
+
+# Uncomment this to turn on verbose mode.
+#export DH_VERBOSE=1
+
+export PYBUILD_NAME=mapnik
+
+%:
+ dh $@ \
+ --with python2,python3 \
+ --buildsystem=pybuild \
+ --parallel
+
+override_dh_auto_clean:
+ # Skip
+
+override_dh_install:
+ dh_install --list-missing
+
diff --git a/debian/upstream/metadata b/debian/upstream/metadata
new file mode 100644
index 0000000..b489bb1
--- /dev/null
+++ b/debian/upstream/metadata
@@ -0,0 +1,6 @@
+---
+Bug-Database: https://github.com/jswhit/pyproj/issues
+Bug-Submit: https://github.com/jswhit/pyproj/issues/new
+Name: pyproj
+Repository: https://github.com/jswhit/pyproj.git
+Repository-Browse: https://github.com/jswhit/pyproj
diff --git a/debian/watch b/debian/watch
new file mode 100644
index 0000000..00cd0f8
--- /dev/null
+++ b/debian/watch
@@ -0,0 +1,3 @@
+version=3
+opts=uversionmangle=s/(rc|a|b|c)/~$1/ \
+http://pypi.debian.net/mapnik/mapnik-(.+)\.(?:zip|tgz|tbz|txz|(?:tar\.(?:gz|bz2|xz)))
--
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-grass/python-mapnik.git
Sebastiaan Couwenberg
2015-06-26 17:33:34 UTC
Permalink
This is an automated email from the git hooks/post-receive script.

sebastic pushed a commit to branch master
in repository python-mapnik.

commit 48fc3e33ddbec59778df0f0df9ece918757446fa
Author: Bas Couwenberg <***@xs4all.nl>
Date: Fri Jun 26 19:20:02 2015 +0200

Only build for Python 2.
---
debian/control | 23 +----------------------
debian/rules | 2 +-
2 files changed, 2 insertions(+), 23 deletions(-)

diff --git a/debian/control b/debian/control
index 6cca41c..de10fdf 100644
--- a/debian/control
+++ b/debian/control
@@ -8,15 +8,12 @@ Build-Depends: debhelper (>= 9),
libboost-python-dev,
libmapnik-dev,
python-all-dev,
- python-setuptools,
- python3-all-dev,
- python3-setuptools
+ python-setuptools
Standards-Version: 3.9.6
Vcs-Browser: http://anonscm.debian.org/cgit/pkg-grass/python-mapnik.git
Vcs-Git: git://anonscm.debian.org/pkg-grass/python-mapnik.git
Homepage: https://github.com/mapnik/python-mapnik
X-Python-Version: >= 2.5
-X-Python3-Version: >= 3.2

Package: python-mapnik
Architecture: any
@@ -36,21 +33,3 @@ Description: Python 2 interface to the mapnik library
.
This package contains the bindings for Python 2.

-Package: python3-mapnik
-Architecture: any
-Depends: ${python3:Depends},
- ${shlibs:Depends},
- ${misc:Depends}
-Provides: ${python3:Provides}
-Description: Python 3 interface to the mapnik library
- Mapnik is an OpenSource C++ toolkit for developing GIS
- (Geographic Information Systems) applications. At the core is a C++
- shared library providing algorithms/patterns for spatial data access and
- visualization.
- .
- Essentially a collection of geographic objects (map, layer, datasource,
- feature, geometry), the library doesn't rely on "windowing systems" and
- is intended to work in multi-threaded environments
- .
- This package contains the bindings for Python 3.
-
diff --git a/debian/rules b/debian/rules
index 4633a79..2ecafd8 100755
--- a/debian/rules
+++ b/debian/rules
@@ -8,7 +8,7 @@ export PYBUILD_NAME=mapnik

%:
dh $@ \
- --with python2,python3 \
+ --with python2 \
--buildsystem=pybuild \
--parallel
--
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-grass/python-mapnik.git
Loading...