#!/usr/bin/env python # -*- coding: utf-8 -*- # Copyright (C) 2008-2011 Étienne Loks # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # This program 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 # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # See the file COPYING for details. """ Models description """ import os, string import simplejson as json import lxml.etree as ElementTree from datetime import datetime, timedelta from subprocess import Popen, PIPE from django.core.files import File from django.utils.translation import ugettext_lazy as _ from django.contrib.gis.db import models from django.contrib.gis.gdal import SpatialReference from django.contrib import admin from chimere import settings from chimere.main.widgets import PointField, RouteField, SelectMultipleField class News(models.Model): """News of the site """ title = models.CharField(_("Name"), max_length=150) available = models.BooleanField(_("Available")) date = models.DateField(_("Date"), auto_now_add=True) content = models.TextField() def __unicode__(self): ordering = ["-date"] return self.title class Meta: verbose_name = _("News") verbose_name_plural = _("News") class TinyUrl(models.Model): """Tinyfied version of permalink parameters """ parameters = models.CharField(_("Parameters"), max_length=500) def __unicode__(self): return self.parameters class Meta: verbose_name = _("TinyUrl") digits = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" base = len(digits) @classmethod def getParametersByUrn(cls, urn): c_id = 0 for idx, char in enumerate(reversed(urn)): c_id += cls.digits.index(char)*pow(cls.base, idx) try: params = cls.objects.get(id=c_id).parameters except cls.DoesNotExist: return '' return params @classmethod def getUrnByParameters(cls, parameters): try: obj = cls.objects.get(parameters=parameters) except cls.DoesNotExist: obj = cls(parameters=parameters) obj.save() n = obj.id urn = '' while 1: idx = n % cls.base urn = cls.digits[idx] + urn n = n / cls.base if n == 0: break return urn class ColorTheme(models.Model): """Color theme """ name = models.CharField(_("Name"), max_length=150) def __unicode__(self): return self.name class Meta: verbose_name = _("Color theme") class Color(models.Model): """Color """ code = models.CharField(_("Code"), max_length=6) order = models.IntegerField(_("Order")) color_theme = models.ForeignKey(ColorTheme, verbose_name=_("Color theme")) def __unicode__(self): return self.code class Meta: ordering = ["order"] verbose_name = _("Color") class Category(models.Model): """Category of Point Of Interest (POI) """ name = models.CharField(_("Name"), max_length=150) available = models.BooleanField(_("Available")) order = models.IntegerField(_("Order")) description = models.TextField(blank=True, null=True) def __unicode__(self): return self.name class Meta: ordering = ["order"] verbose_name = _("Category") class Icon(models.Model): '''Icon ''' name = models.CharField(_("Name"), max_length=150) image = models.ImageField(_("Image"), upload_to='icons', height_field='height', width_field='width') height = models.IntegerField(_("Height")) width = models.IntegerField(_("Width")) def __unicode__(self): return self.name class Meta: verbose_name = _("Icon") class SubCategory(models.Model): '''Sub-category ''' category = models.ForeignKey(Category, verbose_name=_("Category")) name = models.CharField(_("Name"), max_length=150) available = models.BooleanField(_("Available")) areas = SelectMultipleField('Area', related_name='areas', blank=True) icon = models.ForeignKey(Icon, verbose_name=_("Icon")) color_theme = models.ForeignKey(ColorTheme, verbose_name=_("Color theme"), blank=True, null=True) order = models.IntegerField(_("Order")) TYPE = (('M', _('Marker')), ('R', _('Route')), ('B', _('Both')),) item_type = models.CharField(_("Item type"), max_length=1, choices=TYPE) def __unicode__(self): return u"%s / %s" % (self.category.name, self.name) class Meta: ordering = ["category", "order"] verbose_name = _("Subcategory") @classmethod def getAvailable(cls, item_types=None, area_name=None): '''Get list of tuples with first the category and second the associated subcategories ''' sub_categories = {} subcategories = cls.objects.filter(category__available=True) if not item_types: subcategories = subcategories.filter(available=True) else: subcategories = subcategories.filter(item_type__in=item_types) if area_name: area = Area.objects.get(urn=area_name) # if there some restrictions with categories limit them if area.subcategory_set.count(): sub_ids = [sub.id for sub in area.subcategory_set.all()] # if no area is defined for a category don't filter it sub_ids += [sub.id for sub in subcategories if not sub.areas.count()] subcategories = subcategories.filter(id__in=sub_ids) for sub_category in subcategories: if sub_category.category not in sub_categories: sub_categories[sub_category.category] = [] if sub_category.id in settings.DEFAULT_CATEGORIES: sub_category.selected = True sub_category.category.selected = True sub_categories[sub_category.category].append(sub_category) return [(category, sub_cats) for category, sub_cats \ in sub_categories.items()] class Marker(models.Model): '''Marker for a POI ''' name = models.CharField(_("Name"), max_length=150) categories = SelectMultipleField(SubCategory) point = PointField(_("Localisation"), srid=settings.EPSG_DISPLAY_PROJECTION) picture = models.ImageField(_("Image"), upload_to='upload', blank=True, null=True, height_field='height', width_field='width') height = models.IntegerField(_("Height"), blank=True, null=True) width = models.IntegerField(_("Width"), blank=True, null=True) STATUS = (('S', _('Submited')), ('A', _('Available')), ('D', _('Disabled')),) STATUS_DCT = {} for key, label in STATUS: STATUS_DCT[key] = label status = models.CharField(_("Status"), max_length=1, choices=STATUS) if settings.DAYS_BEFORE_EVENT: start_date = models.DateField(_("Start date"), blank=True, null=True, help_text=_(u"Not mandatory. Set it for dated item such as event. "\ u"Format YYYY-MM-DD")) end_date = models.DateField(_("End date"), blank=True, null=True, help_text=_(u"Not mandatory. Set it only if you have a multi-day "\ u"event. Format YYYY-MM-DD")) if 'chimere.rss' in settings.INSTALLED_APPS: available_date = models.DateTimeField(_("Available Date"), blank=True, null=True) route = models.ForeignKey("Route", blank=True, null=True) objects = models.GeoManager() def __unicode__(self): return self.name @property def date(self): if settings.DAYS_BEFORE_EVENT: return self.start_date class Meta: ordering = ('status', 'name') verbose_name = _("Point of interest") def getLatitude(self): '''Return the latitude ''' return self.point.y def getLongitude(self): '''Return the longitude ''' return self.point.x def getProperty(self, propertymodel, safe=None): """Get the property of an associated property model. If safe set to True, verify if the property is available """ if safe and not propertymodel.available: return try: property = Property.objects.get(propertymodel=propertymodel, marker=self) except Property.DoesNotExist: return return property def getProperties(self): """Get all the property availables """ properties = [] for pm in PropertyModel.objects.filter(available=True): property = self.getProperty(pm) if property: properties.append(property) return properties def saveProperties(self, values): """ Save properties """ for propertymodel in PropertyModel.objects.filter(available=True): properties = Property.objects.filter(marker=self, propertymodel=propertymodel).all() # in case of multiple edition as the same time delete arbitrary # the others if len(properties) > 1: for property in properties[1:]: property.delete() val = u"" if unicode(propertymodel.id) in values: val = values[unicode(propertymodel.id)] # new property if not properties: new_property = Property.objects.create(marker=self, propertymodel=propertymodel, value=val) new_property.save() else: property = properties[0] property.value = val property.save() def getGeoJSON(self, categories_id=[]): '''Return a GeoJSON string ''' jsons = [] for cat in self.categories.all(): if categories_id and cat.id not in categories_id: continue items = {'id':self.id, 'name':json.dumps(self.name), 'geometry':self.point.geojson, 'icon_path':cat.icon.image, 'icon_width':cat.icon.image.width, 'icon_height':cat.icon.image.height,} jsons.append(u'{"type":"Feature", "geometry":%(geometry)s, '\ u'"properties":{"pk": %(id)d, "name": %(name)s, '\ u'"icon_path":"%(icon_path)s", "icon_width":%(icon_width)d, '\ u'"icon_height":%(icon_height)d}}' % items) return ",".join(jsons) def get_absolute_url(self): parameters = 'current_feature=%d&checked_categories=%s' % (self.id, self.categories.all()[0].id) return settings.BASE_URL + '/ty/' + TinyUrl.getUrnByParameters(parameters) class RouteFile(models.Model): name = models.CharField(_(u"Name"), max_length=150) raw_file = models.FileField(_(u"Raw file (gpx or kml)"), upload_to='upload') simplified_file = models.FileField(_(u"Simplified file"), upload_to='upload', blank=True, null=True) TYPE = (('K', _(u'KML')), ('G', _(u'GPX'))) file_type = models.CharField(max_length=1, choices=TYPE) def __unicode__(self): return self.name def process(self): if self.simplified_file: return input_name = settings.MEDIA_ROOT + self.raw_file.name output_name = settings.MEDIA_ROOT + self.raw_file.name[:-4] + \ "_simplified" + ".gpx" cli_args = [settings.GPSBABEL, '-i'] if self.file_type == 'K': cli_args.append('kml') elif self.file_type == 'G': cli_args.append('gpx') cli_args += ['-f', input_name, '-x', settings.GPSBABEL_OPTIONS, '-o', 'gpx', '-F', output_name] p = Popen(cli_args, stderr=PIPE) p.wait() if p.returncode: print p.stderr.read() #logger.error(p.stderr.read()) else: self.simplified_file = File(open(output_name)) self.save() os.remove(output_name) @property def route(self): if not self.simplified_file: return mainNS = string.Template("{http://www.topografix.com/GPX/1/0}$tag") trkpt = mainNS.substitute(tag="trkpt") file_name = settings.MEDIA_ROOT + self.simplified_file.name et = ElementTree.parse(open(file_name)) pts = [] for pt in et.findall("//" + trkpt): pts.append((pt.get("lon"), pt.get("lat"))) geojson_tpl = u'{"type":"Feature", "geometry":{ "type": "LineString", '\ '"coordinates":[%s]}}' wkt_tpl = u'LINESTRING(%s)' return wkt_tpl % u','.join([u'%s %s' % (pt[0], pt[1]) \ for pt in pts]) class Route(models.Model): '''Route on the map ''' name = models.CharField(_("Name"), max_length=150) categories = SelectMultipleField(SubCategory) route = RouteField(_("Route"), srid=settings.EPSG_DISPLAY_PROJECTION) associated_file = models.ForeignKey(RouteFile, blank=True, null=True, verbose_name=_(u"Associated file")) picture = models.ImageField(_("Image"), upload_to='upload', blank=True, null=True, height_field='height', width_field='width') height = models.IntegerField(_("Height"), blank=True, null=True) width = models.IntegerField(_("Width"), blank=True, null=True) STATUS = (('S', _('Submited')), ('A', _('Available')), ('D', _('Disabled')),) STATUS_DCT = {} for key, label in STATUS: STATUS_DCT[key] = label if settings.DAYS_BEFORE_EVENT: start_date = models.DateField(_("Start date"), blank=True, null=True, help_text=_(u"Not mandatory. Set it for dated item such as event. "\ u"Format YYYY-MM-DD")) end_date = models.DateField(_("End date"), blank=True, null=True, help_text=_(u"Not mandatory. Set it only if you have a multi-day "\ u"event. Format YYYY-MM-DD")) status = models.CharField(_("Status"), max_length=1, choices=STATUS) objects = models.GeoManager() def __unicode__(self): return self.name class Meta: ordering = ('status', 'name') verbose_name = _("Route") def getProperty(self, propertymodel, safe=None): """Get the property of an associated property model. If safe set to True, verify if the property is available """ if safe and not propertymodel.available: return try: property = Property.objects.get(propertymodel=propertymodel, marker=self) except Property.DoesNotExist: return return property def getProperties(self): """Get all the property availables """ properties = [] for pm in PropertyModel.objects.filter(available=True): property = self.getProperty(pm) if property: properties.append(property) return properties def getGeoJSON(self, color="#000"): '''Return a GeoJSON string ''' if '#' not in color: color = '#' + color attributes = {'id':self.id, 'name':json.dumps(self.name), 'color':color, 'geometry':self.route.geojson,} return u'{"type":"Feature", "geometry":%(geometry)s, '\ u'"properties":{"pk": %(id)d, "name": %(name)s, '\ u'"color":"%(color)s"}}' % attributes def getTinyUrl(self): parameters = 'current_feature=%d&checked_categories=%s' % (self.id, self.categories[0].id) return TinyUrl.getUrnByParameters(parameters) def getDateCondition(): ''' Return an SQL condition for apparition of dates ''' if not settings.DAYS_BEFORE_EVENT: return "" now = datetime.now().strftime('%Y-%m-%d') after = (datetime.now() + timedelta(settings.DAYS_BEFORE_EVENT) ).strftime('%Y-%m-%d') date_condition = " and %(alias)s.start_date is null or " date_condition += "(%(alias)s.start_date >= '" + now + "' and " date_condition += "%(alias)s.start_date <='" + after + "')" date_condition += " or (%(alias)s.start_date <='" + now + "' and " date_condition += "%(alias)s.end_date >='" + now + "') " return date_condition class SimplePoint: """ Point in the map (not in the database) """ def __init__(self, x, y): self.x, self.y = x, y class SimpleArea: """ Rectangular area of a map (not in the database) """ def __init__(self, area): """ Defining upper left corner ans lower right corner from a tuple """ assert len(area) == 4 x1, y1, x2, y2 = area self.upper_left_corner = SimplePoint(x1, y1) self.lower_right_corner = SimplePoint(x2, y2) def isIn(self, area): """ Verify if the current area is in the designated area """ if self.upper_left_corner.x >= area.upper_left_corner.x and \ self.upper_left_corner.y <= area.upper_left_corner.x and \ self.lower_right_corner.x <= area.lower_right_corner.x and \ self.lower_right_corner.y >= area.lower_right_corner.y: return True return False def getCategories(self, status='A', filter_available=True): """ Get categories for this area """ equal_status = '' if len(status) == 1: equal_status = "='%s'" % status[0] elif status: equal_status = " in ('%s')" % "','".join(status) area = u"ST_GeometryFromText('POLYGON((%f %f,%f %f,%f %f,%f %f, %f %f"\ u"))', %d)" % (self.upper_left_corner.x, self.upper_left_corner.y, self.lower_right_corner.x, self.upper_left_corner.y, self.lower_right_corner.x, self.lower_right_corner.y, self.upper_left_corner.x, self.lower_right_corner.y, self.upper_left_corner.x, self.upper_left_corner.y, settings.EPSG_DISPLAY_PROJECTION ) date_condition = getDateCondition() sql_main = '''select subcat.id as id, subcat.category_id as category_id, subcat.name as name, subcat.available as available, subcat.icon_id as icon_id, subcat.color_theme_id as color_theme_id, subcat.order as order, subcat.item_type as item_type from main_subcategory subcat inner join main_category cat on cat.id=subcat.category_id''' sql = sql_main + ''' inner join main_marker mark on ST_Contains(%s, mark.point)''' % area if equal_status: sql += ' and mark.status' + equal_status sql += date_condition % {'alias':'mark'} sql += ''' inner join main_marker_categories mc on mc.subcategory_id=subcat.id and mc.marker_id=mark.id''' if filter_available: sql += ' where subcat.available = TRUE and cat.available = TRUE' subcats = set(SubCategory.objects.raw(sql)) sql = sql_main + ''' inner join main_route rt on (ST_Intersects(%s, rt.route) or ST_Contains(%s, rt.route))''' % (area, area) if equal_status: sql += ' and rt.status' + equal_status sql += date_condition % {'alias':'rt'} sql += ''' inner join main_route_categories rc on rc.subcategory_id=subcat.id and rc.route_id=rt.id''' if filter_available: sql += ' where subcat.available = TRUE and cat.available = TRUE' subcats.union(SubCategory.objects.raw(sql)) return subcats class Area(models.Model, SimpleArea): """Rectangular area of the map """ name = models.CharField(_("Name"), max_length=150) urn = models.SlugField(_("Area urn"), max_length=50, blank=True, unique=True) order = models.IntegerField(_("Order")) available = models.BooleanField(_("Available")) upper_left_corner = models.PointField(_("Upper left corner"), default='POINT(0 0)', srid=settings.EPSG_DISPLAY_PROJECTION) lower_right_corner = models.PointField(_("Lower right corner"), default='POINT(0 0)', srid=settings.EPSG_DISPLAY_PROJECTION) objects = models.GeoManager() def __unicode__(self): return self.name class Meta: ordering = ('order', 'name') verbose_name = _("Area") @classmethod def getAvailable(cls): '''Get available areas ''' return cls.objects.filter(available=True) def getIncludeSql(self, geometry='"main_marker".point'): """ Get the sql statement for the test if the point is included in the area """ area = "ST_GeometryFromText('POLYGON((%f %f,%f %f,%f %f,%f %f, %f %f"\ "))', %d)" % (self.upper_left_corner.x, self.upper_left_corner.y, self.lower_right_corner.x, self.upper_left_corner.y, self.lower_right_corner.x, self.lower_right_corner.y, self.upper_left_corner.x, self.lower_right_corner.y, self.upper_left_corner.x, self.upper_left_corner.y, settings.EPSG_DISPLAY_PROJECTION ) sql = "ST_Contains(" + area + ", " + geometry + ")" return sql class PropertyModel(models.Model): '''Model for a property ''' name = models.CharField(_("Name"), max_length=150) order = models.IntegerField(_("Order")) available = models.BooleanField(_("Available")) TYPE = (('T', _('Text')), ('L', _('Long text')), ('P', _('Password'))) TYPE_WIDGET = {'T':'forms.TextInput', 'L':'TextareaWidget', 'P':'forms.PasswordInput'} type = models.CharField(_("Type"), max_length=1, choices=TYPE) def __unicode__(self): return self.name class Meta: ordering = ('order',) verbose_name = _("Property model") def getNamedId(self): '''Get the name used as named id (easily sortable) ''' return 'property_%d_%d' % (self.order, self.id) class Property(models.Model): '''Property for a POI ''' marker = models.ForeignKey(Marker, verbose_name=_("Point of interest")) propertymodel = models.ForeignKey(PropertyModel, verbose_name=_("Property model")) value = models.TextField(_("Value")) def __unicode__(self): return "%s : %s" % (str(self.propertymodel), self.value) class Meta: verbose_name = _("Property")