{{Format}}

== Overview ==

The [[Tutorial: Editable Mapset]] showed you how to add and edit graphic features that are associated to an existing background map feature.

But what if you want to draw something new on the map, independent of the background? Missing poles, right of way problems, roads, fences, septic lines, you name it - there’s a demand for doodling.

While Partner is obviously no replacement for a real CAD or GIS system, it is simpler, site-licensed, and designed for use both in the office and in the field.

This lesson teaches you how to build a basic drawing tool with a database backend. We’ll start with points and then add in lines as well.

Partner supports a standard Drawing module and mapset (http://update.partnersoft.com/4.4.8/modules/Drawing.zip), but building your own will give you the foundation you need to understand any editable mapset.

This is an advanced lesson, so we will be fairly brief with our descriptions and assume you are familiar with how to create and run actions etc. We also emphasize the use of script libraries to make your code more mature, efficient and maintainable.

This tutorial is part of the [[Course: Partner Workbench]].

=== audience === * system administrators and IT staff * power users * developers

=== objectives === * build a drawing tool * allow users to add point and line features * use libraries to reduce redundant code * learn how to connect the editing user interface tools to a mapset * learn about the tool rack, edit tab, wheel menu and map edit actors

=== prerequisites ===

You need a working Partner installation and basic familiarity with the Workbench. You also need to understand how to create and work with mapsets. [[Tutorial: Making a Mapset]] and its prerequisites are designed with this in mind.

== Design ==

The design here is pretty simple - draw point and line data that is placed by clicking on the map or via GPS. We want to support multiple symbols and line styles. Also, we want it to be easy to add new types of points or lines, without a lot of coding.

We’ll have three major types of graphics: * points, which are standalone point features * vertices, which are points that can have a parent vertex * spans, which are the lines connecting a vertex to its parent

We’ll store them in two database tables, point and vertex. We don’t need a span table since we’ll just draw the line from a vertex to its parent.

Since there’s no real data associated with these graphics, we’ll use a randomly generated globally unique ID (GUID) to identify them and connect vertices to their parent. We could have used a sequential ID, perhaps generated by the database, but the advantage of a GUID is that you can then merge data from multiple installations without fear of conflicts on the same sequential number.

== Module Configuration ==

=== create the module ===

Create a new module in modules/seat named “Doodle”.

=== create the database ===

Create the standard directory “databases” in modules/seat/Doodle. Select the directory and create a new file named “Doodle.xml”. Edit the file and go to the “Database” tab for a form-based editor.

Fill in only these two fields: * Database type: SQLite * File path: data/Doodle/database

This defines a SQLite embedded database located at data/Doodle/database.

=== create the drawing tables ===

Our database is empty, and we need to create the tables for our drawing data. In your module, create an actions/ directory and add the following Workbench action script to it:

==== modules/seat/Doodle/actions/Create Database.groovy ====

<pre> import com.partnersoft.system.SystemServices;

// fetch database configuration database = module.databaseNamed(“Doodle”);

// connect to the database connection = database.openConnection();

// create table (multi-line Groovy string) connection.runScript( “”” create cached table point (

id varchar(255), type varchar(255), x double, y double, rotation double

);

create cached table vertex (
id varchar(255), parent_id varchar(255), type varchar(255), x double y double

);ΒΆ

);

// close connection.close(); </pre>

=== restart workbench ===

Restart the workbench. This is required to actually load our new module and add the action to the Workbench Actions/Doodle menu.

Once it reloads, go to the Workbench menu and select Actions/Doodle/Create Database to run the script.

== Mapset Configuration ==

=== create the mapset ===

From the workbench, create a new mapset in config/seat/mapsets named “Doodle”. Connect it to the module by adding a file info/module.txt with the name of the module in it:

==== config/seat/mapsets/Doodle/info/module.txt ==== <pre> Doodle </pre>

== Points ==

OK, let’s think about the relationship between our data and the map view for a moment. We need to be able to: * add new records with the selected x and y coordinates * see the records on the map as point features * delete records

We’ll need the usual legend, frontend, etc. The new wrinkle is the Map Edit Actor, which we’ve only used a little bit thus far.

=== Map Edit Actor ===

The Map Edit Actor connects the various controls in the Map Viewer’s Edit Tab to your specific mapset data model. This allows the user to use the same tools regardless of which mapset they are working on.

Specifically, it allows you to enable: * drawable tools in the Tool Rack (top of the Edit tab in the viewer) * adding (+ on the wheel menu, or survey using GPS, typed in values, or drag from tool rack) * editing (E on the wheel menu) * deleting (- on the wheel menu) * rotating (R on the wheel menu) * moving (M on the wheel menu) * context-sensitive actions (A on the wheel menu)

The Map Edit Actor must be placed in a file named scripts/Map Edit Actor.groovy (or .bsh or .py or whatever scripting language you prefer).

==== scripts/Map Edit Actor.groovy ====

=== a “library” ===

Rather than scatter the data functions amongst the various actions, frontends, and drawing scripts, and risk duplicating them in many cases, let’s create a function library that handles all of the interactions with the database and map view for points.

Since we’re mature programmers now, we’ll even document it with some block comments for each function.

==== scriptlib/PointLib.bsh ==== <pre> import com.partnersoft.data.Naming; import com.partnersoft.data.RandomGUID; import com.partnersoft.maps.model.MapDataItem;

script.include(“DatabaseLib.bsh”);

/**
  • Creates a DataRecordSource to go over the point objects.

*/

SQLDataRecordSource createPointSource() {
return new SQLDataRecordSource(connection, “select * from point”);

}

/**
  • Adds a point to the database updates the view, and selects it.

*/

addPoint(String type, double x, double y) {

// generate the data record id = RandomGUID.generateGUIDString(); data = new Naming(); data.put(“id”, id); data.put(“type”, type); data.put(“x”, x); data.put(“y”, y); connection.insertOrUpdate(“point”, “id”, data);

// update map view and select app.logic.spaceLogic.enableAndRefresh(mapset); app.logic.selectionLogic.selectAtPoint(x, y);

}

/**
  • Deletes the given point object.

*/

deletePoint(String id) {
log.info(“deleting point ” + id); connection.runScript(“delete from point where id = ” + SQLLib.convertAndQuote(id)); app.logic.spaceLogic.enableAndRefresh(mapset);

}

</pre>

=== translator frontend ===

The frontend is straightforward. Note that instead of using translator.processFindItem() like we did in previous examples, we’re using translator.process() and passing in an actual point graphic. We set the X and Y directly instead of using an existing background feature.

==== translator/frontends/Points.bsh ==== <pre> import com.partnersoft.maps.translator.MapDataPoint;

script.include(“PointLib.bsh”);

source = createPointSource(); for (record : source) {

point = new MapDataPoint(); point.setDataType(record.get(“type”)); point.setGraphicType(record.get(“type”)); point.setX(record.get(“x”)); point.setY(record.get(“y”)); point.setData(record); translator.process(point);

} </pre>

=== my first drawing script === This is our drawing script. It will draw... trees!

Drawing scripts are similar to action scripts, but they go in the “drawing” section and have more [[predefined variables]] than action scripts. They have: * action, which is “Draw”, “GPS”, “Shoot”, or “Delete” * x * y * reading (for GPS)

They also have the usual “selected” variable so that you can use a previous selection to affect what you’re drawing.

Note that this doesn’t really “draw”. What it does is insert a record into the database, and trigger a refresh, which then causes the frontend script to run, which generates the actual graphics, which are then drawn by the map viewer’s rendering engine. But to the user, it does look like they are drawing.

==== drawing/Tree.bsh ==== <pre> script.include(“PointLib.bsh”); if (action.equals(“Delete”)) {

if (selected != null) {
deletePoint(selected.data.get(“id”)); app.logic.selectionLogic.clearSelectionList();

}

} else {

addPoint(“Tree”, x, y);

}

connection.close(); </pre>

=== get something on the map ===

OK, reload the mapset (you may have to restart the software). The drawing interface should now be visible under the data tab. It looks like this:

[[Image:DrawingTab.png|frame|none|The drawing tab should appear in the lower-right corner.]]

Try it out. Trees is the only option now, so it’s selected as your drawn type. Hit the draw button and go crazy clicking on the map. Of course, nothing shows up because we don’t have a legend.

You know what to do: run the “Generate Legends” wizard, then fix the icon so it looks like a tree.

=== more! ===

Adding more types is just a matter of cutting and pasting. Let’s add drawing scripts for “Bad Dog” and “House”. They look just like our Tree script but with different types set.

==== drawing/House.bsh ==== <pre> script.include(“PointLib.bsh”); if (action.equals(“Delete”)) {

if (selected != null) {
deletePoint(selected.data.get(“id”)); app.logic.selectionLogic.clearSelectionList();

}

} else {

addPoint(“House”, x, y); }

connection.close(); </pre>

==== drawing/Bad Dog.bsh ==== <pre> script.include(“PointLib.bsh”); if (action.equals(“Delete”)) {

if (selected != null) {
deletePoint(selected.data.get(“id”)); app.logic.selectionLogic.clearSelectionList();

}

} else {

addPoint(“Bad Dog”, x, y); }

connection.close(); </pre>

==== fixing the legend ====

Use the drawing tool to add some houses and bad dogs. They don’t appear... they aren’t in the legend. If you go to the legend editor (legends/default.xml) you’ll see these new types showing up, but they need styles to display.

This is a good opportunity to learn to add styles and legend entries without using the “Generate Legend” wizard.

First, be sure you have points of all the types added. One clue is log entries like: <pre> Invalid graphic type: point-Bad Dog </pre>

That means the translator frontend has sent some Bad Dogs through but the legend doesn’t have an entry for them.

First, you need some styles. Grab some icons for bad dog and house out of maps/Background/icons or elsewhere. Then create new point styles named “Bad Dog” and “House”, and point them at your icons.

Then go to the legend editing form in the Workbench (legends/default.xml). You should see something like this:

[[Image:LegendsDefault.png|frame|none|The legends cog menu in all of its glory.]]

Set the point-Bad Dog to point at your Bad Dog style and the point-House to point at the House style.

Now you data should show up.

=== reduce and reuse ===

But wait - those drawing scripts look almost exactly the same. Can’t we move some of the code into our function library and make them truly stupid? Of course we can. Add this to the bottom of scriptlib/PointLib.bsh:

<pre> /**

  • Convenience function encapsulating most of what you need to do in a drawing script.

*/

pointDrawingHandler(String type) {
if (action.equals(“Delete”)) {
if (selected != null) {
deletePoint(selected.data.get(“id”)); app.logic.selectionLogic.clearSelectionList();

}

} else {

addPoint(type, x, y);

} connection.close();

} </pre>

And here are your drawing scripts, simplified greatly:

==== drawing/Tree.bsh ==== <pre> script.include(“PointLib.bsh”);

pointDrawingHandler(“Tree”); </pre>

==== drawing/House.bsh ==== <pre> script.include(“PointLib.bsh”);

pointDrawingHandler(“House”); </pre>

==== drawing/Bad Dog.bsh ==== <pre> script.include(“PointLib.bsh”);

pointDrawingHandler(“Bad Dog”); </pre>

They should behave exactly the same as before.

== Lines ==

OK, now for some line data. We don’t really have drawing tools for lines themselves, but we can manipulate the points (vertices) in a line to draw it.

Each vertex has a parent_id variable that contains the GUID of its “upstream” vertex. Our solution will query first the vertexes and draw them as points, then the spans by joining the vertex table to itself linking parent_id to id.

Otherwise, this solution should look very similar to our points setup.

=== Scripts ===

==== scriptlib/VertexLib.bsh ==== <pre> import com.partnersoft.data.Naming; import com.partnersoft.data.RandomGUID; import com.partnersoft.maps.model.MapDataItem;

script.include(“DatabaseLib.bsh”);

/**
  • Creates a DataRecordSource to go over the vertex objects.

*/

SQLDataRecordSource createVertexSource() {
return new SQLDataRecordSource(connection, “select * from vertex”);

}

/**
  • Creates a DataRecordSource to go over the spans between vertices.

*/

SQLDataRecordSource createSpanSource() {
return new SQLDataRecordSource(connection, “select vertex1.type as type, vertex1.x as x1, vertex1.y as y1, vertex2.x as x2, vertex2.y as y2 ” +
” from vertex as vertex1, vertex as vertex2 where vertex1.parent_id = vertex2.id”);

}

/**
  • Adds a vertex, hooks it up to the parent, if any, updates the view and selects it.

*/

addVertex(MapDataItem parent, String type, double x, double y) {

// figure out if parent is valid and use it to get a parent ID for our span parentID = “”; if (parent != null && parent.getData().get(“id”) != null)

parentID = parent.getData().get(“id”);

// generate the actual data id = RandomGUID.generateGUIDString(); data = new Naming(); data.put(“id”, id); data.put(“parent_id”, parentID); data.put(“type”, type); data.put(“x”, x); data.put(“y”, y); connection.insertOrUpdate(“vertex”, “id”, data);

// update the map view and select it app.logic.spaceLogic.enableAndRefresh(mapset); app.logic.selectionLogic.selectAtPoint(x, y);

}

/**
  • Deletes a vertex and its associated span.

*/

deleteVertex(String id) {
connection.runScript(“delete from vertex where id = ” + SQLLib.convertAndQuote(id)); connection.runScript(“update vertex set id=’’ where id=” + SQLLib.convertAndQuote(id)); app.logic.spaceLogic.enableAndRefresh(mapset);

}

/**
  • Convenience function encapsulating most of what you need to do in a drawing script.

*/

vertexDrawingHandler(String type) {
if (action.equals(“Delete”)) {
if (selected != null) {
deleteVertex(selected.data.get(“id”)); app.logic.selectionLogic.clearSelectionList();

}

} else {

addVertex(selected, type, x, y);

} connection.close();

} </pre>

==== translator/frontends/Vertices.bsh ==== <pre> import com.partnersoft.maps.translator.MapDataPoint; import com.partnersoft.maps.translator.MapDataPolyline; import com.partnersoft.data.DoubleBuffer;

script.include(“VertexLib.bsh”);

source = createVertexSource(); for (record : source) {

point = new MapDataPoint(); point.setDataType(record.get(“type”)); point.setGraphicType(record.get(“type”)); point.setX(record.get(“x”)); point.setY(record.get(“y”)); point.setData(record); translator.process(point);

} source.close();

source = createSpanSource(); for (record : source) {

coords = new DoubleBuffer(); coords.add(record.get(“x1”)); coords.add(record.get(“y1”)); coords.add(record.get(“x2”)); coords.add(record.get(“y2”)); line = new MapDataPolyline(coords, record); line.setDataType(record.get(“type”) + ” span”); line.setGraphicType(record.get(“type”) + ” span”); translator.process(line);

} source.close(); connection.close(); </pre>

==== drawing/Road.bsh ==== <pre> script.include(“VertexLib.bsh”);

vertexDrawingHandler(“Road”); </pre>

==== drawing/Stream.bsh ==== <pre> script.include(“VertexLib.bsh”);

vertexDrawingHandler(“Stream”); </pre>

=== Legend and Styles ===

Once again, you need to first draw (without seeing) to “seed” the database with items. Then you can either re-run “Generate Legend” or manually enter in your styles with the forms.

Here’s a specific procedure that works: * draw road and stream points * generate legend * select a road point, then draw another road point (draws an invisible line) * select a stream point, then draw another stream point * generate legend

When you’re done it should look something like this:

[[Image:DrawingExample.png|frame|none|Remember, kids, don’t drink and draw.]]

== Export ==

So what if we wanted to update our GIS or CAD system? One option is exporting ESRI [[shapefiles]], a common format supported by many mapping systems.

==== actions/Export Shapefile.bsh ==== <pre> import com.partnersoft.io.formats.shapefile.*; import com.partnersoft.data.Naming; import com.partnersoft.geometry.Polyline;

script.include(“PointLib.bsh”); script.include(“VertexLib.bsh”);

// first do the points builder = new ShapefileBuilder(); builder.setPath(“data/Drawing/export/point.shp”); builder.setType(ShapefileConstants.POINT); builder.addField(“id”, 32); builder.addField(“type”, 32);

source = createPointSource(); for (record : source) {

builder.writePoint(record.get(“x”), record.get(“y”), record);

} source.close(); builder.close();

// now the vertices builder = new ShapefileBuilder(); builder.setPath(“data/Drawing/export/point.shp”); builder.setType(ShapefileConstants.POINT); builder.addField(“id”, 32); builder.addField(“parent_id”, 32); builder.addField(“type”, 32);

source = createVertexSource(); for (record : source) {

builder.writePoint(record.get(“x”), record.get(“y”), record);

} source.close(); builder.close();

// and the spans builder = new ShapefileBuilder(); builder.setPath(”./example.shp”); builder.setType(ShapefileConstants.POLYLINE); builder.addField(“id”, 32); builder.addField(“parent_id”, 32); builder.addField(“type”, 32);

source = createSpanSource(); for (record : source) {

polyline = new Polyline(4); polyline.addPoint(record.get(“x1”), record.get(“y1”)); polyline.addPoint(record.get(“x2”), record.get(“y2”)); builder.writePolyline(polyline, record);

} source.close(); connection.close(); builder.close(); </pre>

== Ideas for Expansion == * add more drawing scripts with different types * make points editable with a form

Previous topic

<no title>

This Page