kopia lustrzana https://github.com/hholzgra/maposmatic/
356 wiersze
13 KiB
Python
356 wiersze
13 KiB
Python
# coding: utf-8
|
|
|
|
# maposmatic, the web front-end of the MapOSMatic city map generation system
|
|
# Copyright (C) 2009 David Decotigny
|
|
# Copyright (C) 2009 Frédéric Lehobey
|
|
# Copyright (C) 2009 David Mentré
|
|
# Copyright (C) 2009 Maxime Petazzoni
|
|
# Copyright (C) 2009 Thomas Petazzoni
|
|
# Copyright (C) 2009 Gaël Utard
|
|
# Copyright (C) 2018 Hartmut Holzgraefe
|
|
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU Affero General Public License as
|
|
# published by the Free Software Foundation, either version 3 of the
|
|
# License, or 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 Affero General Public License for more details.
|
|
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
from django.core.urlresolvers import reverse
|
|
from django.db import models
|
|
from django.utils.translation import ugettext_lazy as _
|
|
|
|
from datetime import datetime, timedelta
|
|
import www.settings
|
|
import re
|
|
import os
|
|
from slugify import slugify
|
|
|
|
import logging
|
|
LOG = logging.getLogger('maposmatic')
|
|
|
|
def get_track_path(instance, filename):
|
|
return ""
|
|
|
|
def get_umap_path(instance, filename):
|
|
return ""
|
|
|
|
def get_poi_file_path(instance, filename):
|
|
return ""
|
|
|
|
class MapRenderingJobManager(models.Manager):
|
|
def to_render(self):
|
|
return MapRenderingJob.objects.filter(status=0).order_by('submission_time')
|
|
|
|
def queue_size(self):
|
|
return MapRenderingJob.objects.filter(status=0).count()
|
|
|
|
# We try to find a rendered map from the last 15 days, which still
|
|
# has its thumbnail present.
|
|
def get_random_with_thumbnail(self):
|
|
fifteen_days_before = datetime.now() - timedelta(15)
|
|
maps = (MapRenderingJob.objects.filter(status=2)
|
|
.filter(submission_time__gte=fifteen_days_before)
|
|
.filter(resultmsg='ok')
|
|
.order_by('?')[0:10])
|
|
for m in maps:
|
|
if m.get_thumbnail():
|
|
return m
|
|
return None
|
|
|
|
def get_by_filename(self, name):
|
|
"""Tries to find the parent MapRenderingJob of a given file from its
|
|
filename. Both the job ID found in the first part of the prefix and the
|
|
entire files_prefix is used to match a job."""
|
|
|
|
try:
|
|
jobid = int(name.split('_', 1)[0])
|
|
job = MapRenderingJob.objects.get(id=jobid)
|
|
if name.startswith(job.files_prefix()):
|
|
return job
|
|
except (ValueError, IndexError, MapRenderingJob.DoesNotExist):
|
|
pass
|
|
|
|
return None
|
|
|
|
SPACE_REDUCE = re.compile(r"\s+")
|
|
NONASCII_REMOVE = re.compile(r"[^A-Za-z0-9]+")
|
|
|
|
class MapRenderingJob(models.Model):
|
|
|
|
STATUS_LIST = (
|
|
(0, 'Submitted'),
|
|
(1, 'In progress'),
|
|
(2, 'Done'),
|
|
(3, 'Done w/o files'),
|
|
(4, 'Cancelled'),
|
|
)
|
|
|
|
NONCE_SIZE = 16
|
|
|
|
maptitle = models.CharField(max_length=256)
|
|
stylesheet = models.CharField(max_length=256)
|
|
overlay = models.CharField(max_length=256, null=True, blank=True)
|
|
layout = models.CharField(max_length=256)
|
|
paper_width_mm = models.IntegerField()
|
|
paper_height_mm = models.IntegerField()
|
|
bitmap_dpi = models.IntegerField(default=72)
|
|
|
|
# When rendering through administrative city is selected, the
|
|
# following three fields must be non empty
|
|
administrative_city = models.CharField(max_length=256, blank=True)
|
|
administrative_osmid = models.IntegerField(blank=True, null=True)
|
|
|
|
# When rendering through bounding box is selected, the following
|
|
# four fields must be non empty
|
|
lat_upper_left = models.FloatField(blank=True, null=True)
|
|
lon_upper_left = models.FloatField(blank=True, null=True)
|
|
lat_bottom_right = models.FloatField(blank=True, null=True)
|
|
lon_bottom_right = models.FloatField(blank=True, null=True)
|
|
|
|
status = models.IntegerField(choices=STATUS_LIST)
|
|
submission_time = models.DateTimeField(auto_now_add=True)
|
|
startofrendering_time = models.DateTimeField(null=True,blank=True)
|
|
endofrendering_time = models.DateTimeField(null=True,blank=True)
|
|
resultmsg = models.CharField(max_length=256, null=True,blank=True)
|
|
submitterip = models.GenericIPAddressField()
|
|
submittermail = models.EmailField(null=True,blank=True)
|
|
index_queue_at_submission = models.IntegerField()
|
|
map_language = models.CharField(max_length=16)
|
|
|
|
nonce = models.CharField(max_length=NONCE_SIZE, blank=True)
|
|
|
|
|
|
objects = MapRenderingJobManager()
|
|
|
|
def __str__(self):
|
|
return self.maptitle.encode('utf-8')
|
|
|
|
track = models.FileField(upload_to='upload/tracks/%Y/%m/%d/', null=True, blank=True)
|
|
|
|
umap = models.FileField(upload_to='upload/umaps/%Y/%m/%d/', null=True, blank=True)
|
|
|
|
poi_file = models.FileField(upload_to='upload/poi/%Y/%m/%d/', null=True, blank=True)
|
|
|
|
def maptitle_computized(self):
|
|
t = self.maptitle.strip()
|
|
if self.id <= www.settings.LAST_OLD_ID:
|
|
t = SPACE_REDUCE.sub("-", t)
|
|
t = NONASCII_REMOVE.sub("", t)
|
|
else:
|
|
t = slugify(t)
|
|
return t
|
|
|
|
def files_prefix(self):
|
|
try:
|
|
return "%06d_%s_%s" % \
|
|
(self.id,
|
|
self.startofrendering_time.strftime("%Y-%m-%d_%H-%M"),
|
|
self.maptitle_computized())
|
|
except Exception:
|
|
return "%06d_%s" % \
|
|
(self.id,
|
|
self.maptitle_computized())
|
|
|
|
|
|
def start_rendering(self):
|
|
self.status = 1
|
|
self.startofrendering_time = datetime.now()
|
|
self.save()
|
|
|
|
def end_rendering(self, resultmsg):
|
|
self.status = 2
|
|
self.endofrendering_time = datetime.now()
|
|
self.resultmsg = resultmsg
|
|
self.save()
|
|
|
|
def rendering_time_gt_1min(self):
|
|
if self.needs_waiting():
|
|
return False
|
|
|
|
delta = self.endofrendering_time - self.startofrendering_time
|
|
return delta.seconds > 60
|
|
|
|
def __is_ok(self): return self.resultmsg == 'ok'
|
|
|
|
def is_waiting(self): return self.status == 0
|
|
def is_rendering(self): return self.status == 1
|
|
def needs_waiting(self): return self.status < 2
|
|
|
|
def is_done(self): return self.status == 2
|
|
def is_done_ok(self): return self.is_done() and self.__is_ok()
|
|
def is_done_failed(self): return self.is_done() and not self.__is_ok()
|
|
|
|
def is_obsolete(self): return self.status == 3
|
|
def is_obsolete_ok(self): return self.is_obsolete() and self.__is_ok()
|
|
def is_obsolete_failed(self): return self.is_obsolete() and not self.__is_ok()
|
|
|
|
def is_cancelled(self): return self.status == 4
|
|
|
|
def can_recreate(self):
|
|
return (((self.administrative_city and self.administrative_osmid) or
|
|
(self.lat_upper_left and self.lon_upper_left and
|
|
self.lat_bottom_right and self.lon_bottom_right)) and
|
|
self.stylesheet and self.layout and
|
|
self.paper_width_mm != -1 and self.paper_height_mm != -1)
|
|
|
|
def get_map_fileurl(self, format):
|
|
return www.settings.RENDERING_RESULT_URL + "/" + self.files_prefix() + "." + format
|
|
|
|
def get_map_filepath(self, format):
|
|
return os.path.join(www.settings.RENDERING_RESULT_PATH, self.files_prefix() + "." + format)
|
|
|
|
def output_files(self):
|
|
"""Returns a structured dictionary of the output files for this job.
|
|
The result contains two lists, 'maps' and 'indeces', listing the output
|
|
files. Each file is reported by a tuple (format, path, title, size)."""
|
|
|
|
allfiles = {'maps': {}, 'indeces': {}, 'thumbnail': [], 'errorlog': []}
|
|
|
|
formats = www.settings.RENDERING_RESULT_FORMATS
|
|
formats.append('8bit.png')
|
|
formats.append('jpg')
|
|
for format in formats:
|
|
map_path = self.get_map_filepath(format)
|
|
if format != 'csv' and os.path.exists(map_path):
|
|
# Map files (all formats but CSV)
|
|
allfiles['maps'][format] = (
|
|
self.get_map_fileurl(format),
|
|
_("%(title)s %(format)s Map") % {'title': self.maptitle,
|
|
'format': format.upper()},
|
|
os.stat(map_path).st_size,
|
|
map_path)
|
|
elif format == 'csv' and os.path.exists(map_path):
|
|
# Index CSV file
|
|
allfiles['indeces'][format] = (
|
|
self.get_map_fileurl(format),
|
|
_("%(title)s %(format)s Index") % {'title': self.maptitle,
|
|
'format': format.upper()},
|
|
os.stat(map_path).st_size,
|
|
map_path)
|
|
|
|
thumbnail = os.path.join(www.settings.RENDERING_RESULT_PATH, self.files_prefix() + "_small.png")
|
|
if os.path.exists(thumbnail):
|
|
allfiles['thumbnail'].append(thumbnail)
|
|
|
|
errorlog = os.path.join(www.settings.RENDERING_RESULT_PATH, self.files_prefix() + "-errors.txt")
|
|
if os.path.exists(errorlog):
|
|
allfiles['errorlog'].append(errorlog)
|
|
|
|
return allfiles
|
|
|
|
def has_output_files(self):
|
|
"""This function tells whether this job still has its output files
|
|
available on the rendering storage.
|
|
|
|
Their actual presence is checked if the job is considered done and not
|
|
yet obsolete."""
|
|
|
|
if self.is_done():
|
|
files = self.output_files()
|
|
return len(files['maps']) + len(files['indeces']) + len(files['thumbnail'])
|
|
|
|
return False
|
|
|
|
def remove_all_files(self):
|
|
"""Removes all the output files from this job, and returns the space
|
|
saved in bytes (Note: the thumbnail is not removed)."""
|
|
|
|
files = self.output_files()
|
|
saved = 0
|
|
removed = 0
|
|
|
|
for f in (list(files['maps'].values()) + list(files['indeces'].values()) + files['thumbnail']):
|
|
try:
|
|
os.remove(f[3])
|
|
removed += 1
|
|
saved += f[2]
|
|
except OSError:
|
|
pass
|
|
|
|
self.status = 3
|
|
self.save()
|
|
return removed, saved
|
|
|
|
def cancel(self):
|
|
self.status = 4
|
|
self.endofrendering_time = datetime.now()
|
|
self.resultmsg = 'rendering cancelled'
|
|
self.save()
|
|
|
|
def get_thumbnail(self):
|
|
if self.is_waiting() or self.is_cancelled():
|
|
return None
|
|
|
|
thumbnail_file = os.path.join(www.settings.RENDERING_RESULT_PATH, self.files_prefix() + "_small.png")
|
|
thumbnail_url = www.settings.RENDERING_RESULT_URL + "/" + self.files_prefix() + "_small.png"
|
|
if os.path.exists(thumbnail_file):
|
|
return thumbnail_url
|
|
return None
|
|
|
|
def get_errorlog(self):
|
|
if self.is_waiting():
|
|
return None
|
|
|
|
errorlog_file = os.path.join(www.settings.RENDERING_RESULT_PATH, self.files_prefix() + "-errors.txt")
|
|
errorlog_url = www.settings.RENDERING_RESULT_URL + "/" + self.files_prefix() + "-errors.txt"
|
|
if os.path.exists(errorlog_file):
|
|
return errorlog_url
|
|
return None
|
|
|
|
def current_position_in_queue(self):
|
|
return MapRenderingJob.objects.filter(status=0).filter(id__lte=self.id).count()
|
|
|
|
# Estimate the date at which the rendering will be started
|
|
def rendering_estimated_start_time(self):
|
|
waiting_time = datetime.now() - self.submission_time
|
|
progression = self.index_queue_at_submission - self.current_position_in_queue()
|
|
if progression == 0:
|
|
return datetime.now()
|
|
mean_job_rendering_time = waiting_time // progression
|
|
estimated_time_left = mean_job_rendering_time * self.current_position_in_queue()
|
|
return datetime.now() + estimated_time_left
|
|
|
|
def get_absolute_url(self):
|
|
return reverse('map-by-id', args=[self.id])
|
|
|
|
def clean(self):
|
|
from django.core.exceptions import ValidationError
|
|
import ocitysmap
|
|
|
|
errors = {}
|
|
|
|
_ocitysmap = ocitysmap.OCitySMap(www.settings.OCITYSMAP_CFG_PATH)
|
|
|
|
renderer_names = _ocitysmap.get_all_renderer_names()
|
|
if self.layout not in renderer_names:
|
|
errors['layout'] = ValidationError(_("Invalid layout '%s'" % self.layout), code='invalid')
|
|
|
|
style_names = _ocitysmap.get_all_style_names()
|
|
if self.stylesheet not in style_names:
|
|
errors['stylesheet'] = ValidationError(_("Invalid style '%s'" % self.stylesheet), code='invalid')
|
|
|
|
# TODO Django form passes value in weird ways, check how to correctly do this ....
|
|
if self.overlay is not None:
|
|
LOG.warning("Overlays: %s" % self.overlay)
|
|
overlay_names = _ocitysmap.get_all_overlay_names()
|
|
if isinstance (self.overlay, str):
|
|
overlays = self.overlay.split(',')
|
|
else:
|
|
overlays = self.overlay
|
|
|
|
# for test_name in overlays:
|
|
# LOG.warning("checking overlay '%s'" % test_name)
|
|
# if test_name not in overlay_names:
|
|
# errors['overlay'] = ValidationError(_("Invalid overlay '%s'" % test_name), code='invalid')
|
|
# break
|
|
|
|
if errors:
|
|
raise ValidationError(errors)
|