implement new django-based models and loadmappkg command
This commit is contained in:
parent
66661209d2
commit
752b7d6d7d
14 changed files with 318 additions and 196 deletions
|
@ -10,7 +10,7 @@
|
||||||
var map = L.map('mapeditor', {
|
var map = L.map('mapeditor', {
|
||||||
center: [120, 200],
|
center: [120, 200],
|
||||||
zoom: 2,
|
zoom: 2,
|
||||||
maxBounds: [[0, 0], [{{ map.height }}, {{ map.width }}]],
|
maxBounds: {{ bounds }},
|
||||||
maxZoom: 10,
|
maxZoom: 10,
|
||||||
minZoom: 1,
|
minZoom: 1,
|
||||||
crs: L.CRS.Simple,
|
crs: L.CRS.Simple,
|
||||||
|
@ -19,10 +19,10 @@ var map = L.map('mapeditor', {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add Source Layers
|
// Add Source Layers
|
||||||
{% for pkg in map.pkgs.values %}
|
{% for pkg in packages %}
|
||||||
L.control.layers([], {
|
L.control.layers([], {
|
||||||
{% for source in pkg.sources %}
|
{% for source in pkg.sources.all %}
|
||||||
"{{ source.name }}": L.imageOverlay('{% url 'map.source' filename=source.filename %}', {{ source.jsbounds }}),{% endfor %}
|
"{{ source.name }}": L.imageOverlay('{% url 'map.source' source=source.name %}', {{ source.jsbounds }}),{% endfor %}
|
||||||
}).addTo(map);
|
}).addTo(map);
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
|
@ -33,10 +33,10 @@ L.LevelControl = L.Control.extend({
|
||||||
},
|
},
|
||||||
onAdd: function (map) {
|
onAdd: function (map) {
|
||||||
var container = L.DomUtil.create('div', 'leaflet-control leaflet-bar leaflet-levels'), link;
|
var container = L.DomUtil.create('div', 'leaflet-control leaflet-bar leaflet-levels'), link;
|
||||||
{% for level in map.levels reversed %}
|
{% for level in levels reversed %}
|
||||||
link = L.DomUtil.create('a', '{% if current_level == level %}current{% endif %}', container);
|
link = L.DomUtil.create('a', '{% if current_level == level %}current{% endif %}', container);
|
||||||
link.href = '{% url "control.editor" level=level %}';
|
link.href = '{% url "control.editor" level=level.name %}';
|
||||||
link.innerHTML = '{{ level }}';
|
link.innerHTML = '{{ level.name }}';
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
return container;
|
return container;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
from django.contrib.admin.views.decorators import staff_member_required
|
import json
|
||||||
from django.http import Http404
|
|
||||||
from django.shortcuts import redirect, render
|
|
||||||
|
|
||||||
from ..mapdata import mapmanager
|
from django.contrib.admin.views.decorators import staff_member_required
|
||||||
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
|
|
||||||
|
from ..mapdata.models import MapLevel, MapPackage, MapSource
|
||||||
|
|
||||||
|
|
||||||
@staff_member_required
|
@staff_member_required
|
||||||
|
@ -13,10 +14,12 @@ def dashboard(request):
|
||||||
@staff_member_required
|
@staff_member_required
|
||||||
def editor(request, level=None):
|
def editor(request, level=None):
|
||||||
if not level:
|
if not level:
|
||||||
return redirect('control.editor', level=mapmanager.levels[0])
|
return redirect('control.editor', level=MapLevel.objects.first().name)
|
||||||
if level not in mapmanager.levels:
|
|
||||||
raise Http404('Level does not exist')
|
level = get_object_or_404(MapLevel, name=level)
|
||||||
return render(request, 'control/editor.html', {
|
return render(request, 'control/editor.html', {
|
||||||
'map': mapmanager,
|
'bounds': json.dumps(MapSource.max_bounds()),
|
||||||
|
'packages': MapPackage.objects.all(),
|
||||||
|
'levels': MapLevel.objects.all(),
|
||||||
'current_level': level,
|
'current_level': level,
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
from .classes import MapManager
|
|
||||||
|
|
||||||
default_app_config = 'c3nav.mapdata.apps.MapdataConfig'
|
|
||||||
|
|
||||||
mapmanager = MapManager()
|
|
|
@ -1,25 +0,0 @@
|
||||||
from django.apps import AppConfig
|
|
||||||
from django.conf import settings
|
|
||||||
from django.core.checks import Warning, register
|
|
||||||
|
|
||||||
from . import mapmanager
|
|
||||||
|
|
||||||
|
|
||||||
@register()
|
|
||||||
def has_map_data_check(app_configs, **kwargs):
|
|
||||||
if not settings.MAP_DIRS:
|
|
||||||
return [Warning(
|
|
||||||
'There are no map data directories configured.',
|
|
||||||
hint='Add mapdirs=/path/to/directory to your c3nav.cfg.',
|
|
||||||
id='mapdata.W001',
|
|
||||||
)]
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
class MapdataConfig(AppConfig):
|
|
||||||
name = 'c3nav.mapdata'
|
|
||||||
verbose_name = 'map data manager'
|
|
||||||
|
|
||||||
def ready(self):
|
|
||||||
for map_dir in settings.MAP_DIRS:
|
|
||||||
mapmanager.add_map_dir(map_dir)
|
|
|
@ -1,76 +0,0 @@
|
||||||
import json
|
|
||||||
import os
|
|
||||||
from collections import OrderedDict
|
|
||||||
|
|
||||||
|
|
||||||
class MapInitError(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class MapManager:
|
|
||||||
def __init__(self):
|
|
||||||
self.main_pkg = None
|
|
||||||
self.pkgs = OrderedDict()
|
|
||||||
self.levels = []
|
|
||||||
self.sources_by_filename = OrderedDict()
|
|
||||||
|
|
||||||
def add_map_dir(self, path):
|
|
||||||
pkg = MapDataPackage(path)
|
|
||||||
if pkg.name in self.pkgs:
|
|
||||||
raise MapInitError('Duplicate map package: '+pkg.name)
|
|
||||||
|
|
||||||
if pkg.extends is None:
|
|
||||||
if self.main_pkg is not None:
|
|
||||||
raise MapInitError('There can not be more than one root map package: tried to add '+pkg.name+', '
|
|
||||||
'but '+self.main_pkg.name+' was there first.')
|
|
||||||
self.main_pkg = pkg
|
|
||||||
self.levels = pkg.levels
|
|
||||||
self.width = pkg.width
|
|
||||||
self.height = pkg.height
|
|
||||||
else:
|
|
||||||
if pkg.extends not in self.pkgs:
|
|
||||||
raise MapInitError('map package'+pkg.name+' extends '+pkg.exends+', which was not imported '
|
|
||||||
'beforehand.')
|
|
||||||
|
|
||||||
for source in pkg.sources:
|
|
||||||
self.sources_by_filename[source.filename] = source
|
|
||||||
|
|
||||||
self.pkgs[pkg.name] = pkg
|
|
||||||
|
|
||||||
|
|
||||||
class MapDataPackage:
|
|
||||||
def __init__(self, path):
|
|
||||||
self.path = path
|
|
||||||
|
|
||||||
main_file = os.path.join(path, 'map.json')
|
|
||||||
try:
|
|
||||||
data = json.load(open(main_file))
|
|
||||||
except FileNotFoundError:
|
|
||||||
raise MapInitError(main_file+' not found')
|
|
||||||
except json.decoder.JSONDecodeError as e:
|
|
||||||
raise MapInitError('Could not decode '+main_file+': '+str(e))
|
|
||||||
|
|
||||||
self.name = data.get('name')
|
|
||||||
if self.name is None:
|
|
||||||
raise MapInitError('Map package '+path+' has no name in map.json.')
|
|
||||||
|
|
||||||
self.extends = data.get('extends')
|
|
||||||
|
|
||||||
self.width = data.get('width')
|
|
||||||
self.height = data.get('height')
|
|
||||||
|
|
||||||
self.sources = tuple(MapSource(self, source) for source in data.get('sources', []))
|
|
||||||
|
|
||||||
self.levels = data.get('levels')
|
|
||||||
|
|
||||||
|
|
||||||
class MapSource:
|
|
||||||
def __init__(self, pkg, data):
|
|
||||||
self.name = data['name']
|
|
||||||
self.filename = self.name+'.'+data['src'].split('.')[-1]
|
|
||||||
self.src = os.path.join(pkg.path, data['src'])
|
|
||||||
self.bounds = data['bounds']
|
|
||||||
|
|
||||||
@property
|
|
||||||
def jsbounds(self):
|
|
||||||
return json.dumps(self.bounds)
|
|
0
src/c3nav/mapdata/management/__init__.py
Normal file
0
src/c3nav/mapdata/management/__init__.py
Normal file
0
src/c3nav/mapdata/management/commands/__init__.py
Normal file
0
src/c3nav/mapdata/management/commands/__init__.py
Normal file
19
src/c3nav/mapdata/management/commands/loadmappkgs.py
Normal file
19
src/c3nav/mapdata/management/commands/loadmappkgs.py
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
|
from django.db import transaction
|
||||||
|
from ...packageio import MapPackagesIO
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = 'Load the given map packages into the database'
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument('mappkgdir', nargs='+', type=str, help='map package directories')
|
||||||
|
parser.add_argument('-y', action='store_const', const=True, default=False,
|
||||||
|
help='don\'t ask for confirmation')
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
with transaction.atomic():
|
||||||
|
MapPackagesIO(options['mappkgdir']).update_to_db()
|
||||||
|
print()
|
||||||
|
if input('Confirm (y/N): ') != 'y':
|
||||||
|
raise CommandError('Aborted.')
|
|
@ -1,7 +1,8 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Generated by Django 1.9.9 on 2016-08-25 11:37
|
# Generated by Django 1.9.9 on 2016-08-28 15:52
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import c3nav.mapdata.models
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
import parler.models
|
import parler.models
|
||||||
|
@ -16,72 +17,78 @@ class Migration(migrations.Migration):
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='Bounds',
|
name='MapFeature',
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
('bottom', models.DecimalField(decimal_places=2, max_digits=6, verbose_name='bottom coordinate')),
|
('name', models.CharField(help_text='e.g. noc', max_length=50, unique=True, verbose_name='feature identifier')),
|
||||||
('left', models.DecimalField(decimal_places=2, max_digits=6, verbose_name='left coordinate')),
|
('type', models.CharField(choices=[('building', 'Building'), ('room', 'Room'), ('obstacle', 'Obstacle')], max_length=50)),
|
||||||
('top', models.DecimalField(decimal_places=2, max_digits=6, verbose_name='top coordinate')),
|
('geometry', models.TextField()),
|
||||||
('right', models.DecimalField(decimal_places=2, max_digits=6, verbose_name='right coordinate')),
|
|
||||||
],
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
bases=(parler.models.TranslatableModelMixin, models.Model),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='MapFeatureTranslation',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('language_code', models.CharField(db_index=True, max_length=15, verbose_name='Language')),
|
||||||
|
('title', models.CharField(max_length=50, verbose_name='package title')),
|
||||||
|
('master', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='translations', to='mapdata.MapFeature')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'managed': True,
|
||||||
|
'db_tablespace': '',
|
||||||
|
'verbose_name': 'map feature Translation',
|
||||||
|
'db_table': 'mapdata_mapfeature_translation',
|
||||||
|
'default_permissions': (),
|
||||||
|
},
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='MapLevel',
|
name='MapLevel',
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
('name', models.CharField(help_text='Usually just an integer (e.g. -1, 0, 1, 2)', max_length=50, unique=True, verbose_name='level name')),
|
('name', models.CharField(help_text='Usually just an integer (e.g. -1, 0, 1, 2)', max_length=50, unique=True, verbose_name='level name')),
|
||||||
|
('altitude', models.DecimalField(decimal_places=2, max_digits=6, null=True, verbose_name='level altitude')),
|
||||||
],
|
],
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
},
|
|
||||||
bases=(parler.models.TranslatableModelMixin, models.Model),
|
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='MapPackage',
|
name='MapPackage',
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
('name', models.CharField(help_text='e.g. de.c3nav.33c3.base', max_length=50, unique=True, verbose_name='package identifier')),
|
('name', models.CharField(help_text='e.g. de.c3nav.33c3.base', max_length=50, unique=True, verbose_name='package identifier')),
|
||||||
('map', models.CharField(help_text='e.g. de.c3nav.33c3', max_length=50, verbose_name='map identifier')),
|
('bottom', models.DecimalField(decimal_places=2, max_digits=6, null=True, verbose_name='bottom coordinate')),
|
||||||
('bounds', models.OneToOneField(null=True, on_delete=django.db.models.deletion.PROTECT, to='mapdata.Bounds', verbose_name='bounds')),
|
('left', models.DecimalField(decimal_places=2, max_digits=6, null=True, verbose_name='left coordinate')),
|
||||||
|
('top', models.DecimalField(decimal_places=2, max_digits=6, null=True, verbose_name='top coordinate')),
|
||||||
|
('right', models.DecimalField(decimal_places=2, max_digits=6, null=True, verbose_name='right coordinate')),
|
||||||
],
|
],
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
},
|
|
||||||
bases=(parler.models.TranslatableModelMixin, models.Model),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='MapPackageTranslation',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('language_code', models.CharField(db_index=True, max_length=15, verbose_name='Language')),
|
|
||||||
('title', models.CharField(max_length=50, verbose_name='package title')),
|
|
||||||
('master', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='translations', to='mapdata.MapPackage')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'db_tablespace': '',
|
|
||||||
'verbose_name': 'map package Translation',
|
|
||||||
'managed': True,
|
|
||||||
'db_table': 'mapdata_mappackage_translation',
|
|
||||||
'default_permissions': (),
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='MapSource',
|
name='MapSource',
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
('name', models.SlugField(unique=True, verbose_name='source name')),
|
('name', models.SlugField(unique=True, verbose_name='source name')),
|
||||||
('image', models.FileField(upload_to='mapsources/', verbose_name='source image')),
|
('image', models.FileField(max_length=70, storage=c3nav.mapdata.models.MapSourceImageStorage(), upload_to=c3nav.mapdata.models.map_source_filename, verbose_name='source image')),
|
||||||
('bounds', models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, to='mapdata.Bounds', verbose_name='bounds')),
|
('bottom', models.DecimalField(decimal_places=2, max_digits=6, verbose_name='bottom coordinate')),
|
||||||
('package', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='sources', to='mapdata.MapPackage', verbose_name='map package')),
|
('left', models.DecimalField(decimal_places=2, max_digits=6, verbose_name='left coordinate')),
|
||||||
|
('top', models.DecimalField(decimal_places=2, max_digits=6, verbose_name='top coordinate')),
|
||||||
|
('right', models.DecimalField(decimal_places=2, max_digits=6, verbose_name='right coordinate')),
|
||||||
|
('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sources', to='mapdata.MapPackage', verbose_name='map package')),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='maplevel',
|
model_name='maplevel',
|
||||||
name='package',
|
name='package',
|
||||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='levels', to='mapdata.MapPackage', verbose_name='map package'),
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='levels', to='mapdata.MapPackage', verbose_name='map package'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='mapfeature',
|
||||||
|
name='package',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='features', to='mapdata.MapPackage', verbose_name='map package'),
|
||||||
),
|
),
|
||||||
migrations.AlterUniqueTogether(
|
migrations.AlterUniqueTogether(
|
||||||
name='mappackagetranslation',
|
name='mapfeaturetranslation',
|
||||||
unique_together=set([('language_code', 'master')]),
|
unique_together=set([('language_code', 'master')]),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,50 +1,122 @@
|
||||||
from django.db import models
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import models, transaction
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
from django.dispatch import receiver
|
||||||
|
from django.core.files.storage import FileSystemStorage
|
||||||
|
|
||||||
from parler.models import TranslatedFields, TranslatableModel
|
from parler.models import TranslatedFields, TranslatableModel
|
||||||
|
|
||||||
|
|
||||||
class Bounds(models.Model):
|
class MapPackage(models.Model):
|
||||||
bottom = models.DecimalField(_('bottom coordinate'), max_digits=6, decimal_places=2)
|
|
||||||
left = models.DecimalField(_('left coordinate'), max_digits=6, decimal_places=2)
|
|
||||||
top = models.DecimalField(_('top coordinate'), max_digits=6, decimal_places=2)
|
|
||||||
right = models.DecimalField(_('right coordinate'), max_digits=6, decimal_places=2)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def __iter__(self):
|
|
||||||
return iter(((self.bottom, self.left), (self.top, self.right)))
|
|
||||||
|
|
||||||
|
|
||||||
class MapPackage(TranslatableModel):
|
|
||||||
"""
|
"""
|
||||||
A c3nav map package
|
A c3nav map package
|
||||||
"""
|
"""
|
||||||
name = models.CharField(_('package identifier'), unique=True, max_length=50,
|
name = models.CharField(_('package identifier'), unique=True, max_length=50,
|
||||||
help_text=_('e.g. de.c3nav.33c3.base'))
|
help_text=_('e.g. de.c3nav.33c3.base'))
|
||||||
map = models.CharField(_('map identifier'), max_length=50, help_text=_('e.g. de.c3nav.33c3'))
|
|
||||||
bounds = models.OneToOneField('Bounds', null=True, on_delete=models.PROTECT, verbose_name=_('bounds'))
|
|
||||||
|
|
||||||
translations = TranslatedFields(
|
bottom = models.DecimalField(_('bottom coordinate'), null=True, max_digits=6, decimal_places=2)
|
||||||
title=models.CharField(_('package title'), max_length=50),
|
left = models.DecimalField(_('left coordinate'), null=True, max_digits=6, decimal_places=2)
|
||||||
)
|
top = models.DecimalField(_('top coordinate'), null=True, max_digits=6, decimal_places=2)
|
||||||
|
right = models.DecimalField(_('right coordinate'), null=True, max_digits=6, decimal_places=2)
|
||||||
|
|
||||||
|
|
||||||
class MapLevel(TranslatableModel):
|
class MapLevel(models.Model):
|
||||||
"""
|
"""
|
||||||
A map level (-1, 0, 1, 2…)
|
A map level (-1, 0, 1, 2…)
|
||||||
"""
|
"""
|
||||||
name = models.CharField(_('level name'), max_length=50, unique=True,
|
name = models.CharField(_('level name'), max_length=50, unique=True,
|
||||||
help_text=_('Usually just an integer (e.g. -1, 0, 1, 2)'))
|
help_text=_('Usually just an integer (e.g. -1, 0, 1, 2)'))
|
||||||
package = models.ForeignKey('MapPackage', on_delete=models.PROTECT, related_name='levels',
|
altitude = models.DecimalField(_('level altitude'), null=True, max_digits=6, decimal_places=2)
|
||||||
|
package = models.ForeignKey('MapPackage', on_delete=models.CASCADE, related_name='levels',
|
||||||
verbose_name=_('map package'))
|
verbose_name=_('map package'))
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['altitude']
|
||||||
|
|
||||||
|
|
||||||
|
class MapSourceImageStorage(FileSystemStorage):
|
||||||
|
def get_available_name(self, name, *args, max_length=None, **kwargs):
|
||||||
|
if self.exists(name):
|
||||||
|
os.remove(os.path.join(settings.MEDIA_ROOT, name))
|
||||||
|
return super().get_available_name(name, *args, max_length, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def map_source_filename(instance, filename):
|
||||||
|
return os.path.join('mapsources', '%s.%s' % (instance.name, filename.split('.')[-1]))
|
||||||
|
|
||||||
|
|
||||||
class MapSource(models.Model):
|
class MapSource(models.Model):
|
||||||
"""
|
"""
|
||||||
A map source, images of levels that can be useful as backgrounds for the map editor
|
A map source, images of levels that can be useful as backgrounds for the map editor
|
||||||
"""
|
"""
|
||||||
name = models.SlugField(_('source name'), max_length=50, unique=True)
|
name = models.SlugField(_('source name'), max_length=50, unique=True)
|
||||||
package = models.ForeignKey('MapPackage', on_delete=models.PROTECT, related_name='sources',
|
package = models.ForeignKey('MapPackage', on_delete=models.CASCADE, related_name='sources',
|
||||||
verbose_name=_('map package'))
|
verbose_name=_('map package'))
|
||||||
image = models.FileField(_('source image'), upload_to='mapsources/')
|
|
||||||
bounds = models.OneToOneField('Bounds', on_delete=models.PROTECT, verbose_name=_('bounds'))
|
image = models.FileField(_('source image'), max_length=70,
|
||||||
|
upload_to=map_source_filename, storage=MapSourceImageStorage())
|
||||||
|
|
||||||
|
bottom = models.DecimalField(_('bottom coordinate'), max_digits=6, decimal_places=2)
|
||||||
|
left = models.DecimalField(_('left coordinate'), max_digits=6, decimal_places=2)
|
||||||
|
top = models.DecimalField(_('top coordinate'), max_digits=6, decimal_places=2)
|
||||||
|
right = models.DecimalField(_('right coordinate'), max_digits=6, decimal_places=2)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def max_bounds(cls):
|
||||||
|
result = cls.objects.all().aggregate(models.Min('bottom'), models.Min('left'),
|
||||||
|
models.Max('top'), models.Max('right'))
|
||||||
|
return ((float(result['bottom__min']), float(result['left__min'])),
|
||||||
|
(float(result['top__max']), float(result['right__max'])))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def bounds(self):
|
||||||
|
return ((self.bottom, self.left), (self.top, self.right))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def jsbounds(self):
|
||||||
|
return json.dumps(((float(self.bottom), float(self.left)), (float(self.top), float(self.right))))
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(models.signals.post_delete, sender=MapSource)
|
||||||
|
def delete_image_on_mapsource_delete(sender, instance, **kwargs):
|
||||||
|
transaction.on_commit(lambda: instance.image.delete(save=False))
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(models.signals.pre_save, sender=MapSource)
|
||||||
|
def delete_image_on_mapsource_change(sender, instance, **kwargs):
|
||||||
|
if not instance.pk:
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
old_file = MapSource.objects.get(pk=instance.pk).image
|
||||||
|
except MapSource.DoesNotExist:
|
||||||
|
return False
|
||||||
|
|
||||||
|
new_file = instance.image
|
||||||
|
|
||||||
|
if map_source_filename(instance, new_file.name) != old_file.name:
|
||||||
|
transaction.on_commit(lambda: old_file.delete(save=False))
|
||||||
|
|
||||||
|
|
||||||
|
class MapFeature(TranslatableModel):
|
||||||
|
"""
|
||||||
|
A map feature
|
||||||
|
"""
|
||||||
|
TYPES = (
|
||||||
|
('building', _('Building')),
|
||||||
|
('room', _('Room')),
|
||||||
|
('obstacle', _('Obstacle')),
|
||||||
|
)
|
||||||
|
|
||||||
|
name = models.CharField(_('feature identifier'), unique=True, max_length=50, help_text=_('e.g. noc'))
|
||||||
|
package = models.ForeignKey('MapPackage', on_delete=models.CASCADE, related_name='features',
|
||||||
|
verbose_name=_('map package'))
|
||||||
|
type = models.CharField(max_length=50, choices=TYPES)
|
||||||
|
geometry = models.TextField()
|
||||||
|
|
||||||
|
translations = TranslatedFields(
|
||||||
|
title=models.CharField(_('package title'), max_length=50),
|
||||||
|
)
|
||||||
|
|
132
src/c3nav/mapdata/packageio.py
Normal file
132
src/c3nav/mapdata/packageio.py
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
from collections import OrderedDict
|
||||||
|
from django.core.files import File
|
||||||
|
|
||||||
|
from django.core.management.base import CommandError
|
||||||
|
|
||||||
|
|
||||||
|
class PackageIOError(CommandError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class MapPackagesIO():
|
||||||
|
def __init__(self, directories):
|
||||||
|
print('Opening Map Packages…')
|
||||||
|
self.packages = OrderedDict()
|
||||||
|
self.levels = OrderedDict()
|
||||||
|
self.sources = OrderedDict()
|
||||||
|
|
||||||
|
for directory in directories:
|
||||||
|
print('- '+directory)
|
||||||
|
|
||||||
|
try:
|
||||||
|
package = json.load(open(os.path.join(directory, 'pkg.json')))
|
||||||
|
except FileNotFoundError:
|
||||||
|
raise PackageIOError('no pkg.json found in %s' % directory)
|
||||||
|
|
||||||
|
if package['name'] in self.packages:
|
||||||
|
raise PackageIOError('Duplicate package name: %s' % package['name'])
|
||||||
|
|
||||||
|
if 'bounds' in package:
|
||||||
|
self._validate_bounds(package['bounds'])
|
||||||
|
|
||||||
|
package['directory'] = directory
|
||||||
|
self.packages[package['name']] = package
|
||||||
|
|
||||||
|
for level in package.get('levels', []):
|
||||||
|
level = level.copy()
|
||||||
|
if level['name'] in self.levels:
|
||||||
|
raise PackageIOError('Duplicate level name: %s in packages %s and %s' %
|
||||||
|
(level, self.levels[level]['name'], package['name']))
|
||||||
|
|
||||||
|
if not isinstance(level['altitude'], (int, float)):
|
||||||
|
raise PackageIOError('levels: %s: altitude has to be int or float.' % level['name'])
|
||||||
|
|
||||||
|
level['package'] = package['name']
|
||||||
|
self.levels[level['name']] = level
|
||||||
|
|
||||||
|
for source in package.get('sources', []):
|
||||||
|
source = source.copy()
|
||||||
|
if source['name'] in self.sources:
|
||||||
|
raise PackageIOError('Duplicate source name: %s in packages %s and %s' %
|
||||||
|
(source['name'], self.sources[source['name']]['name'], package['name']))
|
||||||
|
|
||||||
|
self._validate_bounds(source['bounds'], 'sources: %s: ' % source['name'])
|
||||||
|
|
||||||
|
source['filename'] = os.path.join(directory, source['src'])
|
||||||
|
if not os.path.isfile(source['filename']):
|
||||||
|
raise PackageIOError('Source file not found: '+source['filename'])
|
||||||
|
|
||||||
|
source['package'] = package['name']
|
||||||
|
self.sources[source['name']] = source
|
||||||
|
|
||||||
|
def _validate_bounds(self, bounds, prefix=''):
|
||||||
|
if len(bounds) != 2 or len(bounds[0]) != 2 or len(bounds[1]) != 2:
|
||||||
|
raise PackageIOError(prefix+'Invalid bounds format.')
|
||||||
|
if not all(isinstance(i, (float, int)) for i in sum(bounds, [])):
|
||||||
|
raise PackageIOError(prefix+'All bounds coordinates have to be int or float.')
|
||||||
|
if bounds[0][0] >= bounds[1][0] or bounds[0][1] >= bounds[1][1]:
|
||||||
|
raise PackageIOError(prefix+'bounds: lower coordinate has to be first.')
|
||||||
|
|
||||||
|
def update_to_db(self):
|
||||||
|
from .models import MapPackage, MapLevel, MapSource
|
||||||
|
print('Updating Map database…')
|
||||||
|
|
||||||
|
# Add new Packages
|
||||||
|
packages = {}
|
||||||
|
print('- Updating packages…')
|
||||||
|
for name, package in self.packages.items():
|
||||||
|
bounds = package.get('bounds')
|
||||||
|
defaults = {
|
||||||
|
'bottom': bounds[0][0],
|
||||||
|
'left': bounds[0][1],
|
||||||
|
'top': bounds[1][0],
|
||||||
|
'right': bounds[1][1],
|
||||||
|
} if bounds else {}
|
||||||
|
|
||||||
|
package, created = MapPackage.objects.update_or_create(name=name, defaults=defaults)
|
||||||
|
packages[name] = package
|
||||||
|
if created:
|
||||||
|
print('- Created package: '+name)
|
||||||
|
|
||||||
|
# Add new levels
|
||||||
|
print('- Updating levels…')
|
||||||
|
for name, level in self.levels.items():
|
||||||
|
package, created = MapLevel.objects.update_or_create(name=name, defaults={
|
||||||
|
'package': packages[level['package']],
|
||||||
|
'altitude': level['altitude'],
|
||||||
|
'name': level['name'],
|
||||||
|
})
|
||||||
|
if created:
|
||||||
|
print('- Created level: '+name)
|
||||||
|
|
||||||
|
# Add new map sources
|
||||||
|
print('- Updating sources…')
|
||||||
|
for name, source in self.sources.items():
|
||||||
|
source, created = MapSource.objects.update_or_create(name=name, defaults={
|
||||||
|
'package': packages[source['package']],
|
||||||
|
'image': File(open(source['filename'], 'rb')),
|
||||||
|
'bottom': source['bounds'][0][0],
|
||||||
|
'left': source['bounds'][0][1],
|
||||||
|
'top': source['bounds'][1][0],
|
||||||
|
'right': source['bounds'][1][1],
|
||||||
|
})
|
||||||
|
if created:
|
||||||
|
print('- Created source: '+name)
|
||||||
|
|
||||||
|
# Remove old sources
|
||||||
|
for source in MapSource.objects.exclude(name__in=self.sources.keys()):
|
||||||
|
print('- Deleted source: '+source.name)
|
||||||
|
source.delete()
|
||||||
|
|
||||||
|
# Remove old levels
|
||||||
|
for level in MapLevel.objects.exclude(name__in=self.levels.keys()):
|
||||||
|
print('- Deleted level: '+level.name)
|
||||||
|
level.delete()
|
||||||
|
|
||||||
|
# Remove old packages
|
||||||
|
for package in MapPackage.objects.exclude(name__in=self.packages.keys()):
|
||||||
|
print('- Deleted package: '+package.name)
|
||||||
|
package.delete()
|
|
@ -3,5 +3,6 @@ from django.conf.urls import url
|
||||||
from . import views
|
from . import views
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^sources/(?P<filename>[^/]+)$', views.source, name='map.source'),
|
url(r'^sources/(?P<source>[^/]+)$', views.source, name='map.source'),
|
||||||
|
url(r'^data/add$', views.source, name='map.edit.source'),
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,19 +1,15 @@
|
||||||
import mimetypes
|
import mimetypes
|
||||||
|
|
||||||
from django.contrib.admin.views.decorators import staff_member_required
|
from django.contrib.admin.views.decorators import staff_member_required
|
||||||
from django.http import Http404, HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.shortcuts import render
|
from django.shortcuts import get_object_or_404, render
|
||||||
|
from .models import MapSource
|
||||||
from ..mapdata import mapmanager
|
|
||||||
|
|
||||||
|
|
||||||
@staff_member_required
|
@staff_member_required
|
||||||
def source(request, filename):
|
def source(request, source):
|
||||||
source = mapmanager.sources_by_filename.get(filename)
|
source = get_object_or_404(MapSource, name=source)
|
||||||
if source is None:
|
response = HttpResponse(content_type=mimetypes.guess_type(source.image.name)[0])
|
||||||
raise Http404('Source does not exist')
|
for chunk in source.image.chunks():
|
||||||
|
response.write(chunk)
|
||||||
response = HttpResponse(content_type=mimetypes.guess_type(source.src)[0])
|
|
||||||
with open(source.src, 'rb') as f:
|
|
||||||
response.write(f.read())
|
|
||||||
return response
|
return response
|
||||||
|
|
|
@ -18,8 +18,6 @@ DATA_DIR = config.get('c3nav', 'datadir', fallback=os.environ.get('DATA_DIR', 'd
|
||||||
LOG_DIR = os.path.join(DATA_DIR, 'logs')
|
LOG_DIR = os.path.join(DATA_DIR, 'logs')
|
||||||
MEDIA_ROOT = os.path.join(DATA_DIR, 'media')
|
MEDIA_ROOT = os.path.join(DATA_DIR, 'media')
|
||||||
|
|
||||||
MAP_DIRS = tuple(n for n in config.get('c3nav', 'mapdirs', fallback='').split(',') if n)
|
|
||||||
|
|
||||||
if not os.path.exists(DATA_DIR):
|
if not os.path.exists(DATA_DIR):
|
||||||
os.mkdir(DATA_DIR)
|
os.mkdir(DATA_DIR)
|
||||||
if not os.path.exists(LOG_DIR):
|
if not os.path.exists(LOG_DIR):
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue