import uuid import math import os import re from pathlib import Path from urllib.parse import urljoin from django.conf import settings from django.db import models from django.urls import reverse from troggle.core.utils import height_from_utm, throw # from troggle.core.models.troggle import DataIssue # circular import. Hmm class SurvexFile(models.Model): path = models.CharField(max_length=200) primary = models.ForeignKey( "SurvexFile", related_name="primarysurvex", blank=True, null=True, on_delete=models.SET_NULL ) cave = models.ForeignKey("Cave", blank=True, null=True, on_delete=models.SET_NULL) class Meta: ordering = ("id",) # Don't change from the default as that breaks troggle webpages and internal referencing! # def __str__(self): # return "[SurvexFile:"+str(self.path) + "-" + str(self.survexdirectory) + "-" + str(self.cave)+"]" def exists(self): """This is only used within the Django templates """ fname = Path(settings.SURVEX_DATA, self.path + ".svx") return fname.is_file() # Don't change from the default as that breaks troggle webpages and internal referencing! # def __str__(self): # return "[SurvexFile:"+str(self.path) + "-" + str(self.survexdirectory) + "-" + str(self.cave)+"]" def __str__(self): return self.path class SurvexStationLookUpManager(models.Manager): """what this does, https://docs.djangoproject.com/en/dev/topics/db/managers/ This changes the .objects thinggy to use a case-insensitive match name__iexact so that now SurvexStation.objects.lookup() works as a case-insensitive match """ def lookup(self, name): blocknames, sep, stationname = name.rpartition(".") return self.get(block=SurvexBlock.objects.lookup(blocknames), name__iexact=stationname) class SurvexStation(models.Model): name = models.CharField(max_length=100) # block = models.ForeignKey("SurvexBlock", null=True, on_delete=models.SET_NULL) # block not used since 2020. survex stations objects are only used for entrance locations and all taken from the .3d file objects = SurvexStationLookUpManager() # overwrites SurvexStation.objects and enables lookup() x = models.FloatField(blank=True, null=True) y = models.FloatField(blank=True, null=True) z = models.FloatField(blank=True, null=True) entrance = models.ForeignKey("Entrance", blank=True, null=True, on_delete=models.SET_NULL) class Meta: ordering = ("id",) def __str__(self): return self.name and str(self.name) or "no name" def latlong(self): return utmToLatLng(33, self.x, self.y, northernHemisphere=True) def lat(self): return utmToLatLng(33, self.x, self.y, northernHemisphere=True)[0] def long(self): return utmToLatLng(33, self.x, self.y, northernHemisphere=True)[1] def srtm_alt(self): """Caches the srtm data so that searches are not done twice on the same page""" if not hasattr(self,"srtm"): self.srtm = height_from_utm(self.x, self.y) # height, distance from reference point return self.srtm # (nearest point, nearest distance) def srtm_diff(self): alt, ref = self.srtm_alt() diff = alt - self.z if diff >= 0.3: colour = "blue" sign = "+" elif diff <= -0.3: colour = "red" sign = "" else: diff = 0 colour = "grey" sign = "" if abs(diff) > 60: weight = "bold" else: weight = "normal" diff_str = f"{sign}{diff:.0f}" if ref >= throw: colour = "grey" weight = "normal" diff = "XX" ref = float("nan") diff_str = f"XX" return diff_str, ref def gpx_location(s): # s == self # 1857.90tunnocks latitude, longitude = utmToLatLng(33, s.x, s.y, northernHemisphere=True) if s.name: return f'{s.z:0.0f}{s.name[5:]}\n' else: return "" def utmToLatLng(zone, easting, northing, northernHemisphere=True): # move this to utils.py ? if not northernHemisphere: northing = 10000000 - northing a = 6378137 e = 0.081819191 e1sq = 0.006739497 k0 = 0.9996 arc = northing / k0 mu = arc / (a * (1 - math.pow(e, 2) / 4.0 - 3 * math.pow(e, 4) / 64.0 - 5 * math.pow(e, 6) / 256.0)) ei = (1 - math.pow((1 - e * e), (1 / 2.0))) / (1 + math.pow((1 - e * e), (1 / 2.0))) ca = 3 * ei / 2 - 27 * math.pow(ei, 3) / 32.0 cb = 21 * math.pow(ei, 2) / 16 - 55 * math.pow(ei, 4) / 32 cc = 151 * math.pow(ei, 3) / 96 cd = 1097 * math.pow(ei, 4) / 512 phi1 = mu + ca * math.sin(2 * mu) + cb * math.sin(4 * mu) + cc * math.sin(6 * mu) + cd * math.sin(8 * mu) n0 = a / math.pow((1 - math.pow((e * math.sin(phi1)), 2)), (1 / 2.0)) r0 = a * (1 - e * e) / math.pow((1 - math.pow((e * math.sin(phi1)), 2)), (3 / 2.0)) fact1 = n0 * math.tan(phi1) / r0 _a1 = 500000 - easting dd0 = _a1 / (n0 * k0) fact2 = dd0 * dd0 / 2 t0 = math.pow(math.tan(phi1), 2) Q0 = e1sq * math.pow(math.cos(phi1), 2) fact3 = (5 + 3 * t0 + 10 * Q0 - 4 * Q0 * Q0 - 9 * e1sq) * math.pow(dd0, 4) / 24 fact4 = (61 + 90 * t0 + 298 * Q0 + 45 * t0 * t0 - 252 * e1sq - 3 * Q0 * Q0) * math.pow(dd0, 6) / 720 lof1 = _a1 / (n0 * k0) lof2 = (1 + 2 * t0 + Q0) * math.pow(dd0, 3) / 6.0 lof3 = (5 - 2 * Q0 + 28 * t0 - 3 * math.pow(Q0, 2) + 8 * e1sq + 24 * math.pow(t0, 2)) * math.pow(dd0, 5) / 120 _a2 = (lof1 - lof2 + lof3) / math.cos(phi1) _a3 = _a2 * 180 / math.pi latitude = 180 * (phi1 - fact1 * (fact2 + fact3 + fact4)) / math.pi if not northernHemisphere: latitude = -latitude longitude = ((zone > 0) and (6 * zone - 183.0) or 3.0) - _a3 return (latitude, longitude) # # Single SurvexBlock # class SurvexBlockLookUpManager(models.Manager): """what this does, https://docs.djangoproject.com/en/dev/topics/db/managers/ This adds a method to the .objects thinggy to use a case-insensitive match name__iexact so that now SurvexBlock.objects.lookup() works as a case-insensitive match. This is used in lookup() in SurvexStationLookUpManager() which is used in Entrance().other_location() which is used in the Cave webpage """ def lookup(self, name): if name == "": blocknames = [] else: blocknames = name.split(".") block = SurvexBlock.objects.get(parent=None, survexfile__path=settings.SURVEX_TOPNAME) for blockname in blocknames: block = SurvexBlock.objects.get(parent=block, name__iexact=blockname) return block class SurvexFix(models.Model): """a *fix line in a survexfile. New at the end of expo in July 2025 This is used to detect *fix stations which are not attached to a *entrance and thus to a cave i.e. it is used to discover potential cave entrances But this does not include the virtual *fix locations which are in locations.py """ objects = SurvexBlockLookUpManager() # overwrites Survexfix.objects and enables lookup() but is this a mistake, Block? name = models.CharField(max_length=100) class Meta: ordering = ("name",) def __str__(self): return self.name + str(self.id) class SurvexBlock(models.Model): """One begin..end block within a survex file. The basic element of a survey trip. Multiple anonymous survex blocks are possible within the same surfex file Blocks can span several *included survexfile though. """ # This ID is generated as soon as you call SurvexBlock((). So we can use it while assembling the data # into the survexblock without having to keep doing a database transaction _blockid = models.UUIDField( primary_key=True, unique=True, default=uuid.uuid4, editable=False ) objects = SurvexBlockLookUpManager() # overwrites SurvexBlock.objects and enables lookup() name = models.CharField(blank=True, max_length=100) title = models.CharField(blank=True, max_length=200) parent = models.ForeignKey("SurvexBlock", blank=True, null=True, on_delete=models.SET_NULL, db_index=True) ref_text = models.CharField(max_length=400, blank=True, null=True) date = models.DateField(blank=True, null=True) expedition = models.ForeignKey("Expedition", blank=True, null=True, on_delete=models.SET_NULL, db_index=True) # if the survexfile object is deleted, then all the survex-blocks in it should be too, # though a block can span more than one file... survexfile = models.ForeignKey("SurvexFile", blank=True, null=True, on_delete=models.CASCADE, db_index=True) # survexpath = models.CharField(max_length=200, blank=True, null=True) No need for this anymore scanswallet = models.ForeignKey( "Wallet", blank=True, null=True, on_delete=models.SET_NULL, db_index=True ) # only ONE wallet per block. The most recent seen overwites.. ugh. Should fix this sometime. legsall = models.IntegerField(null=True) # summary data for this block legslength = models.FloatField(null=True) foreigners = models.BooleanField(default=False) class Meta: ordering = ("_blockid",) def __str__(self): return self.name and str(self.name) or "no_name-#" + str(self.pk)[:5] #pk is primary key def isSurvexBlock(self): # Function used in templates return True def year(self): if self.date: return self.date.year else: return 1970 def DayIndex(self): """This is used to set different colours for the different trips on the calendar view of the expedition""" # print(f"SurvexBlock DayIndex {self.name} '{self.date}' {len(list(SurvexBlock.objects.filter(date=self.date)))} on this date") mx = 10 todays = list(SurvexBlock.objects.filter(date=self.date)) if self in todays: index = todays.index(self) else: print(f"DayIndex: Synchronization error in survex blocks. Restart server or do full reset. {self}") index = 0 if index not in range(0, mx): print(f"DayIndex: More than {mx-1} SurvexBlock items on one day '{index}' {self}, restarting colour sequence.") index = index % mx return index class SurvexPersonRole(models.Model): """The CASCADE means that if a SurvexBlock or a Person is deleted, then the SurvexPersonRole is deleted too """ survexblock = models.ForeignKey("SurvexBlock", on_delete=models.CASCADE, db_index=True) # increasing levels of precision, Surely we only need survexblock and (either person or personexpedition)? personname = models.CharField(max_length=100) person = models.ForeignKey("Person", blank=True, null=True, on_delete=models.CASCADE, db_index=True) # not needed personexpedition = models.ForeignKey("PersonExpedition", blank=True, null=True, on_delete=models.SET_NULL, db_index=True) def __str__(self): return str(self.personname) + " - " + str(self.survexblock) class SingleScan(models.Model): """A single file holding an image. Could be raw notes, an elevation plot or whatever""" ffile = models.CharField(max_length=200) name = models.CharField(max_length=200) wallet = models.ForeignKey("Wallet", null=True, on_delete=models.SET_NULL) class Meta: ordering = ("name",) def get_absolute_url(self): # we do not use URL_ROOT any more. return reverse("scansingle", kwargs={"path": re.sub("#", "%23", self.wallet.walletname), "file": self.name}) def __str__(self): return "Scan Image: " + str(self.name) + " in " + str(self.wallet) class DrawingFile(models.Model): """A file holding a Therion (several types) or a Tunnel drawing Most of the implied capabilities are not implemented yet""" dwgpath = models.CharField(max_length=200) dwgname = models.CharField(max_length=200) dwgwallets = models.ManyToManyField("Wallet") # implicitly links via folders to scans to SVX files scans = models.ManyToManyField("SingleScan") # implicitly links via scans to SVX files # dwgcontains = models.ManyToManyField("DrawingFile") # case when its a frame type - not populated yet filesize = models.IntegerField(default=0) npaths = models.IntegerField(default=0) # survexfiles = models.ManyToManyField("SurvexFile") # direct link to SVX files - not populated yet class Meta: ordering = ("dwgpath",) def __str__(self): return "Drawing File: " + str(self.dwgname) + " (" + str(self.filesize) + " bytes)"