Plotting a country map


#1

Hi,

I am working on a project that requires plotting a USA map. With national, state and county boundaries. A good example of what i am aiming to create is here http://mapengine.igismap.com/

I have got as far as downloading boundary information from this website http://www.naturalearthdata.com/
I then have converted the .shp files to KML(a type of xml) files using tools(ogr2ogr) from here http://trac.osgeo.org/gdal/wiki/DownloadingGdalBinaries

The resulting KML files seem to have all the points necessary to plot boundaries so i was going to parse these and construct Shape2d objects from them.

I had converted the .shp files to .GeoJSON files but i could not work out to load them in using Cinders load functions. I think they are different from JSON files. I would need a separate reader class and would rather use only Cinder.

Has anyone else used Cinder to plot maps?
Is there a better way to do this? Or advice on how to approach this?


#2

Hi,

according to the specification, GeoJSON seems to be properly formatted JSON that you can read using Cinder’s built-in support for JSON. It’s not too hard to write your own parser, especially if all you care about is the geometry.

One way to go about this, is to write classes that together define the specification. Going by the first sample mentioned in the link, a Geometry class could look like this:

#include "jsoncpp/json.h"

class Geometry {
  public:
    //! Geometry types (I've included 3 of the 7 here).
    typedef enum { TYPE_POINT, TYPE_MULTI_POINT, TYPE_LINE_STRING } Type;

    //! Default constructor creates undefined object.
    Geometry() {}
    //! Allows Geometry to be created from a Json::Value.
    Geometry( const Json::Value &val )
    {
        mType = getTypeFromString( val.get( "type", "" ) );

        auto coords = val.get( "coordinates", Json::Value( Json::arrayValue ) );
        if( mType == TYPE_POINT ) {
            assert( coords.size() == 2 );
            mCoordinates.emplace_back( coords[0].asFloat(), coords[1].asFloat() );
        }
        else {
            for( auto &coord : coords ) {
                assert( coord.size() == 2 );
                mCoordinates.emplace_back( coord[0].asFloat(), coord[1].asFloat() );
            }
        }
    }
    //! Allows you to convert back to JSON in case you want to write a file.
    operator Json::Value() const
    {
        Json::Value result;
        result["type"] = getStringFromType( mType ); // [Write it yourself.]
        // [Add coordinates yourself.]
        return result;
    }
    //!
    Type getType() const { return mType; }
    //!
    Type getTypeFromString( const std::string &type ) const
    {
        if( type == "Point" )
            return TYPE_POINT;
        else if( type == "MultiPoint" )
            return TYPE_MULTI_POINT;
        else if( type == "LineString" )
            return TYPE_LINE_STRING;
    }
    //!
    const std::vector<ci::vec2> &getCoordinates() const { return mCoordinates; }

  private:
    Type mType;
    std::vector<ci::vec2> mCoordinates;
};

class Feature {
  public:
    Feature() {}
    Feature( const Json::Value &val )
    {
        // This is how easy it is to parse the geometry now. 
        mGeometry = val.get( "geometry", Geometry() );
    }

    const Geometry &getGeometry() { return mGeometry; }

  private:
    Geometry mGeometry;
};

And then you could do something like this to read the data:

    std::string data = ci::loadString( loadAsset( "data.json" ) );

    Json::Reader reader;
    Json::Value  root;
    if( !reader.parse( data, root ) ) {
        CI_LOG_E( "Failed to parse JSON file: " << reader.getFormattedErrorMessages() );
        return;
    }

    mFeatures.clear(); // Of type std::vector<Feature>

    auto features = root.get( "features", Json::Value( Json::arrayValue ) );
    for( auto &feature : features ) {
        // Calls Feature( const Json::Value &val ) constructor.
        mFeatures.emplace_back( feature ); 
    }

From there, you have all the data to create your shapes and polygons. You won’t need external libraries at all.

-Paul

P.S.: I didn’t test the code, I posted it directly, so it might contain errors.
P.P.S.: an easier to read overview of the GeoJSON specs can be found here.


#3

Thanks Paul! Just noticed Cinder can actually load .geojson files, the earthquake sample is a working example.

Got this code kind of working. Here’s working code should anyone else want to try it.

Problem i have now is the line drawing is not very neat. Squiggly lines everywhere :slight_smile: I think this might be the data (i notice a line from USA main land to Alaska) or my use of Shape.

#include "cinder/app/App.h"
#include "cinder/app/RendererGl.h"
#include "cinder/gl/gl.h"

#include "cinder/Json.h"

#include "jsoncpp/json.h"

using namespace ci;
using namespace ci::app;
using namespace std;


class Geometry {
public:
	//! Geometry types (I've included 3 of the 7 here).
	typedef enum { TYPE_INVALID, TYPE_POINT, TYPE_MULTI_POINT, TYPE_LINE_STRING, TYPE_POLYGON, TYPE_MULTI_POLYGON, TYPE_MULTI_LINE_STRING } Type;

	//! Default constructor creates undefined object.
	Geometry() {}
	//! Allows Geometry to be created from a Json::Value.
	Geometry( const Json::Value &val )
	{
		Json::Value typeVal = val.get("type", "");
		assert(!typeVal.isNull() && !typeVal.empty());

		mType = getTypeFromString(typeVal.asString());

		if (mType == TYPE_INVALID) {
			assert(false);
		} else {
			auto coords = val.get("coordinates", Json::Value(Json::arrayValue));
			switch (mType) {
				case TYPE_POINT:
				{
					assert(coords.size() == 2);
					mCoordinates.emplace_back(coords[0].asFloat(), coords[1].asFloat());
				}
				break;
				case TYPE_POLYGON:
				{
					for (auto& coordSubSet : coords) {
						for (auto& coord : coordSubSet) {
							mCoordinates.emplace_back(coord[0].asFloat(), coord[1].asFloat());
						}
					}
				}
				break;
			}
		}
	}
	//! Allows you to convert back to JSON in case you want to write a file.
	operator Json::Value() const
	{
		Json::Value result;
		result["type"] = getStringFromType( mType ); // [Write it yourself.]
													 // [Add coordinates yourself.]
		for (auto& coord : mCoordinates) {
		}

		return result;
	}
	//!
	Type getType() const { return mType; }
	//!
	Type getTypeFromString( const std::string &type ) const
	{
		if (type == "Point")
			return TYPE_POINT;
		else if (type == "MultiPoint")
			return TYPE_MULTI_POINT;
		else if (type == "LineString")
			return TYPE_LINE_STRING;
		else if (type == "Polygon")
			return TYPE_POLYGON;
		else
			return TYPE_INVALID;
	}

	std::string getStringFromType( const Type type ) const
	{
		switch (type) {
			case TYPE_POINT: return "Point";
			case TYPE_MULTI_POINT: return "MultiPoint";
			case TYPE_LINE_STRING: return "LineString";			
		}

		return "error";
	}

	//!
	const std::vector<ci::vec2> &getCoordinates() const { return mCoordinates; }

private:
	Type mType;
	std::vector<ci::vec2> mCoordinates;
};

class Feature {
public:
	Feature() {}
	Feature( const Json::Value &val )
	{
		// This is how easy it is to parse the geometry now. 
		mGeometry = val.get( "geometry", Geometry() );
	}

	const Geometry &getGeometry() { return mGeometry; }

private:
	Geometry mGeometry;
};

class MapApp : public App {
  public:
	void setup() override;
	void mouseDown( MouseEvent event ) override;
	void update() override;
	void draw() override;



	std::vector<Feature> m_Features;
	std::vector<Shape2d> m_Shapes;
};

void MapApp::setup()
{
	std::string data = ci::loadString( loadAsset( "data.geojson" ) );

	Json::Reader reader;
	Json::Value  root;
	if( !reader.parse( data, root ) ) {
		//  find the header file for this!
		//CI_LOG_E( "Failed to parse JSON file: " << reader.getFormattedErrorMessages() );
		return;
	}

	m_Features.clear(); // Of type std::vector<Feature>

	auto features = root.get( "features", Json::Value( Json::arrayValue ) );
	for( auto &feature : features ) {
		// Calls Feature( const Json::Value &val ) constructor.
		m_Features.emplace_back( feature ); 
	}
	
	for (auto& feature : m_Features) {
		bool bStart = true;
		ci::Shape2d shape{};

		const Geometry& geometry = feature.getGeometry();
		for (auto coord : geometry.getCoordinates()) {

			//  flip the y as data is cartesian coordinates and screen coordinates are 0,0 top left
			coord.y = -coord.y;

			if (bStart) {
				shape.moveTo(coord);
				bStart = false;
			} else {
				shape.lineTo(coord);
			}
		}

		shape.close();

		m_Shapes.push_back(shape);
	}
}

void MapApp::mouseDown( MouseEvent event )
{
}

void MapApp::update()
{
}

void MapApp::draw()
{
	gl::clear( Color( 0, 0, 0 ) ); 
	
	gl::pushModelView();
	gl::translate(getWindowCenter());
	gl::scale(1.5f, 1.5f);

	gl::color(1,1,1);
	for (auto& shape : m_Shapes) {
		gl::draw(shape);
	}

	gl::popModelView();
}

CINDER_APP( MapApp, RendererGl )

#4

Looking through nothing jumps out as obviously wrong. I’d try just rendering a single shape at a time to see if the issue is more obvious when there’s no overlap from other objects.


#5

Looks like those incorrect lines all cross the date line at 180 degrees longitude. You may have to add a check for that.


#6

There was a bug in the code below. It was putting all coordinates into the same shape. They are actually separate shapes. I also think the geojson file i am using has territories and other non-country that that needs to be filtered out

case TYPE_POLYGON:
{
//  WAS THIS
/*
	for (auto& coordSubSet : coords) {
		for (auto& coord : coordSubSet) {
			mCoordinates.emplace_back(coord[0].asFloat(), coord[1].asFloat());
		}
	}
*/

//  SHOULD BE THIS
for (auto coordSubSet : coordsArray) {
	std::vector<ci::vec2> aCoordinates;

	for (auto coord : coordSubSet) {
		aCoordinates.emplace_back(coord[0].asFloat(), coord[1].asFloat());
	}
	mCoordinates.push_back(aCoordinates); //  vector of vector of ci::vec2
}
}
break;

Thanks for the help! Really surprised how easy it was in the end to do this with Cinder.


#7

Hi again,

Thought i would add to this post than spam the forum with another one :slight_smile:

I was looking for advice on what i am trying to do.

I have cities, counties and states in geojson files. I read them in and build shapes out of the points.
I am using gl::draw(shape)
When i render the cities (drawn as a rect), counties and states i get a drop in frame rate(30 frames).
Understandably, due to rendering 50 states, 3000 cities and hundreds of counties.

I would like to improve this by culling any states, counties or cities that are not visible on the cpu.
Then batching all the vertices of the visible states, counties or cities into one big Vbo and sending it to the gpu for fast rendering.

I have a few problems i cant work out.

  1. Each state/county/city is made up of one or more closed paths. How do i tell the gpu to draw the vertices as a closed path? gl::drawArrays(GL_LINE_LOOP)?
  2. State/county/city do not all have the same number of vertices. How do i tell the gpu where one state/county/city vertices end and another starts?
  3. What if i need to apply a color to a state/county/city?

The ParticleSphereGPU example helps with basic setup but it is based on lots of identical particle objects with just the values of their member data changing. I have objects that have differing amount of vertice data.


#8

Hi,

I don’t have the time right now to fully explain how to do it (with code samples), but here are a few hints:

  • The gl::draw( Shape ) method recreates a Vbo every time it is called. This is terribly inefficient. Try to create a VboMesh or Batch from your Shape once and draw that. Much faster.
  • Do you need to render individual shapes, or can you just merge them? Try merging all line segments into one big VboMesh. You may have to change the primitive type to GL_LINES.
  • Priority one is to reduce the number of draw calls, which is why the first 2 hints do exactly that.
  • Culling is almost always worth it. If you insist on using a separate mesh for each state, county or city, do cull them on the CPU. See the FrustumCulling sample for pointers on how to implement this.
  • To render a county in a different color, simply change the current draw color using gl::color(1, 0, 0) (for red) before drawing it. If you merge all counties into a single Vbo, simply add a color vertex attribute.

-Paul


#9

Thanks paul this is a big help.

I am working on points one and two.

How do you recommend converting the shapes into a mesh outline of the state(that can also be filled with color if needed)?

I have worked out two ways to do this.

First way does not render cleanly. There are missing parts to the state/county lines. Think i am going wrong somewhere.

Second way renders cleanly but only in color mode. In wireframe mode is shows the wireframe and not the outline of the state/county. I think this is the way it is meant to work but for my needs i only need outline and color fill.

I think the first way is the way to go but i think i am misunderstanding the way a GL_LINE works

std::vector<ci::vec2> m_aVertices;
std::vector<Path2d> aPaths = Shape.getContours();
for (auto path : aPaths) {
	std::vector<vec2> aPoints = path.getPoints();
	for (auto pt : aPoints) {
		m_aVertices.push_back(pt);
	}
}


// first way
gl::VboMesh::Layout layout;
layout.usage( GL_STATIC_DRAW ).attrib( geom::POSITION, 2 );
gl::VboMeshRef mesh = gl::VboMesh::create( m_aVertices.size(), GL_LINES, { layout } );
mesh->bufferAttrib( geom::POSITION, m_aVertices.size() * sizeof( vec2 ), m_aVertices.data() );

// second way
TriMesh triMesh = Triangulator(Shape, 1.f).calcMesh(Triangulator::WINDING_ODD);
gl::VboMeshRef mesh = gl::VboMesh::create(triMesh);

#10

Hi,

it looks like you’re (only) extracting the vertices from the shape that define the curves (a.k.a. knots) and you’re not subdividing the curves themselves. See Path2d::subdivide(). It will return a lot more vertices that together follow the shape more closely. You can then draw straight lines between them.

The GL_LINES primitive type expects you to send each line segment as a pair of vertices: A,B, B,C, C,D, etc. In your case, you may want to use GL_LINE_LOOP instead, which only requires you to send each vertex once: A, B, C, D, etc. It will also automatically connect the last vertex to the first to close the loop. In my last post I mentioned that you could merge multiple shapes into one big mesh. In that case you can not use GL_LINE_LOOP anymore, because it would draw unwanted lines between the shapes. So in that case you will have to rearrange your vertices and use GL_LINES.

Tessellation is indeed the only way to render filled shapes efficiently. But wireframe rendering will draw all triangles separately, the computer does not know which edges form the outline. So if you want to draw a solid shape with a proper outline, first draw the solid triangles (second way), then draw the lines (first way) on top in a different color.

-Paul


#11

Got it working. Fill and outline using meshes. 60 fps. Ignore Iowa state, think i am missing some data for it.

Really appreciate your help, Paul.