KML Generation Example App

You can see this app running online at: KML Generation App Online

The KML Generation App produces a KML file to display a set of locations and lines which can be previewed on a map and downloaded by the user. It is a very simple app showing how to use the core features of the Tropofy framework:

  • It uses simple SQLAlchemy classes forming an object orientated interface to the “engine” of this app
  • It has a very simple SQLAlchemy relationship between the two classes making implementing the KML creation easier
  • It uses the most commonly used widgets, the tropofy.widgets.SimpleGrid and tropofy.widgets.KMLMap widgets
  • It uses the SimpleKml python package to create the KML
  • It has a very simple two step GUI

To run this app locally:

$ source tropofy_env/bin/activate
$ tropofy quickstart tropofy_generate_kml
$ cd tropofy_generate_kml
$ nano config.py  # Insert your keys
$ python setup.py develop
$ tropofy app -c config.py

Imported Modules

First we import the SQLAlchemy, SimpleKML and Tropofy modules required by our app

from sqlalchemy.types import Text, Float
from sqlalchemy.schema import Column, ForeignKeyConstraint, UniqueConstraint
from sqlalchemy.orm import relationship
from simplekml import Kml, Style, IconStyle, Icon, LineStyle
import pkg_resources
from collections import OrderedDict
from tropofy.app import AppWithDataSets, Step, StepGroup
from tropofy.widgets import SimpleGrid, KMLMap

SQLAlchemy Classes

We then define the two classes that we are going to need, Location and Line

Location Class

  • Note the base class of all SQLAlchemy derived classes must be either DataSetMixin if your app is going to support multiple data sets for your users, or ORMBase if it is not
  • We inlcude a multi-column UniqueConstraint within the __table_args__ to ensure two locations cannot have the same name within the same data set.
  • We do not want any members on our Location class to be empty, so all columns are initialised with nullable=False
  • The __init__ method for the Location class is not strictly required but is included for readability. SQLAlchemy provides a default initialiser with keyword arguements equal to the names of the column members defined for your class. See here for more info.
class Location(DataSetMixin):
    name = Column(Text, nullable=False)
    latitude = Column(Float, nullable=False)
    longitude = Column(Float, nullable=False)

    def __init__(self, name, latitude, longitude):
        self.name = name
        self.latitude = latitude
        self.longitude = longitude

    @classmethod
    def get_table_args(cls):
        return (UniqueConstraint('data_set_id', 'name', name='_location_uc'),)

Line Class

  • When users are creating lines we only want to allow them to use existing locations, and when locations are deleted we want the lines that refer to them to also be deleted. We achieve this by using an SQLAlchemy ForeignKeyConstraint
  • The SQLAlchemy relationships used are for convenience and make constructing and referring to the locations a Line encapsulates in our app, when we are creating the KML much easier
class Line(DataSetMixin):
    start_location_name = Column(Text, nullable=False)
    end_location_name = Column(Text, nullable=False)

    # The primaryjoin argument to relationship is only needed when there is ambiguity
    start_location = relationship(Location, primaryjoin="and_(Line.data_set_id==Location.data_set_id, Line.start_location_name==Location.name)")
    end_location = relationship(Location, primaryjoin="and_(Line.data_set_id==Location.data_set_id, Line.end_location_name==Location.name)")

    def __init__(self, start_location_name, end_location_name):
        self.start_location_name = start_location_name
        self.end_location_name = end_location_name

    @classmethod
    def get_table_args(cls):
        return (
            ForeignKeyConstraint(['start_location_name', 'data_set_id'], ['location.name', 'location.data_set_id'], ondelete='CASCADE', onupdate='CASCADE'),
            ForeignKeyConstraint(['end_location_name', 'data_set_id'], ['location.name', 'location.data_set_id'], ondelete='CASCADE', onupdate='CASCADE')
        )

Widgets

The only widget we are going to need beyond the tropofy.widgets.SimpleGrid widget is the tropofy.widgets.KMLMap widget which will both allow users to preview the KML generated on a map, and provide a download button so that users can download the KML file. The tropofy.widgets.KMLMap.get_kml() method of the KMLMap widget essentially comprises the core of our app. It uses the SimpleKml python package to create the KML.

class MyKMLMap(KMLMap):
    def get_kml(self, app_session):

        kml = Kml()

        def LongLat(l):
            return (l.longitude, l.latitude)

        mylocstyle = Style(iconstyle=IconStyle(scale=0.8, icon=Icon(href='https://maps.google.com/mapfiles/kml/paddle/blu-circle-lv.png')))
        LocsFolder = kml.newfolder(name="Locations")
        locations =  app_session.data_set.query(Location).all()
        if len(locations) < 100:
            for p in [LocsFolder.newpoint(name=loc.name, coords=[LongLat(loc)]) for loc in locations]:
                p.style = mylocstyle

        mylinestyle = Style(linestyle=LineStyle(color='FF00F5FF', width=4))
        LinesFolder = kml.newfolder(name="Lines")

        lines = app_session.data_set.query(Line).all()
        if len(lines) < 100:
            for line in [LinesFolder.newlinestring(name='line', coords=[LongLat(l.start_location), LongLat(l.end_location)]) for l in lines]:
                line.style = mylinestyle

        return kml.kml()

The App Itself

The key features of our app are the gui which we define with the tropofy.app.AppWithDataSets.get_gui() function and the example data we provide with the tropofy.app.AppWithDataSets.get_examples() function.

  • We want users to be able to store different data sets within our app so we derive our app from the tropofy.app.AppWithDataSets class
  • Note there must be one and only one class that derives from tropofy.app.AppWithDataSets in your python file so that Tropofy can instantiate an instance of your app
  • Example data sets are defined by a name and a function which will load the example data into the database
  • Note the functions provided to load up the example data sets take a single tropofy.app.data_set.AppDataSet parameter, which wraps up SQLAlchemy’s session object, and provides an interface to the database and allows us to add, via tropofy.app.AppDataSet.add_all() the example data
  • For simplicity we hard code the example data in our python code. Generally embedding data in code should be avoided.
  • You can use the function read_write_xl.create_example_data_set_from_excel to load your example data from Excel files, other tutorials do exactly this.
  • Our GUI consists of two tropofy.widgets.SimpleGrid widgets and a tropofy.widgets.KMLMap
class MyKMLGeneratorApp(AppWithDataSets):

    def get_name(self):
        return "KML Generation"

    def get_examples(self):
        return {"Demo data for Brisbane": self.load_example_data_for_brisbane}

    def get_static_content_path(self, app_session):
        return pkg_resources.resource_filename('te_generate_kml', 'static')

    def get_gui(self):
        step_group1 = StepGroup(name='Enter your data')
        step_group1.add_step(Step(name='Enter your locations', widgets=[SimpleGrid(Location)]), 'locations')
        step_group1.add_step(Step(name='Enter your lines', widgets=[SimpleGrid(Line)]), 'lines')

        step_group2 = StepGroup(name='View KML')
        step_group2.add_step(Step(name='View your KML', widgets=[MyKMLMap()]), 'kml')

        return OrderedDict([
            ('data', step_group1),
            ('kml', step_group2),
        ])

    @staticmethod
    def load_example_data_for_brisbane(app_session):
        locs = [
            Location("CLAYFIELD", -27.417536, 153.056677),
            Location("SANDGATE", -27.321538, 153.069267),
            Location("KIPPA-RING", -27.226494, 153.085287),
            Location("CHERMSIDE WEST", -27.37862, 153.018257),
            Location("EVERTON PARK", -27.406931, 152.992265),
            Location("MILTON", -27.471324, 153.004781),
            Location("KENMORE HILLS", -27.49615, 152.926702),
            Location("WACOL", -27.590778, 152.929768),
            Location("ARCHERFIELD", -27.568388, 153.024165),
        ]
        app_session.data_set.add_all(locs)
        MyKMLGeneratorApp.load_example_lines(locs, app_session.data_set)

    @staticmethod
    def load_example_lines(locations, data_set):
        lines = [
            Line(locations[0].name, locations[1].name),
            Line(locations[1].name, locations[2].name),
            Line(locations[2].name, locations[3].name),
            Line(locations[3].name, locations[4].name),
            Line(locations[4].name, locations[5].name),
            Line(locations[5].name, locations[6].name),
            Line(locations[6].name, locations[7].name),
            Line(locations[7].name, locations[8].name),
        ]
        data_set.add_all(lines)

    def get_icon_url(self):
        return "/{}/static/kml_generation.png".format(
            self.url_name,
        )

Full code

"""
Author:      www.tropofy.com

Copyright 2015 Tropofy Pty Ltd, all rights reserved.

This source file is part of Tropofy and governed by the Tropofy terms of service
available at: http://www.tropofy.com/terms_of_service.html

This source file 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 license files for details.
"""

from sqlalchemy.types import Text, Float
from sqlalchemy.schema import Column, ForeignKeyConstraint, UniqueConstraint
from sqlalchemy.orm import relationship
from simplekml import Kml, Style, IconStyle, Icon, LineStyle
import pkg_resources
from collections import OrderedDict
from tropofy.app import AppWithDataSets, Step, StepGroup
from tropofy.widgets import SimpleGrid, KMLMap
from tropofy.database.tropofy_orm import DataSetMixin


class Location(DataSetMixin):
    name = Column(Text, nullable=False)
    latitude = Column(Float, nullable=False)
    longitude = Column(Float, nullable=False)

    def __init__(self, name, latitude, longitude):
        self.name = name
        self.latitude = latitude
        self.longitude = longitude

    @classmethod
    def get_table_args(cls):
        return (UniqueConstraint('data_set_id', 'name', name='_location_uc'),)


class Line(DataSetMixin):
    start_location_name = Column(Text, nullable=False)
    end_location_name = Column(Text, nullable=False)

    # The primaryjoin argument to relationship is only needed when there is ambiguity
    start_location = relationship(Location, primaryjoin="and_(Line.data_set_id==Location.data_set_id, Line.start_location_name==Location.name)")
    end_location = relationship(Location, primaryjoin="and_(Line.data_set_id==Location.data_set_id, Line.end_location_name==Location.name)")

    def __init__(self, start_location_name, end_location_name):
        self.start_location_name = start_location_name
        self.end_location_name = end_location_name

    @classmethod
    def get_table_args(cls):
        return (
            ForeignKeyConstraint(['start_location_name', 'data_set_id'], ['location.name', 'location.data_set_id'], ondelete='CASCADE', onupdate='CASCADE'),
            ForeignKeyConstraint(['end_location_name', 'data_set_id'], ['location.name', 'location.data_set_id'], ondelete='CASCADE', onupdate='CASCADE')
        )


class MyKMLMap(KMLMap):
    def get_kml(self, app_session):

        kml = Kml()

        def LongLat(l):
            return (l.longitude, l.latitude)

        mylocstyle = Style(iconstyle=IconStyle(scale=0.8, icon=Icon(href='https://maps.google.com/mapfiles/kml/paddle/blu-circle-lv.png')))
        LocsFolder = kml.newfolder(name="Locations")
        locations =  app_session.data_set.query(Location).all()
        if len(locations) < 100:
            for p in [LocsFolder.newpoint(name=loc.name, coords=[LongLat(loc)]) for loc in locations]:
                p.style = mylocstyle

        mylinestyle = Style(linestyle=LineStyle(color='FF00F5FF', width=4))
        LinesFolder = kml.newfolder(name="Lines")

        lines = app_session.data_set.query(Line).all()
        if len(lines) < 100:
            for line in [LinesFolder.newlinestring(name='line', coords=[LongLat(l.start_location), LongLat(l.end_location)]) for l in lines]:
                line.style = mylinestyle

        return kml.kml()


class MyKMLGeneratorApp(AppWithDataSets):

    def get_name(self):
        return "KML Generation"

    def get_examples(self):
        return {"Demo data for Brisbane": self.load_example_data_for_brisbane}

    def get_static_content_path(self, app_session):
        return pkg_resources.resource_filename('te_generate_kml', 'static')

    def get_gui(self):
        step_group1 = StepGroup(name='Enter your data')
        step_group1.add_step(Step(name='Enter your locations', widgets=[SimpleGrid(Location)]), 'locations')
        step_group1.add_step(Step(name='Enter your lines', widgets=[SimpleGrid(Line)]), 'lines')

        step_group2 = StepGroup(name='View KML')
        step_group2.add_step(Step(name='View your KML', widgets=[MyKMLMap()]), 'kml')

        return OrderedDict([
            ('data', step_group1),
            ('kml', step_group2),
        ])

    @staticmethod
    def load_example_data_for_brisbane(app_session):
        locs = [
            Location("CLAYFIELD", -27.417536, 153.056677),
            Location("SANDGATE", -27.321538, 153.069267),
            Location("KIPPA-RING", -27.226494, 153.085287),
            Location("CHERMSIDE WEST", -27.37862, 153.018257),
            Location("EVERTON PARK", -27.406931, 152.992265),
            Location("MILTON", -27.471324, 153.004781),
            Location("KENMORE HILLS", -27.49615, 152.926702),
            Location("WACOL", -27.590778, 152.929768),
            Location("ARCHERFIELD", -27.568388, 153.024165),
        ]
        app_session.data_set.add_all(locs)
        MyKMLGeneratorApp.load_example_lines(locs, app_session.data_set)

    @staticmethod
    def load_example_lines(locations, data_set):
        lines = [
            Line(locations[0].name, locations[1].name),
            Line(locations[1].name, locations[2].name),
            Line(locations[2].name, locations[3].name),
            Line(locations[3].name, locations[4].name),
            Line(locations[4].name, locations[5].name),
            Line(locations[5].name, locations[6].name),
            Line(locations[6].name, locations[7].name),
            Line(locations[7].name, locations[8].name),
        ]
        data_set.add_all(lines)

    def get_icon_url(self):
        return "/{}/static/kml_generation.png".format(
            self.url_name,
        )