diff --git a/src/nopywer/frontend/app.js b/src/nopywer/frontend/app.js index 60039a4..c246419 100644 --- a/src/nopywer/frontend/app.js +++ b/src/nopywer/frontend/app.js @@ -96,6 +96,7 @@ } } + function cableStyle(feature) { const plugs = feature.properties && feature.properties.plugs_and_sockets_a ? feature.properties.plugs_and_sockets_a @@ -109,59 +110,82 @@ } return { color: "#f59e0b", weight: 4 }; } + function getCableWeight(plugs) { + if (plugs >= 63) return 8; + if (plugs >= 32) return 6; + return 4; + } + function getCableColor(plugs) { + if (plugs >= 63) return "#dc2626"; + if (plugs >= 32) return "#ea580c"; + return "#f59e0b"; + } + + function phaseColor(phase) { + const colors = { 0: "#6f2018dc", 1: "#000000", 2: "#9d9d9d" }; + return colors[phase] ?? "#e81212"; + } function renderOptimizedCables(cablesGeojson) { - optimizedCablesLayer.clearLayers(); + console.log(cablesGeojson.features[0].properties); + + optimizedCablesLayer.clearLayers(); + + const filter = function (feature) { + return feature.geometry && feature.geometry.type === "LineString"; + }; + + // Capa base: tipo de cable (gruesa, semitransparente) + const baseLayer = L.geoJSON(cablesGeojson, { + filter, + style: function (feature) { + const style = cableStyle(feature); + return { + color: style.color, + weight: style.weight * 2, + opacity: 0.5, + }; + }, + }); - const cableLayer = L.geoJSON(cablesGeojson, { - filter: function (feature) { - return feature.geometry && feature.geometry.type === "LineString"; - }, - style: function (feature) { - const style = cableStyle(feature); - return { - color: style.color, - weight: style.weight, - opacity: 0.95, - }; - }, - onEachFeature: function (feature, layer) { - const props = feature.properties || {}; - const details = - "" + - (props.from || "?") + - " → " + - (props.to || "?") + - "
" + - "Length: " + - String(props.length_m ?? "?") + - " m
" + - "Cable: " + - String(props.cable_type || "?") + - "
" + - "Current: " + - String(props.current_a ?? "?") + - " A
" + - "Load: " + - String(props.cum_power_kw ?? "?") + - " kW"; - - layer.bindPopup(details); - layer.bindTooltip(details, { - sticky: true, - direction: "top", - className: "cable-tooltip", - }); - }, - }); + // Capa superior: fase (fina, sólida) + popups + const phaseLayer = L.geoJSON(cablesGeojson, { + filter, + style: function (feature) { + const plugs = feature.properties?.plugs_and_sockets_a ?? 16; + const phase = feature.properties?.phase ?? null; + const baseWeight = cableStyle(feature).weight; + return { + color: phaseColor(phase), + weight: baseWeight, + opacity: 1, + }; + }, + onEachFeature: function (feature, layer) { + const props = feature.properties || {}; + const phase = props.phase ?? null; + const plugs = props.plugs_and_sockets_a ?? 16; + + const details = + "" + (props.from || "?") + " → " + (props.to || "?") + "
" + + "Phase: L" + ((phase ?? "?") + 1) + "
" + + "Cable: " + plugs + "A
" + + "Length: " + String(props.length_m ?? "?") + " m
" + + "Current: " + String(props.current_a ?? "?") + " A
" + + "Load: " + String(props.cum_power_kw ?? "?") + " kW"; + + layer.bindPopup(details); + }, + }); - cableLayer.addTo(optimizedCablesLayer); - if (cableLayer.getLayers().length > 0) { - map.fitBounds(cableLayer.getBounds().pad(0.08)); - } - bringNodesToFront(); - } + baseLayer.addTo(optimizedCablesLayer); + phaseLayer.addTo(optimizedCablesLayer); + if (phaseLayer.getLayers().length > 0) { + map.fitBounds(phaseLayer.getBounds().pad(0.08)); + } + bringNodesToFront(); +} function buildNodesGeojson() { if (!nodesGeojson || !Array.isArray(nodesGeojson.features)) { return { type: "FeatureCollection", features: [] }; @@ -254,3 +278,5 @@ map.setView([41.7008, -0.1379], 17); }); })(); + +console.log(cablesGeojson.features[0].properties); diff --git a/src/nopywer/frontend/favicon.ico b/src/nopywer/frontend/favicon.ico new file mode 100644 index 0000000..0e5b65f Binary files /dev/null and b/src/nopywer/frontend/favicon.ico differ diff --git a/src/nopywer/models.py b/src/nopywer/models.py index 41a7454..136c9ef 100644 --- a/src/nopywer/models.py +++ b/src/nopywer/models.py @@ -14,7 +14,7 @@ class PowerNode: lat: float power_watts: float = 0.0 is_generator: bool = False - phase: int | str | list | None = None + phase: int = 0 parent: str | None = None children: dict[str, str] = field(default_factory=dict) @@ -52,6 +52,7 @@ def to_geojson(self) -> dict: "voltage": self.voltage, "vdrop_percent": self.vdrop_percent, "distro": self.distro, + "phase": self.phase, }, } @@ -62,7 +63,7 @@ class Cable: length_m: float area_mm2: float = 2.5 plugs_and_sockets_a: float = 16.0 - phase: int | str | list | None = None + phase: int = 0 from_node: str = "" to_node: str = "" @@ -113,6 +114,7 @@ def to_geojson(self) -> dict: "current_a": round(max_current, 1), "cum_power_kw": round(cum_power_w / 1000, 2), "vdrop_volts": self.vdrop_volts, + "phase": self.phase, }, } @@ -124,6 +126,7 @@ class Cable16A(Cable): tier_cost: ClassVar[float] = 1.0 num_phases: ClassVar[int] = 1 max_current_a: ClassVar[int] = 16 + area_mm2: float = 2.5 @dataclass diff --git a/telemetry data/telemetry visualizer.py b/telemetry data/telemetry visualizer.py new file mode 100644 index 0000000..9f3b26f --- /dev/null +++ b/telemetry data/telemetry visualizer.py @@ -0,0 +1,57 @@ +from pathlib import Path + +import pandas as pd +import plotly.express as px + + +def load_telemetry(file_path): + phases = {} + current_phase = None + rows = [] + + with open(file_path, encoding="utf-8-sig") as f: + for line in f: + line = line.strip() + + if line.startswith("Phase"): + if current_phase and rows: + df_phase = pd.DataFrame(rows, columns=["Time", "Wh"]) + phases[current_phase] = df_phase + rows = [] + current_phase = line + + elif line and not line.startswith("Time"): + parts = line.split(" , ") + if len(parts) == 2: + rows.append(parts) + + if current_phase and rows: + df_phase = pd.DataFrame(rows, columns=["Time", "Wh"]) + phases[current_phase] = df_phase + return phases + + +def clean_phase_data(df): + df["Time"] = pd.to_datetime(df["Time"], dayfirst=True) + df["Wh"] = pd.to_numeric(df["Wh"]) + return df + + +DATA_DIR = Path("./telemetry data") +files = list(DATA_DIR.glob("*.csv")) +all_data = [] + +for file in files: + phases = load_telemetry(file) + + for phase_name, df in phases.items(): + df = clean_phase_data(df) + df["phase"] = phase_name + df["source"] = file.name.split("_")[0] + all_data.append(df) + +df = pd.concat(all_data) + +fig = px.line(df, x="Time", y="Wh", color="phase", facet_row="source", title="Power telemetry") + +fig.show()