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.
Files changed (47) hide show
  1. {igs_slm-0.1.5b3.dist-info → igs_slm-0.2.0b1.dist-info}/METADATA +2 -2
  2. {igs_slm-0.1.5b3.dist-info → igs_slm-0.2.0b1.dist-info}/RECORD +47 -34
  3. slm/__init__.py +1 -1
  4. slm/admin.py +40 -3
  5. slm/api/edit/views.py +37 -2
  6. slm/api/public/serializers.py +1 -1
  7. slm/defines/CoordinateMode.py +9 -0
  8. slm/defines/SiteLogFormat.py +19 -6
  9. slm/defines/__init__.py +24 -22
  10. slm/file_views/apps.py +7 -0
  11. slm/file_views/config.py +253 -0
  12. slm/file_views/settings.py +124 -0
  13. slm/file_views/static/slm/file_views/banner_header.png +0 -0
  14. slm/file_views/static/slm/file_views/css/listing.css +82 -0
  15. slm/file_views/templates/slm/file_views/listing.html +70 -0
  16. slm/file_views/urls.py +47 -0
  17. slm/file_views/views.py +472 -0
  18. slm/forms.py +22 -4
  19. slm/jinja2/slm/sitelog/ascii_9char.log +1 -1
  20. slm/jinja2/slm/sitelog/legacy.log +1 -1
  21. slm/management/commands/check_upgrade.py +25 -19
  22. slm/management/commands/generate_sinex.py +9 -7
  23. slm/map/settings.py +0 -0
  24. slm/migrations/0001_alter_archivedsitelog_size_and_more.py +44 -0
  25. slm/migrations/0032_archiveindex_valid_range_and_more.py +8 -1
  26. slm/migrations/simplify_daily_index_files.py +86 -0
  27. slm/models/index.py +73 -6
  28. slm/models/sitelog.py +6 -0
  29. slm/models/system.py +35 -2
  30. slm/parsing/__init__.py +10 -0
  31. slm/parsing/legacy/binding.py +3 -2
  32. slm/receivers/cache.py +25 -0
  33. slm/settings/root.py +22 -0
  34. slm/settings/routines.py +2 -0
  35. slm/settings/slm.py +58 -0
  36. slm/settings/urls.py +1 -1
  37. slm/settings/validation.py +5 -4
  38. slm/signals.py +3 -4
  39. slm/static/slm/js/enums.js +7 -6
  40. slm/static/slm/js/form.js +25 -14
  41. slm/static/slm/js/slm.js +4 -2
  42. slm/templatetags/slm.py +1 -1
  43. slm/utils.py +161 -36
  44. slm/validators.py +51 -0
  45. {igs_slm-0.1.5b3.dist-info → igs_slm-0.2.0b1.dist-info}/WHEEL +0 -0
  46. {igs_slm-0.1.5b3.dist-info → igs_slm-0.2.0b1.dist-info}/entry_points.txt +0 -0
  47. {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 is published, or when a section of a site log is
50
- published. Its possible that both the previous and new status states are
51
- Published. If this happens a site log was edited and published simultaneously.
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.
@@ -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"], ["txt", "sitelog"]);
205
- static GEODESY_ML = new SiteLogFormat(2, "GEODESY_ML", "GeodesyML", "application/xml", "bi bi-filetype-xml", "xml", ["xml"], ["gml"]);
206
- static JSON = new SiteLogFormat(3, "JSON", "JSON", "application/json", "bi bi-filetype-json", "json", ["json", "js"], ["js"]);
207
- static ASCII_9CHAR = new SiteLogFormat(4, "ASCII_9CHAR", "ASCII (9-Char)", "text/plain", "bi bi-file-text", "log", ["text", "txt", "9char"], ["txt", "sitelog"]);
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, alt_exts) {
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.alt_exts = alt_exts;
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[field.name] = value;
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
- default:
238
- if (ipt.length > 0) {
239
- throw TypeError(`Array given for form data type: ${ipt.prop('type')}`);
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.map((e, i) => { setField(e, values[i]); });
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
- switch (types) {
306
- case ['date', 'time']:
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
- switch (types) {
316
- case ['date', 'time']:
317
- return `${inputs.get(0).value}T${inputs.get(1).value || "00:00"}Z`
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 = 47
114
+ limit = 48
115
115
  lines = [line.rstrip() for line in text.split("\n")]
116
116
  limited = []
117
117
  for line in lines:
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(dddmmssss):
43
- if dddmmssss is not None:
44
- if isinstance(dddmmssss, str):
45
- dddmmssss = float(dddmmssss)
46
- dddmmssss /= 10000
47
- degrees = int(dddmmssss)
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
- def dddmmss_ss_parts(dec):
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 not None:
72
- if isinstance(dec, str):
73
- dec = float(dec)
74
- degrees = int(dec)
75
- minutes = (dec - degrees) * 60
76
- seconds = float(minutes - int(minutes)) * 60
77
- return degrees, abs(int(minutes)), abs(seconds)
78
- return None, None, None
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
- a_e = 6378.1366e3 # meters
254
- f_e = 1 / 298.25642 # IERS2000 standards
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
+ )