Parcourir la source

Initial commit (WIP)

Baptiste Jonglez il y a 10 ans
Parent
commit
b4aa8e6e56

+ 10 - 0
manage.py

@@ -0,0 +1,10 @@
+#!/usr/bin/env python
+import os
+import sys
+
+if __name__ == "__main__":
+    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "ztulec.settings")
+
+    from django.core.management import execute_from_command_line
+
+    execute_from_command_line(sys.argv)

+ 0 - 0
media/pano/.placeholder


+ 0 - 0
media/tiles/.placeholder


+ 0 - 0
panorama/__init__.py


+ 20 - 0
panorama/admin.py

@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.contrib import admin
+
+from .models import Panorama, ReferencePoint
+
+
+@admin.register(Panorama)
+class PanoramaAdmin(admin.ModelAdmin):
+    model = Panorama
+    list_display = ('name', 'latitude', 'longitude', 'altitude', 'loop')
+    fields = ('name', 'image', 'loop', ('latitude', 'longitude'), 'altitude')
+
+
+@admin.register(ReferencePoint)
+class ReferencePointAdmin(admin.ModelAdmin):
+    model = ReferencePoint
+    list_display = ('name', 'latitude', 'longitude', 'altitude')
+    fields = ('name', ('latitude', 'longitude'), 'altitude')

+ 119 - 0
panorama/gen_tiles.sh

@@ -0,0 +1,119 @@
+#!/bin/bash
+
+trap clean_tmp EXIT
+# fin d'eviter tout problème de locales en reste en C de base.
+
+set -e
+export LANG=C
+
+# pour éliminer systématiquement les fichier temporaires créés ic
+function clean_tmp() {
+    if [ -n "$wfname" ]; then
+	rm $wfname
+    fi
+    if [ -n "$tmp_file" ]; then
+	rm $tmp_file
+    fi
+}
+
+test_mode=false
+memory_limit=256
+crop_x=256
+crop_y=256
+min_scale=0
+max_scale=8
+usage="$0 [-x <x_tile_size>] [-y <y_tile_size>] [-p <prefix_result>] [-t] [-h] [-m <min_zoom>] [-M <max_zoom>] <image_to_convert>\n   example: $0 -r test_res"
+
+if ! which anytopnm pnmscale convert > /dev/null; then
+    echo "il faut installer les paquets netpbm et imageMagick pour utiliser ce script !"
+fi
+
+while getopts m:M:x:y:p:ht prs
+ do
+ case $prs in
+    t)        test_mode=true;;
+    x)        crop_x=$OPTARG;;
+    y)        crop_y=$OPTARG;;
+    m)        min_scale=$OPTARG;;
+    M)        max_scale=$OPTARG;;
+    p)        prefix=$OPTARG;;
+    \? | h)   echo -e $usage
+              exit 2;;
+ esac
+done
+shift `expr $OPTIND - 1`
+
+if [ -z "$1" ]; then
+    echo -e "usage :\n$usage"
+    exit 1
+elif [ ! -f "$1" ]; then
+    echo -e "le paramètre $1 ne correspond pas à un nom de fichier !"
+    exit 1
+fi
+
+fname=$1
+dir=$(dirname $fname)
+
+if [ -z "$prefix" ]; then
+    prefix=$(basename $1|sed 's/\..*$//')
+fi
+
+wfname=$(mktemp ${prefix}_XXXX.pnm)
+if ! $test_mode; then
+    anytopnm $fname > $wfname
+else
+    echo "anytopnm $fname > $wfname"
+fi
+
+echo "préfixe : "$prefix
+
+tmp_file=$(mktemp)
+
+for ((z=$min_scale; z <= $max_scale; z++))
+do
+    fprefix=${prefix}00$z
+    printf -v ratio %1.4lf $(echo "1 / (2^$z)" | bc -l)
+    echo génération du ratio $ratio
+    zwfname=$tmp_file
+
+    if $test_mode; then
+	if [ $ratio = 1.0000 ]; then
+	    zwfname=$wfname
+	else
+	    echo "pnmscale $ratio $wfname > $zwfname"
+	fi
+	echo convert $zwfname \
+	    -limit memory $memory_limit \
+            -crop ${crop_x}x${crop_x} \
+            -set filename:tile "%[fx:page.x/${crop_x}]_%[fx:page.y/${crop_y}]" \
+            +repage +adjoin "${fprefix}_%[filename:tile].jpg"
+    else
+	if [ $ratio = 1.0000 ]; then
+	    zwfname=$wfname
+	else
+	    if ! pnmscale $ratio $wfname > $zwfname; then
+		echo "operation 'pnmscale $ratio $wfname > $zwfname' en erreur"
+		exit 1
+	    fi
+	fi
+	if convert $zwfname \
+	    -limit memory $memory_limit \
+            -crop ${crop_x}x${crop_x} \
+            -set filename:tile "%[fx:page.x/${crop_x}]_%[fx:page.y/${crop_y}]" \
+            +repage +adjoin "${fprefix}_%[filename:tile].jpg"; then
+	    echo "Nombre des fichiers produits :" $(ls -la ${fprefix}_*| wc -l)
+	else
+	    echo "operation 'convert' en erreur"
+	    exit 2
+	fi
+    fi
+done
+
+if ! $test_mode; then
+## les lignes ci dessous sont destinnées à mettre des 0 en debut des numéros de ligne et de colonnes
+## Il y a certainement plus simple mais là c'est du rapide et efficace.
+    rename 's/_(\d\d)_(\d+\.jpg)$/_0$1_$2/' ${prefix}*
+    rename 's/_(\d)_(\d+\.jpg)$/_00$1_$2/' ${prefix}*
+    rename 's/_(\d+)_(\d\d)(\.jpg)$/_$1_0$2$3/' ${prefix}*
+    rename 's/_(\d+)_(\d)(\.jpg)$/_$1_00$2$3/' ${prefix}*
+fi

+ 44 - 0
panorama/migrations/0001_initial.py

@@ -0,0 +1,44 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+import django.core.validators
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='Panorama',
+            fields=[
+                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+                ('latitude', models.FloatField(help_text='In degrees', verbose_name='latitude')),
+                ('longitude', models.FloatField(help_text='In degrees', verbose_name='longitude')),
+                ('altitude', models.FloatField(help_text='In meters', verbose_name='altitude', validators=[django.core.validators.MinValueValidator(0.0)])),
+                ('name', models.CharField(help_text='Name of the panorama', max_length=255, verbose_name='name')),
+                ('loop', models.BooleanField(default=False, help_text='Whether the panorama loops around the edges', verbose_name='360\xb0 panorama')),
+                ('image', models.ImageField(upload_to=b'', verbose_name='image')),
+            ],
+            options={
+                'abstract': False,
+            },
+            bases=(models.Model,),
+        ),
+        migrations.CreateModel(
+            name='ReferencePoint',
+            fields=[
+                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+                ('latitude', models.FloatField(help_text='In degrees', verbose_name='latitude')),
+                ('longitude', models.FloatField(help_text='In degrees', verbose_name='longitude')),
+                ('altitude', models.FloatField(help_text='In meters', verbose_name='altitude', validators=[django.core.validators.MinValueValidator(0.0)])),
+                ('name', models.CharField(help_text='Name of the reference point', max_length=255, verbose_name='name')),
+            ],
+            options={
+                'abstract': False,
+            },
+            bases=(models.Model,),
+        ),
+    ]

+ 0 - 0
panorama/migrations/__init__.py


+ 120 - 0
panorama/models.py

@@ -0,0 +1,120 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+import subprocess
+import os
+from math import radians, degrees, sin, cos, atan2, sqrt
+
+from django.db import models
+from django.conf import settings
+from django.core.validators import MinValueValidator, MaxValueValidator
+from django.utils.encoding import python_2_unicode_compatible
+
+
+class Point(models.Model):
+    latitude = models.FloatField(verbose_name="latitude", help_text="In degrees",
+                                 validators=[MinValueValidator(-90),
+                                             MaxValueValidator(90)])
+    longitude = models.FloatField(verbose_name="longitude", help_text="In degrees",
+                                 validators=[MinValueValidator(-180),
+                                             MaxValueValidator(180)])
+    altitude = models.FloatField(verbose_name="altitude", help_text="In meters",
+                                 validators=[MinValueValidator(0.)])
+
+    def line_distance(self, other):
+        """Distance of the straight line between two points on Earth.
+
+        Note that this is only useful because we are considering
+        line-of-sight links, where straight-line distance is the relevant
+        distance.  For arbitrary points on Earth, great-circle distance
+        would most likely be preferred.
+        """
+        earth_radius = 6371009
+        lat, lon = radians(self.latitude), radians(self.longitude)
+        alt = earth_radius + self.altitude
+        lat2, lon2 = radians(other.latitude), radians(other.longitude)
+        alt2 = earth_radius + other.altitude
+        # Cosine of the angle between the two points on their great circle.
+        cos_angle = sin(lat) * sin(lat2) + cos(lat) * cos(lat2) * cos(lon2 - lon)
+        # Al-Kashi formula
+        return sqrt(alt ** 2 + alt2 ** 2 - 2 * alt * alt2 * cos_angle)
+
+    def bearing(self, other):
+        """Bearing, in degrees, between this point and another point."""
+        lat, lon = radians(self.latitude), radians(self.longitude)
+        lat2, lon2 = radians(other.latitude), radians(other.longitude)
+        y = sin(lon2 - lon) * cos(lat2)
+        x = cos(lat) * sin(lat2) - sin(lat) * cos(lat2) * cos(lon2 - lon)
+        return degrees(atan2(y, x))
+
+    def elevation(self, other):
+        """Elevation, in degrees, between this point and another point."""
+        
+
+    class Meta:
+        abstract = True
+
+
+@python_2_unicode_compatible
+class Panorama(Point):
+    name = models.CharField(verbose_name="name", max_length=255,
+                            help_text="Name of the panorama")
+    loop = models.BooleanField(default=False, verbose_name="360° panorama",
+                               help_text="Whether the panorama loops around the edges")
+    image = models.ImageField(verbose_name="image", upload_to="pano")
+
+    def tiles_dir(self):
+        return os.path.join(settings.MEDIA_ROOT, settings.PANORAMA_TILES_DIR,
+                            str(self.pk))
+
+    def tiles_url(self):
+        return os.path.join(settings.MEDIA_URL, settings.PANORAMA_TILES_DIR,
+                            str(self.pk))
+
+    def to_dict(self):
+        """Useful to pass information to the javascript code as JSON"""
+        return {"id": self.id,
+                "name": self.name,
+                "loop": self.loop,
+                "latitude": self.latitude,
+                "longitude": self.longitude,
+                "altitude": self.altitude,
+                "tiles_url": self.tiles_url()}
+
+    def generate_tiles(self):
+        # The trailing slash is necessary for the shell script.
+        tiles_dir = self.tiles_dir()  + "/"
+        try:
+            os.makedirs(tiles_dir)
+        except OSError:
+            pass
+        script = os.path.join(settings.BASE_DIR, "panorama", "gen_tiles.sh")
+        ret = subprocess.call([script, "-p", tiles_dir, self.image.path])
+        return ret
+
+    def __str__(self):
+        return self.name
+
+
+@python_2_unicode_compatible
+class ReferencePoint(Point):
+    name = models.CharField(verbose_name="name", max_length=255,
+                            help_text="Name of the reference point")
+
+    def to_dict(self):
+        """Useful to pass information to the javascript code as JSON"""
+        return {"id": self.id,
+                "name": self.name,
+                "latitude": self.latitude,
+                "longitude": self.longitude,
+                "altitude": self.altitude}
+
+    def to_dict_extended(self, point):
+        """Same as above, but also includes information relative
+        to the given point: bearing, azimuth, distance."""
+        d = self.to_dict()
+        d['distance'] = self.line_distance(point)
+        return d
+    
+    def __str__(self):
+        return self.name

+ 204 - 0
panorama/static/panorama/css/map.css

@@ -0,0 +1,204 @@
+* {
+    font-family:Arial, Verdana, sans-serif;
+    padding:0;
+    margin:0;
+}
+
+body {
+    overflow:hidden;
+}
+
+#mon-canvas {
+    background-color:#44F;
+    margin:auto;
+    display:block;
+}
+
+#info {
+    display:none;
+    background-color:#FF0;
+    border:1px red solid;
+    position:absolute;
+    margin:1em;
+    padding:0.2em 0.5em;
+    border-radius:0.5em;
+}
+
+#insert {
+    display:none;
+    background-color:#F88;
+    border:1px red solid;
+    position:absolute;
+}
+
+label {
+    display:block;
+    clear:both;
+}
+
+legend {
+    opacity:1;
+    background-color:#F88;
+    border:solid #F00 1px;
+    border-radius:0.5em;
+    padding:0 1em;
+    text-shadow:2px 2px 2px #000, -2px -2px 2px #000;
+}      
+
+legend:hover {
+    color:#FF0;
+    cursor:default;
+}
+
+img {vertical-align:middle}
+
+input {
+    vertical-align:middle;
+}
+
+input[type="number"] {
+    border-radius:1em;
+    display:block;
+    float:right;
+    width:10ex;
+}
+
+input[type="submit"],
+input[type="reset"],
+input[type="button"] {
+    border-radius:0.4rem;
+    padding:0.2em 1em;
+    margin:0.2em;
+}
+
+input[type="checkbox"] {
+    margin:0 1em;
+}
+
+#params {
+    padding:0.2em 0.5em;
+    border-radius:0.5em;
+    background-color:rgba(128,128,128,0.5);
+    border:solid #00F 1px;
+    position:absolute;
+    top:1em;
+    right:2em;
+    min-width:10em;
+    color:#FFF;
+}
+
+#params em {	
+    display:block;
+    float:right;
+    background-color:#88F;
+    border-radius:0.5em;
+    padding:0 0.5em;
+    color:#FF0;
+}
+
+#loca_show {
+    text-shadow:2px 2px 2px #000, -2px -2px 2px #000;
+    color:#FFF;
+    float:left;
+    position:absolute;
+    bottom:1em;
+    left:1em;
+    cursor:pointer;
+    padding:0.2em 0.5em;
+    padding:0 1em; 
+}
+
+#addParams {
+    text-shadow:2px 2px 2px #000, -2px -2px 2px #000;
+    color:#FFF;
+    position:absolute;
+    top:1em;
+    right:2em;
+    border:solid #F00 1px;
+    border-radius:0.5em;
+    padding:0.2em 0.5em;
+    background-color:#F88; 
+    padding:0 1em;
+}
+
+#addParams label {
+    border-radius:0.5em;
+    padding:0 1em;
+}
+
+#addParams label:hover {
+    color: #FF0; 
+}
+
+fieldset {
+    border:solid #F00 1px;
+    border-radius:0.5em;
+    padding:0 0.5em;
+    color:#FFF;
+    position:absolute;
+    background-color:rgba(128,128,128,0.5);
+}
+
+fieldset#control {
+    top:1em;
+    left:2em;
+    padding:0.2em 0.5em;
+}
+
+
+fieldset#locadraw {
+    bottom:1em;
+    left:1em;
+    visibility:hidden;
+    min-width:10em;
+}
+
+fieldset#adding {
+    top:1em;
+    right:2em;
+    visibility: hidden;
+}
+
+#form_param label {	
+    text-shadow:2px 2px 2px #000, -2px -2px 2px #000;
+}
+ 
+
+input:focus {
+  background-color:#004;
+  color:#FFF;
+  border-color:#F00;
+}
+ 
+.validators {
+    text-align:center;
+    background-color:rgba(50%,50%,50%,0.5);
+    border:solid 1px #F55;
+    border-radius:0.5em;
+    border-width:1px;
+    position:absolute;
+    bottom:1em;
+    left:45%;
+    padding:0.2em 0.5em;
+    font-size:80%;
+}
+
+.validators img {height:1em}
+
+#res {
+    padding:0.2em 0.4em;
+    color:#FF8;
+    position:absolute;
+    bottom:1em;
+    right:2em;
+    background-color:rgba(0,0,0,0.3);
+    border:solid #FFF 1px;
+}
+
+#res:empty {display:none}
+
+#res li {
+    list-style-type: none;
+    color:#FFF;
+    background-color:rgba(100,0,0,0.5);
+}

+ 37 - 0
panorama/static/panorama/img/locapoint.svg

@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   version="1.1"
+   width="34.368469"
+   height="63.25069"
+   id="svg2">
+  <metadata
+     id="metadata8">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <defs
+     id="defs6" />
+  <path
+     d="M 17.22788,62.750691 C 16.126185,55.865098 17.254492,46.698747 6.4863542,33.14264 -4.2817838,19.586533 -0.41736984,0.86394177 17.22788,0.50492817 34.873129,0.14591457 38.539189,19.479763 27.969405,33.14264 17.399621,46.805517 17.503304,55.589674 17.22788,62.750691 z"
+     id="path2987"
+     style="fill:#fa7a6f;fill-opacity:1;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+  <path
+     d="m 27.542374,21.069916 a 8.125,8.675848 0 1 1 -16.25,0 8.125,8.675848 0 1 1 16.25,0 z"
+     transform="translate(-2.2331398,-2.2493091)"
+     id="path2989"
+     style="fill:#000000;fill-opacity:1;stroke:none" />
+</svg>

BIN
panorama/static/panorama/img/plus_photo.png


BIN
panorama/static/panorama/img/ptref.png


+ 50 - 0
panorama/static/panorama/img/tetaneutral.svg

@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   version="1.1"
+   width="371.90289"
+   height="55.950001"
+   id="svg2">
+  <metadata
+     id="metadata8">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <defs
+     id="defs6" />
+  <rect
+     width="371.90289"
+     height="55.950001"
+     ry="0"
+     x="0"
+     y="0"
+     id="rect2993"
+     style="opacity:0.91000001;fill:#fffbf4;fill-opacity:1;stroke:none" />
+  <text
+     x="22.015928"
+     y="43.308048"
+     transform="scale(0.97381679,1.0268872)"
+     id="text2987"
+     xml:space="preserve"
+     style="font-size:43.09389877px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#b2a58d;fill-opacity:1;stroke:none;font-family:Sans"><tspan
+       x="22.015928"
+       y="43.308048"
+       id="tspan2989"><tspan
+   id="tspan2991"
+   style="font-size:43.09389877px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#b2a58d;fill-opacity:1;font-family:Sans;-inkscape-font-specification:Sans Bold">teta</tspan><tspan
+   id="tspan3763"
+   style="fill:#e8d7bc;fill-opacity:1">neutral</tspan>.net</tspan></text>
+</svg>

BIN
panorama/static/panorama/img/tsf.png


Fichier diff supprimé car celui-ci est trop grand
+ 366 - 0
panorama/static/panorama/img/valid_css.svg


Fichier diff supprimé car celui-ci est trop grand
+ 923 - 0
panorama/static/panorama/img/valid_xhtml.svg


+ 27 - 0
panorama/static/panorama/js/hide_n_showForm.js

@@ -0,0 +1,27 @@
+function showForm() { 
+    var displayAddParams = document.getElementById('addParams');
+    var displayAdding = document.getElementById ('adding');
+    displayAddParams.style.visibility = 'hidden';
+    displayAdding.style.visibility = 'visible'; 
+} 
+  
+function hideForm() { 
+    var displayAddParams = document.getElementById('addParams');
+    var displayAdding = document.getElementById ('adding');
+    displayAddParams.style.visibility = 'visible';
+    displayAdding.style.visibility = 'hidden';
+} 
+  
+function showLoca() { 
+    var displayloca = document.getElementById('loca_show');
+    var putDraw = document.getElementById ('locadraw');
+    displayloca.style.visibility = 'hidden';
+    putDraw.style.visibility = 'visible'; 
+} 
+  
+function hideLoca() { 
+    var displayloca = document.getElementById('loca_show');
+    var putDraw = document.getElementById ('locadraw');
+    displayloca.style.visibility = 'visible';
+    putDraw.style.visibility = 'hidden';
+}

Fichier diff supprimé car celui-ci est trop grand
+ 1038 - 0
panorama/static/panorama/js/pano.js


+ 0 - 0
panorama/templates/__init__.py


+ 0 - 0
panorama/templates/panorama/__init__.py


+ 7 - 0
panorama/templates/panorama/list.html

@@ -0,0 +1,7 @@
+<p>
+  <ul>
+    {% for pano in panoramas %}
+    <li>{{ pano.name }} ({{ pano.latitude}}°, {{ pano.longitude }}°)</li>
+    {% endfor %}
+  </ul>
+</p>

+ 4 - 0
panorama/templates/panorama/new.html

@@ -0,0 +1,4 @@
+<form action="" enctype="multipart/form-data" method="post">{% csrf_token %}
+  {{ form.as_p }}
+  <input type="submit" value="Submit" />
+</form>

+ 127 - 0
panorama/templates/panorama/view.html

@@ -0,0 +1,127 @@
+{% load staticfiles %}
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="fr" lang="fr">
+  <head>
+    <meta http-equiv="Content-type" content="text/html; charset=UTF-8"/>
+    <title>{{ panorama.name }}</title>
+    <script>
+      var title = "{{ panorama.name|escapejs }}";
+      var img_prefix = '{{ panorama.tiles_url }}';
+      var image_loop = true;
+    </script>
+    <script src="{% static "panorama/js/pano.js" %}"></script>
+    <script>window.onload = load_pano</script>
+    <script>
+      zooms[0] = new tzoom(0);
+      zooms[0].ntiles.x = 94;
+      zooms[0].ntiles.y = 7;
+      zooms[0].tile.width = 256;
+      zooms[0].tile.height = 256;
+      zooms[0].last_tile.width = 109;
+      zooms[0].last_tile.height = 5;
+      zooms[1] = new tzoom(1);
+      zooms[1].ntiles.x = 47;
+      zooms[1].ntiles.y = 4;
+      zooms[1].tile.width = 256;
+      zooms[1].tile.height = 256;
+      zooms[1].last_tile.width = 183;
+      zooms[1].last_tile.height = 3;
+      zooms[2] = new tzoom(2);
+      zooms[2].ntiles.x = 24;
+      zooms[2].ntiles.y = 2;
+      zooms[2].tile.width = 256;
+      zooms[2].tile.height = 256;
+      zooms[2].last_tile.width = 91;
+      zooms[2].last_tile.height = 129;
+      zooms[3] = new tzoom(3);
+      zooms[3].ntiles.x = 12;
+      zooms[3].ntiles.y = 1;
+      zooms[3].tile.width = 256;
+      zooms[3].tile.height = 193;
+      zooms[3].last_tile.width = 174;
+      zooms[3].last_tile.height = 193;
+      zooms[4] = new tzoom(4);
+      zooms[4].ntiles.x = 6;
+      zooms[4].ntiles.y = 1;
+      zooms[4].tile.width = 256;
+      zooms[4].tile.height = 96;
+      zooms[4].last_tile.width = 215;
+      zooms[4].last_tile.height = 96;
+      zooms[5] = new tzoom(5);
+      zooms[5].ntiles.x = 3;
+      zooms[5].ntiles.y = 1;
+      zooms[5].tile.width = 256;
+      zooms[5].tile.height = 48;
+      zooms[5].last_tile.width = 234;
+      zooms[5].last_tile.height = 48;
+      zooms[6] = new tzoom(6);
+      zooms[6].ntiles.x = 2;
+      zooms[6].ntiles.y = 1;
+      zooms[6].tile.width = 256;
+      zooms[6].tile.height = 24;
+      zooms[6].last_tile.width = 117;
+      zooms[6].last_tile.height = 24;
+      zooms[7] = new tzoom(7);
+      zooms[7].ntiles.x = 1;
+      zooms[7].ntiles.y = 1;
+      zooms[7].tile.width = 187;
+      zooms[7].tile.height = 12;
+      zooms[7].last_tile.width = 187;
+      zooms[7].last_tile.height = 12;
+      zooms[8] = new tzoom(8);
+      zooms[8].ntiles.x = 1;
+      zooms[8].ntiles.y = 1;
+      zooms[8].tile.width = 93;
+      zooms[8].tile.height = 6;
+      zooms[8].last_tile.width = 93;
+      zooms[8].last_tile.height = 6;
+      point_list[0] = new Array("Tour Part-Dieu (crayon)", 0.714758, 41.956659, 11.233478, "");
+      point_list[1] = new Array("Tour métallique de Fourvière", 2.133944, -66.563873, 4.625330, "");
+      ref_points = new Array();
+      ref_points["Tour Part-Dieu (crayon)"] = {x:0.82724, cap:41.95666, y:0.43590, ele:11.23348};
+      ref_points["Tour métallique de Fourvière"] = {x:0.52327, cap:-66.56387, y:0.13405, ele:4.62533};
+    </script>
+    <link type="image/x-icon" rel="shortcut icon" href="{% static "panorama/img/tsf.png" %}"/>
+    <link rel="stylesheet" media="screen" href="{% static "panorama/css/map.css" %}" />
+    <script src="{% static "panorama/js/hide_n_showForm.js" %}"></script>
+  </head>
+  <body>
+    <canvas id="mon-canvas">
+      Ce message indique que ce navigateur est vétuste car il ne supporte pas <samp>canvas</samp> (IE6, IE7, IE8, ...)
+    </canvas>
+
+    <fieldset id="control"><legend>contrôle</legend>
+      <label>Zoom : <input type="range" min="0" max="2" value="2" id="zoom_ctrl"/></label>
+      <label>Cap : <input type="number" min="0" max="360" step="10" value="0" autofocus="" id="angle_ctrl"/></label>
+      <label>Élévation : <input type="number" min="-90" max="90" step="1" value="0" autofocus="" id="elvtn_ctrl"/></label>
+    </fieldset>
+
+    <div id="params">
+      <p>latitude :   <em><span id="pos_lat">45.75628</span>°</em></p>
+      <p>longitude : <em><span id="pos_lon">4.84759</span>°</em></p>
+      <p>altitude : <em><span id="pos_alt">189</span> m</em></p>
+    </div>
+    <img src="{% static "panorama/img/locapoint.svg" %}" id="loca_show" alt="localiser un point" title="pour localiser un point..."/>
+    <fieldset id="locadraw"><legend id="loca_hide">Localiser un point</legend>
+      <label class="form_col" title="La latitude ϵ [-90°, 90°]. Ex: 12.55257">Latitude :
+        <input  name="loca_latitude" type="number" min="-90" max="90"  id="loca_latitude"/></label>
+      <label class="form_col" title="La longitude ϵ [-180°, 180°]. Ex: 144.14723">Longitude :
+        <input name="loca_longitude" type="number" min="-180" max="180" id="loca_longitude"/></label>
+      <label class="form_col" title="L'altitude positive Ex: 170">Altitude :
+        <input  name="loca_altitude" type="number" min="-400" id="loca_altitude"/></label>
+      <div class="answer">
+        <input type="button" value="Localiser" id="loca_button"/>
+        <input type="button" value="Effacer" id="loca_erase"/>
+      </div>
+    </fieldset><p id="info"></p>
+    <p id="insert"><select id="sel_point" name="known_points">
+        <option>Tour Part-Dieu (crayon)</option>
+        <option>Tour métallique de Fourvière</option>
+      </select>
+      <input type="button" id="do-insert" value="insérer"/>
+      <input type="button" id="do-delete" value="suppimer"/>
+      <input type="button" id="show-cap" value="visualiser cet axe sur OSM"/>
+      <input type="button" id="do-cancel" value="annuler"/>
+    </p>
+  </body>
+</html>

+ 3 - 0
panorama/tests.py

@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.

+ 16 - 0
panorama/urls.py

@@ -0,0 +1,16 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.conf.urls import patterns, include, url
+
+from .views import PanoramaUpload, PanoramaView, pano_json, pano_refpoints, PanoramaList, PanoramaGenTiles
+
+
+urlpatterns = patterns('',
+    url(r'^$', PanoramaList.as_view(), name="list"),
+    url(r'^pano/new/$', PanoramaUpload.as_view(), name="new"),
+    url(r'^pano/view/(?P<pk>\d+)/$', PanoramaView.as_view(), name="view_pano"),
+    url(r'^pano/json/(?P<pk>\d+)/$', pano_json, name="pano_json"),
+    url(r'^pano/gen_tiles/(?P<id>\d+)/$', PanoramaGenTiles.as_view(), name="gen_tiles"),
+    url(r'^refpoints/around/pano/(?P<pk>\d+)/$', pano_refpoints, name="pano_refpoints"),
+)

+ 57 - 0
panorama/views.py

@@ -0,0 +1,57 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.conf import settings
+from django.core.urlresolvers import reverse_lazy
+from django.http import HttpResponse, JsonResponse
+from django.shortcuts import render, get_object_or_404
+from django.views.generic import CreateView, DetailView, RedirectView, ListView
+
+from .models import Panorama, ReferencePoint
+
+
+class PanoramaUpload(CreateView):
+    model = Panorama
+    fields = ('name', 'image', 'loop', 'latitude', 'longitude', 'altitude')
+    template_name = "panorama/new.html"
+
+    def get_success_url(self):
+        return reverse_lazy("panorama:gen_tiles", kwargs={"id": self.object.id})
+
+class PanoramaView(DetailView):
+    model = Panorama
+    template_name = "panorama/view.html"
+    context_object_name = "panorama"
+
+
+def pano_json(request, pk):
+    pano = get_object_or_404(Panorama, pk=pk)
+    return JsonResponse(pano.to_dict())
+
+def pano_refpoints(request, pk):
+    """Returns the reference points that are close to the given panorama, as a
+    JSON object.  Each reference point also includes information relative
+    to the given panorama (bearing, elevation, distance).
+    """
+    pano = get_object_or_404(Panorama, pk=pk)
+    refpoints = [r.to_dict_extended(pano) for r in ReferencePoint.objects.all()
+                 if r.line_distance(pano) <= settings.PANORAMA_MAX_DISTANCE]
+    return JsonResponse(refpoints, safe=False)
+
+
+class PanoramaGenTiles(RedirectView):
+    permanent = False
+    pattern_name = "panorama:list"
+
+    def get_redirect_url(self, *args, **kwargs):
+        pano = get_object_or_404(Panorama, pk=kwargs['id'])
+        pano.generate_tiles()
+        return super(PanoramaGenTiles, self).get_redirect_url(*args, **kwargs)
+
+
+class PanoramaList(ListView):
+    model = Panorama
+    template_name = "panorama/list.html"
+    context_object_name = "panoramas"
+
+

+ 0 - 0
ztulec/__init__.py


+ 96 - 0
ztulec/settings.py

@@ -0,0 +1,96 @@
+"""
+Django settings for ztulec project.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/1.7/topics/settings/
+
+For the full list of settings and their values, see
+https://docs.djangoproject.com/en/1.7/ref/settings/
+"""
+
+# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
+import os
+BASE_DIR = os.path.dirname(os.path.dirname(__file__))
+
+
+# Quick-start development settings - unsuitable for production
+# See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/
+
+# SECURITY WARNING: keep the secret key used in production secret!
+SECRET_KEY = 'ei3#@ejlp((&tlx2jrscs^wrvpn$y4o-7_(-$a_uc9%j3eux1*'
+
+# SECURITY WARNING: don't run with debug turned on in production!
+DEBUG = True
+
+TEMPLATE_DEBUG = True
+
+ALLOWED_HOSTS = []
+
+
+# Application definition
+
+INSTALLED_APPS = (
+    'django.contrib.admin',
+    'django.contrib.auth',
+    'django.contrib.contenttypes',
+    'django.contrib.sessions',
+    'django.contrib.messages',
+    'django.contrib.staticfiles',
+    'panorama',
+)
+
+MIDDLEWARE_CLASSES = (
+    'django.contrib.sessions.middleware.SessionMiddleware',
+    'django.middleware.common.CommonMiddleware',
+    'django.middleware.csrf.CsrfViewMiddleware',
+    'django.contrib.auth.middleware.AuthenticationMiddleware',
+    'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
+    'django.contrib.messages.middleware.MessageMiddleware',
+    'django.middleware.clickjacking.XFrameOptionsMiddleware',
+)
+
+ROOT_URLCONF = 'ztulec.urls'
+
+WSGI_APPLICATION = 'ztulec.wsgi.application'
+
+
+# Database
+# https://docs.djangoproject.com/en/1.7/ref/settings/#databases
+
+DATABASES = {
+    'default': {
+        'ENGINE': 'django.db.backends.sqlite3',
+        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
+    }
+}
+
+# Internationalization
+# https://docs.djangoproject.com/en/1.7/topics/i18n/
+
+LANGUAGE_CODE = 'fr_fr'
+
+TIME_ZONE = 'UTC'
+
+USE_I18N = True
+
+USE_L10N = True
+
+USE_TZ = True
+
+
+# Static files (CSS, JavaScript, Images)
+# https://docs.djangoproject.com/en/1.7/howto/static-files/
+
+STATIC_URL = '/static/'
+
+
+# For uploaded panorama
+MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
+MEDIA_URL = '/media/'
+
+# Relative to MEDIA_ROOT and MEDIA_URL
+PANORAMA_TILES_DIR = "tiles"
+
+# Max distance around a point at which to consider reference points
+# (in meters)
+PANORAMA_MAX_DISTANCE = 30000

+ 12 - 0
ztulec/urls.py

@@ -0,0 +1,12 @@
+from django.conf import settings
+from django.conf.urls import patterns, include, url
+from django.conf.urls.static import static
+from django.contrib import admin
+
+urlpatterns = patterns('',
+    # Examples:
+    # url(r'^$', 'ztulec.views.home', name='home'),
+    url(r'^admin/', include(admin.site.urls)),
+    url(r'^', include('panorama.urls', namespace="panorama")),
+# In debug mode, serve tiles
+) + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

+ 14 - 0
ztulec/wsgi.py

@@ -0,0 +1,14 @@
+"""
+WSGI config for ztulec project.
+
+It exposes the WSGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/1.7/howto/deployment/wsgi/
+"""
+
+import os
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "ztulec.settings")
+
+from django.core.wsgi import get_wsgi_application
+application = get_wsgi_application()