zhongrj
2025-11-25 b89962006164a462404b79a738bee8cbb6d7fe7e
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
import json
from datetime import datetime
import os
from urllib.parse import urlencode
 
import piexif
from PIL import Image
from rest_framework import serializers
from rest_framework import status
from rest_framework.response import Response
 
from app.plugins import GlobalDataStore, get_site_settings, signals as plugin_signals
from app.plugins.views import TaskView
from app.plugins.worker import task
 
from webodm import settings
from django.dispatch import receiver
 
import requests
 
import logging
 
logger = logging.getLogger('app.logger')
ds = GlobalDataStore('openaerialmap')
 
 
def get_key_for(task_id, key):
    return "task_{}_{}".format(str(task_id), key)
 
 
def get_task_info(task_id):
    return ds.get_json(get_key_for(task_id, "info"), {
        'sharing': False,
        'shared': False,
        'error': ''
    })
 
 
def set_task_info(task_id, json):
    return ds.set_json(get_key_for(task_id, "info"), json)
 
 
@receiver(plugin_signals.task_removed, dispatch_uid="oam_on_task_removed")
@receiver(plugin_signals.task_completed, dispatch_uid="oam_on_task_completed")
def oam_cleanup(sender, task_id, **kwargs):
    # When a task is removed, simply remove clutter
    # When a task is re-processed, make sure we can re-share it if we shared a task previously
 
    logger.info("Cleaning up OAM datastore for task {}".format(str(task_id)))
    ds.del_key(get_key_for(task_id, "info"))
 
 
class Info(TaskView):
    def get(self, request, pk=None):
        task = self.get_and_check_task(request, pk)
 
        task_info = get_task_info(task.id)
 
        # Populate fields from first image in task
        imgs = [f for f in task.scan_images() if not f.lower().endswith(".txt")]
        if len(imgs) > 0:
            img = imgs[0]
            img_path = task.get_image_path(img)
            im = Image.open(img_path)
 
            # TODO: for better data we could look over all images
            # and find actual end and start time
            # Here we're picking an image at random and assuming a one hour flight
            if not 'sensor' in task_info:
                task_info['endDate'] = datetime.utcnow().timestamp() * 1000
                task_info['sensor'] = ''
                task_info['title'] = task.name
                task_info['provider'] = get_site_settings().organization_name
 
                if 'exif' in im.info:
                    exif_dict = piexif.load(im.info['exif'])
                    if 'Exif' in exif_dict:
                        if piexif.ExifIFD.DateTimeOriginal in exif_dict['Exif']:
                            try:
                                parsed_date = datetime.strptime(exif_dict['Exif'][piexif.ExifIFD.DateTimeOriginal].decode('ascii'),
                                                            '%Y:%m:%d %H:%M:%S')
                                task_info['endDate'] = parsed_date.timestamp() * 1000
                            except ValueError:
                                # Ignore date field if we can't parse it
                                pass
                    if '0th' in exif_dict:
                        if piexif.ImageIFD.Make in exif_dict['0th']:
                            task_info['sensor'] = exif_dict['0th'][piexif.ImageIFD.Make].decode('ascii').strip(' \t\r\n\0')
 
                        if piexif.ImageIFD.Model in exif_dict['0th']:
                            task_info['sensor'] = (task_info['sensor'] + " " + exif_dict['0th'][piexif.ImageIFD.Model].decode('ascii')).strip(' \t\r\n\0')
 
                task_info['startDate'] = task_info['endDate'] - 60 * 60 * 1000
                set_task_info(task.id, task_info)
        else:
            task_info['noImages'] = True
 
        return Response(task_info, status=status.HTTP_200_OK)
 
 
class JSONSerializer(serializers.Serializer):
    oamParams = serializers.JSONField(help_text="OpenAerialMap share parameters (sensor, title, provider, etc.)")
 
 
class Share(TaskView):
    def post(self, request, pk=None):
        task = self.get_and_check_task(request, pk)
 
        serializer = JSONSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)
 
        oam_params = serializer['oamParams'].value
 
        task_info = get_task_info(task.id)
        task_info['sharing'] = True
        task_info['oam_upload_id'] = ''
        task_info['error'] = ''
        set_task_info(task.id, task_info)
 
        upload_orthophoto_to_oam.delay(task.id,
                                       task.get_asset_download_path('orthophoto.tif'),
                                       oam_params)
 
        return Response(task_info, status=status.HTTP_200_OK)
 
 
@task
def upload_orthophoto_to_oam(task_id, orthophoto_path, oam_params):
    # Upload to temporary central location since
    # OAM requires a public URL and not all WebODM
    # instances are public
 
    res = requests.post('https://www.webodm.org/oam/upload',
                        files=[
                            ('file', ('orthophoto.tif', open(orthophoto_path, 'rb'), 'image/tiff')),
                        ]).json()
 
    task_info = get_task_info(task_id)
 
    if 'url' in res:
        orthophoto_public_url = res['url']
        logger.info("Orthophoto uploaded to intermediary public URL " + orthophoto_public_url)
 
        # That's OK... we :heart: dronedeploy
        res = requests.post('https://api.openaerialmap.org/dronedeploy?{}'.format(urlencode(oam_params)),
                            json={
                                'download_path': orthophoto_public_url
                            }).json()
 
        if 'results' in res and 'upload' in res['results']:
            task_info['oam_upload_id'] = res['results']['upload']
            task_info['shared'] = True
        else:
            task_info['error'] = 'Could not upload orthophoto to OAM. The server replied: {}'.format(json.dumps(res))
 
            # Attempt to cleanup intermediate results
            requests.get('https://www.webodm.org/oam/cleanup/{}'.format(os.path.basename(orthophoto_public_url)))
    else:
        err_message = res['error'] if 'error' in res else json.dumps(res)
        task_info['error'] = 'Could not upload orthophoto to intermediate location: {}.'.format(err_message)
 
    task_info['sharing'] = False
    set_task_info(task_id, task_info)