diff --git a/src/c3nav/mapdata/management/commands/importsvg.py b/src/c3nav/mapdata/management/commands/importsvg.py new file mode 100644 index 00000000..082d7f27 --- /dev/null +++ b/src/c3nav/mapdata/management/commands/importsvg.py @@ -0,0 +1,182 @@ +import argparse +import logging +import re +from xml.etree import ElementTree + +from django.core.management.base import BaseCommand, CommandError +from django.utils.translation import ugettext_lazy as _ +from shapely.affinity import scale, translate +from shapely.geometry import Polygon + +from c3nav.mapdata.models import Area, MapUpdate, Obstacle, Space +from c3nav.mapdata.utils.cache.changes import changed_geometries + + +class Command(BaseCommand): + help = 'render the map' + + @staticmethod + def space_value(value): + try: + space = Space.objects.get(pk=value) + except Space.DoesNotExist: + raise argparse.ArgumentTypeError( + _('unknown space') + ) + return space + + def add_arguments(self, parser): + parser.add_argument('svgfile', type=argparse.FileType('r'), help=_('svg file to import')) + parser.add_argument('name', type=str, help=_('name of the import')) + parser.add_argument('--type', type=str, required=True, choices=('areas', 'obstacles'), + help=_('type of objects to create')) + parser.add_argument('--space', type=self.space_value, required=True, + help=_('space to add the objects to')) + parser.add_argument('--minx', type=float, required=True, + help=_('minimum x coordinate, everthing left of it will be cropped')) + parser.add_argument('--miny', type=float, required=True, + help=_('minimum y coordinate, everthing below it will be cropped')) + parser.add_argument('--maxx', type=float, required=True, + help=_('maximum x coordinate, everthing right of it will be cropped')) + parser.add_argument('--maxy', type=float, required=True, + help=_('maximum y coordinate, everthing above it will be cropped')) + + @staticmethod + def parse_svg_data(data): + first = False + + last_point = (0, 0) + last_end_point = None + + done_subpaths = [] + current_subpath = [] + while data: + data = data.lstrip().replace(',', ' ') + command = data[0] + if first and command not in 'Mm': + raise ValueError('path data has to start with moveto command.') + data = data[1:].lstrip() + first = False + + numbers = [] + while True: + match = re.match('^-?[0-9]+(\.[0-9]+)?(e-?[0-9]+)?', data) + if match is None: + break + numbers.append(float(match.group(0))) + data = data[len(match.group(0)):].lstrip() + + relative = command.islower() + if command in 'Mm': + if not len(numbers) or len(numbers) % 2: + raise ValueError('Invalid number of arguments for moveto command!') + numbers = iter(numbers) + first = True + for x, y in zip(numbers, numbers): + if relative: + x, y = last_point[0] + x, last_point[1] + y + if first: + first = False + if current_subpath: + done_subpaths.append(current_subpath) + last_end_point = current_subpath[-1] + current_subpath = [] + current_subpath.append((x, y)) + last_point = (x, y) + + elif command in 'Ll': + if not len(numbers) or len(numbers) % 2: + raise ValueError('Invalid number of arguments for lineto command!') + numbers = iter(numbers) + for x, y in zip(numbers, numbers): + if relative: + x, y = last_point[0] + x, last_point[1] + y + if not current_subpath: + current_subpath.append(last_end_point) + current_subpath.append((x, y)) + last_point = (x, y) + + elif command in 'Hh': + if not len(numbers): + raise ValueError('Invalid number of arguments for horizontal lineto command!') + y = last_point[1] + for x in numbers: + if relative: + x = last_point[0] + x + if not current_subpath: + current_subpath.append(last_end_point) + current_subpath.append((x, y)) + last_point = (x, y) + + elif command in 'Vv': + if not len(numbers): + raise ValueError('Invalid number of arguments for vertical lineto command!') + x = last_point[0] + for y in numbers: + if relative: + y = last_point[1] + y + if not current_subpath: + current_subpath.append(last_end_point) + current_subpath.append((x, y)) + last_point = (x, y) + + elif command in 'Zz': + if numbers: + raise ValueError('Invalid number of arguments for closepath command!') + current_subpath.append(current_subpath[0]) + done_subpaths.append(current_subpath) + last_end_point = current_subpath[-1] + current_subpath = [] + + else: + raise ValueError('unknown svg command: ' + command) + + if current_subpath: + done_subpaths.append(current_subpath) + return done_subpaths + + def handle(self, *args, **options): + minx = options['minx'] + miny = options['miny'] + maxx = options['maxx'] + maxy = options['maxy'] + + if minx >= maxx: + raise CommandError(_('minx has to be lower than maxx')) + if miny >= maxy: + raise CommandError(_('miny has to be lower than maxy')) + + width = maxx-minx + height = maxy-miny + + model = {'areas': Area, 'obstacles': Obstacle}[options['type']] + + namespaces = {'svg': 'http://www.w3.org/2000/svg'} + + svg = ElementTree.fromstring(options['svgfile'].read()) + svg_width = float(svg.attrib['width']) + svg_height = float(svg.attrib['height']) + + for element in svg.findall('.//svg:clipPath/..', namespaces): + for clippath in element.findall('./svg:clipPath', namespaces): + element.remove(clippath) + + if svg.findall('.//*[@transform]'): + raise CommandError(_('svg contains transform attributes. Use inkscape apply transforms!')) + + with MapUpdate.lock(): + changed_geometries.reset() + for path in svg.findall('.//svg:path', namespaces): + for polygon in self.parse_svg_data(path.attrib['d']): + polygon = Polygon(polygon) + polygon = scale(polygon, xfact=width / svg_width, yfact=height / svg_height, origin=(0, 0)) + polygon = translate(polygon, xoff=minx, yoff=miny) + obj = model(geometry=polygon, space=options['space'], import_tag=options['name']) + obj.save() + MapUpdate.objects.create(type='importsvg') + + logger = logging.getLogger('c3nav') + logger.info('Imported, map update created.') + logger.info('Next step: go into the shell and edit them using ' + '%s.objects.filter(space_id=%r, import_tag=%r)' % + (model.__name__, options['space'].pk, options['name']))