[python-mapnik] 01/03: Imported Upstream version 0.0~20150619-e477887
Sebastiaan Couwenberg
sebastic at moszumanska.debian.org
Fri Jun 26 17:33:33 UTC 2015
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 <sebastic at 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 @@
+
+[](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 = "flippmoke at 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",®ister_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",©,
+ ( 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
+ at 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
+ at 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]>=0) and ([region]<=50)</Filter>
+ </Rule>
+ <Rule>
+ <Filter>
+ ([region] >= 0)
+ and
+ ([region] <= 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)
+
+ at 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: http://tile.osm.org/13/4194/2747.png
+ 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)
+
+ at 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)
+
+ at 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)
+
+ at raises(IndexError)
+def test_get_pixel_out_of_range_1():
+ im = mapnik.Image(4,4)
+ c = im.get_pixel(5,5)
+
+ at raises(OverflowError)
+def test_get_pixel_out_of_range_2():
+ im = mapnik.Image(4,4)
+ c = im.get_pixel(-1,1)
+
+ at raises(IndexError)
+def test_get_pixel_color_out_of_range_1():
+ im = mapnik.Image(4,4)
+ c = im.get_pixel(5,5,True)
+
+ at 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)'])
+
+ at 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
+ at 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
+ at raises(IndexError)
+def test_map_query_throw2():
+ m = mapnik.Map(256,256)
+ m.query_point(-1,0,0)
+
+# map has never been zoomed (nodata)
+ at 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
+ at 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))
+
+ at 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='scratch at 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='scratch at 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='busines at 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='busines at 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='busines at 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='busines at 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='busines at 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='busines at 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
More information about the Pkg-grass-devel
mailing list