From a3b7d07da027b98f1bfd8cc6fe0124ef0f6fae26 Mon Sep 17 00:00:00 2001 From: artemp Date: Tue, 31 Jul 2012 10:56:13 +0100 Subject: [PATCH 1/7] + select clipper based on geometry type --- src/agg/process_line_symbolizer.cpp | 34 ++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/src/agg/process_line_symbolizer.cpp b/src/agg/process_line_symbolizer.cpp index 1d5388aa3..ec75035b4 100644 --- a/src/agg/process_line_symbolizer.cpp +++ b/src/agg/process_line_symbolizer.cpp @@ -60,7 +60,7 @@ void agg_renderer::process(line_symbolizer const& sym, unsigned g=col.green(); unsigned b=col.blue(); unsigned a=col.alpha(); - + ras_ptr->reset(); set_gamma_method(stroke_, ras_ptr); @@ -76,7 +76,7 @@ void agg_renderer::process(line_symbolizer const& sym, pixfmt_comp_type pixf(buf); pixf.comp_op(static_cast(sym.comp_op())); renderer_base renb(pixf); - + agg::trans_affine tr; evaluate_transform(tr, feature, sym.get_transform()); @@ -94,12 +94,23 @@ void agg_renderer::process(line_symbolizer const& sym, rasterizer_type ras(ren); set_join_caps_aa(stroke_,ras); - typedef boost::mpl::vector conv_types; + typedef boost::mpl::vector conv_types; vertex_converter, rasterizer_type, line_symbolizer, CoordTransform, proj_transform, agg::trans_affine, conv_types> converter(ext,ras,sym,t_,prj_trans,tr,scaled); - if (sym.clip()) converter.set(); // optional clip (default: true) + if (sym.clip() && feature.paths().size() > 0) // optional clip (default: true) + { + eGeomType type = feature.paths()[0].type(); + if (type == Polygon) + converter.set(); + else if (type == LineString) + converter.set(); + // don't clip if type==Point + } + converter.set(); // always transform if (fabs(sym.offset()) > 0.0) converter.set(); // parallel offset converter.set(); // optional affine transform @@ -117,12 +128,23 @@ void agg_renderer::process(line_symbolizer const& sym, } else { - typedef boost::mpl::vector conv_types; + typedef boost::mpl::vector conv_types; + vertex_converter, rasterizer, line_symbolizer, CoordTransform, proj_transform, agg::trans_affine, conv_types> converter(ext,*ras_ptr,sym,t_,prj_trans,tr,scale_factor_); - if (sym.clip()) converter.set(); // optional clip (default: true) + if (sym.clip() && feature.paths().size() > 0) // optional clip (default: true) + { + eGeomType type = feature.paths()[0].type(); + if (type == Polygon) + converter.set(); + else if (type == LineString) + converter.set(); + // don't clip if type==Point + } + converter.set(); // always transform if (fabs(sym.offset()) > 0.0) converter.set(); // parallel offset converter.set(); // optional affine transform From 76569cccb8c1cfc721d7455588c55b270f54a46c Mon Sep 17 00:00:00 2001 From: artemp Date: Tue, 31 Jul 2012 10:57:42 +0100 Subject: [PATCH 2/7] + select clipper based on geometry type + process SEG_CLOSE command --- src/cairo_renderer.cpp | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/cairo_renderer.cpp b/src/cairo_renderer.cpp index 89fa90a9a..c32b3f949 100644 --- a/src/cairo_renderer.cpp +++ b/src/cairo_renderer.cpp @@ -498,7 +498,6 @@ public: } else if (cm == SEG_CLOSE) { - line_to(x, y); close_path(); } } @@ -916,6 +915,10 @@ void cairo_renderer_base::process(building_symbolizer const& sym, { frame->line_to(x,y); } + else if (cm == SEG_CLOSE) + { + frame->close(x,y); + } if (j != 0) { @@ -1000,14 +1003,21 @@ void cairo_renderer_base::process(line_symbolizer const& sym, evaluate_transform(tr, feature, sym.get_transform()); box2d ext = query_extent_ * 1.1; - typedef boost::mpl::vector conv_types; + typedef boost::mpl::vector conv_types; vertex_converter, cairo_context, line_symbolizer, CoordTransform, proj_transform, agg::trans_affine, conv_types> converter(ext,context,sym,t_,prj_trans,tr,scale_factor_); - if (sym.clip()) converter.set(); // optional clip (default: true) + if (sym.clip() && feature.paths().size() > 0) // optional clip (default: true) + { + eGeomType type = feature.paths()[0].type(); + if (type == Polygon) + converter.set(); + else if (type == LineString) + converter.set(); + // don't clip if type==Point + } converter.set(); // always transform - if (fabs(sym.offset()) > 0.0) converter.set(); // parallel offset converter.set(); // optional affine transform if (sym.smooth() > 0.0) converter.set(); // optional smooth converter From c7af665cc753338fd2de4bf680b84b7f530f64d9 Mon Sep 17 00:00:00 2001 From: artemp Date: Tue, 31 Jul 2012 10:59:09 +0100 Subject: [PATCH 3/7] + process SEG_CLOSE --- src/agg/process_building_symbolizer.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/agg/process_building_symbolizer.cpp b/src/agg/process_building_symbolizer.cpp index 8f82ca2b8..b5f6ad678 100644 --- a/src/agg/process_building_symbolizer.cpp +++ b/src/agg/process_building_symbolizer.cpp @@ -82,8 +82,8 @@ void agg_renderer::process(building_symbolizer const& sym, boost::scoped_ptr frame(new geometry_type(LineString)); boost::scoped_ptr roof(new geometry_type(Polygon)); std::deque face_segments; - double x0(0); - double y0(0); + double x0 = 0; + double y0 = 0; geom.rewind(0); unsigned cm = geom.vertex(&x0,&y0); @@ -96,12 +96,11 @@ void agg_renderer::process(building_symbolizer const& sym, { frame->move_to(x,y); } - else if (cm == SEG_LINETO) + else if (cm == SEG_LINETO || cm == SEG_CLOSE) { frame->line_to(x,y); face_segments.push_back(segment_t(x0,y0,x,y)); } - x0 = x; y0 = y; } From d7d833dd135dc808b0c5e4f0c51a9b9d97e5aee8 Mon Sep 17 00:00:00 2001 From: artemp Date: Tue, 31 Jul 2012 13:31:22 +0100 Subject: [PATCH 4/7] + cleanup/small fixes --- src/agg/process_building_symbolizer.cpp | 25 +++++++------ src/cairo_renderer.cpp | 49 ++++++++----------------- 2 files changed, 29 insertions(+), 45 deletions(-) diff --git a/src/agg/process_building_symbolizer.cpp b/src/agg/process_building_symbolizer.cpp index b5f6ad678..4184bc42f 100644 --- a/src/agg/process_building_symbolizer.cpp +++ b/src/agg/process_building_symbolizer.cpp @@ -84,14 +84,11 @@ void agg_renderer::process(building_symbolizer const& sym, std::deque face_segments; double x0 = 0; double y0 = 0; - + double x,y; geom.rewind(0); - unsigned cm = geom.vertex(&x0,&y0); - for (unsigned j=1;jmove_to(x,y); @@ -104,9 +101,12 @@ void agg_renderer::process(building_symbolizer const& sym, x0 = x; y0 = y; } + std::sort(face_segments.begin(),face_segments.end(), y_order); std::deque::const_iterator itr=face_segments.begin(); - for (;itr!=face_segments.end();++itr) + std::deque::const_iterator end=face_segments.end(); + + for (; itr!=end; ++itr) { boost::scoped_ptr faces(new geometry_type(Polygon)); faces->move_to(itr->get<0>(),itr->get<1>()); @@ -119,22 +119,22 @@ void agg_renderer::process(building_symbolizer const& sym, ren.color(agg::rgba8(int(r*0.8), int(g*0.8), int(b*0.8), int(a * sym.get_opacity()))); agg::render_scanlines(*ras_ptr, sl, ren); ras_ptr->reset(); - + // frame->move_to(itr->get<0>(),itr->get<1>()); frame->line_to(itr->get<0>(),itr->get<1>()+height); + } geom.rewind(0); - for (unsigned j=0;jmove_to(x,y+height); roof->move_to(x,y+height); } - else if (cm == SEG_LINETO) + else if (cm == SEG_LINETO || cm == SEG_CLOSE) { frame->line_to(x,y+height); roof->line_to(x,y+height); @@ -152,6 +152,7 @@ void agg_renderer::process(building_symbolizer const& sym, ras_ptr->add_path(roof_path); ren.color(agg::rgba8(r, g, b, int(a * sym.get_opacity()))); agg::render_scanlines(*ras_ptr, sl, ren); + } } } diff --git a/src/cairo_renderer.cpp b/src/cairo_renderer.cpp index c32b3f949..a3fae2a76 100644 --- a/src/cairo_renderer.cpp +++ b/src/cairo_renderer.cpp @@ -894,47 +894,32 @@ void cairo_renderer_base::process(building_symbolizer const& sym, boost::scoped_ptr frame(new geometry_type(LineString)); boost::scoped_ptr roof(new geometry_type(Polygon)); std::deque face_segments; - double x0(0); - double y0(0); - + double x0 = 0; + double y0 = 0; + double x, y; geom.rewind(0); - unsigned cm = geom.vertex(&x0, &y0); - - for (unsigned j = 1; j < geom.size(); ++j) + for (unsigned cm = geom.vertex(&x, &y); cm != SEG_END; + cm = geom.vertex(&x, &y)) { - double x=0; - double y=0; - - cm = geom.vertex(&x,&y); - if (cm == SEG_MOVETO) { frame->move_to(x,y); } - else if (cm == SEG_LINETO) + else if (cm == SEG_LINETO || cm == SEG_CLOSE) { frame->line_to(x,y); + face_segments.push_back(segment_t(x0,y0,x,y)); } - else if (cm == SEG_CLOSE) - { - frame->close(x,y); - } - - if (j != 0) - { - face_segments.push_back(segment_t(x0, y0, x, y)); - } - x0 = x; y0 = y; } std::sort(face_segments.begin(), face_segments.end(), y_order); std::deque::const_iterator itr = face_segments.begin(); - for (; itr != face_segments.end(); ++itr) + std::deque::const_iterator end=face_segments.end(); + for (; itr != end; ++itr) { boost::scoped_ptr faces(new geometry_type(Polygon)); - faces->move_to(itr->get<0>(), itr->get<1>()); faces->line_to(itr->get<2>(), itr->get<3>()); faces->line_to(itr->get<2>(), itr->get<3>() + height); @@ -951,20 +936,18 @@ void cairo_renderer_base::process(building_symbolizer const& sym, } geom.rewind(0); - for (unsigned j = 0; j < geom.size(); ++j) + for (unsigned cm = geom.vertex(&x, &y); cm != SEG_END; + cm = geom.vertex(&x, &y)) { - double x, y; - unsigned cm = geom.vertex(&x, &y); - if (cm == SEG_MOVETO) { - frame->move_to(x, y + height); - roof->move_to(x, y + height); + frame->move_to(x,y+height); + roof->move_to(x,y+height); } - else if (cm == SEG_LINETO) + else if (cm == SEG_LINETO || cm == SEG_CLOSE) { - frame->line_to(x, y + height); - roof->line_to(x, y + height); + frame->line_to(x,y+height); + roof->line_to(x,y+height); } } From e95886f327fe8247dcb1273f59812ac4b9251c54 Mon Sep 17 00:00:00 2001 From: Dane Springmeyer Date: Tue, 31 Jul 2012 10:45:38 -0700 Subject: [PATCH 5/7] catch exceptions upon startup --- demo/viewer/main.cpp | 87 ++++++++++++++++++++++++-------------------- 1 file changed, 47 insertions(+), 40 deletions(-) diff --git a/demo/viewer/main.cpp b/demo/viewer/main.cpp index 905844720..c5cb4ad4c 100644 --- a/demo/viewer/main.cpp +++ b/demo/viewer/main.cpp @@ -36,52 +36,59 @@ int main( int argc, char **argv ) using mapnik::datasource_cache; using mapnik::freetype_engine; - QCoreApplication::setOrganizationName("Mapnik"); - QCoreApplication::setOrganizationDomain("mapnik.org"); - QCoreApplication::setApplicationName("Viewer"); - - QSettings settings("viewer.ini",QSettings::IniFormat); - - // register input plug-ins - QString plugins_dir = settings.value("mapnik/plugins_dir", - QVariant("/usr/local/lib/mapnik/input/")).toString(); - datasource_cache::instance()->register_datasources(plugins_dir.toStdString()); - // register fonts - int count = settings.beginReadArray("mapnik/fonts"); - for (int index=0; index < count; ++index) + try { - settings.setArrayIndex(index); - QString font_dir = settings.value("dir").toString(); - freetype_engine::register_fonts(font_dir.toStdString()); - } - settings.endArray(); + QCoreApplication::setOrganizationName("Mapnik"); + QCoreApplication::setOrganizationDomain("mapnik.org"); + QCoreApplication::setApplicationName("Viewer"); + QSettings settings("viewer.ini",QSettings::IniFormat); - QApplication app( argc, argv ); - MainWindow window; - window.show(); - if (argc > 1) window.open(argv[1]); - if (argc >= 3) - { - QStringList list = QString(argv[2]).split(","); - if (list.size()==4) + // register input plug-ins + QString plugins_dir = settings.value("mapnik/plugins_dir", + QVariant("/usr/local/lib/mapnik/input/")).toString(); + datasource_cache::instance()->register_datasources(plugins_dir.toStdString()); + // register fonts + int count = settings.beginReadArray("mapnik/fonts"); + for (int index=0; index < count; ++index) + { + settings.setArrayIndex(index); + QString font_dir = settings.value("dir").toString(); + freetype_engine::register_fonts(font_dir.toStdString()); + } + settings.endArray(); + + QApplication app( argc, argv ); + MainWindow window; + window.show(); + if (argc > 1) window.open(argv[1]); + if (argc >= 3) + { + QStringList list = QString(argv[2]).split(","); + if (list.size()==4) + { + bool ok; + double x0 = list[0].toDouble(&ok); + double y0 = list[1].toDouble(&ok); + double x1 = list[2].toDouble(&ok); + double y1 = list[3].toDouble(&ok); + if (ok) window.set_default_extent(x0,y0,x1,y1); + } + } + else + { + window.zoom_all(); + } + if (argc == 4) { bool ok; - double x0 = list[0].toDouble(&ok); - double y0 = list[1].toDouble(&ok); - double x1 = list[2].toDouble(&ok); - double y1 = list[3].toDouble(&ok); - if (ok) window.set_default_extent(x0,y0,x1,y1); + double scaling_factor = QString(argv[3]).toDouble(&ok); + if (ok) window.set_scaling_factor(scaling_factor); } + return app.exec(); } - else + catch (std::exception const& ex) { - window.zoom_all(); + std::cerr << "Could not start viewer: '" << ex.what() << "'\n"; + return 1; } - if (argc == 4) - { - bool ok; - double scaling_factor = QString(argv[3]).toDouble(&ok); - if (ok) window.set_scaling_factor(scaling_factor); - } - return app.exec(); } From 967589442cc1d10a3052df729ca1def7e91d0233 Mon Sep 17 00:00:00 2001 From: Dane Springmeyer Date: Tue, 31 Jul 2012 11:23:53 -0700 Subject: [PATCH 6/7] serialize transform for points and sheild --- src/save_map.cpp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/save_map.cpp b/src/save_map.cpp index f572c8fc0..52d0482ec 100644 --- a/src/save_map.cpp +++ b/src/save_map.cpp @@ -78,6 +78,11 @@ public: { set_attr( sym_node, "placement", sym.get_point_placement() ); } + if (sym.get_image_transform()) + { + std::string tr_str = sym.get_image_transform_string(); + set_attr( sym_node, "transform", tr_str ); + } serialize_symbolizer_base(sym_node, sym); } @@ -218,6 +223,11 @@ public: { set_attr(sym_node, "shield-dy", displacement.second); } + if (sym.get_image_transform()) + { + std::string tr_str = sym.get_image_transform_string(); + set_attr( sym_node, "transform", tr_str ); + } serialize_symbolizer_base(sym_node, sym); } @@ -249,7 +259,6 @@ public: { set_attr( sym_node, "height", mapnik::to_expression_string(*sym.height()) ); } - serialize_symbolizer_base(sym_node, sym); } From cb246a436f9332bcbce6fe6853329318e652e607 Mon Sep 17 00:00:00 2001 From: Dane Springmeyer Date: Tue, 31 Jul 2012 13:16:41 -0700 Subject: [PATCH 7/7] update the grid rendering tests with new expected results (should now be passing) --- tests/python_tests/render_grid_test.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/python_tests/render_grid_test.py b/tests/python_tests/render_grid_test.py index f2ce75b3c..596480ab4 100644 --- a/tests/python_tests/render_grid_test.py +++ b/tests/python_tests/render_grid_test.py @@ -20,13 +20,13 @@ def setup(): grid_correct_old = {"keys": ["", "North West", "North East", "South West", "South East"], "data": {"South East": {"Name": "South East"}, "North East": {"Name": "North East"}, "North West": {"Name": "North West"}, "South West": {"Name": "South West"}}, "grid": [" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " !!! ### ", " !!!!! ##### ", " !!!!! ##### ", " !!! ### ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " $$$$ %%%% ", " $$$$$ %%%%% ", " $$$$$ %%%%% ", " $$$ %%% ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " "]} # now using svg rendering -grid_correct_old2 = {"data": {"North East": {"Name": "North East"}, "North West": {"Name": "North West"}, "South East": {"Name": "South East"}, "South West": {"Name": "South West"}}, "grid": [" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " !!! ### ", " !!! ### ", " !!! ### ", " !!! ### ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " $$$ %%% ", " $$$ %%% ", " $$$ %%% ", " $ % ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " "], "keys": ["", "North West", "North East", "South West", "South East"]} +grid_correct_old2 = {"data": {"North East": {"Name": "North East"}, "North West": {"Name": "North West"}, "South East": {"Name": "South East"}, "South West": {"Name": "South West"}}, "grid": [" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " !!! ### ", " !!! ### ", " !!! ### ", " !!! ### ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " $$$ %%% ", " $$$ %%% ", " $$$ %%% ", " $$$ %%% ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " "], "keys": ["", "North West", "North East", "South West", "South East"]} # previous rendering using agg ellipse directly -grid_correct_new = {"keys": ["", "North West", "North East", "South West", "South East"], "data": {"South East": {"Name": "South East"}, "North East": {"Name": "North East"}, "North West": {"Name": "North West"}, "South West": {"Name": "South West"}}, "grid": [" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " !! ## ", " !!! ### ", " !! ## ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " $$$ %% ", " $$$ %%% ", " $$ %% ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " "]} +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": [" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " !! ## ", " !!! ### ", " !! ## ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " $$ %% ", " $$$ %% ", " $$ %% ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " "], "keys": ["", "North West", "North East", "South West", "South East"]} # 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": [" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " !! ## ", " !!! ## ", " !! ## ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " $$ %% ", " $$ %% ", " $ % ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " "], "keys": ["", "North West", "North East", "South West", "South East"]} +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": [" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " !! ## ", " !!! ### ", " !! ## ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " $$ %% ", " $$$ %% ", " $$ %% ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " "], "keys": ["", "North West", "North East", "South West", "South East"]} def resolve(grid,row,col): """ Resolve the attributes for a given pixel in a grid. @@ -164,7 +164,7 @@ def test_render_grid2(): 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": [" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " !! ## ", " !!! ## ", " !! ## ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " $$ %% ", " $$ %% ", " $ % ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " "], "keys": ["", "3", "4", "2", "1"]} +grid_feat_id2 = {"data": {"1": {"Name": "South East"}, "2": {"Name": "South West"}, "3": {"Name": "North West"}, "4": {"Name": "North East"}}, "grid": [" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " !! ## ", " !!! ### ", " !! ## ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " $$ %% ", " $$$ %% ", " $$ %% ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " "], "keys": ["", "3", "4", "2", "1"]} def test_render_grid3(): """ test using feature id"""