Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
ba73409
test: add unit tests for ConfigurationModel
rabii-chaarani Nov 24, 2025
f0381eb
feat: add DataConversionWidget
rabii-chaarani Nov 24, 2025
62807e7
feat: add data conversion GUI components
rabii-chaarani Nov 24, 2025
a195cc1
feat: implement data conversion configuration helpers
rabii-chaarani Nov 24, 2025
b1e8112
feat: DataConversionWidget implemenation
rabii-chaarani Nov 24, 2025
40d2945
fix: improve ui handling
rabii-chaarani Nov 26, 2025
cb9dfcb
fix: update config file
rabii-chaarani Nov 26, 2025
1da4630
refactor: renamed layers
rabii-chaarani Nov 26, 2025
47ce01b
refactor: update geodataframe conversion
rabii-chaarani Nov 26, 2025
076a08c
feat: add run functionality to manual conversion
rabii-chaarani Nov 28, 2025
ebc532a
Merge remote-tracking branch 'origin/dev-0.1.12' into data_converter_…
rabii-chaarani Jan 27, 2026
c108f56
fix: remove manual conversion code
rabii-chaarani Jan 27, 2026
303ca20
fix: add loopdataconverter to requirements
lachlangrose Jan 28, 2026
ba3d524
fix: added data converter as dialog
rabii-chaarani Jan 28, 2026
d1afe01
feat: added along section thickness calc
rabii-chaarani Jan 29, 2026
dbe347c
refactor: added along section code
rabii-chaarani Jan 29, 2026
33f12aa
Merge remote-tracking branch 'origin/dev-0.1.12' into data_converter_…
rabii-chaarani Jan 29, 2026
d901fe3
feat: add layer guessing and filters to conversion widget
rabii-chaarani Jan 29, 2026
2c0fe3b
fix: applying some copilot changes
lachlangrose Jan 30, 2026
2e59d9c
Merge branch 'dev-0.1.12' into data_converter_tool
lachlangrose Jan 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions loopstructural/gui/data_conversion/__init__.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
"""Data conversion GUI components."""

from .data_conversion_widget import AutomaticConversionDialog, AutomaticConversionWidget

__all__ = ["AutomaticConversionDialog", "AutomaticConversionWidget"]
149 changes: 149 additions & 0 deletions loopstructural/gui/data_conversion/configuration.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
"""Configuration helpers used by the data conversion UI."""

from __future__ import annotations

from copy import deepcopy
from typing import Any, Dict, Iterable, MutableMapping


class Config:
"""Container for the default NTGS configuration."""

def __init__(self) -> None:
self.fold_config = {
"structtype_column": "FoldType",
"fold_text": "'Anticline','Syncline','Antiform','Synform','Monocline','Monoform','Neutral','Fold axis','Overturned syncline'",
"description_column": "Desc",
"synform_text": "FoldType",
"foldname_column": "FoldName",
"objectid_column": "OBJECTID",
"tightness_column": "IntlimbAng",
"axial_plane_dipdir_column": "AxPlDipDir",
"axial_plane_dip_column": "AxPlDip",
}

self.fault_config = {
"orientation_type": "dip direction",
"structtype_column": "FaultType",
"fault_text": "'Thrust','Reverse','Normal','Shear zone','Strike-slip','Thrust','Unknown'",
"dip_null_value": "-999",
"dipdir_flag": "num",
"dipdir_column": "DipDir",
"dip_column": "Dip",
"dipestimate_column": "DipEstimate",
"dipestimate_text": "'NORTH_EAST','NORTH',<rest of cardinals>,'NOT ACCESSED'",
"displacement_column": "Displace",
"displacement_text": "'1m-100m', '100m-1km', '1km-5km', '>5km'",
"fault_length_column": "FaultLen",
"fault_length_text": "Small (0-5km),Medium (5-30km),Large (30-100km),Regional (>100km),Unclassified",
"name_column": "FaultName",
"objectid_column": "OBJECTID",
}

self.geology_config = {
"unitname_column": "Formation",
"alt_unitname_column": "Formation",
"group_column": "Group",
"supergroup_column": "Supergroup",
"description_column": "LithDescn1",
"minage_column": "AgeMin",
"maxage_column": "AgeMax",
"rocktype_column": "LithClass",
"alt_rocktype_column": "RockCat",
"sill_text": "RockCat",
"intrusive_text": "RockCat",
"volcanic_text": "RockCat",
"objectid_column": "OBJECTID",
"ignore_lithology_codes": ["cover", "Unknown"],
}

self.structure_config = {
"orientation_type": "dip direction",
"dipdir_column": "DipDir",
"dip_column": "Dip",
"description_column": "FeatDesc",
"bedding_text": "ObsType",
"overturned_column": "Desc",
"overturned_text": "overturned",
"objectid_column": "OBJECTID",
}

self.config_map = {
"geology": self.geology_config,
"structure": self.structure_config,
"fault": self.fault_config,
"fold": self.fold_config,
}

def __getitem__(self, datatype: str) -> Dict[str, Any]:
return self.config_map[datatype]

def as_dict(self) -> Dict[str, Dict[str, Any]]:
"""Return a deep copy of the configuration map."""
return deepcopy(self.config_map)


def _coerce_config_value(template_value: Any, new_value: Any) -> Any:
"""Coerce user supplied values into the template format."""
if isinstance(template_value, list):
if isinstance(new_value, list):
return new_value
if new_value in (None, ""):
return []
if isinstance(new_value, str):
return [item.strip() for item in new_value.split(",") if item.strip()]
return [str(new_value)]

if isinstance(template_value, (int, float)):
try:
return type(template_value)(new_value)
except (TypeError, ValueError):
return template_value

if template_value is None:
return new_value

if new_value is None:
return ""

return str(new_value)


class ConfigurationState:
"""State holder for the NTGS configuration mapping."""

def __init__(self, *, base_config: MutableMapping[str, Dict[str, Any]] | None = None):
self._config = deepcopy(base_config) if base_config is not None else Config().as_dict()

def data_types(self) -> Iterable[str]:
"""Return the supported data types."""
return self._config.keys()

def get_config_for_type(self, data_type: str) -> Dict[str, Any]:
"""Return a copy of the configuration for a single data type."""
self._ensure_data_type(data_type)
return deepcopy(self._config[data_type])

def set_value(self, data_type: str, key: str, value: Any) -> None:
"""Update a single configuration entry."""
self._ensure_data_type(data_type)
template_value = self._config[data_type].get(key)
self._config[data_type][key] = _coerce_config_value(template_value, value)

def update_values(self, data_type: str, updates: Dict[str, Any]) -> None:
"""Bulk update configuration entries for a type."""
for key, value in updates.items():
self.set_value(data_type, key, value)

def get_value(self, data_type: str, key: str) -> Any:
"""Return the stored value for a configuration entry."""
self._ensure_data_type(data_type)
return self._config[data_type].get(key)

def as_dict(self) -> Dict[str, Dict[str, Any]]:
"""Return a deep copy of the entire configuration map."""
return deepcopy(self._config)

def _ensure_data_type(self, data_type: str) -> None:
if data_type not in self._config:
raise KeyError(f"Unknown data type '{data_type}'")
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Data converter Widget by rabii-chaarani · Pull Request #76 · Loop3D/plugin_loopstructural · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
ba73409
test: add unit tests for ConfigurationModel
rabii-chaarani Nov 24, 2025
f0381eb
feat: add DataConversionWidget
rabii-chaarani Nov 24, 2025
62807e7
feat: add data conversion GUI components
rabii-chaarani Nov 24, 2025
a195cc1
feat: implement data conversion configuration helpers
rabii-chaarani Nov 24, 2025
b1e8112
feat: DataConversionWidget implemenation
rabii-chaarani Nov 24, 2025
40d2945
fix: improve ui handling
rabii-chaarani Nov 26, 2025
cb9dfcb
fix: update config file
rabii-chaarani Nov 26, 2025
1da4630
refactor: renamed layers
rabii-chaarani Nov 26, 2025
47ce01b
refactor: update geodataframe conversion
rabii-chaarani Nov 26, 2025
076a08c
feat: add run functionality to manual conversion
rabii-chaarani Nov 28, 2025
ebc532a
Merge remote-tracking branch 'origin/dev-0.1.12' into data_converter_…
rabii-chaarani Jan 27, 2026
c108f56
fix: remove manual conversion code
rabii-chaarani Jan 27, 2026
303ca20
fix: add loopdataconverter to requirements
lachlangrose Jan 28, 2026
ba3d524
fix: added data converter as dialog
rabii-chaarani Jan 28, 2026
d1afe01
feat: added along section thickness calc
rabii-chaarani Jan 29, 2026
dbe347c
refactor: added along section code
rabii-chaarani Jan 29, 2026
33f12aa
Merge remote-tracking branch 'origin/dev-0.1.12' into data_converter_…
rabii-chaarani Jan 29, 2026
d901fe3
feat: add layer guessing and filters to conversion widget
rabii-chaarani Jan 29, 2026
2c0fe3b
fix: applying some copilot changes
lachlangrose Jan 30, 2026
2e59d9c
Merge branch 'dev-0.1.12' into data_converter_tool
lachlangrose Jan 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions loopstructural/gui/data_conversion/__init__.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
"""Data conversion GUI components."""

from .data_conversion_widget import AutomaticConversionDialog, AutomaticConversionWidget

__all__ = ["AutomaticConversionDialog", "AutomaticConversionWidget"]
149 changes: 149 additions & 0 deletions loopstructural/gui/data_conversion/configuration.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
"""Configuration helpers used by the data conversion UI."""

from __future__ import annotations

from copy import deepcopy
from typing import Any, Dict, Iterable, MutableMapping


class Config:
"""Container for the default NTGS configuration."""

def __init__(self) -> None:
self.fold_config = {
"structtype_column": "FoldType",
"fold_text": "'Anticline','Syncline','Antiform','Synform','Monocline','Monoform','Neutral','Fold axis','Overturned syncline'",
"description_column": "Desc",
"synform_text": "FoldType",
"foldname_column": "FoldName",
"objectid_column": "OBJECTID",
"tightness_column": "IntlimbAng",
"axial_plane_dipdir_column": "AxPlDipDir",
"axial_plane_dip_column": "AxPlDip",
}

self.fault_config = {
"orientation_type": "dip direction",
"structtype_column": "FaultType",
"fault_text": "'Thrust','Reverse','Normal','Shear zone','Strike-slip','Thrust','Unknown'",
"dip_null_value": "-999",
"dipdir_flag": "num",
"dipdir_column": "DipDir",
"dip_column": "Dip",
"dipestimate_column": "DipEstimate",
"dipestimate_text": "'NORTH_EAST','NORTH',<rest of cardinals>,'NOT ACCESSED'",
"displacement_column": "Displace",
"displacement_text": "'1m-100m', '100m-1km', '1km-5km', '>5km'",
"fault_length_column": "FaultLen",
"fault_length_text": "Small (0-5km),Medium (5-30km),Large (30-100km),Regional (>100km),Unclassified",
"name_column": "FaultName",
"objectid_column": "OBJECTID",
}

self.geology_config = {
"unitname_column": "Formation",
"alt_unitname_column": "Formation",
"group_column": "Group",
"supergroup_column": "Supergroup",
"description_column": "LithDescn1",
"minage_column": "AgeMin",
"maxage_column": "AgeMax",
"rocktype_column": "LithClass",
"alt_rocktype_column": "RockCat",
"sill_text": "RockCat",
"intrusive_text": "RockCat",
"volcanic_text": "RockCat",
"objectid_column": "OBJECTID",
"ignore_lithology_codes": ["cover", "Unknown"],
}

self.structure_config = {
"orientation_type": "dip direction",
"dipdir_column": "DipDir",
"dip_column": "Dip",
"description_column": "FeatDesc",
"bedding_text": "ObsType",
"overturned_column": "Desc",
"overturned_text": "overturned",
"objectid_column": "OBJECTID",
}

self.config_map = {
"geology": self.geology_config,
"structure": self.structure_config,
"fault": self.fault_config,
"fold": self.fold_config,
}

def __getitem__(self, datatype: str) -> Dict[str, Any]:
return self.config_map[datatype]

def as_dict(self) -> Dict[str, Dict[str, Any]]:
"""Return a deep copy of the configuration map."""
return deepcopy(self.config_map)


def _coerce_config_value(template_value: Any, new_value: Any) -> Any:
"""Coerce user supplied values into the template format."""
if isinstance(template_value, list):
if isinstance(new_value, list):
return new_value
if new_value in (None, ""):
return []
if isinstance(new_value, str):
return [item.strip() for item in new_value.split(",") if item.strip()]
return [str(new_value)]

if isinstance(template_value, (int, float)):
try:
return type(template_value)(new_value)
except (TypeError, ValueError):
return template_value

if template_value is None:
return new_value

if new_value is None:
return ""

return str(new_value)


class ConfigurationState:
"""State holder for the NTGS configuration mapping."""

def __init__(self, *, base_config: MutableMapping[str, Dict[str, Any]] | None = None):
self._config = deepcopy(base_config) if base_config is not None else Config().as_dict()

def data_types(self) -> Iterable[str]:
"""Return the supported data types."""
return self._config.keys()

def get_config_for_type(self, data_type: str) -> Dict[str, Any]:
"""Return a copy of the configuration for a single data type."""
self._ensure_data_type(data_type)
return deepcopy(self._config[data_type])

def set_value(self, data_type: str, key: str, value: Any) -> None:
"""Update a single configuration entry."""
self._ensure_data_type(data_type)
template_value = self._config[data_type].get(key)
self._config[data_type][key] = _coerce_config_value(template_value, value)

def update_values(self, data_type: str, updates: Dict[str, Any]) -> None:
"""Bulk update configuration entries for a type."""
for key, value in updates.items():
self.set_value(data_type, key, value)

def get_value(self, data_type: str, key: str) -> Any:
"""Return the stored value for a configuration entry."""
self._ensure_data_type(data_type)
return self._config[data_type].get(key)

def as_dict(self) -> Dict[str, Dict[str, Any]]:
"""Return a deep copy of the entire configuration map."""
return deepcopy(self._config)

def _ensure_data_type(self, data_type: str) -> None:
if data_type not in self._config:
raise KeyError(f"Unknown data type '{data_type}'")
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Data converter Widget by rabii-chaarani · Pull Request #76 · Loop3D/plugin_loopstructural · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
ba73409
test: add unit tests for ConfigurationModel
rabii-chaarani Nov 24, 2025
f0381eb
feat: add DataConversionWidget
rabii-chaarani Nov 24, 2025
62807e7
feat: add data conversion GUI components
rabii-chaarani Nov 24, 2025
a195cc1
feat: implement data conversion configuration helpers
rabii-chaarani Nov 24, 2025
b1e8112
feat: DataConversionWidget implemenation
rabii-chaarani Nov 24, 2025
40d2945
fix: improve ui handling
rabii-chaarani Nov 26, 2025
cb9dfcb
fix: update config file
rabii-chaarani Nov 26, 2025
1da4630
refactor: renamed layers
rabii-chaarani Nov 26, 2025
47ce01b
refactor: update geodataframe conversion
rabii-chaarani Nov 26, 2025
076a08c
feat: add run functionality to manual conversion
rabii-chaarani Nov 28, 2025
ebc532a
Merge remote-tracking branch 'origin/dev-0.1.12' into data_converter_…
rabii-chaarani Jan 27, 2026
c108f56
fix: remove manual conversion code
rabii-chaarani Jan 27, 2026
303ca20
fix: add loopdataconverter to requirements
lachlangrose Jan 28, 2026
ba3d524
fix: added data converter as dialog
rabii-chaarani Jan 28, 2026
d1afe01
feat: added along section thickness calc
rabii-chaarani Jan 29, 2026
dbe347c
refactor: added along section code
rabii-chaarani Jan 29, 2026
33f12aa
Merge remote-tracking branch 'origin/dev-0.1.12' into data_converter_…
rabii-chaarani Jan 29, 2026
d901fe3
feat: add layer guessing and filters to conversion widget
rabii-chaarani Jan 29, 2026
2c0fe3b
fix: applying some copilot changes
lachlangrose Jan 30, 2026
2e59d9c
Merge branch 'dev-0.1.12' into data_converter_tool
lachlangrose Jan 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions loopstructural/gui/data_conversion/__init__.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
"""Data conversion GUI components."""

from .data_conversion_widget import AutomaticConversionDialog, AutomaticConversionWidget

__all__ = ["AutomaticConversionDialog", "AutomaticConversionWidget"]
149 changes: 149 additions & 0 deletions loopstructural/gui/data_conversion/configuration.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
"""Configuration helpers used by the data conversion UI."""

from __future__ import annotations

from copy import deepcopy
from typing import Any, Dict, Iterable, MutableMapping


class Config:
"""Container for the default NTGS configuration."""

def __init__(self) -> None:
self.fold_config = {
"structtype_column": "FoldType",
"fold_text": "'Anticline','Syncline','Antiform','Synform','Monocline','Monoform','Neutral','Fold axis','Overturned syncline'",
"description_column": "Desc",
"synform_text": "FoldType",
"foldname_column": "FoldName",
"objectid_column": "OBJECTID",
"tightness_column": "IntlimbAng",
"axial_plane_dipdir_column": "AxPlDipDir",
"axial_plane_dip_column": "AxPlDip",
}

self.fault_config = {
"orientation_type": "dip direction",
"structtype_column": "FaultType",
"fault_text": "'Thrust','Reverse','Normal','Shear zone','Strike-slip','Thrust','Unknown'",
"dip_null_value": "-999",
"dipdir_flag": "num",
"dipdir_column": "DipDir",
"dip_column": "Dip",
"dipestimate_column": "DipEstimate",
"dipestimate_text": "'NORTH_EAST','NORTH',<rest of cardinals>,'NOT ACCESSED'",
"displacement_column": "Displace",
"displacement_text": "'1m-100m', '100m-1km', '1km-5km', '>5km'",
"fault_length_column": "FaultLen",
"fault_length_text": "Small (0-5km),Medium (5-30km),Large (30-100km),Regional (>100km),Unclassified",
"name_column": "FaultName",
"objectid_column": "OBJECTID",
}

self.geology_config = {
"unitname_column": "Formation",
"alt_unitname_column": "Formation",
"group_column": "Group",
"supergroup_column": "Supergroup",
"description_column": "LithDescn1",
"minage_column": "AgeMin",
"maxage_column": "AgeMax",
"rocktype_column": "LithClass",
"alt_rocktype_column": "RockCat",
"sill_text": "RockCat",
"intrusive_text": "RockCat",
"volcanic_text": "RockCat",
"objectid_column": "OBJECTID",
"ignore_lithology_codes": ["cover", "Unknown"],
}

self.structure_config = {
"orientation_type": "dip direction",
"dipdir_column": "DipDir",
"dip_column": "Dip",
"description_column": "FeatDesc",
"bedding_text": "ObsType",
"overturned_column": "Desc",
"overturned_text": "overturned",
"objectid_column": "OBJECTID",
}

self.config_map = {
"geology": self.geology_config,
"structure": self.structure_config,
"fault": self.fault_config,
"fold": self.fold_config,
}

def __getitem__(self, datatype: str) -> Dict[str, Any]:
return self.config_map[datatype]

def as_dict(self) -> Dict[str, Dict[str, Any]]:
"""Return a deep copy of the configuration map."""
return deepcopy(self.config_map)


def _coerce_config_value(template_value: Any, new_value: Any) -> Any:
"""Coerce user supplied values into the template format."""
if isinstance(template_value, list):
if isinstance(new_value, list):
return new_value
if new_value in (None, ""):
return []
if isinstance(new_value, str):
return [item.strip() for item in new_value.split(",") if item.strip()]
return [str(new_value)]

if isinstance(template_value, (int, float)):
try:
return type(template_value)(new_value)
except (TypeError, ValueError):
return template_value

if template_value is None:
return new_value

if new_value is None:
return ""

return str(new_value)


class ConfigurationState:
"""State holder for the NTGS configuration mapping."""

def __init__(self, *, base_config: MutableMapping[str, Dict[str, Any]] | None = None):
self._config = deepcopy(base_config) if base_config is not None else Config().as_dict()

def data_types(self) -> Iterable[str]:
"""Return the supported data types."""
return self._config.keys()

def get_config_for_type(self, data_type: str) -> Dict[str, Any]:
"""Return a copy of the configuration for a single data type."""
self._ensure_data_type(data_type)
return deepcopy(self._config[data_type])

def set_value(self, data_type: str, key: str, value: Any) -> None:
"""Update a single configuration entry."""
self._ensure_data_type(data_type)
template_value = self._config[data_type].get(key)
self._config[data_type][key] = _coerce_config_value(template_value, value)

def update_values(self, data_type: str, updates: Dict[str, Any]) -> None:
"""Bulk update configuration entries for a type."""
for key, value in updates.items():
self.set_value(data_type, key, value)

def get_value(self, data_type: str, key: str) -> Any:
"""Return the stored value for a configuration entry."""
self._ensure_data_type(data_type)
return self._config[data_type].get(key)

def as_dict(self) -> Dict[str, Dict[str, Any]]:
"""Return a deep copy of the entire configuration map."""
return deepcopy(self._config)

def _ensure_data_type(self, data_type: str) -> None:
if data_type not in self._config:
raise KeyError(f"Unknown data type '{data_type}'")
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Data converter Widget by rabii-chaarani · Pull Request #76 · Loop3D/plugin_loopstructural · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
ba73409
test: add unit tests for ConfigurationModel
rabii-chaarani Nov 24, 2025
f0381eb
feat: add DataConversionWidget
rabii-chaarani Nov 24, 2025
62807e7
feat: add data conversion GUI components
rabii-chaarani Nov 24, 2025
a195cc1
feat: implement data conversion configuration helpers
rabii-chaarani Nov 24, 2025
b1e8112
feat: DataConversionWidget implemenation
rabii-chaarani Nov 24, 2025
40d2945
fix: improve ui handling
rabii-chaarani Nov 26, 2025
cb9dfcb
fix: update config file
rabii-chaarani Nov 26, 2025
1da4630
refactor: renamed layers
rabii-chaarani Nov 26, 2025
47ce01b
refactor: update geodataframe conversion
rabii-chaarani Nov 26, 2025
076a08c
feat: add run functionality to manual conversion
rabii-chaarani Nov 28, 2025
ebc532a
Merge remote-tracking branch 'origin/dev-0.1.12' into data_converter_…
rabii-chaarani Jan 27, 2026
c108f56
fix: remove manual conversion code
rabii-chaarani Jan 27, 2026
303ca20
fix: add loopdataconverter to requirements
lachlangrose Jan 28, 2026
ba3d524
fix: added data converter as dialog
rabii-chaarani Jan 28, 2026
d1afe01
feat: added along section thickness calc
rabii-chaarani Jan 29, 2026
dbe347c
refactor: added along section code
rabii-chaarani Jan 29, 2026
33f12aa
Merge remote-tracking branch 'origin/dev-0.1.12' into data_converter_…
rabii-chaarani Jan 29, 2026
d901fe3
feat: add layer guessing and filters to conversion widget
rabii-chaarani Jan 29, 2026
2c0fe3b
fix: applying some copilot changes
lachlangrose Jan 30, 2026
2e59d9c
Merge branch 'dev-0.1.12' into data_converter_tool
lachlangrose Jan 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions loopstructural/gui/data_conversion/__init__.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
"""Data conversion GUI components."""

from .data_conversion_widget import AutomaticConversionDialog, AutomaticConversionWidget

__all__ = ["AutomaticConversionDialog", "AutomaticConversionWidget"]
149 changes: 149 additions & 0 deletions loopstructural/gui/data_conversion/configuration.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
"""Configuration helpers used by the data conversion UI."""

from __future__ import annotations

from copy import deepcopy
from typing import Any, Dict, Iterable, MutableMapping


class Config:
"""Container for the default NTGS configuration."""

def __init__(self) -> None:
self.fold_config = {
"structtype_column": "FoldType",
"fold_text": "'Anticline','Syncline','Antiform','Synform','Monocline','Monoform','Neutral','Fold axis','Overturned syncline'",
"description_column": "Desc",
"synform_text": "FoldType",
"foldname_column": "FoldName",
"objectid_column": "OBJECTID",
"tightness_column": "IntlimbAng",
"axial_plane_dipdir_column": "AxPlDipDir",
"axial_plane_dip_column": "AxPlDip",
}

self.fault_config = {
"orientation_type": "dip direction",
"structtype_column": "FaultType",
"fault_text": "'Thrust','Reverse','Normal','Shear zone','Strike-slip','Thrust','Unknown'",
"dip_null_value": "-999",
"dipdir_flag": "num",
"dipdir_column": "DipDir",
"dip_column": "Dip",
"dipestimate_column": "DipEstimate",
"dipestimate_text": "'NORTH_EAST','NORTH',<rest of cardinals>,'NOT ACCESSED'",
"displacement_column": "Displace",
"displacement_text": "'1m-100m', '100m-1km', '1km-5km', '>5km'",
"fault_length_column": "FaultLen",
"fault_length_text": "Small (0-5km),Medium (5-30km),Large (30-100km),Regional (>100km),Unclassified",
"name_column": "FaultName",
"objectid_column": "OBJECTID",
}

self.geology_config = {
"unitname_column": "Formation",
"alt_unitname_column": "Formation",
"group_column": "Group",
"supergroup_column": "Supergroup",
"description_column": "LithDescn1",
"minage_column": "AgeMin",
"maxage_column": "AgeMax",
"rocktype_column": "LithClass",
"alt_rocktype_column": "RockCat",
"sill_text": "RockCat",
"intrusive_text": "RockCat",
"volcanic_text": "RockCat",
"objectid_column": "OBJECTID",
"ignore_lithology_codes": ["cover", "Unknown"],
}

self.structure_config = {
"orientation_type": "dip direction",
"dipdir_column": "DipDir",
"dip_column": "Dip",
"description_column": "FeatDesc",
"bedding_text": "ObsType",
"overturned_column": "Desc",
"overturned_text": "overturned",
"objectid_column": "OBJECTID",
}

self.config_map = {
"geology": self.geology_config,
"structure": self.structure_config,
"fault": self.fault_config,
"fold": self.fold_config,
}

def __getitem__(self, datatype: str) -> Dict[str, Any]:
return self.config_map[datatype]

def as_dict(self) -> Dict[str, Dict[str, Any]]:
"""Return a deep copy of the configuration map."""
return deepcopy(self.config_map)


def _coerce_config_value(template_value: Any, new_value: Any) -> Any:
"""Coerce user supplied values into the template format."""
if isinstance(template_value, list):
if isinstance(new_value, list):
return new_value
if new_value in (None, ""):
return []
if isinstance(new_value, str):
return [item.strip() for item in new_value.split(",") if item.strip()]
return [str(new_value)]

if isinstance(template_value, (int, float)):
try:
return type(template_value)(new_value)
except (TypeError, ValueError):
return template_value

if template_value is None:
return new_value

if new_value is None:
return ""

return str(new_value)


class ConfigurationState:
"""State holder for the NTGS configuration mapping."""

def __init__(self, *, base_config: MutableMapping[str, Dict[str, Any]] | None = None):
self._config = deepcopy(base_config) if base_config is not None else Config().as_dict()

def data_types(self) -> Iterable[str]:
"""Return the supported data types."""
return self._config.keys()

def get_config_for_type(self, data_type: str) -> Dict[str, Any]:
"""Return a copy of the configuration for a single data type."""
self._ensure_data_type(data_type)
return deepcopy(self._config[data_type])

def set_value(self, data_type: str, key: str, value: Any) -> None:
"""Update a single configuration entry."""
self._ensure_data_type(data_type)
template_value = self._config[data_type].get(key)
self._config[data_type][key] = _coerce_config_value(template_value, value)

def update_values(self, data_type: str, updates: Dict[str, Any]) -> None:
"""Bulk update configuration entries for a type."""
for key, value in updates.items():
self.set_value(data_type, key, value)

def get_value(self, data_type: str, key: str) -> Any:
"""Return the stored value for a configuration entry."""
self._ensure_data_type(data_type)
return self._config[data_type].get(key)

def as_dict(self) -> Dict[str, Dict[str, Any]]:
"""Return a deep copy of the entire configuration map."""
return deepcopy(self._config)

def _ensure_data_type(self, data_type: str) -> None:
if data_type not in self._config:
raise KeyError(f"Unknown data type '{data_type}'")
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' Data converter Widget by rabii-chaarani · Pull Request #76 · Loop3D/plugin_loopstructural · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
ba73409
test: add unit tests for ConfigurationModel
rabii-chaarani Nov 24, 2025
f0381eb
feat: add DataConversionWidget
rabii-chaarani Nov 24, 2025
62807e7
feat: add data conversion GUI components
rabii-chaarani Nov 24, 2025
a195cc1
feat: implement data conversion configuration helpers
rabii-chaarani Nov 24, 2025
b1e8112
feat: DataConversionWidget implemenation
rabii-chaarani Nov 24, 2025
40d2945
fix: improve ui handling
rabii-chaarani Nov 26, 2025
cb9dfcb
fix: update config file
rabii-chaarani Nov 26, 2025
1da4630
refactor: renamed layers
rabii-chaarani Nov 26, 2025
47ce01b
refactor: update geodataframe conversion
rabii-chaarani Nov 26, 2025
076a08c
feat: add run functionality to manual conversion
rabii-chaarani Nov 28, 2025
ebc532a
Merge remote-tracking branch 'origin/dev-0.1.12' into data_converter_…
rabii-chaarani Jan 27, 2026
c108f56
fix: remove manual conversion code
rabii-chaarani Jan 27, 2026
303ca20
fix: add loopdataconverter to requirements
lachlangrose Jan 28, 2026
ba3d524
fix: added data converter as dialog
rabii-chaarani Jan 28, 2026
d1afe01
feat: added along section thickness calc
rabii-chaarani Jan 29, 2026
dbe347c
refactor: added along section code
rabii-chaarani Jan 29, 2026
33f12aa
Merge remote-tracking branch 'origin/dev-0.1.12' into data_converter_…
rabii-chaarani Jan 29, 2026
d901fe3
feat: add layer guessing and filters to conversion widget
rabii-chaarani Jan 29, 2026
2c0fe3b
fix: applying some copilot changes
lachlangrose Jan 30, 2026
2e59d9c
Merge branch 'dev-0.1.12' into data_converter_tool
lachlangrose Jan 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions loopstructural/gui/data_conversion/__init__.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
"""Data conversion GUI components."""

from .data_conversion_widget import AutomaticConversionDialog, AutomaticConversionWidget

__all__ = ["AutomaticConversionDialog", "AutomaticConversionWidget"]
149 changes: 149 additions & 0 deletions loopstructural/gui/data_conversion/configuration.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
"""Configuration helpers used by the data conversion UI."""

from __future__ import annotations

from copy import deepcopy
from typing import Any, Dict, Iterable, MutableMapping


class Config:
"""Container for the default NTGS configuration."""

def __init__(self) -> None:
self.fold_config = {
"structtype_column": "FoldType",
"fold_text": "'Anticline','Syncline','Antiform','Synform','Monocline','Monoform','Neutral','Fold axis','Overturned syncline'",
"description_column": "Desc",
"synform_text": "FoldType",
"foldname_column": "FoldName",
"objectid_column": "OBJECTID",
"tightness_column": "IntlimbAng",
"axial_plane_dipdir_column": "AxPlDipDir",
"axial_plane_dip_column": "AxPlDip",
}

self.fault_config = {
"orientation_type": "dip direction",
"structtype_column": "FaultType",
"fault_text": "'Thrust','Reverse','Normal','Shear zone','Strike-slip','Thrust','Unknown'",
"dip_null_value": "-999",
"dipdir_flag": "num",
"dipdir_column": "DipDir",
"dip_column": "Dip",
"dipestimate_column": "DipEstimate",
"dipestimate_text": "'NORTH_EAST','NORTH',<rest of cardinals>,'NOT ACCESSED'",
"displacement_column": "Displace",
"displacement_text": "'1m-100m', '100m-1km', '1km-5km', '>5km'",
"fault_length_column": "FaultLen",
"fault_length_text": "Small (0-5km),Medium (5-30km),Large (30-100km),Regional (>100km),Unclassified",
"name_column": "FaultName",
"objectid_column": "OBJECTID",
}

self.geology_config = {
"unitname_column": "Formation",
"alt_unitname_column": "Formation",
"group_column": "Group",
"supergroup_column": "Supergroup",
"description_column": "LithDescn1",
"minage_column": "AgeMin",
"maxage_column": "AgeMax",
"rocktype_column": "LithClass",
"alt_rocktype_column": "RockCat",
"sill_text": "RockCat",
"intrusive_text": "RockCat",
"volcanic_text": "RockCat",
"objectid_column": "OBJECTID",
"ignore_lithology_codes": ["cover", "Unknown"],
}

self.structure_config = {
"orientation_type": "dip direction",
"dipdir_column": "DipDir",
"dip_column": "Dip",
"description_column": "FeatDesc",
"bedding_text": "ObsType",
"overturned_column": "Desc",
"overturned_text": "overturned",
"objectid_column": "OBJECTID",
}

self.config_map = {
"geology": self.geology_config,
"structure": self.structure_config,
"fault": self.fault_config,
"fold": self.fold_config,
}

def __getitem__(self, datatype: str) -> Dict[str, Any]:
return self.config_map[datatype]

def as_dict(self) -> Dict[str, Dict[str, Any]]:
"""Return a deep copy of the configuration map."""
return deepcopy(self.config_map)


def _coerce_config_value(template_value: Any, new_value: Any) -> Any:
"""Coerce user supplied values into the template format."""
if isinstance(template_value, list):
if isinstance(new_value, list):
return new_value
if new_value in (None, ""):
return []
if isinstance(new_value, str):
return [item.strip() for item in new_value.split(",") if item.strip()]
return [str(new_value)]

if isinstance(template_value, (int, float)):
try:
return type(template_value)(new_value)
except (TypeError, ValueError):
return template_value

if template_value is None:
return new_value

if new_value is None:
return ""

return str(new_value)


class ConfigurationState:
"""State holder for the NTGS configuration mapping."""

def __init__(self, *, base_config: MutableMapping[str, Dict[str, Any]] | None = None):
self._config = deepcopy(base_config) if base_config is not None else Config().as_dict()

def data_types(self) -> Iterable[str]:
"""Return the supported data types."""
return self._config.keys()

def get_config_for_type(self, data_type: str) -> Dict[str, Any]:
"""Return a copy of the configuration for a single data type."""
self._ensure_data_type(data_type)
return deepcopy(self._config[data_type])

def set_value(self, data_type: str, key: str, value: Any) -> None:
"""Update a single configuration entry."""
self._ensure_data_type(data_type)
template_value = self._config[data_type].get(key)
self._config[data_type][key] = _coerce_config_value(template_value, value)

def update_values(self, data_type: str, updates: Dict[str, Any]) -> None:
"""Bulk update configuration entries for a type."""
for key, value in updates.items():
self.set_value(data_type, key, value)

def get_value(self, data_type: str, key: str) -> Any:
"""Return the stored value for a configuration entry."""
self._ensure_data_type(data_type)
return self._config[data_type].get(key)

def as_dict(self) -> Dict[str, Dict[str, Any]]:
"""Return a deep copy of the entire configuration map."""
return deepcopy(self._config)

def _ensure_data_type(self, data_type: str) -> None:
if data_type not in self._config:
raise KeyError(f"Unknown data type '{data_type}'")
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Data converter Widget by rabii-chaarani · Pull Request #76 · Loop3D/plugin_loopstructural · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
ba73409
test: add unit tests for ConfigurationModel
rabii-chaarani Nov 24, 2025
f0381eb
feat: add DataConversionWidget
rabii-chaarani Nov 24, 2025
62807e7
feat: add data conversion GUI components
rabii-chaarani Nov 24, 2025
a195cc1
feat: implement data conversion configuration helpers
rabii-chaarani Nov 24, 2025
b1e8112
feat: DataConversionWidget implemenation
rabii-chaarani Nov 24, 2025
40d2945
fix: improve ui handling
rabii-chaarani Nov 26, 2025
cb9dfcb
fix: update config file
rabii-chaarani Nov 26, 2025
1da4630
refactor: renamed layers
rabii-chaarani Nov 26, 2025
47ce01b
refactor: update geodataframe conversion
rabii-chaarani Nov 26, 2025
076a08c
feat: add run functionality to manual conversion
rabii-chaarani Nov 28, 2025
ebc532a
Merge remote-tracking branch 'origin/dev-0.1.12' into data_converter_…
rabii-chaarani Jan 27, 2026
c108f56
fix: remove manual conversion code
rabii-chaarani Jan 27, 2026
303ca20
fix: add loopdataconverter to requirements
lachlangrose Jan 28, 2026
ba3d524
fix: added data converter as dialog
rabii-chaarani Jan 28, 2026
d1afe01
feat: added along section thickness calc
rabii-chaarani Jan 29, 2026
dbe347c
refactor: added along section code
rabii-chaarani Jan 29, 2026
33f12aa
Merge remote-tracking branch 'origin/dev-0.1.12' into data_converter_…
rabii-chaarani Jan 29, 2026
d901fe3
feat: add layer guessing and filters to conversion widget
rabii-chaarani Jan 29, 2026
2c0fe3b
fix: applying some copilot changes
lachlangrose Jan 30, 2026
2e59d9c
Merge branch 'dev-0.1.12' into data_converter_tool
lachlangrose Jan 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions loopstructural/gui/data_conversion/__init__.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
"""Data conversion GUI components."""

from .data_conversion_widget import AutomaticConversionDialog, AutomaticConversionWidget

__all__ = ["AutomaticConversionDialog", "AutomaticConversionWidget"]
149 changes: 149 additions & 0 deletions loopstructural/gui/data_conversion/configuration.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
"""Configuration helpers used by the data conversion UI."""

from __future__ import annotations

from copy import deepcopy
from typing import Any, Dict, Iterable, MutableMapping


class Config:
"""Container for the default NTGS configuration."""

def __init__(self) -> None:
self.fold_config = {
"structtype_column": "FoldType",
"fold_text": "'Anticline','Syncline','Antiform','Synform','Monocline','Monoform','Neutral','Fold axis','Overturned syncline'",
"description_column": "Desc",
"synform_text": "FoldType",
"foldname_column": "FoldName",
"objectid_column": "OBJECTID",
"tightness_column": "IntlimbAng",
"axial_plane_dipdir_column": "AxPlDipDir",
"axial_plane_dip_column": "AxPlDip",
}

self.fault_config = {
"orientation_type": "dip direction",
"structtype_column": "FaultType",
"fault_text": "'Thrust','Reverse','Normal','Shear zone','Strike-slip','Thrust','Unknown'",
"dip_null_value": "-999",
"dipdir_flag": "num",
"dipdir_column": "DipDir",
"dip_column": "Dip",
"dipestimate_column": "DipEstimate",
"dipestimate_text": "'NORTH_EAST','NORTH',<rest of cardinals>,'NOT ACCESSED'",
"displacement_column": "Displace",
"displacement_text": "'1m-100m', '100m-1km', '1km-5km', '>5km'",
"fault_length_column": "FaultLen",
"fault_length_text": "Small (0-5km),Medium (5-30km),Large (30-100km),Regional (>100km),Unclassified",
"name_column": "FaultName",
"objectid_column": "OBJECTID",
}

self.geology_config = {
"unitname_column": "Formation",
"alt_unitname_column": "Formation",
"group_column": "Group",
"supergroup_column": "Supergroup",
"description_column": "LithDescn1",
"minage_column": "AgeMin",
"maxage_column": "AgeMax",
"rocktype_column": "LithClass",
"alt_rocktype_column": "RockCat",
"sill_text": "RockCat",
"intrusive_text": "RockCat",
"volcanic_text": "RockCat",
"objectid_column": "OBJECTID",
"ignore_lithology_codes": ["cover", "Unknown"],
}

self.structure_config = {
"orientation_type": "dip direction",
"dipdir_column": "DipDir",
"dip_column": "Dip",
"description_column": "FeatDesc",
"bedding_text": "ObsType",
"overturned_column": "Desc",
"overturned_text": "overturned",
"objectid_column": "OBJECTID",
}

self.config_map = {
"geology": self.geology_config,
"structure": self.structure_config,
"fault": self.fault_config,
"fold": self.fold_config,
}

def __getitem__(self, datatype: str) -> Dict[str, Any]:
return self.config_map[datatype]

def as_dict(self) -> Dict[str, Dict[str, Any]]:
"""Return a deep copy of the configuration map."""
return deepcopy(self.config_map)


def _coerce_config_value(template_value: Any, new_value: Any) -> Any:
"""Coerce user supplied values into the template format."""
if isinstance(template_value, list):
if isinstance(new_value, list):
return new_value
if new_value in (None, ""):
return []
if isinstance(new_value, str):
return [item.strip() for item in new_value.split(",") if item.strip()]
return [str(new_value)]

if isinstance(template_value, (int, float)):
try:
return type(template_value)(new_value)
except (TypeError, ValueError):
return template_value

if template_value is None:
return new_value

if new_value is None:
return ""

return str(new_value)


class ConfigurationState:
"""State holder for the NTGS configuration mapping."""

def __init__(self, *, base_config: MutableMapping[str, Dict[str, Any]] | None = None):
self._config = deepcopy(base_config) if base_config is not None else Config().as_dict()

def data_types(self) -> Iterable[str]:
"""Return the supported data types."""
return self._config.keys()

def get_config_for_type(self, data_type: str) -> Dict[str, Any]:
"""Return a copy of the configuration for a single data type."""
self._ensure_data_type(data_type)
return deepcopy(self._config[data_type])

def set_value(self, data_type: str, key: str, value: Any) -> None:
"""Update a single configuration entry."""
self._ensure_data_type(data_type)
template_value = self._config[data_type].get(key)
self._config[data_type][key] = _coerce_config_value(template_value, value)

def update_values(self, data_type: str, updates: Dict[str, Any]) -> None:
"""Bulk update configuration entries for a type."""
for key, value in updates.items():
self.set_value(data_type, key, value)

def get_value(self, data_type: str, key: str) -> Any:
"""Return the stored value for a configuration entry."""
self._ensure_data_type(data_type)
return self._config[data_type].get(key)

def as_dict(self) -> Dict[str, Dict[str, Any]]:
"""Return a deep copy of the entire configuration map."""
return deepcopy(self._config)

def _ensure_data_type(self, data_type: str) -> None:
if data_type not in self._config:
raise KeyError(f"Unknown data type '{data_type}'")
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Data converter Widget by rabii-chaarani · Pull Request #76 · Loop3D/plugin_loopstructural · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
ba73409
test: add unit tests for ConfigurationModel
rabii-chaarani Nov 24, 2025
f0381eb
feat: add DataConversionWidget
rabii-chaarani Nov 24, 2025
62807e7
feat: add data conversion GUI components
rabii-chaarani Nov 24, 2025
a195cc1
feat: implement data conversion configuration helpers
rabii-chaarani Nov 24, 2025
b1e8112
feat: DataConversionWidget implemenation
rabii-chaarani Nov 24, 2025
40d2945
fix: improve ui handling
rabii-chaarani Nov 26, 2025
cb9dfcb
fix: update config file
rabii-chaarani Nov 26, 2025
1da4630
refactor: renamed layers
rabii-chaarani Nov 26, 2025
47ce01b
refactor: update geodataframe conversion
rabii-chaarani Nov 26, 2025
076a08c
feat: add run functionality to manual conversion
rabii-chaarani Nov 28, 2025
ebc532a
Merge remote-tracking branch 'origin/dev-0.1.12' into data_converter_…
rabii-chaarani Jan 27, 2026
c108f56
fix: remove manual conversion code
rabii-chaarani Jan 27, 2026
303ca20
fix: add loopdataconverter to requirements
lachlangrose Jan 28, 2026
ba3d524
fix: added data converter as dialog
rabii-chaarani Jan 28, 2026
d1afe01
feat: added along section thickness calc
rabii-chaarani Jan 29, 2026
dbe347c
refactor: added along section code
rabii-chaarani Jan 29, 2026
33f12aa
Merge remote-tracking branch 'origin/dev-0.1.12' into data_converter_…
rabii-chaarani Jan 29, 2026
d901fe3
feat: add layer guessing and filters to conversion widget
rabii-chaarani Jan 29, 2026
2c0fe3b
fix: applying some copilot changes
lachlangrose Jan 30, 2026
2e59d9c
Merge branch 'dev-0.1.12' into data_converter_tool
lachlangrose Jan 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions loopstructural/gui/data_conversion/__init__.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
"""Data conversion GUI components."""

from .data_conversion_widget import AutomaticConversionDialog, AutomaticConversionWidget

__all__ = ["AutomaticConversionDialog", "AutomaticConversionWidget"]
149 changes: 149 additions & 0 deletions loopstructural/gui/data_conversion/configuration.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
"""Configuration helpers used by the data conversion UI."""

from __future__ import annotations

from copy import deepcopy
from typing import Any, Dict, Iterable, MutableMapping


class Config:
"""Container for the default NTGS configuration."""

def __init__(self) -> None:
self.fold_config = {
"structtype_column": "FoldType",
"fold_text": "'Anticline','Syncline','Antiform','Synform','Monocline','Monoform','Neutral','Fold axis','Overturned syncline'",
"description_column": "Desc",
"synform_text": "FoldType",
"foldname_column": "FoldName",
"objectid_column": "OBJECTID",
"tightness_column": "IntlimbAng",
"axial_plane_dipdir_column": "AxPlDipDir",
"axial_plane_dip_column": "AxPlDip",
}

self.fault_config = {
"orientation_type": "dip direction",
"structtype_column": "FaultType",
"fault_text": "'Thrust','Reverse','Normal','Shear zone','Strike-slip','Thrust','Unknown'",
"dip_null_value": "-999",
"dipdir_flag": "num",
"dipdir_column": "DipDir",
"dip_column": "Dip",
"dipestimate_column": "DipEstimate",
"dipestimate_text": "'NORTH_EAST','NORTH',<rest of cardinals>,'NOT ACCESSED'",
"displacement_column": "Displace",
"displacement_text": "'1m-100m', '100m-1km', '1km-5km', '>5km'",
"fault_length_column": "FaultLen",
"fault_length_text": "Small (0-5km),Medium (5-30km),Large (30-100km),Regional (>100km),Unclassified",
"name_column": "FaultName",
"objectid_column": "OBJECTID",
}

self.geology_config = {
"unitname_column": "Formation",
"alt_unitname_column": "Formation",
"group_column": "Group",
"supergroup_column": "Supergroup",
"description_column": "LithDescn1",
"minage_column": "AgeMin",
"maxage_column": "AgeMax",
"rocktype_column": "LithClass",
"alt_rocktype_column": "RockCat",
"sill_text": "RockCat",
"intrusive_text": "RockCat",
"volcanic_text": "RockCat",
"objectid_column": "OBJECTID",
"ignore_lithology_codes": ["cover", "Unknown"],
}

self.structure_config = {
"orientation_type": "dip direction",
"dipdir_column": "DipDir",
"dip_column": "Dip",
"description_column": "FeatDesc",
"bedding_text": "ObsType",
"overturned_column": "Desc",
"overturned_text": "overturned",
"objectid_column": "OBJECTID",
}

self.config_map = {
"geology": self.geology_config,
"structure": self.structure_config,
"fault": self.fault_config,
"fold": self.fold_config,
}

def __getitem__(self, datatype: str) -> Dict[str, Any]:
return self.config_map[datatype]

def as_dict(self) -> Dict[str, Dict[str, Any]]:
"""Return a deep copy of the configuration map."""
return deepcopy(self.config_map)


def _coerce_config_value(template_value: Any, new_value: Any) -> Any:
"""Coerce user supplied values into the template format."""
if isinstance(template_value, list):
if isinstance(new_value, list):
return new_value
if new_value in (None, ""):
return []
if isinstance(new_value, str):
return [item.strip() for item in new_value.split(",") if item.strip()]
return [str(new_value)]

if isinstance(template_value, (int, float)):
try:
return type(template_value)(new_value)
except (TypeError, ValueError):
return template_value

if template_value is None:
return new_value

if new_value is None:
return ""

return str(new_value)


class ConfigurationState:
"""State holder for the NTGS configuration mapping."""

def __init__(self, *, base_config: MutableMapping[str, Dict[str, Any]] | None = None):
self._config = deepcopy(base_config) if base_config is not None else Config().as_dict()

def data_types(self) -> Iterable[str]:
"""Return the supported data types."""
return self._config.keys()

def get_config_for_type(self, data_type: str) -> Dict[str, Any]:
"""Return a copy of the configuration for a single data type."""
self._ensure_data_type(data_type)
return deepcopy(self._config[data_type])

def set_value(self, data_type: str, key: str, value: Any) -> None:
"""Update a single configuration entry."""
self._ensure_data_type(data_type)
template_value = self._config[data_type].get(key)
self._config[data_type][key] = _coerce_config_value(template_value, value)

def update_values(self, data_type: str, updates: Dict[str, Any]) -> None:
"""Bulk update configuration entries for a type."""
for key, value in updates.items():
self.set_value(data_type, key, value)

def get_value(self, data_type: str, key: str) -> Any:
"""Return the stored value for a configuration entry."""
self._ensure_data_type(data_type)
return self._config[data_type].get(key)

def as_dict(self) -> Dict[str, Dict[str, Any]]:
"""Return a deep copy of the entire configuration map."""
return deepcopy(self._config)

def _ensure_data_type(self, data_type: str) -> None:
if data_type not in self._config:
raise KeyError(f"Unknown data type '{data_type}'")
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); Data converter Widget by rabii-chaarani · Pull Request #76 · Loop3D/plugin_loopstructural · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
ba73409
test: add unit tests for ConfigurationModel
rabii-chaarani Nov 24, 2025
f0381eb
feat: add DataConversionWidget
rabii-chaarani Nov 24, 2025
62807e7
feat: add data conversion GUI components
rabii-chaarani Nov 24, 2025
a195cc1
feat: implement data conversion configuration helpers
rabii-chaarani Nov 24, 2025
b1e8112
feat: DataConversionWidget implemenation
rabii-chaarani Nov 24, 2025
40d2945
fix: improve ui handling
rabii-chaarani Nov 26, 2025
cb9dfcb
fix: update config file
rabii-chaarani Nov 26, 2025
1da4630
refactor: renamed layers
rabii-chaarani Nov 26, 2025
47ce01b
refactor: update geodataframe conversion
rabii-chaarani Nov 26, 2025
076a08c
feat: add run functionality to manual conversion
rabii-chaarani Nov 28, 2025
ebc532a
Merge remote-tracking branch 'origin/dev-0.1.12' into data_converter_…
rabii-chaarani Jan 27, 2026
c108f56
fix: remove manual conversion code
rabii-chaarani Jan 27, 2026
303ca20
fix: add loopdataconverter to requirements
lachlangrose Jan 28, 2026
ba3d524
fix: added data converter as dialog
rabii-chaarani Jan 28, 2026
d1afe01
feat: added along section thickness calc
rabii-chaarani Jan 29, 2026
dbe347c
refactor: added along section code
rabii-chaarani Jan 29, 2026
33f12aa
Merge remote-tracking branch 'origin/dev-0.1.12' into data_converter_…
rabii-chaarani Jan 29, 2026
d901fe3
feat: add layer guessing and filters to conversion widget
rabii-chaarani Jan 29, 2026
2c0fe3b
fix: applying some copilot changes
lachlangrose Jan 30, 2026
2e59d9c
Merge branch 'dev-0.1.12' into data_converter_tool
lachlangrose Jan 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions loopstructural/gui/data_conversion/__init__.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
"""Data conversion GUI components."""

from .data_conversion_widget import AutomaticConversionDialog, AutomaticConversionWidget

__all__ = ["AutomaticConversionDialog", "AutomaticConversionWidget"]
149 changes: 149 additions & 0 deletions loopstructural/gui/data_conversion/configuration.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
"""Configuration helpers used by the data conversion UI."""

from __future__ import annotations

from copy import deepcopy
from typing import Any, Dict, Iterable, MutableMapping


class Config:
"""Container for the default NTGS configuration."""

def __init__(self) -> None:
self.fold_config = {
"structtype_column": "FoldType",
"fold_text": "'Anticline','Syncline','Antiform','Synform','Monocline','Monoform','Neutral','Fold axis','Overturned syncline'",
"description_column": "Desc",
"synform_text": "FoldType",
"foldname_column": "FoldName",
"objectid_column": "OBJECTID",
"tightness_column": "IntlimbAng",
"axial_plane_dipdir_column": "AxPlDipDir",
"axial_plane_dip_column": "AxPlDip",
}

self.fault_config = {
"orientation_type": "dip direction",
"structtype_column": "FaultType",
"fault_text": "'Thrust','Reverse','Normal','Shear zone','Strike-slip','Thrust','Unknown'",
"dip_null_value": "-999",
"dipdir_flag": "num",
"dipdir_column": "DipDir",
"dip_column": "Dip",
"dipestimate_column": "DipEstimate",
"dipestimate_text": "'NORTH_EAST','NORTH',<rest of cardinals>,'NOT ACCESSED'",
"displacement_column": "Displace",
"displacement_text": "'1m-100m', '100m-1km', '1km-5km', '>5km'",
"fault_length_column": "FaultLen",
"fault_length_text": "Small (0-5km),Medium (5-30km),Large (30-100km),Regional (>100km),Unclassified",
"name_column": "FaultName",
"objectid_column": "OBJECTID",
}

self.geology_config = {
"unitname_column": "Formation",
"alt_unitname_column": "Formation",
"group_column": "Group",
"supergroup_column": "Supergroup",
"description_column": "LithDescn1",
"minage_column": "AgeMin",
"maxage_column": "AgeMax",
"rocktype_column": "LithClass",
"alt_rocktype_column": "RockCat",
"sill_text": "RockCat",
"intrusive_text": "RockCat",
"volcanic_text": "RockCat",
"objectid_column": "OBJECTID",
"ignore_lithology_codes": ["cover", "Unknown"],
}

self.structure_config = {
"orientation_type": "dip direction",
"dipdir_column": "DipDir",
"dip_column": "Dip",
"description_column": "FeatDesc",
"bedding_text": "ObsType",
"overturned_column": "Desc",
"overturned_text": "overturned",
"objectid_column": "OBJECTID",
}

self.config_map = {
"geology": self.geology_config,
"structure": self.structure_config,
"fault": self.fault_config,
"fold": self.fold_config,
}

def __getitem__(self, datatype: str) -> Dict[str, Any]:
return self.config_map[datatype]

def as_dict(self) -> Dict[str, Dict[str, Any]]:
"""Return a deep copy of the configuration map."""
return deepcopy(self.config_map)


def _coerce_config_value(template_value: Any, new_value: Any) -> Any:
"""Coerce user supplied values into the template format."""
if isinstance(template_value, list):
if isinstance(new_value, list):
return new_value
if new_value in (None, ""):
return []
if isinstance(new_value, str):
return [item.strip() for item in new_value.split(",") if item.strip()]
return [str(new_value)]

if isinstance(template_value, (int, float)):
try:
return type(template_value)(new_value)
except (TypeError, ValueError):
return template_value

if template_value is None:
return new_value

if new_value is None:
return ""

return str(new_value)


class ConfigurationState:
"""State holder for the NTGS configuration mapping."""

def __init__(self, *, base_config: MutableMapping[str, Dict[str, Any]] | None = None):
self._config = deepcopy(base_config) if base_config is not None else Config().as_dict()

def data_types(self) -> Iterable[str]:
"""Return the supported data types."""
return self._config.keys()

def get_config_for_type(self, data_type: str) -> Dict[str, Any]:
"""Return a copy of the configuration for a single data type."""
self._ensure_data_type(data_type)
return deepcopy(self._config[data_type])

def set_value(self, data_type: str, key: str, value: Any) -> None:
"""Update a single configuration entry."""
self._ensure_data_type(data_type)
template_value = self._config[data_type].get(key)
self._config[data_type][key] = _coerce_config_value(template_value, value)

def update_values(self, data_type: str, updates: Dict[str, Any]) -> None:
"""Bulk update configuration entries for a type."""
for key, value in updates.items():
self.set_value(data_type, key, value)

def get_value(self, data_type: str, key: str) -> Any:
"""Return the stored value for a configuration entry."""
self._ensure_data_type(data_type)
return self._config[data_type].get(key)

def as_dict(self) -> Dict[str, Dict[str, Any]]:
"""Return a deep copy of the entire configuration map."""
return deepcopy(self._config)

def _ensure_data_type(self, data_type: str) -> None:
if data_type not in self._config:
raise KeyError(f"Unknown data type '{data_type}'")
Loading