DavMelchi commited on
Commit
436fac5
·
1 Parent(s): 885850c

feat(gps): add manual decimal<->DMS conversion and roadmap note

Browse files
apps/gps_converter.py CHANGED
@@ -9,155 +9,234 @@ class DataFrames:
9
  Dframe = pd.DataFrame()
10
 
11
 
12
- st.title("GPS Coordinate Converter")
13
- st.write(
14
- """This program allows you to convert coordinates from degree-minute-second (13°15'6.20"N) to decimal (13.2517222222222) and vice versa.
15
- Please choose a file containing the latitude and longitude columns.
16
- """
17
- )
18
-
19
- decimal_to_degrees_sample_file_path = "samples/Decimal_to_DMS.xlsx"
20
- degrees_to_decimal_sample_file_path = "samples/DMS_to_Decimal.xlsx"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
 
22
- col1, col2, col3 = st.columns(3)
 
23
 
24
- with col1:
25
- st.download_button(
26
- label="Download Decimal_to_DMS Sample File",
27
- data=open(decimal_to_degrees_sample_file_path, "rb").read(),
28
- file_name="Decimal_to_DMS.xlsx",
29
- mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
30
- )
31
- with col2:
32
- st.download_button(
33
- label="Download DMS_to_Decimal Sample File",
34
- data=open(degrees_to_decimal_sample_file_path, "rb").read(),
35
- file_name="DMS_to_Decimal.xlsx",
36
- mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
37
  )
38
 
39
- uploaded_file = st.file_uploader("Choose a file", type=["xlsx"])
40
- col1_list = []
41
- if uploaded_file is not None:
42
- DataFrames.Dframe = pd.read_excel(uploaded_file, keep_default_na=False)
43
- col1_list = DataFrames.Dframe.columns.tolist()
44
 
45
- latitude_dd = st.selectbox("Choose Latitude Column", options=col1_list)
46
- longitude_dd = st.selectbox("Choose Longitude Column", options=col1_list)
 
 
47
 
48
- conversion_choice = st.selectbox(
49
- "Choose Conversion Type", options=["Decimal", "DegMinSec"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  )
51
 
52
- if st.button("CONVERT", type="primary"):
53
-
54
- try:
55
- # if not latitude_dd or not longitude_dd:
56
- # st.error("Please choose the latitude and longitude columns")
57
-
58
- # if DataFrames.Dframe.empty:
59
- # st.error("Please choose a file first")
60
-
61
- df = DataFrames.Dframe.copy()
62
-
63
- df["converted_latitude"] = df[latitude_dd]
64
- df["converted_longitude"] = df[longitude_dd]
65
-
66
- if conversion_choice == "Decimal":
67
- df["converted_longitude"] = df["converted_longitude"].str.replace(
68
- "O", "W"
69
- )
70
- df["converted_latitude"] = df["converted_latitude"].apply(parse)
71
- df["converted_longitude"] = df["converted_longitude"].apply(parse)
72
- else:
73
- df["converted_latitude"] = df["converted_latitude"].apply(
74
- to_str_deg_min_sec
75
- )
76
- df["converted_longitude"] = df["converted_longitude"].apply(
77
- to_str_deg_min_sec
78
- )
79
- df["converted_latitude"] = df["converted_latitude"].apply(
80
- lambda x: x.replace("-", "") + "S" if "-" in x else x + "N"
81
- )
82
- df["converted_longitude"] = df["converted_longitude"].apply(
83
- lambda x: x.replace("-", "") + "W" if "-" in x else x + "E"
84
- )
85
-
86
- DataFrames.Dframe = df
87
- st.success("Coordinates converted Sucessfully")
88
-
89
- @st.fragment
90
- def table_data():
91
- if DataFrames.Dframe is not None:
92
- AgGrid(
93
- DataFrames.Dframe,
94
- fit_columns_on_grid_load=True,
95
- theme="streamlit",
96
- enable_enterprise_modules=True,
97
- filter=True,
98
- # columns_auto_size_mode=ColumnsAutoSizeMode.FIT_CONTENTS,
99
- )
100
-
101
- table_data()
102
-
103
- # Display map visualization
104
- st.subheader("📍 Map Visualization")
105
  try:
106
- map_df = DataFrames.Dframe.copy()
107
-
108
- # Determine which columns contain decimal coordinates for mapping
109
- if conversion_choice == "Decimal":
110
- lat_col = "converted_latitude"
111
- lon_col = "converted_longitude"
112
- else:
113
- # For DMS output, use original decimal source columns
114
- lat_col = latitude_dd
115
- lon_col = longitude_dd
116
- # Ensure they are numeric
117
- map_df[lat_col] = pd.to_numeric(map_df[lat_col], errors="coerce")
118
- map_df[lon_col] = pd.to_numeric(map_df[lon_col], errors="coerce")
119
-
120
- # Create hover text with available columns
121
- hover_cols = [
122
- col
123
- for col in map_df.columns
124
- if col not in [lat_col, lon_col, "hover_text"]
125
- ]
126
- if hover_cols:
127
- map_df["hover_text"] = map_df[hover_cols].astype(str).agg(
128
- "<br>".join, axis=1
129
- )
130
- else:
131
- map_df["hover_text"] = "Point"
132
-
133
- # Filter out invalid coordinates
134
- map_df = map_df.dropna(subset=[lat_col, lon_col])
135
-
136
- if not map_df.empty:
137
- fig = px.scatter_map(
138
- map_df,
139
- lat=lat_col,
140
- lon=lon_col,
141
- hover_name="hover_text",
142
- zoom=5,
143
- height=500,
144
- )
145
- fig.update_layout(
146
- mapbox_style="open-street-map",
147
- margin={"r": 0, "t": 0, "l": 0, "b": 0},
148
- )
149
- fig.update_traces(marker=dict(size=12, color="#FF4B4B"))
150
- st.plotly_chart(fig, use_container_width=True)
151
- else:
152
- st.warning("No valid coordinates available for map display.")
153
- except Exception as map_error:
154
- st.warning(
155
- f"Could not display map. Ensure coordinates are valid decimal values. Error: {map_error}"
156
- )
157
-
158
- except Exception as e:
159
- st.error(
160
- f"An error occurred. Make sure the file contains the latitude and longitude columns. Error: {e}"
161
  )
162
- else:
163
- st.info("Please choose a file containing the latitude and longitude columns")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  Dframe = pd.DataFrame()
10
 
11
 
12
+ def _validate_decimal_coordinate(value: float, axis: str) -> float:
13
+ if axis == "lat" and not (-90 <= value <= 90):
14
+ raise ValueError("Latitude must be between -90 and 90.")
15
+ if axis == "lon" and not (-180 <= value <= 180):
16
+ raise ValueError("Longitude must be between -180 and 180.")
17
+ return value
18
+
19
+
20
+ def _decimal_to_dms_with_direction(value: float, axis: str) -> str:
21
+ _validate_decimal_coordinate(value, axis)
22
+ direction = "N" if value >= 0 else "S"
23
+ if axis == "lon":
24
+ direction = "E" if value >= 0 else "W"
25
+ dms = to_str_deg_min_sec(abs(value))
26
+ return f"{dms}{direction}"
27
+
28
+
29
+ def _dms_to_decimal(value: str, axis: str) -> float:
30
+ normalized = value.strip().upper()
31
+ if axis == "lon":
32
+ normalized = normalized.replace("O", "W")
33
+ result = parse(normalized)
34
+ return _validate_decimal_coordinate(result, axis)
35
+
36
+
37
+ def _show_map(df: pd.DataFrame, lat_col: str, lon_col: str, title: str = "Map Visualization"):
38
+ st.subheader(title)
39
+ try:
40
+ map_df = df.copy()
41
+ map_df[lat_col] = pd.to_numeric(map_df[lat_col], errors="coerce")
42
+ map_df[lon_col] = pd.to_numeric(map_df[lon_col], errors="coerce")
43
+ map_df = map_df.dropna(subset=[lat_col, lon_col])
44
+
45
+ if map_df.empty:
46
+ st.warning("No valid coordinates available for map display.")
47
+ return
48
+
49
+ hover_cols = [col for col in map_df.columns if col not in [lat_col, lon_col]]
50
+ if hover_cols:
51
+ map_df["hover_text"] = map_df[hover_cols].astype(str).agg("<br>".join, axis=1)
52
+ else:
53
+ map_df["hover_text"] = "Point"
54
+
55
+ fig = px.scatter_map(
56
+ map_df,
57
+ lat=lat_col,
58
+ lon=lon_col,
59
+ hover_name="hover_text",
60
+ zoom=5,
61
+ height=500,
62
+ )
63
+ fig.update_layout(
64
+ mapbox_style="open-street-map",
65
+ margin={"r": 0, "t": 0, "l": 0, "b": 0},
66
+ )
67
+ fig.update_traces(marker=dict(size=12, color="#FF4B4B"))
68
+ st.plotly_chart(fig, use_container_width=True)
69
+ except Exception as map_error:
70
+ st.warning(f"Could not display map. Error: {map_error}")
71
+
72
+
73
+ def _render_import_mode():
74
+ st.write("Convert coordinates from an Excel file.")
75
+
76
+ decimal_to_degrees_sample_file_path = "samples/Decimal_to_DMS.xlsx"
77
+ degrees_to_decimal_sample_file_path = "samples/DMS_to_Decimal.xlsx"
78
+
79
+ col1, col2, _ = st.columns(3)
80
+
81
+ with col1:
82
+ st.download_button(
83
+ label="Download Decimal_to_DMS Sample File",
84
+ data=open(decimal_to_degrees_sample_file_path, "rb").read(),
85
+ file_name="Decimal_to_DMS.xlsx",
86
+ mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
87
+ )
88
+ with col2:
89
+ st.download_button(
90
+ label="Download DMS_to_Decimal Sample File",
91
+ data=open(degrees_to_decimal_sample_file_path, "rb").read(),
92
+ file_name="DMS_to_Decimal.xlsx",
93
+ mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
94
+ )
95
+
96
+ uploaded_file = st.file_uploader("Choose a file", type=["xlsx"], key="gps_file")
97
+ if uploaded_file is None:
98
+ st.info("Please choose a file containing the latitude and longitude columns.")
99
+ return
100
 
101
+ DataFrames.Dframe = pd.read_excel(uploaded_file, keep_default_na=False)
102
+ columns = DataFrames.Dframe.columns.tolist()
103
 
104
+ latitude_col = st.selectbox("Choose Latitude Column", options=columns)
105
+ longitude_col = st.selectbox("Choose Longitude Column", options=columns)
106
+ conversion_choice = st.selectbox(
107
+ "Choose Conversion Type",
108
+ options=["DMS to Decimal", "Decimal to DMS"],
 
 
 
 
 
 
 
 
109
  )
110
 
111
+ if not st.button("CONVERT", type="primary", key="import_convert"):
112
+ return
 
 
 
113
 
114
+ try:
115
+ df = DataFrames.Dframe.copy()
116
+ df["converted_latitude"] = df[latitude_col]
117
+ df["converted_longitude"] = df[longitude_col]
118
 
119
+ if conversion_choice == "DMS to Decimal":
120
+ df["converted_latitude"] = df["converted_latitude"].apply(
121
+ lambda x: _dms_to_decimal(str(x), "lat")
122
+ )
123
+ df["converted_longitude"] = df["converted_longitude"].apply(
124
+ lambda x: _dms_to_decimal(str(x), "lon")
125
+ )
126
+ map_lat_col = "converted_latitude"
127
+ map_lon_col = "converted_longitude"
128
+ else:
129
+ df["converted_latitude"] = pd.to_numeric(df["converted_latitude"], errors="coerce")
130
+ df["converted_longitude"] = pd.to_numeric(df["converted_longitude"], errors="coerce")
131
+ if df["converted_latitude"].isna().any() or df["converted_longitude"].isna().any():
132
+ raise ValueError("Decimal columns contain invalid numeric values.")
133
+
134
+ df["converted_latitude"] = df["converted_latitude"].apply(
135
+ lambda x: _decimal_to_dms_with_direction(float(x), "lat")
136
+ )
137
+ df["converted_longitude"] = df["converted_longitude"].apply(
138
+ lambda x: _decimal_to_dms_with_direction(float(x), "lon")
139
+ )
140
+ map_lat_col = latitude_col
141
+ map_lon_col = longitude_col
142
+
143
+ DataFrames.Dframe = df
144
+ st.success("Coordinates converted successfully.")
145
+
146
+ AgGrid(
147
+ DataFrames.Dframe,
148
+ fit_columns_on_grid_load=True,
149
+ theme="streamlit",
150
+ enable_enterprise_modules=True,
151
+ filter=True,
152
+ )
153
+
154
+ _show_map(DataFrames.Dframe, map_lat_col, map_lon_col)
155
+
156
+ except Exception as error:
157
+ st.error(
158
+ "An error occurred. Ensure your selected columns contain valid coordinate values. "
159
+ f"Details: {error}"
160
+ )
161
+
162
+
163
+ def _render_manual_mode():
164
+ st.write("Convert a single coordinate pair without importing a file.")
165
+ mode = st.radio(
166
+ "Choose manual conversion type",
167
+ options=["Decimal -> DMS", "DMS -> Decimal"],
168
+ horizontal=True,
169
  )
170
 
171
+ if mode == "Decimal -> DMS":
172
+ col1, col2 = st.columns(2)
173
+ with col1:
174
+ latitude = st.number_input(
175
+ "Latitude (decimal)",
176
+ min_value=-90.0,
177
+ max_value=90.0,
178
+ value=0.0,
179
+ step=0.000001,
180
+ format="%.8f",
181
+ )
182
+ with col2:
183
+ longitude = st.number_input(
184
+ "Longitude (decimal)",
185
+ min_value=-180.0,
186
+ max_value=180.0,
187
+ value=0.0,
188
+ step=0.000001,
189
+ format="%.8f",
190
+ )
191
+
192
+ if st.button("Convert Decimal -> DMS", type="primary", key="manual_decimal_to_dms"):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
193
  try:
194
+ lat_dms = _decimal_to_dms_with_direction(float(latitude), "lat")
195
+ lon_dms = _decimal_to_dms_with_direction(float(longitude), "lon")
196
+ st.success("Conversion successful.")
197
+ st.write(f"Latitude (DMS): `{lat_dms}`")
198
+ st.write(f"Longitude (DMS): `{lon_dms}`")
199
+
200
+ map_df = pd.DataFrame([{"latitude": latitude, "longitude": longitude}])
201
+ _show_map(map_df, "latitude", "longitude", title="Map Visualization (manual input)")
202
+ except Exception as error:
203
+ st.error(f"Invalid input. Details: {error}")
204
+
205
+ else:
206
+ col1, col2 = st.columns(2)
207
+ with col1:
208
+ latitude_dms = st.text_input(
209
+ "Latitude (DMS)",
210
+ placeholder="Example: 13 15 6.20N",
211
+ )
212
+ with col2:
213
+ longitude_dms = st.text_input(
214
+ "Longitude (DMS)",
215
+ placeholder="Example: 2 10 5.00W",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216
  )
217
+
218
+ if st.button("Convert DMS -> Decimal", type="primary", key="manual_dms_to_decimal"):
219
+ try:
220
+ latitude = _dms_to_decimal(latitude_dms, "lat")
221
+ longitude = _dms_to_decimal(longitude_dms, "lon")
222
+ st.success("Conversion successful.")
223
+ st.write(f"Latitude (decimal): `{latitude:.10f}`")
224
+ st.write(f"Longitude (decimal): `{longitude:.10f}`")
225
+
226
+ map_df = pd.DataFrame([{"latitude": latitude, "longitude": longitude}])
227
+ _show_map(map_df, "latitude", "longitude", title="Map Visualization (manual input)")
228
+ except Exception as error:
229
+ st.error(f"Invalid DMS value. Details: {error}")
230
+
231
+
232
+ st.title("GPS Coordinate Converter")
233
+ st.write(
234
+ "Convert coordinates between Decimal and Degree-Minute-Second (DMS). "
235
+ "You can convert from an Excel file or by entering values manually."
236
+ )
237
+
238
+ tab_import, tab_manual = st.tabs(["Import Excel", "Manual Input"])
239
+ with tab_import:
240
+ _render_import_mode()
241
+ with tab_manual:
242
+ _render_manual_mode()
documentations/oml_db_tools_improvement_roadmap.md ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # OML_DB - Roadmap d'amelioration des tools
2
+
3
+ Date: 2026-02-23
4
+
5
+ ## Contexte
6
+ Cette proposition est basee sur une analyse du codebase (et non seulement du menu UI), notamment:
7
+ - `app.py`
8
+ - `apps/database_page.py`
9
+ - `utils/utils_vars.py`
10
+ - `queries/process_all_db.py`
11
+ - `apps/ciq_2g_generator.py`
12
+ - `apps/ciq_3g_generator.py`
13
+ - `apps/ciq_4g_generator.py`
14
+ - `apps/clustering.py`
15
+ - `apps/dump_compare.py`
16
+
17
+ ## Propositions cibles (specifiques au repo)
18
+ 1. Health Summary (nouveau tool)
19
+ - Ajouter une page unique qui agrege:
20
+ - qualite DB
21
+ - KPI
22
+ - checks existants
23
+ - Points de connexion:
24
+ - navigation: `app.py`
25
+ - donnees: `utils/utils_vars.py`, `utils/kpi_analysis_utils.py`
26
+
27
+ 2. Shared Dump Cache
28
+ - Parser le dump une seule fois par session et reutiliser les donnees entre pages/tools.
29
+ - Cible:
30
+ - `apps/database_page.py`
31
+ - harmonisation avec les autres pages qui relisent le meme dump.
32
+
33
+ 3. Export Job Runner + Progress
34
+ - Encapsuler les exports lourds avec:
35
+ - statut
36
+ - progression
37
+ - log utilisateur
38
+ - Cible:
39
+ - orchestration: `apps/database_page.py`
40
+ - execution: `queries/process_all_db.py`
41
+
42
+ 4. Validation Engine (fichiers en entree)
43
+ - Validation schema avant calcul:
44
+ - colonnes obligatoires
45
+ - types minimaux
46
+ - messages d'erreur explicites
47
+ - Priorite:
48
+ - `apps/clustering.py`
49
+ - `apps/dump_compare.py`
50
+
51
+ 5. CIQ Unified Generator
52
+ - Factoriser le flux commun 2G/3G/4G:
53
+ - upload
54
+ - spinner
55
+ - session state
56
+ - download
57
+ - Cible:
58
+ - `apps/ciq_2g_generator.py`
59
+ - `apps/ciq_3g_generator.py`
60
+ - `apps/ciq_4g_generator.py`
61
+
62
+ 6. Settings Persistence
63
+ - Persister les toggles metier (ex: exclusion BSC 2G decom) entre refresh/restart.
64
+ - Cible:
65
+ - `apps/database_page.py`
66
+ - `utils/utils_vars.py`
67
+
68
+ ## Priorisation impact / effort
69
+ 1. Validation Engine
70
+ - Effort: S
71
+ - Impact: Fort immediat (moins de plantages opaques)
72
+
73
+ 2. Export Job Runner + Progress
74
+ - Effort: M
75
+ - Impact: Fort UX (visibilite sur traitements longs)
76
+
77
+ 3. CIQ Unified Generator
78
+ - Effort: M
79
+ - Impact: Fort maintenance (moins de duplication)
80
+
81
+ 4. Shared Dump Cache
82
+ - Effort: M/L
83
+ - Impact: Fort perf (moins de re-parse)
84
+
85
+ 5. Health Summary
86
+ - Effort: M/L
87
+ - Impact: Fort operationnel (vision unique)
88
+
89
+ 6. Settings Persistence
90
+ - Effort: M
91
+ - Impact: Moyen/fort UX (moins de reconfiguration)
92
+
93
+ ## Plan de mise en oeuvre recommande
94
+ Phase 1 (quick wins)
95
+ - Validation Engine sur clustering + dump compare
96
+ - Progress/log minimal sur exports database
97
+
98
+ Phase 2
99
+ - Factorisation CIQ generators
100
+ - Persistence des settings critiques
101
+
102
+ Phase 3
103
+ - Shared Dump Cache transverse
104
+ - Nouvelle page Health Summary
105
+
106
+ ## Notes techniques
107
+ - Attention a l'usage de globals dans `UtilsVars`: definir clairement les regles d'invalidation quand un nouveau dump est charge.
108
+ - Ajouter une convention de cles `st.session_state` pour eviter collisions entre pages.
109
+ - Preferer des fonctions utilitaires partagees pour les patterns repetes (upload/validate/process/download).