igs-slm 0.1.5b2__py3-none-any.whl → 0.2.0b0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {igs_slm-0.1.5b2.dist-info → igs_slm-0.2.0b0.dist-info}/METADATA +2 -2
- {igs_slm-0.1.5b2.dist-info → igs_slm-0.2.0b0.dist-info}/RECORD +47 -34
- slm/__init__.py +1 -1
- slm/admin.py +40 -3
- slm/api/edit/views.py +37 -2
- slm/api/public/serializers.py +1 -1
- slm/defines/CoordinateMode.py +9 -0
- slm/defines/SiteLogFormat.py +19 -6
- slm/defines/__init__.py +24 -22
- slm/file_views/apps.py +7 -0
- slm/file_views/config.py +253 -0
- slm/file_views/settings.py +124 -0
- slm/file_views/static/slm/file_views/banner_header.png +0 -0
- slm/file_views/static/slm/file_views/css/listing.css +82 -0
- slm/file_views/templates/slm/file_views/listing.html +70 -0
- slm/file_views/urls.py +47 -0
- slm/file_views/views.py +472 -0
- slm/forms.py +22 -4
- slm/jinja2/slm/sitelog/ascii_9char.log +1 -1
- slm/jinja2/slm/sitelog/legacy.log +1 -1
- slm/management/commands/check_upgrade.py +11 -11
- slm/management/commands/generate_sinex.py +9 -7
- slm/map/settings.py +0 -0
- slm/migrations/0001_alter_archivedsitelog_size_and_more.py +44 -0
- slm/migrations/0032_archiveindex_valid_range_and_more.py +8 -1
- slm/migrations/simplify_daily_index_files.py +86 -0
- slm/models/index.py +73 -6
- slm/models/sitelog.py +6 -0
- slm/models/system.py +35 -2
- slm/parsing/__init__.py +10 -0
- slm/parsing/legacy/binding.py +3 -2
- slm/receivers/cache.py +25 -0
- slm/settings/root.py +22 -0
- slm/settings/routines.py +1 -0
- slm/settings/slm.py +71 -10
- slm/settings/urls.py +1 -1
- slm/settings/validation.py +5 -4
- slm/signals.py +33 -23
- slm/static/slm/js/enums.js +7 -6
- slm/static/slm/js/form.js +25 -14
- slm/static/slm/js/slm.js +4 -2
- slm/templatetags/slm.py +3 -3
- slm/utils.py +161 -36
- slm/validators.py +51 -0
- {igs_slm-0.1.5b2.dist-info → igs_slm-0.2.0b0.dist-info}/WHEEL +0 -0
- {igs_slm-0.1.5b2.dist-info → igs_slm-0.2.0b0.dist-info}/entry_points.txt +0 -0
- {igs_slm-0.1.5b2.dist-info → igs_slm-0.2.0b0.dist-info}/licenses/LICENSE +0 -0
slm/signals.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
"""
|
2
|
-
All SLM specific signals sent by the system are defined here.
|
3
|
-
mostly include events relating to the site log edit/moderate/publish life
|
2
|
+
All SLM specific :doc:`django:topics/signals` sent by the system are defined here.
|
3
|
+
These signals mostly include events relating to the site log edit/moderate/publish life
|
4
|
+
cycle.
|
4
5
|
|
5
6
|
All signals contain a request object that holds the request that initiated the
|
6
7
|
event. This object is provided mostly for logging purposes and is not
|
@@ -12,6 +13,7 @@ import sys
|
|
12
13
|
from django.dispatch import Signal
|
13
14
|
from django.utils.module_loading import import_string
|
14
15
|
|
16
|
+
site_proposed = Signal()
|
15
17
|
"""
|
16
18
|
Signal sent when a new site is proposed.
|
17
19
|
|
@@ -24,8 +26,9 @@ Signal sent when a new site is proposed.
|
|
24
26
|
responsible agencies.
|
25
27
|
:param kwargs: Misc other key word arguments
|
26
28
|
"""
|
27
|
-
site_proposed = Signal()
|
28
29
|
|
30
|
+
|
31
|
+
site_published = Signal()
|
29
32
|
"""
|
30
33
|
Sent when a site log is published, or when a section of a site log is
|
31
34
|
published.
|
@@ -39,13 +42,13 @@ published.
|
|
39
42
|
section was published this will be the Section object instance.
|
40
43
|
:param kwargs: Misc other key word arguments
|
41
44
|
"""
|
42
|
-
site_published = Signal()
|
43
45
|
|
46
|
+
|
47
|
+
site_status_changed = Signal()
|
44
48
|
"""
|
45
|
-
Sent when a site log
|
46
|
-
|
47
|
-
|
48
|
-
The published timestamp will have increased.
|
49
|
+
Sent when a site log's status changes. Its possible that both the previous and
|
50
|
+
new status states are Published. If this happens a site log was edited and
|
51
|
+
published simultaneously. The published timestamp will have increased.
|
49
52
|
|
50
53
|
:param sender: The sending object (unreliable).
|
51
54
|
:param site: The Site object.
|
@@ -54,8 +57,9 @@ The published timestamp will have increased.
|
|
54
57
|
:param reverted: If true this status change was the result of a reversion
|
55
58
|
:param kwargs: Misc other key word arguments
|
56
59
|
"""
|
57
|
-
site_status_changed = Signal()
|
58
60
|
|
61
|
+
|
62
|
+
section_edited = Signal()
|
59
63
|
"""
|
60
64
|
Sent when a site log section is edited.
|
61
65
|
|
@@ -68,8 +72,9 @@ Sent when a site log section is edited.
|
|
68
72
|
:param fields: The list of edited fields
|
69
73
|
:param kwargs: Misc other key word arguments
|
70
74
|
"""
|
71
|
-
section_edited = Signal()
|
72
75
|
|
76
|
+
|
77
|
+
section_added = Signal()
|
73
78
|
"""
|
74
79
|
Sent when a site log section is added.
|
75
80
|
|
@@ -81,8 +86,9 @@ Sent when a site log section is added.
|
|
81
86
|
:param section: The section object that was added.
|
82
87
|
:param kwargs: Misc other key word arguments
|
83
88
|
"""
|
84
|
-
section_added = Signal()
|
85
89
|
|
90
|
+
|
91
|
+
section_deleted = Signal()
|
86
92
|
"""
|
87
93
|
Sent when a site log section is deleted.
|
88
94
|
|
@@ -94,8 +100,9 @@ Sent when a site log section is deleted.
|
|
94
100
|
:param section: The section object that was deleted.
|
95
101
|
:param kwargs: Misc other key word arguments
|
96
102
|
"""
|
97
|
-
section_deleted = Signal()
|
98
103
|
|
104
|
+
|
105
|
+
fields_flagged = Signal()
|
99
106
|
"""
|
100
107
|
Sent when a site log section has fields flagged.
|
101
108
|
|
@@ -109,8 +116,9 @@ Sent when a site log section has fields flagged.
|
|
109
116
|
:param fields: The list of fields that were flagged.
|
110
117
|
:param kwargs: Misc other key word arguments
|
111
118
|
"""
|
112
|
-
fields_flagged = Signal()
|
113
119
|
|
120
|
+
|
121
|
+
flags_cleared = Signal()
|
114
122
|
"""
|
115
123
|
Sent when a site log section has fields flagged.
|
116
124
|
|
@@ -126,8 +134,9 @@ Sent when a site log section has fields flagged.
|
|
126
134
|
:param clear: True if the section has no remaining flags - false otherwise.
|
127
135
|
:param kwargs: Misc other key word arguments
|
128
136
|
"""
|
129
|
-
flags_cleared = Signal()
|
130
137
|
|
138
|
+
|
139
|
+
site_file_uploaded = Signal()
|
131
140
|
"""
|
132
141
|
Sent when a user uploads a site log.
|
133
142
|
|
@@ -139,8 +148,9 @@ Sent when a user uploads a site log.
|
|
139
148
|
:param upload: The uploaded file (SiteFileUpload).
|
140
149
|
:param kwargs: Misc other key word arguments
|
141
150
|
"""
|
142
|
-
site_file_uploaded = Signal()
|
143
151
|
|
152
|
+
|
153
|
+
site_file_published = Signal()
|
144
154
|
"""
|
145
155
|
Sent when a moderator publishes a site file upload - could be an attachment or
|
146
156
|
an image.
|
@@ -153,9 +163,9 @@ an image.
|
|
153
163
|
:param upload: The uploaded file (SiteFileUpload).
|
154
164
|
:param kwargs: Misc other key word arguments
|
155
165
|
"""
|
156
|
-
site_file_published = Signal()
|
157
166
|
|
158
167
|
|
168
|
+
site_file_unpublished = Signal()
|
159
169
|
"""
|
160
170
|
Sent when a moderator retracts a site file upload - could be an attachment
|
161
171
|
or an image.
|
@@ -168,9 +178,9 @@ or an image.
|
|
168
178
|
:param upload: The uploaded file (SiteFileUpload).
|
169
179
|
:param kwargs: Misc other key word arguments
|
170
180
|
"""
|
171
|
-
site_file_unpublished = Signal()
|
172
181
|
|
173
182
|
|
183
|
+
site_file_deleted = Signal()
|
174
184
|
"""
|
175
185
|
Sent when a user deletes a site file upload through the API. The file must be
|
176
186
|
a type other than a site log - could be an attachment or an image.
|
@@ -183,9 +193,9 @@ a type other than a site log - could be an attachment or an image.
|
|
183
193
|
:param upload: The uploaded file (SiteFileUpload).
|
184
194
|
:param kwargs: Misc other key word arguments
|
185
195
|
"""
|
186
|
-
site_file_deleted = Signal()
|
187
196
|
|
188
197
|
|
198
|
+
review_requested = Signal()
|
189
199
|
"""
|
190
200
|
Sent when a user requests a site log be reviewed and published by moderators.
|
191
201
|
|
@@ -195,8 +205,8 @@ Sent when a user requests a site log be reviewed and published by moderators.
|
|
195
205
|
:param request: The Django request object that contained the request.
|
196
206
|
:param kwargs: Misc other key word arguments
|
197
207
|
"""
|
198
|
-
review_requested = Signal()
|
199
208
|
|
209
|
+
updates_rejected = Signal()
|
200
210
|
"""
|
201
211
|
Sent when a moderator rejects edits requested for publish in a review request.
|
202
212
|
|
@@ -206,8 +216,9 @@ Sent when a moderator rejects edits requested for publish in a review request.
|
|
206
216
|
:param request: The Django request object that contained the rejection.
|
207
217
|
:param kwargs: Misc other key word arguments
|
208
218
|
"""
|
209
|
-
updates_rejected = Signal()
|
210
219
|
|
220
|
+
|
221
|
+
alert_issued = Signal()
|
211
222
|
"""
|
212
223
|
Sent when an alert is issued.
|
213
224
|
|
@@ -215,8 +226,9 @@ Sent when an alert is issued.
|
|
215
226
|
:param alert: The alert object that was issued.
|
216
227
|
:param kwargs: Misc other key word arguments
|
217
228
|
"""
|
218
|
-
alert_issued = Signal()
|
219
229
|
|
230
|
+
|
231
|
+
alert_cleared = Signal()
|
220
232
|
"""
|
221
233
|
Sent when an alert is cleared.
|
222
234
|
|
@@ -224,8 +236,6 @@ Sent when an alert is cleared.
|
|
224
236
|
:param alert: The alert object that was issued.
|
225
237
|
:param kwargs: Misc other key word arguments
|
226
238
|
"""
|
227
|
-
alert_cleared = Signal()
|
228
|
-
|
229
239
|
|
230
240
|
_signal_names_ = {
|
231
241
|
value: f"slm.signals.{key}"
|
slm/static/slm/js/enums.js
CHANGED
@@ -201,12 +201,12 @@ class SiteFileUploadStatus {
|
|
201
201
|
}
|
202
202
|
class SiteLogFormat {
|
203
203
|
|
204
|
-
static LEGACY = new SiteLogFormat(1, "LEGACY", "Legacy (ASCII)", "text/plain", "bi bi-file-text", "log", ["text", "txt", "legacy"
|
205
|
-
static GEODESY_ML = new SiteLogFormat(2, "GEODESY_ML", "GeodesyML", "application/xml", "bi bi-filetype-xml", "xml", ["xml"], ["
|
206
|
-
static JSON = new SiteLogFormat(3, "JSON", "JSON", "application/json", "bi bi-filetype-json", "json", ["json", "js"], ["
|
207
|
-
static ASCII_9CHAR = new SiteLogFormat(4, "ASCII_9CHAR", "ASCII (9-Char)", "text/plain", "bi bi-file-text", "log", ["text", "txt", "9char"], ["
|
204
|
+
static LEGACY = new SiteLogFormat(1, "LEGACY", "Legacy (ASCII)", "text/plain", "bi bi-file-text", "log", ["text", "txt", "legacy", "sitelog"], [], "log");
|
205
|
+
static GEODESY_ML = new SiteLogFormat(2, "GEODESY_ML", "GeodesyML", "application/xml", "bi bi-filetype-xml", "xml", ["xml", "gml"], [], "xml");
|
206
|
+
static JSON = new SiteLogFormat(3, "JSON", "JSON", "application/json", "bi bi-filetype-json", "json", ["json", "js"], [], "json");
|
207
|
+
static ASCII_9CHAR = new SiteLogFormat(4, "ASCII_9CHAR", "ASCII (9-Char)", "text/plain", "bi bi-file-text", "log", ["text", "txt", "9char", "sitelog"], "[(1, 'Legacy (ASCII)')]", "log");
|
208
208
|
|
209
|
-
constructor (value, name, label, mimetype, icon, ext, alts,
|
209
|
+
constructor (value, name, label, mimetype, icon, ext, alts, supersedes, suffix) {
|
210
210
|
this.value = value;
|
211
211
|
this.name = name;
|
212
212
|
this.label = label;
|
@@ -214,7 +214,8 @@ class SiteLogFormat {
|
|
214
214
|
this.icon = icon;
|
215
215
|
this.ext = ext;
|
216
216
|
this.alts = alts;
|
217
|
-
this.
|
217
|
+
this.supersedes = supersedes;
|
218
|
+
this.suffix = suffix;
|
218
219
|
}
|
219
220
|
|
220
221
|
static ciCompare(a, b) {
|
slm/static/slm/js/form.js
CHANGED
@@ -167,7 +167,16 @@ class Form extends slm.Persistable {
|
|
167
167
|
data[field.name] = [value];
|
168
168
|
}
|
169
169
|
} else {
|
170
|
-
data
|
170
|
+
if (Object.hasOwn(data, field.name)) {
|
171
|
+
const current = data[field.name];
|
172
|
+
if (Array.isArray(current)) {
|
173
|
+
current.push(value);
|
174
|
+
} else {
|
175
|
+
data[field.name] = [current, value];
|
176
|
+
}
|
177
|
+
} else {
|
178
|
+
data[field.name] = value;
|
179
|
+
}
|
171
180
|
}
|
172
181
|
}
|
173
182
|
|
@@ -234,16 +243,20 @@ class Form extends slm.Persistable {
|
|
234
243
|
checked.prop('checked', true);
|
235
244
|
this.element.find(`[name="${field}"]:checkbox`).not(checked).prop('checked', false);
|
236
245
|
break;
|
237
|
-
|
238
|
-
if (
|
239
|
-
|
246
|
+
case 'number':
|
247
|
+
if (value.length === ipt.length) {
|
248
|
+
let idx = 0;
|
249
|
+
for (const val of value) { $(ipt[idx++]).val(val); }
|
250
|
+
break;
|
240
251
|
}
|
252
|
+
default:
|
253
|
+
throw TypeError(`Type mismatch: ${value} ${ipt.prop('type')}`);
|
241
254
|
}
|
242
255
|
} else if (ipt.length > 1) {
|
243
256
|
const types = [];
|
244
257
|
ipt.each(function () { types.push($(this).prop('type')); });
|
245
258
|
const values = this.decompose(types, value);
|
246
|
-
ipt.
|
259
|
+
ipt.each((i, e) => setField($(e), values[i]));
|
247
260
|
} else { setField(ipt, value); }
|
248
261
|
}.bind(this));
|
249
262
|
|
@@ -302,22 +315,20 @@ class Form extends slm.Persistable {
|
|
302
315
|
}
|
303
316
|
|
304
317
|
decompose(types, value) {
|
305
|
-
|
306
|
-
|
318
|
+
const typeList = Array.isArray(types) ? types : Array.from(types);
|
319
|
+
if (typeList.length === 2 && typeList[0] === 'date' && typeList[1] === 'time') {
|
307
320
|
if (value) { return value.split('T'); }
|
308
321
|
return ['', ''];
|
309
|
-
default:
|
310
|
-
throw TypeError(`Unexpected decomposition types: ${types}`);
|
311
322
|
}
|
323
|
+
throw TypeError(`Unexpected decomposition types: ${types}`);
|
312
324
|
}
|
313
325
|
|
314
326
|
compose(types, inputs) {
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
default:
|
319
|
-
throw TypeError(`Unexpected composition types: ${types}`);
|
327
|
+
const typeList = Array.isArray(types) ? types : Array.from(types);
|
328
|
+
if (typeList.length === 2 && typeList[0] === 'date' && typeList[1] === 'time') {
|
329
|
+
return `${inputs[0]}T${inputs[1] || "00:00"}Z`
|
320
330
|
}
|
331
|
+
throw TypeError(`Unexpected composition types: ${[...typeList].join(', ')}`);
|
321
332
|
}
|
322
333
|
|
323
334
|
clear() {
|
slm/static/slm/js/slm.js
CHANGED
@@ -130,10 +130,12 @@ slm.handlePostSuccess = function(form, response, status, jqXHR) {
|
|
130
130
|
'button.accordion-button'
|
131
131
|
).removeClass('slm-section-deleted');
|
132
132
|
form.find('.alert.slm-form-deleted').hide();
|
133
|
-
form.find('.form-control:visible').removeAttr('disabled');
|
134
|
-
form.find('input.form-check-input:visible').removeAttr('disabled', '');
|
135
133
|
form.find('.slm-flag').show();
|
136
134
|
form.find('div[contenteditable]').attr('contenteditable', true);
|
135
|
+
|
136
|
+
// TODO Form class setters/getters still not completely working for all forms
|
137
|
+
// const filterForm = new slm.Form(form);
|
138
|
+
// filterForm.data = data;
|
137
139
|
}
|
138
140
|
|
139
141
|
if (data.hasOwnProperty('_diff') && Object.keys(data._diff).length) {
|
slm/templatetags/slm.py
CHANGED
@@ -111,7 +111,7 @@ def iso_utc_full(datetime_field):
|
|
111
111
|
@register.filter(name="multi_line")
|
112
112
|
def multi_line(text):
|
113
113
|
if text:
|
114
|
-
limit =
|
114
|
+
limit = 48
|
115
115
|
lines = [line.rstrip() for line in text.split("\n")]
|
116
116
|
limited = []
|
117
117
|
for line in lines:
|
@@ -254,11 +254,11 @@ def file_icon(file):
|
|
254
254
|
@register.filter(name="file_lines")
|
255
255
|
def file_lines(file):
|
256
256
|
if isinstance(file, str):
|
257
|
-
return file.
|
257
|
+
return file.splitlines()
|
258
258
|
if file and os.path.exists(file.file.path):
|
259
259
|
content = file.file.open().read()
|
260
260
|
try:
|
261
|
-
return content.decode(detect(content).get("encoding", "utf-8")).
|
261
|
+
return content.decode(detect(content).get("encoding", "utf-8")).splitlines()
|
262
262
|
except (UnicodeDecodeError, LookupError, ValueError):
|
263
263
|
return [
|
264
264
|
_("** Unable to determine text encoding - please upload as UTF-8. **")
|
slm/utils.py
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
import json
|
2
2
|
import re
|
3
|
+
import typing as t
|
3
4
|
from datetime import date, datetime, timedelta
|
4
|
-
from math import atan2, cos, sin, sqrt
|
5
|
+
from math import atan2, copysign, cos, floor, sin, sqrt
|
5
6
|
|
6
7
|
from dateutil import parser as date_parser
|
7
8
|
from django.conf import settings
|
@@ -39,43 +40,121 @@ def get_record_model():
|
|
39
40
|
) from ve
|
40
41
|
|
41
42
|
|
42
|
-
def dddmmssss_to_decimal(
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
minutes = (dddmmssss - degrees) * 100
|
49
|
-
seconds = float((minutes - int(minutes)) * 100)
|
50
|
-
return degrees + int(minutes) / 60 + seconds / 3600
|
51
|
-
return None
|
52
|
-
|
53
|
-
|
54
|
-
def decimal_to_dddmmssss(dec):
|
55
|
-
if dec is not None:
|
56
|
-
if isinstance(dec, str):
|
57
|
-
dec = float(dec)
|
58
|
-
degrees = int(dec)
|
59
|
-
minutes = (dec - degrees) * 60
|
60
|
-
seconds = float(minutes - int(minutes)) * 60
|
61
|
-
return degrees * 10000 + int(minutes) * 100 + seconds
|
62
|
-
return None
|
43
|
+
def dddmmssss_to_decimal(
|
44
|
+
dddmmssss: t.Optional[t.Union[float, str, int]], sec_digits: int = 6
|
45
|
+
) -> t.Optional[float]:
|
46
|
+
"""
|
47
|
+
Convert DDDMMSS.ss composite to decimal degrees.
|
48
|
+
Preserves -0.0 and normalizes 60s/60m carries.
|
63
49
|
|
50
|
+
:param dddmmssss: latitude or longitude in DDDMMSS.SS format
|
51
|
+
:return: the coordinate in decimal degrees
|
52
|
+
"""
|
53
|
+
if dddmmssss is None:
|
54
|
+
return None
|
55
|
+
if isinstance(dddmmssss, str):
|
56
|
+
dddmmssss = float(dddmmssss)
|
57
|
+
|
58
|
+
sgn = copysign(1.0, dddmmssss)
|
59
|
+
v = abs(dddmmssss)
|
60
|
+
|
61
|
+
# Extract D, M, S from DDDMMSS.ss
|
62
|
+
degrees = int(v // 10000)
|
63
|
+
remainder = v - degrees * 10000
|
64
|
+
minutes = int(remainder // 100)
|
65
|
+
seconds = remainder - minutes * 100
|
66
|
+
|
67
|
+
# Clean up float noise and normalize carries
|
68
|
+
seconds = round(seconds, sec_digits)
|
69
|
+
if seconds >= 60.0:
|
70
|
+
seconds = 0.0
|
71
|
+
minutes += 1
|
72
|
+
if minutes >= 60:
|
73
|
+
minutes = 0
|
74
|
+
degrees += 1
|
75
|
+
|
76
|
+
dec = degrees + minutes / 60.0 + seconds / 3600.0
|
77
|
+
return copysign(dec, sgn)
|
78
|
+
|
79
|
+
|
80
|
+
def decimal_to_dddmmssss(
|
81
|
+
dec: t.Optional[t.Union[str, float]], sec_digits: int = 2
|
82
|
+
) -> t.Optional[float]:
|
83
|
+
"""
|
84
|
+
Convert decimal degrees to DDDMMSS.SS... as a float.
|
85
|
+
Preserves the sign of -0.0 and normalizes carry (sec/min -> min/deg).
|
64
86
|
|
65
|
-
|
87
|
+
:param: dec: string or float of decimal degrees of either latitude or longitude
|
88
|
+
:return: floating point lat or lon in DDDMMSS.SS format
|
89
|
+
"""
|
90
|
+
if dec is None:
|
91
|
+
return None
|
92
|
+
if isinstance(dec, str):
|
93
|
+
dec = float(dec)
|
94
|
+
|
95
|
+
# Work with absolute value; keep original sign with copysign at the end
|
96
|
+
a = abs(dec)
|
97
|
+
|
98
|
+
degrees = int(a)
|
99
|
+
minutes_full = (a - degrees) * 60.0
|
100
|
+
minutes = int(minutes_full)
|
101
|
+
seconds = (minutes_full - minutes) * 60.0
|
102
|
+
|
103
|
+
# Round seconds, then normalize carries (60s -> +1m, 60m -> +1d)
|
104
|
+
seconds = round(seconds, sec_digits)
|
105
|
+
if seconds >= 60.0:
|
106
|
+
seconds = 0.0
|
107
|
+
minutes += 1
|
108
|
+
if minutes >= 60:
|
109
|
+
minutes = 0
|
110
|
+
degrees += 1
|
111
|
+
|
112
|
+
# Compose as DDDMMSS.ss (minutes and seconds are non-negative)
|
113
|
+
composite = degrees * 10000 + minutes * 100 + seconds
|
114
|
+
return copysign(composite, dec)
|
115
|
+
|
116
|
+
|
117
|
+
def dddmmss_ss_parts(
|
118
|
+
dec: t.Optional[t.Union[str, float, int]], sec_digits: int = 2
|
119
|
+
) -> t.Tuple[t.Optional[float], t.Optional[int], t.Optional[float]]:
|
66
120
|
"""
|
67
121
|
Return (degrees, minutes, seconds) from decimal degrees
|
68
122
|
:param dec: Decimal degrees lat or lon
|
69
|
-
:return:
|
123
|
+
:return: a 3-tuple of degrees, minutes seconds components. The degrees component will be a
|
124
|
+
whole number, but is a float so that it may contain the sign.
|
70
125
|
"""
|
71
|
-
if dec is
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
126
|
+
if dec is None:
|
127
|
+
return None, None, None
|
128
|
+
if isinstance(dec, str):
|
129
|
+
dec = float(dec)
|
130
|
+
|
131
|
+
sign = int(copysign(1, dec))
|
132
|
+
v = abs(dec)
|
133
|
+
|
134
|
+
degrees = floor(v)
|
135
|
+
minutes_full = (v - degrees) * 60
|
136
|
+
minutes = floor(minutes_full)
|
137
|
+
seconds = (minutes_full - minutes) * 60
|
138
|
+
|
139
|
+
seconds = round(seconds, sec_digits)
|
140
|
+
|
141
|
+
# Handle rounding overflow (e.g. 59.9999 → 60.0)
|
142
|
+
if seconds >= 60.0:
|
143
|
+
seconds = 0.0
|
144
|
+
minutes += 1
|
145
|
+
if minutes >= 60:
|
146
|
+
minutes = 0
|
147
|
+
degrees += 1
|
148
|
+
|
149
|
+
return (sign * float(degrees), minutes, seconds)
|
150
|
+
|
151
|
+
|
152
|
+
def lon_180_to_360(lon):
|
153
|
+
return lon % 360.0
|
154
|
+
|
155
|
+
|
156
|
+
def lon_360_to_180(lon):
|
157
|
+
return ((lon + 180) % 360) - 180
|
79
158
|
|
80
159
|
|
81
160
|
def set_protocol(request):
|
@@ -249,11 +328,20 @@ def get_exif_tags(file_path):
|
|
249
328
|
return {}
|
250
329
|
|
251
330
|
|
252
|
-
def xyz2llh(xyz):
|
253
|
-
|
254
|
-
|
331
|
+
def xyz2llh(*xyz, geodetic=True) -> t.Tuple[float, float, float]:
|
332
|
+
"""
|
333
|
+
Convert ECEF to LLH using ITRF2020 Ellipsoid.
|
334
|
+
|
335
|
+
:param xyz: A 3-tuple or 3 xyz parameters, e.g. xyz2llh(x,y,z) or xyz2llh((x,y,z))
|
336
|
+
:return: A 3-tuple of latitude, longitude, height. Longitude is geodetic (+/-180) by default and height is in meters.
|
337
|
+
"""
|
338
|
+
a_e = 6378.137e3 # meters
|
339
|
+
f_e = 1 / 298.257222101 # ITRF2020 flattening
|
255
340
|
radians2degree = 45 / atan2(1, 1)
|
256
341
|
|
342
|
+
# allow xyz2llh(x,y,z) or xyz2llh((x,y,z))
|
343
|
+
xyz = xyz[0] if len(xyz) == 1 else xyz
|
344
|
+
|
257
345
|
xyz_array = [v / a_e for v in xyz]
|
258
346
|
(x, y, z) = (xyz_array[0], xyz_array[1], xyz_array[2])
|
259
347
|
e2 = f_e * (2 - f_e)
|
@@ -271,7 +359,44 @@ def xyz2llh(xyz):
|
|
271
359
|
lon = lon + 360
|
272
360
|
h = a_e * (p * cos(phi) + z * sin(phi) - sqrt(1 - e2 * (sin(phi)) ** 2))
|
273
361
|
|
274
|
-
return lat, lon, h
|
362
|
+
return lat, lon_360_to_180(lon) if geodetic else lon, h
|
363
|
+
|
364
|
+
|
365
|
+
def llh2xyz(*llh) -> t.Tuple[float, float, float]:
|
366
|
+
"""
|
367
|
+
Convert LLH (latitude, longitude, height) to ECEF (X, Y, Z)
|
368
|
+
using ITRF2020 Ellipsoid.
|
369
|
+
|
370
|
+
:param llh: A 3-tuple or 3 separate parameters, e.g. llh2xyz(lat, lon, h) or llh2xyz((lat, lon, h))
|
371
|
+
:returns: (x, y, z) in meters
|
372
|
+
"""
|
373
|
+
a_e = 6378.137e3 # semi-major axis in meters
|
374
|
+
f_e = 1 / 298.257222101 # flattening (ITRF2020)
|
375
|
+
radians_per_degree = atan2(1, 1) / 45 # inverse of your radians2degree
|
376
|
+
|
377
|
+
# allow llh2xyz(lat, lon, h) or llh2xyz((lat, lon, h))
|
378
|
+
llh = llh[0] if len(llh) == 1 else llh
|
379
|
+
lat, lon, h = llh
|
380
|
+
|
381
|
+
# convert to radians
|
382
|
+
phi = lat * radians_per_degree
|
383
|
+
lam = lon * radians_per_degree
|
384
|
+
|
385
|
+
e2 = f_e * (2 - f_e)
|
386
|
+
sin_phi = sin(phi)
|
387
|
+
cos_phi = cos(phi)
|
388
|
+
sin_lam = sin(lam)
|
389
|
+
cos_lam = cos(lam)
|
390
|
+
|
391
|
+
# prime vertical radius of curvature
|
392
|
+
N = a_e / sqrt(1 - e2 * sin_phi**2)
|
393
|
+
|
394
|
+
# ECEF coordinates
|
395
|
+
x = (N + h) * cos_phi * cos_lam
|
396
|
+
y = (N + h) * cos_phi * sin_lam
|
397
|
+
z = ((1 - e2) * N + h) * sin_phi
|
398
|
+
|
399
|
+
return x, y, z
|
275
400
|
|
276
401
|
|
277
402
|
def convert_9to4(text: str, name: str) -> str:
|
slm/validators.py
CHANGED
@@ -295,3 +295,54 @@ class TimeRangeValidator(SLMValidator):
|
|
295
295
|
instance,
|
296
296
|
field,
|
297
297
|
)
|
298
|
+
|
299
|
+
|
300
|
+
class PositionsMatchValidator(SLMValidator):
|
301
|
+
"""
|
302
|
+
Attach this validator to SiteLocation llh and/or xyz fields to validate that these
|
303
|
+
positions are within the given tolerance of each other.
|
304
|
+
"""
|
305
|
+
|
306
|
+
tolerance = 1.0
|
307
|
+
"""
|
308
|
+
3D tolerance in meters between the positions before flagging.
|
309
|
+
"""
|
310
|
+
|
311
|
+
def __init__(
|
312
|
+
self,
|
313
|
+
*args,
|
314
|
+
severity=FlagSeverity.BLOCK_SAVE,
|
315
|
+
tolerance: float = tolerance,
|
316
|
+
**kwargs,
|
317
|
+
):
|
318
|
+
self.tolerance = tolerance
|
319
|
+
super().__init__(*args, severity=severity, **kwargs)
|
320
|
+
|
321
|
+
def __call__(self, instance, field, value):
|
322
|
+
from math import sqrt
|
323
|
+
|
324
|
+
from slm.utils import llh2xyz
|
325
|
+
|
326
|
+
fieldname = field.name
|
327
|
+
xyz1 = value if fieldname == "xyz" else llh2xyz(value)
|
328
|
+
other = "xyz" if fieldname == "llh" else "llh"
|
329
|
+
otherfield = instance._meta.get_field(other)
|
330
|
+
if xyz1:
|
331
|
+
xyz2 = (
|
332
|
+
llh2xyz(getattr(instance, other))
|
333
|
+
if other == "llh"
|
334
|
+
else getattr(instance, other)
|
335
|
+
)
|
336
|
+
if xyz2:
|
337
|
+
diff = sqrt(
|
338
|
+
(xyz1[0] - xyz2[0]) ** 2
|
339
|
+
+ (xyz1[1] - xyz2[1]) ** 2
|
340
|
+
+ (xyz1[2] - xyz2[2]) ** 2
|
341
|
+
)
|
342
|
+
if diff > self.tolerance:
|
343
|
+
print(instance.site.name, diff)
|
344
|
+
self.throw_error(
|
345
|
+
f"{diff:.2f} meters away from {otherfield.verbose_name}",
|
346
|
+
instance,
|
347
|
+
field,
|
348
|
+
)
|
File without changes
|
File without changes
|
File without changes
|