Merge branch 'modified-times' into 'v3'
Scan tab show scan duration See merge request openflexure/openflexure-microscope-server!408
This commit is contained in:
commit
13b190a8ca
3 changed files with 75 additions and 17 deletions
|
|
@ -8,12 +8,21 @@ import zipfile
|
||||||
import threading
|
import threading
|
||||||
import json
|
import json
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
import logging
|
||||||
|
|
||||||
from pydantic import BaseModel, field_validator, field_serializer, ConfigDict
|
from pydantic import (
|
||||||
|
BaseModel,
|
||||||
|
field_validator,
|
||||||
|
field_serializer,
|
||||||
|
ConfigDict,
|
||||||
|
ValidationError,
|
||||||
|
)
|
||||||
|
|
||||||
from openflexure_microscope_server.utilities import requires_lock
|
from openflexure_microscope_server.utilities import requires_lock
|
||||||
from openflexure_microscope_server.utilities import make_name_safe
|
from openflexure_microscope_server.utilities import make_name_safe
|
||||||
|
|
||||||
|
LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
IMG_DIR_NAME = "images"
|
IMG_DIR_NAME = "images"
|
||||||
SCAN_ZERO_PAD_DIGITS = 4
|
SCAN_ZERO_PAD_DIGITS = 4
|
||||||
|
|
||||||
|
|
@ -28,11 +37,12 @@ class NotEnoughFreeSpaceError(IOError):
|
||||||
|
|
||||||
|
|
||||||
class ScanInfo(BaseModel):
|
class ScanInfo(BaseModel):
|
||||||
"""Summary information about a scan folder."""
|
"""Summary information for the UI about a scan folder."""
|
||||||
|
|
||||||
name: str
|
name: str
|
||||||
created: float
|
created: float
|
||||||
modified: float
|
modified: float
|
||||||
|
duration: Optional[float]
|
||||||
number_of_images: int
|
number_of_images: int
|
||||||
stitch_available: bool
|
stitch_available: bool
|
||||||
dzi: Optional[str]
|
dzi: Optional[str]
|
||||||
|
|
@ -257,14 +267,7 @@ class ScanDirectoryManager:
|
||||||
This is a dictionary not a base model as the data format has changed
|
This is a dictionary not a base model as the data format has changed
|
||||||
somewhat over time.
|
somewhat over time.
|
||||||
"""
|
"""
|
||||||
json_fpath = self.get_scan_data_path(scan_name)
|
return ScanDirectory(scan_name, self.base_dir).get_scan_data_dict()
|
||||||
if json_fpath is None:
|
|
||||||
return None
|
|
||||||
try:
|
|
||||||
with open(json_fpath, "r", encoding="utf-8") as data_file:
|
|
||||||
return json.load(data_file)
|
|
||||||
except (json.decoder.JSONDecodeError, IOError):
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@requires_lock
|
@requires_lock
|
||||||
|
|
@ -469,8 +472,40 @@ class ScanDirectory:
|
||||||
"""Return the modified time of the directory."""
|
"""Return the modified time of the directory."""
|
||||||
return max(os.stat(root).st_mtime for root, _, _ in os.walk(self.dir_path))
|
return max(os.stat(root).st_mtime for root, _, _ in os.walk(self.dir_path))
|
||||||
|
|
||||||
|
def get_scan_data_dict(self) -> Optional[dict[str, Any]]:
|
||||||
|
"""Return the scan data from the json file as a dictionary.
|
||||||
|
|
||||||
|
This is safer than get_scan_data for older scans before a defined model was
|
||||||
|
used.
|
||||||
|
|
||||||
|
:return: The data as a dictionary or None if it couldn't be loaded.
|
||||||
|
"""
|
||||||
|
if self.scan_data_path is None:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
with open(self.scan_data_path, "r", encoding="utf-8") as data_file:
|
||||||
|
return json.load(data_file)
|
||||||
|
except (json.decoder.JSONDecodeError, IOError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_scan_data(self) -> Optional[ScanData]:
|
||||||
|
"""Return the scan data from the json file as a ScanData model.
|
||||||
|
|
||||||
|
:return: The data as a ScanData model or None if it couldn't be loaded or
|
||||||
|
valdiated.
|
||||||
|
"""
|
||||||
|
data_dict = self.get_scan_data_dict()
|
||||||
|
if data_dict is None:
|
||||||
|
LOGGER.warning(f"Could not load scan data for {self.name}.")
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return ScanData(**data_dict)
|
||||||
|
except ValidationError:
|
||||||
|
LOGGER.warning(f"Could not validate scan data for {self.name}.")
|
||||||
|
return None
|
||||||
|
|
||||||
def scan_info(self) -> ScanInfo:
|
def scan_info(self) -> ScanInfo:
|
||||||
"""Return the information for the scan directory as a ScanInfo object."""
|
"""Return the information to be used in the UI for the scan."""
|
||||||
scan_files = self.get_scan_files()
|
scan_files = self.get_scan_files()
|
||||||
scan_images = self._extract_scan_images(scan_files)
|
scan_images = self._extract_scan_images(scan_files)
|
||||||
stitches = self._extract_final_stitches(scan_files)
|
stitches = self._extract_final_stitches(scan_files)
|
||||||
|
|
@ -479,10 +514,18 @@ class ScanDirectory:
|
||||||
stitch_available = len(stitches) > 0
|
stitch_available = len(stitches) > 0
|
||||||
dzi = None if not dzi_files else str(dzi_files[0])
|
dzi = None if not dzi_files else str(dzi_files[0])
|
||||||
|
|
||||||
|
scan_data = self.get_scan_data()
|
||||||
|
duration = (
|
||||||
|
None
|
||||||
|
if scan_data is None or scan_data.duration is None
|
||||||
|
else scan_data.duration.total_seconds()
|
||||||
|
)
|
||||||
|
|
||||||
return ScanInfo(
|
return ScanInfo(
|
||||||
name=self.name,
|
name=self.name,
|
||||||
created=self.created_time,
|
created=self.created_time,
|
||||||
modified=self.get_modified_time(),
|
modified=self.get_modified_time(),
|
||||||
|
duration=duration,
|
||||||
number_of_images=number_of_images,
|
number_of_images=number_of_images,
|
||||||
stitch_available=stitch_available,
|
stitch_available=stitch_available,
|
||||||
dzi=dzi,
|
dzi=dzi,
|
||||||
|
|
|
||||||
|
|
@ -61,16 +61,16 @@
|
||||||
Show Stitched Scan
|
Show Stitched Scan
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="scan-info">
|
||||||
<ul>
|
<ul>
|
||||||
<li>{{ scanData.number_of_images }} images</li>
|
<li>{{ scanData.number_of_images }} images</li>
|
||||||
<li>created: {{ formatDate(scanData.created) }}</li>
|
<li>created: {{ formatDate(scanData.created) }}</li>
|
||||||
<li>modified: {{ formatDate(scanData.modified) }}</li>
|
<li>duration: {{ formatDuration(scanData.duration) }}</li>
|
||||||
</ul>
|
</ul>
|
||||||
<ul v-if="!ongoing">
|
<ul v-if="!ongoing">
|
||||||
<li v-if="scanData.number_of_images<3" class="warning-msg">Not enough images to stitch</li>
|
<li v-if="scanData.number_of_images<3" class="warning-msg">Not enough images to stitch</li>
|
||||||
<li v-else-if="!scanData.dzi & scanData.stitch_available" class="alert-msg">Interactive preview not available</li>
|
<li v-else-if="!scanData.dzi && scanData.stitch_available" class="alert-msg">Interactive preview not available</li>
|
||||||
<li v-else-if=!scanData.stitch_available class="alert-msg">High quality stitch not available</li>
|
<li v-else-if="!scanData.stitch_available" class="alert-msg">High quality stitch not available</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -129,6 +129,20 @@ export default {
|
||||||
.padStart(2, "0");
|
.padStart(2, "0");
|
||||||
return `${HH}:${MM} ${dd}/${mm}/${yyyy}`;
|
return `${HH}:${MM} ${dd}/${mm}/${yyyy}`;
|
||||||
},
|
},
|
||||||
|
formatDuration(duration) {
|
||||||
|
if (duration == null || isNaN(duration)) return "Not known";
|
||||||
|
|
||||||
|
const h = Math.floor(duration / 3600);
|
||||||
|
const m = Math.floor((duration % 3600) / 60);
|
||||||
|
const s = Math.floor(duration % 60);
|
||||||
|
|
||||||
|
const m_pad = String(m).padStart(2, "0");
|
||||||
|
const s_pad = String(s).padStart(2, "0");
|
||||||
|
|
||||||
|
if (h > 0) return `${h}h ${m_pad}m ${s_pad}s`;
|
||||||
|
if (m > 0) return `${m_pad}m ${s_pad}s`;
|
||||||
|
return `${s_pad}s`;
|
||||||
|
},
|
||||||
requestViewer() {
|
requestViewer() {
|
||||||
// Notify parent that thumbnail was clicked
|
// Notify parent that thumbnail was clicked
|
||||||
if (!this.ongoing) {
|
if (!this.ongoing) {
|
||||||
|
|
@ -162,10 +176,11 @@ export default {
|
||||||
</script>
|
</script>
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
ul {
|
ul {
|
||||||
display: inline-block;
|
display: block;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
list-style-type:none;
|
list-style-type:none;
|
||||||
margin: 5px 0px 10px 0px;
|
margin: 5px 0px 10px 0px;
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.warning-msg {
|
.warning-msg {
|
||||||
|
|
|
||||||
|
|
@ -106,7 +106,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="scanning">
|
<div v-show="scanning">
|
||||||
<h2 v-if="displayImageOnRight">
|
<h2 v-if="displayImageOnRight" style="text-align: center;">
|
||||||
Live stitching preview
|
Live stitching preview
|
||||||
</h2>
|
</h2>
|
||||||
<mini-stream-display v-if="displayImageOnRight" />
|
<mini-stream-display v-if="displayImageOnRight" />
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue