class Place(AbstractNode, MP_Node): """ Represents a Place with name, used for geocoding https://developers.arcgis.com/rest/geocode/api-reference/geocoding-category-filtering.htm Extending treebeard MP_Node provides the hierarchy """ verified = models.BooleanField(default=False) # true if a user has verified this place, ie by editing it manually extras = ExtrasDotField(default='') # a dictionary of name/value pairs to support flexible extra metadata geometry = ExtrasDotField(default='') # a dictionary of name/value pairs to support geometry node_order_by = ['name'] # for treebeard ordering
class AbstractOperation(models.Model): """Represents an area where a team is operating. Could be a regular station posting, an incident, an exercise, or whatever makes sense. For a discussion of incident file naming conventions see http://gis.nwcg.gov/2008_GISS_Resource/student_workbook/unit_lessons/Unit_08_File_Naming_Review.pdf""" folders = models.ManyToManyField(Folder, related_name='%(app_label)s_%(class)s_folders') name = models.CharField(max_length=32, blank=True, help_text="A descriptive name for this operation. Example: 'beaver_pond'.") operationId = models.CharField(max_length=32, blank=True, verbose_name='operation id', help_text="A formal id for this operation. For incidents, use the incident number. Example: 'PA-DEWA-0001'") minTime = models.DateTimeField(blank=True, null=True, verbose_name='start date') maxTime = models.DateTimeField(blank=True, null=True, verbose_name='end date') minLat = models.FloatField(blank=True, null=True, verbose_name='minimum latitude') # WGS84 degrees minLon = models.FloatField(blank=True, null=True, verbose_name='minimum longitude') # WGS84 degrees maxLat = models.FloatField(blank=True, null=True, verbose_name='maximum latitude') # WGS84 degrees maxLon = models.FloatField(blank=True, null=True, verbose_name='maximum longitude') # WGS84 degrees notes = models.TextField(blank=True) tags = TagField(blank=True) uuid = UuidField() extras = ExtrasDotField(help_text="A place to add extra fields if we need them but for some reason can't modify the table schema. Expressed as a JSON-encoded dict.") objects = AbstractModelManager(parentModel=None) class Meta: abstract = True def __unicode__(self): return '%s %s %s' % (self.__class__.__name__, self.name, self.operationId)
class MapLayer(AbstractMap): """ A map layer which will have a collection of features that have content in them. """ jsonFeatures = ExtrasDotField() defaultColor = models.CharField(max_length=32, null=True, blank=True) def getEditHref(self): return reverse('mapEditLayer', kwargs={'layerID': self.uuid}) def toDict(self): result = modelToDict(self) result['uuid'] = self.uuid return result def getTreeJson(self): """ Get the json block that the fancy tree needs to render this node """ result = super(MapLayer, self).getTreeJson() result["data"]["layerJSON"] = reverse('mapLayerJSON', kwargs={'layerID': self.uuid}) return result def getGoogleEarthUrl(self, request): return request.build_absolute_uri( reverse('mapLayerKML', kwargs={'layerID': self.uuid})) def getFeatureJson(self): return self.jsonFeatures
class Assignment(models.Model): operation = models.ForeignKey(Operation) userProfile = models.ForeignKey('UserProfile') organization = models.CharField(max_length=64, blank=True, help_text="The organization or unit you are assigned to for this operation.") jobTitle = models.CharField(max_length=64, blank=True, help_text="Your job title for this operation.") contactInfo = models.CharField(max_length=128, blank=True, help_text="Your contact info for this operation. Cell phone number is most important.") uuid = UuidField() extras = ExtrasDotField(help_text="A place to add extra fields if we need them but for some reason can't modify the table schema. Expressed as a JSON-encoded dict.")
class IconStyle(UuidModel): name = models.CharField(max_length=40, blank=True) url = models.CharField(max_length=1024, blank=True) width = models.PositiveIntegerField(default=0) height = models.PositiveIntegerField(default=0) scale = models.FloatField(default=1) color = models.CharField( max_length=16, blank=True, help_text='Optional KML color specification, hex in AABBGGRR order') extras = ExtrasDotField() def __unicode__(self): return '%s %s' % (self.__class__.__name__, self.name) def writeKml(self, out, heading=None, urlFn=None, color=None): if not color: color = self.color try: int(self.color) except: try: color = self.color() except: color = self.color if color: colorStr = '<color>%s</color>' % color else: colorStr = '' if self.scale != 1: scaleStr = '<scale>%s</scale>' % self.scale else: scaleStr = '' if heading is not None: headingStr = '<heading>%s</heading>' % heading else: headingStr = '' imgUrl = self.url if urlFn: imgUrl = urlFn(imgUrl) out.write(""" <IconStyle> %(headingStr)s %(colorStr)s %(scaleStr)s <Icon> <href>%(url)s</href> </Icon> </IconStyle> """ % dict(url=imgUrl, scaleStr=scaleStr, colorStr=colorStr, headingStr=headingStr, id=self.pk))
class AutomatchResults(models.Model): issMRF = models.CharField(max_length=255, unique=True, help_text="Please use the following format: <em>[Mission ID]-[Roll]-[Frame number]</em>") matchedImageId = models.CharField(max_length=255, blank=True) matchConfidence = models.CharField(max_length=255, blank=True) matchDate = models.DateTimeField(null=True, blank=True) capturedTime = models.DateTimeField(null=True, blank=True) centerPointSource = models.CharField(max_length=255, blank=True, help_text="source of center point. Either curated, CEO, GeoSens, or Nadir") centerLat = models.FloatField(null=True, blank=True, default=0) centerLon = models.FloatField(null=True, blank=True, default=0) registrationMpp = models.FloatField(null=True, blank=True, default=0) extras = ExtrasDotField() # stores tie point pairs metadataExportName = models.CharField(max_length=255, null=True, blank=True) metadataExport = models.FileField(upload_to=getNewExportFileName, max_length=255, null=True, blank=True) writtenToFile = models.BooleanField(default=False)
class MapLayer(AbstractMap): """ A map layer which will have a collection of features that have content in them. """ jsonFeatures = ExtrasDotField() defaultColor = models.CharField(max_length=32, null=True, blank=True) def getEditHref(self): return reverse('mapEditLayer', kwargs={'layerID': self.uuid}) def toDict(self): result = modelToDict(self) result['uuid'] = self.uuid return result # A bit hacky, but... toMapDict() returns metadata info on the layer object so getMappedObjectsJson() can be used # to return a summary of the available layers if you pass it (e.g.) xgds_map_server.MapLayer as a param. def toMapDict(self): result = {"maxLat": self.maxLat, "minLat": self.minLat, "maxLon": self.maxLon, "minLon": self.minLon, "parent": self.parent.uuid, "creator":self.creator, "defaultColor": self.defaultColor, "description": self.description, "creation_time": self.creation_time, "uuid": self.uuid, "visible": self.visible, "modification_time": self.modification_time, "region": self.region, "name": self.name} return result def get_tree_json(self): """ Get the json block that the fancy tree needs to render this node """ result = super(MapLayer, self).get_tree_json() result["data"]["layerJSON"] = reverse('mapLayerJSON', kwargs={'layerID': self.uuid}) return result def getGoogleEarthUrl(self, request): theUrl = reverse('mapLayerKML', kwargs={'layerID': self.uuid}) theUrl = insertIntoPath(theUrl, 'rest') return request.build_absolute_uri(theUrl) def getFeatureJson(self): return self.jsonFeatures def getKmlUrl(self): """ If this element has an url which returns kml, override this function to return that url. """ return reverse('mapLayerKML', kwargs={'layerID': self.uuid})
class GroupProfile(models.Model): group = models.OneToOneField(Group, help_text='Reference to corresponding Group object of built-in Django authentication system.') context = models.ForeignKey(Context, blank=True, null=True, help_text='Default context associated with this group.') password = models.CharField(max_length=128, blank=True, null=True) uuid = UuidField() extras = ExtrasDotField(help_text="A place to add extra fields if we need them but for some reason can't modify the table schema. Expressed as a JSON-encoded dict.") def password_required(self): return (self.password is None) def set_password(self, raw_password): # Make sure the password field isn't set to none if raw_password is not None: import hashlib import random # Compute the differnt parts we'll need to secure store the password algo = 'sha1' salt = hashlib.sha1(str(random.random())).hexdigest()[:5] hsh = hashlib.sha1(salt + raw_password).hexdigest() # Set the password value self.password = '******' % (algo, salt, hsh) def authenticate(self, user_password): # Make sure the password field isn't set to none if self.password is not None: import hashlib # Get the parts of the group password parts = self.password.split('$') _algo, salt, hsh = parts[:2] # Compute the hash of the user password #user_hsh = get_hexdigest(algo, salt, user_password) user_hash = hashlib.sha1(salt + user_password).hexdigest() # Retrun the resulting comparison return (hsh == user_hash) else: return True
class Context(models.Model): """ A context is a collection of settings that it makes sense to share between members of a group or people on an incident. """ name = models.CharField(max_length=40, help_text="A descriptive name.") uploadFolder = models.ForeignKey(Folder, related_name='%(app_label)s_%(class)s_uploadingContextSet', help_text="Folder to upload data to.") timeZone = models.CharField(max_length=32, choices=TIME_ZONE_CHOICES, default=DEFAULT_TIME_ZONE, help_text="Time zone used to display timestamps and to interpret incoming timestamps that aren't labeled with a time zone.") layerConfigUrl = models.CharField(max_length=256, help_text='URL pointing to a JSON config file specifying what layers to show') uuid = UuidField() extras = ExtrasDotField(help_text="A place to add extra fields if we need them but for some reason can't modify the table schema. Expressed as a JSON-encoded dict.")
class Sensor(models.Model): name = models.CharField(max_length=40, blank=True, help_text='Your name for the instrument. Example: "MicroImager" or "GeoCam"') make = models.CharField(max_length=40, blank=True, help_text='The organization that makes the sensor. Example: "Canon"') model = models.CharField(max_length=40, blank=True, help_text='The model of sensor. Example: "Droid" or "PowerShot G9"') software = models.CharField(max_length=160, blank=True, help_text='Software running on the sensor, including any known firmware and version details. Example: "GeoCam Mobile 1.0.10, Android firmware 2.1-update1 build ESE81"') serialNumber = models.CharField(max_length=80, blank=True, verbose_name='serial number', help_text='Information that uniquely identifies this particular sensor unit. Example: "serialNumber:HT851N002808 phoneNumber:4126573579" ') notes = models.TextField(blank=True) tags = TagField(blank=True) uuid = UuidField() extras = ExtrasDotField(help_text="A place to add extra fields if we need them but for some reason can't modify the table schema. Expressed as a JSON-encoded dict.") def __unicode__(self): return self.name
class AbstractUserProfile(models.Model): """ Adds some extended fields to the django built-in User type. """ user = models.OneToOneField(User, help_text='Reference to corresponding User object of built-in Django authentication system.', related_name='%(app_label)s_%(class)s') displayName = models.CharField(max_length=40, blank=True, help_text="The 'uploaded by' name that will appear next to data you upload. Defaults to 'F. Last', but if other members of your unit use your account you might want to show your unit name instead.") homeOrganization = models.CharField(max_length=64, blank=True, help_text="The home organization you usually work for.") homeJobTitle = models.CharField(max_length=64, blank=True, help_text="Your job title in your home organization.") contactInfo = models.CharField(max_length=128, blank=True, help_text="Your contact info in your home organization.") uuid = UuidField() extras = ExtrasDotField(help_text="A place to add extra fields if we need them but for some reason can't modify the table schema. Expressed as a JSON-encoded dict.") class Meta: ordering = ['user'] abstract = True def __unicode__(self): return u'<User %s "%s %s">' % (self.user.username, self.user.first_name, self.user.last_name)
class LineStyle(UuidModel): name = models.CharField(max_length=40, blank=True) color = models.CharField( max_length=16, blank=True, help_text='Optional KML color specification, hex in AABBGGRR order') width = models.PositiveIntegerField(default=1, null=True, blank=True) extras = ExtrasDotField() def __unicode__(self): return '%s %s' % (self.__class__.__name__, self.name) def writeKml(self, out, urlFn=None, color=None): if not color: color = self.color if color: colorStr = '<color>%s</color>' % color else: colorStr = '' if self.width is not None: widthStr = '<width>%s</width>' % self.width else: widthStr = '' out.write(""" <LineStyle> %(colorStr)s %(widthStr)s </LineStyle> """ % dict(colorStr=colorStr, widthStr=widthStr)) def getAlpha(self): """ Get 0-1 alpha value from color """ if self.color: decvalue = int("0x" + self.color[0:2], 16) return decvalue / 255 return 1.0 def getHexColor(self): if self.color: return self.color[2:] return None
class Feature(models.Model): folders = models.ManyToManyField(Folder, related_name='%(app_label)s_%(class)s_set', blank=True) name = models.CharField(max_length=80, blank=True, default='') author = models.ForeignKey(User, null=True, related_name='%(app_label)s_%(class)s_authoredSet', help_text='The user who collected the data (when you upload data, Share tags you as the author)') sensor = models.ForeignKey(Sensor, blank=True, null=True, related_name='%(app_label)s_%(class)s_set') isAerial = models.BooleanField(default=False, blank=True, verbose_name='aerial data', help_text="True for aerial data. Generally for non-aerial data we snap to terrain in 3D visualizations so that GPS errors can't cause features to be rendered underground.") notes = models.TextField(blank=True) tags = TagField(blank=True) icon = models.CharField(max_length=16, blank=True) # these fields help us handle changes to data products status = models.CharField(max_length=1, choices=STATUS_CHOICES, default=STATUS_CHOICES[0][0]) processed = models.BooleanField(default=False) version = models.PositiveIntegerField(default=0) purgeTime = models.DateTimeField(null=True, blank=True) workflowStatus = models.PositiveIntegerField(choices=WORKFLOW_STATUS_CHOICES, default=DEFAULT_WORKFLOW_STATUS) mtime = models.DateTimeField(null=True, blank=True) uuid = UuidField() extras = ExtrasDotField(help_text="A place to add extra fields if we need them but for some reason can't modify the table schema. Expressed as a JSON-encoded dict.") objects = AbstractModelManager(parentModel=None) viewerExtension = None # override in derived classes class Meta: abstract = True def save(self, **kwargs): self.mtime = datetime.datetime.now() super(Feature, self).save(**kwargs) def deleteFiles(self): shutil.rmtree(self.getDir(), ignore_errors=True) def getCachedField(self, field): relatedId = getattr(self, '%s_id' % field) key = 'fieldCache-geocamCore-Feature-%s-%d' % (field, relatedId) result = cache.get(key) if not result: result = getattr(self, field) cache.set(key, result) return result def getCachedFolder(self): return self.getCachedField('folder') def getCachedAuthor(self): return self.getCachedField('author') def utcToLocalTime(self, dtUtc0): dtUtc = pytz.utc.localize(dtUtc0) # TODO: respect user's context time zone localTz = pytz.timezone(settings.TIME_ZONE) dtLocal = dtUtc.astimezone(localTz) return dtLocal def getAuthor(self): pass def __unicode__(self): return '%s %d %s %s %s' % (self.__class__.__name__, self.id, self.name or '[untitled]', self.author.username, self.uuid) def getDirSuffix(self, version=None): if version is None: version = self.version idStr = str(self.id) + 'p' idList = [idStr[i:(i + 2)] for i in xrange(0, len(idStr), 2)] return [self.__class__.__name__.lower()] + idList + [str(version)] def getDir(self, version=None): return os.path.join(settings.DATA_DIR, *self.getDirSuffix(version)) def getIconDict(self, kind=''): return dict(url=getIconUrl(self.icon + kind), size=getIconSize(self.icon + kind)) def getStyledIconDict(self, kind='', suffix=''): return dict(normal=self.getIconDict(kind + suffix), highlighted=self.getIconDict(kind + 'Highlighted' + suffix)) def getUserDisplayName(self, user): if user.last_name == 'group': return user.first_name else: return '%s %s' % (user.first_name.capitalize(), user.last_name.capitalize()) def getViewerUrl(self): name = self.name or 'untitled' + self.viewerExtension return ('%s%s/%s/%s/%s' % (settings.SCRIPT_NAME, self.__class__._meta.app_label, self.__class__.__name__.lower(), self.id, name)) def getEditUrl(self): return ('%s%s/%s/%s/%s/' % (settings.SCRIPT_NAME, self.__class__._meta.app_label, 'editWidget', self.__class__.__name__.lower(), self.id)) def getProperties(self): tagsList = tagging.utils.parse_tag_input(self.tags) author = self.getCachedAuthor() authorDict = dict(userName=author.username, displayName=self.getUserDisplayName(author)) return dict(name=self.name, version=self.version, isAerial=self.isAerial, author=authorDict, notes=self.notes, tags=tagsList, icon=self.getStyledIconDict(), localId=self.id, subtype=self.__class__.__name__, viewerUrl=self.getViewerUrl(), editUrl=self.getEditUrl(), ) def cleanDict(self, d): return dict(((k, v) for k, v in d.iteritems() if v not in (None, ''))) def getGeometry(self): return None # override in derived classes def getGeoJson(self): return dict(type='Feature', id=self.uuid, geometry=self.getGeometry(), properties=self.cleanDict(self.getProperties())) def getDirUrl(self): return '/'.join([settings.DATA_URL] + list(self.getDirSuffix()))
class AbstractTrack(SearchableModel, UuidModel, HasVehicle, HasFlight): """ This is for an abstract track with a FIXED vehicle model, ie all the tracks have like vehicles. """ name = models.CharField(max_length=40, blank=True) iconStyle = 'set this to DEFAULT_ICON_STYLE_FIELD() or similar in derived classes' lineStyle = 'set this to DEFAULT_LINE_STYLE_FIELD() or similar in derived classes' extras = ExtrasDotField() def __init__(self, *args, **kwargs): self.coordGroups = [] self.timesGroups = [] super(AbstractTrack, self).__init__(*args, **kwargs) if self.id: self.pastposition_set = PAST_POSITION_MODEL.get().objects.filter( track_id=self.id) class Meta: abstract = True ordering = ('name', ) def __unicode__(self): return '%s %s' % (self.__class__.__name__, self.name) def getTimezone(self): """ Override if your model has a different way of getting time zones. It would be nifty to look them up from lat/lon """ return pytz.timezone(settings.TIME_ZONE) def getPositions(self, downsample=False): result = PAST_POSITION_MODEL.get().objects.filter(track=self) if downsample: result = downsample_queryset( result, settings.GEOCAM_TRACK_DOWNSAMPLE_POSITIONS_SECONDS) return result def getCurrentPositions(self): return POSITION_MODEL.get().objects.filter(track=self) def getLabelName(self, pos): return self.name def getLabelExtra(self, pos): return '' @property def lat(self): # This is a total hack to get tracks to show on the map after they were searched. return 1 def get_tree_json(self): vehicle_name = "" if hasattr(self, 'vehicle') and self.vehicle: vehicle_name = self.vehicle.name result = { "title": self.name, "key": self.uuid, "tooltip": "%s for %s" % (settings.GEOCAM_TRACK_TRACK_MONIKER, self.name), "data": { "json": reverse('geocamTrack_mapJsonTrack_downsample', kwargs={'uuid': str(self.uuid)}), "kmlFile": reverse('geocamTrack_trackKml', kwargs={'trackName': self.name}), "sseUrl": "", "type": 'MapLink', "vehicle": vehicle_name } } return result def getIconStyle(self, pos): if hasattr(self, '_currentIcon'): return self._currentIcon # use specific style if given if self.iconStyle is not None: self._currentIcon = self.iconStyle return self.iconStyle # use pointer icon if we know the heading if pos.heading is not None: if not hasattr(self, '_pointerIcon'): self._pointerIcon = IconStyle.objects.get(name='pointer') self._currentIcon = self._pointerIcon return self._pointerIcon # use spot icon otherwise if not hasattr(self, '_defaultIcon'): self._defaultIcon = IconStyle.objects.get(name='default') self._defaultIcon.color = self.getLineStyleColor self._currentIcon = self._defaultIcon return self._currentIcon def getLineStyle(self): if self.lineStyle: return self.lineStyle return LineStyle.objects.get(name='default') def getLineStyleColor(self): if self.lineStyle: return self.lineStyle.color return LineStyle.objects.get(name='default').color def getIconColor(self, pos): try: currentIconStyle = self.getIconStyle(pos) int(str(currentIconStyle.color)) return currentIconStyle.color except: try: return currentIconStyle.color() except: return str(currentIconStyle.color) def getLineColor(self): return self.getLineStyle().color def getKmlUrl(self, **kwargs): kwargs['trackName'] = self.name return getKmlUrl(**kwargs) def writeCurrentKml(self, out, pos, iconStyle=None, urlFn=None): if iconStyle is None: iconStyle = self.getIconStyle(pos) ageStr = '' if settings.GEOCAM_TRACK_SHOW_CURRENT_POSITION_AGE: now = datetime.datetime.now(pytz.utc) diff = now - pos.timestamp diffSecs = diff.days * 24 * 60 * 60 + diff.seconds if diffSecs >= settings.GEOCAM_TRACK_CURRENT_POSITION_AGE_MIN_SECONDS: age = TimeUtil.getTimeShort(pos.timestamp) ageStr = ' (%s)' % age ageStr += ' %s' % getTimeSpinner(datetime.datetime.now(pytz.utc)) label = ('%s%s%s' % (self.getLabelName(pos), self.getLabelExtra(pos), ageStr)) out.write(""" <Placemark> <name>%(label)s</name> """ % dict(label=label)) if iconStyle: out.write("<Style>\n") iconStyle.writeKml(out, pos.getHeading(), urlFn=urlFn, color=self.getIconColor(pos)) out.write("</Style>\n") out.write(""" <Point> <coordinates> """) pos.writeCoordinatesKml(out) out.write(""" </coordinates> </Point> </Placemark> """) def writeCompassKml(self, out, pos, urlFn=None): pngUrl = settings.STATIC_URL + 'geocamTrack/icons/compassRoseLg.png' if urlFn: pngUrl = urlFn(pngUrl) out.write(""" <Placemark> <Style> <IconStyle> <scale>6.0</scale> <Icon> <href>%(pngUrl)s</href> </Icon> </IconStyle> </Style> <Point> <coordinates> """ % {'pngUrl': pngUrl}) pos.writeCoordinatesKml(out) out.write(""" </coordinates> </Point> </Placemark> """) def writeAnimatedPlacemarks(self, out, positions): out.write(" <Folder>\n") out.write(" <name>Trajectory</name>\n") out.write(" <open>0</open>\n") # out.write(' <visibility>1</visibility>\n') numPositions = len(positions) - 1 for i, pos in enumerate(positions): # start new line string out.write(" <Placemark>\n") out.write(" <TimeSpan>\n") begin = pytz.utc.localize(pos.timestamp).astimezone( self.getTimezone()) tzoffset = begin.strftime('%z') tzoffset = tzoffset[0:-2] + ":00" out.write( " <begin>%04d-%02d-%02dT%02d:%02d:%02d%s</begin>\n" % (begin.year, begin.month, begin.day, begin.hour, begin.minute, begin.second, tzoffset)) if i < numPositions: nextpos = positions[i + 1] end = pytz.utc.localize(nextpos.timestamp).astimezone( self.getTimezone()) # end = self.getTimezone().localize(nextpos.timestamp) else: end = begin out.write( " <end>%04d-%02d-%02dT%02d:%02d:%02d%s</end>\n" % (end.year, end.month, end.day, end.hour, end.minute, end.second, tzoffset)) out.write(" </TimeSpan>\n") # out.write(" <styleUrl>#dw%d</styleUrl>\n" % (pos.heading)) out.write(" <styleUrl>#%s</styleUrl>\n" % self.getIconStyle(pos).pk) out.write( " <gx:balloonVisibility>1</gx:balloonVisibility>\n") out.write(" <Point>\n") out.write(" <coordinates>") pos.writeCoordinatesKml(out) out.write(" </coordinates>\n") out.write(" </Point>\n") out.write(" </Placemark>\n") out.write(" </Folder>\n") def writeTrackKml(self, out, positions=None, lineStyle=None, urlFn=None, animated=False): if positions is None: positions = self.getPositions() if lineStyle is None: lineStyle = self.lineStyle n = positions.count() if n == 0: return if n < 2: # kml LineString requires 2 or more positions return out.write("<Folder>\n") out.write(""" <Placemark> <name>%(name)s path</name> """ % dict(name=self.name)) if lineStyle: out.write("<Style>") lineStyle.writeKml(out, urlFn=urlFn, color=self.getLineColor()) out.write("</Style>") if animated: if self.iconStyle: out.write("<Style id=\"%s\">\n" % self.iconStyle.pk) self.iconStyle.writeKml(out, 0, urlFn=urlFn, color=self.getLineColor()) out.write("</Style>\n") out.write(""" <MultiGeometry> <LineString> <tessellate>1</tessellate> <coordinates> """) lastPos = None breakDist = settings.GEOCAM_TRACK_START_NEW_LINE_DISTANCE_METERS for pos in positions: if lastPos and breakDist is not None: diff = geomath.calculateDiffMeters( [lastPos.longitude, lastPos.latitude], [pos.longitude, pos.latitude]) dist = geomath.getLength(diff) if dist > breakDist: # start new line string out.write(""" </coordinates> </LineString> <LineString> <tessellate>1</tessellate> <coordinates> """) pos.writeCoordinatesKml(out) lastPos = pos out.write(""" </coordinates> </LineString> </MultiGeometry> </Placemark> """) if animated: self.writeAnimatedPlacemarks(out, list(positions)) out.write("</Folder>\n") def getInterpolatedPosition(self, utcDt): positions = PAST_POSITION_MODEL.get().objects.filter(track=self) # get closest position after utcDt afterPositions = positions.filter( timestamp__gte=utcDt).order_by('timestamp') if afterPositions.count(): afterPos = afterPositions[0] else: return None afterDelta = timeDeltaTotalSeconds(afterPos.timestamp - utcDt) # special case -- if we have a position exactly matching utcDt if afterPos.timestamp == utcDt: return POSITION_MODEL.get().getInterpolatedPosition( utcDt, 1, afterPos, 0, afterPos) # get closest position before utcDt beforePositions = positions.filter( timestamp__lt=utcDt).order_by('-timestamp') if beforePositions.count(): beforePos = beforePositions[0] else: return None beforeDelta = timeDeltaTotalSeconds(utcDt - beforePos.timestamp) delta = beforeDelta + afterDelta if delta > settings.GEOCAM_TRACK_INTERPOLATE_MAX_SECONDS: return None # interpolate beforeWeight = afterDelta / delta afterWeight = beforeDelta / delta return POSITION_MODEL.get().getInterpolatedPosition( utcDt, beforeWeight, beforePos, afterWeight, afterPos) @classmethod def cls_type(cls): return settings.GEOCAM_TRACK_TRACK_MONIKER @property def color(self): color = self.getLineStyle().getHexColor() if color: return color return None @property def alpha(self): return self.getLineStyle().getAlpha() @property def icon_url(self): if self.iconStyle: return self.iconStyle.url return None @property def icon_color(self): if self.iconStyle: return self.iconStyle.color return None @property def icon_scale(self): if self.iconStyle: return self.iconStyle.scale return 1 def buildTimeCoords(self, downsample=False): currentPositions = self.getPositions(downsample) if currentPositions.count() < 2: return if self.coordGroups: return coords = [] times = [] lastPos = None breakDist = settings.GEOCAM_TRACK_START_NEW_LINE_DISTANCE_METERS for pos in currentPositions: if lastPos and breakDist is not None: diff = geomath.calculateDiffMeters( [lastPos.longitude, lastPos.latitude], [pos.longitude, pos.latitude]) dist = geomath.getLength(diff) if dist > breakDist: # start new line string if coords: self.coordGroups.append(coords) coords = [] self.timesGroups.append(times) times = [] coords.append(pos.coords_array) times.append(pos.timestamp) lastPos = pos self.coordGroups.append(coords) self.timesGroups.append(times) @property def coords(self): self.buildTimeCoords() if self.coordGroups: return self.coordGroups return None @property def times(self): self.buildTimeCoords() if self.timesGroups: return self.timesGroups return None def toMapDict(self, downsample=False): result = super(AbstractTrack, self).toMapDict() result['coords_array_order'] = PAST_POSITION_MODEL.get( ).coords_array_order() if 'vehicle' in result: if self.vehicle: result['vehicle'] = self.vehicle.name else: del result['vehicle'] self.buildTimeCoords(downsample) if self.timesGroups: result['times'] = self.timesGroups if self.coordGroups: result['coords'] = self.coordGroups return result @classmethod def timesearchField(cls): return None @property def event_time(self): self.buildTimeCoords() if self.timesGroups: return self.timesGroups[0][0] return None @classmethod def getSearchFormFields(cls): return ['name', 'vehicle']
class AbstractPlan(models.Model): uuid = UuidField(unique=True, db_index=True) name = models.CharField(max_length=128, db_index=True) dateModified = models.DateTimeField(db_index=True) creator = models.ForeignKey(User, null=True, blank=True, db_index=True) # the canonical serialization of the plan exchanged with javascript clients jsonPlan = ExtrasDotField() # a place to put an auto-generated summary of the plan summary = models.CharField(max_length=4096) # allow users to mark plans as deleted. remember to use this field! deleted = models.BooleanField(blank=True, default=False) # allow users to mark plans as read only, so when they are opened they cannot be edited readOnly = models.BooleanField(blank=True, default=False) # cache commonly used stats derived from the plan (relatively expensive to calculate) numStations = models.PositiveIntegerField(default=0) numSegments = models.PositiveIntegerField(default=0) numCommands = models.PositiveIntegerField(default=0) lengthMeters = models.FloatField(null=True, blank=True) estimatedDurationSeconds = models.FloatField(null=True, blank=True) stats = ExtrasDotField( ) # a place for richer stats such as numCommandsByType namedURLs = GenericRelation(NamedURL) class Meta: ordering = ['-dateModified'] abstract = True @property def acquisition_time(self): return self.dateModified def get_absolute_url(self): return reverse('planner2_plan_save_json', args=[self.pk, self.name]) def extractFromJson(self, overWriteDateModified=True, overWriteUuid=True, request=None): if overWriteUuid: if not self.uuid: self.uuid = makeUuid() self.jsonPlan.uuid = self.uuid self.jsonPlan.serverId = self.pk if overWriteDateModified: self.jsonPlan.dateModified = (datetime.datetime.now( pytz.utc).replace(microsecond=0).isoformat()) self.jsonPlan.dateModified = self.jsonPlan.dateModified[:-6] + 'Z' self.name = self.jsonPlan.name self.jsonPlan.url = self.get_absolute_url() self.jsonPlan.serverId = self.pk self.dateModified = dateparser( self.jsonPlan.dateModified).replace(tzinfo=pytz.utc) plannerUsers = User.objects.filter(username=self.jsonPlan.creator) if plannerUsers: self.creator = plannerUsers[0] else: self.creator = None # fill in stats try: exporter = statsPlanExporter.StatsPlanExporter() # print ' about to do stats' stats = exporter.exportDbPlan(self, request) for f in ('numStations', 'numSegments', 'numCommands', 'lengthMeters', 'estimatedDurationSeconds'): setattr(self, f, stats[f]) self.stats.numCommandsByType = stats["numCommandsByType"] self.summary = statsPlanExporter.getSummary(stats) except: logging.warning( 'extractFromJson: could not extract stats from plan %s', self.uuid) raise # FIX return self def getSummaryOfCommandsByType(self): return statsPlanExporter.getSummaryOfCommandsByType(self.stats) # TODO test def toXpjson(self): platform = self.jsonPlan['platform'] if platform: planSchema = getPlanSchema(platform[u'name']) return xpjson.loadDocumentFromDict(self.jsonPlan, schema=planSchema.getSchema()) logging.warning( 'toXpjson: could not convert to xpjson, probably no schema %s', self.uuid) raise # FIX def escapedName(self): name = re.sub(r'[^\w]', '', self.name) if name == '': return 'plan' else: if self.jsonPlan and self.jsonPlan.planVersion: return name + "_" + self.jsonPlan.planVersion return name def getExportUrl(self, extension): return reverse('planner2_planExport', kwargs={ 'uuid': self.uuid, 'name': self.escapedName() + extension }) def getExporters(self): import choosePlanExporter # delayed import avoids import loop result = [] for exporterInfo in choosePlanExporter.PLAN_EXPORTERS: info = copy.deepcopy(exporterInfo) info.url = self.getExportUrl(info.extension) result.append(info) return result def getLinks(self): """ The links tab wil be populated with the name, value contents of this dictionary as links, name is the string displayed and link is what will be opened """ result = { "KML": reverse('planner2_planExport', kwargs={ 'uuid': self.uuid, 'name': self.name + '.kml' }) } kwargs = { 'plan_id': self.pk, 'crs': settings.XGDS_PLANNER_CRS_UNITS_DEFAULT } if settings.XGDS_PLANNER_CRS_UNITS_DEFAULT: result["SummaryCRS"] = reverse('plan_bearing_distance_crs', kwargs=kwargs) else: result["Summary"] = reverse('plan_bearing_distance', kwargs=kwargs) for exporter in self.getExporters(): result[exporter.label] = exporter.url return result def getEscapedId(self): if self.jsonPlan and self.jsonPlan.id: result = re.sub(r'[^\w]', '', self.jsonPlan.id) result = re.sub('_PLAN$', '', result) return result else: return None def toMapDict(self): """ Return a reduced dictionary that will be turned to JSON for rendering in a map Here we are just interested in the route plan and not in activities We just include stations """ result = {} result['id'] = self.uuid result['author'] = self.jsonPlan.creator result['name'] = self.jsonPlan.name result['type'] = 'Plan' if self.jsonPlan.notes: result['notes'] = self.jsonPlan.notes else: result['notes'] = '' stations = [] seq = self.jsonPlan.sequence for el in seq: if el.type == "Station": sta = {} sta['id'] = el.id sta['coords'] = el.geometry.coordinates sta['notes'] = '' if hasattr(el, 'notes'): if el.notes: sta['notes'] = el.notes stations.append(sta) result['stations'] = stations return result def get_tree_json(self): result = { "title": self.name, "key": self.uuid, "tooltip": self.jsonPlan.notes, "data": { "type": "PlanLink", # we cheat so this will be 'live' "json": reverse('planner2_mapJsonPlan', kwargs={'uuid': str(self.uuid)}), "kmlFile": reverse('planner2_planExport', kwargs={ 'uuid': str(self.uuid), 'name': self.name + '.kml' }), "href": reverse('planner2_edit', kwargs={'plan_id': str(self.pk)}) } } return result @property def executions(self): return LazyGetModelByName( settings.XGDS_PLANNER_PLAN_EXECUTION_MODEL).get().objects.filter( plan=self) def __unicode__(self): if self.name: return self.name else: return 'Unnamed plan ' + self.uuid
class Overlay(models.Model): key = models.AutoField(primary_key=True, unique=True) # author: user who owns this overlay in the MapFasten system author = models.ForeignKey(User, null=True, blank=True) lastModifiedTime = models.DateTimeField() name = models.CharField(max_length=50) description = models.TextField(blank=True) imageSourceUrl = models.URLField(blank=True, verify_exists=False) imageData = models.ForeignKey(ImageData, null=True, blank=True, related_name='currentOverlays', on_delete=models.SET_NULL) unalignedQuadTree = models.ForeignKey(QuadTree, null=True, blank=True, related_name='unalignedOverlays', on_delete=models.SET_NULL) alignedQuadTree = models.ForeignKey(QuadTree, null=True, blank=True, related_name='alignedOverlays', on_delete=models.SET_NULL) isPublic = models.BooleanField( default=settings.GEOCAM_TIE_POINT_PUBLIC_BY_DEFAULT) coverage = models.CharField( max_length=255, blank=True, verbose_name='Name of region covered by the overlay') # creator: name of person or organization who should get the credit # for producing the overlay creator = models.CharField(max_length=255, blank=True) sourceDate = models.CharField(max_length=255, blank=True, verbose_name='Source image creation date') rights = models.CharField(max_length=255, blank=True, verbose_name='Copyright information') license = models.URLField( verify_exists=False, blank=True, verbose_name='License permitting reuse (optional)', choices=settings.GEOCAM_TIE_POINT_LICENSE_CHOICES) # extras: a special JSON-format field that holds additional # schema-free fields in the overlay model. Members of the field can # be accessed using dot notation. currently used extras subfields # include: imageSize, points, transform, bounds extras = ExtrasDotField() # import/export configuration exportFields = ('key', 'lastModifiedTime', 'name', 'description', 'imageSourceUrl') importFields = ('name', 'description', 'imageSourceUrl') importExtrasFields = ('points', 'transform') def getAlignedTilesUrl(self): if self.isPublic: urlName = 'geocamTiePoint_publicTile' else: urlName = 'geocamTiePoint_tile' return reverse( urlName, args=[str(self.alignedQuadTree.id), '[ZOOM]', '[X]', '[Y].png']) def getJsonDict(self): # export all schema-free subfields of extras result = self.extras.copy() # export other schema-controlled fields of self (listed in exportFields) for key in self.exportFields: val = getattr(self, key, None) if val not in ('', None): result[key] = val # conversions result['lastModifiedTime'] = ( result['lastModifiedTime'].replace(microsecond=0).isoformat() + 'Z') # calculate and export urls for client convenience result['url'] = reverse('geocamTiePoint_overlayIdJson', args=[self.key]) if self.unalignedQuadTree is not None: result['unalignedTilesUrl'] = reverse( 'geocamTiePoint_tile', args=[ str(self.unalignedQuadTree.id), '[ZOOM]', '[X]', '[Y].jpg' ]) result['unalignedTilesZoomOffset'] = quadTree.ZOOM_OFFSET if self.alignedQuadTree is not None: result['alignedTilesUrl'] = self.getAlignedTilesUrl() # note: when exportZip has not been set, its value is not # None but <FieldFile: None>, which is False in bool() context if self.alignedQuadTree.exportZip: result['exportUrl'] = reverse( 'geocamTiePoint_overlayExport', args=[self.key, str(self.alignedQuadTree.exportZipName)]) return result def setJsonDict(self, jsonDict): # set schema-controlled fields of self (listed in # self.importFields) for key in self.importFields: val = jsonDict.get(key, MISSING) if val is not MISSING: setattr(self, key, val) # set schema-free subfields of self.extras (listed in # self.importExtrasFields) for key in self.importExtrasFields: val = jsonDict.get(key, MISSING) if val is not MISSING: self.extras[key] = val jsonDict = property(getJsonDict, setJsonDict) class Meta: ordering = ['-key'] def __unicode__(self): return ( 'Overlay key=%s name=%s author=%s %s' % (self.key, self.name, self.author.username, self.lastModifiedTime)) def save(self, *args, **kwargs): self.lastModifiedTime = datetime.datetime.utcnow() super(Overlay, self).save(*args, **kwargs) def getSlug(self): return re.sub('[^\w]', '_', os.path.splitext(self.name)[0]) def getExportName(self): now = datetime.datetime.utcnow() return ('mapfasten-%s-%s' % (self.getSlug(), now.strftime('%Y-%m-%d-%H%M%S-UTC'))) def generateUnalignedQuadTree(self): qt = QuadTree(imageData=self.imageData) qt.save() self.unalignedQuadTree = qt self.save() return qt def generateAlignedQuadTree(self): if self.extras.get('transform') is None: return None qt = QuadTree(imageData=self.imageData, transform=dumps(self.extras.transform)) qt.save() self.alignedQuadTree = qt return qt def generateExport(self): (self.alignedQuadTree.generateExport(self.getExportName(), self.getJsonDict(), self.getSlug())) return self.alignedQuadTree.exportZip def updateAlignment(self): toPts, fromPts = transform.splitPoints(self.extras.points) tform = transform.getTransform(toPts, fromPts) self.extras.transform = tform.getJsonDict() def getSimpleAlignedOverlayViewer(self, request): alignedTilesPath = re.sub(r'/\[ZOOM\].*$', '', self.getAlignedTilesUrl()) alignedTilesRootUrl = request.build_absolute_uri(alignedTilesPath) return (self.alignedQuadTree.getSimpleViewHtml(alignedTilesRootUrl, self.getJsonDict(), self.getSlug()))
class Overlay(models.Model): # required fields key = models.AutoField(primary_key=True, unique=True) lastModifiedTime = models.DateTimeField() name = models.CharField(max_length=50) # optional fields # author: user who owns this overlay in the system author = models.ForeignKey(User, null=True, blank=True) description = models.TextField(blank=True) imageSourceUrl = models.URLField(blank=True) #, verify_exists=False) imageData = models.ForeignKey(ImageData, null=True, blank=True, related_name='currentOverlays', on_delete=models.SET_NULL) unalignedQuadTree = models.ForeignKey(QuadTree, null=True, blank=True, related_name='unalignedOverlays', on_delete=models.SET_NULL) alignedQuadTree = models.ForeignKey(QuadTree, null=True, blank=True, related_name='alignedOverlays', on_delete=models.SET_NULL) isPublic = models.BooleanField(default=settings.GEOCAM_TIE_POINT_PUBLIC_BY_DEFAULT) coverage = models.CharField(max_length=255, blank=True, verbose_name='Name of region covered by the overlay') # creator: name of person or organization who should get the credit # for producing the overlay creator = models.CharField(max_length=255, blank=True) sourceDate = models.CharField(max_length=255, blank=True, verbose_name='Source image creation date') rights = models.CharField(max_length=255, blank=True, verbose_name='Copyright information') license = models.URLField(blank=True, verbose_name='License permitting reuse (optional)', choices=settings.GEOCAM_TIE_POINT_LICENSE_CHOICES) centerLat = models.FloatField(null=True, blank=True, default=0) centerLon = models.FloatField(null=True, blank=True, default=0) nadirLat = models.FloatField(null=True, blank=True, default=0) nadirLon = models.FloatField(null=True, blank=True, default=0) # extras: a special JSON-format field that holds additional # schema-free fields in the overlay model. Members of the field can # be accessed using dot notation. currently used extras subfields # include: imageSize, points, transform, bounds, centerLat, centerLon, rotatedImageSize extras = ExtrasDotField() # import/export configuration readyToExport = models.BooleanField(default=False) # true if output product (geotiff, RMS error, etc) has been written to file. writtenToFile = models.BooleanField(default=False) # exportFields: export these fields to the client side (as JSON) exportFields = ('key', 'lastModifiedTime', 'name', 'description', 'imageSourceUrl', 'creator', 'readyToExport', 'centerLat', 'centerLon', 'nadirLat', 'nadirLon') # importFields: import these fields from the client side and save their values to the database. importFields = ('name', 'description', 'imageSourceUrl', 'readyToExport', 'centerLat', 'centerLon', 'nadirLat', 'nadirLon') importExtrasFields = ('points', 'transform') def getRawImageData(self): """ Returns the original image data created upon image upload (not rotated, not enhanced) """ try: imageData = ImageData.objects.filter(overlay__key = self.key).filter(raw = True) return imageData[0] except: # print "Error: no raw image data available" return None def getAlignedTilesUrl(self): if self.isPublic: urlName = 'geocamTiePoint_publicTile' else: urlName = 'geocamTiePoint_tile' return reverse(urlName, args=[str(self.alignedQuadTree.id)]) def getJsonDict(self): # export all schema-free subfields of extras result = self.extras.copy() # export other schema-controlled fields of self (listed in exportFields) for key in self.exportFields: val = getattr(self, key, None) if val not in ('', None): result[key] = val # conversions result['lmt_datetime'] = result['lastModifiedTime'].strftime('%F %k:%M') result['lastModifiedTime'] = (result['lastModifiedTime'] .replace(microsecond=0) .isoformat() + 'Z') # calculate and export urls for client convenience result['url'] = reverse('geocamTiePoint_overlayIdJson', args=[self.key]) try: deepzoomRoot = settings.DEEPZOOM_ROOT.replace(settings.PROJ_ROOT, '/') deepzoomFile = self.imageData.associated_deepzoom.name + '/' + self.imageData.associated_deepzoom.name + '.dzi' result['deepzoom_path'] = deepzoomRoot + deepzoomFile except: pass # set image size result['imageSize'] = [self.imageData.width, self.imageData.height] if 'issMRF' not in result: result['issMRF'] = self.imageData.issMRF if self.unalignedQuadTree is not None: result['unalignedTilesUrl'] = reverse('geocamTiePoint_tile', args=[str(self.unalignedQuadTree.id)]) result['unalignedTilesZoomOffset'] = quadTree.ZOOM_OFFSET if self.alignedQuadTree is not None: result['alignedTilesUrl'] = self.getAlignedTilesUrl() # note: when exportZip has not been set, its value is not # None but <FieldFile: None>, which is False in bool() context # include image enhancement values as part of json. if self.imageData is not None: try: result['rotationAngle'] = self.imageData.rotationAngle result['brightness'] = self.imageData.brightness result['contrast'] = self.imageData.contrast result['autoenhance'] = self.imageData.autoenhance except: pass try: mission, roll, frame = self.name.split('-') result['mission'] = mission result['roll'] = roll result['frame'] = frame[:-4] except: pass return result def setJsonDict(self, jsonDict): # set schema-controlled fields of self (listed in # self.importFields) for key in self.importFields: val = jsonDict.get(key, MISSING) if val is not MISSING: setattr(self, key, val) # set schema-free subfields of self.extras (listed in # self.importExtrasFields) for key in self.importExtrasFields: val = jsonDict.get(key, MISSING) if val is not MISSING: self.extras[key] = val # get the image enhancement values and save it to the overlay's imagedata. imageDataDict = {} imageDataDict['rotationAngle'] = jsonDict.get('rotationAngle', MISSING) imageDataDict['contrast'] = jsonDict.get('contrast', MISSING) imageDataDict['brightness'] = jsonDict.get('brightness', MISSING) imageDataDict['autoenhance'] = jsonDict.get('autoenhance', MISSING) for key, value in imageDataDict.items(): if value is not MISSING: try: setattr(self.imageData, key, value) except: print "failed to save image data values from the json dict returned from client" self.imageData.save() jsonDict = property(getJsonDict, setJsonDict) class Meta: ordering = ['-key'] def __unicode__(self): return ('Overlay key=%s name=%s author=%s %s' % (self.key, self.name, self.author.username, self.lastModifiedTime)) def save(self, *args, **kwargs): self.lastModifiedTime = datetime.datetime.utcnow() super(Overlay, self).save(*args, **kwargs) def getSlug(self): return re.sub('[^\w]', '_', os.path.splitext(self.name)[0]) def getExportName(self): now = datetime.datetime.utcnow() return 'georef-%s' % self.getSlug() def generateUnalignedQuadTree(self): qt = QuadTree(imageData=self.imageData) qt.save() self.unalignedQuadTree = qt self.save() return qt def generateAlignedQuadTree(self): if self.extras.get('transform') is None: return None # grab the original image's imageData originalImageData = self.getRawImageData() qt = QuadTree(imageData=originalImageData, transform=dumps(self.extras.transform)) qt.save() self.alignedQuadTree = qt return qt def generateHtmlExport(self): (self.alignedQuadTree.generateHtmlExport (self.getExportName(), self.getJsonDict(), self.getSlug())) return self.alignedQuadTree.htmlExport def generateKmlExport(self): (self.alignedQuadTree.generateKmlExport (self.getExportName(), self.getJsonDict(), self.getSlug())) return self.alignedQuadTree.kmlExport def generateGeotiffExport(self): (self.alignedQuadTree.generateGeotiffExport (self.getExportName(), self.getJsonDict(), self.getSlug())) return self.alignedQuadTree.geotiffExport def updateAlignment(self): toPts, fromPts = transform.splitPoints(self.extras.points) tform = transform.getTransform(toPts, fromPts) self.extras.transform = tform.getJsonDict() def getSimpleAlignedOverlayViewer(self, request): alignedTilesPath = re.sub(r'/\[ZOOM\].*$', '', self.getAlignedTilesUrl()) alignedTilesRootUrl = request.build_absolute_uri(alignedTilesPath) return (self.alignedQuadTree .getSimpleViewHtml(alignedTilesRootUrl, self.getJsonDict(), self.getSlug()))