Commits

Arthur Endsley  committed 423992c

Beginning to added handlers for the old service's real-time RTMM requests; encountering insoluble problems with Apache, however

  • Participants
  • Parent commits 762a35e

Comments (0)

Files changed (16)

+Steps to setting up the RTMM service on a new machine:
+
+1. Create user django_user_rtmm on database server
+    a. Set the Connection Limit to -1
+
+2. Create database rtmm_sql
+    a. Make yourself the owner of the database
+    b. Use the postgis-1.5.2 template
+    c. Set the Connection Limit to -1
+
+3. Grant permissions to django_user_rtmm
+    a. Run arbitrary SQL: "GRANT ALL ON DATABASE rtmm_sql TO django_user_rtmm"
+
+4. Populate the database with initial objects and fixtures
+    a. user@ubuntu:$ ./manage.py syncdb
+    b. user@ubuntu:$ ./manage.py load_initial_devices

File src/djangosite/apache/default

     # Configuration for the MichiganView RTMM Applications
     ###############################################################
 
-    Alias /rtmm/media/ "/home/amnester/Documents/projects/rtmm/src/djangosite/media/"
-    <Directory "/home/amnester/Documents/projects/rtmm/src/djangosite/media/">
+    Alias /rtmm/media/ "/usr/local/project/rtmm/src/djangosite/media/"
+    <Directory "/usr/local/project/rtmm/src/djangosite/media/">
         Order allow,deny
         Options Indexes FollowSymLinks
         Allow from all
         IndexOptions FancyIndexing
     </Directory>
 
-    WSGIScriptAlias /rtmm /home/amnester/Documents/projects/rtmm/src/djangosite/apache/django.wsgi
-    <Directory /home/amnester/Documents/projects/rtmm/src/djangosite/apache/django.wsgi>
+    WSGIScriptAlias /rtmm /usr/local/project/rtmm/src/djangosite/apache/django.wsgi
+    <Directory /usr/local/project/rtmm/src/djangosite/apache/django.wsgi>
         Order deny,allow
         Allow from all
     </Directory>

File src/djangosite/apache/django.wsgi

 import os
 import sys
 
-# add the path of the Django version
+# Append the whole local directory
+sys.path.append('/usr/local')
+
+# Add the path of the Django version
+sys.path.append('/usr/local/django/trunk')
 sys.path.append('/usr/local/django/Django-1.3/')
 
-# append the django project root directory to the python path
-sys.path.append('/home/amnester/Documents/projects/rtmm/src/djangosite')
-sys.path.append('/home/amnester/Documents/projects/rtmm/src')
+# Append the Django project root directory to the Python path
+sys.path.append('/usr/local/project')
 
-os.environ['DJANGO_SETTINGS_MODULE'] = 'djangosite.settings'
+# Append the Django-Piston directory
+sys.path.append('/usr/local/project/django-piston')
 
-#os.environ['TEMPORARY_DIRECTORY'] = '/usr/local/project/rtmm/tmp/'
+# Append the django project root directory to the python path
+sys.path.append('/usr/local/project/rtmm/src')
+sys.path.append('/usr/local/project/rtmm/src/djangosite')
+
+os.environ['DJANGO_SETTINGS_MODULE'] = 'settings'
+os.environ['TEMPORARY_DIRECTORY'] = '/usr/local/project/rtmm/tmp/'
 
 import django.core.handlers.wsgi
 application = django.core.handlers.wsgi.WSGIHandler()

File src/djangosite/api/emitters.py

+from django.utils import simplejson
+from django.core.serializers.json import DateTimeAwareJSONEncoder
+from piston.emitters import Emitter
+
+class ExtJSONEmitter(Emitter):
+    """
+    JSON emitter, understands timestamps, wraps result set in object literal
+    for Ext JS compatibility
+    """
+    def render(self, request):
+        cb = request.GET.get('callback')
+        ext_dict = {'success': True, 'data': self.construct()}
+        seria = simplejson.dumps(ext_dict, cls=DateTimeAwareJSONEncoder, ensure_ascii=False, separators=(',',':'), indent=0) 
+        # Remove separators argument set indent to 4 for "pretty printing"
+
+        # Callback
+        if cb:
+            return '%s(%s)' % (cb, seria)
+
+        return seria
+    
+Emitter.register('ext-json', ExtJSONEmitter, 'application/json; charset=utf-8')
+
+class JSONEmitter(Emitter):
+    """
+    JSON emitter, understands timestamps; just like the ExtJSONEmitter but does
+    not wrap in an object literal, which enables compatiblity for trees
+    """
+    def render(self, request):
+        cb = request.GET.get('callback')
+        ext_dict = self.construct()
+        seria = simplejson.dumps(ext_dict, cls=DateTimeAwareJSONEncoder, ensure_ascii=False, separators=(',',':'), indent=0) 
+        # Remove separators argument set indent to 4 for "pretty printing"
+
+        # Callback
+        if cb:
+            return '%s(%s)' % (cb, seria)
+
+        return seria
+    
+Emitter.register('json', JSONEmitter, 'application/json; charset=utf-8')

File src/djangosite/api/views.py

 import re
 import datetime
+import pykml as KML
 from django.shortcuts import render_to_response
 from django.http import HttpResponse
 from django.contrib.gis.shortcuts import render_to_kml
 from django.views.decorators.cache import cache_page
 from djangosite.main.models import Satellite, SatellitePosition, Sensor
 from piston.utils import rc
-import re
-import pykml as KML
-import datetime
 
-def apachetest(request):
+
+
+def apache_test(request):
     return HttpResponse('this works')
     
 
+
 def satellite_to_kml(request):
 
     for key in ['satellite','start','end']:
     else:
         # default return if no observations were returned
         kmldoc = KML.Document()
-    return kmldoc
+    return render_to_kml(kmldoc)
     

File src/djangosite/main/fixtures/initial_data.json

     "model": "main.satellite", 
     "fields": {
       "altitude": 705, 
-      "name": "AQUA"
+      "name": "AQUA",
+      "operational": "True"
     }
   }, 
   {
     "model": "main.satellite", 
     "fields": {
       "altitude": 705, 
-      "name": "TERRA"
+      "name": "TERRA",
+      "operational": "True"
     }
   }, 
   {

File src/djangosite/main/management/commands/load_rtmm_data.py

 from djangosite.main.views import getdevicelist
 from django.db.models import Max
 from djangosite.main.views import UTC
+from djangosite.settings import EARTH_OBSERVING_SATS
 import datetime
 import string
 import math
 
         type = 'sat'
         
-        for satellite in Satellite.objects.all():
+        for satellite in Satellite.objects.filter(name__in=EARTH_OBSERVING_SATS):#.all():
             print satellite.name
             
             # find the last satellite position\

File src/djangosite/main/models.py

         abstract = True
 
 
+
 class Satellite(AbstractGeoTable):
     '''Models a satellite
     '''
-    name         = models.CharField(max_length=50 , unique = True)
-    altitude     = models.IntegerField()
+    name        = models.CharField(max_length=50 , unique = True)
+    altitude    = models.IntegerField()
+    #operational = models.BooleanField(default=False)
     
     def __unicode__(self):
         return "{name}".format(name=self.name)
 
 
+
 class SatellitePosition(AbstractGeoTable):
     '''Models the position of a  satellite at a given time
     '''
             )
 
 
+
 class Sensor(AbstractGeoTable):
     '''Models a satellite sensor
     '''

File src/djangosite/main/views.py

-# Create your views here.
-from lxml import etree
 from pykml.factory import KML_ElementMaker as KML
+from pykml.factory import etree
+from django.shortcuts import render_to_response
+from django.http import HttpResponse
+from django.contrib.gis.shortcuts import render_to_kml
+from django.views.decorators.cache import cache_page
+from djangosite.main.models import Satellite, SatellitePosition, Sensor
+from djangosite.utils import nadir_document, nadir_model_placemark
 import urllib2
 import datetime
 
     def dst(self, dt):
         return datetime.timedelta(0)
 
-def process_request(type,id,start,end,timestep):
+
+
+def real_time_nadir(request):
+    '''
+    On-demand (no use of cached RTMM data) generation of a real-time KML file
+    of satellite ephemeris data. Accepts the following GET parameters:
+        offering    {String}    Name of the satellite
+        timestep    {Integer}   Timestep to use as NetworkLink refresh interval
+    '''
+    attrs = flatten_dict(request.GET)
+
+    if attrs.has_key('data'):
+        ext_posted_data = simplejson.loads(request.GET.get('data'))
+        attrs = flatten_dict(ext_posted_data)
+
+    if 'offering' not in attrs.keys():
+        return HttpResponse(status=400) # 400 Bad Request
+
+    else:
+        attrs['offering'] = attrs['offering'].upper() # For user's convenience
+
+        if attrs['offering'].upper() not in Satellite.objects.values_list('name', flat=True):
+            return HttpResponse(status=501) # 501 Not Implemented
+
+    if 'timestep' not in attrs.keys():
+        attrs['timestep'] = 10 # Defaults to once every 10 seconds
+
+    else: # Check if the specified a refresh rate faster than once every 5 secs
+        attrs['timestep'] = int(attrs['timestep'])
+
+        if attrs['timestep'] < 5:
+            return HttpResponse(status=503) # 503 Throttled
+
+    # We use .utcnow() instead of .now() because RTMM service uses UTC
+    now = datetime.datetime.utcnow()
+
+    # Create formatted time strings such as 2011-09-01T00:00:00Z
+    start = now.strftime('%Y-%m-%dT%H:%M:%SZ')
+    # end = (now + datetime.timedelta(seconds=attrs['timestep'])).strftime('%Y-%m-%dT%H:%M:%SZ')
+
+    # Get a URL for the request
+    request_url = process_request('nadir', attrs['offering'], start, start, attrs['timestep'])
+
+    # The List info looks like: ['AQUA', '1.314899302E9', '1.314899302E9', '1']
+    info, lats, lngs = nadir_track(request_url)
+
+    # Make sure we're getting the right observations
+    assert info[0] == attrs['offering']
+
+    kml_doc = nadir_document(offering, start,
+        {'lookat': {
+            'longitude': lngs[0],
+            'latitude': lats[0],
+            'altitude': Satellite.objects.get(name=attrs['offering']).altitude
+            }
+        })
+
+    kml_doc.append(nadir_placemark())
+
+    return render_to_kml('default.kml', {'content': etree.tostring(kmldoc)})
+
+
+
+def process_request(type, id, start, end, timestep):
+    '''
+    Generates a URL that is constructed for an RTMM request; takes the following
+    arguments:
+        type        {String}    Either 'nadir' or 'footprint'
+        id          {String}    The name of the satellite or sensor (e.g. 'AQUA')
+        start       {String}    The datetime string indicating the start of the
+                                interval to request (e.g. '2011-09-01T00:00:00')
+        end         {String}    The datetime string indicating end of the interval
+        timestep    {Integer}   The number of seconds between observations to return
+    Returns:
+                    {String}    The URL to use in an RTMM service request
+    '''
 
     nadirurl1 =     'http://rtmm2.nsstc.nasa.gov/SOS/nadir?version=1.0&observedProperty=urn:ogc:sensor:satellite:nadirTrack&Service=SOS&request=GetObservation&offering='
     nadirurl2 =     '&format=text/xml;%20subtype=%22om/1.0%22&time='
     else:
             return footprinturl
 
+
+
 def nadir_track(url):
+    '''
+    Processes an XML response for nadir data from the RTMM service; takes the
+    following arguments:
+        url         {String}    The URL from which to obtain the XML feed
+    Returns:
+        infolist    {List}      Contains offering, something, something, something
+        xcoord      {List}      Contains the longitudes of the observations
+        ycoord      {List}      Contains the latitudes of the observations
+    '''
     
     nadirurl = url
     fileobject = urllib2.urlopen(nadirurl)
                                         } 
                         )
     
-    print values
+    #print values
     if not values:#error handling for service problems
 	return (None,None,None)
     
                 
 
 def swath(url):
-    
+    '''
+    Processes an XML response for footprint data from the RTMM service; takes the
+    following arguments:
+        url     {String}    The URL from which to obtain the XML feed
+    Returns:
+                {List}, {List}, {List}
+    '''
     footprinturl = url
     fileobject = urllib2.urlopen(footprinturl)
     tree = etree.parse(fileobject)

File src/djangosite/settings.py

 # Django settings for michiganview rtmm project.
 
+import sys
 from ConfigParser import RawConfigParser
 config = RawConfigParser()
 config.read('/etc/rtmm/settings.ini')
 
+sys.path.append('/usr/local/project/rtmm/src')
+sys.path.append('/usr/local/project/rtmm/src/djangosite')
 
+DEBUG = False #config.getboolean('debug','DEBUG')
+TEMPLATE_DEBUG = False #config.getboolean('debug','TEMPLATE_DEBUG')
 
-DEBUG = config.getboolean('debug','DEBUG')
-TEMPLATE_DEBUG = config.getboolean('debug','TEMPLATE_DEBUG')
+EARTH_OBSERVING_SATS = [
+    'AQUA',
+    'TERRA',
+    'LANDSAT_5',
+    'LANDSAT_7',
+    'ENVISAT',
+    'RADARSAT',
+    'TRMM'
+    ]
 
 ADMINS = (
     tuple(config.items('error mail'))
-)
+    )
 
 MANAGERS = tuple(config.items('404 mail'))
 
 DATABASES = {
     'default': {
-        'ENGINE':   'django.contrib.gis.db.backends.postgis', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
-        'NAME':     config.get('database', 'DATABASE_NAME'),                      # Or path to database file if using sqlite3.
-        'USER':     config.get('database', 'DATABASE_USER'),                      # Not used with sqlite3.
-        'PASSWORD': config.get('database', 'DATABASE_PASSWORD'),                  # Not used with sqlite3.
-        'HOST':     config.get('database', 'DATABASE_HOST'),                      # Set to empty string for localhost. Not used with sqlite3.
-        'PORT':     config.get('database', 'DATABASE_PORT'),                      # Set to empty string for default. Not used with sqlite3.
+        'ENGINE':   'django.contrib.gis.db.backends.postgis', 
+        'NAME':     config.get('database', 'DATABASE_NAME'),
+        'USER':     config.get('database', 'DATABASE_USER'),
+        'PASSWORD': config.get('database', 'DATABASE_PASSWORD'),
+        'HOST':     config.get('database', 'DATABASE_HOST'),
+        'PORT':     config.get('database', 'DATABASE_PORT'),
+        }
     }
-}
 
 # Local time zone for this installation. Choices can be found here:
 # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
     # Put strings here, like "/home/html/static" or "C:/www/django/static".
     # Always use forward slashes, even on Windows.
     # Don't forget to use absolute paths, not relative paths.
-)
+    )
 
 # List of finder classes that know how to find static files in
 # various locations.
 STATICFILES_FINDERS = (
     'django.contrib.staticfiles.finders.FileSystemFinder',
     'django.contrib.staticfiles.finders.AppDirectoriesFinder',
-#    'django.contrib.staticfiles.finders.DefaultStorageFinder',
-)
+#   'django.contrib.staticfiles.finders.DefaultStorageFinder',
+    )
 
 # Make this unique, and don't share it with anybody.
 SECRET_KEY = '+ffgb!)&9ky0byjpjxb6zd79%_cxu)#)7y%p5r767)@&mdc1y%'
 TEMPLATE_LOADERS = (
     'django.template.loaders.filesystem.Loader',
     'django.template.loaders.app_directories.Loader',
-#     'django.template.loaders.eggs.Loader',
-)
+#   'django.template.loaders.eggs.Loader',
+    )
 
 MIDDLEWARE_CLASSES = (
     'django.middleware.common.CommonMiddleware',
     'django.middleware.csrf.CsrfViewMiddleware',
     'django.contrib.auth.middleware.AuthenticationMiddleware',
     'django.contrib.messages.middleware.MessageMiddleware',
-)
+    )
 
 ROOT_URLCONF = 'djangosite.urls'
 
     # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
     # Always use forward slashes, even on Windows.
     # Don't forget to use absolute paths, not relative paths.
-)
+    '/usr/local/project/rtmm/src/djangosite/templates/'
+    )
 
 INSTALLED_APPS = (
     'django.contrib.auth',
     # Uncomment the next line to enable admin documentation:
     # 'django.contrib.admindocs',
     'main',
-)
-
-# A sample logging configuration. The only tangible logging
-# performed by this configuration is to send an email to
-# the site admins on every HTTP 500 error.
-# See http://docs.djangoproject.com/en/dev/topics/logging for
-# more details on how to customize your logging configuration.
-LOGGING = {
-    'version': 1,
-    'disable_existing_loggers': False,
-    'handlers': {
-        'mail_admins': {
-            'level': 'ERROR',
-            'class': 'django.utils.log.AdminEmailHandler'
-        }
-    },
-    'loggers': {
-        'django.request': {
-            'handlers': ['mail_admins'],
-            'level': 'ERROR',
-            'propagate': True,
-        },
-    }
-}
+    )

File src/djangosite/settings_production.py

+# Django settings for michiganview rtmm project.
+
+from settings import *
+from ConfigParser import RawConfigParser
+config = RawConfigParser()
+config.read('/etc/rtmm/settings.ini')
+
+DEBUG = False #config.getboolean('debug','DEBUG')
+TEMPLATE_DEBUG = config.getboolean('debug','TEMPLATE_DEBUG')
+
+APPEND_SLASH = True
+
+ADMINS = (
+    tuple(config.items('error mail'))
+    )
+
+MANAGERS = tuple(config.items('404 mail'))
+
+DATABASES = {
+    'default': {
+        'ENGINE':   'django.contrib.gis.db.backends.postgis', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
+        'NAME':     config.get('database', 'DATABASE_NAME'),                      # Or path to database file if using sqlite3.
+        'USER':     config.get('database', 'DATABASE_USER'),                      # Not used with sqlite3.
+        'PASSWORD': config.get('database', 'DATABASE_PASSWORD'),                  # Not used with sqlite3.
+        'HOST':     config.get('database', 'DATABASE_HOST'),                      # Set to empty string for localhost. Not used with sqlite3.
+        'PORT':     config.get('database', 'DATABASE_PORT'),                      # Set to empty string for default. Not used with sqlite3.
+        }
+    }

File src/djangosite/templates/500.html

+<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
+<html><head>
+<meta http-equiv="content-type" content="text/html; charset=ISO-8859-1">
+
+<title>500 Internal Server Error</title>
+</head><body>
+<h1>Internal Server Error</h1>
+<p>The server encountered an internal error or
+misconfiguration and was unable to complete
+your request.</p>
+<p>Please contact the server administrator,
+ kaendsle@mtu.edu and inform him of the time the error occurred,
+and anything you might have done that may have
+caused the error.</p>
+<p>More information about this error may be available
+in the server error log.</p>
+<hr>
+<address>Apache/2.2.14 (Ubuntu) Server at 127.0.0.1 Port 80</address>
+</body></html>

File src/djangosite/templates/default.html

+<h1>Default page</h1>
+
+<ul>
+<li>
+<a href={% url mainapp.views.display_nadir_list %}>
+Display List of Satellites with Nadir tracks
+</a>
+</li>
+<li>
+<a href={% url mainapp.views.display_nadir offering_id='AQUA',format='html' %}>
+Display Nadir Locations (AQUA) (HTML format)
+</a>
+</li>
+</ul>

File src/djangosite/templates/default.kml

+{{ content|safe }}

File src/djangosite/urls.py

 from django.conf.urls.defaults import patterns, include, url
+from django.contrib import admin
 
-# Uncomment the next two lines to enable the admin:
-from django.contrib import admin
 admin.autodiscover()
 
 urlpatterns = patterns('',
-    # Examples:
-    # url(r'^$', 'djangosite.views.home', name='home'),
-    # url(r'^djangosite/', include('djangosite.foo.urls')),
-
     # Uncomment the admin/doc line below to enable admin documentation:
     # url(r'^admin/doc/', include('django.contrib.admindocs.urls')),
 
-    # Uncomment the next line to enable the admin:
-    url(r'^admin/', include(admin.site.urls)),
-    url(r'^api/',include('djangosite.api.urls')),
-    url(r'^test/','djangosite.api.views.apachetest')
-)
+    url(r'^admin/',     include(admin.site.urls)),
+    url(r'^api/',       include('djangosite.api.urls')),
+    url(r'^test/',  'djangosite.api.views.apache_test'),
+
+    # /rtmm/nadir/*
+    url(r'^nadir/(?P<offering_id>\w+).kml$', # Example: /nadir/AQUA.html
+        'main.views.real_time_nadir'), # Target for NetworkLink
+
+    # /rtmm/nadir/link/*
+    url(r'^nadir/link/(?P<offering_id>\w+).kml$', # Example: /nadir/link/AQUA.kml
+        'main.views.real_time_nadir_link'), # NetworkLink KML
+
+    )

File src/djangosite/utils.py

+from lxml import etree
+from pykml.factory import KML_ElementMaker as KML
+from djangosite.main.models import Satellite, SatellitePosition, Sensor
+import urllib2
+import datetime
+
+
+
+class UTC(datetime.tzinfo):
+    '''UTC'''
+    def utcoffset(self, dt):
+        return datetime.timedelta(0)
+    def tzname(self, dt):
+        return 'UTC'
+    def dst(self, dt):
+        return datetime.timedelta(0)
+
+
+
+def nadir_document(offering, start_time, attrs):
+    '''
+    Constructs a KML Document in a consistent fashion and returns it as a
+    pyKML object; takes the following arguments:
+        offering    {String}                The name of the satellite
+        start_time  {datetime.datetime}     The start of the RTMM observation interval
+        attrs       {Dictionary}            
+    '''
+    offering = offering.upper() # For some satellite names, this is much prettier than the alternative
+
+    if 'altitude' not in attrs['lookat'].keys():
+        attrs['lookat']['altitude'] = 705000 # Defaults to 705 km which is common
+
+    kml_doc = KML.Document(
+        KML.name(' '.join((offering, 'at', start_time.strftime('%Y-%m-%d %H:%M:%S')))),
+        KML.description(' '.join(('The position of the', offering, 'satellite at', start_time.strftime('%c')))),
+        KML.LookAt(
+            KML.longitude(attrs['lookat']['longitude']),
+            KML.latitude(attrs['lookat']['latitude']),
+            KML.altitude(attrs['lookat']['altitude']),
+            KML.heading(90),
+            KML.tilt(50),
+            KML.range(1000000), # For displaying satellite track correctly; derived empirically
+            KML.altitudeMode('absolute')
+            )
+        )
+
+    return kml_doc
+
+
+
+def nadir_model_placemark(offering, attrs):
+    '''
+    Constructs a KML Placemark for a Model in a consistent fashion and returns
+    it as a pyKML object; takes the following arguments:
+        offering    {String}        Name of the satellite
+        attrs       {Dictionary}    
+    '''
+    model_name = offering.upper()
+
+    if offering.upper() not in ['AQUA', 'TERRA', 'LANDSAT_7']:
+        model_name = 'GENERIC'      
+
+    if 'altitude' not in attrs['multigeometry']['model'].keys():
+        attrs['multigeometry']['model']['altitude'] = 705000 # Defaults to 705 km which is common
+
+    lng = attrs['multigeometry']['model']['longitude']
+    lat = attrs['multigeometry']['model']['latitude']
+    alt = attrs['multigeometry']['model']['altitude']
+    coords = ','.join((str(lng), str(lat), str(alt)))
+
+    placemark = KML.Placemark(
+        KML.name(''),
+        KML.Style(
+            KML.IconStyle(
+                # Icon cannot be completely transparent as this will make the extruded line disappear;
+                KML.color('01ffffff'), # Alpha value is aa in color('aabbggrr')
+                KML.colorMode('normal'),
+                ),
+            KML.LineStyle(
+                KML.color('ffffffff'),
+                KML.colorMode('normal'),
+                ),
+            ),
+        KML.MultiGeometry(
+            # Point geometry is necessary for an extruded line
+            KML.Point(
+                KML.extrude(1),
+                KML.altitudeMode('absolute'),
+                KML.coordinates(coords),
+                ),
+            KML.Model(
+                KML.altitudeMode('absolute'),
+                KML.Location(
+                    KML.longitude(lng),
+                    KML.latitude(lat),
+                    KML.altitude(alt),
+                    ),
+                KML.Orientation(
+                    KML.heading(135),
+                    KML.tilt(0),
+                    KML.roll(0),
+                    ),
+                KML.Scale(
+                    KML.x(10000),
+                    KML.y(10000),
+                    KML.z(10000),
+                    ),
+                KML.Link(
+                    KML.href('http://geodjango.mtri.org/static/project/michiganview/models/' + model_name + '.dae'),
+                    )
+                )
+            )
+        )
+
+    return placemark