igs-slm 0.1.5b3__py3-none-any.whl → 0.2.0b1__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.5b3.dist-info → igs_slm-0.2.0b1.dist-info}/METADATA +2 -2
- {igs_slm-0.1.5b3.dist-info → igs_slm-0.2.0b1.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 +25 -19
- 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 +2 -0
- slm/settings/slm.py +58 -0
- slm/settings/urls.py +1 -1
- slm/settings/validation.py +5 -4
- slm/signals.py +3 -4
- 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 +1 -1
- slm/utils.py +161 -36
- slm/validators.py +51 -0
- {igs_slm-0.1.5b3.dist-info → igs_slm-0.2.0b1.dist-info}/WHEEL +0 -0
- {igs_slm-0.1.5b3.dist-info → igs_slm-0.2.0b1.dist-info}/entry_points.txt +0 -0
- {igs_slm-0.1.5b3.dist-info → igs_slm-0.2.0b1.dist-info}/licenses/LICENSE +0 -0
slm/signals.py
CHANGED
@@ -46,10 +46,9 @@ published.
|
|
46
46
|
|
47
47
|
site_status_changed = Signal()
|
48
48
|
"""
|
49
|
-
Sent when a site log
|
50
|
-
|
51
|
-
|
52
|
-
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.
|
53
52
|
|
54
53
|
:param sender: The sending object (unreliable).
|
55
54
|
:param site: The Site object.
|
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
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
|